@m6d/cortex-cli 0.15.0 → 0.17.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.
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { REQUEST_LOCATION_TOOL } from "../client-tools/index";
3
3
  import { backendErrorSchema } from "../errors/index";
4
4
  import { pendingApprovalSchema, TRIGGER_ANSWER_TOOL_NAME } from "../triggers/index";
5
- import { isRecord, unescapePointerToken } from "../json/index";
5
+ import { isRecord, readJsonPointer } from "../json/index";
6
6
  import { APPROVAL_TITLE_FIELD, LOCALES } from "../wire/index";
7
7
 
8
8
  /*
@@ -22,8 +22,11 @@ export const END_USER_TOKEN_HEADER = "X-End-User-Token";
22
22
  export const AGENT_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
23
23
  export const agentSlugSchema = z.string().regex(AGENT_SLUG_PATTERN);
24
24
 
25
+ /** The longest end-user id Cortex stores. */
26
+ export const USER_ID_MAX_LENGTH = 255;
27
+
25
28
  /** An end user's id, as wide as Cortex stores one. */
26
- export const userIdSchema = z.string().max(255);
29
+ export const userIdSchema = z.string().max(USER_ID_MAX_LENGTH);
27
30
 
28
31
  /**
29
32
  * An ASCII JavaScript identifier: what a tool name (a `tools.<name>()`
@@ -32,8 +35,22 @@ export const userIdSchema = z.string().max(255);
32
35
  */
33
36
  export const IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
34
37
 
38
+ /** The longest identifier Cortex stores, for tool, variable and MCP server names alike. */
39
+ export const IDENTIFIER_MAX_LENGTH = 64;
40
+
41
+ export const identifierSchema = z.string().regex(IDENTIFIER_PATTERN).max(IDENTIFIER_MAX_LENGTH);
42
+
43
+ /** A localized value's key. */
35
44
  export const localeSchema = z.enum(LOCALES);
36
45
 
46
+ /** How many variables and tools a server's manifest declares at most, and how long each of their texts runs. */
47
+ export const SERVER_MANIFEST_LIMITS = {
48
+ variables: 100,
49
+ tools: 500,
50
+ description: 500,
51
+ example: 200,
52
+ } as const;
53
+
37
54
  /** The value one thread's variable holds: what its server fills, or an operator sets on a test thread. */
38
55
  const variableValueSchema = z.union([z.string(), z.number(), z.boolean()]);
39
56
 
@@ -46,9 +63,9 @@ export const variableValuesSchema = z.record(z.string(), variableValueSchema);
46
63
  * prompts and as tool input sources alike.
47
64
  */
