human-to-code 0.1.12 → 0.1.13

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.
@@ -1,147 +1,87 @@
1
- /**
2
- * Deep-agent orchestration for the direct `npx human-to-code .` flow.
3
- *
4
- * This path runs a LangChain/LangGraph "deep agent" (via the `deepagents`
5
- * harness) with the four batteries-included pillars:
6
- *
7
- * 1. Planning — the built-in `write_todos` tool decomposes the run.
8
- * 2. File System — a real-disk `FilesystemBackend` rooted at the project
9
- * gives the agent `ls`/`read_file`/`write_file`/`edit_file`/
10
- * `glob`/`grep` over the working tree.
11
- * 3. Sub Agents — a `planner`, `implementer`, and `reviewer` subagent are
12
- * reachable through the built-in `task` tool.
13
- * 4. Prompts — a task-specific system prompt for the main agent and one
14
- * per subagent role.
15
- *
16
- * Unlike the deterministic guided pipeline, the model drives scope, file
17
- * reads/writes, and delegation here. The blast radius is bounded to the project
18
- * root by the backend and by deny-write filesystem permissions on sensitive
19
- * paths, but this is an autonomous agent, not a hash-verified patch pipeline.
20
- */
21
- import { createDeepAgent } from "deepagents";
22
- import { FilesystemBackend } from "deepagents/node";
23
- import { languageProfile } from "./simple.js";
24
- /**
25
- * Writes are denied on version-control internals, dependencies, the tool's own
26
- * state, and credential-bearing files. Paths are backend-absolute globs (rooted
27
- * at the project). Reads stay permissive so the agent can ground itself.
28
- */
29
- const DENY_WRITE_PATHS = [
30
- "/.git/**",
31
- "/.hg/**",
32
- "/.svn/**",
33
- "/node_modules/**",
34
- "/.human-to-code/**",
35
- "/human-to-code.config.json",
36
- "/secrets.human",
37
- "/**/.env",
38
- "/**/.env.*",
39
- "/**/*.env",
40
- "/**/*.env.*",
41
- "/**/*.pem",
42
- "/**/*.key",
43
- "/**/id_rsa",
44
- "/**/id_ed25519",
45
- ];
46
- function mainSystemPrompt(languageLabel) {
1
+ /** Tool-rich, bounded agent orchestration for the direct conversion flow. */
2
+ import { readFile } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import { createAgent } from "langchain";
5
+ import { tool } from "@langchain/core/tools";
6
+ import { AGENT_TOOL_SCHEMAS, AgentToolRuntime, } from "./agent-tools.js";
7
+ import { extractInlineMarkers, languageProfile } from "./simple.js";
8
+ export class DeepAgentIncompleteError extends Error {
9
+ code;
10
+ constructor(code, message) {
11
+ super(message);
12
+ this.name = "DeepAgentIncompleteError";
13
+ this.code = code;
14
+ }
15
+ }
16
+ export const AGENT_TOOL_NAMES = Object.freeze(Object.keys(AGENT_TOOL_SCHEMAS));
17
+ export function agentSystemPrompt(languageLabel) {
47
18
  return [
48
- `You are human-to-code, an autonomous ${languageLabel} coding agent.`,
49
- "Your job is to fulfil natural-language change requests that a developer left in their codebase, in two forms:",
50
- " 1. Whole `.human` files (never `*.strict.human`): generate a sibling source file with the same base name and the language's extension, containing only real code.",
51
- " 2. Inline `@human` markers inside existing source files: replace the marker comment in place with the code it asks for, preserving all surrounding code.",
19
+ `You are human-to-code, an interactive ${languageLabel} software-engineering agent.`,
20
+ "Complete the confirmed worklist by inspecting and editing the project with tools. Be concise and stop as soon as every item is implemented.",
52
21
  "",
53
- "Working method — be efficient; every step is a slow model call, so avoid unnecessary ones:",
54
- "- First call write_todos once to record a short ordered plan covering every worklist item.",
55
- "- Read each target file once with read_file to ground yourself before editing. Every path is absolute and rooted at the project: it must start with `/` (e.g. `/src/a.ts`).",
56
- "- For simple items, write the code yourself directly with write_file (for `.human` file outputs) or edit_file (for inline markers). Only delegate to the `implementer` subagent for genuinely complex items, and only use the `reviewer` subagent when correctness is non-obvious. Do not delegate or review trivial one-liners.",
57
- "- Never remove or rewrite code that a marker did not ask you to change. Reuse declarations that already exist in the file instead of redeclaring them.",
58
- "- Keep todos updated: mark each in_progress when you start it and completed when its code is written.",
59
- "- When every item is done, stop and give a one-line summary. Do not re-read or re-verify files you have already written.",
22
+ "MANDATORY WORKFLOW:",
23
+ "1. Call TodoWrite exactly once at the start with one todo per worklist item. Only one todo may be in_progress.",
24
+ "2. Call Read before Edit, MultiEdit, or overwriting an existing file with Write.",
25
+ "3. Use Edit for one exact replacement, MultiEdit for several replacements in one file, and Write only for a new whole-file output.",
26
+ "4. Use Glob/Grep/LS for discovery. Use Bash only for a necessary build, typecheck, or test command. Never use Bash to read, search, or edit files.",
27
+ "5. Use Task only when an item truly needs multi-step research or independent implementation. Valid subagent_type values are general-purpose, implementer, and reviewer.",
28
+ "6. After writing an item, immediately call TodoWrite with that item completed and the next item in_progress.",
29
+ "7. When all items are completed, return one short sentence with no tool call. This is the stop condition. Never continue checking completed work.",
60
30
  "",
61
- "Output only real, compilable code into files. Do not add comments describing what you changed. Do not touch dependency, VCS, secret, or configuration files.",
31
+ "PATH AND FILE RULES:",
32
+ "- Every tool path is project-rooted and starts with '/'. Example: src/app.ts is /src/app.ts.",
33
+ "- A whole .human file creates the sibling output named in the worklist. Never overwrite an existing output without reading it.",
34
+ "- An inline @human marker is replaced in place. Preserve indentation and all unrelated code.",
35
+ "- Reuse existing declarations. Write only real compilable code; no markdown fences or change-description comments.",
36
+ "- Never touch dependencies, lockfiles, VCS data, tool state, credentials, or configuration unless the worklist explicitly names a normal source configuration file.",
37
+ "",
38
+ "TOOL-CALL FORMAT FOR SMALL MODELS:",
39
+ "Tool arguments are JSON objects. Do not print XML or narrate a tool call.",
40
+ "Plan example: TodoWrite({\"todos\":[{\"id\":\"1\",\"content\":\"Implement /src/math.ts marker\",\"status\":\"in_progress\"}]})",
41
+ "Read example: Read({\"file_path\":\"/src/math.ts\"})",
42
+ "Inline edit example: Edit({\"file_path\":\"/src/math.ts\",\"old_string\":\"// @human add two numbers\",\"new_string\":\"export function add(a: number, b: number) { return a + b; }\"})",
43
+ "New file example: Read({\"file_path\":\"/feature.human\"}) then Write({\"file_path\":\"/feature.ts\",\"content\":\"export const feature = true;\\n\"})",
44
+ "Validation example: Bash({\"command\":\"npm test\",\"description\":\"Runs project tests\"})",
45
+ "Finish example: TodoWrite({\"todos\":[{\"id\":\"1\",\"content\":\"Implement /src/math.ts marker\",\"status\":\"completed\"}]}) then answer: Implemented the requested conversion.",
62
46
  ].join("\n");
63
47
  }
64
- function plannerSubagent(languageLabel) {
65
- return {
66
- name: "planner",
67
- description: "Break a human-to-code conversion request into an ordered, file-by-file implementation plan. Use before writing any code when the worklist is non-trivial.",
68
- systemPrompt: [
69
- `You plan ${languageLabel} conversion work.`,
70
- "Given a worklist of `.human` files and inline `@human` markers, produce a concise, ordered list of concrete implementation steps.",
71
- "Account for dependencies between steps (e.g. a marker that uses a symbol another marker declares).",
72
- "Return only the plan as a numbered list. Write no code.",
73
- ].join("\n"),
74
- };
75
- }
76
- function implementerSubagent(languageLabel) {
77
- return {
78
- name: "implementer",
79
- description: "Generate the exact code for one `.human` file or one inline `@human` marker. Delegate each concrete code-writing step to this subagent.",
80
- systemPrompt: [
81
- `You are a precise ${languageLabel} code generator.`,
82
- "You are given one instruction and the relevant file context.",
83
- "Return only the code that satisfies the instruction: for a whole-file request, the full file body; for an inline marker, only the replacement for that marker.",
84
- "Reuse declarations already present in the file; never redeclare or duplicate them unless explicitly asked to shadow.",
85
- "No explanations, no markdown fences, no comments describing your work.",
86
- ].join("\n"),
87
- };
88
- }
89
- function reviewerSubagent(languageLabel) {
90
- return {
91
- name: "reviewer",
92
- description: "Check that generated code matches its instruction, is syntactically valid, and did not corrupt surrounding code. Use after each implementation step.",
93
- systemPrompt: [
94
- `You review generated ${languageLabel} code.`,
95
- "Given the original instruction and the code that was written, confirm it satisfies the instruction, is syntactically plausible, and preserves unrelated surrounding code.",
96
- "If it is correct, respond APPROVED with a one-line reason. If not, respond CHANGES with a concrete, minimal fix instruction.",
97
- ].join("\n"),
98
- };
48
+ function subagentSystemPrompt(type, languageLabel) {
49
+ const shared = [
50
+ `You are a focused ${languageLabel} ${type} subagent.`,
51
+ "Complete only the delegated task. Use project-rooted tools, preserve unrelated code, and return a concise result.",
52
+ "Do not delegate again. Do not touch protected, dependency, credential, VCS, or tool-state paths.",
53
+ ];
54
+ if (type === "reviewer")
55
+ shared.push("Inspect the requested code and report concrete correctness problems. Do not edit unless the prompt explicitly asks for fixes.");
56
+ else if (type === "implementer")
57
+ shared.push("Read the relevant files, implement the requested code, and stop immediately after the necessary edit.");
58
+ else
59
+ shared.push("Research the project with Read, Glob, and Grep; edit only when the delegated prompt explicitly requests implementation.");
60
+ return shared.join("\n");
99
61
  }
100
- /** Backend paths are absolute, rooted at the project (e.g. `src/a.ts` -> `/src/a.ts`). */
101
62
  function backendPath(relativePath) {
102
- return `/${relativePath.replace(/^\/+/u, "")}`;
63
+ return `/${relativePath.replace(/^\/+/, "")}`;
103
64
  }
104
65
  function taskPrompt(units, languageLabel) {
105
- const pathRule = "All filesystem tool paths are absolute and rooted at the project: they must start with `/` (for example the file `src/a.ts` is `/src/a.ts`). Never pass a relative path.";
106
- if (units.length === 0) {
107
- return [
108
- `Scan the project for any \`.human\` files or inline \`@human\` markers and convert them to ${languageLabel}.`,
109
- pathRule,
110
- "If there are none, report that nothing needs conversion.",
111
- ].join("\n");
112
- }
113
- const worklist = units
114
- .map((unit, index) => {
115
- if (unit.kind === "file") {
116
- return `${index + 1}. Whole file: read "${backendPath(unit.sourcePath)}" and write its generated ${languageLabel} into "${backendPath(unit.outputPath ?? unit.sourcePath)}". Instruction: ${unit.prompt}`;
117
- }
118
- return `${index + 1}. Inline marker in "${backendPath(unit.sourcePath)}": replace the @human marker in place, preserving surrounding code. Instruction: ${unit.prompt}`;
119
- })
120
- .join("\n");
66
+ if (units.length === 0)
67
+ return `Find and convert any .human files or inline @human markers to ${languageLabel}. If none exist, say so and stop.`;
68
+ const worklist = units.map((unit, index) => unit.kind === "file"
69
+ ? `${index + 1}. Whole file: Read ${backendPath(unit.sourcePath)} and create ${backendPath(unit.outputPath ?? unit.sourcePath)}. Instruction: ${unit.prompt}`
70
+ : `${index + 1}. Inline: Read ${backendPath(unit.sourcePath)} and replace only its @human marker. Instruction: ${unit.prompt}`).join("\n");
121
71
  return [
122
- `Convert the following ${units.length} item(s) to ${languageLabel}.`,
123
- pathRule,
124
- "",
72
+ `Convert these ${units.length} ${languageLabel} worklist item(s):`,
125
73
  worklist,
126
74
  "",
127
- "Plan once with write_todos, read each target file once, then write the code directly with write_file or edit_file (only delegating complex items to the implementer subagent). Report a short summary of what you wrote when done.",
75
+ "Start with TodoWrite. Implement in order. Stop immediately after every todo is completed.",
128
76
  ].join("\n");
129
77
  }
130
- /**
131
- * Construct a LangChain chat model for the configured provider.
132
- *
133
- * Both providers use the OpenAI-compatible chat client. Ollama is reached
134
- * through its `/v1` endpoint rather than `@langchain/ollama`'s native
135
- * `/api/chat`: the deep-agent harness emits tool messages with structured
136
- * (array) content, which the native Ollama adapter rejects, whereas the
137
- * OpenAI-compatible surface serializes them correctly.
138
- */
139
78
  export async function buildDeepAgentModel(options) {
140
79
  const { ChatOpenAI } = await import("@langchain/openai");
141
80
  if (options.provider === "openai") {
142
81
  return new ChatOpenAI({
143
82
  model: options.model,
144
83
  temperature: 0,
84
+ maxRetries: 1,
145
85
  ...(options.apiKey ? { apiKey: options.apiKey } : {}),
146
86
  ...(options.baseUrl ? { configuration: { baseURL: options.baseUrl } } : {}),
147
87
  });
@@ -151,115 +91,305 @@ export async function buildDeepAgentModel(options) {
151
91
  return new ChatOpenAI({
152
92
  model: options.model,
153
93
  temperature: 0,
154
- // Ollama ignores the key but the OpenAI client requires a non-empty value.
94
+ maxRetries: 1,
155
95
  apiKey: options.apiKey ?? "ollama",
156
96
  configuration: { baseURL: `${root}/v1` },
157
97
  });
158
98
  }
159
- throw new Error(`Provider '${options.provider}' has no deep-agent chat-model binding. Use openai or ollama.`);
99
+ throw new Error(`Provider '${options.provider}' has no agent chat-model binding. Use openai or ollama.`);
160
100
  }
161
- /**
162
- * Build (but do not run) the deep agent. Exposed so callers and tests can
163
- * inspect wiring without a live provider.
164
- */
165
- export async function buildDeepAgent(options) {
101
+ function toolDescription(name) {
102
+ const descriptions = {
103
+ Task: "Launch one stateless subagent for a complex delegated task. Supply a short description, detailed prompt, and subagent_type.",
104
+ Bash: "Run one bounded shell command from the project root. Use only for builds, tests, and necessary development commands; background runs return a bash_id.",
105
+ Glob: "Find project files using a glob pattern. Paths are project-rooted.",
106
+ Grep: "Search project file contents with ripgrep and optional output controls.",
107
+ LS: "List one project directory.",
108
+ ExitPlanMode: "Record that planning is complete for this already-confirmed conversion and continue implementation.",
109
+ Read: "Read a project file with numbered lines. Must be called before editing an existing file.",
110
+ Edit: "Perform one exact, unique string replacement in a file previously read.",
111
+ MultiEdit: "Atomically perform several exact replacements in one file previously read.",
112
+ Write: "Create a file or overwrite a previously read file.",
113
+ NotebookEdit: "Replace, insert, or delete one Jupyter notebook cell.",
114
+ WebFetch: "Fetch one reviewed public HTTPS URL as bounded text.",
115
+ TodoWrite: "Create or update the complete todo list. Use pending, in_progress, and completed statuses with one in_progress item.",
116
+ WebSearch: "Search the public web for current programming information with optional domain filters.",
117
+ BashOutput: "Read only new output from a background Bash run.",
118
+ KillBash: "Terminate a background Bash run by shell_id.",
119
+ };
120
+ return descriptions[name] ?? `${name} tool.`;
121
+ }
122
+ function createAgentTools(runtime, taskRunner) {
123
+ const handler = (name) => async (input) => {
124
+ if (name === "Task") {
125
+ if (!taskRunner)
126
+ return "Task delegation is disabled inside a subagent; complete the delegated work directly.";
127
+ return taskRunner(input);
128
+ }
129
+ if (name === "Bash")
130
+ return runtime.bash(input);
131
+ if (name === "Glob")
132
+ return runtime.glob(input);
133
+ if (name === "Grep")
134
+ return runtime.grep(input);
135
+ if (name === "LS")
136
+ return runtime.ls(input);
137
+ if (name === "ExitPlanMode")
138
+ return `Plan recorded: ${String(input.plan ?? "")}. Continue implementation now.`;
139
+ if (name === "Read")
140
+ return runtime.read(input);
141
+ if (name === "Edit")
142
+ return runtime.edit(input);
143
+ if (name === "MultiEdit")
144
+ return runtime.multiEdit(input);
145
+ if (name === "Write")
146
+ return runtime.write(input);
147
+ if (name === "NotebookEdit")
148
+ return runtime.notebookEdit(input);
149
+ if (name === "WebFetch")
150
+ return runtime.webFetch(input);
151
+ if (name === "TodoWrite")
152
+ return `Todo list updated (${Array.isArray(input.todos) ? input.todos.length : 0} items).`;
153
+ if (name === "WebSearch")
154
+ return runtime.webSearch(input);
155
+ if (name === "BashOutput")
156
+ return runtime.bashOutput(input);
157
+ if (name === "KillBash")
158
+ return runtime.killBash(input);
159
+ throw new Error(`Unsupported agent tool ${name}.`);
160
+ };
161
+ return AGENT_TOOL_NAMES.map((name) => tool(handler(name), {
162
+ name,
163
+ description: toolDescription(name),
164
+ schema: AGENT_TOOL_SCHEMAS[name],
165
+ }));
166
+ }
167
+ function messageText(message) {
168
+ if (!message || typeof message !== "object")
169
+ return "";
170
+ const content = message.content;
171
+ if (typeof content === "string")
172
+ return content.trim();
173
+ if (!Array.isArray(content))
174
+ return "";
175
+ return content.map((item) => item && typeof item === "object" && typeof item.text === "string"
176
+ ? item.text : "").join("\n").trim();
177
+ }
178
+ async function buildAgentBundle(options) {
166
179
  const label = languageProfile(options.language).label;
167
180
  const model = options.model_override ?? await buildDeepAgentModel(options);
168
- const backend = new FilesystemBackend({ rootDir: options.root });
169
- const permissions = [
170
- { operations: ["write"], paths: [...DENY_WRITE_PATHS], mode: "deny" },
171
- ];
172
- return createDeepAgent({
181
+ const activity = (event) => {
182
+ if (event.kind === "output")
183
+ options.onProgress?.({ kind: "output", name: event.name, text: event.text });
184
+ else
185
+ options.onProgress?.({ kind: "diff", path: event.path, lines: event.lines });
186
+ };
187
+ const runtime = new AgentToolRuntime({ root: options.root, onActivity: activity });
188
+ const taskRunner = async (input) => {
189
+ const type = typeof input.subagent_type === "string" ? input.subagent_type : "general-purpose";
190
+ const prompt = typeof input.prompt === "string" ? input.prompt : "";
191
+ const subagent = createAgent({
192
+ model,
193
+ tools: createAgentTools(runtime),
194
+ systemPrompt: subagentSystemPrompt(type, label),
195
+ });
196
+ const result = await subagent.invoke({ messages: [{ role: "user", content: prompt }] }, { recursionLimit: 48, ...(options.signal ? { signal: options.signal } : {}) });
197
+ const messages = Array.isArray(result.messages) ? result.messages : [];
198
+ return messageText(messages.at(-1)) || "Subagent completed without a written summary.";
199
+ };
200
+ const agent = createAgent({
173
201
  model,
174
- backend,
175
- systemPrompt: mainSystemPrompt(label),
176
- subagents: [plannerSubagent(label), implementerSubagent(label), reviewerSubagent(label)],
177
- permissions,
202
+ tools: createAgentTools(runtime, taskRunner),
203
+ systemPrompt: agentSystemPrompt(label),
178
204
  });
205
+ return { agent, runtime };
206
+ }
207
+ export async function buildDeepAgent(options) {
208
+ return (await buildAgentBundle(options)).agent;
179
209
  }
180
210
  function normalizeTodos(raw) {
181
- return raw.map((todo) => ({
211
+ return raw.map((todo, index) => ({
212
+ id: typeof todo.id === "string" ? todo.id : String(index + 1),
182
213
  content: typeof todo.content === "string" ? todo.content : String(todo.content ?? ""),
183
- status: typeof todo.status === "string" ? todo.status : String(todo.status ?? ""),
214
+ status: typeof todo.status === "string" ? todo.status : String(todo.status ?? "pending"),
184
215
  }));
185
216
  }
186
- /** Short human detail for a tool call (the path it touches or subagent it invokes). */
217
+ function todosFromArgs(args) {
218
+ if (!args || typeof args !== "object" || Array.isArray(args))
219
+ return undefined;
220
+ const raw = args.todos;
221
+ return Array.isArray(raw) ? normalizeTodos(raw) : undefined;
222
+ }
187
223
  function toolDetail(name, args) {
188
- if (!args)
224
+ if (!args || typeof args !== "object" || Array.isArray(args))
189
225
  return undefined;
190
- if (name === "task") {
191
- const sub = args.subagent_type ?? args.subagentType ?? args.name ?? args.description;
192
- return typeof sub === "string" ? sub.split(/\s+/u).slice(0, 6).join(" ") : undefined;
226
+ const record = args;
227
+ if (name.toLowerCase() === "task") {
228
+ const value = record.subagent_type ?? record.subagentType ?? record.description;
229
+ return typeof value === "string" ? value : undefined;
193
230
  }
194
- for (const key of ["file_path", "filePath", "path", "file"]) {
195
- const value = args[key];
231
+ if (name === "Bash" || name === "execute") {
232
+ const value = record.description ?? record.command;
233
+ return typeof value === "string" ? value.slice(0, 100) : undefined;
234
+ }
235
+ for (const key of ["file_path", "notebook_path", "filePath", "path", "file"]) {
236
+ const value = record[key];
196
237
  if (typeof value === "string" && value.length > 0)
197
238
  return value;
198
239
  }
199
240
  return undefined;
200
241
  }
201
- /**
202
- * Run the deep agent against the discovered worklist, streaming live progress
203
- * (planning, tool calls, delegation) through `onProgress`. The agent edits
204
- * files on disk through its filesystem backend; this returns the final plan and
205
- * summary it produced for the report.
206
- */
242
+ async function captureWorklist(units) {
243
+ return Promise.all(units.map(async (unit) => {
244
+ const target = unit.kind === "file" ? resolve(dirnameFor(unit.absoluteSource), unit.outputPath?.split("/").at(-1) ?? "") : unit.absoluteSource;
245
+ const initial = await readFile(target, "utf8").catch(() => undefined);
246
+ return { unit, initial };
247
+ }));
248
+ }
249
+ function dirnameFor(path) {
250
+ return path.slice(0, Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\")) + 1);
251
+ }
252
+ async function worklistComplete(root, states) {
253
+ if (states.length === 0)
254
+ return true;
255
+ for (const state of states) {
256
+ if (state.unit.kind === "file") {
257
+ const output = state.unit.outputPath;
258
+ if (!output)
259
+ return false;
260
+ const current = await readFile(resolve(root, output), "utf8").catch(() => undefined);
261
+ if (current === undefined || current.trim().length === 0 || current === state.initial)
262
+ return false;
263
+ continue;
264
+ }
265
+ const current = await readFile(state.unit.absoluteSource, "utf8").catch(() => undefined);
266
+ if (current === undefined || current === state.initial)
267
+ return false;
268
+ const markers = extractInlineMarkers(current);
269
+ if (markers.some((marker) => marker.prompt.trim() === state.unit.prompt.trim()))
270
+ return false;
271
+ }
272
+ return true;
273
+ }
274
+ function recursionFailure(error) {
275
+ const message = error instanceof Error ? error.message : String(error);
276
+ return /recursion limit|GRAPH_RECURSION_LIMIT/iu.test(message);
277
+ }
278
+ function defaultRecursionLimit(unitCount) {
279
+ return Math.min(120, Math.max(40, 28 + unitCount * 12));
280
+ }
281
+ function completedTodos(todos) {
282
+ return todos.map((todo) => ({ ...todo, status: "completed" }));
283
+ }
207
284
  export async function runDeepAgentConversion(options) {
208
- const agent = await buildDeepAgent(options);
285
+ const states = await captureWorklist(options.units);
286
+ const bundle = await buildAgentBundle(options);
209
287
  const label = languageProfile(options.language).label;
210
288
  const input = { messages: [{ role: "user", content: taskPrompt(options.units, label) }] };
211
- const config = {
212
- recursionLimit: options.recursionLimit ?? 150,
213
- ...(options.signal ? { signal: options.signal } : {}),
214
- };
289
+ const config = { recursionLimit: options.recursionLimit ?? defaultRecursionLimit(options.units.length), ...(options.signal ? { signal: options.signal } : {}) };
215
290
  const seenMessages = new Set();
216
291
  let todos = [];
217
292
  let lastTodosKey = "";
218
293
  let summary = "";
219
294
  let messageCount = 0;
220
- const stream = await agent.stream(input, { ...config, streamMode: "updates" });
221
- for await (const chunk of stream) {
222
- for (const update of Object.values(chunk)) {
223
- if (!update || typeof update !== "object")
224
- continue;
225
- const node = update;
226
- if (Array.isArray(node.todos)) {
227
- const next = normalizeTodos(node.todos);
228
- const key = JSON.stringify(next);
229
- if (key !== lastTodosKey) {
230
- lastTodosKey = key;
231
- todos = next;
232
- options.onProgress?.({ kind: "plan", todos: next });
295
+ let toolCount = 0;
296
+ let lastToolSignature = "";
297
+ let repeatedToolCalls = 0;
298
+ let stopReason = "model";
299
+ try {
300
+ const stream = await bundle.agent.stream(input, { ...config, streamMode: "updates" });
301
+ streamLoop: for await (const chunk of stream) {
302
+ for (const update of Object.values(chunk)) {
303
+ if (!update || typeof update !== "object")
304
+ continue;
305
+ const node = update;
306
+ if (Array.isArray(node.todos)) {
307
+ const next = normalizeTodos(node.todos);
308
+ const key = JSON.stringify(next);
309
+ if (key !== lastTodosKey) {
310
+ lastTodosKey = key;
311
+ todos = next;
312
+ options.onProgress?.({ kind: "plan", todos: next });
313
+ }
233
314
  }
234
- }
235
- if (Array.isArray(node.messages)) {
236
- for (const raw of node.messages) {
237
- const message = raw;
238
- const id = typeof message.id === "string" ? message.id : undefined;
239
- if (id) {
240
- if (seenMessages.has(id))
315
+ if (Array.isArray(node.messages)) {
316
+ for (const raw of node.messages) {
317
+ const message = raw;
318
+ const id = typeof message.id === "string" ? message.id : undefined;
319
+ if (id && seenMessages.has(id))
241
320
  continue;
242
- seenMessages.add(id);
243
- }
244
- messageCount += 1;
245
- const type = typeof message.getType === "function" ? message.getType() : undefined;
246
- if (type === "ai") {
247
- if (Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {
321
+ if (id)
322
+ seenMessages.add(id);
323
+ messageCount += 1;
324
+ const type = typeof message.getType === "function" ? message.getType() : undefined;
325
+ if (type === "ai" && Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {
248
326
  for (const call of message.tool_calls) {
249
327
  const name = typeof call.name === "string" ? call.name : "tool";
328
+ const signature = `${name}:${JSON.stringify(call.args ?? {})}`;
329
+ repeatedToolCalls = signature === lastToolSignature ? repeatedToolCalls + 1 : 1;
330
+ lastToolSignature = signature;
331
+ toolCount += 1;
250
332
  const detail = toolDetail(name, call.args);
251
333
  options.onProgress?.({ kind: "tool", name, ...(detail ? { detail } : {}) });
334
+ if (name === "TodoWrite" || name === "write_todos") {
335
+ const next = todosFromArgs(call.args);
336
+ if (next) {
337
+ todos = next;
338
+ options.onProgress?.({ kind: "plan", todos: next });
339
+ }
340
+ }
341
+ if (repeatedToolCalls >= 4) {
342
+ if (await worklistComplete(options.root, states)) {
343
+ stopReason = "worklist_complete";
344
+ summary = "Implemented every requested conversion; stopped a repeated tool loop.";
345
+ todos = completedTodos(todos);
346
+ options.onProgress?.({ kind: "plan", todos });
347
+ break streamLoop;
348
+ }
349
+ throw new DeepAgentIncompleteError("TOOL_LOOP", `Agent stopped after repeating the same ${name} call four times. No further project changes were attempted.`);
350
+ }
252
351
  }
253
352
  }
254
- else if (typeof message.content === "string" && message.content.trim().length > 0) {
255
- summary = message.content.trim();
256
- options.onProgress?.({ kind: "assistant", text: summary });
353
+ else if (type === "ai") {
354
+ const text = messageText(message);
355
+ if (text.length > 0) {
356
+ summary = text;
357
+ options.onProgress?.({ kind: "assistant", text });
358
+ }
257
359
  }
258
360
  }
259
361
  }
260
362
  }
363
+ if (toolCount > 0 && await worklistComplete(options.root, states)) {
364
+ stopReason = "worklist_complete";
365
+ summary ||= "Implemented every requested conversion.";
366
+ todos = completedTodos(todos);
367
+ options.onProgress?.({ kind: "plan", todos });
368
+ break;
369
+ }
370
+ }
371
+ }
372
+ catch (error) {
373
+ if (error instanceof DeepAgentIncompleteError)
374
+ throw error;
375
+ if (!recursionFailure(error))
376
+ throw error;
377
+ if (await worklistComplete(options.root, states)) {
378
+ stopReason = "worklist_complete";
379
+ summary = "Implemented every requested conversion before the agent step limit.";
380
+ todos = completedTodos(todos);
381
+ options.onProgress?.({ kind: "plan", todos });
261
382
  }
383
+ else {
384
+ throw new DeepAgentIncompleteError("STEP_LIMIT", `Agent reached its bounded ${config.recursionLimit}-step limit before completing the worklist. Partial edits were left visible for review; rerun with a more capable tool-calling model.`);
385
+ }
386
+ }
387
+ finally {
388
+ await bundle.runtime.cleanup();
389
+ }
390
+ if (states.length > 0 && !await worklistComplete(options.root, states)) {
391
+ throw new DeepAgentIncompleteError("INCOMPLETE", "Agent stopped without completing every discovered worklist item. Partial edits were left visible for review.");
262
392
  }
263
- return { todos, messageCount, summary };
393
+ return { todos, messageCount, toolCount, summary, stopReason };
264
394
  }
265
395
  //# sourceMappingURL=deep-agent.js.map