@m6d/cortex-cli 0.14.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",
@@ -97,10 +114,8 @@ export const BUILT_IN_TOOL_NAMES = [
97
114
  "queryDataset",
98
115
  "readAttachment",
99
116
  "readReport",
100
- "searchKnowledge",
101
- "searchServices",
117
+ "searchRegistries",
102
118
  "searchThreadHistory",
103
- "searchTools",
104
119
  REQUEST_LOCATION_TOOL,
105
120
  TRIGGER_ANSWER_TOOL_NAME,
106
121
  ] as const;
@@ -109,30 +124,22 @@ export function isBuiltInToolName(name: string) {
109
124
  return BUILT_IN_TOOL_NAMES.some((builtIn) => builtIn === name);
110
125
  }
111
126
 
112
- /** The longest name a tool takes, a server's or one defined in Cortex. */
113
- export const TOOL_NAME_MAX_LENGTH = 64;
114
-
115
127
  /** A tool name, a server's or one defined in Cortex: an identifier no built-in tool holds. */
116
- const declaredToolNameSchema = z
117
- .string()
118
- .regex(IDENTIFIER_PATTERN)
119
- .max(TOOL_NAME_MAX_LENGTH)
120
- .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
+ );
121
132
 
122
133
  const jsonSchemaSchema = z.record(z.string(), z.unknown());
123
134
 
124
135
  /** A variable the server fills for its agents' threads; the built-in ones are Cortex's. */
125
136
  const serverVariableSchema = z.object({
126
- name: z
127
- .string()
128
- .regex(IDENTIFIER_PATTERN)
129
- .max(64)
130
- .refine(
131
- (name) => !BUILT_IN_VARIABLES.some((builtIn) => builtIn.name === name),
132
- "the name of a built-in variable",
133
- ),
134
- description: z.string().max(500),
135
- 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),
136
143
  type: variableDeclarationSchema.shape.type,
137
144
  });
138
145
  export type ServerVariable = z.infer<typeof serverVariableSchema>;
@@ -145,7 +152,7 @@ export type ServerVariable = z.infer<typeof serverVariableSchema>;
145
152
  */
146
153
  export const serverToolSchema = z.object({
147
154
  name: declaredToolNameSchema,
148
- description: z.string().trim().min(1).max(500),
155
+ description: z.string().trim().min(1).max(SERVER_MANIFEST_LIMITS.description),
149
156
  inputSchema: jsonSchemaSchema.refine(
150
157
  (schema) => schema.type === "object",
151
158
  "a tool's input is an object",
@@ -184,27 +191,9 @@ export const approvalRequestedEventSchema = z.object({
184
191
  });
185
192
  export type ApprovalRequestedEvent = z.infer<typeof approvalRequestedEventSchema>;
186
193
 
187
- /** A JSON Pointer token as a key: percent-decoded, then unescaped; undefined when its encoding is broken. */
188
- function pointerKey(token: string) {
189
- try {
190
- return unescapePointerToken(decodeURIComponent(token));
191
- } catch {
192
- return undefined;
193
- }
194
- }
195
-
196
194
  /** What a local `$ref` (`#/$defs/order`) points at in `root`, read as the input compiler reads it; undefined for any other. */
197
195
  function pointedAt(root: unknown, ref: string) {
198
- if (!ref.startsWith("#")) return undefined;
199
- let node = root;
200
- for (const token of ref.slice(1).split("/").slice(1)) {
201
- const key = pointerKey(token);
202
- node =
203
- key !== undefined && (isRecord(node) || Array.isArray(node))
204
- ? new Map<string, unknown>(Object.entries(node)).get(key)
205
- : undefined;
206
- }
207
- return node;
196
+ return ref.startsWith("#") ? readJsonPointer(root, ref) : undefined;
208
197
  }
209
198
 
210
199
  /** Whether one schema node names `APPROVAL_TITLE_FIELD` as a property or a required field. */
@@ -261,18 +250,15 @@ export function blocksApprovalTitle(inputSchema: unknown) {
261
250
  );
262
251
  }
263
252
 
264
- /** 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`. */
265
254
  export function refuseBlockedApprovalTitle(
266
255
  tool: { requiresApproval?: boolean; inputSchema: unknown },
267
256
  context: z.RefinementCtx,
268
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`,
269
259
  ) {
270
260
  if (!tool.requiresApproval || !blocksApprovalTitle(tool.inputSchema)) return;
271
- context.addIssue({
272
- code: "custom",
273
- path: [...path, "inputSchema"],
274
- 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`,
275
- });
261
+ context.addIssue({ code: "custom", path: [...path, "inputSchema"], message });
276
262
  }
277
263
 
