@datalayer/agent-runtimes 1.5.26 → 1.5.28

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 (49) hide show
  1. package/dist-embed/chunks/{ChatView-CUWE6I5M.js → ChatView-C0PWJ_wj.js} +4 -4
  2. package/dist-embed/datalayer-app-main.js +111 -101
  3. package/lib/apps/apps/AppKept.js +5 -1
  4. package/lib/apps/embed/element.js +10 -4
  5. package/lib/apps/embed/embedConfig.d.ts +10 -1
  6. package/lib/apps/embed/embedConfig.js +25 -1
  7. package/lib/apps/plugins/chat/ChatView.js +2 -2
  8. package/lib/chat/assistant/AssistantStage.js +17 -0
  9. package/lib/chat/assistant/clearance.d.ts +10 -0
  10. package/lib/chat/assistant/clearance.js +14 -0
  11. package/lib/chat/base/ChatBase.js +375 -437
  12. package/lib/chat/base/conversationItems.d.ts +55 -0
  13. package/lib/chat/base/conversationItems.js +126 -0
  14. package/lib/portable/adapterEvents.d.ts +107 -0
  15. package/lib/portable/adapterEvents.js +239 -0
  16. package/lib/portable/approvalCard.d.ts +56 -0
  17. package/lib/portable/approvalCard.js +56 -0
  18. package/lib/portable/conversation.d.ts +71 -18
  19. package/lib/portable/conversation.js +330 -74
  20. package/lib/portable/fixtures.js +12 -0
  21. package/lib/portable/index.d.ts +4 -1
  22. package/lib/portable/index.js +7 -1
  23. package/lib/portable/presence.js +4 -1
  24. package/lib/portable/runMeasures.d.ts +46 -0
  25. package/lib/portable/runMeasures.js +156 -0
  26. package/lib/protocols/ACPAdapter.js +13 -8
  27. package/lib/specs/voices.js +47 -0
  28. package/lib/utils/utils.d.ts +2 -8
  29. package/lib/utils/utils.js +3 -26
  30. package/lib/voice/ComposerVoice.js +25 -3
  31. package/lib/voice/VoiceInput.d.ts +2 -2
  32. package/lib/voice/VoiceInput.js +6 -4
  33. package/lib/voice/VoiceMenu.js +2 -1
  34. package/lib/voice/chatVoice.d.ts +4 -3
  35. package/lib/voice/chatVoice.js +6 -9
  36. package/lib/voice/consent.d.ts +8 -0
  37. package/lib/voice/consent.js +10 -0
  38. package/lib/voice/hearing.d.ts +23 -1
  39. package/lib/voice/hearing.js +18 -3
  40. package/lib/voice/index.d.ts +11 -6
  41. package/lib/voice/index.js +1 -0
  42. package/lib/voice/serverHearing.d.ts +58 -0
  43. package/lib/voice/serverHearing.js +136 -0
  44. package/lib/voice/settings.d.ts +5 -0
  45. package/lib/voice/settings.js +8 -0
  46. package/lib/voice/speaker.d.ts +4 -3
  47. package/lib/voice/speaker.js +1 -2
  48. package/lib/voice/useVoiceSpeaker.js +0 -3
  49. package/package.json +2 -2
