@cap-js/agents 0.9.1 → 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 (48) hide show
  1. package/README.md +2 -1
  2. package/cds-plugin.js +2 -2
  3. package/lib/agents/markdown/backends/outputs-backend.js +16 -12
  4. package/lib/agents/markdown/backends/readonly-backend.js +48 -0
  5. package/lib/agents/markdown/backends/uploads-backend.js +30 -13
  6. package/lib/agents/markdown/deep-agent.js +15 -26
  7. package/lib/agents/middleware/content-filter.js +3 -2
  8. package/lib/agents/middleware/index.js +11 -0
  9. package/lib/agents/middleware/remote-mcp.js +62 -0
  10. package/lib/agents/middleware/tool-wrap.js +52 -0
  11. package/lib/compile.js +6 -2
  12. package/lib/eval/Judge.js +239 -0
  13. package/lib/eval/eval-describe.js +69 -0
  14. package/lib/eval/eval-run.js +147 -0
  15. package/lib/eval/index.js +6 -0
  16. package/lib/eval/metrics.js +53 -0
  17. package/lib/eval/span-collector.js +50 -0
  18. package/lib/index.js +4 -3
  19. package/lib/models/aicore.js +23 -6
  20. package/lib/preview/chat.html +394 -69
  21. package/lib/protocol/agent-card.js +6 -2
  22. package/lib/protocol/persistence/checkpoint-saver.js +4 -1
  23. package/lib/protocol/persistence/cleanup.js +84 -0
  24. package/lib/sidecar.js +1 -1
  25. package/lib/telemetry/chat-tracing.js +61 -8
  26. package/lib/telemetry/mlflow/credentials.js +79 -0
  27. package/lib/telemetry/mlflow/evaluation.js +38 -0
  28. package/lib/telemetry/mlflow/exporter/DatabricksExporter.js +75 -0
  29. package/lib/telemetry/mlflow/exporter/MlflowExporter.js +115 -0
  30. package/lib/telemetry/mlflow/exporter/index.js +23 -0
  31. package/lib/telemetry/mlflow/index.js +16 -0
  32. package/lib/telemetry/mlflow/prompts.js +123 -0
  33. package/lib/telemetry/mlflow/tracing.js +264 -0
  34. package/lib/telemetry/tool-tracing.js +27 -54
  35. package/lib/telemetry/tracing.js +3 -1
  36. package/lib/utils/markdown.js +1 -11
  37. package/lib/utils/resilience.js +133 -0
  38. package/lib/utils/utils.js +17 -0
  39. package/package.json +24 -15
  40. package/{index.cds → srv/entities.cds} +11 -0
  41. package/srv/handlers/chat.js +278 -0
  42. package/srv/handlers/graph-executor.js +48 -37
  43. package/srv/handlers/index.js +8 -0
  44. package/srv/handlers/mcp-tools.js +11 -47
  45. package/srv/handlers/sub-agent-tools.js +12 -3
  46. package/srv/handlers/tools.js +9 -5
  47. package/index.js +0 -0
  48. package/lib/telemetry/mlflow.js +0 -290
@@ -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
 
@@ -429,7 +460,29 @@
429
460
  <script>
430
461
  const AGENT_URL = window.location.pathname.replace(/\/preview\/?$/, "") + "/"
431
462
  const AUTH_HEADER = document.querySelector('meta[name="a2a-auth"]')?.content || ""
432
- const authHeaders = () => (AUTH_HEADER ? { Authorization: AUTH_HEADER } : {})
463
+
464
+ const fetchCsrfToken = async () => {
465
+ if (document.csrfToken === undefined) {
466
+ const res = await fetch("/", {
467
+ method: "HEAD",
468
+ headers: { "x-csrf-token": "fetch", ...(await authHeaders({ includeCsrf: false })) },
469
+ })
470
+ if (!res.ok) {
471
+ throw new Error(`Could not fetch CSRF token ${res.status}: ${res.statusText}`)
472
+ }
473
+ document.csrfToken = res.headers.get("x-csrf-token")
474
+ }
475
+ return document.csrfToken
476
+ }
477
+
478
+ const authHeaders = async ({ includeCsrf = true } = {}) => {
479
+ const headers = AUTH_HEADER ? { Authorization: AUTH_HEADER } : {}
480
+ if (!includeCsrf) return headers
481
+ const csrfToken = await fetchCsrfToken()
482
+ if (csrfToken) headers["x-csrf-token"] = csrfToken
483
+ return headers
484
+ }
485
+
433
486
  document.getElementById("back").addEventListener("click", () => history.back())
434
487
  const messages = document.getElementById("messages")
435
488
  const typing = document.getElementById("typing")
@@ -437,16 +490,35 @@
437
490
  const prompt = document.getElementById("prompt")
438
491
  const sendBtn = document.getElementById("send")
439
492
  const dot = document.getElementById("dot")
