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,326 @@
|
|
|
1
|
+
/** Own PTC direct presentation, prompt projection, and stream normalization. */
|
|
2
|
+
import { canonicalizeToolCallStream } from './tool-call-canonicalizer.js'
|
|
3
|
+
import { editRunCodeSchema } from './rejected-cell-editor.js'
|
|
4
|
+
import { sessionRuntimeContexts } from './runtime-contexts.js'
|
|
5
|
+
import { projectProgramSdk } from './sdk-projection.js'
|
|
6
|
+
import { EDIT_RUN_CODE } from './edit-transport-owner.js'
|
|
7
|
+
import { RUN_CODE } from './runtime-bridge-owner.js'
|
|
8
|
+
import { isRecord } from './record-utils.js'
|
|
9
|
+
|
|
10
|
+
const RUN_CODE_TOOL_DESCRIPTION = 'Evaluate the next TypeScript cell in this session-bound persistent REPL. Earlier top-level bindings remain available, so this call extends the current environment instead of creating a fresh one. Use `code` for the async-function body and `description` for its short UI summary. Successful image-bearing subtool results are attached after the cell.'
|
|
11
|
+
const RUN_CODE_CODE_DESCRIPTION = 'Code for the next REPL cell, parsed as the body of an async TypeScript function.'
|
|
12
|
+
const RUN_CODE_DESCRIPTION_DESCRIPTION = 'Short active-voice summary of what this cell does, 5-10 words (shown in the UI).'
|
|
13
|
+
const CODE_TRANSPORT_INSTRUCTION = '`run_code` and `edit_run_code` are the only tools callable directly. Call every native tool declared by the SDK from inside a program.'
|
|
14
|
+
|
|
15
|
+
function adaptRunCodeSchema(tool) {
|
|
16
|
+
const parameters = tool.parameters
|
|
17
|
+
const properties = isRecord(parameters) ? parameters.properties : undefined
|
|
18
|
+
const code = isRecord(properties) ? properties.code : undefined
|
|
19
|
+
const description = isRecord(properties) ? properties.description : undefined
|
|
20
|
+
if (!isRecord(parameters) || parameters.type !== 'object' || !isRecord(properties)
|
|
21
|
+
|| !isRecord(code) || code.type !== 'string'
|
|
22
|
+
|| !isRecord(description) || description.type !== 'string') {
|
|
23
|
+
throw new Error('ptc-plus: incompatible run_code schema; expected object parameters with string code and description properties')
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
...tool,
|
|
27
|
+
description: RUN_CODE_TOOL_DESCRIPTION,
|
|
28
|
+
parameters: {
|
|
29
|
+
...parameters,
|
|
30
|
+
properties: {
|
|
31
|
+
...properties,
|
|
32
|
+
code: { ...code, description: RUN_CODE_CODE_DESCRIPTION },
|
|
33
|
+
description: { ...description, description: RUN_CODE_DESCRIPTION_DESCRIPTION },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function capabilitySdk(nativeSdk) {
|
|
40
|
+
return `${projectProgramSdk(nativeSdk)}
|
|
41
|
+
|
|
42
|
+
## PTC Plus program capabilities
|
|
43
|
+
|
|
44
|
+
\`\`\`ts
|
|
45
|
+
declare class CapabilityExplorationError extends Error { readonly operation: "tree" | "find" | "inspect" }
|
|
46
|
+
declare const capabilities: {
|
|
47
|
+
tree(): Promise<Array<{ namespace: string; members: string[] }>>
|
|
48
|
+
find(query: string): Promise<Array<{ symbol: string; description?: string; completeness: string; effect: string; replay: string }>>
|
|
49
|
+
inspect(args?: { symbols?: string[]; budget?: number }): Promise<{ symbols: unknown[]; omitted: number; unknown: string[]; budget: number }>
|
|
50
|
+
}
|
|
51
|
+
\`\`\``
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function rejection(message) {
|
|
55
|
+
return {
|
|
56
|
+
isError: true,
|
|
57
|
+
content: [{ type: 'text', text: message }],
|
|
58
|
+
error: { message },
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function presentationState(assembly) {
|
|
63
|
+
const tools = Array.isArray(assembly?.tools) ? assembly.tools : []
|
|
64
|
+
if (!tools.some(tool => tool?.name === RUN_CODE)) {
|
|
65
|
+
return { presentation: 'native', ownerProven: false }
|
|
66
|
+
}
|
|
67
|
+
const collapse = assembly?.sections?.find(section => section?.name === 'tools:code-only')
|
|
68
|
+
if (collapse !== undefined) {
|
|
69
|
+
const presentation = typeof collapse.text === 'string' && collapse.text.trim().length > 0
|
|
70
|
+
? 'code'
|
|
71
|
+
: 'both'
|
|
72
|
+
return { presentation, ownerProven: true }
|
|
73
|
+
}
|
|
74
|
+
const presentation = tools.every(tool => tool?.name === RUN_CODE || tool?.name === EDIT_RUN_CODE)
|
|
75
|
+
? 'code'
|
|
76
|
+
: 'both'
|
|
77
|
+
return { presentation, ownerProven: false }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function* bindCallPolicies(source, policy, calls) {
|
|
81
|
+
const pending = new Map()
|
|
82
|
+
for await (const chunk of source) {
|
|
83
|
+
if (chunk?.type === 'tool-call-delta'
|
|
84
|
+
&& typeof chunk.id === 'string' && chunk.id.length > 0) {
|
|
85
|
+
pending.set(chunk.index, chunk.id)
|
|
86
|
+
} else if (chunk?.type === 'block-end' && chunk.block?.type === 'tool-call') {
|
|
87
|
+
const callId = chunk.block.id
|
|
88
|
+
if (typeof callId === 'string' && callId.length > 0) calls.set(callId, policy)
|
|
89
|
+
pending.delete(chunk.index)
|
|
90
|
+
} else if (chunk?.type === 'finish') {
|
|
91
|
+
for (const callId of pending.values()) calls.set(callId, policy)
|
|
92
|
+
pending.clear()
|
|
93
|
+
}
|
|
94
|
+
yield chunk
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function createDirectSurfaceOwner({
|
|
99
|
+
editTransport,
|
|
100
|
+
runtimeConfig,
|
|
101
|
+
canonicalizeToolCalls,
|
|
102
|
+
sessionId,
|
|
103
|
+
toolSchemasForAgent,
|
|
104
|
+
}) {
|
|
105
|
+
// Composition is anchored to Agent identity because DSH selects presentation
|
|
106
|
+
// once per composed agent. Request signals bind the exact assembly to stream
|
|
107
|
+
// execution, while session identity is the fallback for hosts that replace a
|
|
108
|
+
// signal between those stages. Calls retain the resolved request policy by id
|
|
109
|
+
// so a later assembly cannot reinterpret an in-flight dispatch.
|
|
110
|
+
const compositions = new Map()
|
|
111
|
+
const canonicalRequests = new WeakMap()
|
|
112
|
+
const sessions = new Map()
|
|
113
|
+
let currentCanonicalizeToolCalls = canonicalizeToolCalls
|
|
114
|
+
const tipConfig = {
|
|
115
|
+
enabled: runtimeConfig.tipsEnabled,
|
|
116
|
+
cooldownMessages: runtimeConfig.tipCooldownMessages,
|
|
117
|
+
escalationFailures: runtimeConfig.tipEscalationFailures,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const sessionOwner = (id) => {
|
|
121
|
+
let owner = sessions.get(id)
|
|
122
|
+
if (owner === undefined) {
|
|
123
|
+
owner = { active: true, calls: new Map(), id, latestRequest: undefined }
|
|
124
|
+
sessions.set(id, owner)
|
|
125
|
+
}
|
|
126
|
+
return owner
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const clearSession = (id) => {
|
|
130
|
+
const owner = sessions.get(id)
|
|
131
|
+
if (owner !== undefined) owner.active = false
|
|
132
|
+
sessions.delete(id)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const requestPolicy = (signal, id) => {
|
|
136
|
+
if (signal !== null && typeof signal === 'object') {
|
|
137
|
+
const request = canonicalRequests.get(signal)
|
|
138
|
+
return request?.owner.active === true && (id === undefined || request.owner.id === id)
|
|
139
|
+
? request
|
|
140
|
+
: undefined
|
|
141
|
+
}
|
|
142
|
+
return id === undefined ? undefined : sessions.get(id)?.latestRequest
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const executionPolicy = (id, callId) => {
|
|
146
|
+
if (id === undefined) return undefined
|
|
147
|
+
const owner = sessions.get(id)
|
|
148
|
+
if (typeof callId === 'string' && owner?.calls.has(callId)) {
|
|
149
|
+
return owner.calls.get(callId)
|
|
150
|
+
}
|
|
151
|
+
return undefined
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const rememberRequest = (id, signal, presentation, nativeSchemas) => {
|
|
155
|
+
const owner = sessionOwner(id)
|
|
156
|
+
const request = { presentation, nativeSchemas, owner }
|
|
157
|
+
owner.latestRequest = request
|
|
158
|
+
if (signal !== undefined) canonicalRequests.set(signal, request)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const captureComposition = (agent, id, presentation) => {
|
|
162
|
+
let composition = compositions.get(agent)
|
|
163
|
+
if (composition !== undefined) return composition
|
|
164
|
+
composition = { presentation, sessionId: id }
|
|
165
|
+
compositions.set(agent, composition)
|
|
166
|
+
return composition
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const clearCompositionsForSession = (id) => {
|
|
170
|
+
for (const [agent, composition] of compositions) {
|
|
171
|
+
if (composition.sessionId === id) compositions.delete(agent)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return Object.freeze({
|
|
176
|
+
reconfigure(nextConfig) {
|
|
177
|
+
currentCanonicalizeToolCalls = nextConfig.canonicalizeToolCalls
|
|
178
|
+
tipConfig.enabled = nextConfig.tipsEnabled
|
|
179
|
+
tipConfig.cooldownMessages = nextConfig.tipCooldownMessages
|
|
180
|
+
tipConfig.escalationFailures = nextConfig.tipEscalationFailures
|
|
181
|
+
},
|
|
182
|
+
async assemble(initialAssembly, context, next) {
|
|
183
|
+
const agent = context?.agent
|
|
184
|
+
const id = sessionId(agent)
|
|
185
|
+
const initialState = presentationState(initialAssembly)
|
|
186
|
+
let composition = compositions.get(agent)
|
|
187
|
+
if (composition === undefined && id !== undefined
|
|
188
|
+
&& (initialState.ownerProven || initialState.presentation === 'code')) {
|
|
189
|
+
composition = captureComposition(agent, id, initialState.presentation)
|
|
190
|
+
}
|
|
191
|
+
if (composition?.presentation === 'code' && !editTransport.isInstalled(agent)) {
|
|
192
|
+
editTransport.ensureInstalled(agent)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const assembly = await next()
|
|
196
|
+
const tools = assembly.tools
|
|
197
|
+
if (!Array.isArray(tools)) {
|
|
198
|
+
throw new Error('ptc-plus: incompatible prompt assembly; expected a tools array')
|
|
199
|
+
}
|
|
200
|
+
const completedState = presentationState(assembly)
|
|
201
|
+
if (composition === undefined && id !== undefined) {
|
|
202
|
+
const state = initialState.ownerProven ? initialState : completedState
|
|
203
|
+
composition = captureComposition(agent, id, state.presentation)
|
|
204
|
+
if (composition.presentation === 'code' && !editTransport.isInstalled(agent)) {
|
|
205
|
+
editTransport.ensureInstalled(agent)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const presentation = composition?.presentation
|
|
209
|
+
?? (initialState.ownerProven ? initialState.presentation : completedState.presentation)
|
|
210
|
+
const requestSignal = context?.signal !== null && typeof context?.signal === 'object'
|
|
211
|
+
? context.signal
|
|
212
|
+
: undefined
|
|
213
|
+
if (!tools.some(tool => tool?.name === RUN_CODE)) {
|
|
214
|
+
if (presentation !== 'native') {
|
|
215
|
+
throw new Error(`ptc-plus: ${presentation} agent composition assembled without run_code`)
|
|
216
|
+
}
|
|
217
|
+
if (id !== undefined) {
|
|
218
|
+
rememberRequest(id, requestSignal, 'native', new Map())
|
|
219
|
+
}
|
|
220
|
+
return assembly
|
|
221
|
+
}
|
|
222
|
+
if (presentation === 'native') {
|
|
223
|
+
throw new Error('ptc-plus: native agent composition assembled with run_code')
|
|
224
|
+
}
|
|
225
|
+
const sessionPtc = presentation !== 'native' && id !== undefined
|
|
226
|
+
const sessionCodeOnlyProjection = presentation === 'code' && id !== undefined
|
|
227
|
+
const runCode = tools.find(tool => tool?.name === RUN_CODE)
|
|
228
|
+
let directTools = tools
|
|
229
|
+
if (sessionCodeOnlyProjection) {
|
|
230
|
+
editTransport.ensureInstalled(agent)
|
|
231
|
+
directTools = [adaptRunCodeSchema(runCode), editRunCodeSchema()]
|
|
232
|
+
}
|
|
233
|
+
const runtimeContexts = sessionPtc
|
|
234
|
+
? sessionRuntimeContexts(agent, tipConfig)
|
|
235
|
+
: { contexts: [] }
|
|
236
|
+
const contexts = sessionPtc && Array.isArray(assembly.contexts)
|
|
237
|
+
&& runtimeContexts.contexts.length > 0
|
|
238
|
+
? [...assembly.contexts, ...runtimeContexts.contexts]
|
|
239
|
+
: assembly.contexts
|
|
240
|
+
const projectsSections = presentation !== 'native' && Array.isArray(assembly.sections)
|
|
241
|
+
&& assembly.sections.some(section => section?.name === 'tools:sdk'
|
|
242
|
+
|| (sessionCodeOnlyProjection && section?.name === 'tools:code-only'))
|
|
243
|
+
const sections = projectsSections
|
|
244
|
+
? assembly.sections.map(section => {
|
|
245
|
+
if (section?.name === 'tools:sdk') {
|
|
246
|
+
return { ...section, text: capabilitySdk(section.text) }
|
|
247
|
+
}
|
|
248
|
+
if (sessionCodeOnlyProjection && section?.name === 'tools:code-only') {
|
|
249
|
+
return { ...section, text: CODE_TRANSPORT_INSTRUCTION }
|
|
250
|
+
}
|
|
251
|
+
return section
|
|
252
|
+
})
|
|
253
|
+
: assembly.sections
|
|
254
|
+
|
|
255
|
+
if (id !== undefined) {
|
|
256
|
+
const nativeSchemas = presentation === 'code'
|
|
257
|
+
? new Map(toolSchemasForAgent(agent)
|
|
258
|
+
.filter(schema => typeof schema?.name === 'string'
|
|
259
|
+
&& schema.name !== RUN_CODE && schema.name !== EDIT_RUN_CODE)
|
|
260
|
+
.map(schema => [schema.name, schema]))
|
|
261
|
+
: new Map()
|
|
262
|
+
rememberRequest(id, requestSignal, presentation, nativeSchemas)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
...assembly,
|
|
267
|
+
tools: sessionCodeOnlyProjection
|
|
268
|
+
? directTools
|
|
269
|
+
: directTools.map(tool => tool?.name === RUN_CODE ? adaptRunCodeSchema(tool) : tool),
|
|
270
|
+
sections,
|
|
271
|
+
contexts,
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
stream(options, next) {
|
|
275
|
+
const optionSessionId = options.sessionId === undefined ? undefined : String(options.sessionId)
|
|
276
|
+
const policy = requestPolicy(options?.signal, optionSessionId)
|
|
277
|
+
if (policy === undefined || optionSessionId === undefined) return next()
|
|
278
|
+
const source = policy.presentation === 'code'
|
|
279
|
+
? canonicalizeToolCallStream(next(), {
|
|
280
|
+
tools: options.tools,
|
|
281
|
+
nativeSchemas: currentCanonicalizeToolCalls ? policy.nativeSchemas : new Map(),
|
|
282
|
+
editToolName: EDIT_RUN_CODE,
|
|
283
|
+
})
|
|
284
|
+
: next()
|
|
285
|
+
return bindCallPolicies(source, policy, policy.owner.calls)
|
|
286
|
+
},
|
|
287
|
+
executionRejection(exec) {
|
|
288
|
+
if (exec.name === EDIT_RUN_CODE && exec.parent !== undefined) {
|
|
289
|
+
return rejection(`tool ${EDIT_RUN_CODE} is only callable directly in PTC mode; call native tools from inside run_code`)
|
|
290
|
+
}
|
|
291
|
+
const id = sessionId(exec.agent)
|
|
292
|
+
const policy = executionPolicy(id, exec.callId)
|
|
293
|
+
?? requestPolicy(exec?.signal, id)
|
|
294
|
+
if (exec.name === EDIT_RUN_CODE) {
|
|
295
|
+
return policy?.presentation === 'code'
|
|
296
|
+
? undefined
|
|
297
|
+
: rejection(`tool ${EDIT_RUN_CODE} is not declared for this request`)
|
|
298
|
+
}
|
|
299
|
+
if (exec.name === RUN_CODE || exec.parent !== undefined) {
|
|
300
|
+
return undefined
|
|
301
|
+
}
|
|
302
|
+
if (policy?.presentation !== 'code') return undefined
|
|
303
|
+
return rejection(`tool ${exec.name} is not a direct PTC tool; use run_code or edit_run_code directly, and call native tools from inside run_code`)
|
|
304
|
+
},
|
|
305
|
+
handleResult(exec) {
|
|
306
|
+
const id = sessionId(exec.agent)
|
|
307
|
+
if (id === undefined || typeof exec.callId !== 'string') return
|
|
308
|
+
sessions.get(id)?.calls.delete(exec.callId)
|
|
309
|
+
},
|
|
310
|
+
disposeAgent(agent) {
|
|
311
|
+
const id = sessionId(agent)
|
|
312
|
+
compositions.delete(agent)
|
|
313
|
+
if (id !== undefined) clearSession(id)
|
|
314
|
+
},
|
|
315
|
+
disposeSession(session) {
|
|
316
|
+
const id = String(session.id)
|
|
317
|
+
clearCompositionsForSession(id)
|
|
318
|
+
clearSession(id)
|
|
319
|
+
},
|
|
320
|
+
dispose() {
|
|
321
|
+
compositions.clear()
|
|
322
|
+
for (const owner of sessions.values()) owner.active = false
|
|
323
|
+
sessions.clear()
|
|
324
|
+
},
|
|
325
|
+
})
|
|
326
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/** Own the scoped edit tool, target claims, derived metadata, and paired disposers. */
|
|
2
|
+
import {
|
|
3
|
+
DERIVED_RUN_KEY,
|
|
4
|
+
EDIT_TARGET_KEY,
|
|
5
|
+
JOURNAL_KEY,
|
|
6
|
+
RECOVERY_BOUNDARY_KEY,
|
|
7
|
+
REWRITES_KEY,
|
|
8
|
+
derivedEditResultsEqual,
|
|
9
|
+
liveToolCallSeq,
|
|
10
|
+
normalizeJournal,
|
|
11
|
+
normalizeRecoveryBoundaries,
|
|
12
|
+
normalizeRewrites,
|
|
13
|
+
validatedRewrites,
|
|
14
|
+
} from './session-journal.js'
|
|
15
|
+
import { editRejectedCell, editRunCodeSchema } from './rejected-cell-editor.js'
|
|
16
|
+
import { editTargetForCall, projectSessionLog } from './session-log-view.js'
|
|
17
|
+
import { RUN_CODE } from './runtime-bridge-owner.js'
|
|
18
|
+
import { isRecord } from './record-utils.js'
|
|
19
|
+
|
|
20
|
+
export const EDIT_RUN_CODE = 'edit_run_code'
|
|
21
|
+
|
|
22
|
+
function unavailableDerivedResult(inner) {
|
|
23
|
+
if (inner?.isError !== true) {
|
|
24
|
+
throw new Error('ptc-plus: derived run_code result did not contain a valid execution journal')
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
edited: false,
|
|
28
|
+
error: inner.error?.message ?? 'derived run_code did not enter the runtime',
|
|
29
|
+
logs: [],
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function derivedEditResult(inner) {
|
|
34
|
+
const result = {
|
|
35
|
+
edited: true,
|
|
36
|
+
logs: Array.isArray(inner?.value?.logs) ? inner.value.logs : [],
|
|
37
|
+
}
|
|
38
|
+
if (inner?.isError === true) {
|
|
39
|
+
result.error = inner.error?.message ?? 'derived run_code execution failed'
|
|
40
|
+
} else if (inner?.value !== undefined && !isRecord(inner.value)) {
|
|
41
|
+
result.value = inner.value
|
|
42
|
+
} else if (inner?.value?.result !== undefined) {
|
|
43
|
+
result.value = inner.value.result
|
|
44
|
+
}
|
|
45
|
+
return result
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createEditTransportOwner(ctx, {
|
|
49
|
+
durableReplay,
|
|
50
|
+
executeTentative,
|
|
51
|
+
sessionId,
|
|
52
|
+
toolSchemasForAgent,
|
|
53
|
+
}) {
|
|
54
|
+
let currentDurableReplay = durableReplay
|
|
55
|
+
const editExecutionMetadata = new WeakMap()
|
|
56
|
+
const editClaims = new Map()
|
|
57
|
+
const pendingSettlements = new Map()
|
|
58
|
+
const installedScopes = new Map()
|
|
59
|
+
|
|
60
|
+
const releaseClaim = (id, targetCallSeq) => {
|
|
61
|
+
const claims = editClaims.get(id)
|
|
62
|
+
claims?.delete(targetCallSeq)
|
|
63
|
+
if (claims?.size === 0) editClaims.delete(id)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const discardSettlements = (id) => {
|
|
67
|
+
for (const [exec, pending] of pendingSettlements) {
|
|
68
|
+
if (id !== undefined && pending.id !== id) continue
|
|
69
|
+
pendingSettlements.delete(exec)
|
|
70
|
+
pending.finalize(false)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const definition = editRunCodeSchema()
|
|
75
|
+
const derivedMetadata = (derived) => {
|
|
76
|
+
const meta = {
|
|
77
|
+
[JOURNAL_KEY]: normalizeJournal(derived.journal),
|
|
78
|
+
[EDIT_TARGET_KEY]: { targetCallSeq: derived.targetCallSeq },
|
|
79
|
+
[DERIVED_RUN_KEY]: {
|
|
80
|
+
code: derived.code,
|
|
81
|
+
description: derived.description,
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
if (derived.rewrites !== undefined) {
|
|
85
|
+
meta[REWRITES_KEY] = normalizeRewrites(derived.rewrites)
|
|
86
|
+
}
|
|
87
|
+
if (derived.recoveryBoundaries !== undefined) {
|
|
88
|
+
meta[RECOVERY_BOUNDARY_KEY] = normalizeRecoveryBoundaries(derived.recoveryBoundaries)
|
|
89
|
+
}
|
|
90
|
+
return meta
|
|
91
|
+
}
|
|
92
|
+
definition.output = {
|
|
93
|
+
schema: {
|
|
94
|
+
type: 'object',
|
|
95
|
+
additionalProperties: false,
|
|
96
|
+
properties: {
|
|
97
|
+
edited: { type: 'boolean' },
|
|
98
|
+
reason: { type: 'string' },
|
|
99
|
+
error: { type: 'string' },
|
|
100
|
+
value: {},
|
|
101
|
+
logs: { type: 'array', items: { type: 'string' } },
|
|
102
|
+
},
|
|
103
|
+
required: ['edited'],
|
|
104
|
+
},
|
|
105
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value ?? {}) }],
|
|
106
|
+
presentationMeta: (args) => {
|
|
107
|
+
const meta = {}
|
|
108
|
+
if (!isRecord(args)) return meta
|
|
109
|
+
const derived = editExecutionMetadata.get(args)
|
|
110
|
+
if (derived === undefined) return meta
|
|
111
|
+
return derivedMetadata(derived)
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
definition.execute = async (args, exec) => {
|
|
115
|
+
const agent = exec?.agent
|
|
116
|
+
const callId = typeof exec?.callId === 'string' ? exec.callId : undefined
|
|
117
|
+
const persistedTargetCallSeq = liveToolCallSeq(agent?.session, callId, EDIT_RUN_CODE)
|
|
118
|
+
if (persistedTargetCallSeq === undefined && projectSessionLog(agent).editableRun !== undefined) {
|
|
119
|
+
throw new Error('ptc-plus: edit_run_code requires a unique persisted tool/call event')
|
|
120
|
+
}
|
|
121
|
+
const target = persistedTargetCallSeq === undefined
|
|
122
|
+
? undefined
|
|
123
|
+
: editTargetForCall(agent, callId, persistedTargetCallSeq)
|
|
124
|
+
if (target?.source === undefined || target.callSeq === undefined) {
|
|
125
|
+
return { edited: false, reason: 'no run_code cell is currently eligible for safe editing' }
|
|
126
|
+
}
|
|
127
|
+
const edited = editRejectedCell(args, target.source)
|
|
128
|
+
if (!edited.edited) return edited
|
|
129
|
+
if (typeof ctx.tools.execute !== 'function') {
|
|
130
|
+
throw new Error('ptc-plus: DSH tools.execute is required for derived edit execution')
|
|
131
|
+
}
|
|
132
|
+
const persistedCallSeq = currentDurableReplay ? persistedTargetCallSeq : undefined
|
|
133
|
+
const id = sessionId(agent)
|
|
134
|
+
let claims = editClaims.get(id)
|
|
135
|
+
if (claims === undefined) {
|
|
136
|
+
claims = new Map()
|
|
137
|
+
editClaims.set(id, claims)
|
|
138
|
+
} else {
|
|
139
|
+
for (const [callSeq, phase] of claims) {
|
|
140
|
+
if (phase === 'settled' && callSeq !== target.callSeq) claims.delete(callSeq)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (claims.has(target.callSeq)) {
|
|
144
|
+
return { edited: false, reason: 'the current edit target already has a derived execution' }
|
|
145
|
+
}
|
|
146
|
+
claims.set(target.callSeq, 'executing')
|
|
147
|
+
let retainClaim = false
|
|
148
|
+
let finalizeTentative
|
|
149
|
+
try {
|
|
150
|
+
const dispatch = () => ctx.tools.execute({
|
|
151
|
+
callId: `${String(exec.callId)}:derived`,
|
|
152
|
+
name: RUN_CODE,
|
|
153
|
+
arguments: { code: edited.code, description: edited.description },
|
|
154
|
+
agent,
|
|
155
|
+
signal: exec.signal,
|
|
156
|
+
})
|
|
157
|
+
const tentative = await executeTentative(persistedCallSeq, dispatch)
|
|
158
|
+
const inner = tentative.result
|
|
159
|
+
finalizeTentative = tentative.finalize
|
|
160
|
+
const journalValue = isRecord(inner?.meta) && Object.hasOwn(inner.meta, JOURNAL_KEY)
|
|
161
|
+
? inner.meta[JOURNAL_KEY]
|
|
162
|
+
: undefined
|
|
163
|
+
const journal = journalValue === undefined ? undefined : normalizeJournal(journalValue)
|
|
164
|
+
if (journal === undefined || journal.status === 'noop') return unavailableDerivedResult(inner)
|
|
165
|
+
const rewrites = validatedRewrites(inner.meta)
|
|
166
|
+
const recoveryBoundaries = isRecord(inner?.meta)
|
|
167
|
+
&& Object.hasOwn(inner.meta, RECOVERY_BOUNDARY_KEY)
|
|
168
|
+
? normalizeRecoveryBoundaries(inner.meta[RECOVERY_BOUNDARY_KEY])
|
|
169
|
+
: undefined
|
|
170
|
+
const value = derivedEditResult(inner)
|
|
171
|
+
const derived = {
|
|
172
|
+
targetCallSeq: target.callSeq,
|
|
173
|
+
journal,
|
|
174
|
+
rewrites,
|
|
175
|
+
recoveryBoundaries,
|
|
176
|
+
code: edited.code,
|
|
177
|
+
description: edited.description,
|
|
178
|
+
}
|
|
179
|
+
editExecutionMetadata.set(args, derived)
|
|
180
|
+
claims.set(target.callSeq, 'settled')
|
|
181
|
+
pendingSettlements.set(exec, {
|
|
182
|
+
id,
|
|
183
|
+
targetCallSeq: target.callSeq,
|
|
184
|
+
expectedMeta: derivedMetadata(derived),
|
|
185
|
+
finalize: finalizeTentative,
|
|
186
|
+
})
|
|
187
|
+
finalizeTentative = undefined
|
|
188
|
+
retainClaim = true
|
|
189
|
+
return value
|
|
190
|
+
} finally {
|
|
191
|
+
finalizeTentative?.(false)
|
|
192
|
+
if (!retainClaim) releaseClaim(id, target.callSeq)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const uninstall = (agent) => {
|
|
197
|
+
const installed = installedScopes.get(agent)
|
|
198
|
+
if (installed === undefined) return
|
|
199
|
+
installedScopes.delete(agent)
|
|
200
|
+
installed.dispose()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return Object.freeze({
|
|
204
|
+
definition,
|
|
205
|
+
reconfigure(nextConfig) {
|
|
206
|
+
currentDurableReplay = nextConfig.durableReplay
|
|
207
|
+
},
|
|
208
|
+
isInstalled(agent) {
|
|
209
|
+
return installedScopes.has(agent)
|
|
210
|
+
},
|
|
211
|
+
ensureInstalled(agent) {
|
|
212
|
+
const installed = installedScopes.get(agent)
|
|
213
|
+
if (installed !== undefined) return installed.definition
|
|
214
|
+
const liveSchemas = typeof toolSchemasForAgent === 'function'
|
|
215
|
+
? toolSchemasForAgent(agent)
|
|
216
|
+
: []
|
|
217
|
+
if (Array.isArray(liveSchemas)
|
|
218
|
+
&& liveSchemas.some(schema => schema?.name === EDIT_RUN_CODE)) {
|
|
219
|
+
throw new Error('ptc-plus: cannot install edit_run_code; the agent scope already owns that tool name')
|
|
220
|
+
}
|
|
221
|
+
const scopedTools = agent?.ctx?.tools
|
|
222
|
+
if (typeof scopedTools?.register !== 'function' || typeof scopedTools.presentAs !== 'function') {
|
|
223
|
+
throw new Error('ptc-plus: DSH agent-scoped tools.register and tools.presentAs are required to expose edit_run_code')
|
|
224
|
+
}
|
|
225
|
+
const disposeRegistration = scopedTools.register(definition)
|
|
226
|
+
if (typeof disposeRegistration !== 'function') {
|
|
227
|
+
throw new Error('ptc-plus: DSH tools.register did not return a disposer')
|
|
228
|
+
}
|
|
229
|
+
let disposePresentation
|
|
230
|
+
try {
|
|
231
|
+
disposePresentation = scopedTools.presentAs('both')
|
|
232
|
+
if (typeof disposePresentation !== 'function') {
|
|
233
|
+
throw new Error('ptc-plus: DSH tools.presentAs did not return a disposer')
|
|
234
|
+
}
|
|
235
|
+
} catch (error) {
|
|
236
|
+
disposeRegistration()
|
|
237
|
+
throw error
|
|
238
|
+
}
|
|
239
|
+
let active = true
|
|
240
|
+
const dispose = () => {
|
|
241
|
+
if (!active) return
|
|
242
|
+
active = false
|
|
243
|
+
disposePresentation()
|
|
244
|
+
disposeRegistration()
|
|
245
|
+
}
|
|
246
|
+
installedScopes.set(agent, { definition, dispose, sessionId: sessionId(agent) })
|
|
247
|
+
return definition
|
|
248
|
+
},
|
|
249
|
+
handleResult(exec, result) {
|
|
250
|
+
const pending = pendingSettlements.get(exec)
|
|
251
|
+
pendingSettlements.delete(exec)
|
|
252
|
+
if (pending === undefined) return
|
|
253
|
+
const persisted = derivedEditResultsEqual(
|
|
254
|
+
result?.meta,
|
|
255
|
+
pending.expectedMeta,
|
|
256
|
+
pending.targetCallSeq,
|
|
257
|
+
)
|
|
258
|
+
pending.finalize(persisted)
|
|
259
|
+
if (persisted) return
|
|
260
|
+
releaseClaim(pending.id, pending.targetCallSeq)
|
|
261
|
+
},
|
|
262
|
+
disposeAgent(agent) {
|
|
263
|
+
uninstall(agent)
|
|
264
|
+
const id = sessionId(agent)
|
|
265
|
+
if (id !== undefined) {
|
|
266
|
+
discardSettlements(id)
|
|
267
|
+
editClaims.delete(id)
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
disposeSession(session) {
|
|
271
|
+
const id = String(session.id)
|
|
272
|
+
for (const [agent, installed] of installedScopes) {
|
|
273
|
+
if (installed.sessionId !== id) continue
|
|
274
|
+
installedScopes.delete(agent)
|
|
275
|
+
installed.dispose()
|
|
276
|
+
}
|
|
277
|
+
discardSettlements(id)
|
|
278
|
+
editClaims.delete(id)
|
|
279
|
+
},
|
|
280
|
+
dispose() {
|
|
281
|
+
discardSettlements()
|
|
282
|
+
editClaims.clear()
|
|
283
|
+
for (const installed of installedScopes.values()) installed.dispose()
|
|
284
|
+
installedScopes.clear()
|
|
285
|
+
},
|
|
286
|
+
})
|
|
287
|
+
}
|