@popcomputer/structured-chat 0.1.0 → 0.2.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +122 -108
  3. package/dist/adapters/openai-compatible-model.d.ts +4 -4
  4. package/dist/adapters/openai-compatible-model.js +33 -31
  5. package/dist/core/answer.d.ts +13 -13
  6. package/dist/core/answer.js +21 -12
  7. package/dist/core/chat.d.ts +12 -15
  8. package/dist/core/chat.js +36 -27
  9. package/dist/core/collect-stage.d.ts +11 -14
  10. package/dist/core/collect-stage.js +40 -37
  11. package/dist/core/command.d.ts +1 -1
  12. package/dist/core/command.js +1 -1
  13. package/dist/core/json-value.d.ts +1 -1
  14. package/dist/core/json-value.js +8 -1
  15. package/dist/core/model-guard.d.ts +2 -3
  16. package/dist/core/model-guard.js +4 -4
  17. package/dist/core/model.d.ts +15 -19
  18. package/dist/core/model.js +22 -10
  19. package/dist/core/protocol.d.ts +84 -79
  20. package/dist/core/protocol.js +18 -12
  21. package/dist/core/question.js +17 -15
  22. package/dist/core/repair.js +1 -1
  23. package/dist/core/session.d.ts +22 -28
  24. package/dist/core/session.js +16 -7
  25. package/dist/core/stage-name.d.ts +1 -1
  26. package/dist/core/stage-name.js +1 -1
  27. package/dist/core/stage.d.ts +4 -4
  28. package/dist/core/stage.js +11 -8
  29. package/dist/core/tool-set.js +6 -6
  30. package/dist/core/tool.d.ts +36 -39
  31. package/dist/core/tool.js +58 -31
  32. package/dist/core/view.d.ts +64 -39
  33. package/dist/core/view.js +12 -15
  34. package/dist/integrations/assistant-ui.js +21 -14
  35. package/dist/testing/in-memory-session-store.js +1 -1
  36. package/dist/testing/scenario.js +6 -6
  37. package/examples/answer-modes.ts +60 -49
  38. package/examples/prompt-injection-policy.ts +20 -20
  39. package/examples/resource-search.ts +104 -0
  40. package/package.json +11 -3
  41. package/examples/agency-search.ts +0 -101
@@ -1,7 +1,11 @@
1
1
  import { Effect, Schema } from "effect";
2
2
  /** How strongly a collect-stage answer must be grounded in user messages. */