278
264
  function uniqueNames(entries: ReadonlyArray<{ name: string }>) {
@@ -282,10 +268,13 @@ function uniqueNames(entries: ReadonlyArray<{ name: string }>) {
282
268
  /** `GET /`: everything the server offers, keyed by `version`, which changes whenever anything else does. */
283
269
  export const serverManifestSchema = z.object({
284
270
  version: z.string().min(1).max(128),
285
- 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"),
286
275
  tools: z
287
276
  .array(serverToolSchema)
288
- .max(500)
277
+ .max(SERVER_MANIFEST_LIMITS.tools)
289
278
  .refine(uniqueNames, "tool names repeat")
290
279
  .superRefine((tools, context) => {
291
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;
@@ -131,12 +149,23 @@ export type ClientToolBinding = {
131
149
  */
132
150
  export type AgentCapabilities = {
133
151
  attachments: boolean;
134
- /** False when the agent's `message` trigger is off: the widget hides its composer. */
152
+ /**
153
+ * False when this caller may not send to the chat: the agent's `message`
154
+ * trigger is off, or the caller only reads (`readOnly`). The widget hides
155
+ * its composer and answers no tool call.
156
+ */
135
157
  messages: boolean;
158
+ /** True when the caller only reads, as an operator inspecting a user's thread does. */
159
+ readOnly: boolean;
136
160
  /** Prompt chips on the blank-chat landing per locale; clicking one sends its `prompt` as the first message. */
137
161
  suggestions: Suggestions;
138
162
  /** Headline of the blank-chat landing per locale; "" falls back to the widget's built-in copy. */
139
163
  greeting: Greeting;
164
+ /**
165
+ * Whether this caller gets the debug view: tool inputs and outputs, reasoning,
166
+ * model calls. Cortex sends anyone else only what the widget's normal UI draws.
167
+ */
168
+ debug: boolean;
140
169
  };
141
170
 
142
171
  export type MessageMetadata = {
@@ -178,6 +207,18 @@ export const STATEMENT_TOOLS = ["executeCode", "queryDataset", "describeDataset"
178
207
  */
179
208
  export const APPROVAL_TITLE_FIELD = "approvalTitle";
180
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
+
217
+ /** The title the model wrote into an approval call's input, or undefined while it has none. */
218
+ export function approvalTitleOf(input: unknown) {
219
+ return modelLineOf(input, APPROVAL_TITLE_FIELD);
220
+ }
221
+
181
222
  /**
182
223
  * What Cortex stamps on a tool call's `metadata.cortex` from its first chunk,
183
224
  * so the live part and the stored one agree.
@@ -242,16 +283,27 @@ export function toUiMessage<TPart>(message: CortexMessage<TPart>) {
242
283
  export const CLIENT_TOOL_ANSWERS_KEY = "clientToolAnswers";
243
284
 
244
285
  /**
245
- * One answered call, which Cortex applies to the stored transcript: a client
246
- * tool's output (`complete` / `error`), or the user's decision on a server
247
- * 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.
248
295
  */
249
296
  export type ClientToolAnswer = {
250
297
  toolCallId: string;
251
298
  output: unknown;
252
- state: "complete" | "error" | "approved" | "denied";
299
+ state: (typeof CLIENT_TOOL_ANSWER_STATES)[number];
253
300
  };
254
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
+
255
307
  /**
256
308
  * The id a parked tool call's decision is filed under: the one on its
257
309
  * approval record, or `approval_<toolCallId>`, which is how the SDK names
@@ -261,6 +313,11 @@ export function approvalIdOf(part: { id: string; approval?: { id: string } }) {
261
313
  return part.approval?.id ?? `approval_${part.id}`;
262
314
  }
263
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
+
264
321
  /**
265
322
  * Response of `POST /chat/:threadId/tools/:toolCallId/initiate` for a pending
266
323
  * call stamped with an embed. The payload is for the widget only; it never
@@ -310,24 +367,102 @@ export type ThreadMessagesUpdatedEvent = {
310
367
  payload: { thread: ThreadSummary };
311
368
  };
312
369
 
313
- export type ToolProgressEvent = {
314
- type: "thread:tool-progress";
315
- payload: {
316
- threadId: string;
317
- toolCallId: string;
318
- toolName: string;
319
- status: "started" | "finished";
320
- };
321
- };
322
-
323
370
  export type WsEvent =
324
371
  | ThreadCreatedEvent
325
372
  | ThreadDeletedEvent
326
373
  | ThreadTitleUpdatedEvent
327
374
  | ThreadRunStartedEvent
328
375
  | ThreadRunFinishedEvent
329
- | ThreadMessagesUpdatedEvent
330
- | ToolProgressEvent;
376
+ | ThreadMessagesUpdatedEvent;
331
377
 
332
378
  /** The header the widget sends the browser's IANA time zone in; the turn reasons in that clock. */
333
379
  export const TIMEZONE_HEADER = "X-Cortex-Timezone";
380
+
381
+ /**
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=`.
388
+ */
389
+ export const OPERATOR_HEADER = "X-Cortex-Operator";
390
+
391
+ /**
392
+ * Beside the operator token, the id of a thread the operator inspects: the
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.
396
+ * A WebSocket upgrade carries it as `?inspect=`.
397
+ */
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
+ };