@lazyingart/agintiflow 0.10.0 → 0.12.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 CHANGED
@@ -71,7 +71,11 @@ Inside chat, type normal requests such as `write a small Python CLI app with tes
71
71
 
72
72
  For code edits, AgInTiFlow routes patch/refactor/database-style tasks to DeepSeek v4 pro by default and exposes `apply_patch` as a deterministic workspace tool. It supports exact replacements, Codex-style patch envelopes, and unified diffs, with preflight checks, path guardrails, hashes, and compact per-file diffs. See [docs/patch-tools.md](docs/patch-tools.md).
73
73
 
74
- For larger repositories, use `--profile large-codebase` or choose **Large codebase engineering** in the web UI. This routes to DeepSeek v4 pro, starts with `inspect_project`, then uses search/read/patch/check loops inspired by Codex, Copilot SDK, Claude Code, Gemini CLI, Qwen, and Claw Code. See [docs/large-codebase-engineering.md](docs/large-codebase-engineering.md).
74
+ For larger repositories, use `--profile large-codebase` or choose **Large codebase engineering** in the web UI. The web default stays **Auto**, and Auto now escalates codebase/system/debugging prompts to the same engineering loop when needed. Complex work routes to DeepSeek v4 pro, starts with `inspect_project`, then uses search/read/patch/check loops inspired by Codex, Copilot SDK, Claude Code, Gemini CLI, Qwen, and Claw Code. See [docs/large-codebase-engineering.md](docs/large-codebase-engineering.md).
75
+
76
+ AgInTiFlow can also spend cheap DeepSeek calls on parallel scout notes before the main executor starts a complicated task. Scouts run independently for architecture, implementation, review, and research risks, then the main agent uses those notes while still doing the real file/shell/browser work itself. Disable with `--no-parallel-scouts` or set `--scout-count 1..4`.
77
+
78
+ For current docs, install errors, package/toolchain setup, and source discovery, the agent has a guarded `web_search` tool. It returns compact search results without browser search-engine loops and respects configured domain allowlists. Disable with `--no-web-search`.
75
79
 
76
80
  For raster image work, AgInTiFlow has an optional `image_generation` skill backed by the `generate_image` tool and a local `GRSAI` key. The skill tells DeepSeek when image generation is appropriate; the tool calls GRS AI Nano Banana, saves manifests/images under `artifacts/images`, and sends the result to the canvas. See [docs/auxiliary-image-generation.md](docs/auxiliary-image-generation.md).
77
81
 
@@ -10,6 +10,8 @@ Local agent references informed the design:
10
10
  - Copilot-style SDK surfaces: structured tools, session persistence, plan/history/workspace APIs, and explicit permission hooks.
11
11
  - Claude/Claw-style safety: project-local status, container-first execution, read-only operations by default, and clear failure recovery.
12
12
  - Gemini/Qwen-style extensibility: capability discovery through profiles and tools rather than hardcoding one model behavior.
13
+ - Claw-style `doctor` discipline: check health and environment before treating system symptoms as code bugs.
14
+ - Claude/Codex-style context discipline: read instructions, manifests, entry points, and failing tests before touching broad files.
13
15
 
14
16
  ## Skill vs Tool
15
17
 
@@ -47,3 +49,54 @@ aginti --profile large-codebase "fix the failing tests"
47
49
  or choose **Large codebase engineering** in the web task-profile dropdown.
48
50
 
49
51
  Smart routing sends this profile to DeepSeek v4 pro even when the user prompt is short.
52
+
53
+ ## Auto Profile Behavior
54
+
55
+ The web app still defaults to **Auto**. Auto does not mean weak. When the prompt mentions a large repo, system bug, failing tests, setup, install, migration, or a known language stack, AgInTiFlow adds engineering guidance and raises the step budget automatically.
56
+
57
+ Examples:
58
+
59
+ ```bash
60
+ aginti "debug this Python project system bug and fix failing tests"
61
+ aginti "fix the Rust workspace build"
62
+ aginti "repair the Docker setup and run the Node tests"
63
+ ```
64
+
65
+ These route to DeepSeek v4 pro when the complexity score is high enough.
66
+
67
+ ## Parallel Scout Mode
68
+
69
+ DeepSeek calls are cheap enough that complex tasks can use several short advisory calls before the main executor starts. When enabled, AgInTiFlow runs bounded scouts in parallel:
70
+
71
+ - Architect: decomposes the task and identifies first files/logs/commands.
72
+ - Implementer: predicts patch boundaries and focused checks.
73
+ - Reviewer: looks for missing tests, risks, and instruction-compliance failures.
74
+ - Researcher: suggests `web_search` queries when current information may matter.
75
+
76
+ Scout output is injected as advisory context only. The main agent still owns execution and must use real tools to inspect, edit, run commands, and finish. CLI flags:
77
+
78
+ ```bash
79
+ aginti --parallel-scouts --scout-count 4 "fix this complicated repo bug"
80
+ aginti --no-parallel-scouts "run a cheap short task"
81
+ ```
82
+
83
+ The web app exposes the same toggle and scout count.
84
+
85
+ ## Web Search
86
+
87
+ Use `web_search` for current docs, package/toolchain errors, install instructions, and source discovery. It returns compact titles, URLs, and snippets, and should be preferred over opening a search engine in the browser. Specific result pages can still be opened later with `open_url`.
88
+
89
+ ```bash
90
+ aginti --web-search "look up the current pytest config docs and update this project"
91
+ aginti --no-web-search "work fully offline"
92
+ ```
93
+
94
+ ## Cross-Language Playbook
95
+
96
+ AgInTiFlow gives DeepSeek stack-specific reminders without hardcoding a solution:
97
+
98
+ - JS/TS: inspect package scripts and lockfiles, then run focused `node`, `tsc`, or test commands.
99
+ - Python: inspect `pyproject.toml` or requirements, prefer project-local venv/conda/Docker, then run focused pytest/module checks.
100
+ - Rust/Go/JVM/C/C++: inspect native manifests, format only touched files when possible, and start with narrow build/test targets.
101
+ - R/Stan/LaTeX: keep toolchains project-local or Docker-backed, compile from the right directory, and publish useful artifacts to canvas.
102
+ - System tasks: diagnose first, capture versions/logs, write reversible scripts, use Docker for installs, and avoid silent host-level changes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a resumable Playwright website-control agent with OpenAI-compatible tool calling.",
6
6
  "license": "Apache-2.0",
package/public/app.js CHANGED
@@ -69,6 +69,9 @@ const translations = {
69
69
  shellToolLabel: "Enable shell tool",
70
70
  fileToolLabel: "Enable file tools",
71
71
  auxiliaryToolLabel: "Enable auxiliary skills",
72
+ webSearchLabel: "Enable web search",
73
+ parallelScoutsLabel: "Parallel DeepSeek scouts",
74
+ parallelScoutCountLabel: "Scout count",
72
75
  wrapperToolLabel: "Enable agent wrappers",
73
76
  preferredWrapperLabel: "Preferred wrapper",
74
77
  dockerSandboxLabel: "Use Docker sandbox",
@@ -650,6 +653,9 @@ const runMetaEl = document.querySelector("#run-meta");
650
653
  const stopRunButton = document.querySelector("#stop-run");
651
654
  const keyStatusEl = document.querySelector("#key-status");
652
655
  const allowAuxiliaryToolsField = document.querySelector("#allowAuxiliaryTools");
656
+ const allowWebSearchField = document.querySelector("#allowWebSearch");
657
+ const allowParallelScoutsField = document.querySelector("#allowParallelScouts");
658
+ const parallelScoutCountField = document.querySelector("#parallelScoutCount");
653
659
  const allowWrapperToolsField = document.querySelector("#allowWrapperTools");
654
660
  const preferredWrapperField = document.querySelector("#preferredWrapper");
655
661
  const wrapperStatusEl = document.querySelector("#wrapper-status");
@@ -775,12 +781,25 @@ function renderTaskProfiles(selected = "auto") {
775
781
  taskProfileField.value = profiles.some((profile) => profile.id === selected) ? selected : "auto";
776
782
  }
777
783
 
778
- function recommendedMaxStepsForProfile(profile = "auto") {
784
+ const COMPLEX_ENGINEERING_HINT = /\b(large|complex|complicated|monorepo|codebase|repository|repo-wide|multi[- ]file|cross[- ]file|architecture|refactor|migration|regression|root cause|failing tests?|fix build|system bug|debug|performance|security)\b/i;
785
+
786
+ function recommendedMaxStepsForProfile(profile = "auto", goal = "") {
779
787
  if (profile === "large-codebase") return 36;
780
788
  if (profile === "latex") return 30;
789
+ if (COMPLEX_ENGINEERING_HINT.test(goal || "")) return 36;
790
+ if (/\b(latex|tex|pdflatex|latexmk|pdf|website|app|docker|system|install|setup|debug)\b/i.test(goal || "")) return 30;
781
791
  return 24;
782
792
  }
783
793
 
794
+ function ensureRecommendedMaxStepsForCurrentTask() {
795
+ const maxStepsField = document.querySelector("#maxSteps");
796
+ const goalField = document.querySelector("#goal");
797
+ const recommended = recommendedMaxStepsForProfile(taskProfileField?.value || "auto", goalField?.value || "");
798
+ if (maxStepsField && Number(maxStepsField.value || 0) < recommended) {
799
+ maxStepsField.value = String(recommended);
800
+ }
801
+ }
802
+
784
803
  function renderWrapperStatus(wrappers = lastWrappers) {
785
804
  lastWrappers = wrappers || [];
786
805
  if (lastWrappers.length === 0) {
@@ -990,6 +1009,9 @@ function formPayload() {
990
1009
  allowShellTool: document.querySelector("#allowShellTool").checked,
991
1010
  allowFileTools: document.querySelector("#allowFileTools").checked,
992
1011
  allowAuxiliaryTools: allowAuxiliaryToolsField?.checked ?? true,
1012
+ allowWebSearch: allowWebSearchField?.checked ?? true,
1013
+ allowParallelScouts: allowParallelScoutsField?.checked ?? true,
1014
+ parallelScoutCount: Number(parallelScoutCountField?.value) || 3,
993
1015
  allowWrapperTools: allowWrapperToolsField.checked,
994
1016
  preferredWrapper: preferredWrapperField.value,
995
1017
  taskProfile: taskProfileField?.value || "auto",
@@ -1966,11 +1988,7 @@ packageInstallPolicyField.addEventListener("change", updatePackageWarning);
1966
1988
  allowWrapperToolsField.addEventListener("change", () => renderWrapperStatus());
1967
1989
  preferredWrapperField.addEventListener("change", () => renderWrapperStatus());
1968
1990
  taskProfileField?.addEventListener("change", () => {
1969
- const maxStepsField = document.querySelector("#maxSteps");
1970
- const recommended = recommendedMaxStepsForProfile(taskProfileField.value);
1971
- if (maxStepsField && Number(maxStepsField.value || 0) < recommended) {
1972
- maxStepsField.value = String(recommended);
1973
- }
1991
+ ensureRecommendedMaxStepsForCurrentTask();
1974
1992
  schedulePreferenceSave();
1975
1993
  });
1976
1994
 
@@ -2048,6 +2066,7 @@ form.addEventListener("submit", async (event) => {
2048
2066
  setLogs(t("goalRequired"), "empty");
2049
2067
  return;
2050
2068
  }
2069
+ ensureRecommendedMaxStepsForCurrentTask();
2051
2070
 
2052
2071
  const payload = {
2053
2072
  ...formPayload(),
@@ -2186,6 +2205,9 @@ async function loadConfig() {
2186
2205
  document.querySelector("#allowShellTool").checked = prefs.allowShellTool ?? true;
2187
2206
  document.querySelector("#allowFileTools").checked = prefs.allowFileTools ?? true;
2188
2207
  if (allowAuxiliaryToolsField) allowAuxiliaryToolsField.checked = prefs.allowAuxiliaryTools ?? true;
2208
+ if (allowWebSearchField) allowWebSearchField.checked = prefs.allowWebSearch ?? true;
2209
+ if (allowParallelScoutsField) allowParallelScoutsField.checked = prefs.allowParallelScouts ?? true;
2210
+ if (parallelScoutCountField) parallelScoutCountField.value = String(prefs.parallelScoutCount || 3);
2189
2211
  allowWrapperToolsField.checked = prefs.allowWrapperTools ?? false;
2190
2212
  preferredWrapperField.value = prefs.preferredWrapper || "codex";
2191
2213
  document.querySelector("#dockerSandboxImage").value = prefs.dockerSandboxImage || "agintiflow-sandbox:latest";
package/public/index.html CHANGED
@@ -181,6 +181,16 @@
181
181
  <span class="switch" aria-hidden="true"></span>
182
182
  <span data-i18n="auxiliaryToolLabel">Enable auxiliary skills</span>
183
183
  </label>
184
+ <label class="switch-label">
185
+ <input id="allowWebSearch" name="allowWebSearch" type="checkbox" checked />
186
+ <span class="switch" aria-hidden="true"></span>
187
+ <span data-i18n="webSearchLabel">Enable web search</span>
188
+ </label>
189
+ <label class="switch-label">
190
+ <input id="allowParallelScouts" name="allowParallelScouts" type="checkbox" checked />
191
+ <span class="switch" aria-hidden="true"></span>
192
+ <span data-i18n="parallelScoutsLabel">Parallel DeepSeek scouts</span>
193
+ </label>
184
194
  <label class="switch-label">
185
195
  <input id="allowPasswords" name="allowPasswords" type="checkbox" />
186
196
  <span class="switch" aria-hidden="true"></span>
@@ -193,6 +203,11 @@
193
203
  </label>
194
204
  </div>
195
205
 
206
+ <label>
207
+ <span data-i18n="parallelScoutCountLabel">Scout count</span>
208
+ <input id="parallelScoutCount" name="parallelScoutCount" type="number" min="1" max="4" value="3" />
209
+ </label>
210
+
196
211
  <div class="grid wrapper-controls">
197
212
  <label class="switch-label switch-card">
198
213
  <input id="allowWrapperTools" name="allowWrapperTools" type="checkbox" />
@@ -5,8 +5,11 @@ import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { repairModelMessageHistory, runAgent } from "../src/agent-runner.js";
7
7
  import { resolveRuntimeConfig } from "../src/config.js";
8
+ import { engineeringGuidanceForTask, recommendedMaxStepsForTask } from "../src/engineering-guidance.js";
8
9
  import { selectModelRoute } from "../src/model-routing.js";
10
+ import { shouldRunParallelScouts } from "../src/parallel-scouts.js";
9
11
  import { SessionStore } from "../src/session-store.js";
12
+ import { searchWeb } from "../src/web-search.js";
10
13
  import { executeWorkspaceTool } from "../src/workspace-tools.js";
11
14
 
12
15
  const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -93,6 +96,42 @@ try {
93
96
  taskProfile: "large-codebase",
94
97
  });
95
98
  assert(/pro/i.test(largeProfileRoute.model), "large-codebase profile did not route to DeepSeek pro");
99
+ const autoSystemRoute = selectModelRoute({
100
+ routingMode: "smart",
101
+ provider: "deepseek",
102
+ goal: "debug this Python project system bug and fix failing tests",
103
+ taskProfile: "auto",
104
+ });
105
+ assert(/pro/i.test(autoSystemRoute.model), "auto system/code problem did not route to DeepSeek pro");
106
+ assert(
107
+ recommendedMaxStepsForTask({
108
+ goal: "debug this Python project system bug and fix failing tests",
109
+ taskProfile: "auto",
110
+ complexityScore: autoSystemRoute.complexityScore,
111
+ }) >= 36,
112
+ "auto system/code problem did not get engineering step budget"
113
+ );
114
+ const guidance = engineeringGuidanceForTask("debug this Python project system bug and fix failing tests", "auto");
115
+ assert(guidance.includes("Python:"), "engineering guidance did not include Python stack advice");
116
+ assert(guidance.includes("System/shell:"), "engineering guidance did not include system stack advice");
117
+ assert(
118
+ shouldRunParallelScouts(
119
+ {
120
+ provider: "deepseek",
121
+ allowParallelScouts: true,
122
+ routeComplexityScore: autoSystemRoute.complexityScore,
123
+ taskProfile: "auto",
124
+ goal: "debug this Python project system bug and fix failing tests",
125
+ },
126
+ { meta: {}, goal: "debug this Python project system bug and fix failing tests" }
127
+ ),
128
+ "parallel scouts did not enable for complex auto task"
129
+ );
130
+ const drySearch = await searchWeb(
131
+ { query: "AgInTiFlow web_search smoke", maxResults: 2 },
132
+ { allowWebSearch: true, webSearchDryRun: true }
133
+ );
134
+ assert(drySearch.ok && drySearch.results.length === 1, "web_search dry-run did not return deterministic result");
96
135
 
97
136
  await fs.mkdir(path.join(workspace, "src"), { recursive: true });
98
137
  await fs.mkdir(path.join(workspace, "test"), { recursive: true });
@@ -264,6 +303,10 @@ try {
264
303
  "deepseek_history_repair",
265
304
  "deepseek_pro_patch_route",
266
305
  "large_profile_pro_route",
306
+ "auto_system_pro_route",
307
+ "auto_engineering_guidance",
308
+ "parallel_scout_trigger",
309
+ "web_search_dry_run",
267
310
  "inspect_project",
268
311
  "mock_inspect_project",
269
312
  "write_file",
@@ -18,6 +18,9 @@ import { executeWorkspaceTool, resolveWorkspacePath, summarizeWorkspaceTools, WO
18
18
  import { normalizeCanvasPayload } from "./artifact-tunnel.js";
19
19
  import { getTaskProfile } from "./task-profiles.js";
20
20
  import { generateImage, listAuxiliarySkills } from "./auxiliary-tools.js";
21
+ import { engineeringGuidanceForTask } from "./engineering-guidance.js";
22
+ import { searchWeb } from "./web-search.js";
23
+ import { runParallelScouts, shouldRunParallelScouts } from "./parallel-scouts.js";
21
24
 
22
25
  const exec = promisify(execCallback);
23
26
  const BROWSER_TOOLS = new Set(["open_url", "open_workspace_file", "preview_workspace", "click", "type", "scroll", "press", "back"]);
@@ -233,6 +236,7 @@ export function repairModelMessageHistory(state, config = {}) {
233
236
  function createInitialState(config, sessionId) {
234
237
  const now = new Date().toISOString();
235
238
  const taskProfile = getTaskProfile(config.taskProfile);
239
+ const engineeringGuidance = engineeringGuidanceForTask(config.goal, config.taskProfile);
236
240
  return {
237
241
  sessionId,
238
242
  createdAt: now,
@@ -282,7 +286,14 @@ function createInitialState(config, sessionId) {
282
286
  .map((skill) => `${skill.id} via ${skill.toolName} (${skill.available ? "key available" : `needs ${skill.keyName}`})`)
283
287
  .join(", ")}. Use generate_image for real raster image/photo/illustration/cover/poster/logo requests when appropriate; if the key is missing, ask the user to run /auxilliary grsai or aginti login grsai.`
