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,201 @@
|
|
|
1
|
+
import { bindingNodes, createGeneratedNameAllocator } from './binding-pattern.js'
|
|
2
|
+
import { applySourceEdits, createMappedTextBuilder, identitySourceMap } from './source-position-map.js'
|
|
3
|
+
import { SKIP_AST_CHILDREN, walkAst } from './ast-traversal.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Optional compatibility policy for a persistent REPL. The cell language does
|
|
7
|
+
* not own this policy: callers may replace it when persistence uses separate
|
|
8
|
+
* lexical frames instead of one shared REPL environment.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
function containsTopLevelAwait(node) {
|
|
12
|
+
let found = false
|
|
13
|
+
walkAst(node, current => {
|
|
14
|
+
if (current.type === 'AwaitExpression') found = true
|
|
15
|
+
if (current.type === 'FunctionDeclaration' || current.type === 'FunctionExpression'
|
|
16
|
+
|| current.type === 'ArrowFunctionExpression') return SKIP_AST_CHILDREN
|
|
17
|
+
})
|
|
18
|
+
return found
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function mixedDeclaratorText(statement, declarator, bindings, code, offset, allocateName) {
|
|
22
|
+
const fresh = bindings.filter(binding => !binding.existing)
|
|
23
|
+
const existing = bindings.filter(binding => binding.existing)
|
|
24
|
+
const valueName = allocateName('mixed_value')
|
|
25
|
+
const commitName = allocateName('mixed_commit')
|
|
26
|
+
const valuesName = allocateName('mixed_values')
|
|
27
|
+
const positions = new Map(bindings.map((binding, index) => [binding.name, index]))
|
|
28
|
+
const asynchronous = containsTopLevelAwait(declarator.id)
|
|
29
|
+
const builder = createMappedTextBuilder(code, offset)
|
|
30
|
+
const appendBindingName = binding => builder.appendMapped(binding.name, binding.start, binding.end)
|
|
31
|
+
builder.append(statement.kind === 'var' ? 'var [' : 'let [')
|
|
32
|
+
fresh.forEach((binding, index) => {
|
|
33
|
+
if (index > 0) builder.append(', ')
|
|
34
|
+
appendBindingName(binding)
|
|
35
|
+
})
|
|
36
|
+
builder.append(`] = ${asynchronous ? 'await ' : ''}(`)
|
|
37
|
+
builder.append(`(${asynchronous ? 'async ' : ''}(${valueName}, ${commitName}) => { ${statement.kind} `)
|
|
38
|
+
builder.appendSource(declarator.id.start, declarator.id.end)
|
|
39
|
+
builder.append(` = ${valueName}; return ${commitName}([`)
|
|
40
|
+
bindings.forEach((binding, index) => {
|
|
41
|
+
if (index > 0) builder.append(', ')
|
|
42
|
+
appendBindingName(binding)
|
|
43
|
+
})
|
|
44
|
+
builder.append(`]); })((`)
|
|
45
|
+
if (declarator.init === null) builder.append('undefined')
|
|
46
|
+
else builder.appendSource(declarator.init.start, declarator.init.end)
|
|
47
|
+
builder.append(`), (${valuesName}) => { `)
|
|
48
|
+
existing.forEach((binding, index) => {
|
|
49
|
+
if (index > 0) builder.append(' ')
|
|
50
|
+
appendBindingName(binding)
|
|
51
|
+
builder.append(` = ${valuesName}[${positions.get(binding.name)}];`)
|
|
52
|
+
})
|
|
53
|
+
builder.append(' return [')
|
|
54
|
+
fresh.forEach((binding, index) => {
|
|
55
|
+
if (index > 0) builder.append(', ')
|
|
56
|
+
builder.append(valuesName)
|
|
57
|
+
builder.append(`[${positions.get(binding.name)}]`)
|
|
58
|
+
})
|
|
59
|
+
builder.append(']; }) );')
|
|
60
|
+
return builder.result()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function existingDeclaratorText(declarator, code, offset) {
|
|
64
|
+
const builder = createMappedTextBuilder(code, offset)
|
|
65
|
+
builder.append(';(')
|
|
66
|
+
builder.appendSource(declarator.id.start, declarator.id.end)
|
|
67
|
+
builder.append(' = ')
|
|
68
|
+
if (declarator.init === null) builder.append('undefined')
|
|
69
|
+
else builder.appendSource(declarator.init.start, declarator.init.end)
|
|
70
|
+
builder.append(');')
|
|
71
|
+
return builder.result()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function freshDeclaratorText(statement, declarator, code, offset) {
|
|
75
|
+
const builder = createMappedTextBuilder(code, offset)
|
|
76
|
+
builder.append(statement.kind === 'var' ? 'var ' : 'let ')
|
|
77
|
+
builder.appendSource(declarator.start, declarator.end)
|
|
78
|
+
builder.append(';')
|
|
79
|
+
return builder.result()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function bindingEntries(pattern, declarationSpan) {
|
|
83
|
+
return bindingNodes(pattern).map(node => ({
|
|
84
|
+
name: node.name,
|
|
85
|
+
start: node.start,
|
|
86
|
+
end: node.end,
|
|
87
|
+
span: declarationSpan(node),
|
|
88
|
+
}))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function rewriteLooseRedeclarations({
|
|
92
|
+
code,
|
|
93
|
+
sourceMap = identitySourceMap(code.length),
|
|
94
|
+
body,
|
|
95
|
+
offset,
|
|
96
|
+
knownBindings,
|
|
97
|
+
declarations,
|
|
98
|
+
declarationSpan,
|
|
99
|
+
collisionFor,
|
|
100
|
+
autoSplitRedeclarations,
|
|
101
|
+
}) {
|
|
102
|
+
const redeclared = []
|
|
103
|
+
const rewrites = []
|
|
104
|
+
if (body === undefined) {
|
|
105
|
+
return {
|
|
106
|
+
executableCode: code,
|
|
107
|
+
executableSourceMap: sourceMap,
|
|
108
|
+
collisions: declarations.filter(declaration => knownBindings.has(declaration.name)).map(collisionFor),
|
|
109
|
+
redeclared,
|
|
110
|
+
rewrites,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const replacements = []
|
|
114
|
+
const rejected = []
|
|
115
|
+
const allocateName = createGeneratedNameAllocator(body)
|
|
116
|
+
for (const statement of body) {
|
|
117
|
+
if (statement.type !== 'VariableDeclaration') {
|
|
118
|
+
if ((statement.type === 'FunctionDeclaration' || statement.type === 'ClassDeclaration')
|
|
119
|
+
&& statement.id !== null && knownBindings.has(statement.id.name)) {
|
|
120
|
+
rejected.push({ name: statement.id.name, span: declarationSpan(statement.id) })
|
|
121
|
+
}
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
124
|
+
const entries = []
|
|
125
|
+
let statementRejected = false
|
|
126
|
+
for (const declarator of statement.declarations) {
|
|
127
|
+
const bindings = bindingEntries(declarator.id, declarationSpan)
|
|
128
|
+
const existing = bindings.filter(binding => knownBindings.has(binding.name))
|
|
129
|
+
if (existing.length > 0 && existing.length < bindings.length) {
|
|
130
|
+
if (autoSplitRedeclarations) entries.push({ declarator, bindings, existing })
|
|
131
|
+
else {
|
|
132
|
+
rejected.push(...existing)
|
|
133
|
+
statementRejected = true
|
|
134
|
+
}
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
entries.push({ declarator, bindings, existing })
|
|
138
|
+
}
|
|
139
|
+
if (statementRejected) continue
|
|
140
|
+
if (!entries.some(entry => entry.existing.length > 0)) {
|
|
141
|
+
if (statement.kind === 'const') {
|
|
142
|
+
replacements.push({
|
|
143
|
+
start: statement.start - offset,
|
|
144
|
+
end: statement.start - offset + statement.kind.length,
|
|
145
|
+
text: 'let',
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
let replacementText = ''
|
|
151
|
+
const replacementMappings = []
|
|
152
|
+
const appendPart = (part) => {
|
|
153
|
+
if (replacementText.length > 0) replacementText += '\n'
|
|
154
|
+
const partOffset = replacementText.length
|
|
155
|
+
replacementText += part.text
|
|
156
|
+
part.mappings.forEach(mapping => replacementMappings.push({
|
|
157
|
+
...mapping,
|
|
158
|
+
generatedStart: mapping.generatedStart + partOffset,
|
|
159
|
+
generatedEnd: mapping.generatedEnd + partOffset,
|
|
160
|
+
}))
|
|
161
|
+
}
|
|
162
|
+
for (const { declarator, bindings, existing } of entries) {
|
|
163
|
+
if (existing.length > 0 && existing.length < bindings.length) {
|
|
164
|
+
appendPart(mixedDeclaratorText(
|
|
165
|
+
statement,
|
|
166
|
+
declarator,
|
|
167
|
+
bindings.map(binding => ({ ...binding, existing: knownBindings.has(binding.name) })),
|
|
168
|
+
code,
|
|
169
|
+
offset,
|
|
170
|
+
allocateName,
|
|
171
|
+
))
|
|
172
|
+
redeclared.push(...existing.map(collisionFor))
|
|
173
|
+
rewrites.push({
|
|
174
|
+
kind: 'redeclaration',
|
|
175
|
+
description: 'split a mixed top-level declaration while preserving native pattern initialization',
|
|
176
|
+
source: existing.map(binding => binding.name).join(', '),
|
|
177
|
+
})
|
|
178
|
+
} else if (existing.length === bindings.length && bindings.length > 0) {
|
|
179
|
+
redeclared.push(...existing.map(collisionFor))
|
|
180
|
+
appendPart(existingDeclaratorText(declarator, code, offset))
|
|
181
|
+
} else {
|
|
182
|
+
appendPart(freshDeclaratorText(statement, declarator, code, offset))
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
replacements.push({
|
|
186
|
+
start: statement.start - offset,
|
|
187
|
+
end: statement.end - offset,
|
|
188
|
+
text: replacementText,
|
|
189
|
+
mappings: replacementMappings,
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
const collisions = rejected.map(collisionFor)
|
|
193
|
+
let executableCode = code
|
|
194
|
+
let executableSourceMap = sourceMap
|
|
195
|
+
if (collisions.length === 0) {
|
|
196
|
+
const applied = applySourceEdits(code, sourceMap, replacements)
|
|
197
|
+
executableCode = applied.code
|
|
198
|
+
executableSourceMap = applied.sourceMap
|
|
199
|
+
}
|
|
200
|
+
return { executableCode, executableSourceMap, collisions, redeclared, rewrites }
|
|
201
|
+
}
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/** Own the CodeRuntime patch, per-cell lease, journal projection, and session settlement. */
|
|
2
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
3
|
+
import { assertObjectJsonSchema, validateJsonSchemaValue } from '@deepseek-ai/dsh-tools'
|
|
4
|
+
import { normalizeBindingDescriptors } from './binding-descriptors.js'
|
|
5
|
+
import { SessionRuntime } from './session-runtime.js'
|
|
6
|
+
import {
|
|
7
|
+
JOURNAL_KEY,
|
|
8
|
+
RECOVERY_BOUNDARY_KEY,
|
|
9
|
+
journalsEqual,
|
|
10
|
+
recoveryBoundariesEqual,
|
|
11
|
+
withRecoveryBoundaries,
|
|
12
|
+
withJournal,
|
|
13
|
+
withRewrites,
|
|
14
|
+
} from './session-journal.js'
|
|
15
|
+
import {
|
|
16
|
+
capabilityFind,
|
|
17
|
+
capabilityInspect,
|
|
18
|
+
capabilityTree,
|
|
19
|
+
toolCapabilityMetadata,
|
|
20
|
+
} from './program-bindings.js'
|
|
21
|
+
import { deepFreeze } from './record-utils.js'
|
|
22
|
+
|
|
23
|
+
export const RUN_CODE = 'run_code'
|
|
24
|
+
|
|
25
|
+
const EDIT_RUN_CODE = 'edit_run_code'
|
|
26
|
+
const PLUGIN_PROGRAM_GLOBALS = new Set(['capabilities', 'code', 'repl'])
|
|
27
|
+
|
|
28
|
+
function nestedRunCodeArguments(value) {
|
|
29
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
30
|
+
throw new TypeError('code.run expects an object with code and description strings')
|
|
31
|
+
}
|
|
32
|
+
const keys = Reflect.ownKeys(value)
|
|
33
|
+
if (keys.length !== 2 || !keys.includes('code') || !keys.includes('description')
|
|
34
|
+
|| keys.some(key => typeof key !== 'string' || !Object.prototype.propertyIsEnumerable.call(value, key))
|
|
35
|
+
|| typeof value.code !== 'string' || typeof value.description !== 'string') {
|
|
36
|
+
throw new TypeError('code.run expects exactly code and description string properties')
|
|
37
|
+
}
|
|
38
|
+
return { code: value.code, description: value.description }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function namespace(global, functions, errorName, memberNameProperty) {
|
|
42
|
+
return {
|
|
43
|
+
global,
|
|
44
|
+
functions,
|
|
45
|
+
errorClass: { name: errorName, memberNameProperty },
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function schemaAcceptsEmptyObject(schema) {
|
|
50
|
+
const parameters = schema?.parameters
|
|
51
|
+
try {
|
|
52
|
+
assertObjectJsonSchema(parameters)
|
|
53
|
+
} catch {
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
return validateJsonSchemaValue(parameters, {}).length === 0
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const PROGRAM_CAPABILITY_METADATA = deepFreeze([
|
|
60
|
+
{
|
|
61
|
+
namespace: 'repl',
|
|
62
|
+
members: [{
|
|
63
|
+
name: 'state',
|
|
64
|
+
description: 'List, save, restore, or delete named durable REPL states.',
|
|
65
|
+
parameters: {
|
|
66
|
+
type: 'object',
|
|
67
|
+
properties: {
|
|
68
|
+
action: { enum: ['list', 'save', 'restore', 'delete'] },
|
|
69
|
+
name: { type: 'string' },
|
|
70
|
+
},
|
|
71
|
+
oneOf: [
|
|
72
|
+
{ required: ['action'], properties: { action: { const: 'list' } } },
|
|
73
|
+
{ required: ['action', 'name'], properties: { action: { const: 'save' } } },
|
|
74
|
+
{ required: ['action'], properties: { action: { const: 'restore' } } },
|
|
75
|
+
{ required: ['action', 'name'], properties: { action: { const: 'delete' } } },
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
returns: { type: 'object' },
|
|
79
|
+
effect: 'ptc-state',
|
|
80
|
+
authority: 'ptc-plus-program-binding',
|
|
81
|
+
completeness: 'complete',
|
|
82
|
+
replay: 'recorded-value',
|
|
83
|
+
}],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
namespace: 'code',
|
|
87
|
+
members: [{
|
|
88
|
+
name: 'run',
|
|
89
|
+
description: 'Run isolated source already held as data through the top-level code.run binding; do not use tools.code.run or wrap work that can execute directly in the current cell.',
|
|
90
|
+
parameters: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
additionalProperties: false,
|
|
93
|
+
properties: {
|
|
94
|
+
code: { type: 'string' },
|
|
95
|
+
description: { type: 'string' },
|
|
96
|
+
},
|
|
97
|
+
required: ['code', 'description'],
|
|
98
|
+
},
|
|
99
|
+
returns: {
|
|
100
|
+
type: 'object',
|
|
101
|
+
additionalProperties: false,
|
|
102
|
+
properties: {
|
|
103
|
+
logs: {
|
|
104
|
+
type: 'array',
|
|
105
|
+
items: { type: 'string' },
|
|
106
|
+
description: 'Child console output in emission order.',
|
|
107
|
+
},
|
|
108
|
+
result: {
|
|
109
|
+
description: 'Child return value; omitted when the child returns undefined.',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
required: ['logs'],
|
|
113
|
+
},
|
|
114
|
+
effect: 'unknown',
|
|
115
|
+
authority: 'ptc-plus-program-binding',
|
|
116
|
+
completeness: 'unknown',
|
|
117
|
+
replay: 'recorded-value',
|
|
118
|
+
}],
|
|
119
|
+
},
|
|
120
|
+
])
|
|
121
|
+
|
|
122
|
+
export function createRuntimeBridgeOwner({
|
|
123
|
+
ctx,
|
|
124
|
+
sessionConfig,
|
|
125
|
+
maxNestedRunCodeDepth,
|
|
126
|
+
sessionId,
|
|
127
|
+
toolSchemasForAgent,
|
|
128
|
+
}) {
|
|
129
|
+
const scope = new AsyncLocalStorage()
|
|
130
|
+
// AgentRegistry.withInitiator is AsyncLocalStorage-based. The arrow preserves
|
|
131
|
+
// its receiver when a worker callback re-enters the host tool pipeline.
|
|
132
|
+
const withInitiator = ctx.agents === undefined || typeof ctx.agents.withInitiator !== 'function'
|
|
133
|
+
? undefined
|
|
134
|
+
: (agent, operation) => ctx.agents.withInitiator(agent, operation)
|
|
135
|
+
const sessions = new SessionRuntime(sessionConfig, { withInitiator })
|
|
136
|
+
let currentConfig = sessions.config
|
|
137
|
+
const runtime = ctx.codeRuntime
|
|
138
|
+
const ownRun = Object.getOwnPropertyDescriptor(runtime, 'run')
|
|
139
|
+
const upstreamRun = runtime.run
|
|
140
|
+
const patchedDefinitions = new Map()
|
|
141
|
+
const pending = new WeakMap()
|
|
142
|
+
let active = true
|
|
143
|
+
|
|
144
|
+
const projectBindings = (
|
|
145
|
+
request,
|
|
146
|
+
depth,
|
|
147
|
+
executionToken,
|
|
148
|
+
inheritedTools = undefined,
|
|
149
|
+
cellConfig = currentConfig,
|
|
150
|
+
) => {
|
|
151
|
+
const lease = { active: true }
|
|
152
|
+
const release = () => { lease.active = false }
|
|
153
|
+
const bindingDescriptors = normalizeBindingDescriptors(request.bindings)
|
|
154
|
+
const conflict = bindingDescriptors.namespaces
|
|
155
|
+
.map(binding => binding.global)
|
|
156
|
+
.find(global => PLUGIN_PROGRAM_GLOBALS.has(global))
|
|
157
|
+
if (conflict !== undefined) {
|
|
158
|
+
throw new Error(`ptc-plus: request binding conflicts with reserved program namespace ${JSON.stringify(conflict)}`)
|
|
159
|
+
}
|
|
160
|
+
const toolsNamespace = bindingDescriptors.namespaces.find(binding => binding.global === 'tools')
|
|
161
|
+
const functions = inheritedTools ?? (
|
|
162
|
+
toolsNamespace === undefined ? Object.create(null) : toolsNamespace.functions
|
|
163
|
+
)
|
|
164
|
+
const ensureLease = () => {
|
|
165
|
+
if (!lease.active) throw new Error('PTC execution lease expired')
|
|
166
|
+
}
|
|
167
|
+
const runCode = async (value) => {
|
|
168
|
+
ensureLease()
|
|
169
|
+
const args = nestedRunCodeArguments(value)
|
|
170
|
+
if (depth >= cellConfig.maxNestedRunCodeDepth) {
|
|
171
|
+
throw new RangeError(`code.run recursion depth exceeds configured maximum ${cellConfig.maxNestedRunCodeDepth}`)
|
|
172
|
+
}
|
|
173
|
+
if (typeof functions[RUN_CODE] === 'function') return functions[RUN_CODE](args)
|
|
174
|
+
const childProjected = projectBindings(
|
|
175
|
+
{ ...request, program: args.code }, depth + 1, executionToken, functions, cellConfig,
|
|
176
|
+
)
|
|
177
|
+
let child
|
|
178
|
+
try {
|
|
179
|
+
child = await upstreamRun.call(runtime, childProjected.request)
|
|
180
|
+
} finally {
|
|
181
|
+
childProjected.release()
|
|
182
|
+
}
|
|
183
|
+
if (child.error !== undefined) {
|
|
184
|
+
throw new Error(`nested run_code failed (${child.error.kind}): ${child.error.message}`)
|
|
185
|
+
}
|
|
186
|
+
return { logs: child.logs, ...(child.value === undefined ? {} : { result: child.value }) }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const schemas = toolSchemasForAgent(executionToken?.agent)
|
|
190
|
+
.filter(schema => schema?.name === RUN_CODE
|
|
191
|
+
|| (schema?.name !== EDIT_RUN_CODE && typeof functions[schema?.name] === 'function'))
|
|
192
|
+
const emptyObjectTools = new Set(schemas.flatMap(schema => (
|
|
193
|
+
typeof schema?.name === 'string' && schemaAcceptsEmptyObject(schema)
|
|
194
|
+
? [schema.name]
|
|
195
|
+
: []
|
|
196
|
+
)))
|
|
197
|
+
const projected = bindingDescriptors.namespaces.map((binding) => {
|
|
198
|
+
const wrapped = Object.create(null)
|
|
199
|
+
for (const key of binding.members) {
|
|
200
|
+
Object.defineProperty(wrapped, key, {
|
|
201
|
+
enumerable: true,
|
|
202
|
+
value: async (...args) => {
|
|
203
|
+
ensureLease()
|
|
204
|
+
return binding.functions[key](...args)
|
|
205
|
+
},
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
const emptyObjectMembers = binding.global === 'tools'
|
|
209
|
+
? binding.members.filter(member => emptyObjectTools.has(member))
|
|
210
|
+
: []
|
|
211
|
+
return {
|
|
212
|
+
...binding,
|
|
213
|
+
functions: wrapped,
|
|
214
|
+
...(binding.global === 'tools' ? { emptyObjectMembers } : {}),
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
const annotations = Object.fromEntries(schemas.flatMap(schema => (
|
|
218
|
+
typeof schema?.name === 'string' && schema.name !== RUN_CODE
|
|
219
|
+
? [[schema.name, { replay: 'recorded-value' }]]
|
|
220
|
+
: []
|
|
221
|
+
)))
|
|
222
|
+
const metadata = Object.freeze([
|
|
223
|
+
...toolCapabilityMetadata(schemas, annotations),
|
|
224
|
+
...PROGRAM_CAPABILITY_METADATA,
|
|
225
|
+
])
|
|
226
|
+
projected.push(namespace('capabilities', {
|
|
227
|
+
tree: async value => {
|
|
228
|
+
if (value !== undefined) throw new TypeError('capabilities.tree does not accept arguments')
|
|
229
|
+
ensureLease()
|
|
230
|
+
return capabilityTree(metadata)
|
|
231
|
+
},
|
|
232
|
+
find: async value => {
|
|
233
|
+
ensureLease()
|
|
234
|
+
if (typeof value !== 'string') throw new TypeError('capabilities.find expects a query string')
|
|
235
|
+
return capabilityFind(metadata, value)
|
|
236
|
+
},
|
|
237
|
+
inspect: async value => {
|
|
238
|
+
ensureLease()
|
|
239
|
+
if (value === undefined) return capabilityInspect(metadata)
|
|
240
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
241
|
+
throw new TypeError('capabilities.inspect expects an object')
|
|
242
|
+
}
|
|
243
|
+
return capabilityInspect(metadata, value.symbols, value.budget)
|
|
244
|
+
},
|
|
245
|
+
}, 'CapabilityExplorationError', 'operation'))
|
|
246
|
+
projected.push(namespace('code', { run: runCode }, 'CodeExecutionError', 'operation'))
|
|
247
|
+
return { request: { ...request, bindings: projected }, release }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const patchResultMetadata = (agent) => {
|
|
251
|
+
const definition = ctx.tools.get(RUN_CODE, agent)
|
|
252
|
+
if (definition === undefined) {
|
|
253
|
+
throw new Error('ptc-plus: run_code definition is unavailable for the owning session')
|
|
254
|
+
}
|
|
255
|
+
if (definition.output === undefined) {
|
|
256
|
+
throw new Error('ptc-plus: run_code definition has no output projection')
|
|
257
|
+
}
|
|
258
|
+
if (patchedDefinitions.has(definition)) return
|
|
259
|
+
const output = definition.output
|
|
260
|
+
const original = output.presentationMeta
|
|
261
|
+
const patched = (args, value) => {
|
|
262
|
+
if (!active) return original === undefined ? undefined : original(args, value)
|
|
263
|
+
const base = original === undefined ? undefined : original(args, value)
|
|
264
|
+
const current = scope.getStore()
|
|
265
|
+
const settlement = current?.settlement
|
|
266
|
+
if (settlement === undefined) return base
|
|
267
|
+
let meta = withJournal(base, settlement.journal)
|
|
268
|
+
if (settlement.recoveryBoundaries !== undefined) {
|
|
269
|
+
meta = withRecoveryBoundaries(meta, settlement.recoveryBoundaries)
|
|
270
|
+
}
|
|
271
|
+
return settlement.rewrites === undefined ? meta : withRewrites(meta, settlement.rewrites)
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
Object.defineProperty(output, 'presentationMeta', {
|
|
275
|
+
configurable: true,
|
|
276
|
+
enumerable: true,
|
|
277
|
+
writable: true,
|
|
278
|
+
value: patched,
|
|
279
|
+
})
|
|
280
|
+
patchedDefinitions.set(definition, { output, original, patched })
|
|
281
|
+
} catch (error) {
|
|
282
|
+
throw new Error(`ptc-plus: cannot attach the session journal to run_code results: ${error.message}`)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const patchedRun = function (request) {
|
|
287
|
+
if (!active) return upstreamRun.call(runtime, request)
|
|
288
|
+
const current = scope.getStore()
|
|
289
|
+
if (current === undefined) return upstreamRun.call(runtime, request)
|
|
290
|
+
const projected = projectBindings(request, 0, current)
|
|
291
|
+
return sessions.runTentative(current, { ...projected.request, executionToken: current })
|
|
292
|
+
.then((execution) => {
|
|
293
|
+
current.settlement = execution.settlement
|
|
294
|
+
return execution.result
|
|
295
|
+
})
|
|
296
|
+
.finally(projected.release)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
Object.defineProperty(runtime, 'run', {
|
|
300
|
+
configurable: true,
|
|
301
|
+
writable: true,
|
|
302
|
+
value: patchedRun,
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
return Object.freeze({
|
|
306
|
+
config: sessions.config,
|
|
307
|
+
reconfigure(nextConfig) {
|
|
308
|
+
sessions.reconfigure(nextConfig)
|
|
309
|
+
currentConfig = sessions.config
|
|
310
|
+
},
|
|
311
|
+
// A composite tool's outer result owns the final durability decision.
|
|
312
|
+
async executeTentative(callSeq, operation) {
|
|
313
|
+
const settlement = {
|
|
314
|
+
current: undefined,
|
|
315
|
+
innerConfirmed: false,
|
|
316
|
+
finalized: false,
|
|
317
|
+
}
|
|
318
|
+
const finalize = (outerConfirmed) => {
|
|
319
|
+
if (settlement.finalized) return
|
|
320
|
+
settlement.finalized = true
|
|
321
|
+
if (settlement.current?.settlement !== undefined) {
|
|
322
|
+
sessions.finalize(settlement.current.settlement, outerConfirmed && settlement.innerConfirmed)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const result = await scope.run({
|
|
327
|
+
...(callSeq === undefined ? {} : { persistedCallSeq: callSeq }),
|
|
328
|
+
deferredSettlement: settlement,
|
|
329
|
+
}, operation)
|
|
330
|
+
return Object.freeze({ result, finalize })
|
|
331
|
+
} catch (error) {
|
|
332
|
+
finalize(false)
|
|
333
|
+
throw error
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
handleExecute(exec, next) {
|
|
337
|
+
if (exec.parent !== undefined) return scope.run(undefined, next)
|
|
338
|
+
const id = sessionId(exec.agent)
|
|
339
|
+
if (id === undefined) return next()
|
|
340
|
+
patchResultMetadata(exec.agent)
|
|
341
|
+
const inherited = scope.getStore()
|
|
342
|
+
const persistedCallSeq = inherited?.persistedCallSeq
|
|
343
|
+
const current = {
|
|
344
|
+
id,
|
|
345
|
+
callId: String(exec.callId),
|
|
346
|
+
...(persistedCallSeq === undefined ? {} : { persistedCallSeq }),
|
|
347
|
+
...(inherited?.deferredSettlement === undefined
|
|
348
|
+
? {}
|
|
349
|
+
: { deferredSettlement: inherited.deferredSettlement }),
|
|
350
|
+
session: exec.agent?.session,
|
|
351
|
+
agent: exec.agent,
|
|
352
|
+
}
|
|
353
|
+
if (current.deferredSettlement !== undefined) {
|
|
354
|
+
current.deferredSettlement.current = current
|
|
355
|
+
}
|
|
356
|
+
pending.set(exec, current)
|
|
357
|
+
return scope.run(current, async () => {
|
|
358
|
+
const result = await next()
|
|
359
|
+
const settlement = current.settlement
|
|
360
|
+
if (result?.isError === true && settlement !== undefined) {
|
|
361
|
+
let meta = withJournal(result.meta, settlement.journal)
|
|
362
|
+
if (settlement.recoveryBoundaries !== undefined) {
|
|
363
|
+
meta = withRecoveryBoundaries(meta, settlement.recoveryBoundaries)
|
|
364
|
+
}
|
|
365
|
+
if (settlement.rewrites !== undefined) meta = withRewrites(meta, settlement.rewrites)
|
|
366
|
+
return { ...result, meta }
|
|
367
|
+
}
|
|
368
|
+
return result
|
|
369
|
+
})
|
|
370
|
+
},
|
|
371
|
+
handleResult(exec, result) {
|
|
372
|
+
if (exec.parent !== undefined) return
|
|
373
|
+
const id = sessionId(exec.agent)
|
|
374
|
+
if (id === undefined) return
|
|
375
|
+
const current = pending.get(exec)
|
|
376
|
+
pending.delete(exec)
|
|
377
|
+
const settlement = current?.settlement
|
|
378
|
+
if (settlement === undefined) {
|
|
379
|
+
sessions.noteNoop(id, exec.agent?.session, exec.callId)
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
const meta = result?.meta
|
|
383
|
+
const confirmed = meta !== null && typeof meta === 'object' && !Array.isArray(meta)
|
|
384
|
+
&& Object.hasOwn(meta, JOURNAL_KEY)
|
|
385
|
+
&& journalsEqual(meta[JOURNAL_KEY], settlement.journal)
|
|
386
|
+
&& recoveryBoundariesEqual(
|
|
387
|
+
Object.hasOwn(meta, RECOVERY_BOUNDARY_KEY) ? meta[RECOVERY_BOUNDARY_KEY] : undefined,
|
|
388
|
+
settlement.recoveryBoundaries,
|
|
389
|
+
)
|
|
390
|
+
if (current.deferredSettlement !== undefined) {
|
|
391
|
+
current.deferredSettlement.innerConfirmed = confirmed
|
|
392
|
+
return
|
|
393
|
+
}
|
|
394
|
+
sessions.finalize(settlement, confirmed)
|
|
395
|
+
},
|
|
396
|
+
disposeAgent(agent) {
|
|
397
|
+
return sessions.disposeSession(sessionId(agent) ?? String(agent.id))
|
|
398
|
+
},
|
|
399
|
+
disposeSession(session) {
|
|
400
|
+
return sessions.disposeSession(String(session.id))
|
|
401
|
+
},
|
|
402
|
+
async dispose() {
|
|
403
|
+
active = false
|
|
404
|
+
if (runtime.run === patchedRun) {
|
|
405
|
+
if (ownRun === undefined) delete runtime.run
|
|
406
|
+
else Object.defineProperty(runtime, 'run', ownRun)
|
|
407
|
+
}
|
|
408
|
+
for (const { output, original, patched } of patchedDefinitions.values()) {
|
|
409
|
+
if (output.presentationMeta !== patched) continue
|
|
410
|
+
if (original === undefined) delete output.presentationMeta
|
|
411
|
+
else output.presentationMeta = original
|
|
412
|
+
}
|
|
413
|
+
patchedDefinitions.clear()
|
|
414
|
+
await sessions.dispose()
|
|
415
|
+
},
|
|
416
|
+
})
|
|
417
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONFIG_DEFAULTS,
|
|
3
|
+
MAX_TIMER_DELAY_MS,
|
|
4
|
+
} from './config-spec.js'
|
|
5
|
+
|
|
6
|
+
export { MAX_TIMER_DELAY_MS }
|
|
7
|
+
|
|
8
|
+
/** Positive-safe-integer limits applied to runtime budget fields. */
|
|
9
|
+
const BUDGET_KEYS = Object.freeze([
|
|
10
|
+
'computeMs',
|
|
11
|
+
'maxOutputBytes',
|
|
12
|
+
'maxOldGenerationSizeMb',
|
|
13
|
+
'maxValueNodes',
|
|
14
|
+
'maxValueEdges',
|
|
15
|
+
'maxValueArrayLength',
|
|
16
|
+
'maxValueBigIntDigits',
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
/** Boolean runtime behavior switches. */
|
|
20
|
+
const BOOLEAN_KEYS = Object.freeze([
|
|
21
|
+
'cordisToolsEnabled',
|
|
22
|
+
'canonicalizeToolCalls',
|
|
23
|
+
'looseTopLevelRedeclarations',
|
|
24
|
+
'durableReplay',
|
|
25
|
+
'autoRewriteImports',
|
|
26
|
+
'autoStripExports',
|
|
27
|
+
'autoSplitRedeclarations',
|
|
28
|
+
'tipsEnabled',
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
/** Positive-safe-integer field groups that validate like budgets. */
|
|
32
|
+
const POSITIVE_KEYS = Object.freeze([
|
|
33
|
+
'maxNestedRunCodeDepth',
|
|
34
|
+
'tipCooldownMessages',
|
|
35
|
+
'tipEscalationFailures',
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve plugin/runtime config from raw input, applying the shared defaults
|
|
40
|
+
* and the same validation the Host Config schema exposes.
|
|
41
|
+
* @param config - raw config object; unknown keys are preserved for forwards compatibility.
|
|
42
|
+
* @returns a resolved config object.
|
|
43
|
+
*/
|
|
44
|
+
export function resolveConfig(config = {}) {
|
|
45
|
+
const resolved = { ...CONFIG_DEFAULTS, ...config }
|
|
46
|
+
if (typeof resolved.enabled !== 'boolean') {
|
|
47
|
+
throw new TypeError('ptc-plus: enabled must be a boolean')
|
|
48
|
+
}
|
|
49
|
+
for (const key of [...BUDGET_KEYS, ...POSITIVE_KEYS]) {
|
|
50
|
+
const value = resolved[key]
|
|
51
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
52
|
+
throw new TypeError(`ptc-plus: ${key} must be a positive safe integer`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
validateMaxWallMs(resolved.maxWallMs)
|
|
56
|
+
for (const key of BOOLEAN_KEYS) {
|
|
57
|
+
if (typeof resolved[key] !== 'boolean') {
|
|
58
|
+
throw new TypeError(`ptc-plus: ${key} must be a boolean`)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return resolved
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Validate one wall-clock ceiling against the platform timer limit.
|
|
66
|
+
* @param value - the configured maximum wall time in milliseconds.
|
|
67
|
+
* @returns the validated value.
|
|
68
|
+
*/
|
|
69
|
+
export function validateMaxWallMs(value) {
|
|
70
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
71
|
+
throw new TypeError('ptc-plus: maxWallMs must be a positive safe integer')
|
|
72
|
+
}
|
|
73
|
+
if (value > MAX_TIMER_DELAY_MS) {
|
|
74
|
+
throw new TypeError(`ptc-plus: maxWallMs must not exceed ${MAX_TIMER_DELAY_MS}`)
|
|
75
|
+
}
|
|
76
|
+
return value
|
|
77
|
+
}
|