From b3688d40064ba69682e81219bf528b65ad2dd9eb Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 10 Sep 2026 18:18:20 -0700 Subject: [PATCH] Append pending sub-agent nudges onto existing ephemeral turns --- src/subagent/nudge-director.test.ts | 39 +++++++++++++++++++++++++++++ src/subagent/nudge-director.ts | 10 +++++--- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/subagent/nudge-director.test.ts b/src/subagent/nudge-director.test.ts index 848553c62..d62e77305 100644 --- a/src/subagent/nudge-director.test.ts +++ b/src/subagent/nudge-director.test.ts @@ -270,6 +270,45 @@ describe("SubAgentDirector tool failure recovery", () => { expect(ephemeralTexts(later)).toBeUndefined(); }); + test("recovery nudge appends to ephemeral turns already on the infer", async () => { + const director = new SubAgentDirector("system", [], undefined, 30); + const caps = capabilities(); + // Seed an ephemeral turn only when the caller did not supply any, so the + // infer action applyPendingNudge rewrites already carries ephemeral turns. + const seeding: ReactorCapabilities = { + ...caps, + infer: (options) => { + const existing = (options as { ephemeralTurns?: unknown[] } | undefined) + ?.ephemeralTurns; + return caps.infer({ + ...(options ?? {}), + ...(existing === undefined + ? { + ephemeralTurns: [ + { + role: "user", + content: [{ type: "text", text: "PRE-SEEDED-EPHEMERAL" }], + timestamp: 0, + }, + ], + } + : {}), + }); + }, + }; + + await director.decide(inferenceDone(["seeded-fail"]), state, seeding); + const texts = ephemeralTexts( + inferAction( + await director.decide(toolDone("seeded-fail", true), state, seeding), + ), + ); + + expect(texts).toHaveLength(2); + expect(texts?.[0]).toBe("PRE-SEEDED-EPHEMERAL"); + expect(texts?.[1]).toContain("A tool call failed"); + }); + test("failed-tool recovery supersedes soft re-read guidance", async () => { const director = new SubAgentDirector("system", [], undefined, 30); const caps = capabilities(); diff --git a/src/subagent/nudge-director.ts b/src/subagent/nudge-director.ts index fb1ec456e..947d3757d 100644 --- a/src/subagent/nudge-director.ts +++ b/src/subagent/nudge-director.ts @@ -14,7 +14,6 @@ import type { ReactorAction, ToolDefinition, ConversationTurn, - InferenceOptions, RetryPolicy, } from "@intx/types/runtime"; import { @@ -83,10 +82,15 @@ function inferWithSubAgentNudge( * once the pending tool calls have actually executed. */ function withEphemeralNudge( - options: InferenceOptions | undefined, + options: ExtendedInferenceOptions | undefined, text: string, ): ExtendedInferenceOptions { - return { ...(options ?? {}), ephemeralTurns: [ephemeralNudgeTurn(text)] }; + const turn = ephemeralNudgeTurn(text); + const existing = options?.ephemeralTurns; + if (existing === undefined || existing.length === 0) { + return { ...(options ?? {}), ephemeralTurns: [turn] }; + } + return { ...(options ?? {}), ephemeralTurns: [...existing, turn] }; } function isEmptyContinuation(event: ReactorInboundEvent): boolean {