Skip to content
Open
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
27 changes: 27 additions & 0 deletions apps/server/scripts/acp-mock-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ const emitInterleavedAssistantToolCalls =
const emitV2Fidelity = process.env.T3_ACP_EMIT_V2_FIDELITY === "1";
const vibeRetryOutcome = process.env.T3_ACP_VIBE_RETRY_OUTCOME;
const emitGenericToolPlaceholders = process.env.T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS === "1";
// Keeps the first prompt open after its generic placeholders until session/cancel,
// so a test can interrupt a root that is still prompting.
const holdFirstGenericPromptUntilCancel =
process.env.T3_ACP_HOLD_FIRST_GENERIC_PROMPT_UNTIL_CANCEL === "1";
// Leaves a background monitor running when the generic prompt returns, so the
// root settles held by background work.
const emitRunningMonitorWithGeneric = process.env.T3_ACP_EMIT_RUNNING_MONITOR_WITH_GENERIC === "1";
const emitPostSettleMonitorFlow = process.env.T3_ACP_EMIT_POST_SETTLE_MONITOR_FLOW === "1";
const emitInTurnTaskOutputThenLateDuplicate =
process.env.T3_ACP_EMIT_IN_TURN_TASKOUTPUT_THEN_LATE_DUPLICATE === "1";
Expand Down Expand Up @@ -1968,6 +1975,26 @@ const program = Effect.gen(function* () {
},
});

if (emitRunningMonitorWithGeneric && promptCount === 1) {
yield* agent.client.sessionUpdate({
sessionId: requestedSessionId,
update: {
sessionUpdate: "tool_call_update",
toolCallId: "tool-call-monitor-hold",
title: "Monitor: held background work",
kind: "execute",
status: "in_progress",
rawInput: {},
},
});
}
if (holdFirstGenericPromptUntilCancel && promptCount === 1) {
while (!cancelledSessions.has(requestedSessionId)) {
yield* Effect.sleep("25 millis");
}
cancelledSessions.delete(requestedSessionId);
return yield* finishPrompt(requestedSessionId, "cancelled");
}
return yield* finishPrompt(requestedSessionId, "end_turn");
}

