@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/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' }>;
|
|
@@ -265,7 +267,7 @@ Eight phases, one order for every input; only 3 and 6 spend LLM calls. `turn()`
|
|
|
265
267
|
- `wake` starting with `silence:`: honored only when its `lastAssistantAtMs` equals `session.lastAssistantAt` and the lead has not written since (`lastUserAt`, and the anchor's `lastInboundAt` for lead-anchored flows, both `< lastAssistantAt`); it then goes through the start order below with `trigger.kind: 'silence'`. Otherwise `ignorado: silêncio quebrado`, `changed: false`. Any other `wake`: the run whose `waiting.key` equals it, else `ignorado: wake antigo`. A timer `wait` with `else` whose lead wrote after `waiting.setAt` takes `else` — the reply beat the job.
|
|
266
268
|
- `event` / `start`: flows with a matching trigger start, in this order: `if` (payload as `input`) → `repeat` via claims (cooldown = interval check on `claims[key].at`) → `hop < 5` else `pulado: limite de encadeamento` → one active run per (flow, anchor) against `session.runs` and `claims.active`: a live run still parked on its own `after` timer is **replaced** (`ended.reason: 'replaced'`, its job self-skips), any other live run → `pulado: já em andamento` → `after` parks the new run.
|
|
267
269
|
- Every run about to move re-checks `while` with fresh context; false → ends `pulado: premissa mudou`. Silence-triggered runs add the premise "no lead message since the run started" → `pulado: lead escreveu nesse meio-tempo`.
|
|
268
|
-
3. **Understand** (≤1 call, `schemaName: 'understand'`). Only for `message` and inbound events with text; skipped under `silenced` unless `understand: true`, and when nothing is AI-conditioned. Candidates: the floor holder's flow (always, whatever its trigger), `message` flows passing `if` + `repeat`, `mention` flows with non-empty `mention` passing `if` + `repeat`. Envelope, every property required and nullable: `{ flows: { id: 0-100 }, mentions: { id: boolean }, extract: { id: {...} }, branches: { 'runId/stepId/i': boolean }, fields: { every pending field with extract 'anywhere' } }`. Shortcuts: exactly one eligible `message` flow
|
|
270
|
+
3. **Understand** (≤1 call, `schemaName: 'understand'`). Only for `message` and inbound events with text; skipped under `silenced` unless `understand: true`, and when nothing is AI-conditioned. Candidates: the floor holder's flow (always, whatever its trigger), `message` flows passing `if` + `repeat`, `mention` flows with non-empty `mention` passing `if` + `repeat`. Envelope, every property required and nullable: `{ flows: { id: 0-100 }, mentions: { id: boolean }, extract: { id: {...} }, branches: { 'runId/stepId/i': boolean }, fields: { every pending field with extract 'anywhere' } }`. Shortcuts: exactly one eligible `message` flow, no floor, no catch-all passing and `idle: 'silent'` → it starts, no scoring (with a catch-all or the idle speaker, a low score has somewhere to go, so it is scored); zero candidates and zero pending fields → zero calls (S9).
|
|
269
271
|
4. **Decide** (code). Runs starting this turn apply `clearOnStart` first. Extracted values are checked: unknown keys dropped (`ignorado: campo desconhecido`), strings coerced to number/boolean, enum membership enforced, otherwise `campo descartado: valor fora da lista`; then written to `session.data` (one `known`: not `undefined | null | ''`). Mention runs start in flow order (a trigger `if` with `extract` sees `input` now); `mention: []` code-only detectors start here without the call. The first true branch of the asking step takes its `then`. Routing: if a run took or resumed the floor in Ingest, routing is skipped (scores recorded, not applied). Otherwise the floor (any run `running`/`asking`, not `waiting`) stays unless another flow scores ≥ current + 15 and ≥ 40; no floor → best ≥ 40 starts (a `suspended` run of that flow resumes instead), then a `message: []` catch-all, else the `idle` speaker.
|
|
270
272
|
5. **Run** (code). If no run is `asking`, the most recently `suspended` returns to `asking`. The Runner advances every run that can move, oldest first, through `advance()`: `do` runs the handler with `key = ${runId}:${stepId}:${visit}` (`run.visits[stepId]` increments on entry; at-least-once, before the save); `failed` writes a `failed` outcome and continues to `then` unless `onFail`; `skipped` continues; `defer` re-parks under a fresh wake key; `spoke: true` makes it the turn's speaker. `say` appends a message (`once` → claim `${flowId}:${stepId}:${sessionId}`). `wait ≤ 10s` carries as `afterMs` to the next message emitted this turn, or schedules a real wake if none follows; longer waits park with `waiting` and a `schedule` entry, snapped by `businessHours`. `if` picks `then`/`else`. A talk step whose fields are all known is skipped (0 calls); otherwise it is queued for phase 6 and the current asker becomes `suspended` (a run started this turn by routing never suspends one that took the floor in Ingest). Flow missing from the agent → `pulado: fluxo desativado ou removido`; step missing → `pulado: passo removido`. Caps: 50 steps per run per turn → `falhou: laço de passos`; hop 5.
|
|
271
273
|
6. **Speak** (≤1 call + tool rounds, `schemaName: 'speak'`). One talk step speaks. Talk step = `prompt`, `collect`, `say`. Rules: a `say` or `spoke: true` emitted this turn by a run **other than** the floor holder, in reply to a user message, silences the floor's talk (`pulado: outra resposta já saiu`; the run stays `asking`) — a run's own `say` never silences its own talk. Under `silenced` no message-producing step runs and no run advances past one: an `asking` run stays `asking`; a run whose talk step was reached by a wake, event or start ends `silenciado: <reason>` (an earlier `if: { silenced: true }` routes around it); `do` steps still run; zero calls. Prompt = identity + flow + step guideline + this step's pending fields with their `ask` (all of them; the step prompt says how many to ask) + known fields as facts + tools + instructions + history. A wake states *não há mensagem nova do cliente; você fala primeiro*. Envelope `{ message, ...pending fields of this step }`, all required and nullable, shallow-merged last-wins across tool rounds. Provider failure: in phase 3 the turn throws `ProviderError` (nothing ran, nothing saved; the host retries the input); in phase 6 the turn returns with the talk step re-parked under `${runId}:${stepId}:${visit}:retry:${atMs}` (+1m, +5m, +15m), outcome `deferred: IA indisponível`, session saved, phase 5 effects not repeated.
|
|
@@ -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');
|
|
@@ -420,7 +422,7 @@ const criarFluxo = tool({
|
|
|
420
422
|
|
|
421
423
|
**S8 Scheduling.** `on: [{ message: ['quer marcar', 'quer remarcar'], repeat: 'always' }]`, `clearOnStart: ['data_preferida', 'horario_preferido', 'agenda_event_id', 'confirmado']` (applied before "sexta às 15h" lands), collect with agenda tools, `{ collect: ['confirmado'], ask: { confirmado: 'Confirme a data e o horário em uma frase.' } }`, `do book`, last step `then: { flow: 'pos-agendamento' }`.
|
|
422
424
|
|
|
423
|
-
**S9 Config assistant.** Zero flows, zero fields → understand skipped → one speak call with tools. Interview mode: one `message` flow → single-flow shortcut, no scoring; collect steps skip when known.
|
|
425
|
+
**S9 Config assistant.** Zero flows, zero fields → understand skipped → one speak call with tools. Interview mode: one `message` flow and `idle: 'silent'` → single-flow shortcut, no scoring; collect steps skip when known.
|
|
424
426
|
|
|
425
427
|
**S10.** §8.
|
|
426
428
|
|
|
@@ -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
|
@@ -19,17 +19,19 @@ This page puts the package in a project and runs the first example.
|
|
|
19
19
|
## Add it to your project
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
bun add @falai/agent
|
|
22
|
+
bun add @falai/agent@alpha
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
With npm or pnpm:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
npm install @falai/agent
|
|
28
|
+
npm install @falai/agent@alpha
|
|
29
29
|
# or
|
|
30
|
-
pnpm add @falai/agent
|
|
30
|
+
pnpm add @falai/agent@alpha
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
4.0 is in alpha. Plain `@falai/agent`, with no tag, still installs 3.x, and this tutorial will not compile against it.
|
|
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
|
|
@@ -174,7 +174,7 @@ const agent = f.agent({
|
|
|
174
174
|
|
|
175
175
|
`parameters` use the same `type`, `enum` and `description` as fields (plus `optional: true`), and `run` receives them already typed: `mensagem` is a `string` above, nothing to check.
|
|
176
176
|
|
|
177
|
-
`with` fills the parameters. `{{data.x}}`, `{{context.x}}` and `{{input.x}}` are replaced before the action runs. A placeholder whose value is unknown stays as written, so a skipped `tamanho` reaches the seller as `{{data.tamanho}}`. Word the message without it, or fork first with an `if` step on `{ known: ["tamanho"] }`. A `do` step never talks to the model: zero calls.
|
|
177
|
+
`with` fills the parameters. `{{data.x}}`, `{{context.x}}` and `{{input.x}}` are replaced before the action runs. A placeholder whose value is unknown stays as written, so a skipped `tamanho` reaches the seller as `{{data.tamanho}}`. One the host filled with an empty string drops out instead. Word the message without it, or fork first with an `if` step on `{ known: ["tamanho"] }`. A `do` step never talks to the model: zero calls.
|
|
178
178
|
|
|
179
179
|
The name and the parameters are checked when the agent is built. An unknown action, or a `with` missing a required parameter, throws `FlowConfigurationError` at startup with the step id and the fix.
|
|
180
180
|
|
|
@@ -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-alpha.21",
|
|
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",
|
|
@@ -59,7 +60,8 @@
|
|
|
59
60
|
"release:alpha": "bun publish --tag alpha",
|
|
60
61
|
"release": "bun publish",
|
|
61
62
|
"test": "bun test tests/*.test.ts tests/scenarios/*.test.ts",
|
|
62
|
-
"eval:live": "bun run scripts/eval/live.ts"
|
|
63
|
+
"eval:live": "bun run scripts/eval/live.ts",
|
|
64
|
+
"eval:exclusions": "bun run scripts/eval/exclusions.ts"
|
|
63
65
|
},
|
|
64
66
|
"keywords": [
|
|
65
67
|
"ai",
|
|
@@ -95,7 +97,7 @@
|
|
|
95
97
|
"typescript-eslint": "^8.18.2"
|
|
96
98
|
},
|
|
97
99
|
"dependencies": {
|
|
98
|
-
"@providerkit/core": "^0.
|
|
100
|
+
"@providerkit/core": "^0.13.2",
|
|
99
101
|
"loglevel": "^1.9.2"
|
|
100
102
|
},
|
|
101
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)) {
|
|
@@ -19,13 +19,20 @@ export class CompactionEngine {
|
|
|
19
19
|
* Validate CompactionOptions. Throws on invalid values.
|
|
20
20
|
*/
|
|
21
21
|
static validateOptions(options: CompactionOptions): void {
|
|
22
|
+
if (typeof options.maxTokens !== "number" || !(options.maxTokens > 0)) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`[CompactionEngine] maxTokens is ${String(options.maxTokens)}: it must be above 0. ` +
|
|
25
|
+
`Set it to the most history, in tokens, each call should carry, e.g. 100000.`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
22
28
|
if (
|
|
23
29
|
typeof options.compactionThreshold !== "number" ||
|
|
24
30
|
options.compactionThreshold < 0.5 ||
|
|
25
31
|
options.compactionThreshold > 0.95
|
|
26
32
|
) {
|
|
27
33
|
throw new Error(
|
|
28
|
-
`compactionThreshold must be between 0.5 and 0.95
|
|
34
|
+
`[CompactionEngine] compactionThreshold is ${String(options.compactionThreshold)}: it must be between 0.5 and 0.95. ` +
|
|
35
|
+
`Use 0.8 unless you measured otherwise.`
|
|
29
36
|
);
|
|
30
37
|
}
|
|
31
38
|
if (
|
|
@@ -33,7 +40,7 @@ export class CompactionEngine {
|
|
|
33
40
|
options.preserveRecentCount < 2
|
|
34
41
|
) {
|
|
35
42
|
throw new Error(
|
|
36
|
-
`preserveRecentCount must be
|
|
43
|
+
`[CompactionEngine] preserveRecentCount is ${String(options.preserveRecentCount)}: it must be 2 or more. Use 4, the default.`
|
|
37
44
|
);
|
|
38
45
|
}
|
|
39
46
|
if (
|
|
@@ -41,7 +48,7 @@ export class CompactionEngine {
|
|
|
41
48
|
options.maxToolResultChars <= 0
|
|
42
49
|
) {
|
|
43
50
|
throw new Error(
|
|
44
|
-
`maxToolResultChars must be
|
|
51
|
+
`[CompactionEngine] maxToolResultChars is ${String(options.maxToolResultChars)}: it must be above 0. Use 5000, the default.`
|
|
45
52
|
);
|
|
46
53
|
}
|
|
47
54
|
}
|