@memberjunction/ai-agents 6.2.0-edge.1 → 6.2.0-edge.3

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 (131) hide show
  1. package/README.md +18 -0
  2. package/dist/AgentDecisionService.d.ts +90 -0
  3. package/dist/AgentDecisionService.d.ts.map +1 -0
  4. package/dist/AgentDecisionService.js +159 -0
  5. package/dist/AgentDecisionService.js.map +1 -0
  6. package/dist/AgentRunner.d.ts +9 -0
  7. package/dist/AgentRunner.d.ts.map +1 -1
  8. package/dist/AgentRunner.js +23 -7
  9. package/dist/AgentRunner.js.map +1 -1
  10. package/dist/ConversationCompactionManager.d.ts +7 -1
  11. package/dist/ConversationCompactionManager.d.ts.map +1 -1
  12. package/dist/ConversationCompactionManager.js +4 -1
  13. package/dist/ConversationCompactionManager.js.map +1 -1
  14. package/dist/KnowledgeAgent.d.ts.map +1 -1
  15. package/dist/KnowledgeAgent.js +6 -6
  16. package/dist/KnowledgeAgent.js.map +1 -1
  17. package/dist/PayloadFeedbackManager.d.ts +116 -14
  18. package/dist/PayloadFeedbackManager.d.ts.map +1 -1
  19. package/dist/PayloadFeedbackManager.js +229 -88
  20. package/dist/PayloadFeedbackManager.js.map +1 -1
  21. package/dist/agent-context-injector.d.ts +47 -3
  22. package/dist/agent-context-injector.d.ts.map +1 -1
  23. package/dist/agent-context-injector.js +77 -10
  24. package/dist/agent-context-injector.js.map +1 -1
  25. package/dist/agent-memory-context-builder.d.ts +13 -0
  26. package/dist/agent-memory-context-builder.d.ts.map +1 -1
  27. package/dist/agent-memory-context-builder.js +24 -5
  28. package/dist/agent-memory-context-builder.js.map +1 -1
  29. package/dist/agent-types/flow-agent-type.d.ts +97 -0
  30. package/dist/agent-types/flow-agent-type.d.ts.map +1 -1
  31. package/dist/agent-types/flow-agent-type.js +281 -24
  32. package/dist/agent-types/flow-agent-type.js.map +1 -1
  33. package/dist/agent-types/flow-graph-executor.d.ts +12 -1
  34. package/dist/agent-types/flow-graph-executor.d.ts.map +1 -1
  35. package/dist/agent-types/flow-graph-executor.js +25 -1
  36. package/dist/agent-types/flow-graph-executor.js.map +1 -1
  37. package/dist/agent-types/loop-agent-prompt-params.d.ts +239 -66
  38. package/dist/agent-types/loop-agent-prompt-params.d.ts.map +1 -1
  39. package/dist/agent-types/loop-agent-prompt-params.js +47 -2
  40. package/dist/agent-types/loop-agent-prompt-params.js.map +1 -1
  41. package/dist/agent-types/loop-agent-response-type.d.ts +12 -1
  42. package/dist/agent-types/loop-agent-response-type.d.ts.map +1 -1
  43. package/dist/agent-types/loop-agent-response-type.js.map +1 -1
  44. package/dist/agent-types/loop-agent-type.d.ts +26 -0
  45. package/dist/agent-types/loop-agent-type.d.ts.map +1 -1
  46. package/dist/agent-types/loop-agent-type.js +115 -5
  47. package/dist/agent-types/loop-agent-type.js.map +1 -1
  48. package/dist/base-agent.d.ts +527 -10
  49. package/dist/base-agent.d.ts.map +1 -1
  50. package/dist/base-agent.js +1779 -110
  51. package/dist/base-agent.js.map +1 -1
  52. package/dist/catalog-narrowing.d.ts +136 -0
  53. package/dist/catalog-narrowing.d.ts.map +1 -0
  54. package/dist/catalog-narrowing.js +212 -0
  55. package/dist/catalog-narrowing.js.map +1 -0
  56. package/dist/decision-discovery.d.ts +401 -0
  57. package/dist/decision-discovery.d.ts.map +1 -0
  58. package/dist/decision-discovery.js +511 -0
  59. package/dist/decision-discovery.js.map +1 -0
  60. package/dist/finish-if-mode-warnings.d.ts +39 -0
  61. package/dist/finish-if-mode-warnings.d.ts.map +1 -0
  62. package/dist/finish-if-mode-warnings.js +78 -0
  63. package/dist/finish-if-mode-warnings.js.map +1 -0
  64. package/dist/finish-if-state.d.ts +72 -0
  65. package/dist/finish-if-state.d.ts.map +1 -0
  66. package/dist/finish-if-state.js +109 -0
  67. package/dist/finish-if-state.js.map +1 -0
  68. package/dist/index.d.ts +10 -0
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +11 -0
  71. package/dist/index.js.map +1 -1
  72. package/dist/memory-manager-agent.d.ts +108 -2
  73. package/dist/memory-manager-agent.d.ts.map +1 -1
  74. package/dist/memory-manager-agent.js +172 -9
  75. package/dist/memory-manager-agent.js.map +1 -1
  76. package/dist/memory-note-gate.d.ts +208 -0
  77. package/dist/memory-note-gate.d.ts.map +1 -0
  78. package/dist/memory-note-gate.js +294 -0
  79. package/dist/memory-note-gate.js.map +1 -0
  80. package/dist/native-tools/action-tool-builder.d.ts +14 -0
  81. package/dist/native-tools/action-tool-builder.d.ts.map +1 -1
  82. package/dist/native-tools/action-tool-builder.js +62 -7
  83. package/dist/native-tools/action-tool-builder.js.map +1 -1
  84. package/dist/native-tools/control-tools.d.ts +17 -1
  85. package/dist/native-tools/control-tools.d.ts.map +1 -1
  86. package/dist/native-tools/control-tools.js +40 -3
  87. package/dist/native-tools/control-tools.js.map +1 -1
  88. package/dist/realtime/bridge-host-params.d.ts +22 -0
  89. package/dist/realtime/bridge-host-params.d.ts.map +1 -0
  90. package/dist/realtime/bridge-host-params.js +53 -0
  91. package/dist/realtime/bridge-host-params.js.map +1 -0
  92. package/dist/realtime/bridge-realtime-session-factory.d.ts +23 -2
  93. package/dist/realtime/bridge-realtime-session-factory.d.ts.map +1 -1
  94. package/dist/realtime/bridge-realtime-session-factory.js +22 -6
  95. package/dist/realtime/bridge-realtime-session-factory.js.map +1 -1
  96. package/dist/realtime/bridge-room-transcript-sink.d.ts.map +1 -1
  97. package/dist/realtime/bridge-room-transcript-sink.js +15 -9
  98. package/dist/realtime/bridge-room-transcript-sink.js.map +1 -1
  99. package/dist/realtime/bridge-session-transcript-sink.d.ts +37 -0
  100. package/dist/realtime/bridge-session-transcript-sink.d.ts.map +1 -0
  101. package/dist/realtime/bridge-session-transcript-sink.js +58 -0
  102. package/dist/realtime/bridge-session-transcript-sink.js.map +1 -0
  103. package/dist/realtime/realtime-client-session-service.d.ts +117 -12
  104. package/dist/realtime/realtime-client-session-service.d.ts.map +1 -1
  105. package/dist/realtime/realtime-client-session-service.js +162 -39
  106. package/dist/realtime/realtime-client-session-service.js.map +1 -1
  107. package/dist/realtime/realtime-coagent-resolution.d.ts +90 -0
  108. package/dist/realtime/realtime-coagent-resolution.d.ts.map +1 -0
  109. package/dist/realtime/realtime-coagent-resolution.js +186 -0
  110. package/dist/realtime/realtime-coagent-resolution.js.map +1 -0
  111. package/dist/realtime/realtime-delegation-narrator.d.ts +97 -0
  112. package/dist/realtime/realtime-delegation-narrator.d.ts.map +1 -0
  113. package/dist/realtime/realtime-delegation-narrator.js +162 -0
  114. package/dist/realtime/realtime-delegation-narrator.js.map +1 -0
  115. package/dist/realtime/realtime-session-runner.d.ts +2 -82
  116. package/dist/realtime/realtime-session-runner.d.ts.map +1 -1
  117. package/dist/realtime/realtime-session-runner.js +12 -187
  118. package/dist/realtime/realtime-session-runner.js.map +1 -1
  119. package/dist/realtime/realtime-vendor-resolution.d.ts +8 -3
  120. package/dist/realtime/realtime-vendor-resolution.d.ts.map +1 -1
  121. package/dist/realtime/realtime-vendor-resolution.js +3 -3
  122. package/dist/realtime/realtime-vendor-resolution.js.map +1 -1
  123. package/dist/rubric-evaluation-agent-runner.d.ts +2 -0
  124. package/dist/rubric-evaluation-agent-runner.d.ts.map +1 -0
  125. package/dist/rubric-evaluation-agent-runner.js +47 -0
  126. package/dist/rubric-evaluation-agent-runner.js.map +1 -0
  127. package/dist/self-check.d.ts +101 -0
  128. package/dist/self-check.d.ts.map +1 -0
  129. package/dist/self-check.js +74 -0
  130. package/dist/self-check.js.map +1 -0
  131. package/package.json +19 -18
