@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.
- package/dist-embed/chunks/{ChatView-CUWE6I5M.js → ChatView-C0PWJ_wj.js} +4 -4
- package/dist-embed/datalayer-app-main.js +111 -101
- package/lib/apps/apps/AppKept.js +5 -1
- package/lib/apps/embed/element.js +10 -4
- package/lib/apps/embed/embedConfig.d.ts +10 -1
- package/lib/apps/embed/embedConfig.js +25 -1
- package/lib/apps/plugins/chat/ChatView.js +2 -2
- package/lib/chat/assistant/AssistantStage.js +17 -0
- package/lib/chat/assistant/clearance.d.ts +10 -0
- package/lib/chat/assistant/clearance.js +14 -0
- package/lib/chat/base/ChatBase.js +375 -437
- package/lib/chat/base/conversationItems.d.ts +55 -0
- package/lib/chat/base/conversationItems.js +126 -0
- package/lib/portable/adapterEvents.d.ts +107 -0
- package/lib/portable/adapterEvents.js +239 -0
- package/lib/portable/approvalCard.d.ts +56 -0
- package/lib/portable/approvalCard.js +56 -0
- package/lib/portable/conversation.d.ts +71 -18
- package/lib/portable/conversation.js +330 -74
- package/lib/portable/fixtures.js +12 -0
- package/lib/portable/index.d.ts +4 -1
- package/lib/portable/index.js +7 -1
- package/lib/portable/presence.js +4 -1
- package/lib/portable/runMeasures.d.ts +46 -0
- package/lib/portable/runMeasures.js +156 -0
- package/lib/protocols/ACPAdapter.js +13 -8
- package/lib/specs/voices.js +47 -0
- package/lib/utils/utils.d.ts +2 -8
- package/lib/utils/utils.js +3 -26
- package/lib/voice/ComposerVoice.js +25 -3
- package/lib/voice/VoiceInput.d.ts +2 -2
- package/lib/voice/VoiceInput.js +6 -4
- package/lib/voice/VoiceMenu.js +2 -1
- package/lib/voice/chatVoice.d.ts +4 -3
- package/lib/voice/chatVoice.js +6 -9
- package/lib/voice/consent.d.ts +8 -0
- package/lib/voice/consent.js +10 -0
- package/lib/voice/hearing.d.ts +23 -1
- package/lib/voice/hearing.js +18 -3
- package/lib/voice/index.d.ts +11 -6
- package/lib/voice/index.js +1 -0
- package/lib/voice/serverHearing.d.ts +58 -0
- package/lib/voice/serverHearing.js +136 -0
- package/lib/voice/settings.d.ts +5 -0
- package/lib/voice/settings.js +8 -0
- package/lib/voice/speaker.d.ts +4 -3
- package/lib/voice/speaker.js +1 -2
- package/lib/voice/useVoiceSpeaker.js +0 -3
- 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
|
+
}
|