@roll-agent/runtime 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -199,8 +199,8 @@ declare const compactionRunningWorkSchema: z.ZodReadonly<z.ZodObject<{
|
|
|
199
199
|
workdir: string;
|
|
200
200
|
managerInstanceId: string;
|
|
201
201
|
sessionId: number;
|
|
202
|
-
state: "completed" | "
|
|
203
|
-
recoverability: "
|
|
202
|
+
state: "completed" | "cleanup-failed" | "running" | "draining" | "stopping";
|
|
203
|
+
recoverability: "unavailable" | "live" | "stale";
|
|
204
204
|
commandPreview: string;
|
|
205
205
|
observedAt: string;
|
|
206
206
|
terminationCause?: string | undefined;
|
|
@@ -211,8 +211,8 @@ declare const compactionRunningWorkSchema: z.ZodReadonly<z.ZodObject<{
|
|
|
211
211
|
workdir: string;
|
|
212
212
|
managerInstanceId: string;
|
|
213
213
|
sessionId: number;
|
|
214
|
-
state: "completed" | "
|
|
215
|
-
recoverability: "
|
|
214
|
+
state: "completed" | "cleanup-failed" | "running" | "draining" | "stopping";
|
|
215
|
+
recoverability: "unavailable" | "live" | "stale";
|
|
216
216
|
commandPreview: string;
|
|
217
217
|
observedAt: string;
|
|
218
218
|
terminationCause?: string | undefined;
|
|
@@ -549,8 +549,8 @@ export declare const compactionCheckpointDraftSchema: z.ZodReadonly<z.ZodObject<
|
|
|
549
549
|
workdir: string;
|
|
550
550
|
managerInstanceId: string;
|
|
551
551
|
sessionId: number;
|
|
552
|
-
state: "completed" | "
|
|
553
|
-
recoverability: "
|
|
552
|
+
state: "completed" | "cleanup-failed" | "running" | "draining" | "stopping";
|
|
553
|
+
recoverability: "unavailable" | "live" | "stale";
|
|
554
554
|
commandPreview: string;
|
|
555
555
|
observedAt: string;
|
|
556
556
|
terminationCause?: string | undefined;
|
|
@@ -561,8 +561,8 @@ export declare const compactionCheckpointDraftSchema: z.ZodReadonly<z.ZodObject<
|
|
|
561
561
|
workdir: string;
|
|
562
562
|
managerInstanceId: string;
|
|
563
563
|
sessionId: number;
|
|
564
|
-
state: "completed" | "
|
|
565
|
-
recoverability: "
|
|
564
|
+
state: "completed" | "cleanup-failed" | "running" | "draining" | "stopping";
|
|
565
|
+
recoverability: "unavailable" | "live" | "stale";
|
|
566
566
|
commandPreview: string;
|
|
567
567
|
observedAt: string;
|
|
568
568
|
terminationCause?: string | undefined;
|
|
@@ -698,8 +698,8 @@ export declare const compactionCheckpointDraftSchema: z.ZodReadonly<z.ZodObject<
|
|
|
698
698
|
workdir: string;
|
|
699
699
|
managerInstanceId: string;
|
|
700
700
|
sessionId: number;
|
|
701
|
-
state: "completed" | "
|
|
702
|
-
recoverability: "
|
|
701
|
+
state: "completed" | "cleanup-failed" | "running" | "draining" | "stopping";
|
|
702
|
+
recoverability: "unavailable" | "live" | "stale";
|
|
703
703
|
commandPreview: string;
|
|
704
704
|
observedAt: string;
|
|
705
705
|
terminationCause?: string | undefined;
|
|
@@ -775,8 +775,8 @@ export declare const compactionCheckpointDraftSchema: z.ZodReadonly<z.ZodObject<
|
|
|
775
775
|
workdir: string;
|
|
776
776
|
managerInstanceId: string;
|
|
777
777
|
sessionId: number;
|
|
778
|
-
state: "completed" | "
|
|
779
|
-
recoverability: "
|
|
778
|
+
state: "completed" | "cleanup-failed" | "running" | "draining" | "stopping";
|
|
779
|
+
recoverability: "unavailable" | "live" | "stale";
|
|
780
780
|
commandPreview: string;
|
|
781
781
|
observedAt: string;
|
|
782
782
|
terminationCause?: string | undefined;
|
|
@@ -1033,8 +1033,8 @@ export declare const compactionCheckpointV1Schema: z.ZodReadonly<z.ZodObject<{
|
|
|
1033
1033
|
workdir: string;
|
|
1034
1034
|
managerInstanceId: string;
|
|
1035
1035
|
sessionId: number;
|
|
1036
|
-
state: "completed" | "
|
|
1037
|
-
recoverability: "
|
|
1036
|
+
state: "completed" | "cleanup-failed" | "running" | "draining" | "stopping";
|
|
1037
|
+
recoverability: "unavailable" | "live" | "stale";
|
|
1038
1038
|
commandPreview: string;
|
|
1039
1039
|
observedAt: string;
|
|
1040
1040
|
terminationCause?: string | undefined;
|
|
@@ -1045,8 +1045,8 @@ export declare const compactionCheckpointV1Schema: z.ZodReadonly<z.ZodObject<{
|
|
|
1045
1045
|
workdir: string;
|
|
1046
1046
|
managerInstanceId: string;
|
|
1047
1047
|
sessionId: number;
|
|
1048
|
-
state: "completed" | "
|
|
1049
|
-
recoverability: "
|
|
1048
|
+
state: "completed" | "cleanup-failed" | "running" | "draining" | "stopping";
|
|
1049
|
+
recoverability: "unavailable" | "live" | "stale";
|
|
1050
1050
|
commandPreview: string;
|
|
1051
1051
|
observedAt: string;
|
|
1052
1052
|
terminationCause?: string | undefined;
|
|
@@ -1185,8 +1185,8 @@ export declare const compactionCheckpointV1Schema: z.ZodReadonly<z.ZodObject<{
|
|
|
1185
1185
|
workdir: string;
|
|
1186
1186
|
managerInstanceId: string;
|
|
1187
1187
|
sessionId: number;
|
|
1188
|
-
state: "completed" | "
|
|
1189
|
-
recoverability: "
|
|
1188
|
+
state: "completed" | "cleanup-failed" | "running" | "draining" | "stopping";
|
|
1189
|
+
recoverability: "unavailable" | "live" | "stale";
|
|
1190
1190
|
commandPreview: string;
|
|
1191
1191
|
observedAt: string;
|
|
1192
1192
|
terminationCause?: string | undefined;
|
|
@@ -1278,8 +1278,8 @@ export declare const compactionCheckpointV1Schema: z.ZodReadonly<z.ZodObject<{
|
|
|
1278
1278
|
workdir: string;
|
|
1279
1279
|
managerInstanceId: string;
|
|
1280
1280
|
sessionId: number;
|
|
1281
|
-
state: "completed" | "
|
|
1282
|
-
recoverability: "
|
|
1281
|
+
state: "completed" | "cleanup-failed" | "running" | "draining" | "stopping";
|
|
1282
|
+
recoverability: "unavailable" | "live" | "stale";
|
|
1283
1283
|
commandPreview: string;
|
|
1284
1284
|
observedAt: string;
|
|
1285
1285
|
terminationCause?: string | undefined;
|
|
@@ -2736,8 +2736,8 @@ export declare const compactionCheckpointV2Schema: z.ZodReadonly<z.ZodEffects<z.
|
|
|
2736
2736
|
workdir: string;
|
|
2737
2737
|
managerInstanceId: string;
|
|
2738
2738
|
sessionId: number;
|
|
2739
|
-
state: "completed" | "
|
|
2740
|
-
recoverability: "
|
|
2739
|
+
state: "completed" | "cleanup-failed" | "running" | "draining" | "stopping";
|
|
2740
|
+
recoverability: "unavailable" | "live" | "stale";
|
|
2741
2741
|
commandPreview: string;
|
|
2742
2742
|
observedAt: string;
|
|
2743
2743
|
terminationCause?: string | undefined;
|
|
@@ -2748,8 +2748,8 @@ export declare const compactionCheckpointV2Schema: z.ZodReadonly<z.ZodEffects<z.
|
|
|
2748
2748
|
workdir: string;
|
|
2749
2749
|
managerInstanceId: string;
|
|
2750
2750
|
sessionId: number;
|
|
2751
|
-
state: "completed" | "
|
|
2752
|
-
recoverability: "
|
|
2751
|
+
state: "completed" | "cleanup-failed" | "running" | "draining" | "stopping";
|
|
2752
|
+
recoverability: "unavailable" | "live" | "stale";
|
|
2753
2753
|
commandPreview: string;
|
|
2754
2754
|
observedAt: string;
|
|
2755
2755
|
terminationCause?: string | undefined;
|
|
@@ -2888,8 +2888,8 @@ export declare const compactionCheckpointV2Schema: z.ZodReadonly<z.ZodEffects<z.
|
|
|
2888
2888
|
workdir: string;
|
|
2889
2889
|
managerInstanceId: string;
|
|
2890
2890
|
sessionId: number;
|
|
2891
|
-
state: "completed" | "
|
|
2892
|
-
recoverability: "
|
|
2891
|
+
state: "completed" | "cleanup-failed" | "running" | "draining" | "stopping";
|
|
2892
|
+
recoverability: "unavailable" | "live" | "stale";
|
|
2893
2893
|
commandPreview: string;
|
|
2894
2894
|
observedAt: string;
|
|
2895
2895
|
terminationCause?: string | undefined;
|
|
@@ -3116,8 +3116,8 @@ export declare const compactionCheckpointV2Schema: z.ZodReadonly<z.ZodEffects<z.
|
|
|
3116
3116
|
workdir: string;
|
|
3117
3117
|
managerInstanceId: string;
|
|
3118
3118
|
sessionId: number;
|
|
3119
|
-
state: "completed" | "
|
|
3120
|
-
recoverability: "
|
|
3119
|
+
state: "completed" | "cleanup-failed" | "running" | "draining" | "stopping";
|
|
3120
|
+
recoverability: "unavailable" | "live" | "stale";
|
|
3121
3121
|
commandPreview: string;
|
|
3122
3122
|
observedAt: string;
|
|
3123
3123
|
terminationCause?: string | undefined;
|
|
@@ -3344,8 +3344,8 @@ export declare const compactionCheckpointV2Schema: z.ZodReadonly<z.ZodEffects<z.
|
|
|
3344
3344
|
workdir: string;
|
|
3345
3345
|
managerInstanceId: string;
|
|
3346
3346
|
sessionId: number;
|
|
3347
|
-
state: "completed" | "
|
|
3348
|
-
recoverability: "
|
|
3347
|
+
state: "completed" | "cleanup-failed" | "running" | "draining" | "stopping";
|
|
3348
|
+
recoverability: "unavailable" | "live" | "stale";
|
|
3349
3349
|
commandPreview: string;
|
|
3350
3350
|
observedAt: string;
|
|
3351
3351
|
terminationCause?: string | undefined;
|
|
@@ -3572,8 +3572,8 @@ export declare const compactionCheckpointV2Schema: z.ZodReadonly<z.ZodEffects<z.
|
|
|
3572
3572
|
workdir: string;
|
|
3573
3573
|
managerInstanceId: string;
|
|
3574
3574
|
sessionId: number;
|
|
3575
|
-
state: "completed" | "
|
|
3576
|
-
recoverability: "
|
|
3575
|
+
state: "completed" | "cleanup-failed" | "running" | "draining" | "stopping";
|
|
3576
|
+
recoverability: "unavailable" | "live" | "stale";
|
|
3577
3577
|
commandPreview: string;
|
|
3578
3578
|
observedAt: string;
|
|
3579
3579
|
terminationCause?: string | undefined;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { LanguageModelV4, LanguageModelV4CallOptions, SharedV4ProviderOptions } from "@ai-sdk/provider";
|
|
2
2
|
import { McpClientManager } from "@roll-agent/core/mcp/client-manager";
|
|
3
|
+
import { type AgentUsageLease } from "@roll-agent/core/registry/agent-usage-lease";
|
|
3
4
|
import { resolveAgentCatalog } from "@roll-agent/core/registry/catalog-discovery";
|
|
4
5
|
import { installAgent } from "@roll-agent/core/registry/install";
|
|
5
6
|
import type { RollConfig } from "@roll-agent/core/config/schema";
|
|
@@ -12,6 +13,7 @@ import { AgentSession, type AgentSessionOptions, type SessionAgentRefresh } from
|
|
|
12
13
|
import { type CapabilityHostMode, type CapabilityVcsSnapshot } from "./capability-manifest.ts";
|
|
13
14
|
import { resolveShellProfile, type ShellProfile } from "../bash/profile.ts";
|
|
14
15
|
export type EnsureAgentReady = (agent: RegisteredAgent, env: Readonly<Record<string, string>> | undefined) => Promise<void>;
|
|
16
|
+
export type AcquireAgentUsage = (agent: RegisteredAgent, env: Readonly<Record<string, string>> | undefined) => Promise<AgentUsageLease | undefined>;
|
|
15
17
|
export interface ConversationEngineOptions {
|
|
16
18
|
readonly config: RollConfig;
|
|
17
19
|
readonly agents?: readonly RegisteredAgent[];
|
|
@@ -25,6 +27,7 @@ export interface ConversationEngineOptions {
|
|
|
25
27
|
readonly structuredOutputProviderOptions?: SharedV4ProviderOptions;
|
|
26
28
|
readonly structuredOutputReasoning?: NonNullable<LanguageModelV4CallOptions["reasoning"]>;
|
|
27
29
|
readonly ensureAgentReady?: EnsureAgentReady;
|
|
30
|
+
readonly acquireAgentUsage?: AcquireAgentUsage;
|
|
28
31
|
readonly debugEvents?: boolean;
|
|
29
32
|
readonly onAgentBootstrapIssue?: (issue: AgentBootstrapIssue) => void;
|
|
30
33
|
readonly skillLibrary?: SkillLibrary | null;
|
|
@@ -60,7 +63,7 @@ export declare class ConversationEngine {
|
|
|
60
63
|
private providerOptions;
|
|
61
64
|
private readonly structuredOutputProviderOptions;
|
|
62
65
|
private readonly structuredOutputReasoning;
|
|
63
|
-
private readonly
|
|
66
|
+
private readonly acquireAgentUsage;
|
|
64
67
|
private readonly debugEvents;
|
|
65
68
|
private readonly explicitAgents;
|
|
66
69
|
private readonly explicitModel;
|
|
@@ -83,9 +86,11 @@ export declare class ConversationEngine {
|
|
|
83
86
|
private shellProfileResolution;
|
|
84
87
|
private shellUnsupportedWarned;
|
|
85
88
|
private readonly liveSessions;
|
|
89
|
+
private readonly agentUsageLeases;
|
|
86
90
|
private disposePromise;
|
|
87
91
|
private closing;
|
|
88
92
|
constructor(options: ConversationEngineOptions);
|
|
93
|
+
private acquireDefaultAgentUsage;
|
|
89
94
|
createSession(input?: CreateSessionInput): Promise<AgentSession>;
|
|
90
95
|
resumeSession(threadId: string): Promise<AgentSession>;
|
|
91
96
|
private assertAcceptingSessions;
|
|
@@ -110,5 +115,6 @@ export declare class ConversationEngine {
|
|
|
110
115
|
private resolveProviderName;
|
|
111
116
|
private resolveModelName;
|
|
112
117
|
getContextSummary(): Promise<EngineContextSummary>;
|
|
118
|
+
private releaseAgentUsageLeases;
|
|
113
119
|
dispose(): Promise<void>;
|
|
114
120
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomUUID as e}from"node:crypto";import{waitForPromiseSettlement as t}from"../bounded-wait.js";import{McpClientManager as s}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 u,getAgentCatalog as p}from"@roll-agent/core/registry/catalog";import{resolveAgentCatalog as d}from"@roll-agent/core/registry/catalog-discovery";import{installAgent as m}from"@roll-agent/core/registry/install";import{createSkillLibrary as g}from"@roll-agent/core/skills/library";import{ROLL_RESOURCE_HINTS_META_KEY as f}from"../tool-bridge/build-tools.js";import{TOOL_RESOURCE_ACCESS_MODES as v,TOOL_RESOURCE_HINT_KINDS as y}from"../tool-bridge/tool-execution-coordinator.js";import{AgentSession as S}from"./agent-session.js";import{resolveContextWindow as b}from"./context-window.js";import{CAPABILITY_HOST_MODES as x}from"./capability-manifest.js";import{}from"../tool-bridge/bash-tool.js";import{unknownCommandClassifier as C}from"../types/command-classification.js";import{resolveShellProfile as w}from"../bash/profile.js";import{inspectGitVcsContext as k}from"./vcs-context.js";const A=80,O=6e3;function P(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 M(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}function E(e){return Object.values(y).some(t=>t===e)}function R(e){return Object.values(v).some(t=>t===e)}function L(e){return{hints:void 0,issue:e}}function T(e){if(!M(e)||!M(e._meta))return{hints:void 0,issue:void 0};if(!Object.hasOwn(e._meta,f))return{hints:void 0,issue:void 0};const t=e._meta[f];if(!Array.isArray(t))return L("必须是数组");const s=[];for(const[e,i]of t.entries()){if(!M(i)||"string"!=typeof i.field||0===i.field.trim().length||"string"!=typeof i.kind||!E(i.kind)||void 0!==i.mode&&("string"!=typeof i.mode||!R(i.mode))||void 0!==i.namespace&&"string"!=typeof i.namespace||i.kind===y.custom&&("string"!=typeof i.namespace||0===i.namespace.trim().length))return L(`第 ${String(e+1)} 项字段无效`);const t=i.kind===y.custom&&"string"==typeof i.namespace?i.namespace.trim():void 0;s.push({field:i.field.trim(),kind:i.kind,...void 0!==i.mode?{mode:i.mode}:{},...void 0!==t?{namespace:t}:{}})}return{hints:s.length>0?s:void 0,issue:void 0}}function I(e){return"retry"===e.type?`安装遇到网络问题,${Math.round(e.delayMs/1e3)}s 后重试(第 ${e.attempt+1} 次)...`:"warn"===e.type?`警告:${e.message}`:e.message}async function N(e,t,s){"core-managed"===e.runtime.ownership&&(void 0===n(t,e.skill.name)&&l(e,t,s),await a(e))}export class ConversationEngine{config;clientManager;store;policy;maxSteps;providerOptions;structuredOutputProviderOptions;structuredOutputReasoning;ensureAgentReady;debugEvents;explicitAgents;explicitModel;explicitSources;explicitSkillLibrary;onSkillLibraryIssue;onAgentBootstrapIssue;hostMode;resolveDynamicCapabilityContext;sessionExecEnabled;explicitShellProfile;resolveShellProfileFn;installAgentFn;resolveCatalogFn;inspectVcsContext;shellEnv;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 s,this.store=e.store,this.policy=e.policy,this.maxSteps=e.maxSteps??A,this.providerOptions=e.providerOptions,this.structuredOutputProviderOptions=e.structuredOutputProviderOptions,this.structuredOutputReasoning=e.structuredOutputReasoning,this.ensureAgentReady=e.ensureAgentReady??((e,t)=>N(e,this.config.agents.dataDir,t)),this.debugEvents=e.debugEvents??!1,this.hostMode=e.hostMode??x.embedded,this.resolveDynamicCapabilityContext=e.resolveDynamicCapabilityContext,this.sessionExecEnabled=e.sessionExecEnabled??!0,this.explicitShellProfile=e.shellProfile,this.resolveShellProfileFn=e.resolveShellProfileFn??w,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??m,this.resolveCatalogFn=e.resolveCatalogFn??d,this.inspectVcsContext=e.inspectVcsContext??k,this.shellEnv={...e.shellEnv??process.env}}async createSession(t={}){this.assertAcceptingSessions();const s=await this.ensureReady();this.assertAcceptingSessions();const 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.assertAcceptingSessions(),!this.store)throw new Error("resumeSession requires a ThreadStore");const t=this.liveSessions.get(e);if(void 0!==t)return t;if(!this.store.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const s=await this.ensureReady();this.assertAcceptingSessions();const i=this.liveSessions.get(e);if(void 0!==i)return i;const o=this.store.loadSessionState(e);return this.buildSession(s,e,o.messages,o.checkpoint)}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:this.shellEnv}));const e=this.shellProfileResolution;if(e.supported)return e.profile;if(!this.shellUnsupportedWarned){this.shellUnsupportedWarned=!0;const t="pwsh-version-unsupported"===e.reason?"检测到的 pwsh 版本低于 7":"未检测到 PowerShell 7 (pwsh)";process.stderr.write(`roll chat: ${t},Windows 原生 shell 工具已跳过注册;可运行 winget install Microsoft.PowerShell\n`)}}resolveShellSettings(e){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,env:this.shellEnv}}resolveSessionExecSettings(e){if(!this.sessionExecEnabled)return;const t=this.config.runtime.shell;return t.enabled&&t.session.enabled&&e.supportsSessionExec?{workdir:process.cwd(),profile:e,maxSessions:t.session.maxSessions,defaultYieldMs:t.session.defaultYieldMs,maxOutputTokens:t.session.maxOutputTokens,bufferCapacity:t.maxCaptureBytes,env:this.shellEnv}:void 0}buildSession(e,t,s,i){const o=this.store,r=b(this.resolveModelName(),this.config.runtime.contextWindow),n=(e.skillLibrary?.list()??[]).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:C:void 0,u=this.resolveAgentInstallBinding(),p=this.composeCapabilityContext(e.sources.length,l),d=new S({id:t,model:e.model,sources:e.sources,capabilityContext:p,resolveDynamicCapabilityContext:async e=>{const[t,s]=await Promise.all([this.inspectVcsContext(p.cwd),this.resolveDynamicCapabilityContext?.(e)??{}]),i=s.vcs??t;return{...s.ruleIds?{ruleIds:s.ruleIds}:{},...i?{vcs:i}:{}}},...n?{skillLibrary:n}:{},...a?{bash:a}:{},...h?{bashClassifier:h}:{},...c?{bashSession:c}:{},...u?{agentInstall:u}:{},maxSteps:this.maxSteps,compaction:this.config.runtime.compaction,turnTimeoutMs:this.config.runtime.turnTimeoutMs,debugEvents:this.debugEvents,...this.providerOptions?{providerOptions:this.providerOptions}:{},...this.structuredOutputProviderOptions?{structuredOutputProviderOptions:this.structuredOutputProviderOptions}:{},...this.structuredOutputReasoning?{structuredOutputReasoning:this.structuredOutputReasoning}:{},onProviderOptionsChange:e=>this.syncProviderOptions(e),...void 0!==r?{contextWindow:r}:{},...this.policy?{policy:this.policy}:{},initialMessages:s,...i?{initialCheckpoint:i}:{},...o?{onPersist:e=>o.appendMessages(t,e),onReplace:e=>o.replaceMessages(t,e),onToolExecution:e=>o.appendToolExecution(t,e),listToolExecutions:e=>o.listToolExecutions(t,e),getToolExecution:e=>o.getToolExecution(t,e),listTranscriptMessages:e=>o.listTranscriptMessages(t,e),commitCompaction:e=>o.commitCompaction(t,e),readCheckpointTranscript:e=>o.readCheckpointTranscript(t,e)}:{},onClose:()=>{this.liveSessions.get(t)===d&&this.liveSessions.delete(t)}});return this.liveSessions.set(t,d),d}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 t=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 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 i=this.resolveSkillLibrary(t);return{model:e,sources:s,...i?{skillLibrary:i}:{}}}async connectAgentSource(e,t){this.assertAcceptingSessions();const s=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,s,e.installPath,{samplingModel:t,...this.providerOptions?{samplingProviderOptions:this.providerOptions}:{},...i?{env:i}:{}});this.assertAcceptingSessions();const n=(await o.listTools()).tools;this.assertAcceptingSessions();const l=c(n).map((t,s)=>{const i=T(n[s]);return void 0!==i.issue&&this.onAgentBootstrapIssue?.({agentName:e.skill.name,message:`Tool "${t.name}" 的 ${f} 无效(${i.issue}),已回退 Agent 级资源锁`}),{tool:t,annotations:P(n[s]),...i.hints?{resourceHints:i.hints}:{}}});return{agentName:e.skill.name,client:o,tools:l,...e.source?{agentSource:e.source.type}:{},transport:s.type,runtimeOwnership:e.runtime.ownership,..."stdio"===s.type?{resourceBaseDir:e.installPath}:{}}}async prepareAgentRefresh(e){this.assertAcceptingSessions();const t=this.refreshChain.then(()=>this.runAgentRefresh(e));return this.refreshChain=t.then(()=>{},()=>{}),t}async runAgentRefresh(e){const t=await this.ensureReady(),s=await this.connectAgentSource(e,t.model),i=[...t.sources.filter(e=>e.agentName!==s.agentName),s],r=this.explicitAgents??new o(this.config.agents.dataDir).list(),n=this.resolveSkillLibrary(r);this.ready=Promise.resolve({model:t.model,sources:i,...n?{skillLibrary:n}:{}});const l=n?.list()??[],a=n&&l.length>0?n:void 0,c=this.resolveRuntimeShellProfile();return{source:s,...a?{skillLibrary:a}:{},capabilityContext:this.composeCapabilityContext(i.length,c)}}composeCapabilityContext(e,t){const s=this.resolveAgentOnboardingInfo();return{profile:t?.toolName??"no-shell",hostMode:this.hostMode,cwd:process.cwd(),platform:process.platform,...t?{shellHints:t.systemPromptHints()}:{},agentCount:e,...s?{agentOnboardingCatalog:s}:{}}}agentInstallEnabled(){return void 0===this.explicitSources&&void 0===this.explicitAgents}currentCatalog(){return this.resolvedCatalog??p(this.config)}resolveAgentOnboardingInfo(){if(!this.agentInstallEnabled())return;const e=this.currentCatalog();return 0!==e.length?e.map(e=>({shortName:e.shortName,description:e.description})):void 0}resolveAgentInstallBinding(){if(!this.agentInstallEnabled())return;const e=this.currentCatalog();return 0!==e.length?{catalog:e.map(e=>({shortName:e.shortName,description:e.description})),install:(e,t)=>this.installCatalogAgent(e,t)}:void 0}async installCatalogAgent(e,t){const s=this.currentCatalog().find(t=>t.shortName===e);if(!s)return{outcome:{ok:!1,message:`未知的官方 Agent 短名: ${e}`}};const i=await this.installAgentFn({packageSpec:u(s),skipBrowserSetup:!0,autoStart:!0,expectedSkillName:s.skillName},{agentsConfig:this.config.agents,installConfig:this.config.install,getStartEnv:e=>h(this.config,e),report:e=>t(I(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(),s=this.config.llm.providers[e];if(!s)throw new Error(`LLM provider "${e}" 未配置`);return i(e,t,s.apiKey,s.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,t)=>e+t.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 s=await t(e,O);s||process.stderr.write(`roll chat: Engine 在 ${String(O)}ms 内未完成在飞初始化,将在其结束后再清理迟到连接\n`),await this.clientManager.disconnectAll(),s||e.then(()=>this.clientManager.disconnectAll()).catch(e=>{process.stderr.write(`roll chat: 迟到 MCP 连接清理失败: ${e instanceof Error?e.message:String(e)}\n`)})}})(),this.disposePromise}}
|
|
1
|
+
import{randomUUID as e}from"node:crypto";import{waitForPromiseSettlement as t}from"../bounded-wait.js";import{McpClientManager as s}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 a,waitForAgentReady as l}from"@roll-agent/core/registry/process-manager";import{acquireAgentUsageLease as c}from"@roll-agent/core/registry/agent-usage-lease";import{normalizeListedTools as h}from"@roll-agent/core/cli/utils/agent-tools";import{getAgentEnv as u}from"@roll-agent/core/config/helpers";import{catalogPackageSpec as d,getAgentCatalog as p}from"@roll-agent/core/registry/catalog";import{resolveAgentCatalog as g}from"@roll-agent/core/registry/catalog-discovery";import{installAgent as m}from"@roll-agent/core/registry/install";import{createSkillLibrary as f}from"@roll-agent/core/skills/library";import{ROLL_RESOURCE_HINTS_META_KEY as v}from"../tool-bridge/build-tools.js";import{TOOL_RESOURCE_ACCESS_MODES as y,TOOL_RESOURCE_HINT_KINDS as S}from"../tool-bridge/tool-execution-coordinator.js";import{AgentSession as b}from"./agent-session.js";import{resolveContextWindow as w}from"./context-window.js";import{CAPABILITY_HOST_MODES as x}from"./capability-manifest.js";import{}from"../tool-bridge/bash-tool.js";import{unknownCommandClassifier as C}from"../types/command-classification.js";import{resolveShellProfile as A}from"../bash/profile.js";import{inspectGitVcsContext as k}from"./vcs-context.js";const M=80,O=6e3;function P(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 E(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}function L(e){return Object.values(S).some(t=>t===e)}function R(e){return Object.values(y).some(t=>t===e)}function T(e){return{hints:void 0,issue:e}}function I(e){if(!E(e)||!E(e._meta))return{hints:void 0,issue:void 0};if(!Object.hasOwn(e._meta,v))return{hints:void 0,issue:void 0};const t=e._meta[v];if(!Array.isArray(t))return T("必须是数组");const s=[];for(const[e,i]of t.entries()){if(!E(i)||"string"!=typeof i.field||0===i.field.trim().length||"string"!=typeof i.kind||!L(i.kind)||void 0!==i.mode&&("string"!=typeof i.mode||!R(i.mode))||void 0!==i.namespace&&"string"!=typeof i.namespace||i.kind===S.custom&&("string"!=typeof i.namespace||0===i.namespace.trim().length))return T(`第 ${String(e+1)} 项字段无效`);const t=i.kind===S.custom&&"string"==typeof i.namespace?i.namespace.trim():void 0;s.push({field:i.field.trim(),kind:i.kind,...void 0!==i.mode?{mode:i.mode}:{},...void 0!==t?{namespace:t}:{}})}return{hints:s.length>0?s:void 0,issue:void 0}}function N(e){return"retry"===e.type?`安装遇到网络问题,${Math.round(e.delayMs/1e3)}s 后重试(第 ${e.attempt+1} 次)...`:"warn"===e.type?`警告:${e.message}`:e.message}async function U(e,t,s){"core-managed"===e.runtime.ownership&&(void 0===n(t,e.skill.name)&&a(e,t,s),await l(e))}export class ConversationEngine{config;clientManager;store;policy;maxSteps;providerOptions;structuredOutputProviderOptions;structuredOutputReasoning;acquireAgentUsage;debugEvents;explicitAgents;explicitModel;explicitSources;explicitSkillLibrary;onSkillLibraryIssue;onAgentBootstrapIssue;hostMode;resolveDynamicCapabilityContext;sessionExecEnabled;explicitShellProfile;resolveShellProfileFn;installAgentFn;resolveCatalogFn;inspectVcsContext;shellEnv;ready;refreshChain=Promise.resolve();resolvedCatalog;shellProfileResolution;shellUnsupportedWarned=!1;liveSessions=new Map;agentUsageLeases=new Map;disposePromise;closing=!1;constructor(e){this.config=e.config,this.clientManager=e.clientManager??new s,this.store=e.store,this.policy=e.policy,this.maxSteps=e.maxSteps??M,this.providerOptions=e.providerOptions,this.structuredOutputProviderOptions=e.structuredOutputProviderOptions,this.structuredOutputReasoning=e.structuredOutputReasoning,this.acquireAgentUsage=e.acquireAgentUsage??(e.ensureAgentReady?async(t,s)=>{await(e.ensureAgentReady?.(t,s))}:(e,t)=>this.acquireDefaultAgentUsage(e,t)),this.debugEvents=e.debugEvents??!1,this.hostMode=e.hostMode??x.embedded,this.resolveDynamicCapabilityContext=e.resolveDynamicCapabilityContext,this.sessionExecEnabled=e.sessionExecEnabled??!0,this.explicitShellProfile=e.shellProfile,this.resolveShellProfileFn=e.resolveShellProfileFn??A,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??m,this.resolveCatalogFn=e.resolveCatalogFn??g,this.inspectVcsContext=e.inspectVcsContext??k,this.shellEnv={...e.shellEnv??process.env}}async acquireDefaultAgentUsage(e,t){const s=await c(e,this.config.agents.dataDir,t,{holderKind:"chat",startIfStopped:!0});if(void 0!==s)return s;await U(e,this.config.agents.dataDir,t)}async createSession(t={}){this.assertAcceptingSessions();const s=await this.ensureReady();this.assertAcceptingSessions();const 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.assertAcceptingSessions(),!this.store)throw new Error("resumeSession requires a ThreadStore");const t=this.liveSessions.get(e);if(void 0!==t)return t;if(!this.store.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const s=await this.ensureReady();this.assertAcceptingSessions();const i=this.liveSessions.get(e);if(void 0!==i)return i;const o=this.store.loadSessionState(e);return this.buildSession(s,e,o.messages,o.checkpoint)}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:this.shellEnv}));const e=this.shellProfileResolution;if(e.supported)return e.profile;if(!this.shellUnsupportedWarned){this.shellUnsupportedWarned=!0;const t="pwsh-version-unsupported"===e.reason?"检测到的 pwsh 版本低于 7":"未检测到 PowerShell 7 (pwsh)";process.stderr.write(`roll chat: ${t},Windows 原生 shell 工具已跳过注册;可运行 winget install Microsoft.PowerShell\n`)}}resolveShellSettings(e){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,env:this.shellEnv}}resolveSessionExecSettings(e){if(!this.sessionExecEnabled)return;const t=this.config.runtime.shell;return t.enabled&&t.session.enabled&&e.supportsSessionExec?{workdir:process.cwd(),profile:e,maxSessions:t.session.maxSessions,defaultYieldMs:t.session.defaultYieldMs,maxOutputTokens:t.session.maxOutputTokens,bufferCapacity:t.maxCaptureBytes,env:this.shellEnv}:void 0}buildSession(e,t,s,i){const o=this.store,r=w(this.resolveModelName(),this.config.runtime.contextWindow),n=(e.skillLibrary?.list()??[]).length>0?e.skillLibrary:void 0,a=this.resolveRuntimeShellProfile(),l=a?this.resolveShellSettings(a):void 0,c=a?this.resolveSessionExecSettings(a):void 0,h=a?a.supportsSafeCommandClassification&&this.config.runtime.shell.autoApproveSafe?a:C:void 0,u=this.resolveAgentInstallBinding(),d=this.composeCapabilityContext(e.sources.length,a),p=new b({id:t,model:e.model,sources:e.sources,capabilityContext:d,resolveDynamicCapabilityContext:async e=>{const[t,s]=await Promise.all([this.inspectVcsContext(d.cwd),this.resolveDynamicCapabilityContext?.(e)??{}]),i=s.vcs??t;return{...s.ruleIds?{ruleIds:s.ruleIds}:{},...i?{vcs:i}:{}}},...n?{skillLibrary:n}:{},...l?{bash:l}:{},...h?{bashClassifier:h}:{},...c?{bashSession:c}:{},...u?{agentInstall:u}:{},maxSteps:this.maxSteps,compaction:this.config.runtime.compaction,turnTimeoutMs:this.config.runtime.turnTimeoutMs,debugEvents:this.debugEvents,...this.providerOptions?{providerOptions:this.providerOptions}:{},...this.structuredOutputProviderOptions?{structuredOutputProviderOptions:this.structuredOutputProviderOptions}:{},...this.structuredOutputReasoning?{structuredOutputReasoning:this.structuredOutputReasoning}:{},onProviderOptionsChange:e=>this.syncProviderOptions(e),...void 0!==r?{contextWindow:r}:{},...this.policy?{policy:this.policy}:{},initialMessages:s,...i?{initialCheckpoint:i}:{},...o?{onPersist:e=>o.appendMessages(t,e),onReplace:e=>o.replaceMessages(t,e),onToolExecution:e=>o.appendToolExecution(t,e),listToolExecutions:e=>o.listToolExecutions(t,e),getToolExecution:e=>o.getToolExecution(t,e),listTranscriptMessages:e=>o.listTranscriptMessages(t,e),commitCompaction:e=>o.commitCompaction(t,e),readCheckpointTranscript:e=>o.readCheckpointTranscript(t,e)}:{},onClose:()=>{this.liveSessions.get(t)===p&&this.liveSessions.delete(t)}});return this.liveSessions.set(t,p),p}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 t=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 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 i=this.resolveSkillLibrary(t);return{model:e,sources:s,...i?{skillLibrary:i}:{}}}async connectAgentSource(e,t){this.assertAcceptingSessions();const s=r(e),i=u(this.config,e.skill.name),o=void 0===this.agentUsageLeases.get(e.skill.name)?await this.acquireAgentUsage(e,i):void 0;try{this.assertAcceptingSessions();const r=await this.clientManager.connect(e.skill.name,s,e.installPath,{samplingModel:t,...this.providerOptions?{samplingProviderOptions:this.providerOptions}:{},...i?{env:i}:{}});this.assertAcceptingSessions();const n=(await r.listTools()).tools;this.assertAcceptingSessions();const a=h(n).map((t,s)=>{const i=I(n[s]);return void 0!==i.issue&&this.onAgentBootstrapIssue?.({agentName:e.skill.name,message:`Tool "${t.name}" 的 ${v} 无效(${i.issue}),已回退 Agent 级资源锁`}),{tool:t,annotations:P(n[s]),...i.hints?{resourceHints:i.hints}:{}}});return void 0!==o&&this.agentUsageLeases.set(e.skill.name,o),{agentName:e.skill.name,client:r,tools:a,...e.source?{agentSource:e.source.type}:{},transport:s.type,runtimeOwnership:e.runtime.ownership,..."stdio"===s.type?{resourceBaseDir:e.installPath}:{}}}catch(t){throw void 0!==o&&(await this.clientManager.disconnect(e.skill.name).catch(()=>{}),await o.release().catch(()=>{})),t}}async prepareAgentRefresh(e){this.assertAcceptingSessions();const t=this.refreshChain.then(()=>this.runAgentRefresh(e));return this.refreshChain=t.then(()=>{},()=>{}),t}async runAgentRefresh(e){const t=await this.ensureReady(),s=await this.connectAgentSource(e,t.model),i=[...t.sources.filter(e=>e.agentName!==s.agentName),s],r=this.explicitAgents??new o(this.config.agents.dataDir).list(),n=this.resolveSkillLibrary(r);this.ready=Promise.resolve({model:t.model,sources:i,...n?{skillLibrary:n}:{}});const a=n?.list()??[],l=n&&a.length>0?n:void 0,c=this.resolveRuntimeShellProfile();return{source:s,...l?{skillLibrary:l}:{},capabilityContext:this.composeCapabilityContext(i.length,c)}}composeCapabilityContext(e,t){const s=this.resolveAgentOnboardingInfo();return{profile:t?.toolName??"no-shell",hostMode:this.hostMode,cwd:process.cwd(),platform:process.platform,...t?{shellHints:t.systemPromptHints()}:{},agentCount:e,...s?{agentOnboardingCatalog:s}:{}}}agentInstallEnabled(){return void 0===this.explicitSources&&void 0===this.explicitAgents}currentCatalog(){return this.resolvedCatalog??p(this.config)}resolveAgentOnboardingInfo(){if(!this.agentInstallEnabled())return;const e=this.currentCatalog();return 0!==e.length?e.map(e=>({shortName:e.shortName,description:e.description})):void 0}resolveAgentInstallBinding(){if(!this.agentInstallEnabled())return;const e=this.currentCatalog();return 0!==e.length?{catalog:e.map(e=>({shortName:e.shortName,description:e.description})),install:(e,t)=>this.installCatalogAgent(e,t)}:void 0}async installCatalogAgent(e,t){const s=this.currentCatalog().find(t=>t.shortName===e);if(!s)return{outcome:{ok:!1,message:`未知的官方 Agent 短名: ${e}`}};const i=await this.installAgentFn({packageSpec:d(s),skipBrowserSetup:!0,autoStart:!1,expectedSkillName:s.skillName},{agentsConfig:this.config.agents,installConfig:this.config.install,getStartEnv:e=>u(this.config,e),report:e=>t(N(e))});if(!i.ok)return{outcome:{ok:!1,message:i.message,...i.retryCommand?{retryCommand:i.retryCommand}:{}}};const o=i.agent,r="installed-package"===o.source?.type?o.source.installedVersion:void 0,n="core-managed"===o.runtime.ownership&&void 0!==o.runtime.setup?.playwright,a={ok:!0,agentName:o.skill.name,...r?{version:r}:{},missingEnv:(i.envReport?.missingRequired??[]).map(e=>e.name),...n?{retryCommand:`roll agent install ${s.shortName}`}:{},refreshApplied:!1};if((await this.ensureReady()).sources.some(e=>e.agentName===o.skill.name))return t(`Agent "${o.skill.name}" 本会话已接入旧版本连接,更新需重启 roll chat 生效。`),{outcome:a};try{return{outcome:a,refresh:await this.prepareAgentRefresh(o)}}catch(e){return t(`接入新 Agent 失败:${e instanceof Error?e.message:String(e)}`),{outcome:a}}}resolveSkillLibrary(e){if(null!==this.explicitSkillLibrary)return void 0!==this.explicitSkillLibrary?this.explicitSkillLibrary:f({agents:e,extraDirs:this.config.skills.dirs,...this.onSkillLibraryIssue?{onIssue:this.onSkillLibraryIssue}:{}})}resolveModel(){const e=this.resolveProviderName(),t=this.resolveModelName(),s=this.config.llm.providers[e];if(!s)throw new Error(`LLM provider "${e}" 未配置`);return i(e,t,s.apiKey,s.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,t)=>e+t.tools.length,0),skillCount:e.skillLibrary?.list().length??0}}async releaseAgentUsageLeases(){const e=[...this.agentUsageLeases.entries()],t=await Promise.allSettled(e.map(async([e,t])=>{try{await t.release()}finally{this.agentUsageLeases.get(e)===t&&this.agentUsageLeases.delete(e)}}));for(const e of t)"rejected"===e.status&&process.stderr.write(`roll chat: Agent 使用租约释放失败: ${e.reason instanceof Error?e.reason.message:String(e.reason)}\n`)}async dispose(){if(void 0!==this.disposePromise)return this.disposePromise;this.closing=!0;const e=Promise.allSettled([...this.ready?[this.ready]:[],this.refreshChain]).then(()=>{});return this.disposePromise=(async()=>{let s={failed:!1};try{await Promise.all([...this.liveSessions.values()].map(e=>e.close()))}catch(e){s={failed:!0,error:e}}this.liveSessions.clear();const i=await t(e,O);i||process.stderr.write(`roll chat: Engine 在 ${String(O)}ms 内未完成在飞初始化,将在其结束后再清理迟到连接\n`);let o={failed:!1};try{await this.clientManager.disconnectAll()}catch(e){o={failed:!0,error:e}}if(await this.releaseAgentUsageLeases(),i||e.then(async()=>{try{await this.clientManager.disconnectAll()}finally{await this.releaseAgentUsageLeases()}}).catch(e=>{process.stderr.write(`roll chat: 迟到 MCP/租约清理失败: ${e instanceof Error?e.message:String(e)}\n`)}),s.failed&&o.failed)throw new AggregateError([s.error,o.error],"ConversationEngine 关闭 session 与 MCP 连接均失败。");if(s.failed)throw s.error;if(o.failed)throw o.error})(),this.disposePromise}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{chmodSync as e,existsSync as t,mkdirSync as n}from"node:fs";import{homedir as r}from"node:os";import{resolve as s}from"node:path";import{randomUUID as o}from"node:crypto";import{DatabaseSync as a}from"node:sqlite";import{modelMessageSchema as i}from"ai";import{parsePersistedToolExecutionRecord as c,prepareToolExecutionRecordForPersistence as d,redactSecretText as E}from"../tool-bridge/tool-execution-record.js";import{COMPACTION_CHECKPOINT_VERSION as h,COMPACTION_TRANSCRIPT_COMPLETENESS as T,TRANSCRIPT_MESSAGE_PROVENANCES as p,UnsupportedCompactionCheckpointVersionError as u,archivedTranscriptMessageSchema as _,createCompactionCheckpoint as l,createCompactionCheckpointDraft as m,parseCompactionCheckpoint as g}from"../engine/compaction-checkpoint.js";import{legacyCompactionTranscriptFragmentsSchema as S}from"../engine/compaction-semantic-state.js";import{createLegacyCompactionTranscriptMessage as N}from"../engine/compactor.js";import{ACTIVE_TOOL_PROTOCOL_REPAIR_STATUS as R,repairActiveToolProtocol as O}from"../engine/tool-protocol-repair.js";import{sanitizePersistedExplicitSkillCheckpoint as I}from"../engine/explicit-skill-context.js";const L=4,C=100,x=500,A=50,f=500,q=100;export const TOOL_EXECUTION_RETENTION_POLICY={maxBytesPerThread:16777216,maxRecordsPerThread:2e3,maxAgeMs:2592e6};export const TRANSCRIPT_ENTRY_KINDS=["message","tool_execution"];export function expandTilde(e){return e.startsWith("~/")?s(r(),e.slice(2)):e}export function defaultThreadsDir(){return s(r(),".roll-agent","threads")}function b(e){return e.prepare("PRAGMA foreign_key_list(messages)").all().some(e=>"threads"===e.table&&"thread_id"===e.from&&"id"===e.to&&"CASCADE"===e.on_delete.toUpperCase())}function M(e,t){return"valid"===e.status?e:"fallback"===e.status?{status:"fallback",reason:e.reason,...void 0!==t?{lastValidCheckpointId:t.id}:{}}:{status:"skipped",...void 0!==t?{lastValidCheckpointId:t.id}:{}}}function w(e){if(Number.isInteger(e.schema_version)&&e.schema_version>h)throw new u(e.schema_version);if(!Number.isInteger(e.schema_version))throw new Error("Invalid persisted compaction checkpoint schema version");const t=g(JSON.parse(e.checkpoint_json));if(t.version!==e.schema_version)throw new Error(`Compaction checkpoint schema version mismatch: row v${String(e.schema_version)}, payload v${String(t.version)}`);return t}function v(e){return{id:e.id,title:e.title??void 0,model:e.model??void 0,createdAt:e.created_at,updatedAt:e.updated_at}}export class ThreadStore{db;constructor(r=defaultThreadsDir()){const o=expandTilde(r);t(o)||n(o,{recursive:!0,mode:448}),"win32"!==process.platform&&e(o,448);const i=s(o,"threads.db");if(this.db=new a(i),"win32"!==process.platform)try{e(i,384)}catch(e){throw this.db.close(),e}this.init()}init(){this.db.exec("PRAGMA foreign_keys = ON; PRAGMA secure_delete = ON;");const e=this.db.prepare("PRAGMA user_version").get();if(e.user_version>4)throw new Error(`ThreadStore schema v${String(e.user_version)} 高于当前支持的 v${String(4)}`);this.db.exec("BEGIN IMMEDIATE");try{this.db.exec("CREATE TABLE IF NOT EXISTS threads (\n id TEXT PRIMARY KEY,\n title TEXT,\n model TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n CREATE TABLE IF NOT EXISTS messages (\n thread_id TEXT NOT NULL,\n idx INTEGER NOT NULL,\n role TEXT NOT NULL,\n content_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, idx),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );"),b(this.db)||this.db.exec("CREATE TABLE messages_with_thread_fk (\n thread_id TEXT NOT NULL,\n idx INTEGER NOT NULL,\n role TEXT NOT NULL,\n content_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, idx),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n INSERT INTO messages_with_thread_fk\n (thread_id, idx, role, content_json, created_at)\n SELECT messages.thread_id, messages.idx, messages.role,\n messages.content_json, messages.created_at\n FROM messages\n INNER JOIN threads ON threads.id = messages.thread_id;\n DROP TABLE messages;\n ALTER TABLE messages_with_thread_fk RENAME TO messages;"),this.db.exec("CREATE TABLE IF NOT EXISTS tool_executions (\n thread_id TEXT NOT NULL,\n sequence INTEGER NOT NULL,\n id TEXT NOT NULL,\n tool_call_id TEXT NOT NULL,\n agent_name TEXT NOT NULL,\n tool_name TEXT NOT NULL,\n record_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, id),\n UNIQUE (thread_id, sequence),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE INDEX IF NOT EXISTS idx_tool_executions_thread_call\n ON tool_executions(thread_id, tool_call_id, sequence);\n CREATE TABLE IF NOT EXISTS thread_tool_execution_state (\n thread_id TEXT PRIMARY KEY,\n next_sequence INTEGER NOT NULL CHECK (next_sequence >= 0),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS transcript_messages (\n thread_id TEXT NOT NULL,\n sequence INTEGER NOT NULL,\n role TEXT NOT NULL,\n message_json TEXT NOT NULL,\n provenance TEXT NOT NULL CHECK (provenance IN ('native', 'legacy_snapshot')),\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, sequence),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS thread_transcript_state (\n thread_id TEXT PRIMARY KEY,\n completeness TEXT NOT NULL CHECK (completeness IN ('complete', 'legacy_snapshot')),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS compaction_checkpoints (\n id TEXT PRIMARY KEY,\n thread_id TEXT NOT NULL,\n generation INTEGER NOT NULL,\n schema_version INTEGER NOT NULL,\n message_from_sequence INTEGER NOT NULL,\n message_through_sequence INTEGER NOT NULL,\n tool_from_sequence INTEGER NOT NULL,\n tool_through_sequence INTEGER NOT NULL,\n checkpoint_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n UNIQUE (thread_id, generation),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE INDEX IF NOT EXISTS idx_compaction_checkpoints_thread_generation\n ON compaction_checkpoints(thread_id, generation DESC);"),this.db.exec("INSERT OR IGNORE INTO thread_tool_execution_state (thread_id, next_sequence)\n SELECT id, 0 FROM threads;\n UPDATE thread_tool_execution_state\n SET next_sequence = MAX(\n next_sequence,\n COALESCE((\n SELECT MAX(tool_executions.sequence) + 1\n FROM tool_executions\n WHERE tool_executions.thread_id = thread_tool_execution_state.thread_id\n ), 0)\n );"),e.user_version<3&&this.db.exec("INSERT OR IGNORE INTO thread_transcript_state (thread_id, completeness)\n SELECT id, 'legacy_snapshot' FROM threads;\n INSERT OR IGNORE INTO transcript_messages\n (thread_id, sequence, role, message_json, provenance, created_at)\n SELECT thread_id, idx, role, content_json, 'legacy_snapshot', created_at\n FROM messages;"),e.user_version<4&&(this.migrateToolExecutionPersistenceInTransaction(),this.migrateExplicitSkillCheckpointPersistenceInTransaction()),this.enforceAllToolExecutionRetentionInTransaction((new Date).toISOString()),this.db.exec(`PRAGMA user_version = ${String(4)};`),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}migrateToolExecutionPersistenceInTransaction(){const e=this.db.prepare("SELECT rowid AS storage_rowid, record_json\n FROM tool_executions\n WHERE rowid > ?\n ORDER BY rowid ASC\n LIMIT 100"),t=this.db.prepare("UPDATE tool_executions\n SET tool_call_id = ?, agent_name = ?, tool_name = ?, record_json = ?, created_at = ?\n WHERE rowid = ?");let n=0;for(;;){const r=e.all(n);if(0===r.length)break;for(const e of r){const r=c(JSON.parse(e.record_json));t.run(r.toolCallId,r.agentName,r.toolName,JSON.stringify(r),r.createdAt,e.storage_rowid),n=e.storage_rowid}}}enforceAllToolExecutionRetentionInTransaction(e){const t=this.db.prepare("SELECT DISTINCT thread_id AS id FROM tool_executions").all();for(const{id:n}of t)this.enforceToolExecutionRetentionInTransaction(n,e)}migrateExplicitSkillCheckpointPersistenceInTransaction(){const e=[{table:"messages",column:"content_json"},{table:"transcript_messages",column:"message_json"}];for(const t of e){const e=this.db.prepare(`SELECT rowid AS storage_rowid, ${t.column} AS message_json\n FROM ${t.table}\n WHERE ${t.column} LIKE '%"modelUserContent"%'`).all(),n=this.db.prepare(`UPDATE ${t.table} SET ${t.column} = ? WHERE rowid = ?`);for(const t of e){const e=i.parse(JSON.parse(t.message_json)),r=I(e);n.run(JSON.stringify(r),t.storage_rowid)}}}createThread(e={}){const t=e.id??o(),n=(new Date).toISOString();this.db.exec("BEGIN IMMEDIATE");try{return this.db.prepare("INSERT INTO threads (id, title, model, created_at, updated_at) VALUES (?, ?, ?, ?, ?)").run(t,e.title??null,e.model??null,n,n),this.db.prepare("INSERT INTO thread_transcript_state (thread_id, completeness) VALUES (?, ?)").run(t,T[0]),this.db.prepare("INSERT INTO thread_tool_execution_state (thread_id, next_sequence) VALUES (?, 0)").run(t),this.db.exec("COMMIT"),t}catch(e){throw this.db.exec("ROLLBACK"),e}}hasThread(e){return void 0!==this.db.prepare("SELECT 1 FROM threads WHERE id = ?").get(e)}getThread(e){const t=this.db.prepare("SELECT * FROM threads WHERE id = ?").get(e);return t?v(t):void 0}listThreads(){return this.db.prepare("SELECT * FROM threads ORDER BY updated_at DESC, rowid DESC").all().map(v)}countMessages(e){return this.db.prepare("SELECT COUNT(*) AS n FROM messages WHERE thread_id = ?").get(e).n}updateTitle(e,t){this.db.prepare("UPDATE threads SET title = ? WHERE id = ?").run(t,e)}deleteThread(e){this.db.prepare("DELETE FROM threads WHERE id = ?").run(e)}appendMessages(e,t){if(0===t.length)return;if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=t.map(e=>I(i.parse(e))),r=(new Date).toISOString(),s=this.db.prepare("INSERT INTO messages (thread_id, idx, role, content_json, created_at) VALUES (?, ?, ?, ?, ?)"),o=this.db.prepare("INSERT INTO transcript_messages\n (thread_id, sequence, role, message_json, provenance, created_at)\n VALUES (?, ?, ?, ?, ?, ?)");this.db.exec("BEGIN IMMEDIATE");try{const t=this.db.prepare("SELECT COALESCE(MAX(idx), -1) AS maxIdx FROM messages WHERE thread_id = ?").get(e),a=this.db.prepare("SELECT COALESCE(MAX(sequence), -1) AS maxSequence FROM transcript_messages WHERE thread_id = ?").get(e);let i=t.maxIdx+1,c=a.maxSequence+1;for(const t of n){const n=JSON.stringify(t);s.run(e,i,t.role,n,r),o.run(e,c,t.role,n,p[0],r),i+=1,c+=1}this.db.prepare("UPDATE threads SET updated_at = ? WHERE id = ?").run(r,e),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}replaceMessages(e,t){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=t.map(e=>i.parse(e)),r=(new Date).toISOString();this.db.exec("BEGIN IMMEDIATE");try{this.replaceMessagesInTransaction(e,n,r),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}getMessages(e){return this.db.prepare("SELECT content_json FROM messages WHERE thread_id = ? ORDER BY idx ASC").all(e).map(e=>i.parse(JSON.parse(e.content_json)))}replaceMessagesInTransaction(e,t,n){const r=this.db.prepare("INSERT INTO messages (thread_id, idx, role, content_json, created_at) VALUES (?, ?, ?, ?, ?)");this.db.prepare("DELETE FROM messages WHERE thread_id = ?").run(e),t.forEach((t,s)=>{const o=I(t);r.run(e,s,o.role,JSON.stringify(o),n)}),this.db.prepare("UPDATE threads SET updated_at = ? WHERE id = ?").run(n,e)}appendToolExecution(e,t){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=d(t),r=JSON.stringify(n),s=(new Date).toISOString();this.db.exec("BEGIN IMMEDIATE");try{const t=this.db.prepare("SELECT next_sequence FROM thread_tool_execution_state WHERE thread_id = ?").get(e);if(void 0===t)throw new Error(`Thread "${e}" 缺少 Tool execution sequence state`);const o=t.next_sequence;return this.db.prepare("INSERT INTO tool_executions\n (thread_id, sequence, id, tool_call_id, agent_name, tool_name, record_json, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)").run(e,o,n.id,n.toolCallId,n.agentName,n.toolName,r,n.createdAt),this.db.prepare("UPDATE thread_tool_execution_state SET next_sequence = ? WHERE thread_id = ?").run(o+1,e),this.enforceToolExecutionRetentionInTransaction(e,s),this.db.prepare("UPDATE threads SET updated_at = ? WHERE id = ?").run(s,e),this.db.exec("COMMIT"),o}catch(e){throw this.db.exec("ROLLBACK"),e}}enforceToolExecutionRetentionInTransaction(e,t){const n=new Date(Date.parse(t)-TOOL_EXECUTION_RETENTION_POLICY.maxAgeMs).toISOString();let r=!1;const s=this.db.prepare("DELETE FROM tool_executions\n WHERE thread_id = ?\n AND created_at < ?").run(e,n);r=Number(s.changes)>0;const o=this.db.prepare("SELECT sequence\n FROM (\n SELECT sequence,\n ROW_NUMBER() OVER (ORDER BY sequence DESC) AS retained_rank,\n SUM(length(CAST(record_json AS BLOB))) OVER (\n ORDER BY sequence DESC\n ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n ) AS retained_bytes\n FROM tool_executions\n WHERE thread_id = ?\n )\n WHERE retained_rank > ? OR retained_bytes > ?").all(e,TOOL_EXECUTION_RETENTION_POLICY.maxRecordsPerThread,TOOL_EXECUTION_RETENTION_POLICY.maxBytesPerThread);if(o.length>0){const t=this.db.prepare("DELETE FROM tool_executions WHERE thread_id = ? AND sequence = ?");for(const n of o)t.run(e,n.sequence);r=!0}r&&this.db.prepare("INSERT INTO thread_transcript_state (thread_id, completeness)\n VALUES (?, 'legacy_snapshot')\n ON CONFLICT(thread_id) DO UPDATE SET completeness = 'legacy_snapshot'").run(e)}listToolExecutions(e,t={}){const n=t.afterSequence??-1,r=t.throughSequence??Number.MAX_SAFE_INTEGER,s=t.limit??C;if(!Number.isInteger(n)||n<-1)throw new Error("afterSequence 必须是大于等于 -1 的整数");if(!Number.isInteger(s)||s<1||s>x)throw new Error(`limit 必须是 1-${String(x)} 之间的整数`);if(!Number.isInteger(r)||r<-1)throw new Error("throughSequence 必须是大于等于 -1 的整数");if(r<=n)return[];return this.db.prepare("SELECT sequence, record_json\n FROM tool_executions\n WHERE thread_id = ?\n AND sequence > ?\n AND sequence <= ?\n AND (? IS NULL OR tool_call_id = ?)\n ORDER BY sequence ASC\n LIMIT ?").all(e,n,r,t.toolCallId??null,t.toolCallId??null,s).map(e=>({...c(JSON.parse(e.record_json)),sequence:e.sequence}))}getToolExecution(e,t){const n=this.db.prepare("SELECT sequence, record_json FROM tool_executions WHERE thread_id = ? AND id = ?").get(e,t);if(n)return{...c(JSON.parse(n.record_json)),sequence:n.sequence}}getTranscriptCompleteness(e){const t=this.db.prepare("SELECT completeness FROM thread_transcript_state WHERE thread_id = ?").get(e);return t?.completeness===T[0]?T[0]:T[1]}listTranscriptMessages(e,t={}){const n=t.afterSequence??-1,r=t.throughSequence??Number.MAX_SAFE_INTEGER,s=t.limit??A;if(this.validateTranscriptRange(n,r,s,f),r<=n)return[];return this.db.prepare("SELECT sequence, provenance, created_at, message_json\n FROM transcript_messages\n WHERE thread_id = ?\n AND sequence > ?\n AND sequence <= ?\n ORDER BY sequence ASC\n LIMIT ?").all(e,n,r,s).map(e=>_.parse({sequence:e.sequence,provenance:e.provenance,createdAt:e.created_at,message:JSON.parse(e.message_json)}))}getLatestCheckpoint(e){const t=this.db.prepare("SELECT schema_version, checkpoint_json\n FROM compaction_checkpoints\n WHERE thread_id = ?\n ORDER BY generation DESC").all(e);for(const n of t)try{return this.withCurrentTranscriptCompleteness(e,w(n))}catch(e){if(e instanceof u)throw e}}getLatestSummaryCheckpoint(e){const t=this.db.prepare("SELECT schema_version, checkpoint_json\n FROM compaction_checkpoints\n WHERE thread_id = ?\n ORDER BY generation DESC").all(e);for(const n of t)try{const t=w(n);if("valid"===t.summary.status)return this.withCurrentTranscriptCompleteness(e,t)}catch(e){if(e instanceof u)throw e}}getCheckpoint(e,t){const n=this.db.prepare("SELECT schema_version, checkpoint_json\n FROM compaction_checkpoints\n WHERE thread_id = ? AND id = ?").get(e,t);if(void 0!==n)try{return this.withCurrentTranscriptCompleteness(e,w(n))}catch(e){if(e instanceof u)throw e;return}}loadSessionState(e){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);return{messages:O(this.getMessages(e)).messages,checkpoint:this.getLatestCheckpoint(e),transcriptCompleteness:this.getTranscriptCompleteness(e)}}commitCompaction(e,t){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=t.messages.map(e=>i.parse(e)),r=t.expectedActiveMessages.map(e=>i.parse(e)),s=O(n);if(s.status!==R.valid){const e=[...s.removedToolCallIds,...s.removedToolResultIds].filter((e,t,n)=>n.indexOf(e)===t).sort((e,t)=>e.localeCompare(t));throw new Error(`Compaction active projection contains malformed Tool protocol: ${e.join(", ")}`)}const o=m(t.draft),a=S.parse(t.legacySnapshotTranscriptFragments??[]),c=S.parse(a.map(e=>E(e))).map(e=>i.parse(N(e))),d=(new Date).toISOString();this.db.exec("BEGIN IMMEDIATE");try{const s=O(this.getMessages(e)).messages;if(JSON.stringify(s)!==JSON.stringify(r))throw new Error("Compaction active projection changed after the draft snapshot was captured");const a=this.getLatestCheckpoint(e);if(a?.id!==t.expectedLatestCheckpointId)throw new Error("Compaction checkpoint ancestry changed after the draft snapshot was captured");const i=this.getLatestSummaryCheckpoint(e),E=M(o.summary,i),h=this.getTranscriptCompleteness(e),u=m({...o,summary:E,...h===T[1]&&"valid"===o.toolState.integrityStatus?{toolState:{...o.toolState,integrityStatus:"sanitized"}}:{}}),_=this.db.prepare("SELECT COALESCE(MAX(generation), 0) AS maxGeneration\n FROM compaction_checkpoints\n WHERE thread_id = ?").get(e),g=this.maxTranscriptMessageSequence(e),S=this.maxToolExecutionSequence(e),N=t.evidenceWatermarks.transcriptMessagesThroughSequence,R=t.evidenceWatermarks.toolExecutionsThroughSequence,I=a?.transcript.messages.throughSequence??-1,L=a?.transcript.toolExecutions.throughSequence??-1,C=2===a?.version?a.semanticEvidence.messagesThroughSequence:-1,x=2===a?.version?a.semanticEvidence.toolExecutionsThroughSequence:-1;if(c.length>0&&1!==a?.version)throw new Error("Legacy snapshot transcript archive requires a V1 parent checkpoint");if(!Number.isInteger(N)||N<I||N>g)throw new Error(`Compaction transcript message watermark ${String(N)} is outside the available range ${String(I)}-${String(g)}`);if(!Number.isInteger(t.semanticEvidenceWatermarks.messagesThroughSequence)||t.semanticEvidenceWatermarks.messagesThroughSequence<C||t.semanticEvidenceWatermarks.messagesThroughSequence>N)throw new Error(`Compaction semantic message watermark ${String(t.semanticEvidenceWatermarks.messagesThroughSequence)} is outside the available range ${String(C)}-${String(N)}`);if(!Number.isInteger(t.semanticEvidenceWatermarks.toolExecutionsThroughSequence)||t.semanticEvidenceWatermarks.toolExecutionsThroughSequence<x||t.semanticEvidenceWatermarks.toolExecutionsThroughSequence>R)throw new Error(`Compaction semantic Tool watermark ${String(t.semanticEvidenceWatermarks.toolExecutionsThroughSequence)} is outside the available range ${String(x)}-${String(R)}`);if(!Number.isInteger(R)||R<L||R>S)throw new Error(`Compaction tool execution watermark ${String(R)} is outside the available range ${String(L)}-${String(S)}`);let A=N;if(c.length>0){if(N!==g)throw new Error("Legacy snapshot transcript archive requires the latest message watermark");const t=this.db.prepare("INSERT INTO transcript_messages\n (thread_id, sequence, role, message_json, provenance, created_at)\n VALUES (?, ?, ?, ?, ?, ?)");let n=g+1;for(const r of c)t.run(e,n,r.role,JSON.stringify(r),p[1],d),n+=1;A=n-1}const f={messages:{fromSequenceExclusive:I,throughSequence:A},toolExecutions:{fromSequenceExclusive:L,throughSequence:R},completeness:h},q=l({draft:u,semanticState:t.semanticState,semanticEvidence:t.semanticEvidenceWatermarks,generation:_.maxGeneration+1,transcript:f,...void 0!==a?{previousCheckpointId:a.id}:{},createdAt:d});return this.db.prepare("INSERT INTO compaction_checkpoints\n (id, thread_id, generation, schema_version,\n message_from_sequence, message_through_sequence,\n tool_from_sequence, tool_through_sequence,\n checkpoint_json, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(q.id,e,q.generation,q.version,q.transcript.messages.fromSequenceExclusive,q.transcript.messages.throughSequence,q.transcript.toolExecutions.fromSequenceExclusive,q.transcript.toolExecutions.throughSequence,JSON.stringify(q),q.createdAt),this.replaceMessagesInTransaction(e,n,d),this.db.exec("COMMIT"),q}catch(e){throw this.db.exec("ROLLBACK"),e}}readCheckpointTranscript(e,t){if("message"!==t.kind&&"tool_execution"!==t.kind)throw new Error("kind 必须是 message 或 tool_execution");const n=this.getCheckpoint(e,t.checkpointId);if(void 0===n)throw new Error("checkpoint 不存在、已损坏或不属于当前 thread");const r=t.limit??A,s="message"===t.kind?n.transcript.messages:n.transcript.toolExecutions,o=Math.max(t.afterSequence??s.fromSequenceExclusive,s.fromSequenceExclusive);this.validateTranscriptRange(o,s.throughSequence,r,q);const a="message"===t.kind?this.listTranscriptMessages(e,{afterSequence:o,throughSequence:s.throughSequence,limit:r+1}).map(e=>({kind:"message",...e})):this.listToolExecutions(e,{afterSequence:o,throughSequence:s.throughSequence,limit:Math.min(r+1,x)}).map(e=>({kind:"tool_execution",...e})),i=a.length>r,c=i?a.slice(0,r):a,d=c.at(-1)?.sequence;return{checkpointId:n.id,kind:t.kind,entries:c,...i&&void 0!==d?{nextAfterSequence:d}:{},...void 0!==n.previousCheckpointId?{previousCheckpointId:n.previousCheckpointId}:{},completeness:n.transcript.completeness}}maxTranscriptMessageSequence(e){return this.db.prepare("SELECT COALESCE(MAX(sequence), -1) AS maxSequence FROM transcript_messages WHERE thread_id = ?").get(e).maxSequence}maxToolExecutionSequence(e){const t=this.db.prepare("SELECT next_sequence FROM thread_tool_execution_state WHERE thread_id = ?").get(e);return(t?.next_sequence??0)-1}withCurrentTranscriptCompleteness(e,t){const n=T[1],r=t.transcript.completeness===n||this.getTranscriptCompleteness(e)===n?n:T[0],s=r===n&&"valid"===t.toolState.integrityStatus?{...t.toolState,integrityStatus:"sanitized"}:t.toolState;return t.transcript.completeness===r&&t.toolState===s?t:{...t,toolState:s,transcript:{...t.transcript,completeness:r}}}validateTranscriptRange(e,t,n,r){if(!Number.isInteger(e)||e<-1)throw new Error("afterSequence 必须是大于等于 -1 的整数");if(!Number.isInteger(t)||t<-1)throw new Error("throughSequence 必须是大于等于 -1 的整数");if(!Number.isInteger(n)||n<1||n>r)throw new Error(`limit 必须是 1-${String(r)} 之间的整数`)}close(){this.db.close()}}
|
|
1
|
+
import{chmodSync as e,existsSync as t,mkdirSync as n}from"node:fs";import{homedir as r}from"node:os";import{resolve as s}from"node:path";import{randomUUID as o}from"node:crypto";import{DatabaseSync as a}from"node:sqlite";import{modelMessageSchema as i}from"ai";import{parsePersistedToolExecutionRecord as c,prepareToolExecutionRecordForPersistence as d,redactSecretText as E}from"../tool-bridge/tool-execution-record.js";import{COMPACTION_CHECKPOINT_VERSION as h,COMPACTION_TRANSCRIPT_COMPLETENESS as T,TRANSCRIPT_MESSAGE_PROVENANCES as p,UnsupportedCompactionCheckpointVersionError as u,archivedTranscriptMessageSchema as _,createCompactionCheckpoint as l,createCompactionCheckpointDraft as m,parseCompactionCheckpoint as g}from"../engine/compaction-checkpoint.js";import{legacyCompactionTranscriptFragmentsSchema as S}from"../engine/compaction-semantic-state.js";import{createLegacyCompactionTranscriptMessage as N}from"../engine/compactor.js";import{ACTIVE_TOOL_PROTOCOL_REPAIR_STATUS as R,repairActiveToolProtocol as O}from"../engine/tool-protocol-repair.js";import{sanitizePersistedExplicitSkillCheckpoint as I}from"../engine/explicit-skill-context.js";const L=4,C=100,x=500,A=50,f=500,b=100,q=15e3;export const TOOL_EXECUTION_RETENTION_POLICY={maxBytesPerThread:16777216,maxRecordsPerThread:2e3,maxAgeMs:2592e6};export const TRANSCRIPT_ENTRY_KINDS=["message","tool_execution"];export function expandTilde(e){return e.startsWith("~/")?s(r(),e.slice(2)):e}export function defaultThreadsDir(){return s(r(),".roll-agent","threads")}function M(e){return e.prepare("PRAGMA foreign_key_list(messages)").all().some(e=>"threads"===e.table&&"thread_id"===e.from&&"id"===e.to&&"CASCADE"===e.on_delete.toUpperCase())}function w(e,t){return"valid"===e.status?e:"fallback"===e.status?{status:"fallback",reason:e.reason,...void 0!==t?{lastValidCheckpointId:t.id}:{}}:{status:"skipped",...void 0!==t?{lastValidCheckpointId:t.id}:{}}}function v(e){if(Number.isInteger(e.schema_version)&&e.schema_version>h)throw new u(e.schema_version);if(!Number.isInteger(e.schema_version))throw new Error("Invalid persisted compaction checkpoint schema version");const t=g(JSON.parse(e.checkpoint_json));if(t.version!==e.schema_version)throw new Error(`Compaction checkpoint schema version mismatch: row v${String(e.schema_version)}, payload v${String(t.version)}`);return t}function k(e){return{id:e.id,title:e.title??void 0,model:e.model??void 0,createdAt:e.created_at,updatedAt:e.updated_at}}export class ThreadStore{db;constructor(r=defaultThreadsDir()){const o=expandTilde(r);t(o)||n(o,{recursive:!0,mode:448}),"win32"!==process.platform&&e(o,448);const i=s(o,"threads.db");if(this.db=new a(i),"win32"!==process.platform)try{e(i,384)}catch(e){throw this.db.close(),e}this.init()}init(){this.db.exec(`PRAGMA busy_timeout = ${String(q)};\n PRAGMA foreign_keys = ON;\n PRAGMA secure_delete = ON;`);const e=this.db.prepare("PRAGMA user_version").get();if(e.user_version>4)throw new Error(`ThreadStore schema v${String(e.user_version)} 高于当前支持的 v${String(4)}`);this.db.exec("BEGIN IMMEDIATE");try{this.db.exec("CREATE TABLE IF NOT EXISTS threads (\n id TEXT PRIMARY KEY,\n title TEXT,\n model TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n CREATE TABLE IF NOT EXISTS messages (\n thread_id TEXT NOT NULL,\n idx INTEGER NOT NULL,\n role TEXT NOT NULL,\n content_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, idx),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );"),M(this.db)||this.db.exec("CREATE TABLE messages_with_thread_fk (\n thread_id TEXT NOT NULL,\n idx INTEGER NOT NULL,\n role TEXT NOT NULL,\n content_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, idx),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n INSERT INTO messages_with_thread_fk\n (thread_id, idx, role, content_json, created_at)\n SELECT messages.thread_id, messages.idx, messages.role,\n messages.content_json, messages.created_at\n FROM messages\n INNER JOIN threads ON threads.id = messages.thread_id;\n DROP TABLE messages;\n ALTER TABLE messages_with_thread_fk RENAME TO messages;"),this.db.exec("CREATE TABLE IF NOT EXISTS tool_executions (\n thread_id TEXT NOT NULL,\n sequence INTEGER NOT NULL,\n id TEXT NOT NULL,\n tool_call_id TEXT NOT NULL,\n agent_name TEXT NOT NULL,\n tool_name TEXT NOT NULL,\n record_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, id),\n UNIQUE (thread_id, sequence),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE INDEX IF NOT EXISTS idx_tool_executions_thread_call\n ON tool_executions(thread_id, tool_call_id, sequence);\n CREATE TABLE IF NOT EXISTS thread_tool_execution_state (\n thread_id TEXT PRIMARY KEY,\n next_sequence INTEGER NOT NULL CHECK (next_sequence >= 0),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS transcript_messages (\n thread_id TEXT NOT NULL,\n sequence INTEGER NOT NULL,\n role TEXT NOT NULL,\n message_json TEXT NOT NULL,\n provenance TEXT NOT NULL CHECK (provenance IN ('native', 'legacy_snapshot')),\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, sequence),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS thread_transcript_state (\n thread_id TEXT PRIMARY KEY,\n completeness TEXT NOT NULL CHECK (completeness IN ('complete', 'legacy_snapshot')),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS compaction_checkpoints (\n id TEXT PRIMARY KEY,\n thread_id TEXT NOT NULL,\n generation INTEGER NOT NULL,\n schema_version INTEGER NOT NULL,\n message_from_sequence INTEGER NOT NULL,\n message_through_sequence INTEGER NOT NULL,\n tool_from_sequence INTEGER NOT NULL,\n tool_through_sequence INTEGER NOT NULL,\n checkpoint_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n UNIQUE (thread_id, generation),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE INDEX IF NOT EXISTS idx_compaction_checkpoints_thread_generation\n ON compaction_checkpoints(thread_id, generation DESC);"),this.db.exec("INSERT OR IGNORE INTO thread_tool_execution_state (thread_id, next_sequence)\n SELECT id, 0 FROM threads;\n UPDATE thread_tool_execution_state\n SET next_sequence = MAX(\n next_sequence,\n COALESCE((\n SELECT MAX(tool_executions.sequence) + 1\n FROM tool_executions\n WHERE tool_executions.thread_id = thread_tool_execution_state.thread_id\n ), 0)\n );"),e.user_version<3&&this.db.exec("INSERT OR IGNORE INTO thread_transcript_state (thread_id, completeness)\n SELECT id, 'legacy_snapshot' FROM threads;\n INSERT OR IGNORE INTO transcript_messages\n (thread_id, sequence, role, message_json, provenance, created_at)\n SELECT thread_id, idx, role, content_json, 'legacy_snapshot', created_at\n FROM messages;"),e.user_version<4&&(this.migrateToolExecutionPersistenceInTransaction(),this.migrateExplicitSkillCheckpointPersistenceInTransaction()),this.enforceAllToolExecutionRetentionInTransaction((new Date).toISOString()),this.db.exec(`PRAGMA user_version = ${String(4)};`),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}migrateToolExecutionPersistenceInTransaction(){const e=this.db.prepare("SELECT rowid AS storage_rowid, record_json\n FROM tool_executions\n WHERE rowid > ?\n ORDER BY rowid ASC\n LIMIT 100"),t=this.db.prepare("UPDATE tool_executions\n SET tool_call_id = ?, agent_name = ?, tool_name = ?, record_json = ?, created_at = ?\n WHERE rowid = ?");let n=0;for(;;){const r=e.all(n);if(0===r.length)break;for(const e of r){const r=c(JSON.parse(e.record_json));t.run(r.toolCallId,r.agentName,r.toolName,JSON.stringify(r),r.createdAt,e.storage_rowid),n=e.storage_rowid}}}enforceAllToolExecutionRetentionInTransaction(e){const t=this.db.prepare("SELECT DISTINCT thread_id AS id FROM tool_executions").all();for(const{id:n}of t)this.enforceToolExecutionRetentionInTransaction(n,e)}migrateExplicitSkillCheckpointPersistenceInTransaction(){const e=[{table:"messages",column:"content_json"},{table:"transcript_messages",column:"message_json"}];for(const t of e){const e=this.db.prepare(`SELECT rowid AS storage_rowid, ${t.column} AS message_json\n FROM ${t.table}\n WHERE ${t.column} LIKE '%"modelUserContent"%'`).all(),n=this.db.prepare(`UPDATE ${t.table} SET ${t.column} = ? WHERE rowid = ?`);for(const t of e){const e=i.parse(JSON.parse(t.message_json)),r=I(e);n.run(JSON.stringify(r),t.storage_rowid)}}}createThread(e={}){const t=e.id??o(),n=(new Date).toISOString();this.db.exec("BEGIN IMMEDIATE");try{return this.db.prepare("INSERT INTO threads (id, title, model, created_at, updated_at) VALUES (?, ?, ?, ?, ?)").run(t,e.title??null,e.model??null,n,n),this.db.prepare("INSERT INTO thread_transcript_state (thread_id, completeness) VALUES (?, ?)").run(t,T[0]),this.db.prepare("INSERT INTO thread_tool_execution_state (thread_id, next_sequence) VALUES (?, 0)").run(t),this.db.exec("COMMIT"),t}catch(e){throw this.db.exec("ROLLBACK"),e}}hasThread(e){return void 0!==this.db.prepare("SELECT 1 FROM threads WHERE id = ?").get(e)}getThread(e){const t=this.db.prepare("SELECT * FROM threads WHERE id = ?").get(e);return t?k(t):void 0}listThreads(){return this.db.prepare("SELECT * FROM threads ORDER BY updated_at DESC, rowid DESC").all().map(k)}countMessages(e){return this.db.prepare("SELECT COUNT(*) AS n FROM messages WHERE thread_id = ?").get(e).n}updateTitle(e,t){this.db.prepare("UPDATE threads SET title = ? WHERE id = ?").run(t,e)}deleteThread(e){this.db.prepare("DELETE FROM threads WHERE id = ?").run(e)}appendMessages(e,t){if(0===t.length)return;if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=t.map(e=>I(i.parse(e))),r=(new Date).toISOString(),s=this.db.prepare("INSERT INTO messages (thread_id, idx, role, content_json, created_at) VALUES (?, ?, ?, ?, ?)"),o=this.db.prepare("INSERT INTO transcript_messages\n (thread_id, sequence, role, message_json, provenance, created_at)\n VALUES (?, ?, ?, ?, ?, ?)");this.db.exec("BEGIN IMMEDIATE");try{const t=this.db.prepare("SELECT COALESCE(MAX(idx), -1) AS maxIdx FROM messages WHERE thread_id = ?").get(e),a=this.db.prepare("SELECT COALESCE(MAX(sequence), -1) AS maxSequence FROM transcript_messages WHERE thread_id = ?").get(e);let i=t.maxIdx+1,c=a.maxSequence+1;for(const t of n){const n=JSON.stringify(t);s.run(e,i,t.role,n,r),o.run(e,c,t.role,n,p[0],r),i+=1,c+=1}this.db.prepare("UPDATE threads SET updated_at = ? WHERE id = ?").run(r,e),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}replaceMessages(e,t){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=t.map(e=>i.parse(e)),r=(new Date).toISOString();this.db.exec("BEGIN IMMEDIATE");try{this.replaceMessagesInTransaction(e,n,r),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}getMessages(e){return this.db.prepare("SELECT content_json FROM messages WHERE thread_id = ? ORDER BY idx ASC").all(e).map(e=>i.parse(JSON.parse(e.content_json)))}replaceMessagesInTransaction(e,t,n){const r=this.db.prepare("INSERT INTO messages (thread_id, idx, role, content_json, created_at) VALUES (?, ?, ?, ?, ?)");this.db.prepare("DELETE FROM messages WHERE thread_id = ?").run(e),t.forEach((t,s)=>{const o=I(t);r.run(e,s,o.role,JSON.stringify(o),n)}),this.db.prepare("UPDATE threads SET updated_at = ? WHERE id = ?").run(n,e)}appendToolExecution(e,t){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=d(t),r=JSON.stringify(n),s=(new Date).toISOString();this.db.exec("BEGIN IMMEDIATE");try{const t=this.db.prepare("SELECT next_sequence FROM thread_tool_execution_state WHERE thread_id = ?").get(e);if(void 0===t)throw new Error(`Thread "${e}" 缺少 Tool execution sequence state`);const o=t.next_sequence;return this.db.prepare("INSERT INTO tool_executions\n (thread_id, sequence, id, tool_call_id, agent_name, tool_name, record_json, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)").run(e,o,n.id,n.toolCallId,n.agentName,n.toolName,r,n.createdAt),this.db.prepare("UPDATE thread_tool_execution_state SET next_sequence = ? WHERE thread_id = ?").run(o+1,e),this.enforceToolExecutionRetentionInTransaction(e,s),this.db.prepare("UPDATE threads SET updated_at = ? WHERE id = ?").run(s,e),this.db.exec("COMMIT"),o}catch(e){throw this.db.exec("ROLLBACK"),e}}enforceToolExecutionRetentionInTransaction(e,t){const n=new Date(Date.parse(t)-TOOL_EXECUTION_RETENTION_POLICY.maxAgeMs).toISOString();let r=!1;const s=this.db.prepare("DELETE FROM tool_executions\n WHERE thread_id = ?\n AND created_at < ?").run(e,n);r=Number(s.changes)>0;const o=this.db.prepare("SELECT sequence\n FROM (\n SELECT sequence,\n ROW_NUMBER() OVER (ORDER BY sequence DESC) AS retained_rank,\n SUM(length(CAST(record_json AS BLOB))) OVER (\n ORDER BY sequence DESC\n ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n ) AS retained_bytes\n FROM tool_executions\n WHERE thread_id = ?\n )\n WHERE retained_rank > ? OR retained_bytes > ?").all(e,TOOL_EXECUTION_RETENTION_POLICY.maxRecordsPerThread,TOOL_EXECUTION_RETENTION_POLICY.maxBytesPerThread);if(o.length>0){const t=this.db.prepare("DELETE FROM tool_executions WHERE thread_id = ? AND sequence = ?");for(const n of o)t.run(e,n.sequence);r=!0}r&&this.db.prepare("INSERT INTO thread_transcript_state (thread_id, completeness)\n VALUES (?, 'legacy_snapshot')\n ON CONFLICT(thread_id) DO UPDATE SET completeness = 'legacy_snapshot'").run(e)}listToolExecutions(e,t={}){const n=t.afterSequence??-1,r=t.throughSequence??Number.MAX_SAFE_INTEGER,s=t.limit??C;if(!Number.isInteger(n)||n<-1)throw new Error("afterSequence 必须是大于等于 -1 的整数");if(!Number.isInteger(s)||s<1||s>x)throw new Error(`limit 必须是 1-${String(x)} 之间的整数`);if(!Number.isInteger(r)||r<-1)throw new Error("throughSequence 必须是大于等于 -1 的整数");if(r<=n)return[];return this.db.prepare("SELECT sequence, record_json\n FROM tool_executions\n WHERE thread_id = ?\n AND sequence > ?\n AND sequence <= ?\n AND (? IS NULL OR tool_call_id = ?)\n ORDER BY sequence ASC\n LIMIT ?").all(e,n,r,t.toolCallId??null,t.toolCallId??null,s).map(e=>({...c(JSON.parse(e.record_json)),sequence:e.sequence}))}getToolExecution(e,t){const n=this.db.prepare("SELECT sequence, record_json FROM tool_executions WHERE thread_id = ? AND id = ?").get(e,t);if(n)return{...c(JSON.parse(n.record_json)),sequence:n.sequence}}getTranscriptCompleteness(e){const t=this.db.prepare("SELECT completeness FROM thread_transcript_state WHERE thread_id = ?").get(e);return t?.completeness===T[0]?T[0]:T[1]}listTranscriptMessages(e,t={}){const n=t.afterSequence??-1,r=t.throughSequence??Number.MAX_SAFE_INTEGER,s=t.limit??A;if(this.validateTranscriptRange(n,r,s,f),r<=n)return[];return this.db.prepare("SELECT sequence, provenance, created_at, message_json\n FROM transcript_messages\n WHERE thread_id = ?\n AND sequence > ?\n AND sequence <= ?\n ORDER BY sequence ASC\n LIMIT ?").all(e,n,r,s).map(e=>_.parse({sequence:e.sequence,provenance:e.provenance,createdAt:e.created_at,message:JSON.parse(e.message_json)}))}getLatestCheckpoint(e){const t=this.db.prepare("SELECT schema_version, checkpoint_json\n FROM compaction_checkpoints\n WHERE thread_id = ?\n ORDER BY generation DESC").all(e);for(const n of t)try{return this.withCurrentTranscriptCompleteness(e,v(n))}catch(e){if(e instanceof u)throw e}}getLatestSummaryCheckpoint(e){const t=this.db.prepare("SELECT schema_version, checkpoint_json\n FROM compaction_checkpoints\n WHERE thread_id = ?\n ORDER BY generation DESC").all(e);for(const n of t)try{const t=v(n);if("valid"===t.summary.status)return this.withCurrentTranscriptCompleteness(e,t)}catch(e){if(e instanceof u)throw e}}getCheckpoint(e,t){const n=this.db.prepare("SELECT schema_version, checkpoint_json\n FROM compaction_checkpoints\n WHERE thread_id = ? AND id = ?").get(e,t);if(void 0!==n)try{return this.withCurrentTranscriptCompleteness(e,v(n))}catch(e){if(e instanceof u)throw e;return}}loadSessionState(e){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);return{messages:O(this.getMessages(e)).messages,checkpoint:this.getLatestCheckpoint(e),transcriptCompleteness:this.getTranscriptCompleteness(e)}}commitCompaction(e,t){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=t.messages.map(e=>i.parse(e)),r=t.expectedActiveMessages.map(e=>i.parse(e)),s=O(n);if(s.status!==R.valid){const e=[...s.removedToolCallIds,...s.removedToolResultIds].filter((e,t,n)=>n.indexOf(e)===t).sort((e,t)=>e.localeCompare(t));throw new Error(`Compaction active projection contains malformed Tool protocol: ${e.join(", ")}`)}const o=m(t.draft),a=S.parse(t.legacySnapshotTranscriptFragments??[]),c=S.parse(a.map(e=>E(e))).map(e=>i.parse(N(e))),d=(new Date).toISOString();this.db.exec("BEGIN IMMEDIATE");try{const s=O(this.getMessages(e)).messages;if(JSON.stringify(s)!==JSON.stringify(r))throw new Error("Compaction active projection changed after the draft snapshot was captured");const a=this.getLatestCheckpoint(e);if(a?.id!==t.expectedLatestCheckpointId)throw new Error("Compaction checkpoint ancestry changed after the draft snapshot was captured");const i=this.getLatestSummaryCheckpoint(e),E=w(o.summary,i),h=this.getTranscriptCompleteness(e),u=m({...o,summary:E,...h===T[1]&&"valid"===o.toolState.integrityStatus?{toolState:{...o.toolState,integrityStatus:"sanitized"}}:{}}),_=this.db.prepare("SELECT COALESCE(MAX(generation), 0) AS maxGeneration\n FROM compaction_checkpoints\n WHERE thread_id = ?").get(e),g=this.maxTranscriptMessageSequence(e),S=this.maxToolExecutionSequence(e),N=t.evidenceWatermarks.transcriptMessagesThroughSequence,R=t.evidenceWatermarks.toolExecutionsThroughSequence,I=a?.transcript.messages.throughSequence??-1,L=a?.transcript.toolExecutions.throughSequence??-1,C=2===a?.version?a.semanticEvidence.messagesThroughSequence:-1,x=2===a?.version?a.semanticEvidence.toolExecutionsThroughSequence:-1;if(c.length>0&&1!==a?.version)throw new Error("Legacy snapshot transcript archive requires a V1 parent checkpoint");if(!Number.isInteger(N)||N<I||N>g)throw new Error(`Compaction transcript message watermark ${String(N)} is outside the available range ${String(I)}-${String(g)}`);if(!Number.isInteger(t.semanticEvidenceWatermarks.messagesThroughSequence)||t.semanticEvidenceWatermarks.messagesThroughSequence<C||t.semanticEvidenceWatermarks.messagesThroughSequence>N)throw new Error(`Compaction semantic message watermark ${String(t.semanticEvidenceWatermarks.messagesThroughSequence)} is outside the available range ${String(C)}-${String(N)}`);if(!Number.isInteger(t.semanticEvidenceWatermarks.toolExecutionsThroughSequence)||t.semanticEvidenceWatermarks.toolExecutionsThroughSequence<x||t.semanticEvidenceWatermarks.toolExecutionsThroughSequence>R)throw new Error(`Compaction semantic Tool watermark ${String(t.semanticEvidenceWatermarks.toolExecutionsThroughSequence)} is outside the available range ${String(x)}-${String(R)}`);if(!Number.isInteger(R)||R<L||R>S)throw new Error(`Compaction tool execution watermark ${String(R)} is outside the available range ${String(L)}-${String(S)}`);let A=N;if(c.length>0){if(N!==g)throw new Error("Legacy snapshot transcript archive requires the latest message watermark");const t=this.db.prepare("INSERT INTO transcript_messages\n (thread_id, sequence, role, message_json, provenance, created_at)\n VALUES (?, ?, ?, ?, ?, ?)");let n=g+1;for(const r of c)t.run(e,n,r.role,JSON.stringify(r),p[1],d),n+=1;A=n-1}const f={messages:{fromSequenceExclusive:I,throughSequence:A},toolExecutions:{fromSequenceExclusive:L,throughSequence:R},completeness:h},b=l({draft:u,semanticState:t.semanticState,semanticEvidence:t.semanticEvidenceWatermarks,generation:_.maxGeneration+1,transcript:f,...void 0!==a?{previousCheckpointId:a.id}:{},createdAt:d});return this.db.prepare("INSERT INTO compaction_checkpoints\n (id, thread_id, generation, schema_version,\n message_from_sequence, message_through_sequence,\n tool_from_sequence, tool_through_sequence,\n checkpoint_json, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(b.id,e,b.generation,b.version,b.transcript.messages.fromSequenceExclusive,b.transcript.messages.throughSequence,b.transcript.toolExecutions.fromSequenceExclusive,b.transcript.toolExecutions.throughSequence,JSON.stringify(b),b.createdAt),this.replaceMessagesInTransaction(e,n,d),this.db.exec("COMMIT"),b}catch(e){throw this.db.exec("ROLLBACK"),e}}readCheckpointTranscript(e,t){if("message"!==t.kind&&"tool_execution"!==t.kind)throw new Error("kind 必须是 message 或 tool_execution");const n=this.getCheckpoint(e,t.checkpointId);if(void 0===n)throw new Error("checkpoint 不存在、已损坏或不属于当前 thread");const r=t.limit??A,s="message"===t.kind?n.transcript.messages:n.transcript.toolExecutions,o=Math.max(t.afterSequence??s.fromSequenceExclusive,s.fromSequenceExclusive);this.validateTranscriptRange(o,s.throughSequence,r,b);const a="message"===t.kind?this.listTranscriptMessages(e,{afterSequence:o,throughSequence:s.throughSequence,limit:r+1}).map(e=>({kind:"message",...e})):this.listToolExecutions(e,{afterSequence:o,throughSequence:s.throughSequence,limit:Math.min(r+1,x)}).map(e=>({kind:"tool_execution",...e})),i=a.length>r,c=i?a.slice(0,r):a,d=c.at(-1)?.sequence;return{checkpointId:n.id,kind:t.kind,entries:c,...i&&void 0!==d?{nextAfterSequence:d}:{},...void 0!==n.previousCheckpointId?{previousCheckpointId:n.previousCheckpointId}:{},completeness:n.transcript.completeness}}maxTranscriptMessageSequence(e){return this.db.prepare("SELECT COALESCE(MAX(sequence), -1) AS maxSequence FROM transcript_messages WHERE thread_id = ?").get(e).maxSequence}maxToolExecutionSequence(e){const t=this.db.prepare("SELECT next_sequence FROM thread_tool_execution_state WHERE thread_id = ?").get(e);return(t?.next_sequence??0)-1}withCurrentTranscriptCompleteness(e,t){const n=T[1],r=t.transcript.completeness===n||this.getTranscriptCompleteness(e)===n?n:T[0],s=r===n&&"valid"===t.toolState.integrityStatus?{...t.toolState,integrityStatus:"sanitized"}:t.toolState;return t.transcript.completeness===r&&t.toolState===s?t:{...t,toolState:s,transcript:{...t.transcript,completeness:r}}}validateTranscriptRange(e,t,n,r){if(!Number.isInteger(e)||e<-1)throw new Error("afterSequence 必须是大于等于 -1 的整数");if(!Number.isInteger(t)||t<-1)throw new Error("throughSequence 必须是大于等于 -1 的整数");if(!Number.isInteger(n)||n<1||n>r)throw new Error(`limit 必须是 1-${String(r)} 之间的整数`)}close(){this.db.close()}}
|
|
@@ -12,12 +12,12 @@ export declare const turnCancellationMetadataV1Schema: z.ZodReadonly<z.ZodObject
|
|
|
12
12
|
reason: z.ZodEnum<["user", "timeout", "runtime"]>;
|
|
13
13
|
}, "strip", z.ZodTypeAny, {
|
|
14
14
|
version: 1;
|
|
15
|
-
reason: "user" | "timeout" | "runtime";
|
|
16
15
|
kind: "turn-cancellation";
|
|
16
|
+
reason: "user" | "timeout" | "runtime";
|
|
17
17
|
}, {
|
|
18
18
|
version: 1;
|
|
19
|
-
reason: "user" | "timeout" | "runtime";
|
|
20
19
|
kind: "turn-cancellation";
|
|
20
|
+
reason: "user" | "timeout" | "runtime";
|
|
21
21
|
}>>;
|
|
22
22
|
export declare function createTurnCancellationMessage(content: string, reason: SessionCancellationReason): AssistantModelMessage;
|
|
23
23
|
export declare function readTurnCancellationReason(message: ModelMessage): SessionCancellationReason | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roll-agent/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
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.22.0"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
39
|
"build": "rm -rf dist && tsc -p tsconfig.build.json && node ../../scripts/obfuscate.mjs",
|