dsh-ptc-plus 0.2.3 → 0.3.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/README.en.md +171 -0
- package/README.md +94 -67
- package/assets/dsh-ptc-plus-banner-en.webp +0 -0
- package/assets/ptc-plus-repair-en.png +0 -0
- package/assets/ptc-plus-settings-en.png +0 -0
- package/assets/ptc-plus-settings-zh.png +0 -0
- package/client.js +1211 -128
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +20 -4
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
- package/docs/adr/0022-stage-npm-releases-with-oidc.md +31 -0
- package/docs/architecture.md +10 -9
- package/docs/client-ui.md +30 -12
- package/docs/installation.md +13 -5
- package/docs/publishing.md +25 -1
- package/docs/runtime-reference.md +18 -12
- package/docs/value-wire.md +1 -1
- package/index.js +43 -12
- package/internal/cell-analysis.js +20 -3
- package/internal/cell-rewriter.js +36 -2
- package/internal/config-spec.js +192 -82
- package/internal/direct-surface-owner.js +77 -23
- package/internal/edit-transport-owner.js +10 -1
- package/internal/failure-reporting.js +15 -0
- package/internal/rejected-cell-editor.js +21 -12
- package/internal/repl-memory-projection.js +277 -0
- package/internal/run-code-description.js +34 -0
- package/internal/runtime-bridge-owner.js +63 -15
- package/internal/runtime-config.js +2 -0
- package/internal/session-cell-executor.js +14 -4
- package/internal/session-events.js +7 -0
- package/internal/session-journal.js +3 -2
- package/internal/session-log-view.js +2 -1
- package/internal/session-runtime.js +19 -2
- package/internal/session-state.js +83 -4
- package/internal/settings-compat.js +31 -0
- package/package.json +20 -13
- package/src/client-activity.js +472 -0
- package/src/client.js +536 -38
- package/README.zh.md +0 -144
- package/assets/ptc-plus-settings.png +0 -0
- /package/assets/{dsh-ptc-plus-banner.webp → dsh-ptc-plus-banner-zh.webp} +0 -0
- /package/assets/{ptc-plus-repair.png → ptc-plus-repair-zh.png} +0 -0
|
@@ -6,11 +6,17 @@ import { projectProgramSdk } from './sdk-projection.js'
|
|
|
6
6
|
import { EDIT_RUN_CODE } from './edit-transport-owner.js'
|
|
7
7
|
import { RUN_CODE } from './runtime-bridge-owner.js'
|
|
8
8
|
import { isRecord } from './record-utils.js'
|
|
9
|
+
import { missingDescriptionPath } from './failure-reporting.js'
|
|
10
|
+
import {
|
|
11
|
+
generatedRunCodeExecutionArguments,
|
|
12
|
+
generatedRunCodeDescriptionMeta,
|
|
13
|
+
} from './run-code-description.js'
|
|
9
14
|
|
|
10
15
|
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
16
|
const RUN_CODE_CODE_DESCRIPTION = 'Code for the next REPL cell, parsed as the body of an async TypeScript function.'
|
|
12
17
|
const RUN_CODE_DESCRIPTION_DESCRIPTION = 'Short active-voice summary of what this cell does, 5-10 words (shown in the UI).'
|
|
13
18
|
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.'
|
|
19
|
+
const PTC_COLLAPSE_SECTION_NAMES = Object.freeze(['tools:ptc-only', 'tools:code-only'])
|
|
14
20
|
|
|
15
21
|
function adaptRunCodeSchema(tool) {
|
|
16
22
|
const parameters = tool.parameters
|
|
@@ -94,19 +100,21 @@ export function createCordisRecoveryPolicy(initiallyEnabled) {
|
|
|
94
100
|
function presentationState(assembly) {
|
|
95
101
|
const tools = Array.isArray(assembly?.tools) ? assembly.tools : []
|
|
96
102
|
if (!tools.some(tool => tool?.name === RUN_CODE)) {
|
|
97
|
-
return { presentation: 'native', ownerProven: false }
|
|
103
|
+
return { presentation: 'native', ownerProven: false, collapseSectionName: undefined }
|
|
98
104
|
}
|
|
99
|
-
const collapse =
|
|
105
|
+
const collapse = PTC_COLLAPSE_SECTION_NAMES
|
|
106
|
+
.map(name => assembly?.sections?.find(section => section?.name === name))
|
|
107
|
+
.find(section => section !== undefined)
|
|
100
108
|
if (collapse !== undefined) {
|
|
101
109
|
const presentation = typeof collapse.text === 'string' && collapse.text.trim().length > 0
|
|
102
|
-
? '
|
|
110
|
+
? 'ptc'
|
|
103
111
|
: 'both'
|
|
104
|
-
return { presentation, ownerProven: true }
|
|
112
|
+
return { presentation, ownerProven: true, collapseSectionName: collapse.name }
|
|
105
113
|
}
|
|
106
114
|
const presentation = tools.every(tool => tool?.name === RUN_CODE || tool?.name === EDIT_RUN_CODE)
|
|
107
|
-
? '
|
|
115
|
+
? 'ptc'
|
|
108
116
|
: 'both'
|
|
109
|
-
return { presentation, ownerProven: false }
|
|
117
|
+
return { presentation, ownerProven: false, collapseSectionName: undefined }
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
async function* bindCallPolicies(source, policy, calls) {
|
|
@@ -144,6 +152,7 @@ export function createDirectSurfaceOwner({
|
|
|
144
152
|
const sessions = new Map()
|
|
145
153
|
const cordisRecovery = createCordisRecoveryPolicy(runtimeConfig.cordisToolsEnabled)
|
|
146
154
|
let currentCanonicalizeToolCalls = canonicalizeToolCalls
|
|
155
|
+
let currentAutoDescribeRunCode = runtimeConfig.autoDescribeRunCode
|
|
147
156
|
const tipConfig = {
|
|
148
157
|
enabled: runtimeConfig.tipsEnabled,
|
|
149
158
|
cooldownMessages: runtimeConfig.tipCooldownMessages,
|
|
@@ -184,9 +193,9 @@ export function createDirectSurfaceOwner({
|
|
|
184
193
|
return undefined
|
|
185
194
|
}
|
|
186
195
|
|
|
187
|
-
const rememberRequest = (id, signal, presentation, nativeSchemas) => {
|
|
196
|
+
const rememberRequest = (id, signal, presentation, nativeSchemas, autoDescribeRunCode) => {
|
|
188
197
|
const owner = sessionOwner(id)
|
|
189
|
-
const request = { presentation, nativeSchemas, owner }
|
|
198
|
+
const request = { presentation, nativeSchemas, autoDescribeRunCode, owner }
|
|
190
199
|
owner.latestRequest = request
|
|
191
200
|
if (signal !== undefined) canonicalRequests.set(signal, request)
|
|
192
201
|
}
|
|
@@ -208,21 +217,24 @@ export function createDirectSurfaceOwner({
|
|
|
208
217
|
return Object.freeze({
|
|
209
218
|
reconfigure(nextConfig) {
|
|
210
219
|
currentCanonicalizeToolCalls = nextConfig.canonicalizeToolCalls
|
|
220
|
+
currentAutoDescribeRunCode = nextConfig.autoDescribeRunCode
|
|
211
221
|
cordisRecovery.reconfigure(nextConfig.cordisToolsEnabled)
|
|
212
222
|
tipConfig.enabled = nextConfig.tipsEnabled
|
|
213
223
|
tipConfig.cooldownMessages = nextConfig.tipCooldownMessages
|
|
214
224
|
tipConfig.escalationFailures = nextConfig.tipEscalationFailures
|
|
215
225
|
},
|
|
216
226
|
async assemble(initialAssembly, context, next) {
|
|
227
|
+
// Keep schema projection and later execution on one configuration snapshot.
|
|
228
|
+
const autoDescribeRunCode = currentAutoDescribeRunCode
|
|
217
229
|
const agent = context?.agent
|
|
218
230
|
const id = sessionId(agent)
|
|
219
231
|
const initialState = presentationState(initialAssembly)
|
|
220
232
|
let composition = compositions.get(agent)
|
|
221
233
|
if (composition === undefined && id !== undefined
|
|
222
|
-
&& (initialState.ownerProven || initialState.presentation === '
|
|
234
|
+
&& (initialState.ownerProven || initialState.presentation === 'ptc')) {
|
|
223
235
|
composition = captureComposition(agent, id, initialState.presentation)
|
|
224
236
|
}
|
|
225
|
-
if (composition?.presentation === '
|
|
237
|
+
if (composition?.presentation === 'ptc' && !editTransport.isInstalled(agent)) {
|
|
226
238
|
editTransport.ensureInstalled(agent)
|
|
227
239
|
}
|
|
228
240
|
|
|
@@ -235,7 +247,7 @@ export function createDirectSurfaceOwner({
|
|
|
235
247
|
if (composition === undefined && id !== undefined) {
|
|
236
248
|
const state = initialState.ownerProven ? initialState : completedState
|
|
237
249
|
composition = captureComposition(agent, id, state.presentation)
|
|
238
|
-
if (composition.presentation === '
|
|
250
|
+
if (composition.presentation === 'ptc' && !editTransport.isInstalled(agent)) {
|
|
239
251
|
editTransport.ensureInstalled(agent)
|
|
240
252
|
}
|
|
241
253
|
}
|
|
@@ -249,7 +261,7 @@ export function createDirectSurfaceOwner({
|
|
|
249
261
|
throw new Error(`ptc-plus: ${presentation} agent composition assembled without run_code`)
|
|
250
262
|
}
|
|
251
263
|
if (id !== undefined) {
|
|
252
|
-
rememberRequest(id, requestSignal, 'native', new Map())
|
|
264
|
+
rememberRequest(id, requestSignal, 'native', new Map(), autoDescribeRunCode)
|
|
253
265
|
}
|
|
254
266
|
return assembly
|
|
255
267
|
}
|
|
@@ -257,10 +269,10 @@ export function createDirectSurfaceOwner({
|
|
|
257
269
|
throw new Error('ptc-plus: native agent composition assembled with run_code')
|
|
258
270
|
}
|
|
259
271
|
const sessionPtc = presentation !== 'native' && id !== undefined
|
|
260
|
-
const
|
|
272
|
+
const sessionPtcProjection = presentation === 'ptc' && id !== undefined
|
|
261
273
|
const runCode = tools.find(tool => tool?.name === RUN_CODE)
|
|
262
274
|
let directTools = tools
|
|
263
|
-
if (
|
|
275
|
+
if (sessionPtcProjection) {
|
|
264
276
|
editTransport.ensureInstalled(agent)
|
|
265
277
|
directTools = [adaptRunCodeSchema(runCode), editRunCodeSchema()]
|
|
266
278
|
}
|
|
@@ -277,13 +289,13 @@ export function createDirectSurfaceOwner({
|
|
|
277
289
|
: assembly.contexts
|
|
278
290
|
const projectsSections = presentation !== 'native' && Array.isArray(assembly.sections)
|
|
279
291
|
&& assembly.sections.some(section => section?.name === 'tools:sdk'
|
|
280
|
-
|| (
|
|
292
|
+
|| (sessionPtcProjection && section?.name === completedState.collapseSectionName))
|
|
281
293
|
const sections = projectsSections
|
|
282
294
|
? assembly.sections.map(section => {
|
|
283
295
|
if (section?.name === 'tools:sdk') {
|
|
284
296
|
return { ...section, text: capabilitySdk(section.text) }
|
|
285
297
|
}
|
|
286
|
-
if (
|
|
298
|
+
if (sessionPtcProjection && section?.name === completedState.collapseSectionName) {
|
|
287
299
|
return { ...section, text: CODE_TRANSPORT_INSTRUCTION }
|
|
288
300
|
}
|
|
289
301
|
return section
|
|
@@ -291,20 +303,22 @@ export function createDirectSurfaceOwner({
|
|
|
291
303
|
: assembly.sections
|
|
292
304
|
|
|
293
305
|
if (id !== undefined) {
|
|
294
|
-
const nativeSchemas = presentation === '
|
|
306
|
+
const nativeSchemas = presentation === 'ptc'
|
|
295
307
|
? new Map(toolSchemasForAgent(agent)
|
|
296
308
|
.filter(schema => typeof schema?.name === 'string'
|
|
297
309
|
&& schema.name !== RUN_CODE && schema.name !== EDIT_RUN_CODE)
|
|
298
310
|
.map(schema => [schema.name, schema]))
|
|
299
311
|
: new Map()
|
|
300
|
-
rememberRequest(id, requestSignal, presentation, nativeSchemas)
|
|
312
|
+
rememberRequest(id, requestSignal, presentation, nativeSchemas, autoDescribeRunCode)
|
|
301
313
|
}
|
|
302
314
|
|
|
303
315
|
return {
|
|
304
316
|
...assembly,
|
|
305
|
-
tools:
|
|
317
|
+
tools: sessionPtcProjection
|
|
306
318
|
? directTools
|
|
307
|
-
: directTools.map(tool => tool?.name === RUN_CODE
|
|
319
|
+
: directTools.map(tool => tool?.name === RUN_CODE
|
|
320
|
+
? adaptRunCodeSchema(tool)
|
|
321
|
+
: tool),
|
|
308
322
|
sections,
|
|
309
323
|
contexts,
|
|
310
324
|
}
|
|
@@ -313,7 +327,7 @@ export function createDirectSurfaceOwner({
|
|
|
313
327
|
const optionSessionId = options.sessionId === undefined ? undefined : String(options.sessionId)
|
|
314
328
|
const policy = requestPolicy(options?.signal, optionSessionId)
|
|
315
329
|
if (policy === undefined || optionSessionId === undefined) return next()
|
|
316
|
-
const source = policy.presentation === '
|
|
330
|
+
const source = policy.presentation === 'ptc'
|
|
317
331
|
? canonicalizeToolCallStream(next(), {
|
|
318
332
|
tools: options.tools,
|
|
319
333
|
nativeSchemas: currentCanonicalizeToolCalls ? policy.nativeSchemas : new Map(),
|
|
@@ -330,16 +344,56 @@ export function createDirectSurfaceOwner({
|
|
|
330
344
|
const policy = executionPolicy(id, exec.callId)
|
|
331
345
|
?? requestPolicy(exec?.signal, id)
|
|
332
346
|
if (exec.name === EDIT_RUN_CODE) {
|
|
333
|
-
return policy?.presentation === '
|
|
347
|
+
return policy?.presentation === 'ptc'
|
|
334
348
|
? undefined
|
|
335
349
|
: rejection(`tool ${EDIT_RUN_CODE} is not declared for this request`)
|
|
336
350
|
}
|
|
337
351
|
if (exec.name === RUN_CODE || exec.parent !== undefined) {
|
|
338
352
|
return undefined
|
|
339
353
|
}
|
|
340
|
-
if (policy?.presentation !== '
|
|
354
|
+
if (policy?.presentation !== 'ptc') return undefined
|
|
341
355
|
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`)
|
|
342
356
|
},
|
|
357
|
+
executionArguments(exec) {
|
|
358
|
+
const id = sessionId(exec.agent)
|
|
359
|
+
const policy = executionPolicy(id, exec.callId)
|
|
360
|
+
?? requestPolicy(exec?.signal, id)
|
|
361
|
+
const originalArguments = exec.arguments
|
|
362
|
+
if (exec.name !== RUN_CODE || exec.parent !== undefined
|
|
363
|
+
|| policy?.autoDescribeRunCode !== true || policy.presentation === 'native'
|
|
364
|
+
|| !isRecord(originalArguments) || Object.hasOwn(originalArguments, 'description')
|
|
365
|
+
|| typeof originalArguments.code !== 'string') {
|
|
366
|
+
return originalArguments
|
|
367
|
+
}
|
|
368
|
+
return generatedRunCodeExecutionArguments(originalArguments)
|
|
369
|
+
},
|
|
370
|
+
argumentDiagnostic(exec, result) {
|
|
371
|
+
// DSH uses success-result identity to avoid re-running output projectors
|
|
372
|
+
// after this middleware's settlement scope has ended.
|
|
373
|
+
if (exec?.name !== RUN_CODE || result?.isError !== true) return result
|
|
374
|
+
const meta = generatedRunCodeDescriptionMeta(exec.arguments, result.meta)
|
|
375
|
+
const diagnosed = meta === result.meta
|
|
376
|
+
? result
|
|
377
|
+
: { ...result, ...(meta === undefined ? {} : { meta }) }
|
|
378
|
+
const missingPath = missingDescriptionPath(diagnosed.error)
|
|
379
|
+
if (missingPath === undefined) return diagnosed
|
|
380
|
+
const id = sessionId(exec.agent)
|
|
381
|
+
const policy = executionPolicy(id, exec.callId)
|
|
382
|
+
?? requestPolicy(exec?.signal, id)
|
|
383
|
+
const outer = missingPath === 'description'
|
|
384
|
+
&& policy?.autoDescribeRunCode !== true
|
|
385
|
+
&& !(isRecord(exec.arguments) && typeof exec.arguments.description === 'string')
|
|
386
|
+
const context = {
|
|
387
|
+
name: 'tools:ptc-plus-run-code-arguments',
|
|
388
|
+
text: outer
|
|
389
|
+
? 'The run_code outer transport arguments are invalid at JSON path $.description. Add a sibling string `description` field to run_code; a description nested inside a native-tool argument does not satisfy this outer requirement.'
|
|
390
|
+
: `A nested native-tool argument is missing required property "${missingPath}". Add a string description at that path in the native-tool arguments; the outer run_code description does not satisfy this nested requirement.`,
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
...diagnosed,
|
|
394
|
+
additionalContexts: [...diagnosed.additionalContexts ?? [], context],
|
|
395
|
+
}
|
|
396
|
+
},
|
|
343
397
|
handleResult(exec) {
|
|
344
398
|
const id = sessionId(exec.agent)
|
|
345
399
|
if (id === undefined || typeof exec.callId !== 'string') return
|
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
import { editTargetForCall, projectSessionLog } from './session-log-view.js'
|
|
21
21
|
import { RUN_CODE } from './runtime-bridge-owner.js'
|
|
22
22
|
import { isRecord } from './record-utils.js'
|
|
23
|
+
import {
|
|
24
|
+
validatedReplMemorySnapshot,
|
|
25
|
+
withReplMemorySnapshot,
|
|
26
|
+
} from './repl-memory-projection.js'
|
|
23
27
|
|
|
24
28
|
export const EDIT_RUN_CODE = 'edit_run_code'
|
|
25
29
|
|
|
@@ -52,6 +56,7 @@ function derivedEditResult(inner) {
|
|
|
52
56
|
export function createEditTransportOwner(ctx, {
|
|
53
57
|
durableReplay,
|
|
54
58
|
executeTentative,
|
|
59
|
+
presentationGeneration,
|
|
55
60
|
sessionId,
|
|
56
61
|
toolSchemasForAgent,
|
|
57
62
|
}) {
|
|
@@ -91,7 +96,9 @@ export function createEditTransportOwner(ctx, {
|
|
|
91
96
|
if (derived.recoveryBoundaries !== undefined) {
|
|
92
97
|
meta[RECOVERY_BOUNDARY_KEY] = normalizeRecoveryBoundaries(derived.recoveryBoundaries)
|
|
93
98
|
}
|
|
94
|
-
return
|
|
99
|
+
return derived.replMemory === undefined
|
|
100
|
+
? meta
|
|
101
|
+
: withReplMemorySnapshot(meta, derived.replMemory, presentationGeneration)
|
|
95
102
|
}
|
|
96
103
|
definition.output = {
|
|
97
104
|
schema: {
|
|
@@ -178,12 +185,14 @@ export function createEditTransportOwner(ctx, {
|
|
|
178
185
|
&& Object.hasOwn(inner.meta, RECOVERY_BOUNDARY_KEY)
|
|
179
186
|
? normalizeRecoveryBoundaries(inner.meta[RECOVERY_BOUNDARY_KEY])
|
|
180
187
|
: undefined
|
|
188
|
+
const replMemory = validatedReplMemorySnapshot(inner?.meta, presentationGeneration)
|
|
181
189
|
const value = derivedEditResult(inner)
|
|
182
190
|
const derived = {
|
|
183
191
|
targetCallSeq: target.callSeq,
|
|
184
192
|
journal,
|
|
185
193
|
rewrites,
|
|
186
194
|
recoveryBoundaries,
|
|
195
|
+
replMemory,
|
|
187
196
|
code: edited.code,
|
|
188
197
|
description: edited.description,
|
|
189
198
|
}
|
|
@@ -64,6 +64,21 @@ export function oneLineMessage(error) {
|
|
|
64
64
|
return firstLine(messageOf(error), 'Unknown error').replace(/\s+\(\d+:\d+\)$/, '')
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/** Return DSH's reported missing-description property path when it is actionable. */
|
|
68
|
+
export function missingDescriptionPath(error) {
|
|
69
|
+
const violations = messageOf(error).matchAll(/missing required property ["']([^"']+)["']/g)
|
|
70
|
+
for (const match of violations) {
|
|
71
|
+
const path = match[1]
|
|
72
|
+
if (path === 'description' || path.endsWith('.description')) return path
|
|
73
|
+
}
|
|
74
|
+
return undefined
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Identify DSH's stable missing-description validation fact without parsing arbitrary errors. */
|
|
78
|
+
export function hasMissingDescriptionError(error) {
|
|
79
|
+
return missingDescriptionPath(error) !== undefined
|
|
80
|
+
}
|
|
81
|
+
|
|
67
82
|
/** Keep only the newest log entries so an output-limit error cannot flood the model. */
|
|
68
83
|
export function limitLogs(logs) {
|
|
69
84
|
let bytes = 0
|
|
@@ -252,15 +252,29 @@ function assemble(source, replacements) {
|
|
|
252
252
|
return changed ? { code: chunks.join('') } : { error: 'edits must change at least one matched fragment' }
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
function editRunCodeParameterBranch(operation, operationSchema) {
|
|
256
|
+
return {
|
|
257
|
+
type: 'object',
|
|
258
|
+
additionalProperties: false,
|
|
259
|
+
properties: {
|
|
260
|
+
[operation]: operationSchema,
|
|
261
|
+
[EXPECTED_TARGET_CALL_SEQ]: {
|
|
262
|
+
type: 'integer', minimum: 0,
|
|
263
|
+
description: 'Optional target precondition copied from a validated diagnostic. The edit is rejected if the captured cell has another call sequence.',
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
required: [operation],
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
255
270
|
export function editRunCodeSchema() {
|
|
256
271
|
return {
|
|
257
272
|
name: 'edit_run_code',
|
|
258
273
|
description: 'Make a small exact or regular-expression change to the most recent eligible cell captured when this edit call is dispatched, then run the complete corrected cell. A successful edit becomes the next eligible cell. Use this only when replaying the whole cell is safe. If earlier code may already have caused an external effect, use a new run_code cell and its existing variables instead; this tool does not resume at the error location. Send exactly one atomic edits or regex_edits array, plus expected_target_call_seq only when a diagnostic supplies it to bind a validated repair to its rejected cell. Choose edits for a few unique literal fragments; choose regex_edits when one counted pattern covers repeated fragments. Every resolved range must be non-overlapping in the original cell.',
|
|
259
274
|
parameters: {
|
|
260
275
|
type: 'object',
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
edits: {
|
|
276
|
+
oneOf: [
|
|
277
|
+
editRunCodeParameterBranch('edits', {
|
|
264
278
|
type: 'array', minItems: 1, maxItems: EDIT_LIMITS.exactEdits,
|
|
265
279
|
description: 'Atomic exact replacements, all resolved against the original target cell.',
|
|
266
280
|
items: {
|
|
@@ -271,8 +285,8 @@ export function editRunCodeSchema() {
|
|
|
271
285
|
},
|
|
272
286
|
required: ['old_string', 'new_string'],
|
|
273
287
|
},
|
|
274
|
-
},
|
|
275
|
-
regex_edits
|
|
288
|
+
}),
|
|
289
|
+
editRunCodeParameterBranch('regex_edits', {
|
|
276
290
|
type: 'array', minItems: 1, maxItems: EDIT_LIMITS.regexEdits,
|
|
277
291
|
description: 'Atomic regular-expression replacements, all matched against the original target cell.',
|
|
278
292
|
items: {
|
|
@@ -285,13 +299,8 @@ export function editRunCodeSchema() {
|
|
|
285
299
|
},
|
|
286
300
|
required: ['pattern', 'flags', 'replacement', 'expected_matches'],
|
|
287
301
|
},
|
|
288
|
-
},
|
|
289
|
-
|
|
290
|
-
type: 'integer', minimum: 0,
|
|
291
|
-
description: 'Optional target precondition copied from a validated diagnostic. The edit is rejected if the captured cell has another call sequence.',
|
|
292
|
-
},
|
|
293
|
-
},
|
|
294
|
-
oneOf: [{ required: ['edits'] }, { required: ['regex_edits'] }],
|
|
302
|
+
}),
|
|
303
|
+
],
|
|
295
304
|
},
|
|
296
305
|
}
|
|
297
306
|
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { isRecord } from './record-utils.js'
|
|
2
|
+
|
|
3
|
+
export const REPL_MEMORY_KEY = 'ptcPlusRepl'
|
|
4
|
+
export const REPL_MEMORY_META_KEY = 'dshPtcPlusBindings'
|
|
5
|
+
|
|
6
|
+
const BINDING_KINDS = new Set(['variable', 'function', 'class', 'import'])
|
|
7
|
+
const MAX_BINDINGS = 128
|
|
8
|
+
const MAX_BINDING_NAME_LENGTH = 128
|
|
9
|
+
const MAX_DEFINITION_SOURCE_LENGTH = 1024
|
|
10
|
+
const MAX_DEFINITION_SOURCE_TOTAL_LENGTH = 16 * 1024
|
|
11
|
+
const MAX_PENDING_REPL_CALLS = 256
|
|
12
|
+
const MAX_CALL_ID_LENGTH = 512
|
|
13
|
+
const MAX_GENERATION_LENGTH = 128
|
|
14
|
+
const SNAPSHOT_FIELDS = new Set(['available', 'entries', 'total', 'omitted'])
|
|
15
|
+
const ENTRY_FIELDS = new Set(['name', 'kind', 'definition'])
|
|
16
|
+
const DEFINITION_FIELDS = new Set(['source', 'line', 'column'])
|
|
17
|
+
const MEMORY_META_FIELDS = new Set(['version', 'generation', 'memory'])
|
|
18
|
+
const PROJECTION_STATE_FIELDS = new Set(['generation', 'memory', 'pendingReplCalls'])
|
|
19
|
+
const PENDING_CALL_FIELDS = new Set(['callId', 'seq'])
|
|
20
|
+
const REPL_TOOL_NAMES = new Set(['run_code', 'edit_run_code'])
|
|
21
|
+
const REPL_MEMORY_META_VERSION = 3
|
|
22
|
+
|
|
23
|
+
function exactFields(value, fields) {
|
|
24
|
+
if (!isRecord(value)) return false
|
|
25
|
+
const keys = Reflect.ownKeys(value)
|
|
26
|
+
return keys.length === fields.size && keys.every(key => (
|
|
27
|
+
typeof key === 'string' && fields.has(key)
|
|
28
|
+
&& Object.prototype.propertyIsEnumerable.call(value, key)
|
|
29
|
+
))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const EMPTY_REPL_MEMORY = Object.freeze({
|
|
33
|
+
available: false,
|
|
34
|
+
entries: Object.freeze([]),
|
|
35
|
+
total: 0,
|
|
36
|
+
omitted: 0,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
export function unavailableReplMemorySnapshot() {
|
|
40
|
+
return EMPTY_REPL_MEMORY
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeGeneration(value) {
|
|
44
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > MAX_GENERATION_LENGTH) {
|
|
45
|
+
throw new Error('invalid dsh-ptc-plus REPL memory generation')
|
|
46
|
+
}
|
|
47
|
+
return value
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeDefinition(value) {
|
|
51
|
+
if (!exactFields(value, DEFINITION_FIELDS)
|
|
52
|
+
|| typeof value.source !== 'string' || value.source.length === 0
|
|
53
|
+
|| value.source.length > MAX_DEFINITION_SOURCE_LENGTH
|
|
54
|
+
|| !Number.isSafeInteger(value.line) || value.line < 1
|
|
55
|
+
|| !Number.isSafeInteger(value.column) || value.column < 1) {
|
|
56
|
+
throw new Error('invalid dsh-ptc-plus REPL binding definition')
|
|
57
|
+
}
|
|
58
|
+
return Object.freeze({ source: value.source, line: value.line, column: value.column })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeBinding(value) {
|
|
62
|
+
if (!exactFields(value, ENTRY_FIELDS) || typeof value.name !== 'string'
|
|
63
|
+
|| value.name.length === 0 || value.name.length > MAX_BINDING_NAME_LENGTH
|
|
64
|
+
|| !BINDING_KINDS.has(value.kind)) {
|
|
65
|
+
throw new Error('invalid dsh-ptc-plus REPL memory binding')
|
|
66
|
+
}
|
|
67
|
+
return Object.freeze({
|
|
68
|
+
name: value.name,
|
|
69
|
+
kind: value.kind,
|
|
70
|
+
definition: normalizeDefinition(value.definition),
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Produce bounded, value-independent presentation metadata from the live binding catalog. */
|
|
75
|
+
export function createReplMemorySnapshot(bindings) {
|
|
76
|
+
const all = Array.isArray(bindings) ? bindings : []
|
|
77
|
+
const candidates = []
|
|
78
|
+
const names = new Set()
|
|
79
|
+
for (const binding of all) {
|
|
80
|
+
try {
|
|
81
|
+
const normalized = normalizeBinding(binding)
|
|
82
|
+
if (names.has(normalized.name)) continue
|
|
83
|
+
names.add(normalized.name)
|
|
84
|
+
candidates.push(normalized)
|
|
85
|
+
} catch {}
|
|
86
|
+
}
|
|
87
|
+
const entries = []
|
|
88
|
+
let sourceLength = 0
|
|
89
|
+
for (const binding of candidates) {
|
|
90
|
+
if (entries.length >= MAX_BINDINGS) break
|
|
91
|
+
if (sourceLength + binding.definition.source.length > MAX_DEFINITION_SOURCE_TOTAL_LENGTH) continue
|
|
92
|
+
entries.push(binding)
|
|
93
|
+
sourceLength += binding.definition.source.length
|
|
94
|
+
}
|
|
95
|
+
return Object.freeze({
|
|
96
|
+
available: true,
|
|
97
|
+
entries: Object.freeze(entries),
|
|
98
|
+
total: candidates.length,
|
|
99
|
+
omitted: candidates.length - entries.length,
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Validate one complete session-projection value without accepting unknown fields. */
|
|
104
|
+
export function normalizeReplMemorySnapshot(value) {
|
|
105
|
+
if (!exactFields(value, SNAPSHOT_FIELDS) || typeof value.available !== 'boolean'
|
|
106
|
+
|| !Array.isArray(value.entries) || value.entries.length > MAX_BINDINGS
|
|
107
|
+
|| !Number.isSafeInteger(value.total) || value.total < 0
|
|
108
|
+
|| !Number.isSafeInteger(value.omitted) || value.omitted < 0
|
|
109
|
+
|| value.total !== value.entries.length + value.omitted) {
|
|
110
|
+
throw new Error('invalid dsh-ptc-plus REPL memory snapshot')
|
|
111
|
+
}
|
|
112
|
+
if (!value.available && (value.total !== 0 || value.entries.length !== 0)) {
|
|
113
|
+
throw new Error('unavailable dsh-ptc-plus REPL memory snapshot must be empty')
|
|
114
|
+
}
|
|
115
|
+
const names = new Set()
|
|
116
|
+
let sourceLength = 0
|
|
117
|
+
const entries = value.entries.map((entry) => {
|
|
118
|
+
const normalized = normalizeBinding(entry)
|
|
119
|
+
if (names.has(normalized.name)) {
|
|
120
|
+
throw new Error('invalid dsh-ptc-plus REPL memory binding')
|
|
121
|
+
}
|
|
122
|
+
sourceLength += normalized.definition.source.length
|
|
123
|
+
if (sourceLength > MAX_DEFINITION_SOURCE_TOTAL_LENGTH) {
|
|
124
|
+
throw new Error('dsh-ptc-plus REPL binding definitions exceed the presentation budget')
|
|
125
|
+
}
|
|
126
|
+
names.add(normalized.name)
|
|
127
|
+
return normalized
|
|
128
|
+
})
|
|
129
|
+
return Object.freeze({
|
|
130
|
+
available: value.available,
|
|
131
|
+
entries: Object.freeze(entries),
|
|
132
|
+
total: value.total,
|
|
133
|
+
omitted: value.omitted,
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function normalizeReplMemoryMetadata(value) {
|
|
138
|
+
if (!exactFields(value, MEMORY_META_FIELDS) || value.version !== REPL_MEMORY_META_VERSION) {
|
|
139
|
+
throw new Error('invalid dsh-ptc-plus REPL memory metadata')
|
|
140
|
+
}
|
|
141
|
+
return Object.freeze({
|
|
142
|
+
version: REPL_MEMORY_META_VERSION,
|
|
143
|
+
generation: normalizeGeneration(value.generation),
|
|
144
|
+
memory: normalizeReplMemorySnapshot(value.memory),
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Read current-owner presentation metadata without affecting tool settlement. */
|
|
149
|
+
export function validatedReplMemorySnapshot(meta, generation) {
|
|
150
|
+
if (!isRecord(meta) || !Object.hasOwn(meta, REPL_MEMORY_META_KEY)) return undefined
|
|
151
|
+
try {
|
|
152
|
+
const normalized = normalizeReplMemoryMetadata(meta[REPL_MEMORY_META_KEY])
|
|
153
|
+
return normalized.generation === normalizeGeneration(generation)
|
|
154
|
+
? normalized.memory
|
|
155
|
+
: undefined
|
|
156
|
+
} catch {
|
|
157
|
+
return undefined
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function projectionState(generation, memory, pendingReplCalls) {
|
|
162
|
+
return Object.freeze({
|
|
163
|
+
generation,
|
|
164
|
+
memory,
|
|
165
|
+
pendingReplCalls: pendingReplCalls === null
|
|
166
|
+
? null
|
|
167
|
+
: Object.freeze(pendingReplCalls),
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function emptyProjectionState(generation) {
|
|
172
|
+
return projectionState(normalizeGeneration(generation), EMPTY_REPL_MEMORY, [])
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function normalizeProjectionState(value, generation) {
|
|
176
|
+
if (!exactFields(value, PROJECTION_STATE_FIELDS)) {
|
|
177
|
+
throw new Error('invalid dsh-ptc-plus REPL memory projection state')
|
|
178
|
+
}
|
|
179
|
+
const storedGeneration = normalizeGeneration(value.generation)
|
|
180
|
+
const currentGeneration = normalizeGeneration(generation)
|
|
181
|
+
if (storedGeneration !== currentGeneration) return emptyProjectionState(currentGeneration)
|
|
182
|
+
const memory = normalizeReplMemorySnapshot(value.memory)
|
|
183
|
+
if (value.pendingReplCalls === null) {
|
|
184
|
+
if (memory.available) {
|
|
185
|
+
throw new Error('untracked dsh-ptc-plus REPL calls require unavailable memory')
|
|
186
|
+
}
|
|
187
|
+
return projectionState(currentGeneration, memory, null)
|
|
188
|
+
}
|
|
189
|
+
if (!Array.isArray(value.pendingReplCalls)
|
|
190
|
+
|| value.pendingReplCalls.length > MAX_PENDING_REPL_CALLS) {
|
|
191
|
+
throw new Error('invalid dsh-ptc-plus pending REPL calls')
|
|
192
|
+
}
|
|
193
|
+
let previousSeq
|
|
194
|
+
const callIds = new Set()
|
|
195
|
+
const pendingReplCalls = value.pendingReplCalls.map((pending) => {
|
|
196
|
+
if (!exactFields(pending, PENDING_CALL_FIELDS)
|
|
197
|
+
|| typeof pending.callId !== 'string' || pending.callId.length === 0
|
|
198
|
+
|| pending.callId.length > MAX_CALL_ID_LENGTH || callIds.has(pending.callId)
|
|
199
|
+
|| !Number.isSafeInteger(pending.seq) || pending.seq < 0
|
|
200
|
+
|| (previousSeq !== undefined && previousSeq >= pending.seq)) {
|
|
201
|
+
throw new Error('invalid dsh-ptc-plus pending REPL call')
|
|
202
|
+
}
|
|
203
|
+
callIds.add(pending.callId)
|
|
204
|
+
previousSeq = pending.seq
|
|
205
|
+
return Object.freeze({ callId: pending.callId, seq: pending.seq })
|
|
206
|
+
})
|
|
207
|
+
return projectionState(currentGeneration, memory, pendingReplCalls)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function eventCallId(value) {
|
|
211
|
+
return typeof value === 'string' && value.length > 0 && value.length <= MAX_CALL_ID_LENGTH
|
|
212
|
+
? value
|
|
213
|
+
: undefined
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function trackReplCall(state, event) {
|
|
217
|
+
if (!REPL_TOOL_NAMES.has(event.data?.name)) return state
|
|
218
|
+
const seq = event.seq
|
|
219
|
+
const callId = eventCallId(event.data?.callId)
|
|
220
|
+
if (!Number.isSafeInteger(seq) || seq < 0 || callId === undefined) return state
|
|
221
|
+
const pending = state.pendingReplCalls
|
|
222
|
+
if (pending === null || pending.some(call => call.callId === callId || call.seq === seq)) return state
|
|
223
|
+
if (pending.length >= MAX_PENDING_REPL_CALLS) {
|
|
224
|
+
return projectionState(state.generation, EMPTY_REPL_MEMORY, null)
|
|
225
|
+
}
|
|
226
|
+
return projectionState(state.generation, state.memory, [
|
|
227
|
+
...pending,
|
|
228
|
+
Object.freeze({ callId, seq }),
|
|
229
|
+
].sort((left, right) => left.seq - right.seq))
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function settleReplCall(state, event, generation) {
|
|
233
|
+
if (event.surfaceOp !== undefined && event.surfaceOp !== 'append') return state
|
|
234
|
+
const pending = state.pendingReplCalls
|
|
235
|
+
if (pending === null) return state
|
|
236
|
+
const callId = eventCallId(event.data?.message?.source?.callId)
|
|
237
|
+
if (callId === undefined) return state
|
|
238
|
+
const index = pending.findIndex(call => call.callId === callId)
|
|
239
|
+
if (index < 0) return state
|
|
240
|
+
const remaining = pending.filter((_call, pendingIndex) => pendingIndex !== index)
|
|
241
|
+
const memory = validatedReplMemorySnapshot(event.data?.meta, generation) ?? state.memory
|
|
242
|
+
return projectionState(state.generation, memory, remaining)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Merge the binding snapshot into private, model-invisible result presentation metadata. */
|
|
246
|
+
export function withReplMemorySnapshot(meta, snapshot, generation) {
|
|
247
|
+
const base = isRecord(meta) ? { ...meta } : meta === undefined ? {} : { value: meta }
|
|
248
|
+
base[REPL_MEMORY_META_KEY] = normalizeReplMemoryMetadata({
|
|
249
|
+
version: REPL_MEMORY_META_VERSION,
|
|
250
|
+
generation: normalizeGeneration(generation),
|
|
251
|
+
memory: snapshot,
|
|
252
|
+
})
|
|
253
|
+
return base
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Build the current runtime owner's value-independent session projection. */
|
|
257
|
+
export function createReplMemoryProjection(generation) {
|
|
258
|
+
const currentGeneration = normalizeGeneration(generation)
|
|
259
|
+
return Object.freeze({
|
|
260
|
+
key: REPL_MEMORY_KEY,
|
|
261
|
+
stateVersion: 3,
|
|
262
|
+
stateSchema: Object.freeze({
|
|
263
|
+
parse: value => normalizeProjectionState(value, currentGeneration),
|
|
264
|
+
}),
|
|
265
|
+
init: () => emptyProjectionState(currentGeneration),
|
|
266
|
+
apply(state, event) {
|
|
267
|
+
if (event?.type === 'session/end-seed') return emptyProjectionState(currentGeneration)
|
|
268
|
+
if (event?.type === 'tool/call') return trackReplCall(state, event)
|
|
269
|
+
if (event?.type === 'tool/result') return settleReplCall(state, event, currentGeneration)
|
|
270
|
+
return state
|
|
271
|
+
},
|
|
272
|
+
wire: Object.freeze({
|
|
273
|
+
viewSchema: Object.freeze({ parse: normalizeReplMemorySnapshot }),
|
|
274
|
+
view: state => state.memory,
|
|
275
|
+
}),
|
|
276
|
+
})
|
|
277
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const GENERATED_RUN_CODE_DESCRIPTION = 'Execute the next TypeScript cell in this session'
|
|
2
|
+
export const GENERATED_RUN_CODE_DESCRIPTION_KEY = 'dshPtcPlusRunCodeDescription'
|
|
3
|
+
|
|
4
|
+
// Keep presentation provenance outside the canonical JSON argument graph.
|
|
5
|
+
const generatedArguments = new WeakSet()
|
|
6
|
+
|
|
7
|
+
export function markGeneratedRunCodeArguments(argumentsValue) {
|
|
8
|
+
if (argumentsValue !== null
|
|
9
|
+
&& (typeof argumentsValue === 'object' || typeof argumentsValue === 'function')) {
|
|
10
|
+
generatedArguments.add(argumentsValue)
|
|
11
|
+
}
|
|
12
|
+
return argumentsValue
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function generatedRunCodeExecutionArguments(argumentsValue) {
|
|
16
|
+
markGeneratedRunCodeArguments(argumentsValue)
|
|
17
|
+
return markGeneratedRunCodeArguments({
|
|
18
|
+
...argumentsValue,
|
|
19
|
+
description: GENERATED_RUN_CODE_DESCRIPTION,
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function generatedRunCodeDescriptionMeta(argumentsValue, meta) {
|
|
24
|
+
if (!generatedArguments.has(argumentsValue)) return meta
|
|
25
|
+
const base = meta !== null && typeof meta === 'object' && !Array.isArray(meta)
|
|
26
|
+
? meta
|
|
27
|
+
: meta === undefined ? {} : { value: meta }
|
|
28
|
+
return {
|
|
29
|
+
...base,
|
|
30
|
+
[GENERATED_RUN_CODE_DESCRIPTION_KEY]: GENERATED_RUN_CODE_DESCRIPTION,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { GENERATED_RUN_CODE_DESCRIPTION }
|