@namzu/sdk 62.6.0 → 62.7.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 +35 -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 +57 -3
- 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/executor.d.ts +38 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +130 -11
- 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 +13 -4
- 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/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.map +1 -1
- package/dist/sandbox/file-walk.js +34 -17
- package/dist/sandbox/file-walk.js.map +1 -1
- package/dist/sandbox/glob-segment.d.ts +35 -0
- package/dist/sandbox/glob-segment.d.ts.map +1 -0
- package/dist/sandbox/glob-segment.js +300 -0
- package/dist/sandbox/glob-segment.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/builtins/bash.js +1 -1
- package/dist/tools/builtins/bash.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.js +1 -1
- 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/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/package.json +2 -2
- 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 +62 -3
- 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/executor.ts +159 -19
- 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 +13 -4
- package/src/runtime/query/review-policy.ts +33 -0
- 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 +38 -17
- package/src/sandbox/glob-segment.ts +309 -0
- package/src/store/session-log/index.ts +7 -0
- package/src/store/session-log/turn-outcome.ts +84 -0
- package/src/tools/builtins/bash.ts +1 -1
- 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 +1 -1
- package/src/tools/builtins/notebook-guard.ts +76 -0
- package/src/tools/builtins/read-render.ts +12 -2
- 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
|
@@ -22,8 +22,91 @@ import {
|
|
|
22
22
|
import { withFileMutationLock } from './file-mutation-lock.js'
|
|
23
23
|
import { jsonStringEscapes } from './json-string-hint.js'
|
|
24
24
|
import { lineDiff } from './line-diff.js'
|
|
25
|
+
import { notebookRefusal } from './notebook-guard.js'
|
|
25
26
|
import { staleBeforeReview } from './stale-before-review.js'
|
|
26
27
|
|
|
28
|
+
type RawEditInput = {
|
|
29
|
+
path: string
|
|
30
|
+
old_string?: string
|
|
31
|
+
oldStr?: string
|
|
32
|
+
new_string?: string
|
|
33
|
+
newStr?: string
|
|
34
|
+
insertLine?: number | string
|
|
35
|
+
replace_all?: boolean
|
|
36
|
+
edits?: { old_string: string; new_string: string; replace_all?: boolean }[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const ONE_SHAPE_EACH =
|
|
40
|
+
'Single edit: {"path":"file.md","old_string":"exact unique text","new_string":"replacement text"}\nSeveral edits: {"path":"file.md","edits":[{"old_string":"a","new_string":"b"},{"old_string":"c","new_string":"d"}]}'
|
|
41
|
+
|
|
42
|
+
const hasTopLevelEdit = (value: RawEditInput): boolean =>
|
|
43
|
+
value.old_string !== undefined ||
|
|
44
|
+
value.oldStr !== undefined ||
|
|
45
|
+
value.new_string !== undefined ||
|
|
46
|
+
value.newStr !== undefined ||
|
|
47
|
+
value.insertLine !== undefined
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A top-level edit that is exactly the one entry of a one-entry batch: the same
|
|
51
|
+
* call said twice, not two intentions. Anything else (a different text, a
|
|
52
|
+
* longer list, an insertion) is a real conflict and stays refused.
|
|
53
|
+
*/
|
|
54
|
+
function restatesTheBatch(value: RawEditInput): boolean {
|
|
55
|
+
const entries = value.edits
|
|
56
|
+
if (entries === undefined || entries.length !== 1 || value.insertLine !== undefined) return false
|
|
57
|
+
const [entry] = entries
|
|
58
|
+
return (
|
|
59
|
+
entry !== undefined &&
|
|
60
|
+
(value.old_string ?? value.oldStr) === entry.old_string &&
|
|
61
|
+
(value.new_string ?? value.newStr) === entry.new_string &&
|
|
62
|
+
(value.replace_all ?? false) === (entry.replace_all ?? false)
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Which of the two shapes this call is, and what to say when it is neither.
|
|
68
|
+
*
|
|
69
|
+
* An empty `edits` beside a top-level edit, and a one-entry `edits` that restates
|
|
70
|
+
* the top-level edit, are the single edit: the intent is not in doubt and nothing
|
|
71
|
+
* is dropped. A batch beside a DIFFERENT top-level edit stays refused, because
|
|
72
|
+
* any precedence would silently drop an edit somebody believes was made.
|
|
73
|
+
*/
|
|
74
|
+
function checkEditShape(value: RawEditInput): string | undefined {
|
|
75
|
+
const batch = value.edits !== undefined && value.edits.length > 0
|
|
76
|
+
const top = hasTopLevelEdit(value)
|
|
77
|
+
if (value.edits !== undefined && !batch && !top) {
|
|
78
|
+
return `\`edits\` is an empty list, so there is nothing to change. Send exactly one of these, and leave out the field you are not using:\n${ONE_SHAPE_EACH}`
|
|
79
|
+
}
|
|
80
|
+
if (batch && top && !restatesTheBatch(value)) {
|
|
81
|
+
return `This call has both an \`edits\` list and a different top-level edit, and only one can be applied. Send exactly one of these, and leave out the field you are not using:\n${ONE_SHAPE_EACH}`
|
|
82
|
+
}
|
|
83
|
+
if (batch) return undefined
|
|
84
|
+
if (typeof value.new_string !== 'string' && typeof value.newStr !== 'string') {
|
|
85
|
+
return 'Either new_string or newStr is required.'
|
|
86
|
+
}
|
|
87
|
+
if (
|
|
88
|
+
value.insertLine === undefined &&
|
|
89
|
+
typeof value.old_string !== 'string' &&
|
|
90
|
+
typeof value.oldStr !== 'string'
|
|
91
|
+
) {
|
|
92
|
+
return 'Either old_string/oldStr or insertLine is required.'
|
|
93
|
+
}
|
|
94
|
+
return undefined
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The input every later stage sees: one shape, with the redundant half gone. The
|
|
99
|
+
* review card, the classifier, the dry run and the apply step all read this, so
|
|
100
|
+
* none of them meets `edits: []` or a restated edit.
|
|
101
|
+
*/
|
|
102
|
+
function canonicalEditInput(value: RawEditInput): RawEditInput {
|
|
103
|
+
if (value.edits === undefined) return value
|
|
104
|
+
const { edits, ...rest } = value
|
|
105
|
+
if (edits.length === 0) return rest
|
|
106
|
+
const { old_string, oldStr, new_string, newStr, insertLine, ...kept } = rest
|
|
107
|
+
return { ...kept, edits }
|
|
108
|
+
}
|
|
109
|
+
|
|
27
110
|
/**
|
|
28
111
|
* Two schemas, on purpose.
|
|
29
112
|
*
|
|
@@ -106,48 +189,23 @@ const inputSchema = z
|
|
|
106
189
|
})
|
|
107
190
|
.strict(),
|
|
108
191
|
)
|
|
109
|
-
|
|
192
|
+
// No `.min(1)`: a model that fills every optional field of a flat object
|
|
193
|
+
// sends `edits: []` next to a single top-level edit, and that is the
|
|
194
|
+
// single edit, not an error. An empty list with nothing beside it is
|
|
195
|
+
// refused by `checkEditShape` below, in a sentence that says what to send.
|
|
110
196
|
.optional()
|
|
111
197
|
.describe(
|
|
112
|
-
'Several replacements in one file, applied in order and committed together. Nothing is written unless every one applies.',
|
|
198
|
+
'Several replacements in one file, applied in order and committed together. Nothing is written unless every one applies. Leave out when sending a single old_string/new_string.',
|
|
113
199
|
),
|
|
114
200
|
})
|
|
115
201
|
.strict()
|
|
116
|
-
.
|
|
117
|
-
(value)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
{ message: 'Either new_string or newStr is required.' },
|
|
122
|
-
)
|
|
123
|
-
.refine(
|
|
124
|
-
(value) =>
|
|
125
|
-
value.edits !== undefined ||
|
|
126
|
-
value.insertLine !== undefined ||
|
|
127
|
-
typeof value.old_string === 'string' ||
|
|
128
|
-
typeof value.oldStr === 'string',
|
|
129
|
-
{ message: 'Either old_string/oldStr or insertLine is required.' },
|
|
130
|
-
)
|
|
131
|
-
// Refused rather than resolved. A call carrying both an `edits` list and a
|
|
132
|
-
// top-level replacement is two different intentions in one object, and any
|
|
133
|
-
// precedence this code picked would be a guess about which the model meant
|
|
134
|
-
// — silently dropping the other. There is no reading of that which is safe:
|
|
135
|
-
// the dropped half is an edit somebody believes was made.
|
|
136
|
-
.refine(
|
|
137
|
-
(value) =>
|
|
138
|
-
value.edits === undefined ||
|
|
139
|
-
(value.old_string === undefined &&
|
|
140
|
-
value.oldStr === undefined &&
|
|
141
|
-
value.new_string === undefined &&
|
|
142
|
-
value.newStr === undefined &&
|
|
143
|
-
value.insertLine === undefined),
|
|
144
|
-
{
|
|
145
|
-
message:
|
|
146
|
-
'Use either `edits` or a single top-level edit, not both. Move the top-level old_string/new_string into the edits list.',
|
|
147
|
-
},
|
|
148
|
-
)
|
|
202
|
+
.superRefine((value, ctx) => {
|
|
203
|
+
const problem = checkEditShape(value)
|
|
204
|
+
if (problem) ctx.addIssue({ code: 'custom', message: problem })
|
|
205
|
+
})
|
|
206
|
+
.transform(canonicalEditInput)
|
|
149
207
|
|
|
150
|
-
export type EditInput = z.
|
|
208
|
+
export type EditInput = z.output<typeof inputSchema>
|
|
151
209
|
|
|
152
210
|
/**
|
|
153
211
|
* What an `edit` call WOULD produce for a file body, without touching any file.
|
|
@@ -184,7 +242,10 @@ export function dryRunEdit(
|
|
|
184
242
|
}
|
|
185
243
|
}
|
|
186
244
|
const result = applyEdit(content, normalized.operations)
|
|
187
|
-
|
|
245
|
+
if (!result.success) return result
|
|
246
|
+
const notebook = notebookRefusal(parsed.data.path, result.content, content)
|
|
247
|
+
if (notebook) return { success: false, error: notebook }
|
|
248
|
+
return { success: true, content: result.content }
|
|
188
249
|
}
|
|
189
250
|
|
|
190
251
|
/**
|
|
@@ -205,7 +266,7 @@ const modelInputSchema: Record<string, unknown> = {
|
|
|
205
266
|
old_string: {
|
|
206
267
|
type: 'string',
|
|
207
268
|
description:
|
|
208
|
-
'
|
|
269
|
+
'For a single change: exact unique text from the file, without read-tool line-number prefixes. Must not be empty. Do not send together with edits.',
|
|
209
270
|
},
|
|
210
271
|
new_string: {
|
|
211
272
|
type: 'string',
|
|
@@ -282,7 +343,7 @@ const modelInputSchema: Record<string, unknown> = {
|
|
|
282
343
|
additionalProperties: false,
|
|
283
344
|
},
|
|
284
345
|
description:
|
|
285
|
-
'
|
|
346
|
+
'For SEVERAL changes to one file: a non-empty list, applied in order and committed together. Nothing is written unless every one applies. Leave this field out entirely when you send old_string/new_string; never send an empty list.',
|
|
286
347
|
},
|
|
287
348
|
},
|
|
288
349
|
// `old_string` is deliberately NOT required, and this is the fix.
|
|
@@ -437,7 +498,7 @@ function refusedEdit(result: EditApplyFailure, path: string) {
|
|
|
437
498
|
export const EditTool = defineTool({
|
|
438
499
|
name: 'edit',
|
|
439
500
|
description:
|
|
440
|
-
'Makes targeted edits to a file using exact string find-and-replace or line insertion. To change an existing file, use `edit`: `write` overwrites the whole body and discards earlier work on partial failure, while `edit` keeps the rest of the file byte-for-byte intact and is recoverable: if a single edit fails (old_string/oldStr ambiguous, broader restructuring needed), follow up with another `edit` instead of re-emitting the entire file via `write`. The old_string/oldStr must be unique in the file unless replace_all is true. For insertions, pass insertLine plus new_string/newStr; use insertLine: "end" to extend a file at the end. Keep new_string/newStr under 12000 characters, because a longer payload can be cut off mid-call and fail; use repeated bounded edits for long sections. Preserves file formatting and indentation. To make several changes to one file that only make sense together, send them as `edits
|
|
501
|
+
'Makes targeted edits to a file using exact string find-and-replace or line insertion. To change an existing file, use `edit`: `write` overwrites the whole body and discards earlier work on partial failure, while `edit` keeps the rest of the file byte-for-byte intact and is recoverable: if a single edit fails (old_string/oldStr ambiguous, broader restructuring needed), follow up with another `edit` instead of re-emitting the entire file via `write`. The old_string/oldStr must be unique in the file unless replace_all is true. For insertions, pass insertLine plus new_string/newStr; use insertLine: "end" to extend a file at the end. Keep new_string/newStr under 12000 characters, because a longer payload can be cut off mid-call and fail; use repeated bounded edits for long sections. Preserves file formatting and indentation. To make several changes to one file that only make sense together, send them as `edits` (and then no top-level old_string/new_string; for a single change leave `edits` out): a list of {old_string, new_string} applied in order and committed as one write — if any of them does not apply, nothing is written and the reply names which one, so the file is never left half-changed. An edit that would leave a `.ipynb` file as invalid notebook JSON is refused and the file is unchanged.',
|
|
441
502
|
inputSchema,
|
|
442
503
|
modelInputSchema,
|
|
443
504
|
enforceModelInput: true,
|
|
@@ -458,7 +519,7 @@ export const EditTool = defineTool({
|
|
|
458
519
|
refuseBeforeReview: (input: EditInput, context) => staleBeforeReview(context, input.path, 'edit'),
|
|
459
520
|
// The classifier judges which file changes and in what way, in a line.
|
|
460
521
|
classifierInput: (input: EditInput) => {
|
|
461
|
-
if (input.edits !== undefined) {
|
|
522
|
+
if (input.edits !== undefined && input.edits.length > 0) {
|
|
462
523
|
const shown = input.edits.slice(0, 5).map((e) => describeChange(e.old_string, e.new_string))
|
|
463
524
|
const more =
|
|
464
525
|
input.edits.length > shown.length ? `; ${input.edits.length - shown.length} more` : ''
|
|
@@ -570,6 +631,8 @@ export const EditTool = defineTool({
|
|
|
570
631
|
if (!admitted.success) return admitted
|
|
571
632
|
const result = admitted.result
|
|
572
633
|
if (!result.success) return refusedEdit(result, parsed.data.path)
|
|
634
|
+
const notebook = notebookRefusal(parsed.data.path, result.content, content)
|
|
635
|
+
if (notebook) return { success: false as const, output: '', error: notebook }
|
|
573
636
|
|
|
574
637
|
await context.sandbox.writeFile(parsed.data.path, result.content)
|
|
575
638
|
recordEditedContent(context, parsed.data.path, result.content)
|
|
@@ -593,6 +656,8 @@ export const EditTool = defineTool({
|
|
|
593
656
|
if (!admitted.success) return admitted
|
|
594
657
|
const result = admitted.result
|
|
595
658
|
if (!result.success) return refusedEdit(result, hostPath)
|
|
659
|
+
const notebook = notebookRefusal(hostPath, result.content, content)
|
|
660
|
+
if (notebook) return { success: false as const, output: '', error: notebook }
|
|
596
661
|
|
|
597
662
|
// Temp file, fsync, rename — a reader sees the old body or the new
|
|
598
663
|
// one, never a half-written one. A plain `writeFile` that fails
|
|
@@ -68,7 +68,7 @@ function relativePattern(pattern: string, root: string, sandboxed: boolean): str
|
|
|
68
68
|
export const GlobTool = defineTool({
|
|
69
69
|
name: 'glob',
|
|
70
70
|
description:
|
|
71
|
-
'Finds regular files by glob pattern in a bounded search. "*" searches one directory; use "**" explicitly for recursive discovery. Skips files that .gitignore and .ignore rules exclude unless include_ignored is set, and files the read rules of the session deny. Returns at most 500 paths and says when a result is limited or a search stopped early. Does not follow symlinks.',
|
|
71
|
+
'Finds regular files by glob pattern in a bounded search. "*" searches one directory; use "**" explicitly for recursive discovery. Skips files that .gitignore and .ignore rules exclude unless include_ignored is set, and files the read rules of the session deny. Returns at most 500 paths and says when a result is limited or a search stopped early. Does not follow symlinks. To search inside files, use grep.',
|
|
72
72
|
inputSchema,
|
|
73
73
|
category: 'filesystem',
|
|
74
74
|
// Declared so a path outside the turn's roots can be reviewed before the
|
|
@@ -211,7 +211,7 @@ function isBinaryContent(buffer: Buffer): boolean {
|
|
|
211
211
|
export const GrepTool = defineTool({
|
|
212
212
|
name: 'grep',
|
|
213
213
|
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.',
|
|
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. To find files by name, use glob.',
|
|
215
215
|
inputSchema,
|
|
216
216
|
category: 'analysis',
|
|
217
217
|
// Declared so a path outside the turn's roots can be reviewed before the
|
|
@@ -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
|
'',
|
|
@@ -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
|
: {}),
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { TurnOutcome, TurnOutcomeCheck } from '../../types/session/outcome.js'
|
|
2
|
+
import { KERNEL_RECORD_PREFIX } from '../untrusted-envelope.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The kernel's own sentence about what a delegated run did, for the parent
|
|
6
|
+
* that delegated it.
|
|
7
|
+
*
|
|
8
|
+
* It states the record, not the child's account of itself, and it sits outside
|
|
9
|
+
* the marked block that holds the child's words: the parent reads it as a fact
|
|
10
|
+
* of the kernel's, the way it reads the task id. What it can say is exactly
|
|
11
|
+
* what the record can say. `checked` means a recognised check passed and the
|
|
12
|
+
* workspace did not change after it; it does not mean the check covers the
|
|
13
|
+
* change.
|
|
14
|
+
*
|
|
15
|
+
* A run that changed nothing and failed no check gets no line.
|
|
16
|
+
*
|
|
17
|
+
* The command is the child's own text, so it is shown as plain data: one line
|
|
18
|
+
* of printable ASCII with no quotes or backticks, cut short. A command that
|
|
19
|
+
* does not survive that is named as "a command" instead.
|
|
20
|
+
*/
|
|
21
|
+
export function describeOutcomeRecord(outcome: TurnOutcome): string | undefined {
|
|
22
|
+
const body = describe(outcome)
|
|
23
|
+
return body === undefined ? undefined : `${KERNEL_RECORD_PREFIX}${body}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SHOWN_COMMAND_CHARS = 60
|
|
27
|
+
|
|
28
|
+
/** A command as data inside backticks, or `undefined` when it cannot be shown without being rewritten. */
|
|
29
|
+
export function showCommand(command: string): string | undefined {
|
|
30
|
+
const trimmed = command.trim()
|
|
31
|
+
if (trimmed === '' || trimmed.length > SHOWN_COMMAND_CHARS) return undefined
|
|
32
|
+
// Printable ASCII, and none of the characters that open a quotation or a
|
|
33
|
+
// substitution in whatever reads this next.
|
|
34
|
+
if (!/^[\x20-\x7e]+$/.test(trimmed) || /[`'"$\\#]/.test(trimmed)) return undefined
|
|
35
|
+
return `\`${trimmed}\``
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function named(check: TurnOutcomeCheck): string {
|
|
39
|
+
return showCommand(check.command) ?? 'a command'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function changedPhrase(outcome: TurnOutcome): string | undefined {
|
|
43
|
+
const { total, outsideKnownTools } = outcome.changes
|
|
44
|
+
if (total > 0) return total === 1 ? '1 file changed' : `${total} files changed`
|
|
45
|
+
if (outsideKnownTools) return 'files changed, by something other than the edit tools'
|
|
46
|
+
return undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function describe(outcome: TurnOutcome): string | undefined {
|
|
50
|
+
const changed = changedPhrase(outcome)
|
|
51
|
+
const failed = outcome.checks.filter((check) => check.result === 'failed')
|
|
52
|
+
const lead = changed === undefined ? '' : `${changed}; `
|
|
53
|
+
|
|
54
|
+
switch (outcome.verdict) {
|
|
55
|
+
case 'unchanged':
|
|
56
|
+
// Nothing to say, and a line on every delegation that only read would
|
|
57
|
+
// be noise the parent learns to skip. The record is still in `data`.
|
|
58
|
+
return undefined
|
|
59
|
+
case 'unknown':
|
|
60
|
+
return 'a command ran that may have changed files, and nothing here could tell whether it did.'
|
|
61
|
+
case 'unchecked':
|
|
62
|
+
return `${lead}nothing was run to check ${total(outcome) === 1 ? 'it' : 'them'}.`
|
|
63
|
+
case 'failing': {
|
|
64
|
+
const first = failed[0]
|
|
65
|
+
if (first === undefined) return undefined
|
|
66
|
+
const more = failed.length > 1 ? ` (and ${failed.length - 1} more)` : ''
|
|
67
|
+
return `${lead}${named(first)} failed${more}.`
|
|
68
|
+
}
|
|
69
|
+
case 'checked': {
|
|
70
|
+
const passed = [...outcome.checks].reverse().find((c) => c.result === 'passed' && c.fresh)
|
|
71
|
+
return `${lead}${passed ? named(passed) : 'a check'} passed and nothing changed after it.`
|
|
72
|
+
}
|
|
73
|
+
case 'stale': {
|
|
74
|
+
const passed = [...outcome.checks].reverse().find((c) => c.result === 'passed')
|
|
75
|
+
return `${lead}${passed ? named(passed) : 'a check'} passed, then files changed again.`
|
|
76
|
+
}
|
|
77
|
+
case 'inconclusive': {
|
|
78
|
+
const masked = outcome.checks.find((c) => c.result === 'masked')
|
|
79
|
+
if (masked) {
|
|
80
|
+
const how = /(?<!\|)\|(?!\|)/.test(masked.command) ? 'the pipe' : 'the command line'
|
|
81
|
+
return `${lead}${named(masked)} ran, but ${how} hides whether it passed.`
|
|
82
|
+
}
|
|
83
|
+
const unsettled = outcome.checks.find((c) => c.result === 'unsettled')
|
|
84
|
+
if (unsettled) return `${lead}${named(unsettled)} ran, but its result was not seen.`
|
|
85
|
+
const passed = outcome.checks.find((c) => c.result === 'passed')
|
|
86
|
+
return `${lead}${passed ? named(passed) : 'a check'} passed, but it cannot be told whether files changed after it.`
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function total(outcome: TurnOutcome): number {
|
|
92
|
+
return outcome.changes.total
|
|
93
|
+
}
|
package/src/tools/defineTool.ts
CHANGED
|
@@ -41,6 +41,8 @@ export interface DefineToolOptions<S extends z.ZodType> {
|
|
|
41
41
|
executionBarrier?: boolean
|
|
42
42
|
/** The tool bounds its own output; see {@link ToolDefinition.selfLimitingOutput}. */
|
|
43
43
|
selfLimitingOutput?: boolean
|
|
44
|
+
/** A tighter per-result cap; see {@link ToolDefinition.maxOutputChars}. */
|
|
45
|
+
maxOutputChars?: number
|
|
44
46
|
tier?: string
|
|
45
47
|
/**
|
|
46
48
|
* How this tool's call and result should be shown; see
|
|
@@ -163,6 +165,7 @@ export function defineTool<S extends z.ZodType>(
|
|
|
163
165
|
...(options.selfLimitingOutput !== undefined
|
|
164
166
|
? { selfLimitingOutput: options.selfLimitingOutput }
|
|
165
167
|
: {}),
|
|
168
|
+
...(options.maxOutputChars !== undefined ? { maxOutputChars: options.maxOutputChars } : {}),
|
|
166
169
|
category: options.category,
|
|
167
170
|
permissions: options.permissions,
|
|
168
171
|
isReadOnly:
|