@falai/agent 4.0.0-alpha.8 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/cjs/core/Agent.d.ts +8 -1
- package/dist/cjs/core/Agent.d.ts.map +1 -1
- package/dist/cjs/core/Agent.js +18 -0
- package/dist/cjs/core/Agent.js.map +1 -1
- package/dist/cjs/core/CompactionEngine.d.ts.map +1 -1
- package/dist/cjs/core/CompactionEngine.js +21 -7
- package/dist/cjs/core/CompactionEngine.js.map +1 -1
- package/dist/cjs/core/FlowSpec.d.ts +21 -2
- package/dist/cjs/core/FlowSpec.d.ts.map +1 -1
- package/dist/cjs/core/FlowSpec.js +297 -59
- package/dist/cjs/core/FlowSpec.js.map +1 -1
- package/dist/cjs/core/Migrate.d.ts.map +1 -1
- package/dist/cjs/core/Migrate.js +3 -1
- package/dist/cjs/core/Migrate.js.map +1 -1
- package/dist/cjs/core/Prompt.d.ts +16 -0
- package/dist/cjs/core/Prompt.d.ts.map +1 -1
- package/dist/cjs/core/Prompt.js +39 -0
- package/dist/cjs/core/Prompt.js.map +1 -1
- package/dist/cjs/core/Runner.d.ts +37 -4
- package/dist/cjs/core/Runner.d.ts.map +1 -1
- package/dist/cjs/core/Runner.js +281 -72
- package/dist/cjs/core/Runner.js.map +1 -1
- package/dist/cjs/core/Speak.d.ts.map +1 -1
- package/dist/cjs/core/Speak.js +63 -19
- package/dist/cjs/core/Speak.js.map +1 -1
- package/dist/cjs/core/Understand.d.ts +6 -3
- package/dist/cjs/core/Understand.d.ts.map +1 -1
- package/dist/cjs/core/Understand.js +18 -42
- package/dist/cjs/core/Understand.js.map +1 -1
- package/dist/cjs/core/contracts.d.ts +26 -6
- package/dist/cjs/core/contracts.d.ts.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/persistence/OpenSearchStore.d.ts +2 -1
- package/dist/cjs/persistence/OpenSearchStore.d.ts.map +1 -1
- package/dist/cjs/persistence/OpenSearchStore.js +2 -2
- package/dist/cjs/persistence/OpenSearchStore.js.map +1 -1
- package/dist/cjs/persistence/RedisStore.d.ts +1 -1
- package/dist/cjs/persistence/RedisStore.d.ts.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.d.ts +2 -5
- package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.js +3 -4
- package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
- package/dist/cjs/providers/DeepSeekProvider.d.ts.map +1 -1
- package/dist/cjs/providers/DeepSeekProvider.js +3 -4
- package/dist/cjs/providers/DeepSeekProvider.js.map +1 -1
- package/dist/cjs/providers/FallbackAiProvider.js +1 -1
- package/dist/cjs/providers/FallbackAiProvider.js.map +1 -1
- package/dist/cjs/providers/GeminiProvider.d.ts +1 -2
- package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/cjs/providers/GeminiProvider.js +3 -4
- package/dist/cjs/providers/GeminiProvider.js.map +1 -1
- package/dist/cjs/providers/GenericOpenAICompatibleProvider.js +4 -4
- package/dist/cjs/providers/GenericOpenAICompatibleProvider.js.map +1 -1
- package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenAIProvider.js +3 -4
- package/dist/cjs/providers/OpenAIProvider.js.map +1 -1
- package/dist/cjs/providers/OpenRouterProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenRouterProvider.js +4 -3
- package/dist/cjs/providers/OpenRouterProvider.js.map +1 -1
- package/dist/cjs/providers/ProviderAdapter.d.ts +26 -11
- package/dist/cjs/providers/ProviderAdapter.d.ts.map +1 -1
- package/dist/cjs/providers/ProviderAdapter.js +13 -13
- package/dist/cjs/providers/ProviderAdapter.js.map +1 -1
- package/dist/cjs/providers/ZaiProvider.js +1 -1
- package/dist/cjs/providers/ZaiProvider.js.map +1 -1
- package/dist/cjs/types/agent.d.ts +14 -3
- package/dist/cjs/types/agent.d.ts.map +1 -1
- package/dist/cjs/types/ai.d.ts +10 -6
- package/dist/cjs/types/ai.d.ts.map +1 -1
- package/dist/cjs/types/compaction.d.ts +1 -0
- package/dist/cjs/types/compaction.d.ts.map +1 -1
- package/dist/cjs/types/errors.d.ts +4 -9
- package/dist/cjs/types/errors.d.ts.map +1 -1
- package/dist/cjs/types/errors.js +12 -12
- package/dist/cjs/types/errors.js.map +1 -1
- package/dist/cjs/types/flow.d.ts +17 -1
- package/dist/cjs/types/flow.d.ts.map +1 -1
- package/dist/cjs/types/history.d.ts +0 -7
- package/dist/cjs/types/history.d.ts.map +1 -1
- package/dist/cjs/types/index.d.ts +2 -2
- package/dist/cjs/types/index.d.ts.map +1 -1
- package/dist/cjs/types/session.d.ts +4 -2
- package/dist/cjs/types/session.d.ts.map +1 -1
- package/dist/cjs/utils/clock.js +1 -1
- package/dist/cjs/utils/clock.js.map +1 -1
- package/dist/cjs/utils/outcomes.d.ts +2 -0
- package/dist/cjs/utils/outcomes.d.ts.map +1 -1
- package/dist/cjs/utils/outcomes.js +2 -0
- package/dist/cjs/utils/outcomes.js.map +1 -1
- package/dist/cjs/utils/schema.d.ts +3 -12
- package/dist/cjs/utils/schema.d.ts.map +1 -1
- package/dist/cjs/utils/schema.js +3 -43
- package/dist/cjs/utils/schema.js.map +1 -1
- package/dist/cjs/utils/template.d.ts.map +1 -1
- package/dist/cjs/utils/template.js +9 -2
- package/dist/cjs/utils/template.js.map +1 -1
- package/dist/core/Agent.d.ts +8 -1
- package/dist/core/Agent.d.ts.map +1 -1
- package/dist/core/Agent.js +19 -1
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/CompactionEngine.d.ts.map +1 -1
- package/dist/core/CompactionEngine.js +21 -7
- package/dist/core/CompactionEngine.js.map +1 -1
- package/dist/core/FlowSpec.d.ts +21 -2
- package/dist/core/FlowSpec.d.ts.map +1 -1
- package/dist/core/FlowSpec.js +296 -60
- package/dist/core/FlowSpec.js.map +1 -1
- package/dist/core/Migrate.d.ts.map +1 -1
- package/dist/core/Migrate.js +3 -1
- package/dist/core/Migrate.js.map +1 -1
- package/dist/core/Prompt.d.ts +16 -0
- package/dist/core/Prompt.d.ts.map +1 -1
- package/dist/core/Prompt.js +37 -0
- package/dist/core/Prompt.js.map +1 -1
- package/dist/core/Runner.d.ts +37 -4
- package/dist/core/Runner.d.ts.map +1 -1
- package/dist/core/Runner.js +282 -73
- package/dist/core/Runner.js.map +1 -1
- package/dist/core/Speak.d.ts.map +1 -1
- package/dist/core/Speak.js +64 -20
- package/dist/core/Speak.js.map +1 -1
- package/dist/core/Understand.d.ts +6 -3
- package/dist/core/Understand.d.ts.map +1 -1
- package/dist/core/Understand.js +18 -42
- package/dist/core/Understand.js.map +1 -1
- package/dist/core/contracts.d.ts +26 -6
- package/dist/core/contracts.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/persistence/OpenSearchStore.d.ts +2 -1
- package/dist/persistence/OpenSearchStore.d.ts.map +1 -1
- package/dist/persistence/OpenSearchStore.js +2 -2
- package/dist/persistence/OpenSearchStore.js.map +1 -1
- package/dist/persistence/RedisStore.d.ts +1 -1
- package/dist/persistence/RedisStore.d.ts.map +1 -1
- package/dist/providers/AnthropicProvider.d.ts +2 -5
- package/dist/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/providers/AnthropicProvider.js +3 -4
- package/dist/providers/AnthropicProvider.js.map +1 -1
- package/dist/providers/DeepSeekProvider.d.ts.map +1 -1
- package/dist/providers/DeepSeekProvider.js +3 -4
- package/dist/providers/DeepSeekProvider.js.map +1 -1
- package/dist/providers/FallbackAiProvider.js +1 -1
- package/dist/providers/FallbackAiProvider.js.map +1 -1
- package/dist/providers/GeminiProvider.d.ts +1 -2
- package/dist/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/providers/GeminiProvider.js +3 -4
- package/dist/providers/GeminiProvider.js.map +1 -1
- package/dist/providers/GenericOpenAICompatibleProvider.js +4 -4
- package/dist/providers/GenericOpenAICompatibleProvider.js.map +1 -1
- package/dist/providers/OpenAIProvider.d.ts.map +1 -1
- package/dist/providers/OpenAIProvider.js +3 -4
- package/dist/providers/OpenAIProvider.js.map +1 -1
- package/dist/providers/OpenRouterProvider.d.ts.map +1 -1
- package/dist/providers/OpenRouterProvider.js +4 -3
- package/dist/providers/OpenRouterProvider.js.map +1 -1
- package/dist/providers/ProviderAdapter.d.ts +26 -11
- package/dist/providers/ProviderAdapter.d.ts.map +1 -1
- package/dist/providers/ProviderAdapter.js +14 -14
- package/dist/providers/ProviderAdapter.js.map +1 -1
- package/dist/providers/ZaiProvider.js +1 -1
- package/dist/providers/ZaiProvider.js.map +1 -1
- package/dist/types/agent.d.ts +14 -3
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/ai.d.ts +10 -6
- package/dist/types/ai.d.ts.map +1 -1
- package/dist/types/compaction.d.ts +1 -0
- package/dist/types/compaction.d.ts.map +1 -1
- package/dist/types/errors.d.ts +4 -9
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/errors.js +12 -12
- package/dist/types/errors.js.map +1 -1
- package/dist/types/flow.d.ts +17 -1
- package/dist/types/flow.d.ts.map +1 -1
- package/dist/types/history.d.ts +0 -7
- package/dist/types/history.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/session.d.ts +4 -2
- package/dist/types/session.d.ts.map +1 -1
- package/dist/utils/clock.js +1 -1
- package/dist/utils/clock.js.map +1 -1
- package/dist/utils/outcomes.d.ts +2 -0
- package/dist/utils/outcomes.d.ts.map +1 -1
- package/dist/utils/outcomes.js +2 -0
- package/dist/utils/outcomes.js.map +1 -1
- package/dist/utils/schema.d.ts +3 -12
- package/dist/utils/schema.d.ts.map +1 -1
- package/dist/utils/schema.js +3 -42
- package/dist/utils/schema.js.map +1 -1
- package/dist/utils/template.d.ts.map +1 -1
- package/dist/utils/template.js +9 -2
- package/dist/utils/template.js.map +1 -1
- package/docs/concepts/architecture.md +2 -2
- package/docs/concepts/collection.md +40 -5
- package/docs/concepts/pipeline.md +10 -7
- package/docs/concepts/runs-and-waits.md +2 -2
- package/docs/guides/actions-and-events.md +1 -1
- package/docs/guides/branching.md +4 -2
- package/docs/guides/compaction.md +2 -2
- package/docs/guides/error-handling.md +2 -2
- package/docs/guides/flow-control.md +4 -2
- package/docs/guides/persistence.md +2 -2
- package/docs/guides/testing.md +1 -1
- package/docs/guides/triggers.md +3 -3
- package/docs/migration/v3-to-v4.md +8 -7
- package/docs/reference/actions-events-conditions.md +1 -1
- package/docs/reference/agent.md +9 -5
- package/docs/reference/branches.md +1 -1
- package/docs/reference/errors.md +14 -11
- package/docs/reference/fields.md +5 -3
- package/docs/reference/flow-spec.md +35 -8
- package/docs/reference/flow.md +8 -3
- package/docs/reference/outcomes.md +4 -2
- package/docs/reference/providers.md +5 -3
- package/docs/reference/session.md +2 -0
- package/docs/reference/step.md +9 -5
- package/docs/reference/stores.md +5 -3
- package/docs/reference/trigger.md +2 -2
- package/docs/rfc/v4-one-flow.md +5 -3
- package/docs/start/01-install.md +2 -0
- package/docs/start/05-go-to-production.md +18 -1
- package/examples/05-branches.ts +1 -1
- package/examples/06-triggers-and-waits.ts +4 -3
- package/package.json +4 -3
- package/src/core/Agent.ts +23 -2
- package/src/core/CompactionEngine.ts +25 -9
- package/src/core/FlowSpec.ts +311 -72
- package/src/core/Migrate.ts +2 -1
- package/src/core/Prompt.ts +40 -0
- package/src/core/Runner.ts +279 -69
- package/src/core/Speak.ts +67 -19
- package/src/core/Understand.ts +24 -44
- package/src/core/contracts.ts +26 -4
- package/src/index.ts +1 -1
- package/src/persistence/OpenSearchStore.ts +3 -2
- package/src/persistence/RedisStore.ts +1 -1
- package/src/providers/AnthropicProvider.ts +4 -9
- package/src/providers/DeepSeekProvider.ts +2 -4
- package/src/providers/FallbackAiProvider.ts +1 -1
- package/src/providers/GeminiProvider.ts +3 -6
- package/src/providers/GenericOpenAICompatibleProvider.ts +4 -4
- package/src/providers/OpenAIProvider.ts +3 -4
- package/src/providers/OpenRouterProvider.ts +4 -2
- package/src/providers/ProviderAdapter.ts +35 -33
- package/src/providers/ZaiProvider.ts +1 -1
- package/src/types/agent.ts +15 -4
- package/src/types/ai.ts +10 -6
- package/src/types/compaction.ts +1 -0
- package/src/types/errors.ts +11 -12
- package/src/types/flow.ts +17 -1
- package/src/types/history.ts +0 -10
- package/src/types/index.ts +1 -1
- package/src/types/session.ts +5 -1
- package/src/utils/clock.ts +1 -1
- package/src/utils/outcomes.ts +2 -0
- package/src/utils/schema.ts +3 -48
- package/src/utils/template.ts +9 -2
- package/dist/cjs/providers/index.d.ts +0 -26
- package/dist/cjs/providers/index.d.ts.map +0 -1
- package/dist/cjs/providers/index.js +0 -30
- package/dist/cjs/providers/index.js.map +0 -1
- package/dist/cjs/utils/clone.d.ts +0 -8
- package/dist/cjs/utils/clone.d.ts.map +0 -1
- package/dist/cjs/utils/clone.js +0 -32
- package/dist/cjs/utils/clone.js.map +0 -1
- package/dist/cjs/utils/index.d.ts +0 -9
- package/dist/cjs/utils/index.d.ts.map +0 -1
- package/dist/cjs/utils/index.js +0 -29
- package/dist/cjs/utils/index.js.map +0 -1
- package/dist/providers/index.d.ts +0 -26
- package/dist/providers/index.d.ts.map +0 -1
- package/dist/providers/index.js +0 -17
- package/dist/providers/index.js.map +0 -1
- package/dist/utils/clone.d.ts +0 -8
- package/dist/utils/clone.d.ts.map +0 -1
- package/dist/utils/clone.js +0 -29
- package/dist/utils/clone.js.map +0 -1
- package/dist/utils/index.d.ts +0 -9
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -9
- package/dist/utils/index.js.map +0 -1
- package/src/providers/index.ts +0 -38
- package/src/utils/clone.ts +0 -34
- package/src/utils/index.ts +0 -18
package/dist/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) {
|
|
@@ -453,20 +482,31 @@ class Runner {
|
|
|
453
482
|
}
|
|
454
483
|
const fields = {};
|
|
455
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
|
+
};
|
|
456
489
|
for (const flow of [...(floorFlow ? [floorFlow] : []), ...eligible]) {
|
|
457
|
-
for (const
|
|
458
|
-
|
|
459
|
-
continue;
|
|
460
|
-
for (const field of step.collect ?? []) {
|
|
461
|
-
const def = this.options.fields[field];
|
|
462
|
-
if (def && !(0, schema_js_1.isKnown)(data[field]) && (0, schema_js_1.extractMode)(def) === "anywhere")
|
|
463
|
-
fields[field] = def;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
490
|
+
for (const field of flowFields(flow).filter(harvestable))
|
|
491
|
+
fields[field] = this.options.fields[field];
|
|
466
492
|
}
|
|
467
|
-
|
|
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];
|
|
505
|
+
}
|
|
506
|
+
if (!judging && !Object.keys(fields).length)
|
|
468
507
|
return null;
|
|
469
508
|
return {
|
|
509
|
+
sessionId: turn.session.id,
|
|
470
510
|
text: turn.what.text,
|
|
471
511
|
history: this.historyOf(turn),
|
|
472
512
|
context: turn.context,
|
|
@@ -530,6 +570,8 @@ class Runner {
|
|
|
530
570
|
return;
|
|
531
571
|
turn.llmCalls += understanding?.llmCalls ?? 0;
|
|
532
572
|
turn.usage = (0, usage_js_1.addUsage)(turn.usage, understanding?.usage);
|
|
573
|
+
if (understanding?.asks !== undefined)
|
|
574
|
+
turn.asks = understanding.asks;
|
|
533
575
|
const key = turn.triggerKey;
|
|
534
576
|
const asker = this.asker(turn);
|
|
535
577
|
// Routing is decided before extraction lands so `clearOnStart` runs first, and applied after mention runs.
|
|
@@ -573,6 +615,9 @@ class Runner {
|
|
|
573
615
|
turn.floorRunId = run.id;
|
|
574
616
|
}
|
|
575
617
|
}
|
|
618
|
+
// Unless routing or Ingest moved it, the message is the asker's: written down, so a chain beside it cannot take it.
|
|
619
|
+
if (asker && !route && !turn.floorFromIngest)
|
|
620
|
+
turn.floorRunId = asker.id;
|
|
576
621
|
for (const [field, raw] of Object.entries(understanding?.fields ?? {}))
|
|
577
622
|
this.writeField(turn, field, raw);
|
|
578
623
|
if (asker && turn.session.runs.includes(asker) && asker.status === "asking") {
|
|
@@ -583,7 +628,13 @@ class Runner {
|
|
|
583
628
|
}
|
|
584
629
|
}
|
|
585
630
|
fireBranch(turn, run, flow, step, understanding) {
|
|
586
|
-
|
|
631
|
+
// Staying, a run does not take an `if` branch back onto a path it has already run: the fact it tests is still true,
|
|
632
|
+
// so it would re-run that path on every message. A `when` branch is judged on each new message and always counts.
|
|
633
|
+
// 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`.
|
|
634
|
+
const ran = (then) => typeof then === "string" ? then === "end" || (run.visits[then] ?? 0) > 0 : "step" in then && (run.visits[then.step] ?? 0) > 0;
|
|
635
|
+
const hit = (branch, index) => "if" in branch
|
|
636
|
+
? !(run.staying && ran(branch.then)) && this.holds(branch.if, turn, run)
|
|
637
|
+
: understanding?.branches[`${run.id}/${step.id}/${index}`] === true;
|
|
587
638
|
const index = (step.branches ?? []).findIndex(hit);
|
|
588
639
|
if (index < 0)
|
|
589
640
|
return;
|
|
@@ -613,10 +664,64 @@ class Runner {
|
|
|
613
664
|
async advance(turn) {
|
|
614
665
|
if (turn.ignored)
|
|
615
666
|
return null;
|
|
616
|
-
this.
|
|
667
|
+
this.resume(turn);
|
|
617
668
|
turn.queue = [...turn.session.runs];
|
|
618
669
|
await this.drain(turn);
|
|
619
|
-
|
|
670
|
+
// An asker that moved on without a word hands the message to the run it suspended, and that one to the next.
|
|
671
|
+
// Each pass takes a run off the suspended stack, so the stack's size bounds the loop.
|
|
672
|
+
for (let left = turn.session.runs.length; left > 0; left--) {
|
|
673
|
+
if (turn.what.kind !== "message" || turn.silenced !== undefined || turn.talk || turn.spokeBy.size > 0)
|
|
674
|
+
break;
|
|
675
|
+
const resumed = this.resume(turn);
|
|
676
|
+
if (!resumed)
|
|
677
|
+
break;
|
|
678
|
+
turn.queue.push(resumed);
|
|
679
|
+
await this.drain(turn);
|
|
680
|
+
}
|
|
681
|
+
const speaker = this.speaker(turn);
|
|
682
|
+
if (!speaker || "idle" in speaker || !this.fixedDue(speaker))
|
|
683
|
+
return speaker;
|
|
684
|
+
// A question the lead asked gets its answer: the step speaks once, answering it and asking the fixed question.
|
|
685
|
+
// Appending the text word for word after an answer could repeat or contradict it. Unjudged counts as asked,
|
|
686
|
+
// and so does the retry of a failed answer: a question sent over the lead's own drops it.
|
|
687
|
+
const answerFirst = turn.what.kind === "message" ? turn.asks !== false : this.retrying(speaker);
|
|
688
|
+
if (answerFirst && speaker.step.question !== undefined) {
|
|
689
|
+
speaker.fixedQuestion = (0, template_js_1.render)(speaker.step.question, this.scope(turn, speaker.run));
|
|
690
|
+
return speaker;
|
|
691
|
+
}
|
|
692
|
+
this.askFixed(turn, speaker);
|
|
693
|
+
turn.talk = undefined;
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
/** The step failed at the provider and runs again on its retry wake: deferTalk left `deferred` as its last line. */
|
|
697
|
+
retrying({ run, step }) {
|
|
698
|
+
const last = run.outcomes.at(-1);
|
|
699
|
+
return last?.status === "deferred" && last.stepId === step.id;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* The step's fixed question is due: this is its first ask, with every field it collects still unknown and none asked
|
|
703
|
+
* yet, and the run is not staying (a staying run answers the lead). Otherwise the AI phrases the ask.
|
|
704
|
+
*/
|
|
705
|
+
fixedDue({ run, step, pending }) {
|
|
706
|
+
const { question, collect = [] } = step;
|
|
707
|
+
if (question === undefined || run.staying)
|
|
708
|
+
return false;
|
|
709
|
+
return pending.length === collect.length && pending.every((field) => (run.asked[field] ?? 0) === 0);
|
|
710
|
+
}
|
|
711
|
+
/** Send the step's fixed question when it is due. It counts as one ask of each field. */
|
|
712
|
+
askFixed(turn, talk) {
|
|
713
|
+
const { run, step, pending } = talk;
|
|
714
|
+
if (!this.fixedDue(talk) || step.question === undefined)
|
|
715
|
+
return false;
|
|
716
|
+
const key = this.stepKey(run, step);
|
|
717
|
+
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 });
|
|
718
|
+
turn.afterMs = 0;
|
|
719
|
+
turn.spokeBy.add(run.id);
|
|
720
|
+
turn.spoke = true;
|
|
721
|
+
for (const field of pending)
|
|
722
|
+
run.asked[field] = (run.asked[field] ?? 0) + 1;
|
|
723
|
+
this.outcome(turn, run, { kind: "collect", status: "ok", key, code: "asked-fixed", stepId: step.id });
|
|
724
|
+
return true;
|
|
620
725
|
}
|
|
621
726
|
/**
|
|
622
727
|
* Everything Speak needs for this turn's one speaker. Instructions are agent,
|
|
@@ -639,12 +744,15 @@ class Runner {
|
|
|
639
744
|
const instructions = [...(this.options.instructions ?? []), ...own.instructions].filter((ins) => !ins.if || (0, predicate_js_1.evaluate)(ins.if, ctx, conditions));
|
|
640
745
|
const all = this.options.tools ?? [];
|
|
641
746
|
const allowed = own.tools;
|
|
747
|
+
const retry = !("idle" in talk) && this.retrying(talk);
|
|
642
748
|
return {
|
|
749
|
+
sessionId: turn.session.id,
|
|
643
750
|
talk,
|
|
644
|
-
input: turn.what.kind === "message" ? { kind: turn.kind, text: turn.what.text } : { kind: turn.kind },
|
|
751
|
+
input: turn.what.kind === "message" ? { kind: turn.kind, text: turn.what.text } : { kind: turn.kind, ...(retry ? { retry } : {}) },
|
|
645
752
|
context: turn.context,
|
|
646
753
|
data: turn.session.data,
|
|
647
754
|
history: this.historyOf(turn),
|
|
755
|
+
said: [...turn.messages],
|
|
648
756
|
now: turn.now,
|
|
649
757
|
instructions,
|
|
650
758
|
tools: allowed ? all.filter((tool) => allowed.includes(tool.id)) : all,
|
|
@@ -687,6 +795,11 @@ class Runner {
|
|
|
687
795
|
}
|
|
688
796
|
if (!this.premiseHolds(turn, run, flow))
|
|
689
797
|
return;
|
|
798
|
+
// The flow was edited off 'stay' after this run finished its steps: its new `onEnd` decides now.
|
|
799
|
+
if (run.staying && flow.onEnd !== "stay") {
|
|
800
|
+
this.finishFlow(turn, run, flow);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
690
803
|
const resuming = run.status === "asking";
|
|
691
804
|
run.status = "running";
|
|
692
805
|
if (run.stepId === null) {
|
|
@@ -729,6 +842,11 @@ class Runner {
|
|
|
729
842
|
this.endRun(turn, run, "skipped", "premise-changed");
|
|
730
843
|
return ok;
|
|
731
844
|
}
|
|
845
|
+
/** `silenced: { skip: true }`: the message this step would send is dropped, and the run takes `then` as if it had gone out. */
|
|
846
|
+
skipSilenced(turn, run, flow, step, kind) {
|
|
847
|
+
this.outcome(turn, run, { kind, status: "skipped", key: this.stepKey(run, step), code: "silenced", detail: turn.silenced, next: nextLabel(step.then) });
|
|
848
|
+
this.follow(turn, run, flow, step, step.then);
|
|
849
|
+
}
|
|
732
850
|
async execute(turn, run, flow, step, resuming) {
|
|
733
851
|
const data = turn.session.data;
|
|
734
852
|
const key = this.stepKey(run, step);
|
|
@@ -737,7 +855,9 @@ class Runner {
|
|
|
737
855
|
let pending = [];
|
|
738
856
|
if (step.collect?.length) {
|
|
739
857
|
pending = (0, schema_js_1.pendingFields)(step, data, run.asked);
|
|
740
|
-
|
|
858
|
+
// A `stay` run answers even with nothing left to collect: answering is what it stays for. It only runs on a message
|
|
859
|
+
// (asking runs sit out wakes and events), and on a silenced one the check below keeps it asking.
|
|
860
|
+
if (!pending.length && !run.staying) {
|
|
741
861
|
this.reportMaxAsks(turn, run, step);
|
|
742
862
|
this.outcome(turn, run, resuming
|
|
743
863
|
? { kind, status: "ok", key, next: nextLabel(step.then) }
|
|
@@ -749,6 +869,8 @@ class Runner {
|
|
|
749
869
|
if (turn.silenced !== undefined) {
|
|
750
870
|
if (resuming)
|
|
751
871
|
run.status = "asking"; // the gate is closed; it speaks when the gate opens
|
|
872
|
+
else if (turn.skipSilenced)
|
|
873
|
+
this.skipSilenced(turn, run, flow, step, kind);
|
|
752
874
|
else
|
|
753
875
|
this.endRun(turn, run, "skipped", "silenced", kind, turn.silenced);
|
|
754
876
|
return;
|
|
@@ -760,13 +882,19 @@ class Runner {
|
|
|
760
882
|
}
|
|
761
883
|
}
|
|
762
884
|
run.status = "asking";
|
|
885
|
+
// 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.
|
|
763
886
|
if (!turn.speakDone)
|
|
764
887
|
turn.talk = { run, flow, step, pending };
|
|
888
|
+
else
|
|
889
|
+
this.askFixed(turn, { run, flow, step, pending });
|
|
765
890
|
return;
|
|
766
891
|
}
|
|
767
892
|
if (isSay(step)) {
|
|
768
893
|
if (turn.silenced !== undefined) {
|
|
769
|
-
|
|
894
|
+
if (turn.skipSilenced)
|
|
895
|
+
this.skipSilenced(turn, run, flow, step, "say");
|
|
896
|
+
else
|
|
897
|
+
this.endRun(turn, run, "skipped", "silenced", "say", turn.silenced);
|
|
770
898
|
return;
|
|
771
899
|
}
|
|
772
900
|
if (step.once) {
|
|
@@ -813,6 +941,13 @@ class Runner {
|
|
|
813
941
|
result = { failed: error instanceof Error ? error.message : String(error) };
|
|
814
942
|
}
|
|
815
943
|
}
|
|
944
|
+
// The handler already ran, so a defer that cannot be parsed must not throw
|
|
945
|
+
// the turn: every replay would repeat the side effect and fail again.
|
|
946
|
+
if ("defer" in result && !(0, duration_js_1.isDuration)(String(result.defer))) {
|
|
947
|
+
result = {
|
|
948
|
+
failed: `action "${step.do}" asked to defer by "${String(result.defer)}", which is not a duration. Use a value like "2m" or "1h".`,
|
|
949
|
+
};
|
|
950
|
+
}
|
|
816
951
|
if ("ok" in result) {
|
|
817
952
|
if (result.spoke) {
|
|
818
953
|
turn.spokeBy.add(run.id);
|
|
@@ -904,18 +1039,21 @@ class Runner {
|
|
|
904
1039
|
}
|
|
905
1040
|
if ("step" in target) {
|
|
906
1041
|
const data = turn.session.data;
|
|
907
|
-
|
|
1042
|
+
// Forgetting a field means asking for it from scratch: its ask count goes too, so a fixed question goes out again.
|
|
1043
|
+
for (const field of target.clear ?? []) {
|
|
908
1044
|
delete data[field];
|
|
1045
|
+
delete run.asked[field];
|
|
1046
|
+
}
|
|
909
1047
|
this.jump(turn, run, flow, target.step);
|
|
910
1048
|
return;
|
|
911
1049
|
}
|
|
912
1050
|
const childId = (0, template_js_1.render)(target.flow, this.scope(turn, run));
|
|
913
1051
|
const key = this.stepKey(run, step);
|
|
914
1052
|
this.endRun(turn, run, "flow");
|
|
915
|
-
this.chain(turn, childId, key, target.input ?? run.input, run.hop + 1);
|
|
1053
|
+
this.chain(turn, run, childId, key, target.input ?? run.input, run.hop + 1);
|
|
916
1054
|
}
|
|
917
|
-
/** Start a child flow that
|
|
918
|
-
chain(turn, flowId, key, input, hop, keepData = false) {
|
|
1055
|
+
/** Start a child flow that moves in this same phase. It inherits the floor when its parent held it, or when nobody did. */
|
|
1056
|
+
chain(turn, parent, flowId, key, input, hop, keepData = false) {
|
|
919
1057
|
const child = this.flows.get(flowId);
|
|
920
1058
|
if (!child) {
|
|
921
1059
|
turn.skipped.push({ flowId, anchor: turn.session.id, triggerKey: key, code: "flow-gone", message: outcomes_js_1.OUTCOME_MESSAGES["flow-gone"] });
|
|
@@ -924,9 +1062,11 @@ class Runner {
|
|
|
924
1062
|
const run = this.startRun(turn, child, "flow", key, { payload: input, hop, keepData });
|
|
925
1063
|
if (!run)
|
|
926
1064
|
return;
|
|
927
|
-
turn.floorRunId
|
|
928
|
-
|
|
929
|
-
turn.
|
|
1065
|
+
if (turn.floorRunId === undefined || turn.floorRunId === parent.id) {
|
|
1066
|
+
turn.floorRunId = run.id;
|
|
1067
|
+
if (turn.ingesting)
|
|
1068
|
+
turn.floorFromIngest = true;
|
|
1069
|
+
}
|
|
930
1070
|
turn.queue.push(run);
|
|
931
1071
|
}
|
|
932
1072
|
jump(turn, run, flow, stepId) {
|
|
@@ -941,14 +1081,54 @@ class Runner {
|
|
|
941
1081
|
run.visits[stepId] = (run.visits[stepId] ?? 0) + 1;
|
|
942
1082
|
run.status = "running";
|
|
943
1083
|
delete run.waiting;
|
|
1084
|
+
delete run.staying;
|
|
1085
|
+
}
|
|
1086
|
+
/** `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. */
|
|
1087
|
+
stayAt(run, step) {
|
|
1088
|
+
this.enter(run, step.id);
|
|
1089
|
+
run.staying = true;
|
|
1090
|
+
run.status = "asking";
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* 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.
|
|
1094
|
+
* 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
|
|
1095
|
+
* stays on the flow's last talk step. Upgrade: record the talk step on the run when it talks.
|
|
1096
|
+
*/
|
|
1097
|
+
stayStep(run, flow) {
|
|
1098
|
+
for (let i = run.outcomes.length - 1; i >= 0; i--) {
|
|
1099
|
+
const { kind, stepId } = run.outcomes[i];
|
|
1100
|
+
const step = kind === "prompt" || kind === "collect" ? this.stepOf(flow, stepId ?? null) : undefined;
|
|
1101
|
+
if (step && isTalk(step))
|
|
1102
|
+
return step;
|
|
1103
|
+
}
|
|
1104
|
+
return [...flow.steps].reverse().find(isTalk);
|
|
944
1105
|
}
|
|
945
1106
|
finishFlow(turn, run, flow) {
|
|
946
1107
|
const onEnd = flow.onEnd ?? "end";
|
|
947
1108
|
const last = flow.steps[flow.steps.length - 1];
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
run
|
|
1109
|
+
const stay = onEnd === "stay" ? this.stayStep(run, flow) : undefined;
|
|
1110
|
+
if (stay) {
|
|
1111
|
+
// The steps after it ran once, on the way here; staying, it only answers.
|
|
1112
|
+
this.stayAt(run, stay);
|
|
1113
|
+
// One asker at a time. A run the lead's message went to holds the conversation and suspends the other asker;
|
|
1114
|
+
// any other run waits behind that asker and resumes when it is done.
|
|
1115
|
+
const others = turn.session.runs.filter((r) => r !== run && r.status === "asking");
|
|
1116
|
+
if (others.length && !(turn.what.kind === "message" && turn.floorRunId === run.id)) {
|
|
1117
|
+
run.status = "suspended";
|
|
1118
|
+
run.suspendedAt = turn.nowIso;
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
for (const other of others) {
|
|
1122
|
+
other.status = "suspended";
|
|
1123
|
+
other.suspendedAt = turn.nowIso;
|
|
1124
|
+
if (turn.talk?.run === other)
|
|
1125
|
+
turn.talk = undefined;
|
|
1126
|
+
}
|
|
1127
|
+
// A message nothing has answered yet gets its answer now. Reached in Ingest, the run stays asking: Decide judges its
|
|
1128
|
+
// branches as the asker's, and it answers when it moves.
|
|
1129
|
+
if (!turn.ingesting && turn.what.kind === "message" && turn.silenced === undefined && !turn.speakDone && turn.spokeBy.size === 0) {
|
|
1130
|
+
run.status = "running";
|
|
1131
|
+
}
|
|
952
1132
|
return;
|
|
953
1133
|
}
|
|
954
1134
|
if (onEnd === "reset" && last) {
|
|
@@ -956,7 +1136,7 @@ class Runner {
|
|
|
956
1136
|
// 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.
|
|
957
1137
|
const key = this.stepKey(run, last);
|
|
958
1138
|
this.endRun(turn, run, "reset");
|
|
959
|
-
this.chain(turn, flow.id, key, run.input, run.hop + 1, true);
|
|
1139
|
+
this.chain(turn, run, flow.id, key, run.input, run.hop + 1, true);
|
|
960
1140
|
return;
|
|
961
1141
|
}
|
|
962
1142
|
this.endRun(turn, run, "end");
|
|
@@ -978,10 +1158,21 @@ class Runner {
|
|
|
978
1158
|
run.waiting = { ...waiting, until: at.toISOString(), setAt: turn.nowIso };
|
|
979
1159
|
turn.schedule.push({ key: waiting.key, at });
|
|
980
1160
|
}
|
|
1161
|
+
/** `resumeSuspended`, and on a message the resumed run's `if` branches are judged before it speaks, as the asker's are in Decide. */
|
|
1162
|
+
resume(turn) {
|
|
1163
|
+
const run = this.resumeSuspended(turn);
|
|
1164
|
+
if (run && turn.what.kind === "message") {
|
|
1165
|
+
const flow = this.flows.get(run.flowId);
|
|
1166
|
+
const step = flow && this.stepOf(flow, run.stepId);
|
|
1167
|
+
if (flow && step && isTalk(step))
|
|
1168
|
+
this.fireBranch(turn, run, flow, step, null);
|
|
1169
|
+
}
|
|
1170
|
+
return run;
|
|
1171
|
+
}
|
|
981
1172
|
/** When nobody asks, the most recently suspended run returns to asking. */
|
|
982
1173
|
resumeSuspended(turn) {
|
|
983
1174
|
if (this.asker(turn))
|
|
984
|
-
return;
|
|
1175
|
+
return undefined;
|
|
985
1176
|
const suspendedAt = (r) => r.suspendedAt ?? r.startedAt;
|
|
986
1177
|
const next = turn.session.runs
|
|
987
1178
|
.filter((r) => r.status === "suspended")
|
|
@@ -990,6 +1181,7 @@ class Runner {
|
|
|
990
1181
|
next.status = "asking";
|
|
991
1182
|
delete next.suspendedAt;
|
|
992
1183
|
}
|
|
1184
|
+
return next;
|
|
993
1185
|
}
|
|
994
1186
|
// ── Settle: the one applier after Speak (design §4.7) ─────────────────
|
|
995
1187
|
async settle(turn, outcome) {
|
|
@@ -1041,19 +1233,28 @@ class Runner {
|
|
|
1041
1233
|
for (const [field, raw] of Object.entries(spoken.fields))
|
|
1042
1234
|
this.writeField(turn, field, raw);
|
|
1043
1235
|
Object.assign(turn.session.data, spoken.data);
|
|
1044
|
-
|
|
1236
|
+
const fixed = talk.fixedQuestion !== undefined ? { code: "fixed-in-reply" } : {};
|
|
1237
|
+
this.outcome(turn, run, { kind: talkKind(step), status: "ok", key, llmCalls: spoken.llmCalls, stepId: step.id, ...fixed });
|
|
1045
1238
|
if (!turn.session.runs.includes(run))
|
|
1046
1239
|
return;
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1240
|
+
const pending = step.collect?.length ? (0, schema_js_1.pendingFields)(step, turn.session.data, run.asked) : [];
|
|
1241
|
+
for (const field of pending)
|
|
1242
|
+
run.asked[field] = (run.asked[field] ?? 0) + 1;
|
|
1243
|
+
if (run.staying) {
|
|
1244
|
+
// 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.
|
|
1245
|
+
const maxAsks = step.maxAsks ?? schema_js_1.DEFAULT_MAX_ASKS;
|
|
1246
|
+
for (const field of pending) {
|
|
1247
|
+
if (run.asked[field] === maxAsks)
|
|
1248
|
+
this.outcome(turn, run, { kind: "collect", status: "skipped", key, code: "max-asks", detail: field });
|
|
1054
1249
|
}
|
|
1055
|
-
this.
|
|
1250
|
+
this.stayAt(run, step);
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
if (pending.length) {
|
|
1254
|
+
run.status = "asking";
|
|
1255
|
+
return;
|
|
1056
1256
|
}
|
|
1257
|
+
this.reportMaxAsks(turn, run, step);
|
|
1057
1258
|
run.status = "running";
|
|
1058
1259
|
this.follow(turn, run, flow, step, step.then);
|
|
1059
1260
|
if (turn.session.runs.includes(run) && run.status === "running")
|
|
@@ -1090,16 +1291,23 @@ class Runner {
|
|
|
1090
1291
|
const reset = deferred.resetAtMs;
|
|
1091
1292
|
return new Date(reset !== undefined && reset > ladder ? reset : ladder);
|
|
1092
1293
|
}
|
|
1093
|
-
/** The assistant spoke
|
|
1294
|
+
/** The assistant spoke this turn: its silence wakes replace the ones its last words set. */
|
|
1094
1295
|
armSilence(turn) {
|
|
1296
|
+
const previous = turn.original?.lastAssistantAt;
|
|
1297
|
+
if (turn.session.lastAssistantAt === previous)
|
|
1298
|
+
return;
|
|
1299
|
+
turn.schedule.push(...this.silenceWakes(turn, previous));
|
|
1300
|
+
}
|
|
1301
|
+
/** The assistant spoke last: every silence flow passing `if` and `repeat` gets a wake. */
|
|
1302
|
+
silenceWakes(turn, previous) {
|
|
1095
1303
|
const { session } = turn;
|
|
1096
1304
|
const { lastAssistantAt, lastUserAt } = session;
|
|
1097
|
-
if (!lastAssistantAt
|
|
1098
|
-
return;
|
|
1305
|
+
if (!lastAssistantAt)
|
|
1306
|
+
return [];
|
|
1099
1307
|
if (lastUserAt && Date.parse(lastUserAt) > Date.parse(lastAssistantAt))
|
|
1100
|
-
return;
|
|
1308
|
+
return [];
|
|
1101
1309
|
const ms = Date.parse(lastAssistantAt);
|
|
1102
|
-
const
|
|
1310
|
+
const wakes = [];
|
|
1103
1311
|
for (const flow of this.flows.values()) {
|
|
1104
1312
|
const trigger = (flow.on ?? []).find((t) => "silence" in t);
|
|
1105
1313
|
if (!trigger)
|
|
@@ -1110,12 +1318,13 @@ class Runner {
|
|
|
1110
1318
|
if (!this.repeatAllows(turn, flow, trigger, key))
|
|
1111
1319
|
continue;
|
|
1112
1320
|
const at = this.snap(turn, new Date(ms + (0, duration_js_1.parseDuration)(trigger.silence)), trigger.businessHours);
|
|
1113
|
-
|
|
1321
|
+
wakes.push({
|
|
1114
1322
|
key: `silence:${flow.id}:${session.id}:${ms}`,
|
|
1115
1323
|
at,
|
|
1116
1324
|
...(previous ? { replaces: `silence:${flow.id}:${session.id}:${Date.parse(previous)}` } : {}),
|
|
1117
1325
|
});
|
|
1118
1326
|
}
|
|
1327
|
+
return wakes;
|
|
1119
1328
|
}
|
|
1120
1329
|
// ── Return (design §4.8) ──────────────────────────────────────────────
|
|
1121
1330
|
finish(turn) {
|