@akira-tl/forgerelay 0.6.2 → 0.7.1
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 +26 -0
- package/capabilities/subagents/GUIDE.md +100 -40
- package/dist/activity/lifecycle.js +1 -1
- package/dist/capabilities.js +1 -1
- package/dist/capability-registry.js +34 -0
- package/dist/cli.js +80 -165
- package/dist/db/migrations.js +14 -0
- package/dist/db/schema.js +6 -0
- package/dist/server.js +29 -32
- package/dist/{local-agent-targets.js → subagents/cli-target.js} +6 -6
- package/dist/{local-agent-profiles.js → subagents/profiles.js} +13 -5
- package/dist/subagents/providers/adapters/acp.js +148 -0
- package/dist/subagents/providers/adapters/claude.js +75 -0
- package/dist/{local-agent-runtime.js → subagents/providers/adapters/codex.js} +10 -4
- package/dist/subagents/providers/adapters/opencode.js +137 -0
- package/dist/subagents/providers/adapters/pi.js +232 -0
- package/dist/{local-agent-availability.js → subagents/providers/availability.js} +24 -10
- package/dist/subagents/providers/continuation.js +11 -0
- package/dist/subagents/providers/contract.js +1 -0
- package/dist/subagents/providers/registry.js +26 -0
- package/dist/subagents/providers/shared.js +40 -0
- package/dist/subagents/sessions/capability.js +214 -0
- package/dist/subagents/sessions/delivery-mailbox.js +115 -0
- package/dist/subagents/sessions/execution.js +171 -0
- package/dist/subagents/sessions/manager.js +107 -0
- package/dist/subagents/sessions/mcp/audit.js +85 -0
- package/dist/subagents/sessions/mcp/runtime.js +19 -0
- package/dist/{local-agent-store.js → subagents/sessions/store.js} +75 -39
- package/dist/workspaces.js +3 -3
- package/docs/chatgpt-coding-workflow.md +2 -9
- package/docs/roadmap.md +42 -7
- package/package.json +2 -2
- package/scripts/release/release-gate.test.mjs +2 -2
- package/dist/local-agent-adapters.js +0 -653
- /package/dist/{local-agent-path.js → subagents/providers/path.js} +0 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { asRecord, readArray, readNestedString, requireFinalResponse, unwrapProviderPayload, } from "../shared.js";
|
|
2
|
+
export class OpencodeSubagentAdapter {
|
|
3
|
+
provider = "opencode";
|
|
4
|
+
async run(input) {
|
|
5
|
+
const { createOpencode } = await import("@opencode-ai/sdk/v2");
|
|
6
|
+
const { client, server } = await createOpencode();
|
|
7
|
+
try {
|
|
8
|
+
const sessionId = input.providerSessionId ?? await createOpencodeSession(client, input);
|
|
9
|
+
const promptResult = await promptOpencodeSession(client, sessionId, input);
|
|
10
|
+
await waitForOpencodeSession(client, sessionId);
|
|
11
|
+
const messages = await readOpencodeMessages(client, sessionId);
|
|
12
|
+
const finalResponse = requireFinalResponse("OpenCode", extractOpenCodeFinalResponse(messages) || extractOpenCodeFinalResponse(promptResult));
|
|
13
|
+
return {
|
|
14
|
+
provider: this.provider,
|
|
15
|
+
providerSessionId: sessionId,
|
|
16
|
+
finalResponse,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
server.close();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function createOpencodeSession(client, input) {
|
|
25
|
+
const sessionClient = client;
|
|
26
|
+
const result = await sessionClient.session.create({
|
|
27
|
+
directory: input.workspace,
|
|
28
|
+
location: { directory: input.workspace },
|
|
29
|
+
...(input.model ? { model: parseOpencodeModel(input.model) } : {}),
|
|
30
|
+
}, { throwOnError: true });
|
|
31
|
+
const id = readNestedString(result, ["id"]) ??
|
|
32
|
+
readNestedString(result, ["data", "id"]) ??
|
|
33
|
+
readNestedString(result, ["session", "id"]) ??
|
|
34
|
+
readNestedString(result, ["data", "session", "id"]);
|
|
35
|
+
if (typeof id !== "string") {
|
|
36
|
+
throw new Error("OpenCode did not return a session id.");
|
|
37
|
+
}
|
|
38
|
+
return id;
|
|
39
|
+
}
|
|
40
|
+
async function promptOpencodeSession(client, sessionId, input) {
|
|
41
|
+
const session = client.session;
|
|
42
|
+
const promptInput = {
|
|
43
|
+
sessionID: sessionId,
|
|
44
|
+
directory: input.workspace,
|
|
45
|
+
prompt: { parts: [{ type: "text", text: input.prompt }] },
|
|
46
|
+
parts: [{ type: "text", text: input.prompt }],
|
|
47
|
+
...(input.model ? { model: parseOpencodeModel(input.model) } : {}),
|
|
48
|
+
...(input.thinking ? { variant: input.thinking } : {}),
|
|
49
|
+
};
|
|
50
|
+
return session.prompt(promptInput, { throwOnError: true });
|
|
51
|
+
}
|
|
52
|
+
async function waitForOpencodeSession(client, sessionId) {
|
|
53
|
+
const session = client.session;
|
|
54
|
+
if (!session?.wait)
|
|
55
|
+
return;
|
|
56
|
+
await session.wait({ sessionID: sessionId }, { throwOnError: true });
|
|
57
|
+
}
|
|
58
|
+
async function readOpencodeMessages(client, sessionId) {
|
|
59
|
+
const session = client.session;
|
|
60
|
+
if (!session?.messages)
|
|
61
|
+
return undefined;
|
|
62
|
+
return session.messages({ sessionID: sessionId, order: "asc", limit: 100 }, { throwOnError: true });
|
|
63
|
+
}
|
|
64
|
+
function parseOpencodeModel(model) {
|
|
65
|
+
const separator = model.indexOf("/");
|
|
66
|
+
if (separator === -1)
|
|
67
|
+
return { providerID: "opencode", modelID: model };
|
|
68
|
+
return {
|
|
69
|
+
providerID: model.slice(0, separator),
|
|
70
|
+
modelID: model.slice(separator + 1),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function extractOpenCodeFinalResponse(value) {
|
|
74
|
+
const root = unwrapProviderPayload(value);
|
|
75
|
+
const messages = Array.isArray(root) ? root : readArray(root, "messages");
|
|
76
|
+
if (messages)
|
|
77
|
+
return extractLastOpenCodeAssistantMessageText(messages);
|
|
78
|
+
return extractOpenCodeAssistantMessageText(root);
|
|
79
|
+
}
|
|
80
|
+
function extractLastOpenCodeAssistantMessageText(messages) {
|
|
81
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
82
|
+
const message = asRecord(messages[index]);
|
|
83
|
+
if (!message)
|
|
84
|
+
continue;
|
|
85
|
+
const info = asRecord(message.info);
|
|
86
|
+
const role = typeof info?.role === "string" ? info.role : message.role;
|
|
87
|
+
const type = typeof message.type === "string" ? message.type : undefined;
|
|
88
|
+
if (role !== "assistant" && type !== "assistant")
|
|
89
|
+
continue;
|
|
90
|
+
const text = extractOpenCodeAssistantMessageText(message);
|
|
91
|
+
if (text)
|
|
92
|
+
return text;
|
|
93
|
+
}
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
function extractOpenCodeAssistantMessageText(value) {
|
|
97
|
+
const message = asRecord(value);
|
|
98
|
+
if (!message)
|
|
99
|
+
return "";
|
|
100
|
+
const content = readArray(message, "content");
|
|
101
|
+
if (content) {
|
|
102
|
+
const text = content
|
|
103
|
+
.map((part) => {
|
|
104
|
+
const partRecord = asRecord(part);
|
|
105
|
+
if (!partRecord || partRecord.type !== "text")
|
|
106
|
+
return "";
|
|
107
|
+
return typeof partRecord.text === "string" ? partRecord.text : "";
|
|
108
|
+
})
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.join("");
|
|
111
|
+
if (text.trim())
|
|
112
|
+
return text.trim();
|
|
113
|
+
}
|
|
114
|
+
const parts = readArray(message, "parts");
|
|
115
|
+
if (parts) {
|
|
116
|
+
const text = parts
|
|
117
|
+
.map((part) => {
|
|
118
|
+
const partRecord = asRecord(part);
|
|
119
|
+
if (!partRecord || partRecord.type !== "text")
|
|
120
|
+
return "";
|
|
121
|
+
return typeof partRecord.text === "string" ? partRecord.text : "";
|
|
122
|
+
})
|
|
123
|
+
.filter(Boolean)
|
|
124
|
+
.join("");
|
|
125
|
+
if (text.trim())
|
|
126
|
+
return text.trim();
|
|
127
|
+
}
|
|
128
|
+
const info = asRecord(message.info) ?? message;
|
|
129
|
+
return stringifyStructuredAssistantMessage(info.structured);
|
|
130
|
+
}
|
|
131
|
+
function stringifyStructuredAssistantMessage(value) {
|
|
132
|
+
if (value === undefined || value === null)
|
|
133
|
+
return "";
|
|
134
|
+
if (typeof value === "string")
|
|
135
|
+
return value.trim();
|
|
136
|
+
return JSON.stringify(value);
|
|
137
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { removeDevspaceNodeModulesBinFromPath } from "../path.js";
|
|
3
|
+
import { asRecord, assertPipedChild, errorMessage, readArray, readNestedString, requireFinalResponse, unwrapProviderPayload, } from "../shared.js";
|
|
4
|
+
const PI_AGENT_TIMEOUT_MS = 120_000;
|
|
5
|
+
export class PiRpcSubagentAdapter {
|
|
6
|
+
provider = "pi";
|
|
7
|
+
async run(input) {
|
|
8
|
+
const args = ["--mode", "rpc"];
|
|
9
|
+
if (input.model)
|
|
10
|
+
args.push("--model", input.model);
|
|
11
|
+
if (input.thinking)
|
|
12
|
+
args.push("--thinking", input.thinking);
|
|
13
|
+
if (input.providerSessionId)
|
|
14
|
+
args.push("--session", input.providerSessionId);
|
|
15
|
+
const child = spawn(process.env.PI_COMMAND ?? "pi", args, {
|
|
16
|
+
cwd: input.workspace,
|
|
17
|
+
env: piCommandEnvironment(process.env),
|
|
18
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
19
|
+
windowsHide: true,
|
|
20
|
+
});
|
|
21
|
+
assertPipedChild(child);
|
|
22
|
+
const rpc = new JsonLineRpc(child);
|
|
23
|
+
let streamingText = "";
|
|
24
|
+
let streamingProviderError = "";
|
|
25
|
+
rpc.onEvent((event) => {
|
|
26
|
+
const text = extractPiStreamingText([event]);
|
|
27
|
+
if (text)
|
|
28
|
+
streamingText += text;
|
|
29
|
+
const providerError = extractPiProviderError(event);
|
|
30
|
+
if (providerError)
|
|
31
|
+
streamingProviderError = providerError;
|
|
32
|
+
});
|
|
33
|
+
try {
|
|
34
|
+
const state = await rpc.request({ type: "get_state" });
|
|
35
|
+
const providerSessionId = readNestedString(state, ["sessionId"]) ?? input.providerSessionId ?? null;
|
|
36
|
+
const done = rpc.waitForEvent((event) => asRecord(event)?.type === "agent_end", PI_AGENT_TIMEOUT_MS);
|
|
37
|
+
await rpc.request({ type: "prompt", message: input.prompt });
|
|
38
|
+
const agentEnd = await done;
|
|
39
|
+
const sessionMessages = await rpc.request({ type: "get_messages" });
|
|
40
|
+
const finalResponse = extractPiFinalResponse(agentEnd) ||
|
|
41
|
+
extractPiFinalResponse(sessionMessages) ||
|
|
42
|
+
streamingText.trim();
|
|
43
|
+
if (!finalResponse) {
|
|
44
|
+
const providerError = extractPiProviderError(agentEnd) ||
|
|
45
|
+
extractPiProviderError(sessionMessages) ||
|
|
46
|
+
streamingProviderError;
|
|
47
|
+
if (providerError)
|
|
48
|
+
throw new Error(`Pi returned an error: ${providerError}`);
|
|
49
|
+
}
|
|
50
|
+
requireFinalResponse("Pi", finalResponse);
|
|
51
|
+
return {
|
|
52
|
+
provider: this.provider,
|
|
53
|
+
providerSessionId,
|
|
54
|
+
finalResponse,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
child.kill();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function piCommandEnvironment(env) {
|
|
63
|
+
if (env.PI_COMMAND)
|
|
64
|
+
return env;
|
|
65
|
+
const path = env.PATH;
|
|
66
|
+
if (!path)
|
|
67
|
+
return env;
|
|
68
|
+
return {
|
|
69
|
+
...env,
|
|
70
|
+
PATH: removeDevspaceNodeModulesBinFromPath(path),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
class JsonLineRpc {
|
|
74
|
+
child;
|
|
75
|
+
pending = new Map();
|
|
76
|
+
eventSubscribers = new Set();
|
|
77
|
+
buffer = "";
|
|
78
|
+
nextId = 1;
|
|
79
|
+
stderr = "";
|
|
80
|
+
fatalError;
|
|
81
|
+
constructor(child) {
|
|
82
|
+
this.child = child;
|
|
83
|
+
child.stdout.on("data", (chunk) => this.handleStdout(chunk.toString("utf8")));
|
|
84
|
+
child.stderr.on("data", (chunk) => {
|
|
85
|
+
this.stderr += chunk.toString("utf8");
|
|
86
|
+
});
|
|
87
|
+
child.on("exit", (code, signal) => {
|
|
88
|
+
this.failAll(new Error(`Pi RPC process exited with code ${code ?? "null"} and signal ${signal ?? "null"}\n${this.stderr}`.trim()));
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
request(command) {
|
|
92
|
+
if (this.fatalError) {
|
|
93
|
+
return Promise.reject(this.fatalError);
|
|
94
|
+
}
|
|
95
|
+
const id = `req_${this.nextId}`;
|
|
96
|
+
this.nextId += 1;
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
this.pending.set(id, { resolve, reject });
|
|
99
|
+
this.child.stdin.write(`${JSON.stringify({ ...command, id })}\n`);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
onEvent(callback) {
|
|
103
|
+
this.eventSubscribers.add(callback);
|
|
104
|
+
return () => this.eventSubscribers.delete(callback);
|
|
105
|
+
}
|
|
106
|
+
waitForEvent(predicate, timeoutMs) {
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
const timer = setTimeout(() => {
|
|
109
|
+
unsubscribe();
|
|
110
|
+
reject(new Error(`Pi RPC timed out waiting for agent completion\n${this.stderr}`.trim()));
|
|
111
|
+
}, timeoutMs);
|
|
112
|
+
const unsubscribe = this.onEvent((event) => {
|
|
113
|
+
if (!predicate(event))
|
|
114
|
+
return;
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
unsubscribe();
|
|
117
|
+
resolve(event);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
handleStdout(chunk) {
|
|
122
|
+
this.buffer += chunk;
|
|
123
|
+
for (;;) {
|
|
124
|
+
const newline = this.buffer.indexOf("\n");
|
|
125
|
+
if (newline === -1)
|
|
126
|
+
return;
|
|
127
|
+
const line = this.buffer.slice(0, newline).trim();
|
|
128
|
+
this.buffer = this.buffer.slice(newline + 1);
|
|
129
|
+
if (!line)
|
|
130
|
+
continue;
|
|
131
|
+
let message;
|
|
132
|
+
try {
|
|
133
|
+
message = JSON.parse(line);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
this.stderr += `${line}\n`;
|
|
137
|
+
this.failAll(new Error(`Pi RPC emitted malformed JSON on stdout: ${line}`));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (message.type !== "response") {
|
|
141
|
+
for (const subscriber of this.eventSubscribers)
|
|
142
|
+
subscriber(message);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const id = typeof message.id === "string" ? message.id : undefined;
|
|
146
|
+
if (!id)
|
|
147
|
+
continue;
|
|
148
|
+
const pending = this.pending.get(id);
|
|
149
|
+
if (!pending)
|
|
150
|
+
continue;
|
|
151
|
+
this.pending.delete(id);
|
|
152
|
+
if (message.success === false || message.error) {
|
|
153
|
+
pending.reject(new Error(errorMessage(message.error ?? `Pi RPC request failed: ${message.command ?? id}`)));
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
pending.resolve(message.data ?? message.result ?? message);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
failAll(error) {
|
|
161
|
+
this.fatalError = error;
|
|
162
|
+
for (const pending of this.pending.values()) {
|
|
163
|
+
pending.reject(error);
|
|
164
|
+
}
|
|
165
|
+
this.pending.clear();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export function extractPiFinalResponse(value) {
|
|
169
|
+
const root = unwrapProviderPayload(value);
|
|
170
|
+
const messages = Array.isArray(root) ? root : readArray(root, "messages");
|
|
171
|
+
if (!messages)
|
|
172
|
+
return "";
|
|
173
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
174
|
+
const message = asRecord(messages[index]);
|
|
175
|
+
if (!message || message.role !== "assistant")
|
|
176
|
+
continue;
|
|
177
|
+
const text = extractPiAssistantMessageText(message);
|
|
178
|
+
if (text)
|
|
179
|
+
return text;
|
|
180
|
+
}
|
|
181
|
+
return "";
|
|
182
|
+
}
|
|
183
|
+
export function extractPiStreamingText(events) {
|
|
184
|
+
return events
|
|
185
|
+
.map((event) => {
|
|
186
|
+
const record = asRecord(event);
|
|
187
|
+
if (!record || record.type !== "message_update")
|
|
188
|
+
return "";
|
|
189
|
+
const update = asRecord(record.assistantMessageEvent);
|
|
190
|
+
if (!update || update.type !== "text_delta")
|
|
191
|
+
return "";
|
|
192
|
+
return typeof update.delta === "string" ? update.delta : "";
|
|
193
|
+
})
|
|
194
|
+
.filter(Boolean)
|
|
195
|
+
.join("")
|
|
196
|
+
.trim();
|
|
197
|
+
}
|
|
198
|
+
export function extractPiProviderError(value) {
|
|
199
|
+
const root = unwrapProviderPayload(value);
|
|
200
|
+
if (Array.isArray(root)) {
|
|
201
|
+
for (let index = root.length - 1; index >= 0; index -= 1) {
|
|
202
|
+
const error = extractPiProviderError(root[index]);
|
|
203
|
+
if (error)
|
|
204
|
+
return error;
|
|
205
|
+
}
|
|
206
|
+
return "";
|
|
207
|
+
}
|
|
208
|
+
const messages = readArray(root, "messages");
|
|
209
|
+
if (messages)
|
|
210
|
+
return extractPiProviderError(messages);
|
|
211
|
+
const message = asRecord(root)?.message ?? root;
|
|
212
|
+
const record = asRecord(message);
|
|
213
|
+
if (!record)
|
|
214
|
+
return "";
|
|
215
|
+
const error = record.errorMessage ?? record.error;
|
|
216
|
+
return typeof error === "string" ? error.trim() : "";
|
|
217
|
+
}
|
|
218
|
+
function extractPiAssistantMessageText(message) {
|
|
219
|
+
const content = message.content;
|
|
220
|
+
if (!Array.isArray(content))
|
|
221
|
+
return "";
|
|
222
|
+
return content
|
|
223
|
+
.map((part) => {
|
|
224
|
+
const partRecord = asRecord(part);
|
|
225
|
+
if (!partRecord || partRecord.type !== "text")
|
|
226
|
+
return "";
|
|
227
|
+
return typeof partRecord.text === "string" ? partRecord.text : "";
|
|
228
|
+
})
|
|
229
|
+
.filter(Boolean)
|
|
230
|
+
.join("\n\n")
|
|
231
|
+
.trim();
|
|
232
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { delimiter, resolve } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import { subagentProviderContinuationSupported } from "./continuation.js";
|
|
4
|
+
import { removeDevspaceNodeModulesBinFromPath } from "./path.js";
|
|
5
|
+
import { SUBAGENT_PROVIDERS, } from "../profiles.js";
|
|
6
|
+
export function getSubagentProviderAvailabilitySnapshot(env = process.env) {
|
|
7
|
+
return SUBAGENT_PROVIDERS.map((provider) => checkSubagentProviderAvailability(provider, env));
|
|
7
8
|
}
|
|
8
|
-
export function
|
|
9
|
+
export function checkSubagentProviderAvailability(provider, env = process.env) {
|
|
9
10
|
switch (provider) {
|
|
10
11
|
case "codex":
|
|
11
12
|
return packageAvailability(provider, "@openai/codex-sdk");
|
|
@@ -23,13 +24,16 @@ export function checkLocalAgentProviderAvailability(provider, env = process.env)
|
|
|
23
24
|
return commandAvailability(provider, "copilot");
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
|
-
export function
|
|
27
|
-
const availability =
|
|
27
|
+
export function assertSubagentProviderAvailable(provider, env = process.env) {
|
|
28
|
+
const availability = checkSubagentProviderAvailability(provider, env);
|
|
28
29
|
if (availability.available)
|
|
29
30
|
return;
|
|
30
31
|
throw new Error(`${provider} provider is not available: ${availability.reason ?? "provider preflight failed"}`);
|
|
31
32
|
}
|
|
32
|
-
export function
|
|
33
|
+
export function formatUnavailableSubagentProvider(provider) {
|
|
34
|
+
return `${provider.name} (${provider.reason ?? "unavailable"})`;
|
|
35
|
+
}
|
|
36
|
+
export function formatSubagentProviderAvailabilitySummary(providers) {
|
|
33
37
|
const available = providers
|
|
34
38
|
.filter((provider) => provider.available)
|
|
35
39
|
.map((provider) => provider.name);
|
|
@@ -44,12 +48,17 @@ export function formatLocalAgentProviderAvailabilitySummary(providers) {
|
|
|
44
48
|
function packageAvailability(provider, packageName) {
|
|
45
49
|
try {
|
|
46
50
|
import.meta.resolve(packageName);
|
|
47
|
-
return {
|
|
51
|
+
return {
|
|
52
|
+
name: provider,
|
|
53
|
+
available: true,
|
|
54
|
+
continuationSupported: subagentProviderContinuationSupported(provider),
|
|
55
|
+
};
|
|
48
56
|
}
|
|
49
57
|
catch {
|
|
50
58
|
return {
|
|
51
59
|
name: provider,
|
|
52
60
|
available: false,
|
|
61
|
+
continuationSupported: subagentProviderContinuationSupported(provider),
|
|
53
62
|
reason: `${packageName} package not found`,
|
|
54
63
|
};
|
|
55
64
|
}
|
|
@@ -60,10 +69,15 @@ function commandAvailability(provider, command, options = {}) {
|
|
|
60
69
|
return {
|
|
61
70
|
name: provider,
|
|
62
71
|
available: false,
|
|
72
|
+
continuationSupported: subagentProviderContinuationSupported(provider),
|
|
63
73
|
reason: `${command} executable not found`,
|
|
64
74
|
};
|
|
65
75
|
}
|
|
66
|
-
return {
|
|
76
|
+
return {
|
|
77
|
+
name: provider,
|
|
78
|
+
available: true,
|
|
79
|
+
continuationSupported: subagentProviderContinuationSupported(provider),
|
|
80
|
+
};
|
|
67
81
|
}
|
|
68
82
|
function resolveCommand(command, env = process.env) {
|
|
69
83
|
const commandHasPath = command.includes("/") || command.includes("\\");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { AcpSubagentAdapter } from "./adapters/acp.js";
|
|
2
|
+
import { ClaudeSubagentAdapter } from "./adapters/claude.js";
|
|
3
|
+
import { CodexSubagentAdapter } from "./adapters/codex.js";
|
|
4
|
+
import { extractOpenCodeFinalResponse, OpencodeSubagentAdapter } from "./adapters/opencode.js";
|
|
5
|
+
import { extractPiFinalResponse, PiRpcSubagentAdapter, } from "./adapters/pi.js";
|
|
6
|
+
export async function runSubagentProvider(provider, input) {
|
|
7
|
+
return createSubagentProviderAdapter(provider).run(input);
|
|
8
|
+
}
|
|
9
|
+
export function createSubagentProviderAdapter(provider) {
|
|
10
|
+
switch (provider) {
|
|
11
|
+
case "codex":
|
|
12
|
+
return new CodexSubagentAdapter();
|
|
13
|
+
case "claude":
|
|
14
|
+
return new ClaudeSubagentAdapter();
|
|
15
|
+
case "opencode":
|
|
16
|
+
return new OpencodeSubagentAdapter();
|
|
17
|
+
case "pi":
|
|
18
|
+
return new PiRpcSubagentAdapter();
|
|
19
|
+
case "cursor":
|
|
20
|
+
case "copilot":
|
|
21
|
+
return new AcpSubagentAdapter(provider);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function extractSubagentResponseText(value) {
|
|
25
|
+
return extractOpenCodeFinalResponse(value) || extractPiFinalResponse(value);
|
|
26
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function directString(value) {
|
|
2
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
3
|
+
}
|
|
4
|
+
export function assertPipedChild(child) {
|
|
5
|
+
if (!child.stdin || !child.stdout || !child.stderr) {
|
|
6
|
+
throw new Error("Agent process did not expose stdio pipes.");
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function unwrapProviderPayload(value) {
|
|
10
|
+
const record = asRecord(value);
|
|
11
|
+
if (!record)
|
|
12
|
+
return value;
|
|
13
|
+
return record.data ?? record.result ?? value;
|
|
14
|
+
}
|
|
15
|
+
export function readArray(record, key) {
|
|
16
|
+
const value = asRecord(record)?.[key];
|
|
17
|
+
return Array.isArray(value) ? value : undefined;
|
|
18
|
+
}
|
|
19
|
+
export function asRecord(value) {
|
|
20
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
21
|
+
return undefined;
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
export function readNestedString(value, path) {
|
|
25
|
+
let current = value;
|
|
26
|
+
for (const key of path) {
|
|
27
|
+
current = asRecord(current)?.[key];
|
|
28
|
+
}
|
|
29
|
+
return typeof current === "string" ? current : undefined;
|
|
30
|
+
}
|
|
31
|
+
export function errorMessage(error) {
|
|
32
|
+
return error instanceof Error ? error.message : String(error);
|
|
33
|
+
}
|
|
34
|
+
export function requireFinalResponse(provider, response) {
|
|
35
|
+
const trimmed = response.trim();
|
|
36
|
+
if (!trimmed) {
|
|
37
|
+
throw new Error(`${provider} did not return a final assistant response.`);
|
|
38
|
+
}
|
|
39
|
+
return trimmed;
|
|
40
|
+
}
|