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