@falai/agent 4.0.0-alpha.2 → 4.0.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/cjs/core/Agent.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 +8 -3
- 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 +324 -51
- 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 +46 -1
- package/dist/cjs/core/Prompt.js.map +1 -1
- package/dist/cjs/core/Runner.d.ts +42 -4
- package/dist/cjs/core/Runner.d.ts.map +1 -1
- package/dist/cjs/core/Runner.js +292 -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 +60 -21
- 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 +34 -55
- package/dist/cjs/core/Understand.js.map +1 -1
- package/dist/cjs/core/contracts.d.ts +21 -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 +15 -10
- package/dist/cjs/providers/ProviderAdapter.d.ts.map +1 -1
- package/dist/cjs/providers/ProviderAdapter.js +8 -3
- package/dist/cjs/providers/ProviderAdapter.js.map +1 -1
- package/dist/cjs/providers/ZaiProvider.js +1 -1
- package/dist/cjs/providers/ZaiProvider.js.map +1 -1
- package/dist/cjs/types/agent.d.ts +19 -4
- package/dist/cjs/types/agent.d.ts.map +1 -1
- package/dist/cjs/types/ai.d.ts +4 -6
- package/dist/cjs/types/ai.d.ts.map +1 -1
- package/dist/cjs/types/compaction.d.ts +1 -0
- package/dist/cjs/types/compaction.d.ts.map +1 -1
- package/dist/cjs/types/errors.d.ts +4 -9
- package/dist/cjs/types/errors.d.ts.map +1 -1
- package/dist/cjs/types/errors.js +12 -12
- package/dist/cjs/types/errors.js.map +1 -1
- package/dist/cjs/types/flow.d.ts +16 -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 +1 -0
- package/dist/cjs/utils/outcomes.d.ts.map +1 -1
- package/dist/cjs/utils/outcomes.js +1 -0
- package/dist/cjs/utils/outcomes.js.map +1 -1
- package/dist/cjs/utils/phrases.d.ts +25 -0
- package/dist/cjs/utils/phrases.d.ts.map +1 -0
- package/dist/cjs/utils/phrases.js +38 -0
- package/dist/cjs/utils/phrases.js.map +1 -0
- 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 +8 -0
- package/dist/cjs/utils/template.d.ts.map +1 -1
- package/dist/cjs/utils/template.js +47 -3
- 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 +8 -3
- 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 +323 -52
- 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 +44 -1
- package/dist/core/Prompt.js.map +1 -1
- package/dist/core/Runner.d.ts +42 -4
- package/dist/core/Runner.d.ts.map +1 -1
- package/dist/core/Runner.js +293 -73
- package/dist/core/Runner.js.map +1 -1
- package/dist/core/Speak.d.ts.map +1 -1
- package/dist/core/Speak.js +61 -22
- 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 +34 -55
- package/dist/core/Understand.js.map +1 -1
- package/dist/core/contracts.d.ts +21 -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 +15 -10
- package/dist/providers/ProviderAdapter.d.ts.map +1 -1
- package/dist/providers/ProviderAdapter.js +8 -3
- package/dist/providers/ProviderAdapter.js.map +1 -1
- package/dist/providers/ZaiProvider.js +1 -1
- package/dist/providers/ZaiProvider.js.map +1 -1
- package/dist/types/agent.d.ts +19 -4
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/ai.d.ts +4 -6
- package/dist/types/ai.d.ts.map +1 -1
- package/dist/types/compaction.d.ts +1 -0
- package/dist/types/compaction.d.ts.map +1 -1
- package/dist/types/errors.d.ts +4 -9
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/errors.js +12 -12
- package/dist/types/errors.js.map +1 -1
- package/dist/types/flow.d.ts +16 -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 +1 -0
- package/dist/utils/outcomes.d.ts.map +1 -1
- package/dist/utils/outcomes.js +1 -0
- package/dist/utils/outcomes.js.map +1 -1
- package/dist/utils/phrases.d.ts +25 -0
- package/dist/utils/phrases.d.ts.map +1 -0
- package/dist/utils/phrases.js +35 -0
- package/dist/utils/phrases.js.map +1 -0
- 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 +8 -0
- package/dist/utils/template.d.ts.map +1 -1
- package/dist/utils/template.js +47 -3
- package/dist/utils/template.js.map +1 -1
- package/docs/concepts/architecture.md +3 -3
- package/docs/concepts/collection.md +40 -5
- package/docs/concepts/pipeline.md +13 -10
- package/docs/concepts/runs-and-waits.md +2 -2
- package/docs/guides/actions-and-events.md +2 -2
- package/docs/guides/branching.md +4 -2
- package/docs/guides/compaction.md +2 -2
- package/docs/guides/conditions.md +1 -1
- package/docs/guides/error-handling.md +3 -1
- 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 +5 -5
- package/docs/migration/v3-to-v4.md +16 -11
- package/docs/reference/actions-events-conditions.md +2 -2
- package/docs/reference/agent.md +11 -7
- package/docs/reference/branches.md +1 -1
- package/docs/reference/errors.md +10 -7
- package/docs/reference/fields.md +5 -3
- package/docs/reference/flow-spec.md +36 -9
- package/docs/reference/flow.md +8 -3
- package/docs/reference/outcomes.md +3 -2
- package/docs/reference/providers.md +2 -0
- 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 +24 -4
- package/docs/rfc/v4-one-flow.md +7 -5
- package/docs/start/01-install.md +5 -3
- package/docs/start/04-add-tools.md +1 -1
- 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 +5 -3
- package/src/core/Agent.ts +23 -2
- package/src/core/CompactionEngine.ts +10 -3
- package/src/core/FlowSpec.ts +352 -60
- package/src/core/Migrate.ts +2 -1
- package/src/core/Prompt.ts +47 -1
- package/src/core/Runner.ts +290 -69
- package/src/core/Speak.ts +64 -21
- package/src/core/Understand.ts +35 -55
- package/src/core/contracts.ts +21 -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 +18 -13
- package/src/providers/ZaiProvider.ts +1 -1
- package/src/types/agent.ts +20 -5
- package/src/types/ai.ts +4 -6
- package/src/types/compaction.ts +1 -0
- package/src/types/errors.ts +11 -12
- package/src/types/flow.ts +16 -1
- package/src/types/history.ts +0 -10
- package/src/types/index.ts +1 -1
- package/src/types/session.ts +4 -1
- package/src/utils/clock.ts +1 -1
- package/src/utils/outcomes.ts +1 -0
- package/src/utils/phrases.ts +40 -0
- package/src/utils/schema.ts +3 -48
- package/src/utils/template.ts +46 -3
- 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/agent.md
CHANGED
|
@@ -77,7 +77,7 @@ class Agent<C, D> {
|
|
|
77
77
|
| `tools` | `Tool<C, D>[]` | `[]` | Functions the model may call while it speaks. See [Tool](tool.md). |
|
|
78
78
|
| `instructions` | `Instruction<C, D>[]` | `[]` | Agent-level rules, rendered into every speak call. See [Instruction](instruction.md). |
|
|
79
79
|
| `knowledgeBase` | `Record<string, unknown>` | none | Any JSON the model should know. Rendered as nested bullets. |
|
|
80
|
-
| `idle` | `Idle<C, D>` | `{ prompt: "" }` | The one speaker that is not a step. Answers a message when no run holds the floor. `'silent'` mutes it. |
|
|
80
|
+
| `idle` | `Idle<C, D>` | `{ prompt: "" }` | The one speaker that is not a step. Answers a message when no run holds the floor. `'silent'` mutes it; then a lone message flow with no catch-all starts without being scored. |
|
|
81
81
|
| `clock` | `Clock` | `() => new Date()` | Returns "now". Tests pass `fakeClock(iso)`. |
|
|
82
82
|
| `businessHours` | `BusinessHours<C>` | none | `(at, { context }) => Date`. Moves a timer forward to the next working moment when a trigger or wait says `businessHours: true`. |
|
|
83
83
|
| `maxToolLoops` | `number` | `5` | Tool rounds per speak call. `0` disables tools. After the last round the model is asked once more without tools, so a message always comes back. |
|
|
@@ -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.
|
|
@@ -105,7 +109,7 @@ Every input carries the common fields, plus exactly one of the four kinds.
|
|
|
105
109
|
| `sessionId` | `string` | The conversation's id. A first turn creates the session under this id. |
|
|
106
110
|
| `session` | `Session<D>` | The stored session, as the store returned it. Absent on a first turn. A `wake` without a session is ignored. |
|
|
107
111
|
| `context` | `C` | Your ambient data for this turn. Required unless `C` allows `undefined` (`falai()` with no generic). |
|
|
108
|
-
| `history` | `History` | The conversation
|
|
112
|
+
| `history` | `History` | The conversation before this input. Pass it on every input kind, wakes included; both calls read it. Leave out the message this turn carries: both calls quote it on their own, so a history that ends with it is read twice. Store the message after the turn, not before. Without `history` the framework falls back to `session.history`, then to an empty list. |
|
|
109
113
|
| `silenced` | `Silenced` | Your reason the assistant cannot speak right now. See below. |
|
|
110
114
|
| `anchors` | `Record<string, { key: string; lastInboundAt?: string }>` | The host anchors this session belongs to, by name, e.g. `{ lead: { key: 'lead:456', lastInboundAt } }`. A flow with `anchor: 'lead'` keys its runs and claims by `anchors.lead.key`. |
|
|
111
115
|
| `claims` | `{ held: Record<string, string>; active: string[] }` | Claims from the customer's other sessions: `held` maps a dedupe key to the ISO time it was taken; `active` lists live `${flowId}:${anchor}` pairs. A flow already active elsewhere is skipped with `code: 'already-running'`. |
|
|
@@ -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.
|
|
@@ -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,33 +123,57 @@ 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". |
|
|
133
140
|
| Dangling jump | `then points at step "x", which does not exist` (also `else`, `onFail`, `branches[n].then`) | Use an existing step id or "end". |
|
|
134
141
|
| Missing parameter | `action "notify" needs parameter "message"` | Add it to `with`. |
|
|
135
|
-
| Wrong parameter type | `parameter "tags" of action "add_tags" must be a list of
|
|
142
|
+
| Wrong parameter type | `parameter "tags" of action "add_tags" must be a list of strings, got string` | Values are not coerced; write the right type. |
|
|
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
|
|
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" | "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,7 @@ 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, unless the customer asked something and the step answered first; the run is asking. | |
|
|
137
138
|
| `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
139
|
| `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
140
|
| `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 +227,7 @@ The run leaves `session.runs`, appears in `TurnResult.ended`, and writes one lin
|
|
|
226
227
|
| `cooldown` | `repeat: { cooldown }` and the last run is younger than the cooldown. |
|
|
227
228
|
| `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
229
|
| `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. |
|
|
230
|
+
| `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
231
|
|
|
231
232
|
A trigger whose `if` is false starts nothing and writes nothing.
|
|
232
233
|
|
|
@@ -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. No model call unless the customer's message asks something, which is answered first. 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?"), the question waits for the answer: the step speaks first, answering with its flow's instructions and tools and asking nothing, and the question goes out word for word right after it. 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 with no speak call. An unjudged message, or the retry of a failed answer, counts as asking. Every later ask is the model's own wording, so a customer who asks something back gets an answer; `maxAsks` still applies. 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
|
```
|
|
@@ -49,6 +49,26 @@ The run takes the conversation: it becomes the asker and holds the floor — onl
|
|
|
49
49
|
|
|
50
50
|
The run starts beside the conversation. It is meant for `do` and `say` steps: it does not get routed to. A talk step in it behaves like any talk step and takes the floor.
|
|
51
51
|
|
|
52
|
+
### Phrases that rule a trigger out
|
|
53
|
+
|
|
54
|
+
Phrases are alternatives — one match is enough — so a list alone can only say "any of these". A phrase that opens with `!` says the opposite: it stops the trigger, whatever else matched.
|
|
55
|
+
|
|
56
|
+
```ts fragment
|
|
57
|
+
on: [{
|
|
58
|
+
mention: [
|
|
59
|
+
'o cliente pede para falar com uma pessoa',
|
|
60
|
+
'!o cliente só concorda com o horário oferecido', // "pode sim" is a yes to the meeting
|
|
61
|
+
'!o cliente menciona outra pessoa sem pedir atendimento',
|
|
62
|
+
],
|
|
63
|
+
}]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Without the two exclusions, a customer answering "pode sim" to an offer of a meeting reads as a request for a human, because that sentence really does mention a person. The model is given the two lists separately and told that an exclusion overrides a match.
|
|
67
|
+
|
|
68
|
+
Works in `message` (an exclusion scores the flow 0), in `mention` (an exclusion answers false), and in an instruction's `when`. Whitespace around a phrase is trimmed, a bare `"!"` is ignored, and a `!` anywhere but the first character is ordinary text.
|
|
69
|
+
|
|
70
|
+
A trigger whose phrases are *all* exclusions can never fire, so `validateFlow` rejects it. For a flow that should catch everything else, use `message: []`.
|
|
71
|
+
|
|
52
72
|
### silence
|
|
53
73
|
|
|
54
74
|
| Field | Type | Default | Meaning |
|
|
@@ -65,7 +85,7 @@ The run starts beside the conversation. It is meant for `do` and `say` steps: it
|
|
|
65
85
|
| `event` | `string` | required | The event's name in the agent's `events`. |
|
|
66
86
|
| `after` | `Duration` | none | Park the run this long before its first step. |
|
|
67
87
|
| `if` | `Pred<C, D>` | none | Judged when the event arrives, with the payload as `input`. |
|
|
68
|
-
| `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`. |
|
|
69
89
|
| `repeat` | `Repeat` | `'always'` | |
|
|
70
90
|
|
|
71
91
|
The event's `payload` is the run's `input`.
|
|
@@ -96,15 +116,15 @@ Pass a real message `id` on every message turn. Only `id` is checked against the
|
|
|
96
116
|
|
|
97
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.
|
|
98
118
|
|
|
99
|
-
**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'`.
|
|
100
120
|
|
|
101
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).
|
|
102
122
|
|
|
103
123
|
## Behaviour
|
|
104
124
|
|
|
105
125
|
**How message flows are chosen.** On a message turn, the eligible flows are those with a non-empty `message` list passing `if` and `repeat`, in agent order.
|
|
106
|
-
-
|
|
107
|
-
-
|
|
126
|
+
- No run asking: the understand call scores each eligible flow from 0 to 100, a lone one included. The top flow starts if it scores 40 or more; otherwise the first eligible `message: []` flow starts, if there is one.
|
|
127
|
+
- One exception: a single eligible flow, with no `message: []` flow passing and `idle: 'silent'`, starts without being scored, because a low score would leave the customer with no reply. The understand call is then skipped altogether when there is nothing else to judge: no mention flows, no `when` branches, no unknown `'anywhere'` field.
|
|
108
128
|
- A run is asking: it keeps the floor unless another flow scores at least 15 above it and at least 40. Then that flow starts, or its suspended run resumes, and the asker is suspended.
|
|
109
129
|
- The catch-all `message: []` is never scored. It obeys `if` and `repeat` like any trigger, so with the default `'once'` it fires once per session. When nothing starts and no run is asking, the idle speaker answers.
|
|
110
130
|
|