@armadra/agent 0.6.5 → 0.6.7
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/CHANGELOG.md +19 -0
- package/CHANGELOG.zh-CN.md +15 -0
- package/dist/agent/prompt-rules.d.ts +2 -0
- package/dist/agent/prompt-rules.js +6 -1
- package/dist/agent/session-tools.js +1 -0
- package/dist/agents/external.d.ts +7 -1
- package/dist/agents/external.js +21 -3
- package/dist/bundle/ama.cjs +125 -23
- package/dist/i18n/catalog.d.ts +1 -0
- package/dist/i18n/messages/print.d.ts +2 -0
- package/dist/i18n/messages/print.js +4 -0
- package/dist/modes/interactive/tool-summary.js +2 -0
- package/dist/modes/print/print-mode.d.ts +2 -1
- package/dist/modes/print/print-mode.js +21 -4
- package/dist/modes/print/search-hint.d.ts +10 -0
- package/dist/modes/print/search-hint.js +26 -0
- package/dist/tools/glob.js +2 -1
- package/dist/tools/grep.d.ts +3 -0
- package/dist/tools/grep.js +46 -9
- package/dist/tools/read.d.ts +5 -0
- package/dist/tools/read.js +20 -1
- package/dist/tools/types.d.ts +5 -0
- package/docs/agents.md +1 -1
- package/docs/codemode.md +2 -0
- package/docs/en/host-api.md +1 -1
- package/docs/host-api.md +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ English · [简体中文](CHANGELOG.zh-CN.md)
|
|
|
5
5
|
> This file is in English starting with 0.6.0. Release notes for 0.1 through 0.5.1 are in Chinese in
|
|
6
6
|
> [CHANGELOG.zh-CN.md](CHANGELOG.zh-CN.md). New entries go into both files.
|
|
7
7
|
|
|
8
|
+
## 0.6.7 (2026-10-03)
|
|
9
|
+
|
|
10
|
+
- **A host runner with id `ama` is used**: when the host registers a runner for `ama` via `HostApi.runners.provide`
|
|
11
|
+
(for example another ama node on an Armadra canvas), `task(agent="ama")` now goes to it instead of being unknown; without
|
|
12
|
+
one nothing changes, and the built-in types (`general` / `explore` / `plan`) are always ama sub-sessions. Docs:
|
|
13
|
+
docs/agents.md.
|
|
14
|
+
|
|
15
|
+
## 0.6.6 (2026-10-03)
|
|
16
|
+
|
|
17
|
+
- **Local search hints**: when both `grep` and `glob` are directly available, the system prompt's rules gain one line
|
|
18
|
+
("Locate code with grep/glob before reading; do not guess file paths."); the `grep` / `glob` descriptions now say when
|
|
19
|
+
to use them; `grep` takes `filesOnly: true` to list only the matching files (deduplicated, sorted by path, `limit` counts
|
|
20
|
+
files). `read` on a directory now points at a tool the model can actually call: `ls` when active, otherwise `glob`
|
|
21
|
+
(e.g. `pattern "src/*"`), otherwise a file inside it. In `-p` with the `minimal` / `coordinator` preset, a denied bash
|
|
22
|
+
`grep` / `rg` / `find` adds one stderr line on how to bring the tools back (`tools.default: ["+grep","+glob"]`). Tools
|
|
23
|
+
get the session's active tool set as the optional, read-only `ToolContext.activeTools`. This changes the `default`
|
|
24
|
+
prefix (about +35 tokens), so the first request after upgrading misses the cache once. Docs: docs/design.md §5.6,
|
|
25
|
+
docs/codemode.md.
|
|
26
|
+
|
|
8
27
|
## 0.6.5 (2026-10-03)
|
|
9
28
|
|
|
10
29
|
- **`AcpClient` can pass MCP servers when opening a session**: `newSession`, `resumeSession` and `loadSession` take an
|
package/CHANGELOG.zh-CN.md
CHANGED
|
@@ -4,6 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
> 从 0.6.0 起 [CHANGELOG.md](CHANGELOG.md) 为英文,本文件保留中文记录(0.1–0.5.1 的完整历史在此)。新条目两份都要加。
|
|
6
6
|
|
|
7
|
+
## 0.6.7(2026-10-03)
|
|
8
|
+
|
|
9
|
+
- **宿主注入的 `ama` runner 生效**:宿主经 `HostApi.runners.provide` 注入 id 为 `ama` 的 runner(如 Armadra 画布上另一个
|
|
10
|
+
ama 节点)时,`task(agent="ama")` 交给它;没注入时行为不变,内置类型(`general` / `explore` / `plan`)始终是 ama 子会话。
|
|
11
|
+
文档:docs/agents.md。
|
|
12
|
+
|
|
13
|
+
## 0.6.6(2026-10-03)
|
|
14
|
+
|
|
15
|
+
- **本地检索提示**:`grep` 与 `glob` 都直接可用时,系统提示的规则节多一句「先 grep / glob 定位再 read,不要猜路径」;`grep` /
|
|
16
|
+
`glob` 的描述写明何时用;`grep` 新增 `filesOnly: true`,只列命中文件(去重、按路径排序,`limit` 按文件数计)。`read` 收到目录时
|
|
17
|
+
指向模型当前真能调用的工具:有 `ls` 说用 ls,否则有 `glob` 说用 glob(例如 `pattern "src/*"`),都没有则说读目录里的文件。`-p`
|
|
18
|
+
下 `minimal` / `coordinator` 预设里 bash 的 `grep` / `rg` / `find` 被拒时,stderr 补一行怎么加回
|
|
19
|
+
(`tools.default: ["+grep","+glob"]`)。工具经可选、只读的 `ToolContext.activeTools` 拿到会话活动集。`default` 前缀因此
|
|
20
|
+
变化(约 +35 token),升级后首个请求缓存未命中一次。文档:docs/design.md §5.6、docs/codemode.md。
|
|
21
|
+
|
|
7
22
|
## 0.6.5(2026-10-03)
|
|
8
23
|
|
|
9
24
|
- **`AcpClient` 开会话可传 MCP 服务器**:`newSession`、`resumeSession`、`loadSession` 新增可选的第三个参数
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
* 保持一句、英文、只写模型确实会做错的事;预算见 `src/cli/prompt-budget.test.ts`。
|
|
6
6
|
* 「edit 的多处修改」写在 edit 工具描述里(只在 edit 可用时出现);并行批量在 codemode 独占时
|
|
7
7
|
* 由它自己的说明负责,所以只在 read 直接可用时给。
|
|
8
|
+
* 「先定位再读」只在 grep 与 glob 都直接可用时给(docs/search-plan.md §4.2:`minimal` 下模型会连猜文件名)。
|
|
8
9
|
*/
|
|
9
10
|
import type { ToolDefinition } from "../tools/types.js";
|
|
10
11
|
export declare const DESTRUCTIVE_RULE = "Ask before destructive commands (rm -rf, git reset --hard, force push, deleting branches) unless the user requested them.";
|
|
11
12
|
export declare const PARALLEL_READS_RULE = "Batch independent read-only tool calls into one turn.";
|
|
13
|
+
export declare const LOCATE_RULE = "Locate code with grep/glob before reading; do not guess file paths.";
|
|
12
14
|
export declare function baseRules(tools: readonly Pick<ToolDefinition, "name">[]): string[];
|
|
@@ -5,14 +5,19 @@
|
|
|
5
5
|
* 保持一句、英文、只写模型确实会做错的事;预算见 `src/cli/prompt-budget.test.ts`。
|
|
6
6
|
* 「edit 的多处修改」写在 edit 工具描述里(只在 edit 可用时出现);并行批量在 codemode 独占时
|
|
7
7
|
* 由它自己的说明负责,所以只在 read 直接可用时给。
|
|
8
|
+
* 「先定位再读」只在 grep 与 glob 都直接可用时给(docs/search-plan.md §4.2:`minimal` 下模型会连猜文件名)。
|
|
8
9
|
*/
|
|
9
10
|
export const DESTRUCTIVE_RULE = "Ask before destructive commands (rm -rf, git reset --hard, force push, deleting branches) unless the user requested them.";
|
|
10
11
|
export const PARALLEL_READS_RULE = "Batch independent read-only tool calls into one turn.";
|
|
12
|
+
export const LOCATE_RULE = "Locate code with grep/glob before reading; do not guess file paths.";
|
|
11
13
|
export function baseRules(tools) {
|
|
12
14
|
if (tools.length === 0)
|
|
13
15
|
return [];
|
|
14
16
|
const rules = [DESTRUCTIVE_RULE];
|
|
15
|
-
|
|
17
|
+
const has = (name) => tools.some((tool) => tool.name === name);
|
|
18
|
+
if (has("read"))
|
|
16
19
|
rules.push(PARALLEL_READS_RULE);
|
|
20
|
+
if (has("grep") && has("glob"))
|
|
21
|
+
rules.push(LOCATE_RULE);
|
|
17
22
|
return rules;
|
|
18
23
|
}
|
|
@@ -266,6 +266,7 @@ export function createToolContext(core, runner, call, signal, onUpdate) {
|
|
|
266
266
|
markRead: (path) => {
|
|
267
267
|
core.readFiles.add(path);
|
|
268
268
|
},
|
|
269
|
+
activeTools: new Set(core.activeToolNames()),
|
|
269
270
|
tools: {
|
|
270
271
|
executeTool: (name, input, options) => {
|
|
271
272
|
nested++;
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* 外部 Agent 接入 `task(agent=…)`(docs/wave5-plan.md §5.3–§5.5、§7.3、§7.6,D13、D15、D17)。[W5-EG]
|
|
3
3
|
*
|
|
4
4
|
* - 名字:`claude` / `codex` / `acp:<program>` / 驱动表里的其它 id(`gemini` …,`ama` 除外——那是子会话
|
|
5
|
-
* 类型的 runner 名;ama 自己经 ACP 用 `acp:ama`)/ 宿主注入的 runner id
|
|
5
|
+
* 类型的 runner 名;ama 自己经 ACP 用 `acp:ama`)/ 宿主注入的 runner id(宿主注入的 `ama` 在类型里的 runner
|
|
6
|
+
* 名是 `host:ama`,交给宿主;没注入时 `ama` 照旧不是外部 Agent)。启动时 PATH 上找得到的
|
|
6
7
|
* claude / codex 与已注入的宿主 runner 登记进类型目录(进 task 工具描述);其余按需解析(不进描述,
|
|
7
8
|
* 描述在会话内字节不变)。
|
|
8
9
|
* - 每个主会话一个 {@link ExternalAgents}:审批接 `requestApproval`(只走 broker 链,不经 gateToolCall
|
|
@@ -34,6 +35,11 @@ export declare const externalTesting: {
|
|
|
34
35
|
export declare function isExternalSpec(name: string): boolean;
|
|
35
36
|
/** 外部 CLI 的类型(`task(agent="claude")`)。 */
|
|
36
37
|
export declare function externalDefinition(spec: string): AgentDefinition;
|
|
38
|
+
/**
|
|
39
|
+
* 宿主注入的 id 恰为 `ama` 的 runner(画布上另一个 ama 节点)在类型里的 runner 名。裸 `ama` 是子会话
|
|
40
|
+
* 类型的 runner 名(注册表按它起子会话),不能共用;名字仍是 `ama`,`task(agent="ama")` 照常解析。
|
|
41
|
+
*/
|
|
42
|
+
export declare const HOST_AMA_RUNNER = "host:ama";
|
|
37
43
|
export declare function hostDefinition(runner: HostRunner): AgentDefinition;
|
|
38
44
|
/** 只查 PATH(不起进程):候选链里任一程序在 PATH 上。 */
|
|
39
45
|
export declare function externalOnPath(spec: string, env: NodeJS.ProcessEnv): boolean;
|
package/dist/agents/external.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* 外部 Agent 接入 `task(agent=…)`(docs/wave5-plan.md §5.3–§5.5、§7.3、§7.6,D13、D15、D17)。[W5-EG]
|
|
3
3
|
*
|
|
4
4
|
* - 名字:`claude` / `codex` / `acp:<program>` / 驱动表里的其它 id(`gemini` …,`ama` 除外——那是子会话
|
|
5
|
-
* 类型的 runner 名;ama 自己经 ACP 用 `acp:ama`)/ 宿主注入的 runner id
|
|
5
|
+
* 类型的 runner 名;ama 自己经 ACP 用 `acp:ama`)/ 宿主注入的 runner id(宿主注入的 `ama` 在类型里的 runner
|
|
6
|
+
* 名是 `host:ama`,交给宿主;没注入时 `ama` 照旧不是外部 Agent)。启动时 PATH 上找得到的
|
|
6
7
|
* claude / codex 与已注入的宿主 runner 登记进类型目录(进 task 工具描述);其余按需解析(不进描述,
|
|
7
8
|
* 描述在会话内字节不变)。
|
|
8
9
|
* - 每个主会话一个 {@link ExternalAgents}:审批接 `requestApproval`(只走 broker 链,不经 gateToolCall
|
|
@@ -58,8 +59,17 @@ function definition(name, runner, description, source) {
|
|
|
58
59
|
export function externalDefinition(spec) {
|
|
59
60
|
return definition(spec, spec, `External ${labelOf(spec)} CLI (its own login, model and permission policy; full instructions in prompt).`, "builtin");
|
|
60
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* 宿主注入的 id 恰为 `ama` 的 runner(画布上另一个 ama 节点)在类型里的 runner 名。裸 `ama` 是子会话
|
|
64
|
+
* 类型的 runner 名(注册表按它起子会话),不能共用;名字仍是 `ama`,`task(agent="ama")` 照常解析。
|
|
65
|
+
*/
|
|
66
|
+
export const HOST_AMA_RUNNER = "host:ama";
|
|
67
|
+
/** 宿主 runner id → 类型里的 runner 名。 */
|
|
68
|
+
function hostRunnerName(id) {
|
|
69
|
+
return id === "ama" ? HOST_AMA_RUNNER : id;
|
|
70
|
+
}
|
|
61
71
|
export function hostDefinition(runner) {
|
|
62
|
-
return definition(runner.id, runner.id, runner.description, "host");
|
|
72
|
+
return definition(runner.id, hostRunnerName(runner.id), runner.description, "host");
|
|
63
73
|
}
|
|
64
74
|
/** 只查 PATH(不起进程):候选链里任一程序在 PATH 上。 */
|
|
65
75
|
export function externalOnPath(spec, env) {
|
|
@@ -207,8 +217,16 @@ export class SessionExternalAgents {
|
|
|
207
217
|
runner(agent) {
|
|
208
218
|
if (agent.runner === "ama")
|
|
209
219
|
return undefined;
|
|
210
|
-
|
|
220
|
+
// 宿主注入的 `ama` 走宿主;宿主已注销时报不可用,不退回自起(驱动表里也有一个 ama)。
|
|
221
|
+
// 宿主 id 不在 AgentRunnerSpec 的字面量里(见 definition),按字符串比
|
|
222
|
+
const runnerName = agent.runner;
|
|
223
|
+
const hostAma = runnerName === HOST_AMA_RUNNER;
|
|
224
|
+
const spec = hostAma ? "ama" : agent.runner;
|
|
211
225
|
const host = this.wiring.hostRunners?.get(spec);
|
|
226
|
+
if (hostAma && host === undefined) {
|
|
227
|
+
const error = new AmaError("agent_host_only", `"ama" was provided by the host and is no longer available`);
|
|
228
|
+
return { id: spec, start: () => Promise.reject(error) };
|
|
229
|
+
}
|
|
212
230
|
let inner;
|
|
213
231
|
try {
|
|
214
232
|
inner = this.agents.resolve(spec);
|
package/dist/bundle/ama.cjs
CHANGED
|
@@ -41,7 +41,7 @@ var AMA_VERSION;
|
|
|
41
41
|
var init_version = __esm({
|
|
42
42
|
"src/version.ts"() {
|
|
43
43
|
"use strict";
|
|
44
|
-
AMA_VERSION = true ? "0.6.
|
|
44
|
+
AMA_VERSION = true ? "0.6.7" : readPackageVersion();
|
|
45
45
|
}
|
|
46
46
|
});
|
|
47
47
|
|
|
@@ -3475,7 +3475,8 @@ var init_print = __esm({
|
|
|
3475
3475
|
planPending: (version, where) => `ama: plan v${version} saved, awaiting approval (not executed): ${where}; -p does not approve on anyone's behalf — approve it in the interactive UI or via RPC plan_response, or set plan.unattended: approve to let -p approve and continue`,
|
|
3476
3476
|
limitTurns: (limit) => `ama: reached the turn limit ${limit} (--max-turns / limits.maxTurns); run ended before finishing`,
|
|
3477
3477
|
limitCost: (limit, spent) => `ama: reached the cost limit ${limit} (spent ${spent}; --max-cost / limits.maxCostUsd); run ended before finishing`,
|
|
3478
|
-
denied: (count2, tools, reason) => `ama: ${plural(count2, "tool call")} denied: ${tools.join(", ")}${reason !== "" ? ` (${reason})` : ""}; -p has no one to approve; to allow them use --permission-mode auto-edit|auto or --allow <rule
|
|
3478
|
+
denied: (count2, tools, reason) => `ama: ${plural(count2, "tool call")} denied: ${tools.join(", ")}${reason !== "" ? ` (${reason})` : ""}; -p has no one to approve; to allow them use --permission-mode auto-edit|auto or --allow <rule>`,
|
|
3479
|
+
searchToolsMissing: (preset, missing) => `ama: the ${preset} preset has no ${missing.join(" / ")}; add ${missing.length === 1 ? "it" : "them"} with tools.default: [${missing.map((name) => `"+${name}"`).join(",")}]`
|
|
3479
3480
|
},
|
|
3480
3481
|
/** RPC 响应的 `error`(人读)。 */
|
|
3481
3482
|
rpc: {
|
|
@@ -3520,7 +3521,8 @@ var init_print = __esm({
|
|
|
3520
3521
|
planPending: (version, where) => `ama: 计划 v${version} 已落盘、待审批(未执行):${where};-p 不替人批准——在交互界面或 RPC plan_response 里审批,或设 plan.unattended: approve 让 -p 批准后接着执行`,
|
|
3521
3522
|
limitTurns: (limit) => `ama: 已达到回合上限 ${limit}(--max-turns / limits.maxTurns),运行在完成前结束`,
|
|
3522
3523
|
limitCost: (limit, spent) => `ama: 已达到费用上限 ${limit}(累计 ${spent};--max-cost / limits.maxCostUsd),运行在完成前结束`,
|
|
3523
|
-
denied: (count2, tools, reason) => `ama: ${count2} 次工具调用被拒:${tools.join("、")}${reason !== "" ? `(${reason})` : ""};-p 没有人审批,需要放行时用 --permission-mode auto-edit|auto 或 --allow
|
|
3524
|
+
denied: (count2, tools, reason) => `ama: ${count2} 次工具调用被拒:${tools.join("、")}${reason !== "" ? `(${reason})` : ""};-p 没有人审批,需要放行时用 --permission-mode auto-edit|auto 或 --allow <规则>`,
|
|
3525
|
+
searchToolsMissing: (preset, missing) => `ama: ${preset} 预设没有 ${missing.join(" / ")},可用 tools.default: [${missing.map((name) => `"+${name}"`).join(",")}] 加上`
|
|
3524
3526
|
},
|
|
3525
3527
|
rpc: {
|
|
3526
3528
|
badTaskId: "taskId 应为字符串",
|
|
@@ -8090,7 +8092,7 @@ function createGlobTool() {
|
|
|
8090
8092
|
return {
|
|
8091
8093
|
name: "glob",
|
|
8092
8094
|
label: "Glob",
|
|
8093
|
-
description: "Find files by glob (`**`, `*`, `?`, `[...]`, `{a,b}`, leading `!` excludes). Patterns without `/` match names at any depth. Respects .gitignore/.ignore; newest first.",
|
|
8095
|
+
description: "Find files by glob (`**`, `*`, `?`, `[...]`, `{a,b}`, leading `!` excludes). Patterns without `/` match names at any depth. Respects .gitignore/.ignore; newest first. Use to discover files before read.",
|
|
8094
8096
|
parameters: {
|
|
8095
8097
|
type: "object",
|
|
8096
8098
|
properties: {
|
|
@@ -14456,6 +14458,18 @@ function isBinary(buf) {
|
|
|
14456
14458
|
function error(message) {
|
|
14457
14459
|
return { content: message, isError: true };
|
|
14458
14460
|
}
|
|
14461
|
+
function directoryHint(shown2, ctx) {
|
|
14462
|
+
const active = ctx.activeTools;
|
|
14463
|
+
if (active === void 0 || active.has("ls")) return "use the ls tool instead";
|
|
14464
|
+
if (active.has("glob")) {
|
|
14465
|
+
if (shown2 === ".") return 'use glob (e.g. pattern "*")';
|
|
14466
|
+
if (shown2.startsWith("/") || /^[A-Za-z]:/.test(shown2) || /[*?[\]{}!\\]/.test(shown2)) {
|
|
14467
|
+
return `use glob (e.g. path ${JSON.stringify(shown2)}, pattern "*")`;
|
|
14468
|
+
}
|
|
14469
|
+
return `use glob (e.g. pattern ${JSON.stringify(`${shown2}/*`)})`;
|
|
14470
|
+
}
|
|
14471
|
+
return "read a file inside it";
|
|
14472
|
+
}
|
|
14459
14473
|
function numberLines(lines2, firstLine3) {
|
|
14460
14474
|
return lines2.map((line2, i) => `${String(firstLine3 + i).padStart(6)} ${line2}`).join("\n");
|
|
14461
14475
|
}
|
|
@@ -14495,7 +14509,7 @@ async function executeRead(input2, ctx, options = {}) {
|
|
|
14495
14509
|
} catch {
|
|
14496
14510
|
return error(`File not found: ${shown2}`);
|
|
14497
14511
|
}
|
|
14498
|
-
if (info2.isDirectory()) return error(`${shown2} is a directory;
|
|
14512
|
+
if (info2.isDirectory()) return error(`${shown2} is a directory; ${directoryHint(shown2, ctx)}`);
|
|
14499
14513
|
if (imageMimeFromPath(abs) !== void 0) return readImage(abs, shown2, ctx, options);
|
|
14500
14514
|
const buf = await (0, import_promises5.readFile)(abs);
|
|
14501
14515
|
if (isBinary(buf)) return error(`${shown2} appears to be a binary file; refusing to read it`);
|
|
@@ -15861,7 +15875,7 @@ function compilePattern(input2) {
|
|
|
15861
15875
|
const source = input2.literal ? escapeRegExp(input2.pattern) : input2.pattern;
|
|
15862
15876
|
return new RegExp(source, input2.ignoreCase ? "i" : "");
|
|
15863
15877
|
}
|
|
15864
|
-
async function searchFile(abs, regex) {
|
|
15878
|
+
async function searchFile(abs, regex, firstOnly = false) {
|
|
15865
15879
|
try {
|
|
15866
15880
|
const info2 = await (0, import_promises9.stat)(abs);
|
|
15867
15881
|
if (!info2.isFile() || info2.size > GREP_MAX_FILE_BYTES) return void 0;
|
|
@@ -15870,9 +15884,11 @@ async function searchFile(abs, regex) {
|
|
|
15870
15884
|
const lines2 = buf.toString("utf8").replace(/\r\n/g, "\n").split("\n");
|
|
15871
15885
|
if (lines2[lines2.length - 1] === "") lines2.pop();
|
|
15872
15886
|
const hits = [];
|
|
15873
|
-
lines2.
|
|
15874
|
-
if (regex.test(
|
|
15875
|
-
|
|
15887
|
+
for (let i = 0; i < lines2.length; i++) {
|
|
15888
|
+
if (!regex.test(lines2[i])) continue;
|
|
15889
|
+
hits.push(i);
|
|
15890
|
+
if (firstOnly) break;
|
|
15891
|
+
}
|
|
15876
15892
|
return hits.length > 0 ? { lines: lines2, hits } : void 0;
|
|
15877
15893
|
} catch {
|
|
15878
15894
|
return void 0;
|
|
@@ -15929,6 +15945,7 @@ async function executeGrep(input2, ctx) {
|
|
|
15929
15945
|
return { content: `Path not found: ${displayPath(root, ctx.cwd)}`, isError: true };
|
|
15930
15946
|
}
|
|
15931
15947
|
const files = await candidateFiles(root, rootIsFile, input2.glob, ctx.signal);
|
|
15948
|
+
if (input2.filesOnly === true) return grepFilesOnly(files, regex, limit, ctx);
|
|
15932
15949
|
const blocks = [];
|
|
15933
15950
|
let matches2 = 0;
|
|
15934
15951
|
let matchedFiles = 0;
|
|
@@ -15974,21 +15991,49 @@ async function executeGrep(input2, ctx) {
|
|
|
15974
15991
|
details: { matches: matches2, files: matchedFiles, limited, truncated: cut.truncated }
|
|
15975
15992
|
};
|
|
15976
15993
|
}
|
|
15994
|
+
async function grepFilesOnly(files, regex, limit, ctx) {
|
|
15995
|
+
const sorted = [...new Set(files)].sort();
|
|
15996
|
+
const shown2 = [];
|
|
15997
|
+
let limited = false;
|
|
15998
|
+
outer: for (let i = 0; i < sorted.length; i += GREP_CONCURRENCY) {
|
|
15999
|
+
if (ctx.signal.aborted) return { content: "aborted by user", isError: true };
|
|
16000
|
+
const batch = sorted.slice(i, i + GREP_CONCURRENCY);
|
|
16001
|
+
const results = await Promise.all(batch.map((f) => searchFile(f, regex, true)));
|
|
16002
|
+
for (let j = 0; j < batch.length; j++) {
|
|
16003
|
+
if (!results[j]) continue;
|
|
16004
|
+
if (shown2.length >= limit) {
|
|
16005
|
+
limited = true;
|
|
16006
|
+
break outer;
|
|
16007
|
+
}
|
|
16008
|
+
shown2.push(displayPath(batch[j], ctx.cwd));
|
|
16009
|
+
}
|
|
16010
|
+
}
|
|
16011
|
+
if (shown2.length === 0) {
|
|
16012
|
+
return { content: "No matches found", details: { matches: 0, files: 0, filesOnly: true } };
|
|
16013
|
+
}
|
|
16014
|
+
const unique3 = [...new Set(shown2)].sort();
|
|
16015
|
+
let content = unique3.join("\n");
|
|
16016
|
+
if (limited) content += `
|
|
16017
|
+
|
|
16018
|
+
[Stopped at ${limit} files. Narrow the search or raise limit.]`;
|
|
16019
|
+
return { content, details: { files: unique3.length, filesOnly: true, limited } };
|
|
16020
|
+
}
|
|
15977
16021
|
function createGrepTool() {
|
|
15978
16022
|
return {
|
|
15979
16023
|
name: "grep",
|
|
15980
16024
|
label: "Grep",
|
|
15981
|
-
description: "
|
|
16025
|
+
description: "Use to find where a symbol or text appears (JS regex). Skips ignored, binary, >2 MB files. Output `path:line: text`; filesOnly lists matching files.",
|
|
15982
16026
|
parameters: {
|
|
15983
16027
|
type: "object",
|
|
15984
16028
|
properties: {
|
|
15985
16029
|
pattern: { type: "string" },
|
|
15986
|
-
path: { type: "string", description: "File or dir
|
|
15987
|
-
glob: { type: "string", description: "
|
|
16030
|
+
path: { type: "string", description: "File or dir" },
|
|
16031
|
+
glob: { type: "string", description: "e.g. *.ts" },
|
|
15988
16032
|
ignoreCase: { type: "boolean" },
|
|
15989
16033
|
literal: { type: "boolean" },
|
|
15990
16034
|
context: { type: "integer", description: "0-5 lines" },
|
|
15991
|
-
limit: { type: "integer", description: `Default ${DEFAULT_GREP_LIMIT}` }
|
|
16035
|
+
limit: { type: "integer", description: `Default ${DEFAULT_GREP_LIMIT}` },
|
|
16036
|
+
filesOnly: { type: "boolean" }
|
|
15992
16037
|
},
|
|
15993
16038
|
required: ["pattern"],
|
|
15994
16039
|
additionalProperties: false
|
|
@@ -24309,15 +24354,18 @@ var init_session_subagent = __esm({
|
|
|
24309
24354
|
function baseRules(tools) {
|
|
24310
24355
|
if (tools.length === 0) return [];
|
|
24311
24356
|
const rules = [DESTRUCTIVE_RULE];
|
|
24312
|
-
|
|
24357
|
+
const has = (name) => tools.some((tool) => tool.name === name);
|
|
24358
|
+
if (has("read")) rules.push(PARALLEL_READS_RULE);
|
|
24359
|
+
if (has("grep") && has("glob")) rules.push(LOCATE_RULE);
|
|
24313
24360
|
return rules;
|
|
24314
24361
|
}
|
|
24315
|
-
var DESTRUCTIVE_RULE, PARALLEL_READS_RULE;
|
|
24362
|
+
var DESTRUCTIVE_RULE, PARALLEL_READS_RULE, LOCATE_RULE;
|
|
24316
24363
|
var init_prompt_rules = __esm({
|
|
24317
24364
|
"src/agent/prompt-rules.ts"() {
|
|
24318
24365
|
"use strict";
|
|
24319
24366
|
DESTRUCTIVE_RULE = "Ask before destructive commands (rm -rf, git reset --hard, force push, deleting branches) unless the user requested them.";
|
|
24320
24367
|
PARALLEL_READS_RULE = "Batch independent read-only tool calls into one turn.";
|
|
24368
|
+
LOCATE_RULE = "Locate code with grep/glob before reading; do not guess file paths.";
|
|
24321
24369
|
}
|
|
24322
24370
|
});
|
|
24323
24371
|
|
|
@@ -25493,6 +25541,7 @@ function createToolContext(core, runner, call, signal, onUpdate) {
|
|
|
25493
25541
|
markRead: (path) => {
|
|
25494
25542
|
core.readFiles.add(path);
|
|
25495
25543
|
},
|
|
25544
|
+
activeTools: new Set(core.activeToolNames()),
|
|
25496
25545
|
tools: {
|
|
25497
25546
|
executeTool: (name, input2, options) => {
|
|
25498
25547
|
nested++;
|
|
@@ -33492,8 +33541,11 @@ function externalDefinition(spec2) {
|
|
|
33492
33541
|
"builtin"
|
|
33493
33542
|
);
|
|
33494
33543
|
}
|
|
33544
|
+
function hostRunnerName(id) {
|
|
33545
|
+
return id === "ama" ? HOST_AMA_RUNNER : id;
|
|
33546
|
+
}
|
|
33495
33547
|
function hostDefinition(runner) {
|
|
33496
|
-
return definition(runner.id, runner.id, runner.description, "host");
|
|
33548
|
+
return definition(runner.id, hostRunnerName(runner.id), runner.description, "host");
|
|
33497
33549
|
}
|
|
33498
33550
|
function externalOnPath(spec2, env) {
|
|
33499
33551
|
const found = candidatesFor(spec2);
|
|
@@ -33545,7 +33597,7 @@ function mergeAgentInfos(base4, external) {
|
|
|
33545
33597
|
}
|
|
33546
33598
|
return out;
|
|
33547
33599
|
}
|
|
33548
|
-
var import_node_crypto14, LISTED_EXTERNAL_AGENTS, NO_AGENT_PROBE_ENV, externalTesting, ExternalTaskHandle, infoCache, SessionExternalAgents;
|
|
33600
|
+
var import_node_crypto14, LISTED_EXTERNAL_AGENTS, NO_AGENT_PROBE_ENV, externalTesting, HOST_AMA_RUNNER, ExternalTaskHandle, infoCache, SessionExternalAgents;
|
|
33549
33601
|
var init_external = __esm({
|
|
33550
33602
|
"src/agents/external.ts"() {
|
|
33551
33603
|
"use strict";
|
|
@@ -33562,6 +33614,7 @@ var init_external = __esm({
|
|
|
33562
33614
|
LISTED_EXTERNAL_AGENTS = ["claude", "codex"];
|
|
33563
33615
|
NO_AGENT_PROBE_ENV = "AMA_NO_AGENT_PROBE";
|
|
33564
33616
|
externalTesting = {};
|
|
33617
|
+
HOST_AMA_RUNNER = "host:ama";
|
|
33565
33618
|
ExternalTaskHandle = class {
|
|
33566
33619
|
constructor(inner, reopen) {
|
|
33567
33620
|
this.inner = inner;
|
|
@@ -33640,8 +33693,17 @@ var init_external = __esm({
|
|
|
33640
33693
|
/** 注册表的 `runners(agent)`:ama 子会话类型返回 undefined。 */
|
|
33641
33694
|
runner(agent) {
|
|
33642
33695
|
if (agent.runner === "ama") return void 0;
|
|
33643
|
-
const
|
|
33696
|
+
const runnerName = agent.runner;
|
|
33697
|
+
const hostAma = runnerName === HOST_AMA_RUNNER;
|
|
33698
|
+
const spec2 = hostAma ? "ama" : agent.runner;
|
|
33644
33699
|
const host = this.wiring.hostRunners?.get(spec2);
|
|
33700
|
+
if (hostAma && host === void 0) {
|
|
33701
|
+
const error2 = new AmaError(
|
|
33702
|
+
"agent_host_only",
|
|
33703
|
+
`"ama" was provided by the host and is no longer available`
|
|
33704
|
+
);
|
|
33705
|
+
return { id: spec2, start: () => Promise.reject(error2) };
|
|
33706
|
+
}
|
|
33645
33707
|
let inner;
|
|
33646
33708
|
try {
|
|
33647
33709
|
inner = this.agents.resolve(spec2);
|
|
@@ -45639,6 +45701,7 @@ function resultSummary(input2, theme) {
|
|
|
45639
45701
|
case "grep": {
|
|
45640
45702
|
const matches2 = num9(details["matches"]);
|
|
45641
45703
|
if (matches2 === 0) return muted(m.noMatch);
|
|
45704
|
+
if (details["filesOnly"] === true) return muted(m.files(num9(details["files"]) ?? count2));
|
|
45642
45705
|
if (matches2 === void 0) return muted(m.lines(count2));
|
|
45643
45706
|
return muted(m.matches(matches2, num9(details["files"])));
|
|
45644
45707
|
}
|
|
@@ -47910,6 +47973,27 @@ var init_json_event = __esm({
|
|
|
47910
47973
|
}
|
|
47911
47974
|
});
|
|
47912
47975
|
|
|
47976
|
+
// src/modes/print/search-hint.ts
|
|
47977
|
+
function isSearchCommand(command3) {
|
|
47978
|
+
return SEARCH_COMMAND.test(command3);
|
|
47979
|
+
}
|
|
47980
|
+
function searchToolsHint(deniedBashCommands, preset, activeTools) {
|
|
47981
|
+
if (!HINT_PRESETS.has(preset)) return void 0;
|
|
47982
|
+
const missing = ["grep", "glob"].filter((name) => !activeTools.includes(name));
|
|
47983
|
+
if (missing.length === 0) return void 0;
|
|
47984
|
+
if (!deniedBashCommands.some(isSearchCommand)) return void 0;
|
|
47985
|
+
return msg().print.print.searchToolsMissing(preset, missing);
|
|
47986
|
+
}
|
|
47987
|
+
var SEARCH_COMMAND, HINT_PRESETS;
|
|
47988
|
+
var init_search_hint = __esm({
|
|
47989
|
+
"src/modes/print/search-hint.ts"() {
|
|
47990
|
+
"use strict";
|
|
47991
|
+
init_i18n();
|
|
47992
|
+
SEARCH_COMMAND = /(?:^|[;&|(`]|\$\()\s*(?:[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*(?:sudo\s+)?(?:\S*\/)?(?:grep|egrep|fgrep|rg|ag|ack|find|fd)(?=\s|$)/;
|
|
47993
|
+
HINT_PRESETS = /* @__PURE__ */ new Set(["minimal", "coordinator"]);
|
|
47994
|
+
}
|
|
47995
|
+
});
|
|
47996
|
+
|
|
47913
47997
|
// src/modes/print/print-mode.ts
|
|
47914
47998
|
var print_mode_exports = {};
|
|
47915
47999
|
__export(print_mode_exports, {
|
|
@@ -48002,6 +48086,8 @@ async function runPrintMode(runtime, context) {
|
|
|
48002
48086
|
return ExitCode.Usage;
|
|
48003
48087
|
}
|
|
48004
48088
|
const denied = [];
|
|
48089
|
+
const bashCommands = /* @__PURE__ */ new Map();
|
|
48090
|
+
const deniedBash = [];
|
|
48005
48091
|
let limit;
|
|
48006
48092
|
let plan2;
|
|
48007
48093
|
let stopWaiting = () => void 0;
|
|
@@ -48014,12 +48100,19 @@ async function runPrintMode(runtime, context) {
|
|
|
48014
48100
|
stopWaiting();
|
|
48015
48101
|
} else if (event.type === "plan_proposed") plan2 = event;
|
|
48016
48102
|
else if (event.type === "plan_resolved" && event.planId === plan2?.planId) plan2 = void 0;
|
|
48017
|
-
if (event.type === "
|
|
48103
|
+
if (event.type === "tool_execution_start" && event.toolName === "bash") {
|
|
48104
|
+
const command3 = event.args?.command;
|
|
48105
|
+
if (typeof command3 === "string") bashCommands.set(event.toolCallId, command3);
|
|
48106
|
+
}
|
|
48107
|
+
if (event.type === "tool_execution_end" && event.denied === true) {
|
|
48018
48108
|
denied.push({
|
|
48019
48109
|
toolCallId: event.toolCallId,
|
|
48020
48110
|
toolName: event.toolName,
|
|
48021
48111
|
reason: textOf5(event)
|
|
48022
48112
|
});
|
|
48113
|
+
const command3 = bashCommands.get(event.toolCallId);
|
|
48114
|
+
if (event.toolName === "bash" && command3 !== void 0) deniedBash.push(command3);
|
|
48115
|
+
}
|
|
48023
48116
|
if (format === "stream-json") io.stdout(`${toJsonLine(toWireEvent(event))}
|
|
48024
48117
|
`);
|
|
48025
48118
|
else if (event.type === "auto_retry_start")
|
|
@@ -48098,6 +48191,16 @@ async function runPrintMode(runtime, context) {
|
|
|
48098
48191
|
`
|
|
48099
48192
|
);
|
|
48100
48193
|
}
|
|
48194
|
+
const deniedText = () => {
|
|
48195
|
+
const hint = searchToolsHint(
|
|
48196
|
+
deniedBash,
|
|
48197
|
+
runtime.config.tools?.preset ?? "default",
|
|
48198
|
+
runtime.tools.active().map((tool) => tool.name)
|
|
48199
|
+
);
|
|
48200
|
+
return `${describeDenied(denied)}
|
|
48201
|
+
${hint === void 0 ? "" : `${hint}
|
|
48202
|
+
`}`;
|
|
48203
|
+
};
|
|
48101
48204
|
if (signalled !== void 0) return signalled;
|
|
48102
48205
|
if (stdoutClosed) return ExitCode.Ok;
|
|
48103
48206
|
if (failure2 !== void 0) {
|
|
@@ -48106,8 +48209,7 @@ async function runPrintMode(runtime, context) {
|
|
|
48106
48209
|
return ExitCode.RuntimeError;
|
|
48107
48210
|
}
|
|
48108
48211
|
if (limit !== void 0) {
|
|
48109
|
-
if (denied.length > 0) io.stderr(
|
|
48110
|
-
`);
|
|
48212
|
+
if (denied.length > 0) io.stderr(deniedText());
|
|
48111
48213
|
io.stderr(`${describeLimit(limit)}
|
|
48112
48214
|
`);
|
|
48113
48215
|
return ExitCode.LimitReached;
|
|
@@ -48117,8 +48219,7 @@ async function runPrintMode(runtime, context) {
|
|
|
48117
48219
|
`);
|
|
48118
48220
|
return ExitCode.RuntimeError;
|
|
48119
48221
|
}
|
|
48120
|
-
if (denied.length > 0) io.stderr(
|
|
48121
|
-
`);
|
|
48222
|
+
if (denied.length > 0) io.stderr(deniedText());
|
|
48122
48223
|
if (plan2 !== void 0) {
|
|
48123
48224
|
io.stderr(`${describePlanPending(plan2)}
|
|
48124
48225
|
`);
|
|
@@ -48165,6 +48266,7 @@ var init_print_mode = __esm({
|
|
|
48165
48266
|
init_subagent_registry();
|
|
48166
48267
|
init_image_input();
|
|
48167
48268
|
init_i18n();
|
|
48269
|
+
init_search_hint();
|
|
48168
48270
|
STDIN_WAIT_HINT_MS = 3e3;
|
|
48169
48271
|
STDIN_FIRST_BYTE_MS = 2e3;
|
|
48170
48272
|
}
|
package/dist/i18n/catalog.d.ts
CHANGED
|
@@ -1268,6 +1268,7 @@ declare const en: {
|
|
|
1268
1268
|
limitTurns: (limit: number) => string;
|
|
1269
1269
|
limitCost: (limit: string, spent: string) => string;
|
|
1270
1270
|
denied: (count: number, tools: readonly string[], reason: string) => string;
|
|
1271
|
+
searchToolsMissing: (preset: string, missing: readonly string[]) => string;
|
|
1271
1272
|
};
|
|
1272
1273
|
rpc: {
|
|
1273
1274
|
unknownCommand: (type: string) => string;
|
|
@@ -20,6 +20,7 @@ export declare const en: {
|
|
|
20
20
|
limitTurns: (limit: number) => string;
|
|
21
21
|
limitCost: (limit: string, spent: string) => string;
|
|
22
22
|
denied: (count: number, tools: readonly string[], reason: string) => string;
|
|
23
|
+
searchToolsMissing: (preset: string, missing: readonly string[]) => string;
|
|
23
24
|
};
|
|
24
25
|
/** RPC 响应的 `error`(人读)。 */
|
|
25
26
|
rpc: {
|
|
@@ -62,6 +63,7 @@ export declare const zh: {
|
|
|
62
63
|
limitTurns: (limit: number) => string;
|
|
63
64
|
limitCost: (limit: string, spent: string) => string;
|
|
64
65
|
denied: (count: number, tools: readonly string[], reason: string) => string;
|
|
66
|
+
searchToolsMissing: (preset: string, missing: readonly string[]) => string;
|
|
65
67
|
};
|
|
66
68
|
rpc: {
|
|
67
69
|
badTaskId: string;
|
|
@@ -23,6 +23,8 @@ export const en = {
|
|
|
23
23
|
limitCost: (limit, spent) => `ama: reached the cost limit ${limit} (spent ${spent}; --max-cost / limits.maxCostUsd); run ended before finishing`,
|
|
24
24
|
denied: (count, tools, reason) => `ama: ${plural(count, "tool call")} denied: ${tools.join(", ")}${reason !== "" ? ` (${reason})` : ""}; ` +
|
|
25
25
|
"-p has no one to approve; to allow them use --permission-mode auto-edit|auto or --allow <rule>",
|
|
26
|
+
searchToolsMissing: (preset, missing) => `ama: the ${preset} preset has no ${missing.join(" / ")}; add ${missing.length === 1 ? "it" : "them"} with ` +
|
|
27
|
+
`tools.default: [${missing.map((name) => `"+${name}"`).join(",")}]`,
|
|
26
28
|
},
|
|
27
29
|
/** RPC 响应的 `error`(人读)。 */
|
|
28
30
|
rpc: {
|
|
@@ -67,6 +69,8 @@ export const zh = {
|
|
|
67
69
|
limitCost: (limit, spent) => `ama: 已达到费用上限 ${limit}(累计 ${spent};--max-cost / limits.maxCostUsd),运行在完成前结束`,
|
|
68
70
|
denied: (count, tools, reason) => `ama: ${count} 次工具调用被拒:${tools.join("、")}${reason !== "" ? `(${reason})` : ""};` +
|
|
69
71
|
"-p 没有人审批,需要放行时用 --permission-mode auto-edit|auto 或 --allow <规则>",
|
|
72
|
+
searchToolsMissing: (preset, missing) => `ama: ${preset} 预设没有 ${missing.join(" / ")},可用 ` +
|
|
73
|
+
`tools.default: [${missing.map((name) => `"+${name}"`).join(",")}] 加上`,
|
|
70
74
|
},
|
|
71
75
|
rpc: {
|
|
72
76
|
badTaskId: "taskId 应为字符串",
|
|
@@ -191,6 +191,8 @@ export function resultSummary(input, theme) {
|
|
|
191
191
|
const matches = num(details["matches"]);
|
|
192
192
|
if (matches === 0)
|
|
193
193
|
return muted(m.noMatch);
|
|
194
|
+
if (details["filesOnly"] === true)
|
|
195
|
+
return muted(m.files(num(details["files"]) ?? count));
|
|
194
196
|
if (matches === undefined)
|
|
195
197
|
return muted(m.lines(count));
|
|
196
198
|
return muted(m.matches(matches, num(details["files"])));
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* `context_pressure`)。
|
|
15
15
|
* - 重试:text / json 格式在 stderr 打一行 `↻ 重试 n/m`(stream-json 里本来就有事件),等待期间不再无声。
|
|
16
16
|
* - 无人值守:ask → deny(bootstrap 已按 print 设 unattended)。被拒的调用(`tool_execution_end`
|
|
17
|
-
* 带 `denied`)在 stderr 汇总一行(工具 ×次数、首个原因、放行办法),json 结果带 `deniedTools
|
|
17
|
+
* 带 `denied`)在 stderr 汇总一行(工具 ×次数、首个原因、放行办法),json 结果带 `deniedTools`;
|
|
18
|
+
* [S-A] minimal / coordinator 下被拒的 bash 形如 grep / rg / find 时再补一行怎么加回 grep / glob。
|
|
18
19
|
* - [W5-H2] 预算(`--max-turns N` / `--max-cost USD` / config `limits.*`,agent/limits.ts):到限
|
|
19
20
|
* (会话发 `limit_reached`)→ stderr 一行、json 带 `limitReached{kind, value, limit}`(回合到限另带
|
|
20
21
|
* `maxTurnsReached: true`)、退出码 8。
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* `context_pressure`)。
|
|
15
15
|
* - 重试:text / json 格式在 stderr 打一行 `↻ 重试 n/m`(stream-json 里本来就有事件),等待期间不再无声。
|
|
16
16
|
* - 无人值守:ask → deny(bootstrap 已按 print 设 unattended)。被拒的调用(`tool_execution_end`
|
|
17
|
-
* 带 `denied`)在 stderr 汇总一行(工具 ×次数、首个原因、放行办法),json 结果带 `deniedTools
|
|
17
|
+
* 带 `denied`)在 stderr 汇总一行(工具 ×次数、首个原因、放行办法),json 结果带 `deniedTools`;
|
|
18
|
+
* [S-A] minimal / coordinator 下被拒的 bash 形如 grep / rg / find 时再补一行怎么加回 grep / glob。
|
|
18
19
|
* - [W5-H2] 预算(`--max-turns N` / `--max-cost USD` / config `limits.*`,agent/limits.ts):到限
|
|
19
20
|
* (会话发 `limit_reached`)→ stderr 一行、json 带 `limitReached{kind, value, limit}`(回合到限另带
|
|
20
21
|
* `maxTurnsReached: true`)、退出码 8。
|
|
@@ -36,6 +37,7 @@ import { formatUsd } from "../../agent/limits.js";
|
|
|
36
37
|
import { registryOf } from "../../agent/subagent-registry.js";
|
|
37
38
|
import { promptImages, sessionModel } from "../image-input.js";
|
|
38
39
|
import { msg } from "../../i18n/index.js";
|
|
40
|
+
import { searchToolsHint } from "./search-hint.js";
|
|
39
41
|
export function joinPrompt(argument, piped) {
|
|
40
42
|
const parts = [argument ?? "", piped.replace(/\s+$/, "")].filter((p) => p.trim() !== "");
|
|
41
43
|
return parts.join("\n\n");
|
|
@@ -121,6 +123,8 @@ export async function runPrintMode(runtime, context) {
|
|
|
121
123
|
return ExitCode.Usage;
|
|
122
124
|
}
|
|
123
125
|
const denied = [];
|
|
126
|
+
const bashCommands = new Map();
|
|
127
|
+
const deniedBash = [];
|
|
124
128
|
let limit;
|
|
125
129
|
let plan;
|
|
126
130
|
let stopWaiting = () => undefined;
|
|
@@ -136,12 +140,21 @@ export async function runPrintMode(runtime, context) {
|
|
|
136
140
|
plan = event;
|
|
137
141
|
else if (event.type === "plan_resolved" && event.planId === plan?.planId)
|
|
138
142
|
plan = undefined;
|
|
139
|
-
if (event.type === "
|
|
143
|
+
if (event.type === "tool_execution_start" && event.toolName === "bash") {
|
|
144
|
+
const command = event.args?.command;
|
|
145
|
+
if (typeof command === "string")
|
|
146
|
+
bashCommands.set(event.toolCallId, command);
|
|
147
|
+
}
|
|
148
|
+
if (event.type === "tool_execution_end" && event.denied === true) {
|
|
140
149
|
denied.push({
|
|
141
150
|
toolCallId: event.toolCallId,
|
|
142
151
|
toolName: event.toolName,
|
|
143
152
|
reason: textOf(event),
|
|
144
153
|
});
|
|
154
|
+
const command = bashCommands.get(event.toolCallId);
|
|
155
|
+
if (event.toolName === "bash" && command !== undefined)
|
|
156
|
+
deniedBash.push(command);
|
|
157
|
+
}
|
|
145
158
|
if (format === "stream-json")
|
|
146
159
|
io.stdout(`${toJsonLine(toWireEvent(event))}\n`);
|
|
147
160
|
else if (event.type === "auto_retry_start")
|
|
@@ -223,6 +236,10 @@ export async function runPrintMode(runtime, context) {
|
|
|
223
236
|
entries: session.entries,
|
|
224
237
|
})}\n`);
|
|
225
238
|
}
|
|
239
|
+
const deniedText = () => {
|
|
240
|
+
const hint = searchToolsHint(deniedBash, runtime.config.tools?.preset ?? "default", runtime.tools.active().map((tool) => tool.name));
|
|
241
|
+
return `${describeDenied(denied)}\n${hint === undefined ? "" : `${hint}\n`}`;
|
|
242
|
+
};
|
|
226
243
|
if (signalled !== undefined)
|
|
227
244
|
return signalled;
|
|
228
245
|
if (stdoutClosed)
|
|
@@ -233,7 +250,7 @@ export async function runPrintMode(runtime, context) {
|
|
|
233
250
|
}
|
|
234
251
|
if (limit !== undefined) {
|
|
235
252
|
if (denied.length > 0)
|
|
236
|
-
io.stderr(
|
|
253
|
+
io.stderr(deniedText());
|
|
237
254
|
io.stderr(`${describeLimit(limit)}\n`);
|
|
238
255
|
return ExitCode.LimitReached;
|
|
239
256
|
}
|
|
@@ -242,7 +259,7 @@ export async function runPrintMode(runtime, context) {
|
|
|
242
259
|
return ExitCode.RuntimeError;
|
|
243
260
|
}
|
|
244
261
|
if (denied.length > 0)
|
|
245
|
-
io.stderr(
|
|
262
|
+
io.stderr(deniedText());
|
|
246
263
|
if (plan !== undefined) {
|
|
247
264
|
io.stderr(`${describePlanPending(plan)}\n`);
|
|
248
265
|
return ExitCode.PlanPending;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [S-A] `-p` 下 bash 搜索命令被拒时的一行提示(docs/search-plan.md §4.2)。
|
|
3
|
+
*
|
|
4
|
+
* `minimal` / `coordinator` 预设没有 grep / glob,模型常改用 `bash grep` / `rg` / `find`,在无人值守下被拒后
|
|
5
|
+
* 只能猜文件名(§1.3 E3)。这里只在「被拒的 bash 命令形如搜索」且活动集缺 grep 或 glob 时提示怎么加回来;
|
|
6
|
+
* 不改权限判定,也不进模型上下文。
|
|
7
|
+
*/
|
|
8
|
+
export declare function isSearchCommand(command: string): boolean;
|
|
9
|
+
/** 被拒的 bash 命令里有搜索类命令、预设是 minimal / coordinator、活动集缺 grep 或 glob → 提示一行;否则 undefined。 */
|
|
10
|
+
export declare function searchToolsHint(deniedBashCommands: readonly string[], preset: string, activeTools: readonly string[]): string | undefined;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [S-A] `-p` 下 bash 搜索命令被拒时的一行提示(docs/search-plan.md §4.2)。
|
|
3
|
+
*
|
|
4
|
+
* `minimal` / `coordinator` 预设没有 grep / glob,模型常改用 `bash grep` / `rg` / `find`,在无人值守下被拒后
|
|
5
|
+
* 只能猜文件名(§1.3 E3)。这里只在「被拒的 bash 命令形如搜索」且活动集缺 grep 或 glob 时提示怎么加回来;
|
|
6
|
+
* 不改权限判定,也不进模型上下文。
|
|
7
|
+
*/
|
|
8
|
+
import { msg } from "../../i18n/index.js";
|
|
9
|
+
/** 命令位置(行首或 `;` `&` `|` `(` 之后,允许前置 `VAR=x` / `sudo`)上的搜索类程序。 */
|
|
10
|
+
const SEARCH_COMMAND = /(?:^|[;&|(`]|\$\()\s*(?:[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*(?:sudo\s+)?(?:\S*\/)?(?:grep|egrep|fgrep|rg|ag|ack|find|fd)(?=\s|$)/;
|
|
11
|
+
/** 只在这两个预设下提示:`default` 本来就有 grep / glob,`codemode-only` 的脚本里可调用它们。 */
|
|
12
|
+
const HINT_PRESETS = new Set(["minimal", "coordinator"]);
|
|
13
|
+
export function isSearchCommand(command) {
|
|
14
|
+
return SEARCH_COMMAND.test(command);
|
|
15
|
+
}
|
|
16
|
+
/** 被拒的 bash 命令里有搜索类命令、预设是 minimal / coordinator、活动集缺 grep 或 glob → 提示一行;否则 undefined。 */
|
|
17
|
+
export function searchToolsHint(deniedBashCommands, preset, activeTools) {
|
|
18
|
+
if (!HINT_PRESETS.has(preset))
|
|
19
|
+
return undefined;
|
|
20
|
+
const missing = ["grep", "glob"].filter((name) => !activeTools.includes(name));
|
|
21
|
+
if (missing.length === 0)
|
|
22
|
+
return undefined;
|
|
23
|
+
if (!deniedBashCommands.some(isSearchCommand))
|
|
24
|
+
return undefined;
|
|
25
|
+
return msg().print.print.searchToolsMissing(preset, missing);
|
|
26
|
+
}
|
package/dist/tools/glob.js
CHANGED
|
@@ -197,7 +197,8 @@ export function createGlobTool() {
|
|
|
197
197
|
name: "glob",
|
|
198
198
|
label: "Glob",
|
|
199
199
|
description: "Find files by glob (`**`, `*`, `?`, `[...]`, `{a,b}`, leading `!` excludes). " +
|
|
200
|
-
"Patterns without `/` match names at any depth. Respects .gitignore/.ignore; newest first."
|
|
200
|
+
"Patterns without `/` match names at any depth. Respects .gitignore/.ignore; newest first. " +
|
|
201
|
+
"Use to discover files before read.",
|
|
201
202
|
parameters: {
|
|
202
203
|
type: "object",
|
|
203
204
|
properties: {
|
package/dist/tools/grep.d.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* - 文件按路径排序处理,输出确定;匹配行 `path:line: text`,上下文行 `path-line- text`,
|
|
6
6
|
* 不相邻的组之间 `--`;匹配行截 500 字符;
|
|
7
7
|
* - 匹配数到 limit(缺省 100)即停并提示;整体输出再按 50 KB 头截断。
|
|
8
|
+
* - [S-A] `filesOnly`:只列命中文件(去重、按路径排序,每个文件命中一行即停),limit 按文件数计;
|
|
9
|
+
* 给模型在大仓库里先缩范围(docs/search-plan.md §4.2)。
|
|
8
10
|
*/
|
|
9
11
|
import type { ToolContext, ToolDefinition, ToolResult } from "./types.js";
|
|
10
12
|
export interface GrepInput {
|
|
@@ -15,6 +17,7 @@ export interface GrepInput {
|
|
|
15
17
|
literal?: boolean;
|
|
16
18
|
context?: number;
|
|
17
19
|
limit?: number;
|
|
20
|
+
filesOnly?: boolean;
|
|
18
21
|
}
|
|
19
22
|
export declare const DEFAULT_GREP_LIMIT = 100;
|
|
20
23
|
export declare const GREP_MAX_FILE_BYTES: number;
|
package/dist/tools/grep.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* - 文件按路径排序处理,输出确定;匹配行 `path:line: text`,上下文行 `path-line- text`,
|
|
6
6
|
* 不相邻的组之间 `--`;匹配行截 500 字符;
|
|
7
7
|
* - 匹配数到 limit(缺省 100)即停并提示;整体输出再按 50 KB 头截断。
|
|
8
|
+
* - [S-A] `filesOnly`:只列命中文件(去重、按路径排序,每个文件命中一行即停),limit 按文件数计;
|
|
9
|
+
* 给模型在大仓库里先缩范围(docs/search-plan.md §4.2)。
|
|
8
10
|
*/
|
|
9
11
|
import { readFile, stat } from "node:fs/promises";
|
|
10
12
|
import { displayPath, resolvePath } from "./paths.js";
|
|
@@ -23,7 +25,7 @@ export function compilePattern(input) {
|
|
|
23
25
|
const source = input.literal ? escapeRegExp(input.pattern) : input.pattern;
|
|
24
26
|
return new RegExp(source, input.ignoreCase ? "i" : "");
|
|
25
27
|
}
|
|
26
|
-
async function searchFile(abs, regex) {
|
|
28
|
+
async function searchFile(abs, regex, firstOnly = false) {
|
|
27
29
|
try {
|
|
28
30
|
const info = await stat(abs);
|
|
29
31
|
if (!info.isFile() || info.size > GREP_MAX_FILE_BYTES)
|
|
@@ -35,10 +37,13 @@ async function searchFile(abs, regex) {
|
|
|
35
37
|
if (lines[lines.length - 1] === "")
|
|
36
38
|
lines.pop();
|
|
37
39
|
const hits = [];
|
|
38
|
-
lines.
|
|
39
|
-
if (regex.test(
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
for (let i = 0; i < lines.length; i++) {
|
|
41
|
+
if (!regex.test(lines[i]))
|
|
42
|
+
continue;
|
|
43
|
+
hits.push(i);
|
|
44
|
+
if (firstOnly)
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
42
47
|
return hits.length > 0 ? { lines, hits } : undefined;
|
|
43
48
|
}
|
|
44
49
|
catch {
|
|
@@ -103,6 +108,8 @@ export async function executeGrep(input, ctx) {
|
|
|
103
108
|
return { content: `Path not found: ${displayPath(root, ctx.cwd)}`, isError: true };
|
|
104
109
|
}
|
|
105
110
|
const files = await candidateFiles(root, rootIsFile, input.glob, ctx.signal);
|
|
111
|
+
if (input.filesOnly === true)
|
|
112
|
+
return grepFilesOnly(files, regex, limit, ctx);
|
|
106
113
|
const blocks = [];
|
|
107
114
|
let matches = 0;
|
|
108
115
|
let matchedFiles = 0;
|
|
@@ -150,22 +157,52 @@ export async function executeGrep(input, ctx) {
|
|
|
150
157
|
details: { matches, files: matchedFiles, limited, truncated: cut.truncated },
|
|
151
158
|
};
|
|
152
159
|
}
|
|
160
|
+
/** [S-A] filesOnly:命中文件的显示路径,去重并按路径排序;limit 按文件数计。 */
|
|
161
|
+
async function grepFilesOnly(files, regex, limit, ctx) {
|
|
162
|
+
const sorted = [...new Set(files)].sort();
|
|
163
|
+
const shown = [];
|
|
164
|
+
let limited = false;
|
|
165
|
+
outer: for (let i = 0; i < sorted.length; i += GREP_CONCURRENCY) {
|
|
166
|
+
if (ctx.signal.aborted)
|
|
167
|
+
return { content: "aborted by user", isError: true };
|
|
168
|
+
const batch = sorted.slice(i, i + GREP_CONCURRENCY);
|
|
169
|
+
const results = await Promise.all(batch.map((f) => searchFile(f, regex, true)));
|
|
170
|
+
for (let j = 0; j < batch.length; j++) {
|
|
171
|
+
if (!results[j])
|
|
172
|
+
continue;
|
|
173
|
+
if (shown.length >= limit) {
|
|
174
|
+
limited = true;
|
|
175
|
+
break outer;
|
|
176
|
+
}
|
|
177
|
+
shown.push(displayPath(batch[j], ctx.cwd));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (shown.length === 0) {
|
|
181
|
+
return { content: "No matches found", details: { matches: 0, files: 0, filesOnly: true } };
|
|
182
|
+
}
|
|
183
|
+
const unique = [...new Set(shown)].sort();
|
|
184
|
+
let content = unique.join("\n");
|
|
185
|
+
if (limited)
|
|
186
|
+
content += `\n\n[Stopped at ${limit} files. Narrow the search or raise limit.]`;
|
|
187
|
+
return { content, details: { files: unique.length, filesOnly: true, limited } };
|
|
188
|
+
}
|
|
153
189
|
export function createGrepTool() {
|
|
154
190
|
return {
|
|
155
191
|
name: "grep",
|
|
156
192
|
label: "Grep",
|
|
157
|
-
description: "
|
|
158
|
-
"files. Output
|
|
193
|
+
description: "Use to find where a symbol or text appears (JS regex). Skips ignored, binary, >2 MB " +
|
|
194
|
+
"files. Output `path:line: text`; filesOnly lists matching files.",
|
|
159
195
|
parameters: {
|
|
160
196
|
type: "object",
|
|
161
197
|
properties: {
|
|
162
198
|
pattern: { type: "string" },
|
|
163
|
-
path: { type: "string", description: "File or dir
|
|
164
|
-
glob: { type: "string", description: "
|
|
199
|
+
path: { type: "string", description: "File or dir" },
|
|
200
|
+
glob: { type: "string", description: "e.g. *.ts" },
|
|
165
201
|
ignoreCase: { type: "boolean" },
|
|
166
202
|
literal: { type: "boolean" },
|
|
167
203
|
context: { type: "integer", description: "0-5 lines" },
|
|
168
204
|
limit: { type: "integer", description: `Default ${DEFAULT_GREP_LIMIT}` },
|
|
205
|
+
filesOnly: { type: "boolean" },
|
|
169
206
|
},
|
|
170
207
|
required: ["pattern"],
|
|
171
208
|
additionalProperties: false,
|
package/dist/tools/read.d.ts
CHANGED
|
@@ -21,6 +21,11 @@ export interface ReadToolOptions {
|
|
|
21
21
|
imageOptions?(ctx: ToolContext): ImageFitOptions;
|
|
22
22
|
}
|
|
23
23
|
export declare function isBinary(buf: Buffer): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* [S-A] read 收到目录时的下一步提示:只指向模型当前真能调用的工具(docs/search-plan.md §4.2)。
|
|
26
|
+
* 活动集未知(宿主自建上下文)时沿用旧文案。
|
|
27
|
+
*/
|
|
28
|
+
export declare function directoryHint(shown: string, ctx: Pick<ToolContext, "activeTools">): string;
|
|
24
29
|
/** `cat -n` 形状。 */
|
|
25
30
|
export declare function numberLines(lines: readonly string[], firstLine: number): string;
|
|
26
31
|
export declare function executeRead(input: ReadInput, ctx: ToolContext, options?: ReadToolOptions): Promise<ToolResult>;
|
package/dist/tools/read.js
CHANGED
|
@@ -23,6 +23,25 @@ export function isBinary(buf) {
|
|
|
23
23
|
function error(message) {
|
|
24
24
|
return { content: message, isError: true };
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* [S-A] read 收到目录时的下一步提示:只指向模型当前真能调用的工具(docs/search-plan.md §4.2)。
|
|
28
|
+
* 活动集未知(宿主自建上下文)时沿用旧文案。
|
|
29
|
+
*/
|
|
30
|
+
export function directoryHint(shown, ctx) {
|
|
31
|
+
const active = ctx.activeTools;
|
|
32
|
+
if (active === undefined || active.has("ls"))
|
|
33
|
+
return "use the ls tool instead";
|
|
34
|
+
if (active.has("glob")) {
|
|
35
|
+
if (shown === ".")
|
|
36
|
+
return 'use glob (e.g. pattern "*")';
|
|
37
|
+
// cwd 外(绝对路径)或目录名含 glob 元字符时改用 path 参数,模式保持 `*`
|
|
38
|
+
if (shown.startsWith("/") || /^[A-Za-z]:/.test(shown) || /[*?[\]{}!\\]/.test(shown)) {
|
|
39
|
+
return `use glob (e.g. path ${JSON.stringify(shown)}, pattern "*")`;
|
|
40
|
+
}
|
|
41
|
+
return `use glob (e.g. pattern ${JSON.stringify(`${shown}/*`)})`;
|
|
42
|
+
}
|
|
43
|
+
return "read a file inside it";
|
|
44
|
+
}
|
|
26
45
|
/** `cat -n` 形状。 */
|
|
27
46
|
export function numberLines(lines, firstLine) {
|
|
28
47
|
return lines.map((line, i) => `${String(firstLine + i).padStart(6)}\t${line}`).join("\n");
|
|
@@ -64,7 +83,7 @@ export async function executeRead(input, ctx, options = {}) {
|
|
|
64
83
|
return error(`File not found: ${shown}`);
|
|
65
84
|
}
|
|
66
85
|
if (info.isDirectory())
|
|
67
|
-
return error(`${shown} is a directory;
|
|
86
|
+
return error(`${shown} is a directory; ${directoryHint(shown, ctx)}`);
|
|
68
87
|
if (imageMimeFromPath(abs) !== undefined)
|
|
69
88
|
return readImage(abs, shown, ctx, options);
|
|
70
89
|
const buf = await readFile(abs);
|
package/dist/tools/types.d.ts
CHANGED
|
@@ -230,6 +230,11 @@ export interface ToolContext {
|
|
|
230
230
|
/** 本会话已 read 的绝对路径(write / edit 先读后写检查)。 */
|
|
231
231
|
readonly readFiles: ReadonlySet<string>;
|
|
232
232
|
markRead(absolutePath: string): void;
|
|
233
|
+
/**
|
|
234
|
+
* [S-A] 本次调用时模型可直接调用的工具名(会话活动集,只读快照);工具据此给出可执行的提示
|
|
235
|
+
* (如 read 收到目录时说用 ls 还是 glob)。宿主 / SDK 自建上下文可不给。
|
|
236
|
+
*/
|
|
237
|
+
readonly activeTools?: ReadonlySet<string>;
|
|
233
238
|
/**
|
|
234
239
|
* 嵌套调用其它工具(codemode 脚本、task),受同一管线;按全部未禁用工具查找(不限于活动集),
|
|
235
240
|
* 发带 `parentToolCallId` 的 tool_execution_* 事件。`signal`:与 ctx.signal 合并,用于提前取消
|
package/docs/agents.md
CHANGED
|
@@ -259,7 +259,7 @@ ama 能以各 CLI 自己的账户、模型与权限策略驱动外部编码 Agen
|
|
|
259
259
|
|
|
260
260
|
### 嵌入宿主
|
|
261
261
|
|
|
262
|
-
有宿主(`--host` 或 profile 的 `host`,如嵌入 Armadra)时 ama **不自己启动外部 CLI**:内置外部 Agent 一律不可用,不写进 task 描述,`task(agent="claude")` 以「由宿主提供」失败;只有宿主经 `HostApi.runners.provide(runner)` 注入的 runner 可用,它以同一个 `task(agent=<id>)` 入口出现,同名时替换内置的。`create()` 时已注入的 runner 写进 task 描述(`- <id>: <description>`),之后注入的也能用但不进描述。runner 的 `start` 收到 `prompt`、`cwd`、`mode`(父会话当前模式)、`taskId`、`signal`、`onEvent
|
|
262
|
+
有宿主(`--host` 或 profile 的 `host`,如嵌入 Armadra)时 ama **不自己启动外部 CLI**:内置外部 Agent 一律不可用,不写进 task 描述,`task(agent="claude")` 以「由宿主提供」失败;只有宿主经 `HostApi.runners.provide(runner)` 注入的 runner 可用,它以同一个 `task(agent=<id>)` 入口出现,同名时替换内置的。`create()` 时已注入的 runner 写进 task 描述(`- <id>: <description>`),之后注入的也能用但不进描述。runner 的 `start` 收到 `prompt`、`cwd`、`mode`(父会话当前模式)、`taskId`、`signal`、`onEvent`;不经首次确认。宿主注入的 runner id 也可以是 `ama`(例如画布上另一个 ama 节点):`task(agent="ama")` 交给宿主,不起子会话;没注入时 `ama` 照旧不是可用类型,内置类型(`general` / `explore` / `plan`)始终是 ama 子会话。
|
|
263
263
|
|
|
264
264
|
**在 Armadra 画布终端里直接运行 ama(没有 `--host`)**:此时 ama 按独立模式工作,自己启动的外部 Agent 是这个终端节点里的子进程,不会出现在画布上,也不经连线授权;需要让多个 Agent 在画布上协同,就在画布上连线 Agent 节点,或以宿主模式嵌入 ama。终端带着该节点的身份变量(`ARMADRA_NODE_ID`、`ARMADRA_SESSION_ID`、Hook 端点、`ARMADRA_CANVAS_CONTROL` 等),ama 起子进程时一律剥离(askpass 除外),否则 Armadra 装在 Claude / Codex 上的 Hook 会把子 Agent 的事件记到这个节点名下。
|
|
265
265
|
|
package/docs/codemode.md
CHANGED
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
|
|
25
25
|
缺省配置里不写 `codemode.mode`(`ama init` 生成的 `config.json` 也不写),所以这张映射以后调整时老用户同样生效。`ama config show` / `ama doctor` 显示生效模式与原因(跟随哪个预设、网络由 Node 还是操作系统沙箱隔离);`ama doctor` 另列操作系统沙箱能力。
|
|
26
26
|
|
|
27
|
+
`minimal` / `coordinator` 预设缺省不带 codemode,也没有 grep / glob:模型找代码只能用 bash(`default` 权限模式下每次审批、`-p` 下被拒)。要检索就 `tools.default: ["+grep","+glob"]` 直接加上,比为了检索打开 codemode 便宜(见 [design.md](design.md) §5.6);`codemode-only` 的脚本里 `tools.grep()` / `tools.glob()` / `tools.ls()` 都可调用。
|
|
28
|
+
|
|
27
29
|
`coordinator` 预设即使显式 `on`,脚本里能调用的工具也只限它的活动集(read 与宿主工具):`tools.bash`、`tools.write` 在脚本里同样不存在,协调者「不写文件、不跑 bash」的约定不能经 codemode 绕过。
|
|
28
30
|
|
|
29
31
|
`codemode` 本身的权限类随沙箱能力:网络隔离(Node ≥ 25,或有操作系统沙箱,见下文沙箱)时是 `read` 类,`default` 权限模式下免审批——脚本只能经 `tools.*` 做事,每次内层调用仍逐个经过权限管线;网络未隔离(Node 22 / 24 且没有操作系统沙箱)时是 `execute` 类,`default` 模式下每次都要审批,`-p` 等无人值守场景直接拒绝,此时常用做法是在配置里放行它:
|
package/docs/en/host-api.md
CHANGED
|
@@ -100,7 +100,7 @@ interface ToolResult {
|
|
|
100
100
|
}
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
-
`ToolContext` provides `toolCallId`, `cwd`, `sessionId`, `sessionFile?`, `signal`, `depth`, `model?`, `thinkingLevel?`, `outputDir?`, `onUpdate(partial)` (output while running), `readFiles` / `markRead`, `tools.executeTool(name, input)` (nested calls through the same pipeline), `session.appendCustom` / `lastCustom` (custom entries that never enter the context, see [session-format.md](../session-format.md), Chinese), `spawnSubagent?` and `log`.
|
|
103
|
+
`ToolContext` provides `toolCallId`, `cwd`, `sessionId`, `sessionFile?`, `signal`, `depth`, `model?`, `thinkingLevel?`, `outputDir?`, `onUpdate(partial)` (output while running), `readFiles` / `markRead`, `activeTools?` (read-only snapshot of the session's active tool set, so tools can give actionable hints), `tools.executeTool(name, input)` (nested calls through the same pipeline), `session.appendCustom` / `lastCustom` (custom entries that never enter the context, see [session-format.md](../session-format.md), Chinese), `spawnSubagent?` and `log`.
|
|
104
104
|
|
|
105
105
|
Host tools take the same path as built-in tools: schema validation → command hook PreToolUse → permission pipeline (classified by `permission`) → approval → execution → PostToolUse. They can also be called from codemode scripts as `tools.<name>()`.
|
|
106
106
|
|
package/docs/host-api.md
CHANGED
|
@@ -95,7 +95,7 @@ interface ToolResult {
|
|
|
95
95
|
}
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
`ToolContext` 提供 `toolCallId`、`cwd`、`sessionId`、`sessionFile?`、`signal`、`depth`、`model?`、`thinkingLevel?`、`outputDir?`、`onUpdate(partial)`(运行中输出)、`readFiles` / `markRead`、`tools.executeTool(name, input)`(嵌套调用,受同一管线)、`session.appendCustom` / `lastCustom`(不进上下文的 custom 条目,见 [session-format.md](session-format.md))、`spawnSubagent?`、`log`。
|
|
98
|
+
`ToolContext` 提供 `toolCallId`、`cwd`、`sessionId`、`sessionFile?`、`signal`、`depth`、`model?`、`thinkingLevel?`、`outputDir?`、`onUpdate(partial)`(运行中输出)、`readFiles` / `markRead`、`activeTools?`(会话活动集的只读快照,工具据此给可执行的提示)、`tools.executeTool(name, input)`(嵌套调用,受同一管线)、`session.appendCustom` / `lastCustom`(不进上下文的 custom 条目,见 [session-format.md](session-format.md))、`spawnSubagent?`、`log`。
|
|
99
99
|
|
|
100
100
|
宿主工具与内置工具走同一条路径:schema 校验 → 命令式 Hook PreToolUse → 权限管线(按 `permission` 分类)→ 审批 → 执行 → PostToolUse。在 codemode 脚本里也能以 `tools.<name>()` 调用。
|
|
101
101
|
|