@armadra/agent 0.7.5 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/CHANGELOG.zh-CN.md +34 -0
- package/dist/agent/session-classifier.d.ts +2 -0
- package/dist/agent/session-classifier.js +24 -3
- package/dist/agent/session-extensions.d.ts +1 -0
- package/dist/agent/session-extensions.js +3 -0
- package/dist/agent/session-subagent.js +9 -2
- package/dist/agent/subagent-background.d.ts +5 -1
- package/dist/agent/subagent-background.js +13 -1
- package/dist/agent/subagent-fork.d.ts +10 -5
- package/dist/agent/subagent-fork.js +22 -6
- package/dist/agent/subagent-registry.d.ts +1 -1
- package/dist/agent/subagent-registry.js +1 -1
- package/dist/agents/catalog.d.ts +1 -0
- package/dist/ai/apis/chatgpt-backend.d.ts +3 -1
- package/dist/ai/apis/chatgpt-backend.js +10 -1
- package/dist/ai/apis/max-tokens.d.ts +4 -0
- package/dist/ai/apis/max-tokens.js +12 -1
- package/dist/ai/providers/enrich.d.ts +6 -2
- package/dist/ai/providers/enrich.js +13 -2
- package/dist/ai/providers/max-tokens-cache.d.ts +22 -0
- package/dist/ai/providers/max-tokens-cache.js +95 -0
- package/dist/ai/providers/registry.js +3 -3
- package/dist/bundle/ama.cjs +1250 -375
- package/dist/checkpoints/shadow-git.d.ts +5 -1
- package/dist/checkpoints/shadow-git.js +11 -2
- package/dist/cli/compose-providers.d.ts +1 -0
- package/dist/cli/compose-providers.js +3 -0
- package/dist/cli/compose-store.d.ts +6 -2
- package/dist/cli/compose-store.js +8 -4
- package/dist/cli/compose.js +2 -2
- package/dist/config/json-schema.js +1 -0
- package/dist/config/key-docs.js +1 -0
- package/dist/config/schema-w5.js +2 -0
- package/dist/config/settings-registry.js +1 -0
- package/dist/config/types-w5.d.ts +5 -0
- package/dist/drivers/acp/driver.d.ts +22 -1
- package/dist/drivers/acp/driver.js +59 -5
- package/dist/drivers/agents.js +3 -0
- package/dist/drivers/catalog.d.ts +2 -0
- package/dist/drivers/catalog.js +94 -4
- package/dist/drivers/native/claude-normalize.d.ts +8 -0
- package/dist/drivers/native/claude-normalize.js +27 -0
- package/dist/drivers/native/claude-stream.js +16 -1
- package/dist/drivers/native/codex-app-server.js +4 -2
- package/dist/drivers/native/oneshot.js +12 -2
- package/dist/drivers/native/pi-gate.d.ts +19 -0
- package/dist/drivers/native/pi-gate.js +39 -0
- package/dist/drivers/native/pi-rpc.d.ts +29 -0
- package/dist/drivers/native/pi-rpc.js +483 -0
- package/dist/drivers/runner.js +1 -0
- package/dist/drivers/types.d.ts +1 -1
- package/dist/i18n/catalog.d.ts +5 -0
- package/dist/i18n/messages/auth.d.ts +2 -0
- package/dist/i18n/messages/auth.js +2 -0
- package/dist/i18n/messages/config-keys.d.ts +2 -0
- package/dist/i18n/messages/config-keys.js +2 -0
- package/dist/i18n/messages/config.d.ts +2 -0
- package/dist/i18n/messages/drivers.d.ts +4 -0
- package/dist/i18n/messages/drivers.js +4 -0
- package/dist/i18n/messages/interactive.d.ts +2 -0
- package/dist/i18n/messages/interactive.js +2 -0
- package/dist/i18n/messages/session.js +2 -2
- package/dist/modes/interactive/agent-ui.js +7 -0
- package/dist/session/manager.d.ts +3 -1
- package/dist/session/manager.js +5 -0
- package/dist/session/offload.d.ts +10 -2
- package/dist/session/offload.js +27 -4
- package/dist/tools/background-jobs.d.ts +4 -4
- package/dist/tools/background-jobs.js +4 -4
- package/dist/tools/bash.d.ts +2 -1
- package/dist/tools/bash.js +4 -3
- package/dist/tools/task-ctl.js +2 -1
- package/dist/tools/task.d.ts +9 -0
- package/dist/tools/task.js +27 -1
- package/docs/agents.md +70 -14
- package/docs/en/permissions.md +1 -1
- package/docs/en/providers.md +11 -10
- package/docs/en/sessions.md +2 -1
- package/docs/permissions.md +1 -1
- package/docs/providers.md +16 -11
- package/docs/session-format.md +2 -1
- package/docs/sessions.md +2 -1
- package/package.json +1 -1
|
@@ -231,11 +231,13 @@ class CodexSession {
|
|
|
231
231
|
const start = this.tokensAtStart;
|
|
232
232
|
const end = this.tokensTotal;
|
|
233
233
|
if (end !== undefined) {
|
|
234
|
+
// Codex 的 inputTokens 含缓存命中部分;ama 的 input 不含(与 cacheRead 相加才是总输入)
|
|
235
|
+
const cacheRead = end.cachedInputTokens - (start?.cachedInputTokens ?? 0);
|
|
234
236
|
turn.push({
|
|
235
237
|
type: "usage",
|
|
236
|
-
input: end.inputTokens - (start?.inputTokens ?? 0),
|
|
238
|
+
input: end.inputTokens - (start?.inputTokens ?? 0) - cacheRead,
|
|
237
239
|
output: end.outputTokens - (start?.outputTokens ?? 0),
|
|
238
|
-
cacheRead
|
|
240
|
+
cacheRead,
|
|
239
241
|
...(this.contextTokens !== undefined ? { contextTokens: this.contextTokens } : {}),
|
|
240
242
|
...(this.contextWindow !== undefined ? { contextWindow: this.contextWindow } : {}),
|
|
241
243
|
});
|
|
@@ -36,7 +36,15 @@ export function oneshotArgs(dialect, options, session, prompt) {
|
|
|
36
36
|
return args;
|
|
37
37
|
}
|
|
38
38
|
case "codex": {
|
|
39
|
-
|
|
39
|
+
// ama 只在自己已信任的目录里起外部 Agent:跳过 codex 自己的 git 仓库检查(非 git 目录否则直接失败)
|
|
40
|
+
const common = [
|
|
41
|
+
"--json",
|
|
42
|
+
"--skip-git-repo-check",
|
|
43
|
+
"-c",
|
|
44
|
+
'sandbox_mode="read-only"',
|
|
45
|
+
"-c",
|
|
46
|
+
'approval_policy="never"',
|
|
47
|
+
];
|
|
40
48
|
if (options.model !== undefined)
|
|
41
49
|
common.push("-m", options.model);
|
|
42
50
|
return session.resume
|
|
@@ -243,9 +251,11 @@ class OneshotSession {
|
|
|
243
251
|
}
|
|
244
252
|
case "turn.completed": {
|
|
245
253
|
const u = (msg["usage"] ?? {});
|
|
254
|
+
// input_tokens 含缓存命中部分;ama 的 input 不含
|
|
255
|
+
const input = u["input_tokens"];
|
|
246
256
|
turn.push({
|
|
247
257
|
type: "usage",
|
|
248
|
-
...(
|
|
258
|
+
...(input !== undefined ? { input: input - (u["cached_input_tokens"] ?? 0) } : {}),
|
|
249
259
|
...(u["output_tokens"] !== undefined ? { output: u["output_tokens"] } : {}),
|
|
250
260
|
...(u["cached_input_tokens"] !== undefined
|
|
251
261
|
? { cacheRead: u["cached_input_tokens"] }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi 的审批闸(#198):pi 自己没有审批通道,ama 以 `-e <文件>` 给这一次运行加载一个最小扩展,
|
|
3
|
+
* 把只读内置工具以外的每次工具调用经 RPC 的扩展对话框(`extension_ui_request{method: confirm}`)
|
|
4
|
+
* 交给 ama——ama 再按模式决定直接拒绝、直接放行(auto-edit 下的文件编辑)或交给人。
|
|
5
|
+
*
|
|
6
|
+
* 扩展文件写在每个会话自己的临时目录(`mkdtemp`,只有当前用户可写),会话关闭时删除;
|
|
7
|
+
* 不写 pi 的配置目录,不改用户的扩展设置。
|
|
8
|
+
*/
|
|
9
|
+
/** 对话框标题:ama 只认这个标题的 confirm,其它扩展的对话框一律取消(不代答)。 */
|
|
10
|
+
export declare const PI_GATE_TITLE = "ama-approval";
|
|
11
|
+
/** pi 不经审批就放行的内置只读工具(也用于只读模式的 `--tools`)。 */
|
|
12
|
+
export declare const PI_READ_ONLY_TOOLS: readonly string[];
|
|
13
|
+
/** 扩展源码(pi 用 jiti 加载 TypeScript;这里只用纯 JS 语法)。 */
|
|
14
|
+
export declare const PI_GATE_SOURCE: string;
|
|
15
|
+
/** 写一份闸扩展,返回路径与清理函数。 */
|
|
16
|
+
export declare function writePiGate(): {
|
|
17
|
+
path: string;
|
|
18
|
+
dispose(): void;
|
|
19
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi 的审批闸(#198):pi 自己没有审批通道,ama 以 `-e <文件>` 给这一次运行加载一个最小扩展,
|
|
3
|
+
* 把只读内置工具以外的每次工具调用经 RPC 的扩展对话框(`extension_ui_request{method: confirm}`)
|
|
4
|
+
* 交给 ama——ama 再按模式决定直接拒绝、直接放行(auto-edit 下的文件编辑)或交给人。
|
|
5
|
+
*
|
|
6
|
+
* 扩展文件写在每个会话自己的临时目录(`mkdtemp`,只有当前用户可写),会话关闭时删除;
|
|
7
|
+
* 不写 pi 的配置目录,不改用户的扩展设置。
|
|
8
|
+
*/
|
|
9
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
/** 对话框标题:ama 只认这个标题的 confirm,其它扩展的对话框一律取消(不代答)。 */
|
|
13
|
+
export const PI_GATE_TITLE = "ama-approval";
|
|
14
|
+
/** pi 不经审批就放行的内置只读工具(也用于只读模式的 `--tools`)。 */
|
|
15
|
+
export const PI_READ_ONLY_TOOLS = ["read", "grep", "find", "ls"];
|
|
16
|
+
/** 扩展源码(pi 用 jiti 加载 TypeScript;这里只用纯 JS 语法)。 */
|
|
17
|
+
export const PI_GATE_SOURCE = `// Generated by ama for one pi run: routes non-read-only tool calls to ama for approval.
|
|
18
|
+
const READ_ONLY = new Set(${JSON.stringify(PI_READ_ONLY_TOOLS)});
|
|
19
|
+
export default function (pi) {
|
|
20
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
21
|
+
if (READ_ONLY.has(event.toolName)) return undefined;
|
|
22
|
+
const ok = await ctx.ui.confirm(
|
|
23
|
+
${JSON.stringify(PI_GATE_TITLE)},
|
|
24
|
+
JSON.stringify({ id: event.toolCallId, tool: event.toolName, input: event.input }),
|
|
25
|
+
);
|
|
26
|
+
return ok ? undefined : { block: true, reason: event.toolName + " was not approved by the user" };
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
30
|
+
/** 写一份闸扩展,返回路径与清理函数。 */
|
|
31
|
+
export function writePiGate() {
|
|
32
|
+
const dir = mkdtempSync(join(tmpdir(), "ama-pi-"));
|
|
33
|
+
const path = join(dir, "ama-approval-gate.ts");
|
|
34
|
+
writeFileSync(path, PI_GATE_SOURCE, { mode: 0o600 });
|
|
35
|
+
return {
|
|
36
|
+
path,
|
|
37
|
+
dispose: () => rmSync(dir, { recursive: true, force: true }),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi 原生驱动:`pi --mode rpc` 长驻进程(stdin 命令 / stdout 响应与事件,严格按 LF 分帧)。[#198]
|
|
3
|
+
*
|
|
4
|
+
* `pi --mode rpc --session-id <uuid> [--model <m>] -e <审批闸> [--tools read,grep,find,ls]`
|
|
5
|
+
* (新会话与续聊都用 `--session-id`:pi 找到同 id 的项目会话就续上,否则新建)。
|
|
6
|
+
*
|
|
7
|
+
* - 回合:`prompt` → 响应 `disposition`(`handled` 时没有运行,直接结束)→ 事件流 → `agent_settled`
|
|
8
|
+
* 结束(`aborted` 为真即中断);运行中追加用 `steer`;中断 `abort`(15 s 看门狗)。
|
|
9
|
+
* - 审批:pi 自己不问人,ama 以 `-e` 加载审批闸({@link PI_GATE_SOURCE}),只读内置工具以外的调用
|
|
10
|
+
* 经 `extension_ui_request{confirm, title: "ama-approval"}` 到这里:plan / allowlist 直接拒绝(并以
|
|
11
|
+
* `--tools` 只开只读工具),auto-edit / auto / full-auto 放行 `edit` / `write`,其余交给人(只交给人,
|
|
12
|
+
* 不代答)。其它扩展的对话框(select / input / editor / 别的 confirm)一律取消并提示。
|
|
13
|
+
* - 用量:assistant `message_end` 的 usage 累加(input 不含缓存命中,cost.total 是 pi 按价目表的估算);
|
|
14
|
+
* 回合结束后 `get_session_stats` 的 `contextUsage` 记上下文占用与窗口。
|
|
15
|
+
*/
|
|
16
|
+
import { type DriverDeps } from "../base.js";
|
|
17
|
+
import type { CatalogCandidate } from "../catalog.js";
|
|
18
|
+
import type { AcpToolKind, AgentDriver, DriverOpenOptions, DriverProbe, DriverSession } from "../types.js";
|
|
19
|
+
export declare function piArgs(options: Pick<DriverOpenOptions, "mode" | "model">, sessionId: string, gatePath: string): string[];
|
|
20
|
+
export declare function piToolKind(tool: string): AcpToolKind;
|
|
21
|
+
export declare class PiRpcDriver implements AgentDriver {
|
|
22
|
+
readonly agentId: string;
|
|
23
|
+
private readonly candidate;
|
|
24
|
+
private readonly deps;
|
|
25
|
+
readonly kind: "pi-rpc";
|
|
26
|
+
constructor(agentId: string, candidate: CatalogCandidate, deps?: DriverDeps);
|
|
27
|
+
probe(): Promise<DriverProbe>;
|
|
28
|
+
open(options: DriverOpenOptions): Promise<DriverSession>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi 原生驱动:`pi --mode rpc` 长驻进程(stdin 命令 / stdout 响应与事件,严格按 LF 分帧)。[#198]
|
|
3
|
+
*
|
|
4
|
+
* `pi --mode rpc --session-id <uuid> [--model <m>] -e <审批闸> [--tools read,grep,find,ls]`
|
|
5
|
+
* (新会话与续聊都用 `--session-id`:pi 找到同 id 的项目会话就续上,否则新建)。
|
|
6
|
+
*
|
|
7
|
+
* - 回合:`prompt` → 响应 `disposition`(`handled` 时没有运行,直接结束)→ 事件流 → `agent_settled`
|
|
8
|
+
* 结束(`aborted` 为真即中断);运行中追加用 `steer`;中断 `abort`(15 s 看门狗)。
|
|
9
|
+
* - 审批:pi 自己不问人,ama 以 `-e` 加载审批闸({@link PI_GATE_SOURCE}),只读内置工具以外的调用
|
|
10
|
+
* 经 `extension_ui_request{confirm, title: "ama-approval"}` 到这里:plan / allowlist 直接拒绝(并以
|
|
11
|
+
* `--tools` 只开只读工具),auto-edit / auto / full-auto 放行 `edit` / `write`,其余交给人(只交给人,
|
|
12
|
+
* 不代答)。其它扩展的对话框(select / input / editor / 别的 confirm)一律取消并提示。
|
|
13
|
+
* - 用量:assistant `message_end` 的 usage 累加(input 不含缓存命中,cost.total 是 pi 按价目表的估算);
|
|
14
|
+
* 回合结束后 `get_session_stats` 的 `contextUsage` 记上下文占用与窗口。
|
|
15
|
+
*/
|
|
16
|
+
import { randomUUID } from "node:crypto";
|
|
17
|
+
import { resolve as resolvePath } from "node:path";
|
|
18
|
+
import { AmaError } from "../../errors.js";
|
|
19
|
+
import { createLineReader } from "../../modes/rpc/jsonl.js";
|
|
20
|
+
import { armCancelWatchdog, isReadOnlyMode, probeCandidate, spawnOf, } from "../base.js";
|
|
21
|
+
import { stderrHint } from "../process.js";
|
|
22
|
+
import { TurnCollector, oneLine } from "../turn.js";
|
|
23
|
+
import { PI_GATE_TITLE, PI_READ_ONLY_TOOLS, writePiGate } from "./pi-gate.js";
|
|
24
|
+
import { msg } from "../../i18n/index.js";
|
|
25
|
+
const DIALOG_METHODS = new Set(["select", "confirm", "input", "editor"]);
|
|
26
|
+
/** auto-edit 及更宽的模式下不问人的文件编辑工具。 */
|
|
27
|
+
const EDIT_TOOLS = new Set(["edit", "write"]);
|
|
28
|
+
export function piArgs(options, sessionId, gatePath) {
|
|
29
|
+
const args = ["--session-id", sessionId];
|
|
30
|
+
if (options.model !== undefined)
|
|
31
|
+
args.push("--model", options.model);
|
|
32
|
+
args.push("-e", gatePath);
|
|
33
|
+
if (isReadOnlyMode(options.mode))
|
|
34
|
+
args.push("--tools", PI_READ_ONLY_TOOLS.join(","));
|
|
35
|
+
return args;
|
|
36
|
+
}
|
|
37
|
+
export function piToolKind(tool) {
|
|
38
|
+
switch (tool) {
|
|
39
|
+
case "bash":
|
|
40
|
+
case "powershell":
|
|
41
|
+
return "execute";
|
|
42
|
+
case "edit":
|
|
43
|
+
case "write":
|
|
44
|
+
return "edit";
|
|
45
|
+
case "read":
|
|
46
|
+
return "read";
|
|
47
|
+
case "grep":
|
|
48
|
+
case "find":
|
|
49
|
+
case "ls":
|
|
50
|
+
return "search";
|
|
51
|
+
default:
|
|
52
|
+
return "other";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function piToolTitle(tool, input) {
|
|
56
|
+
if (typeof input["command"] === "string")
|
|
57
|
+
return `$ ${oneLine(input["command"], 100)}`;
|
|
58
|
+
if (typeof input["path"] === "string")
|
|
59
|
+
return `${tool} ${input["path"]}`;
|
|
60
|
+
if (typeof input["pattern"] === "string")
|
|
61
|
+
return `${tool} ${oneLine(input["pattern"], 80)}`;
|
|
62
|
+
return tool;
|
|
63
|
+
}
|
|
64
|
+
function textOf(message) {
|
|
65
|
+
const content = message["content"];
|
|
66
|
+
if (typeof content === "string")
|
|
67
|
+
return content;
|
|
68
|
+
if (!Array.isArray(content))
|
|
69
|
+
return "";
|
|
70
|
+
return content
|
|
71
|
+
.map((b) => (b["type"] === "text" && typeof b["text"] === "string" ? b["text"] : ""))
|
|
72
|
+
.join("");
|
|
73
|
+
}
|
|
74
|
+
export class PiRpcDriver {
|
|
75
|
+
agentId;
|
|
76
|
+
candidate;
|
|
77
|
+
deps;
|
|
78
|
+
kind = "pi-rpc";
|
|
79
|
+
constructor(agentId, candidate, deps = {}) {
|
|
80
|
+
this.agentId = agentId;
|
|
81
|
+
this.candidate = candidate;
|
|
82
|
+
this.deps = deps;
|
|
83
|
+
}
|
|
84
|
+
probe() {
|
|
85
|
+
return probeCandidate(this.candidate, this.deps);
|
|
86
|
+
}
|
|
87
|
+
async open(options) {
|
|
88
|
+
const probed = await probeCandidate(this.candidate, this.deps);
|
|
89
|
+
const sessionId = options.resume ?? randomUUID();
|
|
90
|
+
const gate = writePiGate();
|
|
91
|
+
const transport = spawnOf(this.deps)({
|
|
92
|
+
program: probed.path ?? this.candidate.program,
|
|
93
|
+
args: [...this.candidate.args, ...piArgs(options, sessionId, gate.path)],
|
|
94
|
+
cwd: options.cwd,
|
|
95
|
+
env: options.env,
|
|
96
|
+
});
|
|
97
|
+
const session = new PiSession(this.agentId, sessionId, transport, options, this.deps, () => gate.dispose());
|
|
98
|
+
if (probed.warning !== undefined)
|
|
99
|
+
session.notices.push(probed.warning);
|
|
100
|
+
try {
|
|
101
|
+
await session.start(options.signal);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
await session.close();
|
|
105
|
+
throw new AmaError("agent_start_failed", `${this.agentId}: failed to start (${error.message})${stderrHint(transport) !== "" ? `: ${stderrHint(transport)}` : ""}`, { cause: error });
|
|
106
|
+
}
|
|
107
|
+
return session;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
class PiSession {
|
|
111
|
+
agentId;
|
|
112
|
+
id;
|
|
113
|
+
transport;
|
|
114
|
+
options;
|
|
115
|
+
deps;
|
|
116
|
+
dispose;
|
|
117
|
+
notices = [];
|
|
118
|
+
nextId = 1;
|
|
119
|
+
pending = new Map();
|
|
120
|
+
permissions = new Set();
|
|
121
|
+
turn;
|
|
122
|
+
hooks;
|
|
123
|
+
settle;
|
|
124
|
+
running;
|
|
125
|
+
usage = { input: 0, output: 0, cacheRead: 0, costUsd: 0, seen: false };
|
|
126
|
+
lastAssistant;
|
|
127
|
+
interrupted = false;
|
|
128
|
+
closed = false;
|
|
129
|
+
exited = false;
|
|
130
|
+
exitWaiters = new Set();
|
|
131
|
+
constructor(agentId, id, transport, options, deps, dispose) {
|
|
132
|
+
this.agentId = agentId;
|
|
133
|
+
this.id = id;
|
|
134
|
+
this.transport = transport;
|
|
135
|
+
this.options = options;
|
|
136
|
+
this.deps = deps;
|
|
137
|
+
this.dispose = dispose;
|
|
138
|
+
createLineReader(transport.stdout, (line) => this.onLine(line), () => this.onExit());
|
|
139
|
+
transport.stdin.on?.("error", () => undefined);
|
|
140
|
+
}
|
|
141
|
+
get sessionId() {
|
|
142
|
+
return this.id;
|
|
143
|
+
}
|
|
144
|
+
/** 握手:`get_state` 确认进程起来了,并取 pi 实际使用的会话 id。 */
|
|
145
|
+
async start(signal) {
|
|
146
|
+
const aborted = new Promise((_, reject) => {
|
|
147
|
+
if (signal.aborted)
|
|
148
|
+
reject(new Error("aborted"));
|
|
149
|
+
signal.addEventListener("abort", () => reject(new Error("aborted")), { once: true });
|
|
150
|
+
});
|
|
151
|
+
void aborted.catch(() => undefined);
|
|
152
|
+
const state = await Promise.race([this.request({ type: "get_state" }), aborted]);
|
|
153
|
+
const data = (state["data"] ?? {});
|
|
154
|
+
if (typeof data["sessionId"] === "string" && data["sessionId"] !== "")
|
|
155
|
+
this.id = data["sessionId"];
|
|
156
|
+
}
|
|
157
|
+
async prompt(content, hooks) {
|
|
158
|
+
if (this.closed || this.exited)
|
|
159
|
+
throw new AmaError("agent_closed", `${this.agentId} session is closed`);
|
|
160
|
+
if (this.running !== undefined)
|
|
161
|
+
throw new AmaError("busy", `${this.agentId} is already running`);
|
|
162
|
+
const turn = new TurnCollector((e) => hooks.onEvent(e));
|
|
163
|
+
for (const text of this.notices.splice(0))
|
|
164
|
+
turn.push({ type: "notice", level: "warn", text });
|
|
165
|
+
this.turn = turn;
|
|
166
|
+
this.hooks = hooks;
|
|
167
|
+
this.interrupted = false;
|
|
168
|
+
this.usage = { input: 0, output: 0, cacheRead: 0, costUsd: 0, seen: false };
|
|
169
|
+
this.lastAssistant = undefined;
|
|
170
|
+
this.running = this.run(content, turn);
|
|
171
|
+
try {
|
|
172
|
+
return await this.running;
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
this.running = undefined;
|
|
176
|
+
this.turn = undefined;
|
|
177
|
+
this.hooks = undefined;
|
|
178
|
+
this.settle = undefined;
|
|
179
|
+
this.exitWaiters.clear();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async run(content, turn) {
|
|
183
|
+
// 先让出一拍:`this.running` 赋值之后再写命令(同步回显的对端里事件可能立刻到达并触发 cancel)
|
|
184
|
+
await Promise.resolve();
|
|
185
|
+
const settled = new Promise((resolve) => (this.settle = resolve));
|
|
186
|
+
const response = await this.request({ type: "prompt", ...this.message(content) });
|
|
187
|
+
if (response["success"] !== true)
|
|
188
|
+
throw new AmaError("agent_failed", `${this.agentId} rejected the prompt: ${oneLine(String(response["error"] ?? "unknown"), 300)}`);
|
|
189
|
+
const disposition = (response["data"] ?? {})["disposition"];
|
|
190
|
+
if (disposition === "handled")
|
|
191
|
+
return turn.result("end_turn");
|
|
192
|
+
const aborted = await Promise.race([settled, this.exitPromise()]);
|
|
193
|
+
await this.reportContext(turn);
|
|
194
|
+
return this.complete(turn, aborted);
|
|
195
|
+
}
|
|
196
|
+
async steer(content) {
|
|
197
|
+
if (this.closed || this.exited)
|
|
198
|
+
throw new AmaError("agent_closed", `${this.agentId} session is closed`);
|
|
199
|
+
await this.request({ type: "steer", ...this.message(content) });
|
|
200
|
+
}
|
|
201
|
+
async cancel() {
|
|
202
|
+
const running = this.running;
|
|
203
|
+
if (running === undefined || this.closed)
|
|
204
|
+
return;
|
|
205
|
+
this.interrupted = true;
|
|
206
|
+
for (const c of this.permissions)
|
|
207
|
+
c.abort();
|
|
208
|
+
void this.request({ type: "abort" }).catch(() => undefined);
|
|
209
|
+
armCancelWatchdog(this.transport, running.catch(() => undefined), this.deps);
|
|
210
|
+
}
|
|
211
|
+
async close() {
|
|
212
|
+
if (this.closed)
|
|
213
|
+
return;
|
|
214
|
+
this.closed = true;
|
|
215
|
+
for (const c of this.permissions)
|
|
216
|
+
c.abort();
|
|
217
|
+
if (this.running !== undefined) {
|
|
218
|
+
this.interrupted = true;
|
|
219
|
+
this.write({ type: "abort" });
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
await this.transport.terminate();
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
this.dispose();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
message(content) {
|
|
229
|
+
const text = content.map((b) => (b.type === "text" ? b.text : "")).join("\n");
|
|
230
|
+
const images = content.flatMap((b) => b.type === "image" ? [{ type: "image", data: b.data, mimeType: b.mimeType }] : []);
|
|
231
|
+
return { message: text, ...(images.length > 0 ? { images } : {}) };
|
|
232
|
+
}
|
|
233
|
+
complete(turn, aborted) {
|
|
234
|
+
const u = this.usage;
|
|
235
|
+
if (u.seen)
|
|
236
|
+
turn.push({
|
|
237
|
+
type: "usage",
|
|
238
|
+
input: u.input,
|
|
239
|
+
output: u.output,
|
|
240
|
+
cacheRead: u.cacheRead,
|
|
241
|
+
costUsd: u.costUsd,
|
|
242
|
+
});
|
|
243
|
+
const last = this.lastAssistant;
|
|
244
|
+
if (last !== undefined) {
|
|
245
|
+
const text = textOf(last);
|
|
246
|
+
if (text !== "")
|
|
247
|
+
turn.setFinalText(text);
|
|
248
|
+
}
|
|
249
|
+
if (aborted || this.interrupted)
|
|
250
|
+
return turn.result("cancelled");
|
|
251
|
+
switch (last?.["stopReason"]) {
|
|
252
|
+
case "error":
|
|
253
|
+
throw new AmaError("agent_failed", `${this.agentId} failed: ${oneLine(String(last["errorMessage"] ?? "unknown error"), 300)}`);
|
|
254
|
+
case "aborted":
|
|
255
|
+
return turn.result("cancelled");
|
|
256
|
+
case "length":
|
|
257
|
+
return turn.result("max_tokens");
|
|
258
|
+
default:
|
|
259
|
+
return turn.result("end_turn");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/** `get_session_stats.contextUsage` → 上下文占用与窗口(拿不到就算了)。 */
|
|
263
|
+
async reportContext(turn) {
|
|
264
|
+
if (this.exited)
|
|
265
|
+
return;
|
|
266
|
+
try {
|
|
267
|
+
const stats = await Promise.race([
|
|
268
|
+
this.request({ type: "get_session_stats" }),
|
|
269
|
+
new Promise((resolve) => setTimeout(() => resolve({}), 5_000).unref()),
|
|
270
|
+
]);
|
|
271
|
+
const usage = ((stats["data"] ?? {})["contextUsage"] ?? {});
|
|
272
|
+
const tokens = usage["tokens"];
|
|
273
|
+
const window = usage["contextWindow"];
|
|
274
|
+
if (typeof tokens === "number" || typeof window === "number")
|
|
275
|
+
turn.push({
|
|
276
|
+
type: "usage",
|
|
277
|
+
...(typeof tokens === "number" ? { contextTokens: tokens } : {}),
|
|
278
|
+
...(typeof window === "number" ? { contextWindow: window } : {}),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
// 统计拿不到不影响回合
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
exitPromise() {
|
|
286
|
+
return new Promise((_, reject) => {
|
|
287
|
+
if (this.exited)
|
|
288
|
+
reject(this.exitError());
|
|
289
|
+
else
|
|
290
|
+
this.exitWaiters.add(reject);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
exitError() {
|
|
294
|
+
const hint = stderrHint(this.transport);
|
|
295
|
+
return new AmaError("agent_exited", `${this.agentId} process exited${hint !== "" ? `: ${hint}` : ""}`);
|
|
296
|
+
}
|
|
297
|
+
request(command) {
|
|
298
|
+
if (this.exited)
|
|
299
|
+
return Promise.reject(this.exitError());
|
|
300
|
+
const id = `ama-${this.nextId++}`;
|
|
301
|
+
return new Promise((resolve, reject) => {
|
|
302
|
+
this.pending.set(id, { resolve, reject });
|
|
303
|
+
this.write({ id, ...command });
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
write(record) {
|
|
307
|
+
if (this.exited)
|
|
308
|
+
return;
|
|
309
|
+
this.transport.stdin.write(`${JSON.stringify(record)}\n`);
|
|
310
|
+
}
|
|
311
|
+
onExit() {
|
|
312
|
+
this.exited = true;
|
|
313
|
+
const error = this.exitError();
|
|
314
|
+
for (const p of this.pending.values())
|
|
315
|
+
p.reject(error);
|
|
316
|
+
this.pending.clear();
|
|
317
|
+
for (const reject of this.exitWaiters)
|
|
318
|
+
reject(error);
|
|
319
|
+
this.exitWaiters.clear();
|
|
320
|
+
for (const c of this.permissions)
|
|
321
|
+
c.abort();
|
|
322
|
+
}
|
|
323
|
+
onLine(line) {
|
|
324
|
+
let record;
|
|
325
|
+
try {
|
|
326
|
+
record = JSON.parse(line);
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
this.deps.log?.("debug", msg().drivers.agent.nonJson(this.agentId));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
switch (record["type"]) {
|
|
333
|
+
case "response": {
|
|
334
|
+
const id = record["id"];
|
|
335
|
+
const pending = typeof id === "string" ? this.pending.get(id) : undefined;
|
|
336
|
+
if (pending === undefined || typeof id !== "string")
|
|
337
|
+
return;
|
|
338
|
+
this.pending.delete(id);
|
|
339
|
+
pending.resolve(record);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
case "agent_settled":
|
|
343
|
+
this.settle?.(record["aborted"] === true);
|
|
344
|
+
return;
|
|
345
|
+
case "extension_ui_request":
|
|
346
|
+
void this.onDialog(record);
|
|
347
|
+
return;
|
|
348
|
+
default:
|
|
349
|
+
this.onEvent(record);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
onEvent(record) {
|
|
353
|
+
const turn = this.turn;
|
|
354
|
+
if (turn === undefined)
|
|
355
|
+
return;
|
|
356
|
+
switch (record["type"]) {
|
|
357
|
+
case "message_update": {
|
|
358
|
+
const event = (record["assistantMessageEvent"] ?? {});
|
|
359
|
+
const delta = event["delta"];
|
|
360
|
+
if (typeof delta !== "string")
|
|
361
|
+
return;
|
|
362
|
+
if (event["type"] === "text_delta")
|
|
363
|
+
turn.push({ type: "message_delta", text: delta });
|
|
364
|
+
else if (event["type"] === "thinking_delta")
|
|
365
|
+
turn.push({ type: "thought_delta", text: delta });
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
case "message_end": {
|
|
369
|
+
const message = (record["message"] ?? {});
|
|
370
|
+
if (message["role"] !== "assistant")
|
|
371
|
+
return;
|
|
372
|
+
this.lastAssistant = message;
|
|
373
|
+
const usage = (message["usage"] ?? {});
|
|
374
|
+
const num = (key) => typeof usage[key] === "number" ? usage[key] : 0;
|
|
375
|
+
const cost = (usage["cost"] ?? {})["total"];
|
|
376
|
+
this.usage = {
|
|
377
|
+
input: this.usage.input + num("input"),
|
|
378
|
+
output: this.usage.output + num("output"),
|
|
379
|
+
cacheRead: this.usage.cacheRead + num("cacheRead"),
|
|
380
|
+
costUsd: this.usage.costUsd + (typeof cost === "number" ? cost : 0),
|
|
381
|
+
seen: true,
|
|
382
|
+
};
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
case "tool_execution_start":
|
|
386
|
+
case "tool_execution_end": {
|
|
387
|
+
const tool = String(record["toolName"] ?? "tool");
|
|
388
|
+
const known = turn.tool(String(record["toolCallId"] ?? ""));
|
|
389
|
+
const input = (record["args"] ?? {});
|
|
390
|
+
const path = typeof input["path"] === "string" ? input["path"] : undefined;
|
|
391
|
+
const locations = path !== undefined ? [resolvePath(this.options.cwd, path)] : known?.locations;
|
|
392
|
+
turn.push({
|
|
393
|
+
type: "tool_call",
|
|
394
|
+
id: String(record["toolCallId"] ?? ""),
|
|
395
|
+
title: known?.title ?? piToolTitle(tool, input),
|
|
396
|
+
kind: piToolKind(tool),
|
|
397
|
+
status: record["type"] === "tool_execution_start"
|
|
398
|
+
? "in_progress"
|
|
399
|
+
: record["isError"] === true
|
|
400
|
+
? "failed"
|
|
401
|
+
: "completed",
|
|
402
|
+
...(locations !== undefined && locations.length > 0 ? { locations } : {}),
|
|
403
|
+
});
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
case "auto_retry_start":
|
|
407
|
+
turn.push({
|
|
408
|
+
type: "notice",
|
|
409
|
+
level: "warn",
|
|
410
|
+
text: msg().drivers.agent.error(this.agentId, oneLine(String(record["errorMessage"] ?? "error"), 200), true),
|
|
411
|
+
});
|
|
412
|
+
return;
|
|
413
|
+
default:
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/** 扩展对话框:ama 的审批闸按模式决定或交给人;其它扩展的对话框取消(不代答)。 */
|
|
418
|
+
async onDialog(record) {
|
|
419
|
+
const id = String(record["id"] ?? "");
|
|
420
|
+
const method = String(record["method"] ?? "");
|
|
421
|
+
if (!DIALOG_METHODS.has(method)) {
|
|
422
|
+
if (record["method"] === "notify" && record["notifyType"] !== "info")
|
|
423
|
+
this.turn?.push({
|
|
424
|
+
type: "notice",
|
|
425
|
+
level: "warn",
|
|
426
|
+
text: `${this.agentId}: ${oneLine(String(record["message"] ?? ""), 200)}`,
|
|
427
|
+
});
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (method !== "confirm" || record["title"] !== PI_GATE_TITLE) {
|
|
431
|
+
this.turn?.push({
|
|
432
|
+
type: "notice",
|
|
433
|
+
level: "warn",
|
|
434
|
+
text: msg().drivers.agent.extensionDialog(this.agentId, oneLine(String(record["title"] ?? method), 80)),
|
|
435
|
+
});
|
|
436
|
+
this.write({ type: "extension_ui_response", id, cancelled: true });
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const confirmed = await this.decide(record);
|
|
440
|
+
this.write({ type: "extension_ui_response", id, confirmed });
|
|
441
|
+
}
|
|
442
|
+
async decide(record) {
|
|
443
|
+
let call;
|
|
444
|
+
try {
|
|
445
|
+
call = JSON.parse(String(record["message"] ?? "{}"));
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
const tool = String(call["tool"] ?? "tool");
|
|
451
|
+
const input = (call["input"] ?? {});
|
|
452
|
+
const mode = this.options.mode;
|
|
453
|
+
if (isReadOnlyMode(mode))
|
|
454
|
+
return false;
|
|
455
|
+
if (mode !== "default" && EDIT_TOOLS.has(tool))
|
|
456
|
+
return true;
|
|
457
|
+
const hooks = this.hooks;
|
|
458
|
+
if (hooks === undefined || this.interrupted || this.closed)
|
|
459
|
+
return false;
|
|
460
|
+
const known = this.turn?.tool(String(call["id"] ?? ""));
|
|
461
|
+
const path = typeof input["path"] === "string" ? input["path"] : undefined;
|
|
462
|
+
const controller = new AbortController();
|
|
463
|
+
this.permissions.add(controller);
|
|
464
|
+
try {
|
|
465
|
+
const outcome = await hooks.onPermission({
|
|
466
|
+
toolCall: {
|
|
467
|
+
title: known?.title ?? piToolTitle(tool, input),
|
|
468
|
+
kind: piToolKind(tool),
|
|
469
|
+
...(path !== undefined ? { locations: [resolvePath(this.options.cwd, path)] } : {}),
|
|
470
|
+
inputSummary: oneLine(JSON.stringify(input), 300),
|
|
471
|
+
},
|
|
472
|
+
options: [
|
|
473
|
+
{ optionId: "allow", kind: "allow_once" },
|
|
474
|
+
{ optionId: "reject", kind: "reject_once" },
|
|
475
|
+
],
|
|
476
|
+
}, controller.signal);
|
|
477
|
+
return (!controller.signal.aborted && outcome.outcome === "selected" && outcome.optionId === "allow");
|
|
478
|
+
}
|
|
479
|
+
finally {
|
|
480
|
+
this.permissions.delete(controller);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
package/dist/drivers/runner.js
CHANGED
package/dist/drivers/types.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface DriverCapabilities {
|
|
|
20
20
|
usage: "tokens" | "usd" | "requests" | "none";
|
|
21
21
|
images: boolean;
|
|
22
22
|
}
|
|
23
|
-
export type DriverKind = "acp" | "acp-adapter" | "claude-stream" | "codex-app-server" | "oneshot" | "host";
|
|
23
|
+
export type DriverKind = "acp" | "acp-adapter" | "claude-stream" | "codex-app-server" | "pi-rpc" | "oneshot" | "host";
|
|
24
24
|
export interface DriverProbe {
|
|
25
25
|
installed: boolean;
|
|
26
26
|
version?: string;
|
package/dist/i18n/catalog.d.ts
CHANGED
|
@@ -534,6 +534,7 @@ declare const en: {
|
|
|
534
534
|
backgroundStopped: (jobId: string, command: string) => string;
|
|
535
535
|
compactionFailed: (error: string) => string;
|
|
536
536
|
compactionCancelled: string;
|
|
537
|
+
imageReadBackFailed: (id: string) => string;
|
|
537
538
|
};
|
|
538
539
|
statusLine: {
|
|
539
540
|
usage: string;
|
|
@@ -1446,6 +1447,7 @@ declare const en: {
|
|
|
1446
1447
|
"subagents.maxPending": string;
|
|
1447
1448
|
"subagents.retainSessions": string;
|
|
1448
1449
|
"subagents.defaultModel": string;
|
|
1450
|
+
"subagents.forkMaxContextRatio": string;
|
|
1449
1451
|
"subagents.background": string;
|
|
1450
1452
|
"subagents.autoBackgroundAfterMs": string;
|
|
1451
1453
|
models: string;
|
|
@@ -1606,6 +1608,7 @@ declare const en: {
|
|
|
1606
1608
|
resumeUnsupported: (agent: string) => string;
|
|
1607
1609
|
resumeNotFound: (agent: string, id: string) => string;
|
|
1608
1610
|
noMatchingMode: (agent: string, mode: string) => string;
|
|
1611
|
+
noMatchingModel: (agent: string, model: string) => string;
|
|
1609
1612
|
noImages: (agent: string) => string;
|
|
1610
1613
|
noImagesDriver: (agent: string) => string;
|
|
1611
1614
|
noImagesOneshot: (agent: string) => string;
|
|
@@ -1616,6 +1619,7 @@ declare const en: {
|
|
|
1616
1619
|
error: (agent: string, message: string, retrying: boolean) => string;
|
|
1617
1620
|
askedQuestion: (agent: string, tool: string) => string;
|
|
1618
1621
|
askedQuestionCodex: (agent: string) => string;
|
|
1622
|
+
extensionDialog: (agent: string, title: string) => string;
|
|
1619
1623
|
mcpElicitation: (agent: string) => string;
|
|
1620
1624
|
};
|
|
1621
1625
|
hooks: {
|
|
@@ -2078,6 +2082,7 @@ declare const en: {
|
|
|
2078
2082
|
};
|
|
2079
2083
|
request: {
|
|
2080
2084
|
notEligible: string;
|
|
2085
|
+
modelUnavailable: (model: string) => string;
|
|
2081
2086
|
flavorMismatch: (current: string, ref: string, pinned: string) => string;
|
|
2082
2087
|
};
|
|
2083
2088
|
errors: {
|