dsh-ptc-plus 0.5.0 → 0.5.2
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 +15 -3
- package/README.md +15 -3
- package/client.cjs +1983 -234
- 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 +2 -2
- package/docs/adr/0023-global-user-bindings.md +1 -1
- package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +11 -1
- package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +31 -1
- package/docs/adr/0038-freeze-session-capability-plans.md +1 -1
- package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +35 -0
- 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/README.md +2 -0
- package/docs/architecture.md +2 -2
- package/docs/capability-planning-components.md +2 -2
- package/docs/client-ui.md +13 -10
- package/docs/client-visual-contract.md +1 -1
- package/docs/durability-design.md +7 -1
- package/docs/installation.md +5 -3
- package/docs/publishing.md +2 -2
- package/docs/releases/v0.5.2.md +79 -0
- package/docs/runtime-reference.md +42 -3
- package/docs/unmeasured-cell-recovery-repair-proposal.md +387 -0
- package/docs/verification.md +6 -2
- package/index.js +63 -19
- package/internal/bounded-literal.js +85 -0
- package/internal/capability-command-receipt.js +3 -1
- package/internal/capability-environment.js +171 -13
- package/internal/capability-plan.js +11 -8
- package/internal/capability-statistics.js +6 -2
- package/internal/cell-error.js +13 -0
- package/internal/config-spec.js +122 -2
- package/internal/direct-surface-owner.js +14 -10
- package/internal/edit-transport-owner.js +96 -17
- package/internal/execution-provider-owner.js +25 -0
- package/internal/execution-seam-compat.js +18 -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 +51 -0
- 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/runtime-bridge-owner.js +11 -8
- package/internal/runtime-config.js +2 -0
- package/internal/runtime-contexts.js +14 -5
- package/internal/runtime-messages.js +43 -15
- package/internal/sdk-projection.js +2 -2
- package/internal/session-cell-executor.js +132 -11
- package/internal/session-journal-recovery.js +1009 -99
- package/internal/session-journal-schema.js +7 -5
- package/internal/session-journal.js +177 -14
- package/internal/session-log-view.js +111 -24
- package/internal/session-runtime.js +674 -33
- 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 +8 -27
- 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 +5 -2
- package/src/client-activity.js +27 -6
- package/src/client-capabilities-view.js +774 -125
- package/src/client-copy.js +231 -18
- package/src/client-repl-view.js +6 -1
- package/src/client-styles.js +68 -2
- package/src/client-tool-view.js +9 -2
package/index.js
CHANGED
|
@@ -20,12 +20,14 @@ import { createCordisToolsOwner } from './internal/cordis-tools-owner.js'
|
|
|
20
20
|
import { createEditTransportOwner, EDIT_RUN_CODE } from './internal/edit-transport-owner.js'
|
|
21
21
|
import { createRuntimeBridgeOwner, RUN_CODE } from './internal/runtime-bridge-owner.js'
|
|
22
22
|
import { resolveConfig, watchVolatileConfig } from './internal/runtime-config.js'
|
|
23
|
+
import { configureRetention } from './internal/projection-retention.js'
|
|
23
24
|
import {
|
|
24
25
|
CONFIG_FIELDS,
|
|
25
26
|
SETTINGS_NAMESPACE,
|
|
26
27
|
} from './internal/config-spec.js'
|
|
27
28
|
import { configFieldSchema, installSettingsSectionCompat } from './internal/settings-compat.js'
|
|
28
29
|
import { installExecutionSeam } from './internal/execution-seam-compat.js'
|
|
30
|
+
import { createDeferredRuntime, defersToOfficialRepl } from './internal/official-repl-priority.js'
|
|
29
31
|
import { valueLimitsFromConfig } from './internal/value-wire-schema.js'
|
|
30
32
|
import { createReplMemoryProjection } from './internal/repl-memory-projection.js'
|
|
31
33
|
import { createUserBindingDraftProjection } from './internal/user-binding-draft-projection.js'
|
|
@@ -34,6 +36,7 @@ import { createHostRpc } from './internal/host-rpc.js'
|
|
|
34
36
|
import { RPC_CONTRACTS } from './internal/rpc-contract.js'
|
|
35
37
|
import { createReplObservationInterest } from './internal/repl-observation-interest.js'
|
|
36
38
|
import { guidancePolicies } from './internal/binding-update-policy.js'
|
|
39
|
+
import { PROMPT_SLOT_IDS, resolvePromptTextConfig } from './internal/prompt-text.js'
|
|
37
40
|
import * as dshSettings from '@deepseek-ai/dsh-settings'
|
|
38
41
|
|
|
39
42
|
const INSTALL_CLEANUP = Symbol('ptc-plus install cleanup')
|
|
@@ -58,7 +61,7 @@ export const Config = Schema.object(Object.fromEntries(
|
|
|
58
61
|
*/
|
|
59
62
|
export const inject = ['tools', 'systemPrompt', 'agents']
|
|
60
63
|
|
|
61
|
-
function replGuidance({ bindingPolicy, rewritesEnabled, languageSemantics, durableReplay, cordisToolsEnabled }) {
|
|
64
|
+
function replGuidance({ bindingPolicy, rewritesEnabled, languageSemantics, durableReplay, cordisToolsEnabled }, promptText) {
|
|
62
65
|
const looseTopLevelRedeclarations = bindingPolicy.variableRedeclarations
|
|
63
66
|
const looseTopLevelFunctionClassRedeclarations = bindingPolicy.functionClassRedeclarations
|
|
64
67
|
const autoRewriteImports = rewritesEnabled.autoRewriteImports
|
|
@@ -91,18 +94,22 @@ function replGuidance({ bindingPolicy, rewritesEnabled, languageSemantics, durab
|
|
|
91
94
|
const cordisRecovery = cordisToolsEnabled
|
|
92
95
|
? 'Keep large Cordis plugin source in a binding before the tool call so it can be reused after an error. A Cordis parse or validation failure can follow external effects; repeat the call only when its retry rules and current results establish that it is safe.'
|
|
93
96
|
: ''
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
97
|
+
// Each paragraph is an editable asset; a derived policy sentence is appended to
|
|
98
|
+
// the paragraph that owns it, so rewriting prose can never contradict the
|
|
99
|
+
// configuration that produced it. Omitting a paragraph keeps its derived clause.
|
|
100
|
+
const prose = id => (promptText.omitted(id) ? '' : promptText.text(id))
|
|
101
|
+
const withDerived = (id, derived) => {
|
|
102
|
+
const base = prose(id)
|
|
103
|
+
return base === '' ? derived : `${base} ${derived}`
|
|
104
|
+
}
|
|
105
|
+
const paragraphs = [
|
|
106
|
+
prose(PROMPT_SLOT_IDS.guidanceOpening),
|
|
107
|
+
prose(PROMPT_SLOT_IDS.guidanceErrorRecovery),
|
|
108
|
+
withDerived(PROMPT_SLOT_IDS.guidanceCellConventions, cellConventions),
|
|
109
|
+
prose(PROMPT_SLOT_IDS.guidanceCapabilities),
|
|
110
|
+
withDerived(PROMPT_SLOT_IDS.guidanceExecutionWorld, `${recovery}${cordisToolsEnabled ? ` ${cordisRecovery}` : ''}`),
|
|
111
|
+
]
|
|
112
|
+
return paragraphs.filter(paragraph => paragraph.trim().length > 0).join('\n\n')
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
/** Register the session-bound REPL runtime. */
|
|
@@ -344,6 +351,7 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolS
|
|
|
344
351
|
maxWallMs: activeConfig.maxWallMs,
|
|
345
352
|
maxOutputBytes: activeConfig.maxOutputBytes,
|
|
346
353
|
maxOldGenerationSizeMb: activeConfig.maxOldGenerationSizeMb,
|
|
354
|
+
workerReclaimMs: activeConfig.workerReclaimMs,
|
|
347
355
|
valueLimits: valueLimitsFromConfig(activeConfig),
|
|
348
356
|
})
|
|
349
357
|
capabilityEnvironment = createCapabilityEnvironment({ ctx, config: activeConfig,
|
|
@@ -374,6 +382,7 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolS
|
|
|
374
382
|
presentationGeneration,
|
|
375
383
|
sessionId,
|
|
376
384
|
toolSchemasForAgent,
|
|
385
|
+
promptText: resolvePromptTextConfig(activeConfig),
|
|
377
386
|
})
|
|
378
387
|
directSurface = createDirectSurfaceOwner({
|
|
379
388
|
sessionLogView: sessionLog.project,
|
|
@@ -393,7 +402,7 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolS
|
|
|
393
402
|
order: 98,
|
|
394
403
|
text: context => {
|
|
395
404
|
if (ctx.tools.get(RUN_CODE, context?.scope) === undefined) return ''
|
|
396
|
-
return replGuidance(guidancePolicies(activeConfig))
|
|
405
|
+
return replGuidance(guidancePolicies(activeConfig), resolvePromptTextConfig(activeConfig))
|
|
397
406
|
},
|
|
398
407
|
}))
|
|
399
408
|
disposers.push(ctx.on('system-prompt/assemble', async (assembly, context, next) => {
|
|
@@ -415,8 +424,6 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolS
|
|
|
415
424
|
editTransport.ensureInstalled(agent)
|
|
416
425
|
}
|
|
417
426
|
}
|
|
418
|
-
disposers.push(ctx.on('agent/pre-step', (payload, next) => userBindings.preStep(payload, next)))
|
|
419
|
-
disposers.push(ctx.on('session/event', (session, event) => userBindings.committedEvent(session, event)))
|
|
420
427
|
disposers.push(ctx.on('tools/post-execute', (exec, result, next) => capabilityEnvironment.postExecute(exec, result, next), { prepend: true }))
|
|
421
428
|
disposers.push(ctx.on('tools/ptc-dispatch-log', (dispatch, next) => capabilityEnvironment.dispatchLog(dispatch, next), { prepend: true }))
|
|
422
429
|
disposers.push(ctx.on('agent/status', payload => { if (payload?.agent) capabilityEnvironment.status(payload.agent, payload.status) }))
|
|
@@ -471,7 +478,14 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolS
|
|
|
471
478
|
return { kind: 'error', text: receipt('unavailable') }
|
|
472
479
|
}
|
|
473
480
|
await capabilityEnvironment.refresh(agent, undefined, signal, commandId)
|
|
474
|
-
|
|
481
|
+
// A refresh always republishes the saved configuration; when that
|
|
482
|
+
// configuration has capability management or context off, report
|
|
483
|
+
// the off state instead of claiming an updated environment.
|
|
484
|
+
const described = capabilityEnvironment.describe(agent)
|
|
485
|
+
const state = described.effective?.enabled !== true
|
|
486
|
+
? described.sourceSettings?.contextEnabled === false ? 'paused' : 'off'
|
|
487
|
+
: described.publication === 'pending' ? 'pending' : 'recorded'
|
|
488
|
+
return { kind: 'success', text: receipt(state) }
|
|
475
489
|
}
|
|
476
490
|
return { kind: 'success', text: JSON.stringify(capabilityEnvironment.describe(agent), null, 2) }
|
|
477
491
|
} }), 'ptc-plus capability command')
|
|
@@ -618,8 +632,11 @@ export const apply = (ctx, config = {}) => {
|
|
|
618
632
|
// Configuration is validated before the plugin waits for the host's seam, so
|
|
619
633
|
// an unusable configuration fails at the load call rather than later.
|
|
620
634
|
const resolvedConfig = resolveConfig(config)
|
|
635
|
+
// The retention window is user configuration; install it before any fold can run.
|
|
636
|
+
configureRetention(resolvedConfig)
|
|
621
637
|
return installExecutionSeam(ctx, {
|
|
622
638
|
services: inject,
|
|
639
|
+
attachmentTimeoutMs: resolvedConfig.executionSeamTimeoutMs,
|
|
623
640
|
attach: (scope, seam) => applyWithExecutionSeam(scope, resolvedConfig, seam, ctx, config),
|
|
624
641
|
reportFailure: (error, seamName) => ctx.logger?.warn?.(
|
|
625
642
|
`ptc-plus: execution seam ${seamName} failed to attach`,
|
|
@@ -710,6 +727,11 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatil
|
|
|
710
727
|
)
|
|
711
728
|
return cleanup
|
|
712
729
|
}
|
|
730
|
+
// Execution ownership is a per-attachment property: `owned` installs the
|
|
731
|
+
// session runtime, `deferred` leaves run_code to the official provider.
|
|
732
|
+
const executionShape = nextConfig => (
|
|
733
|
+
defersToOfficialRepl(nextConfig, seam.provider) ? 'deferred' : 'owned'
|
|
734
|
+
)
|
|
713
735
|
const startActivation = record => {
|
|
714
736
|
if (record.cancelled || disposed || activating !== record) {
|
|
715
737
|
record.completion.resolve({ status: 'superseded' })
|
|
@@ -720,7 +742,9 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatil
|
|
|
720
742
|
}
|
|
721
743
|
let candidate
|
|
722
744
|
try {
|
|
723
|
-
candidate =
|
|
745
|
+
candidate = record.shape === 'deferred'
|
|
746
|
+
? createDeferredRuntime()
|
|
747
|
+
: installPtCRuntime(ctx, record.config, seam, toolSchemasForAgent, toolSdkSchemasForAgent, sessionId, sessionLog, () => settingsWriter)
|
|
724
748
|
record.runtime = candidate
|
|
725
749
|
} catch (error) {
|
|
726
750
|
const failedRuntime = error?.[INSTALL_CLEANUP]
|
|
@@ -765,6 +789,7 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatil
|
|
|
765
789
|
const record = {
|
|
766
790
|
config: nextConfig,
|
|
767
791
|
generation,
|
|
792
|
+
shape: executionShape(nextConfig),
|
|
768
793
|
runtime: undefined,
|
|
769
794
|
disposal: undefined,
|
|
770
795
|
disposed: false,
|
|
@@ -814,6 +839,15 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatil
|
|
|
814
839
|
return beginActivation(nextConfig, generation, cleanup)
|
|
815
840
|
}
|
|
816
841
|
const current = committed
|
|
842
|
+
if (current.shape !== executionShape(nextConfig)) {
|
|
843
|
+
// Ownership changes are installation changes: dispose the live kernels
|
|
844
|
+
// and install the other shape, so no committed cell can mix the two
|
|
845
|
+
// execution contracts or extend state the new owner cannot see.
|
|
846
|
+
current.retiring = true
|
|
847
|
+
void disposeRuntime(current)
|
|
848
|
+
const cleanup = disposeRetainedRuntimes()
|
|
849
|
+
return beginActivation(nextConfig, generation, cleanup)
|
|
850
|
+
}
|
|
817
851
|
if (!nextConfig.cordisToolsEnabled) current.runtime.cancelCordisActivation()
|
|
818
852
|
return enqueueTransition(async () => {
|
|
819
853
|
if (disposed || committed !== current) return { status: 'superseded' }
|
|
@@ -863,9 +897,15 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatil
|
|
|
863
897
|
ctx.effect(() => async () => controller.dispose(), 'ptc-plus runtime lifecycle')
|
|
864
898
|
|
|
865
899
|
const handbackOwners = Object.fromEntries(['sessions', 'sessionPersistence', 'sessionQuery'].map(name => [name, ctx.get(name)]))
|
|
900
|
+
// One spelling of "the current document": the same resolved configuration the
|
|
901
|
+
// reconciliation loop applies, so a delivered message and the assembled prompt can
|
|
902
|
+
// never read two different prompt-text documents. Read lazily: the settings section
|
|
903
|
+
// replaces `configSource` after activation.
|
|
904
|
+
const currentPromptText = () => resolvePromptTextConfig(configSource())
|
|
866
905
|
const messageOwner = createRuntimeMessageOwner(context => (
|
|
867
906
|
committed?.retiring ? [] : committed?.runtime.contextsForRequest(context) ?? []
|
|
868
|
-
), sessionLog.project, () => committed !== undefined && committed.retiring !== true, handbackOwners.sessionQuery
|
|
907
|
+
), sessionLog.project, () => committed !== undefined && committed.retiring !== true, handbackOwners.sessionQuery,
|
|
908
|
+
currentPromptText)
|
|
869
909
|
ctx.effect(() => ctx.systemPrompt.context({
|
|
870
910
|
name: PTC_DELIVERY_CONTEXT, order: 98, text: '',
|
|
871
911
|
}), 'ptc-plus dynamic message delivery witness')
|
|
@@ -882,6 +922,8 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatil
|
|
|
882
922
|
await handBackRuntimeSessions(ctx, handbackOwners)
|
|
883
923
|
}, 'ptc-plus provider handback')
|
|
884
924
|
|
|
925
|
+
// The settings section replaces this source after activation; the prompt-text
|
|
926
|
+
// accessor above reads it lazily for exactly that reason.
|
|
885
927
|
let configSource = () => resolveConfig(volatileConfig ?? resolvedConfig)
|
|
886
928
|
let configurationGeneration = 0
|
|
887
929
|
let configurationRollback = false
|
|
@@ -912,6 +954,7 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatil
|
|
|
912
954
|
}
|
|
913
955
|
const reconcile = (propagateFailure = false) => {
|
|
914
956
|
const current = resolveConfig(configSource())
|
|
957
|
+
configureRetention(current)
|
|
915
958
|
const generation = ++configurationGeneration
|
|
916
959
|
if (!current.enabled) {
|
|
917
960
|
const operation = controller.uninstall()
|
|
@@ -959,6 +1002,7 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatil
|
|
|
959
1002
|
}
|
|
960
1003
|
ctx.effect(() => seam.onSourceChange(() => {
|
|
961
1004
|
const current = resolveConfig(configSource())
|
|
1005
|
+
configureRetention(current)
|
|
962
1006
|
if (!current.enabled) return undefined
|
|
963
1007
|
if (seam.language !== 'typescript') {
|
|
964
1008
|
const language = seam.language
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-time evidence that a declaration's initializer is a bounded object
|
|
3
|
+
* literal.
|
|
4
|
+
*
|
|
5
|
+
* Recovery records a binding's value at settlement so it can be installed
|
|
6
|
+
* without re-executing the cell. Recording an arbitrary object is unsafe: the
|
|
7
|
+
* value codec enumerates an object's own keys before it can apply any budget,
|
|
8
|
+
* and enumerating a graph with millions of keys exhausts the worker heap before
|
|
9
|
+
* a cap can reject it (a four-million-key object aborts a 128 MiB worker).
|
|
10
|
+
*
|
|
11
|
+
* This module decides the question where it is cheap — on the syntax tree of the
|
|
12
|
+
* initializer — and returns the proven shape only for literals whose size is
|
|
13
|
+
* fixed by their source: an object literal with a bounded number of plain,
|
|
14
|
+
* non-computed properties whose values are primitives, array literals, or other
|
|
15
|
+
* such literals up to a fixed depth. Everything else (an identifier, a call, a
|
|
16
|
+
* class instance) yields no evidence, and its binding stays unrecorded and is
|
|
17
|
+
* named by recovery instead of being captured.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Maximum number of own properties a recordable literal may declare. */
|
|
21
|
+
export const RECORDED_LITERAL_MAX_PROPERTIES = 32
|
|
22
|
+
|
|
23
|
+
/** Maximum nesting depth of recordable literals, counting the outer object. */
|
|
24
|
+
export const RECORDED_LITERAL_MAX_DEPTH = 3
|
|
25
|
+
|
|
26
|
+
/** Maximum number of array elements a recordable array literal may declare. */
|
|
27
|
+
export const RECORDED_LITERAL_MAX_ELEMENTS = 64
|
|
28
|
+
|
|
29
|
+
const OBJECT_PROPERTY_TYPES = new Set(['ObjectProperty', 'Property'])
|
|
30
|
+
const LITERAL_KEY_TYPES = new Set(['StringLiteral', 'NumericLiteral', 'Literal', 'Identifier'])
|
|
31
|
+
const PRIMITIVE_LITERAL_TYPES = new Set(['StringLiteral', 'NumericLiteral', 'BooleanLiteral', 'NullLiteral',
|
|
32
|
+
'BigIntLiteral', 'RegExpLiteral'])
|
|
33
|
+
|
|
34
|
+
function isRecordableValue(node, depth) {
|
|
35
|
+
if (node === null || node === undefined) return false
|
|
36
|
+
// ESTree parsers emit `Literal`; Babel emits the typed variants above.
|
|
37
|
+
if (node.type === 'Literal') {
|
|
38
|
+
const value = node.value
|
|
39
|
+
return value === null || ['number', 'string', 'boolean', 'bigint'].includes(typeof value)
|
|
40
|
+
|| node.regex !== undefined
|
|
41
|
+
}
|
|
42
|
+
if (PRIMITIVE_LITERAL_TYPES.has(node.type)) return true
|
|
43
|
+
if (node.type === 'TemplateLiteral') return node.expressions.length === 0
|
|
44
|
+
if (node.type === 'UnaryExpression') {
|
|
45
|
+
return ['-', '+', '!', 'void'].includes(node.operator) && isRecordableValue(node.argument, depth)
|
|
46
|
+
}
|
|
47
|
+
if (node.type === 'ObjectExpression') return boundedLiteralEvidence(node, depth) !== undefined
|
|
48
|
+
if (node.type === 'ArrayExpression') {
|
|
49
|
+
return node.elements.length <= RECORDED_LITERAL_MAX_ELEMENTS
|
|
50
|
+
&& node.elements.every(element => isRecordableValue(element, depth))
|
|
51
|
+
}
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Evidence for one object literal, or `undefined` when the literal's size is
|
|
57
|
+
* not fixed by its source.
|
|
58
|
+
*
|
|
59
|
+
* @param {object} node an ESTree/Babel `ObjectExpression`
|
|
60
|
+
* @param {number} [depth] nest level of this literal, 1 for the top object
|
|
61
|
+
* @returns {{ properties: number, depth: number } | undefined}
|
|
62
|
+
*/
|
|
63
|
+
export function boundedLiteralEvidence(node, depth = 1) {
|
|
64
|
+
if (node?.type !== 'ObjectExpression') return undefined
|
|
65
|
+
if (depth > RECORDED_LITERAL_MAX_DEPTH) return undefined
|
|
66
|
+
if (node.properties.length > RECORDED_LITERAL_MAX_PROPERTIES) return undefined
|
|
67
|
+
let proven = 0
|
|
68
|
+
for (const property of node.properties) {
|
|
69
|
+
// `kind` is 'init' on most Babel versions and absent on some parses; an
|
|
70
|
+
// accessor or method arrives as `ObjectMethod`, which this branch rejects
|
|
71
|
+
// through the `type` check above.
|
|
72
|
+
if (!OBJECT_PROPERTY_TYPES.has(property.type) || property.computed === true
|
|
73
|
+
|| (property.kind !== undefined && property.kind !== 'init') || property.method === true) return undefined
|
|
74
|
+
const key = property.key
|
|
75
|
+
if (!LITERAL_KEY_TYPES.has(key.type)) return undefined
|
|
76
|
+
const valueNode = property.value
|
|
77
|
+
if (valueNode.type === 'ObjectExpression') {
|
|
78
|
+
if (boundedLiteralEvidence(valueNode, depth + 1) === undefined) return undefined
|
|
79
|
+
} else if (!isRecordableValue(valueNode, depth)) {
|
|
80
|
+
return undefined
|
|
81
|
+
}
|
|
82
|
+
proven += 1
|
|
83
|
+
}
|
|
84
|
+
return { properties: proven, depth }
|
|
85
|
+
}
|
|
@@ -6,8 +6,10 @@ const FALLBACKS = Object.freeze({
|
|
|
6
6
|
pending: 'Capabilities prepared; they take effect at the first accepted model step for session',
|
|
7
7
|
unavailable: 'Select an active PTC session; the command did not update session',
|
|
8
8
|
usage: 'Use /capabilities to inspect or /capabilities refresh to apply saved settings for session',
|
|
9
|
+
paused: 'Capabilities and context are off; the session stays with the Host capabilities for session',
|
|
10
|
+
off: 'Capability management is off; the session stays with the Host capabilities for session',
|
|
9
11
|
})
|
|
10
|
-
const STATES = new Set(['recorded', 'pending', 'unavailable', 'usage', 'saved-only'])
|
|
12
|
+
const STATES = new Set(['recorded', 'pending', 'unavailable', 'usage', 'saved-only', 'paused', 'off'])
|
|
11
13
|
|
|
12
14
|
export function capabilityCommandReceipt(state, sessionId, error, proposal) {
|
|
13
15
|
if (!STATES.has(state) || typeof sessionId !== 'string' || !sessionId) throw new TypeError('invalid capability command receipt')
|
|
@@ -14,6 +14,8 @@ import { PTC_MESSAGE_SOURCE_KIND } from './message-sources.js'
|
|
|
14
14
|
import { deepFreeze } from './record-utils.js'
|
|
15
15
|
import { capabilityProfileConfiguration } from './runtime-config.js'
|
|
16
16
|
import { readRuntimeAvailability } from './runtime-messages.js'
|
|
17
|
+
import { PLAN_CATEGORY_LINES, PLAN_NAVIGATION, PLAN_OVERAGE, PLAN_OVERAGE_HOLE,
|
|
18
|
+
PROMPT_SLOT_IDS, planCategorySlotId, resolvePromptText, resolvePromptTextConfig } from './prompt-text.js'
|
|
17
19
|
|
|
18
20
|
export const CAPABILITY_PLAN_FORM = 'capability-plan'
|
|
19
21
|
export const CAPABILITY_EVIDENCE_KEY = 'dshPtcPlusCapabilities'
|
|
@@ -25,6 +27,54 @@ const LEGACY_NAVIGATION = 'Use capabilities.search({query}) to retrieve complete
|
|
|
25
27
|
const NAVIGATION = 'Use capabilities.search({query}) to retrieve complete tool, Skill and MCP contracts. Unlisted capabilities remain discoverable. Skill descriptions are summaries; follow each Skill\'s declared invocation eligibility to load its body before following it.'
|
|
26
28
|
const digest = value => createHash('sha256').update(JSON.stringify(value)).digest('hex')
|
|
27
29
|
const messageText = message => message?.content?.filter(block => block.type === 'text').map(block => block.text).join('\n') ?? ''
|
|
30
|
+
const byteLength = value => Buffer.byteLength(value, 'utf8')
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Delivered plan-channel text, resolved from the prompt-text document.
|
|
34
|
+
*
|
|
35
|
+
* The planner must receive the same words the panel shows, so the navigation
|
|
36
|
+
* sentence, the five category lines and the overage template are read once here
|
|
37
|
+
* rather than defaulting inside the planner. `isDefault` decides whether the
|
|
38
|
+
* snapshot can keep a historical generation: only an edited channel needs the v5
|
|
39
|
+
* record, so an untouched configuration stays byte-identical to before.
|
|
40
|
+
*/
|
|
41
|
+
function planChannelText(config) {
|
|
42
|
+
const prompt = resolvePromptTextConfig(config)
|
|
43
|
+
const navigationSentence = prompt.text(PROMPT_SLOT_IDS.planNavigationSentence)
|
|
44
|
+
const overageSentence = prompt.text(PROMPT_SLOT_IDS.planOverageSentence)
|
|
45
|
+
const categoryLines = Object.keys(PLAN_CATEGORY_LINES)
|
|
46
|
+
.map(category => [category, prompt.text(planCategorySlotId(category))])
|
|
47
|
+
const isDefault = navigationSentence === PLAN_NAVIGATION
|
|
48
|
+
&& overageSentence === PLAN_OVERAGE
|
|
49
|
+
&& categoryLines.every(([category, line]) => line === PLAN_CATEGORY_LINES[category])
|
|
50
|
+
return { navigationSentence, overageSentence, categoryLines, isDefault }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Directory budget for the delivered guidance.
|
|
55
|
+
*
|
|
56
|
+
* Editing a navigation sentence changes how many bytes the directory spends before
|
|
57
|
+
* any capability entry, so the budget moves with the text: an unchanged document
|
|
58
|
+
* therefore keeps today's budget exactly, and a longer sentence does not silently
|
|
59
|
+
* shrink the catalog.
|
|
60
|
+
*/
|
|
61
|
+
const planDirectoryBudget = (configured, navigationSentence) => (
|
|
62
|
+
configured + byteLength(navigationSentence) - byteLength(PLAN_NAVIGATION))
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Recorded plan text; a v5 snapshot carries the words and the budget it was
|
|
66
|
+
* composed from.
|
|
67
|
+
*
|
|
68
|
+
* The budget belongs in the record because it depends on the navigation sentence:
|
|
69
|
+
* replaying it from the current default literal would let a later edit of that
|
|
70
|
+
* literal silently change what an old snapshot replays to.
|
|
71
|
+
*/
|
|
72
|
+
const planPromptText = (channel, directoryBudgetBytes) => ({ promptText: {
|
|
73
|
+
navigationSentence: channel.navigationSentence,
|
|
74
|
+
categoryLines: channel.categoryLines.map(([category, line]) => [category, line]),
|
|
75
|
+
overageSentence: channel.overageSentence,
|
|
76
|
+
directoryBudgetBytes,
|
|
77
|
+
} })
|
|
28
78
|
|
|
29
79
|
/** Saved inputs and explicit session choices are distinct; automatic cancels a saved override. */
|
|
30
80
|
export function sessionCapabilitySettings(sourceSettings, sessionOverrides = {}) {
|
|
@@ -85,9 +135,19 @@ function sourcePlan(message, id) {
|
|
|
85
135
|
const source = message?.source
|
|
86
136
|
if (source?.kind !== PTC_MESSAGE_SOURCE_KIND || source.form !== CAPABILITY_PLAN_FORM || source.sessionId !== id) return undefined
|
|
87
137
|
const snapshot = source.snapshot
|
|
88
|
-
const generation = ['ptc-capability-environment/v1', 'ptc-capability-environment/v2', 'ptc-capability-environment/v3',
|
|
138
|
+
const generation = ['ptc-capability-environment/v1', 'ptc-capability-environment/v2', 'ptc-capability-environment/v3',
|
|
139
|
+
'ptc-capability-environment/v4', 'ptc-capability-environment/v5'].indexOf(snapshot?.schema)
|
|
140
|
+
// Historical generations each had one planner generation for a given settings
|
|
141
|
+
// shape, so their plan schema is part of the record's identity. Generation 5
|
|
142
|
+
// records the plan words instead, and its plan schema follows from the recorded
|
|
143
|
+
// settings: the recomputation below proves the whole plan, which makes a separate
|
|
144
|
+
// schema guess both unnecessary and wrong (a v5 snapshot may carry context
|
|
145
|
+
// settings and therefore a v3 plan, or a closed plan and therefore a v2 one).
|
|
146
|
+
const expectedPlanSchema = generation === 2 ? 'ptc-capability-plan/v2'
|
|
147
|
+
: generation >= 3 && snapshot.settings.policy !== undefined ? 'ptc-capability-plan/v3' : 'ptc-capability-plan/v1'
|
|
89
148
|
if (source.digest !== digest(snapshot) || generation < 0
|
|
90
|
-
|| !Array.isArray(snapshot.capabilities)
|
|
149
|
+
|| !Array.isArray(snapshot.capabilities)
|
|
150
|
+
|| (generation < 4 && snapshot.plan?.schema !== expectedPlanSchema)) {
|
|
91
151
|
throw new Error('damaged session capability plan')
|
|
92
152
|
}
|
|
93
153
|
if (generation >= 1) {
|
|
@@ -100,30 +160,74 @@ function sourcePlan(message, id) {
|
|
|
100
160
|
}
|
|
101
161
|
if (JSON.stringify(merged) !== JSON.stringify(snapshot.settings)) throw new Error('inconsistent session capability choices')
|
|
102
162
|
}
|
|
103
|
-
|
|
163
|
+
// v4 implies a context generation; v5 only says the plan words were edited, so it
|
|
164
|
+
// may or may not carry context settings.
|
|
165
|
+
const contextGeneration = generation === 3 ? typeof snapshot.settings.contextEnabled === 'boolean'
|
|
166
|
+
: generation === 4 ? true : snapshot.settings.contextEnabled === undefined
|
|
167
|
+
if (!contextGeneration) {
|
|
104
168
|
throw new Error('inconsistent capability context generation')
|
|
105
169
|
}
|
|
106
|
-
if (generation === 2 || generation
|
|
170
|
+
if (generation === 2 || generation >= 3 && snapshot.settings.policy !== undefined) {
|
|
107
171
|
if (!snapshot.settings.policy || typeof snapshot.settings.recoveryEnabled !== 'boolean'
|
|
108
172
|
|| typeof snapshot.settings.previewEnabled !== 'boolean') throw new Error('damaged capability policy settings')
|
|
109
173
|
} else if (snapshot.settings.policy !== undefined) throw new Error('inconsistent historical capability generation')
|
|
110
|
-
|
|
174
|
+
// A v5 snapshot carries the plan words it was composed from; a historical
|
|
175
|
+
// generation may only claim the registry defaults it was delivered with.
|
|
176
|
+
const recorded = generation === 4 ? planRecord(snapshot) : undefined
|
|
177
|
+
if (recorded === undefined) {
|
|
178
|
+
if (snapshot.promptText !== undefined) throw new Error('inconsistent capability plan text generation')
|
|
179
|
+
if (snapshot.navigationText !== undefined && snapshot.navigationText !== NAVIGATION) throw new Error('unknown capability navigation contract')
|
|
180
|
+
} else if (snapshot.navigationText !== recorded.navigationSentence) {
|
|
181
|
+
throw new Error('damaged capability plan text')
|
|
182
|
+
}
|
|
111
183
|
const expected = createCapabilityPlan({ ...snapshot.settings, capabilities: snapshot.capabilities,
|
|
112
184
|
coverage: snapshot.coverage, navigationText: snapshot.navigationText ?? LEGACY_NAVIGATION,
|
|
185
|
+
...(recorded === undefined ? {} : {
|
|
186
|
+
categories: recorded.categoryLines,
|
|
187
|
+
directoryBudgetBytes: recorded.directoryBudgetBytes,
|
|
188
|
+
}),
|
|
113
189
|
configurationRevision: digest(snapshot.settings) })
|
|
114
|
-
if (
|
|
190
|
+
if ((generation >= 4 && snapshot.plan?.schema !== expected.schema)
|
|
191
|
+
|| JSON.stringify(expected) !== JSON.stringify(snapshot.plan)
|
|
115
192
|
|| messageText(message) !== messageText(planMessage(snapshot, id, source.refresh === true))) {
|
|
116
193
|
throw new Error('inconsistent session capability plan')
|
|
117
194
|
}
|
|
118
195
|
return deepFreeze(structuredClone(snapshot))
|
|
119
196
|
}
|
|
120
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Validated plan words of a v5 snapshot.
|
|
200
|
+
*
|
|
201
|
+
* Returns `undefined` only when the snapshot carries no record at all; a record that
|
|
202
|
+
* exists but cannot be replayed throws, because a half-recorded plan text would let
|
|
203
|
+
* a later replay silently deliver different words than the ones recorded here.
|
|
204
|
+
*/
|
|
205
|
+
function planRecord(snapshot) {
|
|
206
|
+
const record = snapshot.promptText
|
|
207
|
+
if (record === undefined || record === null) return undefined
|
|
208
|
+
const categories = record.categoryLines
|
|
209
|
+
const known = Object.keys(PLAN_CATEGORY_LINES)
|
|
210
|
+
if (typeof record.navigationSentence !== 'string' || record.navigationSentence.length === 0
|
|
211
|
+
|| typeof record.overageSentence !== 'string' || record.overageSentence.split(PLAN_OVERAGE_HOLE).length !== 2
|
|
212
|
+
|| !Number.isSafeInteger(record.directoryBudgetBytes) || record.directoryBudgetBytes < 0
|
|
213
|
+
|| !Array.isArray(categories) || categories.length !== known.length
|
|
214
|
+
|| categories.some(pair => !Array.isArray(pair) || pair.length !== 2
|
|
215
|
+
|| !known.includes(pair[0]) || typeof pair[1] !== 'string' || pair[1].length === 0)
|
|
216
|
+
|| new Set(categories.map(pair => pair[0])).size !== categories.length) {
|
|
217
|
+
throw new Error('damaged capability plan text')
|
|
218
|
+
}
|
|
219
|
+
return { navigationSentence: record.navigationSentence, overageSentence: record.overageSentence,
|
|
220
|
+
directoryBudgetBytes: record.directoryBudgetBytes,
|
|
221
|
+
categoryLines: categories.map(([category, line]) => [category, line]) }
|
|
222
|
+
}
|
|
223
|
+
|
|
121
224
|
function planMessage(snapshot, id, refresh = false) {
|
|
122
225
|
const plan = snapshot.plan
|
|
226
|
+
const overage = snapshot.promptText?.overageSentence ?? PLAN_OVERAGE
|
|
123
227
|
const text = plan.enabled
|
|
124
228
|
? ['PTC Plus session capability environment' + (refresh ? ' refreshed; replaces the preceding capability plan.' : ':'),
|
|
125
229
|
plan.directoryText, plan.catalogText,
|
|
126
|
-
...(plan.budgets.overageBytes ? [
|
|
230
|
+
...(plan.budgets.overageBytes ? [overage.replace(PLAN_OVERAGE_HOLE, String(plan.budgets.overageBytes))] : [])]
|
|
127
231
|
.filter(Boolean).join('\n\n')
|
|
128
232
|
: snapshot.settings.contextEnabled === false
|
|
129
233
|
? 'PTC Plus capability and context settings are paused for this session. Use the host-provided capability contracts and result presentation.'
|
|
@@ -132,6 +236,43 @@ function planMessage(snapshot, id, refresh = false) {
|
|
|
132
236
|
sessionId: id, refresh, digest: digest(snapshot), snapshot }, content: [{ type: 'text', text }] })
|
|
133
237
|
}
|
|
134
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Delivered plan text for a snapshot, without the message envelope.
|
|
241
|
+
*
|
|
242
|
+
* Replay and the surface gate both need the bytes the model reads; keeping the
|
|
243
|
+
* composition private to this module means a caller cannot assemble a different
|
|
244
|
+
* plan and call it the delivered one.
|
|
245
|
+
*/
|
|
246
|
+
export function capabilityPlanText(snapshot, id, refresh = false) {
|
|
247
|
+
return planMessage(snapshot, id, refresh).content[0].text
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Delivered plan message for a snapshot, envelope included.
|
|
252
|
+
*
|
|
253
|
+
* What this composition can claim is consistency, not provenance: the settings merge
|
|
254
|
+
* is a fixed point of the recorded settings, the recorded plan deep-equals a
|
|
255
|
+
* recomputation from them, the delivered text equals a recomposition of that plan and
|
|
256
|
+
* the snapshots it reads are deeply frozen. It recomputes the digest, so it cannot
|
|
257
|
+
* attest that these bytes are the ones a session log once carried.
|
|
258
|
+
*/
|
|
259
|
+
export function capabilityPlanMessage(snapshot, id, refresh = false) {
|
|
260
|
+
return planMessage(snapshot, id, refresh)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Delivered plan text of a plan message, after the replay contract accepted it.
|
|
265
|
+
*
|
|
266
|
+
* The reader above stays a recognizer and hands out no composition state; a caller
|
|
267
|
+
* that needs the bytes the model read asks this projection instead. An unreplayable
|
|
268
|
+
* message throws, so a lane that compares bytes cannot pass by receiving `undefined`.
|
|
269
|
+
*/
|
|
270
|
+
export function capabilityPlanTextFromMessage(message, sessionId) {
|
|
271
|
+
if (message?.source?.form !== CAPABILITY_PLAN_FORM) return undefined
|
|
272
|
+
sourcePlan(message, sessionId)
|
|
273
|
+
return message.content[0].text
|
|
274
|
+
}
|
|
275
|
+
|
|
135
276
|
/** Recognize capability-owned presentation for bounded offline context accounting. */
|
|
136
277
|
export function readCapabilityContextMessage(message, sessionId) {
|
|
137
278
|
const source = message?.source
|
|
@@ -702,12 +843,24 @@ export function createCapabilityEnvironment({ ctx, config, toolSchemasForAgent,
|
|
|
702
843
|
const capabilities = requiresCapabilityInventory(settings) ? await inventory(agent, sections ?? [], settings, signal) : []
|
|
703
844
|
const effective = effectiveCapabilitySettings(settings)
|
|
704
845
|
const coverage = (effective.policy ? effective.enabled && effective.policy.allocation.statistics : effective.statistics?.enabled === true) ? await store?.coverage(agent, capabilities, settings.statistics) : undefined
|
|
846
|
+
const planChannel = planChannelText(currentConfig)
|
|
847
|
+
const directoryBudgetBytes = planDirectoryBudget(settings.directoryBudgetBytes, planChannel.navigationSentence)
|
|
705
848
|
const plan = createCapabilityPlan({ ...settings, configurationRevision: digest(settings),
|
|
706
|
-
capabilities, coverage, navigationText:
|
|
849
|
+
capabilities, coverage, navigationText: planChannel.navigationSentence,
|
|
850
|
+
categories: planChannel.categoryLines,
|
|
851
|
+
directoryBudgetBytes })
|
|
707
852
|
const hasChoices = settings.contextEnabled !== undefined || settings.policy !== undefined || choice !== undefined || previous?.schema === 'ptc-capability-environment/v2'
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
const
|
|
853
|
+
// An edited plan channel needs the v5 record, and that generation requires the
|
|
854
|
+
// saved inputs and session choices it was composed from.
|
|
855
|
+
const recordsText = !planChannel.isDefault
|
|
856
|
+
const snapshot = deepFreeze({ schema: recordsText ? 'ptc-capability-environment/v5'
|
|
857
|
+
: settings.contextEnabled !== undefined ? 'ptc-capability-environment/v4' : settings.policy ? 'ptc-capability-environment/v3'
|
|
858
|
+
: hasChoices ? 'ptc-capability-environment/v2' : 'ptc-capability-environment/v1',
|
|
859
|
+
...(hasChoices || recordsText ? { sourceSettings, sessionOverrides } : {}),
|
|
860
|
+
...(recordsText ? planPromptText(planChannel, directoryBudgetBytes) : {}),
|
|
861
|
+
navigationText: planChannel.navigationSentence, settings, capabilities, ...(coverage === undefined ? {} : { coverage }), plan })
|
|
862
|
+
const initialized = sessionEvents(agent.session).some(event => event.type === 'system/message')
|
|
863
|
+
const message = planMessage(snapshot, id, force && initialized)
|
|
711
864
|
agent.signal?.throwIfAborted?.()
|
|
712
865
|
signal?.throwIfAborted()
|
|
713
866
|
// Initialized sessions commit a refresh and its notification atomically.
|
|
@@ -716,7 +869,7 @@ export function createCapabilityEnvironment({ ctx, config, toolSchemasForAgent,
|
|
|
716
869
|
hostSections: sections ?? [], expanded: new Set(), used: new Map(),
|
|
717
870
|
...(requiresCapabilityInventory(settings) ? { liveCapabilities: new Map(capabilities.map(item => [item.id, item])) } : {}) }
|
|
718
871
|
restoreEvidence(agent, state)
|
|
719
|
-
if (choice?.commandId && !
|
|
872
|
+
if (choice?.commandId && !initialized) {
|
|
720
873
|
state.pendingCommand = choice.commandId
|
|
721
874
|
state.pendingPrevious = stateFor(agent)
|
|
722
875
|
}
|
|
@@ -1186,7 +1339,12 @@ export function createCapabilityEnvironment({ ctx, config, toolSchemasForAgent,
|
|
|
1186
1339
|
|| String(agent.session.id ?? agent.id) !== sessionId) {
|
|
1187
1340
|
throw new Error('ptc-plus: capability observation target is no longer available')
|
|
1188
1341
|
}
|
|
1189
|
-
|
|
1342
|
+
const savedSettings = capabilityConfiguration(currentConfig)
|
|
1343
|
+
// The saved group and management switches gate session writes in change();
|
|
1344
|
+
// report them so the Client can block staged choices with the reason.
|
|
1345
|
+
return { schema: 'ptc-capability-session-catalog/v1', management, entries,
|
|
1346
|
+
savedSettings: { enabled: effectiveCapabilitySettings(savedSettings).enabled === true,
|
|
1347
|
+
contextEnabled: savedSettings.contextEnabled !== false } }
|
|
1190
1348
|
},
|
|
1191
1349
|
async observeAllocation({ sessionId, configuration } = {}, signal) {
|
|
1192
1350
|
signal?.throwIfAborted()
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { capabilityRule, validateCapabilityPolicy, effectiveCapabilitySettings } from './capability-policy.js'
|
|
2
2
|
import { Buffer } from 'node:buffer'
|
|
3
|
+
import { PLAN_CATEGORY_LINES } from './prompt-text.js'
|
|
3
4
|
|
|
4
5
|
const MANAGED_CATEGORIES = new Set(['tool', 'skill', 'resource', 'resource-template', 'task'])
|
|
5
6
|
const EXCLUDED_CATEGORIES = new Set(['language', 'node', 'repl', 'ptc-plus', 'global-binding'])
|
|
6
7
|
const SELECTIONS = new Set(['full', 'on-demand', 'closed'])
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
['skill', 'Skills: search by task or skills.<name> for when to use and how to load instructions.'],
|
|
10
|
-
['resource', 'MCP resources: search by server or resource purpose.'],
|
|
11
|
-
['resource-template', 'MCP templates: search by server or resource purpose.'],
|
|
12
|
-
['task', 'Tasks: search by the operation you need.'],
|
|
13
|
-
])
|
|
8
|
+
/** Category guidance defaults; the registry owns the delivered literals. */
|
|
9
|
+
const CATEGORY_NAVIGATION = new Map(Object.entries(PLAN_CATEGORY_LINES))
|
|
14
10
|
|
|
15
11
|
function isRecord(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) }
|
|
16
12
|
|
|
@@ -121,6 +117,13 @@ function createLegacyCapabilityPlan(input) {
|
|
|
121
117
|
const budgetBytes = integer(options.budgetBytes, 'contract budget bytes')
|
|
122
118
|
const directoryBudgetBytes = integer(options.directoryBudgetBytes, 'directory budget bytes')
|
|
123
119
|
const navigationText = text(options.navigationText, 'bounded discovery guidance')
|
|
120
|
+
const categoryNavigation = options.categories === undefined
|
|
121
|
+
? CATEGORY_NAVIGATION
|
|
122
|
+
: new Map(options.categories.map(pair => {
|
|
123
|
+
if (!Array.isArray(pair) || pair.length !== 2) throw new TypeError('ptc-plus: capability categories must be [category, guidance] pairs')
|
|
124
|
+
const category = text(pair[0], `capability category ${pair[0]}`)
|
|
125
|
+
return [category, text(pair[1], `capability guidance for ${category}`)]
|
|
126
|
+
}))
|
|
124
127
|
if (bytes(navigationText) > directoryBudgetBytes) throw new RangeError('ptc-plus: discovery guidance exceeds directory byte budget')
|
|
125
128
|
const manual = record(options.manual ?? {}, 'manual capability settings')
|
|
126
129
|
const overrides = record(manual.overrides ?? {}, 'capability overrides')
|
|
@@ -220,7 +223,7 @@ function createLegacyCapabilityPlan(input) {
|
|
|
220
223
|
const directoryCategories = []
|
|
221
224
|
const availableCategories = new Set(capabilities.filter(capability => selections.get(capability.id).selection !== 'closed')
|
|
222
225
|
.map(capability => capability.category))
|
|
223
|
-
for (const [category, guidance] of
|
|
226
|
+
for (const [category, guidance] of categoryNavigation) {
|
|
224
227
|
if (!availableCategories.has(category)) continue
|
|
225
228
|
const proposed = `${directoryText}\n${guidance}`
|
|
226
229
|
if (bytes(proposed) <= directoryBudgetBytes) {
|
|
@@ -198,12 +198,16 @@ export function capabilityCoverage(state, query) {
|
|
|
198
198
|
const workspaceId = options.workspaceId === null ? null : identity(options.workspaceId, 'coverage workspace identity')
|
|
199
199
|
if (state.status !== 'usable') return { status: state.status, windowSize: state.limits.windowSize, eligible: null, used: null, coverage: null }
|
|
200
200
|
const window = state.windows.find(item => item.userId === userId && item.workspaceId === workspaceId && item.id === id && item.revision === revision)
|
|
201
|
-
|
|
201
|
+
// "Nothing recorded yet" and "recorded families are not currently settled"
|
|
202
|
+
// are different facts from a measured window; neither may read as a zero.
|
|
203
|
+
if (window === undefined) return { status: 'no-samples', windowSize: state.limits.windowSize, eligible: null, used: null, coverage: null }
|
|
204
|
+
const families = state.families.filter(item => qualifies(item, window)).sort(orderSamples).slice(0, state.limits.windowSize)
|
|
202
205
|
// A resumed family can expose a gap behind the irreversible eviction frontier.
|
|
203
206
|
// Surviving samples do not prove the recent settled window until it is full again.
|
|
204
|
-
if (window
|
|
207
|
+
if (window.floor > 0 && families.length < state.limits.windowSize) {
|
|
205
208
|
return { status: 'incomplete-window', windowSize: state.limits.windowSize, eligible: null, used: null, coverage: null }
|
|
206
209
|
}
|
|
210
|
+
if (families.length === 0) return { status: 'pending-settlement', windowSize: state.limits.windowSize, eligible: null, used: null, coverage: null }
|
|
207
211
|
const used = families.filter(family => family.observations.some(item => item.id === id && item.revision === revision && item.used
|
|
208
212
|
&& (workspaceId === null || item.workspaceId === workspaceId))).length
|
|
209
213
|
return { status: 'usable', windowSize: state.limits.windowSize, eligible: families.length, used, coverage: families.length ? used / families.length : null }
|