fix: relay ACP sessions_spawn parent streaming (#34310) (thanks @vincentkoc) (#34310)

Co-authored-by: Onur Solmaz <2453968+osolmaz@users.noreply.github.com>
This commit is contained in:
Bob
2026-03-04 11:44:20 +01:00
committed by GitHub
parent 61f7cea48b
commit 257e2f5338
10 changed files with 893 additions and 3 deletions

View File

@@ -16,6 +16,7 @@ vi.mock("../subagent-spawn.js", () => ({
vi.mock("../acp-spawn.js", () => ({
ACP_SPAWN_MODES: ["run", "session"],
ACP_SPAWN_STREAM_TARGETS: ["parent"],
spawnAcpDirect: (...args: unknown[]) => hoisted.spawnAcpDirectMock(...args),
}));
@@ -94,6 +95,7 @@ describe("sessions_spawn tool", () => {
cwd: "/workspace",
thread: true,
mode: "session",
streamTo: "parent",
});
expect(result.details).toMatchObject({
@@ -108,6 +110,7 @@ describe("sessions_spawn tool", () => {
cwd: "/workspace",
thread: true,
mode: "session",
streamTo: "parent",
}),
expect.objectContaining({
agentSessionKey: "agent:main:main",
@@ -165,6 +168,26 @@ describe("sessions_spawn tool", () => {
expect(hoisted.spawnSubagentDirectMock).not.toHaveBeenCalled();
});
it('rejects streamTo when runtime is not "acp"', async () => {
const tool = createSessionsSpawnTool({
agentSessionKey: "agent:main:main",
});
const result = await tool.execute("call-3b", {
runtime: "subagent",
task: "analyze file",
streamTo: "parent",
});
expect(result.details).toMatchObject({
status: "error",
});
const details = result.details as { error?: string };
expect(details.error).toContain("streamTo is only supported for runtime=acp");
expect(hoisted.spawnAcpDirectMock).not.toHaveBeenCalled();
expect(hoisted.spawnSubagentDirectMock).not.toHaveBeenCalled();
});
it("keeps attachment content schema unconstrained for llama.cpp grammar safety", () => {
const tool = createSessionsSpawnTool();
const schema = tool.parameters as {

View File

@@ -1,6 +1,6 @@
import { Type } from "@sinclair/typebox";
import type { GatewayMessageChannel } from "../../utils/message-channel.js";
import { ACP_SPAWN_MODES, spawnAcpDirect } from "../acp-spawn.js";
import { ACP_SPAWN_MODES, ACP_SPAWN_STREAM_TARGETS, spawnAcpDirect } from "../acp-spawn.js";
import { optionalStringEnum } from "../schema/typebox.js";
import { SUBAGENT_SPAWN_MODES, spawnSubagentDirect } from "../subagent-spawn.js";
import type { AnyAgentTool } from "./common.js";
@@ -34,6 +34,7 @@ const SessionsSpawnToolSchema = Type.Object({
mode: optionalStringEnum(SUBAGENT_SPAWN_MODES),
cleanup: optionalStringEnum(["delete", "keep"] as const),
sandbox: optionalStringEnum(SESSIONS_SPAWN_SANDBOX_MODES),
streamTo: optionalStringEnum(ACP_SPAWN_STREAM_TARGETS),
// Inline attachments (snapshot-by-value).
// NOTE: Attachment contents are redacted from transcript persistence by sanitizeToolCallInputs.
@@ -97,6 +98,7 @@ export function createSessionsSpawnTool(opts?: {
const cleanup =
params.cleanup === "keep" || params.cleanup === "delete" ? params.cleanup : "keep";
const sandbox = params.sandbox === "require" ? "require" : "inherit";
const streamTo = params.streamTo === "parent" ? "parent" : undefined;
// Back-compat: older callers used timeoutSeconds for this tool.
const timeoutSecondsCandidate =
typeof params.runTimeoutSeconds === "number"
@@ -118,6 +120,13 @@ export function createSessionsSpawnTool(opts?: {
}>)
: undefined;
if (streamTo && runtime !== "acp") {
return jsonResult({
status: "error",
error: `streamTo is only supported for runtime=acp; got runtime=${runtime}`,
});
}
if (runtime === "acp") {
if (Array.isArray(attachments) && attachments.length > 0) {
return jsonResult({
@@ -135,6 +144,7 @@ export function createSessionsSpawnTool(opts?: {
mode: mode && ACP_SPAWN_MODES.includes(mode) ? mode : undefined,
thread,
sandbox,
streamTo,
},
{
agentSessionKey: opts?.agentSessionKey,