agentfootprint-lens 0.33.1 → 0.35.0

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/README.md CHANGED
@@ -358,6 +358,151 @@ The headless half is on `agentfootprint-lens/core` — `defaultSelection`,
358
358
 
359
359
  ---
360
360
 
361
+ ## Render an artifact by its ref
362
+
363
+ agentfootprint 9.21–9.23 taught tools to check large results into an **artifact
364
+ store** and hand the model a ~30-token claim ticket (`art_…`) instead of the
365
+ bytes; the model finishes with `present({ ref, as, label })`. The lens is the
366
+ screen's half of that handshake: it **redeems the ref and renders a component
367
+ the app registered for the artifact's kind** — ids + props, never markup the
368
+ model wrote. The model can say *what* to show; only your registry says *how*.
369
+
370
+ ```tsx
371
+ import {
372
+ ArtifactPane,
373
+ httpArtifactResolver,
374
+ presentedFromEvents,
375
+ registerArtifactComponent,
376
+ } from 'agentfootprint-lens';
377
+
378
+ // 1. Teach Lens your kinds, once at startup. ('dataset/rows' already ships.)
379
+ registerArtifactComponent({
380
+ kind: 'chart/spec',
381
+ component: ({ meta, data }) => <MyBarChart title={meta.label} spec={data} />,
382
+ });
383
+
384
+ // 2. Point a resolver at the served agent — the SAME invoke path the
385
+ // conversation posts to. Session identity rides every redemption.
386
+ const resolver = httpArtifactResolver({ url: '/invoke', sessionId });
387
+
388
+ // 3. Render every artifact the run presented. On history reload, walk the
389
+ // stored transcript instead: readPresentedResult(toolMessage.content).
390
+ {presentedFromEvents(recording.events).map((call) => (
391
+ <ArtifactPane key={call.toolCallId ?? call.ref} presented={call} resolver={resolver} />
392
+ ))}
393
+ ```
394
+
395
+ `<ArtifactPane>` heads the ref first (the render decision), then gets the
396
+ payload and mounts the registered component. A kind nothing is registered for
397
+ falls back to an honest **metadata card** — ticket facts, a bounded payload
398
+ preview, copy/download — plus one line naming the `registerArtifactComponent`
399
+ call that closes the gap. Never a blank pane.
400
+
401
+ ### When the artifact is gone
402
+
403
+ Refs expire — TTL, retention budgets, a restarted store. The `present` result
404
+ carries a **description snapshot** at speak time, so a reloaded conversation
405
+ can still say what stood there, from history alone:
406
+
407
+ > Chart — "Q3 sales by region" (bar-chart, 41.0 KB) — expired; re-run to regenerate.
408
+
409
+ That placeholder needs **no store round-trip beyond the failed head** — this is
410
+ the spreadsheet's `#REF!`, taught manners. Missing, expired and
411
+ another-session's ref all answer one indistinguishable not-found by design (a
412
+ leaked ref probes nothing), and the pane renders the same stated absence for
413
+ all three. A resolution *door* failure (no store attached, wire down) is a
414
+ different sentence: the pane shows the server's own teaching refusal, verbatim.
415
+
416
+ ### The two resolvers
417
+
418
+ ```ts
419
+ import { httpArtifactResolver, storeArtifactResolver } from 'agentfootprint-lens/core';
420
+
421
+ // Over a served agent's wire ops (standingAgent / httpHost / nodeHost, or the
422
+ // managed-runtime dialect): POST { op: 'artifact-head' | 'artifact-get', ref,
423
+ // sessionId } to the invoke path. `userId`, extra `headers` and a custom
424
+ // `fetch` are options.
425
+ const overHttp = httpArtifactResolver({ url: 'https://host/invoke', sessionId: 's-1' });
426
+
427
+ // Over a directly passed store, for same-process demos and tests. The scope is
428
+ // REQUIRED — refs only resolve under the identity they were minted in.
429
+ const inProcess = storeArtifactResolver({ store, scope: { conversationId } });
430
+ ```
431
+
432
+ Both return `{ status: 'live' | 'absent' | 'failed' }` outcomes instead of
433
+ throwing, so a pane can branch. In the EventStream, the whole artifact
434
+ lifecycle — minted, resolved, expired, refused, presented — already reads as
435
+ prose via the default humanizer.
436
+
437
+ ---
438
+
439
+ ## Answer a paused run with a real component (typed HITL)
440
+
441
+ agentfootprint 9.24 taught every human-ask door (`askHuman`, the `ask`
442
+ middleware, `defineTool({ checkInComponent })`) to carry an optional typed
443
+ half beside the prose question: `{ componentId, props?, propsRef? }`. The id
444
+ names a component **your screen registered** — ids + props, never markup the
445
+ model wrote, the same no-eval law artifacts follow. Small props ride the ask
446
+ inline; the big half (a 200-option picker's options) rides the **artifact
447
+ store** as a `propsRef` claim ticket, redeemed through the same resolver and
448
+ the same session identity as every other artifact.
449
+
450
+ `<AwaitingPane>` is the screen's half:
451
+
452
+ ```tsx
453
+ import {
454
+ AwaitingPane,
455
+ decisionRequestBody,
456
+ httpArtifactResolver,
457
+ registerDecisionComponent,
458
+ } from 'agentfootprint-lens';
459
+
460
+ // 1. Teach Lens your collectors, once at startup. ('option-picker' ships.)
461
+ registerDecisionComponent({
462
+ componentId: 'refund-form',
463
+ component: ({ question, props, data, respond }) => (
464
+ <MyRefundForm limits={props} rows={data} onSubmit={(answer) => respond(answer)} />
465
+ ),
466
+ });
467
+
468
+ // 2. When a reply comes back awaiting, render the pane. The lens does NOT
469
+ // own the POST — your app does; decisionRequestBody formats the body.
470
+ const resolver = httpArtifactResolver({ url: '/invoke', sessionId });
471
+ <AwaitingPane
472
+ awaiting={reply.awaiting}
473
+ resolver={resolver}
474
+ onDecision={(decision) =>
475
+ fetch('/invoke', {
476
+ method: 'POST',
477
+ headers: { 'content-type': 'application/json' },
478
+ body: JSON.stringify(decisionRequestBody({ decision, sessionId })),
479
+ })
480
+ }
481
+ />
482
+ ```
483
+
484
+ The person clicks; `onDecision` receives **the structured decision** — the
485
+ option's id, or the approve/decline record (`approveDecision` /
486
+ `declineDecision`, byte-compatible with agentfootprint's check-in
487
+ vocabulary). After answering, the pane renders the decision as one sentence —
488
+ *"Approved by alice@ops — 'verified'."* — which is **display only**: the
489
+ structured decision is the record; the words are a rendering of it.
490
+
491
+ Nothing on this road dead-ends the human. An unknown `componentId` falls back
492
+ to the prose question plus a plain answer box and *says so*; an expired
493
+ `propsRef` renders the honest placeholder plus the answer box; a crashed
494
+ registered component is caught and stated; a consent gate (`checkIn` /
495
+ middleware `ask`) falls back to Approve/Decline with a required "deciding as"
496
+ field, because an audit record with no actor names nobody.
497
+
498
+ The headless half is on `agentfootprint-lens/core` — `readAwaitingComponent`
499
+ (era-robust: reads `awaiting.component` and the three `pauseData` homes),
500
+ `approveDecision` / `declineDecision`, `decisionRequestBody`,
501
+ `decisionSentence`, `isConsentAsk` — so a Vue or CLI shell can build the same
502
+ pane.
503
+
504
+ ---
505
+
361
506
  ## Theming