440
- const streamToggle = document.getElementById("streamToggle")
493
+
494
+ let useStreaming = false
495
+ const THINKING_STEP_BUFFER_MS = 100
496
+ const THINKING_SUMMARY_ANIMATION_MS = 160
441
497
 
442
498
  let busy = false
443
499
  let contextId = null
444
500
  let pendingTaskId = null
445
501
  let activeTaskId = null
446
502
  let activeAbort = null
447
- // Live streaming bubble: created on first token, updated incrementally
448
503
  let streamingBubble = null
449
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(() => {})
450
522
 
451
523
  const HISTORY_KEY = "chatHistory_" + AGENT_URL
452
524
  let inputHistory = JSON.parse(localStorage.getItem(HISTORY_KEY) || "[]")
@@ -499,13 +571,243 @@
499
571
  function addMessage(role, text) {
500
572
  const el = document.createElement("div")
501
573
  el.className = "msg " + role
502
- if (role === "agent") el.innerHTML = md(text, { sanitize: true })
574
+ if (role === "agent" || role === "agent_thinking")
575
+ el.innerHTML = md(text, { sanitize: true })
503
576
  else el.textContent = text
504
- messages.insertBefore(el, typing)
577
+ if (role === "agent_thinking") el.className = "msg agent_thinking"
578
+ messages.insertBefore(el, typing.parentElement === messages ? typing : statusText)
505
579
  messages.scrollTop = messages.scrollHeight
506
580
  return el
507
581
  }
508
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
+
509
811
  function addApprovalPrompt(description) {
510
812
  const el = document.createElement("div")
511
813
  el.className = "msg approval"
@@ -532,7 +834,7 @@
532
834
  actions.appendChild(approveBtn)
533
835
  actions.appendChild(rejectBtn)
534
836
  el.appendChild(actions)
535
- messages.insertBefore(el, typing)
837
+ messages.insertBefore(el, typing.parentElement === messages ? typing : statusText)
536
838
  messages.scrollTop = messages.scrollHeight
537
839
  return el
538
840
  }
@@ -544,6 +846,7 @@
544
846
  function setBusy(val) {
545
847
  busy = val
546
848
  prompt.disabled = val
849
+ if (typing.parentElement !== messages) messages.insertBefore(typing, statusText)
547
850
  typing.style.display = val ? "block" : "none"
548
851
  dot.className = "status-dot" + (val ? " busy" : "")
549
852
  if (val) {
@@ -557,9 +860,6 @@
557
860
  activeTaskId = null
558
861
  activeAbort = null
559
862
  statusText.textContent = ""
560
- // Reset streaming state on any transition to idle
561
- streamingBubble = null
562
- streamingText = ""
563
863
  }
564
864
  messages.scrollTop = messages.scrollHeight
565
865
  }
@@ -573,17 +873,15 @@
573
873
 
574
874
  const abort = new AbortController()
575
875
  activeAbort = abort
576
- streamingBubble = null
577
- streamingText = ""
578
876
 
