foliko 2.0.22 → 2.0.24
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/.editorconfig +56 -56
- package/.lintstagedrc +7 -7
- package/.prettierignore +29 -29
- package/.prettierrc +11 -11
- package/Dockerfile +63 -63
- package/install.ps1 +129 -129
- package/install.sh +121 -121
- package/package.json +2 -5
- package/plugins/core/sub-agent/index.js +8 -4
- package/plugins/core/workflow/context.js +941 -941
- package/plugins/core/workflow/engine.js +66 -66
- package/plugins/core/workflow/js-runner.js +318 -318
- package/plugins/core/workflow/json-runner.js +323 -323
- package/plugins/core/workflow/stages/choice.js +74 -74
- package/plugins/core/workflow/stages/each.js +123 -123
- package/plugins/core/workflow/stages/parallel.js +69 -69
- package/plugins/io/file-system/index.js +1 -1
- package/skills/find-skills/SKILL.md +133 -133
- package/src/agent/chat.js +188 -221
- package/src/agent/sub.js +28 -25
- package/src/agent/tool-loop.js +648 -0
- package/src/common/constants.js +2 -2
- package/src/llm/provider.js +12 -28
- package/src/plugin/base.js +18 -14
- package/src/plugin/manager.js +19 -0
- package/tests/core/chat-tool.test.js +187 -187
- package/tests/core/disable-thinking.test.js +64 -0
- package/tests/core/reasoning-content.test.js +129 -0
- package/tests/core/strip-stale-tool-calls.test.js +154 -0
- package/tests/core/tool-loop.test.js +208 -0
|
@@ -1,941 +1,941 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FlowContext - Shared context for v2 workflow execution
|
|
3
|
-
* Provides $.push(), $.next(), $.send() API for JS format
|
|
4
|
-
* and manages stage results for JSON format
|
|
5
|
-
*
|
|
6
|
-
* Chain-style API support:
|
|
7
|
-
* - $.tool - Direct tool registry/execution (list, execute, register)
|
|
8
|
-
* - $.extension - Extension tools (list, register, remove, execute)
|
|
9
|
-
* - $.workflow - Nested workflow calls (list, execute)
|
|
10
|
-
* - $.agent - Agent creation/calls (create, chat, list)
|
|
11
|
-
* - $.prompt - Prompt/chat operations
|
|
12
|
-
* - $.skill - Skill commands (list, execute)
|
|
13
|
-
* - $.logger - Logging operations (debug, info, warn, error, trace, fatal)
|
|
14
|
-
* - $.storage - Key-value storage operations (set, get, delete, list, clear)
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const { runScriptSafely } = require('../../../src/utils/sandbox');
|
|
18
|
-
|
|
19
|
-
class FlowContext {
|
|
20
|
-
constructor(input = {}, sessionId = null) {
|
|
21
|
-
// Input from workflow caller
|
|
22
|
-
this.input = input;
|
|
23
|
-
|
|
24
|
-
// Flow-level persistent data (like $.data)
|
|
25
|
-
this.data = {};
|
|
26
|
-
|
|
27
|
-
// Stage results storage (stageName -> result)
|
|
28
|
-
this._results = new Map();
|
|
29
|
-
|
|
30
|
-
// Current stage info
|
|
31
|
-
this.current = null;
|
|
32
|
-
this.prev = null;
|
|
33
|
-
|
|
34
|
-
// Session ID for tool execution
|
|
35
|
-
this._sessionId = sessionId;
|
|
36
|
-
|
|
37
|
-
// Framework reference for tool execution
|
|
38
|
-
this._framework = null;
|
|
39
|
-
|
|
40
|
-
// Destruction flag
|
|
41
|
-
this._destroyed = false;
|
|
42
|
-
|
|
43
|
-
// Pending next stage (for async transitions)
|
|
44
|
-
this._pendingNext = null;
|
|
45
|
-
|
|
46
|
-
// Log prefix
|
|
47
|
-
this._log = require('../../../src/common/logger').logger.child('FlowContext');
|
|
48
|
-
|
|
49
|
-
// Lazy-loaded chain-style API objects
|
|
50
|
-
this._toolAccessor = null;
|
|
51
|
-
this._extensionAccessor = null;
|
|
52
|
-
this._workflowAccessor = null;
|
|
53
|
-
this._agentAccessor = null;
|
|
54
|
-
this._promptAccessor = null;
|
|
55
|
-
this._skillAccessor = null;
|
|
56
|
-
this._loggerAccessor = null;
|
|
57
|
-
this._storageAccessor = null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Set framework reference (called by engine)
|
|
62
|
-
*/
|
|
63
|
-
setFramework(framework) {
|
|
64
|
-
this._framework = framework;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ============================================================
|
|
68
|
-
// $.tool - Direct tool registry and execution
|
|
69
|
-
// ============================================================
|
|
70
|
-
get tool() {
|
|
71
|
-
if (!this._toolAccessor) {
|
|
72
|
-
this._toolAccessor = this._createToolAccessor();
|
|
73
|
-
}
|
|
74
|
-
return this._toolAccessor;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
_createToolAccessor() {
|
|
78
|
-
const context = this;
|
|
79
|
-
const log = this._log;
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
/**
|
|
83
|
-
* List all registered tools
|
|
84
|
-
*/
|
|
85
|
-
list: () => {
|
|
86
|
-
if (!context._framework) return [];
|
|
87
|
-
return context._framework.toolRegistry ? context._framework.toolRegistry.listTools() : [];
|
|
88
|
-
},
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Execute a tool directly
|
|
92
|
-
*/
|
|
93
|
-
execute: async (name, args = {}) => {
|
|
94
|
-
return await context._executeTool(name, args);
|
|
95
|
-
},
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Register a new tool (temporary, workflow-scoped)
|
|
99
|
-
*/
|
|
100
|
-
register: (toolDef) => {
|
|
101
|
-
log.warn('tool.register() is not implemented in workflow context - tools must be registered via plugin');
|
|
102
|
-
},
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Get tool definition
|
|
106
|
-
*/
|
|
107
|
-
get: (name) => {
|
|
108
|
-
if (!context._framework?.toolRegistry) return null;
|
|
109
|
-
return context._framework.toolRegistry.getTool(name);
|
|
110
|
-
},
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// ============================================================
|
|
115
|
-
// $.extension - Extension tools (like Plugin.extension accessor)
|
|
116
|
-
// ============================================================
|
|
117
|
-
get extension() {
|
|
118
|
-
if (!this._extensionAccessor) {
|
|
119
|
-
this._extensionAccessor = this._createExtensionAccessor();
|
|
120
|
-
}
|
|
121
|
-
return this._extensionAccessor;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
_createExtensionAccessor() {
|
|
125
|
-
const context = this;
|
|
126
|
-
const log = this._log;
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
/**
|
|
130
|
-
* List all registered extensions
|
|
131
|
-
*/
|
|
132
|
-
list: () => {
|
|
133
|
-
if (!context._framework) return [];
|
|
134
|
-
const extExec = context._framework.pluginManager?.get('extension-executor');
|
|
135
|
-
if (!extExec?._registry) return [];
|
|
136
|
-
return extExec._registry.listExtensions();
|
|
137
|
-
},
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Register an extension tool
|
|
141
|
-
*/
|
|
142
|
-
register: (toolDef) => {
|
|
143
|
-
if (!context._framework) {
|
|
144
|
-
log.warn('extension.register() called before framework is set');
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
if (!toolDef || !toolDef.name) {
|
|
148
|
-
log.warn('extension.register() missing tool name');
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const extExec = context._framework.pluginManager?.get('extension-executor');
|
|
153
|
-
if (!extExec) {
|
|
154
|
-
log.warn('extension-executor plugin not found');
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
extExec.registerTool('workflow', {
|
|
159
|
-
name: 'workflow',
|
|
160
|
-
description: 'Workflow scoped extensions',
|
|
161
|
-
version: '1.0.0',
|
|
162
|
-
}, toolDef);
|
|
163
|
-
|
|
164
|
-
extExec._invalidatePromptCaches(context._framework);
|
|
165
|
-
log.debug(`[Workflow] Registered extension tool: workflow/${toolDef.name}`);
|
|
166
|
-
},
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Remove an extension tool
|
|
170
|
-
*/
|
|
171
|
-
remove: (toolName) => {
|
|
172
|
-
if (!context._framework) {
|
|
173
|
-
log.warn('extension.remove() called before framework is set');
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const extExec = context._framework.pluginManager?.get('extension-executor');
|
|
178
|
-
if (extExec) {
|
|
179
|
-
extExec._unregisterTool('workflow', toolName);
|
|
180
|
-
}
|
|
181
|
-
log.debug(`[Workflow] Removed extension tool: workflow/${toolName}`);
|
|
182
|
-
},
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Execute an extension tool
|
|
186
|
-
* Supports: extension.execute('tool-name', args) or extension.execute('plugin', 'tool-name', args)
|
|
187
|
-
*/
|
|
188
|
-
execute: async (pluginNameOrToolName, toolNameOrArgs, args = {}) => {
|
|
189
|
-
if (!context._framework) {
|
|
190
|
-
throw new Error('extension.execute() called before framework is set');
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const extExec = context._framework.pluginManager?.get('extension-executor');
|
|
194
|
-
if (!extExec) {
|
|
195
|
-
return { success: false, error: 'extension-executor not found' };
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
let pluginName, toolName;
|
|
199
|
-
if (typeof toolNameOrArgs === 'object') {
|
|
200
|
-
// extension.execute('tool-name', args)
|
|
201
|
-
pluginName = 'workflow';
|
|
202
|
-
toolName = pluginNameOrToolName;
|
|
203
|
-
args = toolNameOrArgs;
|
|
204
|
-
} else {
|
|
205
|
-
// extension.execute('plugin', 'tool-name', args)
|
|
206
|
-
pluginName = pluginNameOrToolName;
|
|
207
|
-
toolName = toolNameOrArgs;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const result = await extExec._executeTool(pluginName, toolName, args, context._framework);
|
|
211
|
-
return result;
|
|
212
|
-
},
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Get extension info
|
|
216
|
-
*/
|
|
217
|
-
get: (pluginName) => {
|
|
218
|
-
if (!context._framework) return null;
|
|
219
|
-
const extExec = context._framework.pluginManager?.get('extension-executor');
|
|
220
|
-
if (!extExec?._registry) return null;
|
|
221
|
-
return extExec._registry.getExtension(pluginName);
|
|
222
|
-
},
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// ============================================================
|
|
227
|
-
// $.workflow - Nested workflow operations
|
|
228
|
-
// ============================================================
|
|
229
|
-
get workflow() {
|
|
230
|
-
if (!this._workflowAccessor) {
|
|
231
|
-
this._workflowAccessor = this._createWorkflowAccessor();
|
|
232
|
-
}
|
|
233
|
-
return this._workflowAccessor;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
_createWorkflowAccessor() {
|
|
237
|
-
const context = this;
|
|
238
|
-
const log = this._log;
|
|
239
|
-
|
|
240
|
-
return {
|
|
241
|
-
/**
|
|
242
|
-
* List all available workflows
|
|
243
|
-
*/
|
|
244
|
-
list: () => {
|
|
245
|
-
if (!context._framework) return [];
|
|
246
|
-
const workflowPlugin = context._framework.pluginManager?.get('workflow');
|
|
247
|
-
if (!workflowPlugin?._workflows) return [];
|
|
248
|
-
return Array.from(workflowPlugin._workflows.keys());
|
|
249
|
-
},
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Execute a workflow by name
|
|
253
|
-
*/
|
|
254
|
-
execute: async (workflowName, input = {}, sessionId = null) => {
|
|
255
|
-
if (!context._framework) {
|
|
256
|
-
throw new Error('workflow.execute() called before framework is set');
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const workflowPlugin = context._framework.pluginManager?.get('workflow');
|
|
260
|
-
if (!workflowPlugin) {
|
|
261
|
-
return { success: false, error: 'workflow plugin not found' };
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const sid = sessionId || context._sessionId;
|
|
265
|
-
return await workflowPlugin.executeWorkflow(workflowName, input, sid);
|
|
266
|
-
},
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Get workflow definition
|
|
270
|
-
*/
|
|
271
|
-
get: (workflowName) => {
|
|
272
|
-
if (!context._framework) return null;
|
|
273
|
-
const workflowPlugin = context._framework.pluginManager?.get('workflow');
|
|
274
|
-
if (!workflowPlugin?._workflows) return null;
|
|
275
|
-
return workflowPlugin._workflows.get(workflowName);
|
|
276
|
-
},
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// ============================================================
|
|
281
|
-
// $.agent - Agent operations
|
|
282
|
-
// ============================================================
|
|
283
|
-
get agent() {
|
|
284
|
-
if (!this._agentAccessor) {
|
|
285
|
-
this._agentAccessor = this._createAgentAccessor();
|
|
286
|
-
}
|
|
287
|
-
return this._agentAccessor;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
_createAgentAccessor() {
|
|
291
|
-
const context = this;
|
|
292
|
-
const log = this._log;
|
|
293
|
-
|
|
294
|
-
return {
|
|
295
|
-
/**
|
|
296
|
-
* Create a new agent
|
|
297
|
-
*/
|
|
298
|
-
create: (config = {}) => {
|
|
299
|
-
if (!context._framework) {
|
|
300
|
-
throw new Error('agent.create() called before framework is set');
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const resolvedConfig = context.resolveArgs(config);
|
|
304
|
-
let sessionId = context._sessionId;
|
|
305
|
-
|
|
306
|
-
if (sessionId && context._framework.runWithContext) {
|
|
307
|
-
return context._framework.runWithContext(
|
|
308
|
-
{ sessionId },
|
|
309
|
-
() => context._framework.createAgent(resolvedConfig)
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
return context._framework.createAgent(resolvedConfig);
|
|
313
|
-
},
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Chat with an agent
|
|
317
|
-
*/
|
|
318
|
-
chat: async (config = {}) => {
|
|
319
|
-
if (!context._framework) {
|
|
320
|
-
throw new Error('agent.chat() called before framework is set');
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const resolvedConfig = context.resolveArgs(config);
|
|
324
|
-
const { agentId, message, ...options } = resolvedConfig;
|
|
325
|
-
|
|
326
|
-
if (!message) {
|
|
327
|
-
throw new Error('agent.chat() requires message');
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
let sessionId = context._sessionId;
|
|
331
|
-
|
|
332
|
-
if (sessionId && context._framework.runWithContext) {
|
|
333
|
-
return await context._framework.runWithContext(
|
|
334
|
-
{ sessionId },
|
|
335
|
-
async () => {
|
|
336
|
-
const agent = agentId
|
|
337
|
-
? context._framework.createAgent({ id: agentId, hidden: true, ...options })
|
|
338
|
-
: context._framework.createSubAgent({ hidden: true, ...options });
|
|
339
|
-
return await agent.chat(message, options);
|
|
340
|
-
}
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const agent = agentId
|
|
345
|
-
? context._framework.createAgent({ id: agentId, hidden: true, ...options })
|
|
346
|
-
: context._framework.createSubAgent({ hidden: true, ...options });
|
|
347
|
-
return await agent.chat(message, options);
|
|
348
|
-
},
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* List active agents
|
|
352
|
-
*/
|
|
353
|
-
list: () => {
|
|
354
|
-
if (!context._framework) return [];
|
|
355
|
-
return context._framework._agents || [];
|
|
356
|
-
},
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// ============================================================
|
|
361
|
-
// $.prompt - Prompt operations
|
|
362
|
-
// ============================================================
|
|
363
|
-
get prompt() {
|
|
364
|
-
if (!this._promptAccessor) {
|
|
365
|
-
this._promptAccessor = this._createPromptAccessor();
|
|
366
|
-
}
|
|
367
|
-
return this._promptAccessor;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
_createPromptAccessor() {
|
|
371
|
-
const context = this;
|
|
372
|
-
const log = this._log;
|
|
373
|
-
|
|
374
|
-
return {
|
|
375
|
-
/**
|
|
376
|
-
* Send a prompt and get response (creates temporary agent)
|
|
377
|
-
*/
|
|
378
|
-
send: async (config = {}) => {
|
|
379
|
-
if (!context._framework) {
|
|
380
|
-
throw new Error('prompt.send() called before framework is set');
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const resolvedConfig = context.resolveArgs(config);
|
|
384
|
-
const { message, role, systemPrompt, ...options } = resolvedConfig;
|
|
385
|
-
|
|
386
|
-
if (!message) {
|
|
387
|
-
throw new Error('prompt.send() requires message');
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
let sessionId = context._sessionId;
|
|
391
|
-
|
|
392
|
-
if (sessionId && context._framework.runWithContext) {
|
|
393
|
-
return await context._framework.runWithContext(
|
|
394
|
-
{ sessionId },
|
|
395
|
-
async () => {
|
|
396
|
-
const agent = context._framework.createSubAgent({
|
|
397
|
-
role: role || 'Assistant',
|
|
398
|
-
systemPrompt: systemPrompt || '',
|
|
399
|
-
hidden: true,
|
|
400
|
-
});
|
|
401
|
-
return await agent.chat(message, options);
|
|
402
|
-
}
|
|
403
|
-
);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const agent = context._framework.createSubAgent({
|
|
407
|
-
role: role || 'Assistant',
|
|
408
|
-
systemPrompt: systemPrompt || '',
|
|
409
|
-
hidden: true,
|
|
410
|
-
});
|
|
411
|
-
return await agent.chat(message, options);
|
|
412
|
-
},
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Chat with a prompt (alias for send)
|
|
416
|
-
*/
|
|
417
|
-
chat: async (config = {}) => {
|
|
418
|
-
return this.send(config);
|
|
419
|
-
},
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// ============================================================
|
|
424
|
-
// $.skill - Skill commands
|
|
425
|
-
// ============================================================
|
|
426
|
-
get skill() {
|
|
427
|
-
if (!this._skillAccessor) {
|
|
428
|
-
this._skillAccessor = this._createSkillAccessor();
|
|
429
|
-
}
|
|
430
|
-
return this._skillAccessor;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
_createSkillAccessor() {
|
|
434
|
-
const context = this;
|
|
435
|
-
const log = this._log;
|
|
436
|
-
|
|
437
|
-
return {
|
|
438
|
-
/**
|
|
439
|
-
* List available skills
|
|
440
|
-
*/
|
|
441
|
-
list: () => {
|
|
442
|
-
if (!context._framework) return [];
|
|
443
|
-
const extExec = context._framework.pluginManager?.get('extension-executor');
|
|
444
|
-
if (!extExec?._registry) return [];
|
|
445
|
-
const extensions = extExec._registry.listExtensions();
|
|
446
|
-
return extensions
|
|
447
|
-
.filter(e => e.name.startsWith('skill:'))
|
|
448
|
-
.map(e => e.name.replace('skill:', ''));
|
|
449
|
-
},
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* Execute a skill command
|
|
453
|
-
* Supports: skill.execute('skill-name', 'command', args)
|
|
454
|
-
*/
|
|
455
|
-
execute: async (skillName, commandOrArgs, args = {}) => {
|
|
456
|
-
if (!context._framework) {
|
|
457
|
-
throw new Error('skill.execute() called before framework is set');
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
let command, toolArgs;
|
|
461
|
-
if (typeof commandOrArgs === 'object') {
|
|
462
|
-
command = commandOrArgs.command || commandOrArgs.tool;
|
|
463
|
-
toolArgs = commandOrArgs;
|
|
464
|
-
} else {
|
|
465
|
-
command = commandOrArgs;
|
|
466
|
-
toolArgs = args;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
if (!command) {
|
|
470
|
-
throw new Error('skill.execute() requires command');
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
const plugin = `skill:${skillName}`;
|
|
474
|
-
const resolvedArgs = context.resolveArgs(toolArgs);
|
|
475
|
-
|
|
476
|
-
if (context._sessionId && context._framework.runWithContext) {
|
|
477
|
-
return await context._framework.runWithContext(
|
|
478
|
-
{ sessionId: context._sessionId },
|
|
479
|
-
async () => await context._framework.executeTool('ext_call', {
|
|
480
|
-
plugin,
|
|
481
|
-
tool: command,
|
|
482
|
-
args: resolvedArgs,
|
|
483
|
-
})
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
return await context._framework.executeTool('ext_call', {
|
|
487
|
-
plugin,
|
|
488
|
-
tool: command,
|
|
489
|
-
args: resolvedArgs,
|
|
490
|
-
});
|
|
491
|
-
},
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* Get skill info
|
|
495
|
-
*/
|
|
496
|
-
get: (skillName) => {
|
|
497
|
-
if (!context._framework) return null;
|
|
498
|
-
const extExec = context._framework.pluginManager?.get('extension-executor');
|
|
499
|
-
if (!extExec?._registry) return null;
|
|
500
|
-
return extExec._registry.getExtension(`skill:${skillName}`);
|
|
501
|
-
},
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// ============================================================
|
|
506
|
-
// $.logger - Logging operations
|
|
507
|
-
// ============================================================
|
|
508
|
-
get logger() {
|
|
509
|
-
if (!this._loggerAccessor) {
|
|
510
|
-
this._loggerAccessor = this._createLoggerAccessor();
|
|
511
|
-
}
|
|
512
|
-
return this._loggerAccessor;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
_createLoggerAccessor() {
|
|
516
|
-
const context = this;
|
|
517
|
-
const flowLog = this._log;
|
|
518
|
-
|
|
519
|
-
// Get the underlying logger methods
|
|
520
|
-
const LEVELS = ['debug', 'info', 'warn', 'error', 'trace', 'fatal'];
|
|
521
|
-
|
|
522
|
-
const loggerObj = {
|
|
523
|
-
/**
|
|
524
|
-
* Log debug message
|
|
525
|
-
*/
|
|
526
|
-
debug: (...args) => {
|
|
527
|
-
flowLog.debug(...args);
|
|
528
|
-
},
|
|
529
|
-
|
|
530
|
-
/**
|
|
531
|
-
* Log info message
|
|
532
|
-
*/
|
|
533
|
-
info: (...args) => {
|
|
534
|
-
flowLog.info(...args);
|
|
535
|
-
},
|
|
536
|
-
|
|
537
|
-
/**
|
|
538
|
-
* Log warning message
|
|
539
|
-
*/
|
|
540
|
-
warn: (...args) => {
|
|
541
|
-
flowLog.warn(...args);
|
|
542
|
-
},
|
|
543
|
-
|
|
544
|
-
/**
|
|
545
|
-
* Log error message
|
|
546
|
-
*/
|
|
547
|
-
error: (...args) => {
|
|
548
|
-
flowLog.error(...args);
|
|
549
|
-
},
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
* Log trace message
|
|
553
|
-
*/
|
|
554
|
-
trace: (...args) => {
|
|
555
|
-
flowLog.trace(...args);
|
|
556
|
-
},
|
|
557
|
-
|
|
558
|
-
/**
|
|
559
|
-
* Log fatal message
|
|
560
|
-
*/
|
|
561
|
-
fatal: (...args) => {
|
|
562
|
-
flowLog.fatal(...args);
|
|
563
|
-
},
|
|
564
|
-
|
|
565
|
-
/**
|
|
566
|
-
* Create a child logger with namespace
|
|
567
|
-
*/
|
|
568
|
-
child: (namespace) => {
|
|
569
|
-
return flowLog.child(namespace);
|
|
570
|
-
},
|
|
571
|
-
|
|
572
|
-
/**
|
|
573
|
-
* Log with custom level
|
|
574
|
-
*/
|
|
575
|
-
log: (level, ...args) => {
|
|
576
|
-
if (typeof flowLog[level] === 'function') {
|
|
577
|
-
flowLog[level](...args);
|
|
578
|
-
} else {
|
|
579
|
-
flowLog.info(...args);
|
|
580
|
-
}
|
|
581
|
-
},
|
|
582
|
-
};
|
|
583
|
-
|
|
584
|
-
return loggerObj;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// ============================================================
|
|
588
|
-
// $.storage - Storage operations
|
|
589
|
-
// ============================================================
|
|
590
|
-
get storage() {
|
|
591
|
-
if (!this._storageAccessor) {
|
|
592
|
-
this._storageAccessor = this._createStorageAccessor();
|
|
593
|
-
}
|
|
594
|
-
return this._storageAccessor;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
_createStorageAccessor() {
|
|
598
|
-
const context = this;
|
|
599
|
-
|
|
600
|
-
return {
|
|
601
|
-
/**
|
|
602
|
-
* Set a value in storage
|
|
603
|
-
*/
|
|
604
|
-
set: async (key, value, namespace) => {
|
|
605
|
-
return await context._executeTool('storage_set', { key, value, namespace });
|
|
606
|
-
},
|
|
607
|
-
|
|
608
|
-
/**
|
|
609
|
-
* Get a value from storage
|
|
610
|
-
*/
|
|
611
|
-
get: async (key, namespace) => {
|
|
612
|
-
return await context._executeTool('storage_get', { key, namespace });
|
|
613
|
-
},
|
|
614
|
-
|
|
615
|
-
/**
|
|
616
|
-
* Delete a value from storage
|
|
617
|
-
*/
|
|
618
|
-
delete: async (key, namespace) => {
|
|
619
|
-
return await context._executeTool('storage_delete', { key, namespace });
|
|
620
|
-
},
|
|
621
|
-
|
|
622
|
-
/**
|
|
623
|
-
* List all keys in a namespace
|
|
624
|
-
*/
|
|
625
|
-
list: async (namespace) => {
|
|
626
|
-
return await context._executeTool('storage_list', { namespace });
|
|
627
|
-
},
|
|
628
|
-
|
|
629
|
-
/**
|
|
630
|
-
* Clear all values in a namespace
|
|
631
|
-
*/
|
|
632
|
-
clear: async (namespace) => {
|
|
633
|
-
return await context._executeTool('storage_clear', { namespace });
|
|
634
|
-
},
|
|
635
|
-
|
|
636
|
-
/**
|
|
637
|
-
* Watch a key for changes
|
|
638
|
-
*/
|
|
639
|
-
watch: async (key, namespace) => {
|
|
640
|
-
return await context._executeTool('storage_watch', { key, namespace });
|
|
641
|
-
},
|
|
642
|
-
|
|
643
|
-
/**
|
|
644
|
-
* Get storage statistics
|
|
645
|
-
*/
|
|
646
|
-
stats: async (namespace) => {
|
|
647
|
-
return await context._executeTool('storage_stats', { namespace });
|
|
648
|
-
},
|
|
649
|
-
|
|
650
|
-
/**
|
|
651
|
-
* Trigger storage compaction
|
|
652
|
-
*/
|
|
653
|
-
compact: async (namespace) => {
|
|
654
|
-
return await context._executeTool('storage_compact', { namespace });
|
|
655
|
-
},
|
|
656
|
-
};
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// ============================================================
|
|
660
|
-
// NOTE: 之前这里有 `async tool(name, args)` legacy method,
|
|
661
|
-
// 但与上方的 `get tool()` 冲突 (同名),JavaScript class
|
|
662
|
-
// 让后定义的方法覆盖 getter,导致 $.tool 返回的永远是
|
|
663
|
-
// async function 而非 accessor object。
|
|
664
|
-
// 已删除此 legacy method;新 API 用 `$.tool.execute(name, args)`
|
|
665
|
-
// 走 get tool() 返回的 accessor 对象。
|
|
666
|
-
// ============================================================
|
|
667
|
-
|
|
668
|
-
/**
|
|
669
|
-
* Execute a tool directly
|
|
670
|
-
*/
|
|
671
|
-
async _executeTool(toolName, args) {
|
|
672
|
-
if (!this._framework) {
|
|
673
|
-
throw new Error('Framework not set');
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// Resolve args with variable interpolation
|
|
677
|
-
const resolvedArgs = this.resolveArgs(args || {});
|
|
678
|
-
|
|
679
|
-
let sessionId = this._sessionId;
|
|
680
|
-
if (!sessionId && this._framework.getExecutionContext) {
|
|
681
|
-
const ctx = this._framework.getExecutionContext();
|
|
682
|
-
sessionId = ctx?.sessionId;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
if (sessionId && this._framework.runWithContext) {
|
|
686
|
-
return await this._framework.runWithContext(
|
|
687
|
-
{ sessionId },
|
|
688
|
-
async () => await this._framework.executeTool(toolName, resolvedArgs)
|
|
689
|
-
);
|
|
690
|
-
}
|
|
691
|
-
return await this._framework.executeTool(toolName, resolvedArgs);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
// ============================================================
|
|
695
|
-
// Basic Flow API
|
|
696
|
-
// ============================================================
|
|
697
|
-
|
|
698
|
-
/**
|
|
699
|
-
* Register a stage (JS format - $.push)
|
|
700
|
-
*/
|
|
701
|
-
push(name, fn) {
|
|
702
|
-
if (!this._stages) {
|
|
703
|
-
this._stages = new Map();
|
|
704
|
-
}
|
|
705
|
-
this._stages.set(name, fn);
|
|
706
|
-
return this;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
/**
|
|
710
|
-
* Go to next stage (sync) - $.next
|
|
711
|
-
* 支持直接传参: $.next('stage-name', arg1, arg2, ...)
|
|
712
|
-
* 等同�?$.send({ arg1, arg2, ... }) 后再 $.next('stage-name')
|
|
713
|
-
*/
|
|
714
|
-
next(name, ...args) {
|
|
715
|
-
this._pendingNext = name;
|
|
716
|
-
if (args.length > 0) {
|
|
717
|
-
this._sentData = args.length === 1 ? args[0] : args;
|
|
718
|
-
}
|
|
719
|
-
return this;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
/**
|
|
723
|
-
* Go to next stage (async) - $.next2
|
|
724
|
-
* Returns a function for deferred transition
|
|
725
|
-
*/
|
|
726
|
-
next2(name) {
|
|
727
|
-
return () => {
|
|
728
|
-
this.next(name);
|
|
729
|
-
};
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
/**
|
|
733
|
-
* Pass data to next stage - $.send
|
|
734
|
-
*/
|
|
735
|
-
send(data) {
|
|
736
|
-
if (data !== undefined) {
|
|
737
|
-
this._sentData = data;
|
|
738
|
-
}
|
|
739
|
-
return this;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
/**
|
|
743
|
-
* Built-in delay helper - $.wait
|
|
744
|
-
*/
|
|
745
|
-
wait(ms) {
|
|
746
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
/**
|
|
750
|
-
* Execute a script - $.script
|
|
751
|
-
*/
|
|
752
|
-
async script(code) {
|
|
753
|
-
const scriptContext = {
|
|
754
|
-
input: this.input,
|
|
755
|
-
data: this.data,
|
|
756
|
-
variables: Object.fromEntries(this._results),
|
|
757
|
-
previousResult: this._sentData,
|
|
758
|
-
console: {
|
|
759
|
-
log: (...args) => {
|
|
760
|
-
this._log.debug('[Script]', ...args);
|
|
761
|
-
},
|
|
762
|
-
},
|
|
763
|
-
};
|
|
764
|
-
return await runScriptSafely(code, scriptContext, { timeout: 10000 });
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
/**
|
|
768
|
-
* End workflow - $.destroy
|
|
769
|
-
*/
|
|
770
|
-
destroy() {
|
|
771
|
-
this._destroyed = true;
|
|
772
|
-
this._pendingNext = null;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
/**
|
|
776
|
-
* Get result from a named stage
|
|
777
|
-
*/
|
|
778
|
-
getResult(stageName) {
|
|
779
|
-
return this._results.get(stageName);
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
/**
|
|
783
|
-
* Set result for a stage
|
|
784
|
-
*/
|
|
785
|
-
setResult(stageName, result) {
|
|
786
|
-
this._results.set(stageName, result);
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// ============================================================
|
|
790
|
-
// Variable Resolution
|
|
791
|
-
// ============================================================
|
|
792
|
-
|
|
793
|
-
/**
|
|
794
|
-
* Resolve a value with variable interpolation
|
|
795
|
-
* Supports {{stageName}}, {{stage.field}}, {{input.field}}, {{data.field}}
|
|
796
|
-
*
|
|
797
|
-
* @param {*} value - Value to resolve (string templates only)
|
|
798
|
-
* @param {Object} [options]
|
|
799
|
-
* @param {boolean} [options.raw=false] - If true, return raw resolved value (preserve type).
|
|
800
|
-
* Used by stages like `each` that need actual arrays/objects
|
|
801
|
-
* instead of stringified versions.
|
|
802
|
-
* @returns {*} Resolved value
|
|
803
|
-
*/
|
|
804
|
-
resolveValue(value, options = {}) {
|
|
805
|
-
if (typeof value !== 'string') {
|
|
806
|
-
return value;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
const { raw = false } = options;
|
|
810
|
-
|
|
811
|
-
// Replace {{stageName}} or {{stage.field}} with stage result
|
|
812
|
-
return value.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
|
|
813
|
-
const resolved = this._resolvePath(path);
|
|
814
|
-
|
|
815
|
-
// Path not found - keep the original placeholder
|
|
816
|
-
if (resolved === undefined) {
|
|
817
|
-
return match;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
// Raw mode: return the value as-is (caller handles serialization)
|
|
821
|
-
if (raw) {
|
|
822
|
-
return resolved;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
// String mode: serialize for interpolation
|
|
826
|
-
if (resolved === null) {
|
|
827
|
-
return 'null';
|
|
828
|
-
}
|
|
829
|
-
// For arrays and objects, JSON-encode so the template works as a string
|
|
830
|
-
// (Raw arrays/objects can't be inlined into strings via String.replace without coercion)
|
|
831
|
-
if (typeof resolved === 'object') {
|
|
832
|
-
try {
|
|
833
|
-
return JSON.stringify(resolved);
|
|
834
|
-
} catch {
|
|
835
|
-
return String(resolved);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
return resolved;
|
|
840
|
-
});
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
/**
|
|
844
|
-
* Resolve a template path to its value.
|
|
845
|
-
* Internal helper used by resolveValue and other template consumers.
|
|
846
|
-
* Supports: input.field, data.field, stageName, stage.field
|
|
847
|
-
* Also searches context.data when a simple path is not found in _results.
|
|
848
|
-
*
|
|
849
|
-
* @param {string} path - Template path (without {{ }} markers)
|
|
850
|
-
* @returns {*} Resolved value or undefined if not found
|
|
851
|
-
*/
|
|
852
|
-
_resolvePath(path) {
|
|
853
|
-
// input.field access
|
|
854
|
-
if (path.startsWith('input.')) {
|
|
855
|
-
const val = this._getNestedValue(this.input, path.substring(6));
|
|
856
|
-
return val !== undefined ? val : undefined;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
// data.field access
|
|
860
|
-
if (path.startsWith('data.')) {
|
|
861
|
-
const val = this._getNestedValue(this.data, path.substring(5));
|
|
862
|
-
return val !== undefined ? val : undefined;
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
// stage.field or stageName access
|
|
866
|
-
const dotIndex = path.indexOf('.');
|
|
867
|
-
if (dotIndex > 0) {
|
|
868
|
-
const stageName = path.substring(0, dotIndex);
|
|
869
|
-
const fieldPath = path.substring(dotIndex + 1);
|
|
870
|
-
// Look up the root value (try stage results first, then loop/data context)
|
|
871
|
-
// so that nested property access works for each-loop variables too,
|
|
872
|
-
// e.g. inside an each body: {{u.name}} where u is the current item.
|
|
873
|
-
let stageResult = this._results.get(stageName);
|
|
874
|
-
if (stageResult === undefined) {
|
|
875
|
-
stageResult = this.data[stageName];
|
|
876
|
-
}
|
|
877
|
-
if (stageResult !== undefined) {
|
|
878
|
-
const val = this._getNestedValue(stageResult, fieldPath);
|
|
879
|
-
if (val !== undefined) return val;
|
|
880
|
-
}
|
|
881
|
-
return undefined;
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
// Simple stageName reference: try _results first, then data
|
|
885
|
-
// (data lookup supports each-loop variables, e.g., {{fruit}} inside an each body)
|
|
886
|
-
let val = this._results.get(path);
|
|
887
|
-
if (val === undefined) {
|
|
888
|
-
val = this.data[path];
|
|
889
|
-
}
|
|
890
|
-
return val !== undefined ? val : undefined;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
/**
|
|
894
|
-
* Resolve args object with variable interpolation
|
|
895
|
-
*/
|
|
896
|
-
resolveArgs(args) {
|
|
897
|
-
if (!args || typeof args !== 'object') {
|
|
898
|
-
return args;
|
|
899
|
-
}
|
|
900
|
-
const resolved = {};
|
|
901
|
-
for (const [key, value] of Object.entries(args)) {
|
|
902
|
-
if (typeof value === 'string') {
|
|
903
|
-
resolved[key] = this.resolveValue(value);
|
|
904
|
-
} else if (Array.isArray(value)) {
|
|
905
|
-
// Resolve each element as a template (raw mode preserves types)
|
|
906
|
-
resolved[key] = value.map((v) =>
|
|
907
|
-
typeof v === 'string' ? this.resolveValue(v, { raw: true }) : v
|
|
908
|
-
);
|
|
909
|
-
} else if (typeof value === 'object' && value !== null) {
|
|
910
|
-
resolved[key] = this.resolveArgs(value);
|
|
911
|
-
} else {
|
|
912
|
-
resolved[key] = value;
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
return resolved;
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
/**
|
|
919
|
-
* Get nested value from object by dot notation path
|
|
920
|
-
*/
|
|
921
|
-
_getNestedValue(obj, path) {
|
|
922
|
-
if (!obj) return undefined;
|
|
923
|
-
const parts = path.split('.');
|
|
924
|
-
let current = obj;
|
|
925
|
-
for (const part of parts) {
|
|
926
|
-
if (current === null || current === undefined) return undefined;
|
|
927
|
-
// Handle array index in path like "items.0.name"
|
|
928
|
-
if (typeof current === 'string') {
|
|
929
|
-
try {
|
|
930
|
-
current = JSON.parse(current);
|
|
931
|
-
} catch {
|
|
932
|
-
// Not JSON, continue
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
current = current[part];
|
|
936
|
-
}
|
|
937
|
-
return current;
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
module.exports = { FlowContext };
|
|
1
|
+
/**
|
|
2
|
+
* FlowContext - Shared context for v2 workflow execution
|
|
3
|
+
* Provides $.push(), $.next(), $.send() API for JS format
|
|
4
|
+
* and manages stage results for JSON format
|
|
5
|
+
*
|
|
6
|
+
* Chain-style API support:
|
|
7
|
+
* - $.tool - Direct tool registry/execution (list, execute, register)
|
|
8
|
+
* - $.extension - Extension tools (list, register, remove, execute)
|
|
9
|
+
* - $.workflow - Nested workflow calls (list, execute)
|
|
10
|
+
* - $.agent - Agent creation/calls (create, chat, list)
|
|
11
|
+
* - $.prompt - Prompt/chat operations
|
|
12
|
+
* - $.skill - Skill commands (list, execute)
|
|
13
|
+
* - $.logger - Logging operations (debug, info, warn, error, trace, fatal)
|
|
14
|
+
* - $.storage - Key-value storage operations (set, get, delete, list, clear)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { runScriptSafely } = require('../../../src/utils/sandbox');
|
|
18
|
+
|
|
19
|
+
class FlowContext {
|
|
20
|
+
constructor(input = {}, sessionId = null) {
|
|
21
|
+
// Input from workflow caller
|
|
22
|
+
this.input = input;
|
|
23
|
+
|
|
24
|
+
// Flow-level persistent data (like $.data)
|
|
25
|
+
this.data = {};
|
|
26
|
+
|
|
27
|
+
// Stage results storage (stageName -> result)
|
|
28
|
+
this._results = new Map();
|
|
29
|
+
|
|
30
|
+
// Current stage info
|
|
31
|
+
this.current = null;
|
|
32
|
+
this.prev = null;
|
|
33
|
+
|
|
34
|
+
// Session ID for tool execution
|
|
35
|
+
this._sessionId = sessionId;
|
|
36
|
+
|
|
37
|
+
// Framework reference for tool execution
|
|
38
|
+
this._framework = null;
|
|
39
|
+
|
|
40
|
+
// Destruction flag
|
|
41
|
+
this._destroyed = false;
|
|
42
|
+
|
|
43
|
+
// Pending next stage (for async transitions)
|
|
44
|
+
this._pendingNext = null;
|
|
45
|
+
|
|
46
|
+
// Log prefix
|
|
47
|
+
this._log = require('../../../src/common/logger').logger.child('FlowContext');
|
|
48
|
+
|
|
49
|
+
// Lazy-loaded chain-style API objects
|
|
50
|
+
this._toolAccessor = null;
|
|
51
|
+
this._extensionAccessor = null;
|
|
52
|
+
this._workflowAccessor = null;
|
|
53
|
+
this._agentAccessor = null;
|
|
54
|
+
this._promptAccessor = null;
|
|
55
|
+
this._skillAccessor = null;
|
|
56
|
+
this._loggerAccessor = null;
|
|
57
|
+
this._storageAccessor = null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Set framework reference (called by engine)
|
|
62
|
+
*/
|
|
63
|
+
setFramework(framework) {
|
|
64
|
+
this._framework = framework;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================================
|
|
68
|
+
// $.tool - Direct tool registry and execution
|
|
69
|
+
// ============================================================
|
|
70
|
+
get tool() {
|
|
71
|
+
if (!this._toolAccessor) {
|
|
72
|
+
this._toolAccessor = this._createToolAccessor();
|
|
73
|
+
}
|
|
74
|
+
return this._toolAccessor;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_createToolAccessor() {
|
|
78
|
+
const context = this;
|
|
79
|
+
const log = this._log;
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
/**
|
|
83
|
+
* List all registered tools
|
|
84
|
+
*/
|
|
85
|
+
list: () => {
|
|
86
|
+
if (!context._framework) return [];
|
|
87
|
+
return context._framework.toolRegistry ? context._framework.toolRegistry.listTools() : [];
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Execute a tool directly
|
|
92
|
+
*/
|
|
93
|
+
execute: async (name, args = {}) => {
|
|
94
|
+
return await context._executeTool(name, args);
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Register a new tool (temporary, workflow-scoped)
|
|
99
|
+
*/
|
|
100
|
+
register: (toolDef) => {
|
|
101
|
+
log.warn('tool.register() is not implemented in workflow context - tools must be registered via plugin');
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get tool definition
|
|
106
|
+
*/
|
|
107
|
+
get: (name) => {
|
|
108
|
+
if (!context._framework?.toolRegistry) return null;
|
|
109
|
+
return context._framework.toolRegistry.getTool(name);
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ============================================================
|
|
115
|
+
// $.extension - Extension tools (like Plugin.extension accessor)
|
|
116
|
+
// ============================================================
|
|
117
|
+
get extension() {
|
|
118
|
+
if (!this._extensionAccessor) {
|
|
119
|
+
this._extensionAccessor = this._createExtensionAccessor();
|
|
120
|
+
}
|
|
121
|
+
return this._extensionAccessor;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
_createExtensionAccessor() {
|
|
125
|
+
const context = this;
|
|
126
|
+
const log = this._log;
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
/**
|
|
130
|
+
* List all registered extensions
|
|
131
|
+
*/
|
|
132
|
+
list: () => {
|
|
133
|
+
if (!context._framework) return [];
|
|
134
|
+
const extExec = context._framework.pluginManager?.get('extension-executor');
|
|
135
|
+
if (!extExec?._registry) return [];
|
|
136
|
+
return extExec._registry.listExtensions();
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Register an extension tool
|
|
141
|
+
*/
|
|
142
|
+
register: (toolDef) => {
|
|
143
|
+
if (!context._framework) {
|
|
144
|
+
log.warn('extension.register() called before framework is set');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (!toolDef || !toolDef.name) {
|
|
148
|
+
log.warn('extension.register() missing tool name');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const extExec = context._framework.pluginManager?.get('extension-executor');
|
|
153
|
+
if (!extExec) {
|
|
154
|
+
log.warn('extension-executor plugin not found');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
extExec.registerTool('workflow', {
|
|
159
|
+
name: 'workflow',
|
|
160
|
+
description: 'Workflow scoped extensions',
|
|
161
|
+
version: '1.0.0',
|
|
162
|
+
}, toolDef);
|
|
163
|
+
|
|
164
|
+
extExec._invalidatePromptCaches(context._framework);
|
|
165
|
+
log.debug(`[Workflow] Registered extension tool: workflow/${toolDef.name}`);
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Remove an extension tool
|
|
170
|
+
*/
|
|
171
|
+
remove: (toolName) => {
|
|
172
|
+
if (!context._framework) {
|
|
173
|
+
log.warn('extension.remove() called before framework is set');
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const extExec = context._framework.pluginManager?.get('extension-executor');
|
|
178
|
+
if (extExec) {
|
|
179
|
+
extExec._unregisterTool('workflow', toolName);
|
|
180
|
+
}
|
|
181
|
+
log.debug(`[Workflow] Removed extension tool: workflow/${toolName}`);
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Execute an extension tool
|
|
186
|
+
* Supports: extension.execute('tool-name', args) or extension.execute('plugin', 'tool-name', args)
|
|
187
|
+
*/
|
|
188
|
+
execute: async (pluginNameOrToolName, toolNameOrArgs, args = {}) => {
|
|
189
|
+
if (!context._framework) {
|
|
190
|
+
throw new Error('extension.execute() called before framework is set');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const extExec = context._framework.pluginManager?.get('extension-executor');
|
|
194
|
+
if (!extExec) {
|
|
195
|
+
return { success: false, error: 'extension-executor not found' };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let pluginName, toolName;
|
|
199
|
+
if (typeof toolNameOrArgs === 'object') {
|
|
200
|
+
// extension.execute('tool-name', args)
|
|
201
|
+
pluginName = 'workflow';
|
|
202
|
+
toolName = pluginNameOrToolName;
|
|
203
|
+
args = toolNameOrArgs;
|
|
204
|
+
} else {
|
|
205
|
+
// extension.execute('plugin', 'tool-name', args)
|
|
206
|
+
pluginName = pluginNameOrToolName;
|
|
207
|
+
toolName = toolNameOrArgs;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const result = await extExec._executeTool(pluginName, toolName, args, context._framework);
|
|
211
|
+
return result;
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get extension info
|
|
216
|
+
*/
|
|
217
|
+
get: (pluginName) => {
|
|
218
|
+
if (!context._framework) return null;
|
|
219
|
+
const extExec = context._framework.pluginManager?.get('extension-executor');
|
|
220
|
+
if (!extExec?._registry) return null;
|
|
221
|
+
return extExec._registry.getExtension(pluginName);
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ============================================================
|
|
227
|
+
// $.workflow - Nested workflow operations
|
|
228
|
+
// ============================================================
|
|
229
|
+
get workflow() {
|
|
230
|
+
if (!this._workflowAccessor) {
|
|
231
|
+
this._workflowAccessor = this._createWorkflowAccessor();
|
|
232
|
+
}
|
|
233
|
+
return this._workflowAccessor;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
_createWorkflowAccessor() {
|
|
237
|
+
const context = this;
|
|
238
|
+
const log = this._log;
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
/**
|
|
242
|
+
* List all available workflows
|
|
243
|
+
*/
|
|
244
|
+
list: () => {
|
|
245
|
+
if (!context._framework) return [];
|
|
246
|
+
const workflowPlugin = context._framework.pluginManager?.get('workflow');
|
|
247
|
+
if (!workflowPlugin?._workflows) return [];
|
|
248
|
+
return Array.from(workflowPlugin._workflows.keys());
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Execute a workflow by name
|
|
253
|
+
*/
|
|
254
|
+
execute: async (workflowName, input = {}, sessionId = null) => {
|
|
255
|
+
if (!context._framework) {
|
|
256
|
+
throw new Error('workflow.execute() called before framework is set');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const workflowPlugin = context._framework.pluginManager?.get('workflow');
|
|
260
|
+
if (!workflowPlugin) {
|
|
261
|
+
return { success: false, error: 'workflow plugin not found' };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const sid = sessionId || context._sessionId;
|
|
265
|
+
return await workflowPlugin.executeWorkflow(workflowName, input, sid);
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Get workflow definition
|
|
270
|
+
*/
|
|
271
|
+
get: (workflowName) => {
|
|
272
|
+
if (!context._framework) return null;
|
|
273
|
+
const workflowPlugin = context._framework.pluginManager?.get('workflow');
|
|
274
|
+
if (!workflowPlugin?._workflows) return null;
|
|
275
|
+
return workflowPlugin._workflows.get(workflowName);
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ============================================================
|
|
281
|
+
// $.agent - Agent operations
|
|
282
|
+
// ============================================================
|
|
283
|
+
get agent() {
|
|
284
|
+
if (!this._agentAccessor) {
|
|
285
|
+
this._agentAccessor = this._createAgentAccessor();
|
|
286
|
+
}
|
|
287
|
+
return this._agentAccessor;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
_createAgentAccessor() {
|
|
291
|
+
const context = this;
|
|
292
|
+
const log = this._log;
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
/**
|
|
296
|
+
* Create a new agent
|
|
297
|
+
*/
|
|
298
|
+
create: (config = {}) => {
|
|
299
|
+
if (!context._framework) {
|
|
300
|
+
throw new Error('agent.create() called before framework is set');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const resolvedConfig = context.resolveArgs(config);
|
|
304
|
+
let sessionId = context._sessionId;
|
|
305
|
+
|
|
306
|
+
if (sessionId && context._framework.runWithContext) {
|
|
307
|
+
return context._framework.runWithContext(
|
|
308
|
+
{ sessionId },
|
|
309
|
+
() => context._framework.createAgent(resolvedConfig)
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
return context._framework.createAgent(resolvedConfig);
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Chat with an agent
|
|
317
|
+
*/
|
|
318
|
+
chat: async (config = {}) => {
|
|
319
|
+
if (!context._framework) {
|
|
320
|
+
throw new Error('agent.chat() called before framework is set');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const resolvedConfig = context.resolveArgs(config);
|
|
324
|
+
const { agentId, message, ...options } = resolvedConfig;
|
|
325
|
+
|
|
326
|
+
if (!message) {
|
|
327
|
+
throw new Error('agent.chat() requires message');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
let sessionId = context._sessionId;
|
|
331
|
+
|
|
332
|
+
if (sessionId && context._framework.runWithContext) {
|
|
333
|
+
return await context._framework.runWithContext(
|
|
334
|
+
{ sessionId },
|
|
335
|
+
async () => {
|
|
336
|
+
const agent = agentId
|
|
337
|
+
? context._framework.createAgent({ id: agentId, hidden: true, ...options })
|
|
338
|
+
: context._framework.createSubAgent({ hidden: true, ...options });
|
|
339
|
+
return await agent.chat(message, options);
|
|
340
|
+
}
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const agent = agentId
|
|
345
|
+
? context._framework.createAgent({ id: agentId, hidden: true, ...options })
|
|
346
|
+
: context._framework.createSubAgent({ hidden: true, ...options });
|
|
347
|
+
return await agent.chat(message, options);
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* List active agents
|
|
352
|
+
*/
|
|
353
|
+
list: () => {
|
|
354
|
+
if (!context._framework) return [];
|
|
355
|
+
return context._framework._agents || [];
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ============================================================
|
|
361
|
+
// $.prompt - Prompt operations
|
|
362
|
+
// ============================================================
|
|
363
|
+
get prompt() {
|
|
364
|
+
if (!this._promptAccessor) {
|
|
365
|
+
this._promptAccessor = this._createPromptAccessor();
|
|
366
|
+
}
|
|
367
|
+
return this._promptAccessor;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
_createPromptAccessor() {
|
|
371
|
+
const context = this;
|
|
372
|
+
const log = this._log;
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
/**
|
|
376
|
+
* Send a prompt and get response (creates temporary agent)
|
|
377
|
+
*/
|
|
378
|
+
send: async (config = {}) => {
|
|
379
|
+
if (!context._framework) {
|
|
380
|
+
throw new Error('prompt.send() called before framework is set');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const resolvedConfig = context.resolveArgs(config);
|
|
384
|
+
const { message, role, systemPrompt, ...options } = resolvedConfig;
|
|
385
|
+
|
|
386
|
+
if (!message) {
|
|
387
|
+
throw new Error('prompt.send() requires message');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
let sessionId = context._sessionId;
|
|
391
|
+
|
|
392
|
+
if (sessionId && context._framework.runWithContext) {
|
|
393
|
+
return await context._framework.runWithContext(
|
|
394
|
+
{ sessionId },
|
|
395
|
+
async () => {
|
|
396
|
+
const agent = context._framework.createSubAgent({
|
|
397
|
+
role: role || 'Assistant',
|
|
398
|
+
systemPrompt: systemPrompt || '',
|
|
399
|
+
hidden: true,
|
|
400
|
+
});
|
|
401
|
+
return await agent.chat(message, options);
|
|
402
|
+
}
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const agent = context._framework.createSubAgent({
|
|
407
|
+
role: role || 'Assistant',
|
|
408
|
+
systemPrompt: systemPrompt || '',
|
|
409
|
+
hidden: true,
|
|
410
|
+
});
|
|
411
|
+
return await agent.chat(message, options);
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Chat with a prompt (alias for send)
|
|
416
|
+
*/
|
|
417
|
+
chat: async (config = {}) => {
|
|
418
|
+
return this.send(config);
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ============================================================
|
|
424
|
+
// $.skill - Skill commands
|
|
425
|
+
// ============================================================
|
|
426
|
+
get skill() {
|
|
427
|
+
if (!this._skillAccessor) {
|
|
428
|
+
this._skillAccessor = this._createSkillAccessor();
|
|
429
|
+
}
|
|
430
|
+
return this._skillAccessor;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
_createSkillAccessor() {
|
|
434
|
+
const context = this;
|
|
435
|
+
const log = this._log;
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
/**
|
|
439
|
+
* List available skills
|
|
440
|
+
*/
|
|
441
|
+
list: () => {
|
|
442
|
+
if (!context._framework) return [];
|
|
443
|
+
const extExec = context._framework.pluginManager?.get('extension-executor');
|
|
444
|
+
if (!extExec?._registry) return [];
|
|
445
|
+
const extensions = extExec._registry.listExtensions();
|
|
446
|
+
return extensions
|
|
447
|
+
.filter(e => e.name.startsWith('skill:'))
|
|
448
|
+
.map(e => e.name.replace('skill:', ''));
|
|
449
|
+
},
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Execute a skill command
|
|
453
|
+
* Supports: skill.execute('skill-name', 'command', args)
|
|
454
|
+
*/
|
|
455
|
+
execute: async (skillName, commandOrArgs, args = {}) => {
|
|
456
|
+
if (!context._framework) {
|
|
457
|
+
throw new Error('skill.execute() called before framework is set');
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
let command, toolArgs;
|
|
461
|
+
if (typeof commandOrArgs === 'object') {
|
|
462
|
+
command = commandOrArgs.command || commandOrArgs.tool;
|
|
463
|
+
toolArgs = commandOrArgs;
|
|
464
|
+
} else {
|
|
465
|
+
command = commandOrArgs;
|
|
466
|
+
toolArgs = args;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (!command) {
|
|
470
|
+
throw new Error('skill.execute() requires command');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const plugin = `skill:${skillName}`;
|
|
474
|
+
const resolvedArgs = context.resolveArgs(toolArgs);
|
|
475
|
+
|
|
476
|
+
if (context._sessionId && context._framework.runWithContext) {
|
|
477
|
+
return await context._framework.runWithContext(
|
|
478
|
+
{ sessionId: context._sessionId },
|
|
479
|
+
async () => await context._framework.executeTool('ext_call', {
|
|
480
|
+
plugin,
|
|
481
|
+
tool: command,
|
|
482
|
+
args: resolvedArgs,
|
|
483
|
+
})
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
return await context._framework.executeTool('ext_call', {
|
|
487
|
+
plugin,
|
|
488
|
+
tool: command,
|
|
489
|
+
args: resolvedArgs,
|
|
490
|
+
});
|
|
491
|
+
},
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Get skill info
|
|
495
|
+
*/
|
|
496
|
+
get: (skillName) => {
|
|
497
|
+
if (!context._framework) return null;
|
|
498
|
+
const extExec = context._framework.pluginManager?.get('extension-executor');
|
|
499
|
+
if (!extExec?._registry) return null;
|
|
500
|
+
return extExec._registry.getExtension(`skill:${skillName}`);
|
|
501
|
+
},
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ============================================================
|
|
506
|
+
// $.logger - Logging operations
|
|
507
|
+
// ============================================================
|
|
508
|
+
get logger() {
|
|
509
|
+
if (!this._loggerAccessor) {
|
|
510
|
+
this._loggerAccessor = this._createLoggerAccessor();
|
|
511
|
+
}
|
|
512
|
+
return this._loggerAccessor;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
_createLoggerAccessor() {
|
|
516
|
+
const context = this;
|
|
517
|
+
const flowLog = this._log;
|
|
518
|
+
|
|
519
|
+
// Get the underlying logger methods
|
|
520
|
+
const LEVELS = ['debug', 'info', 'warn', 'error', 'trace', 'fatal'];
|
|
521
|
+
|
|
522
|
+
const loggerObj = {
|
|
523
|
+
/**
|
|
524
|
+
* Log debug message
|
|
525
|
+
*/
|
|
526
|
+
debug: (...args) => {
|
|
527
|
+
flowLog.debug(...args);
|
|
528
|
+
},
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Log info message
|
|
532
|
+
*/
|
|
533
|
+
info: (...args) => {
|
|
534
|
+
flowLog.info(...args);
|
|
535
|
+
},
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Log warning message
|
|
539
|
+
*/
|
|
540
|
+
warn: (...args) => {
|
|
541
|
+
flowLog.warn(...args);
|
|
542
|
+
},
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Log error message
|
|
546
|
+
*/
|
|
547
|
+
error: (...args) => {
|
|
548
|
+
flowLog.error(...args);
|
|
549
|
+
},
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Log trace message
|
|
553
|
+
*/
|
|
554
|
+
trace: (...args) => {
|
|
555
|
+
flowLog.trace(...args);
|
|
556
|
+
},
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Log fatal message
|
|
560
|
+
*/
|
|
561
|
+
fatal: (...args) => {
|
|
562
|
+
flowLog.fatal(...args);
|
|
563
|
+
},
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Create a child logger with namespace
|
|
567
|
+
*/
|
|
568
|
+
child: (namespace) => {
|
|
569
|
+
return flowLog.child(namespace);
|
|
570
|
+
},
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Log with custom level
|
|
574
|
+
*/
|
|
575
|
+
log: (level, ...args) => {
|
|
576
|
+
if (typeof flowLog[level] === 'function') {
|
|
577
|
+
flowLog[level](...args);
|
|
578
|
+
} else {
|
|
579
|
+
flowLog.info(...args);
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
return loggerObj;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ============================================================
|
|
588
|
+
// $.storage - Storage operations
|
|
589
|
+
// ============================================================
|
|
590
|
+
get storage() {
|
|
591
|
+
if (!this._storageAccessor) {
|
|
592
|
+
this._storageAccessor = this._createStorageAccessor();
|
|
593
|
+
}
|
|
594
|
+
return this._storageAccessor;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
_createStorageAccessor() {
|
|
598
|
+
const context = this;
|
|
599
|
+
|
|
600
|
+
return {
|
|
601
|
+
/**
|
|
602
|
+
* Set a value in storage
|
|
603
|
+
*/
|
|
604
|
+
set: async (key, value, namespace) => {
|
|
605
|
+
return await context._executeTool('storage_set', { key, value, namespace });
|
|
606
|
+
},
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Get a value from storage
|
|
610
|
+
*/
|
|
611
|
+
get: async (key, namespace) => {
|
|
612
|
+
return await context._executeTool('storage_get', { key, namespace });
|
|
613
|
+
},
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Delete a value from storage
|
|
617
|
+
*/
|
|
618
|
+
delete: async (key, namespace) => {
|
|
619
|
+
return await context._executeTool('storage_delete', { key, namespace });
|
|
620
|
+
},
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* List all keys in a namespace
|
|
624
|
+
*/
|
|
625
|
+
list: async (namespace) => {
|
|
626
|
+
return await context._executeTool('storage_list', { namespace });
|
|
627
|
+
},
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Clear all values in a namespace
|
|
631
|
+
*/
|
|
632
|
+
clear: async (namespace) => {
|
|
633
|
+
return await context._executeTool('storage_clear', { namespace });
|
|
634
|
+
},
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Watch a key for changes
|
|
638
|
+
*/
|
|
639
|
+
watch: async (key, namespace) => {
|
|
640
|
+
return await context._executeTool('storage_watch', { key, namespace });
|
|
641
|
+
},
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Get storage statistics
|
|
645
|
+
*/
|
|
646
|
+
stats: async (namespace) => {
|
|
647
|
+
return await context._executeTool('storage_stats', { namespace });
|
|
648
|
+
},
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Trigger storage compaction
|
|
652
|
+
*/
|
|
653
|
+
compact: async (namespace) => {
|
|
654
|
+
return await context._executeTool('storage_compact', { namespace });
|
|
655
|
+
},
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// ============================================================
|
|
660
|
+
// NOTE: 之前这里有 `async tool(name, args)` legacy method,
|
|
661
|
+
// 但与上方的 `get tool()` 冲突 (同名),JavaScript class
|
|
662
|
+
// 让后定义的方法覆盖 getter,导致 $.tool 返回的永远是
|
|
663
|
+
// async function 而非 accessor object。
|
|
664
|
+
// 已删除此 legacy method;新 API 用 `$.tool.execute(name, args)`
|
|
665
|
+
// 走 get tool() 返回的 accessor 对象。
|
|
666
|
+
// ============================================================
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Execute a tool directly
|
|
670
|
+
*/
|
|
671
|
+
async _executeTool(toolName, args) {
|
|
672
|
+
if (!this._framework) {
|
|
673
|
+
throw new Error('Framework not set');
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Resolve args with variable interpolation
|
|
677
|
+
const resolvedArgs = this.resolveArgs(args || {});
|
|
678
|
+
|
|
679
|
+
let sessionId = this._sessionId;
|
|
680
|
+
if (!sessionId && this._framework.getExecutionContext) {
|
|
681
|
+
const ctx = this._framework.getExecutionContext();
|
|
682
|
+
sessionId = ctx?.sessionId;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (sessionId && this._framework.runWithContext) {
|
|
686
|
+
return await this._framework.runWithContext(
|
|
687
|
+
{ sessionId },
|
|
688
|
+
async () => await this._framework.executeTool(toolName, resolvedArgs)
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
return await this._framework.executeTool(toolName, resolvedArgs);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// ============================================================
|
|
695
|
+
// Basic Flow API
|
|
696
|
+
// ============================================================
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Register a stage (JS format - $.push)
|
|
700
|
+
*/
|
|
701
|
+
push(name, fn) {
|
|
702
|
+
if (!this._stages) {
|
|
703
|
+
this._stages = new Map();
|
|
704
|
+
}
|
|
705
|
+
this._stages.set(name, fn);
|
|
706
|
+
return this;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Go to next stage (sync) - $.next
|
|
711
|
+
* 支持直接传参: $.next('stage-name', arg1, arg2, ...)
|
|
712
|
+
* 等同�?$.send({ arg1, arg2, ... }) 后再 $.next('stage-name')
|
|
713
|
+
*/
|
|
714
|
+
next(name, ...args) {
|
|
715
|
+
this._pendingNext = name;
|
|
716
|
+
if (args.length > 0) {
|
|
717
|
+
this._sentData = args.length === 1 ? args[0] : args;
|
|
718
|
+
}
|
|
719
|
+
return this;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Go to next stage (async) - $.next2
|
|
724
|
+
* Returns a function for deferred transition
|
|
725
|
+
*/
|
|
726
|
+
next2(name) {
|
|
727
|
+
return () => {
|
|
728
|
+
this.next(name);
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Pass data to next stage - $.send
|
|
734
|
+
*/
|
|
735
|
+
send(data) {
|
|
736
|
+
if (data !== undefined) {
|
|
737
|
+
this._sentData = data;
|
|
738
|
+
}
|
|
739
|
+
return this;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Built-in delay helper - $.wait
|
|
744
|
+
*/
|
|
745
|
+
wait(ms) {
|
|
746
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Execute a script - $.script
|
|
751
|
+
*/
|
|
752
|
+
async script(code) {
|
|
753
|
+
const scriptContext = {
|
|
754
|
+
input: this.input,
|
|
755
|
+
data: this.data,
|
|
756
|
+
variables: Object.fromEntries(this._results),
|
|
757
|
+
previousResult: this._sentData,
|
|
758
|
+
console: {
|
|
759
|
+
log: (...args) => {
|
|
760
|
+
this._log.debug('[Script]', ...args);
|
|
761
|
+
},
|
|
762
|
+
},
|
|
763
|
+
};
|
|
764
|
+
return await runScriptSafely(code, scriptContext, { timeout: 10000 });
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* End workflow - $.destroy
|
|
769
|
+
*/
|
|
770
|
+
destroy() {
|
|
771
|
+
this._destroyed = true;
|
|
772
|
+
this._pendingNext = null;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Get result from a named stage
|
|
777
|
+
*/
|
|
778
|
+
getResult(stageName) {
|
|
779
|
+
return this._results.get(stageName);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Set result for a stage
|
|
784
|
+
*/
|
|
785
|
+
setResult(stageName, result) {
|
|
786
|
+
this._results.set(stageName, result);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// ============================================================
|
|
790
|
+
// Variable Resolution
|
|
791
|
+
// ============================================================
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Resolve a value with variable interpolation
|
|
795
|
+
* Supports {{stageName}}, {{stage.field}}, {{input.field}}, {{data.field}}
|
|
796
|
+
*
|
|
797
|
+
* @param {*} value - Value to resolve (string templates only)
|
|
798
|
+
* @param {Object} [options]
|
|
799
|
+
* @param {boolean} [options.raw=false] - If true, return raw resolved value (preserve type).
|
|
800
|
+
* Used by stages like `each` that need actual arrays/objects
|
|
801
|
+
* instead of stringified versions.
|
|
802
|
+
* @returns {*} Resolved value
|
|
803
|
+
*/
|
|
804
|
+
resolveValue(value, options = {}) {
|
|
805
|
+
if (typeof value !== 'string') {
|
|
806
|
+
return value;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const { raw = false } = options;
|
|
810
|
+
|
|
811
|
+
// Replace {{stageName}} or {{stage.field}} with stage result
|
|
812
|
+
return value.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
|
|
813
|
+
const resolved = this._resolvePath(path);
|
|
814
|
+
|
|
815
|
+
// Path not found - keep the original placeholder
|
|
816
|
+
if (resolved === undefined) {
|
|
817
|
+
return match;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Raw mode: return the value as-is (caller handles serialization)
|
|
821
|
+
if (raw) {
|
|
822
|
+
return resolved;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// String mode: serialize for interpolation
|
|
826
|
+
if (resolved === null) {
|
|
827
|
+
return 'null';
|
|
828
|
+
}
|
|
829
|
+
// For arrays and objects, JSON-encode so the template works as a string
|
|
830
|
+
// (Raw arrays/objects can't be inlined into strings via String.replace without coercion)
|
|
831
|
+
if (typeof resolved === 'object') {
|
|
832
|
+
try {
|
|
833
|
+
return JSON.stringify(resolved);
|
|
834
|
+
} catch {
|
|
835
|
+
return String(resolved);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return resolved;
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Resolve a template path to its value.
|
|
845
|
+
* Internal helper used by resolveValue and other template consumers.
|
|
846
|
+
* Supports: input.field, data.field, stageName, stage.field
|
|
847
|
+
* Also searches context.data when a simple path is not found in _results.
|
|
848
|
+
*
|
|
849
|
+
* @param {string} path - Template path (without {{ }} markers)
|
|
850
|
+
* @returns {*} Resolved value or undefined if not found
|
|
851
|
+
*/
|
|
852
|
+
_resolvePath(path) {
|
|
853
|
+
// input.field access
|
|
854
|
+
if (path.startsWith('input.')) {
|
|
855
|
+
const val = this._getNestedValue(this.input, path.substring(6));
|
|
856
|
+
return val !== undefined ? val : undefined;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// data.field access
|
|
860
|
+
if (path.startsWith('data.')) {
|
|
861
|
+
const val = this._getNestedValue(this.data, path.substring(5));
|
|
862
|
+
return val !== undefined ? val : undefined;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// stage.field or stageName access
|
|
866
|
+
const dotIndex = path.indexOf('.');
|
|
867
|
+
if (dotIndex > 0) {
|
|
868
|
+
const stageName = path.substring(0, dotIndex);
|
|
869
|
+
const fieldPath = path.substring(dotIndex + 1);
|
|
870
|
+
// Look up the root value (try stage results first, then loop/data context)
|
|
871
|
+
// so that nested property access works for each-loop variables too,
|
|
872
|
+
// e.g. inside an each body: {{u.name}} where u is the current item.
|
|
873
|
+
let stageResult = this._results.get(stageName);
|
|
874
|
+
if (stageResult === undefined) {
|
|
875
|
+
stageResult = this.data[stageName];
|
|
876
|
+
}
|
|
877
|
+
if (stageResult !== undefined) {
|
|
878
|
+
const val = this._getNestedValue(stageResult, fieldPath);
|
|
879
|
+
if (val !== undefined) return val;
|
|
880
|
+
}
|
|
881
|
+
return undefined;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Simple stageName reference: try _results first, then data
|
|
885
|
+
// (data lookup supports each-loop variables, e.g., {{fruit}} inside an each body)
|
|
886
|
+
let val = this._results.get(path);
|
|
887
|
+
if (val === undefined) {
|
|
888
|
+
val = this.data[path];
|
|
889
|
+
}
|
|
890
|
+
return val !== undefined ? val : undefined;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Resolve args object with variable interpolation
|
|
895
|
+
*/
|
|
896
|
+
resolveArgs(args) {
|
|
897
|
+
if (!args || typeof args !== 'object') {
|
|
898
|
+
return args;
|
|
899
|
+
}
|
|
900
|
+
const resolved = {};
|
|
901
|
+
for (const [key, value] of Object.entries(args)) {
|
|
902
|
+
if (typeof value === 'string') {
|
|
903
|
+
resolved[key] = this.resolveValue(value);
|
|
904
|
+
} else if (Array.isArray(value)) {
|
|
905
|
+
// Resolve each element as a template (raw mode preserves types)
|
|
906
|
+
resolved[key] = value.map((v) =>
|
|
907
|
+
typeof v === 'string' ? this.resolveValue(v, { raw: true }) : v
|
|
908
|
+
);
|
|
909
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
910
|
+
resolved[key] = this.resolveArgs(value);
|
|
911
|
+
} else {
|
|
912
|
+
resolved[key] = value;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return resolved;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Get nested value from object by dot notation path
|
|
920
|
+
*/
|
|
921
|
+
_getNestedValue(obj, path) {
|
|
922
|
+
if (!obj) return undefined;
|
|
923
|
+
const parts = path.split('.');
|
|
924
|
+
let current = obj;
|
|
925
|
+
for (const part of parts) {
|
|
926
|
+
if (current === null || current === undefined) return undefined;
|
|
927
|
+
// Handle array index in path like "items.0.name"
|
|
928
|
+
if (typeof current === 'string') {
|
|
929
|
+
try {
|
|
930
|
+
current = JSON.parse(current);
|
|
931
|
+
} catch {
|
|
932
|
+
// Not JSON, continue
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
current = current[part];
|
|
936
|
+
}
|
|
937
|
+
return current;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
module.exports = { FlowContext };
|