284
288
  : "Auxiliary skills are disabled for this run.",
289
+ config.allowWebSearch
290
+ ? "web_search is available for current information, docs, package/toolchain errors, and source discovery. Prefer web_search over browser search-engine navigation."
291
+ : "web_search is disabled.",
292
+ config.allowParallelScouts
293
+ ? `Parallel DeepSeek scouts may run before complex execution. Scout count: ${config.parallelScoutCount}.`
294
+ : "Parallel scouts are disabled.",
285
295
  `Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
296
+ engineeringGuidance,
286
297
  "A frontend canvas/artifacts tunnel exists. Use send_to_canvas when important markdown, diffs, screenshots, images, or workspace files should be highlighted in the UI. It is optional and ordinary final text can still go directly to finish.",
287
298
  "For visual-output requests such as draw, plot, graph, chart, diagram, figure, image, or visualization, proactively publish a canvas artifact even when the user does not mention canvas. If workspace file tools are enabled, prefer creating a small SVG or markdown artifact and call send_to_canvas with selected=true.",
288
299
  "Work like a practical coding agent: orient with inspect_project/search/read, patch code with apply_patch, run safe checks when they add confidence, iterate on failures, and keep outputs inside the workspace.",
@@ -320,7 +331,10 @@ function createInitialState(config, sessionId) {
320
331
  .map((skill) => `${skill.id}:${skill.available ? "available" : "missing-key"}`)
321
332
  .join(" ")}`
