@roll-agent/runtime 0.15.0 → 0.16.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 (81) hide show
  1. package/dist/approval/approval-gate.d.ts +1 -0
  2. package/dist/approval/approval-memory.d.ts +5 -0
  3. package/dist/approval/approval-memory.js +1 -0
  4. package/dist/bash/exec.js +1 -1
  5. package/dist/bash/format-result.d.ts +4 -0
  6. package/dist/bash/format-result.js +1 -1
  7. package/dist/bash/output-dump.d.ts +16 -0
  8. package/dist/bash/output-dump.js +1 -0
  9. package/dist/bash/profile.d.ts +5 -0
  10. package/dist/bash/profile.js +1 -1
  11. package/dist/bash/session/session-exec.js +1 -1
  12. package/dist/bash/session/types.d.ts +7 -0
  13. package/dist/bash/session/yield-loop.js +1 -1
  14. package/dist/bash/shell-pipe.d.ts +26 -0
  15. package/dist/bash/shell-pipe.js +1 -0
  16. package/dist/bash/truncate.js +1 -1
  17. package/dist/engine/agent-session.d.ts +28 -3
  18. package/dist/engine/agent-session.js +1 -1
  19. package/dist/engine/cancelled-turn-recovery.d.ts +1 -0
  20. package/dist/engine/cancelled-turn-recovery.js +1 -1
  21. package/dist/engine/capability-manifest.d.ts +3 -0
  22. package/dist/engine/capability-manifest.js +1 -1
  23. package/dist/engine/compactor.d.ts +3 -0
  24. package/dist/engine/compactor.js +1 -1
  25. package/dist/engine/context-window.js +1 -1
  26. package/dist/engine/conversation-engine.d.ts +3 -0
  27. package/dist/engine/conversation-engine.js +1 -1
  28. package/dist/engine/relocate-tool-images.js +1 -1
  29. package/dist/engine/system-prompt.d.ts +10 -0
  30. package/dist/engine/system-prompt.js +1 -1
  31. package/dist/server/runtime-protocol-adapter.js +1 -1
  32. package/dist/service/attachment-store.js +1 -1
  33. package/dist/service/runtime-service.d.ts +2 -1
  34. package/dist/service/runtime-service.js +1 -1
  35. package/dist/store/thread-store.d.ts +19 -2
  36. package/dist/store/thread-store.js +1 -1
  37. package/dist/tool-bridge/bash-tool.d.ts +3 -0
  38. package/dist/tool-bridge/bash-tool.js +1 -1
  39. package/dist/tool-bridge/bounded-param.d.ts +14 -0
  40. package/dist/tool-bridge/bounded-param.js +1 -0
  41. package/dist/tool-bridge/build-tools.d.ts +5 -0
  42. package/dist/tool-bridge/build-tools.js +1 -1
  43. package/dist/tool-bridge/file-tools/control-chars.d.ts +11 -0
  44. package/dist/tool-bridge/file-tools/control-chars.js +1 -0
  45. package/dist/tool-bridge/file-tools/edit-file-tool.d.ts +42 -0
  46. package/dist/tool-bridge/file-tools/edit-file-tool.js +1 -0
  47. package/dist/tool-bridge/file-tools/external-approval.d.ts +3 -0
  48. package/dist/tool-bridge/file-tools/external-approval.js +1 -0
  49. package/dist/tool-bridge/file-tools/file-io.d.ts +35 -0
  50. package/dist/tool-bridge/file-tools/file-io.js +1 -0
  51. package/dist/tool-bridge/file-tools/file-state-tracker.d.ts +11 -0
  52. package/dist/tool-bridge/file-tools/file-state-tracker.js +1 -0
  53. package/dist/tool-bridge/file-tools/glob-tool.d.ts +21 -0
  54. package/dist/tool-bridge/file-tools/glob-tool.js +1 -0
  55. package/dist/tool-bridge/file-tools/grep-tool.d.ts +33 -0
  56. package/dist/tool-bridge/file-tools/grep-tool.js +1 -0
  57. package/dist/tool-bridge/file-tools/index.d.ts +18 -0
  58. package/dist/tool-bridge/file-tools/index.js +1 -0
  59. package/dist/tool-bridge/file-tools/list-dir-tool.d.ts +18 -0
  60. package/dist/tool-bridge/file-tools/list-dir-tool.js +1 -0
  61. package/dist/tool-bridge/file-tools/match-pipeline.d.ts +22 -0
  62. package/dist/tool-bridge/file-tools/match-pipeline.js +1 -0
  63. package/dist/tool-bridge/file-tools/read-file-tool.d.ts +25 -0
  64. package/dist/tool-bridge/file-tools/read-file-tool.js +1 -0
  65. package/dist/tool-bridge/file-tools/rg-exec.d.ts +12 -0
  66. package/dist/tool-bridge/file-tools/rg-exec.js +1 -0
  67. package/dist/tool-bridge/file-tools/settings.d.ts +12 -0
  68. package/dist/tool-bridge/file-tools/settings.js +1 -0
  69. package/dist/tool-bridge/file-tools/text-normalize.d.ts +5 -0
  70. package/dist/tool-bridge/file-tools/text-normalize.js +1 -0
  71. package/dist/tool-bridge/file-tools/verifier-registry.d.ts +42 -0
  72. package/dist/tool-bridge/file-tools/verifier-registry.js +1 -0
  73. package/dist/tool-bridge/file-tools/verify-file-tool.d.ts +30 -0
  74. package/dist/tool-bridge/file-tools/verify-file-tool.js +1 -0
  75. package/dist/tool-bridge/file-tools/write-file-tool.d.ts +22 -0
  76. package/dist/tool-bridge/file-tools/write-file-tool.js +1 -0
  77. package/dist/tool-bridge/session-exec-tool.js +1 -1
  78. package/dist/tool-bridge/tool-execution-coordinator.d.ts +15 -2
  79. package/dist/tool-bridge/tool-execution-coordinator.js +1 -1
  80. package/dist/types/events.d.ts +1 -0
  81. package/package.json +4 -2
