@datalayer/agent-runtimes 1.5.89 → 1.5.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +29 -297
  2. package/dist-embed/datalayer-app-main.js +40 -36
  3. package/lib/apps/embed/element.js +20 -5
  4. package/lib/apps/embed/visitorApp.d.ts +23 -45
  5. package/lib/apps/embed/visitorApp.js +65 -13
  6. package/lib/components/teams/A2ATeamGraph.d.ts +58 -152
  7. package/lib/components/teams/A2ATeamGraph.js +169 -410
  8. package/lib/components/teams/SceneTranscript.d.ts +5 -1
  9. package/lib/components/teams/SceneTranscript.js +2 -2
  10. package/lib/components/teams/SceneView.d.ts +17 -5
  11. package/lib/components/teams/SceneView.js +7 -3
  12. package/lib/components/teams/a2aTeamFlow.d.ts +16 -7
  13. package/lib/components/teams/a2aTeamFlow.js +17 -14
  14. package/lib/components/teams/backdrops/catalogue.d.ts +35 -0
  15. package/lib/components/teams/backdrops/catalogue.js +34 -0
  16. package/lib/components/teams/backdrops/edges.d.ts +20 -0
  17. package/lib/components/teams/backdrops/edges.js +29 -0
  18. package/lib/components/teams/backdrops/faces.d.ts +24 -0
  19. package/lib/components/teams/backdrops/faces.js +39 -0
  20. package/lib/components/teams/backdrops/halo.d.ts +14 -0
  21. package/lib/components/teams/backdrops/halo.js +5 -0
  22. package/lib/components/teams/backdrops/hero.d.ts +14 -0
  23. package/lib/components/teams/backdrops/hero.js +178 -0
  24. package/lib/components/teams/backdrops/index.d.ts +22 -0
  25. package/lib/components/teams/backdrops/index.js +38 -0
  26. package/lib/components/teams/backdrops/nodes.d.ts +123 -0
  27. package/lib/components/teams/backdrops/nodes.js +265 -0
  28. package/lib/components/teams/backdrops/plain.d.ts +14 -0
  29. package/lib/components/teams/backdrops/plain.js +116 -0
  30. package/lib/components/teams/backdrops/studio.d.ts +17 -0
  31. package/lib/components/teams/backdrops/studio.js +122 -0
  32. package/lib/components/teams/backdrops/theme.d.ts +14 -0
  33. package/lib/components/teams/backdrops/theme.js +19 -0
  34. package/lib/components/teams/backdrops/types.d.ts +214 -0
  35. package/lib/components/teams/backdrops/types.js +5 -0
  36. package/lib/components/teams/index.d.ts +2 -0
  37. package/lib/components/teams/index.js +2 -0
  38. package/lib/components/teams/sceneTranscriptRules.d.ts +32 -1
  39. package/lib/components/teams/sceneTranscriptRules.js +104 -2
  40. package/lib/components/teams/transcript/RecordingLabel.d.ts +17 -0
  41. package/lib/components/teams/transcript/RecordingLabel.js +7 -0
  42. package/lib/components/teams/transcript/SequenceDiagram.d.ts +39 -0
  43. package/lib/components/teams/transcript/SequenceDiagram.js +190 -0
  44. package/lib/components/teams/transcript/TranscriptDetail.d.ts +27 -0
  45. package/lib/components/teams/transcript/TranscriptDetail.js +48 -0
  46. package/lib/components/teams/transcript/TranscriptTable.d.ts +23 -0
  47. package/lib/components/teams/transcript/TranscriptTable.js +112 -0
  48. package/lib/components/teams/transcript/TranscriptViews.d.ts +66 -0
  49. package/lib/components/teams/transcript/TranscriptViews.js +135 -0
  50. package/lib/components/teams/transcript/TranscriptViewsTypes.d.ts +13 -0
  51. package/lib/components/teams/transcript/TranscriptViewsTypes.js +5 -0
  52. package/lib/components/teams/transcript/index.d.ts +23 -0
  53. package/lib/components/teams/transcript/index.js +27 -0
  54. package/lib/components/teams/transcript/recordingPlayback.d.ts +52 -0
  55. package/lib/components/teams/transcript/recordingPlayback.js +146 -0
  56. package/lib/components/teams/transcript/selection.d.ts +59 -0
  57. package/lib/components/teams/transcript/selection.js +79 -0
  58. package/lib/components/teams/transcript/sequenceDiagramRules.d.ts +35 -0
  59. package/lib/components/teams/transcript/sequenceDiagramRules.js +158 -0
  60. package/lib/components/teams/transcript/transcriptDetailRules.d.ts +80 -0
  61. package/lib/components/teams/transcript/transcriptDetailRules.js +208 -0
  62. package/lib/components/teams/transcript/transcriptExport.d.ts +31 -0
  63. package/lib/components/teams/transcript/transcriptExport.js +82 -0
  64. package/lib/components/teams/transcript/transcriptRows.d.ts +76 -0
  65. package/lib/components/teams/transcript/transcriptRows.js +187 -0
  66. package/lib/components/teams/transcript/useSceneRecording.d.ts +45 -0
  67. package/lib/components/teams/transcript/useSceneRecording.js +96 -0
  68. package/lib/examples/ScenesExample.js +16 -3
  69. package/lib/portable/hosts/agentClient.d.ts +8 -0
  70. package/lib/portable/hosts/agentClient.js +2 -0
  71. package/lib/portable/hosts/approvalRenderers.d.ts +10 -2
  72. package/lib/portable/hosts/approvalRenderers.js +81 -2
  73. package/lib/portable/hosts/contracts.d.ts +1 -0
  74. package/lib/portable/hosts/contracts.js +2 -0
  75. package/lib/portable/hosts/contracts.json +56 -2
  76. package/lib/specs/actions.js +2 -0
  77. package/lib/specs/apps.d.ts +1 -0
  78. package/lib/specs/apps.js +219 -0
  79. package/lib/specs/sceneRecordings.d.ts +14 -0
  80. package/lib/specs/sceneRecordings.js +12 -0
  81. package/lib/specs/sceneSchema.js +11 -0
  82. package/lib/specs/scenes.d.ts +1 -0
  83. package/lib/specs/scenes.js +159 -0
  84. package/lib/types/scenes.d.ts +54 -0
  85. package/package.json +2 -1
  86. package/scripts/codegen/generate_mcp_servers.py +5 -16
  87. package/scripts/codegen/generate_scenes.py +80 -0
