@weiliang0121/capix-model-exchanges 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wei.liang (https://github.com/weiliang0121)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,37 @@
1
- # Temporary Holding Version
1
+ # ModelExchanges
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `@weiliang0121/capix-model-exchanges` 保存模型的完整回复与每个工具调用的正式反馈。它不保存调用运行状态,不执行工具,不把工具消息写入 Transcript。
4
+
5
+ ```ts
6
+ const exchanges = new ModelExchanges({ persistence });
7
+ const resources = new Resources().register(new Resource({ exchanges, scope: 'business-loop-id' }), ModelExchangeResource);
8
+ ```
9
+
10
+ 业务节点申请 `ModelExchangeResource.request()`。模型子结果的 `result.succeeded` 中调用 `intake(reply)`,工具结果接纳中调用 `receive(ref, callId, result)`;资源适配从当前 `activity.reception()` 取得真实父子来源。预检拒绝使用 `complete(ref, callId, result)`,不伪造子调用。宿主可直接使用实体方法并负责提供可信来源。
11
+
12
+ `scope` 只是宿主固定的业务归属,可以跨多次调用。它不是权限凭据,随机 UUID 也不提供授权;资源供给和宿主隔离决定访问范围。
13
+
14
+ 对象关系:
15
+
16
+ ```text
17
+ ModelExchanges
18
+ └─ ModelExchangeEntry(创建顺序不变)
19
+ ├─ 一份 ModelReply:共同输出 + 不重叠的原生补充
20
+ ├─ feedback:按 toolCallId 固定的结果槽位
21
+ └─ operation / receptions:固定接纳凭据
22
+ ↓
23
+ ModelExchangeResource ModelExchangeViewSource
24
+ 受窗口控制的操作 同一交换作为一个 View 成员
25
+ ```
26
+
27
+ - `intake(scope, operation, reply)` 对同一固定操作确认原回复;冲突拒绝。操作键和反馈摘要采用规范化 JSON,不依赖对象属性构造顺序。
28
+ - `complete` 新增反馈要求准确集合、scope、交换 ID 和当前 revision;已有槽位的同内容、同来源确认允许较旧引用,不新增 sequence。确认返回当前引用。
29
+ - `read` 默认要求准确 revision;资源 `read` 明确允许旧版本引用查询当前槽位,供固定接纳确认。`closed` 要求准确引用、没有重复交换,并且每个工具槽位完整。终止原因是否允许续接仍由模型/业务契约解释。
30
+ - `snapshot/restore` 接纳并冻结纯 JSON。恢复先核验集合外壳与条目数组,再逐条捕获、核验并累计集合容量;不将单条目的结构节点预算误用于整个集合。检查版本、创建顺序、变化计数、唯一效果来源、工具槽位与来源关联,不发布变化或调用持久端口。
31
+ - 可选持久端口先同步 `commit({ collection, expectedSequence, sequence, previous, entry })`,成功后才安装并发布变化。宿主应原子核验 sequence 和 previous。端口失败或异步返回产生 `PersistenceError`,该实体停止后续写入;宿主重新载入决定未知提交的实际结果。不会自动重试。变化发布失败保留已经成立的事实。
32
+ - 默认预算 `defaultModelExchangeLimits` 为 10,000 个交换、累计 64 MiB 的交换 JSON;可显式调整,`maximumModelExchangeLimits` 限定最大 100,000 个、128 MiB。每个交换最大 `maximumModelExchangeEntryBytes`(8 MiB),Provider 单回复/单反馈自己的限制仍适用。超限在持久提交前拒绝,不将实体标为持久故障;已有事实仍可读取和确认。没有自动淘汰:过期与保留策略归宿主。
33
+ - ViewSource 可以固定某个 scope 或全体;版本覆盖成员和反馈变化,完整目录按创建顺序返回,`inspect/read` 只访问请求的 keys。旧快照保持原值,版本变化后读取拒绝。视图策略可退出整个交换,不能留下孤立 tool 消息。
34
+
35
+ 宿主负责集合的创建、持久化和重新载入。当前快照通过 `ModelExchanges.restore(state, { persistence, limits })` 恢复;旧格式明确拒绝,不自动迁移。预算不写进快照,恢复时应传入相同配置;更低预算无法容纳快照时明确拒绝,不截断或淘汰事实。
36
+
37
+ 实体提交不与 Invocation CP 或其他实体构成事务。退出 View 或关闭宿主持有的句柄不会删除交换事实,宿主仍需保留被恢复或续接依据引用的集合。参考服务的 SQLite 装配与归档入口规则见仓库中的 Conversation 服务文档;它们不属于本包的依赖或存储契约。
@@ -0,0 +1,31 @@
1
+ import { type ChangeCapture, type EventConsumer } from '@weiliang0121/capix-changes';
2
+ import { type ModelReply, type ToolResult } from '@weiliang0121/capix-model-provider';
3
+ import type { ExchangeOperation, ExchangeReference, ModelExchangeChange, ModelExchangeEntry, ModelExchangesPersistence, ModelExchangesState } from './types.js';
4
+ export interface ModelExchangesOptions {
5
+ readonly persistence?: ModelExchangesPersistence;
6
+ readonly changes?: ChangeCapture<ModelExchangeChange>;
7
+ readonly observation?: EventConsumer<ModelExchangeChange>;
8
+ readonly limits?: {
9
+ readonly entries?: number;
10
+ readonly bytes?: number;
11
+ };
12
+ }
13
+ export declare class ModelExchanges {
14
+ #private;
15
+ constructor(options?: ModelExchangesOptions);
16
+ get id(): string;
17
+ get sequence(): number;
18
+ intake(scope: string, operation: ExchangeOperation, reply: ModelReply): ExchangeReference;
19
+ complete(scope: string, reference: ExchangeReference, toolCallId: string, result: ToolResult, operation?: ExchangeOperation): ExchangeReference;
20
+ read(scope: string, value: ExchangeReference, exact?: boolean): ModelExchangeEntry;
21
+ closed(scope: string, references: readonly ExchangeReference[]): readonly ModelExchangeEntry[];
22
+ values(scope?: string): readonly ModelExchangeEntry[];
23
+ get(id: string): ModelExchangeEntry | undefined;
24
+ reference(entry: ModelExchangeEntry): ExchangeReference;
25
+ snapshot(): ModelExchangesState;
26
+ static restore(value: unknown, options?: ModelExchangesOptions): ModelExchanges;
27
+ private mutate;
28
+ private nextSequence;
29
+ private install;
30
+ }
31
+ export declare function requireModelExchangeEntry(value: unknown): ModelExchangeEntry;
@@ -0,0 +1,2 @@
1
+ /*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
2
+ var __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,"name",{value,configurable:!0});import{randomUUID}from"node:crypto";import{isDeepStrictEqual}from"node:util";import{RuntimeError}from"@weiliang0121/capix-contracts";import{PersistenceError}from"@weiliang0121/capix-diagnostics";import{publishFact}from"@weiliang0121/capix-changes";import{requireModelReply,requireToolFeedback,requireToolResult}from"@weiliang0121/capix-model-provider";import{canonical,capture,captureSnapshotEnvelope,counter,encodedBytes,identity,record,requireExchangeOperation,requireExchangeReference}from"./value.js";import{defaultModelExchangeLimits,maximumModelExchangeLimits,maximumModelExchangeEntryBytes}from"./limits.js";class ModelExchanges{static{__name(this,"ModelExchanges")}#id=randomUUID();#sequence=0;#bytes=0;#mutating=!1;#persistenceFailure;#entries=new Map;#operations=new Map;#entryLimit;#byteLimit;#persistence;#changes;#observation;constructor(options={}){this.#entryLimit=limit(options.limits?.entries??defaultModelExchangeLimits.entries,maximumModelExchangeLimits.entries,"entries"),this.#byteLimit=limit(options.limits?.bytes??defaultModelExchangeLimits.bytes,maximumModelExchangeLimits.bytes,"bytes"),this.#persistence=options.persistence,this.#changes=options.changes,this.#observation=options.observation}get id(){return this.#id}get sequence(){return this.#sequence}intake(scope,operation,reply){return this.mutate(()=>{scope=identity(scope),operation=requireExchangeOperation(operation),reply=requireModelReply(capture(reply));const key=canonical(operation),previous=this.#operations.get(key);if(previous){const entry2=this.#entries.get(previous.id);if(previous.callId!==void 0||entry2.scope!==scope||!isDeepStrictEqual(entry2.reply,reply))throw conflict("Intake differs from its fixed operation");return this.reference(entry2)}const sequence=this.nextSequence(),entry=Object.freeze({id:randomUUID(),scope,revision:0,sequence,operation,reply,feedback:Object.freeze([]),receptions:Object.freeze([])});return this.install(entry,null,sequence,{key,id:entry.id}),this.reference(entry)})}complete(scope,reference,toolCallId,result,operation){return this.mutate(()=>{const previous=this.read(scope,reference,!1);if(toolCallId=identity(toolCallId),result=requireToolResult(capture(result)),operation=operation===void 0?void 0:requireExchangeOperation(operation),!previous.reply.output.some(item=>item.kind==="tool-call"&&item.id===toolCallId))throw conflict("Unknown tool feedback slot");const feedback=requireToolFeedback(capture({toolCallId,...result})),existing=previous.feedback.find(item=>item.toolCallId===toolCallId);if(existing){const original=previous.receptions.find(item=>item.callId===toolCallId)?.operation;if(!isDeepStrictEqual(existing,feedback)||!isDeepStrictEqual(original,operation))throw conflict("Feedback differs from its fixed slot");return this.reference(previous)}this.read(scope,reference);const key=operation===void 0?void 0:canonical(operation);if(key&&this.#operations.has(key))throw conflict("Feedback operation already belongs to another effect");const sequence=this.nextSequence(),next=Object.freeze({...previous,revision:previous.revision+1,feedback:Object.freeze([...previous.feedback,feedback]),receptions:operation?Object.freeze([...previous.receptions,Object.freeze({callId:toolCallId,operation})]):previous.receptions});return this.install(next,previous,sequence,key?{key,id:next.id,callId:toolCallId}:void 0,toolCallId),this.reference(next)})}read(scope,value,exact=!0){scope=identity(scope);const reference=requireExchangeReference(value),entry=this.#entries.get(reference.id);if(reference.collection!==this.id||!entry||entry.scope!==scope||reference.revision>entry.revision||exact&&reference.revision!==entry.revision)throw conflict("Exchange reference scope or revision mismatch");return entry}closed(scope,references){if(!Array.isArray(references)||references.length>this.#entryLimit)throw new TypeError("Invalid exchange reference list");const accepted=capture(references);if(!Array.isArray(accepted))throw new TypeError("Invalid exchange reference list");const seen=new Set;return Object.freeze(accepted.map(reference=>{const entry=this.read(scope,requireExchangeReference(reference));if(seen.has(entry.id))throw conflict("Duplicate exchange reference");if(seen.add(entry.id),entry.reply.output.filter(item=>item.kind==="tool-call").length!==entry.feedback.length)throw conflict("Exchange is missing tool feedback");return entry}))}values(scope){return scope!==void 0&&identity(scope),Object.freeze([...this.#entries.values()].filter(entry=>scope===void 0||entry.scope===scope))}get(id){return this.#entries.get(identity(id))}reference(entry){if(this.#entries.get(entry.id)!==entry)throw conflict("Exchange entry is not the current entity value");return Object.freeze({collection:this.id,id:entry.id,revision:entry.revision})}snapshot(){return Object.freeze({formatVersion:1,id:this.id,sequence:this.sequence,entries:this.values()})}static restore(value,options={}){const entity=new ModelExchanges(options),saved=captureSnapshotEnvelope(value,entity.#entryLimit);if(saved.formatVersion!==1)throw new TypeError("Unsupported model exchange snapshot");entity.#id=identity(saved.id),entity.#sequence=counter(saved.sequence);let previousCreated=0,changes=0;for(const value2 of saved.entries){const entry=requireModelExchangeEntry(value2);if(entity.#entries.has(entry.id)||entry.sequence<=previousCreated||entry.sequence>changes+1||entry.sequence+entry.revision>entity.sequence)throw new TypeError("Invalid saved model exchange order");if(previousCreated=entry.sequence,changes+=1+entry.revision,entity.#bytes+=encodedBytes(entry),entity.#entries.size>=entity.#entryLimit||entity.#bytes>entity.#byteLimit)throw new RangeError("Model exchange snapshot exceeds collection limits");for(const[operation,callId]of[[entry.operation,void 0],...entry.receptions.map(item=>[item.operation,item.callId])]){const key=canonical(operation);if(entity.#operations.has(key))throw new TypeError("Duplicate saved model exchange operation");entity.#operations.set(key,{id:entry.id,...callId===void 0?{}:{callId}})}entity.#entries.set(entry.id,entry)}if(changes!==entity.sequence)throw new TypeError("Model exchange sequence does not match its changes");return entity}mutate(action){if(this.#persistenceFailure)throw this.#persistenceFailure;if(this.#mutating)throw new Error("Model exchange persistence or publication cannot reenter mutation");this.#mutating=!0;try{return action()}finally{this.#mutating=!1}}nextSequence(){if(!Number.isSafeInteger(this.#sequence+1))throw new RangeError("Model exchange sequence is exhausted");return this.#sequence+1}install(entry,previous,sequence,operation,toolCallId){const size=encodedBytes(entry),bytes=this.#bytes+size-(previous?encodedBytes(previous):0);if(size>maximumModelExchangeEntryBytes||bytes>this.#byteLimit||!previous&&this.#entries.size>=this.#entryLimit)throw new RangeError("Model exchange collection exceeds capacity");const change=Object.freeze({collection:this.id,expectedSequence:this.sequence,sequence,previous,entry});try{const result=this.#persistence?.commit(change);if(result!==void 0)throw Promise.resolve(result).catch(()=>{}),new TypeError("Model exchange persistence must return undefined synchronously")}catch(cause){throw this.#persistenceFailure=cause instanceof PersistenceError?cause:new PersistenceError("model-exchanges.commit",cause),this.#persistenceFailure}this.#entries.set(entry.id,entry),this.#bytes=bytes,this.#sequence=sequence,operation&&this.#operations.set(operation.key,{id:operation.id,...operation.callId===void 0?{}:{callId:operation.callId}});const event=Object.freeze({type:"model-exchange",collection:this.id,sequence,reference:this.reference(entry),scope:entry.scope,kind:previous?"feedback":"intake",...toolCallId===void 0?{}:{toolCallId}});publishFact(event,this.#changes,this.#observation)}}function requireModelExchangeEntry(value){const item=record(capture(value),["id","scope","revision","sequence","operation","reply","feedback","receptions"]);if(!Array.isArray(item.feedback)||!Array.isArray(item.receptions))throw new TypeError("Invalid saved exchange feedback");const reply=requireModelReply(item.reply),feedback=Object.freeze(item.feedback.map(requireToolFeedback)),calls=new Set(reply.output.filter(entry=>entry.kind==="tool-call").map(entry=>entry.id)),completed=new Set(feedback.map(entry=>entry.toolCallId));if(completed.size!==feedback.length||feedback.some(entry=>!calls.has(entry.toolCallId))||item.revision!==feedback.length)throw new TypeError("Invalid saved exchange slots");const receptions=Object.freeze(item.receptions.map(value2=>{const entry=record(value2,["callId","operation"]),callId=identity(entry.callId);if(!completed.has(callId))throw new TypeError("Saved feedback operation has no corresponding slot");return Object.freeze({callId,operation:requireExchangeOperation(entry.operation)})}));if(new Set(receptions.map(item2=>item2.callId)).size!==receptions.length)throw new TypeError("Duplicate saved feedback operation");const sequence=counter(item.sequence);if(sequence===0)throw new TypeError("Exchange creation sequence must be positive");return Object.freeze({id:identity(item.id),scope:identity(item.scope),revision:counter(item.revision),sequence,operation:requireExchangeOperation(item.operation),reply,feedback,receptions})}__name(requireModelExchangeEntry,"requireModelExchangeEntry");function limit(value,maximum,name){if(!Number.isSafeInteger(value)||value<1||value>maximum)throw new RangeError(`Invalid model exchange ${name} limit`);return value}__name(limit,"limit");function conflict(message){return new RuntimeError("MODEL_EXCHANGE_CONFLICT",message)}__name(conflict,"conflict");export{ModelExchanges,requireModelExchangeEntry};
@@ -0,0 +1,6 @@
1
+ export { ModelExchanges, requireModelExchangeEntry, type ModelExchangesOptions } from './exchanges.js';
2
+ export { defaultModelExchangeLimits, maximumModelExchangeLimits, maximumModelExchangeEntryBytes } from './limits.js';
3
+ export { ModelExchangeResource, type ModelExchangeSupply } from './resource.js';
4
+ export { ModelExchangeViewSource, type ModelExchangeSummary } from './view.js';
5
+ export { requireExchangeReference, requireExchangeOperation, requireExchangeIntakeReceipt, requireExchangeFeedbackReceipt, feedbackDigest } from './value.js';
6
+ export type { ExchangeScope, ExchangeReference, ExchangeOperation, ExchangeIntakeReceipt, ExchangeFeedbackReceipt, ModelExchangeEntry, ModelExchangesState, ModelExchangesCommit, ModelExchangesPersistence, ModelExchangeChange } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ /*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
2
+ import{ModelExchanges,requireModelExchangeEntry}from"./exchanges.js";import{defaultModelExchangeLimits,maximumModelExchangeLimits,maximumModelExchangeEntryBytes}from"./limits.js";import{ModelExchangeResource}from"./resource.js";import{ModelExchangeViewSource}from"./view.js";import{requireExchangeReference,requireExchangeOperation,requireExchangeIntakeReceipt,requireExchangeFeedbackReceipt,feedbackDigest}from"./value.js";export{ModelExchangeResource,ModelExchangeViewSource,ModelExchanges,defaultModelExchangeLimits,feedbackDigest,maximumModelExchangeEntryBytes,maximumModelExchangeLimits,requireExchangeFeedbackReceipt,requireExchangeIntakeReceipt,requireExchangeOperation,requireExchangeReference,requireModelExchangeEntry};
@@ -0,0 +1,9 @@
1
+ export declare const defaultModelExchangeLimits: Readonly<{
2
+ entries: 10000;
3
+ bytes: number;
4
+ }>;
5
+ export declare const maximumModelExchangeLimits: Readonly<{
6
+ entries: 100000;
7
+ bytes: number;
8
+ }>;
9
+ export declare const maximumModelExchangeEntryBytes: number;
package/dist/limits.js ADDED
@@ -0,0 +1,2 @@
1
+ /*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
2
+ const defaultModelExchangeLimits=Object.freeze({entries:1e4,bytes:67108864}),maximumModelExchangeLimits=Object.freeze({entries:1e5,bytes:128*1024*1024}),maximumModelExchangeEntryBytes=8*1024*1024;export{defaultModelExchangeLimits,maximumModelExchangeEntryBytes,maximumModelExchangeLimits};
@@ -0,0 +1,15 @@
1
+ import { ResourceOffer } from '@weiliang0121/capix-resources';
2
+ import type { ModelReply, ToolResult } from '@weiliang0121/capix-model-provider';
3
+ import type { ModelExchanges } from './exchanges.js';
4
+ import type { ExchangeFeedbackReceipt, ExchangeIntakeReceipt, ExchangeReference } from './types.js';
5
+ export interface ModelExchangeSupply {
6
+ readonly exchanges: ModelExchanges;
7
+ readonly scope: string;
8
+ }
9
+ export declare const ModelExchangeResource: ResourceOffer<ModelExchangeSupply, Readonly<{
10
+ intake(reply: ModelReply): ExchangeIntakeReceipt;
11
+ read(reference: ExchangeReference): import("./types.js").ModelExchangeEntry;
12
+ closed(references: readonly ExchangeReference[]): readonly import("./types.js").ModelExchangeEntry[];
13
+ receive(reference: ExchangeReference, callId: string, result: ToolResult): ExchangeFeedbackReceipt;
14
+ complete(reference: ExchangeReference, callId: string, result: ToolResult): ExchangeReference;
15
+ }>, ModelExchangeSupply, never>;
@@ -0,0 +1,2 @@
1
+ /*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
2
+ var __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,"name",{value,configurable:!0});import{ResourceOffer}from"@weiliang0121/capix-resources";import{feedbackDigest,identity,requireExchangeOperation}from"./value.js";const ModelExchangeResource=new ResourceOffer({name:"model-exchanges",timing:"access",expose:__name(({exchanges,scope},activity)=>(scope=identity(scope),Object.freeze({intake(reply){return activity.runSync("intake",{},()=>{const operation=requireExchangeOperation({...activity.reception(),position:"model-reply"});return Object.freeze({reference:exchanges.intake(scope,operation,reply),operation})})},read(reference){return activity.runSync("read",{},()=>exchanges.read(scope,reference,!1))},closed(references){return activity.runSync("closed",{},()=>exchanges.closed(scope,references))},receive(reference,callId,result){return activity.runSync("receive",{},()=>{const operation=requireExchangeOperation({...activity.reception(),position:"tool-feedback"}),next=exchanges.complete(scope,reference,callId,result,operation),feedback=exchanges.read(scope,next).feedback.find(item=>item.toolCallId===callId);return Object.freeze({reference:next,operation,feedbackDigest:feedbackDigest(feedback)})})},complete(reference,callId,result){return activity.runSync("complete",{},()=>exchanges.complete(scope,reference,callId,result))}})),"expose")});export{ModelExchangeResource};
@@ -0,0 +1,57 @@
1
+ import type { ModelReply, ToolFeedback } from '@weiliang0121/capix-model-provider';
2
+ export type ExchangeScope = string;
3
+ export interface ExchangeReference {
4
+ readonly collection: string;
5
+ readonly id: string;
6
+ readonly revision: number;
7
+ }
8
+ export interface ExchangeOperation {
9
+ readonly parentContextId: string;
10
+ readonly childContextId: string;
11
+ readonly position: string;
12
+ }
13
+ export interface ExchangeIntakeReceipt {
14
+ readonly reference: ExchangeReference;
15
+ readonly operation: ExchangeOperation;
16
+ }
17
+ export interface ExchangeFeedbackReceipt extends ExchangeIntakeReceipt {
18
+ readonly feedbackDigest: string;
19
+ }
20
+ export interface ModelExchangeEntry {
21
+ readonly id: string;
22
+ readonly scope: ExchangeScope;
23
+ readonly revision: number;
24
+ readonly sequence: number;
25
+ readonly operation: ExchangeOperation;
26
+ readonly reply: ModelReply;
27
+ readonly feedback: readonly ToolFeedback[];
28
+ readonly receptions: readonly {
29
+ readonly callId: string;
30
+ readonly operation: ExchangeOperation;
31
+ }[];
32
+ }
33
+ export interface ModelExchangesState {
34
+ readonly formatVersion: 1;
35
+ readonly id: string;
36
+ readonly sequence: number;
37
+ readonly entries: readonly ModelExchangeEntry[];
38
+ }
39
+ export interface ModelExchangesCommit {
40
+ readonly collection: string;
41
+ readonly expectedSequence: number;
42
+ readonly sequence: number;
43
+ readonly previous: ModelExchangeEntry | null;
44
+ readonly entry: ModelExchangeEntry;
45
+ }
46
+ export interface ModelExchangesPersistence {
47
+ commit(change: ModelExchangesCommit): undefined;
48
+ }
49
+ export interface ModelExchangeChange {
50
+ readonly type: 'model-exchange';
51
+ readonly collection: string;
52
+ readonly sequence: number;
53
+ readonly reference: ExchangeReference;
54
+ readonly scope: ExchangeScope;
55
+ readonly kind: 'intake' | 'feedback';
56
+ readonly toolCallId?: string;
57
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ /*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
@@ -0,0 +1,21 @@
1
+ import type { ExchangeOperation, ExchangeReference, ExchangeIntakeReceipt, ExchangeFeedbackReceipt } from './types.js';
2
+ export type JsonValue = null | boolean | number | string | readonly JsonValue[] | {
3
+ readonly [name: string]: JsonValue;
4
+ };
5
+ export declare function capture(value: unknown, limit?: number): JsonValue;
6
+ export declare function captureSnapshotEnvelope(value: unknown, maximumEntries: number): Readonly<{
7
+ formatVersion: unknown;
8
+ id: unknown;
9
+ sequence: unknown;
10
+ entries: readonly unknown[];
11
+ }>;
12
+ export declare function encodedBytes(value: unknown): number;
13
+ export declare function canonical(value: unknown): string;
14
+ export declare function record(value: unknown, keys: readonly string[]): Record<string, unknown>;
15
+ export declare function identity(value: unknown): string;
16
+ export declare function counter(value: unknown): number;
17
+ export declare function requireExchangeReference(value: unknown): ExchangeReference;
18
+ export declare function requireExchangeOperation(value: unknown): ExchangeOperation;
19
+ export declare function requireExchangeIntakeReceipt(value: unknown): ExchangeIntakeReceipt;
20
+ export declare function requireExchangeFeedbackReceipt(value: unknown): ExchangeFeedbackReceipt;
21
+ export declare function feedbackDigest(value: unknown): string;
package/dist/value.js ADDED
@@ -0,0 +1,2 @@
1
+ /*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
2
+ var __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,"name",{value,configurable:!0});import{createHash}from"node:crypto";import{isProxy}from"node:util/types";import{requireToolFeedback}from"@weiliang0121/capix-model-provider";import{maximumModelExchangeEntryBytes}from"./limits.js";function capture(value,limit=maximumModelExchangeEntryBytes){let nodes=0,bytes=0;const ancestors=new Set;function visit(item,depth){if(++nodes>2e6||depth>64)throw new TypeError("Model exchange value exceeds structural limits");if(item===null||typeof item=="boolean")return item;if(typeof item=="string"){if(bytes+=Buffer.byteLength(item),bytes>limit)throw new RangeError("Model exchange value exceeds byte limit");return item}if(typeof item=="number"&&Number.isFinite(item))return item;if(typeof item!="object"||isProxy(item)||ancestors.has(item))throw new TypeError("Model exchange requires acyclic JSON data");const array=Array.isArray(item);if(!array&&Object.getPrototypeOf(item)!==Object.prototype&&Object.getPrototypeOf(item)!==null)throw new TypeError("Model exchange requires plain JSON data");if(Object.getOwnPropertySymbols(item).length)throw new TypeError("Model exchange does not accept symbol properties");const result2=array?[]:{};ancestors.add(item);const descriptors=Object.getOwnPropertyDescriptors(item);for(const[key,descriptor]of Object.entries(descriptors))if(!(array&&key==="length")){if(!descriptor.enumerable||!("value"in descriptor))throw new TypeError("Model exchange does not accept accessors or hidden properties");if(array&&(!/^(0|[1-9][0-9]*)$/u.test(key)||Number(key)>=item.length))throw new TypeError("Model exchange array contains extra properties");if(bytes+=Buffer.byteLength(key),bytes>limit)throw new RangeError("Model exchange value exceeds byte limit");Object.defineProperty(result2,key,{value:visit(descriptor.value,depth+1),enumerable:!0})}if(array&&Object.keys(descriptors).length!==item.length+1)throw new TypeError("Model exchange does not accept sparse arrays");return ancestors.delete(item),Object.freeze(result2)}__name(visit,"visit");const result=visit(value,0);if(encodedBytes(result)>limit)throw new RangeError("Model exchange value exceeds byte limit");return result}__name(capture,"capture");function captureSnapshotEnvelope(value,maximumEntries){if(value===null||typeof value!="object"||isProxy(value)||Array.isArray(value)||Object.getPrototypeOf(value)!==Object.prototype&&Object.getPrototypeOf(value)!==null)throw new TypeError("Invalid model exchange snapshot object");const fields=Object.getOwnPropertyDescriptors(value),names=["formatVersion","id","sequence","entries"];if(Reflect.ownKeys(fields).length!==names.length||names.some(name=>!Object.hasOwn(fields,name)||!fields[name].enumerable||!("value"in fields[name])))throw new TypeError("Invalid model exchange snapshot fields");const source=fields.entries.value;if(!Array.isArray(source)||isProxy(source)||Object.getPrototypeOf(source)!==Array.prototype)throw new TypeError("Invalid model exchange entries array");if(source.length>maximumEntries)throw new RangeError("Model exchange snapshot exceeds collection limits");if(Reflect.ownKeys(source).length!==source.length+1)throw new TypeError("Model exchange entries must be a dense data array");const entries=[];for(let index=0;index<source.length;index++){const field=Object.getOwnPropertyDescriptor(source,String(index));if(!field||!field.enumerable||!("value"in field))throw new TypeError("Model exchange entries must be a dense data array");entries.push(field.value)}return Object.freeze({formatVersion:fields.formatVersion.value,id:fields.id.value,sequence:fields.sequence.value,entries:Object.freeze(entries)})}__name(captureSnapshotEnvelope,"captureSnapshotEnvelope");function encodedBytes(value){return Buffer.byteLength(JSON.stringify(value))}__name(encodedBytes,"encodedBytes");function canonical(value){const accepted=capture(value);function encode(item){return Array.isArray(item)?`[${item.map(encode).join(",")}]`:item!==null&&typeof item=="object"?`{${Object.keys(item).sort().map(key=>`${JSON.stringify(key)}:${encode(item[key])}`).join(",")}}`:JSON.stringify(item)}return __name(encode,"encode"),encode(accepted)}__name(canonical,"canonical");function record(value,keys){if(value===null||typeof value!="object"||Array.isArray(value)||Object.keys(value).some(key=>!keys.includes(key)))throw new TypeError("Invalid model exchange object");return value}__name(record,"record");function identity(value){if(typeof value!="string"||!value.trim()||value.length>512)throw new TypeError("Invalid model exchange identity");return value}__name(identity,"identity");function counter(value){if(typeof value!="number"||!Number.isSafeInteger(value)||value<0)throw new TypeError("Invalid model exchange revision");return value}__name(counter,"counter");function requireExchangeReference(value){const item=record(capture(value),["collection","id","revision"]);return Object.freeze({collection:identity(item.collection),id:identity(item.id),revision:counter(item.revision)})}__name(requireExchangeReference,"requireExchangeReference");function requireExchangeOperation(value){const item=record(capture(value),["parentContextId","childContextId","position"]);return Object.freeze({parentContextId:identity(item.parentContextId),childContextId:identity(item.childContextId),position:identity(item.position)})}__name(requireExchangeOperation,"requireExchangeOperation");function requireExchangeIntakeReceipt(value){const item=record(capture(value),["reference","operation"]);return Object.freeze({reference:requireExchangeReference(item.reference),operation:requireExchangeOperation(item.operation)})}__name(requireExchangeIntakeReceipt,"requireExchangeIntakeReceipt");function requireExchangeFeedbackReceipt(value){const item=record(capture(value),["reference","operation","feedbackDigest"]);if(typeof item.feedbackDigest!="string"||!/^[a-f0-9]{64}$/u.test(item.feedbackDigest))throw new TypeError("Invalid model exchange feedback digest");return Object.freeze({reference:requireExchangeReference(item.reference),operation:requireExchangeOperation(item.operation),feedbackDigest:item.feedbackDigest})}__name(requireExchangeFeedbackReceipt,"requireExchangeFeedbackReceipt");function feedbackDigest(value){return createHash("sha256").update(canonical(requireToolFeedback(capture(value)))).digest("hex")}__name(feedbackDigest,"feedbackDigest");export{canonical,capture,captureSnapshotEnvelope,counter,encodedBytes,feedbackDigest,identity,record,requireExchangeFeedbackReceipt,requireExchangeIntakeReceipt,requireExchangeOperation,requireExchangeReference};
package/dist/view.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import type { SourceStamp, SyncViewSource } from '@weiliang0121/capix-view';
2
+ import type { ModelExchanges } from './exchanges.js';
3
+ import type { ModelExchangeEntry } from './types.js';
4
+ export interface ModelExchangeSummary {
5
+ readonly model: string;
6
+ readonly replyId: string;
7
+ readonly toolCalls: number;
8
+ readonly completed: number;
9
+ }
10
+ type Source = SyncViewSource<string, ModelExchangeSummary, ModelExchangeEntry>;
11
+ export declare class ModelExchangeViewSource implements Source {
12
+ #private;
13
+ readonly id: string;
14
+ readonly scope: string;
15
+ constructor(exchanges: ModelExchanges, scope?: string);
16
+ index(): ReturnType<Source['index']>;
17
+ inspect(keys: readonly string[]): ReturnType<Source['inspect']>;
18
+ read(keys: readonly string[], expected: SourceStamp): ReturnType<Source['read']>;
19
+ check(expected: SourceStamp): boolean;
20
+ private stamp;
21
+ private requireCurrent;
22
+ private select;
23
+ }
24
+ export {};
package/dist/view.js ADDED
@@ -0,0 +1,2 @@
1
+ /*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
2
+ var __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,"name",{value,configurable:!0});import{RuntimeError}from"@weiliang0121/capix-contracts";import{capture,identity,record}from"./value.js";class ModelExchangeViewSource{static{__name(this,"ModelExchangeViewSource")}id;scope;#exchanges;#group;constructor(exchanges,scope){this.#exchanges=exchanges,this.#group=scope===void 0?void 0:identity(scope),this.id=exchanges.id,this.scope=scope===void 0?"whole":`scope:${JSON.stringify(scope)}`,Object.freeze(this)}index(){const stamp=this.stamp(),entries=Object.freeze(this.#exchanges.values(this.#group).map(entry=>Object.freeze({key:entry.id,summary:summarize(entry)})));return this.requireCurrent(stamp),Object.freeze({stamp,entries})}inspect(keys){const stamp=this.stamp(),entries=Object.freeze(this.select(keys).map(entry=>Object.freeze({key:entry.id,summary:summarize(entry)})));return this.requireCurrent(stamp),Object.freeze({stamp,entries})}read(keys,expected){const value=record(capture(expected),["id","scope","version"]);if(typeof value.id!="string"||typeof value.scope!="string"||typeof value.version!="string")throw new TypeError("Invalid model exchange source stamp");const basis=Object.freeze({id:value.id,scope:value.scope,version:value.version});this.requireCurrent(basis);const entries=Object.freeze(this.select(keys).map(entry=>Object.freeze({key:entry.id,value:entry})));return this.requireCurrent(basis),Object.freeze({stamp:this.stamp(),entries})}check(expected){return expected?.id===this.id&&expected.scope===this.scope&&expected.version===String(this.#exchanges.sequence)}stamp(){return Object.freeze({id:this.id,scope:this.scope,version:String(this.#exchanges.sequence)})}requireCurrent(expected){if(!this.check(expected))throw new RuntimeError("VIEW_SOURCE_CONFLICT","Model exchange view source has changed")}select(keys){const selected=capture(keys);if(!Array.isArray(selected)||selected.some(key=>typeof key!="string"||!key)||new Set(selected).size!==selected.length)throw new TypeError("Model exchange view requires unique string keys");return selected.map(key=>{const entry=this.#exchanges.get(identity(key));if(!entry||this.#group!==void 0&&entry.scope!==this.#group)throw new RangeError("Exchange is outside the view source scope");return entry})}}function summarize(entry){return Object.freeze({model:entry.reply.model,replyId:entry.reply.id,toolCalls:entry.reply.output.filter(item=>item.kind==="tool-call").length,completed:entry.feedback.length})}__name(summarize,"summarize");export{ModelExchangeViewSource};
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
2
  "name": "@weiliang0121/capix-model-exchanges",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ }
11
+ },
12
+ "dependencies": {
13
+ "@weiliang0121/capix-contracts": "0.1.0",
14
+ "@weiliang0121/capix-changes": "0.1.0",
15
+ "@weiliang0121/capix-diagnostics": "0.1.0",
16
+ "@weiliang0121/capix-model-provider": "0.1.0",
17
+ "@weiliang0121/capix-view": "0.1.0",
18
+ "@weiliang0121/capix-resources": "0.1.0"
19
+ },
20
+ "description": "Model reply and tool feedback entities for CAPiX.",
21
+ "license": "MIT",
22
+ "author": "wei.liang (https://github.com/weiliang0121)",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/weiliang0121/capix.git",
26
+ "directory": "server/model-exchanges"
27
+ },
28
+ "engines": {
29
+ "node": ">=22.23.2"
30
+ },
31
+ "main": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "files": [
34
+ "dist/**/*.js",
35
+ "dist/**/*.d.ts",
36
+ "LICENSE",
37
+ "README.md"
38
+ ],
39
+ "publishConfig": {
40
+ "access": "public",
41
+ "registry": "https://registry.npmjs.org"
42
+ },
43
+ "scripts": {
44
+ "build": "tsc -p tsconfig.json",
45
+ "typecheck": "tsc -p tsconfig.json --noEmit"
46
+ }
6
47
  }