@falai/agent 4.0.0-alpha.8 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/cjs/core/Agent.d.ts +8 -1
- package/dist/cjs/core/Agent.d.ts.map +1 -1
- package/dist/cjs/core/Agent.js +18 -0
- package/dist/cjs/core/Agent.js.map +1 -1
- package/dist/cjs/core/CompactionEngine.d.ts.map +1 -1
- package/dist/cjs/core/CompactionEngine.js +21 -7
- package/dist/cjs/core/CompactionEngine.js.map +1 -1
- package/dist/cjs/core/FlowSpec.d.ts +21 -2
- package/dist/cjs/core/FlowSpec.d.ts.map +1 -1
- package/dist/cjs/core/FlowSpec.js +297 -59
- package/dist/cjs/core/FlowSpec.js.map +1 -1
- package/dist/cjs/core/Migrate.d.ts.map +1 -1
- package/dist/cjs/core/Migrate.js +3 -1
- package/dist/cjs/core/Migrate.js.map +1 -1
- package/dist/cjs/core/Prompt.d.ts +16 -0
- package/dist/cjs/core/Prompt.d.ts.map +1 -1
- package/dist/cjs/core/Prompt.js +39 -0
- package/dist/cjs/core/Prompt.js.map +1 -1
- package/dist/cjs/core/Runner.d.ts +37 -4
- package/dist/cjs/core/Runner.d.ts.map +1 -1
- package/dist/cjs/core/Runner.js +281 -72
- package/dist/cjs/core/Runner.js.map +1 -1
- package/dist/cjs/core/Speak.d.ts.map +1 -1
- package/dist/cjs/core/Speak.js +63 -19
- package/dist/cjs/core/Speak.js.map +1 -1
- package/dist/cjs/core/Understand.d.ts +6 -3
- package/dist/cjs/core/Understand.d.ts.map +1 -1
- package/dist/cjs/core/Understand.js +18 -42
- package/dist/cjs/core/Understand.js.map +1 -1
- package/dist/cjs/core/contracts.d.ts +26 -6
- package/dist/cjs/core/contracts.d.ts.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/persistence/OpenSearchStore.d.ts +2 -1
- package/dist/cjs/persistence/OpenSearchStore.d.ts.map +1 -1
- package/dist/cjs/persistence/OpenSearchStore.js +2 -2
- package/dist/cjs/persistence/OpenSearchStore.js.map +1 -1
- package/dist/cjs/persistence/RedisStore.d.ts +1 -1
- package/dist/cjs/persistence/RedisStore.d.ts.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.d.ts +2 -5
- package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.js +3 -4
- package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
- package/dist/cjs/providers/DeepSeekProvider.d.ts.map +1 -1
- package/dist/cjs/providers/DeepSeekProvider.js +3 -4
- package/dist/cjs/providers/DeepSeekProvider.js.map +1 -1
- package/dist/cjs/providers/FallbackAiProvider.js +1 -1
- package/dist/cjs/providers/FallbackAiProvider.js.map +1 -1
- package/dist/cjs/providers/GeminiProvider.d.ts +1 -2
- package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/cjs/providers/GeminiProvider.js +3 -4
- package/dist/cjs/providers/GeminiProvider.js.map +1 -1
- package/dist/cjs/providers/GenericOpenAICompatibleProvider.js +4 -4
- package/dist/cjs/providers/GenericOpenAICompatibleProvider.js.map +1 -1
- package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenAIProvider.js +3 -4
- package/dist/cjs/providers/OpenAIProvider.js.map +1 -1
- package/dist/cjs/providers/OpenRouterProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenRouterProvider.js +4 -3
- package/dist/cjs/providers/OpenRouterProvider.js.map +1 -1
- package/dist/cjs/providers/ProviderAdapter.d.ts +26 -11
- package/dist/cjs/providers/ProviderAdapter.d.ts.map +1 -1
- package/dist/cjs/providers/ProviderAdapter.js +13 -13
- package/dist/cjs/providers/ProviderAdapter.js.map +1 -1
- package/dist/cjs/providers/ZaiProvider.js +1 -1
- package/dist/cjs/providers/ZaiProvider.js.map +1 -1
- package/dist/cjs/types/agent.d.ts +14 -3
- package/dist/cjs/types/agent.d.ts.map +1 -1
- package/dist/cjs/types/ai.d.ts +10 -6
- package/dist/cjs/types/ai.d.ts.map +1 -1
- package/dist/cjs/types/compaction.d.ts +1 -0
- package/dist/cjs/types/compaction.d.ts.map +1 -1
- package/dist/cjs/types/errors.d.ts +4 -9
- package/dist/cjs/types/errors.d.ts.map +1 -1
- package/dist/cjs/types/errors.js +12 -12
- package/dist/cjs/types/errors.js.map +1 -1
- package/dist/cjs/types/flow.d.ts +17 -1
- package/dist/cjs/types/flow.d.ts.map +1 -1
- package/dist/cjs/types/history.d.ts +0 -7
- package/dist/cjs/types/history.d.ts.map +1 -1
- package/dist/cjs/types/index.d.ts +2 -2
- package/dist/cjs/types/index.d.ts.map +1 -1
- package/dist/cjs/types/session.d.ts +4 -2
- package/dist/cjs/types/session.d.ts.map +1 -1
- package/dist/cjs/utils/clock.js +1 -1
- package/dist/cjs/utils/clock.js.map +1 -1
- package/dist/cjs/utils/outcomes.d.ts +2 -0
- package/dist/cjs/utils/outcomes.d.ts.map +1 -1
- package/dist/cjs/utils/outcomes.js +2 -0
- package/dist/cjs/utils/outcomes.js.map +1 -1
- package/dist/cjs/utils/schema.d.ts +3 -12
- package/dist/cjs/utils/schema.d.ts.map +1 -1
- package/dist/cjs/utils/schema.js +3 -43
- package/dist/cjs/utils/schema.js.map +1 -1
- package/dist/cjs/utils/template.d.ts.map +1 -1
- package/dist/cjs/utils/template.js +9 -2
- package/dist/cjs/utils/template.js.map +1 -1
- package/dist/core/Agent.d.ts +8 -1
- package/dist/core/Agent.d.ts.map +1 -1
- package/dist/core/Agent.js +19 -1
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/CompactionEngine.d.ts.map +1 -1
- package/dist/core/CompactionEngine.js +21 -7
- package/dist/core/CompactionEngine.js.map +1 -1
- package/dist/core/FlowSpec.d.ts +21 -2
- package/dist/core/FlowSpec.d.ts.map +1 -1
- package/dist/core/FlowSpec.js +296 -60
- package/dist/core/FlowSpec.js.map +1 -1
- package/dist/core/Migrate.d.ts.map +1 -1
- package/dist/core/Migrate.js +3 -1
- package/dist/core/Migrate.js.map +1 -1
- package/dist/core/Prompt.d.ts +16 -0
- package/dist/core/Prompt.d.ts.map +1 -1
- package/dist/core/Prompt.js +37 -0
- package/dist/core/Prompt.js.map +1 -1
- package/dist/core/Runner.d.ts +37 -4
- package/dist/core/Runner.d.ts.map +1 -1
- package/dist/core/Runner.js +282 -73
- package/dist/core/Runner.js.map +1 -1
- package/dist/core/Speak.d.ts.map +1 -1
- package/dist/core/Speak.js +64 -20
- package/dist/core/Speak.js.map +1 -1
- package/dist/core/Understand.d.ts +6 -3
- package/dist/core/Understand.d.ts.map +1 -1
- package/dist/core/Understand.js +18 -42
- package/dist/core/Understand.js.map +1 -1
- package/dist/core/contracts.d.ts +26 -6
- package/dist/core/contracts.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/persistence/OpenSearchStore.d.ts +2 -1
- package/dist/persistence/OpenSearchStore.d.ts.map +1 -1
- package/dist/persistence/OpenSearchStore.js +2 -2
- package/dist/persistence/OpenSearchStore.js.map +1 -1
- package/dist/persistence/RedisStore.d.ts +1 -1
- package/dist/persistence/RedisStore.d.ts.map +1 -1
- package/dist/providers/AnthropicProvider.d.ts +2 -5
- package/dist/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/providers/AnthropicProvider.js +3 -4
- package/dist/providers/AnthropicProvider.js.map +1 -1
- package/dist/providers/DeepSeekProvider.d.ts.map +1 -1
- package/dist/providers/DeepSeekProvider.js +3 -4
- package/dist/providers/DeepSeekProvider.js.map +1 -1
- package/dist/providers/FallbackAiProvider.js +1 -1
- package/dist/providers/FallbackAiProvider.js.map +1 -1
- package/dist/providers/GeminiProvider.d.ts +1 -2
- package/dist/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/providers/GeminiProvider.js +3 -4
- package/dist/providers/GeminiProvider.js.map +1 -1
- package/dist/providers/GenericOpenAICompatibleProvider.js +4 -4
- package/dist/providers/GenericOpenAICompatibleProvider.js.map +1 -1
- package/dist/providers/OpenAIProvider.d.ts.map +1 -1
- package/dist/providers/OpenAIProvider.js +3 -4
- package/dist/providers/OpenAIProvider.js.map +1 -1
- package/dist/providers/OpenRouterProvider.d.ts.map +1 -1
- package/dist/providers/OpenRouterProvider.js +4 -3
- package/dist/providers/OpenRouterProvider.js.map +1 -1
- package/dist/providers/ProviderAdapter.d.ts +26 -11
- package/dist/providers/ProviderAdapter.d.ts.map +1 -1
- package/dist/providers/ProviderAdapter.js +14 -14
- package/dist/providers/ProviderAdapter.js.map +1 -1
- package/dist/providers/ZaiProvider.js +1 -1
- package/dist/providers/ZaiProvider.js.map +1 -1
- package/dist/types/agent.d.ts +14 -3
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/ai.d.ts +10 -6
- package/dist/types/ai.d.ts.map +1 -1
- package/dist/types/compaction.d.ts +1 -0
- package/dist/types/compaction.d.ts.map +1 -1
- package/dist/types/errors.d.ts +4 -9
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/errors.js +12 -12
- package/dist/types/errors.js.map +1 -1
- package/dist/types/flow.d.ts +17 -1
- package/dist/types/flow.d.ts.map +1 -1
- package/dist/types/history.d.ts +0 -7
- package/dist/types/history.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/session.d.ts +4 -2
- package/dist/types/session.d.ts.map +1 -1
- package/dist/utils/clock.js +1 -1
- package/dist/utils/clock.js.map +1 -1
- package/dist/utils/outcomes.d.ts +2 -0
- package/dist/utils/outcomes.d.ts.map +1 -1
- package/dist/utils/outcomes.js +2 -0
- package/dist/utils/outcomes.js.map +1 -1
- package/dist/utils/schema.d.ts +3 -12
- package/dist/utils/schema.d.ts.map +1 -1
- package/dist/utils/schema.js +3 -42
- package/dist/utils/schema.js.map +1 -1
- package/dist/utils/template.d.ts.map +1 -1
- package/dist/utils/template.js +9 -2
- package/dist/utils/template.js.map +1 -1
- package/docs/concepts/architecture.md +2 -2
- package/docs/concepts/collection.md +40 -5
- package/docs/concepts/pipeline.md +10 -7
- package/docs/concepts/runs-and-waits.md +2 -2
- package/docs/guides/actions-and-events.md +1 -1
- package/docs/guides/branching.md +4 -2
- package/docs/guides/compaction.md +2 -2
- package/docs/guides/error-handling.md +2 -2
- package/docs/guides/flow-control.md +4 -2
- package/docs/guides/persistence.md +2 -2
- package/docs/guides/testing.md +1 -1
- package/docs/guides/triggers.md +3 -3
- package/docs/migration/v3-to-v4.md +8 -7
- package/docs/reference/actions-events-conditions.md +1 -1
- package/docs/reference/agent.md +9 -5
- package/docs/reference/branches.md +1 -1
- package/docs/reference/errors.md +14 -11
- package/docs/reference/fields.md +5 -3
- package/docs/reference/flow-spec.md +35 -8
- package/docs/reference/flow.md +8 -3
- package/docs/reference/outcomes.md +4 -2
- package/docs/reference/providers.md +5 -3
- package/docs/reference/session.md +2 -0
- package/docs/reference/step.md +9 -5
- package/docs/reference/stores.md +5 -3
- package/docs/reference/trigger.md +2 -2
- package/docs/rfc/v4-one-flow.md +5 -3
- package/docs/start/01-install.md +2 -0
- package/docs/start/05-go-to-production.md +18 -1
- package/examples/05-branches.ts +1 -1
- package/examples/06-triggers-and-waits.ts +4 -3
- package/package.json +4 -3
- package/src/core/Agent.ts +23 -2
- package/src/core/CompactionEngine.ts +25 -9
- package/src/core/FlowSpec.ts +311 -72
- package/src/core/Migrate.ts +2 -1
- package/src/core/Prompt.ts +40 -0
- package/src/core/Runner.ts +279 -69
- package/src/core/Speak.ts +67 -19
- package/src/core/Understand.ts +24 -44
- package/src/core/contracts.ts +26 -4
- package/src/index.ts +1 -1
- package/src/persistence/OpenSearchStore.ts +3 -2
- package/src/persistence/RedisStore.ts +1 -1
- package/src/providers/AnthropicProvider.ts +4 -9
- package/src/providers/DeepSeekProvider.ts +2 -4
- package/src/providers/FallbackAiProvider.ts +1 -1
- package/src/providers/GeminiProvider.ts +3 -6
- package/src/providers/GenericOpenAICompatibleProvider.ts +4 -4
- package/src/providers/OpenAIProvider.ts +3 -4
- package/src/providers/OpenRouterProvider.ts +4 -2
- package/src/providers/ProviderAdapter.ts +35 -33
- package/src/providers/ZaiProvider.ts +1 -1
- package/src/types/agent.ts +15 -4
- package/src/types/ai.ts +10 -6
- package/src/types/compaction.ts +1 -0
- package/src/types/errors.ts +11 -12
- package/src/types/flow.ts +17 -1
- package/src/types/history.ts +0 -10
- package/src/types/index.ts +1 -1
- package/src/types/session.ts +5 -1
- package/src/utils/clock.ts +1 -1
- package/src/utils/outcomes.ts +2 -0
- package/src/utils/schema.ts +3 -48
- package/src/utils/template.ts +9 -2
- package/dist/cjs/providers/index.d.ts +0 -26
- package/dist/cjs/providers/index.d.ts.map +0 -1
- package/dist/cjs/providers/index.js +0 -30
- package/dist/cjs/providers/index.js.map +0 -1
- package/dist/cjs/utils/clone.d.ts +0 -8
- package/dist/cjs/utils/clone.d.ts.map +0 -1
- package/dist/cjs/utils/clone.js +0 -32
- package/dist/cjs/utils/clone.js.map +0 -1
- package/dist/cjs/utils/index.d.ts +0 -9
- package/dist/cjs/utils/index.d.ts.map +0 -1
- package/dist/cjs/utils/index.js +0 -29
- package/dist/cjs/utils/index.js.map +0 -1
- package/dist/providers/index.d.ts +0 -26
- package/dist/providers/index.d.ts.map +0 -1
- package/dist/providers/index.js +0 -17
- package/dist/providers/index.js.map +0 -1
- package/dist/utils/clone.d.ts +0 -8
- package/dist/utils/clone.d.ts.map +0 -1
- package/dist/utils/clone.js +0 -29
- package/dist/utils/clone.js.map +0 -1
- package/dist/utils/index.d.ts +0 -9
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -9
- package/dist/utils/index.js.map +0 -1
- package/src/providers/index.ts +0 -38
- package/src/utils/clone.ts +0 -34
- package/src/utils/index.ts +0 -18
package/src/core/Runner.ts
CHANGED
|
@@ -8,13 +8,12 @@
|
|
|
8
8
|
* begin ─▶ understandRequest ─(Understand)─▶ decide ─▶ advance ─(Speak)─▶ settle ─▶ finish
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type { AgentOptions, EndReason, TurnInput, TurnResult } from "../types/agent.js";
|
|
11
|
+
import type { AgentOptions, EndReason, PendingWakesInput, ScheduleEntry, TurnBase, TurnInput, TurnResult } from "../types/agent.js";
|
|
12
12
|
import type { TokenUsage } from "../types/ai.js";
|
|
13
13
|
import type { ActionResult, Branch, DoStep, Duration, Flow, IfStep, Next, Pred, PredCtx, Repeat, SayStep, Step, StepBase, TalkStep, Trigger, WaitEventStep, WaitStep } from "../types/flow.js";
|
|
14
14
|
import type { History } from "../types/history.js";
|
|
15
15
|
import type { Run, Session, StepOutcome, StepOutcomeCode, StepOutcomeKind, TriggerKind } from "../types/session.js";
|
|
16
|
-
import {
|
|
17
|
-
import { parseDuration } from "../utils/duration.js";
|
|
16
|
+
import { isDuration, parseDuration } from "../utils/duration.js";
|
|
18
17
|
import { OUTCOME_MESSAGES } from "../utils/outcomes.js";
|
|
19
18
|
import { coerceField, DEFAULT_MAX_ASKS, extractMode, isKnown, pendingFields } from "../utils/schema.js";
|
|
20
19
|
import { render, renderDeep } from "../utils/template.js";
|
|
@@ -49,7 +48,7 @@ export type What =
|
|
|
49
48
|
|
|
50
49
|
/** The mutable per-turn state. `session` is a deep copy; the input is never touched. */
|
|
51
50
|
export interface Turn<C = unknown, D = unknown> {
|
|
52
|
-
readonly input:
|
|
51
|
+
readonly input: TurnBase<C, D>;
|
|
53
52
|
readonly what: What;
|
|
54
53
|
readonly kind: InputKind;
|
|
55
54
|
/** Message id, wake key, event key or start key: the trigger key of runs started this turn. */
|
|
@@ -62,6 +61,8 @@ export interface Turn<C = unknown, D = unknown> {
|
|
|
62
61
|
readonly silenced?: string;
|
|
63
62
|
/** The host gate allows the understand call even though the assistant cannot speak. */
|
|
64
63
|
readonly understandAllowed: boolean;
|
|
64
|
+
/** The host gate only stops messages: a talk or `say` step is skipped and the run goes on. */
|
|
65
|
+
readonly skipSilenced: boolean;
|
|
65
66
|
/** Set by a short-circuit: the input is a no-op and nothing is saved. */
|
|
66
67
|
ignored: boolean;
|
|
67
68
|
messages: TurnResult<D>["messages"];
|
|
@@ -86,6 +87,8 @@ export interface Turn<C = unknown, D = unknown> {
|
|
|
86
87
|
/** A short `wait` waiting to ride on the next message. */
|
|
87
88
|
afterMs: number;
|
|
88
89
|
talk?: TalkRequest<C, D>;
|
|
90
|
+
/** Understand's judgment that the lead's message asks something; absent when nothing judged it. */
|
|
91
|
+
asks?: boolean;
|
|
89
92
|
/** Speak already happened (or was skipped): a talk step reached now waits for the next message. */
|
|
90
93
|
speakDone: boolean;
|
|
91
94
|
/** Runs still to move this phase; `follow` appends children. */
|
|
@@ -137,6 +140,13 @@ function talkKind(step: { collect?: readonly string[] }): StepOutcomeKind {
|
|
|
137
140
|
return step.collect?.length ? "collect" : "prompt";
|
|
138
141
|
}
|
|
139
142
|
|
|
143
|
+
/** Everything a flow collects: its own `collect`, then each talk step's, once each. */
|
|
144
|
+
function flowFields<C, D>(flow: Flow<C, D>): string[] {
|
|
145
|
+
const fields = new Set<string>(flow.collect);
|
|
146
|
+
for (const step of flow.steps) if (isTalk(step)) for (const field of step.collect ?? []) fields.add(field);
|
|
147
|
+
return [...fields];
|
|
148
|
+
}
|
|
149
|
+
|
|
140
150
|
function kindOf<C, D>(step: Step<C, D> | undefined): StepOutcomeKind {
|
|
141
151
|
if (!step) return "if";
|
|
142
152
|
if (isTalk(step)) return talkKind(step);
|
|
@@ -171,16 +181,31 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
171
181
|
// ── Load + Ingest ─────────────────────────────────────────────────────
|
|
172
182
|
|
|
173
183
|
begin(input: TurnInput<C, D>): Turn<C, D> {
|
|
174
|
-
const now =
|
|
184
|
+
const now = this.now();
|
|
185
|
+
const turn = this.open(input, describe(input, now.toISOString()), now);
|
|
186
|
+
if (turn.what.kind === "wake" && !input.session) {
|
|
187
|
+
this.ignore(turn, "no-session", turn.what.key);
|
|
188
|
+
return turn;
|
|
189
|
+
}
|
|
190
|
+
turn.ingesting = true;
|
|
191
|
+
this.ingest(turn);
|
|
192
|
+
turn.ingesting = false;
|
|
193
|
+
return turn;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private now(): Date {
|
|
197
|
+
return (this.options.clock ?? (() => new Date()))();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private open(input: TurnBase<C, D>, what: What, now: Date): Turn<C, D> {
|
|
175
201
|
const nowIso = now.toISOString();
|
|
176
|
-
const what = describe(input, nowIso);
|
|
177
202
|
const silenced = typeof input.silenced === "string" ? input.silenced : input.silenced?.reason;
|
|
178
|
-
|
|
203
|
+
return {
|
|
179
204
|
input,
|
|
180
205
|
what,
|
|
181
206
|
kind: what.kind,
|
|
182
207
|
triggerKey: what.kind === "message" ? (what.id ?? what.at) : what.key,
|
|
183
|
-
session: input.session ?
|
|
208
|
+
session: input.session ? structuredClone(input.session) : freshSession<D>(input.sessionId),
|
|
184
209
|
original: input.session,
|
|
185
210
|
now,
|
|
186
211
|
nowIso,
|
|
@@ -188,6 +213,7 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
188
213
|
context: input.context as C,
|
|
189
214
|
silenced,
|
|
190
215
|
understandAllowed: typeof input.silenced === "object" && input.silenced.understand === true,
|
|
216
|
+
skipSilenced: typeof input.silenced === "object" && input.silenced.skip === true,
|
|
191
217
|
ignored: false,
|
|
192
218
|
messages: [],
|
|
193
219
|
schedule: [],
|
|
@@ -204,14 +230,17 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
204
230
|
speakDone: false,
|
|
205
231
|
queue: [],
|
|
206
232
|
};
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
this.
|
|
213
|
-
|
|
214
|
-
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Every wake the session waits on: each parked run's own, then each silence flow's since the assistant last spoke. */
|
|
236
|
+
pendingWakes(input: PendingWakesInput<C, D>): ScheduleEntry[] {
|
|
237
|
+
// No input arrives, so nothing reads `what`: the inert wake shape only fills the field.
|
|
238
|
+
const turn = this.open({ ...input, sessionId: input.session.id }, { kind: "wake", key: "" }, this.now());
|
|
239
|
+
// `park` always sets both; the type leaves them optional, so a hand-built blob without them has no wake to give.
|
|
240
|
+
const parked = turn.session.runs.flatMap(({ status, waiting }) =>
|
|
241
|
+
status === "waiting" && waiting?.key && waiting.until ? [{ key: waiting.key, at: new Date(waiting.until) }] : [],
|
|
242
|
+
);
|
|
243
|
+
return [...parked, ...this.silenceWakes(turn)];
|
|
215
244
|
}
|
|
216
245
|
|
|
217
246
|
private ingest(turn: Turn<C, D>): void {
|
|
@@ -285,6 +314,8 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
285
314
|
const branch = step.branches?.find((b) => "if" in b && this.holds(b.if, turn, run));
|
|
286
315
|
const target = branch ? branch.then : step.else;
|
|
287
316
|
this.outcome(turn, run, { kind: "wait", status: "ok", code: "replied", next: nextLabel(target) });
|
|
317
|
+
// The reply is this run's while it moves, so a stay it reaches answers it.
|
|
318
|
+
if (!turn.floorFromIngest) turn.floorRunId = run.id;
|
|
288
319
|
this.follow(turn, run, flow, step, target);
|
|
289
320
|
this.takeFloor(turn, run);
|
|
290
321
|
}
|
|
@@ -323,8 +354,10 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
323
354
|
/** `silence:<flowId>:<sessionId>:<lastAssistantAtMs>`, honoured only while the blob still shows that silence. */
|
|
324
355
|
private silenceWake(turn: Turn<C, D>, key: string): void {
|
|
325
356
|
const rest = key.slice("silence:".length);
|
|
326
|
-
const flowId = rest.slice(0, rest.indexOf(":"));
|
|
327
357
|
const ms = Number(rest.slice(rest.lastIndexOf(":") + 1));
|
|
358
|
+
// A flow id may hold a ":" itself, so cut at the session id rather than at the first ":".
|
|
359
|
+
const cut = rest.lastIndexOf(`:${turn.session.id}:`);
|
|
360
|
+
const flowId = rest.slice(0, cut === -1 ? rest.indexOf(":") : cut);
|
|
328
361
|
const { lastAssistantAt } = turn.session;
|
|
329
362
|
const flow = this.flows.get(flowId);
|
|
330
363
|
if (!flow) {
|
|
@@ -386,11 +419,8 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
386
419
|
return null;
|
|
387
420
|
};
|
|
388
421
|
if (opts.trigger?.if && !this.holds(opts.trigger.if, turn, run)) return null;
|
|
389
|
-
const
|
|
390
|
-
if (
|
|
391
|
-
if (typeof repeat === "string") return skip("already-claimed");
|
|
392
|
-
if (turn.now.getTime() - Date.parse(heldAt) < parseDuration(repeat.cooldown)) return skip("cooldown");
|
|
393
|
-
}
|
|
422
|
+
const blocked = this.repeatBlocks(turn, dedupeKey, repeat);
|
|
423
|
+
if (blocked) return skip(blocked);
|
|
394
424
|
if (hop >= MAX_HOP) return skip("hop-limit");
|
|
395
425
|
const live = session.runs.find((r) => r.flowId === flow.id && r.anchor === anchor);
|
|
396
426
|
if (live) {
|
|
@@ -407,9 +437,11 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
407
437
|
}
|
|
408
438
|
session.runs.push(run);
|
|
409
439
|
turn.started.push({ runId: run.id, flowId: flow.id, anchor, dedupeKey });
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
440
|
+
const eventTrigger = opts.trigger && "event" in opts.trigger ? opts.trigger : undefined;
|
|
441
|
+
const after = eventTrigger?.after;
|
|
442
|
+
const at = this.snap(turn, new Date(turn.now.getTime() + (after ? parseDuration(after) : 0)), eventTrigger?.businessHours);
|
|
443
|
+
// With no `after`, only a closed hour parks the start: inside working hours the snap returns now.
|
|
444
|
+
if (after || at.getTime() > turn.now.getTime()) {
|
|
413
445
|
this.park(turn, run, { kind: "timer", key: `${run.id}:start:${at.getTime()}` }, at);
|
|
414
446
|
this.outcome(turn, run, { kind: "wait", status: "waiting", code: "awaiting-trigger", until: at.toISOString() });
|
|
415
447
|
}
|
|
@@ -433,10 +465,15 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
433
465
|
|
|
434
466
|
private repeatAllows(turn: Turn<C, D>, flow: Flow<C, D>, trigger: Trigger<C, D>, key: string): boolean {
|
|
435
467
|
const repeat = trigger.repeat ?? defaultRepeat(triggerKind(trigger));
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
468
|
+
return !this.repeatBlocks(turn, `${flow.id}:${this.anchorOf(turn, flow)}:${repeat === "always" ? key : ""}`, repeat);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** Why the claim already on `dedupeKey` stops another start, or nothing when there is none or its cooldown has run out. */
|
|
472
|
+
private repeatBlocks(turn: Turn<C, D>, dedupeKey: string, repeat: Repeat): "already-claimed" | "cooldown" | undefined {
|
|
473
|
+
const heldAt = this.claimAt(turn, dedupeKey);
|
|
474
|
+
if (heldAt === undefined) return undefined;
|
|
475
|
+
if (typeof repeat === "string") return "already-claimed";
|
|
476
|
+
return turn.now.getTime() - Date.parse(heldAt) < parseDuration(repeat.cooldown) ? "cooldown" : undefined;
|
|
440
477
|
}
|
|
441
478
|
|
|
442
479
|
/** A run as it would be if started now, for trigger-level predicates. */
|
|
@@ -521,17 +558,27 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
521
558
|
}
|
|
522
559
|
const fields: UnderstandRequest<C, D>["fields"] = {};
|
|
523
560
|
const data: Record<string, unknown> = turn.session.data;
|
|
561
|
+
const harvestable = (field: string): boolean => {
|
|
562
|
+
const def = this.options.fields[field];
|
|
563
|
+
return def !== undefined && !isKnown(data[field]) && extractMode(def) === "anywhere";
|
|
564
|
+
};
|
|
524
565
|
for (const flow of [...(floorFlow ? [floorFlow] : []), ...eligible]) {
|
|
525
|
-
for (const
|
|
526
|
-
if (!isTalk(step)) continue;
|
|
527
|
-
for (const field of step.collect ?? []) {
|
|
528
|
-
const def = this.options.fields[field];
|
|
529
|
-
if (def && !isKnown(data[field]) && extractMode(def) === "anywhere") fields[field] = def;
|
|
530
|
-
}
|
|
531
|
-
}
|
|
566
|
+
for (const field of flowFields(flow).filter(harvestable)) fields[field] = this.options.fields[field];
|
|
532
567
|
}
|
|
533
|
-
|
|
568
|
+
const judging = messageFlows.length > 0 || mentionFlows.length > 0 || branches.length > 0 || Object.keys(fields).length > 0;
|
|
569
|
+
// With nobody on the floor the catch-all may take this message, and an opening message often says the most. The
|
|
570
|
+
// fields its first step asks ride on that step's speak call, so alone they spend no call; a fixed question has no speak call.
|
|
571
|
+
// ponytail: "first step" is steps[0]; a catch-all that opens with a say or an if pays the call for that step's fields too.
|
|
572
|
+
const fallback = floorRun ? undefined : this.messageFlows(turn, (list) => list.length === 0)[0];
|
|
573
|
+
if (fallback) {
|
|
574
|
+
const first = fallback.steps[0];
|
|
575
|
+
const free = new Set<string>(first && isTalk(first) && first.question === undefined ? first.collect : []);
|
|
576
|
+
const own = flowFields(fallback).filter(harvestable);
|
|
577
|
+
if (judging || own.some((field) => !free.has(field))) for (const field of own) fields[field] = this.options.fields[field];
|
|
578
|
+
}
|
|
579
|
+
if (!judging && !Object.keys(fields).length) return null;
|
|
534
580
|
return {
|
|
581
|
+
sessionId: turn.session.id,
|
|
535
582
|
text: turn.what.text,
|
|
536
583
|
history: this.historyOf(turn),
|
|
537
584
|
context: turn.context,
|
|
@@ -597,6 +644,7 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
597
644
|
if (turn.ignored || turn.what.kind !== "message") return;
|
|
598
645
|
turn.llmCalls += understanding?.llmCalls ?? 0;
|
|
599
646
|
turn.usage = addUsage(turn.usage, understanding?.usage);
|
|
647
|
+
if (understanding?.asks !== undefined) turn.asks = understanding.asks;
|
|
600
648
|
const key = turn.triggerKey;
|
|
601
649
|
const asker = this.asker(turn);
|
|
602
650
|
|
|
@@ -639,6 +687,9 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
639
687
|
}
|
|
640
688
|
}
|
|
641
689
|
|
|
690
|
+
// Unless routing or Ingest moved it, the message is the asker's: written down, so a chain beside it cannot take it.
|
|
691
|
+
if (asker && !route && !turn.floorFromIngest) turn.floorRunId = asker.id;
|
|
692
|
+
|
|
642
693
|
for (const [field, raw] of Object.entries(understanding?.fields ?? {})) this.writeField(turn, field, raw);
|
|
643
694
|
|
|
644
695
|
if (asker && turn.session.runs.includes(asker) && asker.status === "asking") {
|
|
@@ -649,8 +700,15 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
649
700
|
}
|
|
650
701
|
|
|
651
702
|
private fireBranch(turn: Turn<C, D>, run: Run, flow: Flow<C, D>, step: TalkOf<C, D>, understanding: Understanding | null): void {
|
|
703
|
+
// Staying, a run does not take an `if` branch back onto a path it has already run: the fact it tests is still true,
|
|
704
|
+
// so it would re-run that path on every message. A `when` branch is judged on each new message and always counts.
|
|
705
|
+
// ponytail: "already run" is the target's visit count, so an `if` restart (`{ step, clear }` to a visited step) never fires while staying; write it as a `when`.
|
|
706
|
+
const ran = (then: Next<D>): boolean =>
|
|
707
|
+
typeof then === "string" ? then === "end" || (run.visits[then] ?? 0) > 0 : "step" in then && (run.visits[then.step] ?? 0) > 0;
|
|
652
708
|
const hit = (branch: Branch<C, D>, index: number): boolean =>
|
|
653
|
-
"if" in branch
|
|
709
|
+
"if" in branch
|
|
710
|
+
? !(run.staying && ran(branch.then)) && this.holds(branch.if, turn, run)
|
|
711
|
+
: understanding?.branches[`${run.id}/${step.id}/${index}`] === true;
|
|
654
712
|
const index = (step.branches ?? []).findIndex(hit);
|
|
655
713
|
if (index < 0) return;
|
|
656
714
|
const branch = (step.branches ?? [])[index];
|
|
@@ -680,10 +738,61 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
680
738
|
|
|
681
739
|
async advance(turn: Turn<C, D>): Promise<TalkRequest<C, D> | IdleRequest<C, D> | null> {
|
|
682
740
|
if (turn.ignored) return null;
|
|
683
|
-
this.
|
|
741
|
+
this.resume(turn);
|
|
684
742
|
turn.queue = [...turn.session.runs];
|
|
685
743
|
await this.drain(turn);
|
|
686
|
-
|
|
744
|
+
// An asker that moved on without a word hands the message to the run it suspended, and that one to the next.
|
|
745
|
+
// Each pass takes a run off the suspended stack, so the stack's size bounds the loop.
|
|
746
|
+
for (let left = turn.session.runs.length; left > 0; left--) {
|
|
747
|
+
if (turn.what.kind !== "message" || turn.silenced !== undefined || turn.talk || turn.spokeBy.size > 0) break;
|
|
748
|
+
const resumed = this.resume(turn);
|
|
749
|
+
if (!resumed) break;
|
|
750
|
+
turn.queue.push(resumed);
|
|
751
|
+
await this.drain(turn);
|
|
752
|
+
}
|
|
753
|
+
const speaker = this.speaker(turn);
|
|
754
|
+
if (!speaker || "idle" in speaker || !this.fixedDue(speaker)) return speaker;
|
|
755
|
+
// A question the lead asked gets its answer: the step speaks once, answering it and asking the fixed question.
|
|
756
|
+
// Appending the text word for word after an answer could repeat or contradict it. Unjudged counts as asked,
|
|
757
|
+
// and so does the retry of a failed answer: a question sent over the lead's own drops it.
|
|
758
|
+
const answerFirst = turn.what.kind === "message" ? turn.asks !== false : this.retrying(speaker);
|
|
759
|
+
if (answerFirst && speaker.step.question !== undefined) {
|
|
760
|
+
speaker.fixedQuestion = render(speaker.step.question, this.scope(turn, speaker.run));
|
|
761
|
+
return speaker;
|
|
762
|
+
}
|
|
763
|
+
this.askFixed(turn, speaker);
|
|
764
|
+
turn.talk = undefined;
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/** The step failed at the provider and runs again on its retry wake: deferTalk left `deferred` as its last line. */
|
|
769
|
+
private retrying({ run, step }: TalkRequest<C, D>): boolean {
|
|
770
|
+
const last = run.outcomes.at(-1);
|
|
771
|
+
return last?.status === "deferred" && last.stepId === step.id;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* The step's fixed question is due: this is its first ask, with every field it collects still unknown and none asked
|
|
776
|
+
* yet, and the run is not staying (a staying run answers the lead). Otherwise the AI phrases the ask.
|
|
777
|
+
*/
|
|
778
|
+
private fixedDue({ run, step, pending }: TalkRequest<C, D>): boolean {
|
|
779
|
+
const { question, collect = [] } = step;
|
|
780
|
+
if (question === undefined || run.staying) return false;
|
|
781
|
+
return pending.length === collect.length && pending.every((field) => (run.asked[field] ?? 0) === 0);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/** Send the step's fixed question when it is due. It counts as one ask of each field. */
|
|
785
|
+
private askFixed(turn: Turn<C, D>, talk: TalkRequest<C, D>): boolean {
|
|
786
|
+
const { run, step, pending } = talk;
|
|
787
|
+
if (!this.fixedDue(talk) || step.question === undefined) return false;
|
|
788
|
+
const key = this.stepKey(run, step);
|
|
789
|
+
turn.messages.push({ text: render(step.question, this.scope(turn, run)), kind: "verbatim", afterMs: turn.afterMs, key, runId: run.id, stepId: step.id });
|
|
790
|
+
turn.afterMs = 0;
|
|
791
|
+
turn.spokeBy.add(run.id);
|
|
792
|
+
turn.spoke = true;
|
|
793
|
+
for (const field of pending) run.asked[field] = (run.asked[field] ?? 0) + 1;
|
|
794
|
+
this.outcome(turn, run, { kind: "collect", status: "ok", key, code: "asked-fixed", stepId: step.id });
|
|
795
|
+
return true;
|
|
687
796
|
}
|
|
688
797
|
|
|
689
798
|
/**
|
|
@@ -707,12 +816,15 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
707
816
|
const instructions = [...(this.options.instructions ?? []), ...own.instructions].filter((ins) => !ins.if || evaluate(ins.if, ctx, conditions));
|
|
708
817
|
const all = this.options.tools ?? [];
|
|
709
818
|
const allowed = own.tools;
|
|
819
|
+
const retry = !("idle" in talk) && this.retrying(talk);
|
|
710
820
|
return {
|
|
821
|
+
sessionId: turn.session.id,
|
|
711
822
|
talk,
|
|
712
|
-
input: turn.what.kind === "message" ? { kind: turn.kind, text: turn.what.text } : { kind: turn.kind },
|
|
823
|
+
input: turn.what.kind === "message" ? { kind: turn.kind, text: turn.what.text } : { kind: turn.kind, ...(retry ? { retry } : {}) },
|
|
713
824
|
context: turn.context,
|
|
714
825
|
data: turn.session.data,
|
|
715
826
|
history: this.historyOf(turn),
|
|
827
|
+
said: [...turn.messages],
|
|
716
828
|
now: turn.now,
|
|
717
829
|
instructions,
|
|
718
830
|
tools: allowed ? all.filter((tool) => allowed.includes(tool.id)) : all,
|
|
@@ -753,6 +865,11 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
753
865
|
return;
|
|
754
866
|
}
|
|
755
867
|
if (!this.premiseHolds(turn, run, flow)) return;
|
|
868
|
+
// The flow was edited off 'stay' after this run finished its steps: its new `onEnd` decides now.
|
|
869
|
+
if (run.staying && flow.onEnd !== "stay") {
|
|
870
|
+
this.finishFlow(turn, run, flow);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
756
873
|
const resuming = run.status === "asking";
|
|
757
874
|
run.status = "running";
|
|
758
875
|
if (run.stepId === null) {
|
|
@@ -795,6 +912,12 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
795
912
|
return ok;
|
|
796
913
|
}
|
|
797
914
|
|
|
915
|
+
/** `silenced: { skip: true }`: the message this step would send is dropped, and the run takes `then` as if it had gone out. */
|
|
916
|
+
private skipSilenced(turn: Turn<C, D>, run: Run, flow: Flow<C, D>, step: Step<C, D>, kind: StepOutcomeKind): void {
|
|
917
|
+
this.outcome(turn, run, { kind, status: "skipped", key: this.stepKey(run, step), code: "silenced", detail: turn.silenced, next: nextLabel(step.then) });
|
|
918
|
+
this.follow(turn, run, flow, step, step.then);
|
|
919
|
+
}
|
|
920
|
+
|
|
798
921
|
private async execute(turn: Turn<C, D>, run: Run, flow: Flow<C, D>, step: Step<C, D>, resuming: boolean): Promise<void> {
|
|
799
922
|
const data: Record<string, unknown> = turn.session.data;
|
|
800
923
|
const key = this.stepKey(run, step);
|
|
@@ -804,7 +927,9 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
804
927
|
let pending: string[] = [];
|
|
805
928
|
if (step.collect?.length) {
|
|
806
929
|
pending = pendingFields(step, data, run.asked);
|
|
807
|
-
|
|
930
|
+
// A `stay` run answers even with nothing left to collect: answering is what it stays for. It only runs on a message
|
|
931
|
+
// (asking runs sit out wakes and events), and on a silenced one the check below keeps it asking.
|
|
932
|
+
if (!pending.length && !run.staying) {
|
|
808
933
|
this.reportMaxAsks(turn, run, step);
|
|
809
934
|
this.outcome(turn, run, resuming
|
|
810
935
|
? { kind, status: "ok", key, next: nextLabel(step.then) }
|
|
@@ -815,6 +940,7 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
815
940
|
}
|
|
816
941
|
if (turn.silenced !== undefined) {
|
|
817
942
|
if (resuming) run.status = "asking"; // the gate is closed; it speaks when the gate opens
|
|
943
|
+
else if (turn.skipSilenced) this.skipSilenced(turn, run, flow, step, kind);
|
|
818
944
|
else this.endRun(turn, run, "skipped", "silenced", kind, turn.silenced);
|
|
819
945
|
return;
|
|
820
946
|
}
|
|
@@ -825,13 +951,16 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
825
951
|
}
|
|
826
952
|
}
|
|
827
953
|
run.status = "asking";
|
|
954
|
+
// Reached after Speak (settle's drain), a talk step waits for the next message; a fixed question costs no call, so it goes out now, as a say would.
|
|
828
955
|
if (!turn.speakDone) turn.talk = { run, flow, step, pending };
|
|
956
|
+
else this.askFixed(turn, { run, flow, step, pending });
|
|
829
957
|
return;
|
|
830
958
|
}
|
|
831
959
|
|
|
832
960
|
if (isSay(step)) {
|
|
833
961
|
if (turn.silenced !== undefined) {
|
|
834
|
-
this.
|
|
962
|
+
if (turn.skipSilenced) this.skipSilenced(turn, run, flow, step, "say");
|
|
963
|
+
else this.endRun(turn, run, "skipped", "silenced", "say", turn.silenced);
|
|
835
964
|
return;
|
|
836
965
|
}
|
|
837
966
|
if (step.once) {
|
|
@@ -877,6 +1006,13 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
877
1006
|
result = { failed: error instanceof Error ? error.message : String(error) };
|
|
878
1007
|
}
|
|
879
1008
|
}
|
|
1009
|
+
// The handler already ran, so a defer that cannot be parsed must not throw
|
|
1010
|
+
// the turn: every replay would repeat the side effect and fail again.
|
|
1011
|
+
if ("defer" in result && !isDuration(String(result.defer))) {
|
|
1012
|
+
result = {
|
|
1013
|
+
failed: `action "${step.do}" asked to defer by "${String(result.defer)}", which is not a duration. Use a value like "2m" or "1h".`,
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
880
1016
|
if ("ok" in result) {
|
|
881
1017
|
if (result.spoke) {
|
|
882
1018
|
turn.spokeBy.add(run.id);
|
|
@@ -966,18 +1102,22 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
966
1102
|
}
|
|
967
1103
|
if ("step" in target) {
|
|
968
1104
|
const data: Record<string, unknown> = turn.session.data;
|
|
969
|
-
|
|
1105
|
+
// Forgetting a field means asking for it from scratch: its ask count goes too, so a fixed question goes out again.
|
|
1106
|
+
for (const field of target.clear ?? []) {
|
|
1107
|
+
delete data[field];
|
|
1108
|
+
delete run.asked[field];
|
|
1109
|
+
}
|
|
970
1110
|
this.jump(turn, run, flow, target.step);
|
|
971
1111
|
return;
|
|
972
1112
|
}
|
|
973
1113
|
const childId = render(target.flow, this.scope(turn, run));
|
|
974
1114
|
const key = this.stepKey(run, step);
|
|
975
1115
|
this.endRun(turn, run, "flow");
|
|
976
|
-
this.chain(turn, childId, key, target.input ?? run.input, run.hop + 1);
|
|
1116
|
+
this.chain(turn, run, childId, key, target.input ?? run.input, run.hop + 1);
|
|
977
1117
|
}
|
|
978
1118
|
|
|
979
|
-
/** Start a child flow that
|
|
980
|
-
private chain(turn: Turn<C, D>, flowId: string, key: string, input: unknown, hop: number, keepData = false): void {
|
|
1119
|
+
/** Start a child flow that moves in this same phase. It inherits the floor when its parent held it, or when nobody did. */
|
|
1120
|
+
private chain(turn: Turn<C, D>, parent: Run, flowId: string, key: string, input: unknown, hop: number, keepData = false): void {
|
|
981
1121
|
const child = this.flows.get(flowId);
|
|
982
1122
|
if (!child) {
|
|
983
1123
|
turn.skipped.push({ flowId, anchor: turn.session.id, triggerKey: key, code: "flow-gone", message: OUTCOME_MESSAGES["flow-gone"] });
|
|
@@ -985,8 +1125,10 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
985
1125
|
}
|
|
986
1126
|
const run = this.startRun(turn, child, "flow", key, { payload: input, hop, keepData });
|
|
987
1127
|
if (!run) return;
|
|
988
|
-
turn.floorRunId
|
|
989
|
-
|
|
1128
|
+
if (turn.floorRunId === undefined || turn.floorRunId === parent.id) {
|
|
1129
|
+
turn.floorRunId = run.id;
|
|
1130
|
+
if (turn.ingesting) turn.floorFromIngest = true;
|
|
1131
|
+
}
|
|
990
1132
|
turn.queue.push(run);
|
|
991
1133
|
}
|
|
992
1134
|
|
|
@@ -1001,15 +1143,55 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
1001
1143
|
run.visits[stepId] = (run.visits[stepId] ?? 0) + 1;
|
|
1002
1144
|
run.status = "running";
|
|
1003
1145
|
delete run.waiting;
|
|
1146
|
+
delete run.staying;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/** `onEnd: 'stay'`: the run sits on its last talk step and answers every message from there, each answer a new visit and so a new key. */
|
|
1150
|
+
private stayAt(run: Run, step: Step<C, D>): void {
|
|
1151
|
+
this.enter(run, step.id);
|
|
1152
|
+
run.staying = true;
|
|
1153
|
+
run.status = "asking";
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* Where `stay` answers from: the talk step this run last took, so a branched flow stays on its own path. The flow's last talk step when the log names none.
|
|
1158
|
+
* ponytail: read back from `run.outcomes`, which keeps 50 lines; a tail of 50+ lines after the talk step (a long polling loop) loses it and the run
|
|
1159
|
+
* stays on the flow's last talk step. Upgrade: record the talk step on the run when it talks.
|
|
1160
|
+
*/
|
|
1161
|
+
private stayStep(run: Run, flow: Flow<C, D>): TalkOf<C, D> | undefined {
|
|
1162
|
+
for (let i = run.outcomes.length - 1; i >= 0; i--) {
|
|
1163
|
+
const { kind, stepId } = run.outcomes[i];
|
|
1164
|
+
const step = kind === "prompt" || kind === "collect" ? this.stepOf(flow, stepId ?? null) : undefined;
|
|
1165
|
+
if (step && isTalk(step)) return step;
|
|
1166
|
+
}
|
|
1167
|
+
return [...flow.steps].reverse().find(isTalk);
|
|
1004
1168
|
}
|
|
1005
1169
|
|
|
1006
1170
|
private finishFlow(turn: Turn<C, D>, run: Run, flow: Flow<C, D>): void {
|
|
1007
1171
|
const onEnd = flow.onEnd ?? "end";
|
|
1008
1172
|
const last = flow.steps[flow.steps.length - 1];
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
run
|
|
1173
|
+
const stay = onEnd === "stay" ? this.stayStep(run, flow) : undefined;
|
|
1174
|
+
if (stay) {
|
|
1175
|
+
// The steps after it ran once, on the way here; staying, it only answers.
|
|
1176
|
+
this.stayAt(run, stay);
|
|
1177
|
+
// One asker at a time. A run the lead's message went to holds the conversation and suspends the other asker;
|
|
1178
|
+
// any other run waits behind that asker and resumes when it is done.
|
|
1179
|
+
const others = turn.session.runs.filter((r) => r !== run && r.status === "asking");
|
|
1180
|
+
if (others.length && !(turn.what.kind === "message" && turn.floorRunId === run.id)) {
|
|
1181
|
+
run.status = "suspended";
|
|
1182
|
+
run.suspendedAt = turn.nowIso;
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
for (const other of others) {
|
|
1186
|
+
other.status = "suspended";
|
|
1187
|
+
other.suspendedAt = turn.nowIso;
|
|
1188
|
+
if (turn.talk?.run === other) turn.talk = undefined;
|
|
1189
|
+
}
|
|
1190
|
+
// A message nothing has answered yet gets its answer now. Reached in Ingest, the run stays asking: Decide judges its
|
|
1191
|
+
// branches as the asker's, and it answers when it moves.
|
|
1192
|
+
if (!turn.ingesting && turn.what.kind === "message" && turn.silenced === undefined && !turn.speakDone && turn.spokeBy.size === 0) {
|
|
1193
|
+
run.status = "running";
|
|
1194
|
+
}
|
|
1013
1195
|
return;
|
|
1014
1196
|
}
|
|
1015
1197
|
if (onEnd === "reset" && last) {
|
|
@@ -1017,7 +1199,7 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
1017
1199
|
// It is a chain into itself, so it costs a hop: a code-only flow that resets forever stops at the hop cap instead of spinning.
|
|
1018
1200
|
const key = this.stepKey(run, last);
|
|
1019
1201
|
this.endRun(turn, run, "reset");
|
|
1020
|
-
this.chain(turn, flow.id, key, run.input, run.hop + 1, true);
|
|
1202
|
+
this.chain(turn, run, flow.id, key, run.input, run.hop + 1, true);
|
|
1021
1203
|
return;
|
|
1022
1204
|
}
|
|
1023
1205
|
this.endRun(turn, run, "end");
|
|
@@ -1040,9 +1222,20 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
1040
1222
|
turn.schedule.push({ key: waiting.key, at });
|
|
1041
1223
|
}
|
|
1042
1224
|
|
|
1225
|
+
/** `resumeSuspended`, and on a message the resumed run's `if` branches are judged before it speaks, as the asker's are in Decide. */
|
|
1226
|
+
private resume(turn: Turn<C, D>): Run | undefined {
|
|
1227
|
+
const run = this.resumeSuspended(turn);
|
|
1228
|
+
if (run && turn.what.kind === "message") {
|
|
1229
|
+
const flow = this.flows.get(run.flowId);
|
|
1230
|
+
const step = flow && this.stepOf(flow, run.stepId);
|
|
1231
|
+
if (flow && step && isTalk(step)) this.fireBranch(turn, run, flow, step, null);
|
|
1232
|
+
}
|
|
1233
|
+
return run;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1043
1236
|
/** When nobody asks, the most recently suspended run returns to asking. */
|
|
1044
|
-
private resumeSuspended(turn: Turn<C, D>):
|
|
1045
|
-
if (this.asker(turn)) return;
|
|
1237
|
+
private resumeSuspended(turn: Turn<C, D>): Run | undefined {
|
|
1238
|
+
if (this.asker(turn)) return undefined;
|
|
1046
1239
|
const suspendedAt = (r: Run): string => r.suspendedAt ?? r.startedAt;
|
|
1047
1240
|
const next = turn.session.runs
|
|
1048
1241
|
.filter((r) => r.status === "suspended")
|
|
@@ -1051,6 +1244,7 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
1051
1244
|
next.status = "asking";
|
|
1052
1245
|
delete next.suspendedAt;
|
|
1053
1246
|
}
|
|
1247
|
+
return next;
|
|
1054
1248
|
}
|
|
1055
1249
|
|
|
1056
1250
|
// ── Settle: the one applier after Speak (design §4.7) ─────────────────
|
|
@@ -1098,17 +1292,25 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
1098
1292
|
}
|
|
1099
1293
|
for (const [field, raw] of Object.entries(spoken.fields)) this.writeField(turn, field, raw);
|
|
1100
1294
|
Object.assign(turn.session.data, spoken.data);
|
|
1101
|
-
|
|
1295
|
+
const fixed = talk.fixedQuestion !== undefined ? { code: "fixed-in-reply" as const } : {};
|
|
1296
|
+
this.outcome(turn, run, { kind: talkKind(step), status: "ok", key, llmCalls: spoken.llmCalls, stepId: step.id, ...fixed });
|
|
1102
1297
|
if (!turn.session.runs.includes(run)) return;
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1298
|
+
const pending = step.collect?.length ? pendingFields(step, turn.session.data, run.asked) : [];
|
|
1299
|
+
for (const field of pending) run.asked[field] = (run.asked[field] ?? 0) + 1;
|
|
1300
|
+
if (run.staying) {
|
|
1301
|
+
// Each answer is a new visit, so a new key, even while a field is still pending; that field's max-asks is reported once, when it gets there.
|
|
1302
|
+
const maxAsks = step.maxAsks ?? DEFAULT_MAX_ASKS;
|
|
1303
|
+
for (const field of pending) {
|
|
1304
|
+
if (run.asked[field] === maxAsks) this.outcome(turn, run, { kind: "collect", status: "skipped", key, code: "max-asks", detail: field });
|
|
1109
1305
|
}
|
|
1110
|
-
this.
|
|
1306
|
+
this.stayAt(run, step);
|
|
1307
|
+
return;
|
|
1111
1308
|
}
|
|
1309
|
+
if (pending.length) {
|
|
1310
|
+
run.status = "asking";
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
this.reportMaxAsks(turn, run, step);
|
|
1112
1314
|
run.status = "running";
|
|
1113
1315
|
this.follow(turn, run, flow, step, step.then);
|
|
1114
1316
|
if (turn.session.runs.includes(run) && run.status === "running") turn.queue.push(run);
|
|
@@ -1146,14 +1348,21 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
1146
1348
|
return new Date(reset !== undefined && reset > ladder ? reset : ladder);
|
|
1147
1349
|
}
|
|
1148
1350
|
|
|
1149
|
-
/** The assistant spoke
|
|
1351
|
+
/** The assistant spoke this turn: its silence wakes replace the ones its last words set. */
|
|
1150
1352
|
private armSilence(turn: Turn<C, D>): void {
|
|
1353
|
+
const previous = turn.original?.lastAssistantAt;
|
|
1354
|
+
if (turn.session.lastAssistantAt === previous) return;
|
|
1355
|
+
turn.schedule.push(...this.silenceWakes(turn, previous));
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/** The assistant spoke last: every silence flow passing `if` and `repeat` gets a wake. */
|
|
1359
|
+
private silenceWakes(turn: Turn<C, D>, previous?: string): ScheduleEntry[] {
|
|
1151
1360
|
const { session } = turn;
|
|
1152
1361
|
const { lastAssistantAt, lastUserAt } = session;
|
|
1153
|
-
if (!lastAssistantAt
|
|
1154
|
-
if (lastUserAt && Date.parse(lastUserAt) > Date.parse(lastAssistantAt)) return;
|
|
1362
|
+
if (!lastAssistantAt) return [];
|
|
1363
|
+
if (lastUserAt && Date.parse(lastUserAt) > Date.parse(lastAssistantAt)) return [];
|
|
1155
1364
|
const ms = Date.parse(lastAssistantAt);
|
|
1156
|
-
const
|
|
1365
|
+
const wakes: ScheduleEntry[] = [];
|
|
1157
1366
|
for (const flow of this.flows.values()) {
|
|
1158
1367
|
const trigger = (flow.on ?? []).find((t): t is SilenceTrigger<C, D> => "silence" in t);
|
|
1159
1368
|
if (!trigger) continue;
|
|
@@ -1161,12 +1370,13 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
1161
1370
|
if (trigger.if && !this.holds(trigger.if, turn, this.draftRun(turn, flow, "silence", key))) continue;
|
|
1162
1371
|
if (!this.repeatAllows(turn, flow, trigger, key)) continue;
|
|
1163
1372
|
const at = this.snap(turn, new Date(ms + parseDuration(trigger.silence)), trigger.businessHours);
|
|
1164
|
-
|
|
1373
|
+
wakes.push({
|
|
1165
1374
|
key: `silence:${flow.id}:${session.id}:${ms}`,
|
|
1166
1375
|
at,
|
|
1167
1376
|
...(previous ? { replaces: `silence:${flow.id}:${session.id}:${Date.parse(previous)}` } : {}),
|
|
1168
1377
|
});
|
|
1169
1378
|
}
|
|
1379
|
+
return wakes;
|
|
1170
1380
|
}
|
|
1171
1381
|
|
|
1172
1382
|
// ── Return (design §4.8) ──────────────────────────────────────────────
|