@memberjunction/ai-agents 6.1.0-edge.0 → 6.1.0-edge.2

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 (63) hide show
  1. package/README.md +24 -2
  2. package/dist/MJAIAgentRequestEntityServer.d.ts.map +1 -1
  3. package/dist/MJAIAgentRequestEntityServer.js +10 -1
  4. package/dist/MJAIAgentRequestEntityServer.js.map +1 -1
  5. package/dist/agent-types/base-agent-type.d.ts +23 -0
  6. package/dist/agent-types/base-agent-type.d.ts.map +1 -1
  7. package/dist/agent-types/base-agent-type.js +47 -1
  8. package/dist/agent-types/base-agent-type.js.map +1 -1
  9. package/dist/agent-types/flow-agent-prompt-params.d.ts +57 -0
  10. package/dist/agent-types/flow-agent-prompt-params.d.ts.map +1 -0
  11. package/dist/agent-types/flow-agent-prompt-params.js +8 -0
  12. package/dist/agent-types/flow-agent-prompt-params.js.map +1 -0
  13. package/dist/agent-types/flow-agent-type.d.ts +77 -6
  14. package/dist/agent-types/flow-agent-type.d.ts.map +1 -1
  15. package/dist/agent-types/flow-agent-type.js +182 -53
  16. package/dist/agent-types/flow-agent-type.js.map +1 -1
  17. package/dist/agent-types/flow-graph-adapters.d.ts +42 -0
  18. package/dist/agent-types/flow-graph-adapters.d.ts.map +1 -0
  19. package/dist/agent-types/flow-graph-adapters.js +99 -0
  20. package/dist/agent-types/flow-graph-adapters.js.map +1 -0
  21. package/dist/agent-types/flow-graph-executor.d.ts +26 -0
  22. package/dist/agent-types/flow-graph-executor.d.ts.map +1 -0
  23. package/dist/agent-types/flow-graph-executor.js +105 -0
  24. package/dist/agent-types/flow-graph-executor.js.map +1 -0
  25. package/dist/agent-types/index.d.ts +2 -0
  26. package/dist/agent-types/index.d.ts.map +1 -1
  27. package/dist/agent-types/index.js +2 -0
  28. package/dist/agent-types/index.js.map +1 -1
  29. package/dist/agent-types/loop-agent-prompt-params.d.ts +27 -0
  30. package/dist/agent-types/loop-agent-prompt-params.d.ts.map +1 -1
  31. package/dist/agent-types/loop-agent-prompt-params.js +5 -1
  32. package/dist/agent-types/loop-agent-prompt-params.js.map +1 -1
  33. package/dist/agent-types/loop-agent-response-type.d.ts +14 -3
  34. package/dist/agent-types/loop-agent-response-type.d.ts.map +1 -1
  35. package/dist/agent-types/loop-agent-response-type.js.map +1 -1
  36. package/dist/agent-types/loop-agent-type.d.ts +25 -0
  37. package/dist/agent-types/loop-agent-type.d.ts.map +1 -1
  38. package/dist/agent-types/loop-agent-type.js +109 -1
  39. package/dist/agent-types/loop-agent-type.js.map +1 -1
  40. package/dist/base-agent.d.ts +89 -10
  41. package/dist/base-agent.d.ts.map +1 -1
  42. package/dist/base-agent.js +306 -91
  43. package/dist/base-agent.js.map +1 -1
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +1 -0
  47. package/dist/index.js.map +1 -1
  48. package/dist/realtime/bridge-realtime-session-factory.d.ts.map +1 -1
  49. package/dist/realtime/bridge-realtime-session-factory.js +2 -13
  50. package/dist/realtime/bridge-realtime-session-factory.js.map +1 -1
  51. package/dist/realtime/realtime-client-session-service.d.ts +67 -16
  52. package/dist/realtime/realtime-client-session-service.d.ts.map +1 -1
  53. package/dist/realtime/realtime-client-session-service.js +79 -29
  54. package/dist/realtime/realtime-client-session-service.js.map +1 -1
  55. package/dist/realtime/realtime-coagent-config.d.ts +143 -14
  56. package/dist/realtime/realtime-coagent-config.d.ts.map +1 -1
  57. package/dist/realtime/realtime-coagent-config.js +187 -18
  58. package/dist/realtime/realtime-coagent-config.js.map +1 -1
  59. package/dist/realtime/realtime-vendor-resolution.d.ts +49 -0
  60. package/dist/realtime/realtime-vendor-resolution.d.ts.map +1 -0
  61. package/dist/realtime/realtime-vendor-resolution.js +43 -0
  62. package/dist/realtime/realtime-vendor-resolution.js.map +1 -0
  63. package/package.json +18 -18
@@ -10,7 +10,7 @@
10
10
  * @author MemberJunction.com
11
11
  * @since 2.49.0
12
12
  */
13
- import { FileStorageEngineBase } from '@memberjunction/core-entities';
13
+ import { FileStorageEngineBase, MJEnvironmentEntityExtended } from '@memberjunction/core-entities';
14
14
  import { Metadata, RunView, LogStatus, LogStatusEx, LogError, LogErrorEx, IsVerboseLoggingEnabled, DatabaseProviderBase } from '@memberjunction/core';
15
15
  import { AgentRunWatchdog } from './agent-run-watchdog.js';
