@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/docs/reference/flow.md
CHANGED
|
@@ -19,6 +19,7 @@ interface Flow<C = unknown, D = unknown> {
|
|
|
19
19
|
on?: Trigger<C, D>[];
|
|
20
20
|
anchor?: string;
|
|
21
21
|
while?: Pred<C, D>;
|
|
22
|
+
collect?: (keyof D & string)[];
|
|
22
23
|
clearOnStart?: (keyof D & string)[];
|
|
23
24
|
steps: Step<C, D>[];
|
|
24
25
|
onEnd?: "end" | "stay" | "reset";
|
|
@@ -37,6 +38,7 @@ interface Flow<C = unknown, D = unknown> {
|
|
|
37
38
|
| `on` | `Trigger<C, D>[]` | none | What starts a run. Absent or empty: only `turn({ start })` or another flow's `then: { flow }` starts it. See [Trigger](trigger.md). |
|
|
38
39
|
| `anchor` | `string` | `'session'` | What a run is keyed to. `'session'` uses the session id. Any other name reads `input.anchors[name].key`, and falls back to the session id when the host did not pass that anchor. |
|
|
39
40
|
| `while` | `Pred<C, D>` | the trigger's `if` | Re-checked before the run moves. When it stops holding, the run ends with `code: 'premise-changed'`. |
|
|
41
|
+
| `collect` | `(keyof D & string)[]` | none | The data this flow needs, as agent field slugs. Talk steps' `collect` says which of it to ask, and in what order. |
|
|
40
42
|
| `clearOnStart` | `(keyof D & string)[]` | none | Fields forgotten when a run of this flow starts, so a second run asks for them again. |
|
|
41
43
|
| `steps` | `Step<C, D>[]` | required | In order. A run enters `steps[0]` and moves to the next step unless `then` says otherwise. See [Step](step.md). |
|
|
42
44
|
| `onEnd` | `'end' \| 'stay' \| 'reset'` | `'end'` | What the run does after its last step. |
|
|
@@ -59,11 +61,13 @@ It enters `steps[0]` in the same turn unless the trigger has `after`. Keys and s
|
|
|
59
61
|
|
|
60
62
|
**`while`.** Checked every time the run is about to move: at the start of each turn's run phase for a running run, and when an asking run resumes on a message. A parked run (`waiting`) or a suspended one is not checked until it moves again. Without `while`, the check is the trigger's `if`: the run holds while any trigger of the same kind as the one that started it would still fire. A run started by `start` or by another flow has no such trigger, so without `while` it always holds. A silence run also ends, with `code: 'customer-replied'`, when a wake finds that the customer wrote after the run started.
|
|
61
63
|
|
|
64
|
+
**`collect`.** The flow's data is its `collect` plus every field its talk steps collect. While the flow holds the conversation, or could take it on this message, the understand call notes any of it the customer gives that is still unknown and has `extract: 'anywhere'`. A field no step asks is still noted that way; it is just never asked for. When nobody holds the conversation, the catch-all (`message: []`) that would take the message counts as a flow that could take it. See [Field collection](../concepts/collection.md).
|
|
65
|
+
|
|
62
66
|
**`clearOnStart`.** Applied at start for every trigger kind, `start` and `{ flow }` chains included. Not applied when `onEnd: 'reset'` restarts the flow: reset keeps the data.
|
|
63
67
|
|
|
64
68
|
**`onEnd`.**
|
|
65
69
|
- `'end'`: the run ends with reason `'end'`.
|
|
66
|
-
- `'stay'`: the run
|
|
70
|
+
- `'stay'`: the run goes back to the last talk step it took and stays there, asking. On a branched flow that is the step on its own path; when its log names none, the flow's last talk step. From then on that step answers every message, even when it has nothing left to collect, and each answer is a new visit, so a new key. The steps after it ran once, on the way to the end, and do not run again. If the run finishes on a message nothing has answered yet (no `say`, no `spoke: true`), the step answers it in that same turn. If another run is asking, the staying run waits `suspended` behind it and takes over when that run is done, answering the message that run moved on from without a word. When the customer's message belongs to this run (it was routed to this flow, or it resolved this run's `wait`), the run takes the conversation instead: the other asker is suspended, and this run answers unless its own `say` already did. Its `when` branches are judged on every message and still move the run. Its `if` branches are judged too, except one that leads to `'end'` or to a step the run has already been through: that path already ran and the fact is still true, so it would run again on every message. A field still pending there (a `when` branch took the run to the end before the customer gave it) is asked for on each answer, each with its own key, and `max-asks` is reported once, on the answer that reaches the limit. If the flow is edited off `'stay'` while a run stays, the new `onEnd` applies on that run's next message. A flow with no talk step ends, as with `'end'`.
|
|
67
71
|
- `'reset'`: the run ends with reason `'reset'` and a fresh run of the same flow starts at `steps[0]`, data kept, one hop deeper. A flow that resets forever without asking anything stops at hop 5, with `code: 'hop-limit'`.
|
|
68
72
|
|
|
69
73
|
**Instructions and tools while speaking.** The speak call sees the agent's instructions, then this flow's, then the step's, each already filtered by its `if`. Tools are the step's `tools`; without one, the flow's `tools`; without that, every agent tool.
|
|
@@ -77,7 +81,8 @@ It enters `steps[0]` in the same turn unless the trigger has `after`. Keys and s
|
|
|
77
81
|
- no `id`, or `steps` is not a list
|
|
78
82
|
- a step with no `id`, the id `'end'`, or an id used twice
|
|
79
83
|
- triggers with zero steps
|
|
80
|
-
- an unknown field slug in `clearOnStart`, `collect`, `ask`, `equals`, `known` or a `clear` list
|
|
84
|
+
- an unknown field slug in the flow's `collect`, `clearOnStart`, a step's `collect`, `ask`, `equals`, `known` or a `clear` list
|
|
85
|
+
- a `question` on a talk step that collects nothing
|
|
81
86
|
- an unknown action in `do`; a `with` that misses a required parameter, names one the action does not have, or gives a value of the wrong type (`with` values are not coerced; a `{{template}}` string is accepted for any enum)
|
|
82
87
|
- an unknown event in a trigger or in `wait: { event }`
|
|
83
88
|
- an unknown condition name, or a malformed built-in (`equals` not an object, `known` not a list, `silenced` not a boolean); an `equals` value whose type does not match the field
|
|
@@ -87,7 +92,7 @@ It enters `steps[0]` in the same turn unless the trigger has `after`. Keys and s
|
|
|
87
92
|
- a branch with neither `when` nor `if`
|
|
88
93
|
- an `if` step whose `then` jumps backward with no `else`
|
|
89
94
|
|
|
90
|
-
It returns warnings, logged by the agent, for
|
|
95
|
+
It returns warnings, logged by the agent, for three things that run but probably not as intended: a jump backward without `clear` (the fields collected since stay known, so those steps skip), a `collect` step with no `prompt`, no `question` and no `ask` on any of its fields, and a field in the flow's `collect` that only an answer can fill (`extract: 'asked'`) when no step asks it.
|
|
91
96
|
|
|
92
97
|
`toSpec(flow)` throws `FlowConfigurationError` when a predicate is a function, because a function cannot be stored as JSON.
|
|
93
98
|
|
|
@@ -25,7 +25,7 @@ type StepOutcomeCode =
|
|
|
25
25
|
// A run ended early
|
|
26
26
|
| "step-loop" | "step-gone" | "customer-replied" | "premise-changed" | "silenced"
|
|
27
27
|
// A step
|
|
28
|
-
| "already-known" | "another-reply" | "already-sent" | "branch" | "max-asks"
|
|
28
|
+
| "already-known" | "asked-fixed" | "fixed-in-reply" | "another-reply" | "already-sent" | "branch" | "max-asks"
|
|
29
29
|
| "inline-delay" | "awaiting-trigger" | "awaiting-event" | "event-arrived"
|
|
30
30
|
| "no-event" | "replied" | "no-reply"
|
|
31
31
|
// A host action
|
|
@@ -134,6 +134,8 @@ Grouped by what produced it. `kind` and `status` are given as `kind / status`. A
|
|
|
134
134
|
|---|---|---|---|
|
|
135
135
|
| `prompt` or `collect / ok` | none; `llmCalls` set | The speak call answered. The run is asking if fields are still pending, else it moved. | none. This line never carries `next`, even when the run moves on; the lines that follow show where it went |
|
|
136
136
|
| `collect / skipped` | `already-known` | The step was entered and every field it collects was already known (or at `maxAsks`). No call. | `then` |
|
|
137
|
+
| `collect / ok` | `asked-fixed` | The step's `question` went out word for word as its first ask. No call; the run is asking. | |
|
|
138
|
+
| `collect / ok` | `fixed-in-reply`; `llmCalls` set | The customer's message asked something on the step's first ask, so one reply answered it and asked the step's `question`, in its words where they fit. It counts as one ask; the run is asking. | |
|
|
137
139
|
| `collect / ok` | none, no `llmCalls` | An asking step whose remaining fields were all known when the customer's next message resumed it: the understand call filled them in from the message, or an action's `ctx.set()` or a tool's `data` had written them since the step last asked. It moved without speaking. | `then` |
|
|
138
140
|
| `collect / skipped` | `max-asks`, `detail` = the field slug | One line per field still unknown when the step moves on because that field reached `maxAsks` (default 3). | |
|
|
139
141
|
| `prompt` or `collect / ok` | `branch` | A branch of the asking step fired: an `if` branch held, or the model answered `when` with true. | the branch's `then` |
|
|
@@ -226,7 +228,7 @@ The run leaves `session.runs`, appears in `TurnResult.ended`, and writes one lin
|
|
|
226
228
|
| `cooldown` | `repeat: { cooldown }` and the last run is younger than the cooldown. |
|
|
227
229
|
| `already-running` | A live run of this flow exists for this anchor, in this session or (through `turn({ claims })`) in another of the customer's sessions. |
|
|
228
230
|
| `hop-limit` | The start would be at hop 5. `{ flow }` jumps and `onEnd: 'reset'` each add a hop. |
|
|
229
|
-
| `flow-gone` | `turn({ start })`, a silence wake, or a `{ flow }` jump named a flow the agent does not have. |
|
|
231
|
+
| `flow-gone` | `turn({ start })`, a silence wake, or a `{ flow }` jump named a flow the agent does not have. For a literal `{ flow }` id, the agent build also logs a warning. |
|
|
230
232
|
|
|
231
233
|
A trigger whose `if` is false starts nothing and writes nothing.
|
|
232
234
|
|
|
@@ -468,7 +468,7 @@ What the adapter does on every call, from `src/providers/ProviderAdapter.ts`:
|
|
|
468
468
|
3. Merges `defaults`, then `parameters.maxOutputTokens` as `maxTokens` and `parameters.reasoning.effort` as `effort`.
|
|
469
469
|
4. Streams with a silence watchdog of `retryConfig.timeout` ms and `retryConfig.retries + 1` attempts, then moves to the next backup model when the error kind allows.
|
|
470
470
|
5. Folds the chunks: text becomes `delta` chunks, tool call fragments are assembled and their arguments parsed, usage lands on `metadata` (`tokensUsed`, `promptTokens`, `completionTokens`, `cachedInputTokens`).
|
|
471
|
-
6. Parses the accumulated text leniently into `structured`. Text that looks like an envelope but did not parse is dropped rather than handed to the customer. A message that is blank after parsing, with no tool calls, throws `Error: No response from <provider>` — after the retries and backup models, so the caller's path (a deferred speak step, or the host replaying the turn) is what picks it up. A stream that
|
|
471
|
+
6. Parses the accumulated text leniently into `structured`. Text that looks like an envelope but did not parse is dropped rather than handed to the customer. A message that is blank after parsing, with no tool calls, throws `Error: No response from <provider>` — after the retries and backup models, so the caller's path (a deferred speak step, or the host replaying the turn) is what picks it up. A stream that produced no text and no tool call is caught earlier, as a `ProviderError`; reasoning alone is not an answer. When it stopped at `maxTokens` the kind is `invalid`, never retried and never sent to a backup model, and the message says how many tokens went to thinking: `the output cap ran out before any answer (2048 of 2048 output tokens went to reasoning) — raise maxTokens or lower effort`. Any other empty stream is `overload`. A stream that stops before the provider's end signal (no finish reason, no `[DONE]`) throws a `ProviderError` of kind `network` once what arrived is delivered, so half an answer is never read as a whole one.
|
|
472
472
|
|
|
473
473
|
## Retries, backup models and fallbacks
|
|
474
474
|
|
|
@@ -480,7 +480,7 @@ Three layers, innermost first. All from `src/providers/ProviderAdapter.ts` and `
|
|
|
480
480
|
| Backup model | `backupModels` | Retries exhausted with a backup-eligible kind, or `kind === "model"` (the endpoint does not serve that id). | Next model on the list, same provider. |
|
|
481
481
|
| Fallback | `fallbacks` (per provider) or `FallbackAiProvider` | The provider fails or is on cooldown. | Next provider. Cooldowns per `kind`; a server-supplied `Retry-After` always wins over the default interval. |
|
|
482
482
|
|
|
483
|
-
The `timeout` bounds silence, not the whole call: a stream that keeps producing tokens is left alone, one that goes quiet for 60 s is cut and retried.
|
|
483
|
+
The `timeout` bounds silence, not the whole call: a stream that keeps producing tokens is left alone, one that goes quiet for 60 s is cut and retried. Keep-alives count as life. The wait for the first chunk, and a stream that sends only keep-alives, are cut by `@providerkit/core`'s progress clock: at 5 minutes, or at `timeout` when that is longer. A provider that rotates through models of its own, each watched on its own (an OpenCode Go chain), takes a long `timeout` so this watch never cuts the rotation short.
|
|
484
484
|
|
|
485
485
|
## Reasoning
|
|
486
486
|
|
|
@@ -490,7 +490,7 @@ interface ReasoningConfig {
|
|
|
490
490
|
}
|
|
491
491
|
```
|
|
492
492
|
|
|
493
|
-
`GenerateMessageInput.parameters.reasoning` is on the interface for a custom caller, but the framework never sets it. The one way to reach the wire is `config.effort` on a provider, which the adapter sends with every call as the provider's default. Absent means the model's own dynamic thinking and is never sent; `"none"` is the only way to say do not think. It matters most under a small `maxTokens`, where thinking tokens and the answer share one budget
|
|
493
|
+
`GenerateMessageInput.parameters.reasoning` is on the interface for a custom caller, but the framework never sets it. The one way to reach the wire is `config.effort` on a provider, which the adapter sends with every call as the provider's default. Absent means the model's own dynamic thinking and is never sent; `"none"` is the only way to say do not think. It matters most under a small `maxTokens`, where thinking tokens and the answer share one budget: a turn that spends all of it thinking fails as `invalid`. Support varies per model; an unsupported level comes back as a 400 (`kind: "invalid"`).
|
|
494
494
|
|
|
495
495
|
## The AiProvider interface
|
|
496
496
|
|
|
@@ -618,6 +618,8 @@ Do not pick from that list. Ask the model you ship.
|
|
|
618
618
|
|
|
619
619
|
Every `ProviderAdapter` subclass has `probeJsonWithTools(opts?)`. It asks the bound model, on the wire, both ways, and reports which shape called the tool on every sample.
|
|
620
620
|
|
|
621
|
+
The probe asks the adapter's own model, never its fallbacks, one call at a time. A rate-limited fallback cannot make a working primary look broken, and a primary that fails the probe throws. A `FallbackAiProvider` has no probe of its own: probe each adapter you put in it.
|
|
622
|
+
|
|
621
623
|
```ts fragment
|
|
622
624
|
interface JsonWithToolsProbe {
|
|
623
625
|
/** The shape to configure, or null when neither called the tool on every sample. */
|
|
@@ -38,6 +38,7 @@ interface Run {
|
|
|
38
38
|
hop: number;
|
|
39
39
|
startedAt: string;
|
|
40
40
|
suspendedAt?: string;
|
|
41
|
+
staying?: true;
|
|
41
42
|
waiting?: { kind: "timer" | "event"; key?: string; until?: string; setAt: string; event?: string };
|
|
42
43
|
asked: Record<string, number>;
|
|
43
44
|
visits: Record<string, number>;
|
|
@@ -86,6 +87,7 @@ From `src/types/session.ts`. Every date is ISO 8601 text, never a `Date`: the bl
|
|
|
86
87
|
| `hop` | `number` | Chaining depth. `0` for a run a trigger started; `+1` per `{ flow }` move and per `onEnd: 'reset'`. A start at hop 5 is skipped: `code: 'hop-limit'` on `TurnResult.skipped`. |
|
|
87
88
|
| `startedAt` | `string` | The clock's now when the run started. |
|
|
88
89
|
| `suspendedAt` | `string?` | Set while `suspended`. The most recently suspended run is the one that resumes. |
|
|
90
|
+
| `staying` | `true?` | Set once an `onEnd: 'stay'` run has finished its steps and sits on its last talk step, answering every message. Any other move clears it. |
|
|
89
91
|
| `waiting` | object? | Set while `waiting`. See below. |
|
|
90
92
|
| `asked` | `Record<string, number>` | Per field, how many times a talk step spoke with that field still pending. A field at the step's `maxAsks` (default 3, `src/utils/schema.ts`) leaves the pending set: `code: 'max-asks'`, with the field's slug in `detail`. |
|
|
91
93
|
| `visits` | `Record<string, number>` | Per step, how many times this run entered it. Part of every message and action key, so a revisit mints new keys. |
|
package/docs/reference/step.md
CHANGED
|
@@ -44,6 +44,7 @@ The model speaks. A guideline, fields to collect, or both.
|
|
|
44
44
|
```ts fragment
|
|
45
45
|
type TalkStep<C, D> = ({ prompt: Template; collect?: (keyof D & string)[] } | { collect: (keyof D & string)[]; prompt?: Template }) & {
|
|
46
46
|
ask?: Partial<Record<keyof D & string, string>>;
|
|
47
|
+
question?: Template;
|
|
47
48
|
maxAsks?: number;
|
|
48
49
|
branches?: Branch<C, D>[];
|
|
49
50
|
tools?: string[];
|
|
@@ -54,8 +55,9 @@ type TalkStep<C, D> = ({ prompt: Template; collect?: (keyof D & string)[] } | {
|
|
|
54
55
|
| Field | Type | Default | Meaning |
|
|
55
56
|
|---|---|---|---|
|
|
56
57
|
| `prompt` | `Template` | "Collect what is still missing below, in the flow of the conversation, one or two things per message." | The guideline for the reply. Required when there is no `collect`. |
|
|
57
|
-
| `collect` | `(keyof D & string)[]` | none | Fields to
|
|
58
|
+
| `collect` | `(keyof D & string)[]` | none | Fields to ask for now, in this order. The step is done when they are known. The flow's own `collect` lists everything it needs; see [Flow](flow.md). |
|
|
58
59
|
| `ask` | `Partial<Record<slug, string>>` | the field's own `ask` | Per-flow wording for a field. |
|
|
60
|
+
| `question` | `Template` | none | A fixed first question, sent word for word with no model call. When the customer's message asks something, one AI reply answers it and asks this question. Needs `collect`. |
|
|
59
61
|
| `maxAsks` | `number` | `3` | Times a field may be asked before it is skipped (`code: 'max-asks'`, the field in `detail`). |
|
|
60
62
|
| `branches` | `Branch<C, D>[]` | none | Exits judged while the step asks. See [Branches](branches.md). |
|
|
61
63
|
| `tools` | `string[]` | the flow's `tools`, else all | Tools the model may call from this step. |
|
|
@@ -64,6 +66,8 @@ type TalkStep<C, D> = ({ prompt: Template; collect?: (keyof D & string)[] } | {
|
|
|
64
66
|
- Pending fields are `collect` minus the known ones minus those at `maxAsks`, in `collect` order. A step the run enters whose pending list is empty is skipped with no model call (`code: 'already-known'`) and the run follows `then`. When the asking run resumes and the message filled the last field, the same move is logged `ok` with no detail.
|
|
65
67
|
- Reaching a talk step suspends any other run that was asking; this run becomes the asker and holds the floor. It speaks in this turn if the speak call has not happened yet; otherwise it speaks on the next message.
|
|
66
68
|
- The speak call returns the message plus one value per pending field. Values are validated and written; each field still pending is counted as asked once more. With pending fields left the run stays asking. With none left, or with no `collect` at all, the run follows `then` in the same turn.
|
|
69
|
+
- With `question`, the step's first ask is that text, as a `kind: 'verbatim'` message (`code: 'asked-fixed'`). It goes out only when every field in `collect` is still pending and none was asked yet, and never on a run that stays (`onEnd: 'stay'`). Reached after this turn's speak call, it goes out in the same turn, the way a `say` does. It counts as one ask of each field.
|
|
70
|
+
- When the customer's message asks something ("quanto tá o 13?"), one AI reply answers it and asks the question, in these words where they fit: only what the answer already covered, or what would contradict it, changes. Sending the text word for word after an answer could repeat or contradict it. The outcome line carries `code: 'fixed-in-reply'`. The understand call judges whether the message asks something (`asks`), and only while some flow has a fixed question. A message that only answers or greets gets the question word for word, with no speak call. An unjudged message, or the retry of a failed answer, counts as asking. Either way the question counts as one ask, so the step moves on at `maxAsks` whatever the customer does. Every later ask is the model's own wording. A `{ step, clear }` that clears the step's fields also clears their ask count, so the question goes out again.
|
|
67
71
|
- With `silenced` set, a talk step ends the run with `code: 'silenced'` (`detail` = your reason); a run that was already asking stays asking instead.
|
|
68
72
|
- Outcome kind: `collect` when `collect` is non-empty, else `prompt`.
|
|
69
73
|
|
|
@@ -153,11 +157,11 @@ The code forks. No model call.
|
|
|
153
157
|
| Form | Meaning |
|
|
154
158
|
|---|---|
|
|
155
159
|
| `'passo'` | Jump to that step id. |
|
|
156
|
-
| `'end'` | Finish the run here, exactly as running past the last step does — `onEnd` still decides: `'end'` ends it, `'stay'`
|
|
157
|
-
| `{ step: 'passo', clear: ['campo'] }` | Delete the listed fields from `session.data
|
|
158
|
-
| `{ flow: 'outro', input? }` | End this run (reason `'flow'`) and start `outro` in the same turn, one hop deeper. The child gets `input`, or this run's `input` when absent
|
|
160
|
+
| `'end'` | Finish the run here, exactly as running past the last step does — `onEnd` still decides: `'end'` ends it, `'stay'` goes back to the last talk step and answers every message from there, `'reset'` starts a fresh run. `'end'` is reserved: no step may use it as an id. |
|
|
161
|
+
| `{ step: 'passo', clear: ['campo'] }` | Delete the listed fields from `session.data` and forget how many times the run asked them, then jump. The way to ask something again. |
|
|
162
|
+
| `{ flow: 'outro', input? }` | End this run (reason `'flow'`) and start `outro` in the same turn, one hop deeper. The child gets `input`, or this run's `input` when absent. It holds the floor when this run did, or when no run did: a `mention` flow that chains does not take the message from the run it was routed to. `flow` is a template. |
|
|
159
163
|
|
|
160
|
-
Entering a step counts a visit; the visit is part of every key minted there, so a step visited twice sends twice. A `{ step }` jump to an id that no longer exists ends the run with `code: 'step-gone'`; a `{ flow }` to an unknown flow is skipped with `code: 'flow-gone'`.
|
|
164
|
+
Entering a step counts a visit; the visit is part of every key minted there, so a step visited twice sends twice. A `{ step }` jump to an id that no longer exists ends the run with `code: 'step-gone'`; a `{ flow }` whose template resolves to an unknown flow is skipped with `code: 'flow-gone'`. A literal flow id the agent does not have is skipped the same way, and the agent build logs a warning for it.
|
|
161
165
|
|
|
162
166
|
## Caps
|
|
163
167
|
|
package/docs/reference/stores.md
CHANGED
|
@@ -261,6 +261,7 @@ return nil
|
|
|
261
261
|
`ARGV` is the expected version, the next version, the blob, now as ISO text and the TTL in seconds. `nil` back is success; a version back is the conflict (`actualVersion` is that number); `'missing'` means the hash is gone (`actualVersion` is `undefined`).
|
|
262
262
|
|
|
263
263
|
- `sessionTTL` defaults to `7 * 24 * 60 * 60` = 604800 seconds and is reset on every save. `0` never expires.
|
|
264
|
+
- A session that expires takes its parked runs and claims with it. A wait longer than the TTL (an event wait defaults to 30 days) wakes to no session, and a `once` flow can fire again. Set `sessionTTL` above your longest wait, or to `0`.
|
|
264
265
|
- `load` returns `null` when the hash has no fields.
|
|
265
266
|
|
|
266
267
|
### Example
|
|
@@ -409,12 +410,13 @@ console.log(await store.load("s1")); // null on a first turn
|
|
|
409
410
|
|
|
410
411
|
## OpenSearchStore
|
|
411
412
|
|
|
412
|
-
Over `@opensearch-project/opensearch`'s client; Elasticsearch 7.x fits the same calls. One document per session, with `blob` stored but not indexed.
|
|
413
|
+
Over `@opensearch-project/opensearch`'s client; Elasticsearch 7.x fits the same calls. One document per session, with `blob` stored but not indexed. Like the other stores, the constructor takes one object with the client in it.
|
|
413
414
|
|
|
414
415
|
### Signature
|
|
415
416
|
|
|
416
417
|
```ts fragment
|
|
417
418
|
interface OpenSearchStoreOptions {
|
|
419
|
+
client: OpenSearchClient;
|
|
418
420
|
/** Index name. Default `agent_sessions`. */
|
|
419
421
|
indices?: { sessions?: string };
|
|
420
422
|
/** Create the index with its mappings on `initialize()`. Default true. */
|
|
@@ -435,7 +437,7 @@ interface OpenSearchClient {
|
|
|
435
437
|
}
|
|
436
438
|
|
|
437
439
|
class OpenSearchStore<D = unknown> implements Store<D> {
|
|
438
|
-
constructor(
|
|
440
|
+
constructor(options: OpenSearchStoreOptions);
|
|
439
441
|
/** Create the index with its mappings when it is missing and `autoCreateIndices` is on. */
|
|
440
442
|
initialize(): Promise<void>;
|
|
441
443
|
}
|
|
@@ -479,7 +481,7 @@ import type { OpenSearchClient } from "@falai/agent";
|
|
|
479
481
|
// const client = new Client({ node: process.env.OPENSEARCH_URL }); // from @opensearch-project/opensearch
|
|
480
482
|
declare const client: OpenSearchClient;
|
|
481
483
|
|
|
482
|
-
const store = new OpenSearchStore<{ nome: string }>(client,
|
|
484
|
+
const store = new OpenSearchStore<{ nome: string }>({ client, indices: { sessions: "conversas" }, refresh: "wait_for" });
|
|
483
485
|
await store.initialize();
|
|
484
486
|
console.log(await store.load("s1")); // null on a first turn
|
|
485
487
|
```
|
|
@@ -85,7 +85,7 @@ A trigger whose phrases are *all* exclusions can never fire, so `validateFlow` r
|
|
|
85
85
|
| `event` | `string` | required | The event's name in the agent's `events`. |
|
|
86
86
|
| `after` | `Duration` | none | Park the run this long before its first step. |
|
|
87
87
|
| `if` | `Pred<C, D>` | none | Judged when the event arrives, with the payload as `input`. |
|
|
88
|
-
| `businessHours` | `boolean` | `false` |
|
|
88
|
+
| `businessHours` | `boolean` | `false` | Start only in working hours: snap the start (now, or now + `after`) forward with the agent's `businessHours`. |
|
|
89
89
|
| `repeat` | `Repeat` | `'always'` | |
|
|
90
90
|
|
|
91
91
|
The event's `payload` is the run's `input`.
|
|
@@ -116,7 +116,7 @@ Pass a real message `id` on every message turn. Only `id` is checked against the
|
|
|
116
116
|
|
|
117
117
|
**Dedupe key.** `${flowId}:${anchor}:${nonce}`. The nonce is the trigger key when `repeat` is `'always'` and empty otherwise. It is written to `session.claims` when the run starts, given to actions as `ctx.dedupeKey`, and returned in `started[]`. The last 50 `'always'` claims per flow and anchor are kept; `'once'` and cooldown claims are never pruned. The host may pass claims from the customer's other sessions in `turn({ claims })`; they count the same.
|
|
118
118
|
|
|
119
|
-
**Wake for `after`.** `${runId}:start:${atMs}`, where `atMs` is the fire time in milliseconds after `businessHours` snapping. The run is returned in `started[]` at once with the outcome `code: 'awaiting-trigger'`. At the wake it enters its first step. A second event for the same flow and anchor while it is parked replaces it: the parked run ends with reason `'replaced'`.
|
|
119
|
+
**Wake for `after`.** `${runId}:start:${atMs}`, where `atMs` is the fire time in milliseconds after `businessHours` snapping. With `businessHours: true` and no `after`, the same wake parks a run whose event arrives outside working hours; inside them it starts at once. The run is returned in `started[]` at once with the outcome `code: 'awaiting-trigger'`. At the wake it enters its first step. A second event for the same flow and anchor while it is parked replaces it: the parked run ends with reason `'replaced'`.
|
|
120
120
|
|
|
121
121
|
**Wake for silence.** `silence:${flowId}:${sessionId}:${lastAssistantAtMs}`. Armed at the end of every turn in which the assistant spoke and the customer has not written since, for every silence flow passing `if` and `repeat`; the entry's `replaces` names the previous silence wake. At fire time it is honoured only while `session.lastAssistantAt` still equals that timestamp and the customer has not written since (`code: 'silence-broken'` otherwise).
|
|
122
122
|
|
package/docs/rfc/v4-one-flow.md
CHANGED
|
@@ -104,6 +104,7 @@ type Trigger<C, D, Cond, E> = { repeat?: Repeat } & ( // default: message/m
|
|
|
104
104
|
|
|
105
105
|
type Talk<C, D, Cond> = ({ prompt: Template; collect?: (keyof D)[] } | { collect: (keyof D)[]; prompt?: Template }) & {
|
|
106
106
|
ask?: Partial<Record<keyof D, string>>; // per-flow wording; schema `ask` is the default
|
|
107
|
+
question?: Template; // fixed first ask, verbatim, no call; needs collect
|
|
107
108
|
maxAsks?: number; branches?: Branch<C, D, Cond>[]; tools?: string[]; instructions?: Instruction<C, D>[];
|
|
108
109
|
};
|
|
109
110
|
|
|
@@ -121,6 +122,7 @@ interface Flow<C, D, Cond, A extends ActionMap, E> {
|
|
|
121
122
|
on?: Trigger<C, D, Cond, E>[]; // absent or [] = Início manual
|
|
122
123
|
anchor?: string; // 'session' (default) or a host anchor name — "vale por conversa / por lead"
|
|
123
124
|
while?: Pred<C, D, Cond>; // re-checked whenever the run moves; default = trigger `if`
|
|
125
|
+
collect?: (keyof D)[]; // the data this flow needs; steps' collect orders the asks
|
|
124
126
|
clearOnStart?: (keyof D)[];
|
|
125
127
|
steps: Step<C, D, Cond, A, E>[]; // ids required, unique, never 'end'
|
|
126
128
|
onEnd?: 'end' | 'stay' | 'reset'; // default 'end'
|
|
@@ -239,7 +241,7 @@ type TurnInput<C, D, E> = {
|
|
|
239
241
|
interface TurnResult<D> {
|
|
240
242
|
session: Session<D>; changed: boolean; // changed: false → save nothing
|
|
241
243
|
messages: Array<{ text: string; kind: 'ai' | 'verbatim'; media?: { slug: string }; afterMs: number; key: string; runId?: string; stepId?: string }>;
|
|
242
|
-
schedule: Array<{ key: string; at: Date; replaces?: string }>; //
|
|
244
|
+
schedule: Array<{ key: string; at: Date; replaces?: string }>; // job id = the key, encoded (BullMQ refuses ':'); at fire: turn({ wake: key })
|
|
243
245
|
outcomes: StepOutcome[];
|
|
244
246
|
started: Array<{ runId: string; flowId: string; anchor: string; dedupeKey: string }>;
|
|
245
247
|
ended: Array<Run & { reason: 'end' | 'flow' | 'reset' | 'skipped' | 'failed' | 'replaced' }>;
|
|
@@ -304,7 +306,7 @@ interface Store<D> { load(id: string): Promise<Session<D> | null>; save(session:
|
|
|
304
306
|
|
|
305
307
|
**Keys.** Trigger key: `message`/`mention` → the input `id` (playground: `at`, replays not idempotent); `silence` → `lastAssistantAtMs`; `event`/`start` → the host key; `flow` → `${parentRunId}:${stepId}:${visit}`. Wake key: `${runId}:${stepId}:${atMs}`. Dedupe key `${flowId}:${anchor}:${nonce}`, nonce `''` for `once` and cooldown (blocked while `now - claims[key].at < cooldown`, else overwritten), the trigger key for `always` (last 50 kept).
|
|
306
308
|
|
|
307
|
-
**Host contract:** (a) one `turn` per session at a time; a wake queues behind a debounced inbound not yet turned. (b) On every input: fresh `context`, `history`, `anchors` (with the lead's `lastInboundAt`), `claims` for non-`always` flows, `silenced` for every "cannot speak now" reason (ownership, Pausa, closed 24h window, quota, `sem conversa`); inbound messages carry the channel `id` and `at`. (c) `changed: false` → nothing. Else one transaction: `store.save`, `started[].dedupeKey` into a unique index, `started`/`ended` into a partial unique index `(flowId, anchor) WHERE live`, `outcomes`/`ended`/`skipped` into the `flowRuns` mirror, `messages[]` and `schedule[]` into the outbox. Conflict or unique violation → discard, replay. (d) Drain the outbox: send honoring `afterMs`, record a refused send against `key` in the mirror (`enviada`/`recusada` beside the framework's `gerada`); enqueue wakes
|
|
309
|
+
**Host contract:** (a) one `turn` per session at a time; a wake queues behind a debounced inbound not yet turned. (b) On every input: fresh `context`, `history`, `anchors` (with the lead's `lastInboundAt`), `claims` for non-`always` flows, `silenced` for every "cannot speak now" reason (ownership, Pausa, closed 24h window, quota, `sem conversa`); inbound messages carry the channel `id` and `at`. (c) `changed: false` → nothing. Else one transaction: `store.save`, `started[].dedupeKey` into a unique index, `started`/`ended` into a partial unique index `(flowId, anchor) WHERE live`, `outcomes`/`ended`/`skipped` into the `flowRuns` mirror, `messages[]` and `schedule[]` into the outbox. Conflict or unique violation → discard, replay. (d) Drain the outbox: send honoring `afterMs`, record a refused send against `key` in the mirror (`enviada`/`recusada` beside the framework's `gerada`); enqueue wakes under a job id made from the key (encoded: BullMQ refuses a `:` in a custom id), remove `replaces` the same way, best-effort. (e) At fire: `turn({ wake, silenced })`. (f) Call `turn` for every inbound even while a human owns the lead. (g) `businessHours(at)` snaps, never clamps. (h) Lead-level events go to the lead's latest open conversation; none → session `lead:<id>` with `silenced: 'sem conversa'`. (i) `ProviderError` → retry the input with backoff.
|
|
308
310
|
|
|
309
311
|
```ts
|
|
310
312
|
const clock = fakeClock('2026-09-20T10:00Z');
|
|
@@ -458,7 +460,7 @@ async function runTurn(sessionId: string, input: TurnInputBody) {
|
|
|
458
460
|
await outbox.put(tx, r.messages, r.schedule); // wakes ride in the same transaction
|
|
459
461
|
});
|
|
460
462
|
} catch (e) { if (e instanceof SessionConflictError || isUniqueViolation(e)) continue; throw e; }
|
|
461
|
-
await outbox.drain(sessionId); // send honoring afterMs, enqueue wakes
|
|
463
|
+
await outbox.drain(sessionId); // send honoring afterMs, enqueue wakes under the encoded key
|
|
462
464
|
return;
|
|
463
465
|
}
|
|
464
466
|
}
|
package/docs/start/01-install.md
CHANGED
|
@@ -30,6 +30,8 @@ npm install @falai/agent
|
|
|
30
30
|
pnpm add @falai/agent
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
This tutorial is for 4.x. If your project is on 3.x, start with the [migration guide](../migration/v3-to-v4.md): 3.x code will not compile against v4.
|
|
34
|
+
|
|
33
35
|
The package ships an ESM build, a CommonJS build and its own TypeScript types. There is nothing else to install.
|
|
34
36
|
|
|
35
37
|
## Set a provider key
|
|
@@ -170,7 +170,7 @@ A `wait` longer than 10 seconds, a `silence` trigger or an `event` trigger with
|
|
|
170
170
|
{ key: "triagem#wamid.HBgL:w1:1790244000000", at: new Date("2026-09-24T10:00:00.000Z") }
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
-
A `silence` trigger's wake also carries `replaces`: the earlier silence wake it supersedes. Put the entry in your queue with
|
|
173
|
+
A `silence` trigger's wake also carries `replaces`: the earlier silence wake it supersedes. Put the entry in your queue with the key and the session id in the payload, under a job id made from the key: every key contains `:`, and BullMQ refuses a custom job id with a `:` in it unless it splits into exactly three parts, so encode it as `encodeURIComponent(key)`. When `replaces` is set, remove the job whose id is `encodeURIComponent(replaces)`; it is best effort, a stale wake is harmless. When the job fires:
|
|
174
174
|
|
|
175
175
|
```ts fragment
|
|
176
176
|
await handle({ sessionId: job.data.sessionId, wake: job.data.key });
|
|
@@ -178,6 +178,21 @@ await handle({ sessionId: job.data.sessionId, wake: job.data.key });
|
|
|
178
178
|
|
|
179
179
|
Only the run still waiting on that exact key honours the wake. Anything else returns `changed: false` with one line in `r.outcomes`: `code: 'stale-wake'` for a wait that a reply already resolved, `code: 'silence-broken'` when the customer wrote after the silence wake was set, `code: 'no-session'` when there is no session. So you do not have to cancel jobs: fire every one and the framework drops the stale ones.
|
|
180
180
|
|
|
181
|
+
### When the queue loses its jobs
|
|
182
|
+
|
|
183
|
+
A flushed Redis, or sessions lifted from 3.x by `migrateSession`, leave conversations waiting on wakes nobody will fire. `agent.pendingWakes({ session, context })` returns every wake a saved session waits on: each parked run's, and each silence flow's counted from `lastAssistantAt`, with the trigger's `if` and `repeat` judged on the `context` and `claims` you pass, as a turn would. It changes nothing and spends no model call. Enqueue what it returns as you would a turn's `schedule[]`:
|
|
184
|
+
|
|
185
|
+
```ts fragment
|
|
186
|
+
for (const wake of agent.pendingWakes({ session, context })) {
|
|
187
|
+
await queue.add("wake", { sessionId: session.id, key: wake.key }, {
|
|
188
|
+
jobId: encodeURIComponent(wake.key),
|
|
189
|
+
delay: Math.max(0, wake.at.getTime() - Date.now()),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Running it twice is safe: BullMQ ignores a job whose id is already queued, and a wake that fires twice is dropped the second time as stale.
|
|
195
|
+
|
|
181
196
|
`fakeClock` and `MemoryScheduler` are the test doubles for this: [Testing](../guides/testing.md) plays a two-day follow-up in one test.
|
|
182
197
|
|
|
183
198
|
## On every input
|
|
@@ -197,6 +212,8 @@ const history: History = [
|
|
|
197
212
|
|
|
198
213
|
A run that was already asking stays asking and speaks when the gate opens. A run that reaches a new talk step while silenced ends, and the log says `code: 'silenced'` with your reason in `detail`. Keep calling `turn()` for every inbound even while a human owns the customer, so waits resolve and the state stays true. `silenced: { reason, understand: true }` still spends the understand call, so fields keep landing while nothing is said.
|
|
199
214
|
|
|
215
|
+
A closed 24-hour window stops messages, not the follow-up. Pass `silenced: { reason, skip: true }` for a gate like that: a talk or `say` step is skipped (`status: 'skipped'`, `code: 'silenced'`) and the run takes the step's `then`, so the `do` step after it still alerts the team. Keep the plain string for a human owner or a paused assistant, where the whole run must stop.
|
|
216
|
+
|
|
200
217
|
**`context`** is the per-turn data your flows and actions read, such as the customer record. It is typed once, `falai<Ctx>()`, and passed on every input. Ana has none. [Agent](../reference/agent.md) has the full `TurnInput`.
|
|
201
218
|
|
|
202
219
|
## Sessions from before v4
|
package/examples/05-branches.ts
CHANGED
|
@@ -54,7 +54,7 @@ const agent = f.agent({
|
|
|
54
54
|
then: "dados",
|
|
55
55
|
},
|
|
56
56
|
],
|
|
57
|
-
// After the last step the run
|
|
57
|
+
// After the last step the run goes back to the last talk step it took, so follow-up questions land there.
|
|
58
58
|
onEnd: "stay",
|
|
59
59
|
}),
|
|
60
60
|
f.flow({
|
|
@@ -117,9 +117,10 @@ const agent = f.agent({
|
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
// ─── The host loop ─────────────────────────────────────────────────────────
|
|
120
|
-
// Real hosts persist `session`, enqueue each `schedule[]` entry
|
|
121
|
-
// `
|
|
122
|
-
// them in memory and jump
|
|
120
|
+
// Real hosts persist `session`, enqueue each `schedule[]` entry under the job
|
|
121
|
+
// id `encodeURIComponent(key)` (BullMQ refuses a `:` in a custom id), and call
|
|
122
|
+
// `turn({ wake: key })` when it fires. Here we keep them in memory and jump
|
|
123
|
+
// the clock.
|
|
123
124
|
|
|
124
125
|
const context: Ctx = { lead: { id: "456", nome: "Ana", etapa: "proposta", dono: "ia", tags: [] } };
|
|
125
126
|
const timers: ScheduleEntry[] = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@falai/agent",
|
|
3
|
-
"
|
|
3
|
+
"packageManager": "bun@1.4.2",
|
|
4
|
+
"version": "4.0.0",
|
|
4
5
|
"description": "Conversational state engine for TypeScript where the AI understands, but the code is in control",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "./dist/cjs/index.js",
|
|
@@ -87,7 +88,7 @@
|
|
|
87
88
|
},
|
|
88
89
|
"devDependencies": {
|
|
89
90
|
"@eslint/js": "^9.17.0",
|
|
90
|
-
"@types/bun": "^1.
|
|
91
|
+
"@types/bun": "^1.4.2",
|
|
91
92
|
"@types/node": "^20.19.22",
|
|
92
93
|
"@types/pg": "^8.15.5",
|
|
93
94
|
"eslint": "^9.17.0",
|
|
@@ -96,7 +97,7 @@
|
|
|
96
97
|
"typescript-eslint": "^8.18.2"
|
|
97
98
|
},
|
|
98
99
|
"dependencies": {
|
|
99
|
-
"@providerkit/core": "^0.
|
|
100
|
+
"@providerkit/core": "^0.20.0",
|
|
100
101
|
"loglevel": "^1.9.2"
|
|
101
102
|
},
|
|
102
103
|
"peerDependencies": {
|
package/src/core/Agent.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* provider call apiece, and Runner settles what they returned.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { AgentOptions, TurnInput, TurnResult, TurnStreamChunk } from "../types/agent.js";
|
|
10
|
+
import type { AgentOptions, PendingWakesInput, ScheduleEntry, TurnInput, TurnResult, TurnStreamChunk } from "../types/agent.js";
|
|
11
11
|
import type { TokenUsage } from "../types/ai.js";
|
|
12
12
|
import type { CompactionOptions } from "../types/compaction.js";
|
|
13
13
|
import { FlowConfigurationError } from "../types/errors.js";
|
|
@@ -15,7 +15,7 @@ import { logger, LoggerLevel } from "../utils/logger.js";
|
|
|
15
15
|
import { addUsage } from "../utils/usage.js";
|
|
16
16
|
import { CompactionEngine } from "./CompactionEngine.js";
|
|
17
17
|
import type { IdleRequest, SpeakOutcome, TalkRequest } from "./contracts.js";
|
|
18
|
-
import { validateFlow } from "./FlowSpec.js";
|
|
18
|
+
import { BUILT_IN_CONDITIONS, checkPred, validateFlow } from "./FlowSpec.js";
|
|
19
19
|
import { Runner, type Turn } from "./Runner.js";
|
|
20
20
|
import { Speak } from "./Speak.js";
|
|
21
21
|
import { Understand } from "./Understand.js";
|
|
@@ -56,6 +56,16 @@ export class Agent<C = unknown, D = unknown> {
|
|
|
56
56
|
yield { done: true, result: this.runner.finish(turn) };
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Every wake a saved session is waiting on, as its turns scheduled them: each parked run's,
|
|
61
|
+
* and each silence flow's since the assistant last spoke. For a queue that lost its jobs, and
|
|
62
|
+
* for sessions no turn has armed yet, such as blobs lifted by `migrateSession`. It changes
|
|
63
|
+
* nothing and spends no call: enqueue the entries as you would a turn's `schedule[]`.
|
|
64
|
+
*/
|
|
65
|
+
pendingWakes(input: PendingWakesInput<C, D>): ScheduleEntry[] {
|
|
66
|
+
return this.runner.pendingWakes(input);
|
|
67
|
+
}
|
|
68
|
+
|
|
59
69
|
/** Load, Ingest, Understand, Decide and Run: everything before the one speaker is known. */
|
|
60
70
|
private async open(input: TurnInput<C, D>): Promise<{ turn: Turn<C, D>; talk: TalkRequest<C, D> | IdleRequest<C, D> | null }> {
|
|
61
71
|
const { runner } = this;
|
|
@@ -100,6 +110,16 @@ function compactionOptions<C, D>(options: AgentOptions<C, D>): CompactionOptions
|
|
|
100
110
|
|
|
101
111
|
/** Every name a flow uses must resolve now, not on the turn that first reaches it. */
|
|
102
112
|
function validate<C, D>(options: AgentOptions<C, D>): void {
|
|
113
|
+
// A host condition under a built-in's name is never called: the built-in answers first.
|
|
114
|
+
for (const name of Object.keys(options.conditions ?? {})) {
|
|
115
|
+
if (BUILT_IN_CONDITIONS.includes(name)) {
|
|
116
|
+
throw new FlowConfigurationError(
|
|
117
|
+
`[FlowConfigurationError] condition "${name}" shadows a built-in: ${BUILT_IN_CONDITIONS.join(", ")} are reserved. Rename it.`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// An agent-level `if` is judged on every turn, so an unknown name here would throw on every turn.
|
|
122
|
+
options.instructions?.forEach((ins, i) => checkPred(ins.if, "agent", `instructions[${i}].if`, options));
|
|
103
123
|
const ids = new Set<string>();
|
|
104
124
|
for (const flow of options.flows ?? []) {
|
|
105
125
|
if (ids.has(flow.id)) {
|
|
@@ -124,6 +144,7 @@ function validate<C, D>(options: AgentOptions<C, D>): void {
|
|
|
124
144
|
}
|
|
125
145
|
const { idle } = options;
|
|
126
146
|
if (idle && idle !== "silent") {
|
|
147
|
+
idle.instructions?.forEach((ins, i) => checkPred(ins.if, "idle", `instructions[${i}].if`, options));
|
|
127
148
|
const known = new Set((options.tools ?? []).map((tool) => tool.id));
|
|
128
149
|
for (const name of idle.tools ?? []) {
|
|
129
150
|
if (!known.has(name)) {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* 4. auto_compact - summarize old messages via LLM provider
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { classify } from "@providerkit/core";
|
|
11
12
|
import log from "loglevel";
|
|
12
13
|
import type { HistoryItem } from "../types/history.js";
|
|
13
14
|
import type { TokenUsage } from "../types/ai.js";
|
|
@@ -19,13 +20,20 @@ export class CompactionEngine {
|
|
|
19
20
|
* Validate CompactionOptions. Throws on invalid values.
|
|
20
21
|
*/
|
|
21
22
|
static validateOptions(options: CompactionOptions): void {
|
|
23
|
+
if (typeof options.maxTokens !== "number" || !(options.maxTokens > 0)) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`[CompactionEngine] maxTokens is ${String(options.maxTokens)}: it must be above 0. ` +
|
|
26
|
+
`Set it to the most history, in tokens, each call should carry, e.g. 100000.`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
22
29
|
if (
|
|
23
30
|
typeof options.compactionThreshold !== "number" ||
|
|
24
31
|
options.compactionThreshold < 0.5 ||
|
|
25
32
|
options.compactionThreshold > 0.95
|
|
26
33
|
) {
|
|
27
34
|
throw new Error(
|
|
28
|
-
`compactionThreshold must be between 0.5 and 0.95
|
|
35
|
+
`[CompactionEngine] compactionThreshold is ${String(options.compactionThreshold)}: it must be between 0.5 and 0.95. ` +
|
|
36
|
+
`Use 0.8 unless you measured otherwise.`
|
|
29
37
|
);
|
|
30
38
|
}
|
|
31
39
|
if (
|
|
@@ -33,7 +41,7 @@ export class CompactionEngine {
|
|
|
33
41
|
options.preserveRecentCount < 2
|
|
34
42
|
) {
|
|
35
43
|
throw new Error(
|
|
36
|
-
`preserveRecentCount must be
|
|
44
|
+
`[CompactionEngine] preserveRecentCount is ${String(options.preserveRecentCount)}: it must be 2 or more. Use 4, the default.`
|
|
37
45
|
);
|
|
38
46
|
}
|
|
39
47
|
if (
|
|
@@ -41,7 +49,7 @@ export class CompactionEngine {
|
|
|
41
49
|
options.maxToolResultChars <= 0
|
|
42
50
|
) {
|
|
43
51
|
throw new Error(
|
|
44
|
-
`maxToolResultChars must be
|
|
52
|
+
`[CompactionEngine] maxToolResultChars is ${String(options.maxToolResultChars)}: it must be above 0. Use 5000, the default.`
|
|
45
53
|
);
|
|
46
54
|
}
|
|
47
55
|
}
|
|
@@ -171,13 +179,24 @@ export class CompactionEngine {
|
|
|
171
179
|
history: [],
|
|
172
180
|
context: {},
|
|
173
181
|
parameters: {
|
|
174
|
-
|
|
182
|
+
// Thinking spends this cap too, at whatever effort the host's
|
|
183
|
+
// provider runs. 1024 fit the summary but not the thinking: in
|
|
184
|
+
// production a ~250-token answer at effort "high" spent all of
|
|
185
|
+
// a 2,048 cap thinking (OpenCode Go, 2026-09-30). 8192 is the
|
|
186
|
+
// most every built-in provider's common models accept
|
|
187
|
+
// (DeepSeek chat and the older Claude models stop there), so a
|
|
188
|
+
// larger cap would 400 on them.
|
|
189
|
+
maxOutputTokens: 8192,
|
|
175
190
|
jsonSchema: {},
|
|
176
191
|
},
|
|
177
192
|
});
|
|
178
193
|
|
|
179
194
|
return { text: result.message, usage: readUsage(result.metadata) };
|
|
180
|
-
} catch {
|
|
195
|
+
} catch (error) {
|
|
196
|
+
// Kind and message, or a cap spent thinking reads like an outage.
|
|
197
|
+
log.warn(
|
|
198
|
+
`CompactionEngine: the summary call failed (${classify(error)}: ${error instanceof Error ? error.message : String(error)}), falling back to aggressive truncation`
|
|
199
|
+
);
|
|
181
200
|
return null;
|
|
182
201
|
}
|
|
183
202
|
}
|
|
@@ -337,10 +356,7 @@ export class CompactionEngine {
|
|
|
337
356
|
};
|
|
338
357
|
}
|
|
339
358
|
|
|
340
|
-
// Fallback: LLM summarization failed — aggressive truncation
|
|
341
|
-
log.warn(
|
|
342
|
-
"CompactionEngine: LLM summarization failed, falling back to aggressive truncation"
|
|
343
|
-
);
|
|
359
|
+
// Fallback: LLM summarization failed (logged with its reason) — aggressive truncation
|
|
344
360
|
const truncated = CompactionEngine.aggressiveTruncate(
|
|
345
361
|
microCompacted,
|
|
346
362
|
options
|