feat: add execution foundation contracts

This commit is contained in:
kris
2026-04-02 21:37:10 +08:00
parent 5e23e1d408
commit e348d6cc5d
4 changed files with 197 additions and 0 deletions

View 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>;
}

View File

@@ -0,0 +1,4 @@
export interface OrchestrationBackend {
backendId: string;
describe(): Promise<{ backendId: string; label: string }>;
}

104
src/lib/execution/types.ts Normal file
View 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;
}

View 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);
});