@@ -0,0 +1,55 @@
1
+ /**
2
+ * What the web chat draws of its conversation (MOBILE M-030): the rows of
3
+ * `reduceConversation`'s state as the `DisplayItem`s `ChatMessageList`
4
+ * renders, with what only the web holds beside them — what a message showed
5
+ * and who said it (Reactor UI ops, REACTOR_UI RU-W), the steps of an
6
+ * application's code (LOOP P-16), and what a message carried that the
7
+ * conversation does not keep (when it was said, a voice's metadata).
8
+ *
9
+ * The conversation is the reducer's; this only draws it. A turn that ended
10
+ * without words and a step of the AG-UI run are not drawn here, as they were
11
+ * not before; a failure is said as the assistant's words.
12
+ *
13
+ * @module chat/base/conversationItems
14
+ */
15
+ import type { Display } from '@datalayer/reactor/ui';
16
+ import type { ConversationItem, ConversationState } from '../../portable/conversation';
17
+ import type { DisplayItem, DisplayToolCallStatus, ToolCallMessage } from '../../types/chat';
18
+ import type { ChatMessage, MessageSpeaker } from '../../types/messages';
19
+ type ToolItem = Extract<ConversationItem, {
20
+ kind: 'tool';
21
+ }>;
22
+ /** What a message carried that the conversation does not keep. */
23
+ export type MessageExtras = Omit<ChatMessage, 'id' | 'role' | 'content'>;
24
+ /** A step of an application's code, drawn after the row that was last when it began. */
25
+ export type StepRow = {
26
+ item: ToolCallMessage;
27
+ after: string | null;
28
+ };
29
+ /** What only the web holds beside the conversation's rows. */
30
+ export type ChatLayer = {
31
+ /** By message id: when it was said, and what else it carried. */
32
+ extras: ReadonlyMap<string, MessageExtras>;
33
+ /** By message id: what it showed (`show`, `ask`, `patch`, `append`). */
34
+ displays: ReadonlyMap<string, Display[]>;
35
+ /** By message id: who said it (`meta`). */
36
+ speakers: ReadonlyMap<string, MessageSpeaker>;
37
+ /** The steps of an application's code, in the order they began. */
38
+ steps: readonly StepRow[];
39
+ };
40
+ export declare const emptyChatLayer: () => ChatLayer;
41
+ /** The id a tool call's card has: its call's, prefixed. */
42
+ export declare const toolCardId: (toolCallId: string) => string;
43
+ /** The conversation row a drawn item is: a card's call, else itself. */
44
+ export declare const rowIdOf: (item: DisplayItem) => string;
45
+ /** A tool call's status, as its card says it. */
46
+ export declare function cardStatusOf(item: ToolItem): DisplayToolCallStatus;
47
+ /** A tool call as its card draws it. */
48
+ export declare function toolCallOf(item: ToolItem): ToolCallMessage;
49
+ /**
50
+ * A drawer of a conversation: each row drawn once, and drawn again only when
51
+ * it, or what the web holds beside it, changed — so a message streaming
52
+ * leaves the others as they were, for React.
53
+ */
54
+ export declare function conversationDrawer(errorWords: (text: string) => string): (state: ConversationState, layer: ChatLayer) => DisplayItem[];
55
+ export {};
@@ -0,0 +1,126 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ export const emptyChatLayer = () => ({
6
+ extras: new Map(),
7
+ displays: new Map(),
8
+ speakers: new Map(),
9
+ steps: [],
10
+ });
11
+ /** The id a tool call's card has: its call's, prefixed. */
12
+ export const toolCardId = (toolCallId) => `tool-${toolCallId}`;
13
+ /** The conversation row a drawn item is: a card's call, else itself. */
14
+ export const rowIdOf = (item) => 'toolCallId' in item && item.id === toolCardId(item.toolCallId)
15
+ ? item.toolCallId
16
+ : item.id;
17
+ /** A tool call's status, as its card says it. */
18
+ export function cardStatusOf(item) {
19
+ if (item.status !== 'running') {
20
+ return item.status;
21
+ }
22
+ return item.awaiting === 'approval' ? 'inProgress' : 'executing';
23
+ }
24
+ /** A tool call as its card draws it. */
25
+ export function toolCallOf(item) {
26
+ return {
27
+ id: toolCardId(item.id),
28
+ type: 'tool-call',
29
+ toolCallId: item.id,
30
+ toolName: item.name,
31
+ args: item.input ?? {},
32
+ // Its value; its words when the value was too long to keep.
33
+ ...(item.value !== undefined
34
+ ? { result: item.value }
35
+ : item.result
36
+ ? { result: item.result }
37
+ : {}),
38
+ status: cardStatusOf(item),
39
+ ...(item.error ? { error: item.error } : {}),
40
+ ...(item.executionError ? { executionError: item.executionError } : {}),
41
+ ...(item.codeError
42
+ ? {
43
+ codeError: {
44
+ name: item.codeError.name,
45
+ value: item.codeError.value,
46
+ ...(item.codeError.traceback
47
+ ? { traceback: item.codeError.traceback }
48
+ : {}),
49
+ },
50
+ }
51
+ : {}),
52
+ ...(item.exitCode !== null ? { exitCode: item.exitCode } : {}),
53
+ };
54
+ }
55
+ /**
56
+ * A drawer of a conversation: each row drawn once, and drawn again only when
57
+ * it, or what the web holds beside it, changed — so a message streaming
58
+ * leaves the others as they were, for React.
59
+ */
60
+ export function conversationDrawer(errorWords) {
61
+ const drawn = new Map();
62
+ const message = (row, layer) => {
63
+ const extras = layer.extras.get(row.id);
64
+ const displays = layer.displays.get(row.id);
65
+ const speaker = layer.speakers.get(row.id);
66
+ const kept = drawn.get(row.id);
67
+ if (kept &&
68
+ kept.row === row &&
69
+ kept.extras === extras &&
70
+ kept.displays === displays &&
71
+ kept.speaker === speaker) {
72
+ return kept.item;
73
+ }
74
+ const item = {
75
+ ...extras,
76
+ createdAt: extras?.createdAt ?? new Date(),
77
+ id: row.id,
78
+ role: row.kind === 'message' ? row.role : 'assistant',
79
+ content: row.kind === 'message' ? row.text : errorWords(row.text),
80
+ ...(displays ? { displays } : {}),
81
+ ...(speaker ? { speaker } : {}),
82
+ };
83
+ drawn.set(row.id, { row, extras, displays, speaker, item });
84
+ return item;
85
+ };
86
+ const tool = (row) => {
87
+ const kept = drawn.get(row.id);
88
+ if (kept && kept.row === row) {
89
+ return kept.item;
90
+ }
91
+ const item = toolCallOf(row);
92
+ drawn.set(row.id, { row, item });
93
+ return item;
94
+ };
95
+ return (state, layer) => {
96
+ const items = [];
97
+ const after = new Map();
98
+ for (const step of layer.steps) {
99
+ after.set(step.after, [...(after.get(step.after) ?? []), step.item]);
100
+ }
101
+ items.push(...(after.get(null) ?? []));
102
+ const seen = new Set();
103
+ for (const row of state.items) {
104
+ seen.add(row.id);
105
+ if (row.kind === 'message' || row.kind === 'error') {
106
+ items.push(message(row, layer));
107
+ }
108
+ else if (row.kind === 'tool') {
109
+ items.push(tool(row));
110
+ }
111
+ items.push(...(after.get(row.id) ?? []));
112
+ }
113
+ // A step after a row since taken away: at the end.
114
+ for (const [anchor, steps] of after) {
115
+ if (anchor !== null && !seen.has(anchor)) {
116
+ items.push(...steps);
117
+ }
118
+ }
119
+ for (const id of drawn.keys()) {
120
+ if (!seen.has(id)) {
121
+ drawn.delete(id);
122
+ }
123
+ }
124
+ return items;
125
+ };
126
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * What every protocol adapter says, as the conversation reducer reads it
3
+ * (MOBILE M-030): the web chat's message state is `reduceConversation`'s,
4
+ * whatever protocol its agent speaks.
5
+ *
6
+ * ## What the adapters say (the inventory)
7
+ *
8
+ * Every adapter under `src/protocols` turns its wire into the same
9
+ * `ProtocolEvent`s, and `ChatBase` read only those:
10
+ *
11
+ * | Adapter | Messages | Tool calls | Results | Ends |
12
+ * |---|---|---|---|---|
13
+ * | AG-UI (`AGUIAdapter`) | `message`, its words so far, at each `TEXT_MESSAGE_CONTENT` and at `TEXT_MESSAGE_END`; a fresh id per `TEXT_MESSAGE_START` | `tool-call` at `TOOL_CALL_START` (`{}`, `argsComplete: false`), again at `TOOL_CALL_END` with the parsed arguments | `tool-result` from `TOOL_CALL_RESULT` through `toolResultOf` | `done` at `RUN_FINISHED` with no frontend call pending; `error` at `RUN_ERROR`; `CUSTOM` as `activity`, `STATE_*` as `state-update` |
14
+ * | Vercel AI (`VercelAIAdapter`) | `message`, its words so far, per `text-delta`, again at `finish-step`/`finish`/`[DONE]` | `tool-call` at `tool-input-available` (whole, `argsComplete: true`) or `tool-call` | `tool-result` at `tool-output-available`/`tool-output-error`; a `pending_approval` result for a server call left waiting at `finish` | `done` at `finish`/`[DONE]`; `error` |
15
+ * | A2A (`A2AAdapter`) | `message`, a whole part's text, a fresh id each | — | — | `state-update` with the task's final state; `activity` for data parts (A2UI) |
16
+ * | ACP (`ACPAdapter`) | `message`, its words so far | `tool-call` | `tool-result` | `activity`; `error` |
17
+ * | In-page agent (`BrowserAgentAdapter`) | `message`, its words so far | `tool-call` | `tool-result` | `done`; `error` |
18
+ *
19
+ * Every adapter that runs a frontend tool emits its result too when the page
20
+ * sends it (`sendToolResult`). ChatBase kept, per adapter alike: the messages
21
+ * by id, written over with each message's words so far (sanitised of a
22
+ * model's leaked call markup); the tool calls by id, their arguments merged,
23
+ * their status from the result (failed for an error, a sandbox's execution
24
+ * error, a code error, a non-zero exit code or `execution_ok: false`; still
25
+ * running while `pending_approval` or while the person confirms its steps); a
26
+ * `state-update` completing the newest running call with the state as its
27
+ * result; stop marking running calls interrupted; history restored from the
28
+ * runtime's messages (tool calls and their results merged into cards).
29
+ *
30
+ * ## The normalisation
31
+ *
32
+ * `conversationEventsOf` says each `ProtocolEvent` as the reducer's input: a
33
+ * message as `MESSAGE_SAID`, a call as `TOOL_CALL_SAID`, a result as
34
+ * `TOOL_CALL_DONE`, `done` as `RUN_FINISHED`, `error` as `RUN_ERROR`, an
35
+ * activity as `CUSTOM` (so `loop.session` is the session's here too).
36
+ * `snapshotOfMessages` says a conversation read again as one AG-UI
37
+ * `MESSAGES_SNAPSHOT`, its tool calls and their results with it. Both refuse
38
+ * what is malformed, in a sentence.
39
+ *
40
+ * @module portable/adapterEvents
41
+ */
42
+ import type { AgUiEvent } from './agui';
43
+ import type { ConversationState } from './conversation';
44
+ /** One event of a protocol adapter (`ProtocolEvent`), as far as the conversation reads it. */
45
+ export type AdapterEvent = {
46
+ type: string;
47
+ data?: unknown;
48
+ message?: {
49
+ id?: unknown;
50
+ role?: unknown;
51
+ content?: unknown;
52
+ };
53
+ toolCall?: {
54
+ toolCallId?: unknown;
55
+ toolName?: unknown;
56
+ args?: unknown;
57
+ argsComplete?: boolean;
58
+ };
59
+ toolResult?: {
60
+ toolCallId?: unknown;
61
+ success?: unknown;
62
+ result?: unknown;
63
+ error?: unknown;
64
+ };
65
+ activity?: {
66
+ type: string;
67
+ data: unknown;
68
+ };
69
+ error?: {
70
+ message?: string;
71
+ };
72
+ };
73
+ /** One message of a conversation read again (`ChatMessage`), as far as the conversation reads it. */
74
+ export type SaidMessage = {
75
+ id: string;
76
+ role: string;
77
+ content: unknown;
78
+ toolCalls?: {
79
+ toolCallId: string;
80
+ toolName: string;
81
+ args?: Record<string, unknown>;
82
+ status?: string;
83
+ result?: unknown;
84
+ }[];
85
+ metadata?: Record<string, unknown>;
86
+ };
87
+ /**
88
+ * An answer without the call markup a model sometimes writes into its words
89
+ * (`<calls>`, `<invoke>`, `<parameter>` and their fragments).
90
+ */
91
+ export declare function sanitizeAssistantContent(content: string): string;
92
+ /**
93
+ * One adapter event as the reducer's input, given the conversation it comes
94
+ * to (a `state-update` answers its newest running call). Nothing for what is
95
+ * not the conversation's: a connection's coming and going, a usage count.
96
+ * Throws, in a sentence, for an event that does not say what its type needs.
97
+ */
98
+ export declare function conversationEventsOf(event: AdapterEvent, state: ConversationState): AgUiEvent[];
99
+ /**
100
+ * A conversation read again (the runtime's messages, a thread given, the
101
+ * messages a store kept) as one AG-UI `MESSAGES_SNAPSHOT`: its words, its
102
+ * tool calls and their results, which `role: 'tool'` messages carry (their
103
+ * call's id in `metadata.toolCallId`) or a call carries itself. An answer's
104
+ * words are sanitised as they were when it streamed. Throws for a message by
105
+ * anyone else, and for a call still under way: the past has none.
106
+ */
107
+ export declare function snapshotOfMessages(messages: readonly SaidMessage[]): AgUiEvent;
@@ -0,0 +1,239 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * An answer without the call markup a model sometimes writes into its words
7
+ * (`<calls>`, `<invoke>`, `<parameter>` and their fragments).
8
+ */
9
+ export function sanitizeAssistantContent(content) {
10
+ if (!content || !content.includes('<')) {
11
+ return content;
12
+ }
13
+ let sanitized = content;
14
+ // Remove complete blocks first.
15
+ sanitized = sanitized
16
+ .replace(/<\s*calls\b[\s\S]*?<\s*\/\s*calls\s*>/gi, '')
17
+ .replace(/<\s*(?:an:)?tml:invoke\b[\s\S]*?<\s*\/\s*(?:an:)?tml:invoke\s*>/gi, '')
18
+ .replace(/<\s*(?:an:)?tml:parameter\b[\s\S]*?<\s*\/\s*(?:an:)?tml:parameter\s*>/gi, '');
19
+ // Remove standalone/open/close tags and malformed tag fragments.
20
+ sanitized = sanitized
21
+ .replace(/<\s*\/?\s*(?:an:[a-z_]+|tml:[a-z_]+|calls)\b[^>]*>?/gi, '')
22
+ .replace(/<\s*\/\s*>/g, '')
23
+ .replace(/^[ \t]+$/gm, '')
24
+ .replace(/\n{3,}/g, '\n\n')
25
+ .trim();
26
+ return sanitized;
27
+ }
28
+ const isRecord = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
29
+ /**
30
+ * The call a `state-update` answers: the newest still running that waits on
31
+ * neither an approval nor the person's steps — a tool that answers by setting
32
+ * the shared state (AG-UI `STATE_SNAPSHOT`).
33
+ */
34
+ function stateAnswers(state) {
35
+ for (let index = state.items.length - 1; index >= 0; index -= 1) {
36
+ const item = state.items[index];
37
+ if (item.kind === 'tool' &&
38
+ item.status === 'running' &&
39
+ item.awaiting !== 'approval') {
40
+ // A call the person confirms (its steps) waits on them.
41
+ return item.input && Array.isArray(item.input.steps) ? null : item.id;
42
+ }
43
+ }
44
+ return null;
45
+ }
46
+ /**
47
+ * One adapter event as the reducer's input, given the conversation it comes
48
+ * to (a `state-update` answers its newest running call). Nothing for what is
49
+ * not the conversation's: a connection's coming and going, a usage count.
50
+ * Throws, in a sentence, for an event that does not say what its type needs.
51
+ */
52
+ export function conversationEventsOf(event, state) {
53
+ switch (event.type) {
54
+ case 'connected':
55
+ case 'disconnected':
56
+ return [];
57
+ case 'message': {
58
+ const message = event.message;
59
+ if (message === undefined) {
60
+ // Only its usage: the turn's count, not its words.
61
+ return [];
62
+ }
63
+ if (typeof message.id !== 'string' || !message.id) {
64
+ throw new Error('A message came without its id.');
65
+ }
66
+ if (message.role !== 'user' && message.role !== 'assistant') {
67
+ throw new Error(`Message ${message.id} is said by ${String(message.role)}: only the person or the assistant speaks here.`);
68
+ }
69
+ if (typeof message.content !== 'string') {
70
+ throw new Error(`Message ${message.id} says no words.`);
71
+ }
72
+ return [
73
+ {
74
+ type: 'MESSAGE_SAID',
75
+ messageId: message.id,
76
+ role: message.role,
77
+ text: message.role === 'assistant'
78
+ ? sanitizeAssistantContent(message.content)
79
+ : message.content,
80
+ },
81
+ ];
82
+ }
83
+ case 'tool-call': {
84
+ const call = event.toolCall;
85
+ if (!call ||
86
+ typeof call.toolCallId !== 'string' ||
87
+ !call.toolCallId ||
88
+ typeof call.toolName !== 'string' ||
89
+ !call.toolName) {
90
+ throw new Error('A tool call came without its id and its name.');
91
+ }
92
+ if (!isRecord(call.args)) {
93
+ throw new Error(`Tool call ${call.toolCallId}'s arguments are not an object.`);
94
+ }
95
+ return [
96
+ {
97
+ type: 'TOOL_CALL_SAID',
98
+ toolCallId: call.toolCallId,
99
+ toolCallName: call.toolName,
100
+ args: call.args,
101
+ },
102
+ ];
103
+ }
104
+ case 'tool-result': {
105
+ const result = event.toolResult;
106
+ if (!result ||
107
+ typeof result.toolCallId !== 'string' ||
108
+ !result.toolCallId) {
109
+ throw new Error('A tool result came without the id of its call.');
110
+ }
111
+ if (typeof result.success !== 'boolean') {
112
+ throw new Error(`Tool result of ${result.toolCallId} does not say whether it succeeded.`);
113
+ }
114
+ if (result.error !== undefined && typeof result.error !== 'string') {
115
+ throw new Error(`Tool result of ${result.toolCallId} says its error in no words.`);
116
+ }
117
+ return [
118
+ {
119
+ type: 'TOOL_CALL_DONE',
120
+ toolCallId: result.toolCallId,
121
+ success: result.success,
122
+ result: result.result,
123
+ ...(result.error !== undefined ? { error: result.error } : {}),
124
+ },
125
+ ];
126
+ }
127
+ case 'state-update': {
128
+ const data = event.data;
129
+ const answers = data ? stateAnswers(state) : null;
130
+ if (!answers) {
131
+ return [];
132
+ }
133
+ const shared = isRecord(data) ? data : {};
134
+ return [
135
+ {
136
+ type: 'TOOL_CALL_DONE',
137
+ toolCallId: answers,
138
+ success: true,
139
+ result: shared.weather ?? shared.result ?? shared.toolResult ?? data,
140
+ },
141
+ ];
142
+ }
143
+ case 'activity': {
144
+ if (!event.activity || typeof event.activity.type !== 'string') {
145
+ throw new Error('An activity came without its type.');
146
+ }
147
+ return [
148
+ {
149
+ type: 'CUSTOM',
150
+ name: event.activity.type,
151
+ value: event.activity.data,
152
+ },
153
+ ];
154
+ }
155
+ case 'done':
156
+ return [{ type: 'RUN_FINISHED' }];
157
+ case 'error':
158
+ return [
159
+ { type: 'RUN_ERROR', message: event.error?.message || 'Run failed' },
160
+ ];
161
+ default:
162
+ throw new Error(`An adapter said ${event.type}, which no client reads.`);
163
+ }
164
+ }
165
+ /** A tool call read again, as an AG-UI assistant message says it. */
166
+ function callOf(call, messageId) {
167
+ if (typeof call.toolCallId !== 'string' || !call.toolCallId) {
168
+ throw new Error(`Message ${messageId} holds a tool call without its id.`);
169
+ }
170
+ return {
171
+ id: call.toolCallId,
172
+ type: 'function',
173
+ function: {
174
+ name: call.toolName,
175
+ arguments: JSON.stringify(call.args ?? {}),
176
+ },
177
+ };
178
+ }
179
+ /**
180
+ * A conversation read again (the runtime's messages, a thread given, the
181
+ * messages a store kept) as one AG-UI `MESSAGES_SNAPSHOT`: its words, its
182
+ * tool calls and their results, which `role: 'tool'` messages carry (their
183
+ * call's id in `metadata.toolCallId`) or a call carries itself. An answer's
184
+ * words are sanitised as they were when it streamed. Throws for a message by
185
+ * anyone else, and for a call still under way: the past has none.
186
+ */
187
+ export function snapshotOfMessages(messages) {
188
+ const said = [];
189
+ for (const message of messages) {
190
+ if (message.role === 'tool') {
191
+ const toolCallId = message.metadata?.toolCallId;
192
+ if (typeof toolCallId !== 'string' || !toolCallId) {
193
+ throw new Error(`Tool result ${message.id} does not say which call it answers.`);
194
+ }
195
+ said.push({
196
+ id: message.id,
197
+ role: 'tool',
198
+ toolCallId,
199
+ content: message.content,
200
+ });
201
+ continue;
202
+ }
203
+ if (message.role !== 'user' && message.role !== 'assistant') {
204
+ throw new Error(`Message ${message.id} is said by ${message.role}: only the person, the assistant and its tools speak here.`);
205
+ }
206
+ const words = typeof message.content === 'string'
207
+ ? message.role === 'assistant'
208
+ ? sanitizeAssistantContent(message.content)
209
+ : message.content
210
+ : '';
211
+ const calls = message.role === 'assistant' ? (message.toolCalls ?? []) : [];
212
+ said.push({
213
+ id: message.id,
214
+ role: message.role,
215
+ content: words,
216
+ ...(calls.length
217
+ ? { toolCalls: calls.map(call => callOf(call, message.id)) }
218
+ : {}),
219
+ });
220
+ for (const call of calls) {
221
+ if (call.status !== undefined &&
222
+ call.status !== 'completed' &&
223
+ call.status !== 'failed') {
224
+ throw new Error(`Tool call ${call.toolCallId} is read again ${call.status}: a conversation's past holds no call under way.`);
225
+ }
226
+ if (call.result !== undefined || call.status === 'failed') {
227
+ said.push({
228
+ id: `${call.toolCallId}-result`,
229
+ role: 'tool',
230
+ toolCallId: call.toolCallId,
231
+ content: call.status === 'failed'
232
+ ? { success: false, error: 'failed', result: call.result ?? null }
233
+ : call.result,
234
+ });
235
+ }
236
+ }
237
+ }
238
+ return { type: 'MESSAGES_SNAPSHOT', messages: said };
239
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * An approval as every card says it (MOBILE M-016): the shared controller
3
+ * and prop contract under the web's *Waiting for you* and Datalayer Mobile's
4
+ * Inbox and detail. A message to send is shown whole — its recipients,
5
+ * subject and body — and anything else is every argument the tool gave,
6
+ * complete (the raw fallback), its private `_` markers said apart as *why it
7
+ * asked*. A renderer owns its markup and its buttons, never these words.
8
+ *
9
+ * @module portable/approvalCard
10
+ */
11
+ import { type ApprovalArgs, type MessageAsked } from './approvals';
12
+ /** What every approval card says. */
13
+ export declare const APPROVAL_CARD_WORDS: {
14
+ readonly to: "To";
15
+ readonly cc: "Cc";
16
+ readonly bcc: "Bcc";
17
+ readonly subject: "Subject";
18
+ readonly forwards: "It forwards a message you received.";
19
+ readonly replies: "A reply, in its conversation.";
20
+ readonly arguments: "What it would do";
21
+ readonly why: "Why it asked";
22
+ readonly noArguments: "It gave no arguments.";
23
+ };
24
+ /** An approval's tool and arguments, under either naming. */
25
+ export type ApprovalCardSource = ApprovalArgs & {
26
+ tool_name?: string;
27
+ toolName?: string;
28
+ };
29
+ /** What a card shows of one approval. */
30
+ export type ApprovalCard = {
31
+ /** The tool as an action, in words: `send_email` → `Send email`. */
32
+ action: string;
33
+ /** Why it asked (its Rule or check), or `''`. */
34
+ why: string;
35
+ /** A message to send, shown whole; `null` for anything else. */
36
+ message: MessageAsked | null;
37
+ /** Every non-private argument, complete, when it is not a message. */
38
+ raw: string;
39
+ /** One line for a list row: a message's recipients and subject (never its body), else why. */
40
+ summary: string;
41
+ };
42
+ /** The props of an approval card, on every platform. */
43
+ export type ApprovalCardProps = {
44
+ card: ApprovalCard;
45
+ status: string;
46
+ /** A decision being sent: both buttons wait. */
47
+ busy?: boolean;
48
+ onApprove?: () => void;
49
+ onReject?: () => void;
50
+ };
51
+ /** `send_email` → `Send email`. */
52
+ export declare const actionOf: (toolName: string) => string;
53
+ /** Every argument, complete, as the raw fallback; private `_` markers are said apart as *why*. */
54
+ export declare function argumentsShown(approval: ApprovalCardSource): string;
55
+ /** What a card shows of an approval. */
56
+ export declare function approvalCardOf(approval: ApprovalCardSource): ApprovalCard;
@@ -0,0 +1,56 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * An approval as every card says it (MOBILE M-016): the shared controller
7
+ * and prop contract under the web's *Waiting for you* and Datalayer Mobile's
8
+ * Inbox and detail. A message to send is shown whole — its recipients,
9
+ * subject and body — and anything else is every argument the tool gave,
10
+ * complete (the raw fallback), its private `_` markers said apart as *why it
11
+ * asked*. A renderer owns its markup and its buttons, never these words.
12
+ *
13
+ * @module portable/approvalCard
14
+ */
15
+ import { messageOfApproval, whyAsked, } from './approvals';
16
+ /** What every approval card says. */
17
+ export const APPROVAL_CARD_WORDS = {
18
+ to: 'To',
19
+ cc: 'Cc',
20
+ bcc: 'Bcc',
21
+ subject: 'Subject',
22
+ forwards: 'It forwards a message you received.',
23
+ replies: 'A reply, in its conversation.',
24
+ arguments: 'What it would do',
25
+ why: 'Why it asked',
26
+ noArguments: 'It gave no arguments.',
27
+ };
28
+ const argsOf = (approval) => (approval.tool_args ?? approval.toolArgs ?? {});
29
+ /** `send_email` → `Send email`. */
30
+ export const actionOf = (toolName) => {
31
+ const words = toolName.replace(/[_-]+/g, ' ').trim();
32
+ return words ? words[0].toUpperCase() + words.slice(1) : 'An action';
33
+ };
34
+ /** Every argument, complete, as the raw fallback; private `_` markers are said apart as *why*. */
35
+ export function argumentsShown(approval) {
36
+ const visible = Object.fromEntries(Object.entries(argsOf(approval)).filter(([key]) => !key.startsWith('_')));
37
+ return Object.keys(visible).length === 0
38
+ ? APPROVAL_CARD_WORDS.noArguments
39
+ : JSON.stringify(visible, null, 2);
40
+ }
41
+ /** What a card shows of an approval. */
42
+ export function approvalCardOf(approval) {
43
+ const message = messageOfApproval(approval);
44
+ const why = whyAsked(approval);
45
+ return {
46
+ action: actionOf(String(approval.tool_name ?? approval.toolName ?? '')),
47
+ why,
48
+ message,
49
+ raw: message ? '' : argumentsShown(approval),
50
+ summary: message
51
+ ? message.subject
52
+ ? `${message.to} · ${message.subject}`
53
+ : message.to
54
+ : why,
55
+ };
56
+ }