narraeon 0.7.4 → 0.7.6
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/protocol/v1.js +4 -0
- package/dist/node/runtime/V1Runtime.js +13 -1
- package/dist/node/runtime/content/CurrentTreeContentLibrary.js +64 -163
- package/dist/node/runtime/content/FileNativeContentTree.js +16 -13
- package/dist/node/runtime/model/ToolArgumentCoercion.js +138 -0
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +34 -18
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +6 -2
- package/dist/node/runtime/play/OrderedPlayPrompts.js +3 -6
- package/dist/node/runtime/play/PlayAdvanceLifecycle.js +211 -0
- package/dist/node/runtime/play/PlayCallChain.js +276 -398
- package/dist/node/runtime/play/PlayDocumentTools.js +19 -0
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +3 -3
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +15 -0
- package/dist/node/runtime/prompt/ToolNameAlias.js +14 -0
- package/dist/node/runtime/setting/SettingAuthoringTransaction.js +370 -169
- 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/dist/node/shared/ordered-play-prompts.js +5 -0
- package/dist/node/shared/turn-guide.js +27 -2
- package/dist/web/assets/index-Cj2JYCia.js +1408 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/index-9N9kO8kY.js +0 -1408
|
@@ -1,19 +1,22 @@
|
|
|
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";
|
|
6
6
|
import { hasLegacyHistoryTools } from "../prompt/FileNativeToolRegistry.js";
|
|
7
|
+
import { renderToolAliasNote, resolveToolName, } from "../prompt/ToolNameAlias.js";
|
|
7
8
|
import { PlayCallChainError } from "./PlayCallChainError.js";
|
|
8
9
|
import { preparePlayerRevisionTrace, prepareForkTrace, restorePlayDocuments, independentContextCopy, } from "./PlayTraceRetention.js";
|
|
9
10
|
import { cloneSharingPromptRuns, currentPlayPrompt, recordedPlayPrompt, playPromptRunsThroughEvents, promptRunStrings, sharePromptRun, } from "./PlayPromptRun.js";
|
|
10
11
|
import { comparePromptPrefixes } from "../prompt/WorldPromptDiagnostics.js";
|
|
11
12
|
import { createHash, randomBytes } from "node:crypto";
|
|
13
|
+
import { isTurnGuideLine } from "../../shared/ordered-play-prompts.js";
|
|
14
|
+
import { turnGuideWithTurnNote } from "../../shared/turn-guide.js";
|
|
12
15
|
import { completedPlayerRounds, continuationNotice, playerInputAppend, toolStepNotice, turnGuideNotice, } from "./PlayContinuity.js";
|
|
13
16
|
import { renderFreshContextCoverage } from "../prompt/WorldMaterialCoverage.js";
|
|
14
17
|
import { renderWriteSizeAdvice, readDeclaredWorldClock, } from "../prompt/WorldMaintenanceReport.js";
|
|
15
18
|
import { WorldDocumentStore } from "../world/WorldDocumentStore.js";
|
|
16
|
-
import { equalModelHostBinding,
|
|
19
|
+
import { equalModelHostBinding, ModelHostCancelledError, } from "../model/ModelHost.js";
|
|
17
20
|
import { FileNativePromptCompiler, } from "../prompt/FileNativePromptCompiler.js";
|
|
18
21
|
import { parsePlayPresetFiles, } from "./FileNativePlayPresetStore.js";
|
|
19
22
|
import { runPlayFollowupRequests, } from "./PlayFollowupRequests.js";
|
|
@@ -82,6 +85,7 @@ export class PlayCallChain {
|
|
|
82
85
|
#invocationSettled = new Map();
|
|
83
86
|
/** Worlds whose release is queued in their WorldQueue slot. */
|
|
84
87
|
#releasing = new Set();
|
|
88
|
+
#advances;
|
|
85
89
|
constructor(worlds, compiler = new FileNativePromptCompiler(), artifacts, failureLog, changed = () => {
|
|
86
90
|
/* No observers attached. */
|
|
87
91
|
}) {
|
|
@@ -90,6 +94,10 @@ export class PlayCallChain {
|
|
|
90
94
|
this.#compiler = compiler;
|
|
91
95
|
this.#artifacts = artifacts;
|
|
92
96
|
this.#failureLog = failureLog;
|
|
97
|
+
this.#advances = new PlayAdvanceLifecycle({
|
|
98
|
+
persist: (session) => this.#persist(session),
|
|
99
|
+
clearMarker: (advance) => this.#worlds.playAdvances.markSettled(advance),
|
|
100
|
+
});
|
|
93
101
|
}
|
|
94
102
|
async start(input) {
|
|
95
103
|
return this.#runInvocation(input, (signal) => this.#start(input, signal));
|
|
@@ -98,6 +106,7 @@ export class PlayCallChain {
|
|
|
98
106
|
validateIdentity(input.chainId, "call-chain ID");
|
|
99
107
|
validateIdentity(input.exchangeId, "exchange ID");
|
|
100
108
|
validatePlayerText(input.playerText);
|
|
109
|
+
validTurnNote(input.turnNote);
|
|
101
110
|
assertModelHostBinding(input.modelBinding, input.modelHost);
|
|
102
111
|
// Only a resubmission to the current context needs that context loaded.
|
|
103
112
|
await this.#worlds.ensureCurrentStorage(input.worldId);
|
|
@@ -115,32 +124,12 @@ export class PlayCallChain {
|
|
|
115
124
|
if (active?.status === "running")
|
|
116
125
|
throw new PlayCallChainError("A model request is still running for this world.");
|
|
117
126
|
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);
|
|
127
|
+
const fresh = await this.#compileFreshContext(input.worldId, binding, {
|
|
128
|
+
hostBinding: input.hostBinding,
|
|
129
|
+
playPreset: input.playPreset,
|
|
130
|
+
modelBinding: input.modelBinding,
|
|
131
|
+
playerText: input.playerText,
|
|
132
|
+
});
|
|
144
133
|
const now = Date.now();
|
|
145
134
|
const session = {
|
|
146
135
|
schemaVersion: 3,
|
|
@@ -159,14 +148,15 @@ export class PlayCallChain {
|
|
|
159
148
|
name: input.playPreset.name,
|
|
160
149
|
revision: input.playPreset.revision,
|
|
161
150
|
},
|
|
162
|
-
followups:
|
|
151
|
+
followups: fresh.followups,
|
|
163
152
|
playPresetScriptsEnabled: input.playPreset.scriptsEnabled,
|
|
164
153
|
presetFiles: structuredClone(input.playPreset.files),
|
|
165
154
|
modelBinding: structuredClone(input.modelBinding),
|
|
166
155
|
status: "ready",
|
|
167
156
|
canRetry: false,
|
|
168
|
-
bootstrap,
|
|
169
|
-
tools:
|
|
157
|
+
bootstrap: fresh.bootstrap,
|
|
158
|
+
tools: fresh.tools,
|
|
159
|
+
...turnNoteField(validTurnNote(input.turnNote)),
|
|
170
160
|
transcript: [],
|
|
171
161
|
events: [],
|
|
172
162
|
completedTools: [],
|
|
@@ -178,7 +168,7 @@ export class PlayCallChain {
|
|
|
178
168
|
lastRequestAttempt: 0,
|
|
179
169
|
lastFailure: null,
|
|
180
170
|
updatedAt: now,
|
|
181
|
-
documents,
|
|
171
|
+
documents: fresh.documents,
|
|
182
172
|
history: historyEntries(binding.history, binding.historyBackgrounds),
|
|
183
173
|
narrativeCheckpoint: binding.narrativeCheckpoint,
|
|
184
174
|
completedToolMap: new Map(),
|
|
@@ -195,6 +185,45 @@ export class PlayCallChain {
|
|
|
195
185
|
await this.#worlds.completeArchiveFreshContext(session.worldId);
|
|
196
186
|
return this.#submitPlayer(session, input.exchangeId, input.playerText, "fresh", input.modelHost, signal, input.observer);
|
|
197
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Compiles a fresh context's input and binds its documents to the bootstrap.
|
|
190
|
+
* Starting a context and a player revision that begins one both come through
|
|
191
|
+
* here; they differ only in the world binding they read. A continuing context
|
|
192
|
+
* compiles against its frozen world context instead (`#refreshPrompt`).
|
|
193
|
+
*/
|
|
194
|
+
async #compileFreshContext(worldId, binding, input) {
|
|
195
|
+
const documents = new FileNativePlayDocuments(binding.files);
|
|
196
|
+
const documentMaintenance = await this.#bindDocumentMaintenance(documents, worldId, binding.parentHead);
|
|
197
|
+
const compilation = this.#compiler.compilePlayCallChain({
|
|
198
|
+
extensionControls: await this.#worlds.extensionControls(worldId, input.playPreset, this.#compiler.locale),
|
|
199
|
+
endpoint: {
|
|
200
|
+
id: `${worldId}:${binding.parentHead}`,
|
|
201
|
+
commit: binding.parentHead,
|
|
202
|
+
},
|
|
203
|
+
hostBinding: structuredClone(input.hostBinding),
|
|
204
|
+
world: {
|
|
205
|
+
controlFingerprint: fingerprintControl(binding.files),
|
|
206
|
+
documentSnapshot: documents.snapshot,
|
|
207
|
+
additionalMaterials: structuredClone(binding.additionalMaterials),
|
|
208
|
+
history: structuredClone(binding.history),
|
|
209
|
+
historyBackgrounds: structuredClone(binding.historyBackgrounds ?? {}),
|
|
210
|
+
narrativeCheckpoint: binding.narrativeCheckpoint,
|
|
211
|
+
documentMaintenance,
|
|
212
|
+
documentMaintenanceUnavailableReason: documents.maintenanceWarning,
|
|
213
|
+
},
|
|
214
|
+
playerInputPlacement: "append",
|
|
215
|
+
playerInput: input.playerText,
|
|
216
|
+
modelBinding: structuredClone(input.modelBinding),
|
|
217
|
+
}, input.playPreset);
|
|
218
|
+
const bootstrap = structuredClone(compilation.bootstrap);
|
|
219
|
+
documents.bindBootstrap(bootstrap);
|
|
220
|
+
return {
|
|
221
|
+
documents,
|
|
222
|
+
bootstrap,
|
|
223
|
+
tools: structuredClone(compilation.toolUniverse),
|
|
224
|
+
followups: structuredClone(compilation.followups ?? []),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
198
227
|
async append(input) {
|
|
199
228
|
return this.#runInvocation(input, (signal) => this.#append(input, signal));
|
|
200
229
|
}
|
|
@@ -219,6 +248,9 @@ export class PlayCallChain {
|
|
|
219
248
|
return this.#dispatch(session, input.modelHost, structuredClone(session.lastRequest), session.lastRequestAttempt + 1, signal, input.observer);
|
|
220
249
|
}
|
|
221
250
|
await this.#refreshPrompt(session, input);
|
|
251
|
+
// Each dispatch carries exactly the request the client sent with it; a
|
|
252
|
+
// continuation without one must not inherit the previous turn's line.
|
|
253
|
+
setTurnNote(session, validTurnNote(input.turnNote));
|
|
222
254
|
session.exchange += 1;
|
|
223
255
|
const last = session.transcript.at(-1);
|
|
224
256
|
if (last?.kind === "assistant" &&
|
|
@@ -227,7 +259,7 @@ export class PlayCallChain {
|
|
|
227
259
|
session.narrativeOrigin = await this.#captureOrigin(session);
|
|
228
260
|
session.transcript.push(continuationNotice(this.#compiler.locale));
|
|
229
261
|
}
|
|
230
|
-
return this.#dispatch(session, input.modelHost, createRequest(session, input.modelHost, session.exchange), 1, signal, input.observer);
|
|
262
|
+
return this.#dispatch(session, input.modelHost, createRequest(session, input.modelHost, session.exchange, this.#compiler.locale), 1, signal, input.observer);
|
|
231
263
|
}
|
|
232
264
|
if (duplicatePlayerExchange(session, input.exchangeId, input.playerText, "append"))
|
|
233
265
|
return projectView(session);
|
|
@@ -236,6 +268,7 @@ export class PlayCallChain {
|
|
|
236
268
|
? "The call chain has an incomplete model response; clear the input and append a continuation, or use a fresh context."
|
|
237
269
|
: "The call chain was interrupted; use a fresh context.");
|
|
238
270
|
await this.#refreshPrompt(session, input);
|
|
271
|
+
setTurnNote(session, validTurnNote(input.turnNote));
|
|
239
272
|
return this.#submitPlayer(session, input.exchangeId, input.playerText, "append", input.modelHost, signal, input.observer);
|
|
240
273
|
}
|
|
241
274
|
async #refreshPrompt(session, input) {
|
|
@@ -722,6 +755,7 @@ export class PlayCallChain {
|
|
|
722
755
|
eventId: input.eventId,
|
|
723
756
|
replacementExchangeId: input.replacementExchangeId,
|
|
724
757
|
replacementText: input.replacementText,
|
|
758
|
+
...turnNoteField(validTurnNote(input.turnNote)),
|
|
725
759
|
continuation: input.continuation,
|
|
726
760
|
restoresHead: playerRevisionRestoresHead(sourceContext, eventIndex),
|
|
727
761
|
discardChainIds: contexts
|
|
@@ -830,6 +864,7 @@ export class PlayCallChain {
|
|
|
830
864
|
previousContexts: [],
|
|
831
865
|
previousChainId,
|
|
832
866
|
timelineGeneration,
|
|
867
|
+
...turnNoteField(intent.turnNote),
|
|
833
868
|
parentHead: head,
|
|
834
869
|
updatedAt: Date.now(),
|
|
835
870
|
};
|
|
@@ -896,35 +931,17 @@ export class PlayCallChain {
|
|
|
896
931
|
}
|
|
897
932
|
async #prepareFreshPlayerRevision(worldId, freshContext, restoresHead, replacementText) {
|
|
898
933
|
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);
|
|
934
|
+
const fresh = await this.#compileFreshContext(worldId, binding, {
|
|
935
|
+
hostBinding: freshContext.hostBinding,
|
|
936
|
+
playPreset: freshContext.playPreset,
|
|
937
|
+
modelBinding: freshContext.modelBinding,
|
|
938
|
+
playerText: replacementText,
|
|
939
|
+
});
|
|
923
940
|
return {
|
|
924
941
|
binding,
|
|
925
|
-
bootstrap:
|
|
926
|
-
tools:
|
|
927
|
-
followups:
|
|
942
|
+
bootstrap: fresh.bootstrap,
|
|
943
|
+
tools: fresh.tools,
|
|
944
|
+
followups: fresh.followups,
|
|
928
945
|
};
|
|
929
946
|
}
|
|
930
947
|
async #buildFreshPlayerRevision(input) {
|
|
@@ -968,6 +985,7 @@ export class PlayCallChain {
|
|
|
968
985
|
revision: input.freshContext.playPreset.revision,
|
|
969
986
|
},
|
|
970
987
|
followups: structuredClone(fresh.followups),
|
|
988
|
+
...turnNoteField(input.intent.turnNote),
|
|
971
989
|
playPresetScriptsEnabled: input.freshContext.playPreset.scriptsEnabled,
|
|
972
990
|
presetFiles: structuredClone(input.freshContext.playPreset.files),
|
|
973
991
|
modelBinding: structuredClone(input.freshContext.modelBinding),
|
|
@@ -1135,10 +1153,7 @@ export class PlayCallChain {
|
|
|
1135
1153
|
throw new PlayCallChainError("The call chain is waiting for the model to return.");
|
|
1136
1154
|
session.narrativeOrigin = await this.#captureOrigin(session);
|
|
1137
1155
|
const exchange = session.exchange + 1;
|
|
1138
|
-
const nextRequest = createRequest(session, modelHost, exchange, [
|
|
1139
|
-
...session.transcript,
|
|
1140
|
-
...playerInputAppend(playerText),
|
|
1141
|
-
]);
|
|
1156
|
+
const nextRequest = createRequest(session, modelHost, exchange, this.#compiler.locale, [...session.transcript, ...playerInputAppend(playerText)]);
|
|
1142
1157
|
const operationKey = `player:${exchangeId}`;
|
|
1143
1158
|
const playerOperationId = operationId(session.chainId, operationKey);
|
|
1144
1159
|
const advance = {
|
|
@@ -1166,23 +1181,9 @@ export class PlayCallChain {
|
|
|
1166
1181
|
await this.#settlePlayerAdvance(session, advance);
|
|
1167
1182
|
}
|
|
1168
1183
|
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);
|
|
1184
|
+
const ending = settlementFailureEnding(error, "Failed to write the original player text.");
|
|
1185
|
+
await this.#advances.end(session, ending, advance);
|
|
1186
|
+
throw new PlayCallChainError(ending.message);
|
|
1186
1187
|
}
|
|
1187
1188
|
return this.#dispatch(session, modelHost, structuredClone(nextRequest), 1, signal, observer);
|
|
1188
1189
|
}
|
|
@@ -1223,8 +1224,6 @@ export class PlayCallChain {
|
|
|
1223
1224
|
...session.events.filter(({ id }) => id < advance.eventId),
|
|
1224
1225
|
playerEvent,
|
|
1225
1226
|
];
|
|
1226
|
-
if (session.transcript.length < advance.transcriptStart)
|
|
1227
|
-
throw new PlayCallChainError("The recovered call-chain transcript is shorter than its player prefix.");
|
|
1228
1227
|
session.transcript = [
|
|
1229
1228
|
...session.transcript.slice(0, advance.transcriptStart),
|
|
1230
1229
|
...structuredClone(transcriptAppendOf(advance.nextRequest).slice(advance.transcriptStart)),
|
|
@@ -1237,24 +1236,28 @@ export class PlayCallChain {
|
|
|
1237
1236
|
else
|
|
1238
1237
|
session.narrativeOrigin = structuredClone(advance.narrativeOrigin);
|
|
1239
1238
|
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);
|
|
1239
|
+
await this.#advances.end(session, {
|
|
1240
|
+
kind: "resumable",
|
|
1241
|
+
reason: "player_committed",
|
|
1242
|
+
request: advance.nextRequest,
|
|
1243
|
+
}, advance);
|
|
1249
1244
|
}
|
|
1250
|
-
|
|
1245
|
+
/**
|
|
1246
|
+
* Settles one complete model response in this process. The tool batch, the
|
|
1247
|
+
* narrative and state commit, and the transcript and event tail advance
|
|
1248
|
+
* together. Nothing is kept for a replay: a process that stops before this
|
|
1249
|
+
* returns is recovered as an interrupted exchange (ADR-0044).
|
|
1250
|
+
*/
|
|
1251
|
+
async #settleResponse(session, advance, response) {
|
|
1252
|
+
const durable = durableModelResponse(response);
|
|
1251
1253
|
const existing = session.events.find(({ id }) => id === advance.eventId);
|
|
1252
1254
|
if (existing?.kind !== "assistant")
|
|
1253
1255
|
throw new PlayCallChainError("The durable Provider result no longer matches its assistant event.");
|
|
1254
|
-
const calls =
|
|
1255
|
-
const text =
|
|
1256
|
+
const calls = durable.toolCalls ?? [];
|
|
1257
|
+
const text = durable.text ?? existing.text;
|
|
1256
1258
|
const hasText = text.trim().length > 0;
|
|
1257
1259
|
const responseKind = calls.length > 0 ? "tool_step" : hasText ? "narrative" : "empty";
|
|
1260
|
+
const narrative = responseKind === "narrative";
|
|
1258
1261
|
const assistantEvent = {
|
|
1259
1262
|
id: advance.eventId,
|
|
1260
1263
|
kind: "assistant",
|
|
@@ -1263,18 +1266,18 @@ export class PlayCallChain {
|
|
|
1263
1266
|
responseKind,
|
|
1264
1267
|
exchange: advance.exchange,
|
|
1265
1268
|
attempt: advance.attempt,
|
|
1266
|
-
...(
|
|
1269
|
+
...(durable.reasoningContent === undefined
|
|
1267
1270
|
? existing.reasoning === undefined
|
|
1268
1271
|
? {}
|
|
1269
1272
|
: { reasoning: existing.reasoning }
|
|
1270
|
-
: { reasoning:
|
|
1271
|
-
...(
|
|
1273
|
+
: { reasoning: durable.reasoningContent }),
|
|
1274
|
+
...(durable.usage === undefined
|
|
1272
1275
|
? {}
|
|
1273
|
-
: { usage: structuredClone(
|
|
1274
|
-
...(
|
|
1276
|
+
: { usage: structuredClone(durable.usage) }),
|
|
1277
|
+
...(durable.stopReason === undefined
|
|
1275
1278
|
? {}
|
|
1276
|
-
: { stopReason:
|
|
1277
|
-
continuation:
|
|
1279
|
+
: { stopReason: durable.stopReason }),
|
|
1280
|
+
continuation: durable.providerState === undefined ? "unavailable" : "available",
|
|
1278
1281
|
};
|
|
1279
1282
|
const workingTools = new Map(session.completedToolMap);
|
|
1280
1283
|
const prepared = [];
|
|
@@ -1282,7 +1285,11 @@ export class PlayCallChain {
|
|
|
1282
1285
|
let nextEventId = advance.eventId + 1;
|
|
1283
1286
|
// Fresh in-memory seed per settlement. It is never persisted: a crashed
|
|
1284
1287
|
// exchange is retried by the player and re-rolls (ADR-0044).
|
|
1285
|
-
const
|
|
1288
|
+
const providedToolNames = currentPlayPrompt(session).tools.map(({ name }) => name);
|
|
1289
|
+
const resolutionSeed = calls.some(({ name }) => {
|
|
1290
|
+
const resolved = resolveToolName(name, providedToolNames);
|
|
1291
|
+
return resolved.kind !== "rejected" && isResolutionTool(resolved.name);
|
|
1292
|
+
})
|
|
1286
1293
|
? randomBytes(32).toString("hex")
|
|
1287
1294
|
: undefined;
|
|
1288
1295
|
for (const call of calls) {
|
|
@@ -1293,6 +1300,9 @@ export class PlayCallChain {
|
|
|
1293
1300
|
kind: "tool_call",
|
|
1294
1301
|
callId: call.id,
|
|
1295
1302
|
name: call.name,
|
|
1303
|
+
...(item.event.executedName === undefined
|
|
1304
|
+
? {}
|
|
1305
|
+
: { executedName: item.event.executedName }),
|
|
1296
1306
|
arguments: structuredClone(call.arguments),
|
|
1297
1307
|
replayed: item.replayed,
|
|
1298
1308
|
});
|
|
@@ -1300,8 +1310,8 @@ export class PlayCallChain {
|
|
|
1300
1310
|
trailingEvents.push(structuredClone(item.event));
|
|
1301
1311
|
}
|
|
1302
1312
|
const stateChanges = session.documents.stateChanges();
|
|
1303
|
-
// Resolve once after the entire tool batch
|
|
1304
|
-
//
|
|
1313
|
+
// Resolve once after the entire tool batch, so every write result reports
|
|
1314
|
+
// the prediction that the commit below makes true.
|
|
1305
1315
|
const writes = [...workingTools.values()].filter(({ result }) => result.candidateWrite !== undefined);
|
|
1306
1316
|
let worldClock;
|
|
1307
1317
|
if (writes.length > 0) {
|
|
@@ -1346,16 +1356,15 @@ export class PlayCallChain {
|
|
|
1346
1356
|
result.candidateWrite.freshContextCoverage = `${this.#compiler.locale === "zh-CN" ? "暂时无法判定下次注入;写入结果不受影响。原因" : "Fresh-context coverage unavailable; the write result is unaffected. Reason"}: ${reason}`;
|
|
1347
1357
|
}
|
|
1348
1358
|
}
|
|
1349
|
-
const visibleText = responseKind === "narrative";
|
|
1350
1359
|
const previousNarrative = session.events.findLast((event) => event.kind === "assistant" &&
|
|
1351
1360
|
event.committedHead !== undefined &&
|
|
1352
1361
|
event.responseKind === "narrative");
|
|
1353
1362
|
const previousPlayer = session.events.findLast((event) => event.kind === "player");
|
|
1354
|
-
const checkpointRequested =
|
|
1363
|
+
const checkpointRequested = narrative &&
|
|
1355
1364
|
session.events.some((event) => event.id >
|
|
1356
1365
|
Math.max(previousNarrative?.id ?? 0, previousPlayer?.id ?? 0) &&
|
|
1357
1366
|
event.kind === "tool_result" &&
|
|
1358
|
-
event.name === "world_checkpoint" &&
|
|
1367
|
+
(event.executedName ?? event.name) === "world_checkpoint" &&
|
|
1359
1368
|
event.ok);
|
|
1360
1369
|
const narrativeCheckpoint = checkpointRequested
|
|
1361
1370
|
? {
|
|
@@ -1369,177 +1378,135 @@ export class PlayCallChain {
|
|
|
1369
1378
|
const assistantItem = {
|
|
1370
1379
|
kind: "assistant",
|
|
1371
1380
|
text,
|
|
1372
|
-
...(
|
|
1381
|
+
...(durable.reasoningContent === undefined
|
|
1373
1382
|
? {}
|
|
1374
|
-
: { reasoningContent:
|
|
1375
|
-
...(
|
|
1383
|
+
: { reasoningContent: durable.reasoningContent }),
|
|
1384
|
+
...(durable.providerState === undefined
|
|
1376
1385
|
? {}
|
|
1377
|
-
: { providerState:
|
|
1386
|
+
: { providerState: durable.providerState }),
|
|
1378
1387
|
toolCalls: structuredClone(calls),
|
|
1379
1388
|
};
|
|
1380
|
-
const background =
|
|
1389
|
+
const background = narrative
|
|
1381
1390
|
? narrativeBackground(advance.narrativeOrigin, session.documents.snapshot)
|
|
1382
1391
|
: 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
|
-
}
|
|
1392
|
+
const transcriptStart = session.transcript.length;
|
|
1393
|
+
const transcriptAppend = [
|
|
1394
|
+
...(calls.length > 0 || narrative || durable.providerState !== undefined
|
|
1395
|
+
? [assistantItem]
|
|
1396
|
+
: []),
|
|
1397
|
+
...prepared.map(({ transcript }) => structuredClone(transcript)),
|
|
1398
|
+
...(calls.length > 0 && hasText
|
|
1399
|
+
? [toolStepNotice(this.#compiler.locale)]
|
|
1400
|
+
: []),
|
|
1401
|
+
];
|
|
1402
|
+
const completedTools = [...workingTools.values()].map((item) => structuredClone(item));
|
|
1403
|
+
const failedTools = trailingEvents.filter((candidate) => candidate.kind === "tool_result" && !candidate.ok);
|
|
1404
|
+
if (failedTools.length > 0 && response.diagnostics !== undefined)
|
|
1405
|
+
await this.#failureLog?.recordFailure({
|
|
1406
|
+
exchange: response.diagnostics,
|
|
1407
|
+
failures: [
|
|
1408
|
+
{
|
|
1409
|
+
kind: "tool_execution",
|
|
1410
|
+
message: "A Runtime tool call from the model was rejected.",
|
|
1411
|
+
details: {
|
|
1412
|
+
calls: failedTools.map((result) => ({
|
|
1413
|
+
id: result.callId,
|
|
1414
|
+
name: result.name,
|
|
1415
|
+
ok: result.ok,
|
|
1416
|
+
markdown: result.markdown,
|
|
1417
|
+
replayed: result.replayed,
|
|
1418
|
+
})),
|
|
1419
|
+
},
|
|
1420
|
+
},
|
|
1421
|
+
],
|
|
1422
|
+
});
|
|
1425
1423
|
let committedHead;
|
|
1426
|
-
if (
|
|
1424
|
+
if (narrative || stateChanges.length > 0) {
|
|
1427
1425
|
const outcome = await this.#worlds.commitPlayStep({
|
|
1428
|
-
|
|
1426
|
+
playContext: session.continuityContextId ?? session.chainId,
|
|
1427
|
+
...(worldClock === undefined || stateChanges.length === 0
|
|
1429
1428
|
? {}
|
|
1430
|
-
: {
|
|
1431
|
-
...(settlement.worldClock === undefined
|
|
1432
|
-
? {}
|
|
1433
|
-
: { worldClock: settlement.worldClock }),
|
|
1429
|
+
: { worldClock }),
|
|
1434
1430
|
operationId: advance.operationId,
|
|
1435
1431
|
worldId: session.worldId,
|
|
1436
1432
|
parentHead: advance.parentHead,
|
|
1437
|
-
historyAppend:
|
|
1433
|
+
historyAppend: narrative
|
|
1438
1434
|
? [
|
|
1439
1435
|
{
|
|
1440
1436
|
role: "narrator",
|
|
1441
|
-
exactText:
|
|
1442
|
-
...(
|
|
1443
|
-
? {}
|
|
1444
|
-
: { background: settlement.background }),
|
|
1437
|
+
exactText: text,
|
|
1438
|
+
...(background === undefined ? {} : { background }),
|
|
1445
1439
|
},
|
|
1446
1440
|
]
|
|
1447
1441
|
: [],
|
|
1448
1442
|
nextMaterials: structuredClone(session.nextMaterials),
|
|
1449
|
-
stateChanges: structuredClone(
|
|
1450
|
-
...(
|
|
1451
|
-
? {}
|
|
1452
|
-
: { narrativeCheckpoint: settlement.narrativeCheckpoint }),
|
|
1443
|
+
stateChanges: structuredClone(stateChanges),
|
|
1444
|
+
...(narrativeCheckpoint === undefined ? {} : { narrativeCheckpoint }),
|
|
1453
1445
|
});
|
|
1454
1446
|
committedHead = outcome.head;
|
|
1455
|
-
|
|
1456
|
-
if (
|
|
1457
|
-
|
|
1447
|
+
assistantEvent.committedHead = outcome.head;
|
|
1448
|
+
if (narrativeCheckpoint !== undefined) {
|
|
1449
|
+
assistantEvent.checkpoint = true;
|
|
1458
1450
|
session.narrativeCheckpoint = {
|
|
1459
|
-
...
|
|
1451
|
+
...narrativeCheckpoint,
|
|
1460
1452
|
head: outcome.head,
|
|
1461
1453
|
historyMessageId: `message.${outcome.head.slice("commit:".length)}.1.narrator`,
|
|
1462
1454
|
};
|
|
1463
1455
|
}
|
|
1464
1456
|
appendCommittedHistory(session, outcome.head, outcome.historyAppend);
|
|
1465
1457
|
}
|
|
1466
|
-
|
|
1458
|
+
finalizeWriteReceipts({
|
|
1459
|
+
stateChanges,
|
|
1460
|
+
completedTools,
|
|
1461
|
+
trailingEvents,
|
|
1462
|
+
transcriptAppend,
|
|
1463
|
+
});
|
|
1467
1464
|
session.events = [
|
|
1468
1465
|
...session.events.filter(({ id }) => id < advance.eventId),
|
|
1469
|
-
structuredClone(
|
|
1470
|
-
...structuredClone(
|
|
1466
|
+
structuredClone(assistantEvent),
|
|
1467
|
+
...structuredClone(trailingEvents),
|
|
1471
1468
|
];
|
|
1472
1469
|
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.");
|
|
1470
|
+
Math.max(advance.eventId, ...trailingEvents.map(({ id }) => id)) + 1;
|
|
1476
1471
|
session.transcript = [
|
|
1477
|
-
...session.transcript.slice(0,
|
|
1478
|
-
...structuredClone(
|
|
1472
|
+
...session.transcript.slice(0, transcriptStart),
|
|
1473
|
+
...structuredClone(transcriptAppend),
|
|
1479
1474
|
];
|
|
1480
|
-
session.completedTools = structuredClone(
|
|
1481
|
-
session.completedToolMap = new Map(
|
|
1482
|
-
item.key,
|
|
1483
|
-
structuredClone(item),
|
|
1484
|
-
]));
|
|
1475
|
+
session.completedTools = structuredClone(completedTools);
|
|
1476
|
+
session.completedToolMap = new Map(completedTools.map((item) => [item.key, structuredClone(item)]));
|
|
1485
1477
|
if (committedHead !== undefined)
|
|
1486
1478
|
session.parentHead = committedHead;
|
|
1487
|
-
if (
|
|
1479
|
+
if (stateChanges.length > 0) {
|
|
1488
1480
|
session.documents.acceptCommittedState();
|
|
1489
|
-
mergeChangedDocuments(session,
|
|
1490
|
-
}
|
|
1491
|
-
if (settlement.stateChanges.length > 0)
|
|
1481
|
+
mergeChangedDocuments(session, stateChanges);
|
|
1492
1482
|
await this.#bindDocumentMaintenance(session.documents, session.worldId, session.parentHead);
|
|
1483
|
+
}
|
|
1484
|
+
const hasToolCalls = calls.length > 0;
|
|
1493
1485
|
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" };
|
|
1486
|
+
// The chain has been `running` since its dispatch began, so its failure
|
|
1487
|
+
// fields are already clear; the ending below replaces them.
|
|
1488
|
+
if (assistantEvent.continuation !== "available") {
|
|
1489
|
+
await this.#advances.end(session, noContinuationEnding(), advance);
|
|
1490
|
+
return { kind: "terminal", status: "interrupted", narrative };
|
|
1510
1491
|
}
|
|
1511
|
-
|
|
1492
|
+
if (hasToolCalls) {
|
|
1512
1493
|
session.exchange += 1;
|
|
1513
|
-
const nextRequest = createRequestWithMaxOutputTokens(session, session.lastRequest?.maxOutputTokens ?? 1, session.exchange);
|
|
1494
|
+
const nextRequest = createRequestWithMaxOutputTokens(session, session.lastRequest?.maxOutputTokens ?? 1, session.exchange, this.#compiler.locale);
|
|
1514
1495
|
if (session.lastRequest?.modelConfig !== undefined)
|
|
1515
1496
|
nextRequest.modelConfig = structuredClone(session.lastRequest.modelConfig);
|
|
1516
|
-
session
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
session.lastFailureKind = "retryable";
|
|
1523
|
-
result = { kind: "continue", nextRequest };
|
|
1497
|
+
await this.#advances.end(session, {
|
|
1498
|
+
kind: "resumable",
|
|
1499
|
+
reason: "tool_step_settled",
|
|
1500
|
+
request: nextRequest,
|
|
1501
|
+
}, advance);
|
|
1502
|
+
return { kind: "continue", nextRequest };
|
|
1524
1503
|
}
|
|
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 };
|
|
1504
|
+
if (!narrative) {
|
|
1505
|
+
await this.#advances.end(session, emptyResponseEnding(), advance);
|
|
1506
|
+
return { kind: "terminal", status: "interrupted", narrative };
|
|
1539
1507
|
}
|
|
1540
|
-
await this.#
|
|
1541
|
-
|
|
1542
|
-
return result;
|
|
1508
|
+
await this.#advances.end(session, { kind: "ready" }, advance);
|
|
1509
|
+
return { kind: "terminal", status: "ready", narrative };
|
|
1543
1510
|
}
|
|
1544
1511
|
async #dispatch(session, modelHost, firstRequest, firstAttempt, signal, observer) {
|
|
1545
1512
|
let request = modelHost.freezeRequest?.(firstRequest) ?? structuredClone(firstRequest);
|
|
@@ -1549,12 +1516,7 @@ export class PlayCallChain {
|
|
|
1549
1516
|
if (signal.aborted)
|
|
1550
1517
|
return this.#cancelBeforeProviderDispatch(session, request, attempt, observer);
|
|
1551
1518
|
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;
|
|
1519
|
+
this.#advances.beginDispatch(session, request, attempt);
|
|
1558
1520
|
const responseExchange = request.exchange ?? session.exchange;
|
|
1559
1521
|
const event = {
|
|
1560
1522
|
id: session.nextEventId++,
|
|
@@ -1639,66 +1601,13 @@ export class PlayCallChain {
|
|
|
1639
1601
|
await this.#failureLog?.recordExchangeIfActive(response.diagnostics);
|
|
1640
1602
|
}
|
|
1641
1603
|
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);
|
|
1604
|
+
await this.#advances.end(session, dispatchFailureEnding(error, event.id), advance, () => this.#notifySnapshot(observer, session));
|
|
1674
1605
|
return projectView(session);
|
|
1675
1606
|
}
|
|
1676
|
-
const durableResponse = durableModelResponse(response);
|
|
1677
1607
|
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);
|
|
1608
|
+
const settled = await this.#settleResponse(session, advance, response);
|
|
1700
1609
|
if (settled.kind === "terminal") {
|
|
1701
|
-
if (!
|
|
1610
|
+
if (!settled.narrative) {
|
|
1702
1611
|
const message = "The model returned neither text nor a complete tool call.";
|
|
1703
1612
|
if (response.diagnostics !== undefined)
|
|
1704
1613
|
await this.#failureLog?.recordFailure({
|
|
@@ -1738,18 +1647,7 @@ export class PlayCallChain {
|
|
|
1738
1647
|
}
|
|
1739
1648
|
await this.#runFollowups(session, modelHost, signal, observer);
|
|
1740
1649
|
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);
|
|
1650
|
+
await this.#advances.end(session, { kind: "ready", followupsCancelled: signal.aborted }, null, () => this.#notifySnapshot(observer, session));
|
|
1753
1651
|
return projectView(session);
|
|
1754
1652
|
}
|
|
1755
1653
|
this.#notifySnapshot(observer, session);
|
|
@@ -1757,58 +1655,29 @@ export class PlayCallChain {
|
|
|
1757
1655
|
attempt = 1;
|
|
1758
1656
|
}
|
|
1759
1657
|
catch (error) {
|
|
1760
|
-
const
|
|
1761
|
-
? error.message
|
|
1762
|
-
: "Call-chain processing failed.";
|
|
1658
|
+
const ending = settlementFailureEnding(error, "Call-chain processing failed.");
|
|
1763
1659
|
if (response.diagnostics !== undefined)
|
|
1764
1660
|
await this.#failureLog?.recordFailure({
|
|
1765
1661
|
exchange: response.diagnostics,
|
|
1766
1662
|
failures: [
|
|
1767
1663
|
{
|
|
1768
1664
|
kind: "runtime_post_processing",
|
|
1769
|
-
message,
|
|
1665
|
+
message: ending.message,
|
|
1770
1666
|
},
|
|
1771
1667
|
],
|
|
1772
1668
|
});
|
|
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);
|
|
1669
|
+
await this.#advances.end(session, ending, advance, () => this.#notifySnapshot(observer, session));
|
|
1786
1670
|
return projectView(session);
|
|
1787
1671
|
}
|
|
1788
1672
|
}
|
|
1789
1673
|
}
|
|
1790
1674
|
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);
|
|
1675
|
+
await this.#advances.end(session, {
|
|
1676
|
+
kind: "cancelled_before_dispatch",
|
|
1677
|
+
request,
|
|
1678
|
+
attempt,
|
|
1679
|
+
...(advance === undefined ? {} : { streamingEventId: advance.eventId }),
|
|
1680
|
+
}, advance ?? null, () => this.#notifySnapshot(observer, session));
|
|
1812
1681
|
return projectView(session);
|
|
1813
1682
|
}
|
|
1814
1683
|
/**
|
|
@@ -1982,42 +1851,10 @@ export class PlayCallChain {
|
|
|
1982
1851
|
: loaded;
|
|
1983
1852
|
if (advance === null && session.status !== "running")
|
|
1984
1853
|
return;
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
await this.#worlds.playAdvances.markSettled(advance);
|
|
1988
|
-
}
|
|
1989
|
-
async #interruptAbandonedDispatch(session, advance) {
|
|
1854
|
+
// Authority already moved past the persisted parent head: the world cannot
|
|
1855
|
+
// be replayed onto that head, so the recovery requires a fresh context.
|
|
1990
1856
|
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);
|
|
1857
|
+
await this.#advances.recoverAbandoned(session, advance, headMoved);
|
|
2021
1858
|
}
|
|
2022
1859
|
async #assertCurrentHead(session) {
|
|
2023
1860
|
if ((await this.#worlds.currentHead(session.worldId)) !== session.parentHead)
|
|
@@ -2121,14 +1958,16 @@ export class PlayCallChain {
|
|
|
2121
1958
|
function followupOperationId(session) {
|
|
2122
1959
|
return `${session.chainId}:followup:${session.parentHead}`;
|
|
2123
1960
|
}
|
|
2124
|
-
function createRequest(session, modelHost, exchange, appended = session.transcript) {
|
|
2125
|
-
const request = createRequestWithMaxOutputTokens(session, modelHost.binding().maxOutputTokens, exchange, appended);
|
|
1961
|
+
function createRequest(session, modelHost, exchange, locale, appended = session.transcript) {
|
|
1962
|
+
const request = createRequestWithMaxOutputTokens(session, modelHost.binding().maxOutputTokens, exchange, locale, appended);
|
|
2126
1963
|
return modelHost.freezeRequest?.(request) ?? request;
|
|
2127
1964
|
}
|
|
2128
|
-
function createRequestWithMaxOutputTokens(session, maxOutputTokens, exchange, appended = session.transcript) {
|
|
1965
|
+
function createRequestWithMaxOutputTokens(session, maxOutputTokens, exchange, locale, appended = session.transcript) {
|
|
2129
1966
|
// The turn guide is a request-level suffix: it follows whatever this
|
|
2130
|
-
// dispatch appends and is never part of the transcript it is built on.
|
|
2131
|
-
|
|
1967
|
+
// dispatch appends and is never part of the transcript it is built on. The
|
|
1968
|
+
// player's one-turn request joins the suffix here, not in the bootstrap, so
|
|
1969
|
+
// the bootstrap snapshot stays shared by every dispatch of the turn.
|
|
1970
|
+
const turnGuide = turnGuideWithTurnNote(locale, currentPlayPrompt(session).bootstrap.turnGuide, session.turnNote);
|
|
2132
1971
|
return {
|
|
2133
1972
|
bootstrap: structuredClone(currentPlayPrompt(session).bootstrap),
|
|
2134
1973
|
tools: structuredClone(currentPlayPrompt(session).tools),
|
|
@@ -2162,7 +2001,11 @@ function durableModelResponse(response) {
|
|
|
2162
2001
|
void _diagnostics;
|
|
2163
2002
|
return structuredClone(durable);
|
|
2164
2003
|
}
|
|
2165
|
-
|
|
2004
|
+
/**
|
|
2005
|
+
* Copies each committed write's receipt into the tool results that report it,
|
|
2006
|
+
* then drops the candidate write. Receipts state what the commit made true.
|
|
2007
|
+
*/
|
|
2008
|
+
function finalizeWriteReceipts(settlement) {
|
|
2166
2009
|
const committedWriteRefs = new Set(settlement.stateChanges.map(({ stableShortRef }) => stableShortRef));
|
|
2167
2010
|
const markdownByCall = new Map();
|
|
2168
2011
|
for (const completed of settlement.completedTools) {
|
|
@@ -2218,13 +2061,17 @@ function appendCommittedHistory(session, head, messages) {
|
|
|
2218
2061
|
function prepareTool(session, working, exchange, call, locale, resolutionSeed) {
|
|
2219
2062
|
const key = `${exchange}:${call.id}`;
|
|
2220
2063
|
const signature = JSON.stringify(call.arguments);
|
|
2064
|
+
const provided = currentPlayPrompt(session).tools.map(({ name }) => name);
|
|
2065
|
+
const resolved = resolveToolName(call.name, provided);
|
|
2066
|
+
// The executed name, not the model's spelling, decides replay identity.
|
|
2067
|
+
const executedName = resolved.kind === "rejected" ? call.name : resolved.name;
|
|
2221
2068
|
const completed = working.get(key);
|
|
2222
2069
|
let result;
|
|
2223
2070
|
let replayed = false;
|
|
2224
2071
|
if (completed !== undefined) {
|
|
2225
2072
|
replayed = true;
|
|
2226
2073
|
result =
|
|
2227
|
-
completed.name ===
|
|
2074
|
+
completed.name === executedName && completed.signature === signature
|
|
2228
2075
|
? structuredClone(completed.result)
|
|
2229
2076
|
: {
|
|
2230
2077
|
ok: false,
|
|
@@ -2232,19 +2079,19 @@ function prepareTool(session, working, exchange, call, locale, resolutionSeed) {
|
|
|
2232
2079
|
markdown: "# Runtime tool rejected\n\nThe same tool-call ID is already bound to another tool name or argument set.",
|
|
2233
2080
|
};
|
|
2234
2081
|
}
|
|
2235
|
-
else if (
|
|
2236
|
-
!
|
|
2082
|
+
else if (resolved.kind === "rejected" ||
|
|
2083
|
+
!callChainToolNames.has(executedName)) {
|
|
2237
2084
|
result = {
|
|
2238
2085
|
ok: false,
|
|
2239
2086
|
failureKind: "protocol",
|
|
2240
|
-
markdown: `# Runtime tool rejected\n\nThe current call chain does not provide ${call.name}.`,
|
|
2087
|
+
markdown: `# Runtime tool rejected\n\nThe current call chain does not provide ${call.name}. Available tools: ${provided.join(", ")}.`,
|
|
2241
2088
|
};
|
|
2242
2089
|
}
|
|
2243
2090
|
else {
|
|
2244
2091
|
try {
|
|
2245
|
-
result = isResolutionTool(
|
|
2246
|
-
? resolutionToolResult(
|
|
2247
|
-
:
|
|
2092
|
+
result = isResolutionTool(executedName)
|
|
2093
|
+
? resolutionToolResult(executedName, call.arguments, resolutionSeed, key, locale)
|
|
2094
|
+
: executedName === "world_checkpoint"
|
|
2248
2095
|
? typeof call.arguments === "object" &&
|
|
2249
2096
|
call.arguments !== null &&
|
|
2250
2097
|
!Array.isArray(call.arguments) &&
|
|
@@ -2260,7 +2107,7 @@ function prepareTool(session, working, exchange, call, locale, resolutionSeed) {
|
|
|
2260
2107
|
failureKind: "protocol",
|
|
2261
2108
|
markdown: "world_checkpoint requires an empty argument object.",
|
|
2262
2109
|
}
|
|
2263
|
-
: session.documents.execute(call, session.history, {
|
|
2110
|
+
: session.documents.execute({ ...call, name: executedName }, session.history, {
|
|
2264
2111
|
historyScope: session.worldId,
|
|
2265
2112
|
locale,
|
|
2266
2113
|
legacyHistoryTools: hasLegacyHistoryTools(currentPlayPrompt(session).tools),
|
|
@@ -2273,9 +2120,14 @@ function prepareTool(session, working, exchange, call, locale, resolutionSeed) {
|
|
|
2273
2120
|
markdown: `# Runtime tool failed\n\n${error instanceof Error ? error.message : "Tool execution failed."}`,
|
|
2274
2121
|
};
|
|
2275
2122
|
}
|
|
2123
|
+
if (resolved.kind === "alias")
|
|
2124
|
+
result = {
|
|
2125
|
+
...result,
|
|
2126
|
+
markdown: `${result.markdown}\n\n${renderToolAliasNote(call.name, executedName, locale)}`,
|
|
2127
|
+
};
|
|
2276
2128
|
working.set(key, {
|
|
2277
2129
|
key,
|
|
2278
|
-
name:
|
|
2130
|
+
name: executedName,
|
|
2279
2131
|
signature,
|
|
2280
2132
|
result: structuredClone(result),
|
|
2281
2133
|
});
|
|
@@ -2285,6 +2137,7 @@ function prepareTool(session, working, exchange, call, locale, resolutionSeed) {
|
|
|
2285
2137
|
kind: "tool_result",
|
|
2286
2138
|
callId: call.id,
|
|
2287
2139
|
name: call.name,
|
|
2140
|
+
...(executedName === call.name ? {} : { executedName }),
|
|
2288
2141
|
ok: result.ok,
|
|
2289
2142
|
markdown: result.markdown,
|
|
2290
2143
|
...(result.resolution === undefined
|
|
@@ -2402,6 +2255,7 @@ function projectView(session) {
|
|
|
2402
2255
|
...projectContext(session),
|
|
2403
2256
|
worldId: session.worldId,
|
|
2404
2257
|
previousContexts: (session.previousContexts ?? []).map((context) => projectContext(context)),
|
|
2258
|
+
...(lastEndingCancelled(session) ? { lastCancelled: true } : {}),
|
|
2405
2259
|
};
|
|
2406
2260
|
}
|
|
2407
2261
|
function projectContextReads(session) {
|
|
@@ -2414,7 +2268,8 @@ function projectContextReads(session) {
|
|
|
2414
2268
|
exchange = event.exchange;
|
|
2415
2269
|
continue;
|
|
2416
2270
|
}
|
|
2417
|
-
if (event.kind === "tool_call" &&
|
|
2271
|
+
if (event.kind === "tool_call" &&
|
|
2272
|
+
(event.executedName ?? event.name) === "context_read") {
|
|
2418
2273
|
const ref = isRecord(event.arguments) && typeof event.arguments.ref === "string"
|
|
2419
2274
|
? event.arguments.ref
|
|
2420
2275
|
: "(invalid ref)";
|
|
@@ -2424,7 +2279,8 @@ function projectContextReads(session) {
|
|
|
2424
2279
|
});
|
|
2425
2280
|
continue;
|
|
2426
2281
|
}
|
|
2427
|
-
if (event.kind !== "tool_result" ||
|
|
2282
|
+
if (event.kind !== "tool_result" ||
|
|
2283
|
+
(event.executedName ?? event.name) !== "context_read")
|
|
2428
2284
|
continue;
|
|
2429
2285
|
const call = calls.get(event.callId);
|
|
2430
2286
|
if (call === undefined)
|
|
@@ -2528,6 +2384,28 @@ function validatePlayerText(value) {
|
|
|
2528
2384
|
if (value.trim().length === 0)
|
|
2529
2385
|
throw new PlayCallChainError("Player text must not be empty.");
|
|
2530
2386
|
}
|
|
2387
|
+
/**
|
|
2388
|
+
* The player's one-turn request: one line, bounded like a turn-guide line.
|
|
2389
|
+
* Blank input means no request. Length beyond the soft limit is only warned
|
|
2390
|
+
* about by the client and Prompt Preview, never rejected here.
|
|
2391
|
+
*/
|
|
2392
|
+
function validTurnNote(value) {
|
|
2393
|
+
if (value === undefined)
|
|
2394
|
+
return undefined;
|
|
2395
|
+
if (!isTurnGuideLine(value))
|
|
2396
|
+
throw new PlayCallChainError("The turn request must be a single line of at most 1000 characters.");
|
|
2397
|
+
const trimmed = value.trim();
|
|
2398
|
+
return trimmed.length === 0 ? undefined : trimmed;
|
|
2399
|
+
}
|
|
2400
|
+
function setTurnNote(session, turnNote) {
|
|
2401
|
+
if (turnNote === undefined)
|
|
2402
|
+
delete session.turnNote;
|
|
2403
|
+
else
|
|
2404
|
+
session.turnNote = turnNote;
|
|
2405
|
+
}
|
|
2406
|
+
function turnNoteField(turnNote) {
|
|
2407
|
+
return turnNote === undefined ? {} : { turnNote };
|
|
2408
|
+
}
|
|
2531
2409
|
function touch(session) {
|
|
2532
2410
|
session.updatedAt = Date.now();
|
|
2533
2411
|
}
|