@cap-js/agents 0.9.2 → 0.9.4

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 (49) hide show
  1. package/README.md +2 -0
  2. package/cds-plugin.js +2 -2
  3. package/lib/agents/middleware/content-filter.js +3 -2
  4. package/lib/agents/middleware/hitl-decision-note-injector.js +18 -0
  5. package/lib/agents/middleware/hitl.js +2 -2
  6. package/lib/agents/middleware/index.js +11 -0
  7. package/lib/agents/middleware/quota-enforcer.js +9 -0
  8. package/lib/agents/middleware/remote-mcp.js +62 -0
  9. package/lib/agents/middleware/tool-wrap.js +52 -0
  10. package/lib/compile.js +6 -2
  11. package/lib/eval/Judge.js +239 -0
  12. package/lib/eval/eval-describe.js +69 -0
  13. package/lib/eval/eval-run.js +147 -0
  14. package/lib/eval/index.js +6 -0
  15. package/lib/eval/metrics.js +53 -0
  16. package/lib/eval/span-collector.js +50 -0
  17. package/lib/index.js +2 -1
  18. package/lib/models/aicore.js +23 -6
  19. package/lib/models/anthropic.js +27 -12
  20. package/lib/preview/chat.html +373 -66
  21. package/lib/protocol/agent-card.js +6 -2
  22. package/lib/sidecar.js +1 -1
  23. package/lib/telemetry/chat-tracing.js +39 -8
  24. package/lib/telemetry/mlflow/credentials.js +79 -0
  25. package/lib/telemetry/mlflow/evaluation.js +38 -0
  26. package/lib/telemetry/mlflow/exporter/DatabricksExporter.js +75 -0
  27. package/lib/telemetry/mlflow/exporter/MlflowExporter.js +115 -0
  28. package/lib/telemetry/mlflow/exporter/index.js +23 -0
  29. package/lib/telemetry/mlflow/index.js +16 -0
  30. package/lib/telemetry/mlflow/prompts.js +123 -0
  31. package/lib/telemetry/mlflow/tracing.js +264 -0
  32. package/lib/telemetry/tool-tracing.js +27 -54
  33. package/lib/telemetry/tracing.js +3 -1
  34. package/lib/utils/markdown.js +1 -11
  35. package/lib/utils/message-handling.js +14 -0
  36. package/lib/utils/resilience.js +133 -0
  37. package/lib/utils/utils.js +17 -0
  38. package/package.json +26 -14
  39. package/srv/handlers/chat.js +278 -0
  40. package/srv/handlers/graph-executor/hitl.js +263 -0
  41. package/srv/handlers/graph-executor.js +100 -286
  42. package/srv/handlers/index.js +5 -1
  43. package/srv/handlers/mcp-tools.js +7 -45
  44. package/srv/handlers/sub-agent-tools.js +30 -12
  45. package/srv/handlers/tools.js +39 -5
  46. package/index.js +0 -0
  47. package/lib/agents/middleware/hitl-edit-note-injector.js +0 -18
  48. package/lib/telemetry/mlflow.js +0 -290
  49. /package/{index.cds → srv/entities.cds} +0 -0
@@ -41,26 +41,6 @@
41
41
  header .back:hover {
42
42
  color: #fff;
43
43
  }
44
- .header-toggle {
45
- display: flex;
46
- align-items: center;
47
- gap: 6px;
48
- font-size: 0.78rem;
49
- color: #c8def0;
50
- cursor: pointer;
51
- user-select: none;
52
- flex-shrink: 0;
53
- }
54
- .header-toggle[hidden] {
55
- display: none;
56
- }
57
- .header-toggle input[type="checkbox"] {
58
- accent-color: #7fbce8;
59
- width: 14px;
60
- height: 14px;
61
- cursor: pointer;
62
- }
63
-
64
44
  header .status-dot {
65
45
  width: 8px;
66
46
  height: 8px;
@@ -93,13 +73,21 @@
93
73
  gap: 14px;
94
74
  }
95
75
 
