dsh-ptc-plus 0.5.1 → 0.5.3
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 +10 -4
- package/README.md +10 -4
- package/client.cjs +1623 -104
- package/compiler-core.cjs +132 -132
- package/compiler-core.cjs.map +3 -3
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +2 -0
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +4 -2
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +2 -2
- package/docs/adr/0023-global-user-bindings.md +12 -0
- package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +11 -1
- package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +1 -1
- package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +2 -2
- package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +31 -1
- package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +4 -4
- package/docs/adr/0045-customize-model-visible-prompt-text.md +38 -0
- package/docs/adr/0046-gate-the-commit-with-the-semantic-evidence-tier.md +35 -0
- package/docs/adr/0047-store-journal-argument-digests-in-the-projection.md +88 -0
- package/docs/adr/0048-supersede-a-rewritten-cell.md +48 -0
- package/docs/adr/0049-prove-a-settlement-against-its-published-record.md +53 -0
- package/docs/adr/0050-read-host-surfaces-through-their-published-contract.md +48 -0
- package/docs/adr/README.md +7 -1
- package/docs/architecture.md +3 -3
- package/docs/client-ui.md +5 -2
- package/docs/durability-design.md +15 -2
- package/docs/installation.md +9 -9
- package/docs/releases/v0.5.2.md +91 -0
- package/docs/releases/v0.5.3.md +77 -0
- package/docs/runtime-reference.md +43 -3
- package/docs/unmeasured-cell-recovery-repair-proposal.md +387 -0
- package/docs/user-bindings.en.md +15 -0
- package/docs/user-bindings.md +15 -0
- package/docs/v0.5.3-goals.md +117 -0
- package/docs/verification.md +7 -3
- package/index.js +100 -31
- package/internal/binding-adoption.js +136 -0
- package/internal/bounded-literal.js +85 -0
- package/internal/capability-command-receipt.js +18 -4
- package/internal/capability-delivery-proof.js +70 -0
- package/internal/capability-environment.js +200 -11
- package/internal/capability-plan.js +11 -8
- package/internal/cell-error.js +13 -0
- package/internal/config-spec.js +126 -6
- package/internal/cordis-tools-owner.js +2 -1
- package/internal/direct-surface-owner.js +39 -21
- package/internal/edit-transport-owner.js +81 -15
- package/internal/execution-provider-owner.js +6 -0
- package/internal/execution-seam-compat.js +25 -0
- package/internal/host-tool-sdk.js +308 -0
- package/internal/isolated-worker.js +134 -17
- package/internal/kernel-worker.js +167 -3
- package/internal/legacy-cell-analysis.js +14 -2
- package/internal/legacy-repl-convenience.js +16 -3
- package/internal/official-repl-priority.js +31 -10
- package/internal/program-bindings.js +12 -1
- package/internal/projection-retention.js +238 -0
- package/internal/prompt-text.js +858 -0
- package/internal/recovery-tips.js +50 -11
- package/internal/rejected-cell-editor.js +3 -2
- package/internal/repl-memory-projection.js +21 -2
- package/internal/runtime-bridge-owner.js +114 -9
- package/internal/runtime-config.js +2 -0
- package/internal/runtime-contexts.js +14 -5
- package/internal/runtime-messages.js +48 -15
- package/internal/session-cell-executor.js +132 -11
- package/internal/session-journal-recovery.js +1204 -116
- package/internal/session-journal-schema.js +10 -5
- package/internal/session-journal.js +214 -14
- package/internal/session-log-view.js +133 -26
- package/internal/session-runtime.js +746 -38
- package/internal/session-state.js +27 -0
- package/internal/stateful-root-compiler.js +11 -0
- package/internal/stateful-root-runtime.js +17 -0
- package/internal/user-binding-console.js +7 -0
- package/internal/user-bindings-owner.js +112 -6
- package/internal/user-bindings.js +2 -2
- package/internal/validated-parse-repair.js +6 -1
- package/internal/worker-client.js +27 -4
- package/package.json +16 -6
- package/src/client-activity.js +24 -4
- package/src/client-capabilities-view.js +504 -27
- package/src/client-copy.js +187 -2
- package/src/client-indicator-view.js +2 -1
- package/src/client-repl-view.js +35 -11
- package/src/client-styles.js +64 -2
- package/src/client.js +3 -1
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import { PTC_STATE_NAMES, recoveryTipIdentity } from './runtime-messages.js'
|
|
2
|
+
import {
|
|
3
|
+
PROMPT_SLOT_IDS, TIP_BODY, TIP_BODY_PLATFORM_DETAILED, TIP_BODY_REPEATED_BINDING,
|
|
4
|
+
TIP_BODY_REPEATED_BINDING_CAPABILITY, TIP_DETAIL_BINDING, TIP_DETAIL_CAPABILITY,
|
|
5
|
+
TIP_PREFIX_PLATFORM_COMMAND, TIP_PREFIX_REPEATED_BINDING,
|
|
6
|
+
} from './prompt-text.js'
|
|
2
7
|
|
|
3
8
|
const PLATFORM_CAUSE_CODES = new Set(['EACCES', 'EINVAL', 'ENOTDIR', 'ENOENT', 'UNKNOWN'])
|
|
4
9
|
const TIP_CONTEXT_PREFIX = 'tools:ptc-plus-tip/'
|
|
5
10
|
const PLATFORM_FACTS = '`return { platform: process.platform, arch: process.arch, node: process.version, cwd: process.cwd(), execPath: process.execPath }`'
|
|
11
|
+
/**
|
|
12
|
+
* The delivered prefixes, one slot per trigger kind.
|
|
13
|
+
*
|
|
14
|
+
* Tip identity comes from the notice name, so a prefix carries no recognizer
|
|
15
|
+
* contract; both are read-only registry literals so their bytes are accounted.
|
|
16
|
+
*/
|
|
6
17
|
const TIP_PREFIXES = Object.freeze({
|
|
7
|
-
'repeated-binding-failure':
|
|
8
|
-
'platform-command-failure':
|
|
18
|
+
'repeated-binding-failure': TIP_PREFIX_REPEATED_BINDING,
|
|
19
|
+
'platform-command-failure': TIP_PREFIX_PLATFORM_COMMAND,
|
|
9
20
|
})
|
|
10
21
|
|
|
11
22
|
// TODO(dsh-tips-api): replace this local provider with an adapter after dsh-tips publishes a stable facts and decision interface.
|
|
@@ -91,18 +102,46 @@ function tipCandidate(view) {
|
|
|
91
102
|
return undefined
|
|
92
103
|
}
|
|
93
104
|
|
|
94
|
-
|
|
105
|
+
const TIP_BODY_SLOTS = Object.freeze({
|
|
106
|
+
platform: PROMPT_SLOT_IDS.tipBodyPlatform,
|
|
107
|
+
platformDetailed: PROMPT_SLOT_IDS.tipBodyPlatformDetailed,
|
|
108
|
+
repeatedBinding: PROMPT_SLOT_IDS.tipBodyRepeatedBinding,
|
|
109
|
+
repeatedBindingCapability: PROMPT_SLOT_IDS.tipBodyRepeatedBindingCapability,
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const TIP_DETAIL_SLOTS = Object.freeze({
|
|
113
|
+
binding: PROMPT_SLOT_IDS.tipDetailBinding,
|
|
114
|
+
capability: PROMPT_SLOT_IDS.tipDetailCapability,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const TIP_PREFIX_SLOTS = Object.freeze({
|
|
118
|
+
'repeated-binding-failure': PROMPT_SLOT_IDS.tipPrefixRepeatedBinding,
|
|
119
|
+
'platform-command-failure': PROMPT_SLOT_IDS.tipPrefixPlatformCommand,
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Every delivered tip body reads its own registry slot; escalation only decides
|
|
124
|
+
* whether the `{detail}` hole carries its sentence, and which of the two registered
|
|
125
|
+
* detail slots fills it. Prefix and detail slots are read-only, so a customised body
|
|
126
|
+
* cannot move tip identity or escalation.
|
|
127
|
+
*/
|
|
128
|
+
function renderTip(id, detailed, candidate = {}, prompt) {
|
|
129
|
+
const body = (slotId, fallback) => prompt?.text(slotId) ?? fallback
|
|
130
|
+
const prefix = body(TIP_PREFIX_SLOTS[id], TIP_PREFIXES[id])
|
|
95
131
|
if (id === 'platform-command-failure') {
|
|
96
|
-
|
|
97
|
-
?
|
|
98
|
-
:
|
|
132
|
+
const template = detailed
|
|
133
|
+
? body(TIP_BODY_SLOTS.platformDetailed, TIP_BODY_PLATFORM_DETAILED)
|
|
134
|
+
: body(TIP_BODY_SLOTS.platform, TIP_BODY)
|
|
135
|
+
return `${prefix} ${template.replace('{platformFacts}', PLATFORM_FACTS)}`
|
|
99
136
|
}
|
|
100
137
|
if (!candidate.capability) {
|
|
101
|
-
|
|
138
|
+
const template = body(TIP_BODY_SLOTS.repeatedBinding, TIP_BODY_REPEATED_BINDING)
|
|
139
|
+
const detail = detailed ? body(TIP_DETAIL_SLOTS.binding, TIP_DETAIL_BINDING) : ''
|
|
140
|
+
return `${prefix} ${template.replace('{detail}', detail)}`
|
|
102
141
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
142
|
+
const template = body(TIP_BODY_SLOTS.repeatedBindingCapability, TIP_BODY_REPEATED_BINDING_CAPABILITY)
|
|
143
|
+
const detail = detailed ? body(TIP_DETAIL_SLOTS.capability, TIP_DETAIL_CAPABILITY) : ''
|
|
144
|
+
return `${prefix} ${template.replace('{detail}', detail)}`
|
|
106
145
|
}
|
|
107
146
|
|
|
108
147
|
export function latestRecoveryTip(view, config) {
|
|
@@ -117,6 +156,6 @@ export function latestRecoveryTip(view, config) {
|
|
|
117
156
|
if (!Number.isSafeInteger(ordinal)) return undefined
|
|
118
157
|
return {
|
|
119
158
|
name: `${TIP_CONTEXT_PREFIX}${candidate.id}/${ordinal}`,
|
|
120
|
-
text: renderTip(candidate.id, (lastTip?.unresolved ?? 0) >= config.escalationFailures, candidate),
|
|
159
|
+
text: renderTip(candidate.id, (lastTip?.unresolved ?? 0) >= config.escalationFailures, candidate, config.prompt),
|
|
121
160
|
}
|
|
122
161
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EDIT_RUN_CODE_DESCRIPTION } from './prompt-text.js'
|
|
1
2
|
import { runInNewContext } from 'node:vm'
|
|
2
3
|
|
|
3
4
|
const DEFAULT_REGEX_TIMEOUT_MS = 100
|
|
@@ -252,10 +253,10 @@ function assemble(source, replacements) {
|
|
|
252
253
|
return changed ? { code: chunks.join('') } : { error: 'edits must change at least one matched fragment' }
|
|
253
254
|
}
|
|
254
255
|
|
|
255
|
-
export function editRunCodeSchema() {
|
|
256
|
+
export function editRunCodeSchema(description = EDIT_RUN_CODE_DESCRIPTION) {
|
|
256
257
|
return {
|
|
257
258
|
name: 'edit_run_code',
|
|
258
|
-
description
|
|
259
|
+
description,
|
|
259
260
|
parameters: {
|
|
260
261
|
type: 'object',
|
|
261
262
|
additionalProperties: false,
|
|
@@ -23,6 +23,7 @@ const COUNTED_ENTRY_FIELDS = new Set([...ENTRY_FIELDS, 'reuseCount'])
|
|
|
23
23
|
const DEFINITION_FIELDS = new Set(['source', 'line', 'column'])
|
|
24
24
|
const MEMORY_META_FIELDS = new Set(['version', 'generation', 'memory'])
|
|
25
25
|
const PROJECTION_STATE_FIELDS = new Set(['generation', 'memory', 'pendingReplCalls'])
|
|
26
|
+
const VIEW_FIELDS = new Set(['runtime', 'memory'])
|
|
26
27
|
const PENDING_CALL_FIELDS = new Set(['callId', 'seq'])
|
|
27
28
|
const REPL_MEMORY_META_VERSION = 5
|
|
28
29
|
|
|
@@ -205,6 +206,24 @@ export function normalizeReplMemorySnapshot(value) {
|
|
|
205
206
|
})
|
|
206
207
|
}
|
|
207
208
|
|
|
209
|
+
/**
|
|
210
|
+
* One runtime's own observation of its environment, together with the owner that
|
|
211
|
+
* produced it.
|
|
212
|
+
*
|
|
213
|
+
* A panel shows an observation, not "the current state". Two questions then have
|
|
214
|
+
* to stay separate, and only the owner answers them: a projection refresh inside
|
|
215
|
+
* one runtime keeps the inventory valid (freshness), while a snapshot from a
|
|
216
|
+
* previous runtime can no longer be confirmed (provenance). Carrying the owner
|
|
217
|
+
* in the view lets a reader decide by data instead of by object identity.
|
|
218
|
+
*/
|
|
219
|
+
export function normalizeReplMemoryView(value) {
|
|
220
|
+
if (!exactFields(value, VIEW_FIELDS)) throw new Error('invalid dsh-ptc-plus REPL memory view')
|
|
221
|
+
return Object.freeze({
|
|
222
|
+
runtime: normalizeGeneration(value.runtime),
|
|
223
|
+
memory: normalizeReplMemorySnapshot(value.memory),
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
208
227
|
/** Reuse fields are version 5 additions; an older metadata version cannot own them. */
|
|
209
228
|
function countedMemory(memory) {
|
|
210
229
|
return memory?.reuseTotal !== undefined
|
|
@@ -349,8 +368,8 @@ export function createReplMemoryProjection(generation) {
|
|
|
349
368
|
return state
|
|
350
369
|
},
|
|
351
370
|
wire: Object.freeze({
|
|
352
|
-
viewSchema: Object.freeze({ parse:
|
|
353
|
-
view: state => state.memory,
|
|
371
|
+
viewSchema: Object.freeze({ parse: normalizeReplMemoryView }),
|
|
372
|
+
view: state => Object.freeze({ runtime: state.generation, memory: state.memory }),
|
|
354
373
|
}),
|
|
355
374
|
})
|
|
356
375
|
}
|
|
@@ -24,6 +24,8 @@ import {
|
|
|
24
24
|
toolCapabilityMetadata,
|
|
25
25
|
} from './program-bindings.js'
|
|
26
26
|
import { deepFreeze } from './record-utils.js'
|
|
27
|
+
import { adaptRunCodeSchema } from './direct-surface-owner.js'
|
|
28
|
+
import { hostTransportDefinition, isBatchSubmission } from './host-tool-sdk.js'
|
|
27
29
|
import { REPL_MEMORY_META_KEY, withReplMemorySnapshot } from './repl-memory-projection.js'
|
|
28
30
|
import { withUserBindingDraftCapability } from './user-binding-draft-projection.js'
|
|
29
31
|
import { POST_EDIT_KEY } from './session-journal-schema.js'
|
|
@@ -160,6 +162,7 @@ export function createRuntimeBridgeOwner({
|
|
|
160
162
|
userBindingDraftForAgent,
|
|
161
163
|
bindingSubmissionForAgent,
|
|
162
164
|
capabilityEnvironment,
|
|
165
|
+
readPromptText = () => undefined,
|
|
163
166
|
}) {
|
|
164
167
|
const scope = new AsyncLocalStorage()
|
|
165
168
|
const childScope = new AsyncLocalStorage()
|
|
@@ -185,6 +188,8 @@ export function createRuntimeBridgeOwner({
|
|
|
185
188
|
})
|
|
186
189
|
let currentConfig = sessions.config
|
|
187
190
|
const patchedDefinitions = new Map()
|
|
191
|
+
/** The host descriptors this attachment replaced on each definition's slots. */
|
|
192
|
+
const slotPatches = new Map()
|
|
188
193
|
const pending = new WeakMap()
|
|
189
194
|
const nativeRunCodeHandoffs = new Set()
|
|
190
195
|
let active = true
|
|
@@ -393,14 +398,94 @@ export function createRuntimeBridgeOwner({
|
|
|
393
398
|
return { request: { ...request, bindings: projected }, release }
|
|
394
399
|
}
|
|
395
400
|
|
|
401
|
+
/**
|
|
402
|
+
* Write one editable run_code slot onto the host's definition.
|
|
403
|
+
*
|
|
404
|
+
* A host may publish the slot as writable data or as a getter-only accessor over
|
|
405
|
+
* its own text, and strict-mode assignment throws on the latter. An owned property
|
|
406
|
+
* definition reaches both shapes, so the plugin never fails silently to deliver the
|
|
407
|
+
* text it was asked to deliver.
|
|
408
|
+
* @param {object} definition - the host's live run_code definition.
|
|
409
|
+
* @param {string} key - the definition field being replaced.
|
|
410
|
+
* @param {unknown} value - the text the plugin owns.
|
|
411
|
+
* @returns {boolean} whether the field now holds that value.
|
|
412
|
+
*/
|
|
413
|
+
function writeSlot(definition, key, value) {
|
|
414
|
+
const own = Object.getOwnPropertyDescriptor(definition, key)
|
|
415
|
+
try {
|
|
416
|
+
Object.defineProperty(definition, key, {
|
|
417
|
+
configurable: own?.configurable ?? true,
|
|
418
|
+
enumerable: own?.enumerable ?? true,
|
|
419
|
+
writable: true,
|
|
420
|
+
value,
|
|
421
|
+
})
|
|
422
|
+
return true
|
|
423
|
+
} catch {
|
|
424
|
+
return false
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Put one slot back to the descriptor the host published before this plugin. */
|
|
429
|
+
function restoreSlot(definition, key, descriptor) {
|
|
430
|
+
try {
|
|
431
|
+
if (descriptor === undefined) delete definition[key]
|
|
432
|
+
else Object.defineProperty(definition, key, descriptor)
|
|
433
|
+
} catch {
|
|
434
|
+
// A host that made the slot non-configurable after the plugin replaced it keeps
|
|
435
|
+
// the last written text; the restore cannot be forced, and the caller that
|
|
436
|
+
// rewrote it owns the consequence.
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* The editable run_code slots have to reach whichever definition the host lets this
|
|
442
|
+
* plugin own. Two writes, one decision: if the host's parameter texts cannot be
|
|
443
|
+
* replaced, the definition keeps all of its own text rather than half of ours. The
|
|
444
|
+
* descriptors the write replaced are recorded once, so re-applying the current
|
|
445
|
+
* prompt document on every request can never mistake the plugin's own text for the
|
|
446
|
+
* host's, and disposal can restore what the host still publishes.
|
|
447
|
+
*/
|
|
448
|
+
function applyEditableSlots(definition, promptText, slotPatches) {
|
|
449
|
+
if (promptText === undefined) return
|
|
450
|
+
let adapted
|
|
451
|
+
try {
|
|
452
|
+
adapted = adaptRunCodeSchema(definition, promptText)
|
|
453
|
+
} catch {
|
|
454
|
+
return
|
|
455
|
+
}
|
|
456
|
+
const recorded = slotPatches.get(definition)
|
|
457
|
+
if (recorded !== undefined) {
|
|
458
|
+
// Re-applied on every request: the descriptors recorded by the first write are
|
|
459
|
+
// the ones disposal restores, so a later application can never mistake the
|
|
460
|
+
// plugin's own text for the host's. A slot the host has since made unreplaceable
|
|
461
|
+
// keeps the text this attachment already delivered.
|
|
462
|
+
writeSlot(definition, 'parameters', adapted.parameters)
|
|
463
|
+
writeSlot(definition, 'description', adapted.description)
|
|
464
|
+
return
|
|
465
|
+
}
|
|
466
|
+
const ownParameters = Object.getOwnPropertyDescriptor(definition, 'parameters')
|
|
467
|
+
const ownDescription = Object.getOwnPropertyDescriptor(definition, 'description')
|
|
468
|
+
if (!writeSlot(definition, 'parameters', adapted.parameters)) return
|
|
469
|
+
if (!writeSlot(definition, 'description', adapted.description)) {
|
|
470
|
+
restoreSlot(definition, 'parameters', ownParameters)
|
|
471
|
+
return
|
|
472
|
+
}
|
|
473
|
+
slotPatches.set(definition, new Map([['parameters', ownParameters], ['description', ownDescription]]))
|
|
474
|
+
}
|
|
475
|
+
|
|
396
476
|
const patchRunCodeDefinition = (agent) => {
|
|
397
|
-
const definition = ctx.tools
|
|
477
|
+
const definition = hostTransportDefinition(ctx.tools, agent, RUN_CODE)
|
|
398
478
|
if (definition === undefined) {
|
|
399
479
|
throw new Error('ptc-plus: run_code definition is unavailable for the owning session')
|
|
400
480
|
}
|
|
401
481
|
if (definition.output === undefined) {
|
|
402
482
|
throw new Error('ptc-plus: run_code definition has no output projection')
|
|
403
483
|
}
|
|
484
|
+
// A host that declares its own model-facing tools never hands this plugin a
|
|
485
|
+
// tools array to adapt, so the editable prompt slots reach the model only on
|
|
486
|
+
// the definition the executor projects. Re-applied on every request, because
|
|
487
|
+
// the prompt document can change while the definition stays the same object.
|
|
488
|
+
applyEditableSlots(definition, readPromptText(), slotPatches)
|
|
404
489
|
if (patchedDefinitions.has(definition)) return
|
|
405
490
|
const output = definition.output
|
|
406
491
|
const ownPresentationMeta = Object.getOwnPropertyDescriptor(output, 'presentationMeta')
|
|
@@ -463,10 +548,10 @@ export function createRuntimeBridgeOwner({
|
|
|
463
548
|
// nested success. Capture its canonical value before policy; the private
|
|
464
549
|
// relation is admitted only if policy retains that same frozen value.
|
|
465
550
|
if (current?.deferredSettlement?.current === current && derived !== undefined
|
|
466
|
-
&& current.exec === exec && exec.token !== undefined
|
|
551
|
+
&& current.exec === exec && exec.token !== undefined
|
|
467
552
|
&& current.id === sessionId(exec.agent) && derived.agent === exec.agent
|
|
468
|
-
&& derived
|
|
469
|
-
&&
|
|
553
|
+
&& isDerivedExecution(derived, exec)
|
|
554
|
+
&& deepEqualJson(derived.arguments, exec.arguments)
|
|
470
555
|
&& current.presentationMetaCalls === undefined && result.isError === false
|
|
471
556
|
&& result.value !== null && typeof result.value === 'object'
|
|
472
557
|
&& Object.isFrozen(result.value) && result.meta?.[JOURNAL_KEY] === undefined
|
|
@@ -610,13 +695,30 @@ export function createRuntimeBridgeOwner({
|
|
|
610
695
|
|
|
611
696
|
const releaseSeam = seam.installExecute(patchedRun)
|
|
612
697
|
|
|
698
|
+
/**
|
|
699
|
+
* Whether this execution is the derived execution the edit transport submitted.
|
|
700
|
+
*
|
|
701
|
+
* A registry host runs the very object the plugin built, so the call id, the root
|
|
702
|
+
* call and the parent token it carries all prove the relation. The batch contract
|
|
703
|
+
* assigns its own identity instead: the parent token is the host's own and the
|
|
704
|
+
* root call is whichever call that host treats as direct. What survives there is
|
|
705
|
+
* the call id this plugin chose, which the host matched the batch result back to,
|
|
706
|
+
* together with the dispatch record; the derived source is compared beside it.
|
|
707
|
+
*/
|
|
708
|
+
const isDerivedExecution = (derived, exec) => {
|
|
709
|
+
if (derived === null || typeof derived !== 'object' || exec === null || typeof exec !== 'object') return false
|
|
710
|
+
if (derived.callId !== exec.callId) return false
|
|
711
|
+
if (isBatchSubmission(derived)) return true
|
|
712
|
+
return exec.parent !== undefined
|
|
713
|
+
&& derived.rootCallId === exec.rootCallId && derived.parent === exec.parent
|
|
714
|
+
}
|
|
715
|
+
|
|
613
716
|
const ownedDerivedScope = (exec, executionArguments = exec.arguments) => {
|
|
614
717
|
const inherited = scope.getStore()
|
|
615
718
|
const derived = inherited?.derivedExecution
|
|
616
719
|
if (inherited?.deferredSettlement === undefined
|
|
617
720
|
|| inherited.id !== sessionId(exec.agent) || derived?.agent !== exec.agent
|
|
618
|
-
|| derived
|
|
619
|
-
|| derived.parent !== exec.parent || exec.name !== RUN_CODE
|
|
721
|
+
|| !isDerivedExecution(derived, exec) || exec.name !== RUN_CODE
|
|
620
722
|
|| !deepEqualJson(derived.arguments, executionArguments)) return undefined
|
|
621
723
|
return inherited
|
|
622
724
|
}
|
|
@@ -800,8 +902,7 @@ export function createRuntimeBridgeOwner({
|
|
|
800
902
|
const derived = current.derivedExecution
|
|
801
903
|
const privateMetadata = projection !== undefined && projection.exec === exec
|
|
802
904
|
&& deferred.current === current && current.exec === exec && current.id === id
|
|
803
|
-
&& derived?.agent === exec.agent && derived
|
|
804
|
-
&& derived.rootCallId === exec.rootCallId && derived.parent === exec.parent
|
|
905
|
+
&& derived?.agent === exec.agent && isDerivedExecution(derived, exec)
|
|
805
906
|
&& deepEqualJson(derived.arguments, exec.arguments)
|
|
806
907
|
&& projection.token === exec.token && projection.value === result?.value
|
|
807
908
|
&& result.isError === false && deepEqualJson(projection.meta, result.meta)
|
|
@@ -831,7 +932,7 @@ export function createRuntimeBridgeOwner({
|
|
|
831
932
|
&& deepEqualJson(meta?.[POST_EDIT_KEY], current.postEdit.provenance))
|
|
832
933
|
},
|
|
833
934
|
disposeAgent(agent) {
|
|
834
|
-
return sessions.
|
|
935
|
+
return sessions.disposeAgent(sessionId(agent) ?? String(agent.id))
|
|
835
936
|
},
|
|
836
937
|
disposeSession(session) {
|
|
837
938
|
return sessions.disposeSession(String(session.id))
|
|
@@ -868,6 +969,10 @@ export function createRuntimeBridgeOwner({
|
|
|
868
969
|
}
|
|
869
970
|
}
|
|
870
971
|
patchedDefinitions.clear()
|
|
972
|
+
for (const [definition, recorded] of slotPatches) {
|
|
973
|
+
for (const [key, descriptor] of recorded) restoreSlot(definition, key, descriptor)
|
|
974
|
+
}
|
|
975
|
+
slotPatches.clear()
|
|
871
976
|
try {
|
|
872
977
|
await sessions.dispose()
|
|
873
978
|
} catch (error) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { capabilityPolicyForConfig } from './capability-policy.js'
|
|
2
|
+
import { validatePromptTextDocument } from './prompt-text.js'
|
|
2
3
|
import {
|
|
3
4
|
CONFIG_DEFAULTS,
|
|
4
5
|
CONFIG_FIELDS,
|
|
@@ -117,6 +118,7 @@ export function resolveConfig(config = {}) {
|
|
|
117
118
|
}
|
|
118
119
|
for (const field of CONFIG_FIELDS) assertField(field, resolved[field.key])
|
|
119
120
|
validateCapabilityProfiles(JSON.parse(resolved.capabilityProfiles))
|
|
121
|
+
validatePromptTextDocument(resolved.promptText)
|
|
120
122
|
for (const [key, array] of [['capabilityBaseline', true], ['capabilityOverrides', false]]) {
|
|
121
123
|
let value
|
|
122
124
|
try { value = JSON.parse(resolved[key]) } catch { throw new TypeError(`ptc-plus: ${key} must contain valid JSON`) }
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { latestRecoveryTip, runtimeHistoryForView } from './recovery-tips.js'
|
|
2
|
+
import { HOT_CELL_DROPPED, HOT_CELL_DROPPED_EN } from './projection-retention.js'
|
|
2
3
|
import { foldSurface, deriveEventMessage, snapshotSessionEvent } from '@deepseek-ai/dsh-session'
|
|
3
4
|
import { currentSessionMessageProjections } from '@deepseek-ai/dsh-session-format-catalog/message-projections'
|
|
4
5
|
import { readSessionLog } from './session-events.js'
|
|
@@ -75,6 +76,12 @@ export async function handBackRuntimeSessions(ctx, owners) {
|
|
|
75
76
|
function continuationFeedback(view) {
|
|
76
77
|
const run = view.latestRun
|
|
77
78
|
const rewrites = run?.rewrites
|
|
79
|
+
if (run === undefined && view.hotCellDropped === true) {
|
|
80
|
+
// The newest cell lost its payload to the retention budget. Silence here reads as
|
|
81
|
+
// "nothing to advise", which is indistinguishable from a healthy session; tell the
|
|
82
|
+
// model that the evidence was removed rather than absent.
|
|
83
|
+
return `${HOT_CELL_DROPPED} / ${HOT_CELL_DROPPED_EN} Inspect the tool result of the dropped cell before continuing; do not replay it automatically.`
|
|
84
|
+
}
|
|
78
85
|
if (rewrites === undefined || rewrites.length === 0 || run.journal !== undefined) return undefined
|
|
79
86
|
const descriptions = rewrites.map(rewrite => rewrite.description).join('; ')
|
|
80
87
|
const details = descriptions.length <= 2048 ? descriptions
|
|
@@ -100,7 +107,7 @@ export function sessionRuntimeContexts(agent, tipConfig, options = {}) {
|
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
/** Project messages without treating an uncommitted proposal as delivery. */
|
|
103
|
-
export function projectRuntimeMessages(view, contexts, pending = []) {
|
|
110
|
+
export function projectRuntimeMessages(view, contexts, pending = [], options) {
|
|
104
111
|
if (view.visibleRuntimeMessages === undefined) return []
|
|
105
112
|
const sections = contexts.filter(context => PTC_STATE_NAMES.includes(context.name) && context.name !== PTC_BINDING_CATALOG)
|
|
106
113
|
const catalog = contexts.find(context => context.name === PTC_BINDING_CATALOG)
|
|
@@ -114,7 +121,7 @@ export function projectRuntimeMessages(view, contexts, pending = []) {
|
|
|
114
121
|
?? (pendingAggregate ?? legacy?.sections)?.filter(section => PTC_STATE_NAMES.includes(section.name))
|
|
115
122
|
const hadState = history.hadState
|
|
116
123
|
const messages = []
|
|
117
|
-
const proposed = pending.map(readRuntimeMessage).filter(record => record !== undefined)
|
|
124
|
+
const proposed = pending.map(record => readRuntimeMessage(record)).filter(record => record !== undefined)
|
|
118
125
|
const pendingState = proposed.filter(record => record.form === 'snapshot').at(-1)
|
|
119
126
|
const previous = pendingState?.sections ?? retained
|
|
120
127
|
if ((sections.length > 0 || (previous !== undefined && previous.length > 0) || (previous === undefined && hadState))
|
|
@@ -126,7 +133,7 @@ export function projectRuntimeMessages(view, contexts, pending = []) {
|
|
|
126
133
|
const hadCatalog = history.hadCatalog
|
|
127
134
|
if ((catalog !== undefined || previousCatalog !== undefined || hadCatalog)
|
|
128
135
|
&& JSON.stringify(previousCatalog?.sections) !== JSON.stringify(catalog === undefined ? [] : [catalog])) {
|
|
129
|
-
messages.push(runtimeBindingCatalogMessage(catalog))
|
|
136
|
+
messages.push(runtimeBindingCatalogMessage(catalog, options))
|
|
130
137
|
}
|
|
131
138
|
for (const tip of contexts.filter(context => context.name.startsWith('tools:ptc-plus-tip/'))) {
|
|
132
139
|
if (proposed.some(record => record.form === 'notice' && record.name === tip.name)) continue
|
|
@@ -137,7 +144,7 @@ export function projectRuntimeMessages(view, contexts, pending = []) {
|
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
/** Passive message delivery follows the actual public assembly and accepted step. */
|
|
140
|
-
export function createRuntimeMessageOwner(contextsForRequest, sessionLogView = projectSessionLog, activeForRequest, sessionQuery) {
|
|
147
|
+
export function createRuntimeMessageOwner(contextsForRequest, sessionLogView = projectSessionLog, activeForRequest, sessionQuery, promptForRequest) {
|
|
141
148
|
const requests = new WeakMap()
|
|
142
149
|
const disposedAgents = new WeakSet()
|
|
143
150
|
let disposed = false
|
|
@@ -162,7 +169,9 @@ export function createRuntimeMessageOwner(contextsForRequest, sessionLogView = p
|
|
|
162
169
|
if (disposed || decision.kind !== 'enter' || payload.signal?.aborted === true
|
|
163
170
|
|| request?.allowed !== true || request.context.signal !== payload.signal) return decision
|
|
164
171
|
const view = sessionLogView(payload.agent)
|
|
165
|
-
const
|
|
172
|
+
const resolved = promptForRequest === undefined ? undefined : promptForRequest(request.context)
|
|
173
|
+
const messages = projectRuntimeMessages(view, contextsForRequest(request.context), decision.messages,
|
|
174
|
+
resolved === undefined ? undefined : { prompt: resolved })
|
|
166
175
|
if (activeForRequest !== undefined) {
|
|
167
176
|
const log = await readSessionLog(payload.agent.session, sessionQuery, payload.signal)
|
|
168
177
|
const availability = projectRuntimeAvailability([
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
2
|
+
import {
|
|
3
|
+
CATALOG_PREFIX, EMPTY_CATALOG, LEGACY_STATE_PREFIX, PREVIOUS_EMPTY_CATALOG, PREVIOUS_STATE_PREFIX,
|
|
4
|
+
PROMPT_SLOT_IDS, PROMPT_TEXT_DEFAULT, STATE_PREFIX, resolvePromptText,
|
|
5
|
+
} from './prompt-text.js'
|
|
2
6
|
|
|
3
7
|
import { isPtcMessageSource, PTC_MESSAGE_SOURCE_KIND } from './message-sources.js'
|
|
8
|
+
import { BINDING_ADOPTION_FORM, readBindingAdoption } from './binding-adoption.js'
|
|
4
9
|
|
|
5
10
|
export const PTC_DELIVERY_CONTEXT = 'tools:ptc-plus-message-delivery'
|
|
6
11
|
export const PTC_BINDING_CATALOG = 'tools:ptc-plus-user-binding-defaults'
|
|
@@ -13,16 +18,12 @@ export const PTC_STATE_NAMES = Object.freeze([
|
|
|
13
18
|
])
|
|
14
19
|
const MAX_STATE_CODE_UNITS = 65536
|
|
15
20
|
const MAX_NOTICE_CODE_UNITS = 8192
|
|
16
|
-
|
|
17
|
-
const PREVIOUS_STATE_PREFIX = 'PTC Plus runtime recovery state. This replaces only earlier PTC state snapshots and PTC sections in historical aggregate snapshots; it does not replace the global binding API catalog, tasks, Skill instructions, tool results, or other producers.'
|
|
21
|
+
|
|
18
22
|
const PREVIOUS_CATALOG_PREFIXES = [
|
|
19
23
|
'Global binding API catalog (PTC Plus). Only a later global binding API catalog replaces this catalog. Host runtime-context snapshots and PTC recovery snapshots do not withdraw it. This describes configured APIs, not successful initialization or current runtime values.',
|
|
20
24
|
'Global binding API catalog (PTC Plus). Replaces earlier binding catalogs only; remains applicable until the next binding catalog.',
|
|
21
25
|
]
|
|
22
|
-
|
|
23
|
-
const CATALOG_PREFIX = 'Global binding API reference for run_code (replaces the previous global binding reference):'
|
|
24
|
-
const EMPTY_CATALOG = 'No entries documented.'
|
|
25
|
-
const PREVIOUS_EMPTY_CATALOG = 'No global binding API documentation is currently configured. Earlier binding catalogs no longer apply.'
|
|
26
|
+
|
|
26
27
|
const AVAILABILITY_TEXT = Object.freeze({
|
|
27
28
|
active: 'PTC Plus is the current continuous-computation provider. Follow its current tool schemas and capability plan; earlier withdrawals do not restore old computation state.',
|
|
28
29
|
disabled: 'PTC Plus is disabled. Earlier PTC Plus continuous-computation instructions, capability plans, discovery instructions, calling-contract disclosures and global binding references no longer describe the current runtime. Use the current Host tools and contracts. Historical computations and effects remain historical; no old PTC Plus heap has been restored. Tasks, Skill instructions, other producers and tool results are unchanged.',
|
|
@@ -56,10 +57,13 @@ export function projectRuntimeAvailability(messages, active, computationDeclared
|
|
|
56
57
|
const last = messages.map(readRuntimeAvailability).filter(Boolean).at(-1)
|
|
57
58
|
if (active) return last && last !== 'active' ? runtimeAvailabilityMessage('active') : undefined
|
|
58
59
|
if (last === 'disabled' || last === 'unloaded') return undefined
|
|
59
|
-
const declared = computationDeclared || last === 'active' || messages.some(message =>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
const declared = computationDeclared || last === 'active' || messages.some(message => {
|
|
61
|
+
const recognized = isPtcMessageSource(message?.source) ? readRuntimeMessage(message) : undefined
|
|
62
|
+
return isPtcMessageSource(message?.source)
|
|
63
|
+
&& (['capability-navigation', 'capability-descriptions', 'capability-contracts'].includes(message.source.form)
|
|
64
|
+
|| message.source.form === 'capability-plan' && message.source.snapshot?.plan?.enabled === true
|
|
65
|
+
|| recognized?.form === 'catalog' && recognized.sections.length > 0)
|
|
66
|
+
})
|
|
63
67
|
return declared ? runtimeAvailabilityMessage('disabled') : undefined
|
|
64
68
|
}
|
|
65
69
|
|
|
@@ -92,6 +96,18 @@ function stateText(sections, prefix = STATE_PREFIX) {
|
|
|
92
96
|
: sections.map(section => section.text).join('\n\n')}`
|
|
93
97
|
}
|
|
94
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Historical emptiness wording a recorded catalog may carry.
|
|
101
|
+
*
|
|
102
|
+
* These are code-owned constants, not the resolved document: a session record
|
|
103
|
+
* written by an older build must stay readable after today's sentence changes,
|
|
104
|
+
* and recognizing it must never depend on the user's current override.
|
|
105
|
+
*/
|
|
106
|
+
const HISTORICAL_EMPTY_CATALOGS = Object.freeze([EMPTY_CATALOG, PREVIOUS_EMPTY_CATALOG])
|
|
107
|
+
|
|
108
|
+
/** The resolved document a producer used. Recognition never depends on its text. */
|
|
109
|
+
const promptOf = prompt => prompt ?? resolvePromptText(PROMPT_TEXT_DEFAULT)
|
|
110
|
+
|
|
95
111
|
/** Recognize only bounded, formed PTC records, never authoring tasks or Skills. */
|
|
96
112
|
export function readRuntimeMessage(message) {
|
|
97
113
|
const source = message?.source
|
|
@@ -101,6 +117,10 @@ export function readRuntimeMessage(message) {
|
|
|
101
117
|
const text = message.content[0].text
|
|
102
118
|
const availability = readRuntimeAvailability(message)
|
|
103
119
|
if (availability !== undefined) return { form: 'runtime-availability', state: availability }
|
|
120
|
+
// A session's binding adoption is recorded as one of its own messages, so the
|
|
121
|
+
// log is its only owner and replay reconstructs it without process memory.
|
|
122
|
+
const adoption = readBindingAdoption(message)
|
|
123
|
+
if (adoption !== undefined) return { form: BINDING_ADOPTION_FORM, declined: [...adoption.declined] }
|
|
104
124
|
if (source.form === 'snapshot') {
|
|
105
125
|
const sections = stateSections(source.sections)
|
|
106
126
|
if (sections !== undefined && [STATE_PREFIX, PREVIOUS_STATE_PREFIX, LEGACY_STATE_PREFIX]
|
|
@@ -112,7 +132,12 @@ export function readRuntimeMessage(message) {
|
|
|
112
132
|
if (prefix === undefined) return undefined
|
|
113
133
|
const body = text.slice(prefix.length + 2)
|
|
114
134
|
if (body.length > 0 && body.length <= MAX_STATE_CODE_UNITS) {
|
|
115
|
-
|
|
135
|
+
// Emptiness is a property of the record (`source.empty`), so live delivery
|
|
136
|
+
// and log replay agree even when the empty sentence is customized. Records
|
|
137
|
+
// written before that flag existed are matched against the code-owned
|
|
138
|
+
// historical wording, never against the current resolved text.
|
|
139
|
+
const empty = source.empty === true
|
|
140
|
+
|| (source.empty === undefined && HISTORICAL_EMPTY_CATALOGS.includes(body))
|
|
116
141
|
return { form: 'catalog', sections: stateSections(empty ? [] : [{ name: PTC_BINDING_CATALOG, text: body }]) }
|
|
117
142
|
}
|
|
118
143
|
} else if (source.form === 'notice' && recoveryTipIdentity(source.summary) !== undefined
|
|
@@ -123,15 +148,23 @@ export function readRuntimeMessage(message) {
|
|
|
123
148
|
}
|
|
124
149
|
|
|
125
150
|
/** One literal configuration document, independent of runtime recovery state. */
|
|
126
|
-
export function runtimeBindingCatalogMessage(context) {
|
|
151
|
+
export function runtimeBindingCatalogMessage(context, options) {
|
|
152
|
+
const resolved = promptOf(options?.prompt)
|
|
153
|
+
const catalogPrefix = resolved.text(PROMPT_SLOT_IDS.catalogPrefix)
|
|
154
|
+
const emptyCatalog = resolved.text(PROMPT_SLOT_IDS.catalogEmpty)
|
|
155
|
+
// Emptiness travels in `source.empty`, so a catalog body may legitimately equal
|
|
156
|
+
// the code-owned empty wording; comparing the text would abort the whole step for
|
|
157
|
+
// a deliverable catalog. Only the shape is rejected here.
|
|
127
158
|
if (context !== undefined && (context?.name !== PTC_BINDING_CATALOG
|
|
128
159
|
|| typeof context.text !== 'string' || context.text.length === 0
|
|
129
|
-
|| context.text.length > MAX_STATE_CODE_UNITS
|
|
160
|
+
|| context.text.length > MAX_STATE_CODE_UNITS)) {
|
|
130
161
|
throw new Error('invalid PTC binding catalog')
|
|
131
162
|
}
|
|
132
163
|
return createUserMessage({
|
|
133
|
-
|
|
134
|
-
|
|
164
|
+
// `empty` records the fact itself, so replay recognizes an empty catalog from
|
|
165
|
+
// the record instead of comparing it against today's sentence.
|
|
166
|
+
source: { kind: PTC_MESSAGE_SOURCE_KIND, form: 'catalog', empty: context === undefined },
|
|
167
|
+
content: [{ type: 'text', text: `${catalogPrefix}\n\n${context?.text ?? emptyCatalog}` }],
|
|
135
168
|
})
|
|
136
169
|
}
|
|
137
170
|
|