@@ -0,0 +1,45 @@
1
+ import type { SceneRecordingFile } from '../../../types/scenes';
2
+ import type { SceneTranscriptLine, SceneTranscriptMember } from '../sceneTranscriptRules';
3
+ import { type RecordedStage } from './recordingPlayback';
4
+ /** How the recording is played. */
5
+ export type SceneRecordingOptions = {
6
+ /** The members, by id and name, with their connections: for the graph's flows and calls. */
7
+ members: readonly SceneTranscriptMember[];
8
+ /** The member the audience talks to: the links the graph draws flowing start there. */
9
+ entry: string;
10
+ /** One beat's lines only, by its id; every line when unsaid. */
11
+ beat?: string;
12
+ /** Faster than recorded (2 is twice as fast); 1 when unsaid: the recorded pace. */
13
+ speed?: number;
14
+ /** The longest pause between two lines, in milliseconds; none when unsaid. */
15
+ maxGapMs?: number;
16
+ /** Start playing at once; true when unsaid. */
17
+ autoPlay?: boolean;
18
+ /** Start again from the first line once the last was shown, after `loopAfterMs`. */
19
+ loop?: boolean;
20
+ /** How long the last line stays before a loop starts again; 8 s when unsaid. */
21
+ loopAfterMs?: number;
22
+ /** How often the stage is drawn again while a message flows; 250 ms when unsaid. */
23
+ tickMs?: number;
24
+ };
25
+ /** A recording at a moment of its playback. */
26
+ export type SceneRecordingPlayback = RecordedStage & {
27
+ /** The lines said so far, in order. */
28
+ lines: SceneTranscriptLine[];
29
+ /** Every line of what is played: the beat's, or the whole recording's. */
30
+ all: SceneTranscriptLine[];
31
+ /** *Recording, taken 29 October 2026*. */
32
+ label: string;
33
+ /** When it was taken, as the recording says it. */
34
+ taken: string;
35
+ /** What the audience is told of it, when the recording says. */
36
+ note?: string;
37
+ playing: boolean;
38
+ /** Every line was shown. */
39
+ done: boolean;
40
+ play: () => void;
41
+ pause: () => void;
42
+ restart: () => void;
43
+ };
44
+ /** Plays a scene's recording at its recorded pace; nothing to play when there is none. */
45
+ export declare function useSceneRecording(recording: SceneRecordingFile | undefined, { members, entry, beat, speed, maxGapMs, autoPlay, loop, loopAfterMs, tickMs, }: SceneRecordingOptions): SceneRecordingPlayback;
@@ -0,0 +1,96 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * A scene's recording, played: when a stage cannot play live, its recording
7
+ * (`SceneRecordingFile`, `SCENE_RECORDINGS` in `specs/sceneRecordings`)
8
+ * plays at the pace it was recorded. The hook hands the page what to draw
9
+ * at each moment — the lines said so far for `TranscriptViews`, the
10
+ * `flows` and `calls` for `A2ATeamGraph`, and the label, *Recording, taken
11
+ * <date>* — and the controls: play, pause, restart.
12
+ *
13
+ * ```tsx
14
+ * const played = useSceneRecording(getSceneRecording(scene.id), { members, entry });
15
+ * <RecordingLabel taken={played.taken} />
16
+ * <A2ATeamGraph … flows={played.flows} calls={played.calls} />
17
+ * <TranscriptViews lines={played.lines} members={members} />
18
+ * ```
19
+ *
20
+ * A line of a recording has no span: the drill-down says so.
21
+ *
22
+ * @module components/teams/transcript/useSceneRecording
23
+ */
24
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
25
+ import { playbackOffsets, recordedLinesOf, recordedMomentAt, recordedStageAt, recordingLabel, } from './recordingPlayback.js';
26
+ const now = () => typeof performance !== 'undefined' ? performance.now() : Date.now();
27
+ /** Plays a scene's recording at its recorded pace; nothing to play when there is none. */
28
+ export function useSceneRecording(recording, { members, entry, beat, speed = 1, maxGapMs, autoPlay = true, loop = false, loopAfterMs = 8000, tickMs = 250, }) {
29
+ const all = useMemo(() => (recording ? recordedLinesOf(recording, beat) : []), [recording, beat]);
30
+ const offsets = useMemo(() => playbackOffsets(all, { speed, maxGapMs }), [all, speed, maxGapMs]);
31
+ const [elapsed, setElapsed] = useState(0);
32
+ const [playing, setPlaying] = useState(autoPlay && all.length > 0);
33
+ // Where the clock started, minus what was played before a pause.
34
+ const started = useRef(now());
35
+ const total = offsets.length ? offsets[offsets.length - 1] : 0;
36
+ useEffect(() => {
37
+ setElapsed(0);
38
+ started.current = now();
39
+ setPlaying(autoPlay && all.length > 0);
40
+ }, [all, autoPlay]);
41
+ useEffect(() => {
42
+ if (!playing) {
43
+ return undefined;
44
+ }
45
+ const timer = setInterval(() => {
46
+ const at = now() - started.current;
47
+ if (at >= total + (loop ? loopAfterMs : 0)) {
48
+ if (loop) {
49
+ started.current = now();
50
+ setElapsed(0);
51
+ return;
52
+ }
53
+ setElapsed(total);
54
+ setPlaying(false);
55
+ return;
56
+ }
57
+ setElapsed(at);
58
+ }, tickMs);
59
+ return () => clearInterval(timer);
60
+ }, [playing, total, loop, loopAfterMs, tickMs]);
61
+ const play = useCallback(() => {
62
+ if (all.length === 0) {
63
+ return;
64
+ }
65
+ started.current = now() - (elapsed >= total ? 0 : elapsed);
66
+ if (elapsed >= total) {
67
+ setElapsed(0);
68
+ }
69
+ setPlaying(true);
70
+ }, [all.length, elapsed, total]);
71
+ const pause = useCallback(() => setPlaying(false), []);
72
+ const restart = useCallback(() => {
73
+ started.current = now();
74
+ setElapsed(0);
75
+ setPlaying(all.length > 0);
76
+ }, [all.length]);
77
+ const shown = offsets.filter(offset => offset <= elapsed).length;
78
+ const lines = useMemo(() => all.slice(0, shown), [all, shown]);
79
+ const stage = useMemo(() => all.length
80
+ ? recordedStageAt(all, recordedMomentAt(all, offsets, elapsed, speed), members, entry)
81
+ : { flows: {}, calls: [] }, [all, offsets, elapsed, speed, members, entry]);
82
+ const taken = recording?.taken ?? '';
83
+ return {
84
+ ...stage,
85
+ lines,
86
+ all,
87
+ label: recordingLabel(taken),
88
+ taken,
89
+ ...(recording?.note ? { note: recording.note } : {}),
90
+ playing,
91
+ done: all.length > 0 && shown === all.length && !playing,
92
+ play,
93
+ pause,
94
+ restart,
95
+ };
96
+ }
@@ -37,7 +37,7 @@ import { ThemedProvider } from './utils/themedProvider.js';
37
37
  import { useBrowserInference } from '../hooks/useBrowserInference.js';