16
16
  import { AIPromptRunner } from '@memberjunction/ai-prompts';
@@ -23,8 +23,9 @@ import { CrushJSON, DescribeCrush, PartitionStablePrefix } from '@memberjunction
23
23
  import { CrushCode } from '@memberjunction/context-crush/code';
24
24
  import { RealtimeSessionRunner } from './realtime/realtime-session-runner.js';
25
25
  import { ResolveNarrationInstructionsTemplate } from './realtime/realtime-narration.js';
26
- import { BuildRealtimeOverridesJson, BuildVoiceMannerSection, GetNarrationPaceMs, GetProviderVoiceSettings, GetSessionTuningSettings, DeepMergeConfigs, ResolveEffectiveRealtimeConfig } from './realtime/realtime-coagent-config.js';
27
- import { RealtimeClientSessionService } from './realtime/realtime-client-session-service.js';
26
+ import { BuildRealtimeOverridesJson, BuildVoiceMannerSection, GetNarrationPaceMs, GetProviderVoiceSettings, GetSessionTuningSettings, GetModelCatalogSessionSettings, DeepMergeConfigs, ResolveEffectiveRealtimeConfig } from './realtime/realtime-coagent-config.js';
27
+ import { SelectRealtimeVendorForModel } from './realtime/realtime-vendor-resolution.js';
28
+ import { RealtimeClientSessionService, WarnOnUnmatchedProviderVoice } from './realtime/realtime-client-session-service.js';
28
29
  import { BuildRealtimeAgentFraming } from './realtime/realtime-tool-broker.js';
29
30
  import { RealtimeRecordingController } from './realtime/realtime-recording-capture.js';
30
31
  import { resolveRecordingStorageAccountID, storeRealtimeRecording } from './realtime/realtime-recording-store.js';