96
- .msg {
76
+ .msg,
77
+ .thinking-summary {
97
78
  max-width: 75%;
79
+ box-sizing: border-box;
80
+ }
81
+
82
+ .msg {
98
83
  padding: 10px 14px;
99
84
  border-radius: 12px;
100
85
  line-height: 1.5;
101
86
  font-size: 90%;
102
87
  word-break: break-word;
88
+ transition:
89
+ background 0.15s,
90
+ color 0.15s;
103
91
  }
104
92
  .msg.user {
105
93
  align-self: flex-end;
@@ -127,6 +115,7 @@
127
115
  color: #1d3e5a;
128
116
  border-bottom-left-radius: 3px;
129
117
  border-left: 3px solid #537492;
118
+ white-space: pre-wrap;
130
119
  }
131
120
  .msg.approval::before {
132
121
  content: "Action Required";
@@ -397,6 +386,58 @@
397
386
  background: #0f2d45;
398
387
  }
399
388
  }
389
+
390
+ /* Thinking messages are streamed into an expanded <details> panel,
391
+ then collapsed once the final response arrives. */
392
+ .thinking-summary {
393
+ align-self: flex-start;
394
+ margin: 4px 0 8px 0;
395
+ font-size: 0.88em;
396
+ color: #666;
397
+ border-left: 2px solid #ccc;
398
+ padding-left: 10px;
399
+ overflow-wrap: anywhere;
400
+ }
401
+ .thinking-summary summary {
402
+ cursor: pointer;
403
+ user-select: none;
404
+ color: #888;
405
+ list-style: none;
406
+ }
407
+ .thinking-summary summary::before {
408
+ content: "▶ ";
409
+ font-size: 0.8em;
410
+ }
411
+ .thinking-summary[open] summary::before {
412
+ content: "▼ ";
413
+ }
414
+ .thinking-step {
415
+ min-width: 0;
416
+ padding: 4px 0;
417
+ font-style: italic;
418
+ overflow-x: auto;
419
+ }
420
+ .thinking-step + .thinking-step {
421
+ border-top: 1px dashed #e0e0e0;
422
+ margin-top: 4px;
423
+ }
424
+ @media (prefers-reduced-motion: no-preference) {
425
+ .thinking-summary {
426
+ transition: height 0.16s ease;
427
+ }
428
+ }
429
+ @media (prefers-color-scheme: dark) {
430
+ .thinking-summary {
431
+ color: #999;
432
+ border-left-color: #444;
433
+ }
434
+ .thinking-summary summary {
435
+ color: #aaa;
436
+ }
437
+ .thinking-step + .thinking-step {
438
+ border-top-color: #333;
439
+ }
440
+ }
400
441
  </style>
401
442
  <script src="marked.min.js"></script>
402
443
  </head>
@@ -404,15 +445,6 @@
404
445
  <header>
405
446
  <a class="back" id="back" title="Back to index">&#8592;</a>
406
447
  <h1>{{agentName}}</h1>
407
- <label
408
- class="header-toggle"
409
- title="Stream tokens as they arrive"
410
- style="margin-left: auto"
411
- hidden
412
- >
413
- <input type="checkbox" id="streamToggle" hidden />
414
- Streaming
415
- </label>
416
448
  <div class="status-dot" id="dot"></div>
417
449
  </header>
418
450
 
@@ -459,16 +491,35 @@
459
491
  const prompt = document.getElementById("prompt")
460
492
  const sendBtn = document.getElementById("send")
461
493
  const dot = document.getElementById("dot")
462
- const streamToggle = document.getElementById("streamToggle")
494
+
495
+ let useStreaming = false
496
+ const THINKING_STEP_BUFFER_MS = 100
497
+ const THINKING_SUMMARY_ANIMATION_MS = 160
463
498
 
464
499
  let busy = false
465
500
  let contextId = null
466
501
  let pendingTaskId = null
467
502
  let activeTaskId = null
468
503
  let activeAbort = null
469
- // Live streaming bubble: created on first token, updated incrementally
470
504
  let streamingBubble = null
471
505
  let streamingText = ""
