agentfootprint-lens 0.34.0 → 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
@@ -436,6 +436,73 @@ prose via the default humanizer.
436
436
 
437
437
  ---
438
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
+
439
506
  ## Theming
440
507
 
441
508
  **Lens inherits theme tokens from your app via CSS variables.** Set `--fp-*`
@@ -613,6 +680,23 @@ The headless half — `httpArtifactResolver`, `storeArtifactResolver`,
613
680
  `agentfootprint-lens/core`. See
614
681
  [Render an artifact by its ref](#render-an-artifact-by-its-ref).
615
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
+
616
700
  ### Headless core
617
701
 
618
702
  `agentfootprint-lens/core` is React-free: `LensRecorder`, `ChangeNotifier`,
@@ -3367,6 +3367,91 @@ function artifactPlaceholder(presented) {
3367
3367
  return `${describePresented(presented)} \u2014 expired; re-run to regenerate.`;
3368
3368
  }
3369
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
+
3370
3455
  export {
3371
3456
  ChangeNotifier,
3372
3457
  lensSnapshotRecorder,
@@ -3431,6 +3516,13 @@ export {
3431
3516
  readPresentedResult,
3432
3517
  presentedFromEvents,
3433
3518
  describePresented,
3434
- artifactPlaceholder
3519
+ artifactPlaceholder,
3520
+ readAwaitingComponent,
3521
+ isConsentAsk,
3522
+ approveDecision,
3523
+ declineDecision,
3524
+ decisionRequestBody,
3525
+ decisionSentence,
3526
+ isConsentDecision
3435
3527
  };
3436
- //# sourceMappingURL=chunk-ZSOYNKUI.js.map
3528
+ //# sourceMappingURL=chunk-PBRAJNVI.js.map