@roll-agent/runtime 0.7.0 → 0.8.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.
Files changed (73) hide show
  1. package/dist/bash/classifier/compound.d.ts +2 -1
  2. package/dist/bash/classifier/compound.js +1 -1
  3. package/dist/bash/classifier/flag-audit.js +1 -1
  4. package/dist/bash/classifier/git-audit.d.ts +9 -1
  5. package/dist/bash/classifier/git-audit.js +1 -1
  6. package/dist/bash/classifier/index.d.ts +0 -1
  7. package/dist/bash/classifier/index.js +1 -1
  8. package/dist/bash/classifier/path-audit.d.ts +1 -1
  9. package/dist/bash/classifier/path-audit.js +1 -1
  10. package/dist/bash/classifier/safe-list.d.ts +6 -0
  11. package/dist/bash/classifier/safe-list.js +1 -1
  12. package/dist/bash/clean-env.d.ts +2 -0
  13. package/dist/bash/clean-env.js +1 -1
  14. package/dist/bash/format-result.d.ts +1 -1
  15. package/dist/bash/format-result.js +1 -1
  16. package/dist/bash/profile.js +1 -1
  17. package/dist/bash/session/session-manager.d.ts +1 -0
  18. package/dist/bash/session/session-manager.js +1 -1
  19. package/dist/bash/workdir.js +1 -1
  20. package/dist/engine/agent-session.d.ts +88 -3
  21. package/dist/engine/agent-session.js +1 -1
  22. package/dist/engine/cancelled-turn-recovery.d.ts +34 -0
  23. package/dist/engine/cancelled-turn-recovery.js +1 -0
  24. package/dist/engine/capability-manifest.d.ts +160 -0
  25. package/dist/engine/capability-manifest.js +1 -0
  26. package/dist/engine/compaction-checkpoint.d.ts +3827 -0
  27. package/dist/engine/compaction-checkpoint.js +1 -0
  28. package/dist/engine/compaction-semantic-state.d.ts +1741 -0
  29. package/dist/engine/compaction-semantic-state.js +1 -0
  30. package/dist/engine/compactor.d.ts +35 -3
  31. package/dist/engine/compactor.js +1 -1
  32. package/dist/engine/context-window.js +1 -1
  33. package/dist/engine/conversation-engine.d.ts +17 -4
  34. package/dist/engine/conversation-engine.js +1 -1
  35. package/dist/engine/explicit-skill-context.d.ts +103 -0
  36. package/dist/engine/explicit-skill-context.js +1 -0
  37. package/dist/engine/system-prompt.d.ts +4 -0
  38. package/dist/engine/system-prompt.js +1 -1
  39. package/dist/engine/tool-protocol-repair.d.ts +21 -0
  40. package/dist/engine/tool-protocol-repair.js +1 -0
  41. package/dist/engine/vcs-context.d.ts +3 -0
  42. package/dist/engine/vcs-context.js +1 -0
  43. package/dist/index.d.ts +6 -4
  44. package/dist/index.js +1 -1
  45. package/dist/server/protocol.d.ts +31 -0
  46. package/dist/server/protocol.js +1 -1
  47. package/dist/server/runtime-server.d.ts +21 -1
  48. package/dist/server/runtime-server.js +1 -1
  49. package/dist/store/thread-store.d.ts +96 -0
  50. package/dist/store/thread-store.js +1 -1
  51. package/dist/tool-bridge/agent-install-tool.js +1 -1
  52. package/dist/tool-bridge/bash-tool.d.ts +1 -0
  53. package/dist/tool-bridge/bash-tool.js +1 -1
  54. package/dist/tool-bridge/build-tools.d.ts +10 -1
  55. package/dist/tool-bridge/build-tools.js +1 -1
  56. package/dist/tool-bridge/naming.d.ts +10 -2
  57. package/dist/tool-bridge/naming.js +1 -1
  58. package/dist/tool-bridge/normalize-result.d.ts +73 -1
  59. package/dist/tool-bridge/normalize-result.js +1 -1
  60. package/dist/tool-bridge/session-exec-tool.d.ts +3 -2
  61. package/dist/tool-bridge/session-exec-tool.js +1 -1
  62. package/dist/tool-bridge/skill-tool.d.ts +3 -2
  63. package/dist/tool-bridge/skill-tool.js +1 -1
  64. package/dist/tool-bridge/tool-execution-coordinator.d.ts +69 -0
  65. package/dist/tool-bridge/tool-execution-coordinator.js +1 -0
  66. package/dist/tool-bridge/tool-execution-record.d.ts +148 -0
  67. package/dist/tool-bridge/tool-execution-record.js +1 -0
  68. package/dist/tool-bridge/transcript-tool.d.ts +30 -0
  69. package/dist/tool-bridge/transcript-tool.js +1 -0
  70. package/dist/types/cancellation.d.ts +18 -0
  71. package/dist/types/cancellation.js +1 -1
  72. package/dist/types/events.d.ts +20 -0
  73. package/package.json +2 -2
@@ -40,6 +40,8 @@ export declare const RpcMethod: {
40
40
  readonly Abort: "session.abort";
41
41
  readonly Close: "session.close";
42
42
  readonly Messages: "session.messages";
43
+ readonly ToolExecutions: "session.toolExecutions";
44
+ readonly Capabilities: "session.capabilities";
43
45
  readonly Compact: "session.compact";
44
46
  };
45
47
  export declare const EVENT_NOTIFICATION = "session.event";
@@ -111,6 +113,35 @@ export declare const messagesParamsSchema: z.ZodObject<{
111
113
  }, {
112
114
  sessionId: string;
113
115
  }>;
