openplanter 0.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 +210 -0
- package/dist/builder.d.ts +11 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +179 -0
- package/dist/builder.js.map +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +548 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +51 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +114 -0
- package/dist/config.js.map +1 -0
- package/dist/credentials.d.ts +52 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +371 -0
- package/dist/credentials.js.map +1 -0
- package/dist/demo.d.ts +26 -0
- package/dist/demo.d.ts.map +1 -0
- package/dist/demo.js +95 -0
- package/dist/demo.js.map +1 -0
- package/dist/engine.d.ts +91 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +1036 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/investigation-tools/aph-holdings.d.ts +61 -0
- package/dist/investigation-tools/aph-holdings.d.ts.map +1 -0
- package/dist/investigation-tools/aph-holdings.js +459 -0
- package/dist/investigation-tools/aph-holdings.js.map +1 -0
- package/dist/investigation-tools/asic-officer-lookup.d.ts +42 -0
- package/dist/investigation-tools/asic-officer-lookup.d.ts.map +1 -0
- package/dist/investigation-tools/asic-officer-lookup.js +197 -0
- package/dist/investigation-tools/asic-officer-lookup.js.map +1 -0
- package/dist/investigation-tools/asx-calendar-fetcher.d.ts +42 -0
- package/dist/investigation-tools/asx-calendar-fetcher.d.ts.map +1 -0
- package/dist/investigation-tools/asx-calendar-fetcher.js +271 -0
- package/dist/investigation-tools/asx-calendar-fetcher.js.map +1 -0
- package/dist/investigation-tools/asx-parser.d.ts +66 -0
- package/dist/investigation-tools/asx-parser.d.ts.map +1 -0
- package/dist/investigation-tools/asx-parser.js +314 -0
- package/dist/investigation-tools/asx-parser.js.map +1 -0
- package/dist/investigation-tools/bulk-asx-announcements.d.ts +53 -0
- package/dist/investigation-tools/bulk-asx-announcements.d.ts.map +1 -0
- package/dist/investigation-tools/bulk-asx-announcements.js +204 -0
- package/dist/investigation-tools/bulk-asx-announcements.js.map +1 -0
- package/dist/investigation-tools/entity-resolver.d.ts +77 -0
- package/dist/investigation-tools/entity-resolver.d.ts.map +1 -0
- package/dist/investigation-tools/entity-resolver.js +346 -0
- package/dist/investigation-tools/entity-resolver.js.map +1 -0
- package/dist/investigation-tools/hotcopper-scraper.d.ts +73 -0
- package/dist/investigation-tools/hotcopper-scraper.d.ts.map +1 -0
- package/dist/investigation-tools/hotcopper-scraper.js +318 -0
- package/dist/investigation-tools/hotcopper-scraper.js.map +1 -0
- package/dist/investigation-tools/index.d.ts +15 -0
- package/dist/investigation-tools/index.d.ts.map +1 -0
- package/dist/investigation-tools/index.js +15 -0
- package/dist/investigation-tools/index.js.map +1 -0
- package/dist/investigation-tools/insider-graph.d.ts +173 -0
- package/dist/investigation-tools/insider-graph.d.ts.map +1 -0
- package/dist/investigation-tools/insider-graph.js +732 -0
- package/dist/investigation-tools/insider-graph.js.map +1 -0
- package/dist/investigation-tools/insider-suspicion-scorer.d.ts +97 -0
- package/dist/investigation-tools/insider-suspicion-scorer.d.ts.map +1 -0
- package/dist/investigation-tools/insider-suspicion-scorer.js +327 -0
- package/dist/investigation-tools/insider-suspicion-scorer.js.map +1 -0
- package/dist/investigation-tools/multi-forum-scraper.d.ts +104 -0
- package/dist/investigation-tools/multi-forum-scraper.d.ts.map +1 -0
- package/dist/investigation-tools/multi-forum-scraper.js +415 -0
- package/dist/investigation-tools/multi-forum-scraper.js.map +1 -0
- package/dist/investigation-tools/price-fetcher.d.ts +81 -0
- package/dist/investigation-tools/price-fetcher.d.ts.map +1 -0
- package/dist/investigation-tools/price-fetcher.js +268 -0
- package/dist/investigation-tools/price-fetcher.js.map +1 -0
- package/dist/investigation-tools/shared.d.ts +39 -0
- package/dist/investigation-tools/shared.d.ts.map +1 -0
- package/dist/investigation-tools/shared.js +203 -0
- package/dist/investigation-tools/shared.js.map +1 -0
- package/dist/investigation-tools/timeline-linker.d.ts +90 -0
- package/dist/investigation-tools/timeline-linker.d.ts.map +1 -0
- package/dist/investigation-tools/timeline-linker.js +219 -0
- package/dist/investigation-tools/timeline-linker.js.map +1 -0
- package/dist/investigation-tools/volume-scanner.d.ts +70 -0
- package/dist/investigation-tools/volume-scanner.d.ts.map +1 -0
- package/dist/investigation-tools/volume-scanner.js +227 -0
- package/dist/investigation-tools/volume-scanner.js.map +1 -0
- package/dist/model.d.ts +136 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +1071 -0
- package/dist/model.js.map +1 -0
- package/dist/patching.d.ts +45 -0
- package/dist/patching.d.ts.map +1 -0
- package/dist/patching.js +317 -0
- package/dist/patching.js.map +1 -0
- package/dist/prompts.d.ts +15 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +351 -0
- package/dist/prompts.js.map +1 -0
- package/dist/replay-log.d.ts +54 -0
- package/dist/replay-log.d.ts.map +1 -0
- package/dist/replay-log.js +94 -0
- package/dist/replay-log.js.map +1 -0
- package/dist/runtime.d.ts +53 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +259 -0
- package/dist/runtime.js.map +1 -0
- package/dist/settings.d.ts +39 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +146 -0
- package/dist/settings.js.map +1 -0
- package/dist/tool-defs.d.ts +58 -0
- package/dist/tool-defs.d.ts.map +1 -0
- package/dist/tool-defs.js +1029 -0
- package/dist/tool-defs.js.map +1 -0
- package/dist/tools.d.ts +72 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +1454 -0
- package/dist/tools.js.map +1 -0
- package/dist/tui.d.ts +49 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +699 -0
- package/dist/tui.js.map +1 -0
- package/package.json +126 -0
package/dist/engine.js
ADDED
|
@@ -0,0 +1,1036 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core RLMEngine – orchestrates the recursive agent loop.
|
|
3
|
+
*
|
|
4
|
+
* Migrated from agent/engine.py to TypeScript (ESM, ES2022, Node16).
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { ModelError } from "./model.js";
|
|
9
|
+
import { buildSystemPrompt } from "./prompts.js";
|
|
10
|
+
import { get_tool_definitions } from "./tool-defs.js";
|
|
11
|
+
import { WorkspaceTools } from "./tools.js";
|
|
12
|
+
function asRecord(obj) {
|
|
13
|
+
return obj;
|
|
14
|
+
}
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Helper functions
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
/** One-line summary of tool call arguments. */
|
|
19
|
+
export function summarizeArgs(args, maxLen = 120) {
|
|
20
|
+
const parts = [];
|
|
21
|
+
for (const [k, v] of Object.entries(args)) {
|
|
22
|
+
let s = String(v);
|
|
23
|
+
if (s.length > 60) {
|
|
24
|
+
s = s.slice(0, 57) + "...";
|
|
25
|
+
}
|
|
26
|
+
parts.push(`${k}=${s}`);
|
|
27
|
+
}
|
|
28
|
+
let joined = parts.join(", ");
|
|
29
|
+
if (joined.length > maxLen) {
|
|
30
|
+
joined = joined.slice(0, maxLen - 3) + "...";
|
|
31
|
+
}
|
|
32
|
+
return joined;
|
|
33
|
+
}
|
|
34
|
+
/** First line or truncated preview of an observation. */
|
|
35
|
+
export function summarizeObservation(text, maxLen = 200) {
|
|
36
|
+
let first = text.split("\n", 1)[0].trim();
|
|
37
|
+
if (first.length > maxLen) {
|
|
38
|
+
first = first.slice(0, maxLen - 3) + "...";
|
|
39
|
+
}
|
|
40
|
+
const lines = (text.match(/\n/g) ?? []).length + 1;
|
|
41
|
+
const chars = text.length;
|
|
42
|
+
if (lines > 1) {
|
|
43
|
+
return `${first} (${lines} lines, ${chars} chars)`;
|
|
44
|
+
}
|
|
45
|
+
return first;
|
|
46
|
+
}
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Legacy alias
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
/** Legacy alias for tests and external code that reference SYSTEM_PROMPT directly. */
|
|
51
|
+
export const SYSTEM_PROMPT = buildSystemPrompt(true);
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Context window constants
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
export const _MODEL_CONTEXT_WINDOWS = {
|
|
56
|
+
"claude-opus-4-6": 200_000,
|
|
57
|
+
"claude-sonnet-4-5-20250929": 200_000,
|
|
58
|
+
"claude-haiku-4-5-20251001": 200_000,
|
|
59
|
+
"gpt-4o": 128_000,
|
|
60
|
+
"gpt-4.1": 1_000_000,
|
|
61
|
+
"gpt-5-turbo-16k": 16_000,
|
|
62
|
+
};
|
|
63
|
+
export const _DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
64
|
+
const _CONDENSATION_THRESHOLD = 0.75;
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Model tier helpers
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
/**
|
|
69
|
+
* Determine capability tier for a model. Lower number = higher capability.
|
|
70
|
+
*
|
|
71
|
+
* Anthropic chain (by model name):
|
|
72
|
+
* opus → 1, sonnet → 2, haiku → 3
|
|
73
|
+
* OpenAI codex chain (by reasoning effort):
|
|
74
|
+
* xhigh → 1, high → 2, medium → 3, low → 4
|
|
75
|
+
* Unknown → 2
|
|
76
|
+
*/
|
|
77
|
+
export function modelTier(modelName, reasoningEffort = null) {
|
|
78
|
+
const lower = modelName.toLowerCase();
|
|
79
|
+
if (lower.includes("opus"))
|
|
80
|
+
return 1;
|
|
81
|
+
if (lower.includes("sonnet"))
|
|
82
|
+
return 2;
|
|
83
|
+
if (lower.includes("haiku"))
|
|
84
|
+
return 3;
|
|
85
|
+
if (lower.startsWith("gpt-5") && lower.includes("codex")) {
|
|
86
|
+
const effort = (reasoningEffort ?? "").toLowerCase();
|
|
87
|
+
const map = {
|
|
88
|
+
xhigh: 1,
|
|
89
|
+
high: 2,
|
|
90
|
+
medium: 3,
|
|
91
|
+
low: 4,
|
|
92
|
+
};
|
|
93
|
+
return map[effort] ?? 2;
|
|
94
|
+
}
|
|
95
|
+
return 2;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Return [modelName, reasoningEffort] for the lowest-tier executor.
|
|
99
|
+
*
|
|
100
|
+
* Anthropic models → haiku. Unknown → no downgrade (return same name).
|
|
101
|
+
*/
|
|
102
|
+
export function lowestTierModel(modelName) {
|
|
103
|
+
const lower = modelName.toLowerCase();
|
|
104
|
+
if (lower.includes("claude")) {
|
|
105
|
+
return ["claude-haiku-4-5-20251001", null];
|
|
106
|
+
}
|
|
107
|
+
return [modelName, null];
|
|
108
|
+
}
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// ExternalContext
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
export class ExternalContext {
|
|
113
|
+
observations = [];
|
|
114
|
+
add(text) {
|
|
115
|
+
this.observations.push(text);
|
|
116
|
+
}
|
|
117
|
+
summary(maxItems = 12, maxChars = 8000) {
|
|
118
|
+
if (this.observations.length === 0)
|
|
119
|
+
return "(empty)";
|
|
120
|
+
if (maxItems <= 0)
|
|
121
|
+
return "(empty)";
|
|
122
|
+
const recent = this.observations.slice(-maxItems);
|
|
123
|
+
const joined = recent.join("\n\n");
|
|
124
|
+
if (joined.length <= maxChars)
|
|
125
|
+
return joined;
|
|
126
|
+
return `${joined.slice(0, maxChars)}\n...[truncated external context]...`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// RLMEngine
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
export class RLMEngine {
|
|
133
|
+
model;
|
|
134
|
+
tools;
|
|
135
|
+
config;
|
|
136
|
+
systemPrompt;
|
|
137
|
+
sessionTokens;
|
|
138
|
+
modelFactory;
|
|
139
|
+
sessionDir;
|
|
140
|
+
sessionId;
|
|
141
|
+
_modelCache = new Map();
|
|
142
|
+
_shellCommandCounts = new Map();
|
|
143
|
+
constructor(opts) {
|
|
144
|
+
this.model = opts.model;
|
|
145
|
+
this.tools = opts.tools;
|
|
146
|
+
this.config = opts.config;
|
|
147
|
+
this.systemPrompt = opts.systemPrompt ?? "";
|
|
148
|
+
this.sessionTokens = opts.sessionTokens ?? {};
|
|
149
|
+
this.modelFactory = opts.modelFactory ?? null;
|
|
150
|
+
this.sessionDir = opts.sessionDir ?? null;
|
|
151
|
+
this.sessionId = opts.sessionId ?? null;
|
|
152
|
+
// __post_init__ equivalent
|
|
153
|
+
if (!this.systemPrompt) {
|
|
154
|
+
this.systemPrompt = buildSystemPrompt(this.config.recursive, this.config.acceptanceCriteria, this.config.demo);
|
|
155
|
+
}
|
|
156
|
+
const ac = this.config.acceptanceCriteria;
|
|
157
|
+
const toolDefs = get_tool_definitions({
|
|
158
|
+
includeSubtask: this.config.recursive,
|
|
159
|
+
includeAcceptanceCriteria: ac,
|
|
160
|
+
});
|
|
161
|
+
if ("toolDefs" in this.model) {
|
|
162
|
+
asRecord(this.model).toolDefs = toolDefs;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// -----------------------------------------------------------------------
|
|
166
|
+
// Public API
|
|
167
|
+
// -----------------------------------------------------------------------
|
|
168
|
+
async solve(objective, onEvent) {
|
|
169
|
+
const [result] = await this.solveWithContext({ objective, onEvent });
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
async solveWithContext(opts) {
|
|
173
|
+
const { objective, context = null, onEvent = null, onStep = null, onContentDelta = null, replayLogger = null, } = opts;
|
|
174
|
+
if (!objective.trim()) {
|
|
175
|
+
return ["No objective provided.", context ?? new ExternalContext()];
|
|
176
|
+
}
|
|
177
|
+
this._shellCommandCounts.clear();
|
|
178
|
+
const activeContext = context ?? new ExternalContext();
|
|
179
|
+
const deadline = this.config.maxSolveSeconds > 0
|
|
180
|
+
? performance.now() / 1000 + this.config.maxSolveSeconds
|
|
181
|
+
: 0;
|
|
182
|
+
try {
|
|
183
|
+
const result = await this._solveRecursive({
|
|
184
|
+
objective: objective.trim(),
|
|
185
|
+
depth: 0,
|
|
186
|
+
context: activeContext,
|
|
187
|
+
onEvent,
|
|
188
|
+
onStep,
|
|
189
|
+
onContentDelta,
|
|
190
|
+
deadline,
|
|
191
|
+
replayLogger,
|
|
192
|
+
});
|
|
193
|
+
return [result, activeContext];
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
const cleanup = asRecord(this.tools).cleanupBgJobs;
|
|
197
|
+
if (typeof cleanup === "function") {
|
|
198
|
+
cleanup.call(this.tools);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// -----------------------------------------------------------------------
|
|
203
|
+
// Internal helpers
|
|
204
|
+
// -----------------------------------------------------------------------
|
|
205
|
+
_emit(msg, onEvent) {
|
|
206
|
+
if (onEvent) {
|
|
207
|
+
try {
|
|
208
|
+
onEvent(msg);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// swallow
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
_clipObservation(text) {
|
|
216
|
+
if (text.length <= this.config.maxObservationChars)
|
|
217
|
+
return text;
|
|
218
|
+
return (`${text.slice(0, this.config.maxObservationChars)}` +
|
|
219
|
+
`\n...[truncated ${text.length - this.config.maxObservationChars} chars]...`);
|
|
220
|
+
}
|
|
221
|
+
_runtimePolicyCheck(name, args, depth) {
|
|
222
|
+
if (name !== "run_shell")
|
|
223
|
+
return null;
|
|
224
|
+
const command = String(args.command ?? "").trim();
|
|
225
|
+
if (!command)
|
|
226
|
+
return null;
|
|
227
|
+
const key = `${depth}::${command}`;
|
|
228
|
+
const count = (this._shellCommandCounts.get(key) ?? 0) + 1;
|
|
229
|
+
this._shellCommandCounts.set(key, count);
|
|
230
|
+
if (count <= 2)
|
|
231
|
+
return null;
|
|
232
|
+
return ("Blocked by runtime policy: identical run_shell command repeated more than twice " +
|
|
233
|
+
"at the same depth. Change strategy instead of retrying the same command.");
|
|
234
|
+
}
|
|
235
|
+
async _judgeResult(objective, acceptanceCriteria, result, currentModel) {
|
|
236
|
+
if (!this.modelFactory)
|
|
237
|
+
return "PASS\n(no judge available)";
|
|
238
|
+
const cur = currentModel ?? this.model;
|
|
239
|
+
const curName = String(asRecord(cur).model ?? "");
|
|
240
|
+
const [judgeName, judgeEffort] = lowestTierModel(curName);
|
|
241
|
+
const cacheKey = `_judge_${judgeName}::${judgeEffort ?? ""}`;
|
|
242
|
+
if (!this._modelCache.has(cacheKey)) {
|
|
243
|
+
try {
|
|
244
|
+
this._modelCache.set(cacheKey, this.modelFactory(judgeName, judgeEffort));
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return "PASS\n(no judge available)";
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const judgeModel = this._modelCache.get(cacheKey);
|
|
251
|
+
if ("toolDefs" in judgeModel) {
|
|
252
|
+
asRecord(judgeModel).toolDefs = [];
|
|
253
|
+
}
|
|
254
|
+
const truncated = result.length > 4000 ? result.slice(0, 4000) : result;
|
|
255
|
+
const prompt = "You are a judge evaluating whether a task result meets acceptance criteria.\n\n" +
|
|
256
|
+
`Objective: ${objective}\n\n` +
|
|
257
|
+
`Acceptance criteria: ${acceptanceCriteria}\n\n` +
|
|
258
|
+
`Result:\n${truncated}\n\n` +
|
|
259
|
+
"Respond with exactly one line starting with PASS: or FAIL: followed by a brief explanation.";
|
|
260
|
+
try {
|
|
261
|
+
const conversation = judgeModel.createConversation("You are a concise evaluator.", prompt);
|
|
262
|
+
const turn = await judgeModel.complete(conversation);
|
|
263
|
+
const verdict = (turn.text ?? "").trim();
|
|
264
|
+
if (!verdict)
|
|
265
|
+
return "PASS\n(judge returned empty response)";
|
|
266
|
+
return verdict;
|
|
267
|
+
}
|
|
268
|
+
catch (exc) {
|
|
269
|
+
return `PASS\n(judge error: ${exc})`;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// -----------------------------------------------------------------------
|
|
273
|
+
// Core recursive solver
|
|
274
|
+
// -----------------------------------------------------------------------
|
|
275
|
+
async _solveRecursive(opts) {
|
|
276
|
+
const { objective, depth, context, onEvent = null, onStep = null, onContentDelta = null, deadline = 0, modelOverride = null, replayLogger = null, } = opts;
|
|
277
|
+
const model = modelOverride ?? this.model;
|
|
278
|
+
this._emit(`[depth ${depth}] objective: ${objective}`, onEvent);
|
|
279
|
+
const nowIso = new Date().toISOString().replace(/\.\d+Z$/, "Z");
|
|
280
|
+
let initialMsgDict;
|
|
281
|
+
if (depth === 0 && !this.config.recursive) {
|
|
282
|
+
initialMsgDict = {
|
|
283
|
+
timestamp: nowIso,
|
|
284
|
+
objective,
|
|
285
|
+
max_steps_per_call: this.config.maxStepsPerCall,
|
|
286
|
+
workspace: this.config.workspace,
|
|
287
|
+
external_context_summary: context.summary(),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
const replHint = depth === 0
|
|
292
|
+
? "Begin REPL cycle 1: start with a broad READ of the workspace."
|
|
293
|
+
: "Begin REPL cycle 1: parent has surveyed — READ only what this objective requires, then act.";
|
|
294
|
+
initialMsgDict = {
|
|
295
|
+
timestamp: nowIso,
|
|
296
|
+
objective,
|
|
297
|
+
depth,
|
|
298
|
+
max_depth: this.config.maxDepth,
|
|
299
|
+
max_steps_per_call: this.config.maxStepsPerCall,
|
|
300
|
+
workspace: this.config.workspace,
|
|
301
|
+
external_context_summary: context.summary(),
|
|
302
|
+
repl_hint: replHint,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
if (this.sessionDir != null) {
|
|
306
|
+
initialMsgDict.session_dir = this.sessionDir;
|
|
307
|
+
}
|
|
308
|
+
if (this.sessionId != null) {
|
|
309
|
+
initialMsgDict.session_id = this.sessionId;
|
|
310
|
+
}
|
|
311
|
+
const initialMessage = JSON.stringify(initialMsgDict);
|
|
312
|
+
const conversation = model.createConversation(this.systemPrompt, initialMessage);
|
|
313
|
+
if (replayLogger && asRecord(replayLogger)._seq === 0) {
|
|
314
|
+
replayLogger.writeHeader({
|
|
315
|
+
provider: model.constructor.name,
|
|
316
|
+
model: String(asRecord(model).model ?? "(unknown)"),
|
|
317
|
+
baseUrl: String(asRecord(model).baseUrl ?? ""),
|
|
318
|
+
systemPrompt: this.systemPrompt,
|
|
319
|
+
toolDefs: asRecord(model).toolDefs ?? [],
|
|
320
|
+
reasoningEffort: asRecord(model).reasoningEffort ?? null,
|
|
321
|
+
temperature: asRecord(model).temperature ?? null,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
for (let step = 1; step <= this.config.maxStepsPerCall; step++) {
|
|
325
|
+
if (deadline && performance.now() / 1000 > deadline) {
|
|
326
|
+
this._emit(`[d${depth}] wall-clock limit reached`, onEvent);
|
|
327
|
+
return "Time limit exceeded. Try a more focused objective.";
|
|
328
|
+
}
|
|
329
|
+
this._emit(`[d${depth}/s${step}] calling model...`, onEvent);
|
|
330
|
+
const t0 = performance.now() / 1000;
|
|
331
|
+
// Stream thinking/text deltas only for top-level calls
|
|
332
|
+
if (onContentDelta && depth === 0 && "onContentDelta" in model) {
|
|
333
|
+
asRecord(model).onContentDelta = onContentDelta;
|
|
334
|
+
}
|
|
335
|
+
let turn;
|
|
336
|
+
try {
|
|
337
|
+
turn = await model.complete(conversation);
|
|
338
|
+
}
|
|
339
|
+
catch (exc) {
|
|
340
|
+
if (exc instanceof ModelError) {
|
|
341
|
+
this._emit(`[d${depth}/s${step}] model error: ${exc}`, onEvent);
|
|
342
|
+
return `Model error at depth ${depth}, step ${step}: ${exc}`;
|
|
343
|
+
}
|
|
344
|
+
throw exc;
|
|
345
|
+
}
|
|
346
|
+
finally {
|
|
347
|
+
if ("onContentDelta" in model) {
|
|
348
|
+
asRecord(model).onContentDelta = null;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const elapsed = performance.now() / 1000 - t0;
|
|
352
|
+
if (replayLogger) {
|
|
353
|
+
try {
|
|
354
|
+
replayLogger.logCall({
|
|
355
|
+
depth,
|
|
356
|
+
step,
|
|
357
|
+
messages: conversation.getMessages(),
|
|
358
|
+
response: turn.raw_response,
|
|
359
|
+
inputTokens: turn.input_tokens,
|
|
360
|
+
outputTokens: turn.output_tokens,
|
|
361
|
+
elapsedSec: elapsed,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
// OSError equivalent – swallow
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// Accumulate token usage per model
|
|
369
|
+
if (turn.input_tokens || turn.output_tokens) {
|
|
370
|
+
const modelName = String(asRecord(model).model ?? "(unknown)");
|
|
371
|
+
if (!this.sessionTokens[modelName]) {
|
|
372
|
+
this.sessionTokens[modelName] = { input: 0, output: 0 };
|
|
373
|
+
}
|
|
374
|
+
this.sessionTokens[modelName].input += turn.input_tokens;
|
|
375
|
+
this.sessionTokens[modelName].output += turn.output_tokens;
|
|
376
|
+
}
|
|
377
|
+
model.appendAssistantTurn(conversation, turn);
|
|
378
|
+
// Context condensation
|
|
379
|
+
if (turn.input_tokens) {
|
|
380
|
+
const modelName = String(asRecord(model).model ?? "(unknown)");
|
|
381
|
+
const contextWindow = _MODEL_CONTEXT_WINDOWS[modelName] ?? _DEFAULT_CONTEXT_WINDOW;
|
|
382
|
+
if (turn.input_tokens > _CONDENSATION_THRESHOLD * contextWindow) {
|
|
383
|
+
const condenseFn = asRecord(model).condenseConversation;
|
|
384
|
+
if (typeof condenseFn === "function") {
|
|
385
|
+
condenseFn.call(model, conversation);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (onStep) {
|
|
390
|
+
try {
|
|
391
|
+
onStep({
|
|
392
|
+
depth,
|
|
393
|
+
step,
|
|
394
|
+
objective,
|
|
395
|
+
action: { name: "_model_turn" },
|
|
396
|
+
observation: "",
|
|
397
|
+
model_text: turn.text ?? "",
|
|
398
|
+
tool_call_names: turn.tool_calls.map((tc) => tc.name),
|
|
399
|
+
input_tokens: turn.input_tokens,
|
|
400
|
+
output_tokens: turn.output_tokens,
|
|
401
|
+
elapsed_sec: Math.round(elapsed * 100) / 100,
|
|
402
|
+
is_final: false,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
// swallow
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// No tool calls + text present = final answer
|
|
410
|
+
if (turn.tool_calls.length === 0 && turn.text) {
|
|
411
|
+
const preview = turn.text.length > 200
|
|
412
|
+
? turn.text.slice(0, 200) + "..."
|
|
413
|
+
: turn.text;
|
|
414
|
+
this._emit(`[d${depth}/s${step}] final answer (${turn.text.length} chars, ${elapsed.toFixed(1)}s): ${preview}`, onEvent);
|
|
415
|
+
if (onStep) {
|
|
416
|
+
try {
|
|
417
|
+
onStep({
|
|
418
|
+
depth,
|
|
419
|
+
step,
|
|
420
|
+
objective,
|
|
421
|
+
action: { name: "final", arguments: { text: turn.text } },
|
|
422
|
+
observation: turn.text,
|
|
423
|
+
is_final: true,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
// swallow
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return turn.text;
|
|
431
|
+
}
|
|
432
|
+
// No tool calls and no text = unexpected empty response
|
|
433
|
+
if (turn.tool_calls.length === 0) {
|
|
434
|
+
this._emit(`[d${depth}/s${step}] empty model response (${elapsed.toFixed(1)}s), nudging...`, onEvent);
|
|
435
|
+
const emptyResult = {
|
|
436
|
+
tool_call_id: "empty",
|
|
437
|
+
name: "system",
|
|
438
|
+
content: "No tool calls and no text in response. Please use a tool or provide a final answer.",
|
|
439
|
+
is_error: false,
|
|
440
|
+
};
|
|
441
|
+
model.appendToolResults(conversation, [emptyResult]);
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
// Log tool calls from model
|
|
445
|
+
const tcNames = turn.tool_calls.map((tc) => tc.name);
|
|
446
|
+
this._emit(`[d${depth}/s${step}] model returned ${turn.tool_calls.length} tool call(s) (${elapsed.toFixed(1)}s): ${tcNames.join(", ")}`, onEvent);
|
|
447
|
+
if (turn.text) {
|
|
448
|
+
this._emit(`[d${depth}/s${step}] model text: ${turn.text.slice(0, 200)}`, onEvent);
|
|
449
|
+
}
|
|
450
|
+
// Execute all tool calls — parallel for subtask/execute, sequential for others.
|
|
451
|
+
const results = [];
|
|
452
|
+
let finalAnswer = null;
|
|
453
|
+
const PARALLEL_TOOLS = new Set(["subtask", "execute"]);
|
|
454
|
+
const sequential = [];
|
|
455
|
+
let parallel = [];
|
|
456
|
+
for (let i = 0; i < turn.tool_calls.length; i++) {
|
|
457
|
+
const tc = turn.tool_calls[i];
|
|
458
|
+
if (PARALLEL_TOOLS.has(tc.name)) {
|
|
459
|
+
parallel.push([i, tc]);
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
sequential.push([i, tc]);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
// If no factory and we have execute calls, fall back to sequential.
|
|
466
|
+
if (!this.modelFactory &&
|
|
467
|
+
parallel.some(([, tc]) => tc.name === "execute")) {
|
|
468
|
+
sequential.length = 0;
|
|
469
|
+
parallel = [];
|
|
470
|
+
for (let i = 0; i < turn.tool_calls.length; i++) {
|
|
471
|
+
sequential.push([i, turn.tool_calls[i]]);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const indexedResults = new Map();
|
|
475
|
+
for (const [idx, tc] of sequential) {
|
|
476
|
+
const [resultEntry, isFinalEntry] = await this._runOneTool({
|
|
477
|
+
tc,
|
|
478
|
+
depth,
|
|
479
|
+
step,
|
|
480
|
+
objective,
|
|
481
|
+
context,
|
|
482
|
+
onEvent,
|
|
483
|
+
onStep,
|
|
484
|
+
deadline,
|
|
485
|
+
currentModel: model,
|
|
486
|
+
replayLogger,
|
|
487
|
+
});
|
|
488
|
+
indexedResults.set(idx, [resultEntry, isFinalEntry]);
|
|
489
|
+
if (isFinalEntry) {
|
|
490
|
+
finalAnswer = resultEntry.content;
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (parallel.length > 0 && finalAnswer === null) {
|
|
495
|
+
const groupId = `d${depth}-s${step}-${Date.now()}`;
|
|
496
|
+
const beginGroup = asRecord(this.tools).beginParallelWriteGroup;
|
|
497
|
+
const endGroup = asRecord(this.tools).endParallelWriteGroup;
|
|
498
|
+
if (typeof beginGroup === "function") {
|
|
499
|
+
beginGroup.call(this.tools, groupId);
|
|
500
|
+
}
|
|
501
|
+
try {
|
|
502
|
+
const parallelPromises = parallel.map(async ([idx, tc]) => {
|
|
503
|
+
const [resultEntry, isFinalEntry] = await this._runOneTool({
|
|
504
|
+
tc,
|
|
505
|
+
depth,
|
|
506
|
+
step,
|
|
507
|
+
objective,
|
|
508
|
+
context,
|
|
509
|
+
onEvent,
|
|
510
|
+
onStep,
|
|
511
|
+
deadline,
|
|
512
|
+
currentModel: model,
|
|
513
|
+
replayLogger,
|
|
514
|
+
parallelGroupId: groupId,
|
|
515
|
+
parallelOwner: `${tc.id ?? "tc"}:${idx}`,
|
|
516
|
+
});
|
|
517
|
+
indexedResults.set(idx, [resultEntry, isFinalEntry]);
|
|
518
|
+
});
|
|
519
|
+
await Promise.all(parallelPromises);
|
|
520
|
+
}
|
|
521
|
+
finally {
|
|
522
|
+
if (typeof endGroup === "function") {
|
|
523
|
+
endGroup.call(this.tools, groupId);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const sortedIndices = Array.from(indexedResults.keys()).sort((a, b) => a - b);
|
|
528
|
+
for (const i of sortedIndices) {
|
|
529
|
+
const [r, isFinalEntry] = indexedResults.get(i);
|
|
530
|
+
results.push(r);
|
|
531
|
+
if (isFinalEntry && finalAnswer === null) {
|
|
532
|
+
finalAnswer = r.content;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
// Timestamp + step budget + context usage awareness
|
|
536
|
+
if (finalAnswer === null && results.length > 0) {
|
|
537
|
+
const budgetTotal = this.config.maxStepsPerCall;
|
|
538
|
+
const remaining = budgetTotal - step;
|
|
539
|
+
const tsTag = `[${new Date().toISOString().replace(/\.\d+Z$/, "Z")}]`;
|
|
540
|
+
const budgetTag = `[Step ${step}/${budgetTotal}]`;
|
|
541
|
+
const _mname = String(asRecord(model).model ?? "(unknown)");
|
|
542
|
+
const _ctxWindow = _MODEL_CONTEXT_WINDOWS[_mname] ?? _DEFAULT_CONTEXT_WINDOW;
|
|
543
|
+
const ctxTag = `[Context ${turn.input_tokens}/${_ctxWindow} tokens]`;
|
|
544
|
+
const r0 = results[0];
|
|
545
|
+
results[0] = {
|
|
546
|
+
tool_call_id: r0.tool_call_id,
|
|
547
|
+
name: r0.name,
|
|
548
|
+
content: `${tsTag} ${budgetTag} ${ctxTag} ${r0.content}`,
|
|
549
|
+
is_error: r0.is_error,
|
|
550
|
+
};
|
|
551
|
+
if (remaining > 0 && remaining <= Math.floor(budgetTotal / 4)) {
|
|
552
|
+
const warning = `\n\n** BUDGET CRITICAL: ${remaining} of ${budgetTotal} steps remain. ` +
|
|
553
|
+
"Stop exploring/surveying. Write your output files NOW with your best answer. " +
|
|
554
|
+
"A partial result beats no result.";
|
|
555
|
+
const rl = results[results.length - 1];
|
|
556
|
+
results[results.length - 1] = {
|
|
557
|
+
tool_call_id: rl.tool_call_id,
|
|
558
|
+
name: rl.name,
|
|
559
|
+
content: rl.content + warning,
|
|
560
|
+
is_error: rl.is_error,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
else if (remaining <= Math.floor(budgetTotal / 2)) {
|
|
564
|
+
const warning = `\n\n** BUDGET WARNING: ${remaining} of ${budgetTotal} steps remain. ` +
|
|
565
|
+
"Focus on completing the task directly. Do not write exploration scripts.";
|
|
566
|
+
const rl = results[results.length - 1];
|
|
567
|
+
results[results.length - 1] = {
|
|
568
|
+
tool_call_id: rl.tool_call_id,
|
|
569
|
+
name: rl.name,
|
|
570
|
+
content: rl.content + warning,
|
|
571
|
+
is_error: rl.is_error,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
// Plan injection — find newest *.plan.md in session dir, append to last result
|
|
576
|
+
if (this.sessionDir != null &&
|
|
577
|
+
results.length > 0 &&
|
|
578
|
+
finalAnswer === null) {
|
|
579
|
+
try {
|
|
580
|
+
const dir = this.sessionDir;
|
|
581
|
+
if (fs.existsSync(dir)) {
|
|
582
|
+
const planFiles = fs
|
|
583
|
+
.readdirSync(dir)
|
|
584
|
+
.filter((f) => f.endsWith(".plan.md"))
|
|
585
|
+
.map((f) => ({
|
|
586
|
+
name: f,
|
|
587
|
+
mtime: fs.statSync(path.join(dir, f)).mtimeMs,
|
|
588
|
+
}))
|
|
589
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
590
|
+
if (planFiles.length > 0) {
|
|
591
|
+
const planPath = path.join(dir, planFiles[0].name);
|
|
592
|
+
let planText = fs.readFileSync(planPath, "utf-8");
|
|
593
|
+
if (planText.trim()) {
|
|
594
|
+
const maxPc = this.config.maxPlanChars;
|
|
595
|
+
if (planText.length > maxPc) {
|
|
596
|
+
planText =
|
|
597
|
+
planText.slice(0, maxPc) + "\n...[plan truncated]...";
|
|
598
|
+
}
|
|
599
|
+
const planBlock = `\n[SESSION PLAN file=${planFiles[0].name}]\n` +
|
|
600
|
+
`${planText}\n[/SESSION PLAN]\n`;
|
|
601
|
+
const rl = results[results.length - 1];
|
|
602
|
+
results[results.length - 1] = {
|
|
603
|
+
tool_call_id: rl.tool_call_id,
|
|
604
|
+
name: rl.name,
|
|
605
|
+
content: rl.content + planBlock,
|
|
606
|
+
is_error: rl.is_error,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
catch {
|
|
613
|
+
// OSError equivalent – swallow
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
model.appendToolResults(conversation, results);
|
|
617
|
+
if (finalAnswer !== null) {
|
|
618
|
+
this._emit(`[d${depth}] completed in ${step} step(s)`, onEvent);
|
|
619
|
+
return finalAnswer;
|
|
620
|
+
}
|
|
621
|
+
for (const r of results) {
|
|
622
|
+
context.add(`[depth ${depth} step ${step}]\n${r.content}`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return (`Step budget exhausted at depth ${depth} for objective: ${objective}\n` +
|
|
626
|
+
"Please try with a more specific task, higher step budget, or deeper recursion.");
|
|
627
|
+
}
|
|
628
|
+
// -----------------------------------------------------------------------
|
|
629
|
+
// Tool execution
|
|
630
|
+
// -----------------------------------------------------------------------
|
|
631
|
+
async _runOneTool(opts) {
|
|
632
|
+
const { tc, depth, step, objective, context, onEvent, onStep, deadline, currentModel, replayLogger, parallelGroupId = null, parallelOwner = null, } = opts;
|
|
633
|
+
const argSummary = summarizeArgs(tc.arguments);
|
|
634
|
+
this._emit(`[d${depth}/s${step}] ${tc.name}(${argSummary})`, onEvent);
|
|
635
|
+
const t1 = performance.now() / 1000;
|
|
636
|
+
let observation;
|
|
637
|
+
let isFinal;
|
|
638
|
+
const doApply = async () => {
|
|
639
|
+
try {
|
|
640
|
+
return await this._applyToolCall({
|
|
641
|
+
toolCall: tc,
|
|
642
|
+
depth,
|
|
643
|
+
context,
|
|
644
|
+
onEvent,
|
|
645
|
+
onStep,
|
|
646
|
+
deadline,
|
|
647
|
+
currentModel,
|
|
648
|
+
replayLogger,
|
|
649
|
+
step,
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
catch (exc) {
|
|
653
|
+
const msg = `Tool ${tc.name} crashed: ${exc instanceof Error ? `${exc.constructor.name}: ${exc.message}` : String(exc)}`;
|
|
654
|
+
return [false, msg];
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
// Execution scope handling
|
|
658
|
+
if (parallelGroupId && parallelOwner) {
|
|
659
|
+
[isFinal, observation] = await this.tools.executionScopeAsync(parallelGroupId, parallelOwner, doApply);
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
[isFinal, observation] = await doApply();
|
|
663
|
+
}
|
|
664
|
+
observation = this._clipObservation(observation);
|
|
665
|
+
const toolElapsed = performance.now() / 1000 - t1;
|
|
666
|
+
const obsSummary = summarizeObservation(observation);
|
|
667
|
+
this._emit(`[d${depth}/s${step}] -> ${obsSummary} (${toolElapsed.toFixed(1)}s)`, onEvent);
|
|
668
|
+
if (onStep) {
|
|
669
|
+
try {
|
|
670
|
+
onStep({
|
|
671
|
+
depth,
|
|
672
|
+
step,
|
|
673
|
+
objective,
|
|
674
|
+
action: { name: tc.name, arguments: tc.arguments },
|
|
675
|
+
observation,
|
|
676
|
+
elapsed_sec: Math.round(toolElapsed * 100) / 100,
|
|
677
|
+
is_final: isFinal,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
catch {
|
|
681
|
+
// swallow
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return [
|
|
685
|
+
{
|
|
686
|
+
tool_call_id: tc.id,
|
|
687
|
+
name: tc.name,
|
|
688
|
+
content: observation,
|
|
689
|
+
is_error: false,
|
|
690
|
+
},
|
|
691
|
+
isFinal,
|
|
692
|
+
];
|
|
693
|
+
}
|
|
694
|
+
// -----------------------------------------------------------------------
|
|
695
|
+
// Tool dispatch
|
|
696
|
+
// -----------------------------------------------------------------------
|
|
697
|
+
async _applyToolCall(opts) {
|
|
698
|
+
const { toolCall, depth, context, onEvent, onStep, deadline = 0, currentModel = null, replayLogger = null, step = 0, } = opts;
|
|
699
|
+
const name = toolCall.name;
|
|
700
|
+
const args = toolCall.arguments;
|
|
701
|
+
const policyError = this._runtimePolicyCheck(name, args, depth);
|
|
702
|
+
if (policyError)
|
|
703
|
+
return [false, policyError];
|
|
704
|
+
// ----- think -----
|
|
705
|
+
if (name === "think") {
|
|
706
|
+
const note = String(args.note ?? "");
|
|
707
|
+
return [false, `Thought noted: ${note}`];
|
|
708
|
+
}
|
|
709
|
+
// ----- list_files -----
|
|
710
|
+
if (name === "list_files") {
|
|
711
|
+
const glob = args.glob != null ? String(args.glob) : undefined;
|
|
712
|
+
return [false, await this.tools.listFiles(glob)];
|
|
713
|
+
}
|
|
714
|
+
// ----- search_files -----
|
|
715
|
+
if (name === "search_files") {
|
|
716
|
+
const query = String(args.query ?? "").trim();
|
|
717
|
+
const glob = args.glob != null ? String(args.glob) : undefined;
|
|
718
|
+
if (!query)
|
|
719
|
+
return [false, "search_files requires non-empty query"];
|
|
720
|
+
return [false, await this.tools.searchFiles(query, glob)];
|
|
721
|
+
}
|
|
722
|
+
// ----- repo_map -----
|
|
723
|
+
if (name === "repo_map") {
|
|
724
|
+
const glob = args.glob != null ? String(args.glob) : undefined;
|
|
725
|
+
const rawMaxFiles = args.max_files ?? 200;
|
|
726
|
+
const maxFiles = typeof rawMaxFiles === "number" ? rawMaxFiles : 200;
|
|
727
|
+
return [false, await this.tools.repoMap(glob, maxFiles)];
|
|
728
|
+
}
|
|
729
|
+
// ----- web_search -----
|
|
730
|
+
if (name === "web_search") {
|
|
731
|
+
const query = String(args.query ?? "").trim();
|
|
732
|
+
if (!query)
|
|
733
|
+
return [false, "web_search requires non-empty query"];
|
|
734
|
+
const rawNumResults = args.num_results ?? 10;
|
|
735
|
+
const numResults = typeof rawNumResults === "number" ? rawNumResults : 10;
|
|
736
|
+
const rawIncludeText = args.include_text ?? false;
|
|
737
|
+
const includeText = typeof rawIncludeText === "boolean" ? rawIncludeText : false;
|
|
738
|
+
return [
|
|
739
|
+
false,
|
|
740
|
+
await this.tools.webSearch(query, numResults, includeText),
|
|
741
|
+
];
|
|
742
|
+
}
|
|
743
|
+
// ----- fetch_url -----
|
|
744
|
+
if (name === "fetch_url") {
|
|
745
|
+
const urls = args.urls;
|
|
746
|
+
if (!Array.isArray(urls))
|
|
747
|
+
return [false, "fetch_url requires a list of URL strings"];
|
|
748
|
+
return [
|
|
749
|
+
false,
|
|
750
|
+
await this.tools.fetchUrl(urls.filter((u) => typeof u === "string")),
|
|
751
|
+
];
|
|
752
|
+
}
|
|
753
|
+
// ----- read_file -----
|
|
754
|
+
if (name === "read_file") {
|
|
755
|
+
const filePath = String(args.path ?? "").trim();
|
|
756
|
+
if (!filePath)
|
|
757
|
+
return [false, "read_file requires path"];
|
|
758
|
+
const hashline = args.hashline != null ? args.hashline : true;
|
|
759
|
+
return [false, await this.tools.readFile(filePath, Boolean(hashline))];
|
|
760
|
+
}
|
|
761
|
+
// ----- write_file -----
|
|
762
|
+
if (name === "write_file") {
|
|
763
|
+
const filePath = String(args.path ?? "").trim();
|
|
764
|
+
if (!filePath)
|
|
765
|
+
return [false, "write_file requires path"];
|
|
766
|
+
const content = String(args.content ?? "");
|
|
767
|
+
return [false, await this.tools.writeFile(filePath, content)];
|
|
768
|
+
}
|
|
769
|
+
// ----- apply_patch -----
|
|
770
|
+
if (name === "apply_patch") {
|
|
771
|
+
const patch = String(args.patch ?? "");
|
|
772
|
+
if (!patch.trim())
|
|
773
|
+
return [false, "apply_patch requires non-empty patch"];
|
|
774
|
+
return [false, await this.tools.applyPatch(patch)];
|
|
775
|
+
}
|
|
776
|
+
// ----- edit_file -----
|
|
777
|
+
if (name === "edit_file") {
|
|
778
|
+
const filePath = String(args.path ?? "").trim();
|
|
779
|
+
if (!filePath)
|
|
780
|
+
return [false, "edit_file requires path"];
|
|
781
|
+
const oldText = String(args.old_text ?? "");
|
|
782
|
+
const newText = String(args.new_text ?? "");
|
|
783
|
+
if (!oldText)
|
|
784
|
+
return [false, "edit_file requires old_text"];
|
|
785
|
+
return [false, await this.tools.editFile(filePath, oldText, newText)];
|
|
786
|
+
}
|
|
787
|
+
// ----- hashline_edit -----
|
|
788
|
+
if (name === "hashline_edit") {
|
|
789
|
+
const filePath = String(args.path ?? "").trim();
|
|
790
|
+
if (!filePath)
|
|
791
|
+
return [false, "hashline_edit requires path"];
|
|
792
|
+
const edits = args.edits;
|
|
793
|
+
if (!Array.isArray(edits))
|
|
794
|
+
return [false, "hashline_edit requires edits array"];
|
|
795
|
+
return [false, await this.tools.hashlineEdit(filePath, edits)];
|
|
796
|
+
}
|
|
797
|
+
// ----- run_shell -----
|
|
798
|
+
if (name === "run_shell") {
|
|
799
|
+
const command = String(args.command ?? "").trim();
|
|
800
|
+
if (!command)
|
|
801
|
+
return [false, "run_shell requires command"];
|
|
802
|
+
const rawTimeout = args.timeout;
|
|
803
|
+
const timeout = rawTimeout != null ? Number(rawTimeout) : undefined;
|
|
804
|
+
return [false, await this.tools.runShell(command, timeout)];
|
|
805
|
+
}
|
|
806
|
+
// ----- run_shell_bg -----
|
|
807
|
+
if (name === "run_shell_bg") {
|
|
808
|
+
const command = String(args.command ?? "").trim();
|
|
809
|
+
if (!command)
|
|
810
|
+
return [false, "run_shell_bg requires command"];
|
|
811
|
+
return [false, this.tools.runShellBg(command)];
|
|
812
|
+
}
|
|
813
|
+
// ----- check_shell_bg -----
|
|
814
|
+
if (name === "check_shell_bg") {
|
|
815
|
+
const rawId = args.job_id;
|
|
816
|
+
if (rawId == null)
|
|
817
|
+
return [false, "check_shell_bg requires job_id"];
|
|
818
|
+
return [false, this.tools.checkShellBg(Number(rawId))];
|
|
819
|
+
}
|
|
820
|
+
// ----- kill_shell_bg -----
|
|
821
|
+
if (name === "kill_shell_bg") {
|
|
822
|
+
const rawId = args.job_id;
|
|
823
|
+
if (rawId == null)
|
|
824
|
+
return [false, "kill_shell_bg requires job_id"];
|
|
825
|
+
return [false, this.tools.killShellBg(Number(rawId))];
|
|
826
|
+
}
|
|
827
|
+
// ----- subtask -----
|
|
828
|
+
if (name === "subtask") {
|
|
829
|
+
if (!this.config.recursive)
|
|
830
|
+
return [false, "Subtask tool not available in flat mode."];
|
|
831
|
+
if (depth >= this.config.maxDepth)
|
|
832
|
+
return [false, "Max recursion depth reached; cannot run subtask."];
|
|
833
|
+
const subtaskObjective = String(args.objective ?? "").trim();
|
|
834
|
+
if (!subtaskObjective)
|
|
835
|
+
return [false, "subtask requires objective"];
|
|
836
|
+
const criteria = String(args.acceptance_criteria ?? "").trim();
|
|
837
|
+
if (this.config.acceptanceCriteria && !criteria) {
|
|
838
|
+
return [
|
|
839
|
+
false,
|
|
840
|
+
"subtask requires acceptance_criteria when acceptance criteria mode is enabled. " +
|
|
841
|
+
"Provide specific, verifiable criteria for judging the result.",
|
|
842
|
+
];
|
|
843
|
+
}
|
|
844
|
+
// Sub-model routing
|
|
845
|
+
const requestedModelName = args.model;
|
|
846
|
+
const requestedEffort = args.reasoning_effort;
|
|
847
|
+
let subtaskModel = null;
|
|
848
|
+
if ((requestedModelName || requestedEffort) &&
|
|
849
|
+
this.modelFactory) {
|
|
850
|
+
const cur = currentModel ?? this.model;
|
|
851
|
+
const curName = String(asRecord(cur).model ?? "");
|
|
852
|
+
const curEffort = asRecord(cur).reasoningEffort;
|
|
853
|
+
const curTier = modelTier(curName, curEffort);
|
|
854
|
+
const reqName = requestedModelName ?? curName;
|
|
855
|
+
const reqEffort = requestedEffort;
|
|
856
|
+
const reqTier = modelTier(reqName, reqEffort ?? curEffort);
|
|
857
|
+
if (reqTier < curTier) {
|
|
858
|
+
return [
|
|
859
|
+
false,
|
|
860
|
+
`Cannot delegate to higher-tier model ` +
|
|
861
|
+
`(current tier ${curTier}, requested tier ${reqTier}). ` +
|
|
862
|
+
`Use an equal or lower-tier model.`,
|
|
863
|
+
];
|
|
864
|
+
}
|
|
865
|
+
const cacheKey = `${reqName}::${requestedEffort ?? ""}`;
|
|
866
|
+
if (!this._modelCache.has(cacheKey)) {
|
|
867
|
+
this._modelCache.set(cacheKey, this.modelFactory(reqName, requestedEffort ?? null));
|
|
868
|
+
}
|
|
869
|
+
subtaskModel = this._modelCache.get(cacheKey);
|
|
870
|
+
}
|
|
871
|
+
this._emit(`[d${depth}] >> entering subtask: ${subtaskObjective}`, onEvent);
|
|
872
|
+
const childLogger = replayLogger
|
|
873
|
+
? replayLogger.child(depth, step)
|
|
874
|
+
: null;
|
|
875
|
+
const subtaskResult = await this._solveRecursive({
|
|
876
|
+
objective: subtaskObjective,
|
|
877
|
+
depth: depth + 1,
|
|
878
|
+
context,
|
|
879
|
+
onEvent,
|
|
880
|
+
onStep,
|
|
881
|
+
onContentDelta: null,
|
|
882
|
+
deadline,
|
|
883
|
+
modelOverride: subtaskModel,
|
|
884
|
+
replayLogger: childLogger,
|
|
885
|
+
});
|
|
886
|
+
let observation = `Subtask result for '${subtaskObjective}':\n${subtaskResult}`;
|
|
887
|
+
if (criteria && this.config.acceptanceCriteria) {
|
|
888
|
+
const verdict = await this._judgeResult(subtaskObjective, criteria, subtaskResult, currentModel);
|
|
889
|
+
const tag = verdict.startsWith("PASS") ? "PASS" : "FAIL";
|
|
890
|
+
observation += `\n\n[ACCEPTANCE CRITERIA: ${tag}]\n${verdict}`;
|
|
891
|
+
}
|
|
892
|
+
return [false, observation];
|
|
893
|
+
}
|
|
894
|
+
// ----- execute -----
|
|
895
|
+
if (name === "execute") {
|
|
896
|
+
const executeObjective = String(args.objective ?? "").trim();
|
|
897
|
+
if (!executeObjective)
|
|
898
|
+
return [false, "execute requires objective"];
|
|
899
|
+
const criteria = String(args.acceptance_criteria ?? "").trim();
|
|
900
|
+
if (this.config.acceptanceCriteria && !criteria) {
|
|
901
|
+
return [
|
|
902
|
+
false,
|
|
903
|
+
"execute requires acceptance_criteria when acceptance criteria mode is enabled. " +
|
|
904
|
+
"Provide specific, verifiable criteria for judging the result.",
|
|
905
|
+
];
|
|
906
|
+
}
|
|
907
|
+
if (depth >= this.config.maxDepth)
|
|
908
|
+
return [false, "Max recursion depth reached; cannot run execute."];
|
|
909
|
+
// Resolve lowest-tier model for the executor.
|
|
910
|
+
const cur = currentModel ?? this.model;
|
|
911
|
+
const curName = String(asRecord(cur).model ?? "");
|
|
912
|
+
const [execName, execEffort] = lowestTierModel(curName);
|
|
913
|
+
let execModel = null;
|
|
914
|
+
if (this.modelFactory) {
|
|
915
|
+
const cacheKey = `${execName}::${execEffort ?? ""}`;
|
|
916
|
+
if (!this._modelCache.has(cacheKey)) {
|
|
917
|
+
this._modelCache.set(cacheKey, this.modelFactory(execName, execEffort));
|
|
918
|
+
}
|
|
919
|
+
execModel = this._modelCache.get(cacheKey);
|
|
920
|
+
}
|
|
921
|
+
// Give executor full tools (no subtask, no execute).
|
|
922
|
+
let savedDefs = undefined;
|
|
923
|
+
if (execModel && "toolDefs" in execModel) {
|
|
924
|
+
asRecord(execModel).toolDefs = get_tool_definitions({
|
|
925
|
+
includeSubtask: false,
|
|
926
|
+
includeAcceptanceCriteria: this.config.acceptanceCriteria,
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
else if (execModel === null && "toolDefs" in cur) {
|
|
930
|
+
savedDefs = asRecord(cur).toolDefs;
|
|
931
|
+
asRecord(cur).toolDefs = get_tool_definitions({
|
|
932
|
+
includeSubtask: false,
|
|
933
|
+
includeAcceptanceCriteria: this.config.acceptanceCriteria,
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
this._emit(`[d${depth}] >> executing leaf: ${executeObjective}`, onEvent);
|
|
937
|
+
const childLogger = replayLogger
|
|
938
|
+
? replayLogger.child(depth, step)
|
|
939
|
+
: null;
|
|
940
|
+
const execResult = await this._solveRecursive({
|
|
941
|
+
objective: executeObjective,
|
|
942
|
+
depth: depth + 1,
|
|
943
|
+
context,
|
|
944
|
+
onEvent,
|
|
945
|
+
onStep,
|
|
946
|
+
onContentDelta: null,
|
|
947
|
+
deadline,
|
|
948
|
+
modelOverride: execModel,
|
|
949
|
+
replayLogger: childLogger,
|
|
950
|
+
});
|
|
951
|
+
if (savedDefs !== undefined) {
|
|
952
|
+
asRecord(cur).toolDefs = savedDefs;
|
|
953
|
+
}
|
|
954
|
+
let observation = `Execute result for '${executeObjective}':\n${execResult}`;
|
|
955
|
+
if (criteria && this.config.acceptanceCriteria) {
|
|
956
|
+
const verdict = await this._judgeResult(executeObjective, criteria, execResult, currentModel);
|
|
957
|
+
const tag = verdict.startsWith("PASS") ? "PASS" : "FAIL";
|
|
958
|
+
observation += `\n\n[ACCEPTANCE CRITERIA: ${tag}]\n${verdict}`;
|
|
959
|
+
}
|
|
960
|
+
return [false, observation];
|
|
961
|
+
}
|
|
962
|
+
// ----- list_artifacts -----
|
|
963
|
+
if (name === "list_artifacts") {
|
|
964
|
+
return [false, this._listArtifacts()];
|
|
965
|
+
}
|
|
966
|
+
// ----- read_artifact -----
|
|
967
|
+
if (name === "read_artifact") {
|
|
968
|
+
const aid = String(args.artifact_id ?? "").trim();
|
|
969
|
+
if (!aid)
|
|
970
|
+
return [false, "read_artifact requires artifact_id"];
|
|
971
|
+
const offset = Number(args.offset ?? 0) || 0;
|
|
972
|
+
const limit = Number(args.limit ?? 100) || 100;
|
|
973
|
+
return [false, this._readArtifact(aid, offset, limit)];
|
|
974
|
+
}
|
|
975
|
+
// ----- Domain-specific tools (tools/ directory) -----
|
|
976
|
+
if (name in WorkspaceTools._DOMAIN_TOOL_SCRIPTS) {
|
|
977
|
+
return [false, await this.tools.runDomainTool(name, args)];
|
|
978
|
+
}
|
|
979
|
+
return [false, `Unknown action type: ${name}`];
|
|
980
|
+
}
|
|
981
|
+
// -----------------------------------------------------------------------
|
|
982
|
+
// Artifact helpers
|
|
983
|
+
// -----------------------------------------------------------------------
|
|
984
|
+
/** List available artifacts. */
|
|
985
|
+
_listArtifacts() {
|
|
986
|
+
const artifactsDir = path.join(this.config.workspace, ".openplanter_artifacts");
|
|
987
|
+
if (!fs.existsSync(artifactsDir))
|
|
988
|
+
return "No artifacts found.";
|
|
989
|
+
let entries;
|
|
990
|
+
try {
|
|
991
|
+
entries = fs
|
|
992
|
+
.readdirSync(artifactsDir)
|
|
993
|
+
.filter((f) => f.endsWith(".jsonl"))
|
|
994
|
+
.sort();
|
|
995
|
+
}
|
|
996
|
+
catch {
|
|
997
|
+
return "No artifacts found.";
|
|
998
|
+
}
|
|
999
|
+
if (entries.length === 0)
|
|
1000
|
+
return "No artifacts found.";
|
|
1001
|
+
const lines = [];
|
|
1002
|
+
for (const entry of entries) {
|
|
1003
|
+
try {
|
|
1004
|
+
const filePath = path.join(artifactsDir, entry);
|
|
1005
|
+
const firstLine = fs
|
|
1006
|
+
.readFileSync(filePath, "utf-8")
|
|
1007
|
+
.split("\n", 1)[0];
|
|
1008
|
+
const first = JSON.parse(firstLine);
|
|
1009
|
+
const artifactId = String(first.artifact_id ?? entry.replace(/\.jsonl$/, ""));
|
|
1010
|
+
const obj = String(first.objective ?? "(no objective)").slice(0, 120);
|
|
1011
|
+
lines.push(`- ${artifactId}: ${obj}`);
|
|
1012
|
+
}
|
|
1013
|
+
catch {
|
|
1014
|
+
lines.push(`- ${entry.replace(/\.jsonl$/, "")}: (unreadable)`);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
return `Artifacts (${lines.length}):\n${lines.join("\n")}`;
|
|
1018
|
+
}
|
|
1019
|
+
/** Read an artifact's conversation log. */
|
|
1020
|
+
_readArtifact(artifactId, offset = 0, limit = 100) {
|
|
1021
|
+
const artifactsDir = path.join(this.config.workspace, ".openplanter_artifacts");
|
|
1022
|
+
const filePath = path.join(artifactsDir, `${artifactId}.jsonl`);
|
|
1023
|
+
if (!fs.existsSync(filePath))
|
|
1024
|
+
return `Artifact '${artifactId}' not found.`;
|
|
1025
|
+
const allLines = fs.readFileSync(filePath, "utf-8").split("\n");
|
|
1026
|
+
// Remove trailing empty line if any
|
|
1027
|
+
if (allLines.length > 0 && allLines[allLines.length - 1] === "") {
|
|
1028
|
+
allLines.pop();
|
|
1029
|
+
}
|
|
1030
|
+
const total = allLines.length;
|
|
1031
|
+
const selected = allLines.slice(offset, offset + limit);
|
|
1032
|
+
const header = `Artifact ${artifactId} (lines ${offset}-${offset + selected.length} of ${total}):\n`;
|
|
1033
|
+
return header + selected.join("\n");
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
//# sourceMappingURL=engine.js.map
|