dsh-ptc-plus 0.4.10 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.en.md +65 -127
  2. package/README.md +65 -127
  3. package/assets/dsh-ptc-plus-banner-en.webp +0 -0
  4. package/assets/dsh-ptc-plus-banner-zh.webp +0 -0
  5. package/assets/ptc-plus-repl-workspace-en.png +0 -0
  6. package/assets/ptc-plus-repl-workspace-zh.png +0 -0
  7. package/assets/ptc-plus-settings-en.png +0 -0
  8. package/assets/ptc-plus-settings-zh.png +0 -0
  9. package/client.cjs +42702 -0
  10. package/cordis.patch.yml +1 -0
  11. package/docs/README.md +6 -2
  12. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +31 -198
  13. package/docs/adr/0008-define-agent-operating-contract.md +5 -1
  14. package/docs/adr/0009-bound-expensive-acceptance-default.md +2 -2
  15. package/docs/adr/0016-archive-resolved-review-findings.md +1 -1
  16. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +4 -4
  17. package/docs/adr/0019-plugin-settings-and-kill-switch.md +1 -1
  18. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +1 -1
  19. package/docs/adr/0022-stage-npm-releases-with-oidc.md +4 -2
  20. package/docs/adr/0023-global-user-bindings.md +2 -0
  21. package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +2 -0
  22. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +25 -23
  23. package/docs/adr/0031-close-semantic-obligations-by-owner.md +1 -1
  24. package/docs/adr/0034-keep-program-values-separate-from-output-projections.md +29 -0
  25. package/docs/adr/0035-run-coverage-only-before-a-new-release.md +27 -0
  26. package/docs/adr/0036-separate-focused-development-from-delivery-verification.md +29 -0
  27. package/docs/adr/0037-keep-tool-hover-previews-passive.md +52 -0
  28. package/docs/adr/0038-freeze-session-capability-plans.md +71 -0
  29. package/docs/adr/0039-verify-documentation-without-runtime-suites.md +29 -0
  30. package/docs/adr/0040-separate-capability-configurations-from-session-observations.md +39 -0
  31. package/docs/adr/0041-record-cell-execution-directories.md +65 -0
  32. package/docs/adr/0042-persist-provider-availability-and-handback.md +41 -0
  33. package/docs/adr/0043-compose-capability-and-context-policies.md +37 -0
  34. package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +35 -0
  35. package/docs/adr/README.md +8 -0
  36. package/docs/architecture.md +23 -25
  37. package/docs/capability-planning-components.md +93 -0
  38. package/docs/capability-projection.md +7 -0
  39. package/docs/client-ui.md +70 -23
  40. package/docs/client-visual-contract.md +26 -9
  41. package/docs/context-management.md +22 -0
  42. package/docs/context-optimization-plan.md +353 -0
  43. package/docs/context-public-interface-coverage.md +25 -0
  44. package/docs/durability-design.md +20 -15
  45. package/docs/evaluation.md +41 -6
  46. package/docs/evaluations/command-goat-deepseek-v4.1-flash-20261006.md +126 -0
  47. package/docs/evaluations/command-goat-deepseek-v4.1-flash.md +281 -0
  48. package/docs/installation.md +33 -10
  49. package/docs/program-data-plane.md +5 -3
  50. package/docs/publishing.md +7 -7
  51. package/docs/release-coverage.md +73 -0
  52. package/docs/release-notes.md +1 -1
  53. package/docs/review-procedure.md +56 -0
  54. package/docs/runtime-reference.md +53 -16
  55. package/docs/semantic-validation.md +14 -2
  56. package/docs/stateful-binding-updates-proposal.md +2 -2
  57. package/docs/user-bindings.md +1 -1
  58. package/docs/value-wire.md +11 -1
  59. package/docs/verification-optimization-work.md +26 -15
  60. package/docs/verification.md +30 -88
  61. package/index.js +146 -20
  62. package/internal/binding-toggle-receipt.js +24 -0
  63. package/internal/capability-command-receipt.js +31 -0
  64. package/internal/capability-environment.js +1253 -0
  65. package/internal/capability-plan.js +285 -0
  66. package/internal/capability-policy.js +96 -0
  67. package/internal/capability-statistics-store.js +180 -0
  68. package/internal/capability-statistics.js +214 -0
  69. package/internal/config-spec.js +89 -10
  70. package/internal/direct-surface-owner.js +27 -29
  71. package/internal/dynamic-environment-runtime.js +9 -9
  72. package/internal/edit-transport-owner.js +195 -140
  73. package/internal/execution-provider-owner.js +40 -12
  74. package/internal/execution-seam-compat.js +26 -8
  75. package/internal/kernel-worker.js +104 -32
  76. package/internal/native-root-dynamic.js +12 -13
  77. package/internal/official-repl-priority.js +51 -0
  78. package/internal/program-bindings.js +190 -0
  79. package/internal/recovery-tips.js +3 -2
  80. package/internal/rejected-cell-editor.js +8 -8
  81. package/internal/repl-observation-interest.js +11 -3
  82. package/internal/rpc-contract.js +1 -0
  83. package/internal/runtime-bridge-owner.js +275 -34
  84. package/internal/runtime-config.js +58 -0
  85. package/internal/runtime-contexts.js +74 -5
  86. package/internal/runtime-intrinsics.js +1 -0
  87. package/internal/runtime-messages.js +41 -0
  88. package/internal/sdk-projection.js +10 -82
  89. package/internal/session-cell-executor.js +42 -19
  90. package/internal/session-events.js +34 -0
  91. package/internal/session-journal-recovery.js +234 -11
  92. package/internal/session-journal-schema.js +6 -3
  93. package/internal/session-journal.js +31 -2
  94. package/internal/session-log-view.js +1 -1
  95. package/internal/session-runtime.js +119 -15
  96. package/internal/settings-compat.js +12 -5
  97. package/internal/stateful-module-compiler.js +3 -2
  98. package/internal/stateful-module-runtime.js +7 -3
  99. package/internal/stateful-root-runtime.js +9 -4
  100. package/internal/user-binding-authoring.js +2 -7
  101. package/internal/user-bindings-owner.js +19 -2
  102. package/internal/validated-parse-repair.js +1 -1
  103. package/internal/value-wire.js +68 -21
  104. package/internal/worker-cwd-virtualization.js +20 -18
  105. package/internal/worker-realm-surfaces.js +3 -3
  106. package/package.json +49 -30
  107. package/src/client-activity.js +3 -2
  108. package/src/client-authoring-view.js +67 -30
  109. package/src/client-binding-review.js +2 -1
  110. package/src/client-binding-toggle.js +18 -0
  111. package/src/client-capabilities-view.js +1555 -0
  112. package/src/client-capability-sort.js +29 -0
  113. package/src/client-copy.js +923 -8
  114. package/src/client-feature-gates.js +3 -0
  115. package/src/client-indicator-view.js +4 -6
  116. package/src/client-primitives.js +33 -2
  117. package/src/client-repl-view.js +20 -13
  118. package/src/client-settings-view.js +411 -41
  119. package/src/client-styles.js +112 -15
  120. package/src/client-tool-hover.js +185 -0
  121. package/src/client-tool-view.js +45 -9
  122. package/src/client-transport.js +44 -9
  123. package/src/client-workbench.js +6 -7
  124. package/src/client.js +76 -22
  125. package/client.js +0 -32479
