dsh-ptc-plus 0.4.3 → 0.4.10

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 (66) hide show
  1. package/README.en.md +15 -3
  2. package/README.md +15 -3
  3. package/client.js +1157 -533
  4. package/cordis.patch.yml +12 -2
  5. package/docs/README.md +2 -0
  6. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +60 -4
  7. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +5 -1
  8. package/docs/adr/0019-plugin-settings-and-kill-switch.md +14 -3
  9. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
  10. package/docs/adr/0023-global-user-bindings.md +1 -1
  11. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +82 -18
  12. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +3 -3
  13. package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +27 -0
  14. package/docs/adr/0033-gate-client-visual-usability-in-a-browser.md +23 -0
  15. package/docs/architecture.md +6 -4
  16. package/docs/binding-review-placement-design.md +29 -21
  17. package/docs/client-ui.md +20 -8
  18. package/docs/client-visual-contract.md +48 -0
  19. package/docs/durability-design.md +12 -0
  20. package/docs/installation.md +48 -9
  21. package/docs/publishing.md +4 -4
  22. package/docs/release-notes.md +109 -0
  23. package/docs/runtime-reference.md +8 -2
  24. package/docs/user-bindings.en.md +2 -2
  25. package/docs/user-bindings.md +3 -3
  26. package/docs/verification-optimization-work.md +26 -0
  27. package/docs/verification.md +7 -1
  28. package/index.js +28 -7
  29. package/internal/compiler-platform.js +5 -3
  30. package/internal/compiler-service.js +2 -9
  31. package/internal/cordis-tools-owner.js +75 -16
  32. package/internal/direct-surface-owner.js +333 -7
  33. package/internal/edit-transport-owner.js +8 -5
  34. package/internal/execution-provider-owner.js +157 -0
  35. package/internal/execution-seam-compat.js +69 -113
  36. package/internal/isolated-worker.js +12 -8
  37. package/internal/recovery-tips.js +50 -21
  38. package/internal/rejected-cell-editor.js +11 -21
  39. package/internal/runtime-bridge-owner.js +88 -57
  40. package/internal/runtime-contexts.js +13 -13
  41. package/internal/session-events.js +40 -4
  42. package/internal/session-journal-recovery.js +678 -181
  43. package/internal/session-log-view.js +189 -43
  44. package/internal/session-runtime.js +58 -10
  45. package/internal/settings-compat.js +12 -2
  46. package/internal/user-bindings-owner.js +6 -5
  47. package/locale/en.json +6 -0
  48. package/locale/zh.json +6 -0
  49. package/package.json +18 -13
  50. package/src/client-authoring-view.js +222 -141
  51. package/src/client-binding-review.js +2 -0
  52. package/src/client-code-editor.js +6 -0
  53. package/src/client-copy.js +10 -0
  54. package/src/client-feature-gates.js +12 -5
  55. package/src/client-host-compat.js +23 -2
  56. package/src/client-primitives.js +249 -0
  57. package/src/client-repl-view.js +3 -3
  58. package/src/client-settings-view.js +2 -29
  59. package/src/client-styles.js +92 -33
  60. package/src/client-tool-view.js +8 -3
  61. package/src/client-workbench.js +9 -7
  62. package/src/client.js +72 -46
  63. package/assets/ptc-plus-bindings-en.png +0 -0
  64. package/assets/ptc-plus-bindings-zh.png +0 -0
  65. package/assets/ptc-plus-repair-en.png +0 -0
  66. package/assets/ptc-plus-repair-zh.png +0 -0
@@ -1,4 +1,7 @@
1
1
  /** Own PTC direct presentation, prompt projection, and stream normalization. */
2
+ import { boundContextSummary, createUserMessage } from '@deepseek-ai/dsh-llm'
3
+ import { validateJsonSchemaValue } from '@deepseek-ai/dsh-tools'
4
+ import { PTC_MESSAGE_SOURCE_KIND } from './message-sources.js'
2
5
  import { canonicalizeToolCallStream } from './tool-call-canonicalizer.js'
3
6
  import { editRunCodeSchema } from './rejected-cell-editor.js'
4
7
  import { sessionRuntimeContexts } from './runtime-contexts.js'
