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
6 changes: 3 additions & 3 deletions docs/development/task-backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,9 +337,9 @@ Status markers:

**Tasks:**

- [ ] Define adapter capabilities for `executeTask`, `cancelTask`, and `getCapabilities`.
- [ ] Define streamed task events for stdout, stderr, tool events, diff, completion, and failure.
- [ ] Commit with message `feat: define runtime adapter interface`.
- [x] Define adapter capabilities for `executeTask`, `cancelTask`, and `getCapabilities`.
- [x] Define streamed task events for stdout, stderr, tool events, diff, completion, and failure.
- [x] Commit with message `feat: define runtime adapter interface`.

**Acceptance Criteria:**

Expand Down
73 changes: 73 additions & 0 deletions packages/runtime-adapters/src/execution/runtime-adapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, expect, it } from "vitest";

import {
createRuntimeAdapterCapabilities,
type RuntimeAdapter,
type RuntimeTaskRequest,
} from "./runtime-adapter.js";
import type { TaskEvent } from "./task-events.js";

describe("RuntimeAdapter contract", () => {
it("allows Codex, Claude, and shell execution through one orchestration interface", async () => {
const adapters: RuntimeAdapter[] = [
createFakeAdapter("codex"),
createFakeAdapter("claude"),
createFakeAdapter("shell"),
];
const request: RuntimeTaskRequest = {
taskId: "task-1",
prompt: "Review the diff",
workspaceRoot: "/tmp/agentdeck",
metadata: {
source: "test",
},
};

const results = await Promise.all(adapters.map((adapter) => adapter.executeTask(request)));

expect(results.map((result) => result.status)).toEqual(["completed", "completed", "completed"]);
expect(adapters.every((adapter) => adapter.getCapabilities().executeTask)).toBe(true);
expect(await adapters[0]?.cancelTask("task-1")).toEqual({ cancelled: true });
});

it("normalizes adapter capabilities", () => {
expect(createRuntimeAdapterCapabilities({ streaming: true, tools: ["shell"] })).toEqual({
executeTask: true,
cancelTask: true,
streaming: true,
tools: ["shell"],
});
});
});

function createFakeAdapter(id: "codex" | "claude" | "shell"): RuntimeAdapter {
const events: TaskEvent[] = [];

return {
id,
name: `${id} adapter`,
runtimeType: id === "shell" ? "node" : id,
async executeTask(request) {
events.push({
id: "event-1",
taskId: request.taskId,
type: "task.completed",
createdAt: "2026-05-29T00:00:00.000Z",
payload: {
exitCode: 0,
},
});
return {
taskId: request.taskId,
status: "completed",
events,
};
},
async cancelTask(taskId) {
return { cancelled: taskId === "task-1" };
},
getCapabilities() {
return createRuntimeAdapterCapabilities();
},
};
}
51 changes: 51 additions & 0 deletions packages/runtime-adapters/src/execution/runtime-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { RuntimeType } from "../runtime-types.js";
import type { TaskEvent } from "./task-events.js";

export type RuntimeAdapterId = "codex" | "claude" | "shell" | string;

export type RuntimeTaskStatus = "pending" | "running" | "completed" | "failed" | "cancelled";

export interface RuntimeTaskRequest {
readonly taskId: string;
readonly prompt: string;
readonly workspaceRoot: string;
readonly metadata?: Readonly<Record<string, string>>;
}

export interface RuntimeTaskResult {
readonly taskId: string;
readonly status: RuntimeTaskStatus;
readonly events: readonly TaskEvent[];
}

export interface RuntimeTaskCancellation {
readonly cancelled: boolean;
readonly reason?: string;
}

export interface RuntimeAdapterCapabilities {
readonly executeTask: boolean;
readonly cancelTask: boolean;
readonly streaming: boolean;
readonly tools: readonly string[];
}

export interface RuntimeAdapter {
readonly id: RuntimeAdapterId;
readonly name: string;
readonly runtimeType: RuntimeType;
readonly executeTask: (request: RuntimeTaskRequest) => Promise<RuntimeTaskResult>;
readonly cancelTask: (taskId: string) => Promise<RuntimeTaskCancellation>;
readonly getCapabilities: () => RuntimeAdapterCapabilities;
}

export function createRuntimeAdapterCapabilities(
input: Partial<RuntimeAdapterCapabilities> = {},
): RuntimeAdapterCapabilities {
return {
executeTask: input.executeTask ?? true,
cancelTask: input.cancelTask ?? true,
streaming: input.streaming ?? false,
tools: input.tools ?? [],
};
}
65 changes: 65 additions & 0 deletions packages/runtime-adapters/src/execution/task-events.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { describe, expect, it } from "vitest";

