@roll-agent/runtime 0.13.0 → 0.14.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.
@@ -1 +1 @@
1
- import{createHash as e,randomUUID as t}from"node:crypto";import{APPROVAL_EXPLANATION_PREVIEW_KEY as r,RUNTIME_ERROR_CODES as s,RUNTIME_FEATURES as n,RUNTIME_METHODS as i,RUNTIME_PROTOCOL_VERSION as a,SUPPORTED_RUNTIME_PROTOCOL_VERSIONS as o,approvalExplanationSchema as d,approvalIdSchema as u,operationIdSchema as l,parseRuntimeMethodResult as c,runtimeInstanceIdSchema as h,runtimeProtocolVersionSchema as p,streamIdSchema as m,threadIdSchema as v}from"@roll-agent/protocol";import{createSafeCapabilitySnapshot as I}from"../engine/capability-manifest.js";import{isSensitiveFieldName as g,redactSecretText as f,toRedactedToolExecutionRecordSummary as y}from"../tool-bridge/tool-execution-record.js";import{ThreadStore as T}from"../store/thread-store.js";const w=4194304,q=500,A=1e4,S=16e3,x=64,O=128,R=16,E=new Set(["_meta","authorization","cookie","password","providerOptions","raw","secret","token"]);export class RuntimeServiceError extends Error{rollCode;retryable;details;constructor(e,t,r={}){super(t),this.name="RuntimeServiceError",this.rollCode=e,this.retryable=r.retryable??!1,this.details=r.details}}function N(t){const r=JSON.stringify(t);if(void 0===r)throw new RuntimeServiceError(s.invalidParams,"无法为 mutation 请求生成稳定指纹");return e("sha256").update(r).digest("hex")}export class MutationRequestCache{maxEntries;inFlight=new Map;settled=new Map;constructor(e){if(!Number.isInteger(e)||e<1)throw new Error("idempotencyCacheEntries must be a positive integer");this.maxEntries=e}run(e,t,r,s){const n=N(r),i=this.inFlight.get(e);if(void 0!==i)return this.assertMatchingRequest(e,t,n,i),i.response.then(e=>c(t,e));const a=this.settled.get(e);if(void 0!==a)return this.assertMatchingRequest(e,t,n,a),this.settled.delete(e),this.settled.set(e,a),a.response.then(e=>c(t,e));const o=Promise.resolve().then(s),d={method:t,fingerprint:n,response:o};return this.inFlight.set(e,d),o.then(()=>{this.settle(e,d)},()=>{this.settle(e,d)}),o}clear(){this.inFlight.clear(),this.settled.clear()}assertMatchingRequest(e,t,r,n){if(n.method!==t||n.fingerprint!==r)throw new RuntimeServiceError(s.invalidParams,`requestId "${e}" 已用于不同请求`)}settle(e,t){if(this.inFlight.get(e)===t)for(this.inFlight.delete(e),this.settled.set(e,t);this.settled.size>this.maxEntries;){const e=this.settled.keys().next().value;if(void 0===e)return;this.settled.delete(e)}}}function C(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}function b(e,t,r=0){if(void 0!==t&&(E.has(t.toLowerCase())||g(t)))return"[redacted]";if(r>=R)return"[nested value omitted]";if(null===e||"boolean"==typeof e)return e;if("string"==typeof e){const t=f(e);return t.length<=S?t:`${t.slice(0,S)}\n[value clipped]`}if("number"==typeof e)return Number.isFinite(e)?e:String(e);if(Array.isArray(e)){const t=e.slice(0,x).map(e=>b(e,void 0,r+1));return e.length>x&&t.push(`[${String(e.length-x)} items omitted]`),t}if(C(e)){const t={};for(const[s,n]of Object.entries(e).slice(0,O))void 0!==n&&(t[s]=b(n,s,r+1));return Object.keys(e).length>O&&(t._omitted=`${String(Object.keys(e).length-O)} keys omitted`),t}return void 0===e?null:String(e)}function P(e){return e instanceof Error?e.message:String(e)}function F(e){const t=e.content;return"string"==typeof t?t:Array.isArray(t)?t.flatMap(e=>C(e)&&"text"===e.type&&"string"==typeof e.text?[e.text]:[]).join(""):""}function j(e){return v.parse(e)}function M(e,t){return{id:j(t.id),...void 0!==t.title?{title:t.title}:{},...void 0!==t.model?{model:t.model}:{},createdAt:t.createdAt,updatedAt:t.updatedAt,messageCount:e.countTranscriptMessages(t.id)}}function V(e){if("user"!==e.message.role&&"assistant"!==e.message.role)return;const t=f(F(e.message));return{sequence:e.sequence,role:e.message.role,createdAt:e.createdAt,parts:t.length>0?[{type:"text",text:t}]:[]}}function $(e){const t=y(e);return{id:l.parse(t.id),sequence:e.sequence,toolCallId:t.toolCallId,agentName:t.agentName,toolName:t.toolName,createdAt:t.createdAt,outcome:{kind:t.outcome.kind,..."reason"in t.outcome&&void 0!==t.outcome.reason?{reason:t.outcome.reason}:{}},display:b(t.display,void 0)}}function B(e,t){const s=b(t.input,void 0),n=void 0===t.explanation?void 0:d.safeParse(f(t.explanation)),i=!0===n?.success&&C(s)?{...s,[r]:n.data}:s;return{id:u.parse(t.approvalId),turnId:e,agentName:t.agentName,toolName:t.toolName,preview:i,...void 0!==t.reason?{reason:f(t.reason)}:{}}}export class RuntimeService{runtimeInstanceId;engine;store;serverName;serverVersion;runtimeVersion;maxFrameBytes;idempotencyCacheEntries;reasoningSummaryProjector;sessions=new Map;activeTurns=new Map;activeTurnOwners=new Map;settledTurnOwners=new Map;pendingApprovals=new Map;listeners=new Set;mutationRequests;sequence=0;closing=!1;constructor(e,r,s={}){this.engine=e,this.store=r,this.serverName=s.serverName??"roll-runtime",this.serverVersion=s.serverVersion??"1.0",this.runtimeVersion=s.runtimeVersion??"development",this.maxFrameBytes=s.maxFrameBytes??w,this.idempotencyCacheEntries=s.idempotencyCacheEntries??A,this.mutationRequests=new MutationRequestCache(this.idempotencyCacheEntries),this.reasoningSummaryProjector=s.reasoningSummaryProjector,this.runtimeInstanceId=h.parse(t())}initialize(e){let t;this.assertOpen();for(const r of e.protocolVersions){const e=p.safeParse(r);if(e.success){t=e.data;break}}if(void 0===t)throw new RuntimeServiceError(s.protocolVersionUnsupported,`不支持客户端协议版本:${e.protocolVersions.join(", ")}`,{details:{supportedVersions:o}});return{protocolVersion:t,runtimeInstanceId:this.runtimeInstanceId,server:{name:this.serverName,version:this.serverVersion,runtimeVersion:this.runtimeVersion},features:n.filter(e=>"reasoning-summary"!==e||void 0!==this.reasoningSummaryProjector),limits:{maxFrameBytes:this.maxFrameBytes,maxPageSize:q,eventReplay:!1,idempotencyCacheEntries:this.idempotencyCacheEntries}}}onEvent(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}listThreads(e){this.assertOpen();const t=void 0===e.cursor?0:Number(e.cursor);if(!Number.isSafeInteger(t)||t<0)throw new RuntimeServiceError(s.invalidParams,"thread.list cursor 超出安全整数范围");const r=this.store.listThreads(),n=r.slice(t,t+e.limit).map(e=>M(this.store,e)),i=t+n.length;return{items:n,nextCursor:i<r.length?String(i):null}}async createThread(e){return this.mutationRequests.run(e.requestId,i.threadCreate,e,async()=>{this.assertOpen();const t=await this.engine.createSession(void 0!==e.title?{title:e.title}:{});return this.sessions.set(t.id,t),{thread:M(this.store,this.requireThread(t.id))}})}async openThread(e){return await this.requireSession(e.threadId),this.snapshotThread({threadId:e.threadId,limit:100})}snapshotThread(e){this.assertOpen();const t=this.requireThread(e.threadId),r=this.store.listRecentTranscriptMessages(e.threadId,{...void 0!==e.messageBeforeSequence?{beforeSequence:e.messageBeforeSequence}:{},limit:e.limit}),s=this.store.listRecentToolExecutions(e.threadId,{...void 0!==e.operationBeforeSequence?{beforeSequence:e.operationBeforeSequence}:{},limit:e.limit}),n=this.activeTurns.get(e.threadId),i=void 0===n?void 0:{id:n.turnId,status:n.status,startedAt:n.startedAt};return{thread:M(this.store,t),messages:{items:r.entries.flatMap(e=>{const t=V(e);return void 0===t?[]:[t]}),nextBeforeSequence:r.nextBeforeSequence??null},operations:{items:s.entries.map($),nextBeforeSequence:s.nextBeforeSequence??null},...void 0!==i?{activeTurn:i}:{},pendingApprovals:[...this.pendingApprovals.values()].filter(t=>t.threadId===e.threadId).map(e=>e.approval),transcriptCompleteness:this.store.getTranscriptCompleteness(e.threadId)}}async renameThread(e){return this.mutationRequests.run(e.requestId,i.threadRename,e,()=>(this.assertOpen(),this.requireThread(e.threadId),this.store.updateTitle(e.threadId,e.title),{thread:M(this.store,this.requireThread(e.threadId))}))}async deleteThread(e){return this.mutationRequests.run(e.requestId,i.threadDelete,e,async()=>{if(this.assertOpen(),this.requireThread(e.threadId),this.activeTurns.has(e.threadId))throw new RuntimeServiceError(s.threadBusy,`Thread "${e.threadId}" 仍有活动 Turn`,{retryable:!0});const t=this.sessions.get(e.threadId);return void 0!==t&&(await t.close(),this.sessions.delete(e.threadId)),this.store.deleteThread(e.threadId),{deleted:!0}})}async detachThread(e){return this.mutationRequests.run(e.requestId,i.threadDetach,e,async()=>{if(this.assertOpen(),this.requireThread(e.threadId),this.activeTurns.has(e.threadId))return{detached:!1};const t=this.sessions.get(e.threadId);return void 0===t?{detached:!1}:(await t.close(),this.sessions.delete(e.threadId),{detached:!0})})}async threadCapabilities(e){const t=await this.requireSession(e.threadId);return{manifest:b(I(t.getCapabilityManifest(),t.getCapabilityTurnContext()),void 0)}}async startTurn(e){return this.mutationRequests.run(e.requestId,i.turnStart,e,async()=>{this.assertOpen();const t=this.findTurnOwner(e.turnId,e.threadId);if(void 0!==t){if(t===e.threadId)return{accepted:!0,turnId:e.turnId};throw new RuntimeServiceError(s.turnAlreadyActive,`Turn "${e.turnId}" 已属于另一个 Thread`)}if(this.activeTurns.has(e.threadId))throw new RuntimeServiceError(s.turnAlreadyActive,`Thread "${e.threadId}" 已有活动 Turn`,{retryable:!0});const r=await this.requireSession(e.threadId),n=this.findTurnOwner(e.turnId,e.threadId);if(void 0!==n){if(n===e.threadId)return{accepted:!0,turnId:e.turnId};throw new RuntimeServiceError(s.turnAlreadyActive,`Turn "${e.turnId}" 已属于另一个 Thread`)}if(this.activeTurns.has(e.threadId))throw new RuntimeServiceError(s.turnAlreadyActive,`Thread "${e.threadId}" 已有活动 Turn`,{retryable:!0});const i={threadId:e.threadId,turnId:e.turnId,session:r,startedAt:(new Date).toISOString(),status:"running",interactionFailure:void 0};return this.activeTurns.set(e.threadId,i),this.activeTurnOwners.set(e.turnId,e.threadId),this.emit(e.threadId,e.turnId,{type:"turn.started"}),this.driveTurn(i,e.input.text).catch(()=>{}),{accepted:!0,turnId:e.turnId}})}async cancelTurn(e){return this.mutationRequests.run(e.requestId,i.turnCancel,e,()=>{this.assertOpen();const t=this.activeTurns.get(e.threadId);if(void 0===t||t.turnId!==e.turnId)throw new RuntimeServiceError(s.turnNotFound,`Turn "${e.turnId}" 不存在或已结束`);t.status="cancelling";let r=!1;try{this.cancelPendingApprovalsForTurn(e.turnId,{status:"cancelled",reason:"Turn 已由客户端取消"})}finally{r=t.session.cancel()}return{cancelling:r}})}async respondApproval(e){return this.mutationRequests.run(e.requestId,i.approvalRespond,e,()=>this.resolvePendingApproval(e,"approve"===e.decision?{decision:"approve"}:{decision:"reject",...void 0!==e.reason?{reason:e.reason}:{}}))}resolvePendingApproval(e,t){this.assertOpen();const r=this.requirePendingApproval(e),n="approve"===t.decision?r.session.approve(e.approvalId):r.session.reject(e.approvalId,t.reason);if(this.pendingApprovals.delete(e.approvalId),!n)throw this.emitApprovalResolved(r,{status:"cancelled",reason:"审批已失效"}),new RuntimeServiceError(s.approvalNotFound,`Approval "${e.approvalId}" 已失效`);return this.emitApprovalResolved(r,"approve"===t.decision?{status:"resolved",decision:"approve"}:{status:"resolved",decision:"reject",...void 0!==t.reason?{reason:t.reason}:{}}),{resolved:!0}}cancelPendingApproval(e,t){const r=this.findPendingApproval(e);return void 0!==r&&(this.pendingApprovals.delete(e.approvalId),this.emitApprovalResolved(r,t),!0)}async failPendingApprovalInteraction(e,t,r={status:"cancelled",reason:t}){const s=this.findPendingApproval(e);if(void 0===s)return!1;const n=this.activeTurns.get(e.threadId);void 0!==n&&n.turnId===e.turnId&&(n.interactionFailure=t),this.pendingApprovals.delete(e.approvalId);try{this.emitApprovalResolved(s,r)}finally{s.session.cancel()}return!0}getOperation(e){this.assertOpen(),this.requireThread(e.threadId);const t=this.store.getToolExecution(e.threadId,e.operationId);return{operation:void 0===t?null:$(t)}}async close(){if(this.closing)return;this.closing=!0;for(const e of this.activeTurns.values())e.session.cancel();const e=[...this.sessions.values()];this.sessions.clear(),this.activeTurns.clear(),this.activeTurnOwners.clear(),this.settledTurnOwners.clear(),this.pendingApprovals.clear(),await Promise.allSettled(e.map(e=>e.close())),this.mutationRequests.clear()}assertOpen(){if(this.closing)throw new RuntimeServiceError(s.runtimeClosing,"Runtime 正在关闭",{retryable:!0})}requireThread(e){const t=this.store.getThread(e);if(void 0===t)throw new RuntimeServiceError(s.threadNotFound,`Thread "${e}" 不存在`);return t}async requireSession(e){this.assertOpen(),this.requireThread(e);const t=this.sessions.get(e);if(void 0!==t)return t;const r=await this.engine.resumeSession(e);return this.sessions.set(e,r),r}emit(e,t,r){const s={protocolVersion:a,runtimeInstanceId:this.runtimeInstanceId,sequence:this.sequence,timestamp:(new Date).toISOString(),threadId:e,...void 0!==t?{turnId:t}:{},event:r};this.sequence+=1;for(const e of this.listeners)try{e(s)}catch{}}async driveTurn(e,t){const r={streamId:void 0,text:"",terminalEvent:void 0};let s;try{for await(const s of e.session.send(t))this.projectSessionEvent(e,r,s);s=r.terminalEvent??{type:"turn.completed"}}catch(e){s=r.terminalEvent??{type:"turn.failed",stage:"execute",message:f(P(e))}}finally{this.activeTurns.get(e.threadId)===e&&this.activeTurns.delete(e.threadId),this.settleTurnOwner(e)}this.emitTurnTerminal(e,s)}findTurnOwner(e,t){const r=this.activeTurnOwners.get(e);if(void 0!==r)return r;const s=this.settledTurnOwners.get(e);return s===t&&(this.settledTurnOwners.delete(e),this.settledTurnOwners.set(e,s)),s}settleTurnOwner(e){if(this.activeTurnOwners.get(e.turnId)===e.threadId&&(this.activeTurnOwners.delete(e.turnId),!this.closing))for(this.settledTurnOwners.set(e.turnId,e.threadId);this.settledTurnOwners.size>this.idempotencyCacheEntries;){const e=this.settledTurnOwners.keys().next().value;if(void 0===e)return;this.settledTurnOwners.delete(e)}}projectSessionEvent(e,r,s){switch(s.type){case"message-start":{const s=m.parse(t());return r.streamId=s,r.text="",void this.emit(e.threadId,e.turnId,{type:"message.started",streamId:s})}case"text-delta":{const n=r.streamId??m.parse(t());return void 0===r.streamId&&(r.streamId=n,this.emit(e.threadId,e.turnId,{type:"message.started",streamId:n})),r.text+=s.delta,void this.emit(e.threadId,e.turnId,{type:"message.delta",streamId:n,delta:s.delta})}case"message-finish":{const n=r.streamId??m.parse(t());return this.emit(e.threadId,e.turnId,{type:"message.completed",streamId:n,text:f(s.text||r.text)}),void(r.terminalEvent??={type:"turn.completed"})}case"reasoning-delta":{const t=this.reasoningSummaryProjector?.(s.delta);return void(void 0!==t&&t.length>0&&this.emit(e.threadId,e.turnId,{type:"reasoning.summary.delta",reasoningId:s.reasoningId,delta:f(t)}))}case"tool-call":return void this.emit(e.threadId,e.turnId,{type:"tool.started",toolCallId:s.toolCallId,agentName:s.agentName,toolName:s.toolName,input:b(s.input,void 0)});case"tool-output-delta":return void this.emit(e.threadId,e.turnId,{type:"tool.output",toolCallId:s.toolCallId,agentName:s.agentName,toolName:s.toolName,stream:s.stream,delta:f(s.delta)});case"tool-result":return void this.emit(e.threadId,e.turnId,{type:"tool.completed",toolCallId:s.toolCallId,agentName:s.agentName,toolName:s.toolName,...void 0!==s.executionId?{operationId:l.parse(s.executionId)}:{},...void 0!==s.outcome?{outcome:this.projectToolOutcome(s.outcome)}:{},display:b(s.display,void 0)});case"confirmation-required":{const t=B(e.turnId,s);return this.pendingApprovals.set(t.id,{threadId:e.threadId,session:e.session,approval:t}),void this.emit(e.threadId,e.turnId,{type:"approval.required",approval:t})}case"turn-cancelled":r.terminalEvent??=void 0===e.interactionFailure?{type:"turn.cancelled",reason:s.reason,message:f(s.message)}:{type:"turn.failed",stage:"execute",message:f(e.interactionFailure)};break;case"error":r.terminalEvent??={type:"turn.failed",stage:s.stage,message:f(s.message)}}}projectToolOutcome(e){return{kind:e.kind,..."reason"in e&&void 0!==e.reason?{reason:f(e.reason)}:{}}}findPendingApproval(e){const t=this.pendingApprovals.get(e.approvalId);if(void 0!==t&&t.threadId===e.threadId&&t.approval.turnId===e.turnId)return t}requirePendingApproval(e){const t=this.findPendingApproval(e);if(void 0===t)throw new RuntimeServiceError(s.approvalNotFound,`Approval "${e.approvalId}" 不存在或已失效`);return t}emitApprovalResolved(e,t){this.emit(e.threadId,e.approval.turnId,{type:"approval.resolved",approvalId:e.approval.id,resolution:t})}cancelPendingApprovalsForTurn(e,t){const r=[...this.pendingApprovals.values()].filter(t=>t.approval.turnId===e);let s;for(const e of r)try{this.cancelPendingApproval({threadId:e.threadId,turnId:e.approval.turnId,approvalId:e.approval.id},t)}catch(e){s??=e}if(void 0!==s)throw s}emitTurnTerminal(e,t){this.cancelPendingApprovalsForTurn(e.turnId,"turn.cancelled"===t.type&&"timeout"===t.reason?{status:"expired",reason:"Turn 已超时"}:{status:"cancelled",reason:`Turn 已终止:${t.type}`}),this.emit(e.threadId,e.turnId,t)}}
1
+ import{createHash as e,randomUUID as t}from"node:crypto";import{APPROVAL_EXPLANATION_PREVIEW_KEY as r,RUNTIME_ERROR_CODES as s,RUNTIME_FEATURES as n,RUNTIME_METHODS as i,RUNTIME_PROTOCOL_VERSION as a,SUPPORTED_RUNTIME_PROTOCOL_VERSIONS as o,approvalExplanationSchema as d,approvalIdSchema as u,normalizeUserInputResultForForm as l,operationIdSchema as c,parseRuntimeMethodResult as h,runtimeDurableEventV13Schema as p,runtimeEphemeralEventV13Schema as I,runtimeInstanceIdSchema as m,runtimeProtocolVersionSchema as v,streamIdSchema as g,threadIdSchema as f,userInputFormSchema as y}from"@roll-agent/protocol";import{createSafeCapabilitySnapshot as T}from"../engine/capability-manifest.js";import{isSensitiveFieldName as w,redactSecretText as A,toRedactedToolExecutionRecordSummary as q}from"../tool-bridge/tool-execution-record.js";import{RuntimeEventCursorExpiredError as S,RuntimeEventCursorGapError as E,ThreadStore as R}from"../store/thread-store.js";const x=4194304,b=500,O=1e4,P=16e3,U=64,C=128,N=16,F=new Set(["_meta","authorization","cookie","password","provideroptions","raw","secret","token"]);export class RuntimeServiceError extends Error{rollCode;retryable;details;constructor(e,t,r={}){super(t),this.name="RuntimeServiceError",this.rollCode=e,this.retryable=r.retryable??!1,this.details=r.details}}function j(t){const r=JSON.stringify(t);if(void 0===r)throw new RuntimeServiceError(s.invalidParams,"无法为 mutation 请求生成稳定指纹");return e("sha256").update(r).digest("hex")}export class MutationRequestCache{maxEntries;inFlight=new Map;settled=new Map;constructor(e){if(!Number.isInteger(e)||e<1)throw new Error("idempotencyCacheEntries must be a positive integer");this.maxEntries=e}run(e,t,r,s){const n=j(r),i=this.inFlight.get(e);if(void 0!==i)return this.assertMatchingRequest(e,t,n,i),i.response.then(e=>h(t,e));const a=this.settled.get(e);if(void 0!==a)return this.assertMatchingRequest(e,t,n,a),this.settled.delete(e),this.settled.set(e,a),a.response.then(e=>h(t,e));const o=Promise.resolve().then(s),d={method:t,fingerprint:n,response:o};return this.inFlight.set(e,d),o.then(()=>{this.settle(e,d)},()=>{this.settle(e,d)}),o}clear(){this.inFlight.clear(),this.settled.clear()}assertMatchingRequest(e,t,r,n){if(n.method!==t||n.fingerprint!==r)throw new RuntimeServiceError(s.invalidParams,`requestId "${e}" 已用于不同请求`)}settle(e,t){if(this.inFlight.get(e)===t)for(this.inFlight.delete(e),this.settled.set(e,t);this.settled.size>this.maxEntries;){const e=this.settled.keys().next().value;if(void 0===e)return;this.settled.delete(e)}}}function M(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}function V(e,t,r=0){if(void 0!==t&&(F.has(t.toLowerCase())||w(t)))return"[redacted]";if(r>=N)return"[nested value omitted]";if(null===e||"boolean"==typeof e)return e;if("string"==typeof e){const t=A(e);return t.length<=P?t:`${t.slice(0,P)}\n[value clipped]`}if("number"==typeof e)return Number.isFinite(e)?e:String(e);if(Array.isArray(e)){const t=e.slice(0,U).map(e=>V(e,void 0,r+1));return e.length>U&&t.push(`[${String(e.length-U)} items omitted]`),t}if(M(e)){const t={};for(const[s,n]of Object.entries(e).slice(0,C))void 0!==n&&(t[s]=V(n,s,r+1));return Object.keys(e).length>C&&(t._omitted=`${String(Object.keys(e).length-C)} keys omitted`),t}return void 0===e?null:String(e)}function $(e){return e instanceof Error?e.message:String(e)}function k(e){const t=e.content;return"string"==typeof t?t:Array.isArray(t)?t.flatMap(e=>M(e)&&"text"===e.type&&"string"==typeof e.text?[e.text]:[]).join(""):""}function B(e){return f.parse(e)}function L(e,t){return{id:B(t.id),...void 0!==t.title?{title:t.title}:{},...void 0!==t.model?{model:t.model}:{},createdAt:t.createdAt,updatedAt:t.updatedAt,messageCount:e.countTranscriptMessages(t.id)}}function z(e){if("user"!==e.message.role&&"assistant"!==e.message.role)return;const t=A(k(e.message));return{sequence:e.sequence,role:e.message.role,createdAt:e.createdAt,parts:t.length>0?[{type:"text",text:t}]:[]}}function D(e){const t=q(e);return{id:c.parse(t.id),sequence:e.sequence,toolCallId:t.toolCallId,agentName:t.agentName,toolName:t.toolName,createdAt:t.createdAt,outcome:{kind:t.outcome.kind,..."reason"in t.outcome&&void 0!==t.outcome.reason?{reason:t.outcome.reason}:{}},display:V(t.display,void 0)}}function _(e,t){const s=V(t.input,void 0),n=void 0===t.explanation?void 0:d.safeParse(A(t.explanation)),i=!0===n?.success&&M(s)?{...s,[r]:n.data}:s;return{id:u.parse(t.approvalId),turnId:e,agentName:t.agentName,toolName:t.toolName,preview:i,...void 0!==t.reason?{reason:A(t.reason)}:{}}}export class RuntimeService{runtimeInstanceId;engine;store;serverName;serverVersion;runtimeVersion;maxFrameBytes;idempotencyCacheEntries;reasoningSummaryProjector;sessions=new Map;activeTurns=new Map;activeTurnOwners=new Map;settledTurnOwners=new Map;pendingApprovals=new Map;pendingUserInputs=new Map;listeners=new Set;fatalErrorListeners=new Set;userInputListeners=new Set;mutationRequests;sequence=0;closing=!1;userInputAvailable=!1;constructor(e,r,s={}){this.engine=e,this.store=r,this.serverName=s.serverName??"roll-runtime",this.serverVersion=s.serverVersion??"1.0",this.runtimeVersion=s.runtimeVersion??"development",this.maxFrameBytes=s.maxFrameBytes??x,this.idempotencyCacheEntries=s.idempotencyCacheEntries??O,this.mutationRequests=new MutationRequestCache(this.idempotencyCacheEntries),this.reasoningSummaryProjector=s.reasoningSummaryProjector,this.runtimeInstanceId=m.parse(t())}initialize(e){let t;this.assertOpen();for(const r of e.protocolVersions){const e=v.safeParse(r);if(e.success){t=e.data;break}}if(void 0===t)throw new RuntimeServiceError(s.protocolVersionUnsupported,`不支持客户端协议版本:${e.protocolVersions.join(", ")}`,{details:{supportedVersions:o}});const r={protocolVersion:t,runtimeInstanceId:this.runtimeInstanceId,server:{name:this.serverName,version:this.serverVersion,runtimeVersion:this.runtimeVersion},features:n.filter(e=>"reasoning-summary"!==e||void 0!==this.reasoningSummaryProjector),limits:{maxFrameBytes:this.maxFrameBytes,maxPageSize:b,idempotencyCacheEntries:this.idempotencyCacheEntries}};return"1.3"===t?{...r,protocolVersion:t,limits:{...r.limits,eventReplay:!0}}:{...r,protocolVersion:t,limits:{...r.limits,eventReplay:!1}}}onEvent(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}onFatalError(e){return this.fatalErrorListeners.add(e),()=>{this.fatalErrorListeners.delete(e)}}onUserInputInteraction(e){return this.userInputListeners.add(e),()=>{this.userInputListeners.delete(e)}}setUserInputAvailable(e){if(this.userInputAvailable!==e){this.userInputAvailable=e,e||this.cancelAllPendingUserInputs("当前客户端已撤销用户输入处理能力");for(const t of this.sessions.values())t.setUserInputAvailable?.(e)}}listThreads(e){this.assertOpen();const t=void 0===e.cursor?0:Number(e.cursor);if(!Number.isSafeInteger(t)||t<0)throw new RuntimeServiceError(s.invalidParams,"thread.list cursor 超出安全整数范围");const r=this.store.listThreads(),n=r.slice(t,t+e.limit).map(e=>L(this.store,e)),i=t+n.length;return{items:n,nextCursor:i<r.length?String(i):null}}async createThread(e){return this.mutationRequests.run(e.requestId,i.threadCreate,e,async()=>{this.assertOpen();const t=await this.engine.createSession(void 0!==e.title?{title:e.title}:{});return this.rememberSession(t),{thread:L(this.store,this.requireThread(t.id))}})}async openThread(e){return await this.requireSession(e.threadId),this.snapshotThread({threadId:e.threadId,limit:100})}snapshotThread(e){this.assertOpen();const t=this.requireThread(e.threadId),r=this.store.listRecentTranscriptMessages(e.threadId,{...void 0!==e.messageBeforeSequence?{beforeSequence:e.messageBeforeSequence}:{},limit:e.limit}),s=this.store.listRecentToolExecutions(e.threadId,{...void 0!==e.operationBeforeSequence?{beforeSequence:e.operationBeforeSequence}:{},limit:e.limit}),n=this.activeTurns.get(e.threadId),i=void 0===n?void 0:{id:n.turnId,status:n.status,startedAt:n.startedAt};return{thread:L(this.store,t),messages:{items:r.entries.flatMap(e=>{const t=z(e);return void 0===t?[]:[t]}),nextBeforeSequence:r.nextBeforeSequence??null},operations:{items:s.entries.map(D),nextBeforeSequence:s.nextBeforeSequence??null},...void 0!==i?{activeTurn:i}:{},pendingApprovals:[...this.pendingApprovals.values()].filter(t=>t.threadId===e.threadId).map(e=>e.approval),transcriptCompleteness:this.store.getTranscriptCompleteness(e.threadId),eventCursor:this.store.getRuntimeEventCursor(e.threadId)}}resumeEvents(e){this.assertOpen(),this.requireThread(e.threadId);try{return this.store.resumeRuntimeEvents(e.threadId,e.afterCursor)}catch(t){if(t instanceof S)throw new RuntimeServiceError(s.eventCursorExpired,"Runtime Event cursor 已超出当前 Thread 的保留窗口,请回退到 thread.snapshot",{details:{threadId:e.threadId}});if(t instanceof E)throw new RuntimeServiceError(s.eventCursorGap,"Runtime Event cursor 与当前 Thread 事件日志不连续,请回退到 thread.snapshot",{details:{threadId:e.threadId}});throw t}}async renameThread(e){return this.mutationRequests.run(e.requestId,i.threadRename,e,()=>(this.assertOpen(),this.requireThread(e.threadId),this.store.updateTitle(e.threadId,e.title),{thread:L(this.store,this.requireThread(e.threadId))}))}async deleteThread(e){return this.mutationRequests.run(e.requestId,i.threadDelete,e,async()=>{if(this.assertOpen(),this.requireThread(e.threadId),this.activeTurns.has(e.threadId))throw new RuntimeServiceError(s.threadBusy,`Thread "${e.threadId}" 仍有活动 Turn`,{retryable:!0});const t=this.sessions.get(e.threadId);return void 0!==t&&(await t.close(),this.sessions.delete(e.threadId)),this.store.deleteThread(e.threadId),{deleted:!0}})}async detachThread(e){return this.mutationRequests.run(e.requestId,i.threadDetach,e,async()=>{if(this.assertOpen(),this.requireThread(e.threadId),this.activeTurns.has(e.threadId))return{detached:!1};const t=this.sessions.get(e.threadId);return void 0===t?{detached:!1}:(await t.close(),this.sessions.delete(e.threadId),{detached:!0})})}async threadCapabilities(e){const t=await this.requireSession(e.threadId);return{manifest:V(T(t.getCapabilityManifest(),t.getCapabilityTurnContext()),void 0)}}async startTurn(e){return this.mutationRequests.run(e.requestId,i.turnStart,e,async()=>{this.assertOpen();const t=this.findTurnOwner(e.turnId,e.threadId);if(void 0!==t){if(t===e.threadId)return{accepted:!0,turnId:e.turnId};throw new RuntimeServiceError(s.turnAlreadyActive,`Turn "${e.turnId}" 已属于另一个 Thread`)}if(this.activeTurns.has(e.threadId))throw new RuntimeServiceError(s.turnAlreadyActive,`Thread "${e.threadId}" 已有活动 Turn`,{retryable:!0});const r=await this.requireSession(e.threadId),n=this.findTurnOwner(e.turnId,e.threadId);if(void 0!==n){if(n===e.threadId)return{accepted:!0,turnId:e.turnId};throw new RuntimeServiceError(s.turnAlreadyActive,`Turn "${e.turnId}" 已属于另一个 Thread`)}if(this.activeTurns.has(e.threadId))throw new RuntimeServiceError(s.turnAlreadyActive,`Thread "${e.threadId}" 已有活动 Turn`,{retryable:!0});const i={threadId:e.threadId,turnId:e.turnId,session:r,startedAt:(new Date).toISOString(),status:"running",interactionFailure:void 0};this.activeTurns.set(e.threadId,i),this.activeTurnOwners.set(e.turnId,e.threadId);try{this.emit(e.threadId,e.turnId,{type:"turn.started"})}catch(t){throw this.activeTurns.delete(e.threadId),this.activeTurnOwners.delete(e.turnId),t}return this.driveTurn(i,e.input.text).catch(()=>{}),{accepted:!0,turnId:e.turnId}})}async cancelTurn(e){return this.mutationRequests.run(e.requestId,i.turnCancel,e,()=>{this.assertOpen();const t=this.activeTurns.get(e.threadId);if(void 0===t||t.turnId!==e.turnId)throw new RuntimeServiceError(s.turnNotFound,`Turn "${e.turnId}" 不存在或已结束`);t.status="cancelling";let r=!1;try{this.cancelPendingUserInputsForTurn(e.turnId,"Turn 已由客户端取消"),this.cancelPendingApprovalsForTurn(e.turnId,{status:"cancelled",reason:"Turn 已由客户端取消"})}finally{r=t.session.cancel()}return{cancelling:r}})}async respondApproval(e){return this.mutationRequests.run(e.requestId,i.approvalRespond,e,()=>this.resolvePendingApproval(e,"approve"===e.decision?{decision:"approve"}:{decision:"reject",...void 0!==e.reason?{reason:e.reason}:{}}))}resolvePendingApproval(e,t){this.assertOpen();const r=this.requirePendingApproval(e),n="approve"===t.decision?r.session.approve(e.approvalId):r.session.reject(e.approvalId,t.reason);if(this.pendingApprovals.delete(e.approvalId),!n)throw this.emitApprovalResolved(r,{status:"cancelled",reason:"审批已失效"}),new RuntimeServiceError(s.approvalNotFound,`Approval "${e.approvalId}" 已失效`);return this.emitApprovalResolved(r,"approve"===t.decision?{status:"resolved",decision:"approve"}:{status:"resolved",decision:"reject",...void 0!==t.reason?{reason:t.reason}:{}}),{resolved:!0}}getPendingApprovalExpiresAt(e){return this.findPendingApproval(e)?.expiresAt}cancelPendingApproval(e,t){const r=this.findPendingApproval(e);return void 0!==r&&(this.pendingApprovals.delete(e.approvalId),this.emitApprovalResolved(r,t),!0)}async failPendingApprovalInteraction(e,t,r={status:"cancelled",reason:t}){const s=this.findPendingApproval(e);if(void 0===s)return!1;const n=this.activeTurns.get(e.threadId);void 0!==n&&n.turnId===e.turnId&&(n.interactionFailure=t),this.pendingApprovals.delete(e.approvalId);try{this.emitApprovalResolved(s,r)}finally{s.session.cancel()}return!0}resolvePendingUserInput(e,t){const r=this.pendingUserInputs.get(e);if(void 0===r)return!1;let s;try{s=l(r.form,t)}catch{return this.cancelPendingUserInput(e,"用户输入不符合原始表单约束"),!1}this.pendingUserInputs.delete(e);const n=r.session.resolveUserInput?.(e,s)??!1;if(!n){const t="用户输入请求已失效";return r.session.cancelUserInput?.(e,t),this.restoreRunningStatus(r),this.emitUserInputInteraction({type:"settled",requestId:e,threadId:r.threadId,turnId:r.turnId,reason:t}),!1}return this.restoreRunningStatus(r),this.emitUserInputInteraction({type:"settled",requestId:e,threadId:r.threadId,turnId:r.turnId,reason:"submitted"===s.status?"用户已提交输入":"用户已取消输入"}),n}cancelPendingUserInput(e,t){const r=this.pendingUserInputs.get(e);if(void 0===r)return!1;this.pendingUserInputs.delete(e);const s=r.session.cancelUserInput?.(e,t)??!1;return this.restoreRunningStatus(r),this.emitUserInputInteraction({type:"settled",requestId:e,threadId:r.threadId,turnId:r.turnId,reason:t}),s}getOperation(e){this.assertOpen(),this.requireThread(e.threadId);const t=this.store.getToolExecution(e.threadId,e.operationId);return{operation:void 0===t?null:D(t)}}async close(){if(this.closing)return;this.closing=!0,this.setUserInputAvailable(!1);for(const e of this.activeTurns.values())e.session.cancel();const e=[...this.sessions.values()];this.sessions.clear(),this.activeTurns.clear(),this.activeTurnOwners.clear(),this.settledTurnOwners.clear(),this.pendingApprovals.clear(),this.pendingUserInputs.clear(),this.userInputListeners.clear(),await Promise.allSettled(e.map(e=>e.close())),this.mutationRequests.clear(),this.fatalErrorListeners.clear()}assertOpen(){if(this.closing)throw new RuntimeServiceError(s.runtimeClosing,"Runtime 正在关闭",{retryable:!0})}requireThread(e){const t=this.store.getThread(e);if(void 0===t)throw new RuntimeServiceError(s.threadNotFound,`Thread "${e}" 不存在`);return t}async requireSession(e){this.assertOpen(),this.requireThread(e);const t=this.sessions.get(e);if(void 0!==t)return t;const r=await this.engine.resumeSession(e);return this.rememberSession(r),r}rememberSession(e){e.setUserInputAvailable?.(this.userInputAvailable),this.sessions.set(e.id,e)}emit(e,t,r){const s=(new Date).toISOString(),n={protocolVersion:a,runtimeInstanceId:this.runtimeInstanceId,sequence:this.sequence,timestamp:s,threadId:e,...void 0!==t?{turnId:t}:{}},i=p.safeParse(r),o=i.success?(()=>{let r;try{r=this.store.appendRuntimeEvent({threadId:e,...void 0===t?{}:{turnId:t},timestamp:s,event:i.data})}catch(e){throw this.notifyFatalError(e),e}return{...n,durability:"durable",eventId:r.eventId,cursor:r.cursor,event:i.data}})():{...n,durability:"ephemeral",event:I.parse(r)};this.sequence+=1;for(const e of this.listeners)try{e(o)}catch{}}notifyFatalError(e){for(const e of this.activeTurns.values())try{e.session.cancel()}catch{}for(const t of this.fatalErrorListeners)try{t(e)}catch{}}async driveTurn(e,t){const r={streamId:void 0,text:"",terminalEvent:void 0};let s;try{for await(const s of e.session.send(t))this.projectSessionEvent(e,r,s);s=r.terminalEvent??{type:"turn.completed"}}catch(e){s=r.terminalEvent??{type:"turn.failed",stage:"execute",message:A($(e))}}finally{this.activeTurns.get(e.threadId)===e&&this.activeTurns.delete(e.threadId),this.settleTurnOwner(e)}this.emitTurnTerminal(e,s)}findTurnOwner(e,t){const r=this.activeTurnOwners.get(e);if(void 0!==r)return r;const s=this.settledTurnOwners.get(e);return s===t&&(this.settledTurnOwners.delete(e),this.settledTurnOwners.set(e,s)),s}settleTurnOwner(e){if(this.activeTurnOwners.get(e.turnId)===e.threadId&&(this.activeTurnOwners.delete(e.turnId),!this.closing))for(this.settledTurnOwners.set(e.turnId,e.threadId);this.settledTurnOwners.size>this.idempotencyCacheEntries;){const e=this.settledTurnOwners.keys().next().value;if(void 0===e)return;this.settledTurnOwners.delete(e)}}projectSessionEvent(e,r,s){switch(s.type){case"message-start":{const s=g.parse(t());return r.streamId=s,r.text="",void this.emit(e.threadId,e.turnId,{type:"message.started",streamId:s})}case"text-delta":{const n=r.streamId??g.parse(t());return void 0===r.streamId&&(r.streamId=n,this.emit(e.threadId,e.turnId,{type:"message.started",streamId:n})),r.text+=s.delta,void this.emit(e.threadId,e.turnId,{type:"message.delta",streamId:n,delta:s.delta})}case"message-finish":{const n=r.streamId??g.parse(t());return this.emit(e.threadId,e.turnId,{type:"message.completed",streamId:n,text:A(s.text||r.text)}),void(r.terminalEvent??={type:"turn.completed"})}case"reasoning-delta":{const t=this.reasoningSummaryProjector?.(s.delta);return void(void 0!==t&&t.length>0&&this.emit(e.threadId,e.turnId,{type:"reasoning.summary.delta",reasoningId:s.reasoningId,delta:A(t)}))}case"tool-call":return void this.emit(e.threadId,e.turnId,{type:"tool.started",toolCallId:s.toolCallId,agentName:s.agentName,toolName:s.toolName,input:V(s.input,void 0)});case"tool-output-delta":return void this.emit(e.threadId,e.turnId,{type:"tool.output",toolCallId:s.toolCallId,agentName:s.agentName,toolName:s.toolName,stream:s.stream,delta:A(s.delta)});case"tool-result":return void this.emit(e.threadId,e.turnId,{type:"tool.completed",toolCallId:s.toolCallId,agentName:s.agentName,toolName:s.toolName,...void 0!==s.executionId?{operationId:c.parse(s.executionId)}:{},...void 0!==s.outcome?{outcome:this.projectToolOutcome(s.outcome)}:{},display:V(s.display,void 0)});case"confirmation-required":{const t=_(e.turnId,s);return this.pendingApprovals.set(t.id,{threadId:e.threadId,session:e.session,approval:t,expiresAt:s.expiresAt}),void this.emit(e.threadId,e.turnId,{type:"approval.required",approval:t})}case"user-input-required":{if(!this.userInputAvailable||void 0===e.session.resolveUserInput||void 0===e.session.cancelUserInput)return void e.session.cancelUserInput?.(s.requestId,"当前客户端未协商用户输入处理能力");const t=y.parse(s.form),r=y.parse(t),n={requestId:s.requestId,threadId:e.threadId,turnId:e.turnId,session:e.session,form:t,expiresAt:s.expiresAt};this.pendingUserInputs.set(s.requestId,n),e.status="waiting-for-user";return void(this.emitUserInputInteraction({type:"required",requestId:s.requestId,threadId:e.threadId,turnId:e.turnId,form:r,expiresAt:s.expiresAt})||this.cancelPendingUserInput(s.requestId,"当前没有可处理用户输入请求的客户端"))}case"user-input-settled":{const e=this.pendingUserInputs.get(s.requestId);if(void 0===e)return;return this.pendingUserInputs.delete(s.requestId),this.restoreRunningStatus(e),void this.emitUserInputInteraction({type:"settled",requestId:e.requestId,threadId:e.threadId,turnId:e.turnId,reason:"submitted"===s.status?"用户输入请求已结算":"用户输入请求已取消"})}case"turn-cancelled":r.terminalEvent??=void 0===e.interactionFailure?{type:"turn.cancelled",reason:s.reason,message:A(s.message)}:{type:"turn.failed",stage:"execute",message:A(e.interactionFailure)};break;case"error":r.terminalEvent??={type:"turn.failed",stage:s.stage,message:A(s.message)}}}projectToolOutcome(e){return{kind:e.kind,..."reason"in e&&void 0!==e.reason?{reason:A(e.reason)}:{}}}findPendingApproval(e){const t=this.pendingApprovals.get(e.approvalId);if(void 0!==t&&t.threadId===e.threadId&&t.approval.turnId===e.turnId)return t}requirePendingApproval(e){const t=this.findPendingApproval(e);if(void 0===t)throw new RuntimeServiceError(s.approvalNotFound,`Approval "${e.approvalId}" 不存在或已失效`);return t}emitApprovalResolved(e,t){const r="reason"in t&&void 0!==t.reason?{...t,reason:A(t.reason)}:t;this.emit(e.threadId,e.approval.turnId,{type:"approval.resolved",approvalId:e.approval.id,resolution:r})}cancelPendingApprovalsForTurn(e,t){const r=[...this.pendingApprovals.values()].filter(t=>t.approval.turnId===e);let s;for(const e of r)try{this.cancelPendingApproval({threadId:e.threadId,turnId:e.approval.turnId,approvalId:e.approval.id},t)}catch(e){s??=e}if(void 0!==s)throw s}cancelPendingUserInputsForTurn(e,t){const r=[...this.pendingUserInputs.values()].filter(t=>t.turnId===e).map(e=>e.requestId);for(const e of r)this.cancelPendingUserInput(e,t)}cancelAllPendingUserInputs(e){for(const t of[...this.pendingUserInputs.keys()])this.cancelPendingUserInput(t,e)}restoreRunningStatus(e){const t=this.activeTurns.get(e.threadId);void 0!==t&&t.turnId===e.turnId&&"waiting-for-user"===t.status&&(t.status="running")}emitUserInputInteraction(e){let t=!1;for(const r of this.userInputListeners)try{r(e),t=!0}catch{}return t}emitTurnTerminal(e,t){this.cancelPendingUserInputsForTurn(e.turnId,`Turn 已终止:${t.type}`),this.cancelPendingApprovalsForTurn(e.turnId,"turn.cancelled"===t.type&&"timeout"===t.reason?{status:"expired",reason:"Turn 已超时"}:{status:"cancelled",reason:`Turn 已终止:${t.type}`}),this.emit(e.threadId,e.turnId,t)}}
@@ -1,4 +1,5 @@
1
1
  import { type ModelMessage } from "ai";
