@falai/agent 4.0.0-alpha.8 → 4.0.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/README.md +2 -0
- package/dist/cjs/core/Agent.d.ts +8 -1
- package/dist/cjs/core/Agent.d.ts.map +1 -1
- package/dist/cjs/core/Agent.js +18 -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 +21 -7
- package/dist/cjs/core/CompactionEngine.js.map +1 -1
- package/dist/cjs/core/FlowSpec.d.ts +21 -2
- package/dist/cjs/core/FlowSpec.d.ts.map +1 -1
- package/dist/cjs/core/FlowSpec.js +297 -59
- package/dist/cjs/core/FlowSpec.js.map +1 -1
- package/dist/cjs/core/Migrate.d.ts.map +1 -1
- package/dist/cjs/core/Migrate.js +3 -1
- package/dist/cjs/core/Migrate.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 +37 -4
- package/dist/cjs/core/Runner.d.ts.map +1 -1
- package/dist/cjs/core/Runner.js +281 -72
- 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 +63 -19
- package/dist/cjs/core/Speak.js.map +1 -1
- package/dist/cjs/core/Understand.d.ts +6 -3
- package/dist/cjs/core/Understand.d.ts.map +1 -1
- package/dist/cjs/core/Understand.js +18 -42
- package/dist/cjs/core/Understand.js.map +1 -1
- package/dist/cjs/core/contracts.d.ts +26 -6
- 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/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 +26 -11
- package/dist/cjs/providers/ProviderAdapter.d.ts.map +1 -1
- package/dist/cjs/providers/ProviderAdapter.js +13 -13
- 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 +14 -3
- package/dist/cjs/types/agent.d.ts.map +1 -1
- package/dist/cjs/types/ai.d.ts +10 -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/flow.d.ts +17 -1
- package/dist/cjs/types/flow.d.ts.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 +2 -2
- package/dist/cjs/types/index.d.ts.map +1 -1
- package/dist/cjs/types/session.d.ts +4 -2
- 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/outcomes.d.ts +2 -0
- package/dist/cjs/utils/outcomes.d.ts.map +1 -1
- package/dist/cjs/utils/outcomes.js +2 -0
- package/dist/cjs/utils/outcomes.js.map +1 -1
- package/dist/cjs/utils/schema.d.ts +3 -12
- package/dist/cjs/utils/schema.d.ts.map +1 -1
- package/dist/cjs/utils/schema.js +3 -43
- package/dist/cjs/utils/schema.js.map +1 -1
- package/dist/cjs/utils/template.d.ts.map +1 -1
- package/dist/cjs/utils/template.js +9 -2
- package/dist/cjs/utils/template.js.map +1 -1
- package/dist/core/Agent.d.ts +8 -1
- package/dist/core/Agent.d.ts.map +1 -1
- package/dist/core/Agent.js +19 -1
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/CompactionEngine.d.ts.map +1 -1
- package/dist/core/CompactionEngine.js +21 -7
- package/dist/core/CompactionEngine.js.map +1 -1
- package/dist/core/FlowSpec.d.ts +21 -2
- package/dist/core/FlowSpec.d.ts.map +1 -1
- package/dist/core/FlowSpec.js +296 -60
- package/dist/core/FlowSpec.js.map +1 -1
- package/dist/core/Migrate.d.ts.map +1 -1
- package/dist/core/Migrate.js +3 -1
- package/dist/core/Migrate.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 +37 -4
- package/dist/core/Runner.d.ts.map +1 -1
- package/dist/core/Runner.js +282 -73
- package/dist/core/Runner.js.map +1 -1
- package/dist/core/Speak.d.ts.map +1 -1
- package/dist/core/Speak.js +64 -20
- package/dist/core/Speak.js.map +1 -1
- package/dist/core/Understand.d.ts +6 -3
- package/dist/core/Understand.d.ts.map +1 -1
- package/dist/core/Understand.js +18 -42
- package/dist/core/Understand.js.map +1 -1
- package/dist/core/contracts.d.ts +26 -6
- 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/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 +26 -11
- package/dist/providers/ProviderAdapter.d.ts.map +1 -1
- package/dist/providers/ProviderAdapter.js +14 -14
- 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 +14 -3
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/ai.d.ts +10 -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/flow.d.ts +17 -1
- package/dist/types/flow.d.ts.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 +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/session.d.ts +4 -2
- 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/outcomes.d.ts +2 -0
- package/dist/utils/outcomes.d.ts.map +1 -1
- package/dist/utils/outcomes.js +2 -0
- package/dist/utils/outcomes.js.map +1 -1
- package/dist/utils/schema.d.ts +3 -12
- package/dist/utils/schema.d.ts.map +1 -1
- package/dist/utils/schema.js +3 -42
- package/dist/utils/schema.js.map +1 -1
- package/dist/utils/template.d.ts.map +1 -1
- package/dist/utils/template.js +9 -2
- package/dist/utils/template.js.map +1 -1
- package/docs/concepts/architecture.md +2 -2
- package/docs/concepts/collection.md +40 -5
- package/docs/concepts/pipeline.md +10 -7
- package/docs/concepts/runs-and-waits.md +2 -2
- package/docs/guides/actions-and-events.md +1 -1
- package/docs/guides/branching.md +4 -2
- package/docs/guides/compaction.md +2 -2
- package/docs/guides/error-handling.md +2 -2
- package/docs/guides/flow-control.md +4 -2
- package/docs/guides/persistence.md +2 -2
- package/docs/guides/testing.md +1 -1
- package/docs/guides/triggers.md +3 -3
- package/docs/migration/v3-to-v4.md +8 -7
- package/docs/reference/actions-events-conditions.md +1 -1
- package/docs/reference/agent.md +9 -5
- package/docs/reference/branches.md +1 -1
- package/docs/reference/errors.md +14 -11
- package/docs/reference/fields.md +5 -3
- package/docs/reference/flow-spec.md +35 -8
- package/docs/reference/flow.md +8 -3
- package/docs/reference/outcomes.md +4 -2
- package/docs/reference/providers.md +5 -3
- package/docs/reference/session.md +2 -0
- package/docs/reference/step.md +9 -5
- package/docs/reference/stores.md +5 -3
- package/docs/reference/trigger.md +2 -2
- package/docs/rfc/v4-one-flow.md +5 -3
- package/docs/start/01-install.md +2 -0
- package/docs/start/05-go-to-production.md +18 -1
- package/examples/05-branches.ts +1 -1
- package/examples/06-triggers-and-waits.ts +4 -3
- package/package.json +4 -3
- package/src/core/Agent.ts +23 -2
- package/src/core/CompactionEngine.ts +25 -9
- package/src/core/FlowSpec.ts +311 -72
- package/src/core/Migrate.ts +2 -1
- package/src/core/Prompt.ts +40 -0
- package/src/core/Runner.ts +279 -69
- package/src/core/Speak.ts +67 -19
- package/src/core/Understand.ts +24 -44
- package/src/core/contracts.ts +26 -4
- package/src/index.ts +1 -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 +35 -33
- package/src/providers/ZaiProvider.ts +1 -1
- package/src/types/agent.ts +15 -4
- package/src/types/ai.ts +10 -6
- package/src/types/compaction.ts +1 -0
- package/src/types/errors.ts +11 -12
- package/src/types/flow.ts +17 -1
- package/src/types/history.ts +0 -10
- package/src/types/index.ts +1 -1
- package/src/types/session.ts +5 -1
- package/src/utils/clock.ts +1 -1
- package/src/utils/outcomes.ts +2 -0
- package/src/utils/schema.ts +3 -48
- package/src/utils/template.ts +9 -2
- 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
|
@@ -44,7 +44,7 @@ import type {
|
|
|
44
44
|
import type { StructuredSchema } from "../types/schema.js";
|
|
45
45
|
import { isDuration } from "../utils/duration.js";
|
|
46
46
|
import { splitPhrases } from "../utils/phrases.js";
|
|
47
|
-
import { toWireSchema } from "../utils/schema.js";
|
|
47
|
+
import { extractMode, toWireSchema } from "../utils/schema.js";
|
|
48
48
|
|
|
49
49
|
// ── The JSON form ───────────────────────────────────────────────────────
|
|
50
50
|
|
|
@@ -77,7 +77,7 @@ interface TalkSpecExtras {
|
|
|
77
77
|
export type StepSpec = StepBase<LooseData> &
|
|
78
78
|
(
|
|
79
79
|
| ({ kind: "prompt"; prompt: Template; collect?: undefined } & TalkSpecExtras)
|
|
80
|
-
| ({ kind: "collect"; collect: string[]; prompt?: Template } & TalkSpecExtras)
|
|
80
|
+
| ({ kind: "collect"; collect: string[]; prompt?: Template; question?: Template } & TalkSpecExtras)
|
|
81
81
|
| ({ kind: "say" } & SayStep)
|
|
82
82
|
| ({ kind: "do" } & DoStep<LooseData>)
|
|
83
83
|
| { kind: "wait"; wait: Duration; businessHours?: boolean; else?: Next<LooseData>; branches?: BranchSpec[] }
|
|
@@ -92,6 +92,7 @@ export interface FlowSpec {
|
|
|
92
92
|
on?: TriggerSpec[];
|
|
93
93
|
anchor?: string;
|
|
94
94
|
while?: ConditionSpec<LooseData>;
|
|
95
|
+
collect?: string[];
|
|
95
96
|
clearOnStart?: string[];
|
|
96
97
|
steps: StepSpec[];
|
|
97
98
|
onEnd?: "end" | "stay" | "reset";
|
|
@@ -99,8 +100,14 @@ export interface FlowSpec {
|
|
|
99
100
|
tools?: string[];
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
/**
|
|
103
|
-
|
|
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
|
+
};
|
|
104
111
|
|
|
105
112
|
// ── fromSpec / toSpec ───────────────────────────────────────────────────
|
|
106
113
|
|
|
@@ -112,9 +119,7 @@ export type Registries = Pick<AgentOptions, "fields" | "actions" | "events" | "c
|
|
|
112
119
|
*/
|
|
113
120
|
export function fromSpec<C = unknown, D = LooseData>(spec: FlowSpec): Flow<C, D> {
|
|
114
121
|
const clean = stripNulls(spec);
|
|
115
|
-
|
|
116
|
-
throw problem(`flow "${clean.id}"`, "has no steps list", "Write steps as a list, even an empty one.");
|
|
117
|
-
}
|
|
122
|
+
checkShape(clean);
|
|
118
123
|
const { steps, ...rest } = clean;
|
|
119
124
|
const flow: Flow<unknown, LooseData> = { ...rest, steps: steps.map(fromStepSpec) };
|
|
120
125
|
return flow as Flow<C, D>;
|
|
@@ -136,6 +141,7 @@ export function toSpec<C, D extends LooseData>(flow: Flow<C, D>): FlowSpec {
|
|
|
136
141
|
on: flow.on?.map((trigger, i) => triggerToSpec(trigger, at(`trigger #${i + 1}`))),
|
|
137
142
|
anchor: flow.anchor,
|
|
138
143
|
while: jsonPred(flow.while, at("while")),
|
|
144
|
+
collect: flow.collect,
|
|
139
145
|
clearOnStart: flow.clearOnStart,
|
|
140
146
|
steps: flow.steps.map((step) => stepToSpec(step, at(`step "${step.id}"`))),
|
|
141
147
|
onEnd: flow.onEnd,
|
|
@@ -189,11 +195,12 @@ function stepToSpec<C, D extends LooseData>(step: Step<C, D>, at: string): StepS
|
|
|
189
195
|
instructions: step.instructions?.map((ins, i) => instructionToSpec(ins, `${at} instructions[${i}]`)),
|
|
190
196
|
};
|
|
191
197
|
if (step.collect !== undefined) {
|
|
192
|
-
return compact<StepSpec>({ ...base, kind: "collect", collect: step.collect, prompt: step.prompt, ...talk });
|
|
198
|
+
return compact<StepSpec>({ ...base, kind: "collect", collect: step.collect, prompt: step.prompt, question: step.question, ...talk });
|
|
193
199
|
}
|
|
194
200
|
if (step.prompt === undefined) {
|
|
195
201
|
throw problem(at, "has neither prompt nor collect", "A talk step needs a guideline, fields to collect, or both.");
|
|
196
202
|
}
|
|
203
|
+
if (step.question !== undefined) throw questionWithoutCollect(at);
|
|
197
204
|
return compact<StepSpec>({ ...base, kind: "prompt", prompt: step.prompt, ...talk });
|
|
198
205
|
}
|
|
199
206
|
|
|
@@ -248,6 +255,7 @@ interface LooseStep {
|
|
|
248
255
|
onFail?: Next<LooseData>;
|
|
249
256
|
prompt?: Template;
|
|
250
257
|
collect?: string[];
|
|
258
|
+
question?: Template;
|
|
251
259
|
ask?: Partial<Record<string, string>>;
|
|
252
260
|
branches?: LooseBranch[];
|
|
253
261
|
instructions?: LooseInstruction[];
|
|
@@ -262,21 +270,23 @@ interface LooseFlow {
|
|
|
262
270
|
id: string;
|
|
263
271
|
on?: LooseTrigger[];
|
|
264
272
|
while?: LoosePred;
|
|
273
|
+
collect?: string[];
|
|
265
274
|
clearOnStart?: string[];
|
|
266
275
|
steps: LooseStep[];
|
|
267
276
|
instructions?: LooseInstruction[];
|
|
268
277
|
tools?: string[];
|
|
269
278
|
}
|
|
270
279
|
|
|
271
|
-
const BUILT_IN_CONDITIONS = ["equals", "known", "silenced"];
|
|
280
|
+
export const BUILT_IN_CONDITIONS = ["equals", "known", "silenced"];
|
|
272
281
|
|
|
273
282
|
const DURATION_HINT = 'Write a number and a unit: "30s", "5m", "24h" or "3d".';
|
|
274
283
|
|
|
275
284
|
/** The four keys one of which makes an `on[]` entry a trigger. */
|
|
276
285
|
const TRIGGER_KINDS = ["message", "mention", "silence", "event"] as const;
|
|
277
286
|
|
|
278
|
-
|
|
279
|
-
|
|
287
|
+
const ON_END = ["end", "stay", "reset"];
|
|
288
|
+
|
|
289
|
+
const INSTRUCTION_KINDS = ["must", "never", "should"];
|
|
280
290
|
|
|
281
291
|
/**
|
|
282
292
|
* Check a flow, typed or as a spec, against the agent's registries. Throws
|
|
@@ -289,7 +299,8 @@ export function validateFlow<C = unknown, D = LooseData>(
|
|
|
289
299
|
): { warnings: string[] } {
|
|
290
300
|
// Nulls mean "not set" in a spec; a typed flow has none, so one pass serves both forms.
|
|
291
301
|
const flow: LooseFlow = stripNulls(input);
|
|
292
|
-
|
|
302
|
+
checkShape(flow);
|
|
303
|
+
const { fields, actions = {}, events = {}, tools = [] } = registries;
|
|
293
304
|
const toolIds = new Set(tools.map((tool) => tool.id));
|
|
294
305
|
const warnings: string[] = [];
|
|
295
306
|
|
|
@@ -297,9 +308,6 @@ export function validateFlow<C = unknown, D = LooseData>(
|
|
|
297
308
|
throw problem("flow", "has no id", "Give the flow a short unique id.");
|
|
298
309
|
}
|
|
299
310
|
const flowAt = `flow "${flow.id}"`;
|
|
300
|
-
if (!Array.isArray(flow.steps)) {
|
|
301
|
-
throw problem(flowAt, "has no steps list", "Write steps as a list, even an empty one.");
|
|
302
|
-
}
|
|
303
311
|
|
|
304
312
|
const index = new Map<string, number>();
|
|
305
313
|
flow.steps.forEach((step, i) => {
|
|
@@ -318,11 +326,7 @@ export function validateFlow<C = unknown, D = LooseData>(
|
|
|
318
326
|
throw problem(flowAt, "has triggers but no steps", "Add at least one step or remove `on`.");
|
|
319
327
|
}
|
|
320
328
|
|
|
321
|
-
const slug = (name: string, at: string, where: string): void =>
|
|
322
|
-
if (!own(fields, name)) {
|
|
323
|
-
throw problem(at, `unknown field "${name}" in ${where}`, "Add it to the agent's fields or fix the slug.");
|
|
324
|
-
}
|
|
325
|
-
};
|
|
329
|
+
const slug = (name: string, at: string, where: string): void => checkSlug(fields, name, at, where);
|
|
326
330
|
|
|
327
331
|
const toolNames = (names: string[] | undefined, at: string): void => {
|
|
328
332
|
for (const name of names ?? []) {
|
|
@@ -330,38 +334,7 @@ export function validateFlow<C = unknown, D = LooseData>(
|
|
|
330
334
|
}
|
|
331
335
|
};
|
|
332
336
|
|
|
333
|
-
const pred = (value: LoosePred | undefined, at: string, where: string): void =>
|
|
334
|
-
if (value === undefined || typeof value === "function") return;
|
|
335
|
-
for (const [name, arg] of Object.entries(value)) {
|
|
336
|
-
if (name === "equals") {
|
|
337
|
-
if (arg === null || typeof arg !== "object" || Array.isArray(arg)) {
|
|
338
|
-
throw problem(at, `${where}.equals is not an object`, "Write equals as { field: value }.");
|
|
339
|
-
}
|
|
340
|
-
for (const [field, given] of Object.entries(arg)) {
|
|
341
|
-
slug(field, at, `${where}.equals`);
|
|
342
|
-
const def = fields[field];
|
|
343
|
-
if (!matches(def, given)) {
|
|
344
|
-
throw problem(
|
|
345
|
-
at,
|
|
346
|
-
`${where}.equals gives "${field}" a ${describe(given)}, but the field is a ${def.type}`,
|
|
347
|
-
`Write a ${def.type}; values are not coerced.`,
|
|
348
|
-
);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
} else if (name === "known") {
|
|
352
|
-
if (!Array.isArray(arg)) throw problem(at, `${where}.known is not a list`, "Write known as [field, ...].");
|
|
353
|
-
for (const field of arg) slug(String(field), at, `${where}.known`);
|
|
354
|
-
} else if (name === "silenced") {
|
|
355
|
-
if (typeof arg !== "boolean") throw problem(at, `${where}.silenced is not a boolean`, "Write true or false.");
|
|
356
|
-
} else if (!own(conditions, name)) {
|
|
357
|
-
throw problem(
|
|
358
|
-
at,
|
|
359
|
-
`unknown condition "${name}" in ${where}`,
|
|
360
|
-
`Register it in conditions or use ${BUILT_IN_CONDITIONS.join(", ")}.`,
|
|
361
|
-
);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
};
|
|
337
|
+
const pred = (value: LoosePred | undefined, at: string, where: string): void => checkPred(value, at, where, registries);
|
|
365
338
|
|
|
366
339
|
const duration = (value: string | undefined, at: string, where: string): void => {
|
|
367
340
|
if (value !== undefined && !isDuration(value)) {
|
|
@@ -379,7 +352,16 @@ export function validateFlow<C = unknown, D = LooseData>(
|
|
|
379
352
|
}
|
|
380
353
|
return to;
|
|
381
354
|
}
|
|
382
|
-
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
|
+
}
|
|
383
365
|
const to = index.get(next.step);
|
|
384
366
|
if (to === undefined) {
|
|
385
367
|
throw problem(at, `${where} points at step "${next.step}", which does not exist`, 'Use an existing step id or "end".');
|
|
@@ -410,11 +392,8 @@ export function validateFlow<C = unknown, D = LooseData>(
|
|
|
410
392
|
continue;
|
|
411
393
|
}
|
|
412
394
|
if (!matchesParam(def, value)) {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
`parameter "${param}" of action "${name}" must be ${describeDef(def)}, got ${describe(value)}`,
|
|
416
|
-
"Values are not coerced; write the right type.",
|
|
417
|
-
);
|
|
395
|
+
const { expected, got, fix } = mismatch(def, value);
|
|
396
|
+
throw problem(at, `parameter "${param}" of action "${name}" must be ${expected}, got ${got}`, fix);
|
|
418
397
|
}
|
|
419
398
|
}
|
|
420
399
|
for (const param of Object.keys(given)) {
|
|
@@ -424,7 +403,18 @@ export function validateFlow<C = unknown, D = LooseData>(
|
|
|
424
403
|
}
|
|
425
404
|
};
|
|
426
405
|
|
|
406
|
+
for (const field of flow.collect ?? []) slug(field, flowAt, "collect");
|
|
427
407
|
for (const field of flow.clearOnStart ?? []) slug(field, flowAt, "clearOnStart");
|
|
408
|
+
// A field taken only from the answer to a step that asks it can never be filled when no step asks it.
|
|
409
|
+
const asked = new Set(flow.steps.flatMap((step) => step.collect ?? []));
|
|
410
|
+
for (const field of flow.collect ?? []) {
|
|
411
|
+
if (!asked.has(field) && extractMode(fields[field]) === "asked") {
|
|
412
|
+
warnings.push(
|
|
413
|
+
`${flowAt}: collect lists "${field}", which is only taken from the answer to a step that asks it, and no ` +
|
|
414
|
+
"step does. Add it to a step's collect, or set extract: 'anywhere' on the field.",
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
428
418
|
pred(flow.while, flowAt, "while");
|
|
429
419
|
toolNames(flow.tools, flowAt);
|
|
430
420
|
flow.instructions?.forEach((ins, i) => pred(ins.if, flowAt, `instructions[${i}].if`));
|
|
@@ -469,25 +459,20 @@ export function validateFlow<C = unknown, D = LooseData>(
|
|
|
469
459
|
|
|
470
460
|
flow.steps.forEach((step, i) => {
|
|
471
461
|
const at = `${flowAt}, step "${step.id}"`;
|
|
472
|
-
// Same reasoning as the trigger above: a step that does none of the five
|
|
473
|
-
// things is a step the run walks straight past, silently. `kind` alone is
|
|
474
|
-
// not enough — the spec form drops it and keeps the body, so what counts
|
|
475
|
-
// is whether the body says what to do.
|
|
476
|
-
if (!STEP_DOES.some((key) => step[key] !== undefined)) {
|
|
477
|
-
throw problem(
|
|
478
|
-
at,
|
|
479
|
-
"does nothing",
|
|
480
|
-
"A step talks (`prompt` / `collect`), says (`say`), acts (`do`), waits (`wait`) or forks (`if`).",
|
|
481
|
-
);
|
|
482
|
-
}
|
|
483
462
|
const thenTo = edge(i, step.then, at, "then");
|
|
484
463
|
edge(i, step.else, at, "else");
|
|
485
464
|
|
|
486
465
|
if (step.collect !== undefined || step.prompt !== undefined) {
|
|
487
466
|
for (const field of step.collect ?? []) slug(field, at, "collect");
|
|
488
467
|
for (const field of Object.keys(step.ask ?? {})) slug(field, at, "ask");
|
|
468
|
+
if (step.question !== undefined && !step.collect?.length) throw questionWithoutCollect(at);
|
|
489
469
|
const fields_ = step.collect ?? [];
|
|
490
|
-
if (
|
|
470
|
+
if (
|
|
471
|
+
step.prompt === undefined &&
|
|
472
|
+
step.question === undefined &&
|
|
473
|
+
fields_.length > 0 &&
|
|
474
|
+
fields_.every((f) => !step.ask?.[f] && !fields[f].ask)
|
|
475
|
+
) {
|
|
491
476
|
warnings.push(
|
|
492
477
|
`${at}: collects ${fields_.map((f) => `"${f}"`).join(", ")} with no prompt and no ask; the AI has nothing ` +
|
|
493
478
|
"to go on. Add a prompt or an ask per field.",
|
|
@@ -499,6 +484,14 @@ export function validateFlow<C = unknown, D = LooseData>(
|
|
|
499
484
|
if (branch.when === undefined && branch.if === undefined) {
|
|
500
485
|
throw problem(at, `${where} has neither when nor if`, "Give the branch an AI condition (when) or a code one (if).");
|
|
501
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
|
+
}
|
|
502
495
|
pred(branch.if, at, `${where}.if`);
|
|
503
496
|
edge(i, branch.then, at, `${where}.then`);
|
|
504
497
|
});
|
|
@@ -535,6 +528,228 @@ export function validateFlow<C = unknown, D = LooseData>(
|
|
|
535
528
|
return { warnings };
|
|
536
529
|
}
|
|
537
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
|
+
|
|
538
753
|
function matchesParam(def: ParamDef, value: unknown): boolean {
|
|
539
754
|
if (def.type === "array") return Array.isArray(value) && value.every((item) => matches(def.items, item));
|
|
540
755
|
return matches(def, value);
|
|
@@ -554,6 +769,22 @@ function matches(def: ScalarDef, value: unknown): boolean {
|
|
|
554
769
|
return typeof value !== "boolean" && def.enum.includes(value);
|
|
555
770
|
}
|
|
556
771
|
|
|
772
|
+
/**
|
|
773
|
+
* What a rejected value should have been and what it was. A value of the right type that is
|
|
774
|
+
* not a listed one names the listed values: "must be a string, got string" would say nothing.
|
|
775
|
+
*/
|
|
776
|
+
function mismatch(def: ParamDef, value: unknown): { listed: boolean; expected: string; got: string; fix: string } {
|
|
777
|
+
const scalar = def.type === "array" ? def.items : def;
|
|
778
|
+
const { enum: allowed, ...typeOnly } = scalar;
|
|
779
|
+
const items = def.type === "array" && Array.isArray(value) ? value : [value];
|
|
780
|
+
const off = allowed ? items.findIndex((item) => matches(typeOnly, item) && !matches(scalar, item)) : -1;
|
|
781
|
+
if (!allowed || off === -1) {
|
|
782
|
+
return { listed: false, expected: describeDef(def), got: describe(value), fix: "Values are not coerced; write the right type." };
|
|
783
|
+
}
|
|
784
|
+
const list = (v: unknown) => JSON.stringify(v);
|
|
785
|
+
return { listed: true, expected: `one of ${allowed.map(list).join(", ")}`, got: list(items[off]), fix: "Use one of the listed values." };
|
|
786
|
+
}
|
|
787
|
+
|
|
557
788
|
function describe(value: unknown): string {
|
|
558
789
|
return Array.isArray(value) ? "list" : value === null ? "null" : typeof value;
|
|
559
790
|
}
|
|
@@ -633,6 +864,8 @@ export function flowSpecSchema(registries: Registries): StructuredSchema {
|
|
|
633
864
|
const branches = orNull(
|
|
634
865
|
list(union([closed({ when: STRING, then: next }), closed({ if: condition, then: next })]), "Exits judged while the step asks"),
|
|
635
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"));
|
|
636
869
|
const stepBase = { id: STRING, label: orNull(STRING), then: nextOrNull };
|
|
637
870
|
const step = union([
|
|
638
871
|
closed({ ...stepBase, kind: enumOf(["prompt"]), prompt: { type: "string", description: "Guideline for the AI's next message" }, branches }),
|
|
@@ -642,6 +875,7 @@ export function flowSpecSchema(registries: Registries): StructuredSchema {
|
|
|
642
875
|
kind: enumOf(["collect"]),
|
|
643
876
|
collect: { ...slugList, description: "Fields the AI asks for until they are known" },
|
|
644
877
|
prompt: orNull(STRING),
|
|
878
|
+
question: orNull({ type: "string", description: "A fixed first question, sent word for word; later asks are the AI's" }),
|
|
645
879
|
maxAsks: orNull({ ...INTEGER, description: "Times a field may be asked before it is skipped; default 3" }),
|
|
646
880
|
branches,
|
|
647
881
|
}),
|
|
@@ -661,7 +895,7 @@ export function flowSpecSchema(registries: Registries): StructuredSchema {
|
|
|
661
895
|
wait: { ...duration, description: "Park this long; then = time passed, else = the lead replied" },
|
|
662
896
|
businessHours: orNull(BOOLEAN),
|
|
663
897
|
else: nextOrNull,
|
|
664
|
-
branches,
|
|
898
|
+
branches: waitBranches,
|
|
665
899
|
}),
|
|
666
900
|
eventNames.length
|
|
667
901
|
? closed({
|
|
@@ -687,9 +921,10 @@ export function flowSpecSchema(registries: Registries): StructuredSchema {
|
|
|
687
921
|
on: orNull(list(trigger, "What starts a run; null = started by hand")),
|
|
688
922
|
anchor: orNull({ type: "string", description: "'session' (default) or a host anchor such as 'lead'" }),
|
|
689
923
|
while: orNull({ ...condition, description: "The run ends when this stops holding" }),
|
|
924
|
+
collect: slugList && orNull({ ...slugList, description: "The data this flow needs; its steps ask for it in order, and any of it the lead gives is noted" }),
|
|
690
925
|
clearOnStart: slugList && orNull({ ...slugList, description: "Fields to forget when a run starts" }),
|
|
691
926
|
steps: list(step, "In order; a run moves to the next step unless `then` says otherwise"),
|
|
692
|
-
onEnd: orNull(enumOf(["end", "stay", "reset"], "After the last step: end the run, stay on it, or reset to the first")),
|
|
927
|
+
onEnd: orNull(enumOf(["end", "stay", "reset"], "After the last step: end the run, stay on the last talk step it took answering every message, or reset to the first")),
|
|
693
928
|
instructions: orNull(list(instruction, "Rules that apply only inside this flow")),
|
|
694
929
|
});
|
|
695
930
|
}
|
|
@@ -738,6 +973,10 @@ function orNull(schema: StructuredSchema): StructuredSchema {
|
|
|
738
973
|
|
|
739
974
|
// ── Shared helpers ──────────────────────────────────────────────────────
|
|
740
975
|
|
|
976
|
+
function questionWithoutCollect(at: string): FlowConfigurationError {
|
|
977
|
+
return problem(at, "has a question but collects nothing", "A fixed question asks for fields: add collect, or send the text with a say step.");
|
|
978
|
+
}
|
|
979
|
+
|
|
741
980
|
function problem(at: string, what: string, fix: string): FlowConfigurationError {
|
|
742
981
|
return new FlowConfigurationError(`[FlowConfigurationError] ${at}: ${what}. ${fix}`);
|
|
743
982
|
}
|
package/src/core/Migrate.ts
CHANGED
|
@@ -112,7 +112,7 @@ function checkRun(raw: unknown, index: number, bad: Bad): Run {
|
|
|
112
112
|
|
|
113
113
|
const status = text("status");
|
|
114
114
|
if (!RUN_STATUS.has(status)) throw bad(`${at}.status is "${status}"`);
|
|
115
|
-
const { stepId, hop, outcomes, input, waiting, suspendedAt } = raw;
|
|
115
|
+
const { stepId, hop, outcomes, input, waiting, suspendedAt, staying } = raw;
|
|
116
116
|
if (stepId !== null && typeof stepId !== "string") throw bad(`${at}.stepId is ${describe(stepId)}, expected text or null`);
|
|
117
117
|
if (!isWhole(hop)) throw bad(`${at}.hop is ${describe(hop)}, expected a whole number`);
|
|
118
118
|
if (!Array.isArray(outcomes)) throw bad(`${at}.outcomes is ${describe(outcomes)}, expected a list`);
|
|
@@ -139,6 +139,7 @@ function checkRun(raw: unknown, index: number, bad: Bad): Run {
|
|
|
139
139
|
if (input !== undefined) run.input = input;
|
|
140
140
|
if (waiting !== undefined) run.waiting = waiting as Run["waiting"];
|
|
141
141
|
if (typeof suspendedAt === "string") run.suspendedAt = suspendedAt;
|
|
142
|
+
if (staying === true) run.staying = true;
|
|
142
143
|
return run;
|
|
143
144
|
}
|
|
144
145
|
|
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
|
+
}
|