@roll-agent/runtime 0.15.1 → 0.17.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 (90) 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 +34 -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/compaction-checkpoint.d.ts +24 -24
  24. package/dist/engine/compactor.d.ts +3 -0
  25. package/dist/engine/compactor.js +1 -1
  26. package/dist/engine/context-window.js +1 -1
  27. package/dist/engine/conversation-engine.d.ts +8 -0
  28. package/dist/engine/conversation-engine.js +1 -1
  29. package/dist/engine/system-prompt.d.ts +16 -1
  30. package/dist/engine/system-prompt.js +1 -1
  31. package/dist/engine/workspace-instructions.d.ts +33 -0
  32. package/dist/engine/workspace-instructions.js +1 -0
  33. package/dist/index.d.ts +4 -0
  34. package/dist/index.js +1 -1
  35. package/dist/server/runtime-protocol-adapter.js +1 -1
  36. package/dist/service/runtime-service.d.ts +2 -1
  37. package/dist/service/runtime-service.js +1 -1
  38. package/dist/store/thread-store.d.ts +19 -2
  39. package/dist/store/thread-store.js +1 -1
  40. package/dist/tool-bridge/bash-tool.d.ts +3 -0
  41. package/dist/tool-bridge/bash-tool.js +1 -1
  42. package/dist/tool-bridge/bounded-param.d.ts +14 -0
  43. package/dist/tool-bridge/bounded-param.js +1 -0
  44. package/dist/tool-bridge/build-tools.d.ts +8 -0
  45. package/dist/tool-bridge/build-tools.js +1 -1
  46. package/dist/tool-bridge/file-tools/control-chars.d.ts +11 -0
  47. package/dist/tool-bridge/file-tools/control-chars.js +1 -0
  48. package/dist/tool-bridge/file-tools/edit-file-tool.d.ts +43 -0
  49. package/dist/tool-bridge/file-tools/edit-file-tool.js +1 -0
  50. package/dist/tool-bridge/file-tools/edit-plan.d.ts +19 -0
  51. package/dist/tool-bridge/file-tools/edit-plan.js +1 -0
  52. package/dist/tool-bridge/file-tools/external-approval.d.ts +3 -0
  53. package/dist/tool-bridge/file-tools/external-approval.js +1 -0
  54. package/dist/tool-bridge/file-tools/file-change-result.d.ts +11 -0
  55. package/dist/tool-bridge/file-tools/file-change-result.js +1 -0
  56. package/dist/tool-bridge/file-tools/file-io.d.ts +35 -0
  57. package/dist/tool-bridge/file-tools/file-io.js +1 -0
  58. package/dist/tool-bridge/file-tools/file-state-tracker.d.ts +11 -0
  59. package/dist/tool-bridge/file-tools/file-state-tracker.js +1 -0
  60. package/dist/tool-bridge/file-tools/glob-tool.d.ts +21 -0
  61. package/dist/tool-bridge/file-tools/glob-tool.js +1 -0
  62. package/dist/tool-bridge/file-tools/grep-tool.d.ts +33 -0
  63. package/dist/tool-bridge/file-tools/grep-tool.js +1 -0
  64. package/dist/tool-bridge/file-tools/index.d.ts +18 -0
  65. package/dist/tool-bridge/file-tools/index.js +1 -0
  66. package/dist/tool-bridge/file-tools/list-dir-tool.d.ts +18 -0
  67. package/dist/tool-bridge/file-tools/list-dir-tool.js +1 -0
  68. package/dist/tool-bridge/file-tools/match-pipeline.d.ts +22 -0
  69. package/dist/tool-bridge/file-tools/match-pipeline.js +1 -0
  70. package/dist/tool-bridge/file-tools/read-file-tool.d.ts +25 -0
  71. package/dist/tool-bridge/file-tools/read-file-tool.js +1 -0
  72. package/dist/tool-bridge/file-tools/rg-exec.d.ts +12 -0
  73. package/dist/tool-bridge/file-tools/rg-exec.js +1 -0
  74. package/dist/tool-bridge/file-tools/settings.d.ts +12 -0
  75. package/dist/tool-bridge/file-tools/settings.js +1 -0
  76. package/dist/tool-bridge/file-tools/text-diff.d.ts +33 -0
  77. package/dist/tool-bridge/file-tools/text-diff.js +1 -0
  78. package/dist/tool-bridge/file-tools/text-normalize.d.ts +5 -0
  79. package/dist/tool-bridge/file-tools/text-normalize.js +1 -0
  80. package/dist/tool-bridge/file-tools/verifier-registry.d.ts +42 -0
  81. package/dist/tool-bridge/file-tools/verifier-registry.js +1 -0
  82. package/dist/tool-bridge/file-tools/verify-file-tool.d.ts +30 -0
  83. package/dist/tool-bridge/file-tools/verify-file-tool.js +1 -0
  84. package/dist/tool-bridge/file-tools/write-file-tool.d.ts +22 -0
  85. package/dist/tool-bridge/file-tools/write-file-tool.js +1 -0
  86. package/dist/tool-bridge/session-exec-tool.js +1 -1
  87. package/dist/tool-bridge/tool-execution-coordinator.d.ts +15 -2
  88. package/dist/tool-bridge/tool-execution-coordinator.js +1 -1
  89. package/dist/types/events.d.ts +3 -1
  90. package/package.json +5 -3