import { TASK_EVENT_TYPES, createTaskEvent } from "./task-events.js";

describe("task events", () => {
it("defines streamed task event types for execution adapters", () => {
expect(TASK_EVENT_TYPES).toEqual([
"task.stdout",
"task.stderr",
"task.tool",
"task.diff",
"task.completed",
"task.failed",
]);
});

it("creates normalized stdout, stderr, tool, diff, completion, and failure events", () => {
expect(
[
createTaskEvent({
id: "stdout-1",
taskId: "task-1",
type: "task.stdout",
createdAt: "2026-05-29T00:00:00.000Z",
payload: { text: "hello" },
}),
createTaskEvent({
id: "stderr-1",
taskId: "task-1",
type: "task.stderr",
createdAt: "2026-05-29T00:00:00.000Z",
payload: { text: "warn" },
}),
createTaskEvent({
id: "tool-1",
taskId: "task-1",
type: "task.tool",
createdAt: "2026-05-29T00:00:00.000Z",
payload: { toolName: "git.diff", status: "completed" },
}),
createTaskEvent({
id: "diff-1",
taskId: "task-1",
type: "task.diff",
createdAt: "2026-05-29T00:00:00.000Z",
payload: { diff: "diff --git" },
}),
createTaskEvent({
id: "completed-1",
taskId: "task-1",
type: "task.completed",
createdAt: "2026-05-29T00:00:00.000Z",
payload: { exitCode: 0 },
}),
createTaskEvent({
id: "failed-1",
taskId: "task-1",
type: "task.failed",
createdAt: "2026-05-29T00:00:00.000Z",
payload: { message: "failed" },
}),
].map((event) => event.type),
).toEqual(TASK_EVENT_TYPES);
});
});
44 changes: 44 additions & 0 deletions packages/runtime-adapters/src/execution/task-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export const TASK_EVENT_TYPES = [
"task.stdout",
"task.stderr",
"task.tool",
"task.diff",
"task.completed",
"task.failed",
] as const;

export type TaskEventType = (typeof TASK_EVENT_TYPES)[number];

export interface TaskEventBase<TType extends TaskEventType, TPayload> {
readonly id: string;
readonly taskId: string;
readonly type: TType;
readonly createdAt: string;
readonly payload: TPayload;
}

export type TaskStdoutEvent = TaskEventBase<"task.stdout", { readonly text: string }>;
export type TaskStderrEvent = TaskEventBase<"task.stderr", { readonly text: string }>;
export type TaskToolEvent = TaskEventBase<
"task.tool",
{
readonly toolName: string;
readonly status: "started" | "completed" | "failed";
readonly message?: string;
}
>;
export type TaskDiffEvent = TaskEventBase<"task.diff", { readonly diff: string }>;
export type TaskCompletedEvent = TaskEventBase<"task.completed", { readonly exitCode: number }>;
export type TaskFailedEvent = TaskEventBase<"task.failed", { readonly message: string }>;

export type TaskEvent =
| TaskStdoutEvent
| TaskStderrEvent
| TaskToolEvent
| TaskDiffEvent
| TaskCompletedEvent
| TaskFailedEvent;

export function createTaskEvent<TEvent extends TaskEvent>(event: TEvent): TEvent {
return event;
}
24 changes: 24 additions & 0 deletions packages/runtime-adapters/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,27 @@ export { detectGitRuntime, type DetectGitRuntimeOptions } from "./detectors/git.
export { detectLmStudioRuntime, type DetectLmStudioRuntimeOptions } from "./detectors/lmstudio.js";
export { detectNodeRuntime, type DetectNodeRuntimeOptions } from "./detectors/node.js";
export { detectOllamaRuntime, type DetectOllamaRuntimeOptions } from "./detectors/ollama.js";

export { createRuntimeAdapterCapabilities } from "./execution/runtime-adapter.js";
export type {
RuntimeAdapter,
RuntimeAdapterCapabilities,
RuntimeAdapterId,
RuntimeTaskCancellation,
RuntimeTaskRequest,
RuntimeTaskResult,
RuntimeTaskStatus,
} from "./execution/runtime-adapter.js";

export { TASK_EVENT_TYPES, createTaskEvent } from "./execution/task-events.js";
export type {
TaskCompletedEvent,
TaskDiffEvent,
TaskEvent,
TaskEventBase,
TaskEventType,
TaskFailedEvent,
TaskStderrEvent,
TaskStdoutEvent,
TaskToolEvent,
} from "./execution/task-events.js";