M2: 基座能力 —— memory / toolbox / channel / message

- memory   任务记忆四性质(只追加·信封盖戳·治理先行·归侧)、活动记忆四层与条目三件套、
           任务日志两级投影(结构化零 LLM / 叙述性小结不入事实库)、度量八指标只读投影
- toolbox  注册两审(静态审 + 试运行审)、效应等级决定运行时待遇、契约演进只增不破坏、
           白名单与纯工具私有作用域、ToolInvoker(两缝之二,live/record/replay)
- channel  通道实例配置、入站归一(解析 → 系统写 → 数据事件)、出站发送与关联记录登记
- message  通知 / 跨任务征答(就地作答 · 驳回重答只增不改)/ 死信 / 无主兜底

app:message 接上 event 的 MessageFallback;派发器按目标 ID 取 advisory lock 串行消费 +
消费幂等表;/api/ingest 模拟邮件通道(mock 只在外界)

已验证:无主 RFQ 邮件入站 → 归属解析 no-match → 事件落库 → 兜底落 OP 岗位的消息要素
门禁五道全绿;150 个单元测试 + 1 个 PG 集成测试

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
JCAI Main Agent
2026-07-27 01:16:52 +08:00
co-authored by Claude Opus 5
parent 14d4fa046a
commit f0444f1417
73 changed files with 7488 additions and 31 deletions
@@ -0,0 +1,123 @@
/**
* 任务日志:任务记忆的两级投影 —— 纯函数,零 IO(编码规范 §5.3;语义出处 OS 8.1)。
*
* 「做事与留痕是同一个动作,不存在补写日志」——
* 结构化日志(零 LLM)以引擎事件流为骨架,按事件类型筛选、模板措辞;
* **任何时刻可重算,口径与源恒一致**;归侧标签使其天然分栏。
* 叙述性小结(LLM,可选)是视图不是事实,不入事实库 —— 由 usecase 经系统用途模型生成。
*/
import type { AttributionSide } from '@contracts';
import { ATTRIBUTION_SIDE_LABEL } from '@contracts';
import type { EntryKind, MemoryEntry } from './task-memory.ts';
export type LogLine = {
readonly id: string;
readonly at: string;
readonly side: AttributionSide;
readonly sideLabel: string;
readonly title: string;
readonly summary: string | undefined;
readonly eventRef: string | undefined;
readonly position: string | undefined;
};
/** 骨架:只有这些种类进结构化日志(OS 8.1 列举) */
const SKELETON: ReadonlySet<EntryKind> = new Set<EntryKind>([
'engine-transition',
'action',
'result',
'card-sent',
'card-reply',
'ledger-write',
'action-intent',
'release-verdict',
'terminal',
'human-input',
]);
const TEMPLATE: Readonly<Record<EntryKind, (e: MemoryEntry) => string>> = {
observation: (e) => `观察:${e.title}`,
thought: (e) => `思考:${e.title}`,
action: (e) => `调用:${e.title}`,
result: (e) => `返回:${e.title}`,
'engine-transition': (e) => `迁移:${e.title}`,
evaluation: (e) => `求值:${e.title}`,
'card-sent': (e) => `发出:${e.title}`,
'card-reply': (e) => `回传:${e.title}`,
'ledger-write': (e) => `写入:${e.title}`,
'action-intent': (e) => `动作意向:${e.title}`,
'release-verdict': (e) => `放行裁决:${e.title}`,
terminal: (e) => `终态:${e.title}`,
'human-input': (e) => `留言:${e.title}`,
};
/** 结构化日志:零 LLM,可重算,每条自带谁 · 何时 · 依据 · 留痕指针 */
export function structuredLog(entries: readonly MemoryEntry[]): LogLine[] {
return entries
.filter((e) => SKELETON.has(e.kind))
.map((e) => ({
id: e.id,
at: e.at,
side: e.side,
sideLabel: ATTRIBUTION_SIDE_LABEL[e.side],
title: TEMPLATE[e.kind](e),
summary: typeof e.detail?.['summary'] === 'string' ? e.detail['summary'] : undefined,
eventRef: e.eventRef,
position: e.stamp.position,
}));
}
/** 归侧标签使日志天然分栏:机器做的 / 人做的 / 人裁的 / 在等谁 */
export function splitBySide(lines: readonly LogLine[]): Record<AttributionSide, LogLine[]> {
const out: Record<AttributionSide, LogLine[]> = {
machine: [],
human: [],
'human-verdict': [],
'world-wait': [],
};
for (const line of lines) out[line.side].push(line);
return out;
}
/* ── 度量八指标(OS 8.3):只读投影,零埋点,统计即查询 ─────────────── */
export type Metrics = {
/** 自主率 = 机器产出 ÷(机器产出 + 人产出);裁决与等待不进分母 */
readonly autonomy: number;
/** 纠正率 = 被纠正批次 ÷ 提交批次 */
readonly correction: number;
/** 有效自主率 = 自主率 ×(1 − 纠正率) */
readonly effectiveAutonomy: number;
/** 打扰率 = 每单任务调用人类工具的次数 */
readonly interruption: number;
/** DMN 覆盖率 = 确定性求值占判断次数的比例 */
readonly dmnCoverage: number;
};
export type MetricsInput = {
readonly entries: readonly MemoryEntry[];
/** 提交批次数:每次放行提交 = 一个产出批次 */
readonly submissions: number;
/** 被纠正批次:驳回重做 / 驳回迭代 / 叫停 / 裁决推翻 */
readonly corrections: number;
readonly taskCount: number;
/** 判断次数中由 DMN / 机检确定性求值的次数 */
readonly deterministicJudgements: number;
readonly totalJudgements: number;
};
export function computeMetrics(input: MetricsInput): Metrics {
const machine = input.entries.filter((e) => e.side === 'machine').length;
const human = input.entries.filter((e) => e.side === 'human').length;
const denominator = machine + human;
const autonomy = denominator === 0 ? 0 : machine / denominator;
const correction = input.submissions === 0 ? 0 : input.corrections / input.submissions;
const cards = input.entries.filter((e) => e.kind === 'card-sent').length;
return {
autonomy,
correction,
effectiveAutonomy: autonomy * (1 - correction),
interruption: input.taskCount === 0 ? 0 : cards / input.taskCount,
dmnCoverage: input.totalJudgements === 0 ? 0 : input.deterministicJudgements / input.totalJudgements,
};
}