dsh-ptc-plus 0.5.2 → 0.5.3
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 +3 -1
- package/README.md +3 -1
- package/client.cjs +165 -29
- 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/durability-design.md +9 -2
- package/docs/installation.md +9 -9
- package/docs/releases/v0.5.2.md +26 -14
- package/docs/releases/v0.5.3.md +77 -0
- package/docs/runtime-reference.md +7 -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 +67 -19
- package/internal/binding-adoption.js +136 -0
- package/internal/capability-command-receipt.js +18 -4
- package/internal/capability-delivery-proof.js +70 -0
- package/internal/capability-environment.js +38 -1
- package/internal/config-spec.js +14 -5
- package/internal/cordis-tools-owner.js +2 -1
- package/internal/direct-surface-owner.js +27 -13
- 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/official-repl-priority.js +31 -10
- package/internal/program-bindings.js +12 -1
- package/internal/repl-memory-projection.js +21 -2
- package/internal/runtime-bridge-owner.js +113 -8
- 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 +22 -2
- package/internal/session-runtime.js +106 -39
- package/internal/user-bindings-owner.js +109 -6
- package/package.json +15 -7
- package/src/client-capabilities-view.js +45 -3
- package/src/client-copy.js +24 -0
- package/src/client-indicator-view.js +2 -1
- package/src/client-repl-view.js +35 -16
- package/src/client.js +3 -1
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host tool-surface shapes.
|
|
3
|
+
*
|
|
4
|
+
* Three host facts differ between lines and none is a release number:
|
|
5
|
+
*
|
|
6
|
+
* 1. Which package renders model-facing tool schemas. One line publishes
|
|
7
|
+
* `jsonSchemaToTs`, `renderToolsSdk`, `jsonSchemaToPy` and `CodeRunFailedError`
|
|
8
|
+
* from the PTC tool executor and stops exporting them from the tool registry;
|
|
9
|
+
* earlier lines publish them only from the registry. A static import from
|
|
10
|
+
* either path makes the plugin fail to load on the other, so the binding is
|
|
11
|
+
* resolved once, at load time, from whichever package actually provides it.
|
|
12
|
+
*
|
|
13
|
+
* 2. Which package publishes the registry error classes. The renderer and the
|
|
14
|
+
* error class moved independently, so each is resolved from the package that
|
|
15
|
+
* exports it rather than from the bundle that provided its neighbour.
|
|
16
|
+
*
|
|
17
|
+
* 3. Which surface publishes the reserved `run_code` transport definition. A
|
|
18
|
+
* registry host registers the definition and answers `tools.get(name)`. An
|
|
19
|
+
* executor host reserves the name instead and keeps the definition inside the
|
|
20
|
+
* executor that dispatches through it. Both surfaces expose the same
|
|
21
|
+
* definition shape, so the binding is resolved by that shape rather than by a
|
|
22
|
+
* version. Which *member* of an executor is asked is a separate contract: only
|
|
23
|
+
* the readers named here are ever invoked, because every other member is host
|
|
24
|
+
* code this plugin does not own. An executor that declares the name and
|
|
25
|
+
* publishes no definition through those readers is reported as such rather than
|
|
26
|
+
* treated as a host without an executor.
|
|
27
|
+
*/
|
|
28
|
+
const RENDERER_CANDIDATES = [
|
|
29
|
+
'@deepseek-ai/dsh-tool-executor-ptc',
|
|
30
|
+
'@deepseek-ai/dsh-tools',
|
|
31
|
+
]
|
|
32
|
+
const REGISTRY_CANDIDATES = [
|
|
33
|
+
'@deepseek-ai/dsh-tools',
|
|
34
|
+
'@deepseek-ai/dsh-tool-executor-ptc',
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
const loaded = new Map()
|
|
38
|
+
async function loadCandidate(specifier) {
|
|
39
|
+
if (loaded.has(specifier)) return loaded.get(specifier)
|
|
40
|
+
let module
|
|
41
|
+
try {
|
|
42
|
+
module = await import(specifier)
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// Absence lets the caller try the next candidate. A package that exists but
|
|
45
|
+
// fails to initialise is a different fact: treating it as absent silently
|
|
46
|
+
// substitutes another implementation, and reporting "no package provides this"
|
|
47
|
+
// hides the real error from the operator.
|
|
48
|
+
// "Cannot find package 'x'" means x is not installed. A module-not-found
|
|
49
|
+
// raised from inside x means x resolved and then failed, which must stay loud.
|
|
50
|
+
const missing = error?.code === 'ERR_MODULE_NOT_FOUND' || error?.code === 'MODULE_NOT_FOUND'
|
|
51
|
+
const absent = missing && String(error?.message ?? '').includes("Cannot find package '" + specifier + "'")
|
|
52
|
+
if (absent) {
|
|
53
|
+
module = undefined
|
|
54
|
+
} else {
|
|
55
|
+
throw new Error(`ptc-plus: host package ${specifier} failed to load: ${error?.message ?? String(error)}`, { cause: error })
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
loaded.set(specifier, module)
|
|
59
|
+
return module
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolve one capability from the first package that publishes it.
|
|
64
|
+
*
|
|
65
|
+
* A capability that no installed package publishes is a load failure of this
|
|
66
|
+
* plugin, not a degraded mode: the renderers and the registry error classes have
|
|
67
|
+
* no fallback that preserves their contract. The loader is injectable so the
|
|
68
|
+
* diagnostic can be proven without uninstalling a package from the checkout.
|
|
69
|
+
* @param {string[]} specifiers - candidate package specifiers, most specific first.
|
|
70
|
+
* @param {string} kind - what is being resolved, for the diagnostic.
|
|
71
|
+
* @param {(candidate: object) => boolean} accepts - whether a module publishes it.
|
|
72
|
+
* @param {(specifier: string) => Promise<object|undefined>} [load] - module loader.
|
|
73
|
+
* @returns {Promise<{api: object, specifier: string}>} the publishing package.
|
|
74
|
+
*/
|
|
75
|
+
async function resolveExport(specifiers, kind, accepts, load = loadCandidate) {
|
|
76
|
+
for (const specifier of specifiers) {
|
|
77
|
+
const candidate = await load(specifier)
|
|
78
|
+
if (candidate !== undefined && accepts(candidate)) return { api: candidate, specifier }
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`ptc-plus: no host package provides ${kind}`)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const renderers = await resolveExport(RENDERER_CANDIDATES, 'the tool schema renderers',
|
|
84
|
+
candidate => typeof candidate.jsonSchemaToTs === 'function' && typeof candidate.renderToolsSdk === 'function')
|
|
85
|
+
const errors = await resolveExport(REGISTRY_CANDIDATES, 'the tool registry error classes',
|
|
86
|
+
candidate => typeof candidate.ToolNotFoundError === 'function')
|
|
87
|
+
|
|
88
|
+
/** The resolver above, for the load-failure diagnostic a host without either package reports. */
|
|
89
|
+
export { resolveExport as resolveHostExport }
|
|
90
|
+
|
|
91
|
+
export const hostToolSdkPackage = renderers.specifier
|
|
92
|
+
export const jsonSchemaToTs = renderers.api.jsonSchemaToTs
|
|
93
|
+
export const renderToolsSdk = renderers.api.renderToolsSdk
|
|
94
|
+
export const ToolNotFoundError = errors.api.ToolNotFoundError
|
|
95
|
+
|
|
96
|
+
/** Executor-published transports, resolved once per executor and transport name. */
|
|
97
|
+
const executorTransports = new WeakMap()
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A transport an executor hands to its dispatch pipeline. The executor's own
|
|
101
|
+
* accessors are untyped, so identification needs the complete published
|
|
102
|
+
* definition: the reserved name, a dispatch function, and a rendering output.
|
|
103
|
+
*/
|
|
104
|
+
function isPublishedTransport(candidate, name) {
|
|
105
|
+
return candidate !== null && typeof candidate === 'object'
|
|
106
|
+
&& candidate.name === name
|
|
107
|
+
&& typeof candidate.execute === 'function'
|
|
108
|
+
&& candidate.output !== null && typeof candidate.output === 'object'
|
|
109
|
+
&& typeof candidate.output.render === 'function'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Read the executor a scope currently dispatches to.
|
|
114
|
+
* @param {object} tools - the host tool registry (`ctx.tools`).
|
|
115
|
+
* @param {object} scope - the viewing agent, preset or scope key.
|
|
116
|
+
* @returns {object|undefined} the selected executor, or undefined when the host
|
|
117
|
+
* has no executor surface or none is selected for this scope.
|
|
118
|
+
*/
|
|
119
|
+
export function hostToolExecutor(tools, scope) {
|
|
120
|
+
if (typeof tools?.selectExecutor !== 'function') return undefined
|
|
121
|
+
try {
|
|
122
|
+
return tools.selectExecutor(scope) ?? undefined
|
|
123
|
+
} catch {
|
|
124
|
+
// No executor is selected for this scope, or its selection was withdrawn.
|
|
125
|
+
return undefined
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The members a transport-owning executor publishes its reserved transport through,
|
|
131
|
+
* in the order they are asked. Which surface publishes the definition is a shape
|
|
132
|
+
* question, but *which member may be invoked* is not: an executor is host code, so
|
|
133
|
+
* only members the plugin's published reader contract names are ever called. A
|
|
134
|
+
* member that is not one of these readers is never a definition reader, however
|
|
135
|
+
* many arguments it takes and whatever it returns.
|
|
136
|
+
*
|
|
137
|
+
* `requirePtcTransport` is the name the tool registry's own transport owner uses, so
|
|
138
|
+
* an executor that keeps the definition for its dispatch pipeline publishes it under
|
|
139
|
+
* the same name; the other two are the shorter spellings a host may publish instead.
|
|
140
|
+
*/
|
|
141
|
+
const TRANSPORT_READERS = Object.freeze(['requirePtcTransport', 'requireTransport', 'transport'])
|
|
142
|
+
|
|
143
|
+
/** Executors whose declared transport could not be read; a fact about the executor,
|
|
144
|
+
* recorded so the caller can report the contract gap instead of "no executor". The
|
|
145
|
+
* readers are still asked again on a later assembly, because a scope can mount the
|
|
146
|
+
* service a reader depends on after the first one. */
|
|
147
|
+
const unreadableExecutors = new WeakSet()
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The reader an executor publishes under one of the contract's names, looking
|
|
151
|
+
* through its prototype chain the way a class member is inherited.
|
|
152
|
+
*/
|
|
153
|
+
function transportReader(executor, name) {
|
|
154
|
+
for (let owner = executor; owner !== null && owner !== Object.prototype; owner = Object.getPrototypeOf(owner)) {
|
|
155
|
+
const descriptor = Object.getOwnPropertyDescriptor(owner, name)
|
|
156
|
+
if (descriptor === undefined) continue
|
|
157
|
+
// A getter is a zero-argument reader of the same shape as a method.
|
|
158
|
+
const accessor = typeof descriptor.value === 'function' ? descriptor.value : descriptor.get
|
|
159
|
+
return typeof accessor === 'function' ? accessor : undefined
|
|
160
|
+
}
|
|
161
|
+
return undefined
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Whether this executor declares the reserved transport among the tools it
|
|
167
|
+
* presents. Only a transport-owning executor is asked for its definition, so an
|
|
168
|
+
* unrelated executor is never invoked at all.
|
|
169
|
+
*/
|
|
170
|
+
function executorOwnsTransport(executor, name) {
|
|
171
|
+
if (typeof executor?.describe !== 'function') return false
|
|
172
|
+
try {
|
|
173
|
+
const described = executor.describe([])
|
|
174
|
+
return Array.isArray(described?.knownNames) && described.knownNames.includes(name)
|
|
175
|
+
} catch {
|
|
176
|
+
// Describing with no visible definitions is how an executor that requires
|
|
177
|
+
// them reports the limit; that is no evidence that it owns the transport.
|
|
178
|
+
return false
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Resolve the transport definition from the surface that publishes it.
|
|
184
|
+
* @param {object} tools - the host tool registry (`ctx.tools`).
|
|
185
|
+
* @param {object} scope - the owning agent or scope key.
|
|
186
|
+
* @param {string} name - the reserved transport name.
|
|
187
|
+
* @returns {object|undefined} the live definition, or undefined when this host
|
|
188
|
+
* publishes it on neither surface.
|
|
189
|
+
*/
|
|
190
|
+
export function hostTransportDefinition(tools, scope, name) {
|
|
191
|
+
// The registry answers a lookup by the reserved name, so any definition it
|
|
192
|
+
// returns is this host's transport. Whether it carries the projection the
|
|
193
|
+
// plugin needs is the caller's diagnostic, not a reason to search elsewhere.
|
|
194
|
+
const registered = typeof tools?.get === 'function' ? tools.get(name, scope) : undefined
|
|
195
|
+
if (registered !== null && typeof registered === 'object') return registered
|
|
196
|
+
const executor = hostToolExecutor(tools, scope)
|
|
197
|
+
if (executor === undefined) return undefined
|
|
198
|
+
const cached = executorTransports.get(executor)?.get(name)
|
|
199
|
+
if (cached !== undefined) return cached
|
|
200
|
+
if (!executorOwnsTransport(executor, name)) return undefined
|
|
201
|
+
for (const reader of TRANSPORT_READERS) {
|
|
202
|
+
const accessor = transportReader(executor, reader)
|
|
203
|
+
if (accessor === undefined) continue
|
|
204
|
+
let candidate
|
|
205
|
+
try {
|
|
206
|
+
candidate = accessor.call(executor)
|
|
207
|
+
} catch {
|
|
208
|
+
// A reader may require a service this scope has not mounted yet; that is no
|
|
209
|
+
// evidence about the surface, so the next contracted reader still answers.
|
|
210
|
+
continue
|
|
211
|
+
}
|
|
212
|
+
if (!isPublishedTransport(candidate, name)) continue
|
|
213
|
+
unreadableExecutors.delete(executor)
|
|
214
|
+
let byName = executorTransports.get(executor)
|
|
215
|
+
if (byName === undefined) executorTransports.set(executor, byName = new Map())
|
|
216
|
+
byName.set(name, candidate)
|
|
217
|
+
return candidate
|
|
218
|
+
}
|
|
219
|
+
// The executor declares the transport and no contracted reader published it. That
|
|
220
|
+
// is a contract gap between host and plugin, not an absent surface; it is recorded
|
|
221
|
+
// so the gate can report which of the two facts holds.
|
|
222
|
+
unreadableExecutors.add(executor)
|
|
223
|
+
return undefined
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Whether this scope's executor declares the reserved transport but publishes no
|
|
228
|
+
* definition this plugin can read.
|
|
229
|
+
*
|
|
230
|
+
* The gate treats that as "no session execution" — a transport must never be
|
|
231
|
+
* invented — but it is not the same fact as a host without an executor, so callers
|
|
232
|
+
* can report the gap instead of dropping every feature that needs execution
|
|
233
|
+
* silently.
|
|
234
|
+
* @param {object} tools - the host tool registry (`ctx.tools`).
|
|
235
|
+
* @param {object} scope - the owning agent or scope key.
|
|
236
|
+
* @param {string} [name] - the reserved transport name.
|
|
237
|
+
* @returns {boolean} true when the host declares it and the definition is unreadable.
|
|
238
|
+
*/
|
|
239
|
+
export function hostTransportUnreadable(tools, scope, name = 'run_code') {
|
|
240
|
+
const executor = hostToolExecutor(tools, scope)
|
|
241
|
+
if (executor === undefined) return false
|
|
242
|
+
const registered = typeof tools?.get === 'function' ? tools.get(name, scope) : undefined
|
|
243
|
+
if (registered !== null && typeof registered === 'object') return false
|
|
244
|
+
return executorOwnsTransport(executor, name)
|
|
245
|
+
&& executorTransports.get(executor)?.get(name) === undefined
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Whether this scope has session execution at all: the host publishes the
|
|
250
|
+
* reserved transport on one of its surfaces. Every feature that can only work by
|
|
251
|
+
* running a program asks this, so a host that keeps the transport inside its
|
|
252
|
+
* tool executor enables them exactly like a host that registers it.
|
|
253
|
+
* @param {object} tools - the host tool registry (`ctx.tools`).
|
|
254
|
+
* @param {object} scope - the owning agent or scope key.
|
|
255
|
+
* @param {string} [name] - the reserved transport name.
|
|
256
|
+
* @returns {boolean} true when a program can be run for this scope.
|
|
257
|
+
*/
|
|
258
|
+
export function sessionExecutionAvailable(tools, scope, name = 'run_code') {
|
|
259
|
+
return hostTransportDefinition(tools, scope, name) !== undefined
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Executions this plugin submitted under the host's batch dispatch contract.
|
|
264
|
+
*
|
|
265
|
+
* That contract assigns the execution identity itself, so the execution object a
|
|
266
|
+
* host runs is not the one the caller built and carries no parent token the caller
|
|
267
|
+
* could compare. The submission record is what remains: the call id this plugin
|
|
268
|
+
* chose, which the host matched its result back to.
|
|
269
|
+
*/
|
|
270
|
+
const batchSubmissions = new WeakSet()
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Whether this execution was submitted as a host batch call.
|
|
274
|
+
* @param {object} execution - an execution object this plugin built.
|
|
275
|
+
* @returns {boolean} true when the batch contract dispatched it.
|
|
276
|
+
*/
|
|
277
|
+
export function isBatchSubmission(execution) {
|
|
278
|
+
return execution !== null && typeof execution === 'object' && batchSubmissions.has(execution)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Dispatch one tool call through the host registry.
|
|
283
|
+
*
|
|
284
|
+
* Two dispatch contracts are published. A registry host accepts one call and
|
|
285
|
+
* resolves to that call's settled result. An executor host accepts a complete
|
|
286
|
+
* model batch, assigns the execution identity itself, and resolves to the
|
|
287
|
+
* ordered outcomes — so the caller supplies only the call it wants run, and the
|
|
288
|
+
* executor's own token is the one its observers see.
|
|
289
|
+
* @param {object} tools - the host tool registry (`ctx.tools`).
|
|
290
|
+
* @param {object} execution - call identity, arguments, agent and signal.
|
|
291
|
+
* @returns {Promise<object>} this call's settled result.
|
|
292
|
+
*/
|
|
293
|
+
export async function dispatchToolCall(tools, execution) {
|
|
294
|
+
if (typeof tools?.selectExecutor !== 'function') return tools.execute(execution)
|
|
295
|
+
// Recorded before the host runs the batch: it calls back into this plugin during
|
|
296
|
+
// the dispatch, and the relation has to be provable then.
|
|
297
|
+
batchSubmissions.add(execution)
|
|
298
|
+
const settled = await tools.execute({
|
|
299
|
+
calls: [{ id: execution.callId, name: execution.name, arguments: JSON.stringify(execution.arguments) }],
|
|
300
|
+
signal: execution.signal,
|
|
301
|
+
...(execution.agent === undefined ? {} : { agent: execution.agent }),
|
|
302
|
+
})
|
|
303
|
+
const matched = settled?.results?.find(entry => entry.callId === execution.callId)
|
|
304
|
+
if (matched === undefined) {
|
|
305
|
+
throw new Error('ptc-plus: the host batch dispatch returned no result for the requested call')
|
|
306
|
+
}
|
|
307
|
+
return matched.result
|
|
308
|
+
}
|
|
@@ -1,27 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Official persistent-REPL priority.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* runtime
|
|
10
|
-
*
|
|
11
|
-
*
|
|
4
|
+
* The question is not which source form the official provider evaluates; it is
|
|
5
|
+
* whether that provider keeps a program environment alive per state owner. A
|
|
6
|
+
* host declares that fact as `retainsState`, and the answer decides who owns PTC
|
|
7
|
+
* session execution: while the user selects the official REPL priority and the
|
|
8
|
+
* live provider retains state, the plugin yields execution to the official
|
|
9
|
+
* implementation and installs no session runtime — `run_code`, its descriptors
|
|
10
|
+
* and its guidance stay official. Neither the source form nor a release
|
|
11
|
+
* identifier can answer it: one host line retains state for `script` programs and
|
|
12
|
+
* another runs the same `script` in a fresh process, and an earlier line could
|
|
13
|
+
* only report the REPL form at all. The decision is re-evaluated on every
|
|
14
|
+
* configuration change and host source change, so either side can take over
|
|
15
|
+
* without restarting the host.
|
|
12
16
|
*/
|
|
13
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Whether a live provider retains a program environment per state owner.
|
|
20
|
+
*
|
|
21
|
+
* A provider that reports `retainsState` answers directly, whatever source form
|
|
22
|
+
* it evaluates. A provider that predates that declaration leaks the same fact
|
|
23
|
+
* through its source form: only `repl` was retained, so a `script` or function
|
|
24
|
+
* body from that host is a fresh environment.
|
|
25
|
+
* @param {object} provider - the provider surface registered for the seam.
|
|
26
|
+
* @returns {boolean} true when the provider keeps state across runs per owner.
|
|
27
|
+
*/
|
|
28
|
+
export function providerRetainsState(provider) {
|
|
29
|
+
const reported = provider?.retainsState
|
|
30
|
+
if (typeof reported === 'boolean') return reported
|
|
31
|
+
// Without the declaration the source form is the only published evidence.
|
|
32
|
+
return provider?.programFormat === 'repl'
|
|
33
|
+
}
|
|
34
|
+
|
|
14
35
|
/**
|
|
15
36
|
* Decide whether this configuration yields PTC session execution to the
|
|
16
37
|
* official provider.
|
|
17
38
|
* @param {object} config - resolved plugin configuration.
|
|
18
39
|
* @param {object} provider - the live provider surface registered for the seam.
|
|
19
|
-
* @returns {boolean} true when the official
|
|
40
|
+
* @returns {boolean} true when the official provider owns execution.
|
|
20
41
|
*/
|
|
21
42
|
export function defersToOfficialRepl(config, provider) {
|
|
22
43
|
if (config?.officialReplPriority !== true) return false
|
|
23
44
|
try {
|
|
24
|
-
return provider
|
|
45
|
+
return providerRetainsState(provider)
|
|
25
46
|
} catch {
|
|
26
47
|
// An unreadable capability descriptor cannot prove official ownership;
|
|
27
48
|
// the plugin keeps its execution responsibility instead of guessing.
|
|
@@ -175,6 +175,17 @@ export function capabilityFind(metadata, query) {
|
|
|
175
175
|
.map(candidate => candidate.value)
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Inspection answers "how do I call this", so it hands back the declaration the
|
|
180
|
+
* model programs against. The JSON schema is how the plugin stores a signature,
|
|
181
|
+
* not how anyone reads one; keeping it in the payload made the model parse JSON
|
|
182
|
+
* to learn a call shape the source already states.
|
|
183
|
+
*/
|
|
184
|
+
function inspectableMember(member) {
|
|
185
|
+
const { schema, ...rest } = member
|
|
186
|
+
return rest
|
|
187
|
+
}
|
|
188
|
+
|
|
178
189
|
/** Batch, budgeted inspection; omitted items remain explicit to the caller. */
|
|
179
190
|
export function capabilityInspect(metadata, symbols = undefined, budget = 50) {
|
|
180
191
|
if (!Number.isSafeInteger(budget) || budget < 1) throw new TypeError('ptc-plus: capability inspection budget must be a positive integer')
|
|
@@ -190,7 +201,7 @@ export function capabilityInspect(metadata, symbols = undefined, budget = 50) {
|
|
|
190
201
|
const selected = requested.slice(0, budget).flatMap((symbol) => {
|
|
191
202
|
const key = text(symbol, 'capability symbol')
|
|
192
203
|
const found = bySymbol.get(key)
|
|
193
|
-
return found === undefined ? [] : [found]
|
|
204
|
+
return found === undefined ? [] : [inspectableMember(found)]
|
|
194
205
|
})
|
|
195
206
|
return {
|
|
196
207
|
symbols: selected,
|
|
@@ -23,6 +23,7 @@ const COUNTED_ENTRY_FIELDS = new Set([...ENTRY_FIELDS, 'reuseCount'])
|
|
|
23
23
|
const DEFINITION_FIELDS = new Set(['source', 'line', 'column'])
|
|
24
24
|
const MEMORY_META_FIELDS = new Set(['version', 'generation', 'memory'])
|
|
25
25
|
const PROJECTION_STATE_FIELDS = new Set(['generation', 'memory', 'pendingReplCalls'])
|
|
26
|
+
const VIEW_FIELDS = new Set(['runtime', 'memory'])
|
|
26
27
|
const PENDING_CALL_FIELDS = new Set(['callId', 'seq'])
|
|
27
28
|
const REPL_MEMORY_META_VERSION = 5
|
|
28
29
|
|
|
@@ -205,6 +206,24 @@ export function normalizeReplMemorySnapshot(value) {
|
|
|
205
206
|
})
|
|
206
207
|
}
|
|
207
208
|
|
|
209
|
+
/**
|
|
210
|
+
* One runtime's own observation of its environment, together with the owner that
|
|
211
|
+
* produced it.
|
|
212
|
+
*
|
|
213
|
+
* A panel shows an observation, not "the current state". Two questions then have
|
|
214
|
+
* to stay separate, and only the owner answers them: a projection refresh inside
|
|
215
|
+
* one runtime keeps the inventory valid (freshness), while a snapshot from a
|
|
216
|
+
* previous runtime can no longer be confirmed (provenance). Carrying the owner
|
|
217
|
+
* in the view lets a reader decide by data instead of by object identity.
|
|
218
|
+
*/
|
|
219
|
+
export function normalizeReplMemoryView(value) {
|
|
220
|
+
if (!exactFields(value, VIEW_FIELDS)) throw new Error('invalid dsh-ptc-plus REPL memory view')
|
|
221
|
+
return Object.freeze({
|
|
222
|
+
runtime: normalizeGeneration(value.runtime),
|
|
223
|
+
memory: normalizeReplMemorySnapshot(value.memory),
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
208
227
|
/** Reuse fields are version 5 additions; an older metadata version cannot own them. */
|
|
209
228
|
function countedMemory(memory) {
|
|
210
229
|
return memory?.reuseTotal !== undefined
|
|
@@ -349,8 +368,8 @@ export function createReplMemoryProjection(generation) {
|
|
|
349
368
|
return state
|
|
350
369
|
},
|
|
351
370
|
wire: Object.freeze({
|
|
352
|
-
viewSchema: Object.freeze({ parse:
|
|
353
|
-
view: state => state.memory,
|
|
371
|
+
viewSchema: Object.freeze({ parse: normalizeReplMemoryView }),
|
|
372
|
+
view: state => Object.freeze({ runtime: state.generation, memory: state.memory }),
|
|
354
373
|
}),
|
|
355
374
|
})
|
|
356
375
|
}
|
|
@@ -24,6 +24,8 @@ import {
|
|
|
24
24
|
toolCapabilityMetadata,
|
|
25
25
|
} from './program-bindings.js'
|
|
26
26
|
import { deepFreeze } from './record-utils.js'
|
|
27
|
+
import { adaptRunCodeSchema } from './direct-surface-owner.js'
|
|
28
|
+
import { hostTransportDefinition, isBatchSubmission } from './host-tool-sdk.js'
|
|
27
29
|
import { REPL_MEMORY_META_KEY, withReplMemorySnapshot } from './repl-memory-projection.js'
|
|
28
30
|
import { withUserBindingDraftCapability } from './user-binding-draft-projection.js'
|
|
29
31
|
import { POST_EDIT_KEY } from './session-journal-schema.js'
|
|
@@ -160,6 +162,7 @@ export function createRuntimeBridgeOwner({
|
|
|
160
162
|
userBindingDraftForAgent,
|
|
161
163
|
bindingSubmissionForAgent,
|
|
162
164
|
capabilityEnvironment,
|
|
165
|
+
readPromptText = () => undefined,
|
|
163
166
|
}) {
|
|
164
167
|
const scope = new AsyncLocalStorage()
|
|
165
168
|
const childScope = new AsyncLocalStorage()
|
|
@@ -185,6 +188,8 @@ export function createRuntimeBridgeOwner({
|
|
|
185
188
|
})
|
|
186
189
|
let currentConfig = sessions.config
|
|
187
190
|
const patchedDefinitions = new Map()
|
|
191
|
+
/** The host descriptors this attachment replaced on each definition's slots. */
|
|
192
|
+
const slotPatches = new Map()
|
|
188
193
|
const pending = new WeakMap()
|
|
189
194
|
const nativeRunCodeHandoffs = new Set()
|
|
190
195
|
let active = true
|
|
@@ -393,14 +398,94 @@ export function createRuntimeBridgeOwner({
|
|
|
393
398
|
return { request: { ...request, bindings: projected }, release }
|
|
394
399
|
}
|
|
395
400
|
|
|
401
|
+
/**
|
|
402
|
+
* Write one editable run_code slot onto the host's definition.
|
|
403
|
+
*
|
|
404
|
+
* A host may publish the slot as writable data or as a getter-only accessor over
|
|
405
|
+
* its own text, and strict-mode assignment throws on the latter. An owned property
|
|
406
|
+
* definition reaches both shapes, so the plugin never fails silently to deliver the
|
|
407
|
+
* text it was asked to deliver.
|
|
408
|
+
* @param {object} definition - the host's live run_code definition.
|
|
409
|
+
* @param {string} key - the definition field being replaced.
|
|
410
|
+
* @param {unknown} value - the text the plugin owns.
|
|
411
|
+
* @returns {boolean} whether the field now holds that value.
|
|
412
|
+
*/
|
|
413
|
+
function writeSlot(definition, key, value) {
|
|
414
|
+
const own = Object.getOwnPropertyDescriptor(definition, key)
|
|
415
|
+
try {
|
|
416
|
+
Object.defineProperty(definition, key, {
|
|
417
|
+
configurable: own?.configurable ?? true,
|
|
418
|
+
enumerable: own?.enumerable ?? true,
|
|
419
|
+
writable: true,
|
|
420
|
+
value,
|
|
421
|
+
})
|
|
422
|
+
return true
|
|
423
|
+
} catch {
|
|
424
|
+
return false
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Put one slot back to the descriptor the host published before this plugin. */
|
|
429
|
+
function restoreSlot(definition, key, descriptor) {
|
|
430
|
+
try {
|
|
431
|
+
if (descriptor === undefined) delete definition[key]
|
|
432
|
+
else Object.defineProperty(definition, key, descriptor)
|
|
433
|
+
} catch {
|
|
434
|
+
// A host that made the slot non-configurable after the plugin replaced it keeps
|
|
435
|
+
// the last written text; the restore cannot be forced, and the caller that
|
|
436
|
+
// rewrote it owns the consequence.
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* The editable run_code slots have to reach whichever definition the host lets this
|
|
442
|
+
* plugin own. Two writes, one decision: if the host's parameter texts cannot be
|
|
443
|
+
* replaced, the definition keeps all of its own text rather than half of ours. The
|
|
444
|
+
* descriptors the write replaced are recorded once, so re-applying the current
|
|
445
|
+
* prompt document on every request can never mistake the plugin's own text for the
|
|
446
|
+
* host's, and disposal can restore what the host still publishes.
|
|
447
|
+
*/
|
|
448
|
+
function applyEditableSlots(definition, promptText, slotPatches) {
|
|
449
|
+
if (promptText === undefined) return
|
|
450
|
+
let adapted
|
|
451
|
+
try {
|
|
452
|
+
adapted = adaptRunCodeSchema(definition, promptText)
|
|
453
|
+
} catch {
|
|
454
|
+
return
|
|
455
|
+
}
|
|
456
|
+
const recorded = slotPatches.get(definition)
|
|
457
|
+
if (recorded !== undefined) {
|
|
458
|
+
// Re-applied on every request: the descriptors recorded by the first write are
|
|
459
|
+
// the ones disposal restores, so a later application can never mistake the
|
|
460
|
+
// plugin's own text for the host's. A slot the host has since made unreplaceable
|
|
461
|
+
// keeps the text this attachment already delivered.
|
|
462
|
+
writeSlot(definition, 'parameters', adapted.parameters)
|
|
463
|
+
writeSlot(definition, 'description', adapted.description)
|
|
464
|
+
return
|
|
465
|
+
}
|
|
466
|
+
const ownParameters = Object.getOwnPropertyDescriptor(definition, 'parameters')
|
|
467
|
+
const ownDescription = Object.getOwnPropertyDescriptor(definition, 'description')
|
|
468
|
+
if (!writeSlot(definition, 'parameters', adapted.parameters)) return
|
|
469
|
+
if (!writeSlot(definition, 'description', adapted.description)) {
|
|
470
|
+
restoreSlot(definition, 'parameters', ownParameters)
|
|
471
|
+
return
|
|
472
|
+
}
|
|
473
|
+
slotPatches.set(definition, new Map([['parameters', ownParameters], ['description', ownDescription]]))
|
|
474
|
+
}
|
|
475
|
+
|
|
396
476
|
const patchRunCodeDefinition = (agent) => {
|
|
397
|
-
const definition = ctx.tools
|
|
477
|
+
const definition = hostTransportDefinition(ctx.tools, agent, RUN_CODE)
|
|
398
478
|
if (definition === undefined) {
|
|
399
479
|
throw new Error('ptc-plus: run_code definition is unavailable for the owning session')
|
|
400
480
|
}
|
|
401
481
|
if (definition.output === undefined) {
|
|
402
482
|
throw new Error('ptc-plus: run_code definition has no output projection')
|
|
403
483
|
}
|
|
484
|
+
// A host that declares its own model-facing tools never hands this plugin a
|
|
485
|
+
// tools array to adapt, so the editable prompt slots reach the model only on
|
|
486
|
+
// the definition the executor projects. Re-applied on every request, because
|
|
487
|
+
// the prompt document can change while the definition stays the same object.
|
|
488
|
+
applyEditableSlots(definition, readPromptText(), slotPatches)
|
|
404
489
|
if (patchedDefinitions.has(definition)) return
|
|
405
490
|
const output = definition.output
|
|
406
491
|
const ownPresentationMeta = Object.getOwnPropertyDescriptor(output, 'presentationMeta')
|
|
@@ -463,10 +548,10 @@ export function createRuntimeBridgeOwner({
|
|
|
463
548
|
// nested success. Capture its canonical value before policy; the private
|
|
464
549
|
// relation is admitted only if policy retains that same frozen value.
|
|
465
550
|
if (current?.deferredSettlement?.current === current && derived !== undefined
|
|
466
|
-
&& current.exec === exec && exec.token !== undefined
|
|
551
|
+
&& current.exec === exec && exec.token !== undefined
|
|
467
552
|
&& current.id === sessionId(exec.agent) && derived.agent === exec.agent
|
|
468
|
-
&& derived
|
|
469
|
-
&&
|
|
553
|
+
&& isDerivedExecution(derived, exec)
|
|
554
|
+
&& deepEqualJson(derived.arguments, exec.arguments)
|
|
470
555
|
&& current.presentationMetaCalls === undefined && result.isError === false
|
|
471
556
|
&& result.value !== null && typeof result.value === 'object'
|
|
472
557
|
&& Object.isFrozen(result.value) && result.meta?.[JOURNAL_KEY] === undefined
|
|
@@ -610,13 +695,30 @@ export function createRuntimeBridgeOwner({
|
|
|
610
695
|
|
|
611
696
|
const releaseSeam = seam.installExecute(patchedRun)
|
|
612
697
|
|
|
698
|
+
/**
|
|
699
|
+
* Whether this execution is the derived execution the edit transport submitted.
|
|
700
|
+
*
|
|
701
|
+
* A registry host runs the very object the plugin built, so the call id, the root
|
|
702
|
+
* call and the parent token it carries all prove the relation. The batch contract
|
|
703
|
+
* assigns its own identity instead: the parent token is the host's own and the
|
|
704
|
+
* root call is whichever call that host treats as direct. What survives there is
|
|
705
|
+
* the call id this plugin chose, which the host matched the batch result back to,
|
|
706
|
+
* together with the dispatch record; the derived source is compared beside it.
|
|
707
|
+
*/
|
|
708
|
+
const isDerivedExecution = (derived, exec) => {
|
|
709
|
+
if (derived === null || typeof derived !== 'object' || exec === null || typeof exec !== 'object') return false
|
|
710
|
+
if (derived.callId !== exec.callId) return false
|
|
711
|
+
if (isBatchSubmission(derived)) return true
|
|
712
|
+
return exec.parent !== undefined
|
|
713
|
+
&& derived.rootCallId === exec.rootCallId && derived.parent === exec.parent
|
|
714
|
+
}
|
|
715
|
+
|
|
613
716
|
const ownedDerivedScope = (exec, executionArguments = exec.arguments) => {
|
|
614
717
|
const inherited = scope.getStore()
|
|
615
718
|
const derived = inherited?.derivedExecution
|
|
616
719
|
if (inherited?.deferredSettlement === undefined
|
|
617
720
|
|| inherited.id !== sessionId(exec.agent) || derived?.agent !== exec.agent
|
|
618
|
-
|| derived
|
|
619
|
-
|| derived.parent !== exec.parent || exec.name !== RUN_CODE
|
|
721
|
+
|| !isDerivedExecution(derived, exec) || exec.name !== RUN_CODE
|
|
620
722
|
|| !deepEqualJson(derived.arguments, executionArguments)) return undefined
|
|
621
723
|
return inherited
|
|
622
724
|
}
|
|
@@ -800,8 +902,7 @@ export function createRuntimeBridgeOwner({
|
|
|
800
902
|
const derived = current.derivedExecution
|
|
801
903
|
const privateMetadata = projection !== undefined && projection.exec === exec
|
|
802
904
|
&& deferred.current === current && current.exec === exec && current.id === id
|
|
803
|
-
&& derived?.agent === exec.agent && derived
|
|
804
|
-
&& derived.rootCallId === exec.rootCallId && derived.parent === exec.parent
|
|
905
|
+
&& derived?.agent === exec.agent && isDerivedExecution(derived, exec)
|
|
805
906
|
&& deepEqualJson(derived.arguments, exec.arguments)
|
|
806
907
|
&& projection.token === exec.token && projection.value === result?.value
|
|
807
908
|
&& result.isError === false && deepEqualJson(projection.meta, result.meta)
|
|
@@ -868,6 +969,10 @@ export function createRuntimeBridgeOwner({
|
|
|
868
969
|
}
|
|
869
970
|
}
|
|
870
971
|
patchedDefinitions.clear()
|
|
972
|
+
for (const [definition, recorded] of slotPatches) {
|
|
973
|
+
for (const [key, descriptor] of recorded) restoreSlot(definition, key, descriptor)
|
|
974
|
+
}
|
|
975
|
+
slotPatches.clear()
|
|
871
976
|
try {
|
|
872
977
|
await sessions.dispose()
|
|
873
978
|
} catch (error) {
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from './prompt-text.js'
|
|
6
6
|
|
|
7
7
|
import { isPtcMessageSource, PTC_MESSAGE_SOURCE_KIND } from './message-sources.js'
|
|
8
|
+
import { BINDING_ADOPTION_FORM, readBindingAdoption } from './binding-adoption.js'
|
|
8
9
|
|
|
9
10
|
export const PTC_DELIVERY_CONTEXT = 'tools:ptc-plus-message-delivery'
|
|
10
11
|
export const PTC_BINDING_CATALOG = 'tools:ptc-plus-user-binding-defaults'
|
|
@@ -116,6 +117,10 @@ export function readRuntimeMessage(message) {
|
|
|
116
117
|
const text = message.content[0].text
|
|
117
118
|
const availability = readRuntimeAvailability(message)
|
|
118
119
|
if (availability !== undefined) return { form: 'runtime-availability', state: availability }
|
|
120
|
+
// A session's binding adoption is recorded as one of its own messages, so the
|
|
121
|
+
// log is its only owner and replay reconstructs it without process memory.
|
|
122
|
+
const adoption = readBindingAdoption(message)
|
|
123
|
+
if (adoption !== undefined) return { form: BINDING_ADOPTION_FORM, declined: [...adoption.declined] }
|
|
119
124
|
if (source.form === 'snapshot') {
|
|
120
125
|
const sections = stateSections(source.sections)
|
|
121
126
|
if (sections !== undefined && [STATE_PREFIX, PREVIOUS_STATE_PREFIX, LEGACY_STATE_PREFIX]
|