@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/dist/core/Runner.js
CHANGED
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* begin ─▶ understandRequest ─(Understand)─▶ decide ─▶ advance ─(Speak)─▶ settle ─▶ finish
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
11
|
-
import { parseDuration } from "../utils/duration.js";
|
|
10
|
+
import { isDuration, parseDuration } from "../utils/duration.js";
|
|
12
11
|
import { OUTCOME_MESSAGES } from "../utils/outcomes.js";
|
|
13
12
|
import { coerceField, DEFAULT_MAX_ASKS, extractMode, isKnown, pendingFields } from "../utils/schema.js";
|
|
14
13
|
import { render, renderDeep } from "../utils/template.js";
|
|
@@ -64,6 +63,15 @@ function defaultRepeat(kind) {
|
|
|
64
63
|
function talkKind(step) {
|
|
65
64
|
return step.collect?.length ? "collect" : "prompt";
|
|
66
65
|
}
|
|
66
|
+
/** Everything a flow collects: its own `collect`, then each talk step's, once each. */
|
|
67
|
+
function flowFields(flow) {
|
|
68
|
+
const fields = new Set(flow.collect);
|
|
69
|
+
for (const step of flow.steps)
|
|
70
|
+
if (isTalk(step))
|
|
71
|
+
for (const field of step.collect ?? [])
|
|
72
|
+
fields.add(field);
|
|
73
|
+
return [...fields];
|
|
74
|
+
}
|
|
67
75
|
function kindOf(step) {
|
|
68
76
|
if (!step)
|
|
69
77
|
return "if";
|
|
@@ -100,16 +108,29 @@ export class Runner {
|
|
|
100
108
|
}
|
|
101
109
|
// ── Load + Ingest ─────────────────────────────────────────────────────
|
|
102
110
|
begin(input) {
|
|
103
|
-
const now =
|
|
111
|
+
const now = this.now();
|
|
112
|
+
const turn = this.open(input, describe(input, now.toISOString()), now);
|
|
113
|
+
if (turn.what.kind === "wake" && !input.session) {
|
|
114
|
+
this.ignore(turn, "no-session", turn.what.key);
|
|
115
|
+
return turn;
|
|
116
|
+
}
|
|
117
|
+
turn.ingesting = true;
|
|
118
|
+
this.ingest(turn);
|
|
119
|
+
turn.ingesting = false;
|
|
120
|
+
return turn;
|
|
121
|
+
}
|
|
122
|
+
now() {
|
|
123
|
+
return (this.options.clock ?? (() => new Date()))();
|
|
124
|
+
}
|
|
125
|
+
open(input, what, now) {
|
|
104
126
|
const nowIso = now.toISOString();
|
|
105
|
-
const what = describe(input, nowIso);
|
|
106
127
|
const silenced = typeof input.silenced === "string" ? input.silenced : input.silenced?.reason;
|
|
107
|
-
|
|
128
|
+
return {
|
|
108
129
|
input,
|
|
109
130
|
what,
|
|
110
131
|
kind: what.kind,
|
|
111
132
|
triggerKey: what.kind === "message" ? (what.id ?? what.at) : what.key,
|
|
112
|
-
session: input.session ?
|
|
133
|
+
session: input.session ? structuredClone(input.session) : freshSession(input.sessionId),
|
|
113
134
|
original: input.session,
|
|
114
135
|
now,
|
|
115
136
|
nowIso,
|
|
@@ -117,6 +138,7 @@ export class Runner {
|
|
|
117
138
|
context: input.context,
|
|
118
139
|
silenced,
|
|
119
140
|
understandAllowed: typeof input.silenced === "object" && input.silenced.understand === true,
|
|
141
|
+
skipSilenced: typeof input.silenced === "object" && input.silenced.skip === true,
|
|
120
142
|
ignored: false,
|
|
121
143
|
messages: [],
|
|
122
144
|
schedule: [],
|
|
@@ -133,14 +155,14 @@ export class Runner {
|
|
|
133
155
|
speakDone: false,
|
|
134
156
|
queue: [],
|
|
135
157
|
};
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
turn
|
|
141
|
-
|
|
142
|
-
turn.
|
|
143
|
-
return turn;
|
|
158
|
+
}
|
|
159
|
+
/** Every wake the session waits on: each parked run's own, then each silence flow's since the assistant last spoke. */
|
|
160
|
+
pendingWakes(input) {
|
|
161
|
+
// No input arrives, so nothing reads `what`: the inert wake shape only fills the field.
|
|
162
|
+
const turn = this.open({ ...input, sessionId: input.session.id }, { kind: "wake", key: "" }, this.now());
|
|
163
|
+
// `park` always sets both; the type leaves them optional, so a hand-built blob without them has no wake to give.
|
|
164
|
+
const parked = turn.session.runs.flatMap(({ status, waiting }) => status === "waiting" && waiting?.key && waiting.until ? [{ key: waiting.key, at: new Date(waiting.until) }] : []);
|
|
165
|
+
return [...parked, ...this.silenceWakes(turn)];
|
|
144
166
|
}
|
|
145
167
|
ingest(turn) {
|
|
146
168
|
const { what, session } = turn;
|
|
@@ -221,6 +243,9 @@ export class Runner {
|
|
|
221
243
|
const branch = step.branches?.find((b) => "if" in b && this.holds(b.if, turn, run));
|
|
222
244
|
const target = branch ? branch.then : step.else;
|
|
223
245
|
this.outcome(turn, run, { kind: "wait", status: "ok", code: "replied", next: nextLabel(target) });
|
|
246
|
+
// The reply is this run's while it moves, so a stay it reaches answers it.
|
|
247
|
+
if (!turn.floorFromIngest)
|
|
248
|
+
turn.floorRunId = run.id;
|
|
224
249
|
this.follow(turn, run, flow, step, target);
|
|
225
250
|
this.takeFloor(turn, run);
|
|
226
251
|
}
|
|
@@ -260,8 +285,10 @@ export class Runner {
|
|
|
260
285
|
/** `silence:<flowId>:<sessionId>:<lastAssistantAtMs>`, honoured only while the blob still shows that silence. */
|
|
261
286
|
silenceWake(turn, key) {
|
|
262
287
|
const rest = key.slice("silence:".length);
|
|
263
|
-
const flowId = rest.slice(0, rest.indexOf(":"));
|
|
264
288
|
const ms = Number(rest.slice(rest.lastIndexOf(":") + 1));
|
|
289
|
+
// A flow id may hold a ":" itself, so cut at the session id rather than at the first ":".
|
|
290
|
+
const cut = rest.lastIndexOf(`:${turn.session.id}:`);
|
|
291
|
+
const flowId = rest.slice(0, cut === -1 ? rest.indexOf(":") : cut);
|
|
265
292
|
const { lastAssistantAt } = turn.session;
|
|
266
293
|
const flow = this.flows.get(flowId);
|
|
267
294
|
if (!flow) {
|
|
@@ -316,13 +343,9 @@ export class Runner {
|
|
|
316
343
|
};
|
|
317
344
|
if (opts.trigger?.if && !this.holds(opts.trigger.if, turn, run))
|
|
318
345
|
return null;
|
|
319
|
-
const
|
|
320
|
-
if (
|
|
321
|
-
|
|
322
|
-
return skip("already-claimed");
|
|
323
|
-
if (turn.now.getTime() - Date.parse(heldAt) < parseDuration(repeat.cooldown))
|
|
324
|
-
return skip("cooldown");
|
|
325
|
-
}
|
|
346
|
+
const blocked = this.repeatBlocks(turn, dedupeKey, repeat);
|
|
347
|
+
if (blocked)
|
|
348
|
+
return skip(blocked);
|
|
326
349
|
if (hop >= MAX_HOP)
|
|
327
350
|
return skip("hop-limit");
|
|
328
351
|
const live = session.runs.find((r) => r.flowId === flow.id && r.anchor === anchor);
|
|
@@ -345,9 +368,11 @@ export class Runner {
|
|
|
345
368
|
}
|
|
346
369
|
session.runs.push(run);
|
|
347
370
|
turn.started.push({ runId: run.id, flowId: flow.id, anchor, dedupeKey });
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
371
|
+
const eventTrigger = opts.trigger && "event" in opts.trigger ? opts.trigger : undefined;
|
|
372
|
+
const after = eventTrigger?.after;
|
|
373
|
+
const at = this.snap(turn, new Date(turn.now.getTime() + (after ? parseDuration(after) : 0)), eventTrigger?.businessHours);
|
|
374
|
+
// With no `after`, only a closed hour parks the start: inside working hours the snap returns now.
|
|
375
|
+
if (after || at.getTime() > turn.now.getTime()) {
|
|
351
376
|
this.park(turn, run, { kind: "timer", key: `${run.id}:start:${at.getTime()}` }, at);
|
|
352
377
|
this.outcome(turn, run, { kind: "wait", status: "waiting", code: "awaiting-trigger", until: at.toISOString() });
|
|
353
378
|
}
|
|
@@ -369,12 +394,16 @@ export class Runner {
|
|
|
369
394
|
}
|
|
370
395
|
repeatAllows(turn, flow, trigger, key) {
|
|
371
396
|
const repeat = trigger.repeat ?? defaultRepeat(triggerKind(trigger));
|
|
372
|
-
|
|
397
|
+
return !this.repeatBlocks(turn, `${flow.id}:${this.anchorOf(turn, flow)}:${repeat === "always" ? key : ""}`, repeat);
|
|
398
|
+
}
|
|
399
|
+
/** Why the claim already on `dedupeKey` stops another start, or nothing when there is none or its cooldown has run out. */
|
|
400
|
+
repeatBlocks(turn, dedupeKey, repeat) {
|
|
401
|
+
const heldAt = this.claimAt(turn, dedupeKey);
|
|
373
402
|
if (heldAt === undefined)
|
|
374
|
-
return
|
|
403
|
+
return undefined;
|
|
375
404
|
if (typeof repeat === "string")
|
|
376
|
-
return
|
|
377
|
-
return turn.now.getTime() - Date.parse(heldAt)
|
|
405
|
+
return "already-claimed";
|
|
406
|
+
return turn.now.getTime() - Date.parse(heldAt) < parseDuration(repeat.cooldown) ? "cooldown" : undefined;
|
|
378
407
|
}
|
|
379
408
|
/** A run as it would be if started now, for trigger-level predicates. */
|
|
380
409
|
draftRun(turn, flow, kind, key, payload) {
|
|
@@ -450,20 +479,31 @@ export class Runner {
|
|
|
450
479
|
}
|
|
451
480
|
const fields = {};
|
|
452
481
|
const data = turn.session.data;
|
|
482
|
+
const harvestable = (field) => {
|
|
483
|
+
const def = this.options.fields[field];
|
|
484
|
+
return def !== undefined && !isKnown(data[field]) && extractMode(def) === "anywhere";
|
|
485
|
+
};
|
|
453
486
|
for (const flow of [...(floorFlow ? [floorFlow] : []), ...eligible]) {
|
|
454
|
-
for (const
|
|
455
|
-
|
|
456
|
-
continue;
|
|
457
|
-
for (const field of step.collect ?? []) {
|
|
458
|
-
const def = this.options.fields[field];
|
|
459
|
-
if (def && !isKnown(data[field]) && extractMode(def) === "anywhere")
|
|
460
|
-
fields[field] = def;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
487
|
+
for (const field of flowFields(flow).filter(harvestable))
|
|
488
|
+
fields[field] = this.options.fields[field];
|
|
463
489
|
}
|
|
464
|
-
|
|
490
|
+
const judging = messageFlows.length > 0 || mentionFlows.length > 0 || branches.length > 0 || Object.keys(fields).length > 0;
|
|
491
|
+
// With nobody on the floor the catch-all may take this message, and an opening message often says the most. The
|
|
492
|
+
// 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.
|
|
493
|
+
// 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.
|
|
494
|
+
const fallback = floorRun ? undefined : this.messageFlows(turn, (list) => list.length === 0)[0];
|
|
495
|
+
if (fallback) {
|
|
496
|
+
const first = fallback.steps[0];
|
|
497
|
+
const free = new Set(first && isTalk(first) && first.question === undefined ? first.collect : []);
|
|
498
|
+
const own = flowFields(fallback).filter(harvestable);
|
|
499
|
+
if (judging || own.some((field) => !free.has(field)))
|
|
500
|
+
for (const field of own)
|
|
501
|
+
fields[field] = this.options.fields[field];
|
|
502
|
+
}
|
|
503
|
+
if (!judging && !Object.keys(fields).length)
|
|
465
504
|
return null;
|
|
466
505
|
return {
|
|
506
|
+
sessionId: turn.session.id,
|
|
467
507
|
text: turn.what.text,
|
|
468
508
|
history: this.historyOf(turn),
|
|
469
509
|
context: turn.context,
|
|
@@ -527,6 +567,8 @@ export class Runner {
|
|
|
527
567
|
return;
|
|
528
568
|
turn.llmCalls += understanding?.llmCalls ?? 0;
|
|
529
569
|
turn.usage = addUsage(turn.usage, understanding?.usage);
|
|
570
|
+
if (understanding?.asks !== undefined)
|
|
571
|
+
turn.asks = understanding.asks;
|
|
530
572
|
const key = turn.triggerKey;
|
|
531
573
|
const asker = this.asker(turn);
|
|
532
574
|
// Routing is decided before extraction lands so `clearOnStart` runs first, and applied after mention runs.
|
|
@@ -570,6 +612,9 @@ export class Runner {
|
|
|
570
612
|
turn.floorRunId = run.id;
|
|
571
613
|
}
|
|
572
614
|
}
|
|
615
|
+
// Unless routing or Ingest moved it, the message is the asker's: written down, so a chain beside it cannot take it.
|
|
616
|
+
if (asker && !route && !turn.floorFromIngest)
|
|
617
|
+
turn.floorRunId = asker.id;
|
|
573
618
|
for (const [field, raw] of Object.entries(understanding?.fields ?? {}))
|
|
574
619
|
this.writeField(turn, field, raw);
|
|
575
620
|
if (asker && turn.session.runs.includes(asker) && asker.status === "asking") {
|
|
@@ -580,7 +625,13 @@ export class Runner {
|
|
|
580
625
|
}
|
|
581
626
|
}
|
|
582
627
|
fireBranch(turn, run, flow, step, understanding) {
|
|
583
|
-
|
|
628
|
+
// Staying, a run does not take an `if` branch back onto a path it has already run: the fact it tests is still true,
|
|
629
|
+
// so it would re-run that path on every message. A `when` branch is judged on each new message and always counts.
|
|
630
|
+
// 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`.
|
|
631
|
+
const ran = (then) => typeof then === "string" ? then === "end" || (run.visits[then] ?? 0) > 0 : "step" in then && (run.visits[then.step] ?? 0) > 0;
|
|
632
|
+
const hit = (branch, index) => "if" in branch
|
|
633
|
+
? !(run.staying && ran(branch.then)) && this.holds(branch.if, turn, run)
|
|
634
|
+
: understanding?.branches[`${run.id}/${step.id}/${index}`] === true;
|
|
584
635
|
const index = (step.branches ?? []).findIndex(hit);
|
|
585
636
|
if (index < 0)
|
|
586
637
|
return;
|
|
@@ -610,10 +661,64 @@ export class Runner {
|
|
|
610
661
|
async advance(turn) {
|
|
611
662
|
if (turn.ignored)
|
|
612
663
|
return null;
|
|
613
|
-
this.
|
|
664
|
+
this.resume(turn);
|
|
614
665
|
turn.queue = [...turn.session.runs];
|
|
615
666
|
await this.drain(turn);
|
|
616
|
-
|
|
667
|
+
// An asker that moved on without a word hands the message to the run it suspended, and that one to the next.
|
|
668
|
+
// Each pass takes a run off the suspended stack, so the stack's size bounds the loop.
|
|
669
|
+
for (let left = turn.session.runs.length; left > 0; left--) {
|
|
670
|
+
if (turn.what.kind !== "message" || turn.silenced !== undefined || turn.talk || turn.spokeBy.size > 0)
|
|
671
|
+
break;
|
|
672
|
+
const resumed = this.resume(turn);
|
|
673
|
+
if (!resumed)
|
|
674
|
+
break;
|
|
675
|
+
turn.queue.push(resumed);
|
|
676
|
+
await this.drain(turn);
|
|
677
|
+
}
|
|
678
|
+
const speaker = this.speaker(turn);
|
|
679
|
+
if (!speaker || "idle" in speaker || !this.fixedDue(speaker))
|
|
680
|
+
return speaker;
|
|
681
|
+
// A question the lead asked gets its answer: the step speaks once, answering it and asking the fixed question.
|
|
682
|
+
// Appending the text word for word after an answer could repeat or contradict it. Unjudged counts as asked,
|
|
683
|
+
// and so does the retry of a failed answer: a question sent over the lead's own drops it.
|
|
684
|
+
const answerFirst = turn.what.kind === "message" ? turn.asks !== false : this.retrying(speaker);
|
|
685
|
+
if (answerFirst && speaker.step.question !== undefined) {
|
|
686
|
+
speaker.fixedQuestion = render(speaker.step.question, this.scope(turn, speaker.run));
|
|
687
|
+
return speaker;
|
|
688
|
+
}
|
|
689
|
+
this.askFixed(turn, speaker);
|
|
690
|
+
turn.talk = undefined;
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
/** The step failed at the provider and runs again on its retry wake: deferTalk left `deferred` as its last line. */
|
|
694
|
+
retrying({ run, step }) {
|
|
695
|
+
const last = run.outcomes.at(-1);
|
|
696
|
+
return last?.status === "deferred" && last.stepId === step.id;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* The step's fixed question is due: this is its first ask, with every field it collects still unknown and none asked
|
|
700
|
+
* yet, and the run is not staying (a staying run answers the lead). Otherwise the AI phrases the ask.
|
|
701
|
+
*/
|
|
702
|
+
fixedDue({ run, step, pending }) {
|
|
703
|
+
const { question, collect = [] } = step;
|
|
704
|
+
if (question === undefined || run.staying)
|
|
705
|
+
return false;
|
|
706
|
+
return pending.length === collect.length && pending.every((field) => (run.asked[field] ?? 0) === 0);
|
|
707
|
+
}
|
|
708
|
+
/** Send the step's fixed question when it is due. It counts as one ask of each field. */
|
|
709
|
+
askFixed(turn, talk) {
|
|
710
|
+
const { run, step, pending } = talk;
|
|
711
|
+
if (!this.fixedDue(talk) || step.question === undefined)
|
|
712
|
+
return false;
|
|
713
|
+
const key = this.stepKey(run, step);
|
|
714
|
+
turn.messages.push({ text: render(step.question, this.scope(turn, run)), kind: "verbatim", afterMs: turn.afterMs, key, runId: run.id, stepId: step.id });
|
|
715
|
+
turn.afterMs = 0;
|
|
716
|
+
turn.spokeBy.add(run.id);
|
|
717
|
+
turn.spoke = true;
|
|
718
|
+
for (const field of pending)
|
|
719
|
+
run.asked[field] = (run.asked[field] ?? 0) + 1;
|
|
720
|
+
this.outcome(turn, run, { kind: "collect", status: "ok", key, code: "asked-fixed", stepId: step.id });
|
|
721
|
+
return true;
|
|
617
722
|
}
|
|
618
723
|
/**
|
|
619
724
|
* Everything Speak needs for this turn's one speaker. Instructions are agent,
|
|
@@ -636,12 +741,15 @@ export class Runner {
|
|
|
636
741
|
const instructions = [...(this.options.instructions ?? []), ...own.instructions].filter((ins) => !ins.if || evaluate(ins.if, ctx, conditions));
|
|
637
742
|
const all = this.options.tools ?? [];
|
|
638
743
|
const allowed = own.tools;
|
|
744
|
+
const retry = !("idle" in talk) && this.retrying(talk);
|
|
639
745
|
return {
|
|
746
|
+
sessionId: turn.session.id,
|
|
640
747
|
talk,
|
|
641
|
-
input: turn.what.kind === "message" ? { kind: turn.kind, text: turn.what.text } : { kind: turn.kind },
|
|
748
|
+
input: turn.what.kind === "message" ? { kind: turn.kind, text: turn.what.text } : { kind: turn.kind, ...(retry ? { retry } : {}) },
|
|
642
749
|
context: turn.context,
|
|
643
750
|
data: turn.session.data,
|
|
644
751
|
history: this.historyOf(turn),
|
|
752
|
+
said: [...turn.messages],
|
|
645
753
|
now: turn.now,
|
|
646
754
|
instructions,
|
|
647
755
|
tools: allowed ? all.filter((tool) => allowed.includes(tool.id)) : all,
|
|
@@ -684,6 +792,11 @@ export class Runner {
|
|
|
684
792
|
}
|
|
685
793
|
if (!this.premiseHolds(turn, run, flow))
|
|
686
794
|
return;
|
|
795
|
+
// The flow was edited off 'stay' after this run finished its steps: its new `onEnd` decides now.
|
|
796
|
+
if (run.staying && flow.onEnd !== "stay") {
|
|
797
|
+
this.finishFlow(turn, run, flow);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
687
800
|
const resuming = run.status === "asking";
|
|
688
801
|
run.status = "running";
|
|
689
802
|
if (run.stepId === null) {
|
|
@@ -726,6 +839,11 @@ export class Runner {
|
|
|
726
839
|
this.endRun(turn, run, "skipped", "premise-changed");
|
|
727
840
|
return ok;
|
|
728
841
|
}
|
|
842
|
+
/** `silenced: { skip: true }`: the message this step would send is dropped, and the run takes `then` as if it had gone out. */
|
|
843
|
+
skipSilenced(turn, run, flow, step, kind) {
|
|
844
|
+
this.outcome(turn, run, { kind, status: "skipped", key: this.stepKey(run, step), code: "silenced", detail: turn.silenced, next: nextLabel(step.then) });
|
|
845
|
+
this.follow(turn, run, flow, step, step.then);
|
|
846
|
+
}
|
|
729
847
|
async execute(turn, run, flow, step, resuming) {
|
|
730
848
|
const data = turn.session.data;
|
|
731
849
|
const key = this.stepKey(run, step);
|
|
@@ -734,7 +852,9 @@ export class Runner {
|
|
|
734
852
|
let pending = [];
|
|
735
853
|
if (step.collect?.length) {
|
|
736
854
|
pending = pendingFields(step, data, run.asked);
|
|
737
|
-
|
|
855
|
+
// A `stay` run answers even with nothing left to collect: answering is what it stays for. It only runs on a message
|
|
856
|
+
// (asking runs sit out wakes and events), and on a silenced one the check below keeps it asking.
|
|
857
|
+
if (!pending.length && !run.staying) {
|
|
738
858
|
this.reportMaxAsks(turn, run, step);
|
|
739
859
|
this.outcome(turn, run, resuming
|
|
740
860
|
? { kind, status: "ok", key, next: nextLabel(step.then) }
|
|
@@ -746,6 +866,8 @@ export class Runner {
|
|
|
746
866
|
if (turn.silenced !== undefined) {
|
|
747
867
|
if (resuming)
|
|
748
868
|
run.status = "asking"; // the gate is closed; it speaks when the gate opens
|
|
869
|
+
else if (turn.skipSilenced)
|
|
870
|
+
this.skipSilenced(turn, run, flow, step, kind);
|
|
749
871
|
else
|
|
750
872
|
this.endRun(turn, run, "skipped", "silenced", kind, turn.silenced);
|
|
751
873
|
return;
|
|
@@ -757,13 +879,19 @@ export class Runner {
|
|
|
757
879
|
}
|
|
758
880
|
}
|
|
759
881
|
run.status = "asking";
|
|
882
|
+
// 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.
|
|
760
883
|
if (!turn.speakDone)
|
|
761
884
|
turn.talk = { run, flow, step, pending };
|
|
885
|
+
else
|
|
886
|
+
this.askFixed(turn, { run, flow, step, pending });
|
|
762
887
|
return;
|
|
763
888
|
}
|
|
764
889
|
if (isSay(step)) {
|
|
765
890
|
if (turn.silenced !== undefined) {
|
|
766
|
-
|
|
891
|
+
if (turn.skipSilenced)
|
|
892
|
+
this.skipSilenced(turn, run, flow, step, "say");
|
|
893
|
+
else
|
|
894
|
+
this.endRun(turn, run, "skipped", "silenced", "say", turn.silenced);
|
|
767
895
|
return;
|
|
768
896
|
}
|
|
769
897
|
if (step.once) {
|
|
@@ -810,6 +938,13 @@ export class Runner {
|
|
|
810
938
|
result = { failed: error instanceof Error ? error.message : String(error) };
|
|
811
939
|
}
|
|
812
940
|
}
|
|
941
|
+
// The handler already ran, so a defer that cannot be parsed must not throw
|
|
942
|
+
// the turn: every replay would repeat the side effect and fail again.
|
|
943
|
+
if ("defer" in result && !isDuration(String(result.defer))) {
|
|
944
|
+
result = {
|
|
945
|
+
failed: `action "${step.do}" asked to defer by "${String(result.defer)}", which is not a duration. Use a value like "2m" or "1h".`,
|
|
946
|
+
};
|
|
947
|
+
}
|
|
813
948
|
if ("ok" in result) {
|
|
814
949
|
if (result.spoke) {
|
|
815
950
|
turn.spokeBy.add(run.id);
|
|
@@ -901,18 +1036,21 @@ export class Runner {
|
|
|
901
1036
|
}
|
|
902
1037
|
if ("step" in target) {
|
|
903
1038
|
const data = turn.session.data;
|
|
904
|
-
|
|
1039
|
+
// Forgetting a field means asking for it from scratch: its ask count goes too, so a fixed question goes out again.
|
|
1040
|
+
for (const field of target.clear ?? []) {
|
|
905
1041
|
delete data[field];
|
|
1042
|
+
delete run.asked[field];
|
|
1043
|
+
}
|
|
906
1044
|
this.jump(turn, run, flow, target.step);
|
|
907
1045
|
return;
|
|
908
1046
|
}
|
|
909
1047
|
const childId = render(target.flow, this.scope(turn, run));
|
|
910
1048
|
const key = this.stepKey(run, step);
|
|
911
1049
|
this.endRun(turn, run, "flow");
|
|
912
|
-
this.chain(turn, childId, key, target.input ?? run.input, run.hop + 1);
|
|
1050
|
+
this.chain(turn, run, childId, key, target.input ?? run.input, run.hop + 1);
|
|
913
1051
|
}
|
|
914
|
-
/** Start a child flow that
|
|
915
|
-
chain(turn, flowId, key, input, hop, keepData = false) {
|
|
1052
|
+
/** Start a child flow that moves in this same phase. It inherits the floor when its parent held it, or when nobody did. */
|
|
1053
|
+
chain(turn, parent, flowId, key, input, hop, keepData = false) {
|
|
916
1054
|
const child = this.flows.get(flowId);
|
|
917
1055
|
if (!child) {
|
|
918
1056
|
turn.skipped.push({ flowId, anchor: turn.session.id, triggerKey: key, code: "flow-gone", message: OUTCOME_MESSAGES["flow-gone"] });
|
|
@@ -921,9 +1059,11 @@ export class Runner {
|
|
|
921
1059
|
const run = this.startRun(turn, child, "flow", key, { payload: input, hop, keepData });
|
|
922
1060
|
if (!run)
|
|
923
1061
|
return;
|
|
924
|
-
turn.floorRunId
|
|
925
|
-
|
|
926
|
-
turn.
|
|
1062
|
+
if (turn.floorRunId === undefined || turn.floorRunId === parent.id) {
|
|
1063
|
+
turn.floorRunId = run.id;
|
|
1064
|
+
if (turn.ingesting)
|
|
1065
|
+
turn.floorFromIngest = true;
|
|
1066
|
+
}
|
|
927
1067
|
turn.queue.push(run);
|
|
928
1068
|
}
|
|
929
1069
|
jump(turn, run, flow, stepId) {
|
|
@@ -938,14 +1078,54 @@ export class Runner {
|
|
|
938
1078
|
run.visits[stepId] = (run.visits[stepId] ?? 0) + 1;
|
|
939
1079
|
run.status = "running";
|
|
940
1080
|
delete run.waiting;
|
|
1081
|
+
delete run.staying;
|
|
1082
|
+
}
|
|
1083
|
+
/** `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. */
|
|
1084
|
+
stayAt(run, step) {
|
|
1085
|
+
this.enter(run, step.id);
|
|
1086
|
+
run.staying = true;
|
|
1087
|
+
run.status = "asking";
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* 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.
|
|
1091
|
+
* 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
|
|
1092
|
+
* stays on the flow's last talk step. Upgrade: record the talk step on the run when it talks.
|
|
1093
|
+
*/
|
|
1094
|
+
stayStep(run, flow) {
|
|
1095
|
+
for (let i = run.outcomes.length - 1; i >= 0; i--) {
|
|
1096
|
+
const { kind, stepId } = run.outcomes[i];
|
|
1097
|
+
const step = kind === "prompt" || kind === "collect" ? this.stepOf(flow, stepId ?? null) : undefined;
|
|
1098
|
+
if (step && isTalk(step))
|
|
1099
|
+
return step;
|
|
1100
|
+
}
|
|
1101
|
+
return [...flow.steps].reverse().find(isTalk);
|
|
941
1102
|
}
|
|
942
1103
|
finishFlow(turn, run, flow) {
|
|
943
1104
|
const onEnd = flow.onEnd ?? "end";
|
|
944
1105
|
const last = flow.steps[flow.steps.length - 1];
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
run
|
|
1106
|
+
const stay = onEnd === "stay" ? this.stayStep(run, flow) : undefined;
|
|
1107
|
+
if (stay) {
|
|
1108
|
+
// The steps after it ran once, on the way here; staying, it only answers.
|
|
1109
|
+
this.stayAt(run, stay);
|
|
1110
|
+
// One asker at a time. A run the lead's message went to holds the conversation and suspends the other asker;
|
|
1111
|
+
// any other run waits behind that asker and resumes when it is done.
|
|
1112
|
+
const others = turn.session.runs.filter((r) => r !== run && r.status === "asking");
|
|
1113
|
+
if (others.length && !(turn.what.kind === "message" && turn.floorRunId === run.id)) {
|
|
1114
|
+
run.status = "suspended";
|
|
1115
|
+
run.suspendedAt = turn.nowIso;
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
for (const other of others) {
|
|
1119
|
+
other.status = "suspended";
|
|
1120
|
+
other.suspendedAt = turn.nowIso;
|
|
1121
|
+
if (turn.talk?.run === other)
|
|
1122
|
+
turn.talk = undefined;
|
|
1123
|
+
}
|
|
1124
|
+
// A message nothing has answered yet gets its answer now. Reached in Ingest, the run stays asking: Decide judges its
|
|
1125
|
+
// branches as the asker's, and it answers when it moves.
|
|
1126
|
+
if (!turn.ingesting && turn.what.kind === "message" && turn.silenced === undefined && !turn.speakDone && turn.spokeBy.size === 0) {
|
|
1127
|
+
run.status = "running";
|
|
1128
|
+
}
|
|
949
1129
|
return;
|
|
950
1130
|
}
|
|
951
1131
|
if (onEnd === "reset" && last) {
|
|
@@ -953,7 +1133,7 @@ export class Runner {
|
|
|
953
1133
|
// 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.
|
|
954
1134
|
const key = this.stepKey(run, last);
|
|
955
1135
|
this.endRun(turn, run, "reset");
|
|
956
|
-
this.chain(turn, flow.id, key, run.input, run.hop + 1, true);
|
|
1136
|
+
this.chain(turn, run, flow.id, key, run.input, run.hop + 1, true);
|
|
957
1137
|
return;
|
|
958
1138
|
}
|
|
959
1139
|
this.endRun(turn, run, "end");
|
|
@@ -975,10 +1155,21 @@ export class Runner {
|
|
|
975
1155
|
run.waiting = { ...waiting, until: at.toISOString(), setAt: turn.nowIso };
|
|
976
1156
|
turn.schedule.push({ key: waiting.key, at });
|
|
977
1157
|
}
|
|
1158
|
+
/** `resumeSuspended`, and on a message the resumed run's `if` branches are judged before it speaks, as the asker's are in Decide. */
|
|
1159
|
+
resume(turn) {
|
|
1160
|
+
const run = this.resumeSuspended(turn);
|
|
1161
|
+
if (run && turn.what.kind === "message") {
|
|
1162
|
+
const flow = this.flows.get(run.flowId);
|
|
1163
|
+
const step = flow && this.stepOf(flow, run.stepId);
|
|
1164
|
+
if (flow && step && isTalk(step))
|
|
1165
|
+
this.fireBranch(turn, run, flow, step, null);
|
|
1166
|
+
}
|
|
1167
|
+
return run;
|
|
1168
|
+
}
|
|
978
1169
|
/** When nobody asks, the most recently suspended run returns to asking. */
|
|
979
1170
|
resumeSuspended(turn) {
|
|
980
1171
|
if (this.asker(turn))
|
|
981
|
-
return;
|
|
1172
|
+
return undefined;
|
|
982
1173
|
const suspendedAt = (r) => r.suspendedAt ?? r.startedAt;
|
|
983
1174
|
const next = turn.session.runs
|
|
984
1175
|
.filter((r) => r.status === "suspended")
|
|
@@ -987,6 +1178,7 @@ export class Runner {
|
|
|
987
1178
|
next.status = "asking";
|
|
988
1179
|
delete next.suspendedAt;
|
|
989
1180
|
}
|
|
1181
|
+
return next;
|
|
990
1182
|
}
|
|
991
1183
|
// ── Settle: the one applier after Speak (design §4.7) ─────────────────
|
|
992
1184
|
async settle(turn, outcome) {
|
|
@@ -1038,19 +1230,28 @@ export class Runner {
|
|
|
1038
1230
|
for (const [field, raw] of Object.entries(spoken.fields))
|
|
1039
1231
|
this.writeField(turn, field, raw);
|
|
1040
1232
|
Object.assign(turn.session.data, spoken.data);
|
|
1041
|
-
|
|
1233
|
+
const fixed = talk.fixedQuestion !== undefined ? { code: "fixed-in-reply" } : {};
|
|
1234
|
+
this.outcome(turn, run, { kind: talkKind(step), status: "ok", key, llmCalls: spoken.llmCalls, stepId: step.id, ...fixed });
|
|
1042
1235
|
if (!turn.session.runs.includes(run))
|
|
1043
1236
|
return;
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1237
|
+
const pending = step.collect?.length ? pendingFields(step, turn.session.data, run.asked) : [];
|
|
1238
|
+
for (const field of pending)
|
|
1239
|
+
run.asked[field] = (run.asked[field] ?? 0) + 1;
|
|
1240
|
+
if (run.staying) {
|
|
1241
|
+
// 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.
|
|
1242
|
+
const maxAsks = step.maxAsks ?? DEFAULT_MAX_ASKS;
|
|
1243
|
+
for (const field of pending) {
|
|
1244
|
+
if (run.asked[field] === maxAsks)
|
|
1245
|
+
this.outcome(turn, run, { kind: "collect", status: "skipped", key, code: "max-asks", detail: field });
|
|
1051
1246
|
}
|
|
1052
|
-
this.
|
|
1247
|
+
this.stayAt(run, step);
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
if (pending.length) {
|
|
1251
|
+
run.status = "asking";
|
|
1252
|
+
return;
|
|
1053
1253
|
}
|
|
1254
|
+
this.reportMaxAsks(turn, run, step);
|
|
1054
1255
|
run.status = "running";
|
|
1055
1256
|
this.follow(turn, run, flow, step, step.then);
|
|
1056
1257
|
if (turn.session.runs.includes(run) && run.status === "running")
|
|
@@ -1087,16 +1288,23 @@ export class Runner {
|
|
|
1087
1288
|
const reset = deferred.resetAtMs;
|
|
1088
1289
|
return new Date(reset !== undefined && reset > ladder ? reset : ladder);
|
|
1089
1290
|
}
|
|
1090
|
-
/** The assistant spoke
|
|
1291
|
+
/** The assistant spoke this turn: its silence wakes replace the ones its last words set. */
|
|
1091
1292
|
armSilence(turn) {
|
|
1293
|
+
const previous = turn.original?.lastAssistantAt;
|
|
1294
|
+
if (turn.session.lastAssistantAt === previous)
|
|
1295
|
+
return;
|
|
1296
|
+
turn.schedule.push(...this.silenceWakes(turn, previous));
|
|
1297
|
+
}
|
|
1298
|
+
/** The assistant spoke last: every silence flow passing `if` and `repeat` gets a wake. */
|
|
1299
|
+
silenceWakes(turn, previous) {
|
|
1092
1300
|
const { session } = turn;
|
|
1093
1301
|
const { lastAssistantAt, lastUserAt } = session;
|
|
1094
|
-
if (!lastAssistantAt
|
|
1095
|
-
return;
|
|
1302
|
+
if (!lastAssistantAt)
|
|
1303
|
+
return [];
|
|
1096
1304
|
if (lastUserAt && Date.parse(lastUserAt) > Date.parse(lastAssistantAt))
|
|
1097
|
-
return;
|
|
1305
|
+
return [];
|
|
1098
1306
|
const ms = Date.parse(lastAssistantAt);
|
|
1099
|
-
const
|
|
1307
|
+
const wakes = [];
|
|
1100
1308
|
for (const flow of this.flows.values()) {
|
|
1101
1309
|
const trigger = (flow.on ?? []).find((t) => "silence" in t);
|
|
1102
1310
|
if (!trigger)
|
|
@@ -1107,12 +1315,13 @@ export class Runner {
|
|
|
1107
1315
|
if (!this.repeatAllows(turn, flow, trigger, key))
|
|
1108
1316
|
continue;
|
|
1109
1317
|
const at = this.snap(turn, new Date(ms + parseDuration(trigger.silence)), trigger.businessHours);
|
|
1110
|
-
|
|
1318
|
+
wakes.push({
|
|
1111
1319
|
key: `silence:${flow.id}:${session.id}:${ms}`,
|
|
1112
1320
|
at,
|
|
1113
1321
|
...(previous ? { replaces: `silence:${flow.id}:${session.id}:${Date.parse(previous)}` } : {}),
|
|
1114
1322
|
});
|
|
1115
1323
|
}
|
|
1324
|
+
return wakes;
|
|
1116
1325
|
}
|
|
1117
1326
|
// ── Return (design §4.8) ──────────────────────────────────────────────
|
|
1118
1327
|
finish(turn) {
|