forked from ChaiHao/BPOS-MVP
用户实测报的:任务起起来了,点通过就卡住,测试台也看不到日志。查下来是一条真事故: **机理**(证据链完整):`land()` 抛错(草稿与正式版并发冲突)→ 异常穿到推进器 → 推进器写一行 stderr 就 break;而这一拍**已经把 runtime 落回 running、已经扣了 一步预算** —— 两秒后又来一次,空转到 steps 触顶。实测:3 步的活跑了 40 步, 终态 `exhausted`,而真正的原因只在服务端 stderr 里刷屏 (`草稿与正式版并发冲突:2 处字段已被他单改动` 刷了几十行), 任务记忆、界面、trace 上一个字都没有 —— 这正是「看不到日志」。 冲突检测本身**是对的**(多单并发写同一行,基线过期即冲突,不该静默覆盖); 坏的是失败处理。三处修: ① **落地失败落终态**:`land` 抛错 → 就地 `blocked` + 原因写进任务记忆(人读得到), 推进器再也不会来推它 —— 空转到预算耗尽那条路断了 ② **失败在时间轴上标红**:非放行终态的节点 span 走 `fail()` 并带原因, 不再收成一条无害的「已结束」(从前失败的单在观测面上与走完的单长得一模一样) ③ 顺带修:`listInstances` 没读回 `assignee_id`(写了不读等于没写)—— 上一提交已含 实测复核(真库 + HTTP 全链):正常路径 A1 四步 released、流转到 A4(修复前是 40 步 exhausted);单测三例锁死失败路径(blocked 终态 / 只调一次 land / 不误伤正常路)。 五闸全过,测试台六例端到端照旧绿。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
99 lines
3.7 KiB
TypeScript
99 lines
3.7 KiB
TypeScript
/**
|
|
* module: trace —— **全链留痕的观测面**(设计台「每个活动完全 Trace 监控」)。
|
|
*
|
|
* 一条工作流 = 一条 trace;节点实例 = 一个 span;活动内的每一步、每次工具调用、
|
|
* 每次模型调用、每张人卡 = 子 span。span 由 **Mastra Observability 组件**产出
|
|
* (我们的执行不经 Mastra 图,所以是**手动打点**),再由自定义 exporter 落库。
|
|
*
|
|
* 与任务记忆的分工:任务记忆是**人读的留痕**(谁在何时做了什么、依据什么),
|
|
* 一件事一行;trace 是**机器读的时间轴**(嵌套、耗时、入出参、错误),
|
|
* 一次调用一个 span。两者并行,不互相替代。
|
|
*/
|
|
export type SpanKind = 'workflow' | 'node' | 'step' | 'tool' | 'model' | 'human';
|
|
|
|
export type TraceSpan = {
|
|
readonly id: string;
|
|
readonly traceId: string;
|
|
readonly parentId?: string;
|
|
readonly name: string;
|
|
readonly kind: SpanKind;
|
|
readonly workflowId?: string;
|
|
readonly nodeId?: string;
|
|
readonly taskId?: string;
|
|
readonly input?: Record<string, unknown>;
|
|
readonly output?: Record<string, unknown>;
|
|
readonly error?: { message: string };
|
|
readonly attributes?: Record<string, unknown>;
|
|
readonly startedAt: string;
|
|
readonly endedAt?: string;
|
|
};
|
|
|
|
/** 一次可观测的执行:开一个 span,干活,收尾 —— 收尾必做,不然时间轴上永远挂着 */
|
|
export type SpanHandle = {
|
|
readonly id: string;
|
|
readonly traceId: string;
|
|
child(input: {
|
|
name: string;
|
|
kind: SpanKind;
|
|
nodeId?: string;
|
|
taskId?: string;
|
|
input?: Record<string, unknown>;
|
|
attributes?: Record<string, unknown>;
|
|
}): SpanHandle;
|
|
end(out?: { output?: Record<string, unknown>; attributes?: Record<string, unknown> }): Promise<void>;
|
|
fail(error: unknown): Promise<void>;
|
|
};
|
|
|
|
export type TraceService = {
|
|
/*
|
|
* 作用域索引 —— 调用方只报 id,父子关系由服务自己拼。
|
|
* 打点散在装配层的十几处,让每处自己拎 handle 传来传去,一处漏传整棵树就断了。
|
|
*/
|
|
/** 开一条工作流的 trace(root span) */
|
|
beginWorkflow(input: { workflowId: string; subject: string; input?: Record<string, unknown> }): void;
|
|
/** 开一个节点 span(自动挂在工作流 span 下) */
|
|
beginNode(input: {
|
|
workflowId: string;
|
|
nodeId: string;
|
|
label: string;
|
|
taskId?: string;
|
|
approvalId?: string;
|
|
assigneeId?: string;
|
|
input?: Record<string, unknown>;
|
|
}): void;
|
|
/** 活动内的一次动作:步骤 / 工具 / 模型 / 人卡 —— 自动挂在该任务的节点 span 下 */
|
|
beginAction(input: {
|
|
taskId: string;
|
|
name: string;
|
|
kind: SpanKind;
|
|
input?: Record<string, unknown>;
|
|
}): SpanHandle | undefined;
|
|
/** 收尾:节点终态 / 工作流终态 */
|
|
endNode(input: {
|
|
taskId?: string;
|
|
workflowId?: string;
|
|
nodeId?: string;
|
|
output?: Record<string, unknown>;
|
|
/** 非放行终态的原因 —— 有它就把这一段标成错,时间轴上一眼看出哪儿坏了 */
|
|
error?: string;
|
|
}): Promise<void>;
|
|
endWorkflow(input: { workflowId: string; state: string }): Promise<void>;
|
|
|
|
/** 起一条 trace(工作流级)或挂在既有 trace 下 —— 低层口,便利层用它 */
|
|
start(input: {
|
|
name: string;
|
|
kind: SpanKind;
|
|
workflowId?: string;
|
|
nodeId?: string;
|
|
taskId?: string;
|
|
parent?: { traceId: string; spanId: string };
|
|
input?: Record<string, unknown>;
|
|
attributes?: Record<string, unknown>;
|
|
}): SpanHandle;
|
|
/** 这条工作流的完整 span 树(按开始时间序;树形由 parentId 拼) */
|
|
ofWorkflow(workflowId: string): Promise<TraceSpan[]>;
|
|
/** 这个任务(活动实例)的 span —— 活动内每一步都在这儿 */
|
|
ofTask(taskId: string): Promise<TraceSpan[]>;
|
|
flush(): Promise<void>;
|
|
};
|