dsh-ptc-plus 0.5.2 → 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 +4 -2
- package/README.md +4 -2
- package/client.cjs +3392 -2811
- package/compiler-core.cjs +127 -127
- package/compiler-core.cjs.map +2 -2
- package/docs/README.md +1 -0
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +4 -2
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +2 -2
- package/docs/adr/0023-global-user-bindings.md +12 -0
- package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +1 -1
- package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +2 -2
- package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +4 -4
- package/docs/adr/0046-gate-the-commit-with-the-semantic-evidence-tier.md +1 -1
- package/docs/adr/0048-supersede-a-rewritten-cell.md +48 -0
- package/docs/adr/0049-prove-a-settlement-against-its-published-record.md +53 -0
- package/docs/adr/0050-read-host-surfaces-through-their-published-contract.md +48 -0
- package/docs/adr/README.md +7 -1
- package/docs/architecture.md +1 -1
- 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/durability-design.md +9 -2
- package/docs/installation.md +17 -10
- package/docs/panel-concept.md +171 -0
- package/docs/releases/v0.5.2.md +26 -14
- package/docs/releases/v0.5.3.md +77 -0
- package/docs/releases/v0.5.4.md +75 -0
- package/docs/runtime-reference.md +21 -1
- package/docs/user-bindings.en.md +15 -0
- package/docs/user-bindings.md +15 -0
- package/docs/v0.5.3-goals.md +117 -0
- package/docs/verification.md +4 -4
- package/index.js +176 -41
- package/internal/binding-adoption.js +228 -0
- package/internal/capability-command-receipt.js +106 -8
- package/internal/capability-environment.js +1109 -312
- package/internal/capability-plan.js +18 -2
- package/internal/capability-policy.js +13 -0
- package/internal/capability-selection.js +330 -0
- package/internal/config-spec.js +14 -5
- package/internal/cordis-tools-owner.js +2 -1
- package/internal/direct-surface-owner.js +44 -17
- package/internal/edit-transport-owner.js +6 -5
- package/internal/execution-provider-owner.js +6 -0
- package/internal/execution-seam-compat.js +25 -0
- package/internal/host-tool-sdk.js +308 -0
- package/internal/message-sources.js +21 -0
- package/internal/official-repl-priority.js +31 -10
- package/internal/program-bindings.js +25 -5
- package/internal/repl-memory-projection.js +21 -2
- package/internal/runtime-bridge-owner.js +118 -9
- package/internal/runtime-config.js +10 -0
- package/internal/runtime-messages.js +5 -0
- package/internal/session-journal-recovery.js +210 -32
- package/internal/session-journal-schema.js +3 -0
- package/internal/session-journal.js +46 -9
- package/internal/session-log-view.js +69 -7
- package/internal/session-runtime.js +106 -39
- package/internal/settings-compat.js +6 -0
- package/internal/user-bindings-owner.js +126 -6
- package/package.json +59 -37
- package/src/client-authoring-view.js +7 -8
- package/src/client-capabilities-view.js +874 -909
- package/src/client-capability-sort.js +22 -9
- package/src/client-copy.js +118 -272
- package/src/client-indicator-view.js +2 -1
- package/src/client-repl-view.js +35 -16
- package/src/client-settings-view.js +90 -33
- package/src/client-styles.js +67 -19
- package/src/client.js +49 -22
|
@@ -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
|
+
}
|
package/internal/config-spec.js
CHANGED
|
@@ -130,10 +130,10 @@ export const CONFIG_FIELDS = Object.freeze([
|
|
|
130
130
|
type: 'enum',
|
|
131
131
|
options: Object.freeze(['stateful', 'protected']),
|
|
132
132
|
default: 'stateful',
|
|
133
|
-
label: '
|
|
134
|
-
labelEn: '
|
|
135
|
-
description: '
|
|
136
|
-
descriptionEn: 'Declarations and assignments update variables, functions, classes and imports in the same scope.
|
|
133
|
+
label: '跨 cell 声明策略',
|
|
134
|
+
labelEn: 'Cross-cell declaration policy',
|
|
135
|
+
description: '同一作用域的声明和赋值可更新变量、函数、类及导入。选“启用名称保护”时改为检测跨 cell 重声明,并保护新建的 const 和 import;静态 import/export 始终可用。',
|
|
136
|
+
descriptionEn: 'Declarations and assignments update variables, functions, classes and imports in the same scope. With “Protect names” selected, cross-cell redeclarations are detected instead and newly created const and import bindings are protected; static import/export remains available.',
|
|
137
137
|
},
|
|
138
138
|
{
|
|
139
139
|
key: 'canonicalizeToolCalls',
|
|
@@ -287,6 +287,15 @@ export const CONFIG_FIELDS = Object.freeze([
|
|
|
287
287
|
description: '检测到官方持久 REPL 模式时由官方运行时接管 PTC 会话执行;PTC Plus 让出 run_code 及其进程内会话状态语义,关闭时始终由 PTC Plus 接管。',
|
|
288
288
|
descriptionEn: 'Lets the official runtime own PTC session execution when the official persistent REPL mode is active; PTC Plus then leaves run_code and its in-process session-state semantics to DSH. Disabled keeps PTC Plus in charge.',
|
|
289
289
|
},
|
|
290
|
+
{
|
|
291
|
+
key: 'editRewritesHistory',
|
|
292
|
+
type: 'boolean',
|
|
293
|
+
default: false,
|
|
294
|
+
label: '编辑时替换原单元格',
|
|
295
|
+
labelEn: 'Replace the edited cell in place',
|
|
296
|
+
description: '默认关闭,一般也不需要打开:编辑只会追加一次执行,原单元格和它创建的绑定都还在。开启后,编辑会替换原单元格,之后的重建按新版本进行,新版本里不再声明的名字会被点名"已被替换",而不是悄悄消失。只有当你希望环境表现得"这一格一直就是新版本"时才需要打开。',
|
|
297
|
+
descriptionEn: 'Off by default, and usually unnecessary: an edit appends one more execution, leaving the original cell and the bindings it created in place. When on, the edit replaces the original cell, later rebuilds follow the new version, and a name the new version no longer declares is reported as replaced rather than silently disappearing. Turn it on only when the environment should behave as if that cell had always been the new version.',
|
|
298
|
+
},
|
|
290
299
|
{
|
|
291
300
|
key: 'tipsEnabled',
|
|
292
301
|
type: 'boolean',
|
|
@@ -483,7 +492,7 @@ export const CONFIG_GROUPS = Object.freeze([
|
|
|
483
492
|
key: 'recovery',
|
|
484
493
|
label: '状态与恢复',
|
|
485
494
|
labelEn: 'State and recovery',
|
|
486
|
-
fields: Object.freeze(['durableReplay', 'unmeasuredCellLimit', 'officialReplPriority', 'tipsEnabled', 'tipCooldownMessages', 'tipEscalationFailures']),
|
|
495
|
+
fields: Object.freeze(['durableReplay', 'editRewritesHistory', 'unmeasuredCellLimit', 'officialReplPriority', 'tipsEnabled', 'tipCooldownMessages', 'tipEscalationFailures']),
|
|
487
496
|
},
|
|
488
497
|
{
|
|
489
498
|
key: 'extensions',
|
|
@@ -5,6 +5,7 @@ import { createRequire } from 'node:module'
|
|
|
5
5
|
import { dirname, isAbsolute, join } from 'node:path'
|
|
6
6
|
import { isDeepStrictEqual } from 'node:util'
|
|
7
7
|
import { RUN_CODE } from './runtime-bridge-owner.js'
|
|
8
|
+
import { sessionExecutionAvailable } from './host-tool-sdk.js'
|
|
8
9
|
|
|
9
10
|
const CORDIS_PRESET_ID = 'cordis'
|
|
10
11
|
const CORDIS_SKILL_NAME = 'cordis-plugin-development'
|
|
@@ -111,7 +112,7 @@ function requireFiberServices(agent, fiber, owner) {
|
|
|
111
112
|
function absentCompanionSurfaces(agent) {
|
|
112
113
|
if (typeof agent?.ctx?.get !== 'function') return undefined
|
|
113
114
|
const missing = []
|
|
114
|
-
if (agent.ctx.tools
|
|
115
|
+
if (!sessionExecutionAvailable(agent.ctx.tools, agent, RUN_CODE)) missing.push(`the ${RUN_CODE} tool`)
|
|
115
116
|
if (agent.ctx.tools?.get?.(COMPANION_SKILL_TOOL, agent) === undefined) {
|
|
116
117
|
missing.push(`the ${COMPANION_SKILL_TOOL} tool`)
|
|
117
118
|
}
|
|
@@ -27,7 +27,7 @@ const INVALID_TOOL_JSON_FAILURE = Object.freeze({
|
|
|
27
27
|
const MALFORMED_TOOL_REJECTION_ID = 'dsh-ptc-plus-malformed-tool-json'
|
|
28
28
|
const MALFORMED_TOOL_REJECTION_ARGUMENTS = '{"dsh_ptc_plus_malformed_tool_json":'
|
|
29
29
|
|
|
30
|
-
function adaptRunCodeSchema(tool, promptText) {
|
|
30
|
+
export function adaptRunCodeSchema(tool, promptText) {
|
|
31
31
|
const parameters = tool.parameters
|
|
32
32
|
const properties = isRecord(parameters) ? parameters.properties : undefined
|
|
33
33
|
const code = isRecord(properties) ? properties.code : undefined
|
|
@@ -109,9 +109,14 @@ export function createCordisRecoveryPolicy(initiallyEnabled) {
|
|
|
109
109
|
})
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
function presentationState(assembly) {
|
|
113
|
-
const tools = Array.isArray(assembly?.tools) ? assembly.tools :
|
|
114
|
-
|
|
112
|
+
function presentationState(assembly, declaresRunCode, agent) {
|
|
113
|
+
const tools = Array.isArray(assembly?.tools) ? assembly.tools : undefined
|
|
114
|
+
// A host that declares tools after the presentation waterfall carries no
|
|
115
|
+
// array here; the transport it publishes is then the same fact.
|
|
116
|
+
const present = tools === undefined
|
|
117
|
+
? declaresRunCode?.(agent) === true
|
|
118
|
+
: tools.some(tool => tool?.name === RUN_CODE)
|
|
119
|
+
if (!present) {
|
|
115
120
|
return { presentation: 'native', ownerProven: false, collapseSectionName: undefined }
|
|
116
121
|
}
|
|
117
122
|
const collapse = PTC_COLLAPSE_SECTION_NAMES
|
|
@@ -453,6 +458,7 @@ export function createDirectSurfaceOwner({
|
|
|
453
458
|
canonicalizeToolCalls,
|
|
454
459
|
sessionId,
|
|
455
460
|
toolSchemasForAgent,
|
|
461
|
+
declaresRunCode,
|
|
456
462
|
userBindingsForAgent = async () => undefined,
|
|
457
463
|
setAgentPresentation = async () => {},
|
|
458
464
|
sessionLogView,
|
|
@@ -578,7 +584,7 @@ export function createDirectSurfaceOwner({
|
|
|
578
584
|
const requestOwner = id === undefined ? undefined : sessionOwner(id)
|
|
579
585
|
const isCurrent = () => !disposed && (requestOwner === undefined
|
|
580
586
|
|| (requestOwner.active && sessions.get(id) === requestOwner))
|
|
581
|
-
const initialState = presentationState(initialAssembly)
|
|
587
|
+
const initialState = presentationState(initialAssembly, declaresRunCode, agent)
|
|
582
588
|
let composition = compositions.get(agent)
|
|
583
589
|
if (composition === undefined && id !== undefined
|
|
584
590
|
&& (initialState.ownerProven || initialState.presentation === 'ptc')) {
|
|
@@ -590,11 +596,17 @@ export function createDirectSurfaceOwner({
|
|
|
590
596
|
|
|
591
597
|
const assembly = await next()
|
|
592
598
|
if (!isCurrent()) return assembly
|
|
599
|
+
// A host may declare the model-facing tool array after this waterfall, in
|
|
600
|
+
// which case the assembly carries none. The declaration surface is then
|
|
601
|
+
// host-owned: the plugin still owns presentation, sections, contexts and
|
|
602
|
+
// request policy, but it must not claim to have replaced declarations the
|
|
603
|
+
// host has not asked it for. A present but non-array declaration is still
|
|
604
|
+
// a malformed assembly.
|
|
593
605
|
const tools = assembly.tools
|
|
594
|
-
if (!Array.isArray(tools)) {
|
|
606
|
+
if (tools !== undefined && !Array.isArray(tools)) {
|
|
595
607
|
throw new Error('ptc-plus: incompatible prompt assembly; expected a tools array')
|
|
596
608
|
}
|
|
597
|
-
const completedState = presentationState(assembly)
|
|
609
|
+
const completedState = presentationState(assembly, declaresRunCode, agent)
|
|
598
610
|
if (composition === undefined && id !== undefined) {
|
|
599
611
|
const state = initialState.ownerProven ? initialState : completedState
|
|
600
612
|
composition = captureComposition(agent, id, state.presentation)
|
|
@@ -607,7 +619,7 @@ export function createDirectSurfaceOwner({
|
|
|
607
619
|
const requestSignal = context?.signal !== null && typeof context?.signal === 'object'
|
|
608
620
|
? context.signal
|
|
609
621
|
: undefined
|
|
610
|
-
if (
|
|
622
|
+
if (completedState.presentation === 'native') {
|
|
611
623
|
if (presentation !== 'native') {
|
|
612
624
|
throw new Error(`ptc-plus: ${presentation} agent composition assembled without run_code`)
|
|
613
625
|
}
|
|
@@ -625,15 +637,25 @@ export function createDirectSurfaceOwner({
|
|
|
625
637
|
if (!isCurrent()) return assembly
|
|
626
638
|
const sessionPtc = presentation !== 'native' && id !== undefined
|
|
627
639
|
const sessionPtcProjection = presentation === 'ptc' && id !== undefined
|
|
628
|
-
|
|
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)
|
|
629
649
|
const userBindings = sessionPtc ? await userBindingsForAgent(agent) : undefined
|
|
630
650
|
if (!isCurrent()) return assembly
|
|
631
|
-
const runCode = tools
|
|
651
|
+
const runCode = tools?.find(tool => tool?.name === RUN_CODE)
|
|
632
652
|
const promptText = currentPromptText
|
|
633
|
-
|
|
653
|
+
const hostDeclaresTools = tools !== undefined
|
|
654
|
+
let directTools = tools === undefined ? undefined
|
|
655
|
+
: sessionPtc ? capabilityEnvironment?.visibleSchemas(agent, tools) ?? tools : tools
|
|
634
656
|
if (sessionPtcProjection) {
|
|
635
657
|
editTransport.ensureInstalled(agent)
|
|
636
|
-
directTools = [adaptRunCodeSchema(runCode, promptText)]
|
|
658
|
+
if (hostDeclaresTools) directTools = [adaptRunCodeSchema(runCode, promptText)]
|
|
637
659
|
}
|
|
638
660
|
const runtimeContexts = sessionPtc
|
|
639
661
|
? sessionRuntimeContexts(agent, tipConfig, {
|
|
@@ -654,9 +676,9 @@ export function createDirectSurfaceOwner({
|
|
|
654
676
|
&& assembly.sections.some(section => section?.name === 'tools:sdk'
|
|
655
677
|
|| (sessionPtcProjection && section?.name === completedState.collapseSectionName))
|
|
656
678
|
const sections = projectsSections
|
|
657
|
-
? (capabilityEnvironment?.projectSections(agent, assembly.sections) ?? assembly.sections).map(section => {
|
|
679
|
+
? (capabilityEnvironment?.projectSections(agent, assembly.sections, delivered) ?? assembly.sections).map(section => {
|
|
658
680
|
if (section?.name === 'tools:sdk') {
|
|
659
|
-
return { ...section, text: capabilitySdk(capabilityEnvironment?.sdk(agent, section.text) ?? section.text,
|
|
681
|
+
return { ...section, text: capabilitySdk(capabilityEnvironment?.sdk(agent, section.text, delivered) ?? section.text,
|
|
660
682
|
userBindings !== undefined, languageSemantics) }
|
|
661
683
|
}
|
|
662
684
|
if (sessionPtcProjection && section?.name === completedState.collapseSectionName) {
|
|
@@ -666,8 +688,13 @@ export function createDirectSurfaceOwner({
|
|
|
666
688
|
})
|
|
667
689
|
: assembly.sections
|
|
668
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)
|
|
669
696
|
const nativeSchemas = presentation === 'ptc'
|
|
670
|
-
? new Map((capabilityEnvironment?.visibleSchemas(agent,
|
|
697
|
+
? new Map((capabilityEnvironment?.visibleSchemas(agent, callable) ?? callable)
|
|
671
698
|
.filter(schema => typeof schema?.name === 'string'
|
|
672
699
|
&& schema.name !== RUN_CODE && schema.name !== EDIT_RUN_CODE)
|
|
673
700
|
.map(schema => [schema.name, schema]))
|
|
@@ -685,11 +712,11 @@ export function createDirectSurfaceOwner({
|
|
|
685
712
|
|
|
686
713
|
return {
|
|
687
714
|
...assembly,
|
|
688
|
-
tools: sessionPtcProjection
|
|
715
|
+
...(directTools === undefined ? {} : { tools: sessionPtcProjection
|
|
689
716
|
? directTools
|
|
690
717
|
: directTools.map(tool => tool?.name === RUN_CODE
|
|
691
718
|
? adaptRunCodeSchema(tool, promptText)
|
|
692
|
-
: tool),
|
|
719
|
+
: tool) }),
|
|
693
720
|
sections,
|
|
694
721
|
}
|
|
695
722
|
},
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
normalizeRecoveryBoundaries,
|
|
13
13
|
normalizeRewrites,
|
|
14
14
|
validatedRewrites,
|
|
15
|
-
|
|
15
|
+
withSettlementProof,
|
|
16
16
|
} from './session-journal.js'
|
|
17
17
|
import {
|
|
18
18
|
EXPECTED_TARGET_CALL_SEQ,
|
|
@@ -29,6 +29,7 @@ import { sessionEvents } from './session-events.js'
|
|
|
29
29
|
import { deepEqualJson, snapshotJsonValue } from '@deepseek-ai/dsh-util-values'
|
|
30
30
|
import { scopeTarget } from '@deepseek-ai/dsh-scope'
|
|
31
31
|
import { RUN_CODE } from './runtime-bridge-owner.js'
|
|
32
|
+
import { dispatchToolCall } from './host-tool-sdk.js'
|
|
32
33
|
import { CAPABILITY_EVIDENCE_KEY } from './capability-environment.js'
|
|
33
34
|
import { isRecord } from './record-utils.js'
|
|
34
35
|
import {
|
|
@@ -199,9 +200,9 @@ export function createEditTransportOwner(ctx, {
|
|
|
199
200
|
derived.draftCapability,
|
|
200
201
|
presentationGeneration,
|
|
201
202
|
)
|
|
202
|
-
// The derived settlement
|
|
203
|
-
//
|
|
204
|
-
const withWording =
|
|
203
|
+
// The derived settlement proves its own facts, so a replay reads the same
|
|
204
|
+
// settlement the root path publishes.
|
|
205
|
+
const withWording = withSettlementProof(withDraft, withDraft[JOURNAL_KEY])
|
|
205
206
|
return derived.replMemory === undefined
|
|
206
207
|
? withWording
|
|
207
208
|
: withReplMemorySnapshot(withWording, derived.replMemory, presentationGeneration)
|
|
@@ -310,7 +311,7 @@ export function createEditTransportOwner(ctx, {
|
|
|
310
311
|
const executionStart = agent.session.append('tool/ptc-dispatch-start', data)
|
|
311
312
|
const derivedExec = { callId: data.subCallId, rootCallId: data.rootCallId,
|
|
312
313
|
parent: rootExec.token, name: RUN_CODE, arguments: argumentsValue, agent, signal: rootSignal }
|
|
313
|
-
const dispatch = () => ctx.tools
|
|
314
|
+
const dispatch = () => dispatchToolCall(ctx.tools, derivedExec)
|
|
314
315
|
const tentative = await executeTentative(executionStart.seq, start.rootCallSeq,
|
|
315
316
|
dispatch, executionLog, derivedExec)
|
|
316
317
|
const inner = tentative.result
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createExecutionSeam, EXECUTION_SEAM_SERVICE, LEGACY_EXECUTION_SEAM_SERVICE } from './execution-seam-compat.js'
|
|
2
|
+
import { providerRetainsState } from './official-repl-priority.js'
|
|
2
3
|
|
|
3
4
|
export const name = 'ptc-plus-execution-provider'
|
|
4
5
|
export const inject = ['loader']
|
|
@@ -70,6 +71,11 @@ export function apply(ctx, config = {}) {
|
|
|
70
71
|
get language() { return readOriginal().language },
|
|
71
72
|
get isolation() { return readOriginal().isolation },
|
|
72
73
|
get programFormat() { return readOriginal().programFormat ?? 'function-body' },
|
|
74
|
+
// This composition isolates state owners and forwards every run to the
|
|
75
|
+
// original provider, so the original's retention declaration is the
|
|
76
|
+
// truthful answer. A host plugin that reads it must not see a stateful
|
|
77
|
+
// environment reported as stateless.
|
|
78
|
+
get retainsState() { return providerRetainsState(readOriginal()) },
|
|
73
79
|
get executionInstructions() { return readOriginal().executionInstructions },
|
|
74
80
|
get sandboxMode() { return readOriginal().sandboxMode },
|
|
75
81
|
get timeout() { return readOriginal().timeout },
|