@popcomputer/structured-chat 0.1.0 → 0.2.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 (50) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +175 -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 +28 -15
  10. package/dist/core/collect-stage.js +58 -37
  11. package/dist/core/command.d.ts +1 -1
  12. package/dist/core/command.js +1 -1
  13. package/dist/core/debug-protocol.d.ts +126 -0
  14. package/dist/core/debug-protocol.js +19 -0
  15. package/dist/core/debug.d.ts +103 -0
  16. package/dist/core/debug.js +276 -0
  17. package/dist/core/json-value.d.ts +1 -1
  18. package/dist/core/json-value.js +8 -1
  19. package/dist/core/model-guard.d.ts +2 -3
  20. package/dist/core/model-guard.js +4 -4
  21. package/dist/core/model.d.ts +15 -19
  22. package/dist/core/model.js +22 -10
  23. package/dist/core/protocol.d.ts +84 -79
  24. package/dist/core/protocol.js +18 -12
  25. package/dist/core/question.js +17 -15
  26. package/dist/core/repair.js +1 -1
  27. package/dist/core/session.d.ts +22 -28
  28. package/dist/core/session.js +16 -7
  29. package/dist/core/stage-name.d.ts +1 -1
  30. package/dist/core/stage-name.js +1 -1
  31. package/dist/core/stage.d.ts +4 -4
  32. package/dist/core/stage.js +11 -8
  33. package/dist/core/tool-set.js +6 -6
  34. package/dist/core/tool.d.ts +36 -39
  35. package/dist/core/tool.js +58 -31
  36. package/dist/core/view.d.ts +64 -39
  37. package/dist/core/view.js +12 -15
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.js +2 -0
  40. package/dist/integrations/assistant-ui-debug.d.ts +25 -0
  41. package/dist/integrations/assistant-ui-debug.js +1162 -0
  42. package/dist/integrations/assistant-ui.d.ts +10 -3
  43. package/dist/integrations/assistant-ui.js +48 -18
  44. package/dist/testing/in-memory-session-store.js +1 -1
  45. package/dist/testing/scenario.js +6 -6
  46. package/examples/answer-modes.ts +60 -49
  47. package/examples/prompt-injection-policy.ts +20 -20
  48. package/examples/resource-search.ts +104 -0
  49. package/package.json +15 -3
  50. package/examples/agency-search.ts +0 -101
@@ -1,14 +1,14 @@
1
1
  import { Effect, Schema } from "effect";
2
2
  import type { ChoiceQuestion, FixedQuestion, QuestionDefinition, QuestionDefinitionContract } from "./question.js";
3
3
  /** How strongly a collect-stage answer must be grounded in user messages. */
4
- export declare const AnswerModeSchema: Schema.Literal<["semantic", "explicit", "confirmed"]>;
4
+ export declare const AnswerModeSchema: Schema.Literals<readonly ["semantic", "explicit", "confirmed"]>;
5
5
  /** How strongly a collect-stage answer must be grounded in user messages. */
6
6
  export type AnswerMode = Schema.Schema.Type<typeof AnswerModeSchema>;
7
7
  /** Minimum runtime shape retained for every collect-stage answer. */
8
8
  export interface AnswerDefinitionContract {
9
9
  readonly _tag: "AnswerDefinition";
10
10
  readonly mode: AnswerMode;
11
- readonly schema: Schema.Schema.AnyNoContext;
11
+ readonly schema: Schema.ConstraintCodec<unknown, unknown>;
12
12
  readonly description: string;
13
13
  readonly question: QuestionDefinitionContract;
14
14
  readonly validate?: (value: never) => Effect.Effect<void, unknown, unknown>;
@@ -20,16 +20,16 @@ export interface AnswerDefinitionContract {
20
20
  };
21
21
  }
22
22
  /** One typed fact required by a collect stage. */