@@ -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)}}}
@@ -0,0 +1,18 @@
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 LIST_DIR_TOOL_NAME = "list_dir";
8
+ declare const listDirInputSchema: z.ZodObject<{
9
+ path: z.ZodOptional<z.ZodString>;
10
+ }, "strip", z.ZodTypeAny, {
11
+ path?: string | undefined;
12
+ }, {
13
+ path?: string | undefined;
14
+ }>;
15
+ export type ListDirInput = z.infer<typeof listDirInputSchema>;
16
+ export declare function executeListDir(settings: ResolvedFileToolsSettings, input: ListDirInput): NormalizedToolResult;
17
+ export declare function buildListDirTool(settings: ResolvedFileToolsSettings, registry: ToolRegistry, ctx: ToolBridgeContext): ToolSet;
18
+ export {};
@@ -0,0 +1 @@
1
+ import{readdirSync as t,statSync as r}from"node:fs";import{resolve as e}from"node:path";import{tool as o}from"ai";import{z as i}from"zod";import{gateToolCall as n}from"../build-tools.js";import{TOOL_OUTCOME_KINDS as s,failedToolResult as a,successfulToolResult as c,toolResultToModelOutput as d}from"../normalize-result.js";import{OPAQUE_SIDE_EFFECT_RESOURCE as p,TOOL_RESOURCE_ACCESS_MODES as u,executeCoordinatedTool as l}from"../tool-execution-coordinator.js";import{canonicalResourcePath as m,captureFilePathAdmission as f,escapesWorkdir as h,resolveFilePath as g,revalidateFilePathAdmission as y}from"./file-io.js";import{gateExternalPath as x}from"./external-approval.js";import{FILE_TOOLS_AGENT_NAME as $}from"./settings.js";export const LIST_DIR_TOOL_NAME="list_dir";const j=300,k=i.object({path:i.string().min(1).optional().describe("目录路径,相对当前工作目录或绝对路径,默认当前工作目录")}),w={readOnlyHint:!0};function D(t){try{return`(${String(r(t).size)} 字节)`}catch{return""}}export function executeListDir(r,o){const i=g(r.workdir,o.path??".");let n;try{n=t(i,{withFileTypes:!0})}catch{return a(s.invalidInput,`目录不存在或不可读: ${i}`)}const d=[...n].sort((t,r)=>t.isDirectory()!==r.isDirectory()?t.isDirectory()?-1:1:t.name.localeCompare(r.name,"zh-Hans-CN")),p=d.slice(0,j),u=p.map(t=>t.isDirectory()?`${t.name}/`:`${t.name}${D(e(i,t.name))}`),l=[`目录: ${i}`,u.join("\n")];return d.length>p.length&&l.push(`(仅显示前 ${String(j)} 项(共 ${String(d.length)} 项))`),c(l.join("\n"))}export function buildListDirTool(t,r,e){const i=r.register($,"list_dir",{annotations:w}),c={prepare:async r=>{const o=k.safeParse(r);if(!o.success)return a(s.invalidInput,"参数校验失败: path 须为非空字符串");if(h(t.workdir,o.data.path??".")){const t=await x(e,"list_dir",o.data,i);if(void 0!==t)return t}return n(e,$,"list_dir",o.data,w)},resources:r=>{const e=k.safeParse(r),o=e.success?`file:${m(g(t.workdir,e.data.path??"."))}`:`file-tools:${t.workdir}`;return[{key:p,mode:u.read},{key:o,mode:u.read}]},captureExecutionState:r=>{const e=k.safeParse(r);return e.success?f(t.workdir,e.data.path??"."):void 0},revalidateExecution:(r,e)=>{const o=k.safeParse(r);if(o.success)return y(t.workdir,o.data.path??".",e)}};return e.coordinator?.register(i,c),{[i]:o({description:"列出目录内容(目录带 / 后缀,文件附字节数)。默认列出当前工作目录。",inputSchema:k,toModelOutput:({output:t})=>d(t),execute:(r,o)=>l(e.coordinator,c,i,o.toolCallId,r,o.abortSignal,()=>Promise.resolve(executeListDir(t,r)))})}}
@@ -0,0 +1,22 @@
1
+ export interface MatchSpan {
2
+ readonly start: number;
3
+ readonly end: number;
4
+ }
5
+ export type MatchResult = {
6
+ readonly kind: "unique";
7
+ readonly span: MatchSpan;
8
+ readonly viaNormalization: boolean;
9
+ } | {
10
+ readonly kind: "multiple";
11
+ readonly spans: readonly MatchSpan[];
12
+ readonly viaNormalization: boolean;
13
+ } | {
14
+ readonly kind: "none";
15
+ };
16
+ export declare function findAllExact(content: string, needle: string): MatchSpan[];
17
+ export declare function findOldString(content: string, oldString: string): MatchResult;
18
+ export declare function renderNumberedLines(lines: readonly string[], firstLineNumber: number): string;
19
+ export declare function lineNumberAt(content: string, offset: number): number;
20
+ export declare function lineNumberPrefixWarning(oldString: string): string | undefined;
21
+ export declare function formatNoMatchDiagnosis(content: string, oldString: string): string;
22
+ export declare function formatMultiMatchDiagnosis(content: string, spans: readonly MatchSpan[]): string;
@@ -0,0 +1 @@
1
+ import{normalizeForMatch as n}from"./text-normalize.js";export function findAllExact(n,t){if(0===t.length)return[];const e=[];let i=0;for(;i<=n.length-t.length;){const r=n.indexOf(t,i);if(-1===r)break;e.push({start:r,end:r+t.length}),i=r+t.length}return e}function t(n,t,e,i){const r=n.map[t],l=n.map[t+e-1];return{start:r??0,end:void 0===l?i:l+1}}export function findOldString(e,i){const r=findAllExact(e,i),l=r[0];if(void 0!==l&&1===r.length)return{kind:"unique",span:l,viaNormalization:!1};if(r.length>1)return{kind:"multiple",spans:r,viaNormalization:!1};const o=n(e),a=n(i).text;if(0===a.length)return{kind:"none"};const s=findAllExact(o.text,a).map(n=>t(o,n.start,a.length,e.length)),h=s[0];return void 0!==h&&1===s.length?{kind:"unique",span:h,viaNormalization:!0}:s.length>1?{kind:"multiple",spans:s,viaNormalization:!0}:{kind:"none"}}export function renderNumberedLines(n,t){return n.map((n,e)=>`${String(t+e).padStart(5)}→${n}`).join("\n")}export function lineNumberAt(n,t){let e=1;for(let i=0;i<t&&i<n.length;i+=1)"\n"===n.charAt(i)&&(e+=1);return e}function e(n,t,e){const i=n.split("\n"),r=Math.max(1,t-e),l=Math.min(i.length,t+e);return renderNumberedLines(i.slice(r-1,l),r)}function i(n,t){if(0===n.length||0===t.length)return 0;const e=Math.min(n.length,t.length);let i=0;for(;i<e&&n.charAt(i)===t.charAt(i);)i+=1;let r=0;for(;r<e-i&&n.charAt(n.length-1-r)===t.charAt(t.length-1-r);)r+=1;return(i+r)/Math.max(n.length,t.length)}function r(n,t){return n.length>t?`${n.slice(0,t)}…`:n}function l(n,t){const e=Math.min(n.length,t.length);let i=0;for(;i<e&&n.charAt(i)===t.charAt(i);)i+=1;return`第 ${String(i+1)} 个字符起不同:old_string 为 "${r(n.slice(i),30)}",文件为 "${r(t.slice(i),30)}"`}export function lineNumberPrefixWarning(n){return/^\s*\d+→/m.test(n)?'警告:old_string 疑似包含 read_file 的行号前缀(如 " 12→")。行号前缀不是文件内容,请删除前缀后重试。':void 0}export function formatNoMatchDiagnosis(n,t){const r=["old_string 未在文件中找到匹配。"],o=lineNumberPrefixWarning(t);void 0!==o&&r.push(o);const a=t.split("\n").map(n=>n.trim()).find(n=>n.length>0);if(void 0!==a){const t=n.split("\n");let o=0,s=0;for(let n=0;n<t.length;n+=1){const e=i(a,(t[n]??"").trim());e>s&&(s=e,o=n+1)}if(s>=.3&&o>0){const i=(t[o-1]??"").trim();r.push(`最接近的位置在第 ${String(o)} 行:`),r.push(e(n,o,3)),i!==a&&r.push(l(a,i))}}return r.push("文件可能在你上次读取后有变化,或 old_string 与文件内容存在不可见差异。请重新 read_file 并逐字复制目标内容。"),r.join("\n")}export function formatMultiMatchDiagnosis(n,t){const e=n.split("\n"),i=t.slice(0,8),l=i.map(t=>{const i=lineNumberAt(n,t.start);return` 第 ${String(i)} 行:${r((e[i-1]??"").trim(),60)}`}),o=t.length>i.length?`\n …(共 ${String(t.length)} 处)`:"";return[`old_string 在文件中出现 ${String(t.length)} 次,需要唯一匹配:`,`${l.join("\n")}${o}`,"请在 old_string 中加入更多上下文行使其唯一,或设置 replace_all: true 一次替换全部。"].join("\n")}
@@ -0,0 +1,25 @@
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 READ_FILE_TOOL_NAME = "read_file";
9
+ declare const readFileInputSchema: z.ZodObject<{
10
+ path: z.ZodString;
11
+ offset: z.ZodOptional<z.ZodNumber>;
12
+ limit: z.ZodOptional<z.ZodNumber>;
13
+ }, "strip", z.ZodTypeAny, {
14
+ path: string;
15
+ offset?: number | undefined;
16
+ limit?: number | undefined;
17
+ }, {
18
+ path: string;
19
+ offset?: number | undefined;
20
+ limit?: number | undefined;
21
+ }>;
22
+ export type ReadFileInput = z.infer<typeof readFileInputSchema>;
23
+ export declare function executeReadFile(settings: ResolvedFileToolsSettings, tracker: FileStateTracker, input: ReadFileInput): NormalizedToolResult;
24
+ export declare function buildReadFileTool(settings: ResolvedFileToolsSettings, tracker: FileStateTracker, registry: ToolRegistry, ctx: ToolBridgeContext): ToolSet;
25
+ export {};
@@ -0,0 +1 @@
1
+ import{tool as t}from"ai";import{z as e}from"zod";import{gateToolCall as r}from"../build-tools.js";import{TOOL_OUTCOME_KINDS as o,failedToolResult as i,successfulToolResult as n,toolResultToModelOutput as s}from"../normalize-result.js";import{OPAQUE_SIDE_EFFECT_RESOURCE as a,TOOL_RESOURCE_ACCESS_MODES as l,executeCoordinatedTool as c}from"../tool-execution-coordinator.js";import{renderNumberedLines as u}from"./match-pipeline.js";import{canonicalResourcePath as d,captureFilePathAdmission as p,escapesWorkdir as m,loadTextFile as f,resolveFilePath as h,revalidateFilePathAdmission as g}from"./file-io.js";import{isWithinOutputDumpDir as x}from"../../bash/output-dump.js";import{describeZodIssues as b}from"../bounded-param.js";import{gateExternalPath as j}from"./external-approval.js";import{FILE_TOOLS_AGENT_NAME as k}from"./settings.js";export const READ_FILE_TOOL_NAME="read_file";const w=2e3,$=1e3,v=e.object({path:e.string().min(1).describe("要读取的文件路径,相对当前工作目录或绝对路径"),offset:e.number().int().min(1).optional().describe("起始行号(从 1 开始),默认 1"),limit:e.number().int().min(1).optional().describe("最多返回的行数,默认 2000")}),O={readOnlyHint:!0};function S(t){return t.length>$?`${t.slice(0,$)}…[行截断]`:t}export function executeReadFile(t,e,r){const s=h(t.workdir,r.path),a=f(s,{maxFileBytes:t.maxFileBytes});if(!a.ok)return i(o.invalidInput,a.message);const l=a.content.split("\n"),c=r.offset??1,d=r.limit??w;if(c>l.length)return i(o.invalidInput,`offset ${String(c)} 超出文件行数(共 ${String(l.length)} 行)`);e.recordKnownContent(a.key,a.content);const p=l.slice(c-1,c-1+d).map(S);let m=u(p,c);if(m.length>t.maxOutputChars){const e=m.lastIndexOf("\n",t.maxOutputChars);m=m.slice(0,e>0?e:t.maxOutputChars)}const g=c+(0===m.length?0:m.split("\n").length),x=[`文件: ${s}(共 ${String(l.length)} 行)`,m];return a.suspectEncoding&&x.push("警告:内容含替换字符,文件可能不是 UTF-8 编码。"),g<=l.length&&x.push(`(未展示全部内容,从第 ${String(g)} 行继续,设置 offset: ${String(g)})`),n(x.join("\n"))}export function buildReadFileTool(e,n,u,f){const w=u.register(k,"read_file",{annotations:O}),$={prepare:async t=>{const n=v.safeParse(t);if(!n.success)return i(o.invalidInput,b(n.error,t)??"参数校验失败: path 必须为非空字符串,offset/limit 须为正整数");const s=h(e.workdir,n.data.path);if(m(e.workdir,n.data.path)&&!x(s)){const t=await j(f,"read_file",n.data,w);if(void 0!==t)return t}return r(f,k,"read_file",n.data,O)},resources:t=>{const r=v.safeParse(t),o=r.success?`file:${d(h(e.workdir,r.data.path))}`:`file-tools:${e.workdir}`;return[{key:a,mode:l.read},{key:o,mode:l.read}]},captureExecutionState:t=>{const r=v.safeParse(t);return r.success?p(e.workdir,r.data.path):void 0},revalidateExecution:(t,r)=>{const o=v.safeParse(t);if(o.success)return g(e.workdir,o.data.path,r)}};return f.coordinator?.register(w,$),{[w]:t({description:'读取文本文件内容。输出每行带行号前缀(如 " 12→"),前缀不是文件内容。编辑文件前必须先用本工具读取。',inputSchema:v,toModelOutput:({output:t})=>s(t),execute:(t,r)=>c(f.coordinator,$,w,r.toolCallId,t,r.abortSignal,()=>Promise.resolve(executeReadFile(e,n,t)))})}}
@@ -0,0 +1,12 @@
1
+ export interface RgRunResult {
2
+ readonly ok: boolean;
3
+ readonly stdout: string;
4
+ readonly truncated: boolean;
5
+ readonly cancelled?: boolean;
6
+ readonly errorMessage?: string;
7
+ }
8
+ export declare function runRg(args: readonly string[], cwd: string, options?: {
9
+ readonly timeoutMs?: number;
10
+ readonly maxOutputBytes?: number;
11
+ readonly abortSignal?: AbortSignal;
12
+ }): Promise<RgRunResult>;
@@ -0,0 +1 @@
1
+ import{execFile as t}from"node:child_process";import{rgPath as e}from"@vscode/ripgrep";const r=1e4,o=1048576;function n(t){return t instanceof Error&&"AbortError"===t.name||"object"==typeof t&&null!==t&&"code"in t&&"ABORT_ERR"===t.code}export function runRg(u,s,c={}){const d=c.maxOutputBytes??o;return new Promise(o=>{t(e,[...u],{cwd:s,timeout:c.timeoutMs??r,maxBuffer:2*d,encoding:"utf8",...c.abortSignal?{signal:c.abortSignal}:{}},(t,e,r)=>{if(n(t)||!0===c.abortSignal?.aborted)return void o({ok:!1,stdout:"",truncated:!1,cancelled:!0,errorMessage:"已取消"});const u="ERR_CHILD_PROCESS_STDIO_MAXBUFFER"===t?.code;if(t&&!u&&"number"==typeof t.code&&t.code>1)return void o({ok:!1,stdout:"",truncated:!1,errorMessage:r.trim()||t.message});if(t&&!u&&"number"!=typeof t.code)return void o({ok:!1,stdout:"",truncated:!1,errorMessage:t.message});const s=e;if(Buffer.byteLength(s,"utf8")>d){const t=Buffer.from(s,"utf8").subarray(0,d).toString("utf8"),e=t.lastIndexOf("\n");return void o({ok:!0,stdout:e>0?t.slice(0,e):t,truncated:!0})}o({ok:!0,stdout:s,truncated:!1})})})}
@@ -0,0 +1,12 @@
1
+ export declare const FILE_TOOLS_AGENT_NAME = "roll";
2
+ export interface SessionFileToolsSettings {
3
+ readonly workdir: string;
4
+ readonly maxFileBytes?: number;
5
+ readonly maxOutputChars?: number;
6
+ }
7
+ export interface ResolvedFileToolsSettings {
8
+ readonly workdir: string;
9
+ readonly maxFileBytes: number;
10
+ readonly maxOutputChars: number;
11
+ }
12
+ export declare function resolveFileToolsSettings(input: SessionFileToolsSettings): ResolvedFileToolsSettings;
@@ -0,0 +1 @@
1
+ export const FILE_TOOLS_AGENT_NAME="roll";export function resolveFileToolsSettings(t){return{workdir:t.workdir,maxFileBytes:t.maxFileBytes??2097152,maxOutputChars:t.maxOutputChars??4e4}}
@@ -0,0 +1,33 @@
1
+ import type { FileChangeDiff, FileChangeKind } from "@roll-agent/protocol";
2
+ export declare const FILE_CHANGE_DIFF_LIMITS: {
3
+ readonly maxUnifiedChars: 12000;
4
+ readonly maxInputBytes: 1048576;
5
+ readonly maxEditDistance: 1000;
6
+ readonly contextLines: 3;
7
+ };
8
+ export type LineOp = {
9
+ readonly kind: "equal";
10
+ readonly oldIndex: number;
11
+ readonly newIndex: number;
12
+ } | {
13
+ readonly kind: "delete";
14
+ readonly oldIndex: number;
15
+ } | {
16
+ readonly kind: "insert";
17
+ readonly newIndex: number;
18
+ };
19
+ type FileChangeDiffLimits = {
20
+ readonly [K in keyof typeof FILE_CHANGE_DIFF_LIMITS]: number;
21
+ };
22
+ export interface BuildFileChangeDiffInput {
23
+ readonly path: string;
24
+ readonly change: FileChangeKind;
25
+ readonly before: string;
26
+ readonly after: string;
27
+ readonly limits?: Partial<FileChangeDiffLimits>;
28
+ }
29
+ export declare function splitLinesKeepingNewline(content: string): string[];
30
+ export declare function diffLines(before: readonly string[], after: readonly string[], maxEditDistance: number): LineOp[];
31
+ export declare function changedLineSignature(unified: string): string;
32
+ export declare function buildFileChangeDiff(input: BuildFileChangeDiffInput): FileChangeDiff;
33
+ export {};
@@ -0,0 +1 @@
1
+ export const FILE_CHANGE_DIFF_LIMITS={maxUnifiedChars:12e3,maxInputBytes:1048576,maxEditDistance:1e3,contextLines:3};const e="\";export function splitLinesKeepingNewline(e){if(0===e.length)return[];const n=[];let t=0;for(;t<e.length;){const i=e.indexOf("\n",t);if(-1===i){n.push(e.slice(t));break}n.push(e.slice(t,i+1)),t=i+1}return n}function n(e,n){let t=0;const i=Math.min(e.length,n.length);for(;t<i&&e[t]===n[t];)t+=1;let d=0;for(;d<i-t&&e[e.length-1-d]===n[n.length-1-d];)d+=1;return{prefix:t,suffix:d}}function t(e,n,t){const d=e.length,o=n.length,r=Math.min(d+o,t),l=r+1;let s=new Int32Array(2*r+3);s[l+1]=0;const u=[];for(let t=0;t<=r;t+=1){u.push(s);const r=new Int32Array(s);for(let a=-t;a<=t;a+=2){let f=a===-t||a!==t&&(s[l+a-1]??0)<(s[l+a+1]??0)?s[l+a+1]??0:(s[l+a-1]??0)+1,c=f-a;for(;f<d&&c<o&&e[f]===n[c];)f+=1,c+=1;if(r[l+a]=f,f>=d&&c>=o)return u.push(r),i(u,l,d,o,t)}s=r}}function i(e,n,t,i,d){const o=[];let r=t,l=i;for(let t=d;t>=0;t-=1){const i=e[t];if(void 0===i)break;const d=r-l,s=d===-t||d!==t&&(i[n+d-1]??0)<(i[n+d+1]??0),u=s?d+1:d-1,a=0===t?0:i[n+u]??0,f=a-u;for(;r>a&&l>f;)r-=1,l-=1,o.push({kind:"equal",oldIndex:r,newIndex:l});t>0&&(s?(l-=1,o.push({kind:"insert",newIndex:l})):(r-=1,o.push({kind:"delete",oldIndex:r})))}return o.reverse()}function d(e,n,t,i){const d=[];for(let t=0;t<n;t+=1)d.push({kind:"delete",oldIndex:e+t});for(let e=0;e<i;e+=1)d.push({kind:"insert",newIndex:t+e});return d}function o(e,n,t){return e.map(e=>"equal"===e.kind?{kind:"equal",oldIndex:e.oldIndex+n,newIndex:e.newIndex+t}:"delete"===e.kind?{kind:"delete",oldIndex:e.oldIndex+n}:{kind:"insert",newIndex:e.newIndex+t})}export function diffLines(e,i,r){const{prefix:l,suffix:s}=n(e,i),u=[];for(let e=0;e<l;e+=1)u.push({kind:"equal",oldIndex:e,newIndex:e});const a=e.slice(l,e.length-s),f=i.slice(l,i.length-s),c=t(a,f,r);u.push(...void 0===c?d(l,a.length,l,f.length):o(c,l,l));for(let n=0;n<s;n+=1)u.push({kind:"equal",oldIndex:e.length-s+n,newIndex:i.length-s+n});return u}function r(e,n){const t=e.flatMap((e,n)=>"equal"===e.kind?[]:[n]);if(0===t.length)return[];const i=[];for(const d of t){const t=Math.max(0,d-n),o=Math.min(e.length-1,d+n),r=i.at(-1);void 0!==r&&t<=r.end+1?r.end=Math.max(r.end,o):i.push({start:t,end:o})}return i.map(n=>{const t=e.slice(n.start,n.end+1),i=t.flatMap(e=>"insert"===e.kind?[]:[e.oldIndex]),d=t.flatMap(e=>"delete"===e.kind?[]:[e.newIndex]),o=i[0],r=d[0];return{ops:t,oldStart:void 0===o?l(t,"old"):o+1,oldCount:i.length,newStart:void 0===r?l(t,"new"):r+1,newCount:d.length}})}function l(e,n){const t=e[0];return void 0===t?0:"old"===n?"insert"===t.kind?0:t.oldIndex:"delete"===t.kind?0:t.newIndex}function s(n,t){return t.endsWith("\n")?[`${n}${t.slice(0,-1)}`]:[`${n}${t}`,e]}function u(e,n,t){const i=[`@@ -${String(e.oldStart)},${String(e.oldCount)} +${String(e.newStart)},${String(e.newCount)} @@`];for(const d of e.ops)"equal"===d.kind?i.push(...s(" ",t[d.newIndex]??"")):"delete"===d.kind?i.push(...s("-",n[d.oldIndex]??"")):i.push(...s("+",t[d.newIndex]??""));return i}function a(e,n){let t=0;const i=[];for(const d of e){const e=d.length+1;if(t+e>n)return{text:i.map(e=>`${e}\n`).join(""),truncated:!0};i.push(d),t+=e}return{text:i.map(e=>`${e}\n`).join(""),truncated:!1}}function f(e){let n=0,t=0;for(const i of e)"insert"===i.kind?n+=1:"delete"===i.kind&&(t+=1);return{added:n,removed:t}}export function changedLineSignature(e){return e.split("\n").slice(2).filter(e=>e.startsWith("+")||e.startsWith("-")).join("\n")}export function buildFileChangeDiff(e){const n={...FILE_CHANGE_DIFF_LIMITS,...e.limits},t=splitLinesKeepingNewline(e.before),i=splitLinesKeepingNewline(e.after),d=diffLines(t,i,n.maxEditDistance),o=r(d,n.contextLines),{added:l,removed:s}=f(d);if(Buffer.byteLength(e.before,"utf8")+Buffer.byteLength(e.after,"utf8")>n.maxInputBytes)return{path:e.path,change:e.change,added:l,removed:s,hunks:o.length,truncated:!1};const c=["create"===e.change?"--- /dev/null":`--- a/${e.path}`,`+++ b/${e.path}`],h=o.flatMap(e=>u(e,t,i)),{text:p,truncated:x}=a([...c,...h],n.maxUnifiedChars);return{path:e.path,change:e.change,added:l,removed:s,hunks:o.length,unified:p,truncated:x}}
@@ -0,0 +1,5 @@
1
+ export interface NormalizedText {
2
+ readonly text: string;
3
+ readonly map: readonly number[];
4
+ }
5
+ export declare function normalizeForMatch(input: string): NormalizedText;
@@ -0,0 +1 @@
1
+ const t={" ":" "," ":" ","“":'"',"”":'"',"‘":"'","’":"'","—":"-","–":"-","―":"-",",":",",":":":",";":";","!":"!","?":"?","(":"(",")":")",".":"."};export function normalizeForMatch(n){let o="";const r=[];for(let e=0;e<n.length;e+=1){const c=n.charAt(e);"\r"!==c&&(o+=t[c]??c,r.push(e))}return{text:o,map:r}}
@@ -0,0 +1,42 @@
1
+ export declare const VERIFIER_LEVELS: {
2
+ readonly fast: "fast";
3
+ readonly project: "project";
4
+ };
5
+ export type VerifierLevel = (typeof VERIFIER_LEVELS)[keyof typeof VERIFIER_LEVELS];
6
+ export interface VerifierCommand {
7
+ readonly bin: string;
8
+ readonly args: readonly string[];
9
+ }
10
+ export interface Verifier {
11
+ readonly id: string;
12
+ readonly level: VerifierLevel;
13
+ readonly executesProjectCode: boolean;
14
+ readonly detect: (workdir: string, filePath: string) => boolean;
15
+ readonly command: (workdir: string, filePath: string) => VerifierCommand | "builtin-json" | "builtin-yaml";
16
+ readonly timeoutMs: number;
17
+ }
18
+ export type VerifierOutcome = {
19
+ readonly id: string;
20
+ readonly status: "pass";
21
+ readonly detail?: string;
22
+ } | {
23
+ readonly id: string;
24
+ readonly status: "fail";
25
+ readonly output: string;
26
+ } | {
27
+ readonly id: string;
28
+ readonly status: "skipped";
29
+ readonly reason: string;
30
+ } | {
31
+ readonly id: string;
32
+ readonly status: "cancelled";
33
+ };
34
+ export type BinaryProbe = (bin: string) => boolean;
35
+ export declare function isBinaryOnPath(bin: string, probe?: BinaryProbe): boolean;
36
+ export declare function verifiersForFile(filePath: string): readonly Verifier[];
37
+ export declare function outcomeFromExecution(verifier: Verifier, exitCode: number, stdout: string, stderr: string): VerifierOutcome;
38
+ export declare function interpretEslintOutcome(verifier: Verifier, exitCode: number, stdout: string, stderr: string): VerifierOutcome;
39
+ export declare function runVerifier(verifier: Verifier, workdir: string, filePath: string, options?: {
40
+ readonly maxFileBytes?: number;
41
+ readonly abortSignal?: AbortSignal;
42
+ }): Promise<VerifierOutcome>;
@@ -0,0 +1 @@
1
+ import{execFile as t,spawnSync as e}from"node:child_process";import{existsSync as n,readFileSync as i}from"node:fs";import{extname as s,join as r}from"node:path";import{loadTextFile as o,resolveFilePath as c}from"./file-io.js";export const VERIFIER_LEVELS={fast:"fast",project:"project"};const a=1e4,u=12e4,f=4e3,d=524288,m=3e3,l=new Map;function g(t){try{return void 0===e(t,["--version"],{timeout:m}).error}catch{return!1}}export function isBinaryOnPath(t,e=g){const n=l.get(t);if(void 0!==n)return n;const i=e(t);return l.set(t,i),i}let p;function y(){if(void 0!==p)return p;try{import.meta.resolve("yaml"),p=!0}catch{p=!1}return p}function E(t,e){return r(t,"node_modules",".bin",e)}const x=["eslint.config.js","eslint.config.mjs","eslint.config.cjs","eslint.config.ts"],j=[".eslintrc",".eslintrc.js",".eslintrc.cjs",".eslintrc.yaml",".eslintrc.yml",".eslintrc.json"];function v(t){try{const e=JSON.parse(i(r(t,"package.json"),"utf8"));return"object"==typeof e&&null!==e&&!Array.isArray(e)&&"eslintConfig"in e}catch{return!1}}function h(t){return!!x.some(e=>n(r(t,e)))||(!!j.some(e=>n(r(t,e)))||v(t))}const b=[".ts",".tsx",".mts",".cts",".js",".jsx",".mjs",".cjs"],R=[".ts",".tsx",".mts",".cts"],I=["-I","-c","import sys; compile(open(sys.argv[1], 'rb').read(), sys.argv[1], 'exec')"],S={id:"eslint",level:VERIFIER_LEVELS.fast,executesProjectCode:!0,detect:t=>n(E(t,"eslint"))&&h(t),command:(t,e)=>({bin:E(t,"eslint"),args:["--no-fix","--format","json",e]}),timeoutMs:a},L={id:"tsc",level:VERIFIER_LEVELS.project,executesProjectCode:!0,detect:t=>n(E(t,"tsc"))&&n(r(t,"tsconfig.json")),command:t=>({bin:E(t,"tsc"),args:["--noEmit"]}),timeoutMs:u},F={id:"ruff",level:VERIFIER_LEVELS.fast,executesProjectCode:!1,detect:()=>isBinaryOnPath("ruff"),command:(t,e)=>({bin:"ruff",args:["check","--no-fix",e]}),timeoutMs:a},V={id:"py-compile",level:VERIFIER_LEVELS.fast,executesProjectCode:!1,detect:()=>!isBinaryOnPath("ruff")&&isBinaryOnPath("python3"),command:(t,e)=>({bin:"python3",args:[...I,e]}),timeoutMs:a},P={id:"json",level:VERIFIER_LEVELS.fast,executesProjectCode:!1,detect:()=>!0,command:()=>"builtin-json",timeoutMs:a},_={id:"yaml",level:VERIFIER_LEVELS.fast,executesProjectCode:!1,detect:()=>y(),command:()=>"builtin-yaml",timeoutMs:a},C={id:"bash-syntax",level:VERIFIER_LEVELS.fast,executesProjectCode:!1,detect:()=>isBinaryOnPath("bash"),command:(t,e)=>({bin:"bash",args:["-n",e]}),timeoutMs:a},O={id:"gofmt",level:VERIFIER_LEVELS.fast,executesProjectCode:!1,detect:()=>isBinaryOnPath("gofmt"),command:(t,e)=>({bin:"gofmt",args:["-l",e]}),timeoutMs:a},B={id:"go-vet",level:VERIFIER_LEVELS.project,executesProjectCode:!0,detect:t=>n(r(t,"go.mod"))&&isBinaryOnPath("go"),command:()=>({bin:"go",args:["vet","./..."]}),timeoutMs:u},M={id:"cargo-check",level:VERIFIER_LEVELS.project,executesProjectCode:!0,detect:t=>n(r(t,"Cargo.toml"))&&isBinaryOnPath("cargo"),command:()=>({bin:"cargo",args:["check","--quiet"]}),timeoutMs:u},A=[{extensions:b,verifier:S},{extensions:R,verifier:L},{extensions:[".py"],verifier:F},{extensions:[".py"],verifier:V},{extensions:[".json"],verifier:P},{extensions:[".yaml",".yml"],verifier:_},{extensions:[".sh",".bash"],verifier:C},{extensions:[".go"],verifier:O},{extensions:[".go"],verifier:B},{extensions:[".rs"],verifier:M}];export function verifiersForFile(t){const e=s(t).toLowerCase();return A.filter(t=>t.extensions.includes(e)).map(t=>t.verifier)}function $(t,e){return[t,e].filter(t=>t.length>0).join("\n")}function k(t){return t.length>f?`${t.slice(0,f)}\n…(输出已截断)`:t}export function outcomeFromExecution(t,e,n,i){const s="gofmt"===t.id&&0===e&&n.trim().length>0;if(0===e&&!s)return{id:t.id,status:"pass"};const r=$(n,i);return{id:t.id,status:"fail",output:r.length>0?k(r):`退出码 ${String(e)}`}}function w(t){return t instanceof Error?t.message:String(t)}function J(t,e,n){const i=o(e,{maxFileBytes:n});if(!i.ok)return{id:t.id,status:"fail",output:i.message};try{return JSON.parse(i.content),{id:t.id,status:"pass"}}catch(e){return{id:t.id,status:"fail",output:w(e)}}}async function N(t,e,n){const i=o(e,{maxFileBytes:n});if(!i.ok)return{id:t.id,status:"fail",output:i.message};try{const{parse:e}=await import("yaml");return e(i.content),{id:t.id,status:"pass"}}catch(e){return{id:t.id,status:"fail",output:w(e)}}}const D="文件被 eslint 配置忽略,未实际检查";function T(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}function q(t){return T(t)&&null===t.ruleId&&"string"==typeof t.message&&t.message.startsWith("File ignored")}function H(t){try{const e=JSON.parse(t);return Array.isArray(e)?e:void 0}catch{return}}function U(t){if("string"!=typeof t.message)return;const e="number"==typeof t.line?t.line:0,n="number"==typeof t.column?t.column:0,i=!0===t.fatal||2===t.severity?"error":"warning",s="string"==typeof t.ruleId?` (${t.ruleId})`:"";return`${String(e)}:${String(n)} ${i} ${t.message}${s}`}function W(t){const e=[];for(const n of t)if(T(n)&&Array.isArray(n.messages))for(const t of n.messages){if(!T(t))continue;const n=U(t);void 0!==n&&e.push(n)}return e.length>0?e.join("\n"):void 0}export function interpretEslintOutcome(t,e,n,i){const s=H(n);if(void 0===s){return $(n,i).includes("File ignored because of a matching ignore pattern")?{id:t.id,status:"skipped",reason:D}:outcomeFromExecution(t,e,n,i)}if(0===s.length)return{id:t.id,status:"skipped",reason:D};const r=s[0];if(T(r)&&Array.isArray(r.messages)&&r.messages.some(q))return{id:t.id,status:"skipped",reason:D};if(0===e){const e=T(r)&&"number"==typeof r.warningCount?r.warningCount:0;return e>0?{id:t.id,status:"pass",detail:`${String(e)} 个 warning`}:{id:t.id,status:"pass"}}const o=W(s);return{id:t.id,status:"fail",output:void 0!==o?k(o):`退出码 ${String(e)}`}}function X(t,e){return!0===t.killed?`执行超时(超过 ${String(e)}ms):${t.message}`:t.message}function z(t){return t instanceof Error&&"AbortError"===t.name||"object"==typeof t&&null!==t&&"code"in t&&"ABORT_ERR"===t.code}function G(t,e,n,i){return"eslint"===t.id?interpretEslintOutcome(t,e,n,i):outcomeFromExecution(t,e,n,i)}function K(e,n,i,s){return new Promise(r=>{t(n.bin,n.args,{cwd:i,timeout:e.timeoutMs,maxBuffer:d,encoding:"utf8",...s?{signal:s}:{}},(t,n,i)=>{null!==t?z(t)||!0===s?.aborted?r({id:e.id,status:"cancelled"}):"ERR_CHILD_PROCESS_STDIO_MAXBUFFER"!==t.code?"number"!=typeof t.code?r({id:e.id,status:"fail",output:X(t,e.timeoutMs)}):r(G(e,t.code,n,i)):r({id:e.id,status:"fail",output:k($(n,i))}):r(G(e,0,n,i))})})}export async function runVerifier(t,e,n,i={}){const s=c(e,n),r=t.command(e,s),o=i.maxFileBytes??2097152;return"builtin-json"===r?J(t,s,o):"builtin-yaml"===r?N(t,s,o):K(t,r,e,i.abortSignal)}
@@ -0,0 +1,30 @@
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 VerifierOutcome } from "./verifier-registry.ts";
7
+ import { type ResolvedFileToolsSettings } from "./settings.ts";
8
+ export declare const VERIFY_FILE_TOOL_NAME = "verify_file";
9
+ declare const verifyFileInputSchema: z.ZodObject<{
10
+ path: z.ZodString;
11
+ level: z.ZodOptional<z.ZodEnum<["fast", "project"]>>;
12
+ }, "strip", z.ZodTypeAny, {
13
+ path: string;
14
+ level?: "project" | "fast" | undefined;
15
+ }, {
16
+ path: string;
17
+ level?: "project" | "fast" | undefined;
18
+ }>;
19
+ export type VerifyFileInput = z.infer<typeof verifyFileInputSchema>;
20
+ export declare function renderVerifyReport(absolutePath: string, outcomes: readonly VerifierOutcome[], fastLevelHiddenIds: readonly string[]): string;
21
+ export declare function executeVerifyFile(settings: ResolvedFileToolsSettings, input: VerifyFileInput, abortSignal?: AbortSignal): Promise<NormalizedToolResult>;
22
+ export declare const VERIFY_ADMISSION_DRIFT_MESSAGE = "\u9A8C\u8BC1\u5668\u6761\u4EF6\u5728\u51C6\u5165\u540E\u53D1\u751F\u53D8\u5316\uFF08\u53EF\u7528\u9A8C\u8BC1\u5668\u6216\u8DEF\u5F84\u5F52\u5C5E\u6539\u53D8\uFF09\uFF0C\u5DF2\u5728\u6267\u884C\u524D\u963B\u6B62\uFF1B\u8BF7\u91CD\u65B0\u63D0\u4EA4\u4EE5\u91CD\u65B0\u786E\u8BA4";
23
+ export interface VerifyAdmission {
24
+ readonly external: boolean;
25
+ readonly detectedIds: readonly string[];
26
+ }
27
+ export declare function captureVerifyAdmission(settings: ResolvedFileToolsSettings, input: VerifyFileInput): VerifyAdmission;
28
+ export declare function revalidateVerifyAdmission(settings: ResolvedFileToolsSettings, input: VerifyFileInput, captured: unknown): NormalizedToolResult | undefined;
29
+ export declare function buildVerifyFileTool(settings: ResolvedFileToolsSettings, registry: ToolRegistry, ctx: ToolBridgeContext): ToolSet;
30
+ export {};
@@ -0,0 +1 @@
1
+ import{realpathSync as e,statSync as t}from"node:fs";import{isAbsolute as r,relative as i}from"node:path";import{tool as o}from"ai";import{z as n}from"zod";import{gateToolCall as a}from"../build-tools.js";import{TOOL_OUTCOME_KINDS as s,failedToolResult as d,successfulToolResult as l,toolResultToModelOutput as c}from"../normalize-result.js";import{OPAQUE_SIDE_EFFECT_RESOURCE as f,TOOL_RESOURCE_ACCESS_MODES as u,executeCoordinatedTool as p}from"../tool-execution-coordinator.js";import{canonicalResourcePath as m,escapesWorkdir as y,formatPathForApproval as v,resolveFilePath as $}from"./file-io.js";import{VERIFIER_LEVELS as j,verifiersForFile as k,runVerifier as x}from"./verifier-registry.js";import{gateExternalPath as h}from"./external-approval.js";import{FILE_TOOLS_AGENT_NAME as w}from"./settings.js";export const VERIFY_FILE_TOOL_NAME="verify_file";const I=n.object({path:n.string().min(1).describe("要验证的文件路径"),level:n.enum(["fast","project"]).optional().describe("fast=单文件快速检查(默认);project=项目级完整检查(较慢,需确认)")}),g={readOnlyHint:!0},_={};function b(t,o){if(!r(o))return o;if(y(t,o))return m(o);let n=t;try{n=e.native(t)}catch{}const a=i(n,m(o));return""===a?".":a}function F(e,t,r){const i=e.command(t,r);return"builtin-json"===i||"builtin-yaml"===i?`${e.id}(内置校验)`:[i.bin,...i.args].map(e=>b(t,e)).join(" ")}function S(e){if("cancelled"===e.status)return`– ${e.id} 已取消`;if("pass"===e.status)return void 0===e.detail?`✓ ${e.id} 通过`:`✓ ${e.id} 通过(${e.detail})`;if("fail"===e.status){const t=e.output.split("\n").map(e=>` ${e}`).join("\n");return`✗ ${e.id} 失败:\n${t}`}return`– ${e.id} 跳过(${e.reason})`}function A(e){return`– ${e} 跳过(level=fast 未包含,用 level: "project" 运行)`}export function renderVerifyReport(e,t,r){const i=[...t.map(S),...r.map(A)],o=`验证 ${e}:`,n=i.length>0?[o,...i].join("\n"):o;if(t.some(e=>"fail"===e.status))return`${n}\n\n验证发现问题,请修复后重试`;if(t.every(e=>"skipped"===e.status))return`${n}\n\n该文件类型当前无可用验证器(未安装或未配置),本次未做任何验证`;return`${n}\n\n验证通过(${t.filter(e=>"pass"===e.status).map(e=>e.id).join(", ")})`}function V(e,t){return e.filter(e=>t===j.project||e.level===j.fast)}export async function executeVerifyFile(e,r,i){const o=$(e.workdir,r.path);try{if(!t(o).isFile())return d(s.invalidInput,`${o} 不是普通文件,文件工具仅支持常规文本文件`)}catch{return d(s.invalidInput,`文件不存在: ${o}`)}const n=r.level??j.fast,a=k(o),c=V(a,n),f=n===j.fast?a.filter(e=>e.level===j.project).map(e=>e.id):[],u=[];for(const t of c){if(!t.detect(e.workdir,o)){u.push({id:t.id,status:"skipped",reason:`未安装或未配置 ${t.id}`});continue}if(!0===i?.aborted)return d(s.cancelled,"已取消");const r=await x(t,e.workdir,o,{maxFileBytes:e.maxFileBytes,...i?{abortSignal:i}:{}});if("cancelled"===r.status)return d(s.cancelled,"已取消");u.push(r)}const p=renderVerifyReport(o,u,f);return u.some(e=>"fail"===e.status)?d(s.toolFailed,p):l(p)}export const VERIFY_ADMISSION_DRIFT_MESSAGE="验证器条件在准入后发生变化(可用验证器或路径归属改变),已在执行前阻止;请重新提交以重新确认";function E(e,t){const r=$(e.workdir,t.path),i=t.level??j.fast;return V(k(r),i).filter(t=>t.detect(e.workdir,r))}export function captureVerifyAdmission(e,t){return{external:y(e.workdir,t.path),detectedIds:E(e,t).map(e=>e.id).sort()}}function M(e){if("object"!=typeof e||null===e)return!1;const t=e;return"boolean"==typeof t.external&&Array.isArray(t.detectedIds)&&t.detectedIds.every(e=>"string"==typeof e)}function O(e,t){return e.length===t.length&&e.every((e,r)=>e===t[r])}export function revalidateVerifyAdmission(e,t,r){if(!M(r))return;const i=captureVerifyAdmission(e,t),o=!r.external&&i.external,n=!O(r.detectedIds,i.detectedIds);return o||n?d(s.toolFailed,VERIFY_ADMISSION_DRIFT_MESSAGE):void 0}export function buildVerifyFileTool(e,t,r){const i=t.register(w,"verify_file"),n={prepare:async t=>{const o=I.safeParse(t);if(!o.success)return d(s.invalidInput,'参数校验失败: path 必须为非空字符串,level 若提供须为 "fast" 或 "project"');const n=$(e.workdir,o.data.path),l=v(e.workdir,o.data.path);if(y(e.workdir,o.data.path)){const e=await h(r,"verify_file",o.data,i);if(void 0!==e)return e}const c=o.data.level??j.fast,f=E(e,o.data);if(c===j.project){const t=f.length>0?f.map(t=>F(t,e.workdir,n)).join("; "):"无可用验证器";return a(r,w,"verify_file",o.data,_,{explanation:`项目级验证 ${l}(将执行 ${t})`})}const u=f.filter(e=>e.executesProjectCode);if(u.length>0){const t=u.map(t=>F(t,e.workdir,n)).join("; "),i=u.map(e=>e.id).join("、"),s=y(e.workdir,o.data.path);return a(r,w,"verify_file",o.data,_,{explanation:`验证 ${l}:将运行项目本地 ${i}(会加载并执行项目本地配置/插件代码)— ${t}`,...s?{}:{memoryKey:"verify_file:workdir",sessionGrantLabel:"本会话内不再询问:在工作目录内运行项目本地验证器(eslint 等)"}})}return a(r,w,"verify_file",o.data,g)},resources:t=>{const r=I.safeParse(t);if(!r.success)return[{key:`file-tools:${e.workdir}`,mode:u.write}];const i=$(e.workdir,r.data.path);return[{key:f,mode:u.read},{key:`file:${m(i)}`,mode:u.read},{key:`verify:${e.workdir}`,mode:u.write}]},captureExecutionState:t=>{const r=I.safeParse(t);return r.success?captureVerifyAdmission(e,r.data):void 0},revalidateExecution:(t,r)=>{const i=I.safeParse(t);if(i.success)return revalidateVerifyAdmission(e,i.data,r)}};return r.coordinator?.register(i,n),{[i]:o({description:'验证文件正确性,按扩展名自动选择验证器(如 eslint/tsc/ruff/JSON/YAML/gofmt 等),零配置探测本地是否可用。level 默认 "fast":纯解析类验证器免确认,会执行项目本地配置/插件代码的验证器(如 eslint)需确认一次。level: "project" 额外运行项目级检查(如 tsc --noEmit、cargo check、go vet,可能执行任意代码,较慢且需用户确认)。修改代码文件后应调用本工具验证,发现问题应先修复再汇报完成。',inputSchema:I,toModelOutput:({output:e})=>c(e),execute:(t,o)=>p(r.coordinator,n,i,o.toolCallId,t,o.abortSignal,()=>executeVerifyFile(e,t,o.abortSignal))})}}
@@ -0,0 +1,22 @@
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 WRITE_FILE_TOOL_NAME = "write_file";
9
+ declare const writeFileInputSchema: z.ZodObject<{
10
+ file_path: z.ZodString;
11
+ content: z.ZodString;
12
+ }, "strip", z.ZodTypeAny, {
13
+ content: string;
14
+ file_path: string;
15
+ }, {
16
+ content: string;
17
+ file_path: string;
18
+ }>;
19
+ export type WriteFileInput = z.infer<typeof writeFileInputSchema>;
20
+ export declare function executeWriteFile(settings: ResolvedFileToolsSettings, tracker: FileStateTracker, input: WriteFileInput): NormalizedToolResult;
21
+ export declare function buildWriteFileTool(settings: ResolvedFileToolsSettings, tracker: FileStateTracker, registry: ToolRegistry, ctx: ToolBridgeContext): ToolSet;
22
+ export {};
@@ -0,0 +1 @@
1
+ import{statSync as t}from"node:fs";import{tool as e}from"ai";import{z as r}from"zod";import{gateToolCall as i}from"../build-tools.js";import{TOOL_OUTCOME_KINDS as o,failedToolResult as n,toolResultToModelOutput as a}from"../normalize-result.js";import{OPAQUE_SIDE_EFFECT_RESOURCE as s,TOOL_RESOURCE_ACCESS_MODES as c,executeCoordinatedTool as d}from"../tool-execution-coordinator.js";import{renderNumberedLines as l}from"./match-pipeline.js";import{canonicalFileKey as f,canonicalResourcePath as u,captureFilePathAdmission as p,escapesWorkdir as m,formatPathForApproval as h,loadTextFile as _,resolveFilePath as g,revalidateFilePathAdmission as k,saveTextFile as v,splitUtf8Bom as w}from"./file-io.js";import{rejectInvalidTextPayload as y}from"./control-chars.js";import{FILE_FRESHNESS as $}from"./file-state-tracker.js";import{FILE_TOOLS_AGENT_NAME as x}from"./settings.js";import{describeFileChange as F,fileChangeToolResult as j}from"./file-change-result.js";export const WRITE_FILE_TOOL_NAME="write_file";const b=10,S=20,P=.5,B=r.object({file_path:r.string().min(1).describe("要写入的文件路径,相对当前工作目录或绝对路径"),content:r.string().describe("完整的文件内容(整文件写入,覆盖原有内容)")}),I={};function E(e){try{return!t(e).isDirectory()||"directory"}catch{return!1}}function L(t,e){if(!e.ok)return{shrinking:!1,warning:void 0};const r=e.content.split("\n").length;if(r<S||t>=r*P)return{shrinking:!1,warning:void 0};const i=Math.round(100*(1-t/r));return{shrinking:!0,warning:`⚠ 新内容 ${String(t)} 行,比原文件 ${String(r)} 行减少 ${String(i)}%,请确认是有意删减`}}function T(t,e,r){const i=t.checkFreshness(r.key,r.content);return i===$.unread?n(o.toolFailed,`${e} 已存在。覆盖前请先用 roll__read_file 读取并确认现有内容;若只改部分内容,优先用 roll__edit_file。`):i===$.stale?n(o.toolFailed,`${e} 在你上次读取后已被修改(可能是用户或其他程序改动)。请重新 roll__read_file 确认最新内容后再决定是否覆盖。`):void 0}export function executeWriteFile(t,e,r){const i=y("content",r.content);if(void 0!==i)return i;const a=g(t.workdir,r.file_path),s=E(a);if("directory"===s)return n(o.invalidInput,`${a} 是目录,不能作为文件写入`);let c;if(!0===s){const r=_(a,{maxFileBytes:t.maxFileBytes});if(!r.ok)return n(o.invalidInput,r.message);const i=T(e,a,r);if(void 0!==i)return i;c=r.content}const{content:d,hadBom:u}=w(r.content);v(a,d,u),e.recordKnownContent(f(a),d);const p=d.split("\n"),m=l(p.slice(0,b),1),h=[`已写入 ${a}(${String(p.length)} 行,${String(Buffer.byteLength(r.content,"utf8"))} 字节):`,m];p.length>b&&h.push(`(预览前 ${String(b)} 行)`);const k=F({workdir:t.workdir,inputPath:r.file_path,change:void 0===c?"create":"modify",before:c??"",after:d});return j(h.join("\n"),k)}export function buildWriteFileTool(t,r,l,f){const v=l.register(x,"write_file",{annotations:I}),$={prepare:async e=>{const a=B.safeParse(e);if(!a.success)return n(o.invalidInput,"参数校验失败: file_path 必须为非空字符串,content 必须为字符串");const s=y("content",a.data.content);if(void 0!==s)return s;const c=a.data.content.split("\n").length,d=g(t.workdir,a.data.file_path),l=m(t.workdir,a.data.file_path),u=l?void 0:_(d,{maxFileBytes:t.maxFileBytes});if(!0===u?.ok){const t=T(r,d,u);if(void 0!==t)return t}const{shrinking:p,warning:k}=void 0!==u?L(c,u):{shrinking:!1,warning:void 0},{content:v}=w(a.data.content),$=void 0===u?void 0:u.ok?"modify":"not-found"===u.code?"create":void 0,j=void 0===$?void 0:F({workdir:t.workdir,inputPath:a.data.file_path,change:$,before:!0===u?.ok?u.content:"",after:v}),b=`写入 ${h(t.workdir,a.data.file_path)}(${String(c)} 行)${void 0!==k?`\n${k}`:""}`,S=p||l?void 0:"write_file:workdir";return i(f,x,"write_file",a.data,I,{explanation:b,...void 0!==j?{diff:j}:{},...void 0!==S?{memoryKey:S,sessionGrantLabel:"本会话内不再询问:写入工作目录内的文件"}:{}})},resources:e=>{const r=B.safeParse(e),i=r.success?`file:${u(g(t.workdir,r.data.file_path))}`:`file-tools:${t.workdir}`;return[{key:s,mode:c.read},{key:i,mode:c.write}]},captureExecutionState:e=>{const r=B.safeParse(e);return r.success?p(t.workdir,r.data.file_path):void 0},revalidateExecution:(e,r)=>{const i=B.safeParse(e);if(i.success)return k(t.workdir,i.data.file_path,r)}};return f.coordinator?.register(v,$),{[v]:e({description:"新建文件或整文件重写。已存在的文件必须先 roll__read_file 读取确认后才能覆盖;只改部分内容时优先用 roll__edit_file。仅支持文本内容,content 含原始 NUL 字符(U+0000)或不成对的 UTF-16 代理项会被拒绝;需要写入转义序列文本时用双反斜杠,需要原始字节时改用 shell。",inputSchema:B,toModelOutput:({output:t})=>a(t),execute:(e,i)=>d(f.coordinator,$,v,i.toolCallId,e,i.abortSignal,()=>Promise.resolve(executeWriteFile(t,r,e)))})}}
@@ -1 +1 @@
1
- import{existsSync as e}from"node:fs";import{resolve as o}from"node:path";import{performance as t}from"node:perf_hooks";import{tool as n}from"ai";import{z as r}from"zod";import{CLASSIFICATION_ANNOTATIONS as i,unknownCommandClassifier as s}from"../types/command-classification.js";import{isWithinWorkdirRoot as a}from"../bash/workdir.js";import{withAutoApprovedShellEnv as l}from"../bash/clean-env.js";import{SessionCapError as d}from"../bash/session/session-manager.js";import{pollUntilDeadline as c,throwIfSessionExecAborted as u}from"../bash/session/yield-loop.js";import{SESSION_STATES as m}from"../bash/session/types.js";import{gateToolCall as p}from"./build-tools.js";import{shellCommandExplanationSchema as _}from"./shell-explanation.js";import{ToolRegistry as x}from"./naming.js";import{TOOL_OUTCOME_KINDS as f,failedToolResult as g,successfulToolResult as w,toolResultToModelOutput as k}from"./normalize-result.js";import{TOOL_RESOURCE_ACCESS_MODES as h,executeCoordinatedTool as b}from"./tool-execution-coordinator.js";import{isUserCancellationSignal as C}from"../types/cancellation.js";export const EXEC_AGENT_NAME="roll";export const EXEC_COMMAND_NAME="exec_command";export const EXEC_POLL_NAME="exec_poll";export const EXEC_LIST_NAME="exec_list";export const EXEC_COMMAND_ID="roll__exec_command";export const EXEC_POLL_ID="roll__exec_poll";export const EXEC_LIST_ID="roll__exec_list";const y=String.fromCharCode(3),S=250,v=3e4,E=5e3,M=3e5,I=8192,$=4096,j=4;function N(e){return"known-safe"===e||"dangerous"===e?e:"unknown"}const P=r.object({command:r.string().min(1).describe("要在后台会话中执行的 shell 命令(单字符串)"),explanation:_.optional(),workdir:r.string().min(1).optional().describe("工作目录绝对路径,默认 roll chat 当前目录,不要用 cd"),yield_time_ms:r.number().int().min(S).max(v).optional().describe("本次等待输出的毫秒数(默认 10000,范围 250-30000);未结束会返回 session_id 供 exec_poll 续查"),max_output_tokens:r.number().int().min(256).max(5e4).optional().describe("本次返回输出的 token 预算")}),T=r.object({session_id:r.number().int().describe("exec_command 返回的会话 id"),chars:r.string().default("").describe('留空表示纯轮询进度;"\\u0003"(Ctrl-C) 表示中断该会话;不支持其它交互输入'),yield_time_ms:r.number().int().min(E).max(M).optional().describe("空轮询等待的毫秒数(默认 10000,范围 5000-300000)")}),O=r.object({});function A(e,o,t){return Math.min(Math.max(e,o),t)}function D(e,o,t){const n=e.emitEvent;if(!n)return;let r=0;return(e,i)=>{if(r>=I)return;r+=1;const s=i.length>$?i.slice(0,$):i;n({type:"tool-output-delta",toolCallId:o,agentName:"roll",toolName:t,stream:e,delta:s})}}async function L(e,o,t,n,r){if(e.policy)return p(e,"roll","exec_command",o,n,{includePolicyReason:"dangerous"===t,...void 0!==r?{explanation:r}:{}});const i=await e.requestApproval({agentName:"roll",toolName:"exec_command",input:o,reason:"dangerous"===t?"破坏性操作":void 0,...void 0!==r?{explanation:r}:{}});return i.approved?void 0:g(f.userRejected,"已取消执行"+(i.reason?`: ${i.reason}`:""),i.reason?{reason:i.reason}:{})}function X(e){const o=["running"===e.kind?`Session: ${String(e.sessionId)} (running)`:`Exit code: ${String(e.exitCode)}`,`Wall time: ${(e.wallTimeMs/1e3).toFixed(1)} s`];e.omitted>0&&o.push(`(省略中间 ${String(e.omitted)} 字符)`),"exited"===e.kind&&e.terminationCause&&o.push(`Termination: ${e.terminationCause}`),"exited"===e.kind&&e.cleanupError&&o.push(`Cleanup error: ${e.cleanupError}`);const t=e.output.length>0?`\n\n${e.output}`:"",n=o.join("\n")+t;return"exited"===e.kind&&(0!==e.exitCode||e.state===m.cleanupFailed||void 0!==e.terminationCause)?g(f.toolFailed,n,{raw:e}):w(n,{raw:e})}function F(e){return 0===e.length?w("当前没有可恢复的后台会话",{raw:e}):w(JSON.stringify({sessions:e.map(e=>({session_id:e.sessionId,state:e.state,command:e.commandPreview,workdir:e.workdir,wall_time_s:Number((e.wallTimeMs/1e3).toFixed(1)),...void 0!==e.exitCode?{exit_code:e.exitCode}:{},...e.terminationCause?{termination_cause:e.terminationCause}:{},...e.cleanupError?{cleanup_error:e.cleanupError}:{}}))},null,2),{raw:e})}function z(e,o,t){if(void 0===e)return()=>{};const n=()=>{C(e)&&t.get(o.id)===o&&t.interrupt(o.id).catch(()=>{})};return e.addEventListener("abort",n,{once:!0}),e.aborted&&n(),()=>e.removeEventListener("abort",n)}export function buildSessionExecToolset(r,m,p,_,x={}){const w=p.register("roll","exec_command"),C=p.register("roll","exec_poll"),I=p.register("roll","exec_list"),$=x.classifier??s,j=e=>({workdir:o(r.workdir,e.workdir??"."),yieldMs:A(e.yield_time_ms??r.defaultYieldMs,S,v)}),R=(e,o)=>{const{workdir:t,yieldMs:n}=j(e),s=a(r.workdir,t)?o??$.classify(e.command,t):"unknown";return{workdir:t,yieldMs:n,classification:s,annotations:i[s]}},Y={prepare:async(o,t)=>{const n=P.safeParse(o);if(!n.success)return g(f.invalidInput,`参数校验失败: ${n.error.issues.map(e=>e.message).join("; ")}`,{raw:n.error.issues});const r=R(n.data,N(t));return e(r.workdir)?L(_,{command:n.data.command,workdir:r.workdir,yield_time_ms:r.yieldMs},r.classification,r.annotations,n.data.explanation):g(f.invalidInput,`工作目录不存在: ${r.workdir}`)},resources:(e,o)=>{const t=P.safeParse(e),n=t.success?R(t.data,N(o)).workdir:r.workdir;return[{key:"exec-manager",mode:h.write},{key:`shell:${n}`,mode:h.write}]},captureExecutionState:e=>{const o=P.safeParse(e);return o.success?R(o.data).classification:"unknown"},revalidateExecution:(e,o)=>{if("known-safe"!==o)return;const t=P.safeParse(e);return t.success&&"known-safe"===R(t.data).classification?void 0:g(f.toolFailed,"后台 shell 命令的安全条件在准入后发生变化,已在执行前阻止;请重新提交以重新确认")}},q={prepare:e=>{const o=T.safeParse(e);return o.success?m.get(o.data.session_id)?""!==o.data.chars&&o.data.chars!==y?g(f.invalidInput,"pipe 会话不支持交互输入(仅支持空 chars 轮询或 Ctrl-C \\u0003 中断)"):void 0:g(f.invalidInput,`会话 ${String(o.data.session_id)} 不存在或已结束`):g(f.invalidInput,`参数校验失败: ${o.error.issues.map(e=>e.message).join("; ")}`,{raw:o.error.issues})},resources:e=>{const o=T.safeParse(e);return[{key:o.success?`exec-session:${String(o.data.session_id)}`:"exec-session",mode:h.write}]}},G={resources:()=>[{key:"exec-manager",mode:h.read}]};return _.coordinator?.register(w,Y),_.coordinator?.register(C,q),_.coordinator?.register(I,G),{[w]:n({description:"在后台会话中执行一条命令,等待一段时间后返回输出。若命令未结束会返回 session_id,用 exec_poll 续查进度、读取退出码。适合运行时间超过单轮预算的长脚本。需确认的命令继承 roll 进程环境;自动批准的 known-safe 命令使用隔离的系统 PATH 与 shell 环境。",inputSchema:P,toModelOutput:({output:e})=>k(e),execute:async(o,n)=>{const{workdir:i,yieldMs:s}=j(o);return b(_.coordinator,Y,w,n.toolCallId,o,n.abortSignal,async a=>{if(u(n.abortSignal),!e(i))return g(f.invalidInput,`工作目录不存在: ${i}`);const p=4*(o.max_output_tokens??r.maxOutputTokens),w=D(_,n.toolCallId,"exec_command");let k;try{k=m.spawn({command:o.command,workdir:i,..."known-safe"===a?{env:l(r.env??process.env)}:{},...w?{onDelta:w}:{}}),x.onSessionTouched?.(k.id)}catch(e){const o=e instanceof d?`${e.message};先调用 ${I} 查看会话,再对 cleanup-failed 终态调用 ${C} 领取结果并释放名额,或等待/中断仍在运行的会话后重试`:`无法启动会话: ${String(e)}`;return g(f.toolFailed,o,{raw:e})}const h=z(n.abortSignal,k,m),b=await c(k,t.now()+s,p,{...n.abortSignal?{abortSignal:n.abortSignal}:{},...w?{onDelta:w}:{}}).finally(h);return"exited"===b.kind&&m.delete(k.id),X(b)})}}),[C]:n({description:'轮询或中断一个 exec_command 会话。session_id 为 exec_command 返回的 id;chars 留空表示纯轮询进度,"\\u0003" 表示发送 Ctrl-C 中断。返回最新输出,进程结束时返回退出码。',inputSchema:T,toModelOutput:({output:e})=>k(e),execute:async(e,o)=>b(_.coordinator,q,C,o.toolCallId,e,o.abortSignal,async()=>{u(o.abortSignal);const n=m.get(e.session_id);if(!n)return g(f.invalidInput,`会话 ${String(e.session_id)} 不存在或已结束`);if(""!==e.chars&&e.chars!==y)return g(f.invalidInput,"pipe 会话不支持交互输入(仅支持空 chars 轮询或 Ctrl-C \\u0003 中断)");x.onSessionTouched?.(n.id),e.chars===y&&await m.interrupt(n.id);const i=D(_,o.toolCallId,"exec_poll"),s=A(e.yield_time_ms??Math.max(r.defaultYieldMs,E),E,M),a=4*r.maxOutputTokens,l=z(o.abortSignal,n,m),d=await c(n,t.now()+s,a,{...o.abortSignal?{abortSignal:o.abortSignal}:{},...i?{onDelta:i}:{}}).finally(l);return"exited"===d.kind&&m.delete(e.session_id),X(d)})}),[I]:n({description:"列出当前 roll chat 进程中的有界近期后台命令会话,包括仍在运行的会话和尚未领取最终结果的已结束会话。用于在轮超时或丢失 session_id 后恢复轮询,不是永久历史。",inputSchema:O,toModelOutput:({output:e})=>k(e),execute:async(e,o)=>b(_.coordinator,G,I,o.toolCallId,e,o.abortSignal,()=>Promise.resolve(F(m.list())))})}}
1
+ import{existsSync as e}from"node:fs";import{resolve as o}from"node:path";import{performance as t}from"node:perf_hooks";import{tool as i}from"ai";import{z as n}from"zod";import{CLASSIFICATION_ANNOTATIONS as r,unknownCommandClassifier as s}from"../types/command-classification.js";import{isWithinWorkdirRoot as a}from"../bash/workdir.js";import{withAutoApprovedShellEnv as d}from"../bash/clean-env.js";import{describeOutputDumpRecovery as l}from"../bash/output-dump.js";import{evaluatePipelineExit as c}from"../bash/shell-pipe.js";import{boundedIntParam as m,describeZodIssues as u}from"./bounded-param.js";import{SessionCapError as p}from"../bash/session/session-manager.js";import{pollUntilDeadline as _,throwIfSessionExecAborted as f}from"../bash/session/yield-loop.js";import{SESSION_STATES as x}from"../bash/session/types.js";import{gateToolCall as g}from"./build-tools.js";import{shellCommandExplanationSchema as h}from"./shell-explanation.js";import{ToolRegistry as w}from"./naming.js";import{TOOL_OUTCOME_KINDS as k,failedToolResult as C,successfulToolResult as y,toolResultToModelOutput as b}from"./normalize-result.js";import{OPAQUE_SIDE_EFFECT_RESOURCE as v,TOOL_RESOURCE_ACCESS_MODES as S,executeCoordinatedTool as E}from"./tool-execution-coordinator.js";import{isUserCancellationSignal as M}from"../types/cancellation.js";export const EXEC_AGENT_NAME="roll";export const EXEC_COMMAND_NAME="exec_command";export const EXEC_POLL_NAME="exec_poll";export const EXEC_LIST_NAME="exec_list";export const EXEC_COMMAND_ID="roll__exec_command";export const EXEC_POLL_ID="roll__exec_poll";export const EXEC_LIST_ID="roll__exec_list";const I=String.fromCharCode(3),j=250,$=3e4,N=5e3,P=3e5,T=8192,O=4096,A=4;function D(e){return"known-safe"===e||"dangerous"===e?e:"unknown"}const L=m({name:"yield_time_ms",min:j,max:$,description:"本次等待输出的毫秒数;未结束会返回 session_id 供 exec_poll 续查",defaultNote:"默认 10000"}),X=m({name:"max_output_tokens",min:256,max:5e4,description:"本次返回输出的 token 预算"}),F=m({name:"yield_time_ms",min:N,max:P,description:"空轮询等待的毫秒数",defaultNote:"默认 10000"}),z=n.object({command:n.string().min(1).describe("要在后台会话中执行的 shell 命令(单字符串)"),explanation:h.optional(),workdir:n.string().min(1).optional().describe("工作目录绝对路径,默认 roll chat 当前目录,不要用 cd"),yield_time_ms:L.schema,max_output_tokens:X.schema}),R=n.object({session_id:n.number().int().describe("exec_command 返回的会话 id"),chars:n.string().default("").describe('留空表示纯轮询进度;"\\u0003"(Ctrl-C) 表示中断该会话;不支持其它交互输入'),yield_time_ms:F.schema}),Y=n.object({});function q(e,o,t){return Math.min(Math.max(e,o),t)}function G(e,o,t){const i=e.emitEvent;if(!i)return;let n=0;return(e,r)=>{if(n>=T)return;n+=1;const s=r.length>O?r.slice(0,O):r;i({type:"tool-output-delta",toolCallId:o,agentName:"roll",toolName:t,stream:e,delta:s})}}async function H(e,o,t,i,n){if(e.policy)return g(e,"roll","exec_command",o,i,{includePolicyReason:"dangerous"===t,...void 0!==n?{explanation:n}:{}});const r=await e.requestApproval({agentName:"roll",toolName:"exec_command",input:o,reason:"dangerous"===t?"破坏性操作":void 0,...void 0!==n?{explanation:n}:{}});return r.approved?void 0:C(k.userRejected,"已取消执行"+(r.reason?`: ${r.reason}`:""),r.reason?{reason:r.reason}:{})}function J(e){const o="exited"===e.kind?c({exitCode:e.exitCode,...void 0!==e.pipeSegments?{segments:e.pipeSegments}:{},capability:e.pipeCapability??"none"}):void 0,t=["running"===e.kind?`Session: ${String(e.sessionId)} (running)`:`Exit code: ${String(o?.effectiveExitCode??e.exitCode)}`];void 0!==o?.note&&t.push(o.note),t.push(`Wall time: ${(e.wallTimeMs/1e3).toFixed(1)} s`),e.omitted>0&&(t.push(`(省略中间 ${String(e.omitted)} 字符)`),"exited"===e.kind&&void 0!==e.dumpPath&&t.push(l(e.dumpPath))),"exited"===e.kind&&e.terminationCause&&t.push(`Termination: ${e.terminationCause}`),"exited"===e.kind&&e.cleanupError&&t.push(`Cleanup error: ${e.cleanupError}`);const i=e.output.length>0?`\n\n${e.output}`:"",n=t.join("\n")+i;return"exited"===e.kind&&(!(o?.ok??0===e.exitCode)||e.state===x.cleanupFailed||void 0!==e.terminationCause)?C(k.toolFailed,n,{raw:e}):y(n,{raw:e})}function W(e){return 0===e.length?y("当前没有可恢复的后台会话",{raw:e}):y(JSON.stringify({sessions:e.map(e=>({session_id:e.sessionId,state:e.state,command:e.commandPreview,workdir:e.workdir,wall_time_s:Number((e.wallTimeMs/1e3).toFixed(1)),...void 0!==e.exitCode?{exit_code:e.exitCode}:{},...e.terminationCause?{termination_cause:e.terminationCause}:{},...e.cleanupError?{cleanup_error:e.cleanupError}:{}}))},null,2),{raw:e})}function B(e,o,t){if(void 0===e)return()=>{};const i=()=>{M(e)&&t.get(o.id)===o&&t.interrupt(o.id).catch(()=>{})};return e.addEventListener("abort",i,{once:!0}),e.aborted&&i(),()=>e.removeEventListener("abort",i)}export function buildSessionExecToolset(n,l,c,m,x={}){const g=c.register("roll","exec_command"),h=c.register("roll","exec_poll"),w=c.register("roll","exec_list"),y=x.classifier??s,M=e=>({workdir:o(n.workdir,e.workdir??"."),yieldMs:q(e.yield_time_ms??n.defaultYieldMs,j,$)}),T=(e,o)=>{const{workdir:t,yieldMs:i}=M(e),s=a(n.workdir,t)?o??y.classify(e.command,t):"unknown";return{workdir:t,yieldMs:i,classification:s,annotations:r[s]}},O={prepare:async(o,t)=>{const i=z.safeParse(o);if(!i.success)return C(k.invalidInput,u(i.error,o)??"参数校验失败",{raw:i.error.issues});const n=T(i.data,D(t));return e(n.workdir)?H(m,{command:i.data.command,workdir:n.workdir,yield_time_ms:n.yieldMs},n.classification,n.annotations,i.data.explanation):C(k.invalidInput,`工作目录不存在: ${n.workdir}`)},resources:(e,o)=>{const t=z.safeParse(e),i=t.success?T(t.data,D(o)):void 0,r=[{key:"exec-manager",mode:S.write},{key:`shell:${i?.workdir??n.workdir}`,mode:S.write}];return"known-safe"===i?.classification?r:[{key:v,mode:S.write},...r]},captureExecutionState:e=>{const o=z.safeParse(e);return o.success?T(o.data).classification:"unknown"},revalidateExecution:(e,o)=>{if("known-safe"!==o)return;const t=z.safeParse(e);return t.success&&"known-safe"===T(t.data).classification?void 0:C(k.toolFailed,"后台 shell 命令的安全条件在准入后发生变化,已在执行前阻止;请重新提交以重新确认")}},A={prepare:e=>{const o=R.safeParse(e);return o.success?l.get(o.data.session_id)?""!==o.data.chars&&o.data.chars!==I?C(k.invalidInput,"pipe 会话不支持交互输入(仅支持空 chars 轮询或 Ctrl-C \\u0003 中断)"):void 0:C(k.invalidInput,`会话 ${String(o.data.session_id)} 不存在或已结束`):C(k.invalidInput,u(o.error,e)??"参数校验失败",{raw:o.error.issues})},resources:e=>{const o=R.safeParse(e);return[{key:o.success?`exec-session:${String(o.data.session_id)}`:"exec-session",mode:S.write}]}},L={resources:()=>[{key:"exec-manager",mode:S.read}]};return m.coordinator?.register(g,O),m.coordinator?.register(h,A),m.coordinator?.register(w,L),{[g]:i({description:"在后台会话中执行一条命令,等待一段时间后返回输出。若命令未结束会返回 session_id,用 exec_poll 续查进度、读取退出码。适合运行时间超过单轮预算的长脚本。需确认的命令继承 roll 进程环境;自动批准的 known-safe 命令使用隔离的系统 PATH 与 shell 环境。",inputSchema:z,toModelOutput:({output:e})=>b(e),execute:async(o,i)=>{const{workdir:r,yieldMs:s}=M(o);return E(m.coordinator,O,g,i.toolCallId,o,i.abortSignal,async a=>{if(f(i.abortSignal),!e(r))return C(k.invalidInput,`工作目录不存在: ${r}`);const c=4*(o.max_output_tokens??n.maxOutputTokens),u=G(m,i.toolCallId,"exec_command");let g;try{g=l.spawn({command:o.command,workdir:r,..."known-safe"===a?{env:d(n.env??process.env)}:{},...u?{onDelta:u}:{}}),x.onSessionTouched?.(g.id)}catch(e){const o=e instanceof p?`${e.message};先调用 ${w} 查看会话,再对 cleanup-failed 终态调用 ${h} 领取结果并释放名额,或等待/中断仍在运行的会话后重试`:`无法启动会话: ${String(e)}`;return C(k.toolFailed,o,{raw:e})}const y=B(i.abortSignal,g,l),b=await _(g,t.now()+s,c,{...i.abortSignal?{abortSignal:i.abortSignal}:{},...u?{onDelta:u}:{}}).finally(y);return"exited"===b.kind&&l.delete(g.id),J(b)})}}),[h]:i({description:'轮询或中断一个 exec_command 会话。session_id 为 exec_command 返回的 id;chars 留空表示纯轮询进度,"\\u0003" 表示发送 Ctrl-C 中断。返回最新输出,进程结束时返回退出码。',inputSchema:R,toModelOutput:({output:e})=>b(e),execute:async(e,o)=>E(m.coordinator,A,h,o.toolCallId,e,o.abortSignal,async()=>{f(o.abortSignal);const i=l.get(e.session_id);if(!i)return C(k.invalidInput,`会话 ${String(e.session_id)} 不存在或已结束`);if(""!==e.chars&&e.chars!==I)return C(k.invalidInput,"pipe 会话不支持交互输入(仅支持空 chars 轮询或 Ctrl-C \\u0003 中断)");x.onSessionTouched?.(i.id),e.chars===I&&await l.interrupt(i.id);const r=G(m,o.toolCallId,"exec_poll"),s=q(e.yield_time_ms??Math.max(n.defaultYieldMs,N),N,P),a=4*n.maxOutputTokens,d=B(o.abortSignal,i,l),c=await _(i,t.now()+s,a,{...o.abortSignal?{abortSignal:o.abortSignal}:{},...r?{onDelta:r}:{}}).finally(d);return"exited"===c.kind&&l.delete(e.session_id),J(c)})}),[w]:i({description:"列出当前 roll chat 进程中的有界近期后台命令会话,包括仍在运行的会话和尚未领取最终结果的已结束会话。用于在轮超时或丢失 session_id 后恢复轮询,不是永久历史。",inputSchema:Y,toModelOutput:({output:e})=>b(e),execute:async(e,o)=>E(m.coordinator,L,w,o.toolCallId,e,o.abortSignal,()=>Promise.resolve(W(l.list())))})}}
@@ -3,6 +3,7 @@ export declare const TOOL_RESOURCE_ACCESS_MODES: {
3
3
  readonly read: "read";
4
4
  readonly write: "write";
5
5
  };
