@m6d/cortex-cli 0.22.0 → 0.24.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.
@@ -0,0 +1,276 @@
1
+ // cspell:ignore fefe fff
2
+ import { RE2JS } from "re2js";
3
+
4
+ /*
5
+ * A tool input schema's `pattern`: a regular expression JavaScript reads with
6
+ * no flags, which Cortex matches on RE2 so a value is checked in time linear
7
+ * in its length. A pattern RE2 cannot match as JavaScript reads it is refused
8
+ * by Cortex and left out of what the TypeScript SDK exports.
9
+ *
10
+ * RE2 matches code points where JavaScript matches UTF-16 units, so the
11
+ * pattern and every input lift each surrogate unit into a private-use plane,
12
+ * where it is one code point: RE2 then reads the units JavaScript reads.
13
+ * Nothing collides there, since a real character in that plane is astral,
14
+ * arrives as two surrogate units and is lifted in turn.
15
+ */
16
+
17
+ /**
18
+ * What one escape or character in a class stands for, spelled as the inside
19
+ * of an RE2 class: a single character carries its UTF-16 unit, and an
20
+ * unescaped `-` is marked as one that may make a range.
21
+ */
22
+ type Atom = { body: string; char?: number; dash?: boolean };
23
+
24
+ /** JavaScript's `\s`, its WhiteSpace and LineTerminator characters, as the inside of an RE2 class. */
25
+ const SPACE =
26
+ "\\x{9}-\\x{d}\\x{20}\\x{a0}\\x{1680}\\x{2000}-\\x{200a}\\x{2028}\\x{2029}\\x{202f}\\x{205f}\\x{3000}\\x{feff}";
27
+ /** Every character `SPACE` leaves out: JavaScript's `\S`. */
28
+ const NON_SPACE =
29
+ "\\x{0}-\\x{8}\\x{e}-\\x{1f}\\x{21}-\\x{9f}\\x{a1}-\\x{167f}\\x{1681}-\\x{1fff}\\x{200b}-\\x{2027}" +
30
+ "\\x{202a}-\\x{202e}\\x{2030}-\\x{205e}\\x{2060}-\\x{2fff}\\x{3001}-\\x{fefe}\\x{ff00}-\\x{10ffff}";
31
+ /** The set escapes as JavaScript reads them; RE2's own `\d` and `\w` match, its `\s` holds only ASCII. */
32
+ const SET_ESCAPES = new Map([
33
+ ["d", "\\d"],
34
+ ["D", "\\D"],
35
+ ["w", "\\w"],
36
+ ["W", "\\W"],
37
+ ["s", SPACE],
38
+ ["S", NON_SPACE],
39
+ ]);
40
+ /** JavaScript's `.`: every character but a line terminator, where RE2's leaves out only `\n`. */
41
+ const DOT = "[^\\x{a}\\x{d}\\x{2028}\\x{2029}]";
42
+ const CONTROL_ESCAPES = new Map([
43
+ ["f", 0x0c],
44
+ ["n", 0x0a],
45
+ ["r", 0x0d],
46
+ ["t", 0x09],
47
+ ["v", 0x0b],
48
+ ]);
49
+ const QUANTIFIER = /\{\d+(?:,\d*)?\}/y;
50
+ const SURROGATE = /[\uD800-\uDFFF]/g;
51
+ /** Where surrogate unit `0xd800 + n` is lifted: code point `0xf0000 + n`. */
52
+ const LIFTED_SURROGATES = 0xf0000;
53
+ const HEX = /[0-9A-Fa-f]+/y;
54
+ /** What JavaScript's `[^]` and `[]` hold: every character, and none. */
55
+ const ANY = "[\\x{0}-\\x{10ffff}]";
56
+ const NONE = "[^\\x{0}-\\x{10ffff}]";
57
+
58
+ /** A thrown error: the pattern holds what RE2 cannot match as JavaScript reads it. */
59
+ function unsupported(what: string): never {
60
+ throw new Error(`a pattern with ${what}`);
61
+ }
62
+
63
+ /** A UTF-16 unit as the one code point RE2 reads: a surrogate lifted, any other unit itself. */
64
+ function lifted(unit: number) {
65
+ return unit >= 0xd800 && unit <= 0xdfff ? LIFTED_SURROGATES + unit - 0xd800 : unit;
66
+ }
67
+
68
+ /** `input` with every surrogate unit lifted, one code point per UTF-16 unit. */
69
+ function liftedText(input: string) {
70
+ return input.replace(SURROGATE, (unit) => String.fromCodePoint(lifted(unit.charCodeAt(0))));
71
+ }
72
+
73
+ /** One UTF-16 unit as RE2 reads it literally, inside a class or out. */
74
+ function literal(unit: number) {
75
+ const char = lifted(unit);
76
+ const spelled = String.fromCodePoint(char);
77
+ return /^[A-Za-z0-9]$/.test(spelled) ? spelled : `\\x{${char.toString(16)}}`;
78
+ }
79
+
80
+ /**
81
+ * The units from `from` to `to` as the inside of an RE2 class: the part
82
+ * among the surrogates, lifted, apart from the parts below and above it.
83
+ */
84
+ function unitRange(from: number, to: number) {
85
+ return (
86
+ [
87
+ [from, Math.min(to, 0xd7ff)],
88
+ [Math.max(from, 0xd800), Math.min(to, 0xdfff)],
89
+ [Math.max(from, 0xe000), to],
90
+ ] satisfies Array<[number, number]>
91
+ )
92
+ .filter(([low, high]) => low <= high)
93
+ .map(([low, high]) => `${literal(low)}-${literal(high)}`)
94
+ .join("");
95
+ }
96
+
97
+ function character(char: number, dash = false) {
98
+ return { body: literal(char), char, dash };
99
+ }
100
+
101
+ /**
102
+ * A class's atoms as the inside of an RE2 class: an unescaped dash between
103
+ * two characters makes a range, and beside a set escape it is a character,
104
+ * as JavaScript reads them.
105
+ */
106
+ function classBody(atoms: Atom[]) {
107
+ let body = "";
108
+ for (let index = 0; index < atoms.length; index += 1) {
109
+ const [from, dash, to] = atoms.slice(index, index + 3);
110
+ if (!from) break;
111
+ if (!dash?.dash || !to) {
112
+ body += from.body;
113
+ continue;
114
+ }
115
+ body +=
116
+ from.char !== undefined && to.char !== undefined
117
+ ? unitRange(from.char, to.char)
118
+ : `${from.body}${dash.body}${to.body}`;
119
+ index += 2;
120
+ }
121
+ return body;
122
+ }
123
+
124
+ /**
125
+ * A pattern read left to right as JavaScript reads it with no flags (Annex B
126
+ * included), written in RE2's syntax; throws on a lookaround, a
127
+ * backreference, a legacy octal escape, a group modifier or a `\c` without
128
+ * its letter. It reads UTF-16 units, as JavaScript does. Groups do not
129
+ * capture: nothing reads them.
130
+ */
131
+ class Translation {
132
+ private at = 0;
133
+
134
+ public constructor(private readonly pattern: string) {}
135
+
136
+ public spell() {
137
+ let re2 = "";
138
+ while (this.at < this.pattern.length) re2 += this.term();
139
+ return re2;
140
+ }
141
+
142
+ private term() {
143
+ const char = this.pattern[this.at] ?? "";
144
+ if (char === "\\") return this.escapeOutsideClass();
145
+ if (char === "[") return this.characterClass();
146
+ if (char === "(") return this.group();
147
+ if (char === "{") return this.brace();
148
+ if (!"|)^$*+?.".includes(char)) return literal(this.unit());
149
+ this.at += 1;
150
+ return char === "." ? DOT : char;
151
+ }
152
+
153
+ private escapeOutsideClass() {
154
+ this.at += 1;
155
+ const assertion = this.pattern[this.at];
156
+ if (assertion === "b" || assertion === "B") {
157
+ this.at += 1;
158
+ return `\\${assertion}`;
159
+ }
160
+ const atom = this.escape(false);
161
+ return "char" in atom ? atom.body : `[${atom.body}]`;
162
+ }
163
+
164
+ /** The escape after a backslash, which `at` is past; in a class `\b` is a backspace. */
165
+ private escape(insideClass: boolean) {
166
+ const next = this.pattern[this.at] ?? unsupported("a trailing backslash");
167
+ this.at += 1;
168
+ const set = SET_ESCAPES.get(next);
169
+ if (set) return { body: set };
170
+ const control = insideClass && next === "b" ? 0x08 : CONTROL_ESCAPES.get(next);
171
+ if (control !== undefined) return character(control);
172
+ if (next === "c") return character(this.controlLetter(insideClass));
173
+ if (next === "x" || next === "u") {
174
+ return character(this.hex(next === "x" ? 2 : 4) ?? next.charCodeAt(0));
175
+ }
176
+ if (next === "0" && !/^\d$/.test(this.pattern[this.at] ?? "")) return character(0);
177
+ if (/^[\dk]$/.test(next)) unsupported("a backreference or an octal escape");
178
+ this.at -= 1;
179
+ return character(this.unit());
180
+ }
181
+
182
+ /** The control character a `\c` names: a letter, or in a class also a digit or `_`. */
183
+ private controlLetter(insideClass: boolean) {
184
+ const letter = this.pattern[this.at] ?? "";
185
+ if (!(insideClass ? /^\w$/ : /^[A-Za-z]$/).test(letter)) {
186
+ unsupported("a \\c without its letter");
187
+ }
188
+ this.at += 1;
189
+ return letter.charCodeAt(0) % 32;
190
+ }
191
+
192
+ /** `length` hex digits at `at` as a UTF-16 unit, consumed; undefined when fewer follow. */
193
+ private hex(length: number) {
194
+ HEX.lastIndex = this.at;
195
+ const digits = HEX.exec(this.pattern)?.[0] ?? "";
196
+ if (digits.length < length) return undefined;
197
+ this.at += length;
198
+ return Number.parseInt(digits.slice(0, length), 16);
199
+ }
200
+
201
+ private unit() {
202
+ const unit = this.pattern.charCodeAt(this.at);
203
+ if (Number.isNaN(unit)) unsupported("a pattern cut short");
204
+ this.at += 1;
205
+ return unit;
206
+ }
207
+
208
+ /**
209
+ * A class, `at` on its `[`. In JavaScript a `]` always closes one, so
210
+ * `[]` holds nothing and `[^]` everything.
211
+ */
212
+ private characterClass() {
213
+ this.at += 1;
214
+ const negated = this.pattern[this.at] === "^";
215
+ if (negated) this.at += 1;
216
+ const atoms: Atom[] = [];
217
+ while (this.pattern[this.at] !== "]") {
218
+ if (this.at >= this.pattern.length) unsupported("an unclosed class");
219
+ atoms.push(this.classAtom());
220
+ }
221
+ this.at += 1;
222
+ const body = classBody(atoms);
223
+ if (!body) return negated ? ANY : NONE;
224
+ return `[${negated ? "^" : ""}${body}]`;
225
+ }
226
+
227
+ private classAtom() {
228
+ const char = this.pattern[this.at];
229
+ this.at += 1;
230
+ if (char === "\\") return this.escape(true);
231
+ if (char === "-") return character(0x2d, true);
232
+ this.at -= 1;
233
+ return character(this.unit());
234
+ }
235
+
236
+ /** A group's opening as RE2 reads it: every group, named or not, without a capture. */
237
+ private group() {
238
+ const opening = this.pattern.slice(this.at, this.at + 4);
239
+ if (/^\(\?<[^=!]/.test(opening)) {
240
+ const name = this.pattern.indexOf(">", this.at);
241
+ if (name < 0) unsupported("an unclosed group name");
242
+ this.at = name + 1;
243
+ } else if (opening.startsWith("(?")) {
244
+ if (!opening.startsWith("(?:")) unsupported("a lookaround or a group modifier");
245
+ this.at += 3;
246
+ } else {
247
+ this.at += 1;
248
+ }
249
+ return "(?:";
250
+ }
251
+
252
+ /** A quantifier, or a brace JavaScript reads as a character because it opens none. */
253
+ private brace() {
254
+ QUANTIFIER.lastIndex = this.at;
255
+ const quantifier = QUANTIFIER.exec(this.pattern)?.[0];
256
+ this.at += quantifier?.length ?? 1;
257
+ return quantifier ?? literal(0x7b);
258
+ }
259
+ }
260
+
261
+ /**
262
+ * A matcher for a pattern JavaScript compiles, meaning what the pattern
263
+ * means to JavaScript with no flags, UTF-16 unit for unit; undefined when
264
+ * RE2 cannot match it so, as for a lookaround, a backreference or a
265
+ * repetition past 1000. Its `test` runs on RE2, in time linear in the input.
266
+ * It never calls `new RegExp`, so it works while a caller has replaced that
267
+ * global.
268
+ */
269
+ export function linearPattern(pattern: string) {
270
+ try {
271
+ const re2 = RE2JS.compile(new Translation(pattern).spell());
272
+ return { test: (input: string) => re2.test(liftedText(input)) };
273
+ } catch {
274
+ return undefined;
275
+ }
276
+ }
@@ -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
+ }