@lmthing/repl 0.0.1
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/dist/crypto-BTVPQPPV.js +45 -0
- package/dist/csv-3IL3IKAY.js +115 -0
- package/dist/date-DL6PIRCI.js +56 -0
- package/dist/db-IVNVHDFE.js +68 -0
- package/dist/env-PPLEQ47H.js +47 -0
- package/dist/fetch-TN4ZJVQW.js +118 -0
- package/dist/fs-QRXDDXB5.js +127 -0
- package/dist/image-FGNY3CMJ.js +56 -0
- package/dist/index.d.ts +1691 -0
- package/dist/index.js +3809 -0
- package/dist/json-VPTMTXR6.js +113 -0
- package/dist/path-K3VLZVTH.js +54 -0
- package/dist/shell-NAGRIUA4.js +102 -0
- package/package.json +39 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3809 @@
|
|
|
1
|
+
// src/session/session.ts
|
|
2
|
+
import { EventEmitter } from "events";
|
|
3
|
+
|
|
4
|
+
// src/session/config.ts
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
var DEFAULT_CONFIG = {
|
|
7
|
+
functionTimeout: 3e4,
|
|
8
|
+
askTimeout: 3e5,
|
|
9
|
+
sessionTimeout: 6e5,
|
|
10
|
+
maxStopCalls: 50,
|
|
11
|
+
maxAsyncTasks: 10,
|
|
12
|
+
maxTasklistReminders: 3,
|
|
13
|
+
maxTaskRetries: 3,
|
|
14
|
+
maxTasksPerTasklist: 20,
|
|
15
|
+
taskAsyncTimeout: 6e4,
|
|
16
|
+
sleepMaxSeconds: 30,
|
|
17
|
+
maxContextTokens: 1e5,
|
|
18
|
+
serializationLimits: {
|
|
19
|
+
maxStringLength: 2e3,
|
|
20
|
+
maxArrayElements: 50,
|
|
21
|
+
maxObjectKeys: 20,
|
|
22
|
+
maxDepth: 5
|
|
23
|
+
},
|
|
24
|
+
workspace: {
|
|
25
|
+
maxScopeVariables: 50,
|
|
26
|
+
maxScopeValueWidth: 50,
|
|
27
|
+
maxScopeTokens: 3e3
|
|
28
|
+
},
|
|
29
|
+
contextWindow: {
|
|
30
|
+
codeWindowLines: 200,
|
|
31
|
+
stopDecayTiers: {
|
|
32
|
+
full: 2,
|
|
33
|
+
keysOnly: 5,
|
|
34
|
+
summary: 10
|
|
35
|
+
},
|
|
36
|
+
neverTruncateInterventions: true
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
function createDefaultConfig() {
|
|
40
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
41
|
+
}
|
|
42
|
+
var sessionConfigSchema = z.object({
|
|
43
|
+
functionTimeout: z.number().positive().optional(),
|
|
44
|
+
askTimeout: z.number().positive().optional(),
|
|
45
|
+
sessionTimeout: z.number().positive().optional(),
|
|
46
|
+
maxStopCalls: z.number().int().positive().optional(),
|
|
47
|
+
maxAsyncTasks: z.number().int().positive().optional(),
|
|
48
|
+
maxTasklistReminders: z.number().int().positive().optional(),
|
|
49
|
+
maxTaskRetries: z.number().int().positive().optional(),
|
|
50
|
+
maxTasksPerTasklist: z.number().int().positive().optional(),
|
|
51
|
+
taskAsyncTimeout: z.number().int().positive().optional(),
|
|
52
|
+
sleepMaxSeconds: z.number().int().positive().optional(),
|
|
53
|
+
maxContextTokens: z.number().int().positive().optional(),
|
|
54
|
+
serializationLimits: z.object({
|
|
55
|
+
maxStringLength: z.number().int().positive().optional(),
|
|
56
|
+
maxArrayElements: z.number().int().positive().optional(),
|
|
57
|
+
maxObjectKeys: z.number().int().positive().optional(),
|
|
58
|
+
maxDepth: z.number().int().positive().optional()
|
|
59
|
+
}).optional(),
|
|
60
|
+
workspace: z.object({
|
|
61
|
+
maxScopeVariables: z.number().int().positive().optional(),
|
|
62
|
+
maxScopeValueWidth: z.number().int().positive().optional(),
|
|
63
|
+
maxScopeTokens: z.number().int().positive().optional()
|
|
64
|
+
}).optional(),
|
|
65
|
+
contextWindow: z.object({
|
|
66
|
+
codeWindowLines: z.number().int().positive().optional(),
|
|
67
|
+
stopDecayTiers: z.object({
|
|
68
|
+
full: z.number().int().nonnegative().optional(),
|
|
69
|
+
keysOnly: z.number().int().nonnegative().optional(),
|
|
70
|
+
summary: z.number().int().nonnegative().optional()
|
|
71
|
+
}).optional(),
|
|
72
|
+
neverTruncateInterventions: z.boolean().optional()
|
|
73
|
+
}).optional()
|
|
74
|
+
});
|
|
75
|
+
function validateConfig(input) {
|
|
76
|
+
const result = sessionConfigSchema.safeParse(input);
|
|
77
|
+
if (result.success) {
|
|
78
|
+
return { valid: true, config: result.data };
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
valid: false,
|
|
82
|
+
errors: result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function mergeConfig(overrides) {
|
|
86
|
+
const base = createDefaultConfig();
|
|
87
|
+
return {
|
|
88
|
+
functionTimeout: overrides.functionTimeout ?? base.functionTimeout,
|
|
89
|
+
askTimeout: overrides.askTimeout ?? base.askTimeout,
|
|
90
|
+
sessionTimeout: overrides.sessionTimeout ?? base.sessionTimeout,
|
|
91
|
+
maxStopCalls: overrides.maxStopCalls ?? base.maxStopCalls,
|
|
92
|
+
maxAsyncTasks: overrides.maxAsyncTasks ?? base.maxAsyncTasks,
|
|
93
|
+
maxTasklistReminders: overrides.maxTasklistReminders ?? base.maxTasklistReminders,
|
|
94
|
+
maxTaskRetries: overrides.maxTaskRetries ?? base.maxTaskRetries,
|
|
95
|
+
maxTasksPerTasklist: overrides.maxTasksPerTasklist ?? base.maxTasksPerTasklist,
|
|
96
|
+
taskAsyncTimeout: overrides.taskAsyncTimeout ?? base.taskAsyncTimeout,
|
|
97
|
+
sleepMaxSeconds: overrides.sleepMaxSeconds ?? base.sleepMaxSeconds,
|
|
98
|
+
maxContextTokens: overrides.maxContextTokens ?? base.maxContextTokens,
|
|
99
|
+
serializationLimits: {
|
|
100
|
+
maxStringLength: overrides.serializationLimits?.maxStringLength ?? base.serializationLimits.maxStringLength,
|
|
101
|
+
maxArrayElements: overrides.serializationLimits?.maxArrayElements ?? base.serializationLimits.maxArrayElements,
|
|
102
|
+
maxObjectKeys: overrides.serializationLimits?.maxObjectKeys ?? base.serializationLimits.maxObjectKeys,
|
|
103
|
+
maxDepth: overrides.serializationLimits?.maxDepth ?? base.serializationLimits.maxDepth
|
|
104
|
+
},
|
|
105
|
+
workspace: {
|
|
106
|
+
maxScopeVariables: overrides.workspace?.maxScopeVariables ?? base.workspace.maxScopeVariables,
|
|
107
|
+
maxScopeValueWidth: overrides.workspace?.maxScopeValueWidth ?? base.workspace.maxScopeValueWidth,
|
|
108
|
+
maxScopeTokens: overrides.workspace?.maxScopeTokens ?? base.workspace.maxScopeTokens
|
|
109
|
+
},
|
|
110
|
+
contextWindow: {
|
|
111
|
+
codeWindowLines: overrides.contextWindow?.codeWindowLines ?? base.contextWindow.codeWindowLines,
|
|
112
|
+
stopDecayTiers: {
|
|
113
|
+
full: overrides.contextWindow?.stopDecayTiers?.full ?? base.contextWindow.stopDecayTiers.full,
|
|
114
|
+
keysOnly: overrides.contextWindow?.stopDecayTiers?.keysOnly ?? base.contextWindow.stopDecayTiers.keysOnly,
|
|
115
|
+
summary: overrides.contextWindow?.stopDecayTiers?.summary ?? base.contextWindow.stopDecayTiers.summary
|
|
116
|
+
},
|
|
117
|
+
neverTruncateInterventions: overrides.contextWindow?.neverTruncateInterventions ?? base.contextWindow.neverTruncateInterventions
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/sandbox/sandbox.ts
|
|
123
|
+
import vm2 from "vm";
|
|
124
|
+
import React from "react";
|
|
125
|
+
|
|
126
|
+
// src/sandbox/executor.ts
|
|
127
|
+
import vm from "vm";
|
|
128
|
+
|
|
129
|
+
// src/sandbox/transpiler.ts
|
|
130
|
+
import ts from "typescript";
|
|
131
|
+
var compilerOptions = {
|
|
132
|
+
target: ts.ScriptTarget.ESNext,
|
|
133
|
+
module: ts.ModuleKind.ESNext,
|
|
134
|
+
jsx: ts.JsxEmit.React,
|
|
135
|
+
jsxFactory: "React.createElement",
|
|
136
|
+
jsxFragmentFactory: "React.Fragment",
|
|
137
|
+
strict: false,
|
|
138
|
+
esModuleInterop: true
|
|
139
|
+
};
|
|
140
|
+
function transpile(code) {
|
|
141
|
+
const result = ts.transpileModule(code, { compilerOptions });
|
|
142
|
+
return result.outputText;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/parser/ast-utils.ts
|
|
146
|
+
import ts2 from "typescript";
|
|
147
|
+
function parseStatement(source) {
|
|
148
|
+
const sourceFile = ts2.createSourceFile(
|
|
149
|
+
"line.ts",
|
|
150
|
+
source,
|
|
151
|
+
ts2.ScriptTarget.ESNext,
|
|
152
|
+
true,
|
|
153
|
+
ts2.ScriptKind.TSX
|
|
154
|
+
);
|
|
155
|
+
const statements = sourceFile.statements;
|
|
156
|
+
if (statements.length === 0) return null;
|
|
157
|
+
return statements[0];
|
|
158
|
+
}
|
|
159
|
+
function extractDeclarations(source) {
|
|
160
|
+
const node = parseStatement(source);
|
|
161
|
+
if (!node) return [];
|
|
162
|
+
const names = [];
|
|
163
|
+
if (ts2.isVariableStatement(node)) {
|
|
164
|
+
for (const decl of node.declarationList.declarations) {
|
|
165
|
+
extractBindingNames(decl.name, names);
|
|
166
|
+
}
|
|
167
|
+
} else if (ts2.isFunctionDeclaration(node) && node.name) {
|
|
168
|
+
names.push(node.name.text);
|
|
169
|
+
} else if (ts2.isClassDeclaration(node) && node.name) {
|
|
170
|
+
names.push(node.name.text);
|
|
171
|
+
}
|
|
172
|
+
return names;
|
|
173
|
+
}
|
|
174
|
+
function extractBindingNames(node, names) {
|
|
175
|
+
if (ts2.isIdentifier(node)) {
|
|
176
|
+
names.push(node.text);
|
|
177
|
+
} else if (ts2.isObjectBindingPattern(node)) {
|
|
178
|
+
for (const element of node.elements) {
|
|
179
|
+
extractBindingNames(element.name, names);
|
|
180
|
+
}
|
|
181
|
+
} else if (ts2.isArrayBindingPattern(node)) {
|
|
182
|
+
for (const element of node.elements) {
|
|
183
|
+
if (ts2.isBindingElement(element)) {
|
|
184
|
+
extractBindingNames(element.name, names);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function recoverArgumentNames(source) {
|
|
190
|
+
const sourceFile = ts2.createSourceFile(
|
|
191
|
+
"line.ts",
|
|
192
|
+
source,
|
|
193
|
+
ts2.ScriptTarget.ESNext,
|
|
194
|
+
true,
|
|
195
|
+
ts2.ScriptKind.TSX
|
|
196
|
+
);
|
|
197
|
+
let callExpr = null;
|
|
198
|
+
function visit(node) {
|
|
199
|
+
if (callExpr) return;
|
|
200
|
+
if (ts2.isCallExpression(node)) {
|
|
201
|
+
const callee = node.expression;
|
|
202
|
+
if (ts2.isIdentifier(callee)) {
|
|
203
|
+
const name = callee.text;
|
|
204
|
+
if (name === "stop" || name === "display" || name === "ask" || name === "async") {
|
|
205
|
+
callExpr = node;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
ts2.forEachChild(node, visit);
|
|
211
|
+
}
|
|
212
|
+
visit(sourceFile);
|
|
213
|
+
if (!callExpr) return [];
|
|
214
|
+
return callExpr.arguments.map((arg, i) => {
|
|
215
|
+
if (ts2.isIdentifier(arg)) {
|
|
216
|
+
return arg.text;
|
|
217
|
+
}
|
|
218
|
+
if (ts2.isPropertyAccessExpression(arg)) {
|
|
219
|
+
return arg.getText(sourceFile);
|
|
220
|
+
}
|
|
221
|
+
if (ts2.isElementAccessExpression(arg)) {
|
|
222
|
+
return arg.getText(sourceFile);
|
|
223
|
+
}
|
|
224
|
+
return `arg_${i}`;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
function extractVariableNames(source) {
|
|
228
|
+
const sourceFile = ts2.createSourceFile(
|
|
229
|
+
"line.ts",
|
|
230
|
+
source,
|
|
231
|
+
ts2.ScriptTarget.ESNext,
|
|
232
|
+
true,
|
|
233
|
+
ts2.ScriptKind.TSX
|
|
234
|
+
);
|
|
235
|
+
const names = /* @__PURE__ */ new Set();
|
|
236
|
+
function visit(node) {
|
|
237
|
+
if (ts2.isIdentifier(node)) {
|
|
238
|
+
const parent = node.parent;
|
|
239
|
+
if (parent && ts2.isPropertyAccessExpression(parent) && parent.name === node) {
|
|
240
|
+
} else {
|
|
241
|
+
names.add(node.text);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
ts2.forEachChild(node, visit);
|
|
245
|
+
}
|
|
246
|
+
visit(sourceFile);
|
|
247
|
+
return [...names];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/sandbox/executor.ts
|
|
251
|
+
async function executeLine(code, lineNumber, context, timeout = 3e4) {
|
|
252
|
+
try {
|
|
253
|
+
const js = transpile(code);
|
|
254
|
+
const trimmedJs = js.trim();
|
|
255
|
+
if (trimmedJs === "") {
|
|
256
|
+
return { ok: true, result: void 0 };
|
|
257
|
+
}
|
|
258
|
+
const hasAwait = trimmedJs.includes("await ");
|
|
259
|
+
if (hasAwait) {
|
|
260
|
+
const declaredNames = extractDeclarations(code);
|
|
261
|
+
const assignments = declaredNames.map((name) => `globalThis[${JSON.stringify(name)}] = typeof ${name} !== 'undefined' ? ${name} : undefined;`).join("\n");
|
|
262
|
+
const wrapped = `(async () => {
|
|
263
|
+
${trimmedJs}
|
|
264
|
+
${assignments}
|
|
265
|
+
})()`;
|
|
266
|
+
const script = new vm.Script(wrapped, { filename: `line-${lineNumber}.js` });
|
|
267
|
+
const result = await script.runInContext(context, { timeout });
|
|
268
|
+
return { ok: true, result };
|
|
269
|
+
} else {
|
|
270
|
+
const script = new vm.Script(trimmedJs, { filename: `line-${lineNumber}.js` });
|
|
271
|
+
const result = await script.runInContext(context, { timeout });
|
|
272
|
+
return { ok: true, result };
|
|
273
|
+
}
|
|
274
|
+
} catch (err) {
|
|
275
|
+
const error = err;
|
|
276
|
+
const payload = {
|
|
277
|
+
type: error.constructor?.name ?? "Error",
|
|
278
|
+
message: error.message,
|
|
279
|
+
line: lineNumber,
|
|
280
|
+
source: code.trim()
|
|
281
|
+
};
|
|
282
|
+
return { ok: false, error: payload };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/sandbox/sandbox.ts
|
|
287
|
+
var BLOCKED_GLOBALS = [
|
|
288
|
+
"process",
|
|
289
|
+
"require",
|
|
290
|
+
"module",
|
|
291
|
+
"exports",
|
|
292
|
+
"__filename",
|
|
293
|
+
"__dirname",
|
|
294
|
+
"eval",
|
|
295
|
+
"Function"
|
|
296
|
+
];
|
|
297
|
+
var Sandbox = class {
|
|
298
|
+
context;
|
|
299
|
+
declaredNames = /* @__PURE__ */ new Set();
|
|
300
|
+
lineCount = 0;
|
|
301
|
+
timeout;
|
|
302
|
+
constructor(options = {}) {
|
|
303
|
+
this.timeout = options.timeout ?? 3e4;
|
|
304
|
+
const contextGlobals = {
|
|
305
|
+
React,
|
|
306
|
+
console,
|
|
307
|
+
setTimeout,
|
|
308
|
+
clearTimeout,
|
|
309
|
+
setInterval,
|
|
310
|
+
clearInterval,
|
|
311
|
+
Promise,
|
|
312
|
+
Array,
|
|
313
|
+
Object,
|
|
314
|
+
String,
|
|
315
|
+
Number,
|
|
316
|
+
Boolean,
|
|
317
|
+
Date,
|
|
318
|
+
Math,
|
|
319
|
+
JSON,
|
|
320
|
+
Map,
|
|
321
|
+
Set,
|
|
322
|
+
WeakMap,
|
|
323
|
+
WeakSet,
|
|
324
|
+
RegExp,
|
|
325
|
+
Error,
|
|
326
|
+
TypeError,
|
|
327
|
+
RangeError,
|
|
328
|
+
SyntaxError,
|
|
329
|
+
ReferenceError,
|
|
330
|
+
Symbol,
|
|
331
|
+
Uint8Array,
|
|
332
|
+
Int32Array,
|
|
333
|
+
Float64Array,
|
|
334
|
+
ArrayBuffer,
|
|
335
|
+
DataView,
|
|
336
|
+
URL,
|
|
337
|
+
URLSearchParams,
|
|
338
|
+
TextEncoder,
|
|
339
|
+
TextDecoder,
|
|
340
|
+
structuredClone,
|
|
341
|
+
atob: globalThis.atob,
|
|
342
|
+
btoa: globalThis.btoa,
|
|
343
|
+
...options.globals
|
|
344
|
+
};
|
|
345
|
+
this.context = vm2.createContext(contextGlobals);
|
|
346
|
+
for (const name of BLOCKED_GLOBALS) {
|
|
347
|
+
Object.defineProperty(this.context, name, {
|
|
348
|
+
get() {
|
|
349
|
+
throw new Error(`${name} is not available in the sandbox`);
|
|
350
|
+
},
|
|
351
|
+
configurable: false
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Execute a line of TypeScript in the sandbox.
|
|
357
|
+
*/
|
|
358
|
+
async execute(code) {
|
|
359
|
+
this.lineCount++;
|
|
360
|
+
const declarations = extractDeclarations(code);
|
|
361
|
+
for (const name of declarations) {
|
|
362
|
+
this.declaredNames.add(name);
|
|
363
|
+
}
|
|
364
|
+
return executeLine(code, this.lineCount, this.context, this.timeout);
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Inject a value into the sandbox's global scope.
|
|
368
|
+
*/
|
|
369
|
+
inject(name, value) {
|
|
370
|
+
this.context[name] = value;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Get a value from the sandbox scope.
|
|
374
|
+
*/
|
|
375
|
+
getValue(name) {
|
|
376
|
+
return this.context[name];
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Get all user-declared variable names.
|
|
380
|
+
*/
|
|
381
|
+
getDeclaredNames() {
|
|
382
|
+
return [...this.declaredNames];
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Get the current scope as ScopeEntry[].
|
|
386
|
+
*/
|
|
387
|
+
getScope() {
|
|
388
|
+
const entries = [];
|
|
389
|
+
for (const name of this.declaredNames) {
|
|
390
|
+
try {
|
|
391
|
+
const value = this.context[name];
|
|
392
|
+
entries.push({
|
|
393
|
+
name,
|
|
394
|
+
type: describeType(value),
|
|
395
|
+
value: truncateValue(value)
|
|
396
|
+
});
|
|
397
|
+
} catch {
|
|
398
|
+
entries.push({ name, type: "unknown", value: "<error reading>" });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return entries;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Get the line count.
|
|
405
|
+
*/
|
|
406
|
+
getLineCount() {
|
|
407
|
+
return this.lineCount;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Get the raw vm.Context (for advanced use).
|
|
411
|
+
*/
|
|
412
|
+
getContext() {
|
|
413
|
+
return this.context;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Destroy the sandbox.
|
|
417
|
+
*/
|
|
418
|
+
destroy() {
|
|
419
|
+
this.declaredNames.clear();
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
function describeType(val) {
|
|
423
|
+
if (val === null) return "null";
|
|
424
|
+
if (val === void 0) return "undefined";
|
|
425
|
+
if (Array.isArray(val)) {
|
|
426
|
+
if (val.length === 0) return "Array";
|
|
427
|
+
const firstType = describeType(val[0]);
|
|
428
|
+
return `Array<${firstType}>`;
|
|
429
|
+
}
|
|
430
|
+
const t = typeof val;
|
|
431
|
+
if (t === "object") {
|
|
432
|
+
const name = val.constructor?.name;
|
|
433
|
+
return name && name !== "Object" ? name : "Object";
|
|
434
|
+
}
|
|
435
|
+
return t;
|
|
436
|
+
}
|
|
437
|
+
function truncateValue(val, maxLen = 50) {
|
|
438
|
+
if (val === null) return "null";
|
|
439
|
+
if (val === void 0) return "undefined";
|
|
440
|
+
if (typeof val === "function") return `[Function: ${val.name || "anonymous"}]`;
|
|
441
|
+
if (typeof val === "symbol") return val.toString();
|
|
442
|
+
try {
|
|
443
|
+
let str;
|
|
444
|
+
if (typeof val === "string") {
|
|
445
|
+
str = JSON.stringify(val);
|
|
446
|
+
} else if (Array.isArray(val)) {
|
|
447
|
+
const preview = val.slice(0, 3).map((v) => truncateValue(v, 20)).join(", ");
|
|
448
|
+
str = val.length > 3 ? `[${preview}, ... +${val.length - 3}]` : `[${preview}]`;
|
|
449
|
+
} else if (typeof val === "object") {
|
|
450
|
+
const keys = Object.keys(val);
|
|
451
|
+
const preview = keys.slice(0, 5).join(", ");
|
|
452
|
+
str = keys.length > 5 ? `{${preview}, ... +${keys.length - 5}}` : `{${preview}}`;
|
|
453
|
+
} else {
|
|
454
|
+
str = String(val);
|
|
455
|
+
}
|
|
456
|
+
if (str.length > maxLen) {
|
|
457
|
+
return str.slice(0, maxLen - 3) + "...";
|
|
458
|
+
}
|
|
459
|
+
return str;
|
|
460
|
+
} catch {
|
|
461
|
+
return "[value]";
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// src/stream/serializer.ts
|
|
466
|
+
var DEFAULT_LIMITS = {
|
|
467
|
+
maxStringLength: 2e3,
|
|
468
|
+
maxArrayElements: 50,
|
|
469
|
+
maxObjectKeys: 20,
|
|
470
|
+
maxDepth: 5
|
|
471
|
+
};
|
|
472
|
+
function serialize(value, limits = {}) {
|
|
473
|
+
const opts = { ...DEFAULT_LIMITS, ...limits };
|
|
474
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
475
|
+
return serializeValue(value, opts, seen, 0);
|
|
476
|
+
}
|
|
477
|
+
function serializeValue(value, limits, seen, depth) {
|
|
478
|
+
if (value === null) return "null";
|
|
479
|
+
if (value === void 0) return "undefined";
|
|
480
|
+
const type = typeof value;
|
|
481
|
+
if (type === "string") {
|
|
482
|
+
const str = value;
|
|
483
|
+
if (str.length > limits.maxStringLength) {
|
|
484
|
+
const half = Math.floor(limits.maxStringLength / 2);
|
|
485
|
+
return JSON.stringify(str.slice(0, half) + `... (truncated, ${str.length} chars total)`);
|
|
486
|
+
}
|
|
487
|
+
return JSON.stringify(str);
|
|
488
|
+
}
|
|
489
|
+
if (type === "number" || type === "boolean") {
|
|
490
|
+
return JSON.stringify(value);
|
|
491
|
+
}
|
|
492
|
+
if (type === "function") {
|
|
493
|
+
const fn = value;
|
|
494
|
+
return `[Function: ${fn.name || "anonymous"}]`;
|
|
495
|
+
}
|
|
496
|
+
if (type === "symbol") {
|
|
497
|
+
return `[Symbol: ${value.description ?? ""}]`;
|
|
498
|
+
}
|
|
499
|
+
if (type === "bigint") {
|
|
500
|
+
return `${value}n`;
|
|
501
|
+
}
|
|
502
|
+
if (value instanceof Error) {
|
|
503
|
+
return `[Error: ${value.message}]`;
|
|
504
|
+
}
|
|
505
|
+
if (value instanceof Promise) {
|
|
506
|
+
return "[Promise]";
|
|
507
|
+
}
|
|
508
|
+
if (value instanceof Date) {
|
|
509
|
+
return `"${value.toISOString()}"`;
|
|
510
|
+
}
|
|
511
|
+
if (value instanceof RegExp) {
|
|
512
|
+
return value.toString();
|
|
513
|
+
}
|
|
514
|
+
if (type === "object") {
|
|
515
|
+
const obj = value;
|
|
516
|
+
if (seen.has(obj)) return "[Circular]";
|
|
517
|
+
seen.add(obj);
|
|
518
|
+
if (depth >= limits.maxDepth) {
|
|
519
|
+
if (Array.isArray(obj)) return `[Array(${obj.length})]`;
|
|
520
|
+
return `[Object]`;
|
|
521
|
+
}
|
|
522
|
+
if (Array.isArray(obj)) {
|
|
523
|
+
return serializeArray(obj, limits, seen, depth);
|
|
524
|
+
}
|
|
525
|
+
if (obj instanceof Map) {
|
|
526
|
+
const entries = [];
|
|
527
|
+
let count = 0;
|
|
528
|
+
for (const [k, v] of obj) {
|
|
529
|
+
if (count >= limits.maxObjectKeys) {
|
|
530
|
+
entries.push(`... +${obj.size - count} more`);
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
entries.push(`${serializeValue(k, limits, seen, depth + 1)}: ${serializeValue(v, limits, seen, depth + 1)}`);
|
|
534
|
+
count++;
|
|
535
|
+
}
|
|
536
|
+
return `Map { ${entries.join(", ")} }`;
|
|
537
|
+
}
|
|
538
|
+
if (obj instanceof Set) {
|
|
539
|
+
const items = [];
|
|
540
|
+
let count = 0;
|
|
541
|
+
for (const v of obj) {
|
|
542
|
+
if (count >= limits.maxArrayElements) {
|
|
543
|
+
items.push(`... +${obj.size - count} more`);
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
items.push(serializeValue(v, limits, seen, depth + 1));
|
|
547
|
+
count++;
|
|
548
|
+
}
|
|
549
|
+
return `Set { ${items.join(", ")} }`;
|
|
550
|
+
}
|
|
551
|
+
return serializeObject(obj, limits, seen, depth);
|
|
552
|
+
}
|
|
553
|
+
return String(value);
|
|
554
|
+
}
|
|
555
|
+
function serializeArray(arr, limits, seen, depth) {
|
|
556
|
+
if (arr.length === 0) return "[]";
|
|
557
|
+
const items = [];
|
|
558
|
+
const max = Math.min(arr.length, limits.maxArrayElements);
|
|
559
|
+
for (let i = 0; i < max; i++) {
|
|
560
|
+
items.push(serializeValue(arr[i], limits, seen, depth + 1));
|
|
561
|
+
}
|
|
562
|
+
if (arr.length > limits.maxArrayElements) {
|
|
563
|
+
items.push(`... +${arr.length - limits.maxArrayElements} more`);
|
|
564
|
+
}
|
|
565
|
+
return `[${items.join(", ")}]`;
|
|
566
|
+
}
|
|
567
|
+
function serializeObject(obj, limits, seen, depth) {
|
|
568
|
+
const keys = Object.keys(obj);
|
|
569
|
+
if (keys.length === 0) return "{}";
|
|
570
|
+
const entries = [];
|
|
571
|
+
const max = Math.min(keys.length, limits.maxObjectKeys);
|
|
572
|
+
for (let i = 0; i < max; i++) {
|
|
573
|
+
const key = keys[i];
|
|
574
|
+
const val = obj[key];
|
|
575
|
+
entries.push(`${JSON.stringify(key)}: ${serializeValue(val, limits, seen, depth + 1)}`);
|
|
576
|
+
}
|
|
577
|
+
if (keys.length > limits.maxObjectKeys) {
|
|
578
|
+
entries.push(`... +${keys.length - limits.maxObjectKeys} more`);
|
|
579
|
+
}
|
|
580
|
+
return `{ ${entries.join(", ")} }`;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// src/context/knowledge-decay.ts
|
|
584
|
+
var KNOWLEDGE_TAG = /* @__PURE__ */ Symbol.for("lmthing:knowledge");
|
|
585
|
+
var DEFAULT_TIERS = {
|
|
586
|
+
full: 0,
|
|
587
|
+
truncated: 2,
|
|
588
|
+
headers: 4
|
|
589
|
+
};
|
|
590
|
+
function getKnowledgeDecayLevel(distance, tiers = DEFAULT_TIERS) {
|
|
591
|
+
if (distance <= tiers.full) return "full";
|
|
592
|
+
if (distance <= tiers.truncated) return "truncated";
|
|
593
|
+
if (distance <= tiers.headers) return "headers";
|
|
594
|
+
return "names";
|
|
595
|
+
}
|
|
596
|
+
function isKnowledgeContent(value) {
|
|
597
|
+
return value !== null && typeof value === "object" && value[KNOWLEDGE_TAG] === true;
|
|
598
|
+
}
|
|
599
|
+
function tagAsKnowledge(obj) {
|
|
600
|
+
Object.defineProperty(obj, KNOWLEDGE_TAG, {
|
|
601
|
+
value: true,
|
|
602
|
+
enumerable: false,
|
|
603
|
+
configurable: false
|
|
604
|
+
});
|
|
605
|
+
return obj;
|
|
606
|
+
}
|
|
607
|
+
function decayKnowledgeValue(content, distance, tiers = DEFAULT_TIERS) {
|
|
608
|
+
const level = getKnowledgeDecayLevel(distance, tiers);
|
|
609
|
+
switch (level) {
|
|
610
|
+
case "full":
|
|
611
|
+
return formatFull(content);
|
|
612
|
+
case "truncated":
|
|
613
|
+
return formatTruncated(content);
|
|
614
|
+
case "headers":
|
|
615
|
+
return formatHeaders(content);
|
|
616
|
+
case "names":
|
|
617
|
+
return formatNames(content);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function formatFull(content) {
|
|
621
|
+
return serializeNested(content, (md) => JSON.stringify(md));
|
|
622
|
+
}
|
|
623
|
+
function formatTruncated(content) {
|
|
624
|
+
return serializeNested(content, (md) => {
|
|
625
|
+
if (md.length <= 300) return JSON.stringify(md);
|
|
626
|
+
return JSON.stringify(md.slice(0, 300) + `...(truncated, ${md.length} chars)`);
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
function formatHeaders(content) {
|
|
630
|
+
return serializeNested(content, (md) => {
|
|
631
|
+
const headers = md.split("\n").filter((line) => /^#{1,4}\s/.test(line)).join(" | ");
|
|
632
|
+
return JSON.stringify(headers || "(no headings)");
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
function formatNames(content) {
|
|
636
|
+
const paths = [];
|
|
637
|
+
collectPaths(content, [], paths);
|
|
638
|
+
return `[knowledge: ${paths.join(", ")}]`;
|
|
639
|
+
}
|
|
640
|
+
function collectPaths(obj, prefix, out) {
|
|
641
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
642
|
+
if (typeof value === "string") {
|
|
643
|
+
out.push([...prefix, key].join("/"));
|
|
644
|
+
} else if (typeof value === "object" && value !== null) {
|
|
645
|
+
collectPaths(value, [...prefix, key], out);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
function serializeNested(content, formatLeaf) {
|
|
650
|
+
return serializeLevel(content, formatLeaf);
|
|
651
|
+
}
|
|
652
|
+
function serializeLevel(obj, formatLeaf) {
|
|
653
|
+
const entries = [];
|
|
654
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
655
|
+
if (typeof value === "string") {
|
|
656
|
+
entries.push(`${JSON.stringify(key)}: ${formatLeaf(value)}`);
|
|
657
|
+
} else if (typeof value === "object" && value !== null) {
|
|
658
|
+
entries.push(`${JSON.stringify(key)}: ${serializeLevel(value, formatLeaf)}`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return `{ ${entries.join(", ")} }`;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// src/sandbox/globals.ts
|
|
665
|
+
function createGlobals(config) {
|
|
666
|
+
const {
|
|
667
|
+
pauseController,
|
|
668
|
+
renderSurface,
|
|
669
|
+
asyncManager,
|
|
670
|
+
askTimeout = 3e5
|
|
671
|
+
} = config;
|
|
672
|
+
const tasklistsState = {
|
|
673
|
+
tasklists: /* @__PURE__ */ new Map()
|
|
674
|
+
};
|
|
675
|
+
let currentSource = "";
|
|
676
|
+
function setCurrentSource(source) {
|
|
677
|
+
currentSource = source;
|
|
678
|
+
}
|
|
679
|
+
let stopResolve = null;
|
|
680
|
+
async function stopFn(...values) {
|
|
681
|
+
const argNames = recoverArgumentNames(currentSource);
|
|
682
|
+
const resolved = await Promise.allSettled(
|
|
683
|
+
values.map((v) => v instanceof Promise ? v : Promise.resolve(v))
|
|
684
|
+
);
|
|
685
|
+
const payload = {};
|
|
686
|
+
for (let i = 0; i < resolved.length; i++) {
|
|
687
|
+
const key = argNames[i] ?? `arg_${i}`;
|
|
688
|
+
const settlement = resolved[i];
|
|
689
|
+
const value = settlement.status === "fulfilled" ? settlement.value : {
|
|
690
|
+
_error: settlement.reason instanceof Error ? settlement.reason.message : String(settlement.reason)
|
|
691
|
+
};
|
|
692
|
+
payload[key] = {
|
|
693
|
+
value,
|
|
694
|
+
display: serialize(value, config.serializationLimits)
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
const asyncPayload = asyncManager.buildStopPayload();
|
|
698
|
+
for (const [key, val] of Object.entries(asyncPayload)) {
|
|
699
|
+
payload[key] = {
|
|
700
|
+
value: val,
|
|
701
|
+
display: serialize(val, config.serializationLimits)
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
const promise = new Promise((resolve) => {
|
|
705
|
+
stopResolve = resolve;
|
|
706
|
+
});
|
|
707
|
+
pauseController.pause();
|
|
708
|
+
config.onStop?.(payload, currentSource);
|
|
709
|
+
return promise;
|
|
710
|
+
}
|
|
711
|
+
function resolveStop() {
|
|
712
|
+
if (stopResolve) {
|
|
713
|
+
const resolve = stopResolve;
|
|
714
|
+
stopResolve = null;
|
|
715
|
+
resolve();
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
function displayFn(element) {
|
|
719
|
+
const id = `display_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
720
|
+
renderSurface.append(id, element);
|
|
721
|
+
config.onDisplay?.(id);
|
|
722
|
+
}
|
|
723
|
+
async function askFn(element) {
|
|
724
|
+
const formId = `form_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
725
|
+
pauseController.pause();
|
|
726
|
+
try {
|
|
727
|
+
const result = await Promise.race([
|
|
728
|
+
renderSurface.renderForm(formId, element),
|
|
729
|
+
new Promise(
|
|
730
|
+
(resolve) => setTimeout(() => resolve({ _timeout: true }), askTimeout)
|
|
731
|
+
)
|
|
732
|
+
]);
|
|
733
|
+
return result;
|
|
734
|
+
} finally {
|
|
735
|
+
pauseController.resume();
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
function asyncFn(fn, label) {
|
|
739
|
+
const derivedLabel = label ?? deriveLabel(currentSource);
|
|
740
|
+
const taskId = asyncManager.register(
|
|
741
|
+
(signal) => fn(),
|
|
742
|
+
derivedLabel
|
|
743
|
+
);
|
|
744
|
+
config.onAsyncStart?.(taskId, derivedLabel);
|
|
745
|
+
}
|
|
746
|
+
function tasklistFn(tasklistId, description, tasks) {
|
|
747
|
+
if (tasklistsState.tasklists.has(tasklistId)) {
|
|
748
|
+
throw new Error(`tasklist() tasklist "${tasklistId}" already declared`);
|
|
749
|
+
}
|
|
750
|
+
if (!tasklistId) {
|
|
751
|
+
throw new Error("tasklist() requires a tasklistId");
|
|
752
|
+
}
|
|
753
|
+
if (!description || !Array.isArray(tasks) || tasks.length === 0) {
|
|
754
|
+
throw new Error("tasklist() requires a description and at least one task");
|
|
755
|
+
}
|
|
756
|
+
const maxTasks = config.maxTasksPerTasklist ?? 20;
|
|
757
|
+
if (tasks.length > maxTasks) {
|
|
758
|
+
throw new Error(`tasklist() exceeds maximum of ${maxTasks} tasks per tasklist`);
|
|
759
|
+
}
|
|
760
|
+
const ids = /* @__PURE__ */ new Set();
|
|
761
|
+
for (const task of tasks) {
|
|
762
|
+
if (!task.id || !task.instructions || !task.outputSchema) {
|
|
763
|
+
throw new Error("Each task must have id, instructions, and outputSchema");
|
|
764
|
+
}
|
|
765
|
+
if (ids.has(task.id)) {
|
|
766
|
+
throw new Error(`Duplicate task id: ${task.id}`);
|
|
767
|
+
}
|
|
768
|
+
ids.add(task.id);
|
|
769
|
+
}
|
|
770
|
+
for (const task of tasks) {
|
|
771
|
+
if (task.dependsOn) {
|
|
772
|
+
for (const dep of task.dependsOn) {
|
|
773
|
+
if (!ids.has(dep)) {
|
|
774
|
+
throw new Error(`Task "${task.id}" depends on unknown task "${dep}" in tasklist "${tasklistId}"`);
|
|
775
|
+
}
|
|
776
|
+
if (dep === task.id) {
|
|
777
|
+
throw new Error(`Task "${task.id}" cannot depend on itself`);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
const hasDependsOn = tasks.some((t) => t.dependsOn && t.dependsOn.length > 0);
|
|
783
|
+
if (!hasDependsOn) {
|
|
784
|
+
for (let i = 1; i < tasks.length; i++) {
|
|
785
|
+
tasks[i] = { ...tasks[i], dependsOn: [tasks[i - 1].id] };
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
const visited = /* @__PURE__ */ new Set();
|
|
789
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
790
|
+
const taskMap = new Map(tasks.map((t) => [t.id, t]));
|
|
791
|
+
function visit(id) {
|
|
792
|
+
if (visited.has(id)) return;
|
|
793
|
+
if (visiting.has(id)) {
|
|
794
|
+
throw new Error(`Cycle detected in tasklist "${tasklistId}" involving task "${id}"`);
|
|
795
|
+
}
|
|
796
|
+
visiting.add(id);
|
|
797
|
+
const task = taskMap.get(id);
|
|
798
|
+
if (task.dependsOn) {
|
|
799
|
+
for (const dep of task.dependsOn) {
|
|
800
|
+
visit(dep);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
visiting.delete(id);
|
|
804
|
+
visited.add(id);
|
|
805
|
+
}
|
|
806
|
+
for (const task of tasks) {
|
|
807
|
+
visit(task.id);
|
|
808
|
+
}
|
|
809
|
+
const readyTasks = /* @__PURE__ */ new Set();
|
|
810
|
+
for (const task of tasks) {
|
|
811
|
+
if (!task.dependsOn || task.dependsOn.length === 0) {
|
|
812
|
+
readyTasks.add(task.id);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
const plan = { tasklistId, description, tasks };
|
|
816
|
+
const tasklistState = {
|
|
817
|
+
plan,
|
|
818
|
+
completed: /* @__PURE__ */ new Map(),
|
|
819
|
+
readyTasks,
|
|
820
|
+
runningTasks: /* @__PURE__ */ new Set(),
|
|
821
|
+
outputs: /* @__PURE__ */ new Map(),
|
|
822
|
+
progressMessages: /* @__PURE__ */ new Map(),
|
|
823
|
+
retryCount: /* @__PURE__ */ new Map()
|
|
824
|
+
};
|
|
825
|
+
tasklistsState.tasklists.set(tasklistId, tasklistState);
|
|
826
|
+
renderSurface.appendTasklistProgress?.(tasklistId, tasklistState);
|
|
827
|
+
config.onTasklistDeclared?.(tasklistId, plan);
|
|
828
|
+
}
|
|
829
|
+
function completeTaskFn(tasklistId, id, output) {
|
|
830
|
+
const tasklist = tasklistsState.tasklists.get(tasklistId);
|
|
831
|
+
if (!tasklist) {
|
|
832
|
+
throw new Error(`completeTask() called with unknown tasklist "${tasklistId}" \u2014 declare it with tasklist() first`);
|
|
833
|
+
}
|
|
834
|
+
const task = tasklist.plan.tasks.find((t) => t.id === id);
|
|
835
|
+
if (!task) {
|
|
836
|
+
throw new Error(`Unknown task id: ${id} in tasklist "${tasklistId}"`);
|
|
837
|
+
}
|
|
838
|
+
if (tasklist.completed.has(id)) {
|
|
839
|
+
throw new Error(`Task "${id}" in tasklist "${tasklistId}" already completed`);
|
|
840
|
+
}
|
|
841
|
+
if (!tasklist.readyTasks.has(id)) {
|
|
842
|
+
const isRunning = tasklist.runningTasks.has(id);
|
|
843
|
+
if (isRunning) {
|
|
844
|
+
throw new Error(`Task "${id}" in tasklist "${tasklistId}" is already running via completeTaskAsync()`);
|
|
845
|
+
}
|
|
846
|
+
const pendingDeps = (task.dependsOn ?? []).filter((dep) => {
|
|
847
|
+
const c = tasklist.completed.get(dep);
|
|
848
|
+
return !c || c.status !== "completed";
|
|
849
|
+
});
|
|
850
|
+
const readyTaskDetails = [...tasklist.readyTasks].map((readyId) => {
|
|
851
|
+
const readyTask = tasklist.plan.tasks.find((t) => t.id === readyId);
|
|
852
|
+
return { id: readyId, instructions: readyTask.instructions, outputSchema: readyTask.outputSchema };
|
|
853
|
+
});
|
|
854
|
+
config.onTaskOrderViolation?.(tasklistId, id, readyTaskDetails);
|
|
855
|
+
throw new Error(
|
|
856
|
+
`Task "${id}" in tasklist "${tasklistId}" is not ready. Waiting on: ${pendingDeps.join(", ")}`
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
for (const [key, schema] of Object.entries(task.outputSchema)) {
|
|
860
|
+
if (!(key in output)) {
|
|
861
|
+
throw new Error(`Task "${id}" output missing required key: ${key}`);
|
|
862
|
+
}
|
|
863
|
+
const expectedType = schema.type;
|
|
864
|
+
const value = output[key];
|
|
865
|
+
const actual = Array.isArray(value) ? "array" : typeof value;
|
|
866
|
+
if (actual !== expectedType) {
|
|
867
|
+
throw new Error(
|
|
868
|
+
`Task "${id}" output key "${key}": expected ${expectedType}, got ${actual}`
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
tasklist.completed.set(id, {
|
|
873
|
+
output,
|
|
874
|
+
timestamp: Date.now(),
|
|
875
|
+
status: "completed"
|
|
876
|
+
});
|
|
877
|
+
tasklist.readyTasks.delete(id);
|
|
878
|
+
tasklist.outputs.set(id, output);
|
|
879
|
+
recomputeReadyTasks(tasklist);
|
|
880
|
+
renderSurface.updateTasklistProgress?.(tasklistId, tasklist);
|
|
881
|
+
config.onTaskComplete?.(tasklistId, id, output);
|
|
882
|
+
const hasRemainingTasks = tasklist.plan.tasks.some((t) => {
|
|
883
|
+
const c = tasklist.completed.get(t.id);
|
|
884
|
+
return (!c || c.status !== "completed" && c.status !== "skipped") && !t.optional;
|
|
885
|
+
});
|
|
886
|
+
if (hasRemainingTasks && tasklist.readyTasks.size > 0) {
|
|
887
|
+
const readyTaskDetails = [...tasklist.readyTasks].map((readyId) => {
|
|
888
|
+
const readyTask = tasklist.plan.tasks.find((t) => t.id === readyId);
|
|
889
|
+
return { id: readyId, instructions: readyTask.instructions, outputSchema: readyTask.outputSchema };
|
|
890
|
+
});
|
|
891
|
+
config.onTaskCompleteContinue?.(tasklistId, id, readyTaskDetails);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
function recomputeReadyTasks(tasklist) {
|
|
895
|
+
for (const task of tasklist.plan.tasks) {
|
|
896
|
+
if (tasklist.completed.has(task.id) || tasklist.readyTasks.has(task.id) || tasklist.runningTasks.has(task.id)) {
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
const deps = task.dependsOn ?? [];
|
|
900
|
+
const allDepsSatisfied = deps.every((dep) => {
|
|
901
|
+
const c = tasklist.completed.get(dep);
|
|
902
|
+
return c && (c.status === "completed" || c.status === "skipped" || c.status === "failed" && tasklist.plan.tasks.find((t) => t.id === dep)?.optional);
|
|
903
|
+
});
|
|
904
|
+
if (allDepsSatisfied) {
|
|
905
|
+
if (task.condition) {
|
|
906
|
+
const conditionMet = evaluateCondition(task.condition, tasklist.outputs);
|
|
907
|
+
if (!conditionMet) {
|
|
908
|
+
tasklist.completed.set(task.id, {
|
|
909
|
+
output: {},
|
|
910
|
+
timestamp: Date.now(),
|
|
911
|
+
status: "skipped"
|
|
912
|
+
});
|
|
913
|
+
config.onTaskSkipped?.(tasklist.plan.tasklistId, task.id, "condition was falsy");
|
|
914
|
+
recomputeReadyTasks(tasklist);
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
tasklist.readyTasks.add(task.id);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
function evaluateCondition(condition, outputs) {
|
|
923
|
+
try {
|
|
924
|
+
const ctx = Object.fromEntries(outputs);
|
|
925
|
+
const paramNames = Object.keys(ctx);
|
|
926
|
+
const paramValues = Object.values(ctx);
|
|
927
|
+
const fn = new Function(...paramNames, `return !!(${condition})`);
|
|
928
|
+
return fn(...paramValues);
|
|
929
|
+
} catch {
|
|
930
|
+
return false;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
function completeTaskAsyncFn(tasklistId, taskId, fn) {
|
|
934
|
+
const tasklist = tasklistsState.tasklists.get(tasklistId);
|
|
935
|
+
if (!tasklist) {
|
|
936
|
+
throw new Error(`completeTaskAsync() called with unknown tasklist "${tasklistId}"`);
|
|
937
|
+
}
|
|
938
|
+
const task = tasklist.plan.tasks.find((t) => t.id === taskId);
|
|
939
|
+
if (!task) {
|
|
940
|
+
throw new Error(`Unknown task id: ${taskId} in tasklist "${tasklistId}"`);
|
|
941
|
+
}
|
|
942
|
+
if (tasklist.completed.has(taskId)) {
|
|
943
|
+
throw new Error(`Task "${taskId}" in tasklist "${tasklistId}" already completed`);
|
|
944
|
+
}
|
|
945
|
+
if (!tasklist.readyTasks.has(taskId)) {
|
|
946
|
+
const readyTaskDetails = [...tasklist.readyTasks].map((readyId) => {
|
|
947
|
+
const readyTask = tasklist.plan.tasks.find((t) => t.id === readyId);
|
|
948
|
+
return { id: readyId, instructions: readyTask.instructions, outputSchema: readyTask.outputSchema };
|
|
949
|
+
});
|
|
950
|
+
config.onTaskOrderViolation?.(tasklistId, taskId, readyTaskDetails);
|
|
951
|
+
throw new Error(`Task "${taskId}" in tasklist "${tasklistId}" is not ready`);
|
|
952
|
+
}
|
|
953
|
+
tasklist.readyTasks.delete(taskId);
|
|
954
|
+
tasklist.runningTasks.add(taskId);
|
|
955
|
+
config.onTaskAsyncStart?.(tasklistId, taskId);
|
|
956
|
+
const startTime = Date.now();
|
|
957
|
+
const promise = fn().then((output) => {
|
|
958
|
+
for (const [key, schema] of Object.entries(task.outputSchema)) {
|
|
959
|
+
if (!(key in output)) {
|
|
960
|
+
throw new Error(`Task "${taskId}" output missing required key: ${key}`);
|
|
961
|
+
}
|
|
962
|
+
const expectedType = schema.type;
|
|
963
|
+
const value = output[key];
|
|
964
|
+
const actual = Array.isArray(value) ? "array" : typeof value;
|
|
965
|
+
if (actual !== expectedType) {
|
|
966
|
+
throw new Error(
|
|
967
|
+
`Task "${taskId}" output key "${key}": expected ${expectedType}, got ${actual}`
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
tasklist.runningTasks.delete(taskId);
|
|
972
|
+
tasklist.completed.set(taskId, {
|
|
973
|
+
output,
|
|
974
|
+
timestamp: Date.now(),
|
|
975
|
+
status: "completed",
|
|
976
|
+
duration: Date.now() - startTime
|
|
977
|
+
});
|
|
978
|
+
tasklist.outputs.set(taskId, output);
|
|
979
|
+
asyncManager.setResult(`task:${taskId}`, output);
|
|
980
|
+
recomputeReadyTasks(tasklist);
|
|
981
|
+
renderSurface.updateTasklistProgress?.(tasklistId, tasklist);
|
|
982
|
+
config.onTaskAsyncComplete?.(tasklistId, taskId, output);
|
|
983
|
+
}).catch((err) => {
|
|
984
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
985
|
+
tasklist.runningTasks.delete(taskId);
|
|
986
|
+
tasklist.completed.set(taskId, {
|
|
987
|
+
output: {},
|
|
988
|
+
timestamp: Date.now(),
|
|
989
|
+
status: "failed",
|
|
990
|
+
error,
|
|
991
|
+
duration: Date.now() - startTime
|
|
992
|
+
});
|
|
993
|
+
asyncManager.setResult(`task:${taskId}`, { error });
|
|
994
|
+
if (task.optional) {
|
|
995
|
+
recomputeReadyTasks(tasklist);
|
|
996
|
+
}
|
|
997
|
+
renderSurface.updateTasklistProgress?.(tasklistId, tasklist);
|
|
998
|
+
config.onTaskAsyncFailed?.(tasklistId, taskId, error);
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
function taskProgressFn(tasklistId, taskId, message, percent) {
|
|
1002
|
+
const tasklist = tasklistsState.tasklists.get(tasklistId);
|
|
1003
|
+
if (!tasklist) {
|
|
1004
|
+
throw new Error(`taskProgress() called with unknown tasklist "${tasklistId}"`);
|
|
1005
|
+
}
|
|
1006
|
+
const task = tasklist.plan.tasks.find((t) => t.id === taskId);
|
|
1007
|
+
if (!task) {
|
|
1008
|
+
throw new Error(`Unknown task id: ${taskId} in tasklist "${tasklistId}"`);
|
|
1009
|
+
}
|
|
1010
|
+
if (!tasklist.readyTasks.has(taskId) && !tasklist.runningTasks.has(taskId)) {
|
|
1011
|
+
throw new Error(`Task "${taskId}" in tasklist "${tasklistId}" is not in ready or running state`);
|
|
1012
|
+
}
|
|
1013
|
+
tasklist.progressMessages.set(taskId, { message, percent });
|
|
1014
|
+
renderSurface.updateTaskProgress?.(tasklistId, taskId, message, percent);
|
|
1015
|
+
config.onTaskProgress?.(tasklistId, taskId, message, percent);
|
|
1016
|
+
}
|
|
1017
|
+
function failTaskFn(tasklistId, taskId, error) {
|
|
1018
|
+
const tasklist = tasklistsState.tasklists.get(tasklistId);
|
|
1019
|
+
if (!tasklist) {
|
|
1020
|
+
throw new Error(`failTask() called with unknown tasklist "${tasklistId}"`);
|
|
1021
|
+
}
|
|
1022
|
+
const task = tasklist.plan.tasks.find((t) => t.id === taskId);
|
|
1023
|
+
if (!task) {
|
|
1024
|
+
throw new Error(`Unknown task id: ${taskId} in tasklist "${tasklistId}"`);
|
|
1025
|
+
}
|
|
1026
|
+
if (!tasklist.readyTasks.has(taskId) && !tasklist.runningTasks.has(taskId)) {
|
|
1027
|
+
throw new Error(`Task "${taskId}" in tasklist "${tasklistId}" is not in ready or running state`);
|
|
1028
|
+
}
|
|
1029
|
+
tasklist.readyTasks.delete(taskId);
|
|
1030
|
+
tasklist.runningTasks.delete(taskId);
|
|
1031
|
+
tasklist.completed.set(taskId, {
|
|
1032
|
+
output: {},
|
|
1033
|
+
timestamp: Date.now(),
|
|
1034
|
+
status: "failed",
|
|
1035
|
+
error
|
|
1036
|
+
});
|
|
1037
|
+
if (task.optional) {
|
|
1038
|
+
recomputeReadyTasks(tasklist);
|
|
1039
|
+
}
|
|
1040
|
+
renderSurface.updateTasklistProgress?.(tasklistId, tasklist);
|
|
1041
|
+
config.onTaskFailed?.(tasklistId, taskId, error);
|
|
1042
|
+
}
|
|
1043
|
+
function retryTaskFn(tasklistId, taskId) {
|
|
1044
|
+
const tasklist = tasklistsState.tasklists.get(tasklistId);
|
|
1045
|
+
if (!tasklist) {
|
|
1046
|
+
throw new Error(`retryTask() called with unknown tasklist "${tasklistId}"`);
|
|
1047
|
+
}
|
|
1048
|
+
const task = tasklist.plan.tasks.find((t) => t.id === taskId);
|
|
1049
|
+
if (!task) {
|
|
1050
|
+
throw new Error(`Unknown task id: ${taskId} in tasklist "${tasklistId}"`);
|
|
1051
|
+
}
|
|
1052
|
+
const completion = tasklist.completed.get(taskId);
|
|
1053
|
+
if (!completion || completion.status !== "failed") {
|
|
1054
|
+
throw new Error(`retryTask() can only retry failed tasks. Task "${taskId}" status: ${completion?.status ?? "not completed"}`);
|
|
1055
|
+
}
|
|
1056
|
+
const maxRetries = config.maxTaskRetries ?? 3;
|
|
1057
|
+
const currentRetries = tasklist.retryCount.get(taskId) ?? 0;
|
|
1058
|
+
if (currentRetries >= maxRetries) {
|
|
1059
|
+
throw new Error(`Task "${taskId}" has exceeded maximum retries (${maxRetries})`);
|
|
1060
|
+
}
|
|
1061
|
+
tasklist.retryCount.set(taskId, currentRetries + 1);
|
|
1062
|
+
tasklist.completed.delete(taskId);
|
|
1063
|
+
tasklist.outputs.delete(taskId);
|
|
1064
|
+
tasklist.readyTasks.add(taskId);
|
|
1065
|
+
tasklist.progressMessages.delete(taskId);
|
|
1066
|
+
renderSurface.updateTasklistProgress?.(tasklistId, tasklist);
|
|
1067
|
+
config.onTaskRetried?.(tasklistId, taskId);
|
|
1068
|
+
}
|
|
1069
|
+
async function sleepFn(seconds) {
|
|
1070
|
+
const maxSeconds = config.sleepMaxSeconds ?? 30;
|
|
1071
|
+
const capped = Math.min(Math.max(0, seconds), maxSeconds);
|
|
1072
|
+
await new Promise((resolve) => setTimeout(resolve, capped * 1e3));
|
|
1073
|
+
}
|
|
1074
|
+
function loadKnowledgeFn(selector) {
|
|
1075
|
+
if (!selector || typeof selector !== "object") {
|
|
1076
|
+
throw new Error("loadKnowledge() requires a selector object: { spaceName: { domain: { field: { option: true } } } }");
|
|
1077
|
+
}
|
|
1078
|
+
if (!config.onLoadKnowledge) {
|
|
1079
|
+
throw new Error("loadKnowledge() is not available \u2014 no space loaded");
|
|
1080
|
+
}
|
|
1081
|
+
return tagAsKnowledge(config.onLoadKnowledge(selector));
|
|
1082
|
+
}
|
|
1083
|
+
const loadedClasses = /* @__PURE__ */ new Set();
|
|
1084
|
+
function loadClassFn(className) {
|
|
1085
|
+
if (typeof className !== "string" || !className) {
|
|
1086
|
+
throw new Error("loadClass() requires a class name string");
|
|
1087
|
+
}
|
|
1088
|
+
if (loadedClasses.has(className)) return;
|
|
1089
|
+
if (!config.getClassInfo) {
|
|
1090
|
+
throw new Error("loadClass() is not available \u2014 no classes exported");
|
|
1091
|
+
}
|
|
1092
|
+
const result = config.getClassInfo(className);
|
|
1093
|
+
if (!result) {
|
|
1094
|
+
throw new Error(`Unknown class: "${className}"`);
|
|
1095
|
+
}
|
|
1096
|
+
loadedClasses.add(className);
|
|
1097
|
+
config.onLoadClass?.(className);
|
|
1098
|
+
}
|
|
1099
|
+
async function askParentFn(message, schema = {}) {
|
|
1100
|
+
if (typeof message !== "string" || !message) {
|
|
1101
|
+
throw new Error("askParent() requires a message string as first argument");
|
|
1102
|
+
}
|
|
1103
|
+
if (config.isFireAndForget || !config.onAskParent) {
|
|
1104
|
+
return { _noParent: true };
|
|
1105
|
+
}
|
|
1106
|
+
pauseController.pause();
|
|
1107
|
+
try {
|
|
1108
|
+
const result = await Promise.race([
|
|
1109
|
+
config.onAskParent({ message, schema }),
|
|
1110
|
+
new Promise(
|
|
1111
|
+
(resolve) => setTimeout(() => resolve({ _timeout: true }), askTimeout)
|
|
1112
|
+
)
|
|
1113
|
+
]);
|
|
1114
|
+
return result;
|
|
1115
|
+
} finally {
|
|
1116
|
+
pauseController.resume();
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
function respondFn(promise, data) {
|
|
1120
|
+
if (!config.onRespond) throw new Error("respond() is not available");
|
|
1121
|
+
if (!data || typeof data !== "object") {
|
|
1122
|
+
throw new Error("respond() requires a data object as second argument");
|
|
1123
|
+
}
|
|
1124
|
+
config.onRespond(promise, data);
|
|
1125
|
+
}
|
|
1126
|
+
return {
|
|
1127
|
+
stop: stopFn,
|
|
1128
|
+
display: displayFn,
|
|
1129
|
+
ask: askFn,
|
|
1130
|
+
async: asyncFn,
|
|
1131
|
+
tasklist: tasklistFn,
|
|
1132
|
+
completeTask: completeTaskFn,
|
|
1133
|
+
completeTaskAsync: completeTaskAsyncFn,
|
|
1134
|
+
taskProgress: taskProgressFn,
|
|
1135
|
+
failTask: failTaskFn,
|
|
1136
|
+
retryTask: retryTaskFn,
|
|
1137
|
+
sleep: sleepFn,
|
|
1138
|
+
loadKnowledge: loadKnowledgeFn,
|
|
1139
|
+
loadClass: loadClassFn,
|
|
1140
|
+
askParent: askParentFn,
|
|
1141
|
+
respond: respondFn,
|
|
1142
|
+
setCurrentSource,
|
|
1143
|
+
resolveStop,
|
|
1144
|
+
getTasklistsState: () => tasklistsState
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
function deriveLabel(source) {
|
|
1148
|
+
const commentMatch = source.match(/\/\/\s*(.+)$/);
|
|
1149
|
+
if (commentMatch) return commentMatch[1].trim();
|
|
1150
|
+
const callMatch = source.match(/=>\s*(?:\{[^}]*)?(\w+)\s*\(/);
|
|
1151
|
+
if (callMatch) return callMatch[1];
|
|
1152
|
+
return "background task";
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// src/sandbox/async-manager.ts
|
|
1156
|
+
var AsyncManager = class {
|
|
1157
|
+
tasks = /* @__PURE__ */ new Map();
|
|
1158
|
+
results = /* @__PURE__ */ new Map();
|
|
1159
|
+
counter = 0;
|
|
1160
|
+
maxTasks;
|
|
1161
|
+
constructor(maxTasks = 10) {
|
|
1162
|
+
this.maxTasks = maxTasks;
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Register a new background task.
|
|
1166
|
+
*/
|
|
1167
|
+
register(fn, label) {
|
|
1168
|
+
if (this.getRunningCount() >= this.maxTasks) {
|
|
1169
|
+
throw new Error(`Maximum async tasks reached (${this.maxTasks})`);
|
|
1170
|
+
}
|
|
1171
|
+
const id = `async_${this.counter++}`;
|
|
1172
|
+
const abortController = new AbortController();
|
|
1173
|
+
const startTime = Date.now();
|
|
1174
|
+
const promise = fn(abortController.signal).then(() => {
|
|
1175
|
+
const task = this.tasks.get(id);
|
|
1176
|
+
if (task && task.status === "running") {
|
|
1177
|
+
task.status = "completed";
|
|
1178
|
+
}
|
|
1179
|
+
}).catch((err) => {
|
|
1180
|
+
const task = this.tasks.get(id);
|
|
1181
|
+
if (task) {
|
|
1182
|
+
if (task.status === "running") {
|
|
1183
|
+
task.status = "failed";
|
|
1184
|
+
task.error = err instanceof Error ? err.message : String(err);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
this.tasks.set(id, {
|
|
1189
|
+
id,
|
|
1190
|
+
label: label ?? id,
|
|
1191
|
+
abortController,
|
|
1192
|
+
promise,
|
|
1193
|
+
status: "running",
|
|
1194
|
+
startTime
|
|
1195
|
+
});
|
|
1196
|
+
return id;
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Cancel a task by ID.
|
|
1200
|
+
*/
|
|
1201
|
+
cancel(taskId, message = "cancelled by user") {
|
|
1202
|
+
const task = this.tasks.get(taskId);
|
|
1203
|
+
if (!task || task.status !== "running") return false;
|
|
1204
|
+
task.abortController.abort(message);
|
|
1205
|
+
task.status = "cancelled";
|
|
1206
|
+
this.results.set(taskId, { cancelled: true, message });
|
|
1207
|
+
return true;
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Store a result from a task's scoped stop() call.
|
|
1211
|
+
*/
|
|
1212
|
+
setResult(taskId, value) {
|
|
1213
|
+
this.results.set(taskId, value);
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Drain all accumulated results and clear the results map.
|
|
1217
|
+
*/
|
|
1218
|
+
drainResults() {
|
|
1219
|
+
const drained = new Map(this.results);
|
|
1220
|
+
this.results.clear();
|
|
1221
|
+
return drained;
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Get a task by ID.
|
|
1225
|
+
*/
|
|
1226
|
+
getTask(taskId) {
|
|
1227
|
+
return this.tasks.get(taskId);
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Get all tasks.
|
|
1231
|
+
*/
|
|
1232
|
+
getAllTasks() {
|
|
1233
|
+
return [...this.tasks.values()];
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Get count of currently running tasks.
|
|
1237
|
+
*/
|
|
1238
|
+
getRunningCount() {
|
|
1239
|
+
return [...this.tasks.values()].filter((t) => t.status === "running").length;
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Build the async portion of a stop payload.
|
|
1243
|
+
* Running tasks show "pending", completed ones show their results.
|
|
1244
|
+
*/
|
|
1245
|
+
buildStopPayload() {
|
|
1246
|
+
const payload = {};
|
|
1247
|
+
const drained = this.drainResults();
|
|
1248
|
+
for (const [taskId, task] of this.tasks) {
|
|
1249
|
+
if (drained.has(taskId)) {
|
|
1250
|
+
payload[task.label] = drained.get(taskId);
|
|
1251
|
+
} else if (task.status === "running") {
|
|
1252
|
+
payload[task.label] = "pending";
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return payload;
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Wait for all running tasks to complete, with timeout.
|
|
1259
|
+
*/
|
|
1260
|
+
async drain(timeoutMs = 5e3) {
|
|
1261
|
+
const running = [...this.tasks.values()].filter((t) => t.status === "running");
|
|
1262
|
+
if (running.length === 0) return;
|
|
1263
|
+
await Promise.race([
|
|
1264
|
+
Promise.allSettled(running.map((t) => t.promise)),
|
|
1265
|
+
new Promise((resolve) => setTimeout(resolve, timeoutMs))
|
|
1266
|
+
]);
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Cancel all running tasks.
|
|
1270
|
+
*/
|
|
1271
|
+
cancelAll() {
|
|
1272
|
+
for (const task of this.tasks.values()) {
|
|
1273
|
+
if (task.status === "running") {
|
|
1274
|
+
task.abortController.abort("session cleanup");
|
|
1275
|
+
task.status = "cancelled";
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
|
|
1281
|
+
// src/stream/bracket-tracker.ts
|
|
1282
|
+
function createBracketState() {
|
|
1283
|
+
return {
|
|
1284
|
+
round: 0,
|
|
1285
|
+
curly: 0,
|
|
1286
|
+
square: 0,
|
|
1287
|
+
inString: false,
|
|
1288
|
+
inLineComment: false,
|
|
1289
|
+
inBlockComment: false,
|
|
1290
|
+
templateDepth: 0,
|
|
1291
|
+
jsxDepth: 0,
|
|
1292
|
+
jsxTagState: "none"
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
function feedChunk(state, chunk) {
|
|
1296
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
1297
|
+
const ch = chunk[i];
|
|
1298
|
+
const next = chunk[i + 1];
|
|
1299
|
+
if (state.inLineComment) {
|
|
1300
|
+
if (ch === "\n") state.inLineComment = false;
|
|
1301
|
+
continue;
|
|
1302
|
+
}
|
|
1303
|
+
if (state.inBlockComment) {
|
|
1304
|
+
if (ch === "*" && next === "/") {
|
|
1305
|
+
state.inBlockComment = false;
|
|
1306
|
+
i++;
|
|
1307
|
+
}
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
if (state.inString) {
|
|
1311
|
+
if (ch === "\\") {
|
|
1312
|
+
i++;
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
if (state.inString === "`") {
|
|
1316
|
+
if (ch === "`") {
|
|
1317
|
+
state.inString = false;
|
|
1318
|
+
}
|
|
1319
|
+
} else if (ch === state.inString) {
|
|
1320
|
+
state.inString = false;
|
|
1321
|
+
}
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
if (ch === "/" && next === "/") {
|
|
1325
|
+
state.inLineComment = true;
|
|
1326
|
+
i++;
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
if (ch === "/" && next === "*") {
|
|
1330
|
+
state.inBlockComment = true;
|
|
1331
|
+
i++;
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
1335
|
+
state.inString = ch;
|
|
1336
|
+
continue;
|
|
1337
|
+
}
|
|
1338
|
+
if (state.jsxTagState === "pending_open") {
|
|
1339
|
+
if (/[a-zA-Z]/.test(ch)) {
|
|
1340
|
+
state.jsxDepth++;
|
|
1341
|
+
state.jsxTagState = "open";
|
|
1342
|
+
continue;
|
|
1343
|
+
} else if (ch === "/") {
|
|
1344
|
+
state.jsxTagState = "close";
|
|
1345
|
+
continue;
|
|
1346
|
+
} else if (ch === ">") {
|
|
1347
|
+
state.jsxDepth++;
|
|
1348
|
+
state.jsxTagState = "none";
|
|
1349
|
+
continue;
|
|
1350
|
+
} else {
|
|
1351
|
+
state.jsxTagState = "none";
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
if (state.jsxTagState === "selfclose_pending") {
|
|
1355
|
+
if (ch === ">") {
|
|
1356
|
+
state.jsxDepth = Math.max(0, state.jsxDepth - 1);
|
|
1357
|
+
state.jsxTagState = "none";
|
|
1358
|
+
continue;
|
|
1359
|
+
}
|
|
1360
|
+
state.jsxTagState = "open";
|
|
1361
|
+
}
|
|
1362
|
+
if (state.jsxTagState === "open") {
|
|
1363
|
+
if (ch === "/") {
|
|
1364
|
+
state.jsxTagState = "selfclose_pending";
|
|
1365
|
+
continue;
|
|
1366
|
+
}
|
|
1367
|
+
if (ch === ">" && state.curly === 0 && state.round === 0 && state.square === 0) {
|
|
1368
|
+
state.jsxTagState = "none";
|
|
1369
|
+
continue;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
if (state.jsxTagState === "close") {
|
|
1373
|
+
if (ch === ">") {
|
|
1374
|
+
state.jsxDepth = Math.max(0, state.jsxDepth - 1);
|
|
1375
|
+
state.jsxTagState = "none";
|
|
1376
|
+
}
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
if (ch === "<" && state.jsxTagState === "none") {
|
|
1380
|
+
state.jsxTagState = "pending_open";
|
|
1381
|
+
continue;
|
|
1382
|
+
}
|
|
1383
|
+
if (ch === "(") state.round++;
|
|
1384
|
+
else if (ch === ")") state.round = Math.max(0, state.round - 1);
|
|
1385
|
+
else if (ch === "{") state.curly++;
|
|
1386
|
+
else if (ch === "}") state.curly = Math.max(0, state.curly - 1);
|
|
1387
|
+
else if (ch === "[") state.square++;
|
|
1388
|
+
else if (ch === "]") state.square = Math.max(0, state.square - 1);
|
|
1389
|
+
}
|
|
1390
|
+
return state;
|
|
1391
|
+
}
|
|
1392
|
+
function isBalanced(state) {
|
|
1393
|
+
return state.round === 0 && state.curly === 0 && state.square === 0 && state.jsxDepth === 0 && state.jsxTagState === "none" && state.inString === false && !state.inBlockComment;
|
|
1394
|
+
}
|
|
1395
|
+
function resetBracketState(state) {
|
|
1396
|
+
state.round = 0;
|
|
1397
|
+
state.curly = 0;
|
|
1398
|
+
state.square = 0;
|
|
1399
|
+
state.inString = false;
|
|
1400
|
+
state.inLineComment = false;
|
|
1401
|
+
state.inBlockComment = false;
|
|
1402
|
+
state.templateDepth = 0;
|
|
1403
|
+
state.jsxDepth = 0;
|
|
1404
|
+
state.jsxTagState = "none";
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// src/parser/statement-detector.ts
|
|
1408
|
+
function isCompleteStatement(buffer) {
|
|
1409
|
+
const trimmed = buffer.trim();
|
|
1410
|
+
if (trimmed.length === 0) return false;
|
|
1411
|
+
let roundDepth = 0;
|
|
1412
|
+
let curlyDepth = 0;
|
|
1413
|
+
let squareDepth = 0;
|
|
1414
|
+
let jsxDepth = 0;
|
|
1415
|
+
let jsxTagState = "none";
|
|
1416
|
+
let inString = false;
|
|
1417
|
+
let inLineComment = false;
|
|
1418
|
+
let inBlockComment = false;
|
|
1419
|
+
let i = 0;
|
|
1420
|
+
while (i < trimmed.length) {
|
|
1421
|
+
const ch = trimmed[i];
|
|
1422
|
+
const next = trimmed[i + 1];
|
|
1423
|
+
if (inLineComment) {
|
|
1424
|
+
if (ch === "\n") inLineComment = false;
|
|
1425
|
+
i++;
|
|
1426
|
+
continue;
|
|
1427
|
+
}
|
|
1428
|
+
if (inBlockComment) {
|
|
1429
|
+
if (ch === "*" && next === "/") {
|
|
1430
|
+
inBlockComment = false;
|
|
1431
|
+
i += 2;
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
i++;
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
if (inString) {
|
|
1438
|
+
if (ch === "\\") {
|
|
1439
|
+
i += 2;
|
|
1440
|
+
continue;
|
|
1441
|
+
}
|
|
1442
|
+
if (inString === "`") {
|
|
1443
|
+
if (ch === "$" && next === "{") {
|
|
1444
|
+
}
|
|
1445
|
+
if (ch === "`") {
|
|
1446
|
+
inString = false;
|
|
1447
|
+
}
|
|
1448
|
+
} else if (ch === inString) {
|
|
1449
|
+
inString = false;
|
|
1450
|
+
}
|
|
1451
|
+
i++;
|
|
1452
|
+
continue;
|
|
1453
|
+
}
|
|
1454
|
+
if (ch === "/" && next === "/") {
|
|
1455
|
+
inLineComment = true;
|
|
1456
|
+
i += 2;
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
if (ch === "/" && next === "*") {
|
|
1460
|
+
inBlockComment = true;
|
|
1461
|
+
i += 2;
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
1465
|
+
inString = ch;
|
|
1466
|
+
i++;
|
|
1467
|
+
continue;
|
|
1468
|
+
}
|
|
1469
|
+
if (jsxTagState === "open" && ch === "/" && next === ">") {
|
|
1470
|
+
jsxDepth = Math.max(0, jsxDepth - 1);
|
|
1471
|
+
jsxTagState = "none";
|
|
1472
|
+
i += 2;
|
|
1473
|
+
continue;
|
|
1474
|
+
}
|
|
1475
|
+
if (jsxTagState === "open" && ch === ">" && curlyDepth === 0 && roundDepth === 0 && squareDepth === 0) {
|
|
1476
|
+
jsxTagState = "none";
|
|
1477
|
+
i++;
|
|
1478
|
+
continue;
|
|
1479
|
+
}
|
|
1480
|
+
if (jsxTagState === "close" && ch === ">") {
|
|
1481
|
+
jsxDepth = Math.max(0, jsxDepth - 1);
|
|
1482
|
+
jsxTagState = "none";
|
|
1483
|
+
i++;
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
if (ch === "<" && jsxTagState === "none" && next && /[a-zA-Z]/.test(next)) {
|
|
1487
|
+
jsxDepth++;
|
|
1488
|
+
jsxTagState = "open";
|
|
1489
|
+
i += 2;
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
if (ch === "<" && jsxTagState === "none" && next === "/") {
|
|
1493
|
+
jsxTagState = "close";
|
|
1494
|
+
i += 2;
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
if (ch === "<" && jsxTagState === "none" && next === ">") {
|
|
1498
|
+
jsxDepth++;
|
|
1499
|
+
i += 2;
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
if (ch === "(") roundDepth++;
|
|
1503
|
+
else if (ch === ")") roundDepth = Math.max(0, roundDepth - 1);
|
|
1504
|
+
else if (ch === "{") curlyDepth++;
|
|
1505
|
+
else if (ch === "}") curlyDepth = Math.max(0, curlyDepth - 1);
|
|
1506
|
+
else if (ch === "[") squareDepth++;
|
|
1507
|
+
else if (ch === "]") squareDepth = Math.max(0, squareDepth - 1);
|
|
1508
|
+
i++;
|
|
1509
|
+
}
|
|
1510
|
+
if (inString !== false || inBlockComment) return false;
|
|
1511
|
+
if (roundDepth !== 0 || curlyDepth !== 0 || squareDepth !== 0) return false;
|
|
1512
|
+
if (jsxDepth !== 0 || jsxTagState !== "none") return false;
|
|
1513
|
+
return true;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// src/stream/line-accumulator.ts
|
|
1517
|
+
function createLineAccumulator() {
|
|
1518
|
+
return {
|
|
1519
|
+
buffer: "",
|
|
1520
|
+
bracketState: createBracketState()
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
function feed(acc, token) {
|
|
1524
|
+
const statements = [];
|
|
1525
|
+
for (const char of token) {
|
|
1526
|
+
acc.buffer += char;
|
|
1527
|
+
feedChunk(acc.bracketState, char);
|
|
1528
|
+
if (char === "\n" && isBalanced(acc.bracketState)) {
|
|
1529
|
+
const trimmed = acc.buffer.trim();
|
|
1530
|
+
if (trimmed.length > 0 && isCompleteStatement(trimmed)) {
|
|
1531
|
+
statements.push(trimmed);
|
|
1532
|
+
acc.buffer = "";
|
|
1533
|
+
resetBracketState(acc.bracketState);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
return {
|
|
1538
|
+
statements,
|
|
1539
|
+
hasRemaining: acc.buffer.trim().length > 0
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
function flush(acc) {
|
|
1543
|
+
const trimmed = acc.buffer.trim();
|
|
1544
|
+
if (trimmed.length === 0) return null;
|
|
1545
|
+
acc.buffer = "";
|
|
1546
|
+
resetBracketState(acc.bracketState);
|
|
1547
|
+
return trimmed;
|
|
1548
|
+
}
|
|
1549
|
+
function clear(acc) {
|
|
1550
|
+
acc.buffer = "";
|
|
1551
|
+
resetBracketState(acc.bracketState);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// src/hooks/hook-executor.ts
|
|
1555
|
+
import ts4 from "typescript";
|
|
1556
|
+
|
|
1557
|
+
// src/hooks/pattern-matcher.ts
|
|
1558
|
+
import ts3 from "typescript";
|
|
1559
|
+
function matchPattern(node, pattern, sourceFile) {
|
|
1560
|
+
const captures = {};
|
|
1561
|
+
if ("oneOf" in pattern) {
|
|
1562
|
+
const p = pattern;
|
|
1563
|
+
for (const sub of p.oneOf) {
|
|
1564
|
+
const match = matchPattern(node, sub, sourceFile);
|
|
1565
|
+
if (match) return match;
|
|
1566
|
+
}
|
|
1567
|
+
return null;
|
|
1568
|
+
}
|
|
1569
|
+
if ("not" in pattern && "type" in pattern) {
|
|
1570
|
+
const p = pattern;
|
|
1571
|
+
if (!matchNodeType(node, p.type)) return null;
|
|
1572
|
+
const negMatch = matchPatternProperties(node, p.not, sourceFile, captures);
|
|
1573
|
+
if (negMatch) return null;
|
|
1574
|
+
return { node, source: node.getText(sourceFile), captures };
|
|
1575
|
+
}
|
|
1576
|
+
if ("type" in pattern) {
|
|
1577
|
+
const p = pattern;
|
|
1578
|
+
if (!matchNodeType(node, p.type)) return null;
|
|
1579
|
+
if (!matchPatternProperties(node, p, sourceFile, captures)) return null;
|
|
1580
|
+
return { node, source: node.getText(sourceFile), captures };
|
|
1581
|
+
}
|
|
1582
|
+
return null;
|
|
1583
|
+
}
|
|
1584
|
+
function matchNodeType(node, type) {
|
|
1585
|
+
if (type === "*") return true;
|
|
1586
|
+
const syntaxKind = ts3.SyntaxKind[node.kind];
|
|
1587
|
+
return String(syntaxKind) === type;
|
|
1588
|
+
}
|
|
1589
|
+
function matchPatternProperties(node, pattern, sourceFile, captures) {
|
|
1590
|
+
for (const [key, expectedValue] of Object.entries(pattern)) {
|
|
1591
|
+
if (key === "type" || key === "oneOf" || key === "not") continue;
|
|
1592
|
+
const actualValue = node[key];
|
|
1593
|
+
if (actualValue === void 0) return false;
|
|
1594
|
+
if (typeof expectedValue === "object" && expectedValue !== null) {
|
|
1595
|
+
if (actualValue && typeof actualValue === "object" && "kind" in actualValue) {
|
|
1596
|
+
if (!matchPatternProperties(actualValue, expectedValue, sourceFile, captures)) {
|
|
1597
|
+
return false;
|
|
1598
|
+
}
|
|
1599
|
+
} else {
|
|
1600
|
+
return false;
|
|
1601
|
+
}
|
|
1602
|
+
} else if (typeof expectedValue === "string") {
|
|
1603
|
+
if (expectedValue.startsWith("$")) {
|
|
1604
|
+
const captureName = expectedValue.slice(1);
|
|
1605
|
+
if (actualValue && typeof actualValue === "object" && "kind" in actualValue) {
|
|
1606
|
+
captures[captureName] = actualValue.getText(sourceFile);
|
|
1607
|
+
} else {
|
|
1608
|
+
captures[captureName] = actualValue;
|
|
1609
|
+
}
|
|
1610
|
+
} else {
|
|
1611
|
+
if (actualValue && typeof actualValue === "object" && "kind" in actualValue) {
|
|
1612
|
+
if (actualValue.getText(sourceFile) !== expectedValue) return false;
|
|
1613
|
+
} else if (String(actualValue) !== expectedValue) {
|
|
1614
|
+
return false;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
} else {
|
|
1618
|
+
if (actualValue !== expectedValue) return false;
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
return true;
|
|
1622
|
+
}
|
|
1623
|
+
function findMatches(sourceFile, pattern) {
|
|
1624
|
+
const matches = [];
|
|
1625
|
+
function visit(node) {
|
|
1626
|
+
const match = matchPattern(node, pattern, sourceFile);
|
|
1627
|
+
if (match) matches.push(match);
|
|
1628
|
+
ts3.forEachChild(node, visit);
|
|
1629
|
+
}
|
|
1630
|
+
visit(sourceFile);
|
|
1631
|
+
return matches;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
// src/hooks/hook-executor.ts
|
|
1635
|
+
async function executeHooks(source, phase, registry, context) {
|
|
1636
|
+
const hooks = registry.listByPhase(phase);
|
|
1637
|
+
const result = {
|
|
1638
|
+
action: "execute",
|
|
1639
|
+
source,
|
|
1640
|
+
sideEffects: [],
|
|
1641
|
+
matchedHooks: []
|
|
1642
|
+
};
|
|
1643
|
+
if (hooks.length === 0) return result;
|
|
1644
|
+
const sourceFile = ts4.createSourceFile(
|
|
1645
|
+
"hook.ts",
|
|
1646
|
+
source,
|
|
1647
|
+
ts4.ScriptTarget.ESNext,
|
|
1648
|
+
true,
|
|
1649
|
+
ts4.ScriptKind.TSX
|
|
1650
|
+
);
|
|
1651
|
+
for (const hook of hooks) {
|
|
1652
|
+
const matches = findMatches(sourceFile, hook.pattern);
|
|
1653
|
+
if (matches.length === 0) continue;
|
|
1654
|
+
for (const match of matches) {
|
|
1655
|
+
let action;
|
|
1656
|
+
try {
|
|
1657
|
+
action = await hook.handler(match, context);
|
|
1658
|
+
registry.recordSuccess(hook.id);
|
|
1659
|
+
} catch (err) {
|
|
1660
|
+
registry.recordFailure(hook.id);
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
result.matchedHooks.push({ hookId: hook.id, action: action.type });
|
|
1664
|
+
switch (action.type) {
|
|
1665
|
+
case "continue":
|
|
1666
|
+
break;
|
|
1667
|
+
case "side_effect":
|
|
1668
|
+
result.sideEffects.push(action.fn);
|
|
1669
|
+
break;
|
|
1670
|
+
case "transform":
|
|
1671
|
+
if (phase === "before") {
|
|
1672
|
+
result.source = action.newSource;
|
|
1673
|
+
}
|
|
1674
|
+
break;
|
|
1675
|
+
case "interrupt":
|
|
1676
|
+
if (phase === "before") {
|
|
1677
|
+
result.action = "interrupt";
|
|
1678
|
+
result.interruptMessage = action.message;
|
|
1679
|
+
return result;
|
|
1680
|
+
}
|
|
1681
|
+
break;
|
|
1682
|
+
case "skip":
|
|
1683
|
+
if (phase === "before") {
|
|
1684
|
+
result.action = "skip";
|
|
1685
|
+
return result;
|
|
1686
|
+
}
|
|
1687
|
+
break;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
return result;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// src/stream/stream-controller.ts
|
|
1695
|
+
var StreamController = class {
|
|
1696
|
+
accumulator = createLineAccumulator();
|
|
1697
|
+
paused = false;
|
|
1698
|
+
pauseResolve = null;
|
|
1699
|
+
options;
|
|
1700
|
+
lineCount = 0;
|
|
1701
|
+
currentBlockId = "";
|
|
1702
|
+
constructor(options) {
|
|
1703
|
+
this.options = options;
|
|
1704
|
+
}
|
|
1705
|
+
/**
|
|
1706
|
+
* Feed tokens from the LLM stream.
|
|
1707
|
+
*/
|
|
1708
|
+
async feedToken(token) {
|
|
1709
|
+
if (this.paused) {
|
|
1710
|
+
await this.waitForResume();
|
|
1711
|
+
}
|
|
1712
|
+
const { statements } = feed(this.accumulator, token);
|
|
1713
|
+
if (token.length > 0) {
|
|
1714
|
+
this.options.onEvent({
|
|
1715
|
+
type: "code",
|
|
1716
|
+
lines: token,
|
|
1717
|
+
blockId: this.currentBlockId || this.newBlockId()
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
for (const statement of statements) {
|
|
1721
|
+
await this.processStatement(statement);
|
|
1722
|
+
if (this.paused) {
|
|
1723
|
+
await this.waitForResume();
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Called when the LLM stream ends. Flush remaining buffer.
|
|
1729
|
+
*/
|
|
1730
|
+
async finalize() {
|
|
1731
|
+
const remaining = flush(this.accumulator);
|
|
1732
|
+
if (remaining) {
|
|
1733
|
+
await this.processStatement(remaining);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
async processStatement(source) {
|
|
1737
|
+
this.lineCount++;
|
|
1738
|
+
const ctx = this.options.hookContext();
|
|
1739
|
+
const hookResult = await executeHooks(
|
|
1740
|
+
source,
|
|
1741
|
+
"before",
|
|
1742
|
+
this.options.hookRegistry,
|
|
1743
|
+
ctx
|
|
1744
|
+
);
|
|
1745
|
+
for (const fn of hookResult.sideEffects) {
|
|
1746
|
+
try {
|
|
1747
|
+
await fn();
|
|
1748
|
+
} catch {
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
for (const match of hookResult.matchedHooks) {
|
|
1752
|
+
this.options.onEvent({
|
|
1753
|
+
type: "hook",
|
|
1754
|
+
hookId: match.hookId,
|
|
1755
|
+
action: match.action,
|
|
1756
|
+
detail: source,
|
|
1757
|
+
blockId: this.currentBlockId
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
if (hookResult.action === "skip") {
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
if (hookResult.action === "interrupt") {
|
|
1764
|
+
this.options.onEvent({
|
|
1765
|
+
type: "hook",
|
|
1766
|
+
hookId: hookResult.matchedHooks[hookResult.matchedHooks.length - 1]?.hookId ?? "unknown",
|
|
1767
|
+
action: "interrupt",
|
|
1768
|
+
detail: hookResult.interruptMessage ?? "",
|
|
1769
|
+
blockId: this.currentBlockId
|
|
1770
|
+
});
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
const finalSource = hookResult.source;
|
|
1774
|
+
this.options.onCodeLine(finalSource);
|
|
1775
|
+
const result = await this.options.onStatement(finalSource);
|
|
1776
|
+
await executeHooks(finalSource, "after", this.options.hookRegistry, ctx);
|
|
1777
|
+
if (!result.ok && result.error) {
|
|
1778
|
+
this.options.onError(result.error);
|
|
1779
|
+
this.options.onEvent({
|
|
1780
|
+
type: "error",
|
|
1781
|
+
error: result.error,
|
|
1782
|
+
blockId: this.currentBlockId
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
// ── StreamPauseController interface ──
|
|
1787
|
+
pause() {
|
|
1788
|
+
this.paused = true;
|
|
1789
|
+
}
|
|
1790
|
+
resume() {
|
|
1791
|
+
this.paused = false;
|
|
1792
|
+
if (this.pauseResolve) {
|
|
1793
|
+
const resolve = this.pauseResolve;
|
|
1794
|
+
this.pauseResolve = null;
|
|
1795
|
+
resolve();
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
isPaused() {
|
|
1799
|
+
return this.paused;
|
|
1800
|
+
}
|
|
1801
|
+
waitForResume() {
|
|
1802
|
+
if (!this.paused) return Promise.resolve();
|
|
1803
|
+
return new Promise((resolve) => {
|
|
1804
|
+
this.pauseResolve = resolve;
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Clear the line accumulator (e.g., on intervention).
|
|
1809
|
+
*/
|
|
1810
|
+
clearBuffer() {
|
|
1811
|
+
clear(this.accumulator);
|
|
1812
|
+
}
|
|
1813
|
+
/**
|
|
1814
|
+
* Set the current block ID for events.
|
|
1815
|
+
*/
|
|
1816
|
+
setBlockId(id) {
|
|
1817
|
+
this.currentBlockId = id;
|
|
1818
|
+
}
|
|
1819
|
+
newBlockId() {
|
|
1820
|
+
this.currentBlockId = `block_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
1821
|
+
return this.currentBlockId;
|
|
1822
|
+
}
|
|
1823
|
+
};
|
|
1824
|
+
|
|
1825
|
+
// src/hooks/hook-registry.ts
|
|
1826
|
+
var HookRegistry = class {
|
|
1827
|
+
hooks = /* @__PURE__ */ new Map();
|
|
1828
|
+
failureCounts = /* @__PURE__ */ new Map();
|
|
1829
|
+
disabledHooks = /* @__PURE__ */ new Set();
|
|
1830
|
+
maxConsecutiveFailures;
|
|
1831
|
+
constructor(maxConsecutiveFailures = 3) {
|
|
1832
|
+
this.maxConsecutiveFailures = maxConsecutiveFailures;
|
|
1833
|
+
}
|
|
1834
|
+
register(hook) {
|
|
1835
|
+
this.hooks.set(hook.id, hook);
|
|
1836
|
+
this.failureCounts.set(hook.id, 0);
|
|
1837
|
+
}
|
|
1838
|
+
unregister(id) {
|
|
1839
|
+
this.failureCounts.delete(id);
|
|
1840
|
+
this.disabledHooks.delete(id);
|
|
1841
|
+
return this.hooks.delete(id);
|
|
1842
|
+
}
|
|
1843
|
+
get(id) {
|
|
1844
|
+
return this.hooks.get(id);
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* List hooks by phase, excluding disabled hooks.
|
|
1848
|
+
*/
|
|
1849
|
+
listByPhase(phase) {
|
|
1850
|
+
return [...this.hooks.values()].filter(
|
|
1851
|
+
(h) => h.phase === phase && !this.disabledHooks.has(h.id)
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
/**
|
|
1855
|
+
* Record a failure for a hook. After maxConsecutiveFailures, disable it.
|
|
1856
|
+
*/
|
|
1857
|
+
recordFailure(id) {
|
|
1858
|
+
const count = (this.failureCounts.get(id) ?? 0) + 1;
|
|
1859
|
+
this.failureCounts.set(id, count);
|
|
1860
|
+
if (count >= this.maxConsecutiveFailures) {
|
|
1861
|
+
this.disabledHooks.add(id);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
/**
|
|
1865
|
+
* Record a success for a hook (resets failure count).
|
|
1866
|
+
*/
|
|
1867
|
+
recordSuccess(id) {
|
|
1868
|
+
this.failureCounts.set(id, 0);
|
|
1869
|
+
}
|
|
1870
|
+
isDisabled(id) {
|
|
1871
|
+
return this.disabledHooks.has(id);
|
|
1872
|
+
}
|
|
1873
|
+
getAll() {
|
|
1874
|
+
return [...this.hooks.values()];
|
|
1875
|
+
}
|
|
1876
|
+
clear() {
|
|
1877
|
+
this.hooks.clear();
|
|
1878
|
+
this.failureCounts.clear();
|
|
1879
|
+
this.disabledHooks.clear();
|
|
1880
|
+
}
|
|
1881
|
+
};
|
|
1882
|
+
|
|
1883
|
+
// src/context/scope-generator.ts
|
|
1884
|
+
function generateScopeTable(entries, options = {}) {
|
|
1885
|
+
const { maxVariables = 50, maxValueWidth = 50 } = options;
|
|
1886
|
+
const visible = entries.slice(0, maxVariables);
|
|
1887
|
+
if (visible.length === 0) {
|
|
1888
|
+
return "(no variables declared)";
|
|
1889
|
+
}
|
|
1890
|
+
const nameCol = Math.max(4, ...visible.map((e) => e.name.length));
|
|
1891
|
+
const typeCol = Math.max(4, ...visible.map((e) => e.type.length));
|
|
1892
|
+
const header = `${"Name".padEnd(nameCol)} ${"Type".padEnd(typeCol)} Value`;
|
|
1893
|
+
const separator = `${"-".repeat(nameCol)} ${"-".repeat(typeCol)} ${"-".repeat(maxValueWidth)}`;
|
|
1894
|
+
const rows = visible.map((e) => {
|
|
1895
|
+
const truncatedValue = e.value.length > maxValueWidth ? e.value.slice(0, maxValueWidth - 3) + "..." : e.value;
|
|
1896
|
+
return `${e.name.padEnd(nameCol)} ${e.type.padEnd(typeCol)} ${truncatedValue}`;
|
|
1897
|
+
});
|
|
1898
|
+
const lines = [header, separator, ...rows];
|
|
1899
|
+
if (entries.length > maxVariables) {
|
|
1900
|
+
lines.push(`... +${entries.length - maxVariables} more variables`);
|
|
1901
|
+
}
|
|
1902
|
+
return lines.join("\n");
|
|
1903
|
+
}
|
|
1904
|
+
function describeType2(val) {
|
|
1905
|
+
if (val === null) return "null";
|
|
1906
|
+
if (val === void 0) return "undefined";
|
|
1907
|
+
if (Array.isArray(val)) {
|
|
1908
|
+
if (val.length === 0) return "Array";
|
|
1909
|
+
return `Array<${describeType2(val[0])}>`;
|
|
1910
|
+
}
|
|
1911
|
+
const t = typeof val;
|
|
1912
|
+
if (t === "object") {
|
|
1913
|
+
const name = val.constructor?.name;
|
|
1914
|
+
return name && name !== "Object" ? name : "Object";
|
|
1915
|
+
}
|
|
1916
|
+
return t;
|
|
1917
|
+
}
|
|
1918
|
+
function truncateValue2(val, maxLen = 50) {
|
|
1919
|
+
if (val === null) return "null";
|
|
1920
|
+
if (val === void 0) return "undefined";
|
|
1921
|
+
if (typeof val === "function") return `[Function: ${val.name || "anonymous"}]`;
|
|
1922
|
+
if (typeof val === "symbol") return val.toString();
|
|
1923
|
+
try {
|
|
1924
|
+
let str;
|
|
1925
|
+
if (typeof val === "string") {
|
|
1926
|
+
str = JSON.stringify(val);
|
|
1927
|
+
} else if (Array.isArray(val)) {
|
|
1928
|
+
const preview = val.slice(0, 3).map((v) => truncateValue2(v, 20)).join(", ");
|
|
1929
|
+
str = val.length > 3 ? `[${preview}, ... +${val.length - 3}]` : `[${preview}]`;
|
|
1930
|
+
} else if (typeof val === "object") {
|
|
1931
|
+
const keys = Object.keys(val);
|
|
1932
|
+
const preview = keys.slice(0, 5).join(", ");
|
|
1933
|
+
str = keys.length > 5 ? `{${preview}, ... +${keys.length - 5}}` : `{${preview}}`;
|
|
1934
|
+
} else {
|
|
1935
|
+
str = String(val);
|
|
1936
|
+
}
|
|
1937
|
+
if (str.length > maxLen) {
|
|
1938
|
+
return str.slice(0, maxLen - 3) + "...";
|
|
1939
|
+
}
|
|
1940
|
+
return str;
|
|
1941
|
+
} catch {
|
|
1942
|
+
return "[value]";
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
// src/context/message-builder.ts
|
|
1947
|
+
function buildStopMessage(payload) {
|
|
1948
|
+
const entries = Object.entries(payload).map(([key, sv]) => `${key}: ${sv.display}`).join(", ");
|
|
1949
|
+
return `\u2190 stop { ${entries} }`;
|
|
1950
|
+
}
|
|
1951
|
+
function buildErrorMessage(error) {
|
|
1952
|
+
return `\u2190 error [${error.type}] ${error.message} (line ${error.line})`;
|
|
1953
|
+
}
|
|
1954
|
+
function buildInterventionMessage(text) {
|
|
1955
|
+
return text;
|
|
1956
|
+
}
|
|
1957
|
+
function buildHookInterruptMessage(hookId, message) {
|
|
1958
|
+
return `\u26A0 [hook:${hookId}] ${message}`;
|
|
1959
|
+
}
|
|
1960
|
+
function buildTasklistReminderMessage(tasklistId, ready, blocked, failed) {
|
|
1961
|
+
let msg = `\u26A0 [system] Tasklist "${tasklistId}" incomplete.`;
|
|
1962
|
+
if (ready.length > 0) msg += ` Ready: ${ready.join(", ")}.`;
|
|
1963
|
+
if (blocked.length > 0) msg += ` Blocked: ${blocked.join(", ")}.`;
|
|
1964
|
+
if (failed.length > 0) msg += ` Failed: ${failed.join(", ")}.`;
|
|
1965
|
+
msg += " Continue with a ready task.";
|
|
1966
|
+
return msg;
|
|
1967
|
+
}
|
|
1968
|
+
function renderTaskLine(task, state) {
|
|
1969
|
+
const completion = state.completed.get(task.id);
|
|
1970
|
+
if (completion?.status === "completed") {
|
|
1971
|
+
const outputStr = JSON.stringify(completion.output);
|
|
1972
|
+
const truncated = outputStr.length > 40 ? outputStr.slice(0, 37) + "..." : outputStr;
|
|
1973
|
+
return { symbol: "\u2713", detail: `\u2192 ${truncated}` };
|
|
1974
|
+
}
|
|
1975
|
+
if (completion?.status === "failed") {
|
|
1976
|
+
return { symbol: "\u2717", detail: `\u2014 ${completion.error ?? "unknown error"}` };
|
|
1977
|
+
}
|
|
1978
|
+
if (completion?.status === "skipped") {
|
|
1979
|
+
return { symbol: "\u2298", detail: "(skipped \u2014 condition was falsy)" };
|
|
1980
|
+
}
|
|
1981
|
+
if (state.runningTasks.has(task.id)) {
|
|
1982
|
+
const progress = state.progressMessages?.get(task.id);
|
|
1983
|
+
const detail = progress ? `(running \u2014 ${progress.percent != null ? progress.percent + "% " : ""}${progress.message})` : "(running)";
|
|
1984
|
+
return { symbol: "\u25C9", detail };
|
|
1985
|
+
}
|
|
1986
|
+
if (state.readyTasks.has(task.id)) {
|
|
1987
|
+
return { symbol: "\u25CE", detail: "(ready \u2014 deps satisfied)" };
|
|
1988
|
+
}
|
|
1989
|
+
const deps = task.dependsOn?.join(", ") ?? "";
|
|
1990
|
+
return { symbol: "\u25CB", detail: deps ? `(blocked \u2014 waiting on: ${deps})` : "(pending)" };
|
|
1991
|
+
}
|
|
1992
|
+
function buildTaskContinueMessage(tasklistId, completedTaskId, readyTasks, tasklistsState) {
|
|
1993
|
+
let msg = `\u2190 completeTask \u2713 ${tasklistId}/${completedTaskId}`;
|
|
1994
|
+
if (readyTasks.length > 0) {
|
|
1995
|
+
msg += `
|
|
1996
|
+
|
|
1997
|
+
Next task:`;
|
|
1998
|
+
const next = readyTasks[0];
|
|
1999
|
+
msg += `
|
|
2000
|
+
Task: ${next.id}`;
|
|
2001
|
+
msg += `
|
|
2002
|
+
Instructions: ${next.instructions}`;
|
|
2003
|
+
const schemaStr = Object.entries(next.outputSchema).map(([k, v]) => `${k}: ${v.type}`).join(", ");
|
|
2004
|
+
msg += `
|
|
2005
|
+
Expected output: { ${schemaStr} }`;
|
|
2006
|
+
if (readyTasks.length > 1) {
|
|
2007
|
+
msg += `
|
|
2008
|
+
|
|
2009
|
+
Also ready: ${readyTasks.slice(1).map((t) => t.id).join(", ")}`;
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
const tasksBlock = generateTasksBlock(tasklistsState);
|
|
2013
|
+
if (tasksBlock) msg += `
|
|
2014
|
+
|
|
2015
|
+
${tasksBlock}`;
|
|
2016
|
+
return msg;
|
|
2017
|
+
}
|
|
2018
|
+
function buildTaskOrderViolationMessage(tasklistId, attemptedTaskId, readyTasks, tasklistsState) {
|
|
2019
|
+
let msg = `\u26A0 [system] Task order violation in tasklist "${tasklistId}": tried to complete "${attemptedTaskId}" but it is not ready.`;
|
|
2020
|
+
if (readyTasks.length > 0) {
|
|
2021
|
+
msg += `
|
|
2022
|
+
|
|
2023
|
+
Next task to complete:`;
|
|
2024
|
+
const next = readyTasks[0];
|
|
2025
|
+
msg += `
|
|
2026
|
+
Task: ${next.id}`;
|
|
2027
|
+
msg += `
|
|
2028
|
+
Instructions: ${next.instructions}`;
|
|
2029
|
+
const schemaStr = Object.entries(next.outputSchema).map(([k, v]) => `${k}: ${v.type}`).join(", ");
|
|
2030
|
+
msg += `
|
|
2031
|
+
Expected output: { ${schemaStr} }`;
|
|
2032
|
+
if (readyTasks.length > 1) {
|
|
2033
|
+
msg += `
|
|
2034
|
+
|
|
2035
|
+
Also ready: ${readyTasks.slice(1).map((t) => t.id).join(", ")}`;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
msg += "\n\nWork on the ready task above, then call completeTask() when done.";
|
|
2039
|
+
const tasksBlock = generateTasksBlock(tasklistsState);
|
|
2040
|
+
if (tasksBlock) msg += `
|
|
2041
|
+
|
|
2042
|
+
${tasksBlock}`;
|
|
2043
|
+
return msg;
|
|
2044
|
+
}
|
|
2045
|
+
function generateCurrentTaskBlock(tasklistsState) {
|
|
2046
|
+
if (tasklistsState.tasklists.size === 0) return null;
|
|
2047
|
+
const lines = [];
|
|
2048
|
+
for (const [tasklistId, state] of tasklistsState.tasklists) {
|
|
2049
|
+
const readyIds = [...state.readyTasks];
|
|
2050
|
+
if (readyIds.length === 0) continue;
|
|
2051
|
+
const next = state.plan.tasks.find((t) => t.id === readyIds[0]);
|
|
2052
|
+
if (!next) continue;
|
|
2053
|
+
lines.push(`{{CURRENT_TASK}}`);
|
|
2054
|
+
lines.push(`Tasklist: ${tasklistId}`);
|
|
2055
|
+
lines.push(`Task: ${next.id}`);
|
|
2056
|
+
lines.push(`Instructions: ${next.instructions}`);
|
|
2057
|
+
const schemaStr = Object.entries(next.outputSchema).map(([k, v]) => `${k}: ${v.type}`).join(", ");
|
|
2058
|
+
lines.push(`Expected output: { ${schemaStr} }`);
|
|
2059
|
+
}
|
|
2060
|
+
return lines.length > 0 ? lines.join("\n") : null;
|
|
2061
|
+
}
|
|
2062
|
+
function generateTasksBlock(tasklistsState) {
|
|
2063
|
+
if (tasklistsState.tasklists.size === 0) return null;
|
|
2064
|
+
const lines = ["{{TASKS}}"];
|
|
2065
|
+
for (const [tasklistId, state] of tasklistsState.tasklists) {
|
|
2066
|
+
const width = Math.max(1, 60 - tasklistId.length - 3);
|
|
2067
|
+
lines.push(`\u250C ${tasklistId} ${"\u2500".repeat(width)}\u2510`);
|
|
2068
|
+
for (const task of state.plan.tasks) {
|
|
2069
|
+
const { symbol, detail } = renderTaskLine(task, state);
|
|
2070
|
+
lines.push(`\u2502 ${symbol} ${task.id.padEnd(18)} ${detail.padEnd(40)}\u2502`);
|
|
2071
|
+
}
|
|
2072
|
+
lines.push(`\u2514${"\u2500".repeat(63)}\u2518`);
|
|
2073
|
+
}
|
|
2074
|
+
return lines.join("\n");
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
// src/sandbox/agent-registry.ts
|
|
2078
|
+
var AgentRegistry = class {
|
|
2079
|
+
entries = /* @__PURE__ */ new Map();
|
|
2080
|
+
questionResolvers = /* @__PURE__ */ new Map();
|
|
2081
|
+
currentTurn = 0;
|
|
2082
|
+
config;
|
|
2083
|
+
constructor(config = {}) {
|
|
2084
|
+
this.config = config;
|
|
2085
|
+
}
|
|
2086
|
+
register(varName, promise, label, childSession) {
|
|
2087
|
+
const entry = {
|
|
2088
|
+
varName,
|
|
2089
|
+
label,
|
|
2090
|
+
status: "running",
|
|
2091
|
+
promise,
|
|
2092
|
+
childSession,
|
|
2093
|
+
registeredAt: Date.now(),
|
|
2094
|
+
registeredTurn: this.currentTurn,
|
|
2095
|
+
pendingQuestion: null
|
|
2096
|
+
};
|
|
2097
|
+
this.entries.set(varName, entry);
|
|
2098
|
+
promise.then(
|
|
2099
|
+
(value) => this.resolve(varName, value),
|
|
2100
|
+
(err) => {
|
|
2101
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
2102
|
+
this.fail(varName, error);
|
|
2103
|
+
}
|
|
2104
|
+
);
|
|
2105
|
+
this.config.onRegistered?.(varName, label);
|
|
2106
|
+
}
|
|
2107
|
+
resolve(varName, value) {
|
|
2108
|
+
const entry = this.entries.get(varName);
|
|
2109
|
+
if (!entry) return;
|
|
2110
|
+
entry.status = "resolved";
|
|
2111
|
+
entry.resolvedValue = value;
|
|
2112
|
+
entry.completedAt = Date.now();
|
|
2113
|
+
this.config.onResolved?.(varName);
|
|
2114
|
+
}
|
|
2115
|
+
fail(varName, error) {
|
|
2116
|
+
const entry = this.entries.get(varName);
|
|
2117
|
+
if (!entry) return;
|
|
2118
|
+
entry.status = "failed";
|
|
2119
|
+
entry.error = error;
|
|
2120
|
+
entry.completedAt = Date.now();
|
|
2121
|
+
this.config.onFailed?.(varName, error);
|
|
2122
|
+
}
|
|
2123
|
+
getAll() {
|
|
2124
|
+
return [...this.entries.values()];
|
|
2125
|
+
}
|
|
2126
|
+
getPending() {
|
|
2127
|
+
return [...this.entries.values()].filter(
|
|
2128
|
+
(e) => e.status === "running" || e.status === "waiting"
|
|
2129
|
+
);
|
|
2130
|
+
}
|
|
2131
|
+
getSnapshot(varName) {
|
|
2132
|
+
const entry = this.entries.get(varName);
|
|
2133
|
+
if (!entry) return null;
|
|
2134
|
+
let tasklistsState = null;
|
|
2135
|
+
if (entry.childSession) {
|
|
2136
|
+
try {
|
|
2137
|
+
tasklistsState = entry.childSession.snapshot().tasklistsState;
|
|
2138
|
+
} catch {
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
return {
|
|
2142
|
+
varName: entry.varName,
|
|
2143
|
+
label: entry.label,
|
|
2144
|
+
status: entry.status,
|
|
2145
|
+
tasklistsState,
|
|
2146
|
+
pendingQuestion: entry.pendingQuestion ?? null,
|
|
2147
|
+
error: entry.error
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
getAllSnapshots() {
|
|
2151
|
+
return [...this.entries.keys()].map((varName) => this.getSnapshot(varName)).filter(Boolean);
|
|
2152
|
+
}
|
|
2153
|
+
findByPromise(promise) {
|
|
2154
|
+
for (const entry of this.entries.values()) {
|
|
2155
|
+
if (entry.promise === promise) return entry;
|
|
2156
|
+
}
|
|
2157
|
+
return null;
|
|
2158
|
+
}
|
|
2159
|
+
advanceTurn() {
|
|
2160
|
+
this.currentTurn++;
|
|
2161
|
+
}
|
|
2162
|
+
getCurrentTurn() {
|
|
2163
|
+
return this.currentTurn;
|
|
2164
|
+
}
|
|
2165
|
+
hasEntries() {
|
|
2166
|
+
return this.entries.size > 0;
|
|
2167
|
+
}
|
|
2168
|
+
hasVisibleEntries() {
|
|
2169
|
+
for (const entry of this.entries.values()) {
|
|
2170
|
+
if (entry.status === "running" || entry.status === "waiting") return true;
|
|
2171
|
+
if (entry.completedAt != null) {
|
|
2172
|
+
const turnsSinceCompletion = this.currentTurn - entry.registeredTurn;
|
|
2173
|
+
if (turnsSinceCompletion <= 5) return true;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
return false;
|
|
2177
|
+
}
|
|
2178
|
+
/**
|
|
2179
|
+
* Low-level setter — updates entry status and question fields.
|
|
2180
|
+
* Prefer askQuestion() for the full flow (sets question + returns Promise).
|
|
2181
|
+
*/
|
|
2182
|
+
setPendingQuestion(varName, question) {
|
|
2183
|
+
const entry = this.entries.get(varName);
|
|
2184
|
+
if (!entry) throw new Error(`setPendingQuestion: unknown agent "${varName}"`);
|
|
2185
|
+
entry.pendingQuestion = question;
|
|
2186
|
+
entry.status = "waiting";
|
|
2187
|
+
}
|
|
2188
|
+
/**
|
|
2189
|
+
* Ask a question on behalf of a child agent. Sets status to 'waiting',
|
|
2190
|
+
* stores the question, and returns a Promise that resolves when the
|
|
2191
|
+
* parent calls respond().
|
|
2192
|
+
*/
|
|
2193
|
+
askQuestion(varName, question) {
|
|
2194
|
+
const entry = this.entries.get(varName);
|
|
2195
|
+
if (!entry) throw new Error(`askQuestion: unknown agent "${varName}"`);
|
|
2196
|
+
entry.pendingQuestion = question;
|
|
2197
|
+
entry.status = "waiting";
|
|
2198
|
+
this.config.onQuestionAsked?.(varName, question);
|
|
2199
|
+
return new Promise((resolve) => {
|
|
2200
|
+
this.questionResolvers.set(varName, resolve);
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
/**
|
|
2204
|
+
* Deliver structured input to a child agent's pending askParent() call.
|
|
2205
|
+
* Resolves the Promise returned by askQuestion(), clears the pending
|
|
2206
|
+
* question, and sets the agent back to 'running'.
|
|
2207
|
+
*/
|
|
2208
|
+
respond(varName, data) {
|
|
2209
|
+
const entry = this.entries.get(varName);
|
|
2210
|
+
if (!entry) throw new Error(`respond: unknown agent "${varName}"`);
|
|
2211
|
+
if (entry.status !== "waiting") {
|
|
2212
|
+
throw new Error(`respond: agent "${varName}" is not waiting for input (status: ${entry.status})`);
|
|
2213
|
+
}
|
|
2214
|
+
const resolver = this.questionResolvers.get(varName);
|
|
2215
|
+
if (!resolver) throw new Error(`respond: no pending question for agent "${varName}"`);
|
|
2216
|
+
entry.pendingQuestion = null;
|
|
2217
|
+
entry.status = "running";
|
|
2218
|
+
this.questionResolvers.delete(varName);
|
|
2219
|
+
resolver(data);
|
|
2220
|
+
this.config.onQuestionAnswered?.(varName);
|
|
2221
|
+
}
|
|
2222
|
+
destroy() {
|
|
2223
|
+
this.entries.clear();
|
|
2224
|
+
this.questionResolvers.clear();
|
|
2225
|
+
}
|
|
2226
|
+
};
|
|
2227
|
+
|
|
2228
|
+
// src/context/agents-block.ts
|
|
2229
|
+
function generateAgentsBlock(registry, resolvedInThisStop) {
|
|
2230
|
+
if (!registry.hasVisibleEntries()) return null;
|
|
2231
|
+
const currentTurn = registry.getCurrentTurn();
|
|
2232
|
+
const lines = ["{{AGENTS}}"];
|
|
2233
|
+
for (const entry of registry.getAll()) {
|
|
2234
|
+
const turnsSinceRegistered = currentTurn - entry.registeredTurn;
|
|
2235
|
+
const completedTurnDistance = entry.completedAt != null ? currentTurn - entry.registeredTurn : 0;
|
|
2236
|
+
if ((entry.status === "resolved" || entry.status === "failed") && completedTurnDistance >= 6) {
|
|
2237
|
+
continue;
|
|
2238
|
+
}
|
|
2239
|
+
const width = Math.max(1, 60 - entry.varName.length - entry.label.length - 5);
|
|
2240
|
+
lines.push(`\u250C ${entry.varName} \u2014 ${entry.label} ${"\u2500".repeat(width)}\u2510`);
|
|
2241
|
+
const isCompact = (entry.status === "resolved" || entry.status === "failed") && completedTurnDistance >= 3;
|
|
2242
|
+
if (entry.status === "running") {
|
|
2243
|
+
lines.push(`\u2502 \u25C9 running${" ".repeat(52)}\u2502`);
|
|
2244
|
+
const snapshot = registry.getSnapshot(entry.varName);
|
|
2245
|
+
if (snapshot?.tasklistsState && snapshot.tasklistsState.tasklists.size > 0) {
|
|
2246
|
+
for (const [tlId, tlState] of snapshot.tasklistsState.tasklists) {
|
|
2247
|
+
const tlWidth = Math.max(1, 56 - tlId.length - 3);
|
|
2248
|
+
lines.push(`\u2502 \u250C tasks ${"\u2500".repeat(tlWidth)}\u2510 \u2502`);
|
|
2249
|
+
for (const task of tlState.plan.tasks) {
|
|
2250
|
+
const { symbol, detail } = renderTaskLine(task, tlState);
|
|
2251
|
+
lines.push(`\u2502 \u2502 ${symbol} ${task.id.padEnd(16)} ${detail.padEnd(36)}\u2502 \u2502`);
|
|
2252
|
+
}
|
|
2253
|
+
lines.push(`\u2502 \u2514${"\u2500".repeat(Math.max(1, 57))}\u2518 \u2502`);
|
|
2254
|
+
}
|
|
2255
|
+
} else {
|
|
2256
|
+
lines.push(`\u2502 (no tasklist)${" ".repeat(48)}\u2502`);
|
|
2257
|
+
}
|
|
2258
|
+
} else if (entry.status === "waiting") {
|
|
2259
|
+
lines.push(`\u2502 ? waiting \u2014 needs input from parent${" ".repeat(26)}\u2502`);
|
|
2260
|
+
if (entry.pendingQuestion && !isCompact) {
|
|
2261
|
+
lines.push(`\u2502 \u250C question \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502`);
|
|
2262
|
+
const msg = entry.pendingQuestion.message.slice(0, 50);
|
|
2263
|
+
lines.push(`\u2502 \u2502 "${msg}"${" ".repeat(Math.max(1, 51 - msg.length))}\u2502 \u2502`);
|
|
2264
|
+
const schemaEntries = Object.entries(entry.pendingQuestion.schema);
|
|
2265
|
+
if (schemaEntries.length > 0) {
|
|
2266
|
+
lines.push(`\u2502 \u2502 schema: {${" ".repeat(43)}\u2502 \u2502`);
|
|
2267
|
+
for (const [key, val] of schemaEntries.slice(0, 5)) {
|
|
2268
|
+
const typeStr = formatSchemaValue(val);
|
|
2269
|
+
lines.push(`\u2502 \u2502 ${key}: ${typeStr}`.padEnd(56) + "\u2502 \u2502");
|
|
2270
|
+
}
|
|
2271
|
+
if (schemaEntries.length > 5) {
|
|
2272
|
+
lines.push(`\u2502 \u2502 ... +${schemaEntries.length - 5} more`.padEnd(56) + "\u2502 \u2502");
|
|
2273
|
+
}
|
|
2274
|
+
lines.push(`\u2502 \u2502 }`.padEnd(56) + "\u2502 \u2502");
|
|
2275
|
+
}
|
|
2276
|
+
lines.push(`\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502`);
|
|
2277
|
+
}
|
|
2278
|
+
} else if (entry.status === "resolved") {
|
|
2279
|
+
if (isCompact) {
|
|
2280
|
+
lines.push(`\u2502 \u2713 resolved${" ".repeat(51)}\u2502`);
|
|
2281
|
+
} else if (resolvedInThisStop.has(entry.varName)) {
|
|
2282
|
+
lines.push(`\u2502 \u2713 (value included in this stop payload)${" ".repeat(22)}\u2502`);
|
|
2283
|
+
} else {
|
|
2284
|
+
lines.push(`\u2502 \u2713 resolved${" ".repeat(51)}\u2502`);
|
|
2285
|
+
}
|
|
2286
|
+
} else if (entry.status === "failed") {
|
|
2287
|
+
if (isCompact) {
|
|
2288
|
+
lines.push(`\u2502 \u2717 failed${" ".repeat(53)}\u2502`);
|
|
2289
|
+
} else {
|
|
2290
|
+
const errMsg = (entry.error ?? "unknown error").slice(0, 50);
|
|
2291
|
+
lines.push(`\u2502 \u2717 ${errMsg.padEnd(59)}\u2502`);
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
lines.push(`\u2514${"\u2500".repeat(63)}\u2518`);
|
|
2295
|
+
}
|
|
2296
|
+
if (lines.length === 1) return null;
|
|
2297
|
+
return lines.join("\n");
|
|
2298
|
+
}
|
|
2299
|
+
function formatSchemaValue(val) {
|
|
2300
|
+
if (!val || typeof val !== "object") return String(val);
|
|
2301
|
+
const obj = val;
|
|
2302
|
+
if (Array.isArray(obj.enum)) {
|
|
2303
|
+
return obj.enum.slice(0, 4).map((e) => `"${e}"`).join(" | ") + (obj.enum.length > 4 ? ` | ...` : "");
|
|
2304
|
+
}
|
|
2305
|
+
if (typeof obj.type === "string") return obj.type;
|
|
2306
|
+
return JSON.stringify(val).slice(0, 30);
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
// src/session/conversation-state.ts
|
|
2310
|
+
function computeScopeDelta(previous, current) {
|
|
2311
|
+
const prevMap = /* @__PURE__ */ new Map();
|
|
2312
|
+
for (const entry of previous) prevMap.set(entry.name, entry);
|
|
2313
|
+
const currMap = /* @__PURE__ */ new Map();
|
|
2314
|
+
for (const entry of current) currMap.set(entry.name, entry);
|
|
2315
|
+
const added = [];
|
|
2316
|
+
const changed = [];
|
|
2317
|
+
for (const entry of current) {
|
|
2318
|
+
const prev = prevMap.get(entry.name);
|
|
2319
|
+
if (!prev) {
|
|
2320
|
+
added.push(entry);
|
|
2321
|
+
} else if (prev.type !== entry.type || prev.value !== entry.value) {
|
|
2322
|
+
changed.push({
|
|
2323
|
+
...entry,
|
|
2324
|
+
previousValue: prev.value,
|
|
2325
|
+
previousType: prev.type
|
|
2326
|
+
});
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
const removed = [];
|
|
2330
|
+
for (const entry of previous) {
|
|
2331
|
+
if (!currMap.has(entry.name)) {
|
|
2332
|
+
removed.push(entry.name);
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
return { added, changed, removed };
|
|
2336
|
+
}
|
|
2337
|
+
function serializeTasklistsState(state) {
|
|
2338
|
+
const tasklists = {};
|
|
2339
|
+
for (const [id, tl] of state.tasklists) {
|
|
2340
|
+
tasklists[id] = {
|
|
2341
|
+
plan: {
|
|
2342
|
+
tasklistId: tl.plan.tasklistId,
|
|
2343
|
+
description: tl.plan.description,
|
|
2344
|
+
tasks: tl.plan.tasks
|
|
2345
|
+
},
|
|
2346
|
+
completed: Object.fromEntries(tl.completed),
|
|
2347
|
+
readyTasks: [...tl.readyTasks],
|
|
2348
|
+
runningTasks: [...tl.runningTasks],
|
|
2349
|
+
outputs: Object.fromEntries(tl.outputs),
|
|
2350
|
+
progressMessages: Object.fromEntries(tl.progressMessages),
|
|
2351
|
+
retryCount: Object.fromEntries(tl.retryCount)
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2354
|
+
return { tasklists };
|
|
2355
|
+
}
|
|
2356
|
+
var ConversationRecorder = class {
|
|
2357
|
+
state;
|
|
2358
|
+
previousScope = [];
|
|
2359
|
+
pendingEvents = [];
|
|
2360
|
+
currentTurnStartedAt;
|
|
2361
|
+
constructor() {
|
|
2362
|
+
const now = Date.now();
|
|
2363
|
+
this.state = {
|
|
2364
|
+
startedAt: now,
|
|
2365
|
+
turns: [],
|
|
2366
|
+
tasklists: { tasklists: {} },
|
|
2367
|
+
stopCount: 0,
|
|
2368
|
+
status: "idle"
|
|
2369
|
+
};
|
|
2370
|
+
this.currentTurnStartedAt = now;
|
|
2371
|
+
}
|
|
2372
|
+
/** Record an assistant turn ending at a stop boundary. */
|
|
2373
|
+
recordStop(code, payload, scope, tasklists) {
|
|
2374
|
+
const stopPayload = {};
|
|
2375
|
+
for (const [key, sv] of Object.entries(payload)) {
|
|
2376
|
+
stopPayload[key] = { display: sv.display, type: typeof sv.value };
|
|
2377
|
+
}
|
|
2378
|
+
this.state.stopCount++;
|
|
2379
|
+
this.pushTurn({
|
|
2380
|
+
role: "assistant",
|
|
2381
|
+
code,
|
|
2382
|
+
message: null,
|
|
2383
|
+
boundary: { type: "stop", payload: stopPayload },
|
|
2384
|
+
scope
|
|
2385
|
+
});
|
|
2386
|
+
this.state.tasklists = serializeTasklistsState(tasklists);
|
|
2387
|
+
}
|
|
2388
|
+
/** Record an assistant turn ending at an error boundary. */
|
|
2389
|
+
recordError(code, error, scope) {
|
|
2390
|
+
this.pushTurn({
|
|
2391
|
+
role: "assistant",
|
|
2392
|
+
code,
|
|
2393
|
+
message: null,
|
|
2394
|
+
boundary: {
|
|
2395
|
+
type: "error",
|
|
2396
|
+
error: {
|
|
2397
|
+
type: error.type,
|
|
2398
|
+
message: error.message,
|
|
2399
|
+
line: error.line,
|
|
2400
|
+
source: error.source
|
|
2401
|
+
}
|
|
2402
|
+
},
|
|
2403
|
+
scope
|
|
2404
|
+
});
|
|
2405
|
+
}
|
|
2406
|
+
/** Record an assistant turn ending at an intervention boundary. */
|
|
2407
|
+
recordIntervention(code, text, scope) {
|
|
2408
|
+
this.pushTurn({
|
|
2409
|
+
role: "assistant",
|
|
2410
|
+
code,
|
|
2411
|
+
message: null,
|
|
2412
|
+
boundary: { type: "intervention", text },
|
|
2413
|
+
scope
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
/** Record an assistant turn ending at a tasklist reminder boundary. */
|
|
2417
|
+
recordTasklistReminder(code, tasklistId, ready, blocked, failed, scope, tasklists) {
|
|
2418
|
+
this.pushTurn({
|
|
2419
|
+
role: "assistant",
|
|
2420
|
+
code,
|
|
2421
|
+
message: null,
|
|
2422
|
+
boundary: { type: "tasklist_reminder", tasklistId, ready, blocked, failed },
|
|
2423
|
+
scope
|
|
2424
|
+
});
|
|
2425
|
+
this.state.tasklists = serializeTasklistsState(tasklists);
|
|
2426
|
+
}
|
|
2427
|
+
/** Record session completion. */
|
|
2428
|
+
recordCompletion(code, scope, tasklists, status) {
|
|
2429
|
+
this.pushTurn({
|
|
2430
|
+
role: "assistant",
|
|
2431
|
+
code,
|
|
2432
|
+
message: null,
|
|
2433
|
+
boundary: { type: "completion" },
|
|
2434
|
+
scope
|
|
2435
|
+
});
|
|
2436
|
+
this.state.tasklists = serializeTasklistsState(tasklists);
|
|
2437
|
+
this.state.status = status;
|
|
2438
|
+
}
|
|
2439
|
+
/** Record a user message turn. */
|
|
2440
|
+
recordUserMessage(text, scope) {
|
|
2441
|
+
this.pushTurn({
|
|
2442
|
+
role: "user",
|
|
2443
|
+
code: null,
|
|
2444
|
+
message: text,
|
|
2445
|
+
boundary: null,
|
|
2446
|
+
scope
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
/** Accumulate a session event (filtered to TurnEvent subset). */
|
|
2450
|
+
recordEvent(event) {
|
|
2451
|
+
const turnEvent = toTurnEvent(event);
|
|
2452
|
+
if (turnEvent) this.pendingEvents.push(turnEvent);
|
|
2453
|
+
if (event.type === "status") {
|
|
2454
|
+
this.state.status = event.status;
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
/** Update session status. */
|
|
2458
|
+
updateStatus(status) {
|
|
2459
|
+
this.state.status = status;
|
|
2460
|
+
}
|
|
2461
|
+
/** Get the current full conversation state (returns a shallow copy). */
|
|
2462
|
+
getState() {
|
|
2463
|
+
return {
|
|
2464
|
+
...this.state,
|
|
2465
|
+
turns: [...this.state.turns]
|
|
2466
|
+
};
|
|
2467
|
+
}
|
|
2468
|
+
pushTurn(opts) {
|
|
2469
|
+
const now = Date.now();
|
|
2470
|
+
const scopeSnapshot = [...opts.scope];
|
|
2471
|
+
const scopeDelta = this.state.turns.length > 0 ? computeScopeDelta(this.previousScope, opts.scope) : opts.scope.length > 0 ? { added: [...opts.scope], changed: [], removed: [] } : null;
|
|
2472
|
+
this.state.turns.push({
|
|
2473
|
+
index: this.state.turns.length,
|
|
2474
|
+
startedAt: this.currentTurnStartedAt,
|
|
2475
|
+
endedAt: now,
|
|
2476
|
+
role: opts.role,
|
|
2477
|
+
code: opts.code ? [...opts.code] : null,
|
|
2478
|
+
message: opts.message,
|
|
2479
|
+
boundary: opts.boundary,
|
|
2480
|
+
scopeSnapshot,
|
|
2481
|
+
scopeDelta,
|
|
2482
|
+
events: this.pendingEvents.splice(0)
|
|
2483
|
+
});
|
|
2484
|
+
this.previousScope = scopeSnapshot;
|
|
2485
|
+
this.currentTurnStartedAt = now;
|
|
2486
|
+
}
|
|
2487
|
+
};
|
|
2488
|
+
function toTurnEvent(event) {
|
|
2489
|
+
switch (event.type) {
|
|
2490
|
+
case "display":
|
|
2491
|
+
return { type: "display", componentId: event.componentId };
|
|
2492
|
+
case "ask_start":
|
|
2493
|
+
return { type: "ask_start", formId: event.formId };
|
|
2494
|
+
case "ask_end":
|
|
2495
|
+
return { type: "ask_end", formId: event.formId };
|
|
2496
|
+
case "async_start":
|
|
2497
|
+
return { type: "async_start", taskId: event.taskId, label: event.label };
|
|
2498
|
+
case "async_complete":
|
|
2499
|
+
return { type: "async_complete", taskId: event.taskId };
|
|
2500
|
+
case "async_failed":
|
|
2501
|
+
return { type: "async_failed", taskId: event.taskId, error: event.error };
|
|
2502
|
+
case "async_cancelled":
|
|
2503
|
+
return { type: "async_cancelled", taskId: event.taskId };
|
|
2504
|
+
case "tasklist_declared":
|
|
2505
|
+
return {
|
|
2506
|
+
type: "tasklist_declared",
|
|
2507
|
+
tasklistId: event.tasklistId,
|
|
2508
|
+
description: event.plan.description,
|
|
2509
|
+
taskCount: event.plan.tasks.length
|
|
2510
|
+
};
|
|
2511
|
+
case "task_complete":
|
|
2512
|
+
return { type: "task_complete", tasklistId: event.tasklistId, taskId: event.id };
|
|
2513
|
+
case "task_failed":
|
|
2514
|
+
return { type: "task_failed", tasklistId: event.tasklistId, taskId: event.id, error: event.error };
|
|
2515
|
+
case "task_retried":
|
|
2516
|
+
return { type: "task_retried", tasklistId: event.tasklistId, taskId: event.id };
|
|
2517
|
+
case "task_skipped":
|
|
2518
|
+
return { type: "task_skipped", tasklistId: event.tasklistId, taskId: event.id, reason: event.reason };
|
|
2519
|
+
case "task_progress":
|
|
2520
|
+
return { type: "task_progress", tasklistId: event.tasklistId, taskId: event.id, message: event.message, percent: event.percent };
|
|
2521
|
+
case "knowledge_loaded":
|
|
2522
|
+
return { type: "knowledge_loaded", domains: event.domains };
|
|
2523
|
+
case "class_loaded":
|
|
2524
|
+
return { type: "class_loaded", className: event.className, methods: event.methods };
|
|
2525
|
+
case "hook":
|
|
2526
|
+
return { type: "hook", hookId: event.hookId, action: event.action };
|
|
2527
|
+
case "agent_registered":
|
|
2528
|
+
return { type: "agent_registered", varName: event.varName, label: event.label };
|
|
2529
|
+
case "agent_resolved":
|
|
2530
|
+
return { type: "agent_resolved", varName: event.varName };
|
|
2531
|
+
case "agent_failed":
|
|
2532
|
+
return { type: "agent_failed", varName: event.varName, error: event.error };
|
|
2533
|
+
default:
|
|
2534
|
+
return null;
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
// src/session/session.ts
|
|
2539
|
+
var Session = class extends EventEmitter {
|
|
2540
|
+
status = "idle";
|
|
2541
|
+
config;
|
|
2542
|
+
sandbox;
|
|
2543
|
+
asyncManager;
|
|
2544
|
+
hookRegistry;
|
|
2545
|
+
streamController;
|
|
2546
|
+
globalsApi;
|
|
2547
|
+
blocks = [];
|
|
2548
|
+
codeLines = [];
|
|
2549
|
+
messages = [];
|
|
2550
|
+
activeFormId = null;
|
|
2551
|
+
stopCount = 0;
|
|
2552
|
+
tasklistReminderCount = 0;
|
|
2553
|
+
agentRegistry;
|
|
2554
|
+
recorder;
|
|
2555
|
+
turnCodeStart = 0;
|
|
2556
|
+
onSpawn;
|
|
2557
|
+
constructor(options = {}) {
|
|
2558
|
+
super();
|
|
2559
|
+
this.config = options.config ? mergeConfig(options.config) : createDefaultConfig();
|
|
2560
|
+
this.asyncManager = new AsyncManager(this.config.maxAsyncTasks);
|
|
2561
|
+
this.hookRegistry = new HookRegistry();
|
|
2562
|
+
this.agentRegistry = new AgentRegistry({
|
|
2563
|
+
onRegistered: (varName, label) => {
|
|
2564
|
+
this.emitEvent({ type: "agent_registered", varName, label });
|
|
2565
|
+
},
|
|
2566
|
+
onResolved: (varName) => {
|
|
2567
|
+
this.emitEvent({ type: "agent_resolved", varName });
|
|
2568
|
+
},
|
|
2569
|
+
onFailed: (varName, error) => {
|
|
2570
|
+
this.emitEvent({ type: "agent_failed", varName, error });
|
|
2571
|
+
},
|
|
2572
|
+
onQuestionAsked: (varName, question) => {
|
|
2573
|
+
this.emitEvent({ type: "agent_question_asked", varName, question });
|
|
2574
|
+
},
|
|
2575
|
+
onQuestionAnswered: (varName) => {
|
|
2576
|
+
this.emitEvent({ type: "agent_question_answered", varName });
|
|
2577
|
+
}
|
|
2578
|
+
});
|
|
2579
|
+
if (options.hooks) {
|
|
2580
|
+
for (const hook of options.hooks) {
|
|
2581
|
+
this.hookRegistry.register(hook);
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
this.sandbox = new Sandbox({
|
|
2585
|
+
timeout: this.config.functionTimeout,
|
|
2586
|
+
globals: options.globals
|
|
2587
|
+
});
|
|
2588
|
+
this.streamController = new StreamController({
|
|
2589
|
+
onStatement: (source) => this.executeStatement(source),
|
|
2590
|
+
onStop: (payload, source) => this.handleStop(payload, source),
|
|
2591
|
+
onError: (error) => this.handleError(error),
|
|
2592
|
+
onEvent: (event) => this.emitEvent(event),
|
|
2593
|
+
onCodeLine: (line) => this.codeLines.push(line),
|
|
2594
|
+
hookRegistry: this.hookRegistry,
|
|
2595
|
+
hookContext: () => ({
|
|
2596
|
+
lineNumber: this.sandbox.getLineCount(),
|
|
2597
|
+
sessionId: `session_${Date.now()}`,
|
|
2598
|
+
scope: this.sandbox.getScope()
|
|
2599
|
+
})
|
|
2600
|
+
});
|
|
2601
|
+
this.globalsApi = createGlobals({
|
|
2602
|
+
pauseController: this.streamController,
|
|
2603
|
+
renderSurface: {
|
|
2604
|
+
append: (id, element) => {
|
|
2605
|
+
this.emitEvent({ type: "display", componentId: id, jsx: serializeReactElement(element) });
|
|
2606
|
+
},
|
|
2607
|
+
renderForm: async (formId, element) => {
|
|
2608
|
+
this.activeFormId = formId;
|
|
2609
|
+
this.emitEvent({ type: "ask_start", formId, jsx: serializeReactElement(element) });
|
|
2610
|
+
return new Promise((resolve) => {
|
|
2611
|
+
this.once(`form:${formId}`, (data) => {
|
|
2612
|
+
this.activeFormId = null;
|
|
2613
|
+
this.emitEvent({ type: "ask_end", formId });
|
|
2614
|
+
resolve(data);
|
|
2615
|
+
});
|
|
2616
|
+
});
|
|
2617
|
+
},
|
|
2618
|
+
cancelForm: (formId) => {
|
|
2619
|
+
this.activeFormId = null;
|
|
2620
|
+
this.emit(`form:${formId}`, { _cancelled: true });
|
|
2621
|
+
}
|
|
2622
|
+
},
|
|
2623
|
+
asyncManager: this.asyncManager,
|
|
2624
|
+
serializationLimits: this.config.serializationLimits,
|
|
2625
|
+
askTimeout: this.config.askTimeout,
|
|
2626
|
+
onStop: (payload, source) => this.handleStop(payload, source),
|
|
2627
|
+
onDisplay: (id) => {
|
|
2628
|
+
},
|
|
2629
|
+
onAsyncStart: (taskId, label) => {
|
|
2630
|
+
this.emitEvent({ type: "async_start", taskId, label });
|
|
2631
|
+
},
|
|
2632
|
+
onTasklistDeclared: (tasklistId, plan) => {
|
|
2633
|
+
this.emitEvent({ type: "tasklist_declared", tasklistId, plan });
|
|
2634
|
+
},
|
|
2635
|
+
onTaskComplete: (tasklistId, id, output) => {
|
|
2636
|
+
this.emitEvent({ type: "task_complete", tasklistId, id, output });
|
|
2637
|
+
},
|
|
2638
|
+
onTaskFailed: (tasklistId, id, error) => {
|
|
2639
|
+
this.emitEvent({ type: "task_failed", tasklistId, id, error });
|
|
2640
|
+
},
|
|
2641
|
+
onTaskRetried: (tasklistId, id) => {
|
|
2642
|
+
this.emitEvent({ type: "task_retried", tasklistId, id });
|
|
2643
|
+
},
|
|
2644
|
+
onTaskSkipped: (tasklistId, id, reason) => {
|
|
2645
|
+
this.emitEvent({ type: "task_skipped", tasklistId, id, reason });
|
|
2646
|
+
},
|
|
2647
|
+
onTaskProgress: (tasklistId, id, message, percent) => {
|
|
2648
|
+
this.emitEvent({ type: "task_progress", tasklistId, id, message, percent });
|
|
2649
|
+
},
|
|
2650
|
+
onTaskAsyncStart: (tasklistId, id) => {
|
|
2651
|
+
this.emitEvent({ type: "task_async_start", tasklistId, id });
|
|
2652
|
+
},
|
|
2653
|
+
onTaskAsyncComplete: (tasklistId, id, output) => {
|
|
2654
|
+
this.emitEvent({ type: "task_async_complete", tasklistId, id, output });
|
|
2655
|
+
},
|
|
2656
|
+
onTaskAsyncFailed: (tasklistId, id, error) => {
|
|
2657
|
+
this.emitEvent({ type: "task_async_failed", tasklistId, id, error });
|
|
2658
|
+
},
|
|
2659
|
+
onTaskOrderViolation: (tasklistId, attemptedTaskId, readyTasks) => {
|
|
2660
|
+
this.emitEvent({ type: "task_order_violation", tasklistId, attemptedTaskId, readyTasks });
|
|
2661
|
+
},
|
|
2662
|
+
onTaskCompleteContinue: (tasklistId, completedTaskId, readyTasks) => {
|
|
2663
|
+
this.emitEvent({ type: "task_complete_continue", tasklistId, completedTaskId, readyTasks });
|
|
2664
|
+
},
|
|
2665
|
+
maxTaskRetries: this.config.maxTaskRetries,
|
|
2666
|
+
maxTasksPerTasklist: this.config.maxTasksPerTasklist,
|
|
2667
|
+
sleepMaxSeconds: this.config.sleepMaxSeconds,
|
|
2668
|
+
onLoadKnowledge: options.knowledgeLoader ? (selector) => {
|
|
2669
|
+
const content = options.knowledgeLoader(selector);
|
|
2670
|
+
const domains = Object.keys(content);
|
|
2671
|
+
this.emitEvent({ type: "knowledge_loaded", domains });
|
|
2672
|
+
return content;
|
|
2673
|
+
} : void 0,
|
|
2674
|
+
getClassInfo: options.getClassInfo ?? void 0,
|
|
2675
|
+
onLoadClass: options.loadClass ? (className) => {
|
|
2676
|
+
const info = options.getClassInfo?.(className);
|
|
2677
|
+
const methodNames = info?.methods.map((m) => m.name) ?? [];
|
|
2678
|
+
options.loadClass(className, this);
|
|
2679
|
+
this.emitEvent({ type: "class_loaded", className, methods: methodNames });
|
|
2680
|
+
} : void 0,
|
|
2681
|
+
onAskParent: options.onAskParent,
|
|
2682
|
+
isFireAndForget: options.isFireAndForget,
|
|
2683
|
+
onRespond: (promise, data) => {
|
|
2684
|
+
const entry = this.agentRegistry.findByPromise(promise);
|
|
2685
|
+
if (!entry) throw new Error("respond: unknown agent \u2014 pass the agent variable as the first argument");
|
|
2686
|
+
this.agentRegistry.respond(entry.varName, data);
|
|
2687
|
+
}
|
|
2688
|
+
});
|
|
2689
|
+
this.sandbox.inject("stop", this.globalsApi.stop);
|
|
2690
|
+
this.sandbox.inject("display", this.globalsApi.display);
|
|
2691
|
+
this.sandbox.inject("ask", this.globalsApi.ask);
|
|
2692
|
+
this.sandbox.inject("async", this.globalsApi.async);
|
|
2693
|
+
this.sandbox.inject("tasklist", this.globalsApi.tasklist);
|
|
2694
|
+
this.sandbox.inject("completeTask", this.globalsApi.completeTask);
|
|
2695
|
+
this.sandbox.inject("completeTaskAsync", this.globalsApi.completeTaskAsync);
|
|
2696
|
+
this.sandbox.inject("taskProgress", this.globalsApi.taskProgress);
|
|
2697
|
+
this.sandbox.inject("failTask", this.globalsApi.failTask);
|
|
2698
|
+
this.sandbox.inject("retryTask", this.globalsApi.retryTask);
|
|
2699
|
+
this.sandbox.inject("sleep", this.globalsApi.sleep);
|
|
2700
|
+
this.sandbox.inject("loadKnowledge", this.globalsApi.loadKnowledge);
|
|
2701
|
+
this.sandbox.inject("loadClass", this.globalsApi.loadClass);
|
|
2702
|
+
this.sandbox.inject("askParent", this.globalsApi.askParent);
|
|
2703
|
+
this.sandbox.inject("respond", this.globalsApi.respond);
|
|
2704
|
+
if (options.agentNamespaces) {
|
|
2705
|
+
for (const [name, ns] of Object.entries(options.agentNamespaces)) {
|
|
2706
|
+
this.sandbox.inject(name, ns);
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
if (options.knowledgeNamespace) {
|
|
2710
|
+
this.sandbox.inject("knowledge", options.knowledgeNamespace);
|
|
2711
|
+
}
|
|
2712
|
+
this.recorder = new ConversationRecorder();
|
|
2713
|
+
this.on("event", (event) => this.recorder.recordEvent(event));
|
|
2714
|
+
this.onSpawn = options.onSpawn ? async (config) => {
|
|
2715
|
+
this.emitEvent({
|
|
2716
|
+
type: "agent_spawn_start",
|
|
2717
|
+
spaceName: config.spaceName,
|
|
2718
|
+
agentSlug: config.agentSlug,
|
|
2719
|
+
actionId: config.actionId
|
|
2720
|
+
});
|
|
2721
|
+
try {
|
|
2722
|
+
const result = await options.onSpawn(config);
|
|
2723
|
+
this.emitEvent({
|
|
2724
|
+
type: "agent_spawn_complete",
|
|
2725
|
+
spaceName: config.spaceName,
|
|
2726
|
+
agentSlug: config.agentSlug,
|
|
2727
|
+
actionId: config.actionId,
|
|
2728
|
+
result
|
|
2729
|
+
});
|
|
2730
|
+
return result;
|
|
2731
|
+
} catch (err) {
|
|
2732
|
+
this.emitEvent({
|
|
2733
|
+
type: "agent_spawn_failed",
|
|
2734
|
+
spaceName: config.spaceName,
|
|
2735
|
+
agentSlug: config.agentSlug,
|
|
2736
|
+
actionId: config.actionId,
|
|
2737
|
+
error: err?.message ?? String(err)
|
|
2738
|
+
});
|
|
2739
|
+
throw err;
|
|
2740
|
+
}
|
|
2741
|
+
} : void 0;
|
|
2742
|
+
}
|
|
2743
|
+
async executeStatement(source) {
|
|
2744
|
+
this.globalsApi.setCurrentSource(source);
|
|
2745
|
+
return this.sandbox.execute(source);
|
|
2746
|
+
}
|
|
2747
|
+
handleStop(payload, source) {
|
|
2748
|
+
this.stopCount++;
|
|
2749
|
+
this.agentRegistry.advanceTurn();
|
|
2750
|
+
const cpState = this.globalsApi.getTasklistsState();
|
|
2751
|
+
const tasksBlock = generateTasksBlock(cpState);
|
|
2752
|
+
const resolvedInThisStop = /* @__PURE__ */ new Set();
|
|
2753
|
+
for (const [, sv] of Object.entries(payload)) {
|
|
2754
|
+
const entry = this.agentRegistry.findByPromise(sv.value);
|
|
2755
|
+
if (entry?.status === "resolved") resolvedInThisStop.add(entry.varName);
|
|
2756
|
+
}
|
|
2757
|
+
const agentsBlock = generateAgentsBlock(this.agentRegistry, resolvedInThisStop);
|
|
2758
|
+
const baseMsg = buildStopMessage(payload);
|
|
2759
|
+
let msg = baseMsg;
|
|
2760
|
+
if (tasksBlock) msg += `
|
|
2761
|
+
|
|
2762
|
+
${tasksBlock}`;
|
|
2763
|
+
if (agentsBlock) msg += `
|
|
2764
|
+
|
|
2765
|
+
${agentsBlock}`;
|
|
2766
|
+
this.messages.push({ role: "assistant", content: this.codeLines.join("\n") });
|
|
2767
|
+
this.messages.push({ role: "user", content: msg });
|
|
2768
|
+
this.emitEvent({
|
|
2769
|
+
type: "read",
|
|
2770
|
+
payload: Object.fromEntries(
|
|
2771
|
+
Object.entries(payload).map(([k, v]) => [k, v.value])
|
|
2772
|
+
),
|
|
2773
|
+
blockId: `stop_${this.stopCount}`
|
|
2774
|
+
});
|
|
2775
|
+
this.emitEvent({ type: "scope", entries: this.sandbox.getScope() });
|
|
2776
|
+
const turnCode = this.codeLines.slice(this.turnCodeStart);
|
|
2777
|
+
this.recorder.recordStop(turnCode, payload, this.sandbox.getScope(), cpState);
|
|
2778
|
+
this.turnCodeStart = this.codeLines.length;
|
|
2779
|
+
}
|
|
2780
|
+
handleError(error) {
|
|
2781
|
+
const msg = buildErrorMessage(error);
|
|
2782
|
+
this.messages.push({ role: "assistant", content: this.codeLines.join("\n") });
|
|
2783
|
+
this.messages.push({ role: "user", content: msg });
|
|
2784
|
+
this.emitEvent({ type: "scope", entries: this.sandbox.getScope() });
|
|
2785
|
+
const turnCode = this.codeLines.slice(this.turnCodeStart);
|
|
2786
|
+
this.recorder.recordError(turnCode, error, this.sandbox.getScope());
|
|
2787
|
+
this.turnCodeStart = this.codeLines.length;
|
|
2788
|
+
}
|
|
2789
|
+
/**
|
|
2790
|
+
* Handle a user message.
|
|
2791
|
+
*/
|
|
2792
|
+
async handleUserMessage(text) {
|
|
2793
|
+
this.setStatus("executing");
|
|
2794
|
+
this.messages.push({ role: "user", content: text });
|
|
2795
|
+
this.recorder.recordUserMessage(text, this.sandbox.getScope());
|
|
2796
|
+
}
|
|
2797
|
+
/**
|
|
2798
|
+
* Feed tokens from the LLM stream.
|
|
2799
|
+
*/
|
|
2800
|
+
async feedToken(token) {
|
|
2801
|
+
await this.streamController.feedToken(token);
|
|
2802
|
+
}
|
|
2803
|
+
/**
|
|
2804
|
+
* Finalize the LLM stream.
|
|
2805
|
+
* Returns 'complete' if done, or 'tasklist_incomplete' if tasks remain.
|
|
2806
|
+
*/
|
|
2807
|
+
async finalize() {
|
|
2808
|
+
await this.streamController.finalize();
|
|
2809
|
+
const cpState = this.globalsApi.getTasklistsState();
|
|
2810
|
+
for (const [tasklistId, tasklist] of cpState.tasklists) {
|
|
2811
|
+
const hasRequiredIncomplete = tasklist.plan.tasks.some((t) => {
|
|
2812
|
+
const completion = tasklist.completed.get(t.id);
|
|
2813
|
+
const isIncomplete = !completion || completion.status !== "completed" && completion.status !== "skipped";
|
|
2814
|
+
return isIncomplete && !t.optional;
|
|
2815
|
+
});
|
|
2816
|
+
if (!hasRequiredIncomplete) continue;
|
|
2817
|
+
if (tasklist.runningTasks.size > 0) {
|
|
2818
|
+
await Promise.race([
|
|
2819
|
+
Promise.allSettled(
|
|
2820
|
+
[...tasklist.runningTasks].map(
|
|
2821
|
+
(id) => new Promise((resolve) => {
|
|
2822
|
+
const check = () => {
|
|
2823
|
+
if (!tasklist.runningTasks.has(id)) {
|
|
2824
|
+
resolve();
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
setTimeout(check, 100);
|
|
2828
|
+
};
|
|
2829
|
+
check();
|
|
2830
|
+
})
|
|
2831
|
+
)
|
|
2832
|
+
),
|
|
2833
|
+
new Promise((resolve) => setTimeout(resolve, this.config.taskAsyncTimeout))
|
|
2834
|
+
]);
|
|
2835
|
+
}
|
|
2836
|
+
if (this.tasklistReminderCount < this.config.maxTasklistReminders) {
|
|
2837
|
+
this.tasklistReminderCount++;
|
|
2838
|
+
const ready = [...tasklist.readyTasks];
|
|
2839
|
+
const blocked = tasklist.plan.tasks.filter((t) => !tasklist.readyTasks.has(t.id) && !tasklist.completed.has(t.id) && !tasklist.runningTasks.has(t.id)).map((t) => `${t.id} (waiting on ${(t.dependsOn ?? []).join(", ")})`);
|
|
2840
|
+
const failed = [...tasklist.completed.entries()].filter(([_, c]) => c.status === "failed").map(([id]) => id);
|
|
2841
|
+
const msg = buildTasklistReminderMessage(tasklistId, ready, blocked, failed);
|
|
2842
|
+
const tasksBlock = generateTasksBlock(cpState);
|
|
2843
|
+
const fullMsg = tasksBlock ? `${msg}
|
|
2844
|
+
|
|
2845
|
+
${tasksBlock}` : msg;
|
|
2846
|
+
this.messages.push({ role: "assistant", content: this.codeLines.join("\n") });
|
|
2847
|
+
this.messages.push({ role: "user", content: fullMsg });
|
|
2848
|
+
const blockedIds = blocked.map((b) => b.split(" ")[0]);
|
|
2849
|
+
this.recorder.recordTasklistReminder(
|
|
2850
|
+
[...this.codeLines],
|
|
2851
|
+
tasklistId,
|
|
2852
|
+
ready,
|
|
2853
|
+
blockedIds,
|
|
2854
|
+
failed,
|
|
2855
|
+
this.sandbox.getScope(),
|
|
2856
|
+
cpState
|
|
2857
|
+
);
|
|
2858
|
+
this.codeLines = [];
|
|
2859
|
+
this.turnCodeStart = 0;
|
|
2860
|
+
this.emitEvent({ type: "tasklist_reminder", tasklistId, ready, blocked: blockedIds, failed });
|
|
2861
|
+
this.emitEvent({ type: "scope", entries: this.sandbox.getScope() });
|
|
2862
|
+
return "tasklist_incomplete";
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
await this.asyncManager.drain(5e3);
|
|
2866
|
+
const turnCode = this.codeLines.slice(this.turnCodeStart);
|
|
2867
|
+
this.recorder.recordCompletion(turnCode, this.sandbox.getScope(), this.globalsApi.getTasklistsState(), "complete");
|
|
2868
|
+
this.turnCodeStart = this.codeLines.length;
|
|
2869
|
+
this.setStatus("complete");
|
|
2870
|
+
return "complete";
|
|
2871
|
+
}
|
|
2872
|
+
/**
|
|
2873
|
+
* Resolve a pending stop() call, allowing sandbox to continue.
|
|
2874
|
+
* Called by the runner after injecting the stop payload as a user message.
|
|
2875
|
+
*/
|
|
2876
|
+
resolveStop() {
|
|
2877
|
+
this.globalsApi.resolveStop();
|
|
2878
|
+
this.streamController.resume();
|
|
2879
|
+
}
|
|
2880
|
+
/**
|
|
2881
|
+
* Inject a value into the sandbox as a global.
|
|
2882
|
+
* Used to inject class namespace objects after loadClass().
|
|
2883
|
+
*/
|
|
2884
|
+
injectGlobal(name, value) {
|
|
2885
|
+
this.sandbox.inject(name, value);
|
|
2886
|
+
}
|
|
2887
|
+
/**
|
|
2888
|
+
* Resolve a pending ask() form.
|
|
2889
|
+
*/
|
|
2890
|
+
resolveAsk(formId, data) {
|
|
2891
|
+
const hasListener = this.listenerCount(`form:${formId}`) > 0;
|
|
2892
|
+
console.log(`\x1B[90m [session] resolveAsk ${formId} hasListener=${hasListener} activeFormId=${this.activeFormId}\x1B[0m`);
|
|
2893
|
+
this.emit(`form:${formId}`, data);
|
|
2894
|
+
}
|
|
2895
|
+
/**
|
|
2896
|
+
* Cancel a pending ask() form.
|
|
2897
|
+
*/
|
|
2898
|
+
cancelAsk(formId) {
|
|
2899
|
+
this.emit(`form:${formId}`, { _cancelled: true });
|
|
2900
|
+
}
|
|
2901
|
+
/**
|
|
2902
|
+
* Cancel an async task.
|
|
2903
|
+
*/
|
|
2904
|
+
cancelAsyncTask(taskId, message = "") {
|
|
2905
|
+
this.asyncManager.cancel(taskId, message);
|
|
2906
|
+
this.emitEvent({ type: "async_cancelled", taskId });
|
|
2907
|
+
}
|
|
2908
|
+
/**
|
|
2909
|
+
* Pause the session.
|
|
2910
|
+
*/
|
|
2911
|
+
pause() {
|
|
2912
|
+
this.streamController.pause();
|
|
2913
|
+
this.setStatus("paused");
|
|
2914
|
+
}
|
|
2915
|
+
/**
|
|
2916
|
+
* Resume the session.
|
|
2917
|
+
*/
|
|
2918
|
+
resume() {
|
|
2919
|
+
this.streamController.resume();
|
|
2920
|
+
this.setStatus("executing");
|
|
2921
|
+
}
|
|
2922
|
+
/**
|
|
2923
|
+
* Handle user intervention (message while agent is running).
|
|
2924
|
+
*/
|
|
2925
|
+
handleIntervention(text) {
|
|
2926
|
+
if (this.activeFormId) {
|
|
2927
|
+
this.cancelAsk(this.activeFormId);
|
|
2928
|
+
}
|
|
2929
|
+
this.streamController.pause();
|
|
2930
|
+
const msg = buildInterventionMessage(text);
|
|
2931
|
+
this.messages.push({ role: "assistant", content: this.codeLines.join("\n") });
|
|
2932
|
+
this.messages.push({ role: "user", content: msg });
|
|
2933
|
+
this.recorder.recordIntervention([...this.codeLines], text, this.sandbox.getScope());
|
|
2934
|
+
this.codeLines = [];
|
|
2935
|
+
this.turnCodeStart = 0;
|
|
2936
|
+
this.emitEvent({ type: "scope", entries: this.sandbox.getScope() });
|
|
2937
|
+
this.streamController.resume();
|
|
2938
|
+
}
|
|
2939
|
+
/**
|
|
2940
|
+
* Get a snapshot of the current session state.
|
|
2941
|
+
*/
|
|
2942
|
+
snapshot() {
|
|
2943
|
+
return {
|
|
2944
|
+
status: this.status,
|
|
2945
|
+
blocks: [...this.blocks],
|
|
2946
|
+
scope: this.sandbox.getScope(),
|
|
2947
|
+
asyncTasks: this.asyncManager.getAllTasks().map((t) => ({
|
|
2948
|
+
id: t.id,
|
|
2949
|
+
label: t.label,
|
|
2950
|
+
status: t.status,
|
|
2951
|
+
elapsed: Date.now() - t.startTime
|
|
2952
|
+
})),
|
|
2953
|
+
activeFormId: this.activeFormId,
|
|
2954
|
+
tasklistsState: this.globalsApi.getTasklistsState(),
|
|
2955
|
+
agentEntries: this.agentRegistry.getAll().map((e) => ({
|
|
2956
|
+
varName: e.varName,
|
|
2957
|
+
label: e.label,
|
|
2958
|
+
status: e.status,
|
|
2959
|
+
error: e.error
|
|
2960
|
+
}))
|
|
2961
|
+
};
|
|
2962
|
+
}
|
|
2963
|
+
/**
|
|
2964
|
+
* Get the full serializable conversation state.
|
|
2965
|
+
*/
|
|
2966
|
+
getConversationState() {
|
|
2967
|
+
return this.recorder.getState();
|
|
2968
|
+
}
|
|
2969
|
+
/**
|
|
2970
|
+
* Get the current status.
|
|
2971
|
+
*/
|
|
2972
|
+
getStatus() {
|
|
2973
|
+
return this.status;
|
|
2974
|
+
}
|
|
2975
|
+
/**
|
|
2976
|
+
* Get messages for context.
|
|
2977
|
+
*/
|
|
2978
|
+
getMessages() {
|
|
2979
|
+
return this.messages;
|
|
2980
|
+
}
|
|
2981
|
+
/**
|
|
2982
|
+
* Get the public globals object (for passing to setup functions).
|
|
2983
|
+
*/
|
|
2984
|
+
getGlobals() {
|
|
2985
|
+
return {
|
|
2986
|
+
stop: this.globalsApi.stop,
|
|
2987
|
+
display: this.globalsApi.display,
|
|
2988
|
+
ask: this.globalsApi.ask,
|
|
2989
|
+
async: this.globalsApi.async,
|
|
2990
|
+
tasklist: this.globalsApi.tasklist,
|
|
2991
|
+
completeTask: this.globalsApi.completeTask,
|
|
2992
|
+
completeTaskAsync: this.globalsApi.completeTaskAsync,
|
|
2993
|
+
taskProgress: this.globalsApi.taskProgress,
|
|
2994
|
+
failTask: this.globalsApi.failTask,
|
|
2995
|
+
retryTask: this.globalsApi.retryTask,
|
|
2996
|
+
sleep: this.globalsApi.sleep,
|
|
2997
|
+
loadKnowledge: this.globalsApi.loadKnowledge,
|
|
2998
|
+
loadClass: this.globalsApi.loadClass,
|
|
2999
|
+
askParent: this.globalsApi.askParent,
|
|
3000
|
+
respond: this.globalsApi.respond
|
|
3001
|
+
};
|
|
3002
|
+
}
|
|
3003
|
+
/**
|
|
3004
|
+
* Get the agent registry.
|
|
3005
|
+
*/
|
|
3006
|
+
getAgentRegistry() {
|
|
3007
|
+
return this.agentRegistry;
|
|
3008
|
+
}
|
|
3009
|
+
/**
|
|
3010
|
+
* Get scope table as string.
|
|
3011
|
+
*/
|
|
3012
|
+
getScopeTable() {
|
|
3013
|
+
return generateScopeTable(this.sandbox.getScope(), {
|
|
3014
|
+
maxVariables: this.config.workspace.maxScopeVariables,
|
|
3015
|
+
maxValueWidth: this.config.workspace.maxScopeValueWidth
|
|
3016
|
+
});
|
|
3017
|
+
}
|
|
3018
|
+
setStatus(status) {
|
|
3019
|
+
this.status = status;
|
|
3020
|
+
this.emitEvent({ type: "status", status });
|
|
3021
|
+
}
|
|
3022
|
+
emitEvent(event) {
|
|
3023
|
+
this.emit("event", event);
|
|
3024
|
+
}
|
|
3025
|
+
/**
|
|
3026
|
+
* Destroy the session and clean up resources.
|
|
3027
|
+
*/
|
|
3028
|
+
destroy() {
|
|
3029
|
+
this.agentRegistry.destroy();
|
|
3030
|
+
this.asyncManager.cancelAll();
|
|
3031
|
+
this.sandbox.destroy();
|
|
3032
|
+
this.hookRegistry.clear();
|
|
3033
|
+
this.removeAllListeners();
|
|
3034
|
+
}
|
|
3035
|
+
};
|
|
3036
|
+
var CLIENT_COMPONENTS = /* @__PURE__ */ new Set([
|
|
3037
|
+
"TextInput",
|
|
3038
|
+
"TextArea",
|
|
3039
|
+
"NumberInput",
|
|
3040
|
+
"Slider",
|
|
3041
|
+
"Checkbox",
|
|
3042
|
+
"Select",
|
|
3043
|
+
"MultiSelect",
|
|
3044
|
+
"DatePicker",
|
|
3045
|
+
"FileUpload"
|
|
3046
|
+
]);
|
|
3047
|
+
function serializeReactElement(element, depth = 0) {
|
|
3048
|
+
if (depth > 20) return { component: "div", props: {}, children: ["[max depth]"] };
|
|
3049
|
+
if (!element || typeof element !== "object" || !("type" in element)) {
|
|
3050
|
+
return { component: "span", props: {}, children: [String(element ?? "")] };
|
|
3051
|
+
}
|
|
3052
|
+
const el = element;
|
|
3053
|
+
const { children, ...restProps } = el.props ?? {};
|
|
3054
|
+
let component;
|
|
3055
|
+
if (typeof el.type === "string") {
|
|
3056
|
+
component = el.type;
|
|
3057
|
+
} else if (typeof el.type === "function") {
|
|
3058
|
+
const name = el.type.name || "";
|
|
3059
|
+
if (CLIENT_COMPONENTS.has(name)) {
|
|
3060
|
+
component = name;
|
|
3061
|
+
} else {
|
|
3062
|
+
const _consoleError = console.error;
|
|
3063
|
+
try {
|
|
3064
|
+
console.error = () => {
|
|
3065
|
+
};
|
|
3066
|
+
const rendered = el.type(el.props);
|
|
3067
|
+
return serializeReactElement(rendered, depth + 1);
|
|
3068
|
+
} catch {
|
|
3069
|
+
component = name || "div";
|
|
3070
|
+
} finally {
|
|
3071
|
+
console.error = _consoleError;
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
} else {
|
|
3075
|
+
component = "div";
|
|
3076
|
+
}
|
|
3077
|
+
const safeProps = {};
|
|
3078
|
+
for (const [key, value] of Object.entries(restProps)) {
|
|
3079
|
+
if (typeof value === "function") continue;
|
|
3080
|
+
if (typeof value === "symbol") continue;
|
|
3081
|
+
safeProps[key] = value;
|
|
3082
|
+
}
|
|
3083
|
+
const serializedChildren = serializeChildren(children, depth);
|
|
3084
|
+
return { component, props: safeProps, children: serializedChildren.length > 0 ? serializedChildren : void 0 };
|
|
3085
|
+
}
|
|
3086
|
+
function serializeChildren(children, depth) {
|
|
3087
|
+
if (children == null) return [];
|
|
3088
|
+
if (typeof children === "string") return [children];
|
|
3089
|
+
if (typeof children === "number" || typeof children === "boolean") return [String(children)];
|
|
3090
|
+
if (Array.isArray(children)) {
|
|
3091
|
+
return children.flatMap((child) => serializeChildren(child, depth));
|
|
3092
|
+
}
|
|
3093
|
+
if (typeof children === "object" && "type" in children) {
|
|
3094
|
+
return [serializeReactElement(children, depth + 1)];
|
|
3095
|
+
}
|
|
3096
|
+
return [String(children)];
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
// src/parser/global-detector.ts
|
|
3100
|
+
var GLOBALS = ["stop", "display", "ask", "async", "tasklist", "completeTask", "loadKnowledge"];
|
|
3101
|
+
function detectGlobalCall(source) {
|
|
3102
|
+
const trimmed = source.trim();
|
|
3103
|
+
const withoutAwait = trimmed.startsWith("await ") ? trimmed.slice(6).trim() : trimmed;
|
|
3104
|
+
for (const name of GLOBALS) {
|
|
3105
|
+
if (withoutAwait.startsWith(name + "(") || withoutAwait.startsWith(name + " (")) {
|
|
3106
|
+
return name;
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
const assignMatch = trimmed.match(/^(?:const|let|var)\s+\w+\s*=\s*(?:await\s+)?(\w+)\s*\(/);
|
|
3110
|
+
if (assignMatch) {
|
|
3111
|
+
const callee = assignMatch[1];
|
|
3112
|
+
if (GLOBALS.includes(callee)) {
|
|
3113
|
+
return callee;
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
return null;
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
// src/context/code-window.ts
|
|
3120
|
+
function compressCodeWindow(turns, maxLines) {
|
|
3121
|
+
if (turns.length === 0) return [];
|
|
3122
|
+
let totalLines = turns.reduce((sum, t) => sum + t.lines.length, 0);
|
|
3123
|
+
if (totalLines <= maxLines) {
|
|
3124
|
+
return turns.flatMap((t) => t.lines);
|
|
3125
|
+
}
|
|
3126
|
+
const result = [];
|
|
3127
|
+
const summaries = [];
|
|
3128
|
+
let linesRemaining = maxLines;
|
|
3129
|
+
for (let i = turns.length - 1; i >= 0; i--) {
|
|
3130
|
+
const turn = turns[i];
|
|
3131
|
+
if (linesRemaining >= turn.lines.length) {
|
|
3132
|
+
result.unshift(...turn.lines);
|
|
3133
|
+
linesRemaining -= turn.lines.length;
|
|
3134
|
+
} else {
|
|
3135
|
+
const startLine = turn.turnIndex;
|
|
3136
|
+
const endLine = startLine + turn.lines.length - 1;
|
|
3137
|
+
const declList = turn.declarations.length > 0 ? ` declared: ${turn.declarations.join(", ")}` : "";
|
|
3138
|
+
summaries.unshift(`// [lines ${startLine}-${endLine} executed]${declList}`);
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
return [...summaries, ...result];
|
|
3142
|
+
}
|
|
3143
|
+
function buildSummaryComment(startLine, endLine, declarations) {
|
|
3144
|
+
const declList = declarations.length > 0 ? ` declared: ${declarations.join(", ")}` : "";
|
|
3145
|
+
return `// [lines ${startLine}-${endLine} executed]${declList}`;
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
// src/context/stop-decay.ts
|
|
3149
|
+
var DEFAULT_TIERS2 = { full: 2, keysOnly: 5, summary: 10 };
|
|
3150
|
+
function getDecayLevel(distance, tiers = DEFAULT_TIERS2) {
|
|
3151
|
+
if (distance <= tiers.full) return "full";
|
|
3152
|
+
if (distance <= tiers.keysOnly) return "keys";
|
|
3153
|
+
if (distance <= tiers.summary) return "count";
|
|
3154
|
+
return "removed";
|
|
3155
|
+
}
|
|
3156
|
+
function decayStopPayload(payload, distance, tiers = DEFAULT_TIERS2) {
|
|
3157
|
+
const level = getDecayLevel(distance, tiers);
|
|
3158
|
+
switch (level) {
|
|
3159
|
+
case "full":
|
|
3160
|
+
return formatFullPayload(payload);
|
|
3161
|
+
case "keys":
|
|
3162
|
+
return formatKeysPayload(payload);
|
|
3163
|
+
case "count":
|
|
3164
|
+
return formatCountPayload(payload);
|
|
3165
|
+
case "removed":
|
|
3166
|
+
return null;
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
function formatFullPayload(payload) {
|
|
3170
|
+
const entries = Object.entries(payload).map(([key, sv]) => `${key}: ${sv.display}`);
|
|
3171
|
+
return `\u2190 stop { ${entries.join(", ")} }`;
|
|
3172
|
+
}
|
|
3173
|
+
function formatKeysPayload(payload) {
|
|
3174
|
+
const entries = Object.entries(payload).map(([key, sv]) => {
|
|
3175
|
+
const type = describeValueType(sv);
|
|
3176
|
+
return `${key}: ${type}`;
|
|
3177
|
+
});
|
|
3178
|
+
return `\u2190 stop { ${entries.join(", ")} }`;
|
|
3179
|
+
}
|
|
3180
|
+
function formatCountPayload(payload) {
|
|
3181
|
+
const count = Object.keys(payload).length;
|
|
3182
|
+
return `\u2190 stop (${count} value${count === 1 ? "" : "s"} read)`;
|
|
3183
|
+
}
|
|
3184
|
+
function describeValueType(sv) {
|
|
3185
|
+
const val = sv.value;
|
|
3186
|
+
if (val === null) return "null";
|
|
3187
|
+
if (val === void 0) return "undefined";
|
|
3188
|
+
if (Array.isArray(val)) return `Array(${val.length})`;
|
|
3189
|
+
if (typeof val === "object") {
|
|
3190
|
+
const keys = Object.keys(val);
|
|
3191
|
+
return `Object{${keys.join(",")}}`;
|
|
3192
|
+
}
|
|
3193
|
+
return typeof val;
|
|
3194
|
+
}
|
|
3195
|
+
function decayErrorMessage(errorMsg, distance, tiers = DEFAULT_TIERS2) {
|
|
3196
|
+
const level = getDecayLevel(distance, tiers);
|
|
3197
|
+
if (level === "removed") return null;
|
|
3198
|
+
if (level === "count") return "\u2190 error (1 error occurred)";
|
|
3199
|
+
return errorMsg;
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
// src/context/system-prompt.ts
|
|
3203
|
+
function buildSystemPrompt(template, slots) {
|
|
3204
|
+
let result = template;
|
|
3205
|
+
for (const [key, value] of Object.entries(slots)) {
|
|
3206
|
+
const marker = `{{${key}}}`;
|
|
3207
|
+
result = result.replaceAll(marker, value);
|
|
3208
|
+
}
|
|
3209
|
+
return result;
|
|
3210
|
+
}
|
|
3211
|
+
function updateScopeInPrompt(systemPrompt, scopeTable) {
|
|
3212
|
+
const scopeStart = systemPrompt.indexOf("{{SCOPE}}");
|
|
3213
|
+
if (scopeStart === -1) {
|
|
3214
|
+
return systemPrompt;
|
|
3215
|
+
}
|
|
3216
|
+
return systemPrompt.replace(
|
|
3217
|
+
/\{\{SCOPE\}\}[\s\S]*?(?=\{\{|$)/,
|
|
3218
|
+
scopeTable + "\n\n"
|
|
3219
|
+
);
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
// src/security/function-registry.ts
|
|
3223
|
+
function wrapFunction(name, fn, options = {}) {
|
|
3224
|
+
const timeout = options.timeout ?? 3e4;
|
|
3225
|
+
const rateState = options.rateLimit ? { calls: [] } : null;
|
|
3226
|
+
const wrapped = async function(...args) {
|
|
3227
|
+
if (rateState && options.rateLimit) {
|
|
3228
|
+
const now = Date.now();
|
|
3229
|
+
const { maxCalls, windowMs } = options.rateLimit;
|
|
3230
|
+
rateState.calls = rateState.calls.filter((t) => now - t < windowMs);
|
|
3231
|
+
if (rateState.calls.length >= maxCalls) {
|
|
3232
|
+
throw new Error(`Rate limit exceeded for ${name}: max ${maxCalls} calls per ${windowMs}ms`);
|
|
3233
|
+
}
|
|
3234
|
+
rateState.calls.push(now);
|
|
3235
|
+
}
|
|
3236
|
+
const start = Date.now();
|
|
3237
|
+
const result = await Promise.race([
|
|
3238
|
+
Promise.resolve(fn(...args)),
|
|
3239
|
+
new Promise(
|
|
3240
|
+
(_, reject) => setTimeout(() => reject(new Error(`Timeout: ${name} exceeded ${timeout}ms`)), timeout)
|
|
3241
|
+
)
|
|
3242
|
+
]);
|
|
3243
|
+
const duration = Date.now() - start;
|
|
3244
|
+
options.onCall?.(name, args, duration);
|
|
3245
|
+
return result;
|
|
3246
|
+
};
|
|
3247
|
+
Object.defineProperty(wrapped, "name", { value: name });
|
|
3248
|
+
return wrapped;
|
|
3249
|
+
}
|
|
3250
|
+
var FunctionRegistry = class {
|
|
3251
|
+
functions = /* @__PURE__ */ new Map();
|
|
3252
|
+
options;
|
|
3253
|
+
constructor(options = {}) {
|
|
3254
|
+
this.options = options;
|
|
3255
|
+
}
|
|
3256
|
+
register(name, fn) {
|
|
3257
|
+
this.functions.set(name, wrapFunction(name, fn, this.options));
|
|
3258
|
+
}
|
|
3259
|
+
get(name) {
|
|
3260
|
+
return this.functions.get(name);
|
|
3261
|
+
}
|
|
3262
|
+
getAll() {
|
|
3263
|
+
const result = {};
|
|
3264
|
+
for (const [name, fn] of this.functions) {
|
|
3265
|
+
result[name] = fn;
|
|
3266
|
+
}
|
|
3267
|
+
return result;
|
|
3268
|
+
}
|
|
3269
|
+
has(name) {
|
|
3270
|
+
return this.functions.has(name);
|
|
3271
|
+
}
|
|
3272
|
+
names() {
|
|
3273
|
+
return [...this.functions.keys()];
|
|
3274
|
+
}
|
|
3275
|
+
};
|
|
3276
|
+
|
|
3277
|
+
// src/security/jsx-sanitizer.ts
|
|
3278
|
+
var BLOCKED_TAGS = /* @__PURE__ */ new Set(["script", "iframe", "object", "embed"]);
|
|
3279
|
+
var DANGEROUS_PROPS = /* @__PURE__ */ new Set(["dangerouslySetInnerHTML"]);
|
|
3280
|
+
var JAVASCRIPT_URL_PATTERN = /^\s*javascript:/i;
|
|
3281
|
+
function sanitizeJSX(jsx, path = "root") {
|
|
3282
|
+
const errors = [];
|
|
3283
|
+
if (BLOCKED_TAGS.has(jsx.component.toLowerCase())) {
|
|
3284
|
+
errors.push({
|
|
3285
|
+
path,
|
|
3286
|
+
message: `Blocked element: <${jsx.component}> is not allowed`
|
|
3287
|
+
});
|
|
3288
|
+
}
|
|
3289
|
+
for (const [key, value] of Object.entries(jsx.props)) {
|
|
3290
|
+
if (DANGEROUS_PROPS.has(key)) {
|
|
3291
|
+
errors.push({
|
|
3292
|
+
path: `${path}.props.${key}`,
|
|
3293
|
+
message: `Dangerous prop: ${key} is not allowed`
|
|
3294
|
+
});
|
|
3295
|
+
}
|
|
3296
|
+
if ((key === "href" || key === "src" || key === "action") && typeof value === "string" && JAVASCRIPT_URL_PATTERN.test(value)) {
|
|
3297
|
+
errors.push({
|
|
3298
|
+
path: `${path}.props.${key}`,
|
|
3299
|
+
message: `javascript: URLs are not allowed in ${key}`
|
|
3300
|
+
});
|
|
3301
|
+
}
|
|
3302
|
+
if (key.startsWith("on") && key.length > 2 && key[2] === key[2].toUpperCase()) {
|
|
3303
|
+
if (typeof value === "string") {
|
|
3304
|
+
errors.push({
|
|
3305
|
+
path: `${path}.props.${key}`,
|
|
3306
|
+
message: `String event handler: ${key} must be a function, not a string`
|
|
3307
|
+
});
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
if (jsx.children) {
|
|
3312
|
+
for (let i = 0; i < jsx.children.length; i++) {
|
|
3313
|
+
const child = jsx.children[i];
|
|
3314
|
+
if (typeof child === "string") continue;
|
|
3315
|
+
const childErrors = sanitizeJSX(child, `${path}.children[${i}]`);
|
|
3316
|
+
errors.push(...childErrors);
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
return errors;
|
|
3320
|
+
}
|
|
3321
|
+
function isJSXSafe(jsx) {
|
|
3322
|
+
return sanitizeJSX(jsx).length === 0;
|
|
3323
|
+
}
|
|
3324
|
+
function validateFormComponents(jsx, allowedComponents, path = "root") {
|
|
3325
|
+
const errors = [];
|
|
3326
|
+
if (path === "root" && jsx.component !== "Form" && jsx.component !== "form") {
|
|
3327
|
+
errors.push({
|
|
3328
|
+
path,
|
|
3329
|
+
message: "ask() root must be a <Form> component"
|
|
3330
|
+
});
|
|
3331
|
+
}
|
|
3332
|
+
if (jsx.children) {
|
|
3333
|
+
for (let i = 0; i < jsx.children.length; i++) {
|
|
3334
|
+
const child = jsx.children[i];
|
|
3335
|
+
if (typeof child === "string") continue;
|
|
3336
|
+
if (!allowedComponents.has(child.component) && child.component !== "Form" && child.component !== "form") {
|
|
3337
|
+
errors.push({
|
|
3338
|
+
path: `${path}.children[${i}]`,
|
|
3339
|
+
message: `Unknown form component: <${child.component}>. Only registered input components are allowed.`
|
|
3340
|
+
});
|
|
3341
|
+
}
|
|
3342
|
+
const childErrors = validateFormComponents(child, allowedComponents, `${path}.children[${i}]`);
|
|
3343
|
+
errors.push(...childErrors);
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
return errors;
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
// src/catalog/index.ts
|
|
3350
|
+
var BUILTIN_MODULE_IDS = [
|
|
3351
|
+
"path",
|
|
3352
|
+
"date",
|
|
3353
|
+
"crypto",
|
|
3354
|
+
"json",
|
|
3355
|
+
"csv",
|
|
3356
|
+
"env",
|
|
3357
|
+
"fs",
|
|
3358
|
+
"fetch",
|
|
3359
|
+
"shell",
|
|
3360
|
+
"image",
|
|
3361
|
+
"db"
|
|
3362
|
+
];
|
|
3363
|
+
async function loadCatalog(moduleIds) {
|
|
3364
|
+
const ids = moduleIds === "all" ? [...BUILTIN_MODULE_IDS] : moduleIds;
|
|
3365
|
+
const modules = [];
|
|
3366
|
+
for (const id of ids) {
|
|
3367
|
+
if (!isBuiltinModule(id)) {
|
|
3368
|
+
throw new Error(`Unknown catalog module: ${id}`);
|
|
3369
|
+
}
|
|
3370
|
+
const mod = await importModule(id);
|
|
3371
|
+
modules.push(mod);
|
|
3372
|
+
}
|
|
3373
|
+
return modules;
|
|
3374
|
+
}
|
|
3375
|
+
function isBuiltinModule(id) {
|
|
3376
|
+
return BUILTIN_MODULE_IDS.includes(id);
|
|
3377
|
+
}
|
|
3378
|
+
async function importModule(id) {
|
|
3379
|
+
switch (id) {
|
|
3380
|
+
case "path":
|
|
3381
|
+
return (await import("./path-K3VLZVTH.js")).default;
|
|
3382
|
+
case "date":
|
|
3383
|
+
return (await import("./date-DL6PIRCI.js")).default;
|
|
3384
|
+
case "crypto":
|
|
3385
|
+
return (await import("./crypto-BTVPQPPV.js")).default;
|
|
3386
|
+
case "json":
|
|
3387
|
+
return (await import("./json-VPTMTXR6.js")).default;
|
|
3388
|
+
case "csv":
|
|
3389
|
+
return (await import("./csv-3IL3IKAY.js")).default;
|
|
3390
|
+
case "env":
|
|
3391
|
+
return (await import("./env-PPLEQ47H.js")).default;
|
|
3392
|
+
case "fs":
|
|
3393
|
+
return (await import("./fs-QRXDDXB5.js")).default;
|
|
3394
|
+
case "fetch":
|
|
3395
|
+
return (await import("./fetch-TN4ZJVQW.js")).default;
|
|
3396
|
+
case "shell":
|
|
3397
|
+
return (await import("./shell-NAGRIUA4.js")).default;
|
|
3398
|
+
case "image":
|
|
3399
|
+
return (await import("./image-FGNY3CMJ.js")).default;
|
|
3400
|
+
case "db":
|
|
3401
|
+
return (await import("./db-IVNVHDFE.js")).default;
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
function mergeCatalogs(modules) {
|
|
3405
|
+
return modules.flatMap((m) => m.functions);
|
|
3406
|
+
}
|
|
3407
|
+
function getCatalogModule(modules, id) {
|
|
3408
|
+
return modules.find((m) => m.id === id);
|
|
3409
|
+
}
|
|
3410
|
+
function formatCatalogForPrompt(modules) {
|
|
3411
|
+
const lines = [];
|
|
3412
|
+
for (const mod of modules) {
|
|
3413
|
+
lines.push(` # Built-in: ${mod.id}`);
|
|
3414
|
+
for (const fn of mod.functions) {
|
|
3415
|
+
lines.push(` ${fn.name}${fn.signature}`);
|
|
3416
|
+
lines.push(` \u2014 ${fn.description}`);
|
|
3417
|
+
}
|
|
3418
|
+
lines.push("");
|
|
3419
|
+
}
|
|
3420
|
+
return lines.join("\n");
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
// src/knowledge/index.ts
|
|
3424
|
+
import { readFileSync, readdirSync, existsSync } from "fs";
|
|
3425
|
+
import { join } from "path";
|
|
3426
|
+
function mergeKnowledgeTrees(trees) {
|
|
3427
|
+
const domainMap = /* @__PURE__ */ new Map();
|
|
3428
|
+
for (const tree of trees) {
|
|
3429
|
+
for (const domain of tree.domains) {
|
|
3430
|
+
const existing = domainMap.get(domain.slug);
|
|
3431
|
+
if (!existing) {
|
|
3432
|
+
domainMap.set(domain.slug, { ...domain, fields: [...domain.fields] });
|
|
3433
|
+
} else {
|
|
3434
|
+
const fieldSlugs = new Set(existing.fields.map((f) => f.slug));
|
|
3435
|
+
for (const field of domain.fields) {
|
|
3436
|
+
if (!fieldSlugs.has(field.slug)) {
|
|
3437
|
+
existing.fields.push(field);
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
return { domains: [...domainMap.values()] };
|
|
3444
|
+
}
|
|
3445
|
+
function buildKnowledgeTree(knowledgeDir) {
|
|
3446
|
+
if (!existsSync(knowledgeDir)) {
|
|
3447
|
+
return { domains: [] };
|
|
3448
|
+
}
|
|
3449
|
+
const domains = [];
|
|
3450
|
+
const entries = readdirSync(knowledgeDir, { withFileTypes: true });
|
|
3451
|
+
for (const entry of entries) {
|
|
3452
|
+
if (!entry.isDirectory()) continue;
|
|
3453
|
+
const domainPath = join(knowledgeDir, entry.name);
|
|
3454
|
+
const domain = readDomain(domainPath, entry.name);
|
|
3455
|
+
if (domain) domains.push(domain);
|
|
3456
|
+
}
|
|
3457
|
+
return { domains };
|
|
3458
|
+
}
|
|
3459
|
+
function readDomain(domainPath, slug) {
|
|
3460
|
+
const configPath = join(domainPath, "config.json");
|
|
3461
|
+
if (!existsSync(configPath)) return null;
|
|
3462
|
+
try {
|
|
3463
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
3464
|
+
const fields = [];
|
|
3465
|
+
const entries = readdirSync(domainPath, { withFileTypes: true });
|
|
3466
|
+
for (const entry of entries) {
|
|
3467
|
+
if (!entry.isDirectory()) continue;
|
|
3468
|
+
const field = readField(join(domainPath, entry.name), entry.name);
|
|
3469
|
+
if (field) fields.push(field);
|
|
3470
|
+
}
|
|
3471
|
+
return {
|
|
3472
|
+
slug,
|
|
3473
|
+
label: config.label ?? slug,
|
|
3474
|
+
description: config.description ?? "",
|
|
3475
|
+
icon: config.icon ?? "",
|
|
3476
|
+
color: config.color ?? "#888888",
|
|
3477
|
+
fields
|
|
3478
|
+
};
|
|
3479
|
+
} catch {
|
|
3480
|
+
return null;
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
function readField(fieldPath, slug) {
|
|
3484
|
+
const configPath = join(fieldPath, "config.json");
|
|
3485
|
+
if (!existsSync(configPath)) return null;
|
|
3486
|
+
try {
|
|
3487
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
3488
|
+
const options = [];
|
|
3489
|
+
const entries = readdirSync(fieldPath, { withFileTypes: true });
|
|
3490
|
+
for (const entry of entries) {
|
|
3491
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
3492
|
+
const optionSlug = entry.name.replace(/\.md$/, "");
|
|
3493
|
+
const option = readOptionMeta(join(fieldPath, entry.name), optionSlug);
|
|
3494
|
+
if (option) options.push(option);
|
|
3495
|
+
}
|
|
3496
|
+
options.sort((a, b) => a.order - b.order);
|
|
3497
|
+
return {
|
|
3498
|
+
slug,
|
|
3499
|
+
label: config.label ?? slug,
|
|
3500
|
+
description: config.description ?? "",
|
|
3501
|
+
fieldType: config.fieldType ?? "select",
|
|
3502
|
+
required: config.required ?? false,
|
|
3503
|
+
default: config.default,
|
|
3504
|
+
variableName: config.variableName ?? slug,
|
|
3505
|
+
options
|
|
3506
|
+
};
|
|
3507
|
+
} catch {
|
|
3508
|
+
return null;
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
function readOptionMeta(filePath, slug) {
|
|
3512
|
+
try {
|
|
3513
|
+
const content = readFileSync(filePath, "utf-8");
|
|
3514
|
+
const fm = parseFrontmatter(content);
|
|
3515
|
+
return {
|
|
3516
|
+
slug,
|
|
3517
|
+
title: fm.title ?? slug,
|
|
3518
|
+
description: fm.description ?? "",
|
|
3519
|
+
order: fm.order ?? 99
|
|
3520
|
+
};
|
|
3521
|
+
} catch {
|
|
3522
|
+
return null;
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
function parseFrontmatter(content) {
|
|
3526
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
3527
|
+
if (!match) return {};
|
|
3528
|
+
const result = {};
|
|
3529
|
+
for (const line of match[1].split("\n")) {
|
|
3530
|
+
const m = line.match(/^(\w[\w-]*)\s*:\s*(.+)$/);
|
|
3531
|
+
if (!m) continue;
|
|
3532
|
+
const [, key, raw] = m;
|
|
3533
|
+
let value = raw.trim();
|
|
3534
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
3535
|
+
value = value.slice(1, -1);
|
|
3536
|
+
}
|
|
3537
|
+
if (/^\d+$/.test(value)) value = parseInt(value, 10);
|
|
3538
|
+
result[key] = value;
|
|
3539
|
+
}
|
|
3540
|
+
return result;
|
|
3541
|
+
}
|
|
3542
|
+
function loadKnowledgeFiles(knowledgeDir, selector) {
|
|
3543
|
+
const result = {};
|
|
3544
|
+
for (const [domainSlug, fields] of Object.entries(selector)) {
|
|
3545
|
+
if (typeof fields !== "object" || fields === null) continue;
|
|
3546
|
+
result[domainSlug] = {};
|
|
3547
|
+
for (const [fieldSlug, options] of Object.entries(fields)) {
|
|
3548
|
+
if (typeof options !== "object" || options === null) continue;
|
|
3549
|
+
result[domainSlug][fieldSlug] = {};
|
|
3550
|
+
for (const [optionSlug, selected] of Object.entries(options)) {
|
|
3551
|
+
if (selected !== true) continue;
|
|
3552
|
+
const filePath = join(knowledgeDir, domainSlug, fieldSlug, `${optionSlug}.md`);
|
|
3553
|
+
if (!existsSync(filePath)) continue;
|
|
3554
|
+
try {
|
|
3555
|
+
const content = readFileSync(filePath, "utf-8");
|
|
3556
|
+
const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "").trim();
|
|
3557
|
+
result[domainSlug][fieldSlug][optionSlug] = body;
|
|
3558
|
+
} catch {
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
return result;
|
|
3564
|
+
}
|
|
3565
|
+
function formatKnowledgeTreeForPrompt(treeOrTrees) {
|
|
3566
|
+
const trees = Array.isArray(treeOrTrees) ? treeOrTrees : [treeOrTrees];
|
|
3567
|
+
const allDomains = trees.flatMap((t) => t.domains);
|
|
3568
|
+
if (allDomains.length === 0) return "(no knowledge loaded)";
|
|
3569
|
+
const lines = ["<knowledge>"];
|
|
3570
|
+
const hasNames = trees.some((t) => t.name);
|
|
3571
|
+
if (hasNames) {
|
|
3572
|
+
const nonEmpty = trees.filter((t) => t.domains.length > 0);
|
|
3573
|
+
for (const tree of nonEmpty) {
|
|
3574
|
+
lines.push(` <space name="${xmlEsc(tree.name ?? "unknown")}">`);
|
|
3575
|
+
for (const domain of tree.domains) {
|
|
3576
|
+
formatDomainXml(lines, domain, " ");
|
|
3577
|
+
}
|
|
3578
|
+
lines.push(" </space>");
|
|
3579
|
+
}
|
|
3580
|
+
} else {
|
|
3581
|
+
for (const domain of allDomains) {
|
|
3582
|
+
formatDomainXml(lines, domain, " ");
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
lines.push("</knowledge>");
|
|
3586
|
+
return lines.join("\n");
|
|
3587
|
+
}
|
|
3588
|
+
function formatDomainXml(lines, domain, indent) {
|
|
3589
|
+
lines.push(`${indent}<domain name="${xmlEsc(domain.slug)}" icon="${xmlEsc(domain.icon)}" label="${xmlEsc(domain.label)}">`);
|
|
3590
|
+
for (const field of domain.fields) {
|
|
3591
|
+
lines.push(`${indent} <field name="${xmlEsc(field.slug)}" type="${xmlEsc(field.fieldType)}" var="${xmlEsc(field.variableName)}">`);
|
|
3592
|
+
for (const option of field.options) {
|
|
3593
|
+
const desc = option.description ? ` \u2014 ${xmlEsc(option.description)}` : "";
|
|
3594
|
+
lines.push(`${indent} <option name="${xmlEsc(option.slug)}">${xmlEsc(option.title)}${desc}</option>`);
|
|
3595
|
+
}
|
|
3596
|
+
lines.push(`${indent} </field>`);
|
|
3597
|
+
}
|
|
3598
|
+
lines.push(`${indent}</domain>`);
|
|
3599
|
+
}
|
|
3600
|
+
function xmlEsc(s) {
|
|
3601
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
3602
|
+
}
|
|
3603
|
+
|
|
3604
|
+
// src/knowledge/writer.ts
|
|
3605
|
+
import { mkdirSync, writeFileSync, unlinkSync, existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
|
|
3606
|
+
import { join as join2 } from "path";
|
|
3607
|
+
var MEMORY_DOMAIN_CONFIG = {
|
|
3608
|
+
label: "Memory",
|
|
3609
|
+
description: "Persistent agent memory",
|
|
3610
|
+
icon: "\u{1F9E0}",
|
|
3611
|
+
color: "#9b59b6",
|
|
3612
|
+
renderAs: "section"
|
|
3613
|
+
};
|
|
3614
|
+
var MEMORY_FIELDS = {
|
|
3615
|
+
user: {
|
|
3616
|
+
label: "User",
|
|
3617
|
+
description: "User preferences and context",
|
|
3618
|
+
fieldType: "text",
|
|
3619
|
+
variableName: "userMemory"
|
|
3620
|
+
},
|
|
3621
|
+
project: {
|
|
3622
|
+
label: "Project",
|
|
3623
|
+
description: "Project-specific knowledge",
|
|
3624
|
+
fieldType: "text",
|
|
3625
|
+
variableName: "projectMemory"
|
|
3626
|
+
},
|
|
3627
|
+
feedback: {
|
|
3628
|
+
label: "Feedback",
|
|
3629
|
+
description: "Behavioral guidance",
|
|
3630
|
+
fieldType: "text",
|
|
3631
|
+
variableName: "feedbackMemory"
|
|
3632
|
+
},
|
|
3633
|
+
reference: {
|
|
3634
|
+
label: "Reference",
|
|
3635
|
+
description: "External resource pointers",
|
|
3636
|
+
fieldType: "text",
|
|
3637
|
+
variableName: "referenceMemory"
|
|
3638
|
+
}
|
|
3639
|
+
};
|
|
3640
|
+
function saveKnowledgeFile(knowledgeDir, domain, field, option, content) {
|
|
3641
|
+
const domainDir = join2(knowledgeDir, domain);
|
|
3642
|
+
const fieldDir = join2(domainDir, field);
|
|
3643
|
+
const filePath = join2(fieldDir, `${option}.md`);
|
|
3644
|
+
mkdirSync(fieldDir, { recursive: true });
|
|
3645
|
+
const domainConfigPath = join2(domainDir, "config.json");
|
|
3646
|
+
if (!existsSync2(domainConfigPath)) {
|
|
3647
|
+
if (domain === "memory") {
|
|
3648
|
+
writeFileSync(domainConfigPath, JSON.stringify(MEMORY_DOMAIN_CONFIG, null, 2), "utf-8");
|
|
3649
|
+
} else {
|
|
3650
|
+
writeFileSync(domainConfigPath, JSON.stringify({
|
|
3651
|
+
label: domain.charAt(0).toUpperCase() + domain.slice(1),
|
|
3652
|
+
description: "",
|
|
3653
|
+
icon: "\u{1F4C1}",
|
|
3654
|
+
color: "#888888",
|
|
3655
|
+
renderAs: "section"
|
|
3656
|
+
}, null, 2), "utf-8");
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
const fieldConfigPath = join2(fieldDir, "config.json");
|
|
3660
|
+
if (!existsSync2(fieldConfigPath)) {
|
|
3661
|
+
const memoryFieldConfig = MEMORY_FIELDS[field];
|
|
3662
|
+
if (domain === "memory" && memoryFieldConfig) {
|
|
3663
|
+
writeFileSync(fieldConfigPath, JSON.stringify({
|
|
3664
|
+
...memoryFieldConfig,
|
|
3665
|
+
required: false,
|
|
3666
|
+
renderAs: "field"
|
|
3667
|
+
}, null, 2), "utf-8");
|
|
3668
|
+
} else {
|
|
3669
|
+
writeFileSync(fieldConfigPath, JSON.stringify({
|
|
3670
|
+
label: field.charAt(0).toUpperCase() + field.slice(1),
|
|
3671
|
+
description: "",
|
|
3672
|
+
fieldType: "text",
|
|
3673
|
+
required: false,
|
|
3674
|
+
variableName: field,
|
|
3675
|
+
renderAs: "field"
|
|
3676
|
+
}, null, 2), "utf-8");
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
const title = option.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
3680
|
+
const hasFrontmatter = content.trimStart().startsWith("---");
|
|
3681
|
+
let fileContent;
|
|
3682
|
+
if (hasFrontmatter) {
|
|
3683
|
+
fileContent = content;
|
|
3684
|
+
} else {
|
|
3685
|
+
fileContent = `---
|
|
3686
|
+
title: ${title}
|
|
3687
|
+
description: ${content.slice(0, 80).replace(/\n/g, " ")}
|
|
3688
|
+
order: 99
|
|
3689
|
+
---
|
|
3690
|
+
|
|
3691
|
+
${content}
|
|
3692
|
+
`;
|
|
3693
|
+
}
|
|
3694
|
+
writeFileSync(filePath, fileContent, "utf-8");
|
|
3695
|
+
}
|
|
3696
|
+
function deleteKnowledgeFile(knowledgeDir, domain, field, option) {
|
|
3697
|
+
const filePath = join2(knowledgeDir, domain, field, `${option}.md`);
|
|
3698
|
+
if (!existsSync2(filePath)) return false;
|
|
3699
|
+
unlinkSync(filePath);
|
|
3700
|
+
return true;
|
|
3701
|
+
}
|
|
3702
|
+
function ensureMemoryDomain(knowledgeDir) {
|
|
3703
|
+
if (!existsSync2(knowledgeDir)) {
|
|
3704
|
+
mkdirSync(knowledgeDir, { recursive: true });
|
|
3705
|
+
}
|
|
3706
|
+
const memoryDir = join2(knowledgeDir, "memory");
|
|
3707
|
+
mkdirSync(memoryDir, { recursive: true });
|
|
3708
|
+
const domainConfigPath = join2(memoryDir, "config.json");
|
|
3709
|
+
if (!existsSync2(domainConfigPath)) {
|
|
3710
|
+
writeFileSync(domainConfigPath, JSON.stringify(MEMORY_DOMAIN_CONFIG, null, 2), "utf-8");
|
|
3711
|
+
}
|
|
3712
|
+
for (const [fieldSlug, fieldConfig] of Object.entries(MEMORY_FIELDS)) {
|
|
3713
|
+
const fieldDir = join2(memoryDir, fieldSlug);
|
|
3714
|
+
mkdirSync(fieldDir, { recursive: true });
|
|
3715
|
+
const fieldConfigPath = join2(fieldDir, "config.json");
|
|
3716
|
+
if (!existsSync2(fieldConfigPath)) {
|
|
3717
|
+
writeFileSync(fieldConfigPath, JSON.stringify({
|
|
3718
|
+
...fieldConfig,
|
|
3719
|
+
required: false,
|
|
3720
|
+
renderAs: "field"
|
|
3721
|
+
}, null, 2), "utf-8");
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
function parseFieldPath(fieldParam) {
|
|
3726
|
+
const parts = fieldParam.split("/");
|
|
3727
|
+
if (parts.length >= 2) {
|
|
3728
|
+
return { domain: parts[0], field: parts.slice(1).join("/") };
|
|
3729
|
+
}
|
|
3730
|
+
return { domain: "memory", field: parts[0] };
|
|
3731
|
+
}
|
|
3732
|
+
export {
|
|
3733
|
+
AgentRegistry,
|
|
3734
|
+
AsyncManager,
|
|
3735
|
+
ConversationRecorder,
|
|
3736
|
+
FunctionRegistry,
|
|
3737
|
+
HookRegistry,
|
|
3738
|
+
KNOWLEDGE_TAG,
|
|
3739
|
+
Sandbox,
|
|
3740
|
+
Session,
|
|
3741
|
+
StreamController,
|
|
3742
|
+
buildErrorMessage,
|
|
3743
|
+
buildHookInterruptMessage,
|
|
3744
|
+
buildInterventionMessage,
|
|
3745
|
+
buildKnowledgeTree,
|
|
3746
|
+
buildStopMessage,
|
|
3747
|
+
buildSummaryComment,
|
|
3748
|
+
buildSystemPrompt,
|
|
3749
|
+
buildTaskContinueMessage,
|
|
3750
|
+
buildTaskOrderViolationMessage,
|
|
3751
|
+
buildTasklistReminderMessage,
|
|
3752
|
+
clear,
|
|
3753
|
+
compressCodeWindow,
|
|
3754
|
+
computeScopeDelta,
|
|
3755
|
+
createBracketState,
|
|
3756
|
+
createDefaultConfig,
|
|
3757
|
+
createGlobals,
|
|
3758
|
+
createLineAccumulator,
|
|
3759
|
+
decayErrorMessage,
|
|
3760
|
+
decayKnowledgeValue,
|
|
3761
|
+
decayStopPayload,
|
|
3762
|
+
deleteKnowledgeFile,
|
|
3763
|
+
describeType2 as describeType,
|
|
3764
|
+
detectGlobalCall,
|
|
3765
|
+
ensureMemoryDomain,
|
|
3766
|
+
executeHooks,
|
|
3767
|
+
executeLine,
|
|
3768
|
+
extractDeclarations,
|
|
3769
|
+
extractVariableNames,
|
|
3770
|
+
feed,
|
|
3771
|
+
feedChunk,
|
|
3772
|
+
findMatches,
|
|
3773
|
+
flush,
|
|
3774
|
+
formatCatalogForPrompt,
|
|
3775
|
+
formatKnowledgeTreeForPrompt,
|
|
3776
|
+
generateAgentsBlock,
|
|
3777
|
+
generateCurrentTaskBlock,
|
|
3778
|
+
generateScopeTable,
|
|
3779
|
+
generateTasksBlock,
|
|
3780
|
+
getCatalogModule,
|
|
3781
|
+
getDecayLevel,
|
|
3782
|
+
getKnowledgeDecayLevel,
|
|
3783
|
+
isBalanced,
|
|
3784
|
+
isCompleteStatement,
|
|
3785
|
+
isJSXSafe,
|
|
3786
|
+
isKnowledgeContent,
|
|
3787
|
+
loadCatalog,
|
|
3788
|
+
loadKnowledgeFiles,
|
|
3789
|
+
matchPattern,
|
|
3790
|
+
mergeCatalogs,
|
|
3791
|
+
mergeConfig,
|
|
3792
|
+
mergeKnowledgeTrees,
|
|
3793
|
+
parseFieldPath,
|
|
3794
|
+
parseStatement,
|
|
3795
|
+
recoverArgumentNames,
|
|
3796
|
+
renderTaskLine,
|
|
3797
|
+
resetBracketState,
|
|
3798
|
+
sanitizeJSX,
|
|
3799
|
+
saveKnowledgeFile,
|
|
3800
|
+
serialize,
|
|
3801
|
+
serializeTasklistsState,
|
|
3802
|
+
tagAsKnowledge,
|
|
3803
|
+
transpile,
|
|
3804
|
+
truncateValue2 as truncateValue,
|
|
3805
|
+
updateScopeInPrompt,
|
|
3806
|
+
validateConfig,
|
|
3807
|
+
validateFormComponents,
|
|
3808
|
+
wrapFunction
|
|
3809
|
+
};
|