@m6d/cortex-cli 0.21.0 → 0.23.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.
@@ -8,6 +8,8 @@
8
8
  * alike.
9
9
  */
10
10
 
11
+ import type { RunReason, RunStatus } from "../runs/index";
12
+
11
13
  /** The UI locales every localized value is keyed by; the console's message catalogs follow it. */
12
14
  export const LOCALES = ["en", "ar"] as const;
13
15
  export type Locale = (typeof LOCALES)[number];
@@ -31,8 +33,8 @@ export function byLocale<T>(valueOf: (locale: Locale) => T) {
31
33
  export const TOOL_SURFACES = ["inline", "modal"] as const;
32
34
  export type ToolSurface = (typeof TOOL_SURFACES)[number];
33
35
 
34
- /** `trigger` rows hold a firing's rendered instructions; the model sees them as `user` turns. */
35
- export const MESSAGE_ROLES = ["system", "user", "assistant", "trigger"] as const;
36
+ /** `event` rows hold something that happened, a trigger's rendered instructions among them; the model sees them as `user` turns. */
37
+ export const MESSAGE_ROLES = ["system", "user", "assistant", "event"] as const;
36
38
  export type MessageRole = (typeof MESSAGE_ROLES)[number];
37
39
 
38
40
  /** The fenced blocks an agent may draw in its replies instead of text. */
@@ -46,6 +48,29 @@ export const DEFAULT_MEDIA_FORMS = [
46
48
  "map",
47
49
  ] as const satisfies readonly MediaForm[];
48
50
 
51
+ /**
52
+ * What an agent does with a message someone sends while a run holds its
53
+ * thread: refuses it, queues it as the next turn, or steers the running turn
54
+ * with it before its next step (one that arrives too late is queued).
55
+ */
56
+ export const WHILE_BUSY = ["refuse", "queue", "steer"] as const;
57
+ export type WhileBusy = (typeof WHILE_BUSY)[number];
58
+
59
+ /**
60
+ * Where one message sent while a run holds its thread goes, named by its
61
+ * sender under `forwardedProps[MESSAGE_BUSY_KEY]`, or set on a waiting one
62
+ * by `PATCH /threads/:threadId/inputs/:inputId` (`{ busy }`): `steer` joins
63
+ * the running turn, `queue` waits for a turn of its own.
64
+ */
65
+ export const MESSAGE_BUSY = ["queue", "steer"] as const;
66
+ export type MessageBusy = (typeof MESSAGE_BUSY)[number];
67
+ export const MESSAGE_BUSY_KEY = "busy";
68
+
69
+ /** What a message sent while a run holds the thread does: its sender's pick, never past an agent that refuses. */
70
+ export function busyFor(whileBusy: WhileBusy, picked: MessageBusy | undefined) {
71
+ return whileBusy === "refuse" ? whileBusy : (picked ?? whileBusy);
72
+ }
73
+
49
74
  export const ATTACHMENT_MIME_TYPES = [
50
75
  "image/png",
51
76
  "image/jpeg",
@@ -164,6 +189,47 @@ export type ClientToolBinding = {
164
189
  embedOrigin: string;
165
190
  };
166
191
 
192
+ /**
193
+ * How the widget connects an end user's account for a `connectAccount`
194
+ * call, stamped on the call: where the engine starts a sign-in, at the URL
195
+ * browsers reach it, the agent it is for, and each server the call may
196
+ * name, by name: its display name, and the scope it still needs when the
197
+ * account is connected but lacks access.
198
+ */
199
+ export type ConnectAccountBinding = {
200
+ startUrl: string;
201
+ agent: string;
202
+ servers: Record<string, { displayName: Partial<LocalizedText>; scope?: string }>;
203
+ };
204
+
205
+ /**
206
+ * Response of `GET /connections/:server?call=<toolCallId>`: where the
207
+ * caller's own account stands with the agent's MCP server of that name, for
208
+ * the card of that `connectAccount` call. `connected` with all a sign-in
209
+ * may ask for; `pending` while it is not connected, or lacks access a
210
+ * sign-in may still grant; `refused` once the card's own sign-in came back
211
+ * without it, or no sign-in may ask again.
212
+ */
213
+ export type AccountConnection = {
214
+ status: "connected" | "pending" | "refused";
215
+ };
216
+
217
+ /**
218
+ * The form `POST /oauth/start` takes, posted into the window a sign-in runs
219
+ * in with its bearer in the body, so no link can start one: the console's
220
+ * operator token and the server's id for the shared connection.
221
+ */
222
+ export type SharedSignInStart = { operatorToken: string; mcpServerId: string };
223
+
224
+ /** An end user's own start: their token, their agent, the server's name, and the `connectAccount` call it answers. */
225
+ export type EndUserSignInStart = {
226
+ token: string;
227
+ agent: string;
228
+ server: string;
229
+ threadId: string;
230
+ toolCallId: string;
231
+ };
232
+
167
233
  /**
168
234
  * What Cortex offers this agent's clients, fetched once at widget startup.
169
235
  * The widget hides UI for anything absent; Cortex enforces each.
@@ -180,6 +246,8 @@ export type AgentCapabilities = {
180
246
  messages: boolean;
181
247
  /** True when the caller only reads, as an operator inspecting a user's thread does. */
182
248
  readOnly: boolean;
249
+ /** What a message sent while a run holds the thread does; under `refuse`, nothing sends until the run ends. */
250
+ whileBusy: WhileBusy;
183
251
  /** Prompt chips on the blank-chat landing per locale; clicking one sends its `prompt` as the first message. */
184
252
  suggestions: Suggestions;
185
253
  /** Headline of the blank-chat landing per locale; "" falls back to the widget's built-in copy. */
@@ -199,8 +267,8 @@ export type MessageMetadata = {
199
267
  isAborted?: boolean;
200
268
  tokenUsage?: TokenUsage;
201
269
  attachments?: AttachmentSummary[];
202
- /** Set on `trigger` messages: the firing that wrote them. */
203
- trigger?: TriggerFiringSummary;
270
+ /** Set on `event` messages: what happened, and the input that brought it. */
271
+ event?: EventSummary;
204
272
  };
205
273
 
206
274
  /**
@@ -239,6 +307,22 @@ export const STATEMENT_TOOLS = ["executeCode", "queryDataset", "describeDataset"
239
307
  */
240
308
  export const APPROVAL_TITLE_FIELD = "approvalTitle";
241
309
 
310
+ /**
311
+ * The chat's own tool by which an agent hands a task to a copy of itself.
312
+ * Its input's `brief` is the task, which the copy's card shows.
313
+ */
314
+ export const DELEGATE_TASK_TOOL = "delegateTask";
315
+
316
+ /** The chat's tool by which an agent stops a copy it handed a task, by the subagent id its start answered. */
317
+ export const CANCEL_TASK_TOOL = "cancelTask";
318
+
319
+ /**
320
+ * The chat's tool by which an agent schedules a check-back: it comes back
321
+ * to the thread on its own at a time it picks, with a note to itself. Its
322
+ * output's `checkBackId` is the input the user may cancel.
323
+ */
324
+ export const SCHEDULE_WAKE_TOOL = "scheduleWake";
325
+
242
326
  /** A line the model wrote into a call input's `field`, trimmed; undefined while it streams in or is blank. */
243
327
  export function modelLineOf(input: unknown, field: string) {
244
328
  if (!input || typeof input !== "object") return undefined;
@@ -263,6 +347,25 @@ export type ToolCallMetadata = {
263
347
  answeredBy?: "client";
264
348
  /** Set beside `answeredBy` on a call of a client tool with an embedded page: the widget mounts the embed for it. */
265
349
  embed?: ClientToolBinding;
350
+ /** Set beside `answeredBy` on a `connectAccount` call: the widget starts the sign-in with it. */
351
+ connect?: ConnectAccountBinding;
352
+ /**
353
+ * Set on a call a run paused on for the user's decision: the interrupt the run raised for
354
+ * it and the run it paused, so a client that reloads re-prompts the decision exactly as the
355
+ * live run asked it.
356
+ */
357
+ paused?: { runId: string; interrupt: RunInterrupt };
358
+ };
359
+
360
+ /** An interrupt as a paused run raises it on its end, in AG-UI's shape. */
361
+ export type RunInterrupt = {
362
+ id: string;
363
+ reason: string;
364
+ message?: string;
365
+ toolCallId?: string;
366
+ responseSchema?: Record<string, unknown>;
367
+ expiresAt?: string;
368
+ metadata?: Record<string, unknown>;
266
369
  };
267
370
 
268
371
  /** The `metadata.cortex` a tool call carries, when Cortex stamped one. */
@@ -271,11 +374,16 @@ export function toolCallMetadataOf(part: { metadata?: unknown }) {
271
374
  return metadata?.cortex ?? undefined;
272
375
  }
273
376
 
274
- export type TriggerFiringSummary = {
377
+ /** An event that entered a conversation: a trigger fired, a schedule came due, a run finished. */
378
+ export type EventSummary = {
379
+ source: "trigger" | "schedule" | "run";
380
+ /** The trigger's name, or what the schedule or run was for. */
275
381
  name: string;
276
- firingId: string;
277
- firedAt: string;
382
+ inputId: string;
383
+ at: string;
278
384
  payload: unknown;
385
+ /** What a trigger that leaves a note tells the thread, for its user to read. */
386
+ note?: string;
279
387
  };
280
388
 
281
389
  /**
@@ -289,19 +397,19 @@ export type TriggerFiringSummary = {
289
397
  */
290
398
  export type CortexMessage<TPart = unknown> = {
291
399
  id: string;
292
- /** `trigger` rows carry a firing's instructions; the UIs render them as an event, not a bubble. */
400
+ /** `event` rows carry what happened; the UIs render them as an event, not a bubble. */
293
401
  role: MessageRole;
294
402
  parts: TPart[];
295
403
  metadata?: MessageMetadata;
296
404
  };
297
405
 
298
406
  /**
299
- * A stored message as the SDK's chat client takes it. The SDK has no `trigger`
300
- * role, so a firing's message travels as a user turn; its true role stays on
301
- * the envelope (`metadata.trigger`), which is what the UIs render the event from.
407
+ * A stored message as the SDK's chat client takes it. The SDK has no `event`
408
+ * role, so an event message travels as a user turn; its true role stays on
409
+ * the envelope (`metadata.event`), which is what the UIs render the event from.
302
410
  */
303
411
  export function toUiMessage<TPart>(message: CortexMessage<TPart>) {
304
- return { ...message, role: message.role === "trigger" ? ("user" as const) : message.role };
412
+ return { ...message, role: message.role === "event" ? ("user" as const) : message.role };
305
413
  }
306
414
 
307
415
  /**
@@ -384,14 +492,20 @@ export type ThreadRunStartedEvent = {
384
492
 
385
493
  /**
386
494
  * Run `runId` ended, committed or not, and left the thread free, so it takes
387
- * a new message now; whatever turn it committed is in the transcript. Sent
388
- * only for a run announced as started, and never by one whose thread another
389
- * run had already taken, so a finish naming a run other than the one a
390
- * client saw start last is stale.
495
+ * a new message now; whatever turn it committed is in the transcript.
496
+ * `status` and `reason` say how: `waiting` for a run parked on calls that
497
+ * wait. Sent only for a run announced as started, and never by one whose
498
+ * thread another run had already taken, so a finish naming a run other than
499
+ * the one a client saw start last is stale.
391
500
  */
392
501
  export type ThreadRunFinishedEvent = {
393
502
  type: "thread:run-finished";
394
- payload: { thread: ThreadSummary; runId: string };
503
+ payload: {
504
+ thread: ThreadSummary;
505
+ runId: string;
506
+ status: RunStatus;
507
+ reason: RunReason | null;
508
+ };
395
509
  };
396
510
 
397
511
  export type ThreadMessagesUpdatedEvent = {
@@ -399,13 +513,26 @@ export type ThreadMessagesUpdatedEvent = {
399
513
  payload: { thread: ThreadSummary };
400
514
  };
401
515
 
516
+ /**
517
+ * The inputs waiting on the thread changed: one was offered, taken up as a
518
+ * turn of its own, or cancelled. `inputIds` are the ones still waiting, in
519
+ * the order dispatch takes them; `GET /threads/:threadId/inputs` reads
520
+ * them. A steer the running turn took in waits on until that turn stores
521
+ * it, which announces nothing: the run's stream carries it.
522
+ */
523
+ export type ThreadInputsChangedEvent = {
524
+ type: "thread:inputs-changed";
525
+ payload: { threadId: string; inputIds: string[] };
526
+ };
527
+
402
528
  export type WsEvent =
403
529
  | ThreadCreatedEvent
404
530
  | ThreadDeletedEvent
405
531
  | ThreadTitleUpdatedEvent
406
532
  | ThreadRunStartedEvent
407
533
  | ThreadRunFinishedEvent
408
- | ThreadMessagesUpdatedEvent;
534
+ | ThreadMessagesUpdatedEvent
535
+ | ThreadInputsChangedEvent;
409
536
 
410
537
  /** The header the widget sends the browser's IANA time zone in; the turn reasons in that clock. */
411
538
  export const TIMEZONE_HEADER = "X-Cortex-Timezone";
@@ -430,53 +557,33 @@ export const OPERATOR_HEADER = "X-Cortex-Operator";
430
557
  export const INSPECT_HEADER = "X-Cortex-Inspect";
431
558
 
432
559
  /**
433
- * Why a firing was dropped: the thread was busy with another run, a call on
434
- * it waits for a decision (answered 409: decide that firing first), the
435
- * daily budget was spent, or the agent's request bucket was.
436
- */
437
- export const TRIGGER_DROP_REASONS = ["busy", "approval_pending", "budget", "rate_limited"] as const;
438
- export type TriggerDropReason = (typeof TRIGGER_DROP_REASONS)[number];
439
-
440
- /**
441
- * Every reason a firing records beside its outcome: why it was dropped, why
442
- * its payload was rejected (`invalid_payload`, `missing_thread_key`), that
443
- * no turn ran for it (`no_turn`), or why a run with an output schema failed
444
- * without an answer the schema accepted (`no_output`, `invalid_output`).
445
- */
446
- export const TRIGGER_REASONS = [
447
- ...TRIGGER_DROP_REASONS,
448
- "invalid_payload",
449
- "missing_thread_key",
450
- "no_turn",
451
- "no_output",
452
- "invalid_output",
453
- ] as const;
454
- export type TriggerReason = (typeof TRIGGER_REASONS)[number];
455
-
456
- /**
457
- * Body of a `POST /chat` the thread did not take: 409 while a run holds the
458
- * thread or a call on it waits for a decision, or when the answers it
459
- * carried fit no open call, were already applied, or answer a call whose
460
- * origin is out of reach (`no_turn`); 429 once the agent's request bucket is
461
- * spent, with `Retry-After`.
560
+ * One input waiting on a thread, in the order dispatch takes them, in
561
+ * `GET /threads/:threadId/inputs`: the user message as it will be stored
562
+ * once a run takes it up, and how it waits: to steer the running turn, or
563
+ * for a turn of its own. `DELETE /threads/:threadId/inputs/:inputId`
564
+ * cancels it and `PATCH` (`{ busy }`) moves it to steer or queue, unless a
565
+ * run already took it in (404).
462
566
  */
463
- export type ChatDropped = {
464
- outcome: "dropped";
465
- reason: Exclude<TriggerDropReason, "budget"> | "no_turn";
567
+ export type WaitingInput<TPart = unknown> = {
568
+ inputId: string;
569
+ delivery: "prompt" | "steer" | "followUp" | "answer" | "note";
570
+ message: CortexMessage<TPart>;
571
+ /** Whether it is text alone, which a running turn may take in as a steer; one with files waits for a turn of its own. */
572
+ textOnly: boolean;
466
573
  };
467
574
 
468
575
  /**
469
- * The drop a widget tells its user about by name: a call on the thread waits
470
- * for approval, so the message goes back to the composer. Any other drop
471
- * shows as a failed turn.
576
+ * The refusal a widget tells its user about by name: a call on the thread
577
+ * waits for approval, so the message goes back to the composer. Any other
578
+ * refusal shows as a failed turn.
472
579
  */
473
- export type SendRefusal = Extract<ChatDropped["reason"], "approval_pending">;
580
+ export type SendRefusal = "approval_pending";
474
581
 
475
582
  /**
476
583
  * What became of the client tool answers a `POST /chat` carried, by the
477
584
  * status Cortex answered it with, apart from how the run it started goes.
478
585
  * `persisted` (2xx): Cortex took them up, and sending them again changes
479
- * nothing. `refused` (4xx, a `ChatDropped` among them): it did not take
586
+ * nothing. `refused` (4xx, an `InputReceipt` among them): it did not take
480
587
  * them up as given, and the stored transcript says what stands. `lost`
481
588
  * (5xx, or no answer at all): its word never came, and only the stored
482
589
  * transcript can tell.
@@ -10,6 +10,8 @@ export type Flag = {
10
10
  description: string;
11
11
  /** A secret: never an argv flag (argv is world-readable), prompted masked or read from stdin. */
12
12
  sensitive: boolean;
13
+ /** Takes `null`, which a blank prompted answer sends. */
14
+ nullable: boolean;
13
15
  };
14
16
 
15
17
  /** Follows a local `$ref` into `components/schemas`; anything else is returned as is. */
@@ -36,6 +38,12 @@ function isSensitive(property: JsonSchema, document: OpenApiDocument): boolean {
36
38
  );
37
39
  }
38
40
 
41
+ /** Whether `null` is allowed, as a type list or as zod's `anyOf` alternative for a nullable property. */
42
+ function isNullable(property: JsonSchema) {
43
+ if (Array.isArray(property.type)) return property.type.includes("null");
44
+ return (property.anyOf ?? []).some((alternative) => alternative.type === "null");
45
+ }
46
+
39
47
  /** The flag kind a property's schema maps to; scalars and scalar arrays get typed flags, the rest is JSON. */
40
48
  function kindOf(property: JsonSchema, document: OpenApiDocument) {
41
49
  if (property.enum) return "enum" as const;
@@ -71,19 +79,21 @@ export function flagsFor(input: JsonSchema, document: OpenApiDocument) {
71
79
  choices: property.enum?.map(String),
72
80
  description: property.description ?? "",
73
81
  sensitive: isSensitive(property, document),
82
+ nullable: isNullable(property),
74
83
  } satisfies Flag;
75
84
  });
76
85
  }
77
86
 
78
87
  /**
79
88
  * A prompted answer, parsed and checked exactly as the flag's option would
80
- * be; `undefined` when it does not fit, so the caller can ask again.
89
+ * be; `undefined` when it does not fit, so the caller can ask again. A blank
90
+ * answer is `null` for a nullable flag.
81
91
  */
82
92
  export function parseAnswer(flag: Flag, answer: string) {
83
- // A string is taken verbatim: a secret's whitespace is part of it. Every
84
- // other kind is lexical and trimmed before parsing.
93
+ // A secret's whitespace is part of it; any other answer of only spaces is blank.
94
+ if ((flag.sensitive ? answer : answer.trim()) === "") return flag.nullable ? null : undefined;
95
+ // A string is taken verbatim. Every other kind is lexical and trimmed before parsing.
85
96
  const text = flag.kind === "string" ? answer : answer.trim();
86
- if (text === "") return undefined;
87
97
  return ANSWER_PARSERS[flag.kind](text, flag);
88
98
  }
89
99
 
@@ -28,7 +28,7 @@ import { applyPorts, type Ports } from "@/scaffold/ports";
28
28
  * `@m6d/cortex` range below are stamped by scripts/sync-package-versions.ts
29
29
  * before the CLI publishes, so a scaffold pins what was current then.
30
30
  */
31
- const CORTEX_VERSION = "0.20.0";
31
+ const CORTEX_VERSION = "0.22.0";
32
32
 
33
33
  /** This machine as Cortex's containers name it: the one private host the stack's egress policy admits. */
34
34
  export const DOCKER_HOST = "host.docker.internal";
@@ -147,7 +147,7 @@ function packageJson({ name, datasets, setup }: Project) {
147
147
  verify: "bun run format:check && bun run lint && bun run spellcheck && bun run check && bun run audit",
148
148
  },
149
149
  dependencies: {
150
- "@m6d/cortex": "~0.8.0",
150
+ "@m6d/cortex": "~0.10.0",
151
151
  // drizzle 1.0 is in beta; this is the build `@m6d/cortex/drizzle` is tested against.
152
152
  ...(datasets ? { "drizzle-orm": "1.0.0-beta.15-859cf75" } : {}),
153
153
  hono: "^4.12.3",
@@ -246,6 +246,9 @@ COMPOSE_PROJECT_NAME=
246
246
  DEFAULT_ADMIN_EMAIL=admin@example.com
247
247
  DEFAULT_ADMIN_PASSWORD=
248
248
  BOOTSTRAP_MANAGEMENT_KEY=
249
+ # The Cortex license key you were given. Without one Cortex serves no console
250
+ # and runs no agent.
251
+ CORTEX_LICENSE_KEY=
249
252
  # Session signing, the key the secrets store is encrypted with, and object
250
253
  # storage's password (storage is published on this machine for uploads).
251
254
  BETTER_AUTH_SECRET=
@@ -497,7 +500,9 @@ A backend for Cortex agents, and Cortex itself to run them on.
497
500
 
498
501
  ## Getting started
499
502
 
500
- On a fresh clone, run \`bun install\` first (\`cortex new\` already did): it writes \`.env\`.
503
+ On a fresh clone, run \`bun install\` first (\`cortex new\` already did): it writes \`.env\`. Put
504
+ your Cortex license key in its \`CORTEX_LICENSE_KEY\`: without one Cortex serves no console and runs no
505
+ agent.
501
506
 
502
507
  \`\`\`sh
503
508
  bun run dev
package/src/ui/prompts.ts CHANGED
@@ -84,40 +84,34 @@ export async function askToProceed(message: string) {
84
84
  }
85
85
 
86
86
  /**
87
- * A required value a command was not given: asked by name, re-asked while
88
- * empty or while `check` returns a complaint about the trimmed value.
87
+ * How an asked value is checked: re-asked while `check` complains about it,
88
+ * and while empty unless `blank` accepts an empty answer.
89
89
  */
90
- export function askValue(
91
- name: string,
92
- hint?: string,
93
- check: (value: string) => string | undefined = () => undefined,
94
- ) {
90
+ type AskOptions = { check?: (value: string) => string | undefined; blank?: boolean };
91
+
92
+ function validator(name: string, { check = () => undefined, blank = false }: AskOptions) {
93
+ return function (value: string | undefined) {
94
+ if (value) return check(value);
95
+ return blank ? undefined : `${name} is required.`;
96
+ };
97
+ }
98
+
99
+ /** A value a command was not given: asked by name and checked trimmed. */
100
+ export function askValue(name: string, { hint, ...options }: AskOptions & { hint?: string } = {}) {
101
+ const validate = validator(name, options);
95
102
  return orExit(
96
103
  text({
97
104
  message: name,
98
105
  placeholder: hint,
99
106
  output,
100
- validate: function (value) {
101
- const candidate = (value ?? "").trim();
102
- return candidate.length === 0 ? `${name} is required.` : check(candidate);
103
- },
107
+ validate: (value) => validate(value?.trim()),
104
108
  }),
105
109
  );
106
110
  }
107
111
 
108
- /** A credential: typed masked, never echoed, and re-asked while `check` complains. */
109
- export function askSecret(
110
- name: string,
111
- check: (value: string) => string | undefined = () => undefined,
112
- ) {
113
- return orExit(
114
- password({
115
- message: name,
116
- output,
117
- validate: (value) =>
118
- (value ?? "").length === 0 ? `${name} is required.` : check(value ?? ""),
119
- }),
120
- );
112
+ /** A credential: typed masked, never echoed, and checked as typed. */
113
+ export function askSecret(name: string, options: AskOptions = {}) {
114
+ return orExit(password({ message: name, output, validate: validator(name, options) }));
121
115
  }
122
116
 
123
117
  type Step = { title: string; run: () => string | undefined | Promise<string | undefined> };