dsh-ptc-plus 0.2.1
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/LICENSE +21 -0
- package/README.md +137 -0
- package/README.zh.md +137 -0
- package/assets/dsh-ptc-plus-banner.webp +0 -0
- package/assets/ptc-plus-repair.png +0 -0
- package/assets/ptc-plus-settings.png +0 -0
- package/client.js +366 -0
- package/cordis.patch.yml +5 -0
- package/docs/README.md +21 -0
- package/docs/adr/0001-delegate-governance-to-dsh.md +3 -0
- package/docs/adr/0002-limit-work-map-scope.md +3 -0
- package/docs/adr/0003-prefer-native-program-surfaces.md +3 -0
- package/docs/adr/0004-declare-program-bindings-at-the-host.md +5 -0
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +129 -0
- package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +45 -0
- package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +39 -0
- package/docs/adr/0008-define-agent-operating-contract.md +25 -0
- package/docs/adr/0009-bound-expensive-acceptance-default.md +39 -0
- package/docs/adr/0010-session-log-derived-recovery-tips.md +26 -0
- package/docs/adr/0011-isolate-the-local-recovery-tip-provider.md +20 -0
- package/docs/adr/0012-preserve-mixed-destructuring-initialization.md +23 -0
- package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +21 -0
- package/docs/adr/0014-persist-cell-rewrite-policy.md +25 -0
- package/docs/adr/0015-preserve-source-positions-through-rewrites.md +25 -0
- package/docs/adr/0016-archive-resolved-review-findings.md +43 -0
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +23 -0
- package/docs/adr/0018-use-versioned-fixture-for-ab-benchmark.md +25 -0
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +32 -0
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +36 -0
- package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +68 -0
- package/docs/adr/README.md +18 -0
- package/docs/architecture.md +134 -0
- package/docs/capability-projection.md +48 -0
- package/docs/client-ui.md +47 -0
- package/docs/durability-design.md +265 -0
- package/docs/evaluation.md +62 -0
- package/docs/installation.md +82 -0
- package/docs/program-data-plane.md +61 -0
- package/docs/publishing.md +37 -0
- package/docs/runtime-reference.md +119 -0
- package/docs/value-wire.md +61 -0
- package/index.js +532 -0
- package/internal/ast-traversal.js +20 -0
- package/internal/binding-descriptors.js +95 -0
- package/internal/binding-pattern.js +54 -0
- package/internal/builtin-esm-sync.js +9 -0
- package/internal/cell-analysis.js +467 -0
- package/internal/cell-rewriter.js +520 -0
- package/internal/config-spec.js +179 -0
- package/internal/cordis-tools-owner.js +363 -0
- package/internal/diagnostic.js +133 -0
- package/internal/direct-surface-owner.js +326 -0
- package/internal/edit-transport-owner.js +287 -0
- package/internal/failure-reporting.js +139 -0
- package/internal/kernel-worker.js +484 -0
- package/internal/module-policy.js +46 -0
- package/internal/program-bindings.js +187 -0
- package/internal/record-utils.js +53 -0
- package/internal/recovery-tips.js +81 -0
- package/internal/rejected-cell-editor.js +328 -0
- package/internal/repl-convenience.js +201 -0
- package/internal/runtime-bridge-owner.js +417 -0
- package/internal/runtime-config.js +77 -0
- package/internal/runtime-contexts.js +31 -0
- package/internal/sdk-projection.js +88 -0
- package/internal/session-cell-executor.js +612 -0
- package/internal/session-journal.js +889 -0
- package/internal/session-log-view.js +80 -0
- package/internal/session-runtime.js +536 -0
- package/internal/session-state.js +45 -0
- package/internal/source-position-map.js +224 -0
- package/internal/tool-call-canonicalizer.js +339 -0
- package/internal/value-wire.js +421 -0
- package/internal/worker-client.js +218 -0
- package/internal/worker-cwd-virtualization.js +155 -0
- package/package.json +100 -0
- package/src/client.js +165 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
foldSessionTimeline,
|
|
3
|
+
} from './session-journal.js'
|
|
4
|
+
|
|
5
|
+
const SYSTEM_PROMPT_PLUGIN = '@deepseek-ai/dsh-system-prompt'
|
|
6
|
+
|
|
7
|
+
function systemPromptSnapshot(event, index, contextStep) {
|
|
8
|
+
const source = event?.data?.source
|
|
9
|
+
if (event?.type !== 'user/message' || source?.kind !== 'plugin'
|
|
10
|
+
|| source.plugin !== SYSTEM_PROMPT_PLUGIN || source.form !== 'snapshot'
|
|
11
|
+
|| !Array.isArray(source.sections)) return undefined
|
|
12
|
+
const names = new Set()
|
|
13
|
+
const sections = []
|
|
14
|
+
for (const section of source.sections) {
|
|
15
|
+
if (section === null || typeof section !== 'object' || Array.isArray(section)
|
|
16
|
+
|| typeof section.name !== 'string' || section.name.length === 0
|
|
17
|
+
|| typeof section.text !== 'string' || names.has(section.name)) return undefined
|
|
18
|
+
names.add(section.name)
|
|
19
|
+
sections.push(Object.freeze({ name: section.name, text: section.text }))
|
|
20
|
+
}
|
|
21
|
+
return Object.freeze({ index, contextStep, sections: Object.freeze(sections) })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isContextStep(event) {
|
|
25
|
+
return event?.type === 'request/header'
|
|
26
|
+
|| event?.type === 'assistant/message'
|
|
27
|
+
|| (event?.type === 'user/message' && event.data?.source?.kind === 'user')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Project session events into immutable facts consumed by prompt presentation. */
|
|
31
|
+
export function projectSessionLog(agent, requestedEdit) {
|
|
32
|
+
const events = agent?.session?.events
|
|
33
|
+
if (!Array.isArray(events)) {
|
|
34
|
+
return Object.freeze({
|
|
35
|
+
openTurn: false,
|
|
36
|
+
contextStep: 0,
|
|
37
|
+
systemPromptSnapshots: Object.freeze([]),
|
|
38
|
+
lastSuccessfulRunIndex: undefined,
|
|
39
|
+
latestRun: undefined,
|
|
40
|
+
editableRun: undefined,
|
|
41
|
+
repairSource: undefined,
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
let contextStep = 0
|
|
45
|
+
const systemPromptSnapshots = []
|
|
46
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
47
|
+
const event = events[index]
|
|
48
|
+
const snapshot = systemPromptSnapshot(event, index, contextStep)
|
|
49
|
+
if (snapshot !== undefined) systemPromptSnapshots.push(snapshot)
|
|
50
|
+
if (isContextStep(event)) contextStep += 1
|
|
51
|
+
}
|
|
52
|
+
const timeline = foldSessionTimeline(events)
|
|
53
|
+
const requestedEntry = requestedEdit === undefined
|
|
54
|
+
? undefined
|
|
55
|
+
: timeline.executableCalls.get(requestedEdit.callSeq)
|
|
56
|
+
const requestedEditTarget = requestedEdit !== undefined
|
|
57
|
+
&& requestedEntry?.event.data?.callId === requestedEdit.callId
|
|
58
|
+
&& requestedEntry.event.data.name === 'edit_run_code'
|
|
59
|
+
? timeline.editTargets.get(requestedEdit.callSeq)
|
|
60
|
+
: undefined
|
|
61
|
+
const latestRun = timeline.openTurn ? timeline.latestRun : undefined
|
|
62
|
+
const editableRun = timeline.openTurn ? timeline.editableRun : undefined
|
|
63
|
+
return Object.freeze({
|
|
64
|
+
openTurn: timeline.openTurn,
|
|
65
|
+
contextStep,
|
|
66
|
+
systemPromptSnapshots: Object.freeze(systemPromptSnapshots),
|
|
67
|
+
lastSuccessfulRunIndex: timeline.lastSuccessfulRunIndex,
|
|
68
|
+
latestRun,
|
|
69
|
+
editableRun,
|
|
70
|
+
repairSource: editableRun?.source,
|
|
71
|
+
...(requestedEdit === undefined ? {} : { requestedEditTarget }),
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Return the target snapshot captured at one persisted edit call event. */
|
|
76
|
+
export function editTargetForCall(agent, callId, callSeq) {
|
|
77
|
+
if (typeof callId !== 'string' || callId.length === 0
|
|
78
|
+
|| !Number.isSafeInteger(callSeq) || callSeq < 0) return undefined
|
|
79
|
+
return projectSessionLog(agent, { callId, callSeq }).requestedEditTarget
|
|
80
|
+
}
|
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import { diagnostic, renderDiagnostic } from './diagnostic.js'
|
|
2
|
+
import { createFailureTracker, messageOf } from './failure-reporting.js'
|
|
3
|
+
import {
|
|
4
|
+
createJournal,
|
|
5
|
+
liveToolCallSeq,
|
|
6
|
+
normalizeJournal,
|
|
7
|
+
pathToHead,
|
|
8
|
+
reduceStateOperations,
|
|
9
|
+
recoverJournal,
|
|
10
|
+
} from './session-journal.js'
|
|
11
|
+
import { normalizeBindingDescriptors } from './binding-descriptors.js'
|
|
12
|
+
import { resolveConfig } from './runtime-config.js'
|
|
13
|
+
import { WorkerClient } from './worker-client.js'
|
|
14
|
+
import { BindingCatalog, durabilityState, transitionDurability } from './session-state.js'
|
|
15
|
+
import { SessionCellExecutor } from './session-cell-executor.js'
|
|
16
|
+
|
|
17
|
+
const WORKER_URL = new URL('./kernel-worker.js', import.meta.url)
|
|
18
|
+
function recoveryDiagnostic(count) {
|
|
19
|
+
return diagnostic({
|
|
20
|
+
code: 'PTC-R002',
|
|
21
|
+
severity: 'warning',
|
|
22
|
+
phase: 'recover',
|
|
23
|
+
message: `Restored the durable head and skipped ${count} unreconstructable historical cell(s); their source remains in the session log.`,
|
|
24
|
+
stateEffect: 'rolled-back',
|
|
25
|
+
help: [
|
|
26
|
+
'continue from the restored bindings',
|
|
27
|
+
'do not reference values created only in the skipped suffix',
|
|
28
|
+
],
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function rewritePolicy(config) {
|
|
33
|
+
return {
|
|
34
|
+
autoRewriteImports: config.autoRewriteImports,
|
|
35
|
+
autoStripExports: config.autoStripExports,
|
|
36
|
+
autoSplitRedeclarations: config.autoSplitRedeclarations,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolvedRuntimeConfig(config) {
|
|
41
|
+
return Object.freeze(resolveConfig(config))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function emptyHistory() {
|
|
45
|
+
return { nodes: [], head: undefined, checkpoints: new Map(), volatileSuffix: [], available: true }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class ReplayFailure extends Error {
|
|
49
|
+
constructor(node, cause) {
|
|
50
|
+
super(messageOf(cause))
|
|
51
|
+
this.node = node
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class ReplayCancelled extends Error {
|
|
56
|
+
constructor(result) {
|
|
57
|
+
super(result.error?.message ?? 'session replay cancelled')
|
|
58
|
+
this.result = result
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class SessionKernel {
|
|
63
|
+
constructor({ config, history, cwd, session, withInitiator }) {
|
|
64
|
+
this.config = config
|
|
65
|
+
this.history = history
|
|
66
|
+
this.cwd = cwd
|
|
67
|
+
this.session = session
|
|
68
|
+
this.withInitiator = withInitiator
|
|
69
|
+
this.durability = durabilityState()
|
|
70
|
+
this.bindingCatalog = new BindingCatalog()
|
|
71
|
+
this.replayed = false
|
|
72
|
+
this.recoveryNotice = history.volatileSuffix.length === 0
|
|
73
|
+
? undefined
|
|
74
|
+
: recoveryDiagnostic(history.volatileSuffix.length)
|
|
75
|
+
this.active = undefined
|
|
76
|
+
this.sequence = 0
|
|
77
|
+
this.tail = Promise.resolve()
|
|
78
|
+
this.tentatives = new WeakMap()
|
|
79
|
+
this.workerReservations = new Set()
|
|
80
|
+
this.cellExecutor = new SessionCellExecutor(this)
|
|
81
|
+
this.client = new WorkerClient({
|
|
82
|
+
workerUrl: WORKER_URL,
|
|
83
|
+
cwd,
|
|
84
|
+
onMessage: message => this.cellExecutor.onMessage(message),
|
|
85
|
+
onFailure: message => this.active?.resolve({
|
|
86
|
+
logs: [], error: { kind: 'worker-exit', message },
|
|
87
|
+
}, true),
|
|
88
|
+
})
|
|
89
|
+
this.failures = createFailureTracker()
|
|
90
|
+
this.disposed = false
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
valueLimits(config = this.config) {
|
|
94
|
+
return {
|
|
95
|
+
maxNodes: config.maxValueNodes,
|
|
96
|
+
maxEdges: config.maxValueEdges,
|
|
97
|
+
maxArrayLength: config.maxValueArrayLength,
|
|
98
|
+
maxBigIntDigits: config.maxValueBigIntDigits,
|
|
99
|
+
maxStringBytes: config.maxOutputBytes,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
assertReconfigurationAllowed(config) {
|
|
104
|
+
if ((this.client.workerLimit !== undefined || this.workerReservations.size > 0)
|
|
105
|
+
&& config.maxOldGenerationSizeMb !== this.config.maxOldGenerationSizeMb) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
'ptc-plus: maxOldGenerationSizeMb cannot change while a session worker is active; retry after the session is disposed',
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
reconfigure(config) {
|
|
113
|
+
this.assertReconfigurationAllowed(config)
|
|
114
|
+
this.config = config
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
reserveWorkerConfiguration(config) {
|
|
118
|
+
const reservation = Object.freeze({ maxOldGenerationSizeMb: config.maxOldGenerationSizeMb })
|
|
119
|
+
this.workerReservations.add(reservation)
|
|
120
|
+
return reservation
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
releaseWorkerConfiguration(reservation) {
|
|
124
|
+
this.workerReservations.delete(reservation)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
run(request, config = this.config) {
|
|
128
|
+
const execute = () => this.execute(request, config)
|
|
129
|
+
const result = this.tail.then(execute, execute)
|
|
130
|
+
this.tail = result.then(() => undefined, () => undefined)
|
|
131
|
+
return result
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async execute(request, config) {
|
|
135
|
+
const recoveryBoundaries = []
|
|
136
|
+
const finishResult = result => recoveryBoundaries.length === 0
|
|
137
|
+
? result
|
|
138
|
+
: { ...result, recoveryBoundaries: recoveryBoundaries.map(boundary => ({ ...boundary })) }
|
|
139
|
+
if (!this.replayed && config.durableReplay) {
|
|
140
|
+
let skipped = 0
|
|
141
|
+
while (!this.replayed) {
|
|
142
|
+
try {
|
|
143
|
+
await this.replayHistory(request, config)
|
|
144
|
+
this.replayed = true
|
|
145
|
+
} catch (error) {
|
|
146
|
+
const worker = this.client.worker
|
|
147
|
+
if (worker !== undefined) await this.client.reset(worker)
|
|
148
|
+
if (error instanceof ReplayCancelled) {
|
|
149
|
+
this.completeJournal(request.journal, 'noop', error.result)
|
|
150
|
+
return finishResult(error.result)
|
|
151
|
+
}
|
|
152
|
+
if (!(error instanceof ReplayFailure)) {
|
|
153
|
+
const result = {
|
|
154
|
+
logs: [],
|
|
155
|
+
error: {
|
|
156
|
+
kind: 'recovery',
|
|
157
|
+
message: `cannot reconstruct REPL from session log: ${messageOf(error)}`,
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
this.completeJournal(request.journal, 'noop', result)
|
|
161
|
+
return finishResult(result)
|
|
162
|
+
}
|
|
163
|
+
const frontier = error.node.parent === undefined
|
|
164
|
+
? undefined
|
|
165
|
+
: this.history.nodes[error.node.parent]
|
|
166
|
+
const previousPathLength = pathToHead(this.history).length
|
|
167
|
+
try {
|
|
168
|
+
const boundary = {
|
|
169
|
+
failedCallSeq: error.node.callSeq,
|
|
170
|
+
frontierCallSeq: frontier?.callSeq ?? null,
|
|
171
|
+
}
|
|
172
|
+
const recovered = recoverJournal(this.session, request.callSeq, {
|
|
173
|
+
extraBoundaries: [boundary],
|
|
174
|
+
})
|
|
175
|
+
recoveryBoundaries.push(boundary)
|
|
176
|
+
this.history = recovered
|
|
177
|
+
} catch (boundaryError) {
|
|
178
|
+
const result = {
|
|
179
|
+
logs: [],
|
|
180
|
+
error: {
|
|
181
|
+
kind: 'recovery',
|
|
182
|
+
message: `cannot apply REPL recovery boundary: ${messageOf(boundaryError)}`,
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
this.completeJournal(request.journal, 'noop', result)
|
|
186
|
+
return finishResult(result)
|
|
187
|
+
}
|
|
188
|
+
const nextPathLength = pathToHead(this.history).length
|
|
189
|
+
skipped += Math.max(1, previousPathLength - nextPathLength)
|
|
190
|
+
this.durability = durabilityState()
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (skipped > 0) this.recoveryNotice = recoveryDiagnostic(skipped)
|
|
194
|
+
}
|
|
195
|
+
const leadingDiagnostics = this.recoveryNotice === undefined ? [] : [this.recoveryNotice]
|
|
196
|
+
this.recoveryNotice = undefined
|
|
197
|
+
if (request.journal !== undefined) request.journal.diagnostics.push(...leadingDiagnostics)
|
|
198
|
+
const result = await this.cellExecutor.executeCell(request, undefined, config)
|
|
199
|
+
if (leadingDiagnostics.length > 0) {
|
|
200
|
+
const rendered = leadingDiagnostics.map(item => renderDiagnostic(item, request.program))
|
|
201
|
+
result.logs = [...rendered, ...result.logs]
|
|
202
|
+
}
|
|
203
|
+
if (result.error === undefined) {
|
|
204
|
+
this.failures.reset()
|
|
205
|
+
} else {
|
|
206
|
+
const hint = this.failures.hint(result.error)
|
|
207
|
+
if (hint !== undefined) {
|
|
208
|
+
result.logs = [...result.logs, renderDiagnostic(hint, request.program)]
|
|
209
|
+
if (request.journal !== undefined) request.journal.diagnostics.push(hint)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return finishResult(result)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async replayHistory(request, config) {
|
|
216
|
+
this.bindingCatalog = new BindingCatalog()
|
|
217
|
+
const path = pathToHead(this.history)
|
|
218
|
+
for (const node of path) {
|
|
219
|
+
try {
|
|
220
|
+
const result = await this.cellExecutor.executeCell(
|
|
221
|
+
{ ...request, program: node.code, journal: undefined },
|
|
222
|
+
node.journal,
|
|
223
|
+
config,
|
|
224
|
+
)
|
|
225
|
+
const completion = node.journal.completion
|
|
226
|
+
if (result.error !== undefined && !['exception', 'invalid-output'].includes(result.error.kind)) {
|
|
227
|
+
if (result.error.kind === 'abort') throw new ReplayCancelled(result)
|
|
228
|
+
throw new Error(`cell replay infrastructure failed (${result.error.kind}): ${result.error.message}`)
|
|
229
|
+
}
|
|
230
|
+
if (completion.kind === 'return' && result.error !== undefined) {
|
|
231
|
+
throw new Error(`cell replay failed: ${result.error.message}`)
|
|
232
|
+
}
|
|
233
|
+
if (completion.kind === 'throw') {
|
|
234
|
+
if (result.error === undefined) throw new Error('cell replay succeeded where the recorded cell failed')
|
|
235
|
+
if (result.error.kind !== completion.error.kind || result.error.message !== completion.error.message) {
|
|
236
|
+
throw new Error('cell replay produced a different semantic failure')
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (error instanceof ReplayCancelled) throw error
|
|
241
|
+
throw new ReplayFailure(node, error)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
completeJournal(journal, status, result, volatileReason, diagnostics = [], completion = undefined) {
|
|
247
|
+
if (journal === undefined) return
|
|
248
|
+
journal.status = status
|
|
249
|
+
journal.completion = result.error === undefined
|
|
250
|
+
? {
|
|
251
|
+
kind: 'return',
|
|
252
|
+
hasValue: completion?.hasValue === true,
|
|
253
|
+
...(completion?.hasValue === true ? { value: completion.value } : {}),
|
|
254
|
+
}
|
|
255
|
+
: { kind: 'throw', error: { kind: result.error.kind, message: result.error.message } }
|
|
256
|
+
if (volatileReason !== undefined) journal.volatileReason = volatileReason
|
|
257
|
+
if (diagnostics.length > 0) journal.diagnostics.push(...diagnostics)
|
|
258
|
+
if (status === 'volatile') {
|
|
259
|
+
journal.operations = journal.operations.filter(operation => operation.action !== 'save')
|
|
260
|
+
}
|
|
261
|
+
if (status === 'discarded' || status === 'noop') {
|
|
262
|
+
journal.calls.length = 0
|
|
263
|
+
journal.operations.length = 0
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
rollbackToDurable() {
|
|
268
|
+
this.durability = durabilityState()
|
|
269
|
+
this.replayed = false
|
|
270
|
+
this.bindingCatalog = new BindingCatalog()
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
settleCell(active, result, terminate = false) {
|
|
274
|
+
/* c8 ignore next */
|
|
275
|
+
if (this.active !== active) return
|
|
276
|
+
const { request, journal, replay, prepared, worker } = active
|
|
277
|
+
clearInterval(active.computeTimer)
|
|
278
|
+
clearTimeout(active.wallTimer)
|
|
279
|
+
request.signal?.removeEventListener('abort', active.onAbort)
|
|
280
|
+
if (active.rewrites !== undefined && active.rewrites.length > 0) {
|
|
281
|
+
result = { ...result, rewrites: active.rewrites }
|
|
282
|
+
}
|
|
283
|
+
if (journal !== undefined && replay === undefined) {
|
|
284
|
+
if (terminate) {
|
|
285
|
+
const volatileReason = active.pendingBindings.values().next().value ?? active.durability.reason
|
|
286
|
+
this.completeJournal(journal, 'discarded', result, volatileReason, active.diagnostics)
|
|
287
|
+
this.rollbackToDurable()
|
|
288
|
+
if (volatileReason !== undefined) {
|
|
289
|
+
this.durability = transitionDurability(this.durability, {
|
|
290
|
+
type: 'volatile',
|
|
291
|
+
reason: volatileReason,
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
const status = active.durability.status
|
|
296
|
+
this.completeJournal(
|
|
297
|
+
journal, status, result, active.durability.reason, active.diagnostics, active.completion,
|
|
298
|
+
)
|
|
299
|
+
this.tentatives.set(journal, {
|
|
300
|
+
callSeq: request.callSeq,
|
|
301
|
+
program: request.program,
|
|
302
|
+
bindingCatalog: active.appliedBindingCatalog
|
|
303
|
+
?? active.priorBindingCatalog.advance(prepared),
|
|
304
|
+
worker,
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (replay !== undefined && !terminate) {
|
|
309
|
+
this.bindingCatalog = active.appliedBindingCatalog
|
|
310
|
+
?? active.priorBindingCatalog.advance(prepared)
|
|
311
|
+
}
|
|
312
|
+
this.active = undefined
|
|
313
|
+
if (terminate) void this.client.reset(worker)
|
|
314
|
+
active.finish(result)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
finalizeJournal(journal, confirmed) {
|
|
318
|
+
const tentative = this.tentatives.get(journal)
|
|
319
|
+
if (tentative === undefined) return
|
|
320
|
+
this.tentatives.delete(journal)
|
|
321
|
+
if (!confirmed) {
|
|
322
|
+
if (journal.status === 'durable' || journal.status === 'volatile') {
|
|
323
|
+
const reason = journal.volatileReason ?? 'run_code journal was not preserved in the final tool result'
|
|
324
|
+
this.durability = transitionDurability(this.durability, {
|
|
325
|
+
type: 'volatile',
|
|
326
|
+
reason,
|
|
327
|
+
})
|
|
328
|
+
this.bindingCatalog = tentative.bindingCatalog
|
|
329
|
+
}
|
|
330
|
+
return
|
|
331
|
+
}
|
|
332
|
+
if (journal.status === 'durable') {
|
|
333
|
+
const normalized = normalizeJournal(journal)
|
|
334
|
+
const node = Object.freeze({
|
|
335
|
+
code: tentative.program,
|
|
336
|
+
journal: normalized,
|
|
337
|
+
...(tentative.callSeq === undefined ? {} : { callSeq: tentative.callSeq }),
|
|
338
|
+
parent: this.history.head,
|
|
339
|
+
})
|
|
340
|
+
const index = this.history.nodes.push(node) - 1
|
|
341
|
+
this.history.head = index
|
|
342
|
+
this.bindingCatalog = tentative.bindingCatalog
|
|
343
|
+
this.finishStateOperations(journal.operations, index, tentative.worker)
|
|
344
|
+
return
|
|
345
|
+
}
|
|
346
|
+
if (journal.status === 'volatile') {
|
|
347
|
+
this.durability = transitionDurability(this.durability, {
|
|
348
|
+
type: 'volatile',
|
|
349
|
+
reason: journal.volatileReason,
|
|
350
|
+
})
|
|
351
|
+
this.bindingCatalog = tentative.bindingCatalog
|
|
352
|
+
this.finishStateOperations(journal.operations, undefined, tentative.worker)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
finishStateOperations(operations, index, worker) {
|
|
357
|
+
const transition = reduceStateOperations(this.history, operations, index)
|
|
358
|
+
this.history.head = transition.head
|
|
359
|
+
this.history.checkpoints = transition.checkpoints
|
|
360
|
+
if (transition.restored) {
|
|
361
|
+
this.rollbackToDurable()
|
|
362
|
+
void this.client.reset(worker)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async dispose() {
|
|
367
|
+
this.disposed = true
|
|
368
|
+
const worker = this.client.worker
|
|
369
|
+
if (worker !== undefined) {
|
|
370
|
+
/* c8 ignore next */
|
|
371
|
+
this.active?.resolve({ logs: [], error: { kind: 'abort', message: 'session kernel disposed' } }, true)
|
|
372
|
+
}
|
|
373
|
+
await this.client.dispose()
|
|
374
|
+
await this.tail
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function sessionOf(sessionContext) {
|
|
379
|
+
if (typeof sessionContext !== 'object' || sessionContext === null) {
|
|
380
|
+
return {
|
|
381
|
+
id: String(sessionContext),
|
|
382
|
+
session: undefined,
|
|
383
|
+
callId: undefined,
|
|
384
|
+
persistedCallSeq: undefined,
|
|
385
|
+
cwd: undefined,
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const session = sessionContext.session
|
|
389
|
+
return {
|
|
390
|
+
id: String(sessionContext.id),
|
|
391
|
+
session,
|
|
392
|
+
callId: sessionContext.callId,
|
|
393
|
+
persistedCallSeq: sessionContext.persistedCallSeq,
|
|
394
|
+
cwd: typeof session?.header?.cwd === 'string' ? session.header.cwd : undefined,
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export class SessionRuntime {
|
|
399
|
+
constructor(config = {}, options = {}) {
|
|
400
|
+
this.config = resolvedRuntimeConfig(config)
|
|
401
|
+
this.kernels = new Map()
|
|
402
|
+
this.pendingNoops = new Map()
|
|
403
|
+
this.settlements = new WeakSet()
|
|
404
|
+
this.disposed = false
|
|
405
|
+
this.withInitiator = typeof options.withInitiator === 'function' ? options.withInitiator : undefined
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async run(sessionContext, request) {
|
|
409
|
+
const execution = await this.runTentative(sessionContext, request)
|
|
410
|
+
if (execution.settlement !== undefined) this.finalize(execution.settlement, true)
|
|
411
|
+
return execution.result
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
reconfigure(config) {
|
|
415
|
+
const resolved = resolvedRuntimeConfig(config)
|
|
416
|
+
const kernels = [...this.kernels.values()]
|
|
417
|
+
for (const kernel of kernels) kernel.assertReconfigurationAllowed(resolved)
|
|
418
|
+
for (const kernel of kernels) kernel.reconfigure(resolved)
|
|
419
|
+
this.config = resolved
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async runTentative(sessionContext, request) {
|
|
423
|
+
const completed = result => Object.freeze({ result, settlement: undefined })
|
|
424
|
+
if (this.disposed) return completed({ logs: [], error: { kind: 'abort', message: 'PTC runtime disposed' } })
|
|
425
|
+
const cellConfig = this.config
|
|
426
|
+
let bindingDescriptors
|
|
427
|
+
try {
|
|
428
|
+
bindingDescriptors = normalizeBindingDescriptors(request?.bindings)
|
|
429
|
+
} catch (error) {
|
|
430
|
+
return completed({ logs: [], error: { kind: 'exception', message: messageOf(error) } })
|
|
431
|
+
}
|
|
432
|
+
request = { ...request, bindings: bindingDescriptors.namespaces, bindingDescriptors }
|
|
433
|
+
const { id: sessionId, session, callId, persistedCallSeq, cwd } = sessionOf(sessionContext)
|
|
434
|
+
let callSeq
|
|
435
|
+
try {
|
|
436
|
+
if (persistedCallSeq !== undefined
|
|
437
|
+
&& (!Number.isSafeInteger(persistedCallSeq) || persistedCallSeq < 0)) {
|
|
438
|
+
throw new Error('persisted tool call sequence must be a non-negative safe integer')
|
|
439
|
+
}
|
|
440
|
+
callSeq = cellConfig.durableReplay
|
|
441
|
+
? persistedCallSeq ?? liveToolCallSeq(session, callId, 'run_code')
|
|
442
|
+
: undefined
|
|
443
|
+
} catch (error) {
|
|
444
|
+
return completed({ logs: [], error: { kind: 'recovery', message: `cannot identify current run_code call in session log: ${messageOf(error)}` } })
|
|
445
|
+
}
|
|
446
|
+
let kernel = this.kernels.get(sessionId)
|
|
447
|
+
if (kernel === undefined) {
|
|
448
|
+
let history
|
|
449
|
+
try {
|
|
450
|
+
history = cellConfig.durableReplay
|
|
451
|
+
? recoverJournal(session, callSeq)
|
|
452
|
+
: emptyHistory()
|
|
453
|
+
} catch (error) {
|
|
454
|
+
return completed({ logs: [], error: { kind: 'recovery', message: `cannot reconstruct REPL from session log: ${messageOf(error)}` } })
|
|
455
|
+
}
|
|
456
|
+
kernel = new SessionKernel({
|
|
457
|
+
config: cellConfig,
|
|
458
|
+
history,
|
|
459
|
+
cwd,
|
|
460
|
+
session,
|
|
461
|
+
withInitiator: this.withInitiator,
|
|
462
|
+
})
|
|
463
|
+
this.kernels.set(sessionId, kernel)
|
|
464
|
+
}
|
|
465
|
+
const journal = createJournal(
|
|
466
|
+
/* c8 ignore next */
|
|
467
|
+
this.pendingNoops.get(sessionId) ?? [],
|
|
468
|
+
cellConfig.looseTopLevelRedeclarations ? 'loose' : 'strict',
|
|
469
|
+
rewritePolicy(cellConfig),
|
|
470
|
+
)
|
|
471
|
+
const workerReservation = kernel.reserveWorkerConfiguration(cellConfig)
|
|
472
|
+
let result
|
|
473
|
+
try {
|
|
474
|
+
result = await kernel.run({ ...request, journal, callSeq }, cellConfig)
|
|
475
|
+
} finally {
|
|
476
|
+
kernel.releaseWorkerConfiguration(workerReservation)
|
|
477
|
+
}
|
|
478
|
+
const settlement = Object.freeze({
|
|
479
|
+
journal,
|
|
480
|
+
kernel,
|
|
481
|
+
sessionId,
|
|
482
|
+
...(result.recoveryBoundaries === undefined
|
|
483
|
+
? {}
|
|
484
|
+
: { recoveryBoundaries: result.recoveryBoundaries }),
|
|
485
|
+
...(result.rewrites === undefined ? {} : { rewrites: result.rewrites }),
|
|
486
|
+
})
|
|
487
|
+
this.settlements.add(settlement)
|
|
488
|
+
const {
|
|
489
|
+
journal: _ignored,
|
|
490
|
+
recoveryBoundaries: _recoveryBoundaries,
|
|
491
|
+
...publicResult
|
|
492
|
+
} = result
|
|
493
|
+
return Object.freeze({ result: publicResult, settlement })
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
noteNoop(sessionId, session, callId) {
|
|
497
|
+
const callSeq = liveToolCallSeq(session, String(callId), 'run_code')
|
|
498
|
+
if (callSeq === undefined) return
|
|
499
|
+
const id = String(sessionId)
|
|
500
|
+
let calls = this.pendingNoops.get(id)
|
|
501
|
+
if (calls === undefined) {
|
|
502
|
+
calls = new Set()
|
|
503
|
+
this.pendingNoops.set(id, calls)
|
|
504
|
+
}
|
|
505
|
+
calls.add(callSeq)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
finalize(settlement, confirmed) {
|
|
509
|
+
if (settlement === null || typeof settlement !== 'object' || !this.settlements.delete(settlement)) {
|
|
510
|
+
throw new TypeError('ptc-plus: finalize requires one unsettled SessionRuntime settlement handle')
|
|
511
|
+
}
|
|
512
|
+
const { journal, kernel, sessionId } = settlement
|
|
513
|
+
kernel.finalizeJournal(journal, confirmed)
|
|
514
|
+
if (!confirmed) return
|
|
515
|
+
const noops = this.pendingNoops.get(sessionId)
|
|
516
|
+
if (noops === undefined) return
|
|
517
|
+
for (const callSeq of journal.confirms ?? []) noops.delete(callSeq)
|
|
518
|
+
if (noops.size === 0) this.pendingNoops.delete(sessionId)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async disposeSession(sessionId) {
|
|
522
|
+
const id = String(sessionId)
|
|
523
|
+
const kernel = this.kernels.get(id)
|
|
524
|
+
this.pendingNoops.delete(id)
|
|
525
|
+
if (kernel === undefined) return
|
|
526
|
+
this.kernels.delete(id)
|
|
527
|
+
await kernel.dispose()
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async dispose() {
|
|
531
|
+
this.disposed = true
|
|
532
|
+
const kernels = [...this.kernels.values()]
|
|
533
|
+
this.kernels.clear()
|
|
534
|
+
await Promise.all(kernels.map(kernel => kernel.dispose()))
|
|
535
|
+
}
|
|
536
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Runtime state transitions shared by the session coordinator and cell executor.
|
|
2
|
+
|
|
3
|
+
export function durabilityState(overrides = {}) {
|
|
4
|
+
return Object.freeze({
|
|
5
|
+
status: 'durable',
|
|
6
|
+
reason: undefined,
|
|
7
|
+
...overrides,
|
|
8
|
+
})
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function transitionDurability(state, transition) {
|
|
12
|
+
if (transition.type !== 'volatile') return state
|
|
13
|
+
return durabilityState({
|
|
14
|
+
...state,
|
|
15
|
+
status: 'volatile',
|
|
16
|
+
reason: state.reason ?? transition.reason,
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class BindingCatalog {
|
|
21
|
+
#known
|
|
22
|
+
#imports
|
|
23
|
+
#namespaces
|
|
24
|
+
|
|
25
|
+
constructor(known = new Set(), imports = new Map(), namespaces = new Set()) {
|
|
26
|
+
this.#known = new Set(known)
|
|
27
|
+
this.#imports = new Map(imports)
|
|
28
|
+
this.#namespaces = new Set(namespaces)
|
|
29
|
+
Object.freeze(this)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
inputs() {
|
|
33
|
+
return {
|
|
34
|
+
knownBindings: new Set(this.#known),
|
|
35
|
+
importBindings: new Map(this.#imports),
|
|
36
|
+
importNamespaces: new Set(this.#namespaces),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
advance(prepared) {
|
|
41
|
+
const known = new Set(this.#known)
|
|
42
|
+
for (const name of prepared.declared) known.add(name)
|
|
43
|
+
return new BindingCatalog(known, prepared.imports, prepared.importNamespaces)
|
|
44
|
+
}
|
|
45
|
+
}
|