506
+ let thinkingFinalized = false
507
+ let thinkingStepEls = [] // thinking steps inside the summary panel
508
+ let thinkingTexts = [] // accumulated text per thinking step
509
+ let thinkingStepCount = 0
510
+ let thinkingSummaryEl = null // <details> containing thinking steps
511
+ let currentThinkingArtifactId = null // artifactId of open thinking step
512
+ let currentThinkingClosed = false // true once lastChunk seen for current id
513
+ let pendingThinkingUpdate = null
514
+ let pendingThinkingTimer = null
515
+
516
+ // Auto-enable streaming when agent card advertises streaming capability
517
+ fetch(AGENT_URL + ".well-known/agent-card.json", { headers: authHeaders() })
518
+ .then((r) => (r.ok ? r.json() : null))
519
+ .then((card) => {
520
+ if (card?.capabilities?.streaming) useStreaming = true
521
+ })
522
+ .catch(() => {})
472
523
 
473
524
  const HISTORY_KEY = "chatHistory_" + AGENT_URL
474
525
  let inputHistory = JSON.parse(localStorage.getItem(HISTORY_KEY) || "[]")
@@ -521,17 +572,249 @@
521
572
  function addMessage(role, text) {
522
573
  const el = document.createElement("div")
523
574
  el.className = "msg " + role
524
- if (role === "agent") el.innerHTML = md(text, { sanitize: true })
575
+ if (role === "agent" || role === "agent_thinking")
576
+ el.innerHTML = md(text, { sanitize: true })
525
577
  else el.textContent = text
526
- messages.insertBefore(el, typing)
578
+ if (role === "agent_thinking") el.className = "msg agent_thinking"
579
+ messages.insertBefore(el, typing.parentElement === messages ? typing : statusText)
527
580
  messages.scrollTop = messages.scrollHeight
528
581
  return el
529
582
  }
530
583
 
