dsh-ptc-plus 0.5.3 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/client.cjs +3326 -2881
- package/compiler-core.cjs +127 -127
- package/compiler-core.cjs.map +2 -2
- package/docs/README.md +1 -0
- package/docs/capability-planning-components.md +6 -6
- package/docs/capability-projection.md +1 -1
- package/docs/capability-surface.md +133 -0
- package/docs/client-ui.md +17 -15
- package/docs/client-visual-contract.md +7 -7
- package/docs/installation.md +9 -2
- package/docs/panel-concept.md +171 -0
- package/docs/releases/v0.5.4.md +75 -0
- package/docs/runtime-reference.md +14 -0
- package/docs/verification.md +1 -1
- package/index.js +117 -30
- package/internal/binding-adoption.js +103 -11
- package/internal/capability-command-receipt.js +99 -15
- package/internal/capability-environment.js +1108 -348
- package/internal/capability-plan.js +18 -2
- package/internal/capability-policy.js +13 -0
- package/internal/capability-selection.js +330 -0
- package/internal/direct-surface-owner.js +17 -4
- package/internal/message-sources.js +21 -0
- package/internal/program-bindings.js +13 -4
- package/internal/runtime-bridge-owner.js +5 -1
- package/internal/runtime-config.js +10 -0
- package/internal/session-log-view.js +47 -5
- package/internal/settings-compat.js +6 -0
- package/internal/user-bindings-owner.js +20 -3
- package/package.json +45 -31
- package/src/client-authoring-view.js +7 -8
- package/src/client-capabilities-view.js +871 -948
- package/src/client-capability-sort.js +22 -9
- package/src/client-copy.js +116 -294
- package/src/client-settings-view.js +90 -33
- package/src/client-styles.js +67 -19
- package/src/client.js +46 -21
- package/internal/capability-delivery-proof.js +0 -70
|
@@ -83,11 +83,19 @@ function createLegacyCapabilityPlan(input) {
|
|
|
83
83
|
if (!Array.isArray(options.capabilities)) throw new TypeError('ptc-plus: capabilities must be an array')
|
|
84
84
|
const ids = new Set()
|
|
85
85
|
const excluded = []
|
|
86
|
+
// Ids whose own invocation path the inventory could not reach. The planner states
|
|
87
|
+
// availability from the recorded choices, so a row the inventory already proved
|
|
88
|
+
// unreachable is closed here too — otherwise a baseline full contract keeps instructing
|
|
89
|
+
// a call the composition does not deliver. The policy generation states the same fact as
|
|
90
|
+
// `availabilitySource: 'invocation-path'`; a recorded v1/v2 inventory carries no such
|
|
91
|
+
// field, so its recomposition is unchanged.
|
|
92
|
+
const pathClosed = new Set()
|
|
86
93
|
const capabilities = options.capabilities.map(raw => {
|
|
87
94
|
const value = record(raw, 'capability')
|
|
88
95
|
const id = text(value.id, 'capability id')
|
|
89
96
|
if (ids.has(id)) throw new TypeError(`ptc-plus: duplicate capability identity ${id}`)
|
|
90
97
|
ids.add(id)
|
|
98
|
+
if (value.effectiveAvailability === 'closed') pathClosed.add(id)
|
|
91
99
|
const revision = text(value.revision, 'capability revision')
|
|
92
100
|
if (EXCLUDED_CATEGORIES.has(value.category)) {
|
|
93
101
|
excluded.push({ id, revision, category: value.category })
|
|
@@ -148,8 +156,10 @@ function createLegacyCapabilityPlan(input) {
|
|
|
148
156
|
throw new TypeError(`ptc-plus: cannot determine the MCP server owner of ${capability.id}; conflicting namespaces: ${capability.mcpServerCandidates.join(', ')}`)
|
|
149
157
|
}
|
|
150
158
|
const serverClosed = capability.mcpServer !== undefined && overrides[`mcpServers.${capability.mcpServer}`] === 'closed'
|
|
151
|
-
const
|
|
152
|
-
const
|
|
159
|
+
const unreachable = pathClosed.has(capability.id)
|
|
160
|
+
const explicit = serverClosed || unreachable ? 'closed' : Object.hasOwn(overrides, capability.id) ? overrides[capability.id] : undefined
|
|
161
|
+
const entry = { id: capability.id, revision: capability.revision, category: capability.category, selection: explicit ?? 'on-demand',
|
|
162
|
+
reason: serverClosed ? 'mcp-server-disabled' : unreachable ? 'invocation-path' : explicit ? 'manual-override' : 'baseline-not-selected' }
|
|
153
163
|
entries.push(entry)
|
|
154
164
|
selections.set(capability.id, entry)
|
|
155
165
|
if (explicit === 'full') full.push(capability)
|
|
@@ -235,6 +245,12 @@ function createLegacyCapabilityPlan(input) {
|
|
|
235
245
|
schema, enabled: true, configurationRevision, entries,
|
|
236
246
|
excluded: excluded.sort(compareIds), catalogText, directoryText, directoryCategories,
|
|
237
247
|
budgets: { unit: 'utf8-bytes', budgetBytes, directoryBudgetBytes, contractBytes: bytes(catalogText), directoryBytes: bytes(directoryText), overageBytes: Math.max(0, bytes(catalogText) - budgetBytes) },
|
|
248
|
+
// Configured ids this composition resolved no definition for. The composition is built
|
|
249
|
+
// from the step's admission, so a configured capability that step does not deliver is
|
|
250
|
+
// listed here beside one that no longer exists at all — the field states what this
|
|
251
|
+
// plan could not resolve, not what this session may call, and no model-visible text is
|
|
252
|
+
// rendered from it. Reading it as the capability list would name a closed capability
|
|
253
|
+
// as available and an absent one as configured.
|
|
238
254
|
unresolvedConfigurationIds: [...new Set([...baseline, ...Object.keys(overrides)])].filter(id => !capabilities.some(entry => entry.id === id)).sort(),
|
|
239
255
|
statisticsStatus: settings.usable ? 'enabled' : settings.reason,
|
|
240
256
|
})
|
|
@@ -82,6 +82,19 @@ export function capabilityRule(policy, capability) {
|
|
|
82
82
|
return { availability, disclosure, availabilitySource, disclosureSource }
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/** The policy document a record's own words make.
|
|
86
|
+
*
|
|
87
|
+
* A configuration that saved no policy has only a synthesized document, whose bottom layer is
|
|
88
|
+
* shaped by switches that belong to no record. Editing a rule, or writing one saved cell, must
|
|
89
|
+
* start from the record's own layers instead: the declared defaults plus the layers the record
|
|
90
|
+
* wrote (the legacy override record and the usage-statistics switch `capabilityPolicyForConfig`
|
|
91
|
+
* folds into the allocation).
|
|
92
|
+
*/
|
|
93
|
+
export function capabilityPolicyFromRecord(config) {
|
|
94
|
+
return capabilityPolicyForConfig({ ...config, capabilityPolicy: undefined,
|
|
95
|
+
capabilityManagementEnabled: false })
|
|
96
|
+
}
|
|
97
|
+
|
|
85
98
|
/** Change one reusable dimension, removing only that dimension for automatic. */
|
|
86
99
|
export function changeCapabilityRule(policy, id, dimension, choice) {
|
|
87
100
|
const next = structuredClone(policy)
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
2
|
+
import { CAPABILITY_PLAN_FORM, isPtcMessageSource, PTC_MESSAGE_SOURCE_KIND, RETIRED_SURFACE_RECORD } from './message-sources.js'
|
|
3
|
+
import { CAPABILITY_DISCLOSURES } from './capability-policy.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Session capability choices: the capability decisions this session recorded.
|
|
7
|
+
*
|
|
8
|
+
* The panel states two different facts side by side. The saved column answers
|
|
9
|
+
* "what does the global configuration say", and this record answers "what did
|
|
10
|
+
* this session decide". Keeping them apart is what lets a session state a choice
|
|
11
|
+
* while the global configuration still reads as it did: neither answer is
|
|
12
|
+
* derived from the other, and neither needs a session runtime.
|
|
13
|
+
*
|
|
14
|
+
* Three states, one question: no record (`none`) means the session inherits the
|
|
15
|
+
* saved column; a readable newest record (`decided`) is that session's own
|
|
16
|
+
* answer; a newest record this build cannot parse (`unreadable`) supersedes the
|
|
17
|
+
* readable ones before it, so no consumer may answer from an older record in its
|
|
18
|
+
* place. The third state is a fact about that record, not a state of the panel:
|
|
19
|
+
* every row it touches keeps its control, and recording a new decision replaces
|
|
20
|
+
* it.
|
|
21
|
+
*
|
|
22
|
+
* A choice never claims supply. It names an entry by id and never adds or
|
|
23
|
+
* restores a row, because the row it applies to exists only while the host
|
|
24
|
+
* declares that entry. A record may therefore outlive the declaration it was
|
|
25
|
+
* written for: the session keeps its decision, the catalogue stops offering the
|
|
26
|
+
* row, and nothing reads the record as a supply. `change()` asks this module's
|
|
27
|
+
* vocabulary before it appends, so a decision about an entry this vocabulary
|
|
28
|
+
* cannot name never becomes a record.
|
|
29
|
+
*
|
|
30
|
+
* A record is not the only shape this session's decision has ever had. Before
|
|
31
|
+
* this record existed, the plugin wrote a session's choices into a plan snapshot —
|
|
32
|
+
* the one it delivered, or the one a pending command receipt carried when no step
|
|
33
|
+
* had accepted it yet — and those sessions have no record at all. Reading such
|
|
34
|
+
* a session as `none` would recompose it from an empty set and withdraw choices
|
|
35
|
+
* its own log still holds, so this module reads the newest delivered plan's
|
|
36
|
+
* `sessionOverrides` as the same statement (`decided`, marked `legacy`) when the
|
|
37
|
+
* session recorded none. The delivered plan is the equivalent form of one
|
|
38
|
+
* statement, never a second owner: a record outvotes it, and the next write
|
|
39
|
+
* restates every choice this read answered with. Only a plan that delivered a
|
|
40
|
+
* choice is such a statement — an empty override map says the session inherited
|
|
41
|
+
* everything, which is the state a session without a record already reads as — and
|
|
42
|
+
* a plan that states the map unreadably reports `unreadable` for the same reason a
|
|
43
|
+
* record does.
|
|
44
|
+
*
|
|
45
|
+
* A choice about this session's capability set is a statement the session makes
|
|
46
|
+
* about itself, not correspondence with the model, and the carrier it travels on
|
|
47
|
+
* decides when it may be written. It travels as the plugin record below, which the
|
|
48
|
+
* host retains on every read and interprets as a surface node by none: a decision
|
|
49
|
+
* taken before the session's first step is therefore durable at once and never
|
|
50
|
+
* becomes a surface node ahead of the protected head. The `user/message` form is
|
|
51
|
+
* the historical carrier that 0.5.4 and earlier wrote; it is still read (a session
|
|
52
|
+
* persisted by those releases keeps its decision), and no writer produces it.
|
|
53
|
+
*/
|
|
54
|
+
export const CAPABILITY_SELECTION_FORM = 'capability-selection'
|
|
55
|
+
export const CAPABILITY_SELECTION_SCHEMA = 'ptc-capability-selection/v1'
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The log record this session's own choice is written as.
|
|
59
|
+
*
|
|
60
|
+
* A choice about this session's capability set is the session's statement about
|
|
61
|
+
* itself, not correspondence with the model, so it belongs in the log beside the
|
|
62
|
+
* turn events rather than on the model-visible surface. A plugin record is the
|
|
63
|
+
* host's carrier for exactly that kind of fact: it is retained on every read,
|
|
64
|
+
* interpreted as a surface node by none, and it may be written at any moment —
|
|
65
|
+
* including before the host publishes its protected surface head, where a message
|
|
66
|
+
* would make the whole log unreadable.
|
|
67
|
+
*
|
|
68
|
+
* A record that exists only as a plugin record means the step that composes the
|
|
69
|
+
* next environment is the only reader that has to know it, which is why the panel
|
|
70
|
+
* and the planner read this type directly instead of waiting for a delivery.
|
|
71
|
+
*/
|
|
72
|
+
export const CAPABILITY_SELECTION_RECORD = 'plugin:ptc-plus/capability-selection'
|
|
73
|
+
|
|
74
|
+
const MAX_CHOICES = 256
|
|
75
|
+
/** The one entry-id vocabulary a choice record may use; its writer validates the same form. */
|
|
76
|
+
export const CAPABILITY_ENTRY_ID = /^(tools|skills|mcp|mcpServers)\.[^\s]{1,200}$/
|
|
77
|
+
const ENTRY_ID = CAPABILITY_ENTRY_ID
|
|
78
|
+
const AVAILABILITY = ['enabled', 'closed', 'automatic']
|
|
79
|
+
const DISCLOSURE = ['automatic', ...CAPABILITY_DISCLOSURES]
|
|
80
|
+
const PREFIX = 'PTC Plus session capability choices. These are this session\'s own decisions; the global configuration still owns the saved column, and the host still owns which entries exist. Choices:'
|
|
81
|
+
|
|
82
|
+
function isRecord(value) {
|
|
83
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function sortedEntries(choices) {
|
|
87
|
+
return Object.entries(choices).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function normalizeCapabilitySelection(value) {
|
|
91
|
+
if (!isRecord(value) || Reflect.ownKeys(value).length !== 2
|
|
92
|
+
|| !Object.hasOwn(value, 'schema') || !Object.hasOwn(value, 'choices')) {
|
|
93
|
+
throw new TypeError('invalid capability selection')
|
|
94
|
+
}
|
|
95
|
+
if (value.schema !== CAPABILITY_SELECTION_SCHEMA) throw new TypeError('invalid capability selection schema')
|
|
96
|
+
if (!isRecord(value.choices) || Reflect.ownKeys(value.choices).length > MAX_CHOICES) {
|
|
97
|
+
throw new TypeError('invalid capability selection choices')
|
|
98
|
+
}
|
|
99
|
+
const choices = {}
|
|
100
|
+
for (const [id, choice] of sortedEntries(value.choices)) {
|
|
101
|
+
if (!ENTRY_ID.test(id) || !isRecord(choice) || Reflect.ownKeys(choice).length === 0) {
|
|
102
|
+
throw new TypeError('invalid capability selection entry')
|
|
103
|
+
}
|
|
104
|
+
const next = {}
|
|
105
|
+
for (const [dimension, selection] of Object.entries(choice)) {
|
|
106
|
+
const allowed = dimension === 'availability' ? AVAILABILITY
|
|
107
|
+
: dimension === 'disclosure' ? DISCLOSURE : undefined
|
|
108
|
+
if (allowed === undefined || !allowed.includes(selection)) throw new TypeError('invalid capability selection dimension')
|
|
109
|
+
next[dimension] = selection
|
|
110
|
+
}
|
|
111
|
+
choices[id] = next
|
|
112
|
+
}
|
|
113
|
+
return Object.freeze({ schema: CAPABILITY_SELECTION_SCHEMA,
|
|
114
|
+
choices: Object.freeze(Object.fromEntries(Object.entries(choices).map(([id, choice]) => [id, Object.freeze(choice)]))) })
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The exact model-visible wording one selection record carries. */
|
|
118
|
+
export function capabilitySelectionText(selection) {
|
|
119
|
+
return `${PREFIX}\n${JSON.stringify(normalizeCapabilitySelection(selection).choices)}`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The historical message this statement was written as before the record carrier.
|
|
124
|
+
*
|
|
125
|
+
* 0.5.4 and earlier appended this message on the model-visible surface, which is what
|
|
126
|
+
* `scripts/repair-session-log.mjs` repairs. No writer produces it now: the record below is
|
|
127
|
+
* the statement, and this form survives so that a session persisted by one of those releases
|
|
128
|
+
* is still read, and so that the repair has the shape it retires to build a fixture from.
|
|
129
|
+
*/
|
|
130
|
+
export function capabilitySelectionMessage(selection) {
|
|
131
|
+
const text = capabilitySelectionText(selection)
|
|
132
|
+
const message = createUserMessage({
|
|
133
|
+
source: { kind: PTC_MESSAGE_SOURCE_KIND, form: CAPABILITY_SELECTION_FORM },
|
|
134
|
+
content: [{ type: 'text', text }],
|
|
135
|
+
})
|
|
136
|
+
if (readCapabilitySelection(message) === undefined) throw new TypeError('invalid capability selection message')
|
|
137
|
+
return message
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Read one committed plugin record back as this session's selection, or nothing. */
|
|
141
|
+
export function readCapabilitySelectionRecord(event) {
|
|
142
|
+
if (event?.type !== CAPABILITY_SELECTION_RECORD || event.ignorable !== true) return undefined
|
|
143
|
+
try {
|
|
144
|
+
return normalizeCapabilitySelection(event.data)
|
|
145
|
+
} catch {
|
|
146
|
+
return undefined
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Whether one event states this session's capability selection at all.
|
|
152
|
+
*
|
|
153
|
+
* The record type states it, and so does the historical message form. A repaired statement
|
|
154
|
+
* (`RETIRED_SURFACE_RECORD`) states it only when the payload it kept is this owner's own form:
|
|
155
|
+
* a repair retires every surface node ahead of the protected head, whichever owner wrote it, so
|
|
156
|
+
* the carrier alone says nothing about who the statement belongs to. Reading another owner's
|
|
157
|
+
* retired statement as this one would answer `unreadable` for a decision this session never made
|
|
158
|
+
* — and would then outvote the selection the session did record.
|
|
159
|
+
*/
|
|
160
|
+
function statesCapabilitySelection(event) {
|
|
161
|
+
if (event?.type === CAPABILITY_SELECTION_RECORD) return true
|
|
162
|
+
const source = event?.data?.source
|
|
163
|
+
return isPtcMessageSource(source) && source.form === CAPABILITY_SELECTION_FORM
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Read one selection out of either carrier.
|
|
168
|
+
*
|
|
169
|
+
* A record and the historical message state the same selection, so a caller that walks a log
|
|
170
|
+
* does not have to know which release wrote it — and a repaired statement answers for the owner
|
|
171
|
+
* whose form it carries.
|
|
172
|
+
*/
|
|
173
|
+
export function readCapabilitySelectionStatement(event) {
|
|
174
|
+
if (event?.type === CAPABILITY_SELECTION_RECORD) return readCapabilitySelectionRecord(event)
|
|
175
|
+
if (event?.type === RETIRED_SURFACE_RECORD) {
|
|
176
|
+
return event.ignorable === true && statesCapabilitySelection(event) ? readCapabilitySelection(event.data) : undefined
|
|
177
|
+
}
|
|
178
|
+
return event?.type === 'user/message' ? readCapabilitySelection(event.data) : undefined
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Read one selection record back from a message, message projection or message fact. */
|
|
182
|
+
export function readCapabilitySelection(value) {
|
|
183
|
+
const source = value?.source
|
|
184
|
+
if (!isPtcMessageSource(source) || source.form !== CAPABILITY_SELECTION_FORM) return undefined
|
|
185
|
+
if (!Array.isArray(value.content) || value.content.length !== 1 || value.content[0]?.type !== 'text') return undefined
|
|
186
|
+
const text = value.content[0].text
|
|
187
|
+
if (typeof text !== 'string' || !text.startsWith(`${PREFIX}\n`)) return undefined
|
|
188
|
+
try {
|
|
189
|
+
const choices = JSON.parse(text.slice(PREFIX.length + 1))
|
|
190
|
+
const selection = normalizeCapabilitySelection({ schema: CAPABILITY_SELECTION_SCHEMA, choices })
|
|
191
|
+
return capabilitySelectionText(selection) === text ? selection : undefined
|
|
192
|
+
} catch {
|
|
193
|
+
return undefined
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* How this session's newest selection record reads.
|
|
199
|
+
*
|
|
200
|
+
* The newest record is the current statement whether or not this build can parse
|
|
201
|
+
* it, so an unreadable one is not evidence for an earlier record either.
|
|
202
|
+
*/
|
|
203
|
+
export function classifyCapabilitySelection(records) {
|
|
204
|
+
if (!Array.isArray(records)) return Object.freeze({ state: 'none' })
|
|
205
|
+
for (let index = records.length - 1; index >= 0; index -= 1) {
|
|
206
|
+
const record = records[index]
|
|
207
|
+
if (record?.form !== CAPABILITY_SELECTION_FORM) continue
|
|
208
|
+
try {
|
|
209
|
+
return Object.freeze({ state: 'decided',
|
|
210
|
+
selection: normalizeCapabilitySelection({ schema: CAPABILITY_SELECTION_SCHEMA, choices: record.choices }) })
|
|
211
|
+
} catch {
|
|
212
|
+
return Object.freeze({ state: 'unreadable' })
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return Object.freeze({ state: 'none' })
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The dimension rule one selection states, or `{}` when it states no decision.
|
|
220
|
+
*
|
|
221
|
+
* One selection is the vocabulary a saved decision, a command intent and a
|
|
222
|
+
* delivered plan's override all use; a record states dimensions. This function
|
|
223
|
+
* is the only place the two spellings meet, so a writer that copies a selection
|
|
224
|
+
* into a record and a reader that turns a delivered override back into one
|
|
225
|
+
* cannot disagree about what that selection meant. Anything narrower — a rule
|
|
226
|
+
* naming only one dimension — is already in this shape and is copied as it is.
|
|
227
|
+
*/
|
|
228
|
+
export function capabilityRuleOfSelection(selection) {
|
|
229
|
+
if (isRecord(selection)) return { ...selection }
|
|
230
|
+
return selection === undefined || selection === 'inherit' ? {}
|
|
231
|
+
: selection === 'automatic' ? { availability: 'automatic', disclosure: 'automatic' }
|
|
232
|
+
: selection === 'closed' ? { availability: 'closed' } : { availability: 'enabled', disclosure: selection }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The dimension form of one recorded-by-plan override.
|
|
237
|
+
*
|
|
238
|
+
* A delivered plan recorded this session's choices in the compact vocabulary a
|
|
239
|
+
* saved configuration uses — one whole selection per entry, or the dimensions a
|
|
240
|
+
* policy rule states — while a record states dimensions only. The two meet in
|
|
241
|
+
* `capabilityRuleOfSelection`, which is why the plan is an equivalent form of one
|
|
242
|
+
* statement rather than a second vocabulary: every value it reads maps back to
|
|
243
|
+
* the same compact selection, so a choice read from a plan and restated into a
|
|
244
|
+
* record says what the plan said.
|
|
245
|
+
*/
|
|
246
|
+
function planChoice(override) {
|
|
247
|
+
const choice = capabilityRuleOfSelection(override)
|
|
248
|
+
return Object.keys(choice).length === 0 ? undefined : choice
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* This session's statement as its newest delivered plan recorded it.
|
|
253
|
+
*
|
|
254
|
+
* Only a plan that delivered at least one choice is a statement: an empty
|
|
255
|
+
* override map says the session inherited everything, which is the state a
|
|
256
|
+
* session without a record already reads as, not a decision to migrate. A plan
|
|
257
|
+
* whose overrides cannot be read as this vocabulary is `unreadable` for the same
|
|
258
|
+
* reason a record is: it supersedes what came before it and this build cannot
|
|
259
|
+
* say what it decided.
|
|
260
|
+
*/
|
|
261
|
+
function planSelection(message) {
|
|
262
|
+
const snapshot = message?.source?.snapshot
|
|
263
|
+
const overrides = snapshot?.sessionOverrides
|
|
264
|
+
// A snapshot that states no choices at all (the v1 generation) recorded nothing to
|
|
265
|
+
// migrate. One that states them in a shape this vocabulary cannot read is
|
|
266
|
+
// `unreadable`: it supersedes what came before it, and reporting `none` would let
|
|
267
|
+
// the composition withdraw a decision the log still holds.
|
|
268
|
+
if (overrides === undefined) return Object.freeze({ state: 'none' })
|
|
269
|
+
if (!isRecord(overrides)) return Object.freeze({ state: 'unreadable' })
|
|
270
|
+
try {
|
|
271
|
+
// The entries are collected as pairs and materialized with own data properties: an
|
|
272
|
+
// override key the record vocabulary cannot name (for example `__proto__`) must reach
|
|
273
|
+
// the vocabulary check as a name it refuses, not silently become the map's prototype
|
|
274
|
+
// and leave the statement reading `none` where the record branch reads `unreadable`.
|
|
275
|
+
const entries = []
|
|
276
|
+
for (const [id, override] of sortedEntries(overrides)) {
|
|
277
|
+
const choice = planChoice(override)
|
|
278
|
+
if (choice !== undefined) entries.push([id, choice])
|
|
279
|
+
}
|
|
280
|
+
if (entries.length === 0) return Object.freeze({ state: 'none' })
|
|
281
|
+
return Object.freeze({ state: 'decided', legacy: true,
|
|
282
|
+
selection: normalizeCapabilitySelection({ schema: CAPABILITY_SELECTION_SCHEMA, choices: Object.fromEntries(entries) }) })
|
|
283
|
+
} catch {
|
|
284
|
+
return Object.freeze({ state: 'unreadable' })
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* How this session's newest statement reads, from its own log events.
|
|
290
|
+
*
|
|
291
|
+
* One entry answers for every consumer: a record when the session wrote one,
|
|
292
|
+
* otherwise the newest delivered plan as the equivalent form. Reading the plan
|
|
293
|
+
* only when no record exists keeps the record the newest word, so a session that
|
|
294
|
+
* upgraded and then decided again is not answered from an older plan.
|
|
295
|
+
*
|
|
296
|
+
* A caller that knows which session the log belongs to passes that identity, and a
|
|
297
|
+
* delivered plan that names a different session is then not read as this session's
|
|
298
|
+
* statement. A plan message carries the session it was composed for, and the writers
|
|
299
|
+
* that restore one require that identity; a record carries none, so the log it was
|
|
300
|
+
* appended to is its only identity.
|
|
301
|
+
*/
|
|
302
|
+
export function capabilitySelectionOf(events, sessionId) {
|
|
303
|
+
const records = []
|
|
304
|
+
let delivered
|
|
305
|
+
for (const event of Array.isArray(events) ? events : []) {
|
|
306
|
+
// A record is the statement itself; the message form is the same statement on a release
|
|
307
|
+
// that wrote one. A repaired statement keeps its wording and loses only the carrier, so all
|
|
308
|
+
// three take part in the same "newest statement wins" ordering — but only while the payload
|
|
309
|
+
// is this owner's own form, because the repair retires every owner's surface node.
|
|
310
|
+
if (event?.type === CAPABILITY_SELECTION_RECORD || event?.type === RETIRED_SURFACE_RECORD) {
|
|
311
|
+
if (!statesCapabilitySelection(event)) continue
|
|
312
|
+
records.push({ form: CAPABILITY_SELECTION_FORM, choices: readCapabilitySelectionStatement(event)?.choices })
|
|
313
|
+
continue
|
|
314
|
+
}
|
|
315
|
+
if (event?.type !== 'user/message') continue
|
|
316
|
+
const source = event.data?.source
|
|
317
|
+
if (!isPtcMessageSource(source)) continue
|
|
318
|
+
if (source.form === CAPABILITY_SELECTION_FORM) {
|
|
319
|
+
records.push({ form: CAPABILITY_SELECTION_FORM, choices: readCapabilitySelection(event.data)?.choices })
|
|
320
|
+
} else if (source.form === CAPABILITY_PLAN_FORM
|
|
321
|
+
&& (sessionId === undefined || source.sessionId === sessionId)) delivered = event.data
|
|
322
|
+
}
|
|
323
|
+
const recorded = classifyCapabilitySelection(records)
|
|
324
|
+
return recorded.state === 'none' ? planSelection(delivered) : recorded
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** The choice this session recorded for one entry, or `undefined` when it recorded none. */
|
|
328
|
+
export function capabilityChoiceOf(statement, id) {
|
|
329
|
+
return statement?.state === 'decided' ? statement.selection.choices[id] : undefined
|
|
330
|
+
}
|
|
@@ -637,7 +637,15 @@ export function createDirectSurfaceOwner({
|
|
|
637
637
|
if (!isCurrent()) return assembly
|
|
638
638
|
const sessionPtc = presentation !== 'native' && id !== undefined
|
|
639
639
|
const sessionPtcProjection = presentation === 'ptc' && id !== undefined
|
|
640
|
-
|
|
640
|
+
// The step's admission is the delivered capability set: the Host renders its own
|
|
641
|
+
// `tools:sdk` block from it and builds a program's `tools` members from it, so a
|
|
642
|
+
// declaration composed from the registered-definitions view can name a member this
|
|
643
|
+
// step does not deliver. Reading it here keeps the declaration, the plan and the
|
|
644
|
+
// program about one set; a host that publishes no admission states no set, and the
|
|
645
|
+
// registry view is then the only evidence left.
|
|
646
|
+
const delivered = sessionPtc && context?.toolSet !== undefined
|
|
647
|
+
? toolSchemasForAgent(agent, context.toolSet) : undefined
|
|
648
|
+
if (sessionPtc) await capabilityEnvironment?.prepare(agent, assembly.sections, false, undefined, delivered)
|
|
641
649
|
const userBindings = sessionPtc ? await userBindingsForAgent(agent) : undefined
|
|
642
650
|
if (!isCurrent()) return assembly
|
|
643
651
|
const runCode = tools?.find(tool => tool?.name === RUN_CODE)
|
|
@@ -668,9 +676,9 @@ export function createDirectSurfaceOwner({
|
|
|
668
676
|
&& assembly.sections.some(section => section?.name === 'tools:sdk'
|
|
669
677
|
|| (sessionPtcProjection && section?.name === completedState.collapseSectionName))
|
|
670
678
|
const sections = projectsSections
|
|
671
|
-
? (capabilityEnvironment?.projectSections(agent, assembly.sections) ?? assembly.sections).map(section => {
|
|
679
|
+
? (capabilityEnvironment?.projectSections(agent, assembly.sections, delivered) ?? assembly.sections).map(section => {
|
|
672
680
|
if (section?.name === 'tools:sdk') {
|
|
673
|
-
return { ...section, text: capabilitySdk(capabilityEnvironment?.sdk(agent, section.text) ?? section.text,
|
|
681
|
+
return { ...section, text: capabilitySdk(capabilityEnvironment?.sdk(agent, section.text, delivered) ?? section.text,
|
|
674
682
|
userBindings !== undefined, languageSemantics) }
|
|
675
683
|
}
|
|
676
684
|
if (sessionPtcProjection && section?.name === completedState.collapseSectionName) {
|
|
@@ -680,8 +688,13 @@ export function createDirectSurfaceOwner({
|
|
|
680
688
|
})
|
|
681
689
|
: assembly.sections
|
|
682
690
|
if (id !== undefined) {
|
|
691
|
+
// A top-level call is repaired into a program call only for a member this step
|
|
692
|
+
// delivers: the admission is what the declaration and the program state, so a
|
|
693
|
+
// name the registry merely carries is a call no member answers and no schema
|
|
694
|
+
// proves. A step that published no admission keeps the registry view.
|
|
695
|
+
const callable = delivered ?? toolSchemasForAgent(agent)
|
|
683
696
|
const nativeSchemas = presentation === 'ptc'
|
|
684
|
-
? new Map((capabilityEnvironment?.visibleSchemas(agent,
|
|
697
|
+
? new Map((capabilityEnvironment?.visibleSchemas(agent, callable) ?? callable)
|
|
685
698
|
.filter(schema => typeof schema?.name === 'string'
|
|
686
699
|
&& schema.name !== RUN_CODE && schema.name !== EDIT_RUN_CODE)
|
|
687
700
|
.map(schema => [schema.name, schema]))
|
|
@@ -7,6 +7,27 @@ export function isPtcMessageSource(source) {
|
|
|
7
7
|
|| (source?.kind === 'plugin' && source.plugin === 'ptc-plus')
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* The delivered capability plan form.
|
|
12
|
+
*
|
|
13
|
+
* The plan producer composes this message; naming its form here keeps the
|
|
14
|
+
* vocabulary of what this plugin writes in one module, so the record reader can
|
|
15
|
+
* recognize a delivered plan without importing the producer that composes one.
|
|
16
|
+
*/
|
|
17
|
+
export const CAPABILITY_PLAN_FORM = 'capability-plan'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The record a surface statement becomes when it may no longer occupy the surface.
|
|
21
|
+
*
|
|
22
|
+
* A session written before the record carrier carries a decision as a `user/message` ahead of the
|
|
23
|
+
* Host's protected head, and the released format refuses such a log on every read.
|
|
24
|
+
* `scripts/repair-session-log.mjs` keeps the sequence number and the payload and gives up only the
|
|
25
|
+
* carrier. The statement is therefore still a statement of the owner whose form its payload names,
|
|
26
|
+
* which is why this carrier lives beside the forms rather than in either owner: two owners read it,
|
|
27
|
+
* and each of them reads only its own.
|
|
28
|
+
*/
|
|
29
|
+
export const RETIRED_SURFACE_RECORD = 'plugin:ptc-plus/retired-surface-statement'
|
|
30
|
+
|
|
10
31
|
/** DSH owns aggregate runtime-context messages independently of PTC messages. */
|
|
11
32
|
export function isHostRuntimeContextSource(source) {
|
|
12
33
|
return source?.kind === 'runtime-context'
|
|
@@ -3,10 +3,19 @@ import { record, text } from './record-utils.js'
|
|
|
3
3
|
import { createHash } from 'node:crypto'
|
|
4
4
|
import { snapshotJsonValue } from '@deepseek-ai/dsh-util-values'
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Derive SDK contracts from the public, scope-qualified tool registry.
|
|
8
|
+
*
|
|
9
|
+
* `toolSet` is the step's admission, and passing it is how a caller states the set the
|
|
10
|
+
* Host actually delivers for that step: the Host renders its own `tools:sdk` block and
|
|
11
|
+
* builds a program's `tools` members from the admitted definitions, so a contract list
|
|
12
|
+
* read from the registered-definitions view can name a member no step delivers.
|
|
13
|
+
*/
|
|
14
|
+
export function toolSdkSchemas(tools, scope, toolSet = undefined) {
|
|
15
|
+
return tools.schemas(scope, toolSet).filter(schema => schema.name !== 'run_code').map(schema => {
|
|
16
|
+
// The definition is read through the same admission, so a contract describes the
|
|
17
|
+
// definition this step delivers rather than one the registry would resolve.
|
|
18
|
+
const definition = tools.get(schema.name, scope, toolSet)
|
|
10
19
|
const output = snapshotJsonValue(definition?.output?.schema)
|
|
11
20
|
if (output === undefined) {
|
|
12
21
|
throw new TypeError(`ptc-plus: tool "${schema.name}" output schema must be lossless JSON before SDK projection`)
|
|
@@ -347,7 +347,11 @@ export function createRuntimeBridgeOwner({
|
|
|
347
347
|
)))
|
|
348
348
|
const metadata = Object.freeze([
|
|
349
349
|
...(capabilityEnvironment?.toolMetadata(executionToken?.agent, toolCapabilityMetadata(schemas, annotations), schemas) ?? toolCapabilityMetadata(schemas, annotations)),
|
|
350
|
-
|
|
350
|
+
// The same set, as the third argument's own evidence: a derived row (`skills.*`,
|
|
351
|
+
// `mcp.*`) states the invocation path it still has, and the operations it can name
|
|
352
|
+
// are the members this program was handed — not the ones the composition froze when
|
|
353
|
+
// the session was last composed, which a step that delivers less does not rewrite.
|
|
354
|
+
...(capabilityEnvironment?.metadata(executionToken?.agent, schemas) ?? []),
|
|
351
355
|
...PROGRAM_CAPABILITY_METADATA.map(entry => entry.namespace === 'code'
|
|
352
356
|
&& cellConfig.userBindingsEnabled === true
|
|
353
357
|
? { ...entry, members: [...entry.members, BINDING_SUBMISSION] }
|
|
@@ -130,6 +130,16 @@ export function resolveConfig(config = {}) {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
resolved.capabilityPolicy = JSON.stringify(capabilityPolicyForConfig(provided.has('capabilityPolicy') ? resolved : { ...resolved, capabilityPolicy: undefined }))
|
|
133
|
+
// Whether the record holds this policy is a fact about the input document, not about this
|
|
134
|
+
// resolution's output: a configuration this function already resolved carries the answer,
|
|
135
|
+
// and resolving that answer's own product again must not re-infer it from the policy this
|
|
136
|
+
// function just wrote. Callers that reconcile by resolving a source that may itself be
|
|
137
|
+
// resolved (the plugin resolves its raw document and then re-resolves the result) would
|
|
138
|
+
// otherwise report every policy as saved, which is what makes a synthesized bottom layer
|
|
139
|
+
// decide the saved column. The marker only means something beside a policy the document
|
|
140
|
+
// itself holds, so a document that writes the marker without one states no provenance.
|
|
141
|
+
resolved.capabilityPolicySaved = provided.has('capabilityPolicy')
|
|
142
|
+
&& (typeof config.capabilityPolicySaved === 'boolean' ? config.capabilityPolicySaved : true)
|
|
133
143
|
if (!provided.has('capabilityPolicy') && resolved.capabilityManagementEnabled) {
|
|
134
144
|
if (!provided.has('capabilityRecoveryEnabled')) resolved.capabilityRecoveryEnabled = true
|
|
135
145
|
if (!provided.has('modelPreviewEnabled')) resolved.modelPreviewEnabled = true
|
|
@@ -9,9 +9,38 @@ import { deepEqualJson, isJsonValue } from '@deepseek-ai/dsh-util-values'
|
|
|
9
9
|
import { deepFreeze, isRecord } from './record-utils.js'
|
|
10
10
|
import { advanceRuntimeHistory, createRuntimeHistory } from './recovery-tips.js'
|
|
11
11
|
|
|
12
|
-
import { isHostRuntimeContextSource } from './message-sources.js'
|
|
12
|
+
import { isHostRuntimeContextSource, isPtcMessageSource } from './message-sources.js'
|
|
13
|
+
import { BINDING_ADOPTION_FORM, readBindingAdoption } from './binding-adoption.js'
|
|
13
14
|
const SYSTEM_PROMPT_CLEARED = 'Current runtime context: none. Earlier runtime-context snapshots no longer apply.'
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* A PTC binding-adoption statement, readable or not.
|
|
18
|
+
*
|
|
19
|
+
* The fold has to recognize the statement even when this build cannot resolve its
|
|
20
|
+
* wording: the fact it keeps is what a restored checkpoint replays, so a fact that
|
|
21
|
+
* dropped the form could not reproduce the record the fold appended.
|
|
22
|
+
*/
|
|
23
|
+
function isBindingAdoptionStatement(message) {
|
|
24
|
+
const source = message?.source
|
|
25
|
+
return isPtcMessageSource(source) && source.form === BINDING_ADOPTION_FORM
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* An adoption statement this build cannot resolve, kept as the statement it is.
|
|
30
|
+
*
|
|
31
|
+
* Both the panel and the `/binding` command answer from this projection, and the
|
|
32
|
+
* answer has to be this session's *newest* statement. A projection that dropped a
|
|
33
|
+
* statement it could not resolve would let the record the session superseded answer
|
|
34
|
+
* instead, so the statement is retained with no declined list — the absence is what
|
|
35
|
+
* marks it unreadable. It never reaches the model-visible surface, which is built
|
|
36
|
+
* from resolvable runtime messages only.
|
|
37
|
+
*/
|
|
38
|
+
function unreadableBindingAdoption(message) {
|
|
39
|
+
if (!isBindingAdoptionStatement(message)) return undefined
|
|
40
|
+
if (readBindingAdoption(message) !== undefined) return undefined
|
|
41
|
+
return Object.freeze({ form: BINDING_ADOPTION_FORM })
|
|
42
|
+
}
|
|
43
|
+
|
|
15
44
|
/** DSH's empty aggregate uses an exact owner marker without a snapshot form. */
|
|
16
45
|
export function systemPromptSnapshotSections(message) {
|
|
17
46
|
const source = message?.source
|
|
@@ -72,7 +101,8 @@ export function runtimeMessageFacts(records, initial = {}) {
|
|
|
72
101
|
|
|
73
102
|
function presentationEvent(event) {
|
|
74
103
|
const recognized = event?.type === 'user/message'
|
|
75
|
-
&& (systemPromptSnapshotSections(event.data) !== undefined || readRuntimeMessage(event.data) !== undefined
|
|
104
|
+
&& (systemPromptSnapshotSections(event.data) !== undefined || readRuntimeMessage(event.data) !== undefined
|
|
105
|
+
|| isBindingAdoptionStatement(event.data))
|
|
76
106
|
const data = recognized ? { source: event.data.source, content: event.data.content }
|
|
77
107
|
: event?.type === 'user/message' && typeof event.data?.source?.kind === 'string'
|
|
78
108
|
? { source: { kind: event.data.source.kind } } : undefined
|
|
@@ -86,11 +116,14 @@ function applyLogEvent(state, event, index = event?.seq,
|
|
|
86
116
|
timeline = advanceSessionTimeline(state.timeline, event, index)) {
|
|
87
117
|
const sections = event?.type === 'user/message' ? systemPromptSnapshotSections(event.data) : undefined
|
|
88
118
|
const ptc = event?.type === 'user/message' ? readRuntimeMessage(event.data) : undefined
|
|
119
|
+
// An adoption statement this build cannot resolve is still this session's statement,
|
|
120
|
+
// so it stays in the projection as the newest one rather than disappearing.
|
|
121
|
+
const unresolvedAdoption = ptc === undefined ? unreadableBindingAdoption(event?.data) : undefined
|
|
89
122
|
const contextStep = state.contextStep + (isContextStep(event) ? 1 : 0)
|
|
90
123
|
const surfaceNodes = advanceSessionSurface(state.surfaceNodes, event)
|
|
91
124
|
let visibleRuntimeMessages = state.visibleRuntimeMessages
|
|
92
|
-
if (timeline === state.timeline && sections === undefined && ptc === undefined &&
|
|
93
|
-
&& surfaceNodes === state.surfaceNodes) return state
|
|
125
|
+
if (timeline === state.timeline && sections === undefined && ptc === undefined && unresolvedAdoption === undefined
|
|
126
|
+
&& contextStep === state.contextStep && surfaceNodes === state.surfaceNodes) return state
|
|
94
127
|
const next = { ...state, timeline, contextStep,
|
|
95
128
|
logFacts: [...state.logFacts, deepFreeze({ index, event: presentationEvent(event) })],
|
|
96
129
|
}
|
|
@@ -110,6 +143,11 @@ function applyLogEvent(state, event, index = event?.seq,
|
|
|
110
143
|
const record = Object.freeze({ ...ptc, index, contextStep: state.contextStep, producer: 'ptc-plus' })
|
|
111
144
|
next.ptcMessages = [...state.ptcMessages, record]
|
|
112
145
|
next.runtimeMessagesBySeq = { ...next.runtimeMessagesBySeq, [`k${event.seq}`]: record }
|
|
146
|
+
} else if (unresolvedAdoption !== undefined) {
|
|
147
|
+
// Listed as this session's newest statement, but not as a resolvable runtime
|
|
148
|
+
// message: the ordered surface below is built from `runtimeMessagesBySeq`.
|
|
149
|
+
next.ptcMessages = [...state.ptcMessages,
|
|
150
|
+
Object.freeze({ ...unresolvedAdoption, index, contextStep: state.contextStep, producer: 'ptc-plus' })]
|
|
113
151
|
}
|
|
114
152
|
if (event?.surfaceOp === 'append') {
|
|
115
153
|
const record = next.runtimeMessagesBySeq[`k${event.seq}`]
|
|
@@ -133,7 +171,11 @@ export function createSessionLogProjection(options = {}) {
|
|
|
133
171
|
const replayBudget = admissionReplayBudget(options.replayBudgetBytes)
|
|
134
172
|
return Object.freeze({
|
|
135
173
|
key: SESSION_LOG_PROJECTION_KEY,
|
|
136
|
-
|
|
174
|
+
// 14 records the binding-adoption statement this build cannot resolve: the fact it
|
|
175
|
+
// keeps now carries the form its own replay needs, and `ptcMessages` gained a record
|
|
176
|
+
// for it. A checkpoint written by an earlier fold answers a different question, so
|
|
177
|
+
// the version moves and stored checkpoints are refolded rather than admitted.
|
|
178
|
+
stateVersion: 14,
|
|
137
179
|
stateSchema: Object.freeze({ parse(value) {
|
|
138
180
|
if (!isJsonValue(value) || !isRecord(value) || !isRecord(value.timeline)
|
|
139
181
|
|| typeof value.timeline.openTurn !== 'boolean' || typeof value.timeline.found !== 'boolean'
|
|
@@ -74,6 +74,12 @@ export function installSettingsSectionCompat({
|
|
|
74
74
|
&& typeof writeNamespace === 'string' && writeNamespace.length > 0
|
|
75
75
|
? Object.freeze({
|
|
76
76
|
update: (patch, expectedRevision) => provider.update(writeNamespace, patch, expectedRevision),
|
|
77
|
+
// The service applies field edits as `{ op: 'set' | 'unset', path }`; `update` can only
|
|
78
|
+
// restate values, so expose the primitive when it exists instead of narrowing the callers
|
|
79
|
+
// to a writer that cannot remove a field.
|
|
80
|
+
...(typeof provider.mutate === 'function'
|
|
81
|
+
? { mutate: (ops, expectedRevision) => provider.mutate(writeNamespace, ops, expectedRevision) }
|
|
82
|
+
: {}),
|
|
77
83
|
read: () => provider.describe?.().find(descriptor => descriptor.ns === writeNamespace),
|
|
78
84
|
}) : undefined
|
|
79
85
|
const unbind = onProvider(provider, writer)
|