@@ -17,9 +17,9 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
17
17
  return c > 3 && r && Object.defineProperty(target, key, r), r;
18
18
  };
19
19
  import { RegisterClass, SafeExpressionEvaluator, UUIDsEqual } from '@memberjunction/global';
20
- import { SelectOutgoingEdges } from '@memberjunction/ai-core-plus';
20
+ import { CollectDecisionStepKeys, DecisionHoldReason, DecisionReferencesIn, NO_DECISIONS, ReadFlowDecisionStepConfiguration, ResolveDecisionState, ResolveDecisionStepAnswers, SelectOutgoingEdges, ValidateTaskGraphSpec, } from '@memberjunction/ai-core-plus';
21
21
  import { AIEngineGraphRepository, SafeConditionEvaluator } from './flow-graph-adapters.js';
22
- import { CompileFlowAgentToTaskGraph, FormatFlowCompileErrors } from './flow-graph-executor.js';
22
+ import { CompileFlowAgentToTaskGraph, FormatFlowCompileErrors, FormatFlowValidationErrors } from './flow-graph-executor.js';
23
23
  import { BaseAgentType } from './base-agent-type.js';
24
24
  import { LogError, LogStatus, LogStatusEx, IsVerboseLoggingEnabled } from '@memberjunction/core';
