@falai/agent 4.0.0-alpha.12 → 4.0.0-alpha.14
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/FlowSpec.d.ts +2 -0
- package/dist/cjs/core/FlowSpec.d.ts.map +1 -1
- package/dist/cjs/core/FlowSpec.js +25 -2
- package/dist/cjs/core/FlowSpec.js.map +1 -1
- package/dist/cjs/core/Runner.d.ts +6 -0
- package/dist/cjs/core/Runner.d.ts.map +1 -1
- package/dist/cjs/core/Runner.js +63 -12
- package/dist/cjs/core/Runner.js.map +1 -1
- package/dist/cjs/providers/ProviderAdapter.d.ts +7 -0
- package/dist/cjs/providers/ProviderAdapter.d.ts.map +1 -1
- package/dist/cjs/providers/ProviderAdapter.js +7 -1
- package/dist/cjs/providers/ProviderAdapter.js.map +1 -1
- package/dist/cjs/types/flow.d.ts +14 -0
- package/dist/cjs/types/flow.d.ts.map +1 -1
- package/dist/cjs/types/session.d.ts +1 -1
- package/dist/cjs/types/session.d.ts.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/schema.d.ts +3 -3
- package/dist/cjs/utils/schema.d.ts.map +1 -1
- package/dist/cjs/utils/schema.js +4 -3
- package/dist/cjs/utils/schema.js.map +1 -1
- package/dist/core/FlowSpec.d.ts +2 -0
- package/dist/core/FlowSpec.d.ts.map +1 -1
- package/dist/core/FlowSpec.js +26 -3
- package/dist/core/FlowSpec.js.map +1 -1
- package/dist/core/Runner.d.ts +6 -0
- package/dist/core/Runner.d.ts.map +1 -1
- package/dist/core/Runner.js +63 -12
- package/dist/core/Runner.js.map +1 -1
- package/dist/providers/ProviderAdapter.d.ts +7 -0
- package/dist/providers/ProviderAdapter.d.ts.map +1 -1
- package/dist/providers/ProviderAdapter.js +7 -1
- package/dist/providers/ProviderAdapter.js.map +1 -1
- package/dist/types/flow.d.ts +14 -0
- package/dist/types/flow.d.ts.map +1 -1
- package/dist/types/session.d.ts +1 -1
- package/dist/types/session.d.ts.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/schema.d.ts +3 -3
- package/dist/utils/schema.d.ts.map +1 -1
- package/dist/utils/schema.js +4 -3
- package/dist/utils/schema.js.map +1 -1
- package/docs/concepts/collection.md +40 -5
- package/docs/concepts/pipeline.md +3 -2
- package/docs/reference/fields.md +5 -3
- package/docs/reference/flow-spec.md +6 -3
- package/docs/reference/flow.md +7 -2
- package/docs/reference/outcomes.md +2 -1
- package/docs/reference/step.md +5 -2
- package/docs/rfc/v4-one-flow.md +2 -0
- package/package.json +2 -2
- package/src/core/FlowSpec.ts +32 -4
- package/src/core/Runner.ts +56 -10
- package/src/providers/ProviderAdapter.ts +9 -1
- package/src/types/flow.ts +14 -0
- package/src/types/session.ts +1 -0
- package/src/utils/outcomes.ts +1 -0
- package/src/utils/schema.ts +4 -3
package/src/core/Runner.ts
CHANGED
|
@@ -137,6 +137,13 @@ function talkKind(step: { collect?: readonly string[] }): StepOutcomeKind {
|
|
|
137
137
|
return step.collect?.length ? "collect" : "prompt";
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/** Everything a flow collects: its own `collect`, then each talk step's, once each. */
|
|
141
|
+
function flowFields<C, D>(flow: Flow<C, D>): string[] {
|
|
142
|
+
const fields = new Set<string>(flow.collect);
|
|
143
|
+
for (const step of flow.steps) if (isTalk(step)) for (const field of step.collect ?? []) fields.add(field);
|
|
144
|
+
return [...fields];
|
|
145
|
+
}
|
|
146
|
+
|
|
140
147
|
function kindOf<C, D>(step: Step<C, D> | undefined): StepOutcomeKind {
|
|
141
148
|
if (!step) return "if";
|
|
142
149
|
if (isTalk(step)) return talkKind(step);
|
|
@@ -543,16 +550,25 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
543
550
|
}
|
|
544
551
|
const fields: UnderstandRequest<C, D>["fields"] = {};
|
|
545
552
|
const data: Record<string, unknown> = turn.session.data;
|
|
553
|
+
const harvestable = (field: string): boolean => {
|
|
554
|
+
const def = this.options.fields[field];
|
|
555
|
+
return def !== undefined && !isKnown(data[field]) && extractMode(def) === "anywhere";
|
|
556
|
+
};
|
|
546
557
|
for (const flow of [...(floorFlow ? [floorFlow] : []), ...eligible]) {
|
|
547
|
-
for (const
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
558
|
+
for (const field of flowFields(flow).filter(harvestable)) fields[field] = this.options.fields[field];
|
|
559
|
+
}
|
|
560
|
+
const judging = messageFlows.length > 0 || mentionFlows.length > 0 || branches.length > 0 || Object.keys(fields).length > 0;
|
|
561
|
+
// With nobody on the floor the catch-all may take this message, and an opening message often says the most. The
|
|
562
|
+
// 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.
|
|
563
|
+
// 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.
|
|
564
|
+
const fallback = floorRun ? undefined : this.messageFlows(turn, (list) => list.length === 0)[0];
|
|
565
|
+
if (fallback) {
|
|
566
|
+
const first = fallback.steps[0];
|
|
567
|
+
const free = new Set<string>(first && isTalk(first) && first.question === undefined ? first.collect : []);
|
|
568
|
+
const own = flowFields(fallback).filter(harvestable);
|
|
569
|
+
if (judging || own.some((field) => !free.has(field))) for (const field of own) fields[field] = this.options.fields[field];
|
|
554
570
|
}
|
|
555
|
-
if (!
|
|
571
|
+
if (!judging && !Object.keys(fields).length) return null;
|
|
556
572
|
return {
|
|
557
573
|
text: turn.what.text,
|
|
558
574
|
history: this.historyOf(turn),
|
|
@@ -724,7 +740,31 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
724
740
|
turn.queue.push(resumed);
|
|
725
741
|
await this.drain(turn);
|
|
726
742
|
}
|
|
727
|
-
|
|
743
|
+
const speaker = this.speaker(turn);
|
|
744
|
+
if (speaker && !("idle" in speaker) && this.askFixed(turn, speaker)) {
|
|
745
|
+
turn.talk = undefined;
|
|
746
|
+
return null;
|
|
747
|
+
}
|
|
748
|
+
return speaker;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Send the step's fixed question when this is its first ask: every field it collects still unknown, none asked yet,
|
|
753
|
+
* and the run not staying (a staying run answers the lead). The question counts as one ask of each field. False when
|
|
754
|
+
* the AI should phrase the ask instead.
|
|
755
|
+
*/
|
|
756
|
+
private askFixed(turn: Turn<C, D>, { run, step, pending }: TalkRequest<C, D>): boolean {
|
|
757
|
+
const { question, collect = [] } = step;
|
|
758
|
+
if (question === undefined || run.staying) return false;
|
|
759
|
+
if (pending.length !== collect.length || pending.some((field) => (run.asked[field] ?? 0) > 0)) return false;
|
|
760
|
+
const key = this.stepKey(run, step);
|
|
761
|
+
turn.messages.push({ text: render(question, this.scope(turn, run)), kind: "verbatim", afterMs: turn.afterMs, key, runId: run.id, stepId: step.id });
|
|
762
|
+
turn.afterMs = 0;
|
|
763
|
+
turn.spokeBy.add(run.id);
|
|
764
|
+
turn.spoke = true;
|
|
765
|
+
for (const field of pending) run.asked[field] = (run.asked[field] ?? 0) + 1;
|
|
766
|
+
this.outcome(turn, run, { kind: "collect", status: "ok", key, code: "asked-fixed", stepId: step.id });
|
|
767
|
+
return true;
|
|
728
768
|
}
|
|
729
769
|
|
|
730
770
|
/**
|
|
@@ -873,7 +913,9 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
873
913
|
}
|
|
874
914
|
}
|
|
875
915
|
run.status = "asking";
|
|
916
|
+
// 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.
|
|
876
917
|
if (!turn.speakDone) turn.talk = { run, flow, step, pending };
|
|
918
|
+
else this.askFixed(turn, { run, flow, step, pending });
|
|
877
919
|
return;
|
|
878
920
|
}
|
|
879
921
|
|
|
@@ -1014,7 +1056,11 @@ export class Runner<C = unknown, D = unknown> {
|
|
|
1014
1056
|
}
|
|
1015
1057
|
if ("step" in target) {
|
|
1016
1058
|
const data: Record<string, unknown> = turn.session.data;
|
|
1017
|
-
|
|
1059
|
+
// Forgetting a field means asking for it from scratch: its ask count goes too, so a fixed question goes out again.
|
|
1060
|
+
for (const field of target.clear ?? []) {
|
|
1061
|
+
delete data[field];
|
|
1062
|
+
delete run.asked[field];
|
|
1063
|
+
}
|
|
1018
1064
|
this.jump(turn, run, flow, target.step);
|
|
1019
1065
|
return;
|
|
1020
1066
|
}
|
|
@@ -290,6 +290,8 @@ export abstract class ProviderAdapter implements AiProvider {
|
|
|
290
290
|
public abstract readonly capabilities: ProviderCapabilities;
|
|
291
291
|
|
|
292
292
|
protected readonly provider: Provider;
|
|
293
|
+
/** `provider` without the fallback chain: the one model this adapter was built for. */
|
|
294
|
+
private readonly primary: Provider;
|
|
293
295
|
protected readonly primaryModel: string;
|
|
294
296
|
protected readonly backupModels: string[];
|
|
295
297
|
protected readonly retryConfig: RetryConfig;
|
|
@@ -304,6 +306,7 @@ export abstract class ProviderAdapter implements AiProvider {
|
|
|
304
306
|
this.primaryModel = init.model;
|
|
305
307
|
this.backupModels = init.backupModels ?? [];
|
|
306
308
|
this.retryConfig = resolveRetryConfig(init.retryConfig);
|
|
309
|
+
this.primary = init.provider;
|
|
307
310
|
|
|
308
311
|
if (init.fallbacks && init.fallbacks.length > 0) {
|
|
309
312
|
const coreProviders: Provider[] = [
|
|
@@ -337,9 +340,14 @@ export abstract class ProviderAdapter implements AiProvider {
|
|
|
337
340
|
* boot when it is `null` — that model cannot serve this framework's turns).
|
|
338
341
|
* Log `calls`: `0/3 and 3/3` is what makes the next model swap's regression
|
|
339
342
|
* obvious. Costs `samples × 2` short calls, and errors propagate.
|
|
343
|
+
*
|
|
344
|
+
* It asks the primary alone, never the fallback chain. The answer configures
|
|
345
|
+
* this adapter's model; a call the chain handed to a fallback would score
|
|
346
|
+
* another model as this one. Each fallback is an adapter of its own: probe it
|
|
347
|
+
* the same way.
|
|
340
348
|
*/
|
|
341
349
|
async probeJsonWithTools(opts?: ProbeOptions): Promise<JsonWithToolsProbe> {
|
|
342
|
-
return probeJsonWithTools(this.
|
|
350
|
+
return probeJsonWithTools(this.primary, {
|
|
343
351
|
model: this.primaryModel,
|
|
344
352
|
...(opts ?? {}),
|
|
345
353
|
});
|
package/src/types/flow.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface ScalarDef<T extends ScalarType = ScalarType> {
|
|
|
45
45
|
|
|
46
46
|
/** One collectable field, authored once on the agent. */
|
|
47
47
|
export interface FieldDef<T extends ScalarType = ScalarType> extends ScalarDef<T> {
|
|
48
|
+
/** The name a person reads, in an editor or next to a collected value. The model never sees it. */
|
|
49
|
+
label?: string;
|
|
48
50
|
/** How the AI should ask for this field when a step collects it. */
|
|
49
51
|
ask?: string;
|
|
50
52
|
/**
|
|
@@ -185,6 +187,12 @@ export type TalkStep<C = unknown, D = unknown> = (
|
|
|
185
187
|
) & {
|
|
186
188
|
/** Per-flow wording for a field; the field's own `ask` is the default. */
|
|
187
189
|
ask?: Partial<Record<keyof D & string, string>>;
|
|
190
|
+
/**
|
|
191
|
+
* A fixed first question, sent word for word with no model call when the step
|
|
192
|
+
* first asks: every field it collects is still unknown and none was asked yet.
|
|
193
|
+
* Any later ask is the AI's own wording. Needs `collect`.
|
|
194
|
+
*/
|
|
195
|
+
question?: Template;
|
|
188
196
|
/** Times a field may be asked before it is skipped. Default 3. */
|
|
189
197
|
maxAsks?: number;
|
|
190
198
|
branches?: Branch<C, D>[];
|
|
@@ -253,6 +261,12 @@ export interface Flow<C = unknown, D = unknown> {
|
|
|
253
261
|
anchor?: string;
|
|
254
262
|
/** Re-checked whenever the run moves. Default: the trigger's `if`. */
|
|
255
263
|
while?: Pred<C, D>;
|
|
264
|
+
/**
|
|
265
|
+
* The data this flow needs, as agent field slugs. Its talk steps' `collect`
|
|
266
|
+
* says which of it to ask, and in what order; a field no step asks is still
|
|
267
|
+
* noted whenever the lead gives it while the flow holds the conversation.
|
|
268
|
+
*/
|
|
269
|
+
collect?: (keyof D & string)[];
|
|
256
270
|
clearOnStart?: (keyof D & string)[];
|
|
257
271
|
steps: Step<C, D>[];
|
|
258
272
|
/** What the run does after its last step. Default `'end'`. `'stay'`: the last talk step the run took answers every later message. */
|
package/src/types/session.ts
CHANGED
package/src/utils/outcomes.ts
CHANGED
|
@@ -28,6 +28,7 @@ export const OUTCOME_MESSAGES = {
|
|
|
28
28
|
silenced: "the host cannot speak right now",
|
|
29
29
|
|
|
30
30
|
"already-known": "every field this step collects is known",
|
|
31
|
+
"asked-fixed": "the step's fixed question went out word for word",
|
|
31
32
|
"another-reply": "another run already answered this turn",
|
|
32
33
|
"already-sent": "this message was already sent once",
|
|
33
34
|
branch: "a branch of this step fired",
|
package/src/utils/schema.ts
CHANGED
|
@@ -76,7 +76,7 @@ export interface WireOptions {
|
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
78
|
* The JSON schema a provider sees: an allow-list of JSON-schema keys, with
|
|
79
|
-
* `ask`, `extract` and `optional` stripped. Closed (`additionalProperties:
|
|
79
|
+
* `label`, `ask`, `extract` and `optional` stripped. Closed (`additionalProperties:
|
|
80
80
|
* false`); every property required unless a parameter says `optional`.
|
|
81
81
|
*/
|
|
82
82
|
export function toWireSchema(defs: FieldDefs | ParamDefs, options: WireOptions = {}): StructuredSchema {
|
|
@@ -102,8 +102,8 @@ function wireProperty(def: FieldDef | ParamDef, nullable: boolean): StructuredSc
|
|
|
102
102
|
|
|
103
103
|
/**
|
|
104
104
|
* Merge field rows authored per flow into the agent's one field set. Two
|
|
105
|
-
* rows for one slug must agree on type and enum; the first non-empty `
|
|
106
|
-
* and `description` win.
|
|
105
|
+
* rows for one slug must agree on type and enum; the first non-empty `label`,
|
|
106
|
+
* `ask` and `description` win.
|
|
107
107
|
*/
|
|
108
108
|
export function buildSchema(groups: Array<{ id: string; fields: FieldDefs }>): FieldDefs {
|
|
109
109
|
const merged: FieldDefs = {};
|
|
@@ -131,6 +131,7 @@ export function buildSchema(groups: Array<{ id: string; fields: FieldDefs }>): F
|
|
|
131
131
|
merged[slug] = {
|
|
132
132
|
...seen,
|
|
133
133
|
enum: seen.enum ?? def.enum,
|
|
134
|
+
label: seen.label ?? def.label,
|
|
134
135
|
ask: seen.ask ?? def.ask,
|
|
135
136
|
description: seen.description ?? def.description,
|
|
136
137
|
extract: seen.extract ?? def.extract,
|