feat: 实现 Queue Q1 API 和 CLI 骨架
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user