@cjhyy/code-shell-core 0.5.0-rc.1 → 0.5.0-rc.2
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/dist/agent/agent-definition-registry.d.ts +18 -3
- package/dist/agent/agent-definition-registry.js +47 -18
- package/dist/agent/agent-definition.d.ts +17 -0
- package/dist/agent/agent-definition.js +22 -1
- package/dist/arena/arena.js +11 -12
- package/dist/arena/context/context-tools.js +2 -4
- package/dist/arena/context/within-root.d.ts +7 -0
- package/dist/arena/context/within-root.js +15 -0
- package/dist/arena/iterate/phases/argue.js +8 -2
- package/dist/arena/iterate/phases/revise.js +1 -1
- package/dist/arena/iterate/phases/tournament.js +3 -3
- package/dist/arena/phases/adjudication.js +1 -4
- package/dist/arena/phases/build-consensus.js +1 -4
- package/dist/arena/phases/cross-review.js +2 -8
- package/dist/arena/phases/debate-rounds.js +1 -4
- package/dist/arena/phases/participant-research.js +1 -4
- package/dist/arena/phases/planning-detail-expansion.js +8 -10
- package/dist/arena/planner.js +0 -1
- package/dist/arena/providers/docs.d.ts +1 -0
- package/dist/arena/providers/docs.js +5 -2
- package/dist/arena/providers/repo.d.ts +1 -0
- package/dist/arena/providers/repo.js +5 -2
- package/dist/arena/strategies/utils.js +36 -3
- package/dist/arena/transitions.d.ts +7 -2
- package/dist/arena/transitions.js +9 -4
- package/dist/arena/types.d.ts +8 -2
- package/dist/automation/cron-expr.d.ts +31 -0
- package/dist/automation/cron-expr.js +151 -0
- package/dist/automation/index.d.ts +41 -0
- package/dist/automation/index.js +39 -0
- package/dist/automation/runner.d.ts +67 -0
- package/dist/automation/runner.js +56 -0
- package/dist/automation/scheduler.d.ts +156 -0
- package/dist/automation/scheduler.js +546 -0
- package/dist/automation/store.d.ts +39 -0
- package/dist/automation/store.js +119 -0
- package/dist/automation/write-policy.d.ts +34 -0
- package/dist/automation/write-policy.js +113 -0
- package/dist/automation/write-run.d.ts +51 -0
- package/dist/automation/write-run.js +38 -0
- package/dist/capability-control/index.d.ts +5 -0
- package/dist/capability-control/index.js +3 -0
- package/dist/capability-control/overlay.d.ts +49 -0
- package/dist/capability-control/overlay.js +89 -0
- package/dist/capability-control/project.d.ts +65 -0
- package/dist/capability-control/project.js +147 -0
- package/dist/capability-control/service.d.ts +70 -0
- package/dist/capability-control/service.js +147 -0
- package/dist/capability-control/types.d.ts +62 -0
- package/dist/capability-control/types.js +17 -0
- package/dist/cli/agent-server-stdio.d.ts +12 -1
- package/dist/cli/agent-server-stdio.js +114 -19
- package/dist/cli/agent-server-tcp.d.ts +20 -0
- package/dist/cli/agent-server-tcp.js +120 -0
- package/dist/cli/graceful-shutdown.d.ts +27 -0
- package/dist/cli/graceful-shutdown.js +30 -0
- package/dist/context/compaction.js +23 -15
- package/dist/context/manager.d.ts +0 -20
- package/dist/context/manager.js +0 -37
- package/dist/cron/cron-runtime.d.ts +2 -0
- package/dist/cron/cron-runtime.js +2 -0
- package/dist/cron/cron-store.d.ts +2 -0
- package/dist/cron/cron-store.js +2 -0
- package/dist/cron/scheduler.d.ts +5 -32
- package/dist/cron/scheduler.js +5 -113
- package/dist/data/openrouter-sync.js +1 -1
- package/dist/engine/engine.d.ts +208 -14
- package/dist/engine/engine.js +754 -169
- package/dist/engine/goal.d.ts +42 -0
- package/dist/engine/goal.js +52 -0
- package/dist/engine/image-compression.d.ts +81 -0
- package/dist/engine/image-compression.js +186 -0
- package/dist/engine/image-policy.d.ts +152 -0
- package/dist/engine/image-policy.js +165 -0
- package/dist/engine/reactive-threshold.d.ts +13 -0
- package/dist/engine/reactive-threshold.js +17 -0
- package/dist/engine/runtime.js +7 -0
- package/dist/engine/session-title.d.ts +13 -0
- package/dist/engine/session-title.js +34 -0
- package/dist/engine/turn-loop.d.ts +29 -8
- package/dist/engine/turn-loop.js +498 -316
- package/dist/git/parse-log.d.ts +13 -0
- package/dist/git/parse-log.js +21 -0
- package/dist/git/utils.d.ts +8 -6
- package/dist/git/utils.js +57 -26
- package/dist/git/worktree.js +33 -18
- package/dist/hooks/events.d.ts +23 -1
- package/dist/hooks/goal-stop-hook.d.ts +30 -0
- package/dist/hooks/goal-stop-hook.js +102 -0
- package/dist/hooks/hook-output.d.ts +33 -0
- package/dist/hooks/hook-output.js +79 -0
- package/dist/hooks/registry.d.ts +7 -0
- package/dist/hooks/registry.js +38 -1
- package/dist/hooks/shell-runner.d.ts +2 -0
- package/dist/hooks/shell-runner.js +43 -2
- package/dist/index.d.ts +31 -6
- package/dist/index.js +30 -3
- package/dist/llm/capabilities/reasoning-control.d.ts +24 -0
- package/dist/llm/capabilities/reasoning-control.js +38 -0
- package/dist/llm/capabilities/rules.js +33 -3
- package/dist/llm/capabilities/types.d.ts +22 -2
- package/dist/llm/clamp-max-tokens.d.ts +11 -0
- package/dist/llm/clamp-max-tokens.js +17 -0
- package/dist/llm/client-base.d.ts +41 -4
- package/dist/llm/client-base.js +67 -13
- package/dist/llm/client-factory.d.ts +3 -3
- package/dist/llm/client-factory.js +2 -2
- package/dist/llm/model-pool.d.ts +15 -11
- package/dist/llm/model-pool.js +23 -20
- package/dist/llm/provider-catalog.d.ts +2 -2
- package/dist/llm/providers/anthropic.d.ts +32 -2
- package/dist/llm/providers/anthropic.js +147 -8
- package/dist/llm/providers/openai.d.ts +11 -2
- package/dist/llm/providers/openai.js +253 -52
- package/dist/llm/reasoning-setting.d.ts +51 -0
- package/dist/llm/reasoning-setting.js +32 -0
- package/dist/llm/stop-reason.d.ts +13 -0
- package/dist/llm/stop-reason.js +21 -0
- package/dist/llm/strip-vision.d.ts +32 -0
- package/dist/llm/strip-vision.js +94 -0
- package/dist/llm/types.d.ts +5 -6
- package/dist/logging/logger.js +18 -4
- package/dist/logging/sanitize-messages.d.ts +10 -0
- package/dist/logging/sanitize-messages.js +90 -0
- package/dist/lsp/client.js +28 -8
- package/dist/lsp/manager.js +2 -1
- package/dist/lsp/root-path.d.ts +9 -0
- package/dist/lsp/root-path.js +12 -0
- package/dist/onboarding.js +18 -11
- package/dist/plugins/gitOps.d.ts +34 -0
- package/dist/plugins/gitOps.js +59 -3
- package/dist/plugins/installer/codex/convertAgents.d.ts +7 -0
- package/dist/plugins/installer/codex/convertAgents.js +55 -0
- package/dist/plugins/installer/codex/convertMcp.d.ts +8 -0
- package/dist/plugins/installer/codex/convertMcp.js +45 -0
- package/dist/plugins/installer/codex/convertSkills.d.ts +12 -0
- package/dist/plugins/installer/codex/convertSkills.js +33 -0
- package/dist/plugins/installer/detectFormat.d.ts +2 -0
- package/dist/plugins/installer/detectFormat.js +6 -0
- package/dist/plugins/installer/install.d.ts +7 -0
- package/dist/plugins/installer/install.js +99 -0
- package/dist/plugins/installer/installFromSource.d.ts +13 -0
- package/dist/plugins/installer/installFromSource.js +45 -0
- package/dist/plugins/installer/list.d.ts +9 -0
- package/dist/plugins/installer/list.js +30 -0
- package/dist/plugins/installer/loadPluginAgents.d.ts +3 -0
- package/dist/plugins/installer/loadPluginAgents.js +23 -0
- package/dist/plugins/installer/loadPluginMcp.d.ts +7 -0
- package/dist/plugins/installer/loadPluginMcp.js +74 -0
- package/dist/plugins/installer/parseSource.d.ts +20 -0
- package/dist/plugins/installer/parseSource.js +74 -0
- package/dist/plugins/installer/paths.d.ts +5 -0
- package/dist/plugins/installer/paths.js +25 -0
- package/dist/plugins/installer/types.d.ts +49 -0
- package/dist/plugins/installer/types.js +27 -0
- package/dist/plugins/installer/uninstall.d.ts +2 -0
- package/dist/plugins/installer/uninstall.js +14 -0
- package/dist/plugins/installer/update.d.ts +14 -0
- package/dist/plugins/installer/update.js +65 -0
- package/dist/plugins/loadPluginHooks.d.ts +8 -1
- package/dist/plugins/loadPluginHooks.js +11 -1
- package/dist/plugins/marketplaceManager.d.ts +8 -1
- package/dist/plugins/marketplaceManager.js +38 -4
- package/dist/plugins/pluginCommandHook.js +34 -0
- package/dist/plugins/pluginInstaller.d.ts +21 -0
- package/dist/plugins/pluginInstaller.js +128 -29
- package/dist/plugins/schemas.js +40 -6
- package/dist/plugins/types.d.ts +4 -0
- package/dist/preset/index.js +16 -0
- package/dist/prompt/composer.d.ts +4 -0
- package/dist/prompt/composer.js +28 -4
- package/dist/prompt/sections/orchestration.md +8 -0
- package/dist/protocol/chat-session-manager.d.ts +8 -1
- package/dist/protocol/chat-session-manager.js +10 -0
- package/dist/protocol/chat-session.d.ts +17 -0
- package/dist/protocol/chat-session.js +28 -0
- package/dist/protocol/redact.d.ts +50 -0
- package/dist/protocol/redact.js +71 -0
- package/dist/protocol/server.d.ts +41 -2
- package/dist/protocol/server.js +231 -33
- package/dist/protocol/tcp-transport.d.ts +45 -0
- package/dist/protocol/tcp-transport.js +74 -0
- package/dist/protocol/types.d.ts +23 -0
- package/dist/protocol/types.js +6 -0
- package/dist/remote/bridge.d.ts +11 -2
- package/dist/remote/bridge.js +61 -33
- package/dist/run/ArtifactTracker.js +5 -4
- package/dist/run/EngineRunner.d.ts +27 -1
- package/dist/run/EngineRunner.js +46 -8
- package/dist/run/FileRunStore.js +32 -8
- package/dist/run/RunLock.d.ts +28 -2
- package/dist/run/RunLock.js +49 -7
- package/dist/run/RunManager.js +51 -3
- package/dist/run/RunQueue.d.ts +1 -0
- package/dist/run/RunQueue.js +8 -2
- package/dist/run/factory.d.ts +7 -0
- package/dist/run/factory.js +1 -0
- package/dist/run/index.d.ts +2 -2
- package/dist/run/index.js +1 -1
- package/dist/run/redirect-target.d.ts +7 -0
- package/dist/run/redirect-target.js +13 -0
- package/dist/run/types.js +1 -1
- package/dist/services/browser-open.d.ts +13 -0
- package/dist/services/browser-open.js +16 -0
- package/dist/services/dream-consolidation.d.ts +57 -0
- package/dist/services/dream-consolidation.js +151 -0
- package/dist/services/memory-orchestrator.js +18 -0
- package/dist/services/notifier.d.ts +8 -0
- package/dist/services/notifier.js +39 -25
- package/dist/services/oauth.js +5 -7
- package/dist/services/session-memory-sort.d.ts +8 -0
- package/dist/services/session-memory-sort.js +9 -0
- package/dist/services/session-memory.js +8 -9
- package/dist/session/file-history.js +10 -3
- package/dist/session/session-manager.d.ts +23 -1
- package/dist/session/session-manager.js +73 -2
- package/dist/settings/disk-defaults.d.ts +35 -0
- package/dist/settings/disk-defaults.js +24 -0
- package/dist/settings/manager.d.ts +33 -0
- package/dist/settings/manager.js +94 -1
- package/dist/settings/personalization.d.ts +19 -0
- package/dist/settings/personalization.js +7 -0
- package/dist/settings/schema.d.ts +1142 -126
- package/dist/settings/schema.js +119 -10
- package/dist/tool-system/builtin/add-marketplace.d.ts +12 -0
- package/dist/tool-system/builtin/add-marketplace.js +76 -0
- package/dist/tool-system/builtin/agent-registry.d.ts +11 -0
- package/dist/tool-system/builtin/agent-registry.js +8 -4
- package/dist/tool-system/builtin/agent.d.ts +29 -10
- package/dist/tool-system/builtin/agent.js +108 -52
- package/dist/tool-system/builtin/apply-patch/index.js +22 -3
- package/dist/tool-system/builtin/arena.js +6 -7
- package/dist/tool-system/builtin/complete-goal.d.ts +25 -0
- package/dist/tool-system/builtin/complete-goal.js +45 -0
- package/dist/tool-system/builtin/cron.js +53 -9
- package/dist/tool-system/builtin/edit.d.ts +2 -1
- package/dist/tool-system/builtin/edit.js +6 -1
- package/dist/tool-system/builtin/generate-image.d.ts +25 -0
- package/dist/tool-system/builtin/generate-image.js +146 -0
- package/dist/tool-system/builtin/glob.js +7 -0
- package/dist/tool-system/builtin/grep.js +6 -0
- package/dist/tool-system/builtin/index.d.ts +18 -1
- package/dist/tool-system/builtin/index.js +62 -1
- package/dist/tool-system/builtin/notebook-edit.d.ts +2 -1
- package/dist/tool-system/builtin/notebook-edit.js +8 -1
- package/dist/tool-system/builtin/read.d.ts +2 -1
- package/dist/tool-system/builtin/read.js +9 -1
- package/dist/tool-system/builtin/sleep.js +9 -3
- package/dist/tool-system/builtin/task.js +4 -1
- package/dist/tool-system/builtin/update-automation-memory.d.ts +24 -0
- package/dist/tool-system/builtin/update-automation-memory.js +60 -0
- package/dist/tool-system/builtin/view-image.d.ts +18 -0
- package/dist/tool-system/builtin/view-image.js +96 -0
- package/dist/tool-system/builtin/web-search.d.ts +4 -1
- package/dist/tool-system/builtin/web-search.js +36 -3
- package/dist/tool-system/builtin/write.d.ts +2 -1
- package/dist/tool-system/builtin/write.js +7 -1
- package/dist/tool-system/context.d.ts +12 -0
- package/dist/tool-system/executor.d.ts +0 -1
- package/dist/tool-system/executor.js +34 -40
- package/dist/tool-system/investigation-guard.d.ts +4 -1
- package/dist/tool-system/investigation-guard.js +19 -3
- package/dist/tool-system/mcp-manager.d.ts +36 -12
- package/dist/tool-system/mcp-manager.js +173 -10
- package/dist/tool-system/path-policy.d.ts +94 -0
- package/dist/tool-system/path-policy.js +279 -0
- package/dist/tool-system/permission.js +43 -50
- package/dist/tool-system/plan-mode-allowlist.d.ts +21 -0
- package/dist/tool-system/plan-mode-allowlist.js +43 -0
- package/dist/tool-system/registry.js +12 -1
- package/dist/tool-system/validation.d.ts +10 -0
- package/dist/tool-system/validation.js +16 -3
- package/dist/types.d.ts +90 -15
- package/dist/utils/format.js +4 -2
- package/dist/utils/lockfile.js +9 -2
- package/dist/utils/theme.d.ts +6 -0
- package/dist/utils/theme.js +4 -2
- package/package.json +2 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* NotebookEditTool — read and edit Jupyter notebook (.ipynb) cells.
|
|
3
3
|
*/
|
|
4
4
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
5
|
+
import { enforcePathPolicy } from "../path-policy.js";
|
|
5
6
|
export const notebookEditToolDef = {
|
|
6
7
|
name: "NotebookEdit",
|
|
7
8
|
description: "Edit a Jupyter notebook (.ipynb) file. Supports actions: " +
|
|
@@ -36,13 +37,19 @@ export const notebookEditToolDef = {
|
|
|
36
37
|
required: ["file_path", "action"],
|
|
37
38
|
},
|
|
38
39
|
};
|
|
39
|
-
export async function notebookEditTool(args) {
|
|
40
|
+
export async function notebookEditTool(args, ctx) {
|
|
40
41
|
const filePath = args.file_path;
|
|
41
42
|
const action = args.action;
|
|
42
43
|
if (!filePath)
|
|
43
44
|
return "Error: file_path is required";
|
|
44
45
|
if (!filePath.endsWith(".ipynb"))
|
|
45
46
|
return "Error: file must be a .ipynb file";
|
|
47
|
+
// Path policy: 'read' goes through the read-side rules (sensitive paths
|
|
48
|
+
// still ask), all other actions are writes and use the stricter rules.
|
|
49
|
+
const op = action === "read" ? "read" : "write";
|
|
50
|
+
const blocked = enforcePathPolicy(filePath, op, ctx?.cwd);
|
|
51
|
+
if (blocked)
|
|
52
|
+
return blocked;
|
|
46
53
|
if (action === "read") {
|
|
47
54
|
if (!existsSync(filePath))
|
|
48
55
|
return `Error: File not found: ${filePath}`;
|
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
* Built-in Read file tool.
|
|
3
3
|
*/
|
|
4
4
|
import type { ToolDefinition } from "../../types.js";
|
|
5
|
+
import type { ToolContext } from "../context.js";
|
|
5
6
|
export declare const readToolDef: ToolDefinition;
|
|
6
|
-
export declare function readTool(args: Record<string, unknown
|
|
7
|
+
export declare function readTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { readFile, stat } from "node:fs/promises";
|
|
5
5
|
import { existsSync } from "node:fs";
|
|
6
6
|
import { fileCache } from "./file-cache.js";
|
|
7
|
+
import { enforcePathPolicy } from "../path-policy.js";
|
|
7
8
|
export const readToolDef = {
|
|
8
9
|
name: "Read",
|
|
9
10
|
description: "Read a file from the local filesystem. Returns the file content with line numbers. " +
|
|
@@ -26,10 +27,17 @@ export const readToolDef = {
|
|
|
26
27
|
},
|
|
27
28
|
};
|
|
28
29
|
const MAX_CONTENT_CHARS = 200_000;
|
|
29
|
-
export async function readTool(args) {
|
|
30
|
+
export async function readTool(args, ctx) {
|
|
30
31
|
const filePath = args.file_path;
|
|
31
32
|
if (!filePath)
|
|
32
33
|
return "Error: file_path is required";
|
|
34
|
+
// Path policy: in-workspace reads pass through; sensitive paths (~/.ssh,
|
|
35
|
+
// .env, etc.) and outside-workspace reads are refused with an explanatory
|
|
36
|
+
// error. existsSync check stays AFTER the policy gate — we don't want to
|
|
37
|
+
// probe sensitive paths' existence as a side-channel.
|
|
38
|
+
const blocked = enforcePathPolicy(filePath, "read", ctx?.cwd);
|
|
39
|
+
if (blocked)
|
|
40
|
+
return blocked;
|
|
33
41
|
if (!existsSync(filePath))
|
|
34
42
|
return `Error: File not found: ${filePath}`;
|
|
35
43
|
try {
|
|
@@ -23,11 +23,17 @@ export async function sleepTool(args) {
|
|
|
23
23
|
if (signal?.aborted)
|
|
24
24
|
return "Sleep aborted.";
|
|
25
25
|
await new Promise((resolve, reject) => {
|
|
26
|
-
const
|
|
27
|
-
signal?.addEventListener("abort", () => {
|
|
26
|
+
const onAbort = () => {
|
|
28
27
|
clearTimeout(timer);
|
|
29
28
|
reject(new Error("Sleep aborted"));
|
|
30
|
-
}
|
|
29
|
+
};
|
|
30
|
+
const timer = setTimeout(() => {
|
|
31
|
+
// Remove the abort listener on normal completion — otherwise every Sleep
|
|
32
|
+
// call leaks a listener on the (shared, per-turn) signal.
|
|
33
|
+
signal?.removeEventListener("abort", onAbort);
|
|
34
|
+
resolve();
|
|
35
|
+
}, seconds * 1000);
|
|
36
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
31
37
|
}).catch(() => { });
|
|
32
38
|
return `Slept for ${seconds} seconds.`;
|
|
33
39
|
}
|
|
@@ -30,7 +30,10 @@ export const todoWriteToolDef = {
|
|
|
30
30
|
name: "TodoWrite",
|
|
31
31
|
description: "Manage the session todo list. Pass the COMPLETE list each call — there's no per-item update API, you rewrite the entire snapshot. " +
|
|
32
32
|
"Use proactively for tasks with 3+ steps. Exactly one item should be in_progress at a time. " +
|
|
33
|
-
"Always provide both `content` (imperative) and `activeForm` (present continuous) so the UI can show what's happening right now."
|
|
33
|
+
"Always provide both `content` (imperative) and `activeForm` (present continuous) so the UI can show what's happening right now. " +
|
|
34
|
+
"Skip the todo list for straightforward tasks (roughly the easiest 25%) and don't make single-step lists. " +
|
|
35
|
+
"Update the list immediately after finishing a sub-task — mark it completed in the same turn you completed it, don't batch updates across turns. " +
|
|
36
|
+
"If you change direction and a planned item will no longer be done, rewrite the list with that item dropped rather than leaving it pending; the list must reflect what you're actually going to do next.",
|
|
34
37
|
inputSchema: {
|
|
35
38
|
type: "object",
|
|
36
39
|
properties: {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UpdateAutomationMemory — lets an automation (cron/scheduled) run persist a
|
|
3
|
+
* one-paragraph summary of what it did, so the NEXT run starts with context.
|
|
4
|
+
*
|
|
5
|
+
* This is a FACTORY: core must not depend on desktop's filesystem layer, so the
|
|
6
|
+
* actual persistence is supplied by an injected `sink` callback. A later task
|
|
7
|
+
* (the desktop automation runner) injects a sink that appends to the task's
|
|
8
|
+
* memory.md; this module only builds the tool + validates input.
|
|
9
|
+
*/
|
|
10
|
+
import type { BuiltinTool } from "./index.js";
|
|
11
|
+
import type { ToolDefinition } from "../../types.js";
|
|
12
|
+
/**
|
|
13
|
+
* Bare tool definition (name/description/inputSchema) — mirrors the export
|
|
14
|
+
* style of the other builtin tool defs (e.g. cron.ts). The factory below wraps
|
|
15
|
+
* this into a registry-ready RegisteredTool.
|
|
16
|
+
*/
|
|
17
|
+
export declare const updateAutomationMemoryToolDef: ToolDefinition;
|
|
18
|
+
/**
|
|
19
|
+
* Build an UpdateAutomationMemory tool bound to a persistence `sink`.
|
|
20
|
+
*
|
|
21
|
+
* @param sink Called with the trimmed summary when a non-empty summary is given.
|
|
22
|
+
* The caller decides where/how to persist it.
|
|
23
|
+
*/
|
|
24
|
+
export declare function makeUpdateAutomationMemoryTool(sink: (summary: string) => void): BuiltinTool;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UpdateAutomationMemory — lets an automation (cron/scheduled) run persist a
|
|
3
|
+
* one-paragraph summary of what it did, so the NEXT run starts with context.
|
|
4
|
+
*
|
|
5
|
+
* This is a FACTORY: core must not depend on desktop's filesystem layer, so the
|
|
6
|
+
* actual persistence is supplied by an injected `sink` callback. A later task
|
|
7
|
+
* (the desktop automation runner) injects a sink that appends to the task's
|
|
8
|
+
* memory.md; this module only builds the tool + validates input.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Bare tool definition (name/description/inputSchema) — mirrors the export
|
|
12
|
+
* style of the other builtin tool defs (e.g. cron.ts). The factory below wraps
|
|
13
|
+
* this into a registry-ready RegisteredTool.
|
|
14
|
+
*/
|
|
15
|
+
export const updateAutomationMemoryToolDef = {
|
|
16
|
+
name: "UpdateAutomationMemory",
|
|
17
|
+
description: "Persist a short memory note for this automation task so your NEXT scheduled run " +
|
|
18
|
+
"starts with context. Call this EXACTLY ONCE, at the very end of the run, with a " +
|
|
19
|
+
"concise one-paragraph summary of what you did, what you found, and anything the " +
|
|
20
|
+
"next run should know or continue (e.g. items processed, last cursor/timestamp, " +
|
|
21
|
+
"open issues). Keep it to a few sentences — it is prepended to your future prompts.",
|
|
22
|
+
inputSchema: {
|
|
23
|
+
type: "object",
|
|
24
|
+
properties: {
|
|
25
|
+
summary: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "A concise one-paragraph summary of this run for the next run to build on.",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
required: ["summary"],
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Build an UpdateAutomationMemory tool bound to a persistence `sink`.
|
|
35
|
+
*
|
|
36
|
+
* @param sink Called with the trimmed summary when a non-empty summary is given.
|
|
37
|
+
* The caller decides where/how to persist it.
|
|
38
|
+
*/
|
|
39
|
+
export function makeUpdateAutomationMemoryTool(sink) {
|
|
40
|
+
return {
|
|
41
|
+
definition: {
|
|
42
|
+
...updateAutomationMemoryToolDef,
|
|
43
|
+
source: "builtin",
|
|
44
|
+
// The summary is automation-internal bookkeeping (not user code); no prompt.
|
|
45
|
+
permissionDefault: "allow",
|
|
46
|
+
// Writes (via the injected sink), so not read-only / not concurrency-safe.
|
|
47
|
+
isReadOnly: false,
|
|
48
|
+
isConcurrencySafe: false,
|
|
49
|
+
},
|
|
50
|
+
execute: async (args) => {
|
|
51
|
+
const raw = typeof args.summary === "string" ? args.summary : "";
|
|
52
|
+
const summary = raw.trim();
|
|
53
|
+
if (!summary) {
|
|
54
|
+
return "Error: summary is required and must not be empty";
|
|
55
|
+
}
|
|
56
|
+
sink(summary);
|
|
57
|
+
return "Automation memory saved for the next run.";
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in view_image tool — 把一个本地图片文件以 base64 image ContentBlock
|
|
3
|
+
* 回传进上下文,让 vision 模型「看」它(对照 codex 的 view_image)。
|
|
4
|
+
*
|
|
5
|
+
* 典型用法:模型先写 SVG/Mermaid 并用 shell 转成 PNG,再调 view_image(png)
|
|
6
|
+
* 检查图画对没有(标签是否重叠、文字是否溢出),不对就改源再重转。
|
|
7
|
+
*
|
|
8
|
+
* 三道闸门,避免污染上下文 / 浪费 token:
|
|
9
|
+
* 1. vision gate —— 当前模型不支持视觉时不读文件,只回文字占位。
|
|
10
|
+
* 2. 格式 gate —— 只支持 png/jpeg/gif/webp(provider image 块能吃的);
|
|
11
|
+
* svg/pdf 等回文字提示「先转 PNG」。
|
|
12
|
+
* 3. 大小 gate —— 超过 MAX_BYTES 回文字提示,不读进 base64。
|
|
13
|
+
*/
|
|
14
|
+
import type { ToolDefinition } from "../../types.js";
|
|
15
|
+
import type { ToolContext } from "../context.js";
|
|
16
|
+
import type { BuiltinToolResult } from "./index.js";
|
|
17
|
+
export declare const viewImageToolDef: ToolDefinition;
|
|
18
|
+
export declare function viewImageTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<BuiltinToolResult>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in view_image tool — 把一个本地图片文件以 base64 image ContentBlock
|
|
3
|
+
* 回传进上下文,让 vision 模型「看」它(对照 codex 的 view_image)。
|
|
4
|
+
*
|
|
5
|
+
* 典型用法:模型先写 SVG/Mermaid 并用 shell 转成 PNG,再调 view_image(png)
|
|
6
|
+
* 检查图画对没有(标签是否重叠、文字是否溢出),不对就改源再重转。
|
|
7
|
+
*
|
|
8
|
+
* 三道闸门,避免污染上下文 / 浪费 token:
|
|
9
|
+
* 1. vision gate —— 当前模型不支持视觉时不读文件,只回文字占位。
|
|
10
|
+
* 2. 格式 gate —— 只支持 png/jpeg/gif/webp(provider image 块能吃的);
|
|
11
|
+
* svg/pdf 等回文字提示「先转 PNG」。
|
|
12
|
+
* 3. 大小 gate —— 超过 MAX_BYTES 回文字提示,不读进 base64。
|
|
13
|
+
*/
|
|
14
|
+
import { readFile, stat } from "node:fs/promises";
|
|
15
|
+
import { extname, isAbsolute, resolve } from "node:path";
|
|
16
|
+
import { capabilitiesFor } from "../../llm/capabilities/index.js";
|
|
17
|
+
const MAX_BYTES = 5 * 1024 * 1024; // 5MB —— vision 模型按 tile 计 token,大图无益且撑大请求
|
|
18
|
+
const MEDIA_TYPES = {
|
|
19
|
+
".png": "image/png",
|
|
20
|
+
".jpg": "image/jpeg",
|
|
21
|
+
".jpeg": "image/jpeg",
|
|
22
|
+
".gif": "image/gif",
|
|
23
|
+
".webp": "image/webp",
|
|
24
|
+
};
|
|
25
|
+
export const viewImageToolDef = {
|
|
26
|
+
name: "view_image",
|
|
27
|
+
description: "Load a local image file into the conversation so you can SEE it (vision). " +
|
|
28
|
+
"Use after generating or rendering an image (e.g. SVG/Mermaid → PNG) to verify it " +
|
|
29
|
+
"looks right — check for overlapping labels, clipped text, wrong layout — then fix the " +
|
|
30
|
+
"source and re-render if needed. Supports PNG/JPEG/GIF/WebP only; convert SVG/PDF to PNG " +
|
|
31
|
+
"first. Requires a vision-capable model; otherwise the image is skipped.",
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: {
|
|
35
|
+
path: {
|
|
36
|
+
type: "string",
|
|
37
|
+
description: "Path to the image file (absolute, or relative to the working directory).",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
required: ["path"],
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
export async function viewImageTool(args, ctx) {
|
|
44
|
+
const rawPath = args.path;
|
|
45
|
+
if (typeof rawPath !== "string" || !rawPath.trim()) {
|
|
46
|
+
return "Error: path is required";
|
|
47
|
+
}
|
|
48
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
49
|
+
const abs = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);
|
|
50
|
+
// 闸门 1:vision gate —— 不支持视觉就不读文件。
|
|
51
|
+
// 缺 llmConfig 时按「非视觉」处理(fail closed),对齐 DEFAULT_CAPABILITY
|
|
52
|
+
// 的保守姿态:运行时 ToolContext.llmConfig 必有,缺失只发生在测试 / 异常
|
|
53
|
+
// 装配下,此时绝不把 base64 读进上下文。
|
|
54
|
+
const supportsVision = (() => {
|
|
55
|
+
if (!ctx?.llmConfig)
|
|
56
|
+
return false;
|
|
57
|
+
const kind = (ctx.llmConfig.providerKind ?? ctx.llmConfig.provider);
|
|
58
|
+
return capabilitiesFor(kind, ctx.llmConfig.model).supportsVision;
|
|
59
|
+
})();
|
|
60
|
+
if (!supportsVision) {
|
|
61
|
+
return `[图片未加载: ${abs} —— 当前模型不支持视觉输入,已跳过。切换到 vision 模型后再 view_image。]`;
|
|
62
|
+
}
|
|
63
|
+
// 闸门 2:格式 gate
|
|
64
|
+
const ext = extname(abs).toLowerCase();
|
|
65
|
+
const mediaType = MEDIA_TYPES[ext];
|
|
66
|
+
if (!mediaType) {
|
|
67
|
+
return `[图片未加载: ${abs} —— 格式 ${ext || "(无扩展名)"} 不支持视觉预览,请先转成 PNG/JPEG。]`;
|
|
68
|
+
}
|
|
69
|
+
// 闸门 3:大小 gate(读 stat,不读全文件)
|
|
70
|
+
let size;
|
|
71
|
+
try {
|
|
72
|
+
size = (await stat(abs)).size;
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
return `Error: 无法读取 ${abs}: ${err.message}`;
|
|
76
|
+
}
|
|
77
|
+
if (size > MAX_BYTES) {
|
|
78
|
+
const mb = (size / 1024 / 1024).toFixed(1);
|
|
79
|
+
return `[图片未加载: ${abs} —— 文件过大 (${mb} MB > 5 MB),请先压缩或缩放再 view_image。]`;
|
|
80
|
+
}
|
|
81
|
+
let buf;
|
|
82
|
+
try {
|
|
83
|
+
buf = await readFile(abs);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
return `Error: 无法读取 ${abs}: ${err.message}`;
|
|
87
|
+
}
|
|
88
|
+
const data = buf.toString("base64");
|
|
89
|
+
const kb = Math.round(buf.length / 1024);
|
|
90
|
+
return {
|
|
91
|
+
contentBlocks: [
|
|
92
|
+
{ type: "image", source: { type: "base64", media_type: mediaType, data } },
|
|
93
|
+
],
|
|
94
|
+
result: `[已加载图片: ${abs} (${mediaType}, ${kb} KB)]`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -21,5 +21,8 @@ export interface ResolvedSearchConfig {
|
|
|
21
21
|
* the user where the value came from when debugging.
|
|
22
22
|
*/
|
|
23
23
|
export declare function resolveSearchConfig(cwd?: string): ResolvedSearchConfig;
|
|
24
|
+
export declare function isWebSearchAvailable(cwd?: string, nowMs?: number): boolean;
|
|
24
25
|
export declare const webSearchToolDef: ToolDefinition;
|
|
25
|
-
export declare function webSearchTool(args: Record<string, unknown
|
|
26
|
+
export declare function webSearchTool(args: Record<string, unknown>, ctx?: {
|
|
27
|
+
cwd?: string;
|
|
28
|
+
}): Promise<string>;
|
|
@@ -18,7 +18,11 @@ export function resolveSearchConfig(cwd = process.cwd()) {
|
|
|
18
18
|
let settingsKey;
|
|
19
19
|
let settingsBaseUrl;
|
|
20
20
|
try {
|
|
21
|
-
|
|
21
|
+
// scope "full" so the USER-level ~/.code-shell/settings.json is read, not
|
|
22
|
+
// just ${cwd}/.code-shell — the search key is typically a personal/global
|
|
23
|
+
// credential. Default "project" scope would miss it (the "No search provider
|
|
24
|
+
// configured" bug, esp. for automation runs rooted at the app dir).
|
|
25
|
+
const merged = new SettingsManager(cwd, "full").get();
|
|
22
26
|
const search = merged.search ?? {};
|
|
23
27
|
const p = search.provider;
|
|
24
28
|
if (p === "serper" || p === "tavily" || p === "searxng")
|
|
@@ -68,6 +72,33 @@ export function resolveSearchConfig(cwd = process.cwd()) {
|
|
|
68
72
|
return { provider: "searxng", baseUrl: envSearxng, source: "env" };
|
|
69
73
|
return { provider: "serper", source: "none" };
|
|
70
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Tool-visibility guard: WebSearch is only useful when a search provider is
|
|
77
|
+
* configured. Mirrors the runtime check in the tool itself (source === "none"
|
|
78
|
+
* means no provider).
|
|
79
|
+
*
|
|
80
|
+
* Cached per-cwd with a short TTL: this runs on EVERY message's toolDefs
|
|
81
|
+
* assembly (engine.ts), and resolveSearchConfig reads + parses settings from
|
|
82
|
+
* disk each call. The 1s TTL collapses back-to-back reads while still picking
|
|
83
|
+
* up a newly-configured provider before the user's next message. The execution
|
|
84
|
+
* path (resolveSearchConfig) is intentionally NOT cached — only this boolean.
|
|
85
|
+
*/
|
|
86
|
+
const availCache = new Map();
|
|
87
|
+
const AVAIL_TTL_MS = 1000;
|
|
88
|
+
export function isWebSearchAvailable(cwd = process.cwd(), nowMs = Date.now()) {
|
|
89
|
+
const hit = availCache.get(cwd);
|
|
90
|
+
if (hit && nowMs - hit.at < AVAIL_TTL_MS)
|
|
91
|
+
return hit.value;
|
|
92
|
+
let value = false;
|
|
93
|
+
try {
|
|
94
|
+
value = resolveSearchConfig(cwd).source !== "none";
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
value = false; // unresolved config → treat as unavailable
|
|
98
|
+
}
|
|
99
|
+
availCache.set(cwd, { at: nowMs, value });
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
71
102
|
export const webSearchToolDef = {
|
|
72
103
|
name: "WebSearch",
|
|
73
104
|
description: "Search the web for information. Returns a list of search results with titles, URLs, and snippets. " +
|
|
@@ -84,12 +115,14 @@ export const webSearchToolDef = {
|
|
|
84
115
|
required: ["query"],
|
|
85
116
|
},
|
|
86
117
|
};
|
|
87
|
-
export async function webSearchTool(args) {
|
|
118
|
+
export async function webSearchTool(args, ctx) {
|
|
88
119
|
const query = args.query;
|
|
89
120
|
if (!query)
|
|
90
121
|
return "Error: query is required";
|
|
91
122
|
const numResults = Math.min(args.num_results || 10, 20);
|
|
92
|
-
|
|
123
|
+
// Use the run's cwd (e.g. an automation job's project dir) so per-project
|
|
124
|
+
// settings resolve correctly; falls back to process.cwd() when absent.
|
|
125
|
+
const config = resolveSearchConfig(ctx?.cwd);
|
|
93
126
|
if (config.source === "none") {
|
|
94
127
|
return "Error: No search provider configured. Open desktop 设置 → 连接 to add credentials, or set SERPER_API_KEY / TAVILY_API_KEY / SEARXNG_URL.";
|
|
95
128
|
}
|
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
* Built-in Write file tool.
|
|
3
3
|
*/
|
|
4
4
|
import type { ToolDefinition } from "../../types.js";
|
|
5
|
+
import type { ToolContext } from "../context.js";
|
|
5
6
|
export declare const writeToolDef: ToolDefinition;
|
|
6
|
-
export declare function writeTool(args: Record<string, unknown
|
|
7
|
+
export declare function writeTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { writeFile, mkdir } from "node:fs/promises";
|
|
5
5
|
import { dirname } from "node:path";
|
|
6
6
|
import { fileCache } from "./file-cache.js";
|
|
7
|
+
import { enforcePathPolicy } from "../path-policy.js";
|
|
7
8
|
export const writeToolDef = {
|
|
8
9
|
name: "Write",
|
|
9
10
|
description: "Write content to a file. Creates the file if it doesn't exist, " +
|
|
@@ -17,13 +18,18 @@ export const writeToolDef = {
|
|
|
17
18
|
required: ["file_path", "content"],
|
|
18
19
|
},
|
|
19
20
|
};
|
|
20
|
-
export async function writeTool(args) {
|
|
21
|
+
export async function writeTool(args, ctx) {
|
|
21
22
|
const filePath = args.file_path;
|
|
22
23
|
const content = args.content;
|
|
23
24
|
if (!filePath)
|
|
24
25
|
return "Error: file_path is required";
|
|
25
26
|
if (content === undefined)
|
|
26
27
|
return "Error: content is required";
|
|
28
|
+
// Path policy gate: deny writes to sensitive paths, refuse writes outside
|
|
29
|
+
// workspace until approved. See tool-system/path-policy.ts.
|
|
30
|
+
const blocked = enforcePathPolicy(filePath, "write", ctx?.cwd);
|
|
31
|
+
if (blocked)
|
|
32
|
+
return blocked;
|
|
27
33
|
try {
|
|
28
34
|
await mkdir(dirname(filePath), { recursive: true });
|
|
29
35
|
await writeFile(filePath, content, "utf-8");
|
|
@@ -82,6 +82,8 @@ export interface SubAgentSpawnRequest {
|
|
|
82
82
|
* the parent's appendSystemPrompt (current behavior).
|
|
83
83
|
*/
|
|
84
84
|
appendSystemPrompt?: string;
|
|
85
|
+
/** True for read-only reviewer/researcher children; tunes investigation guard reminders. */
|
|
86
|
+
readOnlySession?: boolean;
|
|
85
87
|
}
|
|
86
88
|
export interface SubAgentSpawner {
|
|
87
89
|
/** Run a sub-agent synchronously and return its text output. */
|
|
@@ -181,6 +183,16 @@ export interface ToolContext {
|
|
|
181
183
|
* `settings.disabledPlugins` by Engine.run().
|
|
182
184
|
*/
|
|
183
185
|
disabledPlugins?: string[];
|
|
186
|
+
/**
|
|
187
|
+
* Builtin tool names the project's capabilityOverrides marked `off` for
|
|
188
|
+
* this cwd. Engine.run() already HIDES these from the LLM-visible tool
|
|
189
|
+
* list, but the model can still NAME a hidden builtin (hallucinated, or
|
|
190
|
+
* remembered from an earlier turn when it was visible). The executor reads
|
|
191
|
+
* this set and rejects such a call — turning `off` into a real execution
|
|
192
|
+
* gate, not just a prompt-visibility filter. Empty/undefined for
|
|
193
|
+
* sub-agents and no-cwd contexts (same as readBuiltinOverride).
|
|
194
|
+
*/
|
|
195
|
+
disabledBuiltins?: Set<string>;
|
|
184
196
|
}
|
|
185
197
|
/**
|
|
186
198
|
* Per-Engine container that produces a fresh ToolContext on demand.
|
|
@@ -40,7 +40,6 @@ export declare class ToolExecutor {
|
|
|
40
40
|
isConcurrencySafe(toolName: string): boolean;
|
|
41
41
|
executeSingle(callIn: ToolCall): Promise<ToolResult>;
|
|
42
42
|
private isReadOnlyBashCommand;
|
|
43
|
-
executeAll(calls: ToolCall[]): Promise<ToolResult[]>;
|
|
44
43
|
/**
|
|
45
44
|
* Convert tool results into Message entries for the transcript.
|
|
46
45
|
*/
|
|
@@ -5,6 +5,7 @@ import { setMaxListeners } from "node:events";
|
|
|
5
5
|
import { logger as rootLogger, getCurrentSid } from "../logging/logger.js";
|
|
6
6
|
import { recordToolCall, recordToolResult } from "../logging/session-recorder.js";
|
|
7
7
|
import { validateToolArgs } from "./validation.js";
|
|
8
|
+
import { PLAN_MODE_ALLOWED_TOOLS } from "./plan-mode-allowlist.js";
|
|
8
9
|
// A1 hardening: hooks must never promote a non-`allow` classifier
|
|
9
10
|
// decision to `allow`. They may otherwise adjust the decision freely
|
|
10
11
|
// (e.g. tighten `allow` to `deny`/`ask`, or relax `deny` to `ask` to
|
|
@@ -83,22 +84,41 @@ export class ToolExecutor {
|
|
|
83
84
|
// Local `call` so we can rewrite args via pre_tool_use updatedInput
|
|
84
85
|
// without mutating the caller's object.
|
|
85
86
|
let call = callIn;
|
|
87
|
+
// 0. Abort fast-path: if the run signal is already aborted, return a
|
|
88
|
+
// synthetic error result WITHOUT running pre_tool_use hooks, the
|
|
89
|
+
// permission classifier, or the handler. registry.executeTool also checks
|
|
90
|
+
// abort, but only after this method has paid for all the per-tool
|
|
91
|
+
// hook/permission round-trips. For an aborted sub-agent whose turn queued
|
|
92
|
+
// a batch of tools (e.g. 10 Reads), this collapses the whole batch
|
|
93
|
+
// instantly instead of round-tripping each one — part of the sub-agent
|
|
94
|
+
// leak fix that keeps an aborted child from burning work post-abort.
|
|
95
|
+
if (this.signal?.aborted) {
|
|
96
|
+
return {
|
|
97
|
+
id: call.id,
|
|
98
|
+
toolName: call.toolName,
|
|
99
|
+
error: `Tool aborted before execution: ${call.toolName}`,
|
|
100
|
+
isError: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// 0. Capability override: a builtin the project marked `off` is HIDDEN from
|
|
104
|
+
// the model's tool list (engine.ts applyBuiltinOverrideVisibility), but the
|
|
105
|
+
// model can still NAME it (hallucination, or a remembered earlier turn). The
|
|
106
|
+
// registry still holds the tool, so without a gate here it would execute.
|
|
107
|
+
// Reject it the same way plan mode rejects a disallowed tool — return an
|
|
108
|
+
// error result, never run the handler.
|
|
109
|
+
if (this.toolCtx?.disabledBuiltins?.has(call.toolName)) {
|
|
110
|
+
return {
|
|
111
|
+
id: call.id,
|
|
112
|
+
toolName: call.toolName,
|
|
113
|
+
error: `Tool ${call.toolName} is disabled by this project's capability override and cannot be used. Do NOT retry this tool call.`,
|
|
114
|
+
isError: true,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
86
117
|
// 0. Plan mode: only allow read-only tools — no file writes at all
|
|
87
118
|
if (this.toolCtx?.planMode) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
"Read",
|
|
92
|
-
"Glob",
|
|
93
|
-
"Grep",
|
|
94
|
-
"WebSearch",
|
|
95
|
-
"WebFetch",
|
|
96
|
-
"AskUserQuestion",
|
|
97
|
-
"Agent",
|
|
98
|
-
"ToolSearch",
|
|
99
|
-
"TodoWrite",
|
|
100
|
-
]);
|
|
101
|
-
if (!allowedInPlan.has(call.toolName)) {
|
|
119
|
+
// Shared with engine.ts's tool-visibility filter so the set the model
|
|
120
|
+
// SEES and the set the executor RUNS can't drift (they had).
|
|
121
|
+
if (!PLAN_MODE_ALLOWED_TOOLS.has(call.toolName)) {
|
|
102
122
|
if (call.toolName === "Bash" && this.isReadOnlyBashCommand(call.args)) {
|
|
103
123
|
// Read-only bash is fine
|
|
104
124
|
}
|
|
@@ -447,32 +467,6 @@ export class ToolExecutor {
|
|
|
447
467
|
}
|
|
448
468
|
return false;
|
|
449
469
|
}
|
|
450
|
-
async executeAll(calls) {
|
|
451
|
-
const results = [];
|
|
452
|
-
// Separate safe (concurrent) and unsafe (sequential) tools
|
|
453
|
-
const safe = [];
|
|
454
|
-
const unsafe = [];
|
|
455
|
-
for (const call of calls) {
|
|
456
|
-
const tool = this.registry.getTool(call.toolName);
|
|
457
|
-
if (tool?.isConcurrencySafe) {
|
|
458
|
-
safe.push(call);
|
|
459
|
-
}
|
|
460
|
-
else {
|
|
461
|
-
unsafe.push(call);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
// Execute safe tools concurrently
|
|
465
|
-
if (safe.length > 0) {
|
|
466
|
-
const safeResults = await Promise.all(safe.map((c) => this.executeSingle(c)));
|
|
467
|
-
results.push(...safeResults);
|
|
468
|
-
}
|
|
469
|
-
// Execute unsafe tools sequentially
|
|
470
|
-
for (const call of unsafe) {
|
|
471
|
-
const result = await this.executeSingle(call);
|
|
472
|
-
results.push(result);
|
|
473
|
-
}
|
|
474
|
-
return results;
|
|
475
|
-
}
|
|
476
470
|
/**
|
|
477
471
|
* Convert tool results into Message entries for the transcript.
|
|
478
472
|
*/
|
|
@@ -21,6 +21,7 @@ export interface GuardDecision {
|
|
|
21
21
|
block?: string;
|
|
22
22
|
prepend?: string;
|
|
23
23
|
}
|
|
24
|
+
export type InvestigationGuardPolicy = "normal" | "soft" | "read-only-review";
|
|
24
25
|
export declare class InvestigationGuard {
|
|
25
26
|
private readHistory;
|
|
26
27
|
private consecutiveReads;
|
|
@@ -35,8 +36,10 @@ export declare class InvestigationGuard {
|
|
|
35
36
|
* be killed by the guard — it should still get the signal to change
|
|
36
37
|
* strategy, but the tool call goes through.
|
|
37
38
|
*/
|
|
38
|
-
private
|
|
39
|
+
private policy;
|
|
40
|
+
private readOnlyReviewBudgetReminderShown;
|
|
39
41
|
setSoftMode(soft: boolean): void;
|
|
42
|
+
setPolicy(policy: InvestigationGuardPolicy): void;
|
|
40
43
|
preToolCheck(call: ToolCall): GuardDecision | undefined;
|
|
41
44
|
noteText(text: string | undefined): void;
|
|
42
45
|
noteToolResult(_call: ToolCall, _result: ToolResult): void;
|
|
@@ -38,9 +38,13 @@ export class InvestigationGuard {
|
|
|
38
38
|
* be killed by the guard — it should still get the signal to change
|
|
39
39
|
* strategy, but the tool call goes through.
|
|
40
40
|
*/
|
|
41
|
-
|
|
41
|
+
policy = "normal";
|
|
42
|
+
readOnlyReviewBudgetReminderShown = false;
|
|
42
43
|
setSoftMode(soft) {
|
|
43
|
-
this.
|
|
44
|
+
this.policy = soft ? "soft" : "normal";
|
|
45
|
+
}
|
|
46
|
+
setPolicy(policy) {
|
|
47
|
+
this.policy = policy;
|
|
44
48
|
}
|
|
45
49
|
preToolCheck(call) {
|
|
46
50
|
const tool = call.toolName;
|
|
@@ -68,7 +72,7 @@ export class InvestigationGuard {
|
|
|
68
72
|
`Re-reading the same content will not produce new information. ` +
|
|
69
73
|
`Switch strategy: run a command with side effects (Bash, debug log, repro), make a code change, or ask the user a specific question. ` +
|
|
70
74
|
`If you genuinely need this content, summarize what you already know about it from prior reads instead of re-fetching.`;
|
|
71
|
-
if (this.
|
|
75
|
+
if (this.policy !== "normal") {
|
|
72
76
|
// Soft mode (headless): deliver the same signal as a prepend so the
|
|
73
77
|
// model still sees it, but allow the tool call to proceed. Hard-
|
|
74
78
|
// blocking an unattended `code-shell run …` would dead-end the task
|
|
@@ -84,6 +88,14 @@ export class InvestigationGuard {
|
|
|
84
88
|
`Re-reading rarely yields new information. After this result, change strategy (Bash side-effect, edit, or ask user) rather than reading further.</system-reminder>`);
|
|
85
89
|
}
|
|
86
90
|
if (this.consecutiveReads > READ_BUDGET) {
|
|
91
|
+
if (this.policy === "read-only-review") {
|
|
92
|
+
if (!this.readOnlyReviewBudgetReminderShown) {
|
|
93
|
+
this.readOnlyReviewBudgetReminderShown = true;
|
|
94
|
+
prependParts.push(`<system-reminder>Investigation guard: ${this.consecutiveReads} consecutive read-only calls during an explicit read-only review. ` +
|
|
95
|
+
`Continue only if each read is adding new evidence, and surface a concise status update when useful.</system-reminder>`);
|
|
96
|
+
}
|
|
97
|
+
return prependParts.length ? { prepend: prependParts.join("\n") } : undefined;
|
|
98
|
+
}
|
|
87
99
|
prependParts.push(`<system-reminder>Investigation guard: ${this.consecutiveReads} consecutive read-only calls with no action taken. ` +
|
|
88
100
|
`Per coding.md, change strategy now — make a code change, run a command with side effects, or ask the user.</system-reminder>`);
|
|
89
101
|
}
|
|
@@ -106,6 +118,10 @@ export class InvestigationGuard {
|
|
|
106
118
|
this.turnHasSideEffect = false;
|
|
107
119
|
if (wasSilent >= SILENT_TURN_BUDGET && turnNumber !== this.lastReminderSilent) {
|
|
108
120
|
this.lastReminderSilent = turnNumber;
|
|
121
|
+
if (this.policy === "read-only-review") {
|
|
122
|
+
return (`<system-reminder>Investigation guard: you have taken ${wasSilent} consecutive turns of read-only investigation ` +
|
|
123
|
+
`without any text update to the user. Surface a brief status update with what you have confirmed, ruled out, and will inspect next.</system-reminder>`);
|
|
124
|
+
}
|
|
109
125
|
return (`<system-reminder>Investigation guard: you have taken ${wasSilent} consecutive turns of read-only investigation ` +
|
|
110
126
|
`without any text update to the user or side-effecting action. ` +
|
|
111
127
|
`Either surface a status update (what have you ruled out, what's your current hypothesis?), ` +
|