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:
co-authored by
Claude Opus 5
parent
14d4fa046a
commit
f0444f1417
@@ -0,0 +1,35 @@
|
||||
# module: memory
|
||||
|
||||
职责: 任务记忆(会话历史 + 窗口配置)、活动记忆四层存取、任务日志两级投影、
|
||||
蒸馏钩子执行、度量八指标的只读投影
|
||||
不负责: 事件投递(见 event)、模型实例与凭证(见 llm,经 `SystemModel` 端口)、
|
||||
候选增量的回放与放行(见 activity-registry 的进化管线)
|
||||
|
||||
公开出口: index.ts
|
||||
- 类型:`MemoryService` `MemoryEntry` `AppendEntry` `VersionStamp` `GovernanceMark`
|
||||
`ActivityMemoryEntry` `MemoryLayer` `Evidence` `CandidateIncrement` `LogLine` `Metrics`
|
||||
- 错误:`NoEvidenceError`
|
||||
- 端口类型:`TaskMemoryStore` `ActivityMemoryStore` `SystemModel` `Clock` `IdGenerator`
|
||||
- 用例工厂:`createMemoryService(deps: MemoryServiceDeps): MemoryService`
|
||||
- 纯函数:`deriveSide` `windowOf` `distillable` `redactForReader` `assertAppendOnly`
|
||||
`structuredLog` `splitBySide` `computeMetrics` `assertThreePart` `selectForLoading` `isBinding`
|
||||
- adapter:`createTaskMemoryStore` `createActivityMemoryStore`
|
||||
- Drizzle 表:`taskMemoryTable` `taskMemorySealTable` `activityMemoryTable`
|
||||
|
||||
层级: L2(编码规范 §4)
|
||||
依赖: @contracts;@shared/db;port:`SystemModel`(app 注入 llm 的 ModelGateway + 系统用途实例)
|
||||
被依赖: interaction, human-tool, release, activity-registry, loop-engine, flow-engine,
|
||||
skill-engine, task, report, app(治理台 · 记忆管理)
|
||||
|
||||
不变量:
|
||||
- **只追加**:序号严格递增;封卷后不得追加
|
||||
- **归侧自动推导**:`deriveSide(kind)` 是纯函数,调用方不得指定 —— 零埋点,统计即查询
|
||||
- **信封盖戳**:每条带 `VersionStamp`(契约 / 业务模型 / 技能 / 模型版本 + 定位)
|
||||
- **治理先行**:脱敏 / 可学习标记在留痕时刻打上;`distillable` 只放行 learnable 且未脱敏的条目
|
||||
- **投影不得比源暴露更多**:`taskLog` 按读者权限走 `redactForReader`
|
||||
- **写入唯蒸馏**:活动记忆没有直写方法 —— `distill` 是唯一写入口,人只能 `prune`
|
||||
- **无证据链不得入库**:`assertThreePart` 对蒸馏产物同样生效
|
||||
- **记忆不载权力**:`isBinding` 仅在候选增量 `released` 时为真;语义引擎不读活动记忆
|
||||
- 叙述性小结是**视图不是事实**:`narrate` 不落库,系统用途模型未配置时返回 undefined
|
||||
|
||||
影响半径: index/contract/port 未变 → 对外零影响
|
||||
@@ -0,0 +1,189 @@
|
||||
/** 任务记忆四条性质与日志投影 —— domain 测试零 mock(OS 8.1 / AM 9.1) */
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
assertAppendOnly,
|
||||
deriveSide,
|
||||
distillable,
|
||||
ENTRY_KINDS,
|
||||
redactForReader,
|
||||
windowOf,
|
||||
} from '../internal/domain/task-memory.ts';
|
||||
import type { EntryKind, MemoryEntry } from '../internal/domain/task-memory.ts';
|
||||
import { computeMetrics, splitBySide, structuredLog } from '../internal/domain/task-log.ts';
|
||||
import { assertThreePart, isBinding, NoEvidenceError, selectForLoading } from '../internal/domain/activity-memory.ts';
|
||||
import type { ActivityMemoryEntry } from '../internal/domain/activity-memory.ts';
|
||||
|
||||
const entry = (kind: EntryKind, over: Partial<MemoryEntry> = {}): MemoryEntry => ({
|
||||
id: `e-${kind}-${over.seq ?? 1}`,
|
||||
taskId: 't1',
|
||||
seq: over.seq ?? 1,
|
||||
at: '2026-07-27T10:00:00.000Z',
|
||||
kind,
|
||||
side: deriveSide(kind),
|
||||
title: `${kind} 标题`,
|
||||
stamp: { contractVersion: '1', businessModelVersion: '1', skillVersions: [] },
|
||||
governance: { masked: false, learnable: true },
|
||||
...over,
|
||||
});
|
||||
|
||||
describe('deriveSide —— 归侧自动推导,零埋点', () => {
|
||||
it('每一种条目都有确定的归侧', () => {
|
||||
for (const kind of ENTRY_KINDS) expect(deriveSide(kind)).toBeTruthy();
|
||||
});
|
||||
it('机器 / 人 / 人裁 / 世界等待各归其位', () => {
|
||||
expect(deriveSide('action')).toBe('machine');
|
||||
expect(deriveSide('card-reply')).toBe('human');
|
||||
expect(deriveSide('human-input')).toBe('human');
|
||||
expect(deriveSide('release-verdict')).toBe('human-verdict');
|
||||
expect(deriveSide('terminal')).toBe('human-verdict');
|
||||
expect(deriveSide('card-sent')).toBe('world-wait');
|
||||
});
|
||||
});
|
||||
|
||||
describe('assertAppendOnly —— 只追加', () => {
|
||||
it('序号递增放行', () => {
|
||||
expect(() => assertAppendOnly(3, 4)).not.toThrow();
|
||||
});
|
||||
it('回退或重复即拒', () => {
|
||||
expect(() => assertAppendOnly(4, 4)).toThrow(/只追加/);
|
||||
expect(() => assertAppendOnly(4, 3)).toThrow(/只追加/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('windowOf —— 活动侧唯一可配置项', () => {
|
||||
const entries = [1, 2, 3, 4, 5].map((n) => entry('observation', { seq: n }));
|
||||
it('取最后 N 条', () => {
|
||||
expect(windowOf(entries, 2).map((e) => e.seq)).toEqual([4, 5]);
|
||||
});
|
||||
it('窗口大于总数即全量', () => {
|
||||
expect(windowOf(entries, 99)).toHaveLength(5);
|
||||
});
|
||||
it('窗口为零即空 —— 但历史仍完整在库', () => {
|
||||
expect(windowOf(entries, 0)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('治理先行', () => {
|
||||
it('不可学习或已脱敏的条目不进蒸馏队列', () => {
|
||||
const rows = [
|
||||
entry('action', { seq: 1 }),
|
||||
entry('action', { seq: 2, governance: { masked: false, learnable: false } }),
|
||||
entry('action', { seq: 3, governance: { masked: true, learnable: true } }),
|
||||
];
|
||||
expect(distillable(rows).map((e) => e.seq)).toEqual([1]);
|
||||
});
|
||||
|
||||
it('投影不得比源暴露更多', () => {
|
||||
const masked = entry('result', { governance: { masked: true, learnable: true }, detail: { secret: 'x' } });
|
||||
expect(redactForReader(masked, false).title).toBe('【已脱敏】');
|
||||
expect(redactForReader(masked, false).detail).toEqual({});
|
||||
expect(redactForReader(masked, true).title).toBe(masked.title);
|
||||
});
|
||||
});
|
||||
|
||||
describe('structuredLog —— 零 LLM,可重算', () => {
|
||||
const entries = [
|
||||
entry('thought', { seq: 1 }),
|
||||
entry('action', { seq: 2 }),
|
||||
entry('card-sent', { seq: 3 }),
|
||||
entry('card-reply', { seq: 4 }),
|
||||
entry('release-verdict', { seq: 5 }),
|
||||
];
|
||||
|
||||
it('只取骨架条目,思考不入日志', () => {
|
||||
const lines = structuredLog(entries);
|
||||
expect(lines).toHaveLength(4);
|
||||
expect(lines.some((l) => l.title.startsWith('思考'))).toBe(false);
|
||||
});
|
||||
|
||||
it('两次生成完全一致 —— 口径与源恒一致', () => {
|
||||
expect(structuredLog(entries)).toEqual(structuredLog(entries));
|
||||
});
|
||||
|
||||
it('归侧使日志天然分栏', () => {
|
||||
const split = splitBySide(structuredLog(entries));
|
||||
expect(split.machine).toHaveLength(1);
|
||||
expect(split['world-wait']).toHaveLength(1);
|
||||
expect(split.human).toHaveLength(1);
|
||||
expect(split['human-verdict']).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeMetrics —— 度量八指标', () => {
|
||||
it('自主率:裁决与等待不进分母', () => {
|
||||
const entries = [
|
||||
entry('action', { seq: 1 }),
|
||||
entry('result', { seq: 2 }),
|
||||
entry('card-reply', { seq: 3 }),
|
||||
entry('release-verdict', { seq: 4 }),
|
||||
entry('card-sent', { seq: 5 }),
|
||||
];
|
||||
const m = computeMetrics({
|
||||
entries,
|
||||
submissions: 2,
|
||||
corrections: 1,
|
||||
taskCount: 1,
|
||||
deterministicJudgements: 3,
|
||||
totalJudgements: 4,
|
||||
});
|
||||
expect(m.autonomy).toBeCloseTo(2 / 3);
|
||||
expect(m.correction).toBe(0.5);
|
||||
expect(m.effectiveAutonomy).toBeCloseTo((2 / 3) * 0.5);
|
||||
expect(m.interruption).toBe(1);
|
||||
expect(m.dmnCoverage).toBe(0.75);
|
||||
});
|
||||
|
||||
it('零样本不除零', () => {
|
||||
const m = computeMetrics({
|
||||
entries: [],
|
||||
submissions: 0,
|
||||
corrections: 0,
|
||||
taskCount: 0,
|
||||
deterministicJudgements: 0,
|
||||
totalJudgements: 0,
|
||||
});
|
||||
expect(m).toEqual({ autonomy: 0, correction: 0, effectiveAutonomy: 0, interruption: 0, dmnCoverage: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('活动记忆 —— 三件套与不载权力', () => {
|
||||
const base: ActivityMemoryEntry = {
|
||||
id: 'a1',
|
||||
activityId: 'act-1',
|
||||
layer: 'verdict',
|
||||
statement: '低于底价 20% 一律先问 PM',
|
||||
evidence: [{ taskId: 't1', entryIds: ['e-1'] }],
|
||||
createdAt: '2026-07-27T10:00:00.000Z',
|
||||
};
|
||||
|
||||
it('无证据链不得入库', () => {
|
||||
expect(() => assertThreePart({ statement: base.statement, evidence: [] })).toThrow(NoEvidenceError);
|
||||
expect(() => assertThreePart({ statement: base.statement, evidence: [{ taskId: 't1', entryIds: [] }] })).toThrow(
|
||||
NoEvidenceError,
|
||||
);
|
||||
});
|
||||
|
||||
it('空陈述不得入库', () => {
|
||||
expect(() => assertThreePart({ statement: ' ', evidence: base.evidence })).toThrow(NoEvidenceError);
|
||||
});
|
||||
|
||||
it('三件套齐备放行', () => {
|
||||
expect(() => assertThreePart(base)).not.toThrow();
|
||||
});
|
||||
|
||||
it('记忆不载权力:只有已放行的候选增量才有约束力', () => {
|
||||
expect(isBinding(base)).toBe(false);
|
||||
expect(isBinding({ ...base, increment: { target: 'dmn', diff: '+row', state: 'candidate' } })).toBe(false);
|
||||
expect(isBinding({ ...base, increment: { target: 'dmn', diff: '+row', state: 'released' } })).toBe(true);
|
||||
});
|
||||
|
||||
it('装载按层选摘,已修剪的不入上下文', () => {
|
||||
const rows: ActivityMemoryEntry[] = [
|
||||
base,
|
||||
{ ...base, id: 'a2', layer: 'fact' },
|
||||
{ ...base, id: 'a3', layer: 'fact', prunedAt: '2026-07-27T11:00:00.000Z' },
|
||||
];
|
||||
expect(selectForLoading(rows, { layers: ['fact'], limit: 10 }).map((e) => e.id)).toEqual(['a2']);
|
||||
expect(selectForLoading(rows, { limit: 10 })).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* module: memory —— 对外类型(编码规范 §10.1)。
|
||||
*/
|
||||
import type { AttributionSide, TaskId } from '@contracts';
|
||||
import type {
|
||||
ActivityMemoryEntry,
|
||||
CandidateIncrement,
|
||||
Evidence,
|
||||
MemoryLayer,
|
||||
} from './internal/domain/activity-memory.ts';
|
||||
import type { EntryKind, GovernanceMark, MemoryEntry, VersionStamp } from './internal/domain/task-memory.ts';
|
||||
import type { LogLine, Metrics, MetricsInput } from './internal/domain/task-log.ts';
|
||||
|
||||
export type {
|
||||
ActivityMemoryEntry,
|
||||
AttributionSide,
|
||||
CandidateIncrement,
|
||||
EntryKind,
|
||||
Evidence,
|
||||
GovernanceMark,
|
||||
LogLine,
|
||||
MemoryEntry,
|
||||
MemoryLayer,
|
||||
Metrics,
|
||||
MetricsInput,
|
||||
VersionStamp,
|
||||
};
|
||||
|
||||
export { NoEvidenceError } from './internal/domain/activity-memory.ts';
|
||||
|
||||
/** 追加一条任务记忆 —— 归侧由 kind 自动推导,调用方不得指定(零埋点) */
|
||||
export type AppendEntry = {
|
||||
readonly taskId: TaskId;
|
||||
readonly kind: EntryKind;
|
||||
readonly title: string;
|
||||
readonly detail?: Record<string, unknown>;
|
||||
readonly stamp: VersionStamp;
|
||||
readonly governance?: Partial<GovernanceMark>;
|
||||
readonly linksTo?: string;
|
||||
readonly eventRef?: string;
|
||||
};
|
||||
|
||||
export type DistillInput = {
|
||||
readonly taskId: TaskId;
|
||||
readonly activityId: string;
|
||||
readonly trigger: 'terminal' | 'milestone';
|
||||
readonly milestone?: string;
|
||||
readonly prompt: string;
|
||||
};
|
||||
|
||||
export type MemoryService = {
|
||||
/* 任务记忆:框架自动记录、只追加 */
|
||||
append(entry: AppendEntry): Promise<MemoryEntry>;
|
||||
history(taskId: TaskId): Promise<MemoryEntry[]>;
|
||||
/** 活动侧唯一可配置项:窗口大小 —— 每次 LLM 调用加载多少轮 */
|
||||
window(taskId: TaskId, size: number): Promise<MemoryEntry[]>;
|
||||
seal(taskId: TaskId): Promise<void>;
|
||||
|
||||
/* 任务日志:任务记忆的两级投影 */
|
||||
taskLog(taskId: TaskId, options?: { canSeeMasked?: boolean }): Promise<LogLine[]>;
|
||||
/** 叙述性小结(LLM,可选)—— 叙述是视图不是事实,不入事实库 */
|
||||
narrate(taskId: TaskId): Promise<string | undefined>;
|
||||
|
||||
/* 活动记忆:四层存取;写入唯蒸馏 */
|
||||
loadActivityMemory(
|
||||
activityId: string,
|
||||
options?: { layers?: readonly MemoryLayer[]; limit?: number },
|
||||
): Promise<ActivityMemoryEntry[]>;
|
||||
/** **不设直写旁路**:唯一写入口是蒸馏钩子 */
|
||||
distill(input: DistillInput): Promise<ActivityMemoryEntry[]>;
|
||||
/** 人可修剪 —— 治理台 · 记忆管理 */
|
||||
prune(entryId: string): Promise<void>;
|
||||
setIncrementState(entryId: string, state: CandidateIncrement['state']): Promise<void>;
|
||||
|
||||
metrics(input: Omit<MetricsInput, 'entries'> & { taskIds: readonly TaskId[] }): Promise<Metrics>;
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* module: memory —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。
|
||||
*/
|
||||
export type {
|
||||
ActivityMemoryEntry,
|
||||
AppendEntry,
|
||||
AttributionSide,
|
||||
CandidateIncrement,
|
||||
DistillInput,
|
||||
EntryKind,
|
||||
Evidence,
|
||||
GovernanceMark,
|
||||
LogLine,
|
||||
MemoryEntry,
|
||||
MemoryLayer,
|
||||
MemoryService,
|
||||
Metrics,
|
||||
MetricsInput,
|
||||
VersionStamp,
|
||||
} from './contract.ts';
|
||||
export { NoEvidenceError } from './contract.ts';
|
||||
|
||||
export type { ActivityMemoryStore, Clock, IdGenerator, SystemModel, TaskMemoryStore } from './port.ts';
|
||||
|
||||
export type { MemoryServiceDeps } from './internal/usecase/memory-service.ts';
|
||||
export { createMemoryService } from './internal/usecase/memory-service.ts';
|
||||
|
||||
export {
|
||||
assertThreePart,
|
||||
isBinding,
|
||||
MEMORY_LAYER_LABEL,
|
||||
MEMORY_LAYER_TARGET,
|
||||
MEMORY_LAYERS,
|
||||
selectForLoading,
|
||||
} from './internal/domain/activity-memory.ts';
|
||||
export {
|
||||
assertAppendOnly,
|
||||
deriveSide,
|
||||
distillable,
|
||||
ENTRY_KINDS,
|
||||
redactForReader,
|
||||
windowOf,
|
||||
} from './internal/domain/task-memory.ts';
|
||||
export { computeMetrics, splitBySide, structuredLog } from './internal/domain/task-log.ts';
|
||||
|
||||
export { createActivityMemoryStore, createTaskMemoryStore } from './internal/adapter/memory-store.ts';
|
||||
export { activityMemoryTable, taskMemorySealTable, taskMemoryTable } from './internal/adapter/memory-schema.ts';
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* memory 的 sys schema 表(编码规范 §7 / ADR-002)。
|
||||
*
|
||||
* 注:@mastra/memory 的线程/消息(mastra schema)由 Mastra 单写,投影一律经本模块出口(§6.2 规则 3);
|
||||
* 这里存的是**任务记忆的权威条目**(只追加、盖戳、归侧)与活动记忆四层。
|
||||
*/
|
||||
import { boolean, index, integer, jsonb, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||
import { sys } from '@shared/db.ts';
|
||||
|
||||
export const taskMemoryTable = sys.table(
|
||||
'task_memory',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
taskId: text('task_id').notNull(),
|
||||
seq: integer('seq').notNull(),
|
||||
at: timestamp('at', { withTimezone: true }).notNull(),
|
||||
kind: text('kind').notNull(),
|
||||
side: text('side').notNull(),
|
||||
title: text('title').notNull(),
|
||||
detail: jsonb('detail'),
|
||||
stamp: jsonb('stamp').notNull(),
|
||||
governance: jsonb('governance').notNull(),
|
||||
linksTo: text('links_to'),
|
||||
eventRef: text('event_ref'),
|
||||
},
|
||||
(t) => [uniqueIndex('task_memory_seq_key').on(t.taskId, t.seq), index('task_memory_task_idx').on(t.taskId, t.at)],
|
||||
);
|
||||
|
||||
/** 封卷:终态后归档,不再追加 */
|
||||
export const taskMemorySealTable = sys.table('task_memory_seal', {
|
||||
taskId: text('task_id').primaryKey(),
|
||||
sealedAt: timestamp('sealed_at', { withTimezone: true }).notNull(),
|
||||
});
|
||||
|
||||
export const activityMemoryTable = sys.table(
|
||||
'activity_memory',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
activityId: text('activity_id').notNull(),
|
||||
layer: text('layer').notNull(),
|
||||
statement: text('statement').notNull(),
|
||||
/** 证据链 —— 空即不得入库(AM 9.2) */
|
||||
evidence: jsonb('evidence').notNull(),
|
||||
increment: jsonb('increment'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
prunedAt: timestamp('pruned_at', { withTimezone: true }),
|
||||
active: boolean('active').notNull().default(true),
|
||||
},
|
||||
(t) => [index('activity_memory_layer_idx').on(t.activityId, t.layer, t.createdAt)],
|
||||
);
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* memory 存储 adapter —— Drizzle 只准出现在 adapter(编码规范 §5.3)。
|
||||
*/
|
||||
import { and, asc, desc, eq, inArray, isNull, sql } from 'drizzle-orm';
|
||||
import type { TaskId } from '@contracts';
|
||||
import type { Db } from '@shared/db.ts';
|
||||
import type { ActivityMemoryStore, TaskMemoryStore } from '../../port.ts';
|
||||
import type { ActivityMemoryEntry, MemoryLayer } from '../domain/activity-memory.ts';
|
||||
import type { EntryKind, GovernanceMark, MemoryEntry, VersionStamp } from '../domain/task-memory.ts';
|
||||
import { activityMemoryTable, taskMemorySealTable, taskMemoryTable } from './memory-schema.ts';
|
||||
|
||||
type TaskRow = typeof taskMemoryTable.$inferSelect;
|
||||
type ActivityRow = typeof activityMemoryTable.$inferSelect;
|
||||
|
||||
function toEntry(row: TaskRow): MemoryEntry {
|
||||
return {
|
||||
id: row.id,
|
||||
taskId: row.taskId,
|
||||
seq: row.seq,
|
||||
at: row.at.toISOString(),
|
||||
kind: row.kind as EntryKind,
|
||||
side: row.side as MemoryEntry['side'],
|
||||
title: row.title,
|
||||
...(row.detail === null ? {} : { detail: row.detail as Record<string, unknown> }),
|
||||
stamp: row.stamp as VersionStamp,
|
||||
governance: row.governance as GovernanceMark,
|
||||
...(row.linksTo === null ? {} : { linksTo: row.linksTo }),
|
||||
...(row.eventRef === null ? {} : { eventRef: row.eventRef }),
|
||||
};
|
||||
}
|
||||
|
||||
function toActivityEntry(row: ActivityRow): ActivityMemoryEntry {
|
||||
return {
|
||||
id: row.id,
|
||||
activityId: row.activityId,
|
||||
layer: row.layer as MemoryLayer,
|
||||
statement: row.statement,
|
||||
evidence: row.evidence as ActivityMemoryEntry['evidence'],
|
||||
...(row.increment === null
|
||||
? {}
|
||||
: { increment: row.increment as NonNullable<ActivityMemoryEntry['increment']> }),
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
...(row.prunedAt === null ? {} : { prunedAt: row.prunedAt.toISOString() }),
|
||||
};
|
||||
}
|
||||
|
||||
export function createTaskMemoryStore(db: Db): TaskMemoryStore {
|
||||
return {
|
||||
async nextSeq(taskId: TaskId) {
|
||||
const rows = await db
|
||||
.select({ max: sql<number | null>`max(${taskMemoryTable.seq})` })
|
||||
.from(taskMemoryTable)
|
||||
.where(eq(taskMemoryTable.taskId, taskId));
|
||||
return (rows[0]?.max ?? 0) + 1;
|
||||
},
|
||||
async append(entry) {
|
||||
await db.insert(taskMemoryTable).values({
|
||||
id: entry.id,
|
||||
taskId: entry.taskId,
|
||||
seq: entry.seq,
|
||||
at: new Date(entry.at),
|
||||
kind: entry.kind,
|
||||
side: entry.side,
|
||||
title: entry.title,
|
||||
detail: entry.detail ?? null,
|
||||
stamp: entry.stamp,
|
||||
governance: entry.governance,
|
||||
linksTo: entry.linksTo ?? null,
|
||||
eventRef: entry.eventRef ?? null,
|
||||
});
|
||||
},
|
||||
async list(taskId: TaskId) {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(taskMemoryTable)
|
||||
.where(eq(taskMemoryTable.taskId, taskId))
|
||||
.orderBy(asc(taskMemoryTable.seq));
|
||||
return rows.map(toEntry);
|
||||
},
|
||||
async tail(taskId: TaskId, size: number) {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(taskMemoryTable)
|
||||
.where(eq(taskMemoryTable.taskId, taskId))
|
||||
.orderBy(desc(taskMemoryTable.seq))
|
||||
.limit(Math.max(size, 0));
|
||||
return rows.map(toEntry).reverse();
|
||||
},
|
||||
async seal(taskId: TaskId, at: Date) {
|
||||
await db
|
||||
.insert(taskMemorySealTable)
|
||||
.values({ taskId, sealedAt: at })
|
||||
.onConflictDoNothing();
|
||||
},
|
||||
async isSealed(taskId: TaskId) {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(taskMemorySealTable)
|
||||
.where(eq(taskMemorySealTable.taskId, taskId))
|
||||
.limit(1);
|
||||
return rows.length > 0;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createActivityMemoryStore(db: Db): ActivityMemoryStore {
|
||||
return {
|
||||
async list(activityId, layers, limit) {
|
||||
const conditions = [eq(activityMemoryTable.activityId, activityId), isNull(activityMemoryTable.prunedAt)];
|
||||
if (layers !== undefined && layers.length > 0) {
|
||||
conditions.push(inArray(activityMemoryTable.layer, [...layers]));
|
||||
}
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(activityMemoryTable)
|
||||
.where(and(...conditions))
|
||||
.orderBy(desc(activityMemoryTable.createdAt))
|
||||
.limit(limit);
|
||||
return rows.map(toActivityEntry);
|
||||
},
|
||||
async insertMany(entries) {
|
||||
if (entries.length === 0) return;
|
||||
await db.insert(activityMemoryTable).values(
|
||||
entries.map((e) => ({
|
||||
id: e.id,
|
||||
activityId: e.activityId,
|
||||
layer: e.layer,
|
||||
statement: e.statement,
|
||||
evidence: e.evidence,
|
||||
increment: e.increment ?? null,
|
||||
createdAt: new Date(e.createdAt),
|
||||
})),
|
||||
);
|
||||
},
|
||||
async prune(entryId, at) {
|
||||
await db
|
||||
.update(activityMemoryTable)
|
||||
.set({ prunedAt: at, active: false })
|
||||
.where(eq(activityMemoryTable.id, entryId));
|
||||
},
|
||||
async setIncrementState(entryId, state) {
|
||||
await db
|
||||
.update(activityMemoryTable)
|
||||
.set({ increment: sql`jsonb_set(coalesce(${activityMemoryTable.increment}, '{}'::jsonb), '{state}', to_jsonb(${state}::text))` })
|
||||
.where(eq(activityMemoryTable.id, entryId));
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 活动记忆四层 —— 纯逻辑,零 IO(编码规范 §5.3;语义出处 OS 8.2,AM 9.2)。
|
||||
*
|
||||
* 一类一本,跨全部任务实例累积;结构不是自由笔记 —— 每类条目对准业务模型的一个作用面。
|
||||
* 三条铁律:
|
||||
* 条目三件套(必须)—— 归纳陈述 + 证据链 + 候选增量(可选);**无证据链的条目不得入库**
|
||||
* 写入 = 蒸馏(必须)—— 只经蒸馏钩子从任务记忆归纳;人可修剪;**不设直写旁路**
|
||||
* 记忆不载权力(必须)—— 只有参考力,强制力只经候选增量 + 回放 + 人放行进入业务模型
|
||||
*/
|
||||
|
||||
export const MEMORY_LAYERS = ['fact', 'pattern', 'verdict', 'exception'] as const;
|
||||
export type MemoryLayer = (typeof MEMORY_LAYERS)[number];
|
||||
|
||||
export const MEMORY_LAYER_LABEL: Readonly<Record<MemoryLayer, string>> = {
|
||||
fact: '事实层',
|
||||
pattern: '模式层',
|
||||
verdict: '裁决层',
|
||||
exception: '例外层',
|
||||
};
|
||||
|
||||
/** 每层优化谁 —— 记忆天然是模型优化的原料堆(AM 9.2) */
|
||||
export const MEMORY_LAYER_TARGET: Readonly<Record<MemoryLayer, string>> = {
|
||||
fact: '校准时限 · ifPart 阈值 · DMN 输入项 · 重试参数',
|
||||
pattern: '候选结构增量:裁量项转常项 · 新前提 · 阶段重组 · 步序修订',
|
||||
verdict: '候选新前提条件 · 收紧完成判据 · DMN 新规则行',
|
||||
exception: '规则适用边界 · 前提负向条件 · 技能适用描述修正',
|
||||
};
|
||||
|
||||
/** 证据链:指回任务记忆的具体片段,**可回放** */
|
||||
export type Evidence = {
|
||||
readonly taskId: string;
|
||||
readonly entryIds: readonly string[];
|
||||
readonly note?: string;
|
||||
};
|
||||
|
||||
export const INCREMENT_STATES = ['candidate', 'replaying', 'released', 'rejected'] as const;
|
||||
export type IncrementState = (typeof INCREMENT_STATES)[number];
|
||||
|
||||
/** 候选增量:落到业务模型 / 技能的具体 diff */
|
||||
export type CandidateIncrement = {
|
||||
readonly target: 'cmmn' | 'dmn' | 'flow-steps' | 'skill';
|
||||
readonly diff: string;
|
||||
readonly state: IncrementState;
|
||||
};
|
||||
|
||||
export type ActivityMemoryEntry = {
|
||||
readonly id: string;
|
||||
readonly activityId: string;
|
||||
readonly layer: MemoryLayer;
|
||||
/** 归纳陈述 */
|
||||
readonly statement: string;
|
||||
/** 证据链 —— 空即不得入库 */
|
||||
readonly evidence: readonly Evidence[];
|
||||
readonly increment?: CandidateIncrement;
|
||||
readonly createdAt: string;
|
||||
readonly prunedAt?: string;
|
||||
};
|
||||
|
||||
export class NoEvidenceError extends Error {
|
||||
constructor(readonly statement: string) {
|
||||
super(`活动记忆条目无证据链,不得入库:${statement}`);
|
||||
this.name = 'NoEvidenceError';
|
||||
}
|
||||
}
|
||||
|
||||
/** 条目三件套的机检 —— 写入前必过 */
|
||||
export function assertThreePart(entry: Pick<ActivityMemoryEntry, 'statement' | 'evidence'>): void {
|
||||
if (entry.statement.trim() === '') throw new NoEvidenceError('(空陈述)');
|
||||
const hasEvidence = entry.evidence.some((e) => e.entryIds.length > 0);
|
||||
if (!hasEvidence) throw new NoEvidenceError(entry.statement);
|
||||
}
|
||||
|
||||
/**
|
||||
* 装载:按定位选摘相关条目入 Agent 上下文(渐进披露)。
|
||||
* **语义引擎不读活动记忆** —— 引擎零业务,记忆是 Agent 侧的输入(AM 9.2)。
|
||||
*/
|
||||
export function selectForLoading(
|
||||
entries: readonly ActivityMemoryEntry[],
|
||||
options: { readonly layers?: readonly MemoryLayer[]; readonly limit: number },
|
||||
): ActivityMemoryEntry[] {
|
||||
const layers = options.layers;
|
||||
return entries
|
||||
.filter((e) => e.prunedAt === undefined)
|
||||
.filter((e) => layers === undefined || layers.includes(e.layer))
|
||||
.slice(0, options.limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记忆不载权力:条目对行为只有**参考力**。
|
||||
* 强制力的唯一路径是候选增量经回放与人放行进入业务模型本身 —— 本函数是那条路径的判据。
|
||||
*/
|
||||
export function isBinding(entry: ActivityMemoryEntry): boolean {
|
||||
return entry.increment?.state === 'released';
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* 任务记忆的纯逻辑 —— 零 IO(编码规范 §5.3;语义出处 OS 8.1,AM 9.1)。
|
||||
*
|
||||
* 任务记忆 = 会话历史:消息历史 + 引擎事件流,同轴合流、按时间全序。四条性质(必须):
|
||||
* 只追加 · 信封盖戳 · 治理先行 · 归侧
|
||||
*/
|
||||
import type { AttributionSide } from '@contracts';
|
||||
|
||||
/** 条目种类 —— 消息历史与引擎事件流两股同轴 */
|
||||
export const ENTRY_KINDS = [
|
||||
'observation',
|
||||
'thought',
|
||||
'action',
|
||||
'result',
|
||||
'engine-transition',
|
||||
'evaluation',
|
||||
'card-sent',
|
||||
'card-reply',
|
||||
'ledger-write',
|
||||
'action-intent',
|
||||
'release-verdict',
|
||||
'terminal',
|
||||
'human-input',
|
||||
] as const;
|
||||
export type EntryKind = (typeof ENTRY_KINDS)[number];
|
||||
|
||||
/** 版本信封:行为可归因、历史可回放的前提(AM 9.1) */
|
||||
export type VersionStamp = {
|
||||
readonly contractVersion: string;
|
||||
readonly businessModelVersion: string;
|
||||
readonly skillVersions: readonly string[];
|
||||
readonly modelName?: string;
|
||||
/** 定位:loop = 阶段 · 目标项;flow = 步序位置 */
|
||||
readonly position?: string;
|
||||
};
|
||||
|
||||
/** 治理标记:留痕时刻打上,不合规的记录不得进入蒸馏队列 */
|
||||
export type GovernanceMark = {
|
||||
readonly masked: boolean;
|
||||
readonly learnable: boolean;
|
||||
readonly retainUntil?: string;
|
||||
};
|
||||
|
||||
export type MemoryEntry = {
|
||||
readonly id: string;
|
||||
readonly taskId: string;
|
||||
readonly seq: number;
|
||||
readonly at: string;
|
||||
readonly kind: EntryKind;
|
||||
readonly side: AttributionSide;
|
||||
readonly title: string;
|
||||
readonly detail?: Record<string, unknown>;
|
||||
readonly stamp: VersionStamp;
|
||||
readonly governance: GovernanceMark;
|
||||
/** 迟到信号回链原步 —— 不改写因果,只以后续条目累加 */
|
||||
readonly linksTo?: string;
|
||||
/** 留痕指针:事件存储位置 */
|
||||
readonly eventRef?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 归侧:每条按**动作类型自动**打四侧标签 —— 度量的口径来源,零埋点(OS 8.1)。
|
||||
* 这是纯推导,不是可配置项:同一种动作恒定同一侧。
|
||||
*/
|
||||
export function deriveSide(kind: EntryKind): AttributionSide {
|
||||
switch (kind) {
|
||||
case 'observation':
|
||||
case 'thought':
|
||||
case 'action':
|
||||
case 'result':
|
||||
case 'engine-transition':
|
||||
case 'evaluation':
|
||||
case 'ledger-write':
|
||||
case 'action-intent':
|
||||
return 'machine';
|
||||
case 'card-reply':
|
||||
case 'human-input':
|
||||
return 'human';
|
||||
case 'release-verdict':
|
||||
case 'terminal':
|
||||
return 'human-verdict';
|
||||
case 'card-sent':
|
||||
return 'world-wait';
|
||||
}
|
||||
}
|
||||
|
||||
/** 只追加:序号严格递增,同一任务内不得回退或重复 */
|
||||
export function assertAppendOnly(previousSeq: number, nextSeq: number): void {
|
||||
if (nextSeq <= previousSeq) {
|
||||
throw new Error(`任务记忆只追加:序号必须递增(${previousSeq} → ${nextSeq})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 窗口:活动侧**唯一**可配置项 —— 每次 LLM 调用加载多少轮会话历史。
|
||||
* 窗口之外的历史不进上下文,但完整在库(续跑、审计、回放永远对全量记录进行)。
|
||||
*/
|
||||
export function windowOf(entries: readonly MemoryEntry[], window: number): MemoryEntry[] {
|
||||
if (window <= 0) return [];
|
||||
return entries.slice(-window);
|
||||
}
|
||||
|
||||
/** 治理约束:不合规的记录不得进入蒸馏队列(OS 8.1) */
|
||||
export function distillable(entries: readonly MemoryEntry[]): MemoryEntry[] {
|
||||
return entries.filter((e) => e.governance.learnable && !e.governance.masked);
|
||||
}
|
||||
|
||||
/**
|
||||
* 投影不得比源暴露更多 —— 日志继承任务记忆的治理标记,按读者权限裁剪生成(OS 8.1)。
|
||||
*/
|
||||
export function redactForReader(entry: MemoryEntry, canSeeMasked: boolean): MemoryEntry {
|
||||
if (!entry.governance.masked || canSeeMasked) return entry;
|
||||
return { ...entry, title: '【已脱敏】', ...(entry.detail === undefined ? {} : { detail: {} }) };
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* memory 用例 —— 编排 domain,依赖 port.ts(编码规范 §5.3)。
|
||||
*/
|
||||
import type { TaskId } from '@contracts';
|
||||
import type { AppendEntry, DistillInput, MemoryService } from '../../contract.ts';
|
||||
import type { ActivityMemoryStore, Clock, IdGenerator, SystemModel, TaskMemoryStore } from '../../port.ts';
|
||||
import type { ActivityMemoryEntry, MemoryLayer } from '../domain/activity-memory.ts';
|
||||
import { assertThreePart, MEMORY_LAYERS, selectForLoading } from '../domain/activity-memory.ts';
|
||||
import { computeMetrics, structuredLog } from '../domain/task-log.ts';
|
||||
import type { MemoryEntry } from '../domain/task-memory.ts';
|
||||
import { assertAppendOnly, deriveSide, distillable, redactForReader, windowOf } from '../domain/task-memory.ts';
|
||||
|
||||
export type MemoryServiceDeps = {
|
||||
readonly tasks: TaskMemoryStore;
|
||||
readonly activities: ActivityMemoryStore;
|
||||
readonly model: SystemModel;
|
||||
readonly clock: Clock;
|
||||
readonly ids: IdGenerator;
|
||||
};
|
||||
|
||||
/** 蒸馏产物的形状 —— 系统用途模型按此 schema 出参 */
|
||||
const DISTILL_SCHEMA = {
|
||||
type: 'object',
|
||||
required: ['entries'],
|
||||
properties: {
|
||||
entries: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
required: ['layer', 'statement', 'entryIds'],
|
||||
properties: {
|
||||
layer: { enum: [...MEMORY_LAYERS] },
|
||||
statement: { type: 'string' },
|
||||
entryIds: { type: 'array', items: { type: 'string' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
type DistillDraft = { layer: string; statement: string; entryIds: string[] };
|
||||
|
||||
export function createMemoryService(deps: MemoryServiceDeps): MemoryService {
|
||||
return {
|
||||
async append(input: AppendEntry): Promise<MemoryEntry> {
|
||||
if (await deps.tasks.isSealed(input.taskId)) {
|
||||
throw new Error(`任务记忆已封卷,不得追加:${input.taskId}`);
|
||||
}
|
||||
const seq = await deps.tasks.nextSeq(input.taskId);
|
||||
assertAppendOnly(seq - 1, seq);
|
||||
const entry: MemoryEntry = {
|
||||
id: deps.ids.next(),
|
||||
taskId: input.taskId,
|
||||
seq,
|
||||
at: deps.clock.now().toISOString(),
|
||||
kind: input.kind,
|
||||
// 归侧自动推导 —— 调用方不得指定(零埋点,OS 8.1)
|
||||
side: deriveSide(input.kind),
|
||||
title: input.title,
|
||||
...(input.detail === undefined ? {} : { detail: input.detail }),
|
||||
stamp: input.stamp,
|
||||
governance: {
|
||||
masked: input.governance?.masked ?? false,
|
||||
learnable: input.governance?.learnable ?? true,
|
||||
...(input.governance?.retainUntil === undefined ? {} : { retainUntil: input.governance.retainUntil }),
|
||||
},
|
||||
...(input.linksTo === undefined ? {} : { linksTo: input.linksTo }),
|
||||
...(input.eventRef === undefined ? {} : { eventRef: input.eventRef }),
|
||||
};
|
||||
await deps.tasks.append(entry);
|
||||
return entry;
|
||||
},
|
||||
|
||||
async history(taskId) {
|
||||
return deps.tasks.list(taskId);
|
||||
},
|
||||
|
||||
async window(taskId, size) {
|
||||
// 窗口之外的历史不进上下文,但完整在库
|
||||
return windowOf(await deps.tasks.tail(taskId, size), size);
|
||||
},
|
||||
|
||||
async seal(taskId) {
|
||||
await deps.tasks.seal(taskId, deps.clock.now());
|
||||
},
|
||||
|
||||
async taskLog(taskId, options) {
|
||||
const entries = await deps.tasks.list(taskId);
|
||||
const visible = entries.map((e) => redactForReader(e, options?.canSeeMasked ?? false));
|
||||
return structuredLog(visible);
|
||||
},
|
||||
|
||||
async narrate(taskId) {
|
||||
const model = await deps.model.systemModelName();
|
||||
if (model === undefined) return undefined;
|
||||
const lines = await this.taskLog(taskId);
|
||||
if (lines.length === 0) return undefined;
|
||||
const { text } = await deps.model.generate({
|
||||
model,
|
||||
prompt:
|
||||
'以下是一单任务的结构化日志。用不超过 200 字做阶段小结,只陈述日志里有的事实,不要推测。\n\n' +
|
||||
lines.map((l) => `${l.at} [${l.sideLabel}] ${l.title}`).join('\n'),
|
||||
});
|
||||
return text;
|
||||
},
|
||||
|
||||
async loadActivityMemory(activityId, options) {
|
||||
const entries = await deps.activities.list(activityId, options?.layers, options?.limit ?? 50);
|
||||
return selectForLoading(entries, {
|
||||
...(options?.layers === undefined ? {} : { layers: options.layers }),
|
||||
limit: options?.limit ?? 50,
|
||||
});
|
||||
},
|
||||
|
||||
/** 写入唯蒸馏:从任务记忆归纳,条目必挂证据链 */
|
||||
async distill(input: DistillInput): Promise<ActivityMemoryEntry[]> {
|
||||
const model = await deps.model.systemModelName();
|
||||
if (model === undefined) return [];
|
||||
|
||||
const source = distillable(await deps.tasks.list(input.taskId));
|
||||
if (source.length === 0) return [];
|
||||
|
||||
const { object } = await deps.model.generate({
|
||||
model,
|
||||
prompt:
|
||||
`${input.prompt}\n\n` +
|
||||
'从以下任务记忆里归纳活动记忆条目。每条必须给出 layer(fact/pattern/verdict/exception)、' +
|
||||
'statement(一般化陈述)与 entryIds(引用的条目 ID,不得为空)。只输出 JSON。\n\n' +
|
||||
source.map((e) => `${e.id} [${e.kind}] ${e.title}`).join('\n'),
|
||||
outputSchema: DISTILL_SCHEMA,
|
||||
});
|
||||
|
||||
const drafts = extractDrafts(object);
|
||||
const known = new Set(source.map((e) => e.id));
|
||||
const now = deps.clock.now().toISOString();
|
||||
|
||||
const entries: ActivityMemoryEntry[] = [];
|
||||
for (const draft of drafts) {
|
||||
const entryIds = draft.entryIds.filter((id) => known.has(id));
|
||||
const candidate = {
|
||||
id: deps.ids.next(),
|
||||
activityId: input.activityId,
|
||||
layer: normalizeLayer(draft.layer),
|
||||
statement: draft.statement,
|
||||
evidence: [{ taskId: input.taskId, entryIds }],
|
||||
createdAt: now,
|
||||
} satisfies ActivityMemoryEntry;
|
||||
// 无证据链的条目不得入库 —— 蒸馏产物一样过这道门
|
||||
try {
|
||||
assertThreePart(candidate);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
entries.push(candidate);
|
||||
}
|
||||
|
||||
if (entries.length > 0) await deps.activities.insertMany(entries);
|
||||
return entries;
|
||||
},
|
||||
|
||||
async prune(entryId) {
|
||||
await deps.activities.prune(entryId, deps.clock.now());
|
||||
},
|
||||
|
||||
async setIncrementState(entryId, state) {
|
||||
await deps.activities.setIncrementState(entryId, state);
|
||||
},
|
||||
|
||||
async metrics(input) {
|
||||
const all: MemoryEntry[] = [];
|
||||
for (const taskId of input.taskIds) all.push(...(await deps.tasks.list(taskId as TaskId)));
|
||||
return computeMetrics({ ...input, entries: all });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function extractDrafts(object: unknown): DistillDraft[] {
|
||||
if (object === null || typeof object !== 'object') return [];
|
||||
const entries = (object as { entries?: unknown }).entries;
|
||||
if (!Array.isArray(entries)) return [];
|
||||
return entries.flatMap((raw): DistillDraft[] => {
|
||||
if (raw === null || typeof raw !== 'object') return [];
|
||||
const { layer, statement, entryIds } = raw as Record<string, unknown>;
|
||||
if (typeof layer !== 'string' || typeof statement !== 'string') return [];
|
||||
const ids = Array.isArray(entryIds) ? entryIds.filter((x): x is string => typeof x === 'string') : [];
|
||||
return [{ layer, statement, entryIds: ids }];
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeLayer(value: string): MemoryLayer {
|
||||
return (MEMORY_LAYERS as readonly string[]).includes(value) ? (value as MemoryLayer) : 'fact';
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "@modules/memory",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": { ".": "./index.ts" }
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* module: memory —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。
|
||||
*/
|
||||
import type { TaskId } from '@contracts';
|
||||
import type { ActivityMemoryEntry, MemoryLayer } from './internal/domain/activity-memory.ts';
|
||||
import type { MemoryEntry } from './internal/domain/task-memory.ts';
|
||||
|
||||
export type Clock = { now(): Date };
|
||||
export type IdGenerator = { next(): string };
|
||||
|
||||
export type TaskMemoryStore = {
|
||||
nextSeq(taskId: TaskId): Promise<number>;
|
||||
append(entry: MemoryEntry): Promise<void>;
|
||||
list(taskId: TaskId): Promise<MemoryEntry[]>;
|
||||
tail(taskId: TaskId, size: number): Promise<MemoryEntry[]>;
|
||||
seal(taskId: TaskId, at: Date): Promise<void>;
|
||||
isSealed(taskId: TaskId): Promise<boolean>;
|
||||
};
|
||||
|
||||
export type ActivityMemoryStore = {
|
||||
list(activityId: string, layers: readonly MemoryLayer[] | undefined, limit: number): Promise<ActivityMemoryEntry[]>;
|
||||
insertMany(entries: readonly ActivityMemoryEntry[]): Promise<void>;
|
||||
prune(entryId: string, at: Date): Promise<void>;
|
||||
setIncrementState(entryId: string, state: string): Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 模型调用 —— app 注入 `llm`(L1)的 ModelGateway。
|
||||
* 蒸馏钩子与叙述性小结用**系统用途**绑定实例:计量入系统账、不占活动预算(OS 02)。
|
||||
*/
|
||||
export type SystemModel = {
|
||||
generate(input: {
|
||||
readonly model: string;
|
||||
readonly prompt: string;
|
||||
readonly outputSchema?: Record<string, unknown>;
|
||||
}): Promise<{ text: string; object?: unknown }>;
|
||||
/** 系统用途模型名;未配置即 undefined —— 此时叙述与蒸馏静默跳过,不报错 */
|
||||
systemModelName(): Promise<string | undefined>;
|
||||
};
|
||||
Reference in New Issue
Block a user