keystone-cli 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +43 -4
  2. package/package.json +4 -1
  3. package/src/cli.ts +1 -0
  4. package/src/commands/event.ts +9 -0
  5. package/src/commands/run.ts +17 -0
  6. package/src/db/dynamic-state-manager.ts +12 -9
  7. package/src/db/memory-db.test.ts +19 -1
  8. package/src/db/memory-db.ts +101 -22
  9. package/src/db/workflow-db.ts +181 -9
  10. package/src/expression/evaluator.ts +4 -1
  11. package/src/parser/config-schema.ts +6 -0
  12. package/src/parser/schema.ts +1 -0
  13. package/src/runner/__test__/llm-test-setup.ts +43 -11
  14. package/src/runner/durable-timers.test.ts +1 -1
  15. package/src/runner/executors/dynamic-executor.ts +125 -88
  16. package/src/runner/executors/engine-executor.ts +10 -39
  17. package/src/runner/executors/file-executor.ts +67 -0
  18. package/src/runner/executors/foreach-executor.ts +170 -17
  19. package/src/runner/executors/human-executor.ts +18 -0
  20. package/src/runner/executors/llm/stream-handler.ts +103 -0
  21. package/src/runner/executors/llm/tool-manager.ts +360 -0
  22. package/src/runner/executors/llm-executor.ts +288 -555
  23. package/src/runner/executors/memory-executor.ts +41 -34
  24. package/src/runner/executors/shell-executor.ts +96 -52
  25. package/src/runner/executors/subworkflow-executor.ts +16 -0
  26. package/src/runner/executors/types.ts +3 -1
  27. package/src/runner/executors/verification_fixes.test.ts +46 -0
  28. package/src/runner/join-scheduling.test.ts +2 -1
  29. package/src/runner/llm-adapter.integration.test.ts +10 -5
  30. package/src/runner/llm-adapter.ts +57 -18
  31. package/src/runner/llm-clarification.test.ts +4 -1
  32. package/src/runner/llm-executor.test.ts +21 -7
  33. package/src/runner/mcp-client.ts +36 -2
  34. package/src/runner/mcp-server.ts +65 -36
  35. package/src/runner/recovery-security.test.ts +5 -2
  36. package/src/runner/reflexion.test.ts +6 -3
  37. package/src/runner/services/context-builder.ts +13 -4
  38. package/src/runner/services/workflow-validator.ts +2 -1
  39. package/src/runner/standard-tools-ast.test.ts +4 -2
  40. package/src/runner/standard-tools-execution.test.ts +14 -1
  41. package/src/runner/standard-tools-integration.test.ts +6 -0
  42. package/src/runner/standard-tools.ts +13 -10
  43. package/src/runner/step-executor.ts +2 -2
  44. package/src/runner/tool-integration.test.ts +4 -1
  45. package/src/runner/workflow-runner.test.ts +23 -12
  46. package/src/runner/workflow-runner.ts +172 -79
  47. package/src/runner/workflow-state.ts +181 -111
  48. package/src/ui/dashboard.tsx +17 -3
  49. package/src/utils/config-loader.ts +4 -0
  50. package/src/utils/constants.ts +4 -0
  51. package/src/utils/context-injector.test.ts +27 -27
  52. package/src/utils/context-injector.ts +68 -26
  53. package/src/utils/process-sandbox.ts +138 -148
  54. package/src/utils/redactor.ts +39 -9
  55. package/src/utils/resource-loader.ts +24 -19
  56. package/src/utils/sandbox.ts +6 -0
  57. package/src/utils/stream-utils.ts +58 -0