116
+ export declare const toolExecutionsParamsSchema: z.ZodObject<{
117
+ sessionId: z.ZodString;
118
+ executionId: z.ZodOptional<z.ZodString>;
119
+ includeRaw: z.ZodDefault<z.ZodBoolean>;
120
+ afterSequence: z.ZodOptional<z.ZodNumber>;
121
+ limit: z.ZodOptional<z.ZodNumber>;
122
+ toolCallId: z.ZodOptional<z.ZodString>;
123
+ }, "strip", z.ZodTypeAny, {
124
+ sessionId: string;
125
+ includeRaw: boolean;
126
+ toolCallId?: string | undefined;
127
+ limit?: number | undefined;
128
+ executionId?: string | undefined;
129
+ afterSequence?: number | undefined;
130
+ }, {
131
+ sessionId: string;
132
+ toolCallId?: string | undefined;
133
+ limit?: number | undefined;
134
+ executionId?: string | undefined;
135
+ afterSequence?: number | undefined;
136
+ includeRaw?: boolean | undefined;
137
+ }>;
138
+ export declare const capabilitiesParamsSchema: z.ZodObject<{
139
+ sessionId: z.ZodString;
140
+ }, "strip", z.ZodTypeAny, {
141
+ sessionId: string;
142
+ }, {
143
+ sessionId: string;
144
+ }>;
114
145
  export declare const compactParamsSchema: z.ZodObject<{
115
146
  sessionId: z.ZodString;
116
147
  }, "strip", z.ZodTypeAny, {
@@ -1 +1 @@
1
- import{z as s}from"zod";export const RpcMethod={Create:"session.create",Resume:"session.resume",Send:"session.send",Approve:"session.approve",Reject:"session.reject",Abort:"session.abort",Close:"session.close",Messages:"session.messages",Compact:"session.compact"};export const EVENT_NOTIFICATION="session.event";export const createParamsSchema=s.object({title:s.string().optional()});export const resumeParamsSchema=s.object({threadId:s.string()});export const sendParamsSchema=s.object({sessionId:s.string(),input:s.string()});export const approveParamsSchema=s.object({sessionId:s.string(),approvalId:s.string()});export const rejectParamsSchema=s.object({sessionId:s.string(),approvalId:s.string(),reason:s.string().optional()});export const abortParamsSchema=s.object({sessionId:s.string()});export const closeParamsSchema=s.object({sessionId:s.string()});export const messagesParamsSchema=s.object({sessionId:s.string()});export const compactParamsSchema=s.object({sessionId:s.string()});export function isRequest(s){return"method"in s&&"id"in s}
1
+ import{z as s}from"zod";export const RpcMethod={Create:"session.create",Resume:"session.resume",Send:"session.send",Approve:"session.approve",Reject:"session.reject",Abort:"session.abort",Close:"session.close",Messages:"session.messages",ToolExecutions:"session.toolExecutions",Capabilities:"session.capabilities",Compact:"session.compact"};export const EVENT_NOTIFICATION="session.event";export const createParamsSchema=s.object({title:s.string().optional()});export const resumeParamsSchema=s.object({threadId:s.string()});export const sendParamsSchema=s.object({sessionId:s.string(),input:s.string()});export const approveParamsSchema=s.object({sessionId:s.string(),approvalId:s.string()});export const rejectParamsSchema=s.object({sessionId:s.string(),approvalId:s.string(),reason:s.string().optional()});export const abortParamsSchema=s.object({sessionId:s.string()});export const closeParamsSchema=s.object({sessionId:s.string()});export const messagesParamsSchema=s.object({sessionId:s.string()});export const toolExecutionsParamsSchema=s.object({sessionId:s.string(),executionId:s.string().uuid().optional(),includeRaw:s.boolean().default(!1).describe("Requests bounded persisted Tool input/result evidence. RuntimeServer denies this by default; the host must provide an explicit authorization policy."),afterSequence:s.number().int().min(-1).optional(),limit:s.number().int().min(1).max(500).optional(),toolCallId:s.string().min(1).optional()});export const capabilitiesParamsSchema=s.object({sessionId:s.string()});export const compactParamsSchema=s.object({sessionId:s.string()});export function isRequest(s){return"method"in s&&"id"in s}
@@ -1,10 +1,30 @@
1
1
  import type { ConversationEngine } from "../engine/conversation-engine.ts";
2
2
  import { type JsonRpcConnection } from "./protocol.ts";
3
+ export interface RawToolEvidenceAccessRequest {
4
+ readonly sessionId: string;
5
+ readonly executionId?: string;
6
+ }
7
+ export interface RuntimeServerOptions {
8
+ /**
9
+ * Explicit host authorization boundary for bounded raw/input ledger projections.
10
+ * Omission is deny-by-default; `includeRaw: true` is only a request, never authorization.
11
+ */
12
+ readonly authorizeRawToolEvidence?: (request: RawToolEvidenceAccessRequest) => boolean | Promise<boolean>;
13
+ }
14
+ /**
15
+ * JSON-RPC host for trusted local transports.
16
+ *
17
+ * This class does not authenticate the connection or infer tenant ownership. Tool execution
18
+ * queries default to a redacted projection, and `includeRaw: true` remains denied unless the host
19
+ * supplies `authorizeRawToolEvidence`. Even authorized reads return the bounded, write-time
20
+ * redacted persistence projection rather than the original in-memory protocol result.
21
+ */
3
22
  export declare class RuntimeServer {
4
23
  private readonly engine;
5
24
  private readonly connection;
25
+ private readonly authorizeRawToolEvidence;
6
26
  private readonly sessions;
7
- constructor(engine: ConversationEngine, connection: JsonRpcConnection);
27
+ constructor(engine: ConversationEngine, connection: JsonRpcConnection, options?: RuntimeServerOptions);
8
28
  abortAll(): Promise<void>;
9
29
  private handleMessage;
10
30
  private requireSession;
@@ -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 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}`)}}}
1
+ import{createSafeCapabilitySnapshot as e}from"../engine/capability-manifest.js";import{isPersistedToolExecutionRecord as s}from"../tool-bridge/tool-execution-record.js";import{EVENT_NOTIFICATION as o,RpcMethod as i,abortParamsSchema as n,approveParamsSchema as t,capabilitiesParamsSchema as r,closeParamsSchema as a,compactParamsSchema as c,createParamsSchema as d,isRequest as l,messagesParamsSchema as u,rejectParamsSchema as p,resumeParamsSchema as h,sendParamsSchema as m,toolExecutionsParamsSchema as w}from"./protocol.js";function I(e){return e instanceof Error?e.message:String(e)}export class RuntimeServer{engine;connection;authorizeRawToolEvidence;sessions=new Map;constructor(e,s,o={}){this.engine=e,this.connection=s,this.authorizeRawToolEvidence=o.authorizeRawToolEvidence,this.connection.onMessage(e=>this.handleMessage(e))}async abortAll(){const e=[...this.sessions.values()];this.sessions.clear(),await Promise.all(e.map(e=>e.close()))}handleMessage(e){l(e)&&this.dispatch(e).then(s=>{this.connection.send({jsonrpc:"2.0",id:e.id,result:s})}).catch(s=>{this.connection.send({jsonrpc:"2.0",id:e.id,error:{code:-32e3,message:I(s)}})})}requireSession(e){const s=this.sessions.get(e);if(!s)throw new Error(`Session "${e}" 不存在`);return s}async dispatch(l){switch(l.method){case i.Create:{const e=d.parse(l.params),s=await this.engine.createSession(e.title?{title:e.title}:{});return this.sessions.set(s.id,s),{sessionId:s.id}}case i.Resume:{const e=h.parse(l.params),s=await this.engine.resumeSession(e.threadId);return this.sessions.set(s.id,s),{sessionId:s.id}}case i.Send:{const e=m.parse(l.params),s=this.requireSession(e.sessionId);for await(const i of s.send(e.input))this.connection.send({jsonrpc:"2.0",method:o,params:{sessionId:e.sessionId,event:i}});return{status:"completed"}}case i.Approve:{const e=t.parse(l.params);return{resolved:this.requireSession(e.sessionId).approve(e.approvalId)}}case i.Reject:{const e=p.parse(l.params);return{resolved:this.requireSession(e.sessionId).reject(e.approvalId,e.reason)}}case i.Abort:{const e=n.parse(l.params);return{ok:!0,cancelled:this.requireSession(e.sessionId).cancel()}}case i.Close:{const e=a.parse(l.params),s=this.requireSession(e.sessionId);return await s.close(),this.sessions.delete(e.sessionId),{closed:!0}}case i.Messages:{const e=u.parse(l.params);return{messages:this.requireSession(e.sessionId).getMessages()}}case i.ToolExecutions:{const e=w.parse(l.params),o=this.requireSession(e.sessionId);if(e.includeRaw&&!await(this.authorizeRawToolEvidence?.({sessionId:e.sessionId,...void 0!==e.executionId?{executionId:e.executionId}:{}})))throw new Error("Raw Tool evidence access denied by RuntimeServer policy");if(void 0!==e.executionId){const i=o.getToolExecution(e.executionId,e.includeRaw);if(e.includeRaw&&void 0!==i&&!s(i))throw new Error("Raw Tool evidence is unavailable without a durable ledger projection");return{record:i}}const i=o.getToolExecutions({...void 0!==e.afterSequence?{afterSequence:e.afterSequence}:{},...void 0!==e.limit?{limit:e.limit}:{},...void 0!==e.toolCallId?{toolCallId:e.toolCallId}:{}},e.includeRaw);if(e.includeRaw&&i.some(e=>!s(e)))throw new Error("Raw Tool evidence is unavailable without a durable ledger projection");return{records:i}}case i.Capabilities:{const s=r.parse(l.params),o=this.requireSession(s.sessionId);return e(o.getCapabilityManifest(),o.getCapabilityTurnContext())}case i.Compact:{const e=c.parse(l.params),s=this.requireSession(e.sessionId);for await(const i of s.compact("manual"))this.connection.send({jsonrpc:"2.0",method:o,params:{sessionId:e.sessionId,event:i}});return{status:"completed"}}default:throw new Error(`Unknown method: ${l.method}`)}}}
@@ -1,4 +1,15 @@
1
1
  import { type ModelMessage } from "ai";
2
+ import { type ToolExecutionPersistenceMetadata, type ToolExecutionRecord } from "../tool-bridge/tool-execution-record.ts";
3
+ import { COMPACTION_TRANSCRIPT_COMPLETENESS, type ArchivedTranscriptMessage, type CompactionCheckpoint, type CompactionCheckpointDraftInput, type CompactionSemanticEvidenceWatermarks } from "../engine/compaction-checkpoint.ts";
4
+ import { type CompactionSemanticState } from "../engine/compaction-semantic-state.ts";
5
+ export declare const TOOL_EXECUTION_RETENTION_POLICY: {
6
+ readonly maxBytesPerThread: number;
7
+ readonly maxRecordsPerThread: 2000;
8
+ readonly maxAgeMs: number;
9
+ };
10
+ export declare const TRANSCRIPT_ENTRY_KINDS: readonly ["message", "tool_execution"];
11
+ export type TranscriptEntryKind = (typeof TRANSCRIPT_ENTRY_KINDS)[number];
12
+ export type TranscriptCompleteness = (typeof COMPACTION_TRANSCRIPT_COMPLETENESS)[number];
2
13
  export interface ThreadRecord {
3
14
  readonly id: string;
4
15
  readonly title: string | undefined;
@@ -11,12 +22,80 @@ export interface CreateThreadInput {
11
22
  readonly title?: string;
12
23
  readonly model?: string;
13
24
  }
25
+ export type SequencedToolExecutionRecord = ToolExecutionRecord & {
26
+ readonly sequence: number;
27
+ /** Present for records loaded from the bounded durable ledger; absent for in-memory fallbacks. */
28
+ readonly persistence?: ToolExecutionPersistenceMetadata;
29
+ };
30
+ export interface ListToolExecutionsOptions {
31
+ readonly afterSequence?: number;
32
+ readonly throughSequence?: number;
33
+ readonly limit?: number;
34
+ readonly toolCallId?: string;
35
+ }
36
+ export interface ListTranscriptMessagesOptions {
37
+ readonly afterSequence?: number;
38
+ readonly throughSequence?: number;
39
+ readonly limit?: number;
40
+ }
41
+ export interface CompactionEvidenceWatermarks {
42
+ /** Highest append-only transcript sequence that was visible while building the draft. */
43
+ readonly transcriptMessagesThroughSequence: number;
44
+ /** Highest append-only Tool execution sequence that was visible while building the draft. */
45
+ readonly toolExecutionsThroughSequence: number;
46
+ }
47
+ export interface CommitCompactionInput {
48
+ readonly messages: readonly ModelMessage[];
49
+ /** Active projection observed while the checkpoint draft was built. */
50
+ readonly expectedActiveMessages: readonly ModelMessage[];
51
+ /** Latest checkpoint observed while the checkpoint draft was built. */
52
+ readonly expectedLatestCheckpointId: string | undefined;
53
+ readonly draft: CompactionCheckpointDraftInput;
54
+ /** Harness-validated semantic recovery state; model output is never persisted directly. */
55
+ readonly semanticState: CompactionSemanticState;
56
+ /** Evidence actually removed from active history and processed by the semantic draft. */
57
+ readonly semanticEvidenceWatermarks: CompactionSemanticEvidenceWatermarks;
58
+ readonly evidenceWatermarks: CompactionEvidenceWatermarks;
59
+ /** Narrow V1 migration payload; archived atomically and never inserted into active history. */
60
+ readonly legacySnapshotTranscriptFragments?: readonly string[];
61
+ }
62
+ export interface ThreadSessionState {
63
+ readonly messages: readonly ModelMessage[];
64
+ readonly checkpoint: CompactionCheckpoint | undefined;
65
+ readonly transcriptCompleteness: TranscriptCompleteness;
66
+ }
67
+ export type CheckpointTranscriptEntry = {
68
+ readonly kind: "message";
69
+ readonly sequence: number;
70
+ readonly provenance: ArchivedTranscriptMessage["provenance"];
71
+ readonly createdAt: string;
72
+ readonly message: ModelMessage;
73
+ } | ({
74
+ readonly kind: "tool_execution";
75
+ } & SequencedToolExecutionRecord);
76
+ export interface CheckpointTranscriptPage {
77
+ readonly checkpointId: CompactionCheckpoint["id"];
78
+ readonly kind: TranscriptEntryKind;
79
+ readonly entries: readonly CheckpointTranscriptEntry[];
80
+ readonly nextAfterSequence?: number;
81
+ readonly previousCheckpointId?: CompactionCheckpoint["id"];
82
+ readonly completeness: TranscriptCompleteness;
83
+ }
84
+ export interface ReadCheckpointTranscriptOptions {
85
+ readonly checkpointId: string;
86
+ readonly kind: TranscriptEntryKind;
87
+ readonly afterSequence?: number;
88
+ readonly limit?: number;
89
+ }
14
90
  export declare function expandTilde(path: string): string;
15
91
  export declare function defaultThreadsDir(): string;
16
92
  export declare class ThreadStore {
17
93
  private readonly db;
18
94
  constructor(dir?: string);
19
95
  private init;
96
+ private migrateToolExecutionPersistenceInTransaction;
97
+ private enforceAllToolExecutionRetentionInTransaction;
98
+ private migrateExplicitSkillCheckpointPersistenceInTransaction;
20
99
  createThread(input?: CreateThreadInput): string;
21
100
  hasThread(id: string): boolean;
22
101
  getThread(id: string): ThreadRecord | undefined;
@@ -27,5 +106,22 @@ export declare class ThreadStore {
27
106
  appendMessages(threadId: string, messages: readonly ModelMessage[]): void;
28
107
  replaceMessages(threadId: string, messages: readonly ModelMessage[]): void;
29
108
  getMessages(threadId: string): ModelMessage[];
109
+ private replaceMessagesInTransaction;
110
+ appendToolExecution(threadId: string, record: ToolExecutionRecord): number;
111
+ private enforceToolExecutionRetentionInTransaction;
112
+ listToolExecutions(threadId: string, options?: ListToolExecutionsOptions): SequencedToolExecutionRecord[];
113
+ getToolExecution(threadId: string, executionId: string): SequencedToolExecutionRecord | undefined;
114
+ getTranscriptCompleteness(threadId: string): TranscriptCompleteness;
115
+ listTranscriptMessages(threadId: string, options?: ListTranscriptMessagesOptions): ArchivedTranscriptMessage[];
116
+ getLatestCheckpoint(threadId: string): CompactionCheckpoint | undefined;
117
+ getLatestSummaryCheckpoint(threadId: string): CompactionCheckpoint | undefined;
118
+ getCheckpoint(threadId: string, checkpointId: string): CompactionCheckpoint | undefined;
119
+ loadSessionState(threadId: string): ThreadSessionState;
120
+ commitCompaction(threadId: string, input: CommitCompactionInput): CompactionCheckpoint;
121
+ readCheckpointTranscript(threadId: string, options: ReadCheckpointTranscriptOptions): CheckpointTranscriptPage;
122
+ private maxTranscriptMessageSequence;
123
+ private maxToolExecutionSequence;
124
+ private withCurrentTranscriptCompleteness;
125
+ private validateTranscriptRange;
30
126
  close(): void;
31
127
  }
@@ -1 +1 @@
1
- import{existsSync as e,mkdirSync as t}from"node:fs";import{homedir as r}from"node:os";import{resolve as d}from"node:path";import{randomUUID as a}from"node:crypto";import{DatabaseSync as s}from"node:sqlite";import{modelMessageSchema as i}from"ai";const n=1;export function expandTilde(e){return e.startsWith("~/")?d(r(),e.slice(2)):e}export function defaultThreadsDir(){return d(r(),".roll-agent","threads")}function E(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 a=expandTilde(r);e(a)||t(a,{recursive:!0}),this.db=new s(d(a,"threads.db")),this.init()}init(){this.db.exec("PRAGMA foreign_keys = ON;"),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 );\n PRAGMA user_version = ${String(1)};`)}createThread(e={}){const t=e.id??a(),r=(new Date).toISOString();return this.db.prepare("INSERT INTO threads (id, title, model, created_at, updated_at) VALUES (?, ?, ?, ?, ?)").run(t,e.title??null,e.model??null,r,r),t}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?E(t):void 0}listThreads(){return this.db.prepare("SELECT * FROM threads ORDER BY updated_at DESC, rowid DESC").all().map(E)}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}" 不存在`);let r=this.db.prepare("SELECT COALESCE(MAX(idx), -1) AS maxIdx FROM messages WHERE thread_id = ?").get(e).maxIdx+1;const d=(new Date).toISOString(),a=this.db.prepare("INSERT INTO messages (thread_id, idx, role, content_json, created_at) VALUES (?, ?, ?, ?, ?)");this.db.exec("BEGIN");try{for(const s of t)a.run(e,r,s.role,JSON.stringify(s),d),r+=1;this.db.prepare("UPDATE threads SET updated_at = ? WHERE id = ?").run(d,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 r=(new Date).toISOString(),d=this.db.prepare("INSERT INTO messages (thread_id, idx, role, content_json, created_at) VALUES (?, ?, ?, ?, ?)");this.db.exec("BEGIN");try{this.db.prepare("DELETE FROM messages WHERE thread_id = ?").run(e),t.forEach((t,a)=>{d.run(e,a,t.role,JSON.stringify(t),r)}),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}}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)))}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,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 +1 @@
1
- import{tool as n}from"ai";import{z as t}from"zod";export const AGENT_INSTALL_TOOL_AGENT_NAME="roll";export const AGENT_INSTALL_TOOL_NAME="agent_install";export const AGENT_INSTALL_TOOL_ID="roll__agent_install";function e(n,t){const e=t.length>0?`\n\n安装日志:\n${t.join("\n")}`:"";if(!n.ok){const t=n.retryCommand?`\n可在终端重试:${n.retryCommand}`:"";return{output:`安装失败:${n.message}${t}${e}`,isError:!0}}const o=n.version?` v${n.version}`:"",r=n.refreshApplied?"新工具将从下一轮对话开始可用。":"当前会话不会自动接入本次安装的版本,请让用户重新运行 roll chat 后使用。",a=n.missingEnv.length>0?`\n缺少必填环境变量:${n.missingEnv.join(", ")}。请让用户在终端运行 \`roll config setup agent ${n.agentName}\` 完成配置。`:"",i=n.retryCommand?`\n浏览器运行时已跳过安装,请让用户在终端补跑:${n.retryCommand}`:"";return{output:`已安装并注册 Agent "${n.agentName}"${o}。${r}${a}${i}${e}`,isError:!1}}export function buildAgentInstallToolset(o,r,a){const i=o.catalog.map(n=>n.shortName),[s,...l]=i;if(void 0===s)return{};const c=r.register("roll","agent_install"),m=t.object({agent:t.enum([s,...l]).describe(`要安装的官方 Agent 短名(可选:${i.join("、")})`)});return{[c]:n({description:`安装并注册一个官方子 Agent(执行 npm install,需要用户确认,可能耗时较长)。可安装:${o.catalog.map(n=>`${n.shortName}(${n.description})`).join("")}`,inputSchema:m,execute:async n=>{const t=a.policy?.check({agentName:"roll",toolName:"agent_install",input:n,annotations:{destructiveHint:!0}});if("deny"===t?.action)return{output:"策略拒绝执行"+(t.reason?`: ${t.reason}`:""),isError:!0};const r=await a.requestApproval({agentName:"roll",toolName:"agent_install",input:n,reason:`将执行 npm install 并注册子 Agent "${n.agent}"`});if(!r.approved)return{output:"已取消执行"+(r.reason?`: ${r.reason}`:""),isError:!0};const i=[];return e(await o.install(n.agent,n=>i.push(n)),i)}})}}
1
+ import{tool as e}from"ai";import{z as n}from"zod";import{TOOL_OUTCOME_KINDS as t,failedToolResult as o,successfulToolResult as r,toolResultToModelOutput as a}from"./normalize-result.js";import{TOOL_RESOURCE_ACCESS_MODES as s,executeCoordinatedTool as i}from"./tool-execution-coordinator.js";export const AGENT_INSTALL_TOOL_AGENT_NAME="roll";export const AGENT_INSTALL_TOOL_NAME="agent_install";export const AGENT_INSTALL_TOOL_ID="roll__agent_install";function l(e,n){const a=n.length>0?`\n\n安装日志:\n${n.join("\n")}`:"";if(!e.ok){const n=e.retryCommand?`\n可在终端重试:${e.retryCommand}`:"";return o(t.toolFailed,`安装失败:${e.message}${n}${a}`,{raw:e})}const s=e.version?` v${e.version}`:"",i=e.refreshApplied?"新工具将从下一轮对话开始可用。":"当前会话不会自动接入本次安装的版本,请让用户重新运行 roll chat 后使用。",l=e.missingEnv.length>0?`\n缺少必填环境变量:${e.missingEnv.join(", ")}。请让用户在终端运行 \`roll config setup agent ${e.agentName}\` 完成配置。`:"",c=e.retryCommand?`\n浏览器运行时已跳过安装,请让用户在终端补跑:${e.retryCommand}`:"";return r(`已安装并注册 Agent "${e.agentName}"${s}。${i}${l}${c}${a}`,{raw:e})}export function buildAgentInstallToolset(r,c,m){const g=r.catalog.map(e=>e.shortName),[p,...u]=g;if(void 0===p)return{};const d=c.register("roll","agent_install"),$=n.object({agent:n.enum([p,...u]).describe(`要安装的官方 Agent 短名(可选:${g.join("、")})`)}),N={prepare:async e=>{const n=$.safeParse(e);if(!n.success)return o(t.invalidInput,`参数校验失败: ${n.error.issues.map(e=>e.message).join("; ")}`,{raw:n.error.issues});const r=m.policy?.check({agentName:"roll",toolName:"agent_install",input:n.data,annotations:{destructiveHint:!0}});if("deny"===r?.action)return o(t.policyDenied,"策略拒绝执行"+(r.reason?`: ${r.reason}`:""),r.reason?{reason:r.reason}:{});const a=await m.requestApproval({agentName:"roll",toolName:"agent_install",input:n.data,reason:`将执行 npm install 并注册子 Agent "${n.data.agent}"`});return a.approved?void 0:o(t.userRejected,"已取消执行"+(a.reason?`: ${a.reason}`:""),a.reason?{reason:a.reason}:{})},resources:()=>[{key:"agent-registry",mode:s.write},{key:"package-manager",mode:s.write}]};return m.coordinator?.register(d,N),{[d]:e({description:`安装并注册一个官方子 Agent(执行 npm install,需要用户确认,可能耗时较长)。可安装:${r.catalog.map(e=>`${e.shortName}(${e.description})`).join(";")}`,inputSchema:$,toModelOutput:({output:e})=>a(e),execute:async(e,n)=>i(m.coordinator,N,d,n.toolCallId,e,n.abortSignal,async()=>{const n=[];return l(await r.install(e.agent,e=>n.push(e)),n)})})}}
@@ -19,6 +19,7 @@ export interface SessionBashSettings {
19
19
  readonly maxCaptureBytes: number;
20
20
  readonly maxModelOutputChars: number;
21
21
  readonly profile: ShellProfile;
22
+ readonly env?: NodeJS.ProcessEnv;
22
23
  }