@@ -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 a}from"node:crypto";import{DatabaseSync as i}from"node:sqlite";import{modelMessageSchema as o}from"ai";import{RUNTIME_V13_MAX_DURABLE_EVENT_RECORD_BYTES as d,RUNTIME_V13_MAX_DURABLE_EVENT_RECORDS as c,runtimeDurableEventV13Schema as E,runtimeEventCursorSchema as _,runtimeEventIdSchema as h,timestampSchema as u}from"@roll-agent/protocol";import{parsePersistedToolExecutionRecord as T,prepareToolExecutionRecordForPersistence as p,redactSecretText as m}from"../tool-bridge/tool-execution-record.js";import{COMPACTION_CHECKPOINT_VERSION as l,COMPACTION_TRANSCRIPT_COMPLETENESS as N,TRANSCRIPT_MESSAGE_PROVENANCES as R,UnsupportedCompactionCheckpointVersionError as S,archivedTranscriptMessageSchema as g,createCompactionCheckpoint as I,createCompactionCheckpointDraft as O,parseCompactionCheckpoint as L}from"../engine/compaction-checkpoint.js";import{legacyCompactionTranscriptFragmentsSchema as v}from"../engine/compaction-semantic-state.js";import{createLegacyCompactionTranscriptMessage as C}from"../engine/compactor.js";import{ACTIVE_TOOL_PROTOCOL_REPAIR_STATUS as f,repairActiveToolProtocol as x}from"../engine/tool-protocol-repair.js";import{sanitizePersistedExplicitSkillCheckpoint as A}from"../engine/explicit-skill-context.js";const q=5,b=100,M=500,w=50,D=500,U=100,F=15e3;export const TOOL_EXECUTION_RETENTION_POLICY={maxBytesPerThread:16777216,maxRecordsPerThread:2e3,maxAgeMs:2592e6};export const RUNTIME_EVENT_RETENTION_POLICY={maxBytesPerThread:d,maxRecordsPerThread:c,maxAgeMs:2592e6};export class RuntimeEventCursorExpiredError extends Error{cursor;constructor(e){super("Runtime event cursor is older than the retained event prefix"),this.name="RuntimeEventCursorExpiredError",this.cursor=e}}export class RuntimeEventCursorGapError extends Error{cursor;constructor(e){super("Runtime event cursor does not belong to the current Thread event log"),this.name="RuntimeEventCursorGapError",this.cursor=e}}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 k(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 y(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>l)throw new S(e.schema_version);if(!Number.isInteger(e.schema_version))throw new Error("Invalid persisted compaction checkpoint schema version");const t=L(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 X(e){return{id:e.id,title:e.title??void 0,model:e.model??void 0,createdAt:e.created_at,updatedAt:e.updated_at}}function H(){return h.parse(a())}function P(e,t,n){return _.parse(`rte1:${e}:${String(t)}:${n}`)}function B(e){const t=_.safeParse(e);if(!t.success)return;const n=t.data.split(":"),r=Number(n[2]),s=h.safeParse(n[3]);return void 0===n[1]||!Number.isSafeInteger(r)||r<0||!s.success?void 0:{eventLogId:n[1],threadSequence:r,eventId:s.data}}function G(e){return new Date(Date.parse(u.parse(e))).toISOString()}function Y(e,t,n){const r=h.parse(n.event_id);return{eventId:r,cursor:P(e,n.thread_sequence,r),threadSequence:n.thread_sequence,threadId:t,...null===n.turn_id?{}:{turnId:n.turn_id},timestamp:n.event_timestamp,event:E.parse(JSON.parse(n.event_json))}}export class ThreadStore{db;constructor(r=defaultThreadsDir()){const a=expandTilde(r);t(a)||n(a,{recursive:!0,mode:448}),"win32"!==process.platform&&e(a,448);const o=s(a,"threads.db");if(this.db=new i(o),"win32"!==process.platform)try{e(o,384)}catch(e){throw this.db.close(),e}this.init()}init(){this.db.exec(`PRAGMA busy_timeout = ${String(F)};\n PRAGMA foreign_keys = ON;\n PRAGMA secure_delete = ON;`);const e=this.db.prepare("PRAGMA user_version").get();if(e.user_version>5)throw new Error(`ThreadStore schema v${String(e.user_version)} 高于当前支持的 v${String(5)}`);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 );"),k(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);\n CREATE TABLE IF NOT EXISTS thread_runtime_event_state (\n thread_id TEXT PRIMARY KEY,\n event_log_id TEXT NOT NULL UNIQUE,\n next_sequence INTEGER NOT NULL CHECK (next_sequence >= 0),\n retained_from_sequence INTEGER NOT NULL\n CHECK (retained_from_sequence >= 0 AND retained_from_sequence <= next_sequence),\n retained_predecessor_event_id TEXT,\n latest_event_id TEXT,\n retained_count INTEGER NOT NULL CHECK (retained_count >= 0),\n retained_bytes INTEGER NOT NULL CHECK (retained_bytes >= 0),\n CHECK (\n (next_sequence = 0\n AND retained_from_sequence = 0\n AND retained_predecessor_event_id IS NULL\n AND latest_event_id IS NULL)\n OR (next_sequence > 0 AND latest_event_id IS NOT NULL)\n ),\n CHECK (\n (retained_from_sequence = 0 AND retained_predecessor_event_id IS NULL)\n OR (retained_from_sequence > 0 AND retained_predecessor_event_id IS NOT NULL)\n ),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS runtime_events (\n thread_id TEXT NOT NULL,\n thread_sequence INTEGER NOT NULL CHECK (thread_sequence >= 0),\n event_id TEXT NOT NULL UNIQUE,\n turn_id TEXT,\n event_timestamp TEXT NOT NULL,\n event_json TEXT NOT NULL,\n size_bytes INTEGER NOT NULL CHECK (size_bytes > 0),\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, thread_sequence),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE INDEX IF NOT EXISTS idx_runtime_events_thread_created\n ON runtime_events(thread_id, created_at, thread_sequence);"),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.initializeMissingRuntimeEventStatesInTransaction(),this.enforceAllToolExecutionRetentionInTransaction((new Date).toISOString()),this.enforceAllRuntimeEventRetentionInTransaction((new Date).toISOString()),this.db.exec(`PRAGMA user_version = ${String(5)};`),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=T(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=o.parse(JSON.parse(t.message_json)),r=A(e);n.run(JSON.stringify(r),t.storage_rowid)}}}initializeMissingRuntimeEventStatesInTransaction(){const e=this.db.prepare("SELECT threads.id\n FROM threads\n LEFT JOIN thread_runtime_event_state\n ON thread_runtime_event_state.thread_id = threads.id\n WHERE thread_runtime_event_state.thread_id IS NULL").all(),t=this.db.prepare("INSERT INTO thread_runtime_event_state\n (thread_id, event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id, retained_count, retained_bytes)\n VALUES (?, ?, 0, 0, NULL, NULL, 0, 0)");for(const n of e)t.run(n.id,a())}enforceAllRuntimeEventRetentionInTransaction(e){const t=this.db.prepare("SELECT thread_id AS id FROM thread_runtime_event_state").all();for(const n of t)this.enforceRuntimeEventRetentionInTransaction(n.id,e)}createThread(e={}){const t=e.id??a(),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,N[0]),this.db.prepare("INSERT INTO thread_tool_execution_state (thread_id, next_sequence) VALUES (?, 0)").run(t),this.db.prepare("INSERT INTO thread_runtime_event_state\n (thread_id, event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id, retained_count, retained_bytes)\n VALUES (?, ?, 0, 0, NULL, NULL, 0, 0)").run(t,a()),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?X(t):void 0}listThreads(){return this.db.prepare("SELECT * FROM threads ORDER BY updated_at DESC, rowid DESC").all().map(X)}countMessages(e){return this.db.prepare("SELECT COUNT(*) AS n FROM messages WHERE thread_id = ?").get(e).n}countTranscriptMessages(e){return this.db.prepare("SELECT COUNT(*) AS n FROM transcript_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)}appendRuntimeEvent(e){const t=E.parse(e.event),n=JSON.stringify(t),r=G(e.timestamp),s=Buffer.byteLength(n,"utf8")+Buffer.byteLength(r,"utf8")+Buffer.byteLength(e.turnId??"","utf8");if(s>RUNTIME_EVENT_RETENTION_POLICY.maxBytesPerThread)throw new Error("Runtime event exceeds the per-Thread retained byte limit");this.db.exec("BEGIN IMMEDIATE");try{const a=this.db.prepare("SELECT event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id,\n retained_count, retained_bytes\n FROM thread_runtime_event_state\n WHERE thread_id = ?").get(e.threadId);if(void 0===a)throw new Error(`Thread "${e.threadId}" 不存在或缺少 Runtime event state`);const i=H(),o=a.next_sequence;return this.db.prepare("INSERT INTO runtime_events\n (thread_id, thread_sequence, event_id, turn_id, event_timestamp,\n event_json, size_bytes, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)").run(e.threadId,o,i,e.turnId??null,r,n,s,r),this.db.prepare("UPDATE thread_runtime_event_state\n SET next_sequence = ?, latest_event_id = ?,\n retained_count = retained_count + 1,\n retained_bytes = retained_bytes + ?\n WHERE thread_id = ?").run(o+1,i,s,e.threadId),this.enforceRuntimeEventRetentionInTransaction(e.threadId,r),this.db.exec("COMMIT"),{eventId:i,cursor:P(a.event_log_id,o,i),threadSequence:o,threadId:e.threadId,...void 0===e.turnId?{}:{turnId:e.turnId},timestamp:r,event:t}}catch(e){throw this.db.exec("ROLLBACK"),e}}getRuntimeEventCursor(e){const t=this.db.prepare("SELECT event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id,\n retained_count, retained_bytes\n FROM thread_runtime_event_state\n WHERE thread_id = ?").get(e);if(void 0===t)throw new Error(`Thread "${e}" 不存在或缺少 Runtime event state`);return 0===t.next_sequence||null===t.latest_event_id?null:P(t.event_log_id,t.next_sequence-1,h.parse(t.latest_event_id))}resumeRuntimeEvents(e,t){const n=null===t?void 0:B(t);if(null!==t&&void 0===n)throw new RuntimeEventCursorGapError(t);this.db.exec("BEGIN IMMEDIATE");try{this.enforceRuntimeEventRetentionInTransaction(e,(new Date).toISOString());const r=this.db.prepare("SELECT event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id,\n retained_count, retained_bytes\n FROM thread_runtime_event_state\n WHERE thread_id = ?").get(e);if(void 0===r)throw new Error(`Thread "${e}" 不存在或缺少 Runtime event state`);if(null===t&&r.retained_from_sequence>0)throw new RuntimeEventCursorExpiredError(t);if(null!==t&&void 0!==n&&this.validateRuntimeEventCursor(r,t,n,e),0===r.next_sequence||null===r.latest_event_id)return this.db.exec("COMMIT"),{events:[],throughCursor:null,replayedCount:0};const s=r.next_sequence-1,a=n?.threadSequence??-1,i=this.db.prepare("SELECT thread_sequence, event_id, turn_id, event_timestamp, event_json\n FROM runtime_events\n WHERE thread_id = ?\n AND thread_sequence > ?\n AND thread_sequence <= ?\n ORDER BY thread_sequence ASC").all(e,a,s).map(t=>Y(r.event_log_id,e,t)),o=P(r.event_log_id,s,h.parse(r.latest_event_id));return this.db.exec("COMMIT"),{events:i,throughCursor:o,replayedCount:i.length}}catch(e){throw e instanceof RuntimeEventCursorExpiredError||e instanceof RuntimeEventCursorGapError?this.db.exec("COMMIT"):this.db.exec("ROLLBACK"),e}}countRuntimeEvents(e){return this.db.prepare("SELECT COUNT(*) AS count FROM runtime_events WHERE thread_id = ?").get(e).count}validateRuntimeEventCursor(e,t,n,r){if(n.eventLogId!==e.event_log_id||n.threadSequence>=e.next_sequence)throw new RuntimeEventCursorGapError(t);const s=e.retained_from_sequence-1;if(n.threadSequence<s)throw new RuntimeEventCursorExpiredError(t);if(n.threadSequence===s){if(n.eventId!==e.retained_predecessor_event_id)throw new RuntimeEventCursorGapError(t);return}const a=this.db.prepare("SELECT thread_sequence, event_id\n FROM runtime_events\n WHERE thread_id = ? AND thread_sequence = ?").get(r,n.threadSequence);if(void 0===a||a.event_id!==n.eventId)throw new RuntimeEventCursorGapError(t)}enforceRuntimeEventRetentionInTransaction(e,t){const n=this.db.prepare("SELECT event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id,\n retained_count, retained_bytes\n FROM thread_runtime_event_state\n WHERE thread_id = ?").get(e);if(void 0===n||0===n.retained_count)return;let r=n.retained_from_sequence;if(n.retained_count>RUNTIME_EVENT_RETENTION_POLICY.maxRecordsPerThread){const t=this.db.prepare("SELECT thread_sequence AS keep_from\n FROM runtime_events\n WHERE thread_id = ?\n ORDER BY thread_sequence DESC\n LIMIT 1 OFFSET ?").get(e,RUNTIME_EVENT_RETENTION_POLICY.maxRecordsPerThread-1);r=Math.max(r,t?.keep_from??n.next_sequence)}if(n.retained_bytes>RUNTIME_EVENT_RETENTION_POLICY.maxBytesPerThread){const t=this.db.prepare("SELECT MIN(thread_sequence) AS keep_from\n FROM (\n SELECT thread_sequence,\n SUM(size_bytes) OVER (\n ORDER BY thread_sequence DESC\n ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n ) AS retained_bytes\n FROM runtime_events\n WHERE thread_id = ?\n )\n WHERE retained_bytes <= ?").get(e,RUNTIME_EVENT_RETENTION_POLICY.maxBytesPerThread);r=Math.max(r,t.keep_from??n.next_sequence)}const s=new Date(Date.parse(t)-RUNTIME_EVENT_RETENTION_POLICY.maxAgeMs).toISOString(),a=this.db.prepare("SELECT MIN(thread_sequence) AS keep_from\n FROM runtime_events\n WHERE thread_id = ? AND created_at >= ?").get(e,s);if(r=Math.max(r,a.keep_from??n.next_sequence),r<=n.retained_from_sequence)return;const i=this.db.prepare("SELECT thread_sequence, event_id\n FROM runtime_events\n WHERE thread_id = ? AND thread_sequence = ?").get(e,r-1);if(void 0===i)throw new Error("Runtime event retention boundary is not a continuous retained prefix");const o=this.db.prepare("SELECT COUNT(*) AS removed_count, SUM(size_bytes) AS removed_bytes\n FROM runtime_events\n WHERE thread_id = ? AND thread_sequence < ?").get(e,r);this.db.prepare("DELETE FROM runtime_events WHERE thread_id = ? AND thread_sequence < ?").run(e,r),this.db.prepare("UPDATE thread_runtime_event_state\n SET retained_from_sequence = ?, retained_predecessor_event_id = ?,\n retained_count = retained_count - ?,\n retained_bytes = retained_bytes - ?\n WHERE thread_id = ?").run(r,i.event_id,o.removed_count,o.removed_bytes??0,e)}appendMessages(e,t){if(0===t.length)return;if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=t.map(e=>A(o.parse(e))),r=(new Date).toISOString(),s=this.db.prepare("INSERT INTO messages (thread_id, idx, role, content_json, created_at) VALUES (?, ?, ?, ?, ?)"),a=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),i=this.db.prepare("SELECT COALESCE(MAX(sequence), -1) AS maxSequence FROM transcript_messages WHERE thread_id = ?").get(e);let o=t.maxIdx+1,d=i.maxSequence+1;for(const t of n){const n=JSON.stringify(t);s.run(e,o,t.role,n,r),a.run(e,d,t.role,n,R[0],r),o+=1,d+=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=>o.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=>o.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 a=A(t);r.run(e,s,a.role,JSON.stringify(a),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=p(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 a=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,a,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(a+1,e),this.enforceToolExecutionRetentionInTransaction(e,s),this.db.prepare("UPDATE threads SET updated_at = ? WHERE id = ?").run(s,e),this.db.exec("COMMIT"),a}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 a=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(a.length>0){const t=this.db.prepare("DELETE FROM tool_executions WHERE thread_id = ? AND sequence = ?");for(const n of a)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??b;if(!Number.isInteger(n)||n<-1)throw new Error("afterSequence 必须是大于等于 -1 的整数");if(!Number.isInteger(s)||s<1||s>M)throw new Error(`limit 必须是 1-${String(M)} 之间的整数`);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=>({...T(JSON.parse(e.record_json)),sequence:e.sequence}))}listRecentToolExecutions(e,t={}){const n=t.beforeSequence??Number.MAX_SAFE_INTEGER,r=t.limit??b;this.validateRecentPage(n,r,M);const s=this.db.prepare("SELECT sequence, record_json\n FROM tool_executions\n WHERE thread_id = ?\n AND sequence < ?\n ORDER BY sequence DESC\n LIMIT ?").all(e,n,r+1),a=s.length>r,i=s.slice(0,r).map(e=>({...T(JSON.parse(e.record_json)),sequence:e.sequence})).reverse();return{entries:i,nextBeforeSequence:a?i[0]?.sequence:void 0}}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{...T(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===N[0]?N[0]:N[1]}listTranscriptMessages(e,t={}){const n=t.afterSequence??-1,r=t.throughSequence??Number.MAX_SAFE_INTEGER,s=t.limit??w;if(this.validateTranscriptRange(n,r,s,D),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=>g.parse({sequence:e.sequence,provenance:e.provenance,createdAt:e.created_at,message:JSON.parse(e.message_json)}))}listRecentTranscriptMessages(e,t={}){const n=t.beforeSequence??Number.MAX_SAFE_INTEGER,r=t.limit??w;this.validateRecentPage(n,r,D);const s=this.db.prepare("SELECT sequence, provenance, created_at, message_json\n FROM transcript_messages\n WHERE thread_id = ?\n AND sequence < ?\n ORDER BY sequence DESC\n LIMIT ?").all(e,n,r+1),a=s.length>r,i=s.slice(0,r).map(e=>g.parse({sequence:e.sequence,provenance:e.provenance,createdAt:e.created_at,message:JSON.parse(e.message_json)})).reverse();return{entries:i,nextBeforeSequence:a?i[0]?.sequence:void 0}}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 S)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 S)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 S)throw e;return}}loadSessionState(e){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);return{messages:x(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=>o.parse(e)),r=t.expectedActiveMessages.map(e=>o.parse(e)),s=x(n);if(s.status!==f.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 a=O(t.draft),i=v.parse(t.legacySnapshotTranscriptFragments??[]),d=v.parse(i.map(e=>m(e))).map(e=>o.parse(C(e))),c=(new Date).toISOString();this.db.exec("BEGIN IMMEDIATE");try{const s=x(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 i=this.getLatestCheckpoint(e);if(i?.id!==t.expectedLatestCheckpointId)throw new Error("Compaction checkpoint ancestry changed after the draft snapshot was captured");const o=this.getLatestSummaryCheckpoint(e),E=y(a.summary,o),_=this.getTranscriptCompleteness(e),h=O({...a,summary:E,..._===N[1]&&"valid"===a.toolState.integrityStatus?{toolState:{...a.toolState,integrityStatus:"sanitized"}}:{}}),u=this.db.prepare("SELECT COALESCE(MAX(generation), 0) AS maxGeneration\n FROM compaction_checkpoints\n WHERE thread_id = ?").get(e),T=this.maxTranscriptMessageSequence(e),p=this.maxToolExecutionSequence(e),m=t.evidenceWatermarks.transcriptMessagesThroughSequence,l=t.evidenceWatermarks.toolExecutionsThroughSequence,S=i?.transcript.messages.throughSequence??-1,g=i?.transcript.toolExecutions.throughSequence??-1,L=2===i?.version?i.semanticEvidence.messagesThroughSequence:-1,v=2===i?.version?i.semanticEvidence.toolExecutionsThroughSequence:-1;if(d.length>0&&1!==i?.version)throw new Error("Legacy snapshot transcript archive requires a V1 parent checkpoint");if(!Number.isInteger(m)||m<S||m>T)throw new Error(`Compaction transcript message watermark ${String(m)} is outside the available range ${String(S)}-${String(T)}`);if(!Number.isInteger(t.semanticEvidenceWatermarks.messagesThroughSequence)||t.semanticEvidenceWatermarks.messagesThroughSequence<L||t.semanticEvidenceWatermarks.messagesThroughSequence>m)throw new Error(`Compaction semantic message watermark ${String(t.semanticEvidenceWatermarks.messagesThroughSequence)} is outside the available range ${String(L)}-${String(m)}`);if(!Number.isInteger(t.semanticEvidenceWatermarks.toolExecutionsThroughSequence)||t.semanticEvidenceWatermarks.toolExecutionsThroughSequence<v||t.semanticEvidenceWatermarks.toolExecutionsThroughSequence>l)throw new Error(`Compaction semantic Tool watermark ${String(t.semanticEvidenceWatermarks.toolExecutionsThroughSequence)} is outside the available range ${String(v)}-${String(l)}`);if(!Number.isInteger(l)||l<g||l>p)throw new Error(`Compaction tool execution watermark ${String(l)} is outside the available range ${String(g)}-${String(p)}`);let C=m;if(d.length>0){if(m!==T)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=T+1;for(const r of d)t.run(e,n,r.role,JSON.stringify(r),R[1],c),n+=1;C=n-1}const f={messages:{fromSequenceExclusive:S,throughSequence:C},toolExecutions:{fromSequenceExclusive:g,throughSequence:l},completeness:_},A=I({draft:h,semanticState:t.semanticState,semanticEvidence:t.semanticEvidenceWatermarks,generation:u.maxGeneration+1,transcript:f,...void 0!==i?{previousCheckpointId:i.id}:{},createdAt:c});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(A.id,e,A.generation,A.version,A.transcript.messages.fromSequenceExclusive,A.transcript.messages.throughSequence,A.transcript.toolExecutions.fromSequenceExclusive,A.transcript.toolExecutions.throughSequence,JSON.stringify(A),A.createdAt),this.replaceMessagesInTransaction(e,n,c),this.db.exec("COMMIT"),A}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??w,s="message"===t.kind?n.transcript.messages:n.transcript.toolExecutions,a=Math.max(t.afterSequence??s.fromSequenceExclusive,s.fromSequenceExclusive);this.validateTranscriptRange(a,s.throughSequence,r,U);const i="message"===t.kind?this.listTranscriptMessages(e,{afterSequence:a,throughSequence:s.throughSequence,limit:r+1}).map(e=>({kind:"message",...e})):this.listToolExecutions(e,{afterSequence:a,throughSequence:s.throughSequence,limit:Math.min(r+1,M)}).map(e=>({kind:"tool_execution",...e})),o=i.length>r,d=o?i.slice(0,r):i,c=d.at(-1)?.sequence;return{checkpointId:n.id,kind:t.kind,entries:d,...o&&void 0!==c?{nextAfterSequence:c}:{},...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=N[1],r=t.transcript.completeness===n||this.getTranscriptCompleteness(e)===n?n:N[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)} 之间的整数`)}validateRecentPage(e,t,n){if(!Number.isSafeInteger(e)||e<0||e>Number.MAX_SAFE_INTEGER)throw new Error("beforeSequence 必须是非负安全整数");if(!Number.isInteger(t)||t<1||t>n)throw new Error(`limit 必须是 1-${String(n)} 之间的整数`)}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 i}from"node:sqlite";import{modelMessageSchema as a}from"ai";import{RUNTIME_V13_MAX_DURABLE_EVENT_RECORD_BYTES as c,RUNTIME_V13_MAX_DURABLE_EVENT_RECORDS as d,runtimeDurableEventV13Schema as E,runtimeEventCursorSchema as _,runtimeEventIdSchema as u,timestampSchema as h}from"@roll-agent/protocol";import{parsePersistedToolExecutionRecord as T,prepareToolExecutionRecordForPersistence as p,redactSecretText as l}from"../tool-bridge/tool-execution-record.js";import{COMPACTION_CHECKPOINT_VERSION as m,COMPACTION_TRANSCRIPT_COMPLETENESS as N,TRANSCRIPT_MESSAGE_PROVENANCES as R,UnsupportedCompactionCheckpointVersionError as S,archivedTranscriptMessageSchema as g,createCompactionCheckpoint as I,createCompactionCheckpointDraft as O,parseCompactionCheckpoint as L}from"../engine/compaction-checkpoint.js";import{legacyCompactionTranscriptFragmentsSchema as v}from"../engine/compaction-semantic-state.js";import{createLegacyCompactionTranscriptMessage as x}from"../engine/compactor.js";import{ACTIVE_TOOL_PROTOCOL_REPAIR_STATUS as C,repairActiveToolProtocol as f}from"../engine/tool-protocol-repair.js";import{sanitizePersistedExplicitSkillCheckpoint as A}from"../engine/explicit-skill-context.js";const q=6,b=100,M=500,w=50,D=500,U=100,F=15e3;export const TOOL_EXECUTION_RETENTION_POLICY={maxBytesPerThread:16777216,maxRecordsPerThread:2e3,maxAgeMs:2592e6};export const RUNTIME_EVENT_RETENTION_POLICY={maxBytesPerThread:c,maxRecordsPerThread:d,maxAgeMs:2592e6};export class RuntimeEventCursorExpiredError extends Error{cursor;constructor(e){super("Runtime event cursor is older than the retained event prefix"),this.name="RuntimeEventCursorExpiredError",this.cursor=e}}export class RuntimeEventCursorGapError extends Error{cursor;constructor(e){super("Runtime event cursor does not belong to the current Thread event log"),this.name="RuntimeEventCursorGapError",this.cursor=e}}export const TRANSCRIPT_ENTRY_KINDS=["message","tool_execution"];export const TOOL_EXECUTION_CONTEXT_REPRESENTATIONS={rawTranscript:"raw_transcript",recoveryEvidence:"recovery_evidence"};const y="legacy_assumed";export function expandTilde(e){return e.startsWith("~/")?s(r(),e.slice(2)):e}export function defaultThreadsDir(){return s(r(),".roll-agent","threads")}function X(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 k(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>m)throw new S(e.schema_version);if(!Number.isInteger(e.schema_version))throw new Error("Invalid persisted compaction checkpoint schema version");const t=L(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 H(e){return{id:e.id,title:e.title??void 0,model:e.model??void 0,createdAt:e.created_at,updatedAt:e.updated_at}}function P(){return u.parse(o())}function G(e,t,n){return _.parse(`rte1:${e}:${String(t)}:${n}`)}function B(e){const t=_.safeParse(e);if(!t.success)return;const n=t.data.split(":"),r=Number(n[2]),s=u.safeParse(n[3]);return void 0===n[1]||!Number.isSafeInteger(r)||r<0||!s.success?void 0:{eventLogId:n[1],threadSequence:r,eventId:s.data}}function Y(e){return new Date(Date.parse(h.parse(e))).toISOString()}function j(e,t,n){const r=u.parse(n.event_id);return{eventId:r,cursor:G(e,n.thread_sequence,r),threadSequence:n.thread_sequence,threadId:t,...null===n.turn_id?{}:{turnId:n.turn_id},timestamp:n.event_timestamp,event:E.parse(JSON.parse(n.event_json))}}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 a=s(o,"threads.db");if(this.db=new i(a),"win32"!==process.platform)try{e(a,384)}catch(e){throw this.db.close(),e}this.init()}init(){this.db.exec(`PRAGMA busy_timeout = ${String(F)};\n PRAGMA foreign_keys = ON;\n PRAGMA secure_delete = ON;`);const e=this.db.prepare("PRAGMA user_version").get();if(e.user_version>6)throw new Error(`ThreadStore schema v${String(e.user_version)} 高于当前支持的 v${String(6)}`);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 );"),X(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 tool_execution_context_coverage (\n thread_id TEXT NOT NULL,\n execution_id TEXT NOT NULL,\n representation TEXT NOT NULL\n CHECK (representation IN ('raw_transcript', 'recovery_evidence', 'legacy_assumed')),\n transcript_sequence INTEGER,\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, execution_id),\n CHECK (\n (representation = 'legacy_assumed' AND transcript_sequence IS NULL)\n OR (representation <> 'legacy_assumed' AND transcript_sequence IS NOT NULL)\n ),\n FOREIGN KEY (thread_id, execution_id)\n REFERENCES tool_executions(thread_id, id) ON DELETE CASCADE,\n FOREIGN KEY (thread_id, transcript_sequence)\n REFERENCES transcript_messages(thread_id, sequence) 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);\n CREATE TABLE IF NOT EXISTS thread_runtime_event_state (\n thread_id TEXT PRIMARY KEY,\n event_log_id TEXT NOT NULL UNIQUE,\n next_sequence INTEGER NOT NULL CHECK (next_sequence >= 0),\n retained_from_sequence INTEGER NOT NULL\n CHECK (retained_from_sequence >= 0 AND retained_from_sequence <= next_sequence),\n retained_predecessor_event_id TEXT,\n latest_event_id TEXT,\n retained_count INTEGER NOT NULL CHECK (retained_count >= 0),\n retained_bytes INTEGER NOT NULL CHECK (retained_bytes >= 0),\n CHECK (\n (next_sequence = 0\n AND retained_from_sequence = 0\n AND retained_predecessor_event_id IS NULL\n AND latest_event_id IS NULL)\n OR (next_sequence > 0 AND latest_event_id IS NOT NULL)\n ),\n CHECK (\n (retained_from_sequence = 0 AND retained_predecessor_event_id IS NULL)\n OR (retained_from_sequence > 0 AND retained_predecessor_event_id IS NOT NULL)\n ),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS runtime_events (\n thread_id TEXT NOT NULL,\n thread_sequence INTEGER NOT NULL CHECK (thread_sequence >= 0),\n event_id TEXT NOT NULL UNIQUE,\n turn_id TEXT,\n event_timestamp TEXT NOT NULL,\n event_json TEXT NOT NULL,\n size_bytes INTEGER NOT NULL CHECK (size_bytes > 0),\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, thread_sequence),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE INDEX IF NOT EXISTS idx_runtime_events_thread_created\n ON runtime_events(thread_id, created_at, thread_sequence);"),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()),e.user_version<6&&this.db.prepare("INSERT OR IGNORE INTO tool_execution_context_coverage\n (thread_id, execution_id, representation, transcript_sequence, created_at)\n SELECT thread_id, id, ?, NULL, created_at\n FROM tool_executions").run(y),this.initializeMissingRuntimeEventStatesInTransaction(),this.enforceAllToolExecutionRetentionInTransaction((new Date).toISOString()),this.enforceAllRuntimeEventRetentionInTransaction((new Date).toISOString()),this.db.exec(`PRAGMA user_version = ${String(6)};`),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=T(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=a.parse(JSON.parse(t.message_json)),r=A(e);n.run(JSON.stringify(r),t.storage_rowid)}}}initializeMissingRuntimeEventStatesInTransaction(){const e=this.db.prepare("SELECT threads.id\n FROM threads\n LEFT JOIN thread_runtime_event_state\n ON thread_runtime_event_state.thread_id = threads.id\n WHERE thread_runtime_event_state.thread_id IS NULL").all(),t=this.db.prepare("INSERT INTO thread_runtime_event_state\n (thread_id, event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id, retained_count, retained_bytes)\n VALUES (?, ?, 0, 0, NULL, NULL, 0, 0)");for(const n of e)t.run(n.id,o())}enforceAllRuntimeEventRetentionInTransaction(e){const t=this.db.prepare("SELECT thread_id AS id FROM thread_runtime_event_state").all();for(const n of t)this.enforceRuntimeEventRetentionInTransaction(n.id,e)}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,N[0]),this.db.prepare("INSERT INTO thread_tool_execution_state (thread_id, next_sequence) VALUES (?, 0)").run(t),this.db.prepare("INSERT INTO thread_runtime_event_state\n (thread_id, event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id, retained_count, retained_bytes)\n VALUES (?, ?, 0, 0, NULL, NULL, 0, 0)").run(t,o()),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?H(t):void 0}listThreads(){return this.db.prepare("SELECT * FROM threads ORDER BY updated_at DESC, rowid DESC").all().map(H)}countMessages(e){return this.db.prepare("SELECT COUNT(*) AS n FROM messages WHERE thread_id = ?").get(e).n}countTranscriptMessages(e){return this.db.prepare("SELECT COUNT(*) AS n FROM transcript_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)}appendRuntimeEvent(e){const t=E.parse(e.event),n=JSON.stringify(t),r=Y(e.timestamp),s=Buffer.byteLength(n,"utf8")+Buffer.byteLength(r,"utf8")+Buffer.byteLength(e.turnId??"","utf8");if(s>RUNTIME_EVENT_RETENTION_POLICY.maxBytesPerThread)throw new Error("Runtime event exceeds the per-Thread retained byte limit");this.db.exec("BEGIN IMMEDIATE");try{const o=this.db.prepare("SELECT event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id,\n retained_count, retained_bytes\n FROM thread_runtime_event_state\n WHERE thread_id = ?").get(e.threadId);if(void 0===o)throw new Error(`Thread "${e.threadId}" 不存在或缺少 Runtime event state`);const i=P(),a=o.next_sequence;return this.db.prepare("INSERT INTO runtime_events\n (thread_id, thread_sequence, event_id, turn_id, event_timestamp,\n event_json, size_bytes, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)").run(e.threadId,a,i,e.turnId??null,r,n,s,r),this.db.prepare("UPDATE thread_runtime_event_state\n SET next_sequence = ?, latest_event_id = ?,\n retained_count = retained_count + 1,\n retained_bytes = retained_bytes + ?\n WHERE thread_id = ?").run(a+1,i,s,e.threadId),this.enforceRuntimeEventRetentionInTransaction(e.threadId,r),this.db.exec("COMMIT"),{eventId:i,cursor:G(o.event_log_id,a,i),threadSequence:a,threadId:e.threadId,...void 0===e.turnId?{}:{turnId:e.turnId},timestamp:r,event:t}}catch(e){throw this.db.exec("ROLLBACK"),e}}getRuntimeEventCursor(e){const t=this.db.prepare("SELECT event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id,\n retained_count, retained_bytes\n FROM thread_runtime_event_state\n WHERE thread_id = ?").get(e);if(void 0===t)throw new Error(`Thread "${e}" 不存在或缺少 Runtime event state`);return 0===t.next_sequence||null===t.latest_event_id?null:G(t.event_log_id,t.next_sequence-1,u.parse(t.latest_event_id))}resumeRuntimeEvents(e,t){const n=null===t?void 0:B(t);if(null!==t&&void 0===n)throw new RuntimeEventCursorGapError(t);this.db.exec("BEGIN IMMEDIATE");try{this.enforceRuntimeEventRetentionInTransaction(e,(new Date).toISOString());const r=this.db.prepare("SELECT event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id,\n retained_count, retained_bytes\n FROM thread_runtime_event_state\n WHERE thread_id = ?").get(e);if(void 0===r)throw new Error(`Thread "${e}" 不存在或缺少 Runtime event state`);if(null===t&&r.retained_from_sequence>0)throw new RuntimeEventCursorExpiredError(t);if(null!==t&&void 0!==n&&this.validateRuntimeEventCursor(r,t,n,e),0===r.next_sequence||null===r.latest_event_id)return this.db.exec("COMMIT"),{events:[],throughCursor:null,replayedCount:0};const s=r.next_sequence-1,o=n?.threadSequence??-1,i=this.db.prepare("SELECT thread_sequence, event_id, turn_id, event_timestamp, event_json\n FROM runtime_events\n WHERE thread_id = ?\n AND thread_sequence > ?\n AND thread_sequence <= ?\n ORDER BY thread_sequence ASC").all(e,o,s).map(t=>j(r.event_log_id,e,t)),a=G(r.event_log_id,s,u.parse(r.latest_event_id));return this.db.exec("COMMIT"),{events:i,throughCursor:a,replayedCount:i.length}}catch(e){throw e instanceof RuntimeEventCursorExpiredError||e instanceof RuntimeEventCursorGapError?this.db.exec("COMMIT"):this.db.exec("ROLLBACK"),e}}countRuntimeEvents(e){return this.db.prepare("SELECT COUNT(*) AS count FROM runtime_events WHERE thread_id = ?").get(e).count}validateRuntimeEventCursor(e,t,n,r){if(n.eventLogId!==e.event_log_id||n.threadSequence>=e.next_sequence)throw new RuntimeEventCursorGapError(t);const s=e.retained_from_sequence-1;if(n.threadSequence<s)throw new RuntimeEventCursorExpiredError(t);if(n.threadSequence===s){if(n.eventId!==e.retained_predecessor_event_id)throw new RuntimeEventCursorGapError(t);return}const o=this.db.prepare("SELECT thread_sequence, event_id\n FROM runtime_events\n WHERE thread_id = ? AND thread_sequence = ?").get(r,n.threadSequence);if(void 0===o||o.event_id!==n.eventId)throw new RuntimeEventCursorGapError(t)}enforceRuntimeEventRetentionInTransaction(e,t){const n=this.db.prepare("SELECT event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id,\n retained_count, retained_bytes\n FROM thread_runtime_event_state\n WHERE thread_id = ?").get(e);if(void 0===n||0===n.retained_count)return;let r=n.retained_from_sequence;if(n.retained_count>RUNTIME_EVENT_RETENTION_POLICY.maxRecordsPerThread){const t=this.db.prepare("SELECT thread_sequence AS keep_from\n FROM runtime_events\n WHERE thread_id = ?\n ORDER BY thread_sequence DESC\n LIMIT 1 OFFSET ?").get(e,RUNTIME_EVENT_RETENTION_POLICY.maxRecordsPerThread-1);r=Math.max(r,t?.keep_from??n.next_sequence)}if(n.retained_bytes>RUNTIME_EVENT_RETENTION_POLICY.maxBytesPerThread){const t=this.db.prepare("SELECT MIN(thread_sequence) AS keep_from\n FROM (\n SELECT thread_sequence,\n SUM(size_bytes) OVER (\n ORDER BY thread_sequence DESC\n ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n ) AS retained_bytes\n FROM runtime_events\n WHERE thread_id = ?\n )\n WHERE retained_bytes <= ?").get(e,RUNTIME_EVENT_RETENTION_POLICY.maxBytesPerThread);r=Math.max(r,t.keep_from??n.next_sequence)}const s=new Date(Date.parse(t)-RUNTIME_EVENT_RETENTION_POLICY.maxAgeMs).toISOString(),o=this.db.prepare("SELECT MIN(thread_sequence) AS keep_from\n FROM runtime_events\n WHERE thread_id = ? AND created_at >= ?").get(e,s);if(r=Math.max(r,o.keep_from??n.next_sequence),r<=n.retained_from_sequence)return;const i=this.db.prepare("SELECT thread_sequence, event_id\n FROM runtime_events\n WHERE thread_id = ? AND thread_sequence = ?").get(e,r-1);if(void 0===i)throw new Error("Runtime event retention boundary is not a continuous retained prefix");const a=this.db.prepare("SELECT COUNT(*) AS removed_count, SUM(size_bytes) AS removed_bytes\n FROM runtime_events\n WHERE thread_id = ? AND thread_sequence < ?").get(e,r);this.db.prepare("DELETE FROM runtime_events WHERE thread_id = ? AND thread_sequence < ?").run(e,r),this.db.prepare("UPDATE thread_runtime_event_state\n SET retained_from_sequence = ?, retained_predecessor_event_id = ?,\n retained_count = retained_count - ?,\n retained_bytes = retained_bytes - ?\n WHERE thread_id = ?").run(r,i.event_id,a.removed_count,a.removed_bytes??0,e)}appendMessages(e,t,n={}){const r=n.toolExecutionCoverage;if(0===t.length){if(void 0!==r&&r.executionIds.length>0)throw new Error("Tool execution coverage 必须与至少一条 transcript message 原子写入");return}if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);if(void 0!==r&&r.representation!==TOOL_EXECUTION_CONTEXT_REPRESENTATIONS.rawTranscript&&r.representation!==TOOL_EXECUTION_CONTEXT_REPRESENTATIONS.recoveryEvidence)throw new Error("Tool execution coverage representation 无效");const s=r?.executionIds??[],o=new Set;for(const e of s){if("string"!=typeof e||0===e.length)throw new Error("Tool execution coverage execution id 无效");if(o.has(e))throw new Error(`Tool execution coverage execution id 重复: ${e}`);o.add(e)}const i=t.map(e=>A(a.parse(e))),c=(new Date).toISOString();this.db.exec("BEGIN IMMEDIATE");try{this.appendMessagesInTransaction(e,i,c,r),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}recoverUncoveredToolExecutions(e,t){this.db.exec("BEGIN IMMEDIATE");try{if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=this.uncoveredToolExecutionRecords(e,{afterSequence:-1,throughSequence:Number.MAX_SAFE_INTEGER,toolCallId:void 0,limit:Number.MAX_SAFE_INTEGER});if(0===n.length)return void this.db.exec("COMMIT");const r=A(a.parse(t(n))),s={executionIds:n.map(e=>e.id),representation:TOOL_EXECUTION_CONTEXT_REPRESENTATIONS.recoveryEvidence};return this.appendMessagesInTransaction(e,[r],(new Date).toISOString(),s),this.db.exec("COMMIT"),r}catch(e){throw this.db.exec("ROLLBACK"),e}}appendMessagesInTransaction(e,t,n,r){const 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 (?, ?, ?, ?, ?, ?)"),i=this.db.prepare("INSERT INTO tool_execution_context_coverage\n (thread_id, execution_id, representation, transcript_sequence, created_at)\n SELECT thread_id, id, ?, ?, ?\n FROM tool_executions\n WHERE thread_id = ?\n AND id = ?\n AND NOT EXISTS (\n SELECT 1\n FROM tool_execution_context_coverage\n WHERE thread_id = ? AND execution_id = ?\n )"),a=this.db.prepare("SELECT COALESCE(MAX(idx), -1) AS maxIdx FROM messages WHERE thread_id = ?").get(e),c=this.db.prepare("SELECT COALESCE(MAX(sequence), -1) AS maxSequence FROM transcript_messages WHERE thread_id = ?").get(e);let d=a.maxIdx+1,E=c.maxSequence+1;for(const r of t){const t=JSON.stringify(r);s.run(e,d,r.role,t,n),o.run(e,E,r.role,t,R[0],n),d+=1,E+=1}const _=E-1;if(void 0!==r){for(const t of r.executionIds){const s=i.run(r.representation,_,n,e,t,e,t);if(1!==Number(s.changes))throw new Error(`Tool execution "${t}" 不属于 Thread "${e}" 或已被 transcript 覆盖`)}this.enforceToolExecutionRetentionInTransaction(e,n)}this.db.prepare("UPDATE threads SET updated_at = ? WHERE id = ?").run(n,e)}replaceMessages(e,t){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=t.map(e=>a.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=>a.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=A(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=p(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 < ?\n AND EXISTS (\n SELECT 1\n FROM tool_execution_context_coverage\n WHERE tool_execution_context_coverage.thread_id = tool_executions.thread_id\n AND tool_execution_context_coverage.execution_id = tool_executions.id\n )").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\n WHERE thread_id = ?\n AND sequence = ?\n AND EXISTS (\n SELECT 1\n FROM tool_execution_context_coverage\n WHERE tool_execution_context_coverage.thread_id = tool_executions.thread_id\n AND tool_execution_context_coverage.execution_id = tool_executions.id\n )");for(const n of o){const s=t.run(e,n.sequence);r=Number(s.changes)>0||r}}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{afterSequence:n,throughSequence:r,limit:s}=this.resolveToolExecutionListRange(t);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=>({...T(JSON.parse(e.record_json)),sequence:e.sequence}))}listUncoveredToolExecutions(e,t={}){const{afterSequence:n,throughSequence:r,limit:s}=this.resolveToolExecutionListRange(t);return r<=n?[]:this.uncoveredToolExecutionRecords(e,{afterSequence:n,throughSequence:r,toolCallId:t.toolCallId,limit:s})}resolveToolExecutionListRange(e){const t=e.afterSequence??-1,n=e.throughSequence??Number.MAX_SAFE_INTEGER,r=e.limit??b;if(!Number.isInteger(t)||t<-1)throw new Error("afterSequence 必须是大于等于 -1 的整数");if(!Number.isInteger(r)||r<1||r>M)throw new Error(`limit 必须是 1-${String(M)} 之间的整数`);if(!Number.isInteger(n)||n<-1)throw new Error("throughSequence 必须是大于等于 -1 的整数");return{afterSequence:t,throughSequence:n,limit:r}}uncoveredToolExecutionRecords(e,t){return this.db.prepare("SELECT tool_executions.sequence, tool_executions.record_json\n FROM tool_executions\n LEFT JOIN tool_execution_context_coverage\n ON tool_execution_context_coverage.thread_id = tool_executions.thread_id\n AND tool_execution_context_coverage.execution_id = tool_executions.id\n WHERE tool_executions.thread_id = ?\n AND tool_executions.sequence > ?\n AND tool_executions.sequence <= ?\n AND (? IS NULL OR tool_executions.tool_call_id = ?)\n AND tool_execution_context_coverage.execution_id IS NULL\n ORDER BY tool_executions.sequence ASC\n LIMIT ?").all(e,t.afterSequence,t.throughSequence,t.toolCallId??null,t.toolCallId??null,t.limit).map(e=>({...T(JSON.parse(e.record_json)),sequence:e.sequence}))}listRecentToolExecutions(e,t={}){const n=t.beforeSequence??Number.MAX_SAFE_INTEGER,r=t.limit??b;this.validateRecentPage(n,r,M);const s=this.db.prepare("SELECT sequence, record_json\n FROM tool_executions\n WHERE thread_id = ?\n AND sequence < ?\n ORDER BY sequence DESC\n LIMIT ?").all(e,n,r+1),o=s.length>r,i=s.slice(0,r).map(e=>({...T(JSON.parse(e.record_json)),sequence:e.sequence})).reverse();return{entries:i,nextBeforeSequence:o?i[0]?.sequence:void 0}}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{...T(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===N[0]?N[0]:N[1]}listTranscriptMessages(e,t={}){const n=t.afterSequence??-1,r=t.throughSequence??Number.MAX_SAFE_INTEGER,s=t.limit??w;if(this.validateTranscriptRange(n,r,s,D),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=>g.parse({sequence:e.sequence,provenance:e.provenance,createdAt:e.created_at,message:JSON.parse(e.message_json)}))}listRecentTranscriptMessages(e,t={}){const n=t.beforeSequence??Number.MAX_SAFE_INTEGER,r=t.limit??w;this.validateRecentPage(n,r,D);const s=this.db.prepare("SELECT sequence, provenance, created_at, message_json\n FROM transcript_messages\n WHERE thread_id = ?\n AND sequence < ?\n ORDER BY sequence DESC\n LIMIT ?").all(e,n,r+1),o=s.length>r,i=s.slice(0,r).map(e=>g.parse({sequence:e.sequence,provenance:e.provenance,createdAt:e.created_at,message:JSON.parse(e.message_json)})).reverse();return{entries:i,nextBeforeSequence:o?i[0]?.sequence:void 0}}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 S)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 S)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 S)throw e;return}}loadSessionState(e){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);return{messages:f(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=>a.parse(e)),r=t.expectedActiveMessages.map(e=>a.parse(e)),s=f(n);if(s.status!==C.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=O(t.draft),i=v.parse(t.legacySnapshotTranscriptFragments??[]),c=v.parse(i.map(e=>l(e))).map(e=>a.parse(x(e))),d=(new Date).toISOString();this.db.exec("BEGIN IMMEDIATE");try{const s=f(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 i=this.getLatestCheckpoint(e);if(i?.id!==t.expectedLatestCheckpointId)throw new Error("Compaction checkpoint ancestry changed after the draft snapshot was captured");const a=this.getLatestSummaryCheckpoint(e),E=k(o.summary,a),_=this.getTranscriptCompleteness(e),u=O({...o,summary:E,..._===N[1]&&"valid"===o.toolState.integrityStatus?{toolState:{...o.toolState,integrityStatus:"sanitized"}}:{}}),h=this.db.prepare("SELECT COALESCE(MAX(generation), 0) AS maxGeneration\n FROM compaction_checkpoints\n WHERE thread_id = ?").get(e),T=this.maxTranscriptMessageSequence(e),p=this.maxToolExecutionSequence(e),l=t.evidenceWatermarks.transcriptMessagesThroughSequence,m=t.evidenceWatermarks.toolExecutionsThroughSequence,S=i?.transcript.messages.throughSequence??-1,g=i?.transcript.toolExecutions.throughSequence??-1,L=2===i?.version?i.semanticEvidence.messagesThroughSequence:-1,v=2===i?.version?i.semanticEvidence.toolExecutionsThroughSequence:-1;if(c.length>0&&1!==i?.version)throw new Error("Legacy snapshot transcript archive requires a V1 parent checkpoint");if(!Number.isInteger(l)||l<S||l>T)throw new Error(`Compaction transcript message watermark ${String(l)} is outside the available range ${String(S)}-${String(T)}`);if(!Number.isInteger(t.semanticEvidenceWatermarks.messagesThroughSequence)||t.semanticEvidenceWatermarks.messagesThroughSequence<L||t.semanticEvidenceWatermarks.messagesThroughSequence>l)throw new Error(`Compaction semantic message watermark ${String(t.semanticEvidenceWatermarks.messagesThroughSequence)} is outside the available range ${String(L)}-${String(l)}`);if(!Number.isInteger(t.semanticEvidenceWatermarks.toolExecutionsThroughSequence)||t.semanticEvidenceWatermarks.toolExecutionsThroughSequence<v||t.semanticEvidenceWatermarks.toolExecutionsThroughSequence>m)throw new Error(`Compaction semantic Tool watermark ${String(t.semanticEvidenceWatermarks.toolExecutionsThroughSequence)} is outside the available range ${String(v)}-${String(m)}`);if(!Number.isInteger(m)||m<g||m>p)throw new Error(`Compaction tool execution watermark ${String(m)} is outside the available range ${String(g)}-${String(p)}`);let x=l;if(c.length>0){if(l!==T)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=T+1;for(const r of c)t.run(e,n,r.role,JSON.stringify(r),R[1],d),n+=1;x=n-1}const C={messages:{fromSequenceExclusive:S,throughSequence:x},toolExecutions:{fromSequenceExclusive:g,throughSequence:m},completeness:_},A=I({draft:u,semanticState:t.semanticState,semanticEvidence:t.semanticEvidenceWatermarks,generation:h.maxGeneration+1,transcript:C,...void 0!==i?{previousCheckpointId:i.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(A.id,e,A.generation,A.version,A.transcript.messages.fromSequenceExclusive,A.transcript.messages.throughSequence,A.transcript.toolExecutions.fromSequenceExclusive,A.transcript.toolExecutions.throughSequence,JSON.stringify(A),A.createdAt),this.replaceMessagesInTransaction(e,n,d),this.db.exec("COMMIT"),A}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??w,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,U);const i="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,M)}).map(e=>({kind:"tool_execution",...e})),a=i.length>r,c=a?i.slice(0,r):i,d=c.at(-1)?.sequence;return{checkpointId:n.id,kind:t.kind,entries:c,...a&&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=N[1],r=t.transcript.completeness===n||this.getTranscriptCompleteness(e)===n?n:N[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)} 之间的整数`)}validateRecentPage(e,t,n){if(!Number.isSafeInteger(e)||e<0||e>Number.MAX_SAFE_INTEGER)throw new Error("beforeSequence 必须是非负安全整数");if(!Number.isInteger(t)||t<1||t>n)throw new Error(`limit 必须是 1-${String(n)} 之间的整数`)}close(){this.db.close()}}
@@ -33,16 +33,19 @@ declare const bashToolInputSchema: z.ZodObject<{
33
33
  explanation: z.ZodOptional<z.ZodString>;
34
34
  workdir: z.ZodOptional<z.ZodString>;
35
35
  timeout_ms: z.ZodOptional<z.ZodNumber>;
36
+ max_output_chars: z.ZodOptional<z.ZodNumber>;
36
37
  }, "strip", z.ZodTypeAny, {
37
38
  command: string;
38
39
  explanation?: string | undefined;
39
40
  workdir?: string | undefined;
40
41
  timeout_ms?: number | undefined;
42
+ max_output_chars?: number | undefined;
41
43
  }, {
42
44
  command: string;
43
45
  explanation?: string | undefined;
44
46
  workdir?: string | undefined;
45
47
  timeout_ms?: number | undefined;
48
+ max_output_chars?: number | undefined;
46
49
  }>;
47
50
  export type BashToolInput = z.infer<typeof bashToolInputSchema>;
48
51
  export declare function buildBashToolset(settings: SessionBashSettings, registry: ToolRegistry, ctx: BashToolContext, deps?: BashToolDeps): ToolSet;
@@ -1 +1 @@
1
- import{resolve as o}from"node:path";import{tool as t}from"ai";import{z as e}from"zod";import{CLASSIFICATION_ANNOTATIONS as r}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 a}from"../bash/format-result.js";import{isWithinWorkdirRoot as s}from"../bash/workdir.js";import{gateToolCall as l}from"./build-tools.js";import{ToolRegistry as m}from"./naming.js";import{TOOL_OUTCOME_KINDS as c,failedToolResult as u,toolResultToModelOutput as d}from"./normalize-result.js";import{TOOL_RESOURCE_ACCESS_MODES as p,executeCoordinatedTool as f}from"./tool-execution-coordinator.js";import{shellCommandExplanationSchema as w}from"./shell-explanation.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 k=256,h=4096,x="shell:opaque-side-effects";function v(o){return"known-safe"===o||"dangerous"===o?o:"unknown"}const M=e.object({command:e.string().min(1).describe("要执行的 shell 命令(单字符串,由当前 shell 后端执行)"),explanation:w.optional(),workdir:e.string().min(1).optional().describe("工作目录绝对路径,默认为 roll chat 当前目录。不要在 command 里用 cd,改用本字段"),timeout_ms:e.number().int().min(1).max(6e5).optional().describe("超时毫秒数。默认 10000,上限受 maxTimeoutMs 与 turnTimeoutMs 约束;长脚本请显式调大")});function _(o){const t=M.safeParse(o);return t.success?t.data:void 0}function b(o,t,e){const r=o.emitEvent;if(!r)return;let n=0;return(o,i)=>{if(n>=k)return;n+=1;const a=i.length>h?i.slice(0,h):i;r({type:"tool-output-delta",toolCallId:t,agentName:"roll",toolName:e,stream:o,delta:a})}}async function y(o,t,e,r,n,i){if(o.policy)return l(o,"roll",t,e,n,{includePolicyReason:"dangerous"===r,...void 0!==i?{explanation:i}:{}});const a=await o.requestApproval({agentName:"roll",toolName:t,input:e,reason:"dangerous"===r?"破坏性操作":void 0,...void 0!==i?{explanation:i}:{}});return a.approved?void 0:u(c.userRejected,"已取消执行"+(a.reason?`: ${a.reason}`:""),a.reason?{reason:a.reason}:{})}export function buildBashToolset(e,l,m,w={}){const k=e.profile.toolName,h=l.register("roll",k),O=w.classifier??e.profile,g=w.exec??n,T=e.env??process.env,j=t=>({workdir:o(e.workdir,t.workdir??"."),timeoutMs:Math.min(t.timeout_ms??e.defaultTimeoutMs,e.maxTimeoutMs,e.turnTimeoutMs)}),E=(o,t)=>{const{workdir:n,timeoutMs:i}=j(o),a=s(e.workdir,n)?t??O.classify(o.command,n):"unknown";return{workdir:n,timeoutMs:i,classification:a,annotations:r[a]}},S={prepare:async(o,t)=>{const e=_(o);if(void 0===e)return u(c.invalidInput,"参数校验失败: command 必须为非空字符串,explanation 最多 100 字符",{raw:o});const r=E(e,v(t));return y(m,k,{command:e.command,workdir:r.workdir,timeout_ms:r.timeoutMs},r.classification,r.annotations,e.explanation)},resources:(o,t)=>{const r=_(o);if(void 0===r)return[{key:x,mode:p.write},{key:`shell:${e.workdir}`,mode:p.write}];const n=E(r,v(t)),i=!0===n.annotations.readOnlyHint&&!0!==n.annotations.destructiveHint,a={key:`shell:${n.workdir}`,mode:i?p.read:p.write};return i?[a]:[{key:x,mode:p.write},a]},captureExecutionState:o=>{const t=_(o);return void 0===t?"unknown":E(t).classification},revalidateExecution:(o,t)=>{const e=_(o);if("known-safe"===t&&void 0!==e)return"known-safe"===E(e).classification?void 0:u(c.toolFailed,"shell 命令的安全条件在准入后发生变化,已在执行前阻止;请重新提交以重新确认")}};return m.coordinator?.register(h,S),{[h]:t({description:"在当前 shell 后端中执行一条命令并返回输出。需确认的命令继承 roll 进程环境;自动批准的 known-safe 命令使用隔离的系统 PATH 与 shell 环境。总是用 workdir 参数设置工作目录,不要用 cd。",inputSchema:M,toModelOutput:({output:o})=>d(o),execute:async(o,t)=>{const{workdir:r,timeoutMs:n}=j(o);return f(m.coordinator,S,h,t.toolCallId,o,t.abortSignal,async s=>{const l=b(m,t.toolCallId,k),c=await g({command:o.command,workdir:r,timeoutMs:n,maxCaptureBytes:e.maxCaptureBytes,profile:e.profile,env:"known-safe"===s?i(T):T,...t.abortSignal?{abortSignal:t.abortSignal}:{},...l?{onDelta:l}:{}});return a({result:c,maxModelOutputChars:e.maxModelOutputChars})})}})}}
1
+ import{resolve as o}from"node:path";import{tool as t}from"ai";import{z as e}from"zod";import{CLASSIFICATION_ANNOTATIONS as r}from"../types/command-classification.js";import{runBashCommand as n}from"../bash/exec.js";import{withAutoApprovedShellEnv as a}from"../bash/clean-env.js";import{formatBashResult as i}from"../bash/format-result.js";import{isWithinWorkdirRoot as s}from"../bash/workdir.js";import{boundedIntParam as l,describeZodIssues as m}from"./bounded-param.js";import{gateToolCall as c}from"./build-tools.js";import{ToolRegistry as u}from"./naming.js";import{TOOL_OUTCOME_KINDS as d,failedToolResult as p,toolResultToModelOutput as f}from"./normalize-result.js";import{OPAQUE_SIDE_EFFECT_RESOURCE as h,TOOL_RESOURCE_ACCESS_MODES as w,executeCoordinatedTool as x}from"./tool-execution-coordinator.js";import{shellCommandExplanationSchema as k}from"./shell-explanation.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 _=256,v=4096;function M(o){return"known-safe"===o||"dangerous"===o?o:"unknown"}const b=l({name:"timeout_ms",min:1,max:6e5,description:"超时毫秒数,上限受 maxTimeoutMs 与 turnTimeoutMs 约束;长脚本请显式调大",defaultNote:"默认 10000"}),y=l({name:"max_output_chars",min:1e3,max:2e5,description:"本次返回输出的字符预算;控制输出量请用本参数,不要自接 head/tail 管道",defaultNote:"默认继承配置"}),O=e.object({command:e.string().min(1).describe("要执行的 shell 命令(单字符串,由当前 shell 后端执行)"),explanation:k.optional(),workdir:e.string().min(1).optional().describe("工作目录绝对路径,默认为 roll chat 当前目录。不要在 command 里用 cd,改用本字段"),timeout_ms:b.schema,max_output_chars:y.schema});function g(o){const t=O.safeParse(o);return t.success?t.data:void 0}function j(o,t,e){const r=o.emitEvent;if(!r)return;let n=0;return(o,a)=>{if(n>=_)return;n+=1;const i=a.length>v?a.slice(0,v):a;r({type:"tool-output-delta",toolCallId:t,agentName:"roll",toolName:e,stream:o,delta:i})}}async function T(o,t,e,r,n,a){if(o.policy)return c(o,"roll",t,e,n,{includePolicyReason:"dangerous"===r,...void 0!==a?{explanation:a}:{}});const i=await o.requestApproval({agentName:"roll",toolName:t,input:e,reason:"dangerous"===r?"破坏性操作":void 0,...void 0!==a?{explanation:a}:{}});return i.approved?void 0:p(d.userRejected,"已取消执行"+(i.reason?`: ${i.reason}`:""),i.reason?{reason:i.reason}:{})}export function buildBashToolset(e,l,c,u={}){const k=e.profile.toolName,_=l.register("roll",k),v=u.classifier??e.profile,b=u.exec??n,y=e.env??process.env,E=t=>({workdir:o(e.workdir,t.workdir??"."),timeoutMs:Math.min(t.timeout_ms??e.defaultTimeoutMs,e.maxTimeoutMs,e.turnTimeoutMs)}),N=(o,t)=>{const{workdir:n,timeoutMs:a}=E(o),i=s(e.workdir,n)?t??v.classify(o.command,n):"unknown";return{workdir:n,timeoutMs:a,classification:i,annotations:r[i]}},S={prepare:async(o,t)=>{const e=O.safeParse(o);if(!e.success)return p(d.invalidInput,m(e.error,o)??"参数校验失败: command 必须为非空字符串,explanation 最多 100 字符",{raw:o});const r=e.data,n=N(r,M(t));return T(c,k,{command:r.command,workdir:n.workdir,timeout_ms:n.timeoutMs},n.classification,n.annotations,r.explanation)},resources:(o,t)=>{const r=g(o);if(void 0===r)return[{key:h,mode:w.write},{key:`shell:${e.workdir}`,mode:w.write}];const n=N(r,M(t)),a=!0===n.annotations.readOnlyHint&&!0!==n.annotations.destructiveHint,i={key:`shell:${n.workdir}`,mode:a?w.read:w.write};return a?[i]:[{key:h,mode:w.write},i]},captureExecutionState:o=>{const t=g(o);return void 0===t?"unknown":N(t).classification},revalidateExecution:(o,t)=>{const e=g(o);if("known-safe"===t&&void 0!==e)return"known-safe"===N(e).classification?void 0:p(d.toolFailed,"shell 命令的安全条件在准入后发生变化,已在执行前阻止;请重新提交以重新确认")}};return c.coordinator?.register(_,S),{[_]:t({description:"在当前 shell 后端中执行一条命令并返回输出。需确认的命令继承 roll 进程环境;自动批准的 known-safe 命令使用隔离的系统 PATH 与 shell 环境。总是用 workdir 参数设置工作目录,不要用 cd。输出量用 max_output_chars 控制,不要自接 head/tail 管道(管道退出码反映整条管道)。",inputSchema:O,toModelOutput:({output:o})=>f(o),execute:async(o,t)=>{const{workdir:r,timeoutMs:n}=E(o);return x(c.coordinator,S,_,t.toolCallId,o,t.abortSignal,async s=>{const l=j(c,t.toolCallId,k),m=await b({command:o.command,workdir:r,timeoutMs:n,maxCaptureBytes:e.maxCaptureBytes,profile:e.profile,env:"known-safe"===s?a(y):y,...t.abortSignal?{abortSignal:t.abortSignal}:{},...l?{onDelta:l}:{}});return i({result:m,maxModelOutputChars:o.max_output_chars??e.maxModelOutputChars})})}})}}
@@ -0,0 +1,14 @@
1
+ import { z } from "zod";
2
+ export interface BoundedIntParamSpec {
3
+ readonly name: string;
4
+ readonly min: number;
5
+ readonly max: number;
6
+ readonly description: string;
7
+ readonly defaultNote?: string;
8
+ }
9
+ export interface BoundedIntParam {
10
+ readonly schema: z.ZodOptional<z.ZodNumber>;
11
+ }
12
+ export declare function boundedIntParam(spec: BoundedIntParamSpec): BoundedIntParam;
13
+ export declare function describeZodIssues(error: unknown, rawInput: unknown): string | undefined;
14
+ export declare function friendlyInvalidToolInputMessage(error: unknown): string | undefined;
@@ -0,0 +1 @@
1
+ import{z as t}from"zod";export function boundedIntParam(e){const o=`范围 ${String(e.min)}-${String(e.max)}`;return{schema:t.number().int().min(e.min).max(e.max).optional().describe(`${e.description}(整数,${o}${void 0!==e.defaultNote?`,${e.defaultNote}`:""})`)}}function e(t){if(null===t||"object"!=typeof t)return;const o=t;if(Array.isArray(o.issues))return o.issues;if(void 0!==o.error){const t=e(o.error);if(void 0!==t)return t}return void 0!==o.cause?e(o.cause):void 0}function o(t,e){let o=t;for(const t of e){if(null===o||"object"!=typeof o)return;o=o[t]}return o}function r(t){const e="string"==typeof t?t:String(t);return e.length>40?`${e.slice(0,40)}…`:e}export function describeZodIssues(t,n){const i=e(t);if(void 0===i||0===i.length)return;return i.map(t=>{const e=(t.path??[]).join(".")||"(root)",i=r(o(n,t.path??[])),s="number"===t.type?" 的整数":"string"===t.type?" 的字符长度":"";return"too_big"===t.code&&"number"==typeof t.maximum?`${e} 越界:允许≤${String(t.maximum)}${s},你传了 ${i}`:"too_small"===t.code&&"number"==typeof t.minimum?`${e} 越界:允许≥${String(t.minimum)}${s},你传了 ${i}`:"invalid_type"===t.code?`${e} 类型错误:需要${t.expected??"合法类型"},你传了 ${i}`:`${e}: ${t.code??"invalid"}`}).join(";")}const n="AI_InvalidToolInputError:";function i(t){try{return JSON.parse(t)}catch{return}}export function friendlyInvalidToolInputMessage(t){if(null!==t&&"object"==typeof t){const e=t,o="string"==typeof e.toolInput?i(e.toolInput):e.toolInput;return describeZodIssues(e.cause??t,o)}if("string"==typeof t&&t.startsWith(n)){const e=/Value:\s*(\{.*?\})\.\s*Error message:/su.exec(t),o=void 0!==e?.[1]?i(e[1]):void 0,r=/Error message:\s*(\[.*\])\s*$/su.exec(t);if(void 0!==r?.[1]){const t=i(r[1]);return Array.isArray(t)?describeZodIssues({issues:t},o):void 0}}}
@@ -1,6 +1,7 @@
1
1
  import { type ToolSet } from "ai";
2
2
  import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
3
  import type { AgentTool } from "@roll-agent/core/types/agent";
4
+ import type { SessionApprovalMemory } from "../approval/approval-memory.ts";
4
5
  import type { ApprovalDecision } from "../approval/approval-gate.ts";
5
6
  import type { ToolAnnotations, ToolPolicy } from "../types/policy.ts";
6
7
  import { ToolRegistry, type ToolRouteMetadata } from "./naming.ts";
@@ -28,16 +29,20 @@ export interface ApprovalRequest {
28
29
  readonly input: Record<string, unknown>;
29
30
  readonly reason: string | undefined;
30
31
  readonly explanation?: string;
32
+ readonly sessionGrantLabel?: string;
31
33
  }
32
34
  export interface ToolBridgeContext {
33
35
  readonly policy?: ToolPolicy;
34
36
  readonly requestApproval: (request: ApprovalRequest) => Promise<ApprovalDecision>;
35
37
  readonly coordinator?: ToolExecutionCoordinator;
38
+ readonly approvalMemory?: SessionApprovalMemory;
36
39
  }
37
40
  interface ApprovalDisplayOptions {
38
41
  readonly explanation?: string;
39
42
  /** False when a conservative policy label would overstate the actual user-visible risk. */
40
43
  readonly includePolicyReason?: boolean;
44
+ readonly memoryKey?: string;
45
+ readonly sessionGrantLabel?: string;
41
46
  }
42
47
  export interface BuiltToolset {
43
48
  readonly tools: ToolSet;
@@ -1 +1 @@
1
- import{lstatSync as t,readdirSync as n,readlinkSync as e,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 l}from"ai";import{preflightToolCall as f}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 v,toolResultToModelOutput as g}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,n){for(let e=0;e<t.length;e+=1){const o=t.charCodeAt(e),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,e)}${r}${t.slice(e+1)}`;if(!n.has(i))return i}}function O(t){return"object"==typeof t&&null!==t&&"code"in t?t.code:void 0}function x(n,e,o){const r=A(e,o);if(void 0!==r)try{const o=t(c(n,e)),i=t(c(n,r));return o.dev===i.dev&&o.ino===i.ino}catch(t){const n=O(t);return"ENOENT"!==n&&"ENOTDIR"!==n&&void 0}}function j(t,n){if(k.size>=N){const t=k.keys().next().value;void 0!==t&&k.delete(t)}return k.set(t,n),n}function T(e){const o=k.get(e);if(void 0!==o)return o;let r;try{r=t(e).dev}catch{return j(e,!0)}let i=e;for(;;){const o=k.get(i);if(void 0!==o)return j(e,o);try{const t=n(i),o=new Set(t);for(const n of t){const t=x(i,n,o);if(void 0!==t)return j(i,t),j(e,t)}}catch{}const a=s(i);if(a===i)break;try{if(t(a).dev!==r)break}catch{break}i=a}return j(e,!0)}function E(t){let n=t.normalize("NFC");for(let t=0;t<8;t+=1){const t=n.toUpperCase().toLowerCase().normalize("NFC");if(t===n)return n;n=t}return n}function R(t,n){try{const e=r(t,{bigint:!0}),o=n.map(t=>E(t));return`file-unresolved-alias:${e.dev.toString()}:${e.ino.toString()}:${JSON.stringify(o)}`}catch{return}}function z(n,r=new Set){let a=n;const u=[];for(;;)try{const t=o.native(a),n=u.reverse().map(t=>t.normalize("NFC"));if(0===n.length)return{path:t};const e=T(t),r=c(t,...e?n.map(t=>t.toLowerCase()):n),i=e?R(t,n):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 n=e(a);return z(c(s(a),n,...[...u].reverse()),t)}}catch{}const o=s(a);if(o===a)return{path:n};u.push(i(a)),a=o}}function H(t){try{const n=r(t,{bigint:!0});return`file-inode:${n.dev.toString()}:${n.ino.toString()}`}catch{return}}function L(t,n,e){const o=String(n).trim();if(0!==o.length)switch(t.kind){case h.file:{if(!a(o)&&void 0===e)return;const t=z(c(e??"/",o)),n=H(t.path);return[`file:${t.path}`,...void 0===t.unresolvedAliasKey?[]:[t.unresolvedAliasKey],...void 0===n?[]:[n]]}case h.browserSession:return[`browser-session:${o}`];case h.conversation:return[`conversation:${o}`];case h.custom:{const n=t.namespace?.trim();return n?[`${n}:${o}`]:void 0}}}function I(t,n,e,o,r){const i=w(o),s=$(e),a=[{key:`agent:${t}`,mode:i}],c=[];for(const t of r??[]){if(!Object.hasOwn(s,t.field))continue;const e=C(s[t.field]);if(void 0===e)return a;for(const o of e){const e=L(t,o,n);if(void 0===e||0===e.length)return a;c.push(...e.map(n=>({key:n,mode:t.mode??i})))}}return 0===c.length?a:[{key:`agent:${t}`,mode:y.read},...c]}export async function gateToolCall(t,n,e,o,r,i){if(!t.policy)return;const s=t.policy.check({agentName:n,toolName:e,input:o,...r?{annotations:r}:{}});if("deny"===s.action)return m(p.policyDenied,"策略拒绝执行"+(s.reason?`: ${s.reason}`:""),s.reason?{reason:s.reason}:{});if("confirm"===s.action){const r=await t.requestApproval({agentName:n,toolName:e,input:o,reason:!1===i?.includePolicyReason?void 0:s.reason,...void 0!==i?.explanation?{explanation:i.explanation}:{}});if(!r.approved)return m(p.userRejected,"已取消执行"+(r.reason?`: ${r.reason}`:""),r.reason?{reason:r.reason}:{})}}export function buildAgentToolset(t,n,e=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=e.register(i,y.name,{...s?{agentSource:s}:{},...a?{transport:a}:{},...c?{runtimeOwnership:c}:{},...h?{annotations:h}:{}}),C={prepare:async t=>{const e=$(t),o=f(y,e);return o.ok?gateToolCall(n,i,y.name,e,h):m(p.invalidInput,b(o.issues),{raw:o.issues})},resources:t=>I(i,d,t,h,w)};n.coordinator?.register(r,C),o[r]=l({description:y.description??`${y.name} (via ${i})`,inputSchema:u(y.inputSchema),toModelOutput:({output:t})=>g(t),execute:async(e,o)=>{const i=$(e);return S(n.coordinator,C,r,o.toolCallId,i,o.abortSignal,async()=>{const n=o.abortSignal?{signal:o.abortSignal}:void 0,e=await t.callTool({name:y.name,arguments:i},void 0,n);return v(e)})}})}}return{tools:o,registry:e}}
1
+ import{lstatSync as e,readdirSync as t,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 l}from"ai";import{preflightToolCall as f}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 v,toolResultToModelOutput as g}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 b(e){return"object"!=typeof e||null===e||Array.isArray(e)?{}:e}function $(e){return`参数校验失败: ${e.map(e=>e.message).join("; ")}`}function w(e){return!0===e?.readOnlyHint&&!0!==e.destructiveHint?y.read:y.write}function C(e){return"string"==typeof e||"number"==typeof e?[e]:Array.isArray(e)&&0!==e.length&&e.every(e=>"string"==typeof e||"number"==typeof e)?e:void 0}const N=256,k=new Map;function A(e,t){for(let n=0;n<e.length;n+=1){const o=e.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=`${e.slice(0,n)}${r}${e.slice(n+1)}`;if(!t.has(i))return i}}function O(e){return"object"==typeof e&&null!==e&&"code"in e?e.code:void 0}function x(t,n,o){const r=A(n,o);if(void 0!==r)try{const o=e(c(t,n)),i=e(c(t,r));return o.dev===i.dev&&o.ino===i.ino}catch(e){const t=O(e);return"ENOENT"!==t&&"ENOTDIR"!==t&&void 0}}function j(e,t){if(k.size>=N){const e=k.keys().next().value;void 0!==e&&k.delete(e)}return k.set(e,t),t}function L(n){const o=k.get(n);if(void 0!==o)return o;let r;try{r=e(n).dev}catch{return j(n,!0)}let i=n;for(;;){const o=k.get(i);if(void 0!==o)return j(n,o);try{const e=t(i),o=new Set(e);for(const t of e){const e=x(i,t,o);if(void 0!==e)return j(i,e),j(n,e)}}catch{}const a=s(i);if(a===i)break;try{if(e(a).dev!==r)break}catch{break}i=a}return j(n,!0)}function T(e){let t=e.normalize("NFC");for(let e=0;e<8;e+=1){const e=t.toUpperCase().toLowerCase().normalize("NFC");if(e===t)return t;t=e}return t}function E(e,t){try{const n=r(e,{bigint:!0}),o=t.map(e=>T(e));return`file-unresolved-alias:${n.dev.toString()}:${n.ino.toString()}:${JSON.stringify(o)}`}catch{return}}function R(t,r=new Set){let a=t;const u=[];for(;;)try{const e=o.native(a),t=u.reverse().map(e=>e.normalize("NFC"));if(0===t.length)return{path:e};const n=L(e),r=c(e,...n?t.map(e=>e.toLowerCase()):t),i=n?E(e,t):void 0;return{path:r,...void 0===i?{}:{unresolvedAliasKey:i}}}catch{try{if(e(a).isSymbolicLink()&&!r.has(a)){const e=new Set(r);e.add(a);const t=n(a);return R(c(s(a),t,...[...u].reverse()),e)}}catch{}const o=s(a);if(o===a)return{path:t};u.push(i(a)),a=o}}function z(e){try{const t=r(e,{bigint:!0});return`file-inode:${t.dev.toString()}:${t.ino.toString()}`}catch{return}}function H(e,t,n){const o=String(t).trim();if(0!==o.length)switch(e.kind){case h.file:{if(!a(o)&&void 0===n)return;const e=R(c(n??"/",o)),t=z(e.path);return[`file:${e.path}`,...void 0===e.unresolvedAliasKey?[]:[e.unresolvedAliasKey],...void 0===t?[]:[t]]}case h.browserSession:return[`browser-session:${o}`];case h.conversation:return[`conversation:${o}`];case h.custom:{const t=e.namespace?.trim();return t?[`${t}:${o}`]:void 0}}}function K(e,t,n,o,r){const i=w(o),s=b(n),a=[{key:`agent:${e}`,mode:i}],c=[];for(const e of r??[]){if(!Object.hasOwn(s,e.field))continue;const n=C(s[e.field]);if(void 0===n)return a;for(const o of n){const n=H(e,o,t);if(void 0===n||0===n.length)return a;c.push(...n.map(t=>({key:t,mode:e.mode??i})))}}return 0===c.length?a:[{key:`agent:${e}`,mode:y.read},...c]}export async function gateToolCall(e,t,n,o,r,i){if(!e.policy)return;const s=e.policy.check({agentName:t,toolName:n,input:o,...r?{annotations:r}:{}});if("deny"===s.action)return m(p.policyDenied,"策略拒绝执行"+(s.reason?`: ${s.reason}`:""),s.reason?{reason:s.reason}:{});if("confirm"===s.action){const r=i?.memoryKey;if(void 0!==r&&e.approvalMemory?.isGranted(r))return;const a=await e.requestApproval({agentName:t,toolName:n,input:o,reason:!1===i?.includePolicyReason?void 0:s.reason,...void 0!==i?.explanation?{explanation:i.explanation}:{},...void 0!==r&&void 0!==i?.sessionGrantLabel?{sessionGrantLabel:i.sessionGrantLabel}:{}});if(!a.approved)return m(p.userRejected,"已取消执行"+(a.reason?`: ${a.reason}`:""),a.reason?{reason:a.reason}:{});void 0!==r&&"session"===a.scope&&e.approvalMemory?.grant(r)}}export function buildAgentToolset(e,t,n=new d){const o={};for(const r of e){const{client:e,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 e=>{const n=b(e),o=f(y,n);return o.ok?gateToolCall(t,i,y.name,n,h):m(p.invalidInput,$(o.issues),{raw:o.issues})},resources:e=>K(i,d,e,h,w)};t.coordinator?.register(r,C),o[r]=l({description:y.description??`${y.name} (via ${i})`,inputSchema:u(y.inputSchema),toModelOutput:({output:e})=>g(e),execute:async(n,o)=>{const i=b(n);return S(t.coordinator,C,r,o.toolCallId,i,o.abortSignal,async()=>{const t=o.abortSignal?{signal:o.abortSignal}:void 0,n=await e.callTool({name:y.name,arguments:i},void 0,t);return v(n)})}})}}return{tools:o,registry:n}}
@@ -0,0 +1,11 @@
1
+ import { type NormalizedToolResult } from "../normalize-result.ts";
2
+ export declare const RAW_NUL_LABEL = "U+0000";
3
+ export interface RawNulFinding {
4
+ readonly index: number;
5
+ readonly line: number;
6
+ readonly column: number;
7
+ }
8
+ export declare function findRawNul(text: string): RawNulFinding | undefined;
9
+ export declare function describeRawNulRejection(fieldLabel: string, finding: RawNulFinding): string;
10
+ export declare function describeLoneSurrogateRejection(fieldLabel: string): string;
11
+ export declare function rejectInvalidTextPayload(fieldLabel: string, text: string): NormalizedToolResult | undefined;
@@ -0,0 +1 @@
1
+ import{TOOL_OUTCOME_KINDS as e,failedToolResult as n}from"../normalize-result.js";export const RAW_NUL_LABEL="U+0000";const t="\\u0000",o="\\\\u0000",r=`{"content": "${o}"}`;export function findRawNul(e){let n=1,t=0;for(let o=0;o<e.length;o+=1){const r=e.charCodeAt(o);if(10!==r){if(0===r)return{index:o,line:n,column:o-t+1}}else n+=1,t=o+1}}export function describeRawNulRejection(e,n){return[`${e}含原始 NUL 字符 U+0000(第 ${String(n.line)} 行第 ${String(n.column)} 列),文件工具只支持文本内容,已拒绝执行,未写入任何内容。`,`常见原因:JSON 参数里的转义会被解码成真实字符——参数中写 ${t}(6 个字符),解码后是 1 个 NUL 字节。`,`两种正确的做法:(1) 若要在文件中保留转义序列文本(如源码里的 ${t}),JSON 参数请写 ${o},例如 ${r},解码后是 6 个 ASCII 字符;(2) 若确实需要写入原始 NUL 字节,请改用 shell 命令生成该字节(如 printf 或 node 脚本),文件工具不支持。`].join("\n")}export function describeLoneSurrogateRejection(e){return`${e}含不成对的 UTF-16 代理项(lone surrogate),无法作为 UTF-8 文本原样写入,已拒绝执行,未写入任何内容。请检查 JSON 参数中的 \\uD800-\\uDFFF 转义是否成对。`}export function rejectInvalidTextPayload(t,o){const r=findRawNul(o);return void 0!==r?n(e.invalidInput,describeRawNulRejection(t,r)):o.isWellFormed()?void 0:n(e.invalidInput,describeLoneSurrogateRejection(t))}
@@ -0,0 +1,42 @@
1
+ import { type ToolSet } from "ai";
2
+ import { z } from "zod";
3
+ import type { ToolRegistry } from "../naming.ts";
4
+ import type { ToolBridgeContext } from "../build-tools.ts";
5
+ import { type NormalizedToolResult } from "../normalize-result.ts";
6
+ import { type FileStateTracker } from "./file-state-tracker.ts";
7
+ import { type ResolvedFileToolsSettings } from "./settings.ts";
8
+ export declare const EDIT_FILE_TOOL_NAME = "edit_file";
9
+ declare const editFileInputSchema: z.ZodObject<{
10
+ file_path: z.ZodString;
11
+ edits: z.ZodArray<z.ZodObject<{
12
+ old_string: z.ZodString;
13
+ new_string: z.ZodString;
14
+ replace_all: z.ZodOptional<z.ZodBoolean>;
15
+ }, "strip", z.ZodTypeAny, {
16
+ old_string: string;
17
+ new_string: string;
18
+ replace_all?: boolean | undefined;
19
+ }, {
20
+ old_string: string;
21
+ new_string: string;
22
+ replace_all?: boolean | undefined;
23
+ }>, "many">;
24
+ }, "strip", z.ZodTypeAny, {
25
+ file_path: string;
26
+ edits: {
27
+ old_string: string;
28
+ new_string: string;
29
+ replace_all?: boolean | undefined;
30
+ }[];
31
+ }, {
32
+ file_path: string;
33
+ edits: {
34
+ old_string: string;
35
+ new_string: string;
36
+ replace_all?: boolean | undefined;
37
+ }[];
38
+ }>;
39
+ export type EditFileInput = z.infer<typeof editFileInputSchema>;
40
+ export declare function executeEditFile(settings: ResolvedFileToolsSettings, tracker: FileStateTracker, input: EditFileInput): NormalizedToolResult;
41
+ export declare function buildEditFileTool(settings: ResolvedFileToolsSettings, tracker: FileStateTracker, registry: ToolRegistry, ctx: ToolBridgeContext): ToolSet;
42
+ export {};
@@ -0,0 +1 @@
1
+ import{tool as t}from"ai";import{z as e}from"zod";import{gateToolCall as n}from"../build-tools.js";import{TOOL_OUTCOME_KINDS as i,failedToolResult as r,successfulToolResult as o,toolResultToModelOutput as s}from"../normalize-result.js";import{OPAQUE_SIDE_EFFECT_RESOURCE as l,TOOL_RESOURCE_ACCESS_MODES as a,executeCoordinatedTool as d}from"../tool-execution-coordinator.js";import{findAllExact as c,findOldString as f,formatMultiMatchDiagnosis as u,formatNoMatchDiagnosis as g,lineNumberAt as p,renderNumberedLines as _}from"./match-pipeline.js";import{canonicalResourcePath as h,captureFilePathAdmission as m,escapesWorkdir as $,formatPathForApproval as w,loadTextFile as k,resolveFilePath as x,revalidateFilePathAdmission as v,saveTextFile as F}from"./file-io.js";import{rejectInvalidTextPayload as b}from"./control-chars.js";import{FILE_FRESHNESS as j}from"./file-state-tracker.js";import{FILE_TOOLS_AGENT_NAME as S}from"./settings.js";export const EDIT_FILE_TOOL_NAME="edit_file";const y=3,E="若修改面较大或文件已大幅变化,可改用 roll__write_file 整文件重写(需先 read_file)",I=e.object({old_string:e.string().min(1).describe("要替换的原文,必须逐字复制自 read_file 读到的内容(不含行号前缀)"),new_string:e.string().describe("替换后的新内容,必须与 old_string 不同"),replace_all:e.boolean().optional().describe("替换所有精确匹配处,默认 false(要求唯一匹配)")}),M=e.object({file_path:e.string().min(1).describe("要修改的文件路径,相对当前工作目录或绝对路径"),edits:e.array(I).min(1).describe("按顺序应用的编辑列表;任何一条失败则整体不写入")}),L={};function P(t){const e=(t.match(/\r\n/g)??[]).length,n=(t.match(/(?<!\r)\n/g)??[]).length;return e>0&&0===n}function C(t,e){return e?t.replace(/\r?\n/g,"\r\n"):t}function O(t,e,n){for(const i of t)i.position>e&&(i.position+=n)}function T(t,e,n,i){const r=t.slice(0,e.start)+n+t.slice(e.end);return O(i,e.start,n.length-(e.end-e.start)),i.push({position:e.start,length:n.length}),r}function B(t,e,n,i){let r=t;for(let t=e.length-1;t>=0;t-=1){const o=e.at(t);void 0!==o&&(r=r.slice(0,o.start)+n+r.slice(o.end),O(i,o.start,n.length-(o.end-o.start)),i.push({position:o.start,length:n.length}))}return r}function U(t,e,n,i){const r=e.split("\n"),o=[`已完成 ${String(n.length)} 处修改并写入 ${t}:`];n.forEach((t,n)=>{const i=p(e,t.position),s=t.position+Math.max(t.length-1,0),l=p(e,Math.min(s,Math.max(e.length-1,0))),a=Math.max(1,i-3),d=Math.min(r.length,l+3);o.push(`[${String(n+1)}] 第 ${String(i)} 行附近:`),o.push(_(r.slice(a-1,d),a))});const s=o.join("\n");return s.length>i?`${s.slice(0,i)}\n…(快照过长已截断,修改均已写入)`:s}function z(t){for(const[e,n]of t.edits.entries()){const i=`第 ${String(e+1)} 条编辑(共 ${String(t.edits.length)} 条)的`,r=b(`${i} old_string`,n.old_string);if(void 0!==r)return r;const o=b(`${i} new_string`,n.new_string);if(void 0!==o)return o}}export function executeEditFile(t,e,n){const s=z(n);if(void 0!==s)return s;const l=x(t.workdir,n.file_path),a=k(l,{maxFileBytes:t.maxFileBytes});if(!a.ok)return r(i.invalidInput,a.message);const d=e.checkFreshness(a.key,a.content);if(d===j.unread)return r(i.toolFailed,`尚未读取过 ${l}。请先用 roll__read_file 读取文件,再基于读到的内容编辑。`);if(d===j.stale)return r(i.toolFailed,`${l} 在你上次读取后已被修改(可能是用户或其他程序改动)。请重新 roll__read_file 获取最新内容,再基于最新内容编辑,不要用旧内容重试。`);const p=P(a.content);let _=a.content;const h=[];for(const[t,e]of n.edits.entries()){const o=`第 ${String(t+1)} 条编辑(共 ${String(n.edits.length)} 条)`;if(e.old_string===e.new_string)return r(i.invalidInput,`${o}:new_string 与 old_string 相同,没有可应用的变化。未写入任何修改。`);const s=C(e.old_string,p),l=C(e.new_string,p);if(s===l)return r(i.invalidInput,`${o}:该文件使用 CRLF 换行,行尾会自动适配,这条编辑在适配后 new_string 与 old_string 相同(只改换行符不会产生变化)。未写入任何修改。`);if(!0===e.replace_all){const t=c(_,s);if(0===t.length)return r(i.toolFailed,`${o}失败,未写入任何修改。\n${g(_,s)}\n${E}`);_=B(_,t,l,h);continue}const a=f(_,s);if("none"===a.kind)return r(i.toolFailed,`${o}失败,未写入任何修改。\n${g(_,s)}\n${E}`);if("multiple"===a.kind)return r(i.toolFailed,`${o}失败,未写入任何修改。\n${u(_,a.spans)}`);_=T(_,a.span,l,h)}return _===a.content?r(i.invalidInput,"所有编辑应用后文件内容与原文件完全相同,没有可写入的变化。未写入任何修改。"):(F(l,_,a.hadBom),e.recordKnownContent(a.key,_),o(U(l,_,h,t.maxOutputChars)))}export function buildEditFileTool(e,o,c,f){const u=c.register(S,"edit_file",{annotations:L}),g={prepare:async t=>{const o=M.safeParse(t);if(!o.success)return r(i.invalidInput,"参数校验失败: file_path 必须为非空字符串,edits 至少一条且每条含非空 old_string 与 new_string");const s=z(o.data);if(void 0!==s)return s;const l=w(e.workdir,o.data.file_path),a=$(e.workdir,o.data.file_path)?void 0:"edit_file:workdir";return n(f,S,"edit_file",o.data,L,{explanation:`修改 ${l}:${String(o.data.edits.length)} 处编辑`,...void 0!==a?{memoryKey:a,sessionGrantLabel:"本会话内不再询问:修改工作目录内的文件"}:{}})},resources:t=>{const n=M.safeParse(t),i=n.success?`file:${h(x(e.workdir,n.data.file_path))}`:`file-tools:${e.workdir}`;return[{key:l,mode:a.read},{key:i,mode:a.write}]},captureExecutionState:t=>{const n=M.safeParse(t);return n.success?m(e.workdir,n.data.file_path):void 0},revalidateExecution:(t,n)=>{const i=M.safeParse(t);if(i.success)return v(e.workdir,i.data.file_path,n)}};return f.coordinator?.register(u,g),{[u]:t({description:"对文本文件做精确字符串替换。使用前必须先 roll__read_file 读取文件;old_string 逐字复制读到的内容(不含行号前缀)且须唯一定位;同一文件多处修改放进 edits 数组一次提交,任何一条失败则整体不写入。成功返回已含修改点最新内容,无需再次读取确认。仅支持文本内容,old_string/new_string 含原始 NUL 字符(U+0000)或不成对的 UTF-16 代理项会被拒绝;需要写入转义序列文本时用双反斜杠,需要原始字节时改用 shell。",inputSchema:M,toModelOutput:({output:t})=>s(t),execute:(t,n)=>d(f.coordinator,g,u,n.toolCallId,t,n.abortSignal,()=>Promise.resolve(executeEditFile(e,o,t)))})}}
@@ -0,0 +1,3 @@
1
+ import type { ToolBridgeContext } from "../build-tools.ts";
2
+ import { type NormalizedToolResult } from "../normalize-result.ts";
3
+ export declare function gateExternalPath(ctx: ToolBridgeContext, toolName: string, input: Record<string, unknown>, toolId?: string): Promise<NormalizedToolResult | undefined>;
@@ -0,0 +1 @@
1
+ import{TOOL_OUTCOME_KINDS as e,failedToolResult as r}from"../normalize-result.js";import{FILE_TOOLS_AGENT_NAME as a}from"./settings.js";const o="读取工作目录以外的文件";export async function gateExternalPath(n,s,t,p){const i=`${s}:external`;if(n.approvalMemory?.isGranted(i))return;const l=p??s,m=await n.requestApproval({agentName:a,toolName:s,input:t,reason:o,sessionGrantLabel:`本会话内不再询问:${l} 访问工作目录外的任意路径`});if(!m.approved)return r(e.userRejected,"已取消执行"+(m.reason?`: ${m.reason}`:""),m.reason?{reason:m.reason}:{});"session"===m.scope&&n.approvalMemory?.grant(i)}
@@ -0,0 +1,35 @@
1
+ import { type NormalizedToolResult } from "../normalize-result.ts";
2
+ export declare const FILE_CONTAINMENT_DRIFT_MESSAGE = "\u6587\u4EF6\u8DEF\u5F84\u7684\u5B89\u5168\u6761\u4EF6\u5728\u51C6\u5165\u540E\u53D1\u751F\u53D8\u5316\uFF0C\u5DF2\u5728\u6267\u884C\u524D\u963B\u6B62\uFF1B\u8BF7\u91CD\u65B0\u63D0\u4EA4\u4EE5\u91CD\u65B0\u786E\u8BA4";
3
+ export type LoadFileFailure = {
4
+ readonly ok: false;
5
+ readonly code: "not-found" | "is-directory" | "not-regular-file" | "too-large" | "binary";
6
+ readonly message: string;
7
+ };
8
+ export interface LoadedTextFile {
9
+ readonly ok: true;
10
+ readonly path: string;
11
+ readonly key: string;
12
+ readonly content: string;
13
+ readonly hadBom: boolean;
14
+ readonly suspectEncoding: boolean;
15
+ }
16
+ export interface FilePathAdmission {
17
+ readonly admittedExternal: boolean;
18
+ readonly admittedTarget: string | undefined;
19
+ }
20
+ export declare function resolveFilePath(workdir: string, input: string): string;
21
+ export declare function canonicalFileKey(path: string): string;
22
+ export declare function canonicalResourcePath(path: string): string;
23
+ export declare function escapesWorkdir(workdir: string, path: string): boolean;
24
+ export declare function formatPathForApproval(workdir: string, inputPath: string): string;
25
+ export declare function captureFilePathAdmission(workdir: string, inputPath: string): FilePathAdmission;
26
+ export declare function revalidateFilePathAdmission(workdir: string, inputPath: string, captured: unknown): NormalizedToolResult | undefined;
27
+ export interface BomSplitText {
28
+ readonly content: string;
29
+ readonly hadBom: boolean;
30
+ }
31
+ export declare function splitUtf8Bom(raw: string): BomSplitText;
32
+ export declare function loadTextFile(path: string, limits: {
33
+ readonly maxFileBytes: number;
34
+ }): LoadedTextFile | LoadFileFailure;
35
+ export declare function saveTextFile(path: string, content: string, hadBom: boolean): void;
@@ -0,0 +1 @@
1
+ import{lstatSync as t,mkdirSync as e,readFileSync as r,realpathSync as o,statSync as n,writeFileSync as i}from"node:fs";import{basename as a,dirname as c,isAbsolute as s,relative as u,resolve as l,sep as f}from"node:path";import{TOOL_OUTCOME_KINDS as d,failedToolResult as p}from"../normalize-result.js";import{RAW_NUL_LABEL as m}from"./control-chars.js";const h="\ufeff";export const FILE_CONTAINMENT_DRIFT_MESSAGE="文件路径的安全条件在准入后发生变化,已在执行前阻止;请重新提交以重新确认";export function resolveFilePath(t,e){return s(e)?l(e):l(t,e)}export function canonicalFileKey(t){try{return o.native(t)}catch{return t}}export function canonicalResourcePath(t){const e=x(t);return e.ok?e.path:t}function k(t){return""===t||".."!==t&&!t.startsWith(`..${f}`)&&!s(t)}function x(e){let r=e;const n=[];for(;;)try{const t=o.native(r);return{ok:!0,path:l(t,...[...n].reverse())}}catch{try{if(t(r).isSymbolicLink())return{ok:!1}}catch{}const e=c(r);if(e===r)return{ok:!1};n.push(a(r)),r=e}}export function escapesWorkdir(t,e){let r;try{r=o.native(t)}catch{return!0}const n=x(resolveFilePath(t,e));return!n.ok||!k(u(r,n.path))}export function formatPathForApproval(t,e){const r=resolveFilePath(t,e);if(escapesWorkdir(t,e)){const t=x(r);return`${t.ok?t.path:r}(工作目录外)`}let n=t;try{n=o.native(t)}catch{}const i=x(r),a=u(n,i.ok?i.path:r);return""===a?".":a}function F(t,e){const r=x(resolveFilePath(t,e));return r.ok?r.path:void 0}export function captureFilePathAdmission(t,e){return{admittedExternal:escapesWorkdir(t,e),admittedTarget:F(t,e)}}function y(t){if("object"!=typeof t||null===t)return!1;const e=t;return"boolean"==typeof e.admittedExternal&&(void 0===e.admittedTarget||"string"==typeof e.admittedTarget)}export function revalidateFilePathAdmission(t,e,r){if(!y(r))return;const o=escapesWorkdir(t,e),n=F(t,e),i=!r.admittedExternal&&o,a=r.admittedTarget!==n;return i||a?p(d.toolFailed,FILE_CONTAINMENT_DRIFT_MESSAGE):void 0}function g(t){return-1!==t.indexOf(0)}export function splitUtf8Bom(t){const e=t.startsWith(h);return{content:e?t.slice(1):t,hadBom:e}}export function loadTextFile(t,e){let o;try{const e=n(t);if(e.isDirectory())return{ok:!1,code:"is-directory",message:`${t} 是目录,浏览目录请用 roll__list_dir`};if(!e.isFile())return{ok:!1,code:"not-regular-file",message:`${t} 不是普通文件,文件工具仅支持常规文本文件`};o=e.size}catch{return{ok:!1,code:"not-found",message:`文件不存在: ${t}`}}if(o>e.maxFileBytes)return{ok:!1,code:"too-large",message:`文件过大(${String(o)} 字节,上限 ${String(e.maxFileBytes)} 字节),文件工具仅支持文本文件`};const i=r(t);if(g(i))return{ok:!1,code:"binary",message:`${t} 是二进制文件(含原始 NUL 字节 ${m}),文件工具仅支持文本。若确实需要处理该文件,请用 shell 命令。`};const{content:a,hadBom:c}=splitUtf8Bom(i.toString("utf8"));return{ok:!0,path:t,key:canonicalFileKey(t),content:a,hadBom:c,suspectEncoding:a.includes("�")}}export function saveTextFile(t,r,o){e(c(t),{recursive:!0}),i(t,o?`${h}${r}`:r,"utf8")}
@@ -0,0 +1,11 @@
1
+ export declare const FILE_FRESHNESS: {
2
+ readonly fresh: "fresh";
3
+ readonly stale: "stale";
4
+ readonly unread: "unread";
5
+ };
6
+ export type FileFreshness = (typeof FILE_FRESHNESS)[keyof typeof FILE_FRESHNESS];
7
+ export declare class FileStateTracker {
8
+ private readonly digests;
9
+ recordKnownContent(key: string, content: string): void;
10
+ checkFreshness(key: string, currentContent: string): FileFreshness;
11
+ }
@@ -0,0 +1 @@
1
+ import{createHash as e}from"node:crypto";export const FILE_FRESHNESS={fresh:"fresh",stale:"stale",unread:"unread"};const s=512;function t(s){return e("sha256").update(s,"utf8").digest("hex")}export class FileStateTracker{digests=new Map;recordKnownContent(e,i){if(this.digests.has(e))this.digests.delete(e);else if(this.digests.size>=s){const e=this.digests.keys().next().value;void 0!==e&&this.digests.delete(e)}this.digests.set(e,t(i))}checkFreshness(e,s){const i=this.digests.get(e);return void 0===i?FILE_FRESHNESS.unread:i===t(s)?FILE_FRESHNESS.fresh:FILE_FRESHNESS.stale}}
@@ -0,0 +1,21 @@
1
+ import { type ToolSet } from "ai";
2
+ import { z } from "zod";
3
+ import type { ToolRegistry } from "../naming.ts";
4
+ import type { ToolBridgeContext } from "../build-tools.ts";
5
+ import { type NormalizedToolResult } from "../normalize-result.ts";
6
+ import { type ResolvedFileToolsSettings } from "./settings.ts";
7
+ export declare const GLOB_TOOL_NAME = "glob";
8
+ declare const globInputSchema: z.ZodObject<{
9
+ pattern: z.ZodString;
10
+ path: z.ZodOptional<z.ZodString>;
11
+ }, "strip", z.ZodTypeAny, {
12
+ pattern: string;
13
+ path?: string | undefined;
14
+ }, {
15
+ pattern: string;
16
+ path?: string | undefined;
17
+ }>;
18
+ export type GlobInput = z.infer<typeof globInputSchema>;
19
+ export declare function executeGlob(settings: ResolvedFileToolsSettings, input: GlobInput, abortSignal?: AbortSignal): Promise<NormalizedToolResult>;
20
+ export declare function buildGlobTool(settings: ResolvedFileToolsSettings, registry: ToolRegistry, ctx: ToolBridgeContext): ToolSet;
21
+ export {};
@@ -0,0 +1 @@
1
+ import{statSync as t}from"node:fs";import{resolve as r}from"node:path";import{tool as e}from"ai";import{z as o}from"zod";import{gateToolCall as n}from"../build-tools.js";import{TOOL_OUTCOME_KINDS as i,failedToolResult as a,successfulToolResult as s,toolResultToModelOutput as c}from"../normalize-result.js";import{OPAQUE_SIDE_EFFECT_RESOURCE as p,TOOL_RESOURCE_ACCESS_MODES as l,executeCoordinatedTool as u}from"../tool-execution-coordinator.js";import{canonicalResourcePath as d,captureFilePathAdmission as m,escapesWorkdir as f,resolveFilePath as g,revalidateFilePathAdmission as h}from"./file-io.js";import{runRg as b}from"./rg-exec.js";import{gateExternalPath as x}from"./external-approval.js";import{FILE_TOOLS_AGENT_NAME as y}from"./settings.js";export const GLOB_TOOL_NAME="glob";const k=200,j="ripgrep 输出过多已被截断,文件列表可能不完整,请缩小 path 或使用更精确的 pattern",w=o.object({pattern:o.string().min(1).describe('文件名 glob,如 "**/*.md" 或 "src/**/config.*"'),path:o.string().min(1).optional().describe("起始目录,相对当前工作目录或绝对路径,默认工作目录")}),M={readOnlyHint:!0};function v(t,r){return["--files","--glob",t,r]}function I(r){try{return t(r).mtimeMs}catch{return Number.NEGATIVE_INFINITY}}function S(t){return t.map(t=>({path:t,mtimeMs:I(t)})).map((t,r)=>({entry:t,index:r})).sort((t,r)=>r.entry.mtimeMs!==t.entry.mtimeMs?r.entry.mtimeMs-t.entry.mtimeMs:t.index-r.index).map(({entry:t})=>t.path)}function _(t,r){return t>k?`共 ${String(t)} 个文件,仅显示前 200 个(按修改时间倒序)`:r?j:void 0}function E(r){try{return t(r).isFile()}catch{return!1}}export async function executeGlob(t,e,o){const n=g(t.workdir,e.path??".");if(E(n))return a(i.invalidInput,`${n} 是文件而非目录:读取内容用 roll__read_file,按 pattern 找文件请把 path 设为起始目录`);const c=await b(v(e.pattern,n),t.workdir,{...o?{abortSignal:o}:{}});if(!0===c.cancelled)return a(i.cancelled,"已取消");if(!c.ok)return a(i.toolFailed,c.errorMessage??"rg 执行失败");const p=c.stdout.split("\n").filter(t=>t.length>0);if(0===p.length)return s(`未找到匹配 ${e.pattern} 的文件。`);const l=S(p.map(e=>r(t.workdir,e))),u=[l.slice(0,k).join("\n")],d=_(l.length,c.truncated);return void 0!==d&&u.push(d),s(u.join("\n\n"))}export function buildGlobTool(t,r,o){const s=r.register(y,"glob",{annotations:M}),b={prepare:async r=>{const e=w.safeParse(r);if(!e.success)return a(i.invalidInput,"参数校验失败: pattern 必须为非空字符串,path 须为非空字符串");if(f(t.workdir,e.data.path??".")){const t=await x(o,"glob",e.data,s);if(void 0!==t)return t}return n(o,y,"glob",e.data,M)},resources:r=>{const e=w.safeParse(r),o=e.success?`file:${d(g(t.workdir,e.data.path??"."))}`:`file-tools:${t.workdir}`;return[{key:p,mode:l.read},{key:o,mode:l.read}]},captureExecutionState:r=>{const e=w.safeParse(r);return e.success?m(t.workdir,e.data.path??"."):void 0},revalidateExecution:(r,e)=>{const o=w.safeParse(r);if(o.success)return h(t.workdir,o.data.path??".",e)}};return o.coordinator?.register(s,b),{[s]:e({description:'按文件名 glob 模式查找文件(如 "**/*.md"、"src/**/config.*"),结果按修改时间倒序排列,最多返回 200 条。默认在当前工作目录递归查找,可用 path 指定起始目录。',inputSchema:w,toModelOutput:({output:t})=>c(t),execute:(r,e)=>u(o.coordinator,b,s,e.toolCallId,r,e.abortSignal,()=>executeGlob(t,r,e.abortSignal))})}}
@@ -0,0 +1,33 @@
1
+ import { type ToolSet } from "ai";
2
+ import { z } from "zod";
3
+ import type { ToolRegistry } from "../naming.ts";
4
+ import type { ToolBridgeContext } from "../build-tools.ts";
5
+ import { type NormalizedToolResult } from "../normalize-result.ts";
6
+ import { type ResolvedFileToolsSettings } from "./settings.ts";
7
+ export declare const GREP_TOOL_NAME = "grep";
8
+ declare const grepInputSchema: z.ZodObject<{
9
+ pattern: z.ZodString;
10
+ path: z.ZodOptional<z.ZodString>;
11
+ glob: z.ZodOptional<z.ZodString>;
12
+ context: z.ZodOptional<z.ZodNumber>;
13
+ ignore_case: z.ZodOptional<z.ZodBoolean>;
14
+ max_results: z.ZodOptional<z.ZodNumber>;
15
+ }, "strip", z.ZodTypeAny, {
16
+ pattern: string;
17
+ path?: string | undefined;
18
+ context?: number | undefined;
19
+ max_results?: number | undefined;
20
+ glob?: string | undefined;
21
+ ignore_case?: boolean | undefined;
22
+ }, {
23
+ pattern: string;
24
+ path?: string | undefined;
25
+ context?: number | undefined;
26
+ max_results?: number | undefined;
27
+ glob?: string | undefined;
28
+ ignore_case?: boolean | undefined;
29
+ }>;
30
+ export type GrepInput = z.infer<typeof grepInputSchema>;
31
+ export declare function executeGrep(settings: ResolvedFileToolsSettings, input: GrepInput, abortSignal?: AbortSignal): Promise<NormalizedToolResult>;
32
+ export declare function buildGrepTool(settings: ResolvedFileToolsSettings, registry: ToolRegistry, ctx: ToolBridgeContext): ToolSet;
33
+ export {};
@@ -0,0 +1 @@
1
+ import{resolve as t}from"node:path";import{tool as e}from"ai";import{z as r}from"zod";import{gateToolCall as o}from"../build-tools.js";import{TOOL_OUTCOME_KINDS as n,failedToolResult as i,successfulToolResult as a,toolResultToModelOutput as s}from"../normalize-result.js";import{OPAQUE_SIDE_EFFECT_RESOURCE as c,TOOL_RESOURCE_ACCESS_MODES as u,executeCoordinatedTool as l}from"../tool-execution-coordinator.js";import{canonicalResourcePath as p,captureFilePathAdmission as d,escapesWorkdir as m,resolveFilePath as f,revalidateFilePathAdmission as g}from"./file-io.js";import{normalizeForMatch as x}from"./text-normalize.js";import{runRg as h}from"./rg-exec.js";import{gateExternalPath as b}from"./external-approval.js";import{boundedIntParam as w,describeZodIssues as j}from"../bounded-param.js";import{FILE_TOOLS_AGENT_NAME as v}from"./settings.js";export const GREP_TOOL_NAME="grep";const _=100,k=500,y="结果过多已截断,请缩小范围(加 glob 或更精确的 pattern)",C="(输出达上限已截断,请缩小范围)",S=w({name:"context",min:0,max:10,description:"每处命中前后附带的行数",defaultNote:"默认 0"}),$=w({name:"max_results",min:1,max:500,description:"最多返回的命中行数",defaultNote:"默认 100"}),M=r.object({pattern:r.string().min(1).describe("搜索正则(ripgrep 语法)"),path:r.string().min(1).optional().describe("搜索目录或文件,相对当前工作目录或绝对路径,默认工作目录"),glob:r.string().min(1).optional().describe('按 glob 过滤文件,如 "**/*.ts"'),context:S.schema,ignore_case:r.boolean().optional().describe("忽略大小写,默认 false"),max_results:$.schema}),O={readOnlyHint:!0};function N(t,e,r){return["--null","--line-number","--no-heading","--with-filename","--color","never",...t.ignore_case?["-i"]:[],...t.context?["-C",String(t.context)]:[],...t.glob?["-g",t.glob]:[],"--max-count",String(r+1),"-e",t.pattern,e]}const P=/^(\d+)([-:])(.*)$/u;function E(t){const e=t.indexOf("\0");if(-1===e)return;const r=t.slice(0,e),o=t.slice(e+1),n=P.exec(o);if(null===n)return;const i=n[1],a=n[2],s=n[3];return void 0!==i&&void 0!==a&&void 0!==s?{path:r,lineNumber:Number(i),content:s,isMatch:":"===a}:void 0}function G(t,e){const r=e.length>k?`${e.slice(0,k)}…`:e;return`${String(t).padStart(5)}→${r}`}function I(e,r,o){const n=new Map,i=[];let a=0,s=!1;for(const c of r.split("\n")){if(0===c.length)continue;const r=E(c);if(void 0===r)continue;if(r.isMatch){if(a>=o){s=!0;break}a+=1}const u=t(e,r.path);let l=n.get(u);void 0===l&&(l=[],n.set(u,l),i.push(u)),l.push(G(r.lineNumber,r.content))}return{body:i.map(t=>`${t}\n${(n.get(t)??[]).join("\n")}`).join("\n\n"),matchCount:a,fileCount:i.length,truncatedByMaxResults:s}}function z(t,e){if(t.length<=e)return{body:t,wasCapped:!1};const r=t.lastIndexOf("\n",e);return{body:t.slice(0,r>0?r:e),wasCapped:!0}}export async function executeGrep(t,e,r){const o=M.safeParse(e);if(!o.success)return i(n.invalidInput,j(o.error,e)??"参数校验失败");const s=f(t.workdir,e.path??"."),c=e.max_results??_,u=await h(N(e,s,c),t.workdir,{...r?{abortSignal:r}:{}});if(!0===u.cancelled)return i(n.cancelled,"已取消");if(!u.ok)return i(n.toolFailed,u.errorMessage??"rg 执行失败");const l=I(t.workdir,u.stdout,c);if(0===l.matchCount){const t=x(e.pattern),r=["未找到匹配。"];return t.text!==e.pattern&&r.push(`pattern 含全角/智能标点,文件中可能是半角形式,试试:${t.text}`),a(r.join("\n"))}const p=z(l.body,t.maxOutputChars),d=[`共 ${String(l.matchCount)} 处命中(${String(l.fileCount)} 个文件):`,p.body];return(l.truncatedByMaxResults||u.truncated)&&d.push(y),p.wasCapped&&d.push(C),a(d.join("\n\n"))}export function buildGrepTool(t,r,a){const x=r.register(v,"grep",{annotations:O}),h={prepare:async e=>{const r=M.safeParse(e);if(!r.success)return i(n.invalidInput,j(r.error,e)??"参数校验失败: pattern 必须为非空字符串,其余参数须符合类型与范围限制");if(m(t.workdir,r.data.path??".")){const t=await b(a,"grep",r.data,x);if(void 0!==t)return t}return o(a,v,"grep",r.data,O)},resources:e=>{const r=M.safeParse(e),o=r.success?`file:${p(f(t.workdir,r.data.path??"."))}`:`file-tools:${t.workdir}`;return[{key:c,mode:u.read},{key:o,mode:u.read}]},captureExecutionState:e=>{const r=M.safeParse(e);return r.success?d(t.workdir,r.data.path??"."):void 0},revalidateExecution:(e,r)=>{const o=M.safeParse(e);if(o.success)return g(t.workdir,o.data.path??".",r)}};return a.coordinator?.register(x,h),{[x]:e({description:'在文件中搜索正则匹配(ripgrep 语法)。输出按文件分组,命中行前缀行号(如 " 12→",前缀不是文件内容),可与 roll__read_file / roll__edit_file 的行号衔接。默认在当前工作目录递归搜索,可用 path 指定子目录或单个文件、glob 过滤文件、context 附带上下文行。',inputSchema:M,toModelOutput:({output:t})=>s(t),execute:(e,r)=>l(a.coordinator,h,x,r.toolCallId,e,r.abortSignal,()=>executeGrep(t,e,r.abortSignal))})}}
@@ -0,0 +1,18 @@
1
+ import type { ToolSet } from "ai";
2
+ import type { ToolRegistry } from "../naming.ts";
3
+ import type { ToolBridgeContext } from "../build-tools.ts";
4
+ import { type SessionFileToolsSettings } from "./settings.ts";
5
+ export type { SessionFileToolsSettings } from "./settings.ts";
6
+ export { READ_FILE_TOOL_NAME } from "./read-file-tool.ts";
7
+ export { LIST_DIR_TOOL_NAME } from "./list-dir-tool.ts";
8
+ export { EDIT_FILE_TOOL_NAME } from "./edit-file-tool.ts";
9
+ export { WRITE_FILE_TOOL_NAME } from "./write-file-tool.ts";
10
+ export { GREP_TOOL_NAME } from "./grep-tool.ts";
11
+ export { GLOB_TOOL_NAME } from "./glob-tool.ts";
12
+ export { VERIFY_FILE_TOOL_NAME } from "./verify-file-tool.ts";
13
+ export interface BuiltFileToolset {
14
+ readonly readTools: ToolSet;
15
+ readonly editTools: ToolSet;
16
+ readonly verifyTools: ToolSet;
17
+ }
18
+ export declare function buildFileToolset(settings: SessionFileToolsSettings, registry: ToolRegistry, ctx: ToolBridgeContext): BuiltFileToolset;
@@ -0,0 +1 @@
1
+ import{FileStateTracker as o}from"./file-state-tracker.js";import{resolveFileToolsSettings as r}from"./settings.js";import{buildReadFileTool as t}from"./read-file-tool.js";import{buildListDirTool as e}from"./list-dir-tool.js";import{buildEditFileTool as i}from"./edit-file-tool.js";import{buildWriteFileTool as l}from"./write-file-tool.js";import{buildGrepTool as f}from"./grep-tool.js";import{buildGlobTool as s}from"./glob-tool.js";import{buildVerifyFileTool as m}from"./verify-file-tool.js";export{READ_FILE_TOOL_NAME}from"./read-file-tool.js";export{LIST_DIR_TOOL_NAME}from"./list-dir-tool.js";export{EDIT_FILE_TOOL_NAME}from"./edit-file-tool.js";export{WRITE_FILE_TOOL_NAME}from"./write-file-tool.js";export{GREP_TOOL_NAME}from"./grep-tool.js";export{GLOB_TOOL_NAME}from"./glob-tool.js";export{VERIFY_FILE_TOOL_NAME}from"./verify-file-tool.js";export function buildFileToolset(p,_,j){const E=r(p),O=new o;return{readTools:{...t(E,O,_,j),...e(E,_,j),...f(E,_,j),...s(E,_,j)},editTools:{...i(E,O,_,j),...l(E,O,_,j)},verifyTools:{...m(E,_,j)}}}