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.
Files changed (42) hide show
  1. package/README.en.md +171 -0
  2. package/README.md +94 -67
  3. package/assets/dsh-ptc-plus-banner-en.webp +0 -0
  4. package/assets/ptc-plus-repair-en.png +0 -0
  5. package/assets/ptc-plus-settings-en.png +0 -0
  6. package/assets/ptc-plus-settings-zh.png +0 -0
  7. package/client.js +1211 -128
  8. package/docs/adr/0019-plugin-settings-and-kill-switch.md +20 -4
  9. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
  10. package/docs/adr/0022-stage-npm-releases-with-oidc.md +31 -0
  11. package/docs/architecture.md +10 -9
  12. package/docs/client-ui.md +30 -12
  13. package/docs/installation.md +13 -5
  14. package/docs/publishing.md +25 -1
  15. package/docs/runtime-reference.md +18 -12
  16. package/docs/value-wire.md +1 -1
  17. package/index.js +43 -12
  18. package/internal/cell-analysis.js +20 -3
  19. package/internal/cell-rewriter.js +36 -2
  20. package/internal/config-spec.js +192 -82
  21. package/internal/direct-surface-owner.js +77 -23
  22. package/internal/edit-transport-owner.js +10 -1
  23. package/internal/failure-reporting.js +15 -0
  24. package/internal/rejected-cell-editor.js +21 -12
  25. package/internal/repl-memory-projection.js +277 -0
  26. package/internal/run-code-description.js +34 -0
  27. package/internal/runtime-bridge-owner.js +63 -15
  28. package/internal/runtime-config.js +2 -0
  29. package/internal/session-cell-executor.js +14 -4
  30. package/internal/session-events.js +7 -0
  31. package/internal/session-journal.js +3 -2
  32. package/internal/session-log-view.js +2 -1
  33. package/internal/session-runtime.js +19 -2
  34. package/internal/session-state.js +83 -4
  35. package/internal/settings-compat.js +31 -0
  36. package/package.json +20 -13
  37. package/src/client-activity.js +472 -0
  38. package/src/client.js +536 -38
  39. package/README.zh.md +0 -144
  40. package/assets/ptc-plus-settings.png +0 -0
  41. /package/assets/{dsh-ptc-plus-banner.webp → dsh-ptc-plus-banner-zh.webp} +0 -0
  42. /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 = assembly?.sections?.find(section => section?.name === 'tools:code-only')
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
- ? 'code'
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
- ? 'code'
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 === 'code')) {
234
+ && (initialState.ownerProven || initialState.presentation === 'ptc')) {
223
235
  composition = captureComposition(agent, id, initialState.presentation)
224
236
  }
225
- if (composition?.presentation === 'code' && !editTransport.isInstalled(agent)) {
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 === 'code' && !editTransport.isInstalled(agent)) {
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 sessionCodeOnlyProjection = presentation === 'code' && id !== undefined
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 (sessionCodeOnlyProjection) {
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
- || (sessionCodeOnlyProjection && section?.name === 'tools:code-only'))
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 (sessionCodeOnlyProjection && section?.name === 'tools:code-only') {
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 === 'code'
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: sessionCodeOnlyProjection
317
+ tools: sessionPtcProjection
306
318
  ? directTools
307
- : directTools.map(tool => tool?.name === RUN_CODE ? adaptRunCodeSchema(tool) : tool),
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 === 'code'
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 === 'code'
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 !== 'code') return undefined
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 meta
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
- additionalProperties: false,
262
- properties: {
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
- [EXPECTED_TARGET_CALL_SEQ]: {
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 }