feat: 实现 Queue Q1 API 和 CLI 骨架

This commit is contained in:
Codex
2026-06-01 22:20:09 +08:00
parent f4a26e5961
commit 237b10c4da
11 changed files with 566 additions and 14 deletions
+71
View File
@@ -23,6 +23,7 @@ export type RunStatus = "pending" | "claimed" | "running" | "completed" | "faile
export type CommandState = "pending" | "acknowledged" | "completed" | "failed" | "cancelled";
export type TerminalStatus = "completed" | "failed" | "blocked" | "cancelled";
export type BackendProfile = "codex" | "deepseek";
export type QueueTaskState = "pending" | "running" | "completed" | "failed" | "blocked" | "cancelled";
export interface WorkspaceRef extends JsonRecord {
kind: "git-worktree" | "host-path" | "kubernetes-pvc" | "opaque";
@@ -168,6 +169,76 @@ export interface RunnerJobRecord extends JsonRecord {
updatedAt: string;
}
export interface QueueAttemptRef extends JsonRecord {
attemptId: string;
state: QueueTaskState;
runId: string | null;
commandId: string | null;
runnerJobId: string | null;
sessionId: string | null;
sessionPath: string | null;
}
export interface CreateQueueTaskInput extends JsonRecord {
tenantId: string;
projectId: string;
queue: string;
lane: string;
title: string;
priority: number;
backendProfile: BackendProfile;
providerId: string | null;
workspaceRef: WorkspaceRef | null;
executionPolicy: ExecutionPolicy | null;
resourceBundleRef: ResourceBundleRef | null;
payload: JsonRecord;
references: JsonRecord[];
metadata: JsonRecord;
idempotencyKey?: string;
}
export interface QueueTaskRecord extends CreateQueueTaskInput {
id: string;
state: QueueTaskState;
version: number;
payloadHash: string;
latestAttempt: QueueAttemptRef | null;
sessionPath: string | null;
createdAt: string;
updatedAt: string;
cancelledAt: string | null;
cancelReason: string | null;
}
export interface QueueReadCursorRecord extends JsonRecord {
taskId: string;
readerId: string;
taskVersion: number;
readAt: string;
}
export interface QueueTaskListResult extends JsonRecord {
items: QueueTaskRecord[];
count: number;
cursor: string | null;
}
export interface QueueStats extends JsonRecord {
queue: string | null;
total: number;
byState: JsonRecord;
byLane: JsonRecord;
maxVersion: number;
generatedAt: string;
}
export interface QueueCommanderSnapshot extends JsonRecord {
queue: string | null;
stats: QueueStats;
items: QueueTaskRecord[];
generatedAt: string;
}
export interface BackendEvent {
type: EventType;
payload: JsonRecord;
+39 -1
View File
@@ -1,5 +1,5 @@
import { createHash, randomUUID } from "node:crypto";
import type { BackendProfile, CreateCommandInput, CreateRunInput, ExecutionPolicy, JsonRecord, JsonValue, ResourceBundleRef, SecretRef, SessionRef } from "./types.js";
import type { BackendProfile, CreateCommandInput, CreateQueueTaskInput, CreateRunInput, ExecutionPolicy, JsonRecord, JsonValue, QueueTaskState, ResourceBundleRef, SecretRef, SessionRef } from "./types.js";
import { AgentRunError } from "./errors.js";
import { backendProfileSpec, backendProfiles, isBackendProfile } from "./backend-profiles.js";
@@ -168,3 +168,41 @@ export function validateCreateCommand(input: unknown): CreateCommandInput {
const idempotencyKey = typeof record.idempotencyKey === "string" && record.idempotencyKey.trim().length > 0 ? record.idempotencyKey.trim() : undefined;
return { type, payload, ...(idempotencyKey ? { idempotencyKey } : {}) };
}
export function validateCreateQueueTask(input: unknown): CreateQueueTaskInput {
const record = asRecord(input, "queueTask");
const tenantId = requiredString(record, "tenantId");
if (!allowedTenants.has(tenantId)) throw new AgentRunError("tenant-policy-denied", `tenantId ${tenantId} is not allowed`, { httpStatus: 403 });
const backendProfileValue = optionalString(record.backendProfile) ?? "codex";
if (!isBackendProfile(backendProfileValue)) throw new AgentRunError("schema-invalid", `backendProfile ${backendProfileValue} is not supported in v0.1`, { httpStatus: 400, details: { allowedBackends: [...backendProfiles] } });
const queue = optionalString(record.queue) ?? "default";
const lane = optionalString(record.lane) ?? "default";
const priorityValue = record.priority ?? 0;
if (typeof priorityValue !== "number" || !Number.isFinite(priorityValue)) throw new AgentRunError("schema-invalid", "priority must be a finite number", { httpStatus: 400 });
const referencesValue = record.references ?? [];
if (!Array.isArray(referencesValue)) throw new AgentRunError("schema-invalid", "references must be an array", { httpStatus: 400 });
const result: CreateQueueTaskInput = {
tenantId,
projectId: requiredString(record, "projectId"),
queue,
lane,
title: requiredString(record, "title"),
priority: priorityValue,
backendProfile: backendProfileValue,
providerId: optionalString(record.providerId) ?? null,
workspaceRef: record.workspaceRef === undefined || record.workspaceRef === null ? null : requiredRecord(record, "workspaceRef") as CreateQueueTaskInput["workspaceRef"],
executionPolicy: record.executionPolicy === undefined || record.executionPolicy === null ? null : validateExecutionPolicy(requiredRecord(record, "executionPolicy")),
resourceBundleRef: validateResourceBundleRef(record.resourceBundleRef),
payload: record.payload === undefined ? {} : asRecord(record.payload, "payload"),
references: referencesValue.map((item, index) => asRecord(item, `references[${index}]`)),
metadata: record.metadata === undefined ? {} : asRecord(record.metadata, "metadata"),
};
const idempotencyKey = optionalString(record.idempotencyKey);
if (idempotencyKey) result.idempotencyKey = idempotencyKey;
return result;
}
export function validateQueueTaskState(value: string): QueueTaskState {
if (value === "pending" || value === "running" || value === "completed" || value === "failed" || value === "blocked" || value === "cancelled") return value;
throw new AgentRunError("schema-invalid", `queue task state ${value} is not supported`, { httpStatus: 400 });
}