322
333
  : "",
334
+ config.allowWebSearch ? "Web search tool: enabled." : "Web search tool: disabled.",
335
+ config.allowParallelScouts ? `Parallel scouts: enabled count=${config.parallelScoutCount}.` : "Parallel scouts: disabled.",
323
336
  `Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
337
+ engineeringGuidance,
324
338
  "Canvas/artifacts tunnel: available through send_to_canvas for optional frontend rendering.",
325
339
  "Visual-output requests should produce a canvas artifact without requiring the user to ask for canvas explicitly.",
326
340
  "Use file, shell, browser, canvas, and wrapper tools when they are useful; choose the workflow from the user's request. For complicated engineering tasks, keep a tight loop: inspect, choose minimal files, patch, run focused checks, repair, then summarize.",
@@ -410,6 +424,7 @@ function applyContinuationPrompt(state, config, observers) {
410
424
  if (!config.resume || !config.goal) return;
411
425
 
412
426
  const taskProfile = getTaskProfile(config.taskProfile);
427
+ const engineeringGuidance = engineeringGuidanceForTask(config.goal, config.taskProfile);
413
428
  ensureChatState(state);
414
429
  state.goal = config.goal;
415
430
  state.provider = config.provider;
@@ -436,6 +451,7 @@ function applyContinuationPrompt(state, config, observers) {
436
451
  ? `Agent wrappers: selected=${normalizeWrapperName(config.preferredWrapper)}; ${wrapperStatusText()}`
437
452
  : "",
438
453
  `Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
454
+ engineeringGuidance,
439
455
  ]
440
456
  .filter(Boolean)
441
457
  .join("\n"),
@@ -740,6 +756,13 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
740
756
  case "open_url":
741
757
  await abortable(browserState.page.goto(String(args.url), { waitUntil: "domcontentloaded" }), config.abortSignal);
742
758
  break;
759
+ case "web_search": {
760
+ const result = await searchWeb(args, config);
761
+ const eventResult = sanitizeToolResult(result);
762
+ await store.appendEvent(result.ok ? "tool.completed" : "tool.failed", eventResult);
763
+ observers.event(result.ok ? "tool.completed" : "tool.failed", eventResult);
764
+ return result;
765
+ }
743
766
  case "open_workspace_file": {
744
767
  const target = resolveWorkspacePath(config, args.path || args.file || ".");
745
768
  const stat = await fs.stat(target.absolutePath);
@@ -1105,6 +1128,40 @@ export async function runAgent(config) {
1105
1128
  observers.event("plan.created", { plan });
1106
1129
  }
1107
1130
 
1131
+ if (shouldRunParallelScouts(config, state)) {
1132
+ const scouts = await runParallelScouts(client, config, state);
1133
+ state.meta.parallelScoutsCompleted = true;
1134
+ state.meta.parallelScouts = {
1135
+ model: scouts.model,
1136
+ requested: scouts.requested,
1137
+ completed: scouts.completed,
1138
+ };
1139
+ state.messages.push({
1140
+ role: "user",
1141
+ content: scouts.summary,
1142
+ });
1143
+ await store.appendEvent("parallel_scouts.completed", {
1144
+ model: scouts.model,
1145
+ requested: scouts.requested,
1146
+ completed: scouts.completed,
1147
+ scouts: scouts.scouts.map((scout) => ({
1148
+ name: scout.name,
1149
+ model: scout.model,
1150
+ content: scout.content || "",
1151
+ error: scout.error || "",
1152
+ })),
1153
+ });
1154
+ await store.saveState(state);
1155
+ observers.event("parallel_scouts.completed", {
1156
+ model: scouts.model,
1157
+ requested: scouts.requested,
1158
+ completed: scouts.completed,
1159
+ });
1160
+ emitConsole(config, `Parallel scouts: ${scouts.completed}/${scouts.requested} completed using ${scouts.model}`, {
1161
+ kind: "meta",
1162
+ });
1163
+ }
1164
+
1108
1165
  const repair = repairModelMessageHistory(state, config);
1109
1166
  if (repair.changed) {
1110
1167
  await store.appendEvent("history.repaired", repair);
@@ -1123,6 +1180,9 @@ export async function runAgent(config) {
1123
1180
  allowShellTool: config.allowShellTool,
1124
1181
  allowWrapperTools: config.allowWrapperTools,
1125
1182
  preferredWrapper: normalizeWrapperName(config.preferredWrapper),
1183
+ allowWebSearch: config.allowWebSearch,
1184
+ allowParallelScouts: config.allowParallelScouts,
1185
+ parallelScoutCount: config.parallelScoutCount,
1126
1186
  wrappers: config.allowWrapperTools ? wrapperStatusText() : "",
1127
1187
  workspaceFileTools: summarizeWorkspaceTools(config),
1128
1188
  shellSandbox: config.useDockerSandbox ? "docker" : "host",
@@ -1192,6 +1252,8 @@ export async function runAgent(config) {
1192
1252
  agentWrappersAvailable: config.allowWrapperTools,
1193
1253
  preferredWrapper: normalizeWrapperName(config.preferredWrapper),
1194
1254
  agentWrappers: config.allowWrapperTools ? wrapperStatusText() : "",
1255
+ webSearchAvailable: config.allowWebSearch !== false,
1256
+ parallelScouts: state.meta.parallelScouts || null,
1195
1257
  shellSandbox: config.useDockerSandbox ? "docker" : "host",
1196
1258
  sandboxMode: config.sandboxMode,
1197
1259
  packageInstallPolicy: config.packageInstallPolicy,
@@ -207,6 +207,11 @@ export async function buildCapabilityReport(projectRoot, packageVersion, config)
207
207
  toolName: skill.toolName,
208
208
  available: skill.available,
209
209
  })),
210
+ orchestration: {
211
+ webSearch: config.allowWebSearch !== false,
212
+ parallelScouts: config.allowParallelScouts !== false,
213
+ parallelScoutCount: Number(config.parallelScoutCount) || 3,
214
+ },
210
215
  },
211
216
  checks,
212
217
  maintenancePolicy: maintenancePolicyChecks(config),
package/src/cli.js CHANGED
@@ -14,7 +14,8 @@ import {
14
14
  setProviderKey,
15
15
  showProjectSession,
16
16
  } from "./project.js";
