@falai/agent 4.0.0-alpha.9 → 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 +282 -60
- 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 +281 -61
- 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 +296 -70
- 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
|
@@ -68,7 +68,7 @@ Every start, whatever the trigger, goes through the same checks in `Runner.start
|
|
|
68
68
|
At most one run in a session is `asking`. That run holds the floor: its talk step spoke last, and the next message is read as its answer.
|
|
69
69
|
|
|
70
70
|
- A talk step reached by any other run suspends the asker (`status: 'suspended'`, `suspendedAt: now`) and takes the floor. A follow-up nudge that fires while triage is mid-question does exactly this.
|
|
71
|
-
- Whenever nobody is asking, at the start of phase 5 and again after Speak, the **most recently suspended** run returns to asking. It is a stack: `suspendedAt` decides, not `startedAt`. `tests/runner.test.ts` ("the floor") pins this with three runs.
|
|
71
|
+
- Whenever nobody is asking, at the start of phase 5 and again after Speak, the **most recently suspended** run returns to asking. On a message, it also happens in phase 5 once every run has moved, when nothing has answered yet; the resumed run then answers it, or the next one down the stack does if it too moves on without a word. It is a stack: `suspendedAt` decides, not `startedAt`. `tests/runner.test.ts` ("the floor") pins this with three runs.
|
|
72
72
|
- An asking run re-speaks only on a message. Wakes and events leave it alone.
|
|
73
73
|
- On a message, routing may move the floor to another flow: it needs a score of at least 40 and at least 15 above the asker's. Then that flow's suspended run resumes, or a new run starts. Routing never takes the floor from a run that took it in Ingest (a resolved wait, a wake). [Triggers](../guides/triggers.md) has the routing rules.
|
|
74
74
|
- One answer per message. When a run other than the floor holder answered this turn (a `say`, or a `do` returning `spoke: true`), the floor's talk is skipped (`code: 'another-reply'`) and the asker stays asking.
|
|
@@ -81,7 +81,7 @@ A `wait` step parks the run. There are two kinds.
|
|
|
81
81
|
|
|
82
82
|
- **Ten seconds or less**, when the next step is a `say` or a talk step: no wake at all. The delay rides on that message as `afterMs` (`wait: '3s'` gives `afterMs: 3000`) and the run keeps moving. Any other short wait behaves like a long one.
|
|
83
83
|
- **Longer**: the run parks with `waiting: { kind: 'timer', key, until, setAt }`, and `schedule[]` gets `{ key, at }`. `businessHours: true` moves `at` forward to the next open hour, using the agent's `businessHours` function.
|
|
84
|
-
- A message, or an inbound event, while parked: every timer wait with an `else` takes it at Ingest, outcome `code: 'replied'`. An `if` branch on the wait step is checked first and wins over `else`; `when` branches on a wait step are not judged.
|
|
84
|
+
- A message, or an inbound event, while parked: every timer wait with an `else` takes it at Ingest, outcome `code: 'replied'`. An `if` branch on the wait step is checked first and wins over `else`; `when` branches on a wait step are not judged, and `validateFlow` warns about them.
|
|
85
85
|
- The wake fires: `code: 'no-reply'`, `then`. Unless the customer wrote after `waiting.setAt` and the step has an `else`: then `code: 'replied'`, `else`. The reply beat the job.
|
|
86
86
|
|
|
87
87
|
**Event**: `{ wait: { event: 'meeting_booked', upTo?: '7d' }, else? }`. The event arrives: `code: 'event-arrived'`, `then`. `upTo` passes (default 30 days): `code: 'no-event'`, `else`, or the run ends when there is no `else`.
|
|
@@ -111,7 +111,7 @@ const enviarTemplate = f.action({
|
|
|
111
111
|
});
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
**`defer`** is for "not now": no credits, a rate limit, a window that is closed. The run parks under the wake key `<runId>:<stepId>:<atMs>` and the host schedules it like any other wake. When it fires, the step runs again at the same visit, so `ctx.key` is the same. The outcome carries your `detail` and the `until` time.
|
|
114
|
+
**`defer`** is for "not now": no credits, a rate limit, a window that is closed. The run parks under the wake key `<runId>:<stepId>:<atMs>` and the host schedules it like any other wake. When it fires, the step runs again at the same visit, so `ctx.key` is the same. The outcome carries your `detail` and the `until` time. A `defer` that is not a duration (`"2 minutos"`) cannot park anything, so the step fails instead, with `code: 'action-failed'` and a `detail` that quotes the value; the handler has already run, so throwing would make every replay repeat it.
|
|
115
115
|
|
|
116
116
|
**`spoke: true`** says the action itself sent something to the customer, a template through the channel for example. The framework then treats the turn as the assistant having spoken: `lastAssistantAt` is stamped and silence flows are armed. On a message turn, it also means another run's talk step does not speak (`code: 'another-reply'`): one answer per message.
|
|
117
117
|
|
package/docs/guides/branching.md
CHANGED
|
@@ -64,8 +64,8 @@ console.log(t2.messages.map((m) => m.text)); // ["Claro, vou chamar alguém da e
|
|
|
64
64
|
|
|
65
65
|
`branches` is allowed on two step kinds:
|
|
66
66
|
|
|
67
|
-
- A talk step that collects (`collect`, with or without a `prompt`). Both `when` and `if` branches work while it asks. A `prompt` step with no `collect` speaks once and moves on in the same turn, so its branches are only judged in the rare turn where it is still asking: the
|
|
68
|
-
- A timer `wait` step (`wait: '2d'`). Only `if` branches are judged there.
|
|
67
|
+
- A talk step that collects (`collect`, with or without a `prompt`). Both `when` and `if` branches work while it asks. A `prompt` step with no `collect` speaks once and moves on in the same turn, so its branches are only judged in the rare turn where it is still asking: the talk step an `onEnd: 'stay'` flow stays on, or a turn where another run answered the customer first. On the step a run stays on, an `if` branch that leads to `'end'` or to a step the run has already been through is not taken: that path already ran, and the fact would still hold on every message. Write a restart there as a `when`.
|
|
68
|
+
- A timer `wait` step (`wait: '2d'`). Only `if` branches are judged there; `validateFlow` rejects a `when` branch on a wait.
|
|
69
69
|
|
|
70
70
|
`say`, `do`, `if` and `wait: { event }` steps have no branches. A code fork between them is an `if` step.
|
|
71
71
|
|
|
@@ -79,6 +79,8 @@ console.log(t2.messages.map((m) => m.text)); // ["Claro, vou chamar alguém da e
|
|
|
79
79
|
|
|
80
80
|
If no branch holds, the step carries on: it speaks again with what is still pending, or completes when its fields are known.
|
|
81
81
|
|
|
82
|
+
A suspended run that gets the conversation back in the middle of a turn, because the run that was asking finished without a word, is checked the same way before it speaks, but only its `if` branches: the model was not asked about its `when` branches this turn.
|
|
83
|
+
|
|
82
84
|
**On a `wait` step**, the branches are judged only when the customer replies while the run is parked, which is also when `else` applies. The first `if` branch that holds wins over `else`. A `wait` with no `else` ignores the reply, branches included. When the timer fires, branches are not consulted: the run takes `then`, or `else` if the customer wrote after the wait was set. The outcome line reads `code: 'replied'` or `code: 'no-reply'`.
|
|
83
85
|
|
|
84
86
|
```ts
|
|
@@ -34,13 +34,13 @@ With `maxTokens: 2000` the turn compacts when the history is estimated at 1600 t
|
|
|
34
34
|
|
|
35
35
|
| Field | Meaning | Default |
|
|
36
36
|
|---|---|---|
|
|
37
|
-
| `maxTokens` | the token budget for the history | required |
|
|
37
|
+
| `maxTokens` | the token budget for the history; more than 0 | required |
|
|
38
38
|
| `compactionThreshold` | compact when the estimate reaches this share of `maxTokens`; between 0.5 and 0.95 | `0.8` |
|
|
39
39
|
| `preserveRecentCount` | the newest messages are never changed or removed; at least 2 | `4` |
|
|
40
40
|
| `maxToolResultChars` | characters kept of a tool result before it is cut; more than 0 | `5000` |
|
|
41
41
|
| `enabled` | `false` turns compaction off without removing the config | `true` when the config is present |
|
|
42
42
|
|
|
43
|
-
A value out of range throws at construction, as a plain `Error
|
|
43
|
+
A value out of range throws at construction, as a plain `Error` that names the value and the fix, for example `[CompactionEngine] compactionThreshold is 2: it must be between 0.5 and 0.95. Use 0.8 unless you measured otherwise.` The same goes for a `maxTokens` of 0 or less, a `preserveRecentCount` under 2 and a `maxToolResultChars` of 0 or less.
|
|
44
44
|
|
|
45
45
|
## When it runs
|
|
46
46
|
|
|
@@ -76,7 +76,7 @@ The speak call phrases the reply. If the provider fails here, or answers with an
|
|
|
76
76
|
| A spent balance with no stated reset | `provider-quota` | Ends `failed` |
|
|
77
77
|
| The key was rejected or has no access to the model | `provider-auth` | Ends `failed` |
|
|
78
78
|
| The prompt is past the model's context window | `provider-context` | Ends `failed` |
|
|
79
|
-
| The provider refused the request,
|
|
79
|
+
| The provider refused the request, the model does not exist, or the model spent all of `maxTokens` thinking and never answered | `provider-invalid` | Ends `failed` |
|
|
80
80
|
|
|
81
81
|
A failure that waits returns with an outcome `{ kind: "prompt" | "collect", status: "deferred", code, until }`, a `schedule[]` entry keyed `${runId}:${stepId}:${visit}:retry:${atMs}`, and `llmCalls` counting the call that failed. The backoff is 1 minute, then 5, 15, an hour, six hours (`RETRY_BACKOFF` in `src/core/Runner.ts`); the attempt number is the count of trailing `deferred` outcomes for that step on that run. A provider that stated a reset later than the next rung is woken at the reset instead. When the wake fires, the step runs again at the same visit, so its message carries the same key it would have carried the first time.
|
|
82
82
|
|
|
@@ -150,7 +150,7 @@ Most things that go wrong inside a turn become outcome lines, not exceptions:
|
|
|
150
150
|
|
|
151
151
|
The full detail vocabulary is in [outcomes](../reference/outcomes.md).
|
|
152
152
|
|
|
153
|
-
Two errors outside the four classes: a `compaction` option out of range throws a plain `Error` at construction (`compactionThreshold must be between 0.5 and 0.95
|
|
153
|
+
Two errors outside the four classes: a `compaction` option out of range throws a plain `Error` at construction (`[CompactionEngine] compactionThreshold is 2: it must be between 0.5 and 0.95. Use 0.8 unless you measured otherwise.`), and `PrismaStore` throws a `TypeError` when the client has no model by the given name. [Errors](../reference/errors.md) lists the other wiring errors thrown at construction.
|
|
154
154
|
|
|
155
155
|
## Catching by class
|
|
156
156
|
|
|
@@ -144,14 +144,14 @@ Ends this run with `reason: 'flow'` and starts the other flow in the same turn.
|
|
|
144
144
|
- has `hop` one higher than the parent. A chain deeper than 5 stops: the child is skipped with `code: 'hop-limit'`.
|
|
145
145
|
- repeats by default (`'always'`), so a flow may be chained into many times; its claim carries the parent's step key.
|
|
146
146
|
|
|
147
|
-
`flow` is a template: `{{input.flowId}}` resolves against the run's input and context. A flow id
|
|
147
|
+
`flow` is a template: `{{input.flowId}}` resolves against the run's input and context. A literal flow id the agent does not have logs a warning when the agent is built and is skipped at run time with `code: 'flow-gone'`; a template that resolves to no flow lands in `skipped[]` with `code: 'flow-gone'`; a child flow with a live run for the same anchor is skipped with `code: 'already-running'`. This is how one flow hands the conversation to another: the last step of a qualifying flow can `then: { flow: 'agendamento' }`.
|
|
148
148
|
|
|
149
149
|
## `onEnd`: after the last step
|
|
150
150
|
|
|
151
151
|
| `onEnd` | What happens | `ended[].reason` |
|
|
152
152
|
|---|---|---|
|
|
153
153
|
| `'end'` (default) | the run ends; the session is idle | `'end'` |
|
|
154
|
-
| `'stay'` | the run
|
|
154
|
+
| `'stay'` | the run goes back to the last talk step it took and answers every later message from there, with a new key each time; the steps after that talk step do not run again | none: the run does not end |
|
|
155
155
|
| `'reset'` | the run ends and a fresh run of the same flow starts at the first step, data kept, one hop deeper | `'reset'` |
|
|
156
156
|
|
|
157
157
|
```ts
|
|
@@ -169,6 +169,8 @@ const faq = f.flow({
|
|
|
169
169
|
});
|
|
170
170
|
```
|
|
171
171
|
|
|
172
|
+
The talk step does not have to be the last step, and it does not need anything left to collect. A flow that asks for the name, tells the team, and then keeps talking is `steps: [{ id: "quem", collect: ["nome"] }, { id: "avisa", do: "notify" }]` with `onEnd: "stay"`: `avisa` runs once, then `quem` answers every message, even though the name is known. A `say` or a `do` that returned `spoke: true` on the way to the end counts as the answer to that message, so the step waits for the next one. A flow with no talk step ends, as with `'end'`.
|
|
173
|
+
|
|
172
174
|
`'reset'` is a chain into the same flow, so it costs a hop: a flow with no talk step that resets forever stops at the hop cap instead of spinning.
|
|
173
175
|
|
|
174
176
|
## `while`: the run's premise
|
|
@@ -94,7 +94,7 @@ console.log(outbox[0]?.text, (await store.load("s1"))?.version); // "…", 1
|
|
|
94
94
|
|
|
95
95
|
Three attempts is enough: a fourth conflict means two workers are firing on the same session, which is a queue problem, not a race.
|
|
96
96
|
|
|
97
|
-
Three things happen after the save and never before it: messages go out (honouring `afterMs`, keyed by `key`), `schedule[]` entries go into your queue with `
|
|
97
|
+
Three things happen after the save and never before it: messages go out (honouring `afterMs`, keyed by `key`), `schedule[]` entries go into your queue with the key in the payload and `encodeURIComponent(key)` as the job id (BullMQ refuses a `:` in a custom id, and every key has one), and at fire time you call `turn({ wake: key })` through this same loop. `changed: false` means the input changed nothing (a stale wake, a repeated message id): skip the save and the send.
|
|
98
98
|
|
|
99
99
|
## What a row holds
|
|
100
100
|
|
|
@@ -156,7 +156,7 @@ const viaRedis = new RedisStore({ redis, keyPrefix: "agent:", sessionTTL: 7 * 24
|
|
|
156
156
|
const viaMongo = new MongoStore({ client: mongo, databaseName: "app" });
|
|
157
157
|
const sqlite = new SQLiteStore({ db });
|
|
158
158
|
await sqlite.initialize();
|
|
159
|
-
const search = new OpenSearchStore(opensearch,
|
|
159
|
+
const search = new OpenSearchStore({ client: opensearch, refresh: "wait_for" });
|
|
160
160
|
await search.initialize();
|
|
161
161
|
|
|
162
162
|
console.log([postgres, viaPrisma, viaRedis, viaMongo, sqlite, search].length); // 6
|
package/docs/guides/testing.md
CHANGED
|
@@ -218,7 +218,7 @@ What each assertion pins down:
|
|
|
218
218
|
|
|
219
219
|
- `llmCalls` is the budget. A turn that spends more than you scripted throws inside the provider, so an unexpected third call cannot pass silently. `usage` is absent in tests unless your scripted provider reports token counts — real providers do, and the turn adds them up.
|
|
220
220
|
- `messages[].key` is `${runId}:${stepId}:${visit}`, and `runId` is `${flowId}#${triggerKey}`. The trigger key of a message turn is the message `id` you passed; of a silence wake, the `lastAssistantAt` timestamp in milliseconds.
|
|
221
|
-
- `schedule[].key` is what your queue
|
|
221
|
+
- `schedule[].key` is what your queue job carries (its id is the key encoded, since BullMQ refuses a `:` in one) and what you pass back as `wake`. The silence key is `silence:${flowId}:${sessionId}:${ms}`; a timer wait's key is `${runId}:${stepId}:${atMs}`.
|
|
222
222
|
- `outcomes[]` is the execution log, one line per step. Assert on `code`, which is stable across versions, not on `message`, the English sentence beside it; see [outcomes](../reference/outcomes.md).
|
|
223
223
|
|
|
224
224
|
## Replaying the same input
|
package/docs/guides/triggers.md
CHANGED
|
@@ -181,7 +181,7 @@ How it works, in order:
|
|
|
181
181
|
|
|
182
182
|
1. The assistant speaks: a talk step, a `say`, or an action that returns `spoke: true`. The turn stamps `session.lastAssistantAt`.
|
|
183
183
|
2. For every silence flow whose `if` holds and whose `repeat` allows, the turn puts a wake in `schedule[]`: key `silence:<flowId>:<sessionId>:<lastAssistantAtMs>`, `at` = that time plus the duration. `replaces` names the previous silence key of the same flow so the host can drop the old job. Dropping it is best effort; a stale wake is harmless.
|
|
184
|
-
3. The host enqueues the wake with
|
|
184
|
+
3. The host enqueues the wake with the key in the payload and `encodeURIComponent(key)` as the job id, because BullMQ refuses a `:` in a custom id, and calls `turn({ wake: key })` when it fires.
|
|
185
185
|
4. The wake is honoured only while the session still shows that silence: the assistant's last message is still the same one and the customer has not written since. Otherwise the turn ends with `code: 'silence-broken'` and `changed: false`.
|
|
186
186
|
5. The run starts, takes the floor and speaks first. A talk step costs one model call; the prompt tells the model there is no new message from the customer.
|
|
187
187
|
|
|
@@ -229,7 +229,7 @@ console.log(r.outcomes[0]?.code); // "awaiting-trigger"
|
|
|
229
229
|
|
|
230
230
|
- `turn({ event, payload, key })` publishes it. The payload becomes the run's `input`: `{{input.stageId}}` in templates, `ctx.input` in actions, `input` in the trigger's `if`. `input` is typed `unknown` there; narrow it before you read it.
|
|
231
231
|
- `after: '1h'` parks the new run before its first step. The wake key is `<runId>:start:<atMs>`; the outcome line reads `code: 'awaiting-trigger'`. If the same event arrives again for the same flow and anchor while the run is still parked there, the parked run is replaced (`ended[].reason: 'replaced'`). Any other live run makes the new one skip with `code: 'already-running'`.
|
|
232
|
-
- `businessHours: true` moves `after` forward to the next working hour.
|
|
232
|
+
- `businessHours: true` moves `after` forward to the next working hour. With no `after`, it holds an event that arrives after hours until the next working hour, and starts at once inside them.
|
|
233
233
|
- Default `repeat: 'always'`: every event with a new `key` starts a run. The same `key` twice is skipped with `code: 'already-claimed'`, so publishing an event again is safe.
|
|
234
234
|
- Zero model calls, unless the run reaches a talk step: then the assistant speaks first, one call.
|
|
235
235
|
- An event may carry a `direction`, which stamps the session as the customer or the assistant speaking. See [Actions and events](actions-and-events.md).
|
|
@@ -298,7 +298,7 @@ Inside a session, a flow has at most one live run per anchor. A trigger that fir
|
|
|
298
298
|
|
|
299
299
|
## `businessHours`
|
|
300
300
|
|
|
301
|
-
Timers can wait for working hours. Give the agent a `businessHours` function and set `businessHours: true` where it should apply: a silence trigger, an event
|
|
301
|
+
Timers can wait for working hours. Give the agent a `businessHours` function and set `businessHours: true` where it should apply: a silence trigger, an event trigger (its `after`, or the start itself when it has none), a `wait` step.
|
|
302
302
|
|
|
303
303
|
```ts
|
|
304
304
|
import { falai, GeminiProvider } from "@falai/agent";
|
|
@@ -65,12 +65,12 @@ async function onMessage(sessionId: string, context: unknown, history: History,
|
|
|
65
65
|
if (r.changed) {
|
|
66
66
|
await store.save(r.session, session?.version ?? 0); // throws SessionConflictError when another turn saved first: drop this result and run the turn again
|
|
67
67
|
for (const m of r.messages) await send(m.text, { after: m.afterMs, key: m.key });
|
|
68
|
-
for (const s of r.schedule) await queue.add({ jobId: s.key, at: s.at });
|
|
68
|
+
for (const s of r.schedule) await queue.add({ jobId: encodeURIComponent(s.key), at: s.at }); // BullMQ refuses a ':' in a custom id
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
Input kinds, one of: `{ message, id?, at? }`, `{ wake }` (a key from `schedule[]`), `{ event, payload?, key }`, `{ start: { flow, input?, key } }`. Pass `context` and `history` on every call, wakes included. Pass `silenced: 'reason'` whenever the assistant must not speak (a human owns the conversation, the channel window is closed, no credits): `do` steps still run, zero model calls, and nothing is said. A run that was already asking stays asking and speaks on the first turn that is not silenced; a run that reaches a new talk or `say` step ends, and the step's outcome is `code: 'silenced'` with your reason in `detail`. Pass `silenced: { reason, understand: true }` to keep the understand call (extraction, mentions) while muting speech.
|
|
73
|
+
Input kinds, one of: `{ message, id?, at? }`, `{ wake }` (a key from `schedule[]`), `{ event, payload?, key }`, `{ start: { flow, input?, key } }`. Pass `context` and `history` on every call, wakes included. Pass `silenced: 'reason'` whenever the assistant must not speak (a human owns the conversation, the channel window is closed, no credits): `do` steps still run, zero model calls, and nothing is said. A run that was already asking stays asking and speaks on the first turn that is not silenced; a run that reaches a new talk or `say` step ends, and the step's outcome is `code: 'silenced'` with your reason in `detail`. Pass `silenced: { reason, understand: true }` to keep the understand call (extraction, mentions) while muting speech. Pass `silenced: { reason, skip: true }` when the gate only stops messages (a closed channel window): the talk or `say` step is skipped and the run goes on to its `then`.
|
|
74
74
|
|
|
75
75
|
`respondStream()` is `turnStream()`: yields `{ delta }` chunks and one `{ done: true, result }`.
|
|
76
76
|
|
|
@@ -147,7 +147,7 @@ const triagem = f.flow({
|
|
|
147
147
|
{ id: 'aviso', do: 'notify', with: { recipient: 'owner', message: 'Lead: {{data.nome}} ({{data.empresa}})' } },
|
|
148
148
|
{ id: 'tchau', say: 'Um vendedor continua daqui.' },
|
|
149
149
|
],
|
|
150
|
-
onEnd: 'end', // or 'stay' (
|
|
150
|
+
onEnd: 'end', // or 'stay' (the last talk step answers every later message) or 'reset' (first step, data kept)
|
|
151
151
|
});
|
|
152
152
|
```
|
|
153
153
|
|
|
@@ -206,7 +206,7 @@ Per-field wording lives on the field (`ask`); a step may override it (`ask: { no
|
|
|
206
206
|
type Next = string /* step id or 'end' */ | { step: string; clear?: string[] } | { flow: string; input?: unknown };
|
|
207
207
|
```
|
|
208
208
|
|
|
209
|
-
Branches stay on talk steps, judged while the step is asking: `{ when: '...', then }` for the model, `{ if: pred, then }` for code. A `wait` step takes `if` branches only, judged when the customer replies, and only when the step also has an `else`; a `when` branch on a wait
|
|
209
|
+
Branches stay on talk steps, judged while the step is asking: `{ when: '...', then }` for the model, `{ if: pred, then }` for code. A `wait` step takes `if` branches only, judged when the customer replies, and only when the step also has an `else`; `validateFlow` rejects a `when` branch on a wait, since nothing would ever judge it. There is no standalone AI-judged step: the model forks only where fresh customer text exists.
|
|
210
210
|
|
|
211
211
|
```ts fragment
|
|
212
212
|
// ─── v3 ───
|
|
@@ -303,7 +303,7 @@ const retomar = f.flow({
|
|
|
303
303
|
});
|
|
304
304
|
```
|
|
305
305
|
|
|
306
|
-
- `wait: '3s'` (10 s or less, and the next step is a `say` or a talk step) becomes `afterMs` on that message in the same turn; every other wait parks the run (stops it until a wake) and puts `{ key, at }` in `schedule[]`. Enqueue the wake with `
|
|
306
|
+
- `wait: '3s'` (10 s or less, and the next step is a `say` or a talk step) becomes `afterMs` on that message in the same turn; every other wait parks the run (stops it until a wake) and puts `{ key, at }` in `schedule[]`. Enqueue the wake with `encodeURIComponent(key)` as the job id (BullMQ refuses a `:` in a custom id) and call `turn({ wake: key })` when it fires. The framework never cancels a wake itself: a stale one is ignored (`changed: false`). A re-armed silence wake names the one it supersedes in `replaces`; removing that job is optional.
|
|
307
307
|
- `on: [{ event: 'stage_entered', after: '1h' }]` starts a run when your code calls `turn({ event, payload, key })`. Declare events with `f.event<Payload>({ direction? })`: `inbound` counts as the customer speaking, `outbound` as the assistant.
|
|
308
308
|
- `wait: { event: 'meeting_booked', upTo: '7d' }` parks until the event arrives.
|
|
309
309
|
- Runs inside a session are concurrent; at most one is asking a question. A timer-started talk step suspends the current asker and hands the floor back when it is done.
|
|
@@ -349,7 +349,7 @@ interface Store<D> {
|
|
|
349
349
|
}
|
|
350
350
|
```
|
|
351
351
|
|
|
352
|
-
The seven adapters survive as `Store` implementations and take the same client you passed before: `MemoryStore`, `PostgresStore`, `PrismaStore`, `RedisStore`, `MongoStore`, `SQLiteStore`, `OpenSearchStore`. They persist the v4 blob and a version, nothing else; message repositories, `SessionRepository`, `status`, `currentFlow` / `currentStep` columns, `PersistenceManager`, `autoSave`, `schemaVersion` and `restoreSession` are gone. The framework never calls a store: you `load`, `turn`, `save`.
|
|
352
|
+
The seven adapters survive as `Store` implementations and take the same client you passed before: `MemoryStore`, `PostgresStore`, `PrismaStore`, `RedisStore`, `MongoStore`, `SQLiteStore`, `OpenSearchStore`. Each takes one options object with the client in it, so OpenSearch's becomes `new OpenSearchStore({ client, ...options })`. They persist the v4 blob and a version, nothing else; message repositories, `SessionRepository`, `status`, `currentFlow` / `currentStep` columns, `PersistenceManager`, `autoSave`, `schemaVersion` and `restoreSession` are gone. The framework never calls a store: you `load`, `turn`, `save`.
|
|
353
353
|
|
|
354
354
|
**Use a fresh table.** The default names are the 3.x ones (`agent_sessions`, `agent:` prefix), so pass a new one (`tables.sessions` on Postgres, SQLite and Prisma, `collections.sessions` on Mongo, `indices.sessions` on OpenSearch, `keyPrefix` on Redis) or drop the old table first; `initialize()` (Postgres, SQLite, OpenSearch) only creates the table or index when it is missing, and does nothing while one of that name exists. A v4 store read against a live 3.x row fails loudly: Redis, Mongo, Prisma and OpenSearch throw `InvalidSessionError` (no `blob`), Postgres and SQLite fail on the missing `blob` column. Create the new table, then migrate rows on first load as §10 shows.
|
|
355
355
|
|
|
@@ -386,6 +386,7 @@ const session = migrateSession(rowBlob, {
|
|
|
386
386
|
- `version` is 0: the session has no row in the v4 table yet, so your usual `store.save(session, session.version)` is the insert.
|
|
387
387
|
- `pendingDirective` is dropped.
|
|
388
388
|
- A blob that is neither v4 nor a recognisable 3.x state throws `InvalidSessionError`; a corrupt row can no longer become a fresh conversation silently.
|
|
389
|
+
- 3.x did not record when the assistant last spoke, so a lifted session has no `lastAssistantAt` and arms no silence follow-up until the assistant speaks again. At cutover, set `session.lastAssistantAt` and `session.lastUserAt` from your messages table, save, and enqueue `agent.pendingWakes({ session, context })`: a conversation that was quiet at cutover then gets its follow-up on time.
|
|
389
390
|
|
|
390
391
|
Add a test that loads one real (anonymised) row per product and asserts the run's `stepId` and the carried claims.
|
|
391
392
|
|
|
@@ -441,7 +442,7 @@ Then, in this order:
|
|
|
441
442
|
1. Convert stored flows and signal rules to `FlowSpec` rows. Keep talk-step ids; give each migrated signal flow `id = signal key`.
|
|
442
443
|
2. Register your actions, events and conditions on the agent.
|
|
443
444
|
3. Replace the `respond` call site with load → `turn` → save + messages + schedules in one transaction.
|
|
444
|
-
4. Wire wakes (
|
|
445
|
+
4. Wire wakes (job id `encodeURIComponent(key)`, `turn({ wake })` at fire time) and host events. At cutover, enqueue `agent.pendingWakes()` for every lifted session that was quiet.
|
|
445
446
|
5. Put `migrateSession` in your deserializer and let it throw on garbage.
|
|
446
447
|
6. Delete the automation engine, the follow-up sweep and the second composer.
|
|
447
448
|
|
|
@@ -220,7 +220,7 @@ An event turn never spends an understand call. It costs one speak call (plus too
|
|
|
220
220
|
|
|
221
221
|
1. **Direction.** `'inbound'` sets `lastUserAt` to now and resolves reply waits: every run parked on a timer `wait` that has an `else` resumes with `code: 'replied'` and follows a matching `if` branch's `then`, else `else`. `'outbound'` sets `lastAssistantAt` to now, which re-arms `silence` triggers at the end of the turn.
|
|
222
222
|
2. **Waiting runs.** Every run parked on `wait: { event: name }` for this name resumes with `code: 'event-arrived'` and follows `then`. The first one to resume takes the floor for this turn.
|
|
223
|
-
3. **Triggers.** Every flow with `on: [{ event: name }]` goes through the start order: trigger `if`, `repeat` (default `'always'` for events), the hop cap, one live run per flow and anchor. With `after`, the run parks first (`code: 'awaiting-trigger'`, wake key `${runId}:start:${atMs}`) and enters its first step when the wake fires; `businessHours: true` snaps that time forward through the agent's `businessHours` function.
|
|
223
|
+
3. **Triggers.** Every flow with `on: [{ event: name }]` goes through the start order: trigger `if`, `repeat` (default `'always'` for events), the hop cap, one live run per flow and anchor. With `after`, the run parks first (`code: 'awaiting-trigger'`, wake key `${runId}:start:${atMs}`) and enters its first step when the wake fires; `businessHours: true` snaps that time forward through the agent's `businessHours` function. With `businessHours: true` and no `after`, an event that arrives outside working hours parks the run the same way until the next working moment.
|
|
224
224
|
|
|
225
225
|
`wait: { event, upTo }` in a step parks the run for at most `upTo` (default `'30d'`, from `src/core/Runner.ts`). If the event never comes, the line carries `code: 'no-event'` and the run follows `else`, or ends when there is none.
|
|
226
226
|
|
package/docs/reference/agent.md
CHANGED
|
@@ -86,7 +86,7 @@ class Agent<C, D> {
|
|
|
86
86
|
|
|
87
87
|
`Idle` is `{ prompt: Template; tools?: string[]; instructions?: Instruction[] } | 'silent'`. Its `tools` list must name tools registered on the agent.
|
|
88
88
|
|
|
89
|
-
`AgentCompactionConfig` is `{ maxTokens: number; compactionThreshold?: number; preserveRecentCount?: number; maxToolResultChars?: number; enabled?: boolean }`. Defaults: `compactionThreshold` `0.8` — compaction runs when the history passes 80% of `maxTokens` (allowed 0.5 to 0.95); keep the 4 most recent messages (at least 2); cut each tool result at 5000 characters (more than 0); `enabled: true`. Values outside those ranges throw at construction.
|
|
89
|
+
`AgentCompactionConfig` is `{ maxTokens: number; compactionThreshold?: number; preserveRecentCount?: number; maxToolResultChars?: number; enabled?: boolean }`. `maxTokens` must be more than 0. Defaults: `compactionThreshold` `0.8` — compaction runs when the history passes 80% of `maxTokens` (allowed 0.5 to 0.95); keep the 4 most recent messages (at least 2); cut each tool result at 5000 characters (more than 0); `enabled: true`. Values outside those ranges throw at construction.
|
|
90
90
|
|
|
91
91
|
## turn()
|
|
92
92
|
|
|
@@ -94,6 +94,10 @@ class Agent<C, D> {
|
|
|
94
94
|
|
|
95
95
|
`turnStream(input)` runs the same turn and yields `{ delta: string }` chunks while the model phrases the reply, then one `{ done: true, result: TurnResult }`. When nobody speaks, only the last chunk comes. See [Streaming](../guides/streaming.md).
|
|
96
96
|
|
|
97
|
+
## pendingWakes()
|
|
98
|
+
|
|
99
|
+
`pendingWakes({ session, context, anchors?, claims? })` returns the `ScheduleEntry[]` a saved session is waiting on: each parked run's wake, and each silence flow's counted from `session.lastAssistantAt`, none once the customer wrote after it. The trigger's `if` and `repeat` are judged on the `context`, `anchors` and `claims` you pass, as in a turn. It changes nothing and spends no model call; the entries carry no `replaces`. Use it when a queue lost its jobs, and after `migrateSession`, since a 3.x blob has no `lastAssistantAt` and so no silence wake until the assistant speaks again.
|
|
100
|
+
|
|
97
101
|
## TurnInput
|
|
98
102
|
|
|
99
103
|
Every input carries the common fields, plus exactly one of the four kinds.
|
|
@@ -122,10 +126,10 @@ Every input carries the common fields, plus exactly one of the four kinds.
|
|
|
122
126
|
### Silenced
|
|
123
127
|
|
|
124
128
|
```ts fragment
|
|
125
|
-
type Silenced = string | { reason: string; understand?: boolean };
|
|
129
|
+
type Silenced = string | { reason: string; understand?: boolean; skip?: boolean };
|
|
126
130
|
```
|
|
127
131
|
|
|
128
|
-
A string closes the gate: `do` steps still run, nothing is phrased, zero model calls. A talk or `say` step reached while the gate is closed ends its run with `code: 'silenced'` (`detail` = your reason); a run that was already asking stays asking and speaks when the gate opens. `{ reason, understand: true }` keeps the understand call on, so routing, mentions and extraction still happen while the assistant stays quiet. Predicates see the reason as `ctx.silenced`.
|
|
132
|
+
A string closes the gate: `do` steps still run, nothing is phrased, zero model calls. A talk or `say` step reached while the gate is closed ends its run with `code: 'silenced'` (`detail` = your reason); a run that was already asking stays asking and speaks when the gate opens. `{ reason, understand: true }` keeps the understand call on, so routing, mentions and extraction still happen while the assistant stays quiet. `{ reason, skip: true }` is for a gate that only stops messages, such as a closed channel window: the talk or `say` step is skipped with the same `code: 'silenced'` and the run takes its `then`. Predicates see the reason as `ctx.silenced`.
|
|
129
133
|
|
|
130
134
|
## TurnResult
|
|
131
135
|
|
|
@@ -134,7 +138,7 @@ A string closes the gate: `do` steps still run, nothing is phrased, zero model c
|
|
|
134
138
|
| `session` | `Session<D>` | The session after this turn. `version` is unchanged; the host saves it with the version it loaded and the store bumps it. |
|
|
135
139
|
| `changed` | `boolean` | `false` means save nothing and send nothing: the input was ignored or nothing moved. |
|
|
136
140
|
| `messages` | `OutboundMessage[]` | What to send, in order. |
|
|
137
|
-
| `schedule` | `ScheduleEntry[]` | Wakes to enqueue with `
|
|
141
|
+
| `schedule` | `ScheduleEntry[]` | Wakes to enqueue, with `encodeURIComponent(key)` as the job id. At fire time call `turn({ wake: key })`. |
|
|
138
142
|
| `outcomes` | `StepOutcome[]` | One line per step this turn, for your execution log. See [Outcomes](outcomes.md). |
|
|
139
143
|
| `started` | `Array<{ runId; flowId; anchor; dedupeKey }>` | Runs that started this turn. |
|
|
140
144
|
| `ended` | `Array<Run & { reason: EndReason }>` | Runs that ended, with the run's last state and why: `'end'`, `'flow'`, `'reset'`, `'skipped'`, `'failed'` or `'replaced'`. |
|
|
@@ -183,7 +187,7 @@ if (r.usage) {
|
|
|
183
187
|
|
|
184
188
|
| Field | Type | Meaning |
|
|
185
189
|
|---|---|---|
|
|
186
|
-
| `key` | `string` | The wake key.
|
|
190
|
+
| `key` | `string` | The wake key. Pass it back as `turn({ wake: key })`. As a queue job id, encode it: BullMQ refuses a custom id with a `:` in it, and every key has one. |
|
|
187
191
|
| `at` | `Date` | When to fire. |
|
|
188
192
|
| `replaces` | `string` | An earlier wake this one supersedes. Removing it is best effort; a stale wake is ignored anyway. |
|
|
189
193
|
|
|
@@ -34,7 +34,7 @@ type Branch<C = unknown, D = unknown> = { then: Next<D> } & (
|
|
|
34
34
|
3. The run leaves the step with the outcome `code: 'branch'` (kind `prompt` or `collect`, status `ok`, `next` naming the target) and follows `then` in the same turn. Fields the understand call extracted from the same message are written first, so `then` may land on a step that is already satisfied.
|
|
35
35
|
4. When no branch holds, the step continues as usual: pending fields are harvested and the step speaks again.
|
|
36
36
|
|
|
37
|
-
A `when` branch costs the understand call; when it is the only thing to judge, that is one model call the turn would not otherwise spend. An `if` branch is judged on every message turn even when no understand call happens.
|
|
37
|
+
A `when` branch costs the understand call; when it is the only thing to judge, that is one model call the turn would not otherwise spend. An `if` branch is judged on every message turn even when no understand call happens. A suspended run that returns to asking during the turn, because the asker ended or moved on without a word, has its `if` branches judged before its step speaks; its `when` branches were not in the understand call, so they wait for the next message.
|
|
38
38
|
|
|
39
39
|
**On a wait step.** Only `if` branches are judged, and only when the customer writes before the time passes and the step has `else`. The first `if` branch that holds replaces `else` as the target (outcome `code: 'replied'`). A `when` branch on a wait step is never asked, because a reply to a wait step does not reach the understand call. When the wake fires, branches are not consulted: the run follows `then`, or `else` when the customer wrote after the wait was set.
|
|
40
40
|
|
package/docs/reference/errors.md
CHANGED
|
@@ -59,7 +59,7 @@ From `src/types/errors.ts`, `src/core/Migrate.ts` and `@providerkit/core`.
|
|
|
59
59
|
| Class | Thrown by | When | What to do |
|
|
60
60
|
|-------|-----------|------|------------|
|
|
61
61
|
| `FlowConfigurationError` | `f.agent()` / `new Agent()`, `validateFlow`, `fromSpec` | A flow cannot run as written: a flow id or step id declared twice; a step with no id, or with the reserved id `"end"`; an unknown field in `collect` / `ask` / `clearOnStart`; an unknown action, event, condition or tool; a `then` pointing at a step that does not exist; an action `with` missing a required parameter; a duration that does not parse; a talk step with neither `prompt` nor `collect`. Also thrown at run time when a JSON predicate names a condition the agent does not have. | Fix the flow. It is a bug in the flow or the registries, never something to retry. |
|
|
62
|
-
| `SessionConflictError` | every `Store.save` | The stored version is not `expectedVersion`: another turn saved first,
|
|
62
|
+
| `SessionConflictError` | every `Store.save` | The stored version is not `expectedVersion`: another turn saved first, a `save(…, 0)` found a row, or the row was deleted or expired after it was loaded (`actualVersion` is `undefined`). | Load the session again and replay the same input. Nothing was sent, so nothing is duplicated. |
|
|
63
63
|
| `InvalidSessionError` | every `Store.load`, `assertSession`, `migrateSession` | A stored row is not a v4 session and not a recognisable 3.x one: wrong `v`, an `id` that does not match the row, a missing `data`, a run with a bad `status`, text that is not JSON. | Repair or delete the row. The framework never replaces a bad row with a fresh conversation, because that would re-ask every field and re-fire every once-flow. |
|
|
64
64
|
| `ProviderError` | the built-in providers, `FallbackAiProvider` | A model call failed after the provider's own retries, backup models and fallbacks. `kind` says what would fix it. | Match on `kind` (table below). |
|
|
65
65
|
|
|
@@ -80,8 +80,8 @@ The bracket names the class, the text before the colon says what is wrong and wh
|
|
|
80
80
|
[FlowConfigurationError] flow "triagem": has no steps list. Write steps as a list, even an empty one.
|
|
81
81
|
[FlowConfigurationError] flow "a" is declared twice: flow ids must be unique. Rename one of them.
|
|
82
82
|
[FlowConfigurationError] idle: unknown tool "buscar_preco". Register it in the agent's tools or fix the name.
|
|
83
|
-
[SessionConflictError] Session "s1" was modified concurrently: expected version
|
|
84
|
-
[SessionConflictError] Session "s1"
|
|
83
|
+
[SessionConflictError] Session "s1" was modified concurrently: expected version 3, found 4. Reload the session and retry the operation.
|
|
84
|
+
[SessionConflictError] Session "s1" is gone from the store: it was at version 3 and has since been deleted or expired. Load it again; a load that finds nothing starts a new conversation.
|
|
85
85
|
[InvalidSessionError] stored session "s1" is unreadable: v is 3, expected 4. Repair or delete the row; it is never replaced by a fresh conversation.
|
|
86
86
|
[InvalidSessionError] stored session "s1" is unreadable: expected an object, got "garbage". Repair or delete the row; it is never replaced by a fresh conversation.
|
|
87
87
|
[InvalidSessionError] stored session "s1" is unreadable: data is missing, expected an object; not a 3.x session either. Repair or delete the row; it is never replaced by a fresh conversation.
|
|
@@ -96,9 +96,9 @@ Named by what fixes them, from `@providerkit/core`.
|
|
|
96
96
|
| `kind` | Meaning | Retry? |
|
|
97
97
|
|--------|---------|--------|
|
|
98
98
|
| `aborted` | Your own `AbortSignal` fired. | Never. |
|
|
99
|
-
| `timeout` | The
|
|
100
|
-
| `network` | The request never reached the provider
|
|
101
|
-
| `overload` | Theirs and temporary: a 5xx, Anthropic's 529, "overloaded". | Yes, and worth a different model. |
|
|
99
|
+
| `timeout` | The response went silent past `retryConfig.timeout` (default 60 s, keep-alives count as life), sent no chunk for 5 minutes, or the provider sent 408. | Yes. |
|
|
100
|
+
| `network` | The request never reached the provider (socket, DNS, proxy), or the stream ended before the provider's end signal. | Yes. |
|
|
101
|
+
| `overload` | Theirs and temporary: a 5xx, Anthropic's 529, "overloaded", or a turn that ended with no text and no tool call. | Yes, and worth a different model. |
|
|
102
102
|
| `rate` | 429 per-minute throttle. | Wait `retryAfterMs`, or rotate key or model. |
|
|
103
103
|
| `quota` | Balance or usage window exhausted. | Waiting minutes will not fix it. |
|
|
104
104
|
| `entitlement` | The plan never included this API. | No; neither a new key nor a top-up fixes it. |
|
|
@@ -106,7 +106,7 @@ Named by what fixes them, from `@providerkit/core`.
|
|
|
106
106
|
| `model` | The model id does not exist or is not served here. | No; the built-in providers do try the next `backupModels` entry. |
|
|
107
107
|
| `context` | The prompt outgrew the context window. | No; send less. Compaction is the framework's answer. |
|
|
108
108
|
| `content` | Safety filter or refusal. | No. |
|
|
109
|
-
| `invalid` | Any other 4xx: a bug in what was sent. | No. |
|
|
109
|
+
| `invalid` | Any other 4xx: a bug in what was sent. Also a turn that spent its whole `maxTokens` thinking and never answered: raise `config.maxTokens` or lower `config.effort`. | No. |
|
|
110
110
|
| `unknown` | Nothing above matched. | No. |
|
|
111
111
|
|
|
112
112
|
`isTransient` is true for `timeout`, `network`, `overload` and `rate`. `isBackupEligible` is what the provider uses to decide whether to try the next backup model.
|
|
@@ -124,11 +124,13 @@ The same `ProviderError` means two different things depending on which of the tw
|
|
|
124
124
|
|
|
125
125
|
A few throws are plain `Error` or `TypeError`. One happens at run time: a reply that parses to a blank message with no tool calls throws `Error: No response from <provider>` out of `generateMessage`, after the provider's retries and backup models have run. A speak call swallows it and defers the step; an understand call hands it to you, so catch `Error`, not only `ProviderError`. The rest are wiring bugs at construction:
|
|
126
126
|
|
|
127
|
-
- A provider built without a key or model: `
|
|
127
|
+
- A provider built without a key or model: `[GeminiProvider] apiKey is empty: the provider cannot authenticate. Pass { apiKey: process.env.GEMINI_API_KEY } and check the variable is set.`, `[OpenAIProvider] model is empty: there is no default. Pass one, e.g. { model: "gpt-5.6" }.`
|
|
128
128
|
- `createOpenAICompatibleProvider` without `name`, `baseURL`, `apiKey` or `model`.
|
|
129
129
|
- `FallbackAiProvider` with an empty `providers` list.
|
|
130
130
|
- `PrismaStore` whose client has no delegate for the model: `[TypeError] PrismaStore cannot use model "agentSession": …`.
|
|
131
|
-
- `compaction` options out of range (`compactionThreshold` outside 0.5–0.95, `preserveRecentCount` below 2, `maxToolResultChars` at or below 0).
|
|
131
|
+
- `compaction` options out of range (`maxTokens` at or below 0, `compactionThreshold` outside 0.5–0.95, `preserveRecentCount` below 2, `maxToolResultChars` at or below 0): `[CompactionEngine] compactionThreshold is 1.2: it must be between 0.5 and 0.95. Use 0.8 unless you measured otherwise.`
|
|
132
|
+
|
|
133
|
+
Each follows the same `[Class] what: why. how to fix.` shape as the package classes; only the class is plain `Error`.
|
|
132
134
|
|
|
133
135
|
## Example
|
|
134
136
|
|
|
@@ -147,7 +149,7 @@ import type { AiProvider, DataOf, Session, TurnResult } from "@falai/agent";
|
|
|
147
149
|
|
|
148
150
|
declare const provider: AiProvider;
|
|
149
151
|
declare function send(text: string, key: string, afterMs: number): Promise<void>;
|
|
150
|
-
declare function enqueue(jobId: string, at: Date): Promise<void>;
|
|
152
|
+
declare function enqueue(jobId: string, at: Date, payload: { sessionId: string; key: string }): Promise<void>;
|
|
151
153
|
declare function sleep(ms: number): Promise<void>;
|
|
152
154
|
|
|
153
155
|
const f = falai().fields({
|
|
@@ -213,7 +215,8 @@ async function handle(sessionId: string, text: string, id: string): Promise<Turn
|
|
|
213
215
|
}
|
|
214
216
|
|
|
215
217
|
for (const m of result.messages) await send(m.text, m.key, m.afterMs);
|
|
216
|
-
for (
|
|
218
|
+
// The key rides in the payload for turn({ wake: key }); BullMQ refuses a ":" in a custom id, so the job id is encoded.
|
|
219
|
+
for (const s of result.schedule) await enqueue(encodeURIComponent(s.key), s.at, { sessionId, key: s.key });
|
|
217
220
|
return result;
|
|
218
221
|
}
|
|
219
222
|
|
package/docs/reference/fields.md
CHANGED
|
@@ -21,6 +21,7 @@ interface ScalarDef<T extends ScalarType = ScalarType> {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
interface FieldDef<T extends ScalarType = ScalarType> extends ScalarDef<T> {
|
|
24
|
+
label?: string;
|
|
24
25
|
ask?: string;
|
|
25
26
|
extract?: "anywhere" | "asked";
|
|
26
27
|
}
|
|
@@ -37,6 +38,7 @@ type DataOf<T extends { fields: FieldDefs }> = InferData<T["fields"]>;
|
|
|
37
38
|
|---|---|---|---|
|
|
38
39
|
| `type` | `ScalarType` | required | `'string'`, `'number'`, `'integer'` or `'boolean'`. |
|
|
39
40
|
| `enum` | `readonly (string \| number)[]` | none | The allowed values. A value outside the list is dropped (`code: 'not-in-enum'`). In the data type the field becomes the literal union. |
|
|
41
|
+
| `label` | `string` | none | The name a person reads, in an editor or next to a collected value. Never sent to the model. |
|
|
40
42
|
| `description` | `string` | none | What the field is. Sent to the model with the field's type and options. |
|
|
41
43
|
| `ask` | `string` | none | How the model should ask for it. Sent to the speak call as "How to ask" while the field is pending. A talk step's own `ask` overrides it. A template: `{{data.x}}` and `{{context.x}}` are filled in. |
|
|
42
44
|
| `extract` | `'anywhere' \| 'asked'` | `'anywhere'` for string, number and integer; `'asked'` for boolean | Where a value may be taken from. `'anywhere'`: any message from the customer, whether or not the field was asked. `'asked'`: only the reply to the step that lists the field, so a stray "sim" never confirms anything. |
|
|
@@ -55,7 +57,7 @@ Properties are readonly — `fields()` takes the definitions as a `const` type,
|
|
|
55
57
|
|
|
56
58
|
**Known.** A field is known when its value is not `undefined`, `null` or `''`. Known fields are never asked again and never re-extracted; both calls list them under "Already known".
|
|
57
59
|
|
|
58
|
-
**Which call extracts what.** The understand call, on a message turn, extracts every unknown `'anywhere'` field
|
|
60
|
+
**Which call extracts what.** The understand call, on a message turn, extracts every unknown `'anywhere'` field that the flow holding the floor or an eligible message flow lists, in the flow's own `collect` or a talk step's, in one envelope. With nobody on the floor, the catch-all's fields are read too. The speak call extracts the pending fields of the step that speaks, `'asked'` ones included, in the same call that phrases the reply. Tools write `data` and actions call `ctx.set()`; those values are written as given, with no check.
|
|
59
61
|
|
|
60
62
|
**Coercion.** A raw value from the model goes through `coerceField(def, raw)` before it is written:
|
|
61
63
|
|
|
@@ -68,9 +70,9 @@ Properties are readonly — `fields()` takes the definitions as a `const` type,
|
|
|
68
70
|
|
|
69
71
|
Anything else is dropped with the outcome line `code: 'bad-value'`. A value outside `enum` is dropped with `code: 'not-in-enum'`. A value for a slug that is not a field is dropped with `code: 'unknown-field'`. Dropped values leave a `collect` outcome with status `skipped` and no run id; the field stays pending and is asked again.
|
|
70
72
|
|
|
71
|
-
**What reaches the provider.** `toWireSchema(defs)` turns definitions into the JSON schema the model must fill: a closed object (`additionalProperties: false`) with one property per field carrying `type`, `description` and `enum`, and nothing else. `ask` and `extract` are stripped; they
|
|
73
|
+
**What reaches the provider.** `toWireSchema(defs)` turns definitions into the JSON schema the model must fill: a closed object (`additionalProperties: false`) with one property per field carrying `type`, `description` and `enum`, and nothing else. `label`, `ask` and `extract` are stripped; they are for people and the framework, not the schema. In the envelope style used by both calls every property is required and nullable (`type: [type, 'null']`), so the model answers `null` for what the customer did not give. The prompt describes each field once more in words (`nome (string) [a | b]: description`) and, in the speak call, adds the pending field's "How to ask".
|
|
72
74
|
|
|
73
|
-
**Clearing.** `{ step, clear: ['x'] }` on a `then`/`else`/branch and `clearOnStart` on a flow delete the field from `session.data`, so the next step that collects it asks again. `maxAsks` on a talk step (default 3) is the other way a field stops being pending: it is skipped for that run with `code: 'max-asks'`, the field in `detail`.
|
|
75
|
+
**Clearing.** `{ step, clear: ['x'] }` on a `then`/`else`/branch and `clearOnStart` on a flow delete the field from `session.data`, so the next step that collects it asks again. `{ step, clear }` also resets how many times the run asked it. `maxAsks` on a talk step (default 3) is the other way a field stops being pending: it is skipped for that run with `code: 'max-asks'`, the field in `detail`.
|
|
74
76
|
|
|
75
77
|
**Action parameters** use the sibling type `ParamDef`: a `ScalarDef` plus `optional?: true`, or `{ type: 'array', items: ScalarDef }`. `InferParams<P>` gives the `with` shape. They share `toWireSchema` and the same strictness at construction. See [Actions, events and conditions](actions-events-conditions.md).
|
|
76
78
|
|
|
@@ -23,6 +23,7 @@ interface FlowSpec {
|
|
|
23
23
|
on?: TriggerSpec[];
|
|
24
24
|
anchor?: string;
|
|
25
25
|
while?: ConditionSpec;
|
|
26
|
+
collect?: string[];
|
|
26
27
|
clearOnStart?: string[];
|
|
27
28
|
steps: StepSpec[];
|
|
28
29
|
onEnd?: "end" | "stay" | "reset";
|
|
@@ -33,7 +34,7 @@ interface FlowSpec {
|
|
|
33
34
|
type StepSpec = StepBase &
|
|
34
35
|
(
|
|
35
36
|
| { kind: "prompt"; prompt: Template; ask?; maxAsks?; branches?: BranchSpec[]; tools?; instructions?: InstructionSpec[] }
|
|
36
|
-
| { kind: "collect"; collect: string[]; prompt?: Template; ask?; maxAsks?; branches?: BranchSpec[]; tools?; instructions?: InstructionSpec[] }
|
|
37
|
+
| { kind: "collect"; collect: string[]; prompt?: Template; question?: Template; ask?; maxAsks?; branches?: BranchSpec[]; tools?; instructions?: InstructionSpec[] }
|
|
37
38
|
| { kind: "say"; say: Template; media?: { slug: string }; once?: boolean }
|
|
38
39
|
| { kind: "do"; do: string; with?: Record<string, unknown>; onFail?: Next }
|
|
39
40
|
| { kind: "wait"; wait: Duration; businessHours?: boolean; else?: Next; branches?: BranchSpec[] }
|
|
@@ -52,8 +53,10 @@ type BranchSpec = { then: Next } & ({ when: string } | { if: ConditionSpec });
|
|
|
52
53
|
|
|
53
54
|
type InstructionSpec = Omit<Instruction, "if"> & { if?: ConditionSpec };
|
|
54
55
|
|
|
55
|
-
/** What a flow's names resolve against. */
|
|
56
|
-
type Registries = Pick<AgentOptions, "fields" | "actions" | "events" | "conditions" | "tools"
|
|
56
|
+
/** What a flow's names resolve against. With `flows`, a literal `{ flow }` target must name one of them. */
|
|
57
|
+
type Registries = Pick<AgentOptions, "fields" | "actions" | "events" | "conditions" | "tools"> & {
|
|
58
|
+
flows?: ReadonlyArray<{ id: string }>;
|
|
59
|
+
};
|
|
57
60
|
|
|
58
61
|
function fromSpec<C = unknown, D = InferData<FieldDefs>>(spec: FlowSpec): Flow<C, D>;
|
|
59
62
|
function toSpec<C, D>(flow: Flow<C, D>): FlowSpec;
|
|
@@ -89,13 +92,13 @@ Every field means what it means on [Flow](./flow.md). The differences:
|
|
|
89
92
|
| `waitEvent` | `wait: { event }` | An event. |
|
|
90
93
|
| `if` | `if` step | A code fork. |
|
|
91
94
|
|
|
92
|
-
`fromSpec` drops `kind`; `toSpec` derives it from the step's shape by this table. A talk step with neither `prompt` nor `collect` makes `toSpec` throw.
|
|
95
|
+
`fromSpec` drops `kind`; `toSpec` derives it from the step's shape by this table. A talk step with neither `prompt` nor `collect` makes `toSpec` throw: `[FlowConfigurationError] flow "x", step "y": has neither prompt nor collect. A talk step needs a guideline, fields to collect, or both.`
|
|
93
96
|
|
|
94
97
|
## fromSpec
|
|
95
98
|
|
|
96
99
|
- Strips `null` from every optional value, at any depth.
|
|
97
100
|
- Removes `kind` from each step. Nothing else changes.
|
|
98
|
-
- Throws `FlowConfigurationError` when `
|
|
101
|
+
- Throws `FlowConfigurationError` when the JSON has the wrong shape, with the same shape checks `validateFlow` runs first: the flow or a step is not an object, a list is not a list (`steps`, `on`, `collect`, …), a step does two things or none, or its `kind` disagrees with its body. For example `[FlowConfigurationError] flow "x": has no steps list. Write steps as a list, even an empty one.`
|
|
99
102
|
- Does **not** check names. The result is typed as a `Flow` but nothing is verified yet; `validateFlow` does that, and the agent runs it on every flow it is built with.
|
|
100
103
|
|
|
101
104
|
## toSpec
|
|
@@ -120,13 +123,17 @@ Every message has the form `[FlowConfigurationError] <where>: <what>. <fix>`, wh
|
|
|
120
123
|
| Reserved step id | `uses the reserved id "end"` | "end" ends the run; pick another id. |
|
|
121
124
|
| Duplicate step id | `duplicates an earlier step id` | Give each step its own id. |
|
|
122
125
|
| Triggers, no steps | `has triggers but no steps` | Add at least one step or remove `on`. |
|
|
123
|
-
|
|
|
126
|
+
| Trigger with no kind | `names no trigger kind` (`<where>` is `flow "id", trigger #n`; the v3 `{ kind: 'message', when }` shape lands here) | A trigger is one of `message`, `mention`, `silence`, `event`. A flow the host starts itself has no `on` at all. |
|
|
127
|
+
| Only exclusions | `every message phrase starts with "!", so nothing can ever match it` (also `mention`) | A "!" phrase rules the trigger out. Add at least one plain phrase saying when it should fire, or use an empty list for a catch-all. |
|
|
128
|
+
| Step does nothing | `does nothing` | A step talks (`prompt` / `collect`), says (`say`), acts (`do`), waits (`wait`) or forks (`if`). |
|
|
129
|
+
| Unknown field | `unknown field "x" in collect` (the flow's or a step's; also `ask`, `clearOnStart`, `then.clear`, `while.equals`, `if.known`, …) | Add it to the agent's fields or fix the slug. |
|
|
124
130
|
| Unknown tool | `unknown tool "x"` (flow or step `tools`) | Register it in the agent's tools or fix the name. |
|
|
125
131
|
| Unknown action | `unknown action "x"` | Register it in actions or fix the name. |
|
|
126
132
|
| Unknown event | `unknown event "x"` (trigger) or `unknown event "x" in wait` | Register it in events or fix the name. |
|
|
127
133
|
| Unknown condition | `unknown condition "x" in if` | Register it in conditions or use equals, known, silenced. |
|
|
128
134
|
| `equals` shape | `if.equals is not an object` | Write equals as { field: value }. |
|
|
129
135
|
| `equals` type | `if.equals gives "orcamento" a string, but the field is a number` | Write a number; values are not coerced. |
|
|
136
|
+
| `equals` off the list | `if.equals gives "etapa" "frio", which is not one of "novo", "quente"` | Use one of the listed values. |
|
|
130
137
|
| `known` shape | `if.known is not a list` | Write known as [field, ...]. |
|
|
131
138
|
| `silenced` shape | `if.silenced is not a boolean` | Write true or false. |
|
|
132
139
|
| Bad duration | `wait has duration "5 min", which does not parse` (also `silence`, `after`, `repeat.cooldown`, `wait.upTo`) | Write a number and a unit: "30s", "5m", "24h" or "3d". |
|
|
@@ -136,17 +143,37 @@ Every message has the form `[FlowConfigurationError] <where>: <what>. <fix>`, wh
|
|
|
136
143
|
| Extra parameter | `action "notify" has no parameter "to"` | Remove it or fix the name. |
|
|
137
144
|
| Branch without a test | `branches[0] has neither when nor if` | Give the branch an AI condition (when) or a code one (if). |
|
|
138
145
|
| Backward `if` with no `else` | `"if" jumps back to "quem" with no else` | Add else so the false branch has somewhere to go. |
|
|
146
|
+
| Fixed question, nothing to ask | `has a question but collects nothing` | A fixed question asks for fields: add collect, or send the text with a say step. |
|
|
147
|
+
| Not an object | `is null, not an object` (the flow), `steps[0] is null, not an object`, `with is "x", not an object` | Pass the flow itself: { id, name, steps }. / Write each entry of steps as an object. |
|
|
148
|
+
| Not a list | `collect is "nome", not a list` (also `on`, `steps`, `clearOnStart`, `tools`, `instructions`, `branches`, `message`, `mention`, `then.clear`) | Write collect: ["nome"]. |
|
|
149
|
+
| Not text | `say is 42, not text` (also `prompt`, `question`, `description`, `anchor`) | Write say as a string. |
|
|
150
|
+
| Not one of the values | `onEnd is "restart", which is not one of "end", "stay", "reset"` (also `instructions[n].kind`) | Use one of them. |
|
|
151
|
+
| Bad `repeat` | `repeat is "never"` | Use "once", "always" or { cooldown: "24h" }. |
|
|
152
|
+
| Bad `maxAsks` | `maxAsks is "3", not a whole number of 1 or more` | Write a number like 3. |
|
|
153
|
+
| Event wait without an event | `wait has no event` | Write wait: { event: "name" } to wait for an event, or a duration like "1h". |
|
|
154
|
+
| Bad target | `then is 5, not a step id or a target` (also `else`, `onFail`, `branches[n].then`) | Write a step id, "end", { step: "id" } or { flow: "id" }. |
|
|
155
|
+
| Step does two things | `mixes "say" and "do"` | A step does one thing. Split it into one step per kind. |
|
|
156
|
+
| `kind` disagrees with the body | `has kind "do", but its body is a "say" step` | Set kind to "say", or change the body to match. |
|
|
139
157
|
|
|
140
158
|
Parameter values are checked strictly: `"3"` is not a number, `3.5` is not an integer, and an `enum` must contain the value unless the string holds `{{`, because a template's value is only known at run time.
|
|
141
159
|
|
|
142
|
-
|
|
160
|
+
The agent constructor passes its own flows as `registries.flows`, so a literal chain to a flow it does not have is a warning in the log (see below). Five more checks live in the constructor rather than in `validateFlow`:
|
|
161
|
+
|
|
162
|
+
- `flow "x" is declared twice`
|
|
163
|
+
- `idle: unknown tool "x"`
|
|
164
|
+
- `tool "x": parameters must be a JSON Schema object`
|
|
165
|
+
- `condition "known" shadows a built-in`: `equals`, `known` and `silenced` are reserved
|
|
166
|
+
- an unknown condition in an agent or idle instruction's `if`: `agent: unknown condition "vip" in instructions[0].if`
|
|
143
167
|
|
|
144
168
|
### Warnings
|
|
145
169
|
|
|
146
170
|
| Warning | Why |
|
|
147
171
|
|---|---|
|
|
148
172
|
| `flow "f", step "s": then jumps back to "quem" without clear; the fields collected since stay known and those steps skip. Add clear: [...] to re-ask them.` | A `then`, `else`, `onFail` or branch target points at the same or an earlier step and clears nothing, so a collect step it lands on is skipped with `code: 'already-known'`. |
|
|
149
|
-
| `flow "f", step "s": collects "nome", "empresa" with no prompt and no ask; the model has nothing to go on. Add a prompt or an ask per field.` | A collect step with no `prompt`, where no listed field has an `ask` on the step or on the agent. |
|
|
173
|
+
| `flow "f", step "s": collects "nome", "empresa" with no prompt and no ask; the model has nothing to go on. Add a prompt or an ask per field.` | A collect step with no `prompt` and no `question`, where no listed field has an `ask` on the step or on the agent. |
|
|
174
|
+
| `flow "f": collect lists "confirmado", which is only taken from the answer to a step that asks it, and no step does. Add it to a step's collect, or set extract: 'anywhere' on the field.` | A field in the flow's `collect` with `extract: 'asked'` (every boolean, by default) that no step's `collect` lists, so nothing can ever fill it. |
|
|
175
|
+
| `flow "f", step "s": then names flow "humnao", which this agent does not have; a run skips this move with flow-gone. Use one of "vendas", "suporte", or add the flow.` | A literal `{ flow }` target missing from `registries.flows`; only checked when that list is set, and never for an id with `{{`. A warning rather than an error, so a host that drops one bad row keeps the rest of its agent. |
|
|
176
|
+
| `flow "f", step "s": branches[0] is a "when" branch on a wait step, which no call judges, so a reply goes to else. Use "if", or move the branch to a talk step.` | No call judges a wait step, so an AI condition there never fires. `flowSpecSchema` no longer offers one. |
|
|
150
177
|
|
|
151
178
|
## flowSpecSchema
|
|
152
179
|
|