48
65
  export const variableDeclarationSchema = z.object({
49
- name: z.string().regex(IDENTIFIER_PATTERN).max(64),
50
- description: z.string().max(500),
51
- example: z.string().max(200),
66
+ name: identifierSchema,
67
+ description: z.string().max(SERVER_MANIFEST_LIMITS.description),
68
+ example: z.string().max(SERVER_MANIFEST_LIMITS.example),
52
69
  /** The type its value takes. */
53
70
  type: z.enum(["string", "number", "boolean"]),
54
71
  /** Computed by Cortex for every thread; no server can set it. */
@@ -86,8 +103,8 @@ export const BUILT_IN_VARIABLES = [
86
103
 
87
104
  /**
88
105
  * The names of Cortex's built-in tools. No other tool may take one: the
89
- * model would see two tools under one name. The .NET SDK holds its own copy,
90
- * checked against `reserved-names.fixtures.json`.
106
+ * model would see two tools under one name. `constants.fixtures.json`
107
+ * carries them to the .NET SDK.
91
108
  */
92
109
  export const BUILT_IN_TOOL_NAMES = [
93
110
  "describeDataset",
@@ -107,30 +124,22 @@ export function isBuiltInToolName(name: string) {
107
124
  return BUILT_IN_TOOL_NAMES.some((builtIn) => builtIn === name);
108
125
  }
109
126
 
110
- /** The longest name a tool takes, a server's or one defined in Cortex. */
111
- export const TOOL_NAME_MAX_LENGTH = 64;
112
-
113
127
  /** A tool name, a server's or one defined in Cortex: an identifier no built-in tool holds. */
114
- const declaredToolNameSchema = z
115
- .string()
116
- .regex(IDENTIFIER_PATTERN)
117
- .max(TOOL_NAME_MAX_LENGTH)
118
- .refine((name) => !isBuiltInToolName(name), "the name of a built-in tool");
128
+ const declaredToolNameSchema = identifierSchema.refine(
129
+ (name) => !isBuiltInToolName(name),
130
+ "the name of a built-in tool",
131
+ );
119
132
 
120
133
  const jsonSchemaSchema = z.record(z.string(), z.unknown());
121
134
 
122
135
  /** A variable the server fills for its agents' threads; the built-in ones are Cortex's. */
123
136
  const serverVariableSchema = z.object({
124
- name: z
125
- .string()
126
- .regex(IDENTIFIER_PATTERN)
127
- .max(64)
128
- .refine(
129
- (name) => !BUILT_IN_VARIABLES.some((builtIn) => builtIn.name === name),
130
- "the name of a built-in variable",
131
- ),
132
- description: z.string().max(500),
133
- example: z.string().max(200),
137
+ name: identifierSchema.refine(
138
+ (name) => !BUILT_IN_VARIABLES.some((builtIn) => builtIn.name === name),
139
+ "the name of a built-in variable",
140
+ ),
141
+ description: z.string().max(SERVER_MANIFEST_LIMITS.description),
142
+ example: z.string().max(SERVER_MANIFEST_LIMITS.example),
134
143
  type: variableDeclarationSchema.shape.type,
135
144
  });
136
145
  export type ServerVariable = z.infer<typeof serverVariableSchema>;
@@ -143,7 +152,7 @@ export type ServerVariable = z.infer<typeof serverVariableSchema>;
143
152
  */
144
153
  export const serverToolSchema = z.object({
145
154
  name: declaredToolNameSchema,
146
- description: z.string().trim().min(1).max(500),
155
+ description: z.string().trim().min(1).max(SERVER_MANIFEST_LIMITS.description),
147
156
  inputSchema: jsonSchemaSchema.refine(
148
157
  (schema) => schema.type === "object",
149
158
  "a tool's input is an object",
@@ -182,27 +191,9 @@ export const approvalRequestedEventSchema = z.object({
182
191
  });
183
192
  export type ApprovalRequestedEvent = z.infer<typeof approvalRequestedEventSchema>;
184
193
 
185
- /** A JSON Pointer token as a key: percent-decoded, then unescaped; undefined when its encoding is broken. */
186
- function pointerKey(token: string) {
187
- try {
188
- return unescapePointerToken(decodeURIComponent(token));
189
- } catch {
190
- return undefined;
191
- }
192
- }
193
-
194
194
  /** What a local `$ref` (`#/$defs/order`) points at in `root`, read as the input compiler reads it; undefined for any other. */
195
195
  function pointedAt(root: unknown, ref: string) {
196
- if (!ref.startsWith("#")) return undefined;
197
- let node = root;
198
- for (const token of ref.slice(1).split("/").slice(1)) {
199
- const key = pointerKey(token);
200
- node =
201
- key !== undefined && (isRecord(node) || Array.isArray(node))
202
- ? new Map<string, unknown>(Object.entries(node)).get(key)
203
- : undefined;
204
- }
205
- return node;
196
+ return ref.startsWith("#") ? readJsonPointer(root, ref) : undefined;
206
197
  }
207
198
 
208
199
  /** Whether one schema node names `APPROVAL_TITLE_FIELD` as a property or a required field. */
@@ -259,18 +250,15 @@ export function blocksApprovalTitle(inputSchema: unknown) {
259
250
  );
260
251
  }
261
252
 
262
- /** Refuses, under `path`, a tool that needs approval whose input `blocksApprovalTitle`. */
253
+ /** Refuses, under `path` and with `message`, a tool that needs approval whose input `blocksApprovalTitle`. */
263
254
  export function refuseBlockedApprovalTitle(
264
255
  tool: { requiresApproval?: boolean; inputSchema: unknown },
265
256
  context: z.RefinementCtx,
266
257
  path: Array<string | number>,
258
+ message = `a tool that needs approval cannot take an input named ${APPROVAL_TITLE_FIELD}, nor one a closed sub-schema describes: Cortex adds the approval card's title to it`,
267
259
  ) {
268
260
  if (!tool.requiresApproval || !blocksApprovalTitle(tool.inputSchema)) return;
269
- context.addIssue({
270
- code: "custom",
271
- path: [...path, "inputSchema"],
272
- message: `a tool that needs approval cannot take an input named ${APPROVAL_TITLE_FIELD}, nor one a closed sub-schema describes: Cortex adds the approval card's title to it`,
273
- });
261
+ context.addIssue({ code: "custom", path: [...path, "inputSchema"], message });
274
262
  }
275
263
 
276
264
  function uniqueNames(entries: ReadonlyArray<{ name: string }>) {
@@ -280,10 +268,13 @@ function uniqueNames(entries: ReadonlyArray<{ name: string }>) {
280
268
  /** `GET /`: everything the server offers, keyed by `version`, which changes whenever anything else does. */
281
269
  export const serverManifestSchema = z.object({
282
270
  version: z.string().min(1).max(128),
283
- variables: z.array(serverVariableSchema).max(100).refine(uniqueNames, "variable names repeat"),
271
+ variables: z
272
+ .array(serverVariableSchema)
273
+ .max(SERVER_MANIFEST_LIMITS.variables)
274
+ .refine(uniqueNames, "variable names repeat"),
284
275
  tools: z
285
276
  .array(serverToolSchema)
286
- .max(500)
277
+ .max(SERVER_MANIFEST_LIMITS.tools)
287
278
  .refine(uniqueNames, "tool names repeat")
288
279
  .superRefine((tools, context) => {
289
280
  tools.forEach((tool, index) => refuseBlockedApprovalTitle(tool, context, [index]));
@@ -1,5 +1,7 @@
1
1
  import { z } from "zod";
2
2
 
3
+ import { TRIGGER_DROP_REASONS, TRIGGER_REASONS } from "../wire/index";
4
+
3
5
  /*
4
6
  * Triggers: named entry points on an agent. A firing is one invocation with a
5
7
  * payload and starts at most one run. The end user's message is the built-in
@@ -24,13 +26,15 @@ export const TRIGGER_OUTCOMES = [
24
26
  ] as const;
25
27
  export type TriggerOutcome = (typeof TRIGGER_OUTCOMES)[number];
26
28
 
27
- /**
28
- * Why a firing was dropped: the thread was busy with another run, a call on
29
- * it waits for a decision (answered 409: decide that firing first), the
30
- * daily budget was spent, or the agent's request bucket was.
31
- */
32
- const TRIGGER_DROP_REASONS = ["busy", "approval_pending", "budget", "rate_limited"] as const;
33
- export type TriggerDropReason = (typeof TRIGGER_DROP_REASONS)[number];
29
+ /** The engine route of agent `agentSlug`, which the widget chats with and its triggers sit under. */
30
+ export function agentRoute(agentSlug: string) {
31
+ return `/agents/${encodeURIComponent(agentSlug)}`;
32
+ }
33
+
34
+ /** The engine route a trigger is fired at on agent `agentSlug`; its firings are read and answered under it. */
35
+ export function triggerRoute(agentSlug: string, name: string) {
36
+ return `${agentRoute(agentSlug)}/triggers/${encodeURIComponent(name)}`;
37
+ }
34
38
 
35
39
  /** One server tool call a parked run waits on; `POST .../firings/:id/answer` decides it. */
36
40
  export const pendingApprovalSchema = z.object({
@@ -67,7 +71,7 @@ export const triggerFiringResponseSchema = z.discriminatedUnion("outcome", [
67
71
  outcome: z.literal("failed"),
68
72
  threadId: z.uuid(),
69
73
  runId: z.string(),
70
- reason: z.string().nullable(),
74
+ reason: z.enum(TRIGGER_REASONS).nullable(),
71
75
  }),
72
76
  z.object({
73
77
  firingId: z.uuid(),
@@ -101,7 +105,7 @@ export const triggerFiringRecordSchema = z.object({
101
105
  threadId: z.uuid().nullable(),
102
106
  runId: z.string().nullable(),
103
107
  outcome: z.enum(TRIGGER_OUTCOMES),
104
- reason: z.string().nullable(),
108
+ reason: z.enum(TRIGGER_REASONS).nullable(),
105
109
  /** What a completed run answered with, as a fire's `completed` answer carries it; null otherwise. */
106
110
  output: z.unknown(),
107
111
  /** The calls an `awaiting_approval` firing waits on; filled by the single-firing read, empty in the list and for every other outcome. */
@@ -8,10 +8,25 @@
8
8
  * alike.
9
9
  */
10
10
 
11
- /** The UI locales every localized value is keyed by. */
11
+ /** The UI locales every localized value is keyed by; the console's message catalogs follow it. */
12
12
  export const LOCALES = ["en", "ar"] as const;
13
13
  export type Locale = (typeof LOCALES)[number];
14
14
 
15
+ /** The locale a value falls back to, and the one a service is known by. */
16
+ export const BASE_LOCALE = LOCALES[0];
17
+
18
+ /** The way each locale's script runs, which the console lays its text out by. */
19
+ export const LOCALE_DIRECTIONS = { en: "ltr", ar: "rtl" } as const satisfies Record<
20
+ Locale,
21
+ "ltr" | "rtl"
22
+ >;
23
+
24
+ /** A localized value with one entry per locale, each `valueOf` that locale. */
25
+ export function byLocale<T>(valueOf: (locale: Locale) => T) {
26
+ const entries = LOCALES.map((locale) => [locale, valueOf(locale)] as const);
27
+ return Object.fromEntries(entries) as Record<Locale, T>;
28
+ }
29
+
15
30
  /** Where an embedded client tool renders: in the message flow or in a modal. */
16
31
  export const TOOL_SURFACES = ["inline", "modal"] as const;
17
32
  export type ToolSurface = (typeof TOOL_SURFACES)[number];
@@ -70,6 +85,32 @@ export type AttachmentSummary = {
70
85
  description: string | null;
71
86
  };
72
87
 
88
+ /**
89
+ * How an anonymous agent's visitors' user ids begin: Cortex issues each
90
+ * visitor an id of its own, which their browser keeps. No issuer ever mints
91
+ * a subject shaped like this.
92
+ */
93
+ export const VISITOR_USER_PREFIX = "visitor:";
94
+
95
+ /** The reCAPTCHA action a visitor's sign-in runs, which Cortex checks the token was made for. */
96
+ export const VISITOR_RECAPTCHA_ACTION = "cortex_visitor";
97
+
98
+ /** `POST /visitors`: a visitor's sign-in to an anonymous agent, with a reCAPTCHA token when the agent checks visitors. */
99
+ export type VisitorRequest = { recaptcha?: string };
100
+
101
+ /** A visitor's token, the bearer of every request after the sign-in. */
102
+ export type VisitorToken = { token: string };
103
+
104
+ /**
105
+ * Why a sign-in got no token: the agent checks visitors with reCAPTCHA and
106
+ * the request carried no token, or one that failed. `siteKey` is the key the
107
+ * widget runs reCAPTCHA with.
108
+ */
109
+ export type VisitorRefusal = {
110
+ error: "recaptcha_required" | "recaptcha_failed";
111
+ siteKey: string;
112
+ };
113
+
73
114
  export type ThreadSummary = {
74
115
  id: string;
75
116
  title: string | null;
@@ -121,8 +162,6 @@ export type ClientToolBinding = {
121
162
  toolId: string;
122
163
  surface: ToolSurface;
123
164
  embedOrigin: string;
124
- /** Variables the initiate call's input reads. */
125
- variables: string[];
126
165
  };
127
166
 
128
167
  /**
@@ -189,11 +228,16 @@ export const STATEMENT_TOOLS = ["executeCode", "queryDataset", "describeDataset"
189
228
  */
190
229
  export const APPROVAL_TITLE_FIELD = "approvalTitle";
191
230
 
231
+ /** A line the model wrote into a call input's `field`, trimmed; undefined while it streams in or is blank. */
232
+ export function modelLineOf(input: unknown, field: string) {
233
+ if (!input || typeof input !== "object") return undefined;
234
+ const line: unknown = Reflect.get(input, field);
235
+ return typeof line === "string" && line.trim() ? line.trim() : undefined;
236
+ }
237
+
192
238
  /** The title the model wrote into an approval call's input, or undefined while it has none. */
193
239
  export function approvalTitleOf(input: unknown) {
194
- if (!input || typeof input !== "object" || !(APPROVAL_TITLE_FIELD in input)) return undefined;
195
- const title: unknown = input[APPROVAL_TITLE_FIELD];
196
- return typeof title === "string" && title.trim() ? title.trim() : undefined;
240
+ return modelLineOf(input, APPROVAL_TITLE_FIELD);
197
241
  }
198
242
 
199
243
  /**
@@ -260,16 +304,27 @@ export function toUiMessage<TPart>(message: CortexMessage<TPart>) {
260
304
  export const CLIENT_TOOL_ANSWERS_KEY = "clientToolAnswers";
261
305
 
262
306
  /**
263
- * One answered call, which Cortex applies to the stored transcript: a client
264
- * tool's output (`complete` / `error`), or the user's decision on a server
265
- * tool that asked for approval (`approved` / `denied`, output null).
307
+ * How a call was answered: a client tool's output (`complete` / `error`), or
308
+ * the user's decision on a server tool that asked for approval
309
+ * (`approved` / `denied`).
310
+ */
311
+ export const CLIENT_TOOL_ANSWER_STATES = ["complete", "error", "approved", "denied"] as const;
312
+
313
+ /**
314
+ * One answered call, which Cortex applies to the stored transcript. A
315
+ * decision carries a null output.
266
316
  */
267
317
  export type ClientToolAnswer = {
268
318
  toolCallId: string;
269
319
  output: unknown;
270
- state: "complete" | "error" | "approved" | "denied";
320
+ state: (typeof CLIENT_TOOL_ANSWER_STATES)[number];
271
321
  };
272
322
 
323
+ /** Whether the answer is the user's decision on a call that asked for approval, rather than a tool's output. */
324
+ export function isDecision(answer: Pick<ClientToolAnswer, "state">) {
325
+ return answer.state === "approved" || answer.state === "denied";
326
+ }
327
+
273
328
  /**
274
329
  * The id a parked tool call's decision is filed under: the one on its
275
330
  * approval record, or `approval_<toolCallId>`, which is how the SDK names
@@ -279,6 +334,11 @@ export function approvalIdOf(part: { id: string; approval?: { id: string } }) {
279
334
  return part.approval?.id ?? `approval_${part.id}`;
280
335
  }
281
336
 
337
+ /** Whether a tool call asked the user for approval: it carries an approval record, or waits for one. */
338
+ export function askedApproval(part: { approval?: unknown; state?: string }) {
339
+ return part.approval !== undefined || part.state === "approval-requested";
340
+ }
341
+
282
342
  /**
283
343
  * Response of `POST /chat/:threadId/tools/:toolCallId/initiate` for a pending
284
344
  * call stamped with an embed. The payload is for the widget only; it never
@@ -328,40 +388,102 @@ export type ThreadMessagesUpdatedEvent = {
328
388
  payload: { thread: ThreadSummary };
329
389
  };
330
390
 
331
- export type ToolProgressEvent = {
332
- type: "thread:tool-progress";
333
- payload: {
334
- threadId: string;
335
- toolCallId: string;
336
- toolName: string;
337
- status: "started" | "finished";
338
- };
339
- };
340
-
341
391
  export type WsEvent =
342
392
  | ThreadCreatedEvent
343
393
  | ThreadDeletedEvent
344
394
  | ThreadTitleUpdatedEvent
345
395
  | ThreadRunStartedEvent
346
396
  | ThreadRunFinishedEvent
347
- | ThreadMessagesUpdatedEvent
348
- | ToolProgressEvent;
397
+ | ThreadMessagesUpdatedEvent;
349
398
 
350
399
  /** The header the widget sends the browser's IANA time zone in; the turn reasons in that clock. */
351
400
  export const TIMEZONE_HEADER = "X-Cortex-Timezone";
352
401
 
353
402
  /**
354
- * The console operator's short-lived token, sent by the playground and the
355
- * thread inspector on every request to an agent and by the fire and answer
356
- * calls Cortex makes to a trigger's route. A verified one makes the operator the caller unless the
357
- * request also carries an end-user token to test as, and marks the threads it
358
- * opens as test threads. A WebSocket upgrade carries it as `?operator=`.
403
+ * The header carrying a console operator's token, sent by the playground and
404
+ * the thread inspector on every request to an agent and by the fire and
405
+ * answer calls Cortex makes to a trigger's route. A verified one makes the
406
+ * operator the caller unless the request also carries an end-user token to
407
+ * test as, and marks the threads it opens as test threads. A WebSocket
408
+ * upgrade carries it as `?operator=`.
359
409
  */
360
410
  export const OPERATOR_HEADER = "X-Cortex-Operator";
361
411
 
362
412
  /**
363
413
  * Beside the operator token, the id of a thread the operator inspects: the
364
414
  * request then reads as that thread's owner, read-only, with the debug view.
415
+ * The engine refuses any request carrying it but a read (GET, HEAD, OPTIONS),
416
+ * on every agent route.
365
417
  * A WebSocket upgrade carries it as `?inspect=`.
366
418
  */
367
419
  export const INSPECT_HEADER = "X-Cortex-Inspect";
420
+
421
+ /**
422
+ * Why a firing was dropped: the thread was busy with another run, a call on
423
+ * it waits for a decision (answered 409: decide that firing first), the
424
+ * daily budget was spent, or the agent's request bucket was.
425
+ */
426
+ export const TRIGGER_DROP_REASONS = ["busy", "approval_pending", "budget", "rate_limited"] as const;
427
+ export type TriggerDropReason = (typeof TRIGGER_DROP_REASONS)[number];
428
+
429
+ /**
430
+ * Every reason a firing records beside its outcome: why it was dropped, why
431
+ * its payload was rejected (`invalid_payload`, `missing_thread_key`), that
432
+ * no turn ran for it (`no_turn`), or why a run with an output schema failed
433
+ * without an answer the schema accepted (`no_output`, `invalid_output`).
434
+ */
435
+ export const TRIGGER_REASONS = [
436
+ ...TRIGGER_DROP_REASONS,
437
+ "invalid_payload",
438
+ "missing_thread_key",
439
+ "no_turn",
440
+ "no_output",
441
+ "invalid_output",
442
+ ] as const;
443
+ export type TriggerReason = (typeof TRIGGER_REASONS)[number];
444
+
445
+ /**
446
+ * Body of a `POST /chat` the thread did not take: 409 while a run holds the
447
+ * thread or a call on it waits for a decision, or when the answers it
448
+ * carried fit no open call, were already applied, or answer a call whose
449
+ * origin is out of reach (`no_turn`); 429 once the agent's request bucket is
450
+ * spent, with `Retry-After`.
451
+ */
452
+ export type ChatDropped = {
453
+ outcome: "dropped";
454
+ reason: Exclude<TriggerDropReason, "budget"> | "no_turn";
455
+ };
456
+
457
+ /**
458
+ * The drop a widget tells its user about by name: a call on the thread waits
459
+ * for approval, so the message goes back to the composer. Any other drop
460
+ * shows as a failed turn.
461
+ */
462
+ export type SendRefusal = Extract<ChatDropped["reason"], "approval_pending">;
463
+
464
+ /**
465
+ * What became of the client tool answers a `POST /chat` carried, by the
466
+ * status Cortex answered it with, apart from how the run it started goes.
467
+ * `persisted` (2xx): Cortex took them up, and sending them again changes
468
+ * nothing. `refused` (4xx, a `ChatDropped` among them): it did not take
469
+ * them up as given, and the stored transcript says what stands. `lost`
470
+ * (5xx, or no answer at all): its word never came, and only the stored
471
+ * transcript can tell.
472
+ */
473
+ export function answersReceiptOf(status: number) {
474
+ if (status >= 500) return "lost";
475
+ return status >= 400 ? "refused" : "persisted";
476
+ }
477
+ export type AnswersReceipt = ReturnType<typeof answersReceiptOf>;
478
+
479
+ /**
480
+ * One model call of the turn that wrote a message, raw, in the order the
481
+ * turn ran them: `GET /threads/:threadId/messages/:messageId/llm-requests`
482
+ * answers a list, for the request inspector.
483
+ */
484
+ export type LlmRequest = {
485
+ id: string;
486
+ prompt: string;
487
+ output: string | null;
488
+ tokenUsage: TokenUsage | null;
489
+ };