@roll-agent/runtime 0.6.0 → 0.7.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.
- package/dist/bash/profile.js +1 -1
- package/dist/bash/session/head-tail-buffer.d.ts +1 -0
- package/dist/bash/session/head-tail-buffer.js +1 -1
- package/dist/bash/session/session-exec.d.ts +1 -1
- package/dist/bash/session/session-exec.js +1 -1
- package/dist/bash/session/session-manager.d.ts +31 -6
- package/dist/bash/session/session-manager.js +1 -1
- package/dist/bash/session/types.d.ts +48 -1
- package/dist/bash/session/types.js +1 -1
- package/dist/bash/session/yield-loop.d.ts +6 -2
- package/dist/bash/session/yield-loop.js +1 -1
- package/dist/bounded-wait.d.ts +7 -0
- package/dist/bounded-wait.js +1 -0
- package/dist/engine/agent-session.d.ts +8 -0
- package/dist/engine/agent-session.js +1 -1
- package/dist/engine/conversation-engine.d.ts +4 -1
- package/dist/engine/conversation-engine.js +1 -1
- package/dist/engine/system-prompt.d.ts +1 -0
- package/dist/engine/system-prompt.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/server/runtime-server.d.ts +1 -1
- package/dist/server/runtime-server.js +1 -1
- package/dist/tool-bridge/session-exec-tool.d.ts +5 -0
- package/dist/tool-bridge/session-exec-tool.js +1 -1
- package/dist/tool-bridge/skill-tool.js +1 -1
- package/dist/types/events.d.ts +1 -0
- package/package.json +2 -2
package/dist/bash/profile.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{spawn as e,spawnSync as o}from"node:child_process";import{existsSync as t}from"node:fs";import{win32 as r}from"node:path";import{performance as n}from"node:perf_hooks";import{unknownCommandClassifier as i}from"../types/command-classification.js";import{ruleBasedClassifier as s}from"./classifier/index.js";import{DEFAULT_KILL_GRACE_MS as l,killProcessGroup as a}from"./kill.js";import{resolveUserShell as c}from"./shell.js";export const SHELL_PROFILE_IDS=["posix","powershell"];export const SHELL_TOOL_NAMES=["bash","powershell"];const u=["-NoLogo","-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-EncodedCommand"],f=32767,d=1024,p=5e3,m=2e3,w=128,S=["PowerShell","7","pwsh.exe"],h=["System32","taskkill.exe"];function g(e,o){return o?.aborted?Promise.resolve(!1):new Promise(t=>{let r=!1;const n=e=>{r||(r=!0,clearTimeout(s),o?.removeEventListener("abort",i),t(e))},i=()=>n(!1),s=setTimeout(()=>n(!0),e);o?.addEventListener("abort",i,{once:!0})})}function E(e){const o=e.fileExists??t;return{id:"posix",toolName:"bash",supportsSessionExec:!0,supportsSafeCommandClassification:!0,waitForTreeKillAfterRootExit:!1,buildSpawn:(t,r,n)=>({file:c({platform:e.platform,env:e.env,fileExists:o}),args:["-c",t],options:{cwd:r,detached:!0,stdio:["ignore","pipe","pipe"],env:n}}),classify:(e,o)=>s.classify(e,o),async killTree(e,o,t={}){t.signal?.aborted||("interrupt"!==o?(a(e,"SIGTERM"),await g(l,t.signal)&&a(e,"SIGKILL")):a(e,"SIGINT"))},systemPromptHints:()=>["当前 shell 后端是 POSIX shell;优先使用 macOS/Linux 常见命令语法。","过滤和预览输出时可使用 grep、sed、head、tail 等 POSIX 工具。"]}}function v(e,o,t,r,n={}){return void 0===e||n.signal?.aborted?Promise.resolve():void 0===o?Promise.reject(new Error("无法从绝对 SystemRoot 路径解析 taskkill.exe,不能安全清理进程树")):new Promise((i,s)=>{let l,a=!1;const c=e=>{a||(a=!0,clearTimeout(f),n.signal?.removeEventListener("abort",u),e?s(e):i())},u=()=>{try{l.kill()}catch{}try{l.unref()}catch{}c()},f=setTimeout(()=>{try{l.kill()}catch{}try{l.unref()}catch{}c(new Error(`taskkill.exe 在 ${String(r)}ms 内未结束`))},r);try{l=t(o,["/PID",String(e),"/T","/F"],{stdio:"ignore",windowsHide:!0})}catch(e){return void c(new Error(`无法启动 taskkill.exe: ${x(e)}`,{cause:e}))}l.once("error",e=>{c(new Error(`无法启动 taskkill.exe: ${e.message}`,{cause:e}))}),l.once("close",(e,o)=>{if(0===e||e===w)return void c();const t=null===e?`signal=${o??"unknown"}`:`exitCode=${String(e)}`;c(new Error(`taskkill.exe 执行失败: ${t}`))}),n.signal?.addEventListener("abort",u,{once:!0}),n.signal?.aborted&&u()})}function x(e){return e instanceof Error?e.message:String(e)}export function buildPowerShellEncodedCommand(e){const o=["[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)","$OutputEncoding = [Console]::OutputEncoding","$ErrorActionPreference = 'Stop'","try {",e," if (Test-Path variable:LASTEXITCODE) { exit $LASTEXITCODE }","} catch {"," [Console]::Error.WriteLine([string]$_)"," exit 1","}"].join("\n");return Buffer.from(o,"utf16le").toString("base64")}function P(e,o){return[e,...o].reduce((e,o)=>e+o.length+1,0)}function b(e,o){const t=buildPowerShellEncodedCommand(e),r=[...u,t],n=P(o,r)+d;if(n>f)throw new Error(`PowerShell 命令过长:编码后命令行约 ${String(n)} 字符,超过 Windows CreateProcess 上限 ${String(f)};请拆分命令或改为执行脚本文件。`);return r}function k(e,o){const t=o.toLowerCase();for(const[o,r]of Object.entries(e))if(o.toLowerCase()===t&&void 0!==r&&r.length>0)return r}function y(e){const o=e.trim();return o.startsWith('"')&&o.endsWith('"')?o.slice(1,-1).trim():o}function C(e){return r.isAbsolute(e)&&r.parse(e).root.length>1}function T(e){const o=[],t=new Set,n=e=>{if(!C(e))return;const n=r.normalize(e).toLowerCase();t.has(n)||(t.add(n),o.push(r.normalize(e)))},i=k(e.env,"Path");for(const e of i?.split(r.delimiter)??[]){const o=y(e);C(o)&&n(r.join(o,"pwsh.exe"))}const s=[k(e.env,"ProgramFiles"),k(e.env,"ProgramW6432"),k(e.env,"ProgramFiles(x86)")];for(const e of s){if(void 0===e)continue;const o=y(e);C(o)&&n(r.join(o,...S))}return o}function L(e){const o=k(e,"SystemRoot");if(void 0===o)return;const t=y(o);return C(t)?r.join(t,...h):void 0}function I(e,o,t){const r=o(e,["-NoLogo","-NoProfile","-NonInteractive","-Command","$PSVersionTable.PSVersion.Major"],{encoding:"utf8",timeout:t,windowsHide:!0});if(r.error||0!==r.status)return;const n=`${r.stdout??""}`.trim(),i=Number.parseInt(n,10);return Number.isFinite(i)?i:void 0}function $(e,o){const r=e.fileExists??t,i=e.now??(()=>n.now()),s=i()+p;let l=!1;for(const t of T(e)){if(s-i()<=0)break;if(!r(t))continue;const e=Math.ceil(s-i());if(e<=0)break;const n=I(t,o,e);if(void 0!==n){if(n>=7)return{supported:!0,executable:t};l=!0}}return{supported:!1,reason:l?"pwsh-version-unsupported":"pwsh-not-found"}}function O(t){const r=$(t,t.spawnSync??o);if(!r.supported)return r;const n=t.spawn??e,s=L(t.env),l=t.taskkillTimeoutMs??m;return{supported:!0,profile:{id:"powershell",toolName:"powershell",supportsSessionExec:!
|
|
1
|
+
import{spawn as e,spawnSync as o}from"node:child_process";import{existsSync as t}from"node:fs";import{win32 as r}from"node:path";import{performance as n}from"node:perf_hooks";import{unknownCommandClassifier as i}from"../types/command-classification.js";import{ruleBasedClassifier as s}from"./classifier/index.js";import{DEFAULT_KILL_GRACE_MS as l,killProcessGroup as a}from"./kill.js";import{resolveUserShell as c}from"./shell.js";export const SHELL_PROFILE_IDS=["posix","powershell"];export const SHELL_TOOL_NAMES=["bash","powershell"];const u=["-NoLogo","-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-EncodedCommand"],f=32767,d=1024,p=5e3,m=2e3,w=128,S=["PowerShell","7","pwsh.exe"],h=["System32","taskkill.exe"];function g(e,o){return o?.aborted?Promise.resolve(!1):new Promise(t=>{let r=!1;const n=e=>{r||(r=!0,clearTimeout(s),o?.removeEventListener("abort",i),t(e))},i=()=>n(!1),s=setTimeout(()=>n(!0),e);o?.addEventListener("abort",i,{once:!0})})}function E(e){const o=e.fileExists??t;return{id:"posix",toolName:"bash",supportsSessionExec:!0,supportsSafeCommandClassification:!0,waitForTreeKillAfterRootExit:!1,buildSpawn:(t,r,n)=>({file:c({platform:e.platform,env:e.env,fileExists:o}),args:["-c",t],options:{cwd:r,detached:!0,stdio:["ignore","pipe","pipe"],env:n}}),classify:(e,o)=>s.classify(e,o),async killTree(e,o,t={}){t.signal?.aborted||("interrupt"!==o?(a(e,"SIGTERM"),await g(l,t.signal)&&a(e,"SIGKILL")):a(e,"SIGINT"))},systemPromptHints:()=>["当前 shell 后端是 POSIX shell;优先使用 macOS/Linux 常见命令语法。","过滤和预览输出时可使用 grep、sed、head、tail 等 POSIX 工具。"]}}function v(e,o,t,r,n={}){return void 0===e||n.signal?.aborted?Promise.resolve():void 0===o?Promise.reject(new Error("无法从绝对 SystemRoot 路径解析 taskkill.exe,不能安全清理进程树")):new Promise((i,s)=>{let l,a=!1;const c=e=>{a||(a=!0,clearTimeout(f),n.signal?.removeEventListener("abort",u),e?s(e):i())},u=()=>{try{l.kill()}catch{}try{l.unref()}catch{}c()},f=setTimeout(()=>{try{l.kill()}catch{}try{l.unref()}catch{}c(new Error(`taskkill.exe 在 ${String(r)}ms 内未结束`))},r);try{l=t(o,["/PID",String(e),"/T","/F"],{stdio:"ignore",windowsHide:!0})}catch(e){return void c(new Error(`无法启动 taskkill.exe: ${x(e)}`,{cause:e}))}l.once("error",e=>{c(new Error(`无法启动 taskkill.exe: ${e.message}`,{cause:e}))}),l.once("close",(e,o)=>{if(0===e||e===w)return void c();const t=null===e?`signal=${o??"unknown"}`:`exitCode=${String(e)}`;c(new Error(`taskkill.exe 执行失败: ${t}`))}),n.signal?.addEventListener("abort",u,{once:!0}),n.signal?.aborted&&u()})}function x(e){return e instanceof Error?e.message:String(e)}export function buildPowerShellEncodedCommand(e){const o=["[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)","$OutputEncoding = [Console]::OutputEncoding","$ErrorActionPreference = 'Stop'","try {",e," if (Test-Path variable:LASTEXITCODE) { exit $LASTEXITCODE }","} catch {"," [Console]::Error.WriteLine([string]$_)"," exit 1","}"].join("\n");return Buffer.from(o,"utf16le").toString("base64")}function P(e,o){return[e,...o].reduce((e,o)=>e+o.length+1,0)}function b(e,o){const t=buildPowerShellEncodedCommand(e),r=[...u,t],n=P(o,r)+d;if(n>f)throw new Error(`PowerShell 命令过长:编码后命令行约 ${String(n)} 字符,超过 Windows CreateProcess 上限 ${String(f)};请拆分命令或改为执行脚本文件。`);return r}function k(e,o){const t=o.toLowerCase();for(const[o,r]of Object.entries(e))if(o.toLowerCase()===t&&void 0!==r&&r.length>0)return r}function y(e){const o=e.trim();return o.startsWith('"')&&o.endsWith('"')?o.slice(1,-1).trim():o}function C(e){return r.isAbsolute(e)&&r.parse(e).root.length>1}function T(e){const o=[],t=new Set,n=e=>{if(!C(e))return;const n=r.normalize(e).toLowerCase();t.has(n)||(t.add(n),o.push(r.normalize(e)))},i=k(e.env,"Path");for(const e of i?.split(r.delimiter)??[]){const o=y(e);C(o)&&n(r.join(o,"pwsh.exe"))}const s=[k(e.env,"ProgramFiles"),k(e.env,"ProgramW6432"),k(e.env,"ProgramFiles(x86)")];for(const e of s){if(void 0===e)continue;const o=y(e);C(o)&&n(r.join(o,...S))}return o}function L(e){const o=k(e,"SystemRoot");if(void 0===o)return;const t=y(o);return C(t)?r.join(t,...h):void 0}function I(e,o,t){const r=o(e,["-NoLogo","-NoProfile","-NonInteractive","-Command","$PSVersionTable.PSVersion.Major"],{encoding:"utf8",timeout:t,windowsHide:!0});if(r.error||0!==r.status)return;const n=`${r.stdout??""}`.trim(),i=Number.parseInt(n,10);return Number.isFinite(i)?i:void 0}function $(e,o){const r=e.fileExists??t,i=e.now??(()=>n.now()),s=i()+p;let l=!1;for(const t of T(e)){if(s-i()<=0)break;if(!r(t))continue;const e=Math.ceil(s-i());if(e<=0)break;const n=I(t,o,e);if(void 0!==n){if(n>=7)return{supported:!0,executable:t};l=!0}}return{supported:!1,reason:l?"pwsh-version-unsupported":"pwsh-not-found"}}function O(t){const r=$(t,t.spawnSync??o);if(!r.supported)return r;const n=t.spawn??e,s=L(t.env),l=t.taskkillTimeoutMs??m;return{supported:!0,profile:{id:"powershell",toolName:"powershell",supportsSessionExec:!0,supportsSafeCommandClassification:!1,waitForTreeKillAfterRootExit:!0,buildSpawn:(e,o,t)=>({file:r.executable,args:b(e,r.executable),options:{cwd:o,detached:!1,stdio:["ignore","pipe","pipe"],env:t,windowsHide:!0}}),classify:(e,o)=>i.classify(e,o),async killTree(e,o,t={}){await v(e,s,n,l,t)},systemPromptHints:()=>["当前 shell 后端是 PowerShell 7;请使用 PowerShell 语法,例如 Get-ChildItem、Get-Content、Select-String。","过滤和预览输出时优先使用 Select-String、Select-Object -First、Get-Content -TotalCount。"]}}}export function resolveShellProfile(e){return"win32"===e.platform?O(e):{supported:!0,profile:E(e)}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{truncateMiddle as t}from"../truncate.js";export class HeadTailBuffer{head="";tailChunks=[];tailLength=0;omittedMiddle=0;headCapacity;tailCapacity;constructor(t){this.headCapacity=Math.ceil(t/2),this.tailCapacity=Math.max(t-this.headCapacity,1)}append(t){if(0===t.length)return;let i=t;if(0===this.tailLength&&this.head.length<this.headCapacity){const t=this.headCapacity-this.head.length;this.head+=i.slice(0,t),i=i.slice(t)}0!==i.length&&(this.tailChunks.push(i),this.tailLength+=i.length,this.trimTail())}trimTail(){for(;this.tailLength>this.tailCapacity&&this.tailChunks.length>1;){const t=this.tailChunks.shift()??"";this.tailLength-=t.length,this.omittedMiddle+=t.length}if(this.tailLength>this.tailCapacity&&1===this.tailChunks.length){const t=this.tailChunks[0]??"",i=t.slice(t.length-this.tailCapacity);this.omittedMiddle+=t.length-i.length,this.tailChunks=[i],this.tailLength=i.length}}hasPending(){return this.head.length>0||this.tailChunks.length>0}
|
|
1
|
+
import{truncateMiddle as t}from"../truncate.js";export class HeadTailBuffer{head="";tailChunks=[];tailLength=0;omittedMiddle=0;headCapacity;tailCapacity;constructor(t){this.headCapacity=Math.ceil(t/2),this.tailCapacity=Math.max(t-this.headCapacity,1)}append(t){if(0===t.length)return;let i=t;if(0===this.tailLength&&this.head.length<this.headCapacity){const t=this.headCapacity-this.head.length;this.head+=i.slice(0,t),i=i.slice(t)}0!==i.length&&(this.tailChunks.push(i),this.tailLength+=i.length,this.trimTail())}trimTail(){for(;this.tailLength>this.tailCapacity&&this.tailChunks.length>1;){const t=this.tailChunks.shift()??"";this.tailLength-=t.length,this.omittedMiddle+=t.length}if(this.tailLength>this.tailCapacity&&1===this.tailChunks.length){const t=this.tailChunks[0]??"",i=t.slice(t.length-this.tailCapacity);this.omittedMiddle+=t.length-i.length,this.tailChunks=[i],this.tailLength=i.length}}hasPending(){return this.head.length>0||this.tailChunks.length>0}snapshot(i){const h=this.tailChunks.join(""),a=this.omittedMiddle,e=a>0?`${this.head}\n…${String(a)} chars truncated…\n${h}`:this.head+h,s=t(e,i);return{text:s.text,omitted:a+s.removed}}drain(t){const i=this.snapshot(t);return this.head="",this.tailChunks=[],this.tailLength=0,this.omittedMiddle=0,i}}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ManagedSession, type SpawnSessionInput } from "./types.ts";
|
|
2
2
|
export declare function spawnSession(input: SpawnSessionInput): ManagedSession;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{spawn as e}from"node:child_process";import{constants as
|
|
1
|
+
import{spawn as e}from"node:child_process";import{constants as t}from"node:os";import{performance as o}from"node:perf_hooks";import{normalizeExitCode as n}from"../format-result.js";import{HeadTailBuffer as r}from"./head-tail-buffer.js";import{SESSION_STATES as i}from"./types.js";class Gate{opened=!1;waiters=[];open(){if(this.opened)return;this.opened=!0;const e=this.waiters;this.waiters=[];for(const t of e)t()}wait(){return this.opened?Promise.resolve():new Promise(e=>{this.waiters.push(e)})}}export function spawnSession(s){const a=s.profile.buildSpawn(s.command,s.workdir,s.env),d=e(a.file,a.args,a.options),p=new r(s.bufferCapacity),l=o.now(),m=new Gate,c=new Gate,u=new Gate;let w=s.onDelta,f=!1;const v={id:s.id,command:s.command,workdir:s.workdir,profile:s.profile,child:d,buffer:p,startedAt:l,state:i.running,exitCode:void 0,exitObserved:!1,closeObserved:!1,completedAt:void 0,terminationCause:void 0,cleanupError:void 0,lastUsedAt:l,beginPoll:e=>!f&&(f=!0,void 0!==e&&(w=e),!0),endPoll:()=>{w=void 0,f=!1},markStopping:e=>{v.state!==i.completed&&v.state!==i.cleanupFailed&&(v.terminationCause??=e,v.state=i.stopping)},markCompleted:()=>{v.state!==i.cleanupFailed&&(v.exitCode??=1,v.state=i.completed,v.completedAt??=o.now(),u.open())},markCleanupFailed:e=>{v.cleanupError=v.cleanupError?`${v.cleanupError};${e}`:e,v.exitCode??=1,v.state=i.cleanupFailed,v.completedAt??=o.now(),u.open()},waitExit:()=>m.wait(),waitClose:()=>c.wait(),waitSettled:()=>u.wait()},g=(e,t)=>{0!==t.length&&(p.append(t),w?.(e,t))},x=(e,t)=>{if(null===e)return;const o=new TextDecoder("utf-8");e.on("data",e=>{g(t,o.decode(e,{stream:!0}))}),e.on("end",()=>{g(t,o.decode())})};x(d.stdout,"stdout"),x(d.stderr,"stderr");const C=(e,o)=>{if(v.exitObserved)return;const r=null!==o?t.signals[o]:void 0;v.exitCode=n({timedOut:!1,code:e,signalNumber:r}),v.exitObserved=!0,v.state===i.running&&(v.state=i.draining),m.open()};return d.on("exit",(e,t)=>{C(e,t)}),d.on("close",(e,t)=>{C(e,t),v.closeObserved=!0,v.completedAt??=o.now(),c.open(),v.state!==i.stopping&&v.markCompleted()}),d.on("error",e=>{p.append(`无法启动进程: ${e.message}\n`),v.exitCode=v.exitCode??1,v.exitObserved=!0,v.state===i.running&&(v.state=i.draining),m.open()}),v}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BashStreamName } from "../exec.ts";
|
|
2
2
|
import type { ShellProfile } from "../profile.ts";
|
|
3
|
-
import type
|
|
3
|
+
import { type ManagedSession, type SessionSummary, type TerminalSessionState } from "./types.ts";
|
|
4
4
|
export declare class SessionCapError extends Error {
|
|
5
5
|
constructor(maxSessions: number);
|
|
6
6
|
}
|
|
@@ -11,23 +11,48 @@ export interface SessionManagerOptions {
|
|
|
11
11
|
readonly bufferCapacity: number;
|
|
12
12
|
readonly generateId?: () => number;
|
|
13
13
|
readonly interruptGraceMs?: number;
|
|
14
|
+
readonly killTreeTimeoutMs?: number;
|
|
15
|
+
readonly closeDrainTimeoutMs?: number;
|
|
16
|
+
readonly rootSettleTimeoutMs?: number;
|
|
14
17
|
}
|
|
15
18
|
export interface SpawnRequest {
|
|
16
19
|
readonly command: string;
|
|
17
20
|
readonly workdir: string;
|
|
18
21
|
readonly onDelta?: (stream: BashStreamName, delta: string) => void;
|
|
19
22
|
}
|
|
23
|
+
export interface SessionCleanupResult {
|
|
24
|
+
readonly sessionId: number;
|
|
25
|
+
readonly state: TerminalSessionState;
|
|
26
|
+
readonly cleanupError?: string;
|
|
27
|
+
}
|
|
28
|
+
type SessionTarget = number | Iterable<number>;
|
|
20
29
|
export declare class SessionManager {
|
|
21
30
|
private readonly sessions;
|
|
31
|
+
private readonly terminations;
|
|
22
32
|
private readonly options;
|
|
33
|
+
private acceptingSessions;
|
|
23
34
|
constructor(options: SessionManagerOptions);
|
|
24
35
|
spawn(request: SpawnRequest): ManagedSession;
|
|
25
36
|
get(id: number): ManagedSession | undefined;
|
|
26
|
-
|
|
37
|
+
/** Explicitly forget a terminal result. Running sessions cannot be orphaned through this API. */
|
|
38
|
+
delete(id: number): boolean;
|
|
39
|
+
/** Completed tombstones do not consume the cap; cleanup failures do until explicitly read/deleted. */
|
|
27
40
|
size(): number;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
list(): readonly SessionSummary[];
|
|
42
|
+
interrupt(target: SessionTarget): Promise<readonly SessionCleanupResult[]>;
|
|
43
|
+
terminate(target: SessionTarget): Promise<readonly SessionCleanupResult[]>;
|
|
44
|
+
interruptAll(): Promise<readonly SessionCleanupResult[]>;
|
|
45
|
+
terminateAll(): Promise<readonly SessionCleanupResult[]>;
|
|
46
|
+
close(): Promise<readonly SessionCleanupResult[]>;
|
|
47
|
+
private activeIds;
|
|
48
|
+
private stopTargets;
|
|
49
|
+
private requestStop;
|
|
50
|
+
private runTermination;
|
|
51
|
+
private killTree;
|
|
52
|
+
private forceKillRoot;
|
|
53
|
+
private destroyStreams;
|
|
54
|
+
private observeLifecycle;
|
|
55
|
+
private trimTombstones;
|
|
32
56
|
private allocateId;
|
|
33
57
|
}
|
|
58
|
+
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{spawnSession as s}from"./session-exec.js";const
|
|
1
|
+
import{performance as t}from"node:perf_hooks";import{waitForPromiseSettlement as e}from"../../bounded-wait.js";import{spawnSession as s}from"./session-exec.js";import{isTerminalSessionState as r,SESSION_STATES as i,SESSION_TERMINATION_CAUSES as o}from"./types.js";const n=150,a=2500,l=2e3,c=1e3,d=50,p=120;export class SessionCapError extends Error{constructor(t){super(`会话数已达上限 ${String(t)},且无空闲会话可回收`),this.name="SessionCapError"}}function m(){return 1e3+Math.floor(99e3*Math.random())}function h(t){return t instanceof Error?t.message:String(t)}function u(t){const e=Array.from(t,t=>{const e=t.codePointAt(0)??0;return e<32||127===e?" ":t}).join("").replace(/\s+/gu," ").trim(),s=Array.from(e);return s.length<=p?e:`${s.slice(0,119).join("")}…`}export class SessionManager{sessions=new Map;terminations=new Map;options;acceptingSessions=!0;constructor(t){this.options=t}spawn(t){if(!this.acceptingSessions)throw new Error("会话管理器已关闭,不再接受新命令");if(this.trimTombstones(),this.size()>=this.options.maxSessions)throw new SessionCapError(this.options.maxSessions);const e=this.allocateId(),r=s({id:e,command:t.command,workdir:t.workdir,profile:this.options.profile,env:this.options.env,bufferCapacity:this.options.bufferCapacity,...t.onDelta?{onDelta:t.onDelta}:{}});return this.sessions.set(e,r),this.observeLifecycle(r),r}get(t){return this.sessions.get(t)}delete(t){const e=this.sessions.get(t);return!(void 0===e||!r(e.state))&&this.sessions.delete(t)}size(){let t=0;for(const e of this.sessions.values())e.state!==i.completed&&(t+=1);return t}list(){const e=t.now();return[...this.sessions.values()].sort((t,e)=>t.startedAt-e.startedAt).map(t=>({sessionId:t.id,commandPreview:u(t.command),workdir:t.workdir,state:t.state,startedAt:t.startedAt,lastUsedAt:t.lastUsedAt,wallTimeMs:(t.completedAt??e)-t.startedAt,...void 0!==t.exitCode?{exitCode:t.exitCode}:{},...t.terminationCause?{terminationCause:t.terminationCause}:{},...t.cleanupError?{cleanupError:t.cleanupError}:{}}))}async interrupt(t){return this.stopTargets(t,o.interrupt)}async terminate(t){return this.stopTargets(t,o.terminate)}async interruptAll(){return this.stopTargets(this.activeIds(),o.interrupt)}async terminateAll(){return this.stopTargets(this.activeIds(),o.terminate)}async close(){return this.acceptingSessions=!1,this.terminateAll()}activeIds(){return[...this.sessions.values()].filter(t=>!r(t.state)).map(t=>t.id)}async stopTargets(t,e){const s="number"==typeof t?[t]:[...new Set(t)],i=[];for(const t of s){const s=this.sessions.get(t);void 0===s||r(s.state)||i.push(this.requestStop(s,e))}return Promise.all(i)}requestStop(t,e,s){const r=this.terminations.get(t.id);if(void 0!==r)return e===o.terminate&&(r.intent=e),void 0===s||r.errors.includes(s)||r.errors.push(s),r.promise;t.markStopping(e);const n={intent:e,errors:void 0===s?[]:[s],promise:Promise.resolve({sessionId:t.id,state:i.cleanupFailed})};return this.terminations.set(t.id,n),n.promise=this.runTermination(t,n).finally(()=>{this.terminations.delete(t.id),this.trimTombstones()}),n.promise}async runTermination(t,s){const i=s.intent,a=await this.killTree(t,i);let p=void 0!==a;if(void 0!==a&&s.errors.push(a),i===o.interrupt&&!t.closeObserved){s.intent!==o.terminate&&await e(t.waitClose(),this.options.interruptGraceMs??n);const r=t.exitObserved&&!0===t.profile.waitForTreeKillAfterRootExit;if(!t.closeObserved&&!r){const e=await this.killTree(t,o.terminate);void 0!==e&&(p=!0,s.errors.push(e))}}const m=this.options.rootSettleTimeoutMs??c;if(t.exitObserved||p||await e(t.waitExit(),m),!t.exitObserved){const r=this.forceKillRoot(t);void 0!==r&&s.errors.push(r),await e(t.waitExit(),m)}t.exitObserved||s.errors.push("根进程在强制终止请求后仍未确认退出"),t.closeObserved||await e(t.waitClose(),this.options.closeDrainTimeoutMs??l),t.closeObserved||(this.destroyStreams(t),await e(t.waitClose(),d)),t.closeObserved||s.errors.push("进程树终止后仍未观察到根进程退出与 stdio close"),s.errors.length>0?t.markCleanupFailed(s.errors.join(";")):t.markCompleted(),r(t.state)||t.markCleanupFailed("会话清理未能收口");const h=t.state;if(!r(h))throw new Error(`会话 ${String(t.id)} 清理后仍处于非终态`);return{sessionId:t.id,state:h,...t.cleanupError?{cleanupError:t.cleanupError}:{}}}async killTree(t,e){if(t.exitObserved&&!0===t.profile.waitForTreeKillAfterRootExit)return"根进程已退出,不能安全复用旧 PID 清理或确认后代进程树";const s=this.options.killTreeTimeoutMs??a,r=new AbortController;let i;try{const o=t.profile.killTree(t.child.pid,e,{signal:r.signal});return void await Promise.race([o,new Promise((t,e)=>{i=setTimeout(()=>{e(new Error(`进程树终止在 ${String(s)}ms 内未完成`)),r.abort()},s)})])}catch(t){return h(t)}finally{void 0!==i&&clearTimeout(i),r.abort()}}forceKillRoot(t){if(!t.exitObserved)try{return t.child.kill("SIGKILL")?void 0:"根进程强制终止请求未被接受"}catch(t){return`根进程强制终止请求失败: ${h(t)}`}}destroyStreams(t){t.child.stdin?.destroy(),t.child.stdout?.destroy(),t.child.stderr?.destroy(),t.child.unref()}observeLifecycle(t){t.waitSettled().then(()=>this.trimTombstones()),t.waitExit().then(()=>{if(t.closeObserved||r(t.state))return;const s=this.options.closeDrainTimeoutMs??l;e(t.waitSettled(),s).then(e=>{e||r(t.state)||this.requestStop(t,o.terminate,`根进程退出后 stdio 在 ${String(s)}ms 内未关闭`).catch(e=>{t.markCleanupFailed(`会话自动清理失败:${h(e)}`)})})})}trimTombstones(){const t=[...this.sessions.values()].filter(t=>t.state===i.completed&&!this.terminations.has(t.id)).sort((t,e)=>(t.completedAt??0)-(e.completedAt??0)),e=t.length-this.options.maxSessions;for(let s=0;s<e;s+=1){const e=t[s];void 0!==e&&this.sessions.delete(e.id)}}allocateId(){const t=this.options.generateId??m;for(let e=0;e<1e3;e+=1){const e=t();if(!this.sessions.has(e))return e}throw new Error("无法分配唯一会话 id")}}
|
|
@@ -3,17 +3,44 @@ import type { BashStreamName } from "../exec.ts";
|
|
|
3
3
|
import type { ShellProfile } from "../profile.ts";
|
|
4
4
|
import type { HeadTailBuffer } from "./head-tail-buffer.ts";
|
|
5
5
|
export type SessionDeltaHandler = (stream: BashStreamName, delta: string) => void;
|
|
6
|
+
export declare const SESSION_STATES: {
|
|
7
|
+
readonly running: "running";
|
|
8
|
+
readonly draining: "draining";
|
|
9
|
+
readonly stopping: "stopping";
|
|
10
|
+
readonly completed: "completed";
|
|
11
|
+
readonly cleanupFailed: "cleanup-failed";
|
|
12
|
+
};
|
|
13
|
+
export type SessionState = (typeof SESSION_STATES)[keyof typeof SESSION_STATES];
|
|
14
|
+
export type TerminalSessionState = typeof SESSION_STATES.completed | typeof SESSION_STATES.cleanupFailed;
|
|
15
|
+
export declare const SESSION_TERMINATION_CAUSES: {
|
|
16
|
+
readonly interrupt: "interrupt";
|
|
17
|
+
readonly terminate: "terminate";
|
|
18
|
+
};
|
|
19
|
+
export type SessionTerminationCause = (typeof SESSION_TERMINATION_CAUSES)[keyof typeof SESSION_TERMINATION_CAUSES];
|
|
6
20
|
export interface ManagedSession {
|
|
7
21
|
readonly id: number;
|
|
22
|
+
readonly command: string;
|
|
23
|
+
readonly workdir: string;
|
|
8
24
|
readonly profile: ShellProfile;
|
|
9
25
|
readonly child: ChildProcess;
|
|
10
26
|
readonly buffer: HeadTailBuffer;
|
|
11
27
|
readonly startedAt: number;
|
|
28
|
+
state: SessionState;
|
|
12
29
|
exitCode: number | undefined;
|
|
30
|
+
exitObserved: boolean;
|
|
31
|
+
closeObserved: boolean;
|
|
32
|
+
completedAt: number | undefined;
|
|
33
|
+
terminationCause: SessionTerminationCause | undefined;
|
|
34
|
+
cleanupError: string | undefined;
|
|
13
35
|
lastUsedAt: number;
|
|
14
|
-
onDelta
|
|
36
|
+
beginPoll(onDelta?: SessionDeltaHandler): boolean;
|
|
37
|
+
endPoll(): void;
|
|
38
|
+
markStopping(cause: SessionTerminationCause): void;
|
|
39
|
+
markCompleted(): void;
|
|
40
|
+
markCleanupFailed(message: string): void;
|
|
15
41
|
waitExit(): Promise<void>;
|
|
16
42
|
waitClose(): Promise<void>;
|
|
43
|
+
waitSettled(): Promise<void>;
|
|
17
44
|
}
|
|
18
45
|
export type SessionPollResult = {
|
|
19
46
|
readonly kind: "exited";
|
|
@@ -21,6 +48,9 @@ export type SessionPollResult = {
|
|
|
21
48
|
readonly omitted: number;
|
|
22
49
|
readonly wallTimeMs: number;
|
|
23
50
|
readonly exitCode: number;
|
|
51
|
+
readonly state: TerminalSessionState;
|
|
52
|
+
readonly terminationCause?: SessionTerminationCause;
|
|
53
|
+
readonly cleanupError?: string;
|
|
24
54
|
} | {
|
|
25
55
|
readonly kind: "running";
|
|
26
56
|
readonly output: string;
|
|
@@ -37,3 +67,20 @@ export interface SpawnSessionInput {
|
|
|
37
67
|
readonly bufferCapacity: number;
|
|
38
68
|
readonly onDelta?: SessionDeltaHandler;
|
|
39
69
|
}
|
|
70
|
+
export interface SessionPollOptions {
|
|
71
|
+
readonly abortSignal?: AbortSignal;
|
|
72
|
+
readonly onDelta?: SessionDeltaHandler;
|
|
73
|
+
}
|
|
74
|
+
export interface SessionSummary {
|
|
75
|
+
readonly sessionId: number;
|
|
76
|
+
readonly commandPreview: string;
|
|
77
|
+
readonly workdir: string;
|
|
78
|
+
readonly state: SessionState;
|
|
79
|
+
readonly startedAt: number;
|
|
80
|
+
readonly lastUsedAt: number;
|
|
81
|
+
readonly wallTimeMs: number;
|
|
82
|
+
readonly exitCode?: number;
|
|
83
|
+
readonly terminationCause?: SessionTerminationCause;
|
|
84
|
+
readonly cleanupError?: string;
|
|
85
|
+
}
|
|
86
|
+
export declare function isTerminalSessionState(state: SessionState): state is TerminalSessionState;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export{};
|
|
1
|
+
export const SESSION_STATES={running:"running",draining:"draining",stopping:"stopping",completed:"completed",cleanupFailed:"cleanup-failed"};export const SESSION_TERMINATION_CAUSES={interrupt:"interrupt",terminate:"terminate"};export function isTerminalSessionState(n){return n===SESSION_STATES.completed||n===SESSION_STATES.cleanupFailed}
|
|
@@ -1,2 +1,6 @@
|
|
|
1
|
-
import type
|
|
2
|
-
export declare
|
|
1
|
+
import { type ManagedSession, type SessionPollOptions, type SessionPollResult } from "./types.ts";
|
|
2
|
+
export declare class SessionPollInProgressError extends Error {
|
|
3
|
+
constructor(sessionId: number);
|
|
4
|
+
}
|
|
5
|
+
export declare function throwIfSessionExecAborted(signal: AbortSignal | undefined): void;
|
|
6
|
+
export declare function pollUntilDeadline(session: ManagedSession, deadlineMs: number, maxChars: number, options?: SessionPollOptions): Promise<SessionPollResult>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{performance as
|
|
1
|
+
import{performance as t}from"node:perf_hooks";import{isTerminalSessionState as e}from"./types.js";export class SessionPollInProgressError extends Error{constructor(t){super(`会话 ${String(t)} 已有一个轮询窗口在运行`),this.name="SessionPollInProgressError"}}function o(t){const e=t?.reason;return e instanceof Error?e:new DOMException("string"==typeof e&&e.length>0?e:"The operation was aborted","AbortError")}export function throwIfSessionExecAborted(t){if(!0===t?.aborted)throw o(t)}function r(t,r,n){return r<=0||e(t.state)?Promise.resolve():new Promise((e,s)=>{let i=!1;const a=t=>{i||(i=!0,clearTimeout(d),n?.removeEventListener("abort",l),void 0!==t?s(t):e())},l=()=>a(o(n)),d=setTimeout(()=>a(),r);t.waitSettled().then(()=>a()),n?.addEventListener("abort",l,{once:!0}),!0===n?.aborted&&l()})}export async function pollUntilDeadline(o,n,s,i={}){if(!o.beginPoll(i.onDelta))throw new SessionPollInProgressError(o.id);try{throwIfSessionExecAborted(i.abortSignal),await r(o,n-t.now(),i.abortSignal),throwIfSessionExecAborted(i.abortSignal);const a=t.now();o.lastUsedAt=a;const l=o.state,d=e(l),u=d?o.buffer.snapshot(s):o.buffer.drain(s),c=(d?o.completedAt??a:a)-o.startedAt;return d?{kind:"exited",output:u.text,omitted:u.omitted,wallTimeMs:c,exitCode:o.exitCode??1,state:l,...o.terminationCause?{terminationCause:o.terminationCause}:{},...o.cleanupError?{cleanupError:o.cleanupError}:{}}:{kind:"running",output:u.text,omitted:u.omitted,wallTimeMs:c,sessionId:o.id}}finally{o.lastUsedAt=t.now(),o.endPoll()}}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wait for a promise to settle within a bounded interval.
|
|
3
|
+
*
|
|
4
|
+
* Fulfilment and rejection both mean the observed work has settled. The caller remains
|
|
5
|
+
* responsible for handling the promise's domain error before or after using this timing guard.
|
|
6
|
+
*/
|
|
7
|
+
export declare function waitForPromiseSettlement(promise: PromiseLike<unknown>, timeoutMs: number): Promise<boolean>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function waitForPromiseSettlement(e,t){return new Promise(n=>{let o=!1;const r=e=>{o||(o=!0,clearTimeout(i),n(e))},i=setTimeout(()=>r(!1),t);i.unref(),e.then(()=>r(!0),()=>r(!0))})}
|
|
@@ -37,6 +37,7 @@ export interface AgentSessionOptions {
|
|
|
37
37
|
readonly bashClassifier?: CommandClassifier;
|
|
38
38
|
readonly bashSession?: AgentSessionBashSession;
|
|
39
39
|
readonly agentInstall?: AgentSessionAgentInstall;
|
|
40
|
+
readonly onClose?: () => void;
|
|
40
41
|
}
|
|
41
42
|
export interface SessionAgentRefresh {
|
|
42
43
|
readonly source: AgentToolSource;
|
|
@@ -70,6 +71,7 @@ export declare class AgentSession {
|
|
|
70
71
|
private readonly contextWindow;
|
|
71
72
|
private readonly compaction;
|
|
72
73
|
private readonly turnTimeoutMs;
|
|
74
|
+
private readonly onClose;
|
|
73
75
|
private providerOptions;
|
|
74
76
|
private readonly onProviderOptionsChange;
|
|
75
77
|
private readonly debugEvents;
|
|
@@ -85,6 +87,8 @@ export declare class AgentSession {
|
|
|
85
87
|
private readonly sessionManager;
|
|
86
88
|
private emit;
|
|
87
89
|
private activeTurn;
|
|
90
|
+
private closePromise;
|
|
91
|
+
private closed;
|
|
88
92
|
private sessionUsage;
|
|
89
93
|
private lastInputTokens;
|
|
90
94
|
private needsCompaction;
|
|
@@ -107,10 +111,14 @@ export declare class AgentSession {
|
|
|
107
111
|
private runCompaction;
|
|
108
112
|
private recoverFromContextError;
|
|
109
113
|
private isTurnAborted;
|
|
114
|
+
private execSessionIds;
|
|
115
|
+
private interruptExecSessions;
|
|
116
|
+
private abandonTurn;
|
|
110
117
|
private cancellationMessage;
|
|
111
118
|
private emitCancellation;
|
|
112
119
|
private persistCancelledTurn;
|
|
113
120
|
cancel(): boolean;
|
|
121
|
+
close(): Promise<void>;
|
|
114
122
|
abort(): void;
|
|
115
123
|
private debug;
|
|
116
124
|
private scheduleDebug;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomUUID as e}from"node:crypto";import{stepCountIs as t,streamText as s}from"ai";import{buildAgentToolset as o}from"../tool-bridge/build-tools.js";import{buildAgentInstallToolset as i}from"../tool-bridge/agent-install-tool.js";import{buildSkillToolset as n}from"../tool-bridge/skill-tool.js";import{buildBashToolset as a}from"../tool-bridge/bash-tool.js";import{buildSessionExecToolset as r}from"../tool-bridge/session-exec-tool.js";import{SessionManager as l}from"../bash/session/session-manager.js";import{withCleanEnv as c}from"../bash/clean-env.js";import{RUNTIME_CANCELLATION_ABORT_REASON as u,SESSION_CANCELLATION_REASONS as h,TURN_TIMEOUT_ABORT_REASON as p,USER_CANCELLATION_ABORT_REASON as m,isTurnTimeoutAbortReason as d}from"../types/cancellation.js";import{ToolRegistry as g}from"../tool-bridge/naming.js";import{buildChatSystemPrompt as b}from"./system-prompt.js";import{readIsError as v}from"../tool-bridge/normalize-result.js";import{ApprovalGate as T}from"../approval/approval-gate.js";import{compactMessages as k}from"./compactor.js";import{AsyncEventQueue as y}from"./event-queue.js";function f(e){return e instanceof Error?e.message:String(e)}function C(e){return"object"==typeof e&&null!==e&&"output"in e}function x(e){return C(e)?f(e.output):f(e)}function w(e){const t=x(e);return t.startsWith("已取消执行")||t.startsWith("策略拒绝执行")?t:void 0}function S(e){return/context[_ -]?length|context window|maximum context|token limit|too many tokens|prompt is too long|input is too long/i.test(f(e))}function A(e){const t=e.inputTokenDetails?.cacheReadTokens,s=e.inputTokenDetails?.cacheWriteTokens,o=e.outputTokenDetails?.reasoningTokens;return{...void 0!==e.inputTokens?{inputTokens:e.inputTokens}:{},...void 0!==e.outputTokens?{outputTokens:e.outputTokens}:{},...void 0!==e.totalTokens?{totalTokens:e.totalTokens}:{},...void 0!==t?{cachedInputTokens:t}:{},...void 0!==s?{cacheWriteTokens:s}:{},...void 0!==o?{reasoningTokens:o}:{}}}function M(e,t){return void 0===e&&void 0===t?void 0:(e??0)+(t??0)}function R(e,t){const s=M(e.cachedInputTokens,t.cachedInputTokens),o=M(e.cacheWriteTokens,t.cacheWriteTokens),i=M(e.reasoningTokens,t.reasoningTokens);return{inputTokens:(e.inputTokens??0)+(t.inputTokens??0),outputTokens:(e.outputTokens??0)+(t.outputTokens??0),totalTokens:(e.totalTokens??0)+(t.totalTokens??0),...void 0!==s?{cachedInputTokens:s}:{},...void 0!==o?{cacheWriteTokens:o}:{},...void 0!==i?{reasoningTokens:i}:{}}}function N(e,t){return void 0===t?e:void 0===e?t:Math.max(e,t)}function I(e,t){const s=e.trim(),o=t.trim();return s.length>0&&o.length>0&&o.startsWith(s)}function E(e,t){return void 0!==e?e:d(t)?h.timeout:h.runtime}function P(e){return e.flatMap(e=>{if("assistant"!==e.role||!Array.isArray(e.content))return[e];const t=e.content.filter(e=>"reasoning"!==e.type);return 0===t.length?[]:t.length===e.content.length?[e]:[{...e,content:t}]})}export class AgentSession{id;model;maxSteps;messages;onPersist;onReplace;contextWindow;compaction;turnTimeoutMs;providerOptions;onProviderOptionsChange;debugEvents;policy;systemPrompt;skillSummaries;skillLibrary;skillToolBuilt=!1;toolSourceAgentNames;gate=new T;tools;registry;sessionManager;emit;activeTurn;sessionUsage={inputTokens:0,outputTokens:0,totalTokens:0};lastInputTokens;needsCompaction=!1;constructor(e){this.id=e.id,this.model=e.model,this.maxSteps=e.maxSteps,this.messages=e.initialMessages?P(e.initialMessages):[],this.onPersist=e.onPersist,this.onReplace=e.onReplace,this.contextWindow=e.contextWindow,this.compaction=e.compaction,this.turnTimeoutMs=e.turnTimeoutMs,this.providerOptions=e.providerOptions,this.onProviderOptionsChange=e.onProviderOptionsChange,this.debugEvents=e.debugEvents??!1,this.policy=e.policy,this.systemPrompt=e.systemPrompt??b(),this.skillLibrary=e.skillLibrary,this.skillSummaries=e.skillLibrary?.list()??[],this.toolSourceAgentNames=new Set(e.sources.map(e=>e.agentName));const t=new g,s=e.skillLibrary?this.buildSkillTools(t):{},n={...e.policy?{policy:e.policy}:{},requestApproval:e=>this.requestApproval(e),emitEvent:e=>this.emit?.(e)},u=e.bashClassifier?{classifier:e.bashClassifier}:{},h=e.bash?a(e.bash,t,n,u):{};e.bashSession&&(this.sessionManager=new l({maxSessions:e.bashSession.maxSessions,profile:e.bashSession.profile,env:c(process.env),bufferCapacity:e.bashSession.bufferCapacity}));const p=e.bashSession&&this.sessionManager?r({workdir:e.bashSession.workdir,defaultYieldMs:e.bashSession.defaultYieldMs,maxOutputTokens:e.bashSession.maxOutputTokens},this.sessionManager,t,n,u):{},m=e.agentInstall,d=m?i({catalog:m.catalog,install:async(e,t)=>{const s=await m.install(e,t);return s.outcome.ok&&s.refresh?(this.applyAgentRefresh(s.refresh),{...s.outcome,refreshApplied:!0}):s.outcome}},t,{...e.policy?{policy:e.policy}:{},requestApproval:e=>this.requestApproval(e)}):{},v=o(e.sources,{...e.policy?{policy:e.policy}:{},requestApproval:e=>this.requestApproval(e)},t);this.tools={...s,...h,...p,...d,...v.tools},this.registry=v.registry}buildSkillTools(e){return this.skillToolBuilt=!0,n(()=>{if(!this.skillLibrary)throw new Error("skill library 不可用");return this.skillLibrary},e)}applyAgentRefresh(e){if(!this.toolSourceAgentNames.has(e.source.agentName)){const t=o([e.source],{...this.policy?{policy:this.policy}:{},requestApproval:e=>this.requestApproval(e)},this.registry);this.tools={...this.tools,...t.tools},this.toolSourceAgentNames.add(e.source.agentName)}e.skillLibrary&&(this.skillLibrary=e.skillLibrary,this.skillSummaries=e.skillLibrary.list(),this.skillToolBuilt||(this.tools={...this.tools,...this.buildSkillTools(this.registry)})),this.systemPrompt=e.systemPrompt}async*send(e){if(this.activeTurn)throw new Error("session already has an active turn");const t=new y;this.emit=e=>t.push(e);const s={abortController:new AbortController,aborted:!1,cancellationEventEmitted:!1,cancellationPersisted:!1};this.activeTurn=s,this.runTurn(t,s,e).catch(e=>{t.push({type:"error",stage:"execute",message:f(e)}),t.close()});try{for await(const e of t)yield e}finally{this.activeTurn===s&&this.abort(),void 0!==this.emit&&(this.emit=void 0)}}async*compact(e="manual"){if(this.activeTurn)throw new Error("session already has an active turn");const t=new y;this.emit=e=>t.push(e);const s={abortController:new AbortController,aborted:!1,cancellationEventEmitted:!1,cancellationPersisted:!1};this.activeTurn=s,this.runCompactionTurn(t,s,e).catch(e=>{t.push({type:"error",stage:"plan",message:f(e)}),t.close()});try{for await(const e of t)yield e}finally{this.activeTurn===s&&this.abort(),void 0!==this.emit&&(this.emit=void 0)}}async runTurn(o,i,n){const a=Date.now();let r,l;try{if(this.debug(o,"turn","start",a,{messages:this.messages.length,tools:Object.keys(this.tools).length,maxSteps:this.maxSteps,...void 0!==this.contextWindow?{contextWindow:this.contextWindow}:{},...void 0!==this.lastInputTokens?{lastInputTokens:this.lastInputTokens}:{}}),this.shouldAutoCompact()){this.debug(o,"compaction","auto requested before turn",a,{messages:this.messages.length,...void 0!==this.lastInputTokens?{lastInputTokens:this.lastInputTokens}:{}});try{await this.runCompaction(o,"auto",i)}catch(e){return void o.push({type:"error",stage:"plan",message:f(e)})}}if(i.aborted||i.abortController.signal.aborted)return r=this.messages.length,this.messages.push({role:"user",content:n}),void this.persistCancelledTurn(o,i,r,[],a);r=this.messages.length,this.messages.push({role:"user",content:n}),o.push({type:"message-start",messageId:e()}),this.debug(o,"model","calling streamText",a,{messages:this.messages.length,tools:Object.keys(this.tools).length,...void 0!==this.turnTimeoutMs?{timeoutMs:this.turnTimeoutMs}:{}}),void 0!==this.turnTimeoutMs&&(l=setTimeout(()=>{i.abortController.signal.aborted||(i.aborted=!0,i.cancellationReason=h.timeout,this.gate.abortAll("本轮运行超时"),i.abortController.abort(p))},this.turnTimeoutMs));let c=[];const u=s({model:this.model,system:this.systemPrompt,messages:this.messages,tools:this.tools,stopWhen:t(this.maxSteps),abortSignal:i.abortController.signal,...this.providerOptions?{providerOptions:this.providerOptions}:{},onAbort:({steps:e})=>{c=e.flatMap(e=>e.response.messages)}});this.debug(o,"model","streamText returned",a);let m,g,b,T,k,y,C="",M="",W=!1,j=0,O=!1;const D=this.scheduleDebug(o,"model","waiting for first stream event",a,{messages:this.messages.length});try{for await(const e of u.fullStream){switch(O||(O=!0,this.clearDebugTimer(D),this.debug(o,"model","first stream event",a,{part:e.type})),e.type){case"text-delta":if(!W){const t=M+e.text;if(I(t,n)){M=t;break}if(M.length>0){const t=M+e.text;M="",C+=t,o.push({type:"text-delta",delta:t});break}}C+=e.text,o.push({type:"text-delta",delta:e.text});break;case"tool-call":{W=!0,M="";const t=this.registry.resolve(e.toolName);o.push({type:"tool-call",toolCallId:e.toolCallId,agentName:t?.agentName??e.toolName,toolName:t?.toolName??e.toolName,input:e.input});break}case"tool-result":{const t=this.registry.resolve(e.toolName),s=v(e.output);o.push({type:"tool-result",toolCallId:e.toolCallId,agentName:t?.agentName??e.toolName,toolName:t?.toolName??e.toolName,output:e.output,isError:s}),i.aborted||i.abortController.signal.aborted||!s||(y=w(e.output));break}case"tool-error":{const t=this.registry.resolve(e.toolName),s=x(e.error);o.push({type:"tool-result",toolCallId:e.toolCallId,agentName:t?.agentName??e.toolName,toolName:t?.toolName??e.toolName,output:s,isError:!0}),i.aborted||i.abortController.signal.aborted||(y=w(e.error));break}case"finish-step":{const t=A(e.usage);g=N(g,t.inputTokens),j+=1,T=e.finishReason;const s=e.performance.outputTokensPerSecond??e.performance.effectiveOutputTokensPerSecond;void 0!==s&&Number.isFinite(s)&&(b=s),o.push({type:"step-finish",finishReason:e.finishReason,usage:t});break}case"finish":!W&&M.length>0&&(C+=M,o.push({type:"text-delta",delta:M}),M=""),m=A(e.totalUsage);break;case"error":if(this.isTurnAborted(i)||d(e.error)){i.aborted=!0,i.cancellationReason=E(i.cancellationReason,e.error);break}S(e.error)&&(this.needsCompaction=!0),k=f(e.error),o.push({type:"error",stage:"execute",message:k});break;case"abort":i.aborted=!0,i.cancellationReason=E(i.cancellationReason,e.reason)}if(void 0!==y)break}}finally{this.clearDebugTimer(D)}if(this.debug(o,"model","fullStream finished",a,{textChars:C.length}),void 0!==k)return this.messages.splice(r),void(this.needsCompaction&&await this.recoverFromContextError(o,i));if(i.aborted||i.abortController.signal.aborted)return i.cancellationReason=E(i.cancellationReason,i.abortController.signal.reason),void this.persistCancelledTurn(o,i,r,c,a);if(void 0!==y){const e=y,t=0===C.length?e:`\n${e}`;o.push({type:"text-delta",delta:t}),this.messages.push({role:"assistant",content:e}),this.debug(o,"persist","persisting messages",a,{appendedMessages:this.messages.length-r}),this.onPersist?.(this.messages.slice(r)),this.debug(o,"persist","messages persisted",a,{totalMessages:this.messages.length}),m&&(this.sessionUsage=R(this.sessionUsage,m));const s=g??m?.inputTokens;return void 0!==s&&(this.lastInputTokens=s),void o.push({type:"message-finish",text:e,...m?{totalUsage:m}:{},sessionUsage:{...this.sessionUsage},...void 0!==g?{contextInputTokens:g}:{},...void 0!==b?{outputTokensPerSecond:b}:{}})}let U;this.debug(o,"model","awaiting response messages",a);const q=this.scheduleDebug(o,"model","still awaiting response messages",a);try{U=(await u.response).messages}catch(e){return this.clearDebugTimer(q),this.isTurnAborted(i)||d(e)?(i.aborted=!0,i.cancellationReason=E(i.cancellationReason,e),void this.persistCancelledTurn(o,i,r,c,a)):(S(e)&&(this.needsCompaction=!0),this.messages.splice(r),o.push({type:"error",stage:"execute",message:f(e)}),void(this.needsCompaction&&await this.recoverFromContextError(o,i)))}finally{this.clearDebugTimer(q)}this.debug(o,"model","response messages ready",a,{responseMessages:U.length});const L=P(U);this.messages.push(...L),this.debug(o,"persist","persisting messages",a,{appendedMessages:this.messages.length-r}),this.onPersist?.(this.messages.slice(r)),this.debug(o,"persist","messages persisted",a,{totalMessages:this.messages.length}),m&&(this.sessionUsage=R(this.sessionUsage,m));const F=g??m?.inputTokens;void 0!==F&&(this.lastInputTokens=F);const z=j>=this.maxSteps&&"tool-calls"===T;o.push({type:"message-finish",text:C,...m?{totalUsage:m}:{},sessionUsage:{...this.sessionUsage},...void 0!==g?{contextInputTokens:g}:{},...void 0!==b?{outputTokensPerSecond:b}:{},...z?{stoppedAtStepLimit:!0}:{}})}catch(e){if(this.isTurnAborted(i)||d(e))return i.aborted=!0,i.cancellationReason=E(i.cancellationReason,i.abortController.signal.reason??e),void(void 0!==r?this.persistCancelledTurn(o,i,r,[],a):this.emitCancellation(o,i));void 0!==r&&this.messages.splice(r),S(e)&&(this.needsCompaction=!0),this.debug(o,"turn","error",a,{message:f(e)}),o.push({type:"error",stage:"execute",message:f(e)}),this.needsCompaction&&await this.recoverFromContextError(o,i)}finally{void 0!==l&&clearTimeout(l),this.activeTurn===i&&(this.activeTurn=void 0),o.close()}}requestApproval(t){const s=e(),o=this.gate.request(s);return void 0===this.emit?(this.gate.resolve(s,{approved:!1,reason:"approval event could not be delivered"}),o):(this.emit({type:"confirmation-required",approvalId:s,agentName:t.agentName,toolName:t.toolName,input:t.input,...t.reason?{reason:t.reason}:{}}),o)}approve(e){return this.gate.resolve(e,{approved:!0})}reject(e,t){return this.gate.resolve(e,{approved:!1,...t?{reason:t}:{}})}getMessages(){return this.messages}getContextWindow(){return this.contextWindow}getSessionUsage(){return{...this.sessionUsage}}getSkillSummaries(){return this.skillSummaries}setProviderOptions(e){this.providerOptions=e,this.onProviderOptionsChange?.(e)}shouldAutoCompact(){const e=this.compaction;return void 0!==e&&e.enabled&&(this.needsCompaction||void 0!==this.contextWindow&&void 0!==this.lastInputTokens&&this.lastInputTokens/this.contextWindow>=e.threshold)}async runCompactionTurn(e,t,s){try{await this.runCompaction(e,s,t)}catch(t){e.push({type:"error",stage:"plan",message:f(t)})}finally{this.activeTurn===t&&(this.activeTurn=void 0),e.close()}}async runCompaction(e,t,s){const o=Date.now(),i=this.compaction,n=i?.strategy??"summarize";if(this.debug(e,"compaction","start",o,{reason:t,messages:this.messages.length}),e.push({type:"compaction-start",reason:t}),this.isTurnAborted(s))return void(void 0!==s&&this.emitCancellation(e,s));if(!i)return void e.push({type:"context-compacted",reason:t,strategy:n,removed:0,kept:this.messages.length});const a=this.lastInputTokens;let r=i.strategy;const l=s?.abortController.signal;let c;try{c=await k({messages:this.messages,strategy:r,keepRecentTurns:i.keepRecentTurns,keepRecentTokens:i.keepRecentTokens,model:this.model,...l?{abortSignal:l}:{}})}catch(t){if(this.isTurnAborted(s))return void(void 0!==s&&this.emitCancellation(e,s));if("summarize"!==r)throw t;this.debug(e,"compaction","summarize failed, fallback to truncate",o,{message:f(t)}),r="truncate",c=await k({messages:this.messages,strategy:r,keepRecentTurns:i.keepRecentTurns,keepRecentTokens:i.keepRecentTokens,model:this.model,...l?{abortSignal:l}:{}})}if(this.isTurnAborted(s))return void(void 0!==s&&this.emitCancellation(e,s));const u=c.removed>0||c.truncatedTools>0;u&&(this.onReplace?.(c.messages),this.messages.splice(0,this.messages.length,...c.messages),this.lastInputTokens=void 0,this.needsCompaction=!1),e.push({type:"context-compacted",reason:t,strategy:r,removed:c.removed,kept:c.kept,...c.truncatedTools>0?{truncatedTools:c.truncatedTools}:{},...void 0!==a?{beforeInputTokens:a}:{}}),this.debug(e,"compaction","finish",o,{reason:t,strategy:r,removed:c.removed,kept:c.kept,truncatedTools:c.truncatedTools,progressed:u})}async recoverFromContextError(e,t){if(this.compaction?.enabled)try{await this.runCompaction(e,"auto",t)}catch(t){e.push({type:"error",stage:"plan",message:f(t)})}}isTurnAborted(e){return!0===e?.aborted||!0===e?.abortController.signal.aborted}cancellationMessage(e){switch(e.cancellationReason??h.runtime){case h.user:return"已取消本轮;正在运行的模型或工具已收到中断请求,已发生的外部副作用不会自动回滚。";case h.timeout:return`本轮因运行超时${void 0!==this.turnTimeoutMs?`(${String(this.turnTimeoutMs)}ms)`:""}而中断;未返回成功 tool result 或 Exit code: 0 的操作不能视为正常完成。`;case h.runtime:return"本轮被运行时中断;未返回成功 tool result 或 Exit code: 0 的操作状态未知。"}}emitCancellation(e,t){t.cancellationEventEmitted||(t.cancellationReason??=h.runtime,t.cancellationEventEmitted=!0,e.push({type:"turn-cancelled",reason:t.cancellationReason,message:this.cancellationMessage(t)}))}persistCancelledTurn(e,t,s,o,i){t.cancellationPersisted||(this.messages.splice(s+1),this.messages.push(...P(o)),this.messages.push({role:"assistant",content:this.cancellationMessage(t)}),t.cancellationPersisted=!0,this.debug(e,"persist","persisting cancelled turn",i,{appendedMessages:this.messages.length-s}),this.onPersist?.(this.messages.slice(s))),this.emitCancellation(e,t)}cancel(){return!!this.activeTurn&&(this.activeTurn.aborted=!0,this.activeTurn.cancellationReason=h.user,this.gate.abortAll("用户取消本轮"),this.sessionManager?.interruptAll(),this.activeTurn.abortController.abort(m),!0)}abort(){this.activeTurn&&(this.activeTurn.aborted=!0,this.activeTurn.cancellationReason??=h.runtime,this.gate.abortAll(),this.activeTurn.abortController.abort(u)),this.sessionManager?.terminateAll()}debug(e,t,s,o,i){this.debugEvents&&e.push({type:"debug",stage:t,message:s,...void 0!==o?{elapsedMs:Date.now()-o}:{},...void 0!==i?{data:i}:{}})}scheduleDebug(e,t,s,o,i){if(this.debugEvents)return setTimeout(()=>{this.debug(e,t,s,o,i)},5e3)}clearDebugTimer(e){void 0!==e&&clearTimeout(e)}}
|
|
1
|
+
import{randomUUID as e}from"node:crypto";import{waitForPromiseSettlement as t}from"../bounded-wait.js";import{stepCountIs as s,streamText as o}from"ai";import{buildAgentToolset as i}from"../tool-bridge/build-tools.js";import{buildAgentInstallToolset as n}from"../tool-bridge/agent-install-tool.js";import{buildSkillToolset as r}from"../tool-bridge/skill-tool.js";import{buildBashToolset as a}from"../tool-bridge/bash-tool.js";import{buildSessionExecToolset as l}from"../tool-bridge/session-exec-tool.js";import{SessionManager as c}from"../bash/session/session-manager.js";import{withCleanEnv as h}from"../bash/clean-env.js";import{RUNTIME_CANCELLATION_ABORT_REASON as u,SESSION_CANCELLATION_REASONS as p,TURN_TIMEOUT_ABORT_REASON as d,USER_CANCELLATION_ABORT_REASON as m,isTurnTimeoutAbortReason as g}from"../types/cancellation.js";import{ToolRegistry as b}from"../tool-bridge/naming.js";import{buildChatSystemPrompt as v}from"./system-prompt.js";import{readIsError as T}from"../tool-bridge/normalize-result.js";import{ApprovalGate as k}from"../approval/approval-gate.js";import{compactMessages as f}from"./compactor.js";import{AsyncEventQueue as y}from"./event-queue.js";const C=6e3;function x(){return{abortController:new AbortController,execSessionIds:new Set,aborted:!1,cancellationEventEmitted:!1,cancellationPersisted:!1}}function w(e){return e instanceof Error?e.message:String(e)}function S(e){return"object"==typeof e&&null!==e&&"output"in e}function I(e){return S(e)?w(e.output):w(e)}function R(e){const t=I(e);return t.startsWith("已取消执行")||t.startsWith("策略拒绝执行")?t:void 0}function A(e){return/context[_ -]?length|context window|maximum context|token limit|too many tokens|prompt is too long|input is too long/i.test(w(e))}function M(e){const t=e.inputTokenDetails?.cacheReadTokens,s=e.inputTokenDetails?.cacheWriteTokens,o=e.outputTokenDetails?.reasoningTokens;return{...void 0!==e.inputTokens?{inputTokens:e.inputTokens}:{},...void 0!==e.outputTokens?{outputTokens:e.outputTokens}:{},...void 0!==e.totalTokens?{totalTokens:e.totalTokens}:{},...void 0!==t?{cachedInputTokens:t}:{},...void 0!==s?{cacheWriteTokens:s}:{},...void 0!==o?{reasoningTokens:o}:{}}}function N(e,t){return void 0===e&&void 0===t?void 0:(e??0)+(t??0)}function E(e,t){const s=N(e.cachedInputTokens,t.cachedInputTokens),o=N(e.cacheWriteTokens,t.cacheWriteTokens),i=N(e.reasoningTokens,t.reasoningTokens);return{inputTokens:(e.inputTokens??0)+(t.inputTokens??0),outputTokens:(e.outputTokens??0)+(t.outputTokens??0),totalTokens:(e.totalTokens??0)+(t.totalTokens??0),...void 0!==s?{cachedInputTokens:s}:{},...void 0!==o?{cacheWriteTokens:o}:{},...void 0!==i?{reasoningTokens:i}:{}}}function P(e,t){return void 0===t?e:void 0===e?t:Math.max(e,t)}function j(e,t){const s=e.trim(),o=t.trim();return s.length>0&&o.length>0&&o.startsWith(s)}function W(e,t){return void 0!==e?e:g(t)?p.timeout:p.runtime}function $(e){return e.flatMap(e=>{if("assistant"!==e.role||!Array.isArray(e.content))return[e];const t=e.content.filter(e=>"reasoning"!==e.type);return 0===t.length?[]:t.length===e.content.length?[e]:[{...e,content:t}]})}export class AgentSession{id;model;maxSteps;messages;onPersist;onReplace;contextWindow;compaction;turnTimeoutMs;onClose;providerOptions;onProviderOptionsChange;debugEvents;policy;systemPrompt;skillSummaries;skillLibrary;skillToolBuilt=!1;toolSourceAgentNames;gate=new k;tools;registry;sessionManager;emit;activeTurn;closePromise;closed=!1;sessionUsage={inputTokens:0,outputTokens:0,totalTokens:0};lastInputTokens;needsCompaction=!1;constructor(e){this.id=e.id,this.model=e.model,this.maxSteps=e.maxSteps,this.messages=e.initialMessages?$(e.initialMessages):[],this.onPersist=e.onPersist,this.onReplace=e.onReplace,this.contextWindow=e.contextWindow,this.compaction=e.compaction,this.turnTimeoutMs=e.turnTimeoutMs,this.onClose=e.onClose,this.providerOptions=e.providerOptions,this.onProviderOptionsChange=e.onProviderOptionsChange,this.debugEvents=e.debugEvents??!1,this.policy=e.policy,this.systemPrompt=e.systemPrompt??v(),this.skillLibrary=e.skillLibrary,this.skillSummaries=e.skillLibrary?.list()??[],this.toolSourceAgentNames=new Set(e.sources.map(e=>e.agentName));const t=new b,s=e.skillLibrary?this.buildSkillTools(t):{},o={...e.policy?{policy:e.policy}:{},requestApproval:e=>this.requestApproval(e),emitEvent:e=>this.emit?.(e)},r=e.bashClassifier?{classifier:e.bashClassifier}:{},u=e.bash?a(e.bash,t,o,r):{};e.bashSession&&(this.sessionManager=new c({maxSessions:e.bashSession.maxSessions,profile:e.bashSession.profile,env:h(process.env),bufferCapacity:e.bashSession.bufferCapacity}));const p=e.bashSession&&this.sessionManager?l({workdir:e.bashSession.workdir,defaultYieldMs:e.bashSession.defaultYieldMs,maxOutputTokens:e.bashSession.maxOutputTokens},this.sessionManager,t,o,{...r,onSessionTouched:e=>this.activeTurn?.execSessionIds.add(e)}):{},d=e.agentInstall,m=d?n({catalog:d.catalog,install:async(e,t)=>{const s=await d.install(e,t);return s.outcome.ok&&s.refresh?(this.applyAgentRefresh(s.refresh),{...s.outcome,refreshApplied:!0}):s.outcome}},t,{...e.policy?{policy:e.policy}:{},requestApproval:e=>this.requestApproval(e)}):{},g=i(e.sources,{...e.policy?{policy:e.policy}:{},requestApproval:e=>this.requestApproval(e)},t);this.tools={...s,...u,...p,...m,...g.tools},this.registry=g.registry}buildSkillTools(e){return this.skillToolBuilt=!0,r(()=>{if(!this.skillLibrary)throw new Error("skill library 不可用");return this.skillLibrary},e)}applyAgentRefresh(e){if(!this.toolSourceAgentNames.has(e.source.agentName)){const t=i([e.source],{...this.policy?{policy:this.policy}:{},requestApproval:e=>this.requestApproval(e)},this.registry);this.tools={...this.tools,...t.tools},this.toolSourceAgentNames.add(e.source.agentName)}e.skillLibrary&&(this.skillLibrary=e.skillLibrary,this.skillSummaries=e.skillLibrary.list(),this.skillToolBuilt||(this.tools={...this.tools,...this.buildSkillTools(this.registry)})),this.systemPrompt=e.systemPrompt}async*send(e){if(this.closed)throw new Error("session is closed");if(this.activeTurn)throw new Error("session already has an active turn");const t=new y;this.emit=e=>t.push(e);const s=x();this.activeTurn=s,this.runTurn(t,s,e).catch(e=>{t.push({type:"error",stage:"execute",message:w(e)}),t.close()});try{for await(const e of t)yield e}finally{this.activeTurn===s&&this.abandonTurn(s),void 0!==this.emit&&(this.emit=void 0)}}async*compact(e="manual"){if(this.closed)throw new Error("session is closed");if(this.activeTurn)throw new Error("session already has an active turn");const t=new y;this.emit=e=>t.push(e);const s=x();this.activeTurn=s,this.runCompactionTurn(t,s,e).catch(e=>{t.push({type:"error",stage:"plan",message:w(e)}),t.close()});try{for await(const e of t)yield e}finally{this.activeTurn===s&&this.abandonTurn(s),void 0!==this.emit&&(this.emit=void 0)}}async runTurn(t,i,n){const r=Date.now();let a,l;try{if(this.debug(t,"turn","start",r,{messages:this.messages.length,tools:Object.keys(this.tools).length,maxSteps:this.maxSteps,...void 0!==this.contextWindow?{contextWindow:this.contextWindow}:{},...void 0!==this.lastInputTokens?{lastInputTokens:this.lastInputTokens}:{}}),this.shouldAutoCompact()){this.debug(t,"compaction","auto requested before turn",r,{messages:this.messages.length,...void 0!==this.lastInputTokens?{lastInputTokens:this.lastInputTokens}:{}});try{await this.runCompaction(t,"auto",i)}catch(e){return void t.push({type:"error",stage:"plan",message:w(e)})}}if(i.aborted||i.abortController.signal.aborted)return a=this.messages.length,this.messages.push({role:"user",content:n}),void this.persistCancelledTurn(t,i,a,[],r);a=this.messages.length,this.messages.push({role:"user",content:n}),t.push({type:"message-start",messageId:e()}),this.debug(t,"model","calling streamText",r,{messages:this.messages.length,tools:Object.keys(this.tools).length,...void 0!==this.turnTimeoutMs?{timeoutMs:this.turnTimeoutMs}:{}}),void 0!==this.turnTimeoutMs&&(l=setTimeout(()=>{i.abortController.signal.aborted||(i.aborted=!0,i.cancellationReason=p.timeout,this.gate.abortAll("本轮运行超时"),i.abortController.abort(d))},this.turnTimeoutMs));let c=[];const h=o({model:this.model,system:this.systemPrompt,messages:this.messages,tools:this.tools,stopWhen:s(this.maxSteps),abortSignal:i.abortController.signal,...this.providerOptions?{providerOptions:this.providerOptions}:{},onAbort:({steps:e})=>{c=e.flatMap(e=>e.response.messages)}});this.debug(t,"model","streamText returned",r);let u,m,b,v,k,f,y="",C="",x=!1,S=0,N=!1;const O=this.scheduleDebug(t,"model","waiting for first stream event",r,{messages:this.messages.length});try{for await(const e of h.fullStream){if(this.closed){i.aborted=!0,i.cancellationReason??=p.runtime;break}switch(N||(N=!0,this.clearDebugTimer(O),this.debug(t,"model","first stream event",r,{part:e.type})),e.type){case"text-delta":if(!x){const s=C+e.text;if(j(s,n)){C=s;break}if(C.length>0){const s=C+e.text;C="",y+=s,t.push({type:"text-delta",delta:s});break}}y+=e.text,t.push({type:"text-delta",delta:e.text});break;case"tool-call":{x=!0,C="";const s=this.registry.resolve(e.toolName);t.push({type:"tool-call",toolCallId:e.toolCallId,agentName:s?.agentName??e.toolName,toolName:s?.toolName??e.toolName,input:e.input});break}case"tool-result":{const s=this.registry.resolve(e.toolName),o=T(e.output);t.push({type:"tool-result",toolCallId:e.toolCallId,agentName:s?.agentName??e.toolName,toolName:s?.toolName??e.toolName,output:e.output,isError:o}),i.aborted||i.abortController.signal.aborted||!o||(f=R(e.output));break}case"tool-error":{const s=this.registry.resolve(e.toolName),o=I(e.error);t.push({type:"tool-result",toolCallId:e.toolCallId,agentName:s?.agentName??e.toolName,toolName:s?.toolName??e.toolName,output:o,isError:!0}),i.aborted||i.abortController.signal.aborted||(f=R(e.error));break}case"finish-step":{const s=M(e.usage);m=P(m,s.inputTokens),S+=1,v=e.finishReason;const o=e.performance.outputTokensPerSecond??e.performance.effectiveOutputTokensPerSecond;void 0!==o&&Number.isFinite(o)&&(b=o),t.push({type:"step-finish",finishReason:e.finishReason,usage:s});break}case"finish":!x&&C.length>0&&(y+=C,t.push({type:"text-delta",delta:C}),C=""),u=M(e.totalUsage);break;case"error":if(this.isTurnAborted(i)||g(e.error)){i.aborted=!0,i.cancellationReason=W(i.cancellationReason,e.error);break}A(e.error)&&(this.needsCompaction=!0),k=w(e.error),t.push({type:"error",stage:"execute",message:k});break;case"abort":i.aborted=!0,i.cancellationReason=W(i.cancellationReason,e.reason)}if(void 0!==f)break}}finally{this.clearDebugTimer(O)}if(this.debug(t,"model","fullStream finished",r,{textChars:y.length}),void 0!==k)return this.messages.splice(a),void(this.needsCompaction&&await this.recoverFromContextError(t,i));if(i.aborted||i.abortController.signal.aborted)return i.cancellationReason=W(i.cancellationReason,i.abortController.signal.reason),void this.persistCancelledTurn(t,i,a,c,r);if(void 0!==f){const e=f,s=0===y.length?e:`\n${e}`;t.push({type:"text-delta",delta:s}),this.messages.push({role:"assistant",content:e}),this.debug(t,"persist","persisting messages",r,{appendedMessages:this.messages.length-a}),this.onPersist?.(this.messages.slice(a)),this.debug(t,"persist","messages persisted",r,{totalMessages:this.messages.length}),u&&(this.sessionUsage=E(this.sessionUsage,u));const o=m??u?.inputTokens;return void 0!==o&&(this.lastInputTokens=o),void t.push({type:"message-finish",text:e,...u?{totalUsage:u}:{},sessionUsage:{...this.sessionUsage},...void 0!==m?{contextInputTokens:m}:{},...void 0!==b?{outputTokensPerSecond:b}:{}})}let D;this.debug(t,"model","awaiting response messages",r);const U=this.scheduleDebug(t,"model","still awaiting response messages",r);try{D=(await h.response).messages}catch(e){return this.clearDebugTimer(U),this.isTurnAborted(i)||g(e)?(i.aborted=!0,i.cancellationReason=W(i.cancellationReason,e),void this.persistCancelledTurn(t,i,a,c,r)):(A(e)&&(this.needsCompaction=!0),this.messages.splice(a),t.push({type:"error",stage:"execute",message:w(e)}),void(this.needsCompaction&&await this.recoverFromContextError(t,i)))}finally{this.clearDebugTimer(U)}if(this.debug(t,"model","response messages ready",r,{responseMessages:D.length}),this.isTurnAborted(i))return i.cancellationReason=W(i.cancellationReason,i.abortController.signal.reason),void this.persistCancelledTurn(t,i,a,c,r);const q=$(D);this.messages.push(...q),this.debug(t,"persist","persisting messages",r,{appendedMessages:this.messages.length-a}),this.onPersist?.(this.messages.slice(a)),this.debug(t,"persist","messages persisted",r,{totalMessages:this.messages.length}),u&&(this.sessionUsage=E(this.sessionUsage,u));const L=m??u?.inputTokens;void 0!==L&&(this.lastInputTokens=L);const F=S>=this.maxSteps&&"tool-calls"===v;t.push({type:"message-finish",text:y,...u?{totalUsage:u}:{},sessionUsage:{...this.sessionUsage},...void 0!==m?{contextInputTokens:m}:{},...void 0!==b?{outputTokensPerSecond:b}:{},...F?{stoppedAtStepLimit:!0}:{}})}catch(e){if(this.isTurnAborted(i)||g(e))return i.aborted=!0,i.cancellationReason=W(i.cancellationReason,i.abortController.signal.reason??e),void(void 0!==a?this.persistCancelledTurn(t,i,a,[],r):this.emitCancellation(t,i));void 0!==a&&this.messages.splice(a),A(e)&&(this.needsCompaction=!0),this.debug(t,"turn","error",r,{message:w(e)}),t.push({type:"error",stage:"execute",message:w(e)}),this.needsCompaction&&await this.recoverFromContextError(t,i)}finally{void 0!==l&&clearTimeout(l),this.activeTurn===i&&(this.activeTurn=void 0),t.close()}}requestApproval(t){const s=e(),o=this.gate.request(s);return void 0===this.emit?(this.gate.resolve(s,{approved:!1,reason:"approval event could not be delivered"}),o):(this.emit({type:"confirmation-required",approvalId:s,agentName:t.agentName,toolName:t.toolName,input:t.input,...t.reason?{reason:t.reason}:{}}),o)}approve(e){return this.gate.resolve(e,{approved:!0})}reject(e,t){return this.gate.resolve(e,{approved:!1,...t?{reason:t}:{}})}getMessages(){return this.messages}getContextWindow(){return this.contextWindow}getSessionUsage(){return{...this.sessionUsage}}getSkillSummaries(){return this.skillSummaries}setProviderOptions(e){this.providerOptions=e,this.onProviderOptionsChange?.(e)}shouldAutoCompact(){const e=this.compaction;return void 0!==e&&e.enabled&&(this.needsCompaction||void 0!==this.contextWindow&&void 0!==this.lastInputTokens&&this.lastInputTokens/this.contextWindow>=e.threshold)}async runCompactionTurn(e,t,s){try{await this.runCompaction(e,s,t)}catch(t){e.push({type:"error",stage:"plan",message:w(t)})}finally{this.activeTurn===t&&(this.activeTurn=void 0),e.close()}}async runCompaction(e,t,s){const o=Date.now(),i=this.compaction,n=i?.strategy??"summarize";if(this.debug(e,"compaction","start",o,{reason:t,messages:this.messages.length}),e.push({type:"compaction-start",reason:t}),this.isTurnAborted(s))return void(void 0!==s&&this.emitCancellation(e,s));if(!i)return void e.push({type:"context-compacted",reason:t,strategy:n,removed:0,kept:this.messages.length});const r=this.lastInputTokens;let a=i.strategy;const l=s?.abortController.signal;let c;try{c=await f({messages:this.messages,strategy:a,keepRecentTurns:i.keepRecentTurns,keepRecentTokens:i.keepRecentTokens,model:this.model,...l?{abortSignal:l}:{}})}catch(t){if(this.isTurnAborted(s))return void(void 0!==s&&this.emitCancellation(e,s));if("summarize"!==a)throw t;this.debug(e,"compaction","summarize failed, fallback to truncate",o,{message:w(t)}),a="truncate",c=await f({messages:this.messages,strategy:a,keepRecentTurns:i.keepRecentTurns,keepRecentTokens:i.keepRecentTokens,model:this.model,...l?{abortSignal:l}:{}})}if(this.isTurnAborted(s))return void(void 0!==s&&this.emitCancellation(e,s));const h=c.removed>0||c.truncatedTools>0;h&&(this.onReplace?.(c.messages),this.messages.splice(0,this.messages.length,...c.messages),this.lastInputTokens=void 0,this.needsCompaction=!1),e.push({type:"context-compacted",reason:t,strategy:a,removed:c.removed,kept:c.kept,...c.truncatedTools>0?{truncatedTools:c.truncatedTools}:{},...void 0!==r?{beforeInputTokens:r}:{}}),this.debug(e,"compaction","finish",o,{reason:t,strategy:a,removed:c.removed,kept:c.kept,truncatedTools:c.truncatedTools,progressed:h})}async recoverFromContextError(e,t){if(this.compaction?.enabled)try{await this.runCompaction(e,"auto",t)}catch(t){e.push({type:"error",stage:"plan",message:w(t)})}}isTurnAborted(e){return!0===e?.aborted||!0===e?.abortController.signal.aborted}execSessionIds(e){return[...e.execSessionIds].sort((e,t)=>e-t)}interruptExecSessions(e){const t=this.execSessionIds(e);0!==t.length&&this.sessionManager?.interrupt(t).catch(e=>{process.stderr.write(`roll chat: 中断后台会话失败: ${w(e)}\n`)})}abandonTurn(e){this.closed||this.activeTurn!==e||e.abortController.signal.aborted||(e.aborted=!0,e.cancellationReason??=p.runtime,this.gate.abortAll("本轮事件流已停止"),this.interruptExecSessions(e),e.abortController.abort(u))}cancellationMessage(e){const t=e.cancellationReason??p.runtime,s=this.execSessionIds(e),o=s.length>0?`本轮触达的后台会话 session_id: ${s.join(", ")}。`:"";switch(t){case p.user:return`已取消本轮;正在运行的模型或工具已收到中断请求。${o}已发生的外部副作用不会自动回滚。`;case p.timeout:return`本轮因运行超时${void 0!==this.turnTimeoutMs?`(${String(this.turnTimeoutMs)}ms)`:""}而中断;${s.length>0?`后台会话不会因轮超时被终止。${o}可在下一轮用 roll__exec_list 找回后继续轮询;`:""}未返回成功 tool result 或 Exit code: 0 的操作不能视为正常完成。`;case p.runtime:return`本轮被运行时中断;${o}未返回成功 tool result 或 Exit code: 0 的操作状态未知。`}}emitCancellation(e,t){if(t.cancellationEventEmitted)return;t.cancellationReason??=p.runtime,t.cancellationEventEmitted=!0;const s=this.execSessionIds(t);e.push({type:"turn-cancelled",reason:t.cancellationReason,message:this.cancellationMessage(t),...s.length>0?{execSessionIds:s}:{}})}persistCancelledTurn(e,t,s,o,i){if(this.closed)return this.messages.splice(s),t.cancellationPersisted=!0,void this.emitCancellation(e,t);t.cancellationPersisted||(this.messages.splice(s+1),this.messages.push(...$(o)),this.messages.push({role:"assistant",content:this.cancellationMessage(t)}),t.cancellationPersisted=!0,this.debug(e,"persist","persisting cancelled turn",i,{appendedMessages:this.messages.length-s}),this.onPersist?.(this.messages.slice(s))),this.emitCancellation(e,t)}cancel(){const e=this.activeTurn;return!!e&&(e.aborted=!0,e.cancellationReason=p.user,this.gate.abortAll("用户取消本轮"),this.interruptExecSessions(e),e.abortController.abort(m),!0)}close(){if(void 0!==this.closePromise)return this.closePromise;this.closed=!0;const e=this.activeTurn;return e&&(e.aborted=!0,e.cancellationReason??=p.runtime,this.gate.abortAll(),e.abortController.abort(u)),this.closePromise=(async()=>{const e=(this.sessionManager?.close()??Promise.resolve([])).then(e=>{for(const t of e)void 0!==t.cleanupError&&process.stderr.write(`roll chat: 后台会话 ${String(t.sessionId)} 清理失败: ${t.cleanupError}\n`)},e=>{process.stderr.write(`roll chat: 会话 ${this.id} 后台清理失败: ${w(e)}\n`)});await t(e,C)||process.stderr.write(`roll chat: 会话 ${this.id} 在 ${String(C)}ms 内未完成全部关闭步骤\n`);try{this.onClose?.()}catch(e){process.stderr.write(`roll chat: 会话 ${this.id} 关闭回调失败: ${w(e)}\n`)}})(),this.closePromise}abort(){this.close().catch(e=>{process.stderr.write(`roll chat: 会话 ${this.id} 关闭失败: ${w(e)}\n`)})}debug(e,t,s,o,i){this.debugEvents&&e.push({type:"debug",stage:t,message:s,...void 0!==o?{elapsedMs:Date.now()-o}:{},...void 0!==i?{data:i}:{}})}scheduleDebug(e,t,s,o,i){if(this.debugEvents)return setTimeout(()=>{this.debug(e,t,s,o,i)},5e3)}clearDebugTimer(e){void 0!==e&&clearTimeout(e)}}
|
|
@@ -69,10 +69,13 @@ export declare class ConversationEngine {
|
|
|
69
69
|
private resolvedCatalog;
|
|
70
70
|
private shellProfileResolution;
|
|
71
71
|
private shellUnsupportedWarned;
|
|
72
|
-
private
|
|
72
|
+
private readonly liveSessions;
|
|
73
|
+
private disposePromise;
|
|
74
|
+
private closing;
|
|
73
75
|
constructor(options: ConversationEngineOptions);
|
|
74
76
|
createSession(input?: CreateSessionInput): Promise<AgentSession>;
|
|
75
77
|
resumeSession(threadId: string): Promise<AgentSession>;
|
|
78
|
+
private assertAcceptingSessions;
|
|
76
79
|
private resolveRuntimeShellProfile;
|
|
77
80
|
private resolveShellSettings;
|
|
78
81
|
private resolveSessionExecSettings;
|
|
@@ -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}from"@roll-agent/core/llm/providers";import{AgentStore as i}from"@roll-agent/core/registry/store";import{resolveTransportWithDevSpawnSpec as o}from"@roll-agent/core/registry/dev-spawn";import{getAgentPid as r,startAgent as n,waitForAgentReady as l}from"@roll-agent/core/registry/process-manager";import{normalizeListedTools as a}from"@roll-agent/core/cli/utils/agent-tools";import{getAgentEnv as c}from"@roll-agent/core/config/helpers";import{catalogPackageSpec as h,getAgentCatalog as p}from"@roll-agent/core/registry/catalog";import{resolveAgentCatalog as m}from"@roll-agent/core/registry/catalog-discovery";import{installAgent as d}from"@roll-agent/core/registry/install";import{createSkillLibrary as g}from"@roll-agent/core/skills/library";import{AGENT_INSTALL_TOOL_ID as u}from"../tool-bridge/agent-install-tool.js";import{AgentSession as f}from"./agent-session.js";import{resolveContextWindow as y}from"./context-window.js";import{buildChatSystemPrompt as v}from"./system-prompt.js";import{BASH_TOOL_ID as S,POWERSHELL_TOOL_ID as b}from"../tool-bridge/bash-tool.js";import{EXEC_COMMAND_ID as x,EXEC_POLL_ID as w}from"../tool-bridge/session-exec-tool.js";import{unknownCommandClassifier as k}from"../types/command-classification.js";import{resolveShellProfile as C}from"../bash/profile.js";const M=80;function A(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 P(e){return"retry"===e.type?`安装遇到网络问题,${Math.round(e.delayMs/1e3)}s 后重试(第 ${e.attempt+1} 次)...`:"warn"===e.type?`警告:${e.message}`:e.message}async function E(e,t,s){"core-managed"===e.runtime.ownership&&(void 0===r(t,e.skill.name)&&n(e,t,s),await l(e))}export class ConversationEngine{config;clientManager;store;policy;maxSteps;providerOptions;ensureAgentReady;debugEvents;explicitAgents;explicitModel;explicitSources;explicitSkillLibrary;onSkillLibraryIssue;onAgentBootstrapIssue;sessionExecEnabled;explicitShellProfile;resolveShellProfileFn;installAgentFn;resolveCatalogFn;ready;refreshChain=Promise.resolve();resolvedCatalog;shellProfileResolution;shellUnsupportedWarned=!1;shellSessionUnsupportedWarned=!1;constructor(e){this.config=e.config,this.clientManager=e.clientManager??new t,this.store=e.store,this.policy=e.policy,this.maxSteps=e.maxSteps??M,this.providerOptions=e.providerOptions,this.ensureAgentReady=e.ensureAgentReady??((e,t)=>E(e,this.config.agents.dataDir,t)),this.debugEvents=e.debugEvents??!1,this.sessionExecEnabled=e.sessionExecEnabled??!0,this.explicitShellProfile=e.shellProfile,this.resolveShellProfileFn=e.resolveShellProfileFn??C,this.explicitAgents=e.agents,this.explicitModel=e.model,this.explicitSources=e.sources,this.explicitSkillLibrary=e.skillLibrary,this.onSkillLibraryIssue=e.onSkillLibraryIssue,this.onAgentBootstrapIssue=e.onAgentBootstrapIssue,this.installAgentFn=e.installAgentFn??d,this.resolveCatalogFn=e.resolveCatalogFn??m}async createSession(t={}){const s=await this.ensureReady(),i=this.store?this.store.createThread({...t.title?{title:t.title}:{},model:this.resolveModelName()}):e();return this.buildSession(s,i,[])}async resumeSession(e){if(!this.store)throw new Error("resumeSession requires a ThreadStore");if(!this.store.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const t=await this.ensureReady();return this.buildSession(t,e,this.store.getMessages(e))}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:process.env}));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){const t=this.config.runtime.shell;return{workdir:process.cwd(),defaultTimeoutMs:t.defaultTimeoutMs,maxTimeoutMs:t.maxTimeoutMs,turnTimeoutMs:this.config.runtime.turnTimeoutMs,maxCaptureBytes:t.maxCaptureBytes,maxModelOutputChars:t.maxModelOutputChars,profile:e}}resolveSessionExecSettings(e){if(!this.sessionExecEnabled)return;const t=this.config.runtime.shell;if(t.enabled&&t.session.enabled){if(e.supportsSessionExec)return{workdir:process.cwd(),profile:e,maxSessions:t.session.maxSessions,defaultYieldMs:t.session.defaultYieldMs,maxOutputTokens:t.session.maxOutputTokens,bufferCapacity:t.maxCaptureBytes};this.shellSessionUnsupportedWarned||(this.shellSessionUnsupportedWarned=!0,process.stderr.write("roll chat: Windows 原生 session exec 暂未支持,已跳过 roll__exec_command / roll__exec_poll\n"))}}buildSession(e,t,s){const i=this.store,o=y(this.resolveModelName(),this.config.runtime.contextWindow),r=e.skillLibrary?.list()??[],n=r.length>0?e.skillLibrary:void 0,l=this.resolveRuntimeShellProfile(),a=l?this.resolveShellSettings(l):void 0,c=l?this.resolveSessionExecSettings(l):void 0,h=l?l.supportsSafeCommandClassification&&this.config.runtime.shell.autoApproveSafe?l:k:void 0,p=this.composeSystemPrompt(r,e.sources.length,l,c),m=this.resolveAgentInstallBinding();return new f({id:t,model:e.model,sources:e.sources,systemPrompt:p,...n?{skillLibrary:n}:{},...a?{bash:a}:{},...h?{bashClassifier:h}:{},...c?{bashSession:c}:{},...m?{agentInstall:m}:{},maxSteps:this.maxSteps,compaction:this.config.runtime.compaction,turnTimeoutMs:this.config.runtime.turnTimeoutMs,debugEvents:this.debugEvents,...this.providerOptions?{providerOptions:this.providerOptions}:{},onProviderOptionsChange:e=>this.syncProviderOptions(e),...void 0!==o?{contextWindow:o}:{},...this.policy?{policy:this.policy}:{},initialMessages:s,...i?{onPersist:e=>i.appendMessages(t,e),onReplace:e=>i.replaceMessages(t,e)}:{}})}syncProviderOptions(e){this.providerOptions=e,this.clientManager.setSamplingProviderOptions(e)}ensureReady(){return this.ready||(this.ready=this.bootstrap()),this.ready}async bootstrap(){const e=this.explicitModel??this.resolveModel();if(this.explicitSources)return{model:e,sources:this.explicitSources,...this.explicitSkillLibrary?{skillLibrary:this.explicitSkillLibrary}:{}};const t=this.explicitAgents??new i(this.config.agents.dataDir).list();this.agentInstallEnabled()&&(this.resolvedCatalog=await this.resolveCatalogFn(this.config,{allowNetwork:!1,...this.config.install.registry?{registry:this.config.install.registry}:{}}));const s=[];for(const i of t)try{s.push(await this.connectAgentSource(i,e))}catch(e){this.onAgentBootstrapIssue?.({agentName:i.skill.name,message:e instanceof Error?e.message:String(e)})}const o=this.resolveSkillLibrary(t);return{model:e,sources:s,...o?{skillLibrary:o}:{}}}async connectAgentSource(e,t){const s=o(e),i=c(this.config,e.skill.name);await this.ensureAgentReady(e,i);const r=await this.clientManager.connect(e.skill.name,s,e.installPath,{samplingModel:t,...this.providerOptions?{samplingProviderOptions:this.providerOptions}:{},...i?{env:i}:{}}),n=(await r.listTools()).tools,l=a(n).map((e,t)=>({tool:e,annotations:A(n[t])}));return{agentName:e.skill.name,client:r,tools:l}}async prepareAgentRefresh(e){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,t.model),o=[...t.sources.filter(e=>e.agentName!==s.agentName),s],r=this.explicitAgents??new i(this.config.agents.dataDir).list(),n=this.resolveSkillLibrary(r);this.ready=Promise.resolve({model:t.model,sources:o,...n?{skillLibrary:n}:{}});const l=n?.list()??[],a=n&&l.length>0?n:void 0,c=this.resolveRuntimeShellProfile(),h=c?this.resolveSessionExecSettings(c):void 0;return{source:s,...a?{skillLibrary:a}:{},systemPrompt:this.composeSystemPrompt(l,o.length,c,h)}}composeSystemPrompt(e,t,s,i){const o=this.resolveAgentOnboardingInfo();return v({skills:e,...s?{shellToolId:"powershell"===s.toolName?b:S,shellHints:s.systemPromptHints()}:{},...i?{sessionExecToolIds:{command:x,poll:w}}:{},agentCount:t,...o?{agentOnboarding:o}:{}})}agentInstallEnabled(){return void 0===this.explicitSources&&void 0===this.explicitAgents}currentCatalog(){return this.resolvedCatalog??p(this.config)}resolveAgentOnboardingInfo(){if(!this.agentInstallEnabled())return;const e=this.currentCatalog();return 0!==e.length?{installToolId:u,catalog: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:h(s),skipBrowserSetup:!0,autoStart:!0,expectedSkillName:s.skillName},{agentsConfig:this.config.agents,installConfig:this.config.install,getStartEnv:e=>c(this.config,e),report:e=>t(P(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,l={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:l};try{return{outcome:l,refresh:await this.prepareAgentRefresh(o)}}catch(e){return t(`接入新 Agent 失败:${e instanceof Error?e.message:String(e)}`),{outcome:l}}}resolveSkillLibrary(e){if(null!==this.explicitSkillLibrary)return void 0!==this.explicitSkillLibrary?this.explicitSkillLibrary:g({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.config.runtime.provider??this.config.llm.defaultProvider}resolveModelName(){return this.config.runtime.model??this.config.llm.defaultModel}async getContextSummary(){const e=await this.ensureReady();return{agentCount:e.sources.length,toolCount:e.sources.reduce((e,t)=>e+t.tools.length,0),skillCount:e.skillLibrary?.list().length??0}}async dispose(){await this.clientManager.disconnectAll()}}
|
|
1
|
+
import{randomUUID as e}from"node:crypto";import{waitForPromiseSettlement as s}from"../bounded-wait.js";import{McpClientManager as t}from"@roll-agent/core/mcp/client-manager";import{createProviderModel 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 l,waitForAgentReady as a}from"@roll-agent/core/registry/process-manager";import{normalizeListedTools as c}from"@roll-agent/core/cli/utils/agent-tools";import{getAgentEnv as h}from"@roll-agent/core/config/helpers";import{catalogPackageSpec as g,getAgentCatalog as p}from"@roll-agent/core/registry/catalog";import{resolveAgentCatalog as m}from"@roll-agent/core/registry/catalog-discovery";import{installAgent as d}from"@roll-agent/core/registry/install";import{createSkillLibrary as u}from"@roll-agent/core/skills/library";import{AGENT_INSTALL_TOOL_ID as f}from"../tool-bridge/agent-install-tool.js";import{AgentSession as v}from"./agent-session.js";import{resolveContextWindow as y}from"./context-window.js";import{buildChatSystemPrompt as S}from"./system-prompt.js";import{BASH_TOOL_ID as b,POWERSHELL_TOOL_ID as w}from"../tool-bridge/bash-tool.js";import{EXEC_COMMAND_ID as x,EXEC_LIST_ID as k,EXEC_POLL_ID as A}from"../tool-bridge/session-exec-tool.js";import{unknownCommandClassifier as P}from"../types/command-classification.js";import{resolveShellProfile as C}from"../bash/profile.js";const M=80,E=6e3;function L(e){if("object"!=typeof e||null===e||!("annotations"in e))return;const s=e.annotations;if("object"!=typeof s||null===s)return;const t={};return"readOnlyHint"in s&&"boolean"==typeof s.readOnlyHint&&(t.readOnlyHint=s.readOnlyHint),"destructiveHint"in s&&"boolean"==typeof s.destructiveHint&&(t.destructiveHint=s.destructiveHint),void 0===t.readOnlyHint&&void 0===t.destructiveHint?void 0:t}function O(e){return"retry"===e.type?`安装遇到网络问题,${Math.round(e.delayMs/1e3)}s 后重试(第 ${e.attempt+1} 次)...`:"warn"===e.type?`警告:${e.message}`:e.message}async function R(e,s,t){"core-managed"===e.runtime.ownership&&(void 0===n(s,e.skill.name)&&l(e,s,t),await a(e))}export class ConversationEngine{config;clientManager;store;policy;maxSteps;providerOptions;ensureAgentReady;debugEvents;explicitAgents;explicitModel;explicitSources;explicitSkillLibrary;onSkillLibraryIssue;onAgentBootstrapIssue;sessionExecEnabled;explicitShellProfile;resolveShellProfileFn;installAgentFn;resolveCatalogFn;ready;refreshChain=Promise.resolve();resolvedCatalog;shellProfileResolution;shellUnsupportedWarned=!1;liveSessions=new Map;disposePromise;closing=!1;constructor(e){this.config=e.config,this.clientManager=e.clientManager??new t,this.store=e.store,this.policy=e.policy,this.maxSteps=e.maxSteps??M,this.providerOptions=e.providerOptions,this.ensureAgentReady=e.ensureAgentReady??((e,s)=>R(e,this.config.agents.dataDir,s)),this.debugEvents=e.debugEvents??!1,this.sessionExecEnabled=e.sessionExecEnabled??!0,this.explicitShellProfile=e.shellProfile,this.resolveShellProfileFn=e.resolveShellProfileFn??C,this.explicitAgents=e.agents,this.explicitModel=e.model,this.explicitSources=e.sources,this.explicitSkillLibrary=e.skillLibrary,this.onSkillLibraryIssue=e.onSkillLibraryIssue,this.onAgentBootstrapIssue=e.onAgentBootstrapIssue,this.installAgentFn=e.installAgentFn??d,this.resolveCatalogFn=e.resolveCatalogFn??m}async createSession(s={}){this.assertAcceptingSessions();const t=await this.ensureReady();this.assertAcceptingSessions();const i=this.store?this.store.createThread({...s.title?{title:s.title}:{},model:this.resolveModelName()}):e();return this.buildSession(t,i,[])}async resumeSession(e){if(this.assertAcceptingSessions(),!this.store)throw new Error("resumeSession requires a ThreadStore");const s=this.liveSessions.get(e);if(void 0!==s)return s;if(!this.store.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const t=await this.ensureReady();this.assertAcceptingSessions();const i=this.liveSessions.get(e);return void 0!==i?i:this.buildSession(t,e,this.store.getMessages(e))}assertAcceptingSessions(){if(this.closing)throw new Error("ConversationEngine is closing")}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:process.env}));const e=this.shellProfileResolution;if(e.supported)return e.profile;if(!this.shellUnsupportedWarned){this.shellUnsupportedWarned=!0;const s="pwsh-version-unsupported"===e.reason?"检测到的 pwsh 版本低于 7":"未检测到 PowerShell 7 (pwsh)";process.stderr.write(`roll chat: ${s},Windows 原生 shell 工具已跳过注册;可运行 winget install Microsoft.PowerShell\n`)}}resolveShellSettings(e){const s=this.config.runtime.shell;return{workdir:process.cwd(),defaultTimeoutMs:s.defaultTimeoutMs,maxTimeoutMs:s.maxTimeoutMs,turnTimeoutMs:this.config.runtime.turnTimeoutMs,maxCaptureBytes:s.maxCaptureBytes,maxModelOutputChars:s.maxModelOutputChars,profile:e}}resolveSessionExecSettings(e){if(!this.sessionExecEnabled)return;const s=this.config.runtime.shell;return s.enabled&&s.session.enabled&&e.supportsSessionExec?{workdir:process.cwd(),profile:e,maxSessions:s.session.maxSessions,defaultYieldMs:s.session.defaultYieldMs,maxOutputTokens:s.session.maxOutputTokens,bufferCapacity:s.maxCaptureBytes}:void 0}buildSession(e,s,t){const i=this.store,o=y(this.resolveModelName(),this.config.runtime.contextWindow),r=e.skillLibrary?.list()??[],n=r.length>0?e.skillLibrary:void 0,l=this.resolveRuntimeShellProfile(),a=l?this.resolveShellSettings(l):void 0,c=l?this.resolveSessionExecSettings(l):void 0,h=l?l.supportsSafeCommandClassification&&this.config.runtime.shell.autoApproveSafe?l:P:void 0,g=this.composeSystemPrompt(r,e.sources.length,l,c),p=this.resolveAgentInstallBinding(),m=new v({id:s,model:e.model,sources:e.sources,systemPrompt:g,...n?{skillLibrary:n}:{},...a?{bash:a}:{},...h?{bashClassifier:h}:{},...c?{bashSession:c}:{},...p?{agentInstall:p}:{},maxSteps:this.maxSteps,compaction:this.config.runtime.compaction,turnTimeoutMs:this.config.runtime.turnTimeoutMs,debugEvents:this.debugEvents,...this.providerOptions?{providerOptions:this.providerOptions}:{},onProviderOptionsChange:e=>this.syncProviderOptions(e),...void 0!==o?{contextWindow:o}:{},...this.policy?{policy:this.policy}:{},initialMessages:t,...i?{onPersist:e=>i.appendMessages(s,e),onReplace:e=>i.replaceMessages(s,e)}:{},onClose:()=>{this.liveSessions.get(s)===m&&this.liveSessions.delete(s)}});return this.liveSessions.set(s,m),m}syncProviderOptions(e){this.providerOptions=e,this.clientManager.setSamplingProviderOptions(e)}ensureReady(){return this.assertAcceptingSessions(),this.ready||(this.ready=this.bootstrap()),this.ready}async bootstrap(){const e=this.explicitModel??this.resolveModel();if(this.explicitSources)return{model:e,sources:this.explicitSources,...this.explicitSkillLibrary?{skillLibrary:this.explicitSkillLibrary}:{}};const s=this.explicitAgents??new o(this.config.agents.dataDir).list();this.agentInstallEnabled()&&(this.resolvedCatalog=await this.resolveCatalogFn(this.config,{allowNetwork:!1,...this.config.install.registry?{registry:this.config.install.registry}:{}}));const t=[];for(const i of s)try{t.push(await this.connectAgentSource(i,e))}catch(e){this.onAgentBootstrapIssue?.({agentName:i.skill.name,message:e instanceof Error?e.message:String(e)})}const i=this.resolveSkillLibrary(s);return{model:e,sources:t,...i?{skillLibrary:i}:{}}}async connectAgentSource(e,s){this.assertAcceptingSessions();const t=r(e),i=h(this.config,e.skill.name);await this.ensureAgentReady(e,i),this.assertAcceptingSessions();const o=await this.clientManager.connect(e.skill.name,t,e.installPath,{samplingModel:s,...this.providerOptions?{samplingProviderOptions:this.providerOptions}:{},...i?{env:i}:{}});this.assertAcceptingSessions();const n=(await o.listTools()).tools;this.assertAcceptingSessions();const l=c(n).map((e,s)=>({tool:e,annotations:L(n[s])}));return{agentName:e.skill.name,client:o,tools:l}}async prepareAgentRefresh(e){this.assertAcceptingSessions();const s=this.refreshChain.then(()=>this.runAgentRefresh(e));return this.refreshChain=s.then(()=>{},()=>{}),s}async runAgentRefresh(e){const s=await this.ensureReady(),t=await this.connectAgentSource(e,s.model),i=[...s.sources.filter(e=>e.agentName!==t.agentName),t],r=this.explicitAgents??new o(this.config.agents.dataDir).list(),n=this.resolveSkillLibrary(r);this.ready=Promise.resolve({model:s.model,sources:i,...n?{skillLibrary:n}:{}});const l=n?.list()??[],a=n&&l.length>0?n:void 0,c=this.resolveRuntimeShellProfile(),h=c?this.resolveSessionExecSettings(c):void 0;return{source:t,...a?{skillLibrary:a}:{},systemPrompt:this.composeSystemPrompt(l,i.length,c,h)}}composeSystemPrompt(e,s,t,i){const o=this.resolveAgentOnboardingInfo();return S({skills:e,...t?{shellToolId:"powershell"===t.toolName?w:b,shellHints:t.systemPromptHints()}:{},...i?{sessionExecToolIds:{command:x,poll:A,list:k}}:{},agentCount:s,...o?{agentOnboarding:o}:{}})}agentInstallEnabled(){return void 0===this.explicitSources&&void 0===this.explicitAgents}currentCatalog(){return this.resolvedCatalog??p(this.config)}resolveAgentOnboardingInfo(){if(!this.agentInstallEnabled())return;const e=this.currentCatalog();return 0!==e.length?{installToolId:f,catalog: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,s)=>this.installCatalogAgent(e,s)}:void 0}async installCatalogAgent(e,s){const t=this.currentCatalog().find(s=>s.shortName===e);if(!t)return{outcome:{ok:!1,message:`未知的官方 Agent 短名: ${e}`}};const i=await this.installAgentFn({packageSpec:g(t),skipBrowserSetup:!0,autoStart:!0,expectedSkillName:t.skillName},{agentsConfig:this.config.agents,installConfig:this.config.install,getStartEnv:e=>h(this.config,e),report:e=>s(O(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,l={ok:!0,agentName:o.skill.name,...r?{version:r}:{},missingEnv:(i.envReport?.missingRequired??[]).map(e=>e.name),...n?{retryCommand:`roll agent install ${t.shortName}`}:{},refreshApplied:!1};if((await this.ensureReady()).sources.some(e=>e.agentName===o.skill.name))return s(`Agent "${o.skill.name}" 本会话已接入旧版本连接,更新需重启 roll chat 生效。`),{outcome:l};try{return{outcome:l,refresh:await this.prepareAgentRefresh(o)}}catch(e){return s(`接入新 Agent 失败:${e instanceof Error?e.message:String(e)}`),{outcome:l}}}resolveSkillLibrary(e){if(null!==this.explicitSkillLibrary)return void 0!==this.explicitSkillLibrary?this.explicitSkillLibrary:u({agents:e,extraDirs:this.config.skills.dirs,...this.onSkillLibraryIssue?{onIssue:this.onSkillLibraryIssue}:{}})}resolveModel(){const e=this.resolveProviderName(),s=this.resolveModelName(),t=this.config.llm.providers[e];if(!t)throw new Error(`LLM provider "${e}" 未配置`);return i(e,s,t.apiKey,t.baseUrl)}resolveProviderName(){return this.config.runtime.provider??this.config.llm.defaultProvider}resolveModelName(){return this.config.runtime.model??this.config.llm.defaultModel}async getContextSummary(){const e=await this.ensureReady();return this.assertAcceptingSessions(),{agentCount:e.sources.length,toolCount:e.sources.reduce((e,s)=>e+s.tools.length,0),skillCount:e.skillLibrary?.list().length??0}}async dispose(){if(void 0!==this.disposePromise)return this.disposePromise;this.closing=!0;const e=Promise.allSettled([...this.ready?[this.ready]:[],this.refreshChain]).then(()=>{});return this.disposePromise=(async()=>{try{await Promise.all([...this.liveSessions.values()].map(e=>e.close()))}finally{this.liveSessions.clear();const t=await s(e,E);t||process.stderr.write(`roll chat: Engine 在 ${String(E)}ms 内未完成在飞初始化,将在其结束后再清理迟到连接\n`),await this.clientManager.disconnectAll(),t||e.then(()=>this.clientManager.disconnectAll()).catch(e=>{process.stderr.write(`roll chat: 迟到 MCP 连接清理失败: ${e instanceof Error?e.message:String(e)}\n`)})}})(),this.disposePromise}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{SKILL_TOOL_ID as n}from"../tool-bridge/skill-tool.js";const o=240,e="你是花卷 Roll 的会话助手,运行在 roll chat 里。";function
|
|
1
|
+
import{SKILL_TOOL_ID as n}from"../tool-bridge/skill-tool.js";const o=240,e="你是花卷 Roll 的会话助手,运行在 roll chat 里。";function l(n){return e+(n?"你通过已注册 Agent 提供的工具(MCP)观察和操作外部世界,并有一个内建 shell 工具可以在本机执行命令。":"你通过已注册 Agent 提供的工具(MCP)观察和操作外部世界;你没有独立的文件系统或 shell,工具就是你的全部执行手段。")}const t=["# 工具使用纪律","- 一切对外部世界的读取和操作都必须通过真实的工具调用完成。绝不虚构工具调用或其结果,也不要用文本描述来代替真正的调用。","- 只有当本会话中出现了对应的成功工具结果,才能说某个操作已完成。没有调用过工具,就如实说明尚未执行。","- 批量任务(例如给多个人回复)必须逐项执行:每一项都真实调用工具、等到结果后再处理下一项;最后按真实结果逐项汇报成功、失败或未执行,不要掩盖失败。","- 工具返回错误时,如实报告错误内容,再决定重试、换方案或向用户求助。不要把失败说成成功,也不要凭空猜测答案。","- 需要确认的工具调用被用户拒绝时,尊重用户的决定,不要换个方式绕过。"].join("\n"),i=["# 任务推进","- 接到任务后持续推进,直到完成或真正被阻塞,不要停在分析或计划阶段。个别工具调用失败不代表任务失败,先尝试自行恢复。","- 除非用户明确只要建议或分析,否则默认用户希望你实际执行。","- 多步任务先用一两句话说明打算怎么做,然后逐步执行,不要把计划本身当成结果。"].join("\n"),s=["# 输出","- 你可以用 thinking/reasoning 做内部推理,但给用户看的最终回复必须写入普通 text 输出通道,不要只写在 reasoning 里。","- 工具调用完成后,在 text 通道给出简洁结论;最终回复不要重复,也不要复述用户输入。","- 像可靠的同事一样汇报:先结论,后必要细节,保持简洁。"].join("\n");function r(n,o){return n.length<=o?n:`${n.slice(0,o-1)}…`}function a(n,e){return["# Skills",`以下是可用的技能说明书(skill)。当任务涉及某个 skill 的领域时,先调用 ${e} 工具(传 name)加载它的完整内容,按其中的流程和约束行事;skill 中的指导优先于你的默认做法。`,n.map(n=>`- ${n.name}: ${r(n.description.replace(/\s+/g," ").trim(),o)}`).join("\n"),"加载结果中的 SKILL_ROOT 是该 skill 的 canonical absolute root。正文里的 scripts/、references/ 等相对路径一律相对 SKILL_ROOT 解析;执行脚本时把 workdir 设为 SKILL_ROOT,不要再搜索 .roll、.claude、.agents 或其它目录猜路径。",`skill 正文提到 references/ 下的文件时,可再次调用 ${e} 并传 reference 参数读取对应文件。`].join("\n")}function c(n){return["# Agent 安装","当前没有任何已注册的子 Agent,对外部系统的操作能力受限。可安装的官方 Agent:",n.catalog.map(n=>`- ${n.shortName}: ${r(n.description.replace(/\s+/g," ").trim(),o)}`).join("\n"),`当用户的需求涉及上述 Agent 的能力时,先说明它的用途并征得用户同意,再调用 ${n.installToolId} 安装(安装会执行 npm install,用户还需在界面上二次确认)。新 Agent 的工具从下一轮对话开始可用。`,"绝不在用户未明确同意的情况下自行安装。"].join("\n")}function g(n,o,e){const l=o?[`- 预计跑几十秒以上的命令(构建、批处理脚本)不要用 ${n}(会被单轮超时杀掉),改用 ${o.command} 后台执行。`,`- ${o.command} 未结束时会返回 session_id;用 ${o.poll}(chars 留空)轮询进度直到拿到退出码,需要中断时 chars 传 "\\u0003"。`,`- 如果一轮因超时或上下文丢失而没有拿到 session_id,下一轮先用 ${o.list} 找回会话,再用 ${o.poll} 继续;用户取消会中断本轮触达的会话,只能查看终态结果,不应宣称仍在运行。`]:["- 预计耗时较长的命令(如构建、脚本)要显式调大 timeout_ms。"];return["# Shell 工具",`- 需要在本机执行命令时调用 ${n};用 workdir 参数指定工作目录,不要在 command 里用 cd。`,...e.map(n=>`- ${n}`),"- 输出会被截断,优先用精确过滤或预览命令,而不是全量 dump 大文件。","- 优先使用只读命令;有副作用或破坏性的命令可能需要用户确认,被拒绝时不要绕过。",...l].join("\n")}export function buildChatSystemPrompt(o={}){const e=o.shellToolId??o.bashToolId,r=[l(void 0!==e),t,i];0===o.agentCount&&void 0!==o.agentOnboarding&&o.agentOnboarding.catalog.length>0&&r.push(c(o.agentOnboarding));const d=o.skills??[];return d.length>0&&r.push(a(d,o.skillToolId??n)),void 0!==e&&r.push(g(e,o.sessionExecToolIds,o.shellHints??[])),r.push(s),r.join("\n\n")}
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export { type CommandClassifier, type CommandClassification, COMMAND_CLASSIFICAT
|
|
|
12
12
|
export { ruleBasedClassifier } from "./bash/classifier/index.ts";
|
|
13
13
|
export { buildPowerShellEncodedCommand, resolveShellProfile, SHELL_PROFILE_IDS, SHELL_TOOL_NAMES, } from "./bash/profile.ts";
|
|
14
14
|
export type { ShellProfile, ShellProfileId, ShellKillOptions, ShellProfileResolutionDeps, ShellProfileResolutionResult, ShellSpawnSpec, ShellToolName, } from "./bash/profile.ts";
|
|
15
|
-
export { buildSessionExecToolset, EXEC_COMMAND_ID, EXEC_POLL_ID, type SessionExecSettings, } from "./tool-bridge/session-exec-tool.ts";
|
|
15
|
+
export { buildSessionExecToolset, EXEC_COMMAND_ID, EXEC_LIST_ID, EXEC_POLL_ID, type SessionExecSettings, } from "./tool-bridge/session-exec-tool.ts";
|
|
16
16
|
export { ToolRegistry } from "./tool-bridge/naming.ts";
|
|
17
17
|
export type { ToolRoute } from "./tool-bridge/naming.ts";
|
|
18
18
|
export { normalizeToolResult } from "./tool-bridge/normalize-result.ts";
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{ConversationEngine}from"./engine/conversation-engine.js";export{AgentSession}from"./engine/agent-session.js";export{resolveContextWindow,lookupContextWindow}from"./engine/context-window.js";export{compactMessages,findTurnBoundaries,SUMMARY_PREFIX,SUMMARY_ACK}from"./engine/compactor.js";export{buildAgentToolset}from"./tool-bridge/build-tools.js";export{COMMAND_CLASSIFICATIONS,unknownCommandClassifier}from"./types/command-classification.js";export{ruleBasedClassifier}from"./bash/classifier/index.js";export{buildPowerShellEncodedCommand,resolveShellProfile,SHELL_PROFILE_IDS,SHELL_TOOL_NAMES}from"./bash/profile.js";export{buildSessionExecToolset,EXEC_COMMAND_ID,EXEC_POLL_ID}from"./tool-bridge/session-exec-tool.js";export{ToolRegistry}from"./tool-bridge/naming.js";export{normalizeToolResult}from"./tool-bridge/normalize-result.js";export{ThreadStore,defaultThreadsDir,expandTilde}from"./store/thread-store.js";export{DefaultToolPolicy}from"./policy/default-policy.js";export{ConfigurableToolPolicy}from"./policy/configurable-policy.js";export{ApprovalGate}from"./approval/approval-gate.js";export{RuntimeServer}from"./server/runtime-server.js";export{createStdioConnection}from"./server/transport/stdio.js";export{RpcMethod,EVENT_NOTIFICATION,isRequest}from"./server/protocol.js";export{SESSION_CANCELLATION_REASONS,USER_CANCELLATION_ABORT_REASON}from"./types/cancellation.js";
|
|
1
|
+
export{ConversationEngine}from"./engine/conversation-engine.js";export{AgentSession}from"./engine/agent-session.js";export{resolveContextWindow,lookupContextWindow}from"./engine/context-window.js";export{compactMessages,findTurnBoundaries,SUMMARY_PREFIX,SUMMARY_ACK}from"./engine/compactor.js";export{buildAgentToolset}from"./tool-bridge/build-tools.js";export{COMMAND_CLASSIFICATIONS,unknownCommandClassifier}from"./types/command-classification.js";export{ruleBasedClassifier}from"./bash/classifier/index.js";export{buildPowerShellEncodedCommand,resolveShellProfile,SHELL_PROFILE_IDS,SHELL_TOOL_NAMES}from"./bash/profile.js";export{buildSessionExecToolset,EXEC_COMMAND_ID,EXEC_LIST_ID,EXEC_POLL_ID}from"./tool-bridge/session-exec-tool.js";export{ToolRegistry}from"./tool-bridge/naming.js";export{normalizeToolResult}from"./tool-bridge/normalize-result.js";export{ThreadStore,defaultThreadsDir,expandTilde}from"./store/thread-store.js";export{DefaultToolPolicy}from"./policy/default-policy.js";export{ConfigurableToolPolicy}from"./policy/configurable-policy.js";export{ApprovalGate}from"./approval/approval-gate.js";export{RuntimeServer}from"./server/runtime-server.js";export{createStdioConnection}from"./server/transport/stdio.js";export{RpcMethod,EVENT_NOTIFICATION,isRequest}from"./server/protocol.js";export{SESSION_CANCELLATION_REASONS,USER_CANCELLATION_ABORT_REASON}from"./types/cancellation.js";
|
|
@@ -5,7 +5,7 @@ export declare class RuntimeServer {
|
|
|
5
5
|
private readonly connection;
|
|
6
6
|
private readonly sessions;
|
|
7
7
|
constructor(engine: ConversationEngine, connection: JsonRpcConnection);
|
|
8
|
-
abortAll(): void
|
|
8
|
+
abortAll(): Promise<void>;
|
|
9
9
|
private handleMessage;
|
|
10
10
|
private requireSession;
|
|
11
11
|
private dispatch;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{EVENT_NOTIFICATION as s,RpcMethod as e,abortParamsSchema as n,approveParamsSchema as t,closeParamsSchema as o,compactParamsSchema as r,createParamsSchema as i,isRequest as a,messagesParamsSchema as c,rejectParamsSchema as d,resumeParamsSchema as p,sendParamsSchema as h}from"./protocol.js";function
|
|
1
|
+
import{EVENT_NOTIFICATION as s,RpcMethod as e,abortParamsSchema as n,approveParamsSchema as t,closeParamsSchema as o,compactParamsSchema as r,createParamsSchema as i,isRequest as a,messagesParamsSchema as c,rejectParamsSchema as d,resumeParamsSchema as p,sendParamsSchema as h}from"./protocol.js";function m(s){return s instanceof Error?s.message:String(s)}export class RuntimeServer{engine;connection;sessions=new Map;constructor(s,e){this.engine=s,this.connection=e,this.connection.onMessage(s=>this.handleMessage(s))}async abortAll(){const s=[...this.sessions.values()];this.sessions.clear(),await Promise.all(s.map(s=>s.close()))}handleMessage(s){a(s)&&this.dispatch(s).then(e=>{this.connection.send({jsonrpc:"2.0",id:s.id,result:e})}).catch(e=>{this.connection.send({jsonrpc:"2.0",id:s.id,error:{code:-32e3,message:m(e)}})})}requireSession(s){const e=this.sessions.get(s);if(!e)throw new Error(`Session "${s}" 不存在`);return e}async dispatch(a){switch(a.method){case e.Create:{const s=i.parse(a.params),e=await this.engine.createSession(s.title?{title:s.title}:{});return this.sessions.set(e.id,e),{sessionId:e.id}}case e.Resume:{const s=p.parse(a.params),e=await this.engine.resumeSession(s.threadId);return this.sessions.set(e.id,e),{sessionId:e.id}}case e.Send:{const e=h.parse(a.params),n=this.requireSession(e.sessionId);for await(const t of n.send(e.input))this.connection.send({jsonrpc:"2.0",method:s,params:{sessionId:e.sessionId,event:t}});return{status:"completed"}}case e.Approve:{const s=t.parse(a.params);return{resolved:this.requireSession(s.sessionId).approve(s.approvalId)}}case e.Reject:{const s=d.parse(a.params);return{resolved:this.requireSession(s.sessionId).reject(s.approvalId,s.reason)}}case e.Abort:{const s=n.parse(a.params);return{ok:!0,cancelled:this.requireSession(s.sessionId).cancel()}}case e.Close:{const s=o.parse(a.params),e=this.requireSession(s.sessionId);return await e.close(),this.sessions.delete(s.sessionId),{closed:!0}}case e.Messages:{const s=c.parse(a.params);return{messages:this.requireSession(s.sessionId).getMessages()}}case e.Compact:{const e=r.parse(a.params),n=this.requireSession(e.sessionId);for await(const t of n.compact("manual"))this.connection.send({jsonrpc:"2.0",method:s,params:{sessionId:e.sessionId,event:t}});return{status:"completed"}}default:throw new Error(`Unknown method: ${a.method}`)}}}
|
|
@@ -7,8 +7,10 @@ import { ToolRegistry } from "./naming.ts";
|
|
|
7
7
|
export declare const EXEC_AGENT_NAME = "roll";
|
|
8
8
|
export declare const EXEC_COMMAND_NAME = "exec_command";
|
|
9
9
|
export declare const EXEC_POLL_NAME = "exec_poll";
|
|
10
|
+
export declare const EXEC_LIST_NAME = "exec_list";
|
|
10
11
|
export declare const EXEC_COMMAND_ID = "roll__exec_command";
|
|
11
12
|
export declare const EXEC_POLL_ID = "roll__exec_poll";
|
|
13
|
+
export declare const EXEC_LIST_ID = "roll__exec_list";
|
|
12
14
|
export interface SessionExecSettings {
|
|
13
15
|
readonly workdir: string;
|
|
14
16
|
readonly defaultYieldMs: number;
|
|
@@ -16,6 +18,7 @@ export interface SessionExecSettings {
|
|
|
16
18
|
}
|
|
17
19
|
export interface SessionExecDeps {
|
|
18
20
|
readonly classifier?: CommandClassifier;
|
|
21
|
+
readonly onSessionTouched?: (sessionId: number) => void;
|
|
19
22
|
}
|
|
20
23
|
declare const execCommandInputSchema: z.ZodObject<{
|
|
21
24
|
command: z.ZodString;
|
|
@@ -46,7 +49,9 @@ declare const execPollInputSchema: z.ZodObject<{
|
|
|
46
49
|
yield_time_ms?: number | undefined;
|
|
47
50
|
chars?: string | undefined;
|
|
48
51
|
}>;
|
|
52
|
+
declare const execListInputSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
|
|
49
53
|
export type ExecCommandInput = z.infer<typeof execCommandInputSchema>;
|
|
50
54
|
export type ExecPollInput = z.infer<typeof execPollInputSchema>;
|
|
55
|
+
export type ExecListInput = z.infer<typeof execListInputSchema>;
|
|
51
56
|
export declare function buildSessionExecToolset(settings: SessionExecSettings, manager: SessionManager, registry: ToolRegistry, ctx: BashToolContext, deps?: SessionExecDeps): ToolSet;
|
|
52
57
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{existsSync as e}from"node:fs";import{resolve as t}from"node:path";import{performance as o}from"node:perf_hooks";import{tool as i}from"ai";import{z as
|
|
1
|
+
import{existsSync as e}from"node:fs";import{resolve as t}from"node:path";import{performance as o}from"node:perf_hooks";import{tool as i}from"ai";import{z as r}from"zod";import{CLASSIFICATION_ANNOTATIONS as n,unknownCommandClassifier as s}from"../types/command-classification.js";import{isWithinWorkdirRoot as a}from"../bash/workdir.js";import{SessionCapError as l}from"../bash/session/session-manager.js";import{pollUntilDeadline as c,throwIfSessionExecAborted as m}from"../bash/session/yield-loop.js";import{SESSION_STATES as d}from"../bash/session/types.js";import{gateToolCall as u}from"./build-tools.js";import{ToolRegistry as p}from"./naming.js";import{isUserCancellationSignal as _}from"../types/cancellation.js";export const EXEC_AGENT_NAME="roll";export const EXEC_COMMAND_NAME="exec_command";export const EXEC_POLL_NAME="exec_poll";export const EXEC_LIST_NAME="exec_list";export const EXEC_COMMAND_ID="roll__exec_command";export const EXEC_POLL_ID="roll__exec_poll";export const EXEC_LIST_ID="roll__exec_list";const x=String.fromCharCode(3),f=250,g=3e4,b=5e3,E=3e5,h=8192,C=4096,S=4,k=r.object({command:r.string().min(1).describe("要在后台会话中执行的 shell 命令(单字符串)"),workdir:r.string().min(1).optional().describe("工作目录绝对路径,默认 roll chat 当前目录,不要用 cd"),yield_time_ms:r.number().int().min(f).max(g).optional().describe("本次等待输出的毫秒数(默认 10000,范围 250-30000);未结束会返回 session_id 供 exec_poll 续查"),max_output_tokens:r.number().int().min(256).max(5e4).optional().describe("本次返回输出的 token 预算")}),w=r.object({session_id:r.number().int().describe("exec_command 返回的会话 id"),chars:r.string().default("").describe('留空表示纯轮询进度;"\\u0003"(Ctrl-C) 表示中断该会话;不支持其它交互输入'),yield_time_ms:r.number().int().min(b).max(E).optional().describe("空轮询等待的毫秒数(默认 10000,范围 5000-300000)")}),y=r.object({});function M(e,t,o){return Math.min(Math.max(e,t),o)}function $(e,t,o){const i=e.emitEvent;if(!i)return;let r=0;return(e,n)=>{if(r>=h)return;r+=1;const s=n.length>C?n.slice(0,C):n;i({type:"tool-output-delta",toolCallId:t,agentName:"roll",toolName:o,stream:e,delta:s})}}async function N(e,t,o){if(e.policy)return u(e,"roll","exec_command",t,o);const i=await e.requestApproval({agentName:"roll",toolName:"exec_command",input:t,reason:"shell 命令需确认"});return i.approved?void 0:{output:"已取消执行"+(i.reason?`: ${i.reason}`:""),isError:!0}}function j(e){const t=["running"===e.kind?`Session: ${String(e.sessionId)} (running)`:`Exit code: ${String(e.exitCode)}`,`Wall time: ${(e.wallTimeMs/1e3).toFixed(1)} s`];e.omitted>0&&t.push(`(省略中间 ${String(e.omitted)} 字符)`),"exited"===e.kind&&e.terminationCause&&t.push(`Termination: ${e.terminationCause}`),"exited"===e.kind&&e.cleanupError&&t.push(`Cleanup error: ${e.cleanupError}`);const o=e.output.length>0?`\n\n${e.output}`:"";return{output:t.join("\n")+o,isError:"exited"===e.kind&&(0!==e.exitCode||e.state===d.cleanupFailed||void 0!==e.terminationCause)}}function v(e){return 0===e.length?{output:"当前没有可恢复的后台会话",isError:!1}:{output:JSON.stringify({sessions:e.map(e=>({session_id:e.sessionId,state:e.state,command:e.commandPreview,workdir:e.workdir,wall_time_s:Number((e.wallTimeMs/1e3).toFixed(1)),...void 0!==e.exitCode?{exit_code:e.exitCode}:{},...e.terminationCause?{termination_cause:e.terminationCause}:{},...e.cleanupError?{cleanup_error:e.cleanupError}:{}}))},null,2),isError:!1}}function T(e,t,o){if(void 0===e)return()=>{};const i=()=>{_(e)&&o.get(t.id)===t&&o.interrupt(t.id).catch(()=>{})};return e.addEventListener("abort",i,{once:!0}),e.aborted&&i(),()=>e.removeEventListener("abort",i)}export function buildSessionExecToolset(r,d,u,p,_={}){const h=u.register("roll","exec_command"),C=u.register("roll","exec_poll"),S=u.register("roll","exec_list"),I=_.classifier??s;return{[h]:i({description:"在后台会话中执行一条命令,等待一段时间后返回输出。若命令未结束会返回 session_id,用 exec_poll 续查进度、读取退出码。适合运行时间超过单轮预算的长脚本。命令继承 roll 进程的环境变量。",inputSchema:k,execute:async(i,s)=>{m(s.abortSignal);const u=t(r.workdir,i.workdir??".");if(!e(u))return{output:`工作目录不存在: ${u}`,isError:!0};const x=M(i.yield_time_ms??r.defaultYieldMs,f,g),b=a(r.workdir,u)?I.classify(i.command,u):"unknown",E=n[b],h={command:i.command,workdir:u,yield_time_ms:x},k=await N(p,h,E);if(k)return k;const w=4*(i.max_output_tokens??r.maxOutputTokens),y=$(p,s.toolCallId,"exec_command");let v;m(s.abortSignal);try{v=d.spawn({command:i.command,workdir:u,...y?{onDelta:y}:{}}),_.onSessionTouched?.(v.id)}catch(e){return{output:e instanceof l?`${e.message};先调用 ${S} 查看会话,再对 cleanup-failed 终态调用 ${C} 领取结果并释放名额,或等待/中断仍在运行的会话后重试`:`无法启动会话: ${String(e)}`,isError:!0}}const A=T(s.abortSignal,v,d),D=await c(v,o.now()+x,w,{...s.abortSignal?{abortSignal:s.abortSignal}:{},...y?{onDelta:y}:{}}).finally(A);return"exited"===D.kind&&d.delete(v.id),j(D)}}),[C]:i({description:'轮询或中断一个 exec_command 会话。session_id 为 exec_command 返回的 id;chars 留空表示纯轮询进度,"\\u0003" 表示发送 Ctrl-C 中断。返回最新输出,进程结束时返回退出码。',inputSchema:w,execute:async(e,t)=>{m(t.abortSignal);const i=d.get(e.session_id);if(!i)return{output:`会话 ${String(e.session_id)} 不存在或已结束`,isError:!0};if(m(t.abortSignal),""!==e.chars&&e.chars!==x)return{output:"pipe 会话不支持交互输入(仅支持空 chars 轮询或 Ctrl-C \\u0003 中断)",isError:!0};_.onSessionTouched?.(i.id),e.chars===x&&await d.interrupt(i.id);const n=$(p,t.toolCallId,"exec_poll"),s=M(e.yield_time_ms??Math.max(r.defaultYieldMs,b),b,E),a=4*r.maxOutputTokens,l=T(t.abortSignal,i,d),u=await c(i,o.now()+s,a,{...t.abortSignal?{abortSignal:t.abortSignal}:{},...n?{onDelta:n}:{}}).finally(l);return"exited"===u.kind&&d.delete(e.session_id),j(u)}}),[S]:i({description:"列出当前 roll chat 进程中的有界近期后台命令会话,包括仍在运行的会话和尚未领取最终结果的已结束会话。用于在轮超时或丢失 session_id 后恢复轮询,不是永久历史。",inputSchema:y,execute:async e=>v(d.list())})}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{tool as e}from"ai";import{z as r}from"zod";import{SKILL_TOOL_ID as n}from"@roll-agent/core/skills/library";export const SKILL_TOOL_AGENT_NAME="roll";export const SKILL_TOOL_NAME="skill";export{n as SKILL_TOOL_ID};const o=6e4,t=r.object({name:r.string().min(1).describe("要加载的 skill 名称(见 system prompt 中的 Skills 目录)"),reference:r.string().
|
|
1
|
+
import{tool as e}from"ai";import{z as r}from"zod";import{SKILL_TOOL_ID as n}from"@roll-agent/core/skills/library";export const SKILL_TOOL_AGENT_NAME="roll";export const SKILL_TOOL_NAME="skill";export{n as SKILL_TOOL_ID};const o=6e4,t=new Set(["",".","./","/","skill.md","./skill.md","/skill.md"]),i=r.object({name:r.string().trim().min(1).describe("要加载的 skill 名称(见 system prompt 中的 Skills 目录)"),reference:r.string().optional().describe("仅在加载 references/ 下的文件时填写,如 references/workflows.md;加载主 SKILL.md 时省略此参数")});function l(e){return e.length<=o?e:`${e.slice(0,o)}\n\n[内容过长已截断,共 ${String(e.length)} 字符]`}function s(e){if(void 0===e)return;const r=e.trim();return t.has(r.toLowerCase())?void 0:r}function c(e,r){return void 0===e?r:[`SKILL_ROOT=${e}`,"这是该 skill 的 canonical absolute root。所有 scripts/、references/ 和其它相对路径必须相对 SKILL_ROOT 解析;执行脚本时将 workdir 设为 SKILL_ROOT,不要搜索其它 skill 目录。","",r].join("\n")}function u(e,r){return{output:`skill "${r}" 不存在。可用 skill: ${e.list().map(e=>e.name).join(", ")}`,isError:!0}}function f(e,r){const n=e.load(r);if(!n)return u(e,r);const o=n.referencePaths.length>0?`\n\n可用 references(传 reference 参数加载):\n${n.referencePaths.map(e=>`- ${e}`).join("\n")}`:"";return{output:c(n.skillRoot,`${l(n.content)}${o}`),isError:!1}}function m(e,r,n){if(!e.list().some(e=>e.name===r))return u(e,r);const o=void 0===e.loadReferenceDocument?void 0:e.loadReferenceDocument(r,n),t=void 0===e.loadReferenceDocument?e.loadReference(r,n):o?.content;return void 0===t?{output:`skill "${r}" 中不存在 reference "${n}"。加载主 SKILL.md 时请省略 reference;加载附加文档时仅支持 skill 目录内 references/ 下的文件。`,isError:!0}:{output:c(o?.skillRoot,l(t)),isError:!1}}export function executeSkillTool(e,r){const n=s(r.reference);return void 0!==n?m(e,r.name,n):f(e,r.name)}export function buildSkillToolset(r,n){const o="function"==typeof r?r:()=>r,t=n.register("roll","skill");return{[t]:e({description:"加载 skill 主说明书时只传 name,不要传 reference;仅加载其 references/ 下的文件时才传 reference。执行涉及某个 skill 领域的任务前,先用它读取流程与约束。",inputSchema:i,execute:e=>executeSkillTool(o(),e)})}}
|
package/dist/types/events.d.ts
CHANGED
|
@@ -76,6 +76,7 @@ export type SessionEvent = {
|
|
|
76
76
|
readonly type: "turn-cancelled";
|
|
77
77
|
readonly reason: SessionCancellationReason;
|
|
78
78
|
readonly message: string;
|
|
79
|
+
readonly execSessionIds?: readonly number[];
|
|
79
80
|
} | {
|
|
80
81
|
readonly type: "error";
|
|
81
82
|
readonly stage: SessionEventStage;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roll-agent/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/node": "^22.0.0",
|
|
36
|
-
"@roll-agent/core": "0.
|
|
36
|
+
"@roll-agent/core": "0.19.0"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
39
|
"build": "rm -rf dist && tsc -p tsconfig.build.json && node ../../scripts/obfuscate.mjs",
|