keystone-cli 1.0.2 → 1.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 +288 -24
- package/package.json +8 -4
- package/src/cli.ts +538 -419
- package/src/commands/doc.ts +31 -0
- package/src/commands/event.ts +29 -0
- package/src/commands/graph.ts +37 -0
- package/src/commands/index.ts +14 -0
- package/src/commands/init.ts +185 -0
- package/src/commands/run.ts +124 -0
- package/src/commands/schema.ts +40 -0
- package/src/commands/utils.ts +78 -0
- package/src/commands/validate.ts +111 -0
- package/src/db/memory-db.ts +50 -2
- package/src/db/workflow-db.test.ts +314 -0
- package/src/db/workflow-db.ts +810 -210
- package/src/expression/evaluator-audit.test.ts +4 -2
- package/src/expression/evaluator.test.ts +14 -1
- package/src/expression/evaluator.ts +166 -19
- package/src/parser/config-schema.ts +18 -0
- package/src/parser/schema.ts +153 -22
- package/src/parser/test-schema.ts +6 -6
- package/src/parser/workflow-parser.test.ts +24 -0
- package/src/parser/workflow-parser.ts +65 -3
- package/src/runner/auto-heal.test.ts +5 -6
- package/src/runner/blueprint-executor.test.ts +2 -2
- package/src/runner/debug-repl.test.ts +5 -8
- package/src/runner/debug-repl.ts +59 -16
- package/src/runner/durable-timers.test.ts +11 -2
- package/src/runner/engine-executor.test.ts +1 -1
- package/src/runner/events.ts +57 -0
- package/src/runner/executors/artifact-executor.ts +166 -0
- package/src/runner/{blueprint-executor.ts → executors/blueprint-executor.ts} +15 -7
- package/src/runner/{engine-executor.ts → executors/engine-executor.ts} +55 -7
- package/src/runner/executors/file-executor.test.ts +48 -0
- package/src/runner/executors/file-executor.ts +324 -0
- package/src/runner/{foreach-executor.ts → executors/foreach-executor.ts} +168 -80
- package/src/runner/executors/human-executor.ts +144 -0
- package/src/runner/executors/join-executor.ts +75 -0
- package/src/runner/executors/llm-executor.ts +1266 -0
- package/src/runner/executors/memory-executor.ts +71 -0
- package/src/runner/executors/plan-executor.ts +104 -0
- package/src/runner/executors/request-executor.ts +265 -0
- package/src/runner/executors/script-executor.ts +43 -0
- package/src/runner/executors/shell-executor.ts +403 -0
- package/src/runner/executors/subworkflow-executor.ts +114 -0
- package/src/runner/executors/types.ts +69 -0
- package/src/runner/executors/wait-executor.ts +59 -0
- package/src/runner/join-scheduling.test.ts +197 -0
- package/src/runner/llm-adapter-runtime.test.ts +209 -0
- package/src/runner/llm-adapter.test.ts +419 -24
- package/src/runner/llm-adapter.ts +414 -17
- package/src/runner/llm-clarification.test.ts +2 -1
- package/src/runner/llm-executor.test.ts +532 -17
- package/src/runner/mcp-client-audit.test.ts +1 -2
- package/src/runner/mcp-client.ts +136 -46
- package/src/runner/mcp-manager.test.ts +4 -0
- package/src/runner/mcp-server.test.ts +58 -0
- package/src/runner/mcp-server.ts +26 -0
- package/src/runner/memoization.test.ts +190 -0
- package/src/runner/optimization-runner.ts +4 -9
- package/src/runner/quality-gate.test.ts +69 -0
- package/src/runner/reflexion.test.ts +6 -17
- package/src/runner/resource-pool.ts +102 -14
- package/src/runner/services/context-builder.ts +144 -0
- package/src/runner/services/secret-manager.ts +105 -0
- package/src/runner/services/workflow-validator.ts +131 -0
- package/src/runner/shell-executor.test.ts +28 -4
- package/src/runner/standard-tools-ast.test.ts +196 -0
- package/src/runner/standard-tools-execution.test.ts +27 -0
- package/src/runner/standard-tools-integration.test.ts +6 -10
- package/src/runner/standard-tools.ts +339 -102
- package/src/runner/step-executor.test.ts +216 -4
- package/src/runner/step-executor.ts +69 -941
- package/src/runner/stream-utils.ts +7 -3
- package/src/runner/test-harness.ts +20 -1
- package/src/runner/timeout.test.ts +10 -0
- package/src/runner/timeout.ts +11 -2
- package/src/runner/tool-integration.test.ts +1 -1
- package/src/runner/wait-step.test.ts +102 -0
- package/src/runner/workflow-runner.test.ts +208 -15
- package/src/runner/workflow-runner.ts +890 -818
- package/src/runner/workflow-scheduler.ts +75 -0
- package/src/runner/workflow-state.ts +269 -0
- package/src/runner/workflow-subflows.test.ts +13 -12
- package/src/scripts/generate-schemas.ts +16 -0
- package/src/templates/agents/explore.md +1 -0
- package/src/templates/agents/general.md +1 -0
- package/src/templates/agents/handoff-router.md +14 -0
- package/src/templates/agents/handoff-specialist.md +15 -0
- package/src/templates/agents/keystone-architect.md +13 -44
- package/src/templates/agents/my-agent.md +1 -0
- package/src/templates/agents/software-engineer.md +1 -0
- package/src/templates/agents/summarizer.md +1 -0
- package/src/templates/agents/test-agent.md +1 -0
- package/src/templates/agents/tester.md +1 -0
- package/src/templates/{basic-inputs.yaml → basics/basic-inputs.yaml} +2 -0
- package/src/templates/{basic-shell.yaml → basics/basic-shell.yaml} +2 -1
- package/src/templates/{full-feature-demo.yaml → basics/full-feature-demo.yaml} +2 -0
- package/src/templates/{stop-watch.yaml → basics/stop-watch.yaml} +1 -0
- package/src/templates/{child-rollback.yaml → control-flow/child-rollback.yaml} +1 -0
- package/src/templates/{cleanup-finally.yaml → control-flow/cleanup-finally.yaml} +1 -0
- package/src/templates/{fan-out-fan-in.yaml → control-flow/fan-out-fan-in.yaml} +3 -0
- package/src/templates/control-flow/idempotency-example.yaml +30 -0
- package/src/templates/{loop-parallel.yaml → control-flow/loop-parallel.yaml} +3 -0
- package/src/templates/{parent-rollback.yaml → control-flow/parent-rollback.yaml} +1 -0
- package/src/templates/{retry-policy.yaml → control-flow/retry-policy.yaml} +3 -0
- package/src/templates/features/artifact-example.yaml +39 -0
- package/src/templates/{engine-example.yaml → features/engine-example.yaml} +1 -0
- package/src/templates/{human-interaction.yaml → features/human-interaction.yaml} +1 -0
- package/src/templates/{llm-agent.yaml → features/llm-agent.yaml} +1 -0
- package/src/templates/{memory-service.yaml → features/memory-service.yaml} +2 -0
- package/src/templates/{robust-automation.yaml → features/robust-automation.yaml} +3 -0
- package/src/templates/features/script-example.yaml +27 -0
- package/src/templates/patterns/agent-handoff.yaml +53 -0
- package/src/templates/{approval-process.yaml → patterns/approval-process.yaml} +1 -0
- package/src/templates/{batch-processor.yaml → patterns/batch-processor.yaml} +2 -0
- package/src/templates/{composition-child.yaml → patterns/composition-child.yaml} +1 -0
- package/src/templates/{composition-parent.yaml → patterns/composition-parent.yaml} +1 -0
- package/src/templates/{data-pipeline.yaml → patterns/data-pipeline.yaml} +2 -0
- package/src/templates/{decompose-implement.yaml → scaffolding/decompose-implement.yaml} +1 -0
- package/src/templates/{decompose-problem.yaml → scaffolding/decompose-problem.yaml} +1 -0
- package/src/templates/{decompose-research.yaml → scaffolding/decompose-research.yaml} +1 -0
- package/src/templates/{decompose-review.yaml → scaffolding/decompose-review.yaml} +1 -0
- package/src/templates/{dev.yaml → scaffolding/dev.yaml} +1 -0
- package/src/templates/scaffolding/review-loop.yaml +97 -0
- package/src/templates/{scaffold-feature.yaml → scaffolding/scaffold-feature.yaml} +2 -0
- package/src/templates/{scaffold-generate.yaml → scaffolding/scaffold-generate.yaml} +1 -0
- package/src/templates/{scaffold-plan.yaml → scaffolding/scaffold-plan.yaml} +1 -0
- package/src/templates/testing/invalid.yaml +6 -0
- package/src/ui/dashboard.tsx +191 -33
- package/src/utils/auth-manager.test.ts +337 -0
- package/src/utils/auth-manager.ts +157 -61
- package/src/utils/blueprint-utils.ts +4 -6
- package/src/utils/config-loader.test.ts +2 -0
- package/src/utils/config-loader.ts +12 -3
- package/src/utils/constants.ts +76 -0
- package/src/utils/container.ts +63 -0
- package/src/utils/context-injector.test.ts +200 -0
- package/src/utils/context-injector.ts +244 -0
- package/src/utils/doc-generator.ts +85 -0
- package/src/utils/env-filter.ts +45 -0
- package/src/utils/json-parser.test.ts +12 -0
- package/src/utils/json-parser.ts +30 -5
- package/src/utils/logger.ts +12 -1
- package/src/utils/mermaid.ts +4 -0
- package/src/utils/paths.ts +52 -1
- package/src/utils/process-sandbox-worker.test.ts +46 -0
- package/src/utils/process-sandbox.ts +227 -14
- package/src/utils/redactor.test.ts +11 -6
- package/src/utils/redactor.ts +25 -9
- package/src/utils/sandbox.ts +3 -0
- package/src/utils/workflow-registry.test.ts +2 -2
- package/src/runner/llm-executor.ts +0 -638
- package/src/runner/shell-executor.ts +0 -366
- package/src/templates/invalid.yaml +0 -5
|
@@ -1,638 +0,0 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
2
|
-
import type { ExpressionContext } from '../expression/evaluator';
|
|
3
|
-
import { ExpressionEvaluator } from '../expression/evaluator';
|
|
4
|
-
import { parseAgent, resolveAgentPath } from '../parser/agent-parser';
|
|
5
|
-
import type { AgentTool, LlmStep, Step } from '../parser/schema';
|
|
6
|
-
import { LIMITS } from '../utils/constants';
|
|
7
|
-
import { extractJson } from '../utils/json-parser';
|
|
8
|
-
import { ConsoleLogger, type Logger } from '../utils/logger.ts';
|
|
9
|
-
import { RedactionBuffer, Redactor } from '../utils/redactor';
|
|
10
|
-
import { type LLMMessage, getAdapter } from './llm-adapter';
|
|
11
|
-
import { MCPClient } from './mcp-client';
|
|
12
|
-
import type { MCPManager, MCPServerConfig } from './mcp-manager';
|
|
13
|
-
import { STANDARD_TOOLS, validateStandardToolSecurity } from './standard-tools';
|
|
14
|
-
import type { StepResult } from './step-executor';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Truncate message history to prevent unbounded memory growth.
|
|
18
|
-
* Preserves system messages and keeps the most recent messages.
|
|
19
|
-
*/
|
|
20
|
-
function estimateMessageBytes(message: LLMMessage): number {
|
|
21
|
-
let size = 0;
|
|
22
|
-
if (typeof message.content === 'string') {
|
|
23
|
-
size += Buffer.byteLength(message.content, 'utf8');
|
|
24
|
-
}
|
|
25
|
-
if (message.tool_calls) {
|
|
26
|
-
size += Buffer.byteLength(JSON.stringify(message.tool_calls), 'utf8');
|
|
27
|
-
}
|
|
28
|
-
if (message.reasoning) {
|
|
29
|
-
size += Buffer.byteLength(JSON.stringify(message.reasoning), 'utf8');
|
|
30
|
-
}
|
|
31
|
-
if (message.name) {
|
|
32
|
-
size += Buffer.byteLength(message.name, 'utf8');
|
|
33
|
-
}
|
|
34
|
-
return size;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function truncateStringByBytes(value: string, maxBytes: number): string {
|
|
38
|
-
if (maxBytes <= 0) return '';
|
|
39
|
-
if (Buffer.byteLength(value, 'utf8') <= maxBytes) return value;
|
|
40
|
-
|
|
41
|
-
let low = 0;
|
|
42
|
-
let high = value.length;
|
|
43
|
-
while (low < high) {
|
|
44
|
-
const mid = Math.ceil((low + high) / 2);
|
|
45
|
-
const slice = value.slice(0, mid);
|
|
46
|
-
if (Buffer.byteLength(slice, 'utf8') <= maxBytes) {
|
|
47
|
-
low = mid;
|
|
48
|
-
} else {
|
|
49
|
-
high = mid - 1;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return value.slice(0, low);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function truncateToolOutput(content: string, maxBytes: number): string {
|
|
56
|
-
const contentBytes = Buffer.byteLength(content, 'utf8');
|
|
57
|
-
if (contentBytes <= maxBytes) return content;
|
|
58
|
-
|
|
59
|
-
const suffix = '... [truncated output]';
|
|
60
|
-
const suffixBytes = Buffer.byteLength(suffix, 'utf8');
|
|
61
|
-
const truncated = truncateStringByBytes(content, Math.max(0, maxBytes - suffixBytes));
|
|
62
|
-
return `${truncated}${suffix}`;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function safeJsonStringify(value: unknown): string {
|
|
66
|
-
try {
|
|
67
|
-
return JSON.stringify(value);
|
|
68
|
-
} catch {
|
|
69
|
-
const seen = new WeakSet<object>();
|
|
70
|
-
try {
|
|
71
|
-
return JSON.stringify(value, (_key, val) => {
|
|
72
|
-
if (typeof val === 'bigint') return val.toString();
|
|
73
|
-
if (typeof val === 'object' && val !== null) {
|
|
74
|
-
if (seen.has(val)) return '[Circular]';
|
|
75
|
-
seen.add(val);
|
|
76
|
-
}
|
|
77
|
-
return val;
|
|
78
|
-
});
|
|
79
|
-
} catch {
|
|
80
|
-
return String(value);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function truncateMessages(
|
|
86
|
-
messages: LLMMessage[],
|
|
87
|
-
maxHistory: number,
|
|
88
|
-
maxBytes: number
|
|
89
|
-
): LLMMessage[] {
|
|
90
|
-
if (messages.length === 0) return messages;
|
|
91
|
-
|
|
92
|
-
// Keep all system messages
|
|
93
|
-
const systemMessages = messages.filter((m) => m.role === 'system');
|
|
94
|
-
const nonSystem = messages.filter((m) => m.role !== 'system');
|
|
95
|
-
|
|
96
|
-
// Keep most recent non-system messages, accounting for system messages
|
|
97
|
-
const nonSystemLimit = Math.max(0, maxHistory - systemMessages.length);
|
|
98
|
-
let keep = nonSystem.slice(-nonSystemLimit);
|
|
99
|
-
|
|
100
|
-
// Enforce total byte budget with a most-recent tail
|
|
101
|
-
if (maxBytes > 0) {
|
|
102
|
-
const systemBytes = systemMessages.reduce((total, msg) => total + estimateMessageBytes(msg), 0);
|
|
103
|
-
let remaining = maxBytes - systemBytes;
|
|
104
|
-
if (remaining <= 0) {
|
|
105
|
-
return systemMessages;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const tail: LLMMessage[] = [];
|
|
109
|
-
for (let i = keep.length - 1; i >= 0; i--) {
|
|
110
|
-
const msg = keep[i];
|
|
111
|
-
const msgBytes = estimateMessageBytes(msg);
|
|
112
|
-
if (msgBytes > remaining) break;
|
|
113
|
-
tail.push(msg);
|
|
114
|
-
remaining -= msgBytes;
|
|
115
|
-
}
|
|
116
|
-
keep = tail.reverse();
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return [...systemMessages, ...keep];
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
interface ToolDefinition {
|
|
123
|
-
name: string;
|
|
124
|
-
description?: string;
|
|
125
|
-
parameters: unknown;
|
|
126
|
-
source: 'agent' | 'step' | 'mcp' | 'standard' | 'handoff';
|
|
127
|
-
execution?: Step;
|
|
128
|
-
mcpClient?: MCPClient;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export async function executeLlmStep(
|
|
132
|
-
step: LlmStep,
|
|
133
|
-
context: ExpressionContext,
|
|
134
|
-
executeStepFn: (step: Step, context: ExpressionContext) => Promise<StepResult>,
|
|
135
|
-
logger: Logger = new ConsoleLogger(),
|
|
136
|
-
mcpManager?: MCPManager,
|
|
137
|
-
workflowDir?: string,
|
|
138
|
-
abortSignal?: AbortSignal,
|
|
139
|
-
getAdapterFn?: typeof getAdapter
|
|
140
|
-
): Promise<StepResult> {
|
|
141
|
-
const agentPath = resolveAgentPath(step.agent, workflowDir);
|
|
142
|
-
const agent = parseAgent(agentPath);
|
|
143
|
-
|
|
144
|
-
const provider = step.provider || agent.provider;
|
|
145
|
-
const model = step.model || agent.model || 'gpt-4o';
|
|
146
|
-
const prompt = ExpressionEvaluator.evaluateString(step.prompt, context);
|
|
147
|
-
|
|
148
|
-
const fullModelString = provider ? `${provider}:${model}` : model;
|
|
149
|
-
const { adapter, resolvedModel } = (getAdapterFn || getAdapter)(fullModelString);
|
|
150
|
-
|
|
151
|
-
// Inject schema instructions if present
|
|
152
|
-
let systemPrompt = agent.systemPrompt;
|
|
153
|
-
if (step.outputSchema) {
|
|
154
|
-
systemPrompt += `\n\nIMPORTANT: You must output valid JSON that matches the following schema:\n${JSON.stringify(step.outputSchema, null, 2)}`;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
let messages: LLMMessage[] = [];
|
|
158
|
-
const maxToolOutputBytes = LIMITS.MAX_TOOL_OUTPUT_BYTES;
|
|
159
|
-
|
|
160
|
-
// Resume from state if provided
|
|
161
|
-
const stepState =
|
|
162
|
-
context.steps && typeof context.steps === 'object'
|
|
163
|
-
? (context.steps as Record<string, { output?: unknown }>)[step.id]
|
|
164
|
-
: undefined;
|
|
165
|
-
const stepOutput = stepState?.output;
|
|
166
|
-
const resumeOutput =
|
|
167
|
-
stepOutput && typeof stepOutput === 'object' && 'messages' in stepOutput
|
|
168
|
-
? stepOutput
|
|
169
|
-
: context.output;
|
|
170
|
-
|
|
171
|
-
if (resumeOutput && typeof resumeOutput === 'object' && 'messages' in resumeOutput) {
|
|
172
|
-
messages.push(...(resumeOutput.messages as LLMMessage[]));
|
|
173
|
-
|
|
174
|
-
// If we have an answer in inputs, add it as a tool result for the last tool call
|
|
175
|
-
const stepInputs = context.inputs?.[step.id] as Record<string, unknown> | undefined;
|
|
176
|
-
if (stepInputs && typeof stepInputs === 'object' && '__answer' in stepInputs) {
|
|
177
|
-
const answer = stepInputs.__answer;
|
|
178
|
-
const lastMessage = messages[messages.length - 1];
|
|
179
|
-
const askCall = lastMessage?.tool_calls?.find((tc) => tc.function.name === 'ask');
|
|
180
|
-
if (askCall) {
|
|
181
|
-
messages.push({
|
|
182
|
-
role: 'tool',
|
|
183
|
-
tool_call_id: askCall.id,
|
|
184
|
-
name: 'ask',
|
|
185
|
-
content: truncateToolOutput(String(answer), maxToolOutputBytes),
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
} else {
|
|
190
|
-
messages.push({ role: 'system', content: systemPrompt }, { role: 'user', content: prompt });
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const localMcpClients: MCPClient[] = [];
|
|
194
|
-
const allTools: ToolDefinition[] = [];
|
|
195
|
-
const toolRegistry = new Map<string, string>();
|
|
196
|
-
const registerTool = (tool: ToolDefinition) => {
|
|
197
|
-
const existing = toolRegistry.get(tool.name);
|
|
198
|
-
if (existing) {
|
|
199
|
-
throw new Error(
|
|
200
|
-
`Duplicate tool name "${tool.name}" from ${tool.source}; already defined by ${existing}. Rename one of them.`
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
toolRegistry.set(tool.name, tool.source);
|
|
204
|
-
allTools.push(tool);
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
try {
|
|
208
|
-
// 1. Add agent tools
|
|
209
|
-
for (const tool of agent.tools) {
|
|
210
|
-
registerTool({
|
|
211
|
-
name: tool.name,
|
|
212
|
-
description: tool.description,
|
|
213
|
-
parameters: tool.parameters || {
|
|
214
|
-
type: 'object',
|
|
215
|
-
properties: {},
|
|
216
|
-
additionalProperties: true,
|
|
217
|
-
},
|
|
218
|
-
source: 'agent',
|
|
219
|
-
execution: tool.execution,
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// 2. Add step tools
|
|
224
|
-
if (step.tools) {
|
|
225
|
-
for (const tool of step.tools) {
|
|
226
|
-
registerTool({
|
|
227
|
-
name: tool.name,
|
|
228
|
-
description: tool.description,
|
|
229
|
-
parameters: tool.parameters || {
|
|
230
|
-
type: 'object',
|
|
231
|
-
properties: {},
|
|
232
|
-
additionalProperties: true,
|
|
233
|
-
},
|
|
234
|
-
source: 'step',
|
|
235
|
-
execution: tool.execution,
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// 3. Add Standard tools
|
|
241
|
-
if (step.useStandardTools) {
|
|
242
|
-
for (const tool of STANDARD_TOOLS) {
|
|
243
|
-
registerTool({
|
|
244
|
-
name: tool.name,
|
|
245
|
-
description: tool.description,
|
|
246
|
-
parameters: tool.parameters || {
|
|
247
|
-
type: 'object',
|
|
248
|
-
properties: {},
|
|
249
|
-
additionalProperties: true,
|
|
250
|
-
},
|
|
251
|
-
source: 'standard',
|
|
252
|
-
execution: tool.execution,
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// 4. Add Engine handoff tool
|
|
258
|
-
if (step.handoff) {
|
|
259
|
-
const toolName = step.handoff.name || 'handoff';
|
|
260
|
-
const description =
|
|
261
|
-
step.handoff.description || `Delegate to engine ${step.handoff.engine.command}`;
|
|
262
|
-
const parameters = step.handoff.inputSchema || {
|
|
263
|
-
type: 'object',
|
|
264
|
-
properties: {},
|
|
265
|
-
additionalProperties: true,
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
const handoffStep: Step = {
|
|
269
|
-
id: `${step.id}-handoff`,
|
|
270
|
-
type: 'engine',
|
|
271
|
-
command: step.handoff.engine.command,
|
|
272
|
-
args: step.handoff.engine.args,
|
|
273
|
-
env: step.handoff.engine.env,
|
|
274
|
-
cwd: step.handoff.engine.cwd,
|
|
275
|
-
timeout: step.handoff.engine.timeout,
|
|
276
|
-
outputSchema: step.handoff.engine.outputSchema,
|
|
277
|
-
input: step.handoff.engine.input ?? '${{ args }}',
|
|
278
|
-
};
|
|
279
|
-
|
|
280
|
-
registerTool({
|
|
281
|
-
name: toolName,
|
|
282
|
-
description,
|
|
283
|
-
parameters,
|
|
284
|
-
source: 'handoff',
|
|
285
|
-
execution: handoffStep,
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// 5. Add MCP tools
|
|
290
|
-
const mcpServersToConnect: (string | MCPServerConfig)[] = [...(step.mcpServers || [])];
|
|
291
|
-
if (step.useGlobalMcp && mcpManager) {
|
|
292
|
-
const globalServers = mcpManager.getGlobalServers();
|
|
293
|
-
for (const globalServer of globalServers) {
|
|
294
|
-
// Only add if not already explicitly listed
|
|
295
|
-
const alreadyListed = mcpServersToConnect.some((s) => {
|
|
296
|
-
const name = typeof s === 'string' ? s : s.name;
|
|
297
|
-
return name === globalServer.name;
|
|
298
|
-
});
|
|
299
|
-
if (!alreadyListed) {
|
|
300
|
-
mcpServersToConnect.push(globalServer);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (mcpServersToConnect.length > 0) {
|
|
306
|
-
await Promise.all(
|
|
307
|
-
mcpServersToConnect.map(async (server) => {
|
|
308
|
-
let client: MCPClient | undefined;
|
|
309
|
-
const serverName = typeof server === 'string' ? server : server.name;
|
|
310
|
-
|
|
311
|
-
try {
|
|
312
|
-
if (mcpManager) {
|
|
313
|
-
client = await mcpManager.getClient(server as string | MCPServerConfig, logger);
|
|
314
|
-
} else {
|
|
315
|
-
// Fallback if no manager (should not happen in normal workflow run)
|
|
316
|
-
if (typeof server === 'string') {
|
|
317
|
-
logger.error(
|
|
318
|
-
` ✗ Cannot reference global MCP server '${server}' without MCPManager`
|
|
319
|
-
);
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
logger.log(` 🔌 Connecting to MCP server: ${server.name}`);
|
|
323
|
-
client = await MCPClient.createLocal(
|
|
324
|
-
(server as MCPServerConfig).command || 'node',
|
|
325
|
-
(server as MCPServerConfig).args || [],
|
|
326
|
-
(server as MCPServerConfig).env || {}
|
|
327
|
-
);
|
|
328
|
-
await client.initialize();
|
|
329
|
-
localMcpClients.push(client);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (client) {
|
|
333
|
-
const mcpTools = await client.listTools();
|
|
334
|
-
for (const tool of mcpTools) {
|
|
335
|
-
registerTool({
|
|
336
|
-
name: tool.name,
|
|
337
|
-
description: tool.description,
|
|
338
|
-
parameters: tool.inputSchema,
|
|
339
|
-
source: 'mcp',
|
|
340
|
-
mcpClient: client,
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
} catch (error) {
|
|
345
|
-
logger.error(
|
|
346
|
-
` ✗ Failed to list tools from MCP server ${serverName}: ${error instanceof Error ? error.message : String(error)}`
|
|
347
|
-
);
|
|
348
|
-
if (!mcpManager && client) {
|
|
349
|
-
client.stop();
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
})
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const llmTools = allTools.map((t) => ({
|
|
357
|
-
type: 'function' as const,
|
|
358
|
-
function: {
|
|
359
|
-
name: t.name,
|
|
360
|
-
description: t.description,
|
|
361
|
-
parameters: t.parameters as Record<string, unknown>,
|
|
362
|
-
},
|
|
363
|
-
}));
|
|
364
|
-
|
|
365
|
-
if (step.allowClarification) {
|
|
366
|
-
if (toolRegistry.has('ask')) {
|
|
367
|
-
throw new Error(
|
|
368
|
-
'Tool name "ask" is reserved for clarification. Rename your tool or disable allowClarification.'
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
llmTools.push({
|
|
372
|
-
type: 'function' as const,
|
|
373
|
-
function: {
|
|
374
|
-
name: 'ask',
|
|
375
|
-
description:
|
|
376
|
-
'Ask the user a clarifying question if the initial request is ambiguous or missing information.',
|
|
377
|
-
parameters: {
|
|
378
|
-
type: 'object',
|
|
379
|
-
properties: {
|
|
380
|
-
question: {
|
|
381
|
-
type: 'string',
|
|
382
|
-
description: 'The question to ask the user',
|
|
383
|
-
},
|
|
384
|
-
},
|
|
385
|
-
required: ['question'],
|
|
386
|
-
} as Record<string, unknown>,
|
|
387
|
-
},
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// ReAct Loop
|
|
392
|
-
let iterations = 0;
|
|
393
|
-
const maxIterations = step.maxIterations || 10;
|
|
394
|
-
const totalUsage = {
|
|
395
|
-
prompt_tokens: 0,
|
|
396
|
-
completion_tokens: 0,
|
|
397
|
-
total_tokens: 0,
|
|
398
|
-
};
|
|
399
|
-
|
|
400
|
-
// Create redactor once outside the loop for performance (regex compilation)
|
|
401
|
-
const redactor = new Redactor(context.secrets || {}, {
|
|
402
|
-
forcedSecrets: context.secretValues || [],
|
|
403
|
-
});
|
|
404
|
-
const redactionBuffer = new RedactionBuffer(redactor);
|
|
405
|
-
const maxHistory = step.maxMessageHistory || LIMITS.MAX_MESSAGE_HISTORY;
|
|
406
|
-
const maxConversationBytes = LIMITS.MAX_CONVERSATION_BYTES;
|
|
407
|
-
const formatToolContent = (content: string): string =>
|
|
408
|
-
truncateToolOutput(content, maxToolOutputBytes);
|
|
409
|
-
|
|
410
|
-
while (iterations < maxIterations) {
|
|
411
|
-
iterations++;
|
|
412
|
-
if (abortSignal?.aborted) {
|
|
413
|
-
throw new Error('Step canceled');
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Truncate message history to prevent unbounded growth
|
|
417
|
-
messages = truncateMessages(messages, maxHistory, maxConversationBytes);
|
|
418
|
-
const truncatedMessages = messages;
|
|
419
|
-
|
|
420
|
-
const response = await adapter.chat(truncatedMessages, {
|
|
421
|
-
model: resolvedModel,
|
|
422
|
-
tools: llmTools.length > 0 ? llmTools : undefined,
|
|
423
|
-
onStream: (chunk) => {
|
|
424
|
-
if (!step.outputSchema) {
|
|
425
|
-
process.stdout.write(redactionBuffer.process(chunk));
|
|
426
|
-
}
|
|
427
|
-
},
|
|
428
|
-
signal: abortSignal,
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
if (!step.outputSchema) {
|
|
432
|
-
process.stdout.write(redactionBuffer.flush());
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
if (response.usage) {
|
|
436
|
-
totalUsage.prompt_tokens += response.usage.prompt_tokens;
|
|
437
|
-
totalUsage.completion_tokens += response.usage.completion_tokens;
|
|
438
|
-
totalUsage.total_tokens += response.usage.total_tokens;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const { message } = response;
|
|
442
|
-
messages.push(message);
|
|
443
|
-
|
|
444
|
-
if (!message.tool_calls || message.tool_calls.length === 0) {
|
|
445
|
-
let output = message.content;
|
|
446
|
-
|
|
447
|
-
// If schema is defined, attempt to parse JSON
|
|
448
|
-
if (step.outputSchema && typeof output === 'string') {
|
|
449
|
-
try {
|
|
450
|
-
output = extractJson(output) as typeof output;
|
|
451
|
-
} catch (e) {
|
|
452
|
-
const errorMessage = `Failed to parse LLM output as JSON matching schema: ${e instanceof Error ? e.message : String(e)}`;
|
|
453
|
-
logger.error(` ⚠️ ${errorMessage}. Retrying...`);
|
|
454
|
-
|
|
455
|
-
messages.push({
|
|
456
|
-
role: 'user',
|
|
457
|
-
content: `Error: ${errorMessage}\n\nPlease correct your output to be valid JSON matching the schema.`,
|
|
458
|
-
});
|
|
459
|
-
continue;
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
return {
|
|
464
|
-
output,
|
|
465
|
-
status: 'success',
|
|
466
|
-
usage: totalUsage,
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Execute tools
|
|
471
|
-
for (const toolCall of message.tool_calls) {
|
|
472
|
-
if (abortSignal?.aborted) {
|
|
473
|
-
throw new Error('Step canceled');
|
|
474
|
-
}
|
|
475
|
-
const argsStr = toolCall.function.arguments;
|
|
476
|
-
let displayArgs = '';
|
|
477
|
-
try {
|
|
478
|
-
const parsedArgs = JSON.parse(argsStr);
|
|
479
|
-
const keys = Object.keys(parsedArgs);
|
|
480
|
-
if (keys.length > 0) {
|
|
481
|
-
const formatted = JSON.stringify(parsedArgs);
|
|
482
|
-
displayArgs = formatted.length > 100 ? `${formatted.substring(0, 100)}...` : formatted;
|
|
483
|
-
}
|
|
484
|
-
} catch (e) {
|
|
485
|
-
displayArgs = argsStr.length > 100 ? `${argsStr.substring(0, 100)}...` : argsStr;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
logger.log(
|
|
489
|
-
` 🛠️ Tool Call: ${toolCall.function.name}${displayArgs ? ` ${displayArgs}` : ''}`
|
|
490
|
-
);
|
|
491
|
-
const toolInfo = allTools.find((t) => t.name === toolCall.function.name);
|
|
492
|
-
|
|
493
|
-
if (!toolInfo) {
|
|
494
|
-
if (toolCall.function.name === 'ask' && step.allowClarification) {
|
|
495
|
-
let args: { question: string };
|
|
496
|
-
try {
|
|
497
|
-
args = JSON.parse(toolCall.function.arguments);
|
|
498
|
-
} catch (e) {
|
|
499
|
-
messages.push({
|
|
500
|
-
role: 'tool',
|
|
501
|
-
tool_call_id: toolCall.id,
|
|
502
|
-
name: 'ask',
|
|
503
|
-
content: formatToolContent(
|
|
504
|
-
`Error: Invalid JSON in arguments: ${e instanceof Error ? e.message : String(e)}`
|
|
505
|
-
),
|
|
506
|
-
});
|
|
507
|
-
continue;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
if (process.stdin.isTTY) {
|
|
511
|
-
// In TTY, we can use a human step to get the answer immediately
|
|
512
|
-
logger.log(`\n🤔 Question from ${agent.name}: ${args.question}`);
|
|
513
|
-
const result = await executeStepFn(
|
|
514
|
-
{
|
|
515
|
-
id: `${step.id}-clarify`,
|
|
516
|
-
type: 'human',
|
|
517
|
-
message: args.question,
|
|
518
|
-
inputType: 'text',
|
|
519
|
-
} as Step,
|
|
520
|
-
context
|
|
521
|
-
);
|
|
522
|
-
|
|
523
|
-
messages.push({
|
|
524
|
-
role: 'tool',
|
|
525
|
-
tool_call_id: toolCall.id,
|
|
526
|
-
name: 'ask',
|
|
527
|
-
content: formatToolContent(String(result.output)),
|
|
528
|
-
});
|
|
529
|
-
continue;
|
|
530
|
-
}
|
|
531
|
-
// In non-TTY, we suspend
|
|
532
|
-
messages = truncateMessages(messages, maxHistory, maxConversationBytes);
|
|
533
|
-
return {
|
|
534
|
-
status: 'suspended',
|
|
535
|
-
output: {
|
|
536
|
-
messages,
|
|
537
|
-
question: args.question,
|
|
538
|
-
},
|
|
539
|
-
usage: totalUsage,
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
messages.push({
|
|
544
|
-
role: 'tool',
|
|
545
|
-
tool_call_id: toolCall.id,
|
|
546
|
-
name: toolCall.function.name,
|
|
547
|
-
content: formatToolContent(`Error: Tool ${toolCall.function.name} not found`),
|
|
548
|
-
});
|
|
549
|
-
continue;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
let args: Record<string, unknown>;
|
|
553
|
-
try {
|
|
554
|
-
args = JSON.parse(toolCall.function.arguments);
|
|
555
|
-
} catch (e) {
|
|
556
|
-
messages.push({
|
|
557
|
-
role: 'tool',
|
|
558
|
-
tool_call_id: toolCall.id,
|
|
559
|
-
name: toolCall.function.name,
|
|
560
|
-
content: formatToolContent(
|
|
561
|
-
`Error: Invalid JSON in arguments: ${e instanceof Error ? e.message : String(e)}`
|
|
562
|
-
),
|
|
563
|
-
});
|
|
564
|
-
continue;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
if (toolInfo.source === 'mcp' && toolInfo.mcpClient) {
|
|
568
|
-
try {
|
|
569
|
-
const result = await toolInfo.mcpClient.callTool(toolInfo.name, args);
|
|
570
|
-
messages.push({
|
|
571
|
-
role: 'tool',
|
|
572
|
-
tool_call_id: toolCall.id,
|
|
573
|
-
name: toolCall.function.name,
|
|
574
|
-
content: formatToolContent(safeJsonStringify(result)),
|
|
575
|
-
});
|
|
576
|
-
} catch (error) {
|
|
577
|
-
messages.push({
|
|
578
|
-
role: 'tool',
|
|
579
|
-
tool_call_id: toolCall.id,
|
|
580
|
-
name: toolCall.function.name,
|
|
581
|
-
content: formatToolContent(
|
|
582
|
-
`Error: ${error instanceof Error ? error.message : String(error)}`
|
|
583
|
-
),
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
} else if (toolInfo.execution) {
|
|
587
|
-
// Security validation for standard tools
|
|
588
|
-
if (toolInfo.source === 'standard') {
|
|
589
|
-
try {
|
|
590
|
-
validateStandardToolSecurity(toolInfo.name, args, {
|
|
591
|
-
allowOutsideCwd: step.allowOutsideCwd,
|
|
592
|
-
allowInsecure: step.allowInsecure,
|
|
593
|
-
});
|
|
594
|
-
} catch (error) {
|
|
595
|
-
messages.push({
|
|
596
|
-
role: 'tool',
|
|
597
|
-
tool_call_id: toolCall.id,
|
|
598
|
-
name: toolCall.function.name,
|
|
599
|
-
content: formatToolContent(
|
|
600
|
-
`Security Error: ${error instanceof Error ? error.message : String(error)}`
|
|
601
|
-
),
|
|
602
|
-
});
|
|
603
|
-
continue;
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// Execute the tool as a step
|
|
608
|
-
const toolContext: ExpressionContext = {
|
|
609
|
-
...context,
|
|
610
|
-
args, // Use args to pass parameters to tool execution
|
|
611
|
-
};
|
|
612
|
-
|
|
613
|
-
const result = await executeStepFn(toolInfo.execution, toolContext);
|
|
614
|
-
|
|
615
|
-
messages.push({
|
|
616
|
-
role: 'tool',
|
|
617
|
-
tool_call_id: toolCall.id,
|
|
618
|
-
name: toolCall.function.name,
|
|
619
|
-
content: formatToolContent(
|
|
620
|
-
result.status === 'success'
|
|
621
|
-
? safeJsonStringify(result.output)
|
|
622
|
-
: `Error: ${result.error}`
|
|
623
|
-
),
|
|
624
|
-
});
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
messages = truncateMessages(messages, maxHistory, maxConversationBytes);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
throw new Error('Max ReAct iterations reached');
|
|
632
|
-
} finally {
|
|
633
|
-
// Cleanup LOCAL MCP clients only. Shared clients are managed by MCPManager.
|
|
634
|
-
for (const client of localMcpClients) {
|
|
635
|
-
client.stop();
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
}
|