2
+ import { type RuntimeDurableEventV13, type RuntimeEventCursor, type RuntimeEventId } from "@roll-agent/protocol";
2
3
  import { type ToolExecutionPersistenceMetadata, type ToolExecutionRecord } from "../tool-bridge/tool-execution-record.ts";
3
4
  import { COMPACTION_TRANSCRIPT_COMPLETENESS, type ArchivedTranscriptMessage, type CompactionCheckpoint, type CompactionCheckpointDraftInput, type CompactionSemanticEvidenceWatermarks } from "../engine/compaction-checkpoint.ts";
4
5
  import { type CompactionSemanticState } from "../engine/compaction-semantic-state.ts";
@@ -7,6 +8,39 @@ export declare const TOOL_EXECUTION_RETENTION_POLICY: {
7
8
  readonly maxRecordsPerThread: 2000;
8
9
  readonly maxAgeMs: number;
9
10
  };
11
+ export declare const RUNTIME_EVENT_RETENTION_POLICY: {
12
+ readonly maxBytesPerThread: number;
13
+ readonly maxRecordsPerThread: 10000;
14
+ readonly maxAgeMs: number;
15
+ };
16
+ export interface AppendRuntimeEventInput {
17
+ readonly threadId: string;
18
+ readonly turnId?: string;
19
+ readonly timestamp: string;
20
+ readonly event: RuntimeDurableEventV13;
21
+ }
22
+ export interface StoredRuntimeEvent {
23
+ readonly eventId: RuntimeEventId;
24
+ readonly cursor: RuntimeEventCursor;
25
+ readonly threadSequence: number;
26
+ readonly threadId: string;
27
+ readonly turnId?: string;
28
+ readonly timestamp: string;
29
+ readonly event: RuntimeDurableEventV13;
30
+ }
31
+ export interface ResumeRuntimeEventsResult {
32
+ readonly events: readonly StoredRuntimeEvent[];
33
+ readonly throughCursor: RuntimeEventCursor | null;
34
+ readonly replayedCount: number;
35
+ }
36
+ export declare class RuntimeEventCursorExpiredError extends Error {
37
+ readonly cursor: RuntimeEventCursor | null;
38
+ constructor(cursor: RuntimeEventCursor | null);
39
+ }
40
+ export declare class RuntimeEventCursorGapError extends Error {
41
+ readonly cursor: RuntimeEventCursor;
42
+ constructor(cursor: RuntimeEventCursor);
43
+ }
10
44
  export declare const TRANSCRIPT_ENTRY_KINDS: readonly ["message", "tool_execution"];
