@lazyingart/agintiflow 0.9.0 → 0.11.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 +2 -0
- package/docs/large-codebase-engineering.md +75 -0
- package/package.json +1 -1
- package/public/app.js +24 -1
- package/scripts/smoke-coding-tools.js +71 -0
- package/src/agent-runner.js +14 -7
- package/src/cli.js +7 -1
- package/src/config.js +10 -2
- package/src/engineering-guidance.js +102 -0
- package/src/interactive-cli.js +15 -3
- package/src/model-client.js +34 -1
- package/src/model-routing.js +44 -7
- package/src/task-profiles.js +30 -6
- package/src/web-db.js +4 -1
- package/src/workspace-tools.js +232 -2
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. 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
|
+
|
|
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,75 @@
|
|
|
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
|
+
- 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.
|
|
15
|
+
|
|
16
|
+
## Skill vs Tool
|
|
17
|
+
|
|
18
|
+
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.
|
|
19
|
+
|
|
20
|
+
The `inspect_project` function is a tool: it deterministically scans the workspace and returns:
|
|
21
|
+
|
|
22
|
+
- top-level files and directories
|
|
23
|
+
- manifest files such as `package.json`, `pyproject.toml`, `Cargo.toml`, and `go.mod`
|
|
24
|
+
- package scripts
|
|
25
|
+
- likely source and test directories
|
|
26
|
+
- language and extension counts
|
|
27
|
+
- recommended files to read next
|
|
28
|
+
|
|
29
|
+
## Recommended Loop
|
|
30
|
+
|
|
31
|
+
For complicated tasks, the agent should follow this loop:
|
|
32
|
+
|
|
33
|
+
1. `inspect_project` to map the repository.
|
|
34
|
+
2. `read_file` on `AGENTS.md`, `README.md`, and manifests.
|
|
35
|
+
3. `search_files` for symbols, tests, errors, routes, or config names.
|
|
36
|
+
4. `read_file` only on the files needed for the change.
|
|
37
|
+
5. `apply_patch` in small coherent batches.
|
|
38
|
+
6. `run_command` for the narrowest relevant check first.
|
|
39
|
+
7. Broaden checks only after the focused check passes.
|
|
40
|
+
|
|
41
|
+
## CLI And Web Parity
|
|
42
|
+
|
|
43
|
+
Both CLI and web use the same task profile registry and the same model/tool schemas. Use either:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
aginti --profile large-codebase "fix the failing tests"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
or choose **Large codebase engineering** in the web task-profile dropdown.
|
|
50
|
+
|
|
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
|
+
## Cross-Language Playbook
|
|
68
|
+
|
|
69
|
+
AgInTiFlow gives DeepSeek stack-specific reminders without hardcoding a solution:
|
|
70
|
+
|
|
71
|
+
- JS/TS: inspect package scripts and lockfiles, then run focused `node`, `tsc`, or test commands.
|
|
72
|
+
- Python: inspect `pyproject.toml` or requirements, prefer project-local venv/conda/Docker, then run focused pytest/module checks.
|
|
73
|
+
- Rust/Go/JVM/C/C++: inspect native manifests, format only touched files when possible, and start with narrow build/test targets.
|
|
74
|
+
- R/Stan/LaTeX: keep toolchains project-local or Docker-backed, compile from the right directory, and publish useful artifacts to canvas.
|
|
75
|
+
- System tasks: diagnose first, capture versions/logs, write reversible scripts, use Docker for installs, and avoid silent host-level changes.
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -775,6 +775,25 @@ function renderTaskProfiles(selected = "auto") {
|
|
|
775
775
|
taskProfileField.value = profiles.some((profile) => profile.id === selected) ? selected : "auto";
|
|
776
776
|
}
|
|
777
777
|
|
|
778
|
+
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;
|
|
779
|
+
|
|
780
|
+
function recommendedMaxStepsForProfile(profile = "auto", goal = "") {
|
|
781
|
+
if (profile === "large-codebase") return 36;
|
|
782
|
+
if (profile === "latex") return 30;
|
|
783
|
+
if (COMPLEX_ENGINEERING_HINT.test(goal || "")) return 36;
|
|
784
|
+
if (/\b(latex|tex|pdflatex|latexmk|pdf|website|app|docker|system|install|setup|debug)\b/i.test(goal || "")) return 30;
|
|
785
|
+
return 24;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function ensureRecommendedMaxStepsForCurrentTask() {
|
|
789
|
+
const maxStepsField = document.querySelector("#maxSteps");
|
|
790
|
+
const goalField = document.querySelector("#goal");
|
|
791
|
+
const recommended = recommendedMaxStepsForProfile(taskProfileField?.value || "auto", goalField?.value || "");
|
|
792
|
+
if (maxStepsField && Number(maxStepsField.value || 0) < recommended) {
|
|
793
|
+
maxStepsField.value = String(recommended);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
778
797
|
function renderWrapperStatus(wrappers = lastWrappers) {
|
|
779
798
|
lastWrappers = wrappers || [];
|
|
780
799
|
if (lastWrappers.length === 0) {
|
|
@@ -1959,7 +1978,10 @@ sandboxModeField.addEventListener("change", updatePackageWarning);
|
|
|
1959
1978
|
packageInstallPolicyField.addEventListener("change", updatePackageWarning);
|
|
1960
1979
|
allowWrapperToolsField.addEventListener("change", () => renderWrapperStatus());
|
|
1961
1980
|
preferredWrapperField.addEventListener("change", () => renderWrapperStatus());
|
|
1962
|
-
taskProfileField?.addEventListener("change",
|
|
1981
|
+
taskProfileField?.addEventListener("change", () => {
|
|
1982
|
+
ensureRecommendedMaxStepsForCurrentTask();
|
|
1983
|
+
schedulePreferenceSave();
|
|
1984
|
+
});
|
|
1963
1985
|
|
|
1964
1986
|
saveApiKeyButton?.addEventListener("click", async () => {
|
|
1965
1987
|
const provider = setupProviderField.value || "deepseek";
|
|
@@ -2035,6 +2057,7 @@ form.addEventListener("submit", async (event) => {
|
|
|
2035
2057
|
setLogs(t("goalRequired"), "empty");
|
|
2036
2058
|
return;
|
|
2037
2059
|
}
|
|
2060
|
+
ensureRecommendedMaxStepsForCurrentTask();
|
|
2038
2061
|
|
|
2039
2062
|
const payload = {
|
|
2040
2063
|
...formPayload(),
|
|
@@ -5,6 +5,7 @@ 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";
|
|
9
10
|
import { SessionStore } from "../src/session-store.js";
|
|
10
11
|
import { executeWorkspaceTool } from "../src/workspace-tools.js";
|
|
@@ -86,6 +87,71 @@ try {
|
|
|
86
87
|
goal: "patch this large codebase and migrate the database tests",
|
|
87
88
|
});
|
|
88
89
|
assert(/pro/i.test(patchRoute.model), "patch/refactor task did not route to DeepSeek pro");
|
|
90
|
+
const largeProfileRoute = selectModelRoute({
|
|
91
|
+
routingMode: "smart",
|
|
92
|
+
provider: "deepseek",
|
|
93
|
+
goal: "fix this bug",
|
|
94
|
+
taskProfile: "large-codebase",
|
|
95
|
+
});
|
|
96
|
+
assert(/pro/i.test(largeProfileRoute.model), "large-codebase profile did not route to DeepSeek pro");
|
|
97
|
+
const autoSystemRoute = selectModelRoute({
|
|
98
|
+
routingMode: "smart",
|
|
99
|
+
provider: "deepseek",
|
|
100
|
+
goal: "debug this Python project system bug and fix failing tests",
|
|
101
|
+
taskProfile: "auto",
|
|
102
|
+
});
|
|
103
|
+
assert(/pro/i.test(autoSystemRoute.model), "auto system/code problem did not route to DeepSeek pro");
|
|
104
|
+
assert(
|
|
105
|
+
recommendedMaxStepsForTask({
|
|
106
|
+
goal: "debug this Python project system bug and fix failing tests",
|
|
107
|
+
taskProfile: "auto",
|
|
108
|
+
complexityScore: autoSystemRoute.complexityScore,
|
|
109
|
+
}) >= 36,
|
|
110
|
+
"auto system/code problem did not get engineering step budget"
|
|
111
|
+
);
|
|
112
|
+
const guidance = engineeringGuidanceForTask("debug this Python project system bug and fix failing tests", "auto");
|
|
113
|
+
assert(guidance.includes("Python:"), "engineering guidance did not include Python stack advice");
|
|
114
|
+
assert(guidance.includes("System/shell:"), "engineering guidance did not include system stack advice");
|
|
115
|
+
|
|
116
|
+
await fs.mkdir(path.join(workspace, "src"), { recursive: true });
|
|
117
|
+
await fs.mkdir(path.join(workspace, "test"), { recursive: true });
|
|
118
|
+
await fs.writeFile(
|
|
119
|
+
path.join(workspace, "package.json"),
|
|
120
|
+
JSON.stringify(
|
|
121
|
+
{
|
|
122
|
+
name: "agintiflow-inspect-smoke",
|
|
123
|
+
scripts: {
|
|
124
|
+
test: "node --test test/index.test.js",
|
|
125
|
+
check: "node --check src/index.js",
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
null,
|
|
129
|
+
2
|
|
130
|
+
),
|
|
131
|
+
"utf8"
|
|
132
|
+
);
|
|
133
|
+
await fs.writeFile(path.join(workspace, "src/index.js"), "export function answer() { return 42; }\n", "utf8");
|
|
134
|
+
await fs.writeFile(path.join(workspace, "test/index.test.js"), "import test from 'node:test';\n", "utf8");
|
|
135
|
+
const inspected = await executeWorkspaceTool(
|
|
136
|
+
"inspect_project",
|
|
137
|
+
{ path: ".", maxDepth: 4, limit: 200 },
|
|
138
|
+
{
|
|
139
|
+
commandCwd: workspace,
|
|
140
|
+
allowFileTools: true,
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
assert(inspected.ok, "inspect_project failed");
|
|
144
|
+
assert(inspected.manifestFiles.some((item) => item.path === "package.json"), "inspect_project did not find package.json");
|
|
145
|
+
assert(inspected.packageScripts.some((item) => item.name === "test"), "inspect_project did not extract package scripts");
|
|
146
|
+
assert(inspected.sourceDirs.some((item) => item.path === "src"), "inspect_project did not identify src directory");
|
|
147
|
+
assert(inspected.testFiles.some((item) => item.path === "test/index.test.js"), "inspect_project did not identify test file");
|
|
148
|
+
assert(inspected.recommendedReads.includes("package.json"), "inspect_project did not recommend package.json");
|
|
149
|
+
|
|
150
|
+
const inspectRun = await runMock("Inspect this large codebase and recommend next reads.", "coding-inspect");
|
|
151
|
+
assert(
|
|
152
|
+
inspectRun.events.some((event) => event.type === "tool.completed" && event.data?.toolName === "inspect_project"),
|
|
153
|
+
"mock large-codebase run did not use inspect_project"
|
|
154
|
+
);
|
|
89
155
|
|
|
90
156
|
const writeRun = await runMock("Create notes/hello.md with a short coding smoke message.", "coding-write");
|
|
91
157
|
const written = await fs.readFile(path.join(workspace, "notes/hello.md"), "utf8");
|
|
@@ -216,6 +282,11 @@ try {
|
|
|
216
282
|
checks: [
|
|
217
283
|
"deepseek_history_repair",
|
|
218
284
|
"deepseek_pro_patch_route",
|
|
285
|
+
"large_profile_pro_route",
|
|
286
|
+
"auto_system_pro_route",
|
|
287
|
+
"auto_engineering_guidance",
|
|
288
|
+
"inspect_project",
|
|
289
|
+
"mock_inspect_project",
|
|
219
290
|
"write_file",
|
|
220
291
|
"duplicate_write_failed",
|
|
221
292
|
"resume_session_write",
|
package/src/agent-runner.js
CHANGED
|
@@ -18,6 +18,7 @@ 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";
|
|
21
22
|
|
|
22
23
|
const exec = promisify(execCallback);
|
|
23
24
|
const BROWSER_TOOLS = new Set(["open_url", "open_workspace_file", "preview_workspace", "click", "type", "scroll", "press", "back"]);
|
|
@@ -233,6 +234,7 @@ export function repairModelMessageHistory(state, config = {}) {
|
|
|
233
234
|
function createInitialState(config, sessionId) {
|
|
234
235
|
const now = new Date().toISOString();
|
|
235
236
|
const taskProfile = getTaskProfile(config.taskProfile);
|
|
237
|
+
const engineeringGuidance = engineeringGuidanceForTask(config.goal, config.taskProfile);
|
|
236
238
|
return {
|
|
237
239
|
sessionId,
|
|
238
240
|
createdAt: now,
|
|
@@ -272,7 +274,7 @@ function createInitialState(config, sessionId) {
|
|
|
272
274
|
: "A host shell command tool is available under the configured trust policy."
|
|
273
275
|
: "No shell command tool is available.",
|
|
274
276
|
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.`
|
|
277
|
+
? `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
278
|
: "No workspace file tools are available.",
|
|
277
279
|
config.allowWrapperTools
|
|
278
280
|
? `External coding-agent wrappers are available as advisory tools only. Use the selected wrapper only: ${normalizeWrapperName(config.preferredWrapper)}. Wrapper status: ${wrapperStatusText()}.`
|
|
@@ -283,10 +285,11 @@ function createInitialState(config, sessionId) {
|
|
|
283
285
|
.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
286
|
: "Auxiliary skills are disabled for this run.",
|
|
285
287
|
`Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
|
|
288
|
+
engineeringGuidance,
|
|
286
289
|
"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
290
|
"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:
|
|
289
|
-
"For large projects, decompose into useful files and milestones, implement a coherent minimal version
|
|
291
|
+
"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.",
|
|
292
|
+
"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
293
|
"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
294
|
"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
295
|
"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 +313,7 @@ function createInitialState(config, sessionId) {
|
|
|
310
313
|
: `Shell working directory: ${config.commandCwd}`
|
|
311
314
|
: "",
|
|
312
315
|
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.`
|
|
316
|
+
? `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
317
|
: "",
|
|
315
318
|
config.allowWrapperTools
|
|
316
319
|
? `Agent wrappers: selected=${normalizeWrapperName(config.preferredWrapper)}; ${wrapperStatusText()}`
|
|
@@ -321,9 +324,10 @@ function createInitialState(config, sessionId) {
|
|
|
321
324
|
.join(" ")}`
|
|
322
325
|
: "",
|
|
323
326
|
`Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
|
|
327
|
+
engineeringGuidance,
|
|
324
328
|
"Canvas/artifacts tunnel: available through send_to_canvas for optional frontend rendering.",
|
|
325
329
|
"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.",
|
|
330
|
+
"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
331
|
"Do not stop at a plan when tools can accomplish the request. Continue through implementation, checks, artifact selection, and finish.",
|
|
328
332
|
"Use the configured sandbox and package policy for environment or system-maintenance work.",
|
|
329
333
|
]
|
|
@@ -410,6 +414,7 @@ function applyContinuationPrompt(state, config, observers) {
|
|
|
410
414
|
if (!config.resume || !config.goal) return;
|
|
411
415
|
|
|
412
416
|
const taskProfile = getTaskProfile(config.taskProfile);
|
|
417
|
+
const engineeringGuidance = engineeringGuidanceForTask(config.goal, config.taskProfile);
|
|
413
418
|
ensureChatState(state);
|
|
414
419
|
state.goal = config.goal;
|
|
415
420
|
state.provider = config.provider;
|
|
@@ -430,12 +435,13 @@ function applyContinuationPrompt(state, config, observers) {
|
|
|
430
435
|
: `Shell working directory: ${config.commandCwd}`
|
|
431
436
|
: "",
|
|
432
437
|
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.`
|
|
438
|
+
? `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
439
|
: "",
|
|
435
440
|
config.allowWrapperTools
|
|
436
441
|
? `Agent wrappers: selected=${normalizeWrapperName(config.preferredWrapper)}; ${wrapperStatusText()}`
|
|
437
442
|
: "",
|
|
438
443
|
`Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
|
|
444
|
+
engineeringGuidance,
|
|
439
445
|
]
|
|
440
446
|
.filter(Boolean)
|
|
441
447
|
.join("\n"),
|
|
@@ -631,7 +637,7 @@ async function captureSyntheticSnapshot(store, step, config) {
|
|
|
631
637
|
: `Shell tool available in: ${config.commandCwd}`
|
|
632
638
|
: "Shell tool disabled.",
|
|
633
639
|
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.`
|
|
640
|
+
? `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
641
|
: "Workspace file tools disabled.",
|
|
636
642
|
config.allowWrapperTools
|
|
637
643
|
? `Agent wrappers available: selected=${normalizeWrapperName(config.preferredWrapper)}; ${wrapperStatusText()}`
|
|
@@ -815,6 +821,7 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
|
|
|
815
821
|
await new Promise((resolve) => setTimeout(resolve, Number.isFinite(args.ms) ? Number(args.ms) : 1000));
|
|
816
822
|
}
|
|
817
823
|
break;
|
|
824
|
+
case "inspect_project":
|
|
818
825
|
case "list_files":
|
|
819
826
|
case "read_file":
|
|
820
827
|
case "search_files":
|
package/src/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
showProjectSession,
|
|
16
16
|
} from "./project.js";
|
|
17
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";
|
|
@@ -256,7 +257,12 @@ function agentDefaults(args) {
|
|
|
256
257
|
sandboxMode: args.sandboxMode || "docker-workspace",
|
|
257
258
|
packageInstallPolicy: args.packageInstallPolicy || "allow",
|
|
258
259
|
useDockerSandbox: args.useDockerSandbox ?? true,
|
|
259
|
-
maxSteps:
|
|
260
|
+
maxSteps:
|
|
261
|
+
args.maxSteps ||
|
|
262
|
+
recommendedMaxStepsForTask({
|
|
263
|
+
goal: args.goal || "",
|
|
264
|
+
taskProfile: args.taskProfile || (args.latex ? "latex" : "auto"),
|
|
265
|
+
}),
|
|
260
266
|
};
|
|
261
267
|
|
|
262
268
|
if (defaults.sandboxMode === "host") {
|
package/src/config.js
CHANGED
|
@@ -5,6 +5,7 @@ import { normalizePackageInstallPolicy, normalizeSandboxMode } from "./command-p
|
|
|
5
5
|
import { normalizeWrapperName } from "./tool-wrappers.js";
|
|
6
6
|
import { loadProjectEnv, resolveProjectRoot } from "./project.js";
|
|
7
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;
|
|
@@ -33,14 +34,21 @@ export function resolveRuntimeConfig(args, overrides = {}) {
|
|
|
33
34
|
process.env.AGENT_PROVIDER ||
|
|
34
35
|
(process.env.DEEPSEEK_API_KEY ? "deepseek" : process.env.OPENAI_API_KEY ? "openai" : "deepseek");
|
|
35
36
|
const routingMode = normalizeRoutingMode(overrides.routingMode || args.routingMode || process.env.AGENT_ROUTING_MODE || "smart");
|
|
37
|
+
const taskProfile = normalizeTaskProfile(overrides.taskProfile || args.taskProfile || process.env.AGINTI_TASK_PROFILE || "auto");
|
|
36
38
|
const route = selectModelRoute({
|
|
37
39
|
routingMode,
|
|
38
40
|
provider: requestedProvider,
|
|
39
41
|
model: overrides.model || args.model || process.env.LLM_MODEL || "",
|
|
40
42
|
goal: args.goal || "",
|
|
43
|
+
taskProfile,
|
|
41
44
|
});
|
|
42
45
|
|
|
43
46
|
const defaults = getProviderDefaults(route.provider);
|
|
47
|
+
const defaultMaxSteps = recommendedMaxStepsForTask({
|
|
48
|
+
goal: args.goal || "",
|
|
49
|
+
taskProfile,
|
|
50
|
+
complexityScore: route.complexityScore,
|
|
51
|
+
});
|
|
44
52
|
const packageDir = path.resolve(overrides.packageDir || process.env.AGINTIFLOW_PACKAGE_DIR || baseDir);
|
|
45
53
|
const dockerRequested = parseBoolean(overrides.useDockerSandbox ?? args.useDockerSandbox ?? process.env.USE_DOCKER_SANDBOX, true);
|
|
46
54
|
const requestedSandboxMode =
|
|
@@ -56,7 +64,7 @@ export function resolveRuntimeConfig(args, overrides = {}) {
|
|
|
56
64
|
resume: args.resume || "",
|
|
57
65
|
sessionId: overrides.sessionId || args.sessionId || process.env.SESSION_ID || `web-agent-${crypto.randomUUID()}`,
|
|
58
66
|
routingMode,
|
|
59
|
-
taskProfile
|
|
67
|
+
taskProfile,
|
|
60
68
|
routeReason: route.reason,
|
|
61
69
|
routeComplexityScore: route.complexityScore,
|
|
62
70
|
requestedProvider,
|
|
@@ -65,7 +73,7 @@ export function resolveRuntimeConfig(args, overrides = {}) {
|
|
|
65
73
|
apiKey: overrides.apiKey || defaults.apiKey,
|
|
66
74
|
baseURL: overrides.baseURL || defaults.baseURL,
|
|
67
75
|
model: route.model || defaults.model,
|
|
68
|
-
maxSteps: parseNumber(overrides.maxSteps ?? args.maxSteps ?? process.env.MAX_STEPS,
|
|
76
|
+
maxSteps: parseNumber(overrides.maxSteps ?? args.maxSteps ?? process.env.MAX_STEPS, defaultMaxSteps),
|
|
69
77
|
headless: parseBoolean(overrides.headless ?? args.headless ?? process.env.HEADLESS, false),
|
|
70
78
|
allowedDomains: Array.isArray(overrides.allowedDomains)
|
|
71
79
|
? overrides.allowedDomains
|
|
@@ -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/interactive-cli.js
CHANGED
|
@@ -5,7 +5,8 @@ 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
|
+
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");
|
|
@@ -439,7 +440,10 @@ function createState(args = {}) {
|
|
|
439
440
|
preferredWrapper: args.preferredWrapper || "codex",
|
|
440
441
|
taskProfile: normalizeTaskProfile(args.taskProfile || "auto"),
|
|
441
442
|
headless: args.headless ?? false,
|
|
442
|
-
maxSteps:
|
|
443
|
+
maxSteps:
|
|
444
|
+
Number.isFinite(args.maxSteps) && args.maxSteps > 0
|
|
445
|
+
? args.maxSteps
|
|
446
|
+
: defaultMaxStepsForProfile(args.taskProfile || (args.latex ? "latex" : "auto")),
|
|
443
447
|
sessionId: args.resume || "",
|
|
444
448
|
};
|
|
445
449
|
}
|
|
@@ -589,6 +593,7 @@ async function handleCommand(line, state, packageDir) {
|
|
|
589
593
|
}
|
|
590
594
|
if (command === "profile") {
|
|
591
595
|
state.taskProfile = normalizeTaskProfile(value || "auto");
|
|
596
|
+
state.maxSteps = Math.max(state.maxSteps, defaultMaxStepsForProfile(state.taskProfile));
|
|
592
597
|
printSystemLine(`profile=${state.taskProfile}`);
|
|
593
598
|
return true;
|
|
594
599
|
}
|
|
@@ -669,6 +674,13 @@ async function handleCommand(line, state, packageDir) {
|
|
|
669
674
|
|
|
670
675
|
async function runPrompt(prompt, state, packageDir) {
|
|
671
676
|
const controller = new AbortController();
|
|
677
|
+
const runMaxSteps = Math.max(
|
|
678
|
+
state.maxSteps,
|
|
679
|
+
recommendedMaxStepsForTask({
|
|
680
|
+
goal: prompt,
|
|
681
|
+
taskProfile: state.taskProfile,
|
|
682
|
+
})
|
|
683
|
+
);
|
|
672
684
|
const config = loadConfig(
|
|
673
685
|
{
|
|
674
686
|
provider: state.provider,
|
|
@@ -684,7 +696,7 @@ async function runPrompt(prompt, state, packageDir) {
|
|
|
684
696
|
allowDestructive: state.allowDestructive,
|
|
685
697
|
preferredWrapper: state.preferredWrapper,
|
|
686
698
|
taskProfile: state.taskProfile,
|
|
687
|
-
maxSteps:
|
|
699
|
+
maxSteps: runMaxSteps,
|
|
688
700
|
headless: state.headless,
|
|
689
701
|
resume: state.sessionId,
|
|
690
702
|
goal: prompt,
|
package/src/model-client.js
CHANGED
|
@@ -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") {
|
|
@@ -81,6 +82,13 @@ function mockPathForGoal(goal = "") {
|
|
|
81
82
|
function mockWorkspaceToolForGoal(goal = "") {
|
|
82
83
|
const text = String(goal).toLowerCase();
|
|
83
84
|
const targetPath = mockPathForGoal(goal);
|
|
85
|
+
if (/inspect|map|overview|architecture|large codebase|large repo|repository|repo\b|codebase/.test(text)) {
|
|
86
|
+
return mockToolCall("inspect_project", {
|
|
87
|
+
path: ".",
|
|
88
|
+
maxDepth: 6,
|
|
89
|
+
limit: 400,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
84
92
|
if (/patch|replace|edit/.test(text)) {
|
|
85
93
|
if (/multi|codex|unified|several|multiple/i.test(text)) {
|
|
86
94
|
return mockToolCall("apply_patch", {
|
|
@@ -166,6 +174,7 @@ function mockChatResponse(content, toolCalls = []) {
|
|
|
166
174
|
|
|
167
175
|
export async function createPlan(client, config, state) {
|
|
168
176
|
const taskProfile = getTaskProfile(config.taskProfile);
|
|
177
|
+
const engineeringGuidance = engineeringGuidanceForTask(state.goal, config.taskProfile);
|
|
169
178
|
if (client.mock) {
|
|
170
179
|
return [
|
|
171
180
|
"1. Inspect the request and prefer the local shell when available.",
|
|
@@ -194,7 +203,7 @@ export async function createPlan(client, config, state) {
|
|
|
194
203
|
? `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
204
|
: "",
|
|
196
205
|
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.`
|
|
206
|
+
? `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
207
|
: "",
|
|
199
208
|
config.allowWrapperTools
|
|
200
209
|
? `Agent wrappers are enabled. Use the selected wrapper only: ${normalizeWrapperName(config.preferredWrapper)}. Status: ${wrapperStatusText()}.`
|
|
@@ -205,6 +214,7 @@ export async function createPlan(client, config, state) {
|
|
|
205
214
|
.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.`
|
|
206
215
|
: "Auxiliary skills are disabled for this run.",
|
|
207
216
|
`Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
|
|
217
|
+
engineeringGuidance,
|
|
208
218
|
"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.",
|
|
209
219
|
"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.",
|
|
210
220
|
"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.",
|
|
@@ -414,6 +424,24 @@ export async function requestNextStep(client, config, messages) {
|
|
|
414
424
|
tools.splice(
|
|
415
425
|
-1,
|
|
416
426
|
0,
|
|
427
|
+
{
|
|
428
|
+
type: "function",
|
|
429
|
+
function: {
|
|
430
|
+
name: "inspect_project",
|
|
431
|
+
description:
|
|
432
|
+
"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.",
|
|
433
|
+
parameters: {
|
|
434
|
+
type: "object",
|
|
435
|
+
properties: {
|
|
436
|
+
path: { type: "string", description: "Workspace-relative directory to inspect. Defaults to ." },
|
|
437
|
+
maxDepth: { type: "integer", description: "Recursive depth, 1 to 10. Defaults to 6." },
|
|
438
|
+
limit: { type: "integer", description: "Maximum filesystem entries to inspect." },
|
|
439
|
+
includeFiles: { type: "boolean", description: "Include a compact file list when needed. Defaults to false." },
|
|
440
|
+
},
|
|
441
|
+
additionalProperties: false,
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
},
|
|
417
445
|
{
|
|
418
446
|
type: "function",
|
|
419
447
|
function: {
|
|
@@ -604,6 +632,11 @@ export async function requestNextStep(client, config, messages) {
|
|
|
604
632
|
toolPayload.stderr,
|
|
605
633
|
toolPayload.error,
|
|
606
634
|
toolPayload.reason,
|
|
635
|
+
toolPayload.summary ? `Summary: ${toolPayload.summary}` : "",
|
|
636
|
+
toolPayload.counts ? `Counts: ${JSON.stringify(toolPayload.counts)}` : "",
|
|
637
|
+
Array.isArray(toolPayload.recommendedReads) && toolPayload.recommendedReads.length
|
|
638
|
+
? `Recommended reads: ${toolPayload.recommendedReads.join(", ")}`
|
|
639
|
+
: "",
|
|
607
640
|
toolPayload.path ? `Path: ${toolPayload.path}` : "",
|
|
608
641
|
Array.isArray(toolPayload.changes)
|
|
609
642
|
? toolPayload.changes
|
package/src/model-routing.js
CHANGED
|
@@ -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",
|
|
@@ -19,9 +29,35 @@ const COMPLEXITY_KEYWORDS = [
|
|
|
19
29
|
"docker",
|
|
20
30
|
"ci",
|
|
21
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",
|
|
22
53
|
];
|
|
23
54
|
|
|
24
55
|
const COMPLEX_ROUTE_HINTS = [
|
|
56
|
+
/\b(large|big|complex|complicated)\s+(repo|repository|codebase|project|task)\b/i,
|
|
57
|
+
/\b(multi[- ]file|cross[- ]file|repo[- ]wide|workspace[- ]wide)\b/i,
|
|
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,
|
|
25
61
|
/\blatex\b/i,
|
|
26
62
|
/\btexlive\b/i,
|
|
27
63
|
/\bpdflatex\b/i,
|
|
@@ -102,9 +138,10 @@ export function getModelPresets() {
|
|
|
102
138
|
};
|
|
103
139
|
}
|
|
104
140
|
|
|
105
|
-
export function scoreTaskComplexity(goal = "") {
|
|
141
|
+
export function scoreTaskComplexity(goal = "", taskProfile = "auto") {
|
|
106
142
|
const text = String(goal).toLowerCase();
|
|
107
143
|
let score = text.length > 600 ? 2 : text.length > 240 ? 1 : 0;
|
|
144
|
+
if (["large-codebase", "engineering", "codebase"].includes(String(taskProfile || "").toLowerCase())) score += 3;
|
|
108
145
|
for (const keyword of COMPLEXITY_KEYWORDS) {
|
|
109
146
|
if (text.includes(keyword)) score += 1;
|
|
110
147
|
}
|
|
@@ -118,7 +155,7 @@ export function normalizeRoutingMode(value) {
|
|
|
118
155
|
return ROUTING_MODES.includes(value) ? value : "smart";
|
|
119
156
|
}
|
|
120
157
|
|
|
121
|
-
export function selectModelRoute({ routingMode = "smart", provider = "deepseek", model = "", goal = "" } = {}) {
|
|
158
|
+
export function selectModelRoute({ routingMode = "smart", provider = "deepseek", model = "", goal = "", taskProfile = "auto" } = {}) {
|
|
122
159
|
const mode = normalizeRoutingMode(routingMode);
|
|
123
160
|
const presets = getModelPresets();
|
|
124
161
|
|
|
@@ -129,7 +166,7 @@ export function selectModelRoute({ routingMode = "smart", provider = "deepseek",
|
|
|
129
166
|
provider: defaults.provider,
|
|
130
167
|
model: model || defaults.model,
|
|
131
168
|
reason: "Local mock route selected for smoke tests and offline UI/API checks.",
|
|
132
|
-
complexityScore: scoreTaskComplexity(goal),
|
|
169
|
+
complexityScore: scoreTaskComplexity(goal, taskProfile),
|
|
133
170
|
};
|
|
134
171
|
}
|
|
135
172
|
|
|
@@ -140,7 +177,7 @@ export function selectModelRoute({ routingMode = "smart", provider = "deepseek",
|
|
|
140
177
|
provider: defaults.provider,
|
|
141
178
|
model: model || defaults.model,
|
|
142
179
|
reason: "Manual provider/model selection.",
|
|
143
|
-
complexityScore: scoreTaskComplexity(goal),
|
|
180
|
+
complexityScore: scoreTaskComplexity(goal, taskProfile),
|
|
144
181
|
};
|
|
145
182
|
}
|
|
146
183
|
|
|
@@ -150,7 +187,7 @@ export function selectModelRoute({ routingMode = "smart", provider = "deepseek",
|
|
|
150
187
|
provider: presets.complex.provider,
|
|
151
188
|
model: presets.complex.model,
|
|
152
189
|
reason: "Complex route selected explicitly.",
|
|
153
|
-
complexityScore: scoreTaskComplexity(goal),
|
|
190
|
+
complexityScore: scoreTaskComplexity(goal, taskProfile),
|
|
154
191
|
};
|
|
155
192
|
}
|
|
156
193
|
|
|
@@ -160,11 +197,11 @@ export function selectModelRoute({ routingMode = "smart", provider = "deepseek",
|
|
|
160
197
|
provider: presets.fast.provider,
|
|
161
198
|
model: presets.fast.model,
|
|
162
199
|
reason: "Fast route selected explicitly.",
|
|
163
|
-
complexityScore: scoreTaskComplexity(goal),
|
|
200
|
+
complexityScore: scoreTaskComplexity(goal, taskProfile),
|
|
164
201
|
};
|
|
165
202
|
}
|
|
166
203
|
|
|
167
|
-
const complexityScore = scoreTaskComplexity(goal);
|
|
204
|
+
const complexityScore = scoreTaskComplexity(goal, taskProfile);
|
|
168
205
|
const selected = complexityScore >= 3 ? presets.complex : presets.fast;
|
|
169
206
|
return {
|
|
170
207
|
routingMode: mode,
|
package/src/task-profiles.js
CHANGED
|
@@ -3,15 +3,22 @@ export const TASK_PROFILES = {
|
|
|
3
3
|
id: "auto",
|
|
4
4
|
label: "Auto",
|
|
5
5
|
prompt:
|
|
6
|
-
"Infer the task type from the user request.
|
|
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:
|
|
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
|
+
},
|
|
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"],
|
|
15
22
|
},
|
|
16
23
|
writing: {
|
|
17
24
|
id: "writing",
|
|
@@ -80,20 +87,37 @@ export const TASK_PROFILES = {
|
|
|
80
87
|
id: "maintenance",
|
|
81
88
|
label: "System maintenance",
|
|
82
89
|
prompt:
|
|
83
|
-
"For system maintenance, diagnose first, use Docker for broad installs when available,
|
|
84
|
-
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"],
|
|
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
|
+
}
|
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 =
|
|
6
|
+
const PREFERENCES_SCHEMA_VERSION = 5;
|
|
7
7
|
|
|
8
8
|
function defaultPreferences(baseDir) {
|
|
9
9
|
const presets = getModelPresets();
|
|
@@ -95,6 +95,9 @@ export class WebDatabase {
|
|
|
95
95
|
if (!Number.isFinite(Number(parsed.maxSteps)) || Number(parsed.maxSteps) < 24) {
|
|
96
96
|
preferences.maxSteps = 24;
|
|
97
97
|
}
|
|
98
|
+
if ((parsed.preferencesSchemaVersion || 1) < 5) {
|
|
99
|
+
preferences.taskProfile = "auto";
|
|
100
|
+
}
|
|
98
101
|
this.savePreferences(preferences);
|
|
99
102
|
}
|
|
100
103
|
return preferences;
|
package/src/workspace-tools.js
CHANGED
|
@@ -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([
|
|
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":
|