actoviq-agent-sdk 0.4.0 → 0.4.2
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/README-zh.md +15 -2
- package/README.md +20 -7
- package/dist/src/cli/actoviq-react.js +178 -3
- package/dist/src/cli/actoviq-react.js.map +1 -1
- package/dist/src/cli/actoviq-tui.js +2 -2
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +17 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/router/modelRouter.d.ts +46 -0
- package/dist/src/router/modelRouter.d.ts.map +1 -0
- package/dist/src/router/modelRouter.js +227 -0
- package/dist/src/router/modelRouter.js.map +1 -0
- package/dist/src/runtime/actoviqSkills.d.ts +6 -0
- package/dist/src/runtime/actoviqSkills.d.ts.map +1 -1
- package/dist/src/runtime/actoviqSkills.js +69 -1
- package/dist/src/runtime/actoviqSkills.js.map +1 -1
- package/dist/src/runtime/agentClient.d.ts +16 -0
- package/dist/src/runtime/agentClient.d.ts.map +1 -1
- package/dist/src/runtime/agentClient.js +89 -9
- package/dist/src/runtime/agentClient.js.map +1 -1
- package/dist/src/runtime/conversationEngine.d.ts +2 -0
- package/dist/src/runtime/conversationEngine.d.ts.map +1 -1
- package/dist/src/runtime/conversationEngine.js +6 -5
- package/dist/src/runtime/conversationEngine.js.map +1 -1
- package/dist/src/storage/sessionStore.d.ts.map +1 -1
- package/dist/src/storage/sessionStore.js +25 -6
- package/dist/src/storage/sessionStore.js.map +1 -1
- package/dist/src/team/agentPool.d.ts +26 -0
- package/dist/src/team/agentPool.d.ts.map +1 -0
- package/dist/src/team/agentPool.js +97 -0
- package/dist/src/team/agentPool.js.map +1 -0
- package/dist/src/team/modelTeam.d.ts +56 -0
- package/dist/src/team/modelTeam.d.ts.map +1 -0
- package/dist/src/team/modelTeam.js +542 -0
- package/dist/src/team/modelTeam.js.map +1 -0
- package/dist/src/team/pricing.d.ts +15 -0
- package/dist/src/team/pricing.d.ts.map +1 -0
- package/dist/src/team/pricing.js +80 -0
- package/dist/src/team/pricing.js.map +1 -0
- package/dist/src/team/teamDefinitions.d.ts +28 -0
- package/dist/src/team/teamDefinitions.d.ts.map +1 -0
- package/dist/src/team/teamDefinitions.js +151 -0
- package/dist/src/team/teamDefinitions.js.map +1 -0
- package/dist/src/tools/actoviqCoreTools.d.ts.map +1 -1
- package/dist/src/tools/actoviqCoreTools.js +6 -1
- package/dist/src/tools/actoviqCoreTools.js.map +1 -1
- package/dist/src/tools/enterWorktree.d.ts +11 -0
- package/dist/src/tools/enterWorktree.d.ts.map +1 -0
- package/dist/src/tools/enterWorktree.js +79 -0
- package/dist/src/tools/enterWorktree.js.map +1 -0
- package/dist/src/tools/exitWorktree.d.ts +4 -0
- package/dist/src/tools/exitWorktree.d.ts.map +1 -0
- package/dist/src/tools/exitWorktree.js +48 -0
- package/dist/src/tools/exitWorktree.js.map +1 -0
- package/dist/src/tools/tavilySearch.d.ts +12 -0
- package/dist/src/tools/tavilySearch.d.ts.map +1 -0
- package/dist/src/tools/tavilySearch.js +139 -0
- package/dist/src/tools/tavilySearch.js.map +1 -0
- package/dist/src/tui/actoviqTui.d.ts +10 -0
- package/dist/src/tui/actoviqTui.d.ts.map +1 -1
- package/dist/src/tui/actoviqTui.js +606 -35
- package/dist/src/tui/actoviqTui.js.map +1 -1
- package/dist/src/tui/editor.d.ts +2 -0
- package/dist/src/tui/editor.d.ts.map +1 -1
- package/dist/src/tui/editor.js +5 -0
- package/dist/src/tui/editor.js.map +1 -1
- package/dist/src/types.d.ts +323 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js.map +1 -1
- package/dist/src/workflow/workflowPersistence.d.ts +34 -0
- package/dist/src/workflow/workflowPersistence.d.ts.map +1 -0
- package/dist/src/workflow/workflowPersistence.js +144 -0
- package/dist/src/workflow/workflowPersistence.js.map +1 -0
- package/dist/src/workflow/workflowScriptRuntime.d.ts +22 -0
- package/dist/src/workflow/workflowScriptRuntime.d.ts.map +1 -0
- package/dist/src/workflow/workflowScriptRuntime.js +655 -0
- package/dist/src/workflow/workflowScriptRuntime.js.map +1 -0
- package/dist/src/worktree/worktreeHooks.d.ts +38 -0
- package/dist/src/worktree/worktreeHooks.d.ts.map +1 -0
- package/dist/src/worktree/worktreeHooks.js +82 -0
- package/dist/src/worktree/worktreeHooks.js.map +1 -0
- package/dist/src/worktree/worktreeInclude.d.ts +11 -0
- package/dist/src/worktree/worktreeInclude.d.ts.map +1 -0
- package/dist/src/worktree/worktreeInclude.js +65 -0
- package/dist/src/worktree/worktreeInclude.js.map +1 -0
- package/dist/src/worktree/worktreeService.d.ts +43 -0
- package/dist/src/worktree/worktreeService.d.ts.map +1 -0
- package/dist/src/worktree/worktreeService.js +356 -0
- package/dist/src/worktree/worktreeService.js.map +1 -0
- package/package.json +2 -24
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkflowScriptRuntime — executes dynamic workflow JavaScript scripts
|
|
3
|
+
* in a sandboxed vm.Module with Host Bridge for agent()/parallel()/pipeline().
|
|
4
|
+
*
|
|
5
|
+
* The script VM has ZERO fs/net/process access. All external calls go
|
|
6
|
+
* through the Host Bridge via message-passing.
|
|
7
|
+
*/
|
|
8
|
+
import vm from 'node:vm';
|
|
9
|
+
import { randomUUID } from 'node:crypto';
|
|
10
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
11
|
+
// StructuredOutput tool for schema enforcement (append mode)
|
|
12
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
13
|
+
const STRUCTURED_OUTPUT_TOOL_NAME = 'StructuredOutput';
|
|
14
|
+
function createStructuredOutputTool(schema) {
|
|
15
|
+
return {
|
|
16
|
+
kind: 'local',
|
|
17
|
+
name: STRUCTURED_OUTPUT_TOOL_NAME,
|
|
18
|
+
description: 'Return your final structured result. You MUST call this tool with your answer ' +
|
|
19
|
+
'when you have completed the task. All fields in the schema are required unless marked optional.',
|
|
20
|
+
inputSchema: {
|
|
21
|
+
parse: (input) => input,
|
|
22
|
+
_type: undefined,
|
|
23
|
+
},
|
|
24
|
+
inputJsonSchema: schema,
|
|
25
|
+
async execute(input) {
|
|
26
|
+
return JSON.stringify(input);
|
|
27
|
+
},
|
|
28
|
+
interruptBehavior: 'block',
|
|
29
|
+
isConcurrencySafe: () => true,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function validateAgainstSchema(data, schema) {
|
|
33
|
+
if (!schema.properties)
|
|
34
|
+
return { valid: true };
|
|
35
|
+
if (typeof data !== 'object' || data === null) {
|
|
36
|
+
return { valid: false, errors: ['Output must be a JSON object.'] };
|
|
37
|
+
}
|
|
38
|
+
const errors = [];
|
|
39
|
+
const props = schema.properties;
|
|
40
|
+
const required = schema.required ?? [];
|
|
41
|
+
const dataObj = data;
|
|
42
|
+
// Check required fields
|
|
43
|
+
for (const field of required) {
|
|
44
|
+
if (!(field in dataObj) || dataObj[field] === undefined) {
|
|
45
|
+
errors.push(`Missing required field: ${field}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Type check known fields
|
|
49
|
+
for (const [key, propSchema] of Object.entries(props)) {
|
|
50
|
+
if (key in dataObj && dataObj[key] !== undefined) {
|
|
51
|
+
const type = propSchema.type;
|
|
52
|
+
const value = dataObj[key];
|
|
53
|
+
if (type === 'string' && typeof value !== 'string') {
|
|
54
|
+
errors.push(`Field "${key}" must be a string, got ${typeof value}`);
|
|
55
|
+
}
|
|
56
|
+
else if (type === 'number' && typeof value !== 'number') {
|
|
57
|
+
errors.push(`Field "${key}" must be a number, got ${typeof value}`);
|
|
58
|
+
}
|
|
59
|
+
else if (type === 'boolean' && typeof value !== 'boolean') {
|
|
60
|
+
errors.push(`Field "${key}" must be a boolean, got ${typeof value}`);
|
|
61
|
+
}
|
|
62
|
+
else if (type === 'array' && !Array.isArray(value)) {
|
|
63
|
+
errors.push(`Field "${key}" must be an array, got ${typeof value}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { valid: errors.length === 0, errors: errors.length > 0 ? errors : undefined };
|
|
68
|
+
}
|
|
69
|
+
function tryParseStructuredOutput(text) {
|
|
70
|
+
// Try to find JSON in the response (model may wrap it in text)
|
|
71
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
72
|
+
if (!jsonMatch)
|
|
73
|
+
return null;
|
|
74
|
+
try {
|
|
75
|
+
return { parsed: JSON.parse(jsonMatch[0]), json: jsonMatch[0] };
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
82
|
+
// Cache key normalization
|
|
83
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
84
|
+
function sortedJson(obj) {
|
|
85
|
+
if (obj === null || obj === undefined)
|
|
86
|
+
return String(obj);
|
|
87
|
+
if (typeof obj !== 'object')
|
|
88
|
+
return JSON.stringify(obj);
|
|
89
|
+
if (Array.isArray(obj))
|
|
90
|
+
return JSON.stringify(obj.map(sortedJson));
|
|
91
|
+
const sorted = {};
|
|
92
|
+
for (const key of Object.keys(obj).sort()) {
|
|
93
|
+
sorted[key] = obj[key];
|
|
94
|
+
}
|
|
95
|
+
return JSON.stringify(sorted);
|
|
96
|
+
}
|
|
97
|
+
function canonicalAgentKey(prompt, opts, phase) {
|
|
98
|
+
return `${prompt}|${sortedJson(opts)}|${phase ?? ''}`;
|
|
99
|
+
}
|
|
100
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
101
|
+
// Script syntax check
|
|
102
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
103
|
+
const BANNED_GLOBALS = ['Date', 'Math.random', 'process', 'require', 'import', 'fs', 'net', 'child_process'];
|
|
104
|
+
function validateScript(script) {
|
|
105
|
+
// Ban Date.now(), Math.random(), new Date() — break determinism for resume
|
|
106
|
+
if (/Date\.now\s*\(/.test(script)) {
|
|
107
|
+
throw new Error('Date.now() is not allowed in workflow scripts (breaks resume determinism).');
|
|
108
|
+
}
|
|
109
|
+
if (/Math\.random\s*\(/.test(script)) {
|
|
110
|
+
throw new Error('Math.random() is not allowed in workflow scripts (breaks resume determinism).');
|
|
111
|
+
}
|
|
112
|
+
if (/new\s+Date\s*\(/.test(script)) {
|
|
113
|
+
throw new Error('new Date() is not allowed in workflow scripts (breaks resume determinism).');
|
|
114
|
+
}
|
|
115
|
+
// Must have export const meta
|
|
116
|
+
if (!/export\s+const\s+meta\s*=/.test(script)) {
|
|
117
|
+
throw new Error('Workflow script must export const meta = { name, description, phases }.');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
121
|
+
// Meta extraction
|
|
122
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
123
|
+
function extractMeta(script) {
|
|
124
|
+
const metaMatch = script.match(/export\s+const\s+meta\s*=\s*(\{[\s\S]*?\});/);
|
|
125
|
+
if (!metaMatch) {
|
|
126
|
+
throw new Error('Could not extract meta from workflow script.');
|
|
127
|
+
}
|
|
128
|
+
// Validate pure literal (no variables, function calls, template strings)
|
|
129
|
+
const metaBlock = metaMatch[1];
|
|
130
|
+
if (/[a-zA-Z_$]\s*\(/.test(metaBlock.replace(/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/g, '').replace(/`[\s\S]*?`/g, ''))) {
|
|
131
|
+
throw new Error('Workflow meta must be a pure literal — no function calls or computed values.');
|
|
132
|
+
}
|
|
133
|
+
// Evaluate the meta object in a sandbox
|
|
134
|
+
try {
|
|
135
|
+
const sandbox = {};
|
|
136
|
+
vm.createContext(sandbox);
|
|
137
|
+
const metaObj = vm.runInContext(`(${metaBlock})`, sandbox);
|
|
138
|
+
if (!metaObj.name || !metaObj.description) {
|
|
139
|
+
throw new Error('Workflow meta must include name and description.');
|
|
140
|
+
}
|
|
141
|
+
return metaObj;
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
if (err.message?.includes('name') || err.message?.includes('description'))
|
|
145
|
+
throw err;
|
|
146
|
+
throw new Error(`Failed to parse workflow meta: ${err.message}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
150
|
+
// Host Bridge
|
|
151
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
152
|
+
class HostBridge {
|
|
153
|
+
agentQueue = [];
|
|
154
|
+
activeAgents = 0;
|
|
155
|
+
maxConcurrent;
|
|
156
|
+
totalAgents = 0;
|
|
157
|
+
maxTotal = 1000;
|
|
158
|
+
signal;
|
|
159
|
+
sdk;
|
|
160
|
+
cache;
|
|
161
|
+
resumeCompleted;
|
|
162
|
+
agentRecords;
|
|
163
|
+
onEvent;
|
|
164
|
+
runId;
|
|
165
|
+
// Budget tracking
|
|
166
|
+
budgetTotal;
|
|
167
|
+
totalSpentTokens = 0;
|
|
168
|
+
// Phase tracking
|
|
169
|
+
currentPhase = null;
|
|
170
|
+
phases = new Map();
|
|
171
|
+
phaseOrder = [];
|
|
172
|
+
_logMessages = [];
|
|
173
|
+
_errors = [];
|
|
174
|
+
constructor(options) {
|
|
175
|
+
this.sdk = options.sdk;
|
|
176
|
+
this.maxConcurrent = options.maxConcurrent ?? 16;
|
|
177
|
+
this.budgetTotal = options.budgetTotal ?? null;
|
|
178
|
+
this.signal = options.signal;
|
|
179
|
+
this.onEvent = options.onEvent;
|
|
180
|
+
this.runId = randomUUID();
|
|
181
|
+
this.agentRecords = [];
|
|
182
|
+
// Resume state
|
|
183
|
+
this.cache = options.resumeState?.cache ?? new Map();
|
|
184
|
+
this.resumeCompleted = options.resumeState?.completedAgentIds ?? new Set();
|
|
185
|
+
// Restore phases from resume
|
|
186
|
+
if (options.resumeState?.phases) {
|
|
187
|
+
for (const phase of options.resumeState.phases) {
|
|
188
|
+
this.phases.set(phase.title, phase);
|
|
189
|
+
this.phaseOrder.push(phase.title);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// ── Budget ─────────────────────────────────────────────────────
|
|
194
|
+
createBudget() {
|
|
195
|
+
return {
|
|
196
|
+
total: this.budgetTotal,
|
|
197
|
+
spent: () => this.totalSpentTokens,
|
|
198
|
+
remaining: () => {
|
|
199
|
+
if (this.budgetTotal === null)
|
|
200
|
+
return Infinity;
|
|
201
|
+
return Math.max(0, this.budgetTotal - this.totalSpentTokens);
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
// ── Phase ──────────────────────────────────────────────────────
|
|
206
|
+
setPhase(title) {
|
|
207
|
+
this.currentPhase = title;
|
|
208
|
+
if (!this.phases.has(title)) {
|
|
209
|
+
const pp = {
|
|
210
|
+
title,
|
|
211
|
+
agentCount: 0,
|
|
212
|
+
completedCount: 0,
|
|
213
|
+
failedCount: 0,
|
|
214
|
+
totalTokens: 0,
|
|
215
|
+
startedAt: new Date().toISOString(),
|
|
216
|
+
};
|
|
217
|
+
this.phases.set(title, pp);
|
|
218
|
+
this.phaseOrder.push(title);
|
|
219
|
+
}
|
|
220
|
+
this.onEvent?.({
|
|
221
|
+
type: 'workflow.phase.start',
|
|
222
|
+
runId: this.runId,
|
|
223
|
+
phase: title,
|
|
224
|
+
timestamp: new Date().toISOString(),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
// ── Log ────────────────────────────────────────────────────────
|
|
228
|
+
log(message) {
|
|
229
|
+
this._logMessages.push(message);
|
|
230
|
+
this.onEvent?.({
|
|
231
|
+
type: 'workflow.log',
|
|
232
|
+
runId: this.runId,
|
|
233
|
+
message,
|
|
234
|
+
timestamp: new Date().toISOString(),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
// ── Agent ──────────────────────────────────────────────────────
|
|
238
|
+
async agent(prompt, opts = {}) {
|
|
239
|
+
const phase = opts.phase ?? this.currentPhase ?? undefined;
|
|
240
|
+
const key = canonicalAgentKey(prompt, opts, phase);
|
|
241
|
+
// Cache hit?
|
|
242
|
+
const cached = this.cache.get(key);
|
|
243
|
+
if (cached) {
|
|
244
|
+
const record = {
|
|
245
|
+
id: randomUUID(),
|
|
246
|
+
prompt,
|
|
247
|
+
opts,
|
|
248
|
+
phase,
|
|
249
|
+
result: cached.result,
|
|
250
|
+
tokens: cached.tokens,
|
|
251
|
+
durationMs: cached.durationMs,
|
|
252
|
+
startedAt: cached.cachedAt,
|
|
253
|
+
completedAt: cached.cachedAt,
|
|
254
|
+
cached: true,
|
|
255
|
+
};
|
|
256
|
+
this.agentRecords.push(record);
|
|
257
|
+
if (phase) {
|
|
258
|
+
const pp = this.phases.get(phase);
|
|
259
|
+
if (pp) {
|
|
260
|
+
pp.agentCount++;
|
|
261
|
+
pp.completedCount++;
|
|
262
|
+
if (cached.tokens)
|
|
263
|
+
pp.totalTokens += cached.tokens.input + cached.tokens.output;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
this.onEvent?.({
|
|
267
|
+
type: 'workflow.agent.start',
|
|
268
|
+
runId: this.runId,
|
|
269
|
+
agentId: record.id,
|
|
270
|
+
label: opts.label,
|
|
271
|
+
phase,
|
|
272
|
+
cached: true,
|
|
273
|
+
timestamp: new Date().toISOString(),
|
|
274
|
+
});
|
|
275
|
+
this.onEvent?.({
|
|
276
|
+
type: 'workflow.agent.done',
|
|
277
|
+
runId: this.runId,
|
|
278
|
+
agentId: record.id,
|
|
279
|
+
phase,
|
|
280
|
+
cached: true,
|
|
281
|
+
durationMs: cached.durationMs,
|
|
282
|
+
tokens: cached.tokens,
|
|
283
|
+
timestamp: new Date().toISOString(),
|
|
284
|
+
});
|
|
285
|
+
return cached.result;
|
|
286
|
+
}
|
|
287
|
+
// Check budget
|
|
288
|
+
if (this.budgetTotal !== null && this.totalSpentTokens >= this.budgetTotal) {
|
|
289
|
+
throw new Error('Budget exhausted — cannot spawn more agents.');
|
|
290
|
+
}
|
|
291
|
+
// Check total cap
|
|
292
|
+
if (this.totalAgents >= this.maxTotal) {
|
|
293
|
+
throw new Error('Agent cap reached (1000 total per run).');
|
|
294
|
+
}
|
|
295
|
+
// Wait for concurrency slot
|
|
296
|
+
while (this.activeAgents >= this.maxConcurrent) {
|
|
297
|
+
if (this.signal?.aborted)
|
|
298
|
+
throw new Error('Aborted.');
|
|
299
|
+
await delay(50);
|
|
300
|
+
}
|
|
301
|
+
this.activeAgents++;
|
|
302
|
+
this.totalAgents++;
|
|
303
|
+
const record = {
|
|
304
|
+
id: randomUUID(),
|
|
305
|
+
prompt,
|
|
306
|
+
opts,
|
|
307
|
+
phase,
|
|
308
|
+
startedAt: new Date().toISOString(),
|
|
309
|
+
cached: false,
|
|
310
|
+
};
|
|
311
|
+
this.agentRecords.push(record);
|
|
312
|
+
if (phase) {
|
|
313
|
+
const pp = this.phases.get(phase);
|
|
314
|
+
if (pp)
|
|
315
|
+
pp.agentCount++;
|
|
316
|
+
}
|
|
317
|
+
this.onEvent?.({
|
|
318
|
+
type: 'workflow.agent.start',
|
|
319
|
+
runId: this.runId,
|
|
320
|
+
agentId: record.id,
|
|
321
|
+
label: opts.label,
|
|
322
|
+
phase,
|
|
323
|
+
cached: false,
|
|
324
|
+
timestamp: record.startedAt,
|
|
325
|
+
});
|
|
326
|
+
try {
|
|
327
|
+
// Execute via SDK
|
|
328
|
+
const session = await this.sdk.createSession({
|
|
329
|
+
title: opts.label ?? `workflow-agent-${record.id.slice(0, 8)}`,
|
|
330
|
+
});
|
|
331
|
+
// Build tool list: user tools + optional StructuredOutput for schema enforcement
|
|
332
|
+
const userTools = opts.tools?.map((t) => this.sdk.getTool(t)).filter((t) => t !== undefined) ?? [];
|
|
333
|
+
const allTools = opts.schema
|
|
334
|
+
? [...userTools, createStructuredOutputTool(opts.schema)]
|
|
335
|
+
: userTools;
|
|
336
|
+
const maxRetries = 3;
|
|
337
|
+
let lastResult;
|
|
338
|
+
let finalTokens;
|
|
339
|
+
let execResult = await session.send(prompt, {
|
|
340
|
+
systemPrompt: undefined,
|
|
341
|
+
model: opts.model,
|
|
342
|
+
tools: allTools.length > 0 ? allTools : undefined,
|
|
343
|
+
signal: this.signal,
|
|
344
|
+
permissionMode: 'acceptEdits',
|
|
345
|
+
});
|
|
346
|
+
finalTokens = (execResult.usage?.input_tokens != null && execResult.usage?.output_tokens != null)
|
|
347
|
+
? { input: execResult.usage.input_tokens, output: execResult.usage.output_tokens }
|
|
348
|
+
: undefined;
|
|
349
|
+
if (!opts.schema) {
|
|
350
|
+
lastResult = execResult.text;
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
// Schema enforcement with retry
|
|
354
|
+
let validated = false;
|
|
355
|
+
for (let attempt = 0; attempt < maxRetries && !validated; attempt++) {
|
|
356
|
+
if (attempt > 0) {
|
|
357
|
+
// Re-prompt on same session to fix output
|
|
358
|
+
execResult = await session.send('Your previous output did not match the required schema. ' +
|
|
359
|
+
'Please call the StructuredOutput tool with the correct format.', {
|
|
360
|
+
systemPrompt: undefined,
|
|
361
|
+
model: opts.model,
|
|
362
|
+
tools: allTools.length > 0 ? allTools : undefined,
|
|
363
|
+
signal: this.signal,
|
|
364
|
+
permissionMode: 'acceptEdits',
|
|
365
|
+
});
|
|
366
|
+
const retryTokens = (execResult.usage?.input_tokens != null && execResult.usage?.output_tokens != null)
|
|
367
|
+
? { input: execResult.usage.input_tokens, output: execResult.usage.output_tokens }
|
|
368
|
+
: undefined;
|
|
369
|
+
if (retryTokens && finalTokens) {
|
|
370
|
+
finalTokens.input += retryTokens.input;
|
|
371
|
+
finalTokens.output += retryTokens.output;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const parsed = tryParseStructuredOutput(execResult.text);
|
|
375
|
+
if (parsed) {
|
|
376
|
+
const validation = validateAgainstSchema(parsed.parsed, opts.schema);
|
|
377
|
+
if (validation.valid) {
|
|
378
|
+
lastResult = parsed.parsed;
|
|
379
|
+
validated = true;
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
if (attempt < maxRetries - 1) {
|
|
383
|
+
this.log(`Schema validation failed (attempt ${attempt + 1}): ${validation.errors?.join(', ')}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
else if (attempt < maxRetries - 1) {
|
|
387
|
+
this.log(`No structured output found (attempt ${attempt + 1}), retrying...`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (!validated) {
|
|
391
|
+
lastResult = execResult.text; // Last attempt: return raw text
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
record.result = lastResult;
|
|
395
|
+
record.tokens = finalTokens;
|
|
396
|
+
record.completedAt = new Date().toISOString();
|
|
397
|
+
record.durationMs = new Date(record.completedAt).getTime() - new Date(record.startedAt).getTime();
|
|
398
|
+
// Cache
|
|
399
|
+
if (record.tokens) {
|
|
400
|
+
this.totalSpentTokens += record.tokens.input + record.tokens.output;
|
|
401
|
+
}
|
|
402
|
+
this.cache.set(key, {
|
|
403
|
+
key,
|
|
404
|
+
result: record.result,
|
|
405
|
+
tokens: record.tokens,
|
|
406
|
+
durationMs: record.durationMs ?? 0,
|
|
407
|
+
cachedAt: new Date().toISOString(),
|
|
408
|
+
});
|
|
409
|
+
if (phase && record.tokens) {
|
|
410
|
+
const pp = this.phases.get(phase);
|
|
411
|
+
if (pp) {
|
|
412
|
+
pp.completedCount++;
|
|
413
|
+
pp.totalTokens += record.tokens.input + record.tokens.output;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
this.onEvent?.({
|
|
417
|
+
type: 'workflow.agent.done',
|
|
418
|
+
runId: this.runId,
|
|
419
|
+
agentId: record.id,
|
|
420
|
+
phase,
|
|
421
|
+
cached: false,
|
|
422
|
+
durationMs: record.durationMs,
|
|
423
|
+
tokens: record.tokens,
|
|
424
|
+
timestamp: record.completedAt,
|
|
425
|
+
});
|
|
426
|
+
return record.result;
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
record.error = err.message;
|
|
430
|
+
record.completedAt = new Date().toISOString();
|
|
431
|
+
record.durationMs = new Date(record.completedAt).getTime() - new Date(record.startedAt).getTime();
|
|
432
|
+
if (phase) {
|
|
433
|
+
const pp = this.phases.get(phase);
|
|
434
|
+
if (pp)
|
|
435
|
+
pp.failedCount++;
|
|
436
|
+
}
|
|
437
|
+
this._errors.push({ agentId: record.id, phase, error: err.message });
|
|
438
|
+
this.log(`Agent "${opts.label ?? record.id}" failed: ${err.message}`);
|
|
439
|
+
this.onEvent?.({
|
|
440
|
+
type: 'workflow.agent.done',
|
|
441
|
+
runId: this.runId,
|
|
442
|
+
agentId: record.id,
|
|
443
|
+
phase,
|
|
444
|
+
cached: false,
|
|
445
|
+
durationMs: record.durationMs,
|
|
446
|
+
error: err.message,
|
|
447
|
+
timestamp: record.completedAt,
|
|
448
|
+
});
|
|
449
|
+
throw err;
|
|
450
|
+
}
|
|
451
|
+
finally {
|
|
452
|
+
this.activeAgents = Math.max(0, this.activeAgents - 1);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
// ── Parallel ───────────────────────────────────────────────────
|
|
456
|
+
async parallel(thunks) {
|
|
457
|
+
if (thunks.length > 4096) {
|
|
458
|
+
throw new Error('parallel() supports at most 4096 items.');
|
|
459
|
+
}
|
|
460
|
+
return Promise.all(thunks.map(async (thunk) => {
|
|
461
|
+
try {
|
|
462
|
+
return await thunk();
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
}));
|
|
468
|
+
}
|
|
469
|
+
// ── Pipeline ───────────────────────────────────────────────────
|
|
470
|
+
async pipeline(items, ...stages) {
|
|
471
|
+
if (items.length > 4096) {
|
|
472
|
+
throw new Error('pipeline() supports at most 4096 items.');
|
|
473
|
+
}
|
|
474
|
+
const results = new Array(items.length).fill(null);
|
|
475
|
+
const errors = [];
|
|
476
|
+
// Process each item through all stages independently (no barrier)
|
|
477
|
+
const itemPromises = items.map(async (item, index) => {
|
|
478
|
+
let current = null;
|
|
479
|
+
for (let si = 0; si < stages.length; si++) {
|
|
480
|
+
const stage = stages[si];
|
|
481
|
+
try {
|
|
482
|
+
const result = await stage(current, item, index);
|
|
483
|
+
if (result === null) {
|
|
484
|
+
// Item skipped
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
current = result;
|
|
488
|
+
}
|
|
489
|
+
catch (err) {
|
|
490
|
+
errors.push({ itemIndex: index, stageIndex: si, error: err.message });
|
|
491
|
+
this._errors.push({
|
|
492
|
+
agentId: `pipeline-${index}-${si}`,
|
|
493
|
+
error: err.message,
|
|
494
|
+
itemIndex: index,
|
|
495
|
+
stageIndex: si,
|
|
496
|
+
});
|
|
497
|
+
return null; // Item dropped, other items continue
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
results[index] = current;
|
|
501
|
+
return current;
|
|
502
|
+
});
|
|
503
|
+
await Promise.all(itemPromises);
|
|
504
|
+
// Auto-report failure stats
|
|
505
|
+
if (errors.length > 0) {
|
|
506
|
+
this.log(`${errors.length}/${items.length} items failed across pipeline stages.`);
|
|
507
|
+
}
|
|
508
|
+
return results;
|
|
509
|
+
}
|
|
510
|
+
// ── State snapshot for resume ──────────────────────────────────
|
|
511
|
+
getResumeState() {
|
|
512
|
+
return {
|
|
513
|
+
runId: this.runId,
|
|
514
|
+
cache: this.cache,
|
|
515
|
+
agentCallIds: this.agentRecords.map((r) => r.id),
|
|
516
|
+
completedAgentIds: new Set(this.agentRecords.filter((r) => r.completedAt).map((r) => r.id)),
|
|
517
|
+
phases: [...this.phases.values()],
|
|
518
|
+
errors: [...this._errors],
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
getRunState() {
|
|
522
|
+
return {
|
|
523
|
+
runId: this.runId,
|
|
524
|
+
meta: { name: '', description: '' },
|
|
525
|
+
status: 'running',
|
|
526
|
+
phases: [...this.phases.values()],
|
|
527
|
+
agentCalls: this.agentRecords,
|
|
528
|
+
errors: [...this._errors],
|
|
529
|
+
startedAt: '',
|
|
530
|
+
totalTokens: this.totalSpentTokens,
|
|
531
|
+
estimatedCost: null,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
getLogMessages() {
|
|
535
|
+
return [...this._logMessages];
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
539
|
+
// Script Runtime
|
|
540
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
541
|
+
export class WorkflowScriptRuntime {
|
|
542
|
+
options;
|
|
543
|
+
constructor(options) {
|
|
544
|
+
this.options = options;
|
|
545
|
+
}
|
|
546
|
+
async execute(script) {
|
|
547
|
+
// Validate
|
|
548
|
+
validateScript(script);
|
|
549
|
+
// Extract meta
|
|
550
|
+
const meta = extractMeta(script);
|
|
551
|
+
// Create Host Bridge
|
|
552
|
+
const bridge = new HostBridge(this.options);
|
|
553
|
+
// Build sandbox context
|
|
554
|
+
const sandbox = {
|
|
555
|
+
agent: (prompt, opts) => bridge.agent(prompt, opts),
|
|
556
|
+
parallel: (thunks) => bridge.parallel(thunks),
|
|
557
|
+
pipeline: (items, ...stages) => bridge.pipeline(items, ...stages),
|
|
558
|
+
phase: (title) => bridge.setPhase(title),
|
|
559
|
+
log: (message) => bridge.log(message),
|
|
560
|
+
budget: bridge.createBudget(),
|
|
561
|
+
args: this.options.args,
|
|
562
|
+
meta,
|
|
563
|
+
};
|
|
564
|
+
// Strip the `export const meta = ...` block and execute the script body
|
|
565
|
+
const body = script.replace(/export\s+const\s+meta\s*=\s*\{[\s\S]*?\};/, '').trim();
|
|
566
|
+
// Wrap in async IIFE (vm.runInContext doesn't support top-level await).
|
|
567
|
+
// Store the result in __sandbox__.__result so we can read it after execution.
|
|
568
|
+
const wrappedScript = [
|
|
569
|
+
`(async () => {`,
|
|
570
|
+
`const { agent, parallel, pipeline, phase, log, budget, args, meta } = __sandbox__;`,
|
|
571
|
+
body,
|
|
572
|
+
`}).call(null);`,
|
|
573
|
+
].join('\n');
|
|
574
|
+
this.options.onEvent?.({
|
|
575
|
+
type: 'workflow.script.start',
|
|
576
|
+
runId: bridge.getRunState().runId,
|
|
577
|
+
workflowName: meta.name,
|
|
578
|
+
phases: meta.phases,
|
|
579
|
+
timestamp: new Date().toISOString(),
|
|
580
|
+
});
|
|
581
|
+
const startedAt = Date.now();
|
|
582
|
+
try {
|
|
583
|
+
// Execute in vm.Script (not vm.Module since we handle modules differently)
|
|
584
|
+
const vmContext = vm.createContext({
|
|
585
|
+
__sandbox__: sandbox,
|
|
586
|
+
console: {
|
|
587
|
+
log: (...args) => bridge.log(args.map(String).join(' ')),
|
|
588
|
+
error: (...args) => bridge.log(`[ERROR] ${args.map(String).join(' ')}`),
|
|
589
|
+
},
|
|
590
|
+
setTimeout: () => { throw new Error('setTimeout is not available in workflow scripts.'); },
|
|
591
|
+
setInterval: () => { throw new Error('setInterval is not available in workflow scripts.'); },
|
|
592
|
+
clearTimeout: () => { },
|
|
593
|
+
clearInterval: () => { },
|
|
594
|
+
process: undefined,
|
|
595
|
+
require: undefined,
|
|
596
|
+
import: undefined,
|
|
597
|
+
fs: undefined,
|
|
598
|
+
net: undefined,
|
|
599
|
+
child_process: undefined,
|
|
600
|
+
});
|
|
601
|
+
const result = await vm.runInContext(wrappedScript, vmContext, {
|
|
602
|
+
filename: `workflow-${meta.name}.js`,
|
|
603
|
+
timeout: 600_000, // 10 min max
|
|
604
|
+
});
|
|
605
|
+
const durationMs = Date.now() - startedAt;
|
|
606
|
+
const state = bridge.getRunState();
|
|
607
|
+
state.meta = meta;
|
|
608
|
+
state.startedAt = new Date(startedAt).toISOString();
|
|
609
|
+
state.completedAt = new Date().toISOString();
|
|
610
|
+
state.status = 'completed';
|
|
611
|
+
this.options.onEvent?.({
|
|
612
|
+
type: 'workflow.script.done',
|
|
613
|
+
runId: state.runId,
|
|
614
|
+
workflowName: meta.name,
|
|
615
|
+
status: 'completed',
|
|
616
|
+
durationMs,
|
|
617
|
+
agentCount: state.agentCalls.length,
|
|
618
|
+
totalTokens: state.totalTokens,
|
|
619
|
+
errors: state.errors.length > 0 ? state.errors : undefined,
|
|
620
|
+
timestamp: new Date().toISOString(),
|
|
621
|
+
});
|
|
622
|
+
return {
|
|
623
|
+
result,
|
|
624
|
+
state,
|
|
625
|
+
resumeState: bridge.getResumeState(),
|
|
626
|
+
logs: bridge.getLogMessages(),
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
catch (err) {
|
|
630
|
+
const durationMs = Date.now() - startedAt;
|
|
631
|
+
const state = bridge.getRunState();
|
|
632
|
+
state.meta = meta;
|
|
633
|
+
state.startedAt = new Date(startedAt).toISOString();
|
|
634
|
+
state.completedAt = new Date().toISOString();
|
|
635
|
+
state.status = 'failed';
|
|
636
|
+
state.errors.push({ agentId: 'script', error: err.message });
|
|
637
|
+
this.options.onEvent?.({
|
|
638
|
+
type: 'workflow.script.done',
|
|
639
|
+
runId: state.runId,
|
|
640
|
+
workflowName: meta.name,
|
|
641
|
+
status: 'failed',
|
|
642
|
+
durationMs,
|
|
643
|
+
agentCount: state.agentCalls.length,
|
|
644
|
+
totalTokens: state.totalTokens,
|
|
645
|
+
errors: state.errors,
|
|
646
|
+
timestamp: new Date().toISOString(),
|
|
647
|
+
});
|
|
648
|
+
throw err;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
function delay(ms) {
|
|
653
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
654
|
+
}
|
|
655
|
+
//# sourceMappingURL=workflowScriptRuntime.js.map
|