25
25
  import { AIEngine } from '@memberjunction/aiengine';
@@ -46,9 +46,38 @@ export class FlowExecutionState {
46
46
  * later hook, so a run never switches engines part-way through.
47
47
  */
48
48
  this.executionMode = 'dispatch'; // case-violation-ok-legacy-back-compat: object literals are assigned to this class, so an accessor stub changes what they must supply
49
+ /**
50
+ * What this run's Decision steps have answered, keyed by each step's `key` and then by question.
51
+ *
52
+ * `Answers` holds the answers a path condition may act on, and is what the `decisions` root
53
+ * reads. `Unresolved` says why each of the rest is not usable (the call failed, the answer fell
54
+ * below its question's `minConfidence`, or the step was skipped), and a condition that reads one
55
+ * of them holds. Never written into the payload.
56
+ */
57
+ this.Decisions = NO_DECISIONS;
58
+ /**
59
+ * The keys of the Decision steps whose call failed in this run.
60
+ *
61
+ * A walker cannot retry, so such a decision never answers in this run, and a path reading it can
62
+ * never be taken: it is passed over, and a recovery path ranked below it can be. An answer below
63
+ * its `minConfidence` is different — the path might have been the one to take — and holds.
64
+ */
65
+ this.FailedDecisionKeys = new Set();
49
66
  this.agentId = agentId;
50
67
  }
51
68
  }
69
+ /** A run's decisions with one Decision step's answers replacing whatever that step gave before. */
70
+ function withDecisionStep(decisions, config, resolved) {
71
+ const answers = { ...decisions.Answers };
72
+ const unresolved = { ...decisions.Unresolved };
73
+ delete answers[config.key];
74
+ delete unresolved[config.key];
75
+ if (Object.keys(resolved.Answers).length > 0)
76
+ answers[config.key] = resolved.Answers;
77
+ if (Object.keys(resolved.Unresolved).length > 0)
78
+ unresolved[config.key] = resolved.Unresolved;
79
+ return { Answers: answers, Unresolved: unresolved };
80
+ }
52
81
  /**
53
82
  * Implementation of the Flow Agent Type pattern.
54
83
  *
@@ -199,7 +228,11 @@ let FlowAgentType = class FlowAgentType extends BaseAgentType {
199
228
  }
200
229
  // Find valid paths from current step
201
230
  // Pass params to enable data/context access in path conditions (Phase 1)
202
- const paths = await this.getValidPaths(flowState.currentStepId, payload, flowState, params);
231
+ const selection = await this.getValidPaths(flowState.currentStepId, payload, flowState, params);
232
+ if (selection.HoldReason) {
233
+ return this.stopOnHeldPath(flowState.currentStepId, selection.HoldReason, payload);
234
+ }
235
+ const paths = selection.Paths;
203
236
  if (paths.length === 0) {
204
237
  // No valid paths - flow is complete
205
238
  return this.createSuccessStep({
@@ -300,6 +333,7 @@ let FlowAgentType = class FlowAgentType extends BaseAgentType {
300
333
  * - flowContext: Flow execution metadata (current step, completed steps, path)
301
334
  * - data: Transient template data from ExecuteAgentParams (NEW in Phase 1)
302
335
  * - context: Runtime context from ExecuteAgentParams (NEW in Phase 1)
336
+ * - decisions: The usable answers of the run's Decision steps, by key (`decisions.<key>.<question>`)
303
337
  *
304
338
  * This allows path conditions to access runtime parameters like:
305
339
  * - `data.userApproval === true`
@@ -324,21 +358,69 @@ let FlowAgentType = class FlowAgentType extends BaseAgentType {
324
358
  */
