feat: add execution foundation contracts
This commit is contained in:
13
src/lib/execution/execution-backend.ts
Normal file
13
src/lib/execution/execution-backend.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type {
|
||||
ExecutionBackendDescription,
|
||||
ExecutionImmediateResult,
|
||||
ExecutionQueuedResult,
|
||||
ExecutionRequest,
|
||||
} from "@/lib/execution/types";
|
||||
|
||||
export interface ExecutionBackend {
|
||||
backendId: string;
|
||||
canHandle(input: ExecutionRequest): Promise<boolean> | boolean;
|
||||
execute(input: ExecutionRequest): Promise<ExecutionQueuedResult | ExecutionImmediateResult>;
|
||||
describe(input: ExecutionRequest): Promise<ExecutionBackendDescription>;
|
||||
}
|
||||
4
src/lib/execution/orchestration-backend.ts
Normal file
4
src/lib/execution/orchestration-backend.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface OrchestrationBackend {
|
||||
backendId: string;
|
||||
describe(): Promise<{ backendId: string; label: string }>;
|
||||
}
|
||||
104
src/lib/execution/types.ts
Normal file
104
src/lib/execution/types.ts
Normal file
@@ -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<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function hasStringProperty(value: Record<string, unknown>, 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;
|
||||
}
|
||||
76
tests/execution-foundation-contracts.test.ts
Normal file
76
tests/execution-foundation-contracts.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user