dsh-ptc-plus 0.5.2 → 0.5.4
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 +4 -2
- package/README.md +4 -2
- package/client.cjs +3392 -2811
- package/compiler-core.cjs +127 -127
- package/compiler-core.cjs.map +2 -2
- package/docs/README.md +1 -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/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/0044-coexist-with-the-official-persistent-repl.md +4 -4
- package/docs/adr/0046-gate-the-commit-with-the-semantic-evidence-tier.md +1 -1
- 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 +1 -1
- package/docs/capability-planning-components.md +6 -6
- package/docs/capability-projection.md +1 -1
- package/docs/capability-surface.md +133 -0
- package/docs/client-ui.md +17 -15
- package/docs/client-visual-contract.md +7 -7
- package/docs/durability-design.md +9 -2
- package/docs/installation.md +17 -10
- package/docs/panel-concept.md +171 -0
- package/docs/releases/v0.5.2.md +26 -14
- package/docs/releases/v0.5.3.md +77 -0
- package/docs/releases/v0.5.4.md +75 -0
- package/docs/runtime-reference.md +21 -1
- 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 +4 -4
- package/index.js +176 -41
- package/internal/binding-adoption.js +228 -0
- package/internal/capability-command-receipt.js +106 -8
- package/internal/capability-environment.js +1109 -312
- package/internal/capability-plan.js +18 -2
- package/internal/capability-policy.js +13 -0
- package/internal/capability-selection.js +330 -0
- package/internal/config-spec.js +14 -5
- package/internal/cordis-tools-owner.js +2 -1
- package/internal/direct-surface-owner.js +44 -17
- package/internal/edit-transport-owner.js +6 -5
- 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/message-sources.js +21 -0
- package/internal/official-repl-priority.js +31 -10
- package/internal/program-bindings.js +25 -5
- package/internal/repl-memory-projection.js +21 -2
- package/internal/runtime-bridge-owner.js +118 -9
- package/internal/runtime-config.js +10 -0
- package/internal/runtime-messages.js +5 -0
- package/internal/session-journal-recovery.js +210 -32
- package/internal/session-journal-schema.js +3 -0
- package/internal/session-journal.js +46 -9
- package/internal/session-log-view.js +69 -7
- package/internal/session-runtime.js +106 -39
- package/internal/settings-compat.js +6 -0
- package/internal/user-bindings-owner.js +126 -6
- package/package.json +59 -37
- package/src/client-authoring-view.js +7 -8
- package/src/client-capabilities-view.js +874 -909
- package/src/client-capability-sort.js +22 -9
- package/src/client-copy.js +118 -272
- package/src/client-indicator-view.js +2 -1
- package/src/client-repl-view.js +35 -16
- package/src/client-settings-view.js +90 -33
- package/src/client-styles.js +67 -19
- package/src/client.js +49 -22
package/index.js
CHANGED
|
@@ -13,8 +13,9 @@ import { CapabilityStatisticsStore } from './internal/capability-statistics-stor
|
|
|
13
13
|
import { createCapabilityEnvironment } from './internal/capability-environment.js'
|
|
14
14
|
import { capabilityCommandReceipt } from './internal/capability-command-receipt.js'
|
|
15
15
|
import { toolSdkSchemas } from './internal/program-bindings.js'
|
|
16
|
+
import { hostTransportDefinition, hostTransportUnreadable } from './internal/host-tool-sdk.js'
|
|
16
17
|
import { createRuntimeMessageOwner, handBackRuntimeSessions } from './internal/runtime-contexts.js'
|
|
17
|
-
import { createSessionLogOwner } from './internal/session-log-view.js'
|
|
18
|
+
import { createSessionLogOwner, projectionRegistrationDisposer } from './internal/session-log-view.js'
|
|
18
19
|
import { PTC_DELIVERY_CONTEXT } from './internal/runtime-messages.js'
|
|
19
20
|
import { createCordisToolsOwner } from './internal/cordis-tools-owner.js'
|
|
20
21
|
import { createEditTransportOwner, EDIT_RUN_CODE } from './internal/edit-transport-owner.js'
|
|
@@ -32,6 +33,7 @@ import { valueLimitsFromConfig } from './internal/value-wire-schema.js'
|
|
|
32
33
|
import { createReplMemoryProjection } from './internal/repl-memory-projection.js'
|
|
33
34
|
import { createUserBindingDraftProjection } from './internal/user-binding-draft-projection.js'
|
|
34
35
|
import { createUserBindingsOwner } from './internal/user-bindings-owner.js'
|
|
36
|
+
import { latestBindingAdoption } from './internal/binding-adoption.js'
|
|
35
37
|
import { createHostRpc } from './internal/host-rpc.js'
|
|
36
38
|
import { RPC_CONTRACTS } from './internal/rpc-contract.js'
|
|
37
39
|
import { createReplObservationInterest } from './internal/repl-observation-interest.js'
|
|
@@ -112,8 +114,58 @@ function replGuidance({ bindingPolicy, rewritesEnabled, languageSemantics, durab
|
|
|
112
114
|
return paragraphs.filter(paragraph => paragraph.trim().length > 0).join('\n\n')
|
|
113
115
|
}
|
|
114
116
|
|
|
117
|
+
/**
|
|
118
|
+
* The reserved `run_code` transport definition, from whichever surface this
|
|
119
|
+
* host publishes it on. Session execution is available exactly when one of
|
|
120
|
+
* those surfaces answers.
|
|
121
|
+
*/
|
|
122
|
+
function runCodeDefinitionFor(tools, scope) {
|
|
123
|
+
return hostTransportDefinition(tools, scope, RUN_CODE)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* One catalogue row in the shape `/capabilities list` publishes.
|
|
128
|
+
*
|
|
129
|
+
* The command states the panel's two columns, so an entry carries the same
|
|
130
|
+
* statement the panel's column carries: exactly the dimensions that row decides,
|
|
131
|
+
* each naming the answer its own source gave. A dimension this session recorded
|
|
132
|
+
* no choice for stays stated as `inherit`. The saved column states the words the
|
|
133
|
+
* configuration wrote for that object: its own entry, or `automatic` when it wrote
|
|
134
|
+
* none — a category, server or bottom layer that would answer at runtime is
|
|
135
|
+
* behaviour, so it reaches no cell (capability-surface §8.1c). A server row states
|
|
136
|
+
* `automatic` unless its own `mcpServers.<server>` rule writes availability. Compressing a
|
|
137
|
+
* row into one word would
|
|
138
|
+
* answer for the dimension it dropped — a record that decided disclosure but not
|
|
139
|
+
* availability would read as if the session had decided nothing. No column
|
|
140
|
+
* reports the composition: the panel states supply, record and the receipt, and
|
|
141
|
+
* this listing is the same read rather than a second one that could answer for
|
|
142
|
+
* what a runtime built.
|
|
143
|
+
*/
|
|
144
|
+
function capabilityListingEntry(row) {
|
|
145
|
+
// A server decides availability only: its members carry disclosure, so the panel
|
|
146
|
+
// states one value for such a row and this listing states the same one.
|
|
147
|
+
const statement = column => row.kind === 'server' ? { availability: column.availability }
|
|
148
|
+
: { availability: column.availability, disclosure: column.disclosure }
|
|
149
|
+
return { id: row.id, name: row.name, category: row.category, source: row.source,
|
|
150
|
+
...(typeof row.description === 'string' ? { description: row.description } : {}),
|
|
151
|
+
saved: statement(row.saved), temporary: statement(row.session) }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The composition a pending capability command made, in the shape a receipt carries.
|
|
156
|
+
*
|
|
157
|
+
* A command that composed a plan before the Host accepted a step has to carry it in its
|
|
158
|
+
* receipt: the plan is not in the log yet, so a restart would otherwise have no way to know
|
|
159
|
+
* which composition the command made. `recordedPlan` reads it back by matching the command's
|
|
160
|
+
* own id and raw input, which is why the proposal states both.
|
|
161
|
+
*/
|
|
162
|
+
function pendingProposal(capabilityEnvironment, commandId, rawInput, agent) {
|
|
163
|
+
const message = capabilityEnvironment.pendingChoiceMessage(agent)
|
|
164
|
+
return message === undefined ? undefined : { commandId, rawInput, message }
|
|
165
|
+
}
|
|
166
|
+
|
|
115
167
|
/** Register the session-bound REPL runtime. */
|
|
116
|
-
function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolSdkSchemasForAgent, sessionId, sessionLog, settingsForCommand) {
|
|
168
|
+
function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolSdkSchemasForAgent, sessionId, sessionLog, settingsForCommand, readPromptText) {
|
|
117
169
|
const presentationGeneration = randomUUID()
|
|
118
170
|
const replMemoryProjection = createReplMemoryProjection(presentationGeneration)
|
|
119
171
|
const userBindingDraftProjection = createUserBindingDraftProjection(presentationGeneration)
|
|
@@ -247,11 +299,17 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolS
|
|
|
247
299
|
const cancelCordisActivation = () => pendingCordisActivation?.cancel()
|
|
248
300
|
const registerProjection = (projection, label) => {
|
|
249
301
|
try {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
302
|
+
// A newer host returns a ProjectionHandle (`stateOf`/`readerFor`/`dispose`,
|
|
303
|
+
// possibly async); earlier hosts returned a disposer. Accept either shape so
|
|
304
|
+
// the projection is not silently dropped on the newer contract.
|
|
305
|
+
const dispose = projectionRegistrationDisposer(projectionService?.register?.(projection))
|
|
306
|
+
if (dispose === undefined) {
|
|
307
|
+
throw new Error('ptc-plus: sessionProjections.register returned neither a disposer nor a handle')
|
|
253
308
|
}
|
|
254
|
-
|
|
309
|
+
// Unregistration failures belong to the caller that is suspending or
|
|
310
|
+
// reloading the projection: it retries and reports them. Swallowing one
|
|
311
|
+
// here would turn a failed disposal into a silent duplicate projection.
|
|
312
|
+
return () => dispose()
|
|
255
313
|
} catch (error) {
|
|
256
314
|
ctx.logger?.warn?.(`ptc-plus: ${label} projection unavailable`, error)
|
|
257
315
|
return undefined
|
|
@@ -348,6 +406,9 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolS
|
|
|
348
406
|
readSessionLog: sessionLog.read,
|
|
349
407
|
enabled: activeConfig.userBindingsEnabled,
|
|
350
408
|
draftProjectionAvailable,
|
|
409
|
+
// A session's adoption is its own recorded decision, so it is read from
|
|
410
|
+
// that session's log instead of a process-local map.
|
|
411
|
+
bindingAdoptionForAgent: agent => latestBindingAdoption(sessionLog.project(agent)?.ptcMessages ?? []),
|
|
351
412
|
maxWallMs: activeConfig.maxWallMs,
|
|
352
413
|
maxOutputBytes: activeConfig.maxOutputBytes,
|
|
353
414
|
maxOldGenerationSizeMb: activeConfig.maxOldGenerationSizeMb,
|
|
@@ -355,11 +416,15 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolS
|
|
|
355
416
|
valueLimits: valueLimitsFromConfig(activeConfig),
|
|
356
417
|
})
|
|
357
418
|
capabilityEnvironment = createCapabilityEnvironment({ ctx, config: activeConfig,
|
|
358
|
-
toolSchemasForAgent: toolSdkSchemasForAgent,
|
|
419
|
+
toolSchemasForAgent: toolSdkSchemasForAgent,
|
|
420
|
+
// The panel's binding rows report the same catalog `/binding` acts on, so the
|
|
421
|
+
// read borrows the bindings owner's `list()` instead of opening a second store.
|
|
422
|
+
readUserBindings: () => userBindings.list(),
|
|
359
423
|
store: new CapabilityStatisticsStore({ ctx }) })
|
|
360
424
|
runtimeBridge = createRuntimeBridgeOwner({
|
|
361
425
|
ctx,
|
|
362
426
|
seam,
|
|
427
|
+
readPromptText,
|
|
363
428
|
observeSession: id => observationInterest.has(id),
|
|
364
429
|
readSessionLog: sessionLog.read,
|
|
365
430
|
sessionConfig: activeConfig,
|
|
@@ -392,7 +457,8 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolS
|
|
|
392
457
|
sessionId,
|
|
393
458
|
toolSchemasForAgent,
|
|
394
459
|
capabilityEnvironment,
|
|
395
|
-
|
|
460
|
+
declaresRunCode: agent => runCodeDefinitionFor(ctx.tools, agent) !== undefined,
|
|
461
|
+
userBindingsForAgent: agent => userBindings.snapshotForAgent(agent),
|
|
396
462
|
setAgentPresentation: (agent, presentation) => (
|
|
397
463
|
userBindings.setAgentPresentation(agent, presentation)
|
|
398
464
|
),
|
|
@@ -401,25 +467,37 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolS
|
|
|
401
467
|
name: 'tools:ptc-plus-repl',
|
|
402
468
|
order: 98,
|
|
403
469
|
text: context => {
|
|
404
|
-
if (ctx.tools
|
|
470
|
+
if (runCodeDefinitionFor(ctx.tools, context?.scope) === undefined) return ''
|
|
405
471
|
return replGuidance(guidancePolicies(activeConfig), resolvePromptTextConfig(activeConfig))
|
|
406
472
|
},
|
|
407
473
|
}))
|
|
474
|
+
let unreadableTransportReported = false
|
|
408
475
|
disposers.push(ctx.on('system-prompt/assemble', async (assembly, context, next) => {
|
|
409
476
|
const projected = await directSurface.assemble(assembly, context, next)
|
|
410
|
-
|
|
477
|
+
// A capability observation states what the Host declares. It prepares no session,
|
|
478
|
+
// so it neither re-applies the model-facing slots nor reports a transport the
|
|
479
|
+
// operator can do nothing about from a read.
|
|
480
|
+
if (context?.ptcPlusCapabilityObservation === true) return projected
|
|
481
|
+
if (runCodeDefinitionFor(ctx.tools, context?.agent) !== undefined) runtimeBridge.prepareAgent(context?.agent)
|
|
482
|
+
else if (!unreadableTransportReported && hostTransportUnreadable(ctx.tools, context?.agent)) {
|
|
483
|
+
// The host declares the reserved transport and this plugin cannot read it.
|
|
484
|
+
// Every gate that needs execution answers "unavailable", so the operator is
|
|
485
|
+
// told which of the two facts holds instead of finding the features gone.
|
|
486
|
+
unreadableTransportReported = true
|
|
487
|
+
ctx.logger?.warn?.('ptc-plus: the host tool executor declares run_code but publishes no transport definition this plugin can read; session execution stays unavailable')
|
|
488
|
+
}
|
|
411
489
|
return projected
|
|
412
490
|
}))
|
|
413
491
|
disposers.push(() => capabilityEnvironment.dispose())
|
|
414
492
|
disposers.push(ctx.on('agent/pre-step', (payload, next) => capabilityEnvironment.preStep(payload, next), { prepend: true }))
|
|
415
493
|
disposers.push(ctx.on('agent/created', ({ agent }) => {
|
|
416
|
-
if (ctx.tools
|
|
494
|
+
if (runCodeDefinitionFor(ctx.tools, agent) !== undefined) {
|
|
417
495
|
runtimeBridge.prepareAgent(agent)
|
|
418
496
|
editTransport.ensureInstalled(agent)
|
|
419
497
|
}
|
|
420
498
|
}))
|
|
421
499
|
for (const agent of ctx.agents?.list?.() ?? []) {
|
|
422
|
-
if (ctx.tools
|
|
500
|
+
if (runCodeDefinitionFor(ctx.tools, agent) !== undefined) {
|
|
423
501
|
runtimeBridge.prepareAgent(agent)
|
|
424
502
|
editTransport.ensureInstalled(agent)
|
|
425
503
|
}
|
|
@@ -432,7 +510,7 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolS
|
|
|
432
510
|
const capabilitiesRpc = ctx.inject(['ptcPlusRpc'], scope => {
|
|
433
511
|
scope.effect(() => scope.ptcPlusRpc.register(RPC_CONTRACTS.capabilities, async (operation, payload, signal) => {
|
|
434
512
|
signal.throwIfAborted()
|
|
435
|
-
if (operation === 'catalog') return { ok: true, value: await capabilityEnvironment.describeCatalog(signal) }
|
|
513
|
+
if (operation === 'catalog') return { ok: true, value: await capabilityEnvironment.describeCatalog(payload, signal) }
|
|
436
514
|
if (operation === 'targets') return { ok: true, value: capabilityEnvironment.describeTargets() }
|
|
437
515
|
if (operation === 'session-catalog') return { ok: true, value: await capabilityEnvironment.describeSessionCatalog(payload, signal) }
|
|
438
516
|
if (operation === 'allocation') return { ok: true, value: await capabilityEnvironment.observeAllocation(payload, signal) }
|
|
@@ -448,36 +526,58 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolS
|
|
|
448
526
|
input: { hint: 'refresh | list [all|skills|tools|mcp|mcp_tools] [query] | set session|global <id> [availability|disclosure] <choice>' },
|
|
449
527
|
handler: async ({ commandId, agent, rawInput, signal }) => {
|
|
450
528
|
const input = rawInput.trim()
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
529
|
+
// A composition the Host has not committed yet is not in the log, so a pending
|
|
530
|
+
// receipt carries it as the proposal it is: a restart reads that proposal back as
|
|
531
|
+
// the plan the command made. A record needs no proposal — it is in the log already.
|
|
532
|
+
const receipt = (state, error, record) => capabilityCommandReceipt(state, String(agent.session.id), error,
|
|
533
|
+
state === 'pending' ? pendingProposal(capabilityEnvironment, commandId, rawInput, agent) : undefined, record)
|
|
454
534
|
const selection = /^set (session|global) (\S+) (?:(availability|disclosure) )?(full|on-demand|closed|automatic|inherit|enabled|host|adaptive)$/.exec(input)
|
|
455
535
|
if (selection) {
|
|
456
|
-
|
|
536
|
+
// The write is stated before it happens: the receipt names the record
|
|
537
|
+
// this command asked for, and whether that record exists is the answer
|
|
538
|
+
// the command itself returns.
|
|
539
|
+
const record = { scope: selection[1], id: decodeURIComponent(selection[2]),
|
|
540
|
+
...(selection[3] === undefined ? {} : { dimension: selection[3] }), selection: selection[4] }
|
|
457
541
|
const changed = await capabilityEnvironment.change(agent, {
|
|
458
|
-
scope:
|
|
542
|
+
scope: record.scope, id: record.id, dimension: selection[3], selection: record.selection,
|
|
459
543
|
}, signal, settingsForCommand?.())
|
|
544
|
+
// A receipt states what this command wrote — which entry, which choice
|
|
545
|
+
// and which session or global configuration now records it. What a cell
|
|
546
|
+
// later obtained is that row's own `evidence` field, not a sentence here.
|
|
547
|
+
// A session choice is a record of this session's log, so it is durable the
|
|
548
|
+
// moment the command returns and needs no waiting state.
|
|
549
|
+
const state = changed.sessionApplied ? 'recorded' : 'saved-only'
|
|
460
550
|
return { kind: changed.sessionApplied ? 'success' : 'error',
|
|
461
|
-
text: receipt(changed.
|
|
551
|
+
text: receipt(state, changed.error, state === 'recorded' ? record : undefined) }
|
|
462
552
|
}
|
|
463
553
|
const listing = /^list(?: (all|skills|tools|mcp|mcp_tools))?(?: (.*))?$/.exec(input)
|
|
464
554
|
if (listing) {
|
|
465
|
-
|
|
555
|
+
// The listing is the panel's own read: one session catalogue, projected
|
|
556
|
+
// into the columns this command states. The session's scope supplies the
|
|
557
|
+
// declared face while the Host holds its agent, and the plan its log froze
|
|
558
|
+
// supplies it afterwards; the saved configuration states the saved column.
|
|
559
|
+
// Nothing about the PTC runtime enters it, so the two surfaces cannot
|
|
560
|
+
// disagree about which ids exist.
|
|
561
|
+
const catalog = await capabilityEnvironment.describeSessionCatalog({
|
|
562
|
+
sessionId: String(agent.session.id ?? agent.id),
|
|
563
|
+
}, signal)
|
|
466
564
|
const query = (listing[2] ?? '').toLocaleLowerCase()
|
|
467
565
|
const kind = listing[1] ?? 'all'
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
566
|
+
const entries = catalog.rows
|
|
567
|
+
.filter(row => row.kind !== 'binding')
|
|
568
|
+
.filter(row => kind === 'all' || kind === 'skills' && row.category === 'skill'
|
|
569
|
+
|| kind === 'tools' && row.category === 'tool' && row.source?.kind !== 'mcp'
|
|
570
|
+
|| kind === 'mcp' && ['server', 'resource'].includes(row.category)
|
|
571
|
+
|| kind === 'mcp_tools' && row.category === 'tool' && row.source?.kind === 'mcp')
|
|
572
|
+
.filter(row => !query || `${row.id} ${row.name ?? ''} ${row.description ?? ''}`.toLocaleLowerCase().includes(query))
|
|
573
|
+
.map(capabilityListingEntry)
|
|
574
|
+
return { kind: 'success', text: JSON.stringify({ schema: 'ptc-capability-catalog/v1', entries }) }
|
|
474
575
|
}
|
|
475
576
|
if (input !== '' && input !== 'refresh') return { kind: 'error', text: receipt('usage') }
|
|
476
577
|
if (input === 'refresh') {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
await capabilityEnvironment.refresh(agent, undefined, signal, commandId)
|
|
578
|
+
// The command acts on the session it was invoked in; the agent is the
|
|
579
|
+
// argument that states which composition to replace.
|
|
580
|
+
await capabilityEnvironment.refresh(agent, undefined, signal)
|
|
481
581
|
// A refresh always republishes the saved configuration; when that
|
|
482
582
|
// configuration has capability management or context off, report
|
|
483
583
|
// the off state instead of claiming an updated environment.
|
|
@@ -649,11 +749,16 @@ export const apply = (ctx, config = {}) => {
|
|
|
649
749
|
function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatileConfig) {
|
|
650
750
|
let settingsWriter
|
|
651
751
|
const sessionLog = createSessionLogOwner(ctx)
|
|
652
|
-
|
|
653
|
-
|
|
752
|
+
// The second argument is the step's admission. Naming it is how a caller states the set
|
|
753
|
+
// the Host actually delivers for that step; omitting it reads every definition
|
|
754
|
+
// registered at this scope, which is the only view a call outside a model step has.
|
|
755
|
+
const toolSchemasForAgent = (agent, toolSet = undefined) => typeof ctx.tools.schemas === 'function'
|
|
756
|
+
? ctx.tools.schemas(agent, toolSet)
|
|
654
757
|
: []
|
|
655
|
-
// SDK contracts include output schemas; model-facing schemas remain unchanged.
|
|
656
|
-
|
|
758
|
+
// SDK contracts include output schemas; model-facing schemas remain unchanged. The
|
|
759
|
+
// admission is read through the same registry, so a contract describes the definition
|
|
760
|
+
// this step delivers rather than one the registry would resolve.
|
|
761
|
+
const toolSdkSchemasForAgent = (agent, toolSet = undefined) => toolSdkSchemas(ctx.tools, agent, toolSet)
|
|
657
762
|
const sessionId = agent => {
|
|
658
763
|
const id = agent?.session?.id ?? agent?.id
|
|
659
764
|
return id === undefined ? undefined : String(id)
|
|
@@ -730,7 +835,9 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatil
|
|
|
730
835
|
// Execution ownership is a per-attachment property: `owned` installs the
|
|
731
836
|
// session runtime, `deferred` leaves run_code to the official provider.
|
|
732
837
|
const executionShape = nextConfig => (
|
|
733
|
-
defersToOfficialRepl(nextConfig,
|
|
838
|
+
defersToOfficialRepl(nextConfig, {
|
|
839
|
+
get retainsState() { return seam.originalRetainsState },
|
|
840
|
+
}) ? 'deferred' : 'owned'
|
|
734
841
|
)
|
|
735
842
|
const startActivation = record => {
|
|
736
843
|
if (record.cancelled || disposed || activating !== record) {
|
|
@@ -744,7 +851,7 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatil
|
|
|
744
851
|
try {
|
|
745
852
|
candidate = record.shape === 'deferred'
|
|
746
853
|
? createDeferredRuntime()
|
|
747
|
-
: installPtCRuntime(ctx, record.config, seam, toolSchemasForAgent, toolSdkSchemasForAgent, sessionId, sessionLog, () => settingsWriter)
|
|
854
|
+
: installPtCRuntime(ctx, record.config, seam, toolSchemasForAgent, toolSdkSchemasForAgent, sessionId, sessionLog, () => settingsWriter, currentPromptText)
|
|
748
855
|
record.runtime = candidate
|
|
749
856
|
} catch (error) {
|
|
750
857
|
const failedRuntime = error?.[INSTALL_CLEANUP]
|
|
@@ -899,8 +1006,10 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatil
|
|
|
899
1006
|
const handbackOwners = Object.fromEntries(['sessions', 'sessionPersistence', 'sessionQuery'].map(name => [name, ctx.get(name)]))
|
|
900
1007
|
// One spelling of "the current document": the same resolved configuration the
|
|
901
1008
|
// reconciliation loop applies, so a delivered message and the assembled prompt can
|
|
902
|
-
// never read two different prompt-text documents.
|
|
903
|
-
//
|
|
1009
|
+
// never read two different prompt-text documents. One holder per attachment, handed
|
|
1010
|
+
// to the runtime it installs, so two attachments in one process keep their own
|
|
1011
|
+
// document. Read lazily: the settings section replaces `configSource` after
|
|
1012
|
+
// activation.
|
|
904
1013
|
const currentPromptText = () => resolvePromptTextConfig(configSource())
|
|
905
1014
|
const messageOwner = createRuntimeMessageOwner(context => (
|
|
906
1015
|
committed?.retiring ? [] : committed?.runtime.contextsForRequest(context) ?? []
|
|
@@ -918,8 +1027,17 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatil
|
|
|
918
1027
|
ctx.effect(() => ctx.on('agent/disposed', ({ agent }) => messageOwner.disposeAgent(agent)), 'ptc-plus message agent disposal')
|
|
919
1028
|
ctx.effect(() => () => messageOwner.dispose(), 'ptc-plus dynamic message lifecycle')
|
|
920
1029
|
ctx.effect(() => async () => {
|
|
921
|
-
|
|
1030
|
+
// The model-visible handback states a fact about execution, so it must not
|
|
1031
|
+
// ride on whether every projection unregistered: a failed disposal is
|
|
1032
|
+
// reported by the caller that retries it, and the runtime is gone either way.
|
|
1033
|
+
let disposalFailure
|
|
1034
|
+
try {
|
|
1035
|
+
await controller.dispose()
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
disposalFailure = error
|
|
1038
|
+
}
|
|
922
1039
|
await handBackRuntimeSessions(ctx, handbackOwners)
|
|
1040
|
+
if (disposalFailure !== undefined) throw disposalFailure
|
|
923
1041
|
}, 'ptc-plus provider handback')
|
|
924
1042
|
|
|
925
1043
|
// The settings section replaces this source after activation; the prompt-text
|
|
@@ -937,11 +1055,28 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatil
|
|
|
937
1055
|
try {
|
|
938
1056
|
if (settingsWriter?.update === undefined) return
|
|
939
1057
|
const previousConfig = controller.committedConfig()
|
|
1058
|
+
// A policy this process synthesized is not a record the settings document holds: write it
|
|
1059
|
+
// back and the next resolution reports it as saved, so the rollback removes the field with
|
|
1060
|
+
// the service's own `unset` op when it offers one. The two switches derived beside it are
|
|
1061
|
+
// restated to the committed values; a service without `unset` keeps the previous, narrower
|
|
1062
|
+
// behaviour and the residual is stated in capability-surface §8.7.
|
|
1063
|
+
const keepDerived = previousConfig?.capabilityPolicySaved === true
|
|
1064
|
+
// Only a committed configuration that reports it saved no policy lets the rollback remove
|
|
1065
|
+
// one: with no committed record this process does not know what the document holds, and
|
|
1066
|
+
// deleting a policy it never wrote would erase rules the user saved.
|
|
1067
|
+
const removable = previousConfig !== undefined && previousConfig.capabilityPolicySaved === false
|
|
1068
|
+
&& typeof settingsWriter.mutate === 'function'
|
|
940
1069
|
const patch = previousConfig === undefined
|
|
941
1070
|
? { enabled: false }
|
|
942
|
-
: Object.fromEntries(CONFIG_FIELDS
|
|
1071
|
+
: Object.fromEntries(CONFIG_FIELDS
|
|
1072
|
+
// Only the policy is synthesized with the two switches; a writer without `mutate` cannot
|
|
1073
|
+
// remove it, so it omits that one field and still restates everything else it derived.
|
|
1074
|
+
.filter(field => keepDerived || field.key !== 'capabilityPolicy')
|
|
1075
|
+
.map(field => [field.key, previousConfig[field.key]]))
|
|
943
1076
|
try {
|
|
944
|
-
await settingsWriter.update(patch)
|
|
1077
|
+
if (removable && Object.keys(patch).length > 0) await settingsWriter.update(patch)
|
|
1078
|
+
if (removable) await settingsWriter.mutate([{ op: 'unset', path: ['capabilityPolicy'] }])
|
|
1079
|
+
else await settingsWriter.update(patch)
|
|
945
1080
|
} catch (rollbackError) {
|
|
946
1081
|
reportActivationFailure(new Error(
|
|
947
1082
|
`ptc-plus: failed to roll back runtime configuration: ${rollbackError.message}`,
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
2
|
+
import { isPtcMessageSource, PTC_MESSAGE_SOURCE_KIND, RETIRED_SURFACE_RECORD } from './message-sources.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Session binding adoption: which Global User Binding catalog entries this
|
|
6
|
+
* session uses.
|
|
7
|
+
*
|
|
8
|
+
* The question is answered per session and per entry, and the answer is a
|
|
9
|
+
* decision of exactly two states: this session uses the entry, or it does not.
|
|
10
|
+
* An entry the session never named is used, because the global catalog already
|
|
11
|
+
* answered the supply question. That default is the absence of a decision, not
|
|
12
|
+
* a third state: "use" and "never mentioned" lead to the same environment, so
|
|
13
|
+
* they are one state rather than two.
|
|
14
|
+
*
|
|
15
|
+
* Adoption can only take an entry away from this session. It cannot supply one,
|
|
16
|
+
* enable one, or name a source: an entry the global configuration disabled is
|
|
17
|
+
* absent from the catalog, so "adoption revives a deleted entry" has no
|
|
18
|
+
* encoding here and needs no rule forbidding it. Naming an entry the catalog
|
|
19
|
+
* does not offer is rejected at the surface that states the decision, because
|
|
20
|
+
* the record would otherwise claim authority over supply.
|
|
21
|
+
*
|
|
22
|
+
* The neighbour this must not be confused with is the capability pane's
|
|
23
|
+
* `automatic`, which means "the host derives this value". A binding has no host
|
|
24
|
+
* derivation to defer to, so borrowing that word would misdescribe both.
|
|
25
|
+
*
|
|
26
|
+
* The decision is recorded as a plugin record in the session log, so it is
|
|
27
|
+
* reconstructed by replaying that log after a restart instead of being kept in
|
|
28
|
+
* process memory. The message form is the historical carrier that 0.5.4 and
|
|
29
|
+
* earlier wrote; it is still read (those sessions keep their decision, and a
|
|
30
|
+
* repaired statement answers through it), and no writer produces it.
|
|
31
|
+
*/
|
|
32
|
+
export const BINDING_ADOPTION_FORM = 'binding-adoption'
|
|
33
|
+
export const BINDING_ADOPTION_SCHEMA = 'ptc-binding-adoption/v1'
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The log record this session's adoption is written as.
|
|
37
|
+
*
|
|
38
|
+
* Adoption is a decision about this session's own environment: the catalog the
|
|
39
|
+
* model reads already reflects it, so the decision needs no message of its own.
|
|
40
|
+
* Writing it as a record makes it durable and immediate — it is taken the moment
|
|
41
|
+
* the control is used, before any step, without entering the model-visible surface
|
|
42
|
+
* (a surface node ahead of the host's protected head would make the log
|
|
43
|
+
* unreadable to every reader). A host that publishes no plugin-record writer
|
|
44
|
+
* refuses the write instead of falling back to that surface node.
|
|
45
|
+
*/
|
|
46
|
+
export const BINDING_ADOPTION_RECORD = 'plugin:ptc-plus/binding-adoption'
|
|
47
|
+
|
|
48
|
+
const MAX_DECLINED = 64
|
|
49
|
+
const ENTRY_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/
|
|
50
|
+
const PREFIX = 'PTC Plus session binding adoption. This session uses every entry the global catalog supplies except the declined ones. Adoption can only withdraw an entry from this session: it cannot supply, enable or revive one, and the global configuration decides which entries exist at all. Declined entry IDs:'
|
|
51
|
+
|
|
52
|
+
function isRecord(value) {
|
|
53
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function sortIds(ids) {
|
|
57
|
+
return [...ids].sort((left, right) => left < right ? -1 : left > right ? 1 : 0)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function normalizeBindingAdoption(value) {
|
|
61
|
+
if (!isRecord(value) || Reflect.ownKeys(value).length !== 2
|
|
62
|
+
|| !Object.hasOwn(value, 'schema') || !Object.hasOwn(value, 'declined')) {
|
|
63
|
+
throw new TypeError('invalid binding adoption')
|
|
64
|
+
}
|
|
65
|
+
if (value.schema !== BINDING_ADOPTION_SCHEMA) throw new TypeError('invalid binding adoption schema')
|
|
66
|
+
if (!Array.isArray(value.declined) || value.declined.length > MAX_DECLINED) {
|
|
67
|
+
throw new TypeError('invalid binding adoption declined entries')
|
|
68
|
+
}
|
|
69
|
+
const declined = []
|
|
70
|
+
for (const id of value.declined) {
|
|
71
|
+
if (typeof id !== 'string' || !ENTRY_ID.test(id)) throw new TypeError('invalid binding adoption entry id')
|
|
72
|
+
if (declined.includes(id)) throw new TypeError('duplicate binding adoption entry id')
|
|
73
|
+
declined.push(id)
|
|
74
|
+
}
|
|
75
|
+
return Object.freeze({ schema: BINDING_ADOPTION_SCHEMA, declined: Object.freeze(sortIds(declined)) })
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The exact model-visible wording one adoption record carries. */
|
|
79
|
+
export function bindingAdoptionText(adoption) {
|
|
80
|
+
return `${PREFIX}\n${JSON.stringify(normalizeBindingAdoption(adoption).declined)}`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The historical message this adoption was written as before the record carrier. */
|
|
84
|
+
export function bindingAdoptionMessage(adoption) {
|
|
85
|
+
const text = bindingAdoptionText(adoption)
|
|
86
|
+
const message = createUserMessage({
|
|
87
|
+
source: { kind: PTC_MESSAGE_SOURCE_KIND, form: BINDING_ADOPTION_FORM },
|
|
88
|
+
content: [{ type: 'text', text }],
|
|
89
|
+
})
|
|
90
|
+
if (readBindingAdoption(message) === undefined) throw new TypeError('invalid binding adoption message')
|
|
91
|
+
return message
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Read one adoption record back from a message, message projection or message fact. */
|
|
95
|
+
export function readBindingAdoption(value) {
|
|
96
|
+
const source = value?.source
|
|
97
|
+
if (!isPtcMessageSource(source) || source.form !== BINDING_ADOPTION_FORM) return undefined
|
|
98
|
+
if (!Array.isArray(value.content) || value.content.length !== 1 || value.content[0]?.type !== 'text') return undefined
|
|
99
|
+
const text = value.content[0].text
|
|
100
|
+
if (typeof text !== 'string' || !text.startsWith(`${PREFIX}\n`)) return undefined
|
|
101
|
+
try {
|
|
102
|
+
const declined = JSON.parse(text.slice(PREFIX.length + 1))
|
|
103
|
+
const adoption = normalizeBindingAdoption({ schema: BINDING_ADOPTION_SCHEMA, declined })
|
|
104
|
+
return bindingAdoptionText(adoption) === text ? adoption : undefined
|
|
105
|
+
} catch {
|
|
106
|
+
return undefined
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* How this session's newest adoption statement reads.
|
|
112
|
+
*
|
|
113
|
+
* `none` is the absence of a decision, `decided` is a statement this build can
|
|
114
|
+
* read, and `unreadable` is a statement it cannot. The third state is not the
|
|
115
|
+
* first: an unreadable newest statement still supersedes every earlier record,
|
|
116
|
+
* so no consumer may answer from the older one in its place. `latestBindingAdoption`
|
|
117
|
+
* collapses the last two into `undefined`, which is why the panel asks this
|
|
118
|
+
* function instead: `undefined` there means "no decision to apply", not "the
|
|
119
|
+
* session declined nothing".
|
|
120
|
+
*/
|
|
121
|
+
export function classifyBindingAdoption(records) {
|
|
122
|
+
if (!Array.isArray(records)) return Object.freeze({ state: 'none' })
|
|
123
|
+
for (let index = records.length - 1; index >= 0; index -= 1) {
|
|
124
|
+
const record = records[index]
|
|
125
|
+
if (record?.form !== BINDING_ADOPTION_FORM) continue
|
|
126
|
+
try {
|
|
127
|
+
return Object.freeze({ state: 'decided',
|
|
128
|
+
adoption: normalizeBindingAdoption({ schema: BINDING_ADOPTION_SCHEMA, declined: record.declined }) })
|
|
129
|
+
} catch {
|
|
130
|
+
// A record that cannot be read is not evidence for an earlier record
|
|
131
|
+
// either: the newest statement is the current one, and it is unreadable.
|
|
132
|
+
return Object.freeze({ state: 'unreadable' })
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return Object.freeze({ state: 'none' })
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Read one committed plugin record back as this session's adoption, or nothing. */
|
|
139
|
+
export function readBindingAdoptionRecord(event) {
|
|
140
|
+
if (event?.type !== BINDING_ADOPTION_RECORD || event.ignorable !== true) return undefined
|
|
141
|
+
try {
|
|
142
|
+
return normalizeBindingAdoption(event.data)
|
|
143
|
+
} catch {
|
|
144
|
+
return undefined
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Whether one event states this session's adoption at all.
|
|
150
|
+
*
|
|
151
|
+
* The record type states it, and so does the historical message form. A repaired statement
|
|
152
|
+
* (`RETIRED_SURFACE_RECORD`) states it only when the payload it kept is this owner's own form:
|
|
153
|
+
* the repair retires every surface node ahead of the protected head, whichever owner wrote it,
|
|
154
|
+
* so the carrier alone says nothing about who the statement belongs to.
|
|
155
|
+
*/
|
|
156
|
+
function statesBindingAdoption(event) {
|
|
157
|
+
if (event?.type === BINDING_ADOPTION_RECORD) return true
|
|
158
|
+
const source = event?.data?.source
|
|
159
|
+
return isPtcMessageSource(source) && source.form === BINDING_ADOPTION_FORM
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Read one adoption out of either carrier, so a reader never has to know which release wrote it —
|
|
164
|
+
* and a repaired statement answers for the owner whose form it carries.
|
|
165
|
+
*/
|
|
166
|
+
export function readBindingAdoptionStatement(event) {
|
|
167
|
+
if (event?.type === BINDING_ADOPTION_RECORD) return readBindingAdoptionRecord(event)
|
|
168
|
+
if (event?.type === RETIRED_SURFACE_RECORD) {
|
|
169
|
+
return event.ignorable === true && statesBindingAdoption(event) ? readBindingAdoption(event.data) : undefined
|
|
170
|
+
}
|
|
171
|
+
return event?.type === 'user/message' ? readBindingAdoption(event.data) : undefined
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* This session's newest adoption statement, read from its own log events.
|
|
176
|
+
*
|
|
177
|
+
* The log is the owner of the decision, so every adoption statement it carries — a record, the
|
|
178
|
+
* message form an older release wrote, or a repaired statement of the same form — is classified,
|
|
179
|
+
* including one this build cannot read. Extracting the statements and classifying them here keeps
|
|
180
|
+
* every reader on the same cut: a reader that filtered to the readable statements first would hand
|
|
181
|
+
* a superseded record the answer, and the panel and the `/binding` command would then disagree
|
|
182
|
+
* about which record governs this session.
|
|
183
|
+
*/
|
|
184
|
+
export function bindingAdoptionOf(events) {
|
|
185
|
+
const records = []
|
|
186
|
+
for (const event of Array.isArray(events) ? events : []) {
|
|
187
|
+
if (event?.type !== 'user/message' && event?.type !== BINDING_ADOPTION_RECORD
|
|
188
|
+
&& event?.type !== RETIRED_SURFACE_RECORD) continue
|
|
189
|
+
// A repair retires every owner's surface node, so only a statement that kept this owner's
|
|
190
|
+
// form joins the ordering; another owner's retired node is not an adoption at all.
|
|
191
|
+
if (!statesBindingAdoption(event)) continue
|
|
192
|
+
records.push({ form: BINDING_ADOPTION_FORM, declined: readBindingAdoptionStatement(event)?.declined })
|
|
193
|
+
}
|
|
194
|
+
return classifyBindingAdoption(records)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The newest adoption this session recorded, or `undefined` while it recorded
|
|
199
|
+
* none this build can read. The newest record replaces earlier ones, because
|
|
200
|
+
* stating a decision again is how a session changes its mind.
|
|
201
|
+
*/
|
|
202
|
+
export function latestBindingAdoption(records) {
|
|
203
|
+
const statement = classifyBindingAdoption(records)
|
|
204
|
+
return statement.state === 'decided' ? statement.adoption : undefined
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The declined entry IDs of one adoption, as a set the environment uses.
|
|
209
|
+
* An adoption that was never stated declines nothing.
|
|
210
|
+
*/
|
|
211
|
+
export function declinedBindingEntries(adoption) {
|
|
212
|
+
return new Set(adoption === undefined ? [] : normalizeBindingAdoption(adoption).declined)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** State one entry as used (`adopt` true) or not used, leaving the rest alone. */
|
|
216
|
+
export function withBindingAdoption(adoption, entryId, adopt) {
|
|
217
|
+
if (typeof entryId !== 'string' || !ENTRY_ID.test(entryId)) throw new TypeError('invalid binding adoption entry id')
|
|
218
|
+
if (typeof adopt !== 'boolean') throw new TypeError('binding adoption decision must be a boolean')
|
|
219
|
+
const declined = declinedBindingEntries(adoption)
|
|
220
|
+
if (adopt) declined.delete(entryId)
|
|
221
|
+
else declined.add(entryId)
|
|
222
|
+
return normalizeBindingAdoption({ schema: BINDING_ADOPTION_SCHEMA, declined: [...declined] })
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Drop every declined entry: this session again uses the whole catalog. */
|
|
226
|
+
export function clearedBindingAdoption() {
|
|
227
|
+
return normalizeBindingAdoption({ schema: BINDING_ADOPTION_SCHEMA, declined: [] })
|
|
228
|
+
}
|