@namzu/sdk 62.6.1 → 62.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/dist/agents/QueryAgent.d.ts.map +1 -1
- package/dist/agents/QueryAgent.js +5 -0
- package/dist/agents/QueryAgent.js.map +1 -1
- package/dist/agents/examples/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/examples/SupervisorAgent.js +1 -0
- package/dist/agents/examples/SupervisorAgent.js.map +1 -1
- package/dist/agents/explore.d.ts.map +1 -1
- package/dist/agents/explore.js +2 -1
- package/dist/agents/explore.js.map +1 -1
- package/dist/compaction/index.d.ts +1 -1
- package/dist/compaction/index.d.ts.map +1 -1
- package/dist/compaction/index.js +1 -1
- package/dist/compaction/index.js.map +1 -1
- package/dist/compaction/model-summary.js +2 -2
- package/dist/compaction/model-summary.js.map +1 -1
- package/dist/compaction/summary.d.ts +16 -1
- package/dist/compaction/summary.d.ts.map +1 -1
- package/dist/compaction/summary.js +25 -2
- package/dist/compaction/summary.js.map +1 -1
- package/dist/config/runtime.d.ts +8 -0
- package/dist/config/runtime.d.ts.map +1 -1
- package/dist/config/runtime.js +15 -0
- package/dist/config/runtime.js.map +1 -1
- package/dist/execution/workflow/runtime.d.ts.map +1 -1
- package/dist/execution/workflow/runtime.js +3 -0
- package/dist/execution/workflow/runtime.js.map +1 -1
- package/dist/execution/workflow/types.d.ts +8 -0
- package/dist/execution/workflow/types.d.ts.map +1 -1
- package/dist/manager/agent/child-session.d.ts +12 -0
- package/dist/manager/agent/child-session.d.ts.map +1 -1
- package/dist/manager/agent/child-session.js +29 -0
- package/dist/manager/agent/child-session.js.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +8 -0
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/manager/session/turn-recorder.d.ts +13 -2
- package/dist/manager/session/turn-recorder.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.js +107 -29
- package/dist/manager/session/turn-recorder.js.map +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts +3 -3
- package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
- package/dist/prompt/coding-agent-doctrine.js +5 -3
- package/dist/prompt/coding-agent-doctrine.js.map +1 -1
- package/dist/provider/fallback.d.ts.map +1 -1
- package/dist/provider/fallback.js +5 -1
- package/dist/provider/fallback.js.map +1 -1
- package/dist/provider/retry.d.ts +25 -0
- package/dist/provider/retry.d.ts.map +1 -1
- package/dist/provider/retry.js +28 -0
- package/dist/provider/retry.js.map +1 -1
- package/dist/public-runtime.d.ts +3 -3
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +3 -3
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-tools.d.ts +4 -2
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +5 -2
- package/dist/public-tools.js.map +1 -1
- package/dist/runtime/query/auto-review/classifier.d.ts +7 -1
- package/dist/runtime/query/auto-review/classifier.d.ts.map +1 -1
- package/dist/runtime/query/auto-review/classifier.js +8 -4
- package/dist/runtime/query/auto-review/classifier.js.map +1 -1
- package/dist/runtime/query/auto-review/prompt.d.ts.map +1 -1
- package/dist/runtime/query/auto-review/prompt.js +2 -0
- package/dist/runtime/query/auto-review/prompt.js.map +1 -1
- package/dist/runtime/query/auto-review/script-evidence.d.ts +93 -0
- package/dist/runtime/query/auto-review/script-evidence.d.ts.map +1 -0
- package/dist/runtime/query/auto-review/script-evidence.js +566 -0
- package/dist/runtime/query/auto-review/script-evidence.js.map +1 -0
- package/dist/runtime/query/checkpoint.d.ts +13 -0
- package/dist/runtime/query/checkpoint.d.ts.map +1 -1
- package/dist/runtime/query/checkpoint.js +37 -0
- package/dist/runtime/query/checkpoint.js.map +1 -1
- package/dist/runtime/query/context.d.ts.map +1 -1
- package/dist/runtime/query/context.js +28 -2
- package/dist/runtime/query/context.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +38 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +154 -13
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/finalize-turn.d.ts +7 -1
- package/dist/runtime/query/finalize-turn.d.ts.map +1 -1
- package/dist/runtime/query/finalize-turn.js +2 -0
- package/dist/runtime/query/finalize-turn.js.map +1 -1
- package/dist/runtime/query/index.d.ts +8 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +7 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +32 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +158 -10
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.d.ts +12 -0
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +48 -1
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js +5 -0
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.js +3 -3
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
- package/dist/runtime/query/prelude-lease.d.ts.map +1 -1
- package/dist/runtime/query/prelude-lease.js +22 -0
- package/dist/runtime/query/prelude-lease.js.map +1 -1
- package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
- package/dist/runtime/query/prepare-turn.js +30 -12
- package/dist/runtime/query/prepare-turn.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +29 -0
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/runtime/query/session-storage.d.ts.map +1 -1
- package/dist/runtime/query/session-storage.js +44 -3
- package/dist/runtime/query/session-storage.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +2 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +1 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/runtime/query/turn/completion-audit.d.ts +27 -1
- package/dist/runtime/query/turn/completion-audit.d.ts.map +1 -1
- package/dist/runtime/query/turn/completion-audit.js +71 -6
- package/dist/runtime/query/turn/completion-audit.js.map +1 -1
- package/dist/runtime/query/turn/outcome-ledger.d.ts +113 -0
- package/dist/runtime/query/turn/outcome-ledger.d.ts.map +1 -0
- package/dist/runtime/query/turn/outcome-ledger.js +578 -0
- package/dist/runtime/query/turn/outcome-ledger.js.map +1 -0
- package/dist/sandbox/file-walk.d.ts +7 -1
- package/dist/sandbox/file-walk.d.ts.map +1 -1
- package/dist/sandbox/file-walk.js +30 -4
- package/dist/sandbox/file-walk.js.map +1 -1
- package/dist/sandbox/glob-segment.d.ts +8 -1
- package/dist/sandbox/glob-segment.d.ts.map +1 -1
- package/dist/sandbox/glob-segment.js +28 -9
- package/dist/sandbox/glob-segment.js.map +1 -1
- package/dist/store/checkpoint/contract.d.ts +12 -0
- package/dist/store/checkpoint/contract.d.ts.map +1 -1
- package/dist/store/checkpoint/contract.js.map +1 -1
- package/dist/store/checkpoint/disk.d.ts.map +1 -1
- package/dist/store/checkpoint/disk.js +34 -2
- package/dist/store/checkpoint/disk.js.map +1 -1
- package/dist/store/checkpoint/prune.d.ts.map +1 -1
- package/dist/store/checkpoint/prune.js +6 -0
- package/dist/store/checkpoint/prune.js.map +1 -1
- package/dist/store/evidence/disk.js +40 -3
- package/dist/store/evidence/disk.js.map +1 -1
- package/dist/store/session-log/incremental.d.ts +23 -0
- package/dist/store/session-log/incremental.d.ts.map +1 -0
- package/dist/store/session-log/incremental.js +117 -0
- package/dist/store/session-log/incremental.js.map +1 -0
- package/dist/store/session-log/index.d.ts +1 -0
- package/dist/store/session-log/index.d.ts.map +1 -1
- package/dist/store/session-log/index.js +1 -0
- package/dist/store/session-log/index.js.map +1 -1
- package/dist/store/session-log/turn-outcome.d.ts +37 -0
- package/dist/store/session-log/turn-outcome.d.ts.map +1 -0
- package/dist/store/session-log/turn-outcome.js +77 -0
- package/dist/store/session-log/turn-outcome.js.map +1 -0
- package/dist/tools/admit-facts.d.ts.map +1 -1
- package/dist/tools/admit-facts.js +1 -0
- package/dist/tools/admit-facts.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +11 -2
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/bounded-file-read.d.ts +19 -1
- package/dist/tools/builtins/bounded-file-read.d.ts.map +1 -1
- package/dist/tools/builtins/bounded-file-read.js +56 -3
- package/dist/tools/builtins/bounded-file-read.js.map +1 -1
- package/dist/tools/builtins/edit-apply.d.ts.map +1 -1
- package/dist/tools/builtins/edit-apply.js +24 -3
- package/dist/tools/builtins/edit-apply.js.map +1 -1
- package/dist/tools/builtins/edit.d.ts +31 -17
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +92 -27
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/glob.js +1 -1
- package/dist/tools/builtins/glob.js.map +1 -1
- package/dist/tools/builtins/grep.d.ts +8 -0
- package/dist/tools/builtins/grep.d.ts.map +1 -1
- package/dist/tools/builtins/grep.js +148 -21
- package/dist/tools/builtins/grep.js.map +1 -1
- package/dist/tools/builtins/notebook-guard.d.ts +19 -0
- package/dist/tools/builtins/notebook-guard.d.ts.map +1 -0
- package/dist/tools/builtins/notebook-guard.js +75 -0
- package/dist/tools/builtins/notebook-guard.js.map +1 -0
- package/dist/tools/builtins/read-render.d.ts.map +1 -1
- package/dist/tools/builtins/read-render.js +8 -2
- package/dist/tools/builtins/read-render.js.map +1 -1
- package/dist/tools/builtins/search-support.d.ts +8 -0
- package/dist/tools/builtins/search-support.d.ts.map +1 -1
- package/dist/tools/builtins/search-support.js +66 -0
- package/dist/tools/builtins/search-support.js.map +1 -1
- package/dist/tools/builtins/write-file.d.ts.map +1 -1
- package/dist/tools/builtins/write-file.js +14 -1
- package/dist/tools/builtins/write-file.js.map +1 -1
- package/dist/tools/coordinator/agent.d.ts.map +1 -1
- package/dist/tools/coordinator/agent.js +25 -6
- package/dist/tools/coordinator/agent.js.map +1 -1
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +6 -1
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/coordinator/outcome-record.d.ts +22 -0
- package/dist/tools/coordinator/outcome-record.d.ts.map +1 -0
- package/dist/tools/coordinator/outcome-record.js +91 -0
- package/dist/tools/coordinator/outcome-record.js.map +1 -0
- package/dist/tools/defineTool.d.ts +2 -0
- package/dist/tools/defineTool.d.ts.map +1 -1
- package/dist/tools/defineTool.js +1 -0
- package/dist/tools/defineTool.js.map +1 -1
- package/dist/tools/file-read-tracker.d.ts +23 -2
- package/dist/tools/file-read-tracker.d.ts.map +1 -1
- package/dist/tools/file-read-tracker.js +37 -2
- package/dist/tools/file-read-tracker.js.map +1 -1
- package/dist/tools/untrusted-envelope.d.ts +6 -0
- package/dist/tools/untrusted-envelope.d.ts.map +1 -1
- package/dist/tools/untrusted-envelope.js +22 -1
- package/dist/tools/untrusted-envelope.js.map +1 -1
- package/dist/toolsets/manager.d.ts.map +1 -1
- package/dist/toolsets/manager.js +3 -4
- package/dist/toolsets/manager.js.map +1 -1
- package/dist/toolsets/validation-issues.d.ts +27 -0
- package/dist/toolsets/validation-issues.d.ts.map +1 -0
- package/dist/toolsets/validation-issues.js +83 -0
- package/dist/toolsets/validation-issues.js.map +1 -0
- package/dist/turn/memory-recall.d.ts +13 -0
- package/dist/turn/memory-recall.d.ts.map +1 -1
- package/dist/turn/memory-recall.js +18 -0
- package/dist/turn/memory-recall.js.map +1 -1
- package/dist/types/agent/base.d.ts +19 -0
- package/dist/types/agent/base.d.ts.map +1 -1
- package/dist/types/agent/query.d.ts +2 -0
- package/dist/types/agent/query.d.ts.map +1 -1
- package/dist/types/common/index.d.ts +17 -0
- package/dist/types/common/index.d.ts.map +1 -1
- package/dist/types/common/index.js +8 -1
- package/dist/types/common/index.js.map +1 -1
- package/dist/types/hitl/index.d.ts +22 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/session/config.d.ts +20 -0
- package/dist/types/session/config.d.ts.map +1 -1
- package/dist/types/session/events.d.ts +3 -3
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/index.d.ts +1 -0
- package/dist/types/session/index.d.ts.map +1 -1
- package/dist/types/session/index.js +1 -0
- package/dist/types/session/index.js.map +1 -1
- package/dist/types/session/outcome.d.ts +95 -0
- package/dist/types/session/outcome.d.ts.map +1 -0
- package/dist/types/session/outcome.js +45 -0
- package/dist/types/session/outcome.js.map +1 -0
- package/dist/types/session/prepare-step.d.ts +2 -0
- package/dist/types/session/prepare-step.d.ts.map +1 -1
- package/dist/types/session/records.d.ts +167 -2
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +42 -0
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/session/turn.d.ts +18 -0
- package/dist/types/session/turn.d.ts.map +1 -1
- package/dist/types/session/turn.js.map +1 -1
- package/dist/types/tool/index.d.ts +30 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/tool/presentation.d.ts +6 -0
- package/dist/types/tool/presentation.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/agents/QueryAgent.ts +5 -0
- package/src/agents/examples/SupervisorAgent.ts +1 -0
- package/src/agents/explore.ts +2 -1
- package/src/compaction/index.ts +1 -0
- package/src/compaction/model-summary.ts +2 -2
- package/src/compaction/summary.ts +30 -2
- package/src/config/runtime.ts +15 -0
- package/src/execution/workflow/runtime.ts +4 -0
- package/src/execution/workflow/types.ts +9 -0
- package/src/manager/agent/child-session.ts +36 -0
- package/src/manager/agent/lifecycle.ts +9 -0
- package/src/manager/session/turn-recorder.ts +115 -30
- package/src/pricing/rates.source.json +1 -1
- package/src/prompt/coding-agent-doctrine.ts +5 -3
- package/src/provider/fallback.ts +6 -1
- package/src/provider/retry.ts +51 -0
- package/src/public-runtime.ts +10 -1
- package/src/public-tools.ts +6 -1
- package/src/runtime/query/auto-review/classifier.ts +21 -5
- package/src/runtime/query/auto-review/prompt.ts +2 -0
- package/src/runtime/query/auto-review/script-evidence.ts +618 -0
- package/src/runtime/query/checkpoint.ts +53 -1
- package/src/runtime/query/context.ts +33 -6
- package/src/runtime/query/executor.ts +183 -21
- package/src/runtime/query/finalize-turn.ts +8 -1
- package/src/runtime/query/index.ts +19 -0
- package/src/runtime/query/iteration/index.ts +167 -14
- package/src/runtime/query/iteration/phases/compaction.ts +53 -3
- package/src/runtime/query/iteration/phases/context.ts +6 -0
- package/src/runtime/query/iteration/stream-turn.ts +3 -3
- package/src/runtime/query/prelude-lease.ts +19 -0
- package/src/runtime/query/prepare-turn.ts +29 -12
- package/src/runtime/query/review-policy.ts +33 -0
- package/src/runtime/query/session-storage.ts +42 -3
- package/src/runtime/query/tooling.ts +3 -0
- package/src/runtime/query/turn/completion-audit.ts +76 -5
- package/src/runtime/query/turn/outcome-ledger.ts +633 -0
- package/src/sandbox/file-walk.ts +41 -4
- package/src/sandbox/glob-segment.ts +48 -24
- package/src/store/checkpoint/contract.ts +15 -0
- package/src/store/checkpoint/disk.ts +34 -2
- package/src/store/checkpoint/prune.ts +13 -0
- package/src/store/evidence/disk.ts +57 -2
- package/src/store/session-log/incremental.ts +147 -0
- package/src/store/session-log/index.ts +7 -0
- package/src/store/session-log/turn-outcome.ts +84 -0
- package/src/tools/admit-facts.ts +1 -0
- package/src/tools/builtins/bash.ts +12 -2
- package/src/tools/builtins/bounded-file-read.ts +59 -2
- package/src/tools/builtins/edit-apply.ts +26 -4
- package/src/tools/builtins/edit.ts +106 -41
- package/src/tools/builtins/glob.ts +1 -1
- package/src/tools/builtins/grep.ts +173 -27
- package/src/tools/builtins/notebook-guard.ts +76 -0
- package/src/tools/builtins/read-render.ts +12 -2
- package/src/tools/builtins/search-support.ts +62 -0
- package/src/tools/builtins/write-file.ts +12 -1
- package/src/tools/coordinator/agent.ts +31 -7
- package/src/tools/coordinator/index.ts +6 -1
- package/src/tools/coordinator/outcome-record.ts +93 -0
- package/src/tools/defineTool.ts +3 -0
- package/src/tools/file-read-tracker.ts +51 -2
- package/src/tools/untrusted-envelope.ts +23 -1
- package/src/toolsets/manager.ts +6 -10
- package/src/toolsets/validation-issues.ts +97 -0
- package/src/turn/memory-recall.ts +28 -0
- package/src/types/agent/base.ts +20 -0
- package/src/types/agent/query.ts +2 -0
- package/src/types/common/index.ts +25 -1
- package/src/types/hitl/index.ts +27 -0
- package/src/types/session/config.ts +20 -0
- package/src/types/session/events.ts +3 -3
- package/src/types/session/index.ts +1 -0
- package/src/types/session/outcome.ts +139 -0
- package/src/types/session/prepare-step.ts +2 -0
- package/src/types/session/records.ts +67 -1
- package/src/types/session/turn.ts +19 -0
- package/src/types/tool/index.ts +30 -0
- package/src/types/tool/presentation.ts +6 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { stat } from 'node:fs/promises'
|
|
2
|
-
import { isAbsolute, join, relative, sep } from 'node:path'
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
3
3
|
import { z } from 'zod'
|
|
4
4
|
import { walkFilesLocally } from '../../sandbox/file-walk.js'
|
|
5
5
|
import type { SandboxWalkFilesOptions, SandboxWalkSummary } from '../../types/sandbox/index.js'
|
|
@@ -7,6 +7,7 @@ import type { ToolContext } from '../../types/tool/index.js'
|
|
|
7
7
|
import { subscribeToAbort } from '../../utils/abort.js'
|
|
8
8
|
import { defineTool } from '../defineTool.js'
|
|
9
9
|
import {
|
|
10
|
+
canonicalize,
|
|
10
11
|
resolveWithin,
|
|
11
12
|
resolveWithinAny,
|
|
12
13
|
resolveWithinAnyReal,
|
|
@@ -14,7 +15,13 @@ import {
|
|
|
14
15
|
toolRoots,
|
|
15
16
|
} from '../paths.js'
|
|
16
17
|
import { relativePosix, resolveWithinPosix } from '../posix-path.js'
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
CAN_REFUSE_LINKS,
|
|
20
|
+
REFUSE_LINK_FLAG,
|
|
21
|
+
readBoundedChunks,
|
|
22
|
+
readBoundedFile,
|
|
23
|
+
readBoundedFileSync,
|
|
24
|
+
} from './bounded-file-read.js'
|
|
18
25
|
import {
|
|
19
26
|
MAX_FILES_SEARCHED,
|
|
20
27
|
SEARCH_BUDGET_MS,
|
|
@@ -24,6 +31,7 @@ import {
|
|
|
24
31
|
isReadDenied,
|
|
25
32
|
openSearchBudget,
|
|
26
33
|
plural,
|
|
34
|
+
readAhead,
|
|
27
35
|
stoppedNotice,
|
|
28
36
|
} from './search-support.js'
|
|
29
37
|
|
|
@@ -49,6 +57,125 @@ async function readWithSignal(read: () => Promise<Buffer>, signal?: AbortSignal)
|
|
|
49
57
|
}
|
|
50
58
|
}
|
|
51
59
|
|
|
60
|
+
/** Longest the local read loop holds the event loop before it yields. */
|
|
61
|
+
const READ_SLICE_MS = 8
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Largest file the local reader reads synchronously, by the size on the open
|
|
65
|
+
* handle. One synchronous read of a file is one uninterrupted block, which on a
|
|
66
|
+
* slow mount can last tens of milliseconds; a larger file is read through the
|
|
67
|
+
* asynchronous bounded read instead, which lets the event loop run between
|
|
68
|
+
* chunks.
|
|
69
|
+
*/
|
|
70
|
+
export const LOCAL_SYNC_READ_MAX_BYTES = 64 * 1024
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Reads files of a local search root. Each directory is resolved to its real
|
|
74
|
+
* path once and checked against the real root once; a file is then one open
|
|
75
|
+
* (links refused at the last component), fstat, read and close, all
|
|
76
|
+
* synchronous, with a yield to the event loop every few milliseconds. That is
|
|
77
|
+
* the same containment decision as resolving every file, because the walk
|
|
78
|
+
* lists only regular files in real directories and the open refuses a file
|
|
79
|
+
* that has become a link. Not used for a sandbox, nor where the platform cannot
|
|
80
|
+
* refuse links.
|
|
81
|
+
*/
|
|
82
|
+
function createLocalReader(root: string, signal: AbortSignal | undefined) {
|
|
83
|
+
const scratch = Buffer.allocUnsafe(64 * 1024)
|
|
84
|
+
const rootRealPromise = canonicalize(resolve(root))
|
|
85
|
+
const directories = new Map<string, Promise<string>>()
|
|
86
|
+
let sliceStart = performance.now()
|
|
87
|
+
const realDirectory = (directory: string): Promise<string> => {
|
|
88
|
+
let known = directories.get(directory)
|
|
89
|
+
if (!known) {
|
|
90
|
+
known = canonicalize(directory)
|
|
91
|
+
directories.set(directory, known)
|
|
92
|
+
}
|
|
93
|
+
return known
|
|
94
|
+
}
|
|
95
|
+
let rootReal: string | undefined
|
|
96
|
+
return {
|
|
97
|
+
async read(
|
|
98
|
+
filePath: string,
|
|
99
|
+
): Promise<{ buffer: Buffer | 'too-large' | 'gone'; contained: string }> {
|
|
100
|
+
if (performance.now() - sliceStart > READ_SLICE_MS) {
|
|
101
|
+
await new Promise<void>((done) => setImmediate(done))
|
|
102
|
+
sliceStart = performance.now()
|
|
103
|
+
}
|
|
104
|
+
signal?.throwIfAborted()
|
|
105
|
+
const directory = dirname(filePath)
|
|
106
|
+
const real = await realDirectory(directory)
|
|
107
|
+
rootReal ??= await rootRealPromise
|
|
108
|
+
const contained =
|
|
109
|
+
real === directory ? filePath : join(real, filePath.slice(directory.length + 1))
|
|
110
|
+
if (!(contained.startsWith(rootReal + sep) || contained === rootReal)) {
|
|
111
|
+
const rel = relative(rootReal, contained)
|
|
112
|
+
if (rel.startsWith('..') || isAbsolute(rel)) {
|
|
113
|
+
throw new Error('Path escapes the working directory through a link.')
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
signal?.throwIfAborted()
|
|
117
|
+
try {
|
|
118
|
+
const read = readBoundedFileSync(
|
|
119
|
+
contained,
|
|
120
|
+
MAX_FILE_SIZE,
|
|
121
|
+
scratch,
|
|
122
|
+
LOCAL_SYNC_READ_MAX_BYTES,
|
|
123
|
+
)
|
|
124
|
+
if (read !== 'large') return { buffer: read, contained }
|
|
125
|
+
// The handle that measured the file is closed; the asynchronous read opens it
|
|
126
|
+
// again with the same flags (links refused) and repeats every check.
|
|
127
|
+
const large = await readBoundedFile(
|
|
128
|
+
contained,
|
|
129
|
+
MAX_FILE_SIZE,
|
|
130
|
+
signal,
|
|
131
|
+
undefined,
|
|
132
|
+
REFUSE_LINK_FLAG,
|
|
133
|
+
)
|
|
134
|
+
sliceStart = performance.now()
|
|
135
|
+
return { buffer: large, contained }
|
|
136
|
+
} catch (error) {
|
|
137
|
+
// Gone since the walk listed it: absent, as a walk that ran later would find it.
|
|
138
|
+
const code = (error as NodeJS.ErrnoException).code
|
|
139
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') return { buffer: 'gone', contained }
|
|
140
|
+
throw error
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** A walked path that is already inside `root` in normal form, or null when it needs the full check. */
|
|
147
|
+
const PLAIN_TAIL = /(^|\/)\.{1,2}(\/|$)|\/\/|\/$/
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The fast form of what `resolveWithin` and the display path compute for the
|
|
151
|
+
* walk's own entries on a POSIX host: a path the walk built under `root` is
|
|
152
|
+
* already normal, so it is returned as it is and its display name is a
|
|
153
|
+
* prefix plus the tail. Anything else, and every other platform, takes the
|
|
154
|
+
* full computation.
|
|
155
|
+
*/
|
|
156
|
+
function createWalkedPaths(root: string, lexicalRoot: string, workingDirectory: string) {
|
|
157
|
+
if (sep !== '/') return undefined
|
|
158
|
+
const prefix = root === '/' ? '/' : `${root}/`
|
|
159
|
+
const rootRel = relative(workingDirectory, lexicalRoot)
|
|
160
|
+
const display = rootRel.startsWith('..') || isAbsolute(rootRel) ? undefined : rootRel
|
|
161
|
+
return {
|
|
162
|
+
within(path: string): string {
|
|
163
|
+
if (path.startsWith(prefix) && path.length > prefix.length) {
|
|
164
|
+
if (!PLAIN_TAIL.test(path.slice(prefix.length))) return path
|
|
165
|
+
}
|
|
166
|
+
return resolveWithin(root, path)
|
|
167
|
+
},
|
|
168
|
+
displayName(path: string): string | undefined {
|
|
169
|
+
if (display === undefined || !path.startsWith(prefix) || path.length === prefix.length) {
|
|
170
|
+
return undefined
|
|
171
|
+
}
|
|
172
|
+
const tail = path.slice(prefix.length)
|
|
173
|
+
if (PLAIN_TAIL.test(tail)) return undefined
|
|
174
|
+
return display === '' ? tail : `${display}/${tail}`
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
52
179
|
function relativeInclude(include: string, root: string, sandboxed: boolean): string {
|
|
53
180
|
let pattern = sandboxed ? include : include.split(sep).join('/')
|
|
54
181
|
if (pattern.split('/').includes('..'))
|
|
@@ -201,17 +328,13 @@ const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5 MB — skip binaries/large files
|
|
|
201
328
|
const BINARY_CHECK_BYTES = 512
|
|
202
329
|
|
|
203
330
|
function isBinaryContent(buffer: Buffer): boolean {
|
|
204
|
-
|
|
205
|
-
for (const byte of check) {
|
|
206
|
-
if (byte === 0) return true
|
|
207
|
-
}
|
|
208
|
-
return false
|
|
331
|
+
return buffer.subarray(0, BINARY_CHECK_BYTES).includes(0)
|
|
209
332
|
}
|
|
210
333
|
|
|
211
334
|
export const GrepTool = defineTool({
|
|
212
335
|
name: 'grep',
|
|
213
336
|
description:
|
|
214
|
-
'Searches file contents using a regular expression. By default returns the paths of matching files, newest first; output_mode "content" returns matching lines with file paths, line numbers and optional context lines, and "count" returns the number of matching lines per file. Set multiline for a pattern that spans lines and type (js, py, rust, ...) to search one kind of file. In content mode, before_context and after_context override symmetric context_lines; only_matching returns matched substrings without context. Results are paged: head_limit (default 250, 0 for none) and offset. A line over 500 characters is replaced by a notice. Searches hidden files unless include_hidden is false. Skips files that .gitignore and .ignore rules exclude unless include_ignored is set, and files the read rules of the session deny. Says when a result is limited or a search stopped early. Skips binary files, files over 5 MB, and symlinks. Choose the narrowest relevant directory.',
|
|
337
|
+
'Searches file contents using a regular expression. By default returns the paths of matching files, newest first; output_mode "content" returns matching lines with file paths, line numbers and optional context lines, and "count" returns the number of matching lines per file. Set multiline for a pattern that spans lines and type (js, py, rust, ...) to search one kind of file. In content mode, before_context and after_context override symmetric context_lines; only_matching returns matched substrings without context. Results are paged: head_limit (default 250, 0 for none) and offset. A line over 500 characters is replaced by a notice. Searches hidden files unless include_hidden is false. Skips files that .gitignore and .ignore rules exclude unless include_ignored is set, and files the read rules of the session deny. Says when a result is limited or a search stopped early. Skips binary files, files over 5 MB, and symlinks. Choose the narrowest relevant directory. To find files by name, use glob.',
|
|
215
338
|
inputSchema,
|
|
216
339
|
category: 'analysis',
|
|
217
340
|
// Declared so a path outside the turn's roots can be reviewed before the
|
|
@@ -291,7 +414,11 @@ export const GrepTool = defineTool({
|
|
|
291
414
|
walkEnd = value
|
|
292
415
|
},
|
|
293
416
|
}
|
|
294
|
-
|
|
417
|
+
// A local walk is read ahead so its directory reads overlap with matching; a
|
|
418
|
+
// sandbox's walk is left as the adapter produces it.
|
|
419
|
+
const entries = walkSandbox
|
|
420
|
+
? walkSandbox(root, options)
|
|
421
|
+
: readAhead(walkFilesLocally(root, { ...options, withoutSizes: CAN_REFUSE_LINKS }))
|
|
295
422
|
|
|
296
423
|
// `execute` is reachable without the schema's defaults.
|
|
297
424
|
const mode = input.output_mode ?? 'files_with_matches'
|
|
@@ -321,12 +448,16 @@ export const GrepTool = defineTool({
|
|
|
321
448
|
const filesToCollect =
|
|
322
449
|
headLimit > 0 ? offset + Math.max(headLimit * 4, 1000) : Number.POSITIVE_INFINITY
|
|
323
450
|
let enough = false
|
|
451
|
+
const walked = sandbox
|
|
452
|
+
? undefined
|
|
453
|
+
: createWalkedPaths(root, lexicalRoot, context.workingDirectory)
|
|
454
|
+
const localReader = !sandbox && CAN_REFUSE_LINKS ? createLocalReader(root, signal) : undefined
|
|
324
455
|
try {
|
|
325
456
|
for await (const entry of entries) {
|
|
326
457
|
signal?.throwIfAborted()
|
|
327
458
|
const filePath = sandbox
|
|
328
459
|
? resolveWithinPosix(root, entry.path)
|
|
329
|
-
: resolveWithin(root, entry.path)
|
|
460
|
+
: (walked?.within(entry.path) ?? resolveWithin(root, entry.path))
|
|
330
461
|
if (filesSearched >= MAX_FILES_SEARCHED) {
|
|
331
462
|
fileLimit = true
|
|
332
463
|
break
|
|
@@ -339,21 +470,34 @@ export const GrepTool = defineTool({
|
|
|
339
470
|
let content: string
|
|
340
471
|
let contained: string | undefined
|
|
341
472
|
try {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
473
|
+
let buffer: Buffer
|
|
474
|
+
if (localReader) {
|
|
475
|
+
const read = await localReader.read(filePath)
|
|
476
|
+
contained = read.contained
|
|
477
|
+
// Over the cap by the size on the open handle: skipped, not unreadable.
|
|
478
|
+
if (read.buffer === 'too-large') continue
|
|
479
|
+
// Gone since the walk listed it: as if the walk had not listed it.
|
|
480
|
+
if (read.buffer === 'gone') {
|
|
481
|
+
filesSearched--
|
|
482
|
+
continue
|
|
483
|
+
}
|
|
484
|
+
buffer = read.buffer
|
|
485
|
+
} else
|
|
486
|
+
buffer = await readWithSignal(
|
|
487
|
+
() =>
|
|
488
|
+
sandbox
|
|
489
|
+
? readBoundedChunks(
|
|
490
|
+
(offset, length) =>
|
|
491
|
+
sandbox.readFile(filePath, { offset, length, signal: signal }),
|
|
492
|
+
MAX_FILE_SIZE,
|
|
493
|
+
signal,
|
|
494
|
+
)
|
|
495
|
+
: resolveWithinReal(root, filePath).then((path) => {
|
|
496
|
+
contained = path
|
|
497
|
+
return readBoundedFile(path, MAX_FILE_SIZE, signal)
|
|
498
|
+
}),
|
|
499
|
+
signal,
|
|
500
|
+
)
|
|
357
501
|
signal?.throwIfAborted()
|
|
358
502
|
if (buffer.length > MAX_FILE_SIZE || isBinaryContent(buffer)) continue
|
|
359
503
|
content = buffer.toString('utf-8')
|
|
@@ -367,7 +511,8 @@ export const GrepTool = defineTool({
|
|
|
367
511
|
let fileMatches = 0
|
|
368
512
|
const relPath = sandbox
|
|
369
513
|
? relativePosix(sandbox.rootDir, filePath)
|
|
370
|
-
:
|
|
514
|
+
: (walked?.displayName(filePath) ??
|
|
515
|
+
hostDisplayPath(context.workingDirectory, join(lexicalRoot, relative(root, filePath))))
|
|
371
516
|
|
|
372
517
|
if (mode === 'content' && input.only_matching) {
|
|
373
518
|
for (const fragment of matchingFragments(content, regex, multiline)) {
|
|
@@ -580,7 +725,8 @@ function lineHits(
|
|
|
580
725
|
): [number, number][] {
|
|
581
726
|
const hits: [number, number][] = []
|
|
582
727
|
for (let i = 0; i < lines.length; i++) {
|
|
583
|
-
|
|
728
|
+
// A cancellation is seen within a few hundred lines; per line it cost more than the match.
|
|
729
|
+
if ((i & 255) === 0) context.abortSignal?.throwIfAborted()
|
|
584
730
|
regex.lastIndex = 0
|
|
585
731
|
if (regex.test(lines[i] ?? '')) hits.push([i, i])
|
|
586
732
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A notebook is JSON with a fixed shape. `edit` and `write` treat every file
|
|
3
|
+
* as text, so a model can leave an `.ipynb` that no notebook program opens.
|
|
4
|
+
* This check runs on the body about to be written, never on the file on disk,
|
|
5
|
+
* so a refusal leaves the file untouched.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const CELL_TYPES = new Set(['code', 'markdown', 'raw'])
|
|
9
|
+
|
|
10
|
+
export function isNotebookPath(path: string): boolean {
|
|
11
|
+
return path.toLowerCase().endsWith('.ipynb')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
15
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function shapeProblem(parsed: unknown): string | undefined {
|
|
19
|
+
if (!isRecord(parsed)) return 'the top level must be a JSON object'
|
|
20
|
+
if (typeof parsed.nbformat !== 'number') return 'it needs a numeric "nbformat"'
|
|
21
|
+
if (!isRecord(parsed.metadata)) return 'it needs a "metadata" object'
|
|
22
|
+
// Formats before 4 keep their cells inside "worksheets" and name a code cell's text "input".
|
|
23
|
+
// Those files are valid and still open, so only the envelope is checked for them.
|
|
24
|
+
if (parsed.nbformat < 4) {
|
|
25
|
+
return Array.isArray(parsed.worksheets) ? undefined : 'it needs a "worksheets" list'
|
|
26
|
+
}
|
|
27
|
+
if (!Array.isArray(parsed.cells)) return 'it needs a "cells" list'
|
|
28
|
+
for (const [index, cell] of parsed.cells.entries()) {
|
|
29
|
+
if (!isRecord(cell)) return `cell ${index} must be an object`
|
|
30
|
+
if (typeof cell.cell_type !== 'string' || !CELL_TYPES.has(cell.cell_type)) {
|
|
31
|
+
return `cell ${index} needs a "cell_type" of code, markdown or raw`
|
|
32
|
+
}
|
|
33
|
+
const source = cell.source
|
|
34
|
+
const sourceOk =
|
|
35
|
+
typeof source === 'string' ||
|
|
36
|
+
(Array.isArray(source) && source.every((line) => typeof line === 'string'))
|
|
37
|
+
if (!sourceOk) return `cell ${index} needs a "source" that is a string or a list of strings`
|
|
38
|
+
}
|
|
39
|
+
return undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Why a notebook body would not open, or `undefined` when it would. */
|
|
43
|
+
function bodyProblem(content: string): string | undefined {
|
|
44
|
+
let parsed: unknown
|
|
45
|
+
try {
|
|
46
|
+
parsed = JSON.parse(content)
|
|
47
|
+
} catch (error) {
|
|
48
|
+
const reason = error instanceof Error ? error.message : String(error)
|
|
49
|
+
return `it is not valid JSON (${reason})`
|
|
50
|
+
}
|
|
51
|
+
return shapeProblem(parsed)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Returns a plain refusal for a notebook body that would no longer open, or
|
|
56
|
+
* `undefined` when the body is fine or the path is not a notebook.
|
|
57
|
+
*
|
|
58
|
+
* `before` is the file's current body, when the caller has it. A notebook that
|
|
59
|
+
* was already broken cannot be mended one string at a time, because every
|
|
60
|
+
* partial repair still leaves a file that does not open; without saying so the
|
|
61
|
+
* refusal would tell the model to keep a structure the file never had, and it
|
|
62
|
+
* would try the same edit again.
|
|
63
|
+
*/
|
|
64
|
+
export function notebookRefusal(
|
|
65
|
+
path: string,
|
|
66
|
+
content: string,
|
|
67
|
+
before?: string,
|
|
68
|
+
): string | undefined {
|
|
69
|
+
if (!isNotebookPath(path)) return undefined
|
|
70
|
+
const problem = bodyProblem(content)
|
|
71
|
+
if (problem === undefined) return undefined
|
|
72
|
+
if (before !== undefined && bodyProblem(before) !== undefined) {
|
|
73
|
+
return `Not written: ${path} is a notebook that already does not open, and after this change it still would not because ${problem}. Repair it in one change that leaves a whole valid notebook, or replace the file with a write of the corrected notebook. The file is unchanged.`
|
|
74
|
+
}
|
|
75
|
+
return `Not written: ${path} is a notebook, and after this change it would no longer open because ${problem}. Change only text inside a cell and keep the notebook's structure (its cells, nbformat and metadata, with the surrounding quotes, commas and brackets), then try again. The file is unchanged.`
|
|
76
|
+
}
|
|
@@ -80,8 +80,12 @@ export function renderNumberedRead(content: string, input: ReadWindowRequest): R
|
|
|
80
80
|
}
|
|
81
81
|
const selected = lines.slice(start, Math.min(end, lineCount))
|
|
82
82
|
const numbered = selected.map((line, i) => `${start + i + 1}\t${line}`).join('\n')
|
|
83
|
+
// Only a cut the cap imposed is the model's to be warned about: a window it
|
|
84
|
+
// asked for is what it got.
|
|
85
|
+
const asked = (input.readRange?.length ?? 0) >= 2 || Boolean(input.limit)
|
|
86
|
+
const capCut = !asked
|
|
83
87
|
return {
|
|
84
|
-
output: numbered + partialViewNotice(start, selected.length, lineCount),
|
|
88
|
+
output: numbered + partialViewNotice(start, selected.length, lineCount, capCut),
|
|
85
89
|
totalLines: lineCount,
|
|
86
90
|
returnedLines: selected.length,
|
|
87
91
|
partial: selected.length < lineCount,
|
|
@@ -97,9 +101,15 @@ export function renderNumberedRead(content: string, input: ReadWindowRequest): R
|
|
|
97
101
|
* most expensive silent failure a read tool can have. The notice names the
|
|
98
102
|
* exact next call rather than describing it.
|
|
99
103
|
*/
|
|
100
|
-
function partialViewNotice(
|
|
104
|
+
function partialViewNotice(
|
|
105
|
+
start: number,
|
|
106
|
+
returned: number,
|
|
107
|
+
total: number,
|
|
108
|
+
capCut: boolean,
|
|
109
|
+
): string {
|
|
101
110
|
if (returned >= total) return ''
|
|
102
111
|
const shownTo = start + returned
|
|
112
|
+
if (!capCut) return `\n\n[Lines ${start + 1}-${shownTo} of ${total}, as asked.]`
|
|
103
113
|
return [
|
|
104
114
|
'',
|
|
105
115
|
'',
|
|
@@ -105,3 +105,65 @@ export function isReadDenied(
|
|
|
105
105
|
if (relative === '..' || relative.startsWith('../') || posix.isAbsolute(relative)) return true
|
|
106
106
|
return context.readDenied(joinPath(context.workingDirectory, relative))
|
|
107
107
|
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Pulls from `source` ahead of the consumer, up to `ahead` items, so a walk's
|
|
111
|
+
* directory reads overlap with the matching of files already listed. Order is
|
|
112
|
+
* unchanged. An error from the source is thrown to the consumer after the
|
|
113
|
+
* items listed before it; when the consumer stops, the source is closed (its
|
|
114
|
+
* own cleanup runs) before this returns.
|
|
115
|
+
*/
|
|
116
|
+
export async function* readAhead<T>(source: AsyncIterable<T>, ahead = 256): AsyncGenerator<T> {
|
|
117
|
+
const iterator = source[Symbol.asyncIterator]()
|
|
118
|
+
const buffer: T[] = []
|
|
119
|
+
let finished = false
|
|
120
|
+
let failure: { error: unknown } | undefined
|
|
121
|
+
let stop = false
|
|
122
|
+
let wakeConsumer: (() => void) | undefined
|
|
123
|
+
let wakeProducer: (() => void) | undefined
|
|
124
|
+
const notifyConsumer = () => {
|
|
125
|
+
const wake = wakeConsumer
|
|
126
|
+
wakeConsumer = undefined
|
|
127
|
+
wake?.()
|
|
128
|
+
}
|
|
129
|
+
const producer = (async () => {
|
|
130
|
+
try {
|
|
131
|
+
while (!stop) {
|
|
132
|
+
if (buffer.length >= ahead) {
|
|
133
|
+
await new Promise<void>((resolve) => {
|
|
134
|
+
wakeProducer = resolve
|
|
135
|
+
})
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
const next = await iterator.next()
|
|
139
|
+
if (next.done) break
|
|
140
|
+
buffer.push(next.value)
|
|
141
|
+
notifyConsumer()
|
|
142
|
+
}
|
|
143
|
+
} catch (error) {
|
|
144
|
+
failure = { error }
|
|
145
|
+
}
|
|
146
|
+
finished = true
|
|
147
|
+
notifyConsumer()
|
|
148
|
+
})()
|
|
149
|
+
try {
|
|
150
|
+
for (;;) {
|
|
151
|
+
if (buffer.length > 0) {
|
|
152
|
+
const item = buffer.shift() as T
|
|
153
|
+
wakeProducer?.()
|
|
154
|
+
yield item
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
if (failure) throw failure.error
|
|
158
|
+
if (finished) return
|
|
159
|
+
await new Promise<void>((resolve) => {
|
|
160
|
+
wakeConsumer = resolve
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
} finally {
|
|
164
|
+
stop = true
|
|
165
|
+
wakeProducer?.()
|
|
166
|
+
await producer
|
|
167
|
+
await iterator.return?.()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -11,6 +11,7 @@ import { fingerprintContent, staleFileError } from './content-fingerprint.js'
|
|
|
11
11
|
import { withFileMutationLock } from './file-mutation-lock.js'
|
|
12
12
|
import { fileWriteResult } from './file-write-result.js'
|
|
13
13
|
import { jsonStringEscapes } from './json-string-hint.js'
|
|
14
|
+
import { notebookRefusal } from './notebook-guard.js'
|
|
14
15
|
import { staleBeforeReview } from './stale-before-review.js'
|
|
15
16
|
|
|
16
17
|
const inputSchema = z
|
|
@@ -74,7 +75,7 @@ const modelInputSchema: Record<string, unknown> = {
|
|
|
74
75
|
export const WriteFileTool = defineTool({
|
|
75
76
|
name: 'write',
|
|
76
77
|
description:
|
|
77
|
-
'Writes a file to the local filesystem. Overwrites the existing file at the path if there is one.\n\n- If the file already exists, you must use the `read` tool on it first in this conversation, or this call will fail.\n- To modify an existing file, use `edit`: it only sends the diff and preserves the rest of the file byte-for-byte.\n- Use `write` to create a new file or to perform a deliberate full rewrite of a file you have already read.\n- Self-budget content/newStr under 12000 characters before emitting the tool call. For long content, write a smaller opening section, then use `edit` with insertLine: "end" to extend the file section by section. Chaining several `write` calls to one path loses content, because each overwrites the previous.',
|
|
78
|
+
'Writes a file to the local filesystem. Overwrites the existing file at the path if there is one.\n\n- If the file already exists, you must use the `read` tool on it first in this conversation, or this call will fail.\n- To modify an existing file, use `edit`: it only sends the diff and preserves the rest of the file byte-for-byte.\n- Use `write` to create a new file or to perform a deliberate full rewrite of a file you have already read.\n- Self-budget content/newStr under 12000 characters before emitting the tool call. For long content, write a smaller opening section, then use `edit` with insertLine: "end" to extend the file section by section. Chaining several `write` calls to one path loses content, because each overwrites the previous.\n- A `.ipynb` body must stay a valid notebook (JSON with cells, nbformat and metadata), or the write is refused.',
|
|
78
79
|
inputSchema,
|
|
79
80
|
modelInputSchema,
|
|
80
81
|
enforceModelInput: true,
|
|
@@ -154,6 +155,12 @@ export const WriteFileTool = defineTool({
|
|
|
154
155
|
}
|
|
155
156
|
const valid = parsed.data
|
|
156
157
|
const content = valid.content ?? valid.newStr ?? ''
|
|
158
|
+
// The sandbox has its own root, so its path is checked as typed. The
|
|
159
|
+
// host path is checked below, once containment has resolved symlinks.
|
|
160
|
+
if (context.sandbox) {
|
|
161
|
+
const notebook = notebookRefusal(valid.path, content)
|
|
162
|
+
if (notebook) return { success: false as const, output: '', error: notebook }
|
|
163
|
+
}
|
|
157
164
|
// Host-side containment, on the host branch only. The sandbox has its
|
|
158
165
|
// own root and its own resolver; canonicalizing a sandbox-relative
|
|
159
166
|
// path against the HOST filesystem asks a question about the wrong
|
|
@@ -161,6 +168,10 @@ export const WriteFileTool = defineTool({
|
|
|
161
168
|
const filePath = context.sandbox
|
|
162
169
|
? undefined
|
|
163
170
|
: await resolveWithinAnyReal(toolRoots(context), valid.path)
|
|
171
|
+
if (filePath !== undefined) {
|
|
172
|
+
const notebook = notebookRefusal(filePath, content)
|
|
173
|
+
if (notebook) return { success: false as const, output: '', error: notebook }
|
|
174
|
+
}
|
|
164
175
|
// The exists-check and the write are a check-then-act pair. Unlocked,
|
|
165
176
|
// two writers both see "absent", both skip the read-before-overwrite
|
|
166
177
|
// guard, and the second silently discards the first.
|
|
@@ -5,7 +5,8 @@ import type { CreateTaskOptions, TaskHandle, TaskScheduler } from '../../types/a
|
|
|
5
5
|
import type { ToolDefinition } from '../../types/tool/index.js'
|
|
6
6
|
import { jsonStringEscapes } from '../builtins/json-string-hint.js'
|
|
7
7
|
import { defineTool } from '../defineTool.js'
|
|
8
|
-
import { wrapUntrusted } from '../untrusted-envelope.js'
|
|
8
|
+
import { KERNEL_RECORD_PREFIX, wrapUntrusted } from '../untrusted-envelope.js'
|
|
9
|
+
import { describeOutcomeRecord } from './outcome-record.js'
|
|
9
10
|
import { failureLabel, taskSucceeded } from './outcome.js'
|
|
10
11
|
|
|
11
12
|
import { DELEGATION_TIMEOUT_MS } from './index.js'
|
|
@@ -169,13 +170,16 @@ export function buildAgentTool(opts: AgentToolOptions): ToolDefinition {
|
|
|
169
170
|
// of every delegation. Merged into ONE `configOverrides`
|
|
170
171
|
// rather than spread twice — the second spread would replace
|
|
171
172
|
// the first and drop the environment.
|
|
172
|
-
...(Object.keys(context.env ?? {}).length > 0 ||
|
|
173
|
+
...(Object.keys(context.env ?? {}).length > 0 ||
|
|
174
|
+
context.toolResultGuardrails ||
|
|
175
|
+
context.fileReadTracker
|
|
173
176
|
? {
|
|
174
177
|
configOverrides: {
|
|
175
178
|
...(Object.keys(context.env ?? {}).length > 0 ? { env: context.env } : {}),
|
|
176
179
|
...(context.toolResultGuardrails
|
|
177
180
|
? { toolResultGuardrails: context.toolResultGuardrails }
|
|
178
181
|
: {}),
|
|
182
|
+
...(context.fileReadTracker ? { fileReadTracker: context.fileReadTracker } : {}),
|
|
179
183
|
},
|
|
180
184
|
}
|
|
181
185
|
: {}),
|
|
@@ -222,19 +226,28 @@ export function buildAgentTool(opts: AgentToolOptions): ToolDefinition {
|
|
|
222
226
|
? JSON.stringify(completed.result.result)
|
|
223
227
|
: ''
|
|
224
228
|
|
|
229
|
+
// The kernel's own record of the child's turn, never the child's words.
|
|
230
|
+
// Written outside the marked block below, and attributed to the record.
|
|
231
|
+
const outcome = completed.result?.outcome
|
|
232
|
+
const recordLine = outcome ? describeOutcomeRecord(outcome) : undefined
|
|
233
|
+
|
|
225
234
|
if (!succeeded) {
|
|
226
|
-
|
|
227
|
-
|
|
235
|
+
// The child's words are not marked on this path, so a line of them
|
|
236
|
+
// that opens like the kernel's own would read as the kernel's.
|
|
237
|
+
const detail = neutraliseKernelLines(
|
|
238
|
+
completed.result?.lastError ?? resultText ?? '(subagent provided no failure detail)',
|
|
239
|
+
)
|
|
228
240
|
return {
|
|
229
241
|
success: false,
|
|
230
242
|
output: '',
|
|
231
|
-
error: `Subagent ${agentId} ${failureLabel(completed)}: ${detail}`,
|
|
243
|
+
error: `Subagent ${agentId} ${failureLabel(completed)}: ${detail}${recordLine ? `\n${recordLine}` : ''}`,
|
|
232
244
|
data: {
|
|
233
245
|
task_id: handle.taskId,
|
|
234
246
|
subagent_type: agentId,
|
|
235
247
|
state: completed.state,
|
|
236
248
|
status: completed.result?.status,
|
|
237
249
|
lastError: completed.result?.lastError,
|
|
250
|
+
...(outcome ? { outcome } : {}),
|
|
238
251
|
},
|
|
239
252
|
}
|
|
240
253
|
}
|
|
@@ -247,20 +260,21 @@ export function buildAgentTool(opts: AgentToolOptions): ToolDefinition {
|
|
|
247
260
|
// the result programmatically.
|
|
248
261
|
return {
|
|
249
262
|
success: true,
|
|
250
|
-
output: wrapUntrusted(
|
|
263
|
+
output: `${wrapUntrusted(
|
|
251
264
|
{
|
|
252
265
|
kind: 'agent-result',
|
|
253
266
|
attributes: { agent: agentId, task: handle.taskId },
|
|
254
267
|
provenance: `This is the output of the delegated subagent "${agentId}", not this agent's own work.`,
|
|
255
268
|
},
|
|
256
269
|
resultText || '(subagent returned no text)',
|
|
257
|
-
)
|
|
270
|
+
)}${recordLine ? `\n\n${recordLine}` : ''}`,
|
|
258
271
|
data: {
|
|
259
272
|
task_id: handle.taskId,
|
|
260
273
|
subagent_type: agentId,
|
|
261
274
|
result: resultText,
|
|
262
275
|
state: completed.state,
|
|
263
276
|
status: completed.result?.status,
|
|
277
|
+
...(outcome ? { outcome } : {}),
|
|
264
278
|
},
|
|
265
279
|
}
|
|
266
280
|
},
|
|
@@ -339,3 +353,13 @@ async function runBlockingAgentTask(
|
|
|
339
353
|
signal.removeEventListener('abort', onAbort)
|
|
340
354
|
}
|
|
341
355
|
}
|
|
356
|
+
|
|
357
|
+
/** `text` with any line that opens with the kernel record prefix quoted, so it cannot pass for the kernel's line. */
|
|
358
|
+
function neutraliseKernelLines(text: string): string {
|
|
359
|
+
return text
|
|
360
|
+
.split('\n')
|
|
361
|
+
.map((line) =>
|
|
362
|
+
line.trimStart().startsWith(KERNEL_RECORD_PREFIX) ? `> ${line.trimStart()}` : line,
|
|
363
|
+
)
|
|
364
|
+
.join('\n')
|
|
365
|
+
}
|
|
@@ -679,7 +679,9 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
679
679
|
// turned them off had that decision revert behind every
|
|
680
680
|
// delegation. One merged `configOverrides`, because a second
|
|
681
681
|
// spread of the key would replace this one.
|
|
682
|
-
...(Object.keys(_context.env ?? {}).length > 0 ||
|
|
682
|
+
...(Object.keys(_context.env ?? {}).length > 0 ||
|
|
683
|
+
_context.toolResultGuardrails ||
|
|
684
|
+
_context.fileReadTracker
|
|
683
685
|
? {
|
|
684
686
|
configOverrides: {
|
|
685
687
|
...(_context.env && Object.keys(_context.env).length > 0
|
|
@@ -688,6 +690,9 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
688
690
|
...(_context.toolResultGuardrails
|
|
689
691
|
? { toolResultGuardrails: _context.toolResultGuardrails }
|
|
690
692
|
: {}),
|
|
693
|
+
...(_context.fileReadTracker
|
|
694
|
+
? { fileReadTracker: _context.fileReadTracker }
|
|
695
|
+
: {}),
|
|
691
696
|
},
|
|
692
697
|
}
|
|
693
698
|
: {}),
|