Archived
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:
co-authored by
Claude Opus 5
parent
4ad901c452
commit
c6cdb4c310
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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) };
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user