Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/provider-acp-registry/src/server/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -247,6 +253,13 @@ export const makeAcpRegistryAdapterV2 = Effect.fn("makeAcpRegistryAdapterV2")(fu
extractSubagentUpdate: extractDevinSubagentUpdate,
}
: {}),
...(isDeepseek
? {
followAgentWorkState: true,
extractSubagentUpdate: extractDeepseekSubagentUpdate,
extractSubagentSessionEvent: extractDeepseekSubagentSessionEvent,
}
: {}),
...(copilotRouting === undefined
? {}
: {
Expand Down
141 changes: 141 additions & 0 deletions packages/provider-acp-registry/src/server/deepseekAcp.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>, 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<string, unknown>) =>
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();
});
});
151 changes: 151 additions & 0 deletions packages/provider-acp-registry/src/server/deepseekAcp.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> | undefined {
return typeof value === "object" && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
: 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 <id>" 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,
};
}
Loading