38
38
  import { useAnonymousSessionStore } from '../runtimes/browser/anonymousToken.js';
39
39
  import { connectA2APeer } from '../runtimes/browser/a2aPeer.js';
40
- import { A2ATeamGraph, NOTEBOOK_AND_WORDS, PERSON, SceneView, sceneCuesOf, sceneMemberUrl, sceneRuntimePorts, sceneStageOf, teamConnectionsOf, teamToolClassifier, useA2ATeam, } from '../components/teams/index.js';
40
+ import { A2ATeamGraph, NOTEBOOK_AND_WORDS, PERSON, SceneView, sceneCuesOf, sceneMemberUrl, sceneRuntimePorts, sceneStageOf, teamConnectionsOf, teamToolClassifier, useA2ATeam, useSceneTranscript, latestLineOnEdge, selectedEdgeOf, selectionOfLine, stageBackdropOf, } from '../components/teams/index.js';
41
41
  import { listSceneSpecs } from '../specs/scenes.js';
42
42
  import { classifyToolCall, traceA2AFetch, useAgentInspector, } from '../components/inspector/index.js';
43
43
  /** Where the members on a runtime are, by application id: the local ports, unless a variable says. */
@@ -151,6 +151,11 @@ function Scene({ scene }) {
151
151
  name: member.app.name,
152
152
  connections: runtimeMembers.find(one => one.id === member.id)?.connections ?? [],
153
153
  })), [stage, runtimeMembers]);