@@ -19,6 +22,12 @@ const RUN_CODE_CODE_DESCRIPTION = 'Code for the next REPL cell, parsed as the bo
19
22
  const RUN_CODE_DESCRIPTION_DESCRIPTION = 'Short active-voice summary of what this cell does, 5-10 words (shown in the UI).'
20
23
  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.'
21
24
  const PTC_COLLAPSE_SECTION_NAMES = Object.freeze(['tools:ptc-only', 'tools:code-only'])
25
+ const INVALID_TOOL_JSON_FAILURE = Object.freeze({
26
+ code: 'MALFORMED_RESPONSE',
27
+ message: 'DeepSeek Messages stream: tool input is invalid JSON',
28
+ })
29
+ const MALFORMED_TOOL_REJECTION_ID = 'dsh-ptc-plus-malformed-tool-json'
30
+ const MALFORMED_TOOL_REJECTION_ARGUMENTS = '{"dsh_ptc_plus_malformed_tool_json":'
22
31
 
23
32
  function adaptRunCodeSchema(tool) {
24
33
  const parameters = tool.parameters
@@ -139,6 +148,314 @@ async function* bindCallPolicies(source, policy, calls) {
139
148
  }
140
149
  }
141
150
 
151
+ function invalidToolJsonFinish(chunk) {
152
+ const failure = chunk?.reason?.failure
153
+ return chunk?.type === 'finish'
154
+ && chunk.reason.kind === 'error'
155
+ && failure?.code === INVALID_TOOL_JSON_FAILURE.code
156
+ && failure?.message === INVALID_TOOL_JSON_FAILURE.message
157
+ }
158
+
159
+ function invalidToolJsonError(error) {
160
+ const failure = error?.failure ?? error
161
+ return failure?.code === INVALID_TOOL_JSON_FAILURE.code
162
+ && failure?.message === INVALID_TOOL_JSON_FAILURE.message
163
+ }
164
+
165
+ function directToolSchemas(tools) {
166
+ const schemas = new Map()
167
+ if (!Array.isArray(tools)) return schemas
168
+ for (const tool of tools) {
169
+ if (tool?.name !== RUN_CODE && tool?.name !== EDIT_RUN_CODE) continue
170
+ if (schemas.has(tool.name) || !isRecord(tool.parameters)) return new Map()
171
+ schemas.set(tool.name, tool.parameters)
172
+ }
173
+ return schemas
174
+ }
175
+
176
+ function repairableDirectCall(chunks, tools) {
177
+ const finish = chunks.at(-1)
178
+ if (!invalidToolJsonFinish(finish)) return undefined
179
+ const completed = completedToolBlocks(chunks)
180
+ if (completed?.length !== 1) return undefined
181
+ const body = chunks.slice(0, -1)
182
+ const starts = body.filter(chunk => chunk?.type === 'block-start' && chunk.blockType === 'tool-call')
183
+ const deltas = body.filter(chunk => chunk?.type === 'tool-call-delta')
184
+ const ends = body.filter(chunk => chunk?.type === 'block-end' && chunk.block?.type === 'tool-call')
185
+ const usages = body.filter(chunk => chunk?.type === 'usage')
186
+ if (starts.length !== 1 || deltas.length === 0 || ends.length !== 1
187
+ || usages.length > 1
188
+ || body.some(chunk => chunk?.type !== 'usage' && !toolStreamChunk(chunk))) {
189
+ return undefined
190
+ }
191
+ const index = starts[0].index
192
+ const end = ends[0]
193
+ const id = end.block.id
194
+ const name = end.block.name
195
+ if (!Number.isSafeInteger(index) || index < 0 || typeof id !== 'string' || id.length === 0
196
+ || (name !== RUN_CODE && name !== EDIT_RUN_CODE)
197
+ || end.index !== index
198
+ || typeof end.block.arguments !== 'string'
199
+ || deltas.some(chunk => chunk.index !== index || chunk.id !== id
200
+ || (chunk.name !== undefined && chunk.name !== name)
201
+ || typeof chunk.argumentsDelta !== 'string')) {
202
+ return undefined
203
+ }
204
+ const raw = deltas.map(chunk => chunk.argumentsDelta).join('')
205
+ if (raw !== end.block.arguments || !raw.endsWith('}')) return undefined
206
+ const candidate = raw.slice(0, -1)
207
+ let value
208
+ try {
209
+ value = JSON.parse(candidate)
210
+ } catch {
211
+ return undefined
212
+ }
213
+ const schema = directToolSchemas(tools).get(name)
214
+ if (!isRecord(value) || schema === undefined
215
+ || validateJsonSchemaValue(schema, value).length !== 0) return undefined
216
+ return { index, candidate }
217
+ }
218
+
219
+ function canonicalToolBlock(index, block) {
220
+ return [
221
+ { type: 'block-start', index, blockType: 'tool-call' },
222
+ {
223
+ type: 'tool-call-delta', index, id: block.id, name: block.name,
224
+ argumentsDelta: block.arguments,
225
+ },
226
+ { type: 'block-end', index, block },
227
+ ]
228
+ }
229
+
230
+ function repairedDirectChunks(chunks, repair, usageAlreadyEmitted) {
231
+ const end = chunks.find(chunk => chunk?.type === 'block-end'
232
+ && chunk.index === repair.index && chunk.block?.type === 'tool-call')
233
+ const block = { ...end.block, arguments: repair.candidate }
234
+ const usage = usageAlreadyEmitted ? undefined : firstUsageChunk(chunks)
235
+ return [
236
+ ...canonicalToolBlock(repair.index, block),
237
+ ...usage === undefined ? [] : [usage],
238
+ { type: 'finish', reason: { kind: 'tool-calls' } },
239
+ ]
240
+ }
241
+
242
+ function completedToolBlocks(chunks) {
243
+ const body = chunks.slice(0, -1)
244
+ if (body.filter(chunk => chunk?.type === 'usage').length > 1
245
+ || body.some(chunk => chunk?.type !== 'usage' && !toolStreamChunk(chunk))) {
246
+ return undefined
247
+ }
248
+ const calls = new Map()
249
+ for (const chunk of chunks) {
250
+ if (chunk?.type === 'block-start' && chunk.blockType === 'tool-call') {
251
+ if (!Number.isSafeInteger(chunk.index) || chunk.index < 0) return undefined
252
+ const call = calls.get(chunk.index)
253
+ if (call?.started === true || call?.block !== undefined) return undefined
254
+ if (call === undefined) calls.set(chunk.index, {
255
+ arguments: '', hasDelta: false, started: true,
256
+ })
257
+ else call.started = true
258
+ } else if (chunk?.type === 'tool-call-delta') {
259
+ if (!Number.isSafeInteger(chunk.index) || chunk.index < 0) return undefined
260
+ let call = calls.get(chunk.index)
261
+ if (call === undefined) {
262
+ call = { arguments: '', hasDelta: false, started: false }
263
+ calls.set(chunk.index, call)
264
+ }
265
+ if (call.block !== undefined
266
+ || typeof chunk.id !== 'string' || chunk.id.length === 0
267
+ || (call.id !== undefined && call.id !== chunk.id)
268
+ || (chunk.name !== undefined && (typeof chunk.name !== 'string' || chunk.name.length === 0
269
+ || (call.name !== undefined && call.name !== chunk.name)))
270
+ || typeof chunk.argumentsDelta !== 'string') return undefined
271
+ call.id ??= chunk.id
272
+ if (chunk.name !== undefined) call.name ??= chunk.name
273
+ call.hasDelta = true
274
+ call.arguments += chunk.argumentsDelta
275
+ } else if (chunk?.type === 'block-end' && chunk.block?.type === 'tool-call') {
276
+ if (!Number.isSafeInteger(chunk.index) || chunk.index < 0) return undefined
277
+ let call = calls.get(chunk.index)
278
+ if (call === undefined) {
279
+ call = { arguments: '', hasDelta: false, started: false }
280
+ calls.set(chunk.index, call)
281
+ }
282
+ if (call.block !== undefined
283
+ || typeof chunk.block.id !== 'string' || chunk.block.id.length === 0
284
+ || typeof chunk.block.name !== 'string' || chunk.block.name.length === 0
285
+ || typeof chunk.block.arguments !== 'string'
286
+ || (call.id !== undefined && call.id !== chunk.block.id)
287
+ || (call.name !== undefined && call.name !== chunk.block.name)
288
+ || (call.hasDelta && call.arguments !== chunk.block.arguments)) return undefined
289
+ call.block = chunk.block
290
+ }
291
+ }
292
+ if (calls.size === 0 || [...calls.values()].some(call => call.block === undefined)) {
293
+ return undefined
294
+ }
295
+ const completed = [...calls].map(([index, call]) => ({ index, block: call.block }))
296
+ if (new Set(completed.map(call => call.block.id)).size !== completed.length) return undefined
297
+ return completed
298
+ }
299
+
300
+ function firstUsageChunk(chunks) {
301
+ return chunks.find(chunk => chunk?.type === 'usage')
302
+ }
303
+
304
+ function settleMalformedToolCalls(chunks, priorIndexes, usageAlreadyEmitted) {
305
+ if (!invalidToolJsonFinish(chunks.at(-1))) return undefined
306
+ const calls = completedToolBlocks(chunks)
307
+ if (calls === undefined || calls.some(call => priorIndexes.has(call.index))) return undefined
308
+ const usage = usageAlreadyEmitted ? undefined : firstUsageChunk(chunks)
309
+ return [
310
+ ...calls.flatMap(call => canonicalToolBlock(call.index, call.block)),
311
+ ...usage === undefined ? [] : [usage],
312
+ { type: 'finish', reason: { kind: 'tool-calls' } },
313
+ ]
314
+ }
315
+
316
+ function toolStreamChunk(chunk) {
317
+ return chunk?.type === 'tool-call-delta'
318
+ || (chunk?.type === 'block-start' && chunk.blockType === 'tool-call')
319
+ || (chunk?.type === 'block-end' && chunk.block?.type === 'tool-call')
320
+ }
321
+
322
+ function unusedString(base, used) {
323
+ let value = base
324
+ for (let suffix = 2; used.has(value); suffix++) value = `${base}-${suffix}`
325
+ return value
326
+ }
327
+
328
+ function visibleBlockClosures(openBlocks, chunks) {
329
+ const closing = new Map([...openBlocks].map(([index, block]) => [index, { ...block }]))
330
+ for (const chunk of chunks) {
331
+ const block = closing.get(chunk?.index)
332
+ if (block === undefined) continue
333
+ if (chunk.type === `${block.type}-delta` && typeof chunk.text === 'string') {
334
+ block.text += chunk.text
335
+ } else if (chunk.type === 'block-end' && chunk.block?.type === block.type) {
336
+ block.final = chunk.block
337
+ }
338
+ }
339
+ return [...closing].map(([index, block]) => ({
340
+ type: 'block-end',
341
+ index,
342
+ block: block.final ?? { type: block.type, text: block.text },
343
+ }))
344
+ }
345
+
346
+ function rejectedMalformedToolStream(
347
+ chunks,
348
+ priorIndexes = new Set(),
349
+ openBlocks = new Map(),
350
+ usageAlreadyEmitted = false,
351
+ ) {
352
+ const usedIndexes = new Set(priorIndexes)
353
+ const usedIds = new Set()
354
+ for (const chunk of chunks) {
355
+ if (Number.isSafeInteger(chunk?.index) && chunk.index >= 0) usedIndexes.add(chunk.index)
356
+ if (typeof chunk?.id === 'string') usedIds.add(chunk.id)
357
+ if (typeof chunk?.block?.id === 'string') usedIds.add(chunk.block.id)
358
+ }
359
+ let index = 0
360
+ while (usedIndexes.has(index)) index++
361
+ const id = unusedString(MALFORMED_TOOL_REJECTION_ID, usedIds)
362
+ const usage = usageAlreadyEmitted ? undefined : firstUsageChunk(chunks)
363
+ return [
364
+ ...visibleBlockClosures(openBlocks, chunks),
365
+ { type: 'block-start', index, blockType: 'tool-call' },
366
+ {
367
+ type: 'tool-call-delta', index, id, name: RUN_CODE,
368
+ argumentsDelta: MALFORMED_TOOL_REJECTION_ARGUMENTS,
369
+ },
370
+ {
371
+ type: 'block-end', index,
372
+ block: {
373
+ type: 'tool-call', id, name: RUN_CODE,
374
+ arguments: MALFORMED_TOOL_REJECTION_ARGUMENTS,
375
+ },
376
+ },
377
+ ...usage === undefined ? [] : [usage],
378
+ { type: 'finish', reason: { kind: 'tool-calls' } },
379
+ ]
380
+ }
381
+
382
+ function recoveredMalformedChunks(chunks, tools, priorIndexes, openBlocks, usageAlreadyEmitted) {
383
+ if (usageAlreadyEmitted && firstUsageChunk(chunks) !== undefined) {
384
+ return rejectedMalformedToolStream(chunks, priorIndexes, openBlocks, true)
385
+ }
386
+ const repair = repairableDirectCall(chunks, tools)
387
+ const settled = repair !== undefined && !priorIndexes.has(repair.index)
388
+ ? repairedDirectChunks(chunks, repair, usageAlreadyEmitted)
389
+ : settleMalformedToolCalls(chunks, priorIndexes, usageAlreadyEmitted)
390
+ return settled === undefined
391
+ ? rejectedMalformedToolStream(chunks, priorIndexes, openBlocks, usageAlreadyEmitted)
392
+ : [...visibleBlockClosures(openBlocks, chunks), ...settled]
393
+ }
394
+
395
+ function observeVisibleBlock(openBlocks, chunk) {
396
+ if (chunk?.type === 'block-start'
397
+ && (chunk.blockType === 'text' || chunk.blockType === 'reasoning')) {
398
+ openBlocks.set(chunk.index, { type: chunk.blockType, text: '' })
399
+ } else if (chunk?.type === 'text-delta' || chunk?.type === 'reasoning-delta') {
400
+ const block = openBlocks.get(chunk.index)
401
+ const type = chunk.type === 'text-delta' ? 'text' : 'reasoning'
402
+ if (block?.type === type) block.text += chunk.text
403
+ } else if (chunk?.type === 'block-end') {
404
+ openBlocks.delete(chunk.index)
405
+ }
406
+ }
407
+
408
+ async function* recoverMalformedToolCallStream(source, tools) {
409
+ let pending = []
410
+ const priorIndexes = new Set()
411
+ const openBlocks = new Map()
412
+ let usageSeen = false
413
+ try {
414
+ for await (const chunk of source) {
415
+ if (pending.length === 0) {
416
+ if (toolStreamChunk(chunk)) pending.push(chunk)
417
+ else if (chunk?.type === 'usage' && usageSeen) pending.push(chunk)
418
+ else if (invalidToolJsonFinish(chunk)) {
419
+ yield* rejectedMalformedToolStream([chunk], priorIndexes, openBlocks, usageSeen)
420
+ openBlocks.clear()
421
+ } else {
422
+ if (Number.isSafeInteger(chunk?.index) && chunk.index >= 0) priorIndexes.add(chunk.index)
423
+ observeVisibleBlock(openBlocks, chunk)
424
+ if (chunk?.type === 'usage') usageSeen = true
425
+ yield chunk
426
+ }
427
+ continue
428
+ }
429
+ pending.push(chunk)
430
+ if (chunk?.type !== 'finish') continue
431
+ const output = invalidToolJsonFinish(chunk)
432
+ ? recoveredMalformedChunks(pending, tools, priorIndexes, openBlocks, usageSeen)
433
+ : pending
434
+ for (const outputChunk of output) {
435
+ if (Number.isSafeInteger(outputChunk?.index) && outputChunk.index >= 0) {
436
+ priorIndexes.add(outputChunk.index)
437
+ }
438
+ if (outputChunk?.type === 'usage') usageSeen = true
439
+ yield outputChunk
440
+ }
441
+ if (invalidToolJsonFinish(chunk)) openBlocks.clear()
442
+ pending = []
443
+ }
444
+ } catch (error) {
445
+ if (invalidToolJsonError(error)) {
446
+ yield* recoveredMalformedChunks([
447
+ ...pending,
448
+ { type: 'finish', reason: { kind: 'error', failure: INVALID_TOOL_JSON_FAILURE } },
449
+ ], tools, priorIndexes, openBlocks, usageSeen)
450
+ return
451
+ } else {
452
+ yield* pending
453
+ throw error
454
+ }
455
+ }
456
+ yield* pending
457
+ }
458
+
142
459
  export function createDirectSurfaceOwner({
143
460
  editTransport,
144
461
  runtimeConfig,
@@ -147,6 +464,7 @@ export function createDirectSurfaceOwner({
147
464
  toolSchemasForAgent,
148
465
  userBindingsForAgent = async () => undefined,
149
466
  setAgentPresentation = async () => {},
467
+ sessionLogView,
150
468
  }) {
151
469
  // Composition is anchored to Agent identity because DSH selects presentation
152
470
  // once per composed agent. Request signals bind the exact assembly to stream
@@ -317,6 +635,7 @@ export function createDirectSurfaceOwner({
317
635
  }
318
636
  const runtimeContexts = sessionPtc
319
637
  ? sessionRuntimeContexts(agent, tipConfig, {
638
+ sessionLogView,
320
639
  cordisRecoveryRequired(view) {
321
640
  return cordisRecovery.required(agent, view)
322
641
  },
@@ -383,7 +702,8 @@ export function createDirectSurfaceOwner({
383
702
  editToolName: EDIT_RUN_CODE,
384
703
  })
385
704
  : next()
386
- return bindCallPolicies(source, policy, policy.owner.calls)
705
+ const recovered = recoverMalformedToolCallStream(source, options.tools)
706
+ return bindCallPolicies(recovered, policy, policy.owner.calls)
387
707
  },
388
708
  executionRejection(exec) {
389
709
  if (exec.name === EDIT_RUN_CODE && exec.parent !== undefined) {
@@ -430,12 +750,18 @@ export function createDirectSurfaceOwner({
430
750
  const outer = missingPath === 'description'
431
751
  && policy?.autoDescribeRunCode !== true
432
752
  && !(isRecord(exec.arguments) && typeof exec.arguments.description === 'string')
433
- const context = {
434
- name: 'tools:ptc-plus-run-code-arguments',
435
- text: outer
436
- ? '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.'
437
- : `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.`,
438
- }
753
+ const text = outer
754
+ ? '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.'
755
+ : `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.`
756
+ // additionalContexts enter the durable inbox as messages, not prompt sections.
757
+ const context = createUserMessage({
758
+ source: {
759
+ kind: PTC_MESSAGE_SOURCE_KIND,
760
+ form: 'notice',
761
+ summary: boundContextSummary('tools:ptc-plus-run-code-arguments'),
762
+ },
763
+ content: [{ type: 'text', text }],
764
+ })
439
765
  return {
440
766
  ...diagnosed,
441
767
  additionalContexts: [...diagnosed.additionalContexts ?? [], context],
@@ -17,7 +17,7 @@ import {
17
17
  editRejectedCell,
18
18
  editRunCodeSchema,
19
19
  } from './rejected-cell-editor.js'
20
- import { editTargetForCall, projectSessionLog } from './session-log-view.js'
20
+ import { 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
23
  import {
@@ -67,6 +67,8 @@ export function createEditTransportOwner(ctx, {
67
67
  presentationGeneration,
68
68
  sessionId,
69
69
  toolSchemasForAgent,
70
+ readSessionLog,
71
+ sessionLogView = projectSessionLog,
70
72
  }) {
71
73
  let currentDurableReplay = durableReplay
72
74
  const editExecutionMetadata = new WeakMap()
@@ -143,13 +145,14 @@ export function createEditTransportOwner(ctx, {
143
145
  definition.execute = async (args, exec) => {
144
146
  const agent = exec?.agent
145
147
  const callId = typeof exec?.callId === 'string' ? exec.callId : undefined
146
- const persistedTargetCallSeq = liveToolCallSeq(agent?.session, callId, EDIT_RUN_CODE)
147
- if (persistedTargetCallSeq === undefined && projectSessionLog(agent).editableRun !== undefined) {
148
+ const sessionLog = await readSessionLog(agent?.session, exec.signal)
149
+ const persistedTargetCallSeq = liveToolCallSeq(sessionLog, callId, EDIT_RUN_CODE)
150
+ if (persistedTargetCallSeq === undefined && sessionLogView(agent).editableRun !== undefined) {
148
151
  throw new Error('ptc-plus: edit_run_code requires a unique persisted tool/call event')
149
152
  }
150
153
  const target = persistedTargetCallSeq === undefined
151
154
  ? undefined
152
- : editTargetForCall(agent, callId, persistedTargetCallSeq)
155
+ : sessionLogView(agent, { callId, callSeq: persistedTargetCallSeq }).requestedEditTarget
153
156
  if (target?.source === undefined || target.callSeq === undefined) {
154
157
  return { edited: false, reason: 'no run_code cell is currently eligible for safe editing' }
155
158
  }
@@ -190,7 +193,7 @@ export function createEditTransportOwner(ctx, {
190
193
  agent,
191
194
  signal: exec.signal,
192
195
  })
193
- const tentative = await executeTentative(persistedCallSeq, dispatch)
196
+ const tentative = await executeTentative(persistedCallSeq, dispatch, sessionLog)
194
197
  const inner = tentative.result
195
198
  finalizeTentative = tentative.finalize
196
199
  const journalValue = isRecord(inner?.meta) && Object.hasOwn(inner.meta, JOURNAL_KEY)
@@ -0,0 +1,157 @@
1
+ import { createExecutionSeam, EXECUTION_SEAM_SERVICE } from './execution-seam-compat.js'
2
+
3
+ export const name = 'ptc-plus-execution-provider'
4
+ export const inject = ['loader']
5
+
6
+ export function apply(ctx, config = {}) {
7
+ const ownerFiber = ctx.fiber
8
+ const { originalEntryId = 'ptc-runtime', isolation = 'ptc-plus-original' } = config
9
+ if (typeof originalEntryId !== 'string' || originalEntryId.length === 0
10
+ || typeof isolation !== 'string' || isolation.length === 0) {
11
+ throw new TypeError('ptc-plus: execution provider requires an original entry id and named isolation')
12
+ }
13
+ // Profile rows belong to the Include's tree; the global Loader resolves root IDs.
14
+ const entryTree = ownerFiber.entry?.parent.tree ?? ctx.loader
15
+ const originalEntry = entryTree.resolve(originalEntryId)
16
+ const isolated = () => originalEntry.options.isolate?.[EXECUTION_SEAM_SERVICE] === isolation
17
+ if (!isolated()) throw new Error(`ptc-plus: original entry ${originalEntryId} must isolate ${EXECUTION_SEAM_SERVICE} as ${isolation}`)
18
+ const originalContext = originalEntry.context.extend({ [originalEntry.constructor.key]: undefined })
19
+ let watcher
20
+ let live
21
+ let stopped = false
22
+ let suspended = false
23
+ let generation
24
+ const pending = new Set()
25
+ let resolveAttachment
26
+ let rejectAttachment
27
+ const attachment = new Promise((resolve, reject) => {
28
+ resolveAttachment = resolve
29
+ rejectAttachment = reject
30
+ })
31
+ const readOriginal = () => {
32
+ const original = isolated() ? originalContext.get(EXECUTION_SEAM_SERVICE) : undefined
33
+ if (stopped || original === undefined) {
34
+ throw new Error(`ptc-plus: original ${EXECUTION_SEAM_SERVICE} generation is unavailable`)
35
+ }
36
+ return original
37
+ }
38
+ const release = () => {
39
+ const entry = live
40
+ if (entry === undefined) return
41
+ live = undefined
42
+ entry.seam.retire()
43
+ const unregistration = entry.unregister?.()
44
+ const disposal = Promise.allSettled([unregistration, entry.disposeChild?.()]).then(results => {
45
+ const failures = results.filter(result => result.status === 'rejected').map(result => result.reason)
46
+ if (failures.length > 0) throw new AggregateError(failures, 'ptc-plus: execution provider disposal failed')
47
+ })
48
+ pending.add(disposal)
49
+ disposal.then(() => pending.delete(disposal), error => {
50
+ pending.delete(disposal)
51
+ ctx.logger.warn(error)
52
+ })
53
+ return disposal
54
+ }
55
+ const suspend = () => {
56
+ suspended = true
57
+ const disposal = release()
58
+ const previous = watcher
59
+ watcher = undefined
60
+ return Promise.all([disposal, previous?.dispose()])
61
+ }
62
+ const publish = () => {
63
+ const resolutions = new WeakMap()
64
+ const delegate = {
65
+ get language() { return readOriginal().language },
66
+ get isolation() { return readOriginal().isolation },
67
+ get executionInstructions() { return readOriginal().executionInstructions },
68
+ get sandboxMode() { return readOriginal().sandboxMode },
69
+ get timeout() { return readOriginal().timeout },
70
+ resolve(request) {
71
+ const original = readOriginal()
72
+ const spec = original.resolve(request)
73
+ if (spec !== null && typeof spec === 'object') {
74
+ resolutions.set(spec, { original, generation })
75
+ }
76
+ return spec
77
+ },
78
+ run(spec) {
79
+ const resolved = resolutions.get(spec)
80
+ if (resolved?.generation?.active === false) throw new Error('ptc-plus: resolved spec belongs to an unavailable original generation')
81
+ const original = resolved?.original ?? readOriginal()
82
+ return original.run(spec)
83
+ },
84
+ }
85
+ const seam = createExecutionSeam(delegate, EXECUTION_SEAM_SERVICE)
86
+ const entry = { seam }
87
+ live = entry
88
+ entry.disposeChild = ctx.effect(() => {
89
+ const child = ctx.plugin({
90
+ name: 'ptc-plus-composed-runtime',
91
+ apply(childContext) {
92
+ childContext.effect(() => {
93
+ if (stopped || live !== entry || !isolated()) return () => {}
94
+ entry.unregister = childContext.provide(EXECUTION_SEAM_SERVICE, seam.provider)
95
+ return () => entry.unregister()
96
+ }, 'ptc-plus owned execution registration')
97
+ },
98
+ })
99
+ entry.child = child
100
+ return () => child.dispose()
101
+ }, 'ptc-plus owned execution child')
102
+ return entry.child.await().then(() => {
103
+ if (!stopped && live === entry && entry.unregister !== undefined && entry.child.state === 2) resolveAttachment()
104
+ }, error => {
105
+ rejectAttachment(error)
106
+ throw error
107
+ })
108
+ }
109
+ const watch = () => {
110
+ if (stopped || watcher !== undefined || !isolated()) return
111
+ ctx.effect(() => {
112
+ const observer = originalContext.inject([EXECUTION_SEAM_SERVICE], scope => {
113
+ if (stopped || !isolated()) return
114
+ createExecutionSeam(scope.get(EXECUTION_SEAM_SERVICE), EXECUTION_SEAM_SERVICE)
115
+ const currentGeneration = { active: true }
116
+ generation = currentGeneration
117
+ scope.effect(() => () => {
118
+ currentGeneration.active = false
119
+ if (generation === currentGeneration) generation = undefined
120
+ }, 'ptc-plus original generation scope')
121
+ if (live === undefined) return publish()
122
+ return live.seam.sourceChanged()
123
+ })
124
+ watcher = observer
125
+ return () => observer.dispose()
126
+ }, 'ptc-plus original execution observer')
127
+ }
128
+ ctx.on('loader/partial-dispose', (entry, _legacy, active) => {
129
+ if (entry === originalEntry && (!active || !isolated())) void suspend().catch(error => ctx.logger.warn(error))
130
+ }, { prepend: true, global: true })
131
+ ctx.on('loader/patch-context', (entry, next) => {
132
+ if (entry === originalEntry && !isolated()) void suspend().catch(error => ctx.logger.warn(error))
133
+ next()
134
+ if (entry === originalEntry && isolated() && suspended && !stopped && ownerFiber.state === 2) {
135
+ stopped = true
136
+ resolveAttachment()
137
+ void suspend().catch(error => ctx.logger.warn(error))
138
+ void ownerFiber.restart().catch(error => ctx.logger.warn(error))
139
+ }
140
+ }, { prepend: true, global: true })
141
+ ctx.effect(() => () => {
142
+ stopped = true
143
+ resolveAttachment()
144
+ const suspension = suspend()
145
+ return Promise.all([suspension, ...pending])
146
+ }, 'ptc-plus execution provider composition')
147
+ if (originalEntry.disabled) resolveAttachment()
148
+ const timer = setTimeout(() => rejectAttachment(new Error(
149
+ `ptc-plus: original execution entry ${originalEntryId} did not publish ${EXECUTION_SEAM_SERVICE}`,
150
+ )), 30_000)
151
+ ctx.effect(() => () => clearTimeout(timer), 'ptc-plus original execution provider wait')
152
+ const originalReady = originalEntry.fiber?.await() ?? Promise.resolve()
153
+ return originalReady.then(() => {
154
+ watch()
155
+ return attachment
156
+ }).finally(() => clearTimeout(timer))
157
+ }