@namzu/sdk 50.1.0 → 51.1.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/CHANGELOG.md +28 -0
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +1 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/execution/code-runtime/types.d.ts +35 -0
- package/dist/execution/code-runtime/types.d.ts.map +1 -1
- package/dist/execution/code-runtime/types.js.map +1 -1
- package/dist/execution/code-runtime/worker-program.d.ts +1 -1
- package/dist/execution/code-runtime/worker-program.d.ts.map +1 -1
- package/dist/execution/code-runtime/worker-program.js +72 -13
- package/dist/execution/code-runtime/worker-program.js.map +1 -1
- package/dist/execution/code-runtime/worker.d.ts.map +1 -1
- package/dist/execution/code-runtime/worker.js +35 -1
- package/dist/execution/code-runtime/worker.js.map +1 -1
- package/dist/execution/workflow/errors.d.ts +5 -0
- package/dist/execution/workflow/errors.d.ts.map +1 -0
- package/dist/execution/workflow/errors.js +8 -0
- package/dist/execution/workflow/errors.js.map +1 -0
- package/dist/execution/workflow/index.d.ts +9 -0
- package/dist/execution/workflow/index.d.ts.map +1 -0
- package/dist/execution/workflow/index.js +6 -0
- package/dist/execution/workflow/index.js.map +1 -0
- package/dist/execution/workflow/journal.d.ts +18 -0
- package/dist/execution/workflow/journal.d.ts.map +1 -0
- package/dist/execution/workflow/journal.js +43 -0
- package/dist/execution/workflow/journal.js.map +1 -0
- package/dist/execution/workflow/meta.d.ts +14 -0
- package/dist/execution/workflow/meta.d.ts.map +1 -0
- package/dist/execution/workflow/meta.js +314 -0
- package/dist/execution/workflow/meta.js.map +1 -0
- package/dist/execution/workflow/phases.d.ts +32 -0
- package/dist/execution/workflow/phases.d.ts.map +1 -0
- package/dist/execution/workflow/phases.js +57 -0
- package/dist/execution/workflow/phases.js.map +1 -0
- package/dist/execution/workflow/prelude.d.ts +15 -0
- package/dist/execution/workflow/prelude.d.ts.map +1 -0
- package/dist/execution/workflow/prelude.js +128 -0
- package/dist/execution/workflow/prelude.js.map +1 -0
- package/dist/execution/workflow/runtime.d.ts +31 -0
- package/dist/execution/workflow/runtime.d.ts.map +1 -0
- package/dist/execution/workflow/runtime.js +682 -0
- package/dist/execution/workflow/runtime.js.map +1 -0
- package/dist/execution/workflow/schema.d.ts +10 -0
- package/dist/execution/workflow/schema.d.ts.map +1 -0
- package/dist/execution/workflow/schema.js +145 -0
- package/dist/execution/workflow/schema.js.map +1 -0
- package/dist/execution/workflow/types.d.ts +267 -0
- package/dist/execution/workflow/types.d.ts.map +1 -0
- package/dist/execution/workflow/types.js +7 -0
- package/dist/execution/workflow/types.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin/shell-hook.d.ts.map +1 -1
- package/dist/plugin/shell-hook.js +2 -1
- package/dist/plugin/shell-hook.js.map +1 -1
- package/dist/process/kill-tree.d.ts +7 -0
- package/dist/process/kill-tree.d.ts.map +1 -1
- package/dist/process/kill-tree.js +41 -0
- package/dist/process/kill-tree.js.map +1 -1
- package/dist/provider/errors.d.ts +10 -0
- package/dist/provider/errors.d.ts.map +1 -1
- package/dist/provider/errors.js +16 -0
- package/dist/provider/errors.js.map +1 -1
- package/dist/provider/retry.d.ts +27 -0
- package/dist/provider/retry.d.ts.map +1 -1
- package/dist/provider/retry.js +19 -3
- package/dist/provider/retry.js.map +1 -1
- package/dist/provider/token-budget.d.ts.map +1 -1
- package/dist/provider/token-budget.js +31 -2
- package/dist/provider/token-budget.js.map +1 -1
- package/dist/public-runtime.d.ts +1 -0
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +3 -0
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-tools.d.ts +1 -1
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +1 -1
- package/dist/public-tools.js.map +1 -1
- package/dist/public-types.d.ts +3 -0
- package/dist/public-types.d.ts.map +1 -1
- package/dist/runtime/jobs/job-id.d.ts +21 -0
- package/dist/runtime/jobs/job-id.d.ts.map +1 -0
- package/dist/runtime/jobs/job-id.js +66 -0
- package/dist/runtime/jobs/job-id.js.map +1 -0
- package/dist/runtime/jobs/registry.d.ts +0 -1
- package/dist/runtime/jobs/registry.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.js +2 -3
- package/dist/runtime/jobs/registry.js.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.js +24 -1
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
- package/dist/runtime/query/prompt.d.ts.map +1 -1
- package/dist/runtime/query/prompt.js +5 -1
- package/dist/runtime/query/prompt.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts +8 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +20 -3
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/job.d.ts.map +1 -1
- package/dist/tools/builtins/job.js +4 -0
- package/dist/tools/builtins/job.js.map +1 -1
- package/dist/tools/builtins/wait-for-job.d.ts.map +1 -1
- package/dist/tools/builtins/wait-for-job.js +4 -0
- package/dist/tools/builtins/wait-for-job.js.map +1 -1
- package/dist/tools/command-shell.d.ts +55 -0
- package/dist/tools/command-shell.d.ts.map +1 -1
- package/dist/tools/command-shell.js +174 -13
- package/dist/tools/command-shell.js.map +1 -1
- package/dist/types/provider/error.d.ts +6 -0
- package/dist/types/provider/error.d.ts.map +1 -1
- package/dist/types/provider/errors.d.ts.map +1 -1
- package/dist/types/provider/errors.js +15 -1
- package/dist/types/provider/errors.js.map +1 -1
- package/dist/types/provider/stream.d.ts +6 -0
- package/dist/types/provider/stream.d.ts.map +1 -1
- package/dist/types/session/events.d.ts +5 -0
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js.map +1 -1
- package/dist/utils/backoff.d.ts +12 -0
- package/dist/utils/backoff.d.ts.map +1 -1
- package/dist/utils/backoff.js +15 -0
- package/dist/utils/backoff.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/AGENTS.md +134 -46
- package/src/bridge/sse/mapper.ts +1 -0
- package/src/execution/code-runtime/types.ts +37 -1
- package/src/execution/code-runtime/worker-program.ts +72 -13
- package/src/execution/code-runtime/worker.ts +40 -1
- package/src/execution/workflow/errors.ts +7 -0
- package/src/execution/workflow/index.ts +8 -0
- package/src/execution/workflow/journal.ts +46 -0
- package/src/execution/workflow/meta.ts +316 -0
- package/src/execution/workflow/phases.ts +91 -0
- package/src/execution/workflow/prelude.ts +135 -0
- package/src/execution/workflow/runtime.ts +786 -0
- package/src/execution/workflow/schema.ts +137 -0
- package/src/execution/workflow/types.ts +300 -0
- package/src/index.ts +4 -0
- package/src/plugin/shell-hook.ts +2 -1
- package/src/process/kill-tree.ts +43 -0
- package/src/provider/errors.ts +18 -0
- package/src/provider/retry.ts +53 -3
- package/src/provider/token-budget.ts +31 -1
- package/src/public-runtime.ts +11 -0
- package/src/public-tools.ts +6 -0
- package/src/public-types.ts +3 -0
- package/src/runtime/jobs/job-id.ts +66 -0
- package/src/runtime/jobs/registry.ts +2 -3
- package/src/runtime/query/iteration/stream-turn.ts +24 -1
- package/src/runtime/query/prompt.ts +5 -1
- package/src/tools/builtins/bash.ts +33 -3
- package/src/tools/builtins/job.ts +4 -0
- package/src/tools/builtins/wait-for-job.ts +4 -0
- package/src/tools/command-shell.ts +194 -11
- package/src/types/provider/error.ts +6 -0
- package/src/types/provider/errors.ts +16 -1
- package/src/types/provider/stream.ts +6 -0
- package/src/types/session/events.ts +5 -0
- package/src/utils/backoff.ts +21 -0
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { cpus } from 'node:os';
|
|
3
|
+
import { encodeCodeValue } from '../code-runtime/json-value.js';
|
|
4
|
+
import { WorkerCodeRuntime } from '../code-runtime/worker.js';
|
|
5
|
+
import { WorkflowScriptError } from './errors.js';
|
|
6
|
+
import { preview, sha256 } from './journal.js';
|
|
7
|
+
import { splitWorkflowScript } from './meta.js';
|
|
8
|
+
import { PhaseRegistry, summarizeWorkflowPhases } from './phases.js';
|
|
9
|
+
import { buildPrelude, preludeLineCount } from './prelude.js';
|
|
10
|
+
import { parseJsonText, validateAgainstSchema } from './schema.js';
|
|
11
|
+
/** The one capability a script has. */
|
|
12
|
+
export const WORKFLOW_HOST_CALL = 'agent';
|
|
13
|
+
const MAX_ARGS_BYTES = 64 * 1024;
|
|
14
|
+
const MAX_PROMPT_BYTES = 200 * 1024;
|
|
15
|
+
const MAX_SCHEMA_BYTES = 32 * 1024;
|
|
16
|
+
/** Printed lines (`log()` and `phase()` records) the interpreter may emit. */
|
|
17
|
+
const MAX_LOG_OUTPUT_BYTES = 256 * 1024;
|
|
18
|
+
/** Headroom for the prelude and the serialized `args` on top of the script. */
|
|
19
|
+
const SOURCE_OVERHEAD_BYTES = 16 * 1024 + MAX_ARGS_BYTES;
|
|
20
|
+
const AGENT_CALL_KEYS = new Set([
|
|
21
|
+
'prompt',
|
|
22
|
+
'cursor',
|
|
23
|
+
'label',
|
|
24
|
+
'phase',
|
|
25
|
+
'schema',
|
|
26
|
+
'agentType',
|
|
27
|
+
'model',
|
|
28
|
+
'effort',
|
|
29
|
+
]);
|
|
30
|
+
function integer(name, value, min, max) {
|
|
31
|
+
if (!Number.isSafeInteger(value) || value < min || value > max) {
|
|
32
|
+
throw new RangeError(`${name} must be an integer between ${min} and ${max}.`);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
export function resolveWorkflowLimits(limits = {}) {
|
|
37
|
+
const defaultConcurrency = Math.min(8, Math.max(1, cpus().length - 2));
|
|
38
|
+
return {
|
|
39
|
+
maxConcurrency: integer('maxConcurrency', limits.maxConcurrency ?? defaultConcurrency, 1, 16),
|
|
40
|
+
maxAgents: integer('maxAgents', limits.maxAgents ?? 200, 1, 1000),
|
|
41
|
+
maxSourceBytes: integer('maxSourceBytes', limits.maxSourceBytes ?? 256 * 1024, 1, 4 * 1024 * 1024),
|
|
42
|
+
memoryLimitBytes: integer('memoryLimitBytes', limits.memoryLimitBytes ?? 64 * 1024 * 1024, 16 * 1024 * 1024, 256 * 1024 * 1024),
|
|
43
|
+
maxValueBytes: integer('maxValueBytes', limits.maxValueBytes ?? 1024 * 1024, 1, 16 * 1024 * 1024),
|
|
44
|
+
...(limits.tokenBudget !== undefined
|
|
45
|
+
? { tokenBudget: integer('tokenBudget', limits.tokenBudget, 1, Number.MAX_SAFE_INTEGER) }
|
|
46
|
+
: {}),
|
|
47
|
+
maxDurationMs: integer('maxDurationMs', limits.maxDurationMs ?? 6 * 3_600_000, 1, 2_147_483_647),
|
|
48
|
+
cpuSliceMs: integer('cpuSliceMs', limits.cpuSliceMs ?? 2_000, 1, 2_147_483_647),
|
|
49
|
+
cpuTotalMs: integer('cpuTotalMs', limits.cpuTotalMs ?? 60_000, 1, 2_147_483_647),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function boundedText(value, field, max) {
|
|
53
|
+
if (value === undefined)
|
|
54
|
+
return undefined;
|
|
55
|
+
if (typeof value !== 'string' || value.length > max) {
|
|
56
|
+
throw new Error(`agent(): ${field} must be a string of at most ${max} characters.`);
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
/** The host owns this check: the prelude is convenience, not enforcement. */
|
|
61
|
+
function parseAgentCall(input) {
|
|
62
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
63
|
+
throw new Error('agent(): the request must be an object.');
|
|
64
|
+
}
|
|
65
|
+
const record = input;
|
|
66
|
+
for (const key of Object.keys(record)) {
|
|
67
|
+
if (!AGENT_CALL_KEYS.has(key))
|
|
68
|
+
throw new Error(`agent(): unknown option "${key}".`);
|
|
69
|
+
}
|
|
70
|
+
const prompt = record.prompt;
|
|
71
|
+
if (typeof prompt !== 'string' || prompt.length === 0) {
|
|
72
|
+
throw new Error('agent(): a prompt string is required.');
|
|
73
|
+
}
|
|
74
|
+
if (Buffer.byteLength(prompt) > MAX_PROMPT_BYTES) {
|
|
75
|
+
throw new Error(`agent(): the prompt is larger than ${MAX_PROMPT_BYTES} bytes.`);
|
|
76
|
+
}
|
|
77
|
+
const cursor = record.cursor === null || record.cursor === undefined ? null : record.cursor;
|
|
78
|
+
if (cursor !== null && (typeof cursor !== 'string' || cursor.length > 120)) {
|
|
79
|
+
throw new Error('agent(): cursor must be a short string.');
|
|
80
|
+
}
|
|
81
|
+
let schema;
|
|
82
|
+
if (record.schema !== undefined) {
|
|
83
|
+
if (typeof record.schema !== 'object' ||
|
|
84
|
+
record.schema === null ||
|
|
85
|
+
Array.isArray(record.schema)) {
|
|
86
|
+
throw new Error('agent(): schema must be a JSON Schema object.');
|
|
87
|
+
}
|
|
88
|
+
let size;
|
|
89
|
+
try {
|
|
90
|
+
size = JSON.stringify(record.schema).length;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
throw new Error('agent(): schema must be plain JSON.');
|
|
94
|
+
}
|
|
95
|
+
if (size > MAX_SCHEMA_BYTES) {
|
|
96
|
+
throw new Error(`agent(): schema is larger than ${MAX_SCHEMA_BYTES} bytes.`);
|
|
97
|
+
}
|
|
98
|
+
schema = record.schema;
|
|
99
|
+
}
|
|
100
|
+
const label = boundedText(record.label, 'label', 120);
|
|
101
|
+
const phase = boundedText(record.phase, 'phase', 120);
|
|
102
|
+
const agentType = boundedText(record.agentType, 'agentType', 120);
|
|
103
|
+
const model = boundedText(record.model, 'model', 120);
|
|
104
|
+
const effort = boundedText(record.effort, 'effort', 32);
|
|
105
|
+
return {
|
|
106
|
+
prompt,
|
|
107
|
+
cursor,
|
|
108
|
+
...(label !== undefined ? { label } : {}),
|
|
109
|
+
...(phase !== undefined ? { phase } : {}),
|
|
110
|
+
...(schema !== undefined ? { schema } : {}),
|
|
111
|
+
...(agentType !== undefined ? { agentType } : {}),
|
|
112
|
+
...(model !== undefined ? { model } : {}),
|
|
113
|
+
...(effort !== undefined ? { effort } : {}),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function count(value) {
|
|
117
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
118
|
+
}
|
|
119
|
+
const MAX_SOURCE_LINE_CHARS = 200;
|
|
120
|
+
/**
|
|
121
|
+
* The failure text a model reads: the error, then where in ITS script it
|
|
122
|
+
* happened and the line itself. Interpreter lines count the prelude and the
|
|
123
|
+
* worker's wrapper, so the first frame that falls in the author's text is the
|
|
124
|
+
* one reported (the innermost frame may be a prelude helper that rejected a
|
|
125
|
+
* bad argument; the call site is the next one out).
|
|
126
|
+
*/
|
|
127
|
+
function describeScriptFailure(outcome, body, preludeLines) {
|
|
128
|
+
const what = outcome.errorName !== undefined && outcome.errorName !== ''
|
|
129
|
+
? `${outcome.errorName}: ${outcome.error}`
|
|
130
|
+
: outcome.error;
|
|
131
|
+
const frame = outcome.frames
|
|
132
|
+
?.map((f) => ({ line: f.line - preludeLines, column: f.column }))
|
|
133
|
+
.find((f) => f.line >= 1);
|
|
134
|
+
if (frame === undefined) {
|
|
135
|
+
return `${what} — the interpreter gave no line number for this error.`;
|
|
136
|
+
}
|
|
137
|
+
const text = (body.split('\n')[frame.line - 1] ?? '').trim().slice(0, MAX_SOURCE_LINE_CHARS);
|
|
138
|
+
return `${what} — line ${frame.line}, column ${frame.column}: ${text}`;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Runs a workflow script: a program the MODEL wrote once that decides how
|
|
142
|
+
* many subagents run and in which order.
|
|
143
|
+
*
|
|
144
|
+
* The script is untrusted input. It runs in the `run_code` interpreter (a
|
|
145
|
+
* fresh QuickJS WASM context in a worker thread: no filesystem, network,
|
|
146
|
+
* process, modules or timers, JSON-only crossings, memory and CPU limits) and
|
|
147
|
+
* its single capability is `agent()`, which asks the host's `spawnAgent` to
|
|
148
|
+
* start a subagent through the same delegation path as the `Agent` tool.
|
|
149
|
+
* Scheduling, caps, the token budget, events and the journal are host-side.
|
|
150
|
+
*/
|
|
151
|
+
export class WorkflowRuntime {
|
|
152
|
+
options;
|
|
153
|
+
now;
|
|
154
|
+
constructor(options = {}) {
|
|
155
|
+
this.options = options;
|
|
156
|
+
this.now = options.now ?? Date.now;
|
|
157
|
+
}
|
|
158
|
+
async run(options) {
|
|
159
|
+
const limits = resolveWorkflowLimits(options.limits);
|
|
160
|
+
if (Buffer.byteLength(options.script) > limits.maxSourceBytes) {
|
|
161
|
+
throw new WorkflowScriptError(`The script is larger than ${limits.maxSourceBytes} bytes.`);
|
|
162
|
+
}
|
|
163
|
+
const { meta, body } = splitWorkflowScript(options.script);
|
|
164
|
+
const args = options.args === undefined ? {} : options.args;
|
|
165
|
+
try {
|
|
166
|
+
encodeCodeValue(args, MAX_ARGS_BYTES);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
throw new WorkflowScriptError(`The workflow arguments cannot be used: ${error instanceof Error ? error.message : String(error)}`);
|
|
170
|
+
}
|
|
171
|
+
const now = this.now;
|
|
172
|
+
const runId = options.runId ?? `w${[...randomBytes(8)].map((b) => (b % 36).toString(36)).join('')}`;
|
|
173
|
+
const startedAt = now();
|
|
174
|
+
const phases = new PhaseRegistry();
|
|
175
|
+
const agents = new Map();
|
|
176
|
+
const inFlight = new Set();
|
|
177
|
+
const waiters = [];
|
|
178
|
+
const controller = new AbortController();
|
|
179
|
+
let running = 0;
|
|
180
|
+
let requested = 0;
|
|
181
|
+
let fatal;
|
|
182
|
+
let closed = false;
|
|
183
|
+
let journalError;
|
|
184
|
+
let scriptPhase;
|
|
185
|
+
let journalChain = Promise.resolve();
|
|
186
|
+
const journal = (record) => {
|
|
187
|
+
const sink = options.journal;
|
|
188
|
+
if (!sink || closed)
|
|
189
|
+
return;
|
|
190
|
+
journalChain = journalChain
|
|
191
|
+
.then(() => sink.append(record))
|
|
192
|
+
.catch((error) => {
|
|
193
|
+
journalError ??= error instanceof Error ? error.message : String(error);
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
const emit = (event) => {
|
|
197
|
+
if (closed)
|
|
198
|
+
return;
|
|
199
|
+
try {
|
|
200
|
+
options.onEvent?.(event);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// A listener bug must not decide the run's outcome.
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
const setFatal = (reason) => {
|
|
207
|
+
if (fatal !== undefined)
|
|
208
|
+
return;
|
|
209
|
+
fatal = reason;
|
|
210
|
+
controller.abort(new Error(reason));
|
|
211
|
+
};
|
|
212
|
+
const totalTokens = () => {
|
|
213
|
+
let sum = 0;
|
|
214
|
+
for (const a of agents.values())
|
|
215
|
+
sum += a.tokens;
|
|
216
|
+
return sum;
|
|
217
|
+
};
|
|
218
|
+
const announce = (rawTitle, declared) => {
|
|
219
|
+
const { phase, isNew } = phases.register(rawTitle, declared);
|
|
220
|
+
if (isNew) {
|
|
221
|
+
const at = now();
|
|
222
|
+
emit({
|
|
223
|
+
type: 'workflow_phase',
|
|
224
|
+
runId,
|
|
225
|
+
index: phase.index,
|
|
226
|
+
title: phase.title,
|
|
227
|
+
declared,
|
|
228
|
+
at,
|
|
229
|
+
});
|
|
230
|
+
journal({ type: 'phase', at, index: phase.index, title: phase.title, declared });
|
|
231
|
+
}
|
|
232
|
+
return phase;
|
|
233
|
+
};
|
|
234
|
+
const agentEvent = (agent) => ({
|
|
235
|
+
type: 'workflow_agent',
|
|
236
|
+
runId,
|
|
237
|
+
state: agent.state,
|
|
238
|
+
index: agent.index,
|
|
239
|
+
label: agent.label,
|
|
240
|
+
...(agent.phaseIndex !== undefined ? { phaseIndex: agent.phaseIndex } : {}),
|
|
241
|
+
...(agent.phaseTitle !== undefined ? { phaseTitle: agent.phaseTitle } : {}),
|
|
242
|
+
...(agent.agentType !== undefined ? { agentType: agent.agentType } : {}),
|
|
243
|
+
...(agent.model !== undefined ? { model: agent.model } : {}),
|
|
244
|
+
queuedAt: agent.queuedAt,
|
|
245
|
+
...(agent.startedAt !== undefined ? { startedAt: agent.startedAt } : {}),
|
|
246
|
+
...(agent.finishedAt !== undefined ? { finishedAt: agent.finishedAt } : {}),
|
|
247
|
+
lastProgressAt: agent.lastProgressAt,
|
|
248
|
+
tokens: agent.tokens,
|
|
249
|
+
toolCalls: agent.toolCalls,
|
|
250
|
+
...(agent.lastToolName !== undefined ? { lastToolName: agent.lastToolName } : {}),
|
|
251
|
+
...(agent.lastToolSummary !== undefined ? { lastToolSummary: agent.lastToolSummary } : {}),
|
|
252
|
+
promptPreview: preview(agent.prompt, 200),
|
|
253
|
+
...(agent.resultPreview !== undefined ? { resultPreview: agent.resultPreview } : {}),
|
|
254
|
+
...(agent.error !== undefined ? { error: agent.error } : {}),
|
|
255
|
+
...(agent.skipped ? { skipped: true } : {}),
|
|
256
|
+
});
|
|
257
|
+
// FIFO admission: an agent starts when fewer than maxConcurrency run.
|
|
258
|
+
const acquire = (agent) => {
|
|
259
|
+
if (running < limits.maxConcurrency && waiters.length === 0) {
|
|
260
|
+
running++;
|
|
261
|
+
return Promise.resolve(true);
|
|
262
|
+
}
|
|
263
|
+
return new Promise((admit) => {
|
|
264
|
+
waiters.push({ agent, admit });
|
|
265
|
+
});
|
|
266
|
+
};
|
|
267
|
+
const release = () => {
|
|
268
|
+
running--;
|
|
269
|
+
while (running < limits.maxConcurrency && waiters.length > 0) {
|
|
270
|
+
const next = waiters.shift();
|
|
271
|
+
running++;
|
|
272
|
+
next.admit(true);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
const cancelQueued = (agent) => {
|
|
276
|
+
const at = waiters.findIndex((w) => w.agent === agent);
|
|
277
|
+
if (at === -1)
|
|
278
|
+
return false;
|
|
279
|
+
const [waiter] = waiters.splice(at, 1);
|
|
280
|
+
waiter?.admit(false);
|
|
281
|
+
return true;
|
|
282
|
+
};
|
|
283
|
+
const abortAgents = () => {
|
|
284
|
+
for (const waiter of waiters.splice(0))
|
|
285
|
+
waiter.admit(false);
|
|
286
|
+
for (const agent of agents.values()) {
|
|
287
|
+
if (agent.finishedAt === undefined)
|
|
288
|
+
agent.controller.abort(controller.signal.reason);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
controller.signal.addEventListener('abort', abortAgents, { once: true });
|
|
292
|
+
const onUserAbort = () => controller.abort(options.signal?.reason);
|
|
293
|
+
if (options.signal?.aborted)
|
|
294
|
+
onUserAbort();
|
|
295
|
+
else
|
|
296
|
+
options.signal?.addEventListener('abort', onUserAbort, { once: true });
|
|
297
|
+
options.onControl?.({
|
|
298
|
+
skipAgent: (index) => {
|
|
299
|
+
const agent = agents.get(index);
|
|
300
|
+
if (!agent || agent.finishedAt !== undefined)
|
|
301
|
+
return false;
|
|
302
|
+
agent.userSkipped = true;
|
|
303
|
+
if (!cancelQueued(agent))
|
|
304
|
+
agent.controller.abort(new Error('The agent was skipped.'));
|
|
305
|
+
return true;
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
const settle = (agent, outcome) => {
|
|
309
|
+
const at = now();
|
|
310
|
+
agent.state = outcome.state;
|
|
311
|
+
agent.finishedAt = at;
|
|
312
|
+
agent.lastProgressAt = at;
|
|
313
|
+
agent.skipped = outcome.skipped === true;
|
|
314
|
+
if (outcome.error !== undefined)
|
|
315
|
+
agent.error = outcome.error;
|
|
316
|
+
let value = outcome.state === 'done' && !outcome.skipped ? (outcome.value ?? null) : null;
|
|
317
|
+
if (value !== null) {
|
|
318
|
+
agent.resultPreview = preview(typeof value === 'string' ? value : JSON.stringify(value));
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
encodeCodeValue({ v: value, t: totalTokens() }, limits.maxValueBytes);
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
agent.state = 'error';
|
|
325
|
+
agent.error = `Its result is larger than ${limits.maxValueBytes} bytes and could not be given to the script.`;
|
|
326
|
+
agent.resultPreview = undefined;
|
|
327
|
+
agent.skipped = false;
|
|
328
|
+
value = null;
|
|
329
|
+
}
|
|
330
|
+
emit(agentEvent(agent));
|
|
331
|
+
journal({
|
|
332
|
+
type: 'agent_outcome',
|
|
333
|
+
at,
|
|
334
|
+
index: agent.index,
|
|
335
|
+
state: agent.state === 'error' ? 'error' : agent.skipped ? 'skipped' : 'done',
|
|
336
|
+
tokens: agent.tokens,
|
|
337
|
+
toolCalls: agent.toolCalls,
|
|
338
|
+
queuedAt: agent.queuedAt,
|
|
339
|
+
...(agent.startedAt !== undefined ? { startedAt: agent.startedAt } : {}),
|
|
340
|
+
finishedAt: at,
|
|
341
|
+
...(agent.resultPreview !== undefined ? { resultPreview: agent.resultPreview } : {}),
|
|
342
|
+
...(agent.error !== undefined ? { error: agent.error } : {}),
|
|
343
|
+
});
|
|
344
|
+
return { ok: true, value: { v: value, t: totalTokens() } };
|
|
345
|
+
};
|
|
346
|
+
const interpret = (agent, call, outcome) => {
|
|
347
|
+
agent.tokens = count(outcome.tokens);
|
|
348
|
+
agent.toolCalls = count(outcome.toolCalls);
|
|
349
|
+
if (agent.userSkipped || outcome.state === 'skipped') {
|
|
350
|
+
return settle(agent, { state: 'done', skipped: true });
|
|
351
|
+
}
|
|
352
|
+
if (controller.signal.aborted) {
|
|
353
|
+
return settle(agent, {
|
|
354
|
+
state: 'error',
|
|
355
|
+
error: 'The workflow was stopped before this agent finished.',
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
if (outcome.state === 'error') {
|
|
359
|
+
return settle(agent, { state: 'error', error: outcome.error ?? 'The agent failed.' });
|
|
360
|
+
}
|
|
361
|
+
if (call.schema === undefined) {
|
|
362
|
+
return settle(agent, { state: 'done', value: outcome.text ?? '' });
|
|
363
|
+
}
|
|
364
|
+
let candidate = outcome.data;
|
|
365
|
+
if (candidate === undefined) {
|
|
366
|
+
const parsed = parseJsonText(outcome.text ?? '');
|
|
367
|
+
if (!parsed.ok) {
|
|
368
|
+
return settle(agent, {
|
|
369
|
+
state: 'error',
|
|
370
|
+
error: 'The agent was asked for structured output but its answer was not valid JSON.',
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
candidate = parsed.value;
|
|
374
|
+
}
|
|
375
|
+
const problem = validateAgainstSchema(candidate, call.schema);
|
|
376
|
+
if (problem) {
|
|
377
|
+
return settle(agent, {
|
|
378
|
+
state: 'error',
|
|
379
|
+
error: `The agent's answer did not match the schema: ${problem}.`,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
return settle(agent, { state: 'done', value: candidate });
|
|
383
|
+
};
|
|
384
|
+
const handleAgentCall = async (call) => {
|
|
385
|
+
if (controller.signal.aborted) {
|
|
386
|
+
return { ok: false, error: fatal ?? 'The workflow was stopped.' };
|
|
387
|
+
}
|
|
388
|
+
if (requested >= limits.maxAgents) {
|
|
389
|
+
const reason = `This workflow tried to start more than ${limits.maxAgents} agents, which is the limit for one run. No agent past the limit was started.`;
|
|
390
|
+
setFatal(reason);
|
|
391
|
+
return { ok: false, error: reason };
|
|
392
|
+
}
|
|
393
|
+
const index = ++requested;
|
|
394
|
+
const phaseTitle = call.phase ?? call.cursor;
|
|
395
|
+
const phase = phaseTitle?.trim() ? announce(phaseTitle, false) : undefined;
|
|
396
|
+
const queuedAt = now();
|
|
397
|
+
const agent = {
|
|
398
|
+
index,
|
|
399
|
+
label: call.label || `Agent ${index}`,
|
|
400
|
+
prompt: call.prompt,
|
|
401
|
+
...(phase ? { phaseIndex: phase.index, phaseTitle: phase.title } : {}),
|
|
402
|
+
...(call.agentType !== undefined ? { agentType: call.agentType } : {}),
|
|
403
|
+
...(call.model !== undefined ? { model: call.model } : {}),
|
|
404
|
+
queuedAt,
|
|
405
|
+
controller: new AbortController(),
|
|
406
|
+
state: 'start',
|
|
407
|
+
lastProgressAt: queuedAt,
|
|
408
|
+
tokens: 0,
|
|
409
|
+
toolCalls: 0,
|
|
410
|
+
skipped: false,
|
|
411
|
+
userSkipped: false,
|
|
412
|
+
};
|
|
413
|
+
agents.set(index, agent);
|
|
414
|
+
emit(agentEvent(agent));
|
|
415
|
+
journal({
|
|
416
|
+
type: 'agent_request',
|
|
417
|
+
at: queuedAt,
|
|
418
|
+
index,
|
|
419
|
+
label: agent.label,
|
|
420
|
+
...(phase ? { phaseIndex: phase.index, phaseTitle: phase.title } : {}),
|
|
421
|
+
promptPreview: preview(call.prompt),
|
|
422
|
+
promptSha256: sha256(call.prompt),
|
|
423
|
+
options: {
|
|
424
|
+
...(call.agentType !== undefined ? { agentType: call.agentType } : {}),
|
|
425
|
+
...(call.model !== undefined ? { model: call.model } : {}),
|
|
426
|
+
...(call.effort !== undefined ? { effort: call.effort } : {}),
|
|
427
|
+
schema: call.schema !== undefined,
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
const admitted = await acquire(agent);
|
|
431
|
+
if (!admitted) {
|
|
432
|
+
return agent.userSkipped
|
|
433
|
+
? settle(agent, { state: 'done', skipped: true })
|
|
434
|
+
: settle(agent, {
|
|
435
|
+
state: 'error',
|
|
436
|
+
error: 'The workflow was stopped before this agent started.',
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
try {
|
|
440
|
+
if (limits.tokenBudget !== undefined && totalTokens() >= limits.tokenBudget) {
|
|
441
|
+
return settle(agent, {
|
|
442
|
+
state: 'error',
|
|
443
|
+
error: `Not started: the token budget for this workflow (${limits.tokenBudget} tokens) is spent.`,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
if (controller.signal.aborted || agent.userSkipped) {
|
|
447
|
+
return agent.userSkipped
|
|
448
|
+
? settle(agent, { state: 'done', skipped: true })
|
|
449
|
+
: settle(agent, {
|
|
450
|
+
state: 'error',
|
|
451
|
+
error: 'The workflow was stopped before this agent started.',
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
agent.startedAt = now();
|
|
455
|
+
agent.lastProgressAt = agent.startedAt;
|
|
456
|
+
emit(agentEvent(agent));
|
|
457
|
+
let outcome;
|
|
458
|
+
try {
|
|
459
|
+
outcome = await options.spawnAgent({
|
|
460
|
+
index,
|
|
461
|
+
label: agent.label,
|
|
462
|
+
prompt: call.prompt,
|
|
463
|
+
...(phase ? { phaseIndex: phase.index, phaseTitle: phase.title } : {}),
|
|
464
|
+
...(call.schema !== undefined ? { schema: call.schema } : {}),
|
|
465
|
+
...(call.agentType !== undefined ? { agentType: call.agentType } : {}),
|
|
466
|
+
...(call.model !== undefined ? { model: call.model } : {}),
|
|
467
|
+
...(call.effort !== undefined ? { effort: call.effort } : {}),
|
|
468
|
+
onProgress: (progress) => {
|
|
469
|
+
if (agent.finishedAt !== undefined || closed)
|
|
470
|
+
return;
|
|
471
|
+
if (progress.tokens !== undefined)
|
|
472
|
+
agent.tokens = count(progress.tokens);
|
|
473
|
+
if (progress.toolCalls !== undefined)
|
|
474
|
+
agent.toolCalls = count(progress.toolCalls);
|
|
475
|
+
if (progress.lastToolName !== undefined)
|
|
476
|
+
agent.lastToolName = progress.lastToolName;
|
|
477
|
+
if (progress.lastToolSummary !== undefined) {
|
|
478
|
+
agent.lastToolSummary = preview(progress.lastToolSummary, 200);
|
|
479
|
+
}
|
|
480
|
+
agent.state = 'progress';
|
|
481
|
+
agent.lastProgressAt = now();
|
|
482
|
+
emit(agentEvent(agent));
|
|
483
|
+
},
|
|
484
|
+
}, agent.controller.signal);
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
outcome = {
|
|
488
|
+
state: 'error',
|
|
489
|
+
tokens: agent.tokens,
|
|
490
|
+
toolCalls: agent.toolCalls,
|
|
491
|
+
error: error instanceof Error ? error.message : String(error),
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
return interpret(agent, call, outcome);
|
|
495
|
+
}
|
|
496
|
+
finally {
|
|
497
|
+
release();
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
const onOutput = (line) => {
|
|
501
|
+
let record;
|
|
502
|
+
try {
|
|
503
|
+
record = JSON.parse(line);
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (typeof record !== 'object' || record === null)
|
|
509
|
+
return;
|
|
510
|
+
const { k, title, message } = record;
|
|
511
|
+
if (k === 'phase' && typeof title === 'string' && title.trim()) {
|
|
512
|
+
announce(title, false);
|
|
513
|
+
scriptPhase = title.trim();
|
|
514
|
+
}
|
|
515
|
+
else if (k === 'log' && typeof message === 'string') {
|
|
516
|
+
const at = now();
|
|
517
|
+
emit({ type: 'workflow_log', runId, message, at });
|
|
518
|
+
journal({ type: 'log', at, message });
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
// --- start ---
|
|
522
|
+
if (options.journal?.writeScript) {
|
|
523
|
+
try {
|
|
524
|
+
await options.journal.writeScript(options.script, sha256(options.script));
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
journalError = error instanceof Error ? error.message : String(error);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
journal({
|
|
531
|
+
type: 'run_started',
|
|
532
|
+
at: startedAt,
|
|
533
|
+
runId,
|
|
534
|
+
scriptSha256: sha256(options.script),
|
|
535
|
+
scriptBytes: Buffer.byteLength(options.script),
|
|
536
|
+
meta,
|
|
537
|
+
args,
|
|
538
|
+
limits,
|
|
539
|
+
});
|
|
540
|
+
emit({
|
|
541
|
+
type: 'workflow_started',
|
|
542
|
+
runId,
|
|
543
|
+
name: meta.name,
|
|
544
|
+
description: meta.description,
|
|
545
|
+
phases: meta.phases,
|
|
546
|
+
at: startedAt,
|
|
547
|
+
});
|
|
548
|
+
for (const declared of meta.phases)
|
|
549
|
+
announce(declared.title, true);
|
|
550
|
+
const codeRuntime = this.options.codeRuntime ??
|
|
551
|
+
new WorkerCodeRuntime({
|
|
552
|
+
memoryLimitBytes: limits.memoryLimitBytes,
|
|
553
|
+
maxSourceBytes: limits.maxSourceBytes + SOURCE_OVERHEAD_BYTES,
|
|
554
|
+
maxValueBytes: limits.maxValueBytes,
|
|
555
|
+
maxHostCalls: limits.maxAgents + 100,
|
|
556
|
+
maxPendingHostCalls: limits.maxAgents + 100,
|
|
557
|
+
});
|
|
558
|
+
const preludeConfig = { args, tokenLimit: limits.tokenBudget ?? null };
|
|
559
|
+
const runResult = await codeRuntime.run({
|
|
560
|
+
source: `${buildPrelude(preludeConfig)}\n${body}`,
|
|
561
|
+
allowedCalls: [WORKFLOW_HOST_CALL],
|
|
562
|
+
timeoutMs: limits.maxDurationMs,
|
|
563
|
+
maxOutputBytes: MAX_LOG_OUTPUT_BYTES,
|
|
564
|
+
cpuLimits: { sliceMs: limits.cpuSliceMs, totalMs: limits.cpuTotalMs },
|
|
565
|
+
signal: controller.signal,
|
|
566
|
+
onOutput,
|
|
567
|
+
onHostCall: (request) => {
|
|
568
|
+
if (request.name !== WORKFLOW_HOST_CALL) {
|
|
569
|
+
return Promise.resolve({ ok: false, error: `Unknown host call "${request.name}".` });
|
|
570
|
+
}
|
|
571
|
+
let call;
|
|
572
|
+
try {
|
|
573
|
+
call = parseAgentCall(request.input);
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
return Promise.resolve({
|
|
577
|
+
ok: false,
|
|
578
|
+
error: error instanceof Error ? error.message : String(error),
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
const pending = handleAgentCall(call).catch((error) => ({
|
|
582
|
+
ok: false,
|
|
583
|
+
error: error instanceof Error ? error.message : String(error),
|
|
584
|
+
}));
|
|
585
|
+
inFlight.add(pending);
|
|
586
|
+
void pending.finally(() => inFlight.delete(pending));
|
|
587
|
+
return pending;
|
|
588
|
+
},
|
|
589
|
+
});
|
|
590
|
+
// --- finish ---
|
|
591
|
+
const userStopped = options.signal?.aborted === true && fatal === undefined;
|
|
592
|
+
if (!controller.signal.aborted)
|
|
593
|
+
controller.abort(new Error('The workflow finished.'));
|
|
594
|
+
options.signal?.removeEventListener('abort', onUserAbort);
|
|
595
|
+
if (inFlight.size > 0) {
|
|
596
|
+
const grace = this.options.settleGraceMs ?? 5_000;
|
|
597
|
+
let timer;
|
|
598
|
+
await Promise.race([
|
|
599
|
+
Promise.allSettled([...inFlight]),
|
|
600
|
+
new Promise((resolve) => {
|
|
601
|
+
timer = setTimeout(resolve, grace);
|
|
602
|
+
}),
|
|
603
|
+
]);
|
|
604
|
+
clearTimeout(timer);
|
|
605
|
+
}
|
|
606
|
+
let status;
|
|
607
|
+
let result;
|
|
608
|
+
let error;
|
|
609
|
+
const outcome = runResult.outcome;
|
|
610
|
+
if (fatal !== undefined) {
|
|
611
|
+
status = 'failed';
|
|
612
|
+
error = fatal;
|
|
613
|
+
}
|
|
614
|
+
else if (outcome.status === 'completed') {
|
|
615
|
+
status = 'completed';
|
|
616
|
+
result = outcome.result;
|
|
617
|
+
}
|
|
618
|
+
else if (outcome.status === 'cancelled' || userStopped) {
|
|
619
|
+
status = 'stopped';
|
|
620
|
+
}
|
|
621
|
+
else if (outcome.status === 'timed-out') {
|
|
622
|
+
status = 'failed';
|
|
623
|
+
error = `The script was stopped. It spent more than ${limits.cpuSliceMs} ms in one synchronous stretch, more than ${limits.cpuTotalMs} ms in all, or ran past the ${limits.maxDurationMs} ms limit for one run.`;
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
status = 'failed';
|
|
627
|
+
const begun = [...agents.values()].filter((a) => a.startedAt !== undefined);
|
|
628
|
+
const finished = begun.filter((a) => a.state === 'done' && !a.skipped).length;
|
|
629
|
+
const failedAgents = begun.filter((a) => a.state === 'error').length;
|
|
630
|
+
const progress = begun.length === 0
|
|
631
|
+
? 'No agent had started.'
|
|
632
|
+
: `Before it failed: ${begun.length} agent${begun.length === 1 ? '' : 's'} started, ${finished} finished, ${failedAgents} failed. A rerun starts the whole script again, so finished agents run again.`;
|
|
633
|
+
error = `${describeScriptFailure(outcome, body, preludeLineCount(preludeConfig))}\n${progress}${scriptPhase !== undefined ? ` Last phase: "${scriptPhase}".` : ''}`;
|
|
634
|
+
}
|
|
635
|
+
const all = [...agents.values()];
|
|
636
|
+
const usage = {
|
|
637
|
+
agents: all.length,
|
|
638
|
+
agentsDone: all.filter((a) => a.state === 'done' && !a.skipped).length,
|
|
639
|
+
agentsFailed: all.filter((a) => a.state === 'error').length,
|
|
640
|
+
agentsSkipped: all.filter((a) => a.skipped).length,
|
|
641
|
+
tokens: all.reduce((sum, a) => sum + a.tokens, 0),
|
|
642
|
+
toolCalls: all.reduce((sum, a) => sum + a.toolCalls, 0),
|
|
643
|
+
elapsedMs: Math.max(0, now() - startedAt),
|
|
644
|
+
};
|
|
645
|
+
const at = now();
|
|
646
|
+
emit({
|
|
647
|
+
type: 'workflow_finished',
|
|
648
|
+
runId,
|
|
649
|
+
status,
|
|
650
|
+
...(status === 'completed' && result !== undefined ? { result } : {}),
|
|
651
|
+
...(error !== undefined ? { error } : {}),
|
|
652
|
+
usage,
|
|
653
|
+
at,
|
|
654
|
+
});
|
|
655
|
+
journal({
|
|
656
|
+
type: 'run_finished',
|
|
657
|
+
at,
|
|
658
|
+
status,
|
|
659
|
+
...(status === 'completed' && result !== undefined ? { result } : {}),
|
|
660
|
+
...(error !== undefined ? { error } : {}),
|
|
661
|
+
usage,
|
|
662
|
+
});
|
|
663
|
+
await journalChain;
|
|
664
|
+
closed = true;
|
|
665
|
+
const views = all.map((a) => ({
|
|
666
|
+
...(a.phaseIndex !== undefined ? { phaseIndex: a.phaseIndex } : {}),
|
|
667
|
+
state: a.state,
|
|
668
|
+
tokens: a.tokens,
|
|
669
|
+
}));
|
|
670
|
+
return {
|
|
671
|
+
runId,
|
|
672
|
+
status,
|
|
673
|
+
...(status === 'completed' && result !== undefined ? { result } : {}),
|
|
674
|
+
...(error !== undefined ? { error } : {}),
|
|
675
|
+
usage,
|
|
676
|
+
phases: summarizeWorkflowPhases(meta.phases, phases.all(), views),
|
|
677
|
+
logsTruncated: runResult.outputTruncated,
|
|
678
|
+
...(journalError !== undefined ? { journalError } : {}),
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
//# sourceMappingURL=runtime.js.map
|