@ego-z/contracts 0.14.4 → 0.15.3
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 +1 -1
- package/src/draft-conv.d.ts +373 -0
- package/src/index.d.ts +11 -1
- package/src/stream.d.ts +48 -0
- package/src/tenant.d.ts +44 -0
- package/src/thread.d.ts +17 -8
package/package.json
CHANGED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ego-z/contracts — `POST /egoz/conversations/draft` wire types.
|
|
3
|
+
*
|
|
4
|
+
* The GHOSTED CONVERSATION START. When a tenant surface opens a new chat
|
|
5
|
+
* panel, it calls this before the user has typed anything. EgoZ mints a
|
|
6
|
+
* conversation id, stores the caller's init context against it as a draft,
|
|
7
|
+
* and returns quick suggested prompts — so the panel opens with a real id
|
|
8
|
+
* and something to click, rather than acquiring both mid-stream on the first
|
|
9
|
+
* message.
|
|
10
|
+
*
|
|
11
|
+
* No LLM turn is billed. Nothing is persisted as a user message: the init
|
|
12
|
+
* context is rendered into the system prompt as context, never as something
|
|
13
|
+
* the model answers.
|
|
14
|
+
*
|
|
15
|
+
* ── Two ids, and only one of them is an identity ─────────────────────────
|
|
16
|
+
*
|
|
17
|
+
* `conversationId` is EgoZ-minted and canonical — the only id a follow-up
|
|
18
|
+
* `/ask` ever carries. `draftKey` is a caller-generated dedupe token for one
|
|
19
|
+
* chat panel. They are deliberately separate: the thing that needs
|
|
20
|
+
* uniqueness (a panel's attempt to start a draft) is not the thing that
|
|
21
|
+
* needs identity (a conversation), and conflating them would change which id
|
|
22
|
+
* is canonical for every existing caller.
|
|
23
|
+
*
|
|
24
|
+
* ── Evolution ────────────────────────────────────────────────────────────
|
|
25
|
+
*
|
|
26
|
+
* Additive only. Every `initContext` field is optional, so a caller that
|
|
27
|
+
* hasn't implemented a field yet degrades one feature rather than failing a
|
|
28
|
+
* request. Unknown fields are TOLERATED, not rejected — but they are also
|
|
29
|
+
* not forwarded: the server keeps only what it models, because everything
|
|
30
|
+
* here is rendered into a system prompt and arbitrary caller JSON reaching
|
|
31
|
+
* that position is an injection surface, not a convenience. Callers with
|
|
32
|
+
* something unmodelled to say use `notes`.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type { ThreadStatus } from './thread';
|
|
36
|
+
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Init context — the snapshot that accompanies a start
|
|
39
|
+
// ============================================================================
|
|
40
|
+
|
|
41
|
+
/** Resolved colour scheme. Never a preference — see `InitContextUi.theme`. */
|
|
42
|
+
export type InitContextTheme = 'light' | 'dark';
|
|
43
|
+
|
|
44
|
+
/** Viewport class. Deliberately not "device" — see `InitContextUi.viewport`. */
|
|
45
|
+
export type InitContextViewport = 'mobile' | 'desktop';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Which population a user belongs to — see `InitContextUser.audience`.
|
|
49
|
+
*
|
|
50
|
+
* - `'operator'` — runs the tenant's product. Admins, owners, staff,
|
|
51
|
+
* specialists, back-office users: whatever the deployment calls them.
|
|
52
|
+
* - `'customer'` — uses it. End users, buyers, patients, guests.
|
|
53
|
+
*
|
|
54
|
+
* Two values, not a list of job titles: the distinction that changes how an
|
|
55
|
+
* agent should speak is "do you run this, or do you use it". Finer-grained
|
|
56
|
+
* distinctions belong in `roles`, which is free-form precisely because that
|
|
57
|
+
* vocabulary is the deployment's, not EgoZ's.
|
|
58
|
+
*/
|
|
59
|
+
export type InitContextAudience = 'operator' | 'customer';
|
|
60
|
+
|
|
61
|
+
/** One open window in the caller's UI. */
|
|
62
|
+
export interface InitContextOpenWindow {
|
|
63
|
+
/** Epoch millis the window was opened. */
|
|
64
|
+
openedAt?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Human-readable window label, e.g. `"Blue Hoodie"`. Safe to render
|
|
67
|
+
* verbatim — unlike the window KEY, which embeds entity ids and is
|
|
68
|
+
* rendered only for the focused window.
|
|
69
|
+
*/
|
|
70
|
+
title?: string;
|
|
71
|
+
isFocused?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** One visited tab. `visitedAt` gives suggestion ranking a recency tiebreak. */
|
|
75
|
+
export interface InitContextVisitedTab {
|
|
76
|
+
/** Epoch millis the tab was last visited. */
|
|
77
|
+
visitedAt?: number;
|
|
78
|
+
isCurrent?: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* What the user is looking at.
|
|
83
|
+
*
|
|
84
|
+
* The first four fields are structurally identical to the AppNa console's
|
|
85
|
+
* `UserContext`, so a caller that already models an OS-style window system
|
|
86
|
+
* passes its context straight through with no mapper. They are declared here
|
|
87
|
+
* as EgoZ's own type rather than imported: EgoZ serves callers that are not
|
|
88
|
+
* window systems, and a public contract that mirrors one client's internal
|
|
89
|
+
* type makes every refactor of that client a breaking change here.
|
|
90
|
+
*
|
|
91
|
+
* RANKING USES FOCUS, NOT THE SET. "Has Orders open" is weak signal; "is
|
|
92
|
+
* looking at a product detail" is strong. `openWindows` is a secondary
|
|
93
|
+
* signal and `visitedTabs[].visitedAt` a tiebreak.
|
|
94
|
+
*/
|
|
95
|
+
export interface InitContextUi {
|
|
96
|
+
/** Coarse screen the user is on, e.g. `"ecommerce"`, `"orders"`. */
|
|
97
|
+
currentRouteId?: string | null;
|
|
98
|
+
/** Key of the focused window, e.g. `"ecommerce:productDetail:abc"`. */
|
|
99
|
+
focusedWindowId?: string | null;
|
|
100
|
+
openWindows?: Record<string, InitContextOpenWindow>;
|
|
101
|
+
visitedTabs?: Record<string, InitContextVisitedTab>;
|
|
102
|
+
/**
|
|
103
|
+
* The RESOLVED colour scheme — never a preference. A caller whose stored
|
|
104
|
+
* setting is "system" MUST resolve it against `prefers-color-scheme`
|
|
105
|
+
* before sending: `'system'` is a state EgoZ cannot evaluate and has
|
|
106
|
+
* nothing useful to say about, and it would be the value for a large
|
|
107
|
+
* share of users.
|
|
108
|
+
*/
|
|
109
|
+
theme?: InitContextTheme | null;
|
|
110
|
+
/**
|
|
111
|
+
* Viewport class — NOT a device. Callers derive it from a width
|
|
112
|
+
* breakpoint, so a narrowed desktop browser window reports `'mobile'`.
|
|
113
|
+
* Named for what it measures on purpose: `device` would invite the agent
|
|
114
|
+
* to say "since you're on your phone…" to someone who dragged their
|
|
115
|
+
* window narrow. A real device signal (touch capability, UA) would be a
|
|
116
|
+
* separate field with a separate source.
|
|
117
|
+
*/
|
|
118
|
+
viewport?: InitContextViewport | null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Who the user is.
|
|
123
|
+
*
|
|
124
|
+
* `name` is nullable: a phone-only signup genuinely has none, and the
|
|
125
|
+
* greeting has a no-name path rather than producing "Hello, null".
|
|
126
|
+
*
|
|
127
|
+
* ── `roles` is a SET, and it is ADDITIVE-ONLY ────────────────────────────
|
|
128
|
+
*
|
|
129
|
+
* A role gates which actions the agent offers, which makes a client-asserted
|
|
130
|
+
* role a client-asserted permission. It MUST be resolved by whoever
|
|
131
|
+
* authenticated the user; a gateway fills it from the verified session and
|
|
132
|
+
* ignores whatever the browser sent. The vocabulary belongs to the caller's
|
|
133
|
+
* product, so entries are free-form strings.
|
|
134
|
+
*
|
|
135
|
+
* It is an ARRAY, not a single value, because the underlying fact is a set
|
|
136
|
+
* and collapsing a set needs a precedence rule — a precedence rule absent
|
|
137
|
+
* from the contract is one each implementer invents privately, and an owner
|
|
138
|
+
* who also takes appointments then never sees the specialist suggestions.
|
|
139
|
+
*
|
|
140
|
+
* It may only ever WIDEN what the agent offers — add to the capability
|
|
141
|
+
* summary, boost ranking — and may never narrow either. The reason is a
|
|
142
|
+
* property of the source: on a permissions node where present means true
|
|
143
|
+
* and ABSENT MEANS UNKNOWN, `['Specialist']` does not mean "not an owner",
|
|
144
|
+
* it means "specialist is true and the rest is unknown". Every inference of
|
|
145
|
+
* the form *only a specialist, so hide X* is unsound on that data.
|
|
146
|
+
*
|
|
147
|
+
* The asymmetry also matters because the failure is invisible: a wrongly
|
|
148
|
+
* SHOWN suggestion gets reported, a wrongly HIDDEN one never does — the user
|
|
149
|
+
* just sees a less useful assistant and assumes that's what it is. Hard
|
|
150
|
+
* filtering belongs to `tenant.features`, which genuinely can say no.
|
|
151
|
+
*
|
|
152
|
+
* There is no `id` field: the end user is already identified at request
|
|
153
|
+
* level by `externalUserId`. One id, one place.
|
|
154
|
+
*/
|
|
155
|
+
export interface InitContextUser {
|
|
156
|
+
name?: string | null;
|
|
157
|
+
roles?: string[];
|
|
158
|
+
/**
|
|
159
|
+
* WHICH POPULATION this person belongs to — the people who RUN the
|
|
160
|
+
* tenant's product, or the people who USE it.
|
|
161
|
+
*
|
|
162
|
+
* Every EgoZ deployment serves one or both, and the agent should not
|
|
163
|
+
* speak to them the same way: an operator wants "I can help you manage
|
|
164
|
+
* your catalogue", a customer wants "I can help you track your order".
|
|
165
|
+
* Offering the first to the second is wrong in the opening sentence, and
|
|
166
|
+
* it reads as the product not knowing who it is talking to.
|
|
167
|
+
*
|
|
168
|
+
* Deliberately NOT a boolean, and deliberately not named after any one
|
|
169
|
+
* product's org chart. A boolean forces every deployment to express its
|
|
170
|
+
* populations as "staff / not-staff", which is one integration's
|
|
171
|
+
* vocabulary; and a boolean has no way to say "I did not resolve this",
|
|
172
|
+
* so absent and false become indistinguishable at exactly the moment the
|
|
173
|
+
* difference matters.
|
|
174
|
+
*
|
|
175
|
+
* ── The one signal allowed to NARROW ─────────────────────────────────
|
|
176
|
+
*
|
|
177
|
+
* `roles` may only widen, because its source generally cannot tell
|
|
178
|
+
* "false" from "not recorded". This field is different: it is the
|
|
179
|
+
* caller's own answer to a question they authoritatively know, so it may
|
|
180
|
+
* gate — it is the only field here that can.
|
|
181
|
+
*
|
|
182
|
+
* ── ABSENT MEANS `'customer'`. Fail closed. ──────────────────────────
|
|
183
|
+
*
|
|
184
|
+
* Read a missing value as the LESS privileged population, never as
|
|
185
|
+
* "unknown, assume operator". Callers that serve both populations often
|
|
186
|
+
* cannot distinguish them on every path, and the failure directions are
|
|
187
|
+
* not symmetric: greeting an operator as a customer is a smaller,
|
|
188
|
+
* self-correcting mistake than offering a customer administrative
|
|
189
|
+
* actions they will then fail to perform.
|
|
190
|
+
*/
|
|
191
|
+
audience?: InitContextAudience;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Language.
|
|
196
|
+
*
|
|
197
|
+
* TWO DIFFERENT FACTS ABOUT TWO DIFFERENT SUBJECTS — the distinction is
|
|
198
|
+
* load-bearing and was got wrong once already:
|
|
199
|
+
*
|
|
200
|
+
* - `selected` is **which language this person reads**. It is the only input
|
|
201
|
+
* to which language EgoZ replies in. The response echoes the RESOLVED
|
|
202
|
+
* value, so the caller and EgoZ cannot silently disagree about it.
|
|
203
|
+
* - `supported` is **which languages the tenant's CONTENT exists in** — a
|
|
204
|
+
* catalogue of the store's translations, not a list of languages the user
|
|
205
|
+
* understands. It is NEVER consulted when resolving the reply language:
|
|
206
|
+
* doing so gives a French-reading admin at an Arabic-only store Arabic
|
|
207
|
+
* chips, which is the resolver faithfully answering the wrong question.
|
|
208
|
+
*
|
|
209
|
+
* `supported` is still worth sending — it belongs in the prompt, where it
|
|
210
|
+
* makes "translate this product into Hebrew" a sensible suggestion for one
|
|
211
|
+
* tenant and a dead end for another.
|
|
212
|
+
*/
|
|
213
|
+
export interface InitContextLocale {
|
|
214
|
+
/** The language the USER reads, e.g. `"ar"`. Drives the reply language. */
|
|
215
|
+
selected?: string | null;
|
|
216
|
+
/**
|
|
217
|
+
* The languages the TENANT'S CONTENT exists in, e.g. `["en","ar","he"]`.
|
|
218
|
+
* Context for the agent; never used to resolve the reply language.
|
|
219
|
+
*/
|
|
220
|
+
supported?: string[];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The END TENANT this conversation belongs to.
|
|
225
|
+
*
|
|
226
|
+
* NOT necessarily the EgoZ tenant. On a gateway integration one EgoZ project
|
|
227
|
+
* fronts many of the gateway's tenants, so EgoZ's own tenant record
|
|
228
|
+
* describes the DEPLOYMENT and knows nothing about tenant "ABC". Which side
|
|
229
|
+
* is authoritative is declared once per EgoZ tenant (`tenantScope`), not
|
|
230
|
+
* inferred per request.
|
|
231
|
+
*
|
|
232
|
+
* `features` is the highest-value field here for suggestion quality: a
|
|
233
|
+
* per-tenant flag set EgoZ has no way to see. Without it the ranker either
|
|
234
|
+
* offers an action the tenant has switched off, or falls back to generic
|
|
235
|
+
* chips — the exact failure the ranking exists to avoid.
|
|
236
|
+
*/
|
|
237
|
+
export interface InitContextTenant {
|
|
238
|
+
name?: string | null;
|
|
239
|
+
description?: string | null;
|
|
240
|
+
category?: string | null;
|
|
241
|
+
/** Enabled-feature flags, e.g. `{ products: true, booking: false }`. */
|
|
242
|
+
features?: Record<string, boolean>;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The full init context. Every field optional — see the file header on
|
|
247
|
+
* additive evolution.
|
|
248
|
+
*/
|
|
249
|
+
export interface InitContext {
|
|
250
|
+
user?: InitContextUser;
|
|
251
|
+
locale?: InitContextLocale;
|
|
252
|
+
ui?: InitContextUi;
|
|
253
|
+
tenant?: InitContextTenant;
|
|
254
|
+
/** Anything the contract doesn't model yet. Rendered as untrusted data. */
|
|
255
|
+
notes?: string | null;
|
|
256
|
+
/**
|
|
257
|
+
* Epoch millis the caller took the snapshot. Optional; EgoZ stamps it on
|
|
258
|
+
* arrival when absent.
|
|
259
|
+
*
|
|
260
|
+
* It governs how long the `ui` half may still be rendered into a prompt,
|
|
261
|
+
* which is a SHORTER clock than the draft's own lifetime. A draft resumed
|
|
262
|
+
* after lunch is still the user's conversation, but "you have the
|
|
263
|
+
* Products window open" is by then a confident claim about a screen they
|
|
264
|
+
* closed — so past the snapshot window the UI half is dropped while
|
|
265
|
+
* `user` / `locale` / `tenant` still render.
|
|
266
|
+
*/
|
|
267
|
+
capturedAt?: number;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ============================================================================
|
|
271
|
+
// Request / response
|
|
272
|
+
// ============================================================================
|
|
273
|
+
|
|
274
|
+
/** Body of `POST /egoz/conversations/draft`. */
|
|
275
|
+
export interface StartDraftRequestBody {
|
|
276
|
+
/**
|
|
277
|
+
* Idempotency token for ONE chat panel — opaque, caller-generated, stable
|
|
278
|
+
* across remounts of that panel and fresh only for a genuinely new one.
|
|
279
|
+
* A good source is the panel's own window key plus a per-open nonce.
|
|
280
|
+
*
|
|
281
|
+
* Repeating a call with the same key refreshes that draft's init context
|
|
282
|
+
* and returns the SAME `conversationId` rather than minting another row.
|
|
283
|
+
* Callers SHOULD send one: a double-mounting panel that sends none leaks
|
|
284
|
+
* an orphan draft per mount.
|
|
285
|
+
*
|
|
286
|
+
* It is NOT a thread id and can never be used as one — its uniqueness
|
|
287
|
+
* index covers drafts only, and promotion drops it.
|
|
288
|
+
*/
|
|
289
|
+
draftKey?: string;
|
|
290
|
+
/**
|
|
291
|
+
* Consumer-supplied conversation id, for callers that already mint their
|
|
292
|
+
* own (Phase 6). Unrelated to idempotency — that is `draftKey`'s job.
|
|
293
|
+
*/
|
|
294
|
+
externalThreadId?: string;
|
|
295
|
+
/** The end user this conversation is for, as the caller identifies them. */
|
|
296
|
+
externalUserId?: string;
|
|
297
|
+
initContext?: InitContext;
|
|
298
|
+
/**
|
|
299
|
+
* The END TENANT this conversation belongs to, in the caller's own id
|
|
300
|
+
* space — required on a `gateway` integration, meaningless on a
|
|
301
|
+
* dedicated one.
|
|
302
|
+
*
|
|
303
|
+
* ON THE WIRE, not derived from request metadata: it is a scoping key,
|
|
304
|
+
* and a scoping key inferred from a caller-specific metadata field would
|
|
305
|
+
* make EgoZ's privacy boundary depend on one integration's vocabulary.
|
|
306
|
+
* The next gateway would either have to adopt that field name or earn a
|
|
307
|
+
* second branch.
|
|
308
|
+
*
|
|
309
|
+
* Without it, `(tenantId, externalUserId)` spans every one of the
|
|
310
|
+
* gateway's tenants the user belongs to — see `TenantScope`.
|
|
311
|
+
*/
|
|
312
|
+
externalTenantId?: string;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Where a suggested prompt came from. Surfaced because the two have
|
|
317
|
+
* different quality and latency characteristics, and a caller measuring chip
|
|
318
|
+
* performance needs to tell an instant catalog hit from a model-generated
|
|
319
|
+
* fallback rather than averaging them together.
|
|
320
|
+
*/
|
|
321
|
+
export type SuggestedPromptSource = 'template' | 'llm';
|
|
322
|
+
|
|
323
|
+
/** One suggested prompt chip, ready to render. */
|
|
324
|
+
export interface SuggestedPromptWire {
|
|
325
|
+
/** Stable catalog id — for the caller's own analytics, not for display. */
|
|
326
|
+
id: string;
|
|
327
|
+
/** The prompt text, already in the resolved locale. */
|
|
328
|
+
text: string;
|
|
329
|
+
source: SuggestedPromptSource;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** `data` payload of a successful `POST /egoz/conversations/draft`. */
|
|
333
|
+
export interface StartDraftResponseData {
|
|
334
|
+
/**
|
|
335
|
+
* The canonical conversation id. This is what a follow-up `/ask` carries
|
|
336
|
+
* as `threadId`. A turn carrying it MUST come back with the same id —
|
|
337
|
+
* EgoZ never mints a replacement for a supplied id, and a lapsed draft
|
|
338
|
+
* fails with `DraftExpired` rather than quietly becoming a new
|
|
339
|
+
* conversation.
|
|
340
|
+
*/
|
|
341
|
+
conversationId: string;
|
|
342
|
+
/** Always `'draft'` on a successful start; present so callers can assert. */
|
|
343
|
+
status: ThreadStatus;
|
|
344
|
+
/**
|
|
345
|
+
* ISO 8601. When the conversation id stops resolving — the row TTL.
|
|
346
|
+
*
|
|
347
|
+
* One of TWO CLOCKS, deliberately not collapsed into one number: this
|
|
348
|
+
* one governs how long the caller may still resume the conversation.
|
|
349
|
+
*/
|
|
350
|
+
expiresAt: string;
|
|
351
|
+
/**
|
|
352
|
+
* ISO 8601, and much sooner than `expiresAt`. When the `ui` half of the
|
|
353
|
+
* init context stops being rendered into the prompt.
|
|
354
|
+
*
|
|
355
|
+
* The second clock. A draft resumed after lunch is still the user's
|
|
356
|
+
* conversation, but "you have the Products window open" is by then a
|
|
357
|
+
* confident claim about a screen they closed. Past this instant the UI
|
|
358
|
+
* half is dropped while `user` / `locale` / `tenant` keep rendering —
|
|
359
|
+
* those don't go stale.
|
|
360
|
+
*/
|
|
361
|
+
contextExpiresAt: string;
|
|
362
|
+
/**
|
|
363
|
+
* Quick prompts for the panel to render, in `locale`. May be empty when
|
|
364
|
+
* nothing in the catalog fits and the fallback produced nothing — an
|
|
365
|
+
* empty list is a normal outcome, not an error.
|
|
366
|
+
*/
|
|
367
|
+
suggestedPrompts: SuggestedPromptWire[];
|
|
368
|
+
/**
|
|
369
|
+
* The RESOLVED language — what EgoZ actually used — not the requested
|
|
370
|
+
* one. Render the chips in this.
|
|
371
|
+
*/
|
|
372
|
+
locale: string;
|
|
373
|
+
}
|
package/src/index.d.ts
CHANGED
|
@@ -53,8 +53,17 @@
|
|
|
53
53
|
* - `thread.d.ts` — `ThreadWire`, `MessageWire`,
|
|
54
54
|
* `ThreadWithStatsWire`, `ThreadWithMessagesWire`,
|
|
55
55
|
* `ToolCallWire`, list / get response data,
|
|
56
|
-
* vocabularies (`MessageRole`,
|
|
56
|
+
* vocabularies (`MessageRole`, `ThreadStatus`,
|
|
57
57
|
* `MessageFailureReason`).
|
|
58
|
+
* - `draft-conv.d.ts` — the ghosted conversation start:
|
|
59
|
+
* `InitContext` (+ `InitContextUser`,
|
|
60
|
+
* `InitContextLocale`, `InitContextUi`,
|
|
61
|
+
* `InitContextTenant`, `InitContextOpenWindow`,
|
|
62
|
+
* `InitContextVisitedTab`),
|
|
63
|
+
* `StartDraftRequestBody`, `StartDraftResponseData`,
|
|
64
|
+
* `SuggestedPromptWire`, vocabularies
|
|
65
|
+
* (`InitContextTheme`, `InitContextViewport`,
|
|
66
|
+
* `InitContextAudience`).
|
|
58
67
|
* - `personality.d.ts` — Both surfaces share this file. User-level:
|
|
59
68
|
* `PersonalityProfileWire`, `PresetDefinitionWire`,
|
|
60
69
|
* create / update bodies, `ActivePersonalityWire`.
|
|
@@ -89,6 +98,7 @@ export * from './mcp-token';
|
|
|
89
98
|
export * from './mcp-connection';
|
|
90
99
|
export * from './tenant';
|
|
91
100
|
export * from './thread';
|
|
101
|
+
export * from './draft-conv';
|
|
92
102
|
export * from './personality';
|
|
93
103
|
export * from './user-memory';
|
|
94
104
|
export * from './search';
|
package/src/stream.d.ts
CHANGED
|
@@ -17,11 +17,14 @@ import type { Intent, TokenUsage } from './envelope';
|
|
|
17
17
|
* Discriminated union of every event the streaming `/ask` endpoint emits.
|
|
18
18
|
*
|
|
19
19
|
* Notes on subset behaviour:
|
|
20
|
+
* - `start` is always the FIRST frame, in every mode including
|
|
21
|
+
* `'minimal'`. It carries the conversation id.
|
|
20
22
|
* - `stream: 'minimal'` requests omit `rag.*` and `tool.*` events.
|
|
21
23
|
* - `done` is always the terminal success frame.
|
|
22
24
|
* - `error` may appear at any point and is also terminal.
|
|
23
25
|
*/
|
|
24
26
|
export type AskStreamEvent =
|
|
27
|
+
| AskStreamStartEvent
|
|
25
28
|
| AskStreamIntentEvent
|
|
26
29
|
| AskStreamRagSearchingEvent
|
|
27
30
|
| AskStreamRagRetrievedEvent
|
|
@@ -34,6 +37,51 @@ export type AskStreamEvent =
|
|
|
34
37
|
| AskStreamDoneEvent
|
|
35
38
|
| AskStreamErrorEvent;
|
|
36
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The opening frame — emitted before any work begins, on every stream.
|
|
42
|
+
*
|
|
43
|
+
* ── Why the id cannot wait for `done` ────────────────────────────────────
|
|
44
|
+
*
|
|
45
|
+
* The conversation id is known the moment the thread is resolved, which is
|
|
46
|
+
* before the first token. Withholding it until the terminal frame forces
|
|
47
|
+
* every downstream consumer to either buffer the whole turn or guess, and
|
|
48
|
+
* guessing is what silently broke consumers that select a thread id on each
|
|
49
|
+
* event: they read `undefined` on every frame until the last one, so the
|
|
50
|
+
* selection simply never happened and nothing errored.
|
|
51
|
+
*
|
|
52
|
+
* A consumer that renders a transcript needs to know WHICH conversation it
|
|
53
|
+
* is rendering while the tokens are arriving, not after.
|
|
54
|
+
*
|
|
55
|
+
* ── Why a distinct event rather than a field on every frame ──────────────
|
|
56
|
+
*
|
|
57
|
+
* Repeating the id on `delta` would put it on the highest-frequency frame in
|
|
58
|
+
* the protocol for a value that never changes within a stream. One frame at
|
|
59
|
+
* the start says it once, and says it at the only moment a consumer needs to
|
|
60
|
+
* act on it: before anything is rendered.
|
|
61
|
+
*
|
|
62
|
+
* ADDITIVE. Consumers that don't know this variant ignore it — the same way
|
|
63
|
+
* they already ignore any event type they don't handle — and keep reading
|
|
64
|
+
* the id off `done` as before.
|
|
65
|
+
*/
|
|
66
|
+
export interface AskStreamStartEvent {
|
|
67
|
+
type: 'start';
|
|
68
|
+
/**
|
|
69
|
+
* The canonical conversation id, EgoZ-minted. Identical to the
|
|
70
|
+
* `threadId` that will arrive on `done`; a turn NEVER changes id
|
|
71
|
+
* mid-stream, so a consumer can commit to this value immediately.
|
|
72
|
+
*/
|
|
73
|
+
threadId: string;
|
|
74
|
+
/** Echo of the supplied `externalThreadId`, when one was sent. */
|
|
75
|
+
externalThreadId?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Whether this turn PROMOTED a ghosted draft — i.e. it is the first real
|
|
78
|
+
* message in a conversation that already had an id. Lets a consumer tell
|
|
79
|
+
* "this id is new to me" from "this id was pre-seeded at chat-open", which
|
|
80
|
+
* are different UI states even though both are a first turn.
|
|
81
|
+
*/
|
|
82
|
+
promotedFromDraft?: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
37
85
|
export interface AskStreamIntentEvent {
|
|
38
86
|
type: 'intent';
|
|
39
87
|
intent: Intent;
|
package/src/tenant.d.ts
CHANGED
|
@@ -123,6 +123,27 @@ export type TenantIndustry =
|
|
|
123
123
|
* not a goal — net change would be breaking, with zero behavioural
|
|
124
124
|
* value.
|
|
125
125
|
*/
|
|
126
|
+
/**
|
|
127
|
+
* Whether an EgoZ project serves ONE customer or fronts MANY of a caller's
|
|
128
|
+
* own tenants. Mirrors the `egoz_tenants.tenant_scope` CHECK (migration 055).
|
|
129
|
+
*
|
|
130
|
+
* This is a fact about the INTEGRATION, declared once, not something inferred
|
|
131
|
+
* per request — inferring it from a caller-specific field would bake one
|
|
132
|
+
* integration's vocabulary into EgoZ's core scoping logic.
|
|
133
|
+
*
|
|
134
|
+
* - `'dedicated'` (default) — one project, one customer. `(tenantId,
|
|
135
|
+
* externalUserId)` is a privacy boundary, which is what every per-end-user
|
|
136
|
+
* feature assumes.
|
|
137
|
+
* - `'gateway'` — one project fronts many of the caller's tenants, and the
|
|
138
|
+
* real tenant travels inside each request. **`(tenantId, externalUserId)`
|
|
139
|
+
* is NOT a privacy boundary here**: the caller's end-user id is the same
|
|
140
|
+
* value across every one of their tenants that person belongs to, so one
|
|
141
|
+
* identity spans all of them. Per-end-user memory retrieval is gated for
|
|
142
|
+
* these projects until per-tenant scoping lands, because that data is read
|
|
143
|
+
* into the system prompt on every turn.
|
|
144
|
+
*/
|
|
145
|
+
export type TenantScope = 'dedicated' | 'gateway';
|
|
146
|
+
|
|
126
147
|
export interface TenantWire {
|
|
127
148
|
id: string;
|
|
128
149
|
name: string;
|
|
@@ -152,6 +173,16 @@ export interface TenantWire {
|
|
|
152
173
|
status: TenantStatus;
|
|
153
174
|
/** Billing plan (migrations 023/035). Drives usage quotas. Default 'id_ego'. */
|
|
154
175
|
plan: TenantPlan;
|
|
176
|
+
/**
|
|
177
|
+
* Whether this project serves one customer or fronts many of a caller's
|
|
178
|
+
* own tenants. See `TenantScope`. Default `'dedicated'` (migration 055).
|
|
179
|
+
*
|
|
180
|
+
* OPTIONAL on the wire deliberately: the backend has enforced it since
|
|
181
|
+
* 055, but it rides here additively so an older deployment in front of a
|
|
182
|
+
* newer consumer doesn't strand anyone. Read it as `'dedicated'` when
|
|
183
|
+
* absent — that is what a project that has never declared itself is.
|
|
184
|
+
*/
|
|
185
|
+
tenant_scope?: TenantScope;
|
|
155
186
|
|
|
156
187
|
// Time-boxed debug mode (migration 037). Populated on the LIST response
|
|
157
188
|
// (joined from tenant_configs); omitted where the query does not join it.
|
|
@@ -207,6 +238,19 @@ export interface TenantUpdateBody {
|
|
|
207
238
|
name?: string;
|
|
208
239
|
description?: string;
|
|
209
240
|
is_active?: boolean;
|
|
241
|
+
/**
|
|
242
|
+
* Whether this project serves one customer or fronts many of a caller's
|
|
243
|
+
* own tenants. See `TenantScope` (migration 055).
|
|
244
|
+
*
|
|
245
|
+
* ⚠️ ONE DIRECTION IS DESTRUCTIVE, and it is not the obvious one.
|
|
246
|
+
* `dedicated → gateway` is safe: it turns a privacy boundary ON.
|
|
247
|
+
* `gateway → dedicated` **re-enables per-end-user memory retrieval over
|
|
248
|
+
* rows that are still cross-tenant** — it does not undo anything, it
|
|
249
|
+
* re-opens the exposure the gateway setting was closing. A surface
|
|
250
|
+
* offering this field must confirm that direction and name what it
|
|
251
|
+
* re-enables; the reverse needs no confirmation.
|
|
252
|
+
*/
|
|
253
|
+
tenant_scope?: TenantScope;
|
|
210
254
|
|
|
211
255
|
industry?: TenantIndustry;
|
|
212
256
|
data_residency_region?: DataResidencyRegion;
|
package/src/thread.d.ts
CHANGED
|
@@ -44,13 +44,22 @@ export type MessageFailureReason =
|
|
|
44
44
|
* Conversation lifecycle state. Mirrors the CHECK constraint on
|
|
45
45
|
* `egoz_threads.status` (migration 054).
|
|
46
46
|
*
|
|
47
|
-
* - `'draft'`
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
47
|
+
* - `'draft'` — minted by the ghosted start
|
|
48
|
+
* (`POST /egoz/conversations/draft`) before the user has
|
|
49
|
+
* typed anything. Carries the caller's init context, holds a
|
|
50
|
+
* conversation id the caller can keep, and lapses after the
|
|
51
|
+
* draft TTL if it never promotes.
|
|
52
|
+
* - `'active'` — a real conversation. Every thread created by `/ask` is born
|
|
53
|
+
* active, and a draft becomes active on its first user
|
|
54
|
+
* message. An active thread is never swept.
|
|
55
|
+
* - `'expired'` — a lapsed draft. A TOMBSTONE, not a conversation: it cannot
|
|
56
|
+
* be resumed and holds no messages. It exists so that a
|
|
57
|
+
* caller returning with a held draft id gets a distinct
|
|
58
|
+
* `DraftExpired` (410) rather than a `NotFound` it cannot
|
|
59
|
+
* tell apart from a garbage id — the contract requires a
|
|
60
|
+
* lapsed draft to fail loudly and specifically, never to
|
|
61
|
+
* silently become a new conversation. Hard-deleted after a
|
|
62
|
+
* grace window.
|
|
54
63
|
*
|
|
55
64
|
* ORTHOGONAL TO `isActive`, which means *archived* and predates this field.
|
|
56
65
|
* A draft is not an archived thread and an archived thread was never a draft;
|
|
@@ -58,7 +67,7 @@ export type MessageFailureReason =
|
|
|
58
67
|
* draft sweeper's delete predicate would then also match archived user
|
|
59
68
|
* conversations.
|
|
60
69
|
*/
|
|
61
|
-
export type ThreadStatus = 'draft' | 'active';
|
|
70
|
+
export type ThreadStatus = 'draft' | 'active' | 'expired';
|
|
62
71
|
|
|
63
72
|
// ============================================================================
|
|
64
73
|
// Supporting shapes
|