@falai/agent 4.0.0-alpha.1 → 4.0.0-alpha.11
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/dist/cjs/core/Agent.d.ts +8 -1
- package/dist/cjs/core/Agent.d.ts.map +1 -1
- package/dist/cjs/core/Agent.js +9 -0
- package/dist/cjs/core/Agent.js.map +1 -1
- package/dist/cjs/core/FlowSpec.d.ts.map +1 -1
- package/dist/cjs/core/FlowSpec.js +53 -2
- package/dist/cjs/core/FlowSpec.js.map +1 -1
- package/dist/cjs/core/Prompt.d.ts.map +1 -1
- package/dist/cjs/core/Prompt.js +7 -1
- package/dist/cjs/core/Prompt.js.map +1 -1
- package/dist/cjs/core/Runner.d.ts +14 -3
- package/dist/cjs/core/Runner.d.ts.map +1 -1
- package/dist/cjs/core/Runner.js +52 -23
- 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 +11 -2
- package/dist/cjs/core/Speak.js.map +1 -1
- package/dist/cjs/core/Understand.d.ts.map +1 -1
- package/dist/cjs/core/Understand.js +17 -13
- package/dist/cjs/core/Understand.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/providers/ProviderAdapter.d.ts +10 -5
- package/dist/cjs/providers/ProviderAdapter.d.ts.map +1 -1
- package/dist/cjs/providers/ProviderAdapter.js.map +1 -1
- package/dist/cjs/providers/ZaiProvider.d.ts +6 -4
- package/dist/cjs/providers/ZaiProvider.d.ts.map +1 -1
- package/dist/cjs/providers/ZaiProvider.js +6 -4
- package/dist/cjs/providers/ZaiProvider.js.map +1 -1
- package/dist/cjs/types/agent.d.ts +12 -2
- package/dist/cjs/types/agent.d.ts.map +1 -1
- package/dist/cjs/types/index.d.ts +1 -1
- package/dist/cjs/types/index.d.ts.map +1 -1
- package/dist/cjs/types/index.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/template.d.ts +8 -0
- package/dist/cjs/utils/template.d.ts.map +1 -1
- package/dist/cjs/utils/template.js +40 -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 +9 -0
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/FlowSpec.d.ts.map +1 -1
- package/dist/core/FlowSpec.js +53 -2
- package/dist/core/FlowSpec.js.map +1 -1
- package/dist/core/Prompt.d.ts.map +1 -1
- package/dist/core/Prompt.js +7 -1
- package/dist/core/Prompt.js.map +1 -1
- package/dist/core/Runner.d.ts +14 -3
- package/dist/core/Runner.d.ts.map +1 -1
- package/dist/core/Runner.js +52 -23
- package/dist/core/Runner.js.map +1 -1
- package/dist/core/Speak.d.ts.map +1 -1
- package/dist/core/Speak.js +11 -2
- package/dist/core/Speak.js.map +1 -1
- package/dist/core/Understand.d.ts.map +1 -1
- package/dist/core/Understand.js +17 -13
- package/dist/core/Understand.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/providers/ProviderAdapter.d.ts +10 -5
- package/dist/providers/ProviderAdapter.d.ts.map +1 -1
- package/dist/providers/ProviderAdapter.js.map +1 -1
- package/dist/providers/ZaiProvider.d.ts +6 -4
- package/dist/providers/ZaiProvider.d.ts.map +1 -1
- package/dist/providers/ZaiProvider.js +6 -4
- package/dist/providers/ZaiProvider.js.map +1 -1
- package/dist/types/agent.d.ts +12 -2
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.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/template.d.ts +8 -0
- package/dist/utils/template.d.ts.map +1 -1
- package/dist/utils/template.js +40 -3
- package/dist/utils/template.js.map +1 -1
- package/docs/concepts/architecture.md +2 -2
- package/docs/concepts/pipeline.md +4 -4
- package/docs/guides/actions-and-events.md +1 -1
- package/docs/guides/conditions.md +1 -1
- package/docs/guides/error-handling.md +2 -0
- package/docs/guides/persistence.md +1 -1
- package/docs/guides/testing.md +1 -1
- package/docs/guides/triggers.md +5 -5
- package/docs/migration/v3-to-v4.md +12 -7
- package/docs/reference/actions-events-conditions.md +2 -2
- package/docs/reference/agent.md +8 -4
- package/docs/reference/flow-spec.md +1 -1
- package/docs/reference/providers.md +2 -2
- package/docs/reference/trigger.md +24 -4
- package/docs/rfc/v4-one-flow.md +5 -5
- package/docs/start/04-add-tools.md +1 -1
- package/docs/start/05-go-to-production.md +16 -1
- package/examples/06-triggers-and-waits.ts +4 -3
- package/package.json +5 -3
- package/src/core/Agent.ts +11 -1
- package/src/core/FlowSpec.ts +72 -6
- package/src/core/Prompt.ts +7 -1
- package/src/core/Runner.ts +59 -24
- package/src/core/Speak.ts +11 -2
- package/src/core/Understand.ts +12 -11
- package/src/index.ts +1 -0
- package/src/providers/ProviderAdapter.ts +10 -5
- package/src/providers/ZaiProvider.ts +6 -4
- package/src/types/agent.ts +13 -2
- package/src/types/index.ts +1 -0
- package/src/utils/phrases.ts +40 -0
- package/src/utils/template.ts +39 -3
package/src/core/Runner.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* begin ─▶ understandRequest ─(Understand)─▶ decide ─▶ advance ─(Speak)─▶ settle ─▶ finish
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type { AgentOptions, EndReason, TurnInput, TurnResult } from "../types/agent.js";
|
|
11
|
+
import type { AgentOptions, EndReason, PendingWakesInput, ScheduleEntry, TurnBase, TurnInput, TurnResult } from "../types/agent.js";
|
|
12
12
|
import type { TokenUsage } from "../types/ai.js";
|
|
13
13
|
import type { ActionResult, Branch, DoStep, Duration, Flow, IfStep, Next, Pred, PredCtx, Repeat, SayStep, Step, StepBase, TalkStep, Trigger, WaitEventStep, WaitStep } from "../types/flow.js";
|
|
14
14
|
import type { History } from "../types/history.js";
|
|
@@ -49,7 +49,7 @@ export type What =
|
|
|
49
49
|
|
|
50
50
|
/** The mutable per-turn state. `session` is a deep copy; the input is never touched. */
|
|
51
51
|
export interface Turn<C = unknown, D = unknown> {
|
|
52
|
-
readonly input:
|
|
52
|
+
readonly input: TurnBase<C, D>;
|
|
53
53
|
readonly what: What;
|
|
54
54
|
readonly kind: InputKind;
|
|
55
55
|
/** Message id, wake key, event key or start key: the trigger key of runs started this turn. */
|
|
@@ -171,11 +171,26 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
171
171
|
// ── Load + Ingest ─────────────────────────────────────────────────────
|
|
172
172
|
|
|
173
173
|
begin(input: TurnInput<C, D>): Turn<C, D> {
|
|
174
|
-
const now =
|
|
174
|
+
const now = this.now();
|
|
175
|
+
const turn = this.open(input, describe(input, now.toISOString()), now);
|
|
176
|
+
if (turn.what.kind === "wake" && !input.session) {
|
|
177
|
+
this.ignore(turn, "no-session", turn.what.key);
|
|
178
|
+
return turn;
|
|
179
|
+
}
|
|
180
|
+
turn.ingesting = true;
|
|
181
|
+
this.ingest(turn);
|
|
182
|
+
turn.ingesting = false;
|
|
183
|
+
return turn;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private now(): Date {
|
|
187
|
+
return (this.options.clock ?? (() => new Date()))();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private open(input: TurnBase<C, D>, what: What, now: Date): Turn<C, D> {
|
|
175
191
|
const nowIso = now.toISOString();
|
|
176
|
-
const what = describe(input, nowIso);
|
|
177
192
|
const silenced = typeof input.silenced === "string" ? input.silenced : input.silenced?.reason;
|
|
178
|
-
|
|
193
|
+
return {
|
|
179
194
|
input,
|
|
180
195
|
what,
|
|
181
196
|
kind: what.kind,
|
|
@@ -204,14 +219,17 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
204
219
|
speakDone: false,
|
|
205
220
|
queue: [],
|
|
206
221
|
};
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
this.
|
|
213
|
-
|
|
214
|
-
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Every wake the session waits on: each parked run's own, then each silence flow's since the assistant last spoke. */
|
|
225
|
+
pendingWakes(input: PendingWakesInput<C, D>): ScheduleEntry[] {
|
|
226
|
+
// No input arrives, so nothing reads `what`: the inert wake shape only fills the field.
|
|
227
|
+
const turn = this.open({ ...input, sessionId: input.session.id }, { kind: "wake", key: "" }, this.now());
|
|
228
|
+
// `park` always sets both; the type leaves them optional, so a hand-built blob without them has no wake to give.
|
|
229
|
+
const parked = turn.session.runs.flatMap(({ status, waiting }) =>
|
|
230
|
+
status === "waiting" && waiting?.key && waiting.until ? [{ key: waiting.key, at: new Date(waiting.until) }] : [],
|
|
231
|
+
);
|
|
232
|
+
return [...parked, ...this.silenceWakes(turn)];
|
|
215
233
|
}
|
|
216
234
|
|
|
217
235
|
private ingest(turn: Turn<C, D>): void {
|
|
@@ -407,9 +425,11 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
407
425
|
}
|
|
408
426
|
session.runs.push(run);
|
|
409
427
|
turn.started.push({ runId: run.id, flowId: flow.id, anchor, dedupeKey });
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
428
|
+
const eventTrigger = opts.trigger && "event" in opts.trigger ? opts.trigger : undefined;
|
|
429
|
+
const after = eventTrigger?.after;
|
|
430
|
+
const at = this.snap(turn, new Date(turn.now.getTime() + (after ? parseDuration(after) : 0)), eventTrigger?.businessHours);
|
|
431
|
+
// With no `after`, only a closed hour parks the start: inside working hours the snap returns now.
|
|
432
|
+
if (after || at.getTime() > turn.now.getTime()) {
|
|
413
433
|
this.park(turn, run, { kind: "timer", key: `${run.id}:start:${at.getTime()}` }, at);
|
|
414
434
|
this.outcome(turn, run, { kind: "wait", status: "waiting", code: "awaiting-trigger", until: at.toISOString() });
|
|
415
435
|
}
|
|
@@ -508,8 +528,7 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
508
528
|
const floorRun = this.floorRun(turn);
|
|
509
529
|
const floorFlow = floorRun && this.flows.get(floorRun.flowId);
|
|
510
530
|
const eligible = this.eligibleMessageFlows(turn);
|
|
511
|
-
|
|
512
|
-
const messageFlows = !floorRun && eligible.length === 1 ? [] : eligible;
|
|
531
|
+
const messageFlows = !floorRun && this.startsUnscored(turn, eligible) ? [] : eligible;
|
|
513
532
|
const mentionFlows = this.mentionFlows()
|
|
514
533
|
.filter(({ flow, trigger }) => trigger.mention.length > 0 && this.repeatAllows(turn, flow, trigger, turn.triggerKey))
|
|
515
534
|
.map(({ flow }) => flow);
|
|
@@ -562,6 +581,14 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
562
581
|
return this.messageFlows(turn, (list) => list.length > 0);
|
|
563
582
|
}
|
|
564
583
|
|
|
584
|
+
/**
|
|
585
|
+
* The lone eligible flow starts without a score only when a low score would have nowhere
|
|
586
|
+
* else to send the message: no catch-all passes and the idle speaker is silent (S9).
|
|
587
|
+
*/
|
|
588
|
+
private startsUnscored(turn: Turn<C, D>, eligible: Flow<C, D>[]): boolean {
|
|
589
|
+
return eligible.length === 1 && this.options.idle === "silent" && this.messageFlows(turn, (list) => list.length === 0).length === 0;
|
|
590
|
+
}
|
|
591
|
+
|
|
565
592
|
private messageFlows(turn: Turn<C, D>, accept: (list: string[]) => boolean): Flow<C, D>[] {
|
|
566
593
|
const out: Flow<C, D>[] = [];
|
|
567
594
|
for (const flow of this.flows.values()) {
|
|
@@ -605,7 +632,7 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
605
632
|
const current = scores[asker.flowId] ?? 0;
|
|
606
633
|
const top = best(eligible.filter((f) => f.id !== asker.flowId));
|
|
607
634
|
if (top && top.score >= current + ROUTE_STICKY && top.score >= ROUTE_MIN) route = top.flow;
|
|
608
|
-
} else if (
|
|
635
|
+
} else if (this.startsUnscored(turn, eligible)) {
|
|
609
636
|
route = eligible[0];
|
|
610
637
|
} else {
|
|
611
638
|
const top = best(eligible);
|
|
@@ -1139,14 +1166,21 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
1139
1166
|
return new Date(reset !== undefined && reset > ladder ? reset : ladder);
|
|
1140
1167
|
}
|
|
1141
1168
|
|
|
1142
|
-
/** The assistant spoke
|
|
1169
|
+
/** The assistant spoke this turn: its silence wakes replace the ones its last words set. */
|
|
1143
1170
|
private armSilence(turn: Turn<C, D>): void {
|
|
1171
|
+
const previous = turn.original?.lastAssistantAt;
|
|
1172
|
+
if (turn.session.lastAssistantAt === previous) return;
|
|
1173
|
+
turn.schedule.push(...this.silenceWakes(turn, previous));
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/** The assistant spoke last: every silence flow passing `if` and `repeat` gets a wake. */
|
|
1177
|
+
private silenceWakes(turn: Turn<C, D>, previous?: string): ScheduleEntry[] {
|
|
1144
1178
|
const { session } = turn;
|
|
1145
1179
|
const { lastAssistantAt, lastUserAt } = session;
|
|
1146
|
-
if (!lastAssistantAt
|
|
1147
|
-
if (lastUserAt && Date.parse(lastUserAt) > Date.parse(lastAssistantAt)) return;
|
|
1180
|
+
if (!lastAssistantAt) return [];
|
|
1181
|
+
if (lastUserAt && Date.parse(lastUserAt) > Date.parse(lastAssistantAt)) return [];
|
|
1148
1182
|
const ms = Date.parse(lastAssistantAt);
|
|
1149
|
-
const
|
|
1183
|
+
const wakes: ScheduleEntry[] = [];
|
|
1150
1184
|
for (const flow of this.flows.values()) {
|
|
1151
1185
|
const trigger = (flow.on ?? []).find((t): t is SilenceTrigger<C, D> => "silence" in t);
|
|
1152
1186
|
if (!trigger) continue;
|
|
@@ -1154,12 +1188,13 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
1154
1188
|
if (trigger.if && !this.holds(trigger.if, turn, this.draftRun(turn, flow, "silence", key))) continue;
|
|
1155
1189
|
if (!this.repeatAllows(turn, flow, trigger, key)) continue;
|
|
1156
1190
|
const at = this.snap(turn, new Date(ms + parseDuration(trigger.silence)), trigger.businessHours);
|
|
1157
|
-
|
|
1191
|
+
wakes.push({
|
|
1158
1192
|
key: `silence:${flow.id}:${session.id}:${ms}`,
|
|
1159
1193
|
at,
|
|
1160
1194
|
...(previous ? { replaces: `silence:${flow.id}:${session.id}:${Date.parse(previous)}` } : {}),
|
|
1161
1195
|
});
|
|
1162
1196
|
}
|
|
1197
|
+
return wakes;
|
|
1163
1198
|
}
|
|
1164
1199
|
|
|
1165
1200
|
// ── Return (design §4.8) ──────────────────────────────────────────────
|
package/src/core/Speak.ts
CHANGED
|
@@ -78,8 +78,17 @@ function deferralOf(error: unknown): Deferral {
|
|
|
78
78
|
// A usage window that says when it reopens is worth exactly one wake, then.
|
|
79
79
|
// Without that number, waiting is guessing, and the ladder guesses in minutes
|
|
80
80
|
// at a limit measured in hours.
|
|
81
|
-
const
|
|
82
|
-
|
|
81
|
+
const inferred = RETRY_KINDS.has(kind) || (kind === "quota" && reset !== undefined);
|
|
82
|
+
// Unless the provider said so outright. `x-should-retry` is the one answer
|
|
83
|
+
// nobody has to infer, and core already puts it above its own transience
|
|
84
|
+
// test — a 503 that says don't costs five wakes and ten model calls here if
|
|
85
|
+
// this reads the kind instead.
|
|
86
|
+
const stated = error instanceof ProviderError ? error.shouldRetry : undefined;
|
|
87
|
+
return {
|
|
88
|
+
code: DEFER_CODE[kind],
|
|
89
|
+
retryable: stated ?? inferred,
|
|
90
|
+
...(reset !== undefined ? { resetAtMs: reset } : {}),
|
|
91
|
+
};
|
|
83
92
|
}
|
|
84
93
|
/** Gemini rejects any other envelope property name. */
|
|
85
94
|
const WIRE_NAME = /^[a-zA-Z0-9_-]+$/;
|
package/src/core/Understand.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type { FieldDef, FieldDefs, Flow, ParamDef, ParamDefs } from "../types/fl
|
|
|
23
23
|
import type { StructuredSchema } from "../types/schema.js";
|
|
24
24
|
import { extractEmbeddedJSONObject, isRecord } from "../utils/json.js";
|
|
25
25
|
import { logger } from "../utils/logger.js";
|
|
26
|
+
import { splitPhrases } from "../utils/phrases.js";
|
|
26
27
|
import { coerceField, isKnown, pendingFields, toWireSchema } from "../utils/schema.js";
|
|
27
28
|
import { render, type TemplateScope } from "../utils/template.js";
|
|
28
29
|
import { readUsage } from "../utils/usage.js";
|
|
@@ -43,12 +44,9 @@ export class Understand<C = unknown, D = unknown> {
|
|
|
43
44
|
const candidates = candidateFlows(req);
|
|
44
45
|
const onlyRouting =
|
|
45
46
|
req.mentionFlows.length === 0 && req.branches.length === 0 && Object.keys(req.fields).length === 0;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const only = candidates[0];
|
|
50
|
-
return only && !req.floor ? { ...empty(0), flows: { [only.id]: 100 } } : empty(0);
|
|
51
|
-
}
|
|
47
|
+
// Nothing to compare or extract: no candidates, or only the floor's own flow. A lone
|
|
48
|
+
// candidate with nobody on the floor is here because the runner wants it scored.
|
|
49
|
+
if (onlyRouting && candidates.length <= (req.floor ? 1 : 0)) return empty(0);
|
|
52
50
|
|
|
53
51
|
const aliases = new Aliases();
|
|
54
52
|
const jsonSchema = buildEnvelope(req, candidates, aliases);
|
|
@@ -245,8 +243,9 @@ function flowsSection<C, D>(candidates: Flow<C, D>[], aliases: Aliases, t: (text
|
|
|
245
243
|
];
|
|
246
244
|
candidates.forEach((flow, i) => {
|
|
247
245
|
lines.push(`${i + 1}. ${aliases.of(flow.id, "f")} — ${flow.name}${flow.description ? `: ${t(flow.description)}` : ""}`);
|
|
248
|
-
const
|
|
249
|
-
if (
|
|
246
|
+
const { counts, excludes } = splitPhrases(triggerPhrases(flow, "message"));
|
|
247
|
+
if (counts.length) lines.push(` The customer: ${counts.map(t).join("; ")}`);
|
|
248
|
+
if (excludes.length) lines.push(` Score 0 when: ${excludes.map(t).join("; ")}`);
|
|
250
249
|
});
|
|
251
250
|
lines.push(
|
|
252
251
|
"",
|
|
@@ -265,12 +264,14 @@ function mentionsSection<C, D>(flows: Flow<C, D>[], aliases: Aliases, t: (text:
|
|
|
265
264
|
const lines = [
|
|
266
265
|
"## Things the customer may mention",
|
|
267
266
|
"For each item, answer true when the customer's message clearly brings it up, false otherwise. " +
|
|
268
|
-
"Be conservative: true needs clear, explicit evidence in the message. The phrases under an item are alternatives; one match is enough."
|
|
267
|
+
"Be conservative: true needs clear, explicit evidence in the message. The phrases under an item are alternatives; one match is enough. " +
|
|
268
|
+
"A 'Does not count when' line overrides a match: if one of those fits, answer false.",
|
|
269
269
|
];
|
|
270
270
|
for (const flow of flows) {
|
|
271
271
|
lines.push(`- ${aliases.of(flow.id, "f")} — ${flow.name}${flow.description ? `: ${t(flow.description)}` : ""}`);
|
|
272
|
-
const
|
|
273
|
-
if (
|
|
272
|
+
const { counts, excludes } = splitPhrases(triggerPhrases(flow, "mention"));
|
|
273
|
+
if (counts.length) lines.push(` Counts when: ${counts.map(t).join("; ")}`);
|
|
274
|
+
if (excludes.length) lines.push(` Does not count when: ${excludes.map(t).join("; ")}`);
|
|
274
275
|
const defs = extractDefs(flow);
|
|
275
276
|
if (defs) {
|
|
276
277
|
lines.push(" When true, also extract:");
|
package/src/index.ts
CHANGED
|
@@ -101,11 +101,16 @@ export interface RequestConfig {
|
|
|
101
101
|
maxTokens?: number;
|
|
102
102
|
stopSequences?: string[];
|
|
103
103
|
/**
|
|
104
|
-
* How hard the model thinks, bound as this provider's default.
|
|
105
|
-
* the
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
104
|
+
* How hard the model thinks, bound as this provider's default. What absent
|
|
105
|
+
* means is the shape's own business, not one rule: on Gemini it is the
|
|
106
|
+
* model's dynamic thinking; on the OpenAI and OpenRouter dialects nothing is
|
|
107
|
+
* sent and not thinking is the default anyway; on the Anthropic shape — so
|
|
108
|
+
* `AnthropicProvider` and `ZaiProvider` — `@providerkit/core` resolves an
|
|
109
|
+
* absent effort to `"none"`, and Z.ai is sent an explicit disabled marker
|
|
110
|
+
* because its endpoint reads silence as thinking ON. Set a level to ask for
|
|
111
|
+
* thinking where it is off; it matters most under a small `maxTokens`, where
|
|
112
|
+
* thinking tokens come out of the same budget as the answer and can consume
|
|
113
|
+
* all of it.
|
|
109
114
|
*/
|
|
110
115
|
effort?: Effort;
|
|
111
116
|
}
|
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
* `@providerkit/core` (adapter + preset); this file is the constructor, the
|
|
7
7
|
* family default for cheap high-volume work.
|
|
8
8
|
*
|
|
9
|
-
* Dialect notes (
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* Dialect notes (see the core preset for detail): model ids are BARE
|
|
10
|
+
* (`glm-5.3-flash`, not `z-ai/glm-5.3-flash`), and the endpoint reads an
|
|
11
|
+
* absent `thinking` field as thinking ON, so the preset says "no" out loud.
|
|
12
|
+
* That makes thinking OFF here by default (measured 2026-09-21: a call with no
|
|
13
|
+
* `config.effort` sends `thinking: { type: "disabled" }`); ask for it with
|
|
14
|
+
* `config: { effort: 'high' }`.
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
import { createZaiCodingProvider, type FallbackOptions, type Provider } from "@providerkit/core";
|
package/src/types/agent.ts
CHANGED
|
@@ -87,7 +87,11 @@ export type TurnBase<C = unknown, D = unknown> = ContextField<C> & {
|
|
|
87
87
|
sessionId: string;
|
|
88
88
|
/** Absent on a first turn. A wake never creates a session. */
|
|
89
89
|
session?: Session<D>;
|
|
90
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* The conversation BEFORE this input. Pass it on every input kind, wakes included.
|
|
92
|
+
* Leave out the message this turn carries: both calls quote it on their own, so a
|
|
93
|
+
* history that ends with it makes the model read it twice.
|
|
94
|
+
*/
|
|
91
95
|
history?: History;
|
|
92
96
|
silenced?: Silenced;
|
|
93
97
|
/** Host anchors this session belongs to, e.g. `{ lead: { key: 'lead:456', lastInboundAt } }`. */
|
|
@@ -108,6 +112,13 @@ export type TurnKind =
|
|
|
108
112
|
|
|
109
113
|
export type TurnInput<C = unknown, D = unknown> = TurnBase<C, D> & TurnKind;
|
|
110
114
|
|
|
115
|
+
/** A saved session and the host inputs a turn on it would carry: what `pendingWakes` reads. */
|
|
116
|
+
export type PendingWakesInput<C = unknown, D = unknown> = ContextField<C> & {
|
|
117
|
+
session: Session<D>;
|
|
118
|
+
anchors?: TurnBase<C, D>["anchors"];
|
|
119
|
+
claims?: TurnBase<C, D>["claims"];
|
|
120
|
+
};
|
|
121
|
+
|
|
111
122
|
// ── turn() result ───────────────────────────────────────────────────────
|
|
112
123
|
|
|
113
124
|
export interface OutboundMessage {
|
|
@@ -123,7 +134,7 @@ export interface OutboundMessage {
|
|
|
123
134
|
stepId?: string;
|
|
124
135
|
}
|
|
125
136
|
|
|
126
|
-
/** A wake to enqueue
|
|
137
|
+
/** A wake to enqueue, keyed by `key` (encode it as a queue job id: BullMQ refuses a `:`); at fire time call `turn({ wake: key })`. */
|
|
127
138
|
export interface ScheduleEntry {
|
|
128
139
|
key: string;
|
|
129
140
|
at: Date;
|
package/src/types/index.ts
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Splitting an AI-judged phrase list into what counts and what rules it out.
|
|
3
|
+
*
|
|
4
|
+
* A trigger's phrases are alternatives: one match is enough. That makes the
|
|
5
|
+
* list useless for saying "but not this" — and "but not this" is what keeps a
|
|
6
|
+
* classifier honest. A customer answering "pode sim" to an offer of a meeting
|
|
7
|
+
* is agreeing to the meeting, not asking for a human, and without a way to say
|
|
8
|
+
* so every cheerful yes reads as a handoff request.
|
|
9
|
+
*
|
|
10
|
+
* So a phrase that opens with `!` is an exclusion: it does not make the trigger
|
|
11
|
+
* fire, it stops it. Exclusions win over matches, and the model is told both
|
|
12
|
+
* lists separately.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface Phrases {
|
|
16
|
+
/** Any one of these makes it true. */
|
|
17
|
+
counts: string[];
|
|
18
|
+
/** Any one of these makes it false, whatever matched. */
|
|
19
|
+
excludes: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Split a phrase list on the leading `!`. Blank entries and a bare `"!"` are
|
|
24
|
+
* dropped: they would render as an empty bullet and mean nothing to the model.
|
|
25
|
+
*/
|
|
26
|
+
export function splitPhrases(phrases: readonly string[]): Phrases {
|
|
27
|
+
const counts: string[] = [];
|
|
28
|
+
const excludes: string[] = [];
|
|
29
|
+
for (const phrase of phrases) {
|
|
30
|
+
const trimmed = phrase.trim();
|
|
31
|
+
if (!trimmed) continue;
|
|
32
|
+
if (trimmed.startsWith("!")) {
|
|
33
|
+
const body = trimmed.slice(1).trim();
|
|
34
|
+
if (body) excludes.push(body);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
counts.push(trimmed);
|
|
38
|
+
}
|
|
39
|
+
return { counts, excludes };
|
|
40
|
+
}
|
package/src/utils/template.ts
CHANGED
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
* per-turn context) and `input` (the run's trigger payload). A path that
|
|
6
6
|
* resolves to nothing keeps its placeholder, so a typo stays visible instead
|
|
7
7
|
* of vanishing into an empty string.
|
|
8
|
+
*
|
|
9
|
+
* A path the host answered with nothing is a different case: it knows the
|
|
10
|
+
* field and knows it is blank. Then the placeholder goes and `tidy` closes the
|
|
11
|
+
* gap it left, so "Ola {{context.lead.name}}, tudo bem?" sends "Ola, tudo bem?"
|
|
12
|
+
* and never "Ola , tudo bem?". Two things count as blank: an empty string, and
|
|
13
|
+
* a path that walks THROUGH a null — `context.lead` being `null` means there
|
|
14
|
+
* is no lead, so "the lead's name" is blank, not mistyped. A null at the end of
|
|
15
|
+
* a path is still unknown: a field that collected nothing keeps its braces.
|
|
8
16
|
*/
|
|
9
17
|
|
|
10
18
|
export interface TemplateScope {
|
|
@@ -15,11 +23,38 @@ export interface TemplateScope {
|
|
|
15
23
|
|
|
16
24
|
const PLACEHOLDER = /\{\{\s*([^}\s]+)\s*\}\}/g;
|
|
17
25
|
|
|
26
|
+
/** The path ran into a `null` on its way down: the container the host named is absent. */
|
|
27
|
+
const ABSENT = Symbol("absent");
|
|
28
|
+
|
|
18
29
|
export function render(template: string, scope: TemplateScope): string {
|
|
19
|
-
|
|
30
|
+
let emptied = false;
|
|
31
|
+
const out = template.replace(PLACEHOLDER, (match, path: string) => {
|
|
20
32
|
const value = lookup(scope, path.split("."));
|
|
21
|
-
|
|
33
|
+
if (value === ABSENT) {
|
|
34
|
+
emptied = true;
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
37
|
+
if (value === undefined || value === null) return match;
|
|
38
|
+
const text = stringify(value);
|
|
39
|
+
if (text === "") emptied = true;
|
|
40
|
+
return text;
|
|
22
41
|
});
|
|
42
|
+
return emptied ? tidy(out) : out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Close the hole an empty value leaves: a doubled space, a space before
|
|
47
|
+
* punctuation, a space at the end of a line.
|
|
48
|
+
*
|
|
49
|
+
* Deliberately narrow. It only runs on a string where something substituted to
|
|
50
|
+
* "", and it only collapses a run of spaces that follows a visible character,
|
|
51
|
+
* so indentation in a markdown list survives.
|
|
52
|
+
*/
|
|
53
|
+
function tidy(text: string): string {
|
|
54
|
+
return text
|
|
55
|
+
.replace(/(?<=\S)[ \t]{2,}/g, " ")
|
|
56
|
+
.replace(/ +([,.;:!?\u2026])/g, "$1")
|
|
57
|
+
.replace(/[ \t]+$/gm, "");
|
|
23
58
|
}
|
|
24
59
|
|
|
25
60
|
/** `render` over every string inside a value, recursively. Non-strings pass through. */
|
|
@@ -37,7 +72,8 @@ export function renderDeep<T>(value: T, scope: TemplateScope): T {
|
|
|
37
72
|
function lookup(root: unknown, keys: string[]): unknown {
|
|
38
73
|
let current = root;
|
|
39
74
|
for (const key of keys) {
|
|
40
|
-
if (current === null
|
|
75
|
+
if (current === null) return ABSENT;
|
|
76
|
+
if (typeof current !== "object") return undefined;
|
|
41
77
|
current = (current as Record<string, unknown>)[key];
|
|
42
78
|
}
|
|
43
79
|
return current;
|