Expand Down
462 changes: 372 additions & 90 deletions apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,18 @@
{"type":"emit_inbound","label":"notification:_x.ai/session_notification:subagent_progress","frame":{"kind":"notification","method":"_x.ai/session_notification","params":{"sessionId":"00000000-0000-4000-8000-000000000001","update":{"sessionUpdate":"subagent_progress","subagent_id":"00000000-0000-4000-8000-000000000002","attempt_id":"at1.bd80fbaa7c40408eb7999e19714ed4e1","parent_session_id":"00000000-0000-4000-8000-000000000001","child_session_id":"00000000-0000-4000-8000-000000000002","duration_ms":10066,"turn_count":1,"tool_call_count":0,"tokens_used":2912,"context_window_tokens":500000,"context_usage_pct":0,"tools_used":[],"error_count":0}}}}
{"type":"emit_inbound","label":"notification:_x.ai/task_backgrounded:task_backgrounded","frame":{"kind":"notification","method":"_x.ai/task_backgrounded","params":{"sessionId":"00000000-0000-4000-8000-000000000002","update":{"sessionUpdate":"task_backgrounded","tool_call_id":"call-d5448d70-380d-4a88-a7a0-2968806350ea-0","task_id":"call-d5448d70-380d-4a88-a7a0-2968806350ea-0","command":"sleep 20","cwd":"<workspace>","output_file":"/home/grok-replay/.grok/sessions/%3Cworkspace%3E/00000000-0000-4000-8000-000000000002/terminal/call-d5448d70-380d-4a88-a7a0-2968806350ea-0.log","description":"Sleep for 20 seconds as requested"},"_meta":{"eventId":"00000000-0000-4000-8000-000000000002-170","agentTimestampMs":1790321099801}}}}
{"type":"emit_inbound","label":"notification:_x.ai/session_notification:background_tasks","frame":{"kind":"notification","method":"_x.ai/session_notification","params":{"sessionId":"00000000-0000-4000-8000-000000000002","update":{"sessionUpdate":"background_tasks","tasks":[{"task_id":"call-d5448d70-380d-4a88-a7a0-2968806350ea-0","command":"sleep 20","description":"Sleep for 20 seconds as requested","cwd":"<workspace>","kind":"bash","status":"running","started_at":"2026-09-25T07:24:44.780647541+00:00","output_file":"/home/grok-replay/.grok/sessions/%3Cworkspace%3E/00000000-0000-4000-8000-000000000002/terminal/call-d5448d70-380d-4a88-a7a0-2968806350ea-0.log"}]},"_meta":{"eventId":"00000000-0000-4000-8000-000000000002-172","agentTimestampMs":1790321099801}}}}
{"type":"expect_outbound","label":"request:session/prompt:t3-xai-prompt-2","frame":{"kind":"request","method":"session/prompt","params":{"sessionId":"00000000-0000-4000-8000-000000000001","prompt":[{"type":"text","text":"Reply exactly QUEUED_DONE."}],"_meta":{"promptId":"t3-xai-prompt-2","requestId":"t3-xai-prompt-2"}}}}
{"type":"emit_inbound","label":"notification:session/update:agent_message_chunk:t3-xai-prompt-2","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"00000000-0000-4000-8000-000000000001","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"QUEUED"}},"_meta":{"promptId":"t3-xai-prompt-2","agentTimestampMs":1790321085000,"updateType":"AgentMessageChunk","chunkId":1}}}}
{"type":"emit_inbound","label":"notification:session/update:agent_message_chunk:t3-xai-prompt-2","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"00000000-0000-4000-8000-000000000001","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"_DONE"}},"_meta":{"promptId":"t3-xai-prompt-2","agentTimestampMs":1790321085001,"updateType":"AgentMessageChunk","chunkId":2}}}}
{"type":"emit_inbound","label":"notification:_x.ai/session_notification:turn_completed:t3-xai-prompt-2","frame":{"kind":"notification","method":"_x.ai/session_notification","params":{"sessionId":"00000000-0000-4000-8000-000000000001","update":{"sessionUpdate":"turn_completed","prompt_id":"t3-xai-prompt-2","stop_reason":"end_turn"}}}}
{"type":"emit_inbound","label":"notification:_x.ai/session/prompt_complete","frame":{"kind":"notification","method":"_x.ai/session/prompt_complete","params":{"sessionId":"00000000-0000-4000-8000-000000000001","promptId":"t3-xai-prompt-2","stopReason":"end_turn","agentResult":null}}}
{"type":"emit_inbound","label":"response:session/prompt","frame":{"kind":"response","method":"session/prompt","result":{"stopReason":"end_turn","_meta":{"sessionId":"00000000-0000-4000-8000-000000000001","requestId":"t3-xai-prompt-2","promptId":"t3-xai-prompt-2"}}}}
{"type":"expect_outbound","label":"request:session/prompt:t3-xai-prompt-3","frame":{"kind":"request","method":"session/prompt","params":{"sessionId":"00000000-0000-4000-8000-000000000001","prompt":[{"type":"text","text":"Reply exactly STEER_DONE."}],"_meta":{"promptId":"t3-xai-prompt-3","requestId":"t3-xai-prompt-3"}}}}
{"type":"emit_inbound","label":"notification:session/update:agent_message_chunk:t3-xai-prompt-3","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"00000000-0000-4000-8000-000000000001","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"STEER"}},"_meta":{"promptId":"t3-xai-prompt-3","agentTimestampMs":1790321086000,"updateType":"AgentMessageChunk","chunkId":1}}}}
{"type":"emit_inbound","label":"notification:session/update:agent_message_chunk:t3-xai-prompt-3","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"00000000-0000-4000-8000-000000000001","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"_DONE"}},"_meta":{"promptId":"t3-xai-prompt-3","agentTimestampMs":1790321086001,"updateType":"AgentMessageChunk","chunkId":2}}}}
{"type":"emit_inbound","label":"notification:_x.ai/session_notification:turn_completed:t3-xai-prompt-3","frame":{"kind":"notification","method":"_x.ai/session_notification","params":{"sessionId":"00000000-0000-4000-8000-000000000001","update":{"sessionUpdate":"turn_completed","prompt_id":"t3-xai-prompt-3","stop_reason":"end_turn"}}}}
{"type":"emit_inbound","label":"notification:_x.ai/session/prompt_complete","frame":{"kind":"notification","method":"_x.ai/session/prompt_complete","params":{"sessionId":"00000000-0000-4000-8000-000000000001","promptId":"t3-xai-prompt-3","stopReason":"end_turn","agentResult":null}}}
{"type":"emit_inbound","label":"response:session/prompt","frame":{"kind":"response","method":"session/prompt","result":{"stopReason":"end_turn","_meta":{"sessionId":"00000000-0000-4000-8000-000000000001","requestId":"t3-xai-prompt-3","promptId":"t3-xai-prompt-3"}}}}
{"type":"emit_inbound","label":"notification:session/update:tool_call_update:01a0d773-bfa9-72a1-b268-790ccebae43f","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"00000000-0000-4000-8000-000000000002","update":{"sessionUpdate":"tool_call_update","toolCallId":"call-d5448d70-380d-4a88-a7a0-2968806350ea-0","status":"completed","title":"[bg] sleep 20 (call-d54)","content":[{"type":"content","content":{"type":"text","text":"Command \"Sleep for 20 seconds as requested\" has been automatically moved to background because it exceeded auto-background timeout limit of 15s. Process is still running."}}],"rawOutput":{"type":"BackgroundTaskStarted","task_id":"call-d5448d70-380d-4a88-a7a0-2968806350ea-0","task_type":"bash","output_file":"/home/grok-replay/.grok/sessions/%3Cworkspace%3E/00000000-0000-4000-8000-000000000002/terminal/call-d5448d70-380d-4a88-a7a0-2968806350ea-0.log","status":"running","command":"sleep 20","summary":"Command \"Sleep for 20 seconds as requested\" has been automatically moved to background because it exceeded auto-background timeout limit of 15s. Process is still running.","retrieval_hint":"Use get_command_or_subagent_output with task_ids=[\"call-d5448d70-380d-4a88-a7a0-2968806350ea-0\"] when you need the output.","pid":2584853}},"_meta":{"totalTokens":13313,"eventId":"00000000-0000-4000-8000-000000000002-173","agentTimestampMs":1790321099802,"promptId":"01a0d773-bfa9-72a1-b268-790ccebae43f","streamStartMs":1790321082753,"turnStartMs":1790321082313,"updateType":"ToolCallUpdate","updateParams":{"toolCallId":"call-d5448d70-380d-4a88-a7a0-2968806350ea-0","status":"Completed"}}}}}
{"type":"emit_inbound","label":"notification:_x.ai/session_notification:subagent_progress","frame":{"kind":"notification","method":"_x.ai/session_notification","params":{"sessionId":"00000000-0000-4000-8000-000000000001","update":{"sessionUpdate":"subagent_progress","subagent_id":"00000000-0000-4000-8000-000000000002","attempt_id":"at1.bd80fbaa7c40408eb7999e19714ed4e1","parent_session_id":"00000000-0000-4000-8000-000000000001","child_session_id":"00000000-0000-4000-8000-000000000002","duration_ms":18067,"turn_count":1,"tool_call_count":1,"tokens_used":13480,"context_window_tokens":500000,"context_usage_pct":2,"tools_used":["run_terminal_command"],"error_count":0}}}}
{"type":"emit_inbound","label":"notification:session/update:agent_thought_chunk:01a0d773-bfa9-72a1-b268-790ccebae43f","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"00000000-0000-4000-8000-000000000002","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}},"_meta":{"totalTokens":13480,"eventId":"00000000-0000-4000-8000-000000000002-177","agentTimestampMs":1790321101461,"promptId":"01a0d773-bfa9-72a1-b268-790ccebae43f","streamStartMs":1790321100147,"turnStartMs":1790321082313,"updateType":"AgentThoughtChunk","chunkId":1}}}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,33 @@ import type { OrchestratorFixtureInput } from "../shared.ts";

