Skip to content

Commit 9d2ceda

Browse files
authored
fix(core): return structured result for aborted executions (#1932)
1 parent d2827d9 commit 9d2ceda

3 files changed

Lines changed: 151 additions & 12 deletions

File tree

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"build:agentos-protocol": "node ./scripts/compile-agentos-protocol.mjs",
5757
"build:protocols": "pnpm run build:agentos-protocol",
5858
"test": "vitest run --exclude '**/*.nightly.test.ts' --reporter=verbose",
59-
"test:unit": "vitest run tests/agent-exit-event.test.ts tests/agentos-package.test.ts tests/agentos-protocol.test.ts tests/allowed-node-builtins.test.ts tests/bindings-zod.test.ts tests/bindings.test.ts tests/cron-manager.test.ts tests/cron-timer-driver.test.ts tests/generated-protocol.test.ts tests/leak-agent-os-processes.test.ts tests/leak-rpc-client.test.ts tests/mount-descriptors.test.ts tests/mount-reconfigure.test.ts tests/options-schema.test.ts tests/public-api-exports.test.ts tests/root-filesystem-descriptors.test.ts tests/runtime-compat-mount.test.ts tests/session-event-ordering.test.ts tests/session-permission-surface.test.ts tests/sidecar-client.test.ts tests/sidecar-permission-descriptors.test.ts tests/wasm-permission-tiers.test.ts --fileParallelism=false",
59+
"test:unit": "vitest run tests/agent-exit-event.test.ts tests/agentos-package.test.ts tests/agentos-protocol.test.ts tests/allowed-node-builtins.test.ts tests/bindings-zod.test.ts tests/bindings.test.ts tests/cron-manager.test.ts tests/cron-timer-driver.test.ts tests/execution-abort.test.ts tests/generated-protocol.test.ts tests/leak-agent-os-processes.test.ts tests/leak-rpc-client.test.ts tests/mount-descriptors.test.ts tests/mount-reconfigure.test.ts tests/options-schema.test.ts tests/public-api-exports.test.ts tests/root-filesystem-descriptors.test.ts tests/runtime-compat-mount.test.ts tests/session-event-ordering.test.ts tests/session-permission-surface.test.ts tests/sidecar-client.test.ts tests/sidecar-permission-descriptors.test.ts tests/wasm-permission-tiers.test.ts --fileParallelism=false",
6060
"test:pr": "pnpm test:unit && vitest run tests/migration-parity.test.ts --fileParallelism=false --reporter=verbose",
6161
"test:nightly": "vitest run tests/*.nightly.test.ts --reporter=verbose --passWithNoTests"
6262
},

