diff --git a/src/lib/execution/execution-backend.ts b/src/lib/execution/execution-backend.ts new file mode 100644 index 0000000..35e2ba8 --- /dev/null +++ b/src/lib/execution/execution-backend.ts @@ -0,0 +1,13 @@ +import type { + ExecutionBackendDescription, + ExecutionImmediateResult, + ExecutionQueuedResult, + ExecutionRequest, +} from "@/lib/execution/types"; + +export interface ExecutionBackend { + backendId: string; + canHandle(input: ExecutionRequest): Promise | boolean; + execute(input: ExecutionRequest): Promise; + describe(input: ExecutionRequest): Promise; +} diff --git a/src/lib/execution/orchestration-backend.ts b/src/lib/execution/orchestration-backend.ts new file mode 100644 index 0000000..aca6412 --- /dev/null +++ b/src/lib/execution/orchestration-backend.ts @@ -0,0 +1,4 @@ +export interface OrchestrationBackend { + backendId: string; + describe(): Promise<{ backendId: string; label: string }>; +} diff --git a/src/lib/execution/types.ts b/src/lib/execution/types.ts new file mode 100644 index 0000000..56f44e2 --- /dev/null +++ b/src/lib/execution/types.ts @@ -0,0 +1,104 @@ +import type { ReasoningEffort } from "@/lib/boss-data"; + +export type ExecutionRequestKind = + | "master_agent_reply" + | "thread_reply" + | "dispatch_execution" + | "attachment_analysis"; + +export interface ExecutionRequest { + kind: ExecutionRequestKind; + projectId: string; + requestMessageId: string; + body: string; + requestedByAccount?: string; + requestedByLabel?: string; + taskId?: string; + targetThreadId?: string; + targetProjectId?: string; + modelOverride?: string; + reasoningEffortOverride?: ReasoningEffort; +} + +export interface ExecutionQueuedResult { + status: "queued" | "running"; + taskId: string; + backendId: string; + sessionId?: string; +} + +export interface ExecutionImmediateCompletedResult { + status: "completed"; + backendId: string; + output: string; +} + +export interface ExecutionImmediateFailedResult { + status: "failed"; + backendId: string; + error: string; +} + +export type ExecutionImmediateResult = + | ExecutionImmediateCompletedResult + | ExecutionImmediateFailedResult; + +export interface ExecutionBackendDescription { + backendId: string; + label: string; + mode: "local" | "remote" | "api"; +} + +export function createExecutionRequest(input: ExecutionRequest): ExecutionRequest { + return { + kind: input.kind, + projectId: input.projectId, + requestMessageId: input.requestMessageId, + body: input.body, + requestedByAccount: input.requestedByAccount ?? undefined, + requestedByLabel: input.requestedByLabel ?? undefined, + taskId: input.taskId ?? undefined, + targetThreadId: input.targetThreadId ?? undefined, + targetProjectId: input.targetProjectId ?? undefined, + modelOverride: input.modelOverride ?? undefined, + reasoningEffortOverride: input.reasoningEffortOverride ?? undefined, + }; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function hasStringProperty(value: Record, key: string): boolean { + return typeof value[key] === "string" && value[key].length > 0; +} + +export function isQueuedExecutionResult(value: unknown): value is ExecutionQueuedResult { + if (!isRecord(value)) { + return false; + } + + if (value.status !== "queued" && value.status !== "running") { + return false; + } + + return hasStringProperty(value, "taskId") && hasStringProperty(value, "backendId"); +} + +export function isImmediateExecutionResult( + value: unknown, +): value is ExecutionImmediateResult { + if (!isRecord(value)) { + return false; + } + + if (value.status === "completed") { + return hasStringProperty(value, "backendId") && hasStringProperty(value, "output"); + } + + if (value.status === "failed") { + return hasStringProperty(value, "backendId") && hasStringProperty(value, "error"); + } + + return false; +} diff --git a/tests/execution-foundation-contracts.test.ts b/tests/execution-foundation-contracts.test.ts new file mode 100644 index 0000000..f030f2c --- /dev/null +++ b/tests/execution-foundation-contracts.test.ts @@ -0,0 +1,76 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import type { ExecutionImmediateResult } from "@/lib/execution/types"; +import { + createExecutionRequest, + isImmediateExecutionResult, + isQueuedExecutionResult, +} from "@/lib/execution/types"; + +test("ExecutionRequest 工厂会生成稳定默认字段", () => { + const request = createExecutionRequest({ + kind: "master_agent_reply", + projectId: "master-agent", + requestMessageId: "msg-1", + body: "你好", + }); + + assert.equal(request.kind, "master_agent_reply"); + assert.equal(request.projectId, "master-agent"); + assert.equal(request.requestMessageId, "msg-1"); + assert.equal(request.body, "你好"); + assert.equal(request.targetProjectId, undefined); + assert.equal(request.targetThreadId, undefined); + assert.equal(Object.prototype.hasOwnProperty.call(request, "requestedByAccount"), true); + assert.equal(Object.prototype.hasOwnProperty.call(request, "requestedByLabel"), true); + assert.equal(Object.prototype.hasOwnProperty.call(request, "taskId"), true); + assert.equal(Object.prototype.hasOwnProperty.call(request, "targetProjectId"), true); + assert.equal(Object.prototype.hasOwnProperty.call(request, "targetThreadId"), true); + assert.equal(Object.prototype.hasOwnProperty.call(request, "modelOverride"), true); + assert.equal(Object.prototype.hasOwnProperty.call(request, "reasoningEffortOverride"), true); +}); + +test("ExecutionResult 类型守卫能区分 queued 与 immediate", () => { + const queued = { + status: "queued", + taskId: "task-1", + backendId: "master-codex-node", + }; + const running = { + status: "running", + taskId: "task-2", + backendId: "master-codex-node", + sessionId: "session-1", + }; + const completed: ExecutionImmediateResult = { + status: "completed", + backendId: "openai-api", + output: "done", + }; + const failed: ExecutionImmediateResult = { + status: "failed", + backendId: "openai-api", + error: "boom", + }; + const invalidCompleted = { + status: "completed", + backendId: "openai-api", + }; + const invalidFailed = { + status: "failed", + backendId: "openai-api", + }; + + assert.equal(isQueuedExecutionResult(queued), true); + assert.equal(isQueuedExecutionResult(running), true); + assert.equal(isImmediateExecutionResult(queued), false); + assert.equal(isImmediateExecutionResult(running), false); + assert.equal(isQueuedExecutionResult(completed), false); + assert.equal(isImmediateExecutionResult(completed), true); + assert.equal(isQueuedExecutionResult(failed), false); + assert.equal(isImmediateExecutionResult(failed), true); + assert.equal(isImmediateExecutionResult(invalidCompleted), false); + assert.equal(isImmediateExecutionResult(invalidFailed), false); + assert.equal(isQueuedExecutionResult(null), false); + assert.equal(isImmediateExecutionResult(undefined), false); +});