Skip to content

Commit 879d301

Browse files
committed
Harden runner timeouts and docs
1 parent a833b40 commit 879d301

4 files changed

Lines changed: 95 additions & 4 deletions

File tree

README.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,24 @@ devagent-runner cancel <run-id>
4141
devagent-runner inspect <run-id>
4242
```
4343

44+
Example:
45+
46+
```bash
47+
cp ../devagent-sdk/fixtures/request-plan.json /tmp/request-plan.json
48+
devagent-runner run --request /tmp/request-plan.json
49+
devagent-runner inspect <run-id>
50+
```
51+
4452
## Local Development Wiring
4553

4654
For local MVP work this repo consumes `@devagent-sdk/*` through file dependencies from
4755
`../devagent-sdk`, and `devagent-hub` consumes this runner through file dependencies from
4856
`../devagent-runner/packages/*`.
4957

58+
The supported local setup path is the bootstrap flow documented in
59+
[`devagent-hub/README.md`](../devagent-hub/README.md) and
60+
[`devagent-hub/BASELINE_VALIDATION.md`](../devagent-hub/BASELINE_VALIDATION.md).
61+
5062
## Validated Flow
5163

5264
The runner has been validated in the canonical path:
@@ -55,8 +67,16 @@ The runner has been validated in the canonical path:
5567
devagent-hub -> LocalRunnerClient -> LocalRunner -> DevAgentAdapter -> devagent execute
5668
```
5769

58-
Stub smoke tests cover all four adapters, and live Hub validation has exercised the `DevAgentAdapter`
59-
path against a real GitHub repository.
70+
Adapter maturity today:
71+
72+
- `DevAgentAdapter`
73+
- live-validated and supported for the MVP path
74+
- `CodexAdapter`
75+
- `ClaudeAdapter`
76+
- `OpenCodeAdapter`
77+
- adapter-present and smoke-tested, but still experimental
78+
79+
Treat the experimental adapters as development surfaces, not production-equivalent executor paths.
6080

6181
## Development
6282

bun.lock

Lines changed: 11 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/local-runner/src/index.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,22 @@ test("local runner supports cancellation from a fresh process instance", async (
367367
assert.equal(result.status, "cancelled");
368368
});
369369

370+
test("local runner fails a run that exceeds timeoutSec", async () => {
371+
const repo = await createRepo();
372+
const runner = new LocalRunner({
373+
adapters: [new SleepingAdapter()],
374+
});
375+
const request = createRequest(repo, "task-timeout");
376+
request.constraints.timeoutSec = 1;
377+
378+
const { runId } = await runner.startTask(request);
379+
const result = await runner.awaitResult(runId);
380+
381+
assert.equal(result.status, "failed");
382+
assert.equal(result.error?.code, "EXECUTION_FAILED");
383+
assert.equal(result.error?.message, "Task exceeded timeoutSec (1)");
384+
});
385+
370386
test("local runner reads finished runs from persisted metadata", async () => {
371387
const repo = await createRepo();
372388
const runner = new LocalRunner({

packages/local-runner/src/index.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717
TaskExecutionResult,
1818
WorkspaceSpec,
1919
} from "@devagent-sdk/types";
20+
import { PROTOCOL_VERSION } from "@devagent-sdk/types";
2021

2122
type RunMetadata = {
2223
runId: string;
@@ -151,6 +152,24 @@ function safeWorkspaceName(workBranch: string): string {
151152
return normalized || "workspace";
152153
}
153154

155+
function createTimeoutResult(request: TaskExecutionRequest, startedAt: string): TaskExecutionResult {
156+
return {
157+
protocolVersion: PROTOCOL_VERSION,
158+
taskId: request.taskId,
159+
status: "failed",
160+
artifacts: [],
161+
metrics: {
162+
startedAt,
163+
finishedAt: new Date().toISOString(),
164+
durationMs: Date.now() - new Date(startedAt).getTime(),
165+
},
166+
error: {
167+
code: "EXECUTION_FAILED",
168+
message: `Task exceeded timeoutSec (${request.constraints.timeoutSec})`,
169+
},
170+
};
171+
}
172+
154173
export class FileSystemWorkspaceManager implements WorkspaceManager {
155174
async prepare(spec: WorkspaceSpec): Promise<{ workspacePath: string }> {
156175
const runnerRoot = workspaceRootFor(spec.sourceRepoPath);
@@ -282,9 +301,35 @@ export class LocalRunner implements RunnerClient {
282301
await writeFile(join(runsDir, `${handle.id}.json`), JSON.stringify(metadata, null, 2));
283302
this.knownRuns.set(handle.id, metadata);
284303

304+
const startedAt = new Date().toISOString();
305+
const resultPromise = handle.wait();
306+
const timedPromise = request.constraints.timeoutSec && request.constraints.timeoutSec > 0
307+
? new Promise<TaskExecutionResult>((resolve) => {
308+
const timeoutMs = request.constraints.timeoutSec! * 1000;
309+
const timer = setTimeout(async () => {
310+
try {
311+
await handle.cancel();
312+
} catch {
313+
// Best-effort cancel.
314+
}
315+
const timeoutResult = createTimeoutResult(request, startedAt);
316+
onEvent({
317+
protocolVersion: PROTOCOL_VERSION,
318+
type: "completed",
319+
at: timeoutResult.metrics.finishedAt,
320+
taskId: request.taskId,
321+
status: timeoutResult.status,
322+
});
323+
resolve(timeoutResult);
324+
}, timeoutMs);
325+
326+
void resultPromise.finally(() => clearTimeout(timer));
327+
})
328+
: null;
329+
285330
const wrappedHandle = new LocalRunHandle(
286331
handle.id,
287-
handle.wait().then(async (result: TaskExecutionResult) => {
332+
Promise.race([resultPromise, ...(timedPromise ? [timedPromise] : [])]).then(async (result: TaskExecutionResult) => {
288333
metadata.status = result.status;
289334
await writeFile(resultPath, JSON.stringify(result, null, 2));
290335
await writeFile(join(runsDir, `${handle.id}.json`), JSON.stringify(metadata, null, 2));

0 commit comments

Comments
 (0)