@popcomputer/structured-chat 0.2.0-rc.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.
package/README.md CHANGED
@@ -600,6 +600,59 @@ that expose message editing, regeneration, or persistent branching should pair
600
600
  it with an application-owned branch/session policy rather than treating browser
601
601
  history as authoritative.
602
602
 
603
+ ## Inspect development state
604
+
605
+ The optional debug inspector shows the current stage, stage progress, required
606
+ fields, accepted values, issued questions, and supporting evidence. It uses a
607
+ small package-owned panel inspired by DialKit, without adding DialKit or another
608
+ runtime dependency.
609
+
610
+ Debug data uses a separate, explicit response contract. Select it on an
611
+ authenticated development endpoint:
612
+
613
+ ```ts
614
+ import {
615
+ presentChatDebugReply,
616
+ presentChatReply,
617
+ } from "@popcomputer/structured-chat"
618
+
619
+ const response = debugAccessGranted
620
+ ? yield* presentChatDebugReply(
621
+ ResourceFinder,
622
+ { ...reply, sessionId: publicSessionId },
623
+ { inspection: { evidence: "include" } },
624
+ )
625
+ : yield* presentChatReply({ ...reply, sessionId: publicSessionId })
626
+ ```
627
+
628
+ Connect that endpoint to the inspector store:
629
+
630
+ ```tsx
631
+ import { makeAssistantChatModelAdapter } from "@popcomputer/structured-chat/assistant-ui"
632
+ import {
633
+ createStructuredChatDebugStore,
634
+ StructuredChatDebugPanel,
635
+ } from "@popcomputer/structured-chat/assistant-ui/debug"
636
+
637
+ const debugStore = createStructuredChatDebugStore()
638
+ const model = makeAssistantChatModelAdapter({
639
+ endpoint: "/api/resource-finder/debug/turn",
640
+ onDebugSnapshot: debugStore.receive,
641
+ })
642
+
643
+ export function ResourceFinderDebugPanel() {
644
+ return <StructuredChatDebugPanel store={debugStore} />
645
+ }
646
+ ```
647
+
648
+ Without `onDebugSnapshot`, the normal adapter continues to reject a response
649
+ containing debug data. Hiding or unmounting the panel is not an authorization
650
+ boundary: the server must decide whether to emit the debug response. Use
651
+ `evidence: "omit"` when transcript quotes should not cross that boundary.
652
+ Create one store per chat runtime; it reflects the most recently completed
653
+ debug response. Observer failures are isolated and never change the outcome of
654
+ the persisted chat turn.
655
+
603
656
  ## Plan without executing
604
657
 
605
658
  The default remains one call:
@@ -3,7 +3,7 @@ import type { AnswerDefinition, AnswerDefinitionContract, AnswerMode } from "./a
3
3
  import { ChatModelUnavailable, StructuredChatModel, type UnsupportedModelToolSchema, type UntrustedMessage } from "./model.js";
4
4
  import type { ModelGuardError, ModelGuardRequirements, ModelGuardTuple } from "./model-guard.js";
5
5
  import { InvalidToolCall, type InvalidToolProjection } from "./tool.js";
6
- import type { AdaptiveChoiceQuestion, ChoiceQuestion, QuestionChoice } from "./question.js";
6
+ import type { AdaptiveChoiceQuestion, ChoiceQuestion, QuestionDefinitionContract, QuestionChoice } from "./question.js";
7
7
  import { type StructuredDefinition } from "./definition.js";
8
8
  /** Safe reason that a collect-stage model proposal was rejected. */
9
9
  export declare const InvalidCollectStageResponseReasonSchema: Schema.Literals<readonly ["invalid_evidence", "invalid_repair"]>;
@@ -148,15 +148,31 @@ export interface CollectStageRuntime {
148
148
  readonly messages: ReadonlyArray<UntrustedMessage>;
149
149
  }) => Effect.Effect<RuntimeCollectStageTurn, unknown, unknown>;
150
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
+ }
151
163
  declare const collectStageRuntime: unique symbol;
164
+ declare const collectStageInspection: unique symbol;
152
165
  /** Minimum sealed collect-stage shape accepted by a chat definition. */
153
166
  export interface CollectStageDefinitionContract extends StructuredDefinition<"collect_stage"> {
154
167
  readonly _tag: "CollectStage";
155
168
  readonly name: string;
156
169
  readonly [collectStageRuntime]: CollectStageRuntime;
170
+ readonly [collectStageInspection]: CollectStageInspection;
157
171
  }
158
172
  /** @internal Read the erased runtime from an authentic collect stage. */
