@lazyingart/agintiflow 0.8.4 → 0.8.6

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,4 +1,5 @@
1
1
  import readline from "node:readline/promises";
2
+ import { emitKeypressEvents } from "node:readline";
2
3
  import { stdin as input, stdout as output } from "node:process";
3
4
  import { runAgent } from "./agent-runner.js";
4
5
  import { loadConfig } from "./config.js";
@@ -6,8 +7,96 @@ import { initProject, listProjectSessions } from "./project.js";
6
7
  import { normalizePackageInstallPolicy, normalizeSandboxMode } from "./command-policy.js";
7
8
  import { normalizeTaskProfile } from "./task-profiles.js";
8
9
 
10
+ const useColor = Boolean(input.isTTY && output.isTTY && !process.env.NO_COLOR);
11
+ const ansi = {
12
+ reset: "\x1b[0m",
13
+ bold: "\x1b[1m",
14
+ dim: "\x1b[2m",
15
+ cyan: "\x1b[36m",
16
+ green: "\x1b[32m",
17
+ yellow: "\x1b[33m",
18
+ red: "\x1b[31m",
19
+ userBg: "\x1b[48;5;24m\x1b[38;5;231m",
20
+ agentBg: "\x1b[48;5;29m\x1b[38;5;231m",
21
+ systemBg: "\x1b[48;5;236m\x1b[38;5;245m",
22
+ };
23
+
24
+ function color(value, ...codes) {
25
+ if (!useColor || codes.length === 0) return String(value);
26
+ return `${codes.join("")}${value}${ansi.reset}`;
27
+ }
28
+
29
+ function label(name, bgCode) {
30
+ return color(` ${name} `, bgCode, ansi.bold);
31
+ }
32
+
33
+ function userPrompt() {
34
+ return `\n${label("user>", ansi.userBg)} `;
35
+ }
36
+
37
+ function stripMarkdown(text) {
38
+ const lines = String(text || "").split(/\r?\n/);
39
+ let inFence = false;
40
+ const rendered = [];
41
+
42
+ for (const rawLine of lines) {
43
+ let line = rawLine;
44
+ if (/^\s*```/.test(line)) {
45
+ inFence = !inFence;
46
+ if (inFence) rendered.push(color("code", ansi.dim));
47
+ continue;
48
+ }
49
+
50
+ if (!inFence) {
51
+ if (/^\s*[-*_]{3,}\s*$/.test(line)) {
52
+ rendered.push("");
53
+ continue;
54
+ }
55
+ line = line.replace(/^\s{0,3}#{1,6}\s+/, "");
56
+ line = line.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)");
57
+ line = line.replace(/\*\*([^*]+)\*\*/g, (_, value) => color(value, ansi.bold));
58
+ line = line.replace(/__([^_]+)__/g, (_, value) => color(value, ansi.bold));
59
+ line = line.replace(/(^|[^\w])\*([^*\n]+)\*/g, "$1$2");
60
+ line = line.replace(/(^|[^\w])_([^_\n]+)_/g, "$1$2");
61
+ line = line.replace(/`([^`]+)`/g, (_, value) => color(value, ansi.yellow));
62
+ line = line.replace(/^(\s*)[-*+]\s+/, "$1- ");
63
+ line = line.replace(/^\s*>\s?/, " ");
64
+ }
65
+
66
+ rendered.push(line);
67
+ }
68
+
69
+ return rendered.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
70
+ }
71
+
72
+ function printWrapped(prefix, text) {
73
+ const rendered = stripMarkdown(text);
74
+ const lines = rendered.split("\n");
75
+ const gutter = " ".repeat(useColor ? 9 : prefix.length);
76
+ console.log(`${prefix}${lines[0] || ""}`);
77
+ for (const line of lines.slice(1)) {
78
+ console.log(`${gutter}${line}`);
79
+ }
80
+ }
81
+
82
+ function printAgentMessage(text) {
83
+ printWrapped(`${label("aginti>", ansi.agentBg)} `, text);
84
+ }
85
+
86
+ function printSystemLine(text) {
87
+ if (!String(text || "").trim()) {
88
+ console.log("");
89
+ return;
90
+ }
91
+ console.log(`${label("state", ansi.systemBg)} ${color(text, ansi.dim)}`);
92
+ }
93
+
94
+ function printHeading(text) {
95
+ console.log(color(stripMarkdown(text), ansi.bold, ansi.cyan));
96
+ }
97
+
9
98
  function printHelp() {
10
- console.log(
99
+ printAgentMessage(
11
100
  [
12
101
  "Commands:",
13
102
  " /help Show this help.",
@@ -27,23 +116,24 @@ function printHelp() {
27
116
  " /exit Quit.",
28
117
  "",
29
118
  "Type a normal request to run the agent. Example: write a Python CLI app with tests",
119
+ "While a run is active, press Esc or Ctrl+C once to stop gracefully and print a resume command.",
30
120
  ].join("\n")
31
121
  );
32
122
  }
33
123
 
34
124
  function printStatus(state) {
35
- console.log(`project=${process.cwd()}`);
36
- console.log(`cwd=${state.commandCwd || process.cwd()}`);
37
- console.log(`session=${state.sessionId || "new"}`);
38
- console.log(`status=${state.status || "idle"}${state.activeGoal ? ` workingOn=${state.activeGoal}` : ""}`);
39
- if (state.lastEvent) console.log(`last=${state.lastEvent}`);
40
- console.log(`provider=${state.provider || "auto"} routing=${state.routingMode} model=${state.model || "auto"}`);
41
- console.log(`profile=${state.taskProfile} maxSteps=${state.maxSteps}`);
42
- console.log(
125
+ printSystemLine(`project=${process.cwd()}`);
126
+ printSystemLine(`cwd=${state.commandCwd || process.cwd()}`);
127
+ printSystemLine(`session=${state.sessionId || "new"}`);
128
+ printSystemLine(`status=${state.status || "idle"}${state.activeGoal ? ` workingOn=${state.activeGoal}` : ""}`);
129
+ if (state.lastEvent) printSystemLine(`last=${state.lastEvent}`);
130
+ printSystemLine(`provider=${state.provider || "auto"} routing=${state.routingMode} model=${state.model || "auto"}`);
131
+ printSystemLine(`profile=${state.taskProfile} maxSteps=${state.maxSteps}`);
132
+ printSystemLine(
43
133
  `shell=${state.allowShellTool} files=${state.allowFileTools} sandbox=${state.sandboxMode} installs=${state.packageInstallPolicy}`
44
134
  );
45
135
  if (state.sandboxMode !== "host") {
46
- console.log(`dockerWorkspace=/workspace -> ${state.commandCwd || process.cwd()}`);
136
+ printSystemLine(`dockerWorkspace=/workspace -> ${state.commandCwd || process.cwd()}`);
47
137
  }
48
138
  }
49
139
 
@@ -63,26 +153,52 @@ async function latestSession() {
63
153
 
64
154
  function printStatusEvent(state, label, details = "") {
65
155
  state.lastEvent = details ? `${label}: ${details}` : label;
66
- console.log(`status=${state.status || "running"} ${state.lastEvent}`);
156
+ printSystemLine(`status=${state.status || "running"} ${state.lastEvent}`);
157
+ }
158
+
159
+ function attachRunInterrupts(controller) {
160
+ if (!input.isTTY || typeof input.setRawMode !== "function") return () => {};
161
+
162
+ emitKeypressEvents(input);
163
+ const wasRaw = Boolean(input.isRaw);
164
+ input.setRawMode(true);
165
+ const handler = (_str, key = {}) => {
166
+ const isEscape = key.name === "escape";
167
+ const isCtrlC = key.ctrl && key.name === "c";
168
+ if (!isEscape && !isCtrlC) return;
169
+ if (controller.signal.aborted) return;
170
+ const reason = isEscape ? "escape" : "ctrl-c";
171
+ printSystemLine(`status=stopping reason=${reason}`);
172
+ controller.abort(new Error(`Interrupted by ${reason}.`));
173
+ };
174
+ input.on("keypress", handler);
175
+ return () => {
176
+ input.off("keypress", handler);
177
+ if (typeof input.setRawMode === "function") {
178
+ input.setRawMode(wasRaw);
179
+ }
180
+ };
67
181
  }
68
182
 
69
183
  async function printResumeHint(state) {
70
184
  const sessionId = state.sessionId || "";
71
185
  console.log("");
72
186
  if (sessionId) {
73
- console.log("Interrupted. Session saved.");
74
- console.log(`Resume: aginti resume ${sessionId}`);
75
- console.log(`One-shot: aginti resume ${sessionId} "continue"`);
187
+ printAgentMessage(["Interrupted. Session saved.", `Resume: aginti resume ${sessionId}`, `One-shot: aginti resume ${sessionId} "continue"`].join("\n"));
76
188
  } else {
77
- console.log("Interrupted. No active session yet.");
189
+ printAgentMessage("Interrupted. No active session yet.");
78
190
  const sessions = await listProjectSessions(process.cwd(), 5).catch(() => []);
79
191
  if (sessions.length > 0) {
80
- console.log("Recent sessions:");
81
- for (const session of sessions) console.log(` ${formatSessionLine(session)}`);
82
- console.log(`Resume latest: aginti resume ${sessions[0].sessionId}`);
83
- console.log("List all: aginti sessions list");
192
+ printAgentMessage(
193
+ [
194
+ "Recent sessions:",
195
+ ...sessions.map((session) => ` ${formatSessionLine(session)}`),
196
+ `Resume latest: aginti resume ${sessions[0].sessionId}`,
197
+ "List all: aginti sessions list",
198
+ ].join("\n")
199
+ );
84
200
  } else {
85
- console.log("Restart: aginti");
201
+ printAgentMessage("Restart: aginti");
86
202
  }
87
203
  }
88
204
  }
@@ -122,71 +238,75 @@ async function handleCommand(line, state, packageDir) {
122
238
  }
123
239
  if (command === "new") {
124
240
  state.sessionId = "";
125
- console.log("Next message will start a new session.");
241
+ printAgentMessage("Next message will start a new session.");
126
242
  return true;
127
243
  }
128
244
  if (command === "resume") {
129
245
  if (!value || value === "latest") {
130
246
  const latest = await latestSession();
131
247
  if (!latest) {
132
- console.log("No project-local sessions found. Use /new or type a request to start one.");
248
+ printAgentMessage("No project-local sessions found. Use /new or type a request to start one.");
133
249
  } else {
134
250
  state.sessionId = latest.sessionId;
135
- console.log(`Resuming latest ${formatSessionLine(latest)}`);
251
+ printAgentMessage(`Resuming latest ${formatSessionLine(latest)}`);
136
252
  }
137
253
  } else {
138
254
  state.sessionId = value;
139
- console.log(`Resuming ${state.sessionId}`);
255
+ printAgentMessage(`Resuming ${state.sessionId}`);
140
256
  }
141
257
  return true;
142
258
  }
143
259
  if (command === "sessions") {
144
260
  const sessions = await listProjectSessions(process.cwd(), 20);
145
- if (sessions.length === 0) console.log("No project-local sessions found.");
261
+ if (sessions.length === 0) printAgentMessage("No project-local sessions found.");
146
262
  else {
147
- for (const session of sessions) {
148
- const goal = session.goal ? ` ${session.goal.slice(0, 80)}` : "";
149
- console.log(`${session.sessionId} ${session.provider}/${session.model} ${session.updatedAt}${goal}`);
150
- }
263
+ printAgentMessage(
264
+ sessions
265
+ .map((session) => {
266
+ const goal = session.goal ? ` ${session.goal.slice(0, 80)}` : "";
267
+ return `${session.sessionId} ${session.provider}/${session.model} ${session.updatedAt}${goal}`;
268
+ })
269
+ .join("\n")
270
+ );
151
271
  }
152
272
  return true;
153
273
  }
154
274
  if (command === "profile") {
155
275
  state.taskProfile = normalizeTaskProfile(value || "auto");
156
- console.log(`profile=${state.taskProfile}`);
276
+ printSystemLine(`profile=${state.taskProfile}`);
157
277
  return true;
158
278
  }
159
279
  if (command === "routing") {
160
280
  state.routingMode = value || "smart";
161
- console.log(`routing=${state.routingMode}`);
281
+ printSystemLine(`routing=${state.routingMode}`);
162
282
  return true;
163
283
  }
164
284
  if (command === "provider") {
165
285
  state.provider = value === "auto" ? "" : value;
166
- console.log(`provider=${state.provider || "auto"}`);
286
+ printSystemLine(`provider=${state.provider || "auto"}`);
167
287
  return true;
168
288
  }
169
289
  if (command === "model") {
170
290
  state.model = value === "auto" ? "" : value;
171
- console.log(`model=${state.model || "auto"}`);
291
+ printSystemLine(`model=${state.model || "auto"}`);
172
292
  return true;
173
293
  }
174
294
  if (command === "installs") {
175
295
  state.packageInstallPolicy = normalizePackageInstallPolicy(value || "prompt");
176
- console.log(`installs=${state.packageInstallPolicy}`);
296
+ printSystemLine(`installs=${state.packageInstallPolicy}`);
177
297
  return true;
178
298
  }
179
299
  if (command === "docker") {
180
300
  if (value === "on") {
181
301
  state.sandboxMode = "docker-workspace";
182
302
  state.packageInstallPolicy = "allow";
183
- console.log(`docker=on /workspace -> ${state.commandCwd || process.cwd()} installs=allow`);
303
+ printSystemLine(`docker=on /workspace -> ${state.commandCwd || process.cwd()} installs=allow`);
184
304
  } else if (value === "off") {
185
305
  state.sandboxMode = "host";
186
306
  state.packageInstallPolicy = "prompt";
187
- console.log("docker=off sandbox=host installs=prompt");
307
+ printSystemLine("docker=off sandbox=host installs=prompt");
188
308
  } else {
189
- console.log("Usage: /docker on OR /docker off");
309
+ printAgentMessage("Usage: /docker on OR /docker off");
190
310
  }
191
311
  return true;
192
312
  }
@@ -196,36 +316,37 @@ async function handleCommand(line, state, packageDir) {
196
316
  state.sandboxMode = "docker-workspace";
197
317
  state.packageInstallPolicy = "allow";
198
318
  state.maxSteps = Math.max(state.maxSteps, 30);
199
- console.log("latex=on profile=latex sandbox=docker-workspace installs=allow maxSteps=30");
319
+ printSystemLine("latex=on profile=latex sandbox=docker-workspace installs=allow maxSteps=30");
200
320
  } else if (value === "off") {
201
321
  state.taskProfile = "auto";
202
- console.log("latex=off profile=auto");
322
+ printSystemLine("latex=off profile=auto");
203
323
  } else {
204
- console.log("Usage: /latex on OR /latex off");
324
+ printAgentMessage("Usage: /latex on OR /latex off");
205
325
  }
206
326
  return true;
207
327
  }
208
328
  if (command === "cwd") {
209
329
  state.commandCwd = value || process.cwd();
210
- console.log(`cwd=${state.commandCwd}`);
330
+ printSystemLine(`cwd=${state.commandCwd}`);
211
331
  return true;
212
332
  }
213
333
  if (command === "init") {
214
334
  const result = await initProject(process.cwd());
215
- console.log(`initialized project=${result.projectRoot}`);
335
+ printAgentMessage(`initialized project=${result.projectRoot}`);
216
336
  return true;
217
337
  }
218
338
  if (command === "web") {
219
339
  const port = value || "3220";
220
- console.log(`Run in another terminal: aginti web --port ${port}`);
340
+ printAgentMessage(`Run in another terminal: aginti web --port ${port}`);
221
341
  return true;
222
342
  }
223
343
 
224
- console.log(`Unknown command: /${command}. Use /help.`);
344
+ printAgentMessage(`Unknown command: /${command}. Use /help.`);
225
345
  return true;
226
346
  }
227
347
 
228
348
  async function runPrompt(prompt, state, packageDir) {
349
+ const controller = new AbortController();
229
350
  const config = loadConfig(
230
351
  {
231
352
  provider: state.provider,
@@ -252,51 +373,76 @@ async function runPrompt(prompt, state, packageDir) {
252
373
  state.status = "running";
253
374
  state.activeGoal = prompt.replace(/\s+/g, " ").slice(0, 120);
254
375
  state.lastEvent = "";
255
- console.log(`session=${state.sessionId}`);
256
- console.log(`status=running workingOn=${state.activeGoal}`);
257
-
258
- const result = await runAgent({
259
- ...config,
260
- onEvent: (type, data = {}) => {
261
- if (type === "plan.created") {
262
- printStatusEvent(state, "planned");
263
- } else if (type === "tool.started") {
264
- printStatusEvent(state, "tool", data.toolName || "unknown");
265
- } else if (type === "tool.completed") {
266
- printStatusEvent(state, "tool_done", data.toolName || "unknown");
267
- } else if (type === "tool.blocked") {
268
- printStatusEvent(state, "tool_blocked", data.toolName || data.reason || "unknown");
269
- } else if (type === "conversation.queued_input_applied") {
270
- printStatusEvent(state, "queued_input_applied");
271
- } else if (type === "session.finished") {
272
- printStatusEvent(state, "finished");
273
- } else if (type === "session.stopped") {
274
- printStatusEvent(state, "stopped", data.reason || "");
275
- } else if (type === "model.responded") {
276
- printStatusEvent(state, "model_responded", data.content ? data.content.slice(0, 80).replace(/\s+/g, " ") : "");
277
- }
278
- },
279
- });
376
+ printSystemLine(`session=${state.sessionId}`);
377
+ printSystemLine(`status=running workingOn=${state.activeGoal}`);
378
+
379
+ const detachInterrupts = attachRunInterrupts(controller);
380
+ let result;
381
+ try {
382
+ result = await runAgent({
383
+ ...config,
384
+ abortSignal: controller.signal,
385
+ onConsole: (text, options = {}) => {
386
+ if (options.kind === "assistant") {
387
+ printAgentMessage(text);
388
+ } else if (options.kind === "plan") {
389
+ printWrapped(`${label("plan", ansi.systemBg)} `, text);
390
+ } else if (options.kind === "heading") {
391
+ printHeading(text);
392
+ } else if (options.error) {
393
+ console.error(`${label("error", ansi.systemBg)} ${stripMarkdown(text)}`);
394
+ } else {
395
+ printSystemLine(text);
396
+ }
397
+ },
398
+ onEvent: (type, data = {}) => {
399
+ if (type === "plan.created") {
400
+ printStatusEvent(state, "planned");
401
+ } else if (type === "tool.started") {
402
+ printStatusEvent(state, "tool", data.toolName || "unknown");
403
+ } else if (type === "tool.completed") {
404
+ printStatusEvent(state, "tool_done", data.toolName || "unknown");
405
+ } else if (type === "tool.blocked") {
406
+ printStatusEvent(state, "tool_blocked", data.toolName || data.reason || "unknown");
407
+ } else if (type === "loop.guard") {
408
+ printStatusEvent(state, "loop_guard", data.toolName || "");
409
+ } else if (type === "conversation.queued_input_applied") {
410
+ printStatusEvent(state, "queued_input_applied");
411
+ } else if (type === "session.finished") {
412
+ printStatusEvent(state, "finished");
413
+ } else if (type === "session.stopped") {
414
+ printStatusEvent(state, "stopped", data.reason || "");
415
+ } else if (type === "model.responded") {
416
+ printStatusEvent(state, "model_responded", data.content ? data.content.slice(0, 80).replace(/\s+/g, " ") : "");
417
+ }
418
+ },
419
+ });
420
+ } finally {
421
+ detachInterrupts();
422
+ }
280
423
  state.sessionId = result.sessionId || state.sessionId;
281
424
  state.status = result.stopped ? "stopped" : "idle";
282
425
  state.activeGoal = "";
283
- console.log(`status=${state.status} session=${state.sessionId}`);
426
+ printSystemLine(`status=${state.status} session=${state.sessionId}`);
427
+ if (result.stopped && result.reason === "user_interrupt") {
428
+ await printResumeHint(state);
429
+ }
284
430
  }
285
431
 
286
432
  export async function startInteractiveCli(args = {}, { packageDir, packageVersion } = {}) {
287
433
  const state = createState(args);
288
434
  const rl = readline.createInterface({ input, output, terminal: Boolean(input.isTTY && output.isTTY) });
289
435
 
290
- console.log(`AgInTiFlow ${packageVersion || ""}`.trim());
291
- console.log(`Project: ${process.cwd()}`);
292
- console.log("Interactive agent chat. Type /help for commands, /exit to quit.");
436
+ console.log(color(` AgInTiFlow ${packageVersion || ""} `, ansi.agentBg, ansi.bold).trimEnd());
437
+ printSystemLine(`Project: ${process.cwd()}`);
438
+ printAgentMessage("Interactive agent chat. Type /help for commands, /exit to quit.");
293
439
  printStatus(state);
294
440
 
295
441
  try {
296
442
  while (true) {
297
443
  let answer = "";
298
444
  try {
299
- answer = await rl.question("\naginti> ");
445
+ answer = await rl.question(userPrompt());
300
446
  } catch (error) {
301
447
  if (error?.code === "ERR_USE_AFTER_CLOSE") break;
302
448
  if (isAbortError(error)) {
@@ -320,7 +466,7 @@ export async function startInteractiveCli(args = {}, { packageDir, packageVersio
320
466
  await printResumeHint(state);
321
467
  break;
322
468
  }
323
- console.error(`error: ${error.message}`);
469
+ console.error(`${label("error", ansi.systemBg)} ${error.message}`);
324
470
  }
325
471
  }
326
472
  } finally {
@@ -53,6 +53,10 @@ function prepareMessages(config, messages) {
53
53
  });
54
54
  }
55
55
 
56
+ function requestOptions(config) {
57
+ return config.abortSignal ? { signal: config.abortSignal } : undefined;
58
+ }
59
+
56
60
  function mockCommandForGoal(goal = "") {
57
61
  const text = String(goal).toLowerCase();
58
62
  if (/\blist\b|folder contents|directory contents|files?/.test(text)) return "ls -la";
@@ -94,6 +98,18 @@ function mockWorkspaceToolForGoal(goal = "") {
94
98
  return null;
95
99
  }
96
100
 
101
+ function mockPreviewToolForGoal(goal = "") {
102
+ const text = String(goal).toLowerCase();
103
+ if (!/(open|preview|view|browser|website|web\s*site)/.test(text)) return null;
104
+ const targetPath = mockPathForGoal(goal);
105
+ if (targetPath === "mock-output.txt") return null;
106
+ if (!/\.(html|htm|svg|png|jpe?g|webp|pdf|txt|md)$/i.test(targetPath)) return null;
107
+ return mockToolCall("preview_workspace", {
108
+ path: targetPath,
109
+ port: 8765,
110
+ });
111
+ }
112
+
97
113
  function mockCanvasToolForGoal(goal = "") {
98
114
  const text = String(goal).toLowerCase();
99
115
  if (!/canvas|artifact|image|figure|visual|preview|render/.test(text)) return null;
@@ -130,10 +146,11 @@ export async function createPlan(client, config, state) {
130
146
  ].join("\n");
131
147
  }
132
148
 
133
- const response = await client.chat.completions.create({
134
- model: config.model,
135
- temperature: 0,
136
- messages: [
149
+ const response = await client.chat.completions.create(
150
+ {
151
+ model: config.model,
152
+ temperature: 0,
153
+ messages: [
137
154
  {
138
155
  role: "system",
139
156
  content:
@@ -149,7 +166,7 @@ export async function createPlan(client, config, state) {
149
166
  ? `Shell tool is enabled in ${config.commandCwd}. In Docker, this path is mounted as /workspace with persistent /aginti-env and /aginti-cache mounts. Use relative paths or /workspace paths, not absolute host temp paths. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}. For npm/pip/conda/venv setup, explain the need and wait for approval unless policy is allow.`
150
167
  : "",
151
168
  config.allowFileTools
152
- ? `Workspace file tools are enabled in ${config.commandCwd}: list_files, read_file, search_files, write_file, apply_patch. Keep all paths workspace-relative, for example plot_fx.svg or docs/report.tex, and avoid secrets.`
169
+ ? `Workspace file tools are enabled in ${config.commandCwd}: list_files, read_file, search_files, write_file, apply_patch, open_workspace_file, preview_workspace. Keep all paths workspace-relative, for example plot_fx.svg or docs/report.tex, and avoid secrets. For generated local HTML/SVG/PDF/static sites, plan to use open_workspace_file or preview_workspace rather than starting a localhost server inside Docker.`
153
170
  : "",
154
171
  config.allowWrapperTools
155
172
  ? `Agent wrappers are enabled. Use the selected wrapper only: ${normalizeWrapperName(config.preferredWrapper)}. Status: ${wrapperStatusText()}.`
@@ -160,6 +177,7 @@ export async function createPlan(client, config, state) {
160
177
  "Use the canvas tunnel for outputs the user would likely want to inspect visually, such as figures, PDFs, screenshots, images, important markdown, or generated files.",
161
178
  "For environment or system-maintenance work, prefer project-local dry-run plans/scripts unless the configured policy explicitly allows stronger actions.",
162
179
  "Docker language/toolchain installs should prefer /aginti-env or project files so they persist across runs; apt/apk changes are ephemeral unless the image is rebuilt.",
180
+ "If a localhost/browser preview fails, do not loop on the same URL. Switch to open_workspace_file or preview_workspace, or finish with the local path and honest limitation.",
163
181
  "If the run is close to the max-step limit, finish with the best complete artifact and honest limitations instead of starting a new approach.",
164
182
  "Plan for a complete result, not endless exploration; finish once the request is satisfied and checks have passed or been honestly skipped.",
165
183
  "Return a numbered plan only.",
@@ -167,8 +185,10 @@ export async function createPlan(client, config, state) {
167
185
  .filter(Boolean)
168
186
  .join("\n"),
169
187
  },
170
- ],
171
- });
188
+ ],
189
+ },
190
+ requestOptions(config)
191
+ );
172
192
 
173
193
  return response.choices[0]?.message?.content?.trim() || "1. Inspect the page.\n2. Use the smallest safe action.\n3. Finish with a concise answer.";
174
194
  }
@@ -179,7 +199,8 @@ export async function requestNextStep(client, config, messages) {
179
199
  type: "function",
180
200
  function: {
181
201
  name: "open_url",
182
- description: "Open an absolute http or https URL in the browser.",
202
+ description:
203
+ "Open a remote absolute http or https URL in the browser. Do not use this for generated local workspace files or localhost preview loops; use open_workspace_file or preview_workspace when available.",
183
204
  parameters: {
184
205
  type: "object",
185
206
  properties: {
@@ -296,6 +317,45 @@ export async function requestNextStep(client, config, messages) {
296
317
  },
297
318
  ];
298
319
 
320
+ if (config.allowFileTools) {
321
+ tools.splice(
322
+ 0,
323
+ 0,
324
+ {
325
+ type: "function",
326
+ function: {
327
+ name: "open_workspace_file",
328
+ description:
329
+ "Open a workspace-local file directly in the browser, such as generated HTML, SVG, PNG, PDF, or text. Prefer this over starting a localhost server when the user asks to open a generated local page.",
330
+ parameters: {
331
+ type: "object",
332
+ properties: {
333
+ path: { type: "string", description: "Workspace-relative file path to open." },
334
+ },
335
+ required: ["path"],
336
+ additionalProperties: false,
337
+ },
338
+ },
339
+ },
340
+ {
341
+ type: "function",
342
+ function: {
343
+ name: "preview_workspace",
344
+ description:
345
+ "Start a persistent host-side static preview server for a workspace file or directory, automatically choosing a free port, then open it in the browser. Use this for generated local websites instead of running python -m http.server inside Docker or repeatedly opening localhost URLs.",
346
+ parameters: {
347
+ type: "object",
348
+ properties: {
349
+ path: { type: "string", description: "Workspace-relative file or directory to preview. Defaults to ." },
350
+ port: { type: "integer", description: "Preferred localhost port. The runtime chooses another if busy." },
351
+ },
352
+ additionalProperties: false,
353
+ },
354
+ },
355
+ }
356
+ );
357
+ }
358
+
299
359
  if (config.allowShellTool) {
300
360
  tools.splice(-1, 0, {
301
361
  type: "function",
@@ -495,6 +555,13 @@ export async function requestNextStep(client, config, messages) {
495
555
  ]);
496
556
  }
497
557
 
558
+ if (config.allowFileTools) {
559
+ const previewTool = mockPreviewToolForGoal(config.goal);
560
+ if (previewTool) {
561
+ return mockChatResponse("Mock mode will exercise the workspace preview tool.", [previewTool]);
562
+ }
563
+ }
564
+
498
565
  const canvasTool = mockCanvasToolForGoal(config.goal);
499
566
  if (canvasTool) {
500
567
  return mockChatResponse("Mock mode will publish a canvas artifact for the UI tunnel.", [canvasTool]);
@@ -520,12 +587,15 @@ export async function requestNextStep(client, config, messages) {
520
587
  ]);
521
588
  }
522
589
 
523
- return client.chat.completions.create({
524
- model: config.model,
525
- temperature: 0,
526
- tool_choice: "auto",
527
- parallel_tool_calls: false,
528
- messages: prepareMessages(config, messages),
529
- tools,
530
- });
590
+ return client.chat.completions.create(
591
+ {
592
+ model: config.model,
593
+ temperature: 0,
594
+ tool_choice: "auto",
595
+ parallel_tool_calls: false,
596
+ messages: prepareMessages(config, messages),
597
+ tools,
598
+ },
599
+ requestOptions(config)
600
+ );
531
601
  }