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.
- package/dist/api-client/index.d.ts +49 -8
- package/dist/api-client/index.d.ts.map +1 -1
- package/dist/api-client/index.js +417 -158
- package/dist/api-client/index.js.map +1 -1
- package/dist/api-client/kb-view-client.d.ts +28 -1
- package/dist/api-client/kb-view-client.d.ts.map +1 -1
- package/dist/api-client/kb-view-client.js +255 -8
- package/dist/api-client/kb-view-client.js.map +1 -1
- package/dist/commands/import-agent.d.ts.map +1 -1
- package/dist/commands/import-agent.js +30 -6
- package/dist/commands/import-agent.js.map +1 -1
- package/dist/commands/legacy/push-records-mode.d.ts.map +1 -1
- package/dist/commands/legacy/push-records-mode.js +46 -2
- package/dist/commands/legacy/push-records-mode.js.map +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +29 -0
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/pull-agent.d.ts.map +1 -1
- package/dist/commands/pull-agent.js +6 -2
- package/dist/commands/pull-agent.js.map +1 -1
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +58 -4
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/util/agent-mode.d.ts.map +1 -1
- package/dist/commands/util/agent-mode.js +69 -11
- package/dist/commands/util/agent-mode.js.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +6 -1
- package/dist/commands/validate.js.map +1 -1
- package/dist/config/index.d.ts +56 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/index.js +100 -4
- package/dist/index.js.map +1 -1
- package/dist/services/auth.service.d.ts.map +1 -1
- package/dist/services/auth.service.js +21 -0
- package/dist/services/auth.service.js.map +1 -1
- package/dist/telemetry.d.ts +130 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +808 -2
- package/dist/telemetry.js.map +1 -1
- package/package.json +1 -1
package/dist/api-client/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
358
|
-
|
|
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 =
|
|
418
|
+
errorBody = JSON.parse(responseText);
|
|
363
419
|
}
|
|
364
420
|
catch {
|
|
365
|
-
errorBody =
|
|
421
|
+
errorBody = responseText || null;
|
|
366
422
|
}
|
|
367
423
|
throw new AUIAPIError(resp.status, `agents: ${resp.statusText}`, errorBody);
|
|
368
424
|
}
|
|
369
|
-
|
|
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
|
-
//
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
//
|
|
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
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
475
|
-
agentId,
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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.
|
|
592
|
+
const { resp, responseText } = await this.preflightFetch({
|
|
593
|
+
name: "get_version",
|
|
594
|
+
label: "get version",
|
|
595
|
+
url,
|
|
539
596
|
method: "GET",
|
|
540
|
-
|
|
541
|
-
|
|
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 =
|
|
622
|
+
errorBody = JSON.parse(responseText);
|
|
546
623
|
}
|
|
547
624
|
catch {
|
|
548
|
-
errorBody =
|
|
625
|
+
errorBody = responseText || null;
|
|
549
626
|
}
|
|
550
627
|
throw new AUIAPIError(resp.status, `get version: ${resp.statusText}`, errorBody);
|
|
551
628
|
}
|
|
552
|
-
|
|
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 =
|
|
983
|
+
errorBody = JSON.parse(responseText);
|
|
876
984
|
}
|
|
877
985
|
catch {
|
|
878
|
-
errorBody =
|
|
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 = (
|
|
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
|
|
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
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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 =
|
|
1081
|
+
errorBody = JSON.parse(responseText);
|
|
957
1082
|
}
|
|
958
1083
|
catch {
|
|
959
|
-
errorBody =
|
|
1084
|
+
errorBody = responseText;
|
|
960
1085
|
}
|
|
961
1086
|
throw new AUIAPIError(resp.status, `pull version blobs: ${resp.statusText}`, errorBody);
|
|
962
1087
|
}
|
|
963
|
-
return
|
|
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
|
|
1176
|
-
* (typically `aui.import` / `aui.pull` / `aui.push`).
|
|
1177
|
-
*
|
|
1178
|
-
*
|
|
1179
|
-
*
|
|
1180
|
-
*
|
|
1181
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
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:
|
|
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 (
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
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
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
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 =
|
|
1783
|
+
errorBody = JSON.parse(responseText);
|
|
1514
1784
|
}
|
|
1515
1785
|
catch {
|
|
1516
|
-
errorBody =
|
|
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 =
|
|
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;
|