fix(agents): waiting subagents post a false "yielded without a continuation source" warning (#162306)

* fix(agents): recognize registered message waits after yield

Carry registry acceptance of an explicit message wait through the yield callback and current attempt into continuation evidence. Avoid a synthetic missing-continuation reply for registered waits while preserving diagnostics for refused or absent registration.

* test(agents): align yield callback expectations

* test(gateway): include yield registration evidence in callback assertion
This commit is contained in:
Peter Steinberger 2026-10-01 04:01:04 -07:00 • committed by GitHub
parent 0ac1c241fb
commit 2d89ad083b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 193 additions and 87 deletions

View file

@ -35,6 +35,11 @@ The implementation owners are `subagent-registry-requester-yield.ts`,
`agent-task-tracking.ts`. `adoptPausedSubagentRunForFollowUp` uses the existing
registry replacement operation; it does not create a second delegated task.
An explicit `waitFor: "message"` counts as continuation evidence after the
registry accepts the wait. The attempt carries that fact into terminal reply
presentation, so a registered message wait does not produce a missing-continuation
warning. Refused or unregistered waits do not provide that evidence.
Private child results wait for their spawning turn to settle before individual
announcement admission. Normal settlement resumes each finished private child,
even while siblings are still running. Explicit yield assigns the frozen batch

View file

@ -75,6 +75,7 @@ export type EmbeddedAttemptExecutionPhaseInput = {
yieldDetected: boolean;
yieldMessage: string | null;
yieldAcknowledgment?: string;
yieldMessageWaitRegistered?: boolean;
};
setToolSearchCatalogExecutor: (
executor: ReturnType<typeof prepareEmbeddedAttemptStream>["toolSearchCatalogExecutor"],

View file

@ -5,18 +5,19 @@ const hoisted = vi.hoisted(() => ({
isMidTurnPrecheckSignal: vi.fn(() => false),
isSessionsYieldAbortError: vi.fn(() => false),
markYieldAborted: vi.fn(),
persistSessionsYieldContextMessage: vi.fn(async () => undefined),
sendCustomMessage: vi.fn(async () => undefined),
releaseLeasedSteering: vi.fn(),
stripSessionsYieldArtifacts: vi.fn(),
waitForSessionsYieldAbortSettle: vi.fn(async () => undefined),
waitForEmbeddedAbortSettle: vi.fn(async () => undefined),
withOwnedTranscriptWrite: vi.fn(async (operation: () => unknown) => await operation()),
}));
vi.mock("./attempt-sessions-yield.js", () => ({
isSessionsYieldAbortError: hoisted.isSessionsYieldAbortError,
persistSessionsYieldContextMessage: hoisted.persistSessionsYieldContextMessage,
stripSessionsYieldArtifacts: hoisted.stripSessionsYieldArtifacts,
waitForSessionsYieldAbortSettle: hoisted.waitForSessionsYieldAbortSettle,
}));
vi.mock("./attempt-subscription-cleanup.js", () => ({
waitForEmbeddedAbortSettle: hoisted.waitForEmbeddedAbortSettle,
}));
vi.mock("./midturn-precheck.js", () => ({
isMidTurnPrecheckSignal: hoisted.isMidTurnPrecheckSignal,
@ -33,6 +34,7 @@ function createInput(overrides: Partial<PromptErrorInput> = {}): PromptErrorInpu
agent: { state: { messages: [] } },
messages: [],
sessionManager: SessionManager.inMemory(),
sendCustomMessage: hoisted.sendCustomMessage,
},
attempt: { runId: "run-1", sessionId: "session-1" },
error: new Error("prompt failed"),
@ -96,15 +98,21 @@ describe("handleEmbeddedAttemptPromptError", () => {
await expect(handleEmbeddedAttemptPromptError(input)).resolves.toEqual({});
expect(hoisted.markYieldAborted).toHaveBeenCalledOnce();
expect(hoisted.waitForSessionsYieldAbortSettle).toHaveBeenCalledWith({
settlePromise,
expect(hoisted.waitForEmbeddedAbortSettle).toHaveBeenCalledWith({
promise: settlePromise,
runId: "run-1",
sessionId: "session-1",
reason: "sessions_yield",
});
expect(hoisted.stripSessionsYieldArtifacts).toHaveBeenCalledWith(input.activeSession);
expect(hoisted.persistSessionsYieldContextMessage).toHaveBeenCalledWith(
input.activeSession,
"wait for follow-up",
expect(hoisted.sendCustomMessage).toHaveBeenCalledWith(
{
customType: "openclaw.sessions_yield",
content: expect.stringContaining("wait for follow-up"),
display: false,
details: { source: "sessions_yield", message: "wait for follow-up" },
},
{ triggerTurn: false },
);
});
@ -112,7 +120,7 @@ describe("handleEmbeddedAttemptPromptError", () => {
const recoveryError = new Error("settle failed");
let marked = false;
hoisted.isSessionsYieldAbortError.mockReturnValue(true);
hoisted.waitForSessionsYieldAbortSettle.mockImplementationOnce(async () => {
hoisted.waitForEmbeddedAbortSettle.mockImplementationOnce(async () => {
expect(marked).toBe(true);
throw recoveryError;
});

View file

@ -2,6 +2,7 @@ import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
import type { ImageContent } from "../../../llm/types.js";
import type { createTrajectoryRuntimeRecorder } from "../../../trajectory/runtime.js";
import type { AgentMessage } from "../../runtime/index.js";
import { buildSessionsYieldContextMessage } from "../../sessions-yield-context.js";
import { agentSessionQueuePromptContext } from "../../sessions/agent-session-prompting.js";
import {
attachPromptCompactionRequestBudget,
@ -26,10 +27,9 @@ import {
} from "./attempt-llm-boundary.js";
import {
isSessionsYieldAbortError,
persistSessionsYieldContextMessage,
stripSessionsYieldArtifacts,
waitForSessionsYieldAbortSettle,
} from "./attempt-sessions-yield.js";
import { waitForEmbeddedAbortSettle } from "./attempt-subscription-cleanup.js";
import { wrapStreamFnWithMessageTransform } from "./message-transform-stream-wrapper.js";
import { isMidTurnPrecheckSignal, type MidTurnPrecheckRequest } from "./midturn-precheck.js";
import type { RuntimeContextCustomMessage } from "./runtime-context-prompt.js";
@ -287,10 +287,11 @@ export async function handleEmbeddedAttemptPromptError(input: {
if (yieldAborted) {
// Publish terminal state before fallible recovery so outer cleanup still recognizes the yield.
input.markYieldAborted();
await waitForSessionsYieldAbortSettle({
settlePromise: input.yieldAbortSettled,
await waitForEmbeddedAbortSettle({
promise: input.yieldAbortSettled,
runId: input.attempt.runId,
sessionId: input.attempt.sessionId,
reason: "sessions_yield",
});
await input.withOwnedTranscriptWrite(async () => {
const transcriptRewritten = await withSessionManagerWrite(
@ -298,7 +299,12 @@ export async function handleEmbeddedAttemptPromptError(input: {
() => stripSessionsYieldArtifacts(input.activeSession),
);
if (input.yieldMessage) {
await persistSessionsYieldContextMessage(input.activeSession, input.yieldMessage);
await input.activeSession.sendCustomMessage(
buildSessionsYieldContextMessage(input.yieldMessage),
{
triggerTurn: false,
},
);
}
const target = transcriptRewritten && input.activeSession.sessionManager.getSessionTarget();
if (target) {

View file

@ -35,6 +35,7 @@ function createResultFixture(params?: {
didSendViaMessagingTool?: boolean;
yieldDetected?: boolean;
yieldAcknowledgment?: string;
yieldMessageWaitRegistered?: boolean;
assistantTexts?: readonly string[];
toolMetas?: Array<{
toolName: string;
@ -130,6 +131,7 @@ function createResultFixture(params?: {
readYieldState: () => ({
yieldDetected: params?.yieldDetected ?? false,
yieldAcknowledgment: params?.yieldAcknowledgment,
yieldMessageWaitRegistered: params?.yieldMessageWaitRegistered,
}),
},
prepared: {
@ -196,6 +198,7 @@ describe("attempt result projection", () => {
fixture.input.lifecycle.readYieldState = () => ({
yieldDetected: true,
yieldAcknowledgment: "later yield",
yieldMessageWaitRegistered: true,
});
});
@ -216,6 +219,7 @@ describe("attempt result projection", () => {
expect(result.attemptUsage).toBeUndefined();
expect(result).toHaveProperty("yieldDetected", undefined);
expect(result).toHaveProperty("yieldAcknowledgment", undefined);
expect(result).toHaveProperty("yieldMessageWaitRegistered", undefined);
expect(result).not.toHaveProperty("beforeAgentFinalizeRevisionReason");
});
@ -518,17 +522,22 @@ describe("attempt result projection", () => {
},
);
it("carries the explicit yield acknowledgment separately from continuation context", () => {
expect(
completeResult({
it.each([true, false, undefined])(
"carries yield acknowledgment and owner-recorded message wait (%s) separately from private context",
(yieldMessageWaitRegistered) => {
expect(
completeResult({
yieldDetected: true,
yieldAcknowledgment: "Waiting for a continuation.",
yieldMessageWaitRegistered,
}),
).toMatchObject({
yieldDetected: true,
yieldAcknowledgment: "Research started; results will follow.",
}),
).toMatchObject({
yieldDetected: true,
yieldAcknowledgment: "Research started; results will follow.",
});
});
yieldAcknowledgment: "Waiting for a continuation.",
yieldMessageWaitRegistered,
});
},
);
it("defaults missing replay metadata to replay-unsafe", () => {
const attempt = completeResult();

View file

@ -208,6 +208,7 @@ export function completeEmbeddedAttemptResult(
contextBudgetStatus: prompt.contextBudgetStatus,
yieldDetected: input.lifecycle.readYieldState().yieldDetected,
yieldAcknowledgment: input.lifecycle.readYieldState().yieldAcknowledgment,
yieldMessageWaitRegistered: input.lifecycle.readYieldState().yieldMessageWaitRegistered,
didDeliverSourceReplyViaMessageTool: hasDeliveredSourceReply(),
};
const terminal = projectAgentRunAttemptTerminal(state.terminal);

View file

@ -1,27 +1,12 @@
import type { AssistantMessage, AssistantMessageEventStreamLike } from "../../../llm/types.js";
import { isTranscriptOnlyOpenClawAssistantMessage } from "../../../shared/transcript-only-openclaw-assistant.js";
import type { AgentMessage } from "../../runtime/index.js";
import { buildSessionsYieldContextMessage } from "../../sessions-yield-context.js";
import type { SessionManager } from "../../sessions/index.js";
import { buildUsageWithNoCost } from "../../stream-message-shared.js";
import { isRunnerAbortError } from "../abort.js";
import { waitForEmbeddedAbortSettle } from "./attempt-subscription-cleanup.js";
const SESSIONS_YIELD_INTERRUPT_CUSTOM_TYPE = "openclaw.sessions_yield_interrupt";
export async function waitForSessionsYieldAbortSettle(params: {
settlePromise: Promise<void> | null;
runId: string;
sessionId: string;
}): Promise<void> {
await waitForEmbeddedAbortSettle({
promise: params.settlePromise,
runId: params.runId,
sessionId: params.sessionId,
reason: "sessions_yield",
});
}
// Return a synthetic aborted response so agent runtime unwinds without a real provider call.
export function createYieldAbortedResponse(model: {
api?: string;
@ -77,25 +62,6 @@ export function queueSessionsYieldInterruptMessage(activeSession: {
});
}
export async function persistSessionsYieldContextMessage(
activeSession: {
sendCustomMessage: (
message: {
customType: string;
content: string;
display: boolean;
details?: Record<string, unknown>;
},
options?: { triggerTurn?: boolean },
) => Promise<void>;
},
message: string,
) {
await activeSession.sendCustomMessage(buildSessionsYieldContextMessage(message), {
triggerTurn: false,
});
}
// Remove the synthetic yield interrupt + aborted assistant entry from the live transcript.
// After strip, the transcript must end with a non-assistant role so subagent
// completion auto-announce can inject a continuation turn.

View file

@ -18,13 +18,14 @@ afterEach(() => {
vi.unstubAllEnvs();
});
describe("waitForSessionsYieldAbortSettle", () => {
describe("waitForEmbeddedAbortSettle for sessions_yield", () => {
it("logs rejected settlement and clears its pending timer", async () => {
const { waitForSessionsYieldAbortSettle } = await import("./attempt-sessions-yield.js");
await waitForSessionsYieldAbortSettle({
settlePromise: Promise.reject(new Error("settle failed")),
const { waitForEmbeddedAbortSettle } = await import("./attempt-subscription-cleanup.js");
await waitForEmbeddedAbortSettle({
promise: Promise.reject(new Error("settle failed")),
runId: "run-1",
sessionId: "session-1",
reason: "sessions_yield",
});
expect(mocks.warn).toHaveBeenCalledExactlyOnceWith(
@ -34,11 +35,12 @@ describe("waitForSessionsYieldAbortSettle", () => {
});
it("skips missing settlement without scheduling a timer", async () => {
const { waitForSessionsYieldAbortSettle } = await import("./attempt-sessions-yield.js");
await waitForSessionsYieldAbortSettle({
settlePromise: null,
const { waitForEmbeddedAbortSettle } = await import("./attempt-subscription-cleanup.js");
await waitForEmbeddedAbortSettle({
promise: null,
runId: "run-1",
sessionId: "session-1",
reason: "sessions_yield",
});
expect(vi.getTimerCount()).toBe(0);

View file

@ -245,10 +245,11 @@ async function runEmbeddedAttemptOwned(
attempt: params,
setup,
markCoreToolStage: (name) => corePluginToolStages.mark(name),
onYield: (message, acknowledgment) => {
onYield: (message, acknowledgment, messageWaitRegistered) => {
yieldDetected = true;
yieldMessage = message;
yieldAcknowledgment = acknowledgment;
yieldMessageWaitRegistered = messageWaitRegistered;
queueYieldInterruptForSession?.();
runAbortController.abort(SESSIONS_YIELD_ABORT_REASON);
abortSessionForYield?.();
@ -307,6 +308,7 @@ async function runEmbeddedAttemptOwned(
let yieldDetected = false;
let yieldMessage: string | null = null;
let yieldAcknowledgment: string | undefined;
let yieldMessageWaitRegistered: boolean | undefined;
// Late-binding reference so onYield can abort the session (declared after tool creation)
let abortSessionForYield: (() => void) | null = null;
let queueYieldInterruptForSession: (() => void) | null = null;
@ -465,6 +467,7 @@ async function runEmbeddedAttemptOwned(
yieldDetected,
yieldMessage,
yieldAcknowledgment,
yieldMessageWaitRegistered,
}),
setToolSearchCatalogExecutor: (executor) => {
toolSearchCatalogExecutor = executor;

View file

@ -226,6 +226,7 @@ interface YieldContinuationAttempt {
successfulCronAdds?: number;
acceptedSessionSpawns?: readonly { runId: string; childSessionKey: string }[];
runtimeContinuationStarted?: boolean;
yieldMessageWaitRegistered?: boolean;
messagingToolSentTexts?: readonly string[];
messagingToolSentMediaUrls?: readonly string[];
messagingToolSentTargets?: readonly MessagingToolSend[];
@ -242,6 +243,7 @@ export function hasYieldContinuationEvidence(attempt: YieldContinuationAttempt):
hasCommittedMessagingToolDeliveryEvidence(attempt) ||
hasAcceptedSessionSpawn(attempt.acceptedSessionSpawns) ||
attempt.runtimeContinuationStarted === true ||
attempt.yieldMessageWaitRegistered === true ||
hasAsyncActivity(attempt.toolMetas) ||
(attempt.successfulCronAdds ?? 0) > 0
);

View file

@ -434,6 +434,8 @@ export type EmbeddedRunAttemptResult = {
yieldDetected?: boolean;
/** Explicit user-facing waiting status supplied to sessions_yield. */
yieldAcknowledgment?: string;
/** The registry accepted this attempt's explicit incoming-message wait. */
yieldMessageWaitRegistered?: boolean;
/**
* True when code mode owned this attempt's model tool surface. Absent means
* the harness did not report engagement (treated as not engaged), which is

View file

@ -406,6 +406,83 @@ describe("sessions_yield orchestration", () => {
});
describe("yield with continuation evidence", () => {
it.each(["accepted", "refused", "unregistered"] as const)(
"uses the owner's message wait registration as continuation evidence (%s)",
async (registration) => {
const registry = await import("../subagents/registry/subagent-registry.test-helpers.js");
const { createRequesterYieldCallback } =
await import("../openclaw-tools.requester-yield.js");
const { createSessionsYieldTool } = await import("../tools/sessions-yield-tool.js");
const params = {
...createOverflowRunParams(state),
sessionKey: "agent:main:subagent:message-wait",
runId: "message-wait-run",
};
registry.resetSubagentRegistryForTests({ persist: false });
if (registration !== "unregistered") {
registry.addSubagentRunForTests(
createSubagentRunRecord({
runId: params.runId,
childSessionKey: params.sessionKey,
requesterSessionKey: "agent:main:parent",
expectsCompletionMessage: true,
execution: { status: "running" },
completion: { required: true },
delivery: { status: "pending" },
suppressCompletionDelivery: registration === "refused",
}),
);
}
mockedRunEmbeddedAttempt.mockImplementationOnce(async () => {
let yieldMessageWaitRegistered: boolean | undefined;
const onYield = vi.fn(
(_message: string, _acknowledgment?: string, registered?: boolean) => {
yieldMessageWaitRegistered = registered;
},
);
const tool = createSessionsYieldTool({
sessionId: params.sessionId,
claimYield: createRequesterYieldCallback({
requesterSessionKey: params.sessionKey,
requesterAgentId: params.agentId,
requesterTurnRunId: params.runId,
}),
onYield,
});
expect((await tool.execute("yield-message", { waitFor: "message" })).details).toEqual({
status: "yielded",
});
expect(
registry.getSubagentRunByRunId(params.runId)?.requesterSettleWake?.pauseNotice,
).toEqual(
registration === "accepted"
? { acknowledgment: "Paused awaiting continuation." }
: undefined,
);
return makeAttemptResult({
yieldDetected: onYield.mock.calls.length > 0,
yieldMessageWaitRegistered,
assistantTexts: [],
});
});
try {
const result = await runEmbeddedAgent(params);
expect(result.meta.yielded).toBe(true);
expect(result.payloads ?? []).toEqual(
registration === "accepted"
? []
: [
{
text: "⚠️ Turn yielded without a continuation source. Send a message to resume.",
},
],
);
} finally {
registry.resetSubagentRegistryForTests({ persist: false });
}
},
);
it("rejects an unregistered accepted child instead of silently yielding", async () => {
mockedRunEmbeddedAttempt.mockResolvedValueOnce(
makeAttemptResult({

View file

@ -350,7 +350,7 @@ export function registerToolChannelProgressTests({
updateTool(ctx, { toolName: tool.name, toolCallId, partialResult: result });
await endTool(ctx, { toolName: tool.name, toolCallId, result, isError: false });
expect(onYield).toHaveBeenCalledWith(args.message, args.acknowledgment);
expect(onYield).toHaveBeenCalledWith(args.message, args.acknowledgment, undefined);
expect(events).toHaveLength(6);
expect(
events

View file

@ -59,11 +59,13 @@ export function createRequesterYieldCallback(params: {
if (canWaitForMessage && intent?.waitFor === "message" && params.requesterTurnRunId) {
const { markSubagentMessageWait } =
await import("./subagents/registry/subagent-registry.js");
await markSubagentMessageWait({
runId: params.requesterTurnRunId,
sessionKey: requesterSessionKey!,
acknowledgment: intent.acknowledgment,
});
return {
messageWaitRegistered: await markSubagentMessageWait({
runId: params.requesterTurnRunId,
sessionKey: requesterSessionKey!,
acknowledgment: intent.acknowledgment,
}),
};
}
return true;
};

View file

@ -16,6 +16,7 @@ import type { SpawnedToolContext } from "./spawned-context.js";
import type { ToolFsPolicy } from "./tool-fs-policy.js";
import type { CronToolOptions } from "./tools/cron-tool.types.js";
import type { QuestionPromptDelivery } from "./tools/question-prompt-send.js";
import type { SessionsYieldCallback } from "./tools/sessions-yield-tool.js";
/** Options shared by the coding-tool factory and its OpenClaw tool surface. */
export type OpenClawSharedToolsOptions = {
@ -92,7 +93,7 @@ export type OpenClawSharedToolsOptions = {
enableHeartbeatTool?: boolean;
/** Host-only observation after a canonical progress-card replacement commits. */
onProgressCardPlanSaved?: (unfinished: boolean) => void;
onYield?: (message: string, acknowledgment?: string) => Promise<void> | void;
onYield?: SessionsYieldCallback;
claimYieldCompletion?: () => boolean | Promise<boolean>;
/** Records hot-path tool-prep stages for reply startup diagnostics. */
recordToolPrepStage?: (name: string) => void;

View file

@ -285,7 +285,7 @@ export function createSubagentRegistryPublicApi(config: {
};
assertCurrent();
await restoreOnce(stateContext);
await markSubagentMessageWaitInRuns({
return await markSubagentMessageWaitInRuns({
...params,
runs,
context: stateContext,

View file

@ -23,7 +23,7 @@ export async function markSubagentMessageWaitInRuns(params: {
context: OpenClawStateWorkerContext;
assertCurrent: () => void;
persist: Parameters<typeof publishSubagentRunPostimages>[0]["persist"];
}): Promise<void> {
}): Promise<boolean> {
params.assertCurrent();
const entry = params.runs.get(params.runId);
if (
@ -34,10 +34,12 @@ export async function markSubagentMessageWaitInRuns(params: {
entry.execution.status !== "running" ||
entry.killIntent ||
entry.killReconciliation ||
entry.suppressCompletionDelivery ||
entry.requesterSettleWake?.pauseNotice
entry.suppressCompletionDelivery
) {
return;
return false;
}
if (entry.requesterSettleWake?.pauseNotice) {
return true;
}
const previous = captureSubagentRunMutationSnapshot(entry);
entry.requesterSettleWake = {
@ -66,6 +68,7 @@ export async function markSubagentMessageWaitInRuns(params: {
} catch (error) {
throw new SubagentRegistryWriteError("committed", error, result.publication);
}
return true;
}
export function markSubagentRunPausedAfterYield(params: {

View file

@ -392,7 +392,11 @@ describe("subagent registry lifecycle error grace", () => {
status: "yielded",
});
expect(onYield).toHaveBeenCalledOnce();
expect(onYield).toHaveBeenCalledWith("Wait for the visible dashboard child", undefined);
expect(onYield).toHaveBeenCalledWith(
"Wait for the visible dashboard child",
undefined,
undefined,
);
await settleYieldedCliTurn({
requesterTurnRunId,

View file

@ -70,7 +70,7 @@ describe("sessions_yield tool", () => {
expect(details.status).toBe("yielded");
expect(details).not.toHaveProperty("message");
expect(onYield).toHaveBeenCalledOnce();
expect(onYield).toHaveBeenCalledWith("Turn yielded.", undefined);
expect(onYield).toHaveBeenCalledWith("Turn yielded.", undefined, undefined);
});
it.each([undefined, "Research started; results will follow."])(
@ -91,7 +91,7 @@ describe("sessions_yield tool", () => {
});
expect(JSON.stringify(result)).not.toContain(message);
expect(onYield).toHaveBeenCalledOnce();
expect(onYield).toHaveBeenCalledWith(message, acknowledgment);
expect(onYield).toHaveBeenCalledWith(message, acknowledgment, undefined);
},
);

View file

@ -9,9 +9,15 @@ const NO_PENDING_CHILD_COMPLETION_MESSAGE =
export type SessionsYieldClaimResult =
| boolean
| { messageWaitRegistered: boolean }
| { error: string }
| { pendingChildren: readonly UnsettledRequesterChild[] };
export type SessionsYieldIntent = { waitFor?: "message"; acknowledgment?: string };
export type SessionsYieldCallback = (
message: string,
acknowledgment?: string,
messageWaitRegistered?: boolean,
) => Promise<void> | void;
function describePendingChild(child: UnsettledRequesterChild): string {
const name = child.label ? `${child.label} (${child.childSessionKey})` : child.childSessionKey;
@ -70,7 +76,7 @@ export function createSessionsYieldTool(opts?: {
claimYield?: (
intent?: SessionsYieldIntent,
) => SessionsYieldClaimResult | Promise<SessionsYieldClaimResult>;
onYield?: (message: string, acknowledgment?: string) => Promise<void> | void;
onYield?: SessionsYieldCallback;
}): AnyAgentTool {
return {
label: "Yield",
@ -114,10 +120,10 @@ export function createSessionsYieldTool(opts?: {
pendingChildren: claim.pendingChildren,
});
}
if (typeof claim === "object") {
if (typeof claim === "object" && "error" in claim) {
return jsonResult({ status: "error", error: claim.error });
}
if (claim !== true) {
if (claim !== true && typeof claim !== "object") {
// Advisory, not a failure: the model keeps the turn and nothing the user asked for failed.
return jsonResult({
status: "nothing_pending",
@ -125,7 +131,11 @@ export function createSessionsYieldTool(opts?: {
});
}
// The runtime owns the actual pause/end-turn behavior; this tool records intent.
await opts.onYield(message, acknowledgment);
await opts.onYield(
message,
acknowledgment,
typeof claim === "object" ? claim.messageWaitRegistered : undefined,
);
return jsonResult({
status: "yielded",
...(acknowledgment ? { acknowledgment } : {}),

View file

@ -215,7 +215,11 @@ describe("resolveGatewayScopedTools", () => {
requesterSessionKey: "agent:main:telegram:group:-100123",
requesterTurnRunId: "run-123",
});
expect(onYield).toHaveBeenCalledWith("waiting on subagents", "I’m waiting on the subagents.");
expect(onYield).toHaveBeenCalledWith(
"waiting on subagents",
"I’m waiting on the subagents.",
undefined,
);
expect(toolResult.details).toEqual({
status: "yielded",
acknowledgment: "I’m waiting on the subagents.",