579
877
  try {
580
878
  const resp = await fetch(AGENT_URL, {
581
879
  method: "POST",
582
- headers: { "Content-Type": "application/json", ...authHeaders() },
880
+ headers: { "Content-Type": "application/json", ...(await authHeaders()) },
583
881
  signal: abort.signal,
584
882
  body: JSON.stringify({
585
883
  jsonrpc: "2.0",
586
- method: streamToggle.checked ? "message/stream" : "message/send",
884
+ method: useStreaming ? "message/stream" : "message/send",
587
885
  id: crypto.randomUUID(),
588
886
  params: {
589
887
  message: {
@@ -597,7 +895,7 @@
597
895
  }),
598
896
  })
599
897
 
600
- if (streamToggle.checked) {
898
+ if (useStreaming) {
601
899
  await handleSSEResponse(resp)
602
900
  } else {
603
901
  const envelope = await resp.json()
@@ -626,12 +924,15 @@
626
924
  pendingTaskId = taskId
627
925
  const msgParts = result?.status?.message?.parts ?? []
628
926
  const description = partsToText(msgParts)
927
+ flushPendingThinkingUpdate()
629
928
  addApprovalPrompt(description)
630
929
  return
631
930
  }
632
931
 
633
- // If streaming was active and already rendered the final text via lastChunk, skip.
634
- 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
635
936
  streamingText = ""
636
937
  return
637
938
  }
@@ -639,6 +940,7 @@
639
940
  // Clean up any open bubble (streaming was interrupted before lastChunk)
640
941
  if (streamingBubble) {
641
942
  if (streamingText) streamingBubble.innerHTML = md(streamingText)
943
+ finalizeThinking(streamingBubble, streamingText)
642
944
  streamingBubble = null
643
945
  streamingText = ""
644
946
  return
@@ -647,7 +949,9 @@
647
949
  const parts =
648
950
  result?.parts ?? result?.status?.message?.parts ?? result?.artifacts?.[0]?.parts ?? []
649
951
  const reply = partsToText(parts)
650
- addMessage("agent", reply || "(no response)")
952
+ const replyEl =
953
+ promoteLatestThinkingToResponse(reply) || addMessage("agent", reply || "(no response)")
954
+ finalizeThinking(replyEl, reply)
651
955
  }
652
956
 
653
957
  async function send() {
@@ -672,12 +976,20 @@
672
976
  activeAbort = abort
673
977
  streamingBubble = null
674
978
  streamingText = ""
675
-
676
- 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
677
989
  try {
678
990
  const resp = await fetch(AGENT_URL, {
679
991
  method: "POST",
680
- headers: { "Content-Type": "application/json", ...authHeaders() },
992
+ headers: { "Content-Type": "application/json", ...(await authHeaders()) },
681
993
  signal: abort.signal,
682
994
  body: JSON.stringify({
683
995
  jsonrpc: "2.0",
@@ -782,44 +1094,57 @@
782
1094
  return null
783
1095
  }
784
1096
 
785
- // Incremental artifact-update: render tokens as they arrive
786
- if (event.kind === "artifact-update" && streamToggle.checked) {
1097
+ if (event.kind === "artifact-update") {
787
1098
  const parts = event.artifact?.parts ?? []
788
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
+
789
1109
  // A2A spec: append/lastChunk are event-level fields (siblings of `artifact`).
790
1110
  const append = event.append ?? false
791
1111
  const lastChunk = event.lastChunk ?? false
792
1112
 
793
1113
  if (lastChunk) {
794
- // lastChunk carries the authoritative full text — replace accumulated content.
1114
+ // Terminal: current bubble = final answer. Replace with authoritative text.
795
1115
  if (text) {
796
1116
  if (streamingBubble) {
797
1117
  streamingBubble.innerHTML = md(text)
798
1118
  messages.scrollTop = messages.scrollHeight
799
1119
  } else {
800
1120
  typing.style.display = "none"
801
- addMessage("agent", text)
1121
+ streamingBubble = promoteLatestThinkingToResponse(text) || addMessage("agent", text)
802
1122
  }
803
1123
  }
1124
+ finalizeThinking(streamingBubble, text)
1125
+ thinkingFinalized = true
804
1126
  streamingBubble = null
805
- streamingText = text // keep so handleResult skips duplicate render
1127
+ streamingText = text // dedupe with handleResult
806
1128
  return null
807
1129
  }
808
1130
 
809
- if (text) {
810
- if (!append || !streamingBubble) {
811
- // First token — create a live bubble, hide typing indicator
812
- typing.style.display = "none"
813
- streamingBubble = addMessage("agent", text)
814
- streamingText = text
815
- } else {
816
- // Subsequent token — accumulate and re-render markdown
817
- streamingText += text
818
- streamingBubble.innerHTML = md(streamingText)
819
- messages.scrollTop = messages.scrollHeight
820
- }
821
- }
1131
+ if (!text) return null
822
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
823
1148
  return null
824
1149
  }
825
1150
 
@@ -856,7 +1181,7 @@
856
1181
  if (taskId) {
857
1182
  fetch(AGENT_URL, {
858
1183
  method: "POST",
859
- headers: { "Content-Type": "application/json", ...authHeaders() },
1184
+ headers: { "Content-Type": "application/json", ...(await authHeaders()) },
860
1185
  body: JSON.stringify({
861
1186
  jsonrpc: "2.0",
862
1187
  method: "tasks/cancel",
@@ -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)
@@ -151,6 +151,7 @@ export class CdsCheckpointSaver extends BaseCheckpointSaver {
151
151
  const thread_id = config.configurable?.thread_id
152
152
  const checkpoint_ns = config.configurable?.checkpoint_ns ?? ""
153
153
  const parent_checkpoint_id = config.configurable?.checkpoint_id
154
+ const task_id = config.configurable?._taskId || cds.context?.["agent.task.id"] || null
154
155
 
155
156
  if (!thread_id) {
156
157
  throw new Error('Missing required "thread_id" in config.configurable')
@@ -165,12 +166,14 @@ export class CdsCheckpointSaver extends BaseCheckpointSaver {
165
166
  checkpoint_ns,
166
167
  checkpoint_id: checkpoint.id,
167
168
  parent_checkpoint_id,
169
+ task_id,
168
170
  checkpoint: toUtf8String(serializedCheckpoint),
169
171
  metadata: toUtf8String(serializedMetadata),
170
172
  createdBy: resolveUserId(),
171
173
  })
172
174
 
173
- LOG._trace && LOG.debug("Checkpoint saved", { thread_id, checkpoint_id: checkpoint.id })
175
+ LOG._trace &&
176
+ LOG.debug("Checkpoint saved", { thread_id, checkpoint_id: checkpoint.id, task_id })
174
177
 
175
178
  return {
176
179
  configurable: {