584
+ function updateThinkingSummaryLabel() {
585
+ if (!thinkingSummaryEl) return
586
+ const summary = thinkingSummaryEl.querySelector("summary")
587
+ if (!summary) return
588
+ const count = thinkingStepCount
589
+ summary.textContent = `Thinking (${count} step${count === 1 ? "" : "s"})`
590
+ }
591
+
592
+ function ensureThinkingSummary() {
593
+ if (!thinkingSummaryEl) {
594
+ thinkingSummaryEl = document.createElement("details")
595
+ thinkingSummaryEl.className = "thinking-summary"
596
+ thinkingSummaryEl.open = true
597
+ const summary = document.createElement("summary")
598
+ summary.addEventListener("click", toggleThinkingSummary)
599
+ thinkingSummaryEl.appendChild(summary)
600
+ updateThinkingSummaryLabel()
601
+ messages.insertBefore(
602
+ thinkingSummaryEl,
603
+ typing.parentElement === messages ? typing : statusText,
604
+ )
605
+ }
606
+ cancelThinkingSummaryAnimation(thinkingSummaryEl)
607
+ thinkingSummaryEl.open = true
608
+ return thinkingSummaryEl
609
+ }
610
+
611
+ function clearPendingThinkingUpdate() {
612
+ if (pendingThinkingTimer) clearTimeout(pendingThinkingTimer)
613
+ pendingThinkingTimer = null
614
+ pendingThinkingUpdate = null
615
+ }
616
+
617
+ function flushPendingThinkingUpdate() {
618
+ const pending = pendingThinkingUpdate
619
+ clearPendingThinkingUpdate()
620
+ if (pending) renderThinkingStep(pending)
621
+ }
622
+
623
+ function prefersReducedMotion() {
624
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches
625
+ }
626
+
627
+ function resetThinkingSummaryAnimation(el) {
628
+ el.style.height = ""
629
+ el.style.overflow = ""
630
+ }
631
+
632
+ function cancelThinkingSummaryAnimation(el) {
633
+ if (!el) return
634
+ el._thinkingAnimationId = (el._thinkingAnimationId ?? 0) + 1
635
+ resetThinkingSummaryAnimation(el)
636
+ }
637
+
638
+ function toggleThinkingSummary(event) {
639
+ if (prefersReducedMotion()) return
640
+ event.preventDefault()
641
+ const el = event.currentTarget.parentElement
642
+ if (el.open) collapseThinkingSummary(el)
643
+ else expandThinkingSummary(el)
644
+ }
645
+
646
+ function renderThinkingStep(update) {
647
+ const { artifactId, text, lastChunk, append } = update
648
+ const panel = ensureThinkingSummary()
649
+ const idx = thinkingStepEls.findIndex((step) => step._artifactId === artifactId)
650
+ if (append && idx >= 0) {
651
+ thinkingTexts[idx] += text
652
+ thinkingStepEls[idx].innerHTML = md(thinkingTexts[idx])
653
+ } else if (artifactId === currentThinkingArtifactId && !currentThinkingClosed) {
654
+ thinkingTexts[idx] += "\n" + text
655
+ thinkingStepEls[idx].innerHTML = md(thinkingTexts[idx])
656
+ } else {
657
+ const step = document.createElement("div")
658
+ step.className = "thinking-step"
659
+ step._artifactId = artifactId
660
+ step.innerHTML = md(text)
661
+ panel.appendChild(step)
662
+ thinkingStepEls.push(step)
663
+ thinkingTexts.push(text)
664
+ thinkingStepCount = thinkingStepEls.length
665
+ updateThinkingSummaryLabel()
666
+ currentThinkingArtifactId = artifactId
667
+ currentThinkingClosed = false
668
+ }
669
+ if (lastChunk) currentThinkingClosed = true
670
+ messages.scrollTop = messages.scrollHeight
671
+ }
672
+
673
+ function addThinkingStep(artifactId, text, lastChunk, append) {
674
+ if (
675
+ pendingThinkingUpdate &&
676
+ pendingThinkingUpdate.artifactId === artifactId &&
677
+ !pendingThinkingUpdate.lastChunk
678
+ ) {
679
+ pendingThinkingUpdate.text += text
680
+ pendingThinkingUpdate.lastChunk = lastChunk
681
+ } else {
682
+ flushPendingThinkingUpdate()
683
+ pendingThinkingUpdate = { artifactId, text, lastChunk, append }
684
+ }
685
+
686
+ if (pendingThinkingTimer) clearTimeout(pendingThinkingTimer)
687
+ pendingThinkingTimer = setTimeout(flushPendingThinkingUpdate, THINKING_STEP_BUFFER_MS)
688
+ }
689
+
690
+ function latestThinkingMatches(responseText) {
691
+ return (
692
+ responseText &&
693
+ thinkingTexts.length > 0 &&
694
+ thinkingTexts[thinkingTexts.length - 1].trim() === responseText.trim()
695
+ )
696
+ }
697
+
698
+ function promoteLatestThinkingToResponse(responseText) {
699
+ if (!thinkingSummaryEl || !latestThinkingMatches(responseText)) return null
700
+
701
+ thinkingTexts.pop()
702
+ const responseEl = thinkingStepEls.pop()
703
+ thinkingStepCount = thinkingStepEls.length
704
+
705
+ responseEl.className = "msg agent"
706
+ responseEl._promotedFromThinking = true
707
+ responseEl.innerHTML = md(responseText)
708
+ messages.insertBefore(responseEl, typing.parentElement === messages ? typing : statusText)
709
+
710
+ if (thinkingStepEls.length === 0) {
711
+ thinkingSummaryEl.remove()
712
+ thinkingSummaryEl = null
713
+ } else {
714
+ updateThinkingSummaryLabel()
715
+ collapseThinkingSummary(thinkingSummaryEl)
716
+ }
717
+
718
+ messages.scrollTop = messages.scrollHeight
719
+ return responseEl
720
+ }
721
+
722
+ function finalizeThinking(beforeEl, responseText) {
723
+ clearPendingThinkingUpdate()
724
+ if (beforeEl?._promotedFromThinking) return
725
+ if (thinkingStepEls.length === 0 || !thinkingSummaryEl) return
726
+ // Stream labels every model turn as thinking until terminal response is
727
+ // known. Its final turn is authoritative response, never a thinking step.
728
+ thinkingTexts.pop()
729
+ const finalResponse = thinkingStepEls.pop()
730
+ finalResponse?.remove()
731
+ thinkingStepCount = thinkingStepEls.length
732
+ updateThinkingSummaryLabel()
733
+
734
+ if (thinkingStepEls.length === 0) {
735
+ thinkingSummaryEl.remove()
736
+ thinkingSummaryEl = null
737
+ return
738
+ }
739
+
740
+ if (beforeEl && thinkingSummaryEl.nextSibling !== beforeEl) {
741
+ messages.insertBefore(thinkingSummaryEl, beforeEl)
742
+ }
743
+ collapseThinkingSummary(thinkingSummaryEl)
744
+ }
745
+
746
+ function collapseThinkingSummary(el) {
747
+ if (!el || !el.open) return
748
+
749
+ if (prefersReducedMotion()) {
750
+ el.open = false
751
+ return
752
+ }
753
+
754
+ const summary = el.querySelector("summary")
755
+ const startHeight = `${el.offsetHeight}px`
756
+ const endHeight = `${summary?.offsetHeight ?? 0}px`
757
+ const animationId = (el._thinkingAnimationId ?? 0) + 1
758
+ el._thinkingAnimationId = animationId
759
+ let finished = false
760
+
761
+ const finish = () => {
762
+ el.removeEventListener("transitionend", finish)
763
+ if (el._thinkingAnimationId !== animationId) return
764
+ if (finished) return
765
+ finished = true
766
+ el.open = false
767
+ resetThinkingSummaryAnimation(el)
768
+ }
769
+
770
+ resetThinkingSummaryAnimation(el)
771
+ el.style.overflow = "hidden"
772
+ el.style.height = startHeight
773
+ requestAnimationFrame(() => {
774
+ el.style.height = endHeight
775
+ })
776
+ el.addEventListener("transitionend", finish)
777
+ setTimeout(finish, THINKING_SUMMARY_ANIMATION_MS + 80)
778
+ }
779
+
780
+ function expandThinkingSummary(el) {
781
+ if (!el || el.open) return
782
+
783
+ if (prefersReducedMotion()) {
784
+ el.open = true
785
+ return
786
+ }
787
+
788
+ el.open = true
789
+ const summary = el.querySelector("summary")
790
+ const startHeight = `${summary?.offsetHeight ?? 0}px`
791
+ const endHeight = `${el.scrollHeight}px`
792
+ const animationId = (el._thinkingAnimationId ?? 0) + 1
793
+ el._thinkingAnimationId = animationId
794
+ let finished = false
795
+
796
+ const finish = () => {
797
+ el.removeEventListener("transitionend", finish)
798
+ if (el._thinkingAnimationId !== animationId) return
799
+ if (finished) return
800
+ finished = true
801
+ resetThinkingSummaryAnimation(el)
802
+ }
803
+
804
+ resetThinkingSummaryAnimation(el)
805
+ el.style.overflow = "hidden"
806
+ el.style.height = startHeight
807
+ requestAnimationFrame(() => {
808
+ el.style.height = endHeight
809
+ })
810
+ el.addEventListener("transitionend", finish)
811
+ setTimeout(finish, THINKING_SUMMARY_ANIMATION_MS + 80)
812
+ }
813
+
531
814
  function addApprovalPrompt(description) {
532
815
  const el = document.createElement("div")
533
816
  el.className = "msg approval"
534
- el.innerHTML = md(description || "The agent requires your approval to continue.")
817
+ el.textContent = description || "The agent requires your approval to continue."
535
818
  const actions = document.createElement("div")
536
819
  actions.className = "approval-actions"
537
820
 
@@ -554,7 +837,7 @@
554
837
  actions.appendChild(approveBtn)
555
838
  actions.appendChild(rejectBtn)
556
839
  el.appendChild(actions)
557
- messages.insertBefore(el, typing)
840
+ messages.insertBefore(el, typing.parentElement === messages ? typing : statusText)
558
841
  messages.scrollTop = messages.scrollHeight
559
842
  return el
560
843
  }