159
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;
160
176
  /** Shared conversational policy for questions in one collect stage. */
161
177
  export interface CollectQuestionPolicy {
162
178
  /** Trusted style guidance applied to every adaptive question in this stage. */
@@ -19,8 +19,11 @@ export class InvalidCollectStageResponse extends Schema.TaggedError()("InvalidCo
19
19
  export class AnswerValidationRejected extends Data.TaggedError("AnswerValidationRejected") {
20
20
  }
21
21
  const collectStageRuntime = Symbol("@popcomputer/structured-chat/CollectStageRuntime");
22
+ const collectStageInspection = Symbol("@popcomputer/structured-chat/CollectStageInspection");
22
23
  /** @internal Read the erased runtime from an authentic collect stage. */
23
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];
24
27
  const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
25
28
  /** Define one deterministic schema-derived fact collection stage. */
26
29
  export const defineCollectStage = (definition) => {
@@ -171,6 +174,18 @@ export const defineCollectStage = (definition) => {
171
174
  accepted: {},
172
175
  asked: {},
173
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
+ });
174
189
  // SAFETY: when guards are omitted, Guards uses its readonly [] default; an
175
190
  // explicitly supplied tuple is returned unchanged.
176
191
  const guards = definition.guards ?? cast([]);
@@ -630,6 +645,9 @@ export const defineCollectStage = (definition) => {
630
645
  },
631
646
  }),
632
647
  run,
