160 lines
4.6 KiB
TypeScript
160 lines
4.6 KiB
TypeScript
import { Effect, Schema } from "effect";
|
|
|
|
const MeaningfulString = Schema.String.check(
|
|
Schema.makeFilter((value) => value.trim().length > 0, {
|
|
expected: "a non-empty string",
|
|
})
|
|
);
|
|
|
|
export const SignalId = MeaningfulString.pipe(Schema.brand("SignalId"));
|
|
export type SignalId = typeof SignalId.Type;
|
|
|
|
export const OrganizationId = MeaningfulString.pipe(
|
|
Schema.brand("OrganizationId")
|
|
);
|
|
export type OrganizationId = typeof OrganizationId.Type;
|
|
|
|
export const ProjectId = MeaningfulString.pipe(Schema.brand("ProjectId"));
|
|
export type ProjectId = typeof ProjectId.Type;
|
|
|
|
export const ConversationId = MeaningfulString.pipe(
|
|
Schema.brand("ConversationId")
|
|
);
|
|
export type ConversationId = typeof ConversationId.Type;
|
|
|
|
export const ConversationMessageId = MeaningfulString.pipe(
|
|
Schema.brand("ConversationMessageId")
|
|
);
|
|
export type ConversationMessageId = typeof ConversationMessageId.Type;
|
|
|
|
export const TimestampMs = Schema.Int.check(
|
|
Schema.isGreaterThanOrEqualTo(0)
|
|
).pipe(Schema.brand("TimestampMs"));
|
|
export type TimestampMs = typeof TimestampMs.Type;
|
|
|
|
export const OrganizationSignalScope = Schema.Struct({
|
|
_tag: Schema.Literal("Organization"),
|
|
organizationId: OrganizationId,
|
|
});
|
|
export type OrganizationSignalScope = typeof OrganizationSignalScope.Type;
|
|
|
|
export const ProjectSignalScope = Schema.Struct({
|
|
_tag: Schema.Literal("Project"),
|
|
organizationId: OrganizationId,
|
|
projectId: ProjectId,
|
|
});
|
|
export type ProjectSignalScope = typeof ProjectSignalScope.Type;
|
|
|
|
export const SignalScope = Schema.Union([
|
|
OrganizationSignalScope,
|
|
ProjectSignalScope,
|
|
]);
|
|
export type SignalScope = typeof SignalScope.Type;
|
|
|
|
export const SignalSourceMessage = Schema.Struct({
|
|
conversationId: ConversationId,
|
|
createdAt: TimestampMs,
|
|
messageId: ConversationMessageId,
|
|
rawText: Schema.String,
|
|
});
|
|
export type SignalSourceMessage = typeof SignalSourceMessage.Type;
|
|
|
|
export const ProblemStatement = Schema.Struct({
|
|
constraints: Schema.Array(MeaningfulString),
|
|
desiredOutcome: MeaningfulString,
|
|
summary: MeaningfulString,
|
|
title: MeaningfulString,
|
|
});
|
|
export type ProblemStatement = typeof ProblemStatement.Type;
|
|
|
|
export const ProcessedByAgent = Schema.Struct({
|
|
agentInstanceId: MeaningfulString,
|
|
agentName: MeaningfulString,
|
|
});
|
|
export type ProcessedByAgent = typeof ProcessedByAgent.Type;
|
|
|
|
const signalFields = {
|
|
createdAt: TimestampMs,
|
|
id: SignalId,
|
|
problemStatement: ProblemStatement,
|
|
processedBy: ProcessedByAgent,
|
|
scope: SignalScope,
|
|
sourceMessages: Schema.NonEmptyArray(SignalSourceMessage),
|
|
} as const;
|
|
|
|
export const SignalInput = Schema.Struct(signalFields);
|
|
export type SignalInput = typeof SignalInput.Type;
|
|
|
|
export const Signal = Schema.Struct(signalFields);
|
|
export type Signal = typeof Signal.Type;
|
|
|
|
export const SignalValidationReason = Schema.Literals([
|
|
"InvalidInput",
|
|
"DuplicateSource",
|
|
"MixedConversation",
|
|
"OutOfOrderSource",
|
|
]);
|
|
export type SignalValidationReason = typeof SignalValidationReason.Type;
|
|
|
|
export class SignalValidationError extends Schema.TaggedErrorClass<SignalValidationError>()(
|
|
"SignalValidationError",
|
|
{
|
|
message: Schema.String,
|
|
reason: SignalValidationReason,
|
|
}
|
|
) {}
|
|
|
|
export const composeSignal = Effect.fn("Signal.compose")(
|
|
function* composeSignal(input: unknown) {
|
|
const signal: Signal = yield* Schema.decodeUnknownEffect(SignalInput)(
|
|
input
|
|
).pipe(
|
|
Effect.mapError(
|
|
(cause) =>
|
|
new SignalValidationError({
|
|
message: cause.message,
|
|
reason: "InvalidInput",
|
|
})
|
|
)
|
|
);
|
|
|
|
const [{ conversationId, createdAt: firstCreatedAt }] =
|
|
signal.sourceMessages;
|
|
let previousCreatedAt = firstCreatedAt;
|
|
const messageIds = new Set<ConversationMessageId>();
|
|
|
|
for (const source of signal.sourceMessages) {
|
|
if (source.conversationId !== conversationId) {
|
|
return yield* Effect.fail(
|
|
new SignalValidationError({
|
|
message: "Signal sources must belong to one conversation",
|
|
reason: "MixedConversation",
|
|
})
|
|
);
|
|
}
|
|
|
|
if (messageIds.has(source.messageId)) {
|
|
return yield* Effect.fail(
|
|
new SignalValidationError({
|
|
message: "Signal sources must have unique message identities",
|
|
reason: "DuplicateSource",
|
|
})
|
|
);
|
|
}
|
|
messageIds.add(source.messageId);
|
|
|
|
if (source.createdAt < previousCreatedAt) {
|
|
return yield* Effect.fail(
|
|
new SignalValidationError({
|
|
message: "Signal source timestamps must be non-decreasing",
|
|
reason: "OutOfOrderSource",
|
|
})
|
|
);
|
|
}
|
|
previousCreatedAt = source.createdAt;
|
|
}
|
|
|
|
return signal;
|
|
}
|
|
);
|