M5(完成): @activity-sdk 与首个活动包 quote-intake

@activity-sdk(§8.2 / ADR-004):
- 基座六件接口(契约 / 交互 / 记忆 / 数据 / 工具 / LLM)—— 活动看到的全部世界
- defineActivity() 的 ABI;本地开发宿主(mock 工具表 + 模拟工位 + 录制夹具),
  宿主自身就守三规矩:裸调用拒发、视图含脚本拒推、监听无时限拒登记

activities/quote-intake —— 统一报价 A1 · RFQ 需求分析(flow):
- 契约七件 / 步序声明五步(LLM 抽取 → 条件跳步补全 → 查历史价 → DMN 打标 → 验收放行)
- 三分类规则 P-03 落 DMN 决策表(C 优先于 A,命中行留痕)
- 台账声明两对象、监听时限必配、SLA 落 Harness

depcruise 实测拦下活动包越界 import(activities → apps 双向隔离),
测试遂按边界拆分:活动侧只用 @sdk/@contracts/@sfeel

358 个测试通过,门禁五道全绿

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 02:09:43 +08:00
co-authored by Claude Opus 5
parent 4ad901c452
commit c6cdb4c310
11 changed files with 867 additions and 20 deletions
+99
View File
@@ -0,0 +1,99 @@
/**
* 活动基座六件 —— 活动开发者看到的**全部世界**(编码规范 §8.2;语义出处 OS 4.1)。
*
* 「基座即笼子:契约限承诺、工具限能力、交互限入口、记忆与留痕限事实、数据限归属、
* LLM 调用限模型 —— **内容归活动,笼子归系统**。
* 活动代码**无环境能力**,一切能力经调用契约注入,越权在结构上不可能。」
*/
import type {
ActivityMemoryLayer,
ChatCardDraft,
LedgerKey,
LedgerRow,
ViewBlockLike,
WorkOrderDraft,
} from './types.ts';
/* ① 契约 —— 只读:活动启动前冻结,运行中不得变更 */
export type ContractSpec = {
readonly activityId: string;
readonly version: string;
/** 输入:data(已过 schema 校验)+ message(委托消息,可为空) */
readonly data: Readonly<Record<string, unknown>>;
readonly message: string | undefined;
readonly outputPorts: { readonly mandatory: readonly string[]; readonly selective: readonly string[] };
readonly prohibitions: readonly string[];
/** AI 可自决的范围;超出必须调「人 · 决策」 */
readonly discretion: readonly string[];
};
/* ② 交互 —— 对话流(唯一回环输入口,只出不进)+ 视图区容器(只读呈现) */
export type InteractionHandle = {
/** 视图流控:按局面推结构化块 */
pushView(blocks: readonly ViewBlockLike[]): Promise<void>;
/** 视图模版 —— 过净化门:含脚本 / 外部资源即被拒 */
pushHtml(html: string): Promise<void>;
/** 对话区输出(AI 发言) */
say(text: string, tag?: 'conclusion' | 'guidance' | 'criterion'): Promise<void>;
/** 调人:工单两模式 */
issueWorkOrder(order: WorkOrderDraft): Promise<{ id: string }>;
/** 调人:对话卡族(决策 / 补全 / 评价) */
issueCard(card: ChatCardDraft): Promise<{ id: string }>;
};
/* ③ 记忆 —— 任务记忆只有**窗口配置**一个可配项;活动记忆只有存与取 */
export type MemoryHandle = {
/** 每次 LLM 调用加载多少轮会话历史 —— 活动侧唯一记忆接口 */
window(size: number): Promise<ReadonlyArray<{ at: string; title: string; side: string }>>;
/** 活动记忆按定位选摘(渐进披露);**语义引擎不读** */
recall(options?: { layers?: readonly ActivityMemoryLayer[]; limit?: number }): Promise<
ReadonlyArray<{ layer: ActivityMemoryLayer; statement: string }>
>;
};
/* ④ 数据(台账)—— 三级寻址 + 通用事务接口;放行前一切写入落草稿层 */
export type LedgerHandle = {
/** 任务视图 = 正式版 + 本任务草稿层(读己之写) */
read(address: string, where?: Readonly<Record<string, string | number>>): Promise<LedgerRow[]>;
/** 草稿写 —— **不产生数据事件**;放行时合并入正式版 */
write(address: string, key: LedgerKey, patch: LedgerRow): Promise<void>;
};
/* ⑤ 工具(调用契约)—— 授权在白名单与契约,不在活动代码 */
export type ToolInvokerHandle = {
invoke(tool: string, input: Readonly<Record<string, unknown>>): Promise<{
ok: boolean;
output?: Record<string, unknown>;
error?: { code: string; message: string };
}>;
/** 写类动作:登记为动作意向(入参冻结),放行后由系统执行落地 */
intend(tool: string, input: Readonly<Record<string, unknown>>, label: string): Promise<void>;
/** 立监听:AI 对世界只有触达(动作)与等待(监听)两种姿势;**时限必配** */
listen(input: {
eventType: 'data' | 'time';
keyFilter: Readonly<Record<string, string>>;
deadline: string;
}): Promise<{ id: string }>;
};
/* ⑥ LLM 调用 —— 活动只声明**自定义模型名**,不触厂商与密钥 */
export type LlmHandle = {
generate(input: {
/** 自定义模型名(逻辑名) */
model: string;
prompt: string;
system?: string;
/** 给定即要求结构化出参并校验 */
outputSchema?: Record<string, unknown>;
}): Promise<{ text: string; object?: unknown }>;
};
/** 六件合体 —— 活动运行时拿到的全部句柄 */
export type ActivityBase = {
readonly contract: ContractSpec;
readonly interaction: InteractionHandle;
readonly memory: MemoryHandle;
readonly ledger: LedgerHandle;
readonly tools: ToolInvokerHandle;
readonly llm: LlmHandle;
};
+32
View File
@@ -0,0 +1,32 @@
/**
* `defineActivity()` —— 活动包的 ABI 入口(ADR-004)。
*
* 「单文件 ESM,**默认导出** `defineActivity()` 的返回值;能力全部注入(基座六件句柄),
* **零环境能力**;静态审扫描产物 import 与全局引用。」
*/
import type { ActivityBase } from './base.ts';
/** 活动的运行结论 —— 提交放行是唯一的成功出路 */
export type ActivityOutcome =
/** 判定成果已齐,提交放行(必交付物齐备、判据全绿只是可提交) */
| { readonly kind: 'submit'; readonly deliverable: string; readonly rationale: string }
/** 等外界:已立监听,回合到此为止 */
| { readonly kind: 'await'; readonly reason: string }
/** 无法推进,申报受阻待人 */
| { readonly kind: 'blocked'; readonly reason: string };
export type ActivityDefinition = {
readonly name: string;
/** 范式只存在于引擎层;此处声明是给装载器看的 */
readonly paradigm: 'loop' | 'flow';
/**
* 一个回合的推进。由**引擎**驱动调用 —— 活动不自转、不自设 maxSteps;
* 预算与熔断由基座在环外强制。
*/
run(base: ActivityBase): Promise<ActivityOutcome>;
};
export function defineActivity(definition: ActivityDefinition): ActivityDefinition {
if (definition.name.trim() === '') throw new Error('活动必须有名字');
return Object.freeze(definition);
}
+132
View File
@@ -0,0 +1,132 @@
/**
* 本地开发宿主 —— mock 工具表 + 模拟工位(工单 / 卡)+ 回放夹具(编码规范 §8.2)。
*
* 「系统外对着声明开发(本地 mock)」:开发者不必起系统就能跑活动;
* 夹具录下的出入参可直接喂给回放审(§7.3 两缝的同一副夹具)。
*/
import type { ActivityBase, ContractSpec } from './base.ts';
import type { ActivityDefinition, ActivityOutcome } from './define.ts';
import type { ChatCardDraft, LedgerKey, LedgerRow, ViewBlockLike, WorkOrderDraft } from './types.ts';
export type HostRecording = {
readonly views: ViewBlockLike[][];
readonly htmls: string[];
readonly says: string[];
readonly workOrders: WorkOrderDraft[];
readonly cards: ChatCardDraft[];
readonly toolCalls: Array<{ tool: string; input: Record<string, unknown> }>;
readonly intents: Array<{ tool: string; input: Record<string, unknown>; label: string }>;
readonly listeners: Array<{ eventType: string; keyFilter: Record<string, string>; deadline: string }>;
readonly llmCalls: Array<{ model: string; prompt: string }>;
readonly writes: Array<{ address: string; key: LedgerKey; patch: LedgerRow }>;
};
export type HostOptions = {
readonly contract: ContractSpec;
/** mock 工具表:工具名 → 固定出参 */
readonly tools?: Readonly<Record<string, (input: Record<string, unknown>) => Record<string, unknown>>>;
/** mock 模型:模型名 → 固定出参 */
readonly models?: Readonly<Record<string, (prompt: string) => { text: string; object?: unknown }>>;
/** 台账初值:地址 → 行 */
readonly ledger?: Readonly<Record<string, LedgerRow[]>>;
readonly memory?: ReadonlyArray<{ layer: 'fact' | 'pattern' | 'verdict' | 'exception'; statement: string }>;
};
export type MockHost = {
readonly base: ActivityBase;
readonly recording: HostRecording;
run(definition: ActivityDefinition): Promise<ActivityOutcome>;
};
export function createMockHost(options: HostOptions): MockHost {
const recording: HostRecording = {
views: [],
htmls: [],
says: [],
workOrders: [],
cards: [],
toolCalls: [],
intents: [],
listeners: [],
llmCalls: [],
writes: [],
};
const ledger = new Map<string, LedgerRow[]>(Object.entries(options.ledger ?? {}));
let cardSeq = 0;
const base: ActivityBase = {
contract: options.contract,
interaction: {
async pushView(blocks) {
recording.views.push([...blocks]);
},
async pushHtml(html) {
if (/<\s*script\b/i.test(html)) throw new Error('视图模版不得含 <script> —— 容器无脚本执行权');
recording.htmls.push(html);
},
async say(text) {
recording.says.push(text);
},
async issueWorkOrder(order) {
if (order.basis.length === 0) throw new Error('调用必附上下文:裸调用基座拒发');
recording.workOrders.push(order);
return { id: `wo-${(cardSeq += 1)}` };
},
async issueCard(card) {
recording.cards.push(card);
return { id: `card-${(cardSeq += 1)}` };
},
},
memory: {
async window() {
return [];
},
async recall(opts) {
const all = options.memory ?? [];
const layers = opts?.layers;
return all.filter((m) => layers === undefined || layers.includes(m.layer)).slice(0, opts?.limit ?? 50);
},
},
ledger: {
async read(address, where) {
const rows = ledger.get(address) ?? [];
if (where === undefined) return rows;
return rows.filter((row) => Object.entries(where).every(([k, v]) => row[k] === v));
},
async write(address, key, patch) {
recording.writes.push({ address, key, patch });
const rows = ledger.get(address) ?? [];
const index = rows.findIndex((row) => Object.entries(key).every(([k, v]) => row[k] === v));
if (index < 0) rows.push({ ...key, ...patch });
else rows[index] = { ...rows[index], ...patch };
ledger.set(address, rows);
},
},
tools: {
async invoke(tool, input) {
recording.toolCalls.push({ tool, input: { ...input } });
const impl = options.tools?.[tool];
if (impl === undefined) return { ok: false, error: { code: 'not-mocked', message: `工具未 mock:${tool}` } };
return { ok: true, output: impl({ ...input }) };
},
async intend(tool, input, label) {
recording.intents.push({ tool, input: { ...input }, label });
},
async listen(input) {
if (input.deadline.trim() === '') throw new Error('监听时限必配 —— 无死等');
recording.listeners.push({ ...input, keyFilter: { ...input.keyFilter } });
return { id: `listener-${(cardSeq += 1)}` };
},
},
llm: {
async generate(input) {
recording.llmCalls.push({ model: input.model, prompt: input.prompt });
const impl = options.models?.[input.model];
if (impl === undefined) throw new Error(`模型未 mock:${input.model}`);
return impl(input.prompt);
},
},
};
return { base, recording, run: (definition) => definition.run(base) };
}
+28 -3
View File
@@ -1,5 +1,30 @@
/**
* @sdk —— 活动开发面(编码规范 §8.2):基座六件接口 + defineActivity() + 本地开发宿主 + 回放夹具。
* 活动包 ABI 见 ADR-004。
* @sdk —— 活动开发面(编码规范 §8.2)。
*
* 活动包只准 import `@sdk` / `@contracts` 与白名单纯库;
* **不得触任何环境能力**(`process.env` / `fs` / `fetch`…)—— 静态审扫描产物(ADR-004)。
*/
export const SDK_VERSION = '0.0.0-m0';
export type {
ActivityBase,
ContractSpec,
InteractionHandle,
LedgerHandle,
LlmHandle,
MemoryHandle,
ToolInvokerHandle,
} from './base.ts';
export type { ActivityDefinition, ActivityOutcome } from './define.ts';
export { defineActivity } from './define.ts';
export type {
ActivityMemoryLayer,
ChatCardDraft,
FormFieldDraft,
LedgerKey,
LedgerRow,
ViewBlockLike,
WorkOrderDraft,
} from './types.ts';
export type { HostOptions, HostRecording, MockHost } from './host.ts';
export { createMockHost } from './host.ts';
export const SDK_VERSION = '0.1.0-m5';
+52
View File
@@ -0,0 +1,52 @@
/**
* SDK 的轻量类型 —— 与 @contracts 同构但不强绑,活动包只依赖 @sdk 与 @contracts(§5.1)。
*/
export type ActivityMemoryLayer = 'fact' | 'pattern' | 'verdict' | 'exception';
export type LedgerRow = Record<string, unknown>;
export type LedgerKey = Record<string, string | number>;
/** 视图块 —— 结构与 @contracts 的 ViewBlock 一致,此处放宽以免 SDK 追着契约走 */
export type ViewBlockLike = { t: string } & Record<string, unknown>;
export type FormFieldDraft = {
key: string;
label: string;
kind: 'text' | 'textarea' | 'radio' | 'number' | 'select' | 'check' | 'date' | 'money' | 'file';
required?: boolean;
options?: Array<{ value: string; label: string }>;
ai?: string;
hint?: string;
};
/** 工单两模式:执行 = 指导书 + 空表单;验收 = 可修订报告 + 判据核对 */
export type WorkOrderDraft =
| {
mode: 'execution';
title: string;
/** 调用必附上下文 —— 空即被基座拒发 */
basis: Array<{ k: string; v: string }>;
ask: string;
standard?: string;
steps?: string[];
fields: FormFieldDraft[];
}
| {
mode: 'acceptance';
title: string;
basis: Array<{ k: string; v: string }>;
deliverable: string;
criteria: Array<{ label: string; level: 'L1' | 'L2' | 'L3' | 'L4' | 'L5'; pass: boolean }>;
fields?: FormFieldDraft[];
};
export type ChatCardDraft =
| {
k: 'decision';
ask: string;
basis: Array<{ k: string; v: string }>;
options: Array<{ value: string; label: string; detail?: string; consequence?: string; recommended?: boolean }>;
allowCustom?: boolean;
}
| { k: 'elicitation'; ask: string; fields: Array<{ key: string; label: string; placeholder?: string }> }
| { k: 'rating'; ask: string };