@lazyingart/agintiflow 0.11.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
@@ -73,6 +73,10 @@ For code edits, AgInTiFlow routes patch/refactor/database-style tasks to DeepSee
73
73
 
74
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
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`.
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
 
78
82
  Launch the local web UI from an installed package:
@@ -64,6 +64,33 @@ aginti "repair the Docker setup and run the Node tests"
64
64
 
65
65
  These route to DeepSeek v4 pro when the complexity score is high enough.
66
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
+
67
94
  ## Cross-Language Playbook
68
95
 
69
96
  AgInTiFlow gives DeepSeek stack-specific reminders without hardcoding a solution:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.11.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");
@@ -1003,6 +1009,9 @@ function formPayload() {
1003
1009
  allowShellTool: document.querySelector("#allowShellTool").checked,
1004
1010
  allowFileTools: document.querySelector("#allowFileTools").checked,
1005
1011
  allowAuxiliaryTools: allowAuxiliaryToolsField?.checked ?? true,
1012
+ allowWebSearch: allowWebSearchField?.checked ?? true,
1013
+ allowParallelScouts: allowParallelScoutsField?.checked ?? true,
1014
+ parallelScoutCount: Number(parallelScoutCountField?.value) || 3,
1006
1015
  allowWrapperTools: allowWrapperToolsField.checked,
1007
1016
  preferredWrapper: preferredWrapperField.value,
1008
1017
  taskProfile: taskProfileField?.value || "auto",
@@ -2196,6 +2205,9 @@ async function loadConfig() {
2196
2205
  document.querySelector("#allowShellTool").checked = prefs.allowShellTool ?? true;
2197
2206
  document.querySelector("#allowFileTools").checked = prefs.allowFileTools ?? true;
2198
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);
2199
2211
  allowWrapperToolsField.checked = prefs.allowWrapperTools ?? false;
2200
2212
  preferredWrapperField.value = prefs.preferredWrapper || "codex";
2201
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" />
@@ -7,7 +7,9 @@ import { repairModelMessageHistory, runAgent } from "../src/agent-runner.js";
7
7
  import { resolveRuntimeConfig } from "../src/config.js";
8
8
  import { engineeringGuidanceForTask, recommendedMaxStepsForTask } from "../src/engineering-guidance.js";
9
9
  import { selectModelRoute } from "../src/model-routing.js";
10
+ import { shouldRunParallelScouts } from "../src/parallel-scouts.js";
10
11
  import { SessionStore } from "../src/session-store.js";
12
+ import { searchWeb } from "../src/web-search.js";
11
13
  import { executeWorkspaceTool } from "../src/workspace-tools.js";
12
14
 
13
15
  const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -112,6 +114,24 @@ try {
112
114
  const guidance = engineeringGuidanceForTask("debug this Python project system bug and fix failing tests", "auto");
113
115
  assert(guidance.includes("Python:"), "engineering guidance did not include Python stack advice");
114
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");
115
135
 
116
136
  await fs.mkdir(path.join(workspace, "src"), { recursive: true });
117
137
  await fs.mkdir(path.join(workspace, "test"), { recursive: true });
@@ -285,6 +305,8 @@ try {
285
305
  "large_profile_pro_route",
286
306
  "auto_system_pro_route",
287
307
  "auto_engineering_guidance",
308
+ "parallel_scout_trigger",
309
+ "web_search_dry_run",
288
310
  "inspect_project",
289
311
  "mock_inspect_project",
290
312
  "write_file",
@@ -19,6 +19,8 @@ import { normalizeCanvasPayload } from "./artifact-tunnel.js";
19
19
  import { getTaskProfile } from "./task-profiles.js";
20
20
  import { generateImage, listAuxiliarySkills } from "./auxiliary-tools.js";
21
21
  import { engineeringGuidanceForTask } from "./engineering-guidance.js";
22
+ import { searchWeb } from "./web-search.js";
23
+ import { runParallelScouts, shouldRunParallelScouts } from "./parallel-scouts.js";
22
24
 
23
25
  const exec = promisify(execCallback);
24
26
  const BROWSER_TOOLS = new Set(["open_url", "open_workspace_file", "preview_workspace", "click", "type", "scroll", "press", "back"]);
@@ -284,6 +286,12 @@ function createInitialState(config, sessionId) {
284
286
  .map((skill) => `${skill.id} via ${skill.toolName} (${skill.available ? "key available" : `needs ${skill.keyName}`})`)
285
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.`
286
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.",
287
295
  `Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
288
296
  engineeringGuidance,
289
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.",
@@ -323,6 +331,8 @@ function createInitialState(config, sessionId) {
323
331
  .map((skill) => `${skill.id}:${skill.available ? "available" : "missing-key"}`)
324
332
  .join(" ")}`
325
333
  : "",
