@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 +2 -0
- package/docs/large-codebase-engineering.md +49 -0
- package/package.json +1 -1
- package/public/app.js +14 -1
- package/scripts/smoke-coding-tools.js +50 -0
- package/src/agent-runner.js +8 -7
- package/src/cli.js +2 -2
- package/src/config.js +5 -3
- package/src/interactive-cli.js +6 -2
- package/src/model-client.js +31 -1
- package/src/model-routing.js +21 -7
- package/src/task-profiles.js +24 -0
- 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. 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
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",
|
|
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",
|
package/src/agent-runner.js
CHANGED
|
@@ -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:
|
|
289
|
-
"For large projects, decompose into useful files and milestones, implement a coherent minimal version
|
|
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.
|
|
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
|
|
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,
|
|
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
|
package/src/interactive-cli.js
CHANGED
|
@@ -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:
|
|
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
|
}
|
package/src/model-client.js
CHANGED
|
@@ -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
|
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",
|
|
@@ -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,
|
package/src/task-profiles.js
CHANGED
|
@@ -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
|
+
}
|
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":
|