@falai/agent 4.0.0-alpha.13 → 4.0.0-alpha.15
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/README.md +5 -3
- package/dist/cjs/core/Agent.js +9 -0
- package/dist/cjs/core/Agent.js.map +1 -1
- package/dist/cjs/core/CompactionEngine.d.ts.map +1 -1
- package/dist/cjs/core/CompactionEngine.js +8 -3
- package/dist/cjs/core/CompactionEngine.js.map +1 -1
- package/dist/cjs/core/FlowSpec.d.ts +19 -2
- package/dist/cjs/core/FlowSpec.d.ts.map +1 -1
- package/dist/cjs/core/FlowSpec.js +256 -57
- package/dist/cjs/core/FlowSpec.js.map +1 -1
- package/dist/cjs/core/Prompt.d.ts +16 -0
- package/dist/cjs/core/Prompt.d.ts.map +1 -1
- package/dist/cjs/core/Prompt.js +39 -0
- package/dist/cjs/core/Prompt.js.map +1 -1
- package/dist/cjs/core/Runner.d.ts +2 -0
- package/dist/cjs/core/Runner.d.ts.map +1 -1
- package/dist/cjs/core/Runner.js +26 -15
- package/dist/cjs/core/Runner.js.map +1 -1
- package/dist/cjs/core/Speak.d.ts.map +1 -1
- package/dist/cjs/core/Speak.js +22 -16
- package/dist/cjs/core/Speak.js.map +1 -1
- package/dist/cjs/core/Understand.d.ts +4 -3
- package/dist/cjs/core/Understand.d.ts.map +1 -1
- package/dist/cjs/core/Understand.js +5 -38
- package/dist/cjs/core/Understand.js.map +1 -1
- package/dist/cjs/core/contracts.d.ts +5 -5
- package/dist/cjs/core/contracts.d.ts.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/persistence/OpenSearchStore.d.ts +2 -1
- package/dist/cjs/persistence/OpenSearchStore.d.ts.map +1 -1
- package/dist/cjs/persistence/OpenSearchStore.js +2 -2
- package/dist/cjs/persistence/OpenSearchStore.js.map +1 -1
- package/dist/cjs/persistence/RedisStore.d.ts +1 -1
- package/dist/cjs/persistence/RedisStore.d.ts.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.d.ts +2 -5
- package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.js +3 -4
- package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
- package/dist/cjs/providers/DeepSeekProvider.d.ts.map +1 -1
- package/dist/cjs/providers/DeepSeekProvider.js +3 -4
- package/dist/cjs/providers/DeepSeekProvider.js.map +1 -1
- package/dist/cjs/providers/FallbackAiProvider.js +1 -1
- package/dist/cjs/providers/FallbackAiProvider.js.map +1 -1
- package/dist/cjs/providers/GeminiProvider.d.ts +1 -2
- package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/cjs/providers/GeminiProvider.js +3 -4
- package/dist/cjs/providers/GeminiProvider.js.map +1 -1
- package/dist/cjs/providers/GenericOpenAICompatibleProvider.js +4 -4
- package/dist/cjs/providers/GenericOpenAICompatibleProvider.js.map +1 -1
- package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenAIProvider.js +3 -4
- package/dist/cjs/providers/OpenAIProvider.js.map +1 -1
- package/dist/cjs/providers/OpenRouterProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenRouterProvider.js +4 -3
- package/dist/cjs/providers/OpenRouterProvider.js.map +1 -1
- package/dist/cjs/providers/ProviderAdapter.d.ts +12 -9
- package/dist/cjs/providers/ProviderAdapter.d.ts.map +1 -1
- package/dist/cjs/providers/ProviderAdapter.js +8 -3
- package/dist/cjs/providers/ProviderAdapter.js.map +1 -1
- package/dist/cjs/providers/ZaiProvider.js +1 -1
- package/dist/cjs/providers/ZaiProvider.js.map +1 -1
- package/dist/cjs/types/agent.d.ts +1 -1
- package/dist/cjs/types/agent.d.ts.map +1 -1
- package/dist/cjs/types/ai.d.ts +4 -6
- package/dist/cjs/types/ai.d.ts.map +1 -1
- package/dist/cjs/types/compaction.d.ts +1 -0
- package/dist/cjs/types/compaction.d.ts.map +1 -1
- package/dist/cjs/types/errors.d.ts +4 -9
- package/dist/cjs/types/errors.d.ts.map +1 -1
- package/dist/cjs/types/errors.js +12 -12
- package/dist/cjs/types/errors.js.map +1 -1
- package/dist/cjs/types/history.d.ts +0 -7
- package/dist/cjs/types/history.d.ts.map +1 -1
- package/dist/cjs/types/index.d.ts +1 -1
- package/dist/cjs/types/index.d.ts.map +1 -1
- package/dist/cjs/types/index.js.map +1 -1
- package/dist/cjs/types/session.d.ts +1 -1
- package/dist/cjs/types/session.d.ts.map +1 -1
- package/dist/cjs/utils/clock.js +1 -1
- package/dist/cjs/utils/clock.js.map +1 -1
- package/dist/cjs/utils/schema.d.ts +2 -11
- package/dist/cjs/utils/schema.d.ts.map +1 -1
- package/dist/cjs/utils/schema.js +2 -43
- package/dist/cjs/utils/schema.js.map +1 -1
- package/dist/core/Agent.js +10 -1
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/CompactionEngine.d.ts.map +1 -1
- package/dist/core/CompactionEngine.js +8 -3
- package/dist/core/CompactionEngine.js.map +1 -1
- package/dist/core/FlowSpec.d.ts +19 -2
- package/dist/core/FlowSpec.d.ts.map +1 -1
- package/dist/core/FlowSpec.js +254 -57
- package/dist/core/FlowSpec.js.map +1 -1
- package/dist/core/Prompt.d.ts +16 -0
- package/dist/core/Prompt.d.ts.map +1 -1
- package/dist/core/Prompt.js +37 -0
- package/dist/core/Prompt.js.map +1 -1
- package/dist/core/Runner.d.ts +2 -0
- package/dist/core/Runner.d.ts.map +1 -1
- package/dist/core/Runner.js +27 -16
- package/dist/core/Runner.js.map +1 -1
- package/dist/core/Speak.d.ts.map +1 -1
- package/dist/core/Speak.js +23 -17
- package/dist/core/Speak.js.map +1 -1
- package/dist/core/Understand.d.ts +4 -3
- package/dist/core/Understand.d.ts.map +1 -1
- package/dist/core/Understand.js +5 -38
- package/dist/core/Understand.js.map +1 -1
- package/dist/core/contracts.d.ts +5 -5
- package/dist/core/contracts.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/persistence/OpenSearchStore.d.ts +2 -1
- package/dist/persistence/OpenSearchStore.d.ts.map +1 -1
- package/dist/persistence/OpenSearchStore.js +2 -2
- package/dist/persistence/OpenSearchStore.js.map +1 -1
- package/dist/persistence/RedisStore.d.ts +1 -1
- package/dist/persistence/RedisStore.d.ts.map +1 -1
- package/dist/providers/AnthropicProvider.d.ts +2 -5
- package/dist/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/providers/AnthropicProvider.js +3 -4
- package/dist/providers/AnthropicProvider.js.map +1 -1
- package/dist/providers/DeepSeekProvider.d.ts.map +1 -1
- package/dist/providers/DeepSeekProvider.js +3 -4
- package/dist/providers/DeepSeekProvider.js.map +1 -1
- package/dist/providers/FallbackAiProvider.js +1 -1
- package/dist/providers/FallbackAiProvider.js.map +1 -1
- package/dist/providers/GeminiProvider.d.ts +1 -2
- package/dist/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/providers/GeminiProvider.js +3 -4
- package/dist/providers/GeminiProvider.js.map +1 -1
- package/dist/providers/GenericOpenAICompatibleProvider.js +4 -4
- package/dist/providers/GenericOpenAICompatibleProvider.js.map +1 -1
- package/dist/providers/OpenAIProvider.d.ts.map +1 -1
- package/dist/providers/OpenAIProvider.js +3 -4
- package/dist/providers/OpenAIProvider.js.map +1 -1
- package/dist/providers/OpenRouterProvider.d.ts.map +1 -1
- package/dist/providers/OpenRouterProvider.js +4 -3
- package/dist/providers/OpenRouterProvider.js.map +1 -1
- package/dist/providers/ProviderAdapter.d.ts +12 -9
- package/dist/providers/ProviderAdapter.d.ts.map +1 -1
- package/dist/providers/ProviderAdapter.js +8 -3
- package/dist/providers/ProviderAdapter.js.map +1 -1
- package/dist/providers/ZaiProvider.js +1 -1
- package/dist/providers/ZaiProvider.js.map +1 -1
- package/dist/types/agent.d.ts +1 -1
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/ai.d.ts +4 -6
- package/dist/types/ai.d.ts.map +1 -1
- package/dist/types/compaction.d.ts +1 -0
- package/dist/types/compaction.d.ts.map +1 -1
- package/dist/types/errors.d.ts +4 -9
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/errors.js +12 -12
- package/dist/types/errors.js.map +1 -1
- package/dist/types/history.d.ts +0 -7
- package/dist/types/history.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/session.d.ts +1 -1
- package/dist/types/session.d.ts.map +1 -1
- package/dist/utils/clock.js +1 -1
- package/dist/utils/clock.js.map +1 -1
- package/dist/utils/schema.d.ts +2 -11
- package/dist/utils/schema.d.ts.map +1 -1
- package/dist/utils/schema.js +2 -42
- package/dist/utils/schema.js.map +1 -1
- package/docs/concepts/pipeline.md +2 -2
- package/docs/concepts/runs-and-waits.md +1 -1
- package/docs/guides/actions-and-events.md +1 -1
- package/docs/guides/branching.md +1 -1
- package/docs/guides/compaction.md +2 -2
- package/docs/guides/error-handling.md +1 -1
- package/docs/guides/flow-control.md +1 -1
- package/docs/guides/persistence.md +1 -1
- package/docs/migration/v3-to-v4.md +2 -2
- package/docs/reference/agent.md +1 -1
- package/docs/reference/errors.md +10 -7
- package/docs/reference/flow-spec.md +29 -5
- package/docs/reference/outcomes.md +1 -1
- package/docs/reference/providers.md +2 -0
- package/docs/reference/step.md +1 -1
- package/docs/reference/stores.md +5 -3
- package/docs/start/01-install.md +5 -3
- package/package.json +2 -2
- package/src/core/Agent.ts +12 -1
- package/src/core/CompactionEngine.ts +10 -3
- package/src/core/FlowSpec.ts +263 -65
- package/src/core/Prompt.ts +40 -0
- package/src/core/Runner.ts +27 -14
- package/src/core/Speak.ts +23 -16
- package/src/core/Understand.ts +5 -39
- package/src/core/contracts.ts +5 -3
- package/src/index.ts +0 -1
- package/src/persistence/OpenSearchStore.ts +3 -2
- package/src/persistence/RedisStore.ts +1 -1
- package/src/providers/AnthropicProvider.ts +4 -9
- package/src/providers/DeepSeekProvider.ts +2 -4
- package/src/providers/FallbackAiProvider.ts +1 -1
- package/src/providers/GeminiProvider.ts +3 -6
- package/src/providers/GenericOpenAICompatibleProvider.ts +4 -4
- package/src/providers/OpenAIProvider.ts +3 -4
- package/src/providers/OpenRouterProvider.ts +4 -2
- package/src/providers/ProviderAdapter.ts +15 -12
- package/src/providers/ZaiProvider.ts +1 -1
- package/src/types/agent.ts +1 -1
- package/src/types/ai.ts +4 -6
- package/src/types/compaction.ts +1 -0
- package/src/types/errors.ts +11 -12
- package/src/types/history.ts +0 -10
- package/src/types/index.ts +0 -1
- package/src/types/session.ts +1 -1
- package/src/utils/clock.ts +1 -1
- package/src/utils/schema.ts +2 -48
- package/dist/cjs/providers/index.d.ts +0 -26
- package/dist/cjs/providers/index.d.ts.map +0 -1
- package/dist/cjs/providers/index.js +0 -30
- package/dist/cjs/providers/index.js.map +0 -1
- package/dist/cjs/utils/clone.d.ts +0 -8
- package/dist/cjs/utils/clone.d.ts.map +0 -1
- package/dist/cjs/utils/clone.js +0 -32
- package/dist/cjs/utils/clone.js.map +0 -1
- package/dist/cjs/utils/index.d.ts +0 -9
- package/dist/cjs/utils/index.d.ts.map +0 -1
- package/dist/cjs/utils/index.js +0 -29
- package/dist/cjs/utils/index.js.map +0 -1
- package/dist/providers/index.d.ts +0 -26
- package/dist/providers/index.d.ts.map +0 -1
- package/dist/providers/index.js +0 -17
- package/dist/providers/index.js.map +0 -1
- package/dist/utils/clone.d.ts +0 -8
- package/dist/utils/clone.d.ts.map +0 -1
- package/dist/utils/clone.js +0 -29
- package/dist/utils/clone.js.map +0 -1
- package/dist/utils/index.d.ts +0 -9
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -9
- package/dist/utils/index.js.map +0 -1
- package/src/providers/index.ts +0 -38
- package/src/utils/clone.ts +0 -34
- package/src/utils/index.ts +0 -18
package/src/core/Speak.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { render, type TemplateScope } from "../utils/template.js";
|
|
|
32
32
|
import { addUsage, readUsage } from "../utils/usage.js";
|
|
33
33
|
import type { Deferral, SpeakOutcome, SpeakRequest, SpeakStreamChunk } from "./contracts.js";
|
|
34
34
|
import {
|
|
35
|
+
Aliases,
|
|
35
36
|
describeField,
|
|
36
37
|
factsSection,
|
|
37
38
|
instructionsSection,
|
|
@@ -90,8 +91,6 @@ function deferralOf(error: unknown): Deferral {
|
|
|
90
91
|
...(reset !== undefined ? { resetAtMs: reset } : {}),
|
|
91
92
|
};
|
|
92
93
|
}
|
|
93
|
-
/** Gemini rejects any other envelope property name. */
|
|
94
|
-
const WIRE_NAME = /^[a-zA-Z0-9_-]+$/;
|
|
95
94
|
|
|
96
95
|
const GUIDELINE_HEADING = "## Guideline for your reply (adapt to the conversation)";
|
|
97
96
|
const DEFAULT_GUIDELINE =
|
|
@@ -104,6 +103,9 @@ const FINAL_SECTION =
|
|
|
104
103
|
"## Wrap up\nAnswer the customer now, using the tool results in the conversation. Do not call any tools.";
|
|
105
104
|
const SPEAK_FIRST =
|
|
106
105
|
"## Situation\nThere is no new message from the customer. You speak first: open naturally, do not answer a question nobody asked.";
|
|
106
|
+
/** The retry wake after a provider failure, when the customer's last message is still unanswered. */
|
|
107
|
+
const ANSWER_PENDING =
|
|
108
|
+
"## Situation\nThe customer's last message, in the conversation above, has no answer yet. Answer it now.";
|
|
107
109
|
|
|
108
110
|
interface ToolCall {
|
|
109
111
|
toolName: string;
|
|
@@ -165,7 +167,6 @@ export class Speak<C = unknown, D = unknown> {
|
|
|
165
167
|
let history: History = req.history;
|
|
166
168
|
const fields: Record<string, unknown> = {};
|
|
167
169
|
const data: Record<string, unknown> = {};
|
|
168
|
-
const toolCalls: ToolCall[] = [];
|
|
169
170
|
let llmCalls = 0;
|
|
170
171
|
let usage: TokenUsage | undefined;
|
|
171
172
|
let message = "";
|
|
@@ -220,14 +221,13 @@ export class Speak<C = unknown, D = unknown> {
|
|
|
220
221
|
}
|
|
221
222
|
history = [...history, ...executed.items];
|
|
222
223
|
Object.assign(data, executed.data);
|
|
223
|
-
toolCalls.push(...read.toolCalls);
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
if (!message.trim()) {
|
|
227
227
|
logger.warn(`[Speak] the model returned no message after ${llmCalls} call(s); deferring.`);
|
|
228
228
|
return { deferred: UNAVAILABLE, llmCalls, ...(usage ? { usage } : {}) };
|
|
229
229
|
}
|
|
230
|
-
return { spoken: { message, fields, data,
|
|
230
|
+
return { spoken: { message, fields, data, llmCalls, ...(usage ? { usage } : {}) } };
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
/** One provider call. Streaming yields clean message deltas; both paths return the same shape. */
|
|
@@ -331,7 +331,7 @@ function buildPrompt<C, D>(
|
|
|
331
331
|
"idle" in talk
|
|
332
332
|
? [guideline(talk.idle.prompt)]
|
|
333
333
|
: [
|
|
334
|
-
`## Flow\n${talk.flow.name}${talk.flow.description ? `: ${talk.flow.description}` : ""}`,
|
|
334
|
+
`## Flow\n${talk.flow.name}${talk.flow.description ? `: ${render(talk.flow.description, scope)}` : ""}`,
|
|
335
335
|
guideline(talk.step.prompt ?? (talk.pending.length ? DEFAULT_GUIDELINE : ANSWER_GUIDELINE)),
|
|
336
336
|
pendingSection(talk.pending, options.fields, talk.step.ask ?? {}, scope),
|
|
337
337
|
// `Partial<D>` is a mapped type; the guard is how it reaches an index-signature parameter without a cast.
|
|
@@ -344,17 +344,26 @@ function buildPrompt<C, D>(
|
|
|
344
344
|
inline,
|
|
345
345
|
...body,
|
|
346
346
|
instructionsSection([{ caption: "[Always]", items: req.instructions }], scope),
|
|
347
|
-
inputSection(req.input),
|
|
347
|
+
inputSection(req.input, req.history),
|
|
348
348
|
formatSection(envelope, options.fields),
|
|
349
349
|
),
|
|
350
350
|
};
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
-
/**
|
|
354
|
-
|
|
353
|
+
/**
|
|
354
|
+
* The customer's latest text, quoted. Without one, on anything but a message,
|
|
355
|
+
* the assistant opens the exchange. The one exception is the retry of a step
|
|
356
|
+
* the provider failed, while the history still ends with the customer's
|
|
357
|
+
* message: that message gets its answer. A history that merely ends with a
|
|
358
|
+
* customer message is not enough, because a host may skip a message on
|
|
359
|
+
* purpose (an away message, a filtered first contact) and a follow-up must
|
|
360
|
+
* not answer it.
|
|
361
|
+
*/
|
|
362
|
+
function inputSection(input: SpeakRequest["input"], history: History): string | null {
|
|
355
363
|
const text = input.text?.trim();
|
|
356
364
|
if (text) return `## Customer's latest message\n"${text}"`;
|
|
357
|
-
|
|
365
|
+
if (input.kind === "message") return null;
|
|
366
|
+
return input.retry && history.at(-1)?.role === "user" ? ANSWER_PENDING : SPEAK_FIRST;
|
|
358
367
|
}
|
|
359
368
|
|
|
360
369
|
/** The envelope restated in words, for providers that follow the schema by prompt only. */
|
|
@@ -385,16 +394,14 @@ function formatSection(envelope: Envelope, fields: FieldDefs): string {
|
|
|
385
394
|
// ── Envelope ────────────────────────────────────────────────────────────
|
|
386
395
|
|
|
387
396
|
function buildEnvelope(fields: FieldDefs, pending: string[]): Envelope {
|
|
397
|
+
const aliases = new Aliases(["message"]);
|
|
388
398
|
const wire = new Map<string, string>();
|
|
389
399
|
const defs: FieldDefs = { message: { type: "string" } };
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
// rides under an alias; the table maps it back. Ceiling: a real slug named
|
|
393
|
-
// `field_N` could collide with an alias. Upgrade: bump the alias until free.
|
|
394
|
-
const name = WIRE_NAME.test(slug) && slug !== "message" ? slug : `field_${i}`;
|
|
400
|
+
for (const slug of pending) {
|
|
401
|
+
const name = aliases.of(slug, "field_");
|
|
395
402
|
wire.set(slug, name);
|
|
396
403
|
defs[name] = fields[slug] ?? { type: "string" };
|
|
397
|
-
}
|
|
404
|
+
}
|
|
398
405
|
return { schema: toWireSchema(defs, { nullable: true }), wire };
|
|
399
406
|
}
|
|
400
407
|
|
package/src/core/Understand.ts
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* hands back raw values keyed by their real ids. It never writes session
|
|
7
7
|
* data: Runner validates, coerces and applies.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Zero calls when there is nothing to judge: no candidate flow, or only the
|
|
10
|
+
* floor's own, and no mention, branch or field. A lone eligible message flow
|
|
11
|
+
* starts unscored only when no catch-all passes and `idle` is `'silent'`;
|
|
12
|
+
* Runner decides that and sends no request. Otherwise this call scores it.
|
|
12
13
|
*
|
|
13
14
|
* The envelope is built for two kinds of provider at once. Gemini enforces
|
|
14
15
|
* the schema, so every section is a closed object with every property
|
|
@@ -28,15 +29,12 @@ import { coerceField, isKnown, pendingFields, toWireSchema } from "../utils/sche
|
|
|
28
29
|
import { render, type TemplateScope } from "../utils/template.js";
|
|
29
30
|
import { readUsage } from "../utils/usage.js";
|
|
30
31
|
import type { UnderstandRequest, Understanding } from "./contracts.js";
|
|
31
|
-
import { describeField, factsSection, joinSections, stablePrefix } from "./Prompt.js";
|
|
32
|
+
import { Aliases, describeField, factsSection, joinSections, stablePrefix } from "./Prompt.js";
|
|
32
33
|
|
|
33
34
|
export const UNDERSTAND_SCHEMA_NAME = "understand";
|
|
34
35
|
|
|
35
36
|
const SECTIONS = ["flows", "mentions", "extract", "branches", "fields"] as const;
|
|
36
37
|
|
|
37
|
-
/** Gemini rejects any other character in a property name. */
|
|
38
|
-
const SAFE_KEY = /^[a-zA-Z0-9_-]+$/;
|
|
39
|
-
|
|
40
38
|
export class Understand<C = unknown, D = unknown> {
|
|
41
39
|
constructor(private readonly options: AgentOptions<C, D>) {}
|
|
42
40
|
|
|
@@ -111,38 +109,6 @@ function candidateFlows<C, D>(req: UnderstandRequest<C, D>): Flow<C, D>[] {
|
|
|
111
109
|
return [...seen.values()];
|
|
112
110
|
}
|
|
113
111
|
|
|
114
|
-
/**
|
|
115
|
-
* Envelope property names for ids the schema cannot carry. A safe id keeps
|
|
116
|
-
* its own name; anything else (run ids carry `#`, `:` and `/`) gets a short
|
|
117
|
-
* alias, mapped back when the reply is parsed.
|
|
118
|
-
*/
|
|
119
|
-
class Aliases {
|
|
120
|
-
private readonly byReal = new Map<string, string>();
|
|
121
|
-
private readonly byAlias = new Map<string, string>();
|
|
122
|
-
private n = 0;
|
|
123
|
-
|
|
124
|
-
of(real: string, prefix = "k"): string {
|
|
125
|
-
const seen = this.byReal.get(real);
|
|
126
|
-
if (seen) return seen;
|
|
127
|
-
const alias = SAFE_KEY.test(real) && !this.byAlias.has(real) ? real : this.fresh(prefix);
|
|
128
|
-
this.byAlias.set(alias, real);
|
|
129
|
-
this.byReal.set(real, alias);
|
|
130
|
-
return alias;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** Unknown aliases come back as they are; Runner drops keys it does not know. */
|
|
134
|
-
real(alias: string): string {
|
|
135
|
-
return this.byAlias.get(alias) ?? alias;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
private fresh(prefix: string): string {
|
|
139
|
-
let alias: string;
|
|
140
|
-
do alias = `${prefix}${++this.n}`;
|
|
141
|
-
while (this.byAlias.has(alias));
|
|
142
|
-
return alias;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
112
|
function branchKey(branch: { runId: string; stepId: string; index: number }): string {
|
|
147
113
|
return `${branch.runId}/${branch.stepId}/${branch.index}`;
|
|
148
114
|
}
|
package/src/core/contracts.ts
CHANGED
|
@@ -70,8 +70,11 @@ export interface IdleRequest<C = unknown, D = unknown> {
|
|
|
70
70
|
|
|
71
71
|
export interface SpeakRequest<C = unknown, D = unknown> {
|
|
72
72
|
talk: TalkRequest<C, D> | IdleRequest<C, D>;
|
|
73
|
-
/**
|
|
74
|
-
|
|
73
|
+
/**
|
|
74
|
+
* What started this turn. A wake has no text: the assistant speaks first, unless `retry` says this
|
|
75
|
+
* talk step failed at the provider and is running again, with the customer's message still unanswered.
|
|
76
|
+
*/
|
|
77
|
+
input: { kind: InputKind; text?: string; retry?: boolean };
|
|
75
78
|
context: C;
|
|
76
79
|
data: Partial<D>;
|
|
77
80
|
history: History;
|
|
@@ -87,7 +90,6 @@ export interface Spoken {
|
|
|
87
90
|
fields: Record<string, unknown>;
|
|
88
91
|
/** Data patches returned by tools, merged in call order. */
|
|
89
92
|
data: Record<string, unknown>;
|
|
90
|
-
toolCalls: Array<{ toolName: string; arguments: Record<string, unknown> }>;
|
|
91
93
|
llmCalls: number;
|
|
92
94
|
usage?: TokenUsage;
|
|
93
95
|
}
|
package/src/index.ts
CHANGED
|
@@ -39,6 +39,7 @@ export interface OpenSearchClient {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export interface OpenSearchStoreOptions {
|
|
42
|
+
client: OpenSearchClient;
|
|
42
43
|
/** Index name. Default `agent_sessions`. */
|
|
43
44
|
indices?: { sessions?: string };
|
|
44
45
|
/** Create the index with its mappings on `initialize()`. Default true. */
|
|
@@ -58,8 +59,8 @@ export class OpenSearchStore<D = unknown> implements Store<D> {
|
|
|
58
59
|
private readonly autoCreateIndices: boolean;
|
|
59
60
|
private readonly refresh: Refresh;
|
|
60
61
|
|
|
61
|
-
constructor(
|
|
62
|
-
this.client = client;
|
|
62
|
+
constructor(options: OpenSearchStoreOptions) {
|
|
63
|
+
this.client = options.client;
|
|
63
64
|
this.index = options.indices?.sessions ?? "agent_sessions";
|
|
64
65
|
this.autoCreateIndices = options.autoCreateIndices ?? true;
|
|
65
66
|
this.refresh = options.refresh ?? false;
|
|
@@ -23,7 +23,7 @@ export interface RedisStoreOptions {
|
|
|
23
23
|
redis: RedisClient;
|
|
24
24
|
/** Prefix of every key. Default `agent:`. */
|
|
25
25
|
keyPrefix?: string;
|
|
26
|
-
/** Seconds a session lives after its last save; `0` keeps it forever. Default 7 days. */
|
|
26
|
+
/** Seconds a session lives after its last save; `0` keeps it forever. Default 7 days. Keep it above your longest wait: an expired session loses its parked runs and claims. */
|
|
27
27
|
sessionTTL?: number;
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -30,11 +30,8 @@ export interface AnthropicProviderOptions {
|
|
|
30
30
|
/** Idle-stream deadline and retry budget */
|
|
31
31
|
retryConfig?: { timeout?: number; retries?: number };
|
|
32
32
|
/**
|
|
33
|
-
* Replacement `fetch`, for tests that script the wire.
|
|
34
|
-
*
|
|
35
|
-
* v3 note: this replaces the injected SDK client. There is no SDK now, so a
|
|
36
|
-
* test drives the same bytes the provider really receives rather than an
|
|
37
|
-
* SDK's idea of them.
|
|
33
|
+
* Replacement `fetch`, for tests that script the wire. There is no vendor
|
|
34
|
+
* SDK, so a test drives the same bytes the provider really receives.
|
|
38
35
|
*/
|
|
39
36
|
fetchImpl?: typeof fetch;
|
|
40
37
|
}
|
|
@@ -50,10 +47,8 @@ export class AnthropicProvider extends ProviderAdapter {
|
|
|
50
47
|
};
|
|
51
48
|
|
|
52
49
|
constructor(options: AnthropicProviderOptions) {
|
|
53
|
-
if (!options.apiKey) throw new Error(
|
|
54
|
-
if (!options.model) {
|
|
55
|
-
throw new Error("Model is required. Example: 'claude-sonnet-5' or 'claude-opus-5'");
|
|
56
|
-
}
|
|
50
|
+
if (!options.apiKey) throw new Error(`[AnthropicProvider] apiKey is empty: the provider cannot authenticate. Pass { apiKey: process.env.ANTHROPIC_API_KEY } and check the variable is set.`);
|
|
51
|
+
if (!options.model) throw new Error(`[AnthropicProvider] model is empty: there is no default. Pass one, e.g. { model: "claude-sonnet-5" }.`);
|
|
57
52
|
|
|
58
53
|
super({
|
|
59
54
|
provider: createAnthropicProvider({
|
|
@@ -51,10 +51,8 @@ export class DeepSeekProvider extends OpenAICompatibleProvider {
|
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
constructor(options: DeepSeekProviderOptions) {
|
|
54
|
-
if (!options.apiKey) throw new Error(
|
|
55
|
-
if (!options.model) {
|
|
56
|
-
throw new Error("Model is required. Example: 'deepseek-chat' or 'deepseek-reasoner'");
|
|
57
|
-
}
|
|
54
|
+
if (!options.apiKey) throw new Error(`[DeepSeekProvider] apiKey is empty: the provider cannot authenticate. Pass { apiKey: process.env.DEEPSEEK_API_KEY } and check the variable is set.`);
|
|
55
|
+
if (!options.model) throw new Error(`[DeepSeekProvider] model is empty: there is no default. Pass one, e.g. { model: "deepseek-chat" }.`);
|
|
58
56
|
|
|
59
57
|
super({
|
|
60
58
|
id: "deepseek",
|
|
@@ -38,7 +38,7 @@ export class FallbackAiProvider implements AiProvider {
|
|
|
38
38
|
|
|
39
39
|
constructor(options: FallbackAiProviderOptions) {
|
|
40
40
|
if (!options.providers || options.providers.length === 0) {
|
|
41
|
-
throw new Error("FallbackAiProvider
|
|
41
|
+
throw new Error("[FallbackAiProvider] providers is empty: there is nothing to call. Pass at least one, e.g. { providers: [primary, backup] }.");
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
this.name = `fallback(${options.providers.map((p) => p.name).join("->")})`;
|
|
@@ -40,8 +40,7 @@ export interface GeminiProviderOptions {
|
|
|
40
40
|
config?: RequestConfig;
|
|
41
41
|
/** Idle-stream deadline and retry budget */
|
|
42
42
|
retryConfig?: { timeout?: number; retries?: number };
|
|
43
|
-
/** Replacement `fetch`, for tests that script the wire.
|
|
44
|
-
* injected SDK client — see {@link AnthropicProviderOptions.fetchImpl}. */
|
|
43
|
+
/** Replacement `fetch`, for tests that script the wire — see {@link AnthropicProviderOptions.fetchImpl}. */
|
|
45
44
|
fetchImpl?: typeof fetch;
|
|
46
45
|
}
|
|
47
46
|
|
|
@@ -56,10 +55,8 @@ export class GeminiProvider extends ProviderAdapter {
|
|
|
56
55
|
};
|
|
57
56
|
|
|
58
57
|
constructor(options: GeminiProviderOptions) {
|
|
59
|
-
if (!options.apiKey) throw new Error(
|
|
60
|
-
if (!options.model) {
|
|
61
|
-
throw new Error("Model is required. Example: 'gemini-3.1-pro-preview'");
|
|
62
|
-
}
|
|
58
|
+
if (!options.apiKey) throw new Error(`[GeminiProvider] apiKey is empty: the provider cannot authenticate. Pass { apiKey: process.env.GEMINI_API_KEY } and check the variable is set.`);
|
|
59
|
+
if (!options.model) throw new Error(`[GeminiProvider] model is empty: there is no default. Pass one, e.g. { model: "gemini-2.5-flash" }.`);
|
|
63
60
|
|
|
64
61
|
super({
|
|
65
62
|
provider: createGeminiProvider({
|
|
@@ -97,18 +97,18 @@ class GenericOpenAICompatibleProvider extends OpenAICompatibleProvider {
|
|
|
97
97
|
|
|
98
98
|
constructor(options: OpenAICompatibleOptions) {
|
|
99
99
|
if (!options.name) {
|
|
100
|
-
throw new Error(
|
|
100
|
+
throw new Error('[OpenAICompatibleProvider] name is empty: it labels the provider in logs and errors. Pass one, e.g. { name: "ollama" }.');
|
|
101
101
|
}
|
|
102
102
|
if (!options.baseURL) {
|
|
103
|
-
throw new Error(`[${options.name}]
|
|
103
|
+
throw new Error(`[${options.name}] baseURL is empty: there is no server to call. Pass one, e.g. { baseURL: "http://localhost:11434/v1" }.`);
|
|
104
104
|
}
|
|
105
105
|
if (!options.apiKey) {
|
|
106
106
|
throw new Error(
|
|
107
|
-
`[${options.name}]
|
|
107
|
+
`[${options.name}] apiKey is empty: the provider cannot authenticate. Pass your key, or any non-empty string for a server that ignores it.`
|
|
108
108
|
);
|
|
109
109
|
}
|
|
110
110
|
if (!options.model) {
|
|
111
|
-
throw new Error(`[${options.name}]
|
|
111
|
+
throw new Error(`[${options.name}] model is empty: there is no default. Pass the id your server serves, e.g. { model: "llama3.3" }.`);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
super({
|
|
@@ -39,14 +39,13 @@ export class OpenAIProvider extends OpenAICompatibleProvider {
|
|
|
39
39
|
supportsNativeJsonSchema: true,
|
|
40
40
|
supportsStreaming: true,
|
|
41
41
|
supportsStreamingToolCalls: true,
|
|
42
|
-
//
|
|
43
|
-
// count, which the old adapter never read.
|
|
42
|
+
// The shape auto-caches repeated prefixes and reports the hit count.
|
|
44
43
|
supportsPromptCaching: true,
|
|
45
44
|
};
|
|
46
45
|
|
|
47
46
|
constructor(options: OpenAIProviderOptions) {
|
|
48
|
-
if (!options.apiKey) throw new Error(
|
|
49
|
-
if (!options.model) throw new Error(
|
|
47
|
+
if (!options.apiKey) throw new Error(`[OpenAIProvider] apiKey is empty: the provider cannot authenticate. Pass { apiKey: process.env.OPENAI_API_KEY } and check the variable is set.`);
|
|
48
|
+
if (!options.model) throw new Error(`[OpenAIProvider] model is empty: there is no default. Pass one, e.g. { model: "gpt-5.6" }.`);
|
|
50
49
|
|
|
51
50
|
super({
|
|
52
51
|
id: "openai",
|
|
@@ -59,8 +59,10 @@ export class OpenRouterProvider extends OpenAICompatibleProvider {
|
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
constructor(options: OpenRouterProviderOptions) {
|
|
62
|
-
if (!options.apiKey) throw new Error(
|
|
63
|
-
if (!options.model)
|
|
62
|
+
if (!options.apiKey) throw new Error(`[OpenRouterProvider] apiKey is empty: the provider cannot authenticate. Pass { apiKey: process.env.OPENROUTER_API_KEY } and check the variable is set.`);
|
|
63
|
+
if (!options.model) {
|
|
64
|
+
throw new Error(`[OpenRouterProvider] model is empty: there is no default. Pass one, e.g. { model: "anthropic/claude-sonnet-5" }; the ids are listed at https://openrouter.ai/models.`);
|
|
65
|
+
}
|
|
64
66
|
|
|
65
67
|
super({
|
|
66
68
|
id: "openrouter",
|
|
@@ -61,11 +61,9 @@ export interface RetryConfig {
|
|
|
61
61
|
/**
|
|
62
62
|
* How long a stream may stay SILENT before it is considered wedged.
|
|
63
63
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* request that never gets a reply still fails at the same moment; one that is
|
|
68
|
-
* steadily producing tokens is left alone.
|
|
64
|
+
* It bounds silence, not the whole call: time to the first byte, and the gap
|
|
65
|
+
* between any two after it. A request that never gets a reply fails at that
|
|
66
|
+
* moment; one that is steadily producing tokens is left alone, however long.
|
|
69
67
|
*/
|
|
70
68
|
timeout: number;
|
|
71
69
|
/** Retries AFTER the first attempt, so `0` still performs one call. */
|
|
@@ -90,10 +88,8 @@ export function resolveRetryConfig(input?: { timeout?: number; retries?: number
|
|
|
90
88
|
/**
|
|
91
89
|
* Request defaults sent with every call.
|
|
92
90
|
*
|
|
93
|
-
* Only the fields every supported shape has
|
|
94
|
-
*
|
|
95
|
-
* up in the dependency tree of consumers who used a different vendor, and how
|
|
96
|
-
* a field set on one provider silently vanished on another.
|
|
91
|
+
* Only the fields every supported shape has, so a field set on one provider
|
|
92
|
+
* means the same on another and no vendor's package enters the dependency tree.
|
|
97
93
|
*/
|
|
98
94
|
export interface RequestConfig {
|
|
99
95
|
temperature?: number;
|
|
@@ -290,6 +286,8 @@ export abstract class ProviderAdapter implements AiProvider {
|
|
|
290
286
|
public abstract readonly capabilities: ProviderCapabilities;
|
|
291
287
|
|
|
292
288
|
protected readonly provider: Provider;
|
|
289
|
+
/** `provider` without the fallback chain: the one model this adapter was built for. */
|
|
290
|
+
private readonly primary: Provider;
|
|
293
291
|
protected readonly primaryModel: string;
|
|
294
292
|
protected readonly backupModels: string[];
|
|
295
293
|
protected readonly retryConfig: RetryConfig;
|
|
@@ -304,6 +302,7 @@ export abstract class ProviderAdapter implements AiProvider {
|
|
|
304
302
|
this.primaryModel = init.model;
|
|
305
303
|
this.backupModels = init.backupModels ?? [];
|
|
306
304
|
this.retryConfig = resolveRetryConfig(init.retryConfig);
|
|
305
|
+
this.primary = init.provider;
|
|
307
306
|
|
|
308
307
|
if (init.fallbacks && init.fallbacks.length > 0) {
|
|
309
308
|
const coreProviders: Provider[] = [
|
|
@@ -337,9 +336,14 @@ export abstract class ProviderAdapter implements AiProvider {
|
|
|
337
336
|
* boot when it is `null` — that model cannot serve this framework's turns).
|
|
338
337
|
* Log `calls`: `0/3 and 3/3` is what makes the next model swap's regression
|
|
339
338
|
* obvious. Costs `samples × 2` short calls, and errors propagate.
|
|
339
|
+
*
|
|
340
|
+
* It asks the primary alone, never the fallback chain. The answer configures
|
|
341
|
+
* this adapter's model; a call the chain handed to a fallback would score
|
|
342
|
+
* another model as this one. Each fallback is an adapter of its own: probe it
|
|
343
|
+
* the same way.
|
|
340
344
|
*/
|
|
341
345
|
async probeJsonWithTools(opts?: ProbeOptions): Promise<JsonWithToolsProbe> {
|
|
342
|
-
return probeJsonWithTools(this.
|
|
346
|
+
return probeJsonWithTools(this.primary, {
|
|
343
347
|
model: this.primaryModel,
|
|
344
348
|
...(opts ?? {}),
|
|
345
349
|
});
|
|
@@ -468,8 +472,7 @@ export abstract class ProviderAdapter implements AiProvider {
|
|
|
468
472
|
tokensUsed: acc.promptTokens + (acc.completionTokens ?? 0),
|
|
469
473
|
promptTokens: acc.promptTokens,
|
|
470
474
|
completionTokens: acc.completionTokens,
|
|
471
|
-
//
|
|
472
|
-
// adapters never read and which is billed far cheaper.
|
|
475
|
+
// The cache-hit subset of the prompt, billed far cheaper.
|
|
473
476
|
cachedInputTokens: acc.cachedInputTokens,
|
|
474
477
|
}
|
|
475
478
|
: {}),
|
|
@@ -51,7 +51,7 @@ export class ZaiProvider extends ProviderAdapter {
|
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
constructor(options: ZaiProviderOptions) {
|
|
54
|
-
if (!options.apiKey) throw new Error(
|
|
54
|
+
if (!options.apiKey) throw new Error(`[ZaiProvider] apiKey is empty: the provider cannot authenticate. Pass { apiKey: process.env.ZAI_API_KEY } and check the variable is set.`);
|
|
55
55
|
|
|
56
56
|
const model = options.model ?? "glm-5.3-flash";
|
|
57
57
|
super({
|
package/src/types/agent.ts
CHANGED
|
@@ -145,7 +145,7 @@ export interface ScheduleEntry {
|
|
|
145
145
|
export type EndReason = "end" | "flow" | "reset" | "skipped" | "failed" | "replaced";
|
|
146
146
|
|
|
147
147
|
export interface TurnResult<D = unknown> {
|
|
148
|
-
/** Version unchanged;
|
|
148
|
+
/** Version unchanged by a turn; `Store.save` bumps it and returns the saved session. */
|
|
149
149
|
session: Session<D>;
|
|
150
150
|
/** False: save nothing, send nothing. */
|
|
151
151
|
changed: boolean;
|
package/src/types/ai.ts
CHANGED
|
@@ -8,12 +8,10 @@ import type { HistoryItem } from "./history.js";
|
|
|
8
8
|
/**
|
|
9
9
|
* How hard the model thinks before answering.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* "
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* that need them get them switched on whenever an effort is set, which is the
|
|
16
|
-
* only setting that ever produced reasoning output.
|
|
11
|
+
* `@providerkit/core`'s effort union, which every provider speaks: "none" |
|
|
12
|
+
* "low" | "medium" | "high" | "max". Absent means the provider's own default
|
|
13
|
+
* and is never sent. The shapes that need a thought summary switched on get it
|
|
14
|
+
* whenever an effort is set, so there is no separate setting for it.
|
|
17
15
|
*/
|
|
18
16
|
export interface ReasoningConfig {
|
|
19
17
|
effort?: Effort;
|
package/src/types/compaction.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { HistoryItem } from "./history.js";
|
|
|
9
9
|
* Configuration for the compaction engine.
|
|
10
10
|
*
|
|
11
11
|
* Validation constraints:
|
|
12
|
+
* - `maxTokens` must be > 0
|
|
12
13
|
* - `compactionThreshold` must be between 0.5 and 0.95
|
|
13
14
|
* - `preserveRecentCount` must be >= 2
|
|
14
15
|
* - `maxToolResultChars` must be > 0
|
package/src/types/errors.ts
CHANGED
|
@@ -5,16 +5,11 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* The normalized provider failure, and the kinds it comes in.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* fixes them, where this package had eight — so a caller can now tell an
|
|
8
|
+
* `ProviderError` comes from `@providerkit/core` and is re-exported here. Its
|
|
9
|
+
* thirteen kinds are named by what fixes them, so a caller can tell an
|
|
11
10
|
* exhausted balance from a per-minute throttle, a plan that never included the
|
|
12
|
-
* API from a wrong key, and an outgrown context window from a bad request.
|
|
13
|
-
*
|
|
14
|
-
* Breaking: the kind lives on `error.kind`, not `error.code`, and the spellings
|
|
15
|
-
* changed with the taxonomy (`rate_limited` is `rate`, `overloaded` is
|
|
16
|
-
* `overload`, `invalid_request` splits into `invalid`, `context`, `model` and
|
|
17
|
-
* `content`). `schema_rejected` is gone: nothing ever produced it.
|
|
11
|
+
* API from a wrong key, and an outgrown context window from a bad request. The
|
|
12
|
+
* kind lives on `error.kind`.
|
|
18
13
|
*/
|
|
19
14
|
export { ProviderError } from "@providerkit/core";
|
|
20
15
|
export type { ErrorKind } from "@providerkit/core";
|
|
@@ -33,9 +28,13 @@ export class SessionConflictError extends Error {
|
|
|
33
28
|
public readonly actualVersion: number | undefined,
|
|
34
29
|
) {
|
|
35
30
|
super(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
// A row that vanished between load and save (a TTL expiry, a delete) is not a race.
|
|
32
|
+
actualVersion === undefined && expectedVersion > 0
|
|
33
|
+
? `[SessionConflictError] Session "${sessionId}" is gone from the store: it was at version ${expectedVersion} ` +
|
|
34
|
+
`and has since been deleted or expired. Load it again; a load that finds nothing starts a new conversation.`
|
|
35
|
+
: `[SessionConflictError] Session "${sessionId}" was modified concurrently: ` +
|
|
36
|
+
`expected version ${expectedVersion}, found ${actualVersion ?? 'none'}. ` +
|
|
37
|
+
`Reload the session and retry the operation.`
|
|
39
38
|
);
|
|
40
39
|
this.name = 'SessionConflictError';
|
|
41
40
|
}
|
package/src/types/history.ts
CHANGED
|
@@ -176,13 +176,3 @@ export interface Event<
|
|
|
176
176
|
/** Unique event identifier */
|
|
177
177
|
id?: string;
|
|
178
178
|
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* An emitted event (staged for inclusion)
|
|
182
|
-
*/
|
|
183
|
-
export interface EmittedEvent<
|
|
184
|
-
TData = MessageEventData | ToolEventData | StatusEventData
|
|
185
|
-
> extends Event<TData> {
|
|
186
|
-
/** Whether this event has been committed */
|
|
187
|
-
committed?: boolean;
|
|
188
|
-
}
|
package/src/types/index.ts
CHANGED
package/src/types/session.ts
CHANGED
|
@@ -127,7 +127,7 @@ export interface StepOutcome {
|
|
|
127
127
|
export interface Session<D = unknown> {
|
|
128
128
|
id: string;
|
|
129
129
|
v: 4;
|
|
130
|
-
/** Optimistic-concurrency version;
|
|
130
|
+
/** Optimistic-concurrency version. A turn leaves it unchanged; `Store.save` bumps it and returns the saved session. */
|
|
131
131
|
version: number;
|
|
132
132
|
data: Partial<D>;
|
|
133
133
|
/** Live runs only. */
|
package/src/utils/clock.ts
CHANGED
|
@@ -34,7 +34,7 @@ export function fakeClock(iso: string): FakeClock {
|
|
|
34
34
|
|
|
35
35
|
function parseIso(iso: string): number {
|
|
36
36
|
const ms = Date.parse(iso);
|
|
37
|
-
if (Number.isNaN(ms)) throw new Error(`fakeClock
|
|
37
|
+
if (Number.isNaN(ms)) throw new Error(`[fakeClock] "${iso}" is not a date: Date.parse cannot read it. Pass ISO text, e.g. "2026-09-20T10:00:00.000Z".`);
|
|
38
38
|
return ms;
|
|
39
39
|
}
|
|
40
40
|
|