3
- export const AnswerModeSchema = Schema.Literal("semantic", "explicit", "confirmed");
4
- const AnswerDescriptionSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(1_000));
3
+ export const AnswerModeSchema = Schema.Literals([
4
+ "semantic",
5
+ "explicit",
6
+ "confirmed",
7
+ ]);
8
+ const AnswerDescriptionSchema = Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(1_000));
5
9
  const defineAnswer = (mode, schema, input) => {
6
10
  if (input.ask._tag === "ChoiceQuestion") {
7
11
  for (const option of input.ask.options) {
@@ -27,18 +31,23 @@ const defineAnswer = (mode, schema, input) => {
27
31
  description: Schema.decodeSync(AnswerDescriptionSchema)(input.description),
28
32
  question: input.ask,
29
33
  };
30
- const withEscape = input.escape === undefined
31
- ? base
32
- : {
33
- ...base,
34
- escape: {
35
- value: Schema.validateSync(schema)(input.escape.value),
36
- },
37
- };
34
+ if (input.escape === undefined) {
35
+ return input.validate === undefined
36
+ ? base
37
+ : {
38
+ ...base,
39
+ validate: input.validate,
40
+ reject: input.reject,
41
+ };
42
+ }
43
+ const escape = {
44
+ value: Schema.decodeSync(Schema.toType(schema))(input.escape.value),
45
+ };
38
46
  return input.validate === undefined
39
- ? withEscape
47
+ ? { ...base, escape }
40
48
  : {
41
- ...withEscape,
49
+ ...base,
50
+ escape,
42
51
  validate: input.validate,
43
52
  reject: input.reject,
44
53
  };
@@ -1,5 +1,4 @@
1
1
  import { Effect, Schema } from "effect";
2
- import type * as ParseResult from "effect/ParseResult";
3
2
  import type { AcceptedAnswer, CollectAnswers, CollectStage, CollectStageDefinitionContract, CollectStagePrompt, CollectStageState } from "./collect-stage.js";
4
3
  import { type UntrustedMessage } from "./model.js";
5
4
  import { type CommandStage, type CommandStageDefinitionContract, type ToolStage, type ToolStageDefinitionContract } from "./stage.js";
@@ -7,17 +6,15 @@ import type { ToolSetExecution } from "./tool-set.js";
7
6
  import type { StandardRepair } from "./repair.js";
8
7
  import { ChatSessionConflict, ChatSessionStore, InvalidChatSession, type ChatSessionStoreUnavailable } from "./session.js";
9
8
  /** Stable machine-facing name for one structured chat definition. */
10
- export declare const ChatNameSchema: Schema.filter<Schema.filter<typeof Schema.NonEmptyTrimmedString>>;
9
+ export declare const ChatNameSchema: Schema.Trimmed;
11
10
  /** Positive persisted-state version for one structured chat definition. */
12
- export declare const ChatVersionSchema: Schema.filter<Schema.filter<typeof Schema.Number>>;
11
+ export declare const ChatVersionSchema: Schema.Number;
13
12
  /** Safe reason that a server-owned chat transition was rejected. */
14
- export declare const InvalidChatTransitionReasonSchema: Schema.Literal<["already_complete", "invalid_state"]>;
15
- declare const InvalidChatTransition_base: Schema.TaggedErrorClass<InvalidChatTransition, "InvalidChatTransition", {
16
- readonly _tag: Schema.tag<"InvalidChatTransition">;
17
- } & {
18
- chat: Schema.filter<Schema.filter<typeof Schema.NonEmptyTrimmedString>>;
19
- reason: Schema.Literal<["already_complete", "invalid_state"]>;
20
- }>;
13
+ export declare const InvalidChatTransitionReasonSchema: Schema.Literals<readonly ["already_complete", "invalid_state"]>;
14
+ declare const InvalidChatTransition_base: Schema.Class<InvalidChatTransition, Schema.TaggedStruct<"InvalidChatTransition", {
15
+ readonly chat: Schema.Trimmed;
16
+ readonly reason: Schema.Literals<readonly ["already_complete", "invalid_state"]>;
17
+ }>, import("effect/Cause").YieldableError>;
21
18
  /** A server-owned chat state cannot perform the requested transition. */
22
19
  export declare class InvalidChatTransition extends InvalidChatTransition_base {
23
20
  }
@@ -50,12 +47,12 @@ export interface ChatState<Name extends string, Version extends number, Stages e
50
47
  };
51
48
  }
52
49
  type ChatQuestion<Stage> = Stage extends CollectStage<infer _Name, infer Fields, infer _Guards> ? CollectStagePrompt<Fields> : never;
53
- type ChatToolExecution<Stage> = Stage extends ToolStage<infer _Name, infer Tools, infer _Guards> ? ToolSetExecution<Tools> : Stage extends CommandStage<infer _Name, infer _Command, infer _Guards> ? Extract<Effect.Effect.Success<ReturnType<Stage["run"]>>, object> : never;
50
+ type ChatToolExecution<Stage> = Stage extends ToolStage<infer _Name, infer Tools, infer _Guards> ? ToolSetExecution<Tools> : Stage extends CommandStage<infer _Name, infer _Command, infer _Guards> ? Extract<Effect.Success<ReturnType<Stage["run"]>>, object> : never;
54
51
  type StageEffect<Stage> = Stage extends CollectStage<infer _CollectName, infer _Fields, infer _CollectGuards> ? ReturnType<Stage["run"]> : Stage extends ToolStage<infer _ToolName, infer _Tools, infer _ToolGuards> ? ReturnType<Stage["run"]> : Stage extends CommandStage<infer _CommandName, infer _Command, infer _CommandGuards> ? ReturnType<Stage["run"]> : never;
55
52
  /** Failure union produced by any stage in one chat. */
56
- export type ChatError<Stages extends ChatStageTuple> = InvalidChatTransition | Effect.Effect.Error<StageEffect<Stages[number]>>;
53
+ export type ChatError<Stages extends ChatStageTuple> = InvalidChatTransition | Effect.Error<StageEffect<Stages[number]>>;
57
54
  /** Effect service union required by any stage in one chat. */
58
- export type ChatRequirements<Stages extends ChatStageTuple> = Effect.Effect.Context<StageEffect<Stages[number]>>;
55
+ export type ChatRequirements<Stages extends ChatStageTuple> = Effect.Services<StageEffect<Stages[number]>>;
59
56
  /** Question, ongoing tool result, or terminal result emitted by one turn. */
60
57
  export type ChatTurn<Name extends string, Version extends number, Stages extends ChatStageTuple> = {
61
58
  readonly _tag: "Question";
@@ -100,12 +97,12 @@ export interface ChatDefinition<Name extends string, Version extends number, Sta
100
97
  readonly version: Version;
101
98
  readonly stages: Stages;
102
99
  readonly repair: StandardRepair | undefined;
103
- readonly stateSchema: Schema.Schema<ChatState<Name, Version, Stages>, unknown, never>;
100
+ readonly stateSchema: Schema.Codec<ChatState<Name, Version, Stages>, unknown>;
104
101
  readonly initialState: ChatState<Name, Version, Stages>;
105
102
  /** Read one accepted value together with its supporting transcript data. */
106
103
  readonly getAcceptedAnswer: <Stage extends ChatCollectStage<Stages>, Field extends keyof CollectFields<Stage> & string>(state: ChatState<Name, Version, Stages>, stage: Stage, field: Field) => AcceptedAnswer<CollectAnswers<CollectFields<Stage>>[Field]> | undefined;
107
104
  /** Strictly parse persisted server-owned chat state. */
108
- readonly parseState: (input: Schema.Schema.Encoded<Schema.Schema<ChatState<Name, Version, Stages>, unknown, never>>) => Effect.Effect<ChatState<Name, Version, Stages>, ParseResult.ParseError>;
105
+ readonly parseState: (input: Schema.Codec.Encoded<Schema.Codec<ChatState<Name, Version, Stages>, unknown>>) => Effect.Effect<ChatState<Name, Version, Stages>, Schema.SchemaError>;
109
106
  /** Run the active stage and any immediately reachable tool stage. */
110
107
  readonly run: (input: {
111
108
  readonly state: ChatState<Name, Version, Stages>;
package/dist/core/chat.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Effect, Schema, unsafeCoerce } from "effect";
1
+ import { cast, Effect, Schema } from "effect";
2
2
  import { readCollectStageRuntime } from "./collect-stage.js";
3
3
  import { countUntrustedMessageCharacters, UntrustedMessageSchema, } from "./model.js";
4
4
  import { readCommandStageRuntime, readToolStageRuntime, } from "./stage.js";
@@ -7,11 +7,14 @@ import { deriveCommandId } from "./command.js";
7
7
  import { defineTool } from "./tool.js";
8
8
  import { ChatSessionConflict, ChatSessionIdSchema, ChatSessionNamespaceSchema, ChatSessionReplacementSchema, ChatSessionRevisionSchema, ChatSessionSnapshotSchema, ChatSessionStore, InvalidChatSession, } from "./session.js";
9
9
  /** Stable machine-facing name for one structured chat definition. */
10
- export const ChatNameSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(100), Schema.pattern(/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/));
10
+ export const ChatNameSchema = Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(100), Schema.isPattern(/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/));
11
11
  /** Positive persisted-state version for one structured chat definition. */
12
- export const ChatVersionSchema = Schema.Number.pipe(Schema.int(), Schema.between(1, 2_147_483_647));
12
+ export const ChatVersionSchema = Schema.Number.check(Schema.isInt(), Schema.isBetween({ minimum: 1, maximum: 2_147_483_647 }));
13
13
  /** Safe reason that a server-owned chat transition was rejected. */
14
- export const InvalidChatTransitionReasonSchema = Schema.Literal("already_complete", "invalid_state");
14
+ export const InvalidChatTransitionReasonSchema = Schema.Literals([
15
+ "already_complete",
16
+ "invalid_state",
17
+ ]);
15
18
  /** A server-owned chat state cannot perform the requested transition. */
16
19
  export class InvalidChatTransition extends Schema.TaggedError()("InvalidChatTransition", {
17
20
  chat: ChatNameSchema,
@@ -90,9 +93,9 @@ export const defineChat = (definition) => {
90
93
  }
91
94
  const correctionSchema = remainingSchemas.length === 0
92
95
  ? firstSchema
93
- : Schema.Union(firstSchema, ...remainingSchemas);
96
+ : Schema.Union([firstSchema, ...remainingSchemas]);
94
97
  const input = Schema.Struct({
95
- corrections: Schema.NonEmptyArray(correctionSchema).pipe(Schema.maxItems(repair.maximumCorrections)),
98
+ corrections: Schema.NonEmptyArray(correctionSchema).check(Schema.isMaxLength(repair.maximumCorrections)),
96
99
  });
97
100
  return defineTool({
98
101
  name: repairToolName,
@@ -164,8 +167,11 @@ export const defineChat = (definition) => {
164
167
  const baseStateFields = {
165
168
  schemaVersion: Schema.Literal(definition.version),
166
169
  chat: Schema.Literal(definition.name),
167
- stage: Schema.Number.pipe(Schema.int(), Schema.between(0, definition.stages.length - 1)),
168
- status: Schema.Literal("active", "complete"),
170
+ stage: Schema.Number.check(Schema.isInt(), Schema.isBetween({
171
+ minimum: 0,
172
+ maximum: definition.stages.length - 1,
173
+ })),
174
+ status: Schema.Literals(["active", "complete"]),
169
175
  stages: Schema.Struct(stateFields),
170
176
  };
171
177
  const rawStateSchema = repair === undefined
@@ -173,18 +179,21 @@ export const defineChat = (definition) => {
173
179
  : Schema.Struct({
174
180
  ...baseStateFields,
175
181
  repair: Schema.Struct({
176
- pendingStages: Schema.Array(Schema.Number.pipe(Schema.int(), Schema.between(0, finalStageIndex - 1))).pipe(Schema.maxItems(20)),
182
+ pendingStages: Schema.Array(Schema.Number.check(Schema.isInt(), Schema.isBetween({
183
+ minimum: 0,
184
+ maximum: finalStageIndex - 1,
185
+ }))).check(Schema.isMaxLength(20)),
177
186
  }),
178
187
  });
179
188
  // SAFETY: the conditional repair field is erased only for applying the
180
189
  // shared semantic predicate; stateSchema below restores the public type.
181
- const runtimeStateSchema = unsafeCoerce(rawStateSchema);
182
- const refinedStateSchema = runtimeStateSchema.pipe(Schema.filter((state) => isValidRuntimeState(unsafeCoerce(state)), {
190
+ const runtimeStateSchema = cast(rawStateSchema);
191
+ const refinedStateSchema = runtimeStateSchema.check(Schema.makeFilter((state) => isValidRuntimeState(cast(state)), {
183
192
  description: "semantically valid structured-chat state",
184
193
  }));
185
194
  // SAFETY: stage state fields are taken directly from the concrete collect
186
195
  // stages, and the remaining envelope fields are exact literals or bounds.
187
- const stateSchema = unsafeCoerce(refinedStateSchema);
196
+ const stateSchema = cast(refinedStateSchema);
188
197
  const baseInitialState = {
189
198
  schemaVersion: definition.version,
190
199
  chat: definition.name,
@@ -195,7 +204,7 @@ export const defineChat = (definition) => {
195
204
  const initialStateInput = repair === undefined
196
205
  ? baseInitialState
197
206
  : { ...baseInitialState, repair: { pendingStages: [] } };
198
- const initialState = Schema.validateSync(stateSchema)(initialStateInput);
207
+ const initialState = Schema.decodeUnknownSync(Schema.toType(stateSchema))(initialStateInput);
199
208
  const isGroundedInMessages = (state, messages) => definition.stages.every((stage) => {
200
209
  if (stage._tag !== "CollectStage") {
201
210
  return true;
@@ -279,7 +288,7 @@ export const defineChat = (definition) => {
279
288
  }
280
289
  // SAFETY: planWith used the generated repair tool schema, and this
281
290
  // branch is selected by that tool's unique literal name.
282
- const proposal = unsafeCoerce(call.arguments);
291
+ const proposal = cast(call.arguments);
283
292
  return applyConversationRepairs(state, messages, proposal.corrections).pipe(Effect.flatMap((repairedState) => Effect.suspend(() => runTrustedRuntime(repairedState, messages, commandContext, false))));
284
293
  }));
285
294
  }
@@ -348,14 +357,14 @@ export const defineChat = (definition) => {
348
357
  const run = (input) => {
349
358
  // SAFETY: ChatState is generated from the same stage tuple as the sealed
350
359
  // runtime state contract; only generic correlations are erased here.
351
- const runtimeState = unsafeCoerce(input.state);
360
+ const runtimeState = cast(input.state);
352
361
  const runtime = runCheckedRuntime(runtimeState, input.messages);
353
362
  // SAFETY: runtime dispatch follows the exact Stages tuple and each stage
354
363
  // retains its own parsing, errors, dependencies, and output constructor.
355
- return unsafeCoerce(runtime);
364
+ return cast(runtime);
356
365
  };
357
366
  const reply = (input) => Effect.gen(function* () {
358
- const parsedInput = yield* Schema.decodeUnknown(ChatReplyBoundaryInputSchema)(input, { onExcessProperty: "error" }).pipe(Effect.mapError(() => invalidSession("invalid_input")));
367
+ const parsedInput = yield* Schema.decodeUnknownEffect(ChatReplyBoundaryInputSchema)(input, { onExcessProperty: "error" }).pipe(Effect.mapError(() => invalidSession("invalid_input")));
359
368
  const store = yield* ChatSessionStore;
360
369
  const scope = {
361
370
  namespace: parsedInput.namespace ?? "",
@@ -371,7 +380,7 @@ export const defineChat = (definition) => {
371
380
  }));
372
381
  const snapshot = loaded === null
373
382
  ? null
374
- : yield* Schema.decodeUnknown(ChatSessionSnapshotSchema)(loaded, { onExcessProperty: "error" }).pipe(Effect.mapError(() => invalidSession("invalid_snapshot")));
383
+ : yield* Schema.decodeUnknownEffect(ChatSessionSnapshotSchema)(loaded, { onExcessProperty: "error" }).pipe(Effect.mapError(() => invalidSession("invalid_snapshot")));
375
384
  if ((snapshot === null &&
376
385
  parsedInput.expectedRevision !== undefined) ||
377
386
  (snapshot !== null &&
@@ -380,12 +389,12 @@ export const defineChat = (definition) => {
380
389
  }
381
390
  const state = snapshot === null
382
391
  ? initialState
383
- : yield* Schema.decodeUnknown(stateSchema)(snapshot.state, {
392
+ : yield* Schema.decodeUnknownEffect(stateSchema)(snapshot.state, {
384
393
  onExcessProperty: "error",
385
394
  }).pipe(Effect.mapError(() => invalidSession("invalid_state")));
386
395
  const previousMessages = snapshot?.messages ?? [];
387
396
  // SAFETY: stateSchema decoded this definition's exact state envelope.
388
- const runtimeState = unsafeCoerce(state);
397
+ const runtimeState = cast(state);
389
398
  if (!isGroundedInMessages(runtimeState, previousMessages)) {
390
399
  return yield* Effect.fail(invalidSession("invalid_state"));
391
400
  }
@@ -393,7 +402,7 @@ export const defineChat = (definition) => {
393
402
  maximumPersistedMessages) {
394
403
  return yield* Effect.fail(invalidSession("history_limit"));
395
404
  }
396
- const userMessage = yield* Schema.decodeUnknown(UntrustedMessageSchema)({ role: "user", content: parsedInput.message }, { onExcessProperty: "error" }).pipe(Effect.mapError(() => invalidSession("invalid_input")));
405
+ const userMessage = yield* Schema.decodeUnknownEffect(UntrustedMessageSchema)({ role: "user", content: parsedInput.message }, { onExcessProperty: "error" }).pipe(Effect.mapError(() => invalidSession("invalid_input")));
397
406
  const messages = [...previousMessages, userMessage];
398
407
  const commandContext = finalStage?._tag === "CommandStage"
399
408
  ? {
@@ -414,7 +423,7 @@ export const defineChat = (definition) => {
414
423
  snapshot !== null &&
415
424
  state.status === "active" &&
416
425
  state.stage === finalStageIndex);
417
- const turn = yield* unsafeCoerce(trustedTurn);
426
+ const turn = yield* cast(trustedTurn);
418
427
  const toolModelContext = turn._tag === "Question"
419
428
  ? undefined
420
429
  : readToolExecutionModelContext(turn.result);
@@ -438,7 +447,7 @@ export const defineChat = (definition) => {
438
447
  if (persistedMessages.length > maximumPersistedMessages) {
439
448
  return yield* Effect.fail(invalidSession("history_limit"));
440
449
  }
441
- const encodedState = yield* Schema.encodeUnknown(stateSchema)(turn.state, { onExcessProperty: "error" }).pipe(Effect.mapError(() => invalidSession("invalid_state")));
450
+ const encodedState = yield* Schema.encodeUnknownEffect(stateSchema)(turn.state, { onExcessProperty: "error" }).pipe(Effect.mapError(() => invalidSession("invalid_state")));
442
451
  const replaced = yield* store
443
452
  .replace({
444
453
  ...scope,
@@ -456,7 +465,7 @@ export const defineChat = (definition) => {
456
465
  status: turn.state.status,
457
466
  },
458
467
  }));
459
- const replacement = yield* Schema.decodeUnknown(ChatSessionReplacementSchema)(replaced, { onExcessProperty: "error" }).pipe(Effect.mapError(() => invalidSession("invalid_replacement")));
468
+ const replacement = yield* Schema.decodeUnknownEffect(ChatSessionReplacementSchema)(replaced, { onExcessProperty: "error" }).pipe(Effect.mapError(() => invalidSession("invalid_replacement")));
460
469
  return {
461
470
  revision: replacement.revision,
462
471
  turn,
@@ -477,11 +486,11 @@ export const defineChat = (definition) => {
477
486
  }
478
487
  // SAFETY: Stage is restricted to this chat's concrete collect stages,
479
488
  // Field is restricted to its field keys, and state uses the same tuple.
480
- const runtimeState = unsafeCoerce(state);
489
+ const runtimeState = cast(state);
481
490
  const accepted = runtimeState.stages[stage.name]?.accepted[field];
482
- return unsafeCoerce(accepted);
491
+ return cast(accepted);
483
492
  },
484
- parseState: (input) => Schema.decodeUnknown(stateSchema)(input, {
493
+ parseState: (input) => Schema.decodeUnknownEffect(stateSchema)(input, {
485
494
  onExcessProperty: "error",
486
495
  }),
487
496
  run,
@@ -1,5 +1,4 @@
1
1
  import { Effect, Schema } from "effect";
2
- import type * as ParseResult from "effect/ParseResult";
3
2
  import type { AnswerDefinition, AnswerDefinitionContract, AnswerMode } from "./answer.js";
4
3
  import { ChatModelUnavailable, StructuredChatModel, type UnsupportedModelToolSchema, type UntrustedMessage } from "./model.js";
5
4
  import type { ModelGuardError, ModelGuardRequirements, ModelGuardTuple } from "./model-guard.js";
@@ -7,13 +6,11 @@ import { InvalidToolCall, type InvalidToolProjection } from "./tool.js";
7
6
  import type { AdaptiveChoiceQuestion, ChoiceQuestion, QuestionChoice } from "./question.js";
8
7
  import { type StructuredDefinition } from "./definition.js";
9
8
  /** Safe reason that a collect-stage model proposal was rejected. */
10
- export declare const InvalidCollectStageResponseReasonSchema: Schema.Literal<["invalid_evidence", "invalid_repair"]>;
11
- declare const InvalidCollectStageResponse_base: Schema.TaggedErrorClass<InvalidCollectStageResponse, "InvalidCollectStageResponse", {
12
- readonly _tag: Schema.tag<"InvalidCollectStageResponse">;
13
- } & {
14
- stage: Schema.filter<Schema.filter<typeof Schema.NonEmptyTrimmedString>>;
15
- reason: Schema.Literal<["invalid_evidence", "invalid_repair"]>;
16
- }>;
9
+ export declare const InvalidCollectStageResponseReasonSchema: Schema.Literals<readonly ["invalid_evidence", "invalid_repair"]>;
10
+ declare const InvalidCollectStageResponse_base: Schema.Class<InvalidCollectStageResponse, Schema.TaggedStruct<"InvalidCollectStageResponse", {
11
+ readonly stage: Schema.Trimmed;
12
+ readonly reason: Schema.Literals<readonly ["invalid_evidence", "invalid_repair"]>;
13
+ }>, import("effect/Cause").YieldableError>;
17
14
  /** A collect-stage proposal was not grounded in a user message. */
18
15
  export declare class InvalidCollectStageResponse extends InvalidCollectStageResponse_base {
19
16
  }
@@ -104,7 +101,7 @@ export interface CollectStageTurn<Fields extends AnswerFields> {
104
101
  readonly complete: boolean;
105
102
  readonly question: CollectStagePrompt<Fields> | undefined;
106
103
  }
107
- type RuntimeAnswerValue = Schema.Schema.Type<Schema.Schema.AnyNoContext>;
104
+ type RuntimeAnswerValue = Schema.Schema.Type<Schema.Codec<unknown, unknown>>;
108
105
  type RuntimeAcceptedAnswer = AcceptedAnswer<RuntimeAnswerValue>;
109
106
  interface RuntimeCollectStageState {
110
107
  readonly accepted: Readonly<Partial<Record<string, RuntimeAcceptedAnswer>>>;
@@ -139,12 +136,12 @@ interface RuntimeCollectRepairResult {
139
136
  /** @internal Erased collect-stage behavior consumed by the chat runtime. */
140
137
  export interface CollectStageRuntime {
141
138
  readonly initialState: RuntimeCollectStageState;
142
- readonly stateSchema: Schema.Schema.AnyNoContext;
139
+ readonly stateSchema: Schema.Codec<unknown, unknown>;
143
140
  readonly isInitial: (state: RuntimeCollectStageState) => boolean;
144
141
  readonly isValid: (state: RuntimeCollectStageState) => boolean;
145
142
  readonly isGroundedInMessages: (state: RuntimeCollectStageState, messages: ReadonlyArray<UntrustedMessage>) => boolean;
146
143
  readonly isComplete: (state: RuntimeCollectStageState) => boolean;
147
- readonly repairSchema: Schema.Schema.AnyNoContext;
144
+ readonly repairSchema: Schema.Codec<unknown, unknown>;
148
145
  readonly applyRepairs: (state: RuntimeCollectStageState, messages: ReadonlyArray<UntrustedMessage>, repairs: ReadonlyArray<RuntimeCollectRepair>) => Effect.Effect<RuntimeCollectRepairResult, unknown, unknown>;
149
146
  readonly run: (input: {
150
147
  readonly state: RuntimeCollectStageState;
@@ -180,12 +177,12 @@ export interface CollectStage<Name extends string, Fields extends AnswerFields,
180
177
  readonly name: Name;
181
178
  readonly fields: Fields;
182
179
  readonly questions: CollectQuestionPolicy;
183
- readonly answersSchema: Schema.Schema<CollectAnswers<Fields>, unknown, never>;
184
- readonly stateSchema: Schema.Schema<CollectStageState<Fields>, unknown, never>;
180
+ readonly answersSchema: Schema.Codec<CollectAnswers<Fields>, unknown>;
181
+ readonly stateSchema: Schema.Codec<CollectStageState<Fields>, unknown>;
185
182
  readonly initialState: CollectStageState<Fields>;
186
183
  readonly guards: Guards;
187
184
  /** Strictly parse persisted or client-returned stage state. */
188
- readonly parseState: (input: Schema.Schema.Encoded<Schema.Schema<CollectStageState<Fields>, unknown, never>>) => Effect.Effect<CollectStageState<Fields>, ParseResult.ParseError>;
185
+ readonly parseState: (input: Schema.Codec.Encoded<Schema.Codec<CollectStageState<Fields>, unknown>>) => Effect.Effect<CollectStageState<Fields>, Schema.SchemaError>;
189
186
  /** Test whether every schema-defined answer has been populated. */
190
187
  readonly isComplete: (state: CollectStageState<Fields>) => boolean;
191
188
  /** Select the first missing question in schema declaration order. */
@@ -1,11 +1,14 @@
1
- import { Data, Effect, Either, Schema, unsafeCoerce } from "effect";
1
+ import { cast, Data, Effect, Result, Schema, Struct } from "effect";
2
2
  import { StageNameSchema } from "./stage-name.js";
3
3
  import { ChatModelUnavailable, Instruction, runToolStep, StructuredChatModel, } from "./model.js";
4
4
  import { defineTool, InvalidToolCall, } from "./tool.js";
5
5
  import { defineToolSet } from "./tool-set.js";
6
6
  import { structuredDefinition, } from "./definition.js";
7
7
  /** Safe reason that a collect-stage model proposal was rejected. */
8
- export const InvalidCollectStageResponseReasonSchema = Schema.Literal("invalid_evidence", "invalid_repair");
8
+ export const InvalidCollectStageResponseReasonSchema = Schema.Literals([
9
+ "invalid_evidence",
10
+ "invalid_repair",
11
+ ]);
9
12
  /** A collect-stage proposal was not grounded in a user message. */
10
13
  export class InvalidCollectStageResponse extends Schema.TaggedError()("InvalidCollectStageResponse", {
11
14
  stage: StageNameSchema,
@@ -22,8 +25,8 @@ const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
22
25
  /** Define one deterministic schema-derived fact collection stage. */
23
26
  export const defineCollectStage = (definition) => {
24
27
  Schema.decodeSync(StageNameSchema)(definition.name);
25
- const questionGuidanceSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(2_000));
26
- const questionEscapeSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(100));
28
+ const questionGuidanceSchema = Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(2_000));
29
+ const questionEscapeSchema = Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(100));
27
30
  const questionPolicyBuilder = {};
28
31
  if (definition.questions?.guidance !== undefined) {
29
32
  questionPolicyBuilder.guidance = Schema.decodeSync(questionGuidanceSchema)(definition.questions.guidance);
@@ -34,7 +37,7 @@ export const defineCollectStage = (definition) => {
34
37
  const questions = questionPolicyBuilder;
35
38
  // SAFETY: definition.fields is the exact Fields mapping; Object.keys returns
36
39
  // only its enumerable string keys.
37
- const fieldNames = unsafeCoerce(Object.keys(definition.fields));
40
+ const fieldNames = cast(Object.keys(definition.fields));
38
41
  if (fieldNames.length === 0) {
39
42
  throw new Error("Collect stages require at least one answer field");
40
43
  }
@@ -53,8 +56,8 @@ export const defineCollectStage = (definition) => {
53
56
  if (firstField === undefined) {
54
57
  throw new Error("Collect stages require at least one answer field");
55
58
  }
56
- const fieldSchema = Schema.Literal(firstField, ...remainingFields);
57
- const messageIndexSchema = Schema.Number.pipe(Schema.int(), Schema.between(0, 1_000_000));
59
+ const fieldSchema = Schema.Literals([firstField, ...remainingFields]);
60
+ const messageIndexSchema = Schema.Number.check(Schema.isInt(), Schema.isBetween({ minimum: 0, maximum: 1_000_000 }));
58
61
  const getAnswer = (field) => {
59
62
  const answer = definition.fields[field];
60
63
  if (answer === undefined) {
@@ -91,10 +94,10 @@ export const defineCollectStage = (definition) => {
91
94
  }
92
95
  const answerSchemaEntries = fieldNames.map((field) => [field, getAnswer(field).schema]);
93
96
  // SAFETY: every entry uses one exact Fields key and its corresponding schema.
94
- const answerSchemas = unsafeCoerce(Object.fromEntries(answerSchemaEntries));
97
+ const answerSchemas = cast(Object.fromEntries(answerSchemaEntries));
95
98
  const rawAnswersSchema = Schema.Struct(answerSchemas);
96
- const evidenceQuoteSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(2_000));
97
- const questionTextSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(500));
99
+ const evidenceQuoteSchema = Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(2_000));
100
+ const questionTextSchema = Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(500));
98
101
  const acceptedEvidenceSchema = Schema.Struct({
99
102
  messageIndex: messageIndexSchema,
100
103
  quote: evidenceQuoteSchema,
@@ -126,10 +129,10 @@ export const defineCollectStage = (definition) => {
126
129
  }
127
130
  const rawRepairSchema = remainingRepairSchemas.length === 0
128
131
  ? firstRepairSchema
129
- : Schema.Union(firstRepairSchema, ...remainingRepairSchemas);
132
+ : Schema.Union([firstRepairSchema, ...remainingRepairSchemas]);
130
133
  // SAFETY: every dynamically generated member uses only AnyNoContext field
131
134
  // schemas and exact stage, field, and transition literals.
132
- const repairSchema = rawRepairSchema;
135
+ const repairSchema = cast(rawRepairSchema);
133
136
  const acceptedFields = Object.fromEntries(fieldNames.map((field) => [
134
137
  field,
135
138
  Schema.Struct({
@@ -145,8 +148,8 @@ export const defineCollectStage = (definition) => {
145
148
  }),
146
149
  ]));
147
150
  const rawStateSchema = Schema.Struct({
148
- accepted: Schema.partial(Schema.Struct(acceptedFields)),
149
- asked: Schema.partial(Schema.Struct(askedFields)),
151
+ accepted: Schema.Struct(acceptedFields).mapFields(Struct.map(Schema.optional)),
152
+ asked: Schema.Struct(askedFields).mapFields(Struct.map(Schema.optional)),
150
153
  });
151
154
  const isValidState = (state) => {
152
155
  return fieldNames.every((field) => {
@@ -156,49 +159,49 @@ export const defineCollectStage = (definition) => {
156
159
  hasOwn(state.asked, field));
157
160
  });
158
161
  };
159
- const refinedStateSchema = rawStateSchema.pipe(Schema.filter(isValidState, {
162
+ const refinedStateSchema = rawStateSchema.check(Schema.makeFilter(isValidState, {
160
163
  description: "semantically valid collect-stage state",
161
164
  }));
162
165
  // SAFETY: rawAnswersSchema is created from every field's exact schema.
163
- const answersSchema = unsafeCoerce(rawAnswersSchema);
166
+ const answersSchema = cast(rawAnswersSchema);
164
167
  // SAFETY: partial preserves the mapped accepted-answer types, while asked is
165
168
  // a record whose keys are restricted to the exact field literal union.
166
- const stateSchema = unsafeCoerce(refinedStateSchema);
167
- const initialState = Schema.validateSync(stateSchema)({
169
+ const stateSchema = cast(refinedStateSchema);
170
+ const initialState = Schema.decodeSync(Schema.toType(stateSchema))({
168
171
  accepted: {},
169
172
  asked: {},
170
173
  });
171
174
  // SAFETY: when guards are omitted, Guards uses its readonly [] default; an
172
175
  // explicitly supplied tuple is returned unchanged.
173
- const guards = definition.guards ?? unsafeCoerce([]);
176
+ const guards = definition.guards ?? cast([]);
174
177
  // SAFETY: every entry is built from one registered AnyNoContext answer
175
178
  // schema and adds only the model-wire null representation for absence.
176
179
  const proposalAnswerSchemaEntries = fieldNames.map((field) => {
177
180
  const answer = getAnswer(field);
178
181
  return [
179
182
  field,
180
- Schema.NullOr(answer.schema).annotations({
183
+ Schema.NullOr(answer.schema).annotate({
181
184
  description: `${answer.mode}: ${answer.description}`,
182
185
  }),
183
186
  ];
184
187
  });
185
188
  // SAFETY: each entry contains one registered field and its no-context schema.
186
- const proposalAnswerSchemas = unsafeCoerce(Object.fromEntries(proposalAnswerSchemaEntries));
189
+ const proposalAnswerSchemas = cast(Object.fromEntries(proposalAnswerSchemaEntries));
187
190
  const rawProposalSchema = Schema.Struct({
188
191
  answers: Schema.Struct(proposalAnswerSchemas),
189
192
  evidence: Schema.Array(Schema.Struct({
190
193
  field: fieldSchema,
191
194
  quote: evidenceQuoteSchema,
192
- })).pipe(Schema.maxItems(fieldNames.length)),
195
+ })).check(Schema.isMaxLength(fieldNames.length)),
193
196
  nextQuestion: Schema.NullOr(Schema.Struct({
194
197
  field: fieldSchema,
195
198
  text: questionTextSchema,
196
- options: Schema.Array(Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(100))).pipe(Schema.maxItems(20)),
199
+ options: Schema.Array(Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(100))).check(Schema.isMaxLength(20)),
197
200
  })),
198
201
  });
199
202
  // SAFETY: every answer field schema is constrained to AnyNoContext; the
200
203
  // generic mapped Struct cannot prove that fact after Object.fromEntries.
201
- const ProposalSchema = rawProposalSchema;
204
+ const ProposalSchema = cast(rawProposalSchema);
202
205
  const submitAnswers = defineTool({
203
206
  name: "submit_answers",
204
207
  description: "Submit grounded answers from the conversation and optionally phrase the next adaptive question.",
@@ -314,11 +317,11 @@ export const defineCollectStage = (definition) => {
314
317
  // A selected label is later submitted as this answer's wire value,
315
318
  // so model-authored labels that cannot decode would dead-end the
316
319
  // user; fall back to the application-authored options instead.
317
- const decodeLabel = Schema.decodeUnknownEither(getAnswer(pending.field).schema);
320
+ const decodeLabel = Schema.decodeUnknownResult(getAnswer(pending.field).schema);
318
321
  const validOptions = supplied.length < question.minimumOptions ||
319
322
  supplied.length > question.maximumOptions ||
320
323
  new Set(normalized).size !== normalized.length ||
321
- supplied.some(({ label }) => Either.isLeft(decodeLabel(label)))
324
+ supplied.some(({ label }) => Result.isFailure(decodeLabel(label)))
322
325
  ? undefined
323
326
  : supplied;
324
327
  const selectedOptions = validOptions ??
@@ -339,8 +342,8 @@ export const defineCollectStage = (definition) => {
339
342
  options,
340
343
  };
341
344
  return questions.escape === undefined
342
- ? unsafeCoerce(prompt)
343
- : unsafeCoerce({ ...prompt, escape: { label: questions.escape } });
345
+ ? cast(prompt)
346
+ : cast({ ...prompt, escape: { label: questions.escape } });
344
347
  };
345
348
  const askPendingQuestion = (state, messages, adaptive) => {
346
349
  const pending = nextQuestion(state);
@@ -385,8 +388,8 @@ export const defineCollectStage = (definition) => {
385
388
  options: question._tag === "ChoiceQuestion" ? question.options : [],
386
389
  };
387
390
  return questions.escape === undefined
388
- ? unsafeCoerce(prompt)
389
- : unsafeCoerce({ ...prompt, escape: { label: questions.escape } });
391
+ ? cast(prompt)
392
+ : cast({ ...prompt, escape: { label: questions.escape } });
390
393
  };
391
394
  const validateAnswer = (field, value) => {
392
395
  const answer = getAnswer(field);
@@ -395,7 +398,7 @@ export const defineCollectStage = (definition) => {
395
398
  }
396
399
  // SAFETY: field selects the same answer definition whose schema parsed
397
400
  // value before validation, preserving that field's validator input.
398
- const validation = unsafeCoerce(answer.validate);
401
+ const validation = cast(answer.validate);
399
402
  return validation(value).pipe(Effect.mapError((error) => new AnswerValidationRejected({
400
403
  stage: definition.name,
401
404
  field,
@@ -413,7 +416,7 @@ export const defineCollectStage = (definition) => {
413
416
  for (const repair of repairs) {
414
417
  // SAFETY: the field lookup below rejects names outside Fields before
415
418
  // any field-indexed operation runs.
416
- const field = unsafeCoerce(repair.field);
419
+ const field = cast(repair.field);
417
420
  const answer = definition.fields[field];
418
421
  if (answer === undefined ||
419
422
  seen.has(field) ||
@@ -547,7 +550,7 @@ export const defineCollectStage = (definition) => {
547
550
  };
548
551
  // SAFETY: accepted keys come only from fieldNames and every value was
549
552
  // decoded by that field's schema before insertion.
550
- const merged = unsafeCoerce(runtimeMerged);
553
+ const merged = cast(runtimeMerged);
551
554
  if (isComplete(merged)) {
552
555
  return {
553
556
  state: merged,
@@ -577,7 +580,7 @@ export const defineCollectStage = (definition) => {
577
580
  // at the first failure. This keeps application Effects and the selected
578
581
  // retry question deterministic. Each validator came from the same
579
582
  // concrete Fields mapping used by the public conditional unions.
580
- return execution;
583
+ return cast(execution);
581
584
  };
582
585
  const run = ({ state, messages, }) => {
583
586
  if (!isValidState(state) || !isGroundedInMessages(state, messages)) {
@@ -594,12 +597,12 @@ export const defineCollectStage = (definition) => {
594
597
  error.reason === "invalid_response"), (error) => Effect.logWarning("Falling back to the trusted pending question").pipe(Effect.annotateLogs({
595
598
  stage: definition.name,
596
599
  errorTag: error._tag,
597
- }), Effect.as(askPendingQuestion(state, messages, null)))));
600
+ }), Effect.as(askPendingQuestion(state, messages, null))), (error) => Effect.fail(error)));
598
601
  };
599
602
  // SAFETY: The chat runtime calls these erased operations only after the
600
603
  // generated state schema has parsed this exact collect-stage state. The
601
604
  // public lower-level run method already requires CollectStageState<Fields>.
602
- const assumeParsedState = (state) => unsafeCoerce(state);
605
+ const assumeParsedState = (state) => cast(state);
603
606
  return structuredDefinition("collect_stage")({
604
607
  _tag: "CollectStage",
605
608
  name: definition.name,
@@ -609,7 +612,7 @@ export const defineCollectStage = (definition) => {
609
612
  stateSchema,
610
613
  initialState,
611
614
  guards,
612
- parseState: (input) => Schema.decodeUnknown(stateSchema)(input, {
615
+ parseState: (input) => Schema.decodeUnknownEffect(stateSchema)(input, {
613
616
  onExcessProperty: "error",
614
617
  }),
615
618
  isComplete,
@@ -1,6 +1,6 @@
1
1
  import { Effect, Schema } from "effect";
2
2
  /** Opaque deterministic identity supplied to one command execution. */
3
- export declare const CommandIdSchema: Schema.brand<Schema.filter<typeof Schema.String>, "CommandId">;
3
+ export declare const CommandIdSchema: Schema.brand<Schema.String, "CommandId">;
4
4
  /** Opaque deterministic identity supplied to one command execution. */
5
5
  export type CommandId = Schema.Schema.Type<typeof CommandIdSchema>;
6
6
  /** Inputs whose exact tuple identity defines one command attempt. */
@@ -1,6 +1,6 @@
1
1
  import { Effect, Schema } from "effect";
2
2
  /** Opaque deterministic identity supplied to one command execution. */
3
- export const CommandIdSchema = Schema.String.pipe(Schema.pattern(/^cmd_[0-9a-f]{64}$/), Schema.brand("CommandId"));
3
+ export const CommandIdSchema = Schema.String.pipe(Schema.check(Schema.isPattern(/^cmd_[0-9a-f]{64}$/)), Schema.brand("CommandId"));
4
4
  const toHex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
5
5
  /** Derive the stable idempotency key for one persisted command turn. */
6
6
  export const deriveCommandId = (input) => Effect.promise(async () => {
@@ -8,4 +8,4 @@ export interface JsonObject {
8
8
  /** Recursively typed value accepted at serialized JSON boundaries. */
9
9
  export type JsonValue = JsonPrimitive | JsonObject | ReadonlyArray<JsonValue>;
10
10
  /** Runtime parser for recursively JSON-serializable values. */
11
- export declare const JsonValueSchema: Schema.Schema<JsonValue>;
11
+ export declare const JsonValueSchema: Schema.Codec<JsonValue>;