@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.
Files changed (35) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/capabilities/subagents/GUIDE.md +100 -40
  3. package/dist/activity/lifecycle.js +1 -1
  4. package/dist/capabilities.js +1 -1
  5. package/dist/capability-registry.js +34 -0
  6. package/dist/cli.js +80 -165
  7. package/dist/db/migrations.js +14 -0
  8. package/dist/db/schema.js +6 -0
  9. package/dist/server.js +29 -32
  10. package/dist/{local-agent-targets.js → subagents/cli-target.js} +6 -6
  11. package/dist/{local-agent-profiles.js → subagents/profiles.js} +13 -5
  12. package/dist/subagents/providers/adapters/acp.js +148 -0
  13. package/dist/subagents/providers/adapters/claude.js +75 -0
  14. package/dist/{local-agent-runtime.js → subagents/providers/adapters/codex.js} +10 -4
  15. package/dist/subagents/providers/adapters/opencode.js +137 -0
  16. package/dist/subagents/providers/adapters/pi.js +232 -0
  17. package/dist/{local-agent-availability.js → subagents/providers/availability.js} +24 -10
  18. package/dist/subagents/providers/continuation.js +11 -0
  19. package/dist/subagents/providers/contract.js +1 -0
  20. package/dist/subagents/providers/registry.js +26 -0
  21. package/dist/subagents/providers/shared.js +40 -0
  22. package/dist/subagents/sessions/capability.js +214 -0
  23. package/dist/subagents/sessions/delivery-mailbox.js +115 -0
  24. package/dist/subagents/sessions/execution.js +171 -0
  25. package/dist/subagents/sessions/manager.js +107 -0
  26. package/dist/subagents/sessions/mcp/audit.js +85 -0
  27. package/dist/subagents/sessions/mcp/runtime.js +19 -0
  28. package/dist/{local-agent-store.js → subagents/sessions/store.js} +75 -39
  29. package/dist/workspaces.js +3 -3
  30. package/docs/chatgpt-coding-workflow.md +2 -9
  31. package/docs/roadmap.md +42 -7
  32. package/package.json +2 -2
  33. package/scripts/release/release-gate.test.mjs +2 -2
  34. package/dist/local-agent-adapters.js +0 -653
  35. /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 { summarizeLocalAgentProfile } from "./local-agent-profiles.js";
58
- import { formatLocalAgentProviderAvailabilitySummary, getLocalAgentProviderAvailabilitySnapshot, } from "./local-agent-availability.js";
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 workspaceLocalAgentProviderOutputSchema = z.object({
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, localAgentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore, activityQueries) {
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
- if (!target.compositeWorkspaceId || !target.memberName)
1229
- return result;
1230
- return remapCompositeToolResult(result, target.executionWorkspaceId, target.compositeWorkspaceId, target.memberName);
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 runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), activityRequestFor({ workspaceId, name, action: "run", arguments: capabilityArguments, file }, context), {
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(workspaceLocalAgentProviderOutputSchema).optional(),
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 ? localAgentProviders : [];
2546
+ const cardAgentProviders = config.subagents ? subagentProviders : [];
2550
2547
  const cardAgents = workspace.agentProfiles.map((profile) => {
2551
- const summary = summarizeLocalAgentProfile(profile);
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(formatUnavailableAgentProvider).join(", ")}`
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(formatVisibleAgent).join(", ")}`
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 localAgentProviders = config.subagents
3677
- ? getLocalAgentProviderAvailabilitySnapshot()
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, localAgentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore, activityQueries);
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
- localAgentProviders,
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, localAgentProviders } = createServer();
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: ${formatLocalAgentProviderAvailabilitySummary(localAgentProviders)}`);
3922
+ console.log(`subagent providers: ${formatSubagentProviderAvailabilitySummary(subagentProviders)}`);
3926
3923
  }
3927
3924
  });
3928
3925
  let shuttingDown = false;
@@ -1,5 +1,5 @@
1
- import { isLocalAgentProvider, LOCAL_AGENT_PROVIDERS, } from "./local-agent-profiles.js";
2
- export function parseLocalAgentRunArgs(args) {
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 resolveLocalAgentTarget(target, profiles, modelOverride, thinkingOverride) {
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 (isLocalAgentProvider(target)) {
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 formatAvailableLocalAgentTargets(profiles) {
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: ${LOCAL_AGENT_PROVIDERS.join(", ")}`,
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 LOCAL_AGENT_PROVIDERS = [
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(LOCAL_AGENT_PROVIDERS);
15
- export async function loadLocalAgentProfiles(config, workspaceRoot) {
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 summarizeLocalAgentProfile(profile) {
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 isLocalAgentProvider(value) {
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 CodexSdkLocalAgentRuntime {
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 createCodexSdkLocalAgentRuntime(options, codexFactory) {
40
+ export async function createCodexSdkSubagentRuntime(options, codexFactory) {
42
41
  const factory = codexFactory ?? (await defaultCodexFactory());
43
- return new CodexSdkLocalAgentRuntime(factory(options));
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");