@cap-js/agents 0.0.0 → 0.9.2
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.
- package/LICENSE +201 -0
- package/README.md +154 -0
- package/_i18n/messages.properties +31 -0
- package/cds-plugin.js +140 -0
- package/index.cds +116 -0
- package/index.js +0 -0
- package/lib/agents/markdown/backends/mime-utils.js +37 -0
- package/lib/agents/markdown/backends/outputs-backend.js +156 -0
- package/lib/agents/markdown/backends/readonly-backend.js +48 -0
- package/lib/agents/markdown/backends/uploads-backend.js +160 -0
- package/lib/agents/markdown/deep-agent.js +82 -0
- package/lib/agents/middleware/agent-actions.js +18 -0
- package/lib/agents/middleware/content-filter.js +191 -0
- package/lib/agents/middleware/hitl-edit-note-injector.js +18 -0
- package/lib/agents/middleware/hitl.js +20 -0
- package/lib/agents/middleware/index.js +19 -0
- package/lib/agents/middleware/patch-tool-calls.js +51 -0
- package/lib/agents/middleware/quota-enforcer.js +94 -0
- package/lib/agents/middleware/status-update.js +153 -0
- package/lib/agents/middleware/tool-selection.js +22 -0
- package/lib/agents/quota-enforcer-at-start.js +198 -0
- package/lib/agents/summarize-on-timeout.js +91 -0
- package/lib/compile.js +55 -0
- package/lib/index.cjs +1 -0
- package/lib/index.js +422 -0
- package/lib/models/aicore.js +441 -0
- package/lib/models/anthropic.js +77 -0
- package/lib/models/mock.js +88 -0
- package/lib/preview/chat.html +897 -0
- package/lib/preview/preview.js +46 -0
- package/lib/protocol/agent-card.js +297 -0
- package/lib/protocol/persistence/checkpoint-saver.js +320 -0
- package/lib/protocol/persistence/cleanup.js +84 -0
- package/lib/protocol/persistence/file-store.js +209 -0
- package/lib/protocol/persistence/push-notification-store.js +59 -0
- package/lib/protocol/persistence/task-store.js +47 -0
- package/lib/protocol/push-notification-sender.js +57 -0
- package/lib/sidecar.js +162 -0
- package/lib/telemetry/active-users.js +106 -0
- package/lib/telemetry/chat-tracing.js +364 -0
- package/lib/telemetry/metrics.js +85 -0
- package/lib/telemetry/mlflow.js +290 -0
- package/lib/telemetry/tool-tracing.js +164 -0
- package/lib/telemetry/tracing.js +150 -0
- package/lib/utils/inner-auth.js +33 -0
- package/lib/utils/markdown.js +199 -0
- package/lib/utils/message-handling.js +155 -0
- package/lib/utils/utils.js +168 -0
- package/package.json +225 -2
- package/srv/graph-cache.js +82 -0
- package/srv/handlers/graph-executor.js +1374 -0
- package/srv/handlers/index.js +178 -0
- package/srv/handlers/mcp-tools.js +161 -0
- package/srv/handlers/sub-agent-tools.js +316 -0
- package/srv/handlers/system-prompt.js +25 -0
- package/srv/handlers/tools.js +366 -0
- package/srv/langgraph-executor-srv.js +70 -0
- package/srv/push-notification-srv.js +149 -0
|
@@ -0,0 +1,897 @@
|
|
|
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">←</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
|
+
|
|
433
|
+
const fetchCsrfToken = async () => {
|
|
434
|
+
if (document.csrfToken === undefined) {
|
|
435
|
+
const res = await fetch("/", {
|
|
436
|
+
method: "HEAD",
|
|
437
|
+
headers: { "x-csrf-token": "fetch", ...(await authHeaders({ includeCsrf: false })) },
|
|
438
|
+
})
|
|
439
|
+
if (!res.ok) {
|
|
440
|
+
throw new Error(`Could not fetch CSRF token ${res.status}: ${res.statusText}`)
|
|
441
|
+
}
|
|
442
|
+
document.csrfToken = res.headers.get("x-csrf-token")
|
|
443
|
+
}
|
|
444
|
+
return document.csrfToken
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const authHeaders = async ({ includeCsrf = true } = {}) => {
|
|
448
|
+
const headers = AUTH_HEADER ? { Authorization: AUTH_HEADER } : {}
|
|
449
|
+
if (!includeCsrf) return headers
|
|
450
|
+
const csrfToken = await fetchCsrfToken()
|
|
451
|
+
if (csrfToken) headers["x-csrf-token"] = csrfToken
|
|
452
|
+
return headers
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
document.getElementById("back").addEventListener("click", () => history.back())
|
|
456
|
+
const messages = document.getElementById("messages")
|
|
457
|
+
const typing = document.getElementById("typing")
|
|
458
|
+
const statusText = document.getElementById("status-text")
|
|
459
|
+
const prompt = document.getElementById("prompt")
|
|
460
|
+
const sendBtn = document.getElementById("send")
|
|
461
|
+
const dot = document.getElementById("dot")
|
|
462
|
+
const streamToggle = document.getElementById("streamToggle")
|
|
463
|
+
|
|
464
|
+
let busy = false
|
|
465
|
+
let contextId = null
|
|
466
|
+
let pendingTaskId = null
|
|
467
|
+
let activeTaskId = null
|
|
468
|
+
let activeAbort = null
|
|
469
|
+
// Live streaming bubble: created on first token, updated incrementally
|
|
470
|
+
let streamingBubble = null
|
|
471
|
+
let streamingText = ""
|
|
472
|
+
|
|
473
|
+
const HISTORY_KEY = "chatHistory_" + AGENT_URL
|
|
474
|
+
let inputHistory = JSON.parse(localStorage.getItem(HISTORY_KEY) || "[]")
|
|
475
|
+
let historyIndex = -1
|
|
476
|
+
let currentDraft = ""
|
|
477
|
+
|
|
478
|
+
const md = (text) => marked.parse(text, { breaks: true, gfm: true })
|
|
479
|
+
|
|
480
|
+
const partsToText = (parts) =>
|
|
481
|
+
(parts ?? [])
|
|
482
|
+
.filter((p) => p.kind === "text" || p.text)
|
|
483
|
+
.map((p) => p.text)
|
|
484
|
+
.join("")
|
|
485
|
+
|
|
486
|
+
prompt.addEventListener("input", () => {
|
|
487
|
+
prompt.style.height = "auto"
|
|
488
|
+
prompt.style.height = Math.min(prompt.scrollHeight, 140) + "px"
|
|
489
|
+
})
|
|
490
|
+
prompt.addEventListener("keydown", (e) => {
|
|
491
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
492
|
+
e.preventDefault()
|
|
493
|
+
if (busy) cancel()
|
|
494
|
+
else send()
|
|
495
|
+
} else if (e.key === "ArrowUp" && prompt.selectionStart === 0) {
|
|
496
|
+
e.preventDefault()
|
|
497
|
+
if (historyIndex === -1) currentDraft = prompt.value
|
|
498
|
+
if (historyIndex < inputHistory.length - 1) {
|
|
499
|
+
historyIndex++
|
|
500
|
+
prompt.value = inputHistory[inputHistory.length - 1 - historyIndex]
|
|
501
|
+
prompt.selectionStart = prompt.selectionEnd = prompt.value.length
|
|
502
|
+
}
|
|
503
|
+
} else if (e.key === "ArrowDown" && prompt.selectionEnd === prompt.value.length) {
|
|
504
|
+
e.preventDefault()
|
|
505
|
+
if (historyIndex > 0) {
|
|
506
|
+
historyIndex--
|
|
507
|
+
prompt.value = inputHistory[inputHistory.length - 1 - historyIndex]
|
|
508
|
+
prompt.selectionStart = prompt.selectionEnd = prompt.value.length
|
|
509
|
+
} else if (historyIndex === 0) {
|
|
510
|
+
historyIndex = -1
|
|
511
|
+
prompt.value = currentDraft
|
|
512
|
+
prompt.selectionStart = prompt.selectionEnd = prompt.value.length
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
})
|
|
516
|
+
sendBtn.addEventListener("click", () => {
|
|
517
|
+
if (busy) cancel()
|
|
518
|
+
else send()
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
function addMessage(role, text) {
|
|
522
|
+
const el = document.createElement("div")
|
|
523
|
+
el.className = "msg " + role
|
|
524
|
+
if (role === "agent") el.innerHTML = md(text, { sanitize: true })
|
|
525
|
+
else el.textContent = text
|
|
526
|
+
messages.insertBefore(el, typing)
|
|
527
|
+
messages.scrollTop = messages.scrollHeight
|
|
528
|
+
return el
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function addApprovalPrompt(description) {
|
|
532
|
+
const el = document.createElement("div")
|
|
533
|
+
el.className = "msg approval"
|
|
534
|
+
el.innerHTML = md(description || "The agent requires your approval to continue.")
|
|
535
|
+
const actions = document.createElement("div")
|
|
536
|
+
actions.className = "approval-actions"
|
|
537
|
+
|
|
538
|
+
const approveBtn = document.createElement("button")
|
|
539
|
+
approveBtn.className = "btn-approve"
|
|
540
|
+
approveBtn.textContent = "Approve"
|
|
541
|
+
approveBtn.addEventListener("click", () => {
|
|
542
|
+
removeApprovalPrompt(el)
|
|
543
|
+
resume("approve")
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
const rejectBtn = document.createElement("button")
|
|
547
|
+
rejectBtn.className = "btn-reject"
|
|
548
|
+
rejectBtn.textContent = "Reject"
|
|
549
|
+
rejectBtn.addEventListener("click", () => {
|
|
550
|
+
removeApprovalPrompt(el)
|
|
551
|
+
resume("reject")
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
actions.appendChild(approveBtn)
|
|
555
|
+
actions.appendChild(rejectBtn)
|
|
556
|
+
el.appendChild(actions)
|
|
557
|
+
messages.insertBefore(el, typing)
|
|
558
|
+
messages.scrollTop = messages.scrollHeight
|
|
559
|
+
return el
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function removeApprovalPrompt(el) {
|
|
563
|
+
el.remove()
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function setBusy(val) {
|
|
567
|
+
busy = val
|
|
568
|
+
prompt.disabled = val
|
|
569
|
+
typing.style.display = val ? "block" : "none"
|
|
570
|
+
dot.className = "status-dot" + (val ? " busy" : "")
|
|
571
|
+
if (val) {
|
|
572
|
+
sendBtn.textContent = "Cancel"
|
|
573
|
+
sendBtn.classList.add("cancel")
|
|
574
|
+
sendBtn.disabled = false
|
|
575
|
+
} else {
|
|
576
|
+
sendBtn.textContent = "Send"
|
|
577
|
+
sendBtn.classList.remove("cancel")
|
|
578
|
+
sendBtn.disabled = false
|
|
579
|
+
activeTaskId = null
|
|
580
|
+
activeAbort = null
|
|
581
|
+
statusText.textContent = ""
|
|
582
|
+
// Reset streaming state on any transition to idle
|
|
583
|
+
streamingBubble = null
|
|
584
|
+
streamingText = ""
|
|
585
|
+
}
|
|
586
|
+
messages.scrollTop = messages.scrollHeight
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async function resume(decision) {
|
|
590
|
+
const taskId = pendingTaskId
|
|
591
|
+
pendingTaskId = null
|
|
592
|
+
|
|
593
|
+
activeTaskId = taskId
|
|
594
|
+
setBusy(true)
|
|
595
|
+
|
|
596
|
+
const abort = new AbortController()
|
|
597
|
+
activeAbort = abort
|
|
598
|
+
streamingBubble = null
|
|
599
|
+
streamingText = ""
|
|
600
|
+
|
|
601
|
+
try {
|
|
602
|
+
const resp = await fetch(AGENT_URL, {
|
|
603
|
+
method: "POST",
|
|
604
|
+
headers: { "Content-Type": "application/json", ...(await authHeaders()) },
|
|
605
|
+
signal: abort.signal,
|
|
606
|
+
body: JSON.stringify({
|
|
607
|
+
jsonrpc: "2.0",
|
|
608
|
+
method: streamToggle.checked ? "message/stream" : "message/send",
|
|
609
|
+
id: crypto.randomUUID(),
|
|
610
|
+
params: {
|
|
611
|
+
message: {
|
|
612
|
+
role: "user",
|
|
613
|
+
messageId: crypto.randomUUID(),
|
|
614
|
+
taskId,
|
|
615
|
+
contextId,
|
|
616
|
+
parts: [{ kind: "text", text: decision }],
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
}),
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
if (streamToggle.checked) {
|
|
623
|
+
await handleSSEResponse(resp)
|
|
624
|
+
} else {
|
|
625
|
+
const envelope = await resp.json()
|
|
626
|
+
if (envelope.error) throw new Error(envelope.error.message || "Request failed")
|
|
627
|
+
await handleResult(envelope.result)
|
|
628
|
+
}
|
|
629
|
+
} catch (err) {
|
|
630
|
+
if (err.name === "AbortError") return
|
|
631
|
+
dot.className = "status-dot error"
|
|
632
|
+
addMessage("error", "⚠ " + (err.message || String(err)))
|
|
633
|
+
setTimeout(() => (dot.className = "status-dot"), 3000)
|
|
634
|
+
} finally {
|
|
635
|
+
setBusy(false)
|
|
636
|
+
prompt.focus()
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async function handleResult(result) {
|
|
641
|
+
if (result?.contextId) contextId = result.contextId
|
|
642
|
+
if (result?.id) activeTaskId = result.id
|
|
643
|
+
|
|
644
|
+
const state = result?.status?.state ?? result?.state
|
|
645
|
+
const taskId = result?.id ?? result?.taskId
|
|
646
|
+
|
|
647
|
+
if (state === "input-required") {
|
|
648
|
+
pendingTaskId = taskId
|
|
649
|
+
const msgParts = result?.status?.message?.parts ?? []
|
|
650
|
+
const description = partsToText(msgParts)
|
|
651
|
+
addApprovalPrompt(description)
|
|
652
|
+
return
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// If streaming was active and already rendered the final text via lastChunk, skip.
|
|
656
|
+
if (streamToggle.checked && streamingText !== "") {
|
|
657
|
+
streamingText = ""
|
|
658
|
+
return
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Clean up any open bubble (streaming was interrupted before lastChunk)
|
|
662
|
+
if (streamingBubble) {
|
|
663
|
+
if (streamingText) streamingBubble.innerHTML = md(streamingText)
|
|
664
|
+
streamingBubble = null
|
|
665
|
+
streamingText = ""
|
|
666
|
+
return
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const parts =
|
|
670
|
+
result?.parts ?? result?.status?.message?.parts ?? result?.artifacts?.[0]?.parts ?? []
|
|
671
|
+
const reply = partsToText(parts)
|
|
672
|
+
addMessage("agent", reply || "(no response)")
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
async function send() {
|
|
676
|
+
const text = prompt.value.trim()
|
|
677
|
+
if (!text || busy) return
|
|
678
|
+
|
|
679
|
+
// Save to input history (skip if same as last entry)
|
|
680
|
+
if (inputHistory[inputHistory.length - 1] !== text) {
|
|
681
|
+
inputHistory.push(text)
|
|
682
|
+
if (inputHistory.length > 100) inputHistory.shift()
|
|
683
|
+
localStorage.setItem(HISTORY_KEY, JSON.stringify(inputHistory))
|
|
684
|
+
}
|
|
685
|
+
historyIndex = -1
|
|
686
|
+
currentDraft = ""
|
|
687
|
+
|
|
688
|
+
addMessage("user", text)
|
|
689
|
+
prompt.value = ""
|
|
690
|
+
prompt.style.height = "auto"
|
|
691
|
+
setBusy(true)
|
|
692
|
+
|
|
693
|
+
const abort = new AbortController()
|
|
694
|
+
activeAbort = abort
|
|
695
|
+
streamingBubble = null
|
|
696
|
+
streamingText = ""
|
|
697
|
+
|
|
698
|
+
const useStream = streamToggle.checked
|
|
699
|
+
try {
|
|
700
|
+
const resp = await fetch(AGENT_URL, {
|
|
701
|
+
method: "POST",
|
|
702
|
+
headers: { "Content-Type": "application/json", ...(await authHeaders()) },
|
|
703
|
+
signal: abort.signal,
|
|
704
|
+
body: JSON.stringify({
|
|
705
|
+
jsonrpc: "2.0",
|
|
706
|
+
method: useStream ? "message/stream" : "message/send",
|
|
707
|
+
id: crypto.randomUUID(),
|
|
708
|
+
params: {
|
|
709
|
+
message: {
|
|
710
|
+
role: "user",
|
|
711
|
+
messageId: crypto.randomUUID(),
|
|
712
|
+
...(contextId ? { contextId } : {}),
|
|
713
|
+
parts: [{ kind: "text", text }],
|
|
714
|
+
},
|
|
715
|
+
},
|
|
716
|
+
}),
|
|
717
|
+
})
|
|
718
|
+
|
|
719
|
+
if (useStream) {
|
|
720
|
+
await handleSSEResponse(resp)
|
|
721
|
+
} else {
|
|
722
|
+
const envelope = await resp.json()
|
|
723
|
+
if (envelope.error) throw new Error(envelope.error.message || "Request failed")
|
|
724
|
+
await handleResult(envelope.result)
|
|
725
|
+
}
|
|
726
|
+
} catch (err) {
|
|
727
|
+
if (err.name === "AbortError") return
|
|
728
|
+
dot.className = "status-dot error"
|
|
729
|
+
addMessage("error", "⚠ " + (err.message || String(err)))
|
|
730
|
+
setTimeout(() => {
|
|
731
|
+
dot.className = "status-dot"
|
|
732
|
+
}, 3000)
|
|
733
|
+
} finally {
|
|
734
|
+
setBusy(false)
|
|
735
|
+
prompt.focus()
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
async function handleSSEResponse(resp) {
|
|
740
|
+
if (!resp.ok) {
|
|
741
|
+
const body = await resp.text()
|
|
742
|
+
let parsed
|
|
743
|
+
try {
|
|
744
|
+
parsed = JSON.parse(body)
|
|
745
|
+
} catch (e) {
|
|
746
|
+
/* not JSON */
|
|
747
|
+
}
|
|
748
|
+
throw new Error(parsed?.error?.message || `HTTP ${resp.status}: ${body.slice(0, 200)}`)
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const reader = resp.body.getReader()
|
|
752
|
+
const decoder = new TextDecoder()
|
|
753
|
+
let buffer = ""
|
|
754
|
+
let lastResult = null
|
|
755
|
+
|
|
756
|
+
while (true) {
|
|
757
|
+
const { done, value } = await reader.read()
|
|
758
|
+
if (done) break
|
|
759
|
+
buffer += decoder.decode(value, { stream: true })
|
|
760
|
+
|
|
761
|
+
let lineEnd
|
|
762
|
+
while ((lineEnd = buffer.indexOf("\n")) !== -1) {
|
|
763
|
+
const line = buffer.slice(0, lineEnd).trim()
|
|
764
|
+
buffer = buffer.slice(lineEnd + 1)
|
|
765
|
+
|
|
766
|
+
if (line.startsWith("data: ")) {
|
|
767
|
+
try {
|
|
768
|
+
const frame = JSON.parse(line.slice(6))
|
|
769
|
+
// SSE frames are JSON-RPC envelopes: {jsonrpc, id, result} or {jsonrpc, id, error}
|
|
770
|
+
if (frame.error) throw new Error(frame.error.message || "Stream error")
|
|
771
|
+
if (frame.result) {
|
|
772
|
+
lastResult = processSSEEvent(frame.result) || lastResult
|
|
773
|
+
}
|
|
774
|
+
} catch (e) {
|
|
775
|
+
if (e instanceof SyntaxError) continue
|
|
776
|
+
throw e
|
|
777
|
+
}
|
|
778
|
+
} else if (line.startsWith("event: error")) {
|
|
779
|
+
// Next data line will have the error
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (lastResult) await handleResult(lastResult)
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function processSSEEvent(event) {
|
|
788
|
+
// Extract taskId from first task event
|
|
789
|
+
if (event.kind === "task" && event.id) {
|
|
790
|
+
activeTaskId = event.id
|
|
791
|
+
contextId = event.contextId ?? contextId
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Track contextId from status-updates
|
|
795
|
+
if (event.contextId) contextId = event.contextId
|
|
796
|
+
|
|
797
|
+
// Non-final status-update with message → show as progress text from streaming
|
|
798
|
+
if (event.kind === "status-update" && !event.final && event.status?.message) {
|
|
799
|
+
const text = partsToText(event.status.message.parts)
|
|
800
|
+
if (text) {
|
|
801
|
+
statusText.textContent = text
|
|
802
|
+
messages.scrollTop = messages.scrollHeight
|
|
803
|
+
}
|
|
804
|
+
return null
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Incremental artifact-update: render tokens as they arrive
|
|
808
|
+
if (event.kind === "artifact-update" && streamToggle.checked) {
|
|
809
|
+
const parts = event.artifact?.parts ?? []
|
|
810
|
+
const text = partsToText(parts)
|
|
811
|
+
// A2A spec: append/lastChunk are event-level fields (siblings of `artifact`).
|
|
812
|
+
const append = event.append ?? false
|
|
813
|
+
const lastChunk = event.lastChunk ?? false
|
|
814
|
+
|
|
815
|
+
if (lastChunk) {
|
|
816
|
+
// lastChunk carries the authoritative full text — replace accumulated content.
|
|
817
|
+
if (text) {
|
|
818
|
+
if (streamingBubble) {
|
|
819
|
+
streamingBubble.innerHTML = md(text)
|
|
820
|
+
messages.scrollTop = messages.scrollHeight
|
|
821
|
+
} else {
|
|
822
|
+
typing.style.display = "none"
|
|
823
|
+
addMessage("agent", text)
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
streamingBubble = null
|
|
827
|
+
streamingText = text // keep so handleResult skips duplicate render
|
|
828
|
+
return null
|
|
829
|
+
}
|
|
830
|
+
|
|
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
|
+
}
|
|
844
|
+
|
|
845
|
+
return null
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Status update with final=true means terminal state
|
|
849
|
+
if (event.kind === "status-update" && event.final) {
|
|
850
|
+
return {
|
|
851
|
+
id: event.taskId || activeTaskId,
|
|
852
|
+
contextId: event.contextId || contextId,
|
|
853
|
+
status: event.status,
|
|
854
|
+
artifacts: event.artifacts,
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Artifact event
|
|
859
|
+
if (event.kind === "artifact") {
|
|
860
|
+
return {
|
|
861
|
+
id: event.taskId || activeTaskId,
|
|
862
|
+
contextId: event.contextId || contextId,
|
|
863
|
+
status: event.status || { state: "completed" },
|
|
864
|
+
artifacts: [event.artifact || event],
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
return null
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
async function cancel() {
|
|
872
|
+
const taskId = activeTaskId
|
|
873
|
+
|
|
874
|
+
// Abort ongoing fetch
|
|
875
|
+
if (activeAbort) activeAbort.abort()
|
|
876
|
+
|
|
877
|
+
// Send tasks/cancel if we have a taskId
|
|
878
|
+
if (taskId) {
|
|
879
|
+
fetch(AGENT_URL, {
|
|
880
|
+
method: "POST",
|
|
881
|
+
headers: { "Content-Type": "application/json", ...(await authHeaders()) },
|
|
882
|
+
body: JSON.stringify({
|
|
883
|
+
jsonrpc: "2.0",
|
|
884
|
+
method: "tasks/cancel",
|
|
885
|
+
id: crypto.randomUUID(),
|
|
886
|
+
params: { id: taskId },
|
|
887
|
+
}),
|
|
888
|
+
}).catch(() => {})
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
addMessage("agent", "⊘ Task canceled.")
|
|
892
|
+
setBusy(false)
|
|
893
|
+
prompt.focus()
|
|
894
|
+
}
|
|
895
|
+
</script>
|
|
896
|
+
</body>
|
|
897
|
+
</html>
|