@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.
- package/package.json +1 -1
- package/src/commands/management.ts +8 -5
- package/src/commands/setup.ts +15 -8
- package/src/contracts/client-tools/index.ts +21 -0
- package/src/contracts/egress-policy/index.ts +1 -0
- package/src/contracts/egress-policy/url.ts +1 -1
- package/src/contracts/management/procedures.generated.ts +352 -184
- package/src/contracts/manifest/index.ts +186 -64
- package/src/contracts/runs/index.ts +187 -0
- package/src/contracts/schedule/index.ts +62 -0
- package/src/contracts/server/index.ts +101 -29
- package/src/contracts/triggers/index.ts +36 -93
- package/src/contracts/wire/index.ts +162 -55
- package/src/management/flags.ts +14 -4
- package/src/scaffold/files.ts +8 -3
- package/src/ui/prompts.ts +18 -24
|
@@ -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
|
|
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
|
|
1074
|
-
* `per-
|
|
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
|
|
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-
|
|
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
|
-
/**
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
)
|
|
1105
|
-
.
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
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
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
),
|
|
1211
|
-
|
|
1212
|
-
|
|
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
|
-
|
|
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
|
+
}
|