23
- export interface AnswerDefinition<Mode extends AnswerMode, ValueSchema extends Schema.Schema.AnyNoContext, Error = never, Requirements = never> extends AnswerDefinitionContract {
23
+ export interface AnswerDefinition<Mode extends AnswerMode, ValueSchema extends Schema.ConstraintCodec<unknown, unknown>, Error = never, Requirements = never> extends AnswerDefinitionContract {
24
24
  readonly mode: Mode;
25
25
  readonly schema: ValueSchema;
26
- readonly question: QuestionDefinition<Schema.Schema.Type<ValueSchema>>;
27
- readonly validate?: (value: Schema.Schema.Type<ValueSchema>) => Effect.Effect<void, Error, Requirements>;
26
+ readonly question: QuestionDefinition<ValueSchema["Type"]>;
27
+ readonly validate?: (value: ValueSchema["Type"]) => Effect.Effect<void, Error, Requirements>;
28
28
  readonly reject?: {
29
- readonly ask: FixedQuestion | ChoiceQuestion<Schema.Schema.Type<ValueSchema>>;
29
+ readonly ask: FixedQuestion | ChoiceQuestion<ValueSchema["Type"]>;
30
30
  };
31
31
  readonly escape?: {
32
- readonly value: Schema.Schema.Type<ValueSchema>;
32
+ readonly value: ValueSchema["Type"];
33
33
  };
34
34
  }
35
35
  interface DefineAnswerBase<Value> {
@@ -58,12 +58,12 @@ export interface DefineValidatedAnswerInput<Value, Error, Requirements> extends
58
58
  }
59
59
  /** Configuration shared by all answer grounding modes. */
60
60
  export type DefineAnswerInput<Value, Error = never, Requirements = never> = DefineUnvalidatedAnswerInput<Value> | DefineValidatedAnswerInput<Value, Error, Requirements>;
61
- declare function semantic<ValueSchema extends Schema.Schema.AnyNoContext>(schema: ValueSchema, input: DefineUnvalidatedAnswerInput<Schema.Schema.Type<ValueSchema>>): AnswerDefinition<"semantic", ValueSchema, never, never>;
62
- declare function semantic<ValueSchema extends Schema.Schema.AnyNoContext, Error, Requirements>(schema: ValueSchema, input: DefineValidatedAnswerInput<Schema.Schema.Type<ValueSchema>, Error, Requirements>): AnswerDefinition<"semantic", ValueSchema, Error, Requirements>;
63
- declare function explicit<ValueSchema extends Schema.Schema.AnyNoContext>(schema: ValueSchema, input: DefineUnvalidatedAnswerInput<Schema.Schema.Type<ValueSchema>>): AnswerDefinition<"explicit", ValueSchema, never, never>;
64
- declare function explicit<ValueSchema extends Schema.Schema.AnyNoContext, Error, Requirements>(schema: ValueSchema, input: DefineValidatedAnswerInput<Schema.Schema.Type<ValueSchema>, Error, Requirements>): AnswerDefinition<"explicit", ValueSchema, Error, Requirements>;
65
- declare function confirmed<ValueSchema extends Schema.Schema.AnyNoContext>(schema: ValueSchema, input: DefineUnvalidatedAnswerInput<Schema.Schema.Type<ValueSchema>>): AnswerDefinition<"confirmed", ValueSchema, never, never>;
66
- declare function confirmed<ValueSchema extends Schema.Schema.AnyNoContext, Error, Requirements>(schema: ValueSchema, input: DefineValidatedAnswerInput<Schema.Schema.Type<ValueSchema>, Error, Requirements>): AnswerDefinition<"confirmed", ValueSchema, Error, Requirements>;
61
+ declare function semantic<ValueSchema extends Schema.ConstraintCodec<unknown, unknown>>(schema: ValueSchema, input: DefineUnvalidatedAnswerInput<ValueSchema["Type"]>): AnswerDefinition<"semantic", ValueSchema, never, never>;
62
+ declare function semantic<ValueSchema extends Schema.ConstraintCodec<unknown, unknown>, Error, Requirements>(schema: ValueSchema, input: DefineValidatedAnswerInput<ValueSchema["Type"], Error, Requirements>): AnswerDefinition<"semantic", ValueSchema, Error, Requirements>;
63
+ declare function explicit<ValueSchema extends Schema.ConstraintCodec<unknown, unknown>>(schema: ValueSchema, input: DefineUnvalidatedAnswerInput<ValueSchema["Type"]>): AnswerDefinition<"explicit", ValueSchema, never, never>;
64
+ declare function explicit<ValueSchema extends Schema.ConstraintCodec<unknown, unknown>, Error, Requirements>(schema: ValueSchema, input: DefineValidatedAnswerInput<ValueSchema["Type"], Error, Requirements>): AnswerDefinition<"explicit", ValueSchema, Error, Requirements>;
65
+ declare function confirmed<ValueSchema extends Schema.ConstraintCodec<unknown, unknown>>(schema: ValueSchema, input: DefineUnvalidatedAnswerInput<ValueSchema["Type"]>): AnswerDefinition<"confirmed", ValueSchema, never, never>;
66
+ declare function confirmed<ValueSchema extends Schema.ConstraintCodec<unknown, unknown>, Error, Requirements>(schema: ValueSchema, input: DefineValidatedAnswerInput<ValueSchema["Type"], Error, Requirements>): AnswerDefinition<"confirmed", ValueSchema, Error, Requirements>;
67
67
  /** Constructors for semantic, explicit, and explicitly confirmed facts. */
68
68
  export declare const Answer: {
69
69
  readonly semantic: typeof semantic;
@@ -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,19 +1,16 @@
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";
6
5
  import { InvalidToolCall, type InvalidToolProjection } from "./tool.js";
7
- import type { AdaptiveChoiceQuestion, ChoiceQuestion, QuestionChoice } from "./question.js";
6
+ import type { AdaptiveChoiceQuestion, ChoiceQuestion, QuestionDefinitionContract, 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,27 +136,43 @@ 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;
151
148
  readonly messages: ReadonlyArray<UntrustedMessage>;
152
149
  }) => Effect.Effect<RuntimeCollectStageTurn, unknown, unknown>;
153
150
  }
