@falai/agent 4.0.0-alpha.2 → 4.0.0-alpha.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/cjs/core/Agent.d.ts +8 -1
- package/dist/cjs/core/Agent.d.ts.map +1 -1
- package/dist/cjs/core/Agent.js +18 -0
- package/dist/cjs/core/Agent.js.map +1 -1
- package/dist/cjs/core/CompactionEngine.d.ts.map +1 -1
- package/dist/cjs/core/CompactionEngine.js +8 -3
- package/dist/cjs/core/CompactionEngine.js.map +1 -1
- package/dist/cjs/core/FlowSpec.d.ts +21 -2
- package/dist/cjs/core/FlowSpec.d.ts.map +1 -1
- package/dist/cjs/core/FlowSpec.js +324 -51
- package/dist/cjs/core/FlowSpec.js.map +1 -1
- package/dist/cjs/core/Migrate.d.ts.map +1 -1
- package/dist/cjs/core/Migrate.js +3 -1
- package/dist/cjs/core/Migrate.js.map +1 -1
- package/dist/cjs/core/Prompt.d.ts +16 -0
- package/dist/cjs/core/Prompt.d.ts.map +1 -1
- package/dist/cjs/core/Prompt.js +46 -1
- package/dist/cjs/core/Prompt.js.map +1 -1
- package/dist/cjs/core/Runner.d.ts +37 -4
- package/dist/cjs/core/Runner.d.ts.map +1 -1
- package/dist/cjs/core/Runner.js +269 -74
- 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 +50 -19
- package/dist/cjs/core/Speak.js.map +1 -1
- package/dist/cjs/core/Understand.d.ts +4 -3
- package/dist/cjs/core/Understand.d.ts.map +1 -1
- package/dist/cjs/core/Understand.js +22 -51
- package/dist/cjs/core/Understand.js.map +1 -1
- package/dist/cjs/core/contracts.d.ts +11 -6
- package/dist/cjs/core/contracts.d.ts.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/persistence/OpenSearchStore.d.ts +2 -1
- package/dist/cjs/persistence/OpenSearchStore.d.ts.map +1 -1
- package/dist/cjs/persistence/OpenSearchStore.js +2 -2
- package/dist/cjs/persistence/OpenSearchStore.js.map +1 -1
- package/dist/cjs/persistence/RedisStore.d.ts +1 -1
- package/dist/cjs/persistence/RedisStore.d.ts.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.d.ts +2 -5
- package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.js +3 -4
- package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
- package/dist/cjs/providers/DeepSeekProvider.d.ts.map +1 -1
- package/dist/cjs/providers/DeepSeekProvider.js +3 -4
- package/dist/cjs/providers/DeepSeekProvider.js.map +1 -1
- package/dist/cjs/providers/FallbackAiProvider.js +1 -1
- package/dist/cjs/providers/FallbackAiProvider.js.map +1 -1
- package/dist/cjs/providers/GeminiProvider.d.ts +1 -2
- package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/cjs/providers/GeminiProvider.js +3 -4
- package/dist/cjs/providers/GeminiProvider.js.map +1 -1
- package/dist/cjs/providers/GenericOpenAICompatibleProvider.js +4 -4
- package/dist/cjs/providers/GenericOpenAICompatibleProvider.js.map +1 -1
- package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenAIProvider.js +3 -4
- package/dist/cjs/providers/OpenAIProvider.js.map +1 -1
- package/dist/cjs/providers/OpenRouterProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenRouterProvider.js +4 -3
- package/dist/cjs/providers/OpenRouterProvider.js.map +1 -1
- package/dist/cjs/providers/ProviderAdapter.d.ts +15 -10
- package/dist/cjs/providers/ProviderAdapter.d.ts.map +1 -1
- package/dist/cjs/providers/ProviderAdapter.js +8 -3
- package/dist/cjs/providers/ProviderAdapter.js.map +1 -1
- package/dist/cjs/providers/ZaiProvider.js +1 -1
- package/dist/cjs/providers/ZaiProvider.js.map +1 -1
- package/dist/cjs/types/agent.d.ts +19 -4
- package/dist/cjs/types/agent.d.ts.map +1 -1
- package/dist/cjs/types/ai.d.ts +4 -6
- package/dist/cjs/types/ai.d.ts.map +1 -1
- package/dist/cjs/types/compaction.d.ts +1 -0
- package/dist/cjs/types/compaction.d.ts.map +1 -1
- package/dist/cjs/types/errors.d.ts +4 -9
- package/dist/cjs/types/errors.d.ts.map +1 -1
- package/dist/cjs/types/errors.js +12 -12
- package/dist/cjs/types/errors.js.map +1 -1
- package/dist/cjs/types/flow.d.ts +15 -1
- package/dist/cjs/types/flow.d.ts.map +1 -1
- package/dist/cjs/types/history.d.ts +0 -7
- package/dist/cjs/types/history.d.ts.map +1 -1
- package/dist/cjs/types/index.d.ts +2 -2
- package/dist/cjs/types/index.d.ts.map +1 -1
- package/dist/cjs/types/session.d.ts +4 -2
- package/dist/cjs/types/session.d.ts.map +1 -1
- package/dist/cjs/utils/clock.js +1 -1
- package/dist/cjs/utils/clock.js.map +1 -1
- package/dist/cjs/utils/outcomes.d.ts +1 -0
- package/dist/cjs/utils/outcomes.d.ts.map +1 -1
- package/dist/cjs/utils/outcomes.js +1 -0
- package/dist/cjs/utils/outcomes.js.map +1 -1
- package/dist/cjs/utils/phrases.d.ts +25 -0
- package/dist/cjs/utils/phrases.d.ts.map +1 -0
- package/dist/cjs/utils/phrases.js +38 -0
- package/dist/cjs/utils/phrases.js.map +1 -0
- package/dist/cjs/utils/schema.d.ts +3 -12
- package/dist/cjs/utils/schema.d.ts.map +1 -1
- package/dist/cjs/utils/schema.js +3 -43
- package/dist/cjs/utils/schema.js.map +1 -1
- package/dist/cjs/utils/template.d.ts +8 -0
- package/dist/cjs/utils/template.d.ts.map +1 -1
- package/dist/cjs/utils/template.js +47 -3
- package/dist/cjs/utils/template.js.map +1 -1
- package/dist/core/Agent.d.ts +8 -1
- package/dist/core/Agent.d.ts.map +1 -1
- package/dist/core/Agent.js +19 -1
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/CompactionEngine.d.ts.map +1 -1
- package/dist/core/CompactionEngine.js +8 -3
- package/dist/core/CompactionEngine.js.map +1 -1
- package/dist/core/FlowSpec.d.ts +21 -2
- package/dist/core/FlowSpec.d.ts.map +1 -1
- package/dist/core/FlowSpec.js +323 -52
- package/dist/core/FlowSpec.js.map +1 -1
- package/dist/core/Migrate.d.ts.map +1 -1
- package/dist/core/Migrate.js +3 -1
- package/dist/core/Migrate.js.map +1 -1
- package/dist/core/Prompt.d.ts +16 -0
- package/dist/core/Prompt.d.ts.map +1 -1
- package/dist/core/Prompt.js +44 -1
- package/dist/core/Prompt.js.map +1 -1
- package/dist/core/Runner.d.ts +37 -4
- package/dist/core/Runner.d.ts.map +1 -1
- package/dist/core/Runner.js +270 -75
- package/dist/core/Runner.js.map +1 -1
- package/dist/core/Speak.d.ts.map +1 -1
- package/dist/core/Speak.js +51 -20
- package/dist/core/Speak.js.map +1 -1
- package/dist/core/Understand.d.ts +4 -3
- package/dist/core/Understand.d.ts.map +1 -1
- package/dist/core/Understand.js +22 -51
- package/dist/core/Understand.js.map +1 -1
- package/dist/core/contracts.d.ts +11 -6
- package/dist/core/contracts.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/persistence/OpenSearchStore.d.ts +2 -1
- package/dist/persistence/OpenSearchStore.d.ts.map +1 -1
- package/dist/persistence/OpenSearchStore.js +2 -2
- package/dist/persistence/OpenSearchStore.js.map +1 -1
- package/dist/persistence/RedisStore.d.ts +1 -1
- package/dist/persistence/RedisStore.d.ts.map +1 -1
- package/dist/providers/AnthropicProvider.d.ts +2 -5
- package/dist/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/providers/AnthropicProvider.js +3 -4
- package/dist/providers/AnthropicProvider.js.map +1 -1
- package/dist/providers/DeepSeekProvider.d.ts.map +1 -1
- package/dist/providers/DeepSeekProvider.js +3 -4
- package/dist/providers/DeepSeekProvider.js.map +1 -1
- package/dist/providers/FallbackAiProvider.js +1 -1
- package/dist/providers/FallbackAiProvider.js.map +1 -1
- package/dist/providers/GeminiProvider.d.ts +1 -2
- package/dist/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/providers/GeminiProvider.js +3 -4
- package/dist/providers/GeminiProvider.js.map +1 -1
- package/dist/providers/GenericOpenAICompatibleProvider.js +4 -4
- package/dist/providers/GenericOpenAICompatibleProvider.js.map +1 -1
- package/dist/providers/OpenAIProvider.d.ts.map +1 -1
- package/dist/providers/OpenAIProvider.js +3 -4
- package/dist/providers/OpenAIProvider.js.map +1 -1
- package/dist/providers/OpenRouterProvider.d.ts.map +1 -1
- package/dist/providers/OpenRouterProvider.js +4 -3
- package/dist/providers/OpenRouterProvider.js.map +1 -1
- package/dist/providers/ProviderAdapter.d.ts +15 -10
- package/dist/providers/ProviderAdapter.d.ts.map +1 -1
- package/dist/providers/ProviderAdapter.js +8 -3
- package/dist/providers/ProviderAdapter.js.map +1 -1
- package/dist/providers/ZaiProvider.js +1 -1
- package/dist/providers/ZaiProvider.js.map +1 -1
- package/dist/types/agent.d.ts +19 -4
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/ai.d.ts +4 -6
- package/dist/types/ai.d.ts.map +1 -1
- package/dist/types/compaction.d.ts +1 -0
- package/dist/types/compaction.d.ts.map +1 -1
- package/dist/types/errors.d.ts +4 -9
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/errors.js +12 -12
- package/dist/types/errors.js.map +1 -1
- package/dist/types/flow.d.ts +15 -1
- package/dist/types/flow.d.ts.map +1 -1
- package/dist/types/history.d.ts +0 -7
- package/dist/types/history.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/session.d.ts +4 -2
- package/dist/types/session.d.ts.map +1 -1
- package/dist/utils/clock.js +1 -1
- package/dist/utils/clock.js.map +1 -1
- package/dist/utils/outcomes.d.ts +1 -0
- package/dist/utils/outcomes.d.ts.map +1 -1
- package/dist/utils/outcomes.js +1 -0
- package/dist/utils/outcomes.js.map +1 -1
- package/dist/utils/phrases.d.ts +25 -0
- package/dist/utils/phrases.d.ts.map +1 -0
- package/dist/utils/phrases.js +35 -0
- package/dist/utils/phrases.js.map +1 -0
- package/dist/utils/schema.d.ts +3 -12
- package/dist/utils/schema.d.ts.map +1 -1
- package/dist/utils/schema.js +3 -42
- package/dist/utils/schema.js.map +1 -1
- package/dist/utils/template.d.ts +8 -0
- package/dist/utils/template.d.ts.map +1 -1
- package/dist/utils/template.js +47 -3
- package/dist/utils/template.js.map +1 -1
- package/docs/concepts/architecture.md +3 -3
- package/docs/concepts/collection.md +40 -5
- package/docs/concepts/pipeline.md +12 -10
- package/docs/concepts/runs-and-waits.md +2 -2
- package/docs/guides/actions-and-events.md +2 -2
- package/docs/guides/branching.md +4 -2
- package/docs/guides/compaction.md +2 -2
- package/docs/guides/conditions.md +1 -1
- package/docs/guides/error-handling.md +3 -1
- package/docs/guides/flow-control.md +4 -2
- package/docs/guides/persistence.md +2 -2
- package/docs/guides/testing.md +1 -1
- package/docs/guides/triggers.md +5 -5
- package/docs/migration/v3-to-v4.md +16 -11
- package/docs/reference/actions-events-conditions.md +2 -2
- package/docs/reference/agent.md +11 -7
- package/docs/reference/branches.md +1 -1
- package/docs/reference/errors.md +10 -7
- package/docs/reference/fields.md +5 -3
- package/docs/reference/flow-spec.md +36 -9
- package/docs/reference/flow.md +8 -3
- package/docs/reference/outcomes.md +3 -2
- package/docs/reference/providers.md +2 -0
- package/docs/reference/session.md +2 -0
- package/docs/reference/step.md +8 -5
- package/docs/reference/stores.md +5 -3
- package/docs/reference/trigger.md +24 -4
- package/docs/rfc/v4-one-flow.md +7 -5
- package/docs/start/01-install.md +5 -3
- package/docs/start/04-add-tools.md +1 -1
- package/docs/start/05-go-to-production.md +18 -1
- package/examples/05-branches.ts +1 -1
- package/examples/06-triggers-and-waits.ts +4 -3
- package/package.json +5 -3
- package/src/core/Agent.ts +23 -2
- package/src/core/CompactionEngine.ts +10 -3
- package/src/core/FlowSpec.ts +352 -60
- package/src/core/Migrate.ts +2 -1
- package/src/core/Prompt.ts +47 -1
- package/src/core/Runner.ts +266 -71
- package/src/core/Speak.ts +53 -19
- package/src/core/Understand.ts +17 -50
- package/src/core/contracts.ts +11 -4
- package/src/index.ts +1 -1
- package/src/persistence/OpenSearchStore.ts +3 -2
- package/src/persistence/RedisStore.ts +1 -1
- package/src/providers/AnthropicProvider.ts +4 -9
- package/src/providers/DeepSeekProvider.ts +2 -4
- package/src/providers/FallbackAiProvider.ts +1 -1
- package/src/providers/GeminiProvider.ts +3 -6
- package/src/providers/GenericOpenAICompatibleProvider.ts +4 -4
- package/src/providers/OpenAIProvider.ts +3 -4
- package/src/providers/OpenRouterProvider.ts +4 -2
- package/src/providers/ProviderAdapter.ts +18 -13
- package/src/providers/ZaiProvider.ts +1 -1
- package/src/types/agent.ts +20 -5
- package/src/types/ai.ts +4 -6
- package/src/types/compaction.ts +1 -0
- package/src/types/errors.ts +11 -12
- package/src/types/flow.ts +15 -1
- package/src/types/history.ts +0 -10
- package/src/types/index.ts +1 -1
- package/src/types/session.ts +4 -1
- package/src/utils/clock.ts +1 -1
- package/src/utils/outcomes.ts +1 -0
- package/src/utils/phrases.ts +40 -0
- package/src/utils/schema.ts +3 -48
- package/src/utils/template.ts +46 -3
- package/dist/cjs/providers/index.d.ts +0 -26
- package/dist/cjs/providers/index.d.ts.map +0 -1
- package/dist/cjs/providers/index.js +0 -30
- package/dist/cjs/providers/index.js.map +0 -1
- package/dist/cjs/utils/clone.d.ts +0 -8
- package/dist/cjs/utils/clone.d.ts.map +0 -1
- package/dist/cjs/utils/clone.js +0 -32
- package/dist/cjs/utils/clone.js.map +0 -1
- package/dist/cjs/utils/index.d.ts +0 -9
- package/dist/cjs/utils/index.d.ts.map +0 -1
- package/dist/cjs/utils/index.js +0 -29
- package/dist/cjs/utils/index.js.map +0 -1
- package/dist/providers/index.d.ts +0 -26
- package/dist/providers/index.d.ts.map +0 -1
- package/dist/providers/index.js +0 -17
- package/dist/providers/index.js.map +0 -1
- package/dist/utils/clone.d.ts +0 -8
- package/dist/utils/clone.d.ts.map +0 -1
- package/dist/utils/clone.js +0 -29
- package/dist/utils/clone.js.map +0 -1
- package/dist/utils/index.d.ts +0 -9
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -9
- package/dist/utils/index.js.map +0 -1
- package/src/providers/index.ts +0 -38
- package/src/utils/clone.ts +0 -34
- package/src/utils/index.ts +0 -18
package/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) {
|
|
@@ -436,8 +465,7 @@ export class Runner {
|
|
|
436
465
|
const floorRun = this.floorRun(turn);
|
|
437
466
|
const floorFlow = floorRun && this.flows.get(floorRun.flowId);
|
|
438
467
|
const eligible = this.eligibleMessageFlows(turn);
|
|
439
|
-
|
|
440
|
-
const messageFlows = !floorRun && eligible.length === 1 ? [] : eligible;
|
|
468
|
+
const messageFlows = !floorRun && this.startsUnscored(turn, eligible) ? [] : eligible;
|
|
441
469
|
const mentionFlows = this.mentionFlows()
|
|
442
470
|
.filter(({ flow, trigger }) => trigger.mention.length > 0 && this.repeatAllows(turn, flow, trigger, turn.triggerKey))
|
|
443
471
|
.map(({ flow }) => flow);
|
|
@@ -451,18 +479,28 @@ export class Runner {
|
|
|
451
479
|
}
|
|
452
480
|
const fields = {};
|
|
453
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
|
+
};
|
|
454
486
|
for (const flow of [...(floorFlow ? [floorFlow] : []), ...eligible]) {
|
|
455
|
-
for (const
|
|
456
|
-
|
|
457
|
-
continue;
|
|
458
|
-
for (const field of step.collect ?? []) {
|
|
459
|
-
const def = this.options.fields[field];
|
|
460
|
-
if (def && !isKnown(data[field]) && extractMode(def) === "anywhere")
|
|
461
|
-
fields[field] = def;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
487
|
+
for (const field of flowFields(flow).filter(harvestable))
|
|
488
|
+
fields[field] = this.options.fields[field];
|
|
464
489
|
}
|
|
465
|
-
|
|
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)
|
|
466
504
|
return null;
|
|
467
505
|
return {
|
|
468
506
|
text: turn.what.text,
|
|
@@ -491,6 +529,13 @@ export class Runner {
|
|
|
491
529
|
eligibleMessageFlows(turn) {
|
|
492
530
|
return this.messageFlows(turn, (list) => list.length > 0);
|
|
493
531
|
}
|
|
532
|
+
/**
|
|
533
|
+
* The lone eligible flow starts without a score only when a low score would have nowhere
|
|
534
|
+
* else to send the message: no catch-all passes and the idle speaker is silent (S9).
|
|
535
|
+
*/
|
|
536
|
+
startsUnscored(turn, eligible) {
|
|
537
|
+
return eligible.length === 1 && this.options.idle === "silent" && this.messageFlows(turn, (list) => list.length === 0).length === 0;
|
|
538
|
+
}
|
|
494
539
|
messageFlows(turn, accept) {
|
|
495
540
|
const out = [];
|
|
496
541
|
for (const flow of this.flows.values()) {
|
|
@@ -535,7 +580,7 @@ export class Runner {
|
|
|
535
580
|
if (top && top.score >= current + ROUTE_STICKY && top.score >= ROUTE_MIN)
|
|
536
581
|
route = top.flow;
|
|
537
582
|
}
|
|
538
|
-
else if (
|
|
583
|
+
else if (this.startsUnscored(turn, eligible)) {
|
|
539
584
|
route = eligible[0];
|
|
540
585
|
}
|
|
541
586
|
else {
|
|
@@ -564,6 +609,9 @@ export class Runner {
|
|
|
564
609
|
turn.floorRunId = run.id;
|
|
565
610
|
}
|
|
566
611
|
}
|
|
612
|
+
// Unless routing or Ingest moved it, the message is the asker's: written down, so a chain beside it cannot take it.
|
|
613
|
+
if (asker && !route && !turn.floorFromIngest)
|
|
614
|
+
turn.floorRunId = asker.id;
|
|
567
615
|
for (const [field, raw] of Object.entries(understanding?.fields ?? {}))
|
|
568
616
|
this.writeField(turn, field, raw);
|
|
569
617
|
if (asker && turn.session.runs.includes(asker) && asker.status === "asking") {
|
|
@@ -574,7 +622,13 @@ export class Runner {
|
|
|
574
622
|
}
|
|
575
623
|
}
|
|
576
624
|
fireBranch(turn, run, flow, step, understanding) {
|
|
577
|
-
|
|
625
|
+
// Staying, a run does not take an `if` branch back onto a path it has already run: the fact it tests is still true,
|
|
626
|
+
// so it would re-run that path on every message. A `when` branch is judged on each new message and always counts.
|
|
627
|
+
// 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`.
|
|
628
|
+
const ran = (then) => typeof then === "string" ? then === "end" || (run.visits[then] ?? 0) > 0 : "step" in then && (run.visits[then.step] ?? 0) > 0;
|
|
629
|
+
const hit = (branch, index) => "if" in branch
|
|
630
|
+
? !(run.staying && ran(branch.then)) && this.holds(branch.if, turn, run)
|
|
631
|
+
: understanding?.branches[`${run.id}/${step.id}/${index}`] === true;
|
|
578
632
|
const index = (step.branches ?? []).findIndex(hit);
|
|
579
633
|
if (index < 0)
|
|
580
634
|
return;
|
|
@@ -604,10 +658,47 @@ export class Runner {
|
|
|
604
658
|
async advance(turn) {
|
|
605
659
|
if (turn.ignored)
|
|
606
660
|
return null;
|
|
607
|
-
this.
|
|
661
|
+
this.resume(turn);
|
|
608
662
|
turn.queue = [...turn.session.runs];
|
|
609
663
|
await this.drain(turn);
|
|
610
|
-
|
|
664
|
+
// An asker that moved on without a word hands the message to the run it suspended, and that one to the next.
|
|
665
|
+
// Each pass takes a run off the suspended stack, so the stack's size bounds the loop.
|
|
666
|
+
for (let left = turn.session.runs.length; left > 0; left--) {
|
|
667
|
+
if (turn.what.kind !== "message" || turn.silenced !== undefined || turn.talk || turn.spokeBy.size > 0)
|
|
668
|
+
break;
|
|
669
|
+
const resumed = this.resume(turn);
|
|
670
|
+
if (!resumed)
|
|
671
|
+
break;
|
|
672
|
+
turn.queue.push(resumed);
|
|
673
|
+
await this.drain(turn);
|
|
674
|
+
}
|
|
675
|
+
const speaker = this.speaker(turn);
|
|
676
|
+
if (speaker && !("idle" in speaker) && this.askFixed(turn, speaker)) {
|
|
677
|
+
turn.talk = undefined;
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
return speaker;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Send the step's fixed question when this is its first ask: every field it collects still unknown, none asked yet,
|
|
684
|
+
* and the run not staying (a staying run answers the lead). The question counts as one ask of each field. False when
|
|
685
|
+
* the AI should phrase the ask instead.
|
|
686
|
+
*/
|
|
687
|
+
askFixed(turn, { run, step, pending }) {
|
|
688
|
+
const { question, collect = [] } = step;
|
|
689
|
+
if (question === undefined || run.staying)
|
|
690
|
+
return false;
|
|
691
|
+
if (pending.length !== collect.length || pending.some((field) => (run.asked[field] ?? 0) > 0))
|
|
692
|
+
return false;
|
|
693
|
+
const key = this.stepKey(run, step);
|
|
694
|
+
turn.messages.push({ text: render(question, this.scope(turn, run)), kind: "verbatim", afterMs: turn.afterMs, key, runId: run.id, stepId: step.id });
|
|
695
|
+
turn.afterMs = 0;
|
|
696
|
+
turn.spokeBy.add(run.id);
|
|
697
|
+
turn.spoke = true;
|
|
698
|
+
for (const field of pending)
|
|
699
|
+
run.asked[field] = (run.asked[field] ?? 0) + 1;
|
|
700
|
+
this.outcome(turn, run, { kind: "collect", status: "ok", key, code: "asked-fixed", stepId: step.id });
|
|
701
|
+
return true;
|
|
611
702
|
}
|
|
612
703
|
/**
|
|
613
704
|
* Everything Speak needs for this turn's one speaker. Instructions are agent,
|
|
@@ -630,12 +721,16 @@ export class Runner {
|
|
|
630
721
|
const instructions = [...(this.options.instructions ?? []), ...own.instructions].filter((ins) => !ins.if || evaluate(ins.if, ctx, conditions));
|
|
631
722
|
const all = this.options.tools ?? [];
|
|
632
723
|
const allowed = own.tools;
|
|
724
|
+
// deferTalk leaves a `deferred` outcome as the step's last line; the retry wake re-runs the step without adding one.
|
|
725
|
+
const last = "idle" in talk ? undefined : talk.run.outcomes.at(-1);
|
|
726
|
+
const retry = !("idle" in talk) && last?.status === "deferred" && last.stepId === talk.step.id;
|
|
633
727
|
return {
|
|
634
728
|
talk,
|
|
635
|
-
input: turn.what.kind === "message" ? { kind: turn.kind, text: turn.what.text } : { kind: turn.kind },
|
|
729
|
+
input: turn.what.kind === "message" ? { kind: turn.kind, text: turn.what.text } : { kind: turn.kind, ...(retry ? { retry } : {}) },
|
|
636
730
|
context: turn.context,
|
|
637
731
|
data: turn.session.data,
|
|
638
732
|
history: this.historyOf(turn),
|
|
733
|
+
said: [...turn.messages],
|
|
639
734
|
now: turn.now,
|
|
640
735
|
instructions,
|
|
641
736
|
tools: allowed ? all.filter((tool) => allowed.includes(tool.id)) : all,
|
|
@@ -678,6 +773,11 @@ export class Runner {
|
|
|
678
773
|
}
|
|
679
774
|
if (!this.premiseHolds(turn, run, flow))
|
|
680
775
|
return;
|
|
776
|
+
// The flow was edited off 'stay' after this run finished its steps: its new `onEnd` decides now.
|
|
777
|
+
if (run.staying && flow.onEnd !== "stay") {
|
|
778
|
+
this.finishFlow(turn, run, flow);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
681
781
|
const resuming = run.status === "asking";
|
|
682
782
|
run.status = "running";
|
|
683
783
|
if (run.stepId === null) {
|
|
@@ -720,6 +820,11 @@ export class Runner {
|
|
|
720
820
|
this.endRun(turn, run, "skipped", "premise-changed");
|
|
721
821
|
return ok;
|
|
722
822
|
}
|
|
823
|
+
/** `silenced: { skip: true }`: the message this step would send is dropped, and the run takes `then` as if it had gone out. */
|
|
824
|
+
skipSilenced(turn, run, flow, step, kind) {
|
|
825
|
+
this.outcome(turn, run, { kind, status: "skipped", key: this.stepKey(run, step), code: "silenced", detail: turn.silenced, next: nextLabel(step.then) });
|
|
826
|
+
this.follow(turn, run, flow, step, step.then);
|
|
827
|
+
}
|
|
723
828
|
async execute(turn, run, flow, step, resuming) {
|
|
724
829
|
const data = turn.session.data;
|
|
725
830
|
const key = this.stepKey(run, step);
|
|
@@ -728,7 +833,9 @@ export class Runner {
|
|
|
728
833
|
let pending = [];
|
|
729
834
|
if (step.collect?.length) {
|
|
730
835
|
pending = pendingFields(step, data, run.asked);
|
|
731
|
-
|
|
836
|
+
// A `stay` run answers even with nothing left to collect: answering is what it stays for. It only runs on a message
|
|
837
|
+
// (asking runs sit out wakes and events), and on a silenced one the check below keeps it asking.
|
|
838
|
+
if (!pending.length && !run.staying) {
|
|
732
839
|
this.reportMaxAsks(turn, run, step);
|
|
733
840
|
this.outcome(turn, run, resuming
|
|
734
841
|
? { kind, status: "ok", key, next: nextLabel(step.then) }
|
|
@@ -740,6 +847,8 @@ export class Runner {
|
|
|
740
847
|
if (turn.silenced !== undefined) {
|
|
741
848
|
if (resuming)
|
|
742
849
|
run.status = "asking"; // the gate is closed; it speaks when the gate opens
|
|
850
|
+
else if (turn.skipSilenced)
|
|
851
|
+
this.skipSilenced(turn, run, flow, step, kind);
|
|
743
852
|
else
|
|
744
853
|
this.endRun(turn, run, "skipped", "silenced", kind, turn.silenced);
|
|
745
854
|
return;
|
|
@@ -751,13 +860,19 @@ export class Runner {
|
|
|
751
860
|
}
|
|
752
861
|
}
|
|
753
862
|
run.status = "asking";
|
|
863
|
+
// 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.
|
|
754
864
|
if (!turn.speakDone)
|
|
755
865
|
turn.talk = { run, flow, step, pending };
|
|
866
|
+
else
|
|
867
|
+
this.askFixed(turn, { run, flow, step, pending });
|
|
756
868
|
return;
|
|
757
869
|
}
|
|
758
870
|
if (isSay(step)) {
|
|
759
871
|
if (turn.silenced !== undefined) {
|
|
760
|
-
|
|
872
|
+
if (turn.skipSilenced)
|
|
873
|
+
this.skipSilenced(turn, run, flow, step, "say");
|
|
874
|
+
else
|
|
875
|
+
this.endRun(turn, run, "skipped", "silenced", "say", turn.silenced);
|
|
761
876
|
return;
|
|
762
877
|
}
|
|
763
878
|
if (step.once) {
|
|
@@ -804,6 +919,13 @@ export class Runner {
|
|
|
804
919
|
result = { failed: error instanceof Error ? error.message : String(error) };
|
|
805
920
|
}
|
|
806
921
|
}
|
|
922
|
+
// The handler already ran, so a defer that cannot be parsed must not throw
|
|
923
|
+
// the turn: every replay would repeat the side effect and fail again.
|
|
924
|
+
if ("defer" in result && !isDuration(String(result.defer))) {
|
|
925
|
+
result = {
|
|
926
|
+
failed: `action "${step.do}" asked to defer by "${String(result.defer)}", which is not a duration. Use a value like "2m" or "1h".`,
|
|
927
|
+
};
|
|
928
|
+
}
|
|
807
929
|
if ("ok" in result) {
|
|
808
930
|
if (result.spoke) {
|
|
809
931
|
turn.spokeBy.add(run.id);
|
|
@@ -895,18 +1017,21 @@ export class Runner {
|
|
|
895
1017
|
}
|
|
896
1018
|
if ("step" in target) {
|
|
897
1019
|
const data = turn.session.data;
|
|
898
|
-
|
|
1020
|
+
// Forgetting a field means asking for it from scratch: its ask count goes too, so a fixed question goes out again.
|
|
1021
|
+
for (const field of target.clear ?? []) {
|
|
899
1022
|
delete data[field];
|
|
1023
|
+
delete run.asked[field];
|
|
1024
|
+
}
|
|
900
1025
|
this.jump(turn, run, flow, target.step);
|
|
901
1026
|
return;
|
|
902
1027
|
}
|
|
903
1028
|
const childId = render(target.flow, this.scope(turn, run));
|
|
904
1029
|
const key = this.stepKey(run, step);
|
|
905
1030
|
this.endRun(turn, run, "flow");
|
|
906
|
-
this.chain(turn, childId, key, target.input ?? run.input, run.hop + 1);
|
|
1031
|
+
this.chain(turn, run, childId, key, target.input ?? run.input, run.hop + 1);
|
|
907
1032
|
}
|
|
908
|
-
/** Start a child flow that
|
|
909
|
-
chain(turn, flowId, key, input, hop, keepData = false) {
|
|
1033
|
+
/** Start a child flow that moves in this same phase. It inherits the floor when its parent held it, or when nobody did. */
|
|
1034
|
+
chain(turn, parent, flowId, key, input, hop, keepData = false) {
|
|
910
1035
|
const child = this.flows.get(flowId);
|
|
911
1036
|
if (!child) {
|
|
912
1037
|
turn.skipped.push({ flowId, anchor: turn.session.id, triggerKey: key, code: "flow-gone", message: OUTCOME_MESSAGES["flow-gone"] });
|
|
@@ -915,9 +1040,11 @@ export class Runner {
|
|
|
915
1040
|
const run = this.startRun(turn, child, "flow", key, { payload: input, hop, keepData });
|
|
916
1041
|
if (!run)
|
|
917
1042
|
return;
|
|
918
|
-
turn.floorRunId
|
|
919
|
-
|
|
920
|
-
turn.
|
|
1043
|
+
if (turn.floorRunId === undefined || turn.floorRunId === parent.id) {
|
|
1044
|
+
turn.floorRunId = run.id;
|
|
1045
|
+
if (turn.ingesting)
|
|
1046
|
+
turn.floorFromIngest = true;
|
|
1047
|
+
}
|
|
921
1048
|
turn.queue.push(run);
|
|
922
1049
|
}
|
|
923
1050
|
jump(turn, run, flow, stepId) {
|
|
@@ -932,14 +1059,54 @@ export class Runner {
|
|
|
932
1059
|
run.visits[stepId] = (run.visits[stepId] ?? 0) + 1;
|
|
933
1060
|
run.status = "running";
|
|
934
1061
|
delete run.waiting;
|
|
1062
|
+
delete run.staying;
|
|
1063
|
+
}
|
|
1064
|
+
/** `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. */
|
|
1065
|
+
stayAt(run, step) {
|
|
1066
|
+
this.enter(run, step.id);
|
|
1067
|
+
run.staying = true;
|
|
1068
|
+
run.status = "asking";
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* 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.
|
|
1072
|
+
* 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
|
|
1073
|
+
* stays on the flow's last talk step. Upgrade: record the talk step on the run when it talks.
|
|
1074
|
+
*/
|
|
1075
|
+
stayStep(run, flow) {
|
|
1076
|
+
for (let i = run.outcomes.length - 1; i >= 0; i--) {
|
|
1077
|
+
const { kind, stepId } = run.outcomes[i];
|
|
1078
|
+
const step = kind === "prompt" || kind === "collect" ? this.stepOf(flow, stepId ?? null) : undefined;
|
|
1079
|
+
if (step && isTalk(step))
|
|
1080
|
+
return step;
|
|
1081
|
+
}
|
|
1082
|
+
return [...flow.steps].reverse().find(isTalk);
|
|
935
1083
|
}
|
|
936
1084
|
finishFlow(turn, run, flow) {
|
|
937
1085
|
const onEnd = flow.onEnd ?? "end";
|
|
938
1086
|
const last = flow.steps[flow.steps.length - 1];
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
run
|
|
1087
|
+
const stay = onEnd === "stay" ? this.stayStep(run, flow) : undefined;
|
|
1088
|
+
if (stay) {
|
|
1089
|
+
// The steps after it ran once, on the way here; staying, it only answers.
|
|
1090
|
+
this.stayAt(run, stay);
|
|
1091
|
+
// One asker at a time. A run the lead's message went to holds the conversation and suspends the other asker;
|
|
1092
|
+
// any other run waits behind that asker and resumes when it is done.
|
|
1093
|
+
const others = turn.session.runs.filter((r) => r !== run && r.status === "asking");
|
|
1094
|
+
if (others.length && !(turn.what.kind === "message" && turn.floorRunId === run.id)) {
|
|
1095
|
+
run.status = "suspended";
|
|
1096
|
+
run.suspendedAt = turn.nowIso;
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
for (const other of others) {
|
|
1100
|
+
other.status = "suspended";
|
|
1101
|
+
other.suspendedAt = turn.nowIso;
|
|
1102
|
+
if (turn.talk?.run === other)
|
|
1103
|
+
turn.talk = undefined;
|
|
1104
|
+
}
|
|
1105
|
+
// A message nothing has answered yet gets its answer now. Reached in Ingest, the run stays asking: Decide judges its
|
|
1106
|
+
// branches as the asker's, and it answers when it moves.
|
|
1107
|
+
if (!turn.ingesting && turn.what.kind === "message" && turn.silenced === undefined && !turn.speakDone && turn.spokeBy.size === 0) {
|
|
1108
|
+
run.status = "running";
|
|
1109
|
+
}
|
|
943
1110
|
return;
|
|
944
1111
|
}
|
|
945
1112
|
if (onEnd === "reset" && last) {
|
|
@@ -947,7 +1114,7 @@ export class Runner {
|
|
|
947
1114
|
// 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.
|
|
948
1115
|
const key = this.stepKey(run, last);
|
|
949
1116
|
this.endRun(turn, run, "reset");
|
|
950
|
-
this.chain(turn, flow.id, key, run.input, run.hop + 1, true);
|
|
1117
|
+
this.chain(turn, run, flow.id, key, run.input, run.hop + 1, true);
|
|
951
1118
|
return;
|
|
952
1119
|
}
|
|
953
1120
|
this.endRun(turn, run, "end");
|
|
@@ -969,10 +1136,21 @@ export class Runner {
|
|
|
969
1136
|
run.waiting = { ...waiting, until: at.toISOString(), setAt: turn.nowIso };
|
|
970
1137
|
turn.schedule.push({ key: waiting.key, at });
|
|
971
1138
|
}
|
|
1139
|
+
/** `resumeSuspended`, and on a message the resumed run's `if` branches are judged before it speaks, as the asker's are in Decide. */
|
|
1140
|
+
resume(turn) {
|
|
1141
|
+
const run = this.resumeSuspended(turn);
|
|
1142
|
+
if (run && turn.what.kind === "message") {
|
|
1143
|
+
const flow = this.flows.get(run.flowId);
|
|
1144
|
+
const step = flow && this.stepOf(flow, run.stepId);
|
|
1145
|
+
if (flow && step && isTalk(step))
|
|
1146
|
+
this.fireBranch(turn, run, flow, step, null);
|
|
1147
|
+
}
|
|
1148
|
+
return run;
|
|
1149
|
+
}
|
|
972
1150
|
/** When nobody asks, the most recently suspended run returns to asking. */
|
|
973
1151
|
resumeSuspended(turn) {
|
|
974
1152
|
if (this.asker(turn))
|
|
975
|
-
return;
|
|
1153
|
+
return undefined;
|
|
976
1154
|
const suspendedAt = (r) => r.suspendedAt ?? r.startedAt;
|
|
977
1155
|
const next = turn.session.runs
|
|
978
1156
|
.filter((r) => r.status === "suspended")
|
|
@@ -981,6 +1159,7 @@ export class Runner {
|
|
|
981
1159
|
next.status = "asking";
|
|
982
1160
|
delete next.suspendedAt;
|
|
983
1161
|
}
|
|
1162
|
+
return next;
|
|
984
1163
|
}
|
|
985
1164
|
// ── Settle: the one applier after Speak (design §4.7) ─────────────────
|
|
986
1165
|
async settle(turn, outcome) {
|
|
@@ -1035,16 +1214,24 @@ export class Runner {
|
|
|
1035
1214
|
this.outcome(turn, run, { kind: talkKind(step), status: "ok", key, llmCalls: spoken.llmCalls, stepId: step.id });
|
|
1036
1215
|
if (!turn.session.runs.includes(run))
|
|
1037
1216
|
return;
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1217
|
+
const pending = step.collect?.length ? pendingFields(step, turn.session.data, run.asked) : [];
|
|
1218
|
+
for (const field of pending)
|
|
1219
|
+
run.asked[field] = (run.asked[field] ?? 0) + 1;
|
|
1220
|
+
if (run.staying) {
|
|
1221
|
+
// 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.
|
|
1222
|
+
const maxAsks = step.maxAsks ?? DEFAULT_MAX_ASKS;
|
|
1223
|
+
for (const field of pending) {
|
|
1224
|
+
if (run.asked[field] === maxAsks)
|
|
1225
|
+
this.outcome(turn, run, { kind: "collect", status: "skipped", key, code: "max-asks", detail: field });
|
|
1045
1226
|
}
|
|
1046
|
-
this.
|
|
1227
|
+
this.stayAt(run, step);
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
if (pending.length) {
|
|
1231
|
+
run.status = "asking";
|
|
1232
|
+
return;
|
|
1047
1233
|
}
|
|
1234
|
+
this.reportMaxAsks(turn, run, step);
|
|
1048
1235
|
run.status = "running";
|
|
1049
1236
|
this.follow(turn, run, flow, step, step.then);
|
|
1050
1237
|
if (turn.session.runs.includes(run) && run.status === "running")
|
|
@@ -1081,16 +1268,23 @@ export class Runner {
|
|
|
1081
1268
|
const reset = deferred.resetAtMs;
|
|
1082
1269
|
return new Date(reset !== undefined && reset > ladder ? reset : ladder);
|
|
1083
1270
|
}
|
|
1084
|
-
/** The assistant spoke
|
|
1271
|
+
/** The assistant spoke this turn: its silence wakes replace the ones its last words set. */
|
|
1085
1272
|
armSilence(turn) {
|
|
1273
|
+
const previous = turn.original?.lastAssistantAt;
|
|
1274
|
+
if (turn.session.lastAssistantAt === previous)
|
|
1275
|
+
return;
|
|
1276
|
+
turn.schedule.push(...this.silenceWakes(turn, previous));
|
|
1277
|
+
}
|
|
1278
|
+
/** The assistant spoke last: every silence flow passing `if` and `repeat` gets a wake. */
|
|
1279
|
+
silenceWakes(turn, previous) {
|
|
1086
1280
|
const { session } = turn;
|
|
1087
1281
|
const { lastAssistantAt, lastUserAt } = session;
|
|
1088
|
-
if (!lastAssistantAt
|
|
1089
|
-
return;
|
|
1282
|
+
if (!lastAssistantAt)
|
|
1283
|
+
return [];
|
|
1090
1284
|
if (lastUserAt && Date.parse(lastUserAt) > Date.parse(lastAssistantAt))
|
|
1091
|
-
return;
|
|
1285
|
+
return [];
|
|
1092
1286
|
const ms = Date.parse(lastAssistantAt);
|
|
1093
|
-
const
|
|
1287
|
+
const wakes = [];
|
|
1094
1288
|
for (const flow of this.flows.values()) {
|
|
1095
1289
|
const trigger = (flow.on ?? []).find((t) => "silence" in t);
|
|
1096
1290
|
if (!trigger)
|
|
@@ -1101,12 +1295,13 @@ export class Runner {
|
|
|
1101
1295
|
if (!this.repeatAllows(turn, flow, trigger, key))
|
|
1102
1296
|
continue;
|
|
1103
1297
|
const at = this.snap(turn, new Date(ms + parseDuration(trigger.silence)), trigger.businessHours);
|
|
1104
|
-
|
|
1298
|
+
wakes.push({
|
|
1105
1299
|
key: `silence:${flow.id}:${session.id}:${ms}`,
|
|
1106
1300
|
at,
|
|
1107
1301
|
...(previous ? { replaces: `silence:${flow.id}:${session.id}:${Date.parse(previous)}` } : {}),
|
|
1108
1302
|
});
|
|
1109
1303
|
}
|
|
1304
|
+
return wakes;
|
|
1110
1305
|
}
|
|
1111
1306
|
// ── Return (design §4.8) ──────────────────────────────────────────────
|
|
1112
1307
|
finish(turn) {
|