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
|
@@ -948,7 +948,8 @@ function assertPlayEvent(value) {
|
|
|
948
948
|
if (value.kind === "tool_call") {
|
|
949
949
|
if (typeof value.callId !== "string" ||
|
|
950
950
|
typeof value.name !== "string" ||
|
|
951
|
-
typeof value.replayed !== "boolean"
|
|
951
|
+
typeof value.replayed !== "boolean" ||
|
|
952
|
+
!validOptionalString(value.executedName))
|
|
952
953
|
throw new Error("Tool-call timeline event has an invalid shape");
|
|
953
954
|
return;
|
|
954
955
|
}
|
|
@@ -960,7 +961,8 @@ function assertPlayEvent(value) {
|
|
|
960
961
|
typeof value.name !== "string" ||
|
|
961
962
|
typeof value.ok !== "boolean" ||
|
|
962
963
|
typeof value.markdown !== "string" ||
|
|
963
|
-
typeof value.replayed !== "boolean"
|
|
964
|
+
typeof value.replayed !== "boolean" ||
|
|
965
|
+
!validOptionalString(value.executedName))
|
|
964
966
|
throw new Error("Tool-result timeline event has an invalid shape");
|
|
965
967
|
return;
|
|
966
968
|
}
|
|
@@ -1014,6 +1016,7 @@ function assertTimelineEventSummary(value) {
|
|
|
1014
1016
|
if (typeof value.callId !== "string" ||
|
|
1015
1017
|
typeof value.name !== "string" ||
|
|
1016
1018
|
typeof value.replayed !== "boolean" ||
|
|
1019
|
+
!validOptionalString(value.executedName) ||
|
|
1017
1020
|
value.detailsAvailable !== true)
|
|
1018
1021
|
throw new Error("Tool-call timeline summary has an invalid shape");
|
|
1019
1022
|
return;
|
|
@@ -1026,6 +1029,7 @@ function assertTimelineEventSummary(value) {
|
|
|
1026
1029
|
typeof value.name !== "string" ||
|
|
1027
1030
|
typeof value.ok !== "boolean" ||
|
|
1028
1031
|
typeof value.replayed !== "boolean" ||
|
|
1032
|
+
!validOptionalString(value.executedName) ||
|
|
1029
1033
|
value.detailsAvailable !== true)
|
|
1030
1034
|
throw new Error("Tool-result timeline summary has an invalid shape");
|
|
1031
1035
|
return;
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { isMap, isSeq, parseDocument } from "yaml";
|
|
3
|
-
import { builtinPlayPrompts, isPromptMessageRole, promptGroups, } from "../../shared/ordered-play-prompts.js";
|
|
4
|
-
/** One turn-guide line: no line breaks, bounded length; empty clears a builtin default. */
|
|
5
|
-
const turnGuideLine = /^[^\r\n\0]{0,1000}$/u;
|
|
3
|
+
import { builtinPlayPrompts, isPromptMessageRole, isTurnGuideLine, promptGroups, } from "../../shared/ordered-play-prompts.js";
|
|
6
4
|
const promptName = /^[^\r\n\0]{1,160}$/u;
|
|
7
5
|
/** The same strict permission gate is used by portable imports and structured saves. */
|
|
8
6
|
export function parseOrderedPlayPrompts(value, catalog = builtinPlayPrompts("en"), worldRequired = true) {
|
|
@@ -33,8 +31,7 @@ export function parseOrderedPlayPrompts(value, catalog = builtinPlayPrompts("en"
|
|
|
33
31
|
key === "id" ||
|
|
34
32
|
key === "kind") && keys.every((key) => Object.hasOwn(item, key));
|
|
35
33
|
const validTurnGuide = item.turnGuide === undefined ||
|
|
36
|
-
(typeof item.turnGuide === "string" &&
|
|
37
|
-
turnGuideLine.test(item.turnGuide));
|
|
34
|
+
(typeof item.turnGuide === "string" && isTurnGuideLine(item.turnGuide));
|
|
38
35
|
const turnGuide = typeof item.turnGuide === "string" ? { turnGuide: item.turnGuide } : {};
|
|
39
36
|
if (item.kind === "world" && exact(["id", "kind"])) {
|
|
40
37
|
worlds++;
|
|
@@ -47,7 +44,7 @@ export function parseOrderedPlayPrompts(value, catalog = builtinPlayPrompts("en"
|
|
|
47
44
|
item.name.trim().length > 0 &&
|
|
48
45
|
promptName.test(item.name) &&
|
|
49
46
|
typeof item.body === "string" &&
|
|
50
|
-
|
|
47
|
+
isTurnGuideLine(item.body) &&
|
|
51
48
|
typeof item.enabled === "boolean")
|
|
52
49
|
return [
|
|
53
50
|
{
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { ModelHostBindingMismatchError, ModelHostCancelledError, ModelHostContinuationError, ModelHostFailureError, ModelHostOutcomeUnknownError, modelHostFailureRequiresFreshContext, } from "../model/ModelHost.js";
|
|
2
|
+
import { classifyPlayFailure } from "./PlayFailure.js";
|
|
3
|
+
const messages = {
|
|
4
|
+
playerCommitted: "The player message is committed; the frozen model request is ready to continue.",
|
|
5
|
+
toolStepSettled: "The completed tool response is settled; its frozen continuation request is ready.",
|
|
6
|
+
cancelledBeforeDispatch: "The player cancelled before the next Provider dispatch; the frozen request can be resumed without replaying a completed model result.",
|
|
7
|
+
followupsCancelled: "The player cancelled the remaining follow-up generation after the narrative was committed.",
|
|
8
|
+
noContinuation: "The complete model response has no provider-native continuation payload; use a fresh context.",
|
|
9
|
+
noOutput: "The model returned neither text nor a complete tool call.",
|
|
10
|
+
serviceStoppedAfterHead: "The service stopped after the world was updated; use a fresh context.",
|
|
11
|
+
serviceStoppedSavingPlayer: "The service stopped while saving the player message; use a fresh context.",
|
|
12
|
+
serviceStoppedRetryable: "The service stopped while the model request was in progress; retry to call the model again.",
|
|
13
|
+
serviceStoppedNoRequest: "The service stopped before preparing a model request; use a fresh context.",
|
|
14
|
+
dispatchInterrupted: "The model request was interrupted.",
|
|
15
|
+
};
|
|
16
|
+
export class PlayAdvanceLifecycle {
|
|
17
|
+
#ports;
|
|
18
|
+
constructor(ports) {
|
|
19
|
+
this.#ports = ports;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Marks a Provider dispatch as in progress. `running` is not an ending: it is
|
|
23
|
+
* the state that every dispatch replaces with an ending before it returns.
|
|
24
|
+
*/
|
|
25
|
+
beginDispatch(session, request, attempt) {
|
|
26
|
+
session.status = "running";
|
|
27
|
+
session.canRetry = false;
|
|
28
|
+
session.lastFailure = null;
|
|
29
|
+
delete session.lastFailureKind;
|
|
30
|
+
session.lastRequest = structuredClone(request);
|
|
31
|
+
session.lastRequestAttempt = attempt;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Applies an ending, persists the session, then clears the advance marker
|
|
35
|
+
* and notifies. Every ending of an advance goes through here, so no path
|
|
36
|
+
* can persist without clearing the marker it settles.
|
|
37
|
+
*/
|
|
38
|
+
async end(session, ending, advance, notify) {
|
|
39
|
+
applyEnding(session, ending);
|
|
40
|
+
await this.#ports.persist(session);
|
|
41
|
+
if (advance !== null)
|
|
42
|
+
await this.#ports.clearMarker(advance);
|
|
43
|
+
notify?.();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Ends an advance that a process exit cut off (ADR-0044). Nothing is
|
|
47
|
+
* replayed: the exchange is interrupted, and only the chain's own frozen
|
|
48
|
+
* request can be retried, and only while the world has not moved on.
|
|
49
|
+
*/
|
|
50
|
+
async recoverAbandoned(session, advance, headMoved) {
|
|
51
|
+
const playerPhase = advance?.advanceKind === "player";
|
|
52
|
+
const streaming = playerPhase
|
|
53
|
+
? undefined
|
|
54
|
+
: session.events.findLast((event) => event.kind === "assistant" &&
|
|
55
|
+
event.status === "streaming" &&
|
|
56
|
+
(advance === null || event.id === advance.eventId));
|
|
57
|
+
const canRetry = !headMoved && !playerPhase && session.lastRequest !== null;
|
|
58
|
+
const message = headMoved
|
|
59
|
+
? messages.serviceStoppedAfterHead
|
|
60
|
+
: playerPhase
|
|
61
|
+
? messages.serviceStoppedSavingPlayer
|
|
62
|
+
: canRetry
|
|
63
|
+
? messages.serviceStoppedRetryable
|
|
64
|
+
: messages.serviceStoppedNoRequest;
|
|
65
|
+
await this.end(session, {
|
|
66
|
+
kind: "failed",
|
|
67
|
+
message,
|
|
68
|
+
failureKind: canRetry ? "retryable" : "other",
|
|
69
|
+
canRetry,
|
|
70
|
+
streamingEventId: streaming?.id,
|
|
71
|
+
}, advance);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The ending of an exchange that raised an error before its response was
|
|
76
|
+
* settled. Cancellation after dispatch is named explicitly here and is never
|
|
77
|
+
* a retry, even though `ModelHostCancelledError` is also an outcome-unknown
|
|
78
|
+
* error (ADR-0030).
|
|
79
|
+
*/
|
|
80
|
+
export function dispatchFailureEnding(error, streamingEventId) {
|
|
81
|
+
const message = errorMessage(error, messages.dispatchInterrupted);
|
|
82
|
+
if (error instanceof ModelHostCancelledError)
|
|
83
|
+
return { kind: "cancelled_after_dispatch", message, streamingEventId };
|
|
84
|
+
const requiresFreshContext = error instanceof ModelHostBindingMismatchError ||
|
|
85
|
+
error instanceof ModelHostContinuationError ||
|
|
86
|
+
error instanceof ModelHostOutcomeUnknownError ||
|
|
87
|
+
modelHostFailureRequiresFreshContext(error);
|
|
88
|
+
const canRetry = !requiresFreshContext && error instanceof ModelHostFailureError;
|
|
89
|
+
return {
|
|
90
|
+
kind: "failed",
|
|
91
|
+
message,
|
|
92
|
+
failureKind: classifyPlayFailure(error, canRetry),
|
|
93
|
+
canRetry,
|
|
94
|
+
streamingEventId,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The ending of an error raised after a response was read or a player message
|
|
99
|
+
* was written. These failures never grant a retry: Authority may already have
|
|
100
|
+
* moved past the frozen request.
|
|
101
|
+
*/
|
|
102
|
+
export function settlementFailureEnding(error, fallbackMessage) {
|
|
103
|
+
return {
|
|
104
|
+
kind: "failed",
|
|
105
|
+
message: errorMessage(error, fallbackMessage),
|
|
106
|
+
failureKind: classifyPlayFailure(error, false),
|
|
107
|
+
canRetry: false,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/** A settled response with no provider-native continuation payload (ADR-0035). */
|
|
111
|
+
export function noContinuationEnding() {
|
|
112
|
+
return {
|
|
113
|
+
kind: "failed",
|
|
114
|
+
message: messages.noContinuation,
|
|
115
|
+
failureKind: "context_unavailable",
|
|
116
|
+
canRetry: false,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/** A settled response with neither text nor a complete tool call. */
|
|
120
|
+
export function emptyResponseEnding() {
|
|
121
|
+
return {
|
|
122
|
+
kind: "failed",
|
|
123
|
+
message: messages.noOutput,
|
|
124
|
+
failureKind: "other",
|
|
125
|
+
canRetry: false,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Whether a chain's last ending was the player's cancellation. Every
|
|
130
|
+
* cancellation ending writes its event last, so the tail identifies it; callers
|
|
131
|
+
* read the projected flag instead of the event list.
|
|
132
|
+
*/
|
|
133
|
+
export function lastEndingCancelled(session) {
|
|
134
|
+
return session.events.at(-1)?.kind === "cancellation";
|
|
135
|
+
}
|
|
136
|
+
function applyEnding(session, ending) {
|
|
137
|
+
switch (ending.kind) {
|
|
138
|
+
case "ready":
|
|
139
|
+
if (ending.followupsCancelled === true)
|
|
140
|
+
session.events.push({
|
|
141
|
+
id: session.nextEventId++,
|
|
142
|
+
kind: "cancellation",
|
|
143
|
+
message: messages.followupsCancelled,
|
|
144
|
+
});
|
|
145
|
+
session.status = "ready";
|
|
146
|
+
session.canRetry = false;
|
|
147
|
+
session.lastFailure = null;
|
|
148
|
+
delete session.lastFailureKind;
|
|
149
|
+
return;
|
|
150
|
+
case "resumable":
|
|
151
|
+
session.lastRequest = structuredClone(ending.request);
|
|
152
|
+
session.lastRequestAttempt = 0;
|
|
153
|
+
session.status = "interrupted";
|
|
154
|
+
session.canRetry = true;
|
|
155
|
+
session.lastFailure =
|
|
156
|
+
ending.reason === "player_committed"
|
|
157
|
+
? messages.playerCommitted
|
|
158
|
+
: messages.toolStepSettled;
|
|
159
|
+
session.lastFailureKind = "retryable";
|
|
160
|
+
return;
|
|
161
|
+
case "cancelled_before_dispatch":
|
|
162
|
+
interruptStreamingEvent(session, ending.streamingEventId);
|
|
163
|
+
session.events.push({
|
|
164
|
+
id: session.nextEventId++,
|
|
165
|
+
kind: "cancellation",
|
|
166
|
+
message: messages.cancelledBeforeDispatch,
|
|
167
|
+
});
|
|
168
|
+
session.status = "interrupted";
|
|
169
|
+
session.canRetry = true;
|
|
170
|
+
session.lastRequest = structuredClone(ending.request);
|
|
171
|
+
session.lastRequestAttempt = Math.max(0, ending.attempt - 1);
|
|
172
|
+
session.lastFailure = messages.cancelledBeforeDispatch;
|
|
173
|
+
delete session.lastFailureKind;
|
|
174
|
+
return;
|
|
175
|
+
case "cancelled_after_dispatch":
|
|
176
|
+
interruptStreamingEvent(session, ending.streamingEventId);
|
|
177
|
+
session.status = "interrupted";
|
|
178
|
+
session.canRetry = false;
|
|
179
|
+
session.lastFailure = ending.message;
|
|
180
|
+
delete session.lastFailureKind;
|
|
181
|
+
session.events.push({
|
|
182
|
+
id: session.nextEventId++,
|
|
183
|
+
kind: "cancellation",
|
|
184
|
+
message: ending.message,
|
|
185
|
+
});
|
|
186
|
+
return;
|
|
187
|
+
case "failed":
|
|
188
|
+
interruptStreamingEvent(session, ending.streamingEventId);
|
|
189
|
+
session.status = "interrupted";
|
|
190
|
+
session.canRetry = ending.canRetry;
|
|
191
|
+
session.lastFailure = ending.message;
|
|
192
|
+
session.lastFailureKind = ending.failureKind;
|
|
193
|
+
session.events.push({
|
|
194
|
+
id: session.nextEventId++,
|
|
195
|
+
kind: "failure",
|
|
196
|
+
message: ending.message,
|
|
197
|
+
failureKind: ending.failureKind,
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function interruptStreamingEvent(session, eventId) {
|
|
203
|
+
if (eventId === undefined)
|
|
204
|
+
return;
|
|
205
|
+
const event = session.events.find(({ id }) => id === eventId);
|
|
206
|
+
if (event?.kind === "assistant")
|
|
207
|
+
event.status = "interrupted";
|
|
208
|
+
}
|
|
209
|
+
function errorMessage(error, fallback) {
|
|
210
|
+
return error instanceof Error ? error.message : fallback;
|
|
211
|
+
}
|