17
- import { defaultMaxStepsForProfile, listTaskProfiles } from "./task-profiles.js";
17
+ import { listTaskProfiles } from "./task-profiles.js";
18
+ import { recommendedMaxStepsForTask } from "./engineering-guidance.js";
18
19
  import { promptAndSaveDeepSeekKey, promptHidden, shouldPromptForDeepSeek } from "./auth-onboarding.js";
19
20
  import fs from "node:fs/promises";
20
21
  import path from "node:path";
@@ -45,6 +46,9 @@ export function parseArgs(argv) {
45
46
  allowFileTools: undefined,
46
47
  allowWrapperTools: undefined,
47
48
  allowAuxiliaryTools: undefined,
49
+ allowWebSearch: undefined,
50
+ allowParallelScouts: undefined,
51
+ parallelScoutCount: undefined,
48
52
  allowDestructive: undefined,
49
53
  preferredWrapper: "",
50
54
  taskProfile: "",
@@ -182,6 +186,27 @@ export function parseArgs(argv) {
182
186
  result.allowAuxiliaryTools = false;
183
187
  continue;
184
188
  }
189
+ if (arg === "--web-search") {
190
+ result.allowWebSearch = true;
191
+ continue;
192
+ }
193
+ if (arg === "--no-web-search") {
194
+ result.allowWebSearch = false;
195
+ continue;
196
+ }
197
+ if (arg === "--parallel-scouts") {
198
+ result.allowParallelScouts = true;
199
+ continue;
200
+ }
201
+ if (arg === "--no-parallel-scouts") {
202
+ result.allowParallelScouts = false;
203
+ continue;
204
+ }
205
+ if (arg === "--scout-count") {
206
+ result.parallelScoutCount = Number(readOption(argv, i));
207
+ i += 1;
208
+ continue;
209
+ }
185
210
  if (arg === "--allow-wrappers") {
186
211
  result.allowWrapperTools = true;
187
212
  continue;
@@ -236,7 +261,7 @@ export function parseArgs(argv) {
236
261
 
237
262
  function printUsage() {
238
263
  console.log(
239
- 'Usage: aginti [chat] OR aginti web [--port 3210] OR aginti login deepseek|openai|grsai OR aginti resume [latest|<session-id>] ["prompt"] OR aginti queue <session-id> "message" OR aginti [--image] [--latex] [--routing smart|fast|complex|manual] [--provider deepseek|openai|mock] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell|--no-shell] [--allow-destructive] [--allow-file-tools|--no-file-tools] [--allow-auxiliary-tools|--no-auxiliary-tools] [--allow-wrappers --wrapper codex] [--sandbox-status|--sandbox-preflight] "your task"'
264
+ 'Usage: aginti [chat] OR aginti web [--port 3210] OR aginti login deepseek|openai|grsai OR aginti resume [latest|<session-id>] ["prompt"] OR aginti queue <session-id> "message" OR aginti [--image] [--latex] [--routing smart|fast|complex|manual] [--provider deepseek|openai|mock] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell|--no-shell] [--allow-file-tools|--no-file-tools] [--web-search|--no-web-search] [--parallel-scouts|--no-parallel-scouts --scout-count 3] [--allow-auxiliary-tools|--no-auxiliary-tools] [--allow-wrappers --wrapper codex] [--sandbox-status|--sandbox-preflight] "your task"'
240
265
  );
241
266
  }
242
267
 
@@ -253,10 +278,18 @@ function agentDefaults(args) {
253
278
  allowShellTool: args.allowShellTool ?? true,
254
279
  allowFileTools: args.allowFileTools ?? true,
255
280
  allowAuxiliaryTools: args.allowAuxiliaryTools ?? true,
281
+ allowWebSearch: args.allowWebSearch ?? true,
282
+ allowParallelScouts: args.allowParallelScouts ?? true,
283
+ parallelScoutCount: args.parallelScoutCount || 3,
256
284
  sandboxMode: args.sandboxMode || "docker-workspace",
257
285
  packageInstallPolicy: args.packageInstallPolicy || "allow",
258
286
  useDockerSandbox: args.useDockerSandbox ?? true,
259
- maxSteps: args.maxSteps || defaultMaxStepsForProfile(args.taskProfile || (args.latex ? "latex" : "auto")),
287
+ maxSteps:
288
+ args.maxSteps ||
289
+ recommendedMaxStepsForTask({
290
+ goal: args.goal || "",
291
+ taskProfile: args.taskProfile || (args.latex ? "latex" : "auto"),
292
+ }),
260
293
  };
261
294
 
262
295
  if (defaults.sandboxMode === "host") {
package/src/config.js CHANGED
@@ -4,7 +4,8 @@ import { getProviderDefaults, normalizeRoutingMode, selectModelRoute } from "./m
4
4
  import { normalizePackageInstallPolicy, normalizeSandboxMode } from "./command-policy.js";
5
5
  import { normalizeWrapperName } from "./tool-wrappers.js";
6
6
  import { loadProjectEnv, resolveProjectRoot } from "./project.js";
7
- import { defaultMaxStepsForProfile, normalizeTaskProfile } from "./task-profiles.js";
7
+ import { normalizeTaskProfile } from "./task-profiles.js";
8
+ import { recommendedMaxStepsForTask } from "./engineering-guidance.js";
8
9
 
9
10
  function parseBoolean(value, fallback) {
10
11
  if (value === undefined) return fallback;
@@ -43,6 +44,11 @@ export function resolveRuntimeConfig(args, overrides = {}) {
43
44
  });
44
45
 
45
46
  const defaults = getProviderDefaults(route.provider);
47
+ const defaultMaxSteps = recommendedMaxStepsForTask({
48
+ goal: args.goal || "",
49
+ taskProfile,
50
+ complexityScore: route.complexityScore,
51
+ });
46
52
  const packageDir = path.resolve(overrides.packageDir || process.env.AGINTIFLOW_PACKAGE_DIR || baseDir);
47
53
  const dockerRequested = parseBoolean(overrides.useDockerSandbox ?? args.useDockerSandbox ?? process.env.USE_DOCKER_SANDBOX, true);
48
54
  const requestedSandboxMode =
@@ -67,7 +73,7 @@ export function resolveRuntimeConfig(args, overrides = {}) {
67
73
  apiKey: overrides.apiKey || defaults.apiKey,
68
74
  baseURL: overrides.baseURL || defaults.baseURL,
69
75
  model: route.model || defaults.model,
70
- maxSteps: parseNumber(overrides.maxSteps ?? args.maxSteps ?? process.env.MAX_STEPS, defaultMaxStepsForProfile(taskProfile)),
76
+ maxSteps: parseNumber(overrides.maxSteps ?? args.maxSteps ?? process.env.MAX_STEPS, defaultMaxSteps),
71
77
  headless: parseBoolean(overrides.headless ?? args.headless ?? process.env.HEADLESS, false),
72
78
  allowedDomains: Array.isArray(overrides.allowedDomains)
73
79
  ? overrides.allowedDomains
@@ -84,6 +90,12 @@ export function resolveRuntimeConfig(args, overrides = {}) {
84
90
  overrides.allowAuxiliaryTools ?? args.allowAuxiliaryTools ?? process.env.ALLOW_AUXILIARY_TOOLS,
85
91
  true
86
92
  ),
93
+ allowWebSearch: parseBoolean(overrides.allowWebSearch ?? args.allowWebSearch ?? process.env.ALLOW_WEB_SEARCH, true),
94
+ allowParallelScouts: parseBoolean(
95
+ overrides.allowParallelScouts ?? args.allowParallelScouts ?? process.env.AGINTI_PARALLEL_SCOUTS,
96
+ true
97
+ ),
98
+ parallelScoutCount: parseNumber(overrides.parallelScoutCount ?? args.parallelScoutCount ?? process.env.AGINTI_SCOUT_COUNT, 3),
87
99
  preferredWrapper: normalizeWrapperName(
88
100
  overrides.preferredWrapper ?? args.preferredWrapper ?? process.env.PREFERRED_WRAPPER ?? process.env.AGENT_WRAPPER
89
101
  ),
@@ -0,0 +1,102 @@
1
+ import { defaultMaxStepsForProfile, normalizeTaskProfile } from "./task-profiles.js";
2
+
3
+ const LANGUAGE_HINTS = [
4
+ {
5
+ id: "javascript-typescript",
6
+ pattern: /\b(node|npm|pnpm|yarn|bun|javascript|typescript|react|vue|svelte|next\.?js|vite|express)\b/i,
7
+ text:
8
+ "JS/TS: inspect package.json and lockfiles, identify package manager, use npm/pnpm/yarn scripts before inventing commands, prefer targeted node --check/tsc/test runs before broad builds.",
9
+ },
10
+ {
11
+ id: "python",
12
+ pattern: /\b(python|pytest|pip|uv|poetry|conda|venv|jupyter|fastapi|django|flask|pandas|numpy)\b/i,
13
+ text:
14
+ "Python: inspect pyproject/requirements, prefer project-local venv/uv/conda or Docker, run python -m pytest or focused module checks, avoid global package installs on host.",
15
+ },
16
+ {
17
+ id: "rust",
18
+ pattern: /\b(rust|cargo|crate|clippy|rustfmt|tokio|actix)\b/i,
19
+ text:
20
+ "Rust: inspect Cargo.toml/workspace crates, run cargo fmt/check/test on the narrowest crate first, preserve Cargo.lock discipline, avoid broad workspace runs until focused checks pass.",
21
+ },
22
+ {
23
+ id: "go",
24
+ pattern: /\b(golang|go test|go mod|goroutine|gin|grpc)\b/i,
25
+ text:
26
+ "Go: inspect go.mod, use go test ./pkg-or-target first, run gofmt on touched files, avoid changing module paths unless required.",
27
+ },
28
+ {
29
+ id: "java-jvm",
30
+ pattern: /\b(java|kotlin|gradle|maven|spring|junit|jvm)\b/i,
31
+ text:
32
+ "JVM: inspect pom.xml/build.gradle/settings.gradle, use focused Maven/Gradle test targets, keep generated build outputs out of source patches.",
33
+ },
34
+ {
35
+ id: "c-cpp",
36
+ pattern: /\b(c\+\+|cpp|cmake|makefile|gcc|clang|native|segfault|asan|valgrind)\b/i,
37
+ text:
38
+ "C/C++: inspect CMake/Make/build scripts, prefer out-of-tree builds, run compile-only or narrow tests first, use sanitizers only when available and safe.",
39
+ },
40
+ {
41
+ id: "shell-system",
42
+ pattern: /\b(shell|bash|zsh|system|systemd|docker|linux|ubuntu|debian|apt|yum|dnf|brew|service|permission denied|port|network)\b/i,
43
+ text:
44
+ "System/shell: diagnose first with read-only commands, capture versions/logs, make reversible scripts, use Docker for installs/toolchains, and only use host-level changes when policy explicitly allows them.",
45
+ },
46
+ {
47
+ id: "r-stats",
48
+ pattern: /\b(rstats|r language|cmdstanr|stan|renv|tidyverse|shiny)\b/i,
49
+ text:
50
+ "R/Stan: inspect renv/DESCRIPTION and project notes, prefer project-local libraries or Docker, validate scripts with non-interactive Rscript commands when available.",
51
+ },
52
+ {
53
+ id: "latex",
54
+ pattern: /\b(latex|tex|pdflatex|latexmk|bibtex|biber|pdf)\b/i,
55
+ text:
56
+ "LaTeX: keep source/figures together, compile from the document directory, run enough passes for refs/bibliography, publish PDF/source artifacts to canvas.",
57
+ },
58
+ ];
59
+
60
+ const COMPLEX_ENGINEERING_PATTERN =
61
+ /\b(large|complex|complicated|monorepo|codebase|repository|repo-wide|multi[- ]file|cross[- ]file|architecture|refactor|migration|regression|root cause|failing tests?|fix build|system bug|debug|performance|security)\b/i;
62
+
63
+ export function engineeringGuidanceForTask(goal = "", taskProfile = "auto") {
64
+ const normalizedProfile = normalizeTaskProfile(taskProfile);
65
+ const text = String(goal || "");
66
+ const matched = LANGUAGE_HINTS.filter((hint) => hint.pattern.test(text));
67
+ const wantsComplex =
68
+ normalizedProfile === "large-codebase" ||
69
+ normalizedProfile === "maintenance" ||
70
+ COMPLEX_ENGINEERING_PATTERN.test(text) ||
71
+ text.length > 500;
72
+
73
+ if (!wantsComplex && matched.length === 0) return "";
74
+
75
+ const lines = [
76
+ "Engineering operating mode:",
77
+ "Use the proven coding-agent loop: inspect_project, read instructions/manifests, search exact symbols/errors, patch small coherent batches, run focused checks, repair failures, then summarize changed files and residual risks.",
78
+ "Keep CLI and web behavior equivalent: use the same workspace, sessions, profiles, file tools, shell policy, Docker mounts, and canvas artifacts.",
79
+ "For large repositories, preserve context by reading fewer but more relevant files; prefer deterministic tools and diffs over long model memory.",
80
+ "For system repair, act like a doctor: gather evidence first, avoid silent destructive host changes, prefer Docker or project-local scripts for installs, and make every stronger action explicit in logs.",
81
+ ];
82
+
83
+ if (matched.length > 0) {
84
+ lines.push("Stack-specific checks:");
85
+ for (const hint of matched.slice(0, 5)) lines.push(`- ${hint.text}`);
86
+ }
87
+
88
+ return lines.join("\n");
89
+ }
90
+
91
+ export function recommendedMaxStepsForTask({ goal = "", taskProfile = "auto", complexityScore = 0 } = {}) {
92
+ const normalizedProfile = normalizeTaskProfile(taskProfile);
93
+ const profileDefault = defaultMaxStepsForProfile(normalizedProfile);
94
+ const text = String(goal || "");
95
+ if (normalizedProfile === "large-codebase" || complexityScore >= 3 || COMPLEX_ENGINEERING_PATTERN.test(text)) {
96
+ return Math.max(profileDefault, 36);
97
+ }
98
+ if (/\b(latex|tex|pdflatex|latexmk|pdf|website|app|docker|system|install|setup|debug)\b/i.test(text)) {
99
+ return Math.max(profileDefault, 30);
100
+ }
101
+ return profileDefault;
102
+ }
package/src/guardrails.js CHANGED
@@ -72,6 +72,18 @@ export function checkToolUse({ toolName, args, snapshot, config }) {
72
72
  return { allowed: true };
73
73
  }
74
74
 
75
+ if (toolName === "web_search") {
76
+ if (config.allowWebSearch === false) {
77
+ return { allowed: false, reason: "Web search is disabled for this run.", category: "web-search" };
78
+ }
79
+ const query = String(args.query || "").trim();
80
+ if (!query) return { allowed: false, reason: "Search query is required.", category: "web-search" };
81
+ if (Buffer.byteLength(query, "utf8") > 500) {
82
+ return { allowed: false, reason: "Search query is too large.", category: "web-search" };
83
+ }
84
+ return { allowed: true };
85
+ }
86
+
75
87
  if (toolName === "open_workspace_file" || toolName === "preview_workspace") {
76
88
  if (!config.allowFileTools) {
77
89
  return { allowed: false, reason: "Workspace preview tools require file tools to be enabled.", category: "workspace-tools" };
@@ -6,6 +6,7 @@ import { loadConfig } from "./config.js";
6
6
  import { initProject, listProjectSessions, providerKeyStatus, setProviderKey } from "./project.js";
7
7
  import { normalizePackageInstallPolicy, normalizeSandboxMode } from "./command-policy.js";
8
8
  import { defaultMaxStepsForProfile, normalizeTaskProfile } from "./task-profiles.js";
9
+ import { recommendedMaxStepsForTask } from "./engineering-guidance.js";
9
10
  import { promptAndSaveDeepSeekKey, promptHidden, shouldPromptForDeepSeek } from "./auth-onboarding.js";
10
11
 
11
12
  const useColor = Boolean(input.isTTY && output.isTTY && process.env.AGINTIFLOW_NO_COLOR !== "1");
@@ -36,6 +37,8 @@ const SLASH_COMMANDS = [
36
37
  "/resume",
37
38
  "/sessions",
38
39
  "/profile",
40
+ "/web-search",
41
+ "/scouts",
39
42
  "/routing",
40
43
  "/provider",
41
44
  "/model",
@@ -216,6 +219,8 @@ function printHelp() {
216
219
  " /resume <session-id> Continue a saved session.",
217
220
  " /sessions List recent sessions in this project.",
218
221
  " /profile <name> Set task profile, e.g. code, website, latex, maintenance.",
222
+ " /web-search on|off Enable or disable the web_search tool.",
223
+ " /scouts on|off|<1-4> Enable parallel DeepSeek scouts and set scout count.",
219
224
  " /routing <mode> Set routing: smart, fast, complex, manual.",
220
225
  " /provider <name> Set provider: deepseek, openai, mock.",
221
226
  " /model <name> Set an explicit model, or /model auto.",
@@ -349,7 +354,7 @@ function printStatus(state) {
349
354
  printSystemLine(`provider=${state.provider || "auto"} routing=${state.routingMode} model=${state.model || "auto"}`);
350
355
  printSystemLine(`profile=${state.taskProfile} maxSteps=${state.maxSteps}`);
351
356
  printSystemLine(
352
- `shell=${state.allowShellTool} files=${state.allowFileTools} auxiliary=${state.allowAuxiliaryTools} sandbox=${state.sandboxMode} installs=${state.packageInstallPolicy}`
357
+ `shell=${state.allowShellTool} files=${state.allowFileTools} webSearch=${state.allowWebSearch} scouts=${state.allowParallelScouts}:${state.parallelScoutCount} auxiliary=${state.allowAuxiliaryTools} sandbox=${state.sandboxMode} installs=${state.packageInstallPolicy}`
353
358
  );
354
359
  if (state.sandboxMode !== "host") {
355
360
  printSystemLine(`dockerWorkspace=/workspace -> ${state.commandCwd || process.cwd()}`);
@@ -434,6 +439,9 @@ function createState(args = {}) {
434
439
  allowShellTool: args.allowShellTool ?? true,
435
440
  allowFileTools: args.allowFileTools ?? true,
436
441
  allowAuxiliaryTools: args.allowAuxiliaryTools ?? true,
442
+ allowWebSearch: args.allowWebSearch ?? true,
443
+ allowParallelScouts: args.allowParallelScouts ?? true,
444
+ parallelScoutCount: args.parallelScoutCount || 3,
437
445
  allowWrapperTools: args.allowWrapperTools ?? false,
438
446
  allowDestructive: args.allowDestructive ?? false,
439
447
  preferredWrapper: args.preferredWrapper || "codex",
@@ -596,6 +604,22 @@ async function handleCommand(line, state, packageDir) {
596
604
  printSystemLine(`profile=${state.taskProfile}`);
597
605
  return true;
598
606
  }
607
+ if (command === "web-search") {
608
+ state.allowWebSearch = value !== "off";
609
+ printSystemLine(`webSearch=${state.allowWebSearch ? "on" : "off"}`);
610
+ return true;
611
+ }
612
+ if (command === "scouts") {
613
+ if (value === "off") {
614
+ state.allowParallelScouts = false;
615
+ } else {
616
+ state.allowParallelScouts = true;
617
+ const count = Number(value);
618
+ if (Number.isFinite(count) && count > 0) state.parallelScoutCount = Math.min(Math.max(count, 1), 4);
619
+ }
620
+ printSystemLine(`parallelScouts=${state.allowParallelScouts ? "on" : "off"} count=${state.parallelScoutCount}`);
621
+ return true;
622
+ }
599
623
  if (command === "routing") {
600
624
  state.routingMode = value || "smart";
601
625
  printSystemLine(`routing=${state.routingMode}`);
@@ -673,6 +697,13 @@ async function handleCommand(line, state, packageDir) {
673
697
 
674
698
  async function runPrompt(prompt, state, packageDir) {
675
699
  const controller = new AbortController();
700
+ const runMaxSteps = Math.max(
701
+ state.maxSteps,
702
+ recommendedMaxStepsForTask({
703
+ goal: prompt,
704
+ taskProfile: state.taskProfile,
705
+ })
706
+ );
676
707
  const config = loadConfig(
677
708
  {
678
709
  provider: state.provider,
@@ -684,11 +715,14 @@ async function runPrompt(prompt, state, packageDir) {
684
715
  allowShellTool: state.allowShellTool,
685
716
  allowFileTools: state.allowFileTools,
686
717
  allowAuxiliaryTools: state.allowAuxiliaryTools,
718
+ allowWebSearch: state.allowWebSearch,
719
+ allowParallelScouts: state.allowParallelScouts,
720
+ parallelScoutCount: state.parallelScoutCount,
687
721
  allowWrapperTools: state.allowWrapperTools,
688
722
  allowDestructive: state.allowDestructive,
689
723
  preferredWrapper: state.preferredWrapper,
690
724
  taskProfile: state.taskProfile,
691
- maxSteps: state.maxSteps,
725
+ maxSteps: runMaxSteps,
692
726
  headless: state.headless,
693
727
  resume: state.sessionId,
694
728
  goal: prompt,
@@ -2,6 +2,7 @@ import OpenAI from "openai";
2
2
  import { normalizeWrapperName, wrapperStatusText } from "./tool-wrappers.js";
3
3
  import { getTaskProfile } from "./task-profiles.js";
4
4
  import { listAuxiliarySkills } from "./auxiliary-tools.js";
5
+ import { engineeringGuidanceForTask } from "./engineering-guidance.js";
5
6
 
6
7
  export function createClient(config) {
7
8
  if (config.provider === "mock") {
@@ -121,6 +122,16 @@ function mockWorkspaceToolForGoal(goal = "") {
121
122
  return null;
122
123
  }
123
124
 
125
+ function mockWebSearchToolForGoal(goal = "") {
126
+ const text = String(goal || "");
127
+ if (!/\b(web search|search web|search the web|look up|current|latest|recent|docs|documentation|online)\b/i.test(text)) return null;
128
+ const query = text.replace(/\s+/g, " ").slice(0, 180);
129
+ return mockToolCall("web_search", {
130
+ query,
131
+ maxResults: 3,
132
+ });
133
+ }
134
+
124
135
  function mockPreviewToolForGoal(goal = "") {
125
136
  const text = String(goal).toLowerCase();
126
137
  if (!/(open|preview|view|browser|website|web\s*site)/.test(text)) return null;
@@ -173,6 +184,7 @@ function mockChatResponse(content, toolCalls = []) {
173
184
 
174
185
  export async function createPlan(client, config, state) {
175
186
  const taskProfile = getTaskProfile(config.taskProfile);
187
+ const engineeringGuidance = engineeringGuidanceForTask(state.goal, config.taskProfile);
176
188
  if (client.mock) {
177
189
  return [
178
190
  "1. Inspect the request and prefer the local shell when available.",
@@ -211,7 +223,14 @@ export async function createPlan(client, config, state) {
211
223
  .map((skill) => `${skill.id} via ${skill.toolName} (${skill.available ? "key available" : `needs ${skill.keyName}`})`)
212
224
  .join(", ")}. For raster image generation requests, plan to use generate_image when a GRSAI key is available; otherwise ask the user to run /auxilliary grsai or aginti login grsai.`
213
225
  : "Auxiliary skills are disabled for this run.",
226
+ config.allowWebSearch
227
+ ? "web_search is available for current information, docs, install errors, package/toolchain questions, and source discovery. Prefer web_search over opening a search engine in the browser."
228
+ : "web_search is disabled for this run.",
229
+ config.allowParallelScouts
230
+ ? `Parallel scout notes may be injected before execution for complex tasks. Scout count: ${config.parallelScoutCount}.`
231
+ : "Parallel scouts are disabled.",
214
232
  `Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
233
+ engineeringGuidance,
215
234
  "A canvas/artifacts tunnel is available through send_to_canvas. Use it when an output should be highlighted visually, such as screenshots, image files, important markdown, diffs, or generated artifact paths. It is optional for ordinary text answers.",
216
235
  "Work like a practical coding agent: inspect when useful, edit with file tools, run safe checks when they add confidence, and keep outputs inside the workspace.",
217
236
  "For large apps, websites, LaTeX documents, Python/C/shell projects, or system tasks, plan a coherent minimal implementation, then use tools to create files, run checks, and publish artifacts.",
@@ -359,6 +378,26 @@ export async function requestNextStep(client, config, messages) {
359
378
  },
360
379
  ];
361
380
 
381
+ if (config.allowWebSearch !== false) {
382
+ tools.splice(-1, 0, {
383
+ type: "function",
384
+ function: {
385
+ name: "web_search",
386
+ description:
387
+ "Search the public web for current information, documentation, install errors, package/toolchain guidance, and source discovery. Returns compact result titles, URLs, and snippets. Prefer this over browser search-engine navigation; open specific results only when needed.",
388
+ parameters: {
389
+ type: "object",
390
+ properties: {
391
+ query: { type: "string", description: "Search query. Do not include secrets or tokens." },
392
+ maxResults: { type: "integer", description: "Number of results, 1 to 10. Defaults to 5." },
393
+ },
394
+ required: ["query"],
395
+ additionalProperties: false,
396
+ },
397
+ },
398
+ });
399
+ }
400
+
362
401
  if (config.allowFileTools) {
363
402
  tools.splice(
364
403
  0,
@@ -634,6 +673,9 @@ export async function requestNextStep(client, config, messages) {
634
673
  Array.isArray(toolPayload.recommendedReads) && toolPayload.recommendedReads.length
635
674
  ? `Recommended reads: ${toolPayload.recommendedReads.join(", ")}`
636
675
  : "",
676
+ Array.isArray(toolPayload.results) && toolPayload.results.length
677
+ ? `Results:\n${toolPayload.results.map((item, index) => `${index + 1}. ${item.title} ${item.url}`).join("\n")}`
678
+ : "",
637
679
  toolPayload.path ? `Path: ${toolPayload.path}` : "",
638
680
  Array.isArray(toolPayload.changes)
639
681
  ? toolPayload.changes
@@ -674,6 +716,13 @@ export async function requestNextStep(client, config, messages) {
674
716
  }
675
717
  }
676
718
 
719
+ if (config.allowWebSearch !== false) {
720
+ const webSearchTool = mockWebSearchToolForGoal(config.goal);
721
+ if (webSearchTool) {
722
+ return mockChatResponse("Mock mode will exercise the web search tool.", [webSearchTool]);
723
+ }
724
+ }
725
+
677
726
  const canvasTool = mockCanvasToolForGoal(config.goal);
678
727
  if (canvasTool) {
679
728
  return mockChatResponse("Mock mode will publish a canvas artifact for the UI tunnel.", [canvasTool]);
@@ -29,12 +29,35 @@ const COMPLEXITY_KEYWORDS = [
29
29
  "docker",
30
30
  "ci",
31
31
  "github",
32
+ "system",
33
+ "systemd",
34
+ "permission denied",
35
+ "install",
36
+ "setup",
37
+ "toolchain",
38
+ "conda",
39
+ "venv",
40
+ "kubernetes",
41
+ "nginx",
42
+ "postgres",
43
+ "redis",
44
+ "segfault",
45
+ "typescript",
46
+ "python",
47
+ "rust",
48
+ "cargo",
49
+ "golang",
50
+ "cmake",
51
+ "gradle",
52
+ "maven",
32
53
  ];
33
54
 
34
55
  const COMPLEX_ROUTE_HINTS = [
35
56
  /\b(large|big|complex|complicated)\s+(repo|repository|codebase|project|task)\b/i,
36
57
  /\b(multi[- ]file|cross[- ]file|repo[- ]wide|workspace[- ]wide)\b/i,
37
58
  /\b(root cause|regression|failing tests?|fix the build|make it pass)\b/i,
59
+ /\b(system bug|system problem|permission denied|service failed|daemon|systemd|toolchain|install|setup)\b/i,
60
+ /\b(conda|venv|python|node|typescript|rust|cargo|golang|java|gradle|maven|cmake|c\+\+)\b.*\b(project|app|tests?|build|compile|fix)\b/i,
38
61
  /\blatex\b/i,
39
62
  /\btexlive\b/i,
40
63
  /\bpdflatex\b/i,
@@ -0,0 +1,126 @@
1
+ import { engineeringGuidanceForTask } from "./engineering-guidance.js";
2
+ import { getModelPresets } from "./model-routing.js";
3
+ import { getTaskProfile, normalizeTaskProfile } from "./task-profiles.js";
4
+ import { redactSensitiveText } from "./redaction.js";
5
+
6
+ const SCOUTS = [
7
+ {
8
+ name: "architect",
9
+ prompt:
10
+ "Decompose the task into independent workstreams. Identify the first files, manifests, logs, or commands the executor should inspect. Keep it actionable.",
11
+ },
12
+ {
13
+ name: "implementer",
14
+ prompt:
15
+ "Predict the most likely implementation path across languages/toolchains. Suggest patch boundaries and focused checks. Avoid writing full code.",
16
+ },
17
+ {
18
+ name: "reviewer",
19
+ prompt:
20
+ "Find instruction-compliance risks, missing tests, system/environment pitfalls, and stop conditions. Suggest how to avoid loops.",
21
+ },
22
+ {
23
+ name: "researcher",
24
+ prompt:
25
+ "If current external information may matter, suggest exact web_search queries and source types. Otherwise say no web search needed.",
26
+ },
27
+ ];
28
+
29
+ function shouldUseComplexScouts(config, state) {
30
+ const profile = normalizeTaskProfile(config.taskProfile);
31
+ const goal = String(config.goal || state.goal || "");
32
+ return (
33
+ Number(config.routeComplexityScore || 0) >= 3 ||
34
+ ["large-codebase", "maintenance", "code"].includes(profile) ||
35
+ /\b(large|complex|complicated|debug|bug|failing|system|install|setup|codebase|repo|multi[- ]file|architecture|refactor|migration)\b/i.test(
36
+ goal
37
+ )
38
+ );
39
+ }
40
+
41
+ export function shouldRunParallelScouts(config, state) {
42
+ if (config.allowParallelScouts === false) return false;
43
+ if (config.provider === "mock") return false;
44
+ if (state?.meta?.parallelScoutsCompleted) return false;
45
+ return shouldUseComplexScouts(config, state || {});
46
+ }
47
+
48
+ function scoutModel(config) {
49
+ if (config.provider === "deepseek") return getModelPresets().fast.model;
50
+ return config.model;
51
+ }
52
+
53
+ function scoutMessages(config, state, scout) {
54
+ const profile = getTaskProfile(config.taskProfile);
55
+ const guidance = engineeringGuidanceForTask(config.goal || state.goal || "", config.taskProfile);
56
+ return [
57
+ {
58
+ role: "system",
59
+ content:
60
+ "You are a parallel scout for AgInTiFlow. Your answer is advisory only: do not claim work is done, do not ask questions, do not use tools, and keep under 180 words.",
61
+ },
62
+ {
63
+ role: "user",
64
+ content: [
65
+ `Scout role: ${scout.name}`,
66
+ scout.prompt,
67
+ `Goal: ${config.goal || state.goal || ""}`,
68
+ `Task profile: ${profile.label}. ${profile.prompt}`,
69
+ guidance,
70
+ `Workspace: ${config.commandCwd}`,
71
+ `Sandbox: ${config.sandboxMode}; package policy: ${config.packageInstallPolicy}`,
72
+ `Current plan:\n${state.plan || "(none yet)"}`,
73
+ ]
74
+ .filter(Boolean)
75
+ .join("\n"),
76
+ },
77
+ ];
78
+ }
79
+
80
+ export async function runParallelScouts(client, config, state) {
81
+ const count = Math.min(Math.max(Number(config.parallelScoutCount) || 3, 1), SCOUTS.length);
82
+ const selected = SCOUTS.slice(0, count);
83
+ const model = scoutModel(config);
84
+ const settled = await Promise.allSettled(
85
+ selected.map(async (scout) => {
86
+ const response = await client.chat.completions.create(
87
+ {
88
+ model,
89
+ temperature: 0,
90
+ messages: scoutMessages(config, state, scout),
91
+ },
92
+ config.abortSignal ? { signal: config.abortSignal } : undefined
93
+ );
94
+ return {
95
+ name: scout.name,
96
+ model,
97
+ content: redactSensitiveText(response.choices[0]?.message?.content || "").trim(),
98
+ };
99
+ })
100
+ );
101
+
102
+ const scouts = settled.map((item, index) => {
103
+ if (item.status === "fulfilled") return item.value;
104
+ return {
105
+ name: selected[index]?.name || `scout-${index + 1}`,
106
+ model,
107
+ error: redactSensitiveText(item.reason instanceof Error ? item.reason.message : String(item.reason)),
108
+ };
109
+ });
110
+ const completed = scouts.filter((scout) => scout.content).length;
111
+ const summary = [
112
+ "Parallel scout notes. Treat these as advisory, not completed work.",
113
+ ...scouts.map((scout) =>
114
+ scout.content ? `\n## ${scout.name}\n${scout.content}` : `\n## ${scout.name}\nScout failed: ${scout.error || "unknown error"}`
115
+ ),
116
+ ].join("\n");
117
+
118
+ return {
119
+ ok: completed > 0,
120
+ model,
121
+ requested: selected.length,
122
+ completed,
123
+ scouts,
124
+ summary,
125
+ };
126
+ }
@@ -3,15 +3,15 @@ export const TASK_PROFILES = {
3
3
  id: "auto",
4
4
  label: "Auto",
5
5
  prompt:
6
- "Infer the task type from the user request. Prefer the smallest safe tool sequence that actually completes the work, preserve workspace files, run useful checks, and summarize what changed.",
7
- tools: ["browser", "shell", "files", "canvas"],
6
+ "Infer the task type from the user request. For short tasks, use the smallest safe tool sequence that completes the work. For codebase, system, debugging, migration, or multi-language tasks, switch into the engineering loop: inspect, read/search exact context, patch incrementally, run focused checks, repair failures, and summarize changed files plus residual risks.",
7
+ tools: ["browser", "shell", "files", "canvas", "inspect_project"],
8
8
  },
9
9
  code: {
10
10
  id: "code",
11
11
  label: "Code writing",
12
12
  prompt:
13
- "Act like a coding agent: understand the request, edit workspace files, run useful safe checks, iterate on failures, and report changed files and residual risks.",
14
- tools: ["files", "shell", "sandbox"],
13
+ "Act like a coding agent across languages: inspect project manifests and conventions, edit workspace files with patches, run useful focused checks, iterate on failures, and report changed files and residual risks.",
14
+ tools: ["inspect_project", "files", "shell", "sandbox"],
15
15
  },
16
16
  "large-codebase": {
17
17
  id: "large-codebase",
@@ -87,8 +87,8 @@ export const TASK_PROFILES = {
87
87
  id: "maintenance",
88
88
  label: "System maintenance",
89
89
  prompt:
90
- "For system maintenance, diagnose first, use Docker for broad installs when available, and follow the configured trust/package policy for host-level changes.",
91
- tools: ["shell", "sandbox", "files"],
90
+ "For system maintenance and system bugs, diagnose first with read-only evidence, use Docker for broad installs/toolchains when available, generate reversible project-local scripts, follow the configured trust/package policy for host-level changes, and stop with clear next actions if stronger permission is needed.",
91
+ tools: ["shell", "sandbox", "files", "inspect_project"],
92
92
  },
93
93
  };
94
94
 
package/src/web-db.js CHANGED
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
  import { DatabaseSync } from "node:sqlite";
4
4
  import { getModelPresets } from "./model-routing.js";
5
5
 
6
- const PREFERENCES_SCHEMA_VERSION = 4;
6
+ const PREFERENCES_SCHEMA_VERSION = 6;
7
7
 
8
8
  function defaultPreferences(baseDir) {
9
9
  const presets = getModelPresets();
@@ -19,6 +19,10 @@ function defaultPreferences(baseDir) {
19
19
  commandCwd: path.resolve(baseDir),
20
20
  allowShellTool: true,
21
21
  allowFileTools: true,
22
+ allowAuxiliaryTools: true,
23
+ allowWebSearch: true,
24
+ allowParallelScouts: true,
25
+ parallelScoutCount: 3,
22
26
  allowWrapperTools: false,
23
27
  preferredWrapper: "codex",
24
28
  wrapperTimeoutMs: 120000,
@@ -95,6 +99,14 @@ export class WebDatabase {
95
99
  if (!Number.isFinite(Number(parsed.maxSteps)) || Number(parsed.maxSteps) < 24) {
96
100
  preferences.maxSteps = 24;
97
101
  }
102
+ if ((parsed.preferencesSchemaVersion || 1) < 5) {
103
+ preferences.taskProfile = "auto";
104
+ }
105
+ if ((parsed.preferencesSchemaVersion || 1) < 6) {
106
+ preferences.allowWebSearch = true;
107
+ preferences.allowParallelScouts = true;
108
+ preferences.parallelScoutCount = 3;
109
+ }
98
110
  this.savePreferences(preferences);
99
111
  }
100
112
  return preferences;
@@ -0,0 +1,111 @@
1
+ import { isDomainAllowed } from "./guardrails.js";
2
+ import { redactSensitiveText } from "./redaction.js";
3
+
4
+ const MAX_QUERY_BYTES = 500;
5
+ const MAX_RESULTS = 10;
6
+
7
+ function decodeHtml(value = "") {
8
+ return String(value)
9
+ .replace(/&amp;/g, "&")
10
+ .replace(/&lt;/g, "<")
11
+ .replace(/&gt;/g, ">")
12
+ .replace(/&quot;/g, '"')
13
+ .replace(/&#39;/g, "'")
14
+ .replace(/&#x27;/g, "'")
15
+ .replace(/<[^>]+>/g, "")
16
+ .replace(/\s+/g, " ")
17
+ .trim();
18
+ }
19
+
20
+ function normalizeDuckDuckGoHref(href = "") {
21
+ const decoded = decodeHtml(href);
22
+ try {
23
+ const parsed = new URL(decoded, "https://duckduckgo.com");
24
+ const uddg = parsed.searchParams.get("uddg");
25
+ if (uddg) return decodeURIComponent(uddg);
26
+ if (/^https?:\/\//i.test(decoded)) return decoded;
27
+ return parsed.href;
28
+ } catch {
29
+ return decoded;
30
+ }
31
+ }
32
+
33
+ function parseDuckDuckGoHtml(html, maxResults, allowedDomains = []) {
34
+ const results = [];
35
+ const blocks = String(html || "").split(/<div class="result\b/i).slice(1);
36
+ for (const block of blocks) {
37
+ const anchor = block.match(/<a[^>]+class="[^"]*result__a[^"]*"[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/i);
38
+ if (!anchor) continue;
39
+ const url = normalizeDuckDuckGoHref(anchor[1]);
40
+ if (!/^https?:\/\//i.test(url)) continue;
41
+ if (!isDomainAllowed(url, allowedDomains)) continue;
42
+ const snippet = block.match(/<a[^>]+class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/a>/i) ||
43
+ block.match(/<div[^>]+class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/div>/i);
44
+ results.push({
45
+ title: decodeHtml(anchor[2]).slice(0, 220),
46
+ url,
47
+ snippet: snippet ? decodeHtml(snippet[1]).slice(0, 420) : "",
48
+ });
49
+ if (results.length >= maxResults) break;
50
+ }
51
+ return results;
52
+ }
53
+
54
+ export async function searchWeb(args = {}, config = {}) {
55
+ const query = String(args.query || "").trim();
56
+ if (!query) return { ok: false, toolName: "web_search", error: "Search query is required." };
57
+ if (Buffer.byteLength(query, "utf8") > MAX_QUERY_BYTES) {
58
+ return { ok: false, toolName: "web_search", error: "Search query is too large." };
59
+ }
60
+
61
+ const maxResults = Math.min(Math.max(Number(args.maxResults) || 5, 1), MAX_RESULTS);
62
+ const searchUrl = `https://duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
63
+ if (config.webSearchDryRun) {
64
+ return {
65
+ ok: true,
66
+ toolName: "web_search",
67
+ query,
68
+ provider: "duckduckgo-html",
69
+ searchUrl,
70
+ dryRun: true,
71
+ results: [
72
+ {
73
+ title: `Dry-run search result for ${query}`,
74
+ url: "https://example.com/agintiflow-web-search-smoke",
75
+ snippet: "Deterministic web_search dry-run result.",
76
+ },
77
+ ].slice(0, maxResults),
78
+ };
79
+ }
80
+
81
+ try {
82
+ const response = await fetch(searchUrl, {
83
+ signal: config.abortSignal || AbortSignal.timeout(Number(args.timeoutMs) || 12000),
84
+ headers: {
85
+ "User-Agent": "AgInTiFlow/1.0 (+https://flow.lazying.art)",
86
+ Accept: "text/html,application/xhtml+xml",
87
+ },
88
+ });
89
+ const html = await response.text();
90
+ const results = parseDuckDuckGoHtml(html, maxResults, config.allowedDomains || []);
91
+ return {
92
+ ok: true,
93
+ toolName: "web_search",
94
+ query: redactSensitiveText(query),
95
+ provider: "duckduckgo-html",
96
+ status: response.status,
97
+ searchUrl,
98
+ results,
99
+ note: results.length ? "" : "No results parsed. The search URL is included as fallback.",
100
+ };
101
+ } catch (error) {
102
+ return {
103
+ ok: false,
104
+ toolName: "web_search",
105
+ query: redactSensitiveText(query),
106
+ provider: "duckduckgo-html",
107
+ searchUrl,
108
+ error: redactSensitiveText(error instanceof Error ? error.message : String(error)),
109
+ };
110
+ }
111
+ }
package/web.js CHANGED
@@ -118,6 +118,7 @@ function normalizePreferencePayload(body = {}, current = db.getPreferences()) {
118
118
  const providerDefaults = getProviderDefaults(provider);
119
119
  const parsedMaxSteps = Number(body.maxSteps);
120
120
  const parsedWrapperTimeoutMs = Number(body.wrapperTimeoutMs);
121
+ const parsedParallelScoutCount = Number(body.parallelScoutCount);
121
122
  const sandboxMode = normalizeSandboxMode(body.sandboxMode || current.sandboxMode || "docker-workspace");
122
123
 
123
124
  return {
@@ -143,6 +144,14 @@ function normalizePreferencePayload(body = {}, current = db.getPreferences()) {
143
144
  allowFileTools: typeof body.allowFileTools === "boolean" ? body.allowFileTools : current.allowFileTools !== false,
144
145
  allowAuxiliaryTools:
145
146
  typeof body.allowAuxiliaryTools === "boolean" ? body.allowAuxiliaryTools : current.allowAuxiliaryTools !== false,
147
+ allowWebSearch:
148
+ typeof body.allowWebSearch === "boolean" ? body.allowWebSearch : current.allowWebSearch !== false,
149
+ allowParallelScouts:
150
+ typeof body.allowParallelScouts === "boolean" ? body.allowParallelScouts : current.allowParallelScouts !== false,
151
+ parallelScoutCount:
152
+ Number.isFinite(parsedParallelScoutCount) && parsedParallelScoutCount > 0
153
+ ? Math.min(Math.max(parsedParallelScoutCount, 1), 4)
154
+ : Number(current.parallelScoutCount) || 3,
146
155
  allowWrapperTools:
147
156
  typeof body.allowWrapperTools === "boolean" ? body.allowWrapperTools : Boolean(current.allowWrapperTools),
148
157
  preferredWrapper: normalizeWrapperName(body.preferredWrapper || current.preferredWrapper || "codex"),
@@ -221,6 +230,9 @@ function buildRunConfig(body, overrides = {}) {
221
230
  allowShellTool: merged.allowShellTool,
222
231
  allowFileTools: merged.allowFileTools,
223
232
  allowAuxiliaryTools: merged.allowAuxiliaryTools,
233
+ allowWebSearch: merged.allowWebSearch,
234
+ allowParallelScouts: merged.allowParallelScouts,
235
+ parallelScoutCount: merged.parallelScoutCount,
224
236
  allowWrapperTools: merged.allowWrapperTools,
225
237
  preferredWrapper: merged.preferredWrapper,
226
238
  wrapperTimeoutMs: merged.wrapperTimeoutMs,