@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.
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { datasetIdentifierSchema } from "../datasets/index";
3
3
  import { canCarrySecret } from "../egress-policy/index";
4
+ import { triggerScheduleSchema } from "../schedule/index";
4
5
  import {
5
6
  displayNameSchema,
6
7
  EVENT_HEADER,
@@ -11,13 +12,13 @@ import {
11
12
  refuseBlockedApprovalTitle,
12
13
  serverToolSchema,
13
14
  } from "../server/index";
14
- import { MESSAGE_TRIGGER_NAME } from "../triggers/index";
15
15
  import {
16
16
  BASE_LOCALE,
17
17
  byLocale,
18
18
  DEFAULT_MEDIA_FORMS,
19
19
  MEDIA_FORMS,
20
20
  TOOL_SURFACES,
21
+ WHILE_BUSY,
21
22
  type Greeting,
22
23
  type Suggestions,
23
24
  } from "../wire/index";
@@ -502,7 +503,7 @@ export function headerListSchema<TEntry extends z.ZodType<{ header: string }>>(
502
503
  });
503
504
  }
504
505
 
505
- /** A variable by name: one the agent's server declares, or a built-in (`timezone`, `locale`, `utcTime`). */
506
+ /** A variable by name: one the agent's server declares, or a built-in (`timezone`, `locale`, `utcTime`, `threadId`). */
506
507
  const variableSourceSchema = z.object({
507
508
  kind: z.literal("variable"),
508
509
  name: z.string().trim().min(1).max(64),
@@ -522,7 +523,7 @@ const constantHeaderSourceSchema = z.object({
522
523
 
523
524
  /**
524
525
  * Where a header's value comes from: a credential, the run's own token (the
525
- * end user's in a chat, the trigger's in a firing), a variable, or a
526
+ * end user's in a chat, the trigger's in a trigger run), a variable, or a
526
527
  * constant. A run token or a variable needs a run; a call without one (a
527
528
  * webhook delivery, a catalog read) presents only credentials and constants.
528
529
  */
@@ -1070,63 +1071,134 @@ const manifestSkillSchema = z.strictObject({
1070
1071
  export type ManifestSkill = z.input<typeof manifestSkillSchema>;
1071
1072
 
1072
1073
  /**
1073
- * Which thread a firing lands in. `single`: one thread for the trigger.
1074
- * `per-firing`: a fresh thread every time. `ephemeral`: a fresh thread that
1074
+ * Which thread a fire lands in. `single`: one thread for the trigger.
1075
+ * `per-input`: a fresh thread every time. `ephemeral`: a fresh thread that
1075
1076
  * is deleted once the run completes, for one-shot calls: the answer reaches
1076
- * the caller as the firing's output or through the live stream, and nothing
1077
+ * the caller as the run's output or through the live stream, and nothing
1077
1078
  * else outlives the run; a failed run keeps its thread for inspection. `keyed`:
1078
1079
  * one thread per distinct value at a JSON pointer into the payload (e.g.
1079
- * `/pull_request/number`).
1080
+ * `/pull_request/number`). `thread`: the existing thread of the agent's whose
1081
+ * id is at the pointer, a user's among them, such as one a tool passed its
1082
+ * backend. Its run acts as the thread's user without their token, which
1083
+ * only a server that allows delegation hears; the trigger's credential is
1084
+ * never presented. Its runs offer no tool that needs approval: nobody
1085
+ * decides the trigger's calls in a user's chat. With `wake: false` no run
1086
+ * starts: the fire leaves its instructions in the thread as a note, its
1087
+ * payload references filled, in its turn behind any run there.
1080
1088
  */
1081
1089
  export const triggerThreadPolicySchema = z.discriminatedUnion("policy", [
1082
1090
  z.object({ policy: z.literal("single") }),
1083
- z.object({ policy: z.literal("per-firing") }),
1091
+ z.object({ policy: z.literal("per-input") }),
1084
1092
  z.object({ policy: z.literal("ephemeral") }),
1085
1093
  z.object({ policy: z.literal("keyed"), key: jsonPointerSchema }),
1094
+ z.object({
1095
+ policy: z.literal("thread"),
1096
+ key: jsonPointerSchema,
1097
+ wake: z.boolean(),
1098
+ }),
1086
1099
  ]);
1087
1100
  export type TriggerThreadPolicy = z.infer<typeof triggerThreadPolicySchema>;
1088
1101
 
1102
+ /** Whether a policy reads its thread from the payload, which a fire with none cannot do. */
1103
+ export function threadFromPayload(policy: TriggerThreadPolicy) {
1104
+ return policy.policy === "keyed" || policy.policy === "thread";
1105
+ }
1106
+
1107
+ /**
1108
+ * What a trigger with a schedule may not have, by the part it sits in: it
1109
+ * fires with no payload and no caller, so it takes no input fields, no fire
1110
+ * credential and no thread read from a payload. Each caller words the
1111
+ * issues itself. None for one without.
1112
+ */
1113
+ export function scheduledTriggerIssues(trigger: {
1114
+ scheduled: boolean;
1115
+ inputFields: number;
1116
+ fireCredential: boolean;
1117
+ threadPolicy: TriggerThreadPolicy;
1118
+ }) {
1119
+ if (!trigger.scheduled) return [];
1120
+ return [
1121
+ trigger.inputFields > 0 && ("input" as const),
1122
+ trigger.fireCredential && ("fireCredential" as const),
1123
+ threadFromPayload(trigger.threadPolicy) && ("threadPolicy" as const),
1124
+ ].filter((issue) => issue !== false);
1125
+ }
1126
+
1089
1127
  /** A trigger's description and instructions in characters. The console caps its editor at the same. */
1090
1128
  export const TRIGGER_LIMITS = { descriptionLength: 500, instructionsLength: 20_000 } as const;
1091
1129
 
1092
- /** Fired with its secret, which the apply that creates it answers once and no manifest holds. */
1093
- const manifestTriggerSchema = z.strictObject({
1094
- /** `message` is the built-in trigger every agent has. */
1095
- name: nameSchema.refine((name) => name !== MESSAGE_TRIGGER_NAME, "the built-in trigger's name"),
1096
- displayName: displayNameSchema.default({}),
1097
- description: z.string().trim().min(1).max(TRIGGER_LIMITS.descriptionLength),
1098
- inputSchema: objectSchema.default(EMPTY_OBJECT),
1099
- /** The shape the run answers in; absent, the run answers in free text. */
1100
- outputSchema: objectSchema
1101
- .refine(
1102
- (schema) => Object.keys(schema.properties ?? {}).length > 0,
1103
- "declares a property; leave it out for a free-text answer",
1104
- )
1105
- .optional(),
1106
- /** Rich text: it may mention tools, skills and datasets, and read `{{variables}}` and `{{/payload/pointer}}`s. */
1107
- instructions: z.string().trim().min(1).max(TRIGGER_LIMITS.instructionsLength),
1108
- threadPolicy: triggerThreadPolicySchema.default({ policy: "single" }),
1109
- /** Firings a day per agent; absent, no limit. */
1110
- budgetPerDay: z.number().int().positive().optional(),
1111
- /** Consecutive failed or rejected firings before the trigger disables itself; it stays off until an operator enables it. */
1112
- disableAfterFailures: setting(SETTING_BOUNDS.trigger.disableAfterFailures),
1113
- /** The credential the run acts with, presented wherever the end user's token would go. */
1114
- credential: nameSchema.optional(),
1115
- /** A static credential whose value callers fire with, in place of a minted secret. */
1116
- fireCredential: nameSchema.optional(),
1117
- /** The model its runs talk to, over each agent's. */
1118
- model: modelSchema.optional(),
1119
- audience: audienceSchema,
1120
- overridable: overridable([
1121
- "displayName",
1122
- "description",
1123
- "instructions",
1124
- "threadPolicy",
1125
- "budgetPerDay",
1126
- "disableAfterFailures",
1127
- "model",
1128
- ]),
1129
- });
1130
+ /**
1131
+ * Fired with its secret, which the apply that creates it answers once and no
1132
+ * manifest holds; or, with a `schedule`, by Cortex itself on its cron, with
1133
+ * no payload and no caller, so it takes no input fields, no fire credential
1134
+ * and no thread read from a payload. Whether it has a schedule is fixed once it
1135
+ * exists.
1136
+ */
1137
+ const manifestTriggerSchema = z
1138
+ .strictObject({
1139
+ name: nameSchema,
1140
+ displayName: displayNameSchema.default({}),
1141
+ description: z.string().trim().min(1).max(TRIGGER_LIMITS.descriptionLength),
1142
+ inputSchema: objectSchema.default(EMPTY_OBJECT),
1143
+ /** The shape the run answers in; absent, the run answers in free text. */
1144
+ outputSchema: objectSchema
1145
+ .refine(
1146
+ (schema) => Object.keys(schema.properties ?? {}).length > 0,
1147
+ "declares a property; leave it out for a free-text answer",
1148
+ )
1149
+ .optional(),
1150
+ /** Rich text: it may mention tools, skills and datasets, and read `{{variables}}` and `{{/payload/pointer}}`s. */
1151
+ instructions: z.string().trim().min(1).max(TRIGGER_LIMITS.instructionsLength),
1152
+ threadPolicy: triggerThreadPolicySchema.default({ policy: "single" }),
1153
+ /** A fire that finds its thread busy: queued as the next turn (the default), steering a running turn of this trigger's, or refused. */
1154
+ whileBusy: z.enum(WHILE_BUSY).default("queue"),
1155
+ /** Fires a day per agent; absent, no limit. */
1156
+ budgetPerDay: z.number().int().positive().optional(),
1157
+ /** Consecutive failed runs or rejected fires before the trigger disables itself; it stays off until an operator enables it. */
1158
+ disableAfterFailures: setting(SETTING_BOUNDS.trigger.disableAfterFailures),
1159
+ /** The credential the run acts with, presented wherever the end user's token would go. */
1160
+ credential: nameSchema.optional(),
1161
+ /** A static credential whose value callers fire with, in place of a minted secret. */
1162
+ fireCredential: nameSchema.optional(),
1163
+ /** The model its runs talk to, over each agent's. */
1164
+ model: modelSchema.optional(),
1165
+ /** When Cortex fires it itself; absent, it fires when a caller calls it. */
1166
+ schedule: triggerScheduleSchema.optional(),
1167
+ audience: audienceSchema,
1168
+ overridable: overridable([
1169
+ "displayName",
1170
+ "description",
1171
+ "instructions",
1172
+ "threadPolicy",
1173
+ "whileBusy",
1174
+ "budgetPerDay",
1175
+ "disableAfterFailures",
1176
+ "model",
1177
+ "schedule",
1178
+ ]),
1179
+ })
1180
+ .superRefine((trigger, context) => {
1181
+ const issues = {
1182
+ input: [
1183
+ "inputSchema",
1184
+ "a scheduled trigger fires with no payload, so it takes no input fields",
1185
+ ],
1186
+ fireCredential: ["fireCredential", "a scheduled trigger has no caller to fire it"],
1187
+ threadPolicy: [
1188
+ "threadPolicy",
1189
+ "a scheduled trigger has no payload to read its thread from",
1190
+ ],
1191
+ } as const;
1192
+ for (const issue of scheduledTriggerIssues({
1193
+ scheduled: trigger.schedule !== undefined,
1194
+ inputFields: Object.keys(trigger.inputSchema.properties ?? {}).length,
1195
+ fireCredential: trigger.fireCredential !== undefined,
1196
+ threadPolicy: trigger.threadPolicy,
1197
+ })) {
1198
+ const [path, message] = issues[issue];
1199
+ context.addIssue({ code: "custom", path: [path], message });
1200
+ }
1201
+ });
1130
1202
  export type ManifestTrigger = z.input<typeof manifestTriggerSchema>;
1131
1203
 
1132
1204
  /** The datasets `pinned` lists that `expose` does not: a host pins only what it exposes. */
@@ -1196,24 +1268,68 @@ export const mcpToolNamesSchema = z
1196
1268
  )
1197
1269
  .max(200);
1198
1270
 
1199
- const manifestMcpServerSchema = z.strictObject({
1200
- /** Prefixes every tool the server contributes: scripts call `tools.<name>_<tool>()`. */
1201
- name: identifierSchema,
1202
- displayName: displayNameSchema.default({}),
1203
- url: upstreamUrlSchema,
1204
- headers: declaredHeadersSchema(
1205
- presentedHeaderSchema.refine(
1206
- (name) => !mcpTransportHeaders.has(name.toLowerCase()),
1207
- "a header the MCP transport sets",
1208
- ),
1209
- headerSourceSchema,
1210
- ),
1211
- /** The server's own tool names to offer; empty offers every tool. */
1212
- include: mcpToolNamesSchema.default([]),
1213
- exclude: mcpToolNamesSchema.default([]),
1214
- audience: audienceSchema,
1215
- overridable: overridable(["displayName", "include", "exclude", "headers"]),
1271
+ /** Who holds an MCP server's sign-in: one connection an operator makes for every agent, or one per end user of each agent. */
1272
+ export const MCP_CONNECTION_OWNERS = ["shared", "endUser"] as const;
1273
+
1274
+ /**
1275
+ * How Cortex signs in to an MCP server that takes OAuth: who connects, the
1276
+ * scope it asks for (absent, what the server advertises), and, for an
1277
+ * authorization server that takes neither a client ID document nor dynamic
1278
+ * registration, the credential holding the app registered there beforehand:
1279
+ * an OAuth2 client credentials one naming that server as its issuer.
1280
+ */
1281
+ export const mcpSignInSchema = z.strictObject({
1282
+ connects: z.enum(MCP_CONNECTION_OWNERS),
1283
+ scope: z.string().trim().min(1).max(1_000).optional(),
1284
+ credential: nameSchema.optional(),
1216
1285
  });
1286
+ export type McpSignIn = z.infer<typeof mcpSignInSchema>;
1287
+
1288
+ /**
1289
+ * The index of the Authorization header a server that signs in declares
1290
+ * beside its sign-in, whose token travels under that header; -1 when it
1291
+ * signs in without one, or declares one without signing in.
1292
+ */
1293
+ export function authorizationBesideSignIn(server: {
1294
+ signIn?: McpSignIn | null;
1295
+ headers: ReadonlyArray<{ header: string }>;
1296
+ }) {
1297
+ return server.signIn
1298
+ ? server.headers.findIndex((entry) => entry.header.toLowerCase() === "authorization")
1299
+ : -1;
1300
+ }
1301
+
1302
+ const manifestMcpServerSchema = z
1303
+ .strictObject({
1304
+ /** Prefixes every tool the server contributes: scripts call `tools.<name>_<tool>()`. */
1305
+ name: identifierSchema,
1306
+ displayName: displayNameSchema.default({}),
1307
+ url: upstreamUrlSchema,
1308
+ headers: declaredHeadersSchema(
1309
+ presentedHeaderSchema.refine(
1310
+ (name) => !mcpTransportHeaders.has(name.toLowerCase()),
1311
+ "a header the MCP transport sets",
1312
+ ),
1313
+ headerSourceSchema,
1314
+ ),
1315
+ /** Absent, every request carries only its headers. */
1316
+ signIn: mcpSignInSchema.optional(),
1317
+ /** The server's own tool names to offer; empty offers every tool. */
1318
+ include: mcpToolNamesSchema.default([]),
1319
+ exclude: mcpToolNamesSchema.default([]),
1320
+ audience: audienceSchema,
1321
+ overridable: overridable(["displayName", "include", "exclude", "headers", "signIn"]),
1322
+ })
1323
+ .superRefine((server, context) => {
1324
+ const index = authorizationBesideSignIn(server);
1325
+ if (index >= 0) {
1326
+ context.addIssue({
1327
+ code: "custom",
1328
+ path: ["headers", index, "header"],
1329
+ message: "the sign-in's token travels under Authorization",
1330
+ });
1331
+ }
1332
+ });
1217
1333
  export type ManifestMcpServer = z.input<typeof manifestMcpServerSchema>;
1218
1334
 
1219
1335
  /**
@@ -1458,6 +1574,10 @@ export const agentSettingsSchema = z.strictObject({
1458
1574
  toolRegistryEnabled: z.boolean().default(false),
1459
1575
  /** Lets `searchRegistries` search the services the agent sees. */
1460
1576
  serviceRegistryEnabled: z.boolean().default(false),
1577
+ /** Lets the agent hand a task to a copy of itself (`delegateTask`), in its turn or in the background. */
1578
+ subagentsEnabled: z.boolean().default(false),
1579
+ /** Lets the agent schedule a check-back on a user's thread (`scheduleWake`): it comes back later on its own. */
1580
+ checkBacksEnabled: z.boolean().default(false),
1461
1581
  attachmentsEnabled: z.boolean().default(true),
1462
1582
  /** Hosts the built-in `fetchImage` tool may pull images from; none, no such tool. */
1463
1583
  fetchImageHostnames: z
@@ -1468,7 +1588,9 @@ export const agentSettingsSchema = z.strictObject({
1468
1588
  /** The fenced media forms the agent may draw, which its prompt then describes. */
1469
1589
  media: z.array(z.enum(MEDIA_FORMS)).default([...DEFAULT_MEDIA_FORMS]),
1470
1590
  /** Off, end users cannot message the agent: only its triggers reach it. */
1471
- messageTrigger: z.boolean().default(true),
1591
+ messages: z.boolean().default(true),
1592
+ /** A message sent while the agent works: refused, queued, or steering the running turn. */
1593
+ whileBusy: z.enum(WHILE_BUSY).default("refuse"),
1472
1594
  defaultLocale: localeSchema.default(BASE_LOCALE),
1473
1595
  context: z
1474
1596
  .strictObject({
@@ -0,0 +1,187 @@
1
+ import { z } from "zod";
2
+
3
+ /*
4
+ * Runs: every piece of work Cortex does on a thread, a turn, a subagent or a
5
+ * tool moved to the background. A run that is `running` holds a lease its
6
+ * producer renews; one past its lease is lost.
7
+ */
8
+
9
+ export const RUN_KINDS = ["turn", "subagent", "tool"] as const;
10
+
11
+ export const RUN_STATUSES = ["running", "waiting", "completed", "failed", "cancelled"] as const;
12
+ export type RunStatus = (typeof RUN_STATUSES)[number];
13
+
14
+ /**
15
+ * Why a run ended as it did: a trigger's answer that never came or never
16
+ * fit its schema, a stop, a cancel (its own, or its parent's), a producer
17
+ * that died (`lost`), or one that ran out of time.
18
+ */
19
+ export const RUN_REASONS = [
20
+ "no_output",
21
+ "invalid_output",
22
+ "stopped",
23
+ "cancelled",
24
+ "parent_cancelled",
25
+ "lost",
26
+ "timeout",
27
+ ] as const;
28
+ export type RunReason = (typeof RUN_REASONS)[number];
29
+
30
+ /*
31
+ * Inputs: anything that asks a thread for work. Every source offers one the
32
+ * same way, and a run takes it: a user's message, a trigger's fire, a
33
+ * schedule come due, a finished run waking its parent.
34
+ */
35
+
36
+ export const INPUT_SOURCES = ["user", "trigger", "schedule", "run"] as const;
37
+ export type InputSource = (typeof INPUT_SOURCES)[number];
38
+
39
+ /**
40
+ * How an input reaches its thread. `prompt`: the thread's busy rule decides
41
+ * (`whileBusy`). `steer`: it joins the running turn before its next step.
42
+ * `followUp`: a turn of its own after the running one. `answer`: it decides
43
+ * a run's waits. `note`: an event message, and no turn.
44
+ */
45
+ export const INPUT_DELIVERIES = ["prompt", "steer", "followUp", "answer", "note"] as const;
46
+ export type InputDelivery = (typeof INPUT_DELIVERIES)[number];
47
+
48
+ /** `scheduled` until it is due, `waiting` for a run, `taken` by one, `done` once that run stored it; or `refused`, or `cancelled`. */
49
+ export const INPUT_STATES = [
50
+ "scheduled",
51
+ "waiting",
52
+ "taken",
53
+ "done",
54
+ "refused",
55
+ "cancelled",
56
+ ] as const;
57
+ export type InputState = (typeof INPUT_STATES)[number];
58
+
59
+ /**
60
+ * Why an input was refused: its thread is busy and its busy rule refuses, a
61
+ * call waits for a decision, its key names an input with another actor or
62
+ * body, the agent's request bucket or the trigger's daily budget is spent,
63
+ * the thread holds as many waiting prompts as it takes, or an answer finds
64
+ * no call waiting for it.
65
+ */
66
+ export const INPUT_REFUSALS = [
67
+ "busy",
68
+ "approval_pending",
69
+ "conflict",
70
+ "rate_limited",
71
+ "budget",
72
+ "full",
73
+ "not_waiting",
74
+ ] as const;
75
+ export type InputRefusal = (typeof INPUT_REFUSALS)[number];
76
+
77
+ /** Why an input that waited never ran: the thread was stopped or deleted, or its trigger was. */
78
+ export const INPUT_CANCEL_REASONS = ["stopped"] as const;
79
+ export type InputCancelReason = (typeof INPUT_CANCEL_REASONS)[number];
80
+
81
+ /** Every reason an input records: a refusal, or why it never ran. */
82
+ export const INPUT_REASONS = [...INPUT_REFUSALS, ...INPUT_CANCEL_REASONS] as const;
83
+
84
+ /** Who a run acts as: a user, or the named credential a trigger acts with. */
85
+ export type Actor = { type: "user"; id: string } | { type: "credential"; name: string };
86
+
87
+ /**
88
+ * What became of an offered input no run took at once: `POST /chat` and a
89
+ * trigger's fire answer 202 while it waits, 409 or 429 once it is refused,
90
+ * and a fire held for its run (`?wait=`) 409 once it was cancelled before
91
+ * any run took it.
92
+ * A refused input is kept for its history (`inputId`), except a
93
+ * `conflict`, whose key names another input, an answer nothing waits for
94
+ * (`not_waiting`), and one the agent's request bucket refused
95
+ * (`rate_limited`), which leave nothing behind.
96
+ */
97
+ export const inputReceiptSchema = z.discriminatedUnion("state", [
98
+ z.object({
99
+ inputId: z.uuid(),
100
+ state: z.literal("waiting"),
101
+ delivery: z.enum(INPUT_DELIVERIES),
102
+ }),
103
+ z.object({
104
+ inputId: z.uuid().nullable(),
105
+ state: z.literal("refused"),
106
+ reason: z.enum(INPUT_REFUSALS),
107
+ }),
108
+ /* An input that waited and never ran (`INPUT_CANCEL_REASONS`). */
109
+ z.object({
110
+ inputId: z.uuid(),
111
+ state: z.literal("cancelled"),
112
+ reason: z.enum(INPUT_CANCEL_REASONS),
113
+ }),
114
+ ]);
115
+ export type InputReceipt = z.infer<typeof inputReceiptSchema>;
116
+
117
+ /*
118
+ * Waits: what a run needs from outside before it goes on. A run that parks
119
+ * opens one per call it stopped on and waits until each is answered, or
120
+ * ends, which cancels those still open.
121
+ */
122
+
123
+ /** `approval`: a server tool call someone decides. `client_tool`: a call the widget runs and answers. */
124
+ export const WAIT_KINDS = ["approval", "client_tool"] as const;
125
+
126
+ export const WAIT_STATES = ["open", "answered", "cancelled"] as const;
127
+ export type WaitState = (typeof WAIT_STATES)[number];
128
+
129
+ /**
130
+ * One call a waiting run waits on, and the arguments it was made with: an
131
+ * approval, which `POST /agents/:agentName/runs/:id/answer` decides, or a
132
+ * client tool's call, which its widget answers.
133
+ */
134
+ export const runWaitSchema = z.object({
135
+ kind: z.enum(WAIT_KINDS),
136
+ toolCallId: z.string(),
137
+ toolName: z.string(),
138
+ input: z.unknown(),
139
+ });
140
+ export type RunWait = z.infer<typeof runWaitSchema>;
141
+
142
+ /**
143
+ * A run as `GET /agents/:agentName/runs/:id` reads it, and each row of
144
+ * `GET /agents/:agentName/triggers/:name/runs`: the input it answers, and
145
+ * its thread, null once that thread is gone.
146
+ */
147
+ export const runRecordSchema = z.object({
148
+ id: z.uuid(),
149
+ threadId: z.uuid().nullable(),
150
+ inputId: z.uuid().nullable(),
151
+ kind: z.enum(RUN_KINDS),
152
+ /** A subagent's run: the id its parent's stream knew it by; each run of a copy has its own. */
153
+ subagentRunId: z.string().nullable(),
154
+ status: z.enum(RUN_STATUSES),
155
+ reason: z.enum(RUN_REASONS).nullable(),
156
+ /**
157
+ * What a completed run answered with: a trigger's validated answer when
158
+ * it declares an output schema, else the run's last text (null when it
159
+ * wrote none), cut past 1 MiB with a trailing `…[truncated]`; null for
160
+ * any other status.
161
+ */
162
+ result: z.unknown(),
163
+ /** The calls a `waiting` run waits on; empty for any other status. */
164
+ waits: z.array(runWaitSchema),
165
+ createdAt: z.iso.datetime(),
166
+ finishedAt: z.iso.datetime().nullable(),
167
+ });
168
+ export type RunRecord = z.infer<typeof runRecordSchema>;
169
+
170
+ /** `GET /agents/:agentName/triggers/:name/runs`: a page of the trigger's runs, newest first. */
171
+ export const runListSchema = z.object({ runs: z.array(runRecordSchema) });
172
+ export type RunList = z.infer<typeof runListSchema>;
173
+
174
+ /** `POST /agents/:agentName/runs/:id/cancel`: whether it stopped or ended anything; a run already over is left as it is. */
175
+ export const runCancelSchema = z.object({ cancelled: z.boolean() });
176
+ export type RunCancel = z.infer<typeof runCancelSchema>;
177
+
178
+ /**
179
+ * `POST /agents/:agentName/runs/:id/answer`: the decision on one call the
180
+ * run waits on. A call id is as long as the model provider made it (some
181
+ * carry a signature), so only the request's body limit bounds it.
182
+ */
183
+ export const runAnswerSchema = z.object({
184
+ toolCallId: z.string().min(1),
185
+ approved: z.boolean(),
186
+ });
187
+ export type RunAnswer = z.infer<typeof runAnswerSchema>;
@@ -0,0 +1,62 @@
1
+ import { CronExpressionParser } from "cron-parser";
2
+ import { z } from "zod";
3
+
4
+ /*
5
+ * A trigger's schedule: a five-field cron read in an IANA time zone, on
6
+ * which Cortex fires the trigger itself. The manifest checks one with
7
+ * `triggerScheduleSchema`, the console with its own words for the same
8
+ * checks (`triggerScheduleSchemaWith`); the sweep that fires it and the console
9
+ * that shows what comes next read its times with `dueTimes`.
10
+ */
11
+
12
+ /** Whether `timeZone` is an IANA zone this runtime knows. */
13
+ function isTimeZone(timeZone: string) {
14
+ try {
15
+ new Intl.DateTimeFormat("en", { timeZone });
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Whether `cron` is five fields (minute, hour, day of month, month, day of
24
+ * week) the parser reads; seconds and aliases are left out, and so is `H`,
25
+ * which the parser fills with a new random value on every read.
26
+ */
27
+ function isCron(cron: string) {
28
+ if (cron.split(/\s+/).length !== 5 || /\bH\b/i.test(cron)) return false;
29
+ try {
30
+ CronExpressionParser.parse(cron);
31
+ return true;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * A schedule whose issues read `messages`, by the field each sits on: the
39
+ * console passes its translations, `triggerScheduleSchema` the English the
40
+ * manifest and the API answer with.
41
+ */
42
+ export function triggerScheduleSchemaWith(messages: Record<"cron" | "timeZone", () => string>) {
43
+ return z.object({
44
+ cron: z.string().trim().refine(isCron, { error: messages.cron }),
45
+ timeZone: z.string().trim().refine(isTimeZone, { error: messages.timeZone }),
46
+ });
47
+ }
48
+
49
+ export const triggerScheduleSchema = triggerScheduleSchemaWith({
50
+ cron: () => "is not a cron of five fields: minute, hour, day of month, month, day of week",
51
+ timeZone: () => "is not an IANA time zone, such as Asia/Dubai",
52
+ });
53
+ export type TriggerSchedule = z.infer<typeof triggerScheduleSchema>;
54
+
55
+ /** The `count` times `schedule` falls due after `after`, soonest first. */
56
+ export function dueTimes(schedule: TriggerSchedule, after: Date, count = 1) {
57
+ const expression = CronExpressionParser.parse(schedule.cron, {
58
+ tz: schedule.timeZone,
59
+ currentDate: after,
60
+ });
61
+ return Array.from({ length: count }, () => expression.next().toDate());
62
+ }