@ego-z/contracts 0.15.8 → 0.15.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ego-z/contracts",
3
- "version": "0.15.8",
3
+ "version": "0.15.10",
4
4
  "description": "Wire-format type contracts shared between EgoZ backend, SDK, MCP and console. Type-only — no runtime artifacts.",
5
5
  "types": "./src/index.d.ts",
6
6
  "exports": {
package/src/ask.d.ts CHANGED
@@ -6,7 +6,8 @@
6
6
  */
7
7
 
8
8
  import type { Intent, JsonSchemaDefinition, ResponseFormat, TokenUsage } from './envelope';
9
- import type { InitContextUi } from './draft-conv';
9
+ import type { InitContextItem, InitContextUi } from './draft-conv';
10
+ import type { AskQuestionSet } from './questions';
10
11
 
11
12
  // ============================================================================
12
13
  // /ask — request
@@ -189,6 +190,27 @@ export interface AskRequestBody {
189
190
  * with the stored `initContext` used for the whole conversation.
190
191
  */
191
192
  ui?: InitContextUi;
193
+
194
+ /**
195
+ * What the USER explicitly attached to this turn's context.
196
+ *
197
+ * ── A sibling of `ui`, never a part of it ────────────────────────────
198
+ *
199
+ * `ui` is a FACT about the screen; `contextItems` is an INTENTION. The
200
+ * user never edits `ui` — express "don't look at this" or "also consider
201
+ * this" here instead. Editing the fact to carry the intention is how a
202
+ * window the user merely hid ends up reported as one they closed.
203
+ *
204
+ * It is also strictly wider: the user can attach something that was never
205
+ * a window at all, which no amount of flagging inside `ui` could express.
206
+ *
207
+ * Send the full current list every turn, not a delta — a client-computed
208
+ * delta needs a client-held baseline, and that baseline is wrong after a
209
+ * reload, a retry or a reconnect.
210
+ *
211
+ * Optional and additive: omit it and nothing changes.
212
+ */
213
+ contextItems?: InitContextItem[];
192
214
  }
193
215
 
194
216
  /**
@@ -299,6 +321,21 @@ export interface AskResponseData {
299
321
  * consumers that ignore it are unaffected (Path-B native-approvals path).
300
322
  */
301
323
  pendingActions?: Array<{ approvalId: string; toolName: string; summary: string }>;
324
+ /**
325
+ * A structured set of questions the agent is asking this turn, for the
326
+ * consumer to render as a picker instead of leaving the user to re-type a
327
+ * choice from a numbered list in `answer`.
328
+ *
329
+ * Present only on a turn that actually asked; at most one set per turn.
330
+ * Same species as `pendingActions` — an out-of-band card the consumer
331
+ * renders and resolves separately — and, like it, additive: a consumer
332
+ * that ignores this field still gets a coherent `answer`, because the
333
+ * questions are also stated in the reply text.
334
+ *
335
+ * The answer comes back as an ordinary `AskRequestBody.message`; there is
336
+ * no structured answer channel. See `questions.d.ts` for why.
337
+ */
338
+ questionSet?: AskQuestionSet;
302
339
  /**
303
340
  * Per-end-user learning observability (Phase 15 / exURM). Present only when
304
341
  * learning was ACTIVE for the turn (tenant opt-in + `externalUserId` present
@@ -58,6 +58,48 @@ export type InitContextViewport = 'mobile' | 'desktop';
58
58
  */
59
59
  export type InitContextAudience = 'operator' | 'customer';
60
60
 
61
+ /**
62
+ * One thing the user explicitly put into the chat's context.
63
+ *
64
+ * ── The caller resolves it; EgoZ never fetches ───────────────────────────
65
+ *
66
+ * You look the item up and send what it IS. EgoZ never resolves `kind + id`
67
+ * against your data model — doing so would put one integration's vocabulary
68
+ * into a shared contract, and "you can attach anything" would stop being
69
+ * true the day you invented a type we hadn't shipped. **A new `kind` costs
70
+ * zero EgoZ changes**, which is the test of whether this is genuinely
71
+ * open-ended rather than a list we happen to support.
72
+ */
73
+ export interface InitContextItem {
74
+ /**
75
+ * Your own type name — `"product"`, `"invoice"`, anything you like.
76
+ *
77
+ * ⚠️ Free-form, and **never rendered into the prompt**. Both halves go
78
+ * together: free-form so a new kind needs no change at either end, and
79
+ * unrendered because free-form text reaching the instruction block is an
80
+ * injection surface — a `kind` of "ignore previous instructions" would
81
+ * otherwise be printed into a system prompt. Used only to group items.
82
+ */
83
+ kind?: string;
84
+
85
+ /** Your own id for the item. Structural; also never rendered. */
86
+ id?: string;
87
+
88
+ /**
89
+ * Human-readable name — `"Official Chelsea Home Shirt"`. **Required in
90
+ * practice**: it is the only part the model reads, so an item without one
91
+ * is dropped rather than rendered as an id.
92
+ */
93
+ label?: string;
94
+
95
+ /**
96
+ * A short description of the item. Optional, and truncated rather than
97
+ * rejected if long — but keep it to what the thing IS, not its full
98
+ * record. Everything here displaces conversation from the context window.
99
+ */
100
+ excerpt?: string;
101
+ }
102
+
61
103
  /** One open window in the caller's UI. */
62
104
  export interface InitContextOpenWindow {
63
105
  /**
package/src/index.d.ts CHANGED
@@ -26,6 +26,9 @@
26
26
  * `AskResponseData`.
27
27
  * - `stream.d.ts` — full `AskStreamEvent` discriminated union +
28
28
  * every variant.
29
+ * - `questions.d.ts` — the structured question set a turn can carry on
30
+ * `AskResponseData` / `AskStreamDoneEvent`:
31
+ * `AskQuestionSet`, `AskQuestion`, `AskAnswer`.
29
32
  * - `tool.d.ts` — `ToolWire`, `ToolCreateBody`, `ToolUpdateBody`,
30
33
  * `ToolListResponseData`, `ToolGetResponseData`,
31
34
  * `ToolPreview`, vocabularies (`ToolType`,
@@ -91,6 +94,7 @@
91
94
  export * from './envelope';
92
95
  export * from './ask';
93
96
  export * from './stream';
97
+ export * from './questions';
94
98
  export * from './tool';
95
99
  export * from './rag';
96
100
  export * from './api-key';
@@ -0,0 +1,120 @@
1
+ /**
2
+ * @ego-z/contracts — the structured question set a turn can put on the wire.
3
+ *
4
+ * When the agent needs the user to CHOOSE rather than to read, it emits one of
5
+ * these instead of a numbered list in prose. The consumer renders a picker and
6
+ * feeds the selection back as the next turn's `message`.
7
+ *
8
+ * ── Why this is a wire type and not a prompt convention ──────────────────
9
+ *
10
+ * The obvious implementation is a markup token the model is taught to emit
11
+ * (`<Questions>…`) and the consumer parses. That works exactly as long as one
12
+ * provider's prompt keeps honouring it — it is an AUTHORING shape, and it
13
+ * dies on a provider move, silently, in the consumer.
14
+ *
15
+ * So the parse belongs to EgoZ and the wire carries the result. However a
16
+ * provider expresses the intent — a tool call today, a JSON mode response
17
+ * tomorrow — it is normalised here before it leaves the backend, and no
18
+ * consumer ever sees provider-shaped output. Moving providers rewrites one
19
+ * adapter rather than every renderer.
20
+ *
21
+ * ── Ids are minted by the emitter, never derived from labels ─────────────
22
+ *
23
+ * A consumer that keys an answer by its label breaks the first time a
24
+ * provider emits `**#b49c54**` where it used to emit `#b49c54`. Every id here
25
+ * comes from the emission side: a tool's own domain ids where a tool supplied
26
+ * the options, EgoZ-minted otherwise. The model never supplies one — a
27
+ * model-minted id looks stable and isn't.
28
+ *
29
+ * ── The answer does NOT come back through this type ──────────────────────
30
+ *
31
+ * There is no answer field on `AskRequestBody`, deliberately. The model has to
32
+ * READ the pick, and only `message` reaches the prompt, so the answer travels
33
+ * as ordinary message text; the ids ride on `metadata` for the consumer's own
34
+ * correlation. That split is why a question set stays answerable long after
35
+ * the turn that produced it — the text means the same thing tomorrow.
36
+ */
37
+
38
+ /**
39
+ * One set of questions the agent is asking this turn — the whole card.
40
+ *
41
+ * At most one per turn. A turn that would produce a second set is refused at
42
+ * emission rather than overwriting the first, so a consumer can treat "a set
43
+ * arrived" and "the set for this turn" as the same thing.
44
+ */
45
+ export interface AskQuestionSet {
46
+ /**
47
+ * Correlates an answer back to the turn that asked.
48
+ *
49
+ * **Opaque.** No ordering, no derivability from content, and NOT stable
50
+ * across turns: a retried turn produces a different set with different
51
+ * ids, because it is a different emission. A consumer that persists a set
52
+ * keys on this and never assumes two turns share it.
53
+ */
54
+ id: string;
55
+ /** Heading for the whole set, e.g. "A few details before I book that". */
56
+ title?: string;
57
+ /**
58
+ * The language EgoZ actually resolved for this turn — not the one the
59
+ * caller requested.
60
+ *
61
+ * REQUIRED, and it is doing more work than it looks like: it is the only
62
+ * thing that can set text direction. `dir="auto"` infers direction from
63
+ * the first strong character and therefore gets a Hebrew question whose
64
+ * answers are hex colours exactly backwards. Optionality here would mean
65
+ * "guess the direction", which is the bug rather than the fallback.
66
+ */
67
+ locale: string;
68
+ /** At least one. Rendered in order. */
69
+ questions: AskQuestion[];
70
+ }
71
+
72
+ /** One question within a set. */
73
+ export interface AskQuestion {
74
+ /** Opaque, emitter-minted — see `AskQuestionSet.id`. */
75
+ id: string;
76
+ /**
77
+ * What is being asked, e.g. "Which shade?".
78
+ *
79
+ * Must stand alone. It is the only context an answer's `label` gets, both
80
+ * in the UI and in the sentence the consumer composes when the user picks.
81
+ */
82
+ title: string;
83
+ /**
84
+ * The offered answers, in display order. Never empty — a question with
85
+ * nothing to choose between is asked in prose, not in a card, and is
86
+ * rejected at emission.
87
+ */
88
+ answers: AskAnswer[];
89
+ /**
90
+ * Whether this question also accepts a free-text answer — the "something
91
+ * else" slot. Per-question, not per-set: a set can mix a chip question
92
+ * with one that invites a note.
93
+ *
94
+ * It EXTENDS `answers`, never replaces it. Absent means chips only.
95
+ */
96
+ allowCustom?: boolean;
97
+ /**
98
+ * Whether more than one answer may be selected. Absent means single-select.
99
+ *
100
+ * Set by whoever holds the options and knows if they compose — never
101
+ * inferred by a consumer from how the question is phrased.
102
+ */
103
+ multi?: boolean;
104
+ }
105
+
106
+ /** One selectable answer. */
107
+ export interface AskAnswer {
108
+ /** Opaque, emitter-minted — see `AskQuestionSet.id`. */
109
+ id: string;
110
+ /**
111
+ * What the user sees, already in `AskQuestionSet.locale`.
112
+ *
113
+ * Must be meaningful on its own, because it is what ends up in the next
114
+ * turn's message text: `#b49c54` reads fine, `"Option 2"` does not — that
115
+ * one only means anything against a rendered list the model never sees.
116
+ */
117
+ label: string;
118
+ /** Optional secondary line, e.g. a price or a duration. */
119
+ description?: string;
120
+ }
package/src/stream.d.ts CHANGED
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import type { Intent, TokenUsage } from './envelope';
15
+ import type { AskQuestionSet } from './questions';
15
16
 
16
17
  /**
17
18
  * Discriminated union of every event the streaming `/ask` endpoint emits.
@@ -323,6 +324,26 @@ export interface AskStreamDoneEvent {
323
324
  * Approve/Reject card without parsing the final `data` payload separately.
324
325
  */
325
326
  pendingActions?: Array<{ approvalId: string; toolName: string; summary: string }>;
327
+ /**
328
+ * A structured set of questions the agent asked this turn. Same semantics
329
+ * + shape as `AskResponseData.questionSet` — surfaced on the terminal
330
+ * event so streaming consumers can render the picker without parsing the
331
+ * final `data` payload separately.
332
+ *
333
+ * ── Why the terminal frame and not a mid-stream event ────────────────
334
+ *
335
+ * A card that offers choices is only actionable once the turn is settled,
336
+ * and consumers already gate it that way — the Console suppresses its
337
+ * choice row until the stream completes. A dedicated mid-stream variant
338
+ * would be a channel every consumer discards until `done` anyway, bought
339
+ * at the price of a new member of this union (non-breaking for producers,
340
+ * breaking for consumers that exhaustively switch on `event.type`).
341
+ *
342
+ * The cost, chosen rather than overlooked: a turn that terminates with
343
+ * `error` after producing a set loses the card. `pendingActions` carries
344
+ * the identical hole; the retry re-runs the turn.
345
+ */
346
+ questionSet?: AskQuestionSet;
326
347
  /**
327
348
  * Per-end-user learning observability (Phase 15 / exURM). Same semantics as
328
349
  * `AskResponseData.{memoryInjected,memoryProposed}` — surfaced on the