6
+ export declare const OPAQUE_SIDE_EFFECT_RESOURCE = "shell:opaque-side-effects";
6
7
  export type ToolResourceAccessMode = (typeof TOOL_RESOURCE_ACCESS_MODES)[keyof typeof TOOL_RESOURCE_ACCESS_MODES];
7
8
  export interface ToolResourceAccess {
8
9
  readonly key: string;
@@ -35,6 +36,14 @@ export interface ToolBatchCall {
35
36
  readonly toolCallId: string;
36
37
  readonly toolId: string;
37
38
  }
39
+ declare const TOOL_CALL_OCCURRENCE_STATE: unique symbol;
40
+ interface ToolCallOccurrenceState {
41
+ started: boolean;
42
+ }
43
+ export interface ToolCallOccurrence {
44
+ readonly toolCallId: string;
45
+ readonly [TOOL_CALL_OCCURRENCE_STATE]: ToolCallOccurrenceState;
46
+ }
38
47
  /**
39
48
  * Coordinates one AI SDK model-call batch in two phases:
40
49
  * 1. all preflight / policy / user approvals run serially while tool calls stream in;
@@ -43,7 +52,7 @@ export interface ToolBatchCall {
43
52
  export declare class ToolExecutionCoordinator {
44
53
  private readonly plans;
45
54
  private readonly prepared;
46
- private readonly startedToolCallIds;
55
+ private readonly unbatchedOccurrences;
47
56
  private readonly batches;
48
57
  private readonly toolCallBatches;
49
58
  private readonly locks;
@@ -52,7 +61,8 @@ export declare class ToolExecutionCoordinator {
52
61
  register(toolId: string, plan: ToolExecutionPlan): void;
53
62
  /** Returns the same normalized resource plan used by execution, without acquiring locks. */
54
63
  describeResources(toolId: string, input: unknown): readonly ToolResourceAccess[];
55
- hasExecutionStarted(toolCallId: string): boolean;
64
+ captureToolCallOccurrence(toolCallId: string): ToolCallOccurrence;
65
+ hasExecutionStarted(occurrence: ToolCallOccurrence): boolean;
56
66
  startBatch(callId: string): void;
57
67
  prepare(toolCallId: string, toolId: string, input: unknown): Promise<void>;
58
68
  sealBatch(callId: string, calls?: readonly ToolBatchCall[]): void;
@@ -62,8 +72,11 @@ export declare class ToolExecutionCoordinator {
62
72
  private prepareWithPlan;
63
73
  private completeBatchCall;
64
74
  private trackBatchCall;
75
+ private batchOccurrence;
76
+ private executionOccurrence;
65
77
  private releaseBatchAdmissionIfReady;
66
78
  private cancelBatch;
67
79
  private cleanupBatchIfComplete;
68
80
  }
69
81
  export declare function executeCoordinatedTool(coordinator: ToolExecutionCoordinator | undefined, plan: ToolExecutionPlan, toolId: string, toolCallId: string, input: unknown, abortSignal: AbortSignal | undefined, operation: (capturedState: unknown) => Promise<NormalizedToolResult>): Promise<NormalizedToolResult>;
82
+ export {};
@@ -1 +1 @@
1
- import{TOOL_OUTCOME_KINDS as e,failedToolResult as t,readToolOutcome as a}from"./normalize-result.js";export const TOOL_RESOURCE_ACCESS_MODES={read:"read",write:"write"};export const TOOL_RESOURCE_HINT_KINDS={file:"file",browserSession:"browser-session",conversation:"conversation",custom:"custom"};class ToolExecutionCancelledError extends Error{constructor(){super("tool execution cancelled"),this.name="ToolExecutionCancelledError"}}function r(e){return e instanceof Error?e.message:String(e)}function o(e){return e.reason??new ToolExecutionCancelledError}function s(){const e=Promise.withResolvers();return{promise:e.promise,release:e.resolve,released:!1}}function i(e,t){return void 0===t?e:t.aborted?Promise.reject(o(t)):new Promise((a,r)=>{const s=()=>{t.removeEventListener("abort",s),r(o(t))};t.addEventListener("abort",s,{once:!0}),e.then(()=>{t.removeEventListener("abort",s),a()},e=>{t.removeEventListener("abort",s),r(e)})})}function l(e){const t=new Map;for(const a of e){const e=a.key.trim();if(0===e.length)continue;void 0!==t.get(e)&&a.mode!==TOOL_RESOURCE_ACCESS_MODES.write||t.set(e,a.mode)}return[...t].sort(([e],[t])=>e<t?-1:e>t?1:0).map(([e,t])=>({key:e,mode:t}))}class ResourceLockManager{locks=new Map;async run(e,t,a){const r=[];try{for(const a of l(e))r.push(await this.acquire(a,t));if(t?.aborted)throw new ToolExecutionCancelledError;return await a()}finally{for(const e of r.reverse())e()}}acquire(e,t){if(t?.aborted)return Promise.reject(new ToolExecutionCancelledError);const a=this.locks.get(e.key)??{readers:0,writer:!1,queue:[]};this.locks.set(e.key,a);const r=a.queue.some(e=>e.mode===TOOL_RESOURCE_ACCESS_MODES.write);return(e.mode===TOOL_RESOURCE_ACCESS_MODES.read?!a.writer&&!r:!a.writer&&0===a.readers&&0===a.queue.length)?(this.grant(a,e.mode),Promise.resolve(()=>this.release(e.key,a,e.mode))):new Promise((r,o)=>{const s={mode:e.mode,resolve:()=>{t?.removeEventListener("abort",i),this.grant(a,e.mode),r(()=>this.release(e.key,a,e.mode))},reject:o,signal:t,onAbort:void 0===t?void 0:()=>i()},i=()=>{const t=a.queue.indexOf(s);t>=0&&a.queue.splice(t,1),o(new ToolExecutionCancelledError),this.drain(e.key,a)};a.queue.push(s),t?.addEventListener("abort",i,{once:!0})})}grant(e,t){t===TOOL_RESOURCE_ACCESS_MODES.read?e.readers+=1:e.writer=!0}release(e,t,a){a===TOOL_RESOURCE_ACCESS_MODES.read?t.readers-=1:t.writer=!1,this.drain(e,t)}drain(e,t){if(t.writer||t.readers>0)return;for(;t.queue[0]?.signal?.aborted;){const e=t.queue.shift();e?.reject(new ToolExecutionCancelledError)}const a=t.queue[0];if(void 0!==a){if(a.mode===TOOL_RESOURCE_ACCESS_MODES.write)return t.queue.shift(),void a.resolve();for(;t.queue[0]?.mode===TOOL_RESOURCE_ACCESS_MODES.read;)t.queue.shift()?.resolve()}else this.locks.delete(e)}}export class ToolExecutionCoordinator{plans=new Map;prepared=new Map;startedToolCallIds=new Set;batches=new Map;toolCallBatches=new Map;locks=new ResourceLockManager;activeBatch;preparationTail=Promise.resolve();register(e,t){this.plans.set(e,t)}describeResources(e,t){try{const a=this.plans.get(e),r=a?.captureExecutionState?.(t);return[...a?.resources(t,r)??[]]}catch{return[]}}hasExecutionStarted(e){return this.startedToolCallIds.has(e)}startBatch(e){this.activeBatch&&!this.activeBatch.sealed&&this.cancelBatch(this.activeBatch);const t={callId:e,toolCallIds:new Set,preparedToolCallIds:new Set,completedToolCallIds:new Set,admission:s(),userRejected:!1,cancelled:!1,sealed:!1};this.batches.set(e,t),this.activeBatch=t}async prepare(r,o,s){const i=this.toolCallBatches.get(r)??this.activeBatch;i&&this.trackBatchCall(i,r);const l=this.preparationTail;let c;this.preparationTail=new Promise(e=>{c=e}),await l;try{const l=this.plans.get(o),c=i?.cancelled?{blocked:t(e.cancelled,"工具批次已结束,本调用未执行"),resources:[],capturedState:void 0}:i?.userRejected?{blocked:t(e.cancelled,"同批次已有工具被用户拒绝,本调用未执行"),resources:[],capturedState:void 0}:await this.prepareWithPlan(l,s),{blocked:n}=c;n&&a(n).kind===e.userRejected&&i&&(i.userRejected=!0),!0!==i?.cancelled&&this.prepared.set(r,{toolId:o,input:s,batch:i,...c})}finally{i&&(i.preparedToolCallIds.add(r),this.releaseBatchAdmissionIfReady(i)),c?.()}}sealBatch(e,t=[]){const a=this.batches.get(e);if(a){for(const e of t)this.plans.has(e.toolId)&&this.trackBatchCall(a,e.toolCallId);a.sealed=!0,this.releaseBatchAdmissionIfReady(a),0===a.toolCallIds.size&&this.batches.delete(e)}this.activeBatch?.callId===e&&(this.activeBatch=void 0)}finishTurn(){for(const e of this.batches.values())this.cancelBatch(e);this.prepared.clear(),this.startedToolCallIds.clear(),this.batches.clear(),this.toolCallBatches.clear(),this.activeBatch=void 0,this.preparationTail=Promise.resolve()}async execute(a,s,l,c,n){const d=this.toolCallBatches.get(a)??this.activeBatch;let h;d&&this.trackBatchCall(d,a);try{if(d&&(await i(d.admission.promise,c),d.cancelled))return t(e.cancelled,"工具批次已结束,本调用未执行");if(h=this.prepared.get(a),void 0===h){if(d)return t(e.toolFailed,"工具批次准入状态不完整,本调用未执行");if(c?.aborted)throw o(c);const a=this.plans.get(s);h={toolId:s,input:l,batch:void 0,...await this.prepareWithPlan(a,l)}}if(h.blocked)return h.blocked;if(h.batch?.userRejected)return t(e.cancelled,"同批次已有工具被用户拒绝,本调用未执行");if(c?.aborted)throw o(c);const r=this.plans.get(s),u=h;return await this.locks.run(u.resources,c,async()=>{const e=await(r?.revalidateExecution?.(l,u.capturedState));return e||(this.startedToolCallIds.add(a),n(u.capturedState))})}catch(a){if(c?.aborted)throw o(c);return a instanceof ToolExecutionCancelledError?t(e.cancelled,"工具调用已取消",{raw:a}):t(e.toolFailed,r(a),{raw:a})}finally{const e=h?.batch??d,t=this.prepared.get(a);(void 0!==h&&t===h||void 0===h&&void 0!==e&&t?.batch===e)&&this.prepared.delete(a),this.completeBatchCall(e,a)}}async prepareWithPlan(a,o){try{const e=a?.captureExecutionState?.(o),t=await(a?.prepare?.(o,e));return t?{blocked:t,resources:[],capturedState:void 0}:{blocked:void 0,resources:a?.resources(o,e)??[],capturedState:e}}catch(a){return{blocked:t(e.toolFailed,r(a),{raw:a}),resources:[],capturedState:void 0}}}completeBatchCall(e,t){e&&(e.completedToolCallIds.add(t),this.cleanupBatchIfComplete(e))}trackBatchCall(e,t){e.toolCallIds.add(t),this.toolCallBatches.set(t,e)}releaseBatchAdmissionIfReady(e){if(!e.admission.released){if(!e.sealed)return;if(!e.cancelled&&[...e.toolCallIds].some(t=>!e.preparedToolCallIds.has(t)))return;e.admission.released=!0,e.admission.release()}this.cleanupBatchIfComplete(e)}cancelBatch(e){e.cancelled=!0,e.sealed=!0,this.releaseBatchAdmissionIfReady(e)}cleanupBatchIfComplete(e){if(e.admission.released&&!(e.completedToolCallIds.size<e.toolCallIds.size)){this.batches.get(e.callId)===e&&this.batches.delete(e.callId);for(const t of e.toolCallIds)this.toolCallBatches.get(t)===e&&this.toolCallBatches.delete(t),this.prepared.get(t)?.batch===e&&this.prepared.delete(t)}}}export async function executeCoordinatedTool(a,s,i,l,c,n,d){if(a)return a.execute(l,i,c,n,d);try{if(n?.aborted)throw o(n);const e=s.captureExecutionState?.(c),t=await(s.prepare?.(c,e));if(t)return t;if(n?.aborted)throw o(n);const a=await(s.revalidateExecution?.(c,e));return a||await d(e)}catch(a){if(n?.aborted)throw o(n);return t(e.toolFailed,r(a),{raw:a})}}
1
+ import{TOOL_OUTCOME_KINDS as e,failedToolResult as t,readToolOutcome as r}from"./normalize-result.js";export const TOOL_RESOURCE_ACCESS_MODES={read:"read",write:"write"};export const OPAQUE_SIDE_EFFECT_RESOURCE="shell:opaque-side-effects";export const TOOL_RESOURCE_HINT_KINDS={file:"file",browserSession:"browser-session",conversation:"conversation",custom:"custom"};const a=Symbol("tool-call-occurrence-state");function o(e){return{toolCallId:e,[a]:{started:!1}}}class ToolExecutionCancelledError extends Error{constructor(){super("tool execution cancelled"),this.name="ToolExecutionCancelledError"}}function c(e){return e instanceof Error?e.message:String(e)}function s(e){return e.reason??new ToolExecutionCancelledError}function i(){const e=Promise.withResolvers();return{promise:e.promise,release:e.resolve,released:!1}}function l(e,t){return void 0===t?e:t.aborted?Promise.reject(s(t)):new Promise((r,a)=>{const o=()=>{t.removeEventListener("abort",o),a(s(t))};t.addEventListener("abort",o,{once:!0}),e.then(()=>{t.removeEventListener("abort",o),r()},e=>{t.removeEventListener("abort",o),a(e)})})}function n(e){const t=new Map;for(const r of e){const e=r.key.trim();if(0===e.length)continue;void 0!==t.get(e)&&r.mode!==TOOL_RESOURCE_ACCESS_MODES.write||t.set(e,r.mode)}return[...t].sort(([e],[t])=>e<t?-1:e>t?1:0).map(([e,t])=>({key:e,mode:t}))}class ResourceLockManager{locks=new Map;async run(e,t,r){const a=[];try{for(const r of n(e))a.push(await this.acquire(r,t));if(t?.aborted)throw new ToolExecutionCancelledError;return await r()}finally{for(const e of a.reverse())e()}}acquire(e,t){if(t?.aborted)return Promise.reject(new ToolExecutionCancelledError);const r=this.locks.get(e.key)??{readers:0,writer:!1,queue:[]};this.locks.set(e.key,r);const a=r.queue.some(e=>e.mode===TOOL_RESOURCE_ACCESS_MODES.write);return(e.mode===TOOL_RESOURCE_ACCESS_MODES.read?!r.writer&&!a:!r.writer&&0===r.readers&&0===r.queue.length)?(this.grant(r,e.mode),Promise.resolve(()=>this.release(e.key,r,e.mode))):new Promise((a,o)=>{const c={mode:e.mode,resolve:()=>{t?.removeEventListener("abort",s),this.grant(r,e.mode),a(()=>this.release(e.key,r,e.mode))},reject:o,signal:t,onAbort:void 0===t?void 0:()=>s()},s=()=>{const t=r.queue.indexOf(c);t>=0&&r.queue.splice(t,1),o(new ToolExecutionCancelledError),this.drain(e.key,r)};r.queue.push(c),t?.addEventListener("abort",s,{once:!0})})}grant(e,t){t===TOOL_RESOURCE_ACCESS_MODES.read?e.readers+=1:e.writer=!0}release(e,t,r){r===TOOL_RESOURCE_ACCESS_MODES.read?t.readers-=1:t.writer=!1,this.drain(e,t)}drain(e,t){if(t.writer||t.readers>0)return;for(;t.queue[0]?.signal?.aborted;){const e=t.queue.shift();e?.reject(new ToolExecutionCancelledError)}const r=t.queue[0];if(void 0!==r){if(r.mode===TOOL_RESOURCE_ACCESS_MODES.write)return t.queue.shift(),void r.resolve();for(;t.queue[0]?.mode===TOOL_RESOURCE_ACCESS_MODES.read;)t.queue.shift()?.resolve()}else this.locks.delete(e)}}export class ToolExecutionCoordinator{plans=new Map;prepared=new Map;unbatchedOccurrences=new Map;batches=new Map;toolCallBatches=new Map;locks=new ResourceLockManager;activeBatch;preparationTail=Promise.resolve();register(e,t){this.plans.set(e,t)}describeResources(e,t){try{const r=this.plans.get(e),a=r?.captureExecutionState?.(t);return[...r?.resources(t,a)??[]]}catch{return[]}}captureToolCallOccurrence(e){const t=this.toolCallBatches.get(e)??this.activeBatch;if(t)return this.batchOccurrence(t,e);const r=o(e);return this.unbatchedOccurrences.set(e,r),r}hasExecutionStarted(e){return e[a].started}startBatch(e){this.activeBatch&&!this.activeBatch.sealed&&this.cancelBatch(this.activeBatch);const t={callId:e,toolCallIds:new Set,toolCallOccurrences:new Map,preparedToolCallIds:new Set,completedToolCallIds:new Set,admission:i(),userRejected:!1,cancelled:!1,sealed:!1};this.batches.set(e,t),this.activeBatch=t}async prepare(a,o,c){const s=this.toolCallBatches.get(a)??this.activeBatch;s&&this.trackBatchCall(s,a);const i=this.preparationTail;let l;this.preparationTail=new Promise(e=>{l=e}),await i;try{const i=this.plans.get(o),l=s?.cancelled?{blocked:t(e.cancelled,"工具批次已结束,本调用未执行"),resources:[],capturedState:void 0}:s?.userRejected?{blocked:t(e.cancelled,"同批次已有工具被用户拒绝,本调用未执行"),resources:[],capturedState:void 0}:await this.prepareWithPlan(i,c),{blocked:n}=l;n&&r(n).kind===e.userRejected&&s&&(s.userRejected=!0),!0!==s?.cancelled&&this.prepared.set(a,{toolId:o,input:c,batch:s,...l})}finally{s&&(s.preparedToolCallIds.add(a),this.releaseBatchAdmissionIfReady(s)),l?.()}}sealBatch(e,t=[]){const r=this.batches.get(e);if(r){for(const e of t)this.plans.has(e.toolId)&&this.trackBatchCall(r,e.toolCallId);r.sealed=!0,this.releaseBatchAdmissionIfReady(r),0===r.toolCallIds.size&&this.batches.delete(e)}this.activeBatch?.callId===e&&(this.activeBatch=void 0)}finishTurn(){for(const e of this.batches.values())this.cancelBatch(e);this.prepared.clear(),this.unbatchedOccurrences.clear(),this.batches.clear(),this.toolCallBatches.clear(),this.activeBatch=void 0,this.preparationTail=Promise.resolve()}async execute(r,o,i,n,d){const h=this.toolCallBatches.get(r)??this.activeBatch;let u,p;h&&this.trackBatchCall(h,r);try{if(h&&(await l(h.admission.promise,n),h.cancelled))return t(e.cancelled,"工具批次已结束,本调用未执行");if(u=this.prepared.get(r),void 0===u){if(h)return t(e.toolFailed,"工具批次准入状态不完整,本调用未执行");if(n?.aborted)throw s(n);const r=this.plans.get(o);u={toolId:o,input:i,batch:void 0,...await this.prepareWithPlan(r,i)}}if(u.blocked)return u.blocked;if(u.batch?.userRejected)return t(e.cancelled,"同批次已有工具被用户拒绝,本调用未执行");if(n?.aborted)throw s(n);const c=this.plans.get(o),C=u;p=this.executionOccurrence(C.batch??h,r);const f=p;return await this.locks.run(C.resources,n,async()=>{const e=await(c?.revalidateExecution?.(i,C.capturedState));return e||(f[a].started=!0,d(C.capturedState))})}catch(r){if(n?.aborted)throw s(n);return r instanceof ToolExecutionCancelledError?t(e.cancelled,"工具调用已取消",{raw:r}):t(e.toolFailed,c(r),{raw:r})}finally{const e=u?.batch??h,t=this.prepared.get(r);(void 0!==u&&t===u||void 0===u&&void 0!==e&&t?.batch===e)&&this.prepared.delete(r),void 0!==p&&void 0===e&&this.unbatchedOccurrences.get(r)===p&&this.unbatchedOccurrences.delete(r),this.completeBatchCall(e,r)}}async prepareWithPlan(r,a){try{const e=r?.captureExecutionState?.(a),t=await(r?.prepare?.(a,e));return t?{blocked:t,resources:[],capturedState:void 0}:{blocked:void 0,resources:r?.resources(a,e)??[],capturedState:e}}catch(r){return{blocked:t(e.toolFailed,c(r),{raw:r}),resources:[],capturedState:void 0}}}completeBatchCall(e,t){e&&(e.completedToolCallIds.add(t),this.cleanupBatchIfComplete(e))}trackBatchCall(e,t){e.toolCallIds.add(t),this.batchOccurrence(e,t),this.toolCallBatches.set(t,e)}batchOccurrence(e,t){const r=e.toolCallOccurrences.get(t);if(r)return r;const a=o(t);return e.toolCallOccurrences.set(t,a),a}executionOccurrence(e,t){if(e)return this.batchOccurrence(e,t);const r=this.unbatchedOccurrences.get(t);if(r)return r;const a=o(t);return this.unbatchedOccurrences.set(t,a),a}releaseBatchAdmissionIfReady(e){if(!e.admission.released){if(!e.sealed)return;if(!e.cancelled&&[...e.toolCallIds].some(t=>!e.preparedToolCallIds.has(t)))return;e.admission.released=!0,e.admission.release()}this.cleanupBatchIfComplete(e)}cancelBatch(e){e.cancelled=!0,e.sealed=!0,this.releaseBatchAdmissionIfReady(e)}cleanupBatchIfComplete(e){if(e.admission.released&&!(e.completedToolCallIds.size<e.toolCallIds.size)){this.batches.get(e.callId)===e&&this.batches.delete(e.callId);for(const t of e.toolCallIds)this.toolCallBatches.get(t)===e&&this.toolCallBatches.delete(t),this.prepared.get(t)?.batch===e&&this.prepared.delete(t)}}}export async function executeCoordinatedTool(r,a,o,i,l,n,d){if(r)return r.execute(i,o,l,n,d);try{if(n?.aborted)throw s(n);const e=a.captureExecutionState?.(l),t=await(a.prepare?.(l,e));if(t)return t;if(n?.aborted)throw s(n);const r=await(a.revalidateExecution?.(l,e));return r||await d(e)}catch(r){if(n?.aborted)throw s(n);return t(e.toolFailed,c(r),{raw:r})}}
@@ -1,6 +1,6 @@
1
1
  import type { SessionCancellationReason } from "./cancellation.ts";
2
2
  import type { ToolOutcome } from "../tool-bridge/normalize-result.ts";
3
- import type { UserInputForm } from "@roll-agent/protocol";
3
+ import type { FileChangeDiff, UserInputForm } from "@roll-agent/protocol";
4
4
  import type { SessionUserInputRequestId } from "../interaction/user-input-interaction-manager.ts";
5
5
  export interface SessionTokenUsage {
6
6
  readonly inputTokens?: number;
@@ -72,6 +72,8 @@ export type SessionEvent = {
72
72
  readonly reason?: string;
73
73
  /** Model-authored display aid. It never changes approval policy or command execution. */
74
74
  readonly explanation?: string;
75
+ readonly sessionGrantLabel?: string;
76
+ readonly diff?: FileChangeDiff;
75
77
  } | {
76
78
  /** Engine-internal request; Runtime Protocol assigns a separate InteractionId. */
77
79
  readonly type: "user-input-required";