151
+ /** @internal One definition-ordered field exposed to trusted projections. */
152
+ export interface CollectStageInspectionField {
153
+ readonly field: string;
154
+ readonly mode: AnswerMode;
155
+ readonly description: string;
156
+ readonly question: QuestionDefinitionContract;
157
+ readonly encodeValue: (value: RuntimeAnswerValue) => Effect.Effect<unknown, Schema.SchemaError>;
158
+ }
159
+ /** @internal Read-only collect-stage metadata used by trusted projections. */
160
+ export interface CollectStageInspection {
161
+ readonly fields: ReadonlyArray<CollectStageInspectionField>;
162
+ }
154
163
  declare const collectStageRuntime: unique symbol;
164
+ declare const collectStageInspection: unique symbol;
155
165
  /** Minimum sealed collect-stage shape accepted by a chat definition. */
156
166
  export interface CollectStageDefinitionContract extends StructuredDefinition<"collect_stage"> {
157
167
  readonly _tag: "CollectStage";
158
168
  readonly name: string;
159
169
  readonly [collectStageRuntime]: CollectStageRuntime;
170
+ readonly [collectStageInspection]: CollectStageInspection;
160
171
  }
161
172
  /** @internal Read the erased runtime from an authentic collect stage. */
162
173
  export declare const readCollectStageRuntime: (stage: CollectStageDefinitionContract) => CollectStageRuntime;
174
+ /** @internal Read trusted definition metadata from an authentic collect stage. */
175
+ export declare const readCollectStageInspection: (stage: CollectStageDefinitionContract) => CollectStageInspection;
163
176
  /** Shared conversational policy for questions in one collect stage. */
164
177
  export interface CollectQuestionPolicy {
165
178
  /** Trusted style guidance applied to every adaptive question in this stage. */
@@ -180,12 +193,12 @@ export interface CollectStage<Name extends string, Fields extends AnswerFields,
180
193
  readonly name: Name;
181
194
  readonly fields: Fields;
182
195
  readonly questions: CollectQuestionPolicy;
183
- readonly answersSchema: Schema.Schema<CollectAnswers<Fields>, unknown, never>;
184
- readonly stateSchema: Schema.Schema<CollectStageState<Fields>, unknown, never>;
196
+ readonly answersSchema: Schema.Codec<CollectAnswers<Fields>, unknown>;
197
+ readonly stateSchema: Schema.Codec<CollectStageState<Fields>, unknown>;
185
198
  readonly initialState: CollectStageState<Fields>;
186
199
  readonly guards: Guards;
187
200
  /** 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>;
201
+ readonly parseState: (input: Schema.Codec.Encoded<Schema.Codec<CollectStageState<Fields>, unknown>>) => Effect.Effect<CollectStageState<Fields>, Schema.SchemaError>;
189
202
  /** Test whether every schema-defined answer has been populated. */
190
203
  readonly isComplete: (state: CollectStageState<Fields>) => boolean;
191
204
  /** Select the first missing question in schema declaration order. */