@webless/agent 0.6.7 → 0.6.9

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/dist/index.js CHANGED
@@ -268,6 +268,203 @@ function clearPersistedAgentSession(visitorSessionId, options) {
268
268
  sessionStorage.removeItem(runtimeLastMessageKey(visitorSessionId, prefix));
269
269
  }
270
270
 
271
+ // src/runtime/subagent-child-stream.ts
272
+ var INITIAL_RETRY_DELAY_MS = 100;
273
+ var MAX_RETRY_DELAY_MS = 2e3;
274
+ var MAX_CONSECUTIVE_RETRIES = 6;
275
+ function isRecord2(value) {
276
+ return typeof value === "object" && value !== null && !Array.isArray(value);
277
+ }
278
+ function parseError(value) {
279
+ if (!isRecord2(value)) return void 0;
280
+ const { code, message } = value;
281
+ if (typeof code !== "string" || typeof message !== "string") {
282
+ return void 0;
283
+ }
284
+ return { code, message };
285
+ }
286
+ function parseChildStreamEvent(value) {
287
+ if (!isRecord2(value) || typeof value.type !== "string") {
288
+ return { type: "other" };
289
+ }
290
+ if (value.type === "session.waiting" || value.type === "session.completed" || value.type === "session.failed") {
291
+ return { type: "session.boundary" };
292
+ }
293
+ if (value.type === "subagent.event" && isRecord2(value.data) && Object.hasOwn(value.data, "event")) {
294
+ return parseChildStreamEvent(value.data.event);
295
+ }
296
+ if (value.type === "subagent.called" && isRecord2(value.data)) {
297
+ const { childStreamPath } = value.data;
298
+ if (typeof childStreamPath === "string") {
299
+ return { childStreamPath, type: "subagent.called" };
300
+ }
301
+ }
302
+ if (value.type !== "action.result" || !isRecord2(value.data)) {
303
+ return { type: "other" };
304
+ }
305
+ const { data } = value;
306
+ if (data.status !== "completed" && data.status !== "failed" && data.status !== "rejected") {
307
+ return { type: "other" };
308
+ }
309
+ if (!isRecord2(data.result) || data.result.kind !== "tool-result") {
310
+ return { type: "other" };
311
+ }
312
+ const result = data.result;
313
+ if (typeof result.callId !== "string" || typeof result.toolName !== "string") {
314
+ return { type: "other" };
315
+ }
316
+ const error = parseError(data.error);
317
+ return {
318
+ type: "action.result",
319
+ hasOutput: Object.hasOwn(result, "output"),
320
+ result: {
321
+ callId: result.callId,
322
+ toolName: result.toolName,
323
+ status: data.status,
324
+ ...Object.hasOwn(result, "output") ? { output: result.output } : {},
325
+ ...error ? { error } : {}
326
+ }
327
+ };
328
+ }
329
+ async function* readNdjsonStream(body) {
330
+ const reader = body.getReader();
331
+ const decoder = new TextDecoder();
332
+ let buffer = "";
333
+ try {
334
+ while (true) {
335
+ const { done, value } = await reader.read();
336
+ buffer += decoder.decode(value, { stream: !done });
337
+ const lines = buffer.split("\n");
338
+ buffer = lines.pop() ?? "";
339
+ for (const line of lines) {
340
+ const trimmed2 = line.trim();
341
+ if (!trimmed2) continue;
342
+ try {
343
+ const parsed = JSON.parse(trimmed2);
344
+ yield parsed;
345
+ } catch {
346
+ }
347
+ }
348
+ if (done) break;
349
+ }
350
+ const trimmed = buffer.trim();
351
+ if (trimmed) {
352
+ try {
353
+ const parsed = JSON.parse(trimmed);
354
+ yield parsed;
355
+ } catch {
356
+ }
357
+ }
358
+ } finally {
359
+ reader.releaseLock();
360
+ }
361
+ }
362
+ function streamPathAt(path, streamIndex) {
363
+ if (streamIndex === 0) return path;
364
+ return `${path}${path.includes("?") ? "&" : "?"}startIndex=${streamIndex}`;
365
+ }
366
+ function abortableDelay(delayMs, signal) {
367
+ if (signal.aborted) return Promise.resolve();
368
+ return new Promise((resolve) => {
369
+ const finish = () => {
370
+ clearTimeout(timeout);
371
+ signal.removeEventListener("abort", finish);
372
+ resolve();
373
+ };
374
+ const timeout = setTimeout(finish, delayMs);
375
+ signal.addEventListener("abort", finish, { once: true });
376
+ });
377
+ }
378
+ var SubagentChildStreamCoordinator = class {
379
+ constructor(client, handlers, parentSignal) {
380
+ this.client = client;
381
+ this.handlers = handlers;
382
+ this.parentSignal = parentSignal;
383
+ }
384
+ client;
385
+ handlers;
386
+ parentSignal;
387
+ controllers = /* @__PURE__ */ new Map();
388
+ tasks = /* @__PURE__ */ new Map();
389
+ begin(event) {
390
+ this.beginPath(event.data.childStreamPath);
391
+ }
392
+ async waitForAll() {
393
+ let observedTaskCount = -1;
394
+ while (observedTaskCount !== this.tasks.size) {
395
+ observedTaskCount = this.tasks.size;
396
+ await Promise.all(this.tasks.values());
397
+ }
398
+ }
399
+ abortAll() {
400
+ for (const controller of this.controllers.values()) controller.abort();
401
+ this.controllers.clear();
402
+ }
403
+ beginPath(childStreamPath) {
404
+ if (this.tasks.has(childStreamPath)) return;
405
+ const controller = new AbortController();
406
+ const abort = () => controller.abort();
407
+ if (this.parentSignal.aborted) {
408
+ controller.abort();
409
+ } else {
410
+ this.parentSignal.addEventListener("abort", abort, { once: true });
411
+ }
412
+ this.controllers.set(childStreamPath, controller);
413
+ const task = this.consume(childStreamPath, controller.signal).finally(
414
+ () => {
415
+ this.parentSignal.removeEventListener("abort", abort);
416
+ if (this.controllers.get(childStreamPath) === controller) {
417
+ this.controllers.delete(childStreamPath);
418
+ }
419
+ }
420
+ );
421
+ this.tasks.set(childStreamPath, task);
422
+ }
423
+ async consume(path, signal) {
424
+ let streamIndex = 0;
425
+ let consecutiveRetries = 0;
426
+ let retryDelayMs = INITIAL_RETRY_DELAY_MS;
427
+ while (!signal.aborted && consecutiveRetries < MAX_CONSECUTIVE_RETRIES) {
428
+ let receivedEvent = false;
429
+ try {
430
+ const response = await this.client.fetch(
431
+ streamPathAt(path, streamIndex),
432
+ {
433
+ cache: "no-store",
434
+ signal
435
+ }
436
+ );
437
+ if (!response.ok || response.body === null) {
438
+ await response.body?.cancel().catch(() => {
439
+ });
440
+ throw new Error(`Child stream returned ${response.status}.`);
441
+ }
442
+ for await (const rawEvent of readNdjsonStream(response.body)) {
443
+ if (signal.aborted) return;
444
+ receivedEvent = true;
445
+ streamIndex += 1;
446
+ const event = parseChildStreamEvent(rawEvent);
447
+ if (event.type === "session.boundary") return;
448
+ if (event.type === "subagent.called") {
449
+ this.beginPath(event.childStreamPath);
450
+ continue;
451
+ }
452
+ if (event.type !== "action.result") continue;
453
+ this.handlers.onToolResult?.(event.result);
454
+ if (event.hasOutput) {
455
+ this.handlers.onActionResult?.(event.result.output);
456
+ }
457
+ }
458
+ } catch {
459
+ if (signal.aborted) return;
460
+ }
461
+ consecutiveRetries = receivedEvent ? 0 : consecutiveRetries + 1;
462
+ retryDelayMs = receivedEvent ? INITIAL_RETRY_DELAY_MS : Math.min(retryDelayMs * 2, MAX_RETRY_DELAY_MS);
463
+ await abortableDelay(retryDelayMs, signal);
464
+ }
465
+ }
466
+ };
467
+
271
468
  // src/runtime/tool-ui.ts
