dsh-ptc-plus 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -0
  3. package/README.zh.md +137 -0
  4. package/assets/dsh-ptc-plus-banner.webp +0 -0
  5. package/assets/ptc-plus-repair.png +0 -0
  6. package/assets/ptc-plus-settings.png +0 -0
  7. package/client.js +366 -0
  8. package/cordis.patch.yml +5 -0
  9. package/docs/README.md +21 -0
  10. package/docs/adr/0001-delegate-governance-to-dsh.md +3 -0
  11. package/docs/adr/0002-limit-work-map-scope.md +3 -0
  12. package/docs/adr/0003-prefer-native-program-surfaces.md +3 -0
  13. package/docs/adr/0004-declare-program-bindings-at-the-host.md +5 -0
  14. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +129 -0
  15. package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +45 -0
  16. package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +39 -0
  17. package/docs/adr/0008-define-agent-operating-contract.md +25 -0
  18. package/docs/adr/0009-bound-expensive-acceptance-default.md +39 -0
  19. package/docs/adr/0010-session-log-derived-recovery-tips.md +26 -0
  20. package/docs/adr/0011-isolate-the-local-recovery-tip-provider.md +20 -0
  21. package/docs/adr/0012-preserve-mixed-destructuring-initialization.md +23 -0
  22. package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +21 -0
  23. package/docs/adr/0014-persist-cell-rewrite-policy.md +25 -0
  24. package/docs/adr/0015-preserve-source-positions-through-rewrites.md +25 -0
  25. package/docs/adr/0016-archive-resolved-review-findings.md +43 -0
  26. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +23 -0
  27. package/docs/adr/0018-use-versioned-fixture-for-ab-benchmark.md +25 -0
  28. package/docs/adr/0019-plugin-settings-and-kill-switch.md +32 -0
  29. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +36 -0
  30. package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +68 -0
  31. package/docs/adr/README.md +18 -0
  32. package/docs/architecture.md +134 -0
  33. package/docs/capability-projection.md +48 -0
  34. package/docs/client-ui.md +47 -0
  35. package/docs/durability-design.md +265 -0
  36. package/docs/evaluation.md +62 -0
  37. package/docs/installation.md +82 -0
  38. package/docs/program-data-plane.md +61 -0
  39. package/docs/publishing.md +37 -0
  40. package/docs/runtime-reference.md +119 -0
  41. package/docs/value-wire.md +61 -0
  42. package/index.js +532 -0
  43. package/internal/ast-traversal.js +20 -0
  44. package/internal/binding-descriptors.js +95 -0
  45. package/internal/binding-pattern.js +54 -0
  46. package/internal/builtin-esm-sync.js +9 -0
  47. package/internal/cell-analysis.js +467 -0
  48. package/internal/cell-rewriter.js +520 -0
  49. package/internal/config-spec.js +179 -0
  50. package/internal/cordis-tools-owner.js +363 -0
  51. package/internal/diagnostic.js +133 -0
  52. package/internal/direct-surface-owner.js +326 -0
  53. package/internal/edit-transport-owner.js +287 -0
  54. package/internal/failure-reporting.js +139 -0
  55. package/internal/kernel-worker.js +484 -0
  56. package/internal/module-policy.js +46 -0
  57. package/internal/program-bindings.js +187 -0
  58. package/internal/record-utils.js +53 -0
  59. package/internal/recovery-tips.js +81 -0
  60. package/internal/rejected-cell-editor.js +328 -0
  61. package/internal/repl-convenience.js +201 -0
  62. package/internal/runtime-bridge-owner.js +417 -0
  63. package/internal/runtime-config.js +77 -0
  64. package/internal/runtime-contexts.js +31 -0
  65. package/internal/sdk-projection.js +88 -0
  66. package/internal/session-cell-executor.js +612 -0
  67. package/internal/session-journal.js +889 -0
  68. package/internal/session-log-view.js +80 -0
  69. package/internal/session-runtime.js +536 -0
  70. package/internal/session-state.js +45 -0
  71. package/internal/source-position-map.js +224 -0
  72. package/internal/tool-call-canonicalizer.js +339 -0
  73. package/internal/value-wire.js +421 -0
  74. package/internal/worker-client.js +218 -0
  75. package/internal/worker-cwd-virtualization.js +155 -0
  76. package/package.json +100 -0
  77. package/src/client.js +165 -0
