@m6d/cortex-cli 0.15.0 → 0.16.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,9 @@ export type AttachmentSummary = {
70
85
  description: string | null;
71
86
  };
72
87
 
88
+ /** The user id every caller shares on an agent that verifies no end user. */
89
+ export const ANONYMOUS_USER_ID = "00000000-0000-0000-0000-000000000000";
90
+
73
91
  export type ThreadSummary = {
74
92
  id: string;
75
93
  title: string | null;
@@ -189,11 +207,16 @@ export const STATEMENT_TOOLS = ["executeCode", "queryDataset", "describeDataset"
189
207
  */
190
208
  export const APPROVAL_TITLE_FIELD = "approvalTitle";
191
209
 
210
+ /** A line the model wrote into a call input's `field`, trimmed; undefined while it streams in or is blank. */
211
+ export function modelLineOf(input: unknown, field: string) {
212
+ if (!input || typeof input !== "object") return undefined;
213
+ const line: unknown = Reflect.get(input, field);
214
+ return typeof line === "string" && line.trim() ? line.trim() : undefined;
215
+ }
216
+
192
217
  /** The title the model wrote into an approval call's input, or undefined while it has none. */
193
218
  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;
219
+ return modelLineOf(input, APPROVAL_TITLE_FIELD);
197
220
  }
198
221
 
199
222
  /**
@@ -260,16 +283,27 @@ export function toUiMessage<TPart>(message: CortexMessage<TPart>) {
260
283
  export const CLIENT_TOOL_ANSWERS_KEY = "clientToolAnswers";
261
284
 
262
285
  /**
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).
286
+ * How a call was answered: a client tool's output (`complete` / `error`), or
287
+ * the user's decision on a server tool that asked for approval
288
+ * (`approved` / `denied`).
289
+ */
290
+ export const CLIENT_TOOL_ANSWER_STATES = ["complete", "error", "approved", "denied"] as const;
291
+
292
+ /**
293
+ * One answered call, which Cortex applies to the stored transcript. A
294
+ * decision carries a null output.
266
295
  */
267
296
  export type ClientToolAnswer = {
268
297
  toolCallId: string;
269
298
  output: unknown;
270
- state: "complete" | "error" | "approved" | "denied";
299
+ state: (typeof CLIENT_TOOL_ANSWER_STATES)[number];
271
300
  };
272
301
 
302
+ /** Whether the answer is the user's decision on a call that asked for approval, rather than a tool's output. */
303
+ export function isDecision(answer: Pick<ClientToolAnswer, "state">) {
304
+ return answer.state === "approved" || answer.state === "denied";
305
+ }
306
+
273
307
  /**
274
308
  * The id a parked tool call's decision is filed under: the one on its
275
309
  * approval record, or `approval_<toolCallId>`, which is how the SDK names
@@ -279,6 +313,11 @@ export function approvalIdOf(part: { id: string; approval?: { id: string } }) {
279
313
  return part.approval?.id ?? `approval_${part.id}`;
280
314
  }
281
315
 
316
+ /** Whether a tool call asked the user for approval: it carries an approval record, or waits for one. */
317
+ export function askedApproval(part: { approval?: unknown; state?: string }) {
318
+ return part.approval !== undefined || part.state === "approval-requested";
319
+ }
320
+
282
321
  /**
283
322
  * Response of `POST /chat/:threadId/tools/:toolCallId/initiate` for a pending
284
323
  * call stamped with an embed. The payload is for the widget only; it never
@@ -328,40 +367,102 @@ export type ThreadMessagesUpdatedEvent = {
328
367
  payload: { thread: ThreadSummary };
329
368
  };
330
369
 
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
370
  export type WsEvent =
342
371
  | ThreadCreatedEvent
343
372
  | ThreadDeletedEvent
344
373
  | ThreadTitleUpdatedEvent
345
374
  | ThreadRunStartedEvent
346
375
  | ThreadRunFinishedEvent
347
- | ThreadMessagesUpdatedEvent
348
- | ToolProgressEvent;
376
+ | ThreadMessagesUpdatedEvent;
349
377
 
350
378
  /** The header the widget sends the browser's IANA time zone in; the turn reasons in that clock. */
351
379
  export const TIMEZONE_HEADER = "X-Cortex-Timezone";
352
380
 
353
381
  /**
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=`.
382
+ * The header carrying a console operator's token, sent by the playground and
383
+ * the thread inspector on every request to an agent and by the fire and
384
+ * answer calls Cortex makes to a trigger's route. A verified one makes the
385
+ * operator the caller unless the request also carries an end-user token to
386
+ * test as, and marks the threads it opens as test threads. A WebSocket
387
+ * upgrade carries it as `?operator=`.
359
388
  */
360
389
  export const OPERATOR_HEADER = "X-Cortex-Operator";
361
390
 
362
391
  /**
363
392
  * Beside the operator token, the id of a thread the operator inspects: the
364
393
  * request then reads as that thread's owner, read-only, with the debug view.
394
+ * The engine refuses any request carrying it but a read (GET, HEAD, OPTIONS),
395
+ * on every agent route.
365
396
  * A WebSocket upgrade carries it as `?inspect=`.
366
397
  */
367
398
  export const INSPECT_HEADER = "X-Cortex-Inspect";
399
+
400
+ /**
401
+ * Why a firing was dropped: the thread was busy with another run, a call on
402
+ * it waits for a decision (answered 409: decide that firing first), the
403
+ * daily budget was spent, or the agent's request bucket was.
404
+ */
405
+ export const TRIGGER_DROP_REASONS = ["busy", "approval_pending", "budget", "rate_limited"] as const;
406
+ export type TriggerDropReason = (typeof TRIGGER_DROP_REASONS)[number];
407
+
408
+ /**
409
+ * Every reason a firing records beside its outcome: why it was dropped, why
410
+ * its payload was rejected (`invalid_payload`, `missing_thread_key`), that
411
+ * no turn ran for it (`no_turn`), or why a run with an output schema failed
412
+ * without an answer the schema accepted (`no_output`, `invalid_output`).
413
+ */
414
+ export const TRIGGER_REASONS = [
415
+ ...TRIGGER_DROP_REASONS,
416
+ "invalid_payload",
417
+ "missing_thread_key",
418
+ "no_turn",
419
+ "no_output",
420
+ "invalid_output",
421
+ ] as const;
422
+ export type TriggerReason = (typeof TRIGGER_REASONS)[number];
423
+
424
+ /**
425
+ * Body of a `POST /chat` the thread did not take: 409 while a run holds the
426
+ * thread or a call on it waits for a decision, or when the answers it
427
+ * carried fit no open call, were already applied, or answer a call whose
428
+ * origin is out of reach (`no_turn`); 429 once the agent's request bucket is
429
+ * spent, with `Retry-After`.
430
+ */
431
+ export type ChatDropped = {
432
+ outcome: "dropped";
433
+ reason: Exclude<TriggerDropReason, "budget"> | "no_turn";
434
+ };
435
+
436
+ /**
437
+ * The drop a widget tells its user about by name: a call on the thread waits
438
+ * for approval, so the message goes back to the composer. Any other drop
439
+ * shows as a failed turn.
440
+ */
441
+ export type SendRefusal = Extract<ChatDropped["reason"], "approval_pending">;
442
+
443
+ /**
444
+ * What became of the client tool answers a `POST /chat` carried, by the
445
+ * status Cortex answered it with, apart from how the run it started goes.
446
+ * `persisted` (2xx): Cortex took them up, and sending them again changes
447
+ * nothing. `refused` (4xx, a `ChatDropped` among them): it did not take
448
+ * them up as given, and the stored transcript says what stands. `lost`
449
+ * (5xx, or no answer at all): its word never came, and only the stored
450
+ * transcript can tell.
451
+ */
452
+ export function answersReceiptOf(status: number) {
453
+ if (status >= 500) return "lost";
454
+ return status >= 400 ? "refused" : "persisted";
455
+ }
456
+ export type AnswersReceipt = ReturnType<typeof answersReceiptOf>;
457
+
458
+ /**
459
+ * One model call of the turn that wrote a message, raw, in the order the
460
+ * turn ran them: `GET /threads/:threadId/messages/:messageId/llm-requests`
461
+ * answers a list, for the request inspector.
462
+ */
463
+ export type LlmRequest = {
464
+ id: string;
465
+ prompt: string;
466
+ output: string | null;
467
+ tokenUsage: TokenUsage | null;
468
+ };