aui-agent-builder 0.3.109 → 0.3.111

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/api-client/index.d.ts +49 -8
  2. package/dist/api-client/index.d.ts.map +1 -1
  3. package/dist/api-client/index.js +417 -158
  4. package/dist/api-client/index.js.map +1 -1
  5. package/dist/api-client/kb-view-client.d.ts +28 -1
  6. package/dist/api-client/kb-view-client.d.ts.map +1 -1
  7. package/dist/api-client/kb-view-client.js +255 -8
  8. package/dist/api-client/kb-view-client.js.map +1 -1
  9. package/dist/commands/import-agent.d.ts.map +1 -1
  10. package/dist/commands/import-agent.js +30 -6
  11. package/dist/commands/import-agent.js.map +1 -1
  12. package/dist/commands/legacy/push-records-mode.d.ts.map +1 -1
  13. package/dist/commands/legacy/push-records-mode.js +46 -2
  14. package/dist/commands/legacy/push-records-mode.js.map +1 -1
  15. package/dist/commands/login.d.ts.map +1 -1
  16. package/dist/commands/login.js +29 -0
  17. package/dist/commands/login.js.map +1 -1
  18. package/dist/commands/pull-agent.d.ts.map +1 -1
  19. package/dist/commands/pull-agent.js +6 -2
  20. package/dist/commands/pull-agent.js.map +1 -1
  21. package/dist/commands/push.d.ts.map +1 -1
  22. package/dist/commands/push.js +58 -4
  23. package/dist/commands/push.js.map +1 -1
  24. package/dist/commands/util/agent-mode.d.ts.map +1 -1
  25. package/dist/commands/util/agent-mode.js +69 -11
  26. package/dist/commands/util/agent-mode.js.map +1 -1
  27. package/dist/commands/validate.d.ts.map +1 -1
  28. package/dist/commands/validate.js +6 -1
  29. package/dist/commands/validate.js.map +1 -1
  30. package/dist/config/index.d.ts +56 -0
  31. package/dist/config/index.d.ts.map +1 -1
  32. package/dist/config/index.js.map +1 -1
  33. package/dist/index.js +100 -4
  34. package/dist/index.js.map +1 -1
  35. package/dist/services/auth.service.d.ts.map +1 -1
  36. package/dist/services/auth.service.js +21 -0
  37. package/dist/services/auth.service.js.map +1 -1
  38. package/dist/telemetry.d.ts +130 -1
  39. package/dist/telemetry.d.ts.map +1 -1
  40. package/dist/telemetry.js +808 -2
  41. package/dist/telemetry.js.map +1 -1
  42. package/package.json +1 -1
@@ -352,21 +352,82 @@ export class AUIClient {
352
352
  if (filters?.network_category_id)
353
353
  qs.set("network_category_id", filters.network_category_id);
354
354
  const url = `${getAgentManagementBaseUrl()}/v1/agents?${qs}`;
355
- const resp = await this.agentManagementFetch(url, {
355
+ // `list_agents` is the fallback resolver used by `aui pull` /
356
+ // `aui import-agent` when `.auirc` has no `agent_management_id`
357
+ // (legacy projects pre-PR #54). Full preflight telemetry —
358
+ // curl repro + truncated response — so Logfire shows which
359
+ // sibling agents the resolver saw and which one it picked.
360
+ // Mirrors the contract used by `get_agent` and `get_version`.
361
+ const { resp, responseText } = await this.preflightFetch({
362
+ name: "list_agents",
363
+ label: "list agents",
364
+ url,
356
365
  method: "GET",
357
- headers: this.agentManagementHeaders(),
358
- }, "list agents");
366
+ extraAttrs: {
367
+ organization_id: orgId,
368
+ page,
369
+ size,
370
+ ...(filters?.network_id
371
+ ? { filter_network_id: filters.network_id }
372
+ : {}),
373
+ ...(filters?.network_category_id
374
+ ? { filter_network_category_id: filters.network_category_id }
375
+ : {}),
376
+ ...(filters?.scope_type ? { filter_scope_type: filters.scope_type } : {}),
377
+ ...(filters?.kind ? { filter_kind: filters.kind } : {}),
378
+ },
379
+ extractAttrs: (parsed) => {
380
+ const out = {};
381
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
382
+ const obj = parsed;
383
+ if (Array.isArray(obj.items))
384
+ out.items_count = obj.items.length;
385
+ if (typeof obj.total === "number")
386
+ out.total = obj.total;
387
+ if (typeof obj.pages === "number")
388
+ out.pages = obj.pages;
389
+ // Capture the bundle_mode breakdown for the page — useful
390
+ // when the resolver picks the "wrong" sibling on a
391
+ // bundle/records mixed account.
392
+ if (Array.isArray(obj.items)) {
393
+ let bundleTrue = 0;
394
+ let bundleFalse = 0;
395
+ let bundleUnset = 0;
396
+ for (const it of obj.items) {
397
+ const bm = it && typeof it === "object"
398
+ ? it.bundle_mode
399
+ : undefined;
400
+ if (bm === true)
401
+ bundleTrue++;
402
+ else if (bm === false)
403
+ bundleFalse++;
404
+ else
405
+ bundleUnset++;
406
+ }
407
+ out.items_bundle_mode_true = bundleTrue;
408
+ out.items_bundle_mode_false = bundleFalse;
409
+ out.items_bundle_mode_unset = bundleUnset;
410
+ }
411
+ }
412
+ return out;
413
+ },
414
+ });
359
415
  if (!resp.ok) {
360
416
  let errorBody;
361
417
  try {
362
- errorBody = await resp.json();
418
+ errorBody = JSON.parse(responseText);
363
419
  }
364
420
  catch {
365
- errorBody = await resp.text();
421
+ errorBody = responseText || null;
366
422
  }
367
423
  throw new AUIAPIError(resp.status, `agents: ${resp.statusText}`, errorBody);
368
424
  }
