@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.
Files changed (64) hide show
  1. package/dist/cjs/core/FlowSpec.d.ts +2 -0
  2. package/dist/cjs/core/FlowSpec.d.ts.map +1 -1
  3. package/dist/cjs/core/FlowSpec.js +25 -2
  4. package/dist/cjs/core/FlowSpec.js.map +1 -1
  5. package/dist/cjs/core/Runner.d.ts +6 -0
  6. package/dist/cjs/core/Runner.d.ts.map +1 -1
  7. package/dist/cjs/core/Runner.js +63 -12
  8. package/dist/cjs/core/Runner.js.map +1 -1
  9. package/dist/cjs/providers/ProviderAdapter.d.ts +7 -0
  10. package/dist/cjs/providers/ProviderAdapter.d.ts.map +1 -1
  11. package/dist/cjs/providers/ProviderAdapter.js +7 -1
  12. package/dist/cjs/providers/ProviderAdapter.js.map +1 -1
  13. package/dist/cjs/types/flow.d.ts +14 -0
  14. package/dist/cjs/types/flow.d.ts.map +1 -1
  15. package/dist/cjs/types/session.d.ts +1 -1
  16. package/dist/cjs/types/session.d.ts.map +1 -1
  17. package/dist/cjs/utils/outcomes.d.ts +1 -0
  18. package/dist/cjs/utils/outcomes.d.ts.map +1 -1
  19. package/dist/cjs/utils/outcomes.js +1 -0
  20. package/dist/cjs/utils/outcomes.js.map +1 -1
  21. package/dist/cjs/utils/schema.d.ts +3 -3
  22. package/dist/cjs/utils/schema.d.ts.map +1 -1
  23. package/dist/cjs/utils/schema.js +4 -3
  24. package/dist/cjs/utils/schema.js.map +1 -1
  25. package/dist/core/FlowSpec.d.ts +2 -0
  26. package/dist/core/FlowSpec.d.ts.map +1 -1
  27. package/dist/core/FlowSpec.js +26 -3
  28. package/dist/core/FlowSpec.js.map +1 -1
  29. package/dist/core/Runner.d.ts +6 -0
  30. package/dist/core/Runner.d.ts.map +1 -1
  31. package/dist/core/Runner.js +63 -12
  32. package/dist/core/Runner.js.map +1 -1
  33. package/dist/providers/ProviderAdapter.d.ts +7 -0
  34. package/dist/providers/ProviderAdapter.d.ts.map +1 -1
  35. package/dist/providers/ProviderAdapter.js +7 -1
  36. package/dist/providers/ProviderAdapter.js.map +1 -1
  37. package/dist/types/flow.d.ts +14 -0
  38. package/dist/types/flow.d.ts.map +1 -1
  39. package/dist/types/session.d.ts +1 -1
  40. package/dist/types/session.d.ts.map +1 -1
  41. package/dist/utils/outcomes.d.ts +1 -0
  42. package/dist/utils/outcomes.d.ts.map +1 -1
  43. package/dist/utils/outcomes.js +1 -0
  44. package/dist/utils/outcomes.js.map +1 -1
  45. package/dist/utils/schema.d.ts +3 -3
  46. package/dist/utils/schema.d.ts.map +1 -1
  47. package/dist/utils/schema.js +4 -3
  48. package/dist/utils/schema.js.map +1 -1
  49. package/docs/concepts/collection.md +40 -5
  50. package/docs/concepts/pipeline.md +3 -2
  51. package/docs/reference/fields.md +5 -3
  52. package/docs/reference/flow-spec.md +6 -3
  53. package/docs/reference/flow.md +7 -2
  54. package/docs/reference/outcomes.md +2 -1
  55. package/docs/reference/step.md +5 -2
  56. package/docs/rfc/v4-one-flow.md +2 -0
  57. package/package.json +2 -2
  58. package/src/core/FlowSpec.ts +32 -4
  59. package/src/core/Runner.ts +56 -10
  60. package/src/providers/ProviderAdapter.ts +9 -1
  61. package/src/types/flow.ts +14 -0
  62. package/src/types/session.ts +1 -0
  63. package/src/utils/outcomes.ts +1 -0
  64. package/src/utils/schema.ts +4 -3
@@ -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 step of flow.steps) {
548
- if (!isTalk(step)) continue;
549
- for (const field of step.collect ?? []) {
550
- const def = this.options.fields[field];
551
- if (def && !isKnown(data[field]) && extractMode(def) === "anywhere") fields[field] = def;
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 (!messageFlows.length && !mentionFlows.length && !branches.length && !Object.keys(fields).length) return null;
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
- return this.speaker(turn);
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
- for (const field of target.clear ?? []) delete data[field];
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.provider, {
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. */
@@ -77,6 +77,7 @@ export type StepOutcomeCode =
77
77
  | "silenced"
78
78
  // A step
79
79
  | "already-known"
80
+ | "asked-fixed"
80
81
  | "another-reply"
81
82
  | "already-sent"
82
83
  | "branch"
@@ -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",
@@ -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 `ask`
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,