@armadra/agent 0.7.6 → 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 +13 -0
- package/CHANGELOG.zh-CN.md +13 -0
- package/dist/ai/apis/chatgpt-backend.d.ts +3 -1
- package/dist/ai/apis/chatgpt-backend.js +10 -1
- package/dist/bundle/ama.cjs +1018 -336
- 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 +3 -0
- package/dist/i18n/messages/auth.d.ts +2 -0
- package/dist/i18n/messages/auth.js +2 -0
- package/dist/i18n/messages/drivers.d.ts +4 -0
- package/dist/i18n/messages/drivers.js +4 -0
- package/docs/agents.md +56 -11
- package/docs/en/providers.md +7 -6
- package/docs/providers.md +7 -6
- package/package.json +1 -1
|
@@ -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
|
@@ -1608,6 +1608,7 @@ declare const en: {
|
|
|
1608
1608
|
resumeUnsupported: (agent: string) => string;
|
|
1609
1609
|
resumeNotFound: (agent: string, id: string) => string;
|
|
1610
1610
|
noMatchingMode: (agent: string, mode: string) => string;
|
|
1611
|
+
noMatchingModel: (agent: string, model: string) => string;
|
|
1611
1612
|
noImages: (agent: string) => string;
|
|
1612
1613
|
noImagesDriver: (agent: string) => string;
|
|
1613
1614
|
noImagesOneshot: (agent: string) => string;
|
|
@@ -1618,6 +1619,7 @@ declare const en: {
|
|
|
1618
1619
|
error: (agent: string, message: string, retrying: boolean) => string;
|
|
1619
1620
|
askedQuestion: (agent: string, tool: string) => string;
|
|
1620
1621
|
askedQuestionCodex: (agent: string) => string;
|
|
1622
|
+
extensionDialog: (agent: string, title: string) => string;
|
|
1621
1623
|
mcpElicitation: (agent: string) => string;
|
|
1622
1624
|
};
|
|
1623
1625
|
hooks: {
|
|
@@ -2080,6 +2082,7 @@ declare const en: {
|
|
|
2080
2082
|
};
|
|
2081
2083
|
request: {
|
|
2082
2084
|
notEligible: string;
|
|
2085
|
+
modelUnavailable: (model: string) => string;
|
|
2083
2086
|
flavorMismatch: (current: string, ref: string, pinned: string) => string;
|
|
2084
2087
|
};
|
|
2085
2088
|
errors: {
|
|
@@ -52,6 +52,7 @@ export declare const en: {
|
|
|
52
52
|
};
|
|
53
53
|
request: {
|
|
54
54
|
notEligible: string;
|
|
55
|
+
modelUnavailable: (model: string) => string;
|
|
55
56
|
flavorMismatch: (current: string, ref: string, pinned: string) => string;
|
|
56
57
|
};
|
|
57
58
|
errors: {
|
|
@@ -150,6 +151,7 @@ export declare const zh: {
|
|
|
150
151
|
};
|
|
151
152
|
request: {
|
|
152
153
|
notEligible: string;
|
|
154
|
+
modelUnavailable: (model: string) => string;
|
|
153
155
|
flavorMismatch: (current: string, ref: string, pinned: string) => string;
|
|
154
156
|
};
|
|
155
157
|
errors: {
|
|
@@ -57,6 +57,7 @@ export const en = {
|
|
|
57
57
|
" 2. Workspace account: Team / Enterprise / Edu workspaces may not offer it\n" +
|
|
58
58
|
" 3. Region restriction, or not yet rolled out during the preview (most likely when a Pro account still gets this error)\n" +
|
|
59
59
|
"You can sign in the other way instead: ama auth login chatgpt --flavor codex",
|
|
60
|
+
modelUnavailable: (model) => `${model} is not available to this ChatGPT account (400 from the codex backend). The backend's model list changes over time: run ama models discover chatgpt to refresh it, then pick a listed model with /model or --model`,
|
|
60
61
|
flavorMismatch: (current, ref, pinned) => `signed in with ${current}, but ${ref} pins the @${pinned} channel. Drop @${pinned} so the channel follows the sign-in, or run ama auth login chatgpt --flavor ${pinned}`,
|
|
61
62
|
},
|
|
62
63
|
errors: {
|
|
@@ -166,6 +167,7 @@ export const zh = {
|
|
|
166
167
|
" 2. 工作空间账户:Team / Enterprise / Edu 工作空间可能未开放\n" +
|
|
167
168
|
" 3. 地区受限,或预览期尚未开放(Pro 账户仍报此错时最可能是这一条)\n" +
|
|
168
169
|
"可以改用另一种登录方式:ama auth login chatgpt --flavor codex",
|
|
170
|
+
modelUnavailable: (model) => `${model} 在这个 ChatGPT 账户下不可用(codex 后端回 400)。后端的模型表会变化:运行 ama models discover chatgpt 刷新,再用 /model 或 --model 选表里的模型`,
|
|
169
171
|
flavorMismatch: (current, ref, pinned) => `当前以 ${current} 方式登录,但 ${ref} 显式指定了 @${pinned} 渠道。去掉 @${pinned} 让渠道跟随登录方式,或运行 ama auth login chatgpt --flavor ${pinned}`,
|
|
170
172
|
},
|
|
171
173
|
errors: {
|
|
@@ -18,6 +18,7 @@ export declare const en: {
|
|
|
18
18
|
resumeUnsupported: (agent: string) => string;
|
|
19
19
|
resumeNotFound: (agent: string, id: string) => string;
|
|
20
20
|
noMatchingMode: (agent: string, mode: string) => string;
|
|
21
|
+
noMatchingModel: (agent: string, model: string) => string;
|
|
21
22
|
noImages: (agent: string) => string;
|
|
22
23
|
noImagesDriver: (agent: string) => string;
|
|
23
24
|
noImagesOneshot: (agent: string) => string;
|
|
@@ -28,6 +29,7 @@ export declare const en: {
|
|
|
28
29
|
error: (agent: string, message: string, retrying: boolean) => string;
|
|
29
30
|
askedQuestion: (agent: string, tool: string) => string;
|
|
30
31
|
askedQuestionCodex: (agent: string) => string;
|
|
32
|
+
extensionDialog: (agent: string, title: string) => string;
|
|
31
33
|
mcpElicitation: (agent: string) => string;
|
|
32
34
|
};
|
|
33
35
|
/** Hook 配置与输出的警告(给人看;回给模型的阻止理由固定英文)。 */
|
|
@@ -76,6 +78,7 @@ export declare const zh: {
|
|
|
76
78
|
resumeUnsupported: (agent: string) => string;
|
|
77
79
|
resumeNotFound: (agent: string, id: string) => string;
|
|
78
80
|
noMatchingMode: (agent: string, mode: string) => string;
|
|
81
|
+
noMatchingModel: (agent: string, model: string) => string;
|
|
79
82
|
noImages: (agent: string) => string;
|
|
80
83
|
noImagesDriver: (agent: string) => string;
|
|
81
84
|
noImagesOneshot: (agent: string) => string;
|
|
@@ -86,6 +89,7 @@ export declare const zh: {
|
|
|
86
89
|
error: (agent: string, message: string, retrying: boolean) => string;
|
|
87
90
|
askedQuestion: (agent: string, tool: string) => string;
|
|
88
91
|
askedQuestionCodex: (agent: string) => string;
|
|
92
|
+
extensionDialog: (agent: string, title: string) => string;
|
|
89
93
|
mcpElicitation: (agent: string) => string;
|
|
90
94
|
};
|
|
91
95
|
hooks: {
|
|
@@ -19,6 +19,7 @@ export const en = {
|
|
|
19
19
|
resumeUnsupported: (agent) => `${agent} cannot resume sessions; started a new one`,
|
|
20
20
|
resumeNotFound: (agent, id) => `${agent} cannot find session ${id}; started a new one`,
|
|
21
21
|
noMatchingMode: (agent, mode) => `${agent} has no "${mode}" mode; running in its own default mode (actions that need approval still come to you)`,
|
|
22
|
+
noMatchingModel: (agent, model) => `${agent} does not offer model "${model}"; using its own default model`,
|
|
22
23
|
noImages: (agent) => `${agent} does not accept images; omitted`,
|
|
23
24
|
noImagesDriver: (agent) => `the ${agent} driver does not pass images yet; omitted`,
|
|
24
25
|
noImagesOneshot: (agent) => `${agent} one-shot mode does not pass images; omitted`,
|
|
@@ -29,6 +30,7 @@ export const en = {
|
|
|
29
30
|
error: (agent, message, retrying) => `${agent}: ${message}${retrying ? " (retrying)" : ""}`,
|
|
30
31
|
askedQuestion: (agent, tool) => `${agent} wants to ask you a question (${tool}); ama does not answer for you and asked it to put the question in its final reply`,
|
|
31
32
|
askedQuestionCodex: (agent) => `${agent} wants to ask you a question; ama does not answer for you — have it put the question in its final reply`,
|
|
33
|
+
extensionDialog: (agent, title) => `${agent}: an extension asked for input (${title}); ama does not answer for you, cancelled`,
|
|
32
34
|
mcpElicitation: (agent) => `${agent}'s MCP server asked for input; ama does not answer for you, cancelled`,
|
|
33
35
|
},
|
|
34
36
|
/** Hook 配置与输出的警告(给人看;回给模型的阻止理由固定英文)。 */
|
|
@@ -77,6 +79,7 @@ export const zh = {
|
|
|
77
79
|
resumeUnsupported: (agent) => `${agent} 不支持续接会话,已新开`,
|
|
78
80
|
resumeNotFound: (agent, id) => `${agent} 找不到会话 ${id},已新开`,
|
|
79
81
|
noMatchingMode: (agent, mode) => `${agent} 没有「${mode}」模式,按它自己的缺省模式运行(需要授权的操作仍交给你)`,
|
|
82
|
+
noMatchingModel: (agent, model) => `${agent} 没有模型「${model}」,按它自己的缺省模型运行`,
|
|
80
83
|
noImages: (agent) => `${agent} 不接受图片,已省略`,
|
|
81
84
|
noImagesDriver: (agent) => `${agent} 驱动暂不传图片,已省略`,
|
|
82
85
|
noImagesOneshot: (agent) => `${agent} 一次性模式不传图片,已省略`,
|
|
@@ -87,6 +90,7 @@ export const zh = {
|
|
|
87
90
|
error: (agent, message, retrying) => `${agent}:${message}${retrying ? "(重试中)" : ""}`,
|
|
88
91
|
askedQuestion: (agent, tool) => `${agent} 想向你提问(${tool});ama 不代答,已请它把问题写进最终回复`,
|
|
89
92
|
askedQuestionCodex: (agent) => `${agent} 想向你提问;ama 不代答,请让它把问题写进最终回复`,
|
|
93
|
+
extensionDialog: (agent, title) => `${agent}:扩展请求输入(${title});ama 不代答,已取消`,
|
|
90
94
|
mcpElicitation: (agent) => `${agent} 的 MCP 服务器请求输入;ama 不代答,已取消`,
|
|
91
95
|
},
|
|
92
96
|
hooks: {
|