@m6d/cortex-cli 1.7.0 → 1.9.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m6d/cortex-cli",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "Scaffold and operate Cortex servers",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,47 +1,82 @@
1
1
  /*
2
2
  * The grammar and rendering pipeline for user-authored rich text that reaches
3
- * the LLM (agent prompts, service descriptions). Both sides of the wire share
4
- * it: the console edits and validates these strings, the console's runtime API
5
- * pre-renders them (mentions + heading demotion), and `@m6d/cortex-server`
6
- * applies per-request variable values. Two token grammars live here:
3
+ * the LLM (agent prompts, service descriptions, skill bodies). Both sides of
4
+ * the wire share it: the console edits and validates these strings, the
5
+ * console's runtime API pre-renders them (mentions + heading demotion), and
6
+ * `@m6d/cortex-server` applies per-request variable values. Three token
7
+ * grammars live here:
7
8
  *
8
- * - Tool mentions `@[name](uuid)` — the uuid is authoritative; the inline name
9
- * is only a display cache refreshed on save ({@link normalizeMentions}) and
10
- * resolved live when text is rendered for the LLM ({@link renderMentions}).
9
+ * - Tool mentions `@[name](uuid)` and skill mentions `/[name](uuid)` — the
10
+ * uuid is authoritative; the inline name is only a display cache refreshed
11
+ * on save ({@link normalizeMentions}) and resolved live when text is
12
+ * rendered for the LLM ({@link renderMentions}).
11
13
  * - Variables `{{name}}` — validated against a known list at authoring time
12
14
  * ({@link analyzeVariables}) and substituted per request on the server
13
15
  * ({@link substituteVariables}).
14
16
  */
15
17
 
16
- /** One mention token, unanchored and flagless — the editor's tokenizer anchors it, the helpers below add `g`. */
17
- export const MENTION_PATTERN_SOURCE = String.raw`@\[([^\]\n]*)\]\(([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\)`;
18
+ const UUID = String.raw`[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`;
19
+
20
+ /** One tool mention token, unanchored and flagless — the editor's tokenizer anchors it, the helpers below add `g`. */
21
+ export const MENTION_PATTERN_SOURCE = String.raw`@\[([^\]\n]*)\]\((${UUID})\)`;
22
+ /** One skill mention token; `/` is the slash-command convention skills already carry elsewhere. */
23
+ export const SKILL_MENTION_PATTERN_SOURCE = String.raw`/\[([^\]\n]*)\]\((${UUID})\)`;
18
24
 
19
25
  const MENTION_PATTERN = new RegExp(MENTION_PATTERN_SOURCE, "g");
26
+ const SKILL_MENTION_PATTERN = new RegExp(SKILL_MENTION_PATTERN_SOURCE, "g");
20
27
 
21
28
  const VARIABLE_PATTERN = /{{\s*([^{}]*?)\s*}}/g;
22
29
 