packages/core/src/agent-os.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3582,22 +3582,13 @@ export class AgentOs {
35823582
}
35833583
completed = completedBeforeAdmission.has(executionId);
35843584
if (completed) resolveCompletion?.();
3585-
let rejectAbort: ((reason?: unknown) => void) | undefined;
3586-
const aborted =
3587-
!background && options.signal
3588-
? new Promise<never>((_resolve, reject) => {
3589-
rejectAbort = reject;
3590-
})
3591-
: undefined;
35923585
const abort = () => {
3586+
// Admitted executions report cancellation through their structured result.
35933587
void this._cancelExecution(response.response.operationId).catch(
35943588
(error) => {
35953589
console.error("[agentos] failed to cancel aborted execution", error);
35963590
},
35973591
);
3598-
rejectAbort?.(
3599-
options.signal?.reason ?? new DOMException("Aborted", "AbortError"),
3600-
);
36013592
};
36023593
options.signal?.addEventListener("abort", abort, { once: true });
36033594
if (options.signal?.aborted) abort();
@@ -3633,7 +3624,7 @@ export class AgentOs {
36333624
};
36343625
}
36353626
try {
3636-
await (aborted ? Promise.race([completion, aborted]) : completion);
3627+
await completion;
36373628
return await this._waitExecutionResult(response.response.operationId);
36383629
} finally {
36393630
cleanup();
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import {
2+
ExecutionOutcome,
3+
ExecutionState,
4+
} from "@rivet-dev/agentos-runtime-core/protocol";
5+
import { describe, expect, it, vi } from "vitest";
6+
import { AgentOs } from "../src/agent-os.js";
7+
import type { CodeExecutionResult } from "../src/language-execution.js";
8+
9+
const EXECUTION_ID = "execution-1";
10+
11+
type ExecutionCompletedHandler = (event: {
12+
executionId: string;
13+
generation: number;
14+
outcome: "cancelled";
15+
}) => void;
16+
17+
type ExecutionAgent = {
18+
_executionOutputHandlers: Map<string, Set<(event: unknown) => void>>;
19+
_executionCompletedHandlers: Map<string, Set<ExecutionCompletedHandler>>;
20+
_sidecarClient: {
21+
sendVmRequest: ReturnType<typeof vi.fn>;
22+
};
23+
_sidecarSession: unknown;
24+
_sidecarVm: unknown;
25+
_executionOperation(
26+
payload: unknown,
27+
options: { signal?: AbortSignal },
28+
): Promise<CodeExecutionResult>;
29+
};
30+
31+
function createExecutionAgent() {
32+
const agent = Object.create(AgentOs.prototype) as ExecutionAgent;
33+
agent._executionOutputHandlers = new Map();
34+
agent._executionCompletedHandlers = new Map();
35+
agent._sidecarSession = {};
36+
agent._sidecarVm = {};
37+
agent._sidecarClient = {
38+
sendVmRequest: vi.fn(async (_session, _vm, payload) => {
39+
switch (payload.type) {
40+
case "javascript_execution":
41+
return {
42+
type: "execution_accepted",
43+
response: {
44+
operationId: EXECUTION_ID,
45+
execution: {
46+
executionId: EXECUTION_ID,
47+
pid: 123,
48+
createdAtMs: 0n,
49+
},
50+
},
51+
};
52+
case "cancel_execution":
53+
setTimeout(() => {
54+
for (const handler of agent._executionCompletedHandlers.get("*") ??
55+
[]) {
56+
handler({
57+
executionId: EXECUTION_ID,
58+
generation: 1,
59+
outcome: "cancelled",
60+
});
61+
}
62+
}, 0);
63+
return {
64+
type: "execution_descriptor",
65+
response: {
66+
execution: {
67+
executionId: EXECUTION_ID,
68+
state: ExecutionState.Idle,
69+
retainedLanguage: null,
70+
createdAtMs: 0n,
71+
lastStartedAtMs: 0n,
72+
lastCompletedAtMs: 1n,
73+
},
74+
},
75+
};
76+
case "wait_execution":
77+
return {
78+
type: "execution_completed",
79+
response: {
80+
execution: null,
81+
outcome: ExecutionOutcome.Cancelled,
82+
exitCode: null,
83+
error: null,
84+
stdout: null,
85+
stderr: null,
86+
stdoutTruncated: null,
87+
stderrTruncated: null,
88+
evaluationValue: null,
89+
typeScriptCheckResult: null,
90+
},
91+
};
92+
default:
93+
throw new Error(`unexpected request: ${payload.type}`);
94+
}
95+
}),
96+
};
97+
return agent;
98+
}
99+
100+
describe("AgentOs execution abort", () => {
101+
it("rejects without admission when the signal is already aborted", async () => {
102+
const agent = createExecutionAgent();
103+
const controller = new AbortController();
104+
const reason = new DOMException("stop", "AbortError");
105+
controller.abort(reason);
106+
107+
await expect(
108+
agent._executionOperation(
109+
{ type: "javascript_execution" },
110+
{ signal: controller.signal },
111+
),
112+
).rejects.toBe(reason);
113+
expect(agent._sidecarClient.sendVmRequest).not.toHaveBeenCalled();
114+
});
115+
116+
it("returns the structured cancellation result after admission", async () => {
117+
const agent = createExecutionAgent();
118+
const controller = new AbortController();
119+
const execution = agent._executionOperation(
120+
{ type: "javascript_execution" },
121+
{ signal: controller.signal },
122+
);
123+
const settled = execution.then(
124+
(result) => ({ status: "resolved" as const, result }),
125+
(error: unknown) => ({ status: "rejected" as const, error }),
126+
);
127+
128+
await Promise.resolve();
129+
controller.abort(new DOMException("stop", "AbortError"));
130+
131+
expect(await settled).toEqual({
132+
status: "resolved",
133+
result: {
134+
outcome: "cancelled",
135+
error: {
136+
code: "execution_failed",
137+
name: "ExecutionError",
138+
message: "execution completed with cancelled",
139+
},
140+
},
141+
});
142+
expect(agent._sidecarClient.sendVmRequest).toHaveBeenCalledWith(
143+
agent._sidecarSession,
144+
agent._sidecarVm,
145+
{ type: "cancel_execution", request: { executionId: EXECUTION_ID } },
146+
);
147+
});
148+
});

0 commit comments

Comments
 (0)