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.
- package/README.md +43 -4
- package/package.json +4 -1
- package/src/cli.ts +1 -0
- package/src/commands/event.ts +9 -0
- package/src/commands/run.ts +17 -0
- package/src/db/dynamic-state-manager.ts +12 -9
- package/src/db/memory-db.test.ts +19 -1
- package/src/db/memory-db.ts +101 -22
- package/src/db/workflow-db.ts +181 -9
- package/src/expression/evaluator.ts +4 -1
- package/src/parser/config-schema.ts +6 -0
- package/src/parser/schema.ts +1 -0
- package/src/runner/__test__/llm-test-setup.ts +43 -11
- package/src/runner/durable-timers.test.ts +1 -1
- package/src/runner/executors/dynamic-executor.ts +125 -88
- package/src/runner/executors/engine-executor.ts +10 -39
- package/src/runner/executors/file-executor.ts +67 -0
- package/src/runner/executors/foreach-executor.ts +170 -17
- package/src/runner/executors/human-executor.ts +18 -0
- package/src/runner/executors/llm/stream-handler.ts +103 -0
- package/src/runner/executors/llm/tool-manager.ts +360 -0
- package/src/runner/executors/llm-executor.ts +288 -555
- package/src/runner/executors/memory-executor.ts +41 -34
- package/src/runner/executors/shell-executor.ts +96 -52
- package/src/runner/executors/subworkflow-executor.ts +16 -0
- package/src/runner/executors/types.ts +3 -1
- package/src/runner/executors/verification_fixes.test.ts +46 -0
- package/src/runner/join-scheduling.test.ts +2 -1
- package/src/runner/llm-adapter.integration.test.ts +10 -5
- package/src/runner/llm-adapter.ts +57 -18
- package/src/runner/llm-clarification.test.ts +4 -1
- package/src/runner/llm-executor.test.ts +21 -7
- package/src/runner/mcp-client.ts +36 -2
- package/src/runner/mcp-server.ts +65 -36
- package/src/runner/recovery-security.test.ts +5 -2
- package/src/runner/reflexion.test.ts +6 -3
- package/src/runner/services/context-builder.ts +13 -4
- package/src/runner/services/workflow-validator.ts +2 -1
- package/src/runner/standard-tools-ast.test.ts +4 -2
- package/src/runner/standard-tools-execution.test.ts +14 -1
- package/src/runner/standard-tools-integration.test.ts +6 -0
- package/src/runner/standard-tools.ts +13 -10
- package/src/runner/step-executor.ts +2 -2
- package/src/runner/tool-integration.test.ts +4 -1
- package/src/runner/workflow-runner.test.ts +23 -12
- package/src/runner/workflow-runner.ts +172 -79
- package/src/runner/workflow-state.ts +181 -111
- package/src/ui/dashboard.tsx +17 -3
- package/src/utils/config-loader.ts +4 -0
- package/src/utils/constants.ts +4 -0
- package/src/utils/context-injector.test.ts +27 -27
- package/src/utils/context-injector.ts +68 -26
- package/src/utils/process-sandbox.ts +138 -148
- package/src/utils/redactor.ts +39 -9
- package/src/utils/resource-loader.ts +24 -19
- package/src/utils/sandbox.ts +6 -0
- 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
|
+
}
|