23
- export function extractMentionedToolIds(text: string) {
30
+ /** Entity names by id for the two mention grammars; a missing map leaves that grammar's cached names in place. */
31
+ export type MentionNames = {
32
+ tools?: ReadonlyMap<string, string>;
33
+ skills?: ReadonlyMap<string, string>;
34
+ };
35
+
36
+ const NO_NAMES: ReadonlyMap<string, string> = new Map();
37
+
38
+ function extractIds(text: string, pattern: RegExp) {
24
39
  const ids: string[] = [];
25
- for (const match of text.matchAll(MENTION_PATTERN)) {
40
+ for (const match of text.matchAll(pattern)) {
26
41
  const id = match[2];
27
42
  if (id && !ids.includes(id)) ids.push(id);
28
43
  }
29
44
  return ids;
30
45
  }
31
46
 
32
- /** Replaces each mention token with the tool's current name (falling back to the cached inline name). */
33
- export function renderMentions(text: string, toolNameById: ReadonlyMap<string, string>) {
34
- return text.replace(MENTION_PATTERN, (_, cachedName: string, toolId: string) => {
35
- return toolNameById.get(toolId) ?? cachedName;
36
- });
47
+ export function extractMentionedToolIds(text: string) {
48
+ return extractIds(text, MENTION_PATTERN);
49
+ }
50
+
51
+ export function extractMentionedSkillIds(text: string) {
52
+ return extractIds(text, SKILL_MENTION_PATTERN);
53
+ }
54
+
55
+ /** Replaces each mention token with the entity's current name (falling back to the cached inline name). */
56
+ export function renderMentions(text: string, names: MentionNames) {
57
+ const tools = names.tools ?? NO_NAMES;
58
+ const skills = names.skills ?? NO_NAMES;
59
+ return text
60
+ .replace(MENTION_PATTERN, (_, cached: string, id: string) => tools.get(id) ?? cached)
61
+ .replace(
62
+ SKILL_MENTION_PATTERN,
63
+ (_, cached: string, id: string) => skills.get(id) ?? cached,
64
+ );
37
65
  }
38
66
 
39
67
  /** Refreshes stale display names inside stored text, keeping the tokens intact. */
40
- export function normalizeMentions(text: string, toolNameById: ReadonlyMap<string, string>) {
41
- return text.replace(MENTION_PATTERN, (token, _cachedName: string, toolId: string) => {
42
- const name = toolNameById.get(toolId);
43
- return name === undefined ? token : `@[${name}](${toolId})`;
44
- });
68
+ export function normalizeMentions(text: string, names: MentionNames) {
69
+ const tools = names.tools ?? NO_NAMES;
70
+ const skills = names.skills ?? NO_NAMES;
71
+ return text
72
+ .replace(MENTION_PATTERN, (token, _cached: string, id: string) => {
73
+ const name = tools.get(id);
74
+ return name === undefined ? token : `@[${name}](${id})`;
75
+ })
76
+ .replace(SKILL_MENTION_PATTERN, (token, _cached: string, id: string) => {
77
+ const name = skills.get(id);
78
+ return name === undefined ? token : `/[${name}](${id})`;
79
+ });
45
80
  }
46
81
 
47
82
  /** Splits the `{{variables}}` referenced in a text into known and unknown ones. */
@@ -65,7 +100,7 @@ export function analyzeVariables<Variable extends string>(
65
100
 
66
101
  /** Fills `{{variables}}` with per-request values; unknown names stay verbatim. */
67
102
  export function substituteVariables(text: string, values: Record<string, string>) {
68
- return text.replace(/\{\{(\w+)\}\}/g, (match, name: string) => values[name] ?? match);
103
+ return text.replace(VARIABLE_PATTERN, (match, name: string) => values[name] ?? match);
69
104
  }
70
105
 
71
106
  /**
@@ -164,23 +199,21 @@ export function demoteHeadings(text: string) {
164
199
  }
165
200
 
166
201
  /** The one render step for LLM-bound rich text: resolve mentions, then demote headings. */
167
- export function renderRichText(text: string, toolNameById: ReadonlyMap<string, string>) {
168
- return demoteHeadings(renderMentions(text, toolNameById));
202
+ export function renderRichText(text: string, names: MentionNames) {
203
+ return demoteHeadings(renderMentions(text, names));
169
204
  }
170
205
 
171
206
  /**
172
- * Reduces rich text to plain prose for embeddings: mentions become tool names
173
- * (cached inline names when no map is given) and markdown syntax is dropped.
207
+ * Reduces rich text to plain prose for embeddings: mentions become entity
208
+ * names (cached inline names when no map is given) and markdown syntax is
209
+ * dropped.
174
210
  * ponytail: a regex stripper, not a markdown parser — nested emphasis or exotic
175
211
  * constructs may leave residue; swap in a real parser if retrieval quality
176
212
  * ever shows it.
177
213
  */
178
- export function stripToPlaintext(
179
- text: string,
180
- toolNameById: ReadonlyMap<string, string> = new Map(),
181
- ) {
214
+ export function stripToPlaintext(text: string, names: MentionNames = {}) {
182
215
  const lineState = fenceTracker();
183
- return renderMentions(text, toolNameById)
216
+ return renderMentions(text, names)
184
217
  .split("\n")
185
218
  .filter((line) => {
186
219
  const state = lineState(line);
@@ -13,42 +13,22 @@ export const AGENT_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
13
13
  /** Tool names become `tools.<name>()` sandbox bindings on the server. */
14
14
  export const TOOL_NAME_PATTERN = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
15
15
 
16
- export const PROMPT_VARIABLES = ["userName", "channel", "locale", "utcTime", "timezone"] as const;
17
-
18
16
  /**
19
- * One entry per prompt variable, driving editor autocomplete, validation, and
20
- * runtime substitution alike. Adding a variable = extending the tuple above
21
- * plus one entry here (the compiler enforces the pair); the consumer embedding
22
- * the agent supplies the value in its session/request context under the same
23
- * key. Descriptions are plain English on purpose: variables are code-like
24
- * identifiers shown in an LTR suggestion list, not localized UI copy.
17
+ * A variable the server fills per request: a session value, a request-context
18
+ * value, or one the SDK computes. Declared per agent so the console offers
19
+ * exactly what that agent's server provides, in prompts and as tool input
20
+ * sources alike.
25
21
  */
26
- export const PROMPT_VARIABLE_DEFINITIONS = {
27
- userName: {
28
- description: "Display name of the signed-in user",
29
- example: "Sara",
30
- },
31
- channel: {
32
- description: "Channel the conversation arrived on",
33
- example: "web",
34
- },
35
- locale: {
36
- description: "Resolved locale of the session",
37
- example: "ar",
38
- },
39
- // The server computes utcTime per request (a consumer-supplied value
40
- // still wins); timezone has no server default — only the consumer knows
41
- // the session's zone, so it must arrive via session/request context or
42
- // the placeholder stays literal.
43
- utcTime: {
44
- description: "Current time in UTC (ISO 8601)",
45
- example: "2026-08-16T09:30:00Z",
46
- },
47
- timezone: {
48
- description: "IANA time zone of the session",
49
- example: "Asia/Dubai",
50
- },
51
- } satisfies Record<(typeof PROMPT_VARIABLES)[number], { description: string; example: string }>;
22
+ export const variableDeclarationSchema = z.object({
23
+ name: z
24
+ .string()
25
+ .regex(/^[A-Za-z_$][A-Za-z0-9_$]*$/)
26
+ .max(64),
27
+ description: z.string().max(500),
28
+ example: z.string().max(200),
29
+ /** Computed by the SDK per request (`utcTime`, `locale`); no session value can set it. */
30
+ computed: z.boolean().default(false),
31
+ });
52
32
 
53
33
  export const RUNTIME_ERROR_KINDS = [
54
34
  "timeout",
@@ -101,6 +81,13 @@ export const toolEmbedSchema = z.object({
101
81
  embedOrigin: z.url(),
102
82
  });
103
83
 
84
+ /**
85
+ * Variables a tool's input reads (`x-source` of kind `variable` on an input
86
+ * property). The server fills those fields from the agent's declared
87
+ * variables at execute time and the model never sees them.
88
+ */
89
+ const boundVariablesSchema = z.array(variableDeclarationSchema.shape.name);
90
+
104
91
  export const toolSignatureSchema = z.object({
105
92
  toolId: z.uuid(),
106
93
  name: z.string().regex(TOOL_NAME_PATTERN),
@@ -110,6 +97,7 @@ export const toolSignatureSchema = z.object({
110
97
  signature: z.string(),
111
98
  score: z.number().nullable(),
112
99
  pinned: z.boolean(),
100
+ variables: boundVariablesSchema,
113
101
  embed: toolEmbedSchema.optional(),
114
102
  /** Client tools only: the published input JSON Schema, verbatim.
115
103
  * Client-executed declarations need a real schema — the rendered
@@ -117,19 +105,228 @@ export const toolSignatureSchema = z.object({
117
105
  inputSchema: z.record(z.string(), z.unknown()).optional(),
118
106
  });
119
107
 
108
+ /** Consumer-facing slugs: service keys and skill names share it, the model types both. */
109
+ export const SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
110
+
111
+ /**
112
+ * One always-listed skill line. The description is the whole trigger: the
113
+ * model reads it every turn and calls `loadSkill` when it matches the task.
114
+ */
115
+ export const skillLineSchema = z.object({
116
+ name: z.string().regex(SLUG_PATTERN).max(64),
117
+ description: z.string().max(500),
118
+ });
119
+
120
120
  export const serviceCardSchema = z.object({
121
121
  serviceId: z.uuid(),
122
- key: z
123
- .string()
124
- .regex(/^[a-z0-9]+(-[a-z0-9]+)*$/)
125
- .max(64),
122
+ key: z.string().regex(SLUG_PATTERN).max(64),
126
123
  name: z.string(),
127
124
  card: z.string(),
128
125
  score: z.number(),
126
+ /** Skills scoped to this service; listed only while the card is in context. Defaulted so an older Control Center still parses. */
127
+ skills: z.array(skillLineSchema).default([]),
128
+ });
129
+
130
+ /**
131
+ * A skill body served on `loadSkill`: mentions rendered to current names and
132
+ * headings demoted by the console, `{{variables}}` left for the server to
133
+ * fill per request. `mentionedTools` are the sandbox bindings the body
134
+ * refers to, so the turn can register them before the model acts on it.
135
+ */
136
+ /**
137
+ * Opaque handle the Control Center assigns a resolved agent. Servers echo it
138
+ * on reports about that agent so they attach to it even if the slug or alias
139
+ * they used later moves.
140
+ */
141
+ export const agentRefSchema = z.string().max(128);
142
+
143
+ /**
144
+ * Response header carrying the resolved agent's ref on every agent-scoped
145
+ * runtime answer, errors included, so a server can attribute a request even
146
+ * when the body is an error envelope. Absent only when no agent resolved.
147
+ */
148
+ export const AGENT_REF_HEADER = "X-CC-Agent-Ref";
149
+
150
+ /**
151
+ * The playground operator's stand-in for a test thread's session: a
152
+ * URI-encoded JSON object of declared variable values, sent by cc's
153
+ * playground beside `X-Cortex-Playground` and honoured only when that header
154
+ * verifies. The server stores it as the thread's session on creation, so the
155
+ * customer's `loadSessionData` (which never sees cc-minted tokens) is not
156
+ * needed for prompts and tool sources to have values.
157
+ */
158
+ export const PLAYGROUND_SESSION_HEADER = "X-Cortex-Playground-Session";
159
+
160
+ /** A sandbox binding a piece of rich text mentions, so the turn can register it before the model reads the text. */
161
+ export const mentionedToolSchema = z.object({
162
+ toolId: z.uuid(),
163
+ name: z.string().regex(TOOL_NAME_PATTERN),
164
+ readOnly: z.boolean(),
165
+ variables: boundVariablesSchema,
129
166
  });
130
167
 
168
+ export const skillBodyResponseSchema = z.object({
169
+ name: z.string(),
170
+ body: z.string(),
171
+ mentionedTools: z.array(mentionedToolSchema),
172
+ catalogVersion: catalogVersionSchema,
173
+ });
174
+
175
+ /*
176
+ * Triggers: named entry points on an agent. A firing is one invocation with a
177
+ * payload and starts at most one run. The end user's message is the built-in
178
+ * `message` trigger; every other trigger is fired by an external caller over
179
+ * `POST /agents/:agentId/triggers/:name` with the trigger's own secret.
180
+ */
181
+
182
+ export const MESSAGE_TRIGGER_NAME = "message";
183
+
184
+ /**
185
+ * The first characters of a fire bearer say what it is: the secret callers
186
+ * hold, or a one-shot test token the console minted. A test token is a JWT
187
+ * the Control Center signed, so a server verifies it against the Control
188
+ * Center's JWKS before saying a word to it, and the Control Center then
189
+ * consumes it so it fires once.
190
+ */
191
+ export const TRIGGER_SECRET_PREFIXES = { live: "tr_live_", test: "tr_test_" } as const;
192
+ export const TRIGGER_TEST_TOKEN_AUDIENCE = "cortex:trigger-test";
193
+
194
+ /** The claims a test token carries beside the standard ones; the server checks them against the route it arrived on. */
195
+ export const triggerTestTokenClaimsSchema = z.object({
196
+ agent: agentSlugSchema,
197
+ trigger: z.string().regex(SLUG_PATTERN).max(64),
198
+ });
199
+
200
+ const sha256HexSchema = z.string().regex(/^[0-9a-f]{64}$/);
201
+
202
+ export const TRIGGER_OUTCOMES = [
203
+ "started",
204
+ "completed",
205
+ "failed",
206
+ "rejected",
207
+ "dropped",
208
+ "disabled",
209
+ ] as const;
210
+
211
+ /** Why a firing was dropped: the thread was busy, or the daily budget was spent. */
212
+ export const TRIGGER_DROP_REASONS = ["busy", "budget"] as const;
213
+
214
+ export const TRIGGER_SOURCES = ["code", "cc"] as const;
215
+
216
+ /**
217
+ * Which thread a firing lands in. `single`: one thread for the trigger.
218
+ * `per-firing`: a fresh thread every time. `keyed`: one thread per distinct
219
+ * value at a JSON pointer into the payload (e.g. `/pull_request/number`).
220
+ */
221
+ export const triggerThreadPolicySchema = z.discriminatedUnion("policy", [
222
+ z.object({ policy: z.literal("single") }),
223
+ z.object({ policy: z.literal("per-firing") }),
224
+ z.object({ policy: z.literal("keyed"), key: z.string().regex(/^\//).max(256) }),
225
+ ]);
226
+
227
+ /** A trigger as both sides describe it. Secrets and credentials never cross this contract. */
228
+ export const triggerDeclarationSchema = z.object({
229
+ name: z.string().regex(SLUG_PATTERN).max(64),
230
+ description: z.string().max(500),
231
+ /** JSON Schema for the payload; cc triggers are validated from it on the server. */
232
+ input: z.record(z.string(), z.unknown()),
233
+ /**
234
+ * The prompt the run starts from, mentions already rendered to names by
235
+ * the console; `{{variables}}` and `{{/json/pointer}}` payload fields
236
+ * are the server's to fill per firing.
237
+ */
238
+ instructions: z.string().max(20_000),
239
+ /** Tools the instructions mention, registered as sandbox bindings for the run. Defaulted so an older Control Center still parses. */
240
+ mentionedTools: z.array(mentionedToolSchema).default([]),
241
+ thread: triggerThreadPolicySchema.default({ policy: "single" }),
242
+ budgetPerDay: z.number().int().positive().nullable().default(null),
243
+ /** Consecutive failed or rejected firings before the trigger disables itself. */
244
+ disableAfterFailures: z.number().int().positive().default(5),
245
+ hasCredential: z.boolean().default(false),
246
+ /**
247
+ * sha256 of the fire secret, so a server checks a presented bearer in
248
+ * process; the Control Center hears only about console test secrets,
249
+ * which it alone can consume.
250
+ */
251
+ secretHash: sha256HexSchema,
252
+ });
253
+
254
+ /**
255
+ * What a server runs for one agent, sent whole at boot in `agent.declare`. An
256
+ * agent exists in the Control Center only through a declaration, and only
257
+ * its declaring server may serve it. Everything the console validates
258
+ * against (variables, code skills, code triggers) is here rather than
259
+ * assumed.
260
+ */
261
+ export const agentDeclarationSchema = z.object({
262
+ slug: agentSlugSchema,
263
+ variables: z.array(variableDeclarationSchema),
264
+ skills: z.array(skillLineSchema),
265
+ triggers: z.array(
266
+ triggerDeclarationSchema.pick({ name: true, description: true, input: true, thread: true }),
267
+ ),
268
+ });
269
+
270
+ export const triggerFiringResponseSchema = z.discriminatedUnion("outcome", [
271
+ z.object({
272
+ firingId: z.uuid(),
273
+ outcome: z.literal("started"),
274
+ threadId: z.uuid(),
275
+ runId: z.string(),
276
+ }),
277
+ z.object({
278
+ firingId: z.uuid(),
279
+ outcome: z.literal("dropped"),
280
+ reason: z.enum(TRIGGER_DROP_REASONS),
281
+ threadId: z.uuid().nullable(),
282
+ }),
283
+ z.object({ firingId: z.uuid(), outcome: z.literal("disabled") }),
284
+ z.object({
285
+ firingId: z.uuid(),
286
+ outcome: z.literal("rejected"),
287
+ issues: z.array(z.object({ path: z.string(), message: z.string() })),
288
+ }),
289
+ ]);
290
+
291
+ export const triggerFiringSchema = z.object({
292
+ id: z.uuid(),
293
+ triggerName: z.string(),
294
+ threadId: z.uuid().nullable(),
295
+ runId: z.string().nullable(),
296
+ outcome: z.enum(TRIGGER_OUTCOMES),
297
+ reason: z.string().nullable(),
298
+ createdAt: z.iso.datetime(),
299
+ finishedAt: z.iso.datetime().nullable(),
300
+ });
301
+
302
+ export const triggerFiringListResponseSchema = z.object({
303
+ firings: z.array(triggerFiringSchema),
304
+ });
305
+
306
+ /**
307
+ * `POST /api/runtime/agents/:id/triggers/:name/auth`: a server holding a
308
+ * bearer for a cc trigger asks cc whether it is the trigger's secret. Only the
309
+ * hash travels; a positive answer carries the trigger's credential, if any.
310
+ */
311
+ export const triggerAuthRequestSchema = z.object({
312
+ secretHash: sha256HexSchema,
313
+ });
314
+
315
+ export const triggerAuthResponseSchema = z.discriminatedUnion("ok", [
316
+ z.object({
317
+ ok: z.literal(true),
318
+ credential: z.string().nullable(),
319
+ /** The bearer was a single-use test secret, now spent: the server must not cache this verdict. */
320
+ oneShot: z.boolean().optional(),
321
+ }),
322
+ z.object({ ok: z.literal(false) }),
323
+ ]);
324
+
131
325
  export const runtimeAgentConfigSchema = z.object({
326
+ /** The slug the request used: the agent's own or one of the server's aliases. */
132
327
  agentId: agentSlugSchema,
328
+ /** The agent's own slug, the key of its `agents` entry in server code. Equals `agentId` unless an alias was used. */
329
+ slug: agentSlugSchema,
133
330
  /**
134
331
  * End-user JWT verification for this agent, managed by the agent's owner.
135
332
  * Tokens must be asymmetrically signed (the JWKS URI serves the public
@@ -147,7 +344,8 @@ export const runtimeAgentConfigSchema = z.object({
147
344
  })
148
345
  .optional(),
149
346
  systemPrompt: z.string(),
150
- promptVariables: z.array(z.enum(PROMPT_VARIABLES)),
347
+ /** Every declared variable the published prompt, skills, or triggers reference. */
348
+ promptVariables: z.array(variableDeclarationSchema.shape.name),
151
349
  defaultLocale: localeSchema,
152
350
  /**
153
351
  * Whether end users may upload files to this agent. Operational config
@@ -166,6 +364,14 @@ export const runtimeAgentConfigSchema = z.object({
166
364
  executeTimeoutMs: z.number().int().positive(),
167
365
  maxResponseBytes: z.number().int().positive(),
168
366
  }),
367
+ /** Unscoped published skills; service-scoped ones travel on their card. Defaulted so an older Control Center still parses. */
368
+ skills: z.array(skillLineSchema).default([]),
369
+ /** Published triggers. Defaulted so an older Control Center still parses. */
370
+ triggers: z.array(triggerDeclarationSchema).default([]),
371
+ /** Whether end users may message this agent; false makes it headless. */
372
+ messageTrigger: z.boolean().default(true),
373
+ /** Absent from older Control Centers; reports then carry only the slug. */
374
+ agentRef: agentRefSchema.optional(),
169
375
  catalogVersion: catalogVersionSchema,
170
376
  publishedAt: z.iso.datetime(),
171
377
  });
@@ -238,6 +444,8 @@ export const executeRequestSchema = z.object({
238
444
  threadId: z.string().max(128).optional(),
239
445
  userId: z.string().max(128).optional(),
240
446
  locale: localeSchema.optional(),
447
+ /** Values of the variables the tool's input reads; only those names travel. */
448
+ variables: z.record(z.string(), z.unknown()).optional(),
241
449
  })
242
450
  .optional(),
243
451
  });
@@ -299,7 +507,8 @@ export const tokenUsageSchema = z.object({
299
507
  total: tokenCountSchema,
300
508
  });
301
509
 
302
- export const MESSAGE_ROLES = ["system", "user", "assistant", "tool"] as const;
510
+ /** `trigger` rows hold a firing's rendered instructions; the model sees them as `user` turns. */
511
+ export const MESSAGE_ROLES = ["system", "user", "assistant", "tool", "trigger"] as const;
303
512
 
304
513
  export const syncEventSchema = z.discriminatedUnion("type", [
305
514
  z.object({
@@ -309,6 +518,9 @@ export const syncEventSchema = z.discriminatedUnion("type", [
309
518
  userId: z.string().max(255),
310
519
  title: z.string().max(256).nullable(),
311
520
  isTest: z.boolean(),
521
+ /** Set on headless threads a trigger owns; `userId` is then a sentinel. */
522
+ triggerName: z.string().max(64).nullable().default(null),
523
+ triggerKey: z.string().max(256).nullable().default(null),
312
524
  createdAt: z.iso.datetime(),
313
525
  updatedAt: z.iso.datetime(),
314
526
  }),
@@ -337,6 +549,48 @@ export const syncEventSchema = z.discriminatedUnion("type", [
337
549
  // column, but the mirror needs one for time-bucketed usage stats.
338
550
  createdAt: z.iso.datetime(),
339
551
  }),
552
+ // One loadSkill call, hit or miss, from either skill source. Bounded by
553
+ // construction (no body travels), so it can never trip the size cap.
554
+ z.object({
555
+ type: z.literal("skill.load"),
556
+ id: z.uuid(),
557
+ threadId: z.uuid(),
558
+ agentSlug: agentSlugSchema,
559
+ /** The ref cc issued for the agent that answered; a code-only agent or a miss has none. */
560
+ agentRef: agentRefSchema.optional(),
561
+ /** As the model asked for it, even when nothing answered. */
562
+ skillName: z.string().max(64),
563
+ source: z.enum(["code", "cc"]).nullable(),
564
+ hit: z.boolean(),
565
+ /** The user message this turn answers; the mirror joins it for the drill-down. */
566
+ messageId: z.string().max(128),
567
+ createdAt: z.iso.datetime(),
568
+ }),
569
+ // One firing, upserted on start and again on its terminal outcome; the
570
+ // firing id is the event id so both land on one mirror row.
571
+ z.object({
572
+ type: z.literal("trigger.firing"),
573
+ id: z.uuid(),
574
+ agentSlug: agentSlugSchema,
575
+ agentRef: agentRefSchema.optional(),
576
+ triggerName: z.string().max(64),
577
+ source: z.enum(TRIGGER_SOURCES),
578
+ threadId: z.uuid().nullable(),
579
+ runId: z.string().max(64).nullable(),
580
+ outcome: z.enum(TRIGGER_OUTCOMES),
581
+ reason: z.string().max(64).nullable(),
582
+ createdAt: z.iso.datetime(),
583
+ finishedAt: z.iso.datetime().nullable(),
584
+ }),
585
+ // Everything the server runs, sent whole at boot. Replaces the server's
586
+ // previous declaration, so an agent removed from code leaves cc with its
587
+ // skills and triggers.
588
+ z.object({
589
+ type: z.literal("agent.declare"),
590
+ id: z.uuid(),
591
+ agents: z.array(agentDeclarationSchema),
592
+ createdAt: z.iso.datetime(),
593
+ }),
340
594
  ]);
341
595
 
342
596
  /*
@@ -372,6 +626,20 @@ export type ToolEmbed = z.infer<typeof toolEmbedSchema>;
372
626
  export type RuntimeAgentConfig = z.infer<typeof runtimeAgentConfigSchema>;
373
627
  export type ResolveRequest = z.input<typeof resolveRequestSchema>;
374
628
  export type ResolveResponse = z.infer<typeof resolveResponseSchema>;
629
+ export type SkillLine = z.infer<typeof skillLineSchema>;
630
+ export type SkillBodyResponse = z.infer<typeof skillBodyResponseSchema>;
631
+ export type TriggerDeclaration = z.infer<typeof triggerDeclarationSchema>;
632
+ export type AgentDeclaration = z.infer<typeof agentDeclarationSchema>;
633
+ export type VariableDeclaration = z.infer<typeof variableDeclarationSchema>;
634
+ export type TriggerThreadPolicy = z.infer<typeof triggerThreadPolicySchema>;
635
+ export type TriggerOutcome = (typeof TRIGGER_OUTCOMES)[number];
636
+ export type TriggerDropReason = (typeof TRIGGER_DROP_REASONS)[number];
637
+ export type TriggerSource = (typeof TRIGGER_SOURCES)[number];
638
+ export type TriggerFiringResponse = z.infer<typeof triggerFiringResponseSchema>;
639
+ export type TriggerFiring = z.infer<typeof triggerFiringSchema>;
640
+ export type TriggerFiringListResponse = z.infer<typeof triggerFiringListResponseSchema>;
641
+ export type TriggerAuthRequest = z.infer<typeof triggerAuthRequestSchema>;
642
+ export type TriggerAuthResponse = z.infer<typeof triggerAuthResponseSchema>;
375
643
  export type SearchRequest = z.input<typeof searchRequestSchema>;
376
644
  export type ExecuteRequest = z.input<typeof executeRequestSchema>;
377
645
  export type SyncEvent = z.infer<typeof syncEventSchema>;
@@ -21,6 +21,9 @@ export const MAX_ATTACHMENTS_PER_MESSAGE = 5;
21
21
 
22
22
  export const ATTACHMENT_STATUSES = ["pending", "described", "failed", "skipped"] as const;
23
23
 
24
+ /** Who put the file in the thread: the user through the composer, or a tool mid-turn. */
25
+ export const ATTACHMENT_ORIGINS = ["upload", "tool"] as const;
26
+
24
27
  export function checkAttachmentPolicy(contentType: string, sizeBytes: number) {
25
28
  if (!ATTACHMENT_MIME_TYPES.some((type) => type === contentType)) return "type";
26
29
  if (sizeBytes > ATTACHMENT_MAX_BYTES) return "size";
@@ -29,12 +32,15 @@ export function checkAttachmentPolicy(contentType: string, sizeBytes: number) {
29
32
 
30
33
  export type AttachmentStatus = (typeof ATTACHMENT_STATUSES)[number];
31
34
 
35
+ export type AttachmentOrigin = (typeof ATTACHMENT_ORIGINS)[number];
36
+
32
37
  export type AttachmentSummary = {
33
38
  id: string;
34
39
  filename: string;
35
40
  contentType: string;
36
41
  sizeBytes: number;
37
42
  status: AttachmentStatus;
43
+ origin: AttachmentOrigin;
38
44
  description?: string;
39
45
  };
40
46
 
@@ -44,6 +50,8 @@ export type ThreadSummary = {
44
50
  createdAt: string;
45
51
  updatedAt: string;
46
52
  isRunning: boolean;
53
+ /** Present on a headless thread a trigger owns. Widgets never list these. */
54
+ trigger?: { name: string };
47
55
  };
48
56
 
49
57
  /**
@@ -79,6 +87,8 @@ export type ClientToolBinding = {
79
87
  toolId: string;
80
88
  surface: "inline" | "modal";
81
89
  embedOrigin: string;
90
+ /** Variables the initiate call's input reads. */
91
+ variables: string[];
82
92
  };
83
93
 
84
94
  /**
@@ -87,6 +97,8 @@ export type ClientToolBinding = {
87
97
  */
88
98
  export type AgentCapabilities = {
89
99
  attachments: boolean;
100
+ /** False when the agent's `message` trigger is off: the widget hides its composer. */
101
+ messages: boolean;
90
102
  };
91
103
 
92
104
  export type MessageMetadata = {
@@ -96,6 +108,15 @@ export type MessageMetadata = {
96
108
  tokenUsage?: TokenUsage;
97
109
  attachments?: AttachmentSummary[];
98
110
  clientTools?: Record<string, ClientToolBinding>;
111
+ /** Set on `trigger` messages: the firing that wrote them. */
112
+ trigger?: TriggerFiringSummary;
113
+ };
114
+
115
+ export type TriggerFiringSummary = {
116
+ name: string;
117
+ firingId: string;
118
+ firedAt: string;
119
+ payload: unknown;
99
120
  };
100
121
 
101
122
  /**
@@ -108,7 +129,8 @@ export type MessageMetadata = {
108
129
  */
109
130
  export type CortexMessage<TPart = unknown> = {
110
131
  id: string;
111
- role: "system" | "user" | "assistant";
132
+ /** `trigger` rows carry a firing's instructions; the UIs render them as an event, not a bubble. */
133
+ role: "system" | "user" | "assistant" | "trigger";
112
134
  parts: TPart[];
113
135
  metadata?: MessageMetadata;
114
136
  };