AgentSessionRuntime 我们先从代码开始看看,后续再总结整个模块的功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export class AgentSessionRuntime { private rebindSession?: (session : AgentSession ) => Promise <void >; private beforeSessionInvalidate?: () => void ; private _session : AgentSession ; private _services : AgentSessionServices ; private readonly createRuntime : CreateAgentSessionRuntimeFactory ; private _diagnostics : AgentSessionRuntimeDiagnostic []; private _modelFallbackMessage?: string ; }
管理的主体就是AgentSession和AgentSessionServices,然后提供了一些钩子 四个核心方法
switchSession
newSession
fork
importFromJsonl
switchSession 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 async switchSession ( sessionPath : string , options?: { cwdOverride?: string ; withSession?: (ctx : ReplacedSessionContext ) => Promise <void >; projectTrustContextFactory?: (cwd : string ) => ProjectTrustContext ; }, ): Promise <{ cancelled : boolean }> { const beforeResult = await this .emitBeforeSwitch ("resume" , sessionPath); if (beforeResult.cancelled ) { return beforeResult; } const previousSessionFile = this .session .sessionFile ; const sessionManager = SessionManager .open (sessionPath, undefined , options?.cwdOverride ); assertSessionCwdExists (sessionManager, this .cwd ); await this .teardownCurrent ("resume" , sessionManager.getSessionFile ()); this .apply ( await this .createRuntime ({ cwd : sessionManager.getCwd (), agentDir : this .services .agentDir , sessionManager, sessionStartEvent : { type : "session_start" , reason : "resume" , previousSessionFile }, projectTrustContext : options?.projectTrustContextFactory ?.(sessionManager.getCwd ()), }), ); await this .finishSessionReplacement (options?.withSession ); return { cancelled : false }; }private async teardownCurrent (reason : SessionShutdownEvent ["reason" ], targetSessionFile?: string ): Promise <void > { await this .session .abort (); await emitSessionShutdownEvent (this .session .extensionRunner , { type : "session_shutdown" , reason, targetSessionFile, }); this .beforeSessionInvalidate ?.(); this .session .dispose (); }
这里看看工厂函数和apply函数具体干了啥
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 export type CreateAgentSessionRuntimeFactory = (options : { cwd: string ; agentDir: string ; sessionManager: SessionManager; sessionStartEvent?: SessionStartEvent; projectTrustContext?: ProjectTrustContext; } ) => Promise <CreateAgentSessionRuntimeResult >;export interface CreateAgentSessionResult { session : AgentSession ; extensionsResult : LoadExtensionsResult ; modelFallbackMessage?: string ; }export interface CreateAgentSessionRuntimeResult extends CreateAgentSessionResult { services : AgentSessionServices ; diagnostics : AgentSessionRuntimeDiagnostic []; }private apply (result : CreateAgentSessionRuntimeResult ): void { this ._session = result.session ; this ._services = result.services ; this ._diagnostics = result.diagnostics ; this ._modelFallbackMessage = result.modelFallbackMessage ; }
newSession 再看这个逻辑旧非常清晰了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 async newSession (options?: { parentSession?: string ; setup?: (sessionManager : SessionManager ) => Promise <void >; withSession?: (ctx : ReplacedSessionContext ) => Promise <void >; }): Promise <{ cancelled : boolean }> { const beforeResult = await this .emitBeforeSwitch ("new" ); if (beforeResult.cancelled ) { return beforeResult; } const previousSessionFile = this .session .sessionFile ; const sessionDir = this .session .sessionManager .getSessionDir (); const sessionManager = this .session .sessionManager .isPersisted () ? SessionManager .create (this .cwd , sessionDir) : SessionManager .inMemory (this .cwd ); if (options?.parentSession ) { sessionManager.newSession ({ parentSession : options.parentSession }); } await this .teardownCurrent ("new" , sessionManager.getSessionFile ()); this .apply ( await this .createRuntime ({ cwd : this .cwd , agentDir : this .services .agentDir , sessionManager, sessionStartEvent : { type : "session_start" , reason : "new" , previousSessionFile }, }), ); if (options?.setup ) { await options.setup (this .session .sessionManager ); this .session .agent .state .messages = this .session .sessionManager .buildSessionContext ().messages ; } await this .finishSessionReplacement (options?.withSession ); return { cancelled : false }; }
fork 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 async fork ( entryId : string , options?: { position?: "before" | "at" ; withSession?: (ctx : ReplacedSessionContext ) => Promise <void > }, ): Promise <{ cancelled : boolean ; selectedText?: string }> { const position = options?.position ?? "before" ; const beforeResult = await this .emitBeforeFork (entryId, { position }); if (beforeResult.cancelled ) { return { cancelled : true }; } let targetLeafId : string | null ; let selectedText : string | undefined ; const selectedEntry = this .session .sessionManager .getEntry (entryId); if (!selectedEntry) { throw new Error ("Invalid entry ID for forking" ); } if (position === "at" ) { targetLeafId = selectedEntry.id ; } else { if (selectedEntry.type !== "message" || selectedEntry.message .role !== "user" ) { throw new Error ("Invalid entry ID for forking" ); } targetLeafId = selectedEntry.parentId ; selectedText = extractUserMessageText (selectedEntry.message .content ); } const previousSessionFile = this .session .sessionFile ; if (this .session .sessionManager .isPersisted ()) { const currentSessionFile = this .session .sessionFile ; if (!currentSessionFile) { throw new Error ("Persisted session is missing a session file" ); } const sessionDir = this .session .sessionManager .getSessionDir (); if (!targetLeafId) { const sessionManager = SessionManager .create (this .cwd , sessionDir); sessionManager.newSession ({ parentSession : currentSessionFile }); await this .teardownCurrent ("fork" , sessionManager.getSessionFile ()); this .apply ( await this .createRuntime ({ cwd : this .cwd , agentDir : this .services .agentDir , sessionManager, sessionStartEvent : { type : "session_start" , reason : "fork" , previousSessionFile }, }), ); await this .finishSessionReplacement (options?.withSession ); return { cancelled : false , selectedText }; } if (!existsSync (currentSessionFile)) { throw new Error ( "This session has not been saved yet. Wait for the first assistant response before cloning or forking it." , ); } const sessionManager = SessionManager .open (currentSessionFile, sessionDir); const forkedSessionPath = sessionManager.createBranchedSession (targetLeafId); if (!forkedSessionPath) { throw new Error ("Failed to create forked session" ); } await this .teardownCurrent ("fork" , sessionManager.getSessionFile ()); this .apply ( await this .createRuntime ({ cwd : sessionManager.getCwd (), agentDir : this .services .agentDir , sessionManager, sessionStartEvent : { type : "session_start" , reason : "fork" , previousSessionFile }, }), ); await this .finishSessionReplacement (options?.withSession ); return { cancelled : false , selectedText }; } const sessionManager = this .session .sessionManager ; if (!targetLeafId) { sessionManager.newSession ({ parentSession : this .session .sessionFile }); } else { sessionManager.createBranchedSession (targetLeafId); } await this .teardownCurrent ("fork" , sessionManager.getSessionFile ()); this .apply ( await this .createRuntime ({ cwd : this .cwd , agentDir : this .services .agentDir , sessionManager, sessionStartEvent : { type : "session_start" , reason : "fork" , previousSessionFile }, }), ); await this .finishSessionReplacement (options?.withSession ); return { cancelled : false , selectedText }; }
importFromJsonl 这个应该就是从文件中恢复session,不再赘述
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 async importFromJsonl (inputPath : string , cwdOverride?: string ): Promise <{ cancelled : boolean }> { const resolvedPath = resolvePath (inputPath); if (!existsSync (resolvedPath)) { throw new SessionImportFileNotFoundError (resolvedPath); } const sessionDir = this .session .sessionManager .getSessionDir (); if (!existsSync (sessionDir)) { mkdirSync (sessionDir, { recursive : true }); } const destinationPath = join (sessionDir, basename (resolvedPath)); const beforeResult = await this .emitBeforeSwitch ("resume" , destinationPath); if (beforeResult.cancelled ) { return beforeResult; } const previousSessionFile = this .session .sessionFile ; if (resolve (destinationPath) !== resolvedPath) { copyFileSync (resolvedPath, destinationPath); } const sessionManager = SessionManager .open (destinationPath, sessionDir, cwdOverride); assertSessionCwdExists (sessionManager, this .cwd ); await this .teardownCurrent ("resume" , sessionManager.getSessionFile ()); this .apply ( await this .createRuntime ({ cwd : sessionManager.getCwd (), agentDir : this .services .agentDir , sessionManager, sessionStartEvent : { type : "session_start" , reason : "resume" , previousSessionFile }, }), ); await this .finishSessionReplacement (); return { cancelled : false }; }
整体而言,AgentSessionRuntime 扮演的是「会话生命周期的稳定门面」这一角色。它对外暴露一个恒定不变的句柄,把「会话与工作目录会随时被替换」这件事彻底封装在内部:无论是切换、新建、fork 还是从 JSONL 导入,上层拿到的始终是同一个 runtime 引用,而其背后的 AgentSessionServices(目录绑定的基础设施)与 AgentSession(会话主体)则被原子地整体替换。
这种设计的价值在于引用稳定性与状态可变性的解耦 :调用方无需关心底层对象何时被重建,也不必在每次切换后重新持有引用、重新接线;同时通过 rebindSession / beforeSessionInvalidate 以及一系列 session_before_switch、session_shutdown 扩展事件,把「旧会话如何优雅退场、新会话如何平滑接管」这一过程完全暴露出来,让 UI 层与扩展能在恰当的时机介入清理与重绑。可以说,它用一层轻薄的间接,换来了整个上层交互对会话切换的无感知。
这里使用最近热门的archify生成了一个架构图,虽然文章本身线条已经很清晰了,但是架构图有利于全局理解
AgentSessionServices 1 2 3 4 5 6 7 8 9 10 11 12 13 14 export interface AgentSessionServices { cwd : string ; agentDir : string ; modelRuntime : ModelRuntime ; settingsManager : SettingsManager ; resourceLoader : ResourceLoader ; diagnostics : AgentSessionRuntimeDiagnostic []; }
主要是提供session与目录的绑定,便于切换session
AgentSession 这则是一个超级大类了,主要提供会话功能。先看看它持有的核心字段,按功能分组如下:
分组
关键字段
作用
核心依赖
agent / sessionManager / settingsManager / _modelRuntime
底层 agent、会话持久化、设置、模型鉴权
事件订阅
_unsubscribeAgent / _eventListeners / _isAgentRunActive
订阅 agent 事件并转发给模式层
消息队列
_steeringMessages / _followUpMessages / _pendingNextTurnMessages
流式中插队(steer)、排队(followUp)、下一轮上下文
压缩
_compactionAbortController / _autoCompactionAbortController / _overflowRecoveryAttempted
手动 + 自动 compaction,上下文溢出恢复
重试
_retryAbortController / _retryAttempt
助手错误后的自动重试
Bash
_bashAbortControllers / _pendingBashMessages
独立于 LLM 的 bash 执行
扩展/工具
_extensionRunner / _toolRegistry / _toolDefinitions / _baseSystemPrompt
扩展系统、动态工具注册表、系统提示词构建
总体而言就是这些模块构成,我们再看看它对外提供了什么方法。我让ai总结了一些对外提供的方法,总体分为4个部分
分组
方法名
作用
会话交互
prompt / steer / followUp / sendUserMessage / sendCustomMessage / clearQueue / getSteeringMessages / getFollowUpMessages / setSteeringMode / setFollowUpMode / abort / waitForIdle
输入驱动、插队/排队及其模式、中断与等待空闲
模型与上下文
setModel / cycleModel / setScopedModels / setThinkingLevel / cycleThinkingLevel / getAvailableThinkingLevels / supportsThinking / compact / abortCompaction / abortBranchSummary / setAutoCompactionEnabled / abortRetry / setAutoRetryEnabled
模型切换、思考等级、压缩与重试
工具与扩展
getActiveToolNames / getAllTools / getToolDefinition / setActiveToolsByName / subscribe / bindExtensions / reload / hasExtensionHandlers / executeBash / recordBashResult / abortBash
工具查询与启用、扩展绑定与事件、bash 执行
会话管理
setSessionName / navigateTree / getUserMessagesForForking / getSessionStats / getContextUsage / getLastAssistantText / exportToHtml / exportToJsonl / dispose / createReplacedSessionContext
命名、树形分支/fork、统计与导出、资源释放
也就是4个核心部分,会话交互这个部分再之前的agent内核中涉及到一些,这里再具体看看
会话交互 prompt 这里的核心入口是prompt方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 async prompt (text : string , options?: PromptOptions ): Promise <void > { const expandPromptTemplates = options?.expandPromptTemplates ?? true ; const preflightResult = options?.preflightResult ; let messages : AgentMessage [] | undefined ; try { if (expandPromptTemplates && text.startsWith ("/" )) { const handled = await this ._tryExecuteExtensionCommand (text); if (handled) { preflightResult?.(true ); return ; } } let currentText = text; let currentImages = options?.images ; if (this ._extensionRunner .hasHandlers ("input" )) { const inputResult = await this ._extensionRunner .emitInput ( currentText, currentImages, options?.source ?? "interactive" , this .isStreaming ? options?.streamingBehavior : undefined , ); if (inputResult.action === "handled" ) { preflightResult?.(true ); return ; } if (inputResult.action === "transform" ) { currentText = inputResult.text ; currentImages = inputResult.images ?? currentImages; } } let expandedText = currentText; if (expandPromptTemplates) { expandedText = this ._expandSkillCommand (expandedText); expandedText = expandPromptTemplate (expandedText, [...this .promptTemplates ]); } if (this .isStreaming ) { if (!options?.streamingBehavior ) { throw new Error ( "Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message." , ); } if (options.streamingBehavior === "followUp" ) { await this ._queueFollowUp (expandedText, currentImages); } else { await this ._queueSteer (expandedText, currentImages); } preflightResult?.(true ); return ; } this ._flushPendingBashMessages (); if (!this .model ) { throw new Error (formatNoModelSelectedMessage ()); } const hasConfiguredAuth = this ._modelRuntime .hasConfiguredAuth (this .model .provider ) || (await this ._modelRuntime .checkAuth (this .model .provider )) !== undefined ; if (!hasConfiguredAuth) { const isOAuth = this ._modelRuntime .isUsingOAuth (this .model .provider ); if (isOAuth) { throw new Error ( `Authentication failed for "${this .model.provider} ". ` + `Credentials may have expired or network is unavailable. ` + `Run '/login ${this .model.provider} ' to re-authenticate.` , ); } throw new Error (formatNoApiKeyFoundMessage (this .model .provider )); } const lastAssistant = this ._findLastAssistantMessage (); if (lastAssistant) { await this ._checkCompaction (lastAssistant, false ); } messages = []; const userContent : (TextContent | ImageContent )[] = [{ type : "text" , text : expandedText }]; if (currentImages) { userContent.push (...currentImages); } messages.push ({ role : "user" , content : userContent, timestamp : Date .now (), }); for (const msg of this ._pendingNextTurnMessages ) { messages.push (msg); } this ._pendingNextTurnMessages = []; const result = await this ._extensionRunner .emitBeforeAgentStart ( expandedText, currentImages, this ._baseSystemPrompt , this ._baseSystemPromptOptions , ); if (result?.messages ) { for (const msg of result.messages ) { messages.push ({ role : "custom" , customType : msg.customType , content : msg.content ?? [], display : msg.display , details : msg.details , timestamp : Date .now (), }); } } if (result?.systemPrompt !== undefined ) { this ._systemPromptOverride = result.systemPrompt ; this .agent .state .systemPrompt = result.systemPrompt ; } else { this ._systemPromptOverride = undefined ; this .agent .state .systemPrompt = this ._baseSystemPrompt ; } } catch (error) { preflightResult?.(false ); throw error; } if (!messages) { return ; } preflightResult?.(true ); await this ._runAgentPrompt (messages); }
这里再调用了await this._runAgentPrompt(messages)去处理发送message的流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private async _runAgentPrompt (messages : AgentMessage | AgentMessage []): Promise <void > { this ._isAgentRunActive = true ; try { await this .agent .prompt (messages); while (await this ._handlePostAgentRun ()) { await this .agent .continue (); } } finally { this ._systemPromptOverride = undefined ; this ._flushPendingBashMessages (); await this ._emitAgentSettled (); } }
这里我们具体看看是怎么判断是否需要continue的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 private async _handlePostAgentRun (): Promise <boolean > { const msg = this ._lastAssistantMessage ; this ._lastAssistantMessage = undefined ; if (!msg) { return false ; } if (this ._isRetryableError (msg) && (await this ._prepareRetry (msg))) { return true ; } if (msg.stopReason === "error" && this ._retryAttempt > 0 ) { this ._emit ({ type : "auto_retry_end" , success : false , attempt : this ._retryAttempt , finalError : msg.errorMessage , }); this ._retryAttempt = 0 ; } if (await this ._checkCompaction (msg)) { return true ; } return this .agent .hasQueuedMessages (); }
这里还有一个收尾工作,主要是设置非活跃和空闲状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 private async _emitAgentSettled (): Promise <void > { this ._isAgentRunActive = false ; try { await this ._extensionRunner .emit ({ type : "agent_settled" }); this ._emit ({ type : "agent_settled" }); } finally { this ._resolveIdleWaitIfIdle (); } }private _resolveIdleWaitIfIdle (): void { if (this ._isAgentRunActive || !this ._resolveIdleWait ) { return ; } const resolve = this ._resolveIdleWait ; this ._idleWaitPromise = undefined ; this ._resolveIdleWait = undefined ; resolve (); }private _getIdleWaitPromise (): Promise <void > { if (!this ._idleWaitPromise ) { this ._idleWaitPromise = new Promise ((resolve ) => { this ._resolveIdleWait = resolve; }); } return this ._idleWaitPromise ; }async waitForIdle (): Promise <void > { if (this .isIdle ) { return ; } await this ._getIdleWaitPromise (); }
steer 和 followUp 的区别
steer在turn中间能插入就插入
followup则是在一个turn结束再插入
模型与上下文 这一块比较重要的就是上下文压缩,具体可以看 PI分析-上下文压缩
工具与拓展 这里应该与主流程耦合很轻,算是独立模块,所以准备后续开个新文章写写
会话管理 这块其实主要就是SessionManager在管理,AgentSession也就是组合SessionManager的各个方法 这块其实并不算核心,其实本质就是将对话组织成树形形式,从而支持各种操作