dsh-ptc-plus 0.4.10 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +60 -130
- package/README.md +60 -130
- package/assets/dsh-ptc-plus-banner-en.webp +0 -0
- package/assets/dsh-ptc-plus-banner-zh.webp +0 -0
- package/assets/ptc-plus-repl-workspace-en.png +0 -0
- package/assets/ptc-plus-repl-workspace-zh.png +0 -0
- package/assets/ptc-plus-settings-en.png +0 -0
- package/assets/ptc-plus-settings-zh.png +0 -0
- package/client.cjs +42336 -0
- package/cordis.patch.yml +1 -0
- package/docs/README.md +6 -2
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +31 -198
- package/docs/adr/0008-define-agent-operating-contract.md +5 -1
- package/docs/adr/0009-bound-expensive-acceptance-default.md +2 -2
- package/docs/adr/0016-archive-resolved-review-findings.md +1 -1
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +2 -2
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +1 -1
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +1 -1
- package/docs/adr/0022-stage-npm-releases-with-oidc.md +4 -2
- package/docs/adr/0023-global-user-bindings.md +2 -0
- package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +2 -0
- package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +25 -23
- package/docs/adr/0031-close-semantic-obligations-by-owner.md +1 -1
- package/docs/adr/0034-keep-program-values-separate-from-output-projections.md +29 -0
- package/docs/adr/0035-run-coverage-only-before-a-new-release.md +27 -0
- package/docs/adr/0036-separate-focused-development-from-delivery-verification.md +29 -0
- package/docs/adr/0037-keep-tool-hover-previews-passive.md +52 -0
- package/docs/adr/0038-freeze-session-capability-plans.md +71 -0
- package/docs/adr/0039-verify-documentation-without-runtime-suites.md +29 -0
- package/docs/adr/0040-separate-capability-configurations-from-session-observations.md +39 -0
- package/docs/adr/0041-record-cell-execution-directories.md +65 -0
- package/docs/adr/0042-persist-provider-availability-and-handback.md +41 -0
- package/docs/adr/0043-compose-capability-and-context-policies.md +37 -0
- package/docs/adr/README.md +6 -0
- package/docs/architecture.md +23 -25
- package/docs/capability-planning-components.md +93 -0
- package/docs/capability-projection.md +7 -0
- package/docs/client-ui.md +70 -23
- package/docs/client-visual-contract.md +26 -9
- package/docs/context-management.md +22 -0
- package/docs/context-optimization-plan.md +353 -0
- package/docs/context-public-interface-coverage.md +25 -0
- package/docs/durability-design.md +20 -15
- package/docs/evaluation.md +41 -6
- package/docs/evaluations/command-goat-deepseek-v4.1-flash-20261006.md +126 -0
- package/docs/evaluations/command-goat-deepseek-v4.1-flash.md +281 -0
- package/docs/installation.md +31 -10
- package/docs/program-data-plane.md +5 -3
- package/docs/publishing.md +5 -5
- package/docs/release-coverage.md +73 -0
- package/docs/release-notes.md +1 -1
- package/docs/review-procedure.md +56 -0
- package/docs/runtime-reference.md +48 -16
- package/docs/semantic-validation.md +14 -2
- package/docs/stateful-binding-updates-proposal.md +2 -2
- package/docs/user-bindings.md +1 -1
- package/docs/value-wire.md +11 -1
- package/docs/verification-optimization-work.md +26 -15
- package/docs/verification.md +30 -88
- package/index.js +123 -20
- package/internal/binding-toggle-receipt.js +24 -0
- package/internal/capability-command-receipt.js +29 -0
- package/internal/capability-environment.js +1247 -0
- package/internal/capability-plan.js +285 -0
- package/internal/capability-policy.js +96 -0
- package/internal/capability-statistics-store.js +180 -0
- package/internal/capability-statistics.js +210 -0
- package/internal/config-spec.js +79 -9
- package/internal/direct-surface-owner.js +27 -29
- package/internal/dynamic-environment-runtime.js +9 -9
- package/internal/edit-transport-owner.js +180 -139
- package/internal/execution-provider-owner.js +15 -12
- package/internal/execution-seam-compat.js +8 -8
- package/internal/kernel-worker.js +104 -32
- package/internal/native-root-dynamic.js +12 -13
- package/internal/program-bindings.js +190 -0
- package/internal/recovery-tips.js +3 -2
- package/internal/rejected-cell-editor.js +8 -8
- package/internal/repl-observation-interest.js +11 -3
- package/internal/rpc-contract.js +1 -0
- package/internal/runtime-bridge-owner.js +272 -34
- package/internal/runtime-config.js +58 -0
- package/internal/runtime-contexts.js +74 -5
- package/internal/runtime-intrinsics.js +1 -0
- package/internal/runtime-messages.js +41 -0
- package/internal/sdk-projection.js +10 -82
- package/internal/session-cell-executor.js +42 -19
- package/internal/session-events.js +34 -0
- package/internal/session-journal-recovery.js +234 -11
- package/internal/session-journal-schema.js +6 -3
- package/internal/session-journal.js +31 -2
- package/internal/session-log-view.js +1 -1
- package/internal/session-runtime.js +119 -15
- package/internal/settings-compat.js +12 -5
- package/internal/stateful-module-compiler.js +3 -2
- package/internal/stateful-module-runtime.js +7 -3
- package/internal/stateful-root-runtime.js +9 -4
- package/internal/user-binding-authoring.js +2 -7
- package/internal/user-bindings-owner.js +41 -2
- package/internal/validated-parse-repair.js +1 -1
- package/internal/value-wire.js +68 -21
- package/internal/worker-cwd-virtualization.js +20 -18
- package/internal/worker-realm-surfaces.js +3 -3
- package/package.json +47 -29
- package/src/client-authoring-view.js +67 -30
- package/src/client-binding-review.js +2 -1
- package/src/client-binding-toggle.js +18 -0
- package/src/client-capabilities-view.js +1341 -0
- package/src/client-capability-sort.js +29 -0
- package/src/client-copy.js +871 -8
- package/src/client-feature-gates.js +3 -0
- package/src/client-indicator-view.js +4 -6
- package/src/client-primitives.js +33 -2
- package/src/client-repl-view.js +20 -13
- package/src/client-settings-view.js +411 -41
- package/src/client-styles.js +108 -15
- package/src/client-tool-hover.js +185 -0
- package/src/client-tool-view.js +37 -8
- package/src/client-transport.js +44 -9
- package/src/client-workbench.js +6 -7
- package/src/client.js +76 -22
- package/client.js +0 -32479
|
@@ -0,0 +1,1247 @@
|
|
|
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 message = planMessage(snapshot, id, force)
|
|
711
|
+
agent.signal?.throwIfAborted?.()
|
|
712
|
+
signal?.throwIfAborted()
|
|
713
|
+
// Initialized sessions commit a refresh and its notification atomically.
|
|
714
|
+
// First-step proposals are accepted through the Host's pre-step messages.
|
|
715
|
+
const state = { snapshot, message: settings.contextEnabled === undefined && !effective.enabled && !force && !(effective.policy && (effective.statistics?.enabled || effective.recoveryEnabled || effective.previewEnabled)) ? undefined : message,
|
|
716
|
+
hostSections: sections ?? [], expanded: new Set(), used: new Map(),
|
|
717
|
+
...(requiresCapabilityInventory(settings) ? { liveCapabilities: new Map(capabilities.map(item => [item.id, item])) } : {}) }
|
|
718
|
+
restoreEvidence(agent, state)
|
|
719
|
+
if (choice?.commandId && !sessionEvents(agent.session).some(event => event.type === 'system/message')) {
|
|
720
|
+
state.pendingCommand = choice.commandId
|
|
721
|
+
state.pendingPrevious = stateFor(agent)
|
|
722
|
+
}
|
|
723
|
+
return acceptState(agent, state, force, signal)
|
|
724
|
+
})()
|
|
725
|
+
operations.set(agent, task)
|
|
726
|
+
try { return await task } finally { if (operations.get(agent) === task) operations.delete(agent) }
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
async function reconcilePendingChoice(agent, signal, requireReceipt = false) {
|
|
730
|
+
const state = stateFor(agent)
|
|
731
|
+
if (!state?.pendingCommand || planCommitted(agent, state.message)) return
|
|
732
|
+
const history = sessionEvents(agent.session)
|
|
733
|
+
const done = history.findLast(event => event.type === 'command/done' && event.data?.commandId === state.pendingCommand)
|
|
734
|
+
if (!done && !requireReceipt) return
|
|
735
|
+
const proved = recordedPlan(history, String(agent.session.id ?? agent.id))
|
|
736
|
+
if (proved?.id === state.message.id) {
|
|
737
|
+
delete state.pendingCommand; delete state.pendingPrevious
|
|
738
|
+
return
|
|
739
|
+
}
|
|
740
|
+
await acceptState(agent, state.pendingPrevious, false, signal)
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function visibleSchemas(agent, schemas) {
|
|
744
|
+
const state = stateFor(agent)
|
|
745
|
+
if (!state?.snapshot.plan.enabled) return schemas
|
|
746
|
+
return schemas.filter(schema => memberSupplied(state, schema.name))
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return Object.freeze({
|
|
750
|
+
reconfigure(next) { currentConfig = next },
|
|
751
|
+
prepare,
|
|
752
|
+
stateFor,
|
|
753
|
+
visibleSchemas,
|
|
754
|
+
validateSupply,
|
|
755
|
+
async refresh(agent, sections, signal, commandId) {
|
|
756
|
+
signal?.throwIfAborted()
|
|
757
|
+
if (typeof agent.runMaintenance !== 'function') throw new Error('ptc-plus: capability refresh requires the public agent maintenance boundary')
|
|
758
|
+
return agent.runMaintenance(async maintenanceSignal => {
|
|
759
|
+
await prepare(agent, sections, false, maintenanceSignal)
|
|
760
|
+
const assembly = await ctx.systemPrompt.assemble({ agent, scope: agent, signal: maintenanceSignal })
|
|
761
|
+
signal?.throwIfAborted()
|
|
762
|
+
maintenanceSignal.throwIfAborted()
|
|
763
|
+
const preparationSignal = AbortSignal.any([signal, maintenanceSignal, agent.signal].filter(Boolean))
|
|
764
|
+
const previous = stateFor(agent)?.snapshot
|
|
765
|
+
return prepare(agent, stateFor(agent)?.hostSections ?? assembly.sections, true, preparationSignal,
|
|
766
|
+
commandId === undefined ? undefined : {
|
|
767
|
+
sourceSettings: capabilityConfiguration(currentConfig), sessionOverrides: previous?.sessionOverrides ?? {}, commandId,
|
|
768
|
+
})
|
|
769
|
+
})
|
|
770
|
+
},
|
|
771
|
+
async catalogObservation(agent, signal) {
|
|
772
|
+
if (!canRefresh(agent)) throw new Error('ptc-plus: select an active PTC session')
|
|
773
|
+
const assembly = await ctx.systemPrompt.assemble({ agent, scope: agent, signal, ptcPlusCapabilityObservation: true })
|
|
774
|
+
const presentation = new Map()
|
|
775
|
+
const capabilities = await inventory(agent, assembly.sections,
|
|
776
|
+
{ ...capabilityConfiguration(currentConfig), enabled: false, manual: { baseline: [], overrides: {} } }, signal, presentation)
|
|
777
|
+
signal?.throwIfAborted()
|
|
778
|
+
const history = await observeHistory(agent, signal)
|
|
779
|
+
await reconcilePendingChoice(agent, signal)
|
|
780
|
+
const recorded = recordedPlan(history, String(agent.session.id ?? agent.id))
|
|
781
|
+
const snapshot = stateFor(agent)?.snapshot ?? (recorded && sourcePlan(recorded, String(agent.session.id ?? agent.id)))
|
|
782
|
+
const state = snapshot ? { snapshot } : undefined
|
|
783
|
+
const sourceSettings = snapshot?.sourceSettings ?? snapshot?.settings
|
|
784
|
+
const sourceOverrides = sourceSettings?.manual?.overrides
|
|
785
|
+
const sessionSourceSelection = id => sourceOverrides?.[id] ?? (sourceOverrides ? 'automatic' : 'unknown')
|
|
786
|
+
// A paused or unmanaged session has no frozen allocation to report: every
|
|
787
|
+
// row then states the one truth that applies, the Host default. Only a
|
|
788
|
+
// managed session may report a pending refresh boundary.
|
|
789
|
+
const planManaged = state !== undefined && effectiveCapabilitySettings(state.snapshot.settings).enabled === true
|
|
790
|
+
const management = state === undefined ? 'unknown'
|
|
791
|
+
: state.snapshot.settings.contextEnabled === false ? 'paused'
|
|
792
|
+
: planManaged ? 'managed' : 'off'
|
|
793
|
+
const dimensions = capability => {
|
|
794
|
+
const saved = capabilityConfiguration(currentConfig)
|
|
795
|
+
if (!saved.policy && !snapshot?.settings.policy) return { managementEnabled: snapshot && effectiveCapabilitySettings(snapshot.settings).enabled }
|
|
796
|
+
const source = sourceSettings?.policy ? capabilityRule(sourceSettings.policy, capability) : undefined
|
|
797
|
+
const temporary = snapshot?.sessionOverrides?.[capability.id]
|
|
798
|
+
const rule = rules => capability.id.startsWith('mcpServers.') ? rules.mcpServers[capability.id.slice(11)] ?? {} : rules.items[capability.id] ?? {}
|
|
799
|
+
return { managementEnabled: snapshot && effectiveCapabilitySettings(snapshot.settings).enabled, ...(saved.policy ? { savedRule: rule(saved.policy) } : {}),
|
|
800
|
+
...(sourceSettings?.policy ? { sourceRule: rule(sourceSettings.policy), sourceDimensions: source } : {}),
|
|
801
|
+
temporaryRule: temporaryCapabilityRule(temporary),
|
|
802
|
+
...(snapshot?.settings.policy ? { effectiveDimensions: snapshot.plan.entries.find(entry => entry.id === capability.id)
|
|
803
|
+
?? (capability.category === 'server' ? effectiveCapabilitySettings(snapshot.settings).enabled
|
|
804
|
+
? capabilityRule(snapshot.settings.policy, capability) : { availability: 'enabled', disclosure: 'host', availabilitySource: 'host', disclosureSource: 'host' } : undefined) } : {}) }
|
|
805
|
+
}
|
|
806
|
+
const entries = capabilities.map(capability => ({ id: capability.id, category: capability.category,
|
|
807
|
+
description: capability.description, ...presentation.get(capability.id), ...dimensions(capability),
|
|
808
|
+
...(capability.mcpServer === undefined ? {} : { mcpServer: capability.mcpServer }),
|
|
809
|
+
saved: capabilityConfiguration(currentConfig).manual.overrides[capability.id] ?? 'automatic',
|
|
810
|
+
sessionSourceSelection: sessionSourceSelection(capability.id),
|
|
811
|
+
temporary: typeof state?.snapshot.sessionOverrides?.[capability.id] === 'object' ? 'inherit' : state?.snapshot.sessionOverrides?.[capability.id] ?? 'inherit',
|
|
812
|
+
effective: state === undefined ? 'pending'
|
|
813
|
+
: planManaged ? state.snapshot.plan.entries.find(entry => entry.id === capability.id)?.selection ?? 'pending'
|
|
814
|
+
: 'host' }))
|
|
815
|
+
for (const server of new Set(capabilities.map(item => item.mcpServer).filter(Boolean))) {
|
|
816
|
+
const id = `mcpServers.${server}`
|
|
817
|
+
const enclosingClosure = state ? effectiveCapabilitySettings(state.snapshot.settings).enabled && (state.snapshot.settings.policy
|
|
818
|
+
? state.snapshot.settings.policy.mcpServers[server]?.availability === 'closed'
|
|
819
|
+
: state.snapshot.settings.manual.overrides[id] === 'closed') : undefined
|
|
820
|
+
const effective = state === undefined ? 'pending'
|
|
821
|
+
: !planManaged ? 'host'
|
|
822
|
+
: state.snapshot.settings.policy
|
|
823
|
+
? capabilityRule(state.snapshot.settings.policy, { id, category: 'server', mcpServer: server }).availability
|
|
824
|
+
: enclosingClosure ? 'closed' : 'enabled'
|
|
825
|
+
entries.push({ id, category: 'server', name: server, description: `MCP server ${server}`,
|
|
826
|
+
source: { kind: 'mcp', server }, ...dimensions({ id, category: 'server', mcpServer: server }), saved: capabilityConfiguration(currentConfig).manual.overrides[id] ?? 'automatic',
|
|
827
|
+
sessionSourceSelection: sessionSourceSelection(id),
|
|
828
|
+
temporary: typeof state?.snapshot.sessionOverrides?.[id] === 'object' ? 'inherit' : state?.snapshot.sessionOverrides?.[id] ?? 'inherit',
|
|
829
|
+
enclosingClosure, effective })
|
|
830
|
+
}
|
|
831
|
+
return { management, entries }
|
|
832
|
+
},
|
|
833
|
+
async catalogFor(agent, signal) {
|
|
834
|
+
return (await this.catalogObservation(agent, signal)).entries
|
|
835
|
+
},
|
|
836
|
+
async change(agent, intent, signal, globalSettings) {
|
|
837
|
+
if (!canRefresh(agent)) throw new Error('ptc-plus: select an active PTC session')
|
|
838
|
+
const dimension = intent.dimension
|
|
839
|
+
const choices = dimension === 'availability' ? ['enabled', 'closed', 'automatic', 'inherit']
|
|
840
|
+
: dimension === 'disclosure' ? [...CAPABILITY_DISCLOSURES, 'automatic', 'inherit']
|
|
841
|
+
: dimension === undefined ? ['full', 'on-demand', 'closed', 'automatic', 'inherit'] : []
|
|
842
|
+
if (!['session', 'global'].includes(intent.scope) || !choices.includes(intent.selection)
|
|
843
|
+
|| intent.scope === 'global' && intent.selection === 'inherit') throw new TypeError('invalid capability change')
|
|
844
|
+
if (typeof agent.runMaintenance !== 'function') throw new Error('ptc-plus: capability changes require public agent maintenance')
|
|
845
|
+
return agent.runMaintenance(async maintenanceSignal => {
|
|
846
|
+
const combined = AbortSignal.any([signal, maintenanceSignal, agent.signal].filter(Boolean))
|
|
847
|
+
combined.throwIfAborted()
|
|
848
|
+
await prepare(agent, undefined, false, combined)
|
|
849
|
+
const assembly = await ctx.systemPrompt.assemble({ agent, scope: agent, signal: combined, ptcPlusCapabilityObservation: true })
|
|
850
|
+
const completeSettings = { ...capabilityConfiguration(currentConfig), enabled: false, manual: { baseline: [], overrides: {} } }
|
|
851
|
+
const available = await inventory(agent, assembly.sections, completeSettings, combined)
|
|
852
|
+
if (intent.id.startsWith('mcpServers.')) {
|
|
853
|
+
const server = intent.id.slice('mcpServers.'.length)
|
|
854
|
+
if (!available.some(item => item.mcpServer === server)) throw new Error('unknown MCP server in this session')
|
|
855
|
+
if (!dimension && !['closed', 'automatic', 'inherit'].includes(intent.selection)) throw new Error('MCP server choices are closed, automatic or inherit')
|
|
856
|
+
} else if (!available.some(item => item.id === intent.id)) throw new Error('unknown managed capability in this session')
|
|
857
|
+
const previous = stateFor(agent)?.snapshot
|
|
858
|
+
let sourceSettings = previous?.sourceSettings ?? previous?.settings ?? capabilityConfiguration(currentConfig)
|
|
859
|
+
if (dimension && !sourceSettings.policy) {
|
|
860
|
+
sourceSettings = capabilityConfiguration(capabilityProfileConfiguration(savedConfiguration(sourceSettings)))
|
|
861
|
+
}
|
|
862
|
+
if (!effectiveCapabilitySettings(sourceSettings).enabled || !effectiveCapabilitySettings(capabilityConfiguration(currentConfig)).enabled) throw new Error('ptc-plus: enable capability management before changing a session choice')
|
|
863
|
+
const sessionOverrides = { ...previous?.sessionOverrides }
|
|
864
|
+
let globalSaved = false
|
|
865
|
+
if (intent.scope === 'session') {
|
|
866
|
+
if (dimension || sourceSettings.policy) {
|
|
867
|
+
const patch = temporaryCapabilityRule(sessionOverrides[intent.id])
|
|
868
|
+
if (dimension) {
|
|
869
|
+
if (intent.selection === 'inherit') delete patch[dimension]
|
|
870
|
+
else patch[dimension] = intent.selection
|
|
871
|
+
} else if (intent.selection === 'inherit') {
|
|
872
|
+
delete patch.availability; delete patch.disclosure
|
|
873
|
+
} else if (intent.selection === 'closed') patch.availability = 'closed'
|
|
874
|
+
else Object.assign(patch, temporaryCapabilityRule(intent.selection))
|
|
875
|
+
if (Object.keys(patch).length) sessionOverrides[intent.id] = patch
|
|
876
|
+
else delete sessionOverrides[intent.id]
|
|
877
|
+
} else if (intent.selection === 'inherit') delete sessionOverrides[intent.id]
|
|
878
|
+
else sessionOverrides[intent.id] = intent.selection
|
|
879
|
+
} else {
|
|
880
|
+
if (!globalSettings?.read || !globalSettings?.update) throw new Error('ptc-plus: global capability changes require the public settings writer')
|
|
881
|
+
const before = globalSettings.read()
|
|
882
|
+
if (!before || !Number.isSafeInteger(before.revision)) throw new Error('ptc-plus: global capability changes require settings revision evidence')
|
|
883
|
+
if (!effectiveCapabilitySettings(capabilityConfiguration({ ...currentConfig, ...before.value })).enabled) throw new Error('ptc-plus: enable capability management before changing a global choice')
|
|
884
|
+
const overrides = JSON.parse(before.value.capabilityOverrides ?? '{}')
|
|
885
|
+
if (intent.selection === 'automatic') delete overrides[intent.id]
|
|
886
|
+
else overrides[intent.id] = intent.selection
|
|
887
|
+
combined.throwIfAborted()
|
|
888
|
+
const policy = capabilityPolicyForConfig(before.value)
|
|
889
|
+
let nextPolicy = dimension ? changeCapabilityRule(policy, intent.id, dimension, intent.selection)
|
|
890
|
+
: changeCapabilityRule(policy, intent.id, 'availability', intent.selection === 'closed' ? 'closed'
|
|
891
|
+
: intent.selection === 'automatic' ? 'automatic' : 'enabled')
|
|
892
|
+
if (!dimension) nextPolicy = intent.selection === 'automatic' ? changeCapabilityRule(nextPolicy, intent.id, 'disclosure', 'automatic')
|
|
893
|
+
: intent.selection === 'closed' ? nextPolicy : changeCapabilityRule(nextPolicy, intent.id, 'disclosure', intent.selection)
|
|
894
|
+
const patch = before.value.capabilityPolicy || dimension
|
|
895
|
+
? { capabilityPolicy: JSON.stringify(nextPolicy), ...(!dimension ? { capabilityOverrides: JSON.stringify(overrides) } : {}) }
|
|
896
|
+
: { capabilityOverrides: JSON.stringify(overrides) }
|
|
897
|
+
await globalSettings.update(patch, before.revision)
|
|
898
|
+
globalSaved = true
|
|
899
|
+
}
|
|
900
|
+
try {
|
|
901
|
+
if (globalSaved) {
|
|
902
|
+
const saved = globalSettings.read()
|
|
903
|
+
if (!saved?.value || !Number.isSafeInteger(saved.revision)) throw new Error('ptc-plus: saved settings could not be confirmed for this session')
|
|
904
|
+
sourceSettings = capabilityConfiguration({ ...currentConfig, ...saved.value })
|
|
905
|
+
if (dimension) {
|
|
906
|
+
const retained = temporaryCapabilityRule(sessionOverrides[intent.id]); delete retained[dimension]
|
|
907
|
+
if (Object.keys(retained).length) sessionOverrides[intent.id] = retained
|
|
908
|
+
else delete sessionOverrides[intent.id]
|
|
909
|
+
} else delete sessionOverrides[intent.id]
|
|
910
|
+
}
|
|
911
|
+
combined.throwIfAborted()
|
|
912
|
+
if (!effectiveCapabilitySettings(sourceSettings).enabled) throw new Error('ptc-plus: enable capability management before changing a session choice')
|
|
913
|
+
await prepare(agent, assembly.sections, true, combined, { sourceSettings, sessionOverrides, commandId: intent.commandId })
|
|
914
|
+
return { globalSaved, sessionApplied: true, publication: planCommitted(agent, stateFor(agent).message) ? 'accepted' : 'pending' }
|
|
915
|
+
} catch (error) {
|
|
916
|
+
if (globalSaved) return { globalSaved: true, sessionApplied: false, error: String(error?.message ?? error) }
|
|
917
|
+
throw error
|
|
918
|
+
}
|
|
919
|
+
})
|
|
920
|
+
},
|
|
921
|
+
pendingChoiceMessage(agent) {
|
|
922
|
+
const state = stateFor(agent)
|
|
923
|
+
return ['ptc-capability-environment/v2', 'ptc-capability-environment/v3', 'ptc-capability-environment/v4'].includes(state?.snapshot.schema) && !planCommitted(agent, state.message) ? state.message : undefined
|
|
924
|
+
},
|
|
925
|
+
projectSections(agent, sections) {
|
|
926
|
+
const state = stateFor(agent)
|
|
927
|
+
if (!state?.snapshot.plan.enabled) return sections
|
|
928
|
+
const inventory = [...declaredCapabilities(state).values()]
|
|
929
|
+
const retained = inventory.filter(item => declaredSelection(state, item.id) === 'host' && supplied(state, item.id))
|
|
930
|
+
const retainedSections = new Set(retained.flatMap(item => item.dependencies ?? []).map(item => item.id))
|
|
931
|
+
const ownedSections = new Set(inventory.flatMap(item => item.dependencies ?? []).map(item => item.id))
|
|
932
|
+
return sections.flatMap(section => {
|
|
933
|
+
if (section.name === 'mcp-resource-servers') {
|
|
934
|
+
const servers = retained.filter(item => item.category === 'resource').map(item => item.selector.server)
|
|
935
|
+
return servers.length ? [{ ...section, text: `Use the following MCP resource servers as the server argument: ${JSON.stringify(servers)}.` }] : []
|
|
936
|
+
}
|
|
937
|
+
if (ownedSections.has(section.name) && !retainedSections.has(section.name)) return []
|
|
938
|
+
if (section.name?.startsWith('tool:') && !TRANSPORTS.has(section.name.slice(5))
|
|
939
|
+
&& declaredSelection(state, `tools.${section.name.slice(5)}`) !== 'host') return []
|
|
940
|
+
return [section]
|
|
941
|
+
})
|
|
942
|
+
},
|
|
943
|
+
sdk(agent, nativeSdk) {
|
|
944
|
+
const state = stateFor(agent)
|
|
945
|
+
if (!state?.snapshot.plan.enabled) return nativeSdk
|
|
946
|
+
const candidates = toolSchemasForAgent(agent)
|
|
947
|
+
const schemas = candidates.filter(schema => !TRANSPORTS.has(schema.name))
|
|
948
|
+
const host = schemas.filter(schema => declaredSelection(state, `tools.${schema.name}`) === 'host'
|
|
949
|
+
&& memberSupplied(state, schema.name))
|
|
950
|
+
if (host.length === schemas.length) return nativeSdk
|
|
951
|
+
// The edit transport is callable inside every program and is not part of
|
|
952
|
+
// the managed disclosure, so its Host schema stays declared in every
|
|
953
|
+
// managed render. The direct transport is never an SDK member.
|
|
954
|
+
const editTransport = candidates.find(schema => schema.name === EDIT_TRANSPORT)
|
|
955
|
+
return renderToolsSdk(editTransport === undefined ? host : [...host, editTransport])
|
|
956
|
+
},
|
|
957
|
+
projectMembers(agent, members) {
|
|
958
|
+
const state = stateFor(agent)
|
|
959
|
+
if (!state?.snapshot.plan.enabled) return members
|
|
960
|
+
return members.filter(name => memberSupplied(state, name))
|
|
961
|
+
},
|
|
962
|
+
metadata(agent) {
|
|
963
|
+
const state = stateFor(agent)
|
|
964
|
+
if (!state?.snapshot.plan.enabled) return []
|
|
965
|
+
const capabilities = declaredCapabilities(state)
|
|
966
|
+
return ['skill', 'resource'].map(category => ({ namespace: category === 'skill' ? 'skills' : 'mcp',
|
|
967
|
+
members: [...capabilities.values()].filter(item => item.category === category && supplied(state, item.id))
|
|
968
|
+
.map(item => state.liveCapabilities?.get(item.id) ?? item)
|
|
969
|
+
.map(item => ({ name: item.id.slice(item.id.indexOf('.') + 1), description: item.description,
|
|
970
|
+
contract: item.contract, dependencies: item.dependencies ?? [], revision: item.revision, parameters: {}, returns: {},
|
|
971
|
+
...(item.invocation === undefined ? {} : { invocation: item.invocation }),
|
|
972
|
+
...(item.nativeInvocation === undefined ? {} : { nativeInvocation: item.nativeInvocation }),
|
|
973
|
+
completeness: 'complete', effect: 'unknown', replay: 'recorded-value' })) }))
|
|
974
|
+
},
|
|
975
|
+
adaptFunction(agent, name, original) {
|
|
976
|
+
return (...args) => {
|
|
977
|
+
validateSupply(agent, name, args[0])
|
|
978
|
+
return original(...args)
|
|
979
|
+
}
|
|
980
|
+
},
|
|
981
|
+
explored(agent, result) {
|
|
982
|
+
const state = stateFor(agent)
|
|
983
|
+
for (const item of result?.matches ?? result?.symbols ?? []) {
|
|
984
|
+
const id = item.symbol ?? `${item.namespace}.${item.name}`
|
|
985
|
+
if (liveCapability(state, id) === undefined || !supplied(state, id)) continue
|
|
986
|
+
state.expanded.add(id)
|
|
987
|
+
state?.pendingExpanded?.add(id)
|
|
988
|
+
}
|
|
989
|
+
},
|
|
990
|
+
async preStep(payload, next) {
|
|
991
|
+
if (payload.signal?.aborted) return next()
|
|
992
|
+
// Preparation precedes the host's explicit Skill loading, using the same
|
|
993
|
+
// public composition entry that owns PTC/native selection and MCP sections.
|
|
994
|
+
if (ctx.systemPrompt?.assemble) {
|
|
995
|
+
await ctx.systemPrompt.assemble({ agent: payload.agent, scope: payload.agent, signal: payload.signal })
|
|
996
|
+
}
|
|
997
|
+
const prepared = stateFor(payload.agent)
|
|
998
|
+
if (prepared) await qualifyCapabilities(payload.agent, prepared, payload.signal)
|
|
999
|
+
const decision = await next()
|
|
1000
|
+
if (decision.kind !== 'enter' || payload.signal?.aborted) return decision
|
|
1001
|
+
await observeHistory(payload.agent, payload.signal)
|
|
1002
|
+
await reconcilePendingChoice(payload.agent, payload.signal, true)
|
|
1003
|
+
const state = stateFor(payload.agent)
|
|
1004
|
+
if (decision.kind !== 'enter' || !state || payload.signal?.aborted) return decision
|
|
1005
|
+
await statisticsOperation(() => store?.available(payload.agent, statisticsSnapshot(state)))
|
|
1006
|
+
restoreEvidence(payload.agent, state)
|
|
1007
|
+
const visible = payload.agent.session.deriveMessages?.() ?? []
|
|
1008
|
+
const hostSkills = state.snapshot.settings.policy && state.snapshot.capabilities.filter(item => item.category === 'skill' && supplied(state, item.id))
|
|
1009
|
+
.every(item => selection(state, item.id) === 'host' && item.nativeInvocation === undefined)
|
|
1010
|
+
const messages = state.snapshot.plan.enabled ? decision.messages.filter(message =>
|
|
1011
|
+
(message.source?.kind !== 'skill-catalog' || hostSkills)
|
|
1012
|
+
&& !(message.source?.kind === 'skill-invocation' && !supplied(state, `skills.${message.source.name}`))) : decision.messages
|
|
1013
|
+
// An uncommitted proposal enters once through Host acceptance. A committed
|
|
1014
|
+
// plan is already in history; its ancestry cannot resurrect compacted text.
|
|
1015
|
+
const planVisible = state.message && visible.some(message => message.id === state.message.id
|
|
1016
|
+
&& readCapabilityContextMessage(message, String(payload.agent.session.id ?? payload.agent.id))?.form === CAPABILITY_PLAN_FORM)
|
|
1017
|
+
const additions = state.message && !planCommitted(payload.agent, state.message)
|
|
1018
|
+
&& !messages.some(message => message.id === state.message.id) ? [state.message] : []
|
|
1019
|
+
const planSupplied = planVisible || [...messages, ...additions].some(message => message.id === state.message?.id)
|
|
1020
|
+
if (state.snapshot.plan.enabled || state.snapshot.settings.policy && effectiveCapabilitySettings(state.snapshot.settings).recoveryEnabled) {
|
|
1021
|
+
const planDigest = digest(state.snapshot)
|
|
1022
|
+
const sessionId = String(payload.agent.session.id ?? payload.agent.id)
|
|
1023
|
+
const recordedUpdate = [...visible, ...messages].map(message => readContractUpdate(message, planDigest, sessionId))
|
|
1024
|
+
.filter(Boolean).at(-1)
|
|
1025
|
+
const selected = state.snapshot.plan.entries.filter(entry => ['full', 'on-demand', 'host'].includes(entry.selection))
|
|
1026
|
+
const preceding = new Map(recordedUpdate?.views ?? selected.map(entry => [entry.id,
|
|
1027
|
+
callingRevision(state.snapshot.capabilities.find(item => item.id === entry.id))]))
|
|
1028
|
+
const views = selected.map(entry => [entry.id, callingRevision(state.liveCapabilities.get(entry.id))])
|
|
1029
|
+
const changes = selected.filter(entry => preceding.get(entry.id) !== callingRevision(state.liveCapabilities.get(entry.id)))
|
|
1030
|
+
.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) }))
|
|
1031
|
+
if (changes.length) {
|
|
1032
|
+
const livePlan = createCapabilityPlan({ ...state.snapshot.settings, policy: undefined, capabilities: [...state.liveCapabilities.values()]
|
|
1033
|
+
.filter(item => state.snapshot.capabilities.some(original => original.id === item.id)),
|
|
1034
|
+
manual: { baseline: [], overrides: Object.fromEntries(state.snapshot.plan.entries.map(entry => [entry.id, entry.selection === 'host' ? 'full' : entry.selection])) },
|
|
1035
|
+
navigationText: NAVIGATION, configurationRevision: state.snapshot.plan.configurationRevision })
|
|
1036
|
+
additions.push(contractUpdateMessage({ schema: 'ptc-capability-contracts/v1', planDigest, views, changes,
|
|
1037
|
+
overageBytes: livePlan.budgets.overageBytes }, sessionId))
|
|
1038
|
+
}
|
|
1039
|
+
const expandedVisible = new Set([...visible, ...messages].flatMap(message => message.source?.form === 'capability-descriptions'
|
|
1040
|
+
? message.source.entries ?? [] : state.visibleExpansions?.get(message.id) ?? []))
|
|
1041
|
+
for (const entry of selected.filter(entry => entry.selection === 'host')) {
|
|
1042
|
+
if (state.liveCapabilities.has(entry.id)) expandedVisible.add(entry.id)
|
|
1043
|
+
}
|
|
1044
|
+
for (const message of [...visible, ...messages, ...additions]) {
|
|
1045
|
+
const update = readContractUpdate(message, planDigest, sessionId)
|
|
1046
|
+
for (const change of update?.changes ?? []) {
|
|
1047
|
+
if (change.disclosure === 'full' && change.capability !== null
|
|
1048
|
+
&& callingRevision(change.capability) === callingRevision(state.liveCapabilities.get(change.id))) expandedVisible.add(change.id)
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
if (planSupplied) {
|
|
1052
|
+
for (const entry of state.snapshot.plan.entries.filter(entry => entry.selection === 'full')) {
|
|
1053
|
+
if (callingRevision(state.snapshot.capabilities.find(item => item.id === entry.id))
|
|
1054
|
+
=== callingRevision(state.liveCapabilities.get(entry.id))) expandedVisible.add(entry.id)
|
|
1055
|
+
if (!state.expanded.has(entry.id)) state.pendingExpanded.add(entry.id)
|
|
1056
|
+
state.expanded.add(entry.id)
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
if (state.snapshot.plan.directoryText && !planSupplied && ![...visible, ...messages].some(message => message.source?.form === 'capability-navigation')) {
|
|
1060
|
+
additions.push(createUserMessage({ source: { kind: PTC_MESSAGE_SOURCE_KIND, form: 'capability-navigation' },
|
|
1061
|
+
content: [{ type: 'text', text: state.snapshot.plan.directoryText || NAVIGATION }] }))
|
|
1062
|
+
}
|
|
1063
|
+
let body = ''
|
|
1064
|
+
const restored = []
|
|
1065
|
+
const dependencies = new Set()
|
|
1066
|
+
const currentSchemas = new Map(toolSchemasForAgent(payload.agent).map(schema => [`tools.${schema.name}`, digest(schema)]))
|
|
1067
|
+
for (const id of (state.snapshot.settings.policy && !effectiveCapabilitySettings(state.snapshot.settings).recoveryEnabled ? [] : [...state.used.keys()].reverse())) {
|
|
1068
|
+
const capability = state.liveCapabilities.get(id)
|
|
1069
|
+
if (!capability || capability.category === 'skill' || !state.expanded.has(id) || expandedVisible.has(id) || !supplied(state, id)) continue
|
|
1070
|
+
if (capability.category === 'tool' && currentSchemas.get(id) !== capability.revision) continue
|
|
1071
|
+
const missing = (capability.dependencies ?? []).filter(item => !dependencies.has(item.id))
|
|
1072
|
+
const contract = [...missing.map(item => `### dependency:${item.id}\n${item.text}`), `### ${id}\n${capability.contract}`].join('\n\n')
|
|
1073
|
+
if (Buffer.byteLength(body + '\n\n' + contract) > state.snapshot.settings.recoveryBytes) continue
|
|
1074
|
+
body += `${body ? '\n\n' : ''}${contract}`
|
|
1075
|
+
restored.push(id)
|
|
1076
|
+
for (const item of missing) dependencies.add(item.id)
|
|
1077
|
+
}
|
|
1078
|
+
if (body) additions.push(createUserMessage({ source: { kind: PTC_MESSAGE_SOURCE_KIND, form: 'capability-descriptions', entries: restored },
|
|
1079
|
+
content: [{ type: 'text', text: body }] }))
|
|
1080
|
+
}
|
|
1081
|
+
for (const message of messages) {
|
|
1082
|
+
if (message.source?.kind === 'skill-invocation' && supplied(state, `skills.${message.source.name}`)) {
|
|
1083
|
+
const observedSnapshot = statisticsSnapshot(state)
|
|
1084
|
+
void statisticsOperation(() => store?.used(payload.agent, observedSnapshot,
|
|
1085
|
+
`skills.${message.source.name}`, 'skill-body-accepted', 'complete'))
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
// Before task history exists, the initial plan precedes admitted task messages.
|
|
1089
|
+
// Refresh and recovery append after committed history; they never reorder it.
|
|
1090
|
+
const initial = !sessionEvents(payload.agent.session).some(event => ['user/message', 'assistant/message', 'tool/result'].includes(event.type))
|
|
1091
|
+
return { ...decision, messages: initial ? [...additions, ...messages] : [...messages, ...additions] }
|
|
1092
|
+
},
|
|
1093
|
+
admitted(exec) {
|
|
1094
|
+
// Host tokens correlate immutable call identity through mutable wrappers.
|
|
1095
|
+
const state = stateFor(exec.agent)
|
|
1096
|
+
if (exec.token && (state && requiresCapabilityInventory(state.snapshot.settings))) admitted.set(exec.token, statisticsSnapshot(state))
|
|
1097
|
+
},
|
|
1098
|
+
dispatched(exec, result) {
|
|
1099
|
+
const state = stateFor(exec.agent)
|
|
1100
|
+
const observation = exec.token && admitted.get(exec.token)
|
|
1101
|
+
if (!observation || !state || !observation.plan.entries.some(entry => entry.id === `tools.${exec.name}`)
|
|
1102
|
+
|| result?.error?.info?.code === 'ABORTED_BEFORE_DISPATCH' || result?.error?.info?.code === 'UNKNOWN_TOOL') return
|
|
1103
|
+
admitted.delete(exec.token)
|
|
1104
|
+
state.used.delete(`tools.${exec.name}`)
|
|
1105
|
+
state.used.set(`tools.${exec.name}`, true)
|
|
1106
|
+
void statisticsOperation(() => store?.used(exec.agent, observation, `tools.${exec.name}`, 'host-execution-result', result?.error?.info?.code === 'ABORTED' ? 'cancelled' : result?.isError ? 'failed' : 'complete'))
|
|
1107
|
+
if (exec.name === 'skill' && !result?.isError && typeof exec.arguments?.name === 'string') {
|
|
1108
|
+
void statisticsOperation(() => store?.used(exec.agent, observation, `skills.${exec.arguments.name}`, 'skill-body-accepted', 'complete'))
|
|
1109
|
+
}
|
|
1110
|
+
if (exec.name === 'read_mcp_resource' && !result?.isError && typeof exec.arguments?.server === 'string') {
|
|
1111
|
+
const id = `mcp.${exec.arguments.server}`
|
|
1112
|
+
state.used.delete(id)
|
|
1113
|
+
state.used.set(id, true)
|
|
1114
|
+
void statisticsOperation(() => store?.used(exec.agent, observation, `mcp.${exec.arguments.server}`, 'resource-access', 'complete'))
|
|
1115
|
+
}
|
|
1116
|
+
},
|
|
1117
|
+
toolMetadata(agent, metadata, schemas = toolSchemasForAgent(agent)) {
|
|
1118
|
+
const state = stateFor(agent)
|
|
1119
|
+
if (!state || !requiresCapabilityInventory(state.snapshot.settings)) return metadata
|
|
1120
|
+
const capabilities = new Map(schemas.map(schema => {
|
|
1121
|
+
const item = toolCapability(schema, state.hostSections)
|
|
1122
|
+
return [item.id, item]
|
|
1123
|
+
}))
|
|
1124
|
+
return metadata.map(entry => ({ ...entry, members: entry.members.map(member => {
|
|
1125
|
+
const item = capabilities.get(`${entry.namespace}.${member.name}`)
|
|
1126
|
+
return item ? { ...member, contract: item.contract, dependencies: item.dependencies ?? [], revision: item.revision } : member
|
|
1127
|
+
}) }))
|
|
1128
|
+
},
|
|
1129
|
+
complete(exec, result) {
|
|
1130
|
+
const state = stateFor(exec.agent)
|
|
1131
|
+
if (!state || !requiresCapabilityInventory(state.snapshot.settings)) return result
|
|
1132
|
+
if (!completions.has(exec)) {
|
|
1133
|
+
completions.set(exec, { schema: 'ptc-capability-evidence/v1', planDigest: digest(state.snapshot),
|
|
1134
|
+
expanded: [...state.pendingExpanded ?? []], used: [...state.used.keys()] })
|
|
1135
|
+
state.pendingExpanded?.clear()
|
|
1136
|
+
}
|
|
1137
|
+
return { ...result, meta: { ...result.meta, [EVIDENCE_KEY]: completions.get(exec) } }
|
|
1138
|
+
},
|
|
1139
|
+
async postExecute(exec, result, next) {
|
|
1140
|
+
const policy = await previewPolicy(exec.agent)
|
|
1141
|
+
return policy ? policy.waterfall('tools/post-execute', exec, result, next) : next()
|
|
1142
|
+
},
|
|
1143
|
+
async dispatchLog(dispatch, next) {
|
|
1144
|
+
const policy = await previewPolicy(dispatch.exec.agent)
|
|
1145
|
+
return policy ? policy.waterfall('tools/ptc-dispatch-log', dispatch, next) : next()
|
|
1146
|
+
},
|
|
1147
|
+
status(agent, status) {
|
|
1148
|
+
const familyId = store?.family(agent)
|
|
1149
|
+
for (const member of ctx.agents?.list?.() ?? [agent]) {
|
|
1150
|
+
if (member !== agent && store?.family(member) !== familyId) continue
|
|
1151
|
+
const state = stateFor(member)
|
|
1152
|
+
if (state) void statisticsOperation(() => store?.status(member, state.snapshot,
|
|
1153
|
+
member === agent ? status : member.status))
|
|
1154
|
+
}
|
|
1155
|
+
},
|
|
1156
|
+
describe(agent) {
|
|
1157
|
+
const state = stateFor(agent)
|
|
1158
|
+
return state === undefined ? { configured: capabilityConfiguration(currentConfig), effective: null }
|
|
1159
|
+
: { configured: capabilityConfiguration(currentConfig), effective: state.snapshot.plan,
|
|
1160
|
+
publication: state.message === undefined ? 'not-required' : planCommitted(agent, state.message) ? 'accepted' : 'pending',
|
|
1161
|
+
sourceSettings: state.snapshot.sourceSettings ?? state.snapshot.settings,
|
|
1162
|
+
sessionOverrides: state.snapshot.sessionOverrides ?? {},
|
|
1163
|
+
pendingRefresh: digest(capabilityConfiguration(currentConfig)) !== digest(state.snapshot.sourceSettings ?? state.snapshot.settings) }
|
|
1164
|
+
},
|
|
1165
|
+
describeSessions() {
|
|
1166
|
+
return [...sessions].map(([sessionId, state]) => ({ sessionId, effective: state.snapshot.plan,
|
|
1167
|
+
publication: state.message === undefined ? 'not-required' : planCommitted({ session: state.session }, state.message) ? 'accepted' : 'pending',
|
|
1168
|
+
configuration: savedConfiguration(state.snapshot.sourceSettings ?? state.snapshot.settings),
|
|
1169
|
+
sessionOverrides: state.snapshot.sessionOverrides ?? {},
|
|
1170
|
+
pendingRefresh: digest(capabilityConfiguration(currentConfig)) !== digest(state.snapshot.sourceSettings ?? state.snapshot.settings) }))
|
|
1171
|
+
},
|
|
1172
|
+
canRefresh,
|
|
1173
|
+
describeTargets() {
|
|
1174
|
+
return (ctx.agents?.list?.() ?? []).filter(canRefresh)
|
|
1175
|
+
.map(agent => ({ sessionId: String(agent.session.id ?? agent.id) }))
|
|
1176
|
+
},
|
|
1177
|
+
async describeSessionCatalog({ sessionId } = {}, signal) {
|
|
1178
|
+
signal?.throwIfAborted()
|
|
1179
|
+
if (typeof sessionId !== 'string' || !sessionId) throw new TypeError('ptc-plus: select an active PTC session')
|
|
1180
|
+
const agent = (ctx.agents?.list?.() ?? []).find(candidate =>
|
|
1181
|
+
String(candidate.session.id ?? candidate.id) === sessionId && canRefresh(candidate))
|
|
1182
|
+
if (!agent) throw new TypeError('ptc-plus: select an active PTC session')
|
|
1183
|
+
const { management, entries } = await this.catalogObservation(agent, signal)
|
|
1184
|
+
signal?.throwIfAborted()
|
|
1185
|
+
if (!(ctx.agents?.list?.() ?? []).includes(agent) || !canRefresh(agent)
|
|
1186
|
+
|| String(agent.session.id ?? agent.id) !== sessionId) {
|
|
1187
|
+
throw new Error('ptc-plus: capability observation target is no longer available')
|
|
1188
|
+
}
|
|
1189
|
+
return { schema: 'ptc-capability-session-catalog/v1', management, entries }
|
|
1190
|
+
},
|
|
1191
|
+
async observeAllocation({ sessionId, configuration } = {}, signal) {
|
|
1192
|
+
signal?.throwIfAborted()
|
|
1193
|
+
const agent = (ctx.agents?.list?.() ?? []).find(agent => String(agent.session.id ?? agent.id) === sessionId && canRefresh(agent))
|
|
1194
|
+
if (!agent) throw new TypeError('ptc-plus: select an active PTC session to observe its capability inventory and workspace statistics')
|
|
1195
|
+
const raw = configuration === undefined ? savedConfiguration(capabilityConfiguration(currentConfig)) : configuration
|
|
1196
|
+
const validated = capabilityProfileConfiguration(raw, { allowInvalidStatistics: true })
|
|
1197
|
+
const settings = capabilityConfiguration(validated)
|
|
1198
|
+
const assembly = await ctx.systemPrompt.assemble({ agent, scope: agent, signal, ptcPlusCapabilityObservation: true })
|
|
1199
|
+
const capabilities = await inventory(agent, assembly.sections, settings, signal)
|
|
1200
|
+
const coverage = Number.isSafeInteger(settings.statistics?.windowSize) && settings.statistics.windowSize > 0
|
|
1201
|
+
? await store?.coverage(agent, capabilities, settings.statistics) ?? null : null
|
|
1202
|
+
signal?.throwIfAborted()
|
|
1203
|
+
if (!(ctx.agents?.list?.() ?? []).includes(agent) || !canRefresh(agent)) throw new Error('ptc-plus: capability observation target is no longer available')
|
|
1204
|
+
const plan = createCapabilityPlan({ ...settings, capabilities, coverage, navigationText: NAVIGATION,
|
|
1205
|
+
configurationRevision: digest(settings) })
|
|
1206
|
+
return { sessionId, workspace: agent.session.header.cwd, configuration: validated, plan, coverage,
|
|
1207
|
+
capabilities: capabilities.map(item => ({ id: item.id, revision: item.revision, category: item.category,
|
|
1208
|
+
contractBytes: Buffer.byteLength(item.contract, 'utf8') })) }
|
|
1209
|
+
},
|
|
1210
|
+
async describeCatalog(signal) {
|
|
1211
|
+
const available = new Map()
|
|
1212
|
+
for (const agent of ctx.agents?.list?.() ?? []) {
|
|
1213
|
+
signal?.throwIfAborted()
|
|
1214
|
+
const state = stateFor(agent)
|
|
1215
|
+
if (!state && !modelSchemasForAgent(agent).some(schema => schema.name === 'run_code')) continue
|
|
1216
|
+
const hostSections = ctx.systemPrompt?.assemble
|
|
1217
|
+
? (await ctx.systemPrompt.assemble({ agent, scope: agent, signal, ptcPlusCapabilityObservation: true })).sections
|
|
1218
|
+
: state?.hostSections ?? []
|
|
1219
|
+
const presentation = new Map()
|
|
1220
|
+
for (const capability of await inventory(agent, hostSections, capabilityConfiguration(currentConfig), signal, presentation)) {
|
|
1221
|
+
const display = presentation.get(capability.id)
|
|
1222
|
+
const previous = available.get(capability.id)
|
|
1223
|
+
available.set(capability.id, { id: capability.id, category: capability.category, description: capability.description,
|
|
1224
|
+
...display, ...(capability.mcpServer === undefined ? {} : { mcpServer: capability.mcpServer }), ...(previous && JSON.stringify(previous.source) !== JSON.stringify(display?.source)
|
|
1225
|
+
? { source: { kind: 'unknown' }, mcpServer: undefined } : {}),
|
|
1226
|
+
...(previous && (previous.invocationConflict || JSON.stringify(previous.invocation) !== JSON.stringify(display?.invocation))
|
|
1227
|
+
? { invocation: undefined, invocationConflict: true } : {}) })
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
signal?.throwIfAborted()
|
|
1231
|
+
return [...available.values()].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
|
|
1232
|
+
},
|
|
1233
|
+
async disposeAgent(agent) {
|
|
1234
|
+
const projection = skillProjections.get(agent)
|
|
1235
|
+
skillProjections.delete(agent)
|
|
1236
|
+
await projection?.scope.dispose()
|
|
1237
|
+
sessions.delete(String(agent?.session?.id ?? agent?.id))
|
|
1238
|
+
},
|
|
1239
|
+
async dispose() {
|
|
1240
|
+
for (const projection of skillProjections.values()) await projection.scope.dispose()
|
|
1241
|
+
skillProjections.clear()
|
|
1242
|
+
for (const policy of previewPolicies.values()) await policy.fiber.dispose()
|
|
1243
|
+
previewPolicies.clear()
|
|
1244
|
+
sessions.clear()
|
|
1245
|
+
},
|
|
1246
|
+
})
|
|
1247
|
+
}
|