648
+ [collectStageInspection]: {
649
+ fields: inspectionFields,
650
+ },
633
651
  [collectStageRuntime]: {
634
652
  initialState,
635
653
  stateSchema,
@@ -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 {};
@@ -0,0 +1,276 @@
1
+ import { cast, Effect, Schema } from "effect";
2
+ import { AnswerModeSchema } from "./answer.js";
3
+ import { ChatNameSchema, ChatVersionSchema, } from "./chat.js";
4
+ import { readCollectStageInspection, } from "./collect-stage.js";
5
+ import { JsonValueSchema } from "./json-value.js";
6
+ import { readCommandStageRuntime, readToolStageRuntime, ToolStageAfterExecutionSchema, } from "./stage.js";
7
+ import { StageNameSchema } from "./stage-name.js";
8
+ import { ToolNameSchema } from "./tool.js";
9
+ const DebugIndexSchema = Schema.Natural;
10
+ const DebugIssuedQuestionSchema = Schema.Struct({
11
+ messageIndex: DebugIndexSchema,
12
+ text: Schema.String,
13
+ });
14
+ const DebugAnswerEvidenceSchema = Schema.Struct({
15
+ messageIndex: DebugIndexSchema,
16
+ quote: Schema.String,
17
+ });
18
+ const DebugQuestionSchema = Schema.Union([
19
+ Schema.Struct({
20
+ _tag: Schema.Literal("FixedQuestion"),
21
+ text: Schema.String,
22
+ }),
23
+ Schema.Struct({
24
+ _tag: Schema.Literal("AdaptiveQuestion"),
25
+ goal: Schema.String,
26
+ fallback: Schema.String,
27
+ }),
28
+ Schema.Struct({
29
+ _tag: Schema.Literal("AdaptiveChoiceQuestion"),
30
+ prompt: Schema.String,
31
+ minimumOptions: Schema.Natural,
32
+ maximumOptions: Schema.Natural,
33
+ fallbackOptions: Schema.Array(Schema.String),
34
+ }),
35
+ Schema.Struct({
36
+ _tag: Schema.Literal("ChoiceQuestion"),
37
+ text: Schema.String,
38
+ options: Schema.Array(Schema.Struct({
39
+ label: Schema.String,
40
+ })),
41
+ }),
42
+ ]);
43
+ const DebugFieldStateSchema = Schema.Union([
44
+ Schema.Struct({
45
+ _tag: Schema.Literal("Missing"),
46
+ }),
47
+ Schema.Struct({
48
+ _tag: Schema.Literal("Asked"),
49
+ issuedQuestion: DebugIssuedQuestionSchema,
50
+ }),
51
+ Schema.Struct({
52
+ _tag: Schema.Literal("Accepted"),
53
+ value: JsonValueSchema,
54
+ evidence: Schema.NullOr(DebugAnswerEvidenceSchema),
55
+ issuedQuestion: Schema.NullOr(DebugIssuedQuestionSchema),
56
+ }),
57
+ ]);
58
+ const DebugFieldSchema = Schema.Struct({
59
+ field: Schema.String,
60
+ mode: AnswerModeSchema,
61
+ description: Schema.String,
62
+ question: DebugQuestionSchema,
63
+ state: DebugFieldStateSchema,
64
+ });
65
+ const DebugStageStatusSchema = Schema.Literals([
66
+ "complete",
67
+ "current",
68
+ "upcoming",
69
+ ]);
70
+ const DebugStageBaseFields = {
71
+ index: DebugIndexSchema,
72
+ name: StageNameSchema,
73
+ status: DebugStageStatusSchema,
74
+ repairPending: Schema.Boolean,
75
+ };
76
+ const DebugStageSchema = Schema.Union([
77
+ Schema.Struct({
78
+ _tag: Schema.Literal("CollectStage"),
79
+ ...DebugStageBaseFields,
80
+ satisfiedFields: Schema.Natural,
81
+ totalFields: Schema.Natural,
82
+ fields: Schema.Array(DebugFieldSchema),
83
+ }),
84
+ Schema.Struct({
85
+ _tag: Schema.Literal("ToolStage"),
86
+ ...DebugStageBaseFields,
87
+ tools: Schema.Array(ToolNameSchema),
88
+ afterExecution: ToolStageAfterExecutionSchema,
89
+ }),
90
+ Schema.Struct({
91
+ _tag: Schema.Literal("CommandStage"),
92
+ ...DebugStageBaseFields,
93
+ command: ToolNameSchema,
94
+ }),
95
+ ]);
96
+ /** Runtime schema for one JSON-safe structured-chat debug snapshot. */
97
+ export const StructuredChatDebugSnapshotSchema = Schema.Struct({
98
+ schemaVersion: Schema.Literal(1),
99
+ chat: Schema.Struct({
100
+ name: ChatNameSchema,
101
+ version: ChatVersionSchema,
102
+ }),
103
+ status: Schema.Literals(["active", "complete"]),
104
+ currentStage: Schema.Struct({
105
+ index: DebugIndexSchema,
106
+ name: StageNameSchema,
107
+ kind: Schema.Literals(["collect", "tool", "command"]),
108
+ }),
109
+ stages: Schema.Array(DebugStageSchema),
110
+ });
111
+ const InspectChatStateOptionsSchema = Schema.Struct({
112
+ evidence: Schema.optionalKey(Schema.Literals(["include", "omit"])),
113
+ });
114
+ const InvalidChatDebugProjectionReasonSchema = Schema.Literals([
115
+ "invalid_options",
116
+ "invalid_state",
117
+ "invalid_answer_value",
118
+ "invalid_snapshot",
119
+ ]);
120
+ /** A chat state or answer could not be projected into safe debug JSON. */
121
+ export class InvalidChatDebugProjection extends Schema.TaggedError()("InvalidChatDebugProjection", { reason: InvalidChatDebugProjectionReasonSchema }) {
122
+ }
123
+ const invalidProjection = (reason) => new InvalidChatDebugProjection({ reason });
124
+ const projectQuestion = (question) => {
125
+ switch (question._tag) {
126
+ case "FixedQuestion":
127
+ return { _tag: question._tag, text: question.text };
128
+ case "AdaptiveQuestion":
129
+ return {
130
+ _tag: question._tag,
131
+ goal: question.goal,
132
+ fallback: question.fallback,
133
+ };
134
+ case "AdaptiveChoiceQuestion":
135
+ return {
136
+ _tag: question._tag,
137
+ prompt: question.prompt,
138
+ minimumOptions: question.minimumOptions,
139
+ maximumOptions: question.maximumOptions,
140
+ fallbackOptions: question.fallbackOptions,
141
+ };
142
+ case "ChoiceQuestion":
143
+ return {
144
+ _tag: question._tag,
145
+ text: question.text,
146
+ options: question.options.map(({ label }) => ({ label })),
147
+ };
148
+ }
149
+ };
150
+ const stageKind = (stage) => {
151
+ switch (stage._tag) {
152
+ case "CollectStage":
153
+ return "collect";
154
+ case "ToolStage":
155
+ return "tool";
156
+ case "CommandStage":
157
+ return "command";
158
+ }
159
+ };
160
+ const stageStatus = (state, index) => {
161
+ if (index === state.stage) {
162
+ return state.status === "complete" ? "complete" : "current";
163
+ }
164
+ if (index < state.stage) {
165
+ return "complete";
166
+ }
167
+ return "upcoming";
168
+ };
169
+ /**
170
+ * Project one trusted chat definition and Type-side state into browser-safe
171
+ * inspector data without exposing choice values or raw Effect schemas.
172
+ */
173
+ export const inspectChatState = (chat, state, options = {}) => Effect.gen(function* () {
174
+ const parsedOptions = yield* Schema.decodeUnknownEffect(InspectChatStateOptionsSchema)(options, { onExcessProperty: "error" }).pipe(Effect.mapError(() => invalidProjection("invalid_options")));
175
+ const parsedState = yield* Schema.decodeUnknownEffect(Schema.toType(chat.stateSchema))(state, { onExcessProperty: "error" }).pipe(Effect.mapError(() => invalidProjection("invalid_state")));
176
+ // SAFETY: this definition's exact state schema parsed the envelope and all
177
+ // named collect-stage states immediately above; only tuple correlations are
178
+ // erased for definition-ordered read-only projection.
179
+ const runtimeState = cast(parsedState);
180
+ const currentStage = chat.stages[runtimeState.stage];
181
+ if (currentStage === undefined) {
182
+ return yield* Effect.fail(invalidProjection("invalid_state"));
183
+ }
184
+ const stages = [];
185
+ for (const [index, stage] of chat.stages.entries()) {
186
+ const repairPending = runtimeState.repair?.pendingStages.includes(index) ?? false;
187
+ if (stage._tag === "ToolStage") {
188
+ const runtime = readToolStageRuntime(stage);
189
+ stages.push({
190
+ _tag: "ToolStage",
191
+ index,
192
+ name: stage.name,
193
+ status: stageStatus(runtimeState, index),
194
+ repairPending,
195
+ tools: runtime.toolNames,
196
+ afterExecution: runtime.afterExecution,
197
+ });
198
+ continue;
199
+ }
200
+ if (stage._tag === "CommandStage") {
201
+ stages.push({
202
+ _tag: "CommandStage",
203
+ index,
204
+ name: stage.name,
205
+ status: stageStatus(runtimeState, index),
206
+ repairPending,
207
+ command: readCommandStageRuntime(stage).commandName,
208
+ });
209
+ continue;
210
+ }
211
+ const collectState = runtimeState.stages[stage.name];
212
+ if (collectState === undefined) {
213
+ return yield* Effect.fail(invalidProjection("invalid_state"));
214
+ }
215
+ const inspection = readCollectStageInspection(stage);
216
+ const fields = [];
217
+ let satisfiedFields = 0;
218
+ for (const field of inspection.fields) {
219
+ const accepted = collectState.accepted[field.field];
220
+ const issuedQuestion = collectState.asked[field.field];
221
+ const fieldBase = {
222
+ field: field.field,
223
+ mode: field.mode,
224
+ description: field.description,
225
+ question: projectQuestion(field.question),
226
+ };
227
+ if (accepted === undefined) {
228
+ fields.push(issuedQuestion === undefined
229
+ ? { ...fieldBase, state: { _tag: "Missing" } }
230
+ : {
231
+ ...fieldBase,
232
+ state: {
233
+ _tag: "Asked",
234
+ issuedQuestion,
235
+ },
236
+ });
237
+ continue;
238
+ }
239
+ const encoded = yield* field.encodeValue(accepted.value).pipe(Effect.mapError(() => invalidProjection("invalid_answer_value")));
240
+ const value = yield* Schema.decodeUnknownEffect(JsonValueSchema)(encoded, { onExcessProperty: "error" }).pipe(Effect.mapError(() => invalidProjection("invalid_answer_value")));
241
+ satisfiedFields += 1;
242
+ fields.push({
243
+ ...fieldBase,
244
+ state: {
245
+ _tag: "Accepted",
246
+ value,
247
+ evidence: (parsedOptions.evidence ?? "include") === "include"
248
+ ? accepted.evidence
249
+ : null,
250
+ issuedQuestion: issuedQuestion ?? null,
251
+ },
252
+ });
253
+ }
254
+ stages.push({
255
+ _tag: "CollectStage",
256
+ index,
257
+ name: stage.name,
258
+ status: stageStatus(runtimeState, index),
259
+ repairPending,
260
+ satisfiedFields,
261
+ totalFields: inspection.fields.length,
262
+ fields,
263
+ });
264
+ }
265
+ return yield* Schema.decodeUnknownEffect(StructuredChatDebugSnapshotSchema)({
266
+ schemaVersion: 1,
267
+ chat: { name: chat.name, version: chat.version },
268
+ status: runtimeState.status,
269
+ currentStage: {
270
+ index: runtimeState.stage,
271
+ name: currentStage.name,
272
+ kind: stageKind(currentStage),
273
+ },
274
+ stages,
275
+ }, { onExcessProperty: "error" }).pipe(Effect.mapError(() => invalidProjection("invalid_snapshot")));
276
+ });