@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
package/dist/server.js
CHANGED
|
@@ -54,8 +54,10 @@ import { shutdownHttpServer } from "./server-shutdown.js";
|
|
|
54
54
|
import { formatPathForPrompt } from "./skills.js";
|
|
55
55
|
import { createWorkspaceStore } from "./workspace-store.js";
|
|
56
56
|
import { formatAgentsPath, WorkspaceRegistry } from "./workspaces.js";
|
|
57
|
-
import {
|
|
58
|
-
import {
|
|
57
|
+
import { formatAvailableSubagentProfile, summarizeSubagentProfile } from "./subagents/profiles.js";
|
|
58
|
+
import { formatSubagentProviderAvailabilitySummary, formatUnavailableSubagentProvider, getSubagentProviderAvailabilitySnapshot, } from "./subagents/providers/availability.js";
|
|
59
|
+
import { capabilityActivityAuditRequest, capabilityActivityAuditResult } from "./subagents/sessions/mcp/audit.js";
|
|
60
|
+
import { createSubagentMcpRuntime } from "./subagents/sessions/mcp/runtime.js";
|
|
59
61
|
// Legacy MCP Streamable HTTP clients can reconnect without closing the previous
|
|
60
62
|
// transport. Bound stale transport-session retention so abandoned transports do
|
|
61
63
|
// not accumulate for the life of the process.
|
|
@@ -131,17 +133,6 @@ async function assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, wor
|
|
|
131
133
|
"Apply these instructions, then retry this tool call. No mutation or command was executed.",
|
|
132
134
|
].join("\n"));
|
|
133
135
|
}
|
|
134
|
-
function formatVisibleAgent(agent) {
|
|
135
|
-
const model = agent.model ? `, model ${agent.model}` : "";
|
|
136
|
-
const thinking = agent.thinking ? `, thinking ${agent.thinking}` : "";
|
|
137
|
-
const availability = agent.providerAvailable === false
|
|
138
|
-
? `, unavailable: ${agent.providerUnavailableReason ?? "provider unavailable"}`
|
|
139
|
-
: "";
|
|
140
|
-
return `${agent.name} (${agent.provider}${model}${thinking}${availability})`;
|
|
141
|
-
}
|
|
142
|
-
function formatUnavailableAgentProvider(provider) {
|
|
143
|
-
return `${provider.name} (${provider.reason ?? "unavailable"})`;
|
|
144
|
-
}
|
|
145
136
|
function resultOutputSchema(extra = {}) {
|
|
146
137
|
return {
|
|
147
138
|
result: z
|
|
@@ -228,9 +219,10 @@ const workspaceLocalAgentOutputSchema = z.object({
|
|
|
228
219
|
providerAvailable: z.boolean().optional(),
|
|
229
220
|
providerUnavailableReason: z.string().optional(),
|
|
230
221
|
});
|
|
231
|
-
const
|
|
222
|
+
const workspaceSubagentProviderOutputSchema = z.object({
|
|
232
223
|
name: z.string(),
|
|
233
224
|
available: z.boolean(),
|
|
225
|
+
continuationSupported: z.boolean(),
|
|
234
226
|
reason: z.string().optional(),
|
|
235
227
|
});
|
|
236
228
|
const workspaceAvailableAgentsFileOutputSchema = z.object({
|
|
@@ -999,7 +991,7 @@ function activityRequestFor(input, context) {
|
|
|
999
991
|
member: context.activityMember,
|
|
1000
992
|
};
|
|
1001
993
|
}
|
|
1002
|
-
function runActivityTool(lifecycle, workspace, conversationScopeId, tool, request, operation, outcome = standardActivityOutcome, relation = {}) {
|
|
994
|
+
function runActivityTool(lifecycle, workspace, conversationScopeId, tool, request, operation, outcome = standardActivityOutcome, relation = {}, auditResult) {
|
|
1003
995
|
return lifecycle.run({
|
|
1004
996
|
tool,
|
|
1005
997
|
workspace: workspaceActivitySnapshot(workspace),
|
|
@@ -1007,6 +999,7 @@ function runActivityTool(lifecycle, workspace, conversationScopeId, tool, reques
|
|
|
1007
999
|
request,
|
|
1008
1000
|
operation,
|
|
1009
1001
|
outcome,
|
|
1002
|
+
...(auditResult ? { auditResult } : {}),
|
|
1010
1003
|
...relation,
|
|
1011
1004
|
});
|
|
1012
1005
|
}
|
|
@@ -1195,7 +1188,7 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
|
|
|
1195
1188
|
}).then((result) => routing.present(result, target));
|
|
1196
1189
|
});
|
|
1197
1190
|
}
|
|
1198
|
-
export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions,
|
|
1191
|
+
export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, subagentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore, activityQueries, options = {}) {
|
|
1199
1192
|
const connectionScopeId = `mcp-connection:${randomUUID()}`;
|
|
1200
1193
|
const remoteWorkspaces = new RemoteWorkspaceRelay(config.configDir, config.stateDir);
|
|
1201
1194
|
const compositeWorkspaces = new CompositeWorkspaceRegistry(config.stateDir);
|
|
@@ -1224,10 +1217,12 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1224
1217
|
memberName: member.name,
|
|
1225
1218
|
};
|
|
1226
1219
|
};
|
|
1220
|
+
const subagentMcp = createSubagentMcpRuntime(config, activityLifecycle, options);
|
|
1227
1221
|
const presentExecutionResult = (result, target) => {
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1222
|
+
const presented = !target.compositeWorkspaceId || !target.memberName
|
|
1223
|
+
? result
|
|
1224
|
+
: remapCompositeToolResult(result, target.executionWorkspaceId, target.compositeWorkspaceId, target.memberName);
|
|
1225
|
+
return subagentMcp.decorateResult(target.executionWorkspaceId, presented);
|
|
1231
1226
|
};
|
|
1232
1227
|
const hostScopeIdFor = (requestMeta, transportSessionId) => hostConversationScopeId(requestMeta, transportSessionId, connectionScopeId);
|
|
1233
1228
|
const prepareExecutionContext = async (target, requestMeta, signal, sessionId) => {
|
|
@@ -1252,6 +1247,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1252
1247
|
const batchExecuteAvailable = config.toolMode !== "codex";
|
|
1253
1248
|
const capabilityRegistry = createCapabilityRegistry({
|
|
1254
1249
|
inspectHooks: (workspaceRoot) => checkHookConfiguration(workspaceRoot, config.hooks),
|
|
1250
|
+
...subagentMcp.registryDependencies,
|
|
1255
1251
|
batchExecute: {
|
|
1256
1252
|
available: batchExecuteAvailable,
|
|
1257
1253
|
unavailableReason: batchExecuteAvailable
|
|
@@ -1804,7 +1800,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1804
1800
|
const { workspaceId, name, arguments: capabilityArguments, file } = input;
|
|
1805
1801
|
const workspace = workspaces.getWorkspace(workspaceId);
|
|
1806
1802
|
let changedPaths = [];
|
|
1807
|
-
return
|
|
1803
|
+
return runActivityTool(activityLifecycle, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), toolNames.capability, activityRequestFor(capabilityActivityAuditRequest(input), context), (activityContext) => runToolWithHooks(hooks, {
|
|
1808
1804
|
signal: context.signal,
|
|
1809
1805
|
tool: toolNames.capability,
|
|
1810
1806
|
invocation: workspaceHookInvocation(workspace),
|
|
@@ -1820,6 +1816,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1820
1816
|
requestMeta: context.requestMeta,
|
|
1821
1817
|
sessionId: context.sessionId,
|
|
1822
1818
|
batch: context.batch,
|
|
1819
|
+
activityId: activityContext.activityId,
|
|
1823
1820
|
});
|
|
1824
1821
|
changedPaths = execution.changedPaths ?? [];
|
|
1825
1822
|
const result = {
|
|
@@ -1872,7 +1869,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1872
1869
|
return result;
|
|
1873
1870
|
}
|
|
1874
1871
|
},
|
|
1875
|
-
}, activityRelationFor(context));
|
|
1872
|
+
}), standardActivityOutcome, activityRelationFor(context), (result) => capabilityActivityAuditResult(name, result));
|
|
1876
1873
|
},
|
|
1877
1874
|
});
|
|
1878
1875
|
batchExecutor = new BatchExecutor({
|
|
@@ -2108,7 +2105,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2108
2105
|
agentsFiles: z.array(workspaceAgentsFileOutputSchema).optional(),
|
|
2109
2106
|
availableAgentsFiles: z.array(workspaceAvailableAgentsFileOutputSchema).optional(),
|
|
2110
2107
|
skills: z.array(workspaceSkillOutputSchema).optional(),
|
|
2111
|
-
agentProviders: z.array(
|
|
2108
|
+
agentProviders: z.array(workspaceSubagentProviderOutputSchema).optional(),
|
|
2112
2109
|
agents: z.array(workspaceLocalAgentOutputSchema).optional(),
|
|
2113
2110
|
skillDiagnostics: z.array(workspaceSkillDiagnosticOutputSchema).optional(),
|
|
2114
2111
|
workspaces: z.array(workspaceInventoryEntryOutputSchema).optional(),
|
|
@@ -2546,9 +2543,9 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2546
2543
|
path: formatPathForPrompt(guide.filePath),
|
|
2547
2544
|
}));
|
|
2548
2545
|
const capabilityCatalog = capabilityRegistry.catalog(capabilityContextFor(workspace));
|
|
2549
|
-
const cardAgentProviders = config.subagents ?
|
|
2546
|
+
const cardAgentProviders = config.subagents ? subagentProviders : [];
|
|
2550
2547
|
const cardAgents = workspace.agentProfiles.map((profile) => {
|
|
2551
|
-
const summary =
|
|
2548
|
+
const summary = summarizeSubagentProfile(profile);
|
|
2552
2549
|
const availability = cardAgentProviders.find((provider) => provider.name === summary.provider);
|
|
2553
2550
|
return {
|
|
2554
2551
|
...summary,
|
|
@@ -2626,10 +2623,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2626
2623
|
? `Available subagent providers: ${visibleAgentProviders.filter((provider) => provider.available).map((provider) => provider.name).join(", ")}`
|
|
2627
2624
|
: undefined,
|
|
2628
2625
|
visibleAgentProviders.some((provider) => !provider.available)
|
|
2629
|
-
? `Unavailable subagent providers: ${visibleAgentProviders.filter((provider) => !provider.available).map(
|
|
2626
|
+
? `Unavailable subagent providers: ${visibleAgentProviders.filter((provider) => !provider.available).map(formatUnavailableSubagentProvider).join(", ")}`
|
|
2630
2627
|
: undefined,
|
|
2631
2628
|
visibleAgents.length > 0
|
|
2632
|
-
? `Available subagent profiles: ${visibleAgents.map(
|
|
2629
|
+
? `Available subagent profiles: ${visibleAgents.map(formatAvailableSubagentProfile).join(", ")}`
|
|
2633
2630
|
: undefined,
|
|
2634
2631
|
knownWorktrees.length > 0
|
|
2635
2632
|
? `Known worktrees: ${knownWorktrees.map((worktree) => `${worktree.path} [${worktree.workspaceId}]${worktree.branch ? ` branch=${worktree.branch}` : ""}${worktree.targetBranch ? ` target=${worktree.targetBranch}` : ""}${worktree.current ? " (current)" : ""}`).join(", ")}`
|
|
@@ -3673,8 +3670,8 @@ export function createServer(config = loadConfig(), options = {}) {
|
|
|
3673
3670
|
const reviewCheckpoints = createReviewCheckpointManager();
|
|
3674
3671
|
const processSessions = new ProcessManager({ outputAudit: bashOutputStore });
|
|
3675
3672
|
const codeIntelligence = new CodeIntelligenceManager(config);
|
|
3676
|
-
const
|
|
3677
|
-
?
|
|
3673
|
+
const subagentProviders = config.subagents
|
|
3674
|
+
? getSubagentProviderAvailabilitySnapshot()
|
|
3678
3675
|
: [];
|
|
3679
3676
|
const logTransportCloseResults = (reason, results) => {
|
|
3680
3677
|
let closedCount = 0;
|
|
@@ -3857,7 +3854,7 @@ export function createServer(config = loadConfig(), options = {}) {
|
|
|
3857
3854
|
});
|
|
3858
3855
|
}
|
|
3859
3856
|
};
|
|
3860
|
-
const server = createMcpServer(config, workspaces, reviewCheckpoints, processSessions,
|
|
3857
|
+
const server = createMcpServer(config, workspaces, reviewCheckpoints, processSessions, subagentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore, activityQueries);
|
|
3861
3858
|
await server.connect(transport);
|
|
3862
3859
|
}
|
|
3863
3860
|
else {
|
|
@@ -3880,7 +3877,7 @@ export function createServer(config = loadConfig(), options = {}) {
|
|
|
3880
3877
|
return {
|
|
3881
3878
|
app,
|
|
3882
3879
|
config,
|
|
3883
|
-
|
|
3880
|
+
subagentProviders,
|
|
3884
3881
|
close: () => {
|
|
3885
3882
|
closePromise ??= (async () => {
|
|
3886
3883
|
clearInterval(transportCleanupTimer);
|
|
@@ -3906,7 +3903,7 @@ async function isMainModule() {
|
|
|
3906
3903
|
return modulePath === entrypointPath;
|
|
3907
3904
|
}
|
|
3908
3905
|
if (await isMainModule()) {
|
|
3909
|
-
const { app, config, close,
|
|
3906
|
+
const { app, config, close, subagentProviders } = createServer();
|
|
3910
3907
|
const httpServer = app.listen(config.port, config.host, () => {
|
|
3911
3908
|
console.log(`forgerelay listening on http://${config.host}:${config.port}/mcp`);
|
|
3912
3909
|
console.log(`allowed roots: ${config.allowedRoots.join(", ")}`);
|
|
@@ -3922,7 +3919,7 @@ if (await isMainModule()) {
|
|
|
3922
3919
|
: `unsupported on ${process.platform}`;
|
|
3923
3920
|
console.log(`native artifact download: ${artifactDownloadStatus}`);
|
|
3924
3921
|
if (config.subagents) {
|
|
3925
|
-
console.log(`subagent providers: ${
|
|
3922
|
+
console.log(`subagent providers: ${formatSubagentProviderAvailabilitySummary(subagentProviders)}`);
|
|
3926
3923
|
}
|
|
3927
3924
|
});
|
|
3928
3925
|
let shuttingDown = false;
|
|
@@ -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");
|