334
+ config.allowWebSearch ? "Web search tool: enabled." : "Web search tool: disabled.",
335
+ config.allowParallelScouts ? `Parallel scouts: enabled count=${config.parallelScoutCount}.` : "Parallel scouts: disabled.",
326
336
  `Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
327
337
  engineeringGuidance,
328
338
  "Canvas/artifacts tunnel: available through send_to_canvas for optional frontend rendering.",
@@ -746,6 +756,13 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
746
756
  case "open_url":
747
757
  await abortable(browserState.page.goto(String(args.url), { waitUntil: "domcontentloaded" }), config.abortSignal);
748
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
+ }
749
766
  case "open_workspace_file": {
750
767
  const target = resolveWorkspacePath(config, args.path || args.file || ".");
751
768
  const stat = await fs.stat(target.absolutePath);
@@ -1111,6 +1128,40 @@ export async function runAgent(config) {
1111
1128
  observers.event("plan.created", { plan });
1112
1129
  }
1113
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
+
1114
1165
  const repair = repairModelMessageHistory(state, config);
1115
1166
  if (repair.changed) {
1116
1167
  await store.appendEvent("history.repaired", repair);
@@ -1129,6 +1180,9 @@ export async function runAgent(config) {
1129
1180
  allowShellTool: config.allowShellTool,
1130
1181
  allowWrapperTools: config.allowWrapperTools,
1131
1182
  preferredWrapper: normalizeWrapperName(config.preferredWrapper),
1183
+ allowWebSearch: config.allowWebSearch,
1184
+ allowParallelScouts: config.allowParallelScouts,
1185
+ parallelScoutCount: config.parallelScoutCount,
1132
1186
  wrappers: config.allowWrapperTools ? wrapperStatusText() : "",
1133
1187
  workspaceFileTools: summarizeWorkspaceTools(config),
1134
1188
  shellSandbox: config.useDockerSandbox ? "docker" : "host",
@@ -1198,6 +1252,8 @@ export async function runAgent(config) {
1198
1252
  agentWrappersAvailable: config.allowWrapperTools,
1199
1253
  preferredWrapper: normalizeWrapperName(config.preferredWrapper),
1200
1254
  agentWrappers: config.allowWrapperTools ? wrapperStatusText() : "",
1255
+ webSearchAvailable: config.allowWebSearch !== false,
1256
+ parallelScouts: state.meta.parallelScouts || null,
1201
1257
  shellSandbox: config.useDockerSandbox ? "docker" : "host",
1202
1258
  sandboxMode: config.sandboxMode,
1203
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
@@ -46,6 +46,9 @@ export function parseArgs(argv) {
46
46
  allowFileTools: undefined,
47
47
  allowWrapperTools: undefined,
48
48
  allowAuxiliaryTools: undefined,
49
+ allowWebSearch: undefined,
50
+ allowParallelScouts: undefined,
51
+ parallelScoutCount: undefined,
49
52
  allowDestructive: undefined,
50
53
  preferredWrapper: "",
51
54
  taskProfile: "",
@@ -183,6 +186,27 @@ export function parseArgs(argv) {
183
186
  result.allowAuxiliaryTools = false;
184
187
  continue;
185
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
+ }
186
210
  if (arg === "--allow-wrappers") {
187
211
  result.allowWrapperTools = true;
188
212
  continue;
@@ -237,7 +261,7 @@ export function parseArgs(argv) {
237
261
 
238
262
  function printUsage() {
239
263
  console.log(
240
- '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"'
241
265
  );
242
266
  }
243
267
 
@@ -254,6 +278,9 @@ function agentDefaults(args) {
254
278
  allowShellTool: args.allowShellTool ?? true,
255
279
  allowFileTools: args.allowFileTools ?? true,
256
280
  allowAuxiliaryTools: args.allowAuxiliaryTools ?? true,
281
+ allowWebSearch: args.allowWebSearch ?? true,
282
+ allowParallelScouts: args.allowParallelScouts ?? true,
283
+ parallelScoutCount: args.parallelScoutCount || 3,
257
284
  sandboxMode: args.sandboxMode || "docker-workspace",
258
285
  packageInstallPolicy: args.packageInstallPolicy || "allow",
259
286
  useDockerSandbox: args.useDockerSandbox ?? true,
package/src/config.js CHANGED
@@ -90,6 +90,12 @@ export function resolveRuntimeConfig(args, overrides = {}) {
90
90
  overrides.allowAuxiliaryTools ?? args.allowAuxiliaryTools ?? process.env.ALLOW_AUXILIARY_TOOLS,
91
91
  true
92
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),
93
99
  preferredWrapper: normalizeWrapperName(
94
100
  overrides.preferredWrapper ?? args.preferredWrapper ?? process.env.PREFERRED_WRAPPER ?? process.env.AGENT_WRAPPER
95
101
  ),
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" };
@@ -37,6 +37,8 @@ const SLASH_COMMANDS = [
37
37
  "/resume",
38
38
  "/sessions",
39
39
  "/profile",
40
+ "/web-search",
41
+ "/scouts",
40
42
  "/routing",
41
43
  "/provider",
42
44
  "/model",
@@ -217,6 +219,8 @@ function printHelp() {
217
219
  " /resume <session-id> Continue a saved session.",
218
220
  " /sessions List recent sessions in this project.",
219
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.",
220
224
  " /routing <mode> Set routing: smart, fast, complex, manual.",
221
225
  " /provider <name> Set provider: deepseek, openai, mock.",
222
226
  " /model <name> Set an explicit model, or /model auto.",
@@ -350,7 +354,7 @@ function printStatus(state) {
350
354
  printSystemLine(`provider=${state.provider || "auto"} routing=${state.routingMode} model=${state.model || "auto"}`);
351
355
  printSystemLine(`profile=${state.taskProfile} maxSteps=${state.maxSteps}`);
352
356
  printSystemLine(
353
- `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}`
354
358
  );
355
359
  if (state.sandboxMode !== "host") {
356
360
  printSystemLine(`dockerWorkspace=/workspace -> ${state.commandCwd || process.cwd()}`);
@@ -435,6 +439,9 @@ function createState(args = {}) {
435
439
  allowShellTool: args.allowShellTool ?? true,
436
440
  allowFileTools: args.allowFileTools ?? true,
437
441
  allowAuxiliaryTools: args.allowAuxiliaryTools ?? true,
442
+ allowWebSearch: args.allowWebSearch ?? true,
443
+ allowParallelScouts: args.allowParallelScouts ?? true,
444
+ parallelScoutCount: args.parallelScoutCount || 3,
438
445
  allowWrapperTools: args.allowWrapperTools ?? false,
439
446
  allowDestructive: args.allowDestructive ?? false,
440
447
  preferredWrapper: args.preferredWrapper || "codex",
@@ -597,6 +604,22 @@ async function handleCommand(line, state, packageDir) {
597
604
  printSystemLine(`profile=${state.taskProfile}`);
598
605
  return true;
599
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
+ }
600
623
  if (command === "routing") {
601
624
  state.routingMode = value || "smart";
602
625
  printSystemLine(`routing=${state.routingMode}`);
@@ -692,6 +715,9 @@ async function runPrompt(prompt, state, packageDir) {
692
715
  allowShellTool: state.allowShellTool,
693
716
  allowFileTools: state.allowFileTools,
694
717
  allowAuxiliaryTools: state.allowAuxiliaryTools,
718
+ allowWebSearch: state.allowWebSearch,
719
+ allowParallelScouts: state.allowParallelScouts,
720
+ parallelScoutCount: state.parallelScoutCount,
695
721
  allowWrapperTools: state.allowWrapperTools,
696
722
  allowDestructive: state.allowDestructive,
697
723
  preferredWrapper: state.preferredWrapper,
@@ -122,6 +122,16 @@ function mockWorkspaceToolForGoal(goal = "") {
122
122
  return null;
123
123
  }
124
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
+
125
135
  function mockPreviewToolForGoal(goal = "") {
126
136
  const text = String(goal).toLowerCase();
127
137
  if (!/(open|preview|view|browser|website|web\s*site)/.test(text)) return null;
@@ -213,6 +223,12 @@ export async function createPlan(client, config, state) {
213
223
  .map((skill) => `${skill.id} via ${skill.toolName} (${skill.available ? "key available" : `needs ${skill.keyName}`})`)
214
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.`
215
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.",
216
232
  `Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
217
233
  engineeringGuidance,
218
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.",
@@ -362,6 +378,26 @@ export async function requestNextStep(client, config, messages) {
362
378
  },
363
379
  ];
364
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
+
365
401
  if (config.allowFileTools) {
366
402
  tools.splice(
367
403
  0,
@@ -637,6 +673,9 @@ export async function requestNextStep(client, config, messages) {
637
673
  Array.isArray(toolPayload.recommendedReads) && toolPayload.recommendedReads.length
638
674
  ? `Recommended reads: ${toolPayload.recommendedReads.join(", ")}`
639
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
+ : "",
640
679
  toolPayload.path ? `Path: ${toolPayload.path}` : "",
641
680
  Array.isArray(toolPayload.changes)
642
681
  ? toolPayload.changes
@@ -677,6 +716,13 @@ export async function requestNextStep(client, config, messages) {
677
716
  }
678
717
  }
679
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
+
680
726
  const canvasTool = mockCanvasToolForGoal(config.goal);
681
727
  if (canvasTool) {
682
728
  return mockChatResponse("Mock mode will publish a canvas artifact for the UI tunnel.", [canvasTool]);
@@ -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
+ }
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 = 5;
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,
@@ -98,6 +102,11 @@ export class WebDatabase {
98
102
  if ((parsed.preferencesSchemaVersion || 1) < 5) {
99
103
  preferences.taskProfile = "auto";
100
104
  }
105
+ if ((parsed.preferencesSchemaVersion || 1) < 6) {
106
+ preferences.allowWebSearch = true;
107
+ preferences.allowParallelScouts = true;
108
+ preferences.parallelScoutCount = 3;
109
+ }
101
110
  this.savePreferences(preferences);
102
111
  }
103
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,