mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): terminalize copilot diagnosis failures
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
|
||||
最新增量证据(2026-08-15):
|
||||
|
||||
- D-320/ADR-0412(已接受):Cluster Copilot failure diagnosis 已补齐产品入口前的非成功收敛边界。新增的 pre-Model terminalizer 只从 durable Tool failure、受审日志 projection 或数据库观察到的 deadline/cancellation 派生封闭 reason,在 Model start 不存在时以一个 SERIALIZABLE 事务原子提交 StepRun mutations、RunEvents、父 Run 终态和 `pg-9021` append-only content-free receipt;Tool `failed|timed_out`、日志 `not_found|pending|missing|retired`、五秒 Tool budget 不足、deadline 和 cancellation 均可 exact replay。Model 已开始后的 `outcome_unknown` 继续要求强 User 显式 `fail|cancel|retry` resolution;Copilot finalizer 现在对 `fail|cancel` 精确校验 resolution mutation 的 resolved Step digest,既不伪造失败也不自动重试 Provider。实现复用既有 AI package、Cluster AI 进程、Pool 与 ledger,不增加 package、进程、连接、timer、watcher、队列、cache 或产品 route;workspace 仍为 18 package、无单文件/浅平 package,AI 192 个源码中 191 个位于嵌套领域目录。AI 完整测试 244 pass/3 条件 skip,18-package clean build/test 与 backend 1,207 pass/2 条件 skip/0 fail,四项架构审计和 14 档 Local artifact 全部通过。PostgreSQL 18.6 arm64 HA 137/137、timeline `1→2`,覆盖日志不可用 pre-Model terminalization、unknown completion→人工 fail resolution→Run finalization 及晋升后零外部副作用 exact replay;报告 SHA-256 为 `6eaeb20615a62d153c5a69687344f41f31351c6ecf111cfb9cbafad115538c83`,离线审计零 finding。下一 Gate 才增加认证、Policy、audit 与 source fence 保护的 Cluster API,并由 CLI/UI/MCP 复用,仍不得建立旁路执行器。
|
||||
- D-319/ADR-0411(已接受):Cluster Copilot failure diagnosis 已在既有 `ql3-cluster-control-ai` 进程内完成默认关闭的 production composition。Prompt 与 Copilot 共享同一个 PostgreSQL AI Pool、Model Gateway、Provider client、恢复扫描、quota/pricing ledger 与 `maxConcurrent` 预算;有界 successful-completion router 只向声明 exact invocation 的 durable sink 分发,不复制 Gateway、连接或隐藏队列。application service 只接受 source Run/request identity,从数据库当前 Run、latest Attempt、Project Tool snapshot、Policy 和 canonical read-only egress config 派生计划,先 admission,再以 durable plan、historical key 与确定性 nonce 修复 admission→Artifact crash window,随后复用 Trusted Tool、Worker Artifact range reader、Model Gateway 及独立的 invocation/result/model-output 三域 keyring。能力保持 caller-driven,不增加 timer、watcher、队列、HTTP/CLI/UI/MCP route 或 Kubernetes API 权限;Kubernetes 独立可选 component 仅投影 config 与三个 0440 keyring。最终 AI 238 pass/3 条件 skip、Cluster Control 240 pass/2 条件 skip、18-package clean build/test 与 backend 1,207 pass/2 条件 skip/0 fail,四项架构审计、14 档 Local artifact 全部通过;workspace 仍为 18 package、无单文件/浅平 package,AI 187 个源码中 186 个、Cluster Control 59 个中 57 个位于嵌套领域目录。默认 Edge/Standalone 保持 2,589,890/2,589,968 bytes,Edge/Standalone AI 为 3,064,454/3,064,544 bytes,证明 Cluster-only composition 未进入小设备闭包。PostgreSQL 18.6 arm64 HA 130/130、timeline `1→2`,报告 SHA-256 为 `981299b454dce5541e9596450b85816dc40559cba8dc42adf3d5fea571c3d3a6`;本 Gate 不改 migration/schema/role/SQL/HA 拓扑。下一 Gate 是 Tool failure、日志 missing/retired/pending、deadline/cancel 与 Model outcome-unknown 的 durable terminalization/recovery,完成前不开放产品入口。
|
||||
- D-318/ADR-0410(已接受):Cluster Copilot diagnosis Model output 获得独立的只读 projected key authority。canonical `qinglong/copilot-failure-diagnosis-output-projected-keyring@v1` manifest 只允许一个 active key 与最多 16 个 historical 32-byte key;每次 `active()`/`resolve()` 都重新执行 Cluster 私有投影文件的 direct-root、根内 atomic symlink、single-link、mode、dev/inode/size/mtime 与双 realpath fence,不使用 cache、watcher、timer 或 Kubernetes API。该 authority 以结构兼容的本地窄端口位于既有 `cluster-control/copilot/failure-diagnosis/`,只通过 `failure-diagnosis-output-keyring` subpath 发布,避免 Cluster Control 默认源码反向依赖 AI;它不复用 Prompt output、Tool invocation/result 或 Provider credential key domain,也不提前声称 Copilot 产品入口已可达。定向回归 13/13、Cluster Control 239 pass/2 条件 skip、backend 1,207 pass/2 条件 skip、18-package clean build/test、四项架构审计与 14 档 Local artifact 全部通过;workspace 仍无单文件/浅平 package,Cluster Control 58 个源码中 56 个位于嵌套领域目录。默认 Edge/Standalone 保持 2,589,890/2,589,968 bytes,Edge/Standalone AI 保持 3,061,009/3,061,099 bytes,证明 Cluster-only subpath 被裁掉。本 Gate 不改 migration/schema/role/SQL/连接/HA 拓扑,因此数据库基线继续引用 ADR-0409 的 PostgreSQL 18.6 arm64 HA 130/130、timeline `1→2`;下一 Gate 是默认关闭的完整 Cluster Copilot composition,随后补齐 Tool failure、日志 missing/retired、Model admission 前 deadline/cancel 与 outcome-unknown 的 durable terminalization/recovery。
|
||||
- D-316/ADR-0408(已接受):Cluster Copilot 现在能够从 ADR-0407 的 durable admission 恢复 exact `qinglong.run.log.excerpt@1.0.0` authority,复用通用 Trusted Tool start barrier、加密 success/failure completion、result catalog/rekey 和内建日志 adapter;确定性 start/completion identity 让 response-loss replay 直接打开既有证据,不重复读取日志或执行 adapter。Cluster invocation Artifact 使用独立 projected keyring,提供 active+historical material,但每次读取均重新执行 canonical path/symlink/mode/inode/realpath fence,且不取得 PostgreSQL Tool result generation authority。只有 exact `succeeded` completion 才能通过 `pg-9019` 的 SERIALIZABLE 事务把 Model Step 从 `pending` 原子推进到 `ready`,同时写 RunEvent、StepRunMutation 和 append-only unlock receipt;`failed|timed_out` 不解锁。本 Gate 不执行模型、不终态化 diagnosis Run,下一 Gate 是 ADR-0405 builder + Model Gateway + Copilot encrypted model completion/terminalization。实现仍为 18 个 package,无单文件/浅平 package;AI 175 个源码中 174 个、Cluster Control 56 个中 54 个位于嵌套目录,不新增依赖、进程、连接、timer/watcher/cache 或默认 Edge 成本。18-package clean build/test 全绿,AI 229 pass/3 条件 skip、backend 1,207 pass/2 条件 skip,四项架构审计零 finding;14 档 Local artifact 全通过,默认 Edge/Standalone 为 2,589,890/2,589,968 bytes。PostgreSQL 18.4 arm64 HA 130/130、timeline `1→2`,首次执行只读两次日志、密文不含敏感 fixture,提升后 exact replay 零日志读取;报告 SHA-256 为 `d525a303696e178d777b021b376729bd2c5382fb5eb7bc98466a2b79d3940517`,独立审计与 Docker 清理通过。
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# ADR-0412:耐久 Copilot Pre-Model 终态化与 Model Unknown Resolution
|
||||
|
||||
- 状态:Accepted
|
||||
- 日期:2026-08-15
|
||||
- 关联 RFC:QL-RFC-0001 D-320、Phase 2
|
||||
- 关联 ADR:ADR-0087、ADR-0407、ADR-0408、ADR-0409、ADR-0411
|
||||
|
||||
## 问题
|
||||
|
||||
ADR-0411 已把 Cluster Copilot failure diagnosis 组合到既有、默认关闭的 Cluster AI 进程,但成功链之外仍有两类不收敛窗口:
|
||||
|
||||
1. Tool `failed|timed_out`、日志 `not_found|pending|missing|retired`、执行 Tool 所需的固定预算不足、数据库观察到 deadline 或 cancellation 时,Model 尚未开始,diagnosis Run 可能永久停留在 `running`;
|
||||
2. Provider 已开始但结果不可确认时,通用 ModelInvocation 会耐久记录 `outcome_unknown` 和 `lost` Step。人工 `fail|cancel` resolution 会产生一个新的 resolved Step digest,原 Copilot finalizer 却仍只接受 unknown completion 时的 `lost` digest,因此正确 resolution 也无法终态化父 Run。
|
||||
|
||||
不能由调用者直接提交 outcome/reason,也不能用进程内异常或定时器伪造耐久终态。路由器和低配设备部署还要求该补强不能增加 package、进程、连接、队列、watcher、timer 或默认 Profile 闭包。
|
||||
|
||||
## 决策
|
||||
|
||||
1. 在 `@qinglong/ai` 的既有 `copilot/failure-diagnosis/terminalization` 领域目录建立一个 pre-Model terminalization capability,并仅通过 `failure-diagnosis-pre-model-terminalization` 精确 subpath 发布。它不是新 package,也不从 package `src` 根平铺实现文件。
|
||||
2. terminalizer 只接受三种受信触发:耐久 Tool failure completion、耐久 Tool success 加受审日志 projection、或无调用者事实的 boundary observation。repository 再从数据库读取 exact admission、Run、Tool/Model Step、Model start existence 和数据库时间;调用者不能指定 reason、outcome、Step mutation、Run version 或时间。
|
||||
3. reason 与结果采用封闭映射:
|
||||
- `tool_failed` → Run `failed`,取消尚未运行的 Model Step;
|
||||
- `tool_timed_out` → Run `timed_out`,取消 Model Step;
|
||||
- `log_not_found|log_pending|log_missing|log_retired` → Run/Model Step `failed`;
|
||||
- `tool_budget_exhausted|deadline_exceeded` → Run `timed_out`,按 Step 当前状态写 `timed_out|cancelled`;
|
||||
- `cancellation_requested` → Run `cancelled`,取消未开始的两个 Step。
|
||||
4. 任何 Model start 已存在时都拒绝 pre-Model terminalization。Model 已经跨过外部副作用边界后,只能使用 Model completion/recovery/resolution 语义,不能把“不知道”改写为“没有执行”。
|
||||
5. `pg-9021-ai-copilot-failure-diagnosis-pre-model-terminalizations` 增加一个 append-only content-free receipt ledger。`ql3_runtime` 只有 `SELECT, INSERT`,PUBLIC 和其他角色没有权限。一个 `SERIALIZABLE` 事务锁定 Run/Step,复验 admission、reason evidence、version/digest 和 Model-start absence,原子提交 StepRun mutations、RunEvents、父 Run 终态和 receipt;COMMIT response loss 通过 receipt exact replay 收敛,不重复 Tool、日志读取或 Model 调用。
|
||||
6. application service 在 Tool 执行前先检查 boundary terminalization;Tool failure 和非 available 日志 projection 直接进入 terminalizer;只有 available projection 才继续 unlock/Model。Tool 的固定五秒预算必须完整落在 plan deadline 内,否则在 start barrier 前终态化。并发调用继续由既有 request coalescing 收敛。
|
||||
7. `outcome_unknown` 不自动重试、不自动假定失败。强认证 User 仍须通过通用 `DurableModelInvocationResolutionCoordinator` 显式选择 `fail|cancel|retry`。Copilot finalizer 对普通 completion 校验 completion 后 Step digest;对 unknown 的 `fail|cancel` 校验 resolution completion binding 和 resolution mutation 的 resolved Step digest,再终态化父 Run。`retry` 保持 in-progress,且确定性 invocation identity 不允许偷偷再调用 Provider。
|
||||
8. 该 capability 继续 caller-driven,并复用既有 PostgreSQL AI Pool、Run/Step ledger、Tool completion、ModelInvocation resolution 与 Cluster composition;不新增 daemon、scan、timer、watcher、队列、cache、HTTP/CLI/UI/MCP route、Pod 或 Kubernetes API 权限。默认 Edge/Standalone 及其 AI 制品不导入 Cluster-only composition,低配部署没有新增常驻工作。
|
||||
|
||||
## 被否决方案
|
||||
|
||||
1. **由 application catch 后直接返回失败**:没有耐久 Run/Step/Event 事实,崩溃和 replay 会漂移。
|
||||
2. **调用者提交 reason/outcome**:会把数据库时间、取消事实和 Tool completion authority 暴露给不可信边界。
|
||||
3. **后台轮询扫描所有 diagnosis Run**:增加每副本 timer/数据库负载,并与 caller-driven 组合重复竞争。
|
||||
4. **Model start 后仍走 pre-Model terminalizer**:可能把已经产生费用或外部结果的调用伪装为未执行。
|
||||
5. **将 unknown completion 直接映射为 failed**:抹掉真实歧义并绕过显式人工 resolution。
|
||||
6. **新增 terminalization package**:没有独立部署、依赖、权限或多 consumer 边界,只会制造薄 package。
|
||||
|
||||
## 验证标准
|
||||
|
||||
1. 单元测试覆盖 Tool failure/timed-out、四种日志状态、deadline、Tool budget、cancellation、exact replay、并发 coalescing、过早触发和 Model-start 后拒绝。
|
||||
2. PostgreSQL migration 测试固定 migration ID、顺序、表约束和 `SELECT, INSERT` 最小权限;package/dependency/import/deployment 审计必须保持零 finding。
|
||||
3. PostgreSQL 18 physical HA 以三个独立 diagnosis Run 证明:成功基线、日志不可用 pre-Model terminalization、unknown completion→人工 fail resolution→finalization;晋升后全部 exact replay,不重复日志读取、Tool 或 Model 外部执行。
|
||||
4. 18-package clean build/test、完整 backend、14 档 Local artifact、GitNexus staged/change detection 全通过后才允许阶段性提交。
|
||||
|
||||
## 当前验证
|
||||
|
||||
1. `@qinglong/ai` 完整测试 244 pass/3 条件 skip/0 fail;新增 9 条 pre-Model 场景覆盖 Tool failure、四种日志状态、deadline、固定 Tool budget、cancellation 和 Model-start fence。
|
||||
2. 18 个 QL3 package 从清空全部 `dist` 开始完成拓扑构建和全包测试,非沙箱 TLS loopback 回归全绿;backend 1,207 pass/2 条件 skip/0 fail。
|
||||
3. workspace 保持 18 个 package,`singleSourcePackages=[]`、`shallowSourcePackages=[]`。AI 192 个源码中 191 个位于嵌套领域目录;新增五个源码全部位于既有 failure-diagnosis 领域目录,package 根仍只有一个 16 行 public entrypoint。`local-command-file` 虽小但保留多 consumer 的独立 POSIX 文件安全边界,两个 migration 密集目录继续作为有顺序上限的 ledger 接受审计。
|
||||
4. Edge import、Cluster dependency、package boundary 与 Cluster deployment 审计兼容且零 finding;14 档 Local artifact 通过,说明 Cluster-only terminalizer 未进入默认路由设备闭包。
|
||||
5. PostgreSQL 18.6 arm64 physical HA 137/137、timeline `1→2`;21 条 AI migration、新 append-only ledger、最小权限、两条新增恢复链和晋升后 exact replay 全部通过。私有报告 SHA-256 为 `6eaeb20615a62d153c5a69687344f41f31351c6ecf111cfb9cbafad115538c83`,独立离线审计 `compatible=true`、零 finding。
|
||||
|
||||
## 后续门禁
|
||||
|
||||
下一 Gate 才能增加认证、Policy、audit、request identity 与 source fence 保护的 Cluster 产品 API;CLI/UI/MCP 必须复用同一 capability,不得建立旁路执行器。产品入口还需补多副本并发、真实外部 Provider、费用/取消可观测性和明文负证据后再评估默认策略。
|
||||
@@ -415,6 +415,7 @@
|
||||
| [ADR-0409](./ADR-0409-cluster-copilot-failure-diagnosis-model-execution.md) | Cluster Copilot Failure Diagnosis Model Execution、密文输出与 Run 终态化 | Accepted |
|
||||
| [ADR-0410](./ADR-0410-cluster-copilot-failure-diagnosis-output-key-authority.md) | Cluster Copilot Failure Diagnosis Output Projected Key Authority | Accepted |
|
||||
| [ADR-0411](./ADR-0411-default-off-cluster-copilot-composition.md) | 默认关闭的 Cluster Copilot Failure Diagnosis Composition | Accepted |
|
||||
| [ADR-0412](./ADR-0412-durable-copilot-pre-model-terminalization-and-unknown-resolution.md) | 耐久 Copilot Pre-Model 终态化与 Model Unknown Resolution | Accepted |
|
||||
|
||||
## 规则
|
||||
|
||||
|
||||
@@ -60,6 +60,11 @@
|
||||
"require": "./dist/copilot/failure-diagnosis/failureDiagnosisApplication.js",
|
||||
"default": "./dist/copilot/failure-diagnosis/failureDiagnosisApplication.js"
|
||||
},
|
||||
"./failure-diagnosis-pre-model-terminalization": {
|
||||
"types": "./dist/copilot/failure-diagnosis/preModelTerminalization.d.ts",
|
||||
"require": "./dist/copilot/failure-diagnosis/preModelTerminalization.js",
|
||||
"default": "./dist/copilot/failure-diagnosis/preModelTerminalization.js"
|
||||
},
|
||||
"./postgres-failure-diagnosis-model-execution-storage": {
|
||||
"types": "./dist/copilot/failure-diagnosis/postgresModelExecutionRepository.d.ts",
|
||||
"require": "./dist/copilot/failure-diagnosis/postgresModelExecutionRepository.js",
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { SecurityPrincipal } from '@qinglong/runtime-core/security';
|
||||
import type { CopilotFailureDiagnosisAdmissionReceipt } from '../admission/contracts';
|
||||
import type { CopilotFailureDiagnosisModelExecutionResult } from '../model-execution/coordinator';
|
||||
import type { CopilotFailureDiagnosisToolExecutionResult } from '../tool-execution/contracts';
|
||||
import type { CopilotFailureDiagnosisPreModelTerminalizationReceipt } from '../terminalization/contracts';
|
||||
|
||||
export const MAX_ACTIVE_COPILOT_FAILURE_DIAGNOSIS_APPLICATION_REQUESTS = 64;
|
||||
|
||||
@@ -17,8 +18,9 @@ export interface ExecuteCopilotFailureDiagnosisApplicationCommand {
|
||||
export interface ExecuteCopilotFailureDiagnosisApplicationResult {
|
||||
readonly admissionStatus: 'created' | 'existing';
|
||||
readonly admission: Readonly<CopilotFailureDiagnosisAdmissionReceipt>;
|
||||
readonly tool: Readonly<CopilotFailureDiagnosisToolExecutionResult>;
|
||||
readonly tool: Readonly<CopilotFailureDiagnosisToolExecutionResult> | null;
|
||||
readonly model: Readonly<CopilotFailureDiagnosisModelExecutionResult> | null;
|
||||
readonly terminalization: Readonly<CopilotFailureDiagnosisPreModelTerminalizationReceipt> | null;
|
||||
readonly terminalizationRequired: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,7 @@ import {
|
||||
} from '@qinglong/runtime-core/builtin-run-log-excerpt-tool';
|
||||
import type { RunRepositoryReader } from '@qinglong/runtime-core/run-repository';
|
||||
import type { ToolPolicyAuthorizer } from '@qinglong/runtime-core/tool-registry';
|
||||
import {
|
||||
prepareToolInvocation,
|
||||
} from '@qinglong/runtime-core/tool-registry';
|
||||
import { prepareToolInvocation } from '@qinglong/runtime-core/tool-registry';
|
||||
import type { ProjectToolDefinitionSnapshotRepository } from '@qinglong/runtime-core/project-tool-definition-snapshot';
|
||||
import { projectToolDefinitionRegistry } from '@qinglong/runtime-core/project-tool-definition-snapshot';
|
||||
import {
|
||||
@@ -25,9 +23,7 @@ import {
|
||||
} from '@qinglong/runtime-core/trusted-tool-invocation';
|
||||
|
||||
import { MAX_MODEL_INVOCATION_MS } from '../../../model-gateway/model';
|
||||
import {
|
||||
prepareCopilotFailureDiagnosisExecution,
|
||||
} from '../admission/plan';
|
||||
import { prepareCopilotFailureDiagnosisExecution } from '../admission/plan';
|
||||
import type {
|
||||
CopilotFailureDiagnosisAdmissionRepository,
|
||||
CopilotFailureDiagnosisExecutionPlan,
|
||||
@@ -37,10 +33,20 @@ import {
|
||||
executeCopilotFailureDiagnosisTool,
|
||||
type CopilotFailureDiagnosisToolExecutionDependencies,
|
||||
} from '../tool-execution/coordinator';
|
||||
import {
|
||||
CopilotFailureDiagnosisToolExecutionDeadlineExceededError,
|
||||
type CopilotFailureDiagnosisToolExecutionResult,
|
||||
} from '../tool-execution/contracts';
|
||||
import {
|
||||
executeCopilotFailureDiagnosisModel,
|
||||
type CopilotFailureDiagnosisModelExecutionDependencies,
|
||||
} from '../model-execution/coordinator';
|
||||
import {
|
||||
terminalizeCopilotFailureDiagnosisBeforeModel,
|
||||
type CopilotFailureDiagnosisPreModelTerminalizationDependencies,
|
||||
type CopilotFailureDiagnosisPreModelTerminalizationTrigger,
|
||||
} from '../terminalization/coordinator';
|
||||
import { CopilotFailureDiagnosisPreModelTerminalizationNotReadyError } from '../terminalization/contracts';
|
||||
import {
|
||||
CopilotFailureDiagnosisApplicationBusyError,
|
||||
CopilotFailureDiagnosisApplicationConflictError,
|
||||
@@ -64,7 +70,10 @@ const IDENTITY_DOMAIN = Buffer.from(
|
||||
|
||||
export interface CopilotFailureDiagnosisApplicationDependencies {
|
||||
readonly admissions: CopilotFailureDiagnosisAdmissionRepository;
|
||||
readonly snapshots: Pick<ProjectToolDefinitionSnapshotRepository, 'findCurrent'>;
|
||||
readonly snapshots: Pick<
|
||||
ProjectToolDefinitionSnapshotRepository,
|
||||
'findCurrent'
|
||||
>;
|
||||
readonly runs: Pick<
|
||||
RunRepositoryReader,
|
||||
'findRunById' | 'findLatestAttemptByRunId'
|
||||
@@ -77,19 +86,23 @@ export interface CopilotFailureDiagnosisApplicationDependencies {
|
||||
readonly authorizer: ToolPolicyAuthorizer;
|
||||
readonly tool: CopilotFailureDiagnosisToolExecutionDependencies;
|
||||
readonly model: CopilotFailureDiagnosisModelExecutionDependencies;
|
||||
readonly terminalizations: CopilotFailureDiagnosisPreModelTerminalizationDependencies;
|
||||
readonly executeTool: typeof executeCopilotFailureDiagnosisTool;
|
||||
readonly executeModel: typeof executeCopilotFailureDiagnosisModel;
|
||||
readonly terminalizeBeforeModel: typeof terminalizeCopilotFailureDiagnosisBeforeModel;
|
||||
readonly modelIntent: Readonly<PrepareCopilotFailureDiagnosisModelIntent>;
|
||||
readonly executionTimeoutMs: number;
|
||||
readonly now?: () => number;
|
||||
readonly nonceFactory?: (input: Readonly<{
|
||||
key: Uint8Array;
|
||||
keyId: string;
|
||||
requestId: string;
|
||||
projectId: string;
|
||||
sourceRunId: string;
|
||||
invocationActionDigest: string;
|
||||
}>) => Uint8Array;
|
||||
readonly nonceFactory?: (
|
||||
input: Readonly<{
|
||||
key: Uint8Array;
|
||||
keyId: string;
|
||||
requestId: string;
|
||||
projectId: string;
|
||||
sourceRunId: string;
|
||||
invocationActionDigest: string;
|
||||
}>,
|
||||
) => Uint8Array;
|
||||
}
|
||||
|
||||
interface ActiveRequest {
|
||||
@@ -146,14 +159,16 @@ function preview(
|
||||
});
|
||||
}
|
||||
|
||||
function defaultNonce(input: Readonly<{
|
||||
key: Uint8Array;
|
||||
keyId: string;
|
||||
requestId: string;
|
||||
projectId: string;
|
||||
sourceRunId: string;
|
||||
invocationActionDigest: string;
|
||||
}>): Uint8Array {
|
||||
function defaultNonce(
|
||||
input: Readonly<{
|
||||
key: Uint8Array;
|
||||
keyId: string;
|
||||
requestId: string;
|
||||
projectId: string;
|
||||
sourceRunId: string;
|
||||
invocationActionDigest: string;
|
||||
}>,
|
||||
): Uint8Array {
|
||||
const derived = createHmac('sha256', Buffer.from(input.key))
|
||||
.update(NONCE_DOMAIN)
|
||||
.update(
|
||||
@@ -224,6 +239,10 @@ function assertDependencies(
|
||||
typeof value.executeModel !== 'function' ||
|
||||
!value.tool ||
|
||||
!value.model ||
|
||||
typeof value.terminalizations?.repository?.findByRequestId !== 'function' ||
|
||||
typeof value.terminalizations?.repository?.readAuthority !== 'function' ||
|
||||
typeof value.terminalizations?.repository?.commit !== 'function' ||
|
||||
typeof value.terminalizeBeforeModel !== 'function' ||
|
||||
!value.modelIntent ||
|
||||
typeof value.modelIntent !== 'object' ||
|
||||
!Number.isSafeInteger(value.executionTimeoutMs) ||
|
||||
@@ -359,21 +378,91 @@ export class CopilotFailureDiagnosisApplicationService {
|
||||
});
|
||||
}
|
||||
}
|
||||
const tool = await this.#dependencies.executeTool(
|
||||
{
|
||||
requestId: plan.requestId,
|
||||
principal: command.principal,
|
||||
authorizer: this.#dependencies.authorizer,
|
||||
},
|
||||
this.#dependencies.tool,
|
||||
);
|
||||
const boundary = await this.#preModel(plan.requestId, { kind: 'boundary' });
|
||||
if (boundary) {
|
||||
return Object.freeze({
|
||||
admissionStatus,
|
||||
admission,
|
||||
tool: null,
|
||||
model: null,
|
||||
terminalization: boundary,
|
||||
terminalizationRequired: false,
|
||||
});
|
||||
}
|
||||
let tool: Readonly<CopilotFailureDiagnosisToolExecutionResult>;
|
||||
try {
|
||||
tool = await this.#dependencies.executeTool(
|
||||
{
|
||||
requestId: plan.requestId,
|
||||
principal: command.principal,
|
||||
authorizer: this.#dependencies.authorizer,
|
||||
},
|
||||
this.#dependencies.tool,
|
||||
);
|
||||
} catch (cause) {
|
||||
if (
|
||||
!(
|
||||
cause instanceof
|
||||
CopilotFailureDiagnosisToolExecutionDeadlineExceededError
|
||||
)
|
||||
) {
|
||||
throw cause;
|
||||
}
|
||||
const terminalization = await this.#dependencies.terminalizeBeforeModel(
|
||||
plan.requestId,
|
||||
{ kind: 'boundary' },
|
||||
this.#dependencies.terminalizations,
|
||||
);
|
||||
return Object.freeze({
|
||||
admissionStatus,
|
||||
admission,
|
||||
tool: null,
|
||||
model: null,
|
||||
terminalization: terminalization.receipt,
|
||||
terminalizationRequired: false,
|
||||
});
|
||||
}
|
||||
if (tool.outcome !== 'succeeded') {
|
||||
const terminalization = await this.#dependencies.terminalizeBeforeModel(
|
||||
plan.requestId,
|
||||
{ kind: 'tool_failure', completion: tool.completion },
|
||||
this.#dependencies.terminalizations,
|
||||
);
|
||||
return Object.freeze({
|
||||
admissionStatus,
|
||||
admission,
|
||||
tool,
|
||||
model: null,
|
||||
terminalizationRequired: true,
|
||||
terminalization: terminalization.receipt,
|
||||
terminalizationRequired: false,
|
||||
});
|
||||
}
|
||||
const projection = await this.#preModel(plan.requestId, {
|
||||
kind: 'tool_projection',
|
||||
completion: tool.completion,
|
||||
output: tool.output,
|
||||
});
|
||||
if (projection) {
|
||||
return Object.freeze({
|
||||
admissionStatus,
|
||||
admission,
|
||||
tool,
|
||||
model: null,
|
||||
terminalization: projection,
|
||||
terminalizationRequired: false,
|
||||
});
|
||||
}
|
||||
const afterToolBoundary = await this.#preModel(plan.requestId, {
|
||||
kind: 'boundary',
|
||||
});
|
||||
if (afterToolBoundary) {
|
||||
return Object.freeze({
|
||||
admissionStatus,
|
||||
admission,
|
||||
tool,
|
||||
model: null,
|
||||
terminalization: afterToolBoundary,
|
||||
terminalizationRequired: false,
|
||||
});
|
||||
}
|
||||
const model = await this.#dependencies.executeModel(
|
||||
@@ -385,10 +474,33 @@ export class CopilotFailureDiagnosisApplicationService {
|
||||
admission,
|
||||
tool,
|
||||
model,
|
||||
terminalization: null,
|
||||
terminalizationRequired: false,
|
||||
});
|
||||
}
|
||||
|
||||
async #preModel(
|
||||
requestId: string,
|
||||
trigger: CopilotFailureDiagnosisPreModelTerminalizationTrigger,
|
||||
) {
|
||||
try {
|
||||
const result = await this.#dependencies.terminalizeBeforeModel(
|
||||
requestId,
|
||||
trigger,
|
||||
this.#dependencies.terminalizations,
|
||||
);
|
||||
return result.receipt;
|
||||
} catch (cause) {
|
||||
if (
|
||||
cause instanceof
|
||||
CopilotFailureDiagnosisPreModelTerminalizationNotReadyError
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
throw cause;
|
||||
}
|
||||
}
|
||||
|
||||
async #prepare(
|
||||
command: Readonly<ExecuteCopilotFailureDiagnosisApplicationCommand>,
|
||||
) {
|
||||
@@ -447,9 +559,7 @@ export class CopilotFailureDiagnosisApplicationService {
|
||||
const key = await this.#dependencies.invocationKeys.active();
|
||||
try {
|
||||
const baseIdentity = identity('cda', command.requestId);
|
||||
const nonce = (
|
||||
this.#dependencies.nonceFactory ?? defaultNonce
|
||||
)({
|
||||
const nonce = (this.#dependencies.nonceFactory ?? defaultNonce)({
|
||||
key: key.key,
|
||||
keyId: key.keyId,
|
||||
requestId: command.requestId,
|
||||
@@ -507,9 +617,9 @@ export class CopilotFailureDiagnosisApplicationService {
|
||||
return unavailable();
|
||||
}
|
||||
try {
|
||||
const nonce = (
|
||||
this.#dependencies.nonceFactory ?? defaultNonce
|
||||
)(nonceInput(plan, material.key));
|
||||
const nonce = (this.#dependencies.nonceFactory ?? defaultNonce)(
|
||||
nonceInput(plan, material.key),
|
||||
);
|
||||
const inputArtifact = createToolInvocationInputArtifact(
|
||||
{
|
||||
artifactId: plan.tool.invocationArtifact.artifactId,
|
||||
@@ -564,7 +674,10 @@ export class CopilotFailureDiagnosisApplicationService {
|
||||
return unavailable(cause);
|
||||
}
|
||||
if (!Number.isSafeInteger(value) || value < 0) return invalid('clock');
|
||||
if (value + this.#dependencies.executionTimeoutMs > Number.MAX_SAFE_INTEGER) {
|
||||
if (
|
||||
value + this.#dependencies.executionTimeoutMs >
|
||||
Number.MAX_SAFE_INTEGER
|
||||
) {
|
||||
return invalid('deadline overflows');
|
||||
}
|
||||
return value;
|
||||
|
||||
+105
-36
@@ -34,8 +34,7 @@ import {
|
||||
} from './finalization';
|
||||
|
||||
const TABLE = '"ql3_ai"."copilot_failure_diagnosis_model_outputs"';
|
||||
const FINALIZATION_TABLE =
|
||||
'"ql3_ai"."copilot_failure_diagnosis_finalizations"';
|
||||
const FINALIZATION_TABLE = '"ql3_ai"."copilot_failure_diagnosis_finalizations"';
|
||||
|
||||
interface OutputRow extends Record<string, unknown> {
|
||||
readonly artifactJson: unknown;
|
||||
@@ -79,7 +78,9 @@ function unavailable(cause?: unknown): never {
|
||||
});
|
||||
}
|
||||
|
||||
function parse(row: OutputRow): Readonly<CopilotFailureDiagnosisOutputArtifact> {
|
||||
function parse(
|
||||
row: OutputRow,
|
||||
): Readonly<CopilotFailureDiagnosisOutputArtifact> {
|
||||
try {
|
||||
return normalizeCopilotFailureDiagnosisOutputArtifact(
|
||||
row.artifactJson as CopilotFailureDiagnosisOutputArtifact,
|
||||
@@ -119,7 +120,8 @@ async function put(
|
||||
client: PostgresClient,
|
||||
artifactValue: CopilotFailureDiagnosisOutputArtifact,
|
||||
): Promise<Readonly<CopilotFailureDiagnosisOutputArtifact>> {
|
||||
const artifact = normalizeCopilotFailureDiagnosisOutputArtifact(artifactValue);
|
||||
const artifact =
|
||||
normalizeCopilotFailureDiagnosisOutputArtifact(artifactValue);
|
||||
const existing = await read(client, artifact.artifactId);
|
||||
if (existing) {
|
||||
if (JSON.stringify(existing) !== JSON.stringify(artifact)) {
|
||||
@@ -239,7 +241,9 @@ export class PostgresCopilotFailureDiagnosisModelRepository
|
||||
if (!result.rows[0]) return null;
|
||||
const row = result.rows[0];
|
||||
const receipt = normalizeCopilotFailureDiagnosisFinalizationReceipt(
|
||||
object(row.receiptJson) as unknown as CopilotFailureDiagnosisFinalizationReceipt,
|
||||
object(
|
||||
row.receiptJson,
|
||||
) as unknown as CopilotFailureDiagnosisFinalizationReceipt,
|
||||
);
|
||||
if (
|
||||
receipt.requestId !== string(row.requestId) ||
|
||||
@@ -268,7 +272,9 @@ export class PostgresCopilotFailureDiagnosisModelRepository
|
||||
event.step_run_id AS "eventStepRunId",
|
||||
event.payload, event.created_at_ms AS "eventCreatedAtMs",
|
||||
completion.completion_digest AS "completionDigest",
|
||||
completion.outcome AS "completionOutcome"
|
||||
completion.outcome AS "completionOutcome",
|
||||
resolution.decision AS "resolutionDecision",
|
||||
resolution.resolved_at_ms AS "resolvedAtMs"
|
||||
FROM "ql3"."runs" AS run
|
||||
JOIN "ql3"."step_runs" AS step
|
||||
ON step.run_id = run.id AND step.id = $1
|
||||
@@ -276,6 +282,8 @@ export class PostgresCopilotFailureDiagnosisModelRepository
|
||||
ON event.run_id = run.id AND event.id = $2
|
||||
JOIN "ql3_ai"."model_invocation_completions" AS completion
|
||||
ON completion.invocation_id = $3
|
||||
LEFT JOIN "ql3_ai"."model_invocation_resolutions" AS resolution
|
||||
ON resolution.invocation_id = completion.invocation_id
|
||||
WHERE run.id = $4`,
|
||||
[
|
||||
receipt.modelStepRunId,
|
||||
@@ -301,7 +309,15 @@ export class PostgresCopilotFailureDiagnosisModelRepository
|
||||
string(proof.eventStepRunId) !== receipt.modelStepRunId ||
|
||||
integer(proof.eventCreatedAtMs) !== receipt.finalizedAtMs ||
|
||||
string(proof.completionDigest) !== receipt.completionDigest ||
|
||||
string(proof.completionOutcome) !== receipt.outcome ||
|
||||
!(
|
||||
string(proof.completionOutcome) === receipt.outcome ||
|
||||
(string(proof.completionOutcome) === 'outcome_unknown' &&
|
||||
((proof.resolutionDecision === 'fail' &&
|
||||
receipt.outcome === 'failed') ||
|
||||
(proof.resolutionDecision === 'cancel' &&
|
||||
receipt.outcome === 'cancelled')) &&
|
||||
integer(proof.resolvedAtMs) === receipt.finalizedAtMs)
|
||||
) ||
|
||||
payload.requestId !== receipt.requestId ||
|
||||
payload.planDigest !== receipt.planDigest ||
|
||||
payload.invocationId !== receipt.invocationId ||
|
||||
@@ -320,10 +336,12 @@ export class PostgresCopilotFailureDiagnosisModelRepository
|
||||
}
|
||||
}
|
||||
|
||||
async finalize(requestId: string): Promise<Readonly<{
|
||||
status: 'created' | 'existing';
|
||||
receipt: Readonly<CopilotFailureDiagnosisFinalizationReceipt>;
|
||||
}>> {
|
||||
async finalize(requestId: string): Promise<
|
||||
Readonly<{
|
||||
status: 'created' | 'existing';
|
||||
receipt: Readonly<CopilotFailureDiagnosisFinalizationReceipt>;
|
||||
}>
|
||||
> {
|
||||
const existing = await this.findFinalization(requestId);
|
||||
if (existing) {
|
||||
return Object.freeze({ status: 'existing' as const, receipt: existing });
|
||||
@@ -333,7 +351,9 @@ export class PostgresCopilotFailureDiagnosisModelRepository
|
||||
try {
|
||||
client = await this.#pool.connect();
|
||||
} catch (cause) {
|
||||
throw new CopilotFailureDiagnosisFinalizationUnavailableError({ cause });
|
||||
throw new CopilotFailureDiagnosisFinalizationUnavailableError({
|
||||
cause,
|
||||
});
|
||||
}
|
||||
let began = false;
|
||||
try {
|
||||
@@ -373,13 +393,17 @@ export class PostgresCopilotFailureDiagnosisModelRepository
|
||||
}
|
||||
if (
|
||||
cause instanceof CopilotFailureDiagnosisFinalizationConflictError ||
|
||||
cause instanceof CopilotFailureDiagnosisModelExecutionInProgressError ||
|
||||
cause instanceof CopilotFailureDiagnosisModelResolutionRequiredError ||
|
||||
cause instanceof
|
||||
CopilotFailureDiagnosisModelExecutionInProgressError ||
|
||||
cause instanceof
|
||||
CopilotFailureDiagnosisModelResolutionRequiredError ||
|
||||
cause instanceof CopilotFailureDiagnosisFinalizationUnavailableError
|
||||
) {
|
||||
throw cause;
|
||||
}
|
||||
throw new CopilotFailureDiagnosisFinalizationUnavailableError({ cause });
|
||||
throw new CopilotFailureDiagnosisFinalizationUnavailableError({
|
||||
cause,
|
||||
});
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
@@ -390,10 +414,12 @@ export class PostgresCopilotFailureDiagnosisModelRepository
|
||||
async #finalizeInTransaction(
|
||||
client: PostgresClient,
|
||||
requestId: string,
|
||||
): Promise<Readonly<{
|
||||
status: 'created' | 'existing';
|
||||
receipt: Readonly<CopilotFailureDiagnosisFinalizationReceipt>;
|
||||
}>> {
|
||||
): Promise<
|
||||
Readonly<{
|
||||
status: 'created' | 'existing';
|
||||
receipt: Readonly<CopilotFailureDiagnosisFinalizationReceipt>;
|
||||
}>
|
||||
> {
|
||||
const admission = await client.query<Record<string, unknown>>(
|
||||
`SELECT plan_json AS "planJson"
|
||||
FROM "ql3_ai"."copilot_failure_diagnosis_admissions"
|
||||
@@ -404,19 +430,29 @@ export class PostgresCopilotFailureDiagnosisModelRepository
|
||||
throw new CopilotFailureDiagnosisFinalizationConflictError();
|
||||
}
|
||||
const plan = normalizeCopilotFailureDiagnosisExecutionPlan(
|
||||
object(admission.rows[0]!.planJson) as unknown as CopilotFailureDiagnosisExecutionPlan,
|
||||
object(
|
||||
admission.rows[0]!.planJson,
|
||||
) as unknown as CopilotFailureDiagnosisExecutionPlan,
|
||||
);
|
||||
const durable = await client.query<Record<string, unknown>>(
|
||||
`SELECT run.status AS "runStatus", run.version AS "runVersion",
|
||||
run.event_sequence AS "runEventSequence",
|
||||
step.status AS "stepStatus",
|
||||
step.step_run_digest AS "stepRunDigest",
|
||||
completion.record_json AS "completionJson"
|
||||
completion.record_json AS "completionJson",
|
||||
resolution.decision AS "resolutionDecision",
|
||||
resolution.completion_digest AS "resolutionCompletionDigest",
|
||||
resolution.resolved_at_ms AS "resolvedAtMs",
|
||||
resolution_mutation.step_run_digest AS "resolutionStepRunDigest"
|
||||
FROM "ql3"."runs" AS run
|
||||
JOIN "ql3"."step_runs" AS step
|
||||
ON step.run_id = run.id AND step.id = $1
|
||||
LEFT JOIN "ql3_ai"."model_invocation_completions" AS completion
|
||||
ON completion.invocation_id = $2
|
||||
LEFT JOIN "ql3_ai"."model_invocation_resolutions" AS resolution
|
||||
ON resolution.invocation_id = $2
|
||||
LEFT JOIN "ql3"."step_run_mutations" AS resolution_mutation
|
||||
ON resolution_mutation.mutation_id = resolution.mutation_id
|
||||
WHERE run.id = $3
|
||||
FOR UPDATE OF run, step`,
|
||||
[plan.modelStepRunId, plan.modelInvocationId, plan.runId],
|
||||
@@ -438,14 +474,41 @@ export class PostgresCopilotFailureDiagnosisModelRepository
|
||||
completion.stepRunId !== plan.modelStepRunId ||
|
||||
completion.traceId !== plan.traceId ||
|
||||
string(row.runStatus) !== 'running' ||
|
||||
string(row.stepRunDigest) !== completion.completedStepRunDigest
|
||||
(completion.outcome !== 'outcome_unknown' &&
|
||||
string(row.stepRunDigest) !== completion.completedStepRunDigest)
|
||||
) {
|
||||
throw new CopilotFailureDiagnosisFinalizationConflictError();
|
||||
}
|
||||
let outcome: CopilotFailureDiagnosisFinalOutcome;
|
||||
let finalizedAtMs = completion.completedAtMs;
|
||||
if (completion.outcome === 'outcome_unknown') {
|
||||
throw new CopilotFailureDiagnosisModelResolutionRequiredError();
|
||||
if (
|
||||
row.resolutionDecision === null ||
|
||||
row.resolutionDecision === undefined
|
||||
) {
|
||||
throw new CopilotFailureDiagnosisModelResolutionRequiredError();
|
||||
}
|
||||
if (row.resolutionDecision === 'retry') {
|
||||
throw new CopilotFailureDiagnosisModelExecutionInProgressError();
|
||||
}
|
||||
if (
|
||||
row.resolutionDecision !== 'fail' &&
|
||||
row.resolutionDecision !== 'cancel'
|
||||
) {
|
||||
throw new CopilotFailureDiagnosisFinalizationConflictError();
|
||||
}
|
||||
if (
|
||||
string(row.resolutionCompletionDigest) !==
|
||||
completion.completionDigest ||
|
||||
string(row.resolutionStepRunDigest) !== string(row.stepRunDigest)
|
||||
) {
|
||||
throw new CopilotFailureDiagnosisFinalizationConflictError();
|
||||
}
|
||||
outcome = row.resolutionDecision === 'fail' ? 'failed' : 'cancelled';
|
||||
finalizedAtMs = integer(row.resolvedAtMs);
|
||||
} else {
|
||||
outcome = completion.outcome;
|
||||
}
|
||||
const outcome: CopilotFailureDiagnosisFinalOutcome = completion.outcome;
|
||||
if (string(row.stepStatus) !== outcome) {
|
||||
throw new CopilotFailureDiagnosisFinalizationConflictError();
|
||||
}
|
||||
@@ -485,19 +548,25 @@ export class PostgresCopilotFailureDiagnosisModelRepository
|
||||
outputArtifactId: output?.artifactId ?? null,
|
||||
finalRunVersion: runVersion + 1,
|
||||
finalRunEventSequence: eventSequence + 1,
|
||||
finalizedAtMs: completion.completedAtMs,
|
||||
finalizedAtMs,
|
||||
});
|
||||
const failure = outcome === 'succeeded'
|
||||
? { code: null, summary: null }
|
||||
: outcome === 'timed_out'
|
||||
? {
|
||||
code: 'COPILOT_FAILURE_DIAGNOSIS_TIMED_OUT',
|
||||
summary: 'Copilot failure diagnosis timed out',
|
||||
}
|
||||
: {
|
||||
code: 'COPILOT_FAILURE_DIAGNOSIS_FAILED',
|
||||
summary: 'Copilot failure diagnosis failed',
|
||||
};
|
||||
const failure =
|
||||
outcome === 'succeeded'
|
||||
? { code: null, summary: null }
|
||||
: outcome === 'cancelled'
|
||||
? {
|
||||
code: 'COPILOT_FAILURE_DIAGNOSIS_CANCELLED',
|
||||
summary: 'Copilot failure diagnosis was cancelled',
|
||||
}
|
||||
: outcome === 'timed_out'
|
||||
? {
|
||||
code: 'COPILOT_FAILURE_DIAGNOSIS_TIMED_OUT',
|
||||
summary: 'Copilot failure diagnosis timed out',
|
||||
}
|
||||
: {
|
||||
code: 'COPILOT_FAILURE_DIAGNOSIS_FAILED',
|
||||
summary: 'Copilot failure diagnosis failed',
|
||||
};
|
||||
const updated = await client.query(
|
||||
`UPDATE "ql3"."runs"
|
||||
SET status = $1, version = $2, event_sequence = $3,
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './terminalization/contracts';
|
||||
export * from './terminalization/protocol';
|
||||
export * from './terminalization/coordinator';
|
||||
export * from './terminalization/postgresRepository';
|
||||
@@ -0,0 +1,160 @@
|
||||
import type { RunRecord } from '@qinglong/runtime-core';
|
||||
import type {
|
||||
StepRunMutation,
|
||||
StepRunRecord,
|
||||
} from '@qinglong/runtime-core/step-run';
|
||||
|
||||
import type { CopilotFailureDiagnosisExecutionPlan } from '../admission/contracts';
|
||||
|
||||
export const COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_SCHEMA =
|
||||
'qinglong/copilot-failure-diagnosis-pre-model-terminalization@v1' as const;
|
||||
export const COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_COMMAND_SCHEMA =
|
||||
'qinglong/copilot-failure-diagnosis-pre-model-terminalization-command@v1' as const;
|
||||
|
||||
export const COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_REASONS = [
|
||||
'tool_failed',
|
||||
'tool_timed_out',
|
||||
'log_not_found',
|
||||
'log_pending',
|
||||
'log_missing',
|
||||
'log_retired',
|
||||
'tool_budget_exhausted',
|
||||
'deadline_exceeded',
|
||||
'cancellation_requested',
|
||||
] as const;
|
||||
|
||||
export type CopilotFailureDiagnosisPreModelTerminalizationReason =
|
||||
(typeof COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_REASONS)[number];
|
||||
|
||||
export type CopilotFailureDiagnosisPreModelTerminalizationStage =
|
||||
| 'tool'
|
||||
| 'log'
|
||||
| 'deadline'
|
||||
| 'cancellation';
|
||||
|
||||
export type CopilotFailureDiagnosisPreModelTerminalizationOutcome =
|
||||
| 'failed'
|
||||
| 'timed_out'
|
||||
| 'cancelled';
|
||||
|
||||
export interface CopilotFailureDiagnosisTerminalStepReference {
|
||||
readonly stepRunId: string;
|
||||
readonly status: 'failed' | 'timed_out' | 'cancelled';
|
||||
readonly version: number;
|
||||
readonly mutationId: string;
|
||||
readonly mutationDigest: string;
|
||||
readonly eventId: string;
|
||||
}
|
||||
|
||||
export interface CopilotFailureDiagnosisPreModelTerminalizationReceipt {
|
||||
readonly schema: typeof COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_SCHEMA;
|
||||
readonly requestId: string;
|
||||
readonly planDigest: string;
|
||||
readonly runId: string;
|
||||
readonly stage: CopilotFailureDiagnosisPreModelTerminalizationStage;
|
||||
readonly reason: CopilotFailureDiagnosisPreModelTerminalizationReason;
|
||||
readonly outcome: CopilotFailureDiagnosisPreModelTerminalizationOutcome;
|
||||
readonly evidenceDigest: string;
|
||||
readonly toolStartId: string | null;
|
||||
readonly toolCompletionDigest: string | null;
|
||||
readonly terminalSteps: readonly Readonly<CopilotFailureDiagnosisTerminalStepReference>[];
|
||||
readonly finalRunVersion: number;
|
||||
readonly finalRunEventSequence: number;
|
||||
readonly runEventId: string;
|
||||
readonly finalizedAtMs: number;
|
||||
readonly receiptDigest: string;
|
||||
}
|
||||
|
||||
export interface CopilotFailureDiagnosisPreModelTerminalizationCommand {
|
||||
readonly schema: typeof COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_COMMAND_SCHEMA;
|
||||
readonly plan: Readonly<CopilotFailureDiagnosisExecutionPlan>;
|
||||
readonly expectedRunVersion: number;
|
||||
readonly expectedRunEventSequence: number;
|
||||
readonly stepMutations: readonly Readonly<StepRunMutation>[];
|
||||
readonly receipt: Readonly<CopilotFailureDiagnosisPreModelTerminalizationReceipt>;
|
||||
readonly commandDigest: string;
|
||||
}
|
||||
|
||||
export interface CopilotFailureDiagnosisPreModelTerminalizationAuthority {
|
||||
readonly plan: Readonly<CopilotFailureDiagnosisExecutionPlan>;
|
||||
readonly run: Readonly<
|
||||
Pick<
|
||||
RunRecord,
|
||||
| 'id'
|
||||
| 'projectId'
|
||||
| 'status'
|
||||
| 'version'
|
||||
| 'eventSequence'
|
||||
| 'cancelRequestedAtMs'
|
||||
| 'cancelReason'
|
||||
>
|
||||
>;
|
||||
readonly toolStep: Readonly<StepRunRecord>;
|
||||
readonly modelStep: Readonly<StepRunRecord>;
|
||||
readonly modelStartExists: boolean;
|
||||
readonly observedAtMs: number;
|
||||
}
|
||||
|
||||
export interface CopilotFailureDiagnosisPreModelTerminalizationRepository {
|
||||
findByRequestId(
|
||||
requestId: string,
|
||||
): Promise<Readonly<CopilotFailureDiagnosisPreModelTerminalizationReceipt> | null>;
|
||||
readAuthority(
|
||||
requestId: string,
|
||||
): Promise<Readonly<CopilotFailureDiagnosisPreModelTerminalizationAuthority>>;
|
||||
commit(
|
||||
command: Readonly<CopilotFailureDiagnosisPreModelTerminalizationCommand>,
|
||||
): Promise<
|
||||
Readonly<{
|
||||
status: 'created' | 'existing';
|
||||
receipt: Readonly<CopilotFailureDiagnosisPreModelTerminalizationReceipt>;
|
||||
}>
|
||||
>;
|
||||
}
|
||||
|
||||
export class InvalidCopilotFailureDiagnosisPreModelTerminalizationError extends TypeError {
|
||||
readonly code = 'COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_INVALID';
|
||||
|
||||
constructor(message: string) {
|
||||
super(
|
||||
`Copilot failure diagnosis pre-Model terminalization is invalid: ${message}`,
|
||||
);
|
||||
this.name = 'InvalidCopilotFailureDiagnosisPreModelTerminalizationError';
|
||||
}
|
||||
}
|
||||
|
||||
export class CopilotFailureDiagnosisPreModelTerminalizationConflictError extends Error {
|
||||
readonly code =
|
||||
'COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_CONFLICT';
|
||||
|
||||
constructor(message = 'durable terminalization authority changed') {
|
||||
super(
|
||||
`Copilot failure diagnosis pre-Model terminalization conflicts: ${message}`,
|
||||
);
|
||||
this.name = 'CopilotFailureDiagnosisPreModelTerminalizationConflictError';
|
||||
}
|
||||
}
|
||||
|
||||
export class CopilotFailureDiagnosisPreModelTerminalizationNotReadyError extends Error {
|
||||
readonly code =
|
||||
'COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_NOT_READY';
|
||||
|
||||
constructor() {
|
||||
super('Copilot failure diagnosis has no terminal pre-Model condition');
|
||||
this.name = 'CopilotFailureDiagnosisPreModelTerminalizationNotReadyError';
|
||||
}
|
||||
}
|
||||
|
||||
export class CopilotFailureDiagnosisPreModelTerminalizationUnavailableError extends Error {
|
||||
readonly code =
|
||||
'COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_UNAVAILABLE';
|
||||
|
||||
constructor(options?: ErrorOptions) {
|
||||
super(
|
||||
'Copilot failure diagnosis pre-Model terminalization is unavailable',
|
||||
options,
|
||||
);
|
||||
this.name =
|
||||
'CopilotFailureDiagnosisPreModelTerminalizationUnavailableError';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
import type { ToolJsonValue } from '@qinglong/runtime-core/tool-registry';
|
||||
import type { ToolExecutionCompletionRecord } from '@qinglong/runtime-core/tool-execution-completion';
|
||||
import type { ToolExecutionFailureCompletionRecord } from '@qinglong/runtime-core/tool-execution-failure-completion';
|
||||
import { BUILTIN_RUN_LOG_EXCERPT_TIMEOUT_SECONDS } from '@qinglong/runtime-core/builtin-run-log-excerpt-tool';
|
||||
import {
|
||||
STEP_RUN_TERMINAL_STATUSES,
|
||||
transitionStepRunMutation,
|
||||
type StepRunMutation,
|
||||
type StepRunRecord,
|
||||
type StepRunStatus,
|
||||
} from '@qinglong/runtime-core/step-run';
|
||||
|
||||
import {
|
||||
CopilotFailureDiagnosisPreModelTerminalizationConflictError,
|
||||
CopilotFailureDiagnosisPreModelTerminalizationNotReadyError,
|
||||
CopilotFailureDiagnosisPreModelTerminalizationUnavailableError,
|
||||
InvalidCopilotFailureDiagnosisPreModelTerminalizationError,
|
||||
type CopilotFailureDiagnosisPreModelTerminalizationReason,
|
||||
type CopilotFailureDiagnosisPreModelTerminalizationRepository,
|
||||
type CopilotFailureDiagnosisPreModelTerminalizationReceipt,
|
||||
} from './contracts';
|
||||
import {
|
||||
copilotFailureDiagnosisPreModelEvidenceDigest,
|
||||
copilotFailureDiagnosisPreModelTerminalizationMapping,
|
||||
copilotFailureDiagnosisTerminalizationIdentity,
|
||||
createCopilotFailureDiagnosisPreModelTerminalizationCommand,
|
||||
createCopilotFailureDiagnosisPreModelTerminalizationReceipt,
|
||||
terminalStepReference,
|
||||
} from './protocol';
|
||||
|
||||
export type CopilotFailureDiagnosisPreModelTerminalizationTrigger =
|
||||
| Readonly<{
|
||||
kind: 'tool_failure';
|
||||
completion: Readonly<ToolExecutionFailureCompletionRecord>;
|
||||
}>
|
||||
| Readonly<{
|
||||
kind: 'tool_projection';
|
||||
completion: Readonly<ToolExecutionCompletionRecord>;
|
||||
output: ToolJsonValue;
|
||||
}>
|
||||
| Readonly<{ kind: 'boundary' }>;
|
||||
|
||||
export interface CopilotFailureDiagnosisPreModelTerminalizationDependencies {
|
||||
readonly repository: CopilotFailureDiagnosisPreModelTerminalizationRepository;
|
||||
}
|
||||
|
||||
export interface CopilotFailureDiagnosisPreModelTerminalizationResult {
|
||||
readonly status: 'created' | 'existing';
|
||||
readonly receipt: Readonly<CopilotFailureDiagnosisPreModelTerminalizationReceipt>;
|
||||
}
|
||||
|
||||
function invalid(message: string): never {
|
||||
throw new InvalidCopilotFailureDiagnosisPreModelTerminalizationError(message);
|
||||
}
|
||||
|
||||
function conflict(message: string): never {
|
||||
throw new CopilotFailureDiagnosisPreModelTerminalizationConflictError(
|
||||
message,
|
||||
);
|
||||
}
|
||||
|
||||
function unavailable(cause?: unknown): never {
|
||||
throw new CopilotFailureDiagnosisPreModelTerminalizationUnavailableError({
|
||||
cause: cause instanceof Error ? cause : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function assertDependencies(
|
||||
value: CopilotFailureDiagnosisPreModelTerminalizationDependencies,
|
||||
): void {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
typeof value.repository?.findByRequestId !== 'function' ||
|
||||
typeof value.repository?.readAuthority !== 'function' ||
|
||||
typeof value.repository?.commit !== 'function'
|
||||
) {
|
||||
return invalid('dependencies are invalid');
|
||||
}
|
||||
}
|
||||
|
||||
function statusFromProjection(
|
||||
output: ToolJsonValue,
|
||||
runId: string,
|
||||
attemptId: string,
|
||||
): 'not_found' | 'pending' | 'missing' | 'retired' | null {
|
||||
if (!output || typeof output !== 'object' || Array.isArray(output)) {
|
||||
return conflict('Tool projection is not an object');
|
||||
}
|
||||
const record = output as Readonly<Record<string, ToolJsonValue>>;
|
||||
if (
|
||||
record.runId !== runId ||
|
||||
record.attemptId !== attemptId ||
|
||||
record.profile !== 'cluster-control' ||
|
||||
typeof record.status !== 'string' ||
|
||||
!['available', 'not_found', 'pending', 'missing', 'retired'].includes(
|
||||
record.status,
|
||||
)
|
||||
) {
|
||||
return conflict('Tool projection identity changed');
|
||||
}
|
||||
return record.status === 'available'
|
||||
? null
|
||||
: (record.status as 'not_found' | 'pending' | 'missing' | 'retired');
|
||||
}
|
||||
|
||||
function triggerEvidence(
|
||||
trigger: CopilotFailureDiagnosisPreModelTerminalizationTrigger,
|
||||
authority: Awaited<
|
||||
ReturnType<
|
||||
CopilotFailureDiagnosisPreModelTerminalizationRepository['readAuthority']
|
||||
>
|
||||
>,
|
||||
): Readonly<{
|
||||
reason: CopilotFailureDiagnosisPreModelTerminalizationReason;
|
||||
evidenceDigest: string;
|
||||
toolStartId: string | null;
|
||||
toolCompletionDigest: string | null;
|
||||
}> {
|
||||
const { plan, run, observedAtMs } = authority;
|
||||
if (trigger.kind === 'tool_failure') {
|
||||
const completion = trigger.completion;
|
||||
if (
|
||||
completion.runId !== plan.runId ||
|
||||
completion.stepRunId !== plan.toolStepRunId ||
|
||||
completion.outcome !== authority.toolStep.status ||
|
||||
completion.completedStepRunDigest !== authority.toolStep.stepRunDigest
|
||||
)
|
||||
return conflict('Tool failure completion changed');
|
||||
const reason =
|
||||
completion.outcome === 'timed_out' ? 'tool_timed_out' : 'tool_failed';
|
||||
return Object.freeze({
|
||||
reason,
|
||||
evidenceDigest: copilotFailureDiagnosisPreModelEvidenceDigest({
|
||||
reason,
|
||||
planDigest: plan.planDigest,
|
||||
toolCompletionDigest: completion.completionDigest,
|
||||
}),
|
||||
toolStartId: completion.startId,
|
||||
toolCompletionDigest: completion.completionDigest,
|
||||
});
|
||||
}
|
||||
if (trigger.kind === 'tool_projection') {
|
||||
const completion = trigger.completion;
|
||||
if (
|
||||
completion.runId !== plan.runId ||
|
||||
completion.stepRunId !== plan.toolStepRunId ||
|
||||
authority.toolStep.status !== 'succeeded' ||
|
||||
completion.completedStepRunDigest !== authority.toolStep.stepRunDigest
|
||||
)
|
||||
return conflict('Tool success completion changed');
|
||||
const status = statusFromProjection(
|
||||
trigger.output,
|
||||
plan.source.runId,
|
||||
plan.source.attemptId,
|
||||
);
|
||||
if (status === null)
|
||||
throw new CopilotFailureDiagnosisPreModelTerminalizationNotReadyError();
|
||||
const reason =
|
||||
`log_${status}` as CopilotFailureDiagnosisPreModelTerminalizationReason;
|
||||
return Object.freeze({
|
||||
reason,
|
||||
evidenceDigest: copilotFailureDiagnosisPreModelEvidenceDigest({
|
||||
reason,
|
||||
planDigest: plan.planDigest,
|
||||
toolCompletionDigest: completion.completionDigest,
|
||||
sourceStatus: status,
|
||||
}),
|
||||
toolStartId: completion.startId,
|
||||
toolCompletionDigest: completion.completionDigest,
|
||||
});
|
||||
}
|
||||
if (run.cancelRequestedAtMs !== undefined && run.cancelReason !== undefined) {
|
||||
const reason = 'cancellation_requested' as const;
|
||||
return Object.freeze({
|
||||
reason,
|
||||
evidenceDigest: copilotFailureDiagnosisPreModelEvidenceDigest({
|
||||
reason,
|
||||
planDigest: plan.planDigest,
|
||||
cancelRequestedAtMs: run.cancelRequestedAtMs,
|
||||
cancelReason: run.cancelReason,
|
||||
}),
|
||||
toolStartId: null,
|
||||
toolCompletionDigest: null,
|
||||
});
|
||||
}
|
||||
if (observedAtMs >= plan.deadlineAtMs) {
|
||||
const reason = 'deadline_exceeded' as const;
|
||||
return Object.freeze({
|
||||
reason,
|
||||
evidenceDigest: copilotFailureDiagnosisPreModelEvidenceDigest({
|
||||
reason,
|
||||
planDigest: plan.planDigest,
|
||||
deadlineAtMs: plan.deadlineAtMs,
|
||||
}),
|
||||
toolStartId: null,
|
||||
toolCompletionDigest: null,
|
||||
});
|
||||
}
|
||||
const requiredToolBudgetMs = BUILTIN_RUN_LOG_EXCERPT_TIMEOUT_SECONDS * 1_000;
|
||||
if (
|
||||
authority.toolStep.status === 'ready' &&
|
||||
observedAtMs + requiredToolBudgetMs > plan.deadlineAtMs
|
||||
) {
|
||||
const reason = 'tool_budget_exhausted' as const;
|
||||
return Object.freeze({
|
||||
reason,
|
||||
evidenceDigest: copilotFailureDiagnosisPreModelEvidenceDigest({
|
||||
reason,
|
||||
planDigest: plan.planDigest,
|
||||
deadlineAtMs: plan.deadlineAtMs,
|
||||
observedAtMs,
|
||||
requiredToolBudgetMs,
|
||||
}),
|
||||
toolStartId: null,
|
||||
toolCompletionDigest: null,
|
||||
});
|
||||
}
|
||||
throw new CopilotFailureDiagnosisPreModelTerminalizationNotReadyError();
|
||||
}
|
||||
|
||||
function targetFor(
|
||||
step: Readonly<StepRunRecord>,
|
||||
reason: CopilotFailureDiagnosisPreModelTerminalizationReason,
|
||||
outcome: 'failed' | 'timed_out' | 'cancelled',
|
||||
): StepRunStatus | null {
|
||||
if (STEP_RUN_TERMINAL_STATUSES.includes(step.status as never)) return null;
|
||||
if (reason === 'tool_failed' || reason === 'tool_timed_out') {
|
||||
return step.kind === 'model'
|
||||
? 'cancelled'
|
||||
: conflict('Tool Step is not terminal');
|
||||
}
|
||||
if (reason.startsWith('log_')) {
|
||||
return step.kind === 'model'
|
||||
? 'failed'
|
||||
: conflict('log evidence Tool is not terminal');
|
||||
}
|
||||
if (step.status === 'pending') return 'cancelled';
|
||||
return outcome;
|
||||
}
|
||||
|
||||
function transitionFacts(
|
||||
target: StepRunStatus,
|
||||
reason: CopilotFailureDiagnosisPreModelTerminalizationReason,
|
||||
): Readonly<{ resultCode: string; errorSummary?: string }> {
|
||||
if (target === 'failed') {
|
||||
return Object.freeze({
|
||||
resultCode: 'copilot_log_unavailable',
|
||||
errorSummary: 'Failure diagnosis source log is unavailable',
|
||||
});
|
||||
}
|
||||
if (target === 'timed_out') {
|
||||
return Object.freeze({
|
||||
resultCode: 'copilot_deadline_exceeded',
|
||||
errorSummary: 'Copilot failure diagnosis deadline exceeded',
|
||||
});
|
||||
}
|
||||
return Object.freeze({
|
||||
resultCode: reason.startsWith('tool_')
|
||||
? 'copilot_tool_dependency_failed'
|
||||
: 'copilot_cancelled',
|
||||
});
|
||||
}
|
||||
|
||||
function mutations(
|
||||
authority: Awaited<
|
||||
ReturnType<
|
||||
CopilotFailureDiagnosisPreModelTerminalizationRepository['readAuthority']
|
||||
>
|
||||
>,
|
||||
reason: CopilotFailureDiagnosisPreModelTerminalizationReason,
|
||||
outcome: 'failed' | 'timed_out' | 'cancelled',
|
||||
): readonly Readonly<StepRunMutation>[] {
|
||||
let runVersion = authority.run.version;
|
||||
let eventSequence = authority.run.eventSequence;
|
||||
const result: StepRunMutation[] = [];
|
||||
for (const step of [authority.toolStep, authority.modelStep]) {
|
||||
const target = targetFor(step, reason, outcome);
|
||||
if (target === null) continue;
|
||||
const facts = transitionFacts(target, reason);
|
||||
const mutationId = copilotFailureDiagnosisTerminalizationIdentity(
|
||||
'mutation',
|
||||
authority.plan.planDigest,
|
||||
reason,
|
||||
step.id,
|
||||
);
|
||||
const eventId = copilotFailureDiagnosisTerminalizationIdentity(
|
||||
'step-event',
|
||||
authority.plan.planDigest,
|
||||
reason,
|
||||
step.id,
|
||||
);
|
||||
const mutation = transitionStepRunMutation(
|
||||
step,
|
||||
{
|
||||
expectedVersion: step.version,
|
||||
expectedDigest: step.stepRunDigest,
|
||||
mutationId,
|
||||
to: target,
|
||||
atMs: authority.observedAtMs,
|
||||
resultCode: facts.resultCode,
|
||||
...(facts.errorSummary === undefined
|
||||
? {}
|
||||
: { errorSummary: facts.errorSummary }),
|
||||
},
|
||||
{
|
||||
expectedRunVersion: runVersion,
|
||||
expectedRunEventSequence: eventSequence,
|
||||
eventId,
|
||||
dedupeKey: eventId,
|
||||
actor: { type: 'system' },
|
||||
},
|
||||
);
|
||||
result.push(mutation);
|
||||
runVersion += 1;
|
||||
eventSequence += 1;
|
||||
}
|
||||
if (result.length < 1) return conflict('no non-terminal Step remains');
|
||||
return Object.freeze(result);
|
||||
}
|
||||
|
||||
export async function terminalizeCopilotFailureDiagnosisBeforeModel(
|
||||
requestId: string,
|
||||
trigger: CopilotFailureDiagnosisPreModelTerminalizationTrigger,
|
||||
dependencies: CopilotFailureDiagnosisPreModelTerminalizationDependencies,
|
||||
): Promise<Readonly<CopilotFailureDiagnosisPreModelTerminalizationResult>> {
|
||||
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(requestId))
|
||||
return invalid('request id is invalid');
|
||||
if (!trigger || typeof trigger !== 'object' || Array.isArray(trigger))
|
||||
return invalid('trigger is invalid');
|
||||
assertDependencies(dependencies);
|
||||
try {
|
||||
const existing = await dependencies.repository.findByRequestId(requestId);
|
||||
if (existing)
|
||||
return Object.freeze({ status: 'existing' as const, receipt: existing });
|
||||
const authority = await dependencies.repository.readAuthority(requestId);
|
||||
if (
|
||||
authority.plan.requestId !== requestId ||
|
||||
authority.run.id !== authority.plan.runId ||
|
||||
authority.run.status !== 'running' ||
|
||||
authority.run.version !== authority.run.eventSequence ||
|
||||
authority.toolStep.id !== authority.plan.toolStepRunId ||
|
||||
authority.toolStep.runId !== authority.plan.runId ||
|
||||
authority.toolStep.kind !== 'tool' ||
|
||||
authority.modelStep.id !== authority.plan.modelStepRunId ||
|
||||
authority.modelStep.runId !== authority.plan.runId ||
|
||||
authority.modelStep.kind !== 'model' ||
|
||||
authority.modelStartExists ||
|
||||
!Number.isSafeInteger(authority.observedAtMs) ||
|
||||
authority.observedAtMs < authority.plan.plannedAtMs
|
||||
)
|
||||
return conflict('pre-Model authority is invalid');
|
||||
const evidence = triggerEvidence(trigger, authority);
|
||||
const mapping = copilotFailureDiagnosisPreModelTerminalizationMapping(
|
||||
evidence.reason,
|
||||
authority.run.cancelReason,
|
||||
);
|
||||
const stepMutations = mutations(
|
||||
authority,
|
||||
evidence.reason,
|
||||
mapping.outcome,
|
||||
);
|
||||
const receipt = createCopilotFailureDiagnosisPreModelTerminalizationReceipt(
|
||||
{
|
||||
requestId,
|
||||
planDigest: authority.plan.planDigest,
|
||||
runId: authority.plan.runId,
|
||||
stage: mapping.stage,
|
||||
reason: evidence.reason,
|
||||
outcome: mapping.outcome,
|
||||
evidenceDigest: evidence.evidenceDigest,
|
||||
toolStartId: evidence.toolStartId,
|
||||
toolCompletionDigest: evidence.toolCompletionDigest,
|
||||
terminalSteps: stepMutations.map(terminalStepReference),
|
||||
finalRunVersion: authority.run.version + stepMutations.length + 1,
|
||||
finalRunEventSequence:
|
||||
authority.run.eventSequence + stepMutations.length + 1,
|
||||
finalizedAtMs: authority.observedAtMs,
|
||||
},
|
||||
);
|
||||
const command = createCopilotFailureDiagnosisPreModelTerminalizationCommand(
|
||||
{
|
||||
plan: authority.plan,
|
||||
expectedRunVersion: authority.run.version,
|
||||
expectedRunEventSequence: authority.run.eventSequence,
|
||||
stepMutations,
|
||||
receipt,
|
||||
},
|
||||
);
|
||||
return dependencies.repository.commit(command);
|
||||
} catch (cause) {
|
||||
if (
|
||||
cause instanceof
|
||||
InvalidCopilotFailureDiagnosisPreModelTerminalizationError ||
|
||||
cause instanceof
|
||||
CopilotFailureDiagnosisPreModelTerminalizationConflictError ||
|
||||
cause instanceof
|
||||
CopilotFailureDiagnosisPreModelTerminalizationNotReadyError ||
|
||||
cause instanceof
|
||||
CopilotFailureDiagnosisPreModelTerminalizationUnavailableError
|
||||
)
|
||||
throw cause;
|
||||
return unavailable(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,585 @@
|
||||
import type { PostgresClient, PostgresPool } from '@qinglong/runtime-core';
|
||||
import { BUILTIN_RUN_LOG_EXCERPT_TIMEOUT_SECONDS } from '@qinglong/runtime-core/builtin-run-log-excerpt-tool';
|
||||
import {
|
||||
normalizeStepRunMutation,
|
||||
normalizeStepRunRecord,
|
||||
type StepRunMutation,
|
||||
type StepRunRecord,
|
||||
} from '@qinglong/runtime-core/step-run';
|
||||
|
||||
import { normalizeCopilotFailureDiagnosisExecutionPlan } from '../admission/plan';
|
||||
import type { CopilotFailureDiagnosisExecutionPlan } from '../admission/contracts';
|
||||
import {
|
||||
CopilotFailureDiagnosisPreModelTerminalizationConflictError,
|
||||
CopilotFailureDiagnosisPreModelTerminalizationUnavailableError,
|
||||
type CopilotFailureDiagnosisPreModelTerminalizationAuthority,
|
||||
type CopilotFailureDiagnosisPreModelTerminalizationCommand,
|
||||
type CopilotFailureDiagnosisPreModelTerminalizationReceipt,
|
||||
type CopilotFailureDiagnosisPreModelTerminalizationRepository,
|
||||
} from './contracts';
|
||||
import {
|
||||
normalizeCopilotFailureDiagnosisPreModelTerminalizationCommand,
|
||||
normalizeCopilotFailureDiagnosisPreModelTerminalizationReceipt,
|
||||
} from './protocol';
|
||||
|
||||
const TABLE = '"ql3_ai"."copilot_failure_diagnosis_pre_model_terminalizations"';
|
||||
const REQUEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
|
||||
function conflict(message?: string): never {
|
||||
throw new CopilotFailureDiagnosisPreModelTerminalizationConflictError(
|
||||
message,
|
||||
);
|
||||
}
|
||||
|
||||
function unavailable(cause?: unknown): never {
|
||||
throw new CopilotFailureDiagnosisPreModelTerminalizationUnavailableError({
|
||||
cause: cause instanceof Error ? cause : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function object(value: unknown): Record<string, unknown> {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(value) as unknown;
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
return parsed as Record<string, unknown>;
|
||||
}
|
||||
} catch {
|
||||
// Mapped to the generic durable conflict below.
|
||||
}
|
||||
}
|
||||
return conflict();
|
||||
}
|
||||
|
||||
function integer(value: unknown): number {
|
||||
if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string' && /^(0|[1-9]\d*)$/.test(value)) {
|
||||
const parsed = Number(value);
|
||||
if (Number.isSafeInteger(parsed)) return parsed;
|
||||
}
|
||||
return conflict();
|
||||
}
|
||||
|
||||
function string(value: unknown): string {
|
||||
return typeof value === 'string' ? value : conflict();
|
||||
}
|
||||
|
||||
function optionalString(value: unknown): string | undefined {
|
||||
return value === null || value === undefined ? undefined : string(value);
|
||||
}
|
||||
|
||||
function same(left: unknown, right: unknown): boolean {
|
||||
return JSON.stringify(left) === JSON.stringify(right);
|
||||
}
|
||||
|
||||
async function authority(
|
||||
queryable: Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>,
|
||||
requestId: string,
|
||||
lock: boolean,
|
||||
): Promise<Readonly<CopilotFailureDiagnosisPreModelTerminalizationAuthority>> {
|
||||
const result = await queryable.query<Record<string, unknown>>(
|
||||
`SELECT admission.plan_json AS "planJson",
|
||||
run.id AS "runId", run.project_id AS "projectId",
|
||||
run.status AS "runStatus", run.version AS "runVersion",
|
||||
run.event_sequence AS "runEventSequence",
|
||||
run.cancel_requested_at_ms AS "cancelRequestedAtMs",
|
||||
run.cancel_reason AS "cancelReason",
|
||||
tool_step.step_run_json AS "toolStepJson",
|
||||
model_step.step_run_json AS "modelStepJson",
|
||||
(model_start.invocation_id IS NOT NULL) AS "modelStartExists",
|
||||
floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint
|
||||
AS "observedAtMs"
|
||||
FROM "ql3_ai"."copilot_failure_diagnosis_admissions" AS admission
|
||||
JOIN "ql3"."runs" AS run ON run.id = admission.run_id
|
||||
JOIN "ql3"."step_runs" AS tool_step
|
||||
ON tool_step.run_id = run.id
|
||||
AND tool_step.id = admission.tool_step_run_id
|
||||
JOIN "ql3"."step_runs" AS model_step
|
||||
ON model_step.run_id = run.id
|
||||
AND model_step.id = admission.model_step_run_id
|
||||
LEFT JOIN "ql3_ai"."model_invocation_starts" AS model_start
|
||||
ON model_start.invocation_id = admission.plan_json->>'modelInvocationId'
|
||||
WHERE admission.request_id = $1
|
||||
${lock ? 'FOR UPDATE OF run, tool_step, model_step' : ''}`,
|
||||
[requestId],
|
||||
);
|
||||
if (result.rows.length !== 1)
|
||||
return conflict('terminalization authority is absent');
|
||||
const row = result.rows[0]!;
|
||||
const plan = normalizeCopilotFailureDiagnosisExecutionPlan(
|
||||
object(row.planJson) as unknown as CopilotFailureDiagnosisExecutionPlan,
|
||||
);
|
||||
const toolStep = normalizeStepRunRecord(
|
||||
object(row.toolStepJson) as unknown as StepRunRecord,
|
||||
);
|
||||
const modelStep = normalizeStepRunRecord(
|
||||
object(row.modelStepJson) as unknown as StepRunRecord,
|
||||
);
|
||||
return Object.freeze({
|
||||
plan,
|
||||
run: Object.freeze({
|
||||
id: string(row.runId),
|
||||
projectId: string(row.projectId),
|
||||
status: string(row.runStatus) as never,
|
||||
version: integer(row.runVersion),
|
||||
eventSequence: integer(row.runEventSequence),
|
||||
...(row.cancelRequestedAtMs === null
|
||||
? {}
|
||||
: { cancelRequestedAtMs: integer(row.cancelRequestedAtMs) }),
|
||||
...(row.cancelReason === null
|
||||
? {}
|
||||
: { cancelReason: string(row.cancelReason) as never }),
|
||||
}),
|
||||
toolStep,
|
||||
modelStep,
|
||||
modelStartExists: row.modelStartExists === true,
|
||||
observedAtMs: integer(row.observedAtMs),
|
||||
});
|
||||
}
|
||||
|
||||
async function updateStep(
|
||||
client: PostgresClient,
|
||||
mutationValue: Readonly<StepRunMutation>,
|
||||
): Promise<void> {
|
||||
const mutation = normalizeStepRunMutation(mutationValue);
|
||||
const step = mutation.stepRun;
|
||||
const updated = await client.query(
|
||||
`UPDATE "ql3"."step_runs"
|
||||
SET status = $1, version = $2, attempt_count = $3,
|
||||
output_ref = $4, approval_request_id = $5, ready_at_ms = $6,
|
||||
started_at_ms = $7, finished_at_ms = $8, result_code = $9,
|
||||
error_summary = $10, updated_at_ms = $11,
|
||||
last_mutation_id = $12, step_run_digest = $13,
|
||||
step_run_json = $14::jsonb
|
||||
WHERE id = $15 AND run_id = $16 AND version = $17
|
||||
AND step_run_digest = $18 AND status = $19`,
|
||||
[
|
||||
step.status,
|
||||
step.version,
|
||||
step.attemptCount,
|
||||
step.outputRef,
|
||||
step.approvalRequestId,
|
||||
step.readyAtMs,
|
||||
step.startedAtMs,
|
||||
step.finishedAtMs,
|
||||
step.resultCode,
|
||||
step.errorSummary,
|
||||
step.updatedAtMs,
|
||||
step.lastMutationId,
|
||||
step.stepRunDigest,
|
||||
JSON.stringify(step),
|
||||
step.id,
|
||||
step.runId,
|
||||
mutation.expectedStepRunVersion,
|
||||
mutation.expectedStepRunDigest,
|
||||
mutation.previousStatus,
|
||||
],
|
||||
);
|
||||
if ((updated.rowCount ?? updated.rows.length) !== 1) return conflict();
|
||||
const run = await client.query(
|
||||
`UPDATE "ql3"."runs"
|
||||
SET version = version + 1, event_sequence = event_sequence + 1
|
||||
WHERE id = $1 AND status = 'running'
|
||||
AND version = $2 AND event_sequence = $3`,
|
||||
[
|
||||
mutation.runId,
|
||||
mutation.expectedRunVersion,
|
||||
mutation.expectedRunEventSequence,
|
||||
],
|
||||
);
|
||||
if ((run.rowCount ?? run.rows.length) !== 1) return conflict();
|
||||
const event = mutation.event;
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."run_events" (
|
||||
id, run_id, sequence, type, dedupe_key, actor_type, actor_id,
|
||||
attempt_id, step_run_id, payload, created_at_ms
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, NULL, $8, $9::jsonb, $10)`,
|
||||
[
|
||||
event.id,
|
||||
event.runId,
|
||||
event.sequence,
|
||||
event.type,
|
||||
event.dedupeKey,
|
||||
event.actorType,
|
||||
event.actorId ?? null,
|
||||
step.id,
|
||||
JSON.stringify(event.payload),
|
||||
event.createdAtMs,
|
||||
],
|
||||
);
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."step_run_mutations" (
|
||||
mutation_id, mutation_digest, run_id, step_run_id, step_run_digest,
|
||||
event_id, event_sequence, run_version, step_run_json, committed_at_ms
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb,
|
||||
floor(extract(epoch FROM transaction_timestamp()) * 1000)::bigint)`,
|
||||
[
|
||||
mutation.mutationId,
|
||||
mutation.mutationDigest,
|
||||
mutation.runId,
|
||||
step.id,
|
||||
step.stepRunDigest,
|
||||
event.id,
|
||||
event.sequence,
|
||||
mutation.expectedRunVersion + 1,
|
||||
JSON.stringify(step),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
function failure(
|
||||
receipt: Readonly<CopilotFailureDiagnosisPreModelTerminalizationReceipt>,
|
||||
): Readonly<{
|
||||
code: string;
|
||||
summary: string;
|
||||
}> {
|
||||
if (receipt.reason === 'tool_failed') {
|
||||
return Object.freeze({
|
||||
code: 'COPILOT_DIAGNOSIS_TOOL_FAILED',
|
||||
summary: 'Copilot diagnosis Tool failed',
|
||||
});
|
||||
}
|
||||
if (receipt.reason === 'tool_timed_out' || receipt.outcome === 'timed_out') {
|
||||
return Object.freeze({
|
||||
code: 'COPILOT_DIAGNOSIS_TIMED_OUT',
|
||||
summary: 'Copilot diagnosis timed out',
|
||||
});
|
||||
}
|
||||
if (receipt.stage === 'log') {
|
||||
return Object.freeze({
|
||||
code: 'COPILOT_DIAGNOSIS_LOG_UNAVAILABLE',
|
||||
summary: 'Copilot diagnosis log is unavailable',
|
||||
});
|
||||
}
|
||||
return Object.freeze({
|
||||
code: 'COPILOT_DIAGNOSIS_CANCELLED',
|
||||
summary: 'Copilot diagnosis was cancelled',
|
||||
});
|
||||
}
|
||||
|
||||
export class PostgresCopilotFailureDiagnosisPreModelTerminalizationRepository
|
||||
implements CopilotFailureDiagnosisPreModelTerminalizationRepository
|
||||
{
|
||||
constructor(private readonly pool: PostgresPool) {
|
||||
if (
|
||||
!pool ||
|
||||
typeof pool.query !== 'function' ||
|
||||
typeof pool.connect !== 'function'
|
||||
) {
|
||||
return unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
async findByRequestId(
|
||||
requestId: string,
|
||||
): Promise<Readonly<CopilotFailureDiagnosisPreModelTerminalizationReceipt> | null> {
|
||||
if (!REQUEST_ID_PATTERN.test(requestId))
|
||||
return conflict('request id is invalid');
|
||||
try {
|
||||
const result = await this.pool.query<Record<string, unknown>>(
|
||||
`SELECT receipt_json AS "receiptJson" FROM ${TABLE} WHERE request_id = $1`,
|
||||
[requestId],
|
||||
);
|
||||
if (result.rows.length > 1) return conflict();
|
||||
if (!result.rows[0]) return null;
|
||||
const receipt =
|
||||
normalizeCopilotFailureDiagnosisPreModelTerminalizationReceipt(
|
||||
object(
|
||||
result.rows[0].receiptJson,
|
||||
) as unknown as CopilotFailureDiagnosisPreModelTerminalizationReceipt,
|
||||
);
|
||||
const durable = await this.pool.query<Record<string, unknown>>(
|
||||
`SELECT run.status AS "runStatus", run.version AS "runVersion",
|
||||
run.event_sequence AS "runEventSequence",
|
||||
run.finished_at_ms AS "finishedAtMs",
|
||||
event.type AS "eventType", event.payload,
|
||||
event.created_at_ms AS "eventCreatedAtMs"
|
||||
FROM "ql3"."runs" AS run
|
||||
JOIN "ql3"."run_events" AS event
|
||||
ON event.run_id = run.id AND event.id = $1
|
||||
WHERE run.id = $2`,
|
||||
[receipt.runEventId, receipt.runId],
|
||||
);
|
||||
if (durable.rows.length !== 1) return conflict();
|
||||
const proof = durable.rows[0]!;
|
||||
const payload = object(proof.payload);
|
||||
if (
|
||||
string(proof.runStatus) !== receipt.outcome ||
|
||||
integer(proof.runVersion) !== receipt.finalRunVersion ||
|
||||
integer(proof.runEventSequence) !== receipt.finalRunEventSequence ||
|
||||
integer(proof.finishedAtMs) !== receipt.finalizedAtMs ||
|
||||
string(proof.eventType) !== `copilot.diagnosis.${receipt.outcome}` ||
|
||||
integer(proof.eventCreatedAtMs) !== receipt.finalizedAtMs ||
|
||||
payload.requestId !== receipt.requestId ||
|
||||
payload.planDigest !== receipt.planDigest ||
|
||||
payload.reason !== receipt.reason ||
|
||||
payload.evidenceDigest !== receipt.evidenceDigest
|
||||
)
|
||||
return conflict();
|
||||
for (const step of receipt.terminalSteps) {
|
||||
const stepProof = await this.pool.query<Record<string, unknown>>(
|
||||
`SELECT step.status, step.version,
|
||||
mutation.mutation_digest AS "mutationDigest",
|
||||
mutation.event_id AS "eventId"
|
||||
FROM "ql3"."step_runs" AS step
|
||||
JOIN "ql3"."step_run_mutations" AS mutation
|
||||
ON mutation.mutation_id = $1 AND mutation.step_run_id = step.id
|
||||
WHERE step.id = $2 AND step.run_id = $3`,
|
||||
[step.mutationId, step.stepRunId, receipt.runId],
|
||||
);
|
||||
if (
|
||||
stepProof.rows.length !== 1 ||
|
||||
string(stepProof.rows[0]!.status) !== step.status ||
|
||||
integer(stepProof.rows[0]!.version) !== step.version ||
|
||||
string(stepProof.rows[0]!.mutationDigest) !== step.mutationDigest ||
|
||||
string(stepProof.rows[0]!.eventId) !== step.eventId
|
||||
)
|
||||
return conflict();
|
||||
}
|
||||
return receipt;
|
||||
} catch (cause) {
|
||||
if (
|
||||
cause instanceof
|
||||
CopilotFailureDiagnosisPreModelTerminalizationConflictError
|
||||
)
|
||||
throw cause;
|
||||
return unavailable(cause);
|
||||
}
|
||||
}
|
||||
|
||||
async readAuthority(
|
||||
requestId: string,
|
||||
): Promise<
|
||||
Readonly<CopilotFailureDiagnosisPreModelTerminalizationAuthority>
|
||||
> {
|
||||
if (!REQUEST_ID_PATTERN.test(requestId))
|
||||
return conflict('request id is invalid');
|
||||
try {
|
||||
return await authority(this.pool, requestId, false);
|
||||
} catch (cause) {
|
||||
if (
|
||||
cause instanceof
|
||||
CopilotFailureDiagnosisPreModelTerminalizationConflictError
|
||||
)
|
||||
throw cause;
|
||||
return unavailable(cause);
|
||||
}
|
||||
}
|
||||
|
||||
async commit(
|
||||
commandValue: Readonly<CopilotFailureDiagnosisPreModelTerminalizationCommand>,
|
||||
): Promise<
|
||||
Readonly<{
|
||||
status: 'created' | 'existing';
|
||||
receipt: Readonly<CopilotFailureDiagnosisPreModelTerminalizationReceipt>;
|
||||
}>
|
||||
> {
|
||||
const command =
|
||||
normalizeCopilotFailureDiagnosisPreModelTerminalizationCommand(
|
||||
commandValue,
|
||||
);
|
||||
const existing = await this.findByRequestId(command.plan.requestId);
|
||||
if (existing) {
|
||||
if (existing.receiptDigest !== command.receipt.receiptDigest)
|
||||
return conflict();
|
||||
return Object.freeze({ status: 'existing' as const, receipt: existing });
|
||||
}
|
||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||
let client: PostgresClient;
|
||||
try {
|
||||
client = await this.pool.connect();
|
||||
} catch (cause) {
|
||||
return unavailable(cause);
|
||||
}
|
||||
let began = false;
|
||||
try {
|
||||
await client.query('BEGIN ISOLATION LEVEL SERIALIZABLE');
|
||||
began = true;
|
||||
await client.query(`SELECT set_config('statement_timeout', $1, true)`, [
|
||||
'5s',
|
||||
]);
|
||||
await client.query(`SELECT set_config('lock_timeout', $1, true)`, [
|
||||
'2s',
|
||||
]);
|
||||
const current = await authority(client, command.plan.requestId, true);
|
||||
if (
|
||||
!same(current.plan, command.plan) ||
|
||||
current.run.status !== 'running' ||
|
||||
current.run.version !== command.expectedRunVersion ||
|
||||
current.run.eventSequence !== command.expectedRunEventSequence ||
|
||||
current.modelStartExists ||
|
||||
current.observedAtMs < command.receipt.finalizedAtMs ||
|
||||
(command.receipt.reason === 'deadline_exceeded' &&
|
||||
current.observedAtMs < current.plan.deadlineAtMs) ||
|
||||
(command.receipt.reason === 'tool_budget_exhausted' &&
|
||||
(current.toolStep.status !== 'ready' ||
|
||||
current.observedAtMs +
|
||||
BUILTIN_RUN_LOG_EXCERPT_TIMEOUT_SECONDS * 1_000 <=
|
||||
current.plan.deadlineAtMs)) ||
|
||||
(command.receipt.reason === 'cancellation_requested' &&
|
||||
(current.run.cancelRequestedAtMs === undefined ||
|
||||
current.run.cancelReason === undefined))
|
||||
)
|
||||
return conflict();
|
||||
if (command.receipt.stage === 'tool') {
|
||||
const proof = await client.query<Record<string, unknown>>(
|
||||
`SELECT outcome, completion_digest AS "completionDigest"
|
||||
FROM "ql3"."tool_execution_failure_completions"
|
||||
WHERE start_id = $1 AND run_id = $2 AND step_run_id = $3`,
|
||||
[
|
||||
command.receipt.toolStartId,
|
||||
command.plan.runId,
|
||||
command.plan.toolStepRunId,
|
||||
],
|
||||
);
|
||||
if (
|
||||
proof.rows.length !== 1 ||
|
||||
string(proof.rows[0]!.completionDigest) !==
|
||||
command.receipt.toolCompletionDigest ||
|
||||
string(proof.rows[0]!.outcome) !== command.receipt.outcome
|
||||
)
|
||||
return conflict();
|
||||
} else if (command.receipt.stage === 'log') {
|
||||
const proof = await client.query<Record<string, unknown>>(
|
||||
`SELECT completion.completion_digest AS "completionDigest",
|
||||
unlock.tool_completion_digest AS "unlockDigest"
|
||||
FROM "ql3"."tool_execution_completions" AS completion
|
||||
JOIN "ql3_ai"."copilot_failure_diagnosis_tool_unlocks" AS unlock
|
||||
ON unlock.request_id = $1 AND unlock.start_id = completion.start_id
|
||||
WHERE completion.start_id = $2 AND completion.run_id = $3
|
||||
AND completion.step_run_id = $4`,
|
||||
[
|
||||
command.plan.requestId,
|
||||
command.receipt.toolStartId,
|
||||
command.plan.runId,
|
||||
command.plan.toolStepRunId,
|
||||
],
|
||||
);
|
||||
if (
|
||||
proof.rows.length !== 1 ||
|
||||
string(proof.rows[0]!.completionDigest) !==
|
||||
command.receipt.toolCompletionDigest ||
|
||||
string(proof.rows[0]!.unlockDigest) !==
|
||||
command.receipt.toolCompletionDigest
|
||||
)
|
||||
return conflict();
|
||||
}
|
||||
for (const mutation of command.stepMutations)
|
||||
await updateStep(client, mutation);
|
||||
const failureFact = failure(command.receipt);
|
||||
const updated = await client.query(
|
||||
`UPDATE "ql3"."runs"
|
||||
SET status = $1, version = $2, event_sequence = $3,
|
||||
output_ref = NULL, finished_at_ms = $4,
|
||||
error_code = $5, error_summary = $6
|
||||
WHERE id = $7 AND status = 'running'
|
||||
AND version = $8 AND event_sequence = $9`,
|
||||
[
|
||||
command.receipt.outcome,
|
||||
command.receipt.finalRunVersion,
|
||||
command.receipt.finalRunEventSequence,
|
||||
command.receipt.finalizedAtMs,
|
||||
failureFact.code,
|
||||
failureFact.summary,
|
||||
command.plan.runId,
|
||||
command.receipt.finalRunVersion - 1,
|
||||
command.receipt.finalRunEventSequence - 1,
|
||||
],
|
||||
);
|
||||
if ((updated.rowCount ?? updated.rows.length) !== 1) return conflict();
|
||||
const payload = JSON.stringify({
|
||||
requestId: command.receipt.requestId,
|
||||
planDigest: command.receipt.planDigest,
|
||||
reason: command.receipt.reason,
|
||||
evidenceDigest: command.receipt.evidenceDigest,
|
||||
outcome: command.receipt.outcome,
|
||||
});
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."run_events" (
|
||||
id, run_id, sequence, type, dedupe_key, actor_type, actor_id,
|
||||
attempt_id, step_run_id, payload, created_at_ms
|
||||
) VALUES ($1, $2, $3, $4, $1, 'system', NULL, NULL, NULL,
|
||||
$5::jsonb, $6)`,
|
||||
[
|
||||
command.receipt.runEventId,
|
||||
command.receipt.runId,
|
||||
command.receipt.finalRunEventSequence,
|
||||
`copilot.diagnosis.${command.receipt.outcome}`,
|
||||
payload,
|
||||
command.receipt.finalizedAtMs,
|
||||
],
|
||||
);
|
||||
await client.query(
|
||||
`INSERT INTO ${TABLE} (
|
||||
request_id, plan_digest, run_id, stage, reason, outcome,
|
||||
evidence_digest, tool_start_id, tool_completion_digest,
|
||||
terminal_steps_json, final_run_version,
|
||||
final_run_event_sequence, run_event_id, finalized_at_ms,
|
||||
receipt_digest, receipt_json
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb,
|
||||
$11, $12, $13, $14, $15, $16::jsonb)`,
|
||||
[
|
||||
command.receipt.requestId,
|
||||
command.receipt.planDigest,
|
||||
command.receipt.runId,
|
||||
command.receipt.stage,
|
||||
command.receipt.reason,
|
||||
command.receipt.outcome,
|
||||
command.receipt.evidenceDigest,
|
||||
command.receipt.toolStartId,
|
||||
command.receipt.toolCompletionDigest,
|
||||
JSON.stringify(command.receipt.terminalSteps),
|
||||
command.receipt.finalRunVersion,
|
||||
command.receipt.finalRunEventSequence,
|
||||
command.receipt.runEventId,
|
||||
command.receipt.finalizedAtMs,
|
||||
command.receipt.receiptDigest,
|
||||
JSON.stringify(command.receipt),
|
||||
],
|
||||
);
|
||||
await client.query('COMMIT');
|
||||
began = false;
|
||||
return Object.freeze({
|
||||
status: 'created' as const,
|
||||
receipt: command.receipt,
|
||||
});
|
||||
} catch (cause) {
|
||||
if (began) {
|
||||
try {
|
||||
await client.query('ROLLBACK');
|
||||
} catch {
|
||||
/* preserve */
|
||||
}
|
||||
}
|
||||
const state =
|
||||
cause && typeof cause === 'object' && 'code' in cause
|
||||
? String(cause.code)
|
||||
: '';
|
||||
if ((state === '40001' || state === '40P01') && attempt < 2) continue;
|
||||
const recovered = await this.findByRequestId(command.plan.requestId);
|
||||
if (recovered) {
|
||||
if (recovered.receiptDigest !== command.receipt.receiptDigest)
|
||||
return conflict();
|
||||
return Object.freeze({
|
||||
status: 'existing' as const,
|
||||
receipt: recovered,
|
||||
});
|
||||
}
|
||||
if (
|
||||
cause instanceof
|
||||
CopilotFailureDiagnosisPreModelTerminalizationConflictError
|
||||
)
|
||||
throw cause;
|
||||
return unavailable(cause);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
return unavailable();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
import {
|
||||
normalizeStepRunMutation,
|
||||
type StepRunMutation,
|
||||
} from '@qinglong/runtime-core/step-run';
|
||||
|
||||
import { normalizeCopilotFailureDiagnosisExecutionPlan } from '../admission/plan';
|
||||
import {
|
||||
COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_COMMAND_SCHEMA,
|
||||
COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_REASONS,
|
||||
COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_SCHEMA,
|
||||
InvalidCopilotFailureDiagnosisPreModelTerminalizationError,
|
||||
type CopilotFailureDiagnosisPreModelTerminalizationCommand,
|
||||
type CopilotFailureDiagnosisPreModelTerminalizationOutcome,
|
||||
type CopilotFailureDiagnosisPreModelTerminalizationReason,
|
||||
type CopilotFailureDiagnosisPreModelTerminalizationReceipt,
|
||||
type CopilotFailureDiagnosisPreModelTerminalizationStage,
|
||||
type CopilotFailureDiagnosisTerminalStepReference,
|
||||
} from './contracts';
|
||||
|
||||
const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/;
|
||||
const RUN_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,35}$/;
|
||||
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
|
||||
const RECEIPT_DOMAIN =
|
||||
'qinglong/copilot-failure-diagnosis-pre-model-terminalization-receipt@v1\0';
|
||||
const COMMAND_DOMAIN =
|
||||
'qinglong/copilot-failure-diagnosis-pre-model-terminalization-command@v1\0';
|
||||
const IDENTITY_DOMAIN =
|
||||
'qinglong/copilot-failure-diagnosis-pre-model-terminalization-identity@v1\0';
|
||||
|
||||
function invalid(message: string): never {
|
||||
throw new InvalidCopilotFailureDiagnosisPreModelTerminalizationError(message);
|
||||
}
|
||||
|
||||
function exact(
|
||||
value: object,
|
||||
expected: readonly string[],
|
||||
label: string,
|
||||
): void {
|
||||
const keys = Reflect.ownKeys(value);
|
||||
if (
|
||||
keys.length !== expected.length ||
|
||||
keys.some((key) => typeof key !== 'string' || !expected.includes(key))
|
||||
) {
|
||||
return invalid(`${label} shape is invalid`);
|
||||
}
|
||||
}
|
||||
|
||||
function text(value: unknown, pattern: RegExp, label: string): string {
|
||||
if (typeof value !== 'string' || !pattern.test(value)) {
|
||||
return invalid(`${label} is invalid`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function integer(value: unknown, minimum: number, label: string): number {
|
||||
if (!Number.isSafeInteger(value) || (value as number) < minimum) {
|
||||
return invalid(`${label} is invalid`);
|
||||
}
|
||||
return value as number;
|
||||
}
|
||||
|
||||
function hash(domain: string, value: unknown): string {
|
||||
return createHash('sha256')
|
||||
.update(domain)
|
||||
.update(JSON.stringify(value))
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
export function copilotFailureDiagnosisPreModelTerminalizationMapping(
|
||||
reason: CopilotFailureDiagnosisPreModelTerminalizationReason,
|
||||
cancellationReason?: string,
|
||||
): Readonly<{
|
||||
stage: CopilotFailureDiagnosisPreModelTerminalizationStage;
|
||||
outcome: CopilotFailureDiagnosisPreModelTerminalizationOutcome;
|
||||
}> {
|
||||
if (
|
||||
!COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_REASONS.includes(
|
||||
reason,
|
||||
)
|
||||
) {
|
||||
return invalid('reason is invalid');
|
||||
}
|
||||
if (reason === 'tool_failed')
|
||||
return Object.freeze({ stage: 'tool', outcome: 'failed' });
|
||||
if (reason === 'tool_timed_out') {
|
||||
return Object.freeze({ stage: 'tool', outcome: 'timed_out' });
|
||||
}
|
||||
if (reason.startsWith('log_'))
|
||||
return Object.freeze({ stage: 'log', outcome: 'failed' });
|
||||
if (reason === 'tool_budget_exhausted' || reason === 'deadline_exceeded') {
|
||||
return Object.freeze({ stage: 'deadline', outcome: 'timed_out' });
|
||||
}
|
||||
return Object.freeze({
|
||||
stage: 'cancellation',
|
||||
outcome: cancellationReason === 'timeout' ? 'timed_out' : 'cancelled',
|
||||
});
|
||||
}
|
||||
|
||||
export function copilotFailureDiagnosisTerminalizationIdentity(
|
||||
prefix: 'mutation' | 'step-event' | 'run-event',
|
||||
planDigest: string,
|
||||
reason: CopilotFailureDiagnosisPreModelTerminalizationReason,
|
||||
target: string,
|
||||
): string {
|
||||
const digest = hash(IDENTITY_DOMAIN, { prefix, planDigest, reason, target });
|
||||
if (prefix !== 'run-event' && prefix !== 'step-event') {
|
||||
return `cdx:${digest.slice(0, 31)}`;
|
||||
}
|
||||
const hex = digest.slice(0, 32).split('');
|
||||
hex[12] = '4';
|
||||
hex[16] = '8';
|
||||
const value = hex.join('');
|
||||
return `${value.slice(0, 8)}-${value.slice(8, 12)}-${value.slice(
|
||||
12,
|
||||
16,
|
||||
)}-${value.slice(16, 20)}-${value.slice(20)}`;
|
||||
}
|
||||
|
||||
function terminalStep(
|
||||
value: CopilotFailureDiagnosisTerminalStepReference,
|
||||
): Readonly<CopilotFailureDiagnosisTerminalStepReference> {
|
||||
exact(
|
||||
value,
|
||||
[
|
||||
'eventId',
|
||||
'mutationDigest',
|
||||
'mutationId',
|
||||
'status',
|
||||
'stepRunId',
|
||||
'version',
|
||||
],
|
||||
'terminal Step',
|
||||
);
|
||||
if (!['failed', 'timed_out', 'cancelled'].includes(value.status)) {
|
||||
return invalid('terminal Step status is invalid');
|
||||
}
|
||||
return Object.freeze({
|
||||
stepRunId: text(value.stepRunId, ID_PATTERN, 'StepRun id'),
|
||||
status: value.status,
|
||||
version: integer(value.version, 1, 'StepRun version'),
|
||||
mutationId: text(value.mutationId, ID_PATTERN, 'mutation id'),
|
||||
mutationDigest: text(
|
||||
value.mutationDigest,
|
||||
DIGEST_PATTERN,
|
||||
'mutation digest',
|
||||
),
|
||||
eventId: text(value.eventId, RUN_ID_PATTERN, 'event id'),
|
||||
});
|
||||
}
|
||||
|
||||
function unsignedReceipt(
|
||||
value: Omit<
|
||||
CopilotFailureDiagnosisPreModelTerminalizationReceipt,
|
||||
'receiptDigest'
|
||||
>,
|
||||
): object {
|
||||
return {
|
||||
schema: value.schema,
|
||||
requestId: value.requestId,
|
||||
planDigest: value.planDigest,
|
||||
runId: value.runId,
|
||||
stage: value.stage,
|
||||
reason: value.reason,
|
||||
outcome: value.outcome,
|
||||
evidenceDigest: value.evidenceDigest,
|
||||
toolStartId: value.toolStartId,
|
||||
toolCompletionDigest: value.toolCompletionDigest,
|
||||
terminalSteps: value.terminalSteps,
|
||||
finalRunVersion: value.finalRunVersion,
|
||||
finalRunEventSequence: value.finalRunEventSequence,
|
||||
runEventId: value.runEventId,
|
||||
finalizedAtMs: value.finalizedAtMs,
|
||||
};
|
||||
}
|
||||
|
||||
export function copilotFailureDiagnosisPreModelTerminalizationReceiptDigest(
|
||||
value: Omit<
|
||||
CopilotFailureDiagnosisPreModelTerminalizationReceipt,
|
||||
'receiptDigest'
|
||||
>,
|
||||
): string {
|
||||
return hash(RECEIPT_DOMAIN, unsignedReceipt(value));
|
||||
}
|
||||
|
||||
export function normalizeCopilotFailureDiagnosisPreModelTerminalizationReceipt(
|
||||
value: CopilotFailureDiagnosisPreModelTerminalizationReceipt,
|
||||
): Readonly<CopilotFailureDiagnosisPreModelTerminalizationReceipt> {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value))
|
||||
return invalid('receipt');
|
||||
exact(
|
||||
value,
|
||||
[
|
||||
'evidenceDigest',
|
||||
'finalRunEventSequence',
|
||||
'finalRunVersion',
|
||||
'finalizedAtMs',
|
||||
'outcome',
|
||||
'planDigest',
|
||||
'reason',
|
||||
'receiptDigest',
|
||||
'requestId',
|
||||
'runEventId',
|
||||
'runId',
|
||||
'schema',
|
||||
'stage',
|
||||
'terminalSteps',
|
||||
'toolCompletionDigest',
|
||||
'toolStartId',
|
||||
],
|
||||
'receipt',
|
||||
);
|
||||
if (
|
||||
value.schema !== COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_SCHEMA
|
||||
) {
|
||||
return invalid('receipt schema is invalid');
|
||||
}
|
||||
const mapping = copilotFailureDiagnosisPreModelTerminalizationMapping(
|
||||
value.reason,
|
||||
);
|
||||
if (
|
||||
value.stage !== mapping.stage ||
|
||||
(value.reason !== 'cancellation_requested' &&
|
||||
value.outcome !== mapping.outcome) ||
|
||||
(value.reason === 'cancellation_requested' &&
|
||||
!['cancelled', 'timed_out'].includes(value.outcome)) ||
|
||||
!Array.isArray(value.terminalSteps) ||
|
||||
value.terminalSteps.length < 1 ||
|
||||
value.terminalSteps.length > 2
|
||||
) {
|
||||
return invalid('receipt state is invalid');
|
||||
}
|
||||
const terminalSteps = Object.freeze(value.terminalSteps.map(terminalStep));
|
||||
if (
|
||||
new Set(terminalSteps.map((item) => item.stepRunId)).size !==
|
||||
terminalSteps.length
|
||||
) {
|
||||
return invalid('terminal Step identities are duplicated');
|
||||
}
|
||||
const unsigned = Object.freeze({
|
||||
schema: value.schema,
|
||||
requestId: text(value.requestId, ID_PATTERN, 'request id'),
|
||||
planDigest: text(value.planDigest, DIGEST_PATTERN, 'plan digest'),
|
||||
runId: text(value.runId, RUN_ID_PATTERN, 'Run id'),
|
||||
stage: value.stage,
|
||||
reason: value.reason,
|
||||
outcome: value.outcome,
|
||||
evidenceDigest: text(
|
||||
value.evidenceDigest,
|
||||
DIGEST_PATTERN,
|
||||
'evidence digest',
|
||||
),
|
||||
toolStartId:
|
||||
value.toolStartId === null
|
||||
? null
|
||||
: text(value.toolStartId, ID_PATTERN, 'Tool start id'),
|
||||
toolCompletionDigest:
|
||||
value.toolCompletionDigest === null
|
||||
? null
|
||||
: text(
|
||||
value.toolCompletionDigest,
|
||||
DIGEST_PATTERN,
|
||||
'Tool completion digest',
|
||||
),
|
||||
terminalSteps,
|
||||
finalRunVersion: integer(value.finalRunVersion, 1, 'final Run version'),
|
||||
finalRunEventSequence: integer(
|
||||
value.finalRunEventSequence,
|
||||
1,
|
||||
'final Run event sequence',
|
||||
),
|
||||
runEventId: text(value.runEventId, RUN_ID_PATTERN, 'Run event id'),
|
||||
finalizedAtMs: integer(value.finalizedAtMs, 0, 'finalized time'),
|
||||
} satisfies Omit<CopilotFailureDiagnosisPreModelTerminalizationReceipt, 'receiptDigest'>);
|
||||
if (
|
||||
unsigned.finalRunVersion !== unsigned.finalRunEventSequence ||
|
||||
unsigned.runEventId !==
|
||||
copilotFailureDiagnosisTerminalizationIdentity(
|
||||
'run-event',
|
||||
unsigned.planDigest,
|
||||
unsigned.reason,
|
||||
unsigned.runId,
|
||||
) ||
|
||||
(unsigned.stage === 'tool' || unsigned.stage === 'log') !==
|
||||
(unsigned.toolStartId !== null &&
|
||||
unsigned.toolCompletionDigest !== null) ||
|
||||
text(value.receiptDigest, DIGEST_PATTERN, 'receipt digest') !==
|
||||
copilotFailureDiagnosisPreModelTerminalizationReceiptDigest(unsigned)
|
||||
) {
|
||||
return invalid('receipt evidence is invalid');
|
||||
}
|
||||
return Object.freeze({ ...unsigned, receiptDigest: value.receiptDigest });
|
||||
}
|
||||
|
||||
export function createCopilotFailureDiagnosisPreModelTerminalizationReceipt(
|
||||
value: Omit<
|
||||
CopilotFailureDiagnosisPreModelTerminalizationReceipt,
|
||||
'schema' | 'runEventId' | 'receiptDigest'
|
||||
>,
|
||||
): Readonly<CopilotFailureDiagnosisPreModelTerminalizationReceipt> {
|
||||
const unsigned = Object.freeze({
|
||||
schema: COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_SCHEMA,
|
||||
...value,
|
||||
runEventId: copilotFailureDiagnosisTerminalizationIdentity(
|
||||
'run-event',
|
||||
value.planDigest,
|
||||
value.reason,
|
||||
value.runId,
|
||||
),
|
||||
});
|
||||
return normalizeCopilotFailureDiagnosisPreModelTerminalizationReceipt({
|
||||
...unsigned,
|
||||
receiptDigest:
|
||||
copilotFailureDiagnosisPreModelTerminalizationReceiptDigest(unsigned),
|
||||
});
|
||||
}
|
||||
|
||||
function commandUnsigned(
|
||||
value: Omit<
|
||||
CopilotFailureDiagnosisPreModelTerminalizationCommand,
|
||||
'commandDigest'
|
||||
>,
|
||||
): object {
|
||||
return {
|
||||
schema: value.schema,
|
||||
plan: value.plan,
|
||||
expectedRunVersion: value.expectedRunVersion,
|
||||
expectedRunEventSequence: value.expectedRunEventSequence,
|
||||
stepMutations: value.stepMutations,
|
||||
receipt: value.receipt,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeCopilotFailureDiagnosisPreModelTerminalizationCommand(
|
||||
value: CopilotFailureDiagnosisPreModelTerminalizationCommand,
|
||||
): Readonly<CopilotFailureDiagnosisPreModelTerminalizationCommand> {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value))
|
||||
return invalid('command');
|
||||
exact(
|
||||
value,
|
||||
[
|
||||
'commandDigest',
|
||||
'expectedRunEventSequence',
|
||||
'expectedRunVersion',
|
||||
'plan',
|
||||
'receipt',
|
||||
'schema',
|
||||
'stepMutations',
|
||||
],
|
||||
'command',
|
||||
);
|
||||
if (
|
||||
value.schema !==
|
||||
COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_COMMAND_SCHEMA
|
||||
) {
|
||||
return invalid('command schema is invalid');
|
||||
}
|
||||
const plan = normalizeCopilotFailureDiagnosisExecutionPlan(value.plan);
|
||||
const receipt =
|
||||
normalizeCopilotFailureDiagnosisPreModelTerminalizationReceipt(
|
||||
value.receipt,
|
||||
);
|
||||
if (
|
||||
!Array.isArray(value.stepMutations) ||
|
||||
value.stepMutations.length !== receipt.terminalSteps.length
|
||||
) {
|
||||
return invalid('Step mutation count is invalid');
|
||||
}
|
||||
const stepMutations = Object.freeze(
|
||||
value.stepMutations.map(normalizeStepRunMutation),
|
||||
);
|
||||
for (let index = 0; index < stepMutations.length; index += 1) {
|
||||
const mutation = stepMutations[index]!;
|
||||
const reference = receipt.terminalSteps[index]!;
|
||||
if (
|
||||
mutation.runId !== plan.runId ||
|
||||
mutation.stepRun.id !== reference.stepRunId ||
|
||||
mutation.stepRun.status !== reference.status ||
|
||||
mutation.stepRun.version !== reference.version ||
|
||||
mutation.mutationId !== reference.mutationId ||
|
||||
mutation.mutationDigest !== reference.mutationDigest ||
|
||||
mutation.event.id !== reference.eventId
|
||||
)
|
||||
return invalid('Step mutation evidence changed');
|
||||
}
|
||||
const expectedRunVersion = integer(
|
||||
value.expectedRunVersion,
|
||||
1,
|
||||
'expected Run version',
|
||||
);
|
||||
const expectedRunEventSequence = integer(
|
||||
value.expectedRunEventSequence,
|
||||
1,
|
||||
'expected Run event sequence',
|
||||
);
|
||||
if (
|
||||
expectedRunVersion !== expectedRunEventSequence ||
|
||||
receipt.requestId !== plan.requestId ||
|
||||
receipt.planDigest !== plan.planDigest ||
|
||||
receipt.runId !== plan.runId ||
|
||||
receipt.finalRunVersion !== expectedRunVersion + stepMutations.length + 1
|
||||
)
|
||||
return invalid('command aggregate fence is invalid');
|
||||
const unsigned = Object.freeze({
|
||||
schema: value.schema,
|
||||
plan,
|
||||
expectedRunVersion,
|
||||
expectedRunEventSequence,
|
||||
stepMutations,
|
||||
receipt,
|
||||
});
|
||||
if (
|
||||
text(value.commandDigest, DIGEST_PATTERN, 'command digest') !==
|
||||
hash(COMMAND_DOMAIN, commandUnsigned(unsigned))
|
||||
) {
|
||||
return invalid('command digest does not match');
|
||||
}
|
||||
return Object.freeze({ ...unsigned, commandDigest: value.commandDigest });
|
||||
}
|
||||
|
||||
export function createCopilotFailureDiagnosisPreModelTerminalizationCommand(
|
||||
value: Omit<
|
||||
CopilotFailureDiagnosisPreModelTerminalizationCommand,
|
||||
'schema' | 'commandDigest'
|
||||
>,
|
||||
): Readonly<CopilotFailureDiagnosisPreModelTerminalizationCommand> {
|
||||
const unsigned = Object.freeze({
|
||||
schema: COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_COMMAND_SCHEMA,
|
||||
...value,
|
||||
});
|
||||
return normalizeCopilotFailureDiagnosisPreModelTerminalizationCommand({
|
||||
...unsigned,
|
||||
commandDigest: hash(COMMAND_DOMAIN, commandUnsigned(unsigned)),
|
||||
});
|
||||
}
|
||||
|
||||
export function copilotFailureDiagnosisPreModelEvidenceDigest(
|
||||
value: Readonly<{
|
||||
reason: CopilotFailureDiagnosisPreModelTerminalizationReason;
|
||||
planDigest: string;
|
||||
toolCompletionDigest?: string;
|
||||
sourceStatus?: string;
|
||||
deadlineAtMs?: number;
|
||||
observedAtMs?: number;
|
||||
requiredToolBudgetMs?: number;
|
||||
cancelRequestedAtMs?: number;
|
||||
cancelReason?: string;
|
||||
}>,
|
||||
): string {
|
||||
return hash(`${IDENTITY_DOMAIN}evidence\0`, value);
|
||||
}
|
||||
|
||||
export function terminalStepReference(
|
||||
mutation: Readonly<StepRunMutation>,
|
||||
): Readonly<CopilotFailureDiagnosisTerminalStepReference> {
|
||||
const normalized = normalizeStepRunMutation(mutation);
|
||||
if (
|
||||
!['failed', 'timed_out', 'cancelled'].includes(normalized.stepRun.status)
|
||||
) {
|
||||
return invalid('Step mutation is not terminal');
|
||||
}
|
||||
return terminalStep({
|
||||
stepRunId: normalized.stepRun.id,
|
||||
status: normalized.stepRun.status as 'failed' | 'timed_out' | 'cancelled',
|
||||
version: normalized.stepRun.version,
|
||||
mutationId: normalized.mutationId,
|
||||
mutationDigest: normalized.mutationDigest,
|
||||
eventId: normalized.event.id,
|
||||
});
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import type {
|
||||
ToolExecutionCompletionRecord,
|
||||
ToolExecutionResultArtifactReference,
|
||||
} from '@qinglong/runtime-core/tool-execution-completion';
|
||||
import type { ToolJsonValue } from '@qinglong/runtime-core/tool-registry';
|
||||
import type { ToolExecutionFailureCompletionRecord } from '@qinglong/runtime-core/tool-execution-failure-completion';
|
||||
import type { ToolPolicyAuthorizer } from '@qinglong/runtime-core/tool-registry';
|
||||
|
||||
import type {
|
||||
@@ -76,12 +78,14 @@ export type CopilotFailureDiagnosisToolExecutionResult =
|
||||
completionStatus: 'created' | 'existing';
|
||||
unlockStatus: 'created' | 'existing';
|
||||
completion: Readonly<ToolExecutionCompletionRecord>;
|
||||
output: ToolJsonValue;
|
||||
unlock: Readonly<CopilotFailureDiagnosisToolUnlockReceipt>;
|
||||
}>
|
||||
| Readonly<{
|
||||
outcome: 'failed' | 'timed_out';
|
||||
completionStatus: 'created' | 'existing';
|
||||
unlockStatus: null;
|
||||
completion: Readonly<ToolExecutionFailureCompletionRecord>;
|
||||
}>;
|
||||
|
||||
export class InvalidCopilotFailureDiagnosisToolExecutionError extends TypeError {
|
||||
|
||||
@@ -586,6 +586,7 @@ export async function executeCopilotFailureDiagnosisTool(
|
||||
outcome: completed.outcome,
|
||||
completionStatus: completed.status,
|
||||
unlockStatus: null,
|
||||
completion: completed.completion,
|
||||
});
|
||||
}
|
||||
const unlock = await unlockModel(plan, completed.completion, dependencies);
|
||||
@@ -594,6 +595,7 @@ export async function executeCopilotFailureDiagnosisTool(
|
||||
completionStatus: completed.status,
|
||||
unlockStatus: unlock.status,
|
||||
completion: completed.completion,
|
||||
output: completed.output,
|
||||
unlock: unlock.receipt,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -67,6 +67,8 @@ export const POSTGRES_COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_MIGRATION_ID =
|
||||
'pg-9019-ai-copilot-failure-diagnosis-tool-unlocks';
|
||||
export const POSTGRES_COPILOT_FAILURE_DIAGNOSIS_MODEL_EXECUTION_MIGRATION_ID =
|
||||
'pg-9020-ai-copilot-failure-diagnosis-model-executions';
|
||||
export const POSTGRES_COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_MIGRATION_ID =
|
||||
'pg-9021-ai-copilot-failure-diagnosis-pre-model-terminalizations';
|
||||
export const POSTGRES_MODEL_INVOCATION_SCHEMA = 'ql3_ai';
|
||||
export const LOCAL_MODEL_INVOCATION_MIGRATION_HISTORY_TABLE =
|
||||
'QingLong3AiSchemaMigrations';
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_ADMISSION_MIGRATION_ID,
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_MIGRATION_ID,
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_MODEL_EXECUTION_MIGRATION_ID,
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_MIGRATION_ID,
|
||||
POSTGRES_MODEL_INVOCATION_SCHEMA,
|
||||
POSTGRES_MODEL_INVOCATION_MIGRATION_HISTORY_TABLE,
|
||||
} from './identities';
|
||||
@@ -67,6 +68,7 @@ const POSTGRES_HISTORY_IDENTITY = Object.freeze({
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_ADMISSION_MIGRATION_ID,
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_MIGRATION_ID,
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_MODEL_EXECUTION_MIGRATION_ID,
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_MIGRATION_ID,
|
||||
]),
|
||||
streamId: POSTGRES_MODEL_INVOCATION_MIGRATION_STREAM_ID,
|
||||
dialect: 'postgresql' as const,
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_ADMISSION_MIGRATION_ID,
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_MIGRATION_ID,
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_MODEL_EXECUTION_MIGRATION_ID,
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_MIGRATION_ID,
|
||||
POSTGRES_MODEL_INVOCATION_SCHEMA,
|
||||
} from '../identities';
|
||||
import { defineSqlMigration } from '../shared';
|
||||
@@ -14,6 +15,8 @@ const SOURCE_SNAPSHOT_FUNCTION =
|
||||
const TOOL_UNLOCK_TABLE = 'copilot_failure_diagnosis_tool_unlocks';
|
||||
const MODEL_OUTPUT_TABLE = 'copilot_failure_diagnosis_model_outputs';
|
||||
const FINALIZATION_TABLE = 'copilot_failure_diagnosis_finalizations';
|
||||
const PRE_MODEL_TERMINALIZATION_TABLE =
|
||||
'copilot_failure_diagnosis_pre_model_terminalizations';
|
||||
|
||||
const POSTGRES_COPILOT_FAILURE_DIAGNOSIS_ADMISSION_TABLE_SQL = `
|
||||
CREATE TABLE "${POSTGRES_MODEL_INVOCATION_SCHEMA}"."${ADMISSION_TABLE}" (
|
||||
@@ -480,8 +483,102 @@ const postgresCopilotFailureDiagnosisModelExecutionMigration =
|
||||
(context, statement) => context.query(statement).then(() => undefined),
|
||||
);
|
||||
|
||||
const POSTGRES_COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_TABLE_SQL = `
|
||||
CREATE TABLE "${POSTGRES_MODEL_INVOCATION_SCHEMA}"."${PRE_MODEL_TERMINALIZATION_TABLE}" (
|
||||
request_id varchar(128) PRIMARY KEY,
|
||||
plan_digest char(64) NOT NULL UNIQUE,
|
||||
run_id varchar(36) NOT NULL UNIQUE,
|
||||
stage varchar(16) NOT NULL,
|
||||
reason varchar(32) NOT NULL,
|
||||
outcome varchar(16) NOT NULL,
|
||||
evidence_digest char(64) NOT NULL,
|
||||
tool_start_id varchar(128),
|
||||
tool_completion_digest char(64),
|
||||
terminal_steps_json jsonb NOT NULL,
|
||||
final_run_version integer NOT NULL,
|
||||
final_run_event_sequence integer NOT NULL,
|
||||
run_event_id varchar(36) NOT NULL UNIQUE,
|
||||
finalized_at_ms bigint NOT NULL,
|
||||
receipt_digest char(64) NOT NULL UNIQUE,
|
||||
receipt_json jsonb NOT NULL,
|
||||
CONSTRAINT ql3_ai_copilot_pre_model_terminalization_admission_fk
|
||||
FOREIGN KEY (request_id)
|
||||
REFERENCES "${POSTGRES_MODEL_INVOCATION_SCHEMA}"."${ADMISSION_TABLE}"
|
||||
(request_id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_ai_copilot_pre_model_terminalization_run_fk
|
||||
FOREIGN KEY (run_id) REFERENCES "ql3"."runs" (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_ai_copilot_pre_model_terminalization_event_fk
|
||||
FOREIGN KEY (run_event_id) REFERENCES "ql3"."run_events" (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT ql3_ai_copilot_pre_model_terminalization_state_check CHECK (
|
||||
stage IN ('tool', 'log', 'deadline', 'cancellation') AND
|
||||
reason IN (
|
||||
'tool_failed', 'tool_timed_out', 'log_not_found', 'log_pending',
|
||||
'log_missing', 'log_retired', 'tool_budget_exhausted',
|
||||
'deadline_exceeded',
|
||||
'cancellation_requested'
|
||||
) AND
|
||||
outcome IN ('failed', 'timed_out', 'cancelled') AND
|
||||
((stage = 'tool' AND reason IN ('tool_failed', 'tool_timed_out')) OR
|
||||
(stage = 'log' AND reason IN (
|
||||
'log_not_found', 'log_pending', 'log_missing', 'log_retired'
|
||||
)) OR
|
||||
(stage = 'deadline' AND reason IN (
|
||||
'tool_budget_exhausted', 'deadline_exceeded'
|
||||
)) OR
|
||||
(stage = 'cancellation' AND reason = 'cancellation_requested')) AND
|
||||
((stage IN ('tool', 'log') AND tool_start_id IS NOT NULL AND
|
||||
tool_completion_digest IS NOT NULL) OR
|
||||
(stage IN ('deadline', 'cancellation') AND tool_start_id IS NULL AND
|
||||
tool_completion_digest IS NULL)) AND
|
||||
final_run_version >= 1 AND
|
||||
final_run_event_sequence = final_run_version AND
|
||||
finalized_at_ms >= 0
|
||||
),
|
||||
CONSTRAINT ql3_ai_copilot_pre_model_terminalization_digest_check CHECK (
|
||||
plan_digest ~ '^[0-9a-f]{64}$' AND
|
||||
evidence_digest ~ '^[0-9a-f]{64}$' AND
|
||||
(tool_completion_digest IS NULL OR
|
||||
tool_completion_digest ~ '^[0-9a-f]{64}$') AND
|
||||
receipt_digest ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT ql3_ai_copilot_pre_model_terminalization_json_check CHECK (
|
||||
jsonb_typeof(terminal_steps_json) = 'array' AND
|
||||
jsonb_array_length(terminal_steps_json) BETWEEN 1 AND 2 AND
|
||||
jsonb_typeof(receipt_json) = 'object' AND
|
||||
octet_length(receipt_json::text) BETWEEN 2 AND 32768 AND
|
||||
receipt_json @> jsonb_build_object(
|
||||
'schema',
|
||||
'qinglong/copilot-failure-diagnosis-pre-model-terminalization@v1',
|
||||
'requestId', request_id, 'planDigest', plan_digest,
|
||||
'runId', run_id, 'stage', stage, 'reason', reason,
|
||||
'outcome', outcome, 'evidenceDigest', evidence_digest,
|
||||
'terminalSteps', terminal_steps_json,
|
||||
'finalRunVersion', final_run_version,
|
||||
'finalRunEventSequence', final_run_event_sequence,
|
||||
'runEventId', run_event_id, 'finalizedAtMs', finalized_at_ms,
|
||||
'receiptDigest', receipt_digest
|
||||
)
|
||||
)
|
||||
)`;
|
||||
|
||||
const postgresCopilotFailureDiagnosisPreModelTerminalizationMigration =
|
||||
defineSqlMigration<PostgresQueryable>(
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_MIGRATION_ID,
|
||||
[
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_TABLE_SQL,
|
||||
`REVOKE ALL ON TABLE
|
||||
"${POSTGRES_MODEL_INVOCATION_SCHEMA}"."${PRE_MODEL_TERMINALIZATION_TABLE}"
|
||||
FROM PUBLIC`,
|
||||
`GRANT SELECT, INSERT ON TABLE
|
||||
"${POSTGRES_MODEL_INVOCATION_SCHEMA}"."${PRE_MODEL_TERMINALIZATION_TABLE}"
|
||||
TO ql3_runtime`,
|
||||
],
|
||||
(context, statement) => context.query(statement).then(() => undefined),
|
||||
);
|
||||
|
||||
export const postgresCopilotMigrations = Object.freeze([
|
||||
postgresCopilotFailureDiagnosisAdmissionMigration,
|
||||
postgresCopilotFailureDiagnosisToolUnlockMigration,
|
||||
postgresCopilotFailureDiagnosisModelExecutionMigration,
|
||||
postgresCopilotFailureDiagnosisPreModelTerminalizationMigration,
|
||||
]);
|
||||
|
||||
@@ -5,6 +5,9 @@ const {
|
||||
CopilotFailureDiagnosisApplicationService,
|
||||
CopilotFailureDiagnosisApplicationUnavailableError,
|
||||
} = require('@qinglong/ai/failure-diagnosis-application');
|
||||
const {
|
||||
CopilotFailureDiagnosisPreModelTerminalizationNotReadyError,
|
||||
} = require('@qinglong/ai/failure-diagnosis-pre-model-terminalization');
|
||||
const {
|
||||
BUILTIN_RUN_LOG_EXCERPT_TOOL_DEFINITION,
|
||||
} = require('@qinglong/runtime-core/builtin-run-log-excerpt-tool');
|
||||
@@ -80,9 +83,12 @@ function fixture(options = {}) {
|
||||
let resolvedKeyCopies = [];
|
||||
let toolCalls = 0;
|
||||
let modelCalls = 0;
|
||||
let terminalizationCalls = 0;
|
||||
let releaseTool;
|
||||
const toolGate = options.blockTool
|
||||
? new Promise((resolve) => { releaseTool = resolve; })
|
||||
? new Promise((resolve) => {
|
||||
releaseTool = resolve;
|
||||
})
|
||||
: Promise.resolve();
|
||||
const admissions = {
|
||||
async findByRequestId(requestId) {
|
||||
@@ -108,20 +114,30 @@ function fixture(options = {}) {
|
||||
},
|
||||
};
|
||||
const snapshots = {
|
||||
async findCurrent() { return { snapshot: snapshot(), committedAtMs: NOW }; },
|
||||
async findCurrent() {
|
||||
return { snapshot: snapshot(), committedAtMs: NOW };
|
||||
},
|
||||
};
|
||||
const runs = {
|
||||
async findRunById() {
|
||||
return {
|
||||
id: 'source-run-1', projectId: 'project-1', status: 'failed',
|
||||
version: 8, eventSequence: 8,
|
||||
id: 'source-run-1',
|
||||
projectId: 'project-1',
|
||||
status: 'failed',
|
||||
version: 8,
|
||||
eventSequence: 8,
|
||||
};
|
||||
},
|
||||
async findLatestAttemptByRunId() {
|
||||
return {
|
||||
id: 'source-attempt-1', runId: 'source-run-1', attempt: 1,
|
||||
status: 'failed', executorType: 'remote_worker', callbackSequence: 0,
|
||||
createdAtMs: NOW - 3000, finishedAtMs: NOW - 1000,
|
||||
id: 'source-attempt-1',
|
||||
runId: 'source-run-1',
|
||||
attempt: 1,
|
||||
status: 'failed',
|
||||
executorType: 'remote_worker',
|
||||
callbackSequence: 0,
|
||||
createdAtMs: NOW - 3000,
|
||||
finishedAtMs: NOW - 1000,
|
||||
logArtifactId: `wlog-${'d'.repeat(30)}`,
|
||||
};
|
||||
},
|
||||
@@ -140,8 +156,12 @@ function fixture(options = {}) {
|
||||
artifacts = { input, preview };
|
||||
return { status: 'inserted' };
|
||||
},
|
||||
async findInput() { return artifacts?.input ?? null; },
|
||||
async findPreview() { return artifacts?.preview ?? null; },
|
||||
async findInput() {
|
||||
return artifacts?.input ?? null;
|
||||
},
|
||||
async findPreview() {
|
||||
return artifacts?.preview ?? null;
|
||||
},
|
||||
};
|
||||
const invocationKeys = {
|
||||
async active() {
|
||||
@@ -156,23 +176,58 @@ function fixture(options = {}) {
|
||||
return { keyId, key: copy };
|
||||
},
|
||||
};
|
||||
const unlocks = { async findByRequestId() { return null; }, async commit() {} };
|
||||
const unlocks = {
|
||||
async findByRequestId() {
|
||||
return null;
|
||||
},
|
||||
async commit() {},
|
||||
};
|
||||
const tool = {
|
||||
admissions,
|
||||
snapshots,
|
||||
runs,
|
||||
artifacts: artifactRepository,
|
||||
invocationKeys,
|
||||
resultKeys: { async resolve() { return null; } },
|
||||
stepRuns: { async findById() { return null; } },
|
||||
barriers: {}, completions: {}, failureCompletions: {},
|
||||
resultKeyCatalog: {}, resultRekeys: {}, logs: {}, unlocks,
|
||||
resultKeys: {
|
||||
async resolve() {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
stepRuns: {
|
||||
async findById() {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
barriers: {},
|
||||
completions: {},
|
||||
failureCompletions: {},
|
||||
resultKeyCatalog: {},
|
||||
resultRekeys: {},
|
||||
logs: {},
|
||||
unlocks,
|
||||
};
|
||||
const model = {
|
||||
admissions,
|
||||
unlocks,
|
||||
toolResults: {}, modelInvocations: {}, outputs: {}, gateway: {},
|
||||
successfulCompletion: {}, finalizations: {},
|
||||
toolResults: {},
|
||||
modelInvocations: {},
|
||||
outputs: {},
|
||||
gateway: {},
|
||||
successfulCompletion: {},
|
||||
finalizations: {},
|
||||
};
|
||||
const terminalizations = {
|
||||
repository: {
|
||||
async findByRequestId() {
|
||||
return null;
|
||||
},
|
||||
async readAuthority() {
|
||||
return {};
|
||||
},
|
||||
async commit() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
};
|
||||
const service = new CopilotFailureDiagnosisApplicationService({
|
||||
admissions,
|
||||
@@ -183,13 +238,15 @@ function fixture(options = {}) {
|
||||
authorizer: {
|
||||
async authorize() {
|
||||
return {
|
||||
effect: 'allow', reasons: ['role_grant'],
|
||||
effect: 'allow',
|
||||
reasons: ['role_grant'],
|
||||
fence: { projectVersion: 1, bindingVersion: 1 },
|
||||
};
|
||||
},
|
||||
},
|
||||
tool,
|
||||
model,
|
||||
terminalizations,
|
||||
modelIntent: MODEL,
|
||||
executionTimeoutMs: 60_000,
|
||||
now: () => NOW,
|
||||
@@ -197,10 +254,24 @@ function fixture(options = {}) {
|
||||
toolCalls += 1;
|
||||
await toolGate;
|
||||
return options.toolFailure
|
||||
? { outcome: 'failed', completionStatus: 'created', unlockStatus: null }
|
||||
? {
|
||||
outcome: 'failed',
|
||||
completionStatus: 'created',
|
||||
unlockStatus: null,
|
||||
completion: { completionDigest: 'f'.repeat(64) },
|
||||
}
|
||||
: {
|
||||
outcome: 'succeeded', completionStatus: 'created',
|
||||
unlockStatus: 'created', completion: {}, unlock: {},
|
||||
outcome: 'succeeded',
|
||||
completionStatus: 'created',
|
||||
unlockStatus: 'created',
|
||||
completion: { completionDigest: 'e'.repeat(64) },
|
||||
output: {
|
||||
status: 'available',
|
||||
runId: 'source-run-1',
|
||||
attemptId: 'source-attempt-1',
|
||||
profile: 'cluster-control',
|
||||
},
|
||||
unlock: {},
|
||||
};
|
||||
},
|
||||
async executeModel() {
|
||||
@@ -211,13 +282,36 @@ function fixture(options = {}) {
|
||||
finalization: { requestId: 'diagnosis-request-1' },
|
||||
};
|
||||
},
|
||||
async terminalizeBeforeModel(_requestId, trigger) {
|
||||
terminalizationCalls += 1;
|
||||
if (
|
||||
trigger.kind === 'boundary' ||
|
||||
(trigger.kind === 'tool_projection' &&
|
||||
trigger.output.status === 'available')
|
||||
) {
|
||||
throw new CopilotFailureDiagnosisPreModelTerminalizationNotReadyError();
|
||||
}
|
||||
return {
|
||||
status: 'created',
|
||||
receipt: {
|
||||
requestId: 'diagnosis-request-1',
|
||||
reason: 'tool_failed',
|
||||
outcome: 'failed',
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
return {
|
||||
service,
|
||||
releaseTool: () => releaseTool?.(),
|
||||
state: () => ({
|
||||
plan, artifacts, toolCalls, modelCalls,
|
||||
activeKeyCopies, resolvedKeyCopies,
|
||||
plan,
|
||||
artifacts,
|
||||
toolCalls,
|
||||
modelCalls,
|
||||
terminalizationCalls,
|
||||
activeKeyCopies,
|
||||
resolvedKeyCopies,
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -228,6 +322,7 @@ test('application derives, admits and executes one server-owned diagnosis with e
|
||||
assert.equal(first.admissionStatus, 'created');
|
||||
assert.equal(first.tool.outcome, 'succeeded');
|
||||
assert.equal(first.model.outcome, 'succeeded');
|
||||
assert.equal(first.terminalization, null);
|
||||
assert.equal(first.terminalizationRequired, false);
|
||||
const second = await testFixture.service.execute(command());
|
||||
assert.equal(second.admissionStatus, 'existing');
|
||||
@@ -235,9 +330,18 @@ test('application derives, admits and executes one server-owned diagnosis with e
|
||||
assert.equal(state.toolCalls, 2);
|
||||
assert.equal(state.modelCalls, 2);
|
||||
assert.equal(state.plan.source.attemptId, 'source-attempt-1');
|
||||
assert.equal(state.plan.tool.invocationArtifact.artifactId.startsWith('cdia:'), true);
|
||||
assert.equal(state.activeKeyCopies[0].every((value) => value === 0), true);
|
||||
assert.equal(state.resolvedKeyCopies[0].every((value) => value === 0), true);
|
||||
assert.equal(
|
||||
state.plan.tool.invocationArtifact.artifactId.startsWith('cdia:'),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
state.activeKeyCopies[0].every((value) => value === 0),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
state.resolvedKeyCopies[0].every((value) => value === 0),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('application repairs the durable admission-to-Artifact crash window', async () => {
|
||||
@@ -253,7 +357,7 @@ test('application repairs the durable admission-to-Artifact crash window', async
|
||||
assert.ok(testFixture.state().artifacts);
|
||||
});
|
||||
|
||||
test('application coalesces exact callers and exposes Tool terminalization debt', async () => {
|
||||
test('application coalesces exact callers and terminalizes Tool failure', async () => {
|
||||
const concurrent = fixture({ blockTool: true });
|
||||
const first = concurrent.service.execute(command());
|
||||
const second = concurrent.service.execute(command());
|
||||
@@ -266,6 +370,7 @@ test('application coalesces exact callers and exposes Tool terminalization debt'
|
||||
const result = await failed.service.execute(command());
|
||||
assert.equal(result.tool.outcome, 'failed');
|
||||
assert.equal(result.model, null);
|
||||
assert.equal(result.terminalizationRequired, true);
|
||||
assert.equal(result.terminalization.reason, 'tool_failed');
|
||||
assert.equal(result.terminalizationRequired, false);
|
||||
assert.equal(failed.state().modelCalls, 0);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
BUILTIN_RUN_LOG_EXCERPT_TOOL,
|
||||
BUILTIN_RUN_LOG_EXCERPT_TOOL_DEFINITION,
|
||||
createBuiltInRunLogExcerptToolHandlerBinding,
|
||||
} = require('@qinglong/runtime-core/builtin-run-log-excerpt-tool');
|
||||
const {
|
||||
createPluginPackageResourceGenerationFromReferences,
|
||||
} = require('@qinglong/runtime-core/plugin-package-resource-generation');
|
||||
const {
|
||||
createProjectToolDefinitionSnapshot,
|
||||
projectToolDefinitionRegistry,
|
||||
} = require('@qinglong/runtime-core/project-tool-definition-snapshot');
|
||||
const {
|
||||
prepareToolInvocation,
|
||||
} = require('@qinglong/runtime-core/tool-registry');
|
||||
const {
|
||||
TrustedToolHandlerBindingRegistry,
|
||||
createTrustedToolInvocationPlan,
|
||||
} = require('@qinglong/runtime-core/trusted-tool-invocation');
|
||||
const {
|
||||
transitionStepRunMutation,
|
||||
} = require('@qinglong/runtime-core/step-run');
|
||||
const {
|
||||
createCopilotFailureDiagnosisAdmissionBundle,
|
||||
prepareCopilotFailureDiagnosisExecution,
|
||||
} = require('@qinglong/ai/failure-diagnosis-execution-admission');
|
||||
const {
|
||||
CopilotFailureDiagnosisPreModelTerminalizationNotReadyError,
|
||||
normalizeCopilotFailureDiagnosisPreModelTerminalizationCommand,
|
||||
terminalizeCopilotFailureDiagnosisBeforeModel,
|
||||
} = require('@qinglong/ai/failure-diagnosis-pre-model-terminalization');
|
||||
|
||||
const NOW = 4_000;
|
||||
|
||||
async function plan() {
|
||||
const generation = createPluginPackageResourceGenerationFromReferences({
|
||||
installationId: 'install-terminalization-test',
|
||||
projectId: 'project-terminalization',
|
||||
packageName: 'qinglong',
|
||||
lockDigest: 'a'.repeat(64),
|
||||
generation: 1,
|
||||
previousActiveLockDigest: null,
|
||||
contentDigest: 'b'.repeat(64),
|
||||
resources: [],
|
||||
});
|
||||
const snapshot = createProjectToolDefinitionSnapshot({
|
||||
projectId: 'project-terminalization',
|
||||
contributions: [
|
||||
{
|
||||
generation,
|
||||
revisionDigest: 'c'.repeat(64),
|
||||
definitions: [BUILTIN_RUN_LOG_EXCERPT_TOOL_DEFINITION],
|
||||
},
|
||||
],
|
||||
});
|
||||
const binding = createBuiltInRunLogExcerptToolHandlerBinding(snapshot, [
|
||||
'cluster-control',
|
||||
]);
|
||||
const bindings = new TrustedToolHandlerBindingRegistry(snapshot, [binding]);
|
||||
const principal = {
|
||||
subject: { type: 'user', id: 'owner-terminalization' },
|
||||
authenticationId: 'auth-terminalization',
|
||||
authenticatedAtMs: 1_000,
|
||||
expiresAtMs: 10_000,
|
||||
assurance: 'multi_factor',
|
||||
};
|
||||
const invocation = await prepareToolInvocation(
|
||||
projectToolDefinitionRegistry(snapshot),
|
||||
{
|
||||
projectId: 'project-terminalization',
|
||||
principal,
|
||||
nowMs: 1_200,
|
||||
tool: BUILTIN_RUN_LOG_EXCERPT_TOOL,
|
||||
input: { runId: 'source-run-failed', attemptId: 'source-attempt-failed' },
|
||||
},
|
||||
{
|
||||
async authorize() {
|
||||
return {
|
||||
effect: 'allow',
|
||||
reasons: ['role_grant'],
|
||||
fence: { projectVersion: 1, bindingVersion: 1 },
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
const tool = createTrustedToolInvocationPlan(bindings, invocation, {
|
||||
actionRef: 'terminalization-log-tool',
|
||||
inputArtifactId: 'terminalization-input',
|
||||
previewArtifactId: 'terminalization-preview',
|
||||
artifactKeyId: 'terminalization-key',
|
||||
artifactKey: Buffer.alloc(32, 0x11),
|
||||
artifactNonce: Buffer.alloc(12, 0x22),
|
||||
profile: 'cluster-control',
|
||||
preview: {
|
||||
title: 'Read failed Run log',
|
||||
summary: 'Read bounded evidence',
|
||||
fields: [
|
||||
{ kind: 'identifier', label: 'Run', value: 'source-run-failed' },
|
||||
{
|
||||
kind: 'identifier',
|
||||
label: 'Attempt',
|
||||
value: 'source-attempt-failed',
|
||||
},
|
||||
],
|
||||
warnings: ['potentially_sensitive_output'],
|
||||
},
|
||||
sealedAtMs: 2_000,
|
||||
});
|
||||
return prepareCopilotFailureDiagnosisExecution({
|
||||
requestId: 'terminalization-request-1',
|
||||
traceId: 'terminalization-trace-1',
|
||||
source: {
|
||||
runId: 'source-run-failed',
|
||||
runVersion: 4,
|
||||
runStatus: 'failed',
|
||||
attemptId: 'source-attempt-failed',
|
||||
attemptStatus: 'failed',
|
||||
attemptFinishedAtMs: 1_900,
|
||||
logArtifactId: 'source-log-artifact',
|
||||
},
|
||||
toolPlan: tool.plan,
|
||||
bindings,
|
||||
model: {
|
||||
provider: 'provider-primary',
|
||||
model: 'model-diagnosis',
|
||||
modelBoundary: 'external',
|
||||
responseLanguage: 'zh-CN',
|
||||
maxOutputTokens: 256,
|
||||
egressPolicy: {
|
||||
schema: 'qinglong/copilot-model-egress-policy@v1',
|
||||
revision: 'terminalization-policy-v1',
|
||||
potentiallySensitiveDataBoundaries: ['external'],
|
||||
maxInputBytes: 64 * 1024,
|
||||
maxOutputTokens: 512,
|
||||
},
|
||||
},
|
||||
plannedAtMs: 2_100,
|
||||
deadlineAtMs: 8_000,
|
||||
});
|
||||
}
|
||||
|
||||
function transition(step, runVersion, to, atMs, suffix, outputRef) {
|
||||
return transitionStepRunMutation(
|
||||
step,
|
||||
{
|
||||
expectedVersion: step.version,
|
||||
expectedDigest: step.stepRunDigest,
|
||||
mutationId: `fixture-mutation-${suffix}`,
|
||||
to,
|
||||
atMs,
|
||||
...(outputRef ? { outputRef } : {}),
|
||||
...(['failed', 'timed_out', 'cancelled'].includes(to)
|
||||
? { resultCode: `fixture_${to}` }
|
||||
: {}),
|
||||
...(['failed', 'timed_out'].includes(to)
|
||||
? { errorSummary: `Fixture ${to}` }
|
||||
: {}),
|
||||
},
|
||||
{
|
||||
expectedRunVersion: runVersion,
|
||||
expectedRunEventSequence: runVersion,
|
||||
eventId: `fixture-event-${suffix}`,
|
||||
dedupeKey: `fixture-event-${suffix}`,
|
||||
actor: { type: 'system' },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function repository(authority) {
|
||||
let command = null;
|
||||
return {
|
||||
value: {
|
||||
async findByRequestId() {
|
||||
return null;
|
||||
},
|
||||
async readAuthority() {
|
||||
return authority;
|
||||
},
|
||||
async commit(value) {
|
||||
command =
|
||||
normalizeCopilotFailureDiagnosisPreModelTerminalizationCommand(value);
|
||||
return { status: 'created', receipt: command.receipt };
|
||||
},
|
||||
},
|
||||
command: () => command,
|
||||
};
|
||||
}
|
||||
|
||||
test('terminalizes a durable Tool failure and cancels the pending Model Step', async () => {
|
||||
const current = await plan();
|
||||
const admitted = createCopilotFailureDiagnosisAdmissionBundle(current);
|
||||
const started = transition(
|
||||
admitted.toolStepMutation.stepRun,
|
||||
3,
|
||||
'running',
|
||||
2_500,
|
||||
'tool-start',
|
||||
);
|
||||
const failed = transition(started.stepRun, 4, 'failed', 3_000, 'tool-failed');
|
||||
const storage = repository({
|
||||
plan: current,
|
||||
run: {
|
||||
id: current.runId,
|
||||
projectId: current.projectId,
|
||||
status: 'running',
|
||||
version: 5,
|
||||
eventSequence: 5,
|
||||
},
|
||||
toolStep: failed.stepRun,
|
||||
modelStep: admitted.modelStepMutation.stepRun,
|
||||
modelStartExists: false,
|
||||
observedAtMs: NOW,
|
||||
});
|
||||
const result = await terminalizeCopilotFailureDiagnosisBeforeModel(
|
||||
current.requestId,
|
||||
{
|
||||
kind: 'tool_failure',
|
||||
completion: {
|
||||
startId: 'tool-start-1',
|
||||
runId: current.runId,
|
||||
stepRunId: current.toolStepRunId,
|
||||
outcome: 'failed',
|
||||
completedStepRunDigest: failed.stepRun.stepRunDigest,
|
||||
completionDigest: 'd'.repeat(64),
|
||||
},
|
||||
},
|
||||
{ repository: storage.value },
|
||||
);
|
||||
assert.equal(result.receipt.reason, 'tool_failed');
|
||||
assert.equal(result.receipt.outcome, 'failed');
|
||||
assert.equal(result.receipt.terminalSteps.length, 1);
|
||||
assert.equal(
|
||||
result.receipt.terminalSteps[0].stepRunId,
|
||||
current.modelStepRunId,
|
||||
);
|
||||
assert.equal(result.receipt.terminalSteps[0].status, 'cancelled');
|
||||
assert.equal(storage.command().expectedRunVersion, 5);
|
||||
assert.equal(result.receipt.finalRunVersion, 7);
|
||||
});
|
||||
|
||||
test('terminalizes every non-available log status without exposing content', async () => {
|
||||
for (const status of ['not_found', 'pending', 'missing', 'retired']) {
|
||||
const current = await plan();
|
||||
const admitted = createCopilotFailureDiagnosisAdmissionBundle(current);
|
||||
const toolStarted = transition(
|
||||
admitted.toolStepMutation.stepRun,
|
||||
3,
|
||||
'running',
|
||||
2_500,
|
||||
`start-${status}`,
|
||||
);
|
||||
const toolSucceeded = transition(
|
||||
toolStarted.stepRun,
|
||||
4,
|
||||
'succeeded',
|
||||
3_000,
|
||||
`success-${status}`,
|
||||
'tool-result-artifact',
|
||||
);
|
||||
const modelReady = transition(
|
||||
admitted.modelStepMutation.stepRun,
|
||||
5,
|
||||
'ready',
|
||||
3_100,
|
||||
`ready-${status}`,
|
||||
);
|
||||
const storage = repository({
|
||||
plan: current,
|
||||
run: {
|
||||
id: current.runId,
|
||||
projectId: current.projectId,
|
||||
status: 'running',
|
||||
version: 6,
|
||||
eventSequence: 6,
|
||||
},
|
||||
toolStep: toolSucceeded.stepRun,
|
||||
modelStep: modelReady.stepRun,
|
||||
modelStartExists: false,
|
||||
observedAtMs: NOW,
|
||||
});
|
||||
const result = await terminalizeCopilotFailureDiagnosisBeforeModel(
|
||||
current.requestId,
|
||||
{
|
||||
kind: 'tool_projection',
|
||||
completion: {
|
||||
startId: 'tool-start-1',
|
||||
runId: current.runId,
|
||||
stepRunId: current.toolStepRunId,
|
||||
completedStepRunDigest: toolSucceeded.stepRun.stepRunDigest,
|
||||
completionDigest: 'e'.repeat(64),
|
||||
},
|
||||
output: {
|
||||
status,
|
||||
runId: current.source.runId,
|
||||
attemptId: current.source.attemptId,
|
||||
profile: 'cluster-control',
|
||||
},
|
||||
},
|
||||
{ repository: storage.value },
|
||||
);
|
||||
assert.equal(result.receipt.reason, `log_${status}`);
|
||||
assert.equal(result.receipt.outcome, 'failed');
|
||||
assert.equal(result.receipt.terminalSteps[0].status, 'failed');
|
||||
assert.equal(JSON.stringify(result.receipt).includes('log content'), false);
|
||||
}
|
||||
});
|
||||
|
||||
test('database-observed deadline terminalizes both unstarted Steps', async () => {
|
||||
const current = await plan();
|
||||
const admitted = createCopilotFailureDiagnosisAdmissionBundle(current);
|
||||
const storage = repository({
|
||||
plan: current,
|
||||
run: {
|
||||
id: current.runId,
|
||||
projectId: current.projectId,
|
||||
status: 'running',
|
||||
version: 3,
|
||||
eventSequence: 3,
|
||||
},
|
||||
toolStep: admitted.toolStepMutation.stepRun,
|
||||
modelStep: admitted.modelStepMutation.stepRun,
|
||||
modelStartExists: false,
|
||||
observedAtMs: current.deadlineAtMs,
|
||||
});
|
||||
const result = await terminalizeCopilotFailureDiagnosisBeforeModel(
|
||||
current.requestId,
|
||||
{ kind: 'boundary' },
|
||||
{ repository: storage.value },
|
||||
);
|
||||
assert.equal(result.receipt.reason, 'deadline_exceeded');
|
||||
assert.equal(result.receipt.outcome, 'timed_out');
|
||||
assert.deepEqual(
|
||||
result.receipt.terminalSteps.map(({ status }) => status),
|
||||
['timed_out', 'cancelled'],
|
||||
);
|
||||
assert.equal(result.receipt.finalRunVersion, 6);
|
||||
});
|
||||
|
||||
test('insufficient durable Tool budget terminalizes before starting the Tool', async () => {
|
||||
const current = await plan();
|
||||
const admitted = createCopilotFailureDiagnosisAdmissionBundle(current);
|
||||
const storage = repository({
|
||||
plan: current,
|
||||
run: {
|
||||
id: current.runId,
|
||||
projectId: current.projectId,
|
||||
status: 'running',
|
||||
version: 3,
|
||||
eventSequence: 3,
|
||||
},
|
||||
toolStep: admitted.toolStepMutation.stepRun,
|
||||
modelStep: admitted.modelStepMutation.stepRun,
|
||||
modelStartExists: false,
|
||||
observedAtMs: current.deadlineAtMs - 4_999,
|
||||
});
|
||||
const result = await terminalizeCopilotFailureDiagnosisBeforeModel(
|
||||
current.requestId,
|
||||
{ kind: 'boundary' },
|
||||
{ repository: storage.value },
|
||||
);
|
||||
assert.equal(result.receipt.reason, 'tool_budget_exhausted');
|
||||
assert.equal(result.receipt.outcome, 'timed_out');
|
||||
assert.deepEqual(
|
||||
result.receipt.terminalSteps.map(({ status }) => status),
|
||||
['timed_out', 'cancelled'],
|
||||
);
|
||||
});
|
||||
|
||||
test('durable cancellation terminalizes both unstarted Steps', async () => {
|
||||
const current = await plan();
|
||||
const admitted = createCopilotFailureDiagnosisAdmissionBundle(current);
|
||||
const storage = repository({
|
||||
plan: current,
|
||||
run: {
|
||||
id: current.runId,
|
||||
projectId: current.projectId,
|
||||
status: 'running',
|
||||
version: 3,
|
||||
eventSequence: 3,
|
||||
cancelRequestedAtMs: NOW - 1,
|
||||
cancelReason: 'user',
|
||||
},
|
||||
toolStep: admitted.toolStepMutation.stepRun,
|
||||
modelStep: admitted.modelStepMutation.stepRun,
|
||||
modelStartExists: false,
|
||||
observedAtMs: NOW,
|
||||
});
|
||||
const result = await terminalizeCopilotFailureDiagnosisBeforeModel(
|
||||
current.requestId,
|
||||
{ kind: 'boundary' },
|
||||
{ repository: storage.value },
|
||||
);
|
||||
assert.equal(result.receipt.reason, 'cancellation_requested');
|
||||
assert.equal(result.receipt.outcome, 'cancelled');
|
||||
assert.deepEqual(
|
||||
result.receipt.terminalSteps.map(({ status }) => status),
|
||||
['cancelled', 'cancelled'],
|
||||
);
|
||||
});
|
||||
|
||||
test('refuses early boundary settlement and any pre-Model lie after Model start', async () => {
|
||||
const current = await plan();
|
||||
const admitted = createCopilotFailureDiagnosisAdmissionBundle(current);
|
||||
const base = {
|
||||
plan: current,
|
||||
run: {
|
||||
id: current.runId,
|
||||
projectId: current.projectId,
|
||||
status: 'running',
|
||||
version: 3,
|
||||
eventSequence: 3,
|
||||
},
|
||||
toolStep: admitted.toolStepMutation.stepRun,
|
||||
modelStep: admitted.modelStepMutation.stepRun,
|
||||
observedAtMs: current.deadlineAtMs - 5_001,
|
||||
};
|
||||
await assert.rejects(
|
||||
terminalizeCopilotFailureDiagnosisBeforeModel(
|
||||
current.requestId,
|
||||
{ kind: 'boundary' },
|
||||
{ repository: repository({ ...base, modelStartExists: false }).value },
|
||||
),
|
||||
CopilotFailureDiagnosisPreModelTerminalizationNotReadyError,
|
||||
);
|
||||
await assert.rejects(
|
||||
terminalizeCopilotFailureDiagnosisBeforeModel(
|
||||
current.requestId,
|
||||
{ kind: 'boundary' },
|
||||
{
|
||||
repository: repository({
|
||||
...base,
|
||||
modelStartExists: true,
|
||||
observedAtMs: current.deadlineAtMs,
|
||||
}).value,
|
||||
},
|
||||
),
|
||||
/pre-Model authority is invalid/,
|
||||
);
|
||||
});
|
||||
@@ -22,6 +22,7 @@ const {
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_ADMISSION_MIGRATION_ID,
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_MIGRATION_ID,
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_MODEL_EXECUTION_MIGRATION_ID,
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_MIGRATION_ID,
|
||||
POSTGRES_MODEL_INVOCATION_MIGRATION_ID,
|
||||
POSTGRES_MODEL_INVOCATION_MIGRATION_HISTORY_TABLE,
|
||||
POSTGRES_MODEL_INVOCATION_MIGRATION_STREAM_ID,
|
||||
@@ -436,6 +437,10 @@ test('PostgreSQL AI schema is an independent reviewed feature stream', async ()
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_MODEL_EXECUTION_MIGRATION_ID,
|
||||
'pg-9020-ai-copilot-failure-diagnosis-model-executions',
|
||||
);
|
||||
assert.equal(
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_MIGRATION_ID,
|
||||
'pg-9021-ai-copilot-failure-diagnosis-pre-model-terminalizations',
|
||||
);
|
||||
assert.equal(
|
||||
POSTGRES_MODEL_INVOCATION_MIGRATION_HISTORY_TABLE,
|
||||
'ai_schema_migrations',
|
||||
@@ -523,7 +528,7 @@ test('PostgreSQL AI schema is an independent reviewed feature stream', async ()
|
||||
);
|
||||
assert.equal(
|
||||
postgresModelInvocationMigrationDefinition.migrations.length,
|
||||
20,
|
||||
21,
|
||||
);
|
||||
|
||||
const diagnosisAdmissionStatements = [];
|
||||
@@ -597,6 +602,29 @@ test('PostgreSQL AI schema is an independent reviewed feature stream', async ()
|
||||
/GRANT[^;]*(?:UPDATE|DELETE)/,
|
||||
);
|
||||
|
||||
const diagnosisPreModelTerminalizationStatements = [];
|
||||
await postgresModelInvocationMigrationDefinition.migrations[20].up({
|
||||
async query(statement) {
|
||||
diagnosisPreModelTerminalizationStatements.push(statement);
|
||||
return { rows: [] };
|
||||
},
|
||||
});
|
||||
const diagnosisPreModelTerminalizationSql =
|
||||
diagnosisPreModelTerminalizationStatements.join('\n');
|
||||
assert.match(
|
||||
diagnosisPreModelTerminalizationSql,
|
||||
/CREATE TABLE "ql3_ai"\."copilot_failure_diagnosis_pre_model_terminalizations"/,
|
||||
);
|
||||
assert.match(
|
||||
diagnosisPreModelTerminalizationSql,
|
||||
/jsonb_array_length\(terminal_steps_json\) BETWEEN 1 AND 2/,
|
||||
);
|
||||
assert.match(diagnosisPreModelTerminalizationSql, /TO ql3_runtime/);
|
||||
assert.doesNotMatch(
|
||||
diagnosisPreModelTerminalizationSql,
|
||||
/GRANT[^;]*(?:UPDATE|DELETE)/,
|
||||
);
|
||||
|
||||
const retirementStatements = [];
|
||||
await postgresModelInvocationMigrationDefinition.migrations[10].up({
|
||||
async query(statement) {
|
||||
|
||||
+1
-1
@@ -175,7 +175,7 @@ test('readiness binds exact migration history and least-privilege primary author
|
||||
assert.equal(report.ready, true);
|
||||
assert.equal(
|
||||
report.migrationIds.at(-1),
|
||||
'pg-9020-ai-copilot-failure-diagnosis-model-executions',
|
||||
'pg-9021-ai-copilot-failure-diagnosis-pre-model-terminalizations',
|
||||
);
|
||||
assert.match(
|
||||
queries[1],
|
||||
|
||||
@@ -277,7 +277,7 @@ test('tester readiness freezes migration history and least privilege', async ()
|
||||
assert.equal(ready.ready, true);
|
||||
assert.equal(
|
||||
ready.migrationIds.at(-1),
|
||||
'pg-9020-ai-copilot-failure-diagnosis-model-executions',
|
||||
'pg-9021-ai-copilot-failure-diagnosis-pre-model-terminalizations',
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
|
||||
+36
-10
@@ -18,6 +18,10 @@ import {
|
||||
import { PostgresCopilotFailureDiagnosisAdmissionRepository } from '@qinglong/ai/postgres-failure-diagnosis-admission-storage';
|
||||
import { PostgresCopilotFailureDiagnosisModelRepository } from '@qinglong/ai/postgres-failure-diagnosis-model-execution-storage';
|
||||
import { PostgresCopilotFailureDiagnosisToolUnlockRepository } from '@qinglong/ai/postgres-failure-diagnosis-tool-execution-storage';
|
||||
import {
|
||||
PostgresCopilotFailureDiagnosisPreModelTerminalizationRepository,
|
||||
terminalizeCopilotFailureDiagnosisBeforeModel,
|
||||
} from '@qinglong/ai/failure-diagnosis-pre-model-terminalization';
|
||||
import type { ActiveModelGatewayCapability } from '@qinglong/ai/profile';
|
||||
import {
|
||||
PostgresProjectPolicyRepository,
|
||||
@@ -33,9 +37,7 @@ import {
|
||||
PostgresToolResultRekeyReader,
|
||||
type QingLongPostgresPool,
|
||||
} from '@qinglong/cluster-postgres/runtime';
|
||||
import {
|
||||
BuiltInRunLogExcerptToolAdapter,
|
||||
} from '@qinglong/runtime-core/builtin-run-log-excerpt-tool';
|
||||
import { BuiltInRunLogExcerptToolAdapter } from '@qinglong/runtime-core/builtin-run-log-excerpt-tool';
|
||||
import type { RunAttemptLogReadPort } from '@qinglong/runtime-core/builtin-run-log-excerpt-projection';
|
||||
import { ProjectPolicyEngine } from '@qinglong/runtime-core/project-policy';
|
||||
import { RunAttemptLogReadService } from '@qinglong/runtime-core/run-attempt-log-read';
|
||||
@@ -92,7 +94,9 @@ export interface PreparedClusterCopilotFailureDiagnosisProjection {
|
||||
ReturnType<typeof createClusterToolResultProjectedKeyring>
|
||||
>;
|
||||
readonly outputKeys: Awaited<
|
||||
ReturnType<typeof createClusterCopilotFailureDiagnosisOutputProjectedKeyring>
|
||||
ReturnType<
|
||||
typeof createClusterCopilotFailureDiagnosisOutputProjectedKeyring
|
||||
>
|
||||
>;
|
||||
}
|
||||
|
||||
@@ -100,7 +104,10 @@ export class ClusterCopilotFailureDiagnosisCompositionError extends Error {
|
||||
readonly code = 'QL3_CLUSTER_COPILOT_FAILURE_DIAGNOSIS_COMPOSITION_INVALID';
|
||||
|
||||
constructor(message: string, options?: ErrorOptions) {
|
||||
super(`Cluster Copilot failure diagnosis composition is invalid: ${message}`, options);
|
||||
super(
|
||||
`Cluster Copilot failure diagnosis composition is invalid: ${message}`,
|
||||
options,
|
||||
);
|
||||
this.name = 'ClusterCopilotFailureDiagnosisCompositionError';
|
||||
}
|
||||
}
|
||||
@@ -196,7 +203,8 @@ export function normalizeClusterCopilotFailureDiagnosisConfig(
|
||||
],
|
||||
'egress policy',
|
||||
);
|
||||
if (egress.schema !== EGRESS_SCHEMA) return invalid('egress schema is invalid');
|
||||
if (egress.schema !== EGRESS_SCHEMA)
|
||||
return invalid('egress schema is invalid');
|
||||
const selected = egress.potentiallySensitiveDataBoundaries;
|
||||
if (
|
||||
!Array.isArray(selected) ||
|
||||
@@ -307,7 +315,11 @@ function modelIntent(
|
||||
export async function prepareProductionClusterCopilotFailureDiagnosisProjection(
|
||||
projection: ClusterCopilotFailureDiagnosisProjection,
|
||||
): Promise<Readonly<PreparedClusterCopilotFailureDiagnosisProjection>> {
|
||||
if (!projection || typeof projection !== 'object' || Array.isArray(projection)) {
|
||||
if (
|
||||
!projection ||
|
||||
typeof projection !== 'object' ||
|
||||
Array.isArray(projection)
|
||||
) {
|
||||
return invalid('projection is invalid');
|
||||
}
|
||||
const [config, invocationKeys, resultKeys, outputKeys] = await Promise.all([
|
||||
@@ -360,8 +372,12 @@ export async function createProductionClusterCopilotFailureDiagnosis(
|
||||
const runs = new PostgresRunRepository(options.pool);
|
||||
const artifacts = new PostgresToolInvocationArtifactRepository(options.pool);
|
||||
const stepRuns = new PostgresStepRunRepository(options.pool);
|
||||
const barriers = new PostgresToolExecutionStartBarrierRepository(options.pool);
|
||||
const completions = new PostgresToolExecutionCompletionRepository(options.pool);
|
||||
const barriers = new PostgresToolExecutionStartBarrierRepository(
|
||||
options.pool,
|
||||
);
|
||||
const completions = new PostgresToolExecutionCompletionRepository(
|
||||
options.pool,
|
||||
);
|
||||
const failureCompletions =
|
||||
new PostgresToolExecutionFailureCompletionRepository(options.pool);
|
||||
const resultKeyCatalog = new PostgresToolResultKeyCatalogReader(options.pool);
|
||||
@@ -369,7 +385,13 @@ export async function createProductionClusterCopilotFailureDiagnosis(
|
||||
const unlocks = new PostgresCopilotFailureDiagnosisToolUnlockRepository(
|
||||
options.pool,
|
||||
);
|
||||
const models = new PostgresCopilotFailureDiagnosisModelRepository(options.pool);
|
||||
const models = new PostgresCopilotFailureDiagnosisModelRepository(
|
||||
options.pool,
|
||||
);
|
||||
const preModelTerminalizations =
|
||||
new PostgresCopilotFailureDiagnosisPreModelTerminalizationRepository(
|
||||
options.pool,
|
||||
);
|
||||
const logReader = new RunAttemptLogReadService(
|
||||
runs,
|
||||
Object.freeze({
|
||||
@@ -457,8 +479,12 @@ export async function createProductionClusterCopilotFailureDiagnosis(
|
||||
authorizer: policy,
|
||||
tool,
|
||||
model,
|
||||
terminalizations: Object.freeze({
|
||||
repository: preModelTerminalizations,
|
||||
}),
|
||||
executeTool: executeCopilotFailureDiagnosisTool,
|
||||
executeModel: executeCopilotFailureDiagnosisModel,
|
||||
terminalizeBeforeModel: terminalizeCopilotFailureDiagnosisBeforeModel,
|
||||
modelIntent: modelIntent(config),
|
||||
executionTimeoutMs: config.executionTimeoutMs,
|
||||
};
|
||||
|
||||
@@ -1179,6 +1179,7 @@ function auditSourceImports(root, packagePath, findings) {
|
||||
'@qinglong/ai/failure-diagnosis-application',
|
||||
'@qinglong/ai/failure-diagnosis-execution-admission',
|
||||
'@qinglong/ai/failure-diagnosis-model-execution',
|
||||
'@qinglong/ai/failure-diagnosis-pre-model-terminalization',
|
||||
'@qinglong/ai/failure-diagnosis-tool-execution',
|
||||
'@qinglong/ai/postgres-failure-diagnosis-admission-storage',
|
||||
'@qinglong/ai/postgres-failure-diagnosis-model-execution-storage',
|
||||
|
||||
@@ -40,6 +40,7 @@ const {
|
||||
} = require('../packages/ql3-cluster-postgres/dist/migration/migration.js');
|
||||
const {
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_ADMISSION_MIGRATION_ID,
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_MIGRATION_ID,
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_MIGRATION_ID,
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_MODEL_EXECUTION_MIGRATION_ID,
|
||||
POSTGRES_MODEL_INVOCATION_MIGRATION_ID,
|
||||
@@ -73,6 +74,13 @@ const {
|
||||
const {
|
||||
PostgresCopilotFailureDiagnosisToolUnlockRepository,
|
||||
} = require('../packages/ql3-ai/dist/copilot/failure-diagnosis/postgresToolExecutionRepository.js');
|
||||
const {
|
||||
PostgresCopilotFailureDiagnosisPreModelTerminalizationRepository,
|
||||
terminalizeCopilotFailureDiagnosisBeforeModel,
|
||||
} = require('../packages/ql3-ai/dist/copilot/failure-diagnosis/preModelTerminalization.js');
|
||||
const {
|
||||
PostgresCopilotFailureDiagnosisModelRepository,
|
||||
} = require('../packages/ql3-ai/dist/copilot/failure-diagnosis/postgresModelExecutionRepository.js');
|
||||
const {
|
||||
BUILTIN_RUN_LOG_EXCERPT_TOOL,
|
||||
BUILTIN_RUN_LOG_EXCERPT_TOOL_DEFINITION,
|
||||
@@ -137,6 +145,9 @@ const {
|
||||
const {
|
||||
DurableModelInvocationCoordinator,
|
||||
} = require('../packages/ql3-ai/dist/model-invocation/durableModelInvocationCoordinator.js');
|
||||
const {
|
||||
DurableModelInvocationResolutionCoordinator,
|
||||
} = require('../packages/ql3-ai/dist/model-invocation/modelInvocationResolution.js');
|
||||
const {
|
||||
PostgresModelInvocationRepository,
|
||||
} = require('../packages/ql3-ai/dist/model-invocation/postgresModelInvocationRepository.js');
|
||||
@@ -1534,6 +1545,27 @@ async function modelInvocationFeatureFacts(pool) {
|
||||
'SELECT,INSERT,UPDATE,DELETE'
|
||||
)
|
||||
) AS "copilotDiagnosisModelExecutionRuntimeOnly",
|
||||
has_table_privilege(
|
||||
'ql3_runtime',
|
||||
'ql3_ai.copilot_failure_diagnosis_pre_model_terminalizations',
|
||||
'SELECT,INSERT'
|
||||
) AND NOT has_table_privilege(
|
||||
'ql3_runtime',
|
||||
'ql3_ai.copilot_failure_diagnosis_pre_model_terminalizations',
|
||||
'UPDATE,DELETE'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM unnest(ARRAY[
|
||||
'ql3_admin', 'ql3_package_manager', 'ql3_package_executor',
|
||||
'ql3_worker_ingress', 'ql3_ai_maintenance',
|
||||
'ql3_ai_credential_manager', 'ql3_ai_credential_tester'
|
||||
]::text[]) AS denied(role_name)
|
||||
WHERE has_table_privilege(
|
||||
denied.role_name,
|
||||
'ql3_ai.copilot_failure_diagnosis_pre_model_terminalizations',
|
||||
'SELECT,INSERT,UPDATE,DELETE'
|
||||
)
|
||||
) AS "copilotDiagnosisPreModelTerminalizationRuntimeOnly",
|
||||
has_function_privilege(
|
||||
'ql3_runtime',
|
||||
'ql3_ai.copilot_failure_diagnosis_admission_source_snapshot(varchar,varchar,varchar,integer,integer,varchar,varchar)',
|
||||
@@ -1714,6 +1746,108 @@ async function copilotFailureDiagnosisAdmissionFacts(pool, requestId) {
|
||||
};
|
||||
}
|
||||
|
||||
async function copilotFailureDiagnosisTerminalizationFacts(pool, requestId) {
|
||||
const result = await pool.query(
|
||||
`SELECT terminalization.request_id AS "requestId",
|
||||
terminalization.plan_digest AS "planDigest",
|
||||
terminalization.stage, terminalization.reason,
|
||||
terminalization.outcome,
|
||||
terminalization.evidence_digest AS "evidenceDigest",
|
||||
terminalization.receipt_digest AS "receiptDigest",
|
||||
terminalization.final_run_version AS "finalRunVersion",
|
||||
terminalization.final_run_event_sequence AS "finalRunEventSequence",
|
||||
run.status AS "runStatus", run.version AS "runVersion",
|
||||
run.event_sequence AS "runEventSequence",
|
||||
tool.status AS "toolStatus", model.status AS "modelStatus",
|
||||
(SELECT count(*)::integer FROM "ql3"."run_events"
|
||||
WHERE run_id = terminalization.run_id) AS "eventCount",
|
||||
(SELECT count(*)::integer FROM "ql3"."step_run_mutations"
|
||||
WHERE run_id = terminalization.run_id) AS "mutationCount",
|
||||
(SELECT count(*)::integer
|
||||
FROM "ql3_ai"."copilot_failure_diagnosis_pre_model_terminalizations"
|
||||
WHERE request_id = terminalization.request_id)
|
||||
AS "terminalizationCount",
|
||||
terminalization.receipt_json::text AS "receiptJson"
|
||||
FROM "ql3_ai"."copilot_failure_diagnosis_pre_model_terminalizations"
|
||||
AS terminalization
|
||||
JOIN "ql3"."runs" AS run ON run.id = terminalization.run_id
|
||||
JOIN "ql3"."step_runs" AS tool
|
||||
ON tool.run_id = terminalization.run_id
|
||||
AND tool.id = (
|
||||
SELECT tool_step_run_id
|
||||
FROM "ql3_ai"."copilot_failure_diagnosis_admissions"
|
||||
WHERE request_id = terminalization.request_id
|
||||
)
|
||||
JOIN "ql3"."step_runs" AS model
|
||||
ON model.run_id = terminalization.run_id
|
||||
AND model.id = (
|
||||
SELECT model_step_run_id
|
||||
FROM "ql3_ai"."copilot_failure_diagnosis_admissions"
|
||||
WHERE request_id = terminalization.request_id
|
||||
)
|
||||
WHERE terminalization.request_id = $1`,
|
||||
[requestId],
|
||||
);
|
||||
assert.equal(result.rowCount, 1);
|
||||
const { receiptJson, ...facts } = result.rows[0];
|
||||
assert.equal(typeof receiptJson, 'string');
|
||||
return {
|
||||
...facts,
|
||||
receiptJsonDigest: createHash('sha256').update(receiptJson).digest('hex'),
|
||||
receiptContainsLogContent:
|
||||
receiptJson.includes('diagnosis-sensitive-line') ||
|
||||
receiptJson.includes('must-be-redacted'),
|
||||
};
|
||||
}
|
||||
|
||||
async function copilotFailureDiagnosisModelResolutionFacts(pool, requestId) {
|
||||
const result = await pool.query(
|
||||
`SELECT admission.request_id AS "requestId",
|
||||
run.status AS "runStatus", run.version AS "runVersion",
|
||||
run.event_sequence AS "runEventSequence",
|
||||
model.status AS "modelStatus", model.version AS "modelVersion",
|
||||
completion.outcome AS "completionOutcome",
|
||||
resolution.decision AS "resolutionDecision",
|
||||
finalization.outcome AS "finalizationOutcome",
|
||||
finalization.receipt_digest AS "finalizationReceiptDigest",
|
||||
(SELECT count(*)::integer FROM "ql3"."run_events"
|
||||
WHERE run_id = admission.run_id) AS "eventCount",
|
||||
(SELECT count(*)::integer FROM "ql3"."step_run_mutations"
|
||||
WHERE run_id = admission.run_id) AS "mutationCount",
|
||||
(SELECT count(*)::integer
|
||||
FROM "ql3_ai"."model_invocation_resolutions"
|
||||
WHERE invocation_id = admission.plan_json->>'modelInvocationId')
|
||||
AS "resolutionCount",
|
||||
(SELECT count(*)::integer
|
||||
FROM "ql3_ai"."copilot_failure_diagnosis_finalizations"
|
||||
WHERE request_id = admission.request_id) AS "finalizationCount",
|
||||
finalization.receipt_json::text AS "receiptJson"
|
||||
FROM "ql3_ai"."copilot_failure_diagnosis_admissions" AS admission
|
||||
JOIN "ql3"."runs" AS run ON run.id = admission.run_id
|
||||
JOIN "ql3"."step_runs" AS model
|
||||
ON model.run_id = admission.run_id
|
||||
AND model.id = admission.model_step_run_id
|
||||
JOIN "ql3_ai"."model_invocation_completions" AS completion
|
||||
ON completion.invocation_id = admission.plan_json->>'modelInvocationId'
|
||||
JOIN "ql3_ai"."model_invocation_resolutions" AS resolution
|
||||
ON resolution.invocation_id = completion.invocation_id
|
||||
JOIN "ql3_ai"."copilot_failure_diagnosis_finalizations" AS finalization
|
||||
ON finalization.request_id = admission.request_id
|
||||
WHERE admission.request_id = $1`,
|
||||
[requestId],
|
||||
);
|
||||
assert.equal(result.rowCount, 1);
|
||||
const { receiptJson, ...facts } = result.rows[0];
|
||||
assert.equal(typeof receiptJson, 'string');
|
||||
return {
|
||||
...facts,
|
||||
receiptJsonDigest: createHash('sha256').update(receiptJson).digest('hex'),
|
||||
receiptContainsModelContent:
|
||||
receiptJson.includes('diagnosis-sensitive-line') ||
|
||||
receiptJson.includes('must-be-redacted'),
|
||||
};
|
||||
}
|
||||
|
||||
async function runCopilotFailureDiagnosisAdmissionHaEvidence(options) {
|
||||
const { primaryPort, migrationPool } = options;
|
||||
const baseTimeMs = Date.now();
|
||||
@@ -1965,6 +2099,8 @@ async function executeCopilotFailureDiagnosisToolHaEvidence(options) {
|
||||
fixture,
|
||||
resultKeyFixture,
|
||||
expectedExisting = false,
|
||||
expectedTerminalized = false,
|
||||
logStatus = 'available',
|
||||
} = options;
|
||||
const runtimeDatabase = await databaseOpener(
|
||||
'runtime',
|
||||
@@ -2030,6 +2166,8 @@ async function executeCopilotFailureDiagnosisToolHaEvidence(options) {
|
||||
assert.equal(request.projectId, fixture.plan.projectId);
|
||||
assert.equal(request.runId, report.sourceRunId);
|
||||
assert.equal(request.attemptId, report.sourceAttemptId);
|
||||
if (logStatus === 'not_found') return { status: 'not_found' };
|
||||
assert.equal(logStatus, 'available');
|
||||
const totalBytes = logContent.byteLength;
|
||||
const start = Math.min(request.range.offset, totalBytes);
|
||||
const endExclusive = Math.min(start + request.range.length, totalBytes);
|
||||
@@ -2092,7 +2230,7 @@ async function executeCopilotFailureDiagnosisToolHaEvidence(options) {
|
||||
assert.equal(replay.unlockStatus, 'existing');
|
||||
assert.deepEqual(replay.completion, first.completion);
|
||||
assert.deepEqual(replay.unlock, first.unlock);
|
||||
assert.equal(logReads, 2);
|
||||
assert.equal(logReads, logStatus === 'not_found' ? 1 : 2);
|
||||
} else {
|
||||
assert.equal(logReads, 0);
|
||||
}
|
||||
@@ -2116,16 +2254,16 @@ async function executeCopilotFailureDiagnosisToolHaEvidence(options) {
|
||||
resultArtifactContainsPlaintext: facts.resultArtifactContainsPlaintext,
|
||||
},
|
||||
{
|
||||
status: 'running',
|
||||
version: 6,
|
||||
eventSequence: 6,
|
||||
eventCount: 6,
|
||||
mutationCount: 5,
|
||||
status: expectedTerminalized ? 'failed' : 'running',
|
||||
version: expectedTerminalized ? 8 : 6,
|
||||
eventSequence: expectedTerminalized ? 8 : 6,
|
||||
eventCount: expectedTerminalized ? 8 : 6,
|
||||
mutationCount: expectedTerminalized ? 6 : 5,
|
||||
startCount: 1,
|
||||
completionCount: 1,
|
||||
unlockCount: 1,
|
||||
toolStatus: 'succeeded',
|
||||
modelStatus: 'ready',
|
||||
modelStatus: expectedTerminalized ? 'failed' : 'ready',
|
||||
resultArtifactPresent: true,
|
||||
resultArtifactContainsPlaintext: false,
|
||||
},
|
||||
@@ -2139,7 +2277,7 @@ async function executeCopilotFailureDiagnosisToolHaEvidence(options) {
|
||||
report.promotedToolReplayReads = logReads;
|
||||
report.toolExecutionSurvivedPromotion = true;
|
||||
}
|
||||
return facts;
|
||||
return { facts, execution: first };
|
||||
} finally {
|
||||
fixture.invocationKey.fill(0);
|
||||
logContent.fill(0);
|
||||
@@ -2147,6 +2285,242 @@ async function executeCopilotFailureDiagnosisToolHaEvidence(options) {
|
||||
}
|
||||
}
|
||||
|
||||
async function terminalizeCopilotFailureDiagnosisLogUnavailableHaEvidence(
|
||||
options,
|
||||
) {
|
||||
const {
|
||||
port,
|
||||
report,
|
||||
fixture,
|
||||
resultKeyFixture,
|
||||
expectedExisting = false,
|
||||
} = options;
|
||||
const tool = await executeCopilotFailureDiagnosisToolHaEvidence({
|
||||
port,
|
||||
report,
|
||||
fixture,
|
||||
resultKeyFixture,
|
||||
expectedExisting,
|
||||
expectedTerminalized: expectedExisting,
|
||||
logStatus: 'not_found',
|
||||
});
|
||||
const runtimeDatabase = await databaseOpener(
|
||||
'runtime',
|
||||
databaseUrl(RUNTIME_USER, RUNTIME_PASSWORD, port),
|
||||
expectedExisting
|
||||
? 'ql3-ha-copilot-terminalization-promoted'
|
||||
: 'ql3-ha-copilot-terminalization-primary',
|
||||
)();
|
||||
try {
|
||||
const repository =
|
||||
new PostgresCopilotFailureDiagnosisPreModelTerminalizationRepository(
|
||||
runtimeDatabase.pool,
|
||||
);
|
||||
const trigger = {
|
||||
kind: 'tool_projection',
|
||||
completion: tool.execution.completion,
|
||||
output: tool.execution.output,
|
||||
};
|
||||
const first = await terminalizeCopilotFailureDiagnosisBeforeModel(
|
||||
report.requestId,
|
||||
trigger,
|
||||
{ repository },
|
||||
);
|
||||
const replay = await terminalizeCopilotFailureDiagnosisBeforeModel(
|
||||
report.requestId,
|
||||
trigger,
|
||||
{ repository },
|
||||
);
|
||||
assert.equal(first.status, expectedExisting ? 'existing' : 'created');
|
||||
assert.equal(replay.status, 'existing');
|
||||
assert.deepEqual(replay.receipt, first.receipt);
|
||||
assert.equal(first.receipt.reason, 'log_not_found');
|
||||
assert.equal(first.receipt.outcome, 'failed');
|
||||
assert.equal(first.receipt.terminalSteps.length, 1);
|
||||
assert.equal(
|
||||
first.receipt.terminalSteps[0].stepRunId,
|
||||
report.modelStepRunId,
|
||||
);
|
||||
assert.equal(first.receipt.terminalSteps[0].status, 'failed');
|
||||
const facts = await copilotFailureDiagnosisTerminalizationFacts(
|
||||
runtimeDatabase.pool,
|
||||
report.requestId,
|
||||
);
|
||||
assert.deepEqual(
|
||||
{
|
||||
stage: facts.stage,
|
||||
reason: facts.reason,
|
||||
outcome: facts.outcome,
|
||||
runStatus: facts.runStatus,
|
||||
runVersion: facts.runVersion,
|
||||
runEventSequence: facts.runEventSequence,
|
||||
eventCount: facts.eventCount,
|
||||
mutationCount: facts.mutationCount,
|
||||
terminalizationCount: facts.terminalizationCount,
|
||||
toolStatus: facts.toolStatus,
|
||||
modelStatus: facts.modelStatus,
|
||||
receiptContainsLogContent: facts.receiptContainsLogContent,
|
||||
},
|
||||
{
|
||||
stage: 'log',
|
||||
reason: 'log_not_found',
|
||||
outcome: 'failed',
|
||||
runStatus: 'failed',
|
||||
runVersion: 8,
|
||||
runEventSequence: 8,
|
||||
eventCount: 8,
|
||||
mutationCount: 6,
|
||||
terminalizationCount: 1,
|
||||
toolStatus: 'succeeded',
|
||||
modelStatus: 'failed',
|
||||
receiptContainsLogContent: false,
|
||||
},
|
||||
);
|
||||
if (expectedExisting) {
|
||||
report.afterPromotion = facts;
|
||||
report.survivedPromotion = true;
|
||||
report.promotedExactReplay = true;
|
||||
} else {
|
||||
report.beforePromotion = facts;
|
||||
report.exactReplay = true;
|
||||
report.contentFree = true;
|
||||
}
|
||||
return facts;
|
||||
} finally {
|
||||
await runtimeDatabase.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveCopilotFailureDiagnosisUnknownModelHaEvidence(options) {
|
||||
const {
|
||||
port,
|
||||
report,
|
||||
fixture,
|
||||
resultKeyFixture,
|
||||
expectedExisting = false,
|
||||
} = options;
|
||||
if (!expectedExisting) {
|
||||
await executeCopilotFailureDiagnosisToolHaEvidence({
|
||||
port,
|
||||
report,
|
||||
fixture,
|
||||
resultKeyFixture,
|
||||
});
|
||||
}
|
||||
const runtimeDatabase = await databaseOpener(
|
||||
'runtime',
|
||||
databaseUrl(RUNTIME_USER, RUNTIME_PASSWORD, port),
|
||||
expectedExisting
|
||||
? 'ql3-ha-copilot-model-resolution-promoted'
|
||||
: 'ql3-ha-copilot-model-resolution-primary',
|
||||
)();
|
||||
try {
|
||||
const repository = new PostgresCopilotFailureDiagnosisModelRepository(
|
||||
runtimeDatabase.pool,
|
||||
);
|
||||
const coordinator = new DurableModelInvocationCoordinator(repository);
|
||||
const auditBase = {
|
||||
projectId: fixture.plan.projectId,
|
||||
runId: fixture.plan.runId,
|
||||
stepRunId: fixture.plan.modelStepRunId,
|
||||
traceId: fixture.plan.traceId,
|
||||
requestId: fixture.plan.modelInvocationId,
|
||||
provider: fixture.plan.model.provider,
|
||||
model: fixture.plan.model.model,
|
||||
policyRevision: 'ha-diagnosis-resolution-policy-v1',
|
||||
requestDigest: `sha256:${'f'.repeat(64)}`,
|
||||
deadlineAtMs: fixture.plan.deadlineAtMs,
|
||||
inputBytes: 128,
|
||||
maxOutputTokens: fixture.plan.model.maxOutputTokens,
|
||||
};
|
||||
const admitted = await coordinator.record({
|
||||
...auditBase,
|
||||
phase: 'admitted',
|
||||
outputBytes: 0,
|
||||
usage: null,
|
||||
errorCode: null,
|
||||
occurredAtMs: fixture.baseTimeMs + 500,
|
||||
});
|
||||
const completed = await coordinator.record({
|
||||
...auditBase,
|
||||
phase: 'failed',
|
||||
outputBytes: 0,
|
||||
usage: null,
|
||||
errorCode: 'MODEL_INVOCATION_OUTCOME_UNKNOWN',
|
||||
occurredAtMs: fixture.baseTimeMs + 600,
|
||||
});
|
||||
const resolution = await new DurableModelInvocationResolutionCoordinator(
|
||||
repository,
|
||||
).resolve({
|
||||
invocationId: fixture.plan.modelInvocationId,
|
||||
decision: 'fail',
|
||||
resolvedByUserId: fixture.plan.requestedBySubject.id,
|
||||
resolvedAtMs: fixture.baseTimeMs + 700,
|
||||
});
|
||||
const finalization = await repository.finalize(report.requestId);
|
||||
const replay = await repository.finalize(report.requestId);
|
||||
assert.equal(admitted.status, expectedExisting ? 'existing' : 'created');
|
||||
assert.equal(completed.status, expectedExisting ? 'existing' : 'created');
|
||||
assert.equal(resolution.status, expectedExisting ? 'existing' : 'created');
|
||||
assert.equal(
|
||||
finalization.status,
|
||||
expectedExisting ? 'existing' : 'created',
|
||||
);
|
||||
assert.equal(replay.status, 'existing');
|
||||
assert.deepEqual(replay.receipt, finalization.receipt);
|
||||
assert.equal(finalization.receipt.outcome, 'failed');
|
||||
assert.equal(finalization.receipt.outputArtifactId, null);
|
||||
const facts = await copilotFailureDiagnosisModelResolutionFacts(
|
||||
runtimeDatabase.pool,
|
||||
report.requestId,
|
||||
);
|
||||
assert.deepEqual(
|
||||
{
|
||||
runStatus: facts.runStatus,
|
||||
runVersion: facts.runVersion,
|
||||
runEventSequence: facts.runEventSequence,
|
||||
modelStatus: facts.modelStatus,
|
||||
modelVersion: facts.modelVersion,
|
||||
completionOutcome: facts.completionOutcome,
|
||||
resolutionDecision: facts.resolutionDecision,
|
||||
finalizationOutcome: facts.finalizationOutcome,
|
||||
eventCount: facts.eventCount,
|
||||
mutationCount: facts.mutationCount,
|
||||
resolutionCount: facts.resolutionCount,
|
||||
finalizationCount: facts.finalizationCount,
|
||||
receiptContainsModelContent: facts.receiptContainsModelContent,
|
||||
},
|
||||
{
|
||||
runStatus: 'failed',
|
||||
runVersion: 10,
|
||||
runEventSequence: 10,
|
||||
modelStatus: 'failed',
|
||||
modelVersion: 5,
|
||||
completionOutcome: 'outcome_unknown',
|
||||
resolutionDecision: 'fail',
|
||||
finalizationOutcome: 'failed',
|
||||
eventCount: 10,
|
||||
mutationCount: 8,
|
||||
resolutionCount: 1,
|
||||
finalizationCount: 1,
|
||||
receiptContainsModelContent: false,
|
||||
},
|
||||
);
|
||||
if (expectedExisting) {
|
||||
report.afterPromotion = facts;
|
||||
report.survivedPromotion = true;
|
||||
report.promotedExactReplay = true;
|
||||
} else {
|
||||
report.beforePromotion = facts;
|
||||
report.exactReplay = true;
|
||||
report.contentFree = true;
|
||||
}
|
||||
return facts;
|
||||
} finally {
|
||||
await runtimeDatabase.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function runModelProviderCredentialCatalogMatrix(options) {
|
||||
const { primaryPort, migrationPool } = options;
|
||||
const suffix = `${process.pid}-${randomBytes(3).toString('hex')}`;
|
||||
@@ -10598,6 +10972,10 @@ async function main(argv = process.argv.slice(2)) {
|
||||
let modelInvocationFeaturePromotion;
|
||||
let copilotFailureDiagnosisAdmission;
|
||||
let copilotFailureDiagnosisToolFixture;
|
||||
let copilotFailureDiagnosisTerminalization;
|
||||
let copilotFailureDiagnosisTerminalizationFixture;
|
||||
let copilotFailureDiagnosisModelResolution;
|
||||
let copilotFailureDiagnosisModelResolutionFixture;
|
||||
let modelProviderCredentialCatalog;
|
||||
let modelProviderCredentialTestConnection;
|
||||
let runAttemptLogRetentionEvidence;
|
||||
@@ -10698,6 +11076,7 @@ async function main(argv = process.argv.slice(2)) {
|
||||
'copilot_failure_diagnosis_admissions',
|
||||
'copilot_failure_diagnosis_finalizations',
|
||||
'copilot_failure_diagnosis_model_outputs',
|
||||
'copilot_failure_diagnosis_pre_model_terminalizations',
|
||||
'copilot_failure_diagnosis_tool_unlocks',
|
||||
'model_invocation_completions',
|
||||
'model_invocation_price_quotes',
|
||||
@@ -10751,6 +11130,7 @@ async function main(argv = process.argv.slice(2)) {
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_ADMISSION_MIGRATION_ID,
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_TOOL_UNLOCK_MIGRATION_ID,
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_MODEL_EXECUTION_MIGRATION_ID,
|
||||
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_PRE_MODEL_TERMINALIZATION_MIGRATION_ID,
|
||||
],
|
||||
);
|
||||
assert.deepEqual(beforePromotion.privileges, {
|
||||
@@ -10777,6 +11157,7 @@ async function main(argv = process.argv.slice(2)) {
|
||||
copilotDiagnosisAdmissionRuntimeOnly: true,
|
||||
copilotDiagnosisToolUnlockRuntimeOnly: true,
|
||||
copilotDiagnosisModelExecutionRuntimeOnly: true,
|
||||
copilotDiagnosisPreModelTerminalizationRuntimeOnly: true,
|
||||
copilotDiagnosisSnapshotRuntimeOnly: true,
|
||||
migrationHistoryRuntimeReadOnly: true,
|
||||
modelProviderCredentialManagementAuthoritySplit: true,
|
||||
@@ -10789,6 +11170,22 @@ async function main(argv = process.argv.slice(2)) {
|
||||
});
|
||||
copilotFailureDiagnosisAdmission = copilotDiagnosis.report;
|
||||
copilotFailureDiagnosisToolFixture = copilotDiagnosis.fixture;
|
||||
const copilotTerminalization =
|
||||
await runCopilotFailureDiagnosisAdmissionHaEvidence({
|
||||
primaryPort,
|
||||
migrationPool: migrationDatabase.pool,
|
||||
});
|
||||
copilotFailureDiagnosisTerminalization = copilotTerminalization.report;
|
||||
copilotFailureDiagnosisTerminalizationFixture =
|
||||
copilotTerminalization.fixture;
|
||||
const copilotModelResolution =
|
||||
await runCopilotFailureDiagnosisAdmissionHaEvidence({
|
||||
primaryPort,
|
||||
migrationPool: migrationDatabase.pool,
|
||||
});
|
||||
copilotFailureDiagnosisModelResolution = copilotModelResolution.report;
|
||||
copilotFailureDiagnosisModelResolutionFixture =
|
||||
copilotModelResolution.fixture;
|
||||
} finally {
|
||||
await migrationDatabase.close();
|
||||
}
|
||||
@@ -11193,6 +11590,48 @@ async function main(argv = process.argv.slice(2)) {
|
||||
state: 'copilot_failure_diagnosis_tool_completed',
|
||||
atMs: Number((performance.now() - startedAt).toFixed(3)),
|
||||
});
|
||||
await terminalizeCopilotFailureDiagnosisLogUnavailableHaEvidence({
|
||||
port: primaryPort,
|
||||
report: copilotFailureDiagnosisTerminalization,
|
||||
fixture: copilotFailureDiagnosisTerminalizationFixture,
|
||||
resultKeyFixture: toolResultKeyFixture,
|
||||
});
|
||||
await waitFor(async () => {
|
||||
const replicated = await copilotFailureDiagnosisTerminalizationFacts(
|
||||
standbyDatabase.pool,
|
||||
copilotFailureDiagnosisTerminalization.requestId,
|
||||
);
|
||||
return JSON.stringify(replicated) ===
|
||||
JSON.stringify(copilotFailureDiagnosisTerminalization.beforePromotion)
|
||||
? replicated
|
||||
: null;
|
||||
}, 'Copilot failure diagnosis pre-Model terminalization WAL replay');
|
||||
copilotFailureDiagnosisTerminalization.replicatedBeforePromotion = true;
|
||||
timeline.push({
|
||||
state: 'copilot_failure_diagnosis_pre_model_terminalized',
|
||||
atMs: Number((performance.now() - startedAt).toFixed(3)),
|
||||
});
|
||||
await resolveCopilotFailureDiagnosisUnknownModelHaEvidence({
|
||||
port: primaryPort,
|
||||
report: copilotFailureDiagnosisModelResolution,
|
||||
fixture: copilotFailureDiagnosisModelResolutionFixture,
|
||||
resultKeyFixture: toolResultKeyFixture,
|
||||
});
|
||||
await waitFor(async () => {
|
||||
const replicated = await copilotFailureDiagnosisModelResolutionFacts(
|
||||
standbyDatabase.pool,
|
||||
copilotFailureDiagnosisModelResolution.requestId,
|
||||
);
|
||||
return JSON.stringify(replicated) ===
|
||||
JSON.stringify(copilotFailureDiagnosisModelResolution.beforePromotion)
|
||||
? replicated
|
||||
: null;
|
||||
}, 'Copilot failure diagnosis Model resolution WAL replay');
|
||||
copilotFailureDiagnosisModelResolution.replicatedBeforePromotion = true;
|
||||
timeline.push({
|
||||
state: 'copilot_failure_diagnosis_model_resolution_finalized',
|
||||
atMs: Number((performance.now() - startedAt).toFixed(3)),
|
||||
});
|
||||
await waitFor(async () => {
|
||||
const facts = await standbyDatabase.pool.query(
|
||||
`SELECT
|
||||
@@ -12093,6 +12532,20 @@ async function main(argv = process.argv.slice(2)) {
|
||||
resultKeyFixture: toolResultKeyFixture,
|
||||
expectedExisting: true,
|
||||
});
|
||||
await terminalizeCopilotFailureDiagnosisLogUnavailableHaEvidence({
|
||||
port: standbyPort,
|
||||
report: copilotFailureDiagnosisTerminalization,
|
||||
fixture: copilotFailureDiagnosisTerminalizationFixture,
|
||||
resultKeyFixture: toolResultKeyFixture,
|
||||
expectedExisting: true,
|
||||
});
|
||||
await resolveCopilotFailureDiagnosisUnknownModelHaEvidence({
|
||||
port: standbyPort,
|
||||
report: copilotFailureDiagnosisModelResolution,
|
||||
fixture: copilotFailureDiagnosisModelResolutionFixture,
|
||||
resultKeyFixture: toolResultKeyFixture,
|
||||
expectedExisting: true,
|
||||
});
|
||||
await verifyModelProviderCredentialCatalogAfterPromotion({
|
||||
promotedPort: standbyPort,
|
||||
promotedPool: promotedDatabase.pool,
|
||||
@@ -12847,7 +13300,7 @@ async function main(argv = process.argv.slice(2)) {
|
||||
FROM "ql3"."worker_credential_deliveries") AS "credentialDeliveries"`,
|
||||
);
|
||||
assert.deepEqual(sideEffects.rows, [
|
||||
{ runs: 78, runEvents: 182, credentialDeliveries: 4 },
|
||||
{ runs: 82, runEvents: 200, credentialDeliveries: 4 },
|
||||
]);
|
||||
timeline.push({
|
||||
state: 'two_fresh_control_replicas_ready',
|
||||
@@ -12962,6 +13415,8 @@ async function main(argv = process.argv.slice(2)) {
|
||||
runDomainCommitResponseLoss: domainCommitResponseLoss.report,
|
||||
modelInvocationFeaturePromotion,
|
||||
copilotFailureDiagnosisAdmission,
|
||||
copilotFailureDiagnosisTerminalization,
|
||||
copilotFailureDiagnosisModelResolution,
|
||||
modelProviderCredentialCatalog,
|
||||
modelProviderCredentialTestConnection,
|
||||
runAttemptLogRetention,
|
||||
@@ -12996,6 +13451,30 @@ async function main(argv = process.argv.slice(2)) {
|
||||
copilotFailureDiagnosisAdmissionReplicatesAndSurvivesPromotion:
|
||||
copilotFailureDiagnosisAdmission.replicatedBeforePromotion &&
|
||||
copilotFailureDiagnosisAdmission.survivedPromotion,
|
||||
copilotFailureDiagnosisPreModelTerminalizationExactlyReplays:
|
||||
copilotFailureDiagnosisTerminalization.exactReplay &&
|
||||
copilotFailureDiagnosisTerminalization.beforePromotion
|
||||
.terminalizationCount === 1 &&
|
||||
copilotFailureDiagnosisTerminalization.beforePromotion.reason ===
|
||||
'log_not_found' &&
|
||||
copilotFailureDiagnosisTerminalization.beforePromotion.runStatus ===
|
||||
'failed' &&
|
||||
copilotFailureDiagnosisTerminalization.beforePromotion.modelStatus ===
|
||||
'failed',
|
||||
copilotFailureDiagnosisPreModelTerminalizationIsContentFree:
|
||||
copilotFailureDiagnosisTerminalization.contentFree &&
|
||||
copilotFailureDiagnosisTerminalization.beforePromotion
|
||||
.receiptContainsLogContent === false,
|
||||
copilotFailureDiagnosisPreModelTerminalizationReplicatesAndSurvivesPromotion:
|
||||
copilotFailureDiagnosisTerminalization.replicatedBeforePromotion &&
|
||||
copilotFailureDiagnosisTerminalization.survivedPromotion &&
|
||||
copilotFailureDiagnosisTerminalization.promotedExactReplay &&
|
||||
JSON.stringify(
|
||||
copilotFailureDiagnosisTerminalization.afterPromotion,
|
||||
) ===
|
||||
JSON.stringify(
|
||||
copilotFailureDiagnosisTerminalization.beforePromotion,
|
||||
),
|
||||
copilotFailureDiagnosisAdmissionUsesLeastPrivilegeRuntime:
|
||||
modelInvocationFeaturePromotion.beforePromotion.privileges
|
||||
.copilotDiagnosisAdmissionRuntimeOnly &&
|
||||
@@ -13003,6 +13482,33 @@ async function main(argv = process.argv.slice(2)) {
|
||||
.copilotDiagnosisToolUnlockRuntimeOnly &&
|
||||
modelInvocationFeaturePromotion.beforePromotion.privileges
|
||||
.copilotDiagnosisSnapshotRuntimeOnly,
|
||||
copilotFailureDiagnosisTerminalizationUsesLeastPrivilegeRuntime:
|
||||
modelInvocationFeaturePromotion.beforePromotion.privileges
|
||||
.copilotDiagnosisPreModelTerminalizationRuntimeOnly,
|
||||
copilotFailureDiagnosisUnknownModelResolutionExactlyReplays:
|
||||
copilotFailureDiagnosisModelResolution.exactReplay &&
|
||||
copilotFailureDiagnosisModelResolution.beforePromotion
|
||||
.completionOutcome === 'outcome_unknown' &&
|
||||
copilotFailureDiagnosisModelResolution.beforePromotion
|
||||
.resolutionDecision === 'fail' &&
|
||||
copilotFailureDiagnosisModelResolution.beforePromotion
|
||||
.finalizationOutcome === 'failed' &&
|
||||
copilotFailureDiagnosisModelResolution.beforePromotion
|
||||
.finalizationCount === 1,
|
||||
copilotFailureDiagnosisUnknownModelResolutionIsContentFree:
|
||||
copilotFailureDiagnosisModelResolution.contentFree &&
|
||||
copilotFailureDiagnosisModelResolution.beforePromotion
|
||||
.receiptContainsModelContent === false,
|
||||
copilotFailureDiagnosisUnknownModelResolutionReplicatesAndSurvivesPromotion:
|
||||
copilotFailureDiagnosisModelResolution.replicatedBeforePromotion &&
|
||||
copilotFailureDiagnosisModelResolution.survivedPromotion &&
|
||||
copilotFailureDiagnosisModelResolution.promotedExactReplay &&
|
||||
JSON.stringify(
|
||||
copilotFailureDiagnosisModelResolution.afterPromotion,
|
||||
) ===
|
||||
JSON.stringify(
|
||||
copilotFailureDiagnosisModelResolution.beforePromotion,
|
||||
),
|
||||
modelProviderCredentialCatalogSurvivesPromotion:
|
||||
modelProviderCredentialCatalog.survivedPromotion,
|
||||
modelProviderCredentialManagementIdentityLedgerSurvivesPromotion:
|
||||
|
||||
@@ -268,10 +268,10 @@ test('current QL3 workspace has exactly eighteen reviewed package boundaries', (
|
||||
{
|
||||
path: 'packages/ql3-ai',
|
||||
name: '@qinglong/ai',
|
||||
sourceFiles: 187,
|
||||
sourceFiles: 192,
|
||||
rootSourceFiles: 1,
|
||||
rootSourceLines: 16,
|
||||
nestedSourceFiles: 186,
|
||||
nestedSourceFiles: 191,
|
||||
rootSourceFileHardCap: 1,
|
||||
rootSourceLineHardCap: 16,
|
||||
rootSourceFileRoles: {
|
||||
|
||||
Reference in New Issue
Block a user