23
24
  export interface BashToolContext extends ToolBridgeContext {
24
25
  readonly emitEvent?: (event: SessionEvent) => void;
@@ -1 +1 @@
1
- import{resolve as o}from"node:path";import{tool as t}from"ai";import{z as r}from"zod";import{CLASSIFICATION_ANNOTATIONS as e}from"../types/command-classification.js";import{runBashCommand as i}from"../bash/exec.js";import{formatBashResult as s}from"../bash/format-result.js";import{isWithinWorkdirRoot as a}from"../bash/workdir.js";import{gateToolCall as n}from"./build-tools.js";import{ToolRegistry as l}from"./naming.js";export const BASH_TOOL_AGENT_NAME="roll";export const BASH_TOOL_NAME="bash";export const BASH_TOOL_ID="roll__bash";export const POWERSHELL_TOOL_NAME="powershell";export const POWERSHELL_TOOL_ID="roll__powershell";const m=256,c=4096,u=r.object({command:r.string().min(1).describe("要执行的 shell 命令(单字符串,由当前 shell 后端执行)"),workdir:r.string().min(1).optional().describe("工作目录绝对路径,默认为 roll chat 当前目录。不要在 command 里用 cd,改用本字段"),timeout_ms:r.number().int().min(1).max(6e5).optional().describe("超时毫秒数。默认 10000,上限受 maxTimeoutMs 与 turnTimeoutMs 约束;长脚本请显式调大")});function p(o,t,r){const e=o.emitEvent;if(!e)return;let i=0;return(o,s)=>{if(i>=m)return;i+=1;const a=s.length>c?s.slice(0,c):s;e({type:"tool-output-delta",toolCallId:t,agentName:"roll",toolName:r,stream:o,delta:a})}}async function d(o,t,r,e){if(o.policy)return n(o,"roll",t,r,e);const i=await o.requestApproval({agentName:"roll",toolName:t,input:r,reason:"shell 命令需确认"});return i.approved?void 0:{output:"已取消执行"+(i.reason?`: ${i.reason}`:""),isError:!0}}export function buildBashToolset(r,n,l,m={}){const c=r.profile.toolName,f=n.register("roll",c),h=m.classifier??r.profile,_=m.exec??i;return{[f]:t({description:"在当前 shell 后端中执行一条命令并返回输出。命令继承 roll 进程的全部环境变量。总是用 workdir 参数设置工作目录,不要用 cd。",inputSchema:u,execute:async(t,i)=>{const n=o(r.workdir,t.workdir??"."),m=Math.min(t.timeout_ms??r.defaultTimeoutMs,r.maxTimeoutMs,r.turnTimeoutMs),u=a(r.workdir,n)?h.classify(t.command,n):"unknown",f=e[u],x={command:t.command,workdir:n,timeout_ms:m},b=await d(l,c,x,f);if(b)return b;const w=p(l,i.toolCallId,c),O=await _({command:t.command,workdir:n,timeoutMs:m,maxCaptureBytes:r.maxCaptureBytes,profile:r.profile,env:process.env,...i.abortSignal?{abortSignal:i.abortSignal}:{},...w?{onDelta:w}:{}});return s({result:O,maxModelOutputChars:r.maxModelOutputChars})}})}}
1
+ import{resolve as o}from"node:path";import{tool as t}from"ai";import{z as r}from"zod";import{CLASSIFICATION_ANNOTATIONS as e}from"../types/command-classification.js";import{runBashCommand as n}from"../bash/exec.js";import{withAutoApprovedShellEnv as i}from"../bash/clean-env.js";import{formatBashResult as s}from"../bash/format-result.js";import{isWithinWorkdirRoot as a}from"../bash/workdir.js";import{gateToolCall as l}from"./build-tools.js";import{ToolRegistry as m}from"./naming.js";import{TOOL_OUTCOME_KINDS as u,failedToolResult as c,toolResultToModelOutput as d}from"./normalize-result.js";import{TOOL_RESOURCE_ACCESS_MODES as p,executeCoordinatedTool as f}from"./tool-execution-coordinator.js";export const BASH_TOOL_AGENT_NAME="roll";export const BASH_TOOL_NAME="bash";export const BASH_TOOL_ID="roll__bash";export const POWERSHELL_TOOL_NAME="powershell";export const POWERSHELL_TOOL_ID="roll__powershell";const w=256,k=4096,h="shell:opaque-side-effects";function _(o){return"known-safe"===o||"dangerous"===o?o:"unknown"}const b=r.object({command:r.string().min(1).describe("要执行的 shell 命令(单字符串,由当前 shell 后端执行)"),workdir:r.string().min(1).optional().describe("工作目录绝对路径,默认为 roll chat 当前目录。不要在 command 里用 cd,改用本字段"),timeout_ms:r.number().int().min(1).max(6e5).optional().describe("超时毫秒数。默认 10000,上限受 maxTimeoutMs 与 turnTimeoutMs 约束;长脚本请显式调大")});function x(o){return"object"==typeof o&&null!==o&&"command"in o&&!("string"!=typeof o.command||"workdir"in o&&void 0!==o.workdir&&"string"!=typeof o.workdir||"timeout_ms"in o&&void 0!==o.timeout_ms&&"number"!=typeof o.timeout_ms)}function y(o,t,r){const e=o.emitEvent;if(!e)return;let n=0;return(o,i)=>{if(n>=w)return;n+=1;const s=i.length>k?i.slice(0,k):i;e({type:"tool-output-delta",toolCallId:t,agentName:"roll",toolName:r,stream:o,delta:s})}}async function M(o,t,r,e){if(o.policy)return l(o,"roll",t,r,e);const n=await o.requestApproval({agentName:"roll",toolName:t,input:r,reason:"shell 命令需确认"});return n.approved?void 0:c(u.userRejected,"已取消执行"+(n.reason?`: ${n.reason}`:""),n.reason?{reason:n.reason}:{})}export function buildBashToolset(r,l,m,w={}){const k=r.profile.toolName,O=l.register("roll",k),g=w.classifier??r.profile,v=w.exec??n,T=r.env??process.env,j=t=>({workdir:o(r.workdir,t.workdir??"."),timeoutMs:Math.min(t.timeout_ms??r.defaultTimeoutMs,r.maxTimeoutMs,r.turnTimeoutMs)}),E=(o,t)=>{const{workdir:n,timeoutMs:i}=j(o),s=a(r.workdir,n)?t??g.classify(o.command,n):"unknown";return{workdir:n,timeoutMs:i,classification:s,annotations:e[s]}},S={prepare:async(o,t)=>{if(!x(o))return c(u.invalidInput,"参数校验失败: command 必须为字符串",{raw:o});const r=E(o,_(t));return M(m,k,{command:o.command,workdir:r.workdir,timeout_ms:r.timeoutMs},r.annotations)},resources:(o,t)=>{if(!x(o))return[{key:h,mode:p.write},{key:`shell:${r.workdir}`,mode:p.write}];const e=E(o,_(t)),n=!0===e.annotations.readOnlyHint&&!0!==e.annotations.destructiveHint,i={key:`shell:${e.workdir}`,mode:n?p.read:p.write};return n?[i]:[{key:h,mode:p.write},i]},captureExecutionState:o=>x(o)?E(o).classification:"unknown",revalidateExecution:(o,t)=>{if("known-safe"===t&&x(o))return"known-safe"===E(o).classification?void 0:c(u.toolFailed,"shell 命令的安全条件在准入后发生变化,已在执行前阻止;请重新提交以重新确认")}};return m.coordinator?.register(O,S),{[O]:t({description:"在当前 shell 后端中执行一条命令并返回输出。需确认的命令继承 roll 进程环境;自动批准的 known-safe 命令使用隔离的系统 PATH 与 shell 环境。总是用 workdir 参数设置工作目录,不要用 cd。",inputSchema:b,toModelOutput:({output:o})=>d(o),execute:async(o,t)=>{const{workdir:e,timeoutMs:n}=j(o);return f(m.coordinator,S,O,t.toolCallId,o,t.abortSignal,async a=>{const l=y(m,t.toolCallId,k),u=await v({command:o.command,workdir:e,timeoutMs:n,maxCaptureBytes:r.maxCaptureBytes,profile:r.profile,env:"known-safe"===a?i(T):T,...t.abortSignal?{abortSignal:t.abortSignal}:{},...l?{onDelta:l}:{}});return s({result:u,maxModelOutputChars:r.maxModelOutputChars})})}})}}
@@ -3,16 +3,24 @@ import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
3
  import type { AgentTool } from "@roll-agent/core/types/agent";
4
4
  import type { ApprovalDecision } from "../approval/approval-gate.ts";
5
5
  import type { ToolAnnotations, ToolPolicy } from "../types/policy.ts";
6
- import { ToolRegistry } from "./naming.ts";
6
+ import { ToolRegistry, type ToolRouteMetadata } from "./naming.ts";
7
7
  import { type NormalizedToolResult } from "./normalize-result.ts";
8
+ import { type ToolExecutionCoordinator, type ToolResourceHint } from "./tool-execution-coordinator.ts";
9
+ export declare const ROLL_RESOURCE_HINTS_META_KEY = "roll/resourceHints";
8
10
  export interface SourceTool {
9
11
  readonly tool: AgentTool;
10
12
  readonly annotations: ToolAnnotations | undefined;
13
+ readonly resourceHints?: readonly ToolResourceHint[];
11
14
  }
12
15
  export interface AgentToolSource {
13
16
  readonly agentName: string;
14
17
  readonly client: Client;
15
18
  readonly tools: readonly SourceTool[];
19
+ readonly agentSource?: ToolRouteMetadata["agentSource"];
20
+ readonly transport?: ToolRouteMetadata["transport"];
21
+ readonly runtimeOwnership?: ToolRouteMetadata["runtimeOwnership"];
22
+ /** Base directory used by a local stdio Agent to resolve relative file resource hints. */
23
+ readonly resourceBaseDir?: string;
16
24
  }
17
25
  export interface ApprovalRequest {
18
26
  readonly agentName: string;
@@ -23,6 +31,7 @@ export interface ApprovalRequest {
23
31
  export interface ToolBridgeContext {
24
32
  readonly policy?: ToolPolicy;
25
33
  readonly requestApproval: (request: ApprovalRequest) => Promise<ApprovalDecision>;
34
+ readonly coordinator?: ToolExecutionCoordinator;
26
35
  }
27
36
  export interface BuiltToolset {
28
37
  readonly tools: ToolSet;
@@ -1 +1 @@
1
- import{jsonSchema as o,tool as t}from"ai";import{preflightToolCall as n}from"@roll-agent/core/tool-runtime/preflight";import{ToolRegistry as r}from"./naming.js";import{normalizeToolResult as e}from"./normalize-result.js";function a(o){return"object"!=typeof o||null===o||Array.isArray(o)?{}:o}function i(o){return`参数校验失败: ${o.map(o=>o.message).join("; ")}`}export async function gateToolCall(o,t,n,r,e){if(!o.policy)return;const a=o.policy.check({agentName:t,toolName:n,input:r,...e?{annotations:e}:{}});if("deny"===a.action)return{output:"策略拒绝执行"+(a.reason?`: ${a.reason}`:""),isError:!0};if("confirm"===a.action){const e=await o.requestApproval({agentName:t,toolName:n,input:r,reason:a.reason});if(!e.approved)return{output:"已取消执行"+(e.reason?`: ${e.reason}`:""),isError:!0}}}export function buildAgentToolset(s,c,l=new r){const u={};for(const r of s){const{client:s,agentName:m}=r;for(const{tool:p,annotations:f}of r.tools){u[l.register(m,p.name)]=t({description:p.description??`${p.name} (via ${m})`,inputSchema:o(p.inputSchema),execute:async(o,t)=>{const r=a(o),l=n(p,r);if(!l.ok)return{output:i(l.issues),isError:!0};const u=await gateToolCall(c,m,p.name,r,f);if(u)return u;const g=t.abortSignal?{signal:t.abortSignal}:void 0,y=await s.callTool({name:p.name,arguments:r},void 0,g);return e(y)}})}}return{tools:u,registry:l}}
1
+ import{lstatSync as t,readdirSync as e,readlinkSync as n,realpathSync as o,statSync as r}from"node:fs";import{basename as i,dirname as s,isAbsolute as a,resolve as c}from"node:path";import{jsonSchema as u,tool as f}from"ai";import{preflightToolCall as l}from"@roll-agent/core/tool-runtime/preflight";import{ToolRegistry as d}from"./naming.js";import{TOOL_OUTCOME_KINDS as p,failedToolResult as m,normalizeToolResult as g,toolResultToModelOutput as v}from"./normalize-result.js";import{TOOL_RESOURCE_ACCESS_MODES as y,TOOL_RESOURCE_HINT_KINDS as h,executeCoordinatedTool as S}from"./tool-execution-coordinator.js";export const ROLL_RESOURCE_HINTS_META_KEY="roll/resourceHints";function $(t){return"object"!=typeof t||null===t||Array.isArray(t)?{}:t}function b(t){return`参数校验失败: ${t.map(t=>t.message).join("; ")}`}function w(t){return!0===t?.readOnlyHint&&!0!==t.destructiveHint?y.read:y.write}function C(t){return"string"==typeof t||"number"==typeof t?[t]:Array.isArray(t)&&0!==t.length&&t.every(t=>"string"==typeof t||"number"==typeof t)?t:void 0}const N=256,k=new Map;function A(t,e){for(let n=0;n<t.length;n+=1){const o=t.charCodeAt(n),r=o>=65&&o<=90?String.fromCharCode(o+32):o>=97&&o<=122?String.fromCharCode(o-32):void 0;if(void 0===r)continue;const i=`${t.slice(0,n)}${r}${t.slice(n+1)}`;if(!e.has(i))return i}}function O(t){return"object"==typeof t&&null!==t&&"code"in t?t.code:void 0}function j(e,n,o){const r=A(n,o);if(void 0!==r)try{const o=t(c(e,n)),i=t(c(e,r));return o.dev===i.dev&&o.ino===i.ino}catch(t){const e=O(t);return"ENOENT"!==e&&"ENOTDIR"!==e&&void 0}}function T(t,e){if(k.size>=N){const t=k.keys().next().value;void 0!==t&&k.delete(t)}return k.set(t,e),e}function E(n){const o=k.get(n);if(void 0!==o)return o;let r;try{r=t(n).dev}catch{return T(n,!0)}let i=n;for(;;){const o=k.get(i);if(void 0!==o)return T(n,o);try{const t=e(i),o=new Set(t);for(const e of t){const t=j(i,e,o);if(void 0!==t)return T(i,t),T(n,t)}}catch{}const a=s(i);if(a===i)break;try{if(t(a).dev!==r)break}catch{break}i=a}return T(n,!0)}function x(t){let e=t.normalize("NFC");for(let t=0;t<8;t+=1){const t=e.toUpperCase().toLowerCase().normalize("NFC");if(t===e)return e;e=t}return e}function z(t,e){try{const n=r(t,{bigint:!0}),o=e.map(t=>x(t));return`file-unresolved-alias:${n.dev.toString()}:${n.ino.toString()}:${JSON.stringify(o)}`}catch{return}}function H(e,r=new Set){let a=e;const u=[];for(;;)try{const t=o.native(a),e=u.reverse().map(t=>t.normalize("NFC"));if(0===e.length)return{path:t};const n=E(t),r=c(t,...n?e.map(t=>t.toLowerCase()):e),i=n?z(t,e):void 0;return{path:r,...void 0===i?{}:{unresolvedAliasKey:i}}}catch{try{if(t(a).isSymbolicLink()&&!r.has(a)){const t=new Set(r);t.add(a);const e=n(a);return H(c(s(a),e,...[...u].reverse()),t)}}catch{}const o=s(a);if(o===a)return{path:e};u.push(i(a)),a=o}}function L(t){try{const e=r(t,{bigint:!0});return`file-inode:${e.dev.toString()}:${e.ino.toString()}`}catch{return}}function R(t,e,n){const o=String(e).trim();if(0!==o.length)switch(t.kind){case h.file:{if(!a(o)&&void 0===n)return;const t=H(c(n??"/",o)),e=L(t.path);return[`file:${t.path}`,...void 0===t.unresolvedAliasKey?[]:[t.unresolvedAliasKey],...void 0===e?[]:[e]]}case h.browserSession:return[`browser-session:${o}`];case h.conversation:return[`conversation:${o}`];case h.custom:{const e=t.namespace?.trim();return e?[`${e}:${o}`]:void 0}}}function I(t,e,n,o,r){const i=w(o),s=$(n),a=[{key:`agent:${t}`,mode:i}],c=[];for(const t of r??[]){if(!Object.hasOwn(s,t.field))continue;const n=C(s[t.field]);if(void 0===n)return a;for(const o of n){const n=R(t,o,e);if(void 0===n||0===n.length)return a;c.push(...n.map(e=>({key:e,mode:t.mode??i})))}}return 0===c.length?a:[{key:`agent:${t}`,mode:y.read},...c]}export async function gateToolCall(t,e,n,o,r){if(!t.policy)return;const i=t.policy.check({agentName:e,toolName:n,input:o,...r?{annotations:r}:{}});if("deny"===i.action)return m(p.policyDenied,"策略拒绝执行"+(i.reason?`: ${i.reason}`:""),i.reason?{reason:i.reason}:{});if("confirm"===i.action){const r=await t.requestApproval({agentName:e,toolName:n,input:o,reason:i.reason});if(!r.approved)return m(p.userRejected,"已取消执行"+(r.reason?`: ${r.reason}`:""),r.reason?{reason:r.reason}:{})}}export function buildAgentToolset(t,e,n=new d){const o={};for(const r of t){const{client:t,agentName:i,agentSource:s,transport:a,runtimeOwnership:c,resourceBaseDir:d}=r;for(const{tool:y,annotations:h,resourceHints:w}of r.tools){const r=n.register(i,y.name,{...s?{agentSource:s}:{},...a?{transport:a}:{},...c?{runtimeOwnership:c}:{},...h?{annotations:h}:{}}),C={prepare:async t=>{const n=$(t),o=l(y,n);return o.ok?gateToolCall(e,i,y.name,n,h):m(p.invalidInput,b(o.issues),{raw:o.issues})},resources:t=>I(i,d,t,h,w)};e.coordinator?.register(r,C),o[r]=f({description:y.description??`${y.name} (via ${i})`,inputSchema:u(y.inputSchema),toModelOutput:({output:t})=>v(t),execute:async(n,o)=>{const i=$(n);return S(e.coordinator,C,r,o.toolCallId,i,o.abortSignal,async()=>{const e=o.abortSignal?{signal:o.abortSignal}:void 0,n=await t.callTool({name:y.name,arguments:i},void 0,e);return g(n)})}})}}return{tools:o,registry:n}}
@@ -1,10 +1,18 @@
1
- export interface ToolRoute {
1
+ import type { AgentRuntimeOwnership, AgentSourceType, AgentTransport } from "@roll-agent/core/types/agent";
2
+ import type { ToolAnnotations } from "../types/policy.ts";
3
+ export interface ToolRouteMetadata {
4
+ readonly agentSource?: AgentSourceType;
5
+ readonly transport?: AgentTransport["type"];
6
+ readonly runtimeOwnership?: AgentRuntimeOwnership;
7
+ readonly annotations?: ToolAnnotations;
8
+ }
9
+ export interface ToolRoute extends ToolRouteMetadata {
2
10
  readonly agentName: string;
3
11
  readonly toolName: string;
4
12
  }
5
13
  export declare class ToolRegistry {
6
14
  private readonly routes;
7
- register(agentName: string, toolName: string): string;
15
+ register(agentName: string, toolName: string, metadata?: ToolRouteMetadata): string;
8
16
  resolve(id: string): ToolRoute | undefined;
9
17
  size(): number;
10
18
  }
@@ -1 +1 @@
1
- const e=/[^a-zA-Z0-9_-]/g;function t(t){return t.replace(e,"_")}export class ToolRegistry{routes=new Map;register(e,r){const s=`${t(e)}__${t(r)}`;let o=s,n=1;for(;this.routes.has(o);)o=`${s}_${String(n)}`,n+=1;return this.routes.set(o,{agentName:e,toolName:r}),o}resolve(e){return this.routes.get(e)}size(){return this.routes.size}}
1
+ const t=/[^a-zA-Z0-9_-]/g;function e(e){return e.replace(t,"_")}export class ToolRegistry{routes=new Map;register(t,r,n={}){const s=`${e(t)}__${e(r)}`;let o=s,a=1;for(;this.routes.has(o);)o=`${s}_${String(a)}`,a+=1;return this.routes.set(o,{agentName:t,toolName:r,...n.agentSource?{agentSource:n.agentSource}:{},...n.transport?{transport:n.transport}:{},...n.runtimeOwnership?{runtimeOwnership:n.runtimeOwnership}:{},...n.annotations?{annotations:{...n.annotations}}:{}}),o}resolve(t){return this.routes.get(t)}size(){return this.routes.size}}
@@ -1,6 +1,78 @@
1
+ import type { JSONValue } from "@ai-sdk/provider";
2
+ export declare const TOOL_OUTCOME_KINDS: {
3
+ readonly success: "success";
4
+ readonly userRejected: "user_rejected";
5
+ readonly policyDenied: "policy_denied";
6
+ readonly invalidInput: "invalid_input";
7
+ readonly cancelled: "cancelled";
8
+ readonly toolFailed: "tool_failed";
9
+ };
10
+ export type ToolOutcomeKind = (typeof TOOL_OUTCOME_KINDS)[keyof typeof TOOL_OUTCOME_KINDS];
11
+ export type ToolOutcome = {
12
+ readonly kind: typeof TOOL_OUTCOME_KINDS.success;
13
+ } | {
14
+ readonly kind: typeof TOOL_OUTCOME_KINDS.userRejected | typeof TOOL_OUTCOME_KINDS.policyDenied | typeof TOOL_OUTCOME_KINDS.invalidInput | typeof TOOL_OUTCOME_KINDS.cancelled | typeof TOOL_OUTCOME_KINDS.toolFailed;
15
+ readonly reason?: string;
16
+ };
17
+ export type ToolModelContentPart = {
18
+ type: "text";
19
+ text: string;
20
+ } | {
21
+ type: "file";
22
+ data: {
23
+ type: "data";
24
+ data: string;
25
+ };
26
+ mediaType: string;
27
+ filename?: string;
28
+ };
29
+ /** AI SDK compatible model-facing projection of a tool result. */
30
+ export type ToolModelOutput = {
31
+ type: "text";
32
+ value: string;
33
+ } | {
34
+ type: "json";
35
+ value: JSONValue;
36
+ } | {
37
+ type: "execution-denied";
38
+ reason?: string;
39
+ } | {
40
+ type: "error-text";
41
+ value: string;
42
+ } | {
43
+ type: "error-json";
44
+ value: JSONValue;
45
+ } | {
46
+ type: "content";
47
+ value: ToolModelContentPart[];
48
+ };
49
+ /**
50
+ * One execution result, deliberately split for three consumers:
51
+ * - `raw`: lossless Harness / automation value;
52
+ * - `model`: bounded provider-facing value;
53
+ * - `display`: concise terminal / UI value.
54
+ *
55
+ * `output` and `isError` are compatibility aliases for callers that have not migrated yet.
56
+ */
1
57
  export interface NormalizedToolResult {
58
+ readonly raw: unknown;
59
+ readonly model: ToolModelOutput;
60
+ readonly display: unknown;
61
+ readonly outcome: ToolOutcome;
2
62
  readonly output: unknown;
3
63
  readonly isError: boolean;
4
64
  }
65
+ export interface ToolResultOptions {
66
+ readonly raw?: unknown;
67
+ readonly model?: ToolModelOutput;
68
+ readonly reason?: string;
69
+ }
70
+ export declare function createToolResult(outcome: ToolOutcome, display: unknown, options?: ToolResultOptions): NormalizedToolResult;
71
+ export declare function successfulToolResult(display: unknown, options?: Omit<ToolResultOptions, "reason">): NormalizedToolResult;
72
+ export declare function failedToolResult(kind: Exclude<ToolOutcomeKind, typeof TOOL_OUTCOME_KINDS.success>, display: unknown, options?: ToolResultOptions): NormalizedToolResult;
5
73
  export declare function normalizeToolResult(result: unknown): NormalizedToolResult;
6
- export declare function readIsError(output: unknown): boolean;
74
+ export declare function isNormalizedToolResult(value: unknown): value is NormalizedToolResult;
75
+ export declare function readToolOutcome(value: unknown): ToolOutcome;
76
+ export declare function readIsError(value: unknown): boolean;
77
+ export declare function readDisplayOutput(value: unknown): unknown;
78
+ export declare function toolResultToModelOutput(result: NormalizedToolResult): ToolModelOutput;
@@ -1 +1 @@
1
- import{extractTextContent as o,isToolErrorResult as r}from"@roll-agent/core/cli/utils/tool-results";function t(o){return"object"==typeof o&&null!==o&&"content"in o?o.content:void 0}export function normalizeToolResult(n){const e=r(n),i=o(t(n));return{output:i.length>0?i.join("\n"):n,isError:e}}export function readIsError(o){return"object"==typeof o&&null!==o&&"isError"in o&&!0===o.isError}
1
+ import{extractTextContent as t,isToolErrorResult as e}from"@roll-agent/core/cli/utils/tool-results";export const TOOL_OUTCOME_KINDS={success:"success",userRejected:"user_rejected",policyDenied:"policy_denied",invalidInput:"invalid_input",cancelled:"cancelled",toolFailed:"tool_failed"};const n=16e3,r=64,o=6e4,i=128,u="\n\n[工具模型内容已截断;完整结果仍保留在 raw 视图]";function s(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}function l(t){if(null===t||"string"==typeof t||"boolean"==typeof t||"number"==typeof t&&Number.isFinite(t))return t;if(Array.isArray(t))return t.map(l);if(s(t)){const e={};for(const[n,r]of Object.entries(t))e[n]=l(r);return e}return void 0===t?null:String(t)}function c(t){if(!t)return!1;const e=t.replace(/([a-z0-9])([A-Z])/gu,"$1_$2").toLowerCase().split(/[^a-z0-9]+/u).filter(t=>t.length>0);return 1===e.length&&["data","image","audio"].includes(e[0]??"")||e.some(t=>["base64","blob","bytes"].includes(t))||e.includes("data")&&e.some(t=>["image","audio","binary"].includes(t))}function a(t){return`[binary content omitted: ${String(t.length)} chars]`}function p(t,e,n){return t.length<=e?t:e<=n.length?n.slice(0,e):`${t.slice(0,e-n.length)}${n}`}function d(t,e,n=0){if(n>16)return"[nested value omitted]";if("string"==typeof t)return c(e)||/^data:[^,]+;base64,/iu.test(t)?a(t):p(t,o,u);if(null===t||"boolean"==typeof t||"number"==typeof t)return Number.isFinite(t)||"number"!=typeof t?t:String(t);if(Array.isArray(t)){const e=t.slice(0,i).map(t=>d(t,void 0,n+1));return t.length>i&&e.push(`[${String(t.length-i)} items omitted]`),e}if(s(t)){const e={},r=Object.entries(t).filter(([t])=>"_meta"!==t);for(const[t,o]of r.slice(0,i))e[t]=d(o,t,n+1);return r.length>i&&(e["[omitted]"]=`${String(r.length-i)} fields`),e}return void 0===t?null:String(t)}function f(t,e,o=0){if(o>12)return"[nested value omitted]";if("string"==typeof t)return c(e)||/^data:[^,]+;base64,/iu.test(t)?a(t):t.length<=n?t:g(t);if(null===t||"boolean"==typeof t||"number"==typeof t)return Number.isFinite(t)||"number"!=typeof t?t:String(t);if(Array.isArray(t)){const e=t.slice(0,r).map(t=>f(t,void 0,o+1));return t.length>r&&e.push(`[${String(t.length-r)} items omitted]`),e}if(s(t)){const e={},n=Object.entries(t).filter(([t])=>"_meta"!==t);for(const[t,i]of n.slice(0,r))e[t]=f(i,t,o+1);return n.length>r&&(e["[omitted]"]=`${String(n.length-r)} fields`),e}return void 0===t?null:String(t)}function y(t){return"string"==typeof t?t:JSON.stringify(l(t),null,2)}function g(t){return t.length<=n?t:`${t.slice(0,n)}\n\n[工具展示内容已截断]`}function m(t,e=!1){const n=d(t),r=y(n);if(r.length<=o)return e?{type:"error-json",value:n}:{type:"json",value:n};const i=p(r,o,u);return e?{type:"error-text",value:i}:{type:"text",value:i}}function O(t,e=!1){const n=p(t,o,u);return e?{type:"error-text",value:n}:{type:"text",value:n}}function T(t){const e=[];let n=o;for(const r of t.slice(0,i)){if(n<=0)break;if("text"===r.type){const t=p(r.text,n,u);e.push({type:"text",text:t}),n-=t.length;continue}const t=r.data.data.length;if(t<=n){e.push(r),n-=t;continue}const o=p(`[${r.mediaType} file omitted from model projection: ${String(t)} chars]`,n,u);e.push({type:"text",text:o}),n-=o.length}if(t.length>i&&n>0){const r=p(`[${String(t.length-i)} model content parts omitted]`,n,u);e.push({type:"text",text:r})}return e}function h(t,e){return e.kind===TOOL_OUTCOME_KINDS.userRejected?{type:"execution-denied",...void 0!==e.reason?{reason:e.reason}:{}}:e.kind===TOOL_OUTCOME_KINDS.success?"string"==typeof t?O(t):m(t):m({code:e.kind,message:"string"==typeof t?t:y(t),...void 0!==e.reason?{reason:e.reason}:{}},!0)}export function createToolResult(t,e,n={}){const r=t.kind!==TOOL_OUTCOME_KINDS.success;return{raw:Object.prototype.hasOwnProperty.call(n,"raw")?n.raw:e,model:n.model??h(e,t),display:e,outcome:t,output:e,isError:r}}export function successfulToolResult(t,e={}){return createToolResult({kind:TOOL_OUTCOME_KINDS.success},t,e)}export function failedToolResult(t,e,n={}){return createToolResult({kind:t,...void 0!==n.reason?{reason:n.reason}:{}},e,n)}function x(t){return s(t)?t.content:void 0}function _(t){return s(t)?t.structuredContent:void 0}function v(t){return s(t)&&"toolResult"in t?{value:t.toolResult,wrapped:!0}:{value:t,wrapped:!1}}function b(t){if("string"==typeof t.data&&"string"==typeof t.mimeType)return{type:"file",data:{type:"data",data:t.data},mediaType:t.mimeType}}function S(t){const e=s(t.resource)?t.resource:void 0;if(!e)return;const n="string"==typeof e.uri?e.uri:"embedded resource";return"string"==typeof e.text?{type:"text",text:`[${n}]\n${e.text}`}:"string"==typeof e.blob?{type:"file",data:{type:"data",data:e.blob},mediaType:"string"==typeof e.mimeType?e.mimeType:"application/octet-stream",filename:n}:void 0}function $(t){if("string"!=typeof t.uri)return;const e="string"==typeof t.name?t.name:"resource",n="string"==typeof t.description?` — ${t.description}`:"";return{type:"text",text:`[${e}] ${t.uri}${n}`}}function N(t){if(s(t)&&"string"==typeof t.type)return"text"===t.type&&"string"==typeof t.text?{type:"text",text:t.text}:"image"===t.type||"audio"===t.type?b(t):"resource"===t.type?S(t):"resource_link"===t.type?$(t):void 0}function k(t,e,n,r){if(n)return h(e,{kind:TOOL_OUTCOME_KINDS.toolFailed});const o=x(t),i=Array.isArray(o)?o.flatMap(t=>{const e=N(t);return void 0===e?[]:[e]}):[],u=_(t);return void 0!==u&&i.length>0&&i.push({type:"text",text:`[structuredContent]\n${y(d(u))}`}),i.length>0?{type:"content",value:T(i)}:void 0!==u?m(u):r?"string"==typeof t?O(t):m(t):h(e,{kind:TOOL_OUTCOME_KINDS.success})}function M(e,o,i){const u=t(x(e));if(u.length>0)return g(u.join("\n"));const l=_(e);if(void 0!==l){const t=f(l),e=y(t);return e.length<=n?t:g(e)}const c=x(e);if(Array.isArray(c)&&c.length>0){const t=c.slice(0,r).flatMap(t=>{if(!s(t)||"string"!=typeof t.type)return[];const e="string"==typeof t.mimeType?` ${t.mimeType}`:"";return[`[${t.type}${e}]`]});if(t.length>0){const e=c.length-Math.min(c.length,r);return`${t.join("\n")}${e>0?`\n[另有 ${String(e)} 项未展示]`:""}`}}if(i){const t=f(e),r=y(t);return r.length<=n?t:g(r)}return o?"工具执行失败(无可展示的文本结果)":"工具执行完成(无可展示的文本结果)"}export function normalizeToolResult(t){const n=v(t),r=e(n.value),o=M(n.value,r,n.wrapped);return createToolResult(r?{kind:TOOL_OUTCOME_KINDS.toolFailed}:{kind:TOOL_OUTCOME_KINDS.success},o,{raw:t,model:k(n.value,o,r,n.wrapped)})}function C(t){return!(!s(t)||"string"!=typeof t.kind)&&Object.values(TOOL_OUTCOME_KINDS).some(e=>e===t.kind)}export function isNormalizedToolResult(t){return s(t)&&"raw"in t&&"model"in t&&"display"in t&&C(t.outcome)}export function readToolOutcome(t){return isNormalizedToolResult(t)?t.outcome:s(t)&&"execution-denied"===t.type?{kind:TOOL_OUTCOME_KINDS.userRejected,..."string"==typeof t.reason?{reason:t.reason}:{}}:s(t)&&"string"==typeof t.type&&t.type.startsWith("error")||E(t)?{kind:TOOL_OUTCOME_KINDS.toolFailed}:{kind:TOOL_OUTCOME_KINDS.success}}function E(t){return s(t)&&!0===t.isError}export function readIsError(t){return readToolOutcome(t).kind!==TOOL_OUTCOME_KINDS.success}export function readDisplayOutput(t){return isNormalizedToolResult(t)?t.display:s(t)&&"output"in t?t.output:t}export function toolResultToModelOutput(t){return t.model}
@@ -15,6 +15,7 @@ export interface SessionExecSettings {
15
15
  readonly workdir: string;
16
16
  readonly defaultYieldMs: number;
17
17
  readonly maxOutputTokens: number;
18
+ readonly env?: NodeJS.ProcessEnv;
18
19
  }
19
20
  export interface SessionExecDeps {
20
21
  readonly classifier?: CommandClassifier;
@@ -28,13 +29,13 @@ declare const execCommandInputSchema: z.ZodObject<{
28
29
  }, "strip", z.ZodTypeAny, {
29
30
  command: string;
30
31
  workdir?: string | undefined;
31
- yield_time_ms?: number | undefined;
32
32
  max_output_tokens?: number | undefined;
33
+ yield_time_ms?: number | undefined;
33
34
  }, {
34
35
  command: string;
35
36
  workdir?: string | undefined;
36
- yield_time_ms?: number | undefined;
37
37
  max_output_tokens?: number | undefined;
38
+ yield_time_ms?: number | undefined;
38
39
  }>;
39
40
  declare const execPollInputSchema: z.ZodObject<{
40
41
  session_id: z.ZodNumber;