Skip to content
Closed
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
14 changes: 12 additions & 2 deletions services/runner/src/engines/sandbox_agent/acp-interactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface PiToolSpecMeta {
}

export interface AttachPermissionResponderInput {
onPermissionRequestStarted?: (toolCallId: string) => void;
session: any;
run: {
emitEvent: (event: AgentEvent) => void;
Expand Down Expand Up @@ -125,6 +126,7 @@ export function attachPermissionResponder({
onPause,
log,
onPausedToolCall,
onPermissionRequestStarted,
onAllowedExecution,
onAnsweredDeny,
onNonParkablePause,
Expand Down Expand Up @@ -367,6 +369,9 @@ export function attachPermissionResponder({
}),
);
}
if (envelope.toolCallId) {
onPermissionRequestStarted?.(envelope.toolCallId);
}
const verdict = await responder.onPermission({
id,
availableReplies,
Expand Down Expand Up @@ -462,6 +467,11 @@ export function attachPermissionResponder({
return;
}

const toolCallId = stringValue(toolCall?.toolCallId);
if (toolCallId) {
onPermissionRequestStarted?.(toolCallId);
}

const verdict = await responder.onPermission({
id,
availableReplies,
Expand All @@ -476,7 +486,7 @@ export function attachPermissionResponder({
id,
verdict.kind,
availableReplies,
stringValue(toolCall?.toolCallId),
toolCallId,
verdict.interactionToken,
);
}
Expand Down Expand Up @@ -635,4 +645,4 @@ function clientToolReply(

function errorMessage(err: unknown): string {
return err instanceof Error ? err.message : String(err);
}
}
36 changes: 27 additions & 9 deletions services/runner/src/engines/sandbox_agent/pause.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const EVENT_DRAIN_MAX_TICKS = 6;

export class PendingApprovalPauseController {
private pendingApproval = false;
private readonly inFlightPermissionToolCallIds = new Set<string>();
private readonly pausedToolCallIds = new Set<string>();
private readonly allowedExecutionToolCallIds = new Set<string>();
private readonly answeredDenyToolCallIds = new Set<string>();
Expand Down Expand Up @@ -72,23 +73,40 @@ export class PendingApprovalPauseController {
* last word for the call this turn. Later harness frames for the same id are teardown artifacts
* from cancellation/session disposal and must not reach the event stream.
*/
markPausedToolCall(toolCallId: string): void {
if (!toolCallId) return;
const added = !this.pausedToolCallIds.has(toolCallId);
this.pausedToolCallIds.add(toolCallId);
// A late gate must extend terminalization long enough for its paused classification to land.
if (this.pendingApproval && added) this.gateClassificationVersion += 1;
markPausedToolCall(toolCallId: string): void {
if (!toolCallId) return;

this.inFlightPermissionToolCallIds.delete(toolCallId);

const added = !this.pausedToolCallIds.has(toolCallId);
this.pausedToolCallIds.add(toolCallId);

if (this.pendingApproval && added) {
this.gateClassificationVersion += 1;
}
}

markPermissionRequestStarted(toolCallId: string): void {
if (toolCallId) {
this.inFlightPermissionToolCallIds.add(toolCallId);
}
}

isInFlightPermission(toolCallId: string | undefined): boolean {
return toolCallId !== undefined && this.inFlightPermissionToolCallIds.has(toolCallId);
}
isPausedToolCall(toolCallId: string | undefined): boolean {
return toolCallId !== undefined && this.pausedToolCallIds.has(toolCallId);
}

/** Record that this turn answered allow for the call, so pause cleanup preserves its result. */
markAllowedExecution(toolCallId: string): void {
if (!toolCallId) return;
this.allowedExecutionToolCallIds.add(toolCallId);
}
if (!toolCallId) return;

this.inFlightPermissionToolCallIds.delete(toolCallId);

this.allowedExecutionToolCallIds.add(toolCallId);
}

/** Record an answered deny so its authoritative failed frame survives a sibling pause. */
markAnsweredDeny(toolCallId: string): void {
Expand Down
5 changes: 4 additions & 1 deletion services/runner/src/engines/sandbox_agent/run-turn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,9 @@ export async function runTurn(
if (pause.active) {
run.settleOpenToolCalls(
(id) =>
pause.isPausedToolCall(id) || pause.isAllowedExecution(id),
pause.isPausedToolCall(id) ||
pause.isAllowedExecution(id) ||
pause.isInFlightPermission(id),
TOOL_NOT_EXECUTED_PAUSED,
);
}
Expand Down Expand Up @@ -582,6 +584,7 @@ export async function runTurn(
serverPermissions,
log: logger,
onPause: () => pause.pause(),
onPermissionRequestStarted: (id) => pause.markPermissionRequestStarted(id),
onPausedToolCall: (id) => pause.markPausedToolCall(id),
onAllowedExecution: (id) => pause.markAllowedExecution(id),
onAnsweredDeny: (id) => pause.markAnsweredDeny(id),
Expand Down
Loading