Skip to content

Commit 8ad1b3c

Browse files
committed
Initial runner implementation
0 parents  commit 8ad1b3c

19 files changed

Lines changed: 1830 additions & 0 deletions

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules/
2+
dist/
3+
*.tsbuildinfo
4+
.devagent-runner/

README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# DevAgent Runner
2+
3+
Local execution substrate for DevAgent workflow tasks.
4+
5+
## Responsibilities
6+
7+
- prepare isolated workspaces
8+
- launch executor adapters
9+
- stream normalized SDK events
10+
- persist run artifacts and event logs
11+
- support cancellation and cleanup
12+
13+
## Packages
14+
15+
- `@devagent-runner/core`
16+
Shared runner interfaces and error model.
17+
- `@devagent-runner/local-runner`
18+
Local runner implementation with filesystem-backed workspaces, artifacts, and event logs.
19+
- `@devagent-runner/adapters`
20+
CLI adapters for `devagent`, `codex`, `claude`, and `opencode`.
21+
- `@devagent-runner/cli`
22+
Debug CLI for local runs.
23+
24+
## Filesystem Layout
25+
26+
Runner state is created under the source repo:
27+
28+
```text
29+
.devagent-runner/
30+
artifacts/<taskId>/
31+
events/<taskId>.jsonl
32+
runs/<runId>.json
33+
workspaces/<sanitized-work-branch>/
34+
```
35+
36+
## CLI
37+
38+
```bash
39+
devagent-runner run --request request.json
40+
devagent-runner cancel <run-id>
41+
devagent-runner inspect <run-id>
42+
```
43+
44+
## Local Development Wiring
45+
46+
For local MVP work this repo consumes `@devagent-sdk/*` through file dependencies from
47+
`../devagent-sdk`, and `devagent-hub` consumes this runner through file dependencies from
48+
`../devagent-runner/packages/*`.
49+
50+
## Validated Flow
51+
52+
The runner has been validated in the canonical path:
53+
54+
```text
55+
devagent-hub -> LocalRunnerClient -> LocalRunner -> DevAgentAdapter -> devagent execute
56+
```
57+
58+
Stub smoke tests cover all four adapters, and live Hub validation has exercised the `DevAgentAdapter`
59+
path against a real GitHub repository.
60+
61+
## Development
62+
63+
```bash
64+
bun install
65+
bun run typecheck
66+
bun run test
67+
```

bun.lock

Lines changed: 106 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "devagent-runner",
3+
"private": true,
4+
"type": "module",
5+
"packageManager": "bun@1.3.10",
6+
"workspaces": [
7+
"packages/*"
8+
],
9+
"scripts": {
10+
"build": "bunx tsc -b packages/core packages/local-runner packages/adapters packages/cli",
11+
"typecheck": "bunx tsc -b --pretty false packages/core packages/local-runner packages/adapters packages/cli",
12+
"test": "bun run build && bun test ./packages/*/dist/*.test.js"
13+
},
14+
"devDependencies": {
15+
"@types/bun": "^1.3.10",
16+
"@types/node": "^24.3.0",
17+
"typescript": "^5.9.3"
18+
}
19+
}

