@falai/agent 4.0.0-alpha.2 → 4.0.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/cjs/core/Agent.d.ts +8 -1
- package/dist/cjs/core/Agent.d.ts.map +1 -1
- package/dist/cjs/core/Agent.js +18 -0
- package/dist/cjs/core/Agent.js.map +1 -1
- package/dist/cjs/core/CompactionEngine.d.ts.map +1 -1
- package/dist/cjs/core/CompactionEngine.js +8 -3
- package/dist/cjs/core/CompactionEngine.js.map +1 -1
- package/dist/cjs/core/FlowSpec.d.ts +21 -2
- package/dist/cjs/core/FlowSpec.d.ts.map +1 -1
- package/dist/cjs/core/FlowSpec.js +324 -51
- package/dist/cjs/core/FlowSpec.js.map +1 -1
- package/dist/cjs/core/Migrate.d.ts.map +1 -1
- package/dist/cjs/core/Migrate.js +3 -1
- package/dist/cjs/core/Migrate.js.map +1 -1
- package/dist/cjs/core/Prompt.d.ts +16 -0
- package/dist/cjs/core/Prompt.d.ts.map +1 -1
- package/dist/cjs/core/Prompt.js +46 -1
- package/dist/cjs/core/Prompt.js.map +1 -1
- package/dist/cjs/core/Runner.d.ts +42 -4
- package/dist/cjs/core/Runner.d.ts.map +1 -1
- package/dist/cjs/core/Runner.js +292 -72
- package/dist/cjs/core/Runner.js.map +1 -1
- package/dist/cjs/core/Speak.d.ts.map +1 -1
- package/dist/cjs/core/Speak.js +60 -21
- package/dist/cjs/core/Speak.js.map +1 -1
- package/dist/cjs/core/Understand.d.ts +6 -3
- package/dist/cjs/core/Understand.d.ts.map +1 -1
- package/dist/cjs/core/Understand.js +34 -55
- package/dist/cjs/core/Understand.js.map +1 -1
- package/dist/cjs/core/contracts.d.ts +21 -6
- package/dist/cjs/core/contracts.d.ts.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/persistence/OpenSearchStore.d.ts +2 -1
- package/dist/cjs/persistence/OpenSearchStore.d.ts.map +1 -1
- package/dist/cjs/persistence/OpenSearchStore.js +2 -2
- package/dist/cjs/persistence/OpenSearchStore.js.map +1 -1
- package/dist/cjs/persistence/RedisStore.d.ts +1 -1
- package/dist/cjs/persistence/RedisStore.d.ts.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.d.ts +2 -5
- package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.js +3 -4
- package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
- package/dist/cjs/providers/DeepSeekProvider.d.ts.map +1 -1
- package/dist/cjs/providers/DeepSeekProvider.js +3 -4
- package/dist/cjs/providers/DeepSeekProvider.js.map +1 -1
- package/dist/cjs/providers/FallbackAiProvider.js +1 -1
- package/dist/cjs/providers/FallbackAiProvider.js.map +1 -1
- package/dist/cjs/providers/GeminiProvider.d.ts +1 -2
- package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/cjs/providers/GeminiProvider.js +3 -4
- package/dist/cjs/providers/GeminiProvider.js.map +1 -1
- package/dist/cjs/providers/GenericOpenAICompatibleProvider.js +4 -4
- package/dist/cjs/providers/GenericOpenAICompatibleProvider.js.map +1 -1
- package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenAIProvider.js +3 -4
- package/dist/cjs/providers/OpenAIProvider.js.map +1 -1
- package/dist/cjs/providers/OpenRouterProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenRouterProvider.js +4 -3
- package/dist/cjs/providers/OpenRouterProvider.js.map +1 -1
- package/dist/cjs/providers/ProviderAdapter.d.ts +15 -10
- package/dist/cjs/providers/ProviderAdapter.d.ts.map +1 -1
- package/dist/cjs/providers/ProviderAdapter.js +8 -3
- package/dist/cjs/providers/ProviderAdapter.js.map +1 -1
- package/dist/cjs/providers/ZaiProvider.js +1 -1
- package/dist/cjs/providers/ZaiProvider.js.map +1 -1
- package/dist/cjs/types/agent.d.ts +19 -4
- package/dist/cjs/types/agent.d.ts.map +1 -1
- package/dist/cjs/types/ai.d.ts +4 -6
- package/dist/cjs/types/ai.d.ts.map +1 -1
- package/dist/cjs/types/compaction.d.ts +1 -0
- package/dist/cjs/types/compaction.d.ts.map +1 -1
- package/dist/cjs/types/errors.d.ts +4 -9
- package/dist/cjs/types/errors.d.ts.map +1 -1
- package/dist/cjs/types/errors.js +12 -12
- package/dist/cjs/types/errors.js.map +1 -1
- package/dist/cjs/types/flow.d.ts +16 -1
- package/dist/cjs/types/flow.d.ts.map +1 -1
- package/dist/cjs/types/history.d.ts +0 -7
- package/dist/cjs/types/history.d.ts.map +1 -1
- package/dist/cjs/types/index.d.ts +2 -2
- package/dist/cjs/types/index.d.ts.map +1 -1
- package/dist/cjs/types/session.d.ts +4 -2
- package/dist/cjs/types/session.d.ts.map +1 -1
- package/dist/cjs/utils/clock.js +1 -1
- package/dist/cjs/utils/clock.js.map +1 -1
- package/dist/cjs/utils/outcomes.d.ts +1 -0
- package/dist/cjs/utils/outcomes.d.ts.map +1 -1
- package/dist/cjs/utils/outcomes.js +1 -0
- package/dist/cjs/utils/outcomes.js.map +1 -1
- package/dist/cjs/utils/phrases.d.ts +25 -0
- package/dist/cjs/utils/phrases.d.ts.map +1 -0
- package/dist/cjs/utils/phrases.js +38 -0
- package/dist/cjs/utils/phrases.js.map +1 -0
- package/dist/cjs/utils/schema.d.ts +3 -12
- package/dist/cjs/utils/schema.d.ts.map +1 -1
- package/dist/cjs/utils/schema.js +3 -43
- package/dist/cjs/utils/schema.js.map +1 -1
- package/dist/cjs/utils/template.d.ts +8 -0
- package/dist/cjs/utils/template.d.ts.map +1 -1
- package/dist/cjs/utils/template.js +47 -3
- package/dist/cjs/utils/template.js.map +1 -1
- package/dist/core/Agent.d.ts +8 -1
- package/dist/core/Agent.d.ts.map +1 -1
- package/dist/core/Agent.js +19 -1
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/CompactionEngine.d.ts.map +1 -1
- package/dist/core/CompactionEngine.js +8 -3
- package/dist/core/CompactionEngine.js.map +1 -1
- package/dist/core/FlowSpec.d.ts +21 -2
- package/dist/core/FlowSpec.d.ts.map +1 -1
- package/dist/core/FlowSpec.js +323 -52
- package/dist/core/FlowSpec.js.map +1 -1
- package/dist/core/Migrate.d.ts.map +1 -1
- package/dist/core/Migrate.js +3 -1
- package/dist/core/Migrate.js.map +1 -1
- package/dist/core/Prompt.d.ts +16 -0
- package/dist/core/Prompt.d.ts.map +1 -1
- package/dist/core/Prompt.js +44 -1
- package/dist/core/Prompt.js.map +1 -1
- package/dist/core/Runner.d.ts +42 -4
- package/dist/core/Runner.d.ts.map +1 -1
- package/dist/core/Runner.js +293 -73
- package/dist/core/Runner.js.map +1 -1
- package/dist/core/Speak.d.ts.map +1 -1
- package/dist/core/Speak.js +61 -22
- package/dist/core/Speak.js.map +1 -1
- package/dist/core/Understand.d.ts +6 -3
- package/dist/core/Understand.d.ts.map +1 -1
- package/dist/core/Understand.js +34 -55
- package/dist/core/Understand.js.map +1 -1
- package/dist/core/contracts.d.ts +21 -6
- package/dist/core/contracts.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/persistence/OpenSearchStore.d.ts +2 -1
- package/dist/persistence/OpenSearchStore.d.ts.map +1 -1
- package/dist/persistence/OpenSearchStore.js +2 -2
- package/dist/persistence/OpenSearchStore.js.map +1 -1
- package/dist/persistence/RedisStore.d.ts +1 -1
- package/dist/persistence/RedisStore.d.ts.map +1 -1
- package/dist/providers/AnthropicProvider.d.ts +2 -5
- package/dist/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/providers/AnthropicProvider.js +3 -4
- package/dist/providers/AnthropicProvider.js.map +1 -1
- package/dist/providers/DeepSeekProvider.d.ts.map +1 -1
- package/dist/providers/DeepSeekProvider.js +3 -4
- package/dist/providers/DeepSeekProvider.js.map +1 -1
- package/dist/providers/FallbackAiProvider.js +1 -1
- package/dist/providers/FallbackAiProvider.js.map +1 -1
- package/dist/providers/GeminiProvider.d.ts +1 -2
- package/dist/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/providers/GeminiProvider.js +3 -4
- package/dist/providers/GeminiProvider.js.map +1 -1
- package/dist/providers/GenericOpenAICompatibleProvider.js +4 -4
- package/dist/providers/GenericOpenAICompatibleProvider.js.map +1 -1
- package/dist/providers/OpenAIProvider.d.ts.map +1 -1
- package/dist/providers/OpenAIProvider.js +3 -4
- package/dist/providers/OpenAIProvider.js.map +1 -1
- package/dist/providers/OpenRouterProvider.d.ts.map +1 -1
- package/dist/providers/OpenRouterProvider.js +4 -3
- package/dist/providers/OpenRouterProvider.js.map +1 -1
- package/dist/providers/ProviderAdapter.d.ts +15 -10
- package/dist/providers/ProviderAdapter.d.ts.map +1 -1
- package/dist/providers/ProviderAdapter.js +8 -3
- package/dist/providers/ProviderAdapter.js.map +1 -1
- package/dist/providers/ZaiProvider.js +1 -1
- package/dist/providers/ZaiProvider.js.map +1 -1
- package/dist/types/agent.d.ts +19 -4
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/ai.d.ts +4 -6
- package/dist/types/ai.d.ts.map +1 -1
- package/dist/types/compaction.d.ts +1 -0
- package/dist/types/compaction.d.ts.map +1 -1
- package/dist/types/errors.d.ts +4 -9
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/errors.js +12 -12
- package/dist/types/errors.js.map +1 -1
- package/dist/types/flow.d.ts +16 -1
- package/dist/types/flow.d.ts.map +1 -1
- package/dist/types/history.d.ts +0 -7
- package/dist/types/history.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/session.d.ts +4 -2
- package/dist/types/session.d.ts.map +1 -1
- package/dist/utils/clock.js +1 -1
- package/dist/utils/clock.js.map +1 -1
- package/dist/utils/outcomes.d.ts +1 -0
- package/dist/utils/outcomes.d.ts.map +1 -1
- package/dist/utils/outcomes.js +1 -0
- package/dist/utils/outcomes.js.map +1 -1
- package/dist/utils/phrases.d.ts +25 -0
- package/dist/utils/phrases.d.ts.map +1 -0
- package/dist/utils/phrases.js +35 -0
- package/dist/utils/phrases.js.map +1 -0
- package/dist/utils/schema.d.ts +3 -12
- package/dist/utils/schema.d.ts.map +1 -1
- package/dist/utils/schema.js +3 -42
- package/dist/utils/schema.js.map +1 -1
- package/dist/utils/template.d.ts +8 -0
- package/dist/utils/template.d.ts.map +1 -1
- package/dist/utils/template.js +47 -3
- package/dist/utils/template.js.map +1 -1
- package/docs/concepts/architecture.md +3 -3
- package/docs/concepts/collection.md +40 -5
- package/docs/concepts/pipeline.md +13 -10
- package/docs/concepts/runs-and-waits.md +2 -2
- package/docs/guides/actions-and-events.md +2 -2
- package/docs/guides/branching.md +4 -2
- package/docs/guides/compaction.md +2 -2
- package/docs/guides/conditions.md +1 -1
- package/docs/guides/error-handling.md +3 -1
- package/docs/guides/flow-control.md +4 -2
- package/docs/guides/persistence.md +2 -2
- package/docs/guides/testing.md +1 -1
- package/docs/guides/triggers.md +5 -5
- package/docs/migration/v3-to-v4.md +16 -11
- package/docs/reference/actions-events-conditions.md +2 -2
- package/docs/reference/agent.md +11 -7
- package/docs/reference/branches.md +1 -1
- package/docs/reference/errors.md +10 -7
- package/docs/reference/fields.md +5 -3
- package/docs/reference/flow-spec.md +36 -9
- package/docs/reference/flow.md +8 -3
- package/docs/reference/outcomes.md +3 -2
- package/docs/reference/providers.md +2 -0
- package/docs/reference/session.md +2 -0
- package/docs/reference/step.md +9 -5
- package/docs/reference/stores.md +5 -3
- package/docs/reference/trigger.md +24 -4
- package/docs/rfc/v4-one-flow.md +7 -5
- package/docs/start/01-install.md +5 -3
- package/docs/start/04-add-tools.md +1 -1
- package/docs/start/05-go-to-production.md +18 -1
- package/examples/05-branches.ts +1 -1
- package/examples/06-triggers-and-waits.ts +4 -3
- package/package.json +5 -3
- package/src/core/Agent.ts +23 -2
- package/src/core/CompactionEngine.ts +10 -3
- package/src/core/FlowSpec.ts +352 -60
- package/src/core/Migrate.ts +2 -1
- package/src/core/Prompt.ts +47 -1
- package/src/core/Runner.ts +290 -69
- package/src/core/Speak.ts +64 -21
- package/src/core/Understand.ts +35 -55
- package/src/core/contracts.ts +21 -4
- package/src/index.ts +1 -1
- package/src/persistence/OpenSearchStore.ts +3 -2
- package/src/persistence/RedisStore.ts +1 -1
- package/src/providers/AnthropicProvider.ts +4 -9
- package/src/providers/DeepSeekProvider.ts +2 -4
- package/src/providers/FallbackAiProvider.ts +1 -1
- package/src/providers/GeminiProvider.ts +3 -6
- package/src/providers/GenericOpenAICompatibleProvider.ts +4 -4
- package/src/providers/OpenAIProvider.ts +3 -4
- package/src/providers/OpenRouterProvider.ts +4 -2
- package/src/providers/ProviderAdapter.ts +18 -13
- package/src/providers/ZaiProvider.ts +1 -1
- package/src/types/agent.ts +20 -5
- package/src/types/ai.ts +4 -6
- package/src/types/compaction.ts +1 -0
- package/src/types/errors.ts +11 -12
- package/src/types/flow.ts +16 -1
- package/src/types/history.ts +0 -10
- package/src/types/index.ts +1 -1
- package/src/types/session.ts +4 -1
- package/src/utils/clock.ts +1 -1
- package/src/utils/outcomes.ts +1 -0
- package/src/utils/phrases.ts +40 -0
- package/src/utils/schema.ts +3 -48
- package/src/utils/template.ts +46 -3
- package/dist/cjs/providers/index.d.ts +0 -26
- package/dist/cjs/providers/index.d.ts.map +0 -1
- package/dist/cjs/providers/index.js +0 -30
- package/dist/cjs/providers/index.js.map +0 -1
- package/dist/cjs/utils/clone.d.ts +0 -8
- package/dist/cjs/utils/clone.d.ts.map +0 -1
- package/dist/cjs/utils/clone.js +0 -32
- package/dist/cjs/utils/clone.js.map +0 -1
- package/dist/cjs/utils/index.d.ts +0 -9
- package/dist/cjs/utils/index.d.ts.map +0 -1
- package/dist/cjs/utils/index.js +0 -29
- package/dist/cjs/utils/index.js.map +0 -1
- package/dist/providers/index.d.ts +0 -26
- package/dist/providers/index.d.ts.map +0 -1
- package/dist/providers/index.js +0 -17
- package/dist/providers/index.js.map +0 -1
- package/dist/utils/clone.d.ts +0 -8
- package/dist/utils/clone.d.ts.map +0 -1
- package/dist/utils/clone.js +0 -29
- package/dist/utils/clone.js.map +0 -1
- package/dist/utils/index.d.ts +0 -9
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -9
- package/dist/utils/index.js.map +0 -1
- package/src/providers/index.ts +0 -38
- package/src/utils/clone.ts +0 -34
- package/src/utils/index.ts +0 -18
package/dist/cjs/core/Runner.js
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
*/
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.Runner = void 0;
|
|
13
|
-
const clone_js_1 = require("../utils/clone.js");
|
|
14
13
|
const duration_js_1 = require("../utils/duration.js");
|
|
15
14
|
const outcomes_js_1 = require("../utils/outcomes.js");
|
|
16
15
|
const schema_js_1 = require("../utils/schema.js");
|
|
@@ -67,6 +66,15 @@ function defaultRepeat(kind) {
|
|
|
67
66
|
function talkKind(step) {
|
|
68
67
|
return step.collect?.length ? "collect" : "prompt";
|
|
69
68
|
}
|
|
69
|
+
/** Everything a flow collects: its own `collect`, then each talk step's, once each. */
|
|
70
|
+
function flowFields(flow) {
|
|
71
|
+
const fields = new Set(flow.collect);
|
|
72
|
+
for (const step of flow.steps)
|
|
73
|
+
if (isTalk(step))
|
|
74
|
+
for (const field of step.collect ?? [])
|
|
75
|
+
fields.add(field);
|
|
76
|
+
return [...fields];
|
|
77
|
+
}
|
|
70
78
|
function kindOf(step) {
|
|
71
79
|
if (!step)
|
|
72
80
|
return "if";
|
|
@@ -103,16 +111,29 @@ class Runner {
|
|
|
103
111
|
}
|
|
104
112
|
// ── Load + Ingest ─────────────────────────────────────────────────────
|
|
105
113
|
begin(input) {
|
|
106
|
-
const now =
|
|
114
|
+
const now = this.now();
|
|
115
|
+
const turn = this.open(input, describe(input, now.toISOString()), now);
|
|
116
|
+
if (turn.what.kind === "wake" && !input.session) {
|
|
117
|
+
this.ignore(turn, "no-session", turn.what.key);
|
|
118
|
+
return turn;
|
|
119
|
+
}
|
|
120
|
+
turn.ingesting = true;
|
|
121
|
+
this.ingest(turn);
|
|
122
|
+
turn.ingesting = false;
|
|
123
|
+
return turn;
|
|
124
|
+
}
|
|
125
|
+
now() {
|
|
126
|
+
return (this.options.clock ?? (() => new Date()))();
|
|
127
|
+
}
|
|
128
|
+
open(input, what, now) {
|
|
107
129
|
const nowIso = now.toISOString();
|
|
108
|
-
const what = describe(input, nowIso);
|
|
109
130
|
const silenced = typeof input.silenced === "string" ? input.silenced : input.silenced?.reason;
|
|
110
|
-
|
|
131
|
+
return {
|
|
111
132
|
input,
|
|
112
133
|
what,
|
|
113
134
|
kind: what.kind,
|
|
114
135
|
triggerKey: what.kind === "message" ? (what.id ?? what.at) : what.key,
|
|
115
|
-
session: input.session ? (
|
|
136
|
+
session: input.session ? structuredClone(input.session) : freshSession(input.sessionId),
|
|
116
137
|
original: input.session,
|
|
117
138
|
now,
|
|
118
139
|
nowIso,
|
|
@@ -120,6 +141,7 @@ class Runner {
|
|
|
120
141
|
context: input.context,
|
|
121
142
|
silenced,
|
|
122
143
|
understandAllowed: typeof input.silenced === "object" && input.silenced.understand === true,
|
|
144
|
+
skipSilenced: typeof input.silenced === "object" && input.silenced.skip === true,
|
|
123
145
|
ignored: false,
|
|
124
146
|
messages: [],
|
|
125
147
|
schedule: [],
|
|
@@ -136,14 +158,14 @@ class Runner {
|
|
|
136
158
|
speakDone: false,
|
|
137
159
|
queue: [],
|
|
138
160
|
};
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
turn
|
|
144
|
-
|
|
145
|
-
turn.
|
|
146
|
-
return turn;
|
|
161
|
+
}
|
|
162
|
+
/** Every wake the session waits on: each parked run's own, then each silence flow's since the assistant last spoke. */
|
|
163
|
+
pendingWakes(input) {
|
|
164
|
+
// No input arrives, so nothing reads `what`: the inert wake shape only fills the field.
|
|
165
|
+
const turn = this.open({ ...input, sessionId: input.session.id }, { kind: "wake", key: "" }, this.now());
|
|
166
|
+
// `park` always sets both; the type leaves them optional, so a hand-built blob without them has no wake to give.
|
|
167
|
+
const parked = turn.session.runs.flatMap(({ status, waiting }) => status === "waiting" && waiting?.key && waiting.until ? [{ key: waiting.key, at: new Date(waiting.until) }] : []);
|
|
168
|
+
return [...parked, ...this.silenceWakes(turn)];
|
|
147
169
|
}
|
|
148
170
|
ingest(turn) {
|
|
149
171
|
const { what, session } = turn;
|
|
@@ -224,6 +246,9 @@ class Runner {
|
|
|
224
246
|
const branch = step.branches?.find((b) => "if" in b && this.holds(b.if, turn, run));
|
|
225
247
|
const target = branch ? branch.then : step.else;
|
|
226
248
|
this.outcome(turn, run, { kind: "wait", status: "ok", code: "replied", next: nextLabel(target) });
|
|
249
|
+
// The reply is this run's while it moves, so a stay it reaches answers it.
|
|
250
|
+
if (!turn.floorFromIngest)
|
|
251
|
+
turn.floorRunId = run.id;
|
|
227
252
|
this.follow(turn, run, flow, step, target);
|
|
228
253
|
this.takeFloor(turn, run);
|
|
229
254
|
}
|
|
@@ -263,8 +288,10 @@ class Runner {
|
|
|
263
288
|
/** `silence:<flowId>:<sessionId>:<lastAssistantAtMs>`, honoured only while the blob still shows that silence. */
|
|
264
289
|
silenceWake(turn, key) {
|
|
265
290
|
const rest = key.slice("silence:".length);
|
|
266
|
-
const flowId = rest.slice(0, rest.indexOf(":"));
|
|
267
291
|
const ms = Number(rest.slice(rest.lastIndexOf(":") + 1));
|
|
292
|
+
// A flow id may hold a ":" itself, so cut at the session id rather than at the first ":".
|
|
293
|
+
const cut = rest.lastIndexOf(`:${turn.session.id}:`);
|
|
294
|
+
const flowId = rest.slice(0, cut === -1 ? rest.indexOf(":") : cut);
|
|
268
295
|
const { lastAssistantAt } = turn.session;
|
|
269
296
|
const flow = this.flows.get(flowId);
|
|
270
297
|
if (!flow) {
|
|
@@ -319,13 +346,9 @@ class Runner {
|
|
|
319
346
|
};
|
|
320
347
|
if (opts.trigger?.if && !this.holds(opts.trigger.if, turn, run))
|
|
321
348
|
return null;
|
|
322
|
-
const
|
|
323
|
-
if (
|
|
324
|
-
|
|
325
|
-
return skip("already-claimed");
|
|
326
|
-
if (turn.now.getTime() - Date.parse(heldAt) < (0, duration_js_1.parseDuration)(repeat.cooldown))
|
|
327
|
-
return skip("cooldown");
|
|
328
|
-
}
|
|
349
|
+
const blocked = this.repeatBlocks(turn, dedupeKey, repeat);
|
|
350
|
+
if (blocked)
|
|
351
|
+
return skip(blocked);
|
|
329
352
|
if (hop >= MAX_HOP)
|
|
330
353
|
return skip("hop-limit");
|
|
331
354
|
const live = session.runs.find((r) => r.flowId === flow.id && r.anchor === anchor);
|
|
@@ -348,9 +371,11 @@ class Runner {
|
|
|
348
371
|
}
|
|
349
372
|
session.runs.push(run);
|
|
350
373
|
turn.started.push({ runId: run.id, flowId: flow.id, anchor, dedupeKey });
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
374
|
+
const eventTrigger = opts.trigger && "event" in opts.trigger ? opts.trigger : undefined;
|
|
375
|
+
const after = eventTrigger?.after;
|
|
376
|
+
const at = this.snap(turn, new Date(turn.now.getTime() + (after ? (0, duration_js_1.parseDuration)(after) : 0)), eventTrigger?.businessHours);
|
|
377
|
+
// With no `after`, only a closed hour parks the start: inside working hours the snap returns now.
|
|
378
|
+
if (after || at.getTime() > turn.now.getTime()) {
|
|
354
379
|
this.park(turn, run, { kind: "timer", key: `${run.id}:start:${at.getTime()}` }, at);
|
|
355
380
|
this.outcome(turn, run, { kind: "wait", status: "waiting", code: "awaiting-trigger", until: at.toISOString() });
|
|
356
381
|
}
|
|
@@ -372,12 +397,16 @@ class Runner {
|
|
|
372
397
|
}
|
|
373
398
|
repeatAllows(turn, flow, trigger, key) {
|
|
374
399
|
const repeat = trigger.repeat ?? defaultRepeat(triggerKind(trigger));
|
|
375
|
-
|
|
400
|
+
return !this.repeatBlocks(turn, `${flow.id}:${this.anchorOf(turn, flow)}:${repeat === "always" ? key : ""}`, repeat);
|
|
401
|
+
}
|
|
402
|
+
/** Why the claim already on `dedupeKey` stops another start, or nothing when there is none or its cooldown has run out. */
|
|
403
|
+
repeatBlocks(turn, dedupeKey, repeat) {
|
|
404
|
+
const heldAt = this.claimAt(turn, dedupeKey);
|
|
376
405
|
if (heldAt === undefined)
|
|
377
|
-
return
|
|
406
|
+
return undefined;
|
|
378
407
|
if (typeof repeat === "string")
|
|
379
|
-
return
|
|
380
|
-
return turn.now.getTime() - Date.parse(heldAt)
|
|
408
|
+
return "already-claimed";
|
|
409
|
+
return turn.now.getTime() - Date.parse(heldAt) < (0, duration_js_1.parseDuration)(repeat.cooldown) ? "cooldown" : undefined;
|
|
381
410
|
}
|
|
382
411
|
/** A run as it would be if started now, for trigger-level predicates. */
|
|
383
412
|
draftRun(turn, flow, kind, key, payload) {
|
|
@@ -439,8 +468,7 @@ class Runner {
|
|
|
439
468
|
const floorRun = this.floorRun(turn);
|
|
440
469
|
const floorFlow = floorRun && this.flows.get(floorRun.flowId);
|
|
441
470
|
const eligible = this.eligibleMessageFlows(turn);
|
|
442
|
-
|
|
443
|
-
const messageFlows = !floorRun && eligible.length === 1 ? [] : eligible;
|
|
471
|
+
const messageFlows = !floorRun && this.startsUnscored(turn, eligible) ? [] : eligible;
|
|
444
472
|
const mentionFlows = this.mentionFlows()
|
|
445
473
|
.filter(({ flow, trigger }) => trigger.mention.length > 0 && this.repeatAllows(turn, flow, trigger, turn.triggerKey))
|
|
446
474
|
.map(({ flow }) => flow);
|
|
@@ -454,18 +482,28 @@ class Runner {
|
|
|
454
482
|
}
|
|
455
483
|
const fields = {};
|
|
456
484
|
const data = turn.session.data;
|
|
485
|
+
const harvestable = (field) => {
|
|
486
|
+
const def = this.options.fields[field];
|
|
487
|
+
return def !== undefined && !(0, schema_js_1.isKnown)(data[field]) && (0, schema_js_1.extractMode)(def) === "anywhere";
|
|
488
|
+
};
|
|
457
489
|
for (const flow of [...(floorFlow ? [floorFlow] : []), ...eligible]) {
|
|
458
|
-
for (const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
490
|
+
for (const field of flowFields(flow).filter(harvestable))
|
|
491
|
+
fields[field] = this.options.fields[field];
|
|
492
|
+
}
|
|
493
|
+
const judging = messageFlows.length > 0 || mentionFlows.length > 0 || branches.length > 0 || Object.keys(fields).length > 0;
|
|
494
|
+
// With nobody on the floor the catch-all may take this message, and an opening message often says the most. The
|
|
495
|
+
// 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.
|
|
496
|
+
// 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.
|
|
497
|
+
const fallback = floorRun ? undefined : this.messageFlows(turn, (list) => list.length === 0)[0];
|
|
498
|
+
if (fallback) {
|
|
499
|
+
const first = fallback.steps[0];
|
|
500
|
+
const free = new Set(first && isTalk(first) && first.question === undefined ? first.collect : []);
|
|
501
|
+
const own = flowFields(fallback).filter(harvestable);
|
|
502
|
+
if (judging || own.some((field) => !free.has(field)))
|
|
503
|
+
for (const field of own)
|
|
504
|
+
fields[field] = this.options.fields[field];
|
|
467
505
|
}
|
|
468
|
-
if (!
|
|
506
|
+
if (!judging && !Object.keys(fields).length)
|
|
469
507
|
return null;
|
|
470
508
|
return {
|
|
471
509
|
text: turn.what.text,
|
|
@@ -494,6 +532,13 @@ class Runner {
|
|
|
494
532
|
eligibleMessageFlows(turn) {
|
|
495
533
|
return this.messageFlows(turn, (list) => list.length > 0);
|
|
496
534
|
}
|
|
535
|
+
/**
|
|
536
|
+
* The lone eligible flow starts without a score only when a low score would have nowhere
|
|
537
|
+
* else to send the message: no catch-all passes and the idle speaker is silent (S9).
|
|
538
|
+
*/
|
|
539
|
+
startsUnscored(turn, eligible) {
|
|
540
|
+
return eligible.length === 1 && this.options.idle === "silent" && this.messageFlows(turn, (list) => list.length === 0).length === 0;
|
|
541
|
+
}
|
|
497
542
|
messageFlows(turn, accept) {
|
|
498
543
|
const out = [];
|
|
499
544
|
for (const flow of this.flows.values()) {
|
|
@@ -524,6 +569,8 @@ class Runner {
|
|
|
524
569
|
return;
|
|
525
570
|
turn.llmCalls += understanding?.llmCalls ?? 0;
|
|
526
571
|
turn.usage = (0, usage_js_1.addUsage)(turn.usage, understanding?.usage);
|
|
572
|
+
if (understanding?.asks !== undefined)
|
|
573
|
+
turn.asks = understanding.asks;
|
|
527
574
|
const key = turn.triggerKey;
|
|
528
575
|
const asker = this.asker(turn);
|
|
529
576
|
// Routing is decided before extraction lands so `clearOnStart` runs first, and applied after mention runs.
|
|
@@ -538,7 +585,7 @@ class Runner {
|
|
|
538
585
|
if (top && top.score >= current + ROUTE_STICKY && top.score >= ROUTE_MIN)
|
|
539
586
|
route = top.flow;
|
|
540
587
|
}
|
|
541
|
-
else if (
|
|
588
|
+
else if (this.startsUnscored(turn, eligible)) {
|
|
542
589
|
route = eligible[0];
|
|
543
590
|
}
|
|
544
591
|
else {
|
|
@@ -567,6 +614,9 @@ class Runner {
|
|
|
567
614
|
turn.floorRunId = run.id;
|
|
568
615
|
}
|
|
569
616
|
}
|
|
617
|
+
// Unless routing or Ingest moved it, the message is the asker's: written down, so a chain beside it cannot take it.
|
|
618
|
+
if (asker && !route && !turn.floorFromIngest)
|
|
619
|
+
turn.floorRunId = asker.id;
|
|
570
620
|
for (const [field, raw] of Object.entries(understanding?.fields ?? {}))
|
|
571
621
|
this.writeField(turn, field, raw);
|
|
572
622
|
if (asker && turn.session.runs.includes(asker) && asker.status === "asking") {
|
|
@@ -577,7 +627,13 @@ class Runner {
|
|
|
577
627
|
}
|
|
578
628
|
}
|
|
579
629
|
fireBranch(turn, run, flow, step, understanding) {
|
|
580
|
-
|
|
630
|
+
// Staying, a run does not take an `if` branch back onto a path it has already run: the fact it tests is still true,
|
|
631
|
+
// so it would re-run that path on every message. A `when` branch is judged on each new message and always counts.
|
|
632
|
+
// 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`.
|
|
633
|
+
const ran = (then) => typeof then === "string" ? then === "end" || (run.visits[then] ?? 0) > 0 : "step" in then && (run.visits[then.step] ?? 0) > 0;
|
|
634
|
+
const hit = (branch, index) => "if" in branch
|
|
635
|
+
? !(run.staying && ran(branch.then)) && this.holds(branch.if, turn, run)
|
|
636
|
+
: understanding?.branches[`${run.id}/${step.id}/${index}`] === true;
|
|
581
637
|
const index = (step.branches ?? []).findIndex(hit);
|
|
582
638
|
if (index < 0)
|
|
583
639
|
return;
|
|
@@ -607,10 +663,63 @@ class Runner {
|
|
|
607
663
|
async advance(turn) {
|
|
608
664
|
if (turn.ignored)
|
|
609
665
|
return null;
|
|
610
|
-
this.
|
|
666
|
+
this.resume(turn);
|
|
611
667
|
turn.queue = [...turn.session.runs];
|
|
612
668
|
await this.drain(turn);
|
|
613
|
-
|
|
669
|
+
// An asker that moved on without a word hands the message to the run it suspended, and that one to the next.
|
|
670
|
+
// Each pass takes a run off the suspended stack, so the stack's size bounds the loop.
|
|
671
|
+
for (let left = turn.session.runs.length; left > 0; left--) {
|
|
672
|
+
if (turn.what.kind !== "message" || turn.silenced !== undefined || turn.talk || turn.spokeBy.size > 0)
|
|
673
|
+
break;
|
|
674
|
+
const resumed = this.resume(turn);
|
|
675
|
+
if (!resumed)
|
|
676
|
+
break;
|
|
677
|
+
turn.queue.push(resumed);
|
|
678
|
+
await this.drain(turn);
|
|
679
|
+
}
|
|
680
|
+
const speaker = this.speaker(turn);
|
|
681
|
+
if (!speaker || "idle" in speaker || !this.fixedDue(speaker))
|
|
682
|
+
return speaker;
|
|
683
|
+
// A question the lead asked gets its answer first: the step speaks, and its fixed question follows in settle.
|
|
684
|
+
// Unjudged counts as asked, and so does the retry of a failed answer: a question sent over the lead's own drops it.
|
|
685
|
+
const answerFirst = turn.what.kind === "message" ? turn.asks !== false : this.retrying(speaker);
|
|
686
|
+
if (answerFirst && speaker.step.question !== undefined) {
|
|
687
|
+
speaker.fixedAfter = (0, template_js_1.render)(speaker.step.question, this.scope(turn, speaker.run));
|
|
688
|
+
return speaker;
|
|
689
|
+
}
|
|
690
|
+
this.askFixed(turn, speaker);
|
|
691
|
+
turn.talk = undefined;
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
/** The step failed at the provider and runs again on its retry wake: deferTalk left `deferred` as its last line. */
|
|
695
|
+
retrying({ run, step }) {
|
|
696
|
+
const last = run.outcomes.at(-1);
|
|
697
|
+
return last?.status === "deferred" && last.stepId === step.id;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* The step's fixed question is due: this is its first ask, with every field it collects still unknown and none asked
|
|
701
|
+
* yet, and the run is not staying (a staying run answers the lead). Otherwise the AI phrases the ask.
|
|
702
|
+
*/
|
|
703
|
+
fixedDue({ run, step, pending }) {
|
|
704
|
+
const { question, collect = [] } = step;
|
|
705
|
+
if (question === undefined || run.staying)
|
|
706
|
+
return false;
|
|
707
|
+
return pending.length === collect.length && pending.every((field) => (run.asked[field] ?? 0) === 0);
|
|
708
|
+
}
|
|
709
|
+
/** Send the step's fixed question when it is due. It counts as one ask of each field. */
|
|
710
|
+
askFixed(turn, talk) {
|
|
711
|
+
const { run, step, pending } = talk;
|
|
712
|
+
if (!this.fixedDue(talk) || step.question === undefined)
|
|
713
|
+
return false;
|
|
714
|
+
const key = this.stepKey(run, step);
|
|
715
|
+
turn.messages.push({ text: (0, template_js_1.render)(step.question, this.scope(turn, run)), kind: "verbatim", afterMs: turn.afterMs, key, runId: run.id, stepId: step.id });
|
|
716
|
+
turn.afterMs = 0;
|
|
717
|
+
turn.spokeBy.add(run.id);
|
|
718
|
+
turn.spoke = true;
|
|
719
|
+
for (const field of pending)
|
|
720
|
+
run.asked[field] = (run.asked[field] ?? 0) + 1;
|
|
721
|
+
this.outcome(turn, run, { kind: "collect", status: "ok", key, code: "asked-fixed", stepId: step.id });
|
|
722
|
+
return true;
|
|
614
723
|
}
|
|
615
724
|
/**
|
|
616
725
|
* Everything Speak needs for this turn's one speaker. Instructions are agent,
|
|
@@ -633,12 +742,14 @@ class Runner {
|
|
|
633
742
|
const instructions = [...(this.options.instructions ?? []), ...own.instructions].filter((ins) => !ins.if || (0, predicate_js_1.evaluate)(ins.if, ctx, conditions));
|
|
634
743
|
const all = this.options.tools ?? [];
|
|
635
744
|
const allowed = own.tools;
|
|
745
|
+
const retry = !("idle" in talk) && this.retrying(talk);
|
|
636
746
|
return {
|
|
637
747
|
talk,
|
|
638
|
-
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 } : {}) },
|
|
639
749
|
context: turn.context,
|
|
640
750
|
data: turn.session.data,
|
|
641
751
|
history: this.historyOf(turn),
|
|
752
|
+
said: [...turn.messages],
|
|
642
753
|
now: turn.now,
|
|
643
754
|
instructions,
|
|
644
755
|
tools: allowed ? all.filter((tool) => allowed.includes(tool.id)) : all,
|
|
@@ -681,6 +792,11 @@ class Runner {
|
|
|
681
792
|
}
|
|
682
793
|
if (!this.premiseHolds(turn, run, flow))
|
|
683
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
|
+
}
|
|
684
800
|
const resuming = run.status === "asking";
|
|
685
801
|
run.status = "running";
|
|
686
802
|
if (run.stepId === null) {
|
|
@@ -723,6 +839,11 @@ class Runner {
|
|
|
723
839
|
this.endRun(turn, run, "skipped", "premise-changed");
|
|
724
840
|
return ok;
|
|
725
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
|
+
}
|
|
726
847
|
async execute(turn, run, flow, step, resuming) {
|
|
727
848
|
const data = turn.session.data;
|
|
728
849
|
const key = this.stepKey(run, step);
|
|
@@ -731,7 +852,9 @@ class Runner {
|
|
|
731
852
|
let pending = [];
|
|
732
853
|
if (step.collect?.length) {
|
|
733
854
|
pending = (0, schema_js_1.pendingFields)(step, data, run.asked);
|
|
734
|
-
|
|
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) {
|
|
735
858
|
this.reportMaxAsks(turn, run, step);
|
|
736
859
|
this.outcome(turn, run, resuming
|
|
737
860
|
? { kind, status: "ok", key, next: nextLabel(step.then) }
|
|
@@ -743,6 +866,8 @@ class Runner {
|
|
|
743
866
|
if (turn.silenced !== undefined) {
|
|
744
867
|
if (resuming)
|
|
745
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);
|
|
746
871
|
else
|
|
747
872
|
this.endRun(turn, run, "skipped", "silenced", kind, turn.silenced);
|
|
748
873
|
return;
|
|
@@ -754,13 +879,19 @@ class Runner {
|
|
|
754
879
|
}
|
|
755
880
|
}
|
|
756
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.
|
|
757
883
|
if (!turn.speakDone)
|
|
758
884
|
turn.talk = { run, flow, step, pending };
|
|
885
|
+
else
|
|
886
|
+
this.askFixed(turn, { run, flow, step, pending });
|
|
759
887
|
return;
|
|
760
888
|
}
|
|
761
889
|
if (isSay(step)) {
|
|
762
890
|
if (turn.silenced !== undefined) {
|
|
763
|
-
|
|
891
|
+
if (turn.skipSilenced)
|
|
892
|
+
this.skipSilenced(turn, run, flow, step, "say");
|
|
893
|
+
else
|
|
894
|
+
this.endRun(turn, run, "skipped", "silenced", "say", turn.silenced);
|
|
764
895
|
return;
|
|
765
896
|
}
|
|
766
897
|
if (step.once) {
|
|
@@ -807,6 +938,13 @@ class Runner {
|
|
|
807
938
|
result = { failed: error instanceof Error ? error.message : String(error) };
|
|
808
939
|
}
|
|
809
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 && !(0, duration_js_1.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
|
+
}
|
|
810
948
|
if ("ok" in result) {
|
|
811
949
|
if (result.spoke) {
|
|
812
950
|
turn.spokeBy.add(run.id);
|
|
@@ -898,18 +1036,21 @@ class Runner {
|
|
|
898
1036
|
}
|
|
899
1037
|
if ("step" in target) {
|
|
900
1038
|
const data = turn.session.data;
|
|
901
|
-
|
|
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 ?? []) {
|
|
902
1041
|
delete data[field];
|
|
1042
|
+
delete run.asked[field];
|
|
1043
|
+
}
|
|
903
1044
|
this.jump(turn, run, flow, target.step);
|
|
904
1045
|
return;
|
|
905
1046
|
}
|
|
906
1047
|
const childId = (0, template_js_1.render)(target.flow, this.scope(turn, run));
|
|
907
1048
|
const key = this.stepKey(run, step);
|
|
908
1049
|
this.endRun(turn, run, "flow");
|
|
909
|
-
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);
|
|
910
1051
|
}
|
|
911
|
-
/** Start a child flow that
|
|
912
|
-
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) {
|
|
913
1054
|
const child = this.flows.get(flowId);
|
|
914
1055
|
if (!child) {
|
|
915
1056
|
turn.skipped.push({ flowId, anchor: turn.session.id, triggerKey: key, code: "flow-gone", message: outcomes_js_1.OUTCOME_MESSAGES["flow-gone"] });
|
|
@@ -918,9 +1059,11 @@ class Runner {
|
|
|
918
1059
|
const run = this.startRun(turn, child, "flow", key, { payload: input, hop, keepData });
|
|
919
1060
|
if (!run)
|
|
920
1061
|
return;
|
|
921
|
-
turn.floorRunId
|
|
922
|
-
|
|
923
|
-
turn.
|
|
1062
|
+
if (turn.floorRunId === undefined || turn.floorRunId === parent.id) {
|
|
1063
|
+
turn.floorRunId = run.id;
|
|
1064
|
+
if (turn.ingesting)
|
|
1065
|
+
turn.floorFromIngest = true;
|
|
1066
|
+
}
|
|
924
1067
|
turn.queue.push(run);
|
|
925
1068
|
}
|
|
926
1069
|
jump(turn, run, flow, stepId) {
|
|
@@ -935,14 +1078,54 @@ class Runner {
|
|
|
935
1078
|
run.visits[stepId] = (run.visits[stepId] ?? 0) + 1;
|
|
936
1079
|
run.status = "running";
|
|
937
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);
|
|
938
1102
|
}
|
|
939
1103
|
finishFlow(turn, run, flow) {
|
|
940
1104
|
const onEnd = flow.onEnd ?? "end";
|
|
941
1105
|
const last = flow.steps[flow.steps.length - 1];
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
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
|
+
}
|
|
946
1129
|
return;
|
|
947
1130
|
}
|
|
948
1131
|
if (onEnd === "reset" && last) {
|
|
@@ -950,7 +1133,7 @@ class Runner {
|
|
|
950
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.
|
|
951
1134
|
const key = this.stepKey(run, last);
|
|
952
1135
|
this.endRun(turn, run, "reset");
|
|
953
|
-
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);
|
|
954
1137
|
return;
|
|
955
1138
|
}
|
|
956
1139
|
this.endRun(turn, run, "end");
|
|
@@ -972,10 +1155,21 @@ class Runner {
|
|
|
972
1155
|
run.waiting = { ...waiting, until: at.toISOString(), setAt: turn.nowIso };
|
|
973
1156
|
turn.schedule.push({ key: waiting.key, at });
|
|
974
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
|
+
}
|
|
975
1169
|
/** When nobody asks, the most recently suspended run returns to asking. */
|
|
976
1170
|
resumeSuspended(turn) {
|
|
977
1171
|
if (this.asker(turn))
|
|
978
|
-
return;
|
|
1172
|
+
return undefined;
|
|
979
1173
|
const suspendedAt = (r) => r.suspendedAt ?? r.startedAt;
|
|
980
1174
|
const next = turn.session.runs
|
|
981
1175
|
.filter((r) => r.status === "suspended")
|
|
@@ -984,6 +1178,7 @@ class Runner {
|
|
|
984
1178
|
next.status = "asking";
|
|
985
1179
|
delete next.suspendedAt;
|
|
986
1180
|
}
|
|
1181
|
+
return next;
|
|
987
1182
|
}
|
|
988
1183
|
// ── Settle: the one applier after Speak (design §4.7) ─────────────────
|
|
989
1184
|
async settle(turn, outcome) {
|
|
@@ -1038,16 +1233,33 @@ class Runner {
|
|
|
1038
1233
|
this.outcome(turn, run, { kind: talkKind(step), status: "ok", key, llmCalls: spoken.llmCalls, stepId: step.id });
|
|
1039
1234
|
if (!turn.session.runs.includes(run))
|
|
1040
1235
|
return;
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
run.asked[field] = (run.asked[field] ?? 0) + 1;
|
|
1236
|
+
const pending = step.collect?.length ? (0, schema_js_1.pendingFields)(step, turn.session.data, run.asked) : [];
|
|
1237
|
+
if (talk.fixedAfter !== undefined) {
|
|
1238
|
+
// The reply only answered the lead; the step's own question goes out after it and counts the ask.
|
|
1239
|
+
if (this.askFixed(turn, { run, flow, step, pending })) {
|
|
1046
1240
|
run.status = "asking";
|
|
1047
1241
|
return;
|
|
1048
1242
|
}
|
|
1049
|
-
this.reportMaxAsks(turn, run, step);
|
|
1050
1243
|
}
|
|
1244
|
+
else {
|
|
1245
|
+
for (const field of pending)
|
|
1246
|
+
run.asked[field] = (run.asked[field] ?? 0) + 1;
|
|
1247
|
+
}
|
|
1248
|
+
if (run.staying) {
|
|
1249
|
+
// 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.
|
|
1250
|
+
const maxAsks = step.maxAsks ?? schema_js_1.DEFAULT_MAX_ASKS;
|
|
1251
|
+
for (const field of pending) {
|
|
1252
|
+
if (run.asked[field] === maxAsks)
|
|
1253
|
+
this.outcome(turn, run, { kind: "collect", status: "skipped", key, code: "max-asks", detail: field });
|
|
1254
|
+
}
|
|
1255
|
+
this.stayAt(run, step);
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
if (pending.length) {
|
|
1259
|
+
run.status = "asking";
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
this.reportMaxAsks(turn, run, step);
|
|
1051
1263
|
run.status = "running";
|
|
1052
1264
|
this.follow(turn, run, flow, step, step.then);
|
|
1053
1265
|
if (turn.session.runs.includes(run) && run.status === "running")
|
|
@@ -1084,16 +1296,23 @@ class Runner {
|
|
|
1084
1296
|
const reset = deferred.resetAtMs;
|
|
1085
1297
|
return new Date(reset !== undefined && reset > ladder ? reset : ladder);
|
|
1086
1298
|
}
|
|
1087
|
-
/** The assistant spoke
|
|
1299
|
+
/** The assistant spoke this turn: its silence wakes replace the ones its last words set. */
|
|
1088
1300
|
armSilence(turn) {
|
|
1301
|
+
const previous = turn.original?.lastAssistantAt;
|
|
1302
|
+
if (turn.session.lastAssistantAt === previous)
|
|
1303
|
+
return;
|
|
1304
|
+
turn.schedule.push(...this.silenceWakes(turn, previous));
|
|
1305
|
+
}
|
|
1306
|
+
/** The assistant spoke last: every silence flow passing `if` and `repeat` gets a wake. */
|
|
1307
|
+
silenceWakes(turn, previous) {
|
|
1089
1308
|
const { session } = turn;
|
|
1090
1309
|
const { lastAssistantAt, lastUserAt } = session;
|
|
1091
|
-
if (!lastAssistantAt
|
|
1092
|
-
return;
|
|
1310
|
+
if (!lastAssistantAt)
|
|
1311
|
+
return [];
|
|
1093
1312
|
if (lastUserAt && Date.parse(lastUserAt) > Date.parse(lastAssistantAt))
|
|
1094
|
-
return;
|
|
1313
|
+
return [];
|
|
1095
1314
|
const ms = Date.parse(lastAssistantAt);
|
|
1096
|
-
const
|
|
1315
|
+
const wakes = [];
|
|
1097
1316
|
for (const flow of this.flows.values()) {
|
|
1098
1317
|
const trigger = (flow.on ?? []).find((t) => "silence" in t);
|
|
1099
1318
|
if (!trigger)
|
|
@@ -1104,12 +1323,13 @@ class Runner {
|
|
|
1104
1323
|
if (!this.repeatAllows(turn, flow, trigger, key))
|
|
1105
1324
|
continue;
|
|
1106
1325
|
const at = this.snap(turn, new Date(ms + (0, duration_js_1.parseDuration)(trigger.silence)), trigger.businessHours);
|
|
1107
|
-
|
|
1326
|
+
wakes.push({
|
|
1108
1327
|
key: `silence:${flow.id}:${session.id}:${ms}`,
|
|
1109
1328
|
at,
|
|
1110
1329
|
...(previous ? { replaces: `silence:${flow.id}:${session.id}:${Date.parse(previous)}` } : {}),
|
|
1111
1330
|
});
|
|
1112
1331
|
}
|
|
1332
|
+
return wakes;
|
|
1113
1333
|
}
|
|
1114
1334
|
// ── Return (design §4.8) ──────────────────────────────────────────────
|
|
1115
1335
|
finish(turn) {
|