@egox/contracts 0.5.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,275 @@
1
+ /**
2
+ * @egox/contracts — `/egox/personality/*` and
3
+ * `/egox/tenants/:tenantId/personality/*` wire types.
4
+ *
5
+ * Two related but distinct surfaces share this file:
6
+ *
7
+ * - **User-level templates** (`/egox/personality/*`). A library of
8
+ * personality profiles a console user authors and later clones
9
+ * into specific projects. NOT used at `/ask` resolution time.
10
+ * - **Project-scoped profiles** (`/egox/tenants/:tenantId/personality/*`,
11
+ * Item #6 refactor). The single ACTIVE row per tenant drives the
12
+ * LLM's persona on every `/ask`. Cloned from a template or
13
+ * authored fresh in the project.
14
+ *
15
+ * Both shapes share `PersonalityTraitsWire` and `PresetType` —
16
+ * cloning a template is a structural copy plus a `templateSourceUserId`
17
+ * / `templateSourceProfileId` pointer.
18
+ *
19
+ * Drift cleaned up:
20
+ * - Backend `PersonalityProfile` and `TenantPersonalityProfile`
21
+ * used `Date` for timestamps; wire and console already used ISO.
22
+ * Backend formatters now match.
23
+ */
24
+
25
+ // ============================================================================
26
+ // Locked vocabularies
27
+ // ============================================================================
28
+
29
+ /**
30
+ * Preset personality archetypes the console offers as starting points.
31
+ * Mirrors the keys of the backend's preset registry. Adding a preset
32
+ * is a one-edit change here + a new entry in the registry; the union
33
+ * fails the build on any consumer that hard-codes the old set.
34
+ */
35
+ export type PresetType =
36
+ | 'balanced'
37
+ | 'professional'
38
+ | 'friendly'
39
+ | 'casual'
40
+ | 'expert'
41
+ | 'playful'
42
+ | 'empathetic'
43
+ | 'concise';
44
+
45
+ // ============================================================================
46
+ // Supporting shapes
47
+ // ============================================================================
48
+
49
+ /**
50
+ * The eight personality dials (0-100 each). Used as the active
51
+ * configuration on both user templates and project profiles, and as
52
+ * the slider values the console renders.
53
+ *
54
+ * Each axis is documented relative to its low / high pole — that
55
+ * documentation belongs in the contracts package because it's part
56
+ * of what consumers (the SDK exposing it, future MCP tools surfacing
57
+ * it) need to render the control correctly.
58
+ */
59
+ export interface PersonalityTraitsWire {
60
+ /** 0 = serious, 100 = playful. */
61
+ senseOfHumor: number;
62
+ /** 0 = casual, 100 = formal/grave. */
63
+ seriousness: number;
64
+ /** 0 = diplomatic, 100 = blunt. */
65
+ directness: number;
66
+ /** 0 = conventional, 100 = creative. */
67
+ creativity: number;
68
+ /** 0 = neutral, 100 = caring. */
69
+ empathy: number;
70
+ /** 0 = casual, 100 = formal. */
71
+ formality: number;
72
+ /** 0 = brief, 100 = detailed. */
73
+ verbosity: number;
74
+ /** 0 = calm, 100 = energetic. */
75
+ enthusiasm: number;
76
+ }
77
+
78
+ // ============================================================================
79
+ // User-level personality template (the templates library)
80
+ // ============================================================================
81
+
82
+ /**
83
+ * Wire-format user-level personality profile. Returned by
84
+ * `/egox/personality/*` endpoints. Owned by a console user and
85
+ * intended to be cloned INTO projects via the tenant-personality
86
+ * clone endpoint.
87
+ *
88
+ * `isActive` here is a per-template "currently authoring this one"
89
+ * flag, NOT the `/ask` resolution flag (that's
90
+ * `TenantPersonalityProfileWire.isActive`).
91
+ */
92
+ export interface PersonalityProfileWire {
93
+ id: string;
94
+ userId: string;
95
+ name: string;
96
+ traits: PersonalityTraitsWire;
97
+
98
+ customInstructions: string | null;
99
+ exampleResponses: string[];
100
+ avoidPhrases: string[];
101
+ preferredPhrases: string[];
102
+
103
+ agentName: string | null;
104
+ agentRole: string | null;
105
+ agentTone: string | null;
106
+
107
+ isActive: boolean;
108
+ /** `true` for the immutable seed templates the backend ships with. */
109
+ isPreset: boolean;
110
+ presetType: PresetType | null;
111
+
112
+ /** ISO 8601 timestamp. */
113
+ createdAt: string;
114
+ /** ISO 8601 timestamp. */
115
+ updatedAt: string;
116
+ }
117
+
118
+ /**
119
+ * Definition of one immutable seed preset. Returned by the
120
+ * "list presets" endpoint so the console can render the picker
121
+ * without hard-coding the catalogue.
122
+ */
123
+ export interface PresetDefinitionWire {
124
+ type: PresetType;
125
+ name: string;
126
+ description: string;
127
+ traits: PersonalityTraitsWire;
128
+ /** Optional emoji / lucide icon name; console-renderer-defined. */
129
+ icon?: string;
130
+ }
131
+
132
+ /**
133
+ * Wire body of `POST /egox/personality/profiles`. `userId` is derived
134
+ * from the console JWT, not the body.
135
+ */
136
+ export interface PersonalityProfileCreateBody {
137
+ name: string;
138
+ traits?: Partial<PersonalityTraitsWire>;
139
+ customInstructions?: string;
140
+ exampleResponses?: string[];
141
+ avoidPhrases?: string[];
142
+ preferredPhrases?: string[];
143
+ agentName?: string;
144
+ agentRole?: string;
145
+ agentTone?: string;
146
+ }
147
+
148
+ /**
149
+ * Wire body of `PUT /egox/personality/profiles/:profileId`. Every
150
+ * field is optional; `null` is a meaningful value for the nullable
151
+ * string columns (clears them), `undefined` means "leave alone". The
152
+ * service distinguishes between the two.
153
+ */
154
+ export interface PersonalityProfileUpdateBody {
155
+ name?: string;
156
+ traits?: Partial<PersonalityTraitsWire>;
157
+ customInstructions?: string | null;
158
+ exampleResponses?: string[];
159
+ avoidPhrases?: string[];
160
+ preferredPhrases?: string[];
161
+ agentName?: string | null;
162
+ agentRole?: string | null;
163
+ agentTone?: string | null;
164
+ }
165
+
166
+ // ============================================================================
167
+ // Project-scoped personality (Item #6 — drives /ask)
168
+ // ============================================================================
169
+
170
+ /**
171
+ * Wire-format project-scoped personality profile. Returned by
172
+ * `/egox/tenants/:tenantId/personality/*`. The single row with
173
+ * `isActive: true` per tenant is what the orchestrator resolves for
174
+ * `/ask`.
175
+ *
176
+ * `templateSourceUserId` and `templateSourceProfileId` are nullable
177
+ * provenance pointers — preserved as `SET NULL` if the source
178
+ * template or its owner is deleted. They power the future "compare
179
+ * against template" UX without coupling project profile lifetime to
180
+ * template lifetime.
181
+ */
182
+ export interface TenantPersonalityProfileWire {
183
+ id: string;
184
+ tenantId: string;
185
+ name: string;
186
+ traits: PersonalityTraitsWire;
187
+
188
+ customInstructions: string | null;
189
+ exampleResponses: string[];
190
+ avoidPhrases: string[];
191
+ preferredPhrases: string[];
192
+
193
+ agentName: string | null;
194
+ agentRole: string | null;
195
+ agentTone: string | null;
196
+
197
+ isActive: boolean;
198
+
199
+ templateSourceUserId: string | null;
200
+ templateSourceProfileId: string | null;
201
+
202
+ /** ISO 8601 timestamp. */
203
+ createdAt: string;
204
+ /** ISO 8601 timestamp. */
205
+ updatedAt: string;
206
+ }
207
+
208
+ /**
209
+ * Wire body of `POST /egox/tenants/:tenantId/personality/profiles`
210
+ * (fresh, no template lineage). `tenantId` is the URL param, not the
211
+ * body.
212
+ */
213
+ export interface TenantPersonalityCreateBody {
214
+ name: string;
215
+ traits?: Partial<PersonalityTraitsWire>;
216
+ customInstructions?: string;
217
+ exampleResponses?: string[];
218
+ avoidPhrases?: string[];
219
+ preferredPhrases?: string[];
220
+ agentName?: string;
221
+ agentRole?: string;
222
+ agentTone?: string;
223
+ }
224
+
225
+ /**
226
+ * Wire body of `PUT /egox/tenants/:tenantId/personality/profiles/:profileId`.
227
+ * Same null-vs-undefined semantics as `PersonalityProfileUpdateBody`.
228
+ */
229
+ export interface TenantPersonalityUpdateBody {
230
+ name?: string;
231
+ traits?: Partial<PersonalityTraitsWire>;
232
+ customInstructions?: string | null;
233
+ exampleResponses?: string[];
234
+ avoidPhrases?: string[];
235
+ preferredPhrases?: string[];
236
+ agentName?: string | null;
237
+ agentRole?: string | null;
238
+ agentTone?: string | null;
239
+ }
240
+
241
+ /**
242
+ * Wire body of the clone endpoint. `sourceUserId` MUST be the
243
+ * requesting console user (validated server-side); a stolen
244
+ * `sourceProfileId` therefore can't be cloned across user boundaries.
245
+ */
246
+ export interface TenantPersonalityCloneBody {
247
+ sourceUserId: string;
248
+ sourceProfileId: string;
249
+ /** Defaults to `${source.name} (cloned)` server-side. */
250
+ name?: string;
251
+ }
252
+
253
+ // ============================================================================
254
+ // Resolution envelopes (returned by the "what's active?" endpoints)
255
+ // ============================================================================
256
+
257
+ /**
258
+ * Response of `GET /egox/personality/active`. `isDefault: true`
259
+ * means no user-level profile is selected and the future caller
260
+ * should treat the user as having no template-level preference.
261
+ */
262
+ export interface ActivePersonalityWire {
263
+ profile: PersonalityProfileWire | null;
264
+ isDefault: boolean;
265
+ }
266
+
267
+ /**
268
+ * Response of `GET /egox/tenants/:tenantId/personality/active`.
269
+ * `isDefault: true` means no project-level profile is active —
270
+ * `/ask` falls back to the orchestrator's built-in baseline prompt.
271
+ */
272
+ export interface ActiveTenantPersonalityWire {
273
+ profile: TenantPersonalityProfileWire | null;
274
+ isDefault: boolean;
275
+ }
package/src/rag.d.ts ADDED
@@ -0,0 +1,188 @@
1
+ /**
2
+ * @egox/contracts — `/egox/tenants/:tenantId/rag/documents/*` +
3
+ * `/egox/mcp-admin/rag-documents/*` wire types.
4
+ *
5
+ * Three independent consumers used to hand-mirror these shapes:
6
+ * - the backend (`modules/rag/interfaces.ts`)
7
+ * - the MCP server (`egox-mcp/src/backend-client.ts` — `BackendRagDoc`)
8
+ * - the console (`console/src/types/models.ts` — `RagDocument`)
9
+ * Each mirror had its own subtle drift:
10
+ * - MCP's `sourceType` / `processingStatus` were typed as bare `string`
11
+ * (no `'file'|'url'|'manual'` lock), and `sourceUrl` was missing entirely.
12
+ * - Console's `RagDocument` had no `source` (provenance) field at all,
13
+ * so the manual-vs-MCP distinction was invisible in the UI.
14
+ * - Backend timestamps were `Date` in-process but ISO strings on the
15
+ * wire — same wire/runtime mismatch we removed for Tools in P1.
16
+ * Phase 9.2 unifies all three under `RagDocumentWire`.
17
+ *
18
+ * Mirrors the pattern locked in P1 (`tool.d.ts`):
19
+ * - locked vocabularies prefixed `Rag*` to avoid collisions with
20
+ * future families that have their own "source type" concept;
21
+ * - timestamps as ISO 8601 `string` (backend `formatDocument` now
22
+ * emits ISO via the same `toIso` helper Tools uses);
23
+ * - request bodies omit context fields the route layer attaches
24
+ * (`tenantId`, identifying URL params).
25
+ *
26
+ * **Backend-only.** `RagChunk`, `RetrievedChunk`, `RetrievalRequest`,
27
+ * `RetrievalResponse`, and `ChunkOptions` are NOT wire types — they're
28
+ * orchestrator-internal retrieval shapes. The only chunk info that
29
+ * crosses the wire is the minimal `{ documentTitle, score }` pair
30
+ * already on `AskStreamRagRetrievedEvent` in `stream.d.ts`.
31
+ */
32
+
33
+ // ============================================================================
34
+ // Locked vocabularies
35
+ // ============================================================================
36
+
37
+ /**
38
+ * How a RAG document entered the system.
39
+ * - `'file'` — uploaded via Console.
40
+ * - `'url'` — fetched from a URL the user provided.
41
+ * - `'manual'` — pasted as text in the Console editor.
42
+ *
43
+ * Distinct from `RagDocumentSource` (provenance: which user surface
44
+ * created the row, `'manual'` console vs `'mcp'` Cursor flow).
45
+ */
46
+ export type RagDocumentSourceType = 'file' | 'url' | 'manual';
47
+
48
+ /**
49
+ * Async ingestion state. Documents start `'pending'`, advance to
50
+ * `'processing'` while chunking + embedding run, then settle at
51
+ * `'completed'` or `'failed'`. Console surfaces these as badge colors.
52
+ */
53
+ export type RagProcessingStatus = 'pending' | 'processing' | 'completed' | 'failed';
54
+
55
+ /**
56
+ * Provenance of the document.
57
+ * - `'manual'` — created in the Console.
58
+ * - `'mcp'` — created via the MCP server (Cursor / 3rd-party MCP
59
+ * client). Overwriting a `'manual'` document from MCP
60
+ * requires `force: true` (see `RagDocumentUpsertByTitleBody`).
61
+ *
62
+ * Structurally identical to `ToolSource` from `tool.d.ts` but kept as a
63
+ * distinct nominal type so a function expecting one can't be passed the
64
+ * other by mistake — each family owns its own source-of-truth vocab.
65
+ */
66
+ export type RagDocumentSource = 'manual' | 'mcp';
67
+
68
+ // ============================================================================
69
+ // RagDocumentWire — the canonical row shape
70
+ // ============================================================================
71
+
72
+ /**
73
+ * Wire-format RAG document row. Returned by every
74
+ * `/egox/tenants/:tenantId/rag/documents/*` and
75
+ * `/egox/mcp-admin/rag-documents/*` response that produces a document.
76
+ *
77
+ * Backend in-process `RagDocument` is an alias of this type. Timestamps
78
+ * are typed as `string` (ISO 8601) because that's what Fastify
79
+ * serialises Date values to; the backend's `formatDocument` emits ISO
80
+ * strings to match so wire and in-process shapes never disagree.
81
+ *
82
+ * The body of the document and its chunked content live in different
83
+ * places — this row carries the raw `content` (so consumers can
84
+ * display the source text) but does NOT include the derived chunks.
85
+ * Chunks are an orchestrator-internal concept; only the `chunkCount`
86
+ * is exposed.
87
+ */
88
+ export interface RagDocumentWire {
89
+ id: string;
90
+ /**
91
+ * Owning tenant. Present on every response for audit; the request
92
+ * side never carries it (the backend derives tenant from the API
93
+ * key / JWT context).
94
+ */
95
+ tenantId: string;
96
+ /**
97
+ * Display title. Also the natural key for the MCP upsert path
98
+ * (`PUT /egox/mcp-admin/rag-documents/:title`) — uniqueness is
99
+ * enforced per tenant.
100
+ */
101
+ title: string;
102
+ sourceType: RagDocumentSourceType;
103
+ /** Origin URL when `sourceType === 'url'`; `null` otherwise. */
104
+ sourceUrl: string | null;
105
+ /** Raw text content. May be large — Console truncates in list views. */
106
+ content: string;
107
+ /** Tenant-supplied tags. Used for retrieval filtering (`tags?: string[]` on `/ask`). */
108
+ tags: string[];
109
+ /** Free-form per-document metadata (provenance hints, authoring info, etc.). */
110
+ metadata: Record<string, unknown>;
111
+ processingStatus: RagProcessingStatus;
112
+ /** Number of chunks the document was split into. `0` until processing completes. */
113
+ chunkCount: number;
114
+ source: RagDocumentSource;
115
+ /** ISO 8601 timestamp. */
116
+ createdAt: string;
117
+ /** ISO 8601 timestamp. */
118
+ updatedAt: string;
119
+ }
120
+
121
+ // ============================================================================
122
+ // Request bodies (what the client SENDS)
123
+ // ============================================================================
124
+
125
+ /**
126
+ * Wire body of `POST /egox/tenants/:tenantId/rag/documents` (the
127
+ * Console admin path). Notable absences:
128
+ * - `tenantId` — URL param, not body.
129
+ * - `processingStatus` — server-managed, starts at `'pending'`.
130
+ * - `chunkCount` — server-managed, populated by the chunker.
131
+ * - `source` — admin route hardcodes `'manual'`; the MCP
132
+ * path uses `RagDocumentUpsertByTitleBody`
133
+ * and hardcodes `'mcp'`.
134
+ * - timestamps — server-managed.
135
+ */
136
+ export interface RagDocumentCreateBody {
137
+ title: string;
138
+ content: string;
139
+ sourceType?: RagDocumentSourceType;
140
+ sourceUrl?: string;
141
+ tags?: string[];
142
+ metadata?: Record<string, unknown>;
143
+ }
144
+
145
+ /**
146
+ * Wire body of `PUT /egox/mcp-admin/rag-documents/:title` (the MCP
147
+ * idempotent upsert path). Differs from `RagDocumentCreateBody`:
148
+ * - `title` — URL param, not body.
149
+ * - `sourceType`/`sourceUrl` — not accepted; MCP-created docs are
150
+ * always `sourceType: 'manual'`,
151
+ * `sourceUrl: null`, `source: 'mcp'`.
152
+ * - `force` — opt-in escape hatch to overwrite a `'manual'`
153
+ * document of the same title. Without it the
154
+ * backend returns `McpManualOverrideRequired`.
155
+ */
156
+ export interface RagDocumentUpsertByTitleBody {
157
+ content: string;
158
+ tags?: string[];
159
+ metadata?: Record<string, unknown>;
160
+ /** Set to `true` to overwrite a `'manual'`-source document of the same title. */
161
+ force?: boolean;
162
+ }
163
+
164
+ // ============================================================================
165
+ // Response data shapes (carried inside `EgoxApiResponse.data`)
166
+ // ============================================================================
167
+
168
+ export interface RagDocumentListResponseData {
169
+ documents: RagDocumentWire[];
170
+ }
171
+
172
+ export interface RagDocumentGetResponseData {
173
+ document: RagDocumentWire;
174
+ }
175
+
176
+ /**
177
+ * Response payload of the MCP upsert endpoint
178
+ * (`PUT /egox/mcp-admin/rag-documents/:title`).
179
+ * - `action: 'created'` — there was no prior row for this title.
180
+ * - `action: 'replaced'` — the old row was deleted and a fresh one
181
+ * created; chunks are recomputed asynchronously.
182
+ * - `message` — operator-facing hint about async processing.
183
+ */
184
+ export interface RagDocumentUpsertResponseData {
185
+ document: RagDocumentWire;
186
+ action: 'created' | 'replaced';
187
+ message: string;
188
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * search.d.ts — unified tenant search (`GET /egox/tenants/:tenantId/search`).
3
+ *
4
+ * Powers the console command-palette's dynamic fallback: when the static page
5
+ * matrix has no match, the palette searches this tenant's tools/endpoints and
6
+ * KB documents by keyword (min 3 chars) and indexes the results client-side.
7
+ */
8
+
9
+ /** A single searchable entity (tool/endpoint or KB document). */
10
+ export interface SearchResultItem {
11
+ type: 'tool' | 'document';
12
+ id: string;
13
+ /** Display name — tool `toolName` or document `title`. */
14
+ title: string;
15
+ /** Secondary line — tool `toolDescription` or document processing status. */
16
+ subtitle?: string;
17
+ tenantId: string;
18
+ }
19
+
20
+ /** Response of `GET /egox/tenants/:tenantId/search?q=<keyword>&limit=<n>`. */
21
+ export interface TenantSearchResponseData {
22
+ results: SearchResultItem[];
23
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * @egox/contracts — `/ask/stream` SSE event union.
3
+ *
4
+ * Each event is JSON-serialized and framed as a single `data: …\n\n`
5
+ * Server-Sent Event. Backend writes these; SDK + Console parse them.
6
+ * The discriminated union is the single source of truth for both sides
7
+ * so adding a new event variant is a single-edit, type-checked change
8
+ * across the stack.
9
+ *
10
+ * `AskStreamRequestBody` (the wire request shape) lives in `ask.d.ts`
11
+ * because it inherits from `AskRequestBody`.
12
+ */
13
+
14
+ import type { Intent, TokenUsage } from './envelope';
15
+
16
+ /**
17
+ * Discriminated union of every event the streaming `/ask` endpoint emits.
18
+ *
19
+ * Notes on subset behaviour:
20
+ * - `stream: 'minimal'` requests omit `rag.*` and `tool.*` events.
21
+ * - `done` is always the terminal success frame.
22
+ * - `error` may appear at any point and is also terminal.
23
+ */
24
+ export type AskStreamEvent =
25
+ | AskStreamIntentEvent
26
+ | AskStreamRagSearchingEvent
27
+ | AskStreamRagRetrievedEvent
28
+ | AskStreamDeltaEvent
29
+ | AskStreamToolCallingEvent
30
+ | AskStreamToolResultEvent
31
+ | AskStreamLLMDebugEvent
32
+ | AskStreamToolDebugEvent
33
+ | AskStreamDoneEvent
34
+ | AskStreamErrorEvent;
35
+
36
+ export interface AskStreamIntentEvent {
37
+ type: 'intent';
38
+ intent: Intent;
39
+ }
40
+
41
+ export interface AskStreamRagSearchingEvent {
42
+ type: 'rag.searching';
43
+ query: string;
44
+ }
45
+
46
+ export interface AskStreamRagRetrievedEvent {
47
+ type: 'rag.retrieved';
48
+ chunks: Array<{
49
+ documentTitle: string;
50
+ score: number;
51
+ }>;
52
+ }
53
+
54
+ export interface AskStreamDeltaEvent {
55
+ type: 'delta';
56
+ /** Token (or token-like fragment) appended to the running answer. */
57
+ content: string;
58
+ }
59
+
60
+ export interface AskStreamToolCallingEvent {
61
+ type: 'tool.calling';
62
+ name: string;
63
+ args: Record<string, unknown>;
64
+ }
65
+
66
+ export interface AskStreamToolResultEvent {
67
+ type: 'tool.result';
68
+ name: string;
69
+ success: boolean;
70
+ durationMs: number;
71
+ }
72
+
73
+ /**
74
+ * Debug-only message frame describing one LLM chat-completion iteration.
75
+ *
76
+ * Emitted only when the request was made with `debug: true` AND the
77
+ * caller is authenticated as a console JWT user. Safe to ignore from
78
+ * non-debug consumers — the field never appears outside that path.
79
+ */
80
+ export interface AskStreamLLMDebugEvent {
81
+ type: 'llm.debug';
82
+ /** Zero-based index in the orchestrator's tool loop. */
83
+ iteration: number;
84
+ /** Resolved model id, e.g. `gpt-4.1-mini-2025-04-14`. */
85
+ model: string;
86
+ /** Snapshot of the messages array sent INTO this iteration. */
87
+ messages: Array<{
88
+ role: 'system' | 'user' | 'assistant' | 'tool';
89
+ content: string | null;
90
+ name?: string;
91
+ toolCallId?: string;
92
+ toolCalls?: Array<{
93
+ id: string;
94
+ type: 'function';
95
+ function: { name: string; arguments: string };
96
+ }>;
97
+ }>;
98
+ /** What the LLM emitted this iteration (text + tool-call requests). */
99
+ response: {
100
+ content: string;
101
+ finishReason: string;
102
+ toolCalls?: Array<{
103
+ id: string;
104
+ type: 'function';
105
+ function: { name: string; arguments: string };
106
+ }>;
107
+ };
108
+ usage: TokenUsage;
109
+ }
110
+
111
+ /**
112
+ * Debug-only frame describing one tool HTTP execution — the actual
113
+ * request EgoX sent to the tenant server and the response it received.
114
+ *
115
+ * Emitted only when the request was made with `debug: true` AND the
116
+ * caller is authenticated as a console JWT user.
117
+ *
118
+ * Sensitive header values (Authorization, X-Egox-Signature,
119
+ * X-Egox-Timestamp, and anything matching /token|secret|key|auth/i) are
120
+ * already redacted to `***` server-side before this frame is written.
121
+ */
122
+ export interface AskStreamToolDebugEvent {
123
+ type: 'tool.debug';
124
+ name: string;
125
+ durationMs: number;
126
+ success: boolean;
127
+ /** What EgoX sent to the tenant server. */
128
+ request: {
129
+ method: string;
130
+ url: string;
131
+ headers: Record<string, string>;
132
+ body: unknown;
133
+ };
134
+ /** What came back. `statusCode` is null on a network exception. */
135
+ response: {
136
+ statusCode: number | null;
137
+ headers: Record<string, string>;
138
+ body: unknown;
139
+ error?: string;
140
+ };
141
+ /** Total number of attempts including the final one (1 + retries). */
142
+ attempts: number;
143
+ /**
144
+ * Provenance map of which tool-allowlisted headers were actually
145
+ * forwarded and from which source (`'body'` =
146
+ * `AskRequestBody.toolHeaders`, `'inbound'` = inbound HTTP
147
+ * forwarded by `tool.forwardHeaders` allowlist). Header VALUES
148
+ * are NOT included — they're already in `request.headers`
149
+ * (redacted there). Useful for debugging "why did/didn't my
150
+ * header get forwarded?" without leaking secrets a second time.
151
+ * Omitted when no headers were forwarded.
152
+ */
153
+ forwardedHeaderSources?: Record<string, 'body' | 'inbound'>;
154
+ }
155
+
156
+ export interface AskStreamDoneEvent {
157
+ type: 'done';
158
+ threadId: string;
159
+ /**
160
+ * Echo of `AskStreamRequestBody.externalThreadId` when one was
161
+ * supplied — same semantics as `AskResponseData.externalThreadId`.
162
+ * Lets streaming consumers correlate the SSE result back to their
163
+ * own conversation id without parsing the final `data` payload
164
+ * separately (Phase 6).
165
+ */
166
+ externalThreadId?: string;
167
+ intent: Intent;
168
+ toolUsed: string | null;
169
+ ragChunksUsed: number;
170
+ model: string;
171
+ usage: TokenUsage;
172
+ /**
173
+ * Pending high-impact actions awaiting user approval (Path-A, DotCollab #8).
174
+ * Same semantics + shape as `AskResponseData.pendingActions` — surfaced on
175
+ * the stream's terminal event so streaming consumers can render the
176
+ * Approve/Reject card without parsing the final `data` payload separately.
177
+ */
178
+ pendingActions?: Array<{ approvalId: string; toolName: string; summary: string }>;
179
+ }
180
+
181
+ export interface AskStreamErrorEvent {
182
+ type: 'error';
183
+ message: string;
184
+ /** Machine-readable error code, e.g. `INVALID_INPUT`, `INTERNAL_SERVER_ERROR`. */
185
+ code?: string;
186
+ }