@memberjunction/ng-conversations 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 (62) hide show
  1. package/README.md +9 -1
  2. package/dist/lib/components/conversation/conversation-agent-picker.component.js +1 -1
  3. package/dist/lib/components/conversation/conversation-agent-picker.component.js.map +1 -1
  4. package/dist/lib/components/conversation/conversation-chat-area.component.d.ts +146 -6
  5. package/dist/lib/components/conversation/conversation-chat-area.component.d.ts.map +1 -1
  6. package/dist/lib/components/conversation/conversation-chat-area.component.js +603 -130
  7. package/dist/lib/components/conversation/conversation-chat-area.component.js.map +1 -1
  8. package/dist/lib/components/conversation/pinned-messages-panel.component.d.ts +3 -1
  9. package/dist/lib/components/conversation/pinned-messages-panel.component.d.ts.map +1 -1
  10. package/dist/lib/components/conversation/pinned-messages-panel.component.js +22 -8
  11. package/dist/lib/components/conversation/pinned-messages-panel.component.js.map +1 -1
  12. package/dist/lib/components/message/actionable-commands.component.d.ts +12 -0
  13. package/dist/lib/components/message/actionable-commands.component.d.ts.map +1 -1
  14. package/dist/lib/components/message/actionable-commands.component.js +60 -12
  15. package/dist/lib/components/message/actionable-commands.component.js.map +1 -1
  16. package/dist/lib/components/message/message-input.component.d.ts +93 -4
  17. package/dist/lib/components/message/message-input.component.d.ts.map +1 -1
  18. package/dist/lib/components/message/message-input.component.js +224 -25
  19. package/dist/lib/components/message/message-input.component.js.map +1 -1
  20. package/dist/lib/components/message/message-item.component.d.ts +87 -2
  21. package/dist/lib/components/message/message-item.component.d.ts.map +1 -1
  22. package/dist/lib/components/message/message-item.component.js +214 -32
  23. package/dist/lib/components/message/message-item.component.js.map +1 -1
  24. package/dist/lib/components/message/message-list.component.d.ts +20 -1
  25. package/dist/lib/components/message/message-list.component.d.ts.map +1 -1
  26. package/dist/lib/components/message/message-list.component.js +76 -1
  27. package/dist/lib/components/message/message-list.component.js.map +1 -1
  28. package/dist/lib/components/workspace/conversation-workspace.component.d.ts.map +1 -1
  29. package/dist/lib/components/workspace/conversation-workspace.component.js +4 -1
  30. package/dist/lib/components/workspace/conversation-workspace.component.js.map +1 -1
  31. package/dist/lib/models/agent-turn.model.d.ts +12 -2
  32. package/dist/lib/models/agent-turn.model.d.ts.map +1 -1
  33. package/dist/lib/models/agent-turn.model.js.map +1 -1
  34. package/dist/lib/services/conversation-agent.service.d.ts +21 -0
  35. package/dist/lib/services/conversation-agent.service.d.ts.map +1 -1
  36. package/dist/lib/services/conversation-agent.service.js +28 -2
  37. package/dist/lib/services/conversation-agent.service.js.map +1 -1
  38. package/dist/lib/services/conversation-liveness-dom.service.d.ts +42 -0
  39. package/dist/lib/services/conversation-liveness-dom.service.d.ts.map +1 -0
  40. package/dist/lib/services/conversation-liveness-dom.service.js +74 -0
  41. package/dist/lib/services/conversation-liveness-dom.service.js.map +1 -0
  42. package/dist/lib/services/ui-command-handler.service.d.ts +40 -3
  43. package/dist/lib/services/ui-command-handler.service.d.ts.map +1 -1
  44. package/dist/lib/services/ui-command-handler.service.js +78 -9
  45. package/dist/lib/services/ui-command-handler.service.js.map +1 -1
  46. package/dist/lib/util/frame-zone.d.ts +10 -0
  47. package/dist/lib/util/frame-zone.d.ts.map +1 -0
  48. package/dist/lib/util/frame-zone.js +10 -0
  49. package/dist/lib/util/frame-zone.js.map +1 -0
  50. package/dist/lib/utils/agent-turn-routing.d.ts +7 -6
  51. package/dist/lib/utils/agent-turn-routing.d.ts.map +1 -1
  52. package/dist/lib/utils/agent-turn-routing.js +4 -16
  53. package/dist/lib/utils/agent-turn-routing.js.map +1 -1
  54. package/dist/lib/utils/decision-routing.d.ts +68 -0
  55. package/dist/lib/utils/decision-routing.d.ts.map +1 -0
  56. package/dist/lib/utils/decision-routing.js +101 -0
  57. package/dist/lib/utils/decision-routing.js.map +1 -0
  58. package/dist/public-api.d.ts +2 -0
  59. package/dist/public-api.d.ts.map +1 -1
  60. package/dist/public-api.js +5 -0
  61. package/dist/public-api.js.map +1 -1
  62. package/package.json +33 -32
@@ -3,7 +3,9 @@ import { BaseAngularComponent } from '@memberjunction/ng-base-types';
3
3
  import { LogStatusEx } from '@memberjunction/core';
4
4
  import { MJEnvironmentEntityExtended, ConversationEngine, UserInfoEngine, TaskGraphSubmitOperation } from '@memberjunction/core-entities';
5
5
  import { BeforeAgentTurnEventArgs, AfterAgentTurnEventArgs } from '../../events/chat-events';
6
- import { ResolveAgentTurn, IsAgentAllowed, FindDisallowedTaskGraphAgents } from '../../utils/agent-turn-routing';
6
+ import { ResolveAgentTurn, FindDisallowedTaskGraphAgents } from '../../utils/agent-turn-routing';
7
+ import { ApplyRoutingDecision, ArtifactVersionForTurn, RunRoutingDecision, ShouldRunRoutingDecision } from '../../utils/decision-routing';
8
+ import { BuildRecentTurns, BuildRoutingArtifactVersions, CanAskRoutingDecision, CollectRoutingParticipants, IsAgentAllowed } from '@memberjunction/ai-core-plus';
7
9
  import { GenerateAndApplyConversationName } from '../../services/conversation-naming';
