narraeon 0.5.2 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/node/runtime/history/HistoryQuery.js +550 -0
- package/dist/node/runtime/history/HistoryRendering.js +86 -0
- package/dist/node/runtime/play/PlayCallChain.js +33 -292
- package/dist/node/runtime/play/PlayCallChainError.js +6 -0
- package/dist/node/runtime/play/PlayContinuity.js +13 -8
- package/dist/node/runtime/play/PlayDocumentTools.js +58 -125
- package/dist/node/runtime/play/PlayTraceRetention.js +264 -0
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +91 -62
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +56 -12
- package/dist/node/shared/history-use-guidance.js +6 -0
- package/dist/node/shared/ordered-play-prompts.js +7 -2
- package/dist/web/assets/index-EIOVZb8X.js +1211 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/index-CVllMgyP.js +0 -1207
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { HistoryQuery, historyInputs } from "../history/HistoryQuery.js";
|
|
2
|
+
import { renderHistoryResult } from "../history/HistoryRendering.js";
|
|
1
3
|
import { stateDirectoryHandle, parseStateDirectoryHandle, } from "../world/StateDirectoryHandles.js";
|
|
2
4
|
import { renderDocumentWritePosition } from "../prompt/WorldMaintenanceReport.js";
|
|
3
5
|
import { createHash } from "node:crypto";
|
|
@@ -114,19 +116,61 @@ export class FileNativePlayDocuments {
|
|
|
114
116
|
documents,
|
|
115
117
|
};
|
|
116
118
|
}
|
|
117
|
-
execute(call, history) {
|
|
119
|
+
execute(call, history, options = {}) {
|
|
120
|
+
let historyQuery;
|
|
121
|
+
const query = () => (historyQuery ??= new HistoryQuery(options.historyScope ?? "local-document-context", historyInputs(history)));
|
|
122
|
+
const render = (result) => renderHistoryResult(result, options.locale ?? "en");
|
|
123
|
+
const legacy = options.legacyHistoryTools !== false;
|
|
124
|
+
const args = call.arguments;
|
|
125
|
+
if (call.name === "history_search")
|
|
126
|
+
return render(query().search(args));
|
|
127
|
+
if (call.name === "history_read")
|
|
128
|
+
return render(query().read(args));
|
|
129
|
+
if (call.name === "history_list") {
|
|
130
|
+
return render(legacy && record(args) && args.order !== undefined
|
|
131
|
+
? query().legacyList(args, "history_list")
|
|
132
|
+
: query().list(args));
|
|
133
|
+
}
|
|
134
|
+
if (call.name === "context_list" &&
|
|
135
|
+
record(args) &&
|
|
136
|
+
args.source === "history") {
|
|
137
|
+
const { source: _source, ...parameters } = args;
|
|
138
|
+
void _source;
|
|
139
|
+
return legacy
|
|
140
|
+
? render(query().legacyList(parameters, "context_list"))
|
|
141
|
+
: toolFailure("Use history_list for committed history.");
|
|
142
|
+
}
|
|
143
|
+
if (call.name === "context_search" &&
|
|
144
|
+
record(args) &&
|
|
145
|
+
args.source === "history") {
|
|
146
|
+
const { source: _source, ...parameters } = args;
|
|
147
|
+
void _source;
|
|
148
|
+
return legacy
|
|
149
|
+
? render(query().legacySearch(parameters))
|
|
150
|
+
: toolFailure("Use history_search for committed history.");
|
|
151
|
+
}
|
|
152
|
+
if (call.name === "context_read" &&
|
|
153
|
+
record(args) &&
|
|
154
|
+
typeof args.ref === "string" &&
|
|
155
|
+
/^@?history-message-/u.test(args.ref)) {
|
|
156
|
+
if (!legacy)
|
|
157
|
+
return toolFailure("Use history_read with refs supplied in injections or tool results.");
|
|
158
|
+
if (!hasOnlyToolKeys(args, ["ref", "cursor", "maxBytes"]))
|
|
159
|
+
return toolFailure("Legacy context_read requires ref and accepts only obsolete cursor/maxBytes in addition.");
|
|
160
|
+
return renderHistoryResult(query().read({
|
|
161
|
+
ref: args.ref.startsWith("@") ? args.ref : `@${args.ref}`,
|
|
162
|
+
}), options.locale ?? "en", true);
|
|
163
|
+
}
|
|
118
164
|
if (call.name === "context_search")
|
|
119
|
-
return executeContextSearch(this.#candidate.snapshot,
|
|
165
|
+
return executeContextSearch(this.#candidate.snapshot, call.arguments);
|
|
120
166
|
if (call.name === "state_list")
|
|
121
|
-
return executeStateList(this.#candidate.snapshot,
|
|
122
|
-
if (call.name === "history_list")
|
|
123
|
-
return executeHistoryList(this.#candidate.snapshot, history, call.arguments);
|
|
167
|
+
return executeStateList(this.#candidate.snapshot, call.arguments, this.#declaredDirectories);
|
|
124
168
|
// Frozen contexts created before runtime-tools-v5 retain this exact name
|
|
125
169
|
// and argument shape. New contexts never advertise it.
|
|
126
170
|
if (call.name === "context_list")
|
|
127
|
-
return executeContextList(this.#candidate.snapshot,
|
|
171
|
+
return executeContextList(this.#candidate.snapshot, call.arguments, this.#declaredDirectories);
|
|
128
172
|
if (call.name === "context_read") {
|
|
129
|
-
const result = executeContextRead(this.#candidate.snapshot,
|
|
173
|
+
const result = executeContextRead(this.#candidate.snapshot, call.arguments);
|
|
130
174
|
authorizeToolRead(this.#reads, this.#candidate.snapshot, result);
|
|
131
175
|
const ref = result.readAuthorization?.shortRef;
|
|
132
176
|
if (ref !== undefined && result.markdown !== undefined)
|
|
@@ -519,7 +563,7 @@ function toolFailure(message, failureKind = "protocol") {
|
|
|
519
563
|
failureKind,
|
|
520
564
|
};
|
|
521
565
|
}
|
|
522
|
-
function executeContextSearch(snapshot,
|
|
566
|
+
function executeContextSearch(snapshot, args) {
|
|
523
567
|
if (!record(args) ||
|
|
524
568
|
!hasOnlyToolKeys(args, [
|
|
525
569
|
"source",
|
|
@@ -560,38 +604,7 @@ function executeContextSearch(snapshot, history, args) {
|
|
|
560
604
|
? { cursor: args.cursor }
|
|
561
605
|
: {}),
|
|
562
606
|
});
|
|
563
|
-
|
|
564
|
-
historyRef(path),
|
|
565
|
-
contents,
|
|
566
|
-
[`history-commit-${path.split("-")[0] ?? ""}`],
|
|
567
|
-
]);
|
|
568
|
-
const normalizedQuery = normalizeSearch(query, caseSensitive);
|
|
569
|
-
const within = typeof args.within === "string" ? args.within.replace(/^@/u, "") : null;
|
|
570
|
-
const allHits = source.filter(([ref, text, parentScopes]) => (within === null || ref === within || parentScopes.includes(within)) &&
|
|
571
|
-
normalizeSearch(text, caseSensitive).includes(normalizedQuery));
|
|
572
|
-
const scope = JSON.stringify({
|
|
573
|
-
kind: "search",
|
|
574
|
-
source: "history",
|
|
575
|
-
query,
|
|
576
|
-
caseSensitive,
|
|
577
|
-
within: args.within ?? null,
|
|
578
|
-
limit,
|
|
579
|
-
});
|
|
580
|
-
const offset = parseCursor(args.cursor, scope);
|
|
581
|
-
if (offset === null)
|
|
582
|
-
return {
|
|
583
|
-
ok: false,
|
|
584
|
-
markdown: "# Runtime argument error\n\nThe cursor does not match the search criteria or endpoint.",
|
|
585
|
-
};
|
|
586
|
-
const hits = allHits.slice(offset, offset + limit);
|
|
587
|
-
const complete = offset + hits.length >= allHits.length;
|
|
588
|
-
const renderedHits = hits
|
|
589
|
-
.map(([ref, text]) => `- @${ref}\n Exact-match excerpt:\n${quoteMarkdown(snippet(text, query), " ")}`)
|
|
590
|
-
.join("\n");
|
|
591
|
-
return {
|
|
592
|
-
ok: true,
|
|
593
|
-
markdown: `# Literal search\n\nScope: history${args.within === undefined ? "" : ` · ${args.within}`}\nNormalization: ${caseSensitive ? "original text" : "NFKC + case folding"}\nTotal matches: ${allHits.length}\n${renderedHits || "Zero literal matches do not prove that the fact is absent from the world."}\n\n---\nThis page: ${offset}..${offset + hits.length} / ${allHits.length} matches\nComplete: ${complete ? "yes" : "no"}${complete ? "" : `\nNext-page cursor: ${cursorFor(scope, offset + hits.length)}`}`,
|
|
594
|
-
};
|
|
607
|
+
return toolFailure("Use history_search for committed history.");
|
|
595
608
|
}
|
|
596
609
|
function searchState(snapshot, input) {
|
|
597
610
|
const within = parseStateSearchScope(input.within);
|
|
@@ -652,7 +665,7 @@ function knownStateDirectory(snapshot, directory, declaredDirectories) {
|
|
|
652
665
|
const logicalPrefix = `${snapshot.logicalRoot}/${relativePrefix}`;
|
|
653
666
|
return snapshot.files.some(({ path }) => path.startsWith(logicalPrefix));
|
|
654
667
|
}
|
|
655
|
-
function executeStateList(snapshot,
|
|
668
|
+
function executeStateList(snapshot, args, declaredDirectories) {
|
|
656
669
|
if (!record(args) ||
|
|
657
670
|
!hasOnlyToolKeys(args, ["parent", "cursor", "limit"]) ||
|
|
658
671
|
typeof args.parent !== "string" ||
|
|
@@ -663,21 +676,9 @@ function executeStateList(snapshot, history, args, declaredDirectories) {
|
|
|
663
676
|
ok: false,
|
|
664
677
|
markdown: "# Runtime argument error\n\nstate_list requires an @dir-* parent returned by Runtime and accepts only cursor and limit in addition; use history_list for committed history.",
|
|
665
678
|
};
|
|
666
|
-
return executeContextList(snapshot,
|
|
667
|
-
}
|
|
668
|
-
function executeHistoryList(snapshot, history, args) {
|
|
669
|
-
if (!record(args) ||
|
|
670
|
-
!hasOnlyToolKeys(args, ["order", "cursor", "limit"]) ||
|
|
671
|
-
(args.order !== "newest_first" && args.order !== "oldest_first") ||
|
|
672
|
-
(args.limit !== undefined && typeof args.limit !== "number") ||
|
|
673
|
-
!validOptionalCursor(args.cursor))
|
|
674
|
-
return {
|
|
675
|
-
ok: false,
|
|
676
|
-
markdown: "# Runtime argument error\n\nhistory_list requires newest_first or oldest_first order and accepts only cursor and limit in addition; use state_list for state directories.",
|
|
677
|
-
};
|
|
678
|
-
return executeContextList(snapshot, history, { ...args, source: "history" });
|
|
679
|
+
return executeContextList(snapshot, { ...args, source: "state" }, declaredDirectories);
|
|
679
680
|
}
|
|
680
|
-
function executeContextList(snapshot,
|
|
681
|
+
function executeContextList(snapshot, args, declaredDirectories = []) {
|
|
681
682
|
if (!record(args) || (args.source !== "state" && args.source !== "history"))
|
|
682
683
|
return {
|
|
683
684
|
ok: false,
|
|
@@ -744,31 +745,9 @@ function executeContextList(snapshot, history, args, declaredDirectories = []) {
|
|
|
744
745
|
markdown: `# Directory listing\n\nScope: state · ${parent}\nCoverage: ${result.coverage.status === "complete" ? "complete" : "partial"}\n${entries.join("\n") || "(empty)"}\n\n---\nThis page: ${result.page.start}..${result.page.end} / ${result.page.total} items\nComplete: ${result.page.complete ? "yes" : "no"}${result.page.nextCursor === null ? "" : `\nNext-page cursor: ${result.page.nextCursor}`}`,
|
|
745
746
|
};
|
|
746
747
|
}
|
|
747
|
-
|
|
748
|
-
.sort((a, b) => (args.order ?? "newest_first") === "oldest_first"
|
|
749
|
-
? a.path.localeCompare(b.path)
|
|
750
|
-
: b.path.localeCompare(a.path))
|
|
751
|
-
.map(({ path, contents }) => `- @${historyRef(path)}, ${Buffer.byteLength(contents, "utf8")} bytes`);
|
|
752
|
-
const scope = JSON.stringify({
|
|
753
|
-
kind: "list",
|
|
754
|
-
source: "history",
|
|
755
|
-
order: args.order,
|
|
756
|
-
limit,
|
|
757
|
-
});
|
|
758
|
-
const offset = parseCursor(args.cursor, scope);
|
|
759
|
-
if (offset === null)
|
|
760
|
-
return {
|
|
761
|
-
ok: false,
|
|
762
|
-
markdown: "# Runtime argument error\n\nThe cursor does not match the listing criteria or endpoint.",
|
|
763
|
-
};
|
|
764
|
-
const page = entries.slice(offset, offset + limit);
|
|
765
|
-
const complete = offset + page.length >= entries.length;
|
|
766
|
-
return {
|
|
767
|
-
ok: true,
|
|
768
|
-
markdown: `# Directory listing\n\nScope: history · ${String(args.order)}\n${page.join("\n") || "(empty)"}\n\n---\nThis page: ${offset}..${offset + page.length} / ${entries.length} items\nComplete: ${complete ? "yes" : "no"}${complete ? "" : `\nNext-page cursor: ${cursorFor(scope, offset + page.length)}`}`,
|
|
769
|
-
};
|
|
748
|
+
return toolFailure("Use history_list for committed history.");
|
|
770
749
|
}
|
|
771
|
-
function executeContextRead(snapshot,
|
|
750
|
+
function executeContextRead(snapshot, args) {
|
|
772
751
|
if (!record(args) ||
|
|
773
752
|
// Frozen contexts created before complete exact reads still advertise
|
|
774
753
|
// cursor/maxBytes. Accept and ignore those obsolete arguments so an
|
|
@@ -779,13 +758,6 @@ function executeContextRead(snapshot, history, args) {
|
|
|
779
758
|
ok: false,
|
|
780
759
|
markdown: "# Runtime argument error\n\nA stable ref returned by list or search is required.",
|
|
781
760
|
};
|
|
782
|
-
const historyRefValue = args.ref.replace(/^@/u, "");
|
|
783
|
-
const historyEntry = history.find(({ path }) => historyRef(path) === historyRefValue);
|
|
784
|
-
if (historyEntry !== undefined)
|
|
785
|
-
return {
|
|
786
|
-
ok: true,
|
|
787
|
-
markdown: `# Exact read @${historyRefValue}\n\n${historyEntry.contents}\n\n---\nSource: @${historyRefValue}\nComplete: yes`,
|
|
788
|
-
};
|
|
789
761
|
const handle = parseStateReadHandle(args.ref);
|
|
790
762
|
if (handle === null)
|
|
791
763
|
return {
|
|
@@ -926,45 +898,6 @@ function hasOnlyToolKeys(value, allowed) {
|
|
|
926
898
|
function validOptionalCursor(value) {
|
|
927
899
|
return value === undefined || value === null || typeof value === "string";
|
|
928
900
|
}
|
|
929
|
-
function normalizeSearch(value, caseSensitive) {
|
|
930
|
-
const normalized = value.normalize("NFKC");
|
|
931
|
-
return caseSensitive ? normalized : normalized.toLocaleLowerCase("und");
|
|
932
|
-
}
|
|
933
|
-
function snippet(text, query) {
|
|
934
|
-
const position = text
|
|
935
|
-
.normalize("NFKC")
|
|
936
|
-
.toLocaleLowerCase("und")
|
|
937
|
-
.indexOf(query.normalize("NFKC").toLocaleLowerCase("und"));
|
|
938
|
-
const start = Math.max(0, position - 120);
|
|
939
|
-
return text.slice(start, start + 240).replace(/\s+/gu, " ");
|
|
940
|
-
}
|
|
941
|
-
function historyRef(path) {
|
|
942
|
-
return `history-message-${path.replace(/\.md$/u, "")}`;
|
|
943
|
-
}
|
|
944
|
-
function cursorFor(scope, offset) {
|
|
945
|
-
return Buffer.from(JSON.stringify({
|
|
946
|
-
scope: createHash("sha256").update(scope).digest("hex"),
|
|
947
|
-
offset,
|
|
948
|
-
}), "utf8").toString("base64url");
|
|
949
|
-
}
|
|
950
|
-
function parseCursor(cursor, scope) {
|
|
951
|
-
if (cursor === undefined || cursor === null)
|
|
952
|
-
return 0;
|
|
953
|
-
if (typeof cursor !== "string")
|
|
954
|
-
return null;
|
|
955
|
-
try {
|
|
956
|
-
const value = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
957
|
-
if (!record(value) ||
|
|
958
|
-
value.scope !== createHash("sha256").update(scope).digest("hex") ||
|
|
959
|
-
!Number.isSafeInteger(value.offset) ||
|
|
960
|
-
Number(value.offset) < 0)
|
|
961
|
-
return null;
|
|
962
|
-
return Number(value.offset);
|
|
963
|
-
}
|
|
964
|
-
catch {
|
|
965
|
-
return null;
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
901
|
export function parseNarrative(args) {
|
|
969
902
|
if (!record(args) ||
|
|
970
903
|
typeof args.text !== "string" ||
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { FileNativePlayDocuments, } from "./PlayDocumentTools.js";
|
|
2
|
+
import { currentPlayPrompt, playPromptRunsThroughEvents, } from "./PlayPromptRun.js";
|
|
3
|
+
import { isPlayerRoundMarker, playerInputAppend } from "./PlayContinuity.js";
|
|
4
|
+
import { PlayCallChainError } from "./PlayCallChainError.js";
|
|
5
|
+
/**
|
|
6
|
+
* Prepare the selected player's prefix at the existing pre-commit seam.
|
|
7
|
+
* Endpoint reads, revision submission and publication belong to the operation.
|
|
8
|
+
*/
|
|
9
|
+
export function preparePlayerRevisionTrace(source, eventIndex) {
|
|
10
|
+
const prefix = structuredClone(source.events.slice(0, eventIndex));
|
|
11
|
+
const transcript = transcriptThroughEvents(source.transcript, prefix);
|
|
12
|
+
const player = source.events[eventIndex];
|
|
13
|
+
return {
|
|
14
|
+
// This append precedes the operation's materialization check, as before.
|
|
15
|
+
withReplacement(selected, replacement) {
|
|
16
|
+
const events = [
|
|
17
|
+
...prefix,
|
|
18
|
+
{
|
|
19
|
+
id: player.id,
|
|
20
|
+
kind: "player",
|
|
21
|
+
exchangeId: replacement.exchangeId,
|
|
22
|
+
text: replacement.text,
|
|
23
|
+
context: player.context,
|
|
24
|
+
committedHead: replacement.head,
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
const continuedTranscript = [
|
|
28
|
+
...transcript,
|
|
29
|
+
...playerInputAppend({
|
|
30
|
+
history: Object.fromEntries(selected.history.map(({ messageId, exactText }) => [
|
|
31
|
+
messageId,
|
|
32
|
+
exactText,
|
|
33
|
+
])),
|
|
34
|
+
checkpoint: selected.narrativeCheckpoint,
|
|
35
|
+
text: replacement.text,
|
|
36
|
+
locale: replacement.locale,
|
|
37
|
+
checkpointAvailable: source.tools.some(({ name }) => name === "world_checkpoint"),
|
|
38
|
+
}),
|
|
39
|
+
];
|
|
40
|
+
return {
|
|
41
|
+
atEndpoint(endpoint) {
|
|
42
|
+
return assembleRetainedTrace(source, endpoint, {
|
|
43
|
+
events,
|
|
44
|
+
transcript: continuedTranscript,
|
|
45
|
+
toolEvents: prefix,
|
|
46
|
+
authorizationEvents: prefix,
|
|
47
|
+
exchangeEvents: prefix,
|
|
48
|
+
complete: false,
|
|
49
|
+
materials: endpoint.binding.additionalMaterials,
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
pagePrefixAtEndpoint(endpoint) {
|
|
55
|
+
if (prefix.length === 0)
|
|
56
|
+
return null;
|
|
57
|
+
// Preserve the fresh path's post-commit prefix reconstruction seam.
|
|
58
|
+
const prefixTranscript = transcriptThroughEvents(source.transcript, prefix);
|
|
59
|
+
return assembleRetainedTrace(source, endpoint, {
|
|
60
|
+
events: prefix,
|
|
61
|
+
transcript: prefixTranscript,
|
|
62
|
+
toolEvents: prefix,
|
|
63
|
+
authorizationEvents: prefix,
|
|
64
|
+
exchangeEvents: prefix,
|
|
65
|
+
complete: false,
|
|
66
|
+
materials: endpoint.selected.additionalMaterials,
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** A complete context defines its own closure; partial contexts use page events. */
|
|
72
|
+
export function prepareForkTrace(source, events) {
|
|
73
|
+
const complete = events.length === source.events.length;
|
|
74
|
+
const transcript = complete
|
|
75
|
+
? structuredClone(source.transcript)
|
|
76
|
+
: transcriptThroughEvents(source.transcript, events);
|
|
77
|
+
return {
|
|
78
|
+
atEndpoint(endpoint) {
|
|
79
|
+
return assembleRetainedTrace(source, endpoint, {
|
|
80
|
+
events,
|
|
81
|
+
transcript,
|
|
82
|
+
toolEvents: events,
|
|
83
|
+
authorizationEvents: events,
|
|
84
|
+
exchangeEvents: events,
|
|
85
|
+
complete,
|
|
86
|
+
materials: endpoint.binding.additionalMaterials,
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function assembleRetainedTrace(source, endpoint, selection) {
|
|
92
|
+
const completedKeys = selection.complete
|
|
93
|
+
? null
|
|
94
|
+
: completedToolKeys(selection.toolEvents);
|
|
95
|
+
const documents = restorePlayDocuments(endpoint.binding.files, source, selection.authorizationEvents);
|
|
96
|
+
return {
|
|
97
|
+
continuityContextId: source.continuityContextId ?? source.chainId,
|
|
98
|
+
baselineHead: source.baselineHead,
|
|
99
|
+
baselineHistoryLength: source.baselineHistoryLength ?? endpoint.baseline.history.length,
|
|
100
|
+
promptRuns: playPromptRunsThroughEvents(source, selection.events),
|
|
101
|
+
...(source.presetFiles === undefined
|
|
102
|
+
? {}
|
|
103
|
+
: { presetFiles: structuredClone(source.presetFiles) }),
|
|
104
|
+
playPreset: structuredClone(source.playPreset),
|
|
105
|
+
...(source.followups === undefined
|
|
106
|
+
? {}
|
|
107
|
+
: { followups: structuredClone(source.followups) }),
|
|
108
|
+
...(source.playPresetScriptsEnabled === undefined
|
|
109
|
+
? {}
|
|
110
|
+
: { playPresetScriptsEnabled: source.playPresetScriptsEnabled }),
|
|
111
|
+
...(source.modelBinding === undefined
|
|
112
|
+
? {}
|
|
113
|
+
: { modelBinding: structuredClone(source.modelBinding) }),
|
|
114
|
+
status: "ready",
|
|
115
|
+
canRetry: false,
|
|
116
|
+
bootstrap: structuredClone(source.bootstrap),
|
|
117
|
+
tools: structuredClone(source.tools),
|
|
118
|
+
transcript: structuredClone(selection.transcript),
|
|
119
|
+
events: structuredClone([...selection.events]),
|
|
120
|
+
completedTools: source.completedTools
|
|
121
|
+
.filter(({ key }) => completedKeys === null || completedKeys.has(key))
|
|
122
|
+
.map((item) => structuredClone(item)),
|
|
123
|
+
documentAuthorizationCheckpoints: authorizationCheckpointsThroughEvents(source, selection.events, documents.authorizationCheckpoint()),
|
|
124
|
+
changedDocuments: changedDocumentsAtHead(source.changedDocuments, endpoint.baseline.state, endpoint.selected.state),
|
|
125
|
+
nextMaterials: structuredClone(selection.materials),
|
|
126
|
+
nextEventId: Math.max(0, ...selection.events.map(({ id }) => id)) + 1,
|
|
127
|
+
exchange: completedAssistantExchange(selection.exchangeEvents),
|
|
128
|
+
lastRequest: null,
|
|
129
|
+
lastRequestAttempt: 0,
|
|
130
|
+
lastFailure: null,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export function restorePlayDocuments(files, context, events) {
|
|
134
|
+
const documents = new FileNativePlayDocuments(files);
|
|
135
|
+
try {
|
|
136
|
+
const checkpoint = documentAuthorizationThroughEvents(context, events);
|
|
137
|
+
const runs = playPromptRunsThroughEvents(context, events);
|
|
138
|
+
if (checkpoint === undefined || runs.length > 0)
|
|
139
|
+
documents.bindBootstrap(currentPlayPrompt({ ...context, promptRuns: runs }).bootstrap);
|
|
140
|
+
if (checkpoint !== undefined)
|
|
141
|
+
documents.restoreAuthorizationCheckpoint(checkpoint.authorization);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
throw new PlayCallChainError(`Call-chain document authorization could not be restored: ${error instanceof Error ? error.message : "invalid checkpoint"}。`);
|
|
145
|
+
}
|
|
146
|
+
return documents;
|
|
147
|
+
}
|
|
148
|
+
function documentAuthorizationThroughEvents(context, events) {
|
|
149
|
+
const checkpoints = context.documentAuthorizationCheckpoints;
|
|
150
|
+
if (checkpoints === undefined || checkpoints.length === 0)
|
|
151
|
+
return undefined;
|
|
152
|
+
const selectedEventIds = new Set(events.map(({ id }) => id));
|
|
153
|
+
const selected = checkpoints.findLast(({ afterEventId }) => afterEventId === 0 || selectedEventIds.has(afterEventId));
|
|
154
|
+
// Legacy V1 records had no durable dynamic authorization. If the selected
|
|
155
|
+
// prefix predates their first lazily-written checkpoint, retain the exact
|
|
156
|
+
// old recovery behavior and rebuild only bootstrap authorization.
|
|
157
|
+
return selected === undefined ? undefined : structuredClone(selected);
|
|
158
|
+
}
|
|
159
|
+
export function independentContextCopy(source) {
|
|
160
|
+
const context = structuredClone(source);
|
|
161
|
+
context.continuityContextId ??= source.chainId;
|
|
162
|
+
delete context.derivedFrom;
|
|
163
|
+
delete context.branchedBeforePlayer;
|
|
164
|
+
return context;
|
|
165
|
+
}
|
|
166
|
+
function authorizationCheckpointsThroughEvents(context, events, fallback) {
|
|
167
|
+
const eventIds = new Set(events.map(({ id }) => id));
|
|
168
|
+
const selected = (context.documentAuthorizationCheckpoints ?? [])
|
|
169
|
+
.filter(({ afterEventId }) => afterEventId === 0 || eventIds.has(afterEventId))
|
|
170
|
+
.map((checkpoint) => structuredClone(checkpoint));
|
|
171
|
+
if (selected.length > 0)
|
|
172
|
+
return selected;
|
|
173
|
+
return [
|
|
174
|
+
{
|
|
175
|
+
afterEventId: Math.max(0, ...events.map(({ id }) => id)),
|
|
176
|
+
authorization: structuredClone(fallback),
|
|
177
|
+
},
|
|
178
|
+
];
|
|
179
|
+
}
|
|
180
|
+
function transcriptThroughEvents(transcript, events) {
|
|
181
|
+
const result = [];
|
|
182
|
+
let cursor = 0;
|
|
183
|
+
for (const [index, event] of events.entries()) {
|
|
184
|
+
if (event.kind === "player") {
|
|
185
|
+
const marker = transcript[cursor];
|
|
186
|
+
if (isPlayerRoundMarker(marker)) {
|
|
187
|
+
result.push(structuredClone(marker));
|
|
188
|
+
cursor += 1;
|
|
189
|
+
}
|
|
190
|
+
const item = transcript[cursor];
|
|
191
|
+
if (item?.kind !== "player" || item.text !== event.text)
|
|
192
|
+
throw new PlayCallChainError("Source call-chain events do not match the model transcript, so a fork cannot be created safely.");
|
|
193
|
+
result.push(structuredClone(item));
|
|
194
|
+
cursor += 1;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (event.kind === "assistant" && event.status === "completed") {
|
|
198
|
+
const notice = transcript[cursor];
|
|
199
|
+
const hasContinuationNotice = notice?.kind === "runtime_notice" && notice.notice === "continuation";
|
|
200
|
+
const item = transcript[cursor + (hasContinuationNotice ? 1 : 0)];
|
|
201
|
+
const hasToolCall = events
|
|
202
|
+
.slice(index + 1)
|
|
203
|
+
.find((candidate) => candidate.kind === "player" ||
|
|
204
|
+
candidate.kind === "assistant" ||
|
|
205
|
+
candidate.kind === "tool_call");
|
|
206
|
+
const recorded = event.text.trim().length > 0 ||
|
|
207
|
+
hasToolCall?.kind === "tool_call" ||
|
|
208
|
+
item?.kind === "assistant";
|
|
209
|
+
if (!recorded)
|
|
210
|
+
continue;
|
|
211
|
+
if (item?.kind !== "assistant" || item.text !== event.text)
|
|
212
|
+
throw new PlayCallChainError("Source call-chain responses do not match the model transcript, so a fork cannot be created safely.");
|
|
213
|
+
// A continuation notice belongs to the generation it precedes, never
|
|
214
|
+
// to a fork ending at the previously completed narrative.
|
|
215
|
+
if (hasContinuationNotice) {
|
|
216
|
+
result.push(structuredClone(notice));
|
|
217
|
+
cursor += 1;
|
|
218
|
+
}
|
|
219
|
+
result.push(structuredClone(item));
|
|
220
|
+
cursor += 1;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (event.kind === "tool_result") {
|
|
224
|
+
const item = transcript[cursor];
|
|
225
|
+
if (item?.kind !== "tool" || item.toolCallId !== event.callId)
|
|
226
|
+
throw new PlayCallChainError("Source call-chain tool results do not match the model transcript, so a fork cannot be created safely.");
|
|
227
|
+
result.push(structuredClone(item));
|
|
228
|
+
cursor += 1;
|
|
229
|
+
// A tool-step notice follows the entire settled batch and remains part
|
|
230
|
+
// of its closure even when the selected endpoint stops at that batch.
|
|
231
|
+
const notice = transcript[cursor];
|
|
232
|
+
if (notice?.kind === "runtime_notice" && notice.notice === "tool_step") {
|
|
233
|
+
result.push(structuredClone(notice));
|
|
234
|
+
cursor += 1;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
function completedToolKeys(events) {
|
|
241
|
+
const keys = new Set();
|
|
242
|
+
let exchange = null;
|
|
243
|
+
for (const event of events) {
|
|
244
|
+
if (event.kind === "player")
|
|
245
|
+
exchange = null;
|
|
246
|
+
else if (event.kind === "assistant")
|
|
247
|
+
exchange = event.exchange;
|
|
248
|
+
else if (event.kind === "tool_call" && exchange !== null)
|
|
249
|
+
keys.add(`${exchange}:${event.callId}`);
|
|
250
|
+
}
|
|
251
|
+
return keys;
|
|
252
|
+
}
|
|
253
|
+
function changedDocumentsAtHead(changes, baseline, selected) {
|
|
254
|
+
const before = new Map(baseline.map((file) => [file.path, file.contents]));
|
|
255
|
+
const after = new Map(selected.map((file) => [file.path, file.contents]));
|
|
256
|
+
return changes
|
|
257
|
+
.filter(({ path }) => after.has(path) && before.get(path) !== after.get(path))
|
|
258
|
+
.map((change) => structuredClone(change));
|
|
259
|
+
}
|
|
260
|
+
function completedAssistantExchange(events) {
|
|
261
|
+
return Math.max(0, ...events
|
|
262
|
+
.filter((event) => event.kind === "assistant" && event.status === "completed")
|
|
263
|
+
.map(({ exchange }) => exchange));
|
|
264
|
+
}
|