@@ -0,0 +1,1253 @@
1
+ import { capabilityRule, capabilityPolicyForConfig, changeCapabilityRule, effectiveCapabilitySettings, CAPABILITY_DISCLOSURES, CAPABILITY_INVENTORY_CATEGORIES } from './capability-policy.js'
2
+ /** One session-owned capability view for presentation, discovery and preparation. */
3
+ import { createHash } from 'node:crypto'
4
+ import { jsonSchemaToTs, renderToolsSdk, ToolNotFoundError } from '@deepseek-ai/dsh-tools'
5
+ import { createUserMessage } from '@deepseek-ai/dsh-llm'
6
+ import { isModelInvocable, isUserInvocable } from '@deepseek-ai/dsh-skill'
7
+ import { createScope } from '@deepseek-ai/dsh-scope'
8
+ import { Context } from '@deepseek-ai/cordis'
9
+ import * as spillPolicy from '@deepseek-ai/dsh-spill-policy'
10
+ import { readSessionLog } from './session-events.js'
11
+ import { readCapabilityCommandReceipt } from './capability-command-receipt.js'
12
+ import { createCapabilityPlan } from './capability-plan.js'
13
+ import { PTC_MESSAGE_SOURCE_KIND } from './message-sources.js'
14
+ import { deepFreeze } from './record-utils.js'
15
+ import { capabilityProfileConfiguration } from './runtime-config.js'
16
+ import { readRuntimeAvailability } from './runtime-messages.js'
17
+
18
+ export const CAPABILITY_PLAN_FORM = 'capability-plan'
19
+ export const CAPABILITY_EVIDENCE_KEY = 'dshPtcPlusCapabilities'
20
+ const EVIDENCE_KEY = CAPABILITY_EVIDENCE_KEY
21
+ const DIRECT_TRANSPORT = 'run_code'
22
+ const EDIT_TRANSPORT = 'edit_run_code'
23
+ const TRANSPORTS = new Set([DIRECT_TRANSPORT, EDIT_TRANSPORT])
24
+ const LEGACY_NAVIGATION = 'Use capabilities.search({query}) to retrieve complete tool, Skill and MCP contracts. Unlisted capabilities remain discoverable. Skill descriptions are summaries; load the body with tools.skill({name}) before following it.'
25
+ 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
+ const digest = value => createHash('sha256').update(JSON.stringify(value)).digest('hex')
27
+ const messageText = message => message?.content?.filter(block => block.type === 'text').map(block => block.text).join('\n') ?? ''
28
+
29
+ /** Saved inputs and explicit session choices are distinct; automatic cancels a saved override. */
30
+ export function sessionCapabilitySettings(sourceSettings, sessionOverrides = {}) {
31
+ if (!sourceSettings || typeof sourceSettings !== 'object' || Array.isArray(sourceSettings)
32
+ || !sourceSettings.manual || typeof sourceSettings.manual !== 'object' || Array.isArray(sourceSettings.manual)
33
+ || !Array.isArray(sourceSettings.manual.baseline) || !sourceSettings.manual.overrides
34
+ || typeof sourceSettings.manual.overrides !== 'object' || Array.isArray(sourceSettings.manual.overrides)) {
35
+ throw new TypeError('invalid capability source settings')
36
+ }
37
+ if (!sessionOverrides || typeof sessionOverrides !== 'object' || Array.isArray(sessionOverrides)) throw new TypeError('invalid session capability choices')
38
+ let policy = sourceSettings.policy
39
+ const overrides = { ...sourceSettings.manual.overrides }
40
+ for (const [id, selection] of Object.entries(sessionOverrides)) {
41
+ if (policy && selection && typeof selection === 'object' && !Array.isArray(selection)) {
42
+ if (!/^(tools|skills|mcp|mcpServers)\..+$/.test(id) || !Object.keys(selection).length
43
+ || Object.entries(selection).some(([dimension, choice]) => !['availability', 'disclosure'].includes(dimension)
44
+ || !['automatic', ...(dimension === 'availability' ? ['enabled', 'closed'] : CAPABILITY_DISCLOSURES)].includes(choice))) {
45
+ throw new TypeError('invalid session capability choice')
46
+ }
47
+ for (const [dimension, choice] of Object.entries(selection)) policy = changeCapabilityRule(policy, id, dimension, choice)
48
+ continue
49
+ }
50
+ if (!/^(tools|skills|mcp|mcpServers)\..+$/.test(id)
51
+ || !['full', 'on-demand', 'closed', 'automatic'].includes(selection)
52
+ || id.startsWith('mcpServers.') && !['closed', 'automatic'].includes(selection)) throw new TypeError('invalid session capability choice')
53
+ if (selection === 'automatic') delete overrides[id]
54
+ else overrides[id] = selection
55
+ if (policy) {
56
+ if (selection === 'automatic') {
57
+ policy = changeCapabilityRule(changeCapabilityRule(policy, id, 'availability', 'automatic'), id, 'disclosure', 'automatic')
58
+ } else {
59
+ policy = changeCapabilityRule(policy, id, 'availability', selection === 'closed' ? 'closed' : 'enabled')
60
+ if (selection !== 'closed') policy = changeCapabilityRule(policy, id, 'disclosure', selection)
61
+ }
62
+ }
63
+ }
64
+ return { ...sourceSettings, ...(policy ? { policy } : {}), manual: { ...sourceSettings.manual, overrides } }
65
+ }
66
+
67
+ export function capabilityConfiguration(config) {
68
+ let statistics
69
+ try { statistics = JSON.parse(config.capabilityStatistics) } catch { statistics = null }
70
+ return {
71
+ ...(config.capabilityContextEnabled === undefined ? {} : { contextEnabled: config.capabilityContextEnabled }),
72
+ enabled: config.capabilityManagementEnabled === true,
73
+ ...(config.capabilityPolicy ? { policy: capabilityPolicyForConfig(config),
74
+ recoveryEnabled: config.capabilityRecoveryEnabled === true, previewEnabled: config.modelPreviewEnabled === true } : {}),
75
+ manual: { baseline: JSON.parse(config.capabilityBaseline ?? '[]'), overrides: JSON.parse(config.capabilityOverrides ?? '{}') },
76
+ statistics,
77
+ budgetBytes: config.capabilityBudgetBytes ?? 16384,
78
+ directoryBudgetBytes: config.capabilityDirectoryBytes ?? 2048,
79
+ recoveryBytes: config.capabilityRecoveryBytes ?? 8192,
80
+ previewTokens: config.modelPreviewTokens ?? 4096,
81
+ }
82
+ }
83
+
84
+ function sourcePlan(message, id) {
85
+ const source = message?.source
86
+ if (source?.kind !== PTC_MESSAGE_SOURCE_KIND || source.form !== CAPABILITY_PLAN_FORM || source.sessionId !== id) return undefined
87
+ const snapshot = source.snapshot
88
+ const generation = ['ptc-capability-environment/v1', 'ptc-capability-environment/v2', 'ptc-capability-environment/v3', 'ptc-capability-environment/v4'].indexOf(snapshot?.schema)
89
+ if (source.digest !== digest(snapshot) || generation < 0
90
+ || !Array.isArray(snapshot.capabilities) || snapshot.plan?.schema !== (generation === 3 ? 'ptc-capability-plan/v3' : generation === 2 ? 'ptc-capability-plan/v2' : 'ptc-capability-plan/v1')) {
91
+ throw new Error('damaged session capability plan')
92
+ }
93
+ if (generation >= 1) {
94
+ let merged
95
+ try {
96
+ if (snapshot.sessionOverrides === undefined) throw new TypeError('missing session choices')
97
+ merged = sessionCapabilitySettings(snapshot.sourceSettings, snapshot.sessionOverrides)
98
+ } catch (cause) {
99
+ throw new Error('damaged session capability choices', { cause })
100
+ }
101
+ if (JSON.stringify(merged) !== JSON.stringify(snapshot.settings)) throw new Error('inconsistent session capability choices')
102
+ }
103
+ if (generation === 3 ? typeof snapshot.settings.contextEnabled !== 'boolean' : snapshot.settings.contextEnabled !== undefined) {
104
+ throw new Error('inconsistent capability context generation')
105
+ }
106
+ if (generation === 2 || generation === 3 && snapshot.settings.policy !== undefined) {
107
+ if (!snapshot.settings.policy || typeof snapshot.settings.recoveryEnabled !== 'boolean'
108
+ || typeof snapshot.settings.previewEnabled !== 'boolean') throw new Error('damaged capability policy settings')
109
+ } else if (snapshot.settings.policy !== undefined) throw new Error('inconsistent historical capability generation')
110
+ if (snapshot.navigationText !== undefined && snapshot.navigationText !== NAVIGATION) throw new Error('unknown capability navigation contract')
111
+ const expected = createCapabilityPlan({ ...snapshot.settings, capabilities: snapshot.capabilities,
112
+ coverage: snapshot.coverage, navigationText: snapshot.navigationText ?? LEGACY_NAVIGATION,
113
+ configurationRevision: digest(snapshot.settings) })
114
+ if (JSON.stringify(expected) !== JSON.stringify(snapshot.plan)
115
+ || messageText(message) !== messageText(planMessage(snapshot, id, source.refresh === true))) {
116
+ throw new Error('inconsistent session capability plan')
117
+ }
118
+ return deepFreeze(structuredClone(snapshot))
119
+ }
120
+
121
+ function planMessage(snapshot, id, refresh = false) {
122
+ const plan = snapshot.plan
123
+ const text = plan.enabled
124
+ ? ['PTC Plus session capability environment' + (refresh ? ' refreshed; replaces the preceding capability plan.' : ':'),
125
+ plan.directoryText, plan.catalogText,
126
+ ...(plan.budgets.overageBytes ? [`Explicit full contracts exceed the configured budget by ${plan.budgets.overageBytes} UTF-8 bytes.`] : [])]
127
+ .filter(Boolean).join('\n\n')
128
+ : snapshot.settings.contextEnabled === false
129
+ ? 'PTC Plus capability and context settings are paused for this session. Use the host-provided capability contracts and result presentation.'
130
+ : 'PTC Plus capability management is off for this session. Use the host-provided capability contracts.'
131
+ return createUserMessage({ source: { kind: PTC_MESSAGE_SOURCE_KIND, form: CAPABILITY_PLAN_FORM,
132
+ sessionId: id, refresh, digest: digest(snapshot), snapshot }, content: [{ type: 'text', text }] })
133
+ }
134
+
135
+ /** Recognize capability-owned presentation for bounded offline context accounting. */
136
+ export function readCapabilityContextMessage(message, sessionId) {
137
+ const source = message?.source
138
+ if (source?.kind !== PTC_MESSAGE_SOURCE_KIND
139
+ || !['capability-plan', 'capability-navigation', 'capability-descriptions', 'capability-contracts'].includes(source.form)
140
+ || message.content?.length !== 1 || message.content[0]?.type !== 'text'
141
+ || typeof message.content[0].text !== 'string' || !message.content[0].text) return undefined
142
+ if (source.form === CAPABILITY_PLAN_FORM) {
143
+ try { if (!sourcePlan(message, sessionId)) return undefined } catch { return undefined }
144
+ }
145
+ if (source.form === 'capability-contracts'
146
+ && !readContractUpdate(message, source.snapshot?.planDigest, sessionId)) return undefined
147
+ if (source.form === 'capability-descriptions' && (!Array.isArray(source.entries)
148
+ || source.entries.length === 0 || source.entries.some(id => typeof id !== 'string' || !id)
149
+ || new Set(source.entries).size !== source.entries.length)) return undefined
150
+ return { form: source.form, ...(source.form === CAPABILITY_PLAN_FORM
151
+ ? { enabled: source.snapshot.plan.enabled, settings: source.snapshot.settings } : {}),
152
+ sections: [{ name: `tools:ptc-plus-${source.form}`, text: message.content[0].text }] }
153
+ }
154
+
155
+ function resourceServers(sections) {
156
+ const text = sections?.find(section => section.name === 'mcp-resource-servers')?.text
157
+ if (typeof text !== 'string' || !text.trim()) return []
158
+ // The published owner emits a JSON server-name array after this literal attribution.
159
+ const marker = 'as the server argument: '
160
+ const offset = text.indexOf(marker)
161
+ if (offset < 0) throw new Error('ptc-plus: MCP resource server attribution is missing')
162
+ const encoded = text.slice(offset + marker.length).trim().replace(/\.$/, '')
163
+ const names = JSON.parse(encoded)
164
+ if (!Array.isArray(names) || names.some(name => typeof name !== 'string')) throw new Error('ptc-plus: invalid MCP server directory')
165
+ return names
166
+ }
167
+
168
+ /** Host sections may carry no instructions while still proving owner identity. */
169
+ function sectionDependencies(sections) {
170
+ return sections.filter(section => typeof section.text !== 'string' || section.text.trim().length > 0)
171
+ .map(section => ({ id: section.name, text: section.text }))
172
+ }
173
+
174
+ function requiresCapabilityInventory(settings) {
175
+ settings = effectiveCapabilitySettings(settings)
176
+ return settings.enabled || settings.policy && (settings.statistics?.enabled === true || settings.recoveryEnabled)
177
+ }
178
+
179
+ function temporaryCapabilityRule(selection) {
180
+ if (selection && typeof selection === 'object') return { ...selection }
181
+ return selection === undefined || selection === 'inherit' ? {}
182
+ : selection === 'automatic' ? { availability: 'automatic', disclosure: 'automatic' }
183
+ : selection === 'closed' ? { availability: 'closed' } : { availability: 'enabled', disclosure: selection }
184
+ }
185
+
186
+ function savedConfiguration(settings) {
187
+ return { capabilityManagementEnabled: settings.enabled,
188
+ ...(settings.contextEnabled === undefined ? {} : { capabilityContextEnabled: settings.contextEnabled }),
189
+ capabilityBaseline: JSON.stringify(settings.manual.baseline), capabilityOverrides: JSON.stringify(settings.manual.overrides),
190
+ capabilityStatistics: JSON.stringify(settings.statistics), capabilityBudgetBytes: settings.budgetBytes,
191
+ capabilityDirectoryBytes: settings.directoryBudgetBytes, capabilityRecoveryBytes: settings.recoveryBytes,
192
+ modelPreviewTokens: settings.previewTokens,
193
+ ...(settings.policy ? { capabilityPolicy: JSON.stringify(settings.policy), capabilityRecoveryEnabled: settings.recoveryEnabled,
194
+ modelPreviewEnabled: settings.previewEnabled } : {}) }
195
+ }
196
+
197
+ /** One SDK snapshot owns a tool's signature, schemas and revision. */
198
+ // Mirrors the Host SDK generator's member rule: bare name for a valid
199
+ // identifier, quoted index only for an exotic name, so a contract never
200
+ // disagrees with the declaration the model programs against.
201
+ const SDK_MEMBER_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/
202
+ function sdkMemberCall(name, callArguments) {
203
+ return SDK_MEMBER_IDENTIFIER.test(name)
204
+ ? `tools.${name}(${callArguments})`
205
+ : `tools[${JSON.stringify(name)}](${callArguments})`
206
+ }
207
+
208
+ function toolCapability(schema, sections) {
209
+ return { id: `tools.${schema.name}`, category: CAPABILITY_INVENTORY_CATEGORIES.tool,
210
+ revision: digest(schema), schema,
211
+ contract: `\`\`\`ts\n${schema.description ? `/** ${schema.description.replaceAll('*/', '* /')} */\n` : ''}${sdkMemberCall(schema.name, `args: ${jsonSchemaToTs(schema.parameters)}`)}: Promise<${jsonSchemaToTs(schema.output)}>\n\`\`\``,
212
+ dependencies: sectionDependencies(sections.filter(section => section.name === `tool:${schema.name}`
213
+ || (section.name?.startsWith('mcp:') && schema.name.startsWith(`mcp__${section.name.slice(4)}__`)))),
214
+ description: schema.description ?? '' }
215
+ }
216
+
217
+ function skillInvocationContract(name, description, invocation) {
218
+ const loading = invocation.modelInvocable
219
+ ? `Load instructions: await tools.skill(${JSON.stringify({ name })})`
220
+ : invocation.userInvocable ? `User-only Skill: the user can invoke /${name}. The model cannot load this Skill with tools.skill.`
221
+ : 'No invocation path is available in this session.'
222
+ return `${description}\nInvocation: modelInvocable=${invocation.modelInvocable}, userInvocable=${invocation.userInvocable}.\n${loading}`
223
+ }
224
+
225
+ function skillCapability(skill, modelLoaderAvailable = true) {
226
+ const nativeInvocation = { modelInvocable: isModelInvocable(skill), userInvocable: isUserInvocable(skill) }
227
+ const invocation = { ...nativeInvocation, modelInvocable: nativeInvocation.modelInvocable && modelLoaderAvailable }
228
+ const summary = { name: skill.name, provider: skill.provider, source: skill.source,
229
+ description: skill.description, invocation }
230
+ return { id: `skills.${skill.name}`, category: CAPABILITY_INVENTORY_CATEGORIES.skill, revision: digest(summary),
231
+ contract: skillInvocationContract(skill.name, skill.description, invocation),
232
+ description: skill.description, selector: { name: skill.name }, invocation,
233
+ ...(!modelLoaderAvailable ? { nativeInvocation, effectiveAvailability: invocation.userInvocable ? 'enabled' : 'closed' } : {}) }
234
+ }
235
+
236
+ /** Retain proved Skill identity while the live loader still owns model invocation. */
237
+ function retainedSkillCapability(capability, modelLoaderAvailable) {
238
+ const nativeInvocation = capability.nativeInvocation ?? capability.invocation
239
+ const invocation = { ...nativeInvocation, modelInvocable: nativeInvocation.modelInvocable && modelLoaderAvailable }
240
+ if (JSON.stringify(invocation) === JSON.stringify(capability.invocation)) return capability
241
+ const { nativeInvocation: _nativeInvocation, effectiveAvailability: _availability, ...retained } = capability
242
+ return { ...retained, invocation, revision: digest({ revision: capability.revision, invocation }),
243
+ contract: skillInvocationContract(capability.selector.name, capability.description, invocation),
244
+ ...(!modelLoaderAvailable ? { nativeInvocation, effectiveAvailability: invocation.userInvocable ? 'enabled' : 'closed' } : {}) }
245
+ }
246
+
247
+ const callingRevision = capability => capability === undefined || capability.effectiveAvailability === 'closed' ? null : digest({
248
+ revision: capability.revision, contract: capability.contract, dependencies: capability.dependencies ?? [],
249
+ ...(capability.invocation === undefined ? {} : { invocation: capability.invocation }),
250
+ })
251
+
252
+ function contractUpdateMessage(snapshot, sessionId) {
253
+ const dependencies = new Map()
254
+ const text = ['PTC Plus current calling contracts. These updates replace the earlier contracts for the listed capabilities only; session enablement and disclosure choices are unchanged.',
255
+ ...snapshot.changes.map(change => change.capability === null
256
+ ? `### ${change.id}\nThe Host no longer supplies this capability. Its earlier calling contract no longer applies.`
257
+ : change.disclosure === 'full'
258
+ ? [...(change.capability.dependencies ?? []).flatMap(item => {
259
+ const previous = dependencies.get(item.id)
260
+ if (previous !== undefined && previous !== item.text) throw new Error(`conflicting current contract dependency ${item.id}`)
261
+ if (previous !== undefined) return []
262
+ dependencies.set(item.id, item.text)
263
+ return [`### dependency:${item.id}\n${item.text}`]
264
+ }), `### ${change.id}\n${change.capability.contract}`].join('\n\n')
265
+ : `### ${change.id}\nThe Host calling contract has changed. Retrieve the current complete contract with capabilities.search(${JSON.stringify({ query: change.id })}) before invoking it.`),
266
+ ...(snapshot.overageBytes > 0 ? [`The currently selected full contracts exceed the configured budget by ${snapshot.overageBytes} UTF-8 bytes.`] : [])].join('\n\n')
267
+ return createUserMessage({ source: { kind: PTC_MESSAGE_SOURCE_KIND, form: 'capability-contracts', sessionId,
268
+ digest: digest(snapshot), snapshot }, content: [{ type: 'text', text }] })
269
+ }
270
+
271
+ function readContractUpdate(message, planDigest, sessionId) {
272
+ const source = message?.source
273
+ const snapshot = source?.snapshot
274
+ if (source?.kind !== PTC_MESSAGE_SOURCE_KIND || source.form !== 'capability-contracts'
275
+ || source.sessionId !== sessionId || snapshot?.schema !== 'ptc-capability-contracts/v1'
276
+ || snapshot.planDigest !== planDigest || source.digest !== digest(snapshot)
277
+ || !Array.isArray(snapshot.views) || !Array.isArray(snapshot.changes)
278
+ || !Number.isSafeInteger(snapshot.overageBytes) || snapshot.overageBytes < 0) return undefined
279
+ try {
280
+ if (snapshot.views.some(pair => !Array.isArray(pair) || pair.length !== 2 || typeof pair[0] !== 'string' || !pair[0]
281
+ || (pair[1] !== null && (typeof pair[1] !== 'string' || !pair[1])))
282
+ || new Set(snapshot.views.map(pair => pair[0])).size !== snapshot.views.length) return undefined
283
+ const views = new Map(snapshot.views)
284
+ if (snapshot.changes.length === 0 || new Set(snapshot.changes.map(change => change?.id)).size !== snapshot.changes.length
285
+ || snapshot.changes.some(change => !views.has(change?.id) || !['full', 'on-demand'].includes(change.disclosure)
286
+ || callingRevision(change.capability ?? undefined) !== views.get(change.id)
287
+ || (change.capability !== null && (change.capability?.id !== change.id
288
+ || typeof change.capability?.contract !== 'string' || !change.capability.contract)))) return undefined
289
+ if (messageText(message) !== messageText(contractUpdateMessage(snapshot, sessionId))) return undefined
290
+ } catch { return undefined }
291
+ return snapshot
292
+ }
293
+
294
+ /** Derived metadata only: invocation continues through the request's original host functions. */
295
+ export function createCapabilityEnvironment({ ctx, config, toolSchemasForAgent, modelSchemasForAgent = toolSchemasForAgent, store, sessionHistoryForAgent }) {
296
+ let currentConfig = config
297
+ const sessions = new Map()
298
+ const histories = new WeakMap()
299
+ const sessionEvents = session => Array.isArray(session?.events) ? session.events : histories.get(session) ?? []
300
+ async function observeHistory(agent, signal) {
301
+ const query = typeof ctx.get === 'function' ? ctx.get('sessionQuery') : ctx.sessionQuery
302
+ const log = sessionHistoryForAgent ? await sessionHistoryForAgent(agent, signal) : await readSessionLog(agent.session, query, signal)
303
+ const events = log?.events ?? []
304
+ histories.set(agent.session, events)
305
+ return events
306
+ }
307
+ function recordedPlan(history, id) {
308
+ // A routine provider release (`unloaded`) is an infrastructure fact, not a
309
+ // user decision: the session's frozen plan stays its restored baseline.
310
+ // Only an explicit disablement lets a resumed session recompose from the
311
+ // current saved configuration.
312
+ const withdrawn = history.findLastIndex(event => event.type === 'user/message'
313
+ && readRuntimeAvailability(event.data) === 'disabled')
314
+ const runs = new Map()
315
+ let latest
316
+ for (const event of history.slice(withdrawn + 1)) {
317
+ if (event.type === 'command/run' && event.data?.name === 'capabilities') runs.set(event.data.commandId, event.data)
318
+ let message = event.type === 'user/message' ? event.data : undefined
319
+ if (event.type === 'command/done' && event.data?.kind === 'success') {
320
+ const receipt = readCapabilityCommandReceipt(event.data.text)
321
+ const proposal = receipt?.proposal
322
+ const run = runs.get(event.data.commandId)
323
+ if (receipt?.sessionId === id && proposal?.commandId === event.data.commandId && proposal.rawInput === run?.args) message = proposal.message
324
+ }
325
+ if (message?.source?.kind === PTC_MESSAGE_SOURCE_KIND && message.source.form === CAPABILITY_PLAN_FORM && message.source.sessionId === id) latest = message
326
+ }
327
+ return latest
328
+ }
329
+ const operations = new WeakMap()
330
+ const completions = new WeakMap()
331
+ const admitted = new WeakMap()
332
+ const skillProjections = new Map()
333
+ const previewPolicies = new Map()
334
+ const report = error => ctx.logger?.warn?.('ptc-plus: capability statistics unavailable', error)
335
+ const statisticsOperation = operation => Promise.resolve().then(operation).catch(report)
336
+
337
+ function readSkills(context) {
338
+ return typeof context.get === 'function' ? context.get('skills') : context.skills
339
+ }
340
+
341
+ function currentDirectory(agent) {
342
+ const directories = typeof ctx.get === 'function' ? ctx.get('workingDirectory') : ctx.workingDirectory
343
+ return directories?.get(agent.session) ?? agent.session.header.cwd
344
+ }
345
+
346
+ async function observeSkills(agent, signal) {
347
+ if (!readSkills(ctx)?.snapshot) return { skills: [], complete: true }
348
+ // Read the original provider layers through a short-lived child scope. The
349
+ // session projection is exact-scope-only, so catalogue reads cannot toggle
350
+ // a global inventory flag and create a window where closed Skills load.
351
+ const child = {}
352
+ const isolated = typeof ctx.plugin === 'function' ? createScope(ctx, child, { parent: agent }) : null
353
+ const view = isolated ? child : agent
354
+ let observed
355
+ try {
356
+ const combined = [signal, agent.signal].filter(Boolean)
357
+ observed = await readSkills(ctx).snapshot({ cwd: currentDirectory(agent), scope: view,
358
+ signal: combined.length > 1 ? AbortSignal.any(combined) : combined[0] })
359
+ } finally { await isolated?.dispose() }
360
+ return { ...observed, skills: observed.skills.filter(skill => isModelInvocable(skill) || isUserInvocable(skill)) }
361
+ }
362
+
363
+ function recordedSkills(agent, state) {
364
+ const skills = new Map(state.snapshot.capabilities.filter(item => item.category === 'skill').map(item => [item.id, item]))
365
+ const history = sessionEvents(agent.session)
366
+ const planIndex = state.message ? history.findLastIndex(event => event.type === 'user/message' && event.data?.id === state.message.id) : -1
367
+ const planDigest = digest(state.snapshot)
368
+ const sessionId = String(agent.session.id ?? agent.id)
369
+ // Accepted contract updates prove inventory state independently of whether
370
+ // their descriptions remain visible. They do not establish model knowledge.
371
+ for (const event of history.slice(planIndex + 1)) {
372
+ const update = event.type === 'user/message' && readContractUpdate(event.data, planDigest, sessionId)
373
+ for (const change of update?.changes ?? []) {
374
+ if (!change.id.startsWith('skills.')) continue
375
+ if (change.capability === null) skills.delete(change.id)
376
+ else if (change.capability.category === 'skill') skills.set(change.id, change.capability)
377
+ }
378
+ }
379
+ return [...skills.values()]
380
+ }
381
+
382
+ async function qualifyCapabilities(agent, state, signal = agent.signal) {
383
+ if (requiresCapabilityInventory(state.snapshot.settings)) {
384
+ const skills = { retained: state.provedSkills ?? recordedSkills(agent, state) }
385
+ const capabilities = await inventory(agent, state.hostSections, state.snapshot.settings, signal, undefined, state.snapshot.plan.enabled
386
+ ? new Set(state.snapshot.plan.entries.filter(entry => entry.selection !== 'closed').map(entry => entry.id)) : undefined, skills)
387
+ signal?.throwIfAborted()
388
+ state.liveCapabilities = new Map(capabilities.map(item => [item.id, item]))
389
+ state.skillInventoryComplete = skills.complete
390
+ state.partialSkillNames = skills.partialNames
391
+ if (skills.complete) state.provedSkills = capabilities.filter(item => item.category === 'skill')
392
+ await installSkillProjection(agent, state)
393
+ }
394
+ return state
395
+ }
396
+
397
+ async function inventory(agent, sections = [], settings, signal, presentation, frozenIds, skillObservation) {
398
+ if (settings) settings = effectiveCapabilitySettings(settings)
399
+ const schemas = toolSchemasForAgent(agent).filter(schema => !TRANSPORTS.has(schema.name))
400
+ const capabilities = schemas.map(schema => toolCapability(schema, sections))
401
+ const declaredServers = new Set([...resourceServers(sections),
402
+ ...sections.filter(section => section.name?.startsWith('mcp:')).map(section => section.name.slice(4))])
403
+ for (const capability of capabilities) {
404
+ const servers = [...declaredServers].filter(server => capability.schema.name.startsWith(`mcp__${server}__`))
405
+ if (servers.length === 1) capability.mcpServer = servers[0]
406
+ else if (servers.length > 1) capability.mcpServerCandidates = servers
407
+ presentation?.set(capability.id, { name: capability.schema.name,
408
+ source: servers.length === 1 ? { kind: 'mcp', server: servers[0] } : servers.length > 1
409
+ ? { kind: 'unknown' } : { kind: 'host' } })
410
+ }
411
+ if (skillObservation) skillObservation.complete = true
412
+ const hasOperation = name => (!frozenIds || frozenIds.has(`tools.${name}`)) && capabilities.some(item => item.id === `tools.${name}`
413
+ && (!settings?.enabled || !settings.policy || capabilityRule(settings.policy, item).availability !== 'closed'))
414
+ const policyPaths = Boolean(settings?.policy)
415
+ if (readSkills(ctx)?.snapshot !== undefined) {
416
+ const observed = await observeSkills(agent, signal)
417
+ if (skillObservation) {
418
+ skillObservation.complete = observed.complete
419
+ skillObservation.partialNames = observed.complete ? [] : observed.skills.map(skill => skill.name)
420
+ }
421
+ if (!observed.complete && !skillObservation) throw new Error('ptc-plus: the Skill catalog is incomplete; cannot select a new capability environment')
422
+ // A partial catalog cannot prove a withdrawal or a new Skill. Preserve only
423
+ // the last complete view; native dispatch still uses the live Host scope.
424
+ if (!observed.complete) capabilities.push(...skillObservation.retained.map(item =>
425
+ retainedSkillCapability(item, !policyPaths || hasOperation('skill'))))
426
+ for (const skill of observed.complete ? observed.skills : []) {
427
+ capabilities.push(skillCapability(skill, !policyPaths || hasOperation('skill')))
428
+ presentation?.set(`skills.${skill.name}`, { name: skill.name,
429
+ source: { kind: 'skill', bucket: skill.source, provider: skill.provider },
430
+ invocation: { modelInvocable: isModelInvocable(skill), userInvocable: isUserInvocable(skill) } })
431
+ }
432
+ }
433
+ const servers = resourceServers(sections)
434
+ const resourceOperations = ['list_mcp_resources', 'list_mcp_resource_templates', 'read_mcp_resource']
435
+ .filter(name => !policyPaths || hasOperation(name))
436
+ for (const server of servers) capabilities.push({ id: `mcp.${server}`, category: CAPABILITY_INVENTORY_CATEGORIES.resource, mcpServer: server,
437
+ revision: digest({ server, instructions: sections.find(section => section.name === `mcp:${server}`)?.text ?? '' }),
438
+ dependencies: sectionDependencies(sections.filter(section => section.name === `mcp:${server}`)), ...(policyPaths && resourceOperations.length === 0 ? { effectiveAvailability: 'closed' } : {}),
439
+ contract: !policyPaths ? `MCP resource server ${JSON.stringify(server)}. Use tools.list_mcp_resources({server}), tools.list_mcp_resource_templates({server}), and tools.read_mcp_resource({server,uri}); use the listed URI or expand a listed template.`
440
+ : `MCP resource server ${JSON.stringify(server)}. ${resourceOperations.length ? 'Available operations: ' + resourceOperations.map(name => `tools.${name}(${name === 'read_mcp_resource' ? '{server,uri}' : '{server}'})`).join(', ') + '.' : 'No resource operation is available in this session.'}`,
441
+ description: `MCP resource access registered by the Host for server ${server}. This records the registered resource layer and the session's resource operations, not a proved list of exposed resources or templates.`,
442
+ selector: { server } })
443
+ for (const server of servers) presentation?.set(`mcp.${server}`, { name: server, source: { kind: 'mcp', server } })
444
+ return capabilities.filter(item => {
445
+ if (item.category !== 'tool') return true
446
+ const category = item.schema.name === 'skill' ? 'skill'
447
+ : ['list_mcp_resources', 'list_mcp_resource_templates', 'read_mcp_resource'].includes(item.schema.name) ? 'resource' : undefined
448
+ return category === undefined || capabilities.some(member => member.category === category
449
+ && (category !== 'skill' || member.invocation.modelInvocable)
450
+ && (!settings?.enabled || (settings.policy ? capabilityRule(settings.policy, member).availability !== 'closed' : settings.manual.overrides[member.id] !== 'closed'))
451
+ && (!settings?.enabled || settings.policy || member.mcpServer === undefined || settings?.manual.overrides[`mcpServers.${member.mcpServer}`] !== 'closed'))
452
+ })
453
+ }
454
+
455
+ function stateFor(agent) { return sessions.get(String(agent?.session?.id ?? agent?.id)) }
456
+ const canRefresh = agent => modelSchemasForAgent(agent).some(schema => schema.name === 'run_code')
457
+ function planCommitted(agent, message) {
458
+ return message !== undefined && (sessionEvents(agent.session).some(event =>
459
+ event.type === 'user/message' && event.data?.id === message.id)
460
+ || (agent.session.deriveMessages?.() ?? []).some(item => item.id === message.id))
461
+ }
462
+ function publishPlan(agent, message, signal) {
463
+ signal?.throwIfAborted()
464
+ if (typeof agent.session.append !== 'function') throw new Error('ptc-plus: capability plans require the public Session.append surface')
465
+ // The Host establishes its protected system head before accepting first-step
466
+ // messages. Preparation must not create that head or precede it on the surface.
467
+ if (!sessionEvents(agent.session).some(event => event.type === 'system/message')) return
468
+ agent.session.append('user/message', message, { surfaceOp: 'append' })
469
+ }
470
+ function selection(state, id) { return state?.snapshot.plan.entries.find(entry => entry.id === id)?.selection }
471
+ function liveCapability(state, id) {
472
+ return state?.liveCapabilities?.get(id) ?? state?.snapshot.capabilities.find(item => item.id === id)
473
+ }
474
+ function declaredCapabilities(state) {
475
+ const merged = new Map(state?.snapshot.capabilities.map(item => [item.id, item]) ?? [])
476
+ for (const [id, item] of state?.liveCapabilities ?? []) merged.set(id, item)
477
+ return merged
478
+ }
479
+ /**
480
+ * The plan owns the allocation of every identity it froze. A live Host member
481
+ * that the frozen plan never captured stays declared with the Host's own
482
+ * presentation (or its explicit availability closure) until an explicit
483
+ * refresh folds it into the plan; a refresh boundary must not remove the
484
+ * member's identity from the model-visible surface.
485
+ */
486
+ function declaredSelection(state, id) {
487
+ const planned = selection(state, id)
488
+ if (planned !== undefined || !state?.snapshot.plan.enabled) return planned
489
+ const live = state.liveCapabilities?.get(id)
490
+ if (live === undefined) return undefined
491
+ const settings = state.snapshot.settings
492
+ if (settings.policy) return capabilityRule(settings.policy, live).availability === 'closed' ? 'closed' : 'host'
493
+ const overrides = settings.manual?.overrides ?? {}
494
+ return overrides[id] === 'closed'
495
+ || (live.mcpServer !== undefined && overrides[`mcpServers.${live.mcpServer}`] === 'closed') ? 'closed' : 'host'
496
+ }
497
+ function supplied(state, id) {
498
+ if (state?.liveCapabilities && (!state.liveCapabilities.has(id) || state.liveCapabilities.get(id).effectiveAvailability === 'closed')) return false
499
+ if (!state?.snapshot.plan.enabled) return Boolean(state?.liveCapabilities) || !state?.snapshot.settings.policy || selection(state, id) !== 'closed'
500
+ if (!['full', 'on-demand', 'host'].includes(declaredSelection(state, id))) return false
501
+ const capability = liveCapability(state, id)
502
+ return capability?.category !== 'resource'
503
+ || resourceServers(state.hostSections).includes(capability.selector.server)
504
+ }
505
+ function sharedMembers(state, name) {
506
+ const category = name === 'skill' ? 'skill'
507
+ : ['list_mcp_resources', 'list_mcp_resource_templates', 'read_mcp_resource'].includes(name) ? 'resource' : undefined
508
+ return category === undefined ? undefined : [...declaredCapabilities(state).values()]
509
+ .filter(item => item.category === category && supplied(state, item.id)
510
+ && (category !== 'skill' || (state.liveCapabilities?.get(item.id) ?? item).invocation?.modelInvocable !== false))
511
+ .map(item => item.selector[category === 'skill' ? 'name' : 'server'])
512
+ }
513
+ function memberSupplied(state, name) {
514
+ return TRANSPORTS.has(name) || (supplied(state, `tools.${name}`) && sharedMembers(state, name)?.length !== 0)
515
+ }
516
+ function statisticsSnapshot(state) {
517
+ if (!state.liveCapabilities) return state.snapshot
518
+ const eligible = item => state.skillInventoryComplete !== false || item.category !== 'skill'
519
+ return { ...state.snapshot, plan: { ...state.snapshot.plan,
520
+ entries: state.snapshot.plan.enabled ? state.snapshot.plan.entries.filter(entry => supplied(state, entry.id) && state.liveCapabilities.has(entry.id) && eligible(entry))
521
+ .map(entry => ({ ...entry, revision: state.liveCapabilities.get(entry.id).revision }))
522
+ : [...state.liveCapabilities.values()].filter(item => supplied(state, item.id) && eligible(item))
523
+ .map(item => ({ id: item.id, revision: item.revision, category: item.category, selection: 'host' })) } }
524
+ }
525
+ function restoreEvidence(agent, state) {
526
+ state.pendingExpanded ??= new Set()
527
+ state.visibleExpansions ??= new Map()
528
+ for (const event of sessionEvents(agent.session)) {
529
+ const evidence = event.type === 'tool/result' && event.data?.meta?.[EVIDENCE_KEY]
530
+ if (!evidence || evidence.schema !== 'ptc-capability-evidence/v1'
531
+ || evidence.planDigest !== digest(state.snapshot)
532
+ || !Array.isArray(evidence.expanded) || !Array.isArray(evidence.used)
533
+ || [...evidence.expanded, ...evidence.used].some(id => typeof id !== 'string')) continue
534
+ const managed = id => state.snapshot.capabilities.some(item => item.id === id) && supplied(state, id)
535
+ for (const id of evidence.expanded.filter(managed)) state.expanded.add(id)
536
+ for (const id of evidence.used.filter(managed)) { state.used.delete(id); state.used.set(id, true) }
537
+ if (event.data.message?.id) {
538
+ const text = messageText(event.data.message)
539
+ const presented = evidence.expanded.filter(managed).filter(id => {
540
+ const capability = state.snapshot.capabilities.find(item => item.id === id)
541
+ return capability && [capability.contract, ...(capability.dependencies ?? []).map(item => item.text)]
542
+ .every(contract => text.includes(contract) || text.includes(JSON.stringify(contract).slice(1, -1)))
543
+ })
544
+ state.visibleExpansions.set(event.data.message.id, presented)
545
+ }
546
+ }
547
+ return state
548
+ }
549
+
550
+ function validateSupply(agent, name, args) {
551
+ const state = stateFor(agent)
552
+ if (!state?.snapshot.plan.enabled || TRANSPORTS.has(name)) return
553
+ if (!memberSupplied(state, name)) throw new ToolNotFoundError(name)
554
+ if (name === 'skill' && typeof args?.name === 'string' && !supplied(state, `skills.${args.name}`)) {
555
+ throw new Error(`skill "${args.name}" is unknown or no longer available`)
556
+ }
557
+ if (['list_mcp_resources', 'list_mcp_resource_templates', 'read_mcp_resource'].includes(name)
558
+ && typeof args?.server === 'string' && !supplied(state, `mcp.${args.server}`)) {
559
+ throw new Error(`MCP resource server "${args.server}" is not available`)
560
+ }
561
+ }
562
+
563
+ function skillProjectionEntries(state) {
564
+ if (!state.snapshot.plan.enabled) return []
565
+ const capabilities = new Map(state.snapshot.capabilities.map(item => [item.id, item]))
566
+ for (const [id, item] of state.liveCapabilities ?? []) capabilities.set(id, item)
567
+ for (const name of state.partialSkillNames ?? []) {
568
+ const id = `skills.${name}`
569
+ if (!capabilities.has(id)) capabilities.set(id, { id, category: 'skill', selector: { name } })
570
+ }
571
+ return [...capabilities.values()].filter(item => item.category === 'skill' && !supplied(state, item.id))
572
+ .map(item => ({ name: item.selector.name, description: 'Unavailable in this session capability environment.',
573
+ invocation: { modelInvocable: false, userInvocable: false }, source: 'ptc-plus-session-capabilities',
574
+ provider: 'ptc-plus-session-capabilities', rank: -Number.MAX_VALUE, locator: item.id }))
575
+ .sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0)
576
+ }
577
+
578
+ async function installSkillProjection(agent, state) {
579
+ let projection = skillProjections.get(agent)
580
+ if (!projection && state.snapshot.plan.enabled && readSkills(ctx)?.registerProvider) {
581
+ const scope = createScope(ctx, agent)
582
+ projection = { scope, entries: skillProjectionEntries(state) }
583
+ try {
584
+ scope.ctx.effect(() => readSkills(scope.ctx).registerProvider(control => {
585
+ projection.control = control
586
+ const name = 'ptc-plus-session-capabilities'
587
+ return { name,
588
+ async list(options) {
589
+ return options.scope === agent ? projection.entries : []
590
+ },
591
+ // A closed winning entry has no body. Neither model loading nor an
592
+ // explicit user gesture can invoke the original provider.
593
+ async get() { return undefined },
594
+ }
595
+ }), 'ptc-plus: session Skill projection')
596
+ skillProjections.set(agent, projection)
597
+ } catch (error) {
598
+ await scope.dispose()
599
+ throw error
600
+ }
601
+ }
602
+ if (projection) {
603
+ const entries = skillProjectionEntries(state)
604
+ if (JSON.stringify(entries) !== JSON.stringify(projection.entries)) {
605
+ projection.entries = entries
606
+ projection.control.invalidate()
607
+ }
608
+ }
609
+ }
610
+
611
+ async function acceptState(agent, state, publish, signal) {
612
+ const preceding = skillProjections.get(agent)
613
+ const precedingEntries = preceding?.entries
614
+ try {
615
+ await installSkillProjection(agent, state)
616
+ signal?.throwIfAborted()
617
+ if (publish) publishPlan(agent, state.message, signal)
618
+ } catch (error) {
619
+ const projection = skillProjections.get(agent)
620
+ if (preceding) {
621
+ const changed = JSON.stringify(preceding.entries) !== JSON.stringify(precedingEntries)
622
+ preceding.entries = precedingEntries
623
+ try { if (changed) preceding.control.invalidate() } catch (restoration) {
624
+ throw new AggregateError([error, restoration], 'PTC Plus Skill projection restoration failed')
625
+ }
626
+ } else if (projection) {
627
+ skillProjections.delete(agent)
628
+ await projection.scope.dispose()
629
+ }
630
+ throw error
631
+ }
632
+ state.session = agent.session
633
+ sessions.set(String(agent.session.id ?? agent.id), state)
634
+ return state
635
+ }
636
+
637
+ async function previewPolicy(agent) {
638
+ const state = stateFor(agent)
639
+ const settings = state && effectiveCapabilitySettings(state.snapshot.settings)
640
+ if (!(settings?.policy ? settings.previewEnabled : state?.snapshot.plan.enabled)) return undefined
641
+ const cap = state.snapshot.settings.previewTokens
642
+ if (!previewPolicies.has(cap)) {
643
+ // Cordis core events are context infrastructure, not an isolatable service.
644
+ // A separate context owns only the policy listeners; optional services
645
+ // continue to resolve from the live host at the moment of presentation.
646
+ const isolated = new Context()
647
+ spillPolicy.apply({
648
+ on: (...args) => isolated.on(...args),
649
+ get: name => ctx.get(name),
650
+ logger: ctx.logger,
651
+ }, { maxInlineTokens: cap })
652
+ previewPolicies.set(cap, isolated)
653
+ }
654
+ return previewPolicies.get(cap)
655
+ }
656
+
657
+ async function prepare(agent, sections, force = false, signal = agent?.signal, choice) {
658
+ signal?.throwIfAborted()
659
+ if (agent?.session === undefined) {
660
+ if (!effectiveCapabilitySettings(capabilityConfiguration(currentConfig)).enabled) return undefined
661
+ throw new TypeError('ptc-plus: capability management requires an agent Session')
662
+ }
663
+ await observeHistory(agent, signal)
664
+ await reconcilePendingChoice(agent, signal)
665
+ const id = String(agent.session.id ?? agent.id)
666
+ if (!force && sessions.has(id)) {
667
+ const state = sessions.get(id)
668
+ if (sections !== undefined) state.hostSections = sections
669
+ return qualifyCapabilities(agent, state, signal)
670
+ }
671
+ const pending = operations.get(agent)
672
+ if (pending !== undefined && !force) return pending
673
+ const task = (async () => {
674
+ if (!force) {
675
+ const history = sessionEvents(agent.session)
676
+ const recorded = recordedPlan(history, id)
677
+ if (recorded !== undefined) {
678
+ let snapshot
679
+ try { snapshot = sourcePlan(recorded, id) } catch (error) {
680
+ ctx.logger?.warn?.(`ptc-plus: ${error.message}; continuing with the host environment until /capabilities refresh`)
681
+ const settings = { ...capabilityConfiguration(currentConfig), enabled: false, ...(currentConfig.capabilityPolicy ? { recoveryEnabled: false, previewEnabled: false, statistics: { ...capabilityConfiguration(currentConfig).statistics, enabled: false } } : {}) }
682
+ const plan = createCapabilityPlan({ ...settings, configurationRevision: digest(settings), capabilities: [], navigationText: NAVIGATION })
683
+ snapshot = deepFreeze({ schema: settings.contextEnabled !== undefined ? 'ptc-capability-environment/v4' : settings.policy ? 'ptc-capability-environment/v3' : 'ptc-capability-environment/v1', ...(settings.policy || settings.contextEnabled !== undefined ? { sourceSettings: settings, sessionOverrides: {} } : {}), navigationText: NAVIGATION, settings, capabilities: [], plan })
684
+ const notice = planMessage(snapshot, id, true)
685
+ const state = { snapshot, message: notice, hostSections: sections ?? [], expanded: new Set(), used: new Map() }
686
+ return acceptState(agent, state, true, signal)
687
+ }
688
+ const restored = { snapshot, message: snapshot.settings.contextEnabled !== undefined || snapshot.plan.enabled || snapshot.settings.policy && (snapshot.settings.statistics?.enabled || snapshot.settings.recoveryEnabled || snapshot.settings.previewEnabled) || recorded.source.refresh ? recorded : undefined, hostSections: sections ?? [], expanded: new Set(), used: new Map() }
689
+ await qualifyCapabilities(agent, restored, signal)
690
+ signal?.throwIfAborted()
691
+ restoreEvidence(agent, restored)
692
+ return acceptState(agent, restored, false, signal)
693
+ }
694
+ }
695
+ const historical = !force && !sessionEvents(agent.session).some(event => event.type === 'user/message'
696
+ && readRuntimeAvailability(event.data) === 'disabled') && sessionEvents(agent.session).some(event =>
697
+ event.type === 'assistant/message' || event.type === 'tool/result')
698
+ const previous = stateFor(agent)?.snapshot
699
+ const sourceSettings = choice?.sourceSettings ?? { ...capabilityConfiguration(currentConfig), ...(historical ? { enabled: false, ...(currentConfig.capabilityPolicy ? { recoveryEnabled: false, previewEnabled: false, statistics: { ...capabilityConfiguration(currentConfig).statistics, enabled: false } } : {}) } : {}) }
700
+ const sessionOverrides = choice?.sessionOverrides ?? previous?.sessionOverrides ?? {}
701
+ const settings = sessionCapabilitySettings(sourceSettings, sessionOverrides)
702
+ const capabilities = requiresCapabilityInventory(settings) ? await inventory(agent, sections ?? [], settings, signal) : []
703
+ const effective = effectiveCapabilitySettings(settings)
704
+ const coverage = (effective.policy ? effective.enabled && effective.policy.allocation.statistics : effective.statistics?.enabled === true) ? await store?.coverage(agent, capabilities, settings.statistics) : undefined
705
+ const plan = createCapabilityPlan({ ...settings, configurationRevision: digest(settings),
706
+ capabilities, coverage, navigationText: NAVIGATION })
707
+ const hasChoices = settings.contextEnabled !== undefined || settings.policy !== undefined || choice !== undefined || previous?.schema === 'ptc-capability-environment/v2'
708
+ const snapshot = deepFreeze({ schema: settings.contextEnabled !== undefined ? 'ptc-capability-environment/v4' : settings.policy ? 'ptc-capability-environment/v3' : hasChoices ? 'ptc-capability-environment/v2' : 'ptc-capability-environment/v1',
709
+ ...(hasChoices ? { sourceSettings, sessionOverrides } : {}), navigationText: NAVIGATION, settings, capabilities, ...(coverage === undefined ? {} : { coverage }), plan })
710
+ const initialized = sessionEvents(agent.session).some(event => event.type === 'system/message')
711
+ const message = planMessage(snapshot, id, force && initialized)
712
+ agent.signal?.throwIfAborted?.()
713
+ signal?.throwIfAborted()
714
+ // Initialized sessions commit a refresh and its notification atomically.
715
+ // First-step proposals are accepted through the Host's pre-step messages.
716
+ const state = { snapshot, message: settings.contextEnabled === undefined && !effective.enabled && !force && !(effective.policy && (effective.statistics?.enabled || effective.recoveryEnabled || effective.previewEnabled)) ? undefined : message,
717
+ hostSections: sections ?? [], expanded: new Set(), used: new Map(),
718
+ ...(requiresCapabilityInventory(settings) ? { liveCapabilities: new Map(capabilities.map(item => [item.id, item])) } : {}) }
719
+ restoreEvidence(agent, state)
720
+ if (choice?.commandId && !initialized) {
721
+ state.pendingCommand = choice.commandId
722
+ state.pendingPrevious = stateFor(agent)
723
+ }
724
+ return acceptState(agent, state, force, signal)
725
+ })()
726
+ operations.set(agent, task)
727
+ try { return await task } finally { if (operations.get(agent) === task) operations.delete(agent) }
728
+ }
729
+
730
+ async function reconcilePendingChoice(agent, signal, requireReceipt = false) {
731
+ const state = stateFor(agent)
732
+ if (!state?.pendingCommand || planCommitted(agent, state.message)) return
733
+ const history = sessionEvents(agent.session)
734
+ const done = history.findLast(event => event.type === 'command/done' && event.data?.commandId === state.pendingCommand)
735
+ if (!done && !requireReceipt) return
736
+ const proved = recordedPlan(history, String(agent.session.id ?? agent.id))
737
+ if (proved?.id === state.message.id) {
738
+ delete state.pendingCommand; delete state.pendingPrevious
739
+ return
740
+ }
741
+ await acceptState(agent, state.pendingPrevious, false, signal)
742
+ }
743
+
744
+ function visibleSchemas(agent, schemas) {
745
+ const state = stateFor(agent)
746
+ if (!state?.snapshot.plan.enabled) return schemas
747
+ return schemas.filter(schema => memberSupplied(state, schema.name))
748
+ }
749
+
750
+ return Object.freeze({
751
+ reconfigure(next) { currentConfig = next },
752
+ prepare,
753
+ stateFor,
754
+ visibleSchemas,
755
+ validateSupply,
756
+ async refresh(agent, sections, signal, commandId) {
757
+ signal?.throwIfAborted()
758
+ if (typeof agent.runMaintenance !== 'function') throw new Error('ptc-plus: capability refresh requires the public agent maintenance boundary')
759
+ return agent.runMaintenance(async maintenanceSignal => {
760
+ await prepare(agent, sections, false, maintenanceSignal)
761
+ const assembly = await ctx.systemPrompt.assemble({ agent, scope: agent, signal: maintenanceSignal })
762
+ signal?.throwIfAborted()
763
+ maintenanceSignal.throwIfAborted()
764
+ const preparationSignal = AbortSignal.any([signal, maintenanceSignal, agent.signal].filter(Boolean))
765
+ const previous = stateFor(agent)?.snapshot
766
+ return prepare(agent, stateFor(agent)?.hostSections ?? assembly.sections, true, preparationSignal,
767
+ commandId === undefined ? undefined : {
768
+ sourceSettings: capabilityConfiguration(currentConfig), sessionOverrides: previous?.sessionOverrides ?? {}, commandId,
769
+ })
770
+ })
771
+ },
772
+ async catalogObservation(agent, signal) {
773
+ if (!canRefresh(agent)) throw new Error('ptc-plus: select an active PTC session')
774
+ const assembly = await ctx.systemPrompt.assemble({ agent, scope: agent, signal, ptcPlusCapabilityObservation: true })
775
+ const presentation = new Map()
776
+ const capabilities = await inventory(agent, assembly.sections,
777
+ { ...capabilityConfiguration(currentConfig), enabled: false, manual: { baseline: [], overrides: {} } }, signal, presentation)
778
+ signal?.throwIfAborted()
779
+ const history = await observeHistory(agent, signal)
780
+ await reconcilePendingChoice(agent, signal)
781
+ const recorded = recordedPlan(history, String(agent.session.id ?? agent.id))
782
+ const snapshot = stateFor(agent)?.snapshot ?? (recorded && sourcePlan(recorded, String(agent.session.id ?? agent.id)))
783
+ const state = snapshot ? { snapshot } : undefined
784
+ const sourceSettings = snapshot?.sourceSettings ?? snapshot?.settings
785
+ const sourceOverrides = sourceSettings?.manual?.overrides
786
+ const sessionSourceSelection = id => sourceOverrides?.[id] ?? (sourceOverrides ? 'automatic' : 'unknown')
787
+ // A paused or unmanaged session has no frozen allocation to report: every
788
+ // row then states the one truth that applies, the Host default. Only a
789
+ // managed session may report a pending refresh boundary.
790
+ const planManaged = state !== undefined && effectiveCapabilitySettings(state.snapshot.settings).enabled === true
791
+ const management = state === undefined ? 'unknown'
792
+ : state.snapshot.settings.contextEnabled === false ? 'paused'
793
+ : planManaged ? 'managed' : 'off'
794
+ const dimensions = capability => {
795
+ const saved = capabilityConfiguration(currentConfig)
796
+ if (!saved.policy && !snapshot?.settings.policy) return { managementEnabled: snapshot && effectiveCapabilitySettings(snapshot.settings).enabled }
797
+ const source = sourceSettings?.policy ? capabilityRule(sourceSettings.policy, capability) : undefined
798
+ const temporary = snapshot?.sessionOverrides?.[capability.id]
799
+ const rule = rules => capability.id.startsWith('mcpServers.') ? rules.mcpServers[capability.id.slice(11)] ?? {} : rules.items[capability.id] ?? {}
800
+ return { managementEnabled: snapshot && effectiveCapabilitySettings(snapshot.settings).enabled, ...(saved.policy ? { savedRule: rule(saved.policy) } : {}),
801
+ ...(sourceSettings?.policy ? { sourceRule: rule(sourceSettings.policy), sourceDimensions: source } : {}),
802
+ temporaryRule: temporaryCapabilityRule(temporary),
803
+ ...(snapshot?.settings.policy ? { effectiveDimensions: snapshot.plan.entries.find(entry => entry.id === capability.id)
804
+ ?? (capability.category === 'server' ? effectiveCapabilitySettings(snapshot.settings).enabled
805
+ ? capabilityRule(snapshot.settings.policy, capability) : { availability: 'enabled', disclosure: 'host', availabilitySource: 'host', disclosureSource: 'host' } : undefined) } : {}) }
806
+ }
807
+ const entries = capabilities.map(capability => ({ id: capability.id, category: capability.category,
808
+ description: capability.description, ...presentation.get(capability.id), ...dimensions(capability),
809
+ ...(capability.mcpServer === undefined ? {} : { mcpServer: capability.mcpServer }),
810
+ saved: capabilityConfiguration(currentConfig).manual.overrides[capability.id] ?? 'automatic',
811
+ sessionSourceSelection: sessionSourceSelection(capability.id),
812
+ temporary: typeof state?.snapshot.sessionOverrides?.[capability.id] === 'object' ? 'inherit' : state?.snapshot.sessionOverrides?.[capability.id] ?? 'inherit',
813
+ effective: state === undefined ? 'pending'
814
+ : planManaged ? state.snapshot.plan.entries.find(entry => entry.id === capability.id)?.selection ?? 'pending'
815
+ : 'host' }))
816
+ for (const server of new Set(capabilities.map(item => item.mcpServer).filter(Boolean))) {
817
+ const id = `mcpServers.${server}`
818
+ const enclosingClosure = state ? effectiveCapabilitySettings(state.snapshot.settings).enabled && (state.snapshot.settings.policy
819
+ ? state.snapshot.settings.policy.mcpServers[server]?.availability === 'closed'
820
+ : state.snapshot.settings.manual.overrides[id] === 'closed') : undefined
821
+ const effective = state === undefined ? 'pending'
822
+ : !planManaged ? 'host'
823
+ : state.snapshot.settings.policy
824
+ ? capabilityRule(state.snapshot.settings.policy, { id, category: 'server', mcpServer: server }).availability
825
+ : enclosingClosure ? 'closed' : 'enabled'
826
+ entries.push({ id, category: 'server', name: server, description: `MCP server ${server}`,
827
+ source: { kind: 'mcp', server }, ...dimensions({ id, category: 'server', mcpServer: server }), saved: capabilityConfiguration(currentConfig).manual.overrides[id] ?? 'automatic',
828
+ sessionSourceSelection: sessionSourceSelection(id),
829
+ temporary: typeof state?.snapshot.sessionOverrides?.[id] === 'object' ? 'inherit' : state?.snapshot.sessionOverrides?.[id] ?? 'inherit',
830
+ enclosingClosure, effective })
831
+ }
832
+ return { management, entries }
833
+ },
834
+ async catalogFor(agent, signal) {
835
+ return (await this.catalogObservation(agent, signal)).entries
836
+ },
837
+ async change(agent, intent, signal, globalSettings) {
838
+ if (!canRefresh(agent)) throw new Error('ptc-plus: select an active PTC session')
839
+ const dimension = intent.dimension
840
+ const choices = dimension === 'availability' ? ['enabled', 'closed', 'automatic', 'inherit']
841
+ : dimension === 'disclosure' ? [...CAPABILITY_DISCLOSURES, 'automatic', 'inherit']
842
+ : dimension === undefined ? ['full', 'on-demand', 'closed', 'automatic', 'inherit'] : []
843
+ if (!['session', 'global'].includes(intent.scope) || !choices.includes(intent.selection)
844
+ || intent.scope === 'global' && intent.selection === 'inherit') throw new TypeError('invalid capability change')
845
+ if (typeof agent.runMaintenance !== 'function') throw new Error('ptc-plus: capability changes require public agent maintenance')
846
+ return agent.runMaintenance(async maintenanceSignal => {
847
+ const combined = AbortSignal.any([signal, maintenanceSignal, agent.signal].filter(Boolean))
848
+ combined.throwIfAborted()
849
+ await prepare(agent, undefined, false, combined)
850
+ const assembly = await ctx.systemPrompt.assemble({ agent, scope: agent, signal: combined, ptcPlusCapabilityObservation: true })
851
+ const completeSettings = { ...capabilityConfiguration(currentConfig), enabled: false, manual: { baseline: [], overrides: {} } }
852
+ const available = await inventory(agent, assembly.sections, completeSettings, combined)
853
+ if (intent.id.startsWith('mcpServers.')) {
854
+ const server = intent.id.slice('mcpServers.'.length)
855
+ if (!available.some(item => item.mcpServer === server)) throw new Error('unknown MCP server in this session')
856
+ if (!dimension && !['closed', 'automatic', 'inherit'].includes(intent.selection)) throw new Error('MCP server choices are closed, automatic or inherit')
857
+ } else if (!available.some(item => item.id === intent.id)) throw new Error('unknown managed capability in this session')
858
+ const previous = stateFor(agent)?.snapshot
859
+ let sourceSettings = previous?.sourceSettings ?? previous?.settings ?? capabilityConfiguration(currentConfig)
860
+ if (dimension && !sourceSettings.policy) {
861
+ sourceSettings = capabilityConfiguration(capabilityProfileConfiguration(savedConfiguration(sourceSettings)))
862
+ }
863
+ if (!effectiveCapabilitySettings(sourceSettings).enabled || !effectiveCapabilitySettings(capabilityConfiguration(currentConfig)).enabled) throw new Error('ptc-plus: enable capability management before changing a session choice')
864
+ const sessionOverrides = { ...previous?.sessionOverrides }
865
+ let globalSaved = false
866
+ if (intent.scope === 'session') {
867
+ if (dimension || sourceSettings.policy) {
868
+ const patch = temporaryCapabilityRule(sessionOverrides[intent.id])
869
+ if (dimension) {
870
+ if (intent.selection === 'inherit') delete patch[dimension]
871
+ else patch[dimension] = intent.selection
872
+ } else if (intent.selection === 'inherit') {
873
+ delete patch.availability; delete patch.disclosure
874
+ } else if (intent.selection === 'closed') patch.availability = 'closed'
875
+ else Object.assign(patch, temporaryCapabilityRule(intent.selection))
876
+ if (Object.keys(patch).length) sessionOverrides[intent.id] = patch
877
+ else delete sessionOverrides[intent.id]
878
+ } else if (intent.selection === 'inherit') delete sessionOverrides[intent.id]
879
+ else sessionOverrides[intent.id] = intent.selection
880
+ } else {
881
+ if (!globalSettings?.read || !globalSettings?.update) throw new Error('ptc-plus: global capability changes require the public settings writer')
882
+ const before = globalSettings.read()
883
+ if (!before || !Number.isSafeInteger(before.revision)) throw new Error('ptc-plus: global capability changes require settings revision evidence')
884
+ if (!effectiveCapabilitySettings(capabilityConfiguration({ ...currentConfig, ...before.value })).enabled) throw new Error('ptc-plus: enable capability management before changing a global choice')
885
+ const overrides = JSON.parse(before.value.capabilityOverrides ?? '{}')
886
+ if (intent.selection === 'automatic') delete overrides[intent.id]
887
+ else overrides[intent.id] = intent.selection
888
+ combined.throwIfAborted()
889
+ const policy = capabilityPolicyForConfig(before.value)
890
+ let nextPolicy = dimension ? changeCapabilityRule(policy, intent.id, dimension, intent.selection)
891
+ : changeCapabilityRule(policy, intent.id, 'availability', intent.selection === 'closed' ? 'closed'
892
+ : intent.selection === 'automatic' ? 'automatic' : 'enabled')
893
+ if (!dimension) nextPolicy = intent.selection === 'automatic' ? changeCapabilityRule(nextPolicy, intent.id, 'disclosure', 'automatic')
894
+ : intent.selection === 'closed' ? nextPolicy : changeCapabilityRule(nextPolicy, intent.id, 'disclosure', intent.selection)
895
+ const patch = before.value.capabilityPolicy || dimension
896
+ ? { capabilityPolicy: JSON.stringify(nextPolicy), ...(!dimension ? { capabilityOverrides: JSON.stringify(overrides) } : {}) }
897
+ : { capabilityOverrides: JSON.stringify(overrides) }
898
+ await globalSettings.update(patch, before.revision)
899
+ globalSaved = true
900
+ }
901
+ try {
902
+ if (globalSaved) {
903
+ const saved = globalSettings.read()
904
+ if (!saved?.value || !Number.isSafeInteger(saved.revision)) throw new Error('ptc-plus: saved settings could not be confirmed for this session')
905
+ sourceSettings = capabilityConfiguration({ ...currentConfig, ...saved.value })
906
+ if (dimension) {
907
+ const retained = temporaryCapabilityRule(sessionOverrides[intent.id]); delete retained[dimension]
908
+ if (Object.keys(retained).length) sessionOverrides[intent.id] = retained
909
+ else delete sessionOverrides[intent.id]
910
+ } else delete sessionOverrides[intent.id]
911
+ }
912
+ combined.throwIfAborted()
913
+ if (!effectiveCapabilitySettings(sourceSettings).enabled) throw new Error('ptc-plus: enable capability management before changing a session choice')
914
+ await prepare(agent, assembly.sections, true, combined, { sourceSettings, sessionOverrides, commandId: intent.commandId })
915
+ return { globalSaved, sessionApplied: true, publication: planCommitted(agent, stateFor(agent).message) ? 'accepted' : 'pending' }
916
+ } catch (error) {
917
+ if (globalSaved) return { globalSaved: true, sessionApplied: false, error: String(error?.message ?? error) }
918
+ throw error
919
+ }
920
+ })
921
+ },
922
+ pendingChoiceMessage(agent) {
923
+ const state = stateFor(agent)
924
+ return ['ptc-capability-environment/v2', 'ptc-capability-environment/v3', 'ptc-capability-environment/v4'].includes(state?.snapshot.schema) && !planCommitted(agent, state.message) ? state.message : undefined
925
+ },
926
+ projectSections(agent, sections) {
927
+ const state = stateFor(agent)
928
+ if (!state?.snapshot.plan.enabled) return sections
929
+ const inventory = [...declaredCapabilities(state).values()]
930
+ const retained = inventory.filter(item => declaredSelection(state, item.id) === 'host' && supplied(state, item.id))
931
+ const retainedSections = new Set(retained.flatMap(item => item.dependencies ?? []).map(item => item.id))
932
+ const ownedSections = new Set(inventory.flatMap(item => item.dependencies ?? []).map(item => item.id))
933
+ return sections.flatMap(section => {
934
+ if (section.name === 'mcp-resource-servers') {
935
+ const servers = retained.filter(item => item.category === 'resource').map(item => item.selector.server)
936
+ return servers.length ? [{ ...section, text: `Use the following MCP resource servers as the server argument: ${JSON.stringify(servers)}.` }] : []
937
+ }
938
+ if (ownedSections.has(section.name) && !retainedSections.has(section.name)) return []
939
+ if (section.name?.startsWith('tool:') && !TRANSPORTS.has(section.name.slice(5))
940
+ && declaredSelection(state, `tools.${section.name.slice(5)}`) !== 'host') return []
941
+ return [section]
942
+ })
943
+ },
944
+ sdk(agent, nativeSdk) {
945
+ const state = stateFor(agent)
946
+ if (!state?.snapshot.plan.enabled) return nativeSdk
947
+ const candidates = toolSchemasForAgent(agent)
948
+ const schemas = candidates.filter(schema => !TRANSPORTS.has(schema.name))
949
+ const host = schemas.filter(schema => declaredSelection(state, `tools.${schema.name}`) === 'host'
950
+ && memberSupplied(state, schema.name))
951
+ if (host.length === schemas.length) return nativeSdk
952
+ // The edit transport is callable inside every program and is not part of
953
+ // the managed disclosure, so its Host schema stays declared in every
954
+ // managed render. The direct transport is never an SDK member.
955
+ const editTransport = candidates.find(schema => schema.name === EDIT_TRANSPORT)
956
+ return renderToolsSdk(editTransport === undefined ? host : [...host, editTransport])
957
+ },
958
+ projectMembers(agent, members) {
959
+ const state = stateFor(agent)
960
+ if (!state?.snapshot.plan.enabled) return members
961
+ return members.filter(name => memberSupplied(state, name))
962
+ },
963
+ metadata(agent) {
964
+ const state = stateFor(agent)
965
+ if (!state?.snapshot.plan.enabled) return []
966
+ const capabilities = declaredCapabilities(state)
967
+ return ['skill', 'resource'].map(category => ({ namespace: category === 'skill' ? 'skills' : 'mcp',
968
+ members: [...capabilities.values()].filter(item => item.category === category && supplied(state, item.id))
969
+ .map(item => state.liveCapabilities?.get(item.id) ?? item)
970
+ .map(item => ({ name: item.id.slice(item.id.indexOf('.') + 1), description: item.description,
971
+ contract: item.contract, dependencies: item.dependencies ?? [], revision: item.revision, parameters: {}, returns: {},
972
+ ...(item.invocation === undefined ? {} : { invocation: item.invocation }),
973
+ ...(item.nativeInvocation === undefined ? {} : { nativeInvocation: item.nativeInvocation }),
974
+ completeness: 'complete', effect: 'unknown', replay: 'recorded-value' })) }))
975
+ },
976
+ adaptFunction(agent, name, original) {
977
+ return (...args) => {
978
+ validateSupply(agent, name, args[0])
979
+ return original(...args)
980
+ }
981
+ },
982
+ explored(agent, result) {
983
+ const state = stateFor(agent)
984
+ for (const item of result?.matches ?? result?.symbols ?? []) {
985
+ const id = item.symbol ?? `${item.namespace}.${item.name}`
986
+ if (liveCapability(state, id) === undefined || !supplied(state, id)) continue
987
+ state.expanded.add(id)
988
+ state?.pendingExpanded?.add(id)
989
+ }
990
+ },
991
+ async preStep(payload, next) {
992
+ if (payload.signal?.aborted) return next()
993
+ // Preparation precedes the host's explicit Skill loading, using the same
994
+ // public composition entry that owns PTC/native selection and MCP sections.
995
+ if (ctx.systemPrompt?.assemble) {
996
+ await ctx.systemPrompt.assemble({ agent: payload.agent, scope: payload.agent, signal: payload.signal })
997
+ }
998
+ const prepared = stateFor(payload.agent)
999
+ if (prepared) await qualifyCapabilities(payload.agent, prepared, payload.signal)
1000
+ const decision = await next()
1001
+ if (decision.kind !== 'enter' || payload.signal?.aborted) return decision
1002
+ await observeHistory(payload.agent, payload.signal)
1003
+ await reconcilePendingChoice(payload.agent, payload.signal, true)
1004
+ const state = stateFor(payload.agent)
1005
+ if (decision.kind !== 'enter' || !state || payload.signal?.aborted) return decision
1006
+ await statisticsOperation(() => store?.available(payload.agent, statisticsSnapshot(state)))
1007
+ restoreEvidence(payload.agent, state)
1008
+ const visible = payload.agent.session.deriveMessages?.() ?? []
1009
+ const hostSkills = state.snapshot.settings.policy && state.snapshot.capabilities.filter(item => item.category === 'skill' && supplied(state, item.id))
1010
+ .every(item => selection(state, item.id) === 'host' && item.nativeInvocation === undefined)
1011
+ const messages = state.snapshot.plan.enabled ? decision.messages.filter(message =>
1012
+ (message.source?.kind !== 'skill-catalog' || hostSkills)
1013
+ && !(message.source?.kind === 'skill-invocation' && !supplied(state, `skills.${message.source.name}`))) : decision.messages
1014
+ // An uncommitted proposal enters once through Host acceptance. A committed
1015
+ // plan is already in history; its ancestry cannot resurrect compacted text.
1016
+ const planVisible = state.message && visible.some(message => message.id === state.message.id
1017
+ && readCapabilityContextMessage(message, String(payload.agent.session.id ?? payload.agent.id))?.form === CAPABILITY_PLAN_FORM)
1018
+ const additions = state.message && !planCommitted(payload.agent, state.message)
1019
+ && !messages.some(message => message.id === state.message.id) ? [state.message] : []
1020
+ const planSupplied = planVisible || [...messages, ...additions].some(message => message.id === state.message?.id)
1021
+ if (state.snapshot.plan.enabled || state.snapshot.settings.policy && effectiveCapabilitySettings(state.snapshot.settings).recoveryEnabled) {
1022
+ const planDigest = digest(state.snapshot)
1023
+ const sessionId = String(payload.agent.session.id ?? payload.agent.id)
1024
+ const recordedUpdate = [...visible, ...messages].map(message => readContractUpdate(message, planDigest, sessionId))
1025
+ .filter(Boolean).at(-1)
1026
+ const selected = state.snapshot.plan.entries.filter(entry => ['full', 'on-demand', 'host'].includes(entry.selection))
1027
+ const preceding = new Map(recordedUpdate?.views ?? selected.map(entry => [entry.id,
1028
+ callingRevision(state.snapshot.capabilities.find(item => item.id === entry.id))]))
1029
+ const views = selected.map(entry => [entry.id, callingRevision(state.liveCapabilities.get(entry.id))])
1030
+ const changes = selected.filter(entry => preceding.get(entry.id) !== callingRevision(state.liveCapabilities.get(entry.id)))
1031
+ .map(entry => ({ id: entry.id, disclosure: entry.selection === 'host' ? 'full' : entry.selection, capability: callingRevision(state.liveCapabilities.get(entry.id)) === null ? null : state.liveCapabilities.get(entry.id) }))
1032
+ if (changes.length) {
1033
+ const livePlan = createCapabilityPlan({ ...state.snapshot.settings, policy: undefined, capabilities: [...state.liveCapabilities.values()]
1034
+ .filter(item => state.snapshot.capabilities.some(original => original.id === item.id)),
1035
+ manual: { baseline: [], overrides: Object.fromEntries(state.snapshot.plan.entries.map(entry => [entry.id, entry.selection === 'host' ? 'full' : entry.selection])) },
1036
+ navigationText: NAVIGATION, configurationRevision: state.snapshot.plan.configurationRevision })
1037
+ additions.push(contractUpdateMessage({ schema: 'ptc-capability-contracts/v1', planDigest, views, changes,
1038
+ overageBytes: livePlan.budgets.overageBytes }, sessionId))
1039
+ }
1040
+ const expandedVisible = new Set([...visible, ...messages].flatMap(message => message.source?.form === 'capability-descriptions'
1041
+ ? message.source.entries ?? [] : state.visibleExpansions?.get(message.id) ?? []))
1042
+ for (const entry of selected.filter(entry => entry.selection === 'host')) {
1043
+ if (state.liveCapabilities.has(entry.id)) expandedVisible.add(entry.id)
1044
+ }
1045
+ for (const message of [...visible, ...messages, ...additions]) {
1046
+ const update = readContractUpdate(message, planDigest, sessionId)
1047
+ for (const change of update?.changes ?? []) {
1048
+ if (change.disclosure === 'full' && change.capability !== null
1049
+ && callingRevision(change.capability) === callingRevision(state.liveCapabilities.get(change.id))) expandedVisible.add(change.id)
1050
+ }
1051
+ }
1052
+ if (planSupplied) {
1053
+ for (const entry of state.snapshot.plan.entries.filter(entry => entry.selection === 'full')) {
1054
+ if (callingRevision(state.snapshot.capabilities.find(item => item.id === entry.id))
1055
+ === callingRevision(state.liveCapabilities.get(entry.id))) expandedVisible.add(entry.id)
1056
+ if (!state.expanded.has(entry.id)) state.pendingExpanded.add(entry.id)
1057
+ state.expanded.add(entry.id)
1058
+ }
1059
+ }
1060
+ if (state.snapshot.plan.directoryText && !planSupplied && ![...visible, ...messages].some(message => message.source?.form === 'capability-navigation')) {
1061
+ additions.push(createUserMessage({ source: { kind: PTC_MESSAGE_SOURCE_KIND, form: 'capability-navigation' },
1062
+ content: [{ type: 'text', text: state.snapshot.plan.directoryText || NAVIGATION }] }))
1063
+ }
1064
+ let body = ''
1065
+ const restored = []
1066
+ const dependencies = new Set()
1067
+ const currentSchemas = new Map(toolSchemasForAgent(payload.agent).map(schema => [`tools.${schema.name}`, digest(schema)]))
1068
+ for (const id of (state.snapshot.settings.policy && !effectiveCapabilitySettings(state.snapshot.settings).recoveryEnabled ? [] : [...state.used.keys()].reverse())) {
1069
+ const capability = state.liveCapabilities.get(id)
1070
+ if (!capability || capability.category === 'skill' || !state.expanded.has(id) || expandedVisible.has(id) || !supplied(state, id)) continue
1071
+ if (capability.category === 'tool' && currentSchemas.get(id) !== capability.revision) continue
1072
+ const missing = (capability.dependencies ?? []).filter(item => !dependencies.has(item.id))
1073
+ const contract = [...missing.map(item => `### dependency:${item.id}\n${item.text}`), `### ${id}\n${capability.contract}`].join('\n\n')
1074
+ if (Buffer.byteLength(body + '\n\n' + contract) > state.snapshot.settings.recoveryBytes) continue
1075
+ body += `${body ? '\n\n' : ''}${contract}`
1076
+ restored.push(id)
1077
+ for (const item of missing) dependencies.add(item.id)
1078
+ }
1079
+ if (body) additions.push(createUserMessage({ source: { kind: PTC_MESSAGE_SOURCE_KIND, form: 'capability-descriptions', entries: restored },
1080
+ content: [{ type: 'text', text: body }] }))
1081
+ }
1082
+ for (const message of messages) {
1083
+ if (message.source?.kind === 'skill-invocation' && supplied(state, `skills.${message.source.name}`)) {
1084
+ const observedSnapshot = statisticsSnapshot(state)
1085
+ void statisticsOperation(() => store?.used(payload.agent, observedSnapshot,
1086
+ `skills.${message.source.name}`, 'skill-body-accepted', 'complete'))
1087
+ }
1088
+ }
1089
+ // Before task history exists, the initial plan precedes admitted task messages.
1090
+ // Refresh and recovery append after committed history; they never reorder it.
1091
+ const initial = !sessionEvents(payload.agent.session).some(event => ['user/message', 'assistant/message', 'tool/result'].includes(event.type))
1092
+ return { ...decision, messages: initial ? [...additions, ...messages] : [...messages, ...additions] }
1093
+ },
1094
+ admitted(exec) {
1095
+ // Host tokens correlate immutable call identity through mutable wrappers.
1096
+ const state = stateFor(exec.agent)
1097
+ if (exec.token && (state && requiresCapabilityInventory(state.snapshot.settings))) admitted.set(exec.token, statisticsSnapshot(state))
1098
+ },
1099
+ dispatched(exec, result) {
1100
+ const state = stateFor(exec.agent)
1101
+ const observation = exec.token && admitted.get(exec.token)
1102
+ if (!observation || !state || !observation.plan.entries.some(entry => entry.id === `tools.${exec.name}`)
1103
+ || result?.error?.info?.code === 'ABORTED_BEFORE_DISPATCH' || result?.error?.info?.code === 'UNKNOWN_TOOL') return
1104
+ admitted.delete(exec.token)
1105
+ state.used.delete(`tools.${exec.name}`)
1106
+ state.used.set(`tools.${exec.name}`, true)
1107
+ void statisticsOperation(() => store?.used(exec.agent, observation, `tools.${exec.name}`, 'host-execution-result', result?.error?.info?.code === 'ABORTED' ? 'cancelled' : result?.isError ? 'failed' : 'complete'))
1108
+ if (exec.name === 'skill' && !result?.isError && typeof exec.arguments?.name === 'string') {
1109
+ void statisticsOperation(() => store?.used(exec.agent, observation, `skills.${exec.arguments.name}`, 'skill-body-accepted', 'complete'))
1110
+ }
1111
+ if (exec.name === 'read_mcp_resource' && !result?.isError && typeof exec.arguments?.server === 'string') {
1112
+ const id = `mcp.${exec.arguments.server}`
1113
+ state.used.delete(id)
1114
+ state.used.set(id, true)
1115
+ void statisticsOperation(() => store?.used(exec.agent, observation, `mcp.${exec.arguments.server}`, 'resource-access', 'complete'))
1116
+ }
1117
+ },
1118
+ toolMetadata(agent, metadata, schemas = toolSchemasForAgent(agent)) {
1119
+ const state = stateFor(agent)
1120
+ if (!state || !requiresCapabilityInventory(state.snapshot.settings)) return metadata
1121
+ const capabilities = new Map(schemas.map(schema => {
1122
+ const item = toolCapability(schema, state.hostSections)
1123
+ return [item.id, item]
1124
+ }))
1125
+ return metadata.map(entry => ({ ...entry, members: entry.members.map(member => {
1126
+ const item = capabilities.get(`${entry.namespace}.${member.name}`)
1127
+ return item ? { ...member, contract: item.contract, dependencies: item.dependencies ?? [], revision: item.revision } : member
1128
+ }) }))
1129
+ },
1130
+ complete(exec, result) {
1131
+ const state = stateFor(exec.agent)
1132
+ if (!state || !requiresCapabilityInventory(state.snapshot.settings)) return result
1133
+ if (!completions.has(exec)) {
1134
+ completions.set(exec, { schema: 'ptc-capability-evidence/v1', planDigest: digest(state.snapshot),
1135
+ expanded: [...state.pendingExpanded ?? []], used: [...state.used.keys()] })
1136
+ state.pendingExpanded?.clear()
1137
+ }
1138
+ return { ...result, meta: { ...result.meta, [EVIDENCE_KEY]: completions.get(exec) } }
1139
+ },
1140
+ async postExecute(exec, result, next) {
1141
+ const policy = await previewPolicy(exec.agent)
1142
+ return policy ? policy.waterfall('tools/post-execute', exec, result, next) : next()
1143
+ },
1144
+ async dispatchLog(dispatch, next) {
1145
+ const policy = await previewPolicy(dispatch.exec.agent)
1146
+ return policy ? policy.waterfall('tools/ptc-dispatch-log', dispatch, next) : next()
1147
+ },
1148
+ status(agent, status) {
1149
+ const familyId = store?.family(agent)
1150
+ for (const member of ctx.agents?.list?.() ?? [agent]) {
1151
+ if (member !== agent && store?.family(member) !== familyId) continue
1152
+ const state = stateFor(member)
1153
+ if (state) void statisticsOperation(() => store?.status(member, state.snapshot,
1154
+ member === agent ? status : member.status))
1155
+ }
1156
+ },
1157
+ describe(agent) {
1158
+ const state = stateFor(agent)
1159
+ return state === undefined ? { configured: capabilityConfiguration(currentConfig), effective: null }
1160
+ : { configured: capabilityConfiguration(currentConfig), effective: state.snapshot.plan,
1161
+ publication: state.message === undefined ? 'not-required' : planCommitted(agent, state.message) ? 'accepted' : 'pending',
1162
+ sourceSettings: state.snapshot.sourceSettings ?? state.snapshot.settings,
1163
+ sessionOverrides: state.snapshot.sessionOverrides ?? {},
1164
+ pendingRefresh: digest(capabilityConfiguration(currentConfig)) !== digest(state.snapshot.sourceSettings ?? state.snapshot.settings) }
1165
+ },
1166
+ describeSessions() {
1167
+ return [...sessions].map(([sessionId, state]) => ({ sessionId, effective: state.snapshot.plan,
1168
+ publication: state.message === undefined ? 'not-required' : planCommitted({ session: state.session }, state.message) ? 'accepted' : 'pending',
1169
+ configuration: savedConfiguration(state.snapshot.sourceSettings ?? state.snapshot.settings),
1170
+ sessionOverrides: state.snapshot.sessionOverrides ?? {},
1171
+ pendingRefresh: digest(capabilityConfiguration(currentConfig)) !== digest(state.snapshot.sourceSettings ?? state.snapshot.settings) }))
1172
+ },
1173
+ canRefresh,
1174
+ describeTargets() {
1175
+ return (ctx.agents?.list?.() ?? []).filter(canRefresh)
1176
+ .map(agent => ({ sessionId: String(agent.session.id ?? agent.id) }))
1177
+ },
1178
+ async describeSessionCatalog({ sessionId } = {}, signal) {
1179
+ signal?.throwIfAborted()
1180
+ if (typeof sessionId !== 'string' || !sessionId) throw new TypeError('ptc-plus: select an active PTC session')
1181
+ const agent = (ctx.agents?.list?.() ?? []).find(candidate =>
1182
+ String(candidate.session.id ?? candidate.id) === sessionId && canRefresh(candidate))
1183
+ if (!agent) throw new TypeError('ptc-plus: select an active PTC session')
1184
+ const { management, entries } = await this.catalogObservation(agent, signal)
1185
+ signal?.throwIfAborted()
1186
+ if (!(ctx.agents?.list?.() ?? []).includes(agent) || !canRefresh(agent)
1187
+ || String(agent.session.id ?? agent.id) !== sessionId) {
1188
+ throw new Error('ptc-plus: capability observation target is no longer available')
1189
+ }
1190
+ const savedSettings = capabilityConfiguration(currentConfig)
1191
+ // The saved group and management switches gate session writes in change();
1192
+ // report them so the Client can block staged choices with the reason.
1193
+ return { schema: 'ptc-capability-session-catalog/v1', management, entries,
1194
+ savedSettings: { enabled: effectiveCapabilitySettings(savedSettings).enabled === true,
1195
+ contextEnabled: savedSettings.contextEnabled !== false } }
1196
+ },
1197
+ async observeAllocation({ sessionId, configuration } = {}, signal) {
1198
+ signal?.throwIfAborted()
1199
+ const agent = (ctx.agents?.list?.() ?? []).find(agent => String(agent.session.id ?? agent.id) === sessionId && canRefresh(agent))
1200
+ if (!agent) throw new TypeError('ptc-plus: select an active PTC session to observe its capability inventory and workspace statistics')
1201
+ const raw = configuration === undefined ? savedConfiguration(capabilityConfiguration(currentConfig)) : configuration
1202
+ const validated = capabilityProfileConfiguration(raw, { allowInvalidStatistics: true })
1203
+ const settings = capabilityConfiguration(validated)
1204
+ const assembly = await ctx.systemPrompt.assemble({ agent, scope: agent, signal, ptcPlusCapabilityObservation: true })
1205
+ const capabilities = await inventory(agent, assembly.sections, settings, signal)
1206
+ const coverage = Number.isSafeInteger(settings.statistics?.windowSize) && settings.statistics.windowSize > 0
1207
+ ? await store?.coverage(agent, capabilities, settings.statistics) ?? null : null
1208
+ signal?.throwIfAborted()
1209
+ if (!(ctx.agents?.list?.() ?? []).includes(agent) || !canRefresh(agent)) throw new Error('ptc-plus: capability observation target is no longer available')
1210
+ const plan = createCapabilityPlan({ ...settings, capabilities, coverage, navigationText: NAVIGATION,
1211
+ configurationRevision: digest(settings) })
1212
+ return { sessionId, workspace: agent.session.header.cwd, configuration: validated, plan, coverage,
1213
+ capabilities: capabilities.map(item => ({ id: item.id, revision: item.revision, category: item.category,
1214
+ contractBytes: Buffer.byteLength(item.contract, 'utf8') })) }
1215
+ },
1216
+ async describeCatalog(signal) {
1217
+ const available = new Map()
1218
+ for (const agent of ctx.agents?.list?.() ?? []) {
1219
+ signal?.throwIfAborted()
1220
+ const state = stateFor(agent)
1221
+ if (!state && !modelSchemasForAgent(agent).some(schema => schema.name === 'run_code')) continue
1222
+ const hostSections = ctx.systemPrompt?.assemble
1223
+ ? (await ctx.systemPrompt.assemble({ agent, scope: agent, signal, ptcPlusCapabilityObservation: true })).sections
1224
+ : state?.hostSections ?? []
1225
+ const presentation = new Map()
1226
+ for (const capability of await inventory(agent, hostSections, capabilityConfiguration(currentConfig), signal, presentation)) {
1227
+ const display = presentation.get(capability.id)
1228
+ const previous = available.get(capability.id)
1229
+ available.set(capability.id, { id: capability.id, category: capability.category, description: capability.description,
1230
+ ...display, ...(capability.mcpServer === undefined ? {} : { mcpServer: capability.mcpServer }), ...(previous && JSON.stringify(previous.source) !== JSON.stringify(display?.source)
1231
+ ? { source: { kind: 'unknown' }, mcpServer: undefined } : {}),
1232
+ ...(previous && (previous.invocationConflict || JSON.stringify(previous.invocation) !== JSON.stringify(display?.invocation))
1233
+ ? { invocation: undefined, invocationConflict: true } : {}) })
1234
+ }
1235
+ }
1236
+ signal?.throwIfAborted()
1237
+ return [...available.values()].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
1238
+ },
1239
+ async disposeAgent(agent) {
1240
+ const projection = skillProjections.get(agent)
1241
+ skillProjections.delete(agent)
1242
+ await projection?.scope.dispose()
1243
+ sessions.delete(String(agent?.session?.id ?? agent?.id))
1244
+ },
1245
+ async dispose() {
1246
+ for (const projection of skillProjections.values()) await projection.scope.dispose()
1247
+ skillProjections.clear()
1248
+ for (const policy of previewPolicies.values()) await policy.fiber.dispose()
1249
+ previewPolicies.clear()
1250
+ sessions.clear()
1251
+ },
1252
+ })
1253
+ }