369
- return (await resp.json());
425
+ try {
426
+ return JSON.parse(responseText);
427
+ }
428
+ catch {
429
+ throw new AUIAPIError(resp.status, `agents: invalid JSON response`, responseText);
430
+ }
370
431
  },
371
432
  listVersions: async (agentId, page = 1, size = 50, filters) => {
372
433
  const qs = new URLSearchParams({
@@ -424,59 +485,44 @@ export class AUIClient {
424
485
  // "import keeps failing on this agent", the FIRST question is
425
486
  // "what did the get-agent response say (especially bundle_mode)?".
426
487
  //
427
- // To answer that straight from Logfire — without re-running the
428
- // CLI with `AUI_DEBUG=1` we attach an event on the active span
429
- // (`aui.import` / `aui.pull` / `aui.push`) carrying the full
430
- // curl repro (auth redacted), HTTP method/url/status, duration,
431
- // and the response body. Using `addEvent` (vs `setAttribute`)
432
- // preserves multiple per-command calls: push's
433
- // `lookupAgentManagementInfoForPush` + `resolveVersionDraft` can
434
- // call `getAgent` more than once, and we want each call visible
435
- // as its own log entry under the parent span.
488
+ // Telemetry is wired through `preflightFetch` (shared with
489
+ // `getVersion` and `listAgents`), which attaches a span event
490
+ // with curl repro, status, duration, and response body. Using
491
+ // `addEvent` (vs `setAttribute`) preserves multiple per-command
492
+ // calls: push's `lookupAgentManagementInfoForPush` +
493
+ // `resolveVersionDraft` can call `getAgent` more than once, and
494
+ // we want each call visible as its own log entry under the
495
+ // parent span.
436
496
  const url = `${getAgentManagementBaseUrl()}/v1/agents/${agentId}`;
437
- const method = "GET";
438
- const headers = this.agentManagementHeaders();
439
- const startedAt = Date.now();
440
- let resp;
441
- try {
442
- resp = await this.agentManagementFetch(url, { method, headers }, "get agent");
443
- }
444
- catch (err) {
445
- // Network-level failure (DNS, ECONNREFUSED, timeout, abort, ...).
446
- // Attach the request-side telemetry before re-throwing so the
447
- // span tells the full story even when there's no HTTP response.
448
- const networkErr = err instanceof Error ? err : new Error(String(err));
449
- this.attachGetAgentTelemetry({
450
- method,
451
- url,
452
- headers,
453
- agentId,
454
- responseText: networkErr.message,
455
- status: 0,
456
- success: false,
457
- durationMs: Date.now() - startedAt,
458
- });
459
- throw networkErr;
460
- }
461
- // Read the body ONCE — Response bodies are single-shot and we
462
- // need the same string for telemetry, error envelopes, and the
463
- // success JSON parse below. Mirrors the validate-call path.
464
- let responseText;
465
- try {
466
- responseText = await resp.text();
467
- }
468
- catch {
469
- responseText = "";
470
- }
471
- this.attachGetAgentTelemetry({
472
- method,
497
+ const { resp, responseText } = await this.preflightFetch({
498
+ name: "get_agent",
499
+ label: "get agent",
473
500
  url,
474
- headers,
475
- agentId,
476
- responseText,
477
- status: resp.status,
478
- success: resp.ok,
479
- durationMs: Date.now() - startedAt,
501
+ method: "GET",
502
+ extraAttrs: { agent_id: agentId },
503
+ extractAttrs: (parsed, spanAttrs) => {
504
+ const out = {};
505
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
506
+ const obj = parsed;
507
+ if (typeof obj.bundle_mode === "boolean") {
508
+ out.bundle_mode = obj.bundle_mode;
509
+ // Also pin the most-recent observed mode at the span level
510
+ // so it's filterable without expanding the event list.
511
+ // Multiple calls per command overwrite — the last call
512
+ // wins, which is the one whose decision actually drove
513
+ // the dispatch.
514
+ spanAttrs["agent.bundle_mode"] = obj.bundle_mode;
515
+ }
516
+ if (typeof obj.name === "string")
517
+ out.agent_name = obj.name;
518
+ if (typeof obj.active_version_id === "string") {
519
+ out.active_version_id = obj.active_version_id;
520
+ }
521
+ if (typeof obj.kind === "string")
522
+ out.agent_kind = obj.kind;
523
+ }
524
+ return out;
525
+ },
480
526
  });
481
527
  if (!resp.ok) {
482
528
  let errorBody;
@@ -534,22 +580,58 @@ export class AUIClient {
534
580
  return (await resp.json());
535
581
  },
536
582
  getVersion: async (agentId, versionId) => {
583
+ // `GET /v1/agents/{id}/versions/{id}` is the second preflight read
584
+ // every push/pull issues — it resolves the draft target (and
585
+ // returns the version's `status`, `version_number`, etc.). When
586
+ // a user reports "push failed with 422 not-a-draft" or "pull
587
+ // returned 404", the diff between WHAT the CLI saw here and WHAT
588
+ // it then sent on /push or /pull is the smoking gun. Full
589
+ // preflight telemetry — curl repro + truncated response — keeps
590
+ // both pieces of evidence in one Logfire row.
537
591
  const url = `${getAgentManagementBaseUrl()}/v1/agents/${agentId}/versions/${versionId}`;
538
- const resp = await this.agentManagementFetch(url, {
592
+ const { resp, responseText } = await this.preflightFetch({
593
+ name: "get_version",
594
+ label: "get version",
595
+ url,
539
596
  method: "GET",
540
- headers: this.agentManagementHeaders(),
541
- }, "get version");
597
+ extraAttrs: { agent_id: agentId, version_id: versionId },
598
+ extractAttrs: (parsed) => {
599
+ const out = {};
600
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
601
+ const obj = parsed;
602
+ if (typeof obj.status === "string")
603
+ out.status = obj.status;
604
+ if (typeof obj.version_number === "number") {
605
+ out.version_number = obj.version_number;
606
+ }
607
+ if (typeof obj.version_revision_number === "number") {
608
+ out.version_revision_number = obj.version_revision_number;
609
+ }
610
+ if (typeof obj.label === "string")
611
+ out.label = obj.label;
612
+ if (typeof obj.is_active === "boolean") {
613
+ out.is_active = obj.is_active;
614
+ }
615
+ }
616
+ return out;
617
+ },
618
+ });
542
619
  if (!resp.ok) {
543
620
  let errorBody;
544
621
  try {
545
- errorBody = await resp.json();
622
+ errorBody = JSON.parse(responseText);
546
623
  }
547
624
  catch {
548
- errorBody = await resp.text();
625
+ errorBody = responseText || null;
549
626
  }
550
627
  throw new AUIAPIError(resp.status, `get version: ${resp.statusText}`, errorBody);
551
628
  }
552
- return (await resp.json());
629
+ try {
630
+ return JSON.parse(responseText);
631
+ }
632
+ catch {
633
+ throw new AUIAPIError(resp.status, `get version: invalid JSON response`, responseText);
634
+ }
553
635
  },
554
636
  updateVersion: async (agentId, versionId, data) => {
555
637
  const url = `${getAgentManagementBaseUrl()}/v1/agents/${agentId}/versions/${versionId}`;
@@ -854,28 +936,54 @@ export class AUIClient {
854
936
  }
855
937
  if (pushTimer)
856
938
  clearTimeout(pushTimer);
939
+ // Read the body ONCE — used for telemetry, the error envelope,
940
+ // and the success parse. (Web fetch bodies are single-shot.)
941
+ const responseText = await resp.text();
942
+ // Summary body for both captureRequest and the span curl. We do
943
+ // NOT inline the full bundle (it can be megabytes) — the
944
+ // reproducible signal for a push is the endpoint + the RESPONSE
945
+ // (new_version_tag on success, or the 422 BundleValidationResult
946
+ // on failure), both of which we capture in full (truncated 32KB).
947
+ const pushSummaryBody = {
948
+ caller: reqBody.caller,
949
+ commit_message: reqBody.commit_message,
950
+ pushed_by: reqBody.pushed_by,
951
+ bundle_size_bytes: serialized.length,
952
+ };
857
953
  captureRequest({
858
954
  timestamp: new Date().toISOString(),
859
955
  method: "POST",
860
956
  url,
861
957
  headers,
862
- body: {
863
- caller: reqBody.caller,
864
- commit_message: reqBody.commit_message,
865
- pushed_by: reqBody.pushed_by,
866
- bundle_size_bytes: serialized.length,
867
- },
958
+ body: pushSummaryBody,
868
959
  status: resp.status,
960
+ responseBody: responseText,
869
961
  label: "push version blobs",
870
962
  success: resp.ok,
871
963
  });
964
+ // Attach the replayable curl + response to the active span
965
+ // (`aui.push.task.bundle`) for BOTH success and failure, so the
966
+ // bundle-mode push endpoint is fully visible in Logfire — the
967
+ // counterpart to the records-mode per-entity write curls.
968
+ this.attachHttpCurlTelemetry({
969
+ name: "push_bundle",
970
+ label: "push version blobs",
971
+ method: "POST",
972
+ url,
973
+ headers,
974
+ requestBody: pushSummaryBody,
975
+ responseText,
976
+ status: resp.status,
977
+ success: resp.ok,
978
+ durationMs: Date.now() - pushStart,
979
+ });
872
980
  if (!resp.ok) {
873
981
  let errorBody;
874
982
  try {
875
- errorBody = await resp.json();
983
+ errorBody = JSON.parse(responseText);
876
984
  }
877
985
  catch {
878
- errorBody = await resp.text();
986
+ errorBody = responseText;
879
987
  }
880
988
  if (process.env.AUI_DEBUG) {
881
989
  console.log(`[debug] push version blobs failed: ${resp.status} ${JSON.stringify(errorBody)}`);
@@ -908,7 +1016,7 @@ export class AUIClient {
908
1016
  }
909
1017
  let responseData;
910
1018
  try {
911
- responseData = (await resp.json());
1019
+ responseData = (responseText ? JSON.parse(responseText) : undefined);
912
1020
  }
913
1021
  catch {
914
1022
  responseData = undefined;
@@ -942,25 +1050,42 @@ export class AUIClient {
942
1050
  if (process.env.AUI_DEBUG) {
943
1051
  console.log(`[debug] GET ${url}`);
944
1052
  }
945
- const resp = await this.agentManagementFetch(url, {
1053
+ const pullHeaders = {
1054
+ Accept: "application/json",
1055
+ Authorization: `Bearer ${this.authToken}`,
1056
+ "X-Organization-ID": this.organizationId,
1057
+ };
1058
+ const pullStart = Date.now();
1059
+ const resp = await this.agentManagementFetch(url, { method: "GET", headers: pullHeaders }, "pull version blobs");
1060
+ // Read the body ONCE — used for telemetry, the error envelope,
1061
+ // and the success parse.
1062
+ const responseText = await resp.text();
1063
+ // Attach the replayable curl + response to the active span
1064
+ // (`aui.pull` / `aui.import`) for the bundle-mode `/pull` read —
1065
+ // the counterpart to the records-mode `/view` read curls, so the
1066
+ // import endpoint is fully visible in Logfire for both modes.
1067
+ this.attachHttpCurlTelemetry({
1068
+ name: "pull_bundle",
1069
+ label: "pull version blobs",
946
1070
  method: "GET",
947
- headers: {
948
- Accept: "application/json",
949
- Authorization: `Bearer ${this.authToken}`,
950
- "X-Organization-ID": this.organizationId,
951
- },
952
- }, "pull version blobs");
1071
+ url,
1072
+ headers: pullHeaders,
1073
+ responseText,
1074
+ status: resp.status,
1075
+ success: resp.ok,
1076
+ durationMs: Date.now() - pullStart,
1077
+ });
953
1078
  if (!resp.ok) {
954
1079
  let errorBody;
955
1080
  try {
956
- errorBody = await resp.json();
1081
+ errorBody = JSON.parse(responseText);
957
1082
  }
958
1083
  catch {
959
- errorBody = await resp.text();
1084
+ errorBody = responseText;
960
1085
  }
961
1086
  throw new AUIAPIError(resp.status, `pull version blobs: ${resp.statusText}`, errorBody);
962
1087
  }
963
- return (await resp.json());
1088
+ return JSON.parse(responseText);
964
1089
  },
965
1090
  /**
966
1091
  * @deprecated Multipart-files upload was removed on 2026-05-20. The
@@ -1172,17 +1297,26 @@ export class AUIClient {
1172
1297
  }
1173
1298
  }
1174
1299
  /**
1175
- * Attach `GET /v1/agents/{id}` telemetry to the currently-active span
1176
- * (typically `aui.import` / `aui.pull` / `aui.push`). The CLI calls
1177
- * this endpoint before every import/pull/push to read `bundle_mode`
1178
- * and decide which surface to use so making every call self-
1179
- * describing in Logfire (curl repro, status, response body, duration)
1180
- * answers "did the CLI even know the right mode?" without re-running
1181
- * locally. See the comment block in `getAgent` for the rationale.
1300
+ * Attach preflight HTTP telemetry to the currently-active span
1301
+ * (typically `aui.import` / `aui.pull` / `aui.push`).
1302
+ *
1303
+ * The CLI fires up to three preflight reads BEFORE issuing the
1304
+ * actual push/pull body:
1305
+ * 1. `GET /v1/agents/{id}` → reads `bundle_mode`
1306
+ * 2. `GET /v1/agents/{id}/versions/{id}` resolves the draft/version
1307
+ * 3. `GET /v1/agents?network_id=…` → legacy-project fallback
1308
+ * when `.auirc` has no `agent_management_id`.
1309
+ *
1310
+ * All three are essential for triaging "push hit the wrong endpoint"
1311
+ * / "pull pulled the wrong version" reports. Each one is emitted as
1312
+ * its own span event with the full curl repro (auth redacted),
1313
+ * status, duration, response body (truncated), and a handful of
1314
+ * extracted highlights pulled by `extractAttrs` (e.g. `bundle_mode`,
1315
+ * `active_version_id`, version `status`, item counts).
1182
1316
  *
1183
1317
  * Best-effort throughout: telemetry must NEVER break the call path.
1184
1318
  */
1185
- attachGetAgentTelemetry(input) {
1319
+ attachPreflightTelemetry(input) {
1186
1320
  try {
1187
1321
  const span = trace.getActiveSpan();
1188
1322
  if (!span)
@@ -1192,42 +1326,21 @@ export class AUIClient {
1192
1326
  ? input.responseText.slice(0, MAX) +
1193
1327
  `... [truncated ${input.responseText.length - MAX} bytes]`
1194
1328
  : input.responseText;
1195
- // Best-effort: surface `bundle_mode` as its own event attribute
1196
- // so dashboards can filter "show me every push where the agent
1197
- // reported bundle_mode=false" without grepping response bodies.
1198
- let bundleMode;
1199
- let agentName;
1200
- let activeVersionId;
1201
- try {
1202
- const parsed = JSON.parse(input.responseText);
1203
- if (typeof parsed.bundle_mode === "boolean") {
1204
- bundleMode = parsed.bundle_mode;
1205
- }
1206
- if (typeof parsed.name === "string")
1207
- agentName = parsed.name;
1208
- if (typeof parsed.active_version_id === "string") {
1209
- activeVersionId = parsed.active_version_id;
1210
- }
1211
- }
1212
- catch {
1213
- // Response wasn't JSON (network failure stringified error,
1214
- // 5xx HTML page, etc.) — fall through with bundleMode unset.
1215
- }
1216
1329
  const eventAttrs = {
1217
1330
  "http.method": input.method,
1218
1331
  "http.url": input.url,
1219
- "get_agent.agent_id": input.agentId,
1220
- "get_agent.duration_ms": input.durationMs,
1221
- "get_agent.success": input.success,
1222
- "get_agent.response_body": truncatedResponse,
1332
+ [`${input.name}.duration_ms`]: input.durationMs,
1333
+ [`${input.name}.success`]: input.success,
1334
+ [`${input.name}.response_body`]: truncatedResponse,
1223
1335
  "aui.curl_repro": formatAsCurlSafe({
1224
1336
  timestamp: new Date().toISOString(),
1225
1337
  method: input.method,
1226
1338
  url: input.url,
1227
1339
  headers: input.headers,
1340
+ body: input.requestBody,
1228
1341
  status: input.status,
1229
1342
  responseBody: input.responseText,
1230
- label: "get agent",
1343
+ label: input.label,
1231
1344
  success: input.success,
1232
1345
  }, { maxBodyBytes: MAX }),
1233
1346
  };
@@ -1238,20 +1351,157 @@ export class AUIClient {
1238
1351
  if (input.status > 0) {
1239
1352
  eventAttrs["http.status_code"] = input.status;
1240
1353
  }
1241
- if (typeof bundleMode === "boolean") {
1242
- eventAttrs["get_agent.bundle_mode"] = bundleMode;
1243
- // Also pin the most-recent observed mode at the span level so
1244
- // it's filterable without expanding the event list. Multiple
1245
- // calls per command overwrite — the last call wins, which is
1246
- // the one whose decision actually drove the dispatch.
1247
- span.setAttribute("agent.bundle_mode", bundleMode);
1354
+ if (input.extraAttrs) {
1355
+ for (const [k, v] of Object.entries(input.extraAttrs)) {
1356
+ eventAttrs[`${input.name}.${k}`] = v;
1357
+ }
1248
1358
  }
1249
- if (agentName)
1250
- eventAttrs["get_agent.agent_name"] = agentName;
1251
- if (activeVersionId) {
1252
- eventAttrs["get_agent.active_version_id"] = activeVersionId;
1359
+ // Parse the response body once for the per-endpoint highlight
1360
+ // extractor. The extractor can also push attributes onto the
1361
+ // parent span (`spanAttrs`) for things that are stable across
1362
+ // every call (e.g. `agent.bundle_mode`).
1363
+ if (input.extractAttrs) {
1364
+ const spanAttrs = {};
1365
+ try {
1366
+ const parsed = JSON.parse(input.responseText);
1367
+ const extracted = input.extractAttrs(parsed, spanAttrs) || {};
1368
+ for (const [k, v] of Object.entries(extracted)) {
1369
+ eventAttrs[`${input.name}.${k}`] = v;
1370
+ }
1371
+ }
1372
+ catch {
1373
+ // Response wasn't JSON (network failure stringified error,
1374
+ // 5xx HTML page, etc.) — fall through without highlights.
1375
+ }
1376
+ for (const [k, v] of Object.entries(spanAttrs)) {
1377
+ span.setAttribute(k, v);
1378
+ }
1379
+ }
1380
+ span.addEvent(input.name, eventAttrs);
1381
+ }
1382
+ catch {
1383
+ // Telemetry must never break the call path.
1384
+ }
1385
+ }
1386
+ /**
1387
+ * Run a preflight `agentManagementFetch` and attach full
1388
+ * curl+request+response telemetry to the active span. Use for
1389
+ * `GET` reads that drive the push/pull dispatch (getAgent,
1390
+ * getVersion, listAgents). Returns the response text so callers
1391
+ * still parse the body themselves — keeps the existing single-
1392
+ * read-once invariant from `getAgent` (response bodies are
1393
+ * single-shot).
1394
+ */
1395
+ async preflightFetch(input) {
1396
+ const headers = this.agentManagementHeaders();
1397
+ const startedAt = Date.now();
1398
+ let resp;
1399
+ try {
1400
+ resp = await this.agentManagementFetch(input.url, { method: input.method, headers }, input.label);
1401
+ }
1402
+ catch (err) {
1403
+ const networkErr = err instanceof Error ? err : new Error(String(err));
1404
+ this.attachPreflightTelemetry({
1405
+ name: input.name,
1406
+ label: input.label,
1407
+ method: input.method,
1408
+ url: input.url,
1409
+ headers,
1410
+ responseText: networkErr.message,
1411
+ status: 0,
1412
+ success: false,
1413
+ durationMs: Date.now() - startedAt,
1414
+ extraAttrs: input.extraAttrs,
1415
+ extractAttrs: input.extractAttrs,
1416
+ });
1417
+ throw networkErr;
1418
+ }
1419
+ let responseText;
1420
+ try {
1421
+ responseText = await resp.text();
1422
+ }
1423
+ catch {
1424
+ responseText = "";
1425
+ }
1426
+ this.attachPreflightTelemetry({
1427
+ name: input.name,
1428
+ label: input.label,
1429
+ method: input.method,
1430
+ url: input.url,
1431
+ headers,
1432
+ responseText,
1433
+ status: resp.status,
1434
+ success: resp.ok,
1435
+ durationMs: Date.now() - startedAt,
1436
+ extraAttrs: input.extraAttrs,
1437
+ extractAttrs: input.extractAttrs,
1438
+ });
1439
+ return { resp, responseText };
1440
+ }
1441
+ /**
1442
+ * Attach a redacted curl + request/response telemetry to the active
1443
+ * span for the core import/push endpoints (the `/pull` & `/push`
1444
+ * bundle routes and the records-mode `/view` reads & per-entity
1445
+ * writes). These already land in `aui curl` (via `captureRequest`)
1446
+ * and `.aui/push-logs/`, but were NOT visible in Logfire — so a
1447
+ * failed bundle push or a wrong-version pull showed only a span
1448
+ * with a bare error string. This makes the exact replayable curl +
1449
+ * (truncated) response body part of the trace, for BOTH success and
1450
+ * failure, in both records and bundle mode.
1451
+ *
1452
+ * Emits a `http.<name>` event (preserves history when several calls
1453
+ * share one span, e.g. the ~6 `/view` reads under a single
1454
+ * `aui.import`) AND mirrors the curl + HTTP fields onto the span
1455
+ * itself (so a dedicated per-call task span like
1456
+ * `aui.push.task.bundle` / `aui.push.task.<entity>` shows the curl
1457
+ * directly in its attributes). For multi-call spans the span-level
1458
+ * attrs are last-call-wins; the per-call events retain every curl.
1459
+ *
1460
+ * Best-effort throughout: telemetry must NEVER break the call path.
1461
+ */
1462
+ attachHttpCurlTelemetry(input) {
1463
+ try {
1464
+ const span = trace.getActiveSpan();
1465
+ if (!span)
1466
+ return;
1467
+ const MAX = 32 * 1024;
1468
+ const truncatedResponse = input.responseText.length > MAX
1469
+ ? input.responseText.slice(0, MAX) +
1470
+ `... [truncated ${input.responseText.length - MAX} bytes]`
1471
+ : input.responseText;
1472
+ const curl = formatAsCurlSafe({
1473
+ timestamp: new Date().toISOString(),
1474
+ method: input.method,
1475
+ url: input.url,
1476
+ headers: input.headers,
1477
+ body: input.requestBody,
1478
+ status: input.status,
1479
+ responseBody: input.responseText,
1480
+ label: input.label,
1481
+ success: input.success,
1482
+ }, { maxBodyBytes: MAX });
1483
+ const eventAttrs = {
1484
+ "http.method": input.method,
1485
+ "http.url": input.url,
1486
+ [`${input.name}.success`]: input.success,
1487
+ [`${input.name}.response_body`]: truncatedResponse,
1488
+ "aui.curl_repro": curl,
1489
+ };
1490
+ if (input.status > 0)
1491
+ eventAttrs["http.status_code"] = input.status;
1492
+ if (typeof input.durationMs === "number") {
1493
+ eventAttrs[`${input.name}.duration_ms`] = input.durationMs;
1494
+ }
1495
+ span.addEvent(`http.${input.name}`, eventAttrs);
1496
+ // Mirror onto the span for direct visibility on dedicated
1497
+ // per-call spans (push bundle / per-entity write task spans).
1498
+ span.setAttribute("aui.curl_repro", curl);
1499
+ span.setAttribute("http.method", input.method);
1500
+ span.setAttribute("http.url", input.url);
1501
+ span.setAttribute("http.response_body", truncatedResponse);
1502
+ if (input.status > 0) {
1503
+ span.setAttribute("http.status_code", input.status);
1253
1504
  }
1254
- span.addEvent("get_agent", eventAttrs);
1255
1505
  }
1256
1506
  catch {
1257
1507
  // Telemetry must never break the call path.
@@ -1500,20 +1750,40 @@ export class AUIClient {
1500
1750
  console.log(`[debug] GET ${url}`);
1501
1751
  }
1502
1752
  const hdrs = this.agentSettingsReadHeaders();
1753
+ const startedAt = Date.now();
1503
1754
  const resp = await fetch(url, { method: "GET", headers: hdrs });
1755
+ // Read the body ONCE — used for telemetry, the error envelope, and
1756
+ // the success parse. node-fetch bodies are single-shot.
1757
+ const responseText = await resp.text();
1504
1758
  if (resp.status === 401 && !_isRetry) {
1505
1759
  const refreshed = await this.attemptTokenRefresh();
1506
1760
  if (refreshed) {
1507
1761
  return this.fetchV2(path, params, true);
1508
1762
  }
1509
1763
  }
1764
+ // Attach the replayable curl + response to the active span for the
1765
+ // records-mode `/view` reads `aui import` / `aui pull` issue (the
1766
+ // ~6 per-entity reads run under one `aui.import` / `aui.pull`
1767
+ // span). Success + failure, so the import endpoints are fully
1768
+ // visible in Logfire — not just `aui curl`.
1769
+ this.attachHttpCurlTelemetry({
1770
+ name: "import_view",
1771
+ label: `GET ${path}`,
1772
+ method: "GET",
1773
+ url,
1774
+ headers: hdrs,
1775
+ responseText,
1776
+ status: resp.status,
1777
+ success: resp.ok,
1778
+ durationMs: Date.now() - startedAt,
1779
+ });
1510
1780
  if (!resp.ok) {
1511
1781
  let errorBody;
1512
1782
  try {
1513
- errorBody = await resp.json();
1783
+ errorBody = JSON.parse(responseText);
1514
1784
  }
1515
1785
  catch {
1516
- errorBody = await resp.text();
1786
+ errorBody = responseText;
1517
1787
  }
1518
1788
  if (process.env.AUI_DEBUG) {
1519
1789
  console.log(`[debug] ${path} → ${resp.status}: ${JSON.stringify(errorBody)}`);
@@ -1532,7 +1802,7 @@ export class AUIClient {
1532
1802
  err.statusCode = resp.status;
1533
1803
  throw err;
1534
1804
  }
1535
- const data = await resp.json();
1805
+ const data = responseText ? JSON.parse(responseText) : {};
1536
1806
  captureRequest({
1537
1807
  timestamp: new Date().toISOString(),
1538
1808
  method: "GET",
@@ -1677,6 +1947,22 @@ export class AUIClient {
1677
1947
  label: label || `${method} ${path}`,
1678
1948
  success: resp.ok,
1679
1949
  });
1950
+ // Attach the replayable curl + response to the active span for
1951
+ // BOTH success and failure (records-mode per-entity writes run
1952
+ // inside their own `aui.push.task.<entity>` span). Previously only
1953
+ // failures got a curl here; the owner asked for curl on every
1954
+ // pushed endpoint so triage never depends on `.aui/push-logs/`.
1955
+ this.attachHttpCurlTelemetry({
1956
+ name: "entity_write",
1957
+ label: label || `${method} ${path}`,
1958
+ method,
1959
+ url,
1960
+ headers,
1961
+ requestBody: body,
1962
+ responseText,
1963
+ status: resp.status,
1964
+ success: resp.ok,
1965
+ });
1680
1966
  if (this.pushLogDir) {
1681
1967
  this.writePushLog(method, url, headers, body, resp.status, responseText, label);
1682
1968
  }
@@ -1690,33 +1976,6 @@ export class AUIClient {
1690
1976
  if (process.env.AUI_DEBUG) {
1691
1977
  console.log(`[debug] ${method} ${path} → ${resp.status}: ${responseText}`);
1692
1978
  }
1693
- // Attach replayable context to the active span so triage from
1694
- // Logfire doesn't require fishing in `.aui/push-logs/*.txt` or
1695
- // `~/.aui/last-requests.json`. Failure path only — adding curl
1696
- // to every successful write would multiply telemetry egress and
1697
- // surface bodies for traffic that doesn't need triage.
1698
- try {
1699
- const activeSpan = trace.getActiveSpan();
1700
- if (activeSpan) {
1701
- activeSpan.setAttribute("http.method", method);
1702
- activeSpan.setAttribute("http.url", url);
1703
- activeSpan.setAttribute("http.status_code", resp.status);
1704
- activeSpan.setAttribute("aui.curl_repro", formatAsCurlSafe({
1705
- timestamp: new Date().toISOString(),
1706
- method,
1707
- url,
1708
- headers,
1709
- body,
1710
- status: resp.status,
1711
- responseBody: responseText,
1712
- label,
1713
- success: false,
1714
- }, { maxBodyBytes: 32_768 }));
1715
- }
1716
- }
1717
- catch {
1718
- // Telemetry must never break the call path.
1719
- }
1720
1979
  const error = new Error(`${label} failed: ${resp.status} ${responseText}`);
1721
1980
  error.statusCode = resp.status;
1722
1981
  throw error;