dsh-ptc-plus 0.5.1 → 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 +10 -4
- package/README.md +10 -4
- package/client.cjs +1623 -104
- package/compiler-core.cjs +132 -132
- package/compiler-core.cjs.map +3 -3
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +2 -0
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +4 -2
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +2 -2
- package/docs/adr/0023-global-user-bindings.md +12 -0
- package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +11 -1
- 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/0032-own-session-log-facts-in-a-host-only-projection.md +31 -1
- package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +4 -4
- package/docs/adr/0045-customize-model-visible-prompt-text.md +38 -0
- package/docs/adr/0046-gate-the-commit-with-the-semantic-evidence-tier.md +35 -0
- package/docs/adr/0047-store-journal-argument-digests-in-the-projection.md +88 -0
- 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 +3 -3
- package/docs/client-ui.md +5 -2
- package/docs/durability-design.md +15 -2
- package/docs/installation.md +9 -9
- package/docs/releases/v0.5.2.md +91 -0
- package/docs/releases/v0.5.3.md +77 -0
- package/docs/runtime-reference.md +43 -3
- package/docs/unmeasured-cell-recovery-repair-proposal.md +387 -0
- 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 +7 -3
- package/index.js +100 -31
- package/internal/binding-adoption.js +136 -0
- package/internal/bounded-literal.js +85 -0
- package/internal/capability-command-receipt.js +18 -4
- package/internal/capability-delivery-proof.js +70 -0
- package/internal/capability-environment.js +200 -11
- package/internal/capability-plan.js +11 -8
- package/internal/cell-error.js +13 -0
- package/internal/config-spec.js +126 -6
- package/internal/cordis-tools-owner.js +2 -1
- package/internal/direct-surface-owner.js +39 -21
- package/internal/edit-transport-owner.js +81 -15
- 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/isolated-worker.js +134 -17
- package/internal/kernel-worker.js +167 -3
- package/internal/legacy-cell-analysis.js +14 -2
- package/internal/legacy-repl-convenience.js +16 -3
- package/internal/official-repl-priority.js +31 -10
- package/internal/program-bindings.js +12 -1
- package/internal/projection-retention.js +238 -0
- package/internal/prompt-text.js +858 -0
- package/internal/recovery-tips.js +50 -11
- package/internal/rejected-cell-editor.js +3 -2
- package/internal/repl-memory-projection.js +21 -2
- package/internal/runtime-bridge-owner.js +114 -9
- package/internal/runtime-config.js +2 -0
- package/internal/runtime-contexts.js +14 -5
- package/internal/runtime-messages.js +48 -15
- package/internal/session-cell-executor.js +132 -11
- package/internal/session-journal-recovery.js +1204 -116
- package/internal/session-journal-schema.js +10 -5
- package/internal/session-journal.js +214 -14
- package/internal/session-log-view.js +133 -26
- package/internal/session-runtime.js +746 -38
- package/internal/session-state.js +27 -0
- package/internal/stateful-root-compiler.js +11 -0
- package/internal/stateful-root-runtime.js +17 -0
- package/internal/user-binding-console.js +7 -0
- package/internal/user-bindings-owner.js +112 -6
- package/internal/user-bindings.js +2 -2
- package/internal/validated-parse-repair.js +6 -1
- package/internal/worker-client.js +27 -4
- package/package.json +16 -6
- package/src/client-activity.js +24 -4
- package/src/client-capabilities-view.js +504 -27
- package/src/client-copy.js +187 -2
- package/src/client-indicator-view.js +2 -1
- package/src/client-repl-view.js +35 -11
- package/src/client-styles.js +64 -2
- package/src/client.js +3 -1
|
@@ -0,0 +1,858 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-visible prompt text the plugin authors, expressed as anchors and slots.
|
|
3
|
+
*
|
|
4
|
+
* An anchor is one delivered surface (a system section, a tool schema, a runtime
|
|
5
|
+
* context, a result template). A slot is one piece of that surface with its own
|
|
6
|
+
* ownership: `A` may be edited, `B` is read-only and says which code owns the
|
|
7
|
+
* bytes, `C` belongs to someone else and is only mirrored. Slot kinds are a
|
|
8
|
+
* separate axis from reason codes: `projected` says bytes come from facts, a
|
|
9
|
+
* reason code says why a slot cannot be edited.
|
|
10
|
+
*
|
|
11
|
+
* Two rules keep this owner honest:
|
|
12
|
+
* - Every literal default is byte-identical to the text its composer used before
|
|
13
|
+
* overrides existed, so an empty document reproduces today's prompt.
|
|
14
|
+
* - Resolution is total: an unknown, oversize, read-only or empty-required
|
|
15
|
+
* override falls back to the default and reports why. Only saving validates
|
|
16
|
+
* strictly, because only saving has a user who can fix the input.
|
|
17
|
+
*
|
|
18
|
+
* Budgets are measured in **effective UTF-8 bytes**, meaning the bytes the request
|
|
19
|
+
* would carry: an overridden slot counts its override, every other registered slot
|
|
20
|
+
* counts its literal default, and an unknown id counts only its own override. The
|
|
21
|
+
* limits are 4096 per slot, 16384 per anchor and 65536 per document.
|
|
22
|
+
*/
|
|
23
|
+
import {
|
|
24
|
+
CLASS_REDECLARATION_REWRITE as REWRITE_CLASS,
|
|
25
|
+
FUNCTION_REDECLARATION_REWRITE as REWRITE_FUNCTION,
|
|
26
|
+
MIXED_REDECLARATION_REWRITE as REWRITE_MIXED,
|
|
27
|
+
} from './repl-rewrite-contract.js'
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
/** Persisted document shape for prompt-text overrides. */
|
|
31
|
+
export const PROMPT_TEXT_SCHEMA = 'ptc-prompt-text/v1'
|
|
32
|
+
/** Largest effective text one slot may deliver, in UTF-8 bytes. */
|
|
33
|
+
export const PROMPT_TEXT_MAX_SLOT_BYTES = 4096
|
|
34
|
+
/** Largest effective text the slots of one anchor may deliver together. */
|
|
35
|
+
export const PROMPT_TEXT_MAX_ANCHOR_BYTES = 16384
|
|
36
|
+
/** Largest effective text one configuration may deliver. */
|
|
37
|
+
export const PROMPT_TEXT_MAX_DOCUMENT_BYTES = 65536
|
|
38
|
+
|
|
39
|
+
/** Delivery channels, in the order the panel lists them. */
|
|
40
|
+
export const PROMPT_CHANNELS = Object.freeze(['prompt', 'install', 'plan', 'runtime', 'result'])
|
|
41
|
+
/** Slot kinds. A kind never doubles as a reason code. */
|
|
42
|
+
export const PROMPT_SLOT_KINDS = Object.freeze(['literal', 'aggregate', 'projected'])
|
|
43
|
+
/** Why a slot is read-only. Every non-editable slot names exactly one. */
|
|
44
|
+
export const PROMPT_REASON_CODES = Object.freeze([
|
|
45
|
+
'fact-derived',
|
|
46
|
+
'aggregate-frozen',
|
|
47
|
+
'protocol-exact-text',
|
|
48
|
+
'protocol-structural',
|
|
49
|
+
'shadows-host',
|
|
50
|
+
'other-owner',
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
/** Runtime snapshot prefixes: recognizer input, so they stay code-owned. */
|
|
54
|
+
export const STATE_PREFIX = 'PTC Plus recovery status. Replaces earlier PTC recovery status, including PTC state in runtime-context snapshots. Global binding APIs are unchanged.'
|
|
55
|
+
export const PREVIOUS_STATE_PREFIX = 'PTC Plus runtime recovery state. This replaces only earlier PTC state snapshots and PTC sections in historical aggregate snapshots; it does not replace the global binding API catalog, tasks, Skill instructions, tool results, or other producers.'
|
|
56
|
+
export const LEGACY_STATE_PREFIX = 'PTC Plus current state. This replaces only earlier PTC state snapshots and PTC sections in historical aggregate snapshots; it does not replace tasks, Skill instructions, tool results, or other producers.'
|
|
57
|
+
/** Catalog envelope text; the prefix is recognizer input and the empty sentences are prose. */
|
|
58
|
+
export const CATALOG_PREFIX = 'Global binding API reference for run_code (replaces the previous global binding reference):'
|
|
59
|
+
export const EMPTY_CATALOG = 'No entries documented.'
|
|
60
|
+
export const PREVIOUS_EMPTY_CATALOG = 'No global binding API documentation is currently configured. Earlier binding catalogs no longer apply.'
|
|
61
|
+
/**
|
|
62
|
+
* The four delivered tip bodies. One slot per delivered body, so every branch a
|
|
63
|
+
* model can receive is editable; the prefix and the detail sentences stay
|
|
64
|
+
* code-owned because escalation decides which one appears.
|
|
65
|
+
*/
|
|
66
|
+
export const TIP_BODY = 'Inspect bounded runtime facts with {platformFacts}, then choose the executable and path in the current execution world. Use direct argv or a shell only when required; do not assume Windows, WSL, POSIX, or one shell. Do not dump process.env.'
|
|
67
|
+
export const TIP_BODY_PLATFORM_DETAILED = 'Re-check the active execution world and the actual executable before retrying. First inspect bounded runtime facts with {platformFacts}; use the result to choose the executable and path. Use direct argv for a normal executable; use a shell only when its syntax or resolution is required. Windows, WSL, POSIX, and package shims have different paths and launch rules. Do not dump process.env.'
|
|
68
|
+
export const TIP_BODY_REPEATED_BINDING = 'Inspect the visible cell source and results for the local name, scope, initialization, or declaration conflict. Correct that expression before continuing; capability discovery does not list session-local variables.{detail}'
|
|
69
|
+
export const TIP_BODY_REPEATED_BINDING_CAPABILITY = 'Inspect the live request with `capabilities.tree()`, `capabilities.find()`, or `capabilities.inspect()`, then call the typed member through `tools.*`.{detail}'
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Fillers for the `{detail}` hole; escalation decides which one appears.
|
|
73
|
+
*
|
|
74
|
+
* Both are registered read-only slots (`tip.detail.binding`, `tip.detail.capability`):
|
|
75
|
+
* the renderer still decides *whether* the hole is filled, but the delivered bytes
|
|
76
|
+
* come from the registry so accounting sees them and the surface gate can claim them.
|
|
77
|
+
*/
|
|
78
|
+
export const TIP_DETAIL_BINDING = ' Earlier statements may have run; use current expression evidence without assuming a failed declaration initialized its binding.'
|
|
79
|
+
export const TIP_DETAIL_CAPABILITY = ' Do not invent hidden bindings or repeat the same failing expression.'
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The two delivered tip prefixes: one per trigger kind, each its own slot.
|
|
83
|
+
*
|
|
84
|
+
* The recognizer identifies a tip by its notice name (`recoveryTipIdentity`), not
|
|
85
|
+
* by this text, so the prefixes are `protocol-structural` rather than exact-text.
|
|
86
|
+
* They are measured and read-only: escalation decides which trigger fires, but
|
|
87
|
+
* both sentences are plugin-authored bytes, so accounting must see them.
|
|
88
|
+
*/
|
|
89
|
+
export const TIP_PREFIX_REPEATED_BINDING = 'The same binding failure has recurred.'
|
|
90
|
+
export const TIP_PREFIX_PLATFORM_COMMAND = 'An executable, shell, or path failed in the current execution world.'
|
|
91
|
+
|
|
92
|
+
/** Default discovery guidance delivered with the capability plan. */
|
|
93
|
+
export const PLAN_NAVIGATION = 'Use capabilities.search({query}) to retrieve complete tool, Skill and MCP contracts. Unlisted capabilities remain discoverable. Skill descriptions are summaries; follow each Skill\'s declared invocation eligibility to load its body before following it.'
|
|
94
|
+
|
|
95
|
+
/** One guidance line per capability category, in delivery order. */
|
|
96
|
+
export const PLAN_CATEGORY_LINES = Object.freeze({
|
|
97
|
+
tool: 'Tools: search by task or tools.<name> for parameters and results.',
|
|
98
|
+
skill: 'Skills: search by task or skills.<name> for when to use and how to load instructions.',
|
|
99
|
+
resource: 'MCP resources: search by server or resource purpose.',
|
|
100
|
+
'resource-template': 'MCP templates: search by server or resource purpose.',
|
|
101
|
+
task: 'Tasks: search by the operation you need.',
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
/** Sentence delivered when the selected full contracts exceed their budget. */
|
|
105
|
+
export const PLAN_OVERAGE = 'Explicit full contracts exceed the configured budget by {overageBytes} UTF-8 bytes.'
|
|
106
|
+
|
|
107
|
+
/** Accepted hole in a projected slot; a save must keep exactly one occurrence. */
|
|
108
|
+
export const PLAN_OVERAGE_HOLE = '{overageBytes}'
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Result-rendering envelope for a cell that exceeded the output budget.
|
|
113
|
+
*
|
|
114
|
+
* The recognizer reads the byte count this sentence reports, so the sentence is
|
|
115
|
+
* measured and read-only; the hole is the only variable part.
|
|
116
|
+
*/
|
|
117
|
+
export const RESULT_ENVELOPE = 'output exceeded {maxOutputBytes} bytes; this cell was discarded and the worker reset. Only bindings in the verified recovery frontier can be restored. External effects may already have occurred and are not undone. For future cells, bound console output and return a smaller value; do not assume this cell\'s values remain available.'
|
|
118
|
+
|
|
119
|
+
/** Accepted hole in the result envelope. */
|
|
120
|
+
export const RESULT_ENVELOPE_HOLE = '{maxOutputBytes}'
|
|
121
|
+
|
|
122
|
+
const GUIDANCE_OPENING = '`run_code` continues one persistent PTC REPL. Use Node.js modules in the cell; use external processes when their environment is needed. Ordinary top-level bindings remain available to later cells, so reuse them instead of resending setup code. Choose the smallest cell that answers the request and return only the value the next step needs.'
|
|
123
|
+
|
|
124
|
+
const GUIDANCE_ERROR_RECOVERY = 'After an execution error, earlier statements may have taken effect. Inspect relevant values in a short cell, then continue from them; repeat external operations only when their retry rules and current results establish safety. `edit_run_code` edits and reruns a complete cell.'
|
|
125
|
+
|
|
126
|
+
const GUIDANCE_CELL_CONVENTIONS = '## Cell conventions\nUse `return` for an explicit result and `console` for logs. Return supported values directly, including undefined, NaN, infinities, -0 and BigInt; JSON.stringify loses distinctions or rejects BigInt. Do not mix BigInt with Number arithmetic; use a BigInt literal or convert explicitly. Keep functions, Date, Map and other unsupported values in bindings; return needed data or type observations. Native `tools.*` arguments must satisfy their own JSON schemas; PTC value transport does not widen those contracts. Keep large inspection results in bindings or reduce them to targeted excerpts: `tools.read` is bounded inspection, not a lossless whole-file reader. Relative filesystem, path and default child-process operations use the current working directory at the time they run; a successful in-cell `tools.working_directory({ cd })` applies to the rest of that cell, while cell imports and `import.meta` keep the directory the cell started in, and `process.chdir` is unavailable.'
|
|
127
|
+
|
|
128
|
+
const GUIDANCE_CAPABILITIES = '## Available capabilities\nUse `capabilities.tree()`, `capabilities.find()`, and `capabilities.inspect()` for unfamiliar program APIs; Reserve `code.run` for isolated execution of source held as data.\nProgram API references expire when their cell ends. Reusable helpers must read the current namespace when called, for example `async function readNow(args) { return tools.read(args) }`, instead of retaining an earlier `tools` object or method. Completed-cell callbacks cannot call program APIs.'
|
|
129
|
+
|
|
130
|
+
const GUIDANCE_EXECUTION_WORLD = 'Native tool availability depends on the current DSH profile and execution world; inspect executables, shells, and paths instead of assuming Windows, WSL, POSIX, or a particular shell.'
|
|
131
|
+
|
|
132
|
+
const RUN_CODE_TOOL_DESCRIPTION = 'Evaluate the next TypeScript cell in this session-bound persistent REPL, reusing earlier top-level bindings. Use `code` for the cell and `description` for its short UI summary. Successful image-bearing subtool results are attached after the cell.'
|
|
133
|
+
const RUN_CODE_CODE_DESCRIPTION = 'Code for the next PTC TypeScript cell, following the selected language conventions.'
|
|
134
|
+
const RUN_CODE_DESCRIPTION_DESCRIPTION = 'Short active-voice summary of what this cell does, 5-10 words (shown in the UI).'
|
|
135
|
+
|
|
136
|
+
/** Default description of the registered `edit_run_code` tool. */
|
|
137
|
+
export const EDIT_RUN_CODE_DESCRIPTION = 'Call this from within a run_code cell. It accepts an edit of the most recent eligible cell captured at nested dispatch and returns { accepted, scheduled, target_call_seq } when scheduled. The corrected cell runs after the current run_code cell releases its worker; its result is reported with that cell, and a successful edit becomes the next target. This reruns the entire target cell, including prior effects; use it after pre-execution rejection or when repeating those effects is safe under the called API\'s retry rules. Otherwise continue in a new run_code cell using retained values. A diagnostic may supply a localized correction; use it when it matches your intent and preserve its expected_target_call_seq. Send exactly one edits or regex_edits array: edits replaces unique literal fragments; regex_edits replaces a counted set of pattern matches. All ranges must be non-overlapping in the original cell. Regex replacements use JavaScript $1/$<name> captures, not \\1.'
|
|
138
|
+
|
|
139
|
+
/** Label the plugin writes before each delivered binding entry. */
|
|
140
|
+
const USER_BINDINGS_ENTRY_LABEL = 'Binding'
|
|
141
|
+
|
|
142
|
+
/** One editable slot: its bytes are the user's, and omitting is an explicit choice. */
|
|
143
|
+
function editableSlot(id, kind, omittable, defaultText) {
|
|
144
|
+
if (!PROMPT_SLOT_KINDS.includes(kind)) throw new TypeError(`ptc-plus: unknown prompt slot kind ${kind} (${id})`)
|
|
145
|
+
return Object.freeze({
|
|
146
|
+
id, kind, owner: 'A', level: 'prose', reason: null, omittable, defaultText, source: 'registry-literal',
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* One read-only slot.
|
|
152
|
+
*
|
|
153
|
+
* `defaultText` is the literal the composer uses when the slot is a fixed string.
|
|
154
|
+
* A generated slot passes `null` and names its `source`, because its bytes only
|
|
155
|
+
* exist once the request is assembled; byte accounting marks it unmeasured rather
|
|
156
|
+
* than pretending it is empty.
|
|
157
|
+
*/
|
|
158
|
+
function readOnlySlot(id, kind, reason, { source, defaultText = null, omittable = false, owner = 'B' }) {
|
|
159
|
+
if (!PROMPT_SLOT_KINDS.includes(kind)) throw new TypeError(`ptc-plus: unknown prompt slot kind ${kind} (${id})`)
|
|
160
|
+
if (!PROMPT_REASON_CODES.includes(reason)) throw new TypeError(`ptc-plus: unknown prompt reason code ${reason}`)
|
|
161
|
+
if (owner !== 'B' && owner !== 'C') throw new TypeError(`ptc-plus: read-only slot ${id} must belong to B or C`)
|
|
162
|
+
return Object.freeze({
|
|
163
|
+
id, kind, owner, level: 'protocol', reason, omittable, defaultText, source,
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* One aggregate container slot.
|
|
169
|
+
*
|
|
170
|
+
* The set itself is frozen: adding or removing an item would move its neighbours'
|
|
171
|
+
* bytes and the connectors between them, so the row is read-only with reason
|
|
172
|
+
* `aggregate-frozen`. The container owns no text of its own, so it has no literal
|
|
173
|
+
* default and is excluded from byte accounting; the items carry the bytes and stay
|
|
174
|
+
* individually editable.
|
|
175
|
+
*/
|
|
176
|
+
function aggregateSlot(id, items) {
|
|
177
|
+
return Object.freeze({
|
|
178
|
+
id, kind: 'aggregate', owner: 'B', level: 'protocol', reason: 'aggregate-frozen',
|
|
179
|
+
omittable: false, defaultText: null, source: 'registry-set', items: Object.freeze(items),
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Every delivered anchor. `presence` states when the anchor reaches a request, so
|
|
185
|
+
* the panel can show "not delivered now" without hiding the row.
|
|
186
|
+
*
|
|
187
|
+
* `wired` says whether a composer already delivers these slots through the
|
|
188
|
+
* resolver. A declared but unwired anchor is listed with its channel, presence and
|
|
189
|
+
* reason codes, renders no control, and contributes no bytes to a counter: its
|
|
190
|
+
* slots have no literal default, so accounting marks them unmeasured instead of
|
|
191
|
+
* reporting a measured zero.
|
|
192
|
+
*/
|
|
193
|
+
export const PROMPT_ANCHORS = Object.freeze([
|
|
194
|
+
Object.freeze({
|
|
195
|
+
id: 'tools:ptc-plus-repl',
|
|
196
|
+
channel: 'prompt',
|
|
197
|
+
wired: true,
|
|
198
|
+
presence: 'run_code 已注册',
|
|
199
|
+
slots: Object.freeze([
|
|
200
|
+
editableSlot('guidance.opening', 'literal', false, GUIDANCE_OPENING),
|
|
201
|
+
editableSlot('guidance.errorRecovery', 'literal', true, GUIDANCE_ERROR_RECOVERY),
|
|
202
|
+
editableSlot('guidance.cellConventions', 'literal', true, GUIDANCE_CELL_CONVENTIONS),
|
|
203
|
+
editableSlot('guidance.capabilities', 'literal', true, GUIDANCE_CAPABILITIES),
|
|
204
|
+
editableSlot('guidance.executionWorld', 'literal', true, GUIDANCE_EXECUTION_WORLD),
|
|
205
|
+
readOnlySlot('guidance.derivedPolicy', 'projected', 'fact-derived', {
|
|
206
|
+
source: 'internal/binding-update-policy.js guidancePolicies()',
|
|
207
|
+
}),
|
|
208
|
+
]),
|
|
209
|
+
}),
|
|
210
|
+
Object.freeze({
|
|
211
|
+
id: 'tools:ptc-plus-user-binding-defaults',
|
|
212
|
+
channel: 'prompt',
|
|
213
|
+
wired: true,
|
|
214
|
+
presence: '全局绑定启用',
|
|
215
|
+
slots: Object.freeze([
|
|
216
|
+
editableSlot('userBindings.entryLabel', 'literal', false, USER_BINDINGS_ENTRY_LABEL),
|
|
217
|
+
readOnlySlot('userBindings.entryInstructions', 'literal', 'other-owner', {
|
|
218
|
+
owner: 'C',
|
|
219
|
+
source: 'internal/user-bindings.js (entry instructions supplied by the user)',
|
|
220
|
+
}),
|
|
221
|
+
]),
|
|
222
|
+
}),
|
|
223
|
+
Object.freeze({
|
|
224
|
+
id: 'run_code',
|
|
225
|
+
channel: 'prompt',
|
|
226
|
+
wired: true,
|
|
227
|
+
presence: '每次请求',
|
|
228
|
+
slots: Object.freeze([
|
|
229
|
+
editableSlot('runCode.description', 'literal', false, RUN_CODE_TOOL_DESCRIPTION),
|
|
230
|
+
editableSlot('runCode.codeArg', 'literal', false, RUN_CODE_CODE_DESCRIPTION),
|
|
231
|
+
editableSlot('runCode.descriptionArg', 'literal', false, RUN_CODE_DESCRIPTION_DESCRIPTION),
|
|
232
|
+
]),
|
|
233
|
+
}),
|
|
234
|
+
Object.freeze({
|
|
235
|
+
id: 'edit_run_code',
|
|
236
|
+
channel: 'install',
|
|
237
|
+
wired: true,
|
|
238
|
+
presence: '下一次 agent 安装',
|
|
239
|
+
slots: Object.freeze([
|
|
240
|
+
editableSlot('editRunCode.description', 'literal', false, EDIT_RUN_CODE_DESCRIPTION),
|
|
241
|
+
]),
|
|
242
|
+
}),
|
|
243
|
+
Object.freeze({
|
|
244
|
+
id: 'binding-catalog',
|
|
245
|
+
channel: 'runtime',
|
|
246
|
+
wired: true,
|
|
247
|
+
presence: '有绑定条目',
|
|
248
|
+
slots: Object.freeze([
|
|
249
|
+
readOnlySlot('catalog.prefix', 'literal', 'protocol-exact-text', { source: 'registry-literal', defaultText: CATALOG_PREFIX }),
|
|
250
|
+
editableSlot('catalog.empty', 'literal', false, EMPTY_CATALOG),
|
|
251
|
+
// The previous-generation empty sentence is historical wording, not delivered
|
|
252
|
+
// text: no composer emits it, and `HISTORICAL_EMPTY_CATALOGS` keeps it readable
|
|
253
|
+
// for records written by another version. It is therefore deliberately not a
|
|
254
|
+
// slot — an editable slot nobody delivers would inflate the accounting.
|
|
255
|
+
readOnlySlot('catalog.entryInstructions', 'literal', 'other-owner', {
|
|
256
|
+
owner: 'C',
|
|
257
|
+
source: 'internal/user-bindings.js (entry instructions supplied by the user)',
|
|
258
|
+
}),
|
|
259
|
+
]),
|
|
260
|
+
}),
|
|
261
|
+
Object.freeze({
|
|
262
|
+
id: 'tools:sdk#capability-appendix',
|
|
263
|
+
channel: 'prompt',
|
|
264
|
+
wired: false,
|
|
265
|
+
presence: '能力管理启用',
|
|
266
|
+
slots: Object.freeze([
|
|
267
|
+
readOnlySlot('capabilities.appendixProse', 'literal', 'shadows-host', {
|
|
268
|
+
source: 'internal/direct-surface-owner.js capabilitySdk()',
|
|
269
|
+
}),
|
|
270
|
+
readOnlySlot('capabilities.declarations', 'literal', 'protocol-structural', {
|
|
271
|
+
source: 'internal/direct-surface-owner.js capabilitySdk() declare block',
|
|
272
|
+
}),
|
|
273
|
+
]),
|
|
274
|
+
}),
|
|
275
|
+
Object.freeze({
|
|
276
|
+
id: 'plan.navigation',
|
|
277
|
+
channel: 'plan',
|
|
278
|
+
wired: true,
|
|
279
|
+
presence: '新会话或 /capabilities refresh',
|
|
280
|
+
slots: Object.freeze([
|
|
281
|
+
editableSlot('plan.navigationSentence', 'literal', false, PLAN_NAVIGATION),
|
|
282
|
+
]),
|
|
283
|
+
}),
|
|
284
|
+
Object.freeze({
|
|
285
|
+
id: 'plan.categories',
|
|
286
|
+
channel: 'plan',
|
|
287
|
+
wired: true,
|
|
288
|
+
presence: '新会话或 /capabilities refresh',
|
|
289
|
+
slots: Object.freeze([
|
|
290
|
+
aggregateSlot('plan.categories.set', Object.keys(PLAN_CATEGORY_LINES).map(category => `plan.category.${category}`)),
|
|
291
|
+
...Object.entries(PLAN_CATEGORY_LINES).map(([category, line]) =>
|
|
292
|
+
editableSlot(`plan.category.${category}`, 'literal', false, line)),
|
|
293
|
+
]),
|
|
294
|
+
}),
|
|
295
|
+
Object.freeze({
|
|
296
|
+
id: 'plan.overage',
|
|
297
|
+
channel: 'plan',
|
|
298
|
+
wired: true,
|
|
299
|
+
presence: '预算超限时',
|
|
300
|
+
slots: Object.freeze([
|
|
301
|
+
editableSlot('plan.overageSentence', 'projected', false, PLAN_OVERAGE),
|
|
302
|
+
]),
|
|
303
|
+
}),
|
|
304
|
+
Object.freeze({
|
|
305
|
+
id: 'runtime.recovery-snapshot',
|
|
306
|
+
channel: 'runtime',
|
|
307
|
+
wired: true,
|
|
308
|
+
presence: '恢复代际触发',
|
|
309
|
+
slots: Object.freeze([
|
|
310
|
+
readOnlySlot('recovery.snapshotPrefix', 'literal', 'protocol-exact-text', { source: 'registry-literal', defaultText: STATE_PREFIX }),
|
|
311
|
+
readOnlySlot('recovery.previousStatePrefix', 'literal', 'protocol-exact-text', { source: 'registry-literal', defaultText: PREVIOUS_STATE_PREFIX }),
|
|
312
|
+
readOnlySlot('recovery.legacyStatePrefix', 'literal', 'protocol-exact-text', { source: 'registry-literal', defaultText: LEGACY_STATE_PREFIX }),
|
|
313
|
+
]),
|
|
314
|
+
}),
|
|
315
|
+
Object.freeze({
|
|
316
|
+
id: 'runtime.tip',
|
|
317
|
+
channel: 'runtime',
|
|
318
|
+
wired: true,
|
|
319
|
+
presence: '触发命中',
|
|
320
|
+
slots: Object.freeze([
|
|
321
|
+
editableSlot('tip.body.platform', 'projected', false, TIP_BODY),
|
|
322
|
+
editableSlot('tip.body.platformDetailed', 'projected', false, TIP_BODY_PLATFORM_DETAILED),
|
|
323
|
+
editableSlot('tip.body.repeatedBinding', 'projected', false, TIP_BODY_REPEATED_BINDING),
|
|
324
|
+
editableSlot('tip.body.repeatedBindingCapability', 'projected', false, TIP_BODY_REPEATED_BINDING_CAPABILITY),
|
|
325
|
+
readOnlySlot('tip.prefix.repeatedBinding', 'literal', 'protocol-structural', {
|
|
326
|
+
source: 'registry-literal',
|
|
327
|
+
defaultText: TIP_PREFIX_REPEATED_BINDING,
|
|
328
|
+
}),
|
|
329
|
+
readOnlySlot('tip.prefix.platformCommand', 'literal', 'protocol-structural', {
|
|
330
|
+
source: 'registry-literal',
|
|
331
|
+
defaultText: TIP_PREFIX_PLATFORM_COMMAND,
|
|
332
|
+
}),
|
|
333
|
+
// The detail sentences fill the body's `{detail}` hole when a failure escalates.
|
|
334
|
+
// Escalation stays a code decision; the sentences are registered so their bytes
|
|
335
|
+
// are accounted and claimed rather than silently delivered outside the registry.
|
|
336
|
+
readOnlySlot('tip.detail.binding', 'projected', 'fact-derived', {
|
|
337
|
+
source: 'registry-literal',
|
|
338
|
+
defaultText: TIP_DETAIL_BINDING,
|
|
339
|
+
}),
|
|
340
|
+
readOnlySlot('tip.detail.capability', 'projected', 'fact-derived', {
|
|
341
|
+
source: 'registry-literal',
|
|
342
|
+
defaultText: TIP_DETAIL_CAPABILITY,
|
|
343
|
+
}),
|
|
344
|
+
]),
|
|
345
|
+
}),
|
|
346
|
+
Object.freeze({
|
|
347
|
+
id: 'result.rewrite-feedback',
|
|
348
|
+
channel: 'result',
|
|
349
|
+
wired: true,
|
|
350
|
+
presence: '改写发生',
|
|
351
|
+
slots: Object.freeze([
|
|
352
|
+
editableSlot('rewrite.mixed', 'literal', false, REWRITE_MIXED),
|
|
353
|
+
editableSlot('rewrite.function', 'literal', false, REWRITE_FUNCTION),
|
|
354
|
+
editableSlot('rewrite.class', 'literal', false, REWRITE_CLASS),
|
|
355
|
+
]),
|
|
356
|
+
}),
|
|
357
|
+
Object.freeze({
|
|
358
|
+
id: 'result.diagnostics-and-budget',
|
|
359
|
+
channel: 'result',
|
|
360
|
+
wired: true,
|
|
361
|
+
presence: '失败或超限',
|
|
362
|
+
slots: Object.freeze([
|
|
363
|
+
readOnlySlot('result.diagnosticsProse', 'literal', 'fact-derived', {
|
|
364
|
+
source: 'internal/recovery-tips.js advice and budget prose',
|
|
365
|
+
}),
|
|
366
|
+
readOnlySlot('result.diagnosticsCodes', 'literal', 'protocol-structural', {
|
|
367
|
+
source: 'internal/session-journal-schema.js PTC-* codes and help structure',
|
|
368
|
+
}),
|
|
369
|
+
// The envelope template is measured (the plugin writes every byte of it)
|
|
370
|
+
// but not writable: the recognizer reads the byte count it reports.
|
|
371
|
+
readOnlySlot('result.envelope', 'projected', 'fact-derived', {
|
|
372
|
+
source: 'registry-literal',
|
|
373
|
+
defaultText: RESULT_ENVELOPE,
|
|
374
|
+
}),
|
|
375
|
+
]),
|
|
376
|
+
}),
|
|
377
|
+
])
|
|
378
|
+
|
|
379
|
+
/** Flat slot list, in anchor order. */
|
|
380
|
+
export const PROMPT_SLOTS = Object.freeze(PROMPT_ANCHORS.flatMap(anchor => anchor.slots))
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Slot ids as constants.
|
|
384
|
+
*
|
|
385
|
+
* Composers import these instead of writing bare strings, and the coverage test
|
|
386
|
+
* enforces both directions: no bare literal in a composer, every constant used.
|
|
387
|
+
*/
|
|
388
|
+
const slotConstantName = id => id.replace(/[.:-]+(.)?/gu, (_, next) => (next ?? '').toUpperCase())
|
|
389
|
+
export const PROMPT_SLOT_IDS = Object.freeze(Object.fromEntries(
|
|
390
|
+
PROMPT_SLOTS.filter(slot => slot.source === 'registry-literal').map(slot => [slotConstantName(slot.id), slot.id]),
|
|
391
|
+
))
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Backwards-compatible alias for the flat slot list.
|
|
395
|
+
*
|
|
396
|
+
* Composer-side code and the settings pane still read `PROMPT_ASSETS` as a flat
|
|
397
|
+
* list of editable rows; it is the same objects, so the two names cannot drift.
|
|
398
|
+
*/
|
|
399
|
+
export const PROMPT_ASSETS = PROMPT_SLOTS
|
|
400
|
+
|
|
401
|
+
const ANCHORS_BY_ID = new Map(PROMPT_ANCHORS.map(anchor => [anchor.id, anchor]))
|
|
402
|
+
const SLOTS_BY_ID = new Map(PROMPT_SLOTS.map(slot => [slot.id, slot]))
|
|
403
|
+
const ANCHOR_OF_SLOT = new Map(PROMPT_ANCHORS.flatMap(anchor => anchor.slots.map(slot => [slot.id, anchor.id])))
|
|
404
|
+
|
|
405
|
+
/** Default persisted document; an empty configuration reproduces today's prompt. */
|
|
406
|
+
export const PROMPT_TEXT_DEFAULT = JSON.stringify({ schema: PROMPT_TEXT_SCHEMA, overrides: {} })
|
|
407
|
+
|
|
408
|
+
/** Slot id of one category guidance line. */
|
|
409
|
+
export const planCategorySlotId = category => `plan.category.${category}`
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Every file that resolves slot ids through `PROMPT_SLOT_IDS`.
|
|
413
|
+
*
|
|
414
|
+
* The coverage test derives its scan from this list, so adding a composer means
|
|
415
|
+
* registering it here instead of remembering to widen a test-local array.
|
|
416
|
+
*/
|
|
417
|
+
export const PROMPT_COMPOSER_FILES = Object.freeze([
|
|
418
|
+
'index.js',
|
|
419
|
+
'internal/direct-surface-owner.js',
|
|
420
|
+
'internal/edit-transport-owner.js',
|
|
421
|
+
'internal/capability-plan.js',
|
|
422
|
+
'internal/capability-environment.js',
|
|
423
|
+
'internal/runtime-messages.js',
|
|
424
|
+
'internal/recovery-tips.js',
|
|
425
|
+
])
|
|
426
|
+
|
|
427
|
+
/** The anchor with this id. */
|
|
428
|
+
export function promptAnchor(id) {
|
|
429
|
+
const anchor = ANCHORS_BY_ID.get(id)
|
|
430
|
+
if (anchor === undefined) throw new TypeError(`ptc-plus: unknown prompt anchor ${id}`)
|
|
431
|
+
return anchor
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** The anchor that delivers this slot. */
|
|
435
|
+
export function promptAnchorOfSlot(id) {
|
|
436
|
+
const anchorId = ANCHOR_OF_SLOT.get(id)
|
|
437
|
+
if (anchorId === undefined) throw new TypeError(`ptc-plus: unknown prompt slot ${id}`)
|
|
438
|
+
return ANCHORS_BY_ID.get(anchorId)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** One registered slot. */
|
|
442
|
+
export function promptSlot(id) {
|
|
443
|
+
const slot = SLOTS_BY_ID.get(id)
|
|
444
|
+
if (slot === undefined) throw new TypeError(`ptc-plus: unknown prompt slot ${id}`)
|
|
445
|
+
return slot
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** Backwards-compatible name for the slot lookup used by composers. */
|
|
449
|
+
export const promptAsset = promptSlot
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Anchor → slot ids, derived from the registry.
|
|
453
|
+
*
|
|
454
|
+
* The request-surface gate imports this instead of keeping its own copy, so a
|
|
455
|
+
* registered slot can never be missing from the surface map.
|
|
456
|
+
*/
|
|
457
|
+
export function anchorSlotMap() {
|
|
458
|
+
return Object.freeze(Object.fromEntries(
|
|
459
|
+
PROMPT_ANCHORS.map(anchor => [anchor.id, Object.freeze(anchor.slots.map(slot => slot.id))]),
|
|
460
|
+
))
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** Text a composer emits for one slot: the override when usable, else the default. */
|
|
464
|
+
export function promptAssetText(promptText, id) {
|
|
465
|
+
const slot = promptSlot(id)
|
|
466
|
+
if (slot.defaultText === null) {
|
|
467
|
+
throw new TypeError(`ptc-plus: prompt slot ${id} is generated by ${slot.source}; composers may only request registry literals`)
|
|
468
|
+
}
|
|
469
|
+
return promptText === undefined || typeof promptText.text !== 'function'
|
|
470
|
+
? slot.defaultText
|
|
471
|
+
: promptText.text(id)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// TextEncoder keeps the registry usable from both the worker realm and the
|
|
475
|
+
// client bundle, where Node's Buffer does not exist.
|
|
476
|
+
const byteLength = value => new TextEncoder().encode(value).length
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Apply one projected hole, keeping the registered template when the hole is
|
|
480
|
+
* missing: a document that drops the hole would otherwise deliver an envelope
|
|
481
|
+
* whose reported byte count no longer identifies the cell it describes.
|
|
482
|
+
*/
|
|
483
|
+
function fillHole(template, hole, value) {
|
|
484
|
+
return template.includes(hole) ? template.replace(hole, String(value)) : template
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Rewrite descriptions a cell delivers: override per kind, else the default. */
|
|
488
|
+
export function rewriteDescriptions(promptText) {
|
|
489
|
+
return Object.freeze({
|
|
490
|
+
mixed: promptAssetText(promptText, PROMPT_SLOT_IDS.rewriteMixed),
|
|
491
|
+
function: promptAssetText(promptText, PROMPT_SLOT_IDS.rewriteFunction),
|
|
492
|
+
class: promptAssetText(promptText, PROMPT_SLOT_IDS.rewriteClass),
|
|
493
|
+
})
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** The result-rendering envelope for a cell that exceeded its output budget. */
|
|
497
|
+
export function resultEnvelopeText(promptText, maxOutputBytes) {
|
|
498
|
+
return fillHole(promptAssetText(promptText, PROMPT_SLOT_IDS.resultEnvelope), RESULT_ENVELOPE_HOLE, maxOutputBytes)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** Placeholders a projected default uses, in order of appearance. */
|
|
502
|
+
const holeTokens = text => typeof text === 'string' ? (text.match(/\{[a-zA-Z][a-zA-Z0-9]*\}/gu) ?? []) : []
|
|
503
|
+
|
|
504
|
+
/** Bytes a slot delivers: its override when present, else its literal default. */
|
|
505
|
+
function effectiveBytes(slot, overrides) {
|
|
506
|
+
const text = overrides.has(slot.id) ? overrides.get(slot.id) : slot.defaultText
|
|
507
|
+
return typeof text === 'string' ? byteLength(text) : 0
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** Effective bytes of every registered slot in one document. */
|
|
511
|
+
function sumEffectiveBytes(overrides) {
|
|
512
|
+
let total = 0
|
|
513
|
+
for (const slot of PROMPT_SLOTS) total += effectiveBytes(slot, overrides)
|
|
514
|
+
return total
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** Overrides for ids this plugin version does not know still count toward the total. */
|
|
518
|
+
function unknownOverrideBytes(overrides) {
|
|
519
|
+
let total = 0
|
|
520
|
+
for (const [id, text] of overrides) {
|
|
521
|
+
if (SLOTS_BY_ID.has(id)) continue
|
|
522
|
+
total += byteLength(text)
|
|
523
|
+
}
|
|
524
|
+
return total
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function isRecord(value) {
|
|
528
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Shape validation shared by both layers.
|
|
533
|
+
*
|
|
534
|
+
* Throws only for a document nothing can read: not a JSON string, a foreign
|
|
535
|
+
* schema, or a non-string override. Slot and anchor budgets belong to the save
|
|
536
|
+
* path, because render has to keep working from a document saving would reject.
|
|
537
|
+
*/
|
|
538
|
+
export function parsePromptTextDocument(value) {
|
|
539
|
+
if (typeof value !== 'string') throw new TypeError('ptc-plus: promptText must be a JSON string')
|
|
540
|
+
let parsed
|
|
541
|
+
try {
|
|
542
|
+
parsed = JSON.parse(value)
|
|
543
|
+
} catch {
|
|
544
|
+
throw new TypeError('ptc-plus: promptText must contain valid JSON')
|
|
545
|
+
}
|
|
546
|
+
if (!isRecord(parsed) || parsed.schema !== PROMPT_TEXT_SCHEMA || !isRecord(parsed.overrides)) {
|
|
547
|
+
throw new TypeError(`ptc-plus: promptText must be a ${PROMPT_TEXT_SCHEMA} document with an overrides object`)
|
|
548
|
+
}
|
|
549
|
+
for (const [id, text] of Object.entries(parsed.overrides)) {
|
|
550
|
+
if (!id.trim()) throw new TypeError('ptc-plus: promptText override ids must not be empty')
|
|
551
|
+
if (typeof text !== 'string') throw new TypeError(`ptc-plus: promptText override ${id} must be a string`)
|
|
552
|
+
}
|
|
553
|
+
return parsed
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Strict validation for the write path and `resolveConfig`.
|
|
558
|
+
*
|
|
559
|
+
* Adds what only a save can fix: the per-slot, per-anchor and per-document
|
|
560
|
+
* ceilings, a non-empty value for every required slot, and a refusal to write a
|
|
561
|
+
* read-only slot. Unknown ids are accepted shape-wise and counted in the document
|
|
562
|
+
* total, so a document written by another plugin version still loads.
|
|
563
|
+
*/
|
|
564
|
+
export function validatePromptTextDocument(value) {
|
|
565
|
+
const parsed = parsePromptTextDocument(value)
|
|
566
|
+
const overrides = new Map()
|
|
567
|
+
for (const [id, text] of Object.entries(parsed.overrides)) {
|
|
568
|
+
const slot = SLOTS_BY_ID.get(id)
|
|
569
|
+
if (slot === undefined) {
|
|
570
|
+
overrides.set(id, text)
|
|
571
|
+
continue
|
|
572
|
+
}
|
|
573
|
+
if (slot.owner !== 'A') {
|
|
574
|
+
throw new TypeError(`ptc-plus: promptText slot ${id} is read-only (${slot.reason}); its bytes belong to another owner`)
|
|
575
|
+
}
|
|
576
|
+
const bytes = byteLength(text)
|
|
577
|
+
if (bytes > PROMPT_TEXT_MAX_SLOT_BYTES) {
|
|
578
|
+
throw new TypeError(`ptc-plus: promptText slot ${id} would deliver ${bytes} bytes, over the ${PROMPT_TEXT_MAX_SLOT_BYTES} byte slot limit`)
|
|
579
|
+
}
|
|
580
|
+
if (text.trim().length === 0 && !slot.omittable) {
|
|
581
|
+
throw new TypeError(`ptc-plus: promptText slot ${id} must not be empty`)
|
|
582
|
+
}
|
|
583
|
+
// Only a projected slot's placeholder names a fact the composer substitutes.
|
|
584
|
+
// A literal default may also contain braces (`plan.navigationSentence` shows
|
|
585
|
+
// `capabilities.search({query})`), but that is prose: nothing substitutes it,
|
|
586
|
+
// so requiring the token here would forbid legitimate rewording. The render
|
|
587
|
+
// path applies the same rule to the same slot kinds.
|
|
588
|
+
if (slot.kind === 'projected') {
|
|
589
|
+
for (const hole of holeTokens(slot.defaultText)) {
|
|
590
|
+
if (text.split(hole).length - 1 !== 1) {
|
|
591
|
+
throw new TypeError(`ptc-plus: promptText slot ${id} must keep exactly one ${hole} placeholder`)
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
overrides.set(id, text)
|
|
596
|
+
}
|
|
597
|
+
// Effective bytes: the request carries an override when it has one and the
|
|
598
|
+
// literal default otherwise, so the caps must measure what is delivered.
|
|
599
|
+
for (const anchor of PROMPT_ANCHORS) {
|
|
600
|
+
let total = 0
|
|
601
|
+
for (const slot of anchor.slots) {
|
|
602
|
+
total += effectiveBytes(slot, overrides)
|
|
603
|
+
if (total > PROMPT_TEXT_MAX_ANCHOR_BYTES) {
|
|
604
|
+
throw new TypeError(`ptc-plus: promptText anchor ${anchor.id} would deliver ${total} bytes, over the ${PROMPT_TEXT_MAX_ANCHOR_BYTES} byte anchor limit (at slot ${slot.id})`)
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
let document = sumEffectiveBytes(overrides) + unknownOverrideBytes(overrides)
|
|
609
|
+
if (document > PROMPT_TEXT_MAX_DOCUMENT_BYTES) {
|
|
610
|
+
throw new TypeError(`ptc-plus: promptText would deliver ${document} bytes, over the ${PROMPT_TEXT_MAX_DOCUMENT_BYTES} byte document limit`)
|
|
611
|
+
}
|
|
612
|
+
return parsed
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* The document a render path may use, given one configuration object.
|
|
617
|
+
*
|
|
618
|
+
* `capabilityPromptTextEnabled` is the pause switch for this whole feature: when
|
|
619
|
+
* it is off, every consumer must behave as if no document were stored, so
|
|
620
|
+
* assembly, the tool schemas, the capability plan and the tip bodies all fall
|
|
621
|
+
* back to the built-in text. Keeping that decision here means no consumer can
|
|
622
|
+
* read `config.promptText` directly and quietly ignore the switch.
|
|
623
|
+
*/
|
|
624
|
+
export function promptTextDocument(config) {
|
|
625
|
+
return config?.capabilityPromptTextEnabled === false ? undefined : config?.promptText
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/** `resolvePromptText` over a configuration object, honoring the pause switch. */
|
|
629
|
+
export function resolvePromptTextConfig(config) {
|
|
630
|
+
return resolvePromptText(promptTextDocument(config))
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Total resolution for every render and replay path.
|
|
635
|
+
*
|
|
636
|
+
* Never throws: each slot resolves to a usable override or its default, and
|
|
637
|
+
* everything the override cannot be used for is reported as
|
|
638
|
+
* `{ slotId, anchorId, reason }` instead of raised, because a prompt assembly
|
|
639
|
+
* failure would take the whole cell with it.
|
|
640
|
+
*/
|
|
641
|
+
export function resolvePromptText(value) {
|
|
642
|
+
const overrides = new Map()
|
|
643
|
+
const reported = []
|
|
644
|
+
if (value !== undefined && typeof value !== 'string') {
|
|
645
|
+
// A non-string configuration is a malformed document, not an empty one: it
|
|
646
|
+
// must show up in `reported` so the pane can say the stored value is unusable.
|
|
647
|
+
reported.push({ slotId: undefined, anchorId: undefined, reason: 'invalid-document-type' })
|
|
648
|
+
return freezeResolved(overrides, reported)
|
|
649
|
+
}
|
|
650
|
+
let parsed
|
|
651
|
+
try {
|
|
652
|
+
parsed = parsePromptTextDocument(value ?? PROMPT_TEXT_DEFAULT)
|
|
653
|
+
} catch (error) {
|
|
654
|
+
reported.push({ slotId: undefined, anchorId: undefined, reason: error.message })
|
|
655
|
+
return freezeResolved(overrides, reported)
|
|
656
|
+
}
|
|
657
|
+
const anchorTotals = new Map()
|
|
658
|
+
let document = 0
|
|
659
|
+
for (const [id, text] of Object.entries(parsed.overrides)) {
|
|
660
|
+
const bytes = byteLength(text)
|
|
661
|
+
const slot = SLOTS_BY_ID.get(id)
|
|
662
|
+
if (slot === undefined) {
|
|
663
|
+
// An unknown id is never delivered, but it still occupies the document
|
|
664
|
+
// budget: an unreadable id must not let a document grow without bound.
|
|
665
|
+
document += bytes
|
|
666
|
+
reported.push({ slotId: id, anchorId: undefined, reason: 'unknown-slot' })
|
|
667
|
+
continue
|
|
668
|
+
}
|
|
669
|
+
const anchorId = ANCHOR_OF_SLOT.get(id)
|
|
670
|
+
if (slot.owner !== 'A') {
|
|
671
|
+
reported.push({ slotId: id, anchorId, reason: 'not-writable' })
|
|
672
|
+
continue
|
|
673
|
+
}
|
|
674
|
+
if (bytes > PROMPT_TEXT_MAX_SLOT_BYTES) {
|
|
675
|
+
reported.push({ slotId: id, anchorId, reason: 'slot-exceeds-limit' })
|
|
676
|
+
continue
|
|
677
|
+
}
|
|
678
|
+
if (text.trim().length === 0 && !slot.omittable) {
|
|
679
|
+
reported.push({ slotId: id, anchorId, reason: 'required-slot-empty' })
|
|
680
|
+
continue
|
|
681
|
+
}
|
|
682
|
+
// Only a projected slot is rendered through a hole: the composer substitutes
|
|
683
|
+
// `{platformFacts}`, `{detail}`, `{overageBytes}` or `{maxOutputBytes}` at
|
|
684
|
+
// delivery. A literal default may also contain braces (`plan.navigationSentence`
|
|
685
|
+
// shows `capabilities.search({query})`), but that is prose: nothing substitutes
|
|
686
|
+
// it, and requiring the token would forbid legitimate rewording.
|
|
687
|
+
if (slot.kind === 'projected') {
|
|
688
|
+
const holes = holeTokens(slot.defaultText ?? '')
|
|
689
|
+
const kept = holes.filter(hole => text.includes(hole))
|
|
690
|
+
const duplicates = kept.some(hole => text.split(hole).length - 1 > 1)
|
|
691
|
+
if (holes.length > 0 && (kept.length !== holes.length || duplicates)) {
|
|
692
|
+
reported.push({ slotId: id, anchorId, reason: 'missing-hole' })
|
|
693
|
+
continue
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const total = (anchorTotals.get(anchorId) ?? 0) + bytes
|
|
697
|
+
if (total > PROMPT_TEXT_MAX_ANCHOR_BYTES) {
|
|
698
|
+
reported.push({ slotId: id, anchorId, reason: 'anchor-exceeds-limit' })
|
|
699
|
+
continue
|
|
700
|
+
}
|
|
701
|
+
anchorTotals.set(anchorId, total)
|
|
702
|
+
overrides.set(id, text)
|
|
703
|
+
document += bytes
|
|
704
|
+
}
|
|
705
|
+
if (document > PROMPT_TEXT_MAX_DOCUMENT_BYTES) {
|
|
706
|
+
reported.push({ slotId: undefined, anchorId: undefined, reason: 'document-exceeds-limit' })
|
|
707
|
+
overrides.clear()
|
|
708
|
+
}
|
|
709
|
+
enforceEffectiveCeilings(overrides, reported)
|
|
710
|
+
return freezeResolved(overrides, reported)
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Enforce the same ceilings render-side, measured on effective bytes.
|
|
715
|
+
*
|
|
716
|
+
* `resolvePromptText` is also a draft renderer: the settings pane resolves text
|
|
717
|
+
* that never passed `validatePromptTextDocument`. Without this pass a document
|
|
718
|
+
* could deliver more than the anchor or document ceiling while every individual
|
|
719
|
+
* override looked small, so a too-large override is dropped back to its default
|
|
720
|
+
* and reported instead of being delivered.
|
|
721
|
+
*
|
|
722
|
+
* A per-slot ceiling needs no second check here: the loop above admits an
|
|
723
|
+
* override only when its own bytes are within the slot ceiling, so every entry
|
|
724
|
+
* this pass can see already satisfies it.
|
|
725
|
+
*/
|
|
726
|
+
function enforceEffectiveCeilings(overrides, reported) {
|
|
727
|
+
for (const anchor of PROMPT_ANCHORS) {
|
|
728
|
+
const overridden = anchor.slots.filter(slot => overrides.has(slot.id))
|
|
729
|
+
if (overridden.length === 0) continue
|
|
730
|
+
const total = anchor.slots.reduce((sum, slot) => sum + effectiveBytes(slot, overrides), 0)
|
|
731
|
+
if (total <= PROMPT_TEXT_MAX_ANCHOR_BYTES) continue
|
|
732
|
+
for (const slot of overridden) {
|
|
733
|
+
reported.push({ slotId: slot.id, anchorId: anchor.id, reason: 'anchor-exceeds-limit' })
|
|
734
|
+
overrides.delete(slot.id)
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (sumEffectiveBytes(overrides) + unknownOverrideBytes(overrides) <= PROMPT_TEXT_MAX_DOCUMENT_BYTES) return
|
|
738
|
+
for (const id of [...overrides.keys()]) {
|
|
739
|
+
reported.push({ slotId: id, anchorId: ANCHOR_OF_SLOT.get(id), reason: 'document-exceeds-limit' })
|
|
740
|
+
}
|
|
741
|
+
overrides.clear()
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function freezeResolved(overrides, reported) {
|
|
745
|
+
const text = id => {
|
|
746
|
+
const slot = promptSlot(id)
|
|
747
|
+
if (overrides.has(id)) return overrides.get(id)
|
|
748
|
+
return slot.defaultText
|
|
749
|
+
}
|
|
750
|
+
return Object.freeze({
|
|
751
|
+
text,
|
|
752
|
+
omitted: id => overrides.get(id) === '',
|
|
753
|
+
overridden: id => overrides.has(id),
|
|
754
|
+
reported: Object.freeze(reported),
|
|
755
|
+
})
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Byte accounting for the panel counters.
|
|
760
|
+
*
|
|
761
|
+
* Pure function of a resolved document: per-slot bytes, per-anchor bytes, owner
|
|
762
|
+
* totals (A/B/C) and anchor counts by pure owner vs mixed. A slot with no literal
|
|
763
|
+
* default is `measured: false` and contributes no bytes, so a counter never
|
|
764
|
+
* claims bytes the registry cannot see.
|
|
765
|
+
*
|
|
766
|
+
* Two slot labels stay separate and must never be derived from each other:
|
|
767
|
+
* - 未测槽 (`counts.unmeasured`): slots on a channel this build has not wired to
|
|
768
|
+
* the resolver yet. Their bytes only exist once the composer is connected.
|
|
769
|
+
* - 动态槽 (`counts.dynamic`): slots on a wired anchor that have no literal
|
|
770
|
+
* default (policy prose, user-supplied instructions). Their bytes are measured at
|
|
771
|
+
* assembly time by the composer, so they are not an unwired channel.
|
|
772
|
+
* 投递未证 stays outside this function: only a boundary capture lane can report it.
|
|
773
|
+
*
|
|
774
|
+
* An owner card prints measured bytes plus how many of that owner's slots are not
|
|
775
|
+
* connected yet; unwired bytes are never folded into the measured total.
|
|
776
|
+
*/
|
|
777
|
+
export function accountPromptText(resolved) {
|
|
778
|
+
const slots = []
|
|
779
|
+
const anchors = []
|
|
780
|
+
const owners = { A: 0, B: 0, C: 0 }
|
|
781
|
+
const counts = { pure: { A: 0, B: 0, C: 0 }, mixed: 0, total: 0 }
|
|
782
|
+
const channels = new Map()
|
|
783
|
+
const slotsByOwner = { A: 0, B: 0, C: 0 }
|
|
784
|
+
const unmeasuredByOwner = { A: 0, B: 0, C: 0 }
|
|
785
|
+
const dynamicByOwner = { A: 0, B: 0, C: 0 }
|
|
786
|
+
for (const anchor of PROMPT_ANCHORS) {
|
|
787
|
+
const anchorOwners = new Set()
|
|
788
|
+
let anchorBytes = 0
|
|
789
|
+
const anchorSlots = []
|
|
790
|
+
for (const slot of anchor.slots) {
|
|
791
|
+
const declared = resolved.text(slot.id)
|
|
792
|
+
// An aggregate container is a set, not text: it owns no bytes and is neither
|
|
793
|
+
// unwired nor dynamic, so it must not inflate either counter.
|
|
794
|
+
const container = slot.kind === 'aggregate'
|
|
795
|
+
const measured = !container && anchor.wired === true && typeof declared === 'string'
|
|
796
|
+
const bytes = measured ? byteLength(declared) : 0
|
|
797
|
+
// A slot without bytes is either on a channel nobody wired yet, or on a
|
|
798
|
+
// wired anchor whose composer still has to produce the text. The two labels
|
|
799
|
+
// answer different questions, so the accounting keeps them apart.
|
|
800
|
+
const unwired = !container && !measured && anchor.wired !== true
|
|
801
|
+
const dynamic = !container && !measured && anchor.wired === true
|
|
802
|
+
anchorOwners.add(slot.owner)
|
|
803
|
+
owners[slot.owner] += bytes
|
|
804
|
+
slotsByOwner[slot.owner] += 1
|
|
805
|
+
if (unwired) unmeasuredByOwner[slot.owner] += 1
|
|
806
|
+
if (dynamic) dynamicByOwner[slot.owner] += 1
|
|
807
|
+
anchorBytes += bytes
|
|
808
|
+
const row = Object.freeze({
|
|
809
|
+
slotId: slot.id, anchorId: anchor.id, owner: slot.owner, reason: slot.reason,
|
|
810
|
+
kind: slot.kind, bytes, measured, unwired, dynamic, overridden: resolved.overridden(slot.id),
|
|
811
|
+
})
|
|
812
|
+
slots.push(row)
|
|
813
|
+
anchorSlots.push(row)
|
|
814
|
+
}
|
|
815
|
+
const pure = anchorOwners.size === 1 ? [...anchorOwners][0] : undefined
|
|
816
|
+
if (pure === undefined) counts.mixed += 1
|
|
817
|
+
else counts.pure[pure] += 1
|
|
818
|
+
counts.total += 1
|
|
819
|
+
const anchorUnmeasured = anchorSlots.filter(slot => slot.unwired).length
|
|
820
|
+
const anchorDynamic = anchorSlots.filter(slot => slot.dynamic).length
|
|
821
|
+
const perChannel = channels.get(anchor.channel) ?? { anchors: 0, wired: 0, bytes: 0, unmeasuredSlots: 0, dynamicSlots: 0 }
|
|
822
|
+
perChannel.anchors += 1
|
|
823
|
+
if (anchor.wired === true) perChannel.wired += 1
|
|
824
|
+
perChannel.bytes += anchorBytes
|
|
825
|
+
perChannel.unmeasuredSlots += anchorUnmeasured
|
|
826
|
+
perChannel.dynamicSlots += anchorDynamic
|
|
827
|
+
channels.set(anchor.channel, perChannel)
|
|
828
|
+
anchors.push(Object.freeze({
|
|
829
|
+
anchorId: anchor.id, channel: anchor.channel, presence: anchor.presence,
|
|
830
|
+
wired: anchor.wired === true,
|
|
831
|
+
owner: pure ?? 'mixed', bytes: anchorBytes, unmeasuredSlots: anchorUnmeasured,
|
|
832
|
+
dynamicSlots: anchorDynamic,
|
|
833
|
+
slots: Object.freeze(anchorSlots),
|
|
834
|
+
}))
|
|
835
|
+
}
|
|
836
|
+
return Object.freeze({
|
|
837
|
+
slots: Object.freeze(slots),
|
|
838
|
+
anchors: Object.freeze(anchors),
|
|
839
|
+
owners: Object.freeze(owners),
|
|
840
|
+
channels: Object.freeze(PROMPT_CHANNELS.map(channel => Object.freeze({
|
|
841
|
+
channel,
|
|
842
|
+
anchors: channels.get(channel)?.anchors ?? 0,
|
|
843
|
+
wired: channels.get(channel)?.wired ?? 0,
|
|
844
|
+
bytes: channels.get(channel)?.bytes ?? 0,
|
|
845
|
+
unmeasuredSlots: channels.get(channel)?.unmeasuredSlots ?? 0,
|
|
846
|
+
dynamicSlots: channels.get(channel)?.dynamicSlots ?? 0,
|
|
847
|
+
}))),
|
|
848
|
+
counts: Object.freeze({
|
|
849
|
+
pure: Object.freeze(counts.pure), mixed: counts.mixed, total: counts.total,
|
|
850
|
+
slots: Object.freeze(slotsByOwner),
|
|
851
|
+
unmeasured: Object.freeze(unmeasuredByOwner),
|
|
852
|
+
dynamic: Object.freeze(dynamicByOwner),
|
|
853
|
+
// Flat totals so a caller never sums two maps by hand.
|
|
854
|
+
unmeasuredSlots: Object.values(unmeasuredByOwner).reduce((sum, value) => sum + value, 0),
|
|
855
|
+
dynamicSlots: Object.values(dynamicByOwner).reduce((sum, value) => sum + value, 0),
|
|
856
|
+
}),
|
|
857
|
+
})
|
|
858
|
+
}
|