@ego-z/contracts 0.11.0

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,222 @@
1
+ /**
2
+ * @ego-z/contracts — `/egoz/tenants/*` wire types (project CRUD).
3
+ *
4
+ * `Tenant` is what the Console calls "Project" — same row, two names.
5
+ * The wire shape preserves `snake_case` field names because that's the
6
+ * actual JSON the backend has always emitted and the console has always
7
+ * consumed. Renaming to camelCase here would be a breaking wire change
8
+ * (every API caller would need to update) and is deliberately out of
9
+ * scope for the additive Phase 9 sweep. If/when we ever do that
10
+ * migration, this is the file the major-version bump touches.
11
+ *
12
+ * Drift cleaned up:
13
+ * - Backend `Tenant` used `Date` for `created_at` / `updated_at` /
14
+ * `attestation_signed_at`; wire and console already used ISO
15
+ * strings. Backend `formatTenant` now emits ISO via the same
16
+ * pattern as Tools and RAG.
17
+ */
18
+
19
+ // ============================================================================
20
+ // Locked vocabularies (mirror DB CHECK constraints)
21
+ // ============================================================================
22
+
23
+ /**
24
+ * Compliance tier (mirrors `egoz_tenants.compliance_tier` CHECK
25
+ * constraint, migration 010). Phase 1 services `'standard'` only;
26
+ * the rest are forward-compat for Phase 2.
27
+ */
28
+ export type ComplianceTier =
29
+ | 'standard'
30
+ | 'pending_review'
31
+ | 'gdpr'
32
+ | 'hipaa'
33
+ | 'enterprise'
34
+ | 'pending_upgrade';
35
+
36
+ /**
37
+ * Data residency region (mirrors `egoz_tenants.data_residency_region`
38
+ * CHECK constraint, migration 010). Phase 1 deploys only to
39
+ * `'eu-west'`; the rest are forward-compat.
40
+ */
41
+ export type DataResidencyRegion =
42
+ | 'us-east'
43
+ | 'us-west'
44
+ | 'eu-west'
45
+ | 'eu-central'
46
+ | 'ap-south'
47
+ | 'ap-east';
48
+
49
+ /**
50
+ * Operational tenant status (mirrors `egoz_tenants.status` CHECK
51
+ * constraint). The legacy `is_active` boolean still exists in
52
+ * Phase 1 and is NOT auto-synced with `status` — both fields are
53
+ * independent until Phase 1.5 deprecates `is_active`.
54
+ */
55
+ export type TenantStatus =
56
+ | 'active'
57
+ | 'pending_review'
58
+ | 'flagged'
59
+ | 'suspended'
60
+ | 'terminated';
61
+
62
+ /**
63
+ * Billing plan (Phase 1.5, two-plan model — migration 035). Drives usage
64
+ * quotas (e.g. monthly /ask limit).
65
+ *
66
+ * - `id_ego` — "Id Ego": free, pay-as-you-go via BYOK, capped limits.
67
+ * Default for self-service signup.
68
+ * - `superego` — "Superego": $20/mo, higher (but still capped) limits.
69
+ */
70
+ export type TenantPlan = 'id_ego' | 'superego';
71
+
72
+ /**
73
+ * Industry classification driving the signup gate (Phase 1.5+).
74
+ * The regulated entries (`'healthcare'`, `'finance'`, `'insurance'`,
75
+ * `'government'`, `'legal'`) auto-route new tenants to
76
+ * `status: 'pending_review'`.
77
+ */
78
+ export type TenantIndustry =
79
+ | 'saas'
80
+ | 'ecommerce'
81
+ | 'marketing'
82
+ | 'education'
83
+ | 'media'
84
+ | 'travel'
85
+ | 'real_estate'
86
+ | 'productivity'
87
+ | 'other'
88
+ | 'healthcare'
89
+ | 'finance'
90
+ | 'insurance'
91
+ | 'government'
92
+ | 'legal';
93
+
94
+ // ============================================================================
95
+ // TenantWire — the canonical row shape (snake_case on the wire)
96
+ // ============================================================================
97
+
98
+ /**
99
+ * Wire-format tenant row. Returned by every `/egoz/tenants/*` response.
100
+ *
101
+ * **Field naming.** This is the only wire shape in the contracts
102
+ * package that uses `snake_case`. It's preserved verbatim from the
103
+ * legacy backend → JSON path because the Console already consumes
104
+ * `snake_case` here too. Treat the inconsistency as fixed-by-history,
105
+ * not a goal — net change would be breaking, with zero behavioural
106
+ * value.
107
+ */
108
+ export interface TenantWire {
109
+ id: string;
110
+ name: string;
111
+ /** URL-safe slug, unique per parent (when `parent_tenant_id` is set). */
112
+ slug: string;
113
+ description?: string;
114
+ is_active: boolean;
115
+ /** ISO 8601 timestamp. */
116
+ created_at: string;
117
+ /** ISO 8601 timestamp. */
118
+ updated_at: string;
119
+
120
+ // Phase 1 compliance posture (migration 010) ----------------------
121
+ compliance_tier: ComplianceTier;
122
+ data_residency_region: DataResidencyRegion;
123
+ /**
124
+ * UUID of the parent tenant when this row is a sub-project. `null`
125
+ * for top-level tenants. Sub-projects share their parent's
126
+ * `compliance_tier` for billing purposes (enforced at the service
127
+ * layer, not the DB).
128
+ */
129
+ parent_tenant_id: string | null;
130
+ industry: TenantIndustry | null;
131
+ /** ISO 8601 timestamp. `null` until the operator signs an attestation. */
132
+ attestation_signed_at: string | null;
133
+ attestation_version: string | null;
134
+ status: TenantStatus;
135
+ /** Billing plan (migrations 023/035). Drives usage quotas. Default 'id_ego'. */
136
+ plan: TenantPlan;
137
+
138
+ // Time-boxed debug mode (migration 037). Populated on the LIST response
139
+ // (joined from tenant_configs); omitted where the query does not join it.
140
+ // `debug_enabled` is resolved server-side as `debug_until > NOW()`.
141
+ debug_enabled?: boolean;
142
+ /** ISO 8601 expiry backing `debug_enabled`; null when off/expired. */
143
+ debug_until?: string | null;
144
+ }
145
+
146
+ // ============================================================================
147
+ // Request bodies (what the client SENDS)
148
+ // ============================================================================
149
+
150
+ /**
151
+ * Wire body of `POST /egoz/tenants`.
152
+ *
153
+ * Notable absences:
154
+ * - `compliance_tier`, `status` — server-derived from `industry`
155
+ * at signup (Phase 1C / Strategy A).
156
+ * Non-regulated industry →
157
+ * `'standard'` + `'active'`;
158
+ * regulated industry (healthcare,
159
+ * finance, insurance, government,
160
+ * legal) → `'pending_review'` on
161
+ * both. Manual tier moves stay an
162
+ * admin operation via
163
+ * `TenantUpdateBody`.
164
+ * - `is_active`, timestamps — server-managed.
165
+ */
166
+ export interface TenantCreateBody {
167
+ name: string;
168
+ slug?: string;
169
+ description?: string;
170
+
171
+ industry?: TenantIndustry;
172
+ data_residency_region?: DataResidencyRegion;
173
+ parent_tenant_id?: string | null;
174
+ /** ISO 8601 timestamp string the operator submits at signup. */
175
+ attestation_signed_at?: string;
176
+ attestation_version?: string;
177
+ }
178
+
179
+ /**
180
+ * Wire body of `PUT /egoz/tenants/:tenantId`. Every field is optional —
181
+ * the backend applies `COALESCE`-style partial updates. `tenantId` is
182
+ * the URL param, not the body.
183
+ *
184
+ * `compliance_tier` and `status` are admin operations; Phase 1 has no
185
+ * role gating on these — Phase 1.5 will introduce admin-only
186
+ * enforcement.
187
+ */
188
+ export interface TenantUpdateBody {
189
+ name?: string;
190
+ description?: string;
191
+ is_active?: boolean;
192
+
193
+ industry?: TenantIndustry;
194
+ data_residency_region?: DataResidencyRegion;
195
+ parent_tenant_id?: string | null;
196
+ compliance_tier?: ComplianceTier;
197
+ status?: TenantStatus;
198
+ attestation_signed_at?: string;
199
+ attestation_version?: string;
200
+ }
201
+
202
+ // ============================================================================
203
+ // Response data shapes (carried inside `EgozApiResponse.data`)
204
+ // ============================================================================
205
+
206
+ export interface TenantListResponseData {
207
+ tenants: TenantWire[];
208
+ }
209
+
210
+ export interface TenantGetResponseData {
211
+ tenant: TenantWire;
212
+ }
213
+
214
+ /**
215
+ * Returned by `POST /egoz/tenants`. Wrapping the row in `tenant`
216
+ * (rather than returning it raw) matches the rest of the create
217
+ * surfaces and keeps room for additive metadata (creation source,
218
+ * downstream side effects, …) without a major version bump.
219
+ */
220
+ export interface TenantCreatedResponseData {
221
+ tenant: TenantWire;
222
+ }
@@ -0,0 +1,248 @@
1
+ /**
2
+ * @ego-z/contracts — `/egoz/threads/*` and
3
+ * `/egoz/tenants/:tenantId/threads/*` wire types.
4
+ *
5
+ * The conversation memory surface. `Thread` rows are produced by every
6
+ * `/ask` turn (via `ThreadService.getOrCreateThread`) and read back
7
+ * via the admin threads list / detail routes.
8
+ *
9
+ * Phase 6's `externalThreadId` already lives in `AskRequestBody` /
10
+ * `AskResponseData` / `AskStreamDoneEvent` (in `ask.d.ts` /
11
+ * `stream.d.ts`). This file lifts the FULL `Thread` and `Message`
12
+ * row shapes — what the threads-admin endpoints return — into the
13
+ * contracts package so the console mirror can't drift.
14
+ *
15
+ * Drift cleaned up:
16
+ * - Backend `Thread` and `Message` used `Date` for `createdAt` /
17
+ * `updatedAt`; wire and console already used ISO strings.
18
+ * `formatThread` and `formatMessage` now emit ISO to match.
19
+ */
20
+
21
+ import type { Intent } from './envelope';
22
+
23
+ // ============================================================================
24
+ // Locked vocabularies
25
+ // ============================================================================
26
+
27
+ /** Role of a message in the conversation turn. */
28
+ export type MessageRole = 'system' | 'user' | 'assistant' | 'tool';
29
+
30
+ /**
31
+ * Why a turn was stamped with `failedRecovery: true` by the
32
+ * orchestrator. Mirrors the CHECK constraint on
33
+ * `egoz_messages.failure_reason` (migration 014, Phase 5 / Layer 5).
34
+ * Adding a new entry requires a migration + orchestrator change in
35
+ * lockstep — the audit field exists to be queryable, not free-form.
36
+ */
37
+ export type MessageFailureReason =
38
+ | 'tool_retry_storm'
39
+ | 'iteration_cap'
40
+ | 'max_calls_per_turn'
41
+ | 'unrecoverable_error';
42
+
43
+ // ============================================================================
44
+ // Supporting shapes
45
+ // ============================================================================
46
+
47
+ /**
48
+ * Tool-call request emitted by an `assistant` message. Shape matches
49
+ * the OpenAI Chat Completions tool-call envelope verbatim — that's
50
+ * what the orchestrator persists. `arguments` is a JSON string the
51
+ * caller must `JSON.parse` if they want the structured value;
52
+ * EgoZ doesn't decode it on the wire because it's already
53
+ * round-tripped through the LLM as text.
54
+ */
55
+ export interface ToolCallWire {
56
+ id: string;
57
+ type: 'function';
58
+ function: {
59
+ name: string;
60
+ arguments: string;
61
+ };
62
+ }
63
+
64
+ // ============================================================================
65
+ // ThreadWire — the canonical row shape
66
+ // ============================================================================
67
+
68
+ /**
69
+ * Wire-format thread row. Returned by every threads-admin response and
70
+ * embedded in `ThreadWithMessagesWire`.
71
+ *
72
+ * `externalThreadId` is populated when the caller supplied one on the
73
+ * `/ask` that minted the row (Phase 6); `null` for legacy threads and
74
+ * for any caller that uses only the EgoZ UUID path.
75
+ */
76
+ export interface ThreadWire {
77
+ id: string;
78
+ tenantId: string;
79
+ externalUserId: string | null;
80
+ /**
81
+ * Consumer-supplied conversation identifier (Phase 6). `null` for
82
+ * threads created before the column existed and for any caller
83
+ * that didn't supply one. Uniqueness is enforced per
84
+ * `(tenantId, externalThreadId)` among non-NULL rows by the
85
+ * `idx_egoz_threads_tenant_ext` partial unique index — see
86
+ * migration 017.
87
+ */
88
+ externalThreadId: string | null;
89
+ title: string | null;
90
+ metadata: Record<string, unknown>;
91
+ isActive: boolean;
92
+ /** ISO 8601 timestamp. */
93
+ createdAt: string;
94
+ /** ISO 8601 timestamp. */
95
+ updatedAt: string;
96
+ }
97
+
98
+ /**
99
+ * Thread row plus aggregate stats. Returned by the admin threads list
100
+ * endpoint so the operator-facing table can sort by activity without
101
+ * an N+1 fetch.
102
+ */
103
+ export interface ThreadWithStatsWire extends ThreadWire {
104
+ messageCount: number;
105
+ totalTokens: number;
106
+ }
107
+
108
+ // ============================================================================
109
+ // MessageWire — the canonical message-row shape
110
+ // ============================================================================
111
+
112
+ /**
113
+ * Wire-format message row. Carries the full per-turn audit trail —
114
+ * the LLM iteration's prompt/completion token split, the model id,
115
+ * the intent classification, the RAG context that was injected, and
116
+ * the Phase 5 failure-recovery flags.
117
+ *
118
+ * `toolCalls` is populated only on `role: 'assistant'` rows that
119
+ * requested a tool. `toolCallId` + `toolName` are populated only on
120
+ * `role: 'tool'` result rows.
121
+ */
122
+ export interface MessageWire {
123
+ id: string;
124
+ threadId: string;
125
+ tenantId: string;
126
+ role: MessageRole;
127
+ content: string | null;
128
+ toolCalls: ToolCallWire[] | null;
129
+ toolCallId: string | null;
130
+ toolName: string | null;
131
+ intent: Intent | null;
132
+ ragContext: Record<string, unknown> | null;
133
+ promptTokens: number | null;
134
+ completionTokens: number | null;
135
+ totalTokens: number | null;
136
+ model: string | null;
137
+ /** Echo of `AskRequestBody.metadata` for audit. */
138
+ metadata: Record<string, unknown> | null;
139
+ /**
140
+ * `true` when this row's `content` was deliberately dropped under
141
+ * no-store (metadata-only) persistence — see
142
+ * `AskRequestBody.noStore` and the tenant's `persistMessageContent`
143
+ * setting (#5). Distinguishes an intentional NULL (the conversation
144
+ * text was never stored) from an incidental empty turn. Always
145
+ * `false` for tenants that persist content normally.
146
+ */
147
+ contentRedacted: boolean;
148
+ /**
149
+ * `true` when this turn triggered a Phase 5 tool-retry-storm
150
+ * backstop (dedup ≥ MAX_DUPLICATE_CALLS_BEFORE_FLAG / per-tool
151
+ * cap / outer iteration cap) or ended on an unrecoverable error
152
+ * class. Always `false` for non-assistant rows. Phase 5 /
153
+ * Layer 5 — drives the Tool Health "bad turns" filter.
154
+ */
155
+ failedRecovery: boolean;
156
+ /**
157
+ * Dominant cause when `failedRecovery === true`. `null`
158
+ * otherwise. See `MessageFailureReason` for the locked
159
+ * vocabulary.
160
+ */
161
+ failureReason: MessageFailureReason | null;
162
+ /**
163
+ * Manual operator incident flag on this turn — the CURRENT state
164
+ * (latest by `flaggedAt`; at most one `open` at a time). `null`/absent
165
+ * when the turn was never flagged. Additive (v0.5.2); populated only by
166
+ * the thread-detail read path, never on the /ask hot path. See
167
+ * `MessageIncidentWire`.
168
+ */
169
+ incident?: MessageIncidentWire | null;
170
+ /** ISO 8601 timestamp. */
171
+ createdAt: string;
172
+ }
173
+
174
+ /**
175
+ * Returned by the thread-detail endpoint. The full conversation —
176
+ * row + every message in chronological order.
177
+ */
178
+ export interface ThreadWithMessagesWire {
179
+ thread: ThreadWire;
180
+ messages: MessageWire[];
181
+ }
182
+
183
+ // ============================================================================
184
+ // Response data shapes (carried inside `EgozApiResponse.data`)
185
+ // ============================================================================
186
+
187
+ export interface ThreadListResponseData {
188
+ threads: ThreadWithStatsWire[];
189
+ }
190
+
191
+ export interface ThreadGetResponseData extends ThreadWithMessagesWire {}
192
+
193
+ // ============================================================================
194
+ // Message incidents — manual operator flagging of a turn
195
+ //
196
+ // The manual counterpart to the auto-set `failedRecovery`/`failureReason`
197
+ // "bad turn" flags: an operator reviewing a conversation can flag a single
198
+ // turn as an incident (with a reason + optional note), then later resolve it.
199
+ // Metadata only — never message content. Stored in `egoz_message_incidents`
200
+ // (migration 040); at most one `open` incident per message.
201
+ // ============================================================================
202
+
203
+ /**
204
+ * Why an operator flagged a turn. Locked vocabulary — mirrors the
205
+ * migration-040 CHECK constraint; adding a value requires a migration + this
206
+ * type in lockstep so the UI, filter SQL and DB can't drift.
207
+ */
208
+ export type MessageIncidentReason =
209
+ | 'wrong_answer'
210
+ | 'hallucination'
211
+ | 'tool_failure'
212
+ | 'unsafe_content'
213
+ | 'policy_violation'
214
+ | 'other';
215
+
216
+ /** Incident lifecycle. `open` on flag; `resolved` once an operator clears it. */
217
+ export type MessageIncidentStatus = 'open' | 'resolved';
218
+
219
+ /** A manual incident flag raised on a single message/turn. Metadata only. */
220
+ export interface MessageIncidentWire {
221
+ id: string;
222
+ tenantId: string;
223
+ threadId: string;
224
+ messageId: string;
225
+ reason: MessageIncidentReason;
226
+ /** Optional free-text note from the operator who flagged. */
227
+ note: string | null;
228
+ status: MessageIncidentStatus;
229
+ /** User id who flagged (from the validated session), when known. */
230
+ flaggedBy: string | null;
231
+ /** ISO 8601 timestamp. */
232
+ flaggedAt: string;
233
+ /** User id who resolved; `null` while open. */
234
+ resolvedBy: string | null;
235
+ /** ISO 8601 timestamp; `null` while open. */
236
+ resolvedAt: string | null;
237
+ }
238
+
239
+ /** Body for `POST …/threads/:threadId/messages/:messageId/incident` (flag). */
240
+ export interface FlagMessageIncidentBody {
241
+ reason: MessageIncidentReason;
242
+ note?: string;
243
+ }
244
+
245
+ /** Response data for both the flag and resolve endpoints. */
246
+ export interface MessageIncidentResponseData {
247
+ incident: MessageIncidentWire;
248
+ }