@lazyingart/agintiflow 0.9.0 → 0.10.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,6 +71,8 @@ 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).
75
+
74
76
  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).
75
77
 
76
78
  Launch the local web UI from an installed package:
@@ -0,0 +1,49 @@
1
+ # Large Codebase Engineering
2
+
3
+ AgInTiFlow now treats large or complicated coding tasks as a different operating mode from short file edits.
4
+
5
+ ## What Was Borrowed
6
+
7
+ Local agent references informed the design:
8
+
9
+ - Codex-style editing: read/search first, apply deterministic patches, record diffs and hashes, then run checks.
10
+ - Copilot-style SDK surfaces: structured tools, session persistence, plan/history/workspace APIs, and explicit permission hooks.
11
+ - Claude/Claw-style safety: project-local status, container-first execution, read-only operations by default, and clear failure recovery.
12
+ - Gemini/Qwen-style extensibility: capability discovery through profiles and tools rather than hardcoding one model behavior.
13
+
14
+ ## Skill vs Tool
15
+
16
+ The `large-codebase` profile is a skill: it changes the model’s engineering behavior. It tells DeepSeek v4 pro to orient first, plan minimally, patch incrementally, and verify.
17
+
18
+ The `inspect_project` function is a tool: it deterministically scans the workspace and returns:
19
+
20
+ - top-level files and directories
21
+ - manifest files such as `package.json`, `pyproject.toml`, `Cargo.toml`, and `go.mod`
22
+ - package scripts
23
+ - likely source and test directories
24
+ - language and extension counts
25
+ - recommended files to read next
26
+
27
+ ## Recommended Loop
28
+
29
+ For complicated tasks, the agent should follow this loop:
30
+
31
+ 1. `inspect_project` to map the repository.
32
+ 2. `read_file` on `AGENTS.md`, `README.md`, and manifests.
33
+ 3. `search_files` for symbols, tests, errors, routes, or config names.
34
+ 4. `read_file` only on the files needed for the change.
35
+ 5. `apply_patch` in small coherent batches.
36
+ 6. `run_command` for the narrowest relevant check first.
37
+ 7. Broaden checks only after the focused check passes.
38
+
39
+ ## CLI And Web Parity
40
+
41
+ Both CLI and web use the same task profile registry and the same model/tool schemas. Use either:
42
+
43
+ ```bash
44
+ aginti --profile large-codebase "fix the failing tests"
45
+ ```
46
+
47
+ or choose **Large codebase engineering** in the web task-profile dropdown.
48
+
49
+ Smart routing sends this profile to DeepSeek v4 pro even when the user prompt is short.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.9.0",
3
+ "version": "0.10.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
@@ -775,6 +775,12 @@ function renderTaskProfiles(selected = "auto") {
775
775
  taskProfileField.value = profiles.some((profile) => profile.id === selected) ? selected : "auto";
776
776
  }
777
777
 
778
+ function recommendedMaxStepsForProfile(profile = "auto") {
779
+ if (profile === "large-codebase") return 36;
780
+ if (profile === "latex") return 30;
781
+ return 24;
782
+ }
783
+
778
784
  function renderWrapperStatus(wrappers = lastWrappers) {
779
785
  lastWrappers = wrappers || [];
780
786
  if (lastWrappers.length === 0) {
@@ -1959,7 +1965,14 @@ sandboxModeField.addEventListener("change", updatePackageWarning);
1959
1965
  packageInstallPolicyField.addEventListener("change", updatePackageWarning);
1960
1966
  allowWrapperToolsField.addEventListener("change", () => renderWrapperStatus());
1961
1967
  preferredWrapperField.addEventListener("change", () => renderWrapperStatus());
1962
- taskProfileField?.addEventListener("change", schedulePreferenceSave);
1968
+ 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
+ }
1974
+ schedulePreferenceSave();
1975
+ });
1963
1976
 