@@ -0,0 +1,360 @@
1
+ import { tool as createTool, jsonSchema } from 'ai';
2
+ import type { ExpressionContext } from '../../../expression/evaluator';
3
+ import { parseAgent, resolveAgentPath } from '../../../parser/agent-parser';
4
+ import type { Agent, LlmStep, Step } from '../../../parser/schema';
5
+ import { LLM } from '../../../utils/constants';
6
+ import type { Logger } from '../../../utils/logger';
7
+ import { MCPClient } from '../../mcp-client';
8
+ import type { MCPManager, MCPServerConfig } from '../../mcp-manager';
9
+ import { STANDARD_TOOLS, validateStandardToolSecurity } from '../../standard-tools';
10
+
11
+ const { TRANSFER_TOOL_NAME } = LLM;
12
+
13
+ export type ToolContext = {
14
+ step: LlmStep;
15
+ context: ExpressionContext;
16
+ logger: Logger;
17
+ mcpManager?: MCPManager;
18
+ workflowDir?: string;
19
+ abortSignal?: AbortSignal;
20
+ };
21
+
22
+ export type ToolImplementation = {
23
+ name: string;
24
+ description: string;
25
+ parameters: any;
26
+ execute: (args: any) => Promise<any>;
27
+ };
28
+
29
+ function safeJsonStringify(value: unknown): string {
30
+ try {
31
+ return JSON.stringify(value);
32
+ } catch {
33
+ return '{"error": "Circular or non-serializable content"}';
34
+ }
35
+ }
36
+
37
+ export class ToolManager {
38
+ private tools: ToolImplementation[] = [];
39
+ private aiTools: Record<string, any> = {};
40
+ private localMcpClients: MCPClient[] = [];
41
+
42
+ // Special state for control flow tools
43
+ public requiresSuspend = false;
44
+ public suspendData: any = null;
45
+ public pendingTransfer: Agent | undefined;
46
+
47
+ constructor(private ctx: ToolContext) {}
48
+
49
+ public async registerTools(
50
+ activeAgent: Agent,
51
+ executeStepFn: (step: Step, context: ExpressionContext) => Promise<any>
52
+ ): Promise<Record<string, any>> {
53
+ const { step, mcpManager, logger } = this.ctx;
54
+ this.tools = [];
55
+ this.aiTools = {};
56
+ this.requiresSuspend = false;
57
+ this.suspendData = null;
58
+ this.pendingTransfer = undefined;
59
+
60
+ // 1. Agent Tools
61
+ for (const tool of activeAgent.tools) {
62
+ this.registerTool(tool.name, tool.description || '', tool.parameters, async (args) => {
63
+ if (tool.execution) {
64
+ const toolContext = { ...this.ctx.context, args };
65
+ const result = await executeStepFn(tool.execution, toolContext);
66
+ return result.status === 'success'
67
+ ? this.applyContextUpdate(result.output)
68
+ : `Error: ${result.error}`;
69
+ }
70
+ return `Error: Tool ${tool.name} has no implementation.`;
71
+ });
72
+ }
73
+
74
+ // 2. Step Tools & Standard Tools
75
+ const standardToolsRecord = STANDARD_TOOLS as any; // Handle index signature issue
76
+ const extraTools = [
77
+ ...(step.tools || []),
78
+ ...(step.useStandardTools ? Object.values(standardToolsRecord) : []),
79
+ ];
80
+
81
+ // Logic to merge standard tools correctly:
82
+ // If useStandardTools is true, we want all standard tools.
83
+ // But the loop above iterates over step.tools (definitions) + values?
84
+ // In original code: const extraTools = [...(step.tools || []), ...(step.useStandardTools ? STANDARD_TOOLS : [])];
85
+ // Wait, STANDARD_TOOLS is an object, not array.
86
+ // Original code issue: `step.useStandardTools ? STANDARD_TOOLS : []` -> if STANDARD_TOOLS is object, iterate?
87
+ // In original code: `for (const tool of extraTools)`
88
+ // If STANDARD_TOOLS is object, it is NOT iterable.
89
+ // The original code probably relied on `STANDARD_TOOLS` being iterable or `Object.values` was intended?
90
+ // Actually, `STANDARD_TOOLS` import in `llm-executor` might be different?
91
+ // No, strictly it probably failed if `useStandardTools` was true unless `STANDARD_TOOLS` is array-like.
92
+ // Let's assume `STANDARD_TOOLS` is a Record.
93
+ // I will iterate properly.
94
+
95
+ const toolsToRegister: any[] = [...(step.tools || [])];
96
+ if (step.useStandardTools === true) {
97
+ toolsToRegister.push(...Object.values(standardToolsRecord));
98
+ } else if (Array.isArray(step.useStandardTools)) {
99
+ for (const name of step.useStandardTools) {
100
+ if (standardToolsRecord[name]) toolsToRegister.push(standardToolsRecord[name]);
101
+ }
102
+ }
103
+
104
+ for (const tool of toolsToRegister) {
105
+ // Check if it's a standard tool or custom definition
106
+ const isStandard = Object.values(standardToolsRecord).includes(tool);
107
+
108
+ if (isStandard) {
109
+ // Wrap with security check
110
+ this.registerTool(
111
+ tool.name,
112
+ tool.description || '',
113
+ tool.parameters || {},
114
+ async (args) => {
115
+ validateStandardToolSecurity(tool.name, args, {
116
+ allowOutsideCwd: step.allowOutsideCwd,
117
+ allowInsecure: step.allowInsecure,
118
+ });
119
+ if (tool.execution) {
120
+ // Standard tools usually have .execute method directly on them in STANDARD_TOOLS definition?
121
+ // The definition in standard-tools.ts usually has `execute`.
122
+ // But the schema says `execution` (Step).
123
+ // Let's check `standard-tools.ts` structure if possible.
124
+ // Assuming `execute` exists on the tool definition for standard tools.
125
+ if (typeof tool.execute === 'function') {
126
+ return tool.execute(args, this.ctx.context);
127
+ }
128
+ // If it's a mixed definition (WorkflowStep tool format)
129
+ if (tool.execution) {
130
+ const toolContext = { ...this.ctx.context, args };
131
+ const result = await executeStepFn(tool.execution, toolContext);
132
+ return result.status === 'success'
133
+ ? this.applyContextUpdate(result.output)
134
+ : `Error: ${result.error}`;
135
+ }
136
+ // Fallback
137
+ return 'Error: No execution defined for standard tool';
138
+ }
139
+ return 'Error: Invalid tool definition';
140
+ }
141
+ );
142
+ } else {
143
+ // Custom tool
144
+ this.registerTool(
145
+ tool.name,
146
+ tool.description || '',
147
+ tool.parameters || {},
148
+ async (args) => {
149
+ if (tool.execution) {
150
+ const toolContext = { ...this.ctx.context, args };
151
+ const result = await executeStepFn(tool.execution, toolContext);
152
+ return result.status === 'success'
153
+ ? this.applyContextUpdate(result.output)
154
+ : `Error: ${result.error}`;
155
+ }
156
+ return 'Error: No execution defined';
157
+ }
158
+ );
159
+ }
160
+ }
161
+
162
+ // 3. MCP Tools
163
+ const mcpServersToConnect: (string | MCPServerConfig)[] = [...(step.mcpServers || [])];
164
+ if (step.useGlobalMcp && mcpManager) {
165
+ const globalServers = mcpManager.getGlobalServers();
166
+ for (const s of globalServers) {
167
+ if (
168
+ !mcpServersToConnect.some(
169
+ (existing) => (typeof existing === 'string' ? existing : existing.name) === s.name
170
+ )
171
+ ) {
172
+ mcpServersToConnect.push(s);
173
+ }
174
+ }
175
+ }
176
+
177
+ if (mcpServersToConnect.length > 0) {
178
+ for (const server of mcpServersToConnect) {
179
+ try {
180
+ let client: MCPClient | undefined;
181
+ if (mcpManager) {
182
+ client = await mcpManager.getClient(server, logger);
183
+ } else if (typeof server !== 'string') {
184
+ client = await MCPClient.createLocal(
185
+ server.command || 'node',
186
+ server.args || [],
187
+ server.env || {}
188
+ );
189
+ await client.initialize();
190
+ this.localMcpClients.push(client);
191
+ }
192
+
193
+ if (client) {
194
+ const mcpTools = await client.listTools();
195
+ for (const t of mcpTools) {
196
+ this.registerTool(t.name, t.description || '', t.inputSchema || {}, async (args) => {
197
+ const res = await client?.callTool(t.name, args);
198
+ return this.applyContextUpdate(res);
199
+ });
200
+ }
201
+ }
202
+ } catch (e) {
203
+ logger.warn(
204
+ `Failed to connect/list MCP tools for ${typeof server === 'string' ? server : server.name}: ${e}`
205
+ );
206
+ }
207
+ }
208
+ }
209
+
210
+ // 4. Ask & Transfer
211
+ this.registerControlTools(step, activeAgent, executeStepFn);
212
+
213
+ return this.aiTools;
214
+ }
215
+
216
+ private registerControlTools(step: LlmStep, activeAgent: Agent, executeStepFn: any) {
217
+ if (step.allowClarification) {
218
+ if (this.aiTools.ask) throw new Error('Tool "ask" is reserved.');
219
+ this.registerTool(
220
+ 'ask',
221
+ 'Ask the user a clarifying question.',
222
+ {
223
+ type: 'object',
224
+ properties: {
225
+ question: { type: 'string', description: 'The question to ask the user' },
226
+ },
227
+ required: ['question'],
228
+ },
229
+ async (args) => {
230
+ let question = args.question;
231
+ if (!question) {
232
+ question = args.text || args.message || args.query || args.prompt;
233
+ }
234
+ if (!question) {
235
+ this.ctx.logger.warn(`Tool 'ask' called without question`);
236
+ question = '(Please provide guidance)';
237
+ }
238
+
239
+ if (process.stdin.isTTY) {
240
+ const result = await executeStepFn(
241
+ {
242
+ id: `${step.id}-clarify`,
243
+ type: 'human',
244
+ message: `\nšŸ¤” Question from ${activeAgent.name}: ${question}`,
245
+ inputType: 'text',
246
+ } as Step,
247
+ this.ctx.context
248
+ );
249
+ return String(result.output);
250
+ }
251
+ this.requiresSuspend = true;
252
+ this.suspendData = { question };
253
+ return 'Suspended for user input';
254
+ }
255
+ );
256
+ }
257
+
258
+ if (step.allowedHandoffs && step.allowedHandoffs.length > 0) {
259
+ if (this.aiTools[TRANSFER_TOOL_NAME])
260
+ throw new Error(`Tool "${TRANSFER_TOOL_NAME}" is reserved.`);
261
+ this.registerTool(
262
+ TRANSFER_TOOL_NAME,
263
+ `Transfer control to another agent. Allowed: ${step.allowedHandoffs.join(', ')}`,
264
+ {
265
+ type: 'object',
266
+ properties: { agent_name: { type: 'string' } },
267
+ required: ['agent_name'],
268
+ },
269
+ async (args) => {
270
+ if (!step.allowedHandoffs?.includes(args.agent_name))
271
+ return `Error: Agent ${args.agent_name} not allowed.`;
272
+ try {
273
+ const nextAgentPath = resolveAgentPath(args.agent_name, this.ctx.workflowDir);
274
+ const nextAgent = parseAgent(nextAgentPath);
275
+ this.pendingTransfer = nextAgent;
276
+ return `Transferred to agent ${args.agent_name}.`;
277
+ } catch (e) {
278
+ return `Error resolving agent: ${e}`;
279
+ }
280
+ }
281
+ );
282
+ }
283
+ }
284
+
285
+ private registerTool(
286
+ name: string,
287
+ description: string,
288
+ parameters: any,
289
+ execute: (args: any) => Promise<any>
290
+ ) {
291
+ let resolvedParameters = parameters;
292
+ if (
293
+ !resolvedParameters ||
294
+ typeof resolvedParameters !== 'object' ||
295
+ Array.isArray(resolvedParameters)
296
+ ) {
297
+ // fallback or error
298
+ if (!resolvedParameters) resolvedParameters = { type: 'object', properties: {} };
299
+ }
300
+
301
+ const safeParameters = { ...resolvedParameters };
302
+ if (
303
+ safeParameters.type === 'object' &&
304
+ safeParameters.properties &&
305
+ safeParameters.additionalProperties === undefined
306
+ ) {
307
+ safeParameters.additionalProperties = false;
308
+ }
309
+
310
+ this.tools.push({ name, description, parameters: safeParameters, execute });
311
+
312
+ const schema = jsonSchema(safeParameters);
313
+ this.aiTools[name] = createTool({
314
+ description,
315
+ parameters: schema,
316
+ execute: async (args: any) => {
317
+ const actualArgs = args || {};
318
+ if (name !== 'ask') {
319
+ const argsText = Object.keys(actualArgs).length
320
+ ? ` ${safeJsonStringify(actualArgs)}`
321
+ : '';
322
+ this.ctx.logger.log(` šŸ› ļø Tool Call: ${name}${argsText}`);
323
+ } else {
324
+ this.ctx.logger.debug(' šŸ› ļø Tool Call: ask');
325
+ }
326
+ try {
327
+ return await execute(actualArgs);
328
+ } catch (err) {
329
+ const msg = err instanceof Error ? err.message : String(err);
330
+ this.ctx.logger.error(` āœ— Tool Error (${name}): ${msg}`);
331
+ return { error: msg };
332
+ }
333
+ },
334
+ } as any);
335
+ }
336
+
337
+ private applyContextUpdate(value: unknown): unknown {
338
+ // ... logic from llm-executor ...
339
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return value;
340
+ const record = value as Record<string, unknown>;
341
+ const CONTEXT_UPDATE_KEY = LLM.CONTEXT_UPDATE_KEY;
342
+ if (!(CONTEXT_UPDATE_KEY in record)) return value;
343
+
344
+ const update = record[CONTEXT_UPDATE_KEY] as any;
345
+ if (update?.env) {
346
+ this.ctx.context.env = this.ctx.context.env || {};
347
+ Object.assign(this.ctx.context.env, update.env);
348
+ }
349
+ if (update?.memory) {
350
+ this.ctx.context.memory = this.ctx.context.memory || {};
351
+ Object.assign(this.ctx.context.memory, update.memory);
352
+ }
353
+ const { [CONTEXT_UPDATE_KEY]: _ignored, ...cleaned } = record;
354
+ return cleaned;
355
+ }
356
+
357
+ public getToolImplementation(name: string): ToolImplementation | undefined {
358
+ return this.tools.find((t) => t.name === name);
359
+ }
360
+ }