@@ -566,6 +849,7 @@
566
849
  function setBusy(val) {
567
850
  busy = val
568
851
  prompt.disabled = val
852
+ if (typing.parentElement !== messages) messages.insertBefore(typing, statusText)
569
853
  typing.style.display = val ? "block" : "none"
570
854
  dot.className = "status-dot" + (val ? " busy" : "")
571
855
  if (val) {
@@ -579,9 +863,6 @@
579
863
  activeTaskId = null
580
864
  activeAbort = null
581
865
  statusText.textContent = ""
582
- // Reset streaming state on any transition to idle
583
- streamingBubble = null
584
- streamingText = ""
585
866
  }
586
867
  messages.scrollTop = messages.scrollHeight
587
868
  }
@@ -595,8 +876,6 @@
595
876
 
596
877
  const abort = new AbortController()
597
878
  activeAbort = abort
598
- streamingBubble = null
599
- streamingText = ""
600
879
 
601
880
  try {
602
881
  const resp = await fetch(AGENT_URL, {
@@ -605,7 +884,7 @@
605
884
  signal: abort.signal,
606
885
  body: JSON.stringify({
607
886
  jsonrpc: "2.0",
608
- method: streamToggle.checked ? "message/stream" : "message/send",
887
+ method: useStreaming ? "message/stream" : "message/send",
609
888
  id: crypto.randomUUID(),
610
889
  params: {
611
890
  message: {
@@ -619,7 +898,7 @@
619
898
  }),
620
899
  })
621
900
 
622
- if (streamToggle.checked) {
901
+ if (useStreaming) {
623
902
  await handleSSEResponse(resp)
624
903
  } else {
625
904
  const envelope = await resp.json()
@@ -648,12 +927,15 @@
648
927
  pendingTaskId = taskId
649
928
  const msgParts = result?.status?.message?.parts ?? []
650
929
  const description = partsToText(msgParts)
930
+ flushPendingThinkingUpdate()
651
931
  addApprovalPrompt(description)
652
932
  return
653
933
  }
654
934
 
655
- // If streaming was active and already rendered the final text via lastChunk, skip.
656
- if (streamToggle.checked && streamingText !== "") {
935
+ // If streaming already rendered the final text via lastChunk, skip.
936
+ if (useStreaming && streamingText !== "") {
937
+ if (!thinkingFinalized) finalizeThinking(streamingBubble, streamingText)
938
+ streamingBubble = null
657
939
  streamingText = ""
658
940
  return
659
941
  }
@@ -661,6 +943,7 @@
661
943
  // Clean up any open bubble (streaming was interrupted before lastChunk)
662
944
  if (streamingBubble) {
663
945
  if (streamingText) streamingBubble.innerHTML = md(streamingText)
946
+ finalizeThinking(streamingBubble, streamingText)
664
947
  streamingBubble = null
665
948
  streamingText = ""
666
949
  return
@@ -669,7 +952,9 @@
669
952
  const parts =
670
953
  result?.parts ?? result?.status?.message?.parts ?? result?.artifacts?.[0]?.parts ?? []
671
954
  const reply = partsToText(parts)
672
- addMessage("agent", reply || "(no response)")
955
+ const replyEl =
956
+ promoteLatestThinkingToResponse(reply) || addMessage("agent", reply || "(no response)")
957
+ finalizeThinking(replyEl, reply)
673
958
  }
674
959
 
675
960
  async function send() {
@@ -694,8 +979,16 @@
694
979
  activeAbort = abort
695
980
  streamingBubble = null
696
981
  streamingText = ""
697
-
698
- const useStream = streamToggle.checked
982
+ thinkingFinalized = false
983
+ clearPendingThinkingUpdate()
984
+ thinkingStepEls = []
985
+ thinkingTexts = []
986
+ thinkingStepCount = 0
987
+ thinkingSummaryEl = null
988
+ currentThinkingArtifactId = null
989
+ currentThinkingClosed = false
990
+
991
+ const useStream = useStreaming
699
992
  try {
700
993
  const resp = await fetch(AGENT_URL, {
701
994
  method: "POST",
@@ -804,44 +1097,58 @@
804
1097
  return null
805
1098
  }
806
1099
 
807
- // Incremental artifact-update: render tokens as they arrive
808
- if (event.kind === "artifact-update" && streamToggle.checked) {
1100
+ if (event.kind === "artifact-update") {
809
1101
  const parts = event.artifact?.parts ?? []
810
1102
  const text = partsToText(parts)
1103
+
1104
+ if (event.artifact?.artifactId?.startsWith("thinking")) {
1105
+ const lastChunk = event.lastChunk ?? false
1106
+ if (text)
1107
+ addThinkingStep(event.artifact.artifactId, text, lastChunk, event.append ?? false)
1108
+ return null
1109
+ }
1110
+
1111
+ if (event.artifact?.artifactId !== "response") return null
1112
+
811
1113
  // A2A spec: append/lastChunk are event-level fields (siblings of `artifact`).
812
1114
  const append = event.append ?? false
813
1115
  const lastChunk = event.lastChunk ?? false
814
1116
 
815
1117
  if (lastChunk) {
816
- // lastChunk carries the authoritative full text — replace accumulated content.
1118
+ // Terminal: current bubble = final answer. Replace with authoritative text.
817
1119
  if (text) {
818
1120
  if (streamingBubble) {
819
1121
  streamingBubble.innerHTML = md(text)
820
1122
  messages.scrollTop = messages.scrollHeight
821
1123
  } else {
822
1124
  typing.style.display = "none"
823
- addMessage("agent", text)
1125
+ streamingBubble = promoteLatestThinkingToResponse(text) || addMessage("agent", text)
824
1126
  }
825
1127
  }
1128
+ finalizeThinking(streamingBubble, text)
1129
+ thinkingFinalized = true
826
1130
  streamingBubble = null
827
- streamingText = text // keep so handleResult skips duplicate render
1131
+ streamingText = text // dedupe with handleResult
828
1132
  return null
829
1133
  }
830
1134
 
831
- if (text) {
832
- if (!append || !streamingBubble) {
833
- // First token — create a live bubble, hide typing indicator
834
- typing.style.display = "none"
835
- streamingBubble = addMessage("agent", text)
836
- streamingText = text
837
- } else {
838
- // Subsequent token — accumulate and re-render markdown
839
- streamingText += text
840
- streamingBubble.innerHTML = md(streamingText)
841
- messages.scrollTop = messages.scrollHeight
842
- }
843
- }
1135
+ if (!text) return null
844
1136
 
1137
+ if (!append) {
1138
+ typing.style.display = "none"
1139
+ streamingBubble = addMessage("agent", text)
1140
+ streamingText = text
1141
+ } else if (streamingBubble) {
1142
+ // Subsequent token — accumulate into the current turn's bubble.
1143
+ streamingText += text
1144
+ streamingBubble.innerHTML = md(streamingText)
1145
+ } else {
1146
+ // Defensive: append:true with no bubble yet (unexpected). Open one.
1147
+ typing.style.display = "none"
1148
+ streamingBubble = addMessage("agent", text)
1149
+ streamingText = text
1150
+ }
1151
+ messages.scrollTop = messages.scrollHeight
845
1152
  return null
846
1153
  }
847
1154
 
@@ -1,13 +1,17 @@
1
1
  import cds from "@sap/cds"
2
2
  import { createRequire } from "node:module"
3
3
  const { path } = cds.utils
4
- import { getDescription, getFilteredEntities, getFilteredActions } from "../utils/utils.js"
4
+ import {
5
+ getDescription,
6
+ getFilteredEntities,
7
+ getFilteredActions,
8
+ slugified,
9
+ } from "../utils/utils.js"
5
10
  import {
6
11
  scanSkills,
7
12
  parseAgentMetadata,
8
13
  parseAgentCardMd,
9
14
  parseAgentCardFile,
10
- slugified,
11
15
  } from "../utils/markdown.js"
12
16
 
13
17
  const require = createRequire(import.meta.url)
package/lib/sidecar.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import cds from "@sap/cds"
2
2
  import A2AProtocolAdapter, { authContext } from "./index.js"
3
- import { slugified } from "./utils/markdown.js"
3
+ import { slugified } from "./utils/utils.js"
4
4
  import registerDefaultAgentHandlers from "../srv/handlers/index.js"
5
5
 
6
6
  const LOG = cds.log("a2a")