@crossworks/client-types 0.232.127 → 0.232.140

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.
@@ -0,0 +1,192 @@
1
+ /**
2
+ * @mantle/client-types · turns
3
+ *
4
+ * Live turn streaming and the ask_human questionnaire the runner queues raise.
5
+ *
6
+ * Split out of the 2548-line index.ts on 2026-09-02 (audit, tier 3) with the
7
+ * contents unchanged. index.ts re-exports every one of these, so the package's
8
+ * public surface is byte-identical — only the file a symbol lives in moved.
9
+ */
10
+
11
+ // ── Live turn streaming ─────────────────────────────────────────────────────────
12
+
13
+ /**
14
+ * The cross-client contract for live "what the agent is doing" updates during a
15
+ * turn — consumed identically by the web client and the Flutter companion (see
16
+ * `docs/live-turn-streaming.md`). One event stream unifies coarse status, tool
17
+ * activity, reasoning, and token deltas.
18
+ *
19
+ * This is the wire shape ONLY (zero-runtime, per this package's invariant): the
20
+ * server-side channel + publisher + schema-version constant live in
21
+ * `@mantle/turn-stream`; the producer stamps `v`/`seq`/`round`.
22
+ *
23
+ * Evolution rule: new `type`s and new `data` fields are additive (non-breaking) —
24
+ * a client ignores a `type` it doesn't recognise. A breaking change to an
25
+ * existing event's shape bumps `v` (`TURN_EVENT_SCHEMA_VERSION`).
26
+ */
27
+ export type TurnEventType =
28
+ | 'turn-start'
29
+ | 'status'
30
+ | 'tool-start'
31
+ | 'tool-end'
32
+ | 'reasoning-delta'
33
+ | 'text-delta'
34
+ | 'done'
35
+ | 'error';
36
+
37
+ /** A pending outbound message now exists; the client can bind UI to `turnId`. */
38
+ export interface TurnStartData {
39
+ agentSlug: string;
40
+ /** Resolved model id, when known at turn start (else null). */
41
+ model: string | null;
42
+ /** Durable `assistant_messages` id of the inbound (user) row, persisted before
43
+ * the model runs. Lets a client swap its optimistic user bubble for the
44
+ * canonical row without waiting on the POST. Optional (additive): a client
45
+ * that predates this field ignores it. */
46
+ inboundId?: string;
47
+ /** Durable `assistant_messages` id of the outbound (reply) row, inserted
48
+ * `pending` at turn start. This is the turn's authoritative reconciliation
49
+ * handle — the client binds the reply bubble to it and, on `done`, reads the
50
+ * final text from this row (vs. the advisory streamed buffer). Optional. */
51
+ outboundId?: string;
52
+ }
53
+
54
+ /** A short "what it's doing now" line ("Searching your brain…"). `kind` is an
55
+ * optional coarse bucket the UI can theme/iconify. `stepId` ties together the
56
+ * grounded line and its later narrated upgrade for the SAME step, so the client
57
+ * replaces the line in place rather than appending a duplicate. */
58
+ export interface TurnStatusData {
59
+ label: string;
60
+ kind?: string;
61
+ /** Stable id for the step this status describes. Two events sharing a stepId
62
+ * are the same step (grounded → narrated); the client upserts by it. */
63
+ stepId?: string;
64
+ /** Present (true) only on the narrator's rephrased line for a step — the warm
65
+ * first-person paragraph. Grounded lines omit it. Lets clients keep narrated
66
+ * text visible while later grounded lines tick past. */
67
+ narrated?: true;
68
+ }
69
+
70
+ /** A tool round began. `summary` is an optional one-line, secret-free preview. */
71
+ export interface TurnToolStartData {
72
+ name: string;
73
+ summary?: string;
74
+ }
75
+
76
+ /** A tool round finished (`ok=false` = it errored — the turn may still recover). */
77
+ export interface TurnToolEndData {
78
+ name: string;
79
+ ok: boolean;
80
+ }
81
+
82
+ /** A chunk of the model's reasoning stream (raw; may be curated before display). */
83
+ export interface TurnReasoningDeltaData {
84
+ text: string;
85
+ }
86
+
87
+ /** A chunk of the visible reply text. */
88
+ export interface TurnTextDeltaData {
89
+ text: string;
90
+ }
91
+
92
+ /** Terminal success. The client now reconciles against the durable message row;
93
+ * the streamed text is advisory, the DB row is authoritative. */
94
+ export interface TurnDoneData {
95
+ status: 'complete';
96
+ /** Real output-token total for the whole turn (summed across rounds). The
97
+ * client shows a streamed char-based estimate while the reply types out, then
98
+ * swaps it for this exact figure on `done`. Optional + additive: absent when
99
+ * no provider reported usage, or from a producer that predates the field. */
100
+ tokensOut?: number;
101
+ }
102
+
103
+ /** Terminal failure. */
104
+ export interface TurnErrorData {
105
+ status: 'failed';
106
+ message: string;
107
+ }
108
+
109
+ /** Fields every turn event carries. */
110
+ export interface TurnEventBase {
111
+ /** Schema version (`TURN_EVENT_SCHEMA_VERSION` at emit time). */
112
+ v: number;
113
+ /** Durable turn id = the outbound `assistant_messages` id. Stable for the turn. */
114
+ turnId: string;
115
+ /** Monotonic per-turn sequence — the SSE `id:` field and the resume cursor. */
116
+ seq: number;
117
+ /** Tool-loop round this event belongs to (0 = before the first round). */
118
+ round: number;
119
+ }
120
+
121
+ /** One live turn event. Discriminated on `type`; `data` is the matching payload. */
122
+ export type TurnEvent =
123
+ | (TurnEventBase & { type: 'turn-start'; data: TurnStartData })
124
+ | (TurnEventBase & { type: 'status'; data: TurnStatusData })
125
+ | (TurnEventBase & { type: 'tool-start'; data: TurnToolStartData })
126
+ | (TurnEventBase & { type: 'tool-end'; data: TurnToolEndData })
127
+ | (TurnEventBase & { type: 'reasoning-delta'; data: TurnReasoningDeltaData })
128
+ | (TurnEventBase & { type: 'text-delta'; data: TurnTextDeltaData })
129
+ | (TurnEventBase & { type: 'done'; data: TurnDoneData })
130
+ | (TurnEventBase & { type: 'error'; data: TurnErrorData });
131
+
132
+ // ── ask_human questionnaire (runner queues) ───────────────────────────────────
133
+ // THE single source of truth for the questionnaire contract. The plan parser
134
+ // (@mantle/tools) validates against these caps, the answer path (@mantle/runs)
135
+ // re-checks submissions against them, and the client renders whatever they
136
+ // admit. They lived in three places once and immediately disagreed — the
137
+ // client's id fallback diverged from the server's, and the client had no
138
+ // question cap while the API capped answers at 4, so a 5-question form
139
+ // rendered fine and then 400'd on submit.
140
+
141
+ /** One selectable answer. `description` is the muted subtext on the chip. */
142
+ export interface AskHumanFormOption {
143
+ label: string;
144
+ description?: string;
145
+ }
146
+
147
+ /** One sub-question of a questionnaire. `id` is the routing key answers are
148
+ * submitted under; `header` is the short chip shown beside the question. */
149
+ export interface AskHumanFormQuestion {
150
+ id: string;
151
+ header?: string;
152
+ question: string;
153
+ options: AskHumanFormOption[];
154
+ multi_select?: boolean;
155
+ /** Free-text escape. Defaults ON — a question whose options don't fit and
156
+ * offers no way to say so forces a wrong answer. */
157
+ allow_other?: boolean;
158
+ }
159
+
160
+ export interface AskHumanForm {
161
+ questions: AskHumanFormQuestion[];
162
+ }
163
+
164
+ /** One answered sub-question, as submitted to `PATCH /api/pending/:id` and
165
+ * `pending_approve`. `question` is the form question's `id`. */
166
+ export interface AskHumanFormAnswer {
167
+ question: string;
168
+ selected: string[];
169
+ other?: string;
170
+ }
171
+
172
+ /**
173
+ * Caps on a questionnaire. These are a CONTRACT, not advice: every answer
174
+ * surface renders whatever the parser admits, so an unbounded form is an
175
+ * unanswerable screen — and a cap enforced on only one side is a 400 the
176
+ * operator can't act on.
177
+ */
178
+ export const ASK_HUMAN_FORM_LIMITS = {
179
+ /** Ask more than this and the answers to the first few probably change what
180
+ * you still need to ask — use a later `ask_human` step. */
181
+ maxQuestions: 4,
182
+ maxOptions: 8,
183
+ /** A header renders as a chip, not a sentence. */
184
+ maxHeaderChars: 24,
185
+ maxQuestionChars: 300,
186
+ maxLabelChars: 80,
187
+ maxDescriptionChars: 200,
188
+ maxOtherChars: 2_000,
189
+ /** The form rides in `run_items.payload` AND the pending row's args, and
190
+ * both are read into prompts. */
191
+ maxFormJsonBytes: 8_000,
192
+ } as const;