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,136 @@
|
|
|
1
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
2
|
+
import { isPtcMessageSource, PTC_MESSAGE_SOURCE_KIND } from './message-sources.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Session binding adoption: which Global User Binding catalog entries this
|
|
6
|
+
* session uses.
|
|
7
|
+
*
|
|
8
|
+
* The question is answered per session and per entry, and the answer is a
|
|
9
|
+
* decision of exactly two states: this session uses the entry, or it does not.
|
|
10
|
+
* An entry the session never named is used, because the global catalog already
|
|
11
|
+
* answered the supply question. That default is the absence of a decision, not
|
|
12
|
+
* a third state: "use" and "never mentioned" lead to the same environment, so
|
|
13
|
+
* they are one state rather than two.
|
|
14
|
+
*
|
|
15
|
+
* Adoption can only take an entry away from this session. It cannot supply one,
|
|
16
|
+
* enable one, or name a source: an entry the global configuration disabled is
|
|
17
|
+
* absent from the catalog, so "adoption revives a deleted entry" has no
|
|
18
|
+
* encoding here and needs no rule forbidding it. Naming an entry the catalog
|
|
19
|
+
* does not offer is rejected at the surface that states the decision, because
|
|
20
|
+
* the record would otherwise claim authority over supply.
|
|
21
|
+
*
|
|
22
|
+
* The neighbour this must not be confused with is the capability pane's
|
|
23
|
+
* `automatic`, which means "the host derives this value". A binding has no host
|
|
24
|
+
* derivation to defer to, so borrowing that word would misdescribe both.
|
|
25
|
+
*
|
|
26
|
+
* The decision is recorded as one PTC message in the session log, so it is
|
|
27
|
+
* reconstructed by replaying that log after a restart instead of being kept in
|
|
28
|
+
* process memory.
|
|
29
|
+
*/
|
|
30
|
+
export const BINDING_ADOPTION_FORM = 'binding-adoption'
|
|
31
|
+
export const BINDING_ADOPTION_SCHEMA = 'ptc-binding-adoption/v1'
|
|
32
|
+
|
|
33
|
+
const MAX_DECLINED = 64
|
|
34
|
+
const ENTRY_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/
|
|
35
|
+
const PREFIX = 'PTC Plus session binding adoption. This session uses every entry the global catalog supplies except the declined ones. Adoption can only withdraw an entry from this session: it cannot supply, enable or revive one, and the global configuration decides which entries exist at all. Declined entry IDs:'
|
|
36
|
+
|
|
37
|
+
function isRecord(value) {
|
|
38
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sortIds(ids) {
|
|
42
|
+
return [...ids].sort((left, right) => left < right ? -1 : left > right ? 1 : 0)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function normalizeBindingAdoption(value) {
|
|
46
|
+
if (!isRecord(value) || Reflect.ownKeys(value).length !== 2
|
|
47
|
+
|| !Object.hasOwn(value, 'schema') || !Object.hasOwn(value, 'declined')) {
|
|
48
|
+
throw new TypeError('invalid binding adoption')
|
|
49
|
+
}
|
|
50
|
+
if (value.schema !== BINDING_ADOPTION_SCHEMA) throw new TypeError('invalid binding adoption schema')
|
|
51
|
+
if (!Array.isArray(value.declined) || value.declined.length > MAX_DECLINED) {
|
|
52
|
+
throw new TypeError('invalid binding adoption declined entries')
|
|
53
|
+
}
|
|
54
|
+
const declined = []
|
|
55
|
+
for (const id of value.declined) {
|
|
56
|
+
if (typeof id !== 'string' || !ENTRY_ID.test(id)) throw new TypeError('invalid binding adoption entry id')
|
|
57
|
+
if (declined.includes(id)) throw new TypeError('duplicate binding adoption entry id')
|
|
58
|
+
declined.push(id)
|
|
59
|
+
}
|
|
60
|
+
return Object.freeze({ schema: BINDING_ADOPTION_SCHEMA, declined: Object.freeze(sortIds(declined)) })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The exact model-visible wording one adoption record carries. */
|
|
64
|
+
export function bindingAdoptionText(adoption) {
|
|
65
|
+
return `${PREFIX}\n${JSON.stringify(normalizeBindingAdoption(adoption).declined)}`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function bindingAdoptionMessage(adoption) {
|
|
69
|
+
const text = bindingAdoptionText(adoption)
|
|
70
|
+
const message = createUserMessage({
|
|
71
|
+
source: { kind: PTC_MESSAGE_SOURCE_KIND, form: BINDING_ADOPTION_FORM },
|
|
72
|
+
content: [{ type: 'text', text }],
|
|
73
|
+
})
|
|
74
|
+
if (readBindingAdoption(message) === undefined) throw new TypeError('invalid binding adoption message')
|
|
75
|
+
return message
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Read one adoption record back from a message, message projection or message fact. */
|
|
79
|
+
export function readBindingAdoption(value) {
|
|
80
|
+
const source = value?.source
|
|
81
|
+
if (!isPtcMessageSource(source) || source.form !== BINDING_ADOPTION_FORM) return undefined
|
|
82
|
+
if (!Array.isArray(value.content) || value.content.length !== 1 || value.content[0]?.type !== 'text') return undefined
|
|
83
|
+
const text = value.content[0].text
|
|
84
|
+
if (typeof text !== 'string' || !text.startsWith(`${PREFIX}\n`)) return undefined
|
|
85
|
+
try {
|
|
86
|
+
const declined = JSON.parse(text.slice(PREFIX.length + 1))
|
|
87
|
+
const adoption = normalizeBindingAdoption({ schema: BINDING_ADOPTION_SCHEMA, declined })
|
|
88
|
+
return bindingAdoptionText(adoption) === text ? adoption : undefined
|
|
89
|
+
} catch {
|
|
90
|
+
return undefined
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The newest adoption this session recorded, or `undefined` while it recorded
|
|
96
|
+
* none. The newest record replaces earlier ones, because stating a decision
|
|
97
|
+
* again is how a session changes its mind.
|
|
98
|
+
*/
|
|
99
|
+
export function latestBindingAdoption(records) {
|
|
100
|
+
if (!Array.isArray(records)) return undefined
|
|
101
|
+
for (let index = records.length - 1; index >= 0; index -= 1) {
|
|
102
|
+
const record = records[index]
|
|
103
|
+
if (record?.form !== BINDING_ADOPTION_FORM) continue
|
|
104
|
+
try {
|
|
105
|
+
return normalizeBindingAdoption({ schema: BINDING_ADOPTION_SCHEMA, declined: record.declined })
|
|
106
|
+
} catch {
|
|
107
|
+
// A record that cannot be read is not evidence for an earlier record
|
|
108
|
+
// either: the newest statement is the current one, and it is unreadable.
|
|
109
|
+
return undefined
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return undefined
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The declined entry IDs of one adoption, as a set the environment uses.
|
|
117
|
+
* An adoption that was never stated declines nothing.
|
|
118
|
+
*/
|
|
119
|
+
export function declinedBindingEntries(adoption) {
|
|
120
|
+
return new Set(adoption === undefined ? [] : normalizeBindingAdoption(adoption).declined)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** State one entry as used (`adopt` true) or not used, leaving the rest alone. */
|
|
124
|
+
export function withBindingAdoption(adoption, entryId, adopt) {
|
|
125
|
+
if (typeof entryId !== 'string' || !ENTRY_ID.test(entryId)) throw new TypeError('invalid binding adoption entry id')
|
|
126
|
+
if (typeof adopt !== 'boolean') throw new TypeError('binding adoption decision must be a boolean')
|
|
127
|
+
const declined = declinedBindingEntries(adoption)
|
|
128
|
+
if (adopt) declined.delete(entryId)
|
|
129
|
+
else declined.add(entryId)
|
|
130
|
+
return normalizeBindingAdoption({ schema: BINDING_ADOPTION_SCHEMA, declined: [...declined] })
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Drop every declined entry: this session again uses the whole catalog. */
|
|
134
|
+
export function clearedBindingAdoption() {
|
|
135
|
+
return normalizeBindingAdoption({ schema: BINDING_ADOPTION_SCHEMA, declined: [] })
|
|
136
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
/** Structured command outcome for locale-owned Client receipts; text remains a CLI fallback. */
|
|
2
|
+
import { capabilityProofText, normalizeCapabilityProof } from './capability-delivery-proof.js'
|
|
3
|
+
|
|
2
4
|
const PREFIX = '\n\nPTC Plus capability refresh receipt:\n'
|
|
3
5
|
const FALLBACKS = Object.freeze({
|
|
4
6
|
'saved-only': 'Global choice saved, but the session update failed; inspect and retry session',
|
|
@@ -11,13 +13,25 @@ const FALLBACKS = Object.freeze({
|
|
|
11
13
|
})
|
|
12
14
|
const STATES = new Set(['recorded', 'pending', 'unavailable', 'usage', 'saved-only', 'paused', 'off'])
|
|
13
15
|
|
|
14
|
-
export function capabilityCommandReceipt(state, sessionId, error, proposal) {
|
|
16
|
+
export function capabilityCommandReceipt(state, sessionId, error, proposal, proof) {
|
|
15
17
|
if (!STATES.has(state) || typeof sessionId !== 'string' || !sessionId) throw new TypeError('invalid capability command receipt')
|
|
16
18
|
if (error !== undefined && (state !== 'saved-only' || typeof error !== 'string' || !error)) throw new TypeError('invalid capability command error')
|
|
17
19
|
if (proposal !== undefined && (state !== 'pending' || !proposal || typeof proposal.commandId !== 'string'
|
|
18
20
|
|| typeof proposal.rawInput !== 'string' || !proposal.message)) throw new TypeError('invalid capability proposal receipt')
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
// A command that names one capability reports that capability's three layers
|
|
22
|
+
// beside the session outcome, so "saved" cannot be read as "delivered" or
|
|
23
|
+
// "usable". The schema stays v1 because the field is additive: receipts
|
|
24
|
+
// recorded before it existed still reproduce their own text.
|
|
25
|
+
const layers = proof === undefined ? undefined : normalizeCapabilityProof(proof)
|
|
26
|
+
return `${FALLBACKS[state]} ${sessionId}.${error ? ` ${error}` : ''}`
|
|
27
|
+
+ (layers === undefined ? '' : ` ${capabilityProofText(layers)}`)
|
|
28
|
+
+ PREFIX
|
|
29
|
+
+ JSON.stringify({
|
|
30
|
+
schema: 'ptc-capability-command/v1', state, sessionId,
|
|
31
|
+
...(error === undefined ? {} : { error }),
|
|
32
|
+
...(proposal === undefined ? {} : { proposal }),
|
|
33
|
+
...(layers === undefined ? {} : { proof: layers }),
|
|
34
|
+
})
|
|
21
35
|
}
|
|
22
36
|
|
|
23
37
|
export function readCapabilityCommandReceipt(text) {
|
|
@@ -26,6 +40,6 @@ export function readCapabilityCommandReceipt(text) {
|
|
|
26
40
|
const receipt = JSON.parse(text.slice(text.indexOf(PREFIX) + PREFIX.length))
|
|
27
41
|
return receipt?.schema === 'ptc-capability-command/v1' && STATES.has(receipt.state)
|
|
28
42
|
&& typeof receipt.sessionId === 'string' && receipt.sessionId
|
|
29
|
-
&& capabilityCommandReceipt(receipt.state, receipt.sessionId, receipt.error, receipt.proposal) === text ? receipt : undefined
|
|
43
|
+
&& capabilityCommandReceipt(receipt.state, receipt.sessionId, receipt.error, receipt.proposal, receipt.proof) === text ? receipt : undefined
|
|
30
44
|
} catch { return undefined }
|
|
31
45
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One capability, three separate facts.
|
|
3
|
+
*
|
|
4
|
+
* Saving a global choice changes supply: the configuration now records a
|
|
5
|
+
* decision. It does not prove the model ever received that decision, and it does
|
|
6
|
+
* not prove a cell can reach the capability. Those are different questions with
|
|
7
|
+
* different owners:
|
|
8
|
+
*
|
|
9
|
+
* - **saved**: the global configuration records a choice for this capability.
|
|
10
|
+
* - **delivered**: the model-visible capability catalog published it at an
|
|
11
|
+
* accepted step, rather than waiting for the next one or never listing it.
|
|
12
|
+
* - **usable**: a completed execution in this session actually reached it.
|
|
13
|
+
*
|
|
14
|
+
* Usability is only ever proved by an execution result. Nothing else in the
|
|
15
|
+
* product can claim it: a saved switch, a published catalog entry and an
|
|
16
|
+
* installed tool schema are all statements about intent, not about a value a
|
|
17
|
+
* cell obtained. Reporting "saved, delivered, usability unproven" is therefore
|
|
18
|
+
* the honest answer to "did that save take effect", and it is what a layer that
|
|
19
|
+
* cannot prove more must say instead of claiming effect.
|
|
20
|
+
*/
|
|
21
|
+
export const CAPABILITY_PROOF_SCHEMA = 'ptc-capability-proof/v1'
|
|
22
|
+
export const CAPABILITY_DELIVERY_STATES = new Set(['delivered', 'pending', 'absent'])
|
|
23
|
+
export const CAPABILITY_USABILITY_STATES = new Set(['proven', 'unproven'])
|
|
24
|
+
|
|
25
|
+
const CAPABILITY_ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/
|
|
26
|
+
|
|
27
|
+
function isRecord(value) {
|
|
28
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function normalizeCapabilityProof(value) {
|
|
32
|
+
if (!isRecord(value) || Reflect.ownKeys(value).length !== 5
|
|
33
|
+
|| !Object.hasOwn(value, 'schema') || !Object.hasOwn(value, 'id') || !Object.hasOwn(value, 'saved')
|
|
34
|
+
|| !Object.hasOwn(value, 'delivery') || !Object.hasOwn(value, 'usability')) {
|
|
35
|
+
throw new TypeError('invalid capability proof')
|
|
36
|
+
}
|
|
37
|
+
if (value.schema !== CAPABILITY_PROOF_SCHEMA) throw new TypeError('invalid capability proof schema')
|
|
38
|
+
if (typeof value.id !== 'string' || !CAPABILITY_ID.test(value.id)) throw new TypeError('invalid capability proof id')
|
|
39
|
+
if (typeof value.saved !== 'boolean') throw new TypeError('invalid capability proof saved fact')
|
|
40
|
+
if (!CAPABILITY_DELIVERY_STATES.has(value.delivery)) throw new TypeError('invalid capability proof delivery state')
|
|
41
|
+
if (!CAPABILITY_USABILITY_STATES.has(value.usability)) throw new TypeError('invalid capability proof usability state')
|
|
42
|
+
return Object.freeze({
|
|
43
|
+
schema: CAPABILITY_PROOF_SCHEMA,
|
|
44
|
+
id: value.id,
|
|
45
|
+
saved: value.saved,
|
|
46
|
+
delivery: value.delivery,
|
|
47
|
+
usability: value.usability,
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The model-visible sentence for one proof. Each layer states its own result, so
|
|
53
|
+
* a reader can tell which layer failed instead of reading one blanket "applied".
|
|
54
|
+
*/
|
|
55
|
+
export function capabilityProofText(proof) {
|
|
56
|
+
const normalized = normalizeCapabilityProof(proof)
|
|
57
|
+
const id = normalized.id
|
|
58
|
+
const saved = normalized.saved
|
|
59
|
+
? `Saved: yes. The global configuration records a choice for ${id}.`
|
|
60
|
+
: `Saved: no. The global configuration records no choice for ${id}, so this save did not happen; the session choice below is the only change.`
|
|
61
|
+
const delivery = {
|
|
62
|
+
delivered: `Delivered: yes. The model-visible capability catalog published ${id} at an accepted step.`,
|
|
63
|
+
pending: `Delivered: not yet. The saved configuration reaches the model-visible catalog at the next accepted step, so ${id} is not in it now.`,
|
|
64
|
+
absent: `Delivered: no. The published capability catalog does not list ${id}, so the model never received it.`,
|
|
65
|
+
}[normalized.delivery]
|
|
66
|
+
const usability = normalized.usability === 'proven'
|
|
67
|
+
? `Usable: proven. A completed execution in this session reached ${id}.`
|
|
68
|
+
: `Usable: unproven. No completed execution in this session reached ${id}; only such a result would prove it, and a saved or published entry does not.`
|
|
69
|
+
return `${saved} ${delivery} ${usability}`
|
|
70
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { capabilityRule, capabilityPolicyForConfig, changeCapabilityRule, effectiveCapabilitySettings, CAPABILITY_DISCLOSURES, CAPABILITY_INVENTORY_CATEGORIES } from './capability-policy.js'
|
|
2
2
|
/** One session-owned capability view for presentation, discovery and preparation. */
|
|
3
3
|
import { createHash } from 'node:crypto'
|
|
4
|
-
import { jsonSchemaToTs, renderToolsSdk, ToolNotFoundError } from '
|
|
4
|
+
import { jsonSchemaToTs, renderToolsSdk, ToolNotFoundError } from './host-tool-sdk.js'
|
|
5
5
|
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
6
6
|
import { isModelInvocable, isUserInvocable } from '@deepseek-ai/dsh-skill'
|
|
7
7
|
import { createScope } from '@deepseek-ai/dsh-scope'
|
|
@@ -9,6 +9,7 @@ import { Context } from '@deepseek-ai/cordis'
|
|
|
9
9
|
import * as spillPolicy from '@deepseek-ai/dsh-spill-policy'
|
|
10
10
|
import { readSessionLog } from './session-events.js'
|
|
11
11
|
import { readCapabilityCommandReceipt } from './capability-command-receipt.js'
|
|
12
|
+
import { normalizeCapabilityProof } from './capability-delivery-proof.js'
|
|
12
13
|
import { createCapabilityPlan } from './capability-plan.js'
|
|
13
14
|
import { PTC_MESSAGE_SOURCE_KIND } from './message-sources.js'
|
|
14
15
|
import { deepFreeze } from './record-utils.js'
|
|
@@ -1306,6 +1307,42 @@ export function createCapabilityEnvironment({ ctx, config, toolSchemasForAgent,
|
|
|
1306
1307
|
member === agent ? status : member.status))
|
|
1307
1308
|
}
|
|
1308
1309
|
},
|
|
1310
|
+
/**
|
|
1311
|
+
* The saved, delivered and usable facts of one capability. A caller that
|
|
1312
|
+
* only has "the save succeeded" must not report more than this returns.
|
|
1313
|
+
*/
|
|
1314
|
+
describeCapabilityProof(agent, id) {
|
|
1315
|
+
if (typeof id !== 'string' || id.length === 0) throw new TypeError('ptc-plus: capability proof requires a capability id')
|
|
1316
|
+
const state = stateFor(agent)
|
|
1317
|
+
const sourceSettings = state?.snapshot.sourceSettings ?? state?.snapshot.settings
|
|
1318
|
+
?? capabilityConfiguration(currentConfig)
|
|
1319
|
+
// A global choice is recorded either as a legacy override or as a rule in the
|
|
1320
|
+
// policy this owner writes for `/capabilities set global`, so both spellings
|
|
1321
|
+
// prove the same save.
|
|
1322
|
+
const capability = state?.snapshot.capabilities.find(item => item.id === id)
|
|
1323
|
+
const recorded = capability === undefined || sourceSettings.policy === undefined
|
|
1324
|
+
? undefined : capabilityRule(sourceSettings.policy, capability)
|
|
1325
|
+
const saved = Object.hasOwn(sourceSettings.manual?.overrides ?? {}, id)
|
|
1326
|
+
|| recorded?.availabilitySource === `item:${id}`
|
|
1327
|
+
|| recorded?.disclosureSource === `item:${id}`
|
|
1328
|
+
// Delivery is a statement about the model-visible catalog. A plan that holds an
|
|
1329
|
+
// entry whose choice is closed describes an entry the catalog does not list, so
|
|
1330
|
+
// holding it proves nothing was delivered.
|
|
1331
|
+
const entry = state?.snapshot.plan.entries.find(item => item.id === id)
|
|
1332
|
+
const listed = entry !== undefined && entry.selection !== 'closed'
|
|
1333
|
+
const delivery = !listed ? 'absent'
|
|
1334
|
+
: state.message === undefined || planCommitted(agent, state.message) ? 'delivered' : 'pending'
|
|
1335
|
+
return normalizeCapabilityProof({
|
|
1336
|
+
schema: 'ptc-capability-proof/v1',
|
|
1337
|
+
id,
|
|
1338
|
+
saved,
|
|
1339
|
+
delivery,
|
|
1340
|
+
// Only a completed execution in this session proves usability; the
|
|
1341
|
+
// worker's installed schemas and the published catalog prove nothing
|
|
1342
|
+
// about what a cell actually obtained.
|
|
1343
|
+
usability: state?.used?.has(id) === true ? 'proven' : 'unproven',
|
|
1344
|
+
})
|
|
1345
|
+
},
|
|
1309
1346
|
describe(agent) {
|
|
1310
1347
|
const state = stateFor(agent)
|
|
1311
1348
|
return state === undefined ? { configured: capabilityConfiguration(currentConfig), effective: null }
|
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
|
}
|
|
@@ -628,12 +640,14 @@ export function createDirectSurfaceOwner({
|
|
|
628
640
|
if (sessionPtc) await capabilityEnvironment?.prepare(agent, assembly.sections)
|
|
629
641
|
const userBindings = sessionPtc ? await userBindingsForAgent(agent) : undefined
|
|
630
642
|
if (!isCurrent()) return assembly
|
|
631
|
-
const runCode = tools
|
|
643
|
+
const runCode = tools?.find(tool => tool?.name === RUN_CODE)
|
|
632
644
|
const promptText = currentPromptText
|
|
633
|
-
|
|
645
|
+
const hostDeclaresTools = tools !== undefined
|
|
646
|
+
let directTools = tools === undefined ? undefined
|
|
647
|
+
: sessionPtc ? capabilityEnvironment?.visibleSchemas(agent, tools) ?? tools : tools
|
|
634
648
|
if (sessionPtcProjection) {
|
|
635
649
|
editTransport.ensureInstalled(agent)
|
|
636
|
-
directTools = [adaptRunCodeSchema(runCode, promptText)]
|
|
650
|
+
if (hostDeclaresTools) directTools = [adaptRunCodeSchema(runCode, promptText)]
|
|
637
651
|
}
|
|
638
652
|
const runtimeContexts = sessionPtc
|
|
639
653
|
? sessionRuntimeContexts(agent, tipConfig, {
|
|
@@ -685,11 +699,11 @@ export function createDirectSurfaceOwner({
|
|
|
685
699
|
|
|
686
700
|
return {
|
|
687
701
|
...assembly,
|
|
688
|
-
tools: sessionPtcProjection
|
|
702
|
+
...(directTools === undefined ? {} : { tools: sessionPtcProjection
|
|
689
703
|
? directTools
|
|
690
704
|
: directTools.map(tool => tool?.name === RUN_CODE
|
|
691
705
|
? adaptRunCodeSchema(tool, promptText)
|
|
692
|
-
: tool),
|
|
706
|
+
: tool) }),
|
|
693
707
|
sections,
|
|
694
708
|
}
|
|
695
709
|
},
|
|
@@ -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 },
|
|
@@ -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 },
|