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 +175 -0
- package/dist/{chunk-6EHPI5F6.js → chunk-PBRAJNVI.js} +382 -65
- package/dist/chunk-PBRAJNVI.js.map +1 -0
- package/dist/core.cjs +386 -56
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/core.js +27 -1
- package/dist/{index-CPndFgKv.d.cts → index-CmiKvUAS.d.cts} +468 -1
- package/dist/{index-CPndFgKv.d.ts → index-CmiKvUAS.d.ts} +468 -1
- package/dist/index.cjs +1198 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +309 -3
- package/dist/index.d.ts +309 -3
- package/dist/index.js +830 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-6EHPI5F6.js.map +0 -1
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-
|
|
3528
|
+
//# sourceMappingURL=chunk-PBRAJNVI.js.map
|