@falai/agent 4.0.0-alpha.14 → 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 +5 -9
- package/dist/cjs/providers/ProviderAdapter.d.ts.map +1 -1
- package/dist/cjs/providers/ProviderAdapter.js +1 -2
- 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 +5 -9
- package/dist/providers/ProviderAdapter.d.ts.map +1 -1
- package/dist/providers/ProviderAdapter.js +1 -2
- 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 +1 -1
- 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 +6 -11
- 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/FlowSpec.ts
CHANGED
|
@@ -100,8 +100,14 @@ export interface FlowSpec {
|
|
|
100
100
|
tools?: string[];
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
/**
|
|
104
|
-
|
|
103
|
+
/**
|
|
104
|
+
* What a flow's names resolve against: the agent's fields, actions, events,
|
|
105
|
+
* conditions and tools. With `flows`, a literal `{ flow }` target must name
|
|
106
|
+
* one of them; the agent passes its own.
|
|
107
|
+
*/
|
|
108
|
+
export type Registries = Pick<AgentOptions, "fields" | "actions" | "events" | "conditions" | "tools"> & {
|
|
109
|
+
flows?: ReadonlyArray<{ id: string }>;
|
|
110
|
+
};
|
|
105
111
|
|
|
106
112
|
// ── fromSpec / toSpec ───────────────────────────────────────────────────
|
|
107
113
|
|
|
@@ -113,9 +119,7 @@ export type Registries = Pick<AgentOptions, "fields" | "actions" | "events" | "c
|
|
|
113
119
|
*/
|
|
114
120
|
export function fromSpec<C = unknown, D = LooseData>(spec: FlowSpec): Flow<C, D> {
|
|
115
121
|
const clean = stripNulls(spec);
|
|
116
|
-
|
|
117
|
-
throw problem(`flow "${clean.id}"`, "has no steps list", "Write steps as a list, even an empty one.");
|
|
118
|
-
}
|
|
122
|
+
checkShape(clean);
|
|
119
123
|
const { steps, ...rest } = clean;
|
|
120
124
|
const flow: Flow<unknown, LooseData> = { ...rest, steps: steps.map(fromStepSpec) };
|
|
121
125
|
return flow as Flow<C, D>;
|
|
@@ -273,15 +277,16 @@ interface LooseFlow {
|
|
|
273
277
|
tools?: string[];
|
|
274
278
|
}
|
|
275
279
|
|
|
276
|
-
const BUILT_IN_CONDITIONS = ["equals", "known", "silenced"];
|
|
280
|
+
export const BUILT_IN_CONDITIONS = ["equals", "known", "silenced"];
|
|
277
281
|
|
|
278
282
|
const DURATION_HINT = 'Write a number and a unit: "30s", "5m", "24h" or "3d".';
|
|
279
283
|
|
|
280
284
|
/** The four keys one of which makes an `on[]` entry a trigger. */
|
|
281
285
|
const TRIGGER_KINDS = ["message", "mention", "silence", "event"] as const;
|
|
282
286
|
|
|
283
|
-
|
|
284
|
-
|
|
287
|
+
const ON_END = ["end", "stay", "reset"];
|
|
288
|
+
|
|
289
|
+
const INSTRUCTION_KINDS = ["must", "never", "should"];
|
|
285
290
|
|
|
286
291
|
/**
|
|
287
292
|
* Check a flow, typed or as a spec, against the agent's registries. Throws
|
|
@@ -294,7 +299,8 @@ export function validateFlow<C = unknown, D = LooseData>(
|
|
|
294
299
|
): { warnings: string[] } {
|
|
295
300
|
// Nulls mean "not set" in a spec; a typed flow has none, so one pass serves both forms.
|
|
296
301
|
const flow: LooseFlow = stripNulls(input);
|
|
297
|
-
|
|
302
|
+
checkShape(flow);
|
|
303
|
+
const { fields, actions = {}, events = {}, tools = [] } = registries;
|
|
298
304
|
const toolIds = new Set(tools.map((tool) => tool.id));
|
|
299
305
|
const warnings: string[] = [];
|
|
300
306
|
|
|
@@ -302,9 +308,6 @@ export function validateFlow<C = unknown, D = LooseData>(
|
|
|
302
308
|
throw problem("flow", "has no id", "Give the flow a short unique id.");
|
|
303
309
|
}
|
|
304
310
|
const flowAt = `flow "${flow.id}"`;
|
|
305
|
-
if (!Array.isArray(flow.steps)) {
|
|
306
|
-
throw problem(flowAt, "has no steps list", "Write steps as a list, even an empty one.");
|
|
307
|
-
}
|
|
308
311
|
|
|
309
312
|
const index = new Map<string, number>();
|
|
310
313
|
flow.steps.forEach((step, i) => {
|
|
@@ -323,11 +326,7 @@ export function validateFlow<C = unknown, D = LooseData>(
|
|
|
323
326
|
throw problem(flowAt, "has triggers but no steps", "Add at least one step or remove `on`.");
|
|
324
327
|
}
|
|
325
328
|
|
|
326
|
-
const slug = (name: string, at: string, where: string): void =>
|
|
327
|
-
if (!own(fields, name)) {
|
|
328
|
-
throw problem(at, `unknown field "${name}" in ${where}`, "Add it to the agent's fields or fix the slug.");
|
|
329
|
-
}
|
|
330
|
-
};
|
|
329
|
+
const slug = (name: string, at: string, where: string): void => checkSlug(fields, name, at, where);
|
|
331
330
|
|
|
332
331
|
const toolNames = (names: string[] | undefined, at: string): void => {
|
|
333
332
|
for (const name of names ?? []) {
|
|
@@ -335,38 +334,7 @@ export function validateFlow<C = unknown, D = LooseData>(
|
|
|
335
334
|
}
|
|
336
335
|
};
|
|
337
336
|
|
|
338
|
-
const pred = (value: LoosePred | undefined, at: string, where: string): void =>
|
|
339
|
-
if (value === undefined || typeof value === "function") return;
|
|
340
|
-
for (const [name, arg] of Object.entries(value)) {
|
|
341
|
-
if (name === "equals") {
|
|
342
|
-
if (arg === null || typeof arg !== "object" || Array.isArray(arg)) {
|
|
343
|
-
throw problem(at, `${where}.equals is not an object`, "Write equals as { field: value }.");
|
|
344
|
-
}
|
|
345
|
-
for (const [field, given] of Object.entries(arg)) {
|
|
346
|
-
slug(field, at, `${where}.equals`);
|
|
347
|
-
const def = fields[field];
|
|
348
|
-
if (!matches(def, given)) {
|
|
349
|
-
throw problem(
|
|
350
|
-
at,
|
|
351
|
-
`${where}.equals gives "${field}" a ${describe(given)}, but the field is a ${def.type}`,
|
|
352
|
-
`Write a ${def.type}; values are not coerced.`,
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
} else if (name === "known") {
|
|
357
|
-
if (!Array.isArray(arg)) throw problem(at, `${where}.known is not a list`, "Write known as [field, ...].");
|
|
358
|
-
for (const field of arg) slug(String(field), at, `${where}.known`);
|
|
359
|
-
} else if (name === "silenced") {
|
|
360
|
-
if (typeof arg !== "boolean") throw problem(at, `${where}.silenced is not a boolean`, "Write true or false.");
|
|
361
|
-
} else if (!own(conditions, name)) {
|
|
362
|
-
throw problem(
|
|
363
|
-
at,
|
|
364
|
-
`unknown condition "${name}" in ${where}`,
|
|
365
|
-
`Register it in conditions or use ${BUILT_IN_CONDITIONS.join(", ")}.`,
|
|
366
|
-
);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
};
|
|
337
|
+
const pred = (value: LoosePred | undefined, at: string, where: string): void => checkPred(value, at, where, registries);
|
|
370
338
|
|
|
371
339
|
const duration = (value: string | undefined, at: string, where: string): void => {
|
|
372
340
|
if (value !== undefined && !isDuration(value)) {
|
|
@@ -384,7 +352,16 @@ export function validateFlow<C = unknown, D = LooseData>(
|
|
|
384
352
|
}
|
|
385
353
|
return to;
|
|
386
354
|
}
|
|
387
|
-
if ("flow" in next)
|
|
355
|
+
if ("flow" in next) {
|
|
356
|
+
// A templated id resolves per run; only a literal one can be checked now. A warning, not a throw:
|
|
357
|
+
// a host that drops one bad row keeps the rest of its agent, and the Runner skips this move as flow-gone.
|
|
358
|
+
const known = registries.flows?.map((f) => f.id);
|
|
359
|
+
if (known && !next.flow.includes("{{") && !known.includes(next.flow)) {
|
|
360
|
+
const fix = known.length ? `Use one of ${known.map((id) => `"${id}"`).join(", ")}, or add the flow.` : "Add the flow to the agent.";
|
|
361
|
+
warnings.push(`${at}: ${where} names flow "${next.flow}", which this agent does not have; a run skips this move with flow-gone. ${fix}`);
|
|
362
|
+
}
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
388
365
|
const to = index.get(next.step);
|
|
389
366
|
if (to === undefined) {
|
|
390
367
|
throw problem(at, `${where} points at step "${next.step}", which does not exist`, 'Use an existing step id or "end".');
|
|
@@ -482,17 +459,6 @@ export function validateFlow<C = unknown, D = LooseData>(
|
|
|
482
459
|
|
|
483
460
|
flow.steps.forEach((step, i) => {
|
|
484
461
|
const at = `${flowAt}, step "${step.id}"`;
|
|
485
|
-
// Same reasoning as the trigger above: a step that does none of the five
|
|
486
|
-
// things is a step the run walks straight past, silently. `kind` alone is
|
|
487
|
-
// not enough — the spec form drops it and keeps the body, so what counts
|
|
488
|
-
// is whether the body says what to do.
|
|
489
|
-
if (!STEP_DOES.some((key) => step[key] !== undefined)) {
|
|
490
|
-
throw problem(
|
|
491
|
-
at,
|
|
492
|
-
"does nothing",
|
|
493
|
-
"A step talks (`prompt` / `collect`), says (`say`), acts (`do`), waits (`wait`) or forks (`if`).",
|
|
494
|
-
);
|
|
495
|
-
}
|
|
496
462
|
const thenTo = edge(i, step.then, at, "then");
|
|
497
463
|
edge(i, step.else, at, "else");
|
|
498
464
|
|
|
@@ -518,6 +484,14 @@ export function validateFlow<C = unknown, D = LooseData>(
|
|
|
518
484
|
if (branch.when === undefined && branch.if === undefined) {
|
|
519
485
|
throw problem(at, `${where} has neither when nor if`, "Give the branch an AI condition (when) or a code one (if).");
|
|
520
486
|
}
|
|
487
|
+
// No understand call judges a wait, so an AI condition there never fires. A warning, not a throw: the
|
|
488
|
+
// flow still runs as it did, a reply goes to `else`, and editors that offered the branch keep their rows.
|
|
489
|
+
if (branch.when !== undefined && step.wait !== undefined) {
|
|
490
|
+
warnings.push(
|
|
491
|
+
`${at}: ${where} is a "when" branch on a wait step, which no call judges, so a reply goes to else. ` +
|
|
492
|
+
'Use "if", or move the branch to a talk step.',
|
|
493
|
+
);
|
|
494
|
+
}
|
|
521
495
|
pred(branch.if, at, `${where}.if`);
|
|
522
496
|
edge(i, branch.then, at, `${where}.then`);
|
|
523
497
|
});
|
|
@@ -554,6 +528,228 @@ export function validateFlow<C = unknown, D = LooseData>(
|
|
|
554
528
|
return { warnings };
|
|
555
529
|
}
|
|
556
530
|
|
|
531
|
+
function checkSlug(fields: FieldDefs, name: string, at: string, where: string): void {
|
|
532
|
+
if (!own(fields, name)) {
|
|
533
|
+
throw problem(at, `unknown field "${name}" in ${where}`, "Add it to the agent's fields or fix the slug.");
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* A predicate's names against the registries: the fields `equals` and `known`
|
|
539
|
+
* read, and every other key as one of the agent's conditions. A function is
|
|
540
|
+
* code and passes as is. The agent runs this on its own instructions too.
|
|
541
|
+
*/
|
|
542
|
+
export function checkPred(value: LoosePred | undefined, at: string, where: string, registries: Registries): void {
|
|
543
|
+
if (value === undefined || typeof value === "function") return;
|
|
544
|
+
const { fields, conditions = {} } = registries;
|
|
545
|
+
for (const [name, arg] of Object.entries(value)) {
|
|
546
|
+
if (name === "equals") {
|
|
547
|
+
if (!isObject(arg)) {
|
|
548
|
+
throw problem(at, `${where}.equals is not an object`, "Write equals as { field: value }.");
|
|
549
|
+
}
|
|
550
|
+
for (const [field, given] of Object.entries(arg)) {
|
|
551
|
+
checkSlug(fields, field, at, `${where}.equals`);
|
|
552
|
+
const def = fields[field];
|
|
553
|
+
if (!matches(def, given)) {
|
|
554
|
+
const { listed, expected, got, fix } = mismatch(def, given);
|
|
555
|
+
throw listed
|
|
556
|
+
? problem(at, `${where}.equals gives "${field}" ${got}, which is not ${expected}`, fix)
|
|
557
|
+
: problem(at, `${where}.equals gives "${field}" ${article(got)} ${got}, but the field is ${expected}`, `Write ${expected}; values are not coerced.`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
} else if (name === "known") {
|
|
561
|
+
if (!Array.isArray(arg)) throw problem(at, `${where}.known is not a list`, "Write known as [field, ...].");
|
|
562
|
+
for (const field of arg) checkSlug(fields, String(field), at, `${where}.known`);
|
|
563
|
+
} else if (name === "silenced") {
|
|
564
|
+
if (typeof arg !== "boolean") throw problem(at, `${where}.silenced is not a boolean`, "Write true or false.");
|
|
565
|
+
} else if (!own(conditions, name)) {
|
|
566
|
+
throw problem(
|
|
567
|
+
at,
|
|
568
|
+
`unknown condition "${name}" in ${where}`,
|
|
569
|
+
`Register it in conditions or use ${BUILT_IN_CONDITIONS.join(", ")}.`,
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// ── Shape ───────────────────────────────────────────────────────────────
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* The JSON shape, checked before any name is. Stored rows and generated specs
|
|
579
|
+
* are untrusted: a string where a list belongs used to crash with a raw
|
|
580
|
+
* TypeError naming no flow, or pass and misbehave at run time (`collect:
|
|
581
|
+
* "nome"` read as the fields "n", "o", "m", "e").
|
|
582
|
+
*/
|
|
583
|
+
function checkShape(value: unknown): void {
|
|
584
|
+
if (!isObject(value)) throw problem("flow", `is ${show(value)}, not an object`, "Pass the flow itself: { id, name, steps }.");
|
|
585
|
+
const flowAt = typeof value.id === "string" ? `flow "${value.id}"` : "flow";
|
|
586
|
+
if (!Array.isArray(value.steps)) {
|
|
587
|
+
throw problem(flowAt, "has no steps list", "Write steps as a list, even an empty one.");
|
|
588
|
+
}
|
|
589
|
+
text(value, ["description", "anchor"], flowAt);
|
|
590
|
+
for (const key of ["collect", "clearOnStart", "tools"]) listOf(value[key], key, flowAt, "string");
|
|
591
|
+
oneOf(value.onEnd, "onEnd", ON_END, flowAt);
|
|
592
|
+
predShape(value.while, "while", flowAt);
|
|
593
|
+
instructionsShape(value.instructions, flowAt);
|
|
594
|
+
|
|
595
|
+
listOf(value.on, "on", flowAt, "object").forEach((trigger, i) => {
|
|
596
|
+
const at = `${flowAt}, trigger #${i + 1}`;
|
|
597
|
+
if (!isObject(trigger)) return;
|
|
598
|
+
listOf(trigger.message, "message", at, "string");
|
|
599
|
+
listOf(trigger.mention, "mention", at, "string");
|
|
600
|
+
predShape(trigger.if, "if", at);
|
|
601
|
+
const { repeat } = trigger;
|
|
602
|
+
if (repeat !== undefined && repeat !== "once" && repeat !== "always" && !(isObject(repeat) && typeof repeat.cooldown === "string")) {
|
|
603
|
+
throw problem(at, `repeat is ${show(repeat)}`, 'Use "once", "always" or { cooldown: "24h" }.');
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
listOf(value.steps, "steps", flowAt, "object").forEach((step, i) => {
|
|
608
|
+
if (!isObject(step)) return;
|
|
609
|
+
const at = typeof step.id === "string" ? `${flowAt}, step "${step.id}"` : `${flowAt}, step #${i + 1}`;
|
|
610
|
+
stepShape(step, at);
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** One step: what it does, and every value the right kind of thing. */
|
|
615
|
+
function stepShape(step: Record<string, unknown>, at: string): void {
|
|
616
|
+
// A step that does none of the five things is one the run walks straight
|
|
617
|
+
// past; one that does two runs only the first, and the other never happens.
|
|
618
|
+
const kinds = bodyKinds(step);
|
|
619
|
+
if (kinds.length === 0) {
|
|
620
|
+
throw problem(
|
|
621
|
+
at,
|
|
622
|
+
"does nothing",
|
|
623
|
+
"A step talks (`prompt` / `collect`), says (`say`), acts (`do`), waits (`wait`) or forks (`if`).",
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
if (kinds.length > 1) {
|
|
627
|
+
throw problem(at, `mixes ${and(kinds.map((k) => `"${k}"`))}`, "A step does one thing. Split it into one step per kind.");
|
|
628
|
+
}
|
|
629
|
+
// The spec's `kind` must say what the body does; the Runner reads the body.
|
|
630
|
+
if (step.kind !== undefined && step.kind !== kinds[0]) {
|
|
631
|
+
throw problem(at, `has kind ${show(step.kind)}, but its body is a "${kinds[0]}" step`, `Set kind to "${kinds[0]}", or change the body to match.`);
|
|
632
|
+
}
|
|
633
|
+
text(step, ["say", "prompt", "question"], at);
|
|
634
|
+
listOf(step.collect, "collect", at, "string");
|
|
635
|
+
listOf(step.tools, "tools", at, "string");
|
|
636
|
+
if (step.ask !== undefined && !isObject(step.ask)) {
|
|
637
|
+
throw problem(at, `ask is ${show(step.ask)}, not an object`, 'Write ask as { field: "how to ask" }.');
|
|
638
|
+
}
|
|
639
|
+
if (step.with !== undefined && !isObject(step.with)) {
|
|
640
|
+
throw problem(at, `with is ${show(step.with)}, not an object`, "Write with as { parameter: value }.");
|
|
641
|
+
}
|
|
642
|
+
const { maxAsks } = step;
|
|
643
|
+
if (maxAsks !== undefined && !(typeof maxAsks === "number" && Number.isInteger(maxAsks) && maxAsks >= 1)) {
|
|
644
|
+
throw problem(at, `maxAsks is ${show(maxAsks)}, not a whole number of 1 or more`, "Write a number like 3.");
|
|
645
|
+
}
|
|
646
|
+
if (isObject(step.wait) && typeof step.wait.event !== "string") {
|
|
647
|
+
throw problem(at, "wait has no event", 'Write wait: { event: "name" } to wait for an event, or a duration like "1h".');
|
|
648
|
+
}
|
|
649
|
+
predShape(step.if, "if", at);
|
|
650
|
+
for (const key of ["then", "else", "onFail"]) nextShape(step[key], key, at);
|
|
651
|
+
instructionsShape(step.instructions, at);
|
|
652
|
+
listOf(step.branches, "branches", at, "object").forEach((branch, j) => {
|
|
653
|
+
if (!isObject(branch)) return;
|
|
654
|
+
const where = `branches[${j}]`;
|
|
655
|
+
if (branch.when !== undefined && typeof branch.when !== "string") {
|
|
656
|
+
throw problem(at, `${where}.when is ${show(branch.when)}, not text`, "Write the condition as one sentence.");
|
|
657
|
+
}
|
|
658
|
+
predShape(branch.if, `${where}.if`, at);
|
|
659
|
+
nextShape(branch.then, `${where}.then`, at);
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/** The kinds a step's body carries, by its keys. A talk step is `collect` with a list, `prompt` with a guideline alone. */
|
|
664
|
+
function bodyKinds(step: Record<string, unknown>): StepKind[] {
|
|
665
|
+
const kinds: StepKind[] = [];
|
|
666
|
+
if (step.collect !== undefined) kinds.push("collect");
|
|
667
|
+
else if (step.prompt !== undefined) kinds.push("prompt");
|
|
668
|
+
if (step.say !== undefined) kinds.push("say");
|
|
669
|
+
if (step.do !== undefined) kinds.push("do");
|
|
670
|
+
if (step.wait !== undefined) kinds.push(typeof step.wait === "string" ? "wait" : "waitEvent");
|
|
671
|
+
if (step.if !== undefined) kinds.push("if");
|
|
672
|
+
return kinds;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function instructionsShape(value: unknown, at: string): void {
|
|
676
|
+
listOf(value, "instructions", at, "object").forEach((ins, i) => {
|
|
677
|
+
if (!isObject(ins)) return;
|
|
678
|
+
const where = `instructions[${i}]`;
|
|
679
|
+
oneOf(ins.kind, `${where}.kind`, INSTRUCTION_KINDS, at);
|
|
680
|
+
if (typeof ins.prompt !== "string") {
|
|
681
|
+
throw problem(at, `${where}.prompt is ${show(ins.prompt)}, not text`, "Write the rule as a sentence.");
|
|
682
|
+
}
|
|
683
|
+
if (ins.when !== undefined && typeof ins.when !== "string") listOf(ins.when, `${where}.when`, at, "string");
|
|
684
|
+
predShape(ins.if, `${where}.if`, at);
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/** A step id, `"end"`, `{ step, clear? }` or `{ flow, input? }`. */
|
|
689
|
+
function nextShape(value: unknown, where: string, at: string): void {
|
|
690
|
+
if (value === undefined || typeof value === "string") return;
|
|
691
|
+
if (isObject(value) && (typeof value.step === "string" || typeof value.flow === "string")) {
|
|
692
|
+
listOf(value.clear, `${where}.clear`, at, "string");
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
throw problem(at, `${where} is ${show(value)}, not a step id or a target`, 'Write a step id, "end", { step: "id" } or { flow: "id" }.');
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** A condition object, or code on a typed flow. */
|
|
699
|
+
function predShape(value: unknown, where: string, at: string): void {
|
|
700
|
+
if (value === undefined || typeof value === "function" || isObject(value)) return;
|
|
701
|
+
throw problem(at, `${where} is ${show(value)}, not a condition`, 'Write it as an object, e.g. { known: ["nome"] }.');
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/** Each named key, when set, must be text. */
|
|
705
|
+
function text(owner: Record<string, unknown>, keys: string[], at: string): void {
|
|
706
|
+
for (const key of keys) {
|
|
707
|
+
const value = owner[key];
|
|
708
|
+
if (value !== undefined && typeof value !== "string") {
|
|
709
|
+
throw problem(at, `${key} is ${show(value)}, not text`, `Write ${key} as a string.`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function oneOf(value: unknown, where: string, allowed: string[], at: string): void {
|
|
715
|
+
if (value === undefined || (typeof value === "string" && allowed.includes(value))) return;
|
|
716
|
+
throw problem(at, `${where} is ${show(value)}, which is not one of ${allowed.map((v) => `"${v}"`).join(", ")}`, "Use one of them.");
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/** A list whose items are all strings or all objects; absent reads as empty. */
|
|
720
|
+
function listOf(value: unknown, where: string, at: string, item: "string" | "object"): unknown[] {
|
|
721
|
+
if (value === undefined) return [];
|
|
722
|
+
if (!Array.isArray(value)) {
|
|
723
|
+
const example = item === "string" && typeof value === "string" ? `${where}: ${JSON.stringify([value])}` : `${where} as a list`;
|
|
724
|
+
throw problem(at, `${where} is ${show(value)}, not a list`, `Write ${example}.`);
|
|
725
|
+
}
|
|
726
|
+
value.forEach((entry, i) => {
|
|
727
|
+
if (item === "string" ? typeof entry !== "string" : !isObject(entry)) {
|
|
728
|
+
throw problem(at, `${where}[${i}] is ${show(entry)}, not ${item === "string" ? "text" : "an object"}`, `Write each entry of ${where} as ${item === "string" ? "a string" : "an object"}.`);
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
return value;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/** A plain object: not null, not a list, not a function. */
|
|
735
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
736
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/** A wrong value as an error names it: short scalars verbatim, anything else by kind. */
|
|
740
|
+
function show(value: unknown): string {
|
|
741
|
+
if (typeof value === "string") return JSON.stringify(value.length > 40 ? `${value.slice(0, 37)}...` : value);
|
|
742
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
743
|
+
if (value === undefined) return "missing";
|
|
744
|
+
if (value === null) return "null";
|
|
745
|
+
return Array.isArray(value) ? "a list" : typeof value === "function" ? "a function" : "an object";
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/** `"a"`, `"a" and "b"`, `"a", "b" and "c"`. */
|
|
749
|
+
function and(items: string[]): string {
|
|
750
|
+
return items.length < 2 ? items.join("") : `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`;
|
|
751
|
+
}
|
|
752
|
+
|
|
557
753
|
function matchesParam(def: ParamDef, value: unknown): boolean {
|
|
558
754
|
if (def.type === "array") return Array.isArray(value) && value.every((item) => matches(def.items, item));
|
|
559
755
|
return matches(def, value);
|
|
@@ -577,16 +773,16 @@ function matches(def: ScalarDef, value: unknown): boolean {
|
|
|
577
773
|
* What a rejected value should have been and what it was. A value of the right type that is
|
|
578
774
|
* not a listed one names the listed values: "must be a string, got string" would say nothing.
|
|
579
775
|
*/
|
|
580
|
-
function mismatch(def: ParamDef, value: unknown): { expected: string; got: string; fix: string } {
|
|
776
|
+
function mismatch(def: ParamDef, value: unknown): { listed: boolean; expected: string; got: string; fix: string } {
|
|
581
777
|
const scalar = def.type === "array" ? def.items : def;
|
|
582
778
|
const { enum: allowed, ...typeOnly } = scalar;
|
|
583
779
|
const items = def.type === "array" && Array.isArray(value) ? value : [value];
|
|
584
780
|
const off = allowed ? items.findIndex((item) => matches(typeOnly, item) && !matches(scalar, item)) : -1;
|
|
585
781
|
if (!allowed || off === -1) {
|
|
586
|
-
return { expected: describeDef(def), got: describe(value), fix: "Values are not coerced; write the right type." };
|
|
782
|
+
return { listed: false, expected: describeDef(def), got: describe(value), fix: "Values are not coerced; write the right type." };
|
|
587
783
|
}
|
|
588
784
|
const list = (v: unknown) => JSON.stringify(v);
|
|
589
|
-
return { expected: `one of ${allowed.map(list).join(", ")}`, got: list(items[off]), fix: "Use one of the listed values." };
|
|
785
|
+
return { listed: true, expected: `one of ${allowed.map(list).join(", ")}`, got: list(items[off]), fix: "Use one of the listed values." };
|
|
590
786
|
}
|
|
591
787
|
|
|
592
788
|
function describe(value: unknown): string {
|
|
@@ -668,6 +864,8 @@ export function flowSpecSchema(registries: Registries): StructuredSchema {
|
|
|
668
864
|
const branches = orNull(
|
|
669
865
|
list(union([closed({ when: STRING, then: next }), closed({ if: condition, then: next })]), "Exits judged while the step asks"),
|
|
670
866
|
);
|
|
867
|
+
// No understand call judges a wait, so its branches are code only.
|
|
868
|
+
const waitBranches = orNull(list(closed({ if: condition, then: next }), "Exits checked by code when the lead replies before the wait ends"));
|
|
671
869
|
const stepBase = { id: STRING, label: orNull(STRING), then: nextOrNull };
|
|
672
870
|
const step = union([
|
|
673
871
|
closed({ ...stepBase, kind: enumOf(["prompt"]), prompt: { type: "string", description: "Guideline for the AI's next message" }, branches }),
|
|
@@ -697,7 +895,7 @@ export function flowSpecSchema(registries: Registries): StructuredSchema {
|
|
|
697
895
|
wait: { ...duration, description: "Park this long; then = time passed, else = the lead replied" },
|
|
698
896
|
businessHours: orNull(BOOLEAN),
|
|
699
897
|
else: nextOrNull,
|
|
700
|
-
branches,
|
|
898
|
+
branches: waitBranches,
|
|
701
899
|
}),
|
|
702
900
|
eventNames.length
|
|
703
901
|
? closed({
|
package/src/core/Prompt.ts
CHANGED
|
@@ -160,3 +160,43 @@ export function pendingSection(
|
|
|
160
160
|
...lines,
|
|
161
161
|
].join("\n");
|
|
162
162
|
}
|
|
163
|
+
|
|
164
|
+
/** Gemini rejects any other character in a property name. */
|
|
165
|
+
const SAFE_KEY = /^[a-zA-Z0-9_-]+$/;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Envelope property names for ids the schema cannot carry. A safe id keeps
|
|
169
|
+
* its own name; anything else (run ids carry `#`, `:` and `/`) or a name the
|
|
170
|
+
* envelope already uses (`reserved`) gets a short alias, mapped back when the
|
|
171
|
+
* reply is parsed.
|
|
172
|
+
*/
|
|
173
|
+
export class Aliases {
|
|
174
|
+
private readonly byReal = new Map<string, string>();
|
|
175
|
+
private readonly byAlias = new Map<string, string>();
|
|
176
|
+
private n = 0;
|
|
177
|
+
|
|
178
|
+
constructor(reserved: readonly string[] = []) {
|
|
179
|
+
for (const name of reserved) this.byAlias.set(name, name);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
of(real: string, prefix = "k"): string {
|
|
183
|
+
const seen = this.byReal.get(real);
|
|
184
|
+
if (seen) return seen;
|
|
185
|
+
const alias = SAFE_KEY.test(real) && !this.byAlias.has(real) ? real : this.fresh(prefix);
|
|
186
|
+
this.byAlias.set(alias, real);
|
|
187
|
+
this.byReal.set(real, alias);
|
|
188
|
+
return alias;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Unknown aliases come back as they are; Runner drops keys it does not know. */
|
|
192
|
+
real(alias: string): string {
|
|
193
|
+
return this.byAlias.get(alias) ?? alias;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private fresh(prefix: string): string {
|
|
197
|
+
let alias: string;
|
|
198
|
+
do alias = `${prefix}${++this.n}`;
|
|
199
|
+
while (this.byAlias.has(alias));
|
|
200
|
+
return alias;
|
|
201
|
+
}
|
|
202
|
+
}
|
package/src/core/Runner.ts
CHANGED
|
@@ -13,8 +13,7 @@ import type { TokenUsage } from "../types/ai.js";
|
|
|
13
13
|
import type { ActionResult, Branch, DoStep, Duration, Flow, IfStep, Next, Pred, PredCtx, Repeat, SayStep, Step, StepBase, TalkStep, Trigger, WaitEventStep, WaitStep } from "../types/flow.js";
|
|
14
14
|
import type { History } from "../types/history.js";
|
|
15
15
|
import type { Run, Session, StepOutcome, StepOutcomeCode, StepOutcomeKind, TriggerKind } from "../types/session.js";
|
|
16
|
-
import {
|
|
17
|
-
import { parseDuration } from "../utils/duration.js";
|
|
16
|
+
import { isDuration, parseDuration } from "../utils/duration.js";
|
|
18
17
|
import { OUTCOME_MESSAGES } from "../utils/outcomes.js";
|
|
19
18
|
import { coerceField, DEFAULT_MAX_ASKS, extractMode, isKnown, pendingFields } from "../utils/schema.js";
|
|
20
19
|
import { render, renderDeep } from "../utils/template.js";
|
|
@@ -202,7 +201,7 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
202
201
|
what,
|
|
203
202
|
kind: what.kind,
|
|
204
203
|
triggerKey: what.kind === "message" ? (what.id ?? what.at) : what.key,
|
|
205
|
-
session: input.session ?
|
|
204
|
+
session: input.session ? structuredClone(input.session) : freshSession<D>(input.sessionId),
|
|
206
205
|
original: input.session,
|
|
207
206
|
now,
|
|
208
207
|
nowIso,
|
|
@@ -350,8 +349,10 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
350
349
|
/** `silence:<flowId>:<sessionId>:<lastAssistantAtMs>`, honoured only while the blob still shows that silence. */
|
|
351
350
|
private silenceWake(turn: Turn<C, D>, key: string): void {
|
|
352
351
|
const rest = key.slice("silence:".length);
|
|
353
|
-
const flowId = rest.slice(0, rest.indexOf(":"));
|
|
354
352
|
const ms = Number(rest.slice(rest.lastIndexOf(":") + 1));
|
|
353
|
+
// A flow id may hold a ":" itself, so cut at the session id rather than at the first ":".
|
|
354
|
+
const cut = rest.lastIndexOf(`:${turn.session.id}:`);
|
|
355
|
+
const flowId = rest.slice(0, cut === -1 ? rest.indexOf(":") : cut);
|
|
355
356
|
const { lastAssistantAt } = turn.session;
|
|
356
357
|
const flow = this.flows.get(flowId);
|
|
357
358
|
if (!flow) {
|
|
@@ -413,11 +414,8 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
413
414
|
return null;
|
|
414
415
|
};
|
|
415
416
|
if (opts.trigger?.if && !this.holds(opts.trigger.if, turn, run)) return null;
|
|
416
|
-
const
|
|
417
|
-
if (
|
|
418
|
-
if (typeof repeat === "string") return skip("already-claimed");
|
|
419
|
-
if (turn.now.getTime() - Date.parse(heldAt) < parseDuration(repeat.cooldown)) return skip("cooldown");
|
|
420
|
-
}
|
|
417
|
+
const blocked = this.repeatBlocks(turn, dedupeKey, repeat);
|
|
418
|
+
if (blocked) return skip(blocked);
|
|
421
419
|
if (hop >= MAX_HOP) return skip("hop-limit");
|
|
422
420
|
const live = session.runs.find((r) => r.flowId === flow.id && r.anchor === anchor);
|
|
423
421
|
if (live) {
|
|
@@ -462,10 +460,15 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
462
460
|
|
|
463
461
|
private repeatAllows(turn: Turn<C, D>, flow: Flow<C, D>, trigger: Trigger<C, D>, key: string): boolean {
|
|
464
462
|
const repeat = trigger.repeat ?? defaultRepeat(triggerKind(trigger));
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
463
|
+
return !this.repeatBlocks(turn, `${flow.id}:${this.anchorOf(turn, flow)}:${repeat === "always" ? key : ""}`, repeat);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Why the claim already on `dedupeKey` stops another start, or nothing when there is none or its cooldown has run out. */
|
|
467
|
+
private repeatBlocks(turn: Turn<C, D>, dedupeKey: string, repeat: Repeat): "already-claimed" | "cooldown" | undefined {
|
|
468
|
+
const heldAt = this.claimAt(turn, dedupeKey);
|
|
469
|
+
if (heldAt === undefined) return undefined;
|
|
470
|
+
if (typeof repeat === "string") return "already-claimed";
|
|
471
|
+
return turn.now.getTime() - Date.parse(heldAt) < parseDuration(repeat.cooldown) ? "cooldown" : undefined;
|
|
469
472
|
}
|
|
470
473
|
|
|
471
474
|
/** A run as it would be if started now, for trigger-level predicates. */
|
|
@@ -788,9 +791,12 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
788
791
|
const instructions = [...(this.options.instructions ?? []), ...own.instructions].filter((ins) => !ins.if || evaluate(ins.if, ctx, conditions));
|
|
789
792
|
const all = this.options.tools ?? [];
|
|
790
793
|
const allowed = own.tools;
|
|
794
|
+
// deferTalk leaves a `deferred` outcome as the step's last line; the retry wake re-runs the step without adding one.
|
|
795
|
+
const last = "idle" in talk ? undefined : talk.run.outcomes.at(-1);
|
|
796
|
+
const retry = !("idle" in talk) && last?.status === "deferred" && last.stepId === talk.step.id;
|
|
791
797
|
return {
|
|
792
798
|
talk,
|
|
793
|
-
input: turn.what.kind === "message" ? { kind: turn.kind, text: turn.what.text } : { kind: turn.kind },
|
|
799
|
+
input: turn.what.kind === "message" ? { kind: turn.kind, text: turn.what.text } : { kind: turn.kind, ...(retry ? { retry } : {}) },
|
|
794
800
|
context: turn.context,
|
|
795
801
|
data: turn.session.data,
|
|
796
802
|
history: this.historyOf(turn),
|
|
@@ -967,6 +973,13 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
967
973
|
result = { failed: error instanceof Error ? error.message : String(error) };
|
|
968
974
|
}
|
|
969
975
|
}
|
|
976
|
+
// The handler already ran, so a defer that cannot be parsed must not throw
|
|
977
|
+
// the turn: every replay would repeat the side effect and fail again.
|
|
978
|
+
if ("defer" in result && !isDuration(String(result.defer))) {
|
|
979
|
+
result = {
|
|
980
|
+
failed: `action "${step.do}" asked to defer by "${String(result.defer)}", which is not a duration. Use a value like "2m" or "1h".`,
|
|
981
|
+
};
|
|
982
|
+
}
|
|
970
983
|
if ("ok" in result) {
|
|
971
984
|
if (result.spoke) {
|
|
972
985
|
turn.spokeBy.add(run.id);
|