@agentex/agent 0.0.16 → 0.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +158 -18
- package/dist/derived.d.ts +69 -0
- package/dist/derived.d.ts.map +1 -0
- package/dist/derived.js +218 -0
- package/dist/derived.js.map +1 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/_shared/http-agent.d.ts +39 -0
- package/dist/providers/_shared/http-agent.d.ts.map +1 -0
- package/dist/providers/_shared/http-agent.js +265 -0
- package/dist/providers/_shared/http-agent.js.map +1 -0
- package/dist/providers/acp/index.d.ts +29 -0
- package/dist/providers/acp/index.d.ts.map +1 -0
- package/dist/providers/acp/index.js +153 -0
- package/dist/providers/acp/index.js.map +1 -0
- package/dist/providers/acp/parse.d.ts +22 -0
- package/dist/providers/acp/parse.d.ts.map +1 -0
- package/dist/providers/acp/parse.js +122 -0
- package/dist/providers/acp/parse.js.map +1 -0
- package/dist/providers/acp/session.d.ts +36 -0
- package/dist/providers/acp/session.d.ts.map +1 -0
- package/dist/providers/acp/session.js +487 -0
- package/dist/providers/acp/session.js.map +1 -0
- package/dist/providers/claude/execute.d.ts.map +1 -1
- package/dist/providers/claude/execute.js +6 -2
- package/dist/providers/claude/execute.js.map +1 -1
- package/dist/providers/claude/index.d.ts.map +1 -1
- package/dist/providers/claude/index.js +3 -0
- package/dist/providers/claude/index.js.map +1 -1
- package/dist/providers/claude/parse.d.ts.map +1 -1
- package/dist/providers/claude/parse.js +8 -0
- package/dist/providers/claude/parse.js.map +1 -1
- package/dist/providers/claude/session.d.ts +43 -4
- package/dist/providers/claude/session.d.ts.map +1 -1
- package/dist/providers/claude/session.js +215 -30
- package/dist/providers/claude/session.js.map +1 -1
- package/dist/providers/codex/execute.d.ts.map +1 -1
- package/dist/providers/codex/execute.js +5 -1
- package/dist/providers/codex/execute.js.map +1 -1
- package/dist/providers/codex/index.d.ts.map +1 -1
- package/dist/providers/codex/index.js +5 -0
- package/dist/providers/codex/index.js.map +1 -1
- package/dist/providers/codex/modes.d.ts +35 -0
- package/dist/providers/codex/modes.d.ts.map +1 -0
- package/dist/providers/codex/modes.js +148 -0
- package/dist/providers/codex/modes.js.map +1 -0
- package/dist/providers/codex/parse.d.ts.map +1 -1
- package/dist/providers/codex/parse.js +4 -0
- package/dist/providers/codex/parse.js.map +1 -1
- package/dist/providers/codex/session.d.ts +45 -4
- package/dist/providers/codex/session.d.ts.map +1 -1
- package/dist/providers/codex/session.js +408 -66
- package/dist/providers/codex/session.js.map +1 -1
- package/dist/providers/copilot/index.d.ts +15 -0
- package/dist/providers/copilot/index.d.ts.map +1 -0
- package/dist/providers/copilot/index.js +19 -0
- package/dist/providers/copilot/index.js.map +1 -0
- package/dist/providers/cursor/index.d.ts.map +1 -1
- package/dist/providers/cursor/index.js +3 -0
- package/dist/providers/cursor/index.js.map +1 -1
- package/dist/providers/cursor/parse.d.ts.map +1 -1
- package/dist/providers/cursor/parse.js +1 -0
- package/dist/providers/cursor/parse.js.map +1 -1
- package/dist/providers/gemini/index.d.ts.map +1 -1
- package/dist/providers/gemini/index.js +16 -18
- package/dist/providers/gemini/index.js.map +1 -1
- package/dist/providers/openclaw/execute.d.ts +5 -0
- package/dist/providers/openclaw/execute.d.ts.map +1 -1
- package/dist/providers/openclaw/execute.js +13 -173
- package/dist/providers/openclaw/execute.js.map +1 -1
- package/dist/providers/openclaw/index.d.ts.map +1 -1
- package/dist/providers/openclaw/index.js +3 -0
- package/dist/providers/openclaw/index.js.map +1 -1
- package/dist/providers/opencode/event-parse.d.ts +23 -0
- package/dist/providers/opencode/event-parse.d.ts.map +1 -0
- package/dist/providers/opencode/event-parse.js +128 -0
- package/dist/providers/opencode/event-parse.js.map +1 -0
- package/dist/providers/opencode/http-session.d.ts +4 -0
- package/dist/providers/opencode/http-session.d.ts.map +1 -0
- package/dist/providers/opencode/http-session.js +376 -0
- package/dist/providers/opencode/http-session.js.map +1 -0
- package/dist/providers/opencode/index.d.ts.map +1 -1
- package/dist/providers/opencode/index.js +8 -1
- package/dist/providers/opencode/index.js.map +1 -1
- package/dist/providers/opencode/parse.d.ts.map +1 -1
- package/dist/providers/opencode/parse.js +1 -0
- package/dist/providers/opencode/parse.js.map +1 -1
- package/dist/providers/opencode/server.d.ts +8 -0
- package/dist/providers/opencode/server.d.ts.map +1 -0
- package/dist/providers/opencode/server.js +0 -0
- package/dist/providers/opencode/server.js.map +1 -0
- package/dist/providers/pi/index.d.ts.map +1 -1
- package/dist/providers/pi/index.js +8 -1
- package/dist/providers/pi/index.js.map +1 -1
- package/dist/providers/pi/parse.d.ts.map +1 -1
- package/dist/providers/pi/parse.js +1 -0
- package/dist/providers/pi/parse.js.map +1 -1
- package/dist/providers/pi/session.d.ts +40 -0
- package/dist/providers/pi/session.d.ts.map +1 -0
- package/dist/providers/pi/session.js +328 -0
- package/dist/providers/pi/session.js.map +1 -0
- package/dist/providers/process/index.d.ts.map +1 -1
- package/dist/providers/process/index.js +3 -0
- package/dist/providers/process/index.js.map +1 -1
- package/dist/registry.d.ts +1 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +6 -0
- package/dist/registry.js.map +1 -1
- package/dist/types.d.ts +192 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/skill-commands.d.ts.map +1 -1
- package/dist/utils/skill-commands.js +4 -2
- package/dist/utils/skill-commands.js.map +1 -1
- package/dist/utils/tool-names.d.ts +24 -0
- package/dist/utils/tool-names.d.ts.map +1 -0
- package/dist/utils/tool-names.js +50 -0
- package/dist/utils/tool-names.js.map +1 -0
- package/package.json +22 -12
- package/dist/providers/claude/test.d.ts +0 -3
- package/dist/providers/claude/test.d.ts.map +0 -1
- package/dist/providers/claude/test.js +0 -167
- package/dist/providers/claude/test.js.map +0 -1
- package/dist/providers/codex/test.d.ts +0 -3
- package/dist/providers/codex/test.d.ts.map +0 -1
- package/dist/providers/codex/test.js +0 -74
- package/dist/providers/codex/test.js.map +0 -1
- package/dist/providers/cursor/test.d.ts +0 -3
- package/dist/providers/cursor/test.d.ts.map +0 -1
- package/dist/providers/cursor/test.js +0 -58
- package/dist/providers/cursor/test.js.map +0 -1
- package/dist/providers/gemini/codec.d.ts +0 -3
- package/dist/providers/gemini/codec.d.ts.map +0 -1
- package/dist/providers/gemini/codec.js +0 -47
- package/dist/providers/gemini/codec.js.map +0 -1
- package/dist/providers/gemini/execute.d.ts +0 -3
- package/dist/providers/gemini/execute.d.ts.map +0 -1
- package/dist/providers/gemini/execute.js +0 -256
- package/dist/providers/gemini/execute.js.map +0 -1
- package/dist/providers/gemini/parse.d.ts +0 -20
- package/dist/providers/gemini/parse.d.ts.map +0 -1
- package/dist/providers/gemini/parse.js +0 -235
- package/dist/providers/gemini/parse.js.map +0 -1
- package/dist/providers/gemini/test.d.ts +0 -3
- package/dist/providers/gemini/test.d.ts.map +0 -1
- package/dist/providers/gemini/test.js +0 -67
- package/dist/providers/gemini/test.js.map +0 -1
- package/dist/providers/openclaw/test.d.ts +0 -3
- package/dist/providers/openclaw/test.d.ts.map +0 -1
- package/dist/providers/openclaw/test.js +0 -54
- package/dist/providers/openclaw/test.js.map +0 -1
- package/dist/providers/opencode/test.d.ts +0 -3
- package/dist/providers/opencode/test.d.ts.map +0 -1
- package/dist/providers/opencode/test.js +0 -60
- package/dist/providers/opencode/test.js.map +0 -1
- package/dist/providers/pi/test.d.ts +0 -3
- package/dist/providers/pi/test.d.ts.map +0 -1
- package/dist/providers/pi/test.js +0 -60
- package/dist/providers/pi/test.js.map +0 -1
- package/dist/utils/model-cache.d.ts +0 -11
- package/dist/utils/model-cache.d.ts.map +0 -1
- package/dist/utils/model-cache.js +0 -17
- package/dist/utils/model-cache.js.map +0 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
function str(v) {
|
|
2
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
3
|
+
}
|
|
4
|
+
/** Extract text from an ACP `ContentBlock` (a chunk update's `content`). */
|
|
5
|
+
export function extractContentText(content) {
|
|
6
|
+
if (!content || typeof content !== "object")
|
|
7
|
+
return null;
|
|
8
|
+
const c = content;
|
|
9
|
+
if (c["type"] === "text" && typeof c["text"] === "string")
|
|
10
|
+
return c["text"];
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
/** Extract displayable text from a tool call's `content` (ToolCallContent[]). */
|
|
14
|
+
export function extractToolContentText(content) {
|
|
15
|
+
if (!Array.isArray(content))
|
|
16
|
+
return null;
|
|
17
|
+
const parts = [];
|
|
18
|
+
for (const item of content) {
|
|
19
|
+
if (!item || typeof item !== "object")
|
|
20
|
+
continue;
|
|
21
|
+
const c = item;
|
|
22
|
+
if (c["type"] === "content") {
|
|
23
|
+
const inner = extractContentText(c["content"]);
|
|
24
|
+
if (inner)
|
|
25
|
+
parts.push(inner);
|
|
26
|
+
}
|
|
27
|
+
else if (c["type"] === "diff") {
|
|
28
|
+
const nd = c["newText"] ?? c["new_text"];
|
|
29
|
+
if (typeof nd === "string")
|
|
30
|
+
parts.push(nd);
|
|
31
|
+
}
|
|
32
|
+
// Note: a "terminal" ToolCallContent carries only a `terminalId` (its output
|
|
33
|
+
// must be fetched via the ACP terminal/* methods, which we don't implement),
|
|
34
|
+
// so there's no inline text to extract here.
|
|
35
|
+
}
|
|
36
|
+
return parts.length ? parts.join("\n") : null;
|
|
37
|
+
}
|
|
38
|
+
function makeBase(update, info) {
|
|
39
|
+
// ACP doesn't expose per-event/message/turn ids, so those stay null; the
|
|
40
|
+
// tool callId IS available, so tool_call/tool_result correlate properly.
|
|
41
|
+
return {
|
|
42
|
+
timestamp: info.timestamp,
|
|
43
|
+
providerType: info.provider,
|
|
44
|
+
sessionId: info.sessionId,
|
|
45
|
+
messageId: null,
|
|
46
|
+
eventId: null,
|
|
47
|
+
turnId: null,
|
|
48
|
+
parentToolCallId: null,
|
|
49
|
+
raw: update,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Map an ACP `session/update` payload (`params.update`) to an agentex
|
|
54
|
+
* `StreamEvent`. Returns null for updates with no agentex equivalent (user
|
|
55
|
+
* echoes, in-progress tool ticks). Unknown update kinds surface as
|
|
56
|
+
* `type: "unknown"` for forward-compat.
|
|
57
|
+
*/
|
|
58
|
+
export function mapAcpUpdate(update, info) {
|
|
59
|
+
const kind = update["sessionUpdate"];
|
|
60
|
+
const base = makeBase(update, info);
|
|
61
|
+
switch (kind) {
|
|
62
|
+
case "agent_message_chunk": {
|
|
63
|
+
const text = extractContentText(update["content"]);
|
|
64
|
+
return text != null ? { type: "assistant", text, ...base } : null;
|
|
65
|
+
}
|
|
66
|
+
case "agent_thought_chunk": {
|
|
67
|
+
const text = extractContentText(update["content"]);
|
|
68
|
+
return text != null ? { type: "thinking", text, ...base } : null;
|
|
69
|
+
}
|
|
70
|
+
case "user_message_chunk":
|
|
71
|
+
return null;
|
|
72
|
+
case "tool_call":
|
|
73
|
+
return {
|
|
74
|
+
type: "tool_call",
|
|
75
|
+
toolCallId: str(update["toolCallId"]),
|
|
76
|
+
name: str(update["title"]) ?? str(update["kind"]) ?? "tool",
|
|
77
|
+
input: update["rawInput"] ?? null,
|
|
78
|
+
...base,
|
|
79
|
+
};
|
|
80
|
+
case "tool_call_update": {
|
|
81
|
+
const status = str(update["status"]);
|
|
82
|
+
// Only emit a result once the tool reaches a terminal state.
|
|
83
|
+
if (status === "completed" || status === "failed") {
|
|
84
|
+
const content = extractToolContentText(update["content"]) ??
|
|
85
|
+
(typeof update["rawOutput"] === "string" ? update["rawOutput"] : "");
|
|
86
|
+
return {
|
|
87
|
+
type: "tool_result",
|
|
88
|
+
toolCallId: str(update["toolCallId"]),
|
|
89
|
+
toolName: str(update["title"]),
|
|
90
|
+
content,
|
|
91
|
+
isError: status === "failed",
|
|
92
|
+
exitCode: null,
|
|
93
|
+
...base,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
default:
|
|
99
|
+
return { type: "unknown", subtype: str(kind) ?? "unknown", ...base };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// NOTE: ACP's `usage_update` reports context-window `{ size, used }` rather than
|
|
103
|
+
// the input/output token split agentex's `TokenUsage` models, so we intentionally
|
|
104
|
+
// don't synthesize usage from ACP — `TurnResult.usage` stays undefined.
|
|
105
|
+
/** Map an ACP `stopReason` to an agentex TurnResult status. */
|
|
106
|
+
export function mapAcpStopReason(stopReason) {
|
|
107
|
+
switch (stopReason) {
|
|
108
|
+
case "end_turn":
|
|
109
|
+
return "completed";
|
|
110
|
+
case "cancelled":
|
|
111
|
+
return "aborted";
|
|
112
|
+
case "refusal":
|
|
113
|
+
return "failed";
|
|
114
|
+
case "max_tokens":
|
|
115
|
+
return "max_budget";
|
|
116
|
+
case "max_turn_requests":
|
|
117
|
+
return "max_turns";
|
|
118
|
+
default:
|
|
119
|
+
return "completed";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=parse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.js","sourceRoot":"","sources":["../../../src/providers/acp/parse.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { AgentSession, AgentMode, SessionContext } from "../../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Optional hooks to absorb a specific ACP agent's quirks without forking the
|
|
4
|
+
* base — Paseo's pattern (e.g. Copilot hides a deprecated "autopilot" mode and
|
|
5
|
+
* exposes a synthetic "allow-all"). Keep the base generic; put weirdness here.
|
|
6
|
+
*/
|
|
7
|
+
export interface AcpTransformers {
|
|
8
|
+
/** Rewrite the discovered mode list (filter, rename, add synthetic modes). */
|
|
9
|
+
modes?: (modes: AgentMode[]) => AgentMode[];
|
|
10
|
+
/** Map a requested mode id to the protocol mode id before `setSessionMode`. */
|
|
11
|
+
modeId?: (modeId: string) => string;
|
|
12
|
+
}
|
|
13
|
+
/** What `acpProvider` hands a session: how to spawn + drive the agent. */
|
|
14
|
+
export interface AcpSessionDeps {
|
|
15
|
+
/** Provider id (used for event `providerType`). */
|
|
16
|
+
provider: string;
|
|
17
|
+
/** Command to spawn: [binary, ...args]. e.g. ["gemini", "--acp"]. */
|
|
18
|
+
command: string[];
|
|
19
|
+
/** Environment overlay. */
|
|
20
|
+
env?: Record<string, string>;
|
|
21
|
+
/** Default mode id applied on session creation. */
|
|
22
|
+
modeId?: string;
|
|
23
|
+
/** Per-agent quirk transformers. */
|
|
24
|
+
transformers?: AcpTransformers;
|
|
25
|
+
}
|
|
26
|
+
/** Spawn the agent, run the ACP handshake, and return a connected session. */
|
|
27
|
+
export declare function createAcpSession(deps: AcpSessionDeps, ctx: SessionContext): Promise<AgentSession>;
|
|
28
|
+
/**
|
|
29
|
+
* Discover an ACP agent's available modes by spawning it, running the
|
|
30
|
+
* handshake, creating a throwaway session, reading the advertised modes, and
|
|
31
|
+
* closing. Returns [] on any failure (modes are advisory).
|
|
32
|
+
*/
|
|
33
|
+
export declare function listAcpModes(deps: AcpSessionDeps, ctx?: SessionContext): Promise<AgentMode[]>;
|
|
34
|
+
/** Parse an ACP `SessionModeState` (`{ currentModeId, availableModes }`) into AgentMode[]. */
|
|
35
|
+
export declare function parseAcpModes(modeState: unknown): AgentMode[];
|
|
36
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/providers/acp/session.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,YAAY,EACZ,SAAS,EAIT,cAAc,EAGf,MAAM,gBAAgB,CAAC;AAcxB;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,CAAC;IAC5C,+EAA+E;IAC/E,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;CACrC;AAED,0EAA0E;AAC1E,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,2BAA2B;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,YAAY,CAAC,EAAE,eAAe,CAAC;CAChC;AA6CD,8EAA8E;AAC9E,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,cAAc,EACpB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,YAAY,CAAC,CAIvB;AAmaD;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,cAAc,EAAE,GAAG,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAoDnG;AAED,8FAA8F;AAC9F,wBAAgB,aAAa,CAAC,SAAS,EAAE,OAAO,GAAG,SAAS,EAAE,CAiB7D"}
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { Readable, Writable } from "node:stream";
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { buildEnv, ensurePathInEnv } from "../../utils/env.js";
|
|
5
|
+
import { uuidv7 } from "../../utils/uuid.js";
|
|
6
|
+
import { extractContentText, mapAcpStopReason, mapAcpUpdate } from "./parse.js";
|
|
7
|
+
/** Reject if `p` doesn't settle within `ms` — bounds the handshake so a hung
|
|
8
|
+
* agent binary can't hang connect()/listModes() indefinitely. */
|
|
9
|
+
function withTimeout(p, ms, label) {
|
|
10
|
+
return Promise.race([
|
|
11
|
+
p,
|
|
12
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)),
|
|
13
|
+
]);
|
|
14
|
+
}
|
|
15
|
+
/** Resume session id from sessionParams (sessionId / session_id), or null. */
|
|
16
|
+
function readAcpResumeId(sp) {
|
|
17
|
+
if (!sp)
|
|
18
|
+
return null;
|
|
19
|
+
const id = sp["sessionId"] ?? sp["session_id"];
|
|
20
|
+
return typeof id === "string" && id.length > 0 ? id : null;
|
|
21
|
+
}
|
|
22
|
+
const ACP_HANDSHAKE_TIMEOUT_MS = 30_000;
|
|
23
|
+
/** Spawn the agent, run the ACP handshake, and return a connected session. */
|
|
24
|
+
export async function createAcpSession(deps, ctx) {
|
|
25
|
+
const session = new AcpSession(deps, ctx);
|
|
26
|
+
await session.connect();
|
|
27
|
+
return session;
|
|
28
|
+
}
|
|
29
|
+
class AcpSession {
|
|
30
|
+
deps;
|
|
31
|
+
ctx;
|
|
32
|
+
_state = "idle";
|
|
33
|
+
_sessionId = null;
|
|
34
|
+
proc = null;
|
|
35
|
+
connection = null;
|
|
36
|
+
/** A turn is in flight (ACP runs one prompt at a time). */
|
|
37
|
+
_turnActive = false;
|
|
38
|
+
/** Accumulated assistant text for the current turn → TurnResult.summary. */
|
|
39
|
+
_turnText = "";
|
|
40
|
+
_inFlight = null;
|
|
41
|
+
_draining = false;
|
|
42
|
+
/** toolCallId → tool name, so tool_call_update can report a name. Cleared per turn. */
|
|
43
|
+
_toolNames = new Map();
|
|
44
|
+
constructor(deps, ctx) {
|
|
45
|
+
this.deps = deps;
|
|
46
|
+
this.ctx = ctx;
|
|
47
|
+
}
|
|
48
|
+
get sessionId() {
|
|
49
|
+
return this._sessionId;
|
|
50
|
+
}
|
|
51
|
+
get state() {
|
|
52
|
+
return this._state;
|
|
53
|
+
}
|
|
54
|
+
// -------------------------------------------------------------------------
|
|
55
|
+
// Connect — spawn + initialize + newSession
|
|
56
|
+
// -------------------------------------------------------------------------
|
|
57
|
+
async connect() {
|
|
58
|
+
const acp = (await import("@agentclientprotocol/sdk"));
|
|
59
|
+
const binary = this.ctx.config?.command ?? this.deps.command[0];
|
|
60
|
+
const args = this.deps.command.slice(1);
|
|
61
|
+
const env = buildEnv({ ...this.deps.env, ...this.ctx.env });
|
|
62
|
+
ensurePathInEnv(env);
|
|
63
|
+
const proc = spawn(binary, args, {
|
|
64
|
+
cwd: this.ctx.cwd ?? process.cwd(),
|
|
65
|
+
env,
|
|
66
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
67
|
+
});
|
|
68
|
+
this.proc = proc;
|
|
69
|
+
proc.stderr?.setEncoding("utf-8");
|
|
70
|
+
proc.stderr?.on("data", (chunk) => {
|
|
71
|
+
if (this.ctx.onOutput) {
|
|
72
|
+
try {
|
|
73
|
+
void this.ctx.onOutput("stderr", chunk);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* swallow */
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
proc.on("exit", () => {
|
|
81
|
+
if (this._state !== "closed")
|
|
82
|
+
this._state = "closed";
|
|
83
|
+
});
|
|
84
|
+
// Catch spawn failures (ENOENT, EACCES) — an unhandled 'error' event would
|
|
85
|
+
// otherwise crash the host process.
|
|
86
|
+
proc.on("error", () => {
|
|
87
|
+
if (this._state !== "closed")
|
|
88
|
+
this._state = "closed";
|
|
89
|
+
});
|
|
90
|
+
if (!proc.stdin || !proc.stdout)
|
|
91
|
+
throw new Error("Failed to open stdio on ACP agent process");
|
|
92
|
+
// fs callbacks do unconfined disk I/O on agent-supplied paths — equivalent to
|
|
93
|
+
// the agent's own filesystem access (it's a local subprocess with the user's
|
|
94
|
+
// privileges), so this isn't a sandbox escape, just coordination.
|
|
95
|
+
try {
|
|
96
|
+
const writable = Writable.toWeb(proc.stdin);
|
|
97
|
+
const readable = Readable.toWeb(proc.stdout);
|
|
98
|
+
const stream = acp.ndJsonStream(writable, readable);
|
|
99
|
+
const client = {
|
|
100
|
+
requestPermission: (params) => this.handlePermission(params),
|
|
101
|
+
sessionUpdate: (params) => this.handleSessionUpdate(params),
|
|
102
|
+
readTextFile: async (params) => {
|
|
103
|
+
const content = await readFile(params.path, "utf-8");
|
|
104
|
+
return { content };
|
|
105
|
+
},
|
|
106
|
+
writeTextFile: async (params) => {
|
|
107
|
+
await writeFile(params.path, params.content, "utf-8");
|
|
108
|
+
return {};
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
this.connection = new acp.ClientSideConnection(() => client, stream);
|
|
112
|
+
const initResult = await withTimeout(this.connection.initialize({
|
|
113
|
+
protocolVersion: acp.PROTOCOL_VERSION,
|
|
114
|
+
clientCapabilities: { fs: { readTextFile: true, writeTextFile: true } },
|
|
115
|
+
}), ACP_HANDSHAKE_TIMEOUT_MS, "acp initialize");
|
|
116
|
+
const agentCaps = initResult.agentCapabilities;
|
|
117
|
+
const cwd = this.ctx.cwd ?? process.cwd();
|
|
118
|
+
// Resume an existing session when the caller supplied one AND the agent
|
|
119
|
+
// advertises `loadSession`; otherwise start fresh. (ACP `session/load`
|
|
120
|
+
// replays history via notifications, which our turn-isolation gate drops —
|
|
121
|
+
// we want continuity, not a re-emit.) When resume isn't possible the new
|
|
122
|
+
// session id supersedes the caller's, so saved params naturally roll over.
|
|
123
|
+
const resumeId = readAcpResumeId(this.ctx.sessionParams);
|
|
124
|
+
let loaded = false;
|
|
125
|
+
if (resumeId && agentCaps?.loadSession && this.connection.loadSession) {
|
|
126
|
+
try {
|
|
127
|
+
await withTimeout(this.connection.loadSession({ sessionId: resumeId, cwd, mcpServers: [] }), ACP_HANDSHAKE_TIMEOUT_MS, "acp loadSession");
|
|
128
|
+
this._sessionId = resumeId;
|
|
129
|
+
loaded = true;
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
if (this.ctx.onOutput) {
|
|
133
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
134
|
+
try {
|
|
135
|
+
void this.ctx.onOutput("stderr", `agentex: acp session/load failed for ${resumeId}, starting fresh: ${reason}\n`);
|
|
136
|
+
}
|
|
137
|
+
catch { /* swallow */ }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!loaded) {
|
|
142
|
+
const res = await withTimeout(this.connection.newSession({ cwd, mcpServers: [] }), ACP_HANDSHAKE_TIMEOUT_MS, "acp newSession");
|
|
143
|
+
this._sessionId = typeof res.sessionId === "string" ? res.sessionId : null;
|
|
144
|
+
}
|
|
145
|
+
const requested = this.ctx.config?.modeId ?? this.deps.modeId;
|
|
146
|
+
if (requested && this._sessionId && this.connection.setSessionMode) {
|
|
147
|
+
const modeId = this.deps.transformers?.modeId
|
|
148
|
+
? this.deps.transformers.modeId(requested)
|
|
149
|
+
: requested;
|
|
150
|
+
try {
|
|
151
|
+
await this.connection.setSessionMode({ sessionId: this._sessionId, modeId });
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
/* mode application is best-effort */
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
// Spawn/handshake failed — tear down so we don't leak the child process.
|
|
160
|
+
this._state = "closed";
|
|
161
|
+
try {
|
|
162
|
+
proc.kill();
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
/* ignore */
|
|
166
|
+
}
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
// Wire AbortSignal → close.
|
|
170
|
+
if (this.ctx.signal) {
|
|
171
|
+
if (this.ctx.signal.aborted)
|
|
172
|
+
void this.close();
|
|
173
|
+
else
|
|
174
|
+
this.ctx.signal.addEventListener("abort", () => void this.close(), { once: true });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// -------------------------------------------------------------------------
|
|
178
|
+
// Client callbacks (agent → us)
|
|
179
|
+
// -------------------------------------------------------------------------
|
|
180
|
+
async handleSessionUpdate(params) {
|
|
181
|
+
const update = params.update;
|
|
182
|
+
if (!update)
|
|
183
|
+
return;
|
|
184
|
+
// Drop stragglers from a finished/aborted turn (ACP events aren't turn-tagged,
|
|
185
|
+
// so a late update would otherwise be attributed to the next turn).
|
|
186
|
+
if (!this._turnActive)
|
|
187
|
+
return;
|
|
188
|
+
const kind = update["sessionUpdate"];
|
|
189
|
+
if (kind === "agent_message_chunk") {
|
|
190
|
+
const text = extractContentText(update["content"]);
|
|
191
|
+
if (text)
|
|
192
|
+
this._turnText += text;
|
|
193
|
+
}
|
|
194
|
+
// Cache tool names from the initial tool_call so tool_call_update (which often
|
|
195
|
+
// omits `title`) can still report a toolName.
|
|
196
|
+
if (kind === "tool_call") {
|
|
197
|
+
const id = update["toolCallId"];
|
|
198
|
+
const title = update["title"];
|
|
199
|
+
if (typeof id === "string" && typeof title === "string" && title) {
|
|
200
|
+
this._toolNames.set(id, title);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const event = mapAcpUpdate(update, {
|
|
204
|
+
provider: this.deps.provider,
|
|
205
|
+
sessionId: this._sessionId,
|
|
206
|
+
timestamp: new Date().toISOString(),
|
|
207
|
+
});
|
|
208
|
+
if (event) {
|
|
209
|
+
if (event.type === "tool_result" && !event.toolName && event.toolCallId) {
|
|
210
|
+
const cached = this._toolNames.get(event.toolCallId);
|
|
211
|
+
if (cached)
|
|
212
|
+
event.toolName = cached;
|
|
213
|
+
}
|
|
214
|
+
if (this.ctx.onEvent) {
|
|
215
|
+
try {
|
|
216
|
+
await this.ctx.onEvent(event);
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
/* a throwing handler must not break the stream */
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async handlePermission(params) {
|
|
225
|
+
const toolCall = (params.toolCall ?? {});
|
|
226
|
+
const options = (params.options ?? []);
|
|
227
|
+
const select = (allow) => {
|
|
228
|
+
const want = allow ? ["allow_once", "allow_always"] : ["reject_once", "reject_always"];
|
|
229
|
+
const opt = options.find((o) => want.includes(String(o["kind"]))) ?? options[0];
|
|
230
|
+
if (!opt)
|
|
231
|
+
return { outcome: { outcome: "cancelled" } };
|
|
232
|
+
return {
|
|
233
|
+
outcome: { outcome: "selected", optionId: String(opt["optionId"]) },
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
this._state = "waiting_for_approval";
|
|
237
|
+
const restore = () => {
|
|
238
|
+
if (this._state === "waiting_for_approval")
|
|
239
|
+
this._state = this._turnActive ? "thinking" : "idle";
|
|
240
|
+
};
|
|
241
|
+
if (!this.ctx.onUserInputRequest) {
|
|
242
|
+
const r = select(true);
|
|
243
|
+
restore();
|
|
244
|
+
return r;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const result = await this.ctx.onUserInputRequest({
|
|
248
|
+
toolName: String(toolCall["title"] ?? toolCall["kind"] ?? "tool"),
|
|
249
|
+
input: toolCall["rawInput"] ?? toolCall,
|
|
250
|
+
toolUseId: String(toolCall["toolCallId"] ?? ""),
|
|
251
|
+
...(typeof toolCall["title"] === "string" ? { title: toolCall["title"] } : {}),
|
|
252
|
+
});
|
|
253
|
+
const r = select(result.allow);
|
|
254
|
+
restore();
|
|
255
|
+
return r;
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
const r = select(false);
|
|
259
|
+
restore();
|
|
260
|
+
return r;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// -------------------------------------------------------------------------
|
|
264
|
+
// Public API
|
|
265
|
+
// -------------------------------------------------------------------------
|
|
266
|
+
async send(message, options) {
|
|
267
|
+
if (this._state === "closed")
|
|
268
|
+
throw new Error("Session is closed");
|
|
269
|
+
if (this._draining)
|
|
270
|
+
throw new Error("Session is draining — no new sends accepted");
|
|
271
|
+
if (this._turnActive) {
|
|
272
|
+
throw new Error("ACP session is busy — a turn is already in progress (concurrentSend not supported)");
|
|
273
|
+
}
|
|
274
|
+
const uuid = uuidv7();
|
|
275
|
+
this._turnActive = true;
|
|
276
|
+
this._turnText = "";
|
|
277
|
+
this._toolNames.clear();
|
|
278
|
+
this._state = "thinking";
|
|
279
|
+
const result = this.runPrompt(message, options);
|
|
280
|
+
this._inFlight = result;
|
|
281
|
+
return { uuid, result };
|
|
282
|
+
}
|
|
283
|
+
finishTurn() {
|
|
284
|
+
this._turnActive = false;
|
|
285
|
+
this._inFlight = null;
|
|
286
|
+
if (this._state !== "closed")
|
|
287
|
+
this._state = "idle";
|
|
288
|
+
}
|
|
289
|
+
async runPrompt(message, options) {
|
|
290
|
+
const sessionId = this._sessionId;
|
|
291
|
+
if (!this.connection || !sessionId) {
|
|
292
|
+
this.finishTurn();
|
|
293
|
+
return {
|
|
294
|
+
summary: null,
|
|
295
|
+
costUsd: null,
|
|
296
|
+
status: "failed",
|
|
297
|
+
errorCode: "not_initialized",
|
|
298
|
+
errorMessage: "ACP session is not initialized",
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
const timeoutSec = options?.timeoutSec ?? this.ctx.config?.timeoutSec;
|
|
302
|
+
let timer = null;
|
|
303
|
+
const promptP = this.connection
|
|
304
|
+
.prompt({ sessionId, prompt: [{ type: "text", text: message }] })
|
|
305
|
+
.then((res) => ({ ok: true, res }))
|
|
306
|
+
.catch((err) => ({
|
|
307
|
+
ok: false,
|
|
308
|
+
error: err instanceof Error ? err.message : String(err),
|
|
309
|
+
}));
|
|
310
|
+
const guards = [];
|
|
311
|
+
if (timeoutSec && timeoutSec > 0) {
|
|
312
|
+
guards.push(new Promise((resolve) => {
|
|
313
|
+
timer = setTimeout(() => resolve("timeout"), timeoutSec * 1000);
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
if (options?.signal) {
|
|
317
|
+
if (options.signal.aborted)
|
|
318
|
+
guards.push(Promise.resolve("aborted"));
|
|
319
|
+
else
|
|
320
|
+
guards.push(new Promise((resolve) => options.signal.addEventListener("abort", () => resolve("aborted"), { once: true })));
|
|
321
|
+
}
|
|
322
|
+
const outcome = await Promise.race([promptP.then(() => "prompt"), ...guards]);
|
|
323
|
+
if (timer)
|
|
324
|
+
clearTimeout(timer);
|
|
325
|
+
if (outcome === "timeout" || outcome === "aborted") {
|
|
326
|
+
try {
|
|
327
|
+
await this.interrupt();
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
/* best effort */
|
|
331
|
+
}
|
|
332
|
+
// Drain the cancelled turn before finishing: ACP guarantees the agent
|
|
333
|
+
// flushes any pending session/update notifications and then resolves the
|
|
334
|
+
// prompt as cancelled. Awaiting it (bounded) keeps those stragglers inside
|
|
335
|
+
// THIS turn's window so they can't bleed into the next turn's text/events.
|
|
336
|
+
const summaryBeforeDrain = this._turnText;
|
|
337
|
+
await Promise.race([
|
|
338
|
+
promptP,
|
|
339
|
+
new Promise((resolve) => setTimeout(resolve, 2000)),
|
|
340
|
+
]);
|
|
341
|
+
this.finishTurn();
|
|
342
|
+
return {
|
|
343
|
+
summary: (this._turnText || summaryBeforeDrain) || null,
|
|
344
|
+
costUsd: null,
|
|
345
|
+
status: outcome,
|
|
346
|
+
errorCode: outcome,
|
|
347
|
+
errorMessage: outcome === "timeout" ? "Turn exceeded its timeout" : "Turn aborted",
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
const settled = await promptP;
|
|
351
|
+
this.finishTurn();
|
|
352
|
+
if (!settled.ok) {
|
|
353
|
+
return {
|
|
354
|
+
summary: this._turnText || null,
|
|
355
|
+
costUsd: null,
|
|
356
|
+
status: "failed",
|
|
357
|
+
errorCode: "prompt_error",
|
|
358
|
+
errorMessage: settled.error,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
summary: this._turnText || null,
|
|
363
|
+
costUsd: null,
|
|
364
|
+
status: mapAcpStopReason(settled.res?.stopReason),
|
|
365
|
+
errorCode: null,
|
|
366
|
+
errorMessage: null,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
async cancel(_uuid) {
|
|
370
|
+
// ACP has no per-queued-message cancel — only whole-turn cancel via interrupt().
|
|
371
|
+
return { cancelled: false };
|
|
372
|
+
}
|
|
373
|
+
async interrupt() {
|
|
374
|
+
if (this.connection && this._sessionId) {
|
|
375
|
+
try {
|
|
376
|
+
await this.connection.cancel({ sessionId: this._sessionId });
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
/* best effort */
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async drain() {
|
|
384
|
+
this._draining = true;
|
|
385
|
+
const inFlight = this._inFlight;
|
|
386
|
+
if (inFlight) {
|
|
387
|
+
try {
|
|
388
|
+
await inFlight;
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
/* ignore */
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
await this.close();
|
|
395
|
+
}
|
|
396
|
+
async close() {
|
|
397
|
+
this._state = "closed";
|
|
398
|
+
const proc = this.proc;
|
|
399
|
+
this.proc = null;
|
|
400
|
+
if (proc && !proc.killed) {
|
|
401
|
+
try {
|
|
402
|
+
proc.kill();
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
/* ignore */
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Discover an ACP agent's available modes by spawning it, running the
|
|
412
|
+
* handshake, creating a throwaway session, reading the advertised modes, and
|
|
413
|
+
* closing. Returns [] on any failure (modes are advisory).
|
|
414
|
+
*/
|
|
415
|
+
export async function listAcpModes(deps, ctx) {
|
|
416
|
+
let proc = null;
|
|
417
|
+
try {
|
|
418
|
+
const acp = (await import("@agentclientprotocol/sdk"));
|
|
419
|
+
const binary = ctx?.config?.command ?? deps.command[0];
|
|
420
|
+
const args = deps.command.slice(1);
|
|
421
|
+
const env = buildEnv({ ...deps.env, ...ctx?.env });
|
|
422
|
+
ensurePathInEnv(env);
|
|
423
|
+
proc = spawn(binary, args, {
|
|
424
|
+
cwd: ctx?.cwd ?? process.cwd(),
|
|
425
|
+
env,
|
|
426
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
427
|
+
});
|
|
428
|
+
// Swallow spawn errors so they don't surface as an unhandled 'error' crash.
|
|
429
|
+
proc.on("error", () => { });
|
|
430
|
+
if (!proc.stdin || !proc.stdout)
|
|
431
|
+
return [];
|
|
432
|
+
const writable = Writable.toWeb(proc.stdin);
|
|
433
|
+
const readable = Readable.toWeb(proc.stdout);
|
|
434
|
+
const stream = acp.ndJsonStream(writable, readable);
|
|
435
|
+
const client = {
|
|
436
|
+
requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
|
|
437
|
+
sessionUpdate: async () => { },
|
|
438
|
+
};
|
|
439
|
+
const connection = new acp.ClientSideConnection(() => client, stream);
|
|
440
|
+
await withTimeout(connection.initialize({
|
|
441
|
+
protocolVersion: acp.PROTOCOL_VERSION,
|
|
442
|
+
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
|
|
443
|
+
}), ACP_HANDSHAKE_TIMEOUT_MS, "acp initialize (listModes)");
|
|
444
|
+
const res = await withTimeout(connection.newSession({ cwd: ctx?.cwd ?? process.cwd(), mcpServers: [] }), ACP_HANDSHAKE_TIMEOUT_MS, "acp newSession (listModes)");
|
|
445
|
+
const modes = parseAcpModes(res.modes);
|
|
446
|
+
return deps.transformers?.modes ? deps.transformers.modes(modes) : modes;
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
return [];
|
|
450
|
+
}
|
|
451
|
+
finally {
|
|
452
|
+
// Always tear down the throwaway process — initialize/newSession can throw
|
|
453
|
+
// (agent doesn't speak ACP, handshake mismatch) and would otherwise leak it.
|
|
454
|
+
if (proc && !proc.killed) {
|
|
455
|
+
try {
|
|
456
|
+
proc.kill();
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
/* ignore */
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/** Parse an ACP `SessionModeState` (`{ currentModeId, availableModes }`) into AgentMode[]. */
|
|
465
|
+
export function parseAcpModes(modeState) {
|
|
466
|
+
if (!modeState || typeof modeState !== "object")
|
|
467
|
+
return [];
|
|
468
|
+
const available = modeState["availableModes"];
|
|
469
|
+
if (!Array.isArray(available))
|
|
470
|
+
return [];
|
|
471
|
+
const out = [];
|
|
472
|
+
for (const m of available) {
|
|
473
|
+
if (!m || typeof m !== "object")
|
|
474
|
+
continue;
|
|
475
|
+
const r = m;
|
|
476
|
+
const id = typeof r["id"] === "string" ? r["id"] : "";
|
|
477
|
+
if (!id)
|
|
478
|
+
continue;
|
|
479
|
+
out.push({
|
|
480
|
+
id,
|
|
481
|
+
name: typeof r["name"] === "string" ? r["name"] : id,
|
|
482
|
+
...(typeof r["description"] === "string" ? { description: r["description"] } : {}),
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
return out;
|
|
486
|
+
}
|
|
487
|
+
//# sourceMappingURL=session.js.map
|