8
10
  import { AIEngineBase } from '@memberjunction/ai-engine-base';
9
11
  import { ConversationUtility, agentFailureDisposition, agentFailureMessage } from '@memberjunction/ai-core-plus';
@@ -12,6 +14,7 @@ import { PlanModePreference } from '../../utils/plan-mode-preference';
12
14
  import { MJNotificationService } from '@memberjunction/ng-notifications';
13
15
  import { BuildRealtimeConfigOverridesJson, FilterRealtimeCoAgents, LoadCoAgentPairings, PairingsAllowTarget } from '../../services/realtime-pairing';
14
16
  import { UUIDsEqual } from '@memberjunction/global';
17
+ import { InjectFrameZone } from '../../util/frame-zone';
15
18
  import * as i0 from "@angular/core";
16
19
  import * as i1 from "../../services/dialog.service";
17
20
  import * as i2 from "../../services/toast.service";
@@ -47,6 +50,8 @@ function MessageInputComponent_ng_template_5_Template(rf, ctx) { if (rf & 1) {
47
50
  const ctx_r0 = i0.ɵɵnextContext();
48
51
  i0.ɵɵproperty("Provider", ctx_r0.ProviderToUse)("CurrentUser", ctx_r0.currentUser)("Agents", ctx_r0.voicePickerAgents)("DefaultAgentId", ctx_r0.voicePickerDefaultAgentId)("CoAgents", ctx_r0.voicePickerCoAgents)("DefaultCoAgentId", ctx_r0.voicePickerDefaultCoAgentId);
49
52
  } }