@@ -0,0 +1,224 @@
1
+ export function identitySourceMap(length) {
2
+ return [{ generatedStart: 0, generatedEnd: length, originalStart: 0, originalEnd: length }]
3
+ }
4
+
5
+ /** Build generated text while making every copied source range explicit. */
6
+ export function createMappedTextBuilder(source, sourceOffset = 0) {
7
+ if (typeof source !== 'string' || !Number.isSafeInteger(sourceOffset) || sourceOffset < 0) {
8
+ throw new TypeError('mapped text source and offset must be valid')
9
+ }
10
+ let text = ''
11
+ const mappings = []
12
+ const append = (value) => {
13
+ if (typeof value !== 'string') throw new TypeError('mapped text must be a string')
14
+ text += value
15
+ }
16
+ const appendMapped = (value, originalStart, originalEnd) => {
17
+ if (typeof value !== 'string' || !Number.isSafeInteger(originalStart)
18
+ || !Number.isSafeInteger(originalEnd) || originalStart < sourceOffset
19
+ || originalEnd < originalStart || originalEnd > sourceOffset + source.length) {
20
+ throw new RangeError('mapped text range must be bounded by its source')
21
+ }
22
+ const generatedStart = text.length
23
+ text += value
24
+ if (value.length > 0) {
25
+ mappings.push({
26
+ generatedStart,
27
+ generatedEnd: text.length,
28
+ originalStart: originalStart - sourceOffset,
29
+ originalEnd: originalEnd - sourceOffset,
30
+ })
31
+ }
32
+ }
33
+ return {
34
+ append,
35
+ appendMapped,
36
+ appendSource(start, end) {
37
+ appendMapped(source.slice(start - sourceOffset, end - sourceOffset), start, end)
38
+ },
39
+ result() {
40
+ return { text, mappings }
41
+ },
42
+ }
43
+ }
44
+
45
+ function originalOffsetAt(sourceMap, generatedOffset) {
46
+ let low = 0
47
+ let high = sourceMap.length
48
+ while (low < high) {
49
+ const middle = (low + high) >>> 1
50
+ if (generatedOffset < sourceMap[middle].generatedEnd) high = middle
51
+ else low = middle + 1
52
+ }
53
+ const segment = sourceMap[low] ?? sourceMap.at(-1)
54
+ if (segment === undefined) return generatedOffset
55
+ const generatedLength = segment.generatedEnd - segment.generatedStart
56
+ const originalLength = segment.originalEnd - segment.originalStart
57
+ const localOffset = Math.max(0, generatedOffset - segment.generatedStart)
58
+ if (localOffset >= generatedLength) return segment.originalEnd
59
+ if (generatedLength === originalLength) return segment.originalStart + localOffset
60
+ if (generatedLength <= 1 || originalLength <= 1) return segment.originalStart
61
+ return segment.originalStart
62
+ + Math.floor((localOffset * (originalLength - 1)) / (generatedLength - 1))
63
+ }
64
+
65
+ function mappedOffsetRange(sourceMap, start, end) {
66
+ return {
67
+ originalStart: originalOffsetAt(sourceMap, start),
68
+ originalEnd: originalOffsetAt(sourceMap, Math.max(start, end - 1)) + (end > start ? 1 : 0),
69
+ }
70
+ }
71
+
72
+ function replacementSegments(sourceMap, start, end, replacementLength, mappings) {
73
+ const { originalStart: startOriginal, originalEnd: endOriginal } = mappedOffsetRange(sourceMap, start, end)
74
+ if (!Array.isArray(mappings) || mappings.length === 0) {
75
+ return replacementLength === 0 ? [] : [{
76
+ generatedStart: start,
77
+ generatedEnd: start + replacementLength,
78
+ originalStart: startOriginal,
79
+ originalEnd: endOriginal,
80
+ }]
81
+ }
82
+ const segments = []
83
+ let cursor = 0
84
+ for (const mapping of mappings) {
85
+ if (mapping.generatedStart > cursor) {
86
+ segments.push({
87
+ generatedStart: start + cursor,
88
+ generatedEnd: start + mapping.generatedStart,
89
+ originalStart: startOriginal,
90
+ originalEnd: endOriginal,
91
+ })
92
+ }
93
+ const original = mappedOffsetRange(sourceMap, mapping.originalStart, mapping.originalEnd)
94
+ segments.push({
95
+ generatedStart: start + mapping.generatedStart,
96
+ generatedEnd: start + mapping.generatedEnd,
97
+ ...original,
98
+ })
99
+ cursor = mapping.generatedEnd
100
+ }
101
+ if (cursor < replacementLength) {
102
+ segments.push({
103
+ generatedStart: start + cursor,
104
+ generatedEnd: start + replacementLength,
105
+ originalStart: startOriginal,
106
+ originalEnd: endOriginal,
107
+ })
108
+ }
109
+ return segments
110
+ }
111
+
112
+ export function applySourceEdits(code, sourceMap, edits) {
113
+ if (!Array.isArray(edits) || edits.length === 0) return { code, sourceMap }
114
+ const ordered = [...edits].sort((left, right) => left.start - right.start || left.end - right.end)
115
+ const normalized = []
116
+ let previousEnd = 0
117
+ let previous
118
+ for (const item of ordered) {
119
+ const text = item.text ?? ''
120
+ if (!Number.isSafeInteger(item.start) || !Number.isSafeInteger(item.end)
121
+ || item.start < 0 || item.end < item.start || item.end > code.length
122
+ || item.start < previousEnd || typeof text !== 'string'
123
+ || (item.mappings !== undefined && !Array.isArray(item.mappings))
124
+ || (item.start === item.end && previous?.start === item.start && previous.end === item.end)) {
125
+ throw new RangeError('source edits must be bounded, ordered, and non-overlapping')
126
+ }
127
+ const itemMappings = [...(item.mappings ?? [])]
128
+ .sort((left, right) => left.generatedStart - right.generatedStart)
129
+ let mappingEnd = 0
130
+ for (const mapping of itemMappings) {
131
+ if (!Number.isSafeInteger(mapping.generatedStart) || !Number.isSafeInteger(mapping.generatedEnd)
132
+ || !Number.isSafeInteger(mapping.originalStart) || !Number.isSafeInteger(mapping.originalEnd)
133
+ || mapping.generatedStart < mappingEnd || mapping.generatedEnd < mapping.generatedStart
134
+ || mapping.generatedEnd > text.length || mapping.originalStart < 0
135
+ || mapping.originalEnd < mapping.originalStart || mapping.originalEnd > code.length) {
136
+ throw new RangeError('source edit mappings must be bounded and non-overlapping')
137
+ }
138
+ mappingEnd = mapping.generatedEnd
139
+ }
140
+ normalized.push({ ...item, text, mappings: itemMappings })
141
+ previousEnd = item.end
142
+ previous = item
143
+ }
144
+
145
+ const chunks = []
146
+ const mappings = []
147
+ let generatedOffset = 0
148
+ let sourceOffset = 0
149
+ let segmentIndex = 0
150
+ const appendUnchanged = end => {
151
+ if (end <= sourceOffset) return
152
+ chunks.push(code.slice(sourceOffset, end))
153
+ while (segmentIndex < sourceMap.length && sourceMap[segmentIndex].generatedEnd <= sourceOffset) segmentIndex += 1
154
+ for (let index = segmentIndex; index < sourceMap.length; index += 1) {
155
+ const segment = sourceMap[index]
156
+ if (segment.generatedStart >= end) break
157
+ const start = Math.max(sourceOffset, segment.generatedStart)
158
+ const stop = Math.min(end, segment.generatedEnd)
159
+ if (stop <= start) continue
160
+ const { originalStart, originalEnd } = mappedOffsetRange(sourceMap, start, stop)
161
+ mappings.push({
162
+ generatedStart: generatedOffset + start - sourceOffset,
163
+ generatedEnd: generatedOffset + stop - sourceOffset,
164
+ originalStart,
165
+ originalEnd,
166
+ })
167
+ }
168
+ generatedOffset += end - sourceOffset
169
+ sourceOffset = end
170
+ }
171
+
172
+ for (const item of normalized) {
173
+ appendUnchanged(item.start)
174
+ const { text } = item
175
+ chunks.push(text)
176
+ mappings.push(...replacementSegments(sourceMap, item.start, item.end, text.length, item.mappings)
177
+ .map(segment => ({ ...segment, generatedStart: segment.generatedStart - item.start + generatedOffset,
178
+ generatedEnd: segment.generatedEnd - item.start + generatedOffset })))
179
+ generatedOffset += text.length
180
+ sourceOffset = item.end
181
+ }
182
+ appendUnchanged(code.length)
183
+ return { code: chunks.join(''), sourceMap: mappings }
184
+ }
185
+
186
+ function lineStartOffset(source, line) {
187
+ let offset = 0
188
+ for (let current = 1; current < line; current += 1) {
189
+ const newline = source.indexOf('\n', offset)
190
+ if (newline < 0) return source.length
191
+ offset = newline + 1
192
+ }
193
+ return offset
194
+ }
195
+
196
+ function positionAtOffset(source, offset) {
197
+ const prefix = source.slice(0, offset)
198
+ const line = prefix.split(/\r?\n/).length
199
+ const lastBreak = prefix.lastIndexOf('\n')
200
+ return { line, column: offset - lastBreak }
201
+ }
202
+
203
+ export function mapSourcePosition(position, generatedSource, originalSource, sourceMap) {
204
+ if (position === undefined || sourceMap === undefined) return position
205
+ if (!Number.isSafeInteger(position.line) || position.line < 1
206
+ || !Number.isSafeInteger(position.column) || position.column < 1) return position
207
+ const lineStart = lineStartOffset(generatedSource, position.line)
208
+ const mappedOffset = originalOffsetAt(sourceMap, lineStart + position.column - 1)
209
+ return positionAtOffset(originalSource, mappedOffset)
210
+ }
211
+
212
+ /** Map a complete generated-source span into original cell coordinates. */
213
+ export function mapSourceSpan(span, generatedSource, originalSource, sourceMap) {
214
+ if (span === undefined) return undefined
215
+ const start = mapSourcePosition(span, generatedSource, originalSource, sourceMap)
216
+ if (span.end === undefined) return start === span ? span : start
217
+ if (!Number.isSafeInteger(span.end.line) || span.end.line < 1
218
+ || !Number.isSafeInteger(span.end.column) || span.end.column < 1) {
219
+ return { ...start, end: span.end }
220
+ }
221
+ const generatedEnd = lineStartOffset(generatedSource, span.end.line) + span.end.column - 1
222
+ const original = mappedOffsetRange(sourceMap, Math.max(0, generatedEnd - 1), generatedEnd)
223
+ return { ...start, end: positionAtOffset(originalSource, original.originalEnd) }
224
+ }
@@ -0,0 +1,339 @@
1
+ function parseArguments(raw) {
2
+ try {
3
+ return { ok: true, value: JSON.parse(raw) }
4
+ } catch {
5
+ return { ok: false }
6
+ }
7
+ }
8
+
9
+ function jsonValuesEqual(left, right) {
10
+ const pending = [[left, right]]
11
+ while (pending.length > 0) {
12
+ const [currentLeft, currentRight] = pending.pop()
13
+ if (Object.is(currentLeft, currentRight)) continue
14
+ if (currentLeft === null || currentRight === null
15
+ || typeof currentLeft !== 'object' || typeof currentRight !== 'object'
16
+ || Array.isArray(currentLeft) !== Array.isArray(currentRight)) return false
17
+ const leftKeys = Object.keys(currentLeft)
18
+ const rightKeys = Object.keys(currentRight)
19
+ if (leftKeys.length !== rightKeys.length
20
+ || leftKeys.some(key => !Object.hasOwn(currentRight, key))) return false
21
+ for (const key of leftKeys) pending.push([currentLeft[key], currentRight[key]])
22
+ }
23
+ return true
24
+ }
25
+
26
+ function containsOwnProto(value) {
27
+ const pending = [value]
28
+ while (pending.length > 0) {
29
+ const current = pending.pop()
30
+ if (current === null || typeof current !== 'object') continue
31
+ if (!Array.isArray(current) && Object.hasOwn(current, '__proto__')) return true
32
+ for (const child of Object.values(current)) pending.push(child)
33
+ }
34
+ return false
35
+ }
36
+
37
+ const MAX_DIRECT_LITERAL_DEPTH = 128
38
+
39
+ function jsonContainerDepth(value) {
40
+ let maximum = 0
41
+ const pending = [{ value, depth: 0 }]
42
+ while (pending.length > 0) {
43
+ const current = pending.pop()
44
+ if (current.value === null || typeof current.value !== 'object') continue
45
+ const depth = current.depth + 1
46
+ if (depth > maximum) maximum = depth
47
+ for (const child of Object.values(current.value)) pending.push({ value: child, depth })
48
+ }
49
+ return maximum
50
+ }
51
+
52
+ function numberLiteral(value) {
53
+ if (Object.is(value, -0)) return '-0'
54
+ if (value === Infinity) return '1e400'
55
+ if (value === -Infinity) return '-1e400'
56
+ return JSON.stringify(value)
57
+ }
58
+
59
+ function safeJsonLiteral(value) {
60
+ const output = []
61
+ const pending = [{ kind: 'value', value }]
62
+ while (pending.length > 0) {
63
+ const item = pending.pop()
64
+ if (item.kind === 'text') {
65
+ output.push(item.value)
66
+ continue
67
+ }
68
+ const current = item.value
69
+ if (current === null) output.push('null')
70
+ else if (typeof current === 'string') output.push(JSON.stringify(current))
71
+ else if (typeof current === 'boolean') output.push(String(current))
72
+ else if (typeof current === 'number') output.push(numberLiteral(current))
73
+ else if (Array.isArray(current)) {
74
+ output.push('[')
75
+ pending.push({ kind: 'text', value: ']' })
76
+ for (let index = current.length - 1; index >= 0; index -= 1) {
77
+ if (index < current.length - 1) pending.push({ kind: 'text', value: ',' })
78
+ pending.push({ kind: 'value', value: current[index] })
79
+ }
80
+ } else {
81
+ const keys = Object.keys(current)
82
+ output.push('{')
83
+ pending.push({ kind: 'text', value: '}' })
84
+ for (let index = keys.length - 1; index >= 0; index -= 1) {
85
+ const key = keys[index]
86
+ if (index < keys.length - 1) pending.push({ kind: 'text', value: ',' })
87
+ pending.push({ kind: 'value', value: current[key] })
88
+ pending.push({
89
+ kind: 'text',
90
+ value: `${key === '__proto__' ? `[${JSON.stringify(key)}]` : JSON.stringify(key)}:`,
91
+ })
92
+ }
93
+ }
94
+ }
95
+ return output.join('')
96
+ }
97
+
98
+ function isIdentifierName(name) {
99
+ return /^(?:[$_]|\p{ID_Start})(?:[$_\u200C\u200D]|\p{ID_Continue})*$/u.test(name)
100
+ }
101
+
102
+ function toolMemberCode(name) {
103
+ return isIdentifierName(name) ? `tools.${name}` : `tools[${JSON.stringify(name)}]`
104
+ }
105
+
106
+ function nativeCallCode(name, rawArgs, parsedArgs) {
107
+ const argumentCode = jsonContainerDepth(parsedArgs) > MAX_DIRECT_LITERAL_DEPTH
108
+ ? `JSON.parse(${JSON.stringify(rawArgs)})`
109
+ : containsOwnProto(parsedArgs) ? safeJsonLiteral(parsedArgs) : rawArgs
110
+ return `{\n return await ${toolMemberCode(name)}(${argumentCode})\n}`
111
+ }
112
+
113
+ function acceptsTransportView(tools, editToolName) {
114
+ if (!Array.isArray(tools) || tools.length < 1 || tools.length > 2) return false
115
+ const names = tools.map(tool => tool?.name)
116
+ if (new Set(names).size !== names.length || !names.includes('run_code')) return false
117
+ return names.every(name => name === 'run_code' || name === editToolName)
118
+ }
119
+
120
+ function resolveCandidateName(name, nativeSchemas) {
121
+ if (typeof name !== 'string' || name.length === 0) return undefined
122
+ const matches = new Set()
123
+ if (nativeSchemas.has(name)) matches.add(name)
124
+ if (name.startsWith('tools.')) {
125
+ const member = name.slice('tools.'.length)
126
+ if (member.length > 0 && !member.includes('.') && nativeSchemas.has(member)) {
127
+ matches.add(member)
128
+ }
129
+ }
130
+ return matches.size === 1 ? matches.values().next().value : undefined
131
+ }
132
+
133
+ function extractCall(chunks, index) {
134
+ let name
135
+ let id
136
+ let deltaArgs = ''
137
+ let finalArgs
138
+ let deltaIndex = -1
139
+ let invalidId = false
140
+ let inconsistent = false
141
+ let complete = false
142
+ for (let position = 0; position < chunks.length; position += 1) {
143
+ const chunk = chunks[position]
144
+ if (chunk.index !== index) continue
145
+ if (chunk.type === 'tool-call-delta') {
146
+ if (chunk.id === undefined) invalidId = true
147
+ else if (chunk.id !== '') {
148
+ if (id !== undefined && id !== chunk.id) inconsistent = true
149
+ else id = chunk.id
150
+ }
151
+ if (typeof chunk.name === 'string' && chunk.name.length > 0) {
152
+ if (name !== undefined && name !== chunk.name) inconsistent = true
153
+ else name = chunk.name
154
+ }
155
+ if (typeof chunk.argumentsDelta !== 'string') inconsistent = true
156
+ else deltaArgs += chunk.argumentsDelta
157
+ if (deltaIndex < 0) deltaIndex = position
158
+ } else if (chunk.type === 'block-end' && chunk.block?.type === 'tool-call') {
159
+ const block = chunk.block
160
+ complete = true
161
+ if (typeof block.id !== 'string' || block.id.length === 0) invalidId = true
162
+ else if (id !== undefined && id !== block.id) inconsistent = true
163
+ else id ??= block.id
164
+ if (name !== undefined && name !== block.name) inconsistent = true
165
+ else name ??= block.name
166
+ if (typeof block.arguments !== 'string') inconsistent = true
167
+ else if (finalArgs !== undefined && finalArgs !== block.arguments) inconsistent = true
168
+ else finalArgs = block.arguments
169
+ }
170
+ }
171
+ let args = deltaArgs
172
+ if (finalArgs !== undefined) {
173
+ if (deltaArgs.length > 0) {
174
+ const streamed = parseArguments(deltaArgs)
175
+ const finalized = parseArguments(finalArgs)
176
+ if (!streamed.ok || !finalized.ok || !jsonValuesEqual(streamed.value, finalized.value)) {
177
+ inconsistent = true
178
+ }
179
+ }
180
+ args = finalArgs
181
+ }
182
+ return { name, id, args, deltaIndex, invalidId, inconsistent, complete }
183
+ }
184
+
185
+ function transformedChunks(chunks, replacements) {
186
+ const byIndex = new Map(replacements.map(replacement => [replacement.index, replacement]))
187
+ const emitted = new Set()
188
+ return chunks.flatMap(chunk => {
189
+ if (chunk.type === 'finish') {
190
+ const { replayState: _replayState, ...finish } = chunk
191
+ return [finish]
192
+ }
193
+ const replacement = byIndex.get(chunk.index)
194
+ if (replacement === undefined) return [chunk]
195
+ if (chunk.type === 'block-start') return [{ ...chunk, blockType: 'tool-call' }]
196
+ if (chunk.type === 'tool-call-delta') {
197
+ if (emitted.has(chunk.index)) return []
198
+ emitted.add(chunk.index)
199
+ return [{
200
+ ...chunk,
201
+ name: 'run_code',
202
+ argumentsDelta: replacement.arguments,
203
+ }]
204
+ }
205
+ if (chunk.type === 'block-end' && chunk.block?.type === 'tool-call') {
206
+ return [{
207
+ ...chunk,
208
+ block: {
209
+ type: 'tool-call',
210
+ id: replacement.id,
211
+ name: 'run_code',
212
+ arguments: replacement.arguments,
213
+ },
214
+ }]
215
+ }
216
+ return [chunk]
217
+ })
218
+ }
219
+
220
+ function transformCandidate(chunks, index, nativeSchemas, allowDeltaOnly) {
221
+ const call = extractCall(chunks, index)
222
+ const nativeName = resolveCandidateName(call.name, nativeSchemas)
223
+ if ((!call.complete && !allowDeltaOnly) || nativeName === undefined
224
+ || call.id === undefined || call.invalidId
225
+ || call.inconsistent || call.deltaIndex < 0) return undefined
226
+ const parsed = parseArguments(call.args)
227
+ if (!parsed.ok) return undefined
228
+ const argumentsValue = JSON.stringify({
229
+ code: nativeCallCode(nativeName, call.args, parsed.value),
230
+ description: `Call ${nativeName} inside the session REPL`,
231
+ })
232
+ return { index, id: call.id, arguments: argumentsValue }
233
+ }
234
+
235
+ function transformCandidates(chunks, indices, nativeSchemas, allowDeltaOnly = false) {
236
+ const replacements = [...indices].map(index => transformCandidate(
237
+ chunks, index, nativeSchemas, allowDeltaOnly,
238
+ ))
239
+ .filter(replacement => replacement !== undefined)
240
+ return replacements.length === 0 ? undefined : transformedChunks(chunks, replacements)
241
+ }
242
+
243
+ /**
244
+ * Normalize model-emitted native calls into the code-only direct-tool projection without guessing.
245
+ * Unknown or malformed calls pass through unchanged for the host to diagnose.
246
+ */
247
+ export async function* canonicalizeToolCallStream(source, options = {}) {
248
+ const editToolName = typeof options.editToolName === 'string' && options.editToolName.length > 0
249
+ ? options.editToolName
250
+ : undefined
251
+ if (options.enabled === false || !acceptsTransportView(options.tools, editToolName)) {
252
+ yield* source
253
+ return
254
+ }
255
+ const nativeSchemas = options.nativeSchemas instanceof Map
256
+ ? new Map([...options.nativeSchemas].filter(([name]) => name !== 'run_code' && name !== editToolName))
257
+ : new Map()
258
+ if (nativeSchemas.size === 0) {
259
+ yield* source
260
+ return
261
+ }
262
+ const passthrough = new Set()
263
+ const open = new Set()
264
+ let pending = []
265
+ let changed = false
266
+ try {
267
+ for await (const chunk of source) {
268
+ if (pending.length > 0) {
269
+ pending.push(chunk)
270
+ if (chunk.type === 'block-start' && chunk.blockType === 'tool-call') open.add(chunk.index)
271
+ if (chunk.type === 'tool-call-delta'
272
+ && resolveCandidateName(chunk.name, nativeSchemas) !== undefined) open.add(chunk.index)
273
+ if (chunk.type === 'tool-call-delta' && open.size === 1 && open.has(chunk.index)
274
+ && typeof chunk.name === 'string'
275
+ && resolveCandidateName(chunk.name, nativeSchemas) === undefined) {
276
+ yield* pending
277
+ pending = []
278
+ passthrough.add(chunk.index)
279
+ open.clear()
280
+ } else if (chunk.type === 'block-end' && chunk.block?.type === 'tool-call') {
281
+ open.delete(chunk.index)
282
+ if (open.size > 0) continue
283
+ const indices = new Set(pending
284
+ .filter(item => item.type === 'tool-call-delta' || item.type === 'block-end')
285
+ .map(item => item.index))
286
+ const transformed = transformCandidates(pending, indices, nativeSchemas)
287
+ if (transformed === undefined) yield* pending
288
+ else {
289
+ changed = true
290
+ yield* transformed
291
+ }
292
+ pending = []
293
+ } else if (chunk.type === 'finish') {
294
+ const indices = new Set(pending
295
+ .filter(item => item.type === 'tool-call-delta' || item.type === 'block-end')
296
+ .map(item => item.index))
297
+ const transformed = transformCandidates(pending, indices, nativeSchemas, true)
298
+ if (transformed !== undefined) {
299
+ changed = true
300
+ yield* transformed
301
+ } else if (changed && Object.hasOwn(chunk, 'replayState')) {
302
+ const { replayState: _replayState, ...finish } = chunk
303
+ pending[pending.length - 1] = finish
304
+ yield* pending
305
+ } else {
306
+ yield* pending
307
+ }
308
+ pending = []
309
+ open.clear()
310
+ }
311
+ continue
312
+ }
313
+ if (passthrough.has(chunk.index)) {
314
+ yield chunk
315
+ if (chunk.type === 'block-end') passthrough.delete(chunk.index)
316
+ continue
317
+ }
318
+ if (chunk.type === 'block-start' && chunk.blockType === 'tool-call') {
319
+ open.add(chunk.index)
320
+ pending = [chunk]
321
+ } else if (chunk.type === 'tool-call-delta'
322
+ && resolveCandidateName(chunk.name, nativeSchemas) !== undefined) {
323
+ open.add(chunk.index)
324
+ pending = [chunk]
325
+ } else if (changed && chunk.type === 'finish') {
326
+ // Provider replay metadata describes the original response and becomes
327
+ // stale only when at least one block was normalized.
328
+ const { replayState: _replayState, ...finish } = chunk
329
+ yield finish
330
+ } else {
331
+ yield chunk
332
+ }
333
+ }
334
+ } catch (error) {
335
+ yield* pending
336
+ throw error
337
+ }
338
+ yield* pending
339
+ }