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.
- package/LICENSE +21 -0
- package/README.md +137 -0
- package/README.zh.md +137 -0
- package/assets/dsh-ptc-plus-banner.webp +0 -0
- package/assets/ptc-plus-repair.png +0 -0
- package/assets/ptc-plus-settings.png +0 -0
- package/client.js +366 -0
- package/cordis.patch.yml +5 -0
- package/docs/README.md +21 -0
- package/docs/adr/0001-delegate-governance-to-dsh.md +3 -0
- package/docs/adr/0002-limit-work-map-scope.md +3 -0
- package/docs/adr/0003-prefer-native-program-surfaces.md +3 -0
- package/docs/adr/0004-declare-program-bindings-at-the-host.md +5 -0
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +129 -0
- package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +45 -0
- package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +39 -0
- package/docs/adr/0008-define-agent-operating-contract.md +25 -0
- package/docs/adr/0009-bound-expensive-acceptance-default.md +39 -0
- package/docs/adr/0010-session-log-derived-recovery-tips.md +26 -0
- package/docs/adr/0011-isolate-the-local-recovery-tip-provider.md +20 -0
- package/docs/adr/0012-preserve-mixed-destructuring-initialization.md +23 -0
- package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +21 -0
- package/docs/adr/0014-persist-cell-rewrite-policy.md +25 -0
- package/docs/adr/0015-preserve-source-positions-through-rewrites.md +25 -0
- package/docs/adr/0016-archive-resolved-review-findings.md +43 -0
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +23 -0
- package/docs/adr/0018-use-versioned-fixture-for-ab-benchmark.md +25 -0
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +32 -0
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +36 -0
- package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +68 -0
- package/docs/adr/README.md +18 -0
- package/docs/architecture.md +134 -0
- package/docs/capability-projection.md +48 -0
- package/docs/client-ui.md +47 -0
- package/docs/durability-design.md +265 -0
- package/docs/evaluation.md +62 -0
- package/docs/installation.md +82 -0
- package/docs/program-data-plane.md +61 -0
- package/docs/publishing.md +37 -0
- package/docs/runtime-reference.md +119 -0
- package/docs/value-wire.md +61 -0
- package/index.js +532 -0
- package/internal/ast-traversal.js +20 -0
- package/internal/binding-descriptors.js +95 -0
- package/internal/binding-pattern.js +54 -0
- package/internal/builtin-esm-sync.js +9 -0
- package/internal/cell-analysis.js +467 -0
- package/internal/cell-rewriter.js +520 -0
- package/internal/config-spec.js +179 -0
- package/internal/cordis-tools-owner.js +363 -0
- package/internal/diagnostic.js +133 -0
- package/internal/direct-surface-owner.js +326 -0
- package/internal/edit-transport-owner.js +287 -0
- package/internal/failure-reporting.js +139 -0
- package/internal/kernel-worker.js +484 -0
- package/internal/module-policy.js +46 -0
- package/internal/program-bindings.js +187 -0
- package/internal/record-utils.js +53 -0
- package/internal/recovery-tips.js +81 -0
- package/internal/rejected-cell-editor.js +328 -0
- package/internal/repl-convenience.js +201 -0
- package/internal/runtime-bridge-owner.js +417 -0
- package/internal/runtime-config.js +77 -0
- package/internal/runtime-contexts.js +31 -0
- package/internal/sdk-projection.js +88 -0
- package/internal/session-cell-executor.js +612 -0
- package/internal/session-journal.js +889 -0
- package/internal/session-log-view.js +80 -0
- package/internal/session-runtime.js +536 -0
- package/internal/session-state.js +45 -0
- package/internal/source-position-map.js +224 -0
- package/internal/tool-call-canonicalizer.js +339 -0
- package/internal/value-wire.js +421 -0
- package/internal/worker-client.js +218 -0
- package/internal/worker-cwd-virtualization.js +155 -0
- package/package.json +100 -0
- 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
|
+
}
|