362
507
 
363
508
  **Lens inherits theme tokens from your app via CSS variables.** Set `--fp-*`
@@ -522,6 +667,36 @@ file through your own endpoint. Needs agentfootprint 9.9+; on anything older it
522
667
  renders a version hint instead of itself. See
523
668
  [Report a bug with this run](#report-a-bug-with-this-run).
524
669
 
670
+ ### `<ArtifactPane>` — render by ref, with `#REF!` taught manners
671
+
672
+ Takes one `present` call (`presented`) and an `ArtifactResolver` (`resolver`);
673
+ heads the ref, then renders the component registered for the artifact's kind
674
+ (`registerArtifactComponent({ kind, component })`) with `{ meta, data,
675
+ presented }` as props. `'dataset/rows'` ships as a built-in table; every
676
+ unregistered kind falls back to `<ArtifactMetaCard>` with the gap stated. An
677
+ expired/missing ref renders its placeholder from the speak-time snapshot alone.
678
+ The headless half — `httpArtifactResolver`, `storeArtifactResolver`,
679
+ `presentedFromEvents`, `readPresentedResult`, `artifactPlaceholder` — is on
680
+ `agentfootprint-lens/core`. See
681
+ [Render an artifact by its ref](#render-an-artifact-by-its-ref).
682
+
683
+ ### `<AwaitingPane>` — answer a paused run with a real component
684
+
685
+ Takes the `awaiting` payload a served agent replied with (`awaiting`), an
686
+ optional `ArtifactResolver` (`resolver`, only needed when the ask ships a
687
+ `propsRef`), and `onDecision` — which receives the person's STRUCTURED
688
+ decision; your app posts it (`decisionRequestBody` formats the wire body).
689
+ Renders the component registered for the ask's `componentId`
690
+ (`registerDecisionComponent({ componentId, component })`) with `{ question,
691
+ props, data, respond }`; `'option-picker'` ships as a built-in. Unknown id,
692
+ expired ref, failed door and crashed component all state themselves and fall
693
+ back to a live answer surface — never a dead end. After answering it renders
694
+ the decision as one sentence (display only; the structured decision is the
695
+ record). The headless half — `readAwaitingComponent`, `approveDecision` /
696
+ `declineDecision`, `decisionRequestBody`, `decisionSentence`, `isConsentAsk`
697
+ — is on `agentfootprint-lens/core`. See
698
+ [Answer a paused run with a real component](#answer-a-paused-run-with-a-real-component-typed-hitl).
699
+
525
700
  ### Headless core
526
701
 
527
702
  `agentfootprint-lens/core` is React-free: `LensRecorder`, `ChangeNotifier`,
@@ -1693,6 +1693,89 @@ function humanizeCursorMovePick(move) {
1693
1693
  return line;
1694
1694
  }
1695
1695
 
1696
+ // src/core/bugReport/selection.ts
1697
+ var DEFAULT_MAX_BYTES = 24 * 1024 * 1024;
1698
+ function formatBytes(bytes) {
1699
+ if (!Number.isFinite(bytes) || bytes < 0) return "0 bytes";
1700
+ if (bytes < 1024) return `${Math.round(bytes)} bytes`;
1701
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1702
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1703
+ }
1704
+ function conversationUnits(manifest) {
1705
+ return manifest.units.filter((unit) => unit.kind === "conversation");
1706
+ }
1707
+ function fileUnits(manifest) {
1708
+ return manifest.units.filter((unit) => unit.kind !== "conversation");
1709
+ }
1710
+ function defaultSelection(manifest, recent = 3) {
1711
+ const conversations = conversationUnits(manifest);
1712
+ const keep = recent <= 0 ? [] : conversations.slice(Math.max(0, conversations.length - recent));
1713
+ return [...keep.map((unit) => unit.id), ...fileUnits(manifest).map((unit) => unit.id)];
1714
+ }
1715
+ function measureSelection(manifest, selected, limitBytes = DEFAULT_MAX_BYTES) {
1716
+ const ticked = new Set(selected);
1717
+ const units = manifest.units.filter((unit) => ticked.has(unit.id));
1718
+ const bytes = units.reduce((sum, unit) => sum + (unit.bytes || 0), 0);
1719
+ const over = bytes > limitBytes;
1720
+ const hint = over ? trimHintFor(manifest, ticked, limitBytes) : void 0;
1721
+ return {
1722
+ bytes,
1723
+ limitBytes,
1724
+ over,
1725
+ label: `${formatBytes(bytes)} of ${formatBytes(limitBytes)}`,
1726
+ conversations: units.filter((unit) => unit.kind === "conversation").length,
1727
+ ...hint !== void 0 && { hint }
1728
+ };
1729
+ }
1730
+ function trimHintFor(manifest, selected, limitBytes = DEFAULT_MAX_BYTES) {
1731
+ const ticked = new Set(selected);
1732
+ const droppable = manifest.units.filter((unit) => ticked.has(unit.id) && unit.kind === "conversation").sort((left, right) => right.bytes - left.bytes);
1733
+ let remaining = manifest.units.filter((unit) => ticked.has(unit.id)).reduce((sum, unit) => sum + (unit.bytes || 0), 0);
1734
+ if (remaining <= limitBytes) return void 0;
1735
+ const named = [];
1736
+ for (const unit of droppable) {
1737
+ if (named.length >= droppable.length - 1) break;
1738
+ remaining -= unit.bytes || 0;
1739
+ named.push(`${unit.id} (${formatBytes(unit.bytes || 0)})`);
1740
+ if (remaining <= limitBytes) break;
1741
+ }
1742
+ if (remaining > limitBytes) {
1743
+ return `Even with one conversation left this is over ${formatBytes(limitBytes)}. Untick a derived file, record a shorter reproduction, or file the issue and attach the zip by hand.`;
1744
+ }
1745
+ return `Untick ${joinWithAnd(named)} to fit.`;
1746
+ }
1747
+ function joinWithAnd(parts) {
1748
+ if (parts.length <= 1) return parts[0] ?? "";
1749
+ return `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
1750
+ }
1751
+
1752
+ // src/core/humanizeArtifacts.ts
1753
+ var ticket = (ref, kind, bytes) => `${ref ?? "art_?"} [${kind ?? "?"} \xB7 ${formatBytes(bytes ?? 0)}]`;
1754
+ function humanizeArtifactMinted(p) {
1755
+ const label = p.label !== void 0 ? ` "${p.label}"` : "";
1756
+ const by = p.tool !== void 0 ? ` by tool "${p.tool}"` : "";
1757
+ return `Artifact minted${by} \u2014${label} ${ticket(p.ref, p.kind, p.bytes)}. The model routes this ticket, not the bytes.`;
1758
+ }
1759
+ function humanizeArtifactResolved(p) {
1760
+ const actor = p.tool !== void 0 ? `Tool "${p.tool}"` : "The screen";
1761
+ const verb = p.via === "head" ? "described" : p.via === "get" ? "fetched" : `redeemed (${p.via ?? "?"})`;
1762
+ return `${actor} ${verb} artifact ${ticket(p.ref, p.kind, p.bytes)}.`;
1763
+ }
1764
+ function humanizeArtifactExpired(p) {
1765
+ return `Artifact ${ticket(p.ref, p.kind, p.bytes)} swept (${p.reason ?? "retention"}) \u2014 its ref no longer resolves, and says so.`;
1766
+ }
1767
+ function humanizeArtifactRefused(p) {
1768
+ const ref = p.ref !== void 0 ? ` for '${p.ref}'` : "";
1769
+ const by = p.tool !== void 0 ? ` (tool "${p.tool}")` : "";
1770
+ const detail = p.detail !== void 0 ? ` \u2014 ${p.detail}` : "";
1771
+ return `Artifact ${p.op ?? "?"}${ref} refused: ${p.reason ?? "unknown reason"}${by}${detail}`;
1772
+ }
1773
+ function humanizeArtifactPresented(p) {
1774
+ const s = p.snapshot;
1775
+ const label = s?.label !== void 0 ? ` "${s.label}"` : "";
1776
+ return `Presented${label} to the screen as '${p.as ?? "?"}' \u2014 ${ticket(p.ref, s?.kind, s?.bytes)}. The screen redeems the ref under its own session.`;
1777
+ }
1778
+
1696
1779
  // src/core/humanizer.ts
1697
1780
  var defaultHumanizer = (event) => {
1698
1781
  if (event.type === "agentfootprint.context.evaluated") {
@@ -1705,6 +1788,18 @@ var defaultHumanizer = (event) => {
1705
1788
  if (event.type === "agentfootprint.skill.route_conflict") {
1706
1789
  return humanizeRouteConflict(event.payload);
1707
1790
  }
1791
+ switch (event.type) {
1792
+ case "agentfootprint.artifacts.minted":
1793
+ return humanizeArtifactMinted(event.payload);
1794
+ case "agentfootprint.artifacts.resolved":
1795
+ return humanizeArtifactResolved(event.payload);
1796
+ case "agentfootprint.artifacts.expired":
1797
+ return humanizeArtifactExpired(event.payload);
1798
+ case "agentfootprint.artifacts.refused":
1799
+ return humanizeArtifactRefused(event.payload);
1800
+ case "agentfootprint.artifacts.presented":
1801
+ return humanizeArtifactPresented(event.payload);
1802
+ }
1708
1803
  switch (event.type) {
1709
1804
  // Composition
1710
1805
  case "agentfootprint.composition.enter":
@@ -2974,62 +3069,6 @@ function layoutLensGraph(output, options = {}) {
2974
3069
  };
2975
3070
  }
2976
3071
 
2977
- // src/core/bugReport/selection.ts
2978
- var DEFAULT_MAX_BYTES = 24 * 1024 * 1024;
2979
- function formatBytes(bytes) {
2980
- if (!Number.isFinite(bytes) || bytes < 0) return "0 bytes";
2981
- if (bytes < 1024) return `${Math.round(bytes)} bytes`;
2982
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
2983
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2984
- }
2985
- function conversationUnits(manifest) {
2986
- return manifest.units.filter((unit) => unit.kind === "conversation");
2987
- }
2988
- function fileUnits(manifest) {
2989
- return manifest.units.filter((unit) => unit.kind !== "conversation");
2990
- }
2991
- function defaultSelection(manifest, recent = 3) {
2992
- const conversations = conversationUnits(manifest);
2993
- const keep = recent <= 0 ? [] : conversations.slice(Math.max(0, conversations.length - recent));
2994
- return [...keep.map((unit) => unit.id), ...fileUnits(manifest).map((unit) => unit.id)];
2995
- }
2996
- function measureSelection(manifest, selected, limitBytes = DEFAULT_MAX_BYTES) {
2997
- const ticked = new Set(selected);
2998
- const units = manifest.units.filter((unit) => ticked.has(unit.id));
2999
- const bytes = units.reduce((sum, unit) => sum + (unit.bytes || 0), 0);
3000
- const over = bytes > limitBytes;
3001
- const hint = over ? trimHintFor(manifest, ticked, limitBytes) : void 0;
3002
- return {
3003
- bytes,
3004
- limitBytes,
3005
- over,
3006
- label: `${formatBytes(bytes)} of ${formatBytes(limitBytes)}`,
3007
- conversations: units.filter((unit) => unit.kind === "conversation").length,
3008
- ...hint !== void 0 && { hint }
3009
- };
3010
- }
3011
- function trimHintFor(manifest, selected, limitBytes = DEFAULT_MAX_BYTES) {
3012
- const ticked = new Set(selected);
3013
- const droppable = manifest.units.filter((unit) => ticked.has(unit.id) && unit.kind === "conversation").sort((left, right) => right.bytes - left.bytes);
3014
- let remaining = manifest.units.filter((unit) => ticked.has(unit.id)).reduce((sum, unit) => sum + (unit.bytes || 0), 0);
3015
- if (remaining <= limitBytes) return void 0;
3016
- const named = [];
3017
- for (const unit of droppable) {
3018
- if (named.length >= droppable.length - 1) break;
3019
- remaining -= unit.bytes || 0;
3020
- named.push(`${unit.id} (${formatBytes(unit.bytes || 0)})`);
3021
- if (remaining <= limitBytes) break;
3022
- }
3023
- if (remaining > limitBytes) {
3024
- return `Even with one conversation left this is over ${formatBytes(limitBytes)}. Untick a derived file, record a shorter reproduction, or file the issue and attach the zip by hand.`;
3025
- }
3026
- return `Untick ${joinWithAnd(named)} to fit.`;
3027
- }
3028
- function joinWithAnd(parts) {
3029
- if (parts.length <= 1) return parts[0] ?? "";
3030
- return `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
3031
- }
3032
-
3033
3072
  // src/core/bugReport/issueBody.ts
3034
3073
  function buildIssueBody(args) {
3035
3074
  const { fields, manifest, evidence } = args;
@@ -3148,6 +3187,271 @@ function encodeBase64(bytes) {
3148
3187
  return btoa(binary);
3149
3188
  }
3150
3189
 
3190
+ // src/core/artifacts/resolvers.ts
3191
+ var ARTIFACT_HEAD_OP = "artifact-head";
3192
+ var ARTIFACT_GET_OP = "artifact-get";
3193
+ var NOT_FOUND_CODE = "ERR_ARTIFACT_NOT_FOUND";
3194
+ function httpArtifactResolver(options) {
3195
+ if (typeof options?.url !== "string" || options.url.trim().length === 0) {
3196
+ throw new Error(
3197
+ "httpArtifactResolver needs `url` \u2014 the served agent's invoke path, the same one conversation turns POST to (e.g. '/invoke' or 'https://host/invoke')."
3198
+ );
3199
+ }
3200
+ const doFetch = options.fetch ?? globalThis.fetch;
3201
+ if (typeof doFetch !== "function") {
3202
+ throw new Error(
3203
+ "httpArtifactResolver found no fetch: this runtime has no global fetch and none was passed. Pass one via { fetch } (Node < 18 needs a polyfill)."
3204
+ );
3205
+ }
3206
+ const redeem = async (op, ref) => {
3207
+ let response;
3208
+ try {
3209
+ response = await doFetch(options.url, {
3210
+ method: "POST",
3211
+ headers: { "content-type": "application/json", ...options.headers },
3212
+ body: JSON.stringify({
3213
+ op,
3214
+ ref,
3215
+ ...options.sessionId !== void 0 && { sessionId: options.sessionId },
3216
+ ...options.userId !== void 0 && { userId: options.userId }
3217
+ })
3218
+ });
3219
+ } catch (err) {
3220
+ return {
3221
+ status: "failed",
3222
+ message: `Could not reach ${options.url} to redeem '${ref}' (${err instanceof Error ? err.name || "network error" : "network error"}).`
3223
+ };
3224
+ }
3225
+ let body;
3226
+ try {
3227
+ body = await response.json();
3228
+ } catch {
3229
+ return {
3230
+ status: "failed",
3231
+ message: `${options.url} answered ${response.status} with a body that is not JSON \u2014 not an artifact wire reply.`
3232
+ };
3233
+ }
3234
+ const artifact = body?.artifact;
3235
+ if (artifact !== null && typeof artifact === "object") {
3236
+ const parcel = artifact;
3237
+ const meta = parcel.meta;
3238
+ if (meta === null || typeof meta !== "object" || typeof meta.kind !== "string") {
3239
+ return {
3240
+ status: "failed",
3241
+ message: `${options.url} answered with an artifact body carrying no readable meta \u2014 not the published wire shape.`
3242
+ };
3243
+ }
3244
+ return {
3245
+ status: "live",
3246
+ meta,
3247
+ ...op === ARTIFACT_GET_OP && "data" in parcel && { data: parcel.data }
3248
+ };
3249
+ }
3250
+ const code = typeof body?.code === "string" ? body.code : void 0;
3251
+ if (code === NOT_FOUND_CODE) return { status: "absent" };
3252
+ const message = typeof body?.error === "string" ? body.error : `${options.url} answered ${response.status} with no error message.`;
3253
+ return { status: "failed", message, ...code !== void 0 && { code } };
3254
+ };
3255
+ return {
3256
+ head: (ref) => redeem(ARTIFACT_HEAD_OP, ref),
3257
+ get: (ref) => redeem(ARTIFACT_GET_OP, ref)
3258
+ };
3259
+ }
3260
+ function storeArtifactResolver(options) {
3261
+ const store = options?.store;
3262
+ if (store === null || typeof store !== "object" || typeof store.head !== "function" || typeof store.get !== "function") {
3263
+ throw new Error(
3264
+ "storeArtifactResolver needs `store` \u2014 an object with the artifact read verbs `head(scope, ref)` and `get(scope, ref)` (any agentfootprint artifact store qualifies)."
3265
+ );
3266
+ }
3267
+ if (!("scope" in options)) {
3268
+ throw new Error(
3269
+ 'storeArtifactResolver needs `scope` \u2014 the isolation tuple the refs were minted under. A wrong scope resolves to "no data" by design, so it must be stated, never guessed.'
3270
+ );
3271
+ }
3272
+ const failed = (ref, err) => ({
3273
+ status: "failed",
3274
+ message: err instanceof Error && err.message ? err.message : `The store threw while redeeming '${ref}'.`
3275
+ });
3276
+ return {
3277
+ async head(ref) {
3278
+ try {
3279
+ const meta = await store.head(options.scope, ref);
3280
+ return meta === null ? { status: "absent" } : { status: "live", meta };
3281
+ } catch (err) {
3282
+ return failed(ref, err);
3283
+ }
3284
+ },
3285
+ async get(ref) {
3286
+ try {
3287
+ const record = await store.get(options.scope, ref);
3288
+ return record === null ? { status: "absent" } : { status: "live", meta: record.meta, data: record.data };
3289
+ } catch (err) {
3290
+ return failed(ref, err);
3291
+ }
3292
+ }
3293
+ };
3294
+ }
3295
+
3296
+ // src/core/artifacts/presented.ts
3297
+ var PRESENTED_EVENT_TYPE = "agentfootprint.artifacts.presented";
3298
+ function readSnapshot(value) {
3299
+ if (value === null || typeof value !== "object") return void 0;
3300
+ const s = value;
3301
+ if (typeof s.kind !== "string" || typeof s.mediaType !== "string" || typeof s.bytes !== "number")
3302
+ return void 0;
3303
+ return {
3304
+ kind: s.kind,
3305
+ mediaType: s.mediaType,
3306
+ bytes: s.bytes,
3307
+ ...typeof s.label === "string" && { label: s.label }
3308
+ };
3309
+ }
3310
+ function readPresentedShape(value) {
3311
+ if (typeof value.ref !== "string" || typeof value.as !== "string") return void 0;
3312
+ const snapshot = readSnapshot(value.snapshot);
3313
+ if (!snapshot) return void 0;
3314
+ return { ref: value.ref, as: value.as, snapshot };
3315
+ }
3316
+ function readPresentedResult(value) {
3317
+ let parsed = value;
3318
+ if (typeof value === "string") {
3319
+ try {
3320
+ parsed = JSON.parse(value);
3321
+ } catch {
3322
+ return void 0;
3323
+ }
3324
+ }
3325
+ if (parsed === null || typeof parsed !== "object") return void 0;
3326
+ const record = parsed;
3327
+ if (record.presented !== true) return void 0;
3328
+ const trunk = readPresentedShape(record);
3329
+ if (!trunk) return void 0;
3330
+ return {
3331
+ ...trunk,
3332
+ ...typeof record.toolCallId === "string" && { toolCallId: record.toolCallId }
3333
+ };
3334
+ }
3335
+ function presentedFromEvents(events) {
3336
+ if (!events) return [];
3337
+ const found = [];
3338
+ for (const item of events) {
3339
+ if (item === null || typeof item !== "object") continue;
3340
+ const wrapper = item;
3341
+ const event = typeof wrapper.type === "string" ? wrapper : wrapper.event !== null && typeof wrapper.event === "object" ? wrapper.event : void 0;
3342
+ if (!event || event.type !== PRESENTED_EVENT_TYPE) continue;
3343
+ const payload = event.payload;
3344
+ if (payload === null || typeof payload !== "object") continue;
3345
+ const record = payload;
3346
+ const trunk = readPresentedShape(record);
3347
+ if (!trunk) continue;
3348
+ found.push({
3349
+ ...trunk,
3350
+ ...typeof record.toolCallId === "string" && { toolCallId: record.toolCallId },
3351
+ ...typeof record.iteration === "number" && { iteration: record.iteration }
3352
+ });
3353
+ }
3354
+ return found;
3355
+ }
3356
+ function kindNoun(kind) {
3357
+ const head = kind.split("/")[0]?.trim() ?? "";
3358
+ if (head.length === 0) return "Artifact";
3359
+ return head[0].toUpperCase() + head.slice(1);
3360
+ }
3361
+ function describePresented(presented) {
3362
+ const { snapshot } = presented;
3363
+ const name = snapshot.label !== void 0 ? `"${snapshot.label}"` : presented.ref;
3364
+ return `${kindNoun(snapshot.kind)} \u2014 ${name} (${presented.as}, ${formatBytes(snapshot.bytes)})`;
3365
+ }
3366
+ function artifactPlaceholder(presented) {
3367
+ return `${describePresented(presented)} \u2014 expired; re-run to regenerate.`;
3368
+ }
3369
+
3370
+ // src/core/hitl/decision.ts
3371
+ function readAwaitingComponent(awaiting) {
3372
+ if (typeof awaiting !== "object" || awaiting === null) return void 0;
3373
+ const pending = awaiting;
3374
+ const direct = asComponent(pending.component);
3375
+ if (direct) return direct;
3376
+ const bag = pending.pauseData;
3377
+ if (typeof bag !== "object" || bag === null) return void 0;
3378
+ const homes = bag;
3379
+ return asComponent(homes.component) ?? asComponent(
3380
+ typeof homes.checkIn === "object" && homes.checkIn !== null ? homes.checkIn.component : void 0
3381
+ ) ?? asComponent(
3382
+ typeof homes.ask === "object" && homes.ask !== null ? homes.ask.component : void 0
3383
+ );
3384
+ }
3385
+ function asComponent(value) {
3386
+ if (typeof value !== "object" || value === null) return void 0;
3387
+ const c = value;
3388
+ if (typeof c.componentId !== "string" || c.componentId.length === 0) return void 0;
3389
+ return value;
3390
+ }
3391
+ function isConsentAsk(awaiting) {
3392
+ if (typeof awaiting !== "object" || awaiting === null) return false;
3393
+ const pending = awaiting;
3394
+ return typeof pending.checkIn === "object" && pending.checkIn !== null || typeof pending.ask === "object" && pending.ask !== null;
3395
+ }
3396
+ function approveDecision(input) {
3397
+ return buildConsent(true, input, "approveDecision");
3398
+ }
3399
+ function declineDecision(input) {
3400
+ return buildConsent(false, input, "declineDecision");
3401
+ }
3402
+ function buildConsent(approved, input, door) {
3403
+ if (typeof input?.by !== "string" || input.by.trim().length === 0) {
3404
+ throw new Error(
3405
+ `${door} needs \`by\` \u2014 who is deciding (an operator id, an email). The decision is an audit record, and a record with no actor names nobody; it cannot be defaulted.`
3406
+ );
3407
+ }
3408
+ return {
3409
+ approved,
3410
+ by: input.by,
3411
+ at: Date.now(),
3412
+ ...input.note !== void 0 && input.note.length > 0 && { note: input.note }
3413
+ };
3414
+ }
3415
+ function decisionRequestBody(args) {
3416
+ if (args === null || typeof args !== "object" || !("decision" in args)) {
3417
+ throw new Error(
3418
+ "decisionRequestBody needs `decision` \u2014 the structured answer to the pending ask. Its presence is what distinguishes a resume from a new message on the wire."
3419
+ );
3420
+ }
3421
+ if (args.decision === void 0) {
3422
+ throw new Error(
3423
+ "decisionRequestBody refused `decision: undefined` \u2014 JSON drops the key, the wire would read a NEW MESSAGE, and the pause would sit unanswered while the screen believed it answered. Post the actual decision (`null` is allowed; absence is not)."
3424
+ );
3425
+ }
3426
+ return {
3427
+ input: args.input ?? "",
3428
+ decision: args.decision,
3429
+ ...args.sessionId !== void 0 && { sessionId: args.sessionId }
3430
+ };
3431
+ }
3432
+ var SENTENCE_JSON_CAP = 120;
3433
+ function decisionSentence(decision) {
3434
+ if (isConsentDecision(decision)) {
3435
+ const verb = decision.approved ? "Approved" : "Declined";
3436
+ const note = decision.note !== void 0 ? ` \u2014 "${decision.note}"` : "";
3437
+ return `${verb} by ${decision.by}${note}.`;
3438
+ }
3439
+ if (typeof decision === "string") return `Answered: "${decision}".`;
3440
+ if (decision === null || typeof decision !== "object")
3441
+ return `Answered: ${String(decision)}.`;
3442
+ let json;
3443
+ try {
3444
+ json = JSON.stringify(decision) ?? String(decision);
3445
+ } catch {
3446
+ json = "[unserializable]";
3447
+ }
3448
+ if (json.length > SENTENCE_JSON_CAP) json = `${json.slice(0, SENTENCE_JSON_CAP)}\u2026`;
3449
+ return `Answered with a structured decision: ${json}.`;
3450
+ }
3451
+ function isConsentDecision(value) {
3452
+ return typeof value === "object" && value !== null && typeof value.approved === "boolean" && typeof value.by === "string";
3453
+ }
3454
+
3151
3455
  export {
3152
3456
  ChangeNotifier,
3153
3457
  lensSnapshotRecorder,
@@ -3158,6 +3462,13 @@ export {
3158
3462
  observeRecording,
3159
3463
  buildStepGraphFromSnapshot,
3160
3464
  buildSpecTreeFromBoundary,
3465
+ DEFAULT_MAX_BYTES,
3466
+ formatBytes,
3467
+ conversationUnits,
3468
+ fileUnits,
3469
+ defaultSelection,
3470
+ measureSelection,
3471
+ trimHintFor,
3161
3472
  defaultHumanizer,
3162
3473
  humanizeWith,
3163
3474
  makeTeachingHumanizer,
@@ -3194,18 +3505,24 @@ export {
3194
3505
  toReactFlow,
3195
3506
  defaultSize,
3196
3507
  layoutLensGraph,
3197
- DEFAULT_MAX_BYTES,
3198
- formatBytes,
3199
- conversationUnits,
3200
- fileUnits,
3201
- defaultSelection,
3202
- measureSelection,
3203
- trimHintFor,
3204
3508
  buildIssueBody,
3205
3509
  parseGithubRepo,
3206
3510
  MAX_ISSUE_URL_BYTES,
3207
3511
  TRUNCATION_NOTICE,
3208
3512
  buildNewIssueUrl,
3209
- encodeBase64
3513
+ encodeBase64,
3514
+ httpArtifactResolver,
3515
+ storeArtifactResolver,
3516
+ readPresentedResult,
3517
+ presentedFromEvents,
3518
+ describePresented,
3519
+ artifactPlaceholder,
3520
+ readAwaitingComponent,
3521
+ isConsentAsk,
3522
+ approveDecision,
3523
+ declineDecision,
3524
+ decisionRequestBody,
3525
+ decisionSentence,
3526
+ isConsentDecision
3210
3527
  };
3211
- //# sourceMappingURL=chunk-6EHPI5F6.js.map
3528
+ //# sourceMappingURL=chunk-PBRAJNVI.js.map