@roll-agent/runtime 0.20.0 → 0.20.1

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.
@@ -1565,13 +1565,13 @@ export declare const compactionSemanticReminderProjectionSchema: z.ZodReadonly<z
1565
1565
  uncertainty: number;
1566
1566
  }>>;
1567
1567
  }, "strict", z.ZodTypeAny, {
1568
+ version: 1;
1568
1569
  items: Readonly<{
1569
1570
  text: string;
1570
1571
  sourceQuotes: string[];
1571
1572
  itemId: string & z.BRAND<"CompactionSemanticItemId">;
1572
1573
  category: "resource" | "decision" | "running_session" | "goal" | "constraint" | "completed_work" | "pending_work" | "uncertainty";
1573
1574
  }>[];
1574
- version: 1;
1575
1575
  omittedCounts: Readonly<{
1576
1576
  resource: number;
1577
1577
  decision: number;
@@ -1583,13 +1583,13 @@ export declare const compactionSemanticReminderProjectionSchema: z.ZodReadonly<z
1583
1583
  uncertainty: number;
1584
1584
  }>;
1585
1585
  }, {
1586
+ version: 1;
1586
1587
  items: Readonly<{
1587
1588
  text: string;
1588
1589
  sourceQuotes: string[];
1589
1590
  itemId: string;
1590
1591
  category: "resource" | "decision" | "running_session" | "goal" | "constraint" | "completed_work" | "pending_work" | "uncertainty";
1591
1592
  }>[];
1592
- version: 1;
1593
1593
  omittedCounts: Readonly<{
1594
1594
  resource: number;
1595
1595
  decision: number;
@@ -1 +1 @@
1
- import{randomUUID as e}from"node:crypto";import{McpClientManager as t}from"@roll-agent/core/mcp/client-manager";import{createProviderModel as s,providerAcceptsToolSchemaIssues as i}from"@roll-agent/core/llm/providers";import{AgentStore as o}from"@roll-agent/core/registry/store";import{resolveTransportWithDevSpawnSpec as r}from"@roll-agent/core/registry/dev-spawn";import{getAgentPid as n,startAgent as a,waitForAgentReady as l}from"@roll-agent/core/registry/process-manager";import{acquireAgentUsageLease as c}from"@roll-agent/core/registry/agent-usage-lease";import{formatToolSchemaIssue as d,normalizeListedTools as h}from"@roll-agent/core/cli/utils/agent-tools";import{getAgentEnv as u}from"@roll-agent/core/config/helpers";import{catalogPackageSpec as p,getAgentCatalog as g}from"@roll-agent/core/registry/catalog";import{resolveAgentCatalog as m}from"@roll-agent/core/registry/catalog-discovery";import{installAgent as v}from"@roll-agent/core/registry/install";import{createSkillLibrary as f}from"@roll-agent/core/skills/library";import{ROLL_RESOURCE_HINTS_META_KEY as w}from"../tool-bridge/build-tools.js";import{TOOL_RESOURCE_ACCESS_MODES as y,TOOL_RESOURCE_HINT_KINDS as S}from"../tool-bridge/tool-execution-coordinator.js";import{AgentSession as C}from"./agent-session.js";import{resolveModelContextWindow as b}from"./context-window.js";import{createDefaultModelCatalog as x}from"./model-catalog-default.js";import{createWorkspaceInstructionsSource as k,parseWorkspaceInstructionsSetting as A}from"./workspace-instructions.js";import{CAPABILITY_HOST_MODES as O,shouldOfferAgentInstall as E,shouldOfferScheduleCreate as P}from"./capability-manifest.js";import{}from"../tool-bridge/bash-tool.js";import{unknownCommandClassifier as M}from"../types/command-classification.js";import{resolveShellProfile as T}from"../bash/profile.js";import{inspectGitVcsContext as N}from"./vcs-context.js";import{AGENT_BOOTSTRAP_MAX_CONCURRENCY as I,mapWithBoundedConcurrency as L}from"./agent-bootstrap.js";import{createToolLedgerGapRecoveryMessage as R}from"./cancelled-turn-recovery.js";const U=80,B="ConversationEngine is closing",j=3e4,$=6e4,F={connected:"connected",failed:"failed"};export function buildSessionExecSettings(e){const t=e.config.runtime.shell;return{workdir:process.cwd(),profile:e.profile,maxSessions:t.session.maxSessions,defaultYieldMs:t.session.defaultYieldMs,maxOutputTokens:t.session.maxOutputTokens,bufferCapacity:t.maxCaptureBytes,env:e.env,...e.onCommandSpawn?{onCommandSpawn:e.onCommandSpawn}:{}}}export function buildSessionBashSettings(e){const t=e.config.runtime.shell;return{workdir:process.cwd(),defaultTimeoutMs:t.defaultTimeoutMs,maxTimeoutMs:t.maxTimeoutMs,turnTimeoutMs:e.config.runtime.turnTimeoutMs,maxCaptureBytes:t.maxCaptureBytes,maxModelOutputChars:t.maxModelOutputChars,profile:e.profile,env:e.env,...e.onCommandSpawn?{onCommandSpawn:e.onCommandSpawn}:{}}}class AgentBootstrapTimeoutError extends Error{constructor(e){super(`Agent bootstrap timed out after ${String(e)}ms`),this.name="AgentBootstrapTimeoutError"}}class AgentBootstrapCleanupError extends AggregateError{cleanupErrors;constructor(e,t,s){const i=s.map(e=>e instanceof Error?e.message:String(e)).join("; ");super([t,...s],`Agent "${e}" bootstrap failed and resource cleanup also failed: ${i}`),this.name="AgentBootstrapCleanupError",this.cleanupErrors=s}}function D(){return new Error(B)}function W(e){return e.reason??new Error("Operation aborted")}function H(e){if(!0===e?.aborted)throw W(e)}function q(e,t){return void 0===t?e:Math.max(1,Math.min(e,t-Date.now()))}function V(e){if(void 0!==e&&Date.now()>=e)throw new Error("Agent bootstrap deadline expired")}function _(e){if("object"!=typeof e||null===e||!("annotations"in e))return;const t=e.annotations;if("object"!=typeof t||null===t)return;const s={};return"readOnlyHint"in t&&"boolean"==typeof t.readOnlyHint&&(s.readOnlyHint=t.readOnlyHint),"destructiveHint"in t&&"boolean"==typeof t.destructiveHint&&(s.destructiveHint=t.destructiveHint),void 0===s.readOnlyHint&&void 0===s.destructiveHint?void 0:s}function K(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}function Y(e){return Object.values(S).some(t=>t===e)}function z(e){return Object.values(y).some(t=>t===e)}function G(e){return{hints:void 0,issue:e}}function J(e){if(!K(e)||!K(e._meta))return{hints:void 0,issue:void 0};if(!Object.hasOwn(e._meta,w))return{hints:void 0,issue:void 0};const t=e._meta[w];if(!Array.isArray(t))return G("必须是数组");const s=[];for(const[e,i]of t.entries()){if(!K(i)||"string"!=typeof i.field||0===i.field.trim().length||"string"!=typeof i.kind||!Y(i.kind)||void 0!==i.mode&&("string"!=typeof i.mode||!z(i.mode))||void 0!==i.namespace&&"string"!=typeof i.namespace||i.kind===S.custom&&("string"!=typeof i.namespace||0===i.namespace.trim().length))return G(`第 ${String(e+1)} 项字段无效`);const t=i.kind===S.custom&&"string"==typeof i.namespace?i.namespace.trim():void 0;s.push({field:i.field.trim(),kind:i.kind,...void 0!==i.mode?{mode:i.mode}:{},...void 0!==t?{namespace:t}:{}})}return{hints:s.length>0?s:void 0,issue:void 0}}function Q(e){return"retry"===e.type?`安装遇到网络问题,${Math.round(e.delayMs/1e3)}s 后重试(第 ${e.attempt+1} 次)...`:"warn"===e.type?`警告:${e.message}`:e.message}async function X(e,t,s,i){H(i),"core-managed"===e.runtime.ownership&&(void 0===n(t,e.skill.name)&&a(e,t,s),await l(e,{...i?{signal:i}:{}}))}function Z(e){if(null===e.workspaceInstructions)return;if(void 0!==e.workspaceInstructions)return e.workspaceInstructions;const t=process.cwd();return k({cwd:t,setting:A(e.config.chat.instructions,t),...e.onWorkspaceInstructionsIssue?{onIssue:e.onWorkspaceInstructionsIssue}:{}})}export class ConversationEngine{config;clientManager;store;policy;fileToolsEnabled;maxSteps;providerOptions;modelOverride;modelNameOverride;providerNameOverride;switchingProviderName;modelCatalog;structuredOutputProviderOptions;structuredOutputReasoning;acquireAgentUsage;debugEvents;explicitAgents;explicitModel;explicitSources;explicitSkillLibrary;onSkillLibraryIssue;workspaceInstructions;onAgentBootstrapIssue;hostMode;scheduleTools;resolveDynamicCapabilityContext;sessionExecEnabled;explicitShellProfile;resolveShellProfileFn;installAgentFn;resolveCatalogFn;inspectVcsContext;shellEnv;onShellCommandSpawn;ready;refreshChain=Promise.resolve();resolvedCatalog;shellProfileResolution;shellUnsupportedWarned=!1;liveSessions=new Map;agentUsageLeases=new Map;shutdownController=new AbortController;disposePromise;closing=!1;constructor(e){this.config=e.config,this.clientManager=e.clientManager??new t,this.store=e.store,this.modelCatalog=e.modelCatalog??x(),this.policy=e.policy,this.fileToolsEnabled=e.fileToolsEnabled??!0,this.maxSteps=e.maxSteps??U,this.providerOptions=e.providerOptions,this.structuredOutputProviderOptions=e.structuredOutputProviderOptions,this.structuredOutputReasoning=e.structuredOutputReasoning,this.acquireAgentUsage=e.acquireAgentUsage??(e.ensureAgentReady?async(t,s,i)=>{await(e.ensureAgentReady?.(t,s,i))}:(e,t,s)=>this.acquireDefaultAgentUsage(e,t,s)),this.debugEvents=e.debugEvents??!1,this.hostMode=e.hostMode??O.embedded,this.scheduleTools=e.scheduleTools??void 0,this.resolveDynamicCapabilityContext=e.resolveDynamicCapabilityContext,this.sessionExecEnabled=e.sessionExecEnabled??!0,this.explicitShellProfile=e.shellProfile,this.resolveShellProfileFn=e.resolveShellProfileFn??T,this.explicitAgents=e.agents,this.explicitModel=e.model,this.explicitSources=e.sources,this.explicitSkillLibrary=e.skillLibrary,this.onSkillLibraryIssue=e.onSkillLibraryIssue,this.workspaceInstructions=Z(e),this.onAgentBootstrapIssue=e.onAgentBootstrapIssue,this.installAgentFn=e.installAgentFn??v,this.resolveCatalogFn=e.resolveCatalogFn??m,this.inspectVcsContext=e.inspectVcsContext??N,this.shellEnv={...e.shellEnv??process.env},this.onShellCommandSpawn=e.onShellCommandSpawn}async acquireDefaultAgentUsage(e,t,s){const i=await c(e,this.config.agents.dataDir,t,{holderKind:"chat",startIfStopped:!0,...s?{signal:s}:{}});if(void 0!==i)return i;await X(e,this.config.agents.dataDir,t,s)}async createSession(t={}){this.assertAcceptingSessions();const s=await this.ensureReady();this.assertAcceptingSessions();const i=this.store?this.store.createThread({...void 0===t.id?{}:{id:t.id},...void 0===t.origin?{}:{origin:t.origin},...t.title?{title:t.title}:{},model:this.resolveModelName()}):t.id??e();return this.buildSession(s,i,[])}async resumeSession(e){if(this.assertAcceptingSessions(),!this.store)throw new Error("resumeSession requires a ThreadStore");if("scheduled"===this.store.getThread(e)?.origin.kind)throw new Error("定时执行会话是只读记录;请基于该次运行创建新对话");const t=this.liveSessions.get(e);if(void 0!==t)return t;if(!this.store.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const s=await this.ensureReady();this.assertAcceptingSessions();const i=this.liveSessions.get(e);if(void 0!==i)return i;this.recoverUncoveredToolExecutionContext(e);const o=this.store.loadSessionState(e);return this.buildSession(s,e,o.messages,o.checkpoint)}async forkSession(e,t={}){if(this.assertAcceptingSessions(),!this.store)throw new Error("forkSession requires a ThreadStore");const s=await this.ensureReady();this.assertAcceptingSessions();const i=this.store.forkSnapshot(e,{...t,model:this.resolveModelName()});try{this.recoverUncoveredToolExecutionContext(i);const e=this.store.loadSessionState(i);return this.buildSession(s,i,e.messages,e.checkpoint)}catch(e){throw this.store.deleteThread(i),e}}assertAcceptingSessions(){if(this.closing)throw D()}resolveRuntimeShellProfile(){if(!this.config.runtime.shell.enabled)return;if(void 0!==this.explicitShellProfile)return this.explicitShellProfile??void 0;void 0===this.shellProfileResolution&&(this.shellProfileResolution=this.resolveShellProfileFn({platform:process.platform,env:this.shellEnv}));const e=this.shellProfileResolution;if(e.supported)return e.profile;if(!this.shellUnsupportedWarned){this.shellUnsupportedWarned=!0;const t="pwsh-version-unsupported"===e.reason?"检测到的 pwsh 版本低于 7":"未检测到 PowerShell 7 (pwsh)";process.stderr.write(`roll chat: ${t},Windows 原生 shell 工具已跳过注册;可运行 winget install Microsoft.PowerShell\n`)}}resolveShellSettings(e){return buildSessionBashSettings({config:this.config,profile:e,env:this.shellEnv,...this.onShellCommandSpawn?{onCommandSpawn:this.onShellCommandSpawn}:{}})}resolveSessionExecSettings(e){if(!this.sessionExecEnabled)return;const t=this.config.runtime.shell;return t.enabled&&t.session.enabled&&e.supportsSessionExec?buildSessionExecSettings({config:this.config,profile:e,env:this.shellEnv,...this.onShellCommandSpawn?{onCommandSpawn:this.onShellCommandSpawn}:{}}):void 0}buildSession(e,t,s,i){const o=this.store,r=o?.getThread(t)?.derivedFrom,n=this.resolveContextWindowFor(this.resolveProviderName(),this.resolveModelName())?.window,a=(e.skillLibrary?.list()??[]).length>0?e.skillLibrary:void 0,l=this.resolveRuntimeShellProfile(),c=l?this.resolveShellSettings(l):void 0,d=this.fileToolsEnabled?{workdir:c?.workdir??process.cwd()}:void 0,h=l?this.resolveSessionExecSettings(l):void 0,u=l?l.supportsSafeCommandClassification&&this.config.runtime.shell.autoApproveSafe?l:M:void 0,p=E(this.hostMode)?this.resolveAgentInstallBinding():void 0,g=this.scheduleTools,m=this.composeCapabilityContext(e.sources.length,l),v=new C({id:t,model:this.activeModel(e),sources:e.sources,toolSchemaPolicy:this.toolSchemaPolicyFor(this.resolveProviderName()),onToolExcluded:e=>this.onAgentBootstrapIssue?.({agentName:e.agentName,message:`工具 "${e.toolName}" 的参数 schema 含当前 provider "${this.resolveToolSchemaProviderName()}" 无法接受的引用(${e.issues.map(e=>e.ref).join("、")}),已从本会话工具集移除`}),capabilityContext:m,resolveDynamicCapabilityContext:async e=>{const[t,s]=await Promise.all([this.inspectVcsContext(m.cwd),this.resolveDynamicCapabilityContext?.(e)??{}]),i=s.vcs??t;return{...s.ruleIds?{ruleIds:s.ruleIds}:{},...i?{vcs:i}:{},...s.origin?{origin:s.origin}:{}}},...a?{skillLibrary:a}:{},...this.workspaceInstructions?{workspaceInstructions:this.workspaceInstructions}:{},...d?{fileTools:d}:{},...c?{bash:c}:{},...u?{bashClassifier:u}:{},...h?{bashSession:h}:{},...p?{agentInstall:p}:{},...g?{schedules:{port:g,sessionCwd:m.cwd,includeCreate:P(this.hostMode)}}:{},maxSteps:this.maxSteps,compaction:this.config.runtime.compaction,turnTimeoutMs:this.config.runtime.turnTimeoutMs,debugEvents:this.debugEvents,...this.providerOptions?{providerOptions:this.providerOptions}:{},...this.structuredOutputProviderOptions?{structuredOutputProviderOptions:this.structuredOutputProviderOptions}:{},...this.structuredOutputReasoning?{structuredOutputReasoning:this.structuredOutputReasoning}:{},onProviderOptionsChange:e=>this.syncProviderOptions(e),...void 0!==n?{contextWindow:n}:{},...this.policy?{policy:this.policy}:{},initialMessages:s,...void 0!==r?{derivedFrom:r}:{},...i?{initialCheckpoint:i}:{},...o?{onPersist:(e,s)=>o.appendMessages(t,e,s),onReplace:e=>o.replaceMessages(t,e),onToolExecution:e=>o.appendToolExecution(t,e),listToolExecutions:e=>o.listToolExecutions(t,e),getToolExecution:e=>o.getToolExecution(t,e),recoverUncoveredToolExecutions:()=>this.recoverUncoveredToolExecutionContext(t),listTranscriptMessages:e=>o.listTranscriptMessages(t,e),commitCompaction:e=>o.commitCompaction(t,e),readCheckpointTranscript:e=>o.readCheckpointTranscript(t,e)}:{},onClose:()=>{this.liveSessions.get(t)===v&&this.liveSessions.delete(t)}});return this.liveSessions.set(t,v),v}recoverUncoveredToolExecutionContext(e){const t=this.store?.recoverUncoveredToolExecutions(e,R);return void 0===t?[]:[t]}syncProviderOptions(e){this.providerOptions=e,this.clientManager.setSamplingProviderOptions(e)}switchModel(e){this.assertAcceptingSessions();const t=this.resolveContextWindowFor(e.provider,e.modelName),s=t?.window,i={model:e.model,...void 0!==s?{contextWindow:s}:{},...e.providerOptions?{providerOptions:e.providerOptions}:{},...e.structuredOutputProviderOptions?{structuredOutputProviderOptions:e.structuredOutputProviderOptions}:{},...e.structuredOutputReasoning?{structuredOutputReasoning:e.structuredOutputReasoning}:{},toolSchemaPolicy:this.toolSchemaPolicyFor(e.provider)};for(const e of this.liveSessions.values())if(!e.canSwitchModel())throw new Error("存在正在生成回复的会话,稍后再切换模型");this.switchingProviderName=e.provider;try{for(const e of this.liveSessions.values())e.switchModel(i)}finally{this.switchingProviderName=void 0}this.modelOverride=e.model,this.modelNameOverride=e.modelName,this.providerNameOverride=e.provider,this.providerOptions=e.providerOptions,this.structuredOutputProviderOptions=e.structuredOutputProviderOptions,this.structuredOutputReasoning=e.structuredOutputReasoning,this.clientManager.setSamplingModel(e.model),this.clientManager.setSamplingProviderOptions(e.providerOptions);for(const t of this.liveSessions.keys())this.store?.updateModel(t,e.modelName);return t}ensureReady(){return this.assertAcceptingSessions(),this.ready||(this.ready=this.bootstrap()),this.ready}async bootstrap(){const e=Date.now(),t=this.explicitModel??this.resolveModel();if(this.explicitSources)return{model:t,sources:this.explicitSources,...this.explicitSkillLibrary?{skillLibrary:this.explicitSkillLibrary}:{}};const s=this.explicitAgents??new o(this.config.agents.dataDir).list(),i=this.config.runtime.agentBootstrap.timeoutMs,r=e+i,n=new AbortController,a=new AgentBootstrapTimeoutError(i);let l=!1;const c=()=>{n.signal.aborted||n.abort(W(this.shutdownController.signal))},d=()=>{!this.closing&&!n.signal.aborted&&Date.now()>=r&&(l=!0,n.abort(a))};this.shutdownController.signal.aborted?c():this.shutdownController.signal.addEventListener("abort",c,{once:!0});const h=setTimeout(()=>{this.closing||n.signal.aborted||(l=!0,n.abort(a))},Math.max(0,r-Date.now())),u=(e,t)=>({kind:F.failed,issues:[{agentName:e.skill.name,message:a.message},...void 0===t?[]:[{agentName:e.skill.name,message:t.message}]]});try{if(this.agentInstallEnabled())try{this.resolvedCatalog=await this.resolveCatalogFn(this.config,{allowNetwork:!1,signal:n.signal,...this.config.install.registry?{registry:this.config.install.registry}:{}})}catch(e){if(this.closing||this.shutdownController.signal.aborted)throw D();if(d(),!l)throw e}if(this.closing||this.shutdownController.signal.aborted)throw D();if(d(),0===s.length){const e=this.resolveSkillLibrary(s);return{model:t,sources:[],...e?{skillLibrary:e}:{}}}const e=await L(s,I,async e=>{const s=[],i=e=>{s.push(e)};try{d(),H(n.signal);const o=await this.connectAgentSource(e,t,i,{signal:n.signal,deadlineAt:r});return{kind:F.connected,source:o,issues:s}}catch(t){if(this.closing||this.shutdownController.signal.aborted)throw D();return d(),l?u(e,t instanceof AgentBootstrapCleanupError?t:void 0):(s.push({agentName:e.skill.name,message:t instanceof Error?t.message:String(t)}),{kind:F.failed,issues:s})}},{signal:n.signal,onSkipped:e=>u(e)});if(this.closing||this.shutdownController.signal.aborted)throw D();const i=[];for(const t of e){for(const e of t.issues)this.onAgentBootstrapIssue?.(e);t.kind===F.connected&&i.push(t.source)}const o=this.resolveSkillLibrary(s);return{model:t,sources:i,...o?{skillLibrary:o}:{}}}finally{clearTimeout(h),this.shutdownController.signal.removeEventListener("abort",c)}}async connectAgentSource(e,t,s=e=>this.onAgentBootstrapIssue?.(e),i={}){this.assertAcceptingSessions(),H(i.signal),V(i.deadlineAt);const o=r(e),n=u(this.config,e.skill.name),a=this.agentUsageLeases.get(e.skill.name);let l,c;try{l=void 0===a?await this.acquireAgentUsage(e,n,i.signal):void 0,this.assertAcceptingSessions(),H(i.signal),V(i.deadlineAt);const r={timeoutMs:q(j,i.deadlineAt),samplingModel:t,...this.providerOptions?{samplingProviderOptions:this.providerOptions}:{},...n?{env:n}:{},...i.signal?{signal:i.signal}:{}},u=this.clientManager.connectWithOwnership;c="function"==typeof u?await u.call(this.clientManager,e.skill.name,o,e.installPath,r):{client:await this.clientManager.connect(e.skill.name,o,e.installPath,r),commit:()=>{},rollback:async()=>{}},this.assertAcceptingSessions(),H(i.signal),V(i.deadlineAt);const p=(await c.client.listTools(void 0,{timeout:q($,i.deadlineAt),...i.signal?{signal:i.signal}:{}})).tools;this.assertAcceptingSessions(),H(i.signal),V(i.deadlineAt);const g=h(p,{onSchemaIssue:t=>s({agentName:e.skill.name,message:d(e.skill.name,t)})}).map((t,i)=>{const o=J(p[i]);return void 0!==o.issue&&s({agentName:e.skill.name,message:`Tool "${t.name}" 的 ${w} 无效(${o.issue}),已回退 Agent 级资源锁`}),{tool:t,annotations:_(p[i]),...o.hints?{resourceHints:o.hints}:{}}});return c.commit(),void 0!==l&&this.agentUsageLeases.set(e.skill.name,l),{agentName:e.skill.name,client:c.client,tools:g,...e.source?{agentSource:e.source.type}:{},transport:o.type,runtimeOwnership:e.runtime.ownership,..."stdio"===o.type?{resourceBaseDir:e.installPath}:{}}}catch(t){const s=[];if(void 0!==c&&await c.rollback().catch(e=>{s.push(e)}),void 0!==l){const t=l;await t.release().catch(i=>{this.agentUsageLeases.set(e.skill.name,t),s.push(i)})}if(s.length>0)throw new AgentBootstrapCleanupError(e.skill.name,t,s);throw t}}async prepareAgentRefresh(e){this.assertAcceptingSessions();const t=this.refreshChain.then(()=>this.runAgentRefresh(e));return this.refreshChain=t.then(()=>{},()=>{}),t}async runAgentRefresh(e){const t=await this.ensureReady(),s=await this.connectAgentSource(e,this.activeModel(t),void 0,{signal:this.shutdownController.signal}),i=[...t.sources.filter(e=>e.agentName!==s.agentName),s],r=this.explicitAgents??new o(this.config.agents.dataDir).list(),n=this.resolveSkillLibrary(r);this.ready=Promise.resolve({model:this.activeModel(t),sources:i,...n?{skillLibrary:n}:{}});const a=n?.list()??[],l=n&&a.length>0?n:void 0,c=this.resolveRuntimeShellProfile();return{source:s,...l?{skillLibrary:l}:{},capabilityContext:this.composeCapabilityContext(i.length,c)}}composeCapabilityContext(e,t){const s=this.resolveAgentOnboardingInfo();return{profile:t?.toolName??"no-shell",hostMode:this.hostMode,cwd:process.cwd(),platform:process.platform,...t?{shellHints:t.systemPromptHints()}:{},agentCount:e,...s?{agentOnboardingCatalog:s}:{}}}agentInstallEnabled(){return void 0===this.explicitSources&&void 0===this.explicitAgents}currentCatalog(){return this.resolvedCatalog??g(this.config)}resolveAgentOnboardingInfo(){if(!this.agentInstallEnabled())return;const e=this.currentCatalog();return 0!==e.length?e.map(e=>({shortName:e.shortName,description:e.description})):void 0}resolveAgentInstallBinding(){if(!this.agentInstallEnabled())return;const e=this.currentCatalog();return 0!==e.length?{catalog:e.map(e=>({shortName:e.shortName,description:e.description})),install:(e,t)=>this.installCatalogAgent(e,t)}:void 0}async installCatalogAgent(e,t){const s=this.currentCatalog().find(t=>t.shortName===e);if(!s)return{outcome:{ok:!1,message:`未知的官方 Agent 短名: ${e}`}};const i=await this.installAgentFn({packageSpec:p(s),skipBrowserSetup:!0,autoStart:!1,expectedSkillName:s.skillName},{agentsConfig:this.config.agents,installConfig:this.config.install,getStartEnv:e=>u(this.config,e),report:e=>t(Q(e))});if(!i.ok)return{outcome:{ok:!1,message:i.message,...i.retryCommand?{retryCommand:i.retryCommand}:{}}};const o=i.agent,r="installed-package"===o.source?.type?o.source.installedVersion:void 0,n="core-managed"===o.runtime.ownership&&void 0!==o.runtime.setup?.playwright,a={ok:!0,agentName:o.skill.name,...r?{version:r}:{},missingEnv:(i.envReport?.missingRequired??[]).map(e=>e.name),...n?{retryCommand:`roll agent install ${s.shortName}`}:{},refreshApplied:!1};if((await this.ensureReady()).sources.some(e=>e.agentName===o.skill.name))return t(`Agent "${o.skill.name}" 本会话已接入旧版本连接,更新需重启 roll chat 生效。`),{outcome:a};try{return{outcome:a,refresh:await this.prepareAgentRefresh(o)}}catch(e){return t(`接入新 Agent 失败:${e instanceof Error?e.message:String(e)}`),{outcome:a}}}resolveSkillLibrary(e){if(null!==this.explicitSkillLibrary)return void 0!==this.explicitSkillLibrary?this.explicitSkillLibrary:f({agents:e,extraDirs:this.config.skills.dirs,...this.onSkillLibraryIssue?{onIssue:this.onSkillLibraryIssue}:{}})}resolveModel(){const e=this.resolveProviderName(),t=this.resolveModelName(),i=this.config.llm.providers[e];if(!i)throw new Error(`LLM provider "${e}" 未配置`);return s(e,t,i.apiKey,i.baseUrl)}resolveProviderName(){return this.providerNameOverride??this.config.runtime.provider??this.config.llm.defaultProvider}resolveContextWindowFor(e,t){return b({provider:e,model:t,...void 0!==this.config.runtime.contextWindow?{override:this.config.runtime.contextWindow}:{},catalog:this.modelCatalog})}resolveModelName(){return this.modelNameOverride??this.config.runtime.model??this.config.llm.defaultModel}toolSchemaPolicyFor(e){return t=>i(e,t)}resolveToolSchemaProviderName(){return this.switchingProviderName??this.resolveProviderName()}activeModel(e){return this.modelOverride??e.model}async getContextSummary(){const e=await this.ensureReady();this.assertAcceptingSessions();const t=this.workspaceInstructions?.current()?.path;return{agentCount:e.sources.length,toolCount:e.sources.reduce((e,t)=>e+t.tools.length,0),skillCount:e.skillLibrary?.list().length??0,...void 0!==t?{instructionsPath:t}:{}}}async releaseAgentUsageLeases(){const e=[...this.agentUsageLeases.entries()],t=await Promise.allSettled(e.map(async([e,t])=>{try{await t.release()}finally{this.agentUsageLeases.get(e)===t&&this.agentUsageLeases.delete(e)}}));for(const e of t)"rejected"===e.status&&process.stderr.write(`roll chat: Agent 使用租约释放失败: ${e.reason instanceof Error?e.reason.message:String(e.reason)}\n`)}async dispose(){if(void 0!==this.disposePromise)return this.disposePromise;this.closing=!0,this.shutdownController.abort(D());const e=Promise.allSettled([...this.ready?[this.ready]:[],this.refreshChain]).then(()=>{});return this.disposePromise=(async()=>{let t={failed:!1};try{await Promise.all([...this.liveSessions.values()].map(e=>e.close()))}catch(e){t={failed:!0,error:e}}this.liveSessions.clear(),await e;let s={failed:!1};try{await this.clientManager.disconnectAll()}catch(e){s={failed:!0,error:e}}if(await this.releaseAgentUsageLeases(),t.failed&&s.failed)throw new AggregateError([t.error,s.error],"ConversationEngine 关闭 session 与 MCP 连接均失败。");if(t.failed)throw t.error;if(s.failed)throw s.error})(),this.disposePromise}}
1
+ import{randomUUID as e}from"node:crypto";import{McpClientManager as t}from"@roll-agent/core/mcp/client-manager";import{createProviderModel as s,providerAcceptsToolSchemaIssues as i}from"@roll-agent/core/llm/providers";import{AgentStore as o}from"@roll-agent/core/registry/store";import{resolveTransportWithDevSpawnSpec as r}from"@roll-agent/core/registry/dev-spawn";import{getAgentPid as n,startAgent as a,waitForAgentReady as l}from"@roll-agent/core/registry/process-manager";import{acquireAgentUsageLease as c,getExitedAgentUsageNames as d}from"@roll-agent/core/registry/agent-usage-lease";import{formatToolSchemaIssue as h,normalizeListedTools as u}from"@roll-agent/core/cli/utils/agent-tools";import{getAgentEnv as p}from"@roll-agent/core/config/helpers";import{catalogPackageSpec as g,getAgentCatalog as m}from"@roll-agent/core/registry/catalog";import{resolveAgentCatalog as v}from"@roll-agent/core/registry/catalog-discovery";import{installAgent as f}from"@roll-agent/core/registry/install";import{createSkillLibrary as w}from"@roll-agent/core/skills/library";import{ROLL_RESOURCE_HINTS_META_KEY as y}from"../tool-bridge/build-tools.js";import{TOOL_RESOURCE_ACCESS_MODES as S,TOOL_RESOURCE_HINT_KINDS as C}from"../tool-bridge/tool-execution-coordinator.js";import{AgentSession as b}from"./agent-session.js";import{resolveModelContextWindow as x}from"./context-window.js";import{createDefaultModelCatalog as k}from"./model-catalog-default.js";import{createWorkspaceInstructionsSource as A,parseWorkspaceInstructionsSetting as O}from"./workspace-instructions.js";import{CAPABILITY_HOST_MODES as E,shouldOfferAgentInstall as P,shouldOfferScheduleCreate as M}from"./capability-manifest.js";import{}from"../tool-bridge/bash-tool.js";import{unknownCommandClassifier as T}from"../types/command-classification.js";import{resolveShellProfile as N}from"../bash/profile.js";import{inspectGitVcsContext as I}from"./vcs-context.js";import{AGENT_BOOTSTRAP_MAX_CONCURRENCY as L,mapWithBoundedConcurrency as R}from"./agent-bootstrap.js";import{createToolLedgerGapRecoveryMessage as U}from"./cancelled-turn-recovery.js";const B=80,j="ConversationEngine is closing",$=3e4,F=6e4,D={connected:"connected",failed:"failed"};export function buildSessionExecSettings(e){const t=e.config.runtime.shell;return{workdir:process.cwd(),profile:e.profile,maxSessions:t.session.maxSessions,defaultYieldMs:t.session.defaultYieldMs,maxOutputTokens:t.session.maxOutputTokens,bufferCapacity:t.maxCaptureBytes,env:e.env,...e.onCommandSpawn?{onCommandSpawn:e.onCommandSpawn}:{}}}export function buildSessionBashSettings(e){const t=e.config.runtime.shell;return{workdir:process.cwd(),defaultTimeoutMs:t.defaultTimeoutMs,maxTimeoutMs:t.maxTimeoutMs,turnTimeoutMs:e.config.runtime.turnTimeoutMs,maxCaptureBytes:t.maxCaptureBytes,maxModelOutputChars:t.maxModelOutputChars,profile:e.profile,env:e.env,...e.onCommandSpawn?{onCommandSpawn:e.onCommandSpawn}:{}}}class AgentBootstrapTimeoutError extends Error{constructor(e){super(`Agent bootstrap timed out after ${String(e)}ms`),this.name="AgentBootstrapTimeoutError"}}class AgentBootstrapCleanupError extends AggregateError{cleanupErrors;constructor(e,t,s){const i=s.map(e=>e instanceof Error?e.message:String(e)).join("; ");super([t,...s],`Agent "${e}" bootstrap failed and resource cleanup also failed: ${i}`),this.name="AgentBootstrapCleanupError",this.cleanupErrors=s}}function W(){return new Error(j)}function H(e){return e.reason??new Error("Operation aborted")}function q(e){if(!0===e?.aborted)throw H(e)}function V(e,t){return void 0===t?e:Math.max(1,Math.min(e,t-Date.now()))}function _(e){if(void 0!==e&&Date.now()>=e)throw new Error("Agent bootstrap deadline expired")}function K(e){if("object"!=typeof e||null===e||!("annotations"in e))return;const t=e.annotations;if("object"!=typeof t||null===t)return;const s={};return"readOnlyHint"in t&&"boolean"==typeof t.readOnlyHint&&(s.readOnlyHint=t.readOnlyHint),"destructiveHint"in t&&"boolean"==typeof t.destructiveHint&&(s.destructiveHint=t.destructiveHint),void 0===s.readOnlyHint&&void 0===s.destructiveHint?void 0:s}function Y(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}function z(e){return Object.values(C).some(t=>t===e)}function G(e){return Object.values(S).some(t=>t===e)}function J(e){return{hints:void 0,issue:e}}function Q(e){if(!Y(e)||!Y(e._meta))return{hints:void 0,issue:void 0};if(!Object.hasOwn(e._meta,y))return{hints:void 0,issue:void 0};const t=e._meta[y];if(!Array.isArray(t))return J("必须是数组");const s=[];for(const[e,i]of t.entries()){if(!Y(i)||"string"!=typeof i.field||0===i.field.trim().length||"string"!=typeof i.kind||!z(i.kind)||void 0!==i.mode&&("string"!=typeof i.mode||!G(i.mode))||void 0!==i.namespace&&"string"!=typeof i.namespace||i.kind===C.custom&&("string"!=typeof i.namespace||0===i.namespace.trim().length))return J(`第 ${String(e+1)} 项字段无效`);const t=i.kind===C.custom&&"string"==typeof i.namespace?i.namespace.trim():void 0;s.push({field:i.field.trim(),kind:i.kind,...void 0!==i.mode?{mode:i.mode}:{},...void 0!==t?{namespace:t}:{}})}return{hints:s.length>0?s:void 0,issue:void 0}}function X(e){return"retry"===e.type?`安装遇到网络问题,${Math.round(e.delayMs/1e3)}s 后重试(第 ${e.attempt+1} 次)...`:"warn"===e.type?`警告:${e.message}`:e.message}async function Z(e,t,s,i){q(i),"core-managed"===e.runtime.ownership&&(void 0===n(t,e.skill.name)&&a(e,t,s),await l(e,{...i?{signal:i}:{}}))}function ee(e){if(null===e.workspaceInstructions)return;if(void 0!==e.workspaceInstructions)return e.workspaceInstructions;const t=process.cwd();return A({cwd:t,setting:O(e.config.chat.instructions,t),...e.onWorkspaceInstructionsIssue?{onIssue:e.onWorkspaceInstructionsIssue}:{}})}export class ConversationEngine{config;clientManager;store;policy;fileToolsEnabled;maxSteps;providerOptions;modelOverride;modelNameOverride;providerNameOverride;switchingProviderName;modelCatalog;structuredOutputProviderOptions;structuredOutputReasoning;acquireAgentUsage;debugEvents;explicitAgents;explicitModel;explicitSources;explicitSkillLibrary;onSkillLibraryIssue;workspaceInstructions;onAgentBootstrapIssue;hostMode;scheduleTools;resolveDynamicCapabilityContext;sessionExecEnabled;explicitShellProfile;resolveShellProfileFn;installAgentFn;resolveCatalogFn;inspectVcsContext;shellEnv;onShellCommandSpawn;ready;refreshChain=Promise.resolve();resolvedCatalog;shellProfileResolution;shellUnsupportedWarned=!1;liveSessions=new Map;agentUsageLeases=new Map;shutdownController=new AbortController;disposePromise;closing=!1;constructor(e){this.config=e.config,this.clientManager=e.clientManager??new t,this.store=e.store,this.modelCatalog=e.modelCatalog??k(),this.policy=e.policy,this.fileToolsEnabled=e.fileToolsEnabled??!0,this.maxSteps=e.maxSteps??B,this.providerOptions=e.providerOptions,this.structuredOutputProviderOptions=e.structuredOutputProviderOptions,this.structuredOutputReasoning=e.structuredOutputReasoning,this.acquireAgentUsage=e.acquireAgentUsage??(e.ensureAgentReady?async(t,s,i)=>{await(e.ensureAgentReady?.(t,s,i))}:(e,t,s)=>this.acquireDefaultAgentUsage(e,t,s)),this.debugEvents=e.debugEvents??!1,this.hostMode=e.hostMode??E.embedded,this.scheduleTools=e.scheduleTools??void 0,this.resolveDynamicCapabilityContext=e.resolveDynamicCapabilityContext,this.sessionExecEnabled=e.sessionExecEnabled??!0,this.explicitShellProfile=e.shellProfile,this.resolveShellProfileFn=e.resolveShellProfileFn??N,this.explicitAgents=e.agents,this.explicitModel=e.model,this.explicitSources=e.sources,this.explicitSkillLibrary=e.skillLibrary,this.onSkillLibraryIssue=e.onSkillLibraryIssue,this.workspaceInstructions=ee(e),this.onAgentBootstrapIssue=e.onAgentBootstrapIssue,this.installAgentFn=e.installAgentFn??f,this.resolveCatalogFn=e.resolveCatalogFn??v,this.inspectVcsContext=e.inspectVcsContext??I,this.shellEnv={...e.shellEnv??process.env},this.onShellCommandSpawn=e.onShellCommandSpawn}async acquireDefaultAgentUsage(e,t,s){const i=await c(e,this.config.agents.dataDir,t,{holderKind:"chat",startIfStopped:!0,...s?{signal:s}:{}});if(void 0!==i)return i;await Z(e,this.config.agents.dataDir,t,s)}async createSession(t={}){this.assertAcceptingSessions();const s=await this.ensureReady();this.assertAcceptingSessions();const i=this.store?this.store.createThread({...void 0===t.id?{}:{id:t.id},...void 0===t.origin?{}:{origin:t.origin},...t.title?{title:t.title}:{},model:this.resolveModelName()}):t.id??e();return this.buildSession(s,i,[])}async resumeSession(e){if(this.assertAcceptingSessions(),!this.store)throw new Error("resumeSession requires a ThreadStore");if("scheduled"===this.store.getThread(e)?.origin.kind)throw new Error("定时执行会话是只读记录;请基于该次运行创建新对话");const t=this.liveSessions.get(e);if(void 0!==t)return t;if(!this.store.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const s=await this.ensureReady();this.assertAcceptingSessions();const i=this.liveSessions.get(e);if(void 0!==i)return i;this.recoverUncoveredToolExecutionContext(e);const o=this.store.loadSessionState(e);return this.buildSession(s,e,o.messages,o.checkpoint)}async forkSession(e,t={}){if(this.assertAcceptingSessions(),!this.store)throw new Error("forkSession requires a ThreadStore");const s=await this.ensureReady();this.assertAcceptingSessions();const i=this.store.forkSnapshot(e,{...t,model:this.resolveModelName()});try{this.recoverUncoveredToolExecutionContext(i);const e=this.store.loadSessionState(i);return this.buildSession(s,i,e.messages,e.checkpoint)}catch(e){throw this.store.deleteThread(i),e}}assertAcceptingSessions(){if(this.closing)throw W()}resolveRuntimeShellProfile(){if(!this.config.runtime.shell.enabled)return;if(void 0!==this.explicitShellProfile)return this.explicitShellProfile??void 0;void 0===this.shellProfileResolution&&(this.shellProfileResolution=this.resolveShellProfileFn({platform:process.platform,env:this.shellEnv}));const e=this.shellProfileResolution;if(e.supported)return e.profile;if(!this.shellUnsupportedWarned){this.shellUnsupportedWarned=!0;const t="pwsh-version-unsupported"===e.reason?"检测到的 pwsh 版本低于 7":"未检测到 PowerShell 7 (pwsh)";process.stderr.write(`roll chat: ${t},Windows 原生 shell 工具已跳过注册;可运行 winget install Microsoft.PowerShell\n`)}}resolveShellSettings(e){return buildSessionBashSettings({config:this.config,profile:e,env:this.shellEnv,...this.onShellCommandSpawn?{onCommandSpawn:this.onShellCommandSpawn}:{}})}resolveSessionExecSettings(e){if(!this.sessionExecEnabled)return;const t=this.config.runtime.shell;return t.enabled&&t.session.enabled&&e.supportsSessionExec?buildSessionExecSettings({config:this.config,profile:e,env:this.shellEnv,...this.onShellCommandSpawn?{onCommandSpawn:this.onShellCommandSpawn}:{}}):void 0}buildSession(e,t,s,i){const o=this.store,r=o?.getThread(t)?.derivedFrom,n=this.resolveContextWindowFor(this.resolveProviderName(),this.resolveModelName())?.window,a=(e.skillLibrary?.list()??[]).length>0?e.skillLibrary:void 0,l=this.resolveRuntimeShellProfile(),c=l?this.resolveShellSettings(l):void 0,d=this.fileToolsEnabled?{workdir:c?.workdir??process.cwd()}:void 0,h=l?this.resolveSessionExecSettings(l):void 0,u=l?l.supportsSafeCommandClassification&&this.config.runtime.shell.autoApproveSafe?l:T:void 0,p=P(this.hostMode)?this.resolveAgentInstallBinding():void 0,g=this.scheduleTools,m=this.composeCapabilityContext(e.sources.length,l),v=new b({id:t,model:this.activeModel(e),sources:e.sources,toolSchemaPolicy:this.toolSchemaPolicyFor(this.resolveProviderName()),onToolExcluded:e=>this.onAgentBootstrapIssue?.({agentName:e.agentName,message:`工具 "${e.toolName}" 的参数 schema 含当前 provider "${this.resolveToolSchemaProviderName()}" 无法接受的引用(${e.issues.map(e=>e.ref).join("、")}),已从本会话工具集移除`}),capabilityContext:m,resolveDynamicCapabilityContext:async e=>{const[t,s]=await Promise.all([this.inspectVcsContext(m.cwd),this.resolveDynamicCapabilityContext?.(e)??{}]),i=s.vcs??t;return{...s.ruleIds?{ruleIds:s.ruleIds}:{},...i?{vcs:i}:{},...s.origin?{origin:s.origin}:{}}},...a?{skillLibrary:a}:{},...this.workspaceInstructions?{workspaceInstructions:this.workspaceInstructions}:{},...d?{fileTools:d}:{},...c?{bash:c}:{},...u?{bashClassifier:u}:{},...h?{bashSession:h}:{},...p?{agentInstall:p}:{},...g?{schedules:{port:g,sessionCwd:m.cwd,includeCreate:M(this.hostMode)}}:{},maxSteps:this.maxSteps,compaction:this.config.runtime.compaction,turnTimeoutMs:this.config.runtime.turnTimeoutMs,debugEvents:this.debugEvents,...this.providerOptions?{providerOptions:this.providerOptions}:{},...this.structuredOutputProviderOptions?{structuredOutputProviderOptions:this.structuredOutputProviderOptions}:{},...this.structuredOutputReasoning?{structuredOutputReasoning:this.structuredOutputReasoning}:{},onProviderOptionsChange:e=>this.syncProviderOptions(e),...void 0!==n?{contextWindow:n}:{},...this.policy?{policy:this.policy}:{},initialMessages:s,...void 0!==r?{derivedFrom:r}:{},...i?{initialCheckpoint:i}:{},...o?{onPersist:(e,s)=>o.appendMessages(t,e,s),onReplace:e=>o.replaceMessages(t,e),onToolExecution:e=>o.appendToolExecution(t,e),listToolExecutions:e=>o.listToolExecutions(t,e),getToolExecution:e=>o.getToolExecution(t,e),recoverUncoveredToolExecutions:()=>this.recoverUncoveredToolExecutionContext(t),listTranscriptMessages:e=>o.listTranscriptMessages(t,e),commitCompaction:e=>o.commitCompaction(t,e),readCheckpointTranscript:e=>o.readCheckpointTranscript(t,e)}:{},onClose:()=>{this.liveSessions.get(t)===v&&this.liveSessions.delete(t)}});return this.liveSessions.set(t,v),v}recoverUncoveredToolExecutionContext(e){const t=this.store?.recoverUncoveredToolExecutions(e,U);return void 0===t?[]:[t]}syncProviderOptions(e){this.providerOptions=e,this.clientManager.setSamplingProviderOptions(e)}switchModel(e){this.assertAcceptingSessions();const t=this.resolveContextWindowFor(e.provider,e.modelName),s=t?.window,i={model:e.model,...void 0!==s?{contextWindow:s}:{},...e.providerOptions?{providerOptions:e.providerOptions}:{},...e.structuredOutputProviderOptions?{structuredOutputProviderOptions:e.structuredOutputProviderOptions}:{},...e.structuredOutputReasoning?{structuredOutputReasoning:e.structuredOutputReasoning}:{},toolSchemaPolicy:this.toolSchemaPolicyFor(e.provider)};for(const e of this.liveSessions.values())if(!e.canSwitchModel())throw new Error("存在正在生成回复的会话,稍后再切换模型");this.switchingProviderName=e.provider;try{for(const e of this.liveSessions.values())e.switchModel(i)}finally{this.switchingProviderName=void 0}this.modelOverride=e.model,this.modelNameOverride=e.modelName,this.providerNameOverride=e.provider,this.providerOptions=e.providerOptions,this.structuredOutputProviderOptions=e.structuredOutputProviderOptions,this.structuredOutputReasoning=e.structuredOutputReasoning,this.clientManager.setSamplingModel(e.model),this.clientManager.setSamplingProviderOptions(e.providerOptions);for(const t of this.liveSessions.keys())this.store?.updateModel(t,e.modelName);return t}ensureReady(){return this.assertAcceptingSessions(),this.ready||(this.ready=this.bootstrap()),this.ready}async bootstrap(){const e=Date.now(),t=this.explicitModel??this.resolveModel();if(this.explicitSources)return{model:t,sources:this.explicitSources,...this.explicitSkillLibrary?{skillLibrary:this.explicitSkillLibrary}:{}};const s=this.explicitAgents??new o(this.config.agents.dataDir).list(),i=this.config.runtime.agentBootstrap.timeoutMs,r=e+i,n=new AbortController,a=new AgentBootstrapTimeoutError(i);let l=!1;const c=()=>{n.signal.aborted||n.abort(H(this.shutdownController.signal))},d=()=>{!this.closing&&!n.signal.aborted&&Date.now()>=r&&(l=!0,n.abort(a))};this.shutdownController.signal.aborted?c():this.shutdownController.signal.addEventListener("abort",c,{once:!0});const h=setTimeout(()=>{this.closing||n.signal.aborted||(l=!0,n.abort(a))},Math.max(0,r-Date.now())),u=(e,t)=>({kind:D.failed,issues:[{agentName:e.skill.name,message:a.message},...void 0===t?[]:[{agentName:e.skill.name,message:t.message}]]});try{if(this.agentInstallEnabled())try{this.resolvedCatalog=await this.resolveCatalogFn(this.config,{allowNetwork:!1,signal:n.signal,...this.config.install.registry?{registry:this.config.install.registry}:{}})}catch(e){if(this.closing||this.shutdownController.signal.aborted)throw W();if(d(),!l)throw e}if(this.closing||this.shutdownController.signal.aborted)throw W();if(d(),0===s.length){const e=this.resolveSkillLibrary(s);return{model:t,sources:[],...e?{skillLibrary:e}:{}}}const e=await R(s,L,async e=>{const s=[],i=e=>{s.push(e)};try{d(),q(n.signal);const o=await this.connectAgentSource(e,t,i,{signal:n.signal,deadlineAt:r});return{kind:D.connected,source:o,issues:s}}catch(t){if(this.closing||this.shutdownController.signal.aborted)throw W();return d(),l?u(e,t instanceof AgentBootstrapCleanupError?t:void 0):(s.push({agentName:e.skill.name,message:t instanceof Error?t.message:String(t)}),{kind:D.failed,issues:s})}},{signal:n.signal,onSkipped:e=>u(e)});if(this.closing||this.shutdownController.signal.aborted)throw W();const i=[];for(const t of e){for(const e of t.issues)this.onAgentBootstrapIssue?.(e);t.kind===D.connected&&i.push(t.source)}const o=this.resolveSkillLibrary(s);return{model:t,sources:i,...o?{skillLibrary:o}:{}}}finally{clearTimeout(h),this.shutdownController.signal.removeEventListener("abort",c)}}async connectAgentSource(e,t,s=e=>this.onAgentBootstrapIssue?.(e),i={}){this.assertAcceptingSessions(),q(i.signal),_(i.deadlineAt);const o=r(e),n=p(this.config,e.skill.name),a=this.agentUsageLeases.get(e.skill.name);let l,c;try{l=void 0===a?await this.acquireAgentUsage(e,n,i.signal):void 0,this.assertAcceptingSessions(),q(i.signal),_(i.deadlineAt);const r={timeoutMs:V($,i.deadlineAt),samplingModel:t,...this.providerOptions?{samplingProviderOptions:this.providerOptions}:{},...n?{env:n}:{},...i.signal?{signal:i.signal}:{}},d=this.clientManager.connectWithOwnership;c="function"==typeof d?await d.call(this.clientManager,e.skill.name,o,e.installPath,r):{client:await this.clientManager.connect(e.skill.name,o,e.installPath,r),commit:()=>{},rollback:async()=>{}},this.assertAcceptingSessions(),q(i.signal),_(i.deadlineAt);const p=(await c.client.listTools(void 0,{timeout:V(F,i.deadlineAt),...i.signal?{signal:i.signal}:{}})).tools;this.assertAcceptingSessions(),q(i.signal),_(i.deadlineAt);const g=u(p,{onSchemaIssue:t=>s({agentName:e.skill.name,message:h(e.skill.name,t)})}).map((t,i)=>{const o=Q(p[i]);return void 0!==o.issue&&s({agentName:e.skill.name,message:`Tool "${t.name}" 的 ${y} 无效(${o.issue}),已回退 Agent 级资源锁`}),{tool:t,annotations:K(p[i]),...o.hints?{resourceHints:o.hints}:{}}});return c.commit(),void 0!==l&&this.agentUsageLeases.set(e.skill.name,l),{agentName:e.skill.name,client:c.client,tools:g,...e.source?{agentSource:e.source.type}:{},transport:o.type,runtimeOwnership:e.runtime.ownership,..."stdio"===o.type?{resourceBaseDir:e.installPath}:{}}}catch(t){const s=[];if(void 0!==c&&await c.rollback().catch(e=>{s.push(e)}),void 0!==l){const t=l;await t.release().catch(i=>{this.agentUsageLeases.set(e.skill.name,t),s.push(i)})}if(s.length>0)throw new AgentBootstrapCleanupError(e.skill.name,t,s);throw t}}async prepareAgentRefresh(e){this.assertAcceptingSessions();const t=this.refreshChain.then(()=>this.runAgentRefresh(e));return this.refreshChain=t.then(()=>{},()=>{}),t}async runAgentRefresh(e){const t=await this.ensureReady(),s=await this.connectAgentSource(e,this.activeModel(t),void 0,{signal:this.shutdownController.signal}),i=[...t.sources.filter(e=>e.agentName!==s.agentName),s],r=this.explicitAgents??new o(this.config.agents.dataDir).list(),n=this.resolveSkillLibrary(r);this.ready=Promise.resolve({model:this.activeModel(t),sources:i,...n?{skillLibrary:n}:{}});const a=n?.list()??[],l=n&&a.length>0?n:void 0,c=this.resolveRuntimeShellProfile();return{source:s,...l?{skillLibrary:l}:{},capabilityContext:this.composeCapabilityContext(i.length,c)}}composeCapabilityContext(e,t){const s=this.resolveAgentOnboardingInfo();return{profile:t?.toolName??"no-shell",hostMode:this.hostMode,cwd:process.cwd(),platform:process.platform,...t?{shellHints:t.systemPromptHints()}:{},agentCount:e,...s?{agentOnboardingCatalog:s}:{}}}agentInstallEnabled(){return void 0===this.explicitSources&&void 0===this.explicitAgents}currentCatalog(){return this.resolvedCatalog??m(this.config)}resolveAgentOnboardingInfo(){if(!this.agentInstallEnabled())return;const e=this.currentCatalog();return 0!==e.length?e.map(e=>({shortName:e.shortName,description:e.description})):void 0}resolveAgentInstallBinding(){if(!this.agentInstallEnabled())return;const e=this.currentCatalog();return 0!==e.length?{catalog:e.map(e=>({shortName:e.shortName,description:e.description})),install:(e,t)=>this.installCatalogAgent(e,t)}:void 0}async installCatalogAgent(e,t){const s=this.currentCatalog().find(t=>t.shortName===e);if(!s)return{outcome:{ok:!1,message:`未知的官方 Agent 短名: ${e}`}};const i=await this.installAgentFn({packageSpec:g(s),skipBrowserSetup:!0,autoStart:!1,expectedSkillName:s.skillName},{agentsConfig:this.config.agents,installConfig:this.config.install,getStartEnv:e=>p(this.config,e),report:e=>t(X(e))});if(!i.ok)return{outcome:{ok:!1,message:i.message,...i.retryCommand?{retryCommand:i.retryCommand}:{}}};const o=i.agent,r="installed-package"===o.source?.type?o.source.installedVersion:void 0,n="core-managed"===o.runtime.ownership&&void 0!==o.runtime.setup?.playwright,a={ok:!0,agentName:o.skill.name,...r?{version:r}:{},missingEnv:(i.envReport?.missingRequired??[]).map(e=>e.name),...n?{retryCommand:`roll agent install ${s.shortName}`}:{},refreshApplied:!1};if((await this.ensureReady()).sources.some(e=>e.agentName===o.skill.name))return t(`Agent "${o.skill.name}" 本会话已接入旧版本连接,更新需重启 roll chat 生效。`),{outcome:a};try{return{outcome:a,refresh:await this.prepareAgentRefresh(o)}}catch(e){return t(`接入新 Agent 失败:${e instanceof Error?e.message:String(e)}`),{outcome:a}}}resolveSkillLibrary(e){if(null!==this.explicitSkillLibrary)return void 0!==this.explicitSkillLibrary?this.explicitSkillLibrary:w({agents:e,extraDirs:this.config.skills.dirs,...this.onSkillLibraryIssue?{onIssue:this.onSkillLibraryIssue}:{}})}resolveModel(){const e=this.resolveProviderName(),t=this.resolveModelName(),i=this.config.llm.providers[e];if(!i)throw new Error(`LLM provider "${e}" 未配置`);return s(e,t,i.apiKey,i.baseUrl)}resolveProviderName(){return this.providerNameOverride??this.config.runtime.provider??this.config.llm.defaultProvider}resolveContextWindowFor(e,t){return x({provider:e,model:t,...void 0!==this.config.runtime.contextWindow?{override:this.config.runtime.contextWindow}:{},catalog:this.modelCatalog})}resolveModelName(){return this.modelNameOverride??this.config.runtime.model??this.config.llm.defaultModel}toolSchemaPolicyFor(e){return t=>i(e,t)}resolveToolSchemaProviderName(){return this.switchingProviderName??this.resolveProviderName()}activeModel(e){return this.modelOverride??e.model}async getContextSummary(){const e=await this.ensureReady();this.assertAcceptingSessions();const t=this.workspaceInstructions?.current()?.path;return{agentCount:e.sources.length,toolCount:e.sources.reduce((e,t)=>e+t.tools.length,0),skillCount:e.skillLibrary?.list().length??0,...void 0!==t?{instructionsPath:t}:{}}}async releaseAgentUsageLeases(){const e=[...this.agentUsageLeases.entries()],t=await Promise.allSettled(e.map(async([e,t])=>{try{await t.release()}finally{this.agentUsageLeases.get(e)===t&&this.agentUsageLeases.delete(e)}}));for(const e of t)"rejected"===e.status&&process.stderr.write(`roll chat: Agent 使用租约释放失败: ${e.reason instanceof Error?e.reason.message:String(e.reason)}\n`)}async dispose(){if(void 0!==this.disposePromise)return this.disposePromise;this.closing=!0,this.shutdownController.abort(W());const e=Promise.allSettled([...this.ready?[this.ready]:[],this.refreshChain]).then(()=>{});return this.disposePromise=(async()=>{let t={failed:!1};try{await Promise.all([...this.liveSessions.values()].map(e=>e.close()))}catch(e){t={failed:!0,error:e}}this.liveSessions.clear(),await e;let s={failed:!1};try{await this.clientManager.disconnectAll(d(this.agentUsageLeases.values()))}catch(e){s={failed:!0,error:e}}if(await this.releaseAgentUsageLeases(),t.failed&&s.failed)throw new AggregateError([t.error,s.error],"ConversationEngine 关闭 session 与 MCP 连接均失败。");if(t.failed)throw t.error;if(s.failed)throw s.error})(),this.disposePromise}}
@@ -67,6 +67,6 @@ export declare class ModelCatalog {
67
67
  constructor(options: ModelCatalogOptions);
68
68
  data(): ModelCatalogData;
69
69
  lookup(provider: string, modelName: string): number | undefined;
70
- refreshIfStale(): Promise<ModelCatalogRefreshResult>;
70
+ refreshIfStale(signal?: AbortSignal): Promise<ModelCatalogRefreshResult>;
71
71
  }
72
72
  export {};
@@ -1 +1 @@
1
- import{mkdirSync as t,readFileSync as e,renameSync as o,writeFileSync as n}from"node:fs";import{homedir as s}from"node:os";import{dirname as r,resolve as a}from"node:path";import{z as i}from"zod";export const MODEL_CATALOG_SOURCE_URL="https://models.dev/api.json";export const MODEL_CATALOG_PROVIDER_IDS=["openai","anthropic","google","deepseek","xai","alibaba-cn","alibaba"];const c={openai:["openai"],anthropic:["anthropic"],google:["google"],deepseek:["deepseek"],xai:["xai"],qwen:["alibaba-cn","alibaba"]},h=i.object({context:i.number().int().positive(),input:i.number().int().positive().optional()});export const modelCatalogDataSchema=i.object({fetchedAt:i.string().datetime(),providers:i.record(i.string(),i.record(i.string(),h))});const d=i.record(i.string(),i.object({models:i.record(i.string(),i.object({limit:i.unknown().optional()}))}));function p(t){return MODEL_CATALOG_PROVIDER_IDS.some(e=>e===t)}export function trimModelCatalog(t,e){const o=d.parse(t),n={};for(const[t,e]of Object.entries(o)){if(!p(t))continue;const o={};for(const[t,n]of Object.entries(e.models)){const e=h.safeParse(n.limit);e.success&&(o[t]={context:e.data.context,...void 0!==e.data.input?{input:e.data.input}:{}})}Object.keys(o).length>0&&(n[t]=o)}const s=modelCatalogDataSchema.parse({fetchedAt:e,providers:n});if(!u(s))throw new Error("model catalog contains no supported models");return s}function l(t){return Object.values(t.providers).reduce((t,e)=>t+Object.keys(e).length,0)}function u(t){return l(t)>0}const f=/-\d{8}$/u,m="-latest";function E(t){const e=[t];let o=t;for(;;){const t=o.endsWith(m)?o.slice(0,-7):f.test(o)?o.replace(f,""):void 0;if(void 0===t||0===t.length)return e;e.push(t),o=t}}export function lookupCatalogContextWindow(t,e,o){const n=c[e]??[e],s=E(o);for(const e of n){const o=t.providers[e];if(o)for(const t of s){const e=o[t];if(e)return e.input??e.context}}}export const MODEL_CATALOG_REFRESH_RESULTS={skipped:"skipped",refreshed:"refreshed",failed:"failed"};const O=864e5,S=1e4;export function defaultModelCatalogCachePath(){return a(s(),".roll-agent","cache","model-catalog.json")}function g(t){try{const o=JSON.parse(e(t,"utf8")),n=modelCatalogDataSchema.safeParse(o);return n.success&&u(n.data)?n.data:void 0}catch{return}}function _(t,e,o){const n=Date.parse(t.fetchedAt);return n<=o&&n>Date.parse(e.fetchedAt)}export class ModelCatalog{snapshot;cachePath;ttlMs;fetchImpl;now;timeoutMs;current;constructor(t){this.snapshot=t.snapshot,this.cachePath=t.cachePath,this.ttlMs=t.ttlMs??O,this.fetchImpl=t.fetchImpl??fetch,this.now=t.now??Date.now,this.timeoutMs=t.timeoutMs??S}data(){if(this.current)return this.current;const t=void 0===this.cachePath?void 0:g(this.cachePath);return this.current=t&&_(t,this.snapshot,this.now())?t:this.snapshot,this.current}lookup(t,e){return lookupCatalogContextWindow(this.data(),t,e)}async refreshIfStale(){const e=this.cachePath;if(void 0===e)return MODEL_CATALOG_REFRESH_RESULTS.skipped;const s=this.now(),a=s-Date.parse(this.data().fetchedAt);if(a>=0&&a<this.ttlMs)return MODEL_CATALOG_REFRESH_RESULTS.skipped;try{const a=await this.fetchImpl(MODEL_CATALOG_SOURCE_URL,{signal:AbortSignal.timeout(this.timeoutMs)});if(!a.ok)return MODEL_CATALOG_REFRESH_RESULTS.failed;const i=trimModelCatalog(await a.json(),new Date(s).toISOString());t(r(e),{recursive:!0});const c=`${e}.tmp`;return n(c,JSON.stringify(i)),o(c,e),this.current=i,MODEL_CATALOG_REFRESH_RESULTS.refreshed}catch{return MODEL_CATALOG_REFRESH_RESULTS.failed}}}
1
+ import{mkdirSync as t,readFileSync as e,renameSync as o,writeFileSync as n}from"node:fs";import{homedir as s}from"node:os";import{dirname as r,resolve as a}from"node:path";import{z as i}from"zod";export const MODEL_CATALOG_SOURCE_URL="https://models.dev/api.json";export const MODEL_CATALOG_PROVIDER_IDS=["openai","anthropic","google","deepseek","xai","alibaba-cn","alibaba"];const c={openai:["openai"],anthropic:["anthropic"],google:["google"],deepseek:["deepseek"],xai:["xai"],qwen:["alibaba-cn","alibaba"]},h=i.object({context:i.number().int().positive(),input:i.number().int().positive().optional()});export const modelCatalogDataSchema=i.object({fetchedAt:i.string().datetime(),providers:i.record(i.string(),i.record(i.string(),h))});const d=i.record(i.string(),i.object({models:i.record(i.string(),i.object({limit:i.unknown().optional()}))}));function p(t){return MODEL_CATALOG_PROVIDER_IDS.some(e=>e===t)}export function trimModelCatalog(t,e){const o=d.parse(t),n={};for(const[t,e]of Object.entries(o)){if(!p(t))continue;const o={};for(const[t,n]of Object.entries(e.models)){const e=h.safeParse(n.limit);e.success&&(o[t]={context:e.data.context,...void 0!==e.data.input?{input:e.data.input}:{}})}Object.keys(o).length>0&&(n[t]=o)}const s=modelCatalogDataSchema.parse({fetchedAt:e,providers:n});if(!u(s))throw new Error("model catalog contains no supported models");return s}function l(t){return Object.values(t.providers).reduce((t,e)=>t+Object.keys(e).length,0)}function u(t){return l(t)>0}const f=/-\d{8}$/u,m="-latest";function E(t){const e=[t];let o=t;for(;;){const t=o.endsWith(m)?o.slice(0,-7):f.test(o)?o.replace(f,""):void 0;if(void 0===t||0===t.length)return e;e.push(t),o=t}}export function lookupCatalogContextWindow(t,e,o){const n=c[e]??[e],s=E(o);for(const e of n){const o=t.providers[e];if(o)for(const t of s){const e=o[t];if(e)return e.input??e.context}}}export const MODEL_CATALOG_REFRESH_RESULTS={skipped:"skipped",refreshed:"refreshed",failed:"failed"};const S=864e5,O=1e4;export function defaultModelCatalogCachePath(){return a(s(),".roll-agent","cache","model-catalog.json")}function _(t){try{const o=JSON.parse(e(t,"utf8")),n=modelCatalogDataSchema.safeParse(o);return n.success&&u(n.data)?n.data:void 0}catch{return}}function g(t,e,o){const n=Date.parse(t.fetchedAt);return n<=o&&n>Date.parse(e.fetchedAt)}export class ModelCatalog{snapshot;cachePath;ttlMs;fetchImpl;now;timeoutMs;current;constructor(t){this.snapshot=t.snapshot,this.cachePath=t.cachePath,this.ttlMs=t.ttlMs??S,this.fetchImpl=t.fetchImpl??fetch,this.now=t.now??Date.now,this.timeoutMs=t.timeoutMs??O}data(){if(this.current)return this.current;const t=void 0===this.cachePath?void 0:_(this.cachePath);return this.current=t&&g(t,this.snapshot,this.now())?t:this.snapshot,this.current}lookup(t,e){return lookupCatalogContextWindow(this.data(),t,e)}async refreshIfStale(e){if(e?.aborted)return MODEL_CATALOG_REFRESH_RESULTS.skipped;const s=this.cachePath;if(void 0===s)return MODEL_CATALOG_REFRESH_RESULTS.skipped;const a=this.now(),i=a-Date.parse(this.data().fetchedAt);if(i>=0&&i<this.ttlMs)return MODEL_CATALOG_REFRESH_RESULTS.skipped;try{const i=await this.fetchImpl(MODEL_CATALOG_SOURCE_URL,{signal:e?AbortSignal.any([e,AbortSignal.timeout(this.timeoutMs)]):AbortSignal.timeout(this.timeoutMs)});if(!i.ok)return MODEL_CATALOG_REFRESH_RESULTS.failed;const c=trimModelCatalog(await i.json(),new Date(a).toISOString());e?.throwIfAborted(),t(r(s),{recursive:!0});const h=`${s}.tmp`;return n(h,JSON.stringify(c)),o(h,s),this.current=c,MODEL_CATALOG_REFRESH_RESULTS.refreshed}catch{return MODEL_CATALOG_REFRESH_RESULTS.failed}}}
@@ -12,11 +12,11 @@ export declare const turnCancellationMetadataV1Schema: z.ZodReadonly<z.ZodObject
12
12
  reason: z.ZodEnum<["user", "timeout", "runtime"]>;
13
13
  }, "strip", z.ZodTypeAny, {
14
14
  version: 1;
15
- reason: "user" | "timeout" | "runtime";
15
+ reason: "runtime" | "user" | "timeout";
16
16
  kind: "turn-cancellation";
17
17
  }, {
18
18
  version: 1;
19
- reason: "user" | "timeout" | "runtime";
19
+ reason: "runtime" | "user" | "timeout";
20
20
  kind: "turn-cancellation";
21
21
  }>>;
22
22
  export declare function createTurnCancellationMessage(content: string, reason: SessionCancellationReason): AssistantModelMessage;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roll-agent/runtime",
3
- "version": "0.20.0",
3
+ "version": "0.20.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -35,7 +35,7 @@
35
35
  "zod": "^3.25.76"
36
36
  },
37
37
  "devDependencies": {
38
- "@roll-agent/core": "0.37.0",
38
+ "@roll-agent/core": "0.38.2",
39
39
  "@types/node": "^22.0.0"
40
40
  },
41
41
  "scripts": {