325
359
  async getValidPaths(stepId, payload, flowState, params) {
326
360
  const repo = new AIEngineGraphRepository(flowState.agentId);
327
- const selection = SelectOutgoingEdges(stepId, repo, this._conditionEvaluator, this.buildConditionContext(payload, flowState, params));
361
+ // The Decision hold runs BEFORE any condition is evaluated, on every step's paths: a later
362
+ // step's condition can read an earlier Decision step's answer. A condition reading an answer
363
+ // that is not usable is never evaluated, because `undefined === 'billing'` is a confident,
364
+ // wrong `false`.
365
+ const selection = SelectOutgoingEdges(stepId, repo, this._conditionEvaluator, this.buildConditionContext(payload, flowState, params), (condition) => this.decisionHold(condition, flowState));
328
366
  for (const rejection of selection.Rejected) {
329
367
  // ConditionFalse is the normal case and stays quiet. The other three are all "this graph
330
368
  // is not shaped the way its author thinks", and each previously vanished: a broken
331
369
  // expression was logged as if it were a false one, and a dangling edge failed the run
332
370
  // with a message that named the missing ID but not the edge that pointed at it.
333
- if (rejection.Reason === 'ConditionFalse')
371
+ // ConditionHeld is quiet too: one that outranks the choice stops the run with its reason,
372
+ // and one ranked below the choice could not have won.
373
+ if (rejection.Reason === 'ConditionFalse' || rejection.Reason === 'ConditionHeld')
334
374
  continue;
335
375
  LogError(`Flow path ${rejection.EdgeId} from step ${stepId} not followed (${rejection.Reason})` +
336
376
  `${rejection.Detail ? `: ${rejection.Detail}` : ''}`);
337
377
  }
338
378
  const byId = new Map(AIEngine.Instance.GetPathsFromStep(stepId).map(p => [p.ID, p]));
339
- return selection.Edges
340
- .map(e => byId.get(e.id))
341
- .filter((p) => !!p);
379
+ return {
380
+ Paths: selection.Edges
381
+ .map(e => byId.get(e.id))
382
+ .filter((p) => !!p),
383
+ HoldReason: selection.Held?.Detail ?? null
384
+ };
385
+ }
386
+ /**
387
+ * Why a path condition must not be evaluated, because of a decision it reads, or `null`.
388
+ *
389
+ * A condition reading a decision whose call failed is UNANSWERABLE in this run: a walker has no
390
+ * retry, so the path can never be taken, and a recovery path ranked below it is taken instead.
391
+ * Any other unusable answer — below its `minConfidence`, or not given — holds, because the path
392
+ * might have been the one to take.
393
+ */
394
+ decisionHold(condition, flowState) {
395
+ const reason = DecisionHoldReason(condition, flowState.Decisions);
396
+ if (!reason)
397
+ return null;
398
+ const failed = DecisionReferencesIn(condition).References.find(r => flowState.FailedDecisionKeys.has(r.NodeId));
399
+ if (!failed)
400
+ return { Detail: reason };
401
+ const unresolved = flowState.Decisions.Unresolved[failed.NodeId];
402
+ return { Detail: unresolved?.[failed.QuestionKey] ?? reason, Unanswerable: true };
403
+ }
404
+ /**
405
+ * Stops the run because a step cannot choose its next step without guessing.
406
+ *
407
+ * A path ranked at or above every followable one reads a decision that is not usable, so it
408
+ * might have been the one to take. The dispatcher holds such a fork; a walker cannot wait, so it
409
+ * fails the run with the reason instead, and never takes a lower-ranked path in its place.
410
+ */
411
+ stopOnHeldPath(stepId, holdReason, payload) {
412
+ const stepName = AIEngine.Instance.GetAgentStepByID(stepId)?.Name ?? stepId;
413
+ return this.stopFlow(`Step "${stepName}" cannot choose its next step: a path from it that ranks at or above the ` +
414
+ `others reads a decision that is not usable, because ${holdReason}. The flow stops rather than guess.`, payload);
415
+ }
416
+ /** A failed step that ends the run: no recovery path is taken after it. */
417
+ stopFlow(errorMessage, payload) {
418
+ return this.createNextStep('Failed', {
419
+ errorMessage,
420
+ terminate: true,
421
+ newPayload: payload,
422
+ previousPayload: payload
423
+ });
342
424
  }
