| author | |
| committer | |
| log | 818e2be75e1a68bbcac122eac1b6fb815ccf4b73 |
| tree | 2ca3b968e2fc761d81d21c600a80bf9e4bd852ca |
| parent | 9afde1c6dbe00d2b87829732c88789c10f3429c0 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>5 files changed, 402 insertions(+), 3 deletions(-)
apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts+202| ... | ... | @@ -1,4 +1,6 @@ |
| 1 | 1 | import { |
| 2 | extractCopilotSubagentUpdate, | |
| 3 | makeCopilotSubagentRouting, | |
| 2 | 4 | normalizeDevinSessionUpdate, |
| 3 | 5 | normalizeDevinToolCall, |
| 4 | 6 | extractDevinSubagentUpdate, |
| ... | ... | @@ -1464,6 +1466,206 @@ describe("AcpAdapterV2", () => { |
| 1464 | 1466 | }).pipe(Effect.provide(layerTest), Effect.scoped), |
| 1465 | 1467 | ); |
| 1466 | 1468 | |
| 1469 | it.effect("routes interleaved Copilot subagent output into subagent threads", () => | |
| 1470 | Effect.gen(function* () { | |
| 1471 | const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; | |
| 1472 | const path = yield* Path.Path; | |
| 1473 | const mockAgentPath = yield* path.fromFileUrl( | |
| 1474 | new URL("../../../scripts/acp-mock-agent.ts", import.meta.url), | |
| 1475 | ); | |
| 1476 | type Runtime = AcpSessionRuntime.AcpSessionRuntime["Service"]; | |
| 1477 | let handler: Parameters<Runtime["handleSessionUpdate"]>[0] | undefined; | |
| 1478 | let sessionEvent: | |
| 1479 | | ((event: unknown) => Effect.Effect<void, EffectAcpErrors.AcpError>) | |
| 1480 | | undefined; | |
| 1481 | const routing = makeCopilotSubagentRouting(); | |
| 1482 | const instanceId = ProviderInstanceId.make("copilot-replay"); | |
| 1483 | const adapter = yield* makeAcpAdapterV2({ | |
| 1484 | instanceId, | |
| 1485 | selfInvocation: yield* resolveSelfInvocation(), | |
| 1486 | flavor: { | |
| 1487 | driver: ACP_TEST_DRIVER, | |
| 1488 | capabilities: AcpProviderCapabilitiesV2, | |
| 1489 | registerExtensions: routing.registerExtensions, | |
| 1490 | normalizeSessionUpdate: routing.normalizeSessionUpdate, | |
| 1491 | extractSubagentUpdate: extractCopilotSubagentUpdate, | |
| 1492 | makeRuntime: makeMockRuntime({ | |
| 1493 | childProcessSpawner, | |
| 1494 | mockAgentPath, | |
| 1495 | wrapRuntime: (runtime) => ({ | |
| 1496 | ...runtime, | |
| 1497 | handleSessionUpdate: (next) => | |
| 1498 | Effect.sync(() => { | |
| 1499 | handler = next; | |
| 1500 | }).pipe(Effect.andThen(runtime.handleSessionUpdate(next))), | |
| 1501 | handleExtNotification: (method, payload, next) => | |
| 1502 | Effect.sync(() => { | |
| 1503 | if (method === "github.com/copilot/sessionEvent") { | |
| 1504 | sessionEvent = next as typeof sessionEvent; | |
| 1505 | } | |
| 1506 | }).pipe(Effect.andThen(runtime.handleExtNotification(method, payload, next))), | |
| 1507 | prompt: () => | |
| 1508 | Effect.gen(function* () { | |
| 1509 | assert.isDefined(handler); | |
| 1510 | assert.isDefined(sessionEvent); | |
| 1511 | const sessionId = "mock-session-1"; | |
| 1512 | const raw = (type: string, data: object, agentId?: string) => | |
| 1513 | sessionEvent!({ sessionId, type, data, ...(agentId ? { agentId } : {}) }); | |
| 1514 | const update = (next: EffectAcpSchema.SessionUpdate) => | |
| 1515 | handler!({ sessionId, update: next }); | |
| 1516 | // Copilot sends each chunk's raw delta just before the chunk. | |
| 1517 | const chunk = (text: string, task?: string) => | |
| 1518 | raw("assistant.message_delta", { | |
| 1519 | deltaContent: text, | |
| 1520 | ...(task ? { parentToolCallId: task } : {}), | |
| 1521 | }).pipe( | |
| 1522 | Effect.andThen( | |
| 1523 | update({ | |
| 1524 | sessionUpdate: "agent_message_chunk", | |
| 1525 | content: { type: "text", text }, | |
| 1526 | }), | |
| 1527 | ), | |
| 1528 | ); | |
| 1529 | const launch = (toolCallId: string, description: string, mode: string) => | |
| 1530 | update({ | |
| 1531 | sessionUpdate: "tool_call", | |
| 1532 | toolCallId, | |
| 1533 | title: description, | |
| 1534 | kind: "other", | |
| 1535 | status: "pending", | |
| 1536 | rawInput: { | |
| 1537 | description, | |
| 1538 | prompt: `${description}.`, | |
| 1539 | agent_type: "explore", | |
| 1540 | mode, | |
| 1541 | }, | |
| 1542 | }); | |
| 1543 | // Shapes captured from Copilot CLI 1.0.95 with two sync and one | |
| 1544 | // background subagent launched in one batch. | |
| 1545 | yield* chunk("Launching "); | |
| 1546 | yield* launch("task-a", "Describe a.txt", "sync"); | |
| 1547 | yield* launch("task-b", "Describe b.txt", "sync"); | |
| 1548 | yield* launch("task-bg", "List directory", "background"); | |
| 1549 | yield* raw("subagent.started", { toolCallId: "task-a" }, "agent-a"); | |
| 1550 | yield* raw("subagent.started", { toolCallId: "task-b" }, "agent-b"); | |
| 1551 | yield* raw("subagent.started", { toolCallId: "task-bg" }, "agent-bg"); | |
| 1552 | yield* update({ | |
| 1553 | sessionUpdate: "tool_call_update", | |
| 1554 | toolCallId: "task-bg", | |
| 1555 | status: "completed", | |
| 1556 | rawOutput: { content: "Agent started in background with agent_id: agent-bg." }, | |
| 1557 | }); | |
| 1558 | yield* update({ | |
| 1559 | sessionUpdate: "tool_call", | |
| 1560 | toolCallId: "view-a", | |
| 1561 | title: "Viewing a.txt", | |
| 1562 | kind: "read", | |
| 1563 | status: "completed", | |
| 1564 | rawInput: { path: "a.txt" }, | |
| 1565 | _meta: { "github.com/copilot": { agentId: "agent-a" } }, | |
| 1566 | }); | |
| 1567 | yield* chunk("`a.txt` says", "task-a"); | |
| 1568 | yield* chunk("`b.txt` has", "task-b"); | |
| 1569 | yield* chunk(" hello.", "task-a"); | |
| 1570 | yield* chunk("a.txt b.txt", "task-bg"); | |
| 1571 | yield* chunk(" three lines.", "task-b"); | |
| 1572 | for (const [task, agent, text] of [ | |
| 1573 | ["task-a", "agent-a", "`a.txt` says hello."], | |
| 1574 | ["task-b", "agent-b", "`b.txt` has three lines."], | |
| 1575 | ] as const) { | |
| 1576 | yield* update({ | |
| 1577 | sessionUpdate: "tool_call_update", | |
| 1578 | toolCallId: task, | |
| 1579 | status: "completed", | |
| 1580 | rawOutput: { content: text }, | |
| 1581 | }); | |
| 1582 | yield* raw("subagent.completed", { toolCallId: task }, agent); | |
| 1583 | } | |
| 1584 | yield* raw("subagent.completed", { toolCallId: "task-bg" }, "agent-bg"); | |
| 1585 | yield* chunk("all done."); | |
| 1586 | return { stopReason: "end_turn" as const }; | |
| 1587 | }), | |
| 1588 | }), | |
| 1589 | }), | |
| 1590 | }, | |
| 1591 | }); | |
| 1592 | const threadId = ThreadId.make("copilot-replay-parent"); | |
| 1593 | const modelSelection = { instanceId, model: "default" }; | |
| 1594 | const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({ | |
| 1595 | runtimeMode: "full-access", | |
| 1596 | interactionMode: "default", | |
| 1597 | cwd: process.cwd(), | |
| 1598 | }); | |
| 1599 | const runtime = yield* adapter.openSession({ | |
| 1600 | threadId, | |
| 1601 | providerSessionId: ProviderSessionId.make("copilot-replay-session"), | |
| 1602 | modelSelection, | |
| 1603 | runtimePolicy, | |
| 1604 | }); | |
| 1605 | const providerThread = yield* runtime.ensureThread({ | |
| 1606 | threadId, | |
| 1607 | modelSelection, | |
| 1608 | runtimePolicy, | |
| 1609 | }); | |
| 1610 | yield* runtime.startTurn( | |
| 1611 | makeTurnInput({ | |
| 1612 | threadId, | |
| 1613 | providerThread, | |
| 1614 | instanceId, | |
| 1615 | runtimePolicy, | |
| 1616 | now: yield* DateTime.now, | |
| 1617 | }), | |
| 1618 | ); | |
| 1619 | const events = Array.from( | |
| 1620 | yield* runtime.events.pipe( | |
| 1621 | Stream.takeUntil((event) => event.type === "turn.terminal"), | |
| 1622 | Stream.runCollect, | |
| 1623 | ), | |
| 1624 | ); | |
| 1625 | const items = events.flatMap((event) => | |
| 1626 | event.type === "turn_item.updated" ? [event.turnItem] : [], | |
| 1627 | ); | |
| 1628 | const lastText = (thread: string | null | undefined) => | |
| 1629 | new Map( | |
| 1630 | items.flatMap((item) => | |
| 1631 | item.threadId === thread && item.type === "assistant_message" | |
| 1632 | ? [[item.id, item.text] as const] | |
| 1633 | : [], | |
| 1634 | ), | |
| 1635 | ); | |
| 1636 | assert.deepEqual([...lastText(threadId).values()].join(""), "Launching all done."); | |
| 1637 | const tasks = new Map( | |
| 1638 | events.flatMap((event) => | |
| 1639 | event.type === "subagent.updated" | |
| 1640 | ? [[event.subagent.nativeTaskRef?.nativeId, event.subagent] as const] | |
| 1641 | : [], | |
| 1642 | ), | |
| 1643 | ); | |
| 1644 | const outputs = Object.fromEntries( | |
| 1645 | [...tasks].map(([id, task]) => [ | |
| 1646 | id, | |
| 1647 | [task.status, [...lastText(task.childThreadId).values()].join("")], | |
| 1648 | ]), | |
| 1649 | ); | |
| 1650 | assert.deepEqual(outputs, { | |
| 1651 | "task-a": ["completed", "`a.txt` says hello."], | |
| 1652 | "task-b": ["completed", "`b.txt` has three lines."], | |
| 1653 | "task-bg": ["completed", "a.txt b.txt"], | |
| 1654 | }); | |
| 1655 | assert.isTrue( | |
| 1656 | items.some( | |
| 1657 | (item) => | |
| 1658 | item.threadId === tasks.get("task-a")?.childThreadId && | |
| 1659 | item.type === "dynamic_tool" && | |
| 1660 | item.title === "Read a.txt", | |
| 1661 | ), | |
| 1662 | ); | |
| 1663 | assert.isFalse( | |
| 1664 | items.some((item) => item.threadId === threadId && item.type === "dynamic_tool"), | |
| 1665 | ); | |
| 1666 | }).pipe(Effect.provide(layerTest), Effect.scoped), | |
| 1667 | ); | |
| 1668 | ||
| 1467 | 1669 | it.effect("projects ACP v2 fidelity updates into first-class orchestration items", () => |
| 1468 | 1670 | Effect.gen(function* () { |
| 1469 | 1671 | const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
packages/provider-acp-registry/src/server/adapter.ts+21-2| ... | ... | @@ -3,6 +3,11 @@ import { |
| 3 | 3 | normalizeDevinToolCall, |
| 4 | 4 | extractDevinSubagentUpdate, |
| 5 | 5 | } from "./devinAcp.ts"; |
| 6 | import { | |
| 7 | copilotClientCapabilitiesMeta, | |
| 8 | extractCopilotSubagentUpdate, | |
| 9 | makeCopilotSubagentRouting, | |
| 10 | } from "./copilotAcp.ts"; | |
| 6 | 11 | import { defaultInstanceIdForDriver, ProviderDriverKind } from "@t3tools/contracts"; |
| 7 | 12 | import { AcpRegistrySettings } from "../settings.ts"; |
| 8 | 13 | import * as HostProcess from "@t3tools/shared/HostProcess"; |
| ... | ... | @@ -73,7 +78,7 @@ export interface AcpRegistryAdapterV2Options { |
| 73 | 78 | // Agent-specific behavior does not belong here: an agent that needs it gets a |
| 74 | 79 | // dedicated driver (as Grok and Antigravity have). The few exceptions below |
| 75 | 80 | // predate that rule and are small presentation hooks, not permission or tool |
| 76 | // behavior. | |
| 81 | // behavior. Copilot's subagent routing was approved later on the same terms. | |
| 77 | 82 | // |
| 78 | 83 | // Agents changing this file: do NOT add another `agentId === "..."` branch, |
| 79 | 84 | // agent table, or agent-specific hook without explicit approval from the |
| ... | ... | @@ -182,11 +187,17 @@ export const makeAcpRegistryAdapterV2 = Effect.fn("makeAcpRegistryAdapterV2")(fu |
| 182 | 187 | const startupKey = |
| 183 | 188 | options.settings.source === "local" ? `local:${options.instanceId}` : registryAgentId; |
| 184 | 189 | const isDevin = registryAgentId === "devin"; |
| 190 | // Local installs launch the `copilot` binary directly with no registry id. | |
| 191 | const isCopilot = | |
| 192 | registryAgentId === "github-copilot-cli" || | |
| 193 | (options.settings.source === "local" && | |
| 194 | /^copilot(\.exe|\.cmd)?$/i.test(options.settings.commandPath.split(/[\\/]/).pop() ?? "")); | |
| 195 | const copilotRouting = isCopilot ? makeCopilotSubagentRouting() : undefined; | |
| 185 | 196 | const flavor: AcpAdapterV2Flavor = { |
| 186 | 197 | driver: ACP_REGISTRY_PROVIDER, |
| 187 | 198 | capabilities: AcpProviderCapabilitiesV2, |
| 188 | 199 | promptFailure: (cause) => acpRegistryPromptFailure(registryAgentId, cause), |
| 189 | // Per-agent exceptions (Mistral Vibe, Devin): see the note above | |
| 200 | // Per-agent exceptions (Mistral Vibe, Devin, Copilot): see the note above | |
| 190 | 201 | // registerMistralVibeAcpExtensions before adding any more. |
| 191 | 202 | ...(registryAgentId === "mistral-vibe" |
| 192 | 203 | ? { registerExtensions: registerMistralVibeAcpExtensions } |
| ... | ... | @@ -202,6 +213,14 @@ export const makeAcpRegistryAdapterV2 = Effect.fn("makeAcpRegistryAdapterV2")(fu |
| 202 | 213 | extractSubagentUpdate: extractDevinSubagentUpdate, |
| 203 | 214 | } |
| 204 | 215 | : {}), |
| 216 | ...(copilotRouting === undefined | |
| 217 | ? {} | |
| 218 | : { | |
| 219 | clientCapabilitiesMeta: copilotClientCapabilitiesMeta, | |
| 220 | registerExtensions: copilotRouting.registerExtensions, | |
| 221 | normalizeSessionUpdate: copilotRouting.normalizeSessionUpdate, | |
| 222 | extractSubagentUpdate: extractCopilotSubagentUpdate, | |
| 223 | }), | |
| 205 | 224 | makeRuntime: options.makeRuntime ?? makeAcpRegistryRuntime(options, catalog), |
| 206 | 225 | ...(runtimeCoordinator === undefined |
| 207 | 226 | ? {} |
packages/provider-acp-registry/src/server/copilotAcp.ts created+175| ... | ... | @@ -0,0 +1,175 @@ |
| 1 | import * as Effect from "effect/Effect"; | |
| 2 | import * as Schema from "effect/Schema"; | |
| 3 | import type * as AcpSchema from "effect-acp/compat"; | |
| 4 | import type { AcpToolCallState } from "@t3tools/provider-acp/server/runtimeModel"; | |
| 5 | import type { | |
| 6 | AcpAdapterV2ExtensionContext, | |
| 7 | AcpAdapterV2SubagentUpdate, | |
| 8 | } from "@t3tools/provider-acp/server/adapter"; | |
| 9 | ||
| 10 | // Copilot CLI streams subagent text as plain `agent_message_chunk`s on the | |
| 11 | // root session. Only its opt-in raw event feed says who wrote a chunk: each | |
| 12 | // chunk is preceded by an `assistant.*_delta` carrying the spawning `task` | |
| 13 | // tool call id. Both arrive inline on the same reader, in wire order, so the | |
| 14 | // delta just before a chunk is that chunk's author. | |
| 15 | ||
| 16 | const COPILOT_META_KEY = "github.com/copilot"; | |
| 17 | const COPILOT_SESSION_EVENT = "github.com/copilot/sessionEvent"; | |
| 18 | ||
| 19 | /** Subscribes to the raw events subagent routing needs, and nothing else. */ | |
| 20 | export const copilotClientCapabilitiesMeta = { | |
| 21 | [COPILOT_META_KEY]: { | |
| 22 | events: [ | |
| 23 | "subagent.started", | |
| 24 | "subagent.completed", | |
| 25 | "subagent.failed", | |
| 26 | "assistant.message_delta", | |
| 27 | "assistant.reasoning_delta", | |
| 28 | ], | |
| 29 | }, | |
| 30 | }; | |
| 31 | ||
| 32 | const CopilotSessionEvent = Schema.Struct({ | |
| 33 | sessionId: Schema.String, | |
| 34 | type: Schema.String, | |
| 35 | agentId: Schema.optionalKey(Schema.String), | |
| 36 | data: Schema.optionalKey(Schema.Record(Schema.String, Schema.Unknown)), | |
| 37 | }); | |
| 38 | ||
| 39 | function record(value: unknown): Record<string, unknown> | undefined { | |
| 40 | return typeof value === "object" && value !== null && !Array.isArray(value) | |
| 41 | ? (value as Record<string, unknown>) | |
| 42 | : undefined; | |
| 43 | } | |
| 44 | ||
| 45 | const childSessionId = (taskToolCallId: string) => `copilot-task:${taskToolCallId}`; | |
| 46 | ||
| 47 | interface CopilotSessionAttribution { | |
| 48 | /** The last raw delta, consumed by the chunk that follows it. */ | |
| 49 | delta: { readonly kind: string; readonly text: string; readonly task?: string } | undefined; | |
| 50 | /** Running subagent id to the `task` tool call that spawned it. */ | |
| 51 | readonly taskByAgentId: Map<string, string>; | |
| 52 | } | |
| 53 | ||
| 54 | /** Per adapter instance; entries are keyed by root session and drop once idle. */ | |
| 55 | export function makeCopilotSubagentRouting() { | |
| 56 | const sessions = new Map<string, CopilotSessionAttribution>(); | |
| 57 | const session = (sessionId: string) => { | |
| 58 | let current = sessions.get(sessionId); | |
| 59 | if (current === undefined) { | |
| 60 | current = { delta: undefined, taskByAgentId: new Map() }; | |
| 61 | sessions.set(sessionId, current); | |
| 62 | } | |
| 63 | return current; | |
| 64 | }; | |
| 65 | const release = (sessionId: string, current: CopilotSessionAttribution) => { | |
| 66 | if (current.delta === undefined && current.taskByAgentId.size === 0) { | |
| 67 | sessions.delete(sessionId); | |
| 68 | } | |
| 69 | }; | |
| 70 | ||
| 71 | const registerExtensions = (context: AcpAdapterV2ExtensionContext) => | |
| 72 | context.runtime.handleExtNotification(COPILOT_SESSION_EVENT, CopilotSessionEvent, (event) => { | |
| 73 | const data = event.data ?? {}; | |
| 74 | const task = typeof data.parentToolCallId === "string" ? data.parentToolCallId : undefined; | |
| 75 | switch (event.type) { | |
| 76 | case "assistant.message_delta": | |
| 77 | case "assistant.reasoning_delta": { | |
| 78 | if (typeof data.deltaContent !== "string") return Effect.void; | |
| 79 | session(event.sessionId).delta = { | |
| 80 | kind: event.type, | |
| 81 | text: data.deltaContent, | |
| 82 | ...(task === undefined ? {} : { task }), | |
| 83 | }; | |
| 84 | return Effect.void; | |
| 85 | } | |
| 86 | case "subagent.started": { | |
| 87 | if (event.agentId !== undefined && typeof data.toolCallId === "string") { | |
| 88 | session(event.sessionId).taskByAgentId.set(event.agentId, data.toolCallId); | |
| 89 | } | |
| 90 | return Effect.void; | |
| 91 | } | |
| 92 | case "subagent.completed": | |
| 93 | case "subagent.failed": { | |
| 94 | const current = sessions.get(event.sessionId); | |
| 95 | if (current !== undefined && event.agentId !== undefined) { | |
| 96 | current.taskByAgentId.delete(event.agentId); | |
| 97 | release(event.sessionId, current); | |
| 98 | } | |
| 99 | if (typeof data.toolCallId !== "string") return Effect.void; | |
| 100 | // Background tasks end only here; their tool call settles at launch. | |
| 101 | return context.finishSubagent({ | |
| 102 | sessionId: event.sessionId, | |
| 103 | childSessionId: childSessionId(data.toolCallId), | |
| 104 | status: event.type === "subagent.failed" ? "failed" : "completed", | |
| 105 | result: typeof data.error === "string" ? data.error : null, | |
| 106 | }); | |
| 107 | } | |
| 108 | default: | |
| 109 | return Effect.void; | |
| 110 | } | |
| 111 | }); | |
| 112 | ||
| 113 | /** Moves a subagent's chunks and tool calls onto its own child session. */ | |
| 114 | const normalizeSessionUpdate = ( | |
| 115 | notification: AcpSchema.SessionNotification, | |
| 116 | ): AcpSchema.SessionNotification => { | |
| 117 | const update = notification.update; | |
| 118 | const current = sessions.get(notification.sessionId); | |
| 119 | if (current === undefined) return notification; | |
| 120 | let task: string | undefined; | |
| 121 | if ( | |
| 122 | update.sessionUpdate === "agent_message_chunk" || | |
| 123 | update.sessionUpdate === "agent_thought_chunk" | |
| 124 | ) { | |
| 125 | const delta = current.delta; | |
| 126 | current.delta = undefined; | |
| 127 | release(notification.sessionId, current); | |
| 128 | const kind = | |
| 129 | update.sessionUpdate === "agent_message_chunk" | |
| 130 | ? "assistant.message_delta" | |
| 131 | : "assistant.reasoning_delta"; | |
| 132 | if ( | |
| 133 | delta?.kind === kind && | |
| 134 | update.content.type === "text" && | |
| 135 | update.content.text === delta.text | |
| 136 | ) { | |
| 137 | task = delta.task; | |
| 138 | } | |
| 139 | } else if ( | |
| 140 | update.sessionUpdate === "tool_call" || | |
| 141 | update.sessionUpdate === "tool_call_update" | |
| 142 | ) { | |
| 143 | const agentId = record(record(update._meta)?.[COPILOT_META_KEY])?.agentId; | |
| 144 | task = typeof agentId === "string" ? current.taskByAgentId.get(agentId) : undefined; | |
| 145 | } | |
| 146 | return task === undefined ? notification : { ...notification, sessionId: childSessionId(task) }; | |
| 147 | }; | |
| 148 | ||
| 149 | return { registerExtensions, normalizeSessionUpdate }; | |
| 150 | } | |
| 151 | ||
| 152 | /** Copilot's `task` tool launches a subagent; its id names the child session. */ | |
| 153 | export function extractCopilotSubagentUpdate( | |
| 154 | tool: AcpToolCallState, | |
| 155 | ): AcpAdapterV2SubagentUpdate | undefined { | |
| 156 | const input = record(tool.data.rawInput); | |
| 157 | if (typeof input?.agent_type !== "string" || typeof input.prompt !== "string") return undefined; | |
| 158 | const output = record(tool.data.rawOutput)?.content; | |
| 159 | const background = input.mode === "background"; | |
| 160 | const status = | |
| 161 | tool.status === "failed" | |
| 162 | ? "failed" | |
| 163 | : tool.status === "completed" && !background | |
| 164 | ? "completed" | |
| 165 | : "running"; | |
| 166 | return { | |
| 167 | nativeTaskId: tool.toolCallId, | |
| 168 | childSessionId: childSessionId(tool.toolCallId), | |
| 169 | prompt: input.prompt, | |
| 170 | title: typeof input.description === "string" ? input.description : null, | |
| 171 | model: typeof input.model === "string" ? input.model : null, | |
| 172 | status, | |
| 173 | result: status !== "running" && typeof output === "string" ? output : null, | |
| 174 | }; | |
| 175 | } |
packages/provider-acp-registry/src/testing.ts+1| ... | ... | @@ -16,6 +16,7 @@ export { |
| 16 | 16 | normalizeDevinSessionUpdate, |
| 17 | 17 | normalizeDevinToolCall, |
| 18 | 18 | } from "./server/devinAcp.ts"; |
| 19 | export { extractCopilotSubagentUpdate, makeCopilotSubagentRouting } from "./server/copilotAcp.ts"; | |
| 19 | 20 | export { |
| 20 | 21 | acpRegistryProbeFailure, |
| 21 | 22 | acpRegistryProbeResult, |
packages/provider-acp/src/server/adapter.ts+3-1| ... | ... | @@ -218,7 +218,9 @@ export interface AcpAdapterV2Flavor { |
| 218 | 218 | readonly promptFailure?: (cause: unknown) => OrchestrationV2ProviderFailure; |
| 219 | 219 | readonly driver: ProviderDriverKind; |
| 220 | 220 | readonly capabilities: OrchestrationV2ProviderCapabilities; |
| 221 | readonly clientCapabilitiesMeta?: Record<string, boolean>; | |
| 221 | readonly clientCapabilitiesMeta?: NonNullable< | |
| 222 | EffectAcpSchema.InitializeRequest["clientCapabilities"] | |
| 223 | >["_meta"]; | |
| 222 | 224 | readonly normalizeSessionUpdate?: ( |
| 223 | 225 | notification: EffectAcpSchema.SessionNotification, |
| 224 | 226 | ) => EffectAcpSchema.SessionNotification; |