export const GROK_BACKGROUND_SUBAGENT_PROMPT =
"Spawn one background subagent (run it in the background, do not wait for it) whose task is: run the shell command 'sleep 20' and then reply exactly SUBAGENT_DONE. As soon as the subagent is launched, end your turn by replying exactly ROOT_DONE without waiting for it.";
export const GROK_BACKGROUND_SUBAGENT_QUEUED_PROMPT = "Reply exactly QUEUED_DONE.";
export const GROK_BACKGROUND_SUBAGENT_STEER_PROMPT = "Reply exactly STEER_DONE.";

const GROK_SUBAGENT_WAKE_LABEL =
"notification:session/update:agent_message_chunk:subagent-completed-00000000-0000-4000-8000-000000000002";

// The root prompt settles while the subagent still runs, so run 1 is held open
// until `subagent_finished`, then finishes through the adapter's debounce.
// The root prompt settles while the subagent still runs. Run 1 completes with
// its reply and the subagent carries over, so a message queued behind it
// starts at once (run 2), and a steer aimed at it starts a new turn (run 3)
// instead of superseding its finished reply. The queued and steered prompts
// are hand-written in the recorded Grok 1.0.41 shape between the subagent's
// own frames (#17159). Grok's reply to the finished subagent replays as a
// continuation run (run 4), like Claude and Codex background wakes.
export function grokBackgroundSubagentInput(): OrchestratorFixtureInput {
return {
steps: [
{ type: "message", text: GROK_BACKGROUND_SUBAGENT_PROMPT },
{ type: "finish_held_run", targetRunIndex: 1, status: "completed" },
{ type: "queue_message", text: GROK_BACKGROUND_SUBAGENT_QUEUED_PROMPT },
{ type: "capture_shell_snapshot", key: "while-subagent-runs" },
{
type: "steer",
text: GROK_BACKGROUND_SUBAGENT_STEER_PROMPT,
targetRunIndex: 1,
targetReplyFinished: true,
},
{ type: "release_replay_gate", label: GROK_SUBAGENT_WAKE_LABEL },
{ type: "finish_held_run", targetRunIndex: 2, status: "completed" },
{ type: "finish_held_run", targetRunIndex: 4, status: "completed" },
],
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import {
backgroundNotifications,
projectionFor,
} from "../shared.ts";
import { GROK_BACKGROUND_SUBAGENT_PROMPT } from "./input.ts";
import {
GROK_BACKGROUND_SUBAGENT_PROMPT,
GROK_BACKGROUND_SUBAGENT_QUEUED_PROMPT,
GROK_BACKGROUND_SUBAGENT_STEER_PROMPT,
} from "./input.ts";

function runAssistantTexts(
projection: ReturnType<typeof projectionFor>,
Expand All @@ -22,21 +26,29 @@ function runAssistantTexts(

// A background subagent outlives its root turn. Grok ends it only with the
// root-session `subagent_finished` notification: the spawn tool completed at
// launch and nothing names the subagent done in text.
// launch and nothing names the subagent done in text. Run 1 completes with its
// reply while the subagent runs on (#17159): the thread waits on the subagent,
// a queued message starts at once, and a steer starts a new turn instead of
// superseding the finished reply. Grok's own reply to the finished subagent
// (its `subagent-completed-*` wake) is a provider continuation.
export function assertGrokBackgroundSubagentOutput(
result: OrchestratorV2ScenarioResult,
transcript: ProviderReplayTranscript,
) {
assertBaseProjection({
result,
transcript,
runCount: 2,
runStatuses: ["completed", "completed"],
runCount: 4,
runStatuses: ["completed", "completed", "completed", "completed"],
});
const projection = projectionFor(result, transcript.scenario);
assertSemanticProjectionIntegrity(projection);
assertUserMessagesInclude(projection, [GROK_BACKGROUND_SUBAGENT_PROMPT]);
const [rootRun, wakeRun] = projection.runs;
assertUserMessagesInclude(projection, [
GROK_BACKGROUND_SUBAGENT_PROMPT,
GROK_BACKGROUND_SUBAGENT_QUEUED_PROMPT,
GROK_BACKGROUND_SUBAGENT_STEER_PROMPT,
]);
const [rootRun, queuedRun, steerRun, wakeRun] = projection.runs;

assert.lengthOf(projection.subagents, 1);
const subagent = projection.subagents[0];
Expand All @@ -47,6 +59,7 @@ export function assertGrokBackgroundSubagentOutput(
assert.equal(subagentItem?.status, "completed");
assert.equal(subagentItem?.runId, rootRun?.id);

// Run 1 completes with ROOT_DONE; the running subagent does not hold it.
const subagentCompletedAt = result.domainEvents.findIndex(
(event) =>
event.type === "turn-item.updated" &&
Expand All @@ -60,24 +73,46 @@ export function assertGrokBackgroundSubagentOutput(
event.payload.status === "completed",
);
assert.isAtLeast(subagentCompletedAt, 0);
assert.isAbove(rootCompletedAt, subagentCompletedAt, "run 1 completed before the subagent");
assert.isAtLeast(rootCompletedAt, 0);
assert.isBelow(rootCompletedAt, subagentCompletedAt, "run 1 waited for the subagent");
assert.include(runAssistantTexts(projection, rootRun?.id), "ROOT_DONE");

const rootDoneCompletedAt = result.domainEvents.findIndex(
// While the subagent runs, clients see the thread waiting on it, not working.
const waitingShell = result.capturedShellSnapshots
.get("while-subagent-runs")
?.threads.find((thread) => thread.id === projection.thread.id);
assert.deepEqual(
waitingShell?.pendingBackgroundTasks?.map((task) => [
task.kind,
task.kind === "subagent" ? task.childThreadId : undefined,
]),
[["subagent", subagent?.childThreadId]],
"the Waiting strip names the running subagent",
);
assert.isNull(waitingShell?.activeRunId, "no run is working while only the subagent runs");

// The queued message did not wait for the subagent, and the steer started a
// new turn: no finished reply was superseded.
assert.deepEqual(runAssistantTexts(projection, queuedRun?.id), ["QUEUED_DONE"]);
assert.deepEqual(runAssistantTexts(projection, steerRun?.id), ["STEER_DONE"]);
const queuedStartedAt = result.domainEvents.findIndex(
(event) =>
event.type === "turn-item.updated" &&
event.payload.runId === rootRun?.id &&
event.payload.type === "assistant_message" &&
event.payload.text.trim() === "ROOT_DONE" &&
event.payload.status === "completed",
event.type === "run.updated" &&
event.payload.id === queuedRun?.id &&
event.payload.status === "running",
);
assert.isAtLeast(rootDoneCompletedAt, 0);
assert.isAtLeast(queuedStartedAt, 0);
assert.isBelow(
rootDoneCompletedAt,
queuedStartedAt,
subagentCompletedAt,
"ROOT_DONE streamed until the subagent finished",
"the queued message waited for the subagent",
);
assert.deepEqual(
projection.attempts.map((attempt) => attempt.status),
["completed", "completed", "completed", "completed"],
);

// The subagent's own work, done after run 1 settled, lands in its child thread.
if (subagent?.childThreadId == null) {
throw new Error("The background subagent is missing its child thread.");
}
Expand All @@ -89,7 +124,16 @@ export function assertGrokBackgroundSubagentOutput(
),
"the subagent's reply must land in its child thread",
);
assert.notInclude(runAssistantTexts(projection, rootRun?.id), "SUBAGENT_DONE");
const childTools = child?.turnItems.filter((item) => item.type === "dynamic_tool") ?? [];
// Its poll of the sleep, made after run 1 completed, still projects.
assert.include(
childTools.map((item) => `${item.title}:${item.status}`),
"sleep 20 (call-d54):completed",
"the subagent's tool calls must land in its child thread",
);
for (const run of projection.runs) {
assert.notInclude(runAssistantTexts(projection, run.id), "SUBAGENT_DONE");
}

assert.deepEqual(backgroundNotifications(projection), [
{
Expand Down
Loading
Loading