53
+ /** Streamed-delta render cadence where no animation frame will come (hidden tab, no rAF). */
54
+ const STREAMED_FRAME_FALLBACK_MS = 50;
50
55
  export class MessageInputComponent extends BaseAngularComponent {
51
56
  dialogService;
52
57
  toastService;
@@ -96,6 +101,10 @@ export class MessageInputComponent extends BaseAngularComponent {
96
101
  get disabled() {
97
102
  return this.Disabled;
98
103
  }
104
+ /**
105
+ * Blocks sending. Distinct from {@link Disabled}, which means the composer is busy.
106
+ */
107
+ ReadOnly = false;
99
108
  Placeholder = 'Type a message... (Ctrl+Enter to send)';
100
109
  /** @deprecated Use {@link Placeholder}. */
101
110
  set placeholder(value) {
@@ -504,6 +513,21 @@ export class MessageInputComponent extends BaseAngularComponent {
504
513
  * today's behavior; a host that names its own chats turns it off.
505
514
  */
506
515
  AutoNameConversation = true;
516
+ /**
517
+ * Whether an unmentioned message asks one typed decision which agent in the conversation should
518
+ * answer it, instead of always going back to the last agent that answered. The same call can
519
+ * name the artifact version the message modifies. The answer replaces continuity only when it
520
+ * is confident and arrives in time; an error, a slow or unsure answer keeps today's routing, and
521
+ * a tagged message or a form response makes no call (see `decision-routing.ts`). False (the
522
+ * default) changes nothing: no call, and today's routing.
523
+ *
524
+ * What goes to the model: each qualifying message sends these to the model behind the
525
+ * `Default Decision` prompt, which can be a different vendor from the agents' own: the first
526
+ * 1,000 characters of the new message, the last 6 turns (150 characters each), each participant's
527
+ * name, description and last reply, and the names of their artifact versions. Each call writes an
528
+ * `MJ: AI Prompt Runs` row, even when the answer comes too late to be used.
529
+ */
530
+ EnableDecisionRouting = false;
507
531
  MessageSent = new EventEmitter();
508
532
  /**
509
533
  * @deprecated Use {@link MessageSent}.
@@ -513,6 +537,14 @@ export class MessageInputComponent extends BaseAngularComponent {
513
537
  * initialise in order, and the other way round this captures undefined.
514
538
  */
515
539
  messageSent = this.MessageSent;
540
+ /**
541
+ * A streamed final-response delta was applied to an in-progress message: `Message` holds the
542
+ * full reply so far. Informational, so no Before/After pair; coalesced to at most one emission
543
+ * per animation frame; silent once the message has settled, because the completion path owns
544
+ * the final render. Hosts refresh the bubble in place on this, while {@link MessageSent} keeps
545
+ * announcing new and status-changed messages.
546
+ */
547
+ MessageStreamed = new EventEmitter();
516
548
  AgentResponse = new EventEmitter();
517
549
  /**
518
550
  * @deprecated Use {@link AgentResponse}.
@@ -885,6 +917,7 @@ export class MessageInputComponent extends BaseAngularComponent {
885
917
  }
886
918
  // Track completion timestamps to prevent race conditions with late progress updates
887
919
  completionTimestamps = new Map();
920
+ ngZone = InjectFrameZone();
888
921
  // Track registered streaming callbacks for cleanup
889
922
  registeredCallbacks = new Map();
890
923
  // After a post-ACK disconnect, keep observing ConversationDetail.Status until
@@ -976,6 +1009,9 @@ export class MessageInputComponent extends BaseAngularComponent {
976
1009
  * Called from setter or ngAfterViewInit when conditions are met.
977
1010
  */
978
1011
  triggerInitialSend() {
1012
+ if (this.ReadOnly) {
1013
+ return;
1014
+ }
979
1015
  const message = this._initialMessage;
980
1016
  const attachments = this._initialAttachments;
981
1017
  const hasContent = !!message || !!(attachments && attachments.length > 0);
@@ -1075,7 +1111,7 @@ export class MessageInputComponent extends BaseAngularComponent {
1075
1111
  mentionedAgentId = undefined;
1076
1112
  /** True when the mic button should be enabled (have an agent + not disabled). */
1077
1113
  get CanStartRealtime() {
1078
- return !this.Disabled && !this.VoiceActive && !!this.ResolveCurrentAgentId();
1114
+ return !this.ReadOnly && !this.Disabled && !this.VoiceActive && !!this.ResolveCurrentAgentId();
1079
1115
  }
1080
1116
  /** @deprecated Use {@link CanStartRealtime}. */
1081
1117
  get canStartRealtime() {
@@ -1390,11 +1426,51 @@ export class MessageInputComponent extends BaseAngularComponent {
1390
1426
  * Create a progress callback for a specific message ID.
1391
1427
  * This callback will be invoked by the streaming service when progress updates arrive.
1392
1428
  */
1429
+ /**
1430
+ * Emits {@link MessageStreamed} for a coalesced frame of streamed deltas, unless the message
1431
+ * settled in the meantime: then the completion path has already reconciled the bubble and a
1432
+ * late frame has nothing to add. Settled is read off the captured entity's status and off the
1433
+ * callback registration, which markMessageComplete drops: a frame can sleep through completion
1434
+ * in a hidden tab, and the registration outlasts every other trace of it.
1435
+ */
1436
+ emitStreamedUpdate(messageId, message) {
1437
+ if (message.Status !== 'In-Progress' || !this.registeredCallbacks.has(messageId)) {
1438
+ return;
1439
+ }
1440
+ if (this.MessageStreamed.observed) {
1441
+ this.MessageStreamed.emit(message);
1442
+ }
1443
+ else {
1444
+ // TRANSITIONAL: a host that still binds only the old output keeps streaming on MessageSent at
1445
+ // the coalesced cadence. Remove once direct hosts bind MessageStreamed. Re-enters the zone
1446
+ // because MessageSent handlers replace template-bound state.
1447
+ this.ngZone.run(() => this.MessageSent.emit(message));
1448
+ }
1449
+ }
1450
+ /**
1451
+ * Runs `callback` on the next animation frame, outside Angular: the in-place render checks its
1452
+ * own child view, so a zone-triggered application tick per frame would be pure waste, and the
1453
+ * paths that do touch template-bound state re-enter the zone themselves. A hidden tab never
1454
+ * paints a frame, so there (and without requestAnimationFrame) a short timer stands in; the
1455
+ * browser throttles it in the background, the right cadence for a bubble nobody is looking at.
1456
+ */
1457
+ scheduleFrame(callback) {
1458
+ this.ngZone.runOutsideAngular(() => {
1459
+ if (typeof requestAnimationFrame === 'function' && !document.hidden) {
1460
+ requestAnimationFrame(callback);
1461
+ }
1462
+ else {
1463
+ setTimeout(callback, STREAMED_FRAME_FALLBACK_MS);
1464
+ }
1465
+ });
1466
+ }
1393
1467
  createMessageProgressCallback(messageId) {
1394
1468
  // Resolve the message once and reuse it for the callback's lifetime: streamed
1395
1469
  // final-response updates arrive per content delta, and re-awaiting the cache on
1396
1470
  // every delta both wastes work and (on a cold cache) races concurrent loads.
1397
1471
  let resolvedMessage = null;
1472
+ // One pending frame per message: deltas arriving inside a frame collapse into one emission.
1473
+ let streamedFramePending = false;
1398
1474
  return async (progress) => {
1399
1475
  try {
1400
1476
  // Get message from cache (single source of truth)
@@ -1420,9 +1496,15 @@ export class MessageInputComponent extends BaseAngularComponent {
1420
1496
  // bubble with the server-saved final message, so no append/merge is needed here.
1421
1497
  if (progress.streaming) {
1422
1498
  message.Message = progress.streaming.content;
1423
- this.MessageSent.emit(message);
1424
- // Keep the tasks dropdown on a stable status line rather than the growing reply text.
1425
- this.activeTasks.updateStatusByConversationDetailId(message.ID, 'Responding…');
1499
+ if (!streamedFramePending) {
1500
+ streamedFramePending = true;
1501
+ this.scheduleFrame(() => {
1502
+ streamedFramePending = false;
1503
+ this.emitStreamedUpdate(messageId, message);
1504
+ });
1505
+ // Keep the tasks dropdown on a stable status line rather than the growing reply text.
1506
+ this.activeTasks.updateStatusByConversationDetailId(message.ID, 'Responding…');
1507
+ }
1426
1508
  return;
1427
1509
  }
1428
1510
  // Default: plain message (used by RunAIAgentResolver and TaskOrchestrator without step info)
@@ -1463,7 +1545,7 @@ export class MessageInputComponent extends BaseAngularComponent {
1463
1545
  this.registeredCallbacks.clear();
1464
1546
  }
1465
1547
  get CanSend() {
1466
- return !this.Disabled && !this.IsSending && this.MessageText.trim().length > 0;
1548
+ return !this.ReadOnly && !this.Disabled && !this.IsSending && this.MessageText.trim().length > 0;
1467
1549
  }
1468
1550
  /** @deprecated Use {@link CanSend}. */
1469
1551
  get canSend() {
@@ -1493,6 +1575,9 @@ export class MessageInputComponent extends BaseAngularComponent {
1493
1575
  * Handle text submitted from the input box
1494
1576
  */
1495
1577
  async OnTextSubmitted(text) {
1578
+ if (this.ReadOnly) {
1579
+ return;
1580
+ }
1496
1581
  // Check if we have either text or attachments
1497
1582
  const hasText = text && text.trim().length > 0;
1498
1583
  const hasAttachments = this.pendingAttachments.length > 0;
@@ -1619,6 +1704,9 @@ export class MessageInputComponent extends BaseAngularComponent {
1619
1704
  * explicitly and we merge + dedupe (by `id`) before saving.
1620
1705
  */
1621
1706
  async SendMessageWithText(text, extraAttachments) {
1707
+ if (this.ReadOnly) {
1708
+ return false;
1709
+ }
1622
1710
  const merged = (() => {
1623
1711
  if (!extraAttachments || extraAttachments.length === 0) {
1624
1712
  return [...this.pendingAttachments];
@@ -1771,6 +1859,8 @@ export class MessageInputComponent extends BaseAngularComponent {
1771
1859
  * Routing picks the agent ({@link resolveAgentTurnTarget}): a tagged agent, then (under
1772
1860
  * `AgentReplyMode` `'Always'`) the last agent that answered, the conversation's pinned agent,
1773
1861
  * the host's default agent, and the conversation manager, each only if the host allows it.
1862
+ * With {@link EnableDecisionRouting} on, a routing decision may first put another agent in the
1863
+ * last agent's place ({@link decideAgentRouting}).
1774
1864
  * {@link BeforeAgentTurn} is then fired once, before any reply row exists, and the turn runs on
1775
1865
  * MJ's path or through the host's {@link AgentTurnHandler}.
1776
1866
  */
@@ -1796,7 +1886,12 @@ export class MessageInputComponent extends BaseAngularComponent {
1796
1886
  void this.nameConversation(messageDetail.Message, messageDetail.ConversationID)
1797
1887
  .catch(error => console.error('Conversation naming failed:', error));
1798
1888
  }
1799
- const target = this.resolveAgentTurnTarget(mentionResult);
1889
+ // Read the candidates before any await. This composer stays cached while the person opens
1890
+ // another conversation, and the chat area then rebinds its history and pinned agent, so a
1891
+ // read after the routing decision could see another conversation's state (or none).
1892
+ const candidates = this.agentTurnCandidates(this.agentMentionIds(mentionResult));
1893
+ const routing = await this.decideAgentRouting(messageDetail, candidates);
1894
+ const target = this.resolveAgentTurnTarget(candidates, routing);
1800
1895
  if (!target) {
1801
1896
  await this.finishWithoutAgentTurn(messageDetail, 'NoAgent');
1802
1897
  return;
@@ -1806,7 +1901,81 @@ export class MessageInputComponent extends BaseAngularComponent {
1806
1901
  await this.finishWithoutAgentTurn(messageDetail, 'Declined');
1807
1902
  return;
1808
1903
  }
1809
- await this.runAgentTurn(messageDetail, mentionResult, turn);
1904
+ await this.runAgentTurn(messageDetail, mentionResult, turn, routing);
1905
+ }
1906
+ /**
1907
+ * Asks the routing decision for this message when {@link EnableDecisionRouting} is on and the
1908
+ * message qualifies (see `ShouldRunRoutingDecision`). Returns null, with no call, otherwise, and
1909
+ * when anything fails: the message then keeps today's routing.
1910
+ *
1911
+ * @param candidates The turn's candidates, read before any await (see {@link routeMessage}).
1912
+ */
1913
+ async decideAgentRouting(message, candidates) {
1914
+ const continuityAgentId = candidates.ContinuityAgentId;
1915
+ const qualifies = ShouldRunRoutingDecision({
1916
+ Enabled: this.EnableDecisionRouting,
1917
+ ReplyMode: this.AgentReplyMode,
1918
+ MentionedAgentIds: candidates.MentionedAgentIds,
1919
+ Message: message.Message ?? '',
1920
+ ContinuityAgentId: continuityAgentId
1921
+ });
1922
+ if (!qualifies || !continuityAgentId) {
1923
+ return null;
1924
+ }
1925
+ try {
1926
+ // Built before the first await, so it reads the same conversation as the candidates.
1927
+ const input = this.buildRoutingDecisionInput(message, continuityAgentId);
1928
+ if (!CanAskRoutingDecision(input)) {
1929
+ return null;
1930
+ }
1931
+ input.ArtifactVersions = await this.loadRoutingArtifactVersions(message.ConversationID, input.Participants);
1932
+ const outcome = await RunRoutingDecision(input, params => this.agentService.RunDecision(params));
1933
+ LogStatusEx({
1934
+ message: `Decision routing: ${outcome.Verdict}, ${outcome.Reason} (prompt run ${outcome.PromptRunID ?? 'none'})`,
1935
+ verboseOnly: true
1936
+ });
1937
+ return outcome;
1938
+ }
1939
+ catch (error) {
1940
+ console.warn('Decision routing failed, so the message keeps continuity:', error);
1941
+ return null;
1942
+ }
1943
+ }
1944
+ /**
1945
+ * The routing decision's input, rebuilt from the conversation as it is now: the agents that
1946
+ * have answered within the history floor, the last few turns, and the conversation manager.
1947
+ * Artifact versions are added after, once the input is known to be worth asking about.
1948
+ *
1949
+ * Participants come only from the '@' list's agents (`GetAvailableAgents`): active, top-level,
1950
+ * unrestricted agents this person has run permission for. An agent that answered here but that
1951
+ * the person can't run (a shared conversation, a revoked permission) is never offered, so routing
1952
+ * can't send the turn to an agent the server would refuse. Before that list has loaded there are
1953
+ * no participants, so no call is made and the message keeps today's routing. Speaker names in
1954
+ * the recent turns still come from the full catalog, since any agent may have spoken.
1955
+ */
1956
+ buildRoutingDecisionInput(message, continuityAgentId) {
1957
+ const history = this.ConversationHistory.filter(row => this.isWithinHistoryFloor(row) && !UUIDsEqual(row.ID, message.ID));
1958
+ const findAgent = (agentId) => AIEngineBase.Instance.Agents.find(a => UUIDsEqual(a.ID, agentId));
1959
+ const runnable = this.mentionAutocomplete.GetAvailableAgents();
1960
+ const findRunnable = (agentId) => runnable.find(a => UUIDsEqual(a.ID, agentId));
1961
+ const manager = this.ConverationManagerAgent;
1962
+ return {
1963
+ Message: message.Message ?? '',
1964
+ ContinuityAgentId: continuityAgentId,
1965
+ Participants: CollectRoutingParticipants(history, manager?.ID ?? null, this.AllowedAgentIDs, findRunnable),
1966
+ ConversationManager: manager?.ID && IsAgentAllowed(manager.ID, this.AllowedAgentIDs) ? manager : null,
1967
+ RecentTurns: BuildRecentTurns(history, findAgent),
1968
+ ArtifactVersions: [],
1969
+ AllowedAgentIDs: this.AllowedAgentIDs
1970
+ };
1971
+ }
1972
+ /** Every participant's artifact versions in this conversation, for the artifact question. */
1973
+ async loadRoutingArtifactVersions(conversationId, participants) {
1974
+ const artifactsByAgent = await Promise.all(participants.map(async (participant) => ({
1975
+ Agent: participant.Agent,
1976
+ Artifacts: await this.agentService.FindAgentArtifacts(conversationId, participant.Agent.ID, this.AgentHistoryFrom)
1977
+ })));
1978
+ return BuildRoutingArtifactVersions(artifactsByAgent);
1810
1979
  }
1811
1980
  /** The host's rules, as the routing functions take them. */
1812
1981
  get agentTurnRules() {
@@ -1822,10 +1991,16 @@ export class MessageInputComponent extends BaseAngularComponent {
1822
1991
  ConversationManagerAgentId: this.ConverationManagerAgent?.ID ?? null
1823
1992
  };
1824
1993
  }
1825
- /** Picks the agent that answers this message and the route that chose it, or null for no turn. */
1826
- resolveAgentTurnTarget(mentionResult) {
1827
- const mentionedAgentIds = mentionResult.mentions.filter(m => m.type === 'agent').map(m => m.id);
1828
- return ResolveAgentTurn(this.agentTurnCandidates(mentionedAgentIds), this.agentTurnRules, agentId => this.isKnownAgent(agentId));
1994
+ /**
1995
+ * Picks the agent that answers this message and the route that chose it, or null for no turn.
1996
+ * A routing decision, when there was one, is applied to the candidates first.
1997
+ */
1998
+ resolveAgentTurnTarget(candidates, routing) {
1999
+ return ResolveAgentTurn(ApplyRoutingDecision(candidates, routing), this.agentTurnRules, agentId => this.isKnownAgent(agentId));
2000
+ }
2001
+ /** The agents a message tags, in the order they appear. */
2002
+ agentMentionIds(mentionResult) {
2003
+ return mentionResult.mentions.filter(m => m.type === 'agent').map(m => m.id);
1829
2004
  }
1830
2005
  /**
1831
2006
  * Fires {@link BeforeAgentTurn} for the turn routing picked — before any reply row exists —
@@ -1862,11 +2037,16 @@ export class MessageInputComponent extends BaseAngularComponent {
1862
2037
  }
1863
2038
  await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
1864
2039
  }
1865
- /** Runs a turn that {@link BeforeAgentTurn} let through, on the host's handler or MJ's path. */
1866
- async runAgentTurn(userMessage, mentionResult, turn) {
2040
+ /**
2041
+ * Runs a turn that {@link BeforeAgentTurn} let through, on the host's handler or MJ's path.
2042
+ * The routing decision, when there was one, supplies the artifact version the turn's agent
2043
+ * continues from; a host's handler receives it as `TargetArtifactVersionId`.
2044
+ */
2045
+ async runAgentTurn(userMessage, mentionResult, turn, routing = null) {
1867
2046
  const mention = turn.Route === 'Mention' ? this.findAgentMention(mentionResult, turn.AgentId) : null;
2047
+ const targetArtifactVersionId = ArtifactVersionForTurn(routing, turn.AgentId);
1868
2048
  if (this.AgentTurnHandler) {
1869
- await this.runHostAgentTurn(this.AgentTurnHandler, userMessage, turn, mention);
2049
+ await this.runHostAgentTurn(this.AgentTurnHandler, userMessage, turn, mention, targetArtifactVersionId);
1870
2050
  return;
1871
2051
  }
1872
2052
  if (mention) {
@@ -1877,7 +2057,7 @@ export class MessageInputComponent extends BaseAngularComponent {
1877
2057
  }
1878
2058
  else {
1879
2059
  // Pinned and host defaults keep their direct call even when they name the manager, as before.
1880
- await this.handleAgentContinuity(userMessage, turn.AgentId);
2060
+ await this.handleAgentContinuity(userMessage, turn.AgentId, targetArtifactVersionId);
1881
2061
  }
1882
2062
  }
1883
2063
  /**
@@ -1917,9 +2097,15 @@ export class MessageInputComponent extends BaseAngularComponent {
1917
2097
  * method are preserved (not deleted) so we can reintroduce intent checking in the future
1918
2098
  * when browser-local inference is fast enough (~20-50ms) to do this without blocking the
1919
2099
  * user. See PR #2309 for the full discussion.
2100
+ *
2101
+ * With {@link EnableDecisionRouting} on, a typed routing decision runs before routing instead
2102
+ * ({@link decideAgentRouting}); the prompt-based check stays unused. A confident answer from it
2103
+ * may name the artifact version this agent should continue from.
2104
+ *
2105
+ * @param targetArtifactVersionId The artifact version the routing decision named, or null.
1920
2106
  */
1921
- async handleAgentContinuity(messageDetail, agentId) {
1922
- await this.continueWithAgent(messageDetail, agentId, messageDetail.ConversationID, undefined // artifact version targeting unavailable without intent check
2107
+ async handleAgentContinuity(messageDetail, agentId, targetArtifactVersionId = null) {
2108
+ await this.continueWithAgent(messageDetail, agentId, messageDetail.ConversationID, targetArtifactVersionId ?? undefined // set only by a confident routing decision
1923
2109
  );
1924
2110
  }
1925
2111
  /**
@@ -2005,8 +2191,9 @@ export class MessageInputComponent extends BaseAngularComponent {
2005
2191
  * written here: the host writes the reply rows and reports them, and each is shown like a row
2006
2192
  * MJ wrote — one still In-Progress is followed like any other in-progress reply.
2007
2193
  */
2008
- async runHostAgentTurn(handler, userMessage, turn, mention) {
2009
- const result = await this.callAgentTurnHandler(handler, this.buildAgentTurnRequest(userMessage, turn, mention));
2194
+ async runHostAgentTurn(handler, userMessage, turn, mention, targetArtifactVersionId) {
2195
+ const request = this.buildAgentTurnRequest(userMessage, turn, mention, targetArtifactVersionId);
2196
+ const result = await this.callAgentTurnHandler(handler, request);
2010
2197
  if (!result.Success) {
2011
2198
  this.notifyAgentTurnProblem(result.ErrorMessage || 'The agent could not answer this message.', 'error');
2012
2199
  }
@@ -2028,8 +2215,13 @@ export class MessageInputComponent extends BaseAngularComponent {
2028
2215
  return { Success: false, ErrorMessage: error instanceof Error ? error.message : String(error) };
2029
2216
  }
2030
2217
  }
2031
- /** What the host's handler is told about the turn. */
2032
- buildAgentTurnRequest(userMessage, turn, mention) {
2218
+ /**
2219
+ * What the host's handler is told about the turn.
2220
+ *
2221
+ * @param targetArtifactVersionId The artifact version the routing decision named for this
2222
+ * turn's agent, or null.
2223
+ */
2224
+ buildAgentTurnRequest(userMessage, turn, mention, targetArtifactVersionId) {
2033
2225
  return {
2034
2226
  ConversationId: userMessage.ConversationID,
2035
2227
  UserMessageId: userMessage.ID,
@@ -2043,7 +2235,8 @@ export class MessageInputComponent extends BaseAngularComponent {
2043
2235
  // A mention carries its own preset (or none); every other route follows the header picker.
2044
2236
  ConfigurationPresetId: turn.Route === 'Mention' ? (mention?.configurationId ?? null) : this.AgentConfigurationPresetId,
2045
2237
  RequestedSkillIDs: [...this._pendingRequestedSkillIDs],
2046
- PlanMode: this.PlanModeEnabled
2238
+ PlanMode: this.PlanModeEnabled,
2239
+ TargetArtifactVersionId: targetArtifactVersionId
2047
2240
  };
2048
2241
  }
2049
2242
  /** Shows the rows a host's handler reported for its turn, oldest first. */
@@ -3207,7 +3400,7 @@ export class MessageInputComponent extends BaseAngularComponent {
3207
3400
  } if (rf & 2) {
3208
3401
  let _t;
3209
3402
  i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.InputBox = _t.first);
3210
- } }, inputs: { ConversationId: "ConversationId", conversationId: "conversationId", ConversationName: "ConversationName", conversationName: "conversationName", CurrentUser: "CurrentUser", currentUser: "currentUser", Disabled: "Disabled", disabled: "disabled", Placeholder: "Placeholder", placeholder: "placeholder", ParentMessageId: "ParentMessageId", parentMessageId: "parentMessageId", EnableAttachments: "EnableAttachments", enableAttachments: "enableAttachments", EnableMentions: "EnableMentions", enableMentions: "enableMentions", EnableAgentMentions: "EnableAgentMentions", enableAgentMentions: "enableAgentMentions", EnableEntityMentions: "EnableEntityMentions", enableEntityMentions: "enableEntityMentions", EnableSkillCommands: "EnableSkillCommands", enableSkillCommands: "enableSkillCommands", EnablePlanMode: "EnablePlanMode", enablePlanMode: "enablePlanMode", EnableRealtime: "EnableRealtime", enableRealtime: "enableRealtime", MaxAttachments: "MaxAttachments", maxAttachments: "maxAttachments", MaxAttachmentSizeBytes: "MaxAttachmentSizeBytes", maxAttachmentSizeBytes: "maxAttachmentSizeBytes", AcceptedFileTypes: "AcceptedFileTypes", acceptedFileTypes: "acceptedFileTypes", ArtifactsByDetailId: "ArtifactsByDetailId", artifactsByDetailId: "artifactsByDetailId", SystemArtifactsByDetailId: "SystemArtifactsByDetailId", systemArtifactsByDetailId: "systemArtifactsByDetailId", AgentRunsByDetailId: "AgentRunsByDetailId", agentRunsByDetailId: "agentRunsByDetailId", EmptyStateMode: "EmptyStateMode", emptyStateMode: "emptyStateMode", AppContext: "AppContext", appContext: "appContext", DefaultAgentId: "DefaultAgentId", defaultAgentId: "defaultAgentId", ConversationDefaultAgentId: "ConversationDefaultAgentId", conversationDefaultAgentId: "conversationDefaultAgentId", AgentConfigurationPresetId: "AgentConfigurationPresetId", agentConfigurationPresetId: "agentConfigurationPresetId", InitialMessage: "InitialMessage", initialMessage: "initialMessage", InitialAttachments: "InitialAttachments", initialAttachments: "initialAttachments", ConversationHistory: "ConversationHistory", conversationHistory: "conversationHistory", InProgressMessageIds: "InProgressMessageIds", inProgressMessageIds: "inProgressMessageIds", ApplicationId: "ApplicationId", applicationId: "applicationId", AgentReplyMode: "AgentReplyMode", AllowedAgentIDs: "AllowedAgentIDs", MentionPeople: "MentionPeople", AgentHistoryFrom: "AgentHistoryFrom", AgentTurnHandler: "AgentTurnHandler", AutoNameConversation: "AutoNameConversation", InitialDraft: "InitialDraft", initialDraft: "initialDraft" }, outputs: { MessageSent: "MessageSent", messageSent: "messageSent", AgentResponse: "AgentResponse", agentResponse: "agentResponse", BeforeAgentTurn: "BeforeAgentTurn", beforeAgentTurn: "beforeAgentTurn", AfterAgentTurn: "AfterAgentTurn", afterAgentTurn: "afterAgentTurn", AgentRunDetected: "AgentRunDetected", agentRunDetected: "agentRunDetected", AgentRunUpdate: "AgentRunUpdate", agentRunUpdate: "agentRunUpdate", MessageComplete: "MessageComplete", messageComplete: "messageComplete", ArtifactCreated: "ArtifactCreated", artifactCreated: "artifactCreated", ConversationRenamed: "ConversationRenamed", conversationRenamed: "conversationRenamed", IntentCheckStarted: "IntentCheckStarted", intentCheckStarted: "intentCheckStarted", IntentCheckCompleted: "IntentCheckCompleted", intentCheckCompleted: "intentCheckCompleted", InitialMessageAutoSendStarted: "InitialMessageAutoSendStarted", initialMessageAutoSendStarted: "initialMessageAutoSendStarted", InitialMessageAutoSendFailed: "InitialMessageAutoSendFailed", initialMessageAutoSendFailed: "initialMessageAutoSendFailed", EmptyStateSubmit: "EmptyStateSubmit", emptyStateSubmit: "emptyStateSubmit", UploadStateChanged: "UploadStateChanged", uploadStateChanged: "uploadStateChanged", InitialDraftApplied: "InitialDraftApplied", initialDraftApplied: "initialDraftApplied", DraftStateChanged: "DraftStateChanged", ComposerBlurred: "ComposerBlurred" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature, i0.ɵɵNgOnChangesFeature], decls: 6, vars: 29, consts: [["pickerOrigin", "cdkOverlayOrigin"], ["inputBox", ""], ["cdkOverlayOrigin", "", 1, "message-input-wrapper"], [1, "processing-indicator"], [3, "valueChange", "blurred", "textSubmitted", "attachmentsChanged", "attachmentError", "voiceRequested", "voiceOptionsRequested", "planModeToggle", "placeholder", "disabled", "showCharacterCount", "enableMentions", "EnableAgentMentions", "EnableEntityMentions", "EnableSkillCommands", "TargetAgentId", "AllowedAgentIDs", "MentionPeople", "enableAttachments", "maxAttachments", "maxAttachmentSizeBytes", "acceptedFileTypes", "currentUser", "Provider", "rows", "enableRealtime", "voiceActive", "canStartRealtime", "enablePlanMode", "planModeActive", "value"], ["cdkConnectedOverlay", "", "cdkConnectedOverlayBackdropClass", "cdk-overlay-transparent-backdrop", 3, "backdropClick", "detach", "cdkConnectedOverlayOrigin", "cdkConnectedOverlayOpen", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPush", "cdkConnectedOverlayHasBackdrop"], [1, "fas", "fa-circle-notch", "fa-spin"], [3, "AgentPicked", "Cancelled", "Provider", "CurrentUser", "Agents", "DefaultAgentId", "CoAgents", "DefaultCoAgentId"]], template: function MessageInputComponent_Template(rf, ctx) { if (rf & 1) {
3403
+ } }, inputs: { ConversationId: "ConversationId", conversationId: "conversationId", ConversationName: "ConversationName", conversationName: "conversationName", CurrentUser: "CurrentUser", currentUser: "currentUser", Disabled: "Disabled", disabled: "disabled", ReadOnly: "ReadOnly", Placeholder: "Placeholder", placeholder: "placeholder", ParentMessageId: "ParentMessageId", parentMessageId: "parentMessageId", EnableAttachments: "EnableAttachments", enableAttachments: "enableAttachments", EnableMentions: "EnableMentions", enableMentions: "enableMentions", EnableAgentMentions: "EnableAgentMentions", enableAgentMentions: "enableAgentMentions", EnableEntityMentions: "EnableEntityMentions", enableEntityMentions: "enableEntityMentions", EnableSkillCommands: "EnableSkillCommands", enableSkillCommands: "enableSkillCommands", EnablePlanMode: "EnablePlanMode", enablePlanMode: "enablePlanMode", EnableRealtime: "EnableRealtime", enableRealtime: "enableRealtime", MaxAttachments: "MaxAttachments", maxAttachments: "maxAttachments", MaxAttachmentSizeBytes: "MaxAttachmentSizeBytes", maxAttachmentSizeBytes: "maxAttachmentSizeBytes", AcceptedFileTypes: "AcceptedFileTypes", acceptedFileTypes: "acceptedFileTypes", ArtifactsByDetailId: "ArtifactsByDetailId", artifactsByDetailId: "artifactsByDetailId", SystemArtifactsByDetailId: "SystemArtifactsByDetailId", systemArtifactsByDetailId: "systemArtifactsByDetailId", AgentRunsByDetailId: "AgentRunsByDetailId", agentRunsByDetailId: "agentRunsByDetailId", EmptyStateMode: "EmptyStateMode", emptyStateMode: "emptyStateMode", AppContext: "AppContext", appContext: "appContext", DefaultAgentId: "DefaultAgentId", defaultAgentId: "defaultAgentId", ConversationDefaultAgentId: "ConversationDefaultAgentId", conversationDefaultAgentId: "conversationDefaultAgentId", AgentConfigurationPresetId: "AgentConfigurationPresetId", agentConfigurationPresetId: "agentConfigurationPresetId", InitialMessage: "InitialMessage", initialMessage: "initialMessage", InitialAttachments: "InitialAttachments", initialAttachments: "initialAttachments", ConversationHistory: "ConversationHistory", conversationHistory: "conversationHistory", InProgressMessageIds: "InProgressMessageIds", inProgressMessageIds: "inProgressMessageIds", ApplicationId: "ApplicationId", applicationId: "applicationId", AgentReplyMode: "AgentReplyMode", AllowedAgentIDs: "AllowedAgentIDs", MentionPeople: "MentionPeople", AgentHistoryFrom: "AgentHistoryFrom", AgentTurnHandler: "AgentTurnHandler", AutoNameConversation: "AutoNameConversation", EnableDecisionRouting: "EnableDecisionRouting", InitialDraft: "InitialDraft", initialDraft: "initialDraft" }, outputs: { MessageSent: "MessageSent", messageSent: "messageSent", MessageStreamed: "MessageStreamed", AgentResponse: "AgentResponse", agentResponse: "agentResponse", BeforeAgentTurn: "BeforeAgentTurn", beforeAgentTurn: "beforeAgentTurn", AfterAgentTurn: "AfterAgentTurn", afterAgentTurn: "afterAgentTurn", AgentRunDetected: "AgentRunDetected", agentRunDetected: "agentRunDetected", AgentRunUpdate: "AgentRunUpdate", agentRunUpdate: "agentRunUpdate", MessageComplete: "MessageComplete", messageComplete: "messageComplete", ArtifactCreated: "ArtifactCreated", artifactCreated: "artifactCreated", ConversationRenamed: "ConversationRenamed", conversationRenamed: "conversationRenamed", IntentCheckStarted: "IntentCheckStarted", intentCheckStarted: "intentCheckStarted", IntentCheckCompleted: "IntentCheckCompleted", intentCheckCompleted: "intentCheckCompleted", InitialMessageAutoSendStarted: "InitialMessageAutoSendStarted", initialMessageAutoSendStarted: "initialMessageAutoSendStarted", InitialMessageAutoSendFailed: "InitialMessageAutoSendFailed", initialMessageAutoSendFailed: "initialMessageAutoSendFailed", EmptyStateSubmit: "EmptyStateSubmit", emptyStateSubmit: "emptyStateSubmit", UploadStateChanged: "UploadStateChanged", uploadStateChanged: "uploadStateChanged", InitialDraftApplied: "InitialDraftApplied", initialDraftApplied: "initialDraftApplied", DraftStateChanged: "DraftStateChanged", ComposerBlurred: "ComposerBlurred" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature, i0.ɵɵNgOnChangesFeature], decls: 6, vars: 29, consts: [["pickerOrigin", "cdkOverlayOrigin"], ["inputBox", ""], ["cdkOverlayOrigin", "", 1, "message-input-wrapper"], [1, "processing-indicator"], [3, "valueChange", "blurred", "textSubmitted", "attachmentsChanged", "attachmentError", "voiceRequested", "voiceOptionsRequested", "planModeToggle", "placeholder", "disabled", "showCharacterCount", "enableMentions", "EnableAgentMentions", "EnableEntityMentions", "EnableSkillCommands", "TargetAgentId", "AllowedAgentIDs", "MentionPeople", "enableAttachments", "maxAttachments", "maxAttachmentSizeBytes", "acceptedFileTypes", "currentUser", "Provider", "rows", "enableRealtime", "voiceActive", "canStartRealtime", "enablePlanMode", "planModeActive", "value"], ["cdkConnectedOverlay", "", "cdkConnectedOverlayBackdropClass", "cdk-overlay-transparent-backdrop", 3, "backdropClick", "detach", "cdkConnectedOverlayOrigin", "cdkConnectedOverlayOpen", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPush", "cdkConnectedOverlayHasBackdrop"], [1, "fas", "fa-circle-notch", "fa-spin"], [3, "AgentPicked", "Cancelled", "Provider", "CurrentUser", "Agents", "DefaultAgentId", "CoAgents", "DefaultCoAgentId"]], template: function MessageInputComponent_Template(rf, ctx) { if (rf & 1) {
3211
3404
  i0.ɵɵelementStart(0, "div", 2, 0);
3212
3405
  i0.ɵɵconditionalCreate(2, MessageInputComponent_Conditional_2_Template, 4, 1, "div", 3);
3213
3406
  i0.ɵɵelementStart(3, "mj-ai-composer", 4, 1);
@@ -3245,6 +3438,8 @@ export class MessageInputComponent extends BaseAngularComponent {
3245
3438
  type: Input
3246
3439
  }], disabled: [{
3247
3440
  type: Input
3441
+ }], ReadOnly: [{
3442
+ type: Input
3248
3443
  }], Placeholder: [{
3249
3444
  type: Input
3250
3445
  }], placeholder: [{
@@ -3357,10 +3552,14 @@ export class MessageInputComponent extends BaseAngularComponent {
3357
3552
  type: Input
3358
3553
  }], AutoNameConversation: [{
3359
3554
  type: Input
3555
+ }], EnableDecisionRouting: [{
3556
+ type: Input
3360
3557
  }], MessageSent: [{
3361
3558
  type: Output
3362
3559
  }], messageSent: [{
3363
3560
  type: Output
3561
+ }], MessageStreamed: [{
3562
+ type: Output
3364
3563
  }], AgentResponse: [{
3365
3564
  type: Output
3366
3565
  }], agentResponse: [{
@@ -3433,5 +3632,5 @@ export class MessageInputComponent extends BaseAngularComponent {
3433
3632
  }], ComposerBlurred: [{
3434
3633
  type: Output
3435
3634
  }] }); })();
3436
- (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(MessageInputComponent, { className: "MessageInputComponent", filePath: "src/lib/components/message/message-input.component.ts", lineNumber: 48 }); })();
3635
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(MessageInputComponent, { className: "MessageInputComponent", filePath: "src/lib/components/message/message-input.component.ts", lineNumber: 70 }); })();
3437
3636
  //# sourceMappingURL=message-input.component.js.map