dsh-ptc-plus 0.5.1 → 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 +7 -3
- package/README.md +7 -3
- package/client.cjs +1467 -84
- 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/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/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/architecture.md +2 -2
- package/docs/client-ui.md +5 -2
- package/docs/durability-design.md +7 -1
- package/docs/releases/v0.5.2.md +79 -0
- package/docs/runtime-reference.md +36 -2
- package/docs/unmeasured-cell-recovery-repair-proposal.md +387 -0
- package/docs/verification.md +6 -2
- package/index.js +36 -15
- package/internal/bounded-literal.js +85 -0
- package/internal/capability-environment.js +162 -10
- package/internal/capability-plan.js +11 -8
- package/internal/cell-error.js +13 -0
- package/internal/config-spec.js +113 -2
- package/internal/direct-surface-owner.js +14 -10
- package/internal/edit-transport-owner.js +79 -14
- 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/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 +1 -1
- package/internal/runtime-config.js +2 -0
- package/internal/runtime-contexts.js +14 -5
- package/internal/runtime-messages.js +43 -15
- 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 +3 -0
- 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 +3 -1
- package/src/client-activity.js +24 -4
- package/src/client-capabilities-view.js +459 -24
- package/src/client-copy.js +163 -2
- package/src/client-repl-view.js +6 -1
- package/src/client-styles.js +64 -2
package/index.js
CHANGED
|
@@ -20,6 +20,7 @@ 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,
|
|
@@ -35,6 +36,7 @@ import { createHostRpc } from './internal/host-rpc.js'
|
|
|
35
36
|
import { RPC_CONTRACTS } from './internal/rpc-contract.js'
|
|
36
37
|
import { createReplObservationInterest } from './internal/repl-observation-interest.js'
|
|
37
38
|
import { guidancePolicies } from './internal/binding-update-policy.js'
|
|
39
|
+
import { PROMPT_SLOT_IDS, resolvePromptTextConfig } from './internal/prompt-text.js'
|
|
38
40
|
import * as dshSettings from '@deepseek-ai/dsh-settings'
|
|
39
41
|
|
|
40
42
|
const INSTALL_CLEANUP = Symbol('ptc-plus install cleanup')
|
|
@@ -59,7 +61,7 @@ export const Config = Schema.object(Object.fromEntries(
|
|
|
59
61
|
*/
|
|
60
62
|
export const inject = ['tools', 'systemPrompt', 'agents']
|
|
61
63
|
|
|
62
|
-
function replGuidance({ bindingPolicy, rewritesEnabled, languageSemantics, durableReplay, cordisToolsEnabled }) {
|
|
64
|
+
function replGuidance({ bindingPolicy, rewritesEnabled, languageSemantics, durableReplay, cordisToolsEnabled }, promptText) {
|
|
63
65
|
const looseTopLevelRedeclarations = bindingPolicy.variableRedeclarations
|
|
64
66
|
const looseTopLevelFunctionClassRedeclarations = bindingPolicy.functionClassRedeclarations
|
|
65
67
|
const autoRewriteImports = rewritesEnabled.autoRewriteImports
|
|
@@ -92,18 +94,22 @@ function replGuidance({ bindingPolicy, rewritesEnabled, languageSemantics, durab
|
|
|
92
94
|
const cordisRecovery = cordisToolsEnabled
|
|
93
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.'
|
|
94
96
|
: ''
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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')
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
/** Register the session-bound REPL runtime. */
|
|
@@ -345,6 +351,7 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolS
|
|
|
345
351
|
maxWallMs: activeConfig.maxWallMs,
|
|
346
352
|
maxOutputBytes: activeConfig.maxOutputBytes,
|
|
347
353
|
maxOldGenerationSizeMb: activeConfig.maxOldGenerationSizeMb,
|
|
354
|
+
workerReclaimMs: activeConfig.workerReclaimMs,
|
|
348
355
|
valueLimits: valueLimitsFromConfig(activeConfig),
|
|
349
356
|
})
|
|
350
357
|
capabilityEnvironment = createCapabilityEnvironment({ ctx, config: activeConfig,
|
|
@@ -375,6 +382,7 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolS
|
|
|
375
382
|
presentationGeneration,
|
|
376
383
|
sessionId,
|
|
377
384
|
toolSchemasForAgent,
|
|
385
|
+
promptText: resolvePromptTextConfig(activeConfig),
|
|
378
386
|
})
|
|
379
387
|
directSurface = createDirectSurfaceOwner({
|
|
380
388
|
sessionLogView: sessionLog.project,
|
|
@@ -394,7 +402,7 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolS
|
|
|
394
402
|
order: 98,
|
|
395
403
|
text: context => {
|
|
396
404
|
if (ctx.tools.get(RUN_CODE, context?.scope) === undefined) return ''
|
|
397
|
-
return replGuidance(guidancePolicies(activeConfig))
|
|
405
|
+
return replGuidance(guidancePolicies(activeConfig), resolvePromptTextConfig(activeConfig))
|
|
398
406
|
},
|
|
399
407
|
}))
|
|
400
408
|
disposers.push(ctx.on('system-prompt/assemble', async (assembly, context, next) => {
|
|
@@ -624,8 +632,11 @@ export const apply = (ctx, config = {}) => {
|
|
|
624
632
|
// Configuration is validated before the plugin waits for the host's seam, so
|
|
625
633
|
// an unusable configuration fails at the load call rather than later.
|
|
626
634
|
const resolvedConfig = resolveConfig(config)
|
|
635
|
+
// The retention window is user configuration; install it before any fold can run.
|
|
636
|
+
configureRetention(resolvedConfig)
|
|
627
637
|
return installExecutionSeam(ctx, {
|
|
628
638
|
services: inject,
|
|
639
|
+
attachmentTimeoutMs: resolvedConfig.executionSeamTimeoutMs,
|
|
629
640
|
attach: (scope, seam) => applyWithExecutionSeam(scope, resolvedConfig, seam, ctx, config),
|
|
630
641
|
reportFailure: (error, seamName) => ctx.logger?.warn?.(
|
|
631
642
|
`ptc-plus: execution seam ${seamName} failed to attach`,
|
|
@@ -886,9 +897,15 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatil
|
|
|
886
897
|
ctx.effect(() => async () => controller.dispose(), 'ptc-plus runtime lifecycle')
|
|
887
898
|
|
|
888
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())
|
|
889
905
|
const messageOwner = createRuntimeMessageOwner(context => (
|
|
890
906
|
committed?.retiring ? [] : committed?.runtime.contextsForRequest(context) ?? []
|
|
891
|
-
), sessionLog.project, () => committed !== undefined && committed.retiring !== true, handbackOwners.sessionQuery
|
|
907
|
+
), sessionLog.project, () => committed !== undefined && committed.retiring !== true, handbackOwners.sessionQuery,
|
|
908
|
+
currentPromptText)
|
|
892
909
|
ctx.effect(() => ctx.systemPrompt.context({
|
|
893
910
|
name: PTC_DELIVERY_CONTEXT, order: 98, text: '',
|
|
894
911
|
}), 'ptc-plus dynamic message delivery witness')
|
|
@@ -905,6 +922,8 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatil
|
|
|
905
922
|
await handBackRuntimeSessions(ctx, handbackOwners)
|
|
906
923
|
}, 'ptc-plus provider handback')
|
|
907
924
|
|
|
925
|
+
// The settings section replaces this source after activation; the prompt-text
|
|
926
|
+
// accessor above reads it lazily for exactly that reason.
|
|
908
927
|
let configSource = () => resolveConfig(volatileConfig ?? resolvedConfig)
|
|
909
928
|
let configurationGeneration = 0
|
|
910
929
|
let configurationRollback = false
|
|
@@ -935,6 +954,7 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatil
|
|
|
935
954
|
}
|
|
936
955
|
const reconcile = (propagateFailure = false) => {
|
|
937
956
|
const current = resolveConfig(configSource())
|
|
957
|
+
configureRetention(current)
|
|
938
958
|
const generation = ++configurationGeneration
|
|
939
959
|
if (!current.enabled) {
|
|
940
960
|
const operation = controller.uninstall()
|
|
@@ -982,6 +1002,7 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatil
|
|
|
982
1002
|
}
|
|
983
1003
|
ctx.effect(() => seam.onSourceChange(() => {
|
|
984
1004
|
const current = resolveConfig(configSource())
|
|
1005
|
+
configureRetention(current)
|
|
985
1006
|
if (!current.enabled) return undefined
|
|
986
1007
|
if (seam.language !== 'typescript') {
|
|
987
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
|
+
}
|
|
@@ -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,11 +843,22 @@ 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
|
-
|
|
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 })
|
|
710
862
|
const initialized = sessionEvents(agent.session).some(event => event.type === 'system/message')
|
|
711
863
|
const message = planMessage(snapshot, id, force && initialized)
|
|
712
864
|
agent.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) {
|
package/internal/cell-error.js
CHANGED
|
@@ -5,3 +5,16 @@ export class ModuleRewriteError extends SyntaxError {
|
|
|
5
5
|
if (cellPosition !== undefined) this.cellPosition = cellPosition
|
|
6
6
|
}
|
|
7
7
|
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The kernel refuses to admit a cell because a previous helper was not reclaimed.
|
|
11
|
+
*
|
|
12
|
+
* This is a fact about the kernel's own lifecycle, not about the recorded history
|
|
13
|
+
* or the submitted source, so consumers must not contract a frontier or blame a
|
|
14
|
+
* cell for it.
|
|
15
|
+
*/
|
|
16
|
+
export class CellAdmissionBlocked extends AggregateError {
|
|
17
|
+
constructor(failures) {
|
|
18
|
+
super(failures, 'previous computation helper reclamation failed; dispose the session before continuing')
|
|
19
|
+
}
|
|
20
|
+
}
|