@cap-js/agents 0.0.0 → 0.9.1

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 (56) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +155 -0
  3. package/_i18n/messages.properties +31 -0
  4. package/cds-plugin.js +140 -0
  5. package/index.cds +105 -0
  6. package/index.js +0 -0
  7. package/lib/agents/markdown/backends/mime-utils.js +37 -0
  8. package/lib/agents/markdown/backends/outputs-backend.js +152 -0
  9. package/lib/agents/markdown/backends/uploads-backend.js +143 -0
  10. package/lib/agents/markdown/deep-agent.js +93 -0
  11. package/lib/agents/middleware/agent-actions.js +18 -0
  12. package/lib/agents/middleware/content-filter.js +191 -0
  13. package/lib/agents/middleware/hitl-edit-note-injector.js +18 -0
  14. package/lib/agents/middleware/hitl.js +20 -0
  15. package/lib/agents/middleware/index.js +19 -0
  16. package/lib/agents/middleware/patch-tool-calls.js +51 -0
  17. package/lib/agents/middleware/quota-enforcer.js +94 -0
  18. package/lib/agents/middleware/status-update.js +153 -0
  19. package/lib/agents/middleware/tool-selection.js +22 -0
  20. package/lib/agents/quota-enforcer-at-start.js +198 -0
  21. package/lib/agents/summarize-on-timeout.js +91 -0
  22. package/lib/compile.js +55 -0
  23. package/lib/index.cjs +1 -0
  24. package/lib/index.js +422 -0
  25. package/lib/models/aicore.js +441 -0
  26. package/lib/models/anthropic.js +77 -0
  27. package/lib/models/mock.js +88 -0
  28. package/lib/preview/chat.html +875 -0
  29. package/lib/preview/preview.js +46 -0
  30. package/lib/protocol/agent-card.js +297 -0
  31. package/lib/protocol/persistence/checkpoint-saver.js +317 -0
  32. package/lib/protocol/persistence/file-store.js +209 -0
  33. package/lib/protocol/persistence/push-notification-store.js +59 -0
  34. package/lib/protocol/persistence/task-store.js +47 -0
  35. package/lib/protocol/push-notification-sender.js +57 -0
  36. package/lib/sidecar.js +162 -0
  37. package/lib/telemetry/active-users.js +106 -0
  38. package/lib/telemetry/chat-tracing.js +342 -0
  39. package/lib/telemetry/metrics.js +85 -0
  40. package/lib/telemetry/mlflow.js +290 -0
  41. package/lib/telemetry/tool-tracing.js +164 -0
  42. package/lib/telemetry/tracing.js +150 -0
  43. package/lib/utils/inner-auth.js +33 -0
  44. package/lib/utils/markdown.js +199 -0
  45. package/lib/utils/message-handling.js +155 -0
  46. package/lib/utils/utils.js +168 -0
  47. package/package.json +225 -2
  48. package/srv/graph-cache.js +82 -0
  49. package/srv/handlers/graph-executor.js +1369 -0
  50. package/srv/handlers/index.js +173 -0
  51. package/srv/handlers/mcp-tools.js +159 -0
  52. package/srv/handlers/sub-agent-tools.js +314 -0
  53. package/srv/handlers/system-prompt.js +25 -0
  54. package/srv/handlers/tools.js +366 -0
  55. package/srv/langgraph-executor-srv.js +70 -0
  56. package/srv/push-notification-srv.js +149 -0