packages/adapters/package.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "@devagent-runner/adapters",
3+
"version": "0.1.0",
4+
"type": "module",
5+
"main": "dist/index.js",
6+
"types": "dist/index.d.ts",
7+
"exports": {
8+
".": {
9+
"types": "./dist/index.d.ts",
10+
"default": "./dist/index.js"
11+
}
12+
},
13+
"dependencies": {
14+
"@devagent-runner/core": "file:../core",
15+
"@devagent-sdk/types": "file:../../../devagent-sdk/packages/types"
16+
},
17+
"scripts": {
18+
"build": "bunx tsc -p tsconfig.json",
19+
"typecheck": "bunx tsc -p tsconfig.json --noEmit"
20+
}
21+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import test from "node:test";
2+
import assert from "node:assert/strict";
3+
import { join } from "node:path";
4+
import { chmod, mkdtemp, mkdir, readFile, writeFile } from "node:fs/promises";
5+
import { tmpdir } from "node:os";
6+
import {
7+
ClaudeAdapter,
8+
CodexAdapter,
9+
DevAgentAdapter,
10+
OpenCodeAdapter,
11+
} from "./index.js";
12+
import type { ExecutorAdapter } from "@devagent-runner/core";
13+
import type { TaskExecutionEvent, TaskExecutionRequest, TaskExecutionResult } from "@devagent-sdk/types";
14+
import { PROTOCOL_VERSION } from "@devagent-sdk/types";
15+
16+
async function createWorkspace(): Promise<{ root: string; artifactDir: string; workspacePath: string }> {
17+
const root = await mkdtemp(join(tmpdir(), "devagent-adapter-"));
18+
const artifactDir = join(root, "artifacts");
19+
const workspacePath = join(root, "workspace");
20+
await mkdir(artifactDir, { recursive: true });
21+
await mkdir(workspacePath, { recursive: true });
22+
return { root, artifactDir, workspacePath };
23+
}
24+
25+
function createRequest(executorId: TaskExecutionRequest["executor"]["executorId"]): TaskExecutionRequest {
26+
return {
27+
protocolVersion: PROTOCOL_VERSION,
28+
taskId: `task-${executorId}`,
29+
taskType: "triage",
30+
project: { id: "p1", name: "repo" },
31+
workItem: { kind: "github-issue", externalId: "1", title: "Smoke test" },
32+
workspace: {
33+
sourceRepoPath: "/tmp/repo",
34+
workBranch: `devagent/${executorId}/task`,
35+
isolation: "temp-copy",
36+
},
37+
executor: { executorId, model: "test-model" },
38+
constraints: {},
39+
context: { summary: "smoke" },
40+
expectedArtifacts: ["triage-report"],
41+
};
42+
}
43+
44+
async function createStub(path: string, contents: string): Promise<void> {
45+
await writeFile(path, contents);
46+
await chmod(path, 0o755);
47+
}
48+
49+
async function collectEvents(
50+
adapter: ExecutorAdapter,
51+
request: TaskExecutionRequest,
52+
workspacePath: string,
53+
artifactDir: string,
54+
): Promise<{ events: TaskExecutionEvent[]; result: TaskExecutionResult }> {
55+
const events: TaskExecutionEvent[] = [];
56+
const handle = await adapter.launch(request, workspacePath, artifactDir, (event) => {
57+
events.push(event);
58+
});
59+
return {
60+
events,
61+
result: await handle.wait(),
62+
};
63+
}
64+
65+
test("DevAgentAdapter smoke test with stub executable", async () => {
66+
const { root, artifactDir, workspacePath } = await createWorkspace();
67+
const stubPath = join(root, "devagent-stub.js");
68+
await createStub(stubPath, `#!/usr/bin/env node
69+
const fs = require("fs");
70+
const path = require("path");
71+
const args = process.argv.slice(2);
72+
const requestPath = args[args.indexOf("--request") + 1];
73+
const artifactDir = args[args.indexOf("--artifact-dir") + 1];
74+
const request = JSON.parse(fs.readFileSync(requestPath, "utf8"));
75+
const artifactPath = path.join(artifactDir, "triage-report.md");
76+
const resultPath = path.join(artifactDir, "result.json");
77+
fs.writeFileSync(artifactPath, "# Triage\\n\\nStub output\\n");
78+
process.stdout.write(JSON.stringify({ protocolVersion: "0.1", type: "started", at: new Date().toISOString(), taskId: request.taskId }) + "\\n");
79+
process.stdout.write(JSON.stringify({ protocolVersion: "0.1", type: "artifact", at: new Date().toISOString(), taskId: request.taskId, artifact: { kind: "triage-report", path: artifactPath, createdAt: new Date().toISOString() } }) + "\\n");
80+
fs.writeFileSync(resultPath, JSON.stringify({ protocolVersion: "0.1", taskId: request.taskId, status: "success", artifacts: [{ kind: "triage-report", path: artifactPath, createdAt: new Date().toISOString() }], metrics: { startedAt: new Date().toISOString(), finishedAt: new Date().toISOString(), durationMs: 1 } }, null, 2));
81+
process.stdout.write(JSON.stringify({ protocolVersion: "0.1", type: "completed", at: new Date().toISOString(), taskId: request.taskId, status: "success" }) + "\\n");
82+
`);
83+
84+
const { events, result } = await collectEvents(
85+
new DevAgentAdapter(`${process.execPath} ${stubPath}`),
86+
createRequest("devagent"),
87+
workspacePath,
88+
artifactDir,
89+
);
90+
91+
assert.equal(result.status, "success");
92+
assert.deepEqual(events.map((event) => event.type), ["started", "artifact", "completed"]);
93+
});
94+
95+
test("DevAgentAdapter reports cancelled runs as cancelled", async () => {
96+
const { root, artifactDir, workspacePath } = await createWorkspace();
97+
const stubPath = join(root, "devagent-cancel-stub.js");
98+
await createStub(stubPath, `#!/usr/bin/env node
99+
setTimeout(() => process.exit(0), 10000);
100+
`);
101+
102+
const events: TaskExecutionEvent[] = [];
103+
const handle = await new DevAgentAdapter(`${process.execPath} ${stubPath}`).launch(
104+
createRequest("devagent"),
105+
workspacePath,
106+
artifactDir,
107+
(event) => {
108+
events.push(event);
109+
},
110+
);
111+
112+
await handle.cancel();
113+
const result = await handle.wait();
114+
115+
assert.equal(result.status, "cancelled");
116+
assert.equal(result.error?.code, "CANCELLED");
117+
assert.equal(events.length, 0);
118+
});
119+
120+
test("CodexAdapter smoke test with stub executable", async () => {
121+
const { root, artifactDir, workspacePath } = await createWorkspace();
122+
const stubPath = join(root, "codex-stub.js");
123+
await createStub(stubPath, `#!/usr/bin/env node
124+
const fs = require("fs");
125+
const args = process.argv.slice(2);
126+
const outIndex = args.indexOf("-o");
127+
if (outIndex >= 0) fs.writeFileSync(args[outIndex + 1], "stub codex output\\n");
128+
process.stdout.write("{\\"type\\":\\"result\\",\\"message\\":\\"ok\\"}\\n");
129+
`);
130+
131+
const { events, result } = await collectEvents(
132+
new CodexAdapter(`${process.execPath} ${stubPath}`),
133+
createRequest("codex"),
134+
workspacePath,
135+
artifactDir,
136+
);
137+
138+
assert.equal(result.status, "success");
139+
assert.equal(events.at(-1)?.type, "completed");
140+
assert.match(await readFile(join(artifactDir, "triage-report.md"), "utf8"), /stub codex output/);
141+
});
142+
143+
test("ClaudeAdapter smoke test with stub executable", async () => {
144+
const { root, artifactDir, workspacePath } = await createWorkspace();
145+
const stubPath = join(root, "claude-stub.js");
146+
await createStub(stubPath, `#!/usr/bin/env node
147+
process.stdout.write("claude stub output\\n");
148+
`);
149+
150+
const { events, result } = await collectEvents(
151+
new ClaudeAdapter(`${process.execPath} ${stubPath}`),
152+
createRequest("claude"),
153+
workspacePath,
154+
artifactDir,
155+
);
156+
157+
assert.equal(result.status, "success");
158+
assert.equal(events.at(-1)?.type, "completed");
159+
});
160+
161+
test("OpenCodeAdapter smoke test with stub executable", async () => {
162+
const { root, artifactDir, workspacePath } = await createWorkspace();
163+
const stubPath = join(root, "opencode-stub.js");
164+
await createStub(stubPath, `#!/usr/bin/env node
165+
process.stdout.write("opencode stub output\\n");
166+
`);
167+
168+
const { events, result } = await collectEvents(
169+
new OpenCodeAdapter(`${process.execPath} ${stubPath}`),
170+
createRequest("opencode"),
171+
workspacePath,
172+
artifactDir,
173+
);
174+
175+
assert.equal(result.status, "success");
176+
assert.equal(events.at(-1)?.type, "completed");
177+
});

0 commit comments

Comments
 (0)