11
45
  export type TranscriptEntryKind = (typeof TRANSCRIPT_ENTRY_KINDS)[number];
12
46
  export type TranscriptCompleteness = (typeof COMPACTION_TRANSCRIPT_COMPLETENESS)[number];
@@ -108,6 +142,8 @@ export declare class ThreadStore {
108
142
  private migrateToolExecutionPersistenceInTransaction;
109
143
  private enforceAllToolExecutionRetentionInTransaction;
110
144
  private migrateExplicitSkillCheckpointPersistenceInTransaction;
145
+ private initializeMissingRuntimeEventStatesInTransaction;
146
+ private enforceAllRuntimeEventRetentionInTransaction;
111
147
  createThread(input?: CreateThreadInput): string;
112
148
  hasThread(id: string): boolean;
113
149
  getThread(id: string): ThreadRecord | undefined;
@@ -116,6 +152,12 @@ export declare class ThreadStore {
116
152
  countTranscriptMessages(threadId: string): number;
117
153
  updateTitle(threadId: string, title: string): void;
118
154
  deleteThread(threadId: string): void;
155
+ appendRuntimeEvent(input: AppendRuntimeEventInput): StoredRuntimeEvent;
156
+ getRuntimeEventCursor(threadId: string): RuntimeEventCursor | null;
157
+ resumeRuntimeEvents(threadId: string, afterCursor: RuntimeEventCursor | null): ResumeRuntimeEventsResult;
158
+ countRuntimeEvents(threadId: string): number;
159
+ private validateRuntimeEventCursor;
160
+ private enforceRuntimeEventRetentionInTransaction;
119
161
  appendMessages(threadId: string, messages: readonly ModelMessage[]): void;
120
162
  replaceMessages(threadId: string, messages: readonly ModelMessage[]): void;
121
163
  getMessages(threadId: string): ModelMessage[];
@@ -1 +1 @@
1
- import{chmodSync as e,existsSync as t,mkdirSync as n}from"node:fs";import{homedir as r}from"node:os";import{resolve as s}from"node:path";import{randomUUID as a}from"node:crypto";import{DatabaseSync as o}from"node:sqlite";import{modelMessageSchema as i}from"ai";import{parsePersistedToolExecutionRecord as c,prepareToolExecutionRecordForPersistence as E,redactSecretText as d}from"../tool-bridge/tool-execution-record.js";import{COMPACTION_CHECKPOINT_VERSION as h,COMPACTION_TRANSCRIPT_COMPLETENESS as T,TRANSCRIPT_MESSAGE_PROVENANCES as u,UnsupportedCompactionCheckpointVersionError as p,archivedTranscriptMessageSchema as _,createCompactionCheckpoint as l,createCompactionCheckpointDraft as m,parseCompactionCheckpoint as g}from"../engine/compaction-checkpoint.js";import{legacyCompactionTranscriptFragmentsSchema as S}from"../engine/compaction-semantic-state.js";import{createLegacyCompactionTranscriptMessage as N}from"../engine/compactor.js";import{ACTIVE_TOOL_PROTOCOL_REPAIR_STATUS as R,repairActiveToolProtocol as O}from"../engine/tool-protocol-repair.js";import{sanitizePersistedExplicitSkillCheckpoint as I}from"../engine/explicit-skill-context.js";const L=4,C=100,A=500,x=50,f=500,q=100,b=15e3;export const TOOL_EXECUTION_RETENTION_POLICY={maxBytesPerThread:16777216,maxRecordsPerThread:2e3,maxAgeMs:2592e6};export const TRANSCRIPT_ENTRY_KINDS=["message","tool_execution"];export function expandTilde(e){return e.startsWith("~/")?s(r(),e.slice(2)):e}export function defaultThreadsDir(){return s(r(),".roll-agent","threads")}function M(e){return e.prepare("PRAGMA foreign_key_list(messages)").all().some(e=>"threads"===e.table&&"thread_id"===e.from&&"id"===e.to&&"CASCADE"===e.on_delete.toUpperCase())}function v(e,t){return"valid"===e.status?e:"fallback"===e.status?{status:"fallback",reason:e.reason,...void 0!==t?{lastValidCheckpointId:t.id}:{}}:{status:"skipped",...void 0!==t?{lastValidCheckpointId:t.id}:{}}}function w(e){if(Number.isInteger(e.schema_version)&&e.schema_version>h)throw new p(e.schema_version);if(!Number.isInteger(e.schema_version))throw new Error("Invalid persisted compaction checkpoint schema version");const t=g(JSON.parse(e.checkpoint_json));if(t.version!==e.schema_version)throw new Error(`Compaction checkpoint schema version mismatch: row v${String(e.schema_version)}, payload v${String(t.version)}`);return t}function D(e){return{id:e.id,title:e.title??void 0,model:e.model??void 0,createdAt:e.created_at,updatedAt:e.updated_at}}export class ThreadStore{db;constructor(r=defaultThreadsDir()){const a=expandTilde(r);t(a)||n(a,{recursive:!0,mode:448}),"win32"!==process.platform&&e(a,448);const i=s(a,"threads.db");if(this.db=new o(i),"win32"!==process.platform)try{e(i,384)}catch(e){throw this.db.close(),e}this.init()}init(){this.db.exec(`PRAGMA busy_timeout = ${String(b)};\n PRAGMA foreign_keys = ON;\n PRAGMA secure_delete = ON;`);const e=this.db.prepare("PRAGMA user_version").get();if(e.user_version>4)throw new Error(`ThreadStore schema v${String(e.user_version)} 高于当前支持的 v${String(4)}`);this.db.exec("BEGIN IMMEDIATE");try{this.db.exec("CREATE TABLE IF NOT EXISTS threads (\n id TEXT PRIMARY KEY,\n title TEXT,\n model TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n CREATE TABLE IF NOT EXISTS messages (\n thread_id TEXT NOT NULL,\n idx INTEGER NOT NULL,\n role TEXT NOT NULL,\n content_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, idx),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );"),M(this.db)||this.db.exec("CREATE TABLE messages_with_thread_fk (\n thread_id TEXT NOT NULL,\n idx INTEGER NOT NULL,\n role TEXT NOT NULL,\n content_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, idx),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n INSERT INTO messages_with_thread_fk\n (thread_id, idx, role, content_json, created_at)\n SELECT messages.thread_id, messages.idx, messages.role,\n messages.content_json, messages.created_at\n FROM messages\n INNER JOIN threads ON threads.id = messages.thread_id;\n DROP TABLE messages;\n ALTER TABLE messages_with_thread_fk RENAME TO messages;"),this.db.exec("CREATE TABLE IF NOT EXISTS tool_executions (\n thread_id TEXT NOT NULL,\n sequence INTEGER NOT NULL,\n id TEXT NOT NULL,\n tool_call_id TEXT NOT NULL,\n agent_name TEXT NOT NULL,\n tool_name TEXT NOT NULL,\n record_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, id),\n UNIQUE (thread_id, sequence),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE INDEX IF NOT EXISTS idx_tool_executions_thread_call\n ON tool_executions(thread_id, tool_call_id, sequence);\n CREATE TABLE IF NOT EXISTS thread_tool_execution_state (\n thread_id TEXT PRIMARY KEY,\n next_sequence INTEGER NOT NULL CHECK (next_sequence >= 0),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS transcript_messages (\n thread_id TEXT NOT NULL,\n sequence INTEGER NOT NULL,\n role TEXT NOT NULL,\n message_json TEXT NOT NULL,\n provenance TEXT NOT NULL CHECK (provenance IN ('native', 'legacy_snapshot')),\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, sequence),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS thread_transcript_state (\n thread_id TEXT PRIMARY KEY,\n completeness TEXT NOT NULL CHECK (completeness IN ('complete', 'legacy_snapshot')),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS compaction_checkpoints (\n id TEXT PRIMARY KEY,\n thread_id TEXT NOT NULL,\n generation INTEGER NOT NULL,\n schema_version INTEGER NOT NULL,\n message_from_sequence INTEGER NOT NULL,\n message_through_sequence INTEGER NOT NULL,\n tool_from_sequence INTEGER NOT NULL,\n tool_through_sequence INTEGER NOT NULL,\n checkpoint_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n UNIQUE (thread_id, generation),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE INDEX IF NOT EXISTS idx_compaction_checkpoints_thread_generation\n ON compaction_checkpoints(thread_id, generation DESC);"),this.db.exec("INSERT OR IGNORE INTO thread_tool_execution_state (thread_id, next_sequence)\n SELECT id, 0 FROM threads;\n UPDATE thread_tool_execution_state\n SET next_sequence = MAX(\n next_sequence,\n COALESCE((\n SELECT MAX(tool_executions.sequence) + 1\n FROM tool_executions\n WHERE tool_executions.thread_id = thread_tool_execution_state.thread_id\n ), 0)\n );"),e.user_version<3&&this.db.exec("INSERT OR IGNORE INTO thread_transcript_state (thread_id, completeness)\n SELECT id, 'legacy_snapshot' FROM threads;\n INSERT OR IGNORE INTO transcript_messages\n (thread_id, sequence, role, message_json, provenance, created_at)\n SELECT thread_id, idx, role, content_json, 'legacy_snapshot', created_at\n FROM messages;"),e.user_version<4&&(this.migrateToolExecutionPersistenceInTransaction(),this.migrateExplicitSkillCheckpointPersistenceInTransaction()),this.enforceAllToolExecutionRetentionInTransaction((new Date).toISOString()),this.db.exec(`PRAGMA user_version = ${String(4)};`),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}migrateToolExecutionPersistenceInTransaction(){const e=this.db.prepare("SELECT rowid AS storage_rowid, record_json\n FROM tool_executions\n WHERE rowid > ?\n ORDER BY rowid ASC\n LIMIT 100"),t=this.db.prepare("UPDATE tool_executions\n SET tool_call_id = ?, agent_name = ?, tool_name = ?, record_json = ?, created_at = ?\n WHERE rowid = ?");let n=0;for(;;){const r=e.all(n);if(0===r.length)break;for(const e of r){const r=c(JSON.parse(e.record_json));t.run(r.toolCallId,r.agentName,r.toolName,JSON.stringify(r),r.createdAt,e.storage_rowid),n=e.storage_rowid}}}enforceAllToolExecutionRetentionInTransaction(e){const t=this.db.prepare("SELECT DISTINCT thread_id AS id FROM tool_executions").all();for(const{id:n}of t)this.enforceToolExecutionRetentionInTransaction(n,e)}migrateExplicitSkillCheckpointPersistenceInTransaction(){const e=[{table:"messages",column:"content_json"},{table:"transcript_messages",column:"message_json"}];for(const t of e){const e=this.db.prepare(`SELECT rowid AS storage_rowid, ${t.column} AS message_json\n FROM ${t.table}\n WHERE ${t.column} LIKE '%"modelUserContent"%'`).all(),n=this.db.prepare(`UPDATE ${t.table} SET ${t.column} = ? WHERE rowid = ?`);for(const t of e){const e=i.parse(JSON.parse(t.message_json)),r=I(e);n.run(JSON.stringify(r),t.storage_rowid)}}}createThread(e={}){const t=e.id??a(),n=(new Date).toISOString();this.db.exec("BEGIN IMMEDIATE");try{return this.db.prepare("INSERT INTO threads (id, title, model, created_at, updated_at) VALUES (?, ?, ?, ?, ?)").run(t,e.title??null,e.model??null,n,n),this.db.prepare("INSERT INTO thread_transcript_state (thread_id, completeness) VALUES (?, ?)").run(t,T[0]),this.db.prepare("INSERT INTO thread_tool_execution_state (thread_id, next_sequence) VALUES (?, 0)").run(t),this.db.exec("COMMIT"),t}catch(e){throw this.db.exec("ROLLBACK"),e}}hasThread(e){return void 0!==this.db.prepare("SELECT 1 FROM threads WHERE id = ?").get(e)}getThread(e){const t=this.db.prepare("SELECT * FROM threads WHERE id = ?").get(e);return t?D(t):void 0}listThreads(){return this.db.prepare("SELECT * FROM threads ORDER BY updated_at DESC, rowid DESC").all().map(D)}countMessages(e){return this.db.prepare("SELECT COUNT(*) AS n FROM messages WHERE thread_id = ?").get(e).n}countTranscriptMessages(e){return this.db.prepare("SELECT COUNT(*) AS n FROM transcript_messages WHERE thread_id = ?").get(e).n}updateTitle(e,t){this.db.prepare("UPDATE threads SET title = ? WHERE id = ?").run(t,e)}deleteThread(e){this.db.prepare("DELETE FROM threads WHERE id = ?").run(e)}appendMessages(e,t){if(0===t.length)return;if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=t.map(e=>I(i.parse(e))),r=(new Date).toISOString(),s=this.db.prepare("INSERT INTO messages (thread_id, idx, role, content_json, created_at) VALUES (?, ?, ?, ?, ?)"),a=this.db.prepare("INSERT INTO transcript_messages\n (thread_id, sequence, role, message_json, provenance, created_at)\n VALUES (?, ?, ?, ?, ?, ?)");this.db.exec("BEGIN IMMEDIATE");try{const t=this.db.prepare("SELECT COALESCE(MAX(idx), -1) AS maxIdx FROM messages WHERE thread_id = ?").get(e),o=this.db.prepare("SELECT COALESCE(MAX(sequence), -1) AS maxSequence FROM transcript_messages WHERE thread_id = ?").get(e);let i=t.maxIdx+1,c=o.maxSequence+1;for(const t of n){const n=JSON.stringify(t);s.run(e,i,t.role,n,r),a.run(e,c,t.role,n,u[0],r),i+=1,c+=1}this.db.prepare("UPDATE threads SET updated_at = ? WHERE id = ?").run(r,e),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}replaceMessages(e,t){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=t.map(e=>i.parse(e)),r=(new Date).toISOString();this.db.exec("BEGIN IMMEDIATE");try{this.replaceMessagesInTransaction(e,n,r),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}getMessages(e){return this.db.prepare("SELECT content_json FROM messages WHERE thread_id = ? ORDER BY idx ASC").all(e).map(e=>i.parse(JSON.parse(e.content_json)))}replaceMessagesInTransaction(e,t,n){const r=this.db.prepare("INSERT INTO messages (thread_id, idx, role, content_json, created_at) VALUES (?, ?, ?, ?, ?)");this.db.prepare("DELETE FROM messages WHERE thread_id = ?").run(e),t.forEach((t,s)=>{const a=I(t);r.run(e,s,a.role,JSON.stringify(a),n)}),this.db.prepare("UPDATE threads SET updated_at = ? WHERE id = ?").run(n,e)}appendToolExecution(e,t){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=E(t),r=JSON.stringify(n),s=(new Date).toISOString();this.db.exec("BEGIN IMMEDIATE");try{const t=this.db.prepare("SELECT next_sequence FROM thread_tool_execution_state WHERE thread_id = ?").get(e);if(void 0===t)throw new Error(`Thread "${e}" 缺少 Tool execution sequence state`);const a=t.next_sequence;return this.db.prepare("INSERT INTO tool_executions\n (thread_id, sequence, id, tool_call_id, agent_name, tool_name, record_json, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)").run(e,a,n.id,n.toolCallId,n.agentName,n.toolName,r,n.createdAt),this.db.prepare("UPDATE thread_tool_execution_state SET next_sequence = ? WHERE thread_id = ?").run(a+1,e),this.enforceToolExecutionRetentionInTransaction(e,s),this.db.prepare("UPDATE threads SET updated_at = ? WHERE id = ?").run(s,e),this.db.exec("COMMIT"),a}catch(e){throw this.db.exec("ROLLBACK"),e}}enforceToolExecutionRetentionInTransaction(e,t){const n=new Date(Date.parse(t)-TOOL_EXECUTION_RETENTION_POLICY.maxAgeMs).toISOString();let r=!1;const s=this.db.prepare("DELETE FROM tool_executions\n WHERE thread_id = ?\n AND created_at < ?").run(e,n);r=Number(s.changes)>0;const a=this.db.prepare("SELECT sequence\n FROM (\n SELECT sequence,\n ROW_NUMBER() OVER (ORDER BY sequence DESC) AS retained_rank,\n SUM(length(CAST(record_json AS BLOB))) OVER (\n ORDER BY sequence DESC\n ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n ) AS retained_bytes\n FROM tool_executions\n WHERE thread_id = ?\n )\n WHERE retained_rank > ? OR retained_bytes > ?").all(e,TOOL_EXECUTION_RETENTION_POLICY.maxRecordsPerThread,TOOL_EXECUTION_RETENTION_POLICY.maxBytesPerThread);if(a.length>0){const t=this.db.prepare("DELETE FROM tool_executions WHERE thread_id = ? AND sequence = ?");for(const n of a)t.run(e,n.sequence);r=!0}r&&this.db.prepare("INSERT INTO thread_transcript_state (thread_id, completeness)\n VALUES (?, 'legacy_snapshot')\n ON CONFLICT(thread_id) DO UPDATE SET completeness = 'legacy_snapshot'").run(e)}listToolExecutions(e,t={}){const n=t.afterSequence??-1,r=t.throughSequence??Number.MAX_SAFE_INTEGER,s=t.limit??C;if(!Number.isInteger(n)||n<-1)throw new Error("afterSequence 必须是大于等于 -1 的整数");if(!Number.isInteger(s)||s<1||s>A)throw new Error(`limit 必须是 1-${String(A)} 之间的整数`);if(!Number.isInteger(r)||r<-1)throw new Error("throughSequence 必须是大于等于 -1 的整数");if(r<=n)return[];return this.db.prepare("SELECT sequence, record_json\n FROM tool_executions\n WHERE thread_id = ?\n AND sequence > ?\n AND sequence <= ?\n AND (? IS NULL OR tool_call_id = ?)\n ORDER BY sequence ASC\n LIMIT ?").all(e,n,r,t.toolCallId??null,t.toolCallId??null,s).map(e=>({...c(JSON.parse(e.record_json)),sequence:e.sequence}))}listRecentToolExecutions(e,t={}){const n=t.beforeSequence??Number.MAX_SAFE_INTEGER,r=t.limit??C;this.validateRecentPage(n,r,A);const s=this.db.prepare("SELECT sequence, record_json\n FROM tool_executions\n WHERE thread_id = ?\n AND sequence < ?\n ORDER BY sequence DESC\n LIMIT ?").all(e,n,r+1),a=s.length>r,o=s.slice(0,r).map(e=>({...c(JSON.parse(e.record_json)),sequence:e.sequence})).reverse();return{entries:o,nextBeforeSequence:a?o[0]?.sequence:void 0}}getToolExecution(e,t){const n=this.db.prepare("SELECT sequence, record_json FROM tool_executions WHERE thread_id = ? AND id = ?").get(e,t);if(n)return{...c(JSON.parse(n.record_json)),sequence:n.sequence}}getTranscriptCompleteness(e){const t=this.db.prepare("SELECT completeness FROM thread_transcript_state WHERE thread_id = ?").get(e);return t?.completeness===T[0]?T[0]:T[1]}listTranscriptMessages(e,t={}){const n=t.afterSequence??-1,r=t.throughSequence??Number.MAX_SAFE_INTEGER,s=t.limit??x;if(this.validateTranscriptRange(n,r,s,f),r<=n)return[];return this.db.prepare("SELECT sequence, provenance, created_at, message_json\n FROM transcript_messages\n WHERE thread_id = ?\n AND sequence > ?\n AND sequence <= ?\n ORDER BY sequence ASC\n LIMIT ?").all(e,n,r,s).map(e=>_.parse({sequence:e.sequence,provenance:e.provenance,createdAt:e.created_at,message:JSON.parse(e.message_json)}))}listRecentTranscriptMessages(e,t={}){const n=t.beforeSequence??Number.MAX_SAFE_INTEGER,r=t.limit??x;this.validateRecentPage(n,r,f);const s=this.db.prepare("SELECT sequence, provenance, created_at, message_json\n FROM transcript_messages\n WHERE thread_id = ?\n AND sequence < ?\n ORDER BY sequence DESC\n LIMIT ?").all(e,n,r+1),a=s.length>r,o=s.slice(0,r).map(e=>_.parse({sequence:e.sequence,provenance:e.provenance,createdAt:e.created_at,message:JSON.parse(e.message_json)})).reverse();return{entries:o,nextBeforeSequence:a?o[0]?.sequence:void 0}}getLatestCheckpoint(e){const t=this.db.prepare("SELECT schema_version, checkpoint_json\n FROM compaction_checkpoints\n WHERE thread_id = ?\n ORDER BY generation DESC").all(e);for(const n of t)try{return this.withCurrentTranscriptCompleteness(e,w(n))}catch(e){if(e instanceof p)throw e}}getLatestSummaryCheckpoint(e){const t=this.db.prepare("SELECT schema_version, checkpoint_json\n FROM compaction_checkpoints\n WHERE thread_id = ?\n ORDER BY generation DESC").all(e);for(const n of t)try{const t=w(n);if("valid"===t.summary.status)return this.withCurrentTranscriptCompleteness(e,t)}catch(e){if(e instanceof p)throw e}}getCheckpoint(e,t){const n=this.db.prepare("SELECT schema_version, checkpoint_json\n FROM compaction_checkpoints\n WHERE thread_id = ? AND id = ?").get(e,t);if(void 0!==n)try{return this.withCurrentTranscriptCompleteness(e,w(n))}catch(e){if(e instanceof p)throw e;return}}loadSessionState(e){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);return{messages:O(this.getMessages(e)).messages,checkpoint:this.getLatestCheckpoint(e),transcriptCompleteness:this.getTranscriptCompleteness(e)}}commitCompaction(e,t){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=t.messages.map(e=>i.parse(e)),r=t.expectedActiveMessages.map(e=>i.parse(e)),s=O(n);if(s.status!==R.valid){const e=[...s.removedToolCallIds,...s.removedToolResultIds].filter((e,t,n)=>n.indexOf(e)===t).sort((e,t)=>e.localeCompare(t));throw new Error(`Compaction active projection contains malformed Tool protocol: ${e.join(", ")}`)}const a=m(t.draft),o=S.parse(t.legacySnapshotTranscriptFragments??[]),c=S.parse(o.map(e=>d(e))).map(e=>i.parse(N(e))),E=(new Date).toISOString();this.db.exec("BEGIN IMMEDIATE");try{const s=O(this.getMessages(e)).messages;if(JSON.stringify(s)!==JSON.stringify(r))throw new Error("Compaction active projection changed after the draft snapshot was captured");const o=this.getLatestCheckpoint(e);if(o?.id!==t.expectedLatestCheckpointId)throw new Error("Compaction checkpoint ancestry changed after the draft snapshot was captured");const i=this.getLatestSummaryCheckpoint(e),d=v(a.summary,i),h=this.getTranscriptCompleteness(e),p=m({...a,summary:d,...h===T[1]&&"valid"===a.toolState.integrityStatus?{toolState:{...a.toolState,integrityStatus:"sanitized"}}:{}}),_=this.db.prepare("SELECT COALESCE(MAX(generation), 0) AS maxGeneration\n FROM compaction_checkpoints\n WHERE thread_id = ?").get(e),g=this.maxTranscriptMessageSequence(e),S=this.maxToolExecutionSequence(e),N=t.evidenceWatermarks.transcriptMessagesThroughSequence,R=t.evidenceWatermarks.toolExecutionsThroughSequence,I=o?.transcript.messages.throughSequence??-1,L=o?.transcript.toolExecutions.throughSequence??-1,C=2===o?.version?o.semanticEvidence.messagesThroughSequence:-1,A=2===o?.version?o.semanticEvidence.toolExecutionsThroughSequence:-1;if(c.length>0&&1!==o?.version)throw new Error("Legacy snapshot transcript archive requires a V1 parent checkpoint");if(!Number.isInteger(N)||N<I||N>g)throw new Error(`Compaction transcript message watermark ${String(N)} is outside the available range ${String(I)}-${String(g)}`);if(!Number.isInteger(t.semanticEvidenceWatermarks.messagesThroughSequence)||t.semanticEvidenceWatermarks.messagesThroughSequence<C||t.semanticEvidenceWatermarks.messagesThroughSequence>N)throw new Error(`Compaction semantic message watermark ${String(t.semanticEvidenceWatermarks.messagesThroughSequence)} is outside the available range ${String(C)}-${String(N)}`);if(!Number.isInteger(t.semanticEvidenceWatermarks.toolExecutionsThroughSequence)||t.semanticEvidenceWatermarks.toolExecutionsThroughSequence<A||t.semanticEvidenceWatermarks.toolExecutionsThroughSequence>R)throw new Error(`Compaction semantic Tool watermark ${String(t.semanticEvidenceWatermarks.toolExecutionsThroughSequence)} is outside the available range ${String(A)}-${String(R)}`);if(!Number.isInteger(R)||R<L||R>S)throw new Error(`Compaction tool execution watermark ${String(R)} is outside the available range ${String(L)}-${String(S)}`);let x=N;if(c.length>0){if(N!==g)throw new Error("Legacy snapshot transcript archive requires the latest message watermark");const t=this.db.prepare("INSERT INTO transcript_messages\n (thread_id, sequence, role, message_json, provenance, created_at)\n VALUES (?, ?, ?, ?, ?, ?)");let n=g+1;for(const r of c)t.run(e,n,r.role,JSON.stringify(r),u[1],E),n+=1;x=n-1}const f={messages:{fromSequenceExclusive:I,throughSequence:x},toolExecutions:{fromSequenceExclusive:L,throughSequence:R},completeness:h},q=l({draft:p,semanticState:t.semanticState,semanticEvidence:t.semanticEvidenceWatermarks,generation:_.maxGeneration+1,transcript:f,...void 0!==o?{previousCheckpointId:o.id}:{},createdAt:E});return this.db.prepare("INSERT INTO compaction_checkpoints\n (id, thread_id, generation, schema_version,\n message_from_sequence, message_through_sequence,\n tool_from_sequence, tool_through_sequence,\n checkpoint_json, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(q.id,e,q.generation,q.version,q.transcript.messages.fromSequenceExclusive,q.transcript.messages.throughSequence,q.transcript.toolExecutions.fromSequenceExclusive,q.transcript.toolExecutions.throughSequence,JSON.stringify(q),q.createdAt),this.replaceMessagesInTransaction(e,n,E),this.db.exec("COMMIT"),q}catch(e){throw this.db.exec("ROLLBACK"),e}}readCheckpointTranscript(e,t){if("message"!==t.kind&&"tool_execution"!==t.kind)throw new Error("kind 必须是 message 或 tool_execution");const n=this.getCheckpoint(e,t.checkpointId);if(void 0===n)throw new Error("checkpoint 不存在、已损坏或不属于当前 thread");const r=t.limit??x,s="message"===t.kind?n.transcript.messages:n.transcript.toolExecutions,a=Math.max(t.afterSequence??s.fromSequenceExclusive,s.fromSequenceExclusive);this.validateTranscriptRange(a,s.throughSequence,r,q);const o="message"===t.kind?this.listTranscriptMessages(e,{afterSequence:a,throughSequence:s.throughSequence,limit:r+1}).map(e=>({kind:"message",...e})):this.listToolExecutions(e,{afterSequence:a,throughSequence:s.throughSequence,limit:Math.min(r+1,A)}).map(e=>({kind:"tool_execution",...e})),i=o.length>r,c=i?o.slice(0,r):o,E=c.at(-1)?.sequence;return{checkpointId:n.id,kind:t.kind,entries:c,...i&&void 0!==E?{nextAfterSequence:E}:{},...void 0!==n.previousCheckpointId?{previousCheckpointId:n.previousCheckpointId}:{},completeness:n.transcript.completeness}}maxTranscriptMessageSequence(e){return this.db.prepare("SELECT COALESCE(MAX(sequence), -1) AS maxSequence FROM transcript_messages WHERE thread_id = ?").get(e).maxSequence}maxToolExecutionSequence(e){const t=this.db.prepare("SELECT next_sequence FROM thread_tool_execution_state WHERE thread_id = ?").get(e);return(t?.next_sequence??0)-1}withCurrentTranscriptCompleteness(e,t){const n=T[1],r=t.transcript.completeness===n||this.getTranscriptCompleteness(e)===n?n:T[0],s=r===n&&"valid"===t.toolState.integrityStatus?{...t.toolState,integrityStatus:"sanitized"}:t.toolState;return t.transcript.completeness===r&&t.toolState===s?t:{...t,toolState:s,transcript:{...t.transcript,completeness:r}}}validateTranscriptRange(e,t,n,r){if(!Number.isInteger(e)||e<-1)throw new Error("afterSequence 必须是大于等于 -1 的整数");if(!Number.isInteger(t)||t<-1)throw new Error("throughSequence 必须是大于等于 -1 的整数");if(!Number.isInteger(n)||n<1||n>r)throw new Error(`limit 必须是 1-${String(r)} 之间的整数`)}validateRecentPage(e,t,n){if(!Number.isSafeInteger(e)||e<0||e>Number.MAX_SAFE_INTEGER)throw new Error("beforeSequence 必须是非负安全整数");if(!Number.isInteger(t)||t<1||t>n)throw new Error(`limit 必须是 1-${String(n)} 之间的整数`)}close(){this.db.close()}}
1
+ import{chmodSync as e,existsSync as t,mkdirSync as n}from"node:fs";import{homedir as r}from"node:os";import{resolve as s}from"node:path";import{randomUUID as a}from"node:crypto";import{DatabaseSync as i}from"node:sqlite";import{modelMessageSchema as o}from"ai";import{RUNTIME_V13_MAX_DURABLE_EVENT_RECORD_BYTES as d,RUNTIME_V13_MAX_DURABLE_EVENT_RECORDS as c,runtimeDurableEventV13Schema as E,runtimeEventCursorSchema as _,runtimeEventIdSchema as h,timestampSchema as u}from"@roll-agent/protocol";import{parsePersistedToolExecutionRecord as T,prepareToolExecutionRecordForPersistence as p,redactSecretText as m}from"../tool-bridge/tool-execution-record.js";import{COMPACTION_CHECKPOINT_VERSION as l,COMPACTION_TRANSCRIPT_COMPLETENESS as N,TRANSCRIPT_MESSAGE_PROVENANCES as R,UnsupportedCompactionCheckpointVersionError as S,archivedTranscriptMessageSchema as g,createCompactionCheckpoint as I,createCompactionCheckpointDraft as O,parseCompactionCheckpoint as L}from"../engine/compaction-checkpoint.js";import{legacyCompactionTranscriptFragmentsSchema as v}from"../engine/compaction-semantic-state.js";import{createLegacyCompactionTranscriptMessage as C}from"../engine/compactor.js";import{ACTIVE_TOOL_PROTOCOL_REPAIR_STATUS as f,repairActiveToolProtocol as x}from"../engine/tool-protocol-repair.js";import{sanitizePersistedExplicitSkillCheckpoint as A}from"../engine/explicit-skill-context.js";const q=5,b=100,M=500,w=50,D=500,U=100,F=15e3;export const TOOL_EXECUTION_RETENTION_POLICY={maxBytesPerThread:16777216,maxRecordsPerThread:2e3,maxAgeMs:2592e6};export const RUNTIME_EVENT_RETENTION_POLICY={maxBytesPerThread:d,maxRecordsPerThread:c,maxAgeMs:2592e6};export class RuntimeEventCursorExpiredError extends Error{cursor;constructor(e){super("Runtime event cursor is older than the retained event prefix"),this.name="RuntimeEventCursorExpiredError",this.cursor=e}}export class RuntimeEventCursorGapError extends Error{cursor;constructor(e){super("Runtime event cursor does not belong to the current Thread event log"),this.name="RuntimeEventCursorGapError",this.cursor=e}}export const TRANSCRIPT_ENTRY_KINDS=["message","tool_execution"];export function expandTilde(e){return e.startsWith("~/")?s(r(),e.slice(2)):e}export function defaultThreadsDir(){return s(r(),".roll-agent","threads")}function k(e){return e.prepare("PRAGMA foreign_key_list(messages)").all().some(e=>"threads"===e.table&&"thread_id"===e.from&&"id"===e.to&&"CASCADE"===e.on_delete.toUpperCase())}function y(e,t){return"valid"===e.status?e:"fallback"===e.status?{status:"fallback",reason:e.reason,...void 0!==t?{lastValidCheckpointId:t.id}:{}}:{status:"skipped",...void 0!==t?{lastValidCheckpointId:t.id}:{}}}function W(e){if(Number.isInteger(e.schema_version)&&e.schema_version>l)throw new S(e.schema_version);if(!Number.isInteger(e.schema_version))throw new Error("Invalid persisted compaction checkpoint schema version");const t=L(JSON.parse(e.checkpoint_json));if(t.version!==e.schema_version)throw new Error(`Compaction checkpoint schema version mismatch: row v${String(e.schema_version)}, payload v${String(t.version)}`);return t}function X(e){return{id:e.id,title:e.title??void 0,model:e.model??void 0,createdAt:e.created_at,updatedAt:e.updated_at}}function H(){return h.parse(a())}function P(e,t,n){return _.parse(`rte1:${e}:${String(t)}:${n}`)}function B(e){const t=_.safeParse(e);if(!t.success)return;const n=t.data.split(":"),r=Number(n[2]),s=h.safeParse(n[3]);return void 0===n[1]||!Number.isSafeInteger(r)||r<0||!s.success?void 0:{eventLogId:n[1],threadSequence:r,eventId:s.data}}function G(e){return new Date(Date.parse(u.parse(e))).toISOString()}function Y(e,t,n){const r=h.parse(n.event_id);return{eventId:r,cursor:P(e,n.thread_sequence,r),threadSequence:n.thread_sequence,threadId:t,...null===n.turn_id?{}:{turnId:n.turn_id},timestamp:n.event_timestamp,event:E.parse(JSON.parse(n.event_json))}}export class ThreadStore{db;constructor(r=defaultThreadsDir()){const a=expandTilde(r);t(a)||n(a,{recursive:!0,mode:448}),"win32"!==process.platform&&e(a,448);const o=s(a,"threads.db");if(this.db=new i(o),"win32"!==process.platform)try{e(o,384)}catch(e){throw this.db.close(),e}this.init()}init(){this.db.exec(`PRAGMA busy_timeout = ${String(F)};\n PRAGMA foreign_keys = ON;\n PRAGMA secure_delete = ON;`);const e=this.db.prepare("PRAGMA user_version").get();if(e.user_version>5)throw new Error(`ThreadStore schema v${String(e.user_version)} 高于当前支持的 v${String(5)}`);this.db.exec("BEGIN IMMEDIATE");try{this.db.exec("CREATE TABLE IF NOT EXISTS threads (\n id TEXT PRIMARY KEY,\n title TEXT,\n model TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n CREATE TABLE IF NOT EXISTS messages (\n thread_id TEXT NOT NULL,\n idx INTEGER NOT NULL,\n role TEXT NOT NULL,\n content_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, idx),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );"),k(this.db)||this.db.exec("CREATE TABLE messages_with_thread_fk (\n thread_id TEXT NOT NULL,\n idx INTEGER NOT NULL,\n role TEXT NOT NULL,\n content_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, idx),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n INSERT INTO messages_with_thread_fk\n (thread_id, idx, role, content_json, created_at)\n SELECT messages.thread_id, messages.idx, messages.role,\n messages.content_json, messages.created_at\n FROM messages\n INNER JOIN threads ON threads.id = messages.thread_id;\n DROP TABLE messages;\n ALTER TABLE messages_with_thread_fk RENAME TO messages;"),this.db.exec("CREATE TABLE IF NOT EXISTS tool_executions (\n thread_id TEXT NOT NULL,\n sequence INTEGER NOT NULL,\n id TEXT NOT NULL,\n tool_call_id TEXT NOT NULL,\n agent_name TEXT NOT NULL,\n tool_name TEXT NOT NULL,\n record_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, id),\n UNIQUE (thread_id, sequence),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE INDEX IF NOT EXISTS idx_tool_executions_thread_call\n ON tool_executions(thread_id, tool_call_id, sequence);\n CREATE TABLE IF NOT EXISTS thread_tool_execution_state (\n thread_id TEXT PRIMARY KEY,\n next_sequence INTEGER NOT NULL CHECK (next_sequence >= 0),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS transcript_messages (\n thread_id TEXT NOT NULL,\n sequence INTEGER NOT NULL,\n role TEXT NOT NULL,\n message_json TEXT NOT NULL,\n provenance TEXT NOT NULL CHECK (provenance IN ('native', 'legacy_snapshot')),\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, sequence),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS thread_transcript_state (\n thread_id TEXT PRIMARY KEY,\n completeness TEXT NOT NULL CHECK (completeness IN ('complete', 'legacy_snapshot')),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS compaction_checkpoints (\n id TEXT PRIMARY KEY,\n thread_id TEXT NOT NULL,\n generation INTEGER NOT NULL,\n schema_version INTEGER NOT NULL,\n message_from_sequence INTEGER NOT NULL,\n message_through_sequence INTEGER NOT NULL,\n tool_from_sequence INTEGER NOT NULL,\n tool_through_sequence INTEGER NOT NULL,\n checkpoint_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n UNIQUE (thread_id, generation),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE INDEX IF NOT EXISTS idx_compaction_checkpoints_thread_generation\n ON compaction_checkpoints(thread_id, generation DESC);\n CREATE TABLE IF NOT EXISTS thread_runtime_event_state (\n thread_id TEXT PRIMARY KEY,\n event_log_id TEXT NOT NULL UNIQUE,\n next_sequence INTEGER NOT NULL CHECK (next_sequence >= 0),\n retained_from_sequence INTEGER NOT NULL\n CHECK (retained_from_sequence >= 0 AND retained_from_sequence <= next_sequence),\n retained_predecessor_event_id TEXT,\n latest_event_id TEXT,\n retained_count INTEGER NOT NULL CHECK (retained_count >= 0),\n retained_bytes INTEGER NOT NULL CHECK (retained_bytes >= 0),\n CHECK (\n (next_sequence = 0\n AND retained_from_sequence = 0\n AND retained_predecessor_event_id IS NULL\n AND latest_event_id IS NULL)\n OR (next_sequence > 0 AND latest_event_id IS NOT NULL)\n ),\n CHECK (\n (retained_from_sequence = 0 AND retained_predecessor_event_id IS NULL)\n OR (retained_from_sequence > 0 AND retained_predecessor_event_id IS NOT NULL)\n ),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS runtime_events (\n thread_id TEXT NOT NULL,\n thread_sequence INTEGER NOT NULL CHECK (thread_sequence >= 0),\n event_id TEXT NOT NULL UNIQUE,\n turn_id TEXT,\n event_timestamp TEXT NOT NULL,\n event_json TEXT NOT NULL,\n size_bytes INTEGER NOT NULL CHECK (size_bytes > 0),\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, thread_sequence),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE INDEX IF NOT EXISTS idx_runtime_events_thread_created\n ON runtime_events(thread_id, created_at, thread_sequence);"),this.db.exec("INSERT OR IGNORE INTO thread_tool_execution_state (thread_id, next_sequence)\n SELECT id, 0 FROM threads;\n UPDATE thread_tool_execution_state\n SET next_sequence = MAX(\n next_sequence,\n COALESCE((\n SELECT MAX(tool_executions.sequence) + 1\n FROM tool_executions\n WHERE tool_executions.thread_id = thread_tool_execution_state.thread_id\n ), 0)\n );"),e.user_version<3&&this.db.exec("INSERT OR IGNORE INTO thread_transcript_state (thread_id, completeness)\n SELECT id, 'legacy_snapshot' FROM threads;\n INSERT OR IGNORE INTO transcript_messages\n (thread_id, sequence, role, message_json, provenance, created_at)\n SELECT thread_id, idx, role, content_json, 'legacy_snapshot', created_at\n FROM messages;"),e.user_version<4&&(this.migrateToolExecutionPersistenceInTransaction(),this.migrateExplicitSkillCheckpointPersistenceInTransaction()),this.initializeMissingRuntimeEventStatesInTransaction(),this.enforceAllToolExecutionRetentionInTransaction((new Date).toISOString()),this.enforceAllRuntimeEventRetentionInTransaction((new Date).toISOString()),this.db.exec(`PRAGMA user_version = ${String(5)};`),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}migrateToolExecutionPersistenceInTransaction(){const e=this.db.prepare("SELECT rowid AS storage_rowid, record_json\n FROM tool_executions\n WHERE rowid > ?\n ORDER BY rowid ASC\n LIMIT 100"),t=this.db.prepare("UPDATE tool_executions\n SET tool_call_id = ?, agent_name = ?, tool_name = ?, record_json = ?, created_at = ?\n WHERE rowid = ?");let n=0;for(;;){const r=e.all(n);if(0===r.length)break;for(const e of r){const r=T(JSON.parse(e.record_json));t.run(r.toolCallId,r.agentName,r.toolName,JSON.stringify(r),r.createdAt,e.storage_rowid),n=e.storage_rowid}}}enforceAllToolExecutionRetentionInTransaction(e){const t=this.db.prepare("SELECT DISTINCT thread_id AS id FROM tool_executions").all();for(const{id:n}of t)this.enforceToolExecutionRetentionInTransaction(n,e)}migrateExplicitSkillCheckpointPersistenceInTransaction(){const e=[{table:"messages",column:"content_json"},{table:"transcript_messages",column:"message_json"}];for(const t of e){const e=this.db.prepare(`SELECT rowid AS storage_rowid, ${t.column} AS message_json\n FROM ${t.table}\n WHERE ${t.column} LIKE '%"modelUserContent"%'`).all(),n=this.db.prepare(`UPDATE ${t.table} SET ${t.column} = ? WHERE rowid = ?`);for(const t of e){const e=o.parse(JSON.parse(t.message_json)),r=A(e);n.run(JSON.stringify(r),t.storage_rowid)}}}initializeMissingRuntimeEventStatesInTransaction(){const e=this.db.prepare("SELECT threads.id\n FROM threads\n LEFT JOIN thread_runtime_event_state\n ON thread_runtime_event_state.thread_id = threads.id\n WHERE thread_runtime_event_state.thread_id IS NULL").all(),t=this.db.prepare("INSERT INTO thread_runtime_event_state\n (thread_id, event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id, retained_count, retained_bytes)\n VALUES (?, ?, 0, 0, NULL, NULL, 0, 0)");for(const n of e)t.run(n.id,a())}enforceAllRuntimeEventRetentionInTransaction(e){const t=this.db.prepare("SELECT thread_id AS id FROM thread_runtime_event_state").all();for(const n of t)this.enforceRuntimeEventRetentionInTransaction(n.id,e)}createThread(e={}){const t=e.id??a(),n=(new Date).toISOString();this.db.exec("BEGIN IMMEDIATE");try{return this.db.prepare("INSERT INTO threads (id, title, model, created_at, updated_at) VALUES (?, ?, ?, ?, ?)").run(t,e.title??null,e.model??null,n,n),this.db.prepare("INSERT INTO thread_transcript_state (thread_id, completeness) VALUES (?, ?)").run(t,N[0]),this.db.prepare("INSERT INTO thread_tool_execution_state (thread_id, next_sequence) VALUES (?, 0)").run(t),this.db.prepare("INSERT INTO thread_runtime_event_state\n (thread_id, event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id, retained_count, retained_bytes)\n VALUES (?, ?, 0, 0, NULL, NULL, 0, 0)").run(t,a()),this.db.exec("COMMIT"),t}catch(e){throw this.db.exec("ROLLBACK"),e}}hasThread(e){return void 0!==this.db.prepare("SELECT 1 FROM threads WHERE id = ?").get(e)}getThread(e){const t=this.db.prepare("SELECT * FROM threads WHERE id = ?").get(e);return t?X(t):void 0}listThreads(){return this.db.prepare("SELECT * FROM threads ORDER BY updated_at DESC, rowid DESC").all().map(X)}countMessages(e){return this.db.prepare("SELECT COUNT(*) AS n FROM messages WHERE thread_id = ?").get(e).n}countTranscriptMessages(e){return this.db.prepare("SELECT COUNT(*) AS n FROM transcript_messages WHERE thread_id = ?").get(e).n}updateTitle(e,t){this.db.prepare("UPDATE threads SET title = ? WHERE id = ?").run(t,e)}deleteThread(e){this.db.prepare("DELETE FROM threads WHERE id = ?").run(e)}appendRuntimeEvent(e){const t=E.parse(e.event),n=JSON.stringify(t),r=G(e.timestamp),s=Buffer.byteLength(n,"utf8")+Buffer.byteLength(r,"utf8")+Buffer.byteLength(e.turnId??"","utf8");if(s>RUNTIME_EVENT_RETENTION_POLICY.maxBytesPerThread)throw new Error("Runtime event exceeds the per-Thread retained byte limit");this.db.exec("BEGIN IMMEDIATE");try{const a=this.db.prepare("SELECT event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id,\n retained_count, retained_bytes\n FROM thread_runtime_event_state\n WHERE thread_id = ?").get(e.threadId);if(void 0===a)throw new Error(`Thread "${e.threadId}" 不存在或缺少 Runtime event state`);const i=H(),o=a.next_sequence;return this.db.prepare("INSERT INTO runtime_events\n (thread_id, thread_sequence, event_id, turn_id, event_timestamp,\n event_json, size_bytes, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)").run(e.threadId,o,i,e.turnId??null,r,n,s,r),this.db.prepare("UPDATE thread_runtime_event_state\n SET next_sequence = ?, latest_event_id = ?,\n retained_count = retained_count + 1,\n retained_bytes = retained_bytes + ?\n WHERE thread_id = ?").run(o+1,i,s,e.threadId),this.enforceRuntimeEventRetentionInTransaction(e.threadId,r),this.db.exec("COMMIT"),{eventId:i,cursor:P(a.event_log_id,o,i),threadSequence:o,threadId:e.threadId,...void 0===e.turnId?{}:{turnId:e.turnId},timestamp:r,event:t}}catch(e){throw this.db.exec("ROLLBACK"),e}}getRuntimeEventCursor(e){const t=this.db.prepare("SELECT event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id,\n retained_count, retained_bytes\n FROM thread_runtime_event_state\n WHERE thread_id = ?").get(e);if(void 0===t)throw new Error(`Thread "${e}" 不存在或缺少 Runtime event state`);return 0===t.next_sequence||null===t.latest_event_id?null:P(t.event_log_id,t.next_sequence-1,h.parse(t.latest_event_id))}resumeRuntimeEvents(e,t){const n=null===t?void 0:B(t);if(null!==t&&void 0===n)throw new RuntimeEventCursorGapError(t);this.db.exec("BEGIN IMMEDIATE");try{this.enforceRuntimeEventRetentionInTransaction(e,(new Date).toISOString());const r=this.db.prepare("SELECT event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id,\n retained_count, retained_bytes\n FROM thread_runtime_event_state\n WHERE thread_id = ?").get(e);if(void 0===r)throw new Error(`Thread "${e}" 不存在或缺少 Runtime event state`);if(null===t&&r.retained_from_sequence>0)throw new RuntimeEventCursorExpiredError(t);if(null!==t&&void 0!==n&&this.validateRuntimeEventCursor(r,t,n,e),0===r.next_sequence||null===r.latest_event_id)return this.db.exec("COMMIT"),{events:[],throughCursor:null,replayedCount:0};const s=r.next_sequence-1,a=n?.threadSequence??-1,i=this.db.prepare("SELECT thread_sequence, event_id, turn_id, event_timestamp, event_json\n FROM runtime_events\n WHERE thread_id = ?\n AND thread_sequence > ?\n AND thread_sequence <= ?\n ORDER BY thread_sequence ASC").all(e,a,s).map(t=>Y(r.event_log_id,e,t)),o=P(r.event_log_id,s,h.parse(r.latest_event_id));return this.db.exec("COMMIT"),{events:i,throughCursor:o,replayedCount:i.length}}catch(e){throw e instanceof RuntimeEventCursorExpiredError||e instanceof RuntimeEventCursorGapError?this.db.exec("COMMIT"):this.db.exec("ROLLBACK"),e}}countRuntimeEvents(e){return this.db.prepare("SELECT COUNT(*) AS count FROM runtime_events WHERE thread_id = ?").get(e).count}validateRuntimeEventCursor(e,t,n,r){if(n.eventLogId!==e.event_log_id||n.threadSequence>=e.next_sequence)throw new RuntimeEventCursorGapError(t);const s=e.retained_from_sequence-1;if(n.threadSequence<s)throw new RuntimeEventCursorExpiredError(t);if(n.threadSequence===s){if(n.eventId!==e.retained_predecessor_event_id)throw new RuntimeEventCursorGapError(t);return}const a=this.db.prepare("SELECT thread_sequence, event_id\n FROM runtime_events\n WHERE thread_id = ? AND thread_sequence = ?").get(r,n.threadSequence);if(void 0===a||a.event_id!==n.eventId)throw new RuntimeEventCursorGapError(t)}enforceRuntimeEventRetentionInTransaction(e,t){const n=this.db.prepare("SELECT event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id,\n retained_count, retained_bytes\n FROM thread_runtime_event_state\n WHERE thread_id = ?").get(e);if(void 0===n||0===n.retained_count)return;let r=n.retained_from_sequence;if(n.retained_count>RUNTIME_EVENT_RETENTION_POLICY.maxRecordsPerThread){const t=this.db.prepare("SELECT thread_sequence AS keep_from\n FROM runtime_events\n WHERE thread_id = ?\n ORDER BY thread_sequence DESC\n LIMIT 1 OFFSET ?").get(e,RUNTIME_EVENT_RETENTION_POLICY.maxRecordsPerThread-1);r=Math.max(r,t?.keep_from??n.next_sequence)}if(n.retained_bytes>RUNTIME_EVENT_RETENTION_POLICY.maxBytesPerThread){const t=this.db.prepare("SELECT MIN(thread_sequence) AS keep_from\n FROM (\n SELECT thread_sequence,\n SUM(size_bytes) OVER (\n ORDER BY thread_sequence DESC\n ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n ) AS retained_bytes\n FROM runtime_events\n WHERE thread_id = ?\n )\n WHERE retained_bytes <= ?").get(e,RUNTIME_EVENT_RETENTION_POLICY.maxBytesPerThread);r=Math.max(r,t.keep_from??n.next_sequence)}const s=new Date(Date.parse(t)-RUNTIME_EVENT_RETENTION_POLICY.maxAgeMs).toISOString(),a=this.db.prepare("SELECT MIN(thread_sequence) AS keep_from\n FROM runtime_events\n WHERE thread_id = ? AND created_at >= ?").get(e,s);if(r=Math.max(r,a.keep_from??n.next_sequence),r<=n.retained_from_sequence)return;const i=this.db.prepare("SELECT thread_sequence, event_id\n FROM runtime_events\n WHERE thread_id = ? AND thread_sequence = ?").get(e,r-1);if(void 0===i)throw new Error("Runtime event retention boundary is not a continuous retained prefix");const o=this.db.prepare("SELECT COUNT(*) AS removed_count, SUM(size_bytes) AS removed_bytes\n FROM runtime_events\n WHERE thread_id = ? AND thread_sequence < ?").get(e,r);this.db.prepare("DELETE FROM runtime_events WHERE thread_id = ? AND thread_sequence < ?").run(e,r),this.db.prepare("UPDATE thread_runtime_event_state\n SET retained_from_sequence = ?, retained_predecessor_event_id = ?,\n retained_count = retained_count - ?,\n retained_bytes = retained_bytes - ?\n WHERE thread_id = ?").run(r,i.event_id,o.removed_count,o.removed_bytes??0,e)}appendMessages(e,t){if(0===t.length)return;if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=t.map(e=>A(o.parse(e))),r=(new Date).toISOString(),s=this.db.prepare("INSERT INTO messages (thread_id, idx, role, content_json, created_at) VALUES (?, ?, ?, ?, ?)"),a=this.db.prepare("INSERT INTO transcript_messages\n (thread_id, sequence, role, message_json, provenance, created_at)\n VALUES (?, ?, ?, ?, ?, ?)");this.db.exec("BEGIN IMMEDIATE");try{const t=this.db.prepare("SELECT COALESCE(MAX(idx), -1) AS maxIdx FROM messages WHERE thread_id = ?").get(e),i=this.db.prepare("SELECT COALESCE(MAX(sequence), -1) AS maxSequence FROM transcript_messages WHERE thread_id = ?").get(e);let o=t.maxIdx+1,d=i.maxSequence+1;for(const t of n){const n=JSON.stringify(t);s.run(e,o,t.role,n,r),a.run(e,d,t.role,n,R[0],r),o+=1,d+=1}this.db.prepare("UPDATE threads SET updated_at = ? WHERE id = ?").run(r,e),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}replaceMessages(e,t){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=t.map(e=>o.parse(e)),r=(new Date).toISOString();this.db.exec("BEGIN IMMEDIATE");try{this.replaceMessagesInTransaction(e,n,r),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}getMessages(e){return this.db.prepare("SELECT content_json FROM messages WHERE thread_id = ? ORDER BY idx ASC").all(e).map(e=>o.parse(JSON.parse(e.content_json)))}replaceMessagesInTransaction(e,t,n){const r=this.db.prepare("INSERT INTO messages (thread_id, idx, role, content_json, created_at) VALUES (?, ?, ?, ?, ?)");this.db.prepare("DELETE FROM messages WHERE thread_id = ?").run(e),t.forEach((t,s)=>{const a=A(t);r.run(e,s,a.role,JSON.stringify(a),n)}),this.db.prepare("UPDATE threads SET updated_at = ? WHERE id = ?").run(n,e)}appendToolExecution(e,t){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=p(t),r=JSON.stringify(n),s=(new Date).toISOString();this.db.exec("BEGIN IMMEDIATE");try{const t=this.db.prepare("SELECT next_sequence FROM thread_tool_execution_state WHERE thread_id = ?").get(e);if(void 0===t)throw new Error(`Thread "${e}" 缺少 Tool execution sequence state`);const a=t.next_sequence;return this.db.prepare("INSERT INTO tool_executions\n (thread_id, sequence, id, tool_call_id, agent_name, tool_name, record_json, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)").run(e,a,n.id,n.toolCallId,n.agentName,n.toolName,r,n.createdAt),this.db.prepare("UPDATE thread_tool_execution_state SET next_sequence = ? WHERE thread_id = ?").run(a+1,e),this.enforceToolExecutionRetentionInTransaction(e,s),this.db.prepare("UPDATE threads SET updated_at = ? WHERE id = ?").run(s,e),this.db.exec("COMMIT"),a}catch(e){throw this.db.exec("ROLLBACK"),e}}enforceToolExecutionRetentionInTransaction(e,t){const n=new Date(Date.parse(t)-TOOL_EXECUTION_RETENTION_POLICY.maxAgeMs).toISOString();let r=!1;const s=this.db.prepare("DELETE FROM tool_executions\n WHERE thread_id = ?\n AND created_at < ?").run(e,n);r=Number(s.changes)>0;const a=this.db.prepare("SELECT sequence\n FROM (\n SELECT sequence,\n ROW_NUMBER() OVER (ORDER BY sequence DESC) AS retained_rank,\n SUM(length(CAST(record_json AS BLOB))) OVER (\n ORDER BY sequence DESC\n ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n ) AS retained_bytes\n FROM tool_executions\n WHERE thread_id = ?\n )\n WHERE retained_rank > ? OR retained_bytes > ?").all(e,TOOL_EXECUTION_RETENTION_POLICY.maxRecordsPerThread,TOOL_EXECUTION_RETENTION_POLICY.maxBytesPerThread);if(a.length>0){const t=this.db.prepare("DELETE FROM tool_executions WHERE thread_id = ? AND sequence = ?");for(const n of a)t.run(e,n.sequence);r=!0}r&&this.db.prepare("INSERT INTO thread_transcript_state (thread_id, completeness)\n VALUES (?, 'legacy_snapshot')\n ON CONFLICT(thread_id) DO UPDATE SET completeness = 'legacy_snapshot'").run(e)}listToolExecutions(e,t={}){const n=t.afterSequence??-1,r=t.throughSequence??Number.MAX_SAFE_INTEGER,s=t.limit??b;if(!Number.isInteger(n)||n<-1)throw new Error("afterSequence 必须是大于等于 -1 的整数");if(!Number.isInteger(s)||s<1||s>M)throw new Error(`limit 必须是 1-${String(M)} 之间的整数`);if(!Number.isInteger(r)||r<-1)throw new Error("throughSequence 必须是大于等于 -1 的整数");if(r<=n)return[];return this.db.prepare("SELECT sequence, record_json\n FROM tool_executions\n WHERE thread_id = ?\n AND sequence > ?\n AND sequence <= ?\n AND (? IS NULL OR tool_call_id = ?)\n ORDER BY sequence ASC\n LIMIT ?").all(e,n,r,t.toolCallId??null,t.toolCallId??null,s).map(e=>({...T(JSON.parse(e.record_json)),sequence:e.sequence}))}listRecentToolExecutions(e,t={}){const n=t.beforeSequence??Number.MAX_SAFE_INTEGER,r=t.limit??b;this.validateRecentPage(n,r,M);const s=this.db.prepare("SELECT sequence, record_json\n FROM tool_executions\n WHERE thread_id = ?\n AND sequence < ?\n ORDER BY sequence DESC\n LIMIT ?").all(e,n,r+1),a=s.length>r,i=s.slice(0,r).map(e=>({...T(JSON.parse(e.record_json)),sequence:e.sequence})).reverse();return{entries:i,nextBeforeSequence:a?i[0]?.sequence:void 0}}getToolExecution(e,t){const n=this.db.prepare("SELECT sequence, record_json FROM tool_executions WHERE thread_id = ? AND id = ?").get(e,t);if(n)return{...T(JSON.parse(n.record_json)),sequence:n.sequence}}getTranscriptCompleteness(e){const t=this.db.prepare("SELECT completeness FROM thread_transcript_state WHERE thread_id = ?").get(e);return t?.completeness===N[0]?N[0]:N[1]}listTranscriptMessages(e,t={}){const n=t.afterSequence??-1,r=t.throughSequence??Number.MAX_SAFE_INTEGER,s=t.limit??w;if(this.validateTranscriptRange(n,r,s,D),r<=n)return[];return this.db.prepare("SELECT sequence, provenance, created_at, message_json\n FROM transcript_messages\n WHERE thread_id = ?\n AND sequence > ?\n AND sequence <= ?\n ORDER BY sequence ASC\n LIMIT ?").all(e,n,r,s).map(e=>g.parse({sequence:e.sequence,provenance:e.provenance,createdAt:e.created_at,message:JSON.parse(e.message_json)}))}listRecentTranscriptMessages(e,t={}){const n=t.beforeSequence??Number.MAX_SAFE_INTEGER,r=t.limit??w;this.validateRecentPage(n,r,D);const s=this.db.prepare("SELECT sequence, provenance, created_at, message_json\n FROM transcript_messages\n WHERE thread_id = ?\n AND sequence < ?\n ORDER BY sequence DESC\n LIMIT ?").all(e,n,r+1),a=s.length>r,i=s.slice(0,r).map(e=>g.parse({sequence:e.sequence,provenance:e.provenance,createdAt:e.created_at,message:JSON.parse(e.message_json)})).reverse();return{entries:i,nextBeforeSequence:a?i[0]?.sequence:void 0}}getLatestCheckpoint(e){const t=this.db.prepare("SELECT schema_version, checkpoint_json\n FROM compaction_checkpoints\n WHERE thread_id = ?\n ORDER BY generation DESC").all(e);for(const n of t)try{return this.withCurrentTranscriptCompleteness(e,W(n))}catch(e){if(e instanceof S)throw e}}getLatestSummaryCheckpoint(e){const t=this.db.prepare("SELECT schema_version, checkpoint_json\n FROM compaction_checkpoints\n WHERE thread_id = ?\n ORDER BY generation DESC").all(e);for(const n of t)try{const t=W(n);if("valid"===t.summary.status)return this.withCurrentTranscriptCompleteness(e,t)}catch(e){if(e instanceof S)throw e}}getCheckpoint(e,t){const n=this.db.prepare("SELECT schema_version, checkpoint_json\n FROM compaction_checkpoints\n WHERE thread_id = ? AND id = ?").get(e,t);if(void 0!==n)try{return this.withCurrentTranscriptCompleteness(e,W(n))}catch(e){if(e instanceof S)throw e;return}}loadSessionState(e){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);return{messages:x(this.getMessages(e)).messages,checkpoint:this.getLatestCheckpoint(e),transcriptCompleteness:this.getTranscriptCompleteness(e)}}commitCompaction(e,t){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=t.messages.map(e=>o.parse(e)),r=t.expectedActiveMessages.map(e=>o.parse(e)),s=x(n);if(s.status!==f.valid){const e=[...s.removedToolCallIds,...s.removedToolResultIds].filter((e,t,n)=>n.indexOf(e)===t).sort((e,t)=>e.localeCompare(t));throw new Error(`Compaction active projection contains malformed Tool protocol: ${e.join(", ")}`)}const a=O(t.draft),i=v.parse(t.legacySnapshotTranscriptFragments??[]),d=v.parse(i.map(e=>m(e))).map(e=>o.parse(C(e))),c=(new Date).toISOString();this.db.exec("BEGIN IMMEDIATE");try{const s=x(this.getMessages(e)).messages;if(JSON.stringify(s)!==JSON.stringify(r))throw new Error("Compaction active projection changed after the draft snapshot was captured");const i=this.getLatestCheckpoint(e);if(i?.id!==t.expectedLatestCheckpointId)throw new Error("Compaction checkpoint ancestry changed after the draft snapshot was captured");const o=this.getLatestSummaryCheckpoint(e),E=y(a.summary,o),_=this.getTranscriptCompleteness(e),h=O({...a,summary:E,..._===N[1]&&"valid"===a.toolState.integrityStatus?{toolState:{...a.toolState,integrityStatus:"sanitized"}}:{}}),u=this.db.prepare("SELECT COALESCE(MAX(generation), 0) AS maxGeneration\n FROM compaction_checkpoints\n WHERE thread_id = ?").get(e),T=this.maxTranscriptMessageSequence(e),p=this.maxToolExecutionSequence(e),m=t.evidenceWatermarks.transcriptMessagesThroughSequence,l=t.evidenceWatermarks.toolExecutionsThroughSequence,S=i?.transcript.messages.throughSequence??-1,g=i?.transcript.toolExecutions.throughSequence??-1,L=2===i?.version?i.semanticEvidence.messagesThroughSequence:-1,v=2===i?.version?i.semanticEvidence.toolExecutionsThroughSequence:-1;if(d.length>0&&1!==i?.version)throw new Error("Legacy snapshot transcript archive requires a V1 parent checkpoint");if(!Number.isInteger(m)||m<S||m>T)throw new Error(`Compaction transcript message watermark ${String(m)} is outside the available range ${String(S)}-${String(T)}`);if(!Number.isInteger(t.semanticEvidenceWatermarks.messagesThroughSequence)||t.semanticEvidenceWatermarks.messagesThroughSequence<L||t.semanticEvidenceWatermarks.messagesThroughSequence>m)throw new Error(`Compaction semantic message watermark ${String(t.semanticEvidenceWatermarks.messagesThroughSequence)} is outside the available range ${String(L)}-${String(m)}`);if(!Number.isInteger(t.semanticEvidenceWatermarks.toolExecutionsThroughSequence)||t.semanticEvidenceWatermarks.toolExecutionsThroughSequence<v||t.semanticEvidenceWatermarks.toolExecutionsThroughSequence>l)throw new Error(`Compaction semantic Tool watermark ${String(t.semanticEvidenceWatermarks.toolExecutionsThroughSequence)} is outside the available range ${String(v)}-${String(l)}`);if(!Number.isInteger(l)||l<g||l>p)throw new Error(`Compaction tool execution watermark ${String(l)} is outside the available range ${String(g)}-${String(p)}`);let C=m;if(d.length>0){if(m!==T)throw new Error("Legacy snapshot transcript archive requires the latest message watermark");const t=this.db.prepare("INSERT INTO transcript_messages\n (thread_id, sequence, role, message_json, provenance, created_at)\n VALUES (?, ?, ?, ?, ?, ?)");let n=T+1;for(const r of d)t.run(e,n,r.role,JSON.stringify(r),R[1],c),n+=1;C=n-1}const f={messages:{fromSequenceExclusive:S,throughSequence:C},toolExecutions:{fromSequenceExclusive:g,throughSequence:l},completeness:_},A=I({draft:h,semanticState:t.semanticState,semanticEvidence:t.semanticEvidenceWatermarks,generation:u.maxGeneration+1,transcript:f,...void 0!==i?{previousCheckpointId:i.id}:{},createdAt:c});return this.db.prepare("INSERT INTO compaction_checkpoints\n (id, thread_id, generation, schema_version,\n message_from_sequence, message_through_sequence,\n tool_from_sequence, tool_through_sequence,\n checkpoint_json, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(A.id,e,A.generation,A.version,A.transcript.messages.fromSequenceExclusive,A.transcript.messages.throughSequence,A.transcript.toolExecutions.fromSequenceExclusive,A.transcript.toolExecutions.throughSequence,JSON.stringify(A),A.createdAt),this.replaceMessagesInTransaction(e,n,c),this.db.exec("COMMIT"),A}catch(e){throw this.db.exec("ROLLBACK"),e}}readCheckpointTranscript(e,t){if("message"!==t.kind&&"tool_execution"!==t.kind)throw new Error("kind 必须是 message 或 tool_execution");const n=this.getCheckpoint(e,t.checkpointId);if(void 0===n)throw new Error("checkpoint 不存在、已损坏或不属于当前 thread");const r=t.limit??w,s="message"===t.kind?n.transcript.messages:n.transcript.toolExecutions,a=Math.max(t.afterSequence??s.fromSequenceExclusive,s.fromSequenceExclusive);this.validateTranscriptRange(a,s.throughSequence,r,U);const i="message"===t.kind?this.listTranscriptMessages(e,{afterSequence:a,throughSequence:s.throughSequence,limit:r+1}).map(e=>({kind:"message",...e})):this.listToolExecutions(e,{afterSequence:a,throughSequence:s.throughSequence,limit:Math.min(r+1,M)}).map(e=>({kind:"tool_execution",...e})),o=i.length>r,d=o?i.slice(0,r):i,c=d.at(-1)?.sequence;return{checkpointId:n.id,kind:t.kind,entries:d,...o&&void 0!==c?{nextAfterSequence:c}:{},...void 0!==n.previousCheckpointId?{previousCheckpointId:n.previousCheckpointId}:{},completeness:n.transcript.completeness}}maxTranscriptMessageSequence(e){return this.db.prepare("SELECT COALESCE(MAX(sequence), -1) AS maxSequence FROM transcript_messages WHERE thread_id = ?").get(e).maxSequence}maxToolExecutionSequence(e){const t=this.db.prepare("SELECT next_sequence FROM thread_tool_execution_state WHERE thread_id = ?").get(e);return(t?.next_sequence??0)-1}withCurrentTranscriptCompleteness(e,t){const n=N[1],r=t.transcript.completeness===n||this.getTranscriptCompleteness(e)===n?n:N[0],s=r===n&&"valid"===t.toolState.integrityStatus?{...t.toolState,integrityStatus:"sanitized"}:t.toolState;return t.transcript.completeness===r&&t.toolState===s?t:{...t,toolState:s,transcript:{...t.transcript,completeness:r}}}validateTranscriptRange(e,t,n,r){if(!Number.isInteger(e)||e<-1)throw new Error("afterSequence 必须是大于等于 -1 的整数");if(!Number.isInteger(t)||t<-1)throw new Error("throughSequence 必须是大于等于 -1 的整数");if(!Number.isInteger(n)||n<1||n>r)throw new Error(`limit 必须是 1-${String(r)} 之间的整数`)}validateRecentPage(e,t,n){if(!Number.isSafeInteger(e)||e<0||e>Number.MAX_SAFE_INTEGER)throw new Error("beforeSequence 必须是非负安全整数");if(!Number.isInteger(t)||t<1||t>n)throw new Error(`limit 必须是 1-${String(n)} 之间的整数`)}close(){this.db.close()}}
@@ -0,0 +1,16 @@
1
+ import { type ToolSet } from "ai";
2
+ import { type UserInputForm, type UserInputResult } from "@roll-agent/protocol";
3
+ import type { ToolRegistry } from "./naming.ts";
4
+ import { type ToolExecutionCoordinator } from "./tool-execution-coordinator.ts";
5
+ export declare const USER_INPUT_TOOL_AGENT_NAME = "roll";
6
+ export declare const USER_INPUT_TOOL_NAME = "user_input";
7
+ export interface UserInputToolContext {
8
+ readonly sessionId: string;
9
+ isAvailable(): boolean;
10
+ request(form: UserInputForm, abortSignal: AbortSignal | undefined): Promise<UserInputResult>;
11
+ }
12
+ export interface BuiltUserInputTool {
13
+ readonly id: string;
14
+ readonly tools: ToolSet;
15
+ }
16
+ export declare function buildUserInputTool(context: UserInputToolContext, registry: ToolRegistry, coordinator: ToolExecutionCoordinator): BuiltUserInputTool;
@@ -0,0 +1 @@
1
+ import{tool as t}from"ai";import{userInputFormSchema as o}from"@roll-agent/protocol";import{successfulToolResult as e,toolResultToModelOutput as s}from"./normalize-result.js";import{TOOL_RESOURCE_ACCESS_MODES as r,executeCoordinatedTool as i}from"./tool-execution-coordinator.js";export const USER_INPUT_TOOL_AGENT_NAME="roll";export const USER_INPUT_TOOL_NAME="user_input";function n(t){const o="submitted"===t.status?{status:t.status,submittedControlIds:t.values.map(t=>t.id)}:{status:t.status};return e(o,{raw:t,model:{type:"json",value:t}})}function a(){return n({status:"cancelled",reason:"当前客户端不再支持用户输入请求"})}export function buildUserInputTool(e,u,l){const p=u.register("roll","user_input",{annotations:{readOnlyHint:!0}}),c={prepare:()=>e.isAvailable()?void 0:a(),resources:()=>[{key:`session:${e.sessionId}:user-input`,mode:r.write}],revalidateExecution:()=>e.isAvailable()?void 0:a()};return l.register(p,c),{id:p,tools:{[p]:t({description:"向用户展示一个结构化表单,仅用于补充完成当前任务所必需且无法从上下文推断的信息。不得请求密码、令牌、密钥或其他敏感凭证。",inputSchema:o,toModelOutput:({output:t})=>s(t),execute:async(t,o)=>i(l,c,p,o.toolCallId,t,o.abortSignal,async()=>e.isAvailable()?n(await e.request(t,o.abortSignal)):a())})}}}
@@ -1,5 +1,7 @@
1
1
  import type { SessionCancellationReason } from "./cancellation.ts";
2
2
  import type { ToolOutcome } from "../tool-bridge/normalize-result.ts";
3
+ import type { UserInputForm } from "@roll-agent/protocol";
4
+ import type { SessionUserInputRequestId } from "../interaction/user-input-interaction-manager.ts";
3
5
  export interface SessionTokenUsage {
4
6
  readonly inputTokens?: number;
5
7
  readonly outputTokens?: number;
@@ -65,9 +67,22 @@ export type SessionEvent = {
65
67
  readonly agentName: string;
66
68
  readonly toolName: string;
67
69
  readonly input: unknown;
70
+ /** Absolute deadline shared with Runtime-to-Client interaction delivery. */
71
+ readonly expiresAt?: string;
68
72
  readonly reason?: string;
69
73
  /** Model-authored display aid. It never changes approval policy or command execution. */
70
74
  readonly explanation?: string;
75
+ } | {
76
+ /** Engine-internal request; Runtime Protocol assigns a separate InteractionId. */
77
+ readonly type: "user-input-required";
78
+ readonly requestId: SessionUserInputRequestId;
79
+ readonly form: UserInputForm;
80
+ readonly expiresAt: string;
81
+ } | {
82
+ /** Contains no submitted values so public event projectors cannot leak form answers. */
83
+ readonly type: "user-input-settled";
84
+ readonly requestId: SessionUserInputRequestId;
85
+ readonly status: "submitted" | "cancelled";
71
86
  } | {
72
87
  readonly type: "step-finish";
73
88
  readonly finishReason: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roll-agent/runtime",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,11 +30,11 @@
30
30
  "@modelcontextprotocol/sdk": "^1.12.0",
31
31
  "ai": "^7.0.9",
32
32
  "zod": "^3.25.76",
33
- "@roll-agent/protocol": "0.3.0"
33
+ "@roll-agent/protocol": "0.4.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^22.0.0",
37
- "@roll-agent/core": "0.26.0"
37
+ "@roll-agent/core": "0.27.0"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "rm -rf dist && tsc -p tsconfig.build.json && node ../../scripts/obfuscate.mjs",