mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 18:08:20 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,510 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
import { normalizeStepRunMutation, type StepRunMutation } from '../run/stepRun';
|
||||
import {
|
||||
normalizeToolExecutionStartBarrierRecord,
|
||||
type ToolExecutionStartBarrierRecord,
|
||||
} from './toolExecutionStartBarrier';
|
||||
|
||||
export const TOOL_EXECUTION_FAILURE_RESULT_SCHEMA =
|
||||
'qinglong/tool-execution-failure-result@v1' as const;
|
||||
export const TOOL_EXECUTION_FAILURE_COMPLETION_SCHEMA =
|
||||
'qinglong/tool-execution-failure-completion@v1' as const;
|
||||
export const TOOL_EXECUTION_FAILURE_COMPLETION_COMMAND_SCHEMA =
|
||||
'qinglong/tool-execution-failure-completion-command@v1' as const;
|
||||
export const MAX_TOOL_EXECUTION_FAILURE_COMPLETION_JSON_BYTES = 24 * 1024;
|
||||
|
||||
export const TOOL_EXECUTION_FAILURE_OUTCOMES = ['failed', 'timed_out'] as const;
|
||||
|
||||
export type ToolExecutionFailureOutcome =
|
||||
(typeof TOOL_EXECUTION_FAILURE_OUTCOMES)[number];
|
||||
|
||||
export const TOOL_EXECUTION_FAILURE_FACTS = Object.freeze({
|
||||
failed: Object.freeze({
|
||||
resultCode: 'tool_adapter_failed',
|
||||
errorSummary: 'Trusted Tool execution failed',
|
||||
}),
|
||||
timed_out: Object.freeze({
|
||||
resultCode: 'tool_deadline_exceeded',
|
||||
errorSummary: 'Trusted Tool execution deadline exceeded',
|
||||
}),
|
||||
} satisfies Record<ToolExecutionFailureOutcome, Readonly<{ resultCode: string; errorSummary: string }>>);
|
||||
|
||||
export interface ToolExecutionFailureResult {
|
||||
readonly schema: typeof TOOL_EXECUTION_FAILURE_RESULT_SCHEMA;
|
||||
readonly startId: string;
|
||||
readonly barrierDigest: string;
|
||||
readonly adapterDigest: string;
|
||||
readonly outcome: ToolExecutionFailureOutcome;
|
||||
readonly resultCode: string;
|
||||
readonly errorSummary: string;
|
||||
readonly completedAtMs: number;
|
||||
readonly failureDigest: string;
|
||||
}
|
||||
|
||||
export interface ToolExecutionFailureCompletionRecord {
|
||||
readonly schema: typeof TOOL_EXECUTION_FAILURE_COMPLETION_SCHEMA;
|
||||
readonly startId: string;
|
||||
readonly projectId: string;
|
||||
readonly runId: string;
|
||||
readonly stepRunId: string;
|
||||
readonly startedStepRunVersion: number;
|
||||
readonly completedStepRunVersion: number;
|
||||
readonly barrierDigest: string;
|
||||
readonly adapterDigest: string;
|
||||
readonly outcome: ToolExecutionFailureOutcome;
|
||||
readonly resultCode: string;
|
||||
readonly errorSummary: string;
|
||||
readonly stepRunMutationId: string;
|
||||
readonly stepRunMutationDigest: string;
|
||||
readonly completedStepRunDigest: string;
|
||||
readonly runEventId: string;
|
||||
readonly completedAtMs: number;
|
||||
readonly completionDigest: string;
|
||||
}
|
||||
|
||||
export interface ToolExecutionFailureCompletionCommand {
|
||||
readonly schema: typeof TOOL_EXECUTION_FAILURE_COMPLETION_COMMAND_SCHEMA;
|
||||
readonly barrier: Readonly<ToolExecutionStartBarrierRecord>;
|
||||
readonly failure: Readonly<ToolExecutionFailureResult>;
|
||||
readonly stepRunMutation: Readonly<StepRunMutation>;
|
||||
readonly commandDigest: string;
|
||||
}
|
||||
|
||||
export interface CommitToolExecutionFailureCompletionResult {
|
||||
readonly status: 'created' | 'existing';
|
||||
readonly completion: Readonly<ToolExecutionFailureCompletionRecord>;
|
||||
}
|
||||
|
||||
export interface ToolExecutionFailureCompletionRepository {
|
||||
findByStartId(
|
||||
startId: string,
|
||||
): Promise<Readonly<ToolExecutionFailureCompletionRecord> | null>;
|
||||
commit(
|
||||
command: ToolExecutionFailureCompletionCommand,
|
||||
): Promise<Readonly<CommitToolExecutionFailureCompletionResult>>;
|
||||
}
|
||||
|
||||
export class InvalidToolExecutionFailureCompletionError extends TypeError {
|
||||
readonly code = 'TOOL_EXECUTION_FAILURE_COMPLETION_INVALID';
|
||||
|
||||
constructor(message: string) {
|
||||
super(`Tool execution failure completion is invalid: ${message}`);
|
||||
this.name = 'InvalidToolExecutionFailureCompletionError';
|
||||
}
|
||||
}
|
||||
|
||||
export class ToolExecutionFailureCompletionConflictError extends Error {
|
||||
readonly code = 'TOOL_EXECUTION_FAILURE_COMPLETION_CONFLICT';
|
||||
|
||||
constructor() {
|
||||
super('Tool execution failure completion conflicts with durable state');
|
||||
this.name = 'ToolExecutionFailureCompletionConflictError';
|
||||
}
|
||||
}
|
||||
|
||||
export class ToolExecutionFailureCompletionUnavailableError extends Error {
|
||||
readonly code = 'TOOL_EXECUTION_FAILURE_COMPLETION_UNAVAILABLE';
|
||||
|
||||
constructor(options?: ErrorOptions) {
|
||||
super(
|
||||
'Tool execution failure completion authority is unavailable',
|
||||
options,
|
||||
);
|
||||
this.name = 'ToolExecutionFailureCompletionUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
const IDENTITY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
|
||||
const FAILURE_DIGEST_DOMAIN = Buffer.from(
|
||||
'qinglong/tool-execution-failure-result-digest@v1\0',
|
||||
'utf8',
|
||||
);
|
||||
const COMPLETION_DIGEST_DOMAIN = Buffer.from(
|
||||
'qinglong/tool-execution-failure-completion-digest@v1\0',
|
||||
'utf8',
|
||||
);
|
||||
const COMMAND_DIGEST_DOMAIN = Buffer.from(
|
||||
'qinglong/tool-execution-failure-completion-command-digest@v1\0',
|
||||
'utf8',
|
||||
);
|
||||
|
||||
function invalid(message: string): never {
|
||||
throw new InvalidToolExecutionFailureCompletionError(message);
|
||||
}
|
||||
|
||||
function hash(domain: Uint8Array, value: unknown): string {
|
||||
return createHash('sha256')
|
||||
.update(domain)
|
||||
.update(JSON.stringify(value))
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
function record(value: unknown, label: string): Record<string, unknown> {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
Object.getPrototypeOf(value) !== Object.prototype
|
||||
) {
|
||||
return invalid(`${label} is not a plain object`);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function exactKeys(
|
||||
value: Record<string, unknown>,
|
||||
keys: readonly string[],
|
||||
label: string,
|
||||
): void {
|
||||
const actual = Object.keys(value).sort();
|
||||
const expected = [...keys].sort();
|
||||
if (
|
||||
actual.length !== expected.length ||
|
||||
actual.some((key, index) => key !== expected[index])
|
||||
) {
|
||||
invalid(`${label} shape is invalid`);
|
||||
}
|
||||
}
|
||||
|
||||
function identity(value: unknown, label: string): string {
|
||||
if (typeof value !== 'string' || !IDENTITY_PATTERN.test(value)) {
|
||||
return invalid(`${label} is invalid`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function digest(value: unknown, label: string): string {
|
||||
if (typeof value !== 'string' || !DIGEST_PATTERN.test(value)) {
|
||||
return invalid(`${label} is invalid`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function timestamp(value: unknown, label: string): number {
|
||||
if (!Number.isSafeInteger(value) || (value as number) < 0) {
|
||||
return invalid(`${label} is invalid`);
|
||||
}
|
||||
return value as number;
|
||||
}
|
||||
|
||||
function version(value: unknown, label: string): number {
|
||||
if (
|
||||
!Number.isSafeInteger(value) ||
|
||||
(value as number) < 2 ||
|
||||
(value as number) > 2_147_483_647
|
||||
) {
|
||||
return invalid(`${label} is invalid`);
|
||||
}
|
||||
return value as number;
|
||||
}
|
||||
|
||||
function outcome(value: unknown): ToolExecutionFailureOutcome {
|
||||
if (
|
||||
typeof value !== 'string' ||
|
||||
!TOOL_EXECUTION_FAILURE_OUTCOMES.includes(
|
||||
value as ToolExecutionFailureOutcome,
|
||||
)
|
||||
) {
|
||||
return invalid('failure outcome is invalid');
|
||||
}
|
||||
return value as ToolExecutionFailureOutcome;
|
||||
}
|
||||
|
||||
function exactFailureFacts(
|
||||
value: Readonly<{
|
||||
outcome: ToolExecutionFailureOutcome;
|
||||
resultCode: unknown;
|
||||
errorSummary: unknown;
|
||||
}>,
|
||||
): Readonly<{ resultCode: string; errorSummary: string }> {
|
||||
const expected = TOOL_EXECUTION_FAILURE_FACTS[value.outcome];
|
||||
if (
|
||||
value.resultCode !== expected.resultCode ||
|
||||
value.errorSummary !== expected.errorSummary
|
||||
) {
|
||||
return invalid('failure facts are invalid');
|
||||
}
|
||||
return expected;
|
||||
}
|
||||
|
||||
export function normalizeToolExecutionFailureResult(
|
||||
value: ToolExecutionFailureResult,
|
||||
): Readonly<ToolExecutionFailureResult> {
|
||||
const candidate = record(value, 'failure result');
|
||||
exactKeys(
|
||||
candidate,
|
||||
[
|
||||
'adapterDigest',
|
||||
'barrierDigest',
|
||||
'completedAtMs',
|
||||
'errorSummary',
|
||||
'failureDigest',
|
||||
'outcome',
|
||||
'resultCode',
|
||||
'schema',
|
||||
'startId',
|
||||
],
|
||||
'failure result',
|
||||
);
|
||||
if (value.schema !== TOOL_EXECUTION_FAILURE_RESULT_SCHEMA) {
|
||||
return invalid('failure result schema is invalid');
|
||||
}
|
||||
const normalizedOutcome = outcome(value.outcome);
|
||||
const facts = exactFailureFacts({
|
||||
outcome: normalizedOutcome,
|
||||
resultCode: value.resultCode,
|
||||
errorSummary: value.errorSummary,
|
||||
});
|
||||
const unsigned = Object.freeze({
|
||||
schema: TOOL_EXECUTION_FAILURE_RESULT_SCHEMA,
|
||||
startId: identity(value.startId, 'failure start id'),
|
||||
barrierDigest: digest(value.barrierDigest, 'failure barrier digest'),
|
||||
adapterDigest: digest(value.adapterDigest, 'failure adapter digest'),
|
||||
outcome: normalizedOutcome,
|
||||
resultCode: facts.resultCode,
|
||||
errorSummary: facts.errorSummary,
|
||||
completedAtMs: timestamp(value.completedAtMs, 'failure completion time'),
|
||||
});
|
||||
const failureDigest = digest(value.failureDigest, 'failure digest');
|
||||
if (hash(FAILURE_DIGEST_DOMAIN, unsigned) !== failureDigest) {
|
||||
return invalid('failure digest does not match');
|
||||
}
|
||||
return Object.freeze({ ...unsigned, failureDigest });
|
||||
}
|
||||
|
||||
export function createToolExecutionFailureResult(
|
||||
barrierValue: ToolExecutionStartBarrierRecord,
|
||||
failureOutcome: ToolExecutionFailureOutcome,
|
||||
completedAtMs: number,
|
||||
): Readonly<ToolExecutionFailureResult> {
|
||||
const barrier = normalizeToolExecutionStartBarrierRecord(barrierValue);
|
||||
const normalizedOutcome = outcome(failureOutcome);
|
||||
const facts = TOOL_EXECUTION_FAILURE_FACTS[normalizedOutcome];
|
||||
const unsigned = Object.freeze({
|
||||
schema: TOOL_EXECUTION_FAILURE_RESULT_SCHEMA,
|
||||
startId: barrier.startId,
|
||||
barrierDigest: barrier.barrierDigest,
|
||||
adapterDigest: barrier.adapterDigest,
|
||||
outcome: normalizedOutcome,
|
||||
resultCode: facts.resultCode,
|
||||
errorSummary: facts.errorSummary,
|
||||
completedAtMs: timestamp(completedAtMs, 'failure completion time'),
|
||||
});
|
||||
if (unsigned.completedAtMs < barrier.startedAtMs) {
|
||||
return invalid('failure completion precedes durable start');
|
||||
}
|
||||
return normalizeToolExecutionFailureResult({
|
||||
...unsigned,
|
||||
failureDigest: hash(FAILURE_DIGEST_DOMAIN, unsigned),
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeToolExecutionFailureCompletionRecord(
|
||||
value: ToolExecutionFailureCompletionRecord,
|
||||
): Readonly<ToolExecutionFailureCompletionRecord> {
|
||||
const candidate = record(value, 'failure completion');
|
||||
exactKeys(
|
||||
candidate,
|
||||
[
|
||||
'adapterDigest',
|
||||
'barrierDigest',
|
||||
'completedAtMs',
|
||||
'completedStepRunDigest',
|
||||
'completedStepRunVersion',
|
||||
'completionDigest',
|
||||
'errorSummary',
|
||||
'outcome',
|
||||
'projectId',
|
||||
'resultCode',
|
||||
'runEventId',
|
||||
'runId',
|
||||
'schema',
|
||||
'startId',
|
||||
'startedStepRunVersion',
|
||||
'stepRunId',
|
||||
'stepRunMutationDigest',
|
||||
'stepRunMutationId',
|
||||
],
|
||||
'failure completion',
|
||||
);
|
||||
if (value.schema !== TOOL_EXECUTION_FAILURE_COMPLETION_SCHEMA) {
|
||||
return invalid('failure completion schema is invalid');
|
||||
}
|
||||
const normalizedOutcome = outcome(value.outcome);
|
||||
const facts = exactFailureFacts({
|
||||
outcome: normalizedOutcome,
|
||||
resultCode: value.resultCode,
|
||||
errorSummary: value.errorSummary,
|
||||
});
|
||||
const startedStepRunVersion = version(
|
||||
value.startedStepRunVersion,
|
||||
'started StepRun version',
|
||||
);
|
||||
const completedStepRunVersion = version(
|
||||
value.completedStepRunVersion,
|
||||
'completed StepRun version',
|
||||
);
|
||||
if (completedStepRunVersion !== startedStepRunVersion + 1) {
|
||||
return invalid('failure completion StepRun version fence is invalid');
|
||||
}
|
||||
const unsigned = Object.freeze({
|
||||
schema: TOOL_EXECUTION_FAILURE_COMPLETION_SCHEMA,
|
||||
startId: identity(value.startId, 'failure completion start id'),
|
||||
projectId: identity(value.projectId, 'failure completion project id'),
|
||||
runId: identity(value.runId, 'failure completion Run id'),
|
||||
stepRunId: identity(value.stepRunId, 'failure completion StepRun id'),
|
||||
startedStepRunVersion,
|
||||
completedStepRunVersion,
|
||||
barrierDigest: digest(
|
||||
value.barrierDigest,
|
||||
'failure completion barrier digest',
|
||||
),
|
||||
adapterDigest: digest(
|
||||
value.adapterDigest,
|
||||
'failure completion adapter digest',
|
||||
),
|
||||
outcome: normalizedOutcome,
|
||||
resultCode: facts.resultCode,
|
||||
errorSummary: facts.errorSummary,
|
||||
stepRunMutationId: identity(
|
||||
value.stepRunMutationId,
|
||||
'failure completion mutation id',
|
||||
),
|
||||
stepRunMutationDigest: digest(
|
||||
value.stepRunMutationDigest,
|
||||
'failure completion mutation digest',
|
||||
),
|
||||
completedStepRunDigest: digest(
|
||||
value.completedStepRunDigest,
|
||||
'failed StepRun digest',
|
||||
),
|
||||
runEventId: identity(value.runEventId, 'failure completion Run event id'),
|
||||
completedAtMs: timestamp(value.completedAtMs, 'failure completion time'),
|
||||
});
|
||||
if (
|
||||
Buffer.byteLength(JSON.stringify(unsigned), 'utf8') >
|
||||
MAX_TOOL_EXECUTION_FAILURE_COMPLETION_JSON_BYTES
|
||||
) {
|
||||
return invalid('failure completion exceeds its budget');
|
||||
}
|
||||
const completionDigest = digest(
|
||||
value.completionDigest,
|
||||
'failure completion digest',
|
||||
);
|
||||
if (hash(COMPLETION_DIGEST_DOMAIN, unsigned) !== completionDigest) {
|
||||
return invalid('failure completion digest does not match');
|
||||
}
|
||||
return Object.freeze({ ...unsigned, completionDigest });
|
||||
}
|
||||
|
||||
function completionFromParts(
|
||||
barrier: Readonly<ToolExecutionStartBarrierRecord>,
|
||||
failure: Readonly<ToolExecutionFailureResult>,
|
||||
mutation: Readonly<StepRunMutation>,
|
||||
): Readonly<ToolExecutionFailureCompletionRecord> {
|
||||
const unsigned = Object.freeze({
|
||||
schema: TOOL_EXECUTION_FAILURE_COMPLETION_SCHEMA,
|
||||
startId: barrier.startId,
|
||||
projectId: barrier.projectId,
|
||||
runId: barrier.runId,
|
||||
stepRunId: barrier.stepRunId,
|
||||
startedStepRunVersion: barrier.startedStepRunVersion,
|
||||
completedStepRunVersion: mutation.stepRun.version,
|
||||
barrierDigest: barrier.barrierDigest,
|
||||
adapterDigest: barrier.adapterDigest,
|
||||
outcome: failure.outcome,
|
||||
resultCode: failure.resultCode,
|
||||
errorSummary: failure.errorSummary,
|
||||
stepRunMutationId: mutation.mutationId,
|
||||
stepRunMutationDigest: mutation.mutationDigest,
|
||||
completedStepRunDigest: mutation.stepRun.stepRunDigest,
|
||||
runEventId: mutation.event.id,
|
||||
completedAtMs: failure.completedAtMs,
|
||||
});
|
||||
return normalizeToolExecutionFailureCompletionRecord({
|
||||
...unsigned,
|
||||
completionDigest: hash(COMPLETION_DIGEST_DOMAIN, unsigned),
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeToolExecutionFailureCompletionCommand(
|
||||
value: ToolExecutionFailureCompletionCommand,
|
||||
): Readonly<ToolExecutionFailureCompletionCommand> {
|
||||
const candidate = record(value, 'failure completion command');
|
||||
exactKeys(
|
||||
candidate,
|
||||
['barrier', 'commandDigest', 'failure', 'schema', 'stepRunMutation'],
|
||||
'failure completion command',
|
||||
);
|
||||
if (value.schema !== TOOL_EXECUTION_FAILURE_COMPLETION_COMMAND_SCHEMA) {
|
||||
return invalid('failure completion command schema is invalid');
|
||||
}
|
||||
const barrier = normalizeToolExecutionStartBarrierRecord(value.barrier);
|
||||
const failure = normalizeToolExecutionFailureResult(value.failure);
|
||||
const stepRunMutation = normalizeStepRunMutation(value.stepRunMutation);
|
||||
if (
|
||||
failure.startId !== barrier.startId ||
|
||||
failure.barrierDigest !== barrier.barrierDigest ||
|
||||
failure.adapterDigest !== barrier.adapterDigest ||
|
||||
failure.completedAtMs < barrier.startedAtMs ||
|
||||
stepRunMutation.runId !== barrier.runId ||
|
||||
stepRunMutation.stepRun.id !== barrier.stepRunId ||
|
||||
stepRunMutation.stepRun.kind !== 'tool' ||
|
||||
stepRunMutation.previousStatus !== 'running' ||
|
||||
stepRunMutation.expectedStepRunVersion !== barrier.startedStepRunVersion ||
|
||||
stepRunMutation.expectedStepRunDigest !== barrier.startedStepRunDigest ||
|
||||
stepRunMutation.stepRun.status !== failure.outcome ||
|
||||
stepRunMutation.stepRun.outputRef !== null ||
|
||||
stepRunMutation.stepRun.resultCode !== failure.resultCode ||
|
||||
stepRunMutation.stepRun.errorSummary !== failure.errorSummary ||
|
||||
stepRunMutation.stepRun.finishedAtMs !== failure.completedAtMs ||
|
||||
stepRunMutation.stepRun.updatedAtMs !== failure.completedAtMs
|
||||
) {
|
||||
throw new ToolExecutionFailureCompletionConflictError();
|
||||
}
|
||||
const unsigned = Object.freeze({
|
||||
schema: TOOL_EXECUTION_FAILURE_COMPLETION_COMMAND_SCHEMA,
|
||||
barrier,
|
||||
failure,
|
||||
stepRunMutation,
|
||||
});
|
||||
const commandDigest = digest(
|
||||
value.commandDigest,
|
||||
'failure completion command digest',
|
||||
);
|
||||
if (hash(COMMAND_DIGEST_DOMAIN, unsigned) !== commandDigest) {
|
||||
return invalid('failure completion command digest does not match');
|
||||
}
|
||||
return Object.freeze({ ...unsigned, commandDigest });
|
||||
}
|
||||
|
||||
export function createToolExecutionFailureCompletionCommand(
|
||||
value: Omit<
|
||||
ToolExecutionFailureCompletionCommand,
|
||||
'commandDigest' | 'schema'
|
||||
>,
|
||||
): Readonly<ToolExecutionFailureCompletionCommand> {
|
||||
const unsigned = Object.freeze({
|
||||
schema: TOOL_EXECUTION_FAILURE_COMPLETION_COMMAND_SCHEMA,
|
||||
barrier: normalizeToolExecutionStartBarrierRecord(value.barrier),
|
||||
failure: normalizeToolExecutionFailureResult(value.failure),
|
||||
stepRunMutation: normalizeStepRunMutation(value.stepRunMutation),
|
||||
});
|
||||
return normalizeToolExecutionFailureCompletionCommand({
|
||||
...unsigned,
|
||||
commandDigest: hash(COMMAND_DIGEST_DOMAIN, unsigned),
|
||||
});
|
||||
}
|
||||
|
||||
export function toolExecutionFailureCompletionRecord(
|
||||
commandValue: ToolExecutionFailureCompletionCommand,
|
||||
): Readonly<ToolExecutionFailureCompletionRecord> {
|
||||
const command = normalizeToolExecutionFailureCompletionCommand(commandValue);
|
||||
return completionFromParts(
|
||||
command.barrier,
|
||||
command.failure,
|
||||
command.stepRunMutation,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user