narraeon 0.7.4 → 0.7.5
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/dist/node/runtime/V1Runtime.js +1 -1
- package/dist/node/runtime/content/CurrentTreeContentLibrary.js +64 -163
- package/dist/node/runtime/content/FileNativeContentTree.js +16 -13
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +34 -18
- package/dist/node/runtime/play/PlayAdvanceLifecycle.js +211 -0
- package/dist/node/runtime/play/PlayCallChain.js +200 -374
- package/dist/node/runtime/setting/SettingAuthoringTransaction.js +312 -167
- package/dist/node/runtime/world/FileNativeWorldStore.js +151 -109
- package/dist/node/runtime/world/WorldDocumentMaintenance.js +2 -1
- package/dist/node/runtime/world/WorldDocumentStore.js +1 -1
- package/dist/node/runtime/world/WorldLedgerAuthority.js +37 -3
- package/dist/node/runtime/world/WorldTurnLedger.js +17 -4
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +2 -4
- package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +19 -241
- package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +8 -27
- package/dist/node/server/createServer.js +1 -4
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { projectLegacyModelBindingFailure } from "./LegacyModelBindingFailure.js";
|
|
2
|
-
import {
|
|
2
|
+
import { dispatchFailureEnding, emptyResponseEnding, lastEndingCancelled, noContinuationEnding, PlayAdvanceLifecycle, settlementFailureEnding, } from "./PlayAdvanceLifecycle.js";
|
|
3
3
|
import { isResolutionTool, resolutionMarkdown, resolutionToolNames, resolveTabletop, seededRandom, } from "./TabletopResolution.js";
|
|
4
4
|
import { captureNarrativeOrigin, snapshotBackground, narrativeBackground, } from "../history/HistoryBackground.js";
|
|
5
5
|
import { isDeepStrictEqual } from "node:util";
|
|
@@ -13,7 +13,7 @@ import { completedPlayerRounds, continuationNotice, playerInputAppend, toolStepN
|
|
|
13
13
|
import { renderFreshContextCoverage } from "../prompt/WorldMaterialCoverage.js";
|
|
14
14
|
import { renderWriteSizeAdvice, readDeclaredWorldClock, } from "../prompt/WorldMaintenanceReport.js";
|
|
15
15
|
import { WorldDocumentStore } from "../world/WorldDocumentStore.js";
|
|
16
|
-
import { equalModelHostBinding,
|
|
16
|
+
import { equalModelHostBinding, ModelHostCancelledError, } from "../model/ModelHost.js";
|
|
17
17
|
import { FileNativePromptCompiler, } from "../prompt/FileNativePromptCompiler.js";
|
|
18
18
|
import { parsePlayPresetFiles, } from "./FileNativePlayPresetStore.js";
|
|
19
19
|
import { runPlayFollowupRequests, } from "./PlayFollowupRequests.js";
|
|
@@ -82,6 +82,7 @@ export class PlayCallChain {
|
|
|
82
82
|
#invocationSettled = new Map();
|
|
83
83
|
/** Worlds whose release is queued in their WorldQueue slot. */
|
|
84
84
|
#releasing = new Set();
|
|
85
|
+
#advances;
|
|
85
86
|
constructor(worlds, compiler = new FileNativePromptCompiler(), artifacts, failureLog, changed = () => {
|
|
86
87
|
/* No observers attached. */
|
|
87
88
|
}) {
|
|
@@ -90,6 +91,10 @@ export class PlayCallChain {
|
|
|
90
91
|
this.#compiler = compiler;
|
|
91
92
|
this.#artifacts = artifacts;
|
|
92
93
|
this.#failureLog = failureLog;
|
|
94
|
+
this.#advances = new PlayAdvanceLifecycle({
|
|
95
|
+
persist: (session) => this.#persist(session),
|
|
96
|
+
clearMarker: (advance) => this.#worlds.playAdvances.markSettled(advance),
|
|
97
|
+
});
|
|
93
98
|
}
|
|
94
99
|
async start(input) {
|
|
95
100
|
return this.#runInvocation(input, (signal) => this.#start(input, signal));
|
|
@@ -115,32 +120,12 @@ export class PlayCallChain {
|
|
|
115
120
|
if (active?.status === "running")
|
|
116
121
|
throw new PlayCallChainError("A model request is still running for this world.");
|
|
117
122
|
const binding = await this.#worlds.bindPlayCallChain(input.worldId);
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
commit: binding.parentHead,
|
|
125
|
-
},
|
|
126
|
-
hostBinding: structuredClone(input.hostBinding),
|
|
127
|
-
world: {
|
|
128
|
-
controlFingerprint: fingerprintControl(binding.files),
|
|
129
|
-
documentSnapshot: documents.snapshot,
|
|
130
|
-
additionalMaterials: structuredClone(binding.additionalMaterials),
|
|
131
|
-
history: structuredClone(binding.history),
|
|
132
|
-
historyBackgrounds: structuredClone(binding.historyBackgrounds ?? {}),
|
|
133
|
-
narrativeCheckpoint: binding.narrativeCheckpoint,
|
|
134
|
-
documentMaintenance,
|
|
135
|
-
documentMaintenanceUnavailableReason: documents.maintenanceWarning,
|
|
136
|
-
},
|
|
137
|
-
playerInputPlacement: "append",
|
|
138
|
-
playerInput: input.playerText,
|
|
139
|
-
modelBinding: structuredClone(input.modelBinding),
|
|
140
|
-
}, input.playPreset);
|
|
141
|
-
const tools = structuredClone(compilation.toolUniverse);
|
|
142
|
-
const bootstrap = structuredClone(compilation.bootstrap);
|
|
143
|
-
documents.bindBootstrap(bootstrap);
|
|
123
|
+
const fresh = await this.#compileFreshContext(input.worldId, binding, {
|
|
124
|
+
hostBinding: input.hostBinding,
|
|
125
|
+
playPreset: input.playPreset,
|
|
126
|
+
modelBinding: input.modelBinding,
|
|
127
|
+
playerText: input.playerText,
|
|
128
|
+
});
|
|
144
129
|
const now = Date.now();
|
|
145
130
|
const session = {
|
|
146
131
|
schemaVersion: 3,
|
|
@@ -159,14 +144,14 @@ export class PlayCallChain {
|
|
|
159
144
|
name: input.playPreset.name,
|
|
160
145
|
revision: input.playPreset.revision,
|
|
161
146
|
},
|
|
162
|
-
followups:
|
|
147
|
+
followups: fresh.followups,
|
|
163
148
|
playPresetScriptsEnabled: input.playPreset.scriptsEnabled,
|
|
164
149
|
presetFiles: structuredClone(input.playPreset.files),
|
|
165
150
|
modelBinding: structuredClone(input.modelBinding),
|
|
166
151
|
status: "ready",
|
|
167
152
|
canRetry: false,
|
|
168
|
-
bootstrap,
|
|
169
|
-
tools:
|
|
153
|
+
bootstrap: fresh.bootstrap,
|
|
154
|
+
tools: fresh.tools,
|
|
170
155
|
transcript: [],
|
|
171
156
|
events: [],
|
|
172
157
|
completedTools: [],
|
|
@@ -178,7 +163,7 @@ export class PlayCallChain {
|
|
|
178
163
|
lastRequestAttempt: 0,
|
|
179
164
|
lastFailure: null,
|
|
180
165
|
updatedAt: now,
|
|
181
|
-
documents,
|
|
166
|
+
documents: fresh.documents,
|
|
182
167
|
history: historyEntries(binding.history, binding.historyBackgrounds),
|
|
183
168
|
narrativeCheckpoint: binding.narrativeCheckpoint,
|
|
184
169
|
completedToolMap: new Map(),
|
|
@@ -195,6 +180,45 @@ export class PlayCallChain {
|
|
|
195
180
|
await this.#worlds.completeArchiveFreshContext(session.worldId);
|
|
196
181
|
return this.#submitPlayer(session, input.exchangeId, input.playerText, "fresh", input.modelHost, signal, input.observer);
|
|
197
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Compiles a fresh context's input and binds its documents to the bootstrap.
|
|
185
|
+
* Starting a context and a player revision that begins one both come through
|
|
186
|
+
* here; they differ only in the world binding they read. A continuing context
|
|
187
|
+
* compiles against its frozen world context instead (`#refreshPrompt`).
|
|
188
|
+
*/
|
|
189
|
+
async #compileFreshContext(worldId, binding, input) {
|
|
190
|
+
const documents = new FileNativePlayDocuments(binding.files);
|
|
191
|
+
const documentMaintenance = await this.#bindDocumentMaintenance(documents, worldId, binding.parentHead);
|
|
192
|
+
const compilation = this.#compiler.compilePlayCallChain({
|
|
193
|
+
extensionControls: await this.#worlds.extensionControls(worldId, input.playPreset, this.#compiler.locale),
|
|
194
|
+
endpoint: {
|
|
195
|
+
id: `${worldId}:${binding.parentHead}`,
|
|
196
|
+
commit: binding.parentHead,
|
|
197
|
+
},
|
|
198
|
+
hostBinding: structuredClone(input.hostBinding),
|
|
199
|
+
world: {
|
|
200
|
+
controlFingerprint: fingerprintControl(binding.files),
|
|
201
|
+
documentSnapshot: documents.snapshot,
|
|
202
|
+
additionalMaterials: structuredClone(binding.additionalMaterials),
|
|
203
|
+
history: structuredClone(binding.history),
|
|
204
|
+
historyBackgrounds: structuredClone(binding.historyBackgrounds ?? {}),
|
|
205
|
+
narrativeCheckpoint: binding.narrativeCheckpoint,
|
|
206
|
+
documentMaintenance,
|
|
207
|
+
documentMaintenanceUnavailableReason: documents.maintenanceWarning,
|
|
208
|
+
},
|
|
209
|
+
playerInputPlacement: "append",
|
|
210
|
+
playerInput: input.playerText,
|
|
211
|
+
modelBinding: structuredClone(input.modelBinding),
|
|
212
|
+
}, input.playPreset);
|
|
213
|
+
const bootstrap = structuredClone(compilation.bootstrap);
|
|
214
|
+
documents.bindBootstrap(bootstrap);
|
|
215
|
+
return {
|
|
216
|
+
documents,
|
|
217
|
+
bootstrap,
|
|
218
|
+
tools: structuredClone(compilation.toolUniverse),
|
|
219
|
+
followups: structuredClone(compilation.followups ?? []),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
198
222
|
async append(input) {
|
|
199
223
|
return this.#runInvocation(input, (signal) => this.#append(input, signal));
|
|
200
224
|
}
|
|
@@ -896,35 +920,17 @@ export class PlayCallChain {
|
|
|
896
920
|
}
|
|
897
921
|
async #prepareFreshPlayerRevision(worldId, freshContext, restoresHead, replacementText) {
|
|
898
922
|
const binding = await this.#worlds.bindPlayCallChainAt(worldId, restoresHead);
|
|
899
|
-
const
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
commit: binding.parentHead,
|
|
906
|
-
},
|
|
907
|
-
hostBinding: structuredClone(freshContext.hostBinding),
|
|
908
|
-
world: {
|
|
909
|
-
controlFingerprint: fingerprintControl(binding.files),
|
|
910
|
-
documentSnapshot: documents.snapshot,
|
|
911
|
-
additionalMaterials: structuredClone(binding.additionalMaterials),
|
|
912
|
-
history: structuredClone(binding.history),
|
|
913
|
-
historyBackgrounds: structuredClone(binding.historyBackgrounds ?? {}),
|
|
914
|
-
narrativeCheckpoint: binding.narrativeCheckpoint,
|
|
915
|
-
documentMaintenance,
|
|
916
|
-
documentMaintenanceUnavailableReason: documents.maintenanceWarning,
|
|
917
|
-
},
|
|
918
|
-
playerInputPlacement: "append",
|
|
919
|
-
playerInput: replacementText,
|
|
920
|
-
modelBinding: structuredClone(freshContext.modelBinding),
|
|
921
|
-
}, freshContext.playPreset);
|
|
922
|
-
documents.bindBootstrap(compilation.bootstrap);
|
|
923
|
+
const fresh = await this.#compileFreshContext(worldId, binding, {
|
|
924
|
+
hostBinding: freshContext.hostBinding,
|
|
925
|
+
playPreset: freshContext.playPreset,
|
|
926
|
+
modelBinding: freshContext.modelBinding,
|
|
927
|
+
playerText: replacementText,
|
|
928
|
+
});
|
|
923
929
|
return {
|
|
924
930
|
binding,
|
|
925
|
-
bootstrap:
|
|
926
|
-
tools:
|
|
927
|
-
followups:
|
|
931
|
+
bootstrap: fresh.bootstrap,
|
|
932
|
+
tools: fresh.tools,
|
|
933
|
+
followups: fresh.followups,
|
|
928
934
|
};
|
|
929
935
|
}
|
|
930
936
|
async #buildFreshPlayerRevision(input) {
|
|
@@ -1166,23 +1172,9 @@ export class PlayCallChain {
|
|
|
1166
1172
|
await this.#settlePlayerAdvance(session, advance);
|
|
1167
1173
|
}
|
|
1168
1174
|
catch (error) {
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
const failureKind = classifyPlayFailure(error, false);
|
|
1173
|
-
session.events.push({
|
|
1174
|
-
id: session.nextEventId++,
|
|
1175
|
-
kind: "failure",
|
|
1176
|
-
message,
|
|
1177
|
-
failureKind,
|
|
1178
|
-
});
|
|
1179
|
-
session.status = "interrupted";
|
|
1180
|
-
session.canRetry = false;
|
|
1181
|
-
session.lastFailure = message;
|
|
1182
|
-
session.lastFailureKind = failureKind;
|
|
1183
|
-
await this.#persist(session);
|
|
1184
|
-
await this.#worlds.playAdvances.markSettled(advance);
|
|
1185
|
-
throw new PlayCallChainError(message);
|
|
1175
|
+
const ending = settlementFailureEnding(error, "Failed to write the original player text.");
|
|
1176
|
+
await this.#advances.end(session, ending, advance);
|
|
1177
|
+
throw new PlayCallChainError(ending.message);
|
|
1186
1178
|
}
|
|
1187
1179
|
return this.#dispatch(session, modelHost, structuredClone(nextRequest), 1, signal, observer);
|
|
1188
1180
|
}
|
|
@@ -1223,8 +1215,6 @@ export class PlayCallChain {
|
|
|
1223
1215
|
...session.events.filter(({ id }) => id < advance.eventId),
|
|
1224
1216
|
playerEvent,
|
|
1225
1217
|
];
|
|
1226
|
-
if (session.transcript.length < advance.transcriptStart)
|
|
1227
|
-
throw new PlayCallChainError("The recovered call-chain transcript is shorter than its player prefix.");
|
|
1228
1218
|
session.transcript = [
|
|
1229
1219
|
...session.transcript.slice(0, advance.transcriptStart),
|
|
1230
1220
|
...structuredClone(transcriptAppendOf(advance.nextRequest).slice(advance.transcriptStart)),
|
|
@@ -1237,24 +1227,28 @@ export class PlayCallChain {
|
|
|
1237
1227
|
else
|
|
1238
1228
|
session.narrativeOrigin = structuredClone(advance.narrativeOrigin);
|
|
1239
1229
|
session.exchange = advance.exchange;
|
|
1240
|
-
session
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
"The player message is committed; the frozen model request is ready to continue.";
|
|
1246
|
-
session.lastFailureKind = "retryable";
|
|
1247
|
-
await this.#persist(session);
|
|
1248
|
-
await this.#worlds.playAdvances.markSettled(advance);
|
|
1230
|
+
await this.#advances.end(session, {
|
|
1231
|
+
kind: "resumable",
|
|
1232
|
+
reason: "player_committed",
|
|
1233
|
+
request: advance.nextRequest,
|
|
1234
|
+
}, advance);
|
|
1249
1235
|
}
|
|
1250
|
-
|
|
1236
|
+
/**
|
|
1237
|
+
* Settles one complete model response in this process. The tool batch, the
|
|
1238
|
+
* narrative and state commit, and the transcript and event tail advance
|
|
1239
|
+
* together. Nothing is kept for a replay: a process that stops before this
|
|
1240
|
+
* returns is recovered as an interrupted exchange (ADR-0044).
|
|
1241
|
+
*/
|
|
1242
|
+
async #settleResponse(session, advance, response) {
|
|
1243
|
+
const durable = durableModelResponse(response);
|
|
1251
1244
|
const existing = session.events.find(({ id }) => id === advance.eventId);
|
|
1252
1245
|
if (existing?.kind !== "assistant")
|
|
1253
1246
|
throw new PlayCallChainError("The durable Provider result no longer matches its assistant event.");
|
|
1254
|
-
const calls =
|
|
1255
|
-
const text =
|
|
1247
|
+
const calls = durable.toolCalls ?? [];
|
|
1248
|
+
const text = durable.text ?? existing.text;
|
|
1256
1249
|
const hasText = text.trim().length > 0;
|
|
1257
1250
|
const responseKind = calls.length > 0 ? "tool_step" : hasText ? "narrative" : "empty";
|
|
1251
|
+
const narrative = responseKind === "narrative";
|
|
1258
1252
|
const assistantEvent = {
|
|
1259
1253
|
id: advance.eventId,
|
|
1260
1254
|
kind: "assistant",
|
|
@@ -1263,18 +1257,18 @@ export class PlayCallChain {
|
|
|
1263
1257
|
responseKind,
|
|
1264
1258
|
exchange: advance.exchange,
|
|
1265
1259
|
attempt: advance.attempt,
|
|
1266
|
-
...(
|
|
1260
|
+
...(durable.reasoningContent === undefined
|
|
1267
1261
|
? existing.reasoning === undefined
|
|
1268
1262
|
? {}
|
|
1269
1263
|
: { reasoning: existing.reasoning }
|
|
1270
|
-
: { reasoning:
|
|
1271
|
-
...(
|
|
1264
|
+
: { reasoning: durable.reasoningContent }),
|
|
1265
|
+
...(durable.usage === undefined
|
|
1272
1266
|
? {}
|
|
1273
|
-
: { usage: structuredClone(
|
|
1274
|
-
...(
|
|
1267
|
+
: { usage: structuredClone(durable.usage) }),
|
|
1268
|
+
...(durable.stopReason === undefined
|
|
1275
1269
|
? {}
|
|
1276
|
-
: { stopReason:
|
|
1277
|
-
continuation:
|
|
1270
|
+
: { stopReason: durable.stopReason }),
|
|
1271
|
+
continuation: durable.providerState === undefined ? "unavailable" : "available",
|
|
1278
1272
|
};
|
|
1279
1273
|
const workingTools = new Map(session.completedToolMap);
|
|
1280
1274
|
const prepared = [];
|
|
@@ -1300,8 +1294,8 @@ export class PlayCallChain {
|
|
|
1300
1294
|
trailingEvents.push(structuredClone(item.event));
|
|
1301
1295
|
}
|
|
1302
1296
|
const stateChanges = session.documents.stateChanges();
|
|
1303
|
-
// Resolve once after the entire tool batch
|
|
1304
|
-
//
|
|
1297
|
+
// Resolve once after the entire tool batch, so every write result reports
|
|
1298
|
+
// the prediction that the commit below makes true.
|
|
1305
1299
|
const writes = [...workingTools.values()].filter(({ result }) => result.candidateWrite !== undefined);
|
|
1306
1300
|
let worldClock;
|
|
1307
1301
|
if (writes.length > 0) {
|
|
@@ -1346,12 +1340,11 @@ export class PlayCallChain {
|
|
|
1346
1340
|
result.candidateWrite.freshContextCoverage = `${this.#compiler.locale === "zh-CN" ? "暂时无法判定下次注入;写入结果不受影响。原因" : "Fresh-context coverage unavailable; the write result is unaffected. Reason"}: ${reason}`;
|
|
1347
1341
|
}
|
|
1348
1342
|
}
|
|
1349
|
-
const visibleText = responseKind === "narrative";
|
|
1350
1343
|
const previousNarrative = session.events.findLast((event) => event.kind === "assistant" &&
|
|
1351
1344
|
event.committedHead !== undefined &&
|
|
1352
1345
|
event.responseKind === "narrative");
|
|
1353
1346
|
const previousPlayer = session.events.findLast((event) => event.kind === "player");
|
|
1354
|
-
const checkpointRequested =
|
|
1347
|
+
const checkpointRequested = narrative &&
|
|
1355
1348
|
session.events.some((event) => event.id >
|
|
1356
1349
|
Math.max(previousNarrative?.id ?? 0, previousPlayer?.id ?? 0) &&
|
|
1357
1350
|
event.kind === "tool_result" &&
|
|
@@ -1369,177 +1362,135 @@ export class PlayCallChain {
|
|
|
1369
1362
|
const assistantItem = {
|
|
1370
1363
|
kind: "assistant",
|
|
1371
1364
|
text,
|
|
1372
|
-
...(
|
|
1365
|
+
...(durable.reasoningContent === undefined
|
|
1373
1366
|
? {}
|
|
1374
|
-
: { reasoningContent:
|
|
1375
|
-
...(
|
|
1367
|
+
: { reasoningContent: durable.reasoningContent }),
|
|
1368
|
+
...(durable.providerState === undefined
|
|
1376
1369
|
? {}
|
|
1377
|
-
: { providerState:
|
|
1370
|
+
: { providerState: durable.providerState }),
|
|
1378
1371
|
toolCalls: structuredClone(calls),
|
|
1379
1372
|
};
|
|
1380
|
-
const background =
|
|
1373
|
+
const background = narrative
|
|
1381
1374
|
? narrativeBackground(advance.narrativeOrigin, session.documents.snapshot)
|
|
1382
1375
|
: undefined;
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
// recorded; every newly prepared fact is checked against the current rule.
|
|
1415
|
-
if (settlement.assistantEvent.responseKind !== undefined) {
|
|
1416
|
-
const expectedResponseKind = hasToolCalls
|
|
1417
|
-
? "tool_step"
|
|
1418
|
-
: settlement.assistantEvent.text.trim().length > 0
|
|
1419
|
-
? "narrative"
|
|
1420
|
-
: "empty";
|
|
1421
|
-
if (settlement.assistantEvent.responseKind !== expectedResponseKind ||
|
|
1422
|
-
settlement.visibleText !== (expectedResponseKind === "narrative"))
|
|
1423
|
-
throw new PlayCallChainError("The prepared response settlement has an inconsistent narrative classification.");
|
|
1424
|
-
}
|
|
1376
|
+
const transcriptStart = session.transcript.length;
|
|
1377
|
+
const transcriptAppend = [
|
|
1378
|
+
...(calls.length > 0 || narrative || durable.providerState !== undefined
|
|
1379
|
+
? [assistantItem]
|
|
1380
|
+
: []),
|
|
1381
|
+
...prepared.map(({ transcript }) => structuredClone(transcript)),
|
|
1382
|
+
...(calls.length > 0 && hasText
|
|
1383
|
+
? [toolStepNotice(this.#compiler.locale)]
|
|
1384
|
+
: []),
|
|
1385
|
+
];
|
|
1386
|
+
const completedTools = [...workingTools.values()].map((item) => structuredClone(item));
|
|
1387
|
+
const failedTools = trailingEvents.filter((candidate) => candidate.kind === "tool_result" && !candidate.ok);
|
|
1388
|
+
if (failedTools.length > 0 && response.diagnostics !== undefined)
|
|
1389
|
+
await this.#failureLog?.recordFailure({
|
|
1390
|
+
exchange: response.diagnostics,
|
|
1391
|
+
failures: [
|
|
1392
|
+
{
|
|
1393
|
+
kind: "tool_execution",
|
|
1394
|
+
message: "A Runtime tool call from the model was rejected.",
|
|
1395
|
+
details: {
|
|
1396
|
+
calls: failedTools.map((result) => ({
|
|
1397
|
+
id: result.callId,
|
|
1398
|
+
name: result.name,
|
|
1399
|
+
ok: result.ok,
|
|
1400
|
+
markdown: result.markdown,
|
|
1401
|
+
replayed: result.replayed,
|
|
1402
|
+
})),
|
|
1403
|
+
},
|
|
1404
|
+
},
|
|
1405
|
+
],
|
|
1406
|
+
});
|
|
1425
1407
|
let committedHead;
|
|
1426
|
-
if (
|
|
1408
|
+
if (narrative || stateChanges.length > 0) {
|
|
1427
1409
|
const outcome = await this.#worlds.commitPlayStep({
|
|
1428
|
-
|
|
1410
|
+
playContext: session.continuityContextId ?? session.chainId,
|
|
1411
|
+
...(worldClock === undefined || stateChanges.length === 0
|
|
1429
1412
|
? {}
|
|
1430
|
-
: {
|
|
1431
|
-
...(settlement.worldClock === undefined
|
|
1432
|
-
? {}
|
|
1433
|
-
: { worldClock: settlement.worldClock }),
|
|
1413
|
+
: { worldClock }),
|
|
1434
1414
|
operationId: advance.operationId,
|
|
1435
1415
|
worldId: session.worldId,
|
|
1436
1416
|
parentHead: advance.parentHead,
|
|
1437
|
-
historyAppend:
|
|
1417
|
+
historyAppend: narrative
|
|
1438
1418
|
? [
|
|
1439
1419
|
{
|
|
1440
1420
|
role: "narrator",
|
|
1441
|
-
exactText:
|
|
1442
|
-
...(
|
|
1443
|
-
? {}
|
|
1444
|
-
: { background: settlement.background }),
|
|
1421
|
+
exactText: text,
|
|
1422
|
+
...(background === undefined ? {} : { background }),
|
|
1445
1423
|
},
|
|
1446
1424
|
]
|
|
1447
1425
|
: [],
|
|
1448
1426
|
nextMaterials: structuredClone(session.nextMaterials),
|
|
1449
|
-
stateChanges: structuredClone(
|
|
1450
|
-
...(
|
|
1451
|
-
? {}
|
|
1452
|
-
: { narrativeCheckpoint: settlement.narrativeCheckpoint }),
|
|
1427
|
+
stateChanges: structuredClone(stateChanges),
|
|
1428
|
+
...(narrativeCheckpoint === undefined ? {} : { narrativeCheckpoint }),
|
|
1453
1429
|
});
|
|
1454
1430
|
committedHead = outcome.head;
|
|
1455
|
-
|
|
1456
|
-
if (
|
|
1457
|
-
|
|
1431
|
+
assistantEvent.committedHead = outcome.head;
|
|
1432
|
+
if (narrativeCheckpoint !== undefined) {
|
|
1433
|
+
assistantEvent.checkpoint = true;
|
|
1458
1434
|
session.narrativeCheckpoint = {
|
|
1459
|
-
...
|
|
1435
|
+
...narrativeCheckpoint,
|
|
1460
1436
|
head: outcome.head,
|
|
1461
1437
|
historyMessageId: `message.${outcome.head.slice("commit:".length)}.1.narrator`,
|
|
1462
1438
|
};
|
|
1463
1439
|
}
|
|
1464
1440
|
appendCommittedHistory(session, outcome.head, outcome.historyAppend);
|
|
1465
1441
|
}
|
|
1466
|
-
|
|
1442
|
+
finalizeWriteReceipts({
|
|
1443
|
+
stateChanges,
|
|
1444
|
+
completedTools,
|
|
1445
|
+
trailingEvents,
|
|
1446
|
+
transcriptAppend,
|
|
1447
|
+
});
|
|
1467
1448
|
session.events = [
|
|
1468
1449
|
...session.events.filter(({ id }) => id < advance.eventId),
|
|
1469
|
-
structuredClone(
|
|
1470
|
-
...structuredClone(
|
|
1450
|
+
structuredClone(assistantEvent),
|
|
1451
|
+
...structuredClone(trailingEvents),
|
|
1471
1452
|
];
|
|
1472
1453
|
session.nextEventId =
|
|
1473
|
-
Math.max(advance.eventId, ...
|
|
1474
|
-
if (session.transcript.length < settlement.transcriptStart)
|
|
1475
|
-
throw new PlayCallChainError("The recovered call-chain transcript is shorter than its settlement prefix.");
|
|
1454
|
+
Math.max(advance.eventId, ...trailingEvents.map(({ id }) => id)) + 1;
|
|
1476
1455
|
session.transcript = [
|
|
1477
|
-
...session.transcript.slice(0,
|
|
1478
|
-
...structuredClone(
|
|
1456
|
+
...session.transcript.slice(0, transcriptStart),
|
|
1457
|
+
...structuredClone(transcriptAppend),
|
|
1479
1458
|
];
|
|
1480
|
-
session.completedTools = structuredClone(
|
|
1481
|
-
session.completedToolMap = new Map(
|
|
1482
|
-
item.key,
|
|
1483
|
-
structuredClone(item),
|
|
1484
|
-
]));
|
|
1459
|
+
session.completedTools = structuredClone(completedTools);
|
|
1460
|
+
session.completedToolMap = new Map(completedTools.map((item) => [item.key, structuredClone(item)]));
|
|
1485
1461
|
if (committedHead !== undefined)
|
|
1486
1462
|
session.parentHead = committedHead;
|
|
1487
|
-
if (
|
|
1463
|
+
if (stateChanges.length > 0) {
|
|
1488
1464
|
session.documents.acceptCommittedState();
|
|
1489
|
-
mergeChangedDocuments(session,
|
|
1490
|
-
}
|
|
1491
|
-
if (settlement.stateChanges.length > 0)
|
|
1465
|
+
mergeChangedDocuments(session, stateChanges);
|
|
1492
1466
|
await this.#bindDocumentMaintenance(session.documents, session.worldId, session.parentHead);
|
|
1467
|
+
}
|
|
1468
|
+
const hasToolCalls = calls.length > 0;
|
|
1493
1469
|
session.exchange = Math.max(session.exchange, advance.exchange);
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
const message = "The complete model response has no provider-native continuation payload; use a fresh context.";
|
|
1500
|
-
session.events.push({
|
|
1501
|
-
id: session.nextEventId++,
|
|
1502
|
-
kind: "failure",
|
|
1503
|
-
message,
|
|
1504
|
-
failureKind: "context_unavailable",
|
|
1505
|
-
});
|
|
1506
|
-
session.status = "interrupted";
|
|
1507
|
-
session.lastFailure = message;
|
|
1508
|
-
session.lastFailureKind = "context_unavailable";
|
|
1509
|
-
result = { kind: "terminal", status: "interrupted" };
|
|
1470
|
+
// The chain has been `running` since its dispatch began, so its failure
|
|
1471
|
+
// fields are already clear; the ending below replaces them.
|
|
1472
|
+
if (assistantEvent.continuation !== "available") {
|
|
1473
|
+
await this.#advances.end(session, noContinuationEnding(), advance);
|
|
1474
|
+
return { kind: "terminal", status: "interrupted", narrative };
|
|
1510
1475
|
}
|
|
1511
|
-
|
|
1476
|
+
if (hasToolCalls) {
|
|
1512
1477
|
session.exchange += 1;
|
|
1513
1478
|
const nextRequest = createRequestWithMaxOutputTokens(session, session.lastRequest?.maxOutputTokens ?? 1, session.exchange);
|
|
1514
1479
|
if (session.lastRequest?.modelConfig !== undefined)
|
|
1515
1480
|
nextRequest.modelConfig = structuredClone(session.lastRequest.modelConfig);
|
|
1516
|
-
session
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
session.lastFailureKind = "retryable";
|
|
1523
|
-
result = { kind: "continue", nextRequest };
|
|
1481
|
+
await this.#advances.end(session, {
|
|
1482
|
+
kind: "resumable",
|
|
1483
|
+
reason: "tool_step_settled",
|
|
1484
|
+
request: nextRequest,
|
|
1485
|
+
}, advance);
|
|
1486
|
+
return { kind: "continue", nextRequest };
|
|
1524
1487
|
}
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
const message = "The model returned neither text nor a complete tool call.";
|
|
1529
|
-
session.events.push({
|
|
1530
|
-
id: session.nextEventId++,
|
|
1531
|
-
kind: "failure",
|
|
1532
|
-
message,
|
|
1533
|
-
failureKind: "other",
|
|
1534
|
-
});
|
|
1535
|
-
session.lastFailure = message;
|
|
1536
|
-
session.lastFailureKind = "other";
|
|
1537
|
-
}
|
|
1538
|
-
result = { kind: "terminal", status: session.status };
|
|
1488
|
+
if (!narrative) {
|
|
1489
|
+
await this.#advances.end(session, emptyResponseEnding(), advance);
|
|
1490
|
+
return { kind: "terminal", status: "interrupted", narrative };
|
|
1539
1491
|
}
|
|
1540
|
-
await this.#
|
|
1541
|
-
|
|
1542
|
-
return result;
|
|
1492
|
+
await this.#advances.end(session, { kind: "ready" }, advance);
|
|
1493
|
+
return { kind: "terminal", status: "ready", narrative };
|
|
1543
1494
|
}
|
|
1544
1495
|
async #dispatch(session, modelHost, firstRequest, firstAttempt, signal, observer) {
|
|
1545
1496
|
let request = modelHost.freezeRequest?.(firstRequest) ?? structuredClone(firstRequest);
|
|
@@ -1549,12 +1500,7 @@ export class PlayCallChain {
|
|
|
1549
1500
|
if (signal.aborted)
|
|
1550
1501
|
return this.#cancelBeforeProviderDispatch(session, request, attempt, observer);
|
|
1551
1502
|
this.#observeInvocation(session.chainId, "waiting");
|
|
1552
|
-
session
|
|
1553
|
-
session.canRetry = false;
|
|
1554
|
-
session.lastFailure = null;
|
|
1555
|
-
delete session.lastFailureKind;
|
|
1556
|
-
session.lastRequest = structuredClone(request);
|
|
1557
|
-
session.lastRequestAttempt = attempt;
|
|
1503
|
+
this.#advances.beginDispatch(session, request, attempt);
|
|
1558
1504
|
const responseExchange = request.exchange ?? session.exchange;
|
|
1559
1505
|
const event = {
|
|
1560
1506
|
id: session.nextEventId++,
|
|
@@ -1639,66 +1585,13 @@ export class PlayCallChain {
|
|
|
1639
1585
|
await this.#failureLog?.recordExchangeIfActive(response.diagnostics);
|
|
1640
1586
|
}
|
|
1641
1587
|
catch (error) {
|
|
1642
|
-
event.
|
|
1643
|
-
const message = error instanceof Error
|
|
1644
|
-
? error.message
|
|
1645
|
-
: "The model request was interrupted.";
|
|
1646
|
-
session.status = "interrupted";
|
|
1647
|
-
const requiresFreshContext = error instanceof ModelHostBindingMismatchError ||
|
|
1648
|
-
error instanceof ModelHostContinuationError ||
|
|
1649
|
-
error instanceof ModelHostOutcomeUnknownError ||
|
|
1650
|
-
modelHostFailureRequiresFreshContext(error);
|
|
1651
|
-
session.canRetry =
|
|
1652
|
-
!requiresFreshContext && error instanceof ModelHostFailureError;
|
|
1653
|
-
session.lastFailure = message;
|
|
1654
|
-
if (error instanceof ModelHostCancelledError) {
|
|
1655
|
-
delete session.lastFailureKind;
|
|
1656
|
-
session.events.push({
|
|
1657
|
-
id: session.nextEventId++,
|
|
1658
|
-
kind: "cancellation",
|
|
1659
|
-
message,
|
|
1660
|
-
});
|
|
1661
|
-
}
|
|
1662
|
-
else {
|
|
1663
|
-
session.lastFailureKind = classifyPlayFailure(error, session.canRetry);
|
|
1664
|
-
session.events.push({
|
|
1665
|
-
id: session.nextEventId++,
|
|
1666
|
-
kind: "failure",
|
|
1667
|
-
message,
|
|
1668
|
-
failureKind: session.lastFailureKind,
|
|
1669
|
-
});
|
|
1670
|
-
}
|
|
1671
|
-
await this.#persist(session);
|
|
1672
|
-
await this.#worlds.playAdvances.markSettled(advance);
|
|
1673
|
-
this.#notifySnapshot(observer, session);
|
|
1588
|
+
await this.#advances.end(session, dispatchFailureEnding(error, event.id), advance, () => this.#notifySnapshot(observer, session));
|
|
1674
1589
|
return projectView(session);
|
|
1675
1590
|
}
|
|
1676
|
-
const durableResponse = durableModelResponse(response);
|
|
1677
1591
|
try {
|
|
1678
|
-
const
|
|
1679
|
-
const failedTools = settlement.trailingEvents.filter((candidate) => candidate.kind === "tool_result" && !candidate.ok);
|
|
1680
|
-
if (failedTools.length > 0 && response.diagnostics !== undefined)
|
|
1681
|
-
await this.#failureLog?.recordFailure({
|
|
1682
|
-
exchange: response.diagnostics,
|
|
1683
|
-
failures: [
|
|
1684
|
-
{
|
|
1685
|
-
kind: "tool_execution",
|
|
1686
|
-
message: "A Runtime tool call from the model was rejected.",
|
|
1687
|
-
details: {
|
|
1688
|
-
calls: failedTools.map((result) => ({
|
|
1689
|
-
id: result.callId,
|
|
1690
|
-
name: result.name,
|
|
1691
|
-
ok: result.ok,
|
|
1692
|
-
markdown: result.markdown,
|
|
1693
|
-
replayed: result.replayed,
|
|
1694
|
-
})),
|
|
1695
|
-
},
|
|
1696
|
-
},
|
|
1697
|
-
],
|
|
1698
|
-
});
|
|
1699
|
-
const settled = await this.#settleResponseAdvance(session, advance, settlement);
|
|
1592
|
+
const settled = await this.#settleResponse(session, advance, response);
|
|
1700
1593
|
if (settled.kind === "terminal") {
|
|
1701
|
-
if (!
|
|
1594
|
+
if (!settled.narrative) {
|
|
1702
1595
|
const message = "The model returned neither text nor a complete tool call.";
|
|
1703
1596
|
if (response.diagnostics !== undefined)
|
|
1704
1597
|
await this.#failureLog?.recordFailure({
|
|
@@ -1738,18 +1631,7 @@ export class PlayCallChain {
|
|
|
1738
1631
|
}
|
|
1739
1632
|
await this.#runFollowups(session, modelHost, signal, observer);
|
|
1740
1633
|
this.#setInvocationAbortable(session.chainId, false);
|
|
1741
|
-
|
|
1742
|
-
session.events.push({
|
|
1743
|
-
id: session.nextEventId++,
|
|
1744
|
-
kind: "cancellation",
|
|
1745
|
-
message: "The player cancelled the remaining follow-up generation after the narrative was committed.",
|
|
1746
|
-
});
|
|
1747
|
-
touch(session);
|
|
1748
|
-
this.changed(session.worldId);
|
|
1749
|
-
}
|
|
1750
|
-
session.status = "ready";
|
|
1751
|
-
await this.#persist(session);
|
|
1752
|
-
this.#notifySnapshot(observer, session);
|
|
1634
|
+
await this.#advances.end(session, { kind: "ready", followupsCancelled: signal.aborted }, null, () => this.#notifySnapshot(observer, session));
|
|
1753
1635
|
return projectView(session);
|
|
1754
1636
|
}
|
|
1755
1637
|
this.#notifySnapshot(observer, session);
|
|
@@ -1757,58 +1639,29 @@ export class PlayCallChain {
|
|
|
1757
1639
|
attempt = 1;
|
|
1758
1640
|
}
|
|
1759
1641
|
catch (error) {
|
|
1760
|
-
const
|
|
1761
|
-
? error.message
|
|
1762
|
-
: "Call-chain processing failed.";
|
|
1642
|
+
const ending = settlementFailureEnding(error, "Call-chain processing failed.");
|
|
1763
1643
|
if (response.diagnostics !== undefined)
|
|
1764
1644
|
await this.#failureLog?.recordFailure({
|
|
1765
1645
|
exchange: response.diagnostics,
|
|
1766
1646
|
failures: [
|
|
1767
1647
|
{
|
|
1768
1648
|
kind: "runtime_post_processing",
|
|
1769
|
-
message,
|
|
1649
|
+
message: ending.message,
|
|
1770
1650
|
},
|
|
1771
1651
|
],
|
|
1772
1652
|
});
|
|
1773
|
-
|
|
1774
|
-
session.events.push({
|
|
1775
|
-
id: session.nextEventId++,
|
|
1776
|
-
kind: "failure",
|
|
1777
|
-
message,
|
|
1778
|
-
failureKind,
|
|
1779
|
-
});
|
|
1780
|
-
session.status = "interrupted";
|
|
1781
|
-
session.canRetry = false;
|
|
1782
|
-
session.lastFailure = message;
|
|
1783
|
-
session.lastFailureKind = failureKind;
|
|
1784
|
-
await this.#persist(session);
|
|
1785
|
-
this.#notifySnapshot(observer, session);
|
|
1653
|
+
await this.#advances.end(session, ending, advance, () => this.#notifySnapshot(observer, session));
|
|
1786
1654
|
return projectView(session);
|
|
1787
1655
|
}
|
|
1788
1656
|
}
|
|
1789
1657
|
}
|
|
1790
1658
|
async #cancelBeforeProviderDispatch(session, request, attempt, observer, advance) {
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
session.events.push({
|
|
1798
|
-
id: session.nextEventId++,
|
|
1799
|
-
kind: "cancellation",
|
|
1800
|
-
message,
|
|
1801
|
-
});
|
|
1802
|
-
session.status = "interrupted";
|
|
1803
|
-
session.canRetry = true;
|
|
1804
|
-
session.lastRequest = structuredClone(request);
|
|
1805
|
-
session.lastRequestAttempt = Math.max(0, attempt - 1);
|
|
1806
|
-
session.lastFailure = message;
|
|
1807
|
-
delete session.lastFailureKind;
|
|
1808
|
-
await this.#persist(session);
|
|
1809
|
-
if (advance !== undefined)
|
|
1810
|
-
await this.#worlds.playAdvances.markSettled(advance);
|
|
1811
|
-
this.#notifySnapshot(observer, session);
|
|
1659
|
+
await this.#advances.end(session, {
|
|
1660
|
+
kind: "cancelled_before_dispatch",
|
|
1661
|
+
request,
|
|
1662
|
+
attempt,
|
|
1663
|
+
...(advance === undefined ? {} : { streamingEventId: advance.eventId }),
|
|
1664
|
+
}, advance ?? null, () => this.#notifySnapshot(observer, session));
|
|
1812
1665
|
return projectView(session);
|
|
1813
1666
|
}
|
|
1814
1667
|
/**
|
|
@@ -1982,42 +1835,10 @@ export class PlayCallChain {
|
|
|
1982
1835
|
: loaded;
|
|
1983
1836
|
if (advance === null && session.status !== "running")
|
|
1984
1837
|
return;
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
await this.#worlds.playAdvances.markSettled(advance);
|
|
1988
|
-
}
|
|
1989
|
-
async #interruptAbandonedDispatch(session, advance) {
|
|
1838
|
+
// Authority already moved past the persisted parent head: the world cannot
|
|
1839
|
+
// be replayed onto that head, so the recovery requires a fresh context.
|
|
1990
1840
|
const headMoved = (await this.#worlds.currentHead(session.worldId)) !== session.parentHead;
|
|
1991
|
-
|
|
1992
|
-
const streaming = playerPhase
|
|
1993
|
-
? undefined
|
|
1994
|
-
: session.events.findLast((event) => event.kind === "assistant" &&
|
|
1995
|
-
event.status === "streaming" &&
|
|
1996
|
-
(advance === null || event.id === advance.eventId));
|
|
1997
|
-
if (streaming !== undefined)
|
|
1998
|
-
streaming.status = "interrupted";
|
|
1999
|
-
// Authority already moved past the persisted parent head: the world
|
|
2000
|
-
// cannot be replayed onto that head, so a fresh context is required.
|
|
2001
|
-
const canRetry = !headMoved && !playerPhase && session.lastRequest !== null;
|
|
2002
|
-
const message = headMoved
|
|
2003
|
-
? "The service stopped after the world was updated; use a fresh context."
|
|
2004
|
-
: playerPhase
|
|
2005
|
-
? "The service stopped while saving the player message; use a fresh context."
|
|
2006
|
-
: canRetry
|
|
2007
|
-
? "The service stopped while the model request was in progress; retry to call the model again."
|
|
2008
|
-
: "The service stopped before preparing a model request; use a fresh context.";
|
|
2009
|
-
const failureKind = canRetry ? "retryable" : "other";
|
|
2010
|
-
session.events.push({
|
|
2011
|
-
id: session.nextEventId++,
|
|
2012
|
-
kind: "failure",
|
|
2013
|
-
message,
|
|
2014
|
-
failureKind,
|
|
2015
|
-
});
|
|
2016
|
-
session.status = "interrupted";
|
|
2017
|
-
session.canRetry = canRetry;
|
|
2018
|
-
session.lastFailure = message;
|
|
2019
|
-
session.lastFailureKind = failureKind;
|
|
2020
|
-
await this.#persist(session);
|
|
1841
|
+
await this.#advances.recoverAbandoned(session, advance, headMoved);
|
|
2021
1842
|
}
|
|
2022
1843
|
async #assertCurrentHead(session) {
|
|
2023
1844
|
if ((await this.#worlds.currentHead(session.worldId)) !== session.parentHead)
|
|
@@ -2162,7 +1983,11 @@ function durableModelResponse(response) {
|
|
|
2162
1983
|
void _diagnostics;
|
|
2163
1984
|
return structuredClone(durable);
|
|
2164
1985
|
}
|
|
2165
|
-
|
|
1986
|
+
/**
|
|
1987
|
+
* Copies each committed write's receipt into the tool results that report it,
|
|
1988
|
+
* then drops the candidate write. Receipts state what the commit made true.
|
|
1989
|
+
*/
|
|
1990
|
+
function finalizeWriteReceipts(settlement) {
|
|
2166
1991
|
const committedWriteRefs = new Set(settlement.stateChanges.map(({ stableShortRef }) => stableShortRef));
|
|
2167
1992
|
const markdownByCall = new Map();
|
|
2168
1993
|
for (const completed of settlement.completedTools) {
|
|
@@ -2402,6 +2227,7 @@ function projectView(session) {
|
|
|
2402
2227
|
...projectContext(session),
|
|
2403
2228
|
worldId: session.worldId,
|
|
2404
2229
|
previousContexts: (session.previousContexts ?? []).map((context) => projectContext(context)),
|
|
2230
|
+
...(lastEndingCancelled(session) ? { lastCancelled: true } : {}),
|
|
2405
2231
|
};
|
|
2406
2232
|
}
|
|
2407
2233
|
function projectContextReads(session) {
|