@weiliang0121/capix-tool 0.2.0 → 0.3.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/README.md CHANGED
@@ -61,7 +61,11 @@ const text = await runner.run({ request: { messages: [{ role: 'user', content: '
61
61
 
62
62
  ## 预算与等待
63
63
 
64
- 首版工具串行执行,1–32 个登记成员;`maxModelCalls` 默认 8,允许 1–32;`waitMs` 默认 30000。预算按完整 JSON 的 UTF-8 字节计量。
64
+ 工具串行执行,1–32 个登记成员;`maxModelCalls` 默认 1000,接受正安全整数;`waitMs` 默认 30000。调用次数用于限制单次 Tool 调用的持续推进,作为异常循环的兜底,作者也可按业务要求显式设置更小或更大的值。次数包含最后一次生成回答的模型请求;一次请求可以提出多个工具调用,因此不等于工具执行次数。
65
+
66
+ 已达到 `maxModelCalls` 时,不再启动下一次模型子调用,并抛出公开的 `ToolModelCallLimitError`(`code: 'TOOL_MODEL_CALL_LIMIT'`,`calls` 为已启动的模型调用数,`limit` 为配置上限)。最后一次允许的请求若给出最终回答,可正常完成;若提出工具,则完成这些工具的处理,再在下一次模型请求前检查次数。已有工具效果不会撤销。宿主可据错误类型明确展示次数耗尽,不应将其当作模型服务故障或自动重试。
67
+
68
+ 次数按同一调用累计,显式恢复不会重置。它仍属于目录的固定配置:上限改变会拒绝原 CP;此前省略配置的调用使用默认 8,与现在的默认 1000 不同。提高次数不扩大上下文、实体、持久材料或时间容量,也不提供累计 token 额度;需要 token 或费用策略时由宿主独立计量。下列容量按完整 JSON 的 UTF-8 字节计量。
65
69
 
66
70
  | 配置 | 默认值 | 检查位置 |
67
71
  | ----------------------------- | ------ | --------------------------------------------------- |
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { Tool } from './tool.js';
1
+ export { Tool, ToolModelCallLimitError } from './tool.js';
2
2
  export type { ToolMember, ToolMembers, ToolConfiguration, ToolInput, ToolBaseRequest } from './types.js';
3
3
  export type { ToolCatalog } from './catalog.js';
4
4
  export { ToolModelResource, ToolModelPending, type ToolModelSource, type ToolInteractionRecovery } from './model.js';
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
2
- import{Tool}from"./tool.js";import{ToolModelResource,ToolModelPending}from"./model.js";import{ToolBudgetError,defaultToolByteBudgets,assertToolOutputBytes}from"./budget.js";export{Tool,ToolBudgetError,ToolModelPending,ToolModelResource,assertToolOutputBytes,defaultToolByteBudgets};
2
+ import{Tool,ToolModelCallLimitError}from"./tool.js";import{ToolModelResource,ToolModelPending}from"./model.js";import{ToolBudgetError,defaultToolByteBudgets,assertToolOutputBytes}from"./budget.js";export{Tool,ToolBudgetError,ToolModelCallLimitError,ToolModelPending,ToolModelResource,assertToolOutputBytes,defaultToolByteBudgets};
package/dist/tool.d.ts CHANGED
@@ -39,6 +39,12 @@ declare const deps: {
39
39
  }>>;
40
40
  };
41
41
  type ResourceValues = ResourcesOf<typeof deps>;
42
+ export declare class ToolModelCallLimitError extends Error {
43
+ readonly calls: number;
44
+ readonly limit: number;
45
+ readonly code = "TOOL_MODEL_CALL_LIMIT";
46
+ constructor(calls: number, limit: number);
47
+ }
42
48
  export declare class Tool<const Definitions extends Readonly<Record<string, ChildCallable>>> extends Group<ToolInput, string, ResourceValues> {
43
49
  readonly run: NonNullable<Group<ToolInput, string, ResourceValues>['run']>;
44
50
  readonly tools: ToolMembers<Definitions>;
package/dist/tool.js CHANGED
@@ -1,2 +1,2 @@
1
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{types}from"node:util";import{isDeepStrictEqual as equal}from"node:util";import{z}from"zod";import{cloneCheckpointValue}from"@weiliang0121/capix-checkpoint";import{modelReplyText,requireModelRequest}from"@weiliang0121/capix-model-provider";import{ModelExchangeResource,feedbackDigest,requireExchangeIntakeReceipt,requireExchangeFeedbackReceipt}from"@weiliang0121/capix-model-exchanges";import{Group,checkpointPoint,childReferences,reception}from"@weiliang0121/capix-structure";import{codec,stringCodec}from"./codec.js";import{createToolCompletion,ToolModelResource}from"./model.js";import{createToolCatalog,selectTool,toolEntries}from"./catalog.js";import{assertToolFeedbackBytes,assertToolReplyBytes,assertToolOutputBytes,resolveToolBudgets,ToolBudgetError}from"./budget.js";const refSchema=z.strictObject({collection:z.string().min(1),id:z.string().min(1),revision:z.number().int().nonnegative()}).readonly();function inputCodecFor(catalog){return codec(z.strictObject({request:z.custom(value=>value===null||typeof value!="object"||"tools"in value||"exchanges"in value?!1:(requireModelRequest({...value,tools:catalog.tools}),!0))}).readonly())}__name(inputCodecFor,"inputCodecFor");const preparedCodec=codec(z.strictObject({id:z.string().min(1),catalog:z.string().min(1)}).readonly()),originSchema=z.strictObject({collection:z.string().min(1),id:z.string().min(1),digest:z.string().min(1)}).readonly(),deps={exchanges:ModelExchangeResource.request(),model:ToolModelResource.request()},stateCodec=codec(z.strictObject({id:z.string().min(1),catalog:z.string().min(1),origin:originSchema,exchanges:z.array(refSchema).readonly(),current:refSchema.nullable(),cursor:z.number().int().nonnegative(),active:z.strictObject({name:z.string(),index:z.number().int().nonnegative(),callId:z.string()}).readonly().nullable()}).readonly()),intakeCodec={check:requireExchangeIntakeReceipt,encode:__name(value=>cloneCheckpointValue(requireExchangeIntakeReceipt(value)),"encode"),decode:requireExchangeIntakeReceipt},feedbackCodec={check:requireExchangeFeedbackReceipt,encode:__name(value=>cloneCheckpointValue(requireExchangeFeedbackReceipt(value)),"encode"),decode:requireExchangeFeedbackReceipt};function refsAt(refs,name){const value=refs[name];if(!value)throw new Error("Tool checkpoint references an unknown member");return value}__name(refsAt,"refsAt");function referenceAt(refs,name,index){const value=refsAt(refs,name).at(index);if(!value)throw new Error("Tool checkpoint references a missing child");return value}__name(referenceAt,"referenceAt");class Tool extends Group{static{__name(this,"Tool")}tools;catalog;maxModelCalls;waitMs;recoverable;budgets;stages;checkToolCall;constructor(configuration){const options=snapshotConfiguration(configuration);if(super({name:"Tool",deps,output:{result:__name(value=>{if(typeof value!="string"||!value.trim())throw new TypeError("Tool final answer must be nonempty");return value},"result")}}),this.maxModelCalls=options.maxModelCalls===void 0?8:options.maxModelCalls,this.waitMs=options.waitMs===void 0?3e4:options.waitMs,this.recoverable=options.checkpoint===!0,this.budgets=resolveToolBudgets(options.budgets),!Number.isSafeInteger(this.maxModelCalls)||this.maxModelCalls<1||this.maxModelCalls>32)throw new RangeError("Tool model budget must be within 1..32");if(!Number.isSafeInteger(this.waitMs)||this.waitMs<0||this.waitMs>2147483647)throw new RangeError("Tool wait must be a nonnegative timer duration");const entries=toolEntries(options.tools,this.recoverable);this.tools=Object.freeze(Object.fromEntries(entries.map(entry=>[entry.name,entry.registration]))),Object.defineProperty(this,"tools",{value:this.tools,enumerable:!0,writable:!1,configurable:!1}),this.checkToolCall=call=>selectTool(entries,call),Object.defineProperty(this,"checkToolCall",{value:this.checkToolCall,enumerable:!1,writable:!1,configurable:!1});const catalog=this.catalog=createToolCatalog(entries,options.policyVersion,{maxModelCalls:this.maxModelCalls,waitMs:this.waitMs,budgets:this.budgets}),inputCodec=inputCodecFor(catalog);for(const name of["catalog","maxModelCalls","waitMs","recoverable","budgets"])Object.defineProperty(this,name,{value:this[name],enumerable:!0,writable:!1,configurable:!1});const budgets=this.budgets,completion=createToolCompletion(this.recoverable,catalog,budgets,this.waitMs);this.stages=Object.freeze({model:completion.id,tools:Object.freeze(entries.map(entry=>Object.freeze({structureId:entry.registration.node.id,name:entry.name})))}),Object.defineProperty(this,"stages",{value:this.stages,enumerable:!0,writable:!1,configurable:!1});const tools=new Map(entries.map(entry=>[entry.name,entry])),modelReception=reception(completion,intakeCodec),toolReceptions=new Map(entries.map(entry=>[entry.name,reception(entry.registration.node,feedbackCodec)])),slots=Object.freeze(Object.fromEntries([["models",childReferences(completion)],...entries.map(entry=>[entry.name,childReferences(entry.registration.node)])])),members=Object.freeze([completion,...entries.map(entry=>entry.registration.node)]),maxModelCalls=this.maxModelCalls,recoverable=this.recoverable,point=checkpointPoint(),prepared=point({name:"prepared",state:preparedCodec,references:slots,check:__name((state,facts)=>{if(state.catalog!==catalog.digest||facts.children.values().length||Object.values(facts.references).some(refs=>refs.length))throw new Error("Prepared Tool configuration or child relations changed")},"check"),resume:{deps,uses:members,run:__name((work,state,saved)=>prepare(work,state,selections(work,saved)),"run")}}),waiting=point({name:"model-waiting",state:stateCodec,references:slots,check,resume:{deps,uses:members,run:resumeModel}}),processing=point({name:"tools",state:stateCodec,references:slots,check,resume:{deps,uses:members,run:resumeTools}}),completed=point({name:"completed",state:stateCodec,references:slots,check,resume:{deps,uses:members,run:__name(async(work,state,refs)=>(await confirmExchanges(work,state,refs),finalText(work,state)),"run")}});recoverable&&(this.checkpoint={key:"capix.tool",version:1,input:inputCodec,output:stringCodec,receptions:[modelReception,...toolReceptions.values()],points:[prepared,waiting,processing,completed]});const save=__name((work,name,state,refs)=>work.checkpoint?.({point:name==="waiting"?waiting:name==="processing"?processing:completed,state,refs}),"save"),selections=__name((work,saved)=>Object.fromEntries(Object.entries(slots).map(([name,slot])=>[name,work.checkpoint?work.checkpoint.select(slot.definition,saved?.[name]):saved?.[name]??[]])),"selections"),append=__name((refs,name,reference)=>{const selected=refsAt(refs,name);return"add"in selected?(selected.add(reference),refs):{...refs,[name]:[...selected,reference]}},"append"),toolReception=__name(name=>{const value=toolReceptions.get(name);if(!value)throw new Error("Tool reception is not registered");return value},"toolReception"),modelFact=__name((facts,ref)=>facts.require(completion,ref),"modelFact");function check(state,facts){if(state.catalog!==catalog.digest||state.id!==state.origin.id)throw new Error("Tool fixed catalog or budgets changed");const models=facts.references.models;if(!models||facts.children.values().length!==Object.values(facts.references).reduce((sum,values)=>sum+values.length,0))throw new Error("Tool checkpoint omitted child relations");if(models.length!==state.exchanges.length+1||models.length>maxModelCalls)throw new Error("Tool model prefix changed");if(models.forEach((fact,index)=>{const child2=modelFact(facts.children,fact.ref);if(!equal(child2.input,{origin:state.origin,exchanges:state.exchanges.slice(0,index)}))throw new Error("Model input differs from fixed exchange prefix");const expected=state.exchanges[index];if(expected){const receipt=facts.children.accepted(child2.ref,modelReception);if(receipt.reference.collection!==expected.collection||receipt.reference.id!==expected.id)throw new Error("Model reception differs from exchange prefix")}}),!state.current){if(state.active||state.cursor!==0)throw new Error("Waiting model has tool progress");return}const latest=models.at(-1);if(!latest)throw new Error("Current exchange lacks its model");const child=modelFact(facts.children,latest.ref),received=facts.children.accepted(child.ref,modelReception);if(state.current.collection!==received.reference.collection||state.current.id!==received.reference.id||child.returned?.status!=="succeeded")throw new Error("Current exchange differs from accepted model");const calls=child.returned.output.reply.output.filter(item=>item.kind==="tool-call");if(state.cursor>calls.length)throw new Error("Tool cursor exceeds model requests");for(let index=0;index<state.cursor;index++){const call=calls[index],selected=selectTool(entries,call);if(selected.kind==="rejected")continue;const found=facts.references[selected.entry.name]?.find(fact=>fact.order>child.order&&fact.key===call.id&&equal(fact.input,selected.input));if(!found)throw new Error("Processed tool has no exact child");const receipt=found.reception.status==="accepted"?facts.children.accepted(found.ref,toolReception(selected.entry.name)):facts.children.handled(found.ref,toolReception(selected.entry.name));if(receipt.reference.collection!==state.current.collection||receipt.reference.id!==state.current.id)throw new Error("Tool receipt belongs to another exchange")}if(state.active){const call=calls[state.cursor],relation=facts.references[state.active.name]?.[state.active.index],selected=call&&selectTool(entries,call);if(!call||selected?.kind!=="accepted"||selected.entry.name!==state.active.name||call.id!==state.active.callId||!relation||relation.order<=child.order||relation.key!==call.id||!equal(relation.input,selected.input))throw new Error("Active tool differs from fixed model request")}}__name(check,"check");async function receiveModel(work,state,refs,child){const result={succeeded:__name(({reply})=>work.resources.exchanges.intake(state.origin,assertToolReplyBytes(reply,budgets)),"succeeded")},received=recoverable?await child.invoke({reception:modelReception,result}):await child.invoke({result}),next={...state,current:received.reference,cursor:0,active:null};return save(work,"processing",next,refs),processTools(work,next,refs)}__name(receiveModel,"receiveModel");function startModel(work,state,refs){const models=refsAt(refs,"models");if(models.length>=maxModelCalls)throw new Error("Tool model call budget exhausted");verifyOrigin(work,state);const child=work.children.create(completion,{origin:state.origin,exchanges:state.exchanges},{key:`model:${models.length}`}),nextRefs=append(refs,"models",child.ref);return save(work,"waiting",state,nextRefs),receiveModel(work,state,nextRefs,child)}__name(startModel,"startModel");async function resumeModel(work,state,saved){const refs=selections(work,saved);await confirmExchanges(work,state,refs);const child=modelFact(work.children.facts,referenceAt(refs,"models",refsAt(refs,"models").length-1));if(child.reception.status==="accepted"){const receipt=work.children.facts.accepted(child.ref,modelReception);return processTools(work,{...state,current:receipt.reference,cursor:0,active:null},refs)}return receiveModel(work,state,refs,work.children.resume(child.ref))}__name(resumeModel,"resumeModel");async function receiveTool(work,state,refs,child){if(!state.current||!state.active)throw new Error("Missing active tool state");const active=state.active,reference=state.current,entry=tools.get(active.name);if(!entry)throw new Error("Tool is not registered");const result={succeeded:__name(async value=>{entry.registration.maxOutputBytes!==void 0&&assertToolOutputBytes(value,entry.registration.maxOutputBytes);const mapped=assertToolFeedbackBytes(active.callId,await entry.registration.result.succeeded(value),budgets);return work.resources.exchanges.receive(state.origin,reference,active.callId,mapped)},"succeeded"),...entry.registration.result.failed?{failed:__name(async failure=>{if(failure instanceof ToolBudgetError)throw failure;return work.resources.exchanges.receive(state.origin,reference,active.callId,assertToolFeedbackBytes(active.callId,await entry.registration.result.failed(failure),budgets))},"failed")}:{}},receipt=recoverable?await child.invoke({reception:toolReception(active.name),result}):await child.invoke({result}),next={...state,current:receipt.reference,cursor:state.cursor+1,active:null};return save(work,"processing",next,refs),processTools(work,next,refs)}__name(receiveTool,"receiveTool");async function resumeTools(work,state,saved){const refs=selections(work,saved);if(await confirmExchanges(work,state,refs),!state.active)return processTools(work,state,refs);const ref=referenceAt(refs,state.active.name,state.active.index),fact=work.children.facts.get(ref);if(fact.reception.status==="accepted"||fact.reception.status==="handled"){const receipt=fact.reception.status==="accepted"?work.children.facts.accepted(ref,toolReception(state.active.name)):work.children.facts.handled(ref,toolReception(state.active.name));return processTools(work,{...state,current:receipt.reference,cursor:state.cursor+1,active:null},refs)}return receiveTool(work,state,refs,work.children.resume(ref))}__name(resumeTools,"resumeTools");async function confirmExchanges(work,state,refs){verifyOrigin(work,state);const modelRefs=Array.from(refsAt(refs,"models"));for(const[index,ref]of modelRefs.entries()){const fact=modelFact(work.children.facts,ref);if(fact.reception.status!=="accepted")continue;const receipt=work.children.facts.accepted(fact.ref,modelReception),entry=work.resources.exchanges.read(state.origin,receipt.reference,!1);if(assertToolReplyBytes(entry.reply,budgets),fact.returned?.status!=="succeeded"||!equal(entry.reply,fact.returned.output.reply)||!equal(entry.operation,receipt.operation))throw new Error("Exchange differs from original model return or reception");await work.resources.model.confirm({reference:fact.returned.output.reference,basis:fact.input,reply:fact.returned.output.reply,budgets});const next=modelRefs[index+1],end=next?work.children.facts.get(next).order:1/0;for(const feedback of entry.feedback){assertToolFeedbackBytes(feedback.toolCallId,{status:feedback.status,content:feedback.content},budgets);const call=entry.reply.output.find(item=>item.kind==="tool-call"&&item.id===feedback.toolCallId);if(!call||call.kind!=="tool-call")throw new Error("Feedback has no original model call");const selected=selectTool(entries,call);if(selected.kind==="rejected"){if(!equal(feedback,{toolCallId:call.id,...selected.result})||entry.receptions.some(receipt2=>receipt2.callId===call.id))throw new Error("Precheck feedback differs from fixed policy");continue}const child=Array.from(refsAt(refs,selected.entry.name)).map(ref2=>work.children.facts.get(ref2)).find(child2=>child2.order>fact.order&&child2.order<end&&child2.key===call.id&&equal(child2.input,selected.input));if(!child)throw new Error("Feedback has no exact tool child");if(child.returned?.status==="succeeded"&&selected.entry.registration.maxOutputBytes!==void 0&&assertToolOutputBytes(child.returned.output,selected.entry.registration.maxOutputBytes),child.reception.status!=="accepted"&&child.reception.status!=="handled")continue;const prior=child.reception.status==="accepted"?work.children.facts.accepted(child.ref,toolReception(selected.entry.name)):work.children.facts.handled(child.ref,toolReception(selected.entry.name));if(prior.reference.collection!==receipt.reference.collection||prior.reference.id!==receipt.reference.id||prior.feedbackDigest!==feedbackDigest(feedback)||!equal(prior.operation,entry.receptions.find(item=>item.callId===call.id)?.operation))throw new Error("Feedback differs from original tool reception")}}for(const entry of entries)for(const ref of refsAt(refs,entry.name)){const child=work.children.facts.require(entry.registration.node,ref);if(child.reception.status!=="accepted"&&child.reception.status!=="handled")continue;const receipt=child.reception.status==="accepted"?work.children.facts.accepted(ref,toolReception(entry.name)):work.children.facts.handled(ref,toolReception(entry.name)),exchange=work.resources.exchanges.read(state.origin,receipt.reference,!1),feedback=exchange.feedback.find(feedback2=>feedback2.toolCallId===child.key);if(!feedback||receipt.feedbackDigest!==feedbackDigest(feedback)||!equal(receipt.operation,exchange.receptions.find(reception2=>reception2.callId===child.key)?.operation))throw new Error("Saved tool reception has no exact exchange feedback")}}__name(confirmExchanges,"confirmExchanges");function finalText(work,state){if(!state.current)throw new Error("Missing final model exchange");const reply=work.resources.exchanges.read(state.origin,state.current,!1).reply;if(reply.output.some(item=>item.kind==="tool-call"))throw new Error("Tool final reply still contains tool calls");if(reply.termination.kind!=="completed")throw new Error("Incomplete model reply cannot finish Tool");const text=modelReplyText(reply);if(!text.trim())throw new Error("Tool final answer is empty");return text}__name(finalText,"finalText");async function processTools(work,initial,refs){let state=initial;if(!state.current)throw new Error("Missing model exchange");verifyOrigin(work,state);const entry=work.resources.exchanges.read(state.origin,state.current,!1);if(entry.reply.termination.kind!=="completed")throw new Error("Incomplete model reply cannot execute tools");const calls=entry.reply.output.filter(item=>item.kind==="tool-call");if(!calls.length){const value=finalText(work,state);return save(work,"completed",state,refs),value}for(await work.resources.model.preflight({origin:state.origin,exchanges:state.exchanges,current:state.current,budgets});state.cursor<calls.length;){const call=calls[state.cursor],selected=selectTool(entries,call);if(selected.kind==="rejected"){const current=work.resources.exchanges.complete(state.origin,state.current,call.id,assertToolFeedbackBytes(call.id,selected.result,budgets));state={...state,current,cursor:state.cursor+1},save(work,"processing",state,refs);continue}const name=selected.entry.name,child=work.children.create(selected.entry.registration.node,selected.input,{key:call.id}),currentRefs=refsAt(refs,name),active={name,index:currentRefs.length,callId:call.id},nextRefs=append(refs,name,child.ref),next={...state,active};return save(work,"processing",next,nextRefs),receiveTool(work,next,nextRefs,child)}const exchanges=[...state.exchanges,state.current];return work.resources.exchanges.closed(state.origin,exchanges),startModel(work,{...state,exchanges,current:null,cursor:0,active:null},refs)}__name(processTools,"processTools");function prepare(work,state,refs){if(state.catalog!==catalog.digest)throw new Error("Tool fixed catalog or policy changed");const origin=work.resources.exchanges.begin(state.id,{request:work.input.request,catalog});return startModel(work,{...state,origin,exchanges:[],current:null,cursor:0,active:null},refs)}__name(prepare,"prepare");function verifyOrigin(work,state){const origin=work.resources.exchanges.origin(state.origin);if(state.catalog!==catalog.digest||state.id!==origin.id||!equal(origin.request,work.input.request)||!equal(origin.catalog,catalog))throw new Error("Tool fixed origin differs from its input or execution catalog");work.resources.exchanges.closed(state.origin,state.exchanges)}__name(verifyOrigin,"verifyOrigin");const run=__name(work=>{inputCodec.encode(work.input);const state={id:randomUUID(),catalog:catalog.digest},refs=selections(work);return work.checkpoint?.({point:prepared,state,refs}),prepare(work,state,refs)},"run");Object.defineProperty(this,"run",{get:__name(()=>run,"get"),enumerable:!0,configurable:!0}),this.add(...members)}}function snapshotConfiguration(value){if(!value||typeof value!="object"||Array.isArray(value)||types.isProxy(value))throw new TypeError("Tool configuration must be a data object");const descriptors=Object.getOwnPropertyDescriptors(value);if(Object.getOwnPropertySymbols(value).length||Object.keys(descriptors).some(key=>!["tools","policyVersion","checkpoint","maxModelCalls","waitMs","budgets"].includes(key)))throw new TypeError("Unknown Tool configuration field");const result={};for(const key of["tools","policyVersion","checkpoint","maxModelCalls","waitMs","budgets"]){const field=Object.getOwnPropertyDescriptor(value,key);if(!field&&key in value||field&&!("value"in field))throw new TypeError("Tool configuration must use own data properties");if(field&&!field.enumerable)throw new TypeError("Tool configuration fields must be enumerable");field&&(result[key]=field.value)}if(result.checkpoint!==void 0&&typeof result.checkpoint!="boolean")throw new TypeError("Tool checkpoint must be boolean");if(result.budgets!==void 0){const budgets=result.budgets;if(!budgets||typeof budgets!="object"||Array.isArray(budgets)||types.isProxy(budgets)||Object.getOwnPropertySymbols(budgets).length)throw new TypeError("Tool budgets must be a data object");const snapshot={};for(const[key,descriptor]of Object.entries(Object.getOwnPropertyDescriptors(budgets))){if(!["maxReplyBytes","maxFeedbackBytes","maxRequestBytes","maxWireRequestBytes","maxCollectionBytes"].includes(key)||!descriptor.enumerable||!("value"in descriptor))throw new TypeError("Tool budgets must contain known data properties");snapshot[key]=descriptor.value}result.budgets=Object.freeze(snapshot)}return Object.freeze(result)}__name(snapshotConfiguration,"snapshotConfiguration");export{Tool};
2
+ var __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,"name",{value,configurable:!0});import{randomUUID}from"node:crypto";import{types}from"node:util";import{isDeepStrictEqual as equal}from"node:util";import{z}from"zod";import{cloneCheckpointValue}from"@weiliang0121/capix-checkpoint";import{modelReplyText,requireModelRequest}from"@weiliang0121/capix-model-provider";import{ModelExchangeResource,feedbackDigest,requireExchangeIntakeReceipt,requireExchangeFeedbackReceipt}from"@weiliang0121/capix-model-exchanges";import{Group,checkpointPoint,childReferences,reception}from"@weiliang0121/capix-structure";import{codec,stringCodec}from"./codec.js";import{createToolCompletion,ToolModelResource}from"./model.js";import{createToolCatalog,selectTool,toolEntries}from"./catalog.js";import{assertToolFeedbackBytes,assertToolReplyBytes,assertToolOutputBytes,resolveToolBudgets,ToolBudgetError}from"./budget.js";const refSchema=z.strictObject({collection:z.string().min(1),id:z.string().min(1),revision:z.number().int().nonnegative()}).readonly();function inputCodecFor(catalog){return codec(z.strictObject({request:z.custom(value=>value===null||typeof value!="object"||"tools"in value||"exchanges"in value?!1:(requireModelRequest({...value,tools:catalog.tools}),!0))}).readonly())}__name(inputCodecFor,"inputCodecFor");const preparedCodec=codec(z.strictObject({id:z.string().min(1),catalog:z.string().min(1)}).readonly()),originSchema=z.strictObject({collection:z.string().min(1),id:z.string().min(1),digest:z.string().min(1)}).readonly(),deps={exchanges:ModelExchangeResource.request(),model:ToolModelResource.request()},stateCodec=codec(z.strictObject({id:z.string().min(1),catalog:z.string().min(1),origin:originSchema,exchanges:z.array(refSchema).readonly(),current:refSchema.nullable(),cursor:z.number().int().nonnegative(),active:z.strictObject({name:z.string(),index:z.number().int().nonnegative(),callId:z.string()}).readonly().nullable()}).readonly()),intakeCodec={check:requireExchangeIntakeReceipt,encode:__name(value=>cloneCheckpointValue(requireExchangeIntakeReceipt(value)),"encode"),decode:requireExchangeIntakeReceipt},feedbackCodec={check:requireExchangeFeedbackReceipt,encode:__name(value=>cloneCheckpointValue(requireExchangeFeedbackReceipt(value)),"encode"),decode:requireExchangeFeedbackReceipt};function refsAt(refs,name){const value=refs[name];if(!value)throw new Error("Tool checkpoint references an unknown member");return value}__name(refsAt,"refsAt");function referenceAt(refs,name,index){const value=refsAt(refs,name).at(index);if(!value)throw new Error("Tool checkpoint references a missing child");return value}__name(referenceAt,"referenceAt");class ToolModelCallLimitError extends Error{static{__name(this,"ToolModelCallLimitError")}calls;limit;code="TOOL_MODEL_CALL_LIMIT";constructor(calls,limit){super(`TOOL_MODEL_CALL_LIMIT: ${calls} model calls reached limit ${limit}`),this.calls=calls,this.limit=limit,this.name="ToolModelCallLimitError"}}class Tool extends Group{static{__name(this,"Tool")}tools;catalog;maxModelCalls;waitMs;recoverable;budgets;stages;checkToolCall;constructor(configuration){const options=snapshotConfiguration(configuration);if(super({name:"Tool",deps,output:{result:__name(value=>{if(typeof value!="string"||!value.trim())throw new TypeError("Tool final answer must be nonempty");return value},"result")}}),this.maxModelCalls=options.maxModelCalls===void 0?1e3:options.maxModelCalls,this.waitMs=options.waitMs===void 0?3e4:options.waitMs,this.recoverable=options.checkpoint===!0,this.budgets=resolveToolBudgets(options.budgets),!Number.isSafeInteger(this.maxModelCalls)||this.maxModelCalls<1)throw new RangeError("Tool model call limit must be a positive safe integer");if(!Number.isSafeInteger(this.waitMs)||this.waitMs<0||this.waitMs>2147483647)throw new RangeError("Tool wait must be a nonnegative timer duration");const entries=toolEntries(options.tools,this.recoverable);this.tools=Object.freeze(Object.fromEntries(entries.map(entry=>[entry.name,entry.registration]))),Object.defineProperty(this,"tools",{value:this.tools,enumerable:!0,writable:!1,configurable:!1}),this.checkToolCall=call=>selectTool(entries,call),Object.defineProperty(this,"checkToolCall",{value:this.checkToolCall,enumerable:!1,writable:!1,configurable:!1});const catalog=this.catalog=createToolCatalog(entries,options.policyVersion,{maxModelCalls:this.maxModelCalls,waitMs:this.waitMs,budgets:this.budgets}),inputCodec=inputCodecFor(catalog);for(const name of["catalog","maxModelCalls","waitMs","recoverable","budgets"])Object.defineProperty(this,name,{value:this[name],enumerable:!0,writable:!1,configurable:!1});const budgets=this.budgets,completion=createToolCompletion(this.recoverable,catalog,budgets,this.waitMs);this.stages=Object.freeze({model:completion.id,tools:Object.freeze(entries.map(entry=>Object.freeze({structureId:entry.registration.node.id,name:entry.name})))}),Object.defineProperty(this,"stages",{value:this.stages,enumerable:!0,writable:!1,configurable:!1});const tools=new Map(entries.map(entry=>[entry.name,entry])),modelReception=reception(completion,intakeCodec),toolReceptions=new Map(entries.map(entry=>[entry.name,reception(entry.registration.node,feedbackCodec)])),slots=Object.freeze(Object.fromEntries([["models",childReferences(completion)],...entries.map(entry=>[entry.name,childReferences(entry.registration.node)])])),members=Object.freeze([completion,...entries.map(entry=>entry.registration.node)]),maxModelCalls=this.maxModelCalls,recoverable=this.recoverable,point=checkpointPoint(),prepared=point({name:"prepared",state:preparedCodec,references:slots,check:__name((state,facts)=>{if(state.catalog!==catalog.digest||facts.children.values().length||Object.values(facts.references).some(refs=>refs.length))throw new Error("Prepared Tool configuration or child relations changed")},"check"),resume:{deps,uses:members,run:__name((work,state,saved)=>prepare(work,state,selections(work,saved)),"run")}}),waiting=point({name:"model-waiting",state:stateCodec,references:slots,check,resume:{deps,uses:members,run:resumeModel}}),processing=point({name:"tools",state:stateCodec,references:slots,check,resume:{deps,uses:members,run:resumeTools}}),completed=point({name:"completed",state:stateCodec,references:slots,check,resume:{deps,uses:members,run:__name(async(work,state,refs)=>(await confirmExchanges(work,state,refs),finalText(work,state)),"run")}});recoverable&&(this.checkpoint={key:"capix.tool",version:1,input:inputCodec,output:stringCodec,receptions:[modelReception,...toolReceptions.values()],points:[prepared,waiting,processing,completed]});const save=__name((work,name,state,refs)=>work.checkpoint?.({point:name==="waiting"?waiting:name==="processing"?processing:completed,state,refs}),"save"),selections=__name((work,saved)=>Object.fromEntries(Object.entries(slots).map(([name,slot])=>[name,work.checkpoint?work.checkpoint.select(slot.definition,saved?.[name]):saved?.[name]??[]])),"selections"),append=__name((refs,name,reference)=>{const selected=refsAt(refs,name);return"add"in selected?(selected.add(reference),refs):{...refs,[name]:[...selected,reference]}},"append"),toolReception=__name(name=>{const value=toolReceptions.get(name);if(!value)throw new Error("Tool reception is not registered");return value},"toolReception"),modelFact=__name((facts,ref)=>facts.require(completion,ref),"modelFact");function check(state,facts){if(state.catalog!==catalog.digest||state.id!==state.origin.id)throw new Error("Tool fixed catalog or budgets changed");const models=facts.references.models;if(!models||facts.children.values().length!==Object.values(facts.references).reduce((sum,values)=>sum+values.length,0))throw new Error("Tool checkpoint omitted child relations");if(models.length!==state.exchanges.length+1||models.length>maxModelCalls)throw new Error("Tool model prefix changed");if(models.forEach((fact,index)=>{const child2=modelFact(facts.children,fact.ref);if(!equal(child2.input,{origin:state.origin,exchanges:state.exchanges.slice(0,index)}))throw new Error("Model input differs from fixed exchange prefix");const expected=state.exchanges[index];if(expected){const receipt=facts.children.accepted(child2.ref,modelReception);if(receipt.reference.collection!==expected.collection||receipt.reference.id!==expected.id)throw new Error("Model reception differs from exchange prefix")}}),!state.current){if(state.active||state.cursor!==0)throw new Error("Waiting model has tool progress");return}const latest=models.at(-1);if(!latest)throw new Error("Current exchange lacks its model");const child=modelFact(facts.children,latest.ref),received=facts.children.accepted(child.ref,modelReception);if(state.current.collection!==received.reference.collection||state.current.id!==received.reference.id||child.returned?.status!=="succeeded")throw new Error("Current exchange differs from accepted model");const calls=child.returned.output.reply.output.filter(item=>item.kind==="tool-call");if(state.cursor>calls.length)throw new Error("Tool cursor exceeds model requests");for(let index=0;index<state.cursor;index++){const call=calls[index],selected=selectTool(entries,call);if(selected.kind==="rejected")continue;const found=facts.references[selected.entry.name]?.find(fact=>fact.order>child.order&&fact.key===call.id&&equal(fact.input,selected.input));if(!found)throw new Error("Processed tool has no exact child");const receipt=found.reception.status==="accepted"?facts.children.accepted(found.ref,toolReception(selected.entry.name)):facts.children.handled(found.ref,toolReception(selected.entry.name));if(receipt.reference.collection!==state.current.collection||receipt.reference.id!==state.current.id)throw new Error("Tool receipt belongs to another exchange")}if(state.active){const call=calls[state.cursor],relation=facts.references[state.active.name]?.[state.active.index],selected=call&&selectTool(entries,call);if(!call||selected?.kind!=="accepted"||selected.entry.name!==state.active.name||call.id!==state.active.callId||!relation||relation.order<=child.order||relation.key!==call.id||!equal(relation.input,selected.input))throw new Error("Active tool differs from fixed model request")}}__name(check,"check");async function receiveModel(work,state,refs,child){const result={succeeded:__name(({reply})=>work.resources.exchanges.intake(state.origin,assertToolReplyBytes(reply,budgets)),"succeeded")},received=recoverable?await child.invoke({reception:modelReception,result}):await child.invoke({result}),next={...state,current:received.reference,cursor:0,active:null};return save(work,"processing",next,refs),processTools(work,next,refs)}__name(receiveModel,"receiveModel");function startModel(work,state,refs){const models=refsAt(refs,"models");if(models.length>=maxModelCalls)throw new ToolModelCallLimitError(models.length,maxModelCalls);verifyOrigin(work,state);const child=work.children.create(completion,{origin:state.origin,exchanges:state.exchanges},{key:`model:${models.length}`}),nextRefs=append(refs,"models",child.ref);return save(work,"waiting",state,nextRefs),receiveModel(work,state,nextRefs,child)}__name(startModel,"startModel");async function resumeModel(work,state,saved){const refs=selections(work,saved);await confirmExchanges(work,state,refs);const child=modelFact(work.children.facts,referenceAt(refs,"models",refsAt(refs,"models").length-1));if(child.reception.status==="accepted"){const receipt=work.children.facts.accepted(child.ref,modelReception);return processTools(work,{...state,current:receipt.reference,cursor:0,active:null},refs)}return receiveModel(work,state,refs,work.children.resume(child.ref))}__name(resumeModel,"resumeModel");async function receiveTool(work,state,refs,child){if(!state.current||!state.active)throw new Error("Missing active tool state");const active=state.active,reference=state.current,entry=tools.get(active.name);if(!entry)throw new Error("Tool is not registered");const result={succeeded:__name(async value=>{entry.registration.maxOutputBytes!==void 0&&assertToolOutputBytes(value,entry.registration.maxOutputBytes);const mapped=assertToolFeedbackBytes(active.callId,await entry.registration.result.succeeded(value),budgets);return work.resources.exchanges.receive(state.origin,reference,active.callId,mapped)},"succeeded"),...entry.registration.result.failed?{failed:__name(async failure=>{if(failure instanceof ToolBudgetError)throw failure;return work.resources.exchanges.receive(state.origin,reference,active.callId,assertToolFeedbackBytes(active.callId,await entry.registration.result.failed(failure),budgets))},"failed")}:{}},receipt=recoverable?await child.invoke({reception:toolReception(active.name),result}):await child.invoke({result}),next={...state,current:receipt.reference,cursor:state.cursor+1,active:null};return save(work,"processing",next,refs),processTools(work,next,refs)}__name(receiveTool,"receiveTool");async function resumeTools(work,state,saved){const refs=selections(work,saved);if(await confirmExchanges(work,state,refs),!state.active)return processTools(work,state,refs);const ref=referenceAt(refs,state.active.name,state.active.index),fact=work.children.facts.get(ref);if(fact.reception.status==="accepted"||fact.reception.status==="handled"){const receipt=fact.reception.status==="accepted"?work.children.facts.accepted(ref,toolReception(state.active.name)):work.children.facts.handled(ref,toolReception(state.active.name));return processTools(work,{...state,current:receipt.reference,cursor:state.cursor+1,active:null},refs)}return receiveTool(work,state,refs,work.children.resume(ref))}__name(resumeTools,"resumeTools");async function confirmExchanges(work,state,refs){verifyOrigin(work,state);const modelRefs=Array.from(refsAt(refs,"models"));for(const[index,ref]of modelRefs.entries()){const fact=modelFact(work.children.facts,ref);if(fact.reception.status!=="accepted")continue;const receipt=work.children.facts.accepted(fact.ref,modelReception),entry=work.resources.exchanges.read(state.origin,receipt.reference,!1);if(assertToolReplyBytes(entry.reply,budgets),fact.returned?.status!=="succeeded"||!equal(entry.reply,fact.returned.output.reply)||!equal(entry.operation,receipt.operation))throw new Error("Exchange differs from original model return or reception");await work.resources.model.confirm({reference:fact.returned.output.reference,basis:fact.input,reply:fact.returned.output.reply,budgets});const next=modelRefs[index+1],end=next?work.children.facts.get(next).order:1/0;for(const feedback of entry.feedback){assertToolFeedbackBytes(feedback.toolCallId,{status:feedback.status,content:feedback.content},budgets);const call=entry.reply.output.find(item=>item.kind==="tool-call"&&item.id===feedback.toolCallId);if(!call||call.kind!=="tool-call")throw new Error("Feedback has no original model call");const selected=selectTool(entries,call);if(selected.kind==="rejected"){if(!equal(feedback,{toolCallId:call.id,...selected.result})||entry.receptions.some(receipt2=>receipt2.callId===call.id))throw new Error("Precheck feedback differs from fixed policy");continue}const child=Array.from(refsAt(refs,selected.entry.name)).map(ref2=>work.children.facts.get(ref2)).find(child2=>child2.order>fact.order&&child2.order<end&&child2.key===call.id&&equal(child2.input,selected.input));if(!child)throw new Error("Feedback has no exact tool child");if(child.returned?.status==="succeeded"&&selected.entry.registration.maxOutputBytes!==void 0&&assertToolOutputBytes(child.returned.output,selected.entry.registration.maxOutputBytes),child.reception.status!=="accepted"&&child.reception.status!=="handled")continue;const prior=child.reception.status==="accepted"?work.children.facts.accepted(child.ref,toolReception(selected.entry.name)):work.children.facts.handled(child.ref,toolReception(selected.entry.name));if(prior.reference.collection!==receipt.reference.collection||prior.reference.id!==receipt.reference.id||prior.feedbackDigest!==feedbackDigest(feedback)||!equal(prior.operation,entry.receptions.find(item=>item.callId===call.id)?.operation))throw new Error("Feedback differs from original tool reception")}}for(const entry of entries)for(const ref of refsAt(refs,entry.name)){const child=work.children.facts.require(entry.registration.node,ref);if(child.reception.status!=="accepted"&&child.reception.status!=="handled")continue;const receipt=child.reception.status==="accepted"?work.children.facts.accepted(ref,toolReception(entry.name)):work.children.facts.handled(ref,toolReception(entry.name)),exchange=work.resources.exchanges.read(state.origin,receipt.reference,!1),feedback=exchange.feedback.find(feedback2=>feedback2.toolCallId===child.key);if(!feedback||receipt.feedbackDigest!==feedbackDigest(feedback)||!equal(receipt.operation,exchange.receptions.find(reception2=>reception2.callId===child.key)?.operation))throw new Error("Saved tool reception has no exact exchange feedback")}}__name(confirmExchanges,"confirmExchanges");function finalText(work,state){if(!state.current)throw new Error("Missing final model exchange");const reply=work.resources.exchanges.read(state.origin,state.current,!1).reply;if(reply.output.some(item=>item.kind==="tool-call"))throw new Error("Tool final reply still contains tool calls");if(reply.termination.kind!=="completed")throw new Error("Incomplete model reply cannot finish Tool");const text=modelReplyText(reply);if(!text.trim())throw new Error("Tool final answer is empty");return text}__name(finalText,"finalText");async function processTools(work,initial,refs){let state=initial;if(!state.current)throw new Error("Missing model exchange");verifyOrigin(work,state);const entry=work.resources.exchanges.read(state.origin,state.current,!1);if(entry.reply.termination.kind!=="completed")throw new Error("Incomplete model reply cannot execute tools");const calls=entry.reply.output.filter(item=>item.kind==="tool-call");if(!calls.length){const value=finalText(work,state);return save(work,"completed",state,refs),value}for(await work.resources.model.preflight({origin:state.origin,exchanges:state.exchanges,current:state.current,budgets});state.cursor<calls.length;){const call=calls[state.cursor],selected=selectTool(entries,call);if(selected.kind==="rejected"){const current=work.resources.exchanges.complete(state.origin,state.current,call.id,assertToolFeedbackBytes(call.id,selected.result,budgets));state={...state,current,cursor:state.cursor+1},save(work,"processing",state,refs);continue}const name=selected.entry.name,child=work.children.create(selected.entry.registration.node,selected.input,{key:call.id}),currentRefs=refsAt(refs,name),active={name,index:currentRefs.length,callId:call.id},nextRefs=append(refs,name,child.ref),next={...state,active};return save(work,"processing",next,nextRefs),receiveTool(work,next,nextRefs,child)}const exchanges=[...state.exchanges,state.current];return work.resources.exchanges.closed(state.origin,exchanges),startModel(work,{...state,exchanges,current:null,cursor:0,active:null},refs)}__name(processTools,"processTools");function prepare(work,state,refs){if(state.catalog!==catalog.digest)throw new Error("Tool fixed catalog or policy changed");const origin=work.resources.exchanges.begin(state.id,{request:work.input.request,catalog});return startModel(work,{...state,origin,exchanges:[],current:null,cursor:0,active:null},refs)}__name(prepare,"prepare");function verifyOrigin(work,state){const origin=work.resources.exchanges.origin(state.origin);if(state.catalog!==catalog.digest||state.id!==origin.id||!equal(origin.request,work.input.request)||!equal(origin.catalog,catalog))throw new Error("Tool fixed origin differs from its input or execution catalog");work.resources.exchanges.closed(state.origin,state.exchanges)}__name(verifyOrigin,"verifyOrigin");const run=__name(work=>{inputCodec.encode(work.input);const state={id:randomUUID(),catalog:catalog.digest},refs=selections(work);return work.checkpoint?.({point:prepared,state,refs}),prepare(work,state,refs)},"run");Object.defineProperty(this,"run",{get:__name(()=>run,"get"),enumerable:!0,configurable:!0}),this.add(...members)}}function snapshotConfiguration(value){if(!value||typeof value!="object"||Array.isArray(value)||types.isProxy(value))throw new TypeError("Tool configuration must be a data object");const descriptors=Object.getOwnPropertyDescriptors(value);if(Object.getOwnPropertySymbols(value).length||Object.keys(descriptors).some(key=>!["tools","policyVersion","checkpoint","maxModelCalls","waitMs","budgets"].includes(key)))throw new TypeError("Unknown Tool configuration field");const result={};for(const key of["tools","policyVersion","checkpoint","maxModelCalls","waitMs","budgets"]){const field=Object.getOwnPropertyDescriptor(value,key);if(!field&&key in value||field&&!("value"in field))throw new TypeError("Tool configuration must use own data properties");if(field&&!field.enumerable)throw new TypeError("Tool configuration fields must be enumerable");field&&(result[key]=field.value)}if(result.checkpoint!==void 0&&typeof result.checkpoint!="boolean")throw new TypeError("Tool checkpoint must be boolean");if(result.budgets!==void 0){const budgets=result.budgets;if(!budgets||typeof budgets!="object"||Array.isArray(budgets)||types.isProxy(budgets)||Object.getOwnPropertySymbols(budgets).length)throw new TypeError("Tool budgets must be a data object");const snapshot={};for(const[key,descriptor]of Object.entries(Object.getOwnPropertyDescriptors(budgets))){if(!["maxReplyBytes","maxFeedbackBytes","maxRequestBytes","maxWireRequestBytes","maxCollectionBytes"].includes(key)||!descriptor.enumerable||!("value"in descriptor))throw new TypeError("Tool budgets must contain known data properties");snapshot[key]=descriptor.value}result.budgets=Object.freeze(snapshot)}return Object.freeze(result)}__name(snapshotConfiguration,"snapshotConfiguration");export{Tool,ToolModelCallLimitError};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weiliang0121/capix-tool",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -11,14 +11,14 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "zod": "4.6.5",
14
- "@weiliang0121/capix-contracts": "0.2.0",
15
- "@weiliang0121/capix-diagnostics": "0.2.0",
16
- "@weiliang0121/capix-checkpoint": "0.2.0",
17
- "@weiliang0121/capix-structure": "0.2.0",
18
- "@weiliang0121/capix-interaction": "0.2.0",
19
- "@weiliang0121/capix-resources": "0.2.0",
20
- "@weiliang0121/capix-model-exchanges": "0.2.0",
21
- "@weiliang0121/capix-model-provider": "0.2.0"
14
+ "@weiliang0121/capix-contracts": "0.3.0",
15
+ "@weiliang0121/capix-diagnostics": "0.3.0",
16
+ "@weiliang0121/capix-checkpoint": "0.3.0",
17
+ "@weiliang0121/capix-interaction": "0.3.0",
18
+ "@weiliang0121/capix-resources": "0.3.0",
19
+ "@weiliang0121/capix-structure": "0.3.0",
20
+ "@weiliang0121/capix-model-provider": "0.3.0",
21
+ "@weiliang0121/capix-model-exchanges": "0.3.0"
22
22
  },
23
23
  "description": "A native model tool-calling Group preset for CAPiX.",
24
24
  "license": "MIT",
@@ -44,7 +44,7 @@
44
44
  "registry": "https://registry.npmjs.org"
45
45
  },
46
46
  "devDependencies": {
47
- "@weiliang0121/capix-runtime": "0.2.0"
47
+ "@weiliang0121/capix-runtime": "0.3.0"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "tsc -p tsconfig.json",