1964
1977
  saveApiKeyButton?.addEventListener("click", async () => {
1965
1978
  const provider = setupProviderField.value || "deepseek";
@@ -86,6 +86,53 @@ try {
86
86
  goal: "patch this large codebase and migrate the database tests",
87
87
  });
88
88
  assert(/pro/i.test(patchRoute.model), "patch/refactor task did not route to DeepSeek pro");
89
+ const largeProfileRoute = selectModelRoute({
90
+ routingMode: "smart",
91
+ provider: "deepseek",
92
+ goal: "fix this bug",
93
+ taskProfile: "large-codebase",
94
+ });
95
+ assert(/pro/i.test(largeProfileRoute.model), "large-codebase profile did not route to DeepSeek pro");
96
+
97
+ await fs.mkdir(path.join(workspace, "src"), { recursive: true });
98
+ await fs.mkdir(path.join(workspace, "test"), { recursive: true });
99
+ await fs.writeFile(
100
+ path.join(workspace, "package.json"),
101
+ JSON.stringify(
102
+ {
103
+ name: "agintiflow-inspect-smoke",
104
+ scripts: {
105
+ test: "node --test test/index.test.js",
106
+ check: "node --check src/index.js",
107
+ },
108
+ },
109
+ null,
110
+ 2
111
+ ),
112
+ "utf8"
113
+ );
114
+ await fs.writeFile(path.join(workspace, "src/index.js"), "export function answer() { return 42; }\n", "utf8");
115
+ await fs.writeFile(path.join(workspace, "test/index.test.js"), "import test from 'node:test';\n", "utf8");
116
+ const inspected = await executeWorkspaceTool(
117
+ "inspect_project",
118
+ { path: ".", maxDepth: 4, limit: 200 },
119
+ {
120
+ commandCwd: workspace,
121
+ allowFileTools: true,
122
+ }
123
+ );
124
+ assert(inspected.ok, "inspect_project failed");
125
+ assert(inspected.manifestFiles.some((item) => item.path === "package.json"), "inspect_project did not find package.json");
126
+ assert(inspected.packageScripts.some((item) => item.name === "test"), "inspect_project did not extract package scripts");
127
+ assert(inspected.sourceDirs.some((item) => item.path === "src"), "inspect_project did not identify src directory");
128
+ assert(inspected.testFiles.some((item) => item.path === "test/index.test.js"), "inspect_project did not identify test file");
129
+ assert(inspected.recommendedReads.includes("package.json"), "inspect_project did not recommend package.json");
130
+
131
+ const inspectRun = await runMock("Inspect this large codebase and recommend next reads.", "coding-inspect");
132
+ assert(
133
+ inspectRun.events.some((event) => event.type === "tool.completed" && event.data?.toolName === "inspect_project"),
134
+ "mock large-codebase run did not use inspect_project"
135
+ );
89
136
 
90
137
  const writeRun = await runMock("Create notes/hello.md with a short coding smoke message.", "coding-write");
91
138
  const written = await fs.readFile(path.join(workspace, "notes/hello.md"), "utf8");
@@ -216,6 +263,9 @@ try {
216
263
  checks: [
217
264
  "deepseek_history_repair",
218
265
  "deepseek_pro_patch_route",
266
+ "large_profile_pro_route",
267
+ "inspect_project",
268
+ "mock_inspect_project",
219
269
  "write_file",
220
270
  "duplicate_write_failed",
221
271
  "resume_session_write",
@@ -272,7 +272,7 @@ function createInitialState(config, sessionId) {
272
272
  : "A host shell command tool is available under the configured trust policy."
273
273
  : "No shell command tool is available.",
274
274
  config.allowFileTools
275
- ? `Workspace file tools are available in ${config.commandCwd}: list_files, read_file, search_files, write_file, apply_patch, open_workspace_file, and preview_workspace. apply_patch supports exact single-file replacements plus Codex-style/unified multi-file patches; prefer it for source edits after reading/searching the relevant context. Always use workspace-relative paths such as plot_fx.svg or docs/report.tex, never absolute host paths. Secret paths, .git internals, node_modules writes, and huge files are blocked. For generated local websites/pages, use open_workspace_file or preview_workspace instead of starting a localhost server inside Docker.`
275
+ ? `Workspace file tools are available in ${config.commandCwd}: inspect_project, list_files, read_file, search_files, write_file, apply_patch, open_workspace_file, and preview_workspace. For large or unfamiliar repositories, call inspect_project first, then search/read exact files before editing. apply_patch supports exact single-file replacements plus Codex-style/unified multi-file patches; prefer it for source edits after reading/searching the relevant context. Always use workspace-relative paths such as plot_fx.svg or docs/report.tex, never absolute host paths. Secret paths, .git internals, node_modules writes, and huge files are blocked. For generated local websites/pages, use open_workspace_file or preview_workspace instead of starting a localhost server inside Docker.`
276
276
  : "No workspace file tools are available.",
277
277
  config.allowWrapperTools
278
278
  ? `External coding-agent wrappers are available as advisory tools only. Use the selected wrapper only: ${normalizeWrapperName(config.preferredWrapper)}. Wrapper status: ${wrapperStatusText()}.`
@@ -285,8 +285,8 @@ function createInitialState(config, sessionId) {
285
285
  `Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
286
286
  "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
287
  "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
- "Work like a practical coding agent: inspect when useful, patch code with apply_patch, run safe checks when they add confidence, iterate on failures, and keep outputs inside the workspace.",
289
- "For large projects, decompose into useful files and milestones, implement a coherent minimal version first, then iterate with checks rather than only describing what you would do.",
288
+ "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.",
289
+ "For large projects, decompose into useful files and milestones, identify entry points/tests/contracts first, implement a coherent minimal version, then iterate with checks rather than only describing what you would do.",
290
290
  "For website/app/code/LaTeX/Python/C/shell tasks, create or edit real workspace files, run available build/compile/test commands, and surface artifacts through the canvas when useful.",
291
291
  "For research or web-search tasks, use browser tools or safe shell network tools when the current policy allows; cite or save useful sources in workspace notes when the task needs traceability.",
292
292
  "Use the canvas tunnel for outputs the user would likely want to inspect visually, such as figures, PDFs, screenshots, images, important markdown, or generated files.",
@@ -310,7 +310,7 @@ function createInitialState(config, sessionId) {
310
310
  : `Shell working directory: ${config.commandCwd}`
311
311
  : "",
312
312
  config.allowFileTools
313
- ? `Workspace file tools enabled in: ${config.commandCwd}. Use workspace-relative paths. Use apply_patch for code edits; it accepts exact replacements or Codex-style/unified multi-file patches. Local preview tools available: open_workspace_file and preview_workspace.`
313
+ ? `Workspace file tools enabled in: ${config.commandCwd}. Use inspect_project first for large/unfamiliar codebases. Use workspace-relative paths. Use apply_patch for code edits; it accepts exact replacements or Codex-style/unified multi-file patches. Local preview tools available: open_workspace_file and preview_workspace.`
314
314
  : "",
315
315
  config.allowWrapperTools
316
316
  ? `Agent wrappers: selected=${normalizeWrapperName(config.preferredWrapper)}; ${wrapperStatusText()}`
@@ -323,7 +323,7 @@ function createInitialState(config, sessionId) {
323
323
  `Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
324
324
  "Canvas/artifacts tunnel: available through send_to_canvas for optional frontend rendering.",
325
325
  "Visual-output requests should produce a canvas artifact without requiring the user to ask for canvas explicitly.",
326
- "Use file, shell, browser, canvas, and wrapper tools when they are useful; choose the workflow from the user's request.",
326
+ "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.",
327
327
  "Do not stop at a plan when tools can accomplish the request. Continue through implementation, checks, artifact selection, and finish.",
328
328
  "Use the configured sandbox and package policy for environment or system-maintenance work.",
329
329
  ]
@@ -430,7 +430,7 @@ function applyContinuationPrompt(state, config, observers) {
430
430
  : `Shell working directory: ${config.commandCwd}`
431
431
  : "",
432
432
  config.allowFileTools
433
- ? `Workspace file tools enabled in: ${config.commandCwd}. Use workspace-relative paths. For generated local files/sites, use open_workspace_file or preview_workspace.`
433
+ ? `Workspace file tools enabled in: ${config.commandCwd}. Use inspect_project first for large or unfamiliar codebases, then search/read exact files before editing. Use workspace-relative paths. Use apply_patch for code edits; it accepts exact replacements or Codex-style/unified multi-file patches. For generated local files/sites, use open_workspace_file or preview_workspace.`
434
434
  : "",
435
435
  config.allowWrapperTools
436
436
  ? `Agent wrappers: selected=${normalizeWrapperName(config.preferredWrapper)}; ${wrapperStatusText()}`
@@ -631,7 +631,7 @@ async function captureSyntheticSnapshot(store, step, config) {
631
631
  : `Shell tool available in: ${config.commandCwd}`
632
632
  : "Shell tool disabled.",
633
633
  config.allowFileTools
634
- ? `Workspace file tools available in: ${config.commandCwd}. Use workspace-relative paths. Use apply_patch for code edits; it supports exact single-file replacement and multi-file Codex-style/unified patches.`
634
+ ? `Workspace file tools available in: ${config.commandCwd}. Use inspect_project first for large or unfamiliar codebases, then search/read exact files before editing. Use workspace-relative paths. Use apply_patch for code edits; it supports exact single-file replacement and multi-file Codex-style/unified patches.`
635
635
  : "Workspace file tools disabled.",
636
636
  config.allowWrapperTools
637
637
  ? `Agent wrappers available: selected=${normalizeWrapperName(config.preferredWrapper)}; ${wrapperStatusText()}`
@@ -815,6 +815,7 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
815
815
  await new Promise((resolve) => setTimeout(resolve, Number.isFinite(args.ms) ? Number(args.ms) : 1000));
816
816
  }
817
817
  break;
818
+ case "inspect_project":
818
819
  case "list_files":
819
820
  case "read_file":
820
821
  case "search_files":
package/src/cli.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  setProviderKey,
15
15
  showProjectSession,
16
16
  } from "./project.js";
17
- import { listTaskProfiles } from "./task-profiles.js";
17
+ import { defaultMaxStepsForProfile, listTaskProfiles } from "./task-profiles.js";
18
18
  import { promptAndSaveDeepSeekKey, promptHidden, shouldPromptForDeepSeek } from "./auth-onboarding.js";
19
19
  import fs from "node:fs/promises";
20
20
  import path from "node:path";
@@ -256,7 +256,7 @@ function agentDefaults(args) {
256
256
  sandboxMode: args.sandboxMode || "docker-workspace",
257
257
  packageInstallPolicy: args.packageInstallPolicy || "allow",
258
258
  useDockerSandbox: args.useDockerSandbox ?? true,
259
- maxSteps: args.maxSteps || (args.latex || args.taskProfile === "latex" ? 30 : 24),
259
+ maxSteps: args.maxSteps || defaultMaxStepsForProfile(args.taskProfile || (args.latex ? "latex" : "auto")),
260
260
  };
261
261
 
262
262
  if (defaults.sandboxMode === "host") {
package/src/config.js CHANGED
@@ -4,7 +4,7 @@ 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 { normalizeTaskProfile } from "./task-profiles.js";
7
+ import { defaultMaxStepsForProfile, normalizeTaskProfile } from "./task-profiles.js";
8
8
 
9
9
  function parseBoolean(value, fallback) {
10
10
  if (value === undefined) return fallback;
@@ -33,11 +33,13 @@ export function resolveRuntimeConfig(args, overrides = {}) {
33
33
  process.env.AGENT_PROVIDER ||
34
34
  (process.env.DEEPSEEK_API_KEY ? "deepseek" : process.env.OPENAI_API_KEY ? "openai" : "deepseek");
35
35
  const routingMode = normalizeRoutingMode(overrides.routingMode || args.routingMode || process.env.AGENT_ROUTING_MODE || "smart");
36
+ const taskProfile = normalizeTaskProfile(overrides.taskProfile || args.taskProfile || process.env.AGINTI_TASK_PROFILE || "auto");
36
37
  const route = selectModelRoute({
37
38
  routingMode,
38
39
  provider: requestedProvider,
39
40
  model: overrides.model || args.model || process.env.LLM_MODEL || "",
40
41
  goal: args.goal || "",
42
+ taskProfile,
41
43
  });
42
44
 
43
45
  const defaults = getProviderDefaults(route.provider);
@@ -56,7 +58,7 @@ export function resolveRuntimeConfig(args, overrides = {}) {
56
58
  resume: args.resume || "",
57
59
  sessionId: overrides.sessionId || args.sessionId || process.env.SESSION_ID || `web-agent-${crypto.randomUUID()}`,
58
60
  routingMode,
59
- taskProfile: normalizeTaskProfile(overrides.taskProfile || args.taskProfile || process.env.AGINTI_TASK_PROFILE || "auto"),
61
+ taskProfile,
60
62
  routeReason: route.reason,
61
63
  routeComplexityScore: route.complexityScore,
62
64
  requestedProvider,
@@ -65,7 +67,7 @@ export function resolveRuntimeConfig(args, overrides = {}) {
65
67
  apiKey: overrides.apiKey || defaults.apiKey,
66
68
  baseURL: overrides.baseURL || defaults.baseURL,
67
69
  model: route.model || defaults.model,
68
- maxSteps: parseNumber(overrides.maxSteps ?? args.maxSteps ?? process.env.MAX_STEPS, 24),
70
+ maxSteps: parseNumber(overrides.maxSteps ?? args.maxSteps ?? process.env.MAX_STEPS, defaultMaxStepsForProfile(taskProfile)),
69
71
  headless: parseBoolean(overrides.headless ?? args.headless ?? process.env.HEADLESS, false),
70
72
  allowedDomains: Array.isArray(overrides.allowedDomains)
71
73
  ? overrides.allowedDomains
@@ -5,7 +5,7 @@ import { runAgent } from "./agent-runner.js";
5
5
  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
- import { normalizeTaskProfile } from "./task-profiles.js";
8
+ import { defaultMaxStepsForProfile, normalizeTaskProfile } from "./task-profiles.js";
9
9
  import { promptAndSaveDeepSeekKey, promptHidden, shouldPromptForDeepSeek } from "./auth-onboarding.js";
10
10
 
11
11
  const useColor = Boolean(input.isTTY && output.isTTY && process.env.AGINTIFLOW_NO_COLOR !== "1");
@@ -439,7 +439,10 @@ function createState(args = {}) {
439
439
  preferredWrapper: args.preferredWrapper || "codex",
440
440
  taskProfile: normalizeTaskProfile(args.taskProfile || "auto"),
441
441
  headless: args.headless ?? false,
442
- maxSteps: Number.isFinite(args.maxSteps) && args.maxSteps > 0 ? args.maxSteps : args.latex ? 30 : 24,
442
+ maxSteps:
443
+ Number.isFinite(args.maxSteps) && args.maxSteps > 0
444
+ ? args.maxSteps
445
+ : defaultMaxStepsForProfile(args.taskProfile || (args.latex ? "latex" : "auto")),
443
446
  sessionId: args.resume || "",
444
447
  };
445
448
  }
@@ -589,6 +592,7 @@ async function handleCommand(line, state, packageDir) {
589
592
  }
590
593
  if (command === "profile") {
591
594
  state.taskProfile = normalizeTaskProfile(value || "auto");
595
+ state.maxSteps = Math.max(state.maxSteps, defaultMaxStepsForProfile(state.taskProfile));
592
596
  printSystemLine(`profile=${state.taskProfile}`);
593
597
  return true;
594
598
  }
@@ -81,6 +81,13 @@ function mockPathForGoal(goal = "") {
81
81
  function mockWorkspaceToolForGoal(goal = "") {
82
82
  const text = String(goal).toLowerCase();
83
83
  const targetPath = mockPathForGoal(goal);
84
+ if (/inspect|map|overview|architecture|large codebase|large repo|repository|repo\b|codebase/.test(text)) {
85
+ return mockToolCall("inspect_project", {
86
+ path: ".",
87
+ maxDepth: 6,
88
+ limit: 400,
89
+ });
90
+ }
84
91
  if (/patch|replace|edit/.test(text)) {
85
92
  if (/multi|codex|unified|several|multiple/i.test(text)) {
86
93
  return mockToolCall("apply_patch", {
@@ -194,7 +201,7 @@ export async function createPlan(client, config, state) {
194
201
  ? `Shell tool is enabled in ${config.commandCwd}. In Docker, this path is mounted as /workspace with persistent /aginti-env and /aginti-cache mounts. Use relative paths or /workspace paths, not absolute host temp paths. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}. For npm/pip/conda/venv setup, explain the need and wait for approval unless policy is allow.`
195
202
  : "",
196
203
  config.allowFileTools
197
- ? `Workspace file tools are enabled in ${config.commandCwd}: list_files, read_file, search_files, write_file, apply_patch, open_workspace_file, preview_workspace. apply_patch supports exact single-file replacements and Codex-style/unified multi-file patches; prefer it for edits after reading relevant context. Keep all paths workspace-relative, for example plot_fx.svg or docs/report.tex, and avoid secrets. For generated local HTML/SVG/PDF/static sites, plan to use open_workspace_file or preview_workspace rather than starting a localhost server inside Docker.`
204
+ ? `Workspace file tools are enabled in ${config.commandCwd}: inspect_project, list_files, read_file, search_files, write_file, apply_patch, open_workspace_file, preview_workspace. For large or unfamiliar repos, plan to call inspect_project first, then search/read exact files. apply_patch supports exact single-file replacements and Codex-style/unified multi-file patches; prefer it for edits after reading relevant context. Keep all paths workspace-relative, for example plot_fx.svg or docs/report.tex, and avoid secrets. For generated local HTML/SVG/PDF/static sites, plan to use open_workspace_file or preview_workspace rather than starting a localhost server inside Docker.`
198
205
  : "",
199
206
  config.allowWrapperTools
200
207
  ? `Agent wrappers are enabled. Use the selected wrapper only: ${normalizeWrapperName(config.preferredWrapper)}. Status: ${wrapperStatusText()}.`
@@ -414,6 +421,24 @@ export async function requestNextStep(client, config, messages) {
414
421
  tools.splice(
415
422
  -1,
416
423
  0,
424
+ {
425
+ type: "function",
426
+ function: {
427
+ name: "inspect_project",
428
+ description:
429
+ "Build a compact, deterministic map of the workspace for large-codebase work. Returns top-level entries, manifests, source/test directories, package scripts, language counts, and recommended files to read next. Use this before editing an unfamiliar or multi-file repository.",
430
+ parameters: {
431
+ type: "object",
432
+ properties: {
433
+ path: { type: "string", description: "Workspace-relative directory to inspect. Defaults to ." },
434
+ maxDepth: { type: "integer", description: "Recursive depth, 1 to 10. Defaults to 6." },
435
+ limit: { type: "integer", description: "Maximum filesystem entries to inspect." },
436
+ includeFiles: { type: "boolean", description: "Include a compact file list when needed. Defaults to false." },
437
+ },
438
+ additionalProperties: false,
439
+ },
440
+ },
441
+ },
417
442
  {
418
443
  type: "function",
419
444
  function: {
@@ -604,6 +629,11 @@ export async function requestNextStep(client, config, messages) {
604
629
  toolPayload.stderr,
605
630
  toolPayload.error,
606
631
  toolPayload.reason,
632
+ toolPayload.summary ? `Summary: ${toolPayload.summary}` : "",
633
+ toolPayload.counts ? `Counts: ${JSON.stringify(toolPayload.counts)}` : "",
634
+ Array.isArray(toolPayload.recommendedReads) && toolPayload.recommendedReads.length
635
+ ? `Recommended reads: ${toolPayload.recommendedReads.join(", ")}`
636
+ : "",
607
637
  toolPayload.path ? `Path: ${toolPayload.path}` : "",
608
638
  Array.isArray(toolPayload.changes)
609
639
  ? toolPayload.changes
@@ -9,6 +9,16 @@ const COMPLEXITY_KEYWORDS = [
9
9
  "apply_patch",
10
10
  "edit",
11
11
  "large codebase",
12
+ "codebase",
13
+ "monorepo",
14
+ "repository",
15
+ "cross-file",
16
+ "multi file",
17
+ "large repo",
18
+ "engineering",
19
+ "entry point",
20
+ "regression",
21
+ "root cause",
12
22
  "design",
13
23
  "review",
14
24
  "migrate",
@@ -22,6 +32,9 @@ const COMPLEXITY_KEYWORDS = [
22
32
  ];
23
33
 
24
34
  const COMPLEX_ROUTE_HINTS = [
35
+ /\b(large|big|complex|complicated)\s+(repo|repository|codebase|project|task)\b/i,
36
+ /\b(multi[- ]file|cross[- ]file|repo[- ]wide|workspace[- ]wide)\b/i,
37
+ /\b(root cause|regression|failing tests?|fix the build|make it pass)\b/i,
25
38
  /\blatex\b/i,
26
39
  /\btexlive\b/i,
27
40
  /\bpdflatex\b/i,
@@ -102,9 +115,10 @@ export function getModelPresets() {
102
115
  };
103
116
  }
104
117
 
105
- export function scoreTaskComplexity(goal = "") {
118
+ export function scoreTaskComplexity(goal = "", taskProfile = "auto") {
106
119
  const text = String(goal).toLowerCase();
107
120
  let score = text.length > 600 ? 2 : text.length > 240 ? 1 : 0;
121
+ if (["large-codebase", "engineering", "codebase"].includes(String(taskProfile || "").toLowerCase())) score += 3;
108
122
  for (const keyword of COMPLEXITY_KEYWORDS) {
109
123
  if (text.includes(keyword)) score += 1;
110
124
  }
@@ -118,7 +132,7 @@ export function normalizeRoutingMode(value) {
118
132
  return ROUTING_MODES.includes(value) ? value : "smart";
119
133
  }
120
134
 
121
- export function selectModelRoute({ routingMode = "smart", provider = "deepseek", model = "", goal = "" } = {}) {
135
+ export function selectModelRoute({ routingMode = "smart", provider = "deepseek", model = "", goal = "", taskProfile = "auto" } = {}) {
122
136
  const mode = normalizeRoutingMode(routingMode);
123
137
  const presets = getModelPresets();
124
138
 
@@ -129,7 +143,7 @@ export function selectModelRoute({ routingMode = "smart", provider = "deepseek",
129
143
  provider: defaults.provider,
130
144
  model: model || defaults.model,
131
145
  reason: "Local mock route selected for smoke tests and offline UI/API checks.",
132
- complexityScore: scoreTaskComplexity(goal),
146
+ complexityScore: scoreTaskComplexity(goal, taskProfile),
133
147
  };
134
148
  }
135
149
 
@@ -140,7 +154,7 @@ export function selectModelRoute({ routingMode = "smart", provider = "deepseek",
140
154
  provider: defaults.provider,
141
155
  model: model || defaults.model,
142
156
  reason: "Manual provider/model selection.",
143
- complexityScore: scoreTaskComplexity(goal),
157
+ complexityScore: scoreTaskComplexity(goal, taskProfile),
144
158
  };
145
159
  }
146
160
 
@@ -150,7 +164,7 @@ export function selectModelRoute({ routingMode = "smart", provider = "deepseek",
150
164
  provider: presets.complex.provider,
151
165
  model: presets.complex.model,
152
166
  reason: "Complex route selected explicitly.",
153
- complexityScore: scoreTaskComplexity(goal),
167
+ complexityScore: scoreTaskComplexity(goal, taskProfile),
154
168
  };
155
169
  }
156
170
 
@@ -160,11 +174,11 @@ export function selectModelRoute({ routingMode = "smart", provider = "deepseek",
160
174
  provider: presets.fast.provider,
161
175
  model: presets.fast.model,
162
176
  reason: "Fast route selected explicitly.",
163
- complexityScore: scoreTaskComplexity(goal),
177
+ complexityScore: scoreTaskComplexity(goal, taskProfile),
164
178
  };
165
179
  }
166
180
 
167
- const complexityScore = scoreTaskComplexity(goal);
181
+ const complexityScore = scoreTaskComplexity(goal, taskProfile);
168
182
  const selected = complexityScore >= 3 ? presets.complex : presets.fast;
169
183
  return {
170
184
  routingMode: mode,
@@ -13,6 +13,13 @@ export const TASK_PROFILES = {
13
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
14
  tools: ["files", "shell", "sandbox"],
15
15
  },
16
+ "large-codebase": {
17
+ id: "large-codebase",
18
+ label: "Large codebase engineering",
19
+ prompt:
20
+ "For large or complicated engineering work, behave like a senior coding agent: inspect_project first unless the repo is already known, read AGENTS/README/manifests, locate entry points and tests, make a small explicit change plan, patch in coherent batches, run the narrowest relevant checks first, escalate to broader checks when stable, and summarize files changed, checks, tradeoffs, and remaining risks.",
21
+ tools: ["inspect_project", "search_files", "read_file", "apply_patch", "shell", "sandbox", "canvas"],
22
+ },
16
23
  writing: {
17
24
  id: "writing",
18
25
  label: "Book/script writing",
@@ -85,15 +92,32 @@ export const TASK_PROFILES = {
85
92
  },
86
93
  };
87
94
 
95
+ const PROFILE_ALIASES = {
96
+ large: "large-codebase",
97
+ codebase: "large-codebase",
98
+ repo: "large-codebase",
99
+ repository: "large-codebase",
100
+ engineering: "large-codebase",
101
+ engineer: "large-codebase",
102
+ };
103
+
88
104
  export function listTaskProfiles() {
89
105
  return Object.values(TASK_PROFILES);
90
106
  }
91
107
 
92
108
  export function normalizeTaskProfile(value = "auto") {
93
109
  const key = String(value || "auto").trim().toLowerCase();
110
+ if (PROFILE_ALIASES[key]) return PROFILE_ALIASES[key];
94
111
  return TASK_PROFILES[key] ? key : "auto";
95
112
  }
96
113
 
97
114
  export function getTaskProfile(value = "auto") {
98
115
  return TASK_PROFILES[normalizeTaskProfile(value)];
99
116
  }
117
+
118
+ export function defaultMaxStepsForProfile(value = "auto") {
119
+ const profile = normalizeTaskProfile(value);
120
+ if (profile === "large-codebase") return 36;
121
+ if (profile === "latex") return 30;
122
+ return 24;
123
+ }
@@ -3,7 +3,7 @@ import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { redactSensitiveText } from "./redaction.js";
5
5
 
6
- export const WORKSPACE_TOOL_NAMES = ["list_files", "read_file", "search_files", "write_file", "apply_patch"];
6
+ export const WORKSPACE_TOOL_NAMES = ["inspect_project", "list_files", "read_file", "search_files", "write_file", "apply_patch"];
7
7
  export const WORKSPACE_WRITE_TOOL_NAMES = ["write_file", "apply_patch"];
8
8
 
9
9
  const MAX_READ_BYTES = 220_000;
@@ -11,8 +11,51 @@ const MAX_WRITE_BYTES = 220_000;
11
11
  const MAX_PATCH_BYTES = 260_000;
12
12
  const MAX_LIST_ENTRIES = 360;
13
13
  const MAX_SEARCH_RESULTS = 80;
14
+ const MAX_INSPECT_ENTRIES = 1400;
14
15
  const DEFAULT_MAX_DEPTH = 4;
15
- const SKIP_DIRS = new Set([".git", "node_modules", ".sessions"]);
16
+ const SKIP_DIRS = new Set([
17
+ ".git",
18
+ ".sessions",
19
+ ".aginti",
20
+ "node_modules",
21
+ "dist",
22
+ "build",
23
+ "coverage",
24
+ ".next",
25
+ ".nuxt",
26
+ ".cache",
27
+ ".venv",
28
+ "venv",
29
+ "__pycache__",
30
+ ]);
31
+ const IMPORTANT_MANIFESTS = new Set([
32
+ "package.json",
33
+ "package-lock.json",
34
+ "pnpm-lock.yaml",
35
+ "yarn.lock",
36
+ "tsconfig.json",
37
+ "jsconfig.json",
38
+ "vite.config.js",
39
+ "vite.config.ts",
40
+ "next.config.js",
41
+ "pyproject.toml",
42
+ "requirements.txt",
43
+ "requirements-dev.txt",
44
+ "setup.py",
45
+ "Pipfile",
46
+ "Cargo.toml",
47
+ "go.mod",
48
+ "pom.xml",
49
+ "build.gradle",
50
+ "Makefile",
51
+ "Dockerfile",
52
+ "docker-compose.yml",
53
+ "compose.yml",
54
+ "README.md",
55
+ "AGENTS.md",
56
+ ]);
57
+ const SOURCE_DIR_NAMES = new Set(["src", "app", "lib", "packages", "apps", "bin", "scripts", "server", "client", "public", "docs"]);
58
+ const TEST_DIR_NAMES = new Set(["test", "tests", "__tests__", "spec", "specs", "e2e"]);
16
59
  const SENSITIVE_EXTENSIONS = new Set([".key", ".pem", ".p12", ".pfx", ".crt", ".csr"]);
17
60
  const SENSITIVE_BASENAMES = new Set([
18
61
  ".env",
@@ -183,10 +226,68 @@ export function summarizeWorkspaceTools(config) {
183
226
  maxPatchBytes: MAX_PATCH_BYTES,
184
227
  maxListEntries: MAX_LIST_ENTRIES,
185
228
  maxSearchResults: MAX_SEARCH_RESULTS,
229
+ maxInspectEntries: MAX_INSPECT_ENTRIES,
186
230
  },
187
231
  };
188
232
  }
189
233
 
234
+ function languageForExtension(ext) {
235
+ const normalized = String(ext || "").toLowerCase();
236
+ if ([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"].includes(normalized)) return "javascript";
237
+ if ([".py", ".pyw"].includes(normalized)) return "python";
238
+ if ([".rs"].includes(normalized)) return "rust";
239
+ if ([".go"].includes(normalized)) return "go";
240
+ if ([".java", ".kt", ".scala"].includes(normalized)) return "jvm";
241
+ if ([".c", ".cc", ".cpp", ".h", ".hpp"].includes(normalized)) return "c-cpp";
242
+ if ([".sh", ".bash", ".zsh"].includes(normalized)) return "shell";
243
+ if ([".md", ".mdx", ".rst"].includes(normalized)) return "docs";
244
+ if ([".json", ".yaml", ".yml", ".toml", ".ini"].includes(normalized)) return "config";
245
+ if ([".html", ".css", ".scss", ".svg"].includes(normalized)) return "web";
246
+ return normalized ? normalized.slice(1) : "no-extension";
247
+ }
248
+
249
+ function isLikelyTest(relativePath) {
250
+ const normalized = normalizeRelative(relativePath).toLowerCase();
251
+ const base = path.basename(normalized);
252
+ return (
253
+ normalized.split("/").some((segment) => TEST_DIR_NAMES.has(segment)) ||
254
+ /\.(test|spec)\.[cm]?[jt]sx?$/.test(base) ||
255
+ /^test_.*\.py$/.test(base) ||
256
+ /_test\.go$/.test(base) ||
257
+ /test.*\.rs$/.test(base)
258
+ );
259
+ }
260
+
261
+ function sortCounts(map, limit = 16) {
262
+ return [...map.entries()]
263
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
264
+ .slice(0, limit)
265
+ .map(([name, count]) => ({ name, count }));
266
+ }
267
+
268
+ async function readJsonFileSafe(absolutePath) {
269
+ try {
270
+ const stat = await fs.stat(absolutePath);
271
+ if (!stat.isFile() || stat.size > 120_000) return null;
272
+ const buffer = await fs.readFile(absolutePath);
273
+ if (buffer.includes(0)) return null;
274
+ return JSON.parse(buffer.toString("utf8"));
275
+ } catch {
276
+ return null;
277
+ }
278
+ }
279
+
280
+ function recommendedReads(summary) {
281
+ const reads = [];
282
+ for (const name of ["AGENTS.md", "README.md", "package.json", "pyproject.toml", "Cargo.toml", "go.mod"]) {
283
+ const match = summary.manifestFiles.find((item) => item.path === name || item.path.endsWith(`/${name}`));
284
+ if (match) reads.push(match.path);
285
+ }
286
+ for (const dir of summary.sourceDirs.slice(0, 4)) reads.push(dir.path);
287
+ for (const file of summary.testFiles.slice(0, 4)) reads.push(file.path);
288
+ return [...new Set(reads)].slice(0, 12);
289
+ }
290
+
190
291
  async function fileInfo(absolutePath, root) {
191
292
  const stat = await fs.stat(absolutePath);
192
293
  return {
@@ -316,6 +417,133 @@ async function searchFiles(config, args) {
316
417
  };
317
418
  }
318
419
 
420
+ async function inspectProject(config, args) {
421
+ const target = resolveWorkspacePath(config, args.path || ".");
422
+ const maxDepth = Math.min(Math.max(Number(args.maxDepth) || 6, 1), 10);
423
+ const limit = Math.min(Math.max(Number(args.limit) || MAX_INSPECT_ENTRIES, 80), MAX_INSPECT_ENTRIES);
424
+ const includeFiles = Boolean(args.includeFiles);
425
+ const files = [];
426
+ const sourceDirs = [];
427
+ const testFiles = [];
428
+ const manifestFiles = [];
429
+ const extensionCounts = new Map();
430
+ const languageCounts = new Map();
431
+ const topLevel = [];
432
+ let fileCount = 0;
433
+ let dirCount = 0;
434
+ let totalBytes = 0;
435
+ let truncated = false;
436
+
437
+ async function visit(currentPath, depth = 0) {
438
+ if (fileCount + dirCount >= limit) {
439
+ truncated = true;
440
+ return;
441
+ }
442
+
443
+ const stat = await fs.stat(currentPath).catch(() => null);
444
+ if (!stat) return;
445
+ const relativePath = normalizeRelative(path.relative(target.root, currentPath));
446
+ const segments = pathSegments(relativePath);
447
+ const base = path.basename(currentPath);
448
+
449
+ if (relativePath !== ".") {
450
+ const policy = pathPolicy("read_file", relativePath);
451
+ if (!policy.allowed) return;
452
+ if (segments.some((segment) => SKIP_DIRS.has(segment))) return;
453
+ }
454
+
455
+ if (stat.isDirectory()) {
456
+ dirCount += 1;
457
+ if (depth === 1) topLevel.push({ path: relativePath, type: "directory" });
458
+ if (SOURCE_DIR_NAMES.has(base) || TEST_DIR_NAMES.has(base)) {
459
+ sourceDirs.push({ path: relativePath, kind: TEST_DIR_NAMES.has(base) ? "tests" : "source" });
460
+ }
461
+ if (depth >= maxDepth) return;
462
+ const children = await fs.readdir(currentPath, { withFileTypes: true }).catch(() => []);
463
+ children.sort((a, b) => a.name.localeCompare(b.name));
464
+ for (const child of children) {
465
+ if (SKIP_DIRS.has(child.name)) continue;
466
+ await visit(path.join(currentPath, child.name), depth + 1);
467
+ if (truncated) break;
468
+ }
469
+ return;
470
+ }
471
+
472
+ if (!stat.isFile()) return;
473
+ fileCount += 1;
474
+ totalBytes += stat.size;
475
+ const ext = path.extname(base).toLowerCase() || "(none)";
476
+ extensionCounts.set(ext, (extensionCounts.get(ext) || 0) + 1);
477
+ const language = languageForExtension(ext);
478
+ languageCounts.set(language, (languageCounts.get(language) || 0) + 1);
479
+
480
+ const item = {
481
+ path: relativePath,
482
+ size: stat.size,
483
+ modifiedAt: stat.mtime.toISOString(),
484
+ };
485
+ if (depth === 1) topLevel.push({ path: relativePath, type: "file", size: stat.size });
486
+ if (IMPORTANT_MANIFESTS.has(base) || base.endsWith(".config.js") || base.endsWith(".config.ts")) {
487
+ manifestFiles.push(item);
488
+ }
489
+ if (isLikelyTest(relativePath)) testFiles.push(item);
490
+ if (includeFiles) files.push(item);
491
+ }
492
+
493
+ await visit(target.absolutePath, 0);
494
+
495
+ const packageJsonPath = path.join(target.absolutePath, "package.json");
496
+ const packageJson = await readJsonFileSafe(packageJsonPath);
497
+ const packageScripts =
498
+ packageJson && packageJson.scripts && typeof packageJson.scripts === "object"
499
+ ? Object.entries(packageJson.scripts)
500
+ .slice(0, 30)
501
+ .map(([name, command]) => ({ name, command: redactSensitiveText(String(command)).slice(0, 220) }))
502
+ : [];
503
+ const packageManagers = [
504
+ manifestFiles.some((item) => item.path.endsWith("pnpm-lock.yaml")) ? "pnpm" : "",
505
+ manifestFiles.some((item) => item.path.endsWith("yarn.lock")) ? "yarn" : "",
506
+ manifestFiles.some((item) => item.path.endsWith("package-lock.json")) ? "npm" : "",
507
+ manifestFiles.some((item) => item.path.endsWith("pyproject.toml")) ? "python/pyproject" : "",
508
+ manifestFiles.some((item) => item.path.endsWith("Cargo.toml")) ? "cargo" : "",
509
+ manifestFiles.some((item) => item.path.endsWith("go.mod")) ? "go" : "",
510
+ ].filter(Boolean);
511
+
512
+ const summary = {
513
+ ok: true,
514
+ toolName: "inspect_project",
515
+ path: target.relativePath,
516
+ root: target.root,
517
+ summary: `${fileCount} files, ${dirCount} directories, ${sortCounts(languageCounts, 5)
518
+ .map((item) => `${item.name}:${item.count}`)
519
+ .join(" ")}`,
520
+ counts: {
521
+ files: fileCount,
522
+ directories: dirCount,
523
+ totalBytes,
524
+ },
525
+ truncated,
526
+ topLevel: topLevel.slice(0, 80),
527
+ manifestFiles: manifestFiles.slice(0, 80),
528
+ sourceDirs: sourceDirs.slice(0, 60),
529
+ testFiles: testFiles.slice(0, 80),
530
+ extensionCounts: sortCounts(extensionCounts),
531
+ languageCounts: sortCounts(languageCounts),
532
+ packageManagers,
533
+ packageScripts,
534
+ recommendedReads: [],
535
+ engineeringHints: [
536
+ "Read AGENTS/README/manifests before editing.",
537
+ "Use search_files to locate symbols and tests, then read exact files.",
538
+ "Use apply_patch for source edits and run the smallest relevant check first.",
539
+ "If a change spans modules, patch in small batches and verify after each batch.",
540
+ ],
541
+ };
542
+ summary.recommendedReads = recommendedReads(summary);
543
+ if (includeFiles) summary.files = files.slice(0, limit);
544
+ return summary;
545
+ }
546
+
319
547
  function compactDiff(relativePath, beforeText, afterText) {
320
548
  const beforeLines = String(beforeText || "").split("\n");
321
549
  const afterLines = String(afterText || "").split("\n");
@@ -723,6 +951,8 @@ export async function executeWorkspaceTool(toolName, args, config) {
723
951
  }
724
952
 
725
953
  switch (toolName) {
954
+ case "inspect_project":
955
+ return inspectProject(config, args);
726
956
  case "list_files":
727
957
  return listFiles(config, args);
728
958
  case "read_file":