@ego-z/contracts 0.15.0 → 0.15.5
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 +4 -1
- package/src/ask.d.ts +15 -0
- package/src/draft-conv.d.ts +112 -0
- package/src/index.d.ts +2 -1
- package/src/stream.d.ts +48 -0
- package/src/tenant.d.ts +44 -0
- package/src/thread.d.ts +12 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ego-z/contracts",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.5",
|
|
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": {
|
|
@@ -44,5 +44,8 @@
|
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"typescript": "^5.0.0"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@ego-z/contracts": "^0.15.0"
|
|
47
50
|
}
|
|
48
51
|
}
|
package/src/ask.d.ts
CHANGED
|
@@ -25,6 +25,21 @@ import type { Intent, JsonSchemaDefinition, ResponseFormat, TokenUsage } from '.
|
|
|
25
25
|
* These are intentionally NOT body fields — they live where HTTP puts them.
|
|
26
26
|
*/
|
|
27
27
|
export interface AskRequestBody {
|
|
28
|
+
/**
|
|
29
|
+
* The CALLER'S own tenant for this turn, in their id space — required on
|
|
30
|
+
* a `gateway` integration (see `TenantScope`), meaningless otherwise.
|
|
31
|
+
*
|
|
32
|
+
* Scopes every per-user read on the turn and guards the resume: a
|
|
33
|
+
* conversation belonging to a different one of the caller's tenants
|
|
34
|
+
* fails as `ThreadMismatch` (409) rather than `NotFound`, because a
|
|
35
|
+
* gateway that hears "not found" for a real conversation bootstraps its
|
|
36
|
+
* own record under the wrong tenant and forks the transcript.
|
|
37
|
+
*
|
|
38
|
+
* On the wire rather than derived from `metadata`: a scoping key
|
|
39
|
+
* inferred from one caller's metadata field name would make EgoZ's
|
|
40
|
+
* privacy boundary depend on that caller's vocabulary.
|
|
41
|
+
*/
|
|
42
|
+
externalTenantId?: string;
|
|
28
43
|
/** End-user's message for this turn. Required, non-empty. */
|
|
29
44
|
message: string;
|
|
30
45
|
|
package/src/draft-conv.d.ts
CHANGED
|
@@ -44,6 +44,20 @@ export type InitContextTheme = 'light' | 'dark';
|
|
|
44
44
|
/** Viewport class. Deliberately not "device" — see `InitContextUi.viewport`. */
|
|
45
45
|
export type InitContextViewport = 'mobile' | 'desktop';
|
|
46
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
|
+
|
|
47
61
|
/** One open window in the caller's UI. */
|
|
48
62
|
export interface InitContextOpenWindow {
|
|
49
63
|
/** Epoch millis the window was opened. */
|
|
@@ -141,6 +155,40 @@ export interface InitContextUi {
|
|
|
141
155
|
export interface InitContextUser {
|
|
142
156
|
name?: string | null;
|
|
143
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;
|
|
144
192
|
}
|
|
145
193
|
|
|
146
194
|
/**
|
|
@@ -194,6 +242,53 @@ export interface InitContextTenant {
|
|
|
194
242
|
features?: Record<string, boolean>;
|
|
195
243
|
}
|
|
196
244
|
|
|
245
|
+
/**
|
|
246
|
+
* A short recap of the user's PREVIOUS conversation, so the agent can open
|
|
247
|
+
* with continuity instead of as though it has never met them.
|
|
248
|
+
*
|
|
249
|
+
* ── Supplied by the caller, deliberately ─────────────────────────────────
|
|
250
|
+
*
|
|
251
|
+
* EgoZ stores conversations too, so it looks like EgoZ should resolve this
|
|
252
|
+
* itself — but on a gateway integration EgoZ's own copy is keyed by
|
|
253
|
+
* `(tenantId, externalUserId)`, which spans every one of the caller's
|
|
254
|
+
* tenants a person belongs to. Sourcing it there would recap store B's
|
|
255
|
+
* conversation into store A's prompt. The caller's store is per-tenant by
|
|
256
|
+
* construction, so it can answer the question safely, and a conversation the
|
|
257
|
+
* user deleted stops being quotable immediately with no propagation chain.
|
|
258
|
+
*
|
|
259
|
+
* ── Keep it SHORT ────────────────────────────────────────────────────────
|
|
260
|
+
*
|
|
261
|
+
* `lastMessage` is a truncated snippet, not a transcript. It rides in the
|
|
262
|
+
* system prompt on every turn of the new conversation, so a long one buys
|
|
263
|
+
* a little continuity at the cost of crowding out the context that makes
|
|
264
|
+
* the agent useful. Cap it at the sender.
|
|
265
|
+
*
|
|
266
|
+
* ── Recency matters more than it looks ───────────────────────────────────
|
|
267
|
+
*
|
|
268
|
+
* "Last time we spoke…" about a conversation from three months ago reads as
|
|
269
|
+
* the assistant having lost the plot, not as continuity. Callers should omit
|
|
270
|
+
* this entirely past a sensible window rather than send something stale.
|
|
271
|
+
*/
|
|
272
|
+
export interface InitContextLastConversation {
|
|
273
|
+
/** The previous conversation's title, if it has one. */
|
|
274
|
+
title?: string | null;
|
|
275
|
+
/** ISO 8601 — when it was last active. Lets the agent judge recency. */
|
|
276
|
+
updatedAt?: string | null;
|
|
277
|
+
/**
|
|
278
|
+
* A TRUNCATED snippet of the last message in it. Not a transcript —
|
|
279
|
+
* see the type docs on why the cap belongs at the sender.
|
|
280
|
+
*/
|
|
281
|
+
lastMessage?: string | null;
|
|
282
|
+
/**
|
|
283
|
+
* Whether the agent replied to that last message.
|
|
284
|
+
*
|
|
285
|
+
* `false` is the interesting value: the user said something and never
|
|
286
|
+
* got an answer, which is worth opening on. Distinguishing it from
|
|
287
|
+
* "unknown" is why this is optional rather than defaulting to `true`.
|
|
288
|
+
*/
|
|
289
|
+
answered?: boolean;
|
|
290
|
+
}
|
|
291
|
+
|
|
197
292
|
/**
|
|
198
293
|
* The full init context. Every field optional — see the file header on
|
|
199
294
|
* additive evolution.
|
|
@@ -203,6 +298,8 @@ export interface InitContext {
|
|
|
203
298
|
locale?: InitContextLocale;
|
|
204
299
|
ui?: InitContextUi;
|
|
205
300
|
tenant?: InitContextTenant;
|
|
301
|
+
/** A short recap of the user's previous conversation, if there is one. */
|
|
302
|
+
lastConversation?: InitContextLastConversation;
|
|
206
303
|
/** Anything the contract doesn't model yet. Rendered as untrusted data. */
|
|
207
304
|
notes?: string | null;
|
|
208
305
|
/**
|
|
@@ -247,6 +344,21 @@ export interface StartDraftRequestBody {
|
|
|
247
344
|
/** The end user this conversation is for, as the caller identifies them. */
|
|
248
345
|
externalUserId?: string;
|
|
249
346
|
initContext?: InitContext;
|
|
347
|
+
/**
|
|
348
|
+
* The END TENANT this conversation belongs to, in the caller's own id
|
|
349
|
+
* space — required on a `gateway` integration, meaningless on a
|
|
350
|
+
* dedicated one.
|
|
351
|
+
*
|
|
352
|
+
* ON THE WIRE, not derived from request metadata: it is a scoping key,
|
|
353
|
+
* and a scoping key inferred from a caller-specific metadata field would
|
|
354
|
+
* make EgoZ's privacy boundary depend on one integration's vocabulary.
|
|
355
|
+
* The next gateway would either have to adopt that field name or earn a
|
|
356
|
+
* second branch.
|
|
357
|
+
*
|
|
358
|
+
* Without it, `(tenantId, externalUserId)` spans every one of the
|
|
359
|
+
* gateway's tenants the user belongs to — see `TenantScope`.
|
|
360
|
+
*/
|
|
361
|
+
externalTenantId?: string;
|
|
250
362
|
}
|
|
251
363
|
|
|
252
364
|
/**
|
package/src/index.d.ts
CHANGED
|
@@ -62,7 +62,8 @@
|
|
|
62
62
|
* `InitContextVisitedTab`),
|
|
63
63
|
* `StartDraftRequestBody`, `StartDraftResponseData`,
|
|
64
64
|
* `SuggestedPromptWire`, vocabularies
|
|
65
|
-
* (`InitContextTheme`, `InitContextViewport
|
|
65
|
+
* (`InitContextTheme`, `InitContextViewport`,
|
|
66
|
+
* `InitContextAudience`).
|
|
66
67
|
* - `personality.d.ts` — Both surfaces share this file. User-level:
|
|
67
68
|
* `PersonalityProfileWire`, `PresetDefinitionWire`,
|
|
68
69
|
* create / update bodies, `ActivePersonalityWire`.
|
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
|
@@ -115,6 +115,18 @@ export interface ThreadWire {
|
|
|
115
115
|
* migration 017.
|
|
116
116
|
*/
|
|
117
117
|
externalThreadId: string | null;
|
|
118
|
+
/**
|
|
119
|
+
* The CALLER'S own tenant this conversation belongs to, in their id
|
|
120
|
+
* space (migration 056). Populated on `gateway` projects — where one
|
|
121
|
+
* EgoZ project fronts many of the caller's tenants — and `null`
|
|
122
|
+
* otherwise, including for every row predating the column.
|
|
123
|
+
*
|
|
124
|
+
* It supplies the third part of the per-user scoping key. Without it,
|
|
125
|
+
* `(tenantId, externalUserId)` spans every one of the caller's tenants
|
|
126
|
+
* a person belongs to, because their end-user id is identical across
|
|
127
|
+
* all of them. See `TenantScope`.
|
|
128
|
+
*/
|
|
129
|
+
externalTenantId?: string | null;
|
|
118
130
|
title: string | null;
|
|
119
131
|
metadata: Record<string, unknown>;
|
|
120
132
|
isActive: boolean;
|