From b70e426238ce8544d8acb2bfc9b8c68b01c25b40 Mon Sep 17 00:00:00 2001 From: eleven_qy <64311644+alph-cmky@users.noreply.github.com> Date: Fri, 29 May 2026 12:42:54 +0800 Subject: [PATCH] feat: define runtime adapter interface --- docs/development/task-backlog.md | 6 +- .../src/execution/runtime-adapter.test.ts | 73 +++++++++++++++++++ .../src/execution/runtime-adapter.ts | 51 +++++++++++++ .../src/execution/task-events.test.ts | 65 +++++++++++++++++ .../src/execution/task-events.ts | 44 +++++++++++ packages/runtime-adapters/src/index.ts | 24 ++++++ 6 files changed, 260 insertions(+), 3 deletions(-) create mode 100644 packages/runtime-adapters/src/execution/runtime-adapter.test.ts create mode 100644 packages/runtime-adapters/src/execution/runtime-adapter.ts create mode 100644 packages/runtime-adapters/src/execution/task-events.test.ts create mode 100644 packages/runtime-adapters/src/execution/task-events.ts diff --git a/docs/development/task-backlog.md b/docs/development/task-backlog.md index 3077b6b..4de7aa9 100644 --- a/docs/development/task-backlog.md +++ b/docs/development/task-backlog.md @@ -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:** diff --git a/packages/runtime-adapters/src/execution/runtime-adapter.test.ts b/packages/runtime-adapters/src/execution/runtime-adapter.test.ts new file mode 100644 index 0000000..9c0d3d8 --- /dev/null +++ b/packages/runtime-adapters/src/execution/runtime-adapter.test.ts @@ -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(); + }, + }; +} diff --git a/packages/runtime-adapters/src/execution/runtime-adapter.ts b/packages/runtime-adapters/src/execution/runtime-adapter.ts new file mode 100644 index 0000000..dd9dc85 --- /dev/null +++ b/packages/runtime-adapters/src/execution/runtime-adapter.ts @@ -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>; +} + +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; + readonly cancelTask: (taskId: string) => Promise; + readonly getCapabilities: () => RuntimeAdapterCapabilities; +} + +export function createRuntimeAdapterCapabilities( + input: Partial = {}, +): RuntimeAdapterCapabilities { + return { + executeTask: input.executeTask ?? true, + cancelTask: input.cancelTask ?? true, + streaming: input.streaming ?? false, + tools: input.tools ?? [], + }; +} diff --git a/packages/runtime-adapters/src/execution/task-events.test.ts b/packages/runtime-adapters/src/execution/task-events.test.ts new file mode 100644 index 0000000..119ce02 --- /dev/null +++ b/packages/runtime-adapters/src/execution/task-events.test.ts @@ -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); + }); +}); diff --git a/packages/runtime-adapters/src/execution/task-events.ts b/packages/runtime-adapters/src/execution/task-events.ts new file mode 100644 index 0000000..b2b4768 --- /dev/null +++ b/packages/runtime-adapters/src/execution/task-events.ts @@ -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 { + 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(event: TEvent): TEvent { + return event; +} diff --git a/packages/runtime-adapters/src/index.ts b/packages/runtime-adapters/src/index.ts index b3b2579..3576c8a 100644 --- a/packages/runtime-adapters/src/index.ts +++ b/packages/runtime-adapters/src/index.ts @@ -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";