272
469
  var AGENT_TOOL_UI_SCHEMA_VERSION = "webless.tool-ui.v1";
273
470
  var AGENT_STRUCTURED_TOOL_INPUT_SCHEMA_VERSION = "webless.structured-tool-input.v1";
@@ -281,7 +478,7 @@ function formatAgentStructuredToolInput(surface, values) {
281
478
  };
282
479
  return `${AGENT_STRUCTURED_TOOL_INPUT_HEADER} ${JSON.stringify(payload)}`;
283
480
  }
284
- function isRecord2(value) {
481
+ function isRecord3(value) {
285
482
  return value !== null && typeof value === "object" && !Array.isArray(value);
286
483
  }
287
484
  function isJsonValue(value, seen = /* @__PURE__ */ new Set()) {
@@ -330,7 +527,7 @@ function isFieldKind(value) {
330
527
  ].includes(value);
331
528
  }
332
529
  function parseField(value) {
333
- if (!isRecord2(value)) return null;
530
+ if (!isRecord3(value)) return null;
334
531
  if (!hasOnlyKeys(value, [
335
532
  "description",
336
533
  "kind",
@@ -381,7 +578,7 @@ function parseField(value) {
381
578
  if (!Array.isArray(value.options) || value.options.length > 100)
382
579
  return null;
383
580
  for (const option of value.options) {
384
- if (!isRecord2(option) || !boundedString(option.label, 160) || !isJsonValue(option.value)) {
581
+ if (!isRecord3(option) || !boundedString(option.label, 160) || !isJsonValue(option.value)) {
385
582
  return null;
386
583
  }
387
584
  }
@@ -404,7 +601,7 @@ function parseField(value) {
404
601
  };
405
602
  }
406
603
  function parseStep(value) {
407
- if (!isRecord2(value)) return null;
604
+ if (!isRecord3(value)) return null;
408
605
  if (!hasOnlyKeys(value, ["description", "fieldPaths", "id", "label"])) {
409
606
  return null;
410
607
  }
@@ -424,7 +621,7 @@ function parseStep(value) {
424
621
  };
425
622
  }
426
623
  function parseAction(value) {
427
- if (!isRecord2(value)) return null;
624
+ if (!isRecord3(value)) return null;
428
625
  if (!hasOnlyKeys(value, ["id", "label"])) return null;
429
626
  const label = boundedString(value.label, 80);
430
627
  if (value.id !== "submit" && value.id !== "back" && value.id !== "next" && value.id !== "reset" || !label) {
@@ -436,7 +633,7 @@ function parseAction(value) {
436
633
  };
437
634
  }
438
635
  function parseAgentToolUiSurface(value) {
439
- if (!isRecord2(value) || value.schemaVersion !== AGENT_TOOL_UI_SCHEMA_VERSION)
636
+ if (!isRecord3(value) || value.schemaVersion !== AGENT_TOOL_UI_SCHEMA_VERSION)
440
637
  return null;
441
638
  if (!hasOnlyKeys(value, [
442
639
  "actions",
@@ -474,7 +671,7 @@ function parseAgentToolUiSurface(value) {
474
671
  if (value.operationId !== void 0 && !operationId) return null;
475
672
  if (value.requestId !== void 0 && !requestId) return null;
476
673
  if (value.submitLabel !== void 0 && !submitLabel) return null;
477
- const values = value.values !== void 0 && isRecord2(value.values) ? value.values : void 0;
674
+ const values = value.values !== void 0 && isRecord3(value.values) ? value.values : void 0;
478
675
  if (value.values !== void 0) {
479
676
  if (!values || !Object.values(values).every((item) => isJsonValue(item)))
480
677
  return null;
@@ -511,8 +708,7 @@ function applyMessageEvent(event, rendered, handlers, workItems) {
511
708
  if (event.type === "message.completed") {
512
709
  handlers.onComplete?.();
513
710
  }
514
- if (event.type === "action.result") emitActionResult(event, handlers);
515
- if (event.type === "input.requested") emitInputRequests(event, handlers);
711
+ emitVisitorInteractionEvent(event, handlers);
516
712
  if (event.type !== "message.appended") return rendered;
517
713
  const { messageDelta, messageSoFar } = event.data;
518
714
  let delta = messageDelta;
@@ -562,6 +758,13 @@ function emitInputRequests(event, handlers) {
562
758
  })
563
759
  );
564
760
  }
761
+ function emitVisitorInteractionEvent(event, handlers) {
762
+ if (event.type === "action.result") emitActionResult(event, handlers);
763
+ if (event.type === "input.requested") emitInputRequests(event, handlers);
764
+ if (event.type === "subagent.event") {
765
+ emitVisitorInteractionEvent(event.data.event, handlers);
766
+ }
767
+ }
565
768
  function isResumeTurnMessage(received, candidate) {
566
769
  if (received === candidate) return true;
567
770
  return Boolean(candidate) && received.endsWith(`
@@ -734,11 +937,14 @@ var AgentSession = class {
734
937
  clientHost;
735
938
  session;
736
939
  activeResponse;
940
+ childStreams;
737
941
  capability;
738
942
  getActiveSessionId() {
739
943
  return this.session?.state.sessionId ?? loadPersistedAgentSession(this.visitorSessionId, this.storeOptions)?.sessionId;
740
944
  }
741
945
  reset() {
946
+ this.childStreams?.abortAll();
947
+ this.childStreams = void 0;
742
948
  if (this.activeResponse) {
743
949
  void this.activeResponse.cancel().catch(() => {
744
950
  });
@@ -834,6 +1040,12 @@ var AgentSession = class {
834
1040
  this.persistSessionCursor(session);
835
1041
  }
836
1042
  this.activeResponse = response;
1043
+ const childStreams = new SubagentChildStreamCoordinator(
1044
+ client,
1045
+ handlers,
1046
+ signal
1047
+ );
1048
+ this.childStreams = childStreams;
837
1049
  let streamIndex = session?.state.streamIndex ?? 0;
838
1050
  let rendered = "";
839
1051
  const workItems = /* @__PURE__ */ new Map();
@@ -841,6 +1053,7 @@ var AgentSession = class {
841
1053
  try {
842
1054
  for await (const event of response) {
843
1055
  if (signal.aborted) break;
1056
+ if (event.type === "subagent.called") childStreams.begin(event);
844
1057
  if (event.type === "input.requested") requestedInput = true;
845
1058
  rendered = applyMessageEvent(event, rendered, handlers, workItems);
846
1059
  streamIndex += 1;
@@ -853,8 +1066,11 @@ var AgentSession = class {
853
1066
  );
854
1067
  }
855
1068
  }
1069
+ await childStreams.waitForAll();
856
1070
  } finally {
1071
+ childStreams.abortAll();
857
1072
  this.activeResponse = void 0;
1073
+ if (this.childStreams === childStreams) this.childStreams = void 0;
858
1074
  if (session) {
859
1075
  this.persistSessionCursor(session);
860
1076
  }
@@ -890,64 +1106,80 @@ var AgentSession = class {
890
1106
  }
891
1107
  let rendered = renderTurn(turnEvents);
892
1108
  const workItems = /* @__PURE__ */ new Map();
893
- for (const event of turnEvents) {
894
- applyWorkEvent(event, handlers, workItems);
895
- if (event.type === "input.requested") emitInputRequests(event, handlers);
896
- if (event.type === "action.result") emitActionResult(event, handlers);
897
- }
898
- if (rendered.startsWith(initialText)) {
899
- const missedText = rendered.slice(initialText.length);
900
- if (missedText) handlers.onDelta(missedText);
901
- } else if (initialText.startsWith(rendered)) {
902
- rendered = initialText;
903
- } else if (!initialText.startsWith(rendered)) {
904
- rendered = initialText + rendered;
905
- }
906
- let session = client.sessions.attach(snapshot.session.sessionId, {
907
- streamIndex: snapshot.session.streamIndex
908
- });
909
- this.session = session;
910
- this.persistSessionCursor(session);
911
- let snapshotBoundary;
912
- for (let index = turnEvents.length - 1; index >= 0; index -= 1) {
913
- const event = turnEvents[index];
914
- if (event && isTurnBoundary(event)) {
915
- snapshotBoundary = event;
916
- break;
1109
+ const childStreams = new SubagentChildStreamCoordinator(
1110
+ client,
1111
+ handlers,
1112
+ signal
1113
+ );
1114
+ this.childStreams = childStreams;
1115
+ try {
1116
+ for (const event of turnEvents) {
1117
+ if (event.type === "subagent.called") childStreams.begin(event);
1118
+ applyWorkEvent(event, handlers, workItems);
1119
+ emitVisitorInteractionEvent(event, handlers);
917
1120
  }
918
- }
919
- if (snapshotBoundary) {
920
- if (snapshotBoundary.type === "session.failed") {
921
- throw new Error(
922
- snapshotBoundary.data.message || snapshotBoundary.data.code
1121
+ if (rendered.startsWith(initialText)) {
1122
+ const missedText = rendered.slice(initialText.length);
1123
+ if (missedText) handlers.onDelta(missedText);
1124
+ } else if (initialText.startsWith(rendered)) {
1125
+ rendered = initialText;
1126
+ } else if (!initialText.startsWith(rendered)) {
1127
+ rendered = initialText + rendered;
1128
+ }
1129
+ let session = client.sessions.attach(snapshot.session.sessionId, {
1130
+ streamIndex: snapshot.session.streamIndex
1131
+ });
1132
+ this.session = session;
1133
+ this.persistSessionCursor(session);
1134
+ let snapshotBoundary;
1135
+ for (let index = turnEvents.length - 1; index >= 0; index -= 1) {
1136
+ const event = turnEvents[index];
1137
+ if (event && isTurnBoundary(event)) {
1138
+ snapshotBoundary = event;
1139
+ break;
1140
+ }
1141
+ }
1142
+ if (snapshotBoundary) {
1143
+ if (snapshotBoundary.type === "session.failed") {
1144
+ throw new Error(
1145
+ snapshotBoundary.data.message || snapshotBoundary.data.code
1146
+ );
1147
+ }
1148
+ handlers.onComplete?.();
1149
+ await childStreams.waitForAll();
1150
+ if (!rendered.trim() && !hasInputRequest) {
1151
+ throw new Error("Empty response from runtime");
1152
+ }
1153
+ return rendered.trim();
1154
+ }
1155
+ let streamIndex = snapshot.session.streamIndex;
1156
+ for await (const event of session.stream({ signal })) {
1157
+ if (signal.aborted) break;
1158
+ if (event.type === "subagent.called") childStreams.begin(event);
1159
+ rendered = applyMessageEvent(event, rendered, handlers, workItems);
1160
+ streamIndex += 1;
1161
+ savePersistedAgentSession(
1162
+ this.visitorSessionId,
1163
+ session.state.sessionId,
1164
+ streamIndex,
1165
+ this.storeOptions
923
1166
  );
1167
+ if (isTurnBoundary(event)) break;
924
1168
  }
925
- handlers.onComplete?.();
926
- if (!rendered.trim() && !hasInputRequest) {
1169
+ await childStreams.waitForAll();
1170
+ session = client.sessions.attach(session.state.sessionId, {
1171
+ streamIndex
1172
+ });
1173
+ this.session = session;
1174
+ this.persistSessionCursor(session);
1175
+ if (!rendered.trim() && !signal.aborted && !hasInputRequest) {
927
1176
  throw new Error("Empty response from runtime");
928
1177
  }
929
1178
  return rendered.trim();
1179
+ } finally {
1180
+ childStreams.abortAll();
1181
+ if (this.childStreams === childStreams) this.childStreams = void 0;
930
1182
  }
931
- let streamIndex = snapshot.session.streamIndex;
932
- for await (const event of session.stream({ signal })) {
933
- if (signal.aborted) break;
934
- rendered = applyMessageEvent(event, rendered, handlers, workItems);
935
- streamIndex += 1;
936
- savePersistedAgentSession(
937
- this.visitorSessionId,
938
- session.state.sessionId,
939
- streamIndex,
940
- this.storeOptions
941
- );
942
- if (isTurnBoundary(event)) break;
943
- }
944
- session = client.sessions.attach(session.state.sessionId, { streamIndex });
945
- this.session = session;
946
- this.persistSessionCursor(session);
947
- if (!rendered.trim() && !signal.aborted && !hasInputRequest) {
948
- throw new Error("Empty response from runtime");
949
- }
950
- return rendered.trim();
951
1183
  }
952
1184
  async respondTurn(responses, signal, handlers) {
953
1185
  const client = this.ensureClient();
@@ -968,6 +1200,12 @@ var AgentSession = class {
968
1200
  () => session.respond(inputResponses, { signal })
969
1201
  );
970
1202
  this.activeResponse = response;
1203
+ const childStreams = new SubagentChildStreamCoordinator(
1204
+ client,
1205
+ handlers,
1206
+ signal
1207
+ );
1208
+ this.childStreams = childStreams;
971
1209
  let streamIndex = session.state.streamIndex;
972
1210
  let rendered = "";
973
1211
  let requestedInput = false;
@@ -975,6 +1213,7 @@ var AgentSession = class {
975
1213
  try {
976
1214
  for await (const event of response) {
977
1215
  if (signal.aborted) break;
1216
+ if (event.type === "subagent.called") childStreams.begin(event);
978
1217
  if (event.type === "input.requested") requestedInput = true;
979
1218
  rendered = applyMessageEvent(event, rendered, handlers, workItems);
980
1219
  streamIndex += 1;
@@ -985,8 +1224,11 @@ var AgentSession = class {
985
1224
  this.storeOptions
986
1225
  );
987
1226
  }
1227
+ await childStreams.waitForAll();
988
1228
  } finally {
1229
+ childStreams.abortAll();
989
1230
  this.activeResponse = void 0;
1231
+ if (this.childStreams === childStreams) this.childStreams = void 0;
990
1232
  this.session = client.sessions.attach(session.state.sessionId, {
991
1233
  streamIndex
992
1234
  });
@@ -998,6 +1240,7 @@ var AgentSession = class {
998
1240
  return rendered.trim();
999
1241
  }
1000
1242
  cancelActive() {
1243
+ this.childStreams?.abortAll();
1001
1244
  if (this.activeResponse) {
1002
1245
  this.activeResponse.cancel().catch(() => {
1003
1246
  });
@@ -1113,7 +1356,7 @@ var ENVELOPE_KEYS = /* @__PURE__ */ new Set([
1113
1356
  "presentationKinds",
1114
1357
  "ui"
1115
1358
  ]);
1116
- function isRecord3(value) {
1359
+ function isRecord4(value) {
1117
1360
  return value !== null && typeof value === "object" && !Array.isArray(value);
1118
1361
  }
1119
1362
  function isJsonValue2(value, seen = /* @__PURE__ */ new Set()) {
@@ -1140,7 +1383,7 @@ function decodeEnvelope(value) {
1140
1383
  }
1141
1384
  function parseAgentToolResultEnvelope(value) {
1142
1385
  const decoded = decodeEnvelope(value);
1143
- if (!isRecord3(decoded)) return null;
1386
+ if (!isRecord4(decoded)) return null;
1144
1387
  const keys = Object.keys(decoded);
1145
1388
  if (keys.some((key) => !ENVELOPE_KEYS.has(key)) || decoded.schemaVersion !== "webless.tool-result.v1" || !isJsonValue2(decoded.output) || !Array.isArray(decoded.presentationKinds) || decoded.presentationKinds.length < 1 || decoded.presentationKinds.length > 16) {
1146
1389
  return null;