@@ -0,0 +1,875 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <title>{{agentName}}</title>
5
+ <meta charset="utf-8" />
6
+ <meta name="color-scheme" content="dark light" />
7
+ <meta name="a2a-auth" content="{{authHeader}}" />
8
+ <style>
9
+ body {
10
+ font-family:
11
+ Avenir Next,
12
+ sans-serif;
13
+ margin: 0;
14
+ background: #f8fbff;
15
+ color: #2a3d52;
16
+ display: flex;
17
+ flex-direction: column;
18
+ height: 100dvh;
19
+ }
20
+
21
+ header {
22
+ background: #537492;
23
+ color: #fff;
24
+ padding: 12px 20px;
25
+ display: flex;
26
+ align-items: center;
27
+ gap: 12px;
28
+ flex-shrink: 0;
29
+ }
30
+ header h1 {
31
+ font-size: 1rem;
32
+ font-weight: 500;
33
+ margin: 0;
34
+ }
35
+ header .back {
36
+ color: #c8def0;
37
+ text-decoration: none;
38
+ font-size: 0.85rem;
39
+ cursor: pointer;
40
+ }
41
+ header .back:hover {
42
+ color: #fff;
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
+ header .status-dot {
65
+ width: 8px;
66
+ height: 8px;
67
+ border-radius: 50%;
68
+ background: #4caf50;
69
+ flex-shrink: 0;
70
+ transition: background 0.3s;
71
+ }
72
+ header .status-dot.busy {
73
+ background: #f9a825;
74
+ }
75
+ header .status-dot.error {
76
+ background: #e53935;
77
+ }
78
+
79
+ table thead {
80
+ text-align: left;
81
+ }
82
+ table {
83
+ border-spacing: 1em 0.2em;
84
+ margin: 1em 0;
85
+ }
86
+
87
+ #messages {
88
+ flex: 1;
89
+ overflow-y: auto;
90
+ padding: 20px;
91
+ display: flex;
92
+ flex-direction: column;
93
+ gap: 14px;
94
+ }
95
+
96
+ .msg {
97
+ max-width: 75%;
98
+ padding: 10px 14px;
99
+ border-radius: 12px;
100
+ line-height: 1.5;
101
+ font-size: 90%;
102
+ word-break: break-word;
103
+ }
104
+ .msg.user {
105
+ align-self: flex-end;
106
+ background: #537492;
107
+ color: #fff;
108
+ border-bottom-right-radius: 3px;
109
+ white-space: pre-wrap;
110
+ }
111
+ .msg.agent {
112
+ align-self: flex-start;
113
+ background: #d0e9ff;
114
+ color: #1d3e5a;
115
+ border-bottom-left-radius: 3px;
116
+ }
117
+ .msg.error {
118
+ align-self: flex-start;
119
+ background: #fde8e8;
120
+ color: #b71c1c;
121
+ border-bottom-left-radius: 3px;
122
+ white-space: pre-wrap;
123
+ }
124
+ .msg.approval {
125
+ align-self: flex-start;
126
+ background: #d0e9ff;
127
+ color: #1d3e5a;
128
+ border-bottom-left-radius: 3px;
129
+ border-left: 3px solid #537492;
130
+ }
131
+ .msg.approval::before {
132
+ content: "Action Required";
133
+ display: block;
134
+ font-size: 0.75rem;
135
+ font-weight: 600;
136
+ color: #537492;
137
+ margin-bottom: 6px;
138
+ letter-spacing: 0.03em;
139
+ text-transform: uppercase;
140
+ }
141
+
142
+ .msg.agent p {
143
+ margin: 0 0 6px;
144
+ }
145
+ .msg.agent p:last-child {
146
+ margin-bottom: 0;
147
+ }
148
+ .msg.agent h1,
149
+ .msg.agent h2,
150
+ .msg.agent h3 {
151
+ font-weight: 600;
152
+ margin: 8px 0 4px;
153
+ }
154
+ .msg.agent h1 {
155
+ font-size: 1.1em;
156
+ }
157
+ .msg.agent h2 {
158
+ font-size: 1em;
159
+ }
160
+ .msg.agent h3 {
161
+ font-size: 0.95em;
162
+ }
163
+ .msg.agent ul,
164
+ .msg.agent ol {
165
+ margin: 4px 0 6px 18px;
166
+ padding: 0;
167
+ }
168
+ .msg.agent li {
169
+ margin-bottom: 2px;
170
+ }
171
+ .msg.agent pre {
172
+ background: rgba(0, 0, 0, 0.08);
173
+ border-radius: 6px;
174
+ padding: 8px 10px;
175
+ overflow-x: auto;
176
+ margin: 6px 0;
177
+ white-space: pre;
178
+ }
179
+ .msg.agent code {
180
+ font-family: ui-monospace, "Cascadia Code", monospace;
181
+ font-size: 0.85em;
182
+ background: rgba(0, 0, 0, 0.08);
183
+ padding: 1px 4px;
184
+ border-radius: 3px;
185
+ }
186
+ .msg.agent pre code {
187
+ background: none;
188
+ padding: 0;
189
+ font-size: 0.82em;
190
+ }
191
+ .msg.agent a {
192
+ color: inherit;
193
+ text-decoration: underline;
194
+ }
195
+ .msg.agent hr {
196
+ border: none;
197
+ border-top: 1px solid rgba(0, 0, 0, 0.15);
198
+ margin: 8px 0;
199
+ }
200
+
201
+ .typing {
202
+ align-self: flex-start;
203
+ padding: 10px 16px;
204
+ background: #d0e9ff;
205
+ border-radius: 12px;
206
+ border-bottom-left-radius: 3px;
207
+ display: none;
208
+ }
209
+ .typing span {
210
+ display: inline-block;
211
+ width: 7px;
212
+ height: 7px;
213
+ background: #537492;
214
+ border-radius: 50%;
215
+ animation: bounce 1.2s infinite;
216
+ }
217
+ .typing span:nth-child(2) {
218
+ animation-delay: 0.2s;
219
+ }
220
+ .typing span:nth-child(3) {
221
+ animation-delay: 0.4s;
222
+ }
223
+ @keyframes bounce {
224
+ 0%,
225
+ 80%,
226
+ 100% {
227
+ transform: translateY(0);
228
+ }
229
+ 40% {
230
+ transform: translateY(-6px);
231
+ }
232
+ }
233
+
234
+ .status-text {
235
+ font-size: 0.78rem;
236
+ color: #6a8fa8;
237
+ padding: 2px 12px;
238
+ font-style: italic;
239
+ }
240
+
241
+ #input-row {
242
+ display: flex;
243
+ gap: 10px;
244
+ padding: 14px 20px;
245
+ border-top: 1px solid #c8d9e8;
246
+ background: #f8fbff;
247
+ flex-shrink: 0;
248
+ }
249
+
250
+ #prompt {
251
+ flex: 1;
252
+ padding: 10px 14px;
253
+ border: 1px solid #91adc5;
254
+ border-radius: 8px;
255
+ font-family: inherit;
256
+ font-size: 0.9rem;
257
+ resize: none;
258
+ outline: none;
259
+ background: #fff;
260
+ color: inherit;
261
+ line-height: 1.4;
262
+ max-height: 140px;
263
+ overflow-y: auto;
264
+ }
265
+ #prompt:focus {
266
+ border-color: #537492;
267
+ }
268
+
269
+ #send {
270
+ align-self: flex-end;
271
+ padding: 10px 20px;
272
+ background: #537492;
273
+ color: #fff;
274
+ border: none;
275
+ border-radius: 8px;
276
+ cursor: pointer;
277
+ font-family: inherit;
278
+ font-size: 0.9rem;
279
+ transition: background 0.15s;
280
+ }
281
+ #send:hover:not(:disabled) {
282
+ background: #3e5a73;
283
+ }
284
+ #send:disabled {
285
+ background: #91adc5;
286
+ cursor: not-allowed;
287
+ }
288
+ #send.cancel {
289
+ background: #e53935;
290
+ }
291
+ #send.cancel:hover {
292
+ background: #c62828;
293
+ }
294
+
295
+ .approval-actions {
296
+ display: flex;
297
+ gap: 8px;
298
+ margin-top: 10px;
299
+ }
300
+ .approval-actions button {
301
+ padding: 7px 18px;
302
+ border: none;
303
+ border-radius: 7px;
304
+ cursor: pointer;
305
+ font-family: inherit;
306
+ font-size: 0.85rem;
307
+ font-weight: 500;
308
+ transition: background 0.15s;
309
+ }
310
+ .btn-approve {
311
+ background: #537492;
312
+ color: #fff;
313
+ }
314
+ .btn-approve:hover {
315
+ background: #3e5a73;
316
+ }
317
+ .btn-reject {
318
+ background: transparent;
319
+ color: #537492;
320
+ border: 1px solid #91adc5;
321
+ }
322
+ .btn-reject:hover {
323
+ background: #c8d9e8;
324
+ }
325
+
326
+ @media (prefers-color-scheme: dark) {
327
+ body {
328
+ background: #0a2138;
329
+ color: #c8dff0;
330
+ }
331
+ header {
332
+ background: #304c65;
333
+ }
334
+ #input-row {
335
+ background: #0a2138;
336
+ border-color: #2a4a63;
337
+ }
338
+ #prompt {
339
+ background: #0f2d45;
340
+ border-color: #2a4a63;
341
+ color: #c8dff0;
342
+ }
343
+ #prompt:focus {
344
+ border-color: #537492;
345
+ }
346
+ .msg.agent {
347
+ background: #1a3d58;
348
+ color: #c8dff0;
349
+ }
350
+ .msg.user {
351
+ background: #537492;
352
+ }
353
+ .msg.error {
354
+ background: #3b1818;
355
+ color: #ff8a80;
356
+ }
357
+ .typing {
358
+ background: #1a3d58;
359
+ }
360
+ .typing span {
361
+ background: #91adc5;
362
+ }
363
+ #send {
364
+ background: #537492;
365
+ }
366
+ #send:hover:not(:disabled) {
367
+ background: #6a8fa8;
368
+ }
369
+ #send:disabled {
370
+ background: #304c65;
371
+ }
372
+ #send.cancel {
373
+ background: #e53935;
374
+ }
375
+ #send.cancel:hover {
376
+ background: #ef5350;
377
+ }
378
+ .msg.approval {
379
+ background: #1a3d58;
380
+ color: #c8dff0;
381
+ border-left-color: #91adc5;
382
+ }
383
+ .msg.approval::before {
384
+ color: #91adc5;
385
+ }
386
+ .btn-approve {
387
+ background: #537492;
388
+ }
389
+ .btn-approve:hover {
390
+ background: #6a8fa8;
391
+ }
392
+ .btn-reject {
393
+ color: #c8dff0;
394
+ border-color: #2a4a63;
395
+ }
396
+ .btn-reject:hover {
397
+ background: #0f2d45;
398
+ }
399
+ }
400
+ </style>
401
+ <script src="marked.min.js"></script>
402
+ </head>
403
+ <body>
404
+ <header>
405
+ <a class="back" id="back" title="Back to index">&#8592;</a>
406
+ <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
+ <div class="status-dot" id="dot"></div>
417
+ </header>
418
+
419
+ <div id="messages">
420
+ <div class="typing" id="typing"><span></span><span></span><span></span></div>
421
+ <div class="status-text" id="status-text"></div>
422
+ </div>
423
+
424
+ <div id="input-row">
425
+ <textarea id="prompt" rows="1" placeholder="Message {{agentName}}…" autofocus></textarea>
426
+ <button id="send">Send</button>
427
+ </div>
428
+
429
+ <script>
430
+ const AGENT_URL = window.location.pathname.replace(/\/preview\/?$/, "") + "/"
431
+ const AUTH_HEADER = document.querySelector('meta[name="a2a-auth"]')?.content || ""
432
+ const authHeaders = () => (AUTH_HEADER ? { Authorization: AUTH_HEADER } : {})
433
+ document.getElementById("back").addEventListener("click", () => history.back())
434
+ const messages = document.getElementById("messages")
435
+ const typing = document.getElementById("typing")
436
+ const statusText = document.getElementById("status-text")
437
+ const prompt = document.getElementById("prompt")
438
+ const sendBtn = document.getElementById("send")
439
+ const dot = document.getElementById("dot")
440
+ const streamToggle = document.getElementById("streamToggle")
441
+
442
+ let busy = false
443
+ let contextId = null
444
+ let pendingTaskId = null
445
+ let activeTaskId = null
446
+ let activeAbort = null
447
+ // Live streaming bubble: created on first token, updated incrementally
448
+ let streamingBubble = null
449
+ let streamingText = ""
450
+
451
+ const HISTORY_KEY = "chatHistory_" + AGENT_URL
452
+ let inputHistory = JSON.parse(localStorage.getItem(HISTORY_KEY) || "[]")
453
+ let historyIndex = -1
454
+ let currentDraft = ""
455
+
456
+ const md = (text) => marked.parse(text, { breaks: true, gfm: true })
457
+
458
+ const partsToText = (parts) =>
459
+ (parts ?? [])
460
+ .filter((p) => p.kind === "text" || p.text)
461
+ .map((p) => p.text)
462
+ .join("")
463
+
464
+ prompt.addEventListener("input", () => {
465
+ prompt.style.height = "auto"
466
+ prompt.style.height = Math.min(prompt.scrollHeight, 140) + "px"
467
+ })
468
+ prompt.addEventListener("keydown", (e) => {
469
+ if (e.key === "Enter" && !e.shiftKey) {
470
+ e.preventDefault()
471
+ if (busy) cancel()
472
+ else send()
473
+ } else if (e.key === "ArrowUp" && prompt.selectionStart === 0) {
474
+ e.preventDefault()
475
+ if (historyIndex === -1) currentDraft = prompt.value
476
+ if (historyIndex < inputHistory.length - 1) {
477
+ historyIndex++
478
+ prompt.value = inputHistory[inputHistory.length - 1 - historyIndex]
479
+ prompt.selectionStart = prompt.selectionEnd = prompt.value.length
480
+ }
481
+ } else if (e.key === "ArrowDown" && prompt.selectionEnd === prompt.value.length) {
482
+ e.preventDefault()
483
+ if (historyIndex > 0) {
484
+ historyIndex--
485
+ prompt.value = inputHistory[inputHistory.length - 1 - historyIndex]
486
+ prompt.selectionStart = prompt.selectionEnd = prompt.value.length
487
+ } else if (historyIndex === 0) {
488
+ historyIndex = -1
489
+ prompt.value = currentDraft
490
+ prompt.selectionStart = prompt.selectionEnd = prompt.value.length
491
+ }
492
+ }
493
+ })
494
+ sendBtn.addEventListener("click", () => {
495
+ if (busy) cancel()
496
+ else send()
497
+ })
498
+
499
+ function addMessage(role, text) {
500
+ const el = document.createElement("div")
501
+ el.className = "msg " + role
502
+ if (role === "agent") el.innerHTML = md(text, { sanitize: true })
503
+ else el.textContent = text
504
+ messages.insertBefore(el, typing)
505
+ messages.scrollTop = messages.scrollHeight
506
+ return el
507
+ }
508
+
509
+ function addApprovalPrompt(description) {
510
+ const el = document.createElement("div")
511
+ el.className = "msg approval"
512
+ el.innerHTML = md(description || "The agent requires your approval to continue.")
513
+ const actions = document.createElement("div")
514
+ actions.className = "approval-actions"
515
+
516
+ const approveBtn = document.createElement("button")
517
+ approveBtn.className = "btn-approve"
518
+ approveBtn.textContent = "Approve"
519
+ approveBtn.addEventListener("click", () => {
520
+ removeApprovalPrompt(el)
521
+ resume("approve")
522
+ })
523
+
524
+ const rejectBtn = document.createElement("button")
525
+ rejectBtn.className = "btn-reject"
526
+ rejectBtn.textContent = "Reject"
527
+ rejectBtn.addEventListener("click", () => {
528
+ removeApprovalPrompt(el)
529
+ resume("reject")
530
+ })
531
+
532
+ actions.appendChild(approveBtn)
533
+ actions.appendChild(rejectBtn)
534
+ el.appendChild(actions)
535
+ messages.insertBefore(el, typing)
536
+ messages.scrollTop = messages.scrollHeight
537
+ return el
538
+ }
539
+
540
+ function removeApprovalPrompt(el) {
541
+ el.remove()
542
+ }
543
+
544
+ function setBusy(val) {
545
+ busy = val
546
+ prompt.disabled = val
547
+ typing.style.display = val ? "block" : "none"
548
+ dot.className = "status-dot" + (val ? " busy" : "")
549
+ if (val) {
550
+ sendBtn.textContent = "Cancel"
551
+ sendBtn.classList.add("cancel")
552
+ sendBtn.disabled = false
553
+ } else {
554
+ sendBtn.textContent = "Send"
555
+ sendBtn.classList.remove("cancel")
556
+ sendBtn.disabled = false
557
+ activeTaskId = null
558
+ activeAbort = null
559
+ statusText.textContent = ""
560
+ // Reset streaming state on any transition to idle
561
+ streamingBubble = null
562
+ streamingText = ""
563
+ }
564
+ messages.scrollTop = messages.scrollHeight
565
+ }
566
+
567
+ async function resume(decision) {
568
+ const taskId = pendingTaskId
569
+ pendingTaskId = null
570
+
571
+ activeTaskId = taskId
572
+ setBusy(true)
573
+
574
+ const abort = new AbortController()
575
+ activeAbort = abort
576
+ streamingBubble = null
577
+ streamingText = ""
578
+
579
+ try {
580
+ const resp = await fetch(AGENT_URL, {
581
+ method: "POST",
582
+ headers: { "Content-Type": "application/json", ...authHeaders() },
583
+ signal: abort.signal,
584
+ body: JSON.stringify({
585
+ jsonrpc: "2.0",
586
+ method: streamToggle.checked ? "message/stream" : "message/send",
587
+ id: crypto.randomUUID(),
588
+ params: {
589
+ message: {
590
+ role: "user",
591
+ messageId: crypto.randomUUID(),
592
+ taskId,
593
+ contextId,
594
+ parts: [{ kind: "text", text: decision }],
595
+ },
596
+ },
597
+ }),
598
+ })
599
+
600
+ if (streamToggle.checked) {
601
+ await handleSSEResponse(resp)
602
+ } else {
603
+ const envelope = await resp.json()
604
+ if (envelope.error) throw new Error(envelope.error.message || "Request failed")
605
+ await handleResult(envelope.result)
606
+ }
607
+ } catch (err) {
608
+ if (err.name === "AbortError") return
609
+ dot.className = "status-dot error"
610
+ addMessage("error", "⚠ " + (err.message || String(err)))
611
+ setTimeout(() => (dot.className = "status-dot"), 3000)
612
+ } finally {
613
+ setBusy(false)
614
+ prompt.focus()
615
+ }
616
+ }
617
+
618
+ async function handleResult(result) {
619
+ if (result?.contextId) contextId = result.contextId
620
+ if (result?.id) activeTaskId = result.id
621
+
622
+ const state = result?.status?.state ?? result?.state
623
+ const taskId = result?.id ?? result?.taskId
624
+
625
+ if (state === "input-required") {
626
+ pendingTaskId = taskId
627
+ const msgParts = result?.status?.message?.parts ?? []
628
+ const description = partsToText(msgParts)
629
+ addApprovalPrompt(description)
630
+ return
631
+ }
632
+
633
+ // If streaming was active and already rendered the final text via lastChunk, skip.
634
+ if (streamToggle.checked && streamingText !== "") {
635
+ streamingText = ""
636
+ return
637
+ }
638
+
639
+ // Clean up any open bubble (streaming was interrupted before lastChunk)
640
+ if (streamingBubble) {
641
+ if (streamingText) streamingBubble.innerHTML = md(streamingText)
642
+ streamingBubble = null
643
+ streamingText = ""
644
+ return
645
+ }
646
+
647
+ const parts =
648
+ result?.parts ?? result?.status?.message?.parts ?? result?.artifacts?.[0]?.parts ?? []
649
+ const reply = partsToText(parts)
650
+ addMessage("agent", reply || "(no response)")
651
+ }
652
+
653
+ async function send() {
654
+ const text = prompt.value.trim()
655
+ if (!text || busy) return
656
+
657
+ // Save to input history (skip if same as last entry)
658
+ if (inputHistory[inputHistory.length - 1] !== text) {
659
+ inputHistory.push(text)
660
+ if (inputHistory.length > 100) inputHistory.shift()
661
+ localStorage.setItem(HISTORY_KEY, JSON.stringify(inputHistory))
662
+ }
663
+ historyIndex = -1
664
+ currentDraft = ""
665
+
666
+ addMessage("user", text)
667
+ prompt.value = ""
668
+ prompt.style.height = "auto"
669
+ setBusy(true)
670
+
671
+ const abort = new AbortController()
672
+ activeAbort = abort
673
+ streamingBubble = null
674
+ streamingText = ""
675
+
676
+ const useStream = streamToggle.checked
677
+ try {
678
+ const resp = await fetch(AGENT_URL, {
679
+ method: "POST",
680
+ headers: { "Content-Type": "application/json", ...authHeaders() },
681
+ signal: abort.signal,
682
+ body: JSON.stringify({
683
+ jsonrpc: "2.0",
684
+ method: useStream ? "message/stream" : "message/send",
685
+ id: crypto.randomUUID(),
686
+ params: {
687
+ message: {
688
+ role: "user",
689
+ messageId: crypto.randomUUID(),
690
+ ...(contextId ? { contextId } : {}),
691
+ parts: [{ kind: "text", text }],
692
+ },
693
+ },
694
+ }),
695
+ })
696
+
697
+ if (useStream) {
698
+ await handleSSEResponse(resp)
699
+ } else {
700
+ const envelope = await resp.json()
701
+ if (envelope.error) throw new Error(envelope.error.message || "Request failed")
702
+ await handleResult(envelope.result)
703
+ }
704
+ } catch (err) {
705
+ if (err.name === "AbortError") return
706
+ dot.className = "status-dot error"
707
+ addMessage("error", "⚠ " + (err.message || String(err)))
708
+ setTimeout(() => {
709
+ dot.className = "status-dot"
710
+ }, 3000)
711
+ } finally {
712
+ setBusy(false)
713
+ prompt.focus()
714
+ }
715
+ }
716
+
717
+ async function handleSSEResponse(resp) {
718
+ if (!resp.ok) {
719
+ const body = await resp.text()
720
+ let parsed
721
+ try {
722
+ parsed = JSON.parse(body)
723
+ } catch (e) {
724
+ /* not JSON */
725
+ }
726
+ throw new Error(parsed?.error?.message || `HTTP ${resp.status}: ${body.slice(0, 200)}`)
727
+ }
728
+
729
+ const reader = resp.body.getReader()
730
+ const decoder = new TextDecoder()
731
+ let buffer = ""
732
+ let lastResult = null
733
+
734
+ while (true) {
735
+ const { done, value } = await reader.read()
736
+ if (done) break
737
+ buffer += decoder.decode(value, { stream: true })
738
+
739
+ let lineEnd
740
+ while ((lineEnd = buffer.indexOf("\n")) !== -1) {
741
+ const line = buffer.slice(0, lineEnd).trim()
742
+ buffer = buffer.slice(lineEnd + 1)
743
+
744
+ if (line.startsWith("data: ")) {
745
+ try {
746
+ const frame = JSON.parse(line.slice(6))
747
+ // SSE frames are JSON-RPC envelopes: {jsonrpc, id, result} or {jsonrpc, id, error}
748
+ if (frame.error) throw new Error(frame.error.message || "Stream error")
749
+ if (frame.result) {
750
+ lastResult = processSSEEvent(frame.result) || lastResult
751
+ }
752
+ } catch (e) {
753
+ if (e instanceof SyntaxError) continue
754
+ throw e
755
+ }
756
+ } else if (line.startsWith("event: error")) {
757
+ // Next data line will have the error
758
+ }
759
+ }
760
+ }
761
+
762
+ if (lastResult) await handleResult(lastResult)
763
+ }
764
+
765
+ function processSSEEvent(event) {
766
+ // Extract taskId from first task event
767
+ if (event.kind === "task" && event.id) {
768
+ activeTaskId = event.id
769
+ contextId = event.contextId ?? contextId
770
+ }
771
+
772
+ // Track contextId from status-updates
773
+ if (event.contextId) contextId = event.contextId
774
+
775
+ // Non-final status-update with message → show as progress text from streaming
776
+ if (event.kind === "status-update" && !event.final && event.status?.message) {
777
+ const text = partsToText(event.status.message.parts)
778
+ if (text) {
779
+ statusText.textContent = text
780
+ messages.scrollTop = messages.scrollHeight
781
+ }
782
+ return null
783
+ }
784
+
785
+ // Incremental artifact-update: render tokens as they arrive
786
+ if (event.kind === "artifact-update" && streamToggle.checked) {
787
+ const parts = event.artifact?.parts ?? []
788
+ const text = partsToText(parts)
789
+ // A2A spec: append/lastChunk are event-level fields (siblings of `artifact`).
790
+ const append = event.append ?? false
791
+ const lastChunk = event.lastChunk ?? false
792
+
793
+ if (lastChunk) {
794
+ // lastChunk carries the authoritative full text — replace accumulated content.
795
+ if (text) {
796
+ if (streamingBubble) {
797
+ streamingBubble.innerHTML = md(text)
798
+ messages.scrollTop = messages.scrollHeight
799
+ } else {
800
+ typing.style.display = "none"
801
+ addMessage("agent", text)
802
+ }
803
+ }
804
+ streamingBubble = null
805
+ streamingText = text // keep so handleResult skips duplicate render
806
+ return null
807
+ }
808
+
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
+ }
822
+
823
+ return null
824
+ }
825
+
826
+ // Status update with final=true means terminal state
827
+ if (event.kind === "status-update" && event.final) {
828
+ return {
829
+ id: event.taskId || activeTaskId,
830
+ contextId: event.contextId || contextId,
831
+ status: event.status,
832
+ artifacts: event.artifacts,
833
+ }
834
+ }
835
+
836
+ // Artifact event
837
+ if (event.kind === "artifact") {
838
+ return {
839
+ id: event.taskId || activeTaskId,
840
+ contextId: event.contextId || contextId,
841
+ status: event.status || { state: "completed" },
842
+ artifacts: [event.artifact || event],
843
+ }
844
+ }
845
+
846
+ return null
847
+ }
848
+
849
+ async function cancel() {
850
+ const taskId = activeTaskId
851
+
852
+ // Abort ongoing fetch
853
+ if (activeAbort) activeAbort.abort()
854
+
855
+ // Send tasks/cancel if we have a taskId
856
+ if (taskId) {
857
+ fetch(AGENT_URL, {
858
+ method: "POST",
859
+ headers: { "Content-Type": "application/json", ...authHeaders() },
860
+ body: JSON.stringify({
861
+ jsonrpc: "2.0",
862
+ method: "tasks/cancel",
863
+ id: crypto.randomUUID(),
864
+ params: { id: taskId },
865
+ }),
866
+ }).catch(() => {})
867
+ }
868
+
869
+ addMessage("agent", "⊘ Task canceled.")
870
+ setBusy(false)
871
+ prompt.focus()
872
+ }
873
+ </script>
874
+ </body>
875
+ </html>