@roll-agent/runtime 0.16.0 → 0.17.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_V13 as n,RUNTIME_METHODS as i,RUNTIME_PROTOCOL_VERSION as a,RUNTIME_V14_MAX_ATTACHMENT_BYTES as o,RUNTIME_V14_MAX_ATTACHMENT_CHUNK_BYTES as d,RUNTIME_V14_MAX_STAGED_ATTACHMENTS as u,RUNTIME_V14_MAX_TURN_ATTACHMENTS as h,SUPPORTED_RUNTIME_PROTOCOL_VERSIONS as c,approvalExplanationSchema as l,approvalIdSchema as p,normalizeUserInputResultForForm as m,operationIdSchema as I,parseLatestRuntimeMethodResult as v,runtimeDurableEventV13Schema as g,runtimeEphemeralEventV13Schema as f,runtimeInstanceIdSchema as y,runtimeProtocolVersionSchema as T,streamIdSchema as A,threadIdSchema as w,userInputFormSchema as q}from"@roll-agent/protocol";import{AttachmentStore as S}from"./attachment-store.js";import{createSafeCapabilitySnapshot as R}from"../engine/capability-manifest.js";import{isSensitiveFieldName as x,redactSecretText as E,toRedactedToolExecutionRecordSummary as b}from"../tool-bridge/tool-execution-record.js";import{RuntimeEventCursorExpiredError as O,RuntimeEventCursorGapError as C,ThreadStore as P}from"../store/thread-store.js";const U=4194304,N=500,F=1e4,k=16e3,B=64,j=128,M=16,V=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 $(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=$(r),i=this.inFlight.get(e);if(void 0!==i)return this.assertMatchingRequest(e,t,n,i),i.response.then(e=>v(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=>v(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 L(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}function z(e,t,r=0){if(void 0!==t&&(V.has(t.toLowerCase())||x(t)))return"[redacted]";if(r>=M)return"[nested value omitted]";if(null===e||"boolean"==typeof e)return e;if("string"==typeof e){const t=E(e);return t.length<=k?t:`${t.slice(0,k)}\n[value clipped]`}if("number"==typeof e)return Number.isFinite(e)?e:String(e);if(Array.isArray(e)){const t=e.slice(0,B).map(e=>z(e,void 0,r+1));return e.length>B&&t.push(`[${String(e.length-B)} items omitted]`),t}if(L(e)){const t={};for(const[s,n]of Object.entries(e).slice(0,j))void 0!==n&&(t[s]=z(n,s,r+1));return Object.keys(e).length>j&&(t._omitted=`${String(Object.keys(e).length-j)} keys omitted`),t}return void 0===e?null:String(e)}function D(e){return e instanceof Error?e.message:String(e)}function W(e){const t=e.content;return"string"==typeof t?t:Array.isArray(t)?t.flatMap(e=>L(e)&&"text"===e.type&&"string"==typeof e.text?[e.text]:[]).join(""):""}function _(e){const t=e.endsWith("==")?2:e.endsWith("=")?1:0;return Math.max(0,3*Math.floor(e.length/4)-t)}function G(e){const t=e.content;return"user"===e.role&&Array.isArray(t)?t.flatMap(e=>L(e)?"file"===e.type&&"string"==typeof e.mediaType&&"string"==typeof e.data?[{type:"attachment",mediaType:e.mediaType,bytes:_(e.data)}]:"image"===e.type&&"string"==typeof e.image?[{type:"attachment",mediaType:"image/*",bytes:_(e.image)}]:[]:[]):[]}function J(e){return w.parse(e)}function H(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 K(e){if("user"!==e.message.role&&"assistant"!==e.message.role)return;const t=E(W(e.message));return{sequence:e.sequence,role:e.message.role,createdAt:e.createdAt,parts:[...t.length>0?[{type:"text",text:t}]:[],...G(e.message)]}}function Q(e){const t=b(e);return{id:I.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:z(t.display,void 0)}}function X(e,t){const s=z(t.input,void 0),n=void 0===t.explanation?void 0:l.safeParse(E(t.explanation)),i=!0===n?.success&&L(s)?{...s,[r]:n.data}:s;return{id:p.parse(t.approvalId),turnId:e,agentName:t.agentName,toolName:t.toolName,preview:i,...void 0!==t.reason?{reason:E(t.reason)}:{}}}export class RuntimeService{runtimeInstanceId;engine;store;serverName;serverVersion;runtimeVersion;maxFrameBytes;idempotencyCacheEntries;reasoningSummaryProjector;attachmentStore;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??U,this.idempotencyCacheEntries=s.idempotencyCacheEntries??F,this.mutationRequests=new MutationRequestCache(this.idempotencyCacheEntries),this.reasoningSummaryProjector=s.reasoningSummaryProjector,this.attachmentStore=s.attachmentStore,this.runtimeInstanceId=y.parse(t())}initialize(e){let t;this.assertOpen();for(const r of e.protocolVersions){const e=T.safeParse(r);if(e.success){t=e.data;break}}if(void 0===t)throw new RuntimeServiceError(s.protocolVersionUnsupported,`不支持客户端协议版本:${e.protocolVersions.join(", ")}`,{details:{supportedVersions:c}});const r=n.filter(e=>"reasoning-summary"!==e||void 0!==this.reasoningSummaryProjector),i={protocolVersion:t,runtimeInstanceId:this.runtimeInstanceId,server:{name:this.serverName,version:this.serverVersion,runtimeVersion:this.runtimeVersion},features:r,limits:{maxFrameBytes:this.maxFrameBytes,maxPageSize:N,idempotencyCacheEntries:this.idempotencyCacheEntries}};if("1.4"===t){const e=this.attachmentStore?.limits;return{...i,protocolVersion:t,features:void 0!==this.attachmentStore?[...r,"attachments"]:[...r],limits:{...i.limits,eventReplay:!0,maxAttachmentBytes:e?.maxAttachmentBytes??o,maxAttachmentChunkBytes:e?.maxAttachmentChunkBytes??d,maxTurnAttachments:h,maxStagedAttachments:e?.maxStagedAttachments??u}}}return"1.3"===t?{...i,protocolVersion:t,limits:{...i.limits,eventReplay:!0}}:{...i,protocolVersion:t,limits:{...i.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=>H(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:H(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:H(this.store,t),messages:{items:r.entries.flatMap(e=>{const t=K(e);return void 0===t?[]:[t]}),nextBeforeSequence:r.nextBeforeSequence??null},operations:{items:s.entries.map(Q),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 O)throw new RuntimeServiceError(s.eventCursorExpired,"Runtime Event cursor 已超出当前 Thread 的保留窗口,请回退到 thread.snapshot",{details:{threadId:e.threadId}});if(t instanceof C)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:H(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.attachmentStore?.releaseThread(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:z(R(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=this.resolveTurnAttachments(e),a={threadId:e.threadId,turnId:e.turnId,session:r,startedAt:(new Date).toISOString(),status:"running",interactionFailure:void 0};this.activeTurns.set(e.threadId,a),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(a,e.input.text,i).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,t.scope):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}async stageAttachment(e){return this.mutationRequests.run(e.requestId,i.attachmentStage,e,()=>{this.assertOpen(),this.requireThread(e.threadId);const t=this.requireAttachmentStore().stage({threadId:e.threadId,fileName:e.fileName,mediaType:e.mediaType,bytes:e.bytes,sha256:e.sha256,source:e.source,sourcePath:e.sourcePath});return t.ok||this.throwAttachmentFailure(t),{attachmentId:t.attachmentId,state:t.state,...void 0!==t.descriptor?{descriptor:t.descriptor}:{}}})}async appendAttachmentChunk(e){return this.mutationRequests.run(e.requestId,i.attachmentChunk,e,()=>{this.assertOpen();const t=this.requireAttachmentStore().appendChunk({threadId:e.threadId,attachmentId:e.attachmentId,sequence:e.sequence,dataBase64:e.dataBase64});return t.ok||this.throwAttachmentFailure(t),{receivedBytes:t.receivedBytes,nextSequence:t.nextSequence}})}async commitAttachment(e){return this.mutationRequests.run(e.requestId,i.attachmentCommit,e,()=>{this.assertOpen();const t=this.requireAttachmentStore().commit({threadId:e.threadId,attachmentId:e.attachmentId});return t.ok||this.throwAttachmentFailure(t),{descriptor:t.descriptor}})}async releaseAttachment(e){return this.mutationRequests.run(e.requestId,i.attachmentRelease,e,()=>{this.assertOpen();return{released:this.requireAttachmentStore().release({threadId:e.threadId,attachmentId:e.attachmentId}).released}})}requireAttachmentStore(){if(void 0===this.attachmentStore)throw new RuntimeServiceError(s.capabilityUnavailable,"Runtime 未配置附件存储,attachments 能力不可用");return this.attachmentStore}throwAttachmentFailure(e){throw new RuntimeServiceError(e.code,e.message,{...void 0!==e.retryable?{retryable:e.retryable}:{}})}resolveTurnAttachments(e){const t=e.input.attachments;if(void 0===t||0===t.length)return;const r=this.requireAttachmentStore(),n=new Set,i=[];for(const a of t){if(n.has(a))throw new RuntimeServiceError(s.invalidParams,`附件 "${a}" 在同一 Turn 中重复引用`);n.add(a);const t=r.readCommitted({threadId:e.threadId,attachmentId:a});t.ok||this.throwAttachmentFailure(t),i.push({data:t.dataBase64,mediaType:t.descriptor.mediaType})}return i}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=m(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:Q(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(),this.attachmentStore?.close(),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=g.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:f.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,r){const s={streamId:void 0,text:"",terminalEvent:void 0},n=void 0!==r&&r.length>0?{text:t,attachments:r}:t;let i;try{for await(const t of e.session.send(n))this.projectSessionEvent(e,s,t);i=s.terminalEvent??{type:"turn.completed"}}catch(e){i=s.terminalEvent??{type:"turn.failed",stage:"execute",message:E(D(e))}}finally{this.activeTurns.get(e.threadId)===e&&this.activeTurns.delete(e.threadId),this.settleTurnOwner(e)}this.emitTurnTerminal(e,i)}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=A.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??A.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??A.parse(t());return void this.emit(e.threadId,e.turnId,{type:"message.completed",streamId:n,text:E(s.text||r.text)})}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:E(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:z(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:E(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:I.parse(s.executionId)}:{},...void 0!==s.outcome?{outcome:this.projectToolOutcome(s.outcome)}:{},display:z(s.display,void 0)});case"confirmation-required":{const t=X(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=q.parse(s.form),r=q.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:E(s.message)}:{type:"turn.failed",stage:"execute",message:E(e.interactionFailure)};break;case"error":r.terminalEvent??={type:"turn.failed",stage:s.stage,message:E(s.message)}}}projectToolOutcome(e){return{kind:e.kind,..."reason"in e&&void 0!==e.reason?{reason:E(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:E(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
+ import{createHash as e,randomUUID as t}from"node:crypto";import{APPROVAL_DIFF_PREVIEW_KEY as r,APPROVAL_EXPLANATION_PREVIEW_KEY as s,RUNTIME_ERROR_CODES as n,RUNTIME_FEATURES_V13 as i,RUNTIME_METHODS as a,RUNTIME_PROTOCOL_VERSION as o,RUNTIME_V14_MAX_ATTACHMENT_BYTES as d,RUNTIME_V14_MAX_ATTACHMENT_CHUNK_BYTES as u,RUNTIME_V14_MAX_STAGED_ATTACHMENTS as h,RUNTIME_V14_MAX_TURN_ATTACHMENTS as c,SUPPORTED_RUNTIME_PROTOCOL_VERSIONS as l,approvalExplanationSchema as p,approvalIdSchema as m,normalizeUserInputResultForForm as I,operationIdSchema as v,parseLatestRuntimeMethodResult as g,runtimeDurableEventV13Schema as f,runtimeEphemeralEventV13Schema as y,runtimeInstanceIdSchema as T,runtimeProtocolVersionSchema as A,streamIdSchema as w,threadIdSchema as q,userInputFormSchema as S}from"@roll-agent/protocol";import{AttachmentStore as R}from"./attachment-store.js";import{createSafeCapabilitySnapshot as x}from"../engine/capability-manifest.js";import{isSensitiveFieldName as E,redactSecretText as b,toRedactedToolExecutionRecordSummary as O}from"../tool-bridge/tool-execution-record.js";import{RuntimeEventCursorExpiredError as C,RuntimeEventCursorGapError as P,ThreadStore as U}from"../store/thread-store.js";const N=4194304,F=500,k=1e4,B=16e3,j=64,M=128,V=16,$=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 L(t){const r=JSON.stringify(t);if(void 0===r)throw new RuntimeServiceError(n.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=L(r),i=this.inFlight.get(e);if(void 0!==i)return this.assertMatchingRequest(e,t,n,i),i.response.then(e=>g(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=>g(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,s){if(s.method!==t||s.fingerprint!==r)throw new RuntimeServiceError(n.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 z(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}function D(e,t,r=0){if(void 0!==t&&($.has(t.toLowerCase())||E(t)))return"[redacted]";if(r>=V)return"[nested value omitted]";if(null===e||"boolean"==typeof e)return e;if("string"==typeof e){const t=b(e);return t.length<=B?t:`${t.slice(0,B)}\n[value clipped]`}if("number"==typeof e)return Number.isFinite(e)?e:String(e);if(Array.isArray(e)){const t=e.slice(0,j).map(e=>D(e,void 0,r+1));return e.length>j&&t.push(`[${String(e.length-j)} items omitted]`),t}if(z(e)){const t={};for(const[s,n]of Object.entries(e).slice(0,M))void 0!==n&&(t[s]=D(n,s,r+1));return Object.keys(e).length>M&&(t._omitted=`${String(Object.keys(e).length-M)} keys omitted`),t}return void 0===e?null:String(e)}function W(e){return e instanceof Error?e.message:String(e)}function _(e){const t=e.content;return"string"==typeof t?t:Array.isArray(t)?t.flatMap(e=>z(e)&&"text"===e.type&&"string"==typeof e.text?[e.text]:[]).join(""):""}function G(e){const t=e.endsWith("==")?2:e.endsWith("=")?1:0;return Math.max(0,3*Math.floor(e.length/4)-t)}function J(e){const t=e.content;return"user"===e.role&&Array.isArray(t)?t.flatMap(e=>z(e)?"file"===e.type&&"string"==typeof e.mediaType&&"string"==typeof e.data?[{type:"attachment",mediaType:e.mediaType,bytes:G(e.data)}]:"image"===e.type&&"string"==typeof e.image?[{type:"attachment",mediaType:"image/*",bytes:G(e.image)}]:[]:[]):[]}function H(e){return q.parse(e)}function K(e,t){return{id:H(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 Q(e){if("user"!==e.message.role&&"assistant"!==e.message.role)return;const t=b(_(e.message));return{sequence:e.sequence,role:e.message.role,createdAt:e.createdAt,parts:[...t.length>0?[{type:"text",text:t}]:[],...J(e.message)]}}function X(e){const t=O(e);return{id:v.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:D(t.display,void 0)}}function Y(e,t){const n=D(t.input,void 0),i=void 0===t.explanation?void 0:p.safeParse(b(t.explanation)),a=void 0===t.diff?void 0:D(t.diff,void 0),o=z(n)?{...n,...!0===i?.success?{[s]:i.data}:{},...void 0!==a?{[r]:a}:{}}:n;return{id:m.parse(t.approvalId),turnId:e,agentName:t.agentName,toolName:t.toolName,preview:o,...void 0!==t.reason?{reason:b(t.reason)}:{}}}export class RuntimeService{runtimeInstanceId;engine;store;serverName;serverVersion;runtimeVersion;maxFrameBytes;idempotencyCacheEntries;reasoningSummaryProjector;attachmentStore;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??N,this.idempotencyCacheEntries=s.idempotencyCacheEntries??k,this.mutationRequests=new MutationRequestCache(this.idempotencyCacheEntries),this.reasoningSummaryProjector=s.reasoningSummaryProjector,this.attachmentStore=s.attachmentStore,this.runtimeInstanceId=T.parse(t())}initialize(e){let t;this.assertOpen();for(const r of e.protocolVersions){const e=A.safeParse(r);if(e.success){t=e.data;break}}if(void 0===t)throw new RuntimeServiceError(n.protocolVersionUnsupported,`不支持客户端协议版本:${e.protocolVersions.join(", ")}`,{details:{supportedVersions:l}});const r=i.filter(e=>"reasoning-summary"!==e||void 0!==this.reasoningSummaryProjector),s={protocolVersion:t,runtimeInstanceId:this.runtimeInstanceId,server:{name:this.serverName,version:this.serverVersion,runtimeVersion:this.runtimeVersion},features:r,limits:{maxFrameBytes:this.maxFrameBytes,maxPageSize:F,idempotencyCacheEntries:this.idempotencyCacheEntries}};if("1.4"===t){const e=this.attachmentStore?.limits;return{...s,protocolVersion:t,features:void 0!==this.attachmentStore?[...r,"attachments"]:[...r],limits:{...s.limits,eventReplay:!0,maxAttachmentBytes:e?.maxAttachmentBytes??d,maxAttachmentChunkBytes:e?.maxAttachmentChunkBytes??u,maxTurnAttachments:c,maxStagedAttachments:e?.maxStagedAttachments??h}}}return"1.3"===t?{...s,protocolVersion:t,limits:{...s.limits,eventReplay:!0}}:{...s,protocolVersion:t,limits:{...s.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(n.invalidParams,"thread.list cursor 超出安全整数范围");const r=this.store.listThreads(),s=r.slice(t,t+e.limit).map(e=>K(this.store,e)),i=t+s.length;return{items:s,nextCursor:i<r.length?String(i):null}}async createThread(e){return this.mutationRequests.run(e.requestId,a.threadCreate,e,async()=>{this.assertOpen();const t=await this.engine.createSession(void 0!==e.title?{title:e.title}:{});return this.rememberSession(t),{thread:K(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:K(this.store,t),messages:{items:r.entries.flatMap(e=>{const t=Q(e);return void 0===t?[]:[t]}),nextBeforeSequence:r.nextBeforeSequence??null},operations:{items:s.entries.map(X),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 C)throw new RuntimeServiceError(n.eventCursorExpired,"Runtime Event cursor 已超出当前 Thread 的保留窗口,请回退到 thread.snapshot",{details:{threadId:e.threadId}});if(t instanceof P)throw new RuntimeServiceError(n.eventCursorGap,"Runtime Event cursor 与当前 Thread 事件日志不连续,请回退到 thread.snapshot",{details:{threadId:e.threadId}});throw t}}async renameThread(e){return this.mutationRequests.run(e.requestId,a.threadRename,e,()=>(this.assertOpen(),this.requireThread(e.threadId),this.store.updateTitle(e.threadId,e.title),{thread:K(this.store,this.requireThread(e.threadId))}))}async deleteThread(e){return this.mutationRequests.run(e.requestId,a.threadDelete,e,async()=>{if(this.assertOpen(),this.requireThread(e.threadId),this.activeTurns.has(e.threadId))throw new RuntimeServiceError(n.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.attachmentStore?.releaseThread(e.threadId),this.store.deleteThread(e.threadId),{deleted:!0}})}async detachThread(e){return this.mutationRequests.run(e.requestId,a.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:D(x(t.getCapabilityManifest(),t.getCapabilityTurnContext()),void 0)}}async startTurn(e){return this.mutationRequests.run(e.requestId,a.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(n.turnAlreadyActive,`Turn "${e.turnId}" 已属于另一个 Thread`)}if(this.activeTurns.has(e.threadId))throw new RuntimeServiceError(n.turnAlreadyActive,`Thread "${e.threadId}" 已有活动 Turn`,{retryable:!0});const r=await this.requireSession(e.threadId),s=this.findTurnOwner(e.turnId,e.threadId);if(void 0!==s){if(s===e.threadId)return{accepted:!0,turnId:e.turnId};throw new RuntimeServiceError(n.turnAlreadyActive,`Turn "${e.turnId}" 已属于另一个 Thread`)}if(this.activeTurns.has(e.threadId))throw new RuntimeServiceError(n.turnAlreadyActive,`Thread "${e.threadId}" 已有活动 Turn`,{retryable:!0});const i=this.resolveTurnAttachments(e),a={threadId:e.threadId,turnId:e.turnId,session:r,startedAt:(new Date).toISOString(),status:"running",interactionFailure:void 0};this.activeTurns.set(e.threadId,a),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(a,e.input.text,i).catch(()=>{}),{accepted:!0,turnId:e.turnId}})}async cancelTurn(e){return this.mutationRequests.run(e.requestId,a.turnCancel,e,()=>{this.assertOpen();const t=this.activeTurns.get(e.threadId);if(void 0===t||t.turnId!==e.turnId)throw new RuntimeServiceError(n.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,a.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),s="approve"===t.decision?r.session.approve(e.approvalId,t.scope):r.session.reject(e.approvalId,t.reason);if(this.pendingApprovals.delete(e.approvalId),!s)throw this.emitApprovalResolved(r,{status:"cancelled",reason:"审批已失效"}),new RuntimeServiceError(n.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}async stageAttachment(e){return this.mutationRequests.run(e.requestId,a.attachmentStage,e,()=>{this.assertOpen(),this.requireThread(e.threadId);const t=this.requireAttachmentStore().stage({threadId:e.threadId,fileName:e.fileName,mediaType:e.mediaType,bytes:e.bytes,sha256:e.sha256,source:e.source,sourcePath:e.sourcePath});return t.ok||this.throwAttachmentFailure(t),{attachmentId:t.attachmentId,state:t.state,...void 0!==t.descriptor?{descriptor:t.descriptor}:{}}})}async appendAttachmentChunk(e){return this.mutationRequests.run(e.requestId,a.attachmentChunk,e,()=>{this.assertOpen();const t=this.requireAttachmentStore().appendChunk({threadId:e.threadId,attachmentId:e.attachmentId,sequence:e.sequence,dataBase64:e.dataBase64});return t.ok||this.throwAttachmentFailure(t),{receivedBytes:t.receivedBytes,nextSequence:t.nextSequence}})}async commitAttachment(e){return this.mutationRequests.run(e.requestId,a.attachmentCommit,e,()=>{this.assertOpen();const t=this.requireAttachmentStore().commit({threadId:e.threadId,attachmentId:e.attachmentId});return t.ok||this.throwAttachmentFailure(t),{descriptor:t.descriptor}})}async releaseAttachment(e){return this.mutationRequests.run(e.requestId,a.attachmentRelease,e,()=>{this.assertOpen();return{released:this.requireAttachmentStore().release({threadId:e.threadId,attachmentId:e.attachmentId}).released}})}requireAttachmentStore(){if(void 0===this.attachmentStore)throw new RuntimeServiceError(n.capabilityUnavailable,"Runtime 未配置附件存储,attachments 能力不可用");return this.attachmentStore}throwAttachmentFailure(e){throw new RuntimeServiceError(e.code,e.message,{...void 0!==e.retryable?{retryable:e.retryable}:{}})}resolveTurnAttachments(e){const t=e.input.attachments;if(void 0===t||0===t.length)return;const r=this.requireAttachmentStore(),s=new Set,i=[];for(const a of t){if(s.has(a))throw new RuntimeServiceError(n.invalidParams,`附件 "${a}" 在同一 Turn 中重复引用`);s.add(a);const t=r.readCommitted({threadId:e.threadId,attachmentId:a});t.ok||this.throwAttachmentFailure(t),i.push({data:t.dataBase64,mediaType:t.descriptor.mediaType})}return i}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=I(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:X(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(),this.attachmentStore?.close(),await Promise.allSettled(e.map(e=>e.close())),this.mutationRequests.clear(),this.fatalErrorListeners.clear()}assertOpen(){if(this.closing)throw new RuntimeServiceError(n.runtimeClosing,"Runtime 正在关闭",{retryable:!0})}requireThread(e){const t=this.store.getThread(e);if(void 0===t)throw new RuntimeServiceError(n.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:o,runtimeInstanceId:this.runtimeInstanceId,sequence:this.sequence,timestamp:s,threadId:e,...void 0!==t?{turnId:t}:{}},i=f.safeParse(r),a=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:y.parse(r)};this.sequence+=1;for(const e of this.listeners)try{e(a)}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,r){const s={streamId:void 0,text:"",terminalEvent:void 0},n=void 0!==r&&r.length>0?{text:t,attachments:r}:t;let i;try{for await(const t of e.session.send(n))this.projectSessionEvent(e,s,t);i=s.terminalEvent??{type:"turn.completed"}}catch(e){i=s.terminalEvent??{type:"turn.failed",stage:"execute",message:b(W(e))}}finally{this.activeTurns.get(e.threadId)===e&&this.activeTurns.delete(e.threadId),this.settleTurnOwner(e)}this.emitTurnTerminal(e,i)}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=w.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??w.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??w.parse(t());return void this.emit(e.threadId,e.turnId,{type:"message.completed",streamId:n,text:b(s.text||r.text)})}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:b(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:D(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:b(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:v.parse(s.executionId)}:{},...void 0!==s.outcome?{outcome:this.projectToolOutcome(s.outcome)}:{},display:D(s.display,void 0)});case"confirmation-required":{const t=Y(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=S.parse(s.form),r=S.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:b(s.message)}:{type:"turn.failed",stage:"execute",message:b(e.interactionFailure)};break;case"error":r.terminalEvent??={type:"turn.failed",stage:s.stage,message:b(s.message)}}}projectToolOutcome(e){return{kind:e.kind,..."reason"in e&&void 0!==e.reason?{reason:b(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(n.approvalNotFound,`Approval "${e.approvalId}" 不存在或已失效`);return t}emitApprovalResolved(e,t){const r="reason"in t&&void 0!==t.reason?{...t,reason:b(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 ToolSet } from "ai";
2
+ import type { FileChangeDiff } from "@roll-agent/protocol";
2
3
  import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
4
  import type { AgentTool } from "@roll-agent/core/types/agent";
4
5
  import type { SessionApprovalMemory } from "../approval/approval-memory.ts";
@@ -30,6 +31,7 @@ export interface ApprovalRequest {
30
31
  readonly reason: string | undefined;
31
32
  readonly explanation?: string;
32
33
  readonly sessionGrantLabel?: string;
34
+ readonly diff?: FileChangeDiff;
33
35
  }
34
36
  export interface ToolBridgeContext {
35
37
  readonly policy?: ToolPolicy;
@@ -43,6 +45,7 @@ interface ApprovalDisplayOptions {
43
45
  readonly includePolicyReason?: boolean;
44
46
  readonly memoryKey?: string;
45
47
  readonly sessionGrantLabel?: string;
48
+ readonly diff?: FileChangeDiff;
46
49
  }
47
50
  export interface BuiltToolset {
48
51
  readonly tools: ToolSet;
@@ -1 +1 @@
1
- import{lstatSync as e,readdirSync as t,readlinkSync as n,realpathSync as o,statSync as r}from"node:fs";import{basename as i,dirname as s,isAbsolute as a,resolve as c}from"node:path";import{jsonSchema as u,tool as l}from"ai";import{preflightToolCall as f}from"@roll-agent/core/tool-runtime/preflight";import{ToolRegistry as d}from"./naming.js";import{TOOL_OUTCOME_KINDS as p,failedToolResult as m,normalizeToolResult as v,toolResultToModelOutput as g}from"./normalize-result.js";import{TOOL_RESOURCE_ACCESS_MODES as y,TOOL_RESOURCE_HINT_KINDS as h,executeCoordinatedTool as S}from"./tool-execution-coordinator.js";export const ROLL_RESOURCE_HINTS_META_KEY="roll/resourceHints";function b(e){return"object"!=typeof e||null===e||Array.isArray(e)?{}:e}function $(e){return`参数校验失败: ${e.map(e=>e.message).join("; ")}`}function w(e){return!0===e?.readOnlyHint&&!0!==e.destructiveHint?y.read:y.write}function C(e){return"string"==typeof e||"number"==typeof e?[e]:Array.isArray(e)&&0!==e.length&&e.every(e=>"string"==typeof e||"number"==typeof e)?e:void 0}const N=256,k=new Map;function A(e,t){for(let n=0;n<e.length;n+=1){const o=e.charCodeAt(n),r=o>=65&&o<=90?String.fromCharCode(o+32):o>=97&&o<=122?String.fromCharCode(o-32):void 0;if(void 0===r)continue;const i=`${e.slice(0,n)}${r}${e.slice(n+1)}`;if(!t.has(i))return i}}function O(e){return"object"==typeof e&&null!==e&&"code"in e?e.code:void 0}function x(t,n,o){const r=A(n,o);if(void 0!==r)try{const o=e(c(t,n)),i=e(c(t,r));return o.dev===i.dev&&o.ino===i.ino}catch(e){const t=O(e);return"ENOENT"!==t&&"ENOTDIR"!==t&&void 0}}function j(e,t){if(k.size>=N){const e=k.keys().next().value;void 0!==e&&k.delete(e)}return k.set(e,t),t}function L(n){const o=k.get(n);if(void 0!==o)return o;let r;try{r=e(n).dev}catch{return j(n,!0)}let i=n;for(;;){const o=k.get(i);if(void 0!==o)return j(n,o);try{const e=t(i),o=new Set(e);for(const t of e){const e=x(i,t,o);if(void 0!==e)return j(i,e),j(n,e)}}catch{}const a=s(i);if(a===i)break;try{if(e(a).dev!==r)break}catch{break}i=a}return j(n,!0)}function T(e){let t=e.normalize("NFC");for(let e=0;e<8;e+=1){const e=t.toUpperCase().toLowerCase().normalize("NFC");if(e===t)return t;t=e}return t}function E(e,t){try{const n=r(e,{bigint:!0}),o=t.map(e=>T(e));return`file-unresolved-alias:${n.dev.toString()}:${n.ino.toString()}:${JSON.stringify(o)}`}catch{return}}function R(t,r=new Set){let a=t;const u=[];for(;;)try{const e=o.native(a),t=u.reverse().map(e=>e.normalize("NFC"));if(0===t.length)return{path:e};const n=L(e),r=c(e,...n?t.map(e=>e.toLowerCase()):t),i=n?E(e,t):void 0;return{path:r,...void 0===i?{}:{unresolvedAliasKey:i}}}catch{try{if(e(a).isSymbolicLink()&&!r.has(a)){const e=new Set(r);e.add(a);const t=n(a);return R(c(s(a),t,...[...u].reverse()),e)}}catch{}const o=s(a);if(o===a)return{path:t};u.push(i(a)),a=o}}function z(e){try{const t=r(e,{bigint:!0});return`file-inode:${t.dev.toString()}:${t.ino.toString()}`}catch{return}}function H(e,t,n){const o=String(t).trim();if(0!==o.length)switch(e.kind){case h.file:{if(!a(o)&&void 0===n)return;const e=R(c(n??"/",o)),t=z(e.path);return[`file:${e.path}`,...void 0===e.unresolvedAliasKey?[]:[e.unresolvedAliasKey],...void 0===t?[]:[t]]}case h.browserSession:return[`browser-session:${o}`];case h.conversation:return[`conversation:${o}`];case h.custom:{const t=e.namespace?.trim();return t?[`${t}:${o}`]:void 0}}}function K(e,t,n,o,r){const i=w(o),s=b(n),a=[{key:`agent:${e}`,mode:i}],c=[];for(const e of r??[]){if(!Object.hasOwn(s,e.field))continue;const n=C(s[e.field]);if(void 0===n)return a;for(const o of n){const n=H(e,o,t);if(void 0===n||0===n.length)return a;c.push(...n.map(t=>({key:t,mode:e.mode??i})))}}return 0===c.length?a:[{key:`agent:${e}`,mode:y.read},...c]}export async function gateToolCall(e,t,n,o,r,i){if(!e.policy)return;const s=e.policy.check({agentName:t,toolName:n,input:o,...r?{annotations:r}:{}});if("deny"===s.action)return m(p.policyDenied,"策略拒绝执行"+(s.reason?`: ${s.reason}`:""),s.reason?{reason:s.reason}:{});if("confirm"===s.action){const r=i?.memoryKey;if(void 0!==r&&e.approvalMemory?.isGranted(r))return;const a=await e.requestApproval({agentName:t,toolName:n,input:o,reason:!1===i?.includePolicyReason?void 0:s.reason,...void 0!==i?.explanation?{explanation:i.explanation}:{},...void 0!==r&&void 0!==i?.sessionGrantLabel?{sessionGrantLabel:i.sessionGrantLabel}:{}});if(!a.approved)return m(p.userRejected,"已取消执行"+(a.reason?`: ${a.reason}`:""),a.reason?{reason:a.reason}:{});void 0!==r&&"session"===a.scope&&e.approvalMemory?.grant(r)}}export function buildAgentToolset(e,t,n=new d){const o={};for(const r of e){const{client:e,agentName:i,agentSource:s,transport:a,runtimeOwnership:c,resourceBaseDir:d}=r;for(const{tool:y,annotations:h,resourceHints:w}of r.tools){const r=n.register(i,y.name,{...s?{agentSource:s}:{},...a?{transport:a}:{},...c?{runtimeOwnership:c}:{},...h?{annotations:h}:{}}),C={prepare:async e=>{const n=b(e),o=f(y,n);return o.ok?gateToolCall(t,i,y.name,n,h):m(p.invalidInput,$(o.issues),{raw:o.issues})},resources:e=>K(i,d,e,h,w)};t.coordinator?.register(r,C),o[r]=l({description:y.description??`${y.name} (via ${i})`,inputSchema:u(y.inputSchema),toModelOutput:({output:e})=>g(e),execute:async(n,o)=>{const i=b(n);return S(t.coordinator,C,r,o.toolCallId,i,o.abortSignal,async()=>{const t=o.abortSignal?{signal:o.abortSignal}:void 0,n=await e.callTool({name:y.name,arguments:i},void 0,t);return v(n)})}})}}return{tools:o,registry:n}}
1
+ import{lstatSync as e,readdirSync as t,readlinkSync as n,realpathSync as o,statSync as r}from"node:fs";import{basename as i,dirname as s,isAbsolute as a,resolve as c}from"node:path";import{jsonSchema as u,tool as l}from"ai";import{preflightToolCall as f}from"@roll-agent/core/tool-runtime/preflight";import{ToolRegistry as d}from"./naming.js";import{TOOL_OUTCOME_KINDS as p,failedToolResult as m,normalizeToolResult as v,toolResultToModelOutput as g}from"./normalize-result.js";import{TOOL_RESOURCE_ACCESS_MODES as y,TOOL_RESOURCE_HINT_KINDS as h,executeCoordinatedTool as S}from"./tool-execution-coordinator.js";export const ROLL_RESOURCE_HINTS_META_KEY="roll/resourceHints";function b(e){return"object"!=typeof e||null===e||Array.isArray(e)?{}:e}function $(e){return`参数校验失败: ${e.map(e=>e.message).join("; ")}`}function w(e){return!0===e?.readOnlyHint&&!0!==e.destructiveHint?y.read:y.write}function C(e){return"string"==typeof e||"number"==typeof e?[e]:Array.isArray(e)&&0!==e.length&&e.every(e=>"string"==typeof e||"number"==typeof e)?e:void 0}const N=256,k=new Map;function A(e,t){for(let n=0;n<e.length;n+=1){const o=e.charCodeAt(n),r=o>=65&&o<=90?String.fromCharCode(o+32):o>=97&&o<=122?String.fromCharCode(o-32):void 0;if(void 0===r)continue;const i=`${e.slice(0,n)}${r}${e.slice(n+1)}`;if(!t.has(i))return i}}function O(e){return"object"==typeof e&&null!==e&&"code"in e?e.code:void 0}function x(t,n,o){const r=A(n,o);if(void 0!==r)try{const o=e(c(t,n)),i=e(c(t,r));return o.dev===i.dev&&o.ino===i.ino}catch(e){const t=O(e);return"ENOENT"!==t&&"ENOTDIR"!==t&&void 0}}function j(e,t){if(k.size>=N){const e=k.keys().next().value;void 0!==e&&k.delete(e)}return k.set(e,t),t}function L(n){const o=k.get(n);if(void 0!==o)return o;let r;try{r=e(n).dev}catch{return j(n,!0)}let i=n;for(;;){const o=k.get(i);if(void 0!==o)return j(n,o);try{const e=t(i),o=new Set(e);for(const t of e){const e=x(i,t,o);if(void 0!==e)return j(i,e),j(n,e)}}catch{}const a=s(i);if(a===i)break;try{if(e(a).dev!==r)break}catch{break}i=a}return j(n,!0)}function T(e){let t=e.normalize("NFC");for(let e=0;e<8;e+=1){const e=t.toUpperCase().toLowerCase().normalize("NFC");if(e===t)return t;t=e}return t}function E(e,t){try{const n=r(e,{bigint:!0}),o=t.map(e=>T(e));return`file-unresolved-alias:${n.dev.toString()}:${n.ino.toString()}:${JSON.stringify(o)}`}catch{return}}function R(t,r=new Set){let a=t;const u=[];for(;;)try{const e=o.native(a),t=u.reverse().map(e=>e.normalize("NFC"));if(0===t.length)return{path:e};const n=L(e),r=c(e,...n?t.map(e=>e.toLowerCase()):t),i=n?E(e,t):void 0;return{path:r,...void 0===i?{}:{unresolvedAliasKey:i}}}catch{try{if(e(a).isSymbolicLink()&&!r.has(a)){const e=new Set(r);e.add(a);const t=n(a);return R(c(s(a),t,...[...u].reverse()),e)}}catch{}const o=s(a);if(o===a)return{path:t};u.push(i(a)),a=o}}function z(e){try{const t=r(e,{bigint:!0});return`file-inode:${t.dev.toString()}:${t.ino.toString()}`}catch{return}}function H(e,t,n){const o=String(t).trim();if(0!==o.length)switch(e.kind){case h.file:{if(!a(o)&&void 0===n)return;const e=R(c(n??"/",o)),t=z(e.path);return[`file:${e.path}`,...void 0===e.unresolvedAliasKey?[]:[e.unresolvedAliasKey],...void 0===t?[]:[t]]}case h.browserSession:return[`browser-session:${o}`];case h.conversation:return[`conversation:${o}`];case h.custom:{const t=e.namespace?.trim();return t?[`${t}:${o}`]:void 0}}}function K(e,t,n,o,r){const i=w(o),s=b(n),a=[{key:`agent:${e}`,mode:i}],c=[];for(const e of r??[]){if(!Object.hasOwn(s,e.field))continue;const n=C(s[e.field]);if(void 0===n)return a;for(const o of n){const n=H(e,o,t);if(void 0===n||0===n.length)return a;c.push(...n.map(t=>({key:t,mode:e.mode??i})))}}return 0===c.length?a:[{key:`agent:${e}`,mode:y.read},...c]}export async function gateToolCall(e,t,n,o,r,i){if(!e.policy)return;const s=e.policy.check({agentName:t,toolName:n,input:o,...r?{annotations:r}:{}});if("deny"===s.action)return m(p.policyDenied,"策略拒绝执行"+(s.reason?`: ${s.reason}`:""),s.reason?{reason:s.reason}:{});if("confirm"===s.action){const r=i?.memoryKey;if(void 0!==r&&e.approvalMemory?.isGranted(r))return;const a=await e.requestApproval({agentName:t,toolName:n,input:o,reason:!1===i?.includePolicyReason?void 0:s.reason,...void 0!==i?.explanation?{explanation:i.explanation}:{},...void 0!==i?.diff?{diff:i.diff}:{},...void 0!==r&&void 0!==i?.sessionGrantLabel?{sessionGrantLabel:i.sessionGrantLabel}:{}});if(!a.approved)return m(p.userRejected,"已取消执行"+(a.reason?`: ${a.reason}`:""),a.reason?{reason:a.reason}:{});void 0!==r&&"session"===a.scope&&e.approvalMemory?.grant(r)}}export function buildAgentToolset(e,t,n=new d){const o={};for(const r of e){const{client:e,agentName:i,agentSource:s,transport:a,runtimeOwnership:c,resourceBaseDir:d}=r;for(const{tool:y,annotations:h,resourceHints:w}of r.tools){const r=n.register(i,y.name,{...s?{agentSource:s}:{},...a?{transport:a}:{},...c?{runtimeOwnership:c}:{},...h?{annotations:h}:{}}),C={prepare:async e=>{const n=b(e),o=f(y,n);return o.ok?gateToolCall(t,i,y.name,n,h):m(p.invalidInput,$(o.issues),{raw:o.issues})},resources:e=>K(i,d,e,h,w)};t.coordinator?.register(r,C),o[r]=l({description:y.description??`${y.name} (via ${i})`,inputSchema:u(y.inputSchema),toModelOutput:({output:e})=>g(e),execute:async(n,o)=>{const i=b(n);return S(t.coordinator,C,r,o.toolCallId,i,o.abortSignal,async()=>{const t=o.abortSignal?{signal:o.abortSignal}:void 0,n=await e.callTool({name:y.name,arguments:i},void 0,t);return v(n)})}})}}return{tools:o,registry:n}}
@@ -1,5 +1,6 @@
1
1
  import { type ToolSet } from "ai";
2
2
  import { z } from "zod";
3
+ import type { FileChangeDiff } from "@roll-agent/protocol";
3
4
  import type { ToolRegistry } from "../naming.ts";
4
5
  import type { ToolBridgeContext } from "../build-tools.ts";
5
6
  import { type NormalizedToolResult } from "../normalize-result.ts";
@@ -37,6 +38,6 @@ declare const editFileInputSchema: z.ZodObject<{
37
38
  }[];
38
39
  }>;
39
40
  export type EditFileInput = z.infer<typeof editFileInputSchema>;
40
- export declare function executeEditFile(settings: ResolvedFileToolsSettings, tracker: FileStateTracker, input: EditFileInput): NormalizedToolResult;
41
+ export declare function executeEditFile(settings: ResolvedFileToolsSettings, tracker: FileStateTracker, input: EditFileInput, previewed?: FileChangeDiff): NormalizedToolResult;
41
42
  export declare function buildEditFileTool(settings: ResolvedFileToolsSettings, tracker: FileStateTracker, registry: ToolRegistry, ctx: ToolBridgeContext): ToolSet;
42
43
  export {};
@@ -1 +1 @@
1
- import{tool as t}from"ai";import{z as e}from"zod";import{gateToolCall as n}from"../build-tools.js";import{TOOL_OUTCOME_KINDS as i,failedToolResult as r,successfulToolResult as o,toolResultToModelOutput as s}from"../normalize-result.js";import{OPAQUE_SIDE_EFFECT_RESOURCE as l,TOOL_RESOURCE_ACCESS_MODES as a,executeCoordinatedTool as d}from"../tool-execution-coordinator.js";import{findAllExact as c,findOldString as f,formatMultiMatchDiagnosis as u,formatNoMatchDiagnosis as g,lineNumberAt as p,renderNumberedLines as _}from"./match-pipeline.js";import{canonicalResourcePath as h,captureFilePathAdmission as m,escapesWorkdir as $,formatPathForApproval as w,loadTextFile as k,resolveFilePath as x,revalidateFilePathAdmission as v,saveTextFile as F}from"./file-io.js";import{rejectInvalidTextPayload as b}from"./control-chars.js";import{FILE_FRESHNESS as j}from"./file-state-tracker.js";import{FILE_TOOLS_AGENT_NAME as S}from"./settings.js";export const EDIT_FILE_TOOL_NAME="edit_file";const y=3,E="若修改面较大或文件已大幅变化,可改用 roll__write_file 整文件重写(需先 read_file)",I=e.object({old_string:e.string().min(1).describe("要替换的原文,必须逐字复制自 read_file 读到的内容(不含行号前缀)"),new_string:e.string().describe("替换后的新内容,必须与 old_string 不同"),replace_all:e.boolean().optional().describe("替换所有精确匹配处,默认 false(要求唯一匹配)")}),M=e.object({file_path:e.string().min(1).describe("要修改的文件路径,相对当前工作目录或绝对路径"),edits:e.array(I).min(1).describe("按顺序应用的编辑列表;任何一条失败则整体不写入")}),L={};function P(t){const e=(t.match(/\r\n/g)??[]).length,n=(t.match(/(?<!\r)\n/g)??[]).length;return e>0&&0===n}function C(t,e){return e?t.replace(/\r?\n/g,"\r\n"):t}function O(t,e,n){for(const i of t)i.position>e&&(i.position+=n)}function T(t,e,n,i){const r=t.slice(0,e.start)+n+t.slice(e.end);return O(i,e.start,n.length-(e.end-e.start)),i.push({position:e.start,length:n.length}),r}function B(t,e,n,i){let r=t;for(let t=e.length-1;t>=0;t-=1){const o=e.at(t);void 0!==o&&(r=r.slice(0,o.start)+n+r.slice(o.end),O(i,o.start,n.length-(o.end-o.start)),i.push({position:o.start,length:n.length}))}return r}function U(t,e,n,i){const r=e.split("\n"),o=[`已完成 ${String(n.length)} 处修改并写入 ${t}:`];n.forEach((t,n)=>{const i=p(e,t.position),s=t.position+Math.max(t.length-1,0),l=p(e,Math.min(s,Math.max(e.length-1,0))),a=Math.max(1,i-3),d=Math.min(r.length,l+3);o.push(`[${String(n+1)}] 第 ${String(i)} 行附近:`),o.push(_(r.slice(a-1,d),a))});const s=o.join("\n");return s.length>i?`${s.slice(0,i)}\n…(快照过长已截断,修改均已写入)`:s}function z(t){for(const[e,n]of t.edits.entries()){const i=`第 ${String(e+1)} 条编辑(共 ${String(t.edits.length)} 条)的`,r=b(`${i} old_string`,n.old_string);if(void 0!==r)return r;const o=b(`${i} new_string`,n.new_string);if(void 0!==o)return o}}export function executeEditFile(t,e,n){const s=z(n);if(void 0!==s)return s;const l=x(t.workdir,n.file_path),a=k(l,{maxFileBytes:t.maxFileBytes});if(!a.ok)return r(i.invalidInput,a.message);const d=e.checkFreshness(a.key,a.content);if(d===j.unread)return r(i.toolFailed,`尚未读取过 ${l}。请先用 roll__read_file 读取文件,再基于读到的内容编辑。`);if(d===j.stale)return r(i.toolFailed,`${l} 在你上次读取后已被修改(可能是用户或其他程序改动)。请重新 roll__read_file 获取最新内容,再基于最新内容编辑,不要用旧内容重试。`);const p=P(a.content);let _=a.content;const h=[];for(const[t,e]of n.edits.entries()){const o=`第 ${String(t+1)} 条编辑(共 ${String(n.edits.length)} 条)`;if(e.old_string===e.new_string)return r(i.invalidInput,`${o}:new_string 与 old_string 相同,没有可应用的变化。未写入任何修改。`);const s=C(e.old_string,p),l=C(e.new_string,p);if(s===l)return r(i.invalidInput,`${o}:该文件使用 CRLF 换行,行尾会自动适配,这条编辑在适配后 new_string 与 old_string 相同(只改换行符不会产生变化)。未写入任何修改。`);if(!0===e.replace_all){const t=c(_,s);if(0===t.length)return r(i.toolFailed,`${o}失败,未写入任何修改。\n${g(_,s)}\n${E}`);_=B(_,t,l,h);continue}const a=f(_,s);if("none"===a.kind)return r(i.toolFailed,`${o}失败,未写入任何修改。\n${g(_,s)}\n${E}`);if("multiple"===a.kind)return r(i.toolFailed,`${o}失败,未写入任何修改。\n${u(_,a.spans)}`);_=T(_,a.span,l,h)}return _===a.content?r(i.invalidInput,"所有编辑应用后文件内容与原文件完全相同,没有可写入的变化。未写入任何修改。"):(F(l,_,a.hadBom),e.recordKnownContent(a.key,_),o(U(l,_,h,t.maxOutputChars)))}export function buildEditFileTool(e,o,c,f){const u=c.register(S,"edit_file",{annotations:L}),g={prepare:async t=>{const o=M.safeParse(t);if(!o.success)return r(i.invalidInput,"参数校验失败: file_path 必须为非空字符串,edits 至少一条且每条含非空 old_string 与 new_string");const s=z(o.data);if(void 0!==s)return s;const l=w(e.workdir,o.data.file_path),a=$(e.workdir,o.data.file_path)?void 0:"edit_file:workdir";return n(f,S,"edit_file",o.data,L,{explanation:`修改 ${l}:${String(o.data.edits.length)} 处编辑`,...void 0!==a?{memoryKey:a,sessionGrantLabel:"本会话内不再询问:修改工作目录内的文件"}:{}})},resources:t=>{const n=M.safeParse(t),i=n.success?`file:${h(x(e.workdir,n.data.file_path))}`:`file-tools:${e.workdir}`;return[{key:l,mode:a.read},{key:i,mode:a.write}]},captureExecutionState:t=>{const n=M.safeParse(t);return n.success?m(e.workdir,n.data.file_path):void 0},revalidateExecution:(t,n)=>{const i=M.safeParse(t);if(i.success)return v(e.workdir,i.data.file_path,n)}};return f.coordinator?.register(u,g),{[u]:t({description:"对文本文件做精确字符串替换。使用前必须先 roll__read_file 读取文件;old_string 逐字复制读到的内容(不含行号前缀)且须唯一定位;同一文件多处修改放进 edits 数组一次提交,任何一条失败则整体不写入。成功返回已含修改点最新内容,无需再次读取确认。仅支持文本内容,old_string/new_string 含原始 NUL 字符(U+0000)或不成对的 UTF-16 代理项会被拒绝;需要写入转义序列文本时用双反斜杠,需要原始字节时改用 shell。",inputSchema:M,toModelOutput:({output:t})=>s(t),execute:(t,n)=>d(f.coordinator,g,u,n.toolCallId,t,n.abortSignal,()=>Promise.resolve(executeEditFile(e,o,t)))})}}
1
+ import{tool as e}from"ai";import{z as t}from"zod";import{gateToolCall as i}from"../build-tools.js";import{TOOL_OUTCOME_KINDS as r,failedToolResult as o,toolResultToModelOutput as n}from"../normalize-result.js";import{OPAQUE_SIDE_EFFECT_RESOURCE as d,TOOL_RESOURCE_ACCESS_MODES as s,executeCoordinatedTool as a}from"../tool-execution-coordinator.js";import{lineNumberAt as l,renderNumberedLines as f}from"./match-pipeline.js";import{canonicalResourcePath as c,captureFilePathAdmission as u,escapesWorkdir as p,formatPathForApproval as m,loadTextFile as _,resolveFilePath as h,revalidateFilePathAdmission as g,saveTextFile as v}from"./file-io.js";import{rejectInvalidTextPayload as k}from"./control-chars.js";import{FILE_FRESHNESS as w}from"./file-state-tracker.js";import{FILE_TOOLS_AGENT_NAME as x}from"./settings.js";import{planEdits as b}from"./edit-plan.js";import{describeFileChange as j,fileChangeToolResult as $}from"./file-change-result.js";import{changedLineSignature as y}from"./text-diff.js";export const EDIT_FILE_TOOL_NAME="edit_file";const F=3,E=t.object({old_string:t.string().min(1).describe("要替换的原文,必须逐字复制自 read_file 读到的内容(不含行号前缀)"),new_string:t.string().describe("替换后的新内容,必须与 old_string 不同"),replace_all:t.boolean().optional().describe("替换所有精确匹配处,默认 false(要求唯一匹配)")}),S=t.object({file_path:t.string().min(1).describe("要修改的文件路径,相对当前工作目录或绝对路径"),edits:t.array(E).min(1).describe("按顺序应用的编辑列表;任何一条失败则整体不写入")}),M={};function P(e,t,i,r){const o=t.split("\n"),n=[`已完成 ${String(i.length)} 处修改并写入 ${e}:`];i.forEach((e,i)=>{const r=l(t,e.position),d=e.position+Math.max(e.length-1,0),s=l(t,Math.min(d,Math.max(t.length-1,0))),a=Math.max(1,r-3),c=Math.min(o.length,s+3);n.push(`[${String(i+1)}] 第 ${String(r)} 行附近:`),n.push(f(o.slice(a-1,c),a))});const d=n.join("\n");return d.length>r?`${d.slice(0,r)}\n…(快照过长已截断,修改均已写入)`:d}function I(e){for(const[t,i]of e.edits.entries()){const r=`第 ${String(t+1)} 条编辑(共 ${String(e.edits.length)} 条)的`,o=k(`${r} old_string`,i.old_string);if(void 0!==o)return o;const n=k(`${r} new_string`,i.new_string);if(void 0!==n)return n}}function B(e,t,i){const n=e.checkFreshness(i.key,i.content);return n===w.unread?o(r.toolFailed,`尚未读取过 ${t}。请先用 roll__read_file 读取文件,再基于读到的内容编辑。`):n===w.stale?o(r.toolFailed,`${t} 在你上次读取后已被修改(可能是用户或其他程序改动)。请重新 roll__read_file 获取最新内容,再基于最新内容编辑,不要用旧内容重试。`):void 0}function L(e,t){return{...u(e,t),previewed:void 0}}function O(e){return"object"==typeof e&&null!==e&&"previewed"in e}function T(e,t,i){const o=h(e.workdir,i.file_path),n=_(o,{maxFileBytes:e.maxFileBytes});if(!n.ok||void 0!==B(t,o,n))return{kind:"unavailable"};const d=b(n.content,i.edits);if(!d.ok)return d.result.outcome.kind===r.invalidInput?{kind:"rejected",result:d.result}:{kind:"unavailable"};const s=j({workdir:e.workdir,inputPath:i.file_path,change:"modify",before:n.content,after:d.next});return void 0===s?{kind:"unavailable"}:{kind:"diff",diff:s}}function C(e,t){return void 0!==e.unified&&void 0!==t.unified?y(e.unified)===y(t.unified):e.added===t.added&&e.removed===t.removed}export function executeEditFile(e,t,i,n){const d=I(i);if(void 0!==d)return d;const s=h(e.workdir,i.file_path),a=_(s,{maxFileBytes:e.maxFileBytes});if(!a.ok)return o(r.invalidInput,a.message);const l=B(t,s,a);if(void 0!==l)return l;const f=b(a.content,i.edits);if(!f.ok)return f.result;const c=j({workdir:e.workdir,inputPath:i.file_path,change:"modify",before:a.content,after:f.next});return void 0===n||void 0===c||C(n,c)?(v(s,f.next,a.hadBom),t.recordKnownContent(a.key,f.next),$(P(s,f.next,f.applied,e.maxOutputChars),c)):o(r.toolFailed,`${s} 的内容在确认后已发生变化,实际变更与审批时预览的不一致,已阻止写入。请重新 roll__read_file 获取最新内容后再编辑。`)}export function buildEditFileTool(t,l,f,u){const _=f.register(x,"edit_file",{annotations:M}),v={prepare:async(e,n)=>{const d=S.safeParse(e);if(!d.success)return o(r.invalidInput,"参数校验失败: file_path 必须为非空字符串,edits 至少一条且每条含非空 old_string 与 new_string");const s=I(d.data);if(void 0!==s)return s;const a=p(t.workdir,d.data.file_path),f=a?void 0:T(t,l,d.data);if("rejected"===f?.kind)return f.result;const c="diff"===f?.kind?f.diff:void 0;O(n)&&(n.previewed=c);const _=m(t.workdir,d.data.file_path),h=a?void 0:"edit_file:workdir";return i(u,x,"edit_file",d.data,M,{explanation:`修改 ${_}:${String(d.data.edits.length)} 处编辑`,...void 0!==c?{diff:c}:{},...void 0!==h?{memoryKey:h,sessionGrantLabel:"本会话内不再询问:修改工作目录内的文件"}:{}})},resources:e=>{const i=S.safeParse(e),r=i.success?`file:${c(h(t.workdir,i.data.file_path))}`:`file-tools:${t.workdir}`;return[{key:d,mode:s.read},{key:r,mode:s.write}]},captureExecutionState:e=>{const i=S.safeParse(e);return i.success?L(t.workdir,i.data.file_path):void 0},revalidateExecution:(e,i)=>{const r=S.safeParse(e);if(r.success)return g(t.workdir,r.data.file_path,i)}};return u.coordinator?.register(_,v),{[_]:e({description:"对文本文件做精确字符串替换。使用前必须先 roll__read_file 读取文件;old_string 逐字复制读到的内容(不含行号前缀)且须唯一定位;同一文件多处修改放进 edits 数组一次提交,任何一条失败则整体不写入。成功返回已含修改点最新内容,无需再次读取确认。仅支持文本内容,old_string/new_string 含原始 NUL 字符(U+0000)或不成对的 UTF-16 代理项会被拒绝;需要写入转义序列文本时用双反斜杠,需要原始字节时改用 shell。",inputSchema:S,toModelOutput:({output:e})=>n(e),execute:(e,i)=>a(u.coordinator,v,_,i.toolCallId,e,i.abortSignal,i=>Promise.resolve(executeEditFile(t,l,e,O(i)?i.previewed:void 0)))})}}
@@ -0,0 +1,19 @@
1
+ import { type NormalizedToolResult } from "../normalize-result.ts";
2
+ export interface EditEntry {
3
+ readonly old_string: string;
4
+ readonly new_string: string;
5
+ readonly replace_all?: boolean | undefined;
6
+ }
7
+ export interface AppliedEdit {
8
+ position: number;
9
+ length: number;
10
+ }
11
+ export type EditPlan = {
12
+ readonly ok: true;
13
+ readonly next: string;
14
+ readonly applied: readonly AppliedEdit[];
15
+ } | {
16
+ readonly ok: false;
17
+ readonly result: NormalizedToolResult;
18
+ };
19
+ export declare function planEdits(content: string, edits: readonly EditEntry[]): EditPlan;
@@ -0,0 +1 @@
1
+ import{TOOL_OUTCOME_KINDS as n,failedToolResult as t}from"../normalize-result.js";import{findAllExact as e,findOldString as r,formatMultiMatchDiagnosis as i,formatNoMatchDiagnosis as o}from"./match-pipeline.js";const l="若修改面较大或文件已大幅变化,可改用 roll__write_file 整文件重写(需先 read_file)";function s(n){const t=(n.match(/\r\n/g)??[]).length,e=(n.match(/(?<!\r)\n/g)??[]).length;return t>0&&0===e}function c(n,t){return t?n.replace(/\r?\n/g,"\r\n"):n}function a(n,t,e){for(const r of n)r.position>t&&(r.position+=e)}function u(n,t,e,r){const i=n.slice(0,t.start)+e+n.slice(t.end);return a(r,t.start,e.length-(t.end-t.start)),r.push({position:t.start,length:e.length}),i}function g(n,t,e,r){let i=n;for(let n=t.length-1;n>=0;n-=1){const o=t.at(n);void 0!==o&&(i=i.slice(0,o.start)+e+i.slice(o.end),a(r,o.start,e.length-(o.end-o.start)),r.push({position:o.start,length:e.length}))}return i}function f(n){return{ok:!1,result:n}}export function planEdits(a,p){const d=s(a);let h=a;const _=[];for(const[s,a]of p.entries()){const $=`第 ${String(s+1)} 条编辑(共 ${String(p.length)} 条)`;if(a.old_string===a.new_string)return f(t(n.invalidInput,`${$}:new_string 与 old_string 相同,没有可应用的变化。未写入任何修改。`));const m=c(a.old_string,d),w=c(a.new_string,d);if(m===w)return f(t(n.invalidInput,`${$}:该文件使用 CRLF 换行,行尾会自动适配,这条编辑在适配后 new_string 与 old_string 相同(只改换行符不会产生变化)。未写入任何修改。`));if(!0===a.replace_all){const r=e(h,m);if(0===r.length)return f(t(n.toolFailed,`${$}失败,未写入任何修改。\n${o(h,m)}\n${l}`));h=g(h,r,w,_);continue}const k=r(h,m);if("none"===k.kind)return f(t(n.toolFailed,`${$}失败,未写入任何修改。\n${o(h,m)}\n${l}`));if("multiple"===k.kind)return f(t(n.toolFailed,`${$}失败,未写入任何修改。\n${i(h,k.spans)}`));h=u(h,k.span,w,_)}return h===a?f(t(n.invalidInput,"所有编辑应用后文件内容与原文件完全相同,没有可写入的变化。未写入任何修改。")):{ok:!0,next:h,applied:_}}
@@ -0,0 +1,11 @@
1
+ import type { FileChangeDiff, FileChangeKind } from "@roll-agent/protocol";
2
+ import { type NormalizedToolResult } from "../normalize-result.ts";
3
+ export interface DescribeFileChangeInput {
4
+ readonly workdir: string;
5
+ readonly inputPath: string;
6
+ readonly change: FileChangeKind;
7
+ readonly before: string;
8
+ readonly after: string;
9
+ }
10
+ export declare function describeFileChange(input: DescribeFileChangeInput): FileChangeDiff | undefined;
11
+ export declare function fileChangeToolResult(text: string, diff: FileChangeDiff | undefined): NormalizedToolResult;
@@ -0,0 +1 @@
1
+ import{successfulToolResult as e}from"../normalize-result.js";import{formatPathForApproval as r}from"./file-io.js";import{buildFileChangeDiff as t}from"./text-diff.js";export function describeFileChange(e){try{return t({path:r(e.workdir,e.inputPath),change:e.change,before:e.before,after:e.after})}catch{return}}export function fileChangeToolResult(r,t){const o=e(r);return void 0===t?o:e({text:r,diff:t},{model:o.model})}
@@ -0,0 +1,33 @@
1
+ import type { FileChangeDiff, FileChangeKind } from "@roll-agent/protocol";
2
+ export declare const FILE_CHANGE_DIFF_LIMITS: {
3
+ readonly maxUnifiedChars: 12000;
4
+ readonly maxInputBytes: 1048576;
5
+ readonly maxEditDistance: 1000;
6
+ readonly contextLines: 3;
7
+ };
8
+ export type LineOp = {
9
+ readonly kind: "equal";
10
+ readonly oldIndex: number;
11
+ readonly newIndex: number;
12
+ } | {
13
+ readonly kind: "delete";
14
+ readonly oldIndex: number;
15
+ } | {
16
+ readonly kind: "insert";
17
+ readonly newIndex: number;
18
+ };
19
+ type FileChangeDiffLimits = {
20
+ readonly [K in keyof typeof FILE_CHANGE_DIFF_LIMITS]: number;
21
+ };
22
+ export interface BuildFileChangeDiffInput {
23
+ readonly path: string;
24
+ readonly change: FileChangeKind;
25
+ readonly before: string;
26
+ readonly after: string;
27
+ readonly limits?: Partial<FileChangeDiffLimits>;
28
+ }
29
+ export declare function splitLinesKeepingNewline(content: string): string[];
30
+ export declare function diffLines(before: readonly string[], after: readonly string[], maxEditDistance: number): LineOp[];
31
+ export declare function changedLineSignature(unified: string): string;
32
+ export declare function buildFileChangeDiff(input: BuildFileChangeDiffInput): FileChangeDiff;
33
+ export {};
@@ -0,0 +1 @@
1
+ export const FILE_CHANGE_DIFF_LIMITS={maxUnifiedChars:12e3,maxInputBytes:1048576,maxEditDistance:1e3,contextLines:3};const e="\";export function splitLinesKeepingNewline(e){if(0===e.length)return[];const n=[];let t=0;for(;t<e.length;){const i=e.indexOf("\n",t);if(-1===i){n.push(e.slice(t));break}n.push(e.slice(t,i+1)),t=i+1}return n}function n(e,n){let t=0;const i=Math.min(e.length,n.length);for(;t<i&&e[t]===n[t];)t+=1;let d=0;for(;d<i-t&&e[e.length-1-d]===n[n.length-1-d];)d+=1;return{prefix:t,suffix:d}}function t(e,n,t){const d=e.length,o=n.length,r=Math.min(d+o,t),l=r+1;let s=new Int32Array(2*r+3);s[l+1]=0;const u=[];for(let t=0;t<=r;t+=1){u.push(s);const r=new Int32Array(s);for(let a=-t;a<=t;a+=2){let f=a===-t||a!==t&&(s[l+a-1]??0)<(s[l+a+1]??0)?s[l+a+1]??0:(s[l+a-1]??0)+1,c=f-a;for(;f<d&&c<o&&e[f]===n[c];)f+=1,c+=1;if(r[l+a]=f,f>=d&&c>=o)return u.push(r),i(u,l,d,o,t)}s=r}}function i(e,n,t,i,d){const o=[];let r=t,l=i;for(let t=d;t>=0;t-=1){const i=e[t];if(void 0===i)break;const d=r-l,s=d===-t||d!==t&&(i[n+d-1]??0)<(i[n+d+1]??0),u=s?d+1:d-1,a=0===t?0:i[n+u]??0,f=a-u;for(;r>a&&l>f;)r-=1,l-=1,o.push({kind:"equal",oldIndex:r,newIndex:l});t>0&&(s?(l-=1,o.push({kind:"insert",newIndex:l})):(r-=1,o.push({kind:"delete",oldIndex:r})))}return o.reverse()}function d(e,n,t,i){const d=[];for(let t=0;t<n;t+=1)d.push({kind:"delete",oldIndex:e+t});for(let e=0;e<i;e+=1)d.push({kind:"insert",newIndex:t+e});return d}function o(e,n,t){return e.map(e=>"equal"===e.kind?{kind:"equal",oldIndex:e.oldIndex+n,newIndex:e.newIndex+t}:"delete"===e.kind?{kind:"delete",oldIndex:e.oldIndex+n}:{kind:"insert",newIndex:e.newIndex+t})}export function diffLines(e,i,r){const{prefix:l,suffix:s}=n(e,i),u=[];for(let e=0;e<l;e+=1)u.push({kind:"equal",oldIndex:e,newIndex:e});const a=e.slice(l,e.length-s),f=i.slice(l,i.length-s),c=t(a,f,r);u.push(...void 0===c?d(l,a.length,l,f.length):o(c,l,l));for(let n=0;n<s;n+=1)u.push({kind:"equal",oldIndex:e.length-s+n,newIndex:i.length-s+n});return u}function r(e,n){const t=e.flatMap((e,n)=>"equal"===e.kind?[]:[n]);if(0===t.length)return[];const i=[];for(const d of t){const t=Math.max(0,d-n),o=Math.min(e.length-1,d+n),r=i.at(-1);void 0!==r&&t<=r.end+1?r.end=Math.max(r.end,o):i.push({start:t,end:o})}return i.map(n=>{const t=e.slice(n.start,n.end+1),i=t.flatMap(e=>"insert"===e.kind?[]:[e.oldIndex]),d=t.flatMap(e=>"delete"===e.kind?[]:[e.newIndex]),o=i[0],r=d[0];return{ops:t,oldStart:void 0===o?l(t,"old"):o+1,oldCount:i.length,newStart:void 0===r?l(t,"new"):r+1,newCount:d.length}})}function l(e,n){const t=e[0];return void 0===t?0:"old"===n?"insert"===t.kind?0:t.oldIndex:"delete"===t.kind?0:t.newIndex}function s(n,t){return t.endsWith("\n")?[`${n}${t.slice(0,-1)}`]:[`${n}${t}`,e]}function u(e,n,t){const i=[`@@ -${String(e.oldStart)},${String(e.oldCount)} +${String(e.newStart)},${String(e.newCount)} @@`];for(const d of e.ops)"equal"===d.kind?i.push(...s(" ",t[d.newIndex]??"")):"delete"===d.kind?i.push(...s("-",n[d.oldIndex]??"")):i.push(...s("+",t[d.newIndex]??""));return i}function a(e,n){let t=0;const i=[];for(const d of e){const e=d.length+1;if(t+e>n)return{text:i.map(e=>`${e}\n`).join(""),truncated:!0};i.push(d),t+=e}return{text:i.map(e=>`${e}\n`).join(""),truncated:!1}}function f(e){let n=0,t=0;for(const i of e)"insert"===i.kind?n+=1:"delete"===i.kind&&(t+=1);return{added:n,removed:t}}export function changedLineSignature(e){return e.split("\n").slice(2).filter(e=>e.startsWith("+")||e.startsWith("-")).join("\n")}export function buildFileChangeDiff(e){const n={...FILE_CHANGE_DIFF_LIMITS,...e.limits},t=splitLinesKeepingNewline(e.before),i=splitLinesKeepingNewline(e.after),d=diffLines(t,i,n.maxEditDistance),o=r(d,n.contextLines),{added:l,removed:s}=f(d);if(Buffer.byteLength(e.before,"utf8")+Buffer.byteLength(e.after,"utf8")>n.maxInputBytes)return{path:e.path,change:e.change,added:l,removed:s,hunks:o.length,truncated:!1};const c=["create"===e.change?"--- /dev/null":`--- a/${e.path}`,`+++ b/${e.path}`],h=o.flatMap(e=>u(e,t,i)),{text:p,truncated:x}=a([...c,...h],n.maxUnifiedChars);return{path:e.path,change:e.change,added:l,removed:s,hunks:o.length,unified:p,truncated:x}}
@@ -1 +1 @@
1
- import{statSync as t}from"node:fs";import{tool as e}from"ai";import{z as r}from"zod";import{gateToolCall as i}from"../build-tools.js";import{TOOL_OUTCOME_KINDS as o,failedToolResult as n,successfulToolResult as s,toolResultToModelOutput as a}from"../normalize-result.js";import{OPAQUE_SIDE_EFFECT_RESOURCE as c,TOOL_RESOURCE_ACCESS_MODES as l,executeCoordinatedTool as d}from"../tool-execution-coordinator.js";import{renderNumberedLines as f}from"./match-pipeline.js";import{canonicalFileKey as u,canonicalResourcePath as p,captureFilePathAdmission as m,escapesWorkdir as _,formatPathForApproval as h,loadTextFile as g,resolveFilePath as k,revalidateFilePathAdmission as w,saveTextFile as v,splitUtf8Bom as $}from"./file-io.js";import{rejectInvalidTextPayload as y}from"./control-chars.js";import{FILE_FRESHNESS as x}from"./file-state-tracker.js";import{FILE_TOOLS_AGENT_NAME as F}from"./settings.js";export const WRITE_FILE_TOOL_NAME="write_file";const j=10,S=20,b=.5,B=r.object({file_path:r.string().min(1).describe("要写入的文件路径,相对当前工作目录或绝对路径"),content:r.string().describe("完整的文件内容(整文件写入,覆盖原有内容)")}),I={};function E(e){try{return!t(e).isDirectory()||"directory"}catch{return!1}}function L(t,e){if(!e.ok)return{shrinking:!1,warning:void 0};const r=e.content.split("\n").length;if(r<S||t>=r*b)return{shrinking:!1,warning:void 0};const i=Math.round(100*(1-t/r));return{shrinking:!0,warning:`⚠ 新内容 ${String(t)} 行,比原文件 ${String(r)} 行减少 ${String(i)}%,请确认是有意删减`}}function P(t,e,r){const i=t.checkFreshness(r.key,r.content);return i===x.unread?n(o.toolFailed,`${e} 已存在。覆盖前请先用 roll__read_file 读取并确认现有内容;若只改部分内容,优先用 roll__edit_file。`):i===x.stale?n(o.toolFailed,`${e} 在你上次读取后已被修改(可能是用户或其他程序改动)。请重新 roll__read_file 确认最新内容后再决定是否覆盖。`):void 0}export function executeWriteFile(t,e,r){const i=y("content",r.content);if(void 0!==i)return i;const a=k(t.workdir,r.file_path),c=E(a);if("directory"===c)return n(o.invalidInput,`${a} 是目录,不能作为文件写入`);if(!0===c){const r=g(a,{maxFileBytes:t.maxFileBytes});if(!r.ok)return n(o.invalidInput,r.message);const i=P(e,a,r);if(void 0!==i)return i}const{content:l,hadBom:d}=$(r.content);v(a,l,d),e.recordKnownContent(u(a),l);const p=l.split("\n"),m=f(p.slice(0,j),1),_=[`已写入 ${a}(${String(p.length)} 行,${String(Buffer.byteLength(r.content,"utf8"))} 字节):`,m];return p.length>j&&_.push(`(预览前 ${String(j)} 行)`),s(_.join("\n"))}export function buildWriteFileTool(t,r,s,f){const u=s.register(F,"write_file",{annotations:I}),v={prepare:async e=>{const s=B.safeParse(e);if(!s.success)return n(o.invalidInput,"参数校验失败: file_path 必须为非空字符串,content 必须为字符串");const a=y("content",s.data.content);if(void 0!==a)return a;const c=s.data.content.split("\n").length,l=k(t.workdir,s.data.file_path),d=g(l,{maxFileBytes:t.maxFileBytes});if(d.ok){const t=P(r,l,d);if(void 0!==t)return t}const{shrinking:u,warning:p}=L(c,d),m=`写入 ${h(t.workdir,s.data.file_path)}(${String(c)} 行)${void 0!==p?`\n${p}`:""}`,w=_(t.workdir,s.data.file_path),v=u||w?void 0:"write_file:workdir";return i(f,F,"write_file",s.data,I,{explanation:m,...void 0!==v?{memoryKey:v,sessionGrantLabel:"本会话内不再询问:写入工作目录内的文件"}:{}})},resources:e=>{const r=B.safeParse(e),i=r.success?`file:${p(k(t.workdir,r.data.file_path))}`:`file-tools:${t.workdir}`;return[{key:c,mode:l.read},{key:i,mode:l.write}]},captureExecutionState:e=>{const r=B.safeParse(e);return r.success?m(t.workdir,r.data.file_path):void 0},revalidateExecution:(e,r)=>{const i=B.safeParse(e);if(i.success)return w(t.workdir,i.data.file_path,r)}};return f.coordinator?.register(u,v),{[u]:e({description:"新建文件或整文件重写。已存在的文件必须先 roll__read_file 读取确认后才能覆盖;只改部分内容时优先用 roll__edit_file。仅支持文本内容,content 含原始 NUL 字符(U+0000)或不成对的 UTF-16 代理项会被拒绝;需要写入转义序列文本时用双反斜杠,需要原始字节时改用 shell。",inputSchema:B,toModelOutput:({output:t})=>a(t),execute:(e,i)=>d(f.coordinator,v,u,i.toolCallId,e,i.abortSignal,()=>Promise.resolve(executeWriteFile(t,r,e)))})}}
1
+ import{statSync as t}from"node:fs";import{tool as e}from"ai";import{z as r}from"zod";import{gateToolCall as i}from"../build-tools.js";import{TOOL_OUTCOME_KINDS as o,failedToolResult as n,toolResultToModelOutput as a}from"../normalize-result.js";import{OPAQUE_SIDE_EFFECT_RESOURCE as s,TOOL_RESOURCE_ACCESS_MODES as c,executeCoordinatedTool as d}from"../tool-execution-coordinator.js";import{renderNumberedLines as l}from"./match-pipeline.js";import{canonicalFileKey as f,canonicalResourcePath as u,captureFilePathAdmission as p,escapesWorkdir as m,formatPathForApproval as h,loadTextFile as _,resolveFilePath as g,revalidateFilePathAdmission as k,saveTextFile as v,splitUtf8Bom as w}from"./file-io.js";import{rejectInvalidTextPayload as y}from"./control-chars.js";import{FILE_FRESHNESS as $}from"./file-state-tracker.js";import{FILE_TOOLS_AGENT_NAME as x}from"./settings.js";import{describeFileChange as F,fileChangeToolResult as j}from"./file-change-result.js";export const WRITE_FILE_TOOL_NAME="write_file";const b=10,S=20,P=.5,B=r.object({file_path:r.string().min(1).describe("要写入的文件路径,相对当前工作目录或绝对路径"),content:r.string().describe("完整的文件内容(整文件写入,覆盖原有内容)")}),I={};function E(e){try{return!t(e).isDirectory()||"directory"}catch{return!1}}function L(t,e){if(!e.ok)return{shrinking:!1,warning:void 0};const r=e.content.split("\n").length;if(r<S||t>=r*P)return{shrinking:!1,warning:void 0};const i=Math.round(100*(1-t/r));return{shrinking:!0,warning:`⚠ 新内容 ${String(t)} 行,比原文件 ${String(r)} 行减少 ${String(i)}%,请确认是有意删减`}}function T(t,e,r){const i=t.checkFreshness(r.key,r.content);return i===$.unread?n(o.toolFailed,`${e} 已存在。覆盖前请先用 roll__read_file 读取并确认现有内容;若只改部分内容,优先用 roll__edit_file。`):i===$.stale?n(o.toolFailed,`${e} 在你上次读取后已被修改(可能是用户或其他程序改动)。请重新 roll__read_file 确认最新内容后再决定是否覆盖。`):void 0}export function executeWriteFile(t,e,r){const i=y("content",r.content);if(void 0!==i)return i;const a=g(t.workdir,r.file_path),s=E(a);if("directory"===s)return n(o.invalidInput,`${a} 是目录,不能作为文件写入`);let c;if(!0===s){const r=_(a,{maxFileBytes:t.maxFileBytes});if(!r.ok)return n(o.invalidInput,r.message);const i=T(e,a,r);if(void 0!==i)return i;c=r.content}const{content:d,hadBom:u}=w(r.content);v(a,d,u),e.recordKnownContent(f(a),d);const p=d.split("\n"),m=l(p.slice(0,b),1),h=[`已写入 ${a}(${String(p.length)} 行,${String(Buffer.byteLength(r.content,"utf8"))} 字节):`,m];p.length>b&&h.push(`(预览前 ${String(b)} 行)`);const k=F({workdir:t.workdir,inputPath:r.file_path,change:void 0===c?"create":"modify",before:c??"",after:d});return j(h.join("\n"),k)}export function buildWriteFileTool(t,r,l,f){const v=l.register(x,"write_file",{annotations:I}),$={prepare:async e=>{const a=B.safeParse(e);if(!a.success)return n(o.invalidInput,"参数校验失败: file_path 必须为非空字符串,content 必须为字符串");const s=y("content",a.data.content);if(void 0!==s)return s;const c=a.data.content.split("\n").length,d=g(t.workdir,a.data.file_path),l=m(t.workdir,a.data.file_path),u=l?void 0:_(d,{maxFileBytes:t.maxFileBytes});if(!0===u?.ok){const t=T(r,d,u);if(void 0!==t)return t}const{shrinking:p,warning:k}=void 0!==u?L(c,u):{shrinking:!1,warning:void 0},{content:v}=w(a.data.content),$=void 0===u?void 0:u.ok?"modify":"not-found"===u.code?"create":void 0,j=void 0===$?void 0:F({workdir:t.workdir,inputPath:a.data.file_path,change:$,before:!0===u?.ok?u.content:"",after:v}),b=`写入 ${h(t.workdir,a.data.file_path)}(${String(c)} 行)${void 0!==k?`\n${k}`:""}`,S=p||l?void 0:"write_file:workdir";return i(f,x,"write_file",a.data,I,{explanation:b,...void 0!==j?{diff:j}:{},...void 0!==S?{memoryKey:S,sessionGrantLabel:"本会话内不再询问:写入工作目录内的文件"}:{}})},resources:e=>{const r=B.safeParse(e),i=r.success?`file:${u(g(t.workdir,r.data.file_path))}`:`file-tools:${t.workdir}`;return[{key:s,mode:c.read},{key:i,mode:c.write}]},captureExecutionState:e=>{const r=B.safeParse(e);return r.success?p(t.workdir,r.data.file_path):void 0},revalidateExecution:(e,r)=>{const i=B.safeParse(e);if(i.success)return k(t.workdir,i.data.file_path,r)}};return f.coordinator?.register(v,$),{[v]:e({description:"新建文件或整文件重写。已存在的文件必须先 roll__read_file 读取确认后才能覆盖;只改部分内容时优先用 roll__edit_file。仅支持文本内容,content 含原始 NUL 字符(U+0000)或不成对的 UTF-16 代理项会被拒绝;需要写入转义序列文本时用双反斜杠,需要原始字节时改用 shell。",inputSchema:B,toModelOutput:({output:t})=>a(t),execute:(e,i)=>d(f.coordinator,$,v,i.toolCallId,e,i.abortSignal,()=>Promise.resolve(executeWriteFile(t,r,e)))})}}
@@ -1,6 +1,6 @@
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";
3
+ import type { FileChangeDiff, UserInputForm } from "@roll-agent/protocol";
4
4
  import type { SessionUserInputRequestId } from "../interaction/user-input-interaction-manager.ts";
5
5
  export interface SessionTokenUsage {
6
6
  readonly inputTokens?: number;
@@ -73,6 +73,7 @@ export type SessionEvent = {
73
73
  /** Model-authored display aid. It never changes approval policy or command execution. */
74
74
  readonly explanation?: string;
75
75
  readonly sessionGrantLabel?: string;
76
+ readonly diff?: FileChangeDiff;
76
77
  } | {
77
78
  /** Engine-internal request; Runtime Protocol assigns a separate InteractionId. */
78
79
  readonly type: "user-input-required";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roll-agent/runtime",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,11 +32,11 @@
32
32
  "ai": "^7.0.9",
33
33
  "yaml": "2.8.3",
34
34
  "zod": "^3.25.76",
35
- "@roll-agent/protocol": "0.5.0"
35
+ "@roll-agent/protocol": "0.6.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^22.0.0",
39
- "@roll-agent/core": "0.31.0"
39
+ "@roll-agent/core": "0.32.0"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "rm -rf dist && tsc -p tsconfig.build.json && node ../../scripts/obfuscate.mjs",