-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathstep-runner.ts
More file actions
166 lines (147 loc) · 6.6 KB
/
Copy pathstep-runner.ts
File metadata and controls
166 lines (147 loc) · 6.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
/**
* Step runner — executes a single compiled plan node as a subagent.
*
* Reuses the existing subagent infrastructure (runSubagent, SubagentManager,
* profile activation) but wraps it to produce a StepExecResult with replan
* detection and file-modification tracking.
*/
import type { BaseChatModel } from "@langchain/core/language_models/chat_models";
import { logger } from "../logger";
import { ToolRegistry } from "../tools/registry";
import { runSubagent } from "../subagents/runner";
import type { AgentProfileRegistry } from "../agents/registry";
import { activateProfile } from "../agents/activator";
import { createLLM } from "../llm";
import { appConfig } from "../config";
import type { CompiledPlanNode, StepExecResult } from "./types";
// ─────────────────────────────────────────────────────────────────────────────
// Constants
// ─────────────────────────────────────────────────────────────────────────────
/** Map complexity to iteration budget. */
function iterationBudget(complexity: "low" | "medium" | "high"): number {
if (complexity === "high") return 20;
if (complexity === "medium") return 12;
return 6;
}
/** Markers that trigger a replan request from step output. */
const REPLAN_MARKERS = [
"[REPLAN_REQUESTED]",
"[REQUEST_REPLAN]",
"REPLAN_REQUESTED",
];
// ─────────────────────────────────────────────────────────────────────────────
// Public API
// ─────────────────────────────────────────────────────────────────────────────
export interface StepRunnerDeps {
registry: ToolRegistry;
llm?: BaseChatModel;
profileRegistry?: AgentProfileRegistry;
/** Shared context from the graph state (conversation history, prior step outputs). */
sharedContext?: Record<string, unknown>;
/** The original user request; injected into every step prompt to prevent hallucination. */
originalRequest?: string;
}
/**
* Execute a single plan node using the subagent runner.
*
* - Selects tool subset from node.toolsNeeded
* - Applies agentProfile if specified
* - Sets iteration budget from complexity
* - Detects replan requests in the output
*/
export async function runPlannedStep(
node: CompiledPlanNode,
deps: StepRunnerDeps,
): Promise<StepExecResult> {
let stepLlm = deps.llm;
let stepTools = node.toolsNeeded;
// Apply agent profile overrides when specified
if (node.agentProfile && deps.profileRegistry) {
const profile = deps.profileRegistry.get(node.agentProfile);
if (profile) {
const runtimeConfig = activateProfile(profile);
if (runtimeConfig.activeTools.length > 0 && stepTools.length > 0) {
const profileToolSet = new Set(runtimeConfig.activeTools);
stepTools = stepTools.filter((t) => profileToolSet.has(t));
} else if (runtimeConfig.activeTools.length > 0) {
stepTools = runtimeConfig.activeTools;
}
const needsNewLlm =
runtimeConfig.model !== undefined || runtimeConfig.temperature !== undefined;
if (needsNewLlm) {
stepLlm = createLLM({
...appConfig,
...(runtimeConfig.model !== undefined && { llmModel: runtimeConfig.model }),
...(runtimeConfig.temperature !== undefined && {
llmTemperature: runtimeConfig.temperature,
}),
});
}
} else {
logger.warn({ nodeId: node.id, agentProfile: node.agentProfile },
"Node references unknown agent profile; using defaults");
}
}
const stepToolNames = stepTools.length > 0
? stepTools
: deps.registry.list().map((t) => t.name);
const toolList = stepToolNames.length > 0
? `Available tools: ${stepToolNames.join(", ")}.`
: "No tools available.";
const stepSystemPrompt =
`You are an AI agent executing one step of a larger plan.\n` +
(deps.originalRequest ? `Original user request (for context): ${deps.originalRequest}\n` : ``) +
`Step: ${node.description}\n` +
`${toolList}\n` +
`Instructions:\n` +
`- Use tools only as needed to complete the step.\n` +
`- Once you have enough information, respond with your final answer directly — do NOT call more tools.\n` +
`- Do NOT repeat a tool call if you already have a useful result from it.\n` +
`- Be concise.`;
try {
const result = await runSubagent(
{
name: `graph-step-${node.id}`,
tools: stepToolNames,
maxIterations: iterationBudget(node.estimatedComplexity),
systemPrompt: stepSystemPrompt,
sharedContext: deps.sharedContext,
},
node.description,
deps.registry,
stepLlm,
);
// Detect replan request in the output
const replanRequested = detectReplanRequest(result.output);
return {
status: "success",
output: result.output,
filesModified: result.filesModified,
replanRequested: replanRequested.requested,
replanReason: replanRequested.reason,
};
} catch (err) {
const errorMsg = err instanceof Error ? err.message : String(err);
logger.warn({ nodeId: node.id, error: errorMsg }, "Step execution failed");
return {
status: "failed",
output: "",
error: errorMsg,
};
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Replan detection
// ─────────────────────────────────────────────────────────────────────────────
function detectReplanRequest(output: string): { requested: boolean; reason?: string } {
for (const marker of REPLAN_MARKERS) {
if (output.includes(marker)) {
// Try to extract a reason after the marker
const idx = output.indexOf(marker);
const after = output.slice(idx + marker.length).trim();
const reason = after.length > 0 ? after.slice(0, 200) : "Step requested replan";
return { requested: true, reason };
}
}
return { requested: false };
}