@@ -38,13 +39,16 @@ import { FormatToolResultSection, FormatToolErrorSection, RenderToolResultData,
38
39
  import { PriorTurnToolResultCache } from './prior-turn-tool-result-cache.js';
39
40
  import { PromptComponentResolver, InjectScopedPromptParts } from './prompt-component-resolver.js';
40
41
  import { ScopedPromptConfigResolver, ApplyScopedPromptConfig } from './scoped-prompt-config-resolver.js';
41
- import { AIPromptParams, ChildPromptParam, ConversationUtility, ParseFileOutputRef, parseAssignmentStrategy, ResolveClientTools, initAgentRunStep, finalizeAgentRunStep, AgentRunStepSaveQueue, ExtractPromptResultText } from '@memberjunction/ai-core-plus';
42
+ import { AIPromptParams, ChildPromptParam, ConversationUtility, ParseFileOutputRef, parseAssignmentStrategy, ResolveClientTools, initAgentRunStep, finalizeAgentRunStep, AgentRunStepSaveQueue, ExtractPromptResultText, GetTaskGraphSubmitter } from '@memberjunction/ai-core-plus';
42
43
  import { AgentRunner } from './AgentRunner.js';
43
44
  import { PayloadManager } from './PayloadManager.js';
44
45
  import { ScratchpadManager } from './ScratchpadManager.js';
45
46
  import { ArtifactToolManager } from './ArtifactToolManager.js';
46
47
  import { MemoryWriteManager } from './MemoryWriteManager.js';
47
48
  import { PipelineExecutor, PipelineToolRegistry, ActionInvocable, ArtifactToolInvocable, BuildPipelineToolDocs, formatFinalOutput, summarizePipelineStages, } from './pipeline/index.js';
49
+ // The ONE payload-mapping dialect. Loop agents and the task-graph dispatcher read the same authored
50
+ // mapping strings, so these must be the same functions rather than two copies that agree today.
51
+ import { GetValueFromPath, SetMappedValue } from '@memberjunction/ai-core-plus';
48
52
  import { AgentDataPreloader } from './AgentDataPreloader.js';
49
53
  import { ClientToolRequestManager } from './ClientToolRequestManager.js';
50
54
  import { ConversationMessageResolver } from './utils/ConversationMessageResolver.js';
@@ -144,6 +148,29 @@ export class BaseAgent {
144
148
  * @private
145
149
  */
146
150
  this._agentRun = null;
151
+ /**
152
+ * The task graph this run submitted and is now waiting on, or null.
153
+ *
154
+ * **Why a run parks instead of completing.** Submitting a graph is submit-and-detach: the run
155
+ * returns as soon as the graph is durable, and the dispatcher executes it afterwards — possibly
156
+ * minutes later, possibly on another instance, and for a graph containing a human approval,
157
+ * possibly days later. That is deliberate and must not change: awaiting would hold a run (and the
158
+ * conversation turn behind it) open for the length of an approval, and a graph that settles on a
159
+ * different host could never complete an awaiting run at all.
160
+ *
161
+ * But finishing as `Completed` said something untrue. A run that reported success, a duration of
162
+ * 441ms and a green COMPLETED badge, above a workflow where nothing had happened yet, is not a
163
+ * display problem — the row itself claimed to be done. So the run ends in `Paused`, which the
164
+ * conversation UI already reads as in-progress, and the DISPATCHER completes it when the graph
165
+ * settles (`TaskGraphDispatcher.settleSubmittingRun`). Nothing blocks, and no row claims to be
166
+ * finished while work it caused is still in flight.
167
+ *
168
+ * Set for whichever run submitted — root or sub-agent. A sub-agent that dispatches a graph parks
169
+ * its own run exactly the same way; its parent is unaffected and completes normally, which is
170
+ * correct, because the parent's own work IS done. What a viewer of the PARENT sees is covered by
171
+ * the run tree, which walks into the sub-agent's graph and reports it still running.
172
+ */
173
+ this._awaitingWorkflowTaskID = null;
147
174
  /**
148
175
  * Stores the ID of an AIAgentRequest created when a Chat step fires.
149
176
  * Populated by executeChatStep(), returned in ExecuteAgentResult.feedbackRequestId.
@@ -602,7 +629,7 @@ export class BaseAgent {
602
629
  // Check if this param is marked as MediaOutput in action metadata
603
630
  // Note: 'MediaOutput' ValueType is added in v3.1.x migration.
604
631
  // Before CodeGen runs, this property may not exist on the entity type.
605
- const paramMetadata = actionEntity?.Params?.find(p => p.Name === param.Name);
632
+ const paramMetadata = actionEntity?.Params?.Items.find(p => p.Name === param.Name);
606
633
  const valueType = paramMetadata?.ValueType;
607
634
  const isMediaOutputParam = valueType === 'MediaOutput';
608
635
  // Handle MediaOutput params (e.g., Generate Image action)
@@ -1355,6 +1382,7 @@ export class BaseAgent {
1355
1382
  // passed in, not our chained signal.
1356
1383
  params.cancellationToken = upstreamToken;
1357
1384
  this.releasePerRunDataCache();
1385
+ await this.finalizeRun(this.deriveRunOutcome());
1358
1386
  }
1359
1387
  }
1360
1388
  /**
@@ -1369,6 +1397,32 @@ export class BaseAgent {
1369
1397
  AgentDataPreloader.Instance.clearRunCache(this._agentRun.ID);
1370
1398
  }
1371
1399
  }
1400
+ /**
1401
+ * Subclass extension point for per-run cleanup of resources this instance owns outside of
1402
+ * MJ's own tracked state (e.g. an external sandbox or session). No-op by default. Called
1403
+ * unconditionally from `Execute()`'s top-level `finally` block, once per `Execute()` call,
1404
+ * so it runs on every exit path (success, failure, or cancellation) exactly like
1405
+ * {@link releasePerRunDataCache}.
1406
+ */
1407
+ async finalizeRun(outcome) {
1408
+ // Intentionally empty — subclasses override as needed.
1409
+ }
1410
+ /**
1411
+ * Maps the just-completed run's final `AgentRun.Status` to the 3-value outcome that
1412
+ * {@link finalizeRun} hooks care about. `'AwaitingFeedback'` is a normal settled end-of-turn
1413
+ * (see {@link settledRunStatuses}) — the conversational turn is genuinely over, not paused
1414
+ * mid-`Execute()` — so it maps to `'success'`, same as `'Completed'`.
1415
+ */
1416
+ deriveRunOutcome() {
1417
+ switch (this._agentRun?.Status) {
1418
+ case 'Cancelled':
1419
+ return 'cancelled';
1420
+ case 'Failed':
1421
+ return 'failure';
1422
+ default:
1423
+ return 'success';
1424
+ }
1425
+ }
1372
1426
  // =====================================================================================
1373
1427
  // REALTIME (SESSION-DRIVEN) AGENT SUPPORT
1374
1428
  //
@@ -1544,19 +1598,26 @@ export class BaseAgent {
1544
1598
  // a usable model exists. This mirrors the same fix in RealtimeClientSessionService.
1545
1599
  const candidates = this.selectRealtimeModelCandidates(params.agent, overrideModelID);
1546
1600
  for (const model of candidates) {
1547
- const vendor = this.selectRealtimeVendor(model.ID);
1601
+ const vendor = SelectRealtimeVendorForModel(model.ID);
1548
1602
  if (!vendor) {
1549
1603
  continue;
1550
1604
  }
1551
- const apiKey = GetAIAPIKey(vendor.driverClass);
1605
+ const apiKey = GetAIAPIKey(vendor.DriverClass);
1552
1606
  if (!apiKey) {
1553
1607
  continue;
1554
1608
  }
1555
- const instance = MJGlobal.Instance.ClassFactory.CreateInstance(BaseRealtimeModel, vendor.driverClass, apiKey);
1609
+ const instance = MJGlobal.Instance.ClassFactory.CreateInstance(BaseRealtimeModel, vendor.DriverClass, apiKey);
1556
1610
  if (!instance) {
1557
1611
  continue;
1558
1612
  }
1559
- return { model: instance, modelID: model.ID, vendorID: vendor.vendorID, apiName: vendor.apiName, driverClass: vendor.driverClass };
1613
+ return {
1614
+ model: instance,
1615
+ modelID: model.ID,
1616
+ vendorID: vendor.VendorID,
1617
+ modelVendorID: vendor.ModelVendorID,
1618
+ apiName: vendor.APIName,
1619
+ driverClass: vendor.DriverClass
1620
+ };
1560
1621
  }
1561
1622
  return null;
1562
1623
  }
@@ -1622,24 +1683,6 @@ export class BaseAgent {
1622
1683
  }
1623
1684
  return ResolveEffectiveRealtimeConfig(typeDefault, agent.TypeConfiguration ?? null, null);
1624
1685
  }
1625
- /**
1626
- * Selects the highest-priority active vendor for a model whose `DriverClass` has a resolvable
1627
- * API key. Mirrors the vendor-selection pattern used by prompt execution.
1628
- *
1629
- * @param modelID The chosen model's ID.
1630
- * @returns The vendor driver/api identifiers, or `null` when none has a usable key.
1631
- */
1632
- selectRealtimeVendor(modelID) {
1633
- const vendors = AIEngine.Instance.ModelVendors
1634
- .filter(mv => UUIDsEqual(mv.ModelID, modelID) && mv.Status === 'Active' && mv.DriverClass != null)
1635
- .sort((a, b) => (b.Priority ?? 0) - (a.Priority ?? 0));
1636
- for (const v of vendors) {
1637
- if (GetAIAPIKey(v.DriverClass)) {
1638
- return { vendorID: v.VendorID ?? '', driverClass: v.DriverClass, apiName: v.APIName ?? '' };
1639
- }
1640
- }
1641
- return null;
1642
- }
1643
1686
  /**
1644
1687
  * Creates the single long-lived `AIPromptRun` that realtime usage is checkpointed onto.
1645
1688
  *
@@ -1699,7 +1742,7 @@ export class BaseAgent {
1699
1742
  */
1700
1743
  async buildRealtimeSessionDeps(params, config, modelResolution, promptRun) {
1701
1744
  const effectiveConfig = this.resolveRealtimeEffectiveConfig(params.agent);
1702
- const sessionParams = await this.buildRealtimeSessionParams(params, config, modelResolution.apiName, effectiveConfig, modelResolution.driverClass);
1745
+ const sessionParams = await this.buildRealtimeSessionParams(params, config, modelResolution.apiName, effectiveConfig, modelResolution.driverClass, modelResolution.modelID, modelResolution.modelVendorID);
1703
1746
  return {
1704
1747
  Model: modelResolution.model,
1705
1748
  SessionParams: sessionParams,
@@ -1737,7 +1780,7 @@ export class BaseAgent {
1737
1780
  * @param modelApiName The vendor API name of the resolved realtime model.
1738
1781
  * @returns The session parameters.
1739
1782
  */
1740
- async buildRealtimeSessionParams(params, config, modelApiName, effectiveConfig, driverClass) {
1783
+ async buildRealtimeSessionParams(params, config, modelApiName, effectiveConfig, driverClass, modelID, modelVendorID) {
1741
1784
  // Identity framing comes from the ONE shared producer so the agent speaks first-person AS the
1742
1785
  // TARGET (Sage / Marketing Agent / …), identical to every other realtime host — not as the co-agent.
1743
1786
  // See BuildRealtimeAgentFraming + plans/realtime/realtime-core-host-convergence.md.
@@ -1750,13 +1793,22 @@ export class BaseAgent {
1750
1793
  const systemPrompt = [framing, basePrompt, voiceManner, memoryContext]
1751
1794
  .filter(part => part && part.trim().length > 0)
1752
1795
  .join('\n\n');
1753
- // Provider-matched voice settings (realtime.voice.providers.<provider>) AND session-tuning
1754
- // knobs (realtime.session) flow into the driver's open Config bag — the same pact every
1755
- // other config entry rides, mirroring the client-direct builder's cascade exactly.
1796
+ // Model-catalog defaults (the AIModelType < AIModel < AIModelVendor ModelConfiguration
1797
+ // cascade) merge as the BASE layer, then the voice settings — the persona's agnostic
1798
+ // wire-level slots (see RealtimeVoicePersona) plus any matching
1799
+ // `realtime.voice.providers.<provider>` bag —
1800
+ // AND session-tuning knobs (realtime.session) flow into the driver's open Config bag: the same
1801
+ // pact every other config entry rides, mirroring the client-direct builder's cascade exactly.
1802
+ // Same unmatched-provider diagnosis too, so this surface cannot drift back into dropping
1803
+ // authored settings silently (#3530).
1804
+ const catalogSettings = modelID
1805
+ ? GetModelCatalogSessionSettings(AIEngine.Instance.GetEffectiveModelConfiguration(modelID, modelVendorID))
1806
+ : null;
1756
1807
  const providerVoice = GetProviderVoiceSettings(effectiveConfig, driverClass ?? null);
1808
+ WarnOnUnmatchedProviderVoice(effectiveConfig, driverClass, 'BaseAgent.buildRealtimeSessionParams');
1757
1809
  const sessionTuning = GetSessionTuningSettings(effectiveConfig);
1758
- const configBag = (sessionTuning || providerVoice)
1759
- ? DeepMergeConfigs(sessionTuning, providerVoice)
1810
+ const configBag = (catalogSettings || sessionTuning || providerVoice)
1811
+ ? DeepMergeConfigs(catalogSettings, sessionTuning, providerVoice)
1760
1812
  : undefined;
1761
1813
  return {
1762
1814
  Model: modelApiName,
@@ -3124,6 +3176,12 @@ export class BaseAgent {
3124
3176
  case 'ClientTools':
3125
3177
  // Client tools are valid - execution handled by executeClientToolsStep
3126
3178
  return nextStep;
3179
+ // Type assertion required because 'Tasks' is not part of the BaseAgentNextStep step
3180
+ // union (submit-and-detach — the terminal step it produces is 'Success'). The graph was
3181
+ // already validated against TaskGraphSpec by the agent type, so there is nothing left
3182
+ // to check here; submission is handled by executeTasksStep.
3183
+ case 'Tasks':
3184
+ return nextStep;
3127
3185
  default:
3128
3186
  // if we get here, the next step is not recognized, we can return a retry step
3129
3187
  this.logError(`Invalid next step '${nextStep.step}' for agent '${params.agent.Name}'`, {
@@ -3782,6 +3840,29 @@ export class BaseAgent {
3782
3840
  */
3783
3841
  async hasExceededAgentRunGuardrails(params, agentRun) {
3784
3842
  const agent = params.agent;
3843
+ // Refresh the run's accumulated cost/token actuals before comparing them to the agent's
3844
+ // static limits.
3845
+ //
3846
+ // The LIMITS are static — MaxCostPerRun and MaxTokensPerRun live on the agent and never
3847
+ // change during a run. What was missing is the other side of the comparison: TotalCost and
3848
+ // TotalTokensUsed are DERIVED from the run's steps by calculateTokenStats(), and the only
3849
+ // writer (applyTokenStatsToRun) previously ran on terminal paths alone —
3850
+ // createFailureResult / createCancelledResult / finalizeAgentRun — plus the post-compaction
3851
+ // top-up, which only fires if compaction happens to trigger.
3852
+ //
3853
+ // So mid-run both fields sat at 0, and because the checks below are guarded on
3854
+ // `agent.MaxCostPerRun && agentRun.TotalCost`, a falsy 0 short-circuited them entirely. The
3855
+ // cost and token ceilings were evaluated only at the moment a run ENDED, which is too late
3856
+ // to stop anything: they became reporting, not guardrails. Only the iteration and time
3857
+ // limits actually interrupted a run, because TotalPromptIterations is incremented in the
3858
+ // loop.
3859
+ //
3860
+ // Recomputing here rather than at the call site means every caller — including subclasses
3861
+ // that override the loop — gets a truthful comparison, and the recompute is cheap: it walks
3862
+ // the in-memory step array, with no database round trip.
3863
+ if (this._agentRun === agentRun) {
3864
+ this.applyTokenStatsToRun(agentRun, this.calculateTokenStats());
3865
+ }
3785
3866
  // Check absolute maximum iterations (safety net to prevent infinite loops)
3786
3867
  const DEFAULT_ABSOLUTE_MAX_ITERATIONS = 5000;
3787
3868
  const absoluteMaxIterations = params.absoluteMaxIterations ?? DEFAULT_ABSOLUTE_MAX_ITERATIONS;
@@ -5610,6 +5691,20 @@ The context is now within limits. Please retry your request with the recovered c
5610
5691
  responseType[responseTypeKey] = true;
5611
5692
  }
5612
5693
  }
5694
+ // Capability gates align the OTHER way: default OFF, and the section appears only when the
5695
+ // gate is explicitly on. Running these through the loop above would invert their meaning —
5696
+ // that loop's `else` branch defaults an unset key to true, which is exactly what a gated
5697
+ // capability must never do.
5698
+ const gateMappings = [
5699
+ { gateFlag: 'enableTaskGraphs', responseTypeKey: 'tasks' }
5700
+ ];
5701
+ for (const { gateFlag, responseTypeKey } of gateMappings) {
5702
+ const wasExplicitlySet = explicitResponseType &&
5703
+ Object.prototype.hasOwnProperty.call(explicitResponseType, responseTypeKey);
5704
+ if (!wasExplicitlySet) {
5705
+ responseType[responseTypeKey] = params[gateFlag] === true;
5706
+ }
5707
+ }
5613
5708
  }
5614
5709
  /**
5615
5710
  * Extracts default values from a JSON Schema definition.
@@ -5967,12 +6062,12 @@ The context is now within limits. Please retry your request with the recovered c
5967
6062
  const lines = [];
5968
6063
  lines.push(`### ${action.Name}`);
5969
6064
  lines.push(action.Description);
5970
- const inputParams = action.Params
6065
+ const inputParams = action.Params.Items
5971
6066
  .filter(p => {
5972
6067
  const t = p.Type.trim().toLowerCase();
5973
6068
  return t === 'input' || t === 'both';
5974
6069
  });
5975
- const outputParams = action.Params
6070
+ const outputParams = action.Params.Items
5976
6071
  .filter(p => {
5977
6072
  const t = p.Type.trim().toLowerCase();
5978
6073
  return t === 'output' || t === 'both';
@@ -5983,8 +6078,8 @@ The context is now within limits. Please retry your request with the recovered c
5983
6078
  if (outputParams.length > 0) {
5984
6079
  lines.push(`**Output:** ${outputParams.map(p => this.formatActionParameter(p)).join(', ')}`);
5985
6080
  }
5986
- if (action.ResultCodes.length > 0) {
5987
- const rcParts = action.ResultCodes.map(rc => {
6081
+ if (action.ResultCodes.Items.length > 0) {
6082
+ const rcParts = action.ResultCodes.Items.map(rc => {
5988
6083
  const marker = rc.IsSuccess ? '✓' : '✗';
5989
6084
  const desc = rc.Description && rc.Description.toLowerCase() !== rc.ResultCode.toLowerCase()
5990
6085
  ? ` ${rc.Description}`
@@ -6590,6 +6685,10 @@ The context is now within limits. Please retry your request with the recovered c
6590
6685
  // If neither is set, EffortLevel remains null (no effort level override was used)
6591
6686
  // Set parent run ID if we're in a sub-agent context
6592
6687
  this._agentRun.ParentRunID = params.parentRun?.ID;
6688
+ // Only a continuation deliverer sets this; every ordinary run stays at the column default of
6689
+ // 0. It has to be recorded on the run itself because the run is the only thing that outlives
6690
+ // the dispatcher that started it, and the next graph this run submits reads it back.
6691
+ this._agentRun.ContinuationDepth = params.continuationDepth ?? 0;
6593
6692
  // Set LastRunID for run chaining (different from ParentRunID)
6594
6693
  if (params.lastRunId) {
6595
6694
  // Check for circular references in the run chain
@@ -7166,6 +7265,10 @@ The context is now within limits. Please retry your request with the recovered c
7166
7265
  }
7167
7266
  return await this.executePromptStep(params, config, previousDecision, stepCount);
7168
7267
  case 'Sub-Agent':
7268
+ // A Sub-Agent step carrying a task graph is a FOLDED graph (D9): the agent type
7269
+ // rewrote a one-node graph into an ordinary in-run call. Record it before running,
7270
+ // so the run shows what was emitted even if the sub-agent then fails.
7271
+ await this.recordFoldedTaskGraph(params, previousDecision);
7169
7272
  return await this.processSubAgentStep(params, previousDecision, undefined, undefined, stepCount);
7170
7273
  case 'Actions':
7171
7274
  return await this.executeActionsStep(params, previousDecision, undefined, true, stepCount);
@@ -7179,6 +7282,11 @@ The context is now within limits. Please retry your request with the recovered c
7179
7282
  // (Plan Mode). executePlanStep's terminal return is 'Chat'-shaped (see its doc comment).
7180
7283
  case 'Plan':
7181
7284
  return await this.executePlanStep(params, previousDecision);
7285
+ // Type assertion required because 'Tasks' is not part of the BaseAgentNextStep step
7286
+ // union — LoopAgentType.DetermineNextStep() emits it when an opted-in agent submits a
7287
+ // durable task graph. executeTasksStep detaches (see its doc comment).
7288
+ case 'Tasks':
7289
+ return await this.executeTasksStep(params, previousDecision);
7182
7290
  // Type assertion required because 'ClientTools' is not part of the BaseAgentNextStep
7183
7291
  // step union — LoopAgentType.DetermineNextStep() emits it when the LLM chooses client tools.
7184
7292
  case 'ClientTools':
@@ -8793,24 +8901,14 @@ The context is now within limits. Please retry your request with the recovered c
8793
8901
  * @param value - Value to set or append
8794
8902
  * @private
8795
8903
  */
8904
+ /**
8905
+ * Sets a value on a target object, honouring the `[]` array-append suffix.
8906
+ *
8907
+ * Delegates for the same reason as {@link getValueFromPath} — `results[]` must mean append in
8908
+ * both engines, or a workflow accumulates in one and overwrites in the other.
8909
+ */
8796
8910
  setMappedValue(target, key, value) {
8797
- const isArrayAppend = key.endsWith('[]');
8798
- const actualKey = isArrayAppend ? key.slice(0, -2) : key;
8799
- if (isArrayAppend) {
8800
- // Array append operation
8801
- if (!(actualKey in target)) {
8802
- target[actualKey] = [];
8803
- }
8804
- if (!Array.isArray(target[actualKey])) {
8805
- throw new Error(`Cannot append to '${actualKey}': target is not an array. ` +
8806
- `Use '${actualKey}' without [] suffix for property update.`);
8807
- }
8808
- target[actualKey].push(value);
8809
- }
8810
- else {
8811
- // Normal property assignment
8812
- target[actualKey] = value;
8813
- }
8911
+ SetMappedValue(target, key, value);
8814
8912
  }
8815
8913
  /**
8816
8914
  * Applies sub-agent input mapping to prepare initial payload for related sub-agent.
@@ -9731,6 +9829,131 @@ The context is now within limits. Please retry your request with the recovered c
9731
9829
  *
9732
9830
  * @protected
9733
9831
  */
9832
+ /**
9833
+ * Submits a durable task graph and ends the turn (submit-and-detach).
9834
+ *
9835
+ * **Why detaching is the whole point.** The alternative — holding the run open until the graph
9836
+ * finishes — is exactly what the old client-driven path did, and it is what made a page reload
9837
+ * lose the work, a server restart orphan it, and every non-Explorer channel unable to reach it
9838
+ * at all. Once submission returns, the graph lives in Task rows and a server-side dispatcher
9839
+ * owns it; the run has no reason to stay alive, so it doesn't.
9840
+ *
9841
+ * **The run step is written whether or not submission succeeds.** A rejected graph is the case
9842
+ * where forensics matter most — the operator needs to see what the model emitted and why it was
9843
+ * refused, and a step written only on the happy path would erase precisely that.
9844
+ *
9845
+ * **A missing submitter is reported, never swallowed.** In a host with no durable-execution
9846
+ * package loaded there is nobody to run the graph. Returning a failure the model can read is
9847
+ * the honest outcome; silently continuing would leave the agent believing it had scheduled work
9848
+ * that does not exist.
9849
+ *
9850
+ * @protected
9851
+ */
9852
+ async executeTasksStep(params, previousDecision) {
9853
+ const graph = previousDecision.taskGraph;
9854
+ const spec = graph?.spec;
9855
+ if (!spec) {
9856
+ return {
9857
+ step: 'Failed',
9858
+ terminate: true,
9859
+ errorMessage: 'A Tasks step reached execution with no task graph attached.',
9860
+ previousPayload: previousDecision.previousPayload,
9861
+ newPayload: previousDecision.newPayload || previousDecision.previousPayload
9862
+ };
9863
+ }
9864
+ const stepEntity = await this.createStepEntity({
9865
+ stepType: 'TaskGraph',
9866
+ stepName: `Task Graph: ${spec.workflowName}`,
9867
+ contextUser: params.contextUser,
9868
+ inputData: { spec, folded: false }
9869
+ });
9870
+ const submitter = GetTaskGraphSubmitter();
9871
+ if (!submitter) {
9872
+ const errorMessage = 'No task-graph submitter is registered on this host, so the graph could not be made durable. ' +
9873
+ 'Task graphs require the durable-execution package to be loaded.';
9874
+ await this.finalizeStepEntity(stepEntity, false, errorMessage, { spec, submitted: false });
9875
+ return {
9876
+ step: 'Failed',
9877
+ terminate: true,
9878
+ errorMessage,
9879
+ previousPayload: previousDecision.previousPayload,
9880
+ newPayload: previousDecision.newPayload || previousDecision.previousPayload
9881
+ };
9882
+ }
9883
+ const outcome = await submitter.Submit({
9884
+ Spec: spec,
9885
+ EnvironmentID: MJEnvironmentEntityExtended.DefaultEnvironmentID,
9886
+ ConversationDetailID: params.conversationDetailId ?? null,
9887
+ AgentRunID: this._agentRun?.ID ?? null,
9888
+ // If THIS run was itself started by a finished graph, the graph it emits inherits that
9889
+ // depth + 1. Without this the chain restarts at zero on every hop and
9890
+ // MAX_REINVOKE_DEPTH never fires — a graph reinvoking an agent that emits a graph would
9891
+ // run forever.
9892
+ ReinvokeDepth: this._agentRun?.ContinuationDepth ?? 0,
9893
+ ContextUser: params.contextUser,
9894
+ Provider: this.ProviderToUse
9895
+ });
9896
+ await this.finalizeStepEntity(stepEntity, outcome.Success, outcome.ErrorMessage, { spec, submitted: outcome.Success, parentTaskID: outcome.ParentTaskID });
9897
+ if (!outcome.Success) {
9898
+ return {
9899
+ step: 'Failed',
9900
+ terminate: true,
9901
+ errorMessage: outcome.ErrorMessage || 'Task graph submission failed.',
9902
+ previousPayload: previousDecision.previousPayload,
9903
+ newPayload: previousDecision.newPayload || previousDecision.previousPayload
9904
+ };
9905
+ }
9906
+ // Success means "this graph is durable and will run", NOT "this graph has run". Saying
9907
+ // otherwise here is the exact lie the old await-everything path told when it returned early.
9908
+ //
9909
+ // The run PARKS on it rather than completing — see `_awaitingWorkflowTaskID`. Recorded only
9910
+ // on a successful submission with a real parent task: a failed submission has nothing to
9911
+ // wait for, and a graph nobody can point at could never be settled by the dispatcher, which
9912
+ // would leave the run parked forever.
9913
+ if (outcome.ParentTaskID) {
9914
+ this._awaitingWorkflowTaskID = outcome.ParentTaskID;
9915
+ }
9916
+ return {
9917
+ step: 'Success',
9918
+ terminate: true,
9919
+ message: previousDecision.message ||
9920
+ `Started **${spec.workflowName}** — ${spec.tasks.length} task(s) running. ` +
9921
+ `I'll follow up when it finishes.`,
9922
+ reasoning: previousDecision.reasoning,
9923
+ confidence: previousDecision.confidence,
9924
+ previousPayload: previousDecision.previousPayload,
9925
+ newPayload: previousDecision.newPayload || previousDecision.previousPayload
9926
+ };
9927
+ }
9928
+ /**
9929
+ * Records a constant-folded task graph (D9) without submitting it.
9930
+ *
9931
+ * Folding rewrites a one-node graph into an ordinary in-run sub-agent call, which means no Task
9932
+ * row and no dispatcher hop. Writing the step anyway is what keeps that decision legible: run
9933
+ * forensics show why a graph did not reach the dispatcher, a user who edits a two-node graph
9934
+ * down to one can read the durability change off the run record instead of inferring it, and
9935
+ * Save as Workflow (D17) can attach to the recorded spec — so the single-node case, the shape
9936
+ * most likely worth promoting, is promotable like any other.
9937
+ *
9938
+ * @protected
9939
+ */
9940
+ async recordFoldedTaskGraph(params, nextStep) {
9941
+ const graph = nextStep.taskGraph;
9942
+ if (!graph?.folded)
9943
+ return;
9944
+ const stepEntity = await this.createStepEntity({
9945
+ stepType: 'TaskGraph',
9946
+ stepName: `Task Graph (folded): ${graph.spec.workflowName}`,
9947
+ contextUser: params.contextUser,
9948
+ inputData: { spec: graph.spec, folded: true, foldReason: graph.foldReason }
9949
+ });
9950
+ await this.finalizeStepEntity(stepEntity, true, undefined, {
9951
+ spec: graph.spec,
9952
+ folded: true,
9953
+ foldReason: graph.foldReason,
9954
+ submitted: false
9955
+ });
9956
+ }
9734
9957
  async executePlanStep(params, previousDecision) {
9735
9958
  const planText = previousDecision.planDetails?.plan ?? '';
9736
9959
  const stepEntity = await this.createStepEntity({
@@ -10812,7 +11035,15 @@ The context is now within limits. Please retry your request with the recovered c
10812
11035
  ? this.resolveMediaPlaceholdersInPayload(finalStep.actionableCommands)
10813
11036
  : finalStep.actionableCommands;
10814
11037
  if (this._agentRun) {
10815
- this._agentRun.CompletedAt = new Date();
11038
+ // A run waiting on a workflow has NOT completed, so it gets no completion time. The
11039
+ // dispatcher stamps this when the graph settles; until then the absence is the honest
11040
+ // record, and a duration computed from it would be the 441ms it took to hand the work
11041
+ // off rather than the time the work took.
11042
+ const parkedOnWorkflow = !!this._awaitingWorkflowTaskID
11043
+ && (finalStep.step === 'Success' || finalStep.step === 'Chat');
11044
+ if (!parkedOnWorkflow) {
11045
+ this._agentRun.CompletedAt = new Date();
11046
+ }
10816
11047
  this._agentRun.Success = finalStep.step === 'Success' || finalStep.step === 'Chat';
10817
11048
  if (!this._agentRun.Success) {
10818
11049
  // Capture error message from either errorMessage or message field
@@ -10826,6 +11057,17 @@ The context is now within limits. Please retry your request with the recovered c
10826
11057
  // set status to Failed
10827
11058
  this._agentRun.Status = 'Failed';
10828
11059
  }
11060
+ else if (parkedOnWorkflow) {
11061
+ // Waiting on a task graph the dispatcher is still executing. `Paused` rather than a
11062
+ // new status because it already exists on the entity AND the conversation's process
11063
+ // panel already reads it as in-progress (`Status === 'Running' || === 'Paused'`), so
11064
+ // a parked run presents correctly with no UI change.
11065
+ //
11066
+ // `Success` stays true: the turn genuinely succeeded at what it was asked to do —
11067
+ // start the workflow. Whether the WORKFLOW succeeded is a different question, and
11068
+ // the dispatcher answers it here when the graph settles.
11069
+ this._agentRun.Status = 'Paused';
11070
+ }
10829
11071
  else if (finalStep.step === 'Chat') {
10830
11072
  // Chat steps mean the agent is waiting for human input
10831
11073
  this._agentRun.Status = 'AwaitingFeedback';
@@ -11797,43 +12039,16 @@ The context is now within limits. Please retry your request with the recovered c
11797
12039
  });
11798
12040
  }
11799
12041
  /**
11800
- * Helper to get value from nested object path - extracts from LoopAgentType
12042
+ * Reads a value out of a nested object by dotted path, with `name[0]` array indexing.
12043
+ *
12044
+ * **Delegates to the one implementation.** This used to be a private copy of the same walk the
12045
+ * task-graph dispatcher performs, and the two had to agree exactly: a Loop agent and the
12046
+ * compiled graph of the same workflow read the SAME authored mapping strings, so any divergence
12047
+ * would make a workflow behave differently depending on which engine ran it — silently, and only
12048
+ * for the paths where they differed.
11801
12049
  */
11802
12050
  getValueFromPath(obj, path) {
11803
- const parts = path.split('.');
11804
- let current = obj;
11805
- for (const part of parts) {
11806
- if (!part)
11807
- continue;
11808
- // Check for array indexing
11809
- const arrayMatch = part.match(/^([^[]+)\[(\d+)\]$/);
11810
- if (arrayMatch) {
11811
- const arrayName = arrayMatch[1];
11812
- const index = parseInt(arrayMatch[2], 10);
11813
- if (current && typeof current === 'object' && arrayName in current) {
11814
- current = current[arrayName];
11815
- if (Array.isArray(current) && index >= 0 && index < current.length) {
11816
- current = current[index];
11817
- }
11818
- else {
11819
- return undefined;
11820
- }
11821
- }
11822
- else {
11823
- return undefined;
11824
- }
11825
- }
11826
- else {
11827
- // Regular property access
11828
- if (current && typeof current === 'object' && part in current) {
11829
- current = current[part];
11830
- }
11831
- else {
11832
- return undefined;
11833
- }
11834
- }
11835
- }
11836
- return current;
12051
+ return GetValueFromPath(obj, path);
11837
12052
  }
11838
12053
  /**
11839
12054
  * Parse the agent's SecondaryScopeConfig from the ScopeConfig field.