From e4212851b387dd2d869997d8dcf3b7012fce7a05 Mon Sep 17 00:00:00 2001 From: goncalompontes Date: Wed, 11 Nov 2026 00:02:00 +0000 Subject: [PATCH] feat(acp): surface DeepSeek Harness native subagents Add a DeepSeek ACP flavor that publishes native subagent tasks from the parent subagent tool call and maps DSH child-session state/transcript updates into T3's provider-native subagent stream. --- .../src/server/adapter.ts | 13 ++ .../src/server/deepseekAcp.test.ts | 141 ++++++++++++++++ .../src/server/deepseekAcp.ts | 151 ++++++++++++++++++ 3 files changed, 305 insertions(+) create mode 100644 packages/provider-acp-registry/src/server/deepseekAcp.test.ts create mode 100644 packages/provider-acp-registry/src/server/deepseekAcp.ts diff --git a/packages/provider-acp-registry/src/server/adapter.ts b/packages/provider-acp-registry/src/server/adapter.ts index 1b8b4240fd00e..619f0f489cd90 100644 --- a/packages/provider-acp-registry/src/server/adapter.ts +++ b/packages/provider-acp-registry/src/server/adapter.ts @@ -15,6 +15,11 @@ import { isCopilotLostSessionError, makeCopilotSubagentRouting, } from "./copilotAcp.ts"; +import { + extractDeepseekSubagentSessionEvent, + extractDeepseekSubagentUpdate, + isDeepseekAcpAgent, +} from "./deepseekAcp.ts"; import { defaultInstanceIdForDriver, ProviderDriverKind } from "@t3tools/contracts"; import { readCustomModelEntries } from "@t3tools/shared/model"; import { AcpRegistrySettings } from "../settings.ts"; @@ -198,6 +203,7 @@ export const makeAcpRegistryAdapterV2 = Effect.fn("makeAcpRegistryAdapterV2")(fu /^claude-agent-acp(\.exe|\.cmd)?$/i.test( options.settings.commandPath.split(/[\\/]/).pop() ?? "", )); + const isDeepseek = isDeepseekAcpAgent(registryAgentId, options.settings.commandPath); const runtimeOptions = isClaude ? { ...options, @@ -247,6 +253,13 @@ export const makeAcpRegistryAdapterV2 = Effect.fn("makeAcpRegistryAdapterV2")(fu extractSubagentUpdate: extractDevinSubagentUpdate, } : {}), + ...(isDeepseek + ? { + followAgentWorkState: true, + extractSubagentUpdate: extractDeepseekSubagentUpdate, + extractSubagentSessionEvent: extractDeepseekSubagentSessionEvent, + } + : {}), ...(copilotRouting === undefined ? {} : { diff --git a/packages/provider-acp-registry/src/server/deepseekAcp.test.ts b/packages/provider-acp-registry/src/server/deepseekAcp.test.ts new file mode 100644 index 0000000000000..b71fa8c13a2ff --- /dev/null +++ b/packages/provider-acp-registry/src/server/deepseekAcp.test.ts @@ -0,0 +1,141 @@ +import { describe, expect, it } from "@effect/vitest"; +import type * as AcpSchema from "effect-acp/compat"; +import { + DEEPSEEK_SUBAGENT_META, + deepseekSubagentIdFromOutput, + extractDeepseekSubagentSessionEvent, + extractDeepseekSubagentUpdate, + isDeepseekAcpAgent, +} from "./deepseekAcp.ts"; + +const notification = (update: Record, sessionId = "child-1") => + ({ sessionId, update }) as unknown as AcpSchema.SessionNotification; + +describe("DeepSeek Harness ACP subagents", () => { + it("detects registry and local dsh agents", () => { + expect(isDeepseekAcpAgent("deepseek-harness", "/usr/bin/dsh")).toBe(true); + expect(isDeepseekAcpAgent("", "/home/me/.local/bin/dsh")).toBe(true); + expect(isDeepseekAcpAgent("", "C:\\tools\\dsh.cmd")).toBe(true); + expect(isDeepseekAcpAgent("claude-acp", "/usr/bin/claude-agent-acp")).toBe(false); + expect(isDeepseekAcpAgent("", "/usr/bin/codex")).toBe(false); + }); + + it("publishes the delegation prompt before the first state frame", () => { + const event = extractDeepseekSubagentSessionEvent( + notification({ + sessionUpdate: "state_update", + state: "running", + _meta: { + [DEEPSEEK_SUBAGENT_META]: { + parentSessionId: "root", + runId: "run-1", + phase: "delegated", + prompt: "Investigate the failure", + title: "Investigate", + }, + }, + }), + ); + expect(event).toEqual({ + type: "delegation", + childSessionId: "child-1", + parentSessionId: "root", + delegationId: "run-1", + prompt: "Investigate the failure", + }); + }); + + it("maps running and terminal child states", () => { + const frame = (update: Record) => + notification({ + sessionUpdate: "state_update", + ...update, + _meta: { + [DEEPSEEK_SUBAGENT_META]: { parentSessionId: "root", runId: "run-1" }, + }, + }); + expect(extractDeepseekSubagentSessionEvent(frame({ state: "running" }))).toMatchObject({ + type: "state", + childSessionId: "child-1", + status: "running", + }); + expect(extractDeepseekSubagentSessionEvent(frame({ state: "idle" }))).toMatchObject({ + status: "completed", + }); + expect( + extractDeepseekSubagentSessionEvent( + frame({ state: "idle", stopReason: "error", error: { message: "boom" } }), + ), + ).toMatchObject({ status: "failed", result: "boom" }); + expect( + extractDeepseekSubagentSessionEvent(frame({ state: "idle", stopReason: "cancelled" })), + ).toMatchObject({ status: "cancelled" }); + }); + + it("ignores frames without DeepSeek subagent metadata", () => { + expect( + extractDeepseekSubagentSessionEvent(notification({ sessionUpdate: "agent_message_chunk" })), + ).toBeUndefined(); + }); +}); + +describe("DeepSeek Harness subagent tool calls", () => { + const tool = (rawOutput: unknown, status = "completed") => + ({ + toolCallId: "call-1", + title: "subagent", + status, + data: { + name: "subagent", + rawInput: { + prompt: "Investigate the failure", + description: "Investigate", + name: "investigate", + }, + rawOutput, + }, + }) as never; + + it("publishes a visible subagent task from the parent tool call", () => { + expect(extractDeepseekSubagentUpdate(tool("started subagent investigate"))).toEqual({ + nativeTaskId: "call-1", + childSessionId: null, + prompt: "Investigate the failure", + title: "Investigate", + model: null, + status: "running", + result: null, + }); + }); + + it("links the child session id from structured or textual tool output", () => { + expect(deepseekSubagentIdFromOutput({ agent_id: "agent-7" })).toBe("agent-7"); + expect(deepseekSubagentIdFromOutput("started subagent investigate (agent agent-7)")).toBe( + "agent-7", + ); + expect(deepseekSubagentIdFromOutput("started subagent investigate")).toBeUndefined(); + // Generic ids can name a run/task and must not be treated as the child session. + expect(deepseekSubagentIdFromOutput({ id: "run-7" })).toBeUndefined(); + expect(deepseekSubagentIdFromOutput({ session_id: "other" })).toBeUndefined(); + expect(deepseekSubagentIdFromOutput("session id: other")).toBeUndefined(); + expect( + extractDeepseekSubagentUpdate( + tool({ kind: "activation", name: "investigate", agent_id: "agent-7" }), + ), + ).toMatchObject({ childSessionId: "agent-7" }); + }); + + it("marks a failed spawn failed and ignores unrelated tools", () => { + expect(extractDeepseekSubagentUpdate(tool("spawn failed", "failed"))).toMatchObject({ + status: "failed", + result: "spawn failed", + }); + expect( + extractDeepseekSubagentUpdate({ + toolCallId: "call-2", + status: "completed", + data: { name: "bash", rawInput: {}, rawOutput: "ok" }, + } as never), + ).toBeUndefined(); + }); +}); diff --git a/packages/provider-acp-registry/src/server/deepseekAcp.ts b/packages/provider-acp-registry/src/server/deepseekAcp.ts new file mode 100644 index 0000000000000..6f297c88a1707 --- /dev/null +++ b/packages/provider-acp-registry/src/server/deepseekAcp.ts @@ -0,0 +1,151 @@ +import * as Option from "effect/Option"; +import * as Schema from "effect/Schema"; +import type * as AcpSchema from "effect-acp/compat"; +import type { + AcpAdapterV2SubagentUpdate, + AcpSubagentSessionEvent, +} from "@t3tools/provider-acp/server/adapter"; +import type { AcpToolCallState } from "@t3tools/provider-acp/server/runtimeModel"; + +/** Custom session-update metadata emitted by DeepSeek Harness for native child agents. */ +export const DEEPSEEK_SUBAGENT_META = "deepseek.ai/subagent"; + +const decodeSubagentMeta = Schema.decodeUnknownOption( + Schema.Struct({ + parentSessionId: Schema.String, + runId: Schema.String, + provider: Schema.optionalKey(Schema.String), + local: Schema.optionalKey(Schema.Boolean), + /** "delegated" publishes the prompt/title; all other values publish state. */ + phase: Schema.optionalKey(Schema.String), + prompt: Schema.optionalKey(Schema.String), + title: Schema.optionalKey(Schema.String), + }), +); + +function record(value: unknown): Record | undefined { + return typeof value === "object" && value !== null && !Array.isArray(value) + ? (value as Record) + : undefined; +} + +/** + * DeepSeek Harness exposes in-process children by sending standard ACP state updates under the + * child's session id, tagged with `deepseek.ai/subagent` metadata. Map those frames into T3's + * provider-native subagent stream. + */ +export function extractDeepseekSubagentSessionEvent( + notification: AcpSchema.SessionNotification, +): AcpSubagentSessionEvent | undefined { + const update = notification.update; + const updateMeta = record("_meta" in update ? update._meta : undefined); + const meta = Option.getOrUndefined(decodeSubagentMeta(updateMeta?.[DEEPSEEK_SUBAGENT_META])); + if (meta === undefined) return undefined; + + if (meta.phase === "delegated") { + return { + type: "delegation", + childSessionId: notification.sessionId, + parentSessionId: meta.parentSessionId, + delegationId: meta.runId, + prompt: meta.prompt ?? meta.title ?? "", + }; + } + + if (update.sessionUpdate !== "state_update") return undefined; + if (update.state !== "idle") { + return { + type: "state", + childSessionId: notification.sessionId, + parentSessionId: meta.parentSessionId, + status: "running", + result: null, + }; + } + const status = + update.stopReason === "error" + ? "failed" + : update.stopReason === "cancelled" + ? "cancelled" + : "completed"; + return { + type: "state", + childSessionId: notification.sessionId, + parentSessionId: meta.parentSessionId, + status, + result: update.error?.message ?? null, + }; +} + +/** + * DeepSeek Harness is the only ACP agent here whose command is `dsh --profile acp`. Registry + * installs use the agent id; local installs are identified by the launcher name. + */ +export function isDeepseekAcpAgent(registryAgentId: string, commandPath: string): boolean { + if (/deepseek/i.test(registryAgentId)) return true; + const binary = commandPath.split(/[\\/]/).pop() ?? ""; + return /^dsh(?:\.exe|\.cmd)?$/i.test(binary); +} + +const decodeSubagentInput = Schema.decodeUnknownOption( + Schema.Struct({ + prompt: Schema.String, + description: Schema.optionalKey(Schema.String), + name: Schema.optionalKey(Schema.String), + }), +); + +function textFromUnknown(value: unknown): string | undefined { + if (typeof value === "string") return value; + const object = record(value); + if (typeof object?.text === "string") return object.text; + if (Array.isArray(object?.content)) { + const text = object.content + .map((item: unknown) => (record(item)?.text as string | undefined) ?? "") + .join(""); + return text.length > 0 ? text : undefined; + } + return undefined; +} + +/** DSH reports the child session id in the structured result, or in its "agent " text. */ +export function deepseekSubagentIdFromOutput(output: unknown): string | undefined { + const object = record(output); + // Only the DSH subagent document keys identify the child. A generic `id` or `session_id` + // can name a run, task, or unrelated record and would mislink the child session. + for (const key of ["agent_id", "agentId"]) { + const value = object?.[key]; + if (typeof value === "string" && value.trim().length > 0) return value.trim(); + } + const text = textFromUnknown(output); + if (text === undefined) return undefined; + const match = + text.match(/agent[ _-]?id\s*[:=]\s*([A-Za-z0-9_-]+)/i) ?? + text.match(/\(agent ([A-Za-z0-9_-]+)\)/i); + return match?.[1]; +} + +/** + * DSH's `subagent` tool starts a child in the background and returns immediately. The parent + * tool call is the only place with the prompt, so publish a native subagent task from it. The + * child session id is linked when the tool result exposes it (newer DSH) or when child state + * frames arrive. + */ +export function extractDeepseekSubagentUpdate( + tool: AcpToolCallState, +): AcpAdapterV2SubagentUpdate | undefined { + if (tool.data.name !== "subagent" && tool.data.name !== "subagent_fork") return undefined; + const input = Option.getOrUndefined(decodeSubagentInput(tool.data.rawInput)); + if (input === undefined) return undefined; + const childSessionId = deepseekSubagentIdFromOutput(tool.data.rawOutput); + const outputText = textFromUnknown(tool.data.rawOutput); + return { + nativeTaskId: tool.toolCallId, + childSessionId: childSessionId ?? null, + prompt: input.prompt, + title: input.description ?? input.name ?? null, + model: null, + status: tool.status === "failed" ? "failed" : "running", + result: tool.status === "failed" ? (outputText ?? null) : null, + }; +}