@ego-z/contracts 0.15.0 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ego-z/contracts",
3
- "version": "0.15.0",
3
+ "version": "0.15.3",
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,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
  /**
@@ -247,6 +295,21 @@ export interface StartDraftRequestBody {
247
295
  /** The end user this conversation is for, as the caller identifies them. */
248
296
  externalUserId?: string;
249
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;
250
313
  }
251
314
 
252
315
  /**
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;