154
+ // The exchange selected in the transcript, as the edge of the graph it is
155
+ // on (S3-02): the graph draws it with a halo; a click on an edge selects
156
+ // its latest exchange.
157
+ const [selectedEdge, setSelectedEdge] = useState(null);
158
+ const lines = useSceneTranscript(inspector, transcriptMembers);
154
159
  const whereOf = (member) => member.runsIn === 'browser'
155
160
  ? 'in your browser'
156
161
  : `on a runtime, ${new URL(runtimeMembers.find(one => one.id === member.id)?.url ?? 'http://runtime').host}`;
@@ -173,7 +178,7 @@ function Scene({ scene }) {
173
178
  return (_jsxs(Box, { "data-scene-example": scene.id, children: [_jsx(Text, { as: "p", sx: { color: 'fg.muted', mt: 0 }, "data-scene-assumes": "", children: stage.assumes }), anonymous.status === 'active' && anonymous.expiresAt && (_jsx(Box, { mb: 3, children: _jsx(AnonymousKeyTimer, { expiresAt: anonymous.expiresAt, grantedMs: anonymous.grantedMs, label: `${entry.app.name}'s trial key`, onExpire: () => useAnonymousSessionStore.getState().expire() }) })), anonymous.status === 'expired' && (_jsx(Box, { mb: 3, children: _jsx(AnonymousKeyExpired, { agentName: entry.app.name, onSignedIn: () => useAnonymousSessionStore.getState().clear() }) })), entryInBrowser && needsSignIn && (_jsxs(Flash, { variant: "warning", sx: { mb: 3 }, children: [entry.app.name, " asks its model through the Datalayer inference service: sign in, or wait for a visitor\u2019s trial key."] })), notReached.map(member => (_jsx(Flash, { variant: "danger", sx: { mb: 3 }, "data-scene-needs": member.app.id, children: memberNeeds(member, reachedOf(member.id).error ?? '').map(line => (_jsx(Text, { as: "p", sx: { m: 0 }, children: line }, line))) }, member.id))), runtimeMembers.map(member => (_jsx(ReachMember, { member: member,
174
179
  // Its requests are the entry's, or the person's own when the
175
180
  // page asks it itself.
176
- asker: member.entry ? PERSON : entry.app.name, tracer: inspector, onReached: onReached }, member.id))), _jsx(Box, { border: "1px solid", borderColor: "border.default", borderRadius: 2, bg: "canvas.subtle", px: 2, pt: 2, mb: 3, children: _jsx(SceneView, { members: transcriptMembers, tracer: inspector, emptyText: `Nothing said yet: ask ${entry.app.name} with one of the suggestions.`, children: _jsx(A2ATeamGraph, { entry: {
181
+ asker: member.entry ? PERSON : entry.app.name, tracer: inspector, onReached: onReached }, member.id))), _jsx(Box, { border: "1px solid", borderColor: "border.default", borderRadius: 2, bg: "canvas.subtle", px: 2, pt: 2, mb: 3, children: _jsx(SceneView, { members: transcriptMembers, tracer: inspector, onSelect: selection => setSelectedEdge(selectedEdgeOf(selection, transcriptMembers) ?? null), emptyText: `Nothing said yet: ask ${entry.app.name} with one of the suggestions.`, children: _jsx(A2ATeamGraph, { entry: {
177
182
  id: entry.app.id,
178
183
  name: entry.app.name,
179
184
  emoji: entry.cast.persona.face || entry.app.emoji,
@@ -228,7 +233,15 @@ function Scene({ scene }) {
228
233
  ];
229
234
  })),
230
235
  // Room for the balloons: suggestions, *more* and a notebook.
231
- balloonRoom: 320 }) }) }), _jsx(Box, { ref: notebookArea, mb: team.notebook ? 3 : 0, "data-team-notebook-placement": "under-graph" }), _jsxs(Box, { display: "flex", gap: 3, flexWrap: "wrap", children: [_jsxs(Box, { flex: "1 1 420px", minWidth: 0, children: [_jsxs(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: ["Ask ", entry.app.name] }), _jsx(Text, { as: "p", sx: { color: 'fg.muted', fontSize: 1, mt: 0 }, children: "Or choose one of its suggestions in its balloon: the script\u2019s cues." }), _jsx(Textarea, { ref: composer, block: true, rows: 3, value: draft, placeholder: suggestions[0]?.prompt ?? 'Ask…', onChange: event => setDraft(event.target.value), onKeyDown: event => {
236
+ balloonRoom: 320,
237
+ // Drawn as the scene says: studio, hero or plain.
238
+ backdrop: stageBackdropOf(scene), selected: selectedEdge, onSelectEdge: edge => {
239
+ const line = latestLineOnEdge(edge, lines, transcriptMembers);
240
+ setSelectedEdge(line
241
+ ? (selectedEdgeOf(selectionOfLine(line), transcriptMembers) ??
242
+ null)
243
+ : null);
244
+ } }) }) }), _jsx(Box, { ref: notebookArea, mb: team.notebook ? 3 : 0, "data-team-notebook-placement": "under-graph" }), _jsxs(Box, { display: "flex", gap: 3, flexWrap: "wrap", children: [_jsxs(Box, { flex: "1 1 420px", minWidth: 0, children: [_jsxs(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: ["Ask ", entry.app.name] }), _jsx(Text, { as: "p", sx: { color: 'fg.muted', fontSize: 1, mt: 0 }, children: "Or choose one of its suggestions in its balloon: the script\u2019s cues." }), _jsx(Textarea, { ref: composer, block: true, rows: 3, value: draft, placeholder: suggestions[0]?.prompt ?? 'Ask…', onChange: event => setDraft(event.target.value), onKeyDown: event => {
232
245
  if (event.key === 'Enter' && !event.shiftKey) {
233
246
  event.preventDefault();
234
247
  send(draft);
@@ -20,6 +20,12 @@ export type HostTurnRequest<P> = Readonly<{
20
20
  * another application): a new session's uid is derived with it.
21
21
  */
22
22
  sessionGeneration?: number;
23
+ /**
24
+ * The host user never linked: exchanged as a visitor, which ai-agents
25
+ * answers only when the installation's owner opened it to visitors and the
26
+ * deployment opens to somebody signed out (WHATSAPP WA-03).
27
+ */
28
+ visitor?: boolean;
23
29
  }>;
24
30
  export type TurnStream = Readonly<{
25
31
  /**
@@ -111,6 +117,8 @@ export declare class HostKeyCredentials<P> implements HostCredentials<P> {
111
117
  hostKey: string;
112
118
  /** Where the turn is asked from: Datalayer refuses a denied channel at once. */
113
119
  place?: Place;
120
+ /** As a visitor: the host user never linked (WHATSAPP WA-03). */
121
+ visitor?: boolean;
114
122
  }): Promise<HostExchange>;
115
123
  forTurn(request: HostTurnRequest<P>): Promise<HostSession>;
116
124
  }
@@ -44,6 +44,7 @@ export class HostKeyCredentials {
44
44
  externalUserId: request.externalUserId,
45
45
  hostKey,
46
46
  place: this.shape.placeOf(request),
47
+ ...(request.visitor ? { visitor: true } : {}),
47
48
  });
48
49
  }
49
50
  /** The exchange for one host user and deployment, with a key in hand (a delivery, an action). */
@@ -53,6 +54,7 @@ export class HostKeyCredentials {
53
54
  deployment_uid: input.deploymentUid,
54
55
  external_user_id: input.externalUserId,
55
56
  ...(input.place ? { place: input.place } : {}),
57
+ ...(input.visitor ? { visitor: true } : {}),
56
58
  }),
57
59
  headers: {
58
60
  'content-type': 'application/json',
@@ -6,7 +6,9 @@ import type { ApprovalPresentation } from './contracts';
6
6
  * presented is named by the renderer, or the card fails closed. An unknown
7
7
  * tool, an argument the renderer does not know, an attachment, and every
8
8
  * action that is not mail (reads, writes, other sends, destructive, financial,
9
- * publishing, executing) are reviewed in Datalayer (decided by the user on 16 October 2026: mail only).
9
+ * publishing, executing) are reviewed in Datalayer (decided by the user on 16 October 2026: mail only),
10
+ * but for a company's `pap_confirm`, decided in a host with the operation's
11
+ * exact revision since 28 October 2026 (the user, WHATSAPP WA-43).
10
12
  * They render ai-agents' presentation v1, a different input
11
13
  * from `../approvals.ts` and `../approvalCard.ts`. Moved from the Slack
12
14
  * service (DC-004).
@@ -39,7 +41,13 @@ type Renderer = Readonly<{
39
41
  export declare const DATALAYER_ONLY: Set<"unknown" | "read" | "write" | "send" | "delete" | "publish" | "execute" | "financial">;
40
42
  /** The words of a tool's name that make a send mail: contracts.json `mail_words`. */
41
43
  export declare const MAIL_WORDS: ReadonlySet<string>;
42
- /** The renderers, the first that matches wins: Datalayer-only before any other. */
44
+ export declare const isPapConfirm: (toolName: string) => boolean;
45
+ /** What a `pap_confirm` approval carries: contracts.json `presentation.pap_confirm_keys`. */
46
+ export declare const PAP_CONFIRM_KEYS: readonly ["key", "company", "summary", "terms", "operation_ref", "revision", "digest", "class", "session", "only_the_person", "was"];
47
+ /**
48
+ * The renderers, the first that matches wins: a company's `pap_confirm`
49
+ * (whose name says no class), then Datalayer-only before any other.
50
+ */
43
51
  export declare const RENDERERS: readonly Renderer[];
44
52
  /**
45
53
  * How a host shows an approval, by the renderer that knows its tool; with
@@ -98,6 +98,82 @@ const mail = {
98
98
  return rendered('mail', presentation, fields, MAIL_KEYS, extra);
99
99
  },
100
100
  };
101
+ /** `pap_confirm`, as the gateway names it with or without a prefix. */
102
+ const PAP_CONFIRM = /(^|[_.:\-/])pap_confirm$/;
103
+ export const isPapConfirm = (toolName) => PAP_CONFIRM.test(toolName);
104
+ /** What a `pap_confirm` approval carries: contracts.json `presentation.pap_confirm_keys`. */
105
+ export const PAP_CONFIRM_KEYS = [
106
+ 'key',
107
+ 'company',
108
+ 'summary',
109
+ 'terms',
110
+ 'operation_ref',
111
+ 'revision',
112
+ 'digest',
113
+ 'class',
114
+ 'session',
115
+ 'only_the_person',
116
+ 'was',
117
+ ];
118
+ /**
119
+ * A company's operation the person confirms (WHATSAPP WA-43, decided by the
120
+ * user 28 October 2026): the company, its own summary, every term, the
121
+ * operation and its **exact revision** — a decision is on that revision and
122
+ * no other; a changed revision is another approval. A missing company,
123
+ * summary, operation, revision or digest fails the card closed.
124
+ */
125
+ const papConfirm = {
126
+ name: 'pap-confirm',
127
+ matches: presentation => isPapConfirm(presentation.tool_name),
128
+ render(presentation) {
129
+ const args = presentation.arguments;
130
+ const fields = [];
131
+ const missing = [];
132
+ const extra = [];
133
+ const company = typeof args.company === 'string' ? args.company : '';
134
+ const summary = typeof args.summary === 'string' ? args.summary : '';
135
+ const operation = typeof args.operation_ref === 'string' ? args.operation_ref : '';
136
+ const revision = Number.isInteger(args.revision)
137
+ ? args.revision
138
+ : undefined;
139
+ if (!company)
140
+ missing.push('the company');
141
+ else
142
+ fields.push({ label: 'Company', value: company });
143
+ if (!summary)
144
+ missing.push('what it does, in its words');
145
+ else
146
+ fields.push({ label: 'Summary', value: summary });
147
+ const termsAreFields = args.terms === undefined ||
148
+ args.terms === null ||
149
+ (typeof args.terms === 'object' && !Array.isArray(args.terms));
150
+ if (!termsAreFields)
151
+ extra.push('Its terms are not named one by one: review it in Datalayer.');
152
+ else
153
+ for (const [name, value] of Object.entries((args.terms ?? {})))
154
+ fields.push({ label: name, value: text(value) });
155
+ if (!operation)
156
+ missing.push('the operation');
157
+ else
158
+ fields.push({ label: 'Operation', value: operation });
159
+ if (revision === undefined)
160
+ missing.push('its exact revision');
161
+ else
162
+ fields.push({ label: 'Revision', value: String(revision) });
163
+ if (typeof args.digest !== 'string' || !args.digest)
164
+ missing.push('the digest of its terms');
165
+ if (typeof args.class === 'string' && args.class)
166
+ fields.push({ label: 'What it does', value: args.class });
167
+ const was = args.was && typeof args.was === 'object'
168
+ ? args.was
169
+ : undefined;
170
+ if (was && was.revision !== undefined)
171
+ fields.push({ label: 'Replaces revision', value: text(was.revision) });
172
+ if (missing.length)
173
+ extra.push(`The request does not say ${missing.join(', ')}: review it in Datalayer.`);
174
+ return rendered('pap-confirm', presentation, fields, PAP_CONFIRM_KEYS, extra);
175
+ },
176
+ };
101
177
  /** Everything but mail: shown, decided only in Datalayer. */
102
178
  const datalayerOnly = {
103
179
  name: 'datalayer-only',
@@ -122,8 +198,11 @@ const datalayerOnly = {
122
198
  };
123
199
  },
124
200
  };
125
- /** The renderers, the first that matches wins: Datalayer-only before any other. */
126
- export const RENDERERS = [datalayerOnly, mail];
201
+ /**
202
+ * The renderers, the first that matches wins: a company's `pap_confirm`
203
+ * (whose name says no class), then Datalayer-only before any other.
204
+ */
205
+ export const RENDERERS = [papConfirm, datalayerOnly, mail];
127
206
  /**
128
207
  * How a host shows an approval, by the renderer that knows its tool; with
129
208
  * none, nothing is decided in the host.
@@ -93,6 +93,7 @@ export declare const exchangeSchema: z.ZodObject<{
93
93
  expires_in: z.ZodNumber;
94
94
  exp: z.ZodNumber;
95
95
  principal_uid: z.ZodString;
96
+ visitor: z.ZodOptional<z.ZodBoolean>;
96
97
  host_uid: z.ZodString;
97
98
  deployment: z.ZodObject<{
98
99
  uid: z.ZodString;
@@ -104,6 +104,8 @@ export const exchangeSchema = z.object({
104
104
  expires_in: z.number().int().nonnegative(),
105
105
  exp: z.number().int(),
106
106
  principal_uid: z.string().min(1),
107
+ /** A person who never linked, answered as a visitor (WHATSAPP WA-03): `principal_uid` is `visitor:<hash>`. */
108
+ visitor: z.boolean().optional(),
107
109
  host_uid: z.string().min(1),
108
110
  deployment: z.object({
109
111
  uid: z.string().min(1),
@@ -114,6 +114,45 @@
114
114
  "direct"
115
115
  ]
116
116
  },
117
+ "visitor_exchange": {
118
+ "about": "WHATSAPP WA-03 (decided 28 October 2026): a host's user who never linked, answered as a visitor when the installation's owner turned its visitors switch on and the deployment opens to somebody signed out. sub is visitor:<hash of the host's id with the installation's salt>; no link; a daily cap per visitor at the exchange (429).",
119
+ "request": {
120
+ "external_user_id": "US.13491208655302741918",
121
+ "deployment_uid": "dep-1",
122
+ "visitor": true
123
+ },
124
+ "response_keys": [
125
+ "success",
126
+ "access_token",
127
+ "token_type",
128
+ "expires_in",
129
+ "exp",
130
+ "principal_uid",
131
+ "visitor",
132
+ "host_uid",
133
+ "deployment",
134
+ "session_api"
135
+ ],
136
+ "token_claims": [
137
+ "jti",
138
+ "iss",
139
+ "iat",
140
+ "exp",
141
+ "aud",
142
+ "sub",
143
+ "azp",
144
+ "host_kind",
145
+ "deployment_uid",
146
+ "app_uid",
147
+ "visitor",
148
+ "scope"
149
+ ],
150
+ "sub_prefix": "visitor:",
151
+ "refusals": {
152
+ "visitors_off": 403,
153
+ "capped": 429
154
+ }
155
+ },
117
156
  "whoami": {
118
157
  "response_keys": [
119
158
  "success",
@@ -124,7 +163,8 @@
124
163
  "app_uid",
125
164
  "link",
126
165
  "scope",
127
- "expires_in"
166
+ "expires_in",
167
+ "visitor"
128
168
  ]
129
169
  },
130
170
  "bind_session": {
@@ -328,7 +368,21 @@
328
368
  "host_may_decide": true,
329
369
  "reasons": [],
330
370
  "digest": "0000000000000000000000000000000000000000000000000000000000000000"
331
- }
371
+ },
372
+ "pap_confirm_keys": [
373
+ "key",
374
+ "company",
375
+ "summary",
376
+ "terms",
377
+ "operation_ref",
378
+ "revision",
379
+ "digest",
380
+ "class",
381
+ "session",
382
+ "only_the_person",
383
+ "was"
384
+ ],
385
+ "pap_confirm_about": "WHATSAPP WA-43 (decided 28 October 2026): a company's pap_confirm (any gateway prefix) is decided in a host, on its exact revision; every other non-mail action stays in Datalayer."
332
386
  },
333
387
  "decision": {
334
388
  "request": {
@@ -788,6 +788,7 @@ export const APP_BEHAVIOURS = {
788
788
  'earthdata.search_earth_datagranules': 'do_it',
789
789
  'earthdata.search_earth_datasets': 'do_it',
790
790
  },
791
+ 'code-explainer': {},
791
792
  'crop-monitoring': {
792
793
  'earthdata.download_earth_data_granules': 'leave_to_me',
793
794
  'earthdata.search_earth_datagranules': 'do_it',
@@ -1011,6 +1012,7 @@ export const APP_ESCALATIONS = {
1011
1012
  accounting: {},
1012
1013
  'bookkeeping-desk': {},
1013
1014
  'change-detection': {},
1015
+ 'code-explainer': {},
1014
1016
  'crop-monitoring': {},
1015
1017
  'customer-interview': {},
1016
1018
  'data-quality': {},
@@ -11,6 +11,7 @@ import type { AppBuilt, AppKind, AppSpec } from '../types/agentspecs';
11
11
  export declare const ACCOUNTING_APP_0_0_2: AppSpec;
12
12
  export declare const BOOKKEEPING_DESK_APP_0_0_2: AppSpec;
13
13
  export declare const CHANGE_DETECTION_APP_0_0_2: AppSpec;
14
+ export declare const CODE_EXPLAINER_APP_0_0_1: AppSpec;
14
15
  export declare const CROP_MONITORING_APP_0_0_2: AppSpec;
15
16
  export declare const CUSTOMER_INTERVIEW_APP_0_0_3: AppSpec;
16
17
  export declare const DATA_QUALITY_APP_0_0_3: AppSpec;