@akira-tl/forgerelay 0.6.1 → 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 +43 -0
- package/README.md +38 -0
- package/capabilities/subagents/GUIDE.md +100 -40
- package/dist/activity/lifecycle.js +1 -1
- package/dist/activity/mcp-query-tools.js +1 -0
- package/dist/activity/query-service.js +1 -0
- package/dist/capabilities.js +1 -1
- package/dist/capability-registry.js +34 -0
- package/dist/cli.js +80 -165
- package/dist/composite-activity.js +155 -0
- package/dist/composite-workspaces.js +197 -0
- package/dist/db/migrations.js +14 -0
- package/dist/db/schema.js +6 -0
- package/dist/remote-workspace-relay.js +16 -0
- package/dist/server.js +698 -172
- 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/ui/.vite/manifest.json +33 -33
- package/dist/ui/activity-panel-app.html +3 -3
- package/dist/ui/assets/{activity-panel-app-CjZVvVNc.js → activity-panel-app-E1ju2dqI.js} +1 -1
- package/dist/ui/assets/{heavy-payload-vGgBRvNX.js → heavy-payload-CeW-n9w5.js} +1 -1
- package/dist/ui/assets/{review-payload-4erWKckt.js → review-payload-B9CO298v.js} +1 -1
- package/dist/ui/assets/{scrollbar-CaOPzUJd.js → scrollbar-C2twAENW.js} +1 -1
- package/dist/ui/assets/workspace-app-BztEvZIC.js +5 -0
- package/dist/ui/assets/{workspace-app-DkAiSl_0.js → workspace-app-CwbJnb_w.js} +1 -1
- package/dist/ui/assets/workspace-app-YnUST8IP.css +1 -0
- package/dist/ui/assets/workspace-app-rKuhdae8.js +1 -0
- package/dist/ui/assets/workspace-lifecycle-app-CEfMdudP.js +1 -0
- package/dist/ui/workspace-app.html +4 -4
- package/dist/ui/workspace-lifecycle-app.html +4 -4
- package/dist/workspaces.js +3 -3
- package/docs/chatgpt-coding-workflow.md +2 -9
- package/docs/configuration.md +23 -0
- package/docs/debugging.md +7 -0
- package/docs/roadmap.md +42 -7
- package/package.json +2 -2
- package/scripts/debug/runtime.mjs +23 -1
- package/scripts/debug/runtime.test.mjs +14 -2
- package/scripts/debug/serve.mjs +4 -4
- package/scripts/release/release-gate.test.mjs +2 -2
- package/dist/local-agent-adapters.js +0 -653
- package/dist/ui/assets/workspace-app-CcrHAUIn.css +0 -1
- package/dist/ui/assets/workspace-app-DJmkPYJC.js +0 -1
- package/dist/ui/assets/workspace-app-QyauBrJX.js +0 -5
- package/dist/ui/assets/workspace-lifecycle-app-BIXEo53I.js +0 -1
- /package/dist/{local-agent-path.js → subagents/providers/path.js} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export function
|
|
1
|
+
import { isSubagentProvider, SUBAGENT_PROVIDERS, } from "./profiles.js";
|
|
2
|
+
export function parseSubagentRunArgs(args) {
|
|
3
3
|
const [target, ...rest] = args;
|
|
4
4
|
if (!target) {
|
|
5
5
|
throw new Error('Usage: forgerelay agents run <profile-or-provider-or-id> [--model <model>] [--thinking <level>] "<prompt>"');
|
|
@@ -47,7 +47,7 @@ export function parseLocalAgentRunArgs(args) {
|
|
|
47
47
|
}
|
|
48
48
|
return { target, prompt, model, thinking };
|
|
49
49
|
}
|
|
50
|
-
export function
|
|
50
|
+
export function resolveSubagentTarget(target, profiles, modelOverride, thinkingOverride) {
|
|
51
51
|
const profile = profiles.find((candidate) => candidate.name === target);
|
|
52
52
|
if (profile) {
|
|
53
53
|
return {
|
|
@@ -59,7 +59,7 @@ export function resolveLocalAgentTarget(target, profiles, modelOverride, thinkin
|
|
|
59
59
|
profile,
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
-
if (
|
|
62
|
+
if (isSubagentProvider(target)) {
|
|
63
63
|
return {
|
|
64
64
|
kind: "provider",
|
|
65
65
|
name: target,
|
|
@@ -70,11 +70,11 @@ export function resolveLocalAgentTarget(target, profiles, modelOverride, thinkin
|
|
|
70
70
|
}
|
|
71
71
|
return undefined;
|
|
72
72
|
}
|
|
73
|
-
export function
|
|
73
|
+
export function formatAvailableSubagentTargets(profiles) {
|
|
74
74
|
const profileNames = profiles.map((profile) => profile.name);
|
|
75
75
|
const parts = [
|
|
76
76
|
profileNames.length > 0 ? `profiles: ${profileNames.join(", ")}` : undefined,
|
|
77
|
-
`providers: ${
|
|
77
|
+
`providers: ${SUBAGENT_PROVIDERS.join(", ")}`,
|
|
78
78
|
].filter(Boolean);
|
|
79
79
|
return parts.join("; ");
|
|
80
80
|
}
|
|
@@ -2,7 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { readdir, readFile } from "node:fs/promises";
|
|
3
3
|
import { basename, join, resolve } from "node:path";
|
|
4
4
|
import { parse as parseYaml } from "yaml";
|
|
5
|
-
export const
|
|
5
|
+
export const SUBAGENT_PROVIDERS = [
|
|
6
6
|
"codex",
|
|
7
7
|
"claude",
|
|
8
8
|
"opencode",
|
|
@@ -11,8 +11,8 @@ export const LOCAL_AGENT_PROVIDERS = [
|
|
|
11
11
|
"copilot",
|
|
12
12
|
];
|
|
13
13
|
const FRONTMATTER_DELIMITER = "---";
|
|
14
|
-
const PROVIDERS = new Set(
|
|
15
|
-
export async function
|
|
14
|
+
const PROVIDERS = new Set(SUBAGENT_PROVIDERS);
|
|
15
|
+
export async function loadSubagentProfiles(config, workspaceRoot) {
|
|
16
16
|
if (!config.subagents)
|
|
17
17
|
return [];
|
|
18
18
|
const profileDirs = [
|
|
@@ -30,7 +30,7 @@ export async function loadLocalAgentProfiles(config, workspaceRoot) {
|
|
|
30
30
|
.filter((profile) => !profile.disabled)
|
|
31
31
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
32
32
|
}
|
|
33
|
-
export function
|
|
33
|
+
export function summarizeSubagentProfile(profile) {
|
|
34
34
|
return {
|
|
35
35
|
name: profile.name,
|
|
36
36
|
description: profile.description,
|
|
@@ -39,6 +39,14 @@ export function summarizeLocalAgentProfile(profile) {
|
|
|
39
39
|
thinking: profile.thinking,
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
+
export function formatAvailableSubagentProfile(profile) {
|
|
43
|
+
const model = profile.model ? `, model ${profile.model}` : "";
|
|
44
|
+
const thinking = profile.thinking ? `, thinking ${profile.thinking}` : "";
|
|
45
|
+
const availability = profile.providerAvailable === false
|
|
46
|
+
? `, unavailable: ${profile.providerUnavailableReason ?? "provider unavailable"}`
|
|
47
|
+
: "";
|
|
48
|
+
return `${profile.name} (${profile.provider}${model}${thinking}${availability})`;
|
|
49
|
+
}
|
|
42
50
|
async function loadProfilesFromDirectory(directory) {
|
|
43
51
|
const resolvedDirectory = resolve(directory);
|
|
44
52
|
if (!existsSync(resolvedDirectory))
|
|
@@ -121,7 +129,7 @@ function readProvider(frontmatter, filePath) {
|
|
|
121
129
|
}
|
|
122
130
|
return provider;
|
|
123
131
|
}
|
|
124
|
-
export function
|
|
132
|
+
export function isSubagentProvider(value) {
|
|
125
133
|
return PROVIDERS.has(value);
|
|
126
134
|
}
|
|
127
135
|
function readString(frontmatter, key) {
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { Readable, Writable } from "node:stream";
|
|
3
|
+
import { asRecord, assertPipedChild, directString, errorMessage, readArray, } from "../shared.js";
|
|
4
|
+
const ACP_COMMANDS = {
|
|
5
|
+
cursor: ["cursor-agent", "acp"],
|
|
6
|
+
copilot: ["copilot", "--acp"],
|
|
7
|
+
};
|
|
8
|
+
export class AcpSubagentAdapter {
|
|
9
|
+
provider;
|
|
10
|
+
command;
|
|
11
|
+
constructor(provider, command = ACP_COMMANDS[provider]) {
|
|
12
|
+
this.provider = provider;
|
|
13
|
+
this.command = command;
|
|
14
|
+
}
|
|
15
|
+
async run(input) {
|
|
16
|
+
const { client } = await import("@agentclientprotocol/sdk");
|
|
17
|
+
const { methods } = await import("@agentclientprotocol/sdk");
|
|
18
|
+
const { ndJsonStream } = await import("@agentclientprotocol/sdk");
|
|
19
|
+
const [command, ...args] = this.command;
|
|
20
|
+
const child = spawn(command, args, {
|
|
21
|
+
cwd: input.workspace,
|
|
22
|
+
env: process.env,
|
|
23
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
24
|
+
windowsHide: true,
|
|
25
|
+
});
|
|
26
|
+
assertPipedChild(child);
|
|
27
|
+
let stderr = "";
|
|
28
|
+
child.stderr.on("data", (chunk) => {
|
|
29
|
+
stderr += chunk.toString("utf8");
|
|
30
|
+
});
|
|
31
|
+
const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
|
|
32
|
+
try {
|
|
33
|
+
let providerSessionId = input.providerSessionId ?? null;
|
|
34
|
+
const finalResponse = await client({ name: "ForgeRelay" })
|
|
35
|
+
.onRequest(methods.client.session.requestPermission, (context) => {
|
|
36
|
+
const selected = selectAcpAllowPermissionOption(context.params.options);
|
|
37
|
+
return selected
|
|
38
|
+
? { outcome: { outcome: "selected", optionId: selected.optionId } }
|
|
39
|
+
: { outcome: { outcome: "cancelled" } };
|
|
40
|
+
})
|
|
41
|
+
.connectWith(stream, async (context) => {
|
|
42
|
+
const session = await context.buildSession(input.workspace).start();
|
|
43
|
+
providerSessionId = session.sessionId;
|
|
44
|
+
try {
|
|
45
|
+
if (input.model) {
|
|
46
|
+
const config = resolveAcpModelConfigUpdate(session, input.model, this.provider);
|
|
47
|
+
await context.request(methods.agent.session.setConfigOption, config);
|
|
48
|
+
}
|
|
49
|
+
if (input.thinking) {
|
|
50
|
+
const config = resolveAcpThinkingConfigUpdate(session, input.thinking, this.provider);
|
|
51
|
+
await context.request(methods.agent.session.setConfigOption, config);
|
|
52
|
+
}
|
|
53
|
+
const prompt = session.prompt(input.prompt);
|
|
54
|
+
const textParts = [];
|
|
55
|
+
for (;;) {
|
|
56
|
+
const message = await session.nextUpdate();
|
|
57
|
+
if (message.kind === "stop") {
|
|
58
|
+
await prompt;
|
|
59
|
+
return textParts.join("").trim();
|
|
60
|
+
}
|
|
61
|
+
const update = message.update;
|
|
62
|
+
if (update.sessionUpdate !== "agent_message_chunk")
|
|
63
|
+
continue;
|
|
64
|
+
const content = update.content;
|
|
65
|
+
if (content.type === "text")
|
|
66
|
+
textParts.push(content.text);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
session.dispose();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
provider: this.provider,
|
|
75
|
+
providerSessionId,
|
|
76
|
+
finalResponse: finalResponse.trim(),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
throw new Error(`${this.provider} ACP run failed: ${errorMessage(error)}${stderr ? `\n${stderr.trim()}` : ""}`);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
child.kill();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export function resolveAcpModelConfigUpdate(session, model, provider) {
|
|
88
|
+
return resolveAcpSelectConfigUpdate(session, {
|
|
89
|
+
category: "model",
|
|
90
|
+
label: "model",
|
|
91
|
+
provider,
|
|
92
|
+
value: model,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
export function resolveAcpThinkingConfigUpdate(session, thinking, provider) {
|
|
96
|
+
return resolveAcpSelectConfigUpdate(session, {
|
|
97
|
+
category: "thought_level",
|
|
98
|
+
label: "thinking option",
|
|
99
|
+
provider,
|
|
100
|
+
value: thinking,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function resolveAcpSelectConfigUpdate(session, options) {
|
|
104
|
+
const record = asRecord(session);
|
|
105
|
+
if (!record)
|
|
106
|
+
throw new Error(`${options.provider} ACP session did not return session metadata.`);
|
|
107
|
+
const sessionId = typeof record.sessionId === "string" ? record.sessionId : undefined;
|
|
108
|
+
if (!sessionId)
|
|
109
|
+
throw new Error(`${options.provider} ACP session did not return a session id.`);
|
|
110
|
+
const response = asRecord(record.newSessionResponse);
|
|
111
|
+
const configOptions = response ? readArray(response, "configOptions") ?? [] : [];
|
|
112
|
+
const config = configOptions
|
|
113
|
+
.map(asRecord)
|
|
114
|
+
.find((option) => option?.type === "select" && option.category === options.category);
|
|
115
|
+
if (!config) {
|
|
116
|
+
throw new Error(`${options.provider} ACP server does not expose a ${options.label}.`);
|
|
117
|
+
}
|
|
118
|
+
const configId = directString(config.id);
|
|
119
|
+
if (!configId)
|
|
120
|
+
throw new Error(`${options.provider} ACP ${options.label} is missing an id.`);
|
|
121
|
+
const available = flattenAcpSelectValues(config);
|
|
122
|
+
if (!available.includes(options.value)) {
|
|
123
|
+
const suffix = available.length > 0 ? ` Available values: ${available.join(", ")}.` : "";
|
|
124
|
+
throw new Error(`${options.provider} ACP ${options.label} does not support '${options.value}'.${suffix}`);
|
|
125
|
+
}
|
|
126
|
+
return { sessionId, configId, value: options.value };
|
|
127
|
+
}
|
|
128
|
+
function flattenAcpSelectValues(option) {
|
|
129
|
+
const values = [];
|
|
130
|
+
for (const item of readArray(option, "options") ?? []) {
|
|
131
|
+
const record = asRecord(item);
|
|
132
|
+
const value = directString(record?.value);
|
|
133
|
+
if (value) {
|
|
134
|
+
values.push(value);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
for (const nested of readArray(record, "options") ?? []) {
|
|
138
|
+
const nestedValue = directString(asRecord(nested)?.value);
|
|
139
|
+
if (nestedValue)
|
|
140
|
+
values.push(nestedValue);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return values;
|
|
144
|
+
}
|
|
145
|
+
function selectAcpAllowPermissionOption(options) {
|
|
146
|
+
return (options.find((option) => option.kind === "allow_once") ??
|
|
147
|
+
options.find((option) => option.kind === "allow_always"));
|
|
148
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { directString, requireFinalResponse } from "../shared.js";
|
|
3
|
+
export class ClaudeSubagentAdapter {
|
|
4
|
+
provider = "claude";
|
|
5
|
+
async run(input) {
|
|
6
|
+
const { query } = await import("@anthropic-ai/claude-agent-sdk");
|
|
7
|
+
const claudeExecutable = process.env.CLAUDE_COMMAND ?? resolveExecutable("claude");
|
|
8
|
+
const messages = query({
|
|
9
|
+
prompt: input.prompt,
|
|
10
|
+
options: {
|
|
11
|
+
cwd: input.workspace,
|
|
12
|
+
model: input.model,
|
|
13
|
+
...(input.thinking ? { thinking: { type: "adaptive" }, effort: input.thinking } : {}),
|
|
14
|
+
resume: input.providerSessionId,
|
|
15
|
+
permissionMode: "bypassPermissions",
|
|
16
|
+
allowDangerouslySkipPermissions: true,
|
|
17
|
+
env: claudeCommandEnvironment(process.env),
|
|
18
|
+
...(claudeExecutable ? { pathToClaudeCodeExecutable: claudeExecutable } : {}),
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
let providerSessionId = input.providerSessionId ?? null;
|
|
22
|
+
let finalResponse = "";
|
|
23
|
+
for await (const message of messages) {
|
|
24
|
+
const record = message;
|
|
25
|
+
if (typeof record.session_id === "string")
|
|
26
|
+
providerSessionId = record.session_id;
|
|
27
|
+
if (record.type === "result" && typeof record.result === "string") {
|
|
28
|
+
const resultError = claudeResultError(record);
|
|
29
|
+
if (resultError)
|
|
30
|
+
throw new Error(resultError);
|
|
31
|
+
finalResponse = record.result;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
finalResponse = requireFinalResponse("Claude", finalResponse);
|
|
35
|
+
return {
|
|
36
|
+
provider: this.provider,
|
|
37
|
+
providerSessionId,
|
|
38
|
+
finalResponse,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function claudeResultError(record) {
|
|
43
|
+
const subtype = typeof record.subtype === "string" ? record.subtype : undefined;
|
|
44
|
+
const isError = record.is_error === true || subtype?.startsWith("error");
|
|
45
|
+
if (!isError)
|
|
46
|
+
return undefined;
|
|
47
|
+
const message = directString(record.error) ??
|
|
48
|
+
directString(record.message) ??
|
|
49
|
+
directString(record.result) ??
|
|
50
|
+
subtype ??
|
|
51
|
+
"Claude returned an error result.";
|
|
52
|
+
return `Claude returned an error result: ${message}`;
|
|
53
|
+
}
|
|
54
|
+
function resolveExecutable(command) {
|
|
55
|
+
const result = spawnSync(process.platform === "win32" ? "where.exe" : "command", [
|
|
56
|
+
...(process.platform === "win32" ? [command] : ["-v", command]),
|
|
57
|
+
], {
|
|
58
|
+
encoding: "utf8",
|
|
59
|
+
shell: process.platform !== "win32",
|
|
60
|
+
});
|
|
61
|
+
const executable = result.stdout?.split(/\r?\n/).find((line) => line.trim());
|
|
62
|
+
return executable?.trim() || undefined;
|
|
63
|
+
}
|
|
64
|
+
export function claudeCommandEnvironment(env) {
|
|
65
|
+
const next = { ...env };
|
|
66
|
+
for (const key of [
|
|
67
|
+
"CLAUDECODE",
|
|
68
|
+
"CLAUDE_CODE_ENTRYPOINT",
|
|
69
|
+
"CLAUDE_CODE_SSE_PORT",
|
|
70
|
+
"CLAUDE_AGENT_SDK_VERSION",
|
|
71
|
+
]) {
|
|
72
|
+
delete next[key];
|
|
73
|
+
}
|
|
74
|
+
return next;
|
|
75
|
+
}
|
|
@@ -18,7 +18,7 @@ function threadOptionsFor(input) {
|
|
|
18
18
|
modelReasoningEffort: input.thinking,
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
|
-
export class
|
|
21
|
+
export class CodexSdkSubagentRuntime {
|
|
22
22
|
provider = "codex";
|
|
23
23
|
codex;
|
|
24
24
|
constructor(codex) {
|
|
@@ -34,13 +34,19 @@ export class CodexSdkLocalAgentRuntime {
|
|
|
34
34
|
provider: this.provider,
|
|
35
35
|
providerSessionId: thread.id,
|
|
36
36
|
finalResponse: turn.finalResponse,
|
|
37
|
-
items: turn.items,
|
|
38
37
|
};
|
|
39
38
|
}
|
|
40
39
|
}
|
|
41
|
-
export async function
|
|
40
|
+
export async function createCodexSdkSubagentRuntime(options, codexFactory) {
|
|
42
41
|
const factory = codexFactory ?? (await defaultCodexFactory());
|
|
43
|
-
return new
|
|
42
|
+
return new CodexSdkSubagentRuntime(factory(options));
|
|
43
|
+
}
|
|
44
|
+
export class CodexSubagentAdapter {
|
|
45
|
+
provider = "codex";
|
|
46
|
+
async run(input) {
|
|
47
|
+
const runtime = await createCodexSdkSubagentRuntime();
|
|
48
|
+
return runtime.run(input);
|
|
49
|
+
}
|
|
44
50
|
}
|
|
45
51
|
async function defaultCodexFactory() {
|
|
46
52
|
const module = await import("@openai/codex-sdk");
|
|
@@ -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
|
+
}
|