343
425
  /**
344
426
  * The variables a path condition can reference.
@@ -361,7 +443,9 @@ let FlowAgentType = class FlowAgentType extends BaseAgentType {
361
443
  stepCount: flowState.completedStepIds.size
362
444
  },
363
445
  data: params.data || {},
364
- context: params.context || {}
446
+ context: params.context || {},
447
+ // Only the usable answers. A condition reading any other one holds before it is evaluated.
448
+ decisions: flowState.Decisions.Answers
365
449
  };
366
450
  }
367
451
  /**
@@ -518,8 +602,16 @@ let FlowAgentType = class FlowAgentType extends BaseAgentType {
518
602
  flowState.executionPath.push(node.ID);
519
603
  // Store a skip marker as the step result
520
604
  flowState.stepResults.set(node.ID, { skipped: true, stepName: node.Name });
605
+ // A skipped Decision step answered nothing, so every condition that reads it holds.
606
+ if (node.StepType === 'Decision') {
607
+ this.recordDecisionOutcome(flowState, node, { Status: 'Skipped' });
608
+ }
521
609
  // Find the next step by evaluating paths from this skipped step
522
- const paths = await this.getValidPaths(node.ID, payload, flowState, params);
610
+ const selection = await this.getValidPaths(node.ID, payload, flowState, params);
611
+ if (selection.HoldReason) {
612
+ return this.stopOnHeldPath(node.ID, selection.HoldReason, payload);
613
+ }
614
+ const paths = selection.Paths;
523
615
  if (paths.length === 0) {
524
616
  // No more paths - flow is complete
525
617
  return this.createSuccessStep({
@@ -634,6 +726,8 @@ let FlowAgentType = class FlowAgentType extends BaseAgentType {
634
726
  return await this.convertForEachStepToOperation(node, payload, flowState, params);
635
727
  case 'While':
636
728
  return await this.convertWhileStepToOperation(node, payload, flowState, params);
729
+ case 'Decision':
730
+ return this.createDecisionStep(node, payload);
637
731
  default:
638
732
  return this.createNextStep('Failed', {
639
733
  errorMessage: `Unknown step type: ${node.StepType}`,
@@ -660,6 +754,120 @@ let FlowAgentType = class FlowAgentType extends BaseAgentType {
660
754
  const agent = AIEngine.Instance.Agents.find(a => UUIDsEqual(a.ID, agentId));
661
755
  return agent?.Name || null;
662
756
  }
757
+ /**
758
+ * Turns a Decision step into the `'Decision'` next step BaseAgent runs.
759
+ *
760
+ * The same hand-off a Prompt step makes with its marker: the walker does not call a model
761
+ * itself. BaseAgent runs the request through its own decision execution, so the call gets the
762
+ * prompt default, state resolution and error handling every agent decision gets, and is logged
763
+ * as a `Decision` run step. The answers come back to {@link completeDecisionStep}.
764
+ *
765
+ * A configuration that cannot be read, or a prompt that no longer exists, ends the run: the
766
+ * dispatched path refuses the same flow before it starts.
767
+ *
768
+ * A state the dispatched path refuses — missing, or empty, `{}` and whitespace-only text
769
+ * included — is refused here by the same rule (`ResolveDecisionState`), before any model is
770
+ * asked. The step then fails as a failed call does, so its recovery path runs.
771
+ */
772
+ createDecisionStep(node, payload) {
773
+ const read = ReadFlowDecisionStepConfiguration(node.Configuration);
774
+ if ('Error' in read) {
775
+ return this.stopFlow(`Decision step "${node.Name}" cannot run: ${read.Error}.`, payload);
776
+ }
777
+ const promptName = this.decisionPromptName(node);
778
+ if (promptName === null) {
779
+ return this.stopFlow(`Decision step "${node.Name}" refers to a prompt that no longer exists (${node.PromptID}). ` +
780
+ `Point it at an existing one, or clear it to use the Default Decision prompt.`, payload);
781
+ }
782
+ const { key, state, questions } = read.Config;
783
+ const resolved = ResolveDecisionState(state, payload);
784
+ if ('ErrorMessage' in resolved) {
785
+ return this.decisionNotAsked(key, resolved.ErrorMessage, payload);
786
+ }
787
+ return this.createNextStep('Decision', {
788
+ decisions: [{ id: key, state: state ?? 'payload', questions }],
789
+ ...(promptName ? { decisionPromptName: promptName } : {}),
790
+ newPayload: payload,
791
+ previousPayload: payload
792
+ });
793
+ }
794
+ /**
795
+ * A Decision step that is not asked, handed back the way BaseAgent hands back a failed call: a
796
+ * `'Retry'` carrying one failed result. {@link completeDecisionStep} then fails the step, so a
797
+ * condition reading its answers holds or is passed over, and its recovery path is taken, as
798
+ * after any failed call. No model is asked and no Decision run step is logged; the dispatched
799
+ * path logs none for a step it does not ask either.
800
+ */
801
+ decisionNotAsked(key, reason, payload) {
802
+ return this.createNextStep('Retry', {
803
+ decisionResults: [{ id: key, success: false, error: `it was not asked: ${reason}` }],
804
+ newPayload: payload,
805
+ previousPayload: payload
806
+ });
807
+ }
808
+ /**
809
+ * A Decision step's prompt by name: `undefined` when it has none (BaseAgent then uses
810
+ * `Default Decision`), `null` when the prompt it names is gone.
811
+ */
812
+ decisionPromptName(node) {
813
+ if (!node.PromptID)
814
+ return undefined;
815
+ return AIEngine.Instance.Prompts.find(p => UUIDsEqual(p.ID, node.PromptID))?.Name ?? null;
816
+ }
817
+ /**
818
+ * Records what the current Decision step answered, and turns BaseAgent's `'Retry'` into the
819
+ * step's outcome.
820
+ *
821
+ * A call that succeeded makes the step `'Success'`; one that failed makes it `'Failed'`, so a
822
+ * failed decision takes the same recovery handling as any failed step: the paths that read its
823
+ * answers are passed over, and a recovery path is taken whatever its rank.
824
+ *
825
+ * @param payload the payload the step hands on, which is its `stepResult.result`
826
+ */
827
+ completeDecisionStep(step, flowState, payload) {
828
+ const node = flowState.currentStepId ? AIEngine.Instance.GetAgentStepByID(flowState.currentStepId) : null;
829
+ const result = step.decisionResults?.[0];
830
+ const answers = result?.success && result.answers && !Array.isArray(result.answers) ? result.answers : null;
831
+ const outcome = answers
832
+ ? { Status: 'Complete', Answers: answers }
833
+ : { Status: 'Failed', ErrorMessage: result?.error || 'the decision returned no answers' };
834
+ if (node)
835
+ this.recordDecisionOutcome(flowState, node, outcome);
836
+ if (outcome.Status === 'Complete') {
837
+ return {
838
+ Next: { ...step, step: 'Success' },
839
+ StepResult: { Success: true, step: 'Success', result: payload }
840
+ };
841
+ }
842
+ const errorMessage = `Decision step "${node?.Name ?? flowState.currentStepId}" failed: ${outcome.ErrorMessage}`;
843
+ return {
844
+ Next: { ...step, step: 'Failed', errorMessage },
845
+ StepResult: { Success: false, step: 'Failed', result: payload }
846
+ };
847
+ }
848
+ /**
849
+ * Puts a Decision step's answers into the flow state under its key, replacing any it gave on an
850
+ * earlier visit. The shared answer rules decide which are usable and say why the rest are not.
851
+ */
852
+ recordDecisionOutcome(flowState, node, outcome) {
853
+ const read = ReadFlowDecisionStepConfiguration(node.Configuration);
854
+ // A step whose settings cannot be read has no key, so nothing can name it: a condition that
855
+ // tries still holds, on "no Decision step has answered".
856
+ if ('Error' in read)
857
+ return;
858
+ const run = {
859
+ Name: node.Name,
860
+ Status: outcome.Status,
861
+ ErrorMessage: outcome.Status === 'Failed' ? outcome.ErrorMessage : null
862
+ };
863
+ const given = outcome.Status === 'Complete' ? outcome.Answers : {};
864
+ const resolved = ResolveDecisionStepAnswers(run, read.Config.questions, given);
865
+ flowState.Decisions = withDecisionStep(flowState.Decisions, read.Config, resolved);
866
+ if (outcome.Status === 'Failed')
867
+ flowState.FailedDecisionKeys.add(read.Config.key);
868
+ else
869
+ flowState.FailedDecisionKeys.delete(read.Config.key);
870
+ }
663
871
  /**
664
872
  * Processes action results and applies output mapping if configured
665
873
  * This should be called by BaseAgent after action execution
@@ -1036,24 +1244,44 @@ let FlowAgentType = class FlowAgentType extends BaseAgentType {
1036
1244
  `workflow from its starting step, or split it so the portion you want is its own workflow.`
1037
1245
  });
1038
1246
  }
1039
- const compiled = CompileFlowAgentToTaskGraph(params.agent.ID, params.agent.Name, params.agent.Description ?? undefined);
1040
- if (!compiled.Success || !compiled.Spec) {
1041
- return this.createNextStep('Failed', {
1042
- errorMessage: `Workflow '${params.agent.Name}' could not be prepared to run:\n${FormatFlowCompileErrors(compiled)}`
1043
- });
1247
+ const prepared = this.compileAndValidateFlow(params);
1248
+ if ('ErrorMessage' in prepared) {
1249
+ return this.createNextStep('Failed', { errorMessage: prepared.ErrorMessage });
1044
1250
  }
1045
- this.logCompiledGraph(params.agent.Name, compiled.Spec.tasks.length, compiled.Excluded);
1251
+ this.logCompiledGraph(params.agent.Name, prepared.Spec.tasks.length, prepared.Excluded);
1046
1252
  // 'Tasks' is not in the BaseAgentNextStep step union — it is non-terminal in the same sense
1047
1253
  // as 'ClientTools'/'Plan' (see that type's doc comment), and BaseAgent routes it to
1048
1254
  // executeTasksStep, whose terminal step is 'Success'.
1049
1255
  return {
1050
1256
  step: 'Tasks',
1051
1257
  terminate: false,
1052
- taskGraph: { spec: compiled.Spec, folded: false },
1258
+ taskGraph: { spec: prepared.Spec, folded: false },
1053
1259
  previousPayload: payload,
1054
1260
  newPayload: payload
1055
1261
  };
1056
1262
  }
1263
+ /**
1264
+ * Compiles the flow and holds it to the checks `Submit` makes, or says why it cannot run.
1265
+ *
1266
+ * The dispatched path runs this before it submits, and the in-run walker before its first step
1267
+ * when the flow has a Decision step, so a flow one mode accepts the other cannot refuse — an
1268
+ * incomplete Choice fork, a decision read before it can answer, or one read through the payload
1269
+ * is refused in both. The refusal names the author's steps: a compiled graph's tempIds are step
1270
+ * IDs, and an incomplete fork, for one, is reported by its group, which is its origin's ID.
1271
+ */
1272
+ compileAndValidateFlow(params) {
1273
+ const compiled = CompileFlowAgentToTaskGraph(params.agent.ID, params.agent.Name, params.agent.Description ?? undefined);
1274
+ if (!compiled.Success || !compiled.Spec) {
1275
+ return { ErrorMessage: `Workflow '${params.agent.Name}' could not be prepared to run:\n${FormatFlowCompileErrors(compiled)}` };
1276
+ }
1277
+ const validation = ValidateTaskGraphSpec(compiled.Spec);
1278
+ if (!validation.Valid) {
1279
+ return {
1280
+ ErrorMessage: `Workflow '${params.agent.Name}' could not be prepared to run:\n${FormatFlowValidationErrors(validation.Errors, compiled.Spec)}`
1281
+ };
1282
+ }
1283
+ return { Spec: compiled.Spec, Excluded: compiled.Excluded };
1284
+ }
1057
1285
  /**
1058
1286
  * Logs what the compile produced.
1059
1287
  *
@@ -1084,6 +1312,25 @@ let FlowAgentType = class FlowAgentType extends BaseAgentType {
1084
1312
  async determineInitialStepInRun(params, payload, agentTypeState) {
1085
1313
  const flowState = agentTypeState;
1086
1314
  const payloadToUse = payload || {};
1315
+ // The compiler's own key check, over every active step: the walker can start anywhere
1316
+ // (startAtStep), so it cannot limit itself to what the entry reaches. A shared key would
1317
+ // leave a condition's `decisions.<key>` naming two steps, and the dispatched path refuses
1318
+ // the same flow before it starts.
1319
+ const activeSteps = AIEngine.Instance.GetAgentSteps(flowState.agentId, 'Active') ?? [];
1320
+ const duplicateKeys = CollectDecisionStepKeys(activeSteps).Errors;
1321
+ if (duplicateKeys.length > 0) {
1322
+ return this.stopFlow(`Workflow '${params.agent.Name}' cannot run:\n${FormatFlowCompileErrors({ Success: false, Errors: duplicateKeys, Excluded: [] })}`, payloadToUse);
1323
+ }
1324
+ // A flow with a Decision step gets the dispatched path's whole check, once, before its first
1325
+ // step: an incomplete Choice fork ends the flow "successfully" on the option nobody drew a
1326
+ // path for, and in-run is the default mode for every sub-agent flow. So such a flow must be
1327
+ // one the dispatcher would accept — which also rules out a loop drawn with a path back. A
1328
+ // flow without a Decision step is walked as it always was.
1329
+ if (activeSteps.some(s => s.StepType === 'Decision')) {
1330
+ const prepared = this.compileAndValidateFlow(params);
1331
+ if ('ErrorMessage' in prepared)
1332
+ return this.stopFlow(prepared.ErrorMessage, payloadToUse);
1333
+ }
1087
1334
  // Check for startAtStep in agentTypeParams (FlowAgentExecuteParams)
1088
1335
  const flowParams = params.agentTypeParams;
1089
1336
  if (flowParams?.startAtStep) {
@@ -1171,9 +1418,14 @@ let FlowAgentType = class FlowAgentType extends BaseAgentType {
1171
1418
  previousPayload: currentPayload
1172
1419
  });
1173
1420
  }
1421
+ // A Decision step's answers come back on the 'Retry' BaseAgent returns after running it. They
1422
+ // are recorded first, so this step's paths, and every later step's, can read them. From here
1423
+ // on the step is its outcome: Success, or Failed when the call failed.
1424
+ const decision = step.decisionResults ? this.completeDecisionStep(step, flowState, currentPayload) : null;
1425
+ const finished = decision?.Next ?? step;
1174
1426
  // Store the step result so path conditions can access it via stepResult
1175
1427
  // The step parameter contains the result from the just-completed step (Sub-Agent, Action, or Prompt)
1176
- flowState.stepResults.set(flowState.currentStepId, step);
1428
+ flowState.stepResults.set(flowState.currentStepId, decision ? decision.StepResult : step);
1177
1429
  // Add to execution path if not already present
1178
1430
  if (!flowState.executionPath.includes(flowState.currentStepId)) {
1179
1431
  flowState.executionPath.push(flowState.currentStepId);
@@ -1182,17 +1434,21 @@ let FlowAgentType = class FlowAgentType extends BaseAgentType {
1182
1434
  flowState.completedStepIds.add(flowState.currentStepId);
1183
1435
  // Find valid paths from current step using updated payload
1184
1436
  // Pass params to enable data/context access in path conditions (Phase 1)
1185
- const paths = await this.getValidPaths(flowState.currentStepId, currentPayload, flowState, params);
1437
+ const selection = await this.getValidPaths(flowState.currentStepId, currentPayload, flowState, params);
1438
+ if (selection.HoldReason) {
1439
+ return this.stopOnHeldPath(flowState.currentStepId, selection.HoldReason, currentPayload);
1440
+ }
1441
+ const paths = selection.Paths;
1186
1442
  if (paths.length === 0) {
1187
1443
  // No valid paths found
1188
1444
  // If the previous step failed, propagate the failure with its error message
1189
- if (step.step === 'Failed') {
1445
+ if (finished.step === 'Failed') {
1190
1446
  LogStatusEx({
1191
- message: `⚠️ Flow Agent: Step failed with no recovery path. Error: ${step.errorMessage || 'No error message'}`,
1447
+ message: `⚠️ Flow Agent: Step failed with no recovery path. Error: ${finished.errorMessage || 'No error message'}`,
1192
1448
  verboseOnly: false
1193
1449
  });
1194
1450
  return this.createNextStep('Failed', {
1195
- errorMessage: step.errorMessage || 'Flow step failed with no recovery path',
1451
+ errorMessage: finished.errorMessage || 'Flow step failed with no recovery path',
1196
1452
  newPayload: currentPayload,
1197
1453
  previousPayload: currentPayload,
1198
1454
  terminate: true
@@ -1221,7 +1477,7 @@ let FlowAgentType = class FlowAgentType extends BaseAgentType {
1221
1477
  });
1222
1478
  }
1223
1479
  // Log when we're navigating to a recovery path after a failure
1224
- if (step.step === 'Failed') {
1480
+ if (finished.step === 'Failed') {
1225
1481
  LogStatusEx({
1226
1482
  message: `🔄 Flow Agent: Failure detected, navigating to recovery path: '${nextStep.Name}'`,
1227
1483
  verboseOnly: false
@@ -1233,7 +1489,7 @@ let FlowAgentType = class FlowAgentType extends BaseAgentType {
1233
1489
  for (let i = 1; i < paths.length; i++) {
1234
1490
  const alternateStep = await this.getStepById(paths[i].DestinationStepID);
1235
1491
  if (alternateStep && alternateStep.Status === 'Active') {
1236
- if (step.step === 'Failed') {
1492
+ if (finished.step === 'Failed') {
1237
1493
  LogStatusEx({
1238
1494
  message: `🔄 Flow Agent: Using alternate recovery path: '${alternateStep.Name}'`,
1239
1495
  verboseOnly: false
@@ -1468,6 +1724,7 @@ let FlowAgentType = class FlowAgentType extends BaseAgentType {
1468
1724
  */
1469
1725
  GetPromptConfigurationGuidance() {
1470
1726
  return ` - Flow agents use step-level prompts (StepType="Prompt" with PromptID)\n` +
1727
+ ` - Decision steps (StepType="Decision") use PromptID for their decision prompt; empty means "Default Decision"\n` +
1471
1728
  ` - Prompts should be configured in agent steps, not AI Agent Prompts relationship\n` +
1472
1729
  ` - Verify that prompts exist in AI Prompts table and are active`;
1473
1730
  }