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.
Files changed (72) hide show
  1. package/README.en.md +4 -2
  2. package/README.md +4 -2
  3. package/client.cjs +3392 -2811
  4. package/compiler-core.cjs +127 -127
  5. package/compiler-core.cjs.map +2 -2
  6. package/docs/README.md +1 -0
  7. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +4 -2
  8. package/docs/adr/0019-plugin-settings-and-kill-switch.md +2 -2
  9. package/docs/adr/0023-global-user-bindings.md +12 -0
  10. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +1 -1
  11. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +2 -2
  12. package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +4 -4
  13. package/docs/adr/0046-gate-the-commit-with-the-semantic-evidence-tier.md +1 -1
  14. package/docs/adr/0048-supersede-a-rewritten-cell.md +48 -0
  15. package/docs/adr/0049-prove-a-settlement-against-its-published-record.md +53 -0
  16. package/docs/adr/0050-read-host-surfaces-through-their-published-contract.md +48 -0
  17. package/docs/adr/README.md +7 -1
  18. package/docs/architecture.md +1 -1
  19. package/docs/capability-planning-components.md +6 -6
  20. package/docs/capability-projection.md +1 -1
  21. package/docs/capability-surface.md +133 -0
  22. package/docs/client-ui.md +17 -15
  23. package/docs/client-visual-contract.md +7 -7
  24. package/docs/durability-design.md +9 -2
  25. package/docs/installation.md +17 -10
  26. package/docs/panel-concept.md +171 -0
  27. package/docs/releases/v0.5.2.md +26 -14
  28. package/docs/releases/v0.5.3.md +77 -0
  29. package/docs/releases/v0.5.4.md +75 -0
  30. package/docs/runtime-reference.md +21 -1
  31. package/docs/user-bindings.en.md +15 -0
  32. package/docs/user-bindings.md +15 -0
  33. package/docs/v0.5.3-goals.md +117 -0
  34. package/docs/verification.md +4 -4
  35. package/index.js +176 -41
  36. package/internal/binding-adoption.js +228 -0
  37. package/internal/capability-command-receipt.js +106 -8
  38. package/internal/capability-environment.js +1109 -312
  39. package/internal/capability-plan.js +18 -2
  40. package/internal/capability-policy.js +13 -0
  41. package/internal/capability-selection.js +330 -0
  42. package/internal/config-spec.js +14 -5
  43. package/internal/cordis-tools-owner.js +2 -1
  44. package/internal/direct-surface-owner.js +44 -17
  45. package/internal/edit-transport-owner.js +6 -5
  46. package/internal/execution-provider-owner.js +6 -0
  47. package/internal/execution-seam-compat.js +25 -0
  48. package/internal/host-tool-sdk.js +308 -0
  49. package/internal/message-sources.js +21 -0
  50. package/internal/official-repl-priority.js +31 -10
  51. package/internal/program-bindings.js +25 -5
  52. package/internal/repl-memory-projection.js +21 -2
  53. package/internal/runtime-bridge-owner.js +118 -9
  54. package/internal/runtime-config.js +10 -0
  55. package/internal/runtime-messages.js +5 -0
  56. package/internal/session-journal-recovery.js +210 -32
  57. package/internal/session-journal-schema.js +3 -0
  58. package/internal/session-journal.js +46 -9
  59. package/internal/session-log-view.js +69 -7
  60. package/internal/session-runtime.js +106 -39
  61. package/internal/settings-compat.js +6 -0
  62. package/internal/user-bindings-owner.js +126 -6
  63. package/package.json +59 -37
  64. package/src/client-authoring-view.js +7 -8
  65. package/src/client-capabilities-view.js +874 -909
  66. package/src/client-capability-sort.js +22 -9
  67. package/src/client-copy.js +118 -272
  68. package/src/client-indicator-view.js +2 -1
  69. package/src/client-repl-view.js +35 -16
  70. package/src/client-settings-view.js +90 -33
  71. package/src/client-styles.js +67 -19
  72. package/src/client.js +49 -22
@@ -1,4 +1,6 @@
1
1
  /** Resolved-spec Cordis execution contract. */
2
+ import { providerRetainsState } from './official-repl-priority.js'
3
+
2
4
  export const EXECUTION_SEAM_SERVICE = 'ptcRuntime'
3
5
 
4
6
  /** Request-only Cordis execution contract. */
@@ -63,7 +65,24 @@ export function createExecutionSeam(service, serviceName) {
63
65
  // Source evaluation form belongs to the provider that actually receives
64
66
  // the program. Program-format consumers delegate to the original provider,
65
67
  // so this descriptor always follows the original generation.
68
+ //
69
+ // Retention is different: while an installed adapter runs the program this
70
+ // provider keeps a per-session environment, so the answer describes what
71
+ // this provider does, not what the original would do. The ownership decision
72
+ // asks the original through `seam.originalRetainsState` instead.
66
73
  get programFormat() { requireCompatible(); return service.programFormat ?? 'function-body' },
74
+ // Retention belongs to the provider that actually receives the program, so
75
+ // this descriptor follows the original generation exactly like the source
76
+ // form. A host plugin that reads it must not see a stateful environment
77
+ // reported as stateless.
78
+ get retainsState() {
79
+ requireCompatible()
80
+ // While the installed adapter runs the program, this provider retains a
81
+ // per-session environment, whatever the original would do. Reporting the
82
+ // original's answer here would describe a stateful environment as
83
+ // stateless to every host plugin that asks.
84
+ return installed === undefined ? providerRetainsState(service) : true
85
+ },
67
86
  get executionInstructions() { requireCompatible(); return installed === undefined ? service.executionInstructions ?? '' : '' },
68
87
  get sandboxMode() { requireCompatible(); return installed === undefined ? service.sandboxMode : undefined },
69
88
  get timeout() { requireCompatible(); return installed === undefined ? service.timeout : undefined },
@@ -93,6 +112,12 @@ export function createExecutionSeam(service, serviceName) {
93
112
  })
94
113
  const seam = Object.freeze({
95
114
  serviceName,
115
+ /**
116
+ * Whether the original provider retains state per owner. Execution ownership
117
+ * is decided against the original — the provider that would receive the
118
+ * program if this attachment stood down — not against the composition.
119
+ */
120
+ get originalRetainsState() { requireAvailable(); return providerRetainsState(service) },
96
121
  get language() { requireAvailable(); return service.language },
97
122
  provider,
98
123
  get active() { return installed !== undefined && service.language === installed.language },
@@ -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
+ }
@@ -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'
@@ -1,27 +1,48 @@
1
1
  /**
2
2
  * Official persistent-REPL priority.
3
3
  *
4
- * The official `persistent-in-memory` execution mode is a live provider
5
- * capability, not a release fact: a provider reports `programFormat` `'repl'`
6
- * exactly when it retains a REPL per state owner. While the user selects the
7
- * official REPL priority and that provider is live, the plugin yields PTC
8
- * session execution to the official implementation and installs no session
9
- * runtime: `run_code`, its descriptors and its guidance stay official. The
10
- * decision is re-evaluated on every configuration change and host source
11
- * change, so either side can take over without restarting the host.
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 persistent REPL owns execution.
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?.programFormat === 'repl'
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.
@@ -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
- /** Derive SDK contracts from the public, scope-qualified tool registry. */
7
- export function toolSdkSchemas(tools, scope) {
8
- return tools.schemas(scope).filter(schema => schema.name !== 'run_code').map(schema => {
9
- const definition = tools.get(schema.name, scope)
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`)
@@ -175,6 +184,17 @@ export function capabilityFind(metadata, query) {
175
184
  .map(candidate => candidate.value)
176
185
  }
177
186
 
187
+ /**
188
+ * Inspection answers "how do I call this", so it hands back the declaration the
189
+ * model programs against. The JSON schema is how the plugin stores a signature,
190
+ * not how anyone reads one; keeping it in the payload made the model parse JSON
191
+ * to learn a call shape the source already states.
192
+ */
193
+ function inspectableMember(member) {
194
+ const { schema, ...rest } = member
195
+ return rest
196
+ }
197
+
178
198
  /** Batch, budgeted inspection; omitted items remain explicit to the caller. */
179
199
  export function capabilityInspect(metadata, symbols = undefined, budget = 50) {
180
200
  if (!Number.isSafeInteger(budget) || budget < 1) throw new TypeError('ptc-plus: capability inspection budget must be a positive integer')
@@ -190,7 +210,7 @@ export function capabilityInspect(metadata, symbols = undefined, budget = 50) {
190
210
  const selected = requested.slice(0, budget).flatMap((symbol) => {
191
211
  const key = text(symbol, 'capability symbol')
192
212
  const found = bySymbol.get(key)
193
- return found === undefined ? [] : [found]
213
+ return found === undefined ? [] : [inspectableMember(found)]
194
214
  })
195
215
  return {
196
216
  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: normalizeReplMemorySnapshot }),
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
  }