@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,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,
@@ -16,14 +19,17 @@ export class InvalidCollectStageResponse extends Schema.TaggedError()("InvalidCo
16
19
  export class AnswerValidationRejected extends Data.TaggedError("AnswerValidationRejected") {
17
20
  }
18
21
  const collectStageRuntime = Symbol("@popcomputer/structured-chat/CollectStageRuntime");
22
+ const collectStageInspection = Symbol("@popcomputer/structured-chat/CollectStageInspection");
19
23
  /** @internal Read the erased runtime from an authentic collect stage. */
20
24
  export const readCollectStageRuntime = (stage) => stage[collectStageRuntime];
25
+ /** @internal Read trusted definition metadata from an authentic collect stage. */
26
+ export const readCollectStageInspection = (stage) => stage[collectStageInspection];
21
27
  const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
22
28
  /** Define one deterministic schema-derived fact collection stage. */
23
29
  export const defineCollectStage = (definition) => {
24
30
  Schema.decodeSync(StageNameSchema)(definition.name);
25
- const questionGuidanceSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(2_000));
26
- const questionEscapeSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(100));
31
+ const questionGuidanceSchema = Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(2_000));
32
+ const questionEscapeSchema = Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(100));
27
33
  const questionPolicyBuilder = {};
28
34
  if (definition.questions?.guidance !== undefined) {
29
35
  questionPolicyBuilder.guidance = Schema.decodeSync(questionGuidanceSchema)(definition.questions.guidance);
@@ -34,7 +40,7 @@ export const defineCollectStage = (definition) => {
34
40
  const questions = questionPolicyBuilder;
35
41
  // SAFETY: definition.fields is the exact Fields mapping; Object.keys returns
36
42
  // only its enumerable string keys.
37
- const fieldNames = unsafeCoerce(Object.keys(definition.fields));
43
+ const fieldNames = cast(Object.keys(definition.fields));
38
44
  if (fieldNames.length === 0) {
39
45
  throw new Error("Collect stages require at least one answer field");
40
46
  }
@@ -53,8 +59,8 @@ export const defineCollectStage = (definition) => {
53
59
  if (firstField === undefined) {
54
60
  throw new Error("Collect stages require at least one answer field");
55
61
  }
56
- const fieldSchema = Schema.Literal(firstField, ...remainingFields);
57
- const messageIndexSchema = Schema.Number.pipe(Schema.int(), Schema.between(0, 1_000_000));
62
+ const fieldSchema = Schema.Literals([firstField, ...remainingFields]);
63
+ const messageIndexSchema = Schema.Number.check(Schema.isInt(), Schema.isBetween({ minimum: 0, maximum: 1_000_000 }));
58
64
  const getAnswer = (field) => {
59
65
  const answer = definition.fields[field];
60
66
  if (answer === undefined) {
@@ -91,10 +97,10 @@ export const defineCollectStage = (definition) => {
91
97
  }
92
98
  const answerSchemaEntries = fieldNames.map((field) => [field, getAnswer(field).schema]);
93
99
  // SAFETY: every entry uses one exact Fields key and its corresponding schema.
94
- const answerSchemas = unsafeCoerce(Object.fromEntries(answerSchemaEntries));
100
+ const answerSchemas = cast(Object.fromEntries(answerSchemaEntries));
95
101
  const rawAnswersSchema = Schema.Struct(answerSchemas);
96
- const evidenceQuoteSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(2_000));
97
- const questionTextSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(500));
102
+ const evidenceQuoteSchema = Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(2_000));
103
+ const questionTextSchema = Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(500));
98
104
  const acceptedEvidenceSchema = Schema.Struct({
99
105
  messageIndex: messageIndexSchema,
100
106
  quote: evidenceQuoteSchema,
@@ -126,10 +132,10 @@ export const defineCollectStage = (definition) => {
126
132
  }
127
133
  const rawRepairSchema = remainingRepairSchemas.length === 0
128
134
  ? firstRepairSchema
129
- : Schema.Union(firstRepairSchema, ...remainingRepairSchemas);
135
+ : Schema.Union([firstRepairSchema, ...remainingRepairSchemas]);
130
136
  // SAFETY: every dynamically generated member uses only AnyNoContext field
131
137
  // schemas and exact stage, field, and transition literals.
132
- const repairSchema = rawRepairSchema;
138
+ const repairSchema = cast(rawRepairSchema);
133
139
  const acceptedFields = Object.fromEntries(fieldNames.map((field) => [
134
140
  field,
135
141
  Schema.Struct({
@@ -145,8 +151,8 @@ export const defineCollectStage = (definition) => {
145
151
  }),
146
152
  ]));
147
153
  const rawStateSchema = Schema.Struct({
148
- accepted: Schema.partial(Schema.Struct(acceptedFields)),
149
- asked: Schema.partial(Schema.Struct(askedFields)),
154
+ accepted: Schema.Struct(acceptedFields).mapFields(Struct.map(Schema.optional)),
155
+ asked: Schema.Struct(askedFields).mapFields(Struct.map(Schema.optional)),
150
156
  });
151
157
  const isValidState = (state) => {
152
158
  return fieldNames.every((field) => {
@@ -156,49 +162,61 @@ export const defineCollectStage = (definition) => {
156
162
  hasOwn(state.asked, field));
157
163
  });
158
164
  };
159
- const refinedStateSchema = rawStateSchema.pipe(Schema.filter(isValidState, {
165
+ const refinedStateSchema = rawStateSchema.check(Schema.makeFilter(isValidState, {
160
166
  description: "semantically valid collect-stage state",
161
167
  }));
162
168
  // SAFETY: rawAnswersSchema is created from every field's exact schema.
163
- const answersSchema = unsafeCoerce(rawAnswersSchema);
169
+ const answersSchema = cast(rawAnswersSchema);
164
170
  // SAFETY: partial preserves the mapped accepted-answer types, while asked is
165
171
  // a record whose keys are restricted to the exact field literal union.
166
- const stateSchema = unsafeCoerce(refinedStateSchema);
167
- const initialState = Schema.validateSync(stateSchema)({
172
+ const stateSchema = cast(refinedStateSchema);
173
+ const initialState = Schema.decodeSync(Schema.toType(stateSchema))({
168
174
  accepted: {},
169
175
  asked: {},
170
176
  });
177
+ const inspectionFields = fieldNames.map((field) => {
178
+ const answer = getAnswer(field);
179
+ return {
180
+ field,
181
+ mode: answer.mode,
182
+ description: answer.description,
183
+ question: answer.question,
184
+ encodeValue: (value) => Schema.encodeUnknownEffect(answer.schema)(value, {
185
+ onExcessProperty: "error",
186
+ }),
187
+ };
188
+ });
171
189
  // SAFETY: when guards are omitted, Guards uses its readonly [] default; an
172
190
  // explicitly supplied tuple is returned unchanged.
173
- const guards = definition.guards ?? unsafeCoerce([]);
191
+ const guards = definition.guards ?? cast([]);
174
192
  // SAFETY: every entry is built from one registered AnyNoContext answer
175
193
  // schema and adds only the model-wire null representation for absence.
176
194
  const proposalAnswerSchemaEntries = fieldNames.map((field) => {
177
195
  const answer = getAnswer(field);
178
196
  return [
179
197
  field,
180
- Schema.NullOr(answer.schema).annotations({
198
+ Schema.NullOr(answer.schema).annotate({
181
199
  description: `${answer.mode}: ${answer.description}`,
182
200
  }),
183
201
  ];
184
202
  });
185
203
  // SAFETY: each entry contains one registered field and its no-context schema.
186
- const proposalAnswerSchemas = unsafeCoerce(Object.fromEntries(proposalAnswerSchemaEntries));
204
+ const proposalAnswerSchemas = cast(Object.fromEntries(proposalAnswerSchemaEntries));
187
205
  const rawProposalSchema = Schema.Struct({
188
206
  answers: Schema.Struct(proposalAnswerSchemas),
189
207
  evidence: Schema.Array(Schema.Struct({
190
208
  field: fieldSchema,
191
209
  quote: evidenceQuoteSchema,
192
- })).pipe(Schema.maxItems(fieldNames.length)),
210
+ })).check(Schema.isMaxLength(fieldNames.length)),
193
211
  nextQuestion: Schema.NullOr(Schema.Struct({
194
212
  field: fieldSchema,
195
213
  text: questionTextSchema,
196
- options: Schema.Array(Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(100))).pipe(Schema.maxItems(20)),
214
+ options: Schema.Array(Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(100))).check(Schema.isMaxLength(20)),
197
215
  })),
198
216
  });
199
217
  // SAFETY: every answer field schema is constrained to AnyNoContext; the
200
218
  // generic mapped Struct cannot prove that fact after Object.fromEntries.
201
- const ProposalSchema = rawProposalSchema;
219
+ const ProposalSchema = cast(rawProposalSchema);
202
220
  const submitAnswers = defineTool({
203
221
  name: "submit_answers",
204
222
  description: "Submit grounded answers from the conversation and optionally phrase the next adaptive question.",
@@ -314,11 +332,11 @@ export const defineCollectStage = (definition) => {
314
332
  // A selected label is later submitted as this answer's wire value,
315
333
  // so model-authored labels that cannot decode would dead-end the
316
334
  // user; fall back to the application-authored options instead.
317
- const decodeLabel = Schema.decodeUnknownEither(getAnswer(pending.field).schema);
335
+ const decodeLabel = Schema.decodeUnknownResult(getAnswer(pending.field).schema);
318
336
  const validOptions = supplied.length < question.minimumOptions ||
319
337
  supplied.length > question.maximumOptions ||
320
338
  new Set(normalized).size !== normalized.length ||
321
- supplied.some(({ label }) => Either.isLeft(decodeLabel(label)))
339
+ supplied.some(({ label }) => Result.isFailure(decodeLabel(label)))
322
340
  ? undefined
323
341
  : supplied;
324
342
  const selectedOptions = validOptions ??
@@ -339,8 +357,8 @@ export const defineCollectStage = (definition) => {
339
357
  options,
340
358
  };
341
359
  return questions.escape === undefined
342
- ? unsafeCoerce(prompt)
343
- : unsafeCoerce({ ...prompt, escape: { label: questions.escape } });
360
+ ? cast(prompt)
361
+ : cast({ ...prompt, escape: { label: questions.escape } });
344
362
  };
345
363
  const askPendingQuestion = (state, messages, adaptive) => {
346
364
  const pending = nextQuestion(state);
@@ -385,8 +403,8 @@ export const defineCollectStage = (definition) => {
385
403
  options: question._tag === "ChoiceQuestion" ? question.options : [],
386
404
  };
387
405
  return questions.escape === undefined
388
- ? unsafeCoerce(prompt)
389
- : unsafeCoerce({ ...prompt, escape: { label: questions.escape } });
406
+ ? cast(prompt)
407
+ : cast({ ...prompt, escape: { label: questions.escape } });
390
408
  };
391
409
  const validateAnswer = (field, value) => {
392
410
  const answer = getAnswer(field);
@@ -395,7 +413,7 @@ export const defineCollectStage = (definition) => {
395
413
  }
396
414
  // SAFETY: field selects the same answer definition whose schema parsed
397
415
  // value before validation, preserving that field's validator input.
398
- const validation = unsafeCoerce(answer.validate);
416
+ const validation = cast(answer.validate);
399
417
  return validation(value).pipe(Effect.mapError((error) => new AnswerValidationRejected({
400
418
  stage: definition.name,
401
419
  field,
@@ -413,7 +431,7 @@ export const defineCollectStage = (definition) => {
413
431
  for (const repair of repairs) {
414
432
  // SAFETY: the field lookup below rejects names outside Fields before
415
433
  // any field-indexed operation runs.
416
- const field = unsafeCoerce(repair.field);
434
+ const field = cast(repair.field);
417
435
  const answer = definition.fields[field];
418
436
  if (answer === undefined ||
419
437
  seen.has(field) ||
@@ -547,7 +565,7 @@ export const defineCollectStage = (definition) => {
547
565
  };
548
566
  // SAFETY: accepted keys come only from fieldNames and every value was
549
567
  // decoded by that field's schema before insertion.
550
- const merged = unsafeCoerce(runtimeMerged);
568
+ const merged = cast(runtimeMerged);
551
569
  if (isComplete(merged)) {
552
570
  return {
553
571
  state: merged,
@@ -577,7 +595,7 @@ export const defineCollectStage = (definition) => {
577
595
  // at the first failure. This keeps application Effects and the selected
578
596
  // retry question deterministic. Each validator came from the same
579
597
  // concrete Fields mapping used by the public conditional unions.
580
- return execution;
598
+ return cast(execution);
581
599
  };
582
600
  const run = ({ state, messages, }) => {
583
601
  if (!isValidState(state) || !isGroundedInMessages(state, messages)) {
@@ -594,12 +612,12 @@ export const defineCollectStage = (definition) => {
594
612
  error.reason === "invalid_response"), (error) => Effect.logWarning("Falling back to the trusted pending question").pipe(Effect.annotateLogs({
595
613
  stage: definition.name,
596
614
  errorTag: error._tag,
597
- }), Effect.as(askPendingQuestion(state, messages, null)))));
615
+ }), Effect.as(askPendingQuestion(state, messages, null))), (error) => Effect.fail(error)));
598
616
  };
599
617
  // SAFETY: The chat runtime calls these erased operations only after the
600
618
  // generated state schema has parsed this exact collect-stage state. The
601
619
  // public lower-level run method already requires CollectStageState<Fields>.
602
- const assumeParsedState = (state) => unsafeCoerce(state);
620
+ const assumeParsedState = (state) => cast(state);
603
621
  return structuredDefinition("collect_stage")({
604
622
  _tag: "CollectStage",
605
623
  name: definition.name,
@@ -609,7 +627,7 @@ export const defineCollectStage = (definition) => {
609
627
  stateSchema,
610
628
  initialState,
611
629
  guards,
612
- parseState: (input) => Schema.decodeUnknown(stateSchema)(input, {
630
+ parseState: (input) => Schema.decodeUnknownEffect(stateSchema)(input, {
613
631
  onExcessProperty: "error",
614
632
  }),
615
633
  isComplete,
@@ -627,6 +645,9 @@ export const defineCollectStage = (definition) => {
627
645
  },
628
646
  }),
629
647
  run,
648
+ [collectStageInspection]: {
649
+ fields: inspectionFields,
650
+ },
630
651
  [collectStageRuntime]: {
631
652
  initialState,
632
653
  stateSchema,
@@ -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 () => {
@@ -0,0 +1,126 @@
1
+ import { Effect, Schema } from "effect";
2
+ import type { ChatDefinition, ChatReply, ChatStageTuple } from "./chat.js";
3
+ import { type InspectChatStateOptions, type InvalidChatDebugProjection } from "./debug.js";
4
+ import { presentChatReply, type InvalidChatPresentation, type PresentChatReplyOptions } from "./protocol.js";
5
+ type BrowserPresentableTurn = Parameters<typeof presentChatReply>[0]["turn"];
6
+ type DebugChatTurn<Name extends string, Version extends number, Stages extends ChatStageTuple> = ChatReply<Name, Version, Stages>["turn"] & BrowserPresentableTurn;
7
+ type DebugChatReply<Name extends string, Version extends number, Stages extends ChatStageTuple> = Omit<ChatReply<Name, Version, Stages>, "turn"> & {
8
+ readonly sessionId: string;
9
+ readonly turn: DebugChatTurn<Name, Version, Stages>;
10
+ };
11
+ /** Explicit opt-in browser response carrying one debug state projection. */
12
+ export declare const StructuredChatDebugTurnResponseSchema: Schema.Struct<{
13
+ readonly debug: Schema.Struct<{
14
+ readonly schemaVersion: Schema.Literal<1>;
15
+ readonly chat: Schema.Struct<{
16
+ readonly name: Schema.Trimmed;
17
+ readonly version: Schema.Number;
18
+ }>;
19
+ readonly status: Schema.Literals<readonly ["active", "complete"]>;
20
+ readonly currentStage: Schema.Struct<{
21
+ readonly index: Schema.Natural;
22
+ readonly name: Schema.Trimmed;
23
+ readonly kind: Schema.Literals<readonly ["collect", "tool", "command"]>;
24
+ }>;
25
+ readonly stages: Schema.$Array<Schema.Union<readonly [Schema.Struct<{
26
+ readonly satisfiedFields: Schema.Natural;
27
+ readonly totalFields: Schema.Natural;
28
+ readonly fields: Schema.$Array<Schema.Struct<{
29
+ readonly field: Schema.String;
30
+ readonly mode: Schema.Literals<readonly ["semantic", "explicit", "confirmed"]>;
31
+ readonly description: Schema.String;
32
+ readonly question: Schema.Union<readonly [Schema.Struct<{
33
+ readonly _tag: Schema.Literal<"FixedQuestion">;
34
+ readonly text: Schema.String;
35
+ }>, Schema.Struct<{
36
+ readonly _tag: Schema.Literal<"AdaptiveQuestion">;
37
+ readonly goal: Schema.String;
38
+ readonly fallback: Schema.String;
39
+ }>, Schema.Struct<{
40
+ readonly _tag: Schema.Literal<"AdaptiveChoiceQuestion">;
41
+ readonly prompt: Schema.String;
42
+ readonly minimumOptions: Schema.Natural;
43
+ readonly maximumOptions: Schema.Natural;
44
+ readonly fallbackOptions: Schema.$Array<Schema.String>;
45
+ }>, Schema.Struct<{
46
+ readonly _tag: Schema.Literal<"ChoiceQuestion">;
47
+ readonly text: Schema.String;
48
+ readonly options: Schema.$Array<Schema.Struct<{
49
+ readonly label: Schema.String;
50
+ }>>;
51
+ }>]>;
52
+ readonly state: Schema.Union<readonly [Schema.Struct<{
53
+ readonly _tag: Schema.Literal<"Missing">;
54
+ }>, Schema.Struct<{
55
+ readonly _tag: Schema.Literal<"Asked">;
56
+ readonly issuedQuestion: Schema.Struct<{
57
+ readonly messageIndex: Schema.Natural;
58
+ readonly text: Schema.String;
59
+ }>;
60
+ }>, Schema.Struct<{
61
+ readonly _tag: Schema.Literal<"Accepted">;
62
+ readonly value: Schema.Codec<import("./json-value.js").JsonValue, import("./json-value.js").JsonValue, never, never>;
63
+ readonly evidence: Schema.NullOr<Schema.Struct<{
64
+ readonly messageIndex: Schema.Natural;
65
+ readonly quote: Schema.String;
66
+ }>>;
67
+ readonly issuedQuestion: Schema.NullOr<Schema.Struct<{
68
+ readonly messageIndex: Schema.Natural;
69
+ readonly text: Schema.String;
70
+ }>>;
71
+ }>]>;
72
+ }>>;
73
+ readonly index: Schema.Natural;
74
+ readonly name: Schema.Trimmed;
75
+ readonly status: Schema.Literals<readonly ["complete", "current", "upcoming"]>;
76
+ readonly repairPending: Schema.Boolean;
77
+ readonly _tag: Schema.Literal<"CollectStage">;
78
+ }>, Schema.Struct<{
79
+ readonly tools: Schema.$Array<Schema.Trimmed>;
80
+ readonly afterExecution: Schema.Literals<readonly ["stay", "complete"]>;
81
+ readonly index: Schema.Natural;
82
+ readonly name: Schema.Trimmed;
83
+ readonly status: Schema.Literals<readonly ["complete", "current", "upcoming"]>;
84
+ readonly repairPending: Schema.Boolean;
85
+ readonly _tag: Schema.Literal<"ToolStage">;
86
+ }>, Schema.Struct<{
87
+ readonly command: Schema.Trimmed;
88
+ readonly index: Schema.Natural;
89
+ readonly name: Schema.Trimmed;
90
+ readonly status: Schema.Literals<readonly ["complete", "current", "upcoming"]>;
91
+ readonly repairPending: Schema.Boolean;
92
+ readonly _tag: Schema.Literal<"CommandStage">;
93
+ }>]>>;
94
+ }>;
95
+ readonly schemaVersion: Schema.Literal<1>;
96
+ readonly session: Schema.optional<Schema.Struct<{
97
+ readonly id: Schema.Trimmed;
98
+ readonly revision: Schema.Trimmed;
99
+ }>>;
100
+ readonly message: Schema.Struct<{
101
+ readonly role: Schema.Literal<"assistant">;
102
+ readonly content: Schema.NonEmptyArray<Schema.Union<readonly [Schema.Struct<{
103
+ readonly type: Schema.Literal<"text">;
104
+ readonly text: Schema.Trimmed;
105
+ }>, Schema.Struct<{
106
+ readonly type: Schema.Literal<"data">;
107
+ readonly name: Schema.Trimmed;
108
+ readonly data: Schema.Unknown;
109
+ }>]>>;
110
+ }>;
111
+ }>;
112
+ /** Explicit opt-in browser response carrying one debug state projection. */
113
+ export type StructuredChatDebugTurnResponse = Schema.Schema.Type<typeof StructuredChatDebugTurnResponseSchema>;
114
+ /** Presentation and state-inspection policies for one debug chat reply. */
115
+ export interface PresentChatDebugReplyOptions<Name extends string, Version extends number, Stages extends ChatStageTuple> {
116
+ readonly presentation?: PresentChatReplyOptions<DebugChatTurn<Name, Version, Stages>>;
117
+ readonly inspection?: InspectChatStateOptions;
118
+ }
119
+ /**
120
+ * Project one persisted reply into the explicit debug browser protocol.
121
+ *
122
+ * Applications must select this presenter deliberately and should authorize
123
+ * its endpoint independently from whether a debug panel is visually mounted.
124
+ */
125
+ export declare const presentChatDebugReply: <const Name extends string, const Version extends number, const Stages extends ChatStageTuple>(chat: ChatDefinition<Name, Version, Stages>, reply: DebugChatReply<Name, Version, Stages>, options?: PresentChatDebugReplyOptions<Name, Version, Stages>) => Effect.Effect<StructuredChatDebugTurnResponse, InvalidChatPresentation | InvalidChatDebugProjection>;
126
+ export {};
@@ -0,0 +1,19 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { inspectChatState, StructuredChatDebugSnapshotSchema, } from "./debug.js";
3
+ import { presentChatReply, StructuredChatTurnResponseSchema, } from "./protocol.js";
4
+ /** Explicit opt-in browser response carrying one debug state projection. */
5
+ export const StructuredChatDebugTurnResponseSchema = Schema.Struct({
6
+ ...StructuredChatTurnResponseSchema.fields,
7
+ debug: StructuredChatDebugSnapshotSchema,
8
+ });
9
+ /**
10
+ * Project one persisted reply into the explicit debug browser protocol.
11
+ *
12
+ * Applications must select this presenter deliberately and should authorize
13
+ * its endpoint independently from whether a debug panel is visually mounted.
14
+ */
15
+ export const presentChatDebugReply = (chat, reply, options = {}) => Effect.gen(function* () {
16
+ const response = yield* presentChatReply(reply, options.presentation);
17
+ const debug = yield* inspectChatState(chat, reply.turn.state, options.inspection);
18
+ return { ...response, debug };
19
+ });
@@ -0,0 +1,103 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { type ChatDefinition, type ChatStageTuple, type ChatState } from "./chat.js";
3
+ /** Runtime schema for one JSON-safe structured-chat debug snapshot. */
4
+ export declare const StructuredChatDebugSnapshotSchema: Schema.Struct<{
5
+ readonly schemaVersion: Schema.Literal<1>;
6
+ readonly chat: Schema.Struct<{
7
+ readonly name: Schema.Trimmed;
8
+ readonly version: Schema.Number;
9
+ }>;
10
+ readonly status: Schema.Literals<readonly ["active", "complete"]>;
11
+ readonly currentStage: Schema.Struct<{
12
+ readonly index: Schema.Natural;
13
+ readonly name: Schema.Trimmed;
14
+ readonly kind: Schema.Literals<readonly ["collect", "tool", "command"]>;
15
+ }>;
16
+ readonly stages: Schema.$Array<Schema.Union<readonly [Schema.Struct<{
17
+ readonly satisfiedFields: Schema.Natural;
18
+ readonly totalFields: Schema.Natural;
19
+ readonly fields: Schema.$Array<Schema.Struct<{
20
+ readonly field: Schema.String;
21
+ readonly mode: Schema.Literals<readonly ["semantic", "explicit", "confirmed"]>;
22
+ readonly description: Schema.String;
23
+ readonly question: Schema.Union<readonly [Schema.Struct<{
24
+ readonly _tag: Schema.Literal<"FixedQuestion">;
25
+ readonly text: Schema.String;
26
+ }>, Schema.Struct<{
27
+ readonly _tag: Schema.Literal<"AdaptiveQuestion">;
28
+ readonly goal: Schema.String;
29
+ readonly fallback: Schema.String;
30
+ }>, Schema.Struct<{
31
+ readonly _tag: Schema.Literal<"AdaptiveChoiceQuestion">;
32
+ readonly prompt: Schema.String;
33
+ readonly minimumOptions: Schema.Natural;
34
+ readonly maximumOptions: Schema.Natural;
35
+ readonly fallbackOptions: Schema.$Array<Schema.String>;
36
+ }>, Schema.Struct<{
37
+ readonly _tag: Schema.Literal<"ChoiceQuestion">;
38
+ readonly text: Schema.String;
39
+ readonly options: Schema.$Array<Schema.Struct<{
40
+ readonly label: Schema.String;
41
+ }>>;
42
+ }>]>;
43
+ readonly state: Schema.Union<readonly [Schema.Struct<{
44
+ readonly _tag: Schema.Literal<"Missing">;
45
+ }>, Schema.Struct<{
46
+ readonly _tag: Schema.Literal<"Asked">;
47
+ readonly issuedQuestion: Schema.Struct<{
48
+ readonly messageIndex: Schema.Natural;
49
+ readonly text: Schema.String;
50
+ }>;
51
+ }>, Schema.Struct<{
52
+ readonly _tag: Schema.Literal<"Accepted">;
53
+ readonly value: Schema.Codec<import("./json-value.js").JsonValue, import("./json-value.js").JsonValue, never, never>;
54
+ readonly evidence: Schema.NullOr<Schema.Struct<{
55
+ readonly messageIndex: Schema.Natural;
56
+ readonly quote: Schema.String;
57
+ }>>;
58
+ readonly issuedQuestion: Schema.NullOr<Schema.Struct<{
59
+ readonly messageIndex: Schema.Natural;
60
+ readonly text: Schema.String;
61
+ }>>;
62
+ }>]>;
63
+ }>>;
64
+ readonly index: Schema.Natural;
65
+ readonly name: Schema.Trimmed;
66
+ readonly status: Schema.Literals<readonly ["complete", "current", "upcoming"]>;
67
+ readonly repairPending: Schema.Boolean;
68
+ readonly _tag: Schema.Literal<"CollectStage">;
69
+ }>, Schema.Struct<{
70
+ readonly tools: Schema.$Array<Schema.Trimmed>;
71
+ readonly afterExecution: Schema.Literals<readonly ["stay", "complete"]>;
72
+ readonly index: Schema.Natural;
73
+ readonly name: Schema.Trimmed;
74
+ readonly status: Schema.Literals<readonly ["complete", "current", "upcoming"]>;
75
+ readonly repairPending: Schema.Boolean;
76
+ readonly _tag: Schema.Literal<"ToolStage">;
77
+ }>, Schema.Struct<{
78
+ readonly command: Schema.Trimmed;
79
+ readonly index: Schema.Natural;
80
+ readonly name: Schema.Trimmed;
81
+ readonly status: Schema.Literals<readonly ["complete", "current", "upcoming"]>;
82
+ readonly repairPending: Schema.Boolean;
83
+ readonly _tag: Schema.Literal<"CommandStage">;
84
+ }>]>>;
85
+ }>;
86
+ /** JSON-safe read model rendered by a structured-chat debug inspector. */
87
+ export type StructuredChatDebugSnapshot = Schema.Schema.Type<typeof StructuredChatDebugSnapshotSchema>;
88
+ /** Controls sensitive provenance included in a structured-chat debug snapshot. */
89
+ export interface InspectChatStateOptions {
90
+ readonly evidence?: "include" | "omit";
91
+ }
92
+ declare const InvalidChatDebugProjection_base: Schema.Class<InvalidChatDebugProjection, Schema.TaggedStruct<"InvalidChatDebugProjection", {
93
+ readonly reason: Schema.Literals<readonly ["invalid_options", "invalid_state", "invalid_answer_value", "invalid_snapshot"]>;
94
+ }>, import("effect/Cause").YieldableError>;
95
+ /** A chat state or answer could not be projected into safe debug JSON. */
96
+ export declare class InvalidChatDebugProjection extends InvalidChatDebugProjection_base {
97
+ }
98
+ /**
99
+ * Project one trusted chat definition and Type-side state into browser-safe
100
+ * inspector data without exposing choice values or raw Effect schemas.
101
+ */
102
+ export declare const inspectChatState: <const Name extends string, const Version extends number, const Stages extends ChatStageTuple>(chat: ChatDefinition<Name, Version, Stages>, state: ChatState<Name, Version, Stages>, options?: InspectChatStateOptions) => Effect.Effect<StructuredChatDebugSnapshot, InvalidChatDebugProjection>;
103
+ export {};