From c5f9a83896b19a538890587d010a531e656b517d Mon Sep 17 00:00:00 2001
From: Waishnav <86405648+Waishnav@users.noreply.github.com>
Date: Mon, 31 Aug 2026 04:00:00 +0530
Subject: [PATCH 1/2] feat(agents): add event-driven multi-agent wait
---
src/cli.test.ts | 81 ++++++++++-
src/cli.ts | 66 +++++++--
src/local-agent-client.ts | 46 +++++--
src/local-agent-daemon-lifecycle.ts | 2 +-
src/local-agent-daemon-protocol.test.ts | 48 ++++++-
src/local-agent-daemon-protocol.ts | 91 +++++++++++++
src/local-agent-daemon.test.ts | 63 ++++++++-
src/local-agent-daemon.ts | 29 +++-
src/local-agent-manager.test.ts | 70 ++++++++++
src/local-agent-manager.ts | 171 +++++++++++++++++++++++-
src/local-agent-presentation.ts | 5 +-
src/local-agent-store.ts | 12 ++
12 files changed, 633 insertions(+), 51 deletions(-)
diff --git a/src/cli.test.ts b/src/cli.test.ts
index 2caa5ae2a..1d9824159 100644
--- a/src/cli.test.ts
+++ b/src/cli.test.ts
@@ -8,7 +8,10 @@ import { join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { promisify } from "node:util";
import { loadConfig } from "./config.js";
-import { localAgentDaemonPaths } from "./local-agent-daemon-lifecycle.js";
+import {
+ LOCAL_AGENT_DAEMON_PROTOCOL_VERSION,
+ localAgentDaemonPaths,
+} from "./local-agent-daemon-lifecycle.js";
import { encodeLocalAgentDaemonResponse } from "./local-agent-daemon-protocol.js";
import { LocalAgentStore } from "./local-agent-store.js";
import { writeTestDevspaceConfig } from "./test-support/config.test.js";
@@ -100,7 +103,7 @@ try {
if (request.method === "agent.start") {
socket.end(encodeLocalAgentDaemonResponse({
requestId: request.requestId,
- protocolVersion: 3,
+ protocolVersion: LOCAL_AGENT_DAEMON_PROTOCOL_VERSION,
ok: false,
error: {
code: "UNKNOWN_TARGET",
@@ -113,10 +116,17 @@ try {
}
const result = request.method === "agent.list"
? [current]
+ : request.method === "agent.get"
+ ? current
+ : request.method === "agent.wait"
+ ? [
+ { id: current.id, status: "completed", response: "Review complete." },
+ { id: other.id, status: "running", wait: "timeout" },
+ ]
: request.method === "hello"
? {
state: "ready",
- protocolVersion: 3,
+ protocolVersion: LOCAL_AGENT_DAEMON_PROTOCOL_VERSION,
pid: process.pid,
endpoint: daemonSocket,
startedAt: "now",
@@ -127,7 +137,7 @@ try {
: null;
socket.end(encodeLocalAgentDaemonResponse({
requestId: request.requestId,
- protocolVersion: 3,
+ protocolVersion: LOCAL_AGENT_DAEMON_PROTOCOL_VERSION,
ok: true,
result,
}));
@@ -192,6 +202,69 @@ try {
const directList = [...daemonRequests].reverse().find((request) => request.method === "agent.list");
assert.deepEqual(directList?.params, { workspaceRoot: realpathSync.native(projectRoot) });
+ const { stdout: showOutput } = await execFileAsync(
+ "node",
+ ["--import", "tsx", "src/cli.ts", "agents", "show", current.id],
+ {
+ cwd: process.cwd(),
+ encoding: "utf8",
+ env: {
+ ...process.env,
+ ...cliConfigEnv,
+ DEVSPACE_WORKSPACE_ID: "ws_current",
+ DEVSPACE_WORKSPACE_ROOT: projectRoot,
+ },
+ },
+ );
+ assert.equal(
+ showOutput,
+ `Review complete.\n`,
+ );
+ assert.equal(
+ daemonRequests.filter((request) => request.method === "agent.get").length,
+ 1,
+ "show must be an immediate snapshot",
+ );
+
+ const { stdout: waitOutput } = await execFileAsync(
+ "node",
+ [
+ "--import",
+ "tsx",
+ "src/cli.ts",
+ "agents",
+ "wait",
+ current.id,
+ other.id,
+ "--timeout",
+ "0",
+ ],
+ {
+ cwd: process.cwd(),
+ encoding: "utf8",
+ env: {
+ ...process.env,
+ ...cliConfigEnv,
+ DEVSPACE_WORKSPACE_ID: "ws_current",
+ DEVSPACE_WORKSPACE_ROOT: projectRoot,
+ },
+ },
+ );
+ assert.equal(
+ waitOutput,
+ [
+ `Review complete.`,
+ ``,
+ "",
+ ].join("\n"),
+ );
+ const waitRequest = daemonRequests.find((request) => request.method === "agent.wait");
+ assert.deepEqual(waitRequest?.params, {
+ ids: [current.id, other.id],
+ scope: { workspaceId: "ws_current", workspaceRoot: realpathSync.native(projectRoot) },
+ timeoutMs: 0,
+ });
+
let commandFailure: unknown;
try {
await execFileAsync(
diff --git a/src/cli.ts b/src/cli.ts
index d18b488cc..27e7976c2 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -459,6 +459,9 @@ async function runAgentsCommand(args: string[]): Promise {
case "show":
await runAgentWorkflowCommand(json, () => runAgentsShow(commandArgs, json));
return;
+ case "wait":
+ await runAgentWorkflowCommand(json, () => runAgentsWait(commandArgs, json));
+ return;
case "targets":
await runAgentWorkflowCommand(json, () => runAgentsTargets(commandArgs, json));
return;
@@ -558,22 +561,62 @@ async function runAgentsShow(args: string[], json: boolean): Promise {
const client = createLocalAgentClient(config);
const scope = resolveCliWorkspaceContext(config.allowedRoots);
const initial = await client.get(id, scope);
- let record = presentAgentWorkflowResult(initial, json);
+ const record = presentAgentWorkflowResult(initial, json);
if (!record) return;
- const deadline = Date.now() + 15_000;
- while ((record.status === "starting" || record.status === "running") && Date.now() < deadline) {
- await sleep(500);
- const refreshed = presentAgentWorkflowResult(await client.get(id, scope), json);
- if (!refreshed) return;
- record = refreshed;
- }
-
const observation = presentAgentObservation(record);
if (json) printJson(observation);
else printAgentXml(formatAgentObservation(observation));
}
+async function runAgentsWait(args: string[], json: boolean): Promise {
+ const { ids, timeoutMs } = parseAgentsWaitArgs(args);
+ const config = loadConfig();
+ const client = createLocalAgentClient(config);
+ const scope = resolveCliWorkspaceContext(config.allowedRoots);
+ const results = presentAgentWorkflowResult(await client.wait(ids, scope, timeoutMs), json);
+ if (!results) return;
+ if (json) {
+ printJson(results);
+ return;
+ }
+ printAgentXml(results.map(formatAgentObservation).join("\n"));
+}
+
+function parseAgentsWaitArgs(args: string[]): { ids: string[]; timeoutMs?: number } {
+ const ids: string[] = [];
+ let timeoutMs: number | undefined;
+ for (let index = 0; index < args.length; index += 1) {
+ const argument = args[index]!;
+ if (argument === "--timeout") {
+ timeoutMs = parseAgentWaitTimeout(args[index + 1]);
+ index += 1;
+ continue;
+ }
+ if (argument.startsWith("--timeout=")) {
+ timeoutMs = parseAgentWaitTimeout(argument.slice("--timeout=".length));
+ continue;
+ }
+ if (argument.startsWith("-")) throw new Error(`Unknown option: ${argument}.`);
+ ids.push(argument);
+ }
+ if (ids.length === 0) {
+ throw new Error("Usage: devspace agents wait ... [--timeout ] [--json]");
+ }
+ return { ids, ...(timeoutMs === undefined ? {} : { timeoutMs }) };
+}
+
+function parseAgentWaitTimeout(value: string | undefined): number {
+ if (!value || !/^\d+$/.test(value)) {
+ throw new Error("Agent wait timeout must be a non-negative integer number of seconds.");
+ }
+ const timeoutMs = Number(value) * 1_000;
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs > 2_147_483_647) {
+ throw new Error("Agent wait timeout is too large.");
+ }
+ return timeoutMs;
+}
+
async function runAgentsDaemon(args: string[], json: boolean): Promise {
const [subcommand, ...extra] = args;
if (extra.length > 0) throw new Error("Usage: devspace agents daemon [--json]");
@@ -672,10 +715,6 @@ function printJson(value: unknown): void {
console.log(JSON.stringify(value));
}
-function sleep(ms: number): Promise {
- return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
-}
-
function printAgentsHelp(): void {
console.log(
[
@@ -686,6 +725,7 @@ function printAgentsHelp(): void {
" devspace agents run [--model ] [--effort ] [--json] ",
" devspace agents continue [--model ] [--effort ] [--json] ",
" devspace agents show [--json]",
+ " devspace agents wait ... [--timeout ] [--json]",
" devspace agents targets [--json]",
" devspace agents daemon [--json]",
].join("\n"),
diff --git a/src/local-agent-client.ts b/src/local-agent-client.ts
index 01f8c1cdc..5d31464b6 100644
--- a/src/local-agent-client.ts
+++ b/src/local-agent-client.ts
@@ -22,6 +22,7 @@ import {
import {
decodeAgentRecord,
decodeAgentRecordList,
+ decodeAgentWaitResults,
decodeDaemonLogs,
decodeDaemonStatus,
decodeLocalAgentDaemonResponse,
@@ -45,6 +46,8 @@ import type {
AgentListError,
AgentLookupError,
AgentStartError,
+ AgentWaitError,
+ LocalAgentWaitResult,
RunOverrides,
StartLocalAgentInput,
} from "./local-agent-manager.js";
@@ -60,6 +63,7 @@ type RequestError =
: M extends "agent.continue" ? AgentContinueError | AgentDaemonError
: M extends "agent.get" ? AgentLookupError | AgentDaemonError
: M extends "agent.list" ? AgentListError | AgentDaemonError
+ : M extends "agent.wait" ? AgentWaitError | AgentDaemonError
: AgentDaemonError;
export interface LocalAgentClientOptions {
@@ -134,6 +138,22 @@ export class LocalAgentClient {
return decodeRequestResult(result, "agent.list", decodeAgentRecordList);
}
+ async wait(
+ agentIds: readonly string[],
+ scope: LocalAgentWorkspaceScope,
+ timeoutMs?: number,
+ ): Promise> {
+ const transportTimeoutMs = timeoutMs === undefined
+ ? null
+ : Math.min(2_147_483_647, timeoutMs + this.requestTimeoutMs);
+ const result = await this.request("agent.wait", {
+ ids: [...agentIds],
+ scope,
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
+ }, transportTimeoutMs);
+ return decodeRequestResult(result, "agent.wait", decodeAgentWaitResults);
+ }
+
async status(): Promise> {
const result = await this.requestExisting("daemon.status", {});
return decodeRequestResult(result, "daemon.status", decodeDaemonStatus);
@@ -308,6 +328,7 @@ export class LocalAgentClient {
private async request(
method: M,
params: Extract['params'],
+ timeoutMs: number | null = this.requestTimeoutMs,
): Promise>> {
const ready = await this.ensureReady();
if (ready.isErr()) return ready as BetterResult>;
@@ -319,7 +340,7 @@ export class LocalAgentClient {
authToken: authToken.value,
method,
params,
- } as LocalAgentDaemonRequest, this.requestTimeoutMs);
+ } as LocalAgentDaemonRequest, timeoutMs ?? undefined);
if (response.isErr()) return response as BetterResult>;
if (!response.value.ok) {
const error = decodeRemoteError(response.value.error, method);
@@ -459,20 +480,22 @@ export function resolveDaemonEntrypoint(): string {
async function sendRequest(
endpoint: string,
request: LocalAgentDaemonRequest,
- timeoutMs: number,
+ timeoutMs?: number,
): Promise> {
return new Promise((resolve) => {
const socket = createConnection(endpoint);
let buffer = "";
let settled = false;
- const timer = setTimeout(() => {
- finish(Result.err(new AgentDaemonTimeoutError({
- code: "DAEMON_TIMEOUT",
- operation: request.method,
- retryable: true,
- message: "Timed out waiting for the local agent daemon.",
- })), true);
- }, timeoutMs);
+ const timer = timeoutMs === undefined
+ ? undefined
+ : setTimeout(() => {
+ finish(Result.err(new AgentDaemonTimeoutError({
+ code: "DAEMON_TIMEOUT",
+ operation: request.method,
+ retryable: true,
+ message: "Timed out waiting for the local agent daemon.",
+ })), true);
+ }, timeoutMs);
const finish = (
result: BetterResult,
@@ -480,7 +503,7 @@ async function sendRequest(
) => {
if (settled) return;
settled = true;
- clearTimeout(timer);
+ if (timer) clearTimeout(timer);
if (destroy) socket.destroy();
resolve(result);
};
@@ -599,6 +622,7 @@ function isRequestError(
|| category === "conflict"
|| category === "store";
case "agent.get":
+ case "agent.wait":
return category === "target" || category === "scope" || category === "store";
case "agent.list":
return category === "scope" || category === "store";
diff --git a/src/local-agent-daemon-lifecycle.ts b/src/local-agent-daemon-lifecycle.ts
index df0b81b95..250bb2319 100644
--- a/src/local-agent-daemon-lifecycle.ts
+++ b/src/local-agent-daemon-lifecycle.ts
@@ -12,7 +12,7 @@ import {
} from "node:fs";
import { join, resolve } from "node:path";
-export const LOCAL_AGENT_DAEMON_PROTOCOL_VERSION = 3;
+export const LOCAL_AGENT_DAEMON_PROTOCOL_VERSION = 4;
export const LOCAL_AGENT_DAEMON_SOCKET_NAME = "agentd.sock";
export const LOCAL_AGENT_DAEMON_PID_NAME = "agentd.pid";
export const LOCAL_AGENT_DAEMON_LOCK_NAME = "agentd.lock";
diff --git a/src/local-agent-daemon-protocol.test.ts b/src/local-agent-daemon-protocol.test.ts
index 708180987..b5fb282c1 100644
--- a/src/local-agent-daemon-protocol.test.ts
+++ b/src/local-agent-daemon-protocol.test.ts
@@ -1,15 +1,17 @@
import assert from "node:assert/strict";
import {
decodeAgentRecord,
+ decodeAgentWaitResults,
decodeLocalAgentDaemonRequest,
decodeLocalAgentDaemonResponse,
encodeLocalAgentDaemonResponse,
LocalAgentDaemonProtocolError,
} from "./local-agent-daemon-protocol.js";
+import { LOCAL_AGENT_DAEMON_PROTOCOL_VERSION } from "./local-agent-daemon-lifecycle.js";
const request = decodeLocalAgentDaemonRequest({
requestId: "req_1",
- protocolVersion: 3,
+ protocolVersion: LOCAL_AGENT_DAEMON_PROTOCOL_VERSION,
authToken: "test-secret",
method: "agent.start",
params: {
@@ -26,7 +28,7 @@ assert.equal(request.params.writeMode, "read_only");
const whitespaceRequest = decodeLocalAgentDaemonRequest({
requestId: "req_whitespace",
- protocolVersion: 3,
+ protocolVersion: LOCAL_AGENT_DAEMON_PROTOCOL_VERSION,
authToken: "test-secret",
method: "agent.start",
params: {
@@ -41,7 +43,7 @@ assert.equal(whitespaceRequest.params.prompt, " keep prompt whitespace \n");
const directRequest = decodeLocalAgentDaemonRequest({
requestId: "req_direct",
- protocolVersion: 3,
+ protocolVersion: LOCAL_AGENT_DAEMON_PROTOCOL_VERSION,
authToken: "test-secret",
method: "agent.start",
params: {
@@ -56,7 +58,7 @@ assert.equal(directRequest.params.workspaceId, undefined);
assert.throws(
() => decodeLocalAgentDaemonRequest({
requestId: "req_2",
- protocolVersion: 3,
+ protocolVersion: LOCAL_AGENT_DAEMON_PROTOCOL_VERSION,
authToken: "test-secret",
method: "agent.start",
params: { target: "reviewer", prompt: "" },
@@ -83,7 +85,7 @@ assert.equal(directRecord.workspaceId, undefined);
const response = decodeLocalAgentDaemonResponse({
requestId: "req_1",
- protocolVersion: 3,
+ protocolVersion: LOCAL_AGENT_DAEMON_PROTOCOL_VERSION,
ok: true,
result: record,
});
@@ -91,7 +93,7 @@ assert.equal(response.ok, true);
const errorResponse = decodeLocalAgentDaemonResponse(JSON.parse(encodeLocalAgentDaemonResponse({
requestId: "req_error",
- protocolVersion: 3,
+ protocolVersion: LOCAL_AGENT_DAEMON_PROTOCOL_VERSION,
ok: false,
error: {
code: "PROVIDER_UNAVAILABLE",
@@ -126,3 +128,37 @@ const failedRecord = decodeAgentRecord({
});
assert.equal(failedRecord.errorCode, "DAEMON_TIMEOUT");
assert.equal(failedRecord.errorRetryable, true);
+
+const waitRequest = decodeLocalAgentDaemonRequest({
+ requestId: "req_wait",
+ protocolVersion: LOCAL_AGENT_DAEMON_PROTOCOL_VERSION,
+ authToken: "test-secret",
+ method: "agent.wait",
+ params: {
+ ids: ["agt_one", "agt_two"],
+ scope: { workspaceId: "ws_test", workspaceRoot: "/tmp/project" },
+ timeoutMs: 5_000,
+ },
+});
+assert.equal(waitRequest.method, "agent.wait");
+if (waitRequest.method !== "agent.wait") throw new Error("expected agent.wait request");
+assert.deepEqual(waitRequest.params.ids, ["agt_one", "agt_two"]);
+assert.equal(waitRequest.params.timeoutMs, 5_000);
+
+assert.deepEqual(decodeAgentWaitResults([
+ { id: "agt_one", status: "completed", response: "Done." },
+ { id: "agt_two", status: "running", wait: "timeout" },
+ {
+ id: "agt_three",
+ status: "failed",
+ error: { code: "PROVIDER_EXECUTION_ERROR", message: "Failed.", retryable: true },
+ },
+]), [
+ { id: "agt_one", status: "completed", response: "Done." },
+ { id: "agt_two", status: "running", wait: "timeout" },
+ {
+ id: "agt_three",
+ status: "failed",
+ error: { code: "PROVIDER_EXECUTION_ERROR", message: "Failed.", retryable: true },
+ },
+]);
diff --git a/src/local-agent-daemon-protocol.ts b/src/local-agent-daemon-protocol.ts
index bf9bf8e65..402b94bc7 100644
--- a/src/local-agent-daemon-protocol.ts
+++ b/src/local-agent-daemon-protocol.ts
@@ -4,6 +4,7 @@ import type {
LocalAgentWorkspaceScope,
} from "./local-agent-store.js";
import type {
+ LocalAgentWaitResult,
RunOverrides,
StartLocalAgentInput,
} from "./local-agent-manager.js";
@@ -16,6 +17,7 @@ export type LocalAgentDaemonMethod =
| "agent.continue"
| "agent.get"
| "agent.list"
+ | "agent.wait"
| "daemon.status"
| "daemon.stop"
| "daemon.logs";
@@ -26,6 +28,11 @@ export type LocalAgentDaemonRequest =
| AgentDaemonRequestBase<"agent.continue", { id: string; prompt: string; scope: LocalAgentWorkspaceScope; overrides?: RunOverrides }>
| AgentDaemonRequestBase<"agent.get", { id: string; scope: LocalAgentWorkspaceScope }>
| AgentDaemonRequestBase<"agent.list", LocalAgentWorkspaceScope>
+ | AgentDaemonRequestBase<"agent.wait", {
+ ids: string[];
+ scope: LocalAgentWorkspaceScope;
+ timeoutMs?: number;
+ }>
| AgentDaemonRequestBase<"daemon.status", Record>
| AgentDaemonRequestBase<"daemon.stop", Record>
| AgentDaemonRequestBase<"daemon.logs", { lines?: number }>;
@@ -133,6 +140,14 @@ export function decodeLocalAgentDaemonRequest(value: unknown): LocalAgentDaemonR
method,
params: decodeListScope(params),
} as LocalAgentDaemonRequest;
+ case "agent.wait":
+ return {
+ requestId,
+ protocolVersion,
+ authToken,
+ method,
+ params: decodeWaitParams(params),
+ } as LocalAgentDaemonRequest;
case "daemon.logs":
return {
requestId,
@@ -202,6 +217,40 @@ export function decodeAgentRecordList(value: unknown): LocalAgentRecord[] {
return value.map(decodeAgentRecord);
}
+export function decodeAgentWaitResults(value: unknown): LocalAgentWaitResult[] {
+ if (!Array.isArray(value)) {
+ throw new LocalAgentDaemonProtocolError("INVALID_RESULT", "Daemon returned invalid agent wait results.");
+ }
+ return value.map((entry): LocalAgentWaitResult => {
+ const record = asRecord(entry);
+ const id = requiredString(record?.id, "id");
+ const status = requiredString(record?.status, "status");
+ switch (status) {
+ case "running": {
+ const wait = optionalString(record?.wait);
+ if (wait !== undefined && wait !== "timeout") {
+ throw new LocalAgentDaemonProtocolError("INVALID_RESULT", "Invalid agent wait state.");
+ }
+ return { id, status, ...(wait ? { wait } : {}) };
+ }
+ case "completed": {
+ const response = optionalContentString(record?.response);
+ return { id, status, ...(response === undefined ? {} : { response }) };
+ }
+ case "failed":
+ return { id, status, error: decodeWaitError(record?.error) };
+ case "stopped":
+ return {
+ id,
+ status,
+ ...(record?.error === undefined ? {} : { error: decodeWaitError(record.error) }),
+ };
+ default:
+ throw new LocalAgentDaemonProtocolError("INVALID_RESULT", "Invalid agent wait result status.");
+ }
+ });
+}
+
export function decodeDaemonStatus(value: unknown): LocalAgentDaemonStatus {
const record = asRecord(value);
const state = requiredString(record?.state, "state");
@@ -282,6 +331,48 @@ function decodeListScope(value: unknown): LocalAgentWorkspaceScope {
return decodeWorkspaceScope(value);
}
+function decodeWaitParams(value: unknown): {
+ ids: string[];
+ scope: LocalAgentWorkspaceScope;
+ timeoutMs?: number;
+} {
+ const record = asRecord(value);
+ if (!record) {
+ throw new LocalAgentDaemonProtocolError("INVALID_PARAMS", "Agent wait options must be an object.");
+ }
+ const ids = record?.ids;
+ if (!Array.isArray(ids) || ids.length === 0) {
+ throw new LocalAgentDaemonProtocolError("INVALID_PARAMS", "At least one subagent id is required.");
+ }
+ const timeoutMs = record.timeoutMs;
+ if (
+ timeoutMs !== undefined
+ && (typeof timeoutMs !== "number"
+ || !Number.isSafeInteger(timeoutMs)
+ || timeoutMs < 0
+ || timeoutMs > 2_147_483_647)
+ ) {
+ throw new LocalAgentDaemonProtocolError(
+ "INVALID_PARAMS",
+ "Wait timeout must be an integer between 0 and 2147483647 milliseconds.",
+ );
+ }
+ return {
+ ids: ids.map((id, index) => requiredString(id, `ids[${index}]`)),
+ scope: decodeWorkspaceScope(record.scope),
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
+ };
+}
+
+function decodeWaitError(value: unknown): { code: string; message: string; retryable: boolean } {
+ const record = asRecord(value);
+ return {
+ code: requiredString(record?.code, "error.code"),
+ message: requiredContentString(record?.message, "error.message"),
+ retryable: optionalBoolean(record?.retryable) ?? false,
+ };
+}
+
function decodeLogsParams(value: unknown): { lines?: number } {
if (value === undefined) return {};
const record = asRecord(value);
diff --git a/src/local-agent-daemon.test.ts b/src/local-agent-daemon.test.ts
index 6ea66e652..4d196ef3b 100644
--- a/src/local-agent-daemon.test.ts
+++ b/src/local-agent-daemon.test.ts
@@ -40,6 +40,9 @@ class FakeManager implements LocalAgentDaemonManager {
runtimeCount = 0;
closed = false;
lastInput?: StartLocalAgentInput;
+ blockWaitUntilAbort = false;
+ waitStarted = false;
+ waitAborted = false;
async start(input: StartLocalAgentInput) {
this.lastInput = input;
@@ -63,6 +66,21 @@ class FakeManager implements LocalAgentDaemonManager {
return Result.ok([record]);
}
+ async wait(agentIds: readonly string[], _scope: unknown, _timeoutMs?: number, signal?: AbortSignal) {
+ this.waitStarted = true;
+ if (this.blockWaitUntilAbort) {
+ await new Promise((resolveAbort) => {
+ const onAbort = () => {
+ this.waitAborted = true;
+ resolveAbort();
+ };
+ if (signal?.aborted) onAbort();
+ else signal?.addEventListener("abort", onAbort, { once: true });
+ });
+ }
+ return Result.ok(agentIds.map((id) => ({ id, status: "running" as const })));
+ }
+
async evictIdle(): Promise {}
async close(): Promise {
@@ -135,6 +153,9 @@ try {
const recordScope = { workspaceId: record.workspaceId!, workspaceRoot: record.workspaceRoot };
assert.equal(unwrap(await client.get(record.id, recordScope)).id, record.id);
assert.equal(unwrap(await client.list(recordScope))[0]?.id, record.id);
+ assert.deepEqual(unwrap(await client.wait([record.id], recordScope, 0)), [
+ { id: record.id, status: "running" },
+ ]);
assert.equal(unwrap(await client.status()).state, "ready");
unwrap(await client.stop());
@@ -247,7 +268,7 @@ const legacyServer = createNetServer((socket) => {
ok: false,
error: {
code: "DAEMON_PROTOCOL_MISMATCH",
- message: "Unsupported daemon protocol version 3; expected 1.",
+ message: `Unsupported daemon protocol version ${LOCAL_AGENT_DAEMON_PROTOCOL_VERSION}; expected 1.`,
retryable: false,
},
}));
@@ -301,10 +322,17 @@ const upgradeClient = new LocalAgentClient({
},
});
try {
- assert.equal(unwrap(await upgradeClient.ensureReady()).protocolVersion, 3);
+ assert.equal(
+ unwrap(await upgradeClient.ensureReady()).protocolVersion,
+ LOCAL_AGENT_DAEMON_PROTOCOL_VERSION,
+ );
assert.equal(replacementSpawns, 1);
assert.equal(spawnedBeforeLegacyLockReleased, false);
- assert.deepEqual(legacyMethods.slice(0, 3), ["hello:3", "hello:1", "daemon.stop:1"]);
+ assert.deepEqual(legacyMethods.slice(0, 3), [
+ `hello:${LOCAL_AGENT_DAEMON_PROTOCOL_VERSION}`,
+ "hello:1",
+ "daemon.stop:1",
+ ]);
} finally {
legacyLock.release();
await replacementDaemon.close();
@@ -397,11 +425,11 @@ const timeoutServer = createNetServer((socket) => {
if (request.method !== "hello") return;
socket.end(encodeLocalAgentDaemonResponse({
requestId: request.requestId,
- protocolVersion: 3,
+ protocolVersion: LOCAL_AGENT_DAEMON_PROTOCOL_VERSION,
ok: true,
result: {
state: "ready",
- protocolVersion: 3,
+ protocolVersion: LOCAL_AGENT_DAEMON_PROTOCOL_VERSION,
pid: process.pid,
endpoint: timeoutPaths.endpoint,
startedAt: "now",
@@ -443,7 +471,7 @@ const invalidServer = createNetServer((socket) => {
if (!buffer.includes("\n")) return;
socket.end(encodeLocalAgentDaemonResponse({
requestId: "wrong_request_id",
- protocolVersion: 3,
+ protocolVersion: LOCAL_AGENT_DAEMON_PROTOCOL_VERSION,
ok: true,
result: {},
}));
@@ -487,6 +515,27 @@ const socketDaemon = new LocalAgentDaemon({
try {
await socketDaemon.start();
+ socketManager.blockWaitUntilAbort = true;
+ const waitSocket = createConnection(socketDaemon.paths.endpoint);
+ await new Promise((resolveConnect, rejectConnect) => {
+ waitSocket.once("error", rejectConnect);
+ waitSocket.once("connect", resolveConnect);
+ });
+ waitSocket.write(JSON.stringify({
+ requestId: "disconnect-wait",
+ protocolVersion: LOCAL_AGENT_DAEMON_PROTOCOL_VERSION,
+ authToken: ensureLocalAgentDaemonSecret(socketDaemon.paths),
+ method: "agent.wait",
+ params: {
+ ids: [record.id],
+ scope: { workspaceId: record.workspaceId, workspaceRoot: record.workspaceRoot },
+ },
+ }) + "\n");
+ await waitFor(() => socketManager.waitStarted);
+ waitSocket.destroy();
+ await waitFor(() => socketManager.waitAborted);
+ socketManager.blockWaitUntilAbort = false;
+
const timedOutRequest = await sendRawRequest(socketDaemon.paths.endpoint);
assert.equal(timedOutRequest.ok, false);
if (!timedOutRequest.ok) {
@@ -497,7 +546,7 @@ try {
const unauthorized = await sendRawRequest(socketDaemon.paths.endpoint, JSON.stringify({
requestId: "unauthorized",
- protocolVersion: 3,
+ protocolVersion: LOCAL_AGENT_DAEMON_PROTOCOL_VERSION,
authToken: "wrong-secret",
method: "hello",
params: {},
diff --git a/src/local-agent-daemon.ts b/src/local-agent-daemon.ts
index dfd3a1499..75a3b6dd4 100644
--- a/src/local-agent-daemon.ts
+++ b/src/local-agent-daemon.ts
@@ -37,6 +37,8 @@ import type {
AgentListError,
AgentLookupError,
AgentStartError,
+ AgentWaitError,
+ LocalAgentWaitResult,
RunOverrides,
StartLocalAgentInput,
} from "./local-agent-manager.js";
@@ -53,6 +55,12 @@ export interface LocalAgentDaemonManager {
continue(agentId: string, prompt: string, overrides: RunOverrides | undefined, scope: LocalAgentWorkspaceScope): Promise>;
get(agentId: string, scope: LocalAgentWorkspaceScope): Result;
list(scope: LocalAgentWorkspaceScope): Result;
+ wait(
+ agentIds: readonly string[],
+ scope: LocalAgentWorkspaceScope,
+ timeoutMs?: number,
+ signal?: AbortSignal,
+ ): Promise>;
evictIdle(now?: number): Promise;
close(): Promise;
readonly activeTurnCount: number;
@@ -210,6 +218,7 @@ export class LocalAgentDaemon {
private handleConnection(socket: Socket): void {
this.sockets.add(socket);
+ const disconnected = new AbortController();
socket.setEncoding("utf8");
let buffer = "";
let handled = false;
@@ -243,14 +252,17 @@ export class LocalAgentDaemon {
handled = true;
clearTimeout(requestTimer);
const line = buffer.slice(0, newline);
- void this.handleLine(socket, line);
+ void this.handleLine(socket, line, disconnected.signal);
});
socket.on("error", () => undefined);
- socket.on("close", () => this.sockets.delete(socket));
+ socket.on("close", () => {
+ disconnected.abort();
+ this.sockets.delete(socket);
+ });
socket.on("error", () => clearTimeout(requestTimer));
}
- private async handleLine(socket: Socket, line: string): Promise {
+ private async handleLine(socket: Socket, line: string, signal: AbortSignal): Promise {
let requestId = "";
try {
let parsed: unknown;
@@ -261,7 +273,7 @@ export class LocalAgentDaemon {
}
requestId = readRequestId(parsed);
const request = decodeLocalAgentDaemonRequest(parsed);
- const response = await this.dispatch(request);
+ const response = await this.dispatch(request, signal);
socket.end(encodeLocalAgentDaemonResponse({
requestId: request.requestId,
protocolVersion: LOCAL_AGENT_DAEMON_PROTOCOL_VERSION,
@@ -274,7 +286,7 @@ export class LocalAgentDaemon {
}
}
- private async dispatch(request: LocalAgentDaemonRequest): Promise {
+ private async dispatch(request: LocalAgentDaemonRequest, signal: AbortSignal): Promise {
if (request.protocolVersion !== LOCAL_AGENT_DAEMON_PROTOCOL_VERSION) {
throw new LocalAgentDaemonProtocolError(
"PROTOCOL_MISMATCH",
@@ -307,6 +319,13 @@ export class LocalAgentDaemon {
return unwrapManagerResult(this.manager.get(request.params.id, request.params.scope));
case "agent.list":
return unwrapManagerResult(this.manager.list(request.params));
+ case "agent.wait":
+ return unwrapManagerResult(await this.manager.wait(
+ request.params.ids,
+ request.params.scope,
+ request.params.timeoutMs,
+ signal,
+ ));
case "daemon.status":
return this.status();
case "daemon.stop":
diff --git a/src/local-agent-manager.test.ts b/src/local-agent-manager.test.ts
index 803866fb9..758f69ae5 100644
--- a/src/local-agent-manager.test.ts
+++ b/src/local-agent-manager.test.ts
@@ -303,6 +303,76 @@ const earlyFailure = unwrap(await manager.start({
await waitFor(() => getRecord(earlyFailure.id).status === "error");
assert.equal(getRecord(earlyFailure.id).providerSessionId, "thread_early");
+const waitingOne = unwrap(await manager.start({
+ target: "reviewer",
+ prompt: "hold wait one",
+ workspaceId: scope.workspaceId,
+ workspaceRoot: root,
+}));
+const waitingTwo = unwrap(await manager.start({
+ target: "reviewer",
+ prompt: "hold wait two",
+ workspaceId: scope.workspaceId,
+ workspaceRoot: root,
+}));
+await waitFor(() => runtimes.get(waitingOne.id)?.inputs.length === 1);
+await waitFor(() => runtimes.get(waitingTwo.id)?.inputs.length === 1);
+let multiWaitSettled = false;
+const multiWait = manager.wait([waitingOne.id, waitingTwo.id, waitingOne.id], scope)
+ .then((result) => {
+ multiWaitSettled = true;
+ return result;
+ });
+runtimes.get(waitingOne.id)!.release();
+await waitFor(() => getRecord(waitingOne.id).status === "idle");
+assert.equal(multiWaitSettled, false, "multi-agent wait must remain pending until every turn finishes");
+runtimes.get(waitingTwo.id)!.release();
+assert.deepEqual(unwrap(await multiWait).map((result) => ({ id: result.id, status: result.status })), [
+ { id: waitingOne.id, status: "completed" },
+ { id: waitingTwo.id, status: "completed" },
+]);
+
+const timedWaitAgent = unwrap(await manager.start({
+ target: "reviewer",
+ prompt: "hold timed wait",
+ workspaceId: scope.workspaceId,
+ workspaceRoot: root,
+}));
+await waitFor(() => runtimes.get(timedWaitAgent.id)?.inputs.length === 1);
+assert.deepEqual(unwrap(await manager.wait([earlyFailure.id, timedWaitAgent.id], scope, 5)), [
+ {
+ id: earlyFailure.id,
+ status: "failed",
+ error: {
+ code: "PROVIDER_EXECUTION_ERROR",
+ message: "provider failed after session creation",
+ retryable: false,
+ },
+ },
+ { id: timedWaitAgent.id, status: "running", wait: "timeout" },
+]);
+runtimes.get(timedWaitAgent.id)!.release();
+await waitFor(() => getRecord(timedWaitAgent.id).status === "idle");
+
+const cancelledWaitAgent = unwrap(await manager.start({
+ target: "reviewer",
+ prompt: "hold cancelled wait",
+ workspaceId: scope.workspaceId,
+ workspaceRoot: root,
+}));
+await waitFor(() => runtimes.get(cancelledWaitAgent.id)?.inputs.length === 1);
+const waitAbort = new AbortController();
+const cancelledWait = manager.wait([cancelledWaitAgent.id], scope, undefined, waitAbort.signal);
+waitAbort.abort();
+assert.deepEqual(unwrap(await cancelledWait), [{ id: cancelledWaitAgent.id, status: "running" }]);
+assert.equal(getRecord(cancelledWaitAgent.id).status, "running", "cancelling a waiter must not stop its turn");
+runtimes.get(cancelledWaitAgent.id)!.release();
+await waitFor(() => getRecord(cancelledWaitAgent.id).status === "idle");
+
+const invalidWait = await manager.wait([waitingOne.id, "agt_missing"], scope, 5);
+assert.equal(invalidWait.isErr(), true);
+if (invalidWait.isErr()) assert.equal(invalidWait.error.code, "AGENT_NOT_FOUND");
+
const wrongWorkspace = await manager.continue(
first.id,
"wrong workspace",
diff --git a/src/local-agent-manager.ts b/src/local-agent-manager.ts
index ef8a3c720..30fe1e1f4 100644
--- a/src/local-agent-manager.ts
+++ b/src/local-agent-manager.ts
@@ -20,6 +20,7 @@ import {
import {
type LocalAgentRecord,
type LocalAgentStore,
+ type LocalAgentTurnRecord,
type LocalAgentWorkspaceScope,
} from "./local-agent-store.js";
import {
@@ -71,6 +72,18 @@ export type AgentStartError = AgentTargetError | AgentScopeError | AgentConflict
export type AgentContinueError = AgentStartError;
export type AgentLookupError = AgentTargetError | AgentScopeError | AgentStoreError;
export type AgentListError = AgentScopeError | AgentStoreError;
+export type AgentWaitError = AgentLookupError;
+
+export type LocalAgentWaitResult =
+ | { id: string; status: "running"; wait?: "timeout" }
+ | { id: string; status: "completed"; response?: string }
+ | { id: string; status: "failed"; error: { code: string; message: string; retryable: boolean } }
+ | { id: string; status: "stopped"; error?: { code: string; message: string; retryable: boolean } };
+
+interface ActiveLocalAgentTurn {
+ turnId: number;
+ completion: Promise;
+}
/**
* Owns one durable DevSpace agent's turn lifecycle. Provider runtimes remain
@@ -86,7 +99,7 @@ export class LocalAgentManager {
private readonly allowedRoots?: readonly string[];
private readonly logger?: LocalAgentManagerLogger;
private readonly subagents: SubagentsConfig;
- private readonly activeTurns = new Map>();
+ private readonly activeTurns = new Map();
private accepting = true;
private closePromise?: Promise;
@@ -199,10 +212,57 @@ export class LocalAgentManager {
));
}
+ async wait(
+ agentIds: readonly string[],
+ scope: LocalAgentWorkspaceScope,
+ timeoutMs?: number,
+ signal?: AbortSignal,
+ ): Promise> {
+ const captures: Array<{ agent: LocalAgentRecord; turn?: LocalAgentTurnRecord }> = [];
+ for (const agentId of unique(agentIds)) {
+ const agent = this.get(agentId, scope);
+ if (agent.isErr()) return agent;
+ const turn = this.store.getLatestTurnResult(agentId);
+ if (turn.isErr()) return turn;
+ captures.push({ agent: agent.value, turn: turn.value });
+ }
+
+ const pending: Promise[] = [];
+ for (const capture of captures) {
+ if (capture.turn?.status !== "running") continue;
+ const active = this.activeTurns.get(capture.agent.id);
+ if (active?.turnId !== capture.turn.id) {
+ return Result.err(new AgentStoreError(
+ "wait",
+ new Error(`Turn ${capture.turn.id} is not active.`),
+ `Running turn state is unavailable for subagent ${capture.agent.id}.`,
+ ));
+ }
+ pending.push(active.completion);
+ }
+
+ const timedOut = pending.length > 0
+ ? await waitForTurns(pending, timeoutMs, signal)
+ : false;
+ const results: LocalAgentWaitResult[] = [];
+ for (const capture of captures) {
+ if (!capture.turn) {
+ results.push(waitResultFromAgent(capture.agent, timedOut));
+ continue;
+ }
+ const turn = this.store.getTurnByIdResult(capture.turn.id);
+ if (turn.isErr()) return turn;
+ results.push(turn.value
+ ? waitResultFromTurn(turn.value, timedOut)
+ : waitResultFromAgent(capture.agent, timedOut));
+ }
+ return Result.ok(results);
+ }
+
async close(): Promise {
if (this.closePromise) return this.closePromise;
this.accepting = false;
- const turns = Array.from(this.activeTurns.values());
+ const turns = Array.from(this.activeTurns.values(), (turn) => turn.completion);
this.closePromise = (async () => {
// Closing pooled runtimes is what interrupts provider turns. Waiting for
// those turns first can strand a provider process indefinitely.
@@ -257,7 +317,7 @@ export class LocalAgentManager {
const turn = Promise.resolve().then(() => (
this.runTurn(begun.value.agent, begun.value.turn.id, prompt, overrides, workspaceId)
));
- this.activeTurns.set(record.id, turn);
+ this.activeTurns.set(record.id, { turnId: begun.value.turn.id, completion: turn });
void turn.catch(() => undefined);
return Result.ok(begun.value.agent);
}
@@ -604,3 +664,108 @@ function agentNotFound(agentId: string): AgentTargetError {
message: `Unknown subagent id: ${agentId}.`,
});
}
+
+function unique(values: readonly string[]): string[] {
+ return [...new Set(values)];
+}
+
+async function waitForTurns(
+ turns: readonly Promise[],
+ timeoutMs: number | undefined,
+ signal: AbortSignal | undefined,
+): Promise {
+ let timer: NodeJS.Timeout | undefined;
+ let onAbort: (() => void) | undefined;
+ const timeout = timeoutMs === undefined
+ ? undefined
+ : new Promise<"timeout">((resolveTimeout) => {
+ timer = setTimeout(() => resolveTimeout("timeout"), timeoutMs);
+ });
+ const aborted = signal
+ ? new Promise<"aborted">((resolveAbort) => {
+ onAbort = () => resolveAbort("aborted");
+ if (signal.aborted) onAbort();
+ else signal.addEventListener("abort", onAbort, { once: true });
+ })
+ : undefined;
+ try {
+ const result = await Promise.race([
+ Promise.allSettled(turns).then(() => "completed" as const),
+ ...(timeout ? [timeout] : []),
+ ...(aborted ? [aborted] : []),
+ ]);
+ return result === "timeout";
+ } finally {
+ if (timer) clearTimeout(timer);
+ if (signal && onAbort) signal.removeEventListener("abort", onAbort);
+ }
+}
+
+function waitResultFromTurn(turn: LocalAgentTurnRecord, timedOut: boolean): LocalAgentWaitResult {
+ switch (turn.status) {
+ case "running":
+ return { id: turn.agentId, status: "running", ...(timedOut ? { wait: "timeout" } : {}) };
+ case "completed":
+ return {
+ id: turn.agentId,
+ status: "completed",
+ ...(turn.response === undefined ? {} : { response: turn.response }),
+ };
+ case "failed":
+ return { id: turn.agentId, status: "failed", error: turnFailure(turn) };
+ case "stopped":
+ return {
+ id: turn.agentId,
+ status: "stopped",
+ ...(hasTurnFailure(turn) ? { error: turnFailure(turn) } : {}),
+ };
+ }
+}
+
+function waitResultFromAgent(agent: LocalAgentRecord, timedOut: boolean): LocalAgentWaitResult {
+ switch (agent.status) {
+ case "starting":
+ case "running":
+ return { id: agent.id, status: "running", ...(timedOut ? { wait: "timeout" } : {}) };
+ case "idle":
+ return {
+ id: agent.id,
+ status: "completed",
+ ...(agent.latestResponse === undefined ? {} : { response: agent.latestResponse }),
+ };
+ case "error":
+ return {
+ id: agent.id,
+ status: "failed",
+ error: {
+ code: agent.errorCode ?? "AGENT_FAILED",
+ message: agent.error ?? "Subagent failed without an error message.",
+ retryable: agent.errorRetryable ?? false,
+ },
+ };
+ case "stopped":
+ return {
+ id: agent.id,
+ status: "stopped",
+ ...(agent.error || agent.errorCode || agent.errorRetryable !== undefined
+ ? { error: {
+ code: agent.errorCode ?? "AGENT_STOPPED",
+ message: agent.error ?? "Subagent stopped.",
+ retryable: agent.errorRetryable ?? false,
+ } }
+ : {}),
+ };
+ }
+}
+
+function hasTurnFailure(turn: LocalAgentTurnRecord): boolean {
+ return turn.error !== undefined || turn.errorCode !== undefined || turn.errorRetryable !== undefined;
+}
+
+function turnFailure(turn: LocalAgentTurnRecord): { code: string; message: string; retryable: boolean } {
+ return {
+ code: turn.errorCode ?? "AGENT_FAILED",
+ message: turn.error ?? "Subagent failed without an error message.",
+ retryable: turn.errorRetryable ?? false,
+ };
+}
diff --git a/src/local-agent-presentation.ts b/src/local-agent-presentation.ts
index 9def4d39f..f5ff569ec 100644
--- a/src/local-agent-presentation.ts
+++ b/src/local-agent-presentation.ts
@@ -46,7 +46,7 @@ export interface AgentCommandErrorOutput {
}
export type AgentObservationOutput =
- | { id: string; status: "running" }
+ | { id: string; status: "running"; wait?: "timeout" }
| { id: string; status: "completed"; response?: string }
| { id: string; status: "failed"; error: AgentFailureOutput }
| { id: string; status: "stopped"; error?: AgentFailureOutput };
@@ -123,6 +123,9 @@ export function formatAgentSummary(summary: AgentSummaryOutput): string {
}
export function formatAgentObservation(observation: AgentObservationOutput): string {
+ if (observation.status === "running" && observation.wait) {
+ return ``;
+ }
if (observation.status === "completed" && observation.response !== undefined) {
return `${escapeXmlText(observation.response)}`;
}
diff --git a/src/local-agent-store.ts b/src/local-agent-store.ts
index bdb014a66..3aab64da6 100644
--- a/src/local-agent-store.ts
+++ b/src/local-agent-store.ts
@@ -393,6 +393,12 @@ export class LocalAgentStore {
return row ? rowToLocalAgentTurnRecord(row) : undefined;
}
+ getTurnByIdResult(
+ turnId: number,
+ ): BetterResult {
+ return storeResult("get_turn", () => this.getTurnById(turnId));
+ }
+
getLatestTurn(agentId: string): LocalAgentTurnRecord | undefined {
const row = this.database.sqlite
.prepare("select * from local_agent_turns where agent_id = ? order by id desc limit 1")
@@ -400,6 +406,12 @@ export class LocalAgentStore {
return row ? rowToLocalAgentTurnRecord(row) : undefined;
}
+ getLatestTurnResult(
+ agentId: string,
+ ): BetterResult {
+ return storeResult("get_latest_turn", () => this.getLatestTurn(agentId));
+ }
+
listTurns(agentId: string): LocalAgentTurnRecord[] {
const rows = this.database.sqlite
.prepare("select * from local_agent_turns where agent_id = ? order by id asc")
From f1cffc6b84c8202e55a595ba7786af23425d6ed3 Mon Sep 17 00:00:00 2001
From: Waishnav <86405648+Waishnav@users.noreply.github.com>
Date: Mon, 31 Aug 2026 19:01:58 +0530
Subject: [PATCH 2/2] fix(agents): preserve empty wait responses
---
src/local-agent-daemon-protocol.test.ts | 4 ++++
src/local-agent-daemon-protocol.ts | 2 +-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/local-agent-daemon-protocol.test.ts b/src/local-agent-daemon-protocol.test.ts
index b5fb282c1..24acaf316 100644
--- a/src/local-agent-daemon-protocol.test.ts
+++ b/src/local-agent-daemon-protocol.test.ts
@@ -147,6 +147,8 @@ assert.equal(waitRequest.params.timeoutMs, 5_000);
assert.deepEqual(decodeAgentWaitResults([
{ id: "agt_one", status: "completed", response: "Done." },
+ { id: "agt_empty", status: "completed", response: "" },
+ { id: "agt_whitespace", status: "completed", response: " \n" },
{ id: "agt_two", status: "running", wait: "timeout" },
{
id: "agt_three",
@@ -155,6 +157,8 @@ assert.deepEqual(decodeAgentWaitResults([
},
]), [
{ id: "agt_one", status: "completed", response: "Done." },
+ { id: "agt_empty", status: "completed", response: "" },
+ { id: "agt_whitespace", status: "completed", response: " \n" },
{ id: "agt_two", status: "running", wait: "timeout" },
{
id: "agt_three",
diff --git a/src/local-agent-daemon-protocol.ts b/src/local-agent-daemon-protocol.ts
index 402b94bc7..bf87a4714 100644
--- a/src/local-agent-daemon-protocol.ts
+++ b/src/local-agent-daemon-protocol.ts
@@ -234,7 +234,7 @@ export function decodeAgentWaitResults(value: unknown): LocalAgentWaitResult[] {
return { id, status, ...(wait ? { wait } : {}) };
}
case "completed": {
- const response = optionalContentString(record?.response);
+ const response = typeof record?.response === "string" ? record.response : undefined;
return { id, status, ...(response === undefined ? {} : { response }) };
}
case "failed":