narraeon 0.7.10 → 0.8.0
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/modelConnections.js +15 -0
- package/dist/node/protocol/modelUsage.js +4 -0
- package/dist/node/protocol/sillyTavernContent.js +1 -0
- package/dist/node/protocol/v1.js +75 -0
- package/dist/node/runtime/V1Runtime.js +71 -1
- package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +15 -3
- package/dist/node/runtime/authoring/DamagedGoogleContinuation.js +68 -0
- package/dist/node/runtime/authoring/ModelToolConversation.js +47 -22
- package/dist/node/runtime/content/ContentWorkspace.js +3 -0
- package/dist/node/runtime/content/SillyTavernContent.js +796 -0
- package/dist/node/runtime/model/AiFailureLog.js +8 -1
- package/dist/node/runtime/model/FileNativeModelAdapters.js +610 -18
- package/dist/node/runtime/model/GoogleHopJournal.js +90 -0
- package/dist/node/runtime/model/GoogleInteractionsStream.js +242 -0
- package/dist/node/runtime/model/GoogleRequestPreview.js +184 -0
- package/dist/node/runtime/model/ModelConnectionStore.js +4 -1
- package/dist/node/runtime/model/ProviderExchangeState.js +44 -1
- package/dist/node/runtime/model/ProviderStream.js +4 -2
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +4 -4
- package/dist/node/runtime/play/FileNativePlayPresetStore.js +150 -78
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +150 -10
- package/dist/node/runtime/play/OrderedPlayPrompts.js +139 -178
- package/dist/node/runtime/play/PlayAdvanceLifecycle.js +33 -9
- package/dist/node/runtime/play/PlayCallChain.js +76 -4
- package/dist/node/runtime/play/PlayFollowupRequests.js +23 -3
- package/dist/node/runtime/prompt/PromptCompilationCodec.js +2 -1
- package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +112 -8
- package/dist/node/runtime/setting/SettingImprovementSession.js +48 -7
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +84 -8
- package/dist/node/runtime/world-revision/WorldRevisionSession.js +125 -17
- package/dist/node/shared/application-preset-files.js +29 -0
- package/dist/node/shared/default-preset-host.js +50 -38
- package/dist/node/shared/first-party-action-choices.js +7 -11
- package/dist/node/shared/first-party-generic-panels.js +7 -11
- package/dist/node/shared/first-party-player-view.js +7 -11
- package/dist/node/shared/ordered-author-prompts.js +6 -4
- package/dist/node/shared/ordered-play-prompts.js +4 -2
- package/dist/node/shared/prompt-arrangement.js +205 -0
- package/dist/node/shared/sillytavern-preset.js +454 -0
- package/dist/web/assets/{index-DH2BVsYd.css → index-BKA1D0Rz.css} +1 -1
- package/dist/web/assets/index-C_Qta8UL.js +1432 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-B2kbaOGe.js +0 -1408
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, readdir, rename, rm, writeFile, } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { isSafeGoogleHop, withGoogleHop } from "./GoogleRequestPreview.js";
|
|
5
|
+
const pendingWrites = new Map();
|
|
6
|
+
const pendingSnapshots = new Map();
|
|
7
|
+
function ownerDirectory(root, owner) {
|
|
8
|
+
return join(root, createHash("sha256").update(owner).digest("hex"));
|
|
9
|
+
}
|
|
10
|
+
/** Each hop has its own file, so unrelated exchanges never overwrite one another. */
|
|
11
|
+
export function writeGoogleHop(root, owner, hop, retained = () => Promise.resolve(true)) {
|
|
12
|
+
if (!isSafeGoogleHop(hop))
|
|
13
|
+
throw new Error("Unsafe Google hop evidence");
|
|
14
|
+
const directory = ownerDirectory(root, owner);
|
|
15
|
+
const path = join(directory, `${hop.exchange}-${hop.attempt}-${hop.hop}.json`);
|
|
16
|
+
const snapshot = JSON.stringify(hop);
|
|
17
|
+
pendingSnapshots.set(path, structuredClone(hop));
|
|
18
|
+
const previous = pendingWrites.get(path) ?? Promise.resolve();
|
|
19
|
+
const pending = previous
|
|
20
|
+
.catch(() => undefined)
|
|
21
|
+
.then(async () => {
|
|
22
|
+
if (!(await retained()))
|
|
23
|
+
return;
|
|
24
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
25
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
26
|
+
try {
|
|
27
|
+
await writeFile(temporary, `${snapshot}\n`, { mode: 0o600 });
|
|
28
|
+
if (!(await retained()))
|
|
29
|
+
return;
|
|
30
|
+
await rename(temporary, path);
|
|
31
|
+
if (!(await retained()))
|
|
32
|
+
await rm(root, { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
await rm(temporary, { force: true });
|
|
36
|
+
if (!(await retained()))
|
|
37
|
+
await rm(root, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
pendingWrites.set(path, pending);
|
|
41
|
+
void pending
|
|
42
|
+
.finally(() => {
|
|
43
|
+
if (pendingWrites.get(path) === pending) {
|
|
44
|
+
pendingWrites.delete(path);
|
|
45
|
+
pendingSnapshots.delete(path);
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
.catch(() => undefined);
|
|
49
|
+
return pending;
|
|
50
|
+
}
|
|
51
|
+
/** Corrupt optional evidence is omitted without affecting the underlying request. */
|
|
52
|
+
export async function readGoogleHops(root, owner, wire) {
|
|
53
|
+
const directory = ownerDirectory(root, owner);
|
|
54
|
+
const names = await readdir(directory).catch(() => []);
|
|
55
|
+
let result = wire;
|
|
56
|
+
for (const name of names
|
|
57
|
+
.filter((name) => /^[1-9]\d*-[1-9]\d*-[1-9]\d*\.json$/u.test(name))
|
|
58
|
+
.sort()) {
|
|
59
|
+
try {
|
|
60
|
+
const hop = JSON.parse(await readFile(join(directory, name), "utf8"));
|
|
61
|
+
if (`${hop.exchange}-${hop.attempt}-${hop.hop}.json` !== name)
|
|
62
|
+
continue;
|
|
63
|
+
const existing = result.googleHops?.find((item) => item.exchange === hop.exchange &&
|
|
64
|
+
item.attempt === hop.attempt &&
|
|
65
|
+
item.hop === hop.hop);
|
|
66
|
+
if (existing !== undefined && existing.status !== "attempted")
|
|
67
|
+
continue;
|
|
68
|
+
result = withGoogleHop(result, hop);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// A single damaged diagnostic cannot prevent the request from loading.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const [path, hop] of pendingSnapshots)
|
|
75
|
+
if (path.startsWith(`${directory}/`)) {
|
|
76
|
+
try {
|
|
77
|
+
const existing = wire.googleHops?.find((item) => item.exchange === hop.exchange &&
|
|
78
|
+
item.attempt === hop.attempt &&
|
|
79
|
+
item.hop === hop.hop);
|
|
80
|
+
if (existing === undefined || existing.status === "attempted")
|
|
81
|
+
result = withGoogleHop(result, hop);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Invalid pending evidence is treated like a damaged diagnostic file.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (result.googleHops !== undefined)
|
|
88
|
+
result.googleHops.sort((a, b) => a.exchange - b.exchange || a.attempt - b.attempt || a.hop - b.hop);
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { ModelHostOutcomeUnknownError, } from "./ModelHost.js";
|
|
2
|
+
import { providerStreamEvents } from "./ProviderStream.js";
|
|
3
|
+
import { validGoogleInteractionsSteps } from "./ProviderExchangeState.js";
|
|
4
|
+
function record(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function invalid() {
|
|
8
|
+
throw new ModelHostOutcomeUnknownError("Google Interactions stream is incomplete or cannot be preserved losslessly");
|
|
9
|
+
}
|
|
10
|
+
function parseLossless(source) {
|
|
11
|
+
return JSON.parse(source, (_key, value, context) => {
|
|
12
|
+
if (typeof value === "number" &&
|
|
13
|
+
(!Number.isFinite(value) ||
|
|
14
|
+
(Number.isInteger(value) && !Number.isSafeInteger(value)) ||
|
|
15
|
+
context?.source !== JSON.stringify(value)))
|
|
16
|
+
invalid();
|
|
17
|
+
return value;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/** Assemble only documented deltas; unknown shapes fail closed rather than lose native data. */
|
|
21
|
+
export async function aggregateGoogleInteractions(body, onDelta, onUsage) {
|
|
22
|
+
const steps = new Map();
|
|
23
|
+
let terminal;
|
|
24
|
+
let latestUsage;
|
|
25
|
+
let created = false;
|
|
26
|
+
let lifecycleValid = true;
|
|
27
|
+
let sawStep = false;
|
|
28
|
+
let done = false;
|
|
29
|
+
for await (const event of providerStreamEvents(body, { fatalUtf8: true })) {
|
|
30
|
+
if (event.data === "[DONE]") {
|
|
31
|
+
if (terminal === undefined || done)
|
|
32
|
+
lifecycleValid = false;
|
|
33
|
+
done = true;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (done || terminal !== undefined)
|
|
37
|
+
lifecycleValid = false;
|
|
38
|
+
let value;
|
|
39
|
+
try {
|
|
40
|
+
value = parseLossless(event.data);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
invalid();
|
|
44
|
+
}
|
|
45
|
+
if (!record(value) || typeof value.event_type !== "string")
|
|
46
|
+
invalid();
|
|
47
|
+
if (!created && value.event_type !== "interaction.created")
|
|
48
|
+
lifecycleValid = false;
|
|
49
|
+
if (value.event_type === "interaction.created") {
|
|
50
|
+
if (created || sawStep || terminal !== undefined || done)
|
|
51
|
+
lifecycleValid = false;
|
|
52
|
+
created = true;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (value.event_type === "interaction.status_update") {
|
|
56
|
+
if (terminal !== undefined)
|
|
57
|
+
invalid();
|
|
58
|
+
if (value.usage !== undefined) {
|
|
59
|
+
latestUsage = value.usage;
|
|
60
|
+
onUsage?.(value.usage);
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (value.event_type === "interaction.completed") {
|
|
65
|
+
if (terminal !== undefined || !record(value.interaction))
|
|
66
|
+
invalid();
|
|
67
|
+
terminal = value.interaction;
|
|
68
|
+
if (terminal.usage !== undefined) {
|
|
69
|
+
latestUsage = terminal.usage;
|
|
70
|
+
onUsage?.(terminal.usage);
|
|
71
|
+
}
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (terminal !== undefined ||
|
|
75
|
+
typeof value.index !== "number" ||
|
|
76
|
+
!Number.isSafeInteger(value.index) ||
|
|
77
|
+
value.index < 0)
|
|
78
|
+
invalid();
|
|
79
|
+
const index = value.index;
|
|
80
|
+
sawStep = true;
|
|
81
|
+
if (value.event_type === "step.start") {
|
|
82
|
+
if (steps.has(index) ||
|
|
83
|
+
!record(value.step) ||
|
|
84
|
+
typeof value.step.type !== "string")
|
|
85
|
+
invalid();
|
|
86
|
+
steps.set(index, {
|
|
87
|
+
step: structuredClone(value.step),
|
|
88
|
+
stopped: false,
|
|
89
|
+
args: null,
|
|
90
|
+
});
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const current = steps.get(index);
|
|
94
|
+
if (current === undefined || current.stopped)
|
|
95
|
+
invalid();
|
|
96
|
+
if (value.event_type === "step.stop") {
|
|
97
|
+
if (current.args !== null) {
|
|
98
|
+
try {
|
|
99
|
+
current.step.arguments = parseLossless(current.args);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
invalid();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
current.stopped = true;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (value.event_type !== "step.delta" || !record(value.delta))
|
|
109
|
+
invalid();
|
|
110
|
+
const delta = value.delta;
|
|
111
|
+
if (delta.type === "thought_signature" &&
|
|
112
|
+
current.step.type === "thought" &&
|
|
113
|
+
typeof delta.signature === "string" &&
|
|
114
|
+
Object.keys(delta).length === 2 &&
|
|
115
|
+
(current.step.signature === undefined || current.step.signature === "") &&
|
|
116
|
+
delta.signature.length > 0) {
|
|
117
|
+
current.step.signature = delta.signature;
|
|
118
|
+
}
|
|
119
|
+
else if ((delta.type === "arguments_delta" || delta.type === "arguments") &&
|
|
120
|
+
current.step.type === "function_call") {
|
|
121
|
+
const fragment = delta.type === "arguments_delta"
|
|
122
|
+
? delta.arguments
|
|
123
|
+
: delta.partial_arguments;
|
|
124
|
+
if (typeof fragment !== "string" || Object.keys(delta).length !== 2)
|
|
125
|
+
invalid();
|
|
126
|
+
if (current.args === null &&
|
|
127
|
+
current.step.arguments !== undefined &&
|
|
128
|
+
(!record(current.step.arguments) ||
|
|
129
|
+
Object.keys(current.step.arguments).length !== 0))
|
|
130
|
+
invalid();
|
|
131
|
+
current.args = (current.args ?? "") + fragment;
|
|
132
|
+
}
|
|
133
|
+
else if (delta.type === "text" &&
|
|
134
|
+
current.step.type === "model_output" &&
|
|
135
|
+
typeof delta.text === "string" &&
|
|
136
|
+
Object.keys(delta).length === 2) {
|
|
137
|
+
if (current.step.content === undefined)
|
|
138
|
+
current.step.content = [];
|
|
139
|
+
if (!Array.isArray(current.step.content))
|
|
140
|
+
invalid();
|
|
141
|
+
const last = current.step.content.at(-1);
|
|
142
|
+
if (record(last) &&
|
|
143
|
+
last.type === "text" &&
|
|
144
|
+
typeof last.text === "string" &&
|
|
145
|
+
Object.keys(last).length === 2)
|
|
146
|
+
last.text += delta.text;
|
|
147
|
+
else
|
|
148
|
+
current.step.content.push({ type: "text", text: delta.text });
|
|
149
|
+
onDelta?.({ kind: "text", text: delta.text });
|
|
150
|
+
}
|
|
151
|
+
else if (delta.type === "thought_summary" &&
|
|
152
|
+
current.step.type === "thought" &&
|
|
153
|
+
record(delta.content) &&
|
|
154
|
+
delta.content.type === "text" &&
|
|
155
|
+
typeof delta.content.text === "string" &&
|
|
156
|
+
Object.keys(delta).length === 2 &&
|
|
157
|
+
Object.keys(delta.content).length === 2) {
|
|
158
|
+
if (current.step.summary === undefined)
|
|
159
|
+
current.step.summary = [];
|
|
160
|
+
if (!Array.isArray(current.step.summary))
|
|
161
|
+
invalid();
|
|
162
|
+
const last = current.step.summary.at(-1);
|
|
163
|
+
if (record(last) &&
|
|
164
|
+
last.type === "text" &&
|
|
165
|
+
typeof last.text === "string" &&
|
|
166
|
+
Object.keys(last).length === 2)
|
|
167
|
+
last.text += delta.content.text;
|
|
168
|
+
else
|
|
169
|
+
current.step.summary.push(structuredClone(delta.content));
|
|
170
|
+
onDelta?.({ kind: "reasoning", text: delta.content.text });
|
|
171
|
+
}
|
|
172
|
+
else
|
|
173
|
+
invalid();
|
|
174
|
+
}
|
|
175
|
+
if (terminal === undefined ||
|
|
176
|
+
typeof terminal.status !== "string" ||
|
|
177
|
+
[...steps.values()].some((entry) => !entry.stopped))
|
|
178
|
+
invalid();
|
|
179
|
+
const ordered = [...steps.entries()].sort(([a], [b]) => a - b);
|
|
180
|
+
if (ordered.some(([index], position) => index !== position))
|
|
181
|
+
invalid();
|
|
182
|
+
return {
|
|
183
|
+
steps: ordered.map(([, entry]) => entry.step),
|
|
184
|
+
status: terminal.status,
|
|
185
|
+
usage: latestUsage,
|
|
186
|
+
continuationToken: terminal.continuation_token,
|
|
187
|
+
completeLifecycle: created && lifecycleValid,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
export function projectGoogleInteractions(payload) {
|
|
191
|
+
if (payload.status !== "completed" && payload.status !== "requires_action")
|
|
192
|
+
throw new ModelHostOutcomeUnknownError(`Google Interactions returned unsupported terminal status: ${payload.status}`);
|
|
193
|
+
if (!validGoogleInteractionsSteps(payload.steps))
|
|
194
|
+
invalid();
|
|
195
|
+
const calls = [];
|
|
196
|
+
let text = "";
|
|
197
|
+
let reasoning = "";
|
|
198
|
+
for (const step of payload.steps) {
|
|
199
|
+
if (step.type === "function_call") {
|
|
200
|
+
if (typeof step.id !== "string" ||
|
|
201
|
+
!step.id ||
|
|
202
|
+
typeof step.name !== "string" ||
|
|
203
|
+
!step.name ||
|
|
204
|
+
!record(step.arguments) ||
|
|
205
|
+
calls.some((call) => call.id === step.id))
|
|
206
|
+
invalid();
|
|
207
|
+
calls.push({
|
|
208
|
+
id: step.id,
|
|
209
|
+
name: step.name,
|
|
210
|
+
arguments: structuredClone(step.arguments),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
else if (step.type === "model_output") {
|
|
214
|
+
if (!Array.isArray(step.content))
|
|
215
|
+
invalid();
|
|
216
|
+
for (const part of step.content)
|
|
217
|
+
if (record(part) &&
|
|
218
|
+
part.type === "text" &&
|
|
219
|
+
typeof part.text === "string")
|
|
220
|
+
text += part.text;
|
|
221
|
+
}
|
|
222
|
+
else if (step.type === "thought" && Array.isArray(step.summary)) {
|
|
223
|
+
for (const part of step.summary)
|
|
224
|
+
if (record(part) &&
|
|
225
|
+
part.type === "text" &&
|
|
226
|
+
typeof part.text === "string")
|
|
227
|
+
reasoning += part.text;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if ((payload.status === "requires_action") !== calls.length > 0)
|
|
231
|
+
invalid();
|
|
232
|
+
return {
|
|
233
|
+
...(text === "" ? {} : { text }),
|
|
234
|
+
...(reasoning === "" ? {} : { reasoningContent: reasoning }),
|
|
235
|
+
toolCalls: calls,
|
|
236
|
+
stopReason: payload.status,
|
|
237
|
+
providerState: {
|
|
238
|
+
protocol: "google_interactions",
|
|
239
|
+
steps: structuredClone(payload.steps),
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
export function withGoogleHop(wire, hop) {
|
|
2
|
+
const hops = [...(wire.googleHops ?? [])];
|
|
3
|
+
const index = hops.findIndex((old) => old.exchange === hop.exchange &&
|
|
4
|
+
old.attempt === hop.attempt &&
|
|
5
|
+
old.hop === hop.hop);
|
|
6
|
+
if (index < 0)
|
|
7
|
+
hops.push(structuredClone(hop));
|
|
8
|
+
else
|
|
9
|
+
hops[index] = structuredClone(hop);
|
|
10
|
+
const updated = { ...wire, googleHops: hops };
|
|
11
|
+
if (!isSafeGoogleWireRequest(updated))
|
|
12
|
+
throw new Error("Unsafe Google hop evidence");
|
|
13
|
+
return updated;
|
|
14
|
+
}
|
|
15
|
+
export function isSafeGoogleWireRequest(value) {
|
|
16
|
+
return validWireRequest(value, true);
|
|
17
|
+
}
|
|
18
|
+
function validWireRequest(value, allowHops) {
|
|
19
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
20
|
+
return false;
|
|
21
|
+
const record = value;
|
|
22
|
+
return (Object.keys(record).every((key) => [
|
|
23
|
+
"provider",
|
|
24
|
+
"method",
|
|
25
|
+
"endpointPath",
|
|
26
|
+
"headerNames",
|
|
27
|
+
"body",
|
|
28
|
+
"systemInstructionSources",
|
|
29
|
+
"redactedProviderContinuation",
|
|
30
|
+
...(allowHops ? ["googleHops"] : []),
|
|
31
|
+
].includes(key)) &&
|
|
32
|
+
record.provider === "google_interactions" &&
|
|
33
|
+
record.redactedProviderContinuation === true &&
|
|
34
|
+
record.method === "POST" &&
|
|
35
|
+
typeof record.endpointPath === "string" &&
|
|
36
|
+
(record.endpointPath === "" ||
|
|
37
|
+
/^\/(?:[A-Za-z0-9._~!$&'()*+,;=:@%/-]*\/)?interactions$/u.test(record.endpointPath)) &&
|
|
38
|
+
Array.isArray(record.headerNames) &&
|
|
39
|
+
record.headerNames.length === 3 &&
|
|
40
|
+
record.headerNames[0] === "Accept" &&
|
|
41
|
+
record.headerNames[1] === "x-goog-api-key" &&
|
|
42
|
+
record.headerNames[2] === "Content-Type" &&
|
|
43
|
+
validGooglePreviewBody(record.body) &&
|
|
44
|
+
(record.googleHops === undefined ||
|
|
45
|
+
(allowHops &&
|
|
46
|
+
Array.isArray(record.googleHops) &&
|
|
47
|
+
record.googleHops.every((hop) => validHop(hop, record.endpointPath)))) &&
|
|
48
|
+
(record.systemInstructionSources === undefined ||
|
|
49
|
+
(Array.isArray(record.systemInstructionSources) &&
|
|
50
|
+
record.systemInstructionSources.every((source) => typeof source === "object" &&
|
|
51
|
+
source !== null &&
|
|
52
|
+
!Array.isArray(source) &&
|
|
53
|
+
Object.keys(source).every((key) => key === "promptId" || key === "sources") &&
|
|
54
|
+
(!("promptId" in source) || typeof source.promptId === "string") &&
|
|
55
|
+
"sources" in source &&
|
|
56
|
+
Array.isArray(source.sources) &&
|
|
57
|
+
source.sources.every((item) => typeof item === "string")))));
|
|
58
|
+
}
|
|
59
|
+
export function isSafeGoogleHop(value) {
|
|
60
|
+
return validHop(value);
|
|
61
|
+
}
|
|
62
|
+
function validHop(value, endpointPath) {
|
|
63
|
+
if (!isRecord(value) ||
|
|
64
|
+
!Object.keys(value).every((key) => [
|
|
65
|
+
"exchange",
|
|
66
|
+
"attempt",
|
|
67
|
+
"hop",
|
|
68
|
+
"tokenSupplied",
|
|
69
|
+
"status",
|
|
70
|
+
"httpStatus",
|
|
71
|
+
"usage",
|
|
72
|
+
"request",
|
|
73
|
+
].includes(key)))
|
|
74
|
+
return false;
|
|
75
|
+
return (Number.isSafeInteger(value.exchange) &&
|
|
76
|
+
Number(value.exchange) > 0 &&
|
|
77
|
+
Number.isSafeInteger(value.attempt) &&
|
|
78
|
+
Number(value.attempt) > 0 &&
|
|
79
|
+
Number.isSafeInteger(value.hop) &&
|
|
80
|
+
Number(value.hop) > 0 &&
|
|
81
|
+
typeof value.tokenSupplied === "boolean" &&
|
|
82
|
+
[
|
|
83
|
+
"attempted",
|
|
84
|
+
"completed",
|
|
85
|
+
"requires_action",
|
|
86
|
+
"requires_continuation",
|
|
87
|
+
"failed",
|
|
88
|
+
"cancelled",
|
|
89
|
+
"incomplete",
|
|
90
|
+
"interrupted",
|
|
91
|
+
"rejected",
|
|
92
|
+
].includes(String(value.status)) &&
|
|
93
|
+
(value.httpStatus === undefined ||
|
|
94
|
+
(Number.isSafeInteger(value.httpStatus) &&
|
|
95
|
+
Number(value.httpStatus) >= 100 &&
|
|
96
|
+
Number(value.httpStatus) <= 599)) &&
|
|
97
|
+
(value.usage === undefined ||
|
|
98
|
+
(isRecord(value.usage) &&
|
|
99
|
+
Object.entries(value.usage).every(([key, count]) => [
|
|
100
|
+
"total_input_tokens",
|
|
101
|
+
"total_cached_tokens",
|
|
102
|
+
"total_thought_tokens",
|
|
103
|
+
"total_output_tokens",
|
|
104
|
+
"total_tool_use_tokens",
|
|
105
|
+
"total_tokens",
|
|
106
|
+
].includes(key) &&
|
|
107
|
+
Number.isSafeInteger(count) &&
|
|
108
|
+
Number(count) >= 0))) &&
|
|
109
|
+
(value.request === undefined
|
|
110
|
+
? Number(value.hop) > 1
|
|
111
|
+
: validWireRequest(value.request, false) &&
|
|
112
|
+
isRecord(value.request) &&
|
|
113
|
+
(endpointPath === undefined ||
|
|
114
|
+
value.request.endpointPath === endpointPath)));
|
|
115
|
+
}
|
|
116
|
+
function validGooglePreviewBody(value) {
|
|
117
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
118
|
+
return false;
|
|
119
|
+
const body = value;
|
|
120
|
+
if (!Object.keys(body).every((key) => [
|
|
121
|
+
"model",
|
|
122
|
+
"store",
|
|
123
|
+
"stream",
|
|
124
|
+
"system_instruction",
|
|
125
|
+
"input",
|
|
126
|
+
"tools",
|
|
127
|
+
"generation_config",
|
|
128
|
+
].includes(key)) ||
|
|
129
|
+
typeof body.model !== "string" ||
|
|
130
|
+
body.store !== false ||
|
|
131
|
+
body.stream !== true ||
|
|
132
|
+
(body.system_instruction !== undefined &&
|
|
133
|
+
typeof body.system_instruction !== "string") ||
|
|
134
|
+
!Array.isArray(body.input) ||
|
|
135
|
+
!isRecord(body.generation_config))
|
|
136
|
+
return false;
|
|
137
|
+
if (!Object.keys(body.generation_config).every((key) => key === "max_output_tokens" || key === "tool_choice") ||
|
|
138
|
+
typeof body.generation_config.max_output_tokens !== "number" ||
|
|
139
|
+
(body.generation_config.tool_choice !== undefined &&
|
|
140
|
+
body.generation_config.tool_choice !== "none"))
|
|
141
|
+
return false;
|
|
142
|
+
if (!body.input.every(validGooglePreviewItem))
|
|
143
|
+
return false;
|
|
144
|
+
return (body.tools === undefined ||
|
|
145
|
+
(Array.isArray(body.tools) &&
|
|
146
|
+
body.tools.every((tool) => isRecord(tool) &&
|
|
147
|
+
Object.keys(tool).every((key) => ["type", "name", "description", "parameters"].includes(key)) &&
|
|
148
|
+
tool.type === "function" &&
|
|
149
|
+
typeof tool.name === "string" &&
|
|
150
|
+
typeof tool.description === "string" &&
|
|
151
|
+
isRecord(tool.parameters))));
|
|
152
|
+
}
|
|
153
|
+
function validGooglePreviewItem(value) {
|
|
154
|
+
if (!isRecord(value) || typeof value.type !== "string")
|
|
155
|
+
return false;
|
|
156
|
+
if (value.type === "runtime_redacted_provider_step")
|
|
157
|
+
return Object.keys(value).length === 1;
|
|
158
|
+
if (value.type === "user_input" || value.type === "model_output")
|
|
159
|
+
return (Object.keys(value).every((key) => key === "type" || key === "content") &&
|
|
160
|
+
validTextParts(value.content));
|
|
161
|
+
if (value.type === "function_call")
|
|
162
|
+
return (Object.keys(value).every((key) => ["type", "id", "name", "arguments"].includes(key)) &&
|
|
163
|
+
typeof value.id === "string" &&
|
|
164
|
+
value.name === "runtime_load_context" &&
|
|
165
|
+
isRecord(value.arguments) &&
|
|
166
|
+
Object.keys(value.arguments).length === 0);
|
|
167
|
+
if (value.type === "function_result")
|
|
168
|
+
return (Object.keys(value).every((key) => ["type", "call_id", "name", "result", "is_error"].includes(key)) &&
|
|
169
|
+
typeof value.call_id === "string" &&
|
|
170
|
+
typeof value.name === "string" &&
|
|
171
|
+
validTextParts(value.result) &&
|
|
172
|
+
(value.is_error === undefined || value.is_error === true));
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
function validTextParts(value) {
|
|
176
|
+
return (Array.isArray(value) &&
|
|
177
|
+
value.every((part) => isRecord(part) &&
|
|
178
|
+
Object.keys(part).length === 2 &&
|
|
179
|
+
part.type === "text" &&
|
|
180
|
+
typeof part.text === "string"));
|
|
181
|
+
}
|
|
182
|
+
function isRecord(value) {
|
|
183
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
184
|
+
}
|
|
@@ -121,6 +121,8 @@ export class ModelConnectionStore {
|
|
|
121
121
|
async listModels(input) {
|
|
122
122
|
await this.#mutationTail;
|
|
123
123
|
const provider = validateProvider(input.provider);
|
|
124
|
+
if (provider === "google_interactions")
|
|
125
|
+
throw new Error("Google Interactions model discovery is unverified; enter a model ID manually");
|
|
124
126
|
const baseUrl = normalizeBaseUrl(requiredTrimmed(input.baseUrl, "Base URL", 4096));
|
|
125
127
|
let apiKey = normalizeOptionalApiKey(input.apiKey);
|
|
126
128
|
if (apiKey === undefined && input.connectionId !== undefined) {
|
|
@@ -396,7 +398,8 @@ function toBinding(connection) {
|
|
|
396
398
|
function validateProvider(value) {
|
|
397
399
|
if (value !== "chat_completions" &&
|
|
398
400
|
value !== "openai_responses" &&
|
|
399
|
-
value !== "anthropic_messages"
|
|
401
|
+
value !== "anthropic_messages" &&
|
|
402
|
+
value !== "google_interactions")
|
|
400
403
|
throw new Error("Model-connection protocol is invalid");
|
|
401
404
|
return value;
|
|
402
405
|
}
|
|
@@ -1 +1,44 @@
|
|
|
1
|
-
|
|
1
|
+
/** Check known structural requirements without removing unknown fields. */
|
|
2
|
+
export function validGoogleInteractionsSteps(value) {
|
|
3
|
+
if (!Array.isArray(value))
|
|
4
|
+
return false;
|
|
5
|
+
const ids = new Set();
|
|
6
|
+
return value.every((step) => {
|
|
7
|
+
if (!isRecord(step) ||
|
|
8
|
+
typeof step.type !== "string" ||
|
|
9
|
+
step.type.length === 0)
|
|
10
|
+
return false;
|
|
11
|
+
if (step.type === "function_call") {
|
|
12
|
+
if (typeof step.id !== "string" ||
|
|
13
|
+
step.id.length === 0 ||
|
|
14
|
+
ids.has(step.id) ||
|
|
15
|
+
typeof step.name !== "string" ||
|
|
16
|
+
step.name.length === 0 ||
|
|
17
|
+
!isRecord(step.arguments))
|
|
18
|
+
return false;
|
|
19
|
+
ids.add(step.id);
|
|
20
|
+
}
|
|
21
|
+
if (step.type === "model_output" && !validContent(step.content))
|
|
22
|
+
return false;
|
|
23
|
+
if (step.type === "thought" &&
|
|
24
|
+
(typeof step.signature !== "string" ||
|
|
25
|
+
step.signature.length === 0 ||
|
|
26
|
+
(step.summary !== undefined && !validContent(step.summary))))
|
|
27
|
+
return false;
|
|
28
|
+
return true;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/** Empty terminal results are valid, but an assistant history item must replay steps. */
|
|
32
|
+
export function validGoogleInteractionsContinuation(value) {
|
|
33
|
+
return validGoogleInteractionsSteps(value) && value.length > 0;
|
|
34
|
+
}
|
|
35
|
+
function validContent(value) {
|
|
36
|
+
return (Array.isArray(value) &&
|
|
37
|
+
value.every((part) => isRecord(part) &&
|
|
38
|
+
typeof part.type === "string" &&
|
|
39
|
+
part.type.length > 0 &&
|
|
40
|
+
(part.type !== "text" || typeof part.text === "string")));
|
|
41
|
+
}
|
|
42
|
+
function isRecord(value) {
|
|
43
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
44
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
export async function* providerStreamEvents(body) {
|
|
1
|
+
export async function* providerStreamEvents(body, options) {
|
|
2
2
|
const reader = body.getReader();
|
|
3
|
-
const decoder = new TextDecoder(
|
|
3
|
+
const decoder = new TextDecoder("utf-8", {
|
|
4
|
+
fatal: options?.fatalUtf8 === true,
|
|
5
|
+
});
|
|
4
6
|
let pending = "";
|
|
5
7
|
let event = null;
|
|
6
8
|
let data = [];
|
|
@@ -8,10 +8,10 @@ const LAYOUT_MARKER = "play-advance-layout.json";
|
|
|
8
8
|
* The single in-progress play advance of a call chain, if any.
|
|
9
9
|
*
|
|
10
10
|
* It exists only between `begin` and `markSettled`. A marker that survives a
|
|
11
|
-
* restart means the advance
|
|
12
|
-
*
|
|
13
|
-
* never block a world from opening
|
|
14
|
-
* (see `migrateLayout`).
|
|
11
|
+
* restart means the advance needs recovery: a persisted ending is retained,
|
|
12
|
+
* and an unfinished request follows its protocol's retry boundary. Released
|
|
13
|
+
* records from earlier layouts never block a world from opening; the storage
|
|
14
|
+
* upgrade removes them (see `migrateLayout`).
|
|
15
15
|
*/
|
|
16
16
|
export class FileNativePlayAdvanceStore {
|
|
17
17
|
#worldsRoot;
|