@datalayer/agent-runtimes 1.3.61 → 1.3.62

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.
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Another application, asked over A2A from an agent whose loop turns in the page.
3
+ *
4
+ * A team of applications says who asks whom (agentspecs `teams`, `talks_to`
5
+ * over `a2a`). The application asked is served on a runtime with fasta2a; the
6
+ * one that asks may run here, in the browser, and asks with the A2A SDK
7
+ * (`@a2a-js/sdk` 1.x) itself, with no wrapper of ours between them:
8
+ *
9
+ * - {@link connectA2APeer} reads the peer's agent card and makes a client
10
+ * from it. The card says where to send a request, what to ask (its skill:
11
+ * description, examples, text in, text out) and that a key is needed.
12
+ * - {@link a2aPeerTool} is the tool the asking agent calls. It takes one
13
+ * request in words, as the skill does, streams the task and returns the
14
+ * peer's answer in words. Each step is told to {@link A2APeerEvent} as it
15
+ * happens, so a page can show the exchange: asked, working, answered.
16
+ *
17
+ * The key goes as a bearer token on every request, the card's too. The
18
+ * runtime hands it to the peer's run, which then reaches the peer's
19
+ * connections with it and with nothing broader.
20
+ *
21
+ * @module runtimes/browser/a2aPeer
22
+ */
23
+ import { type AgentCard, type AgentSkill, type Message, type Part } from '@a2a-js/sdk';
24
+ import { type Client } from '@a2a-js/sdk/client';
25
+ import { type Tool } from 'ai';
26
+ /** Where an A2A agent's card is, under the address it is served at. */
27
+ export declare const AGENT_CARD_PATH = ".well-known/agent-card.json";
28
+ /** A peer reached over A2A: its client, its card, and the skill it is asked for. */
29
+ export type A2APeer = {
30
+ client: Client;
31
+ card: AgentCard;
32
+ skill: AgentSkill;
33
+ };
34
+ export type ConnectA2APeerOptions = {
35
+ /** Where the peer is served: `…/api/v1/a2a/agents/<application id>`. */
36
+ url: string;
37
+ /** A key granted to the peer's route, sent as a bearer token. */
38
+ key?: string;
39
+ /** The fetch to use, for tests and hosts that wrap the network. */
40
+ fetch?: typeof globalThis.fetch;
41
+ };
42
+ /**
43
+ * Read a peer's agent card and make a client from it.
44
+ *
45
+ * The card's first skill is what the peer is asked for: a peer served from an
46
+ * application has one, named for it.
47
+ */
48
+ export declare function connectA2APeer(options: ConnectA2APeerOptions): Promise<A2APeer>;
49
+ /** What happens to one request, as it happens. */
50
+ export type A2APeerEvent = {
51
+ phase: 'asked';
52
+ request: string;
53
+ } | {
54
+ phase: 'working';
55
+ taskId: string;
56
+ note?: string;
57
+ } | {
58
+ phase: 'answered';
59
+ taskId: string;
60
+ answer: string;
61
+ } | {
62
+ phase: 'failed';
63
+ error: string;
64
+ };
65
+ /** The text of a message's or an artifact's parts. */
66
+ export declare function textOf(parts: Part[] | undefined): string;
67
+ /** A user's message of one text part, as the SDK's 1.x types write it. */
68
+ export declare function textMessage(text: string): Message;
69
+ /**
70
+ * Ask a peer once, streaming the task, and return its answer.
71
+ *
72
+ * The answer is the text of the task's artifact, assembled from its chunks,
73
+ * or the final status message's when there is no artifact. A task that ends
74
+ * other than completed is thrown, with what the peer said.
75
+ */
76
+ export declare function askA2APeer(peer: A2APeer, request: string, options?: {
77
+ signal?: AbortSignal;
78
+ onEvent?: (event: A2APeerEvent) => void;
79
+ }): Promise<string>;
80
+ export type A2APeerToolOptions = {
81
+ peer: A2APeer;
82
+ /** Told each step of each request, as it happens. */
83
+ onEvent?: (event: A2APeerEvent) => void;
84
+ };
85
+ /** The tool's description: the peer's skill, and what it may be asked. */
86
+ export declare function peerToolDescription(peer: A2APeer): string;
87
+ /**
88
+ * The tool an agent asks its peer with: a request in, the peer's answer out.
89
+ *
90
+ * A failed request is returned rather than thrown, so the agent can tell the
91
+ * person what the peer said instead of the whole run failing.
92
+ */
93
+ export declare function a2aPeerTool(options: A2APeerToolOptions): Tool;
@@ -0,0 +1,241 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * Another application, asked over A2A from an agent whose loop turns in the page.
7
+ *
8
+ * A team of applications says who asks whom (agentspecs `teams`, `talks_to`
9
+ * over `a2a`). The application asked is served on a runtime with fasta2a; the
10
+ * one that asks may run here, in the browser, and asks with the A2A SDK
11
+ * (`@a2a-js/sdk` 1.x) itself, with no wrapper of ours between them:
12
+ *
13
+ * - {@link connectA2APeer} reads the peer's agent card and makes a client
14
+ * from it. The card says where to send a request, what to ask (its skill:
15
+ * description, examples, text in, text out) and that a key is needed.
16
+ * - {@link a2aPeerTool} is the tool the asking agent calls. It takes one
17
+ * request in words, as the skill does, streams the task and returns the
18
+ * peer's answer in words. Each step is told to {@link A2APeerEvent} as it
19
+ * happens, so a page can show the exchange: asked, working, answered.
20
+ *
21
+ * The key goes as a bearer token on every request, the card's too. The
22
+ * runtime hands it to the peer's run, which then reaches the peer's
23
+ * connections with it and with nothing broader.
24
+ *
25
+ * @module runtimes/browser/a2aPeer
26
+ */
27
+ import { Role, TaskState, } from '@a2a-js/sdk';
28
+ import { ClientFactory, ClientFactoryOptions, DefaultAgentCardResolver, JsonRpcTransportFactory, } from '@a2a-js/sdk/client';
29
+ import { jsonSchema, tool } from 'ai';
30
+ /** Where an A2A agent's card is, under the address it is served at. */
31
+ export const AGENT_CARD_PATH = '.well-known/agent-card.json';
32
+ /** A fetch that sends the key as a bearer token, beside the caller's headers. */
33
+ function withKey(base, key) {
34
+ if (!key) {
35
+ return base;
36
+ }
37
+ return (input, init) => {
38
+ const headers = new Headers(init?.headers);
39
+ headers.set('Authorization', `Bearer ${key}`);
40
+ return base(input, { ...init, headers });
41
+ };
42
+ }
43
+ /**
44
+ * Read a peer's agent card and make a client from it.
45
+ *
46
+ * The card's first skill is what the peer is asked for: a peer served from an
47
+ * application has one, named for it.
48
+ */
49
+ export async function connectA2APeer(options) {
50
+ const base = options.fetch ?? globalThis.fetch.bind(globalThis);
51
+ const fetchImpl = withKey(base, options.key);
52
+ const factory = new ClientFactory(ClientFactoryOptions.createFrom(ClientFactoryOptions.default, {
53
+ transports: [new JsonRpcTransportFactory({ fetchImpl })],
54
+ cardResolver: new DefaultAgentCardResolver({ fetchImpl }),
55
+ }));
56
+ const root = options.url.replace(/\/+$/, '');
57
+ // The full address of the card, with an empty path: the resolver's default
58
+ // path would replace the peer's own path rather than extend it.
59
+ const client = await factory.createFromUrl(`${root}/${AGENT_CARD_PATH}`, '');
60
+ const card = await client.getAgentCard();
61
+ const [skill] = card.skills;
62
+ if (!skill) {
63
+ throw new Error(`${card.name || root} offers no skill on its agent card: there is nothing to ask it.`);
64
+ }
65
+ return { client, card, skill };
66
+ }
67
+ /** The text of a message's or an artifact's parts. */
68
+ export function textOf(parts) {
69
+ return (parts ?? [])
70
+ .map(part => (part.content?.$case === 'text' ? part.content.value : ''))
71
+ .join('');
72
+ }
73
+ /** A user's message of one text part, as the SDK's 1.x types write it. */
74
+ export function textMessage(text) {
75
+ return {
76
+ messageId: globalThis.crypto.randomUUID(),
77
+ contextId: '',
78
+ taskId: '',
79
+ role: Role.ROLE_USER,
80
+ parts: [
81
+ {
82
+ content: { $case: 'text', value: text },
83
+ metadata: undefined,
84
+ filename: '',
85
+ mediaType: '',
86
+ },
87
+ ],
88
+ metadata: undefined,
89
+ extensions: [],
90
+ referenceTaskIds: [],
91
+ };
92
+ }
93
+ /** What a tool call or a tool result says while the peer works, in a few words. */
94
+ function noteOf(message) {
95
+ for (const part of message?.parts ?? []) {
96
+ const content = part.content;
97
+ if (content?.$case === 'text' && content.value.trim()) {
98
+ return content.value;
99
+ }
100
+ if (content?.$case === 'data' && content.value) {
101
+ // What the runtime's A2A worker publishes while its agent works.
102
+ const data = content.value;
103
+ const name = (data.tool_call ?? data.tool_result)?.name;
104
+ if (name) {
105
+ return data.tool_call ? `Calling ${name}` : `Read ${name}`;
106
+ }
107
+ }
108
+ }
109
+ return undefined;
110
+ }
111
+ const ENDED_BADLY = {
112
+ [TaskState.TASK_STATE_FAILED]: 'failed',
113
+ [TaskState.TASK_STATE_CANCELED]: 'was canceled',
114
+ [TaskState.TASK_STATE_REJECTED]: 'was rejected',
115
+ [TaskState.TASK_STATE_AUTH_REQUIRED]: 'asks for a key it was not given',
116
+ [TaskState.TASK_STATE_INPUT_REQUIRED]: 'asks for more than it was told',
117
+ };
118
+ /**
119
+ * Ask a peer once, streaming the task, and return its answer.
120
+ *
121
+ * The answer is the text of the task's artifact, assembled from its chunks,
122
+ * or the final status message's when there is no artifact. A task that ends
123
+ * other than completed is thrown, with what the peer said.
124
+ */
125
+ export async function askA2APeer(peer, request, options = {}) {
126
+ const { signal, onEvent } = options;
127
+ onEvent?.({ phase: 'asked', request });
128
+ try {
129
+ return await streamTask(peer, request, signal, onEvent);
130
+ }
131
+ catch (reason) {
132
+ const error = reason instanceof Error ? reason.message : String(reason);
133
+ onEvent?.({ phase: 'failed', error });
134
+ throw reason;
135
+ }
136
+ }
137
+ async function streamTask(peer, request, signal, onEvent) {
138
+ const artifacts = new Map();
139
+ let taskId = '';
140
+ let final = '';
141
+ let ended;
142
+ const stream = peer.client.sendMessageStream({
143
+ tenant: '',
144
+ message: textMessage(request),
145
+ configuration: undefined,
146
+ metadata: undefined,
147
+ }, { signal });
148
+ for await (const event of stream) {
149
+ const payload = event.payload;
150
+ if (!payload) {
151
+ continue;
152
+ }
153
+ if (payload.$case === 'task') {
154
+ taskId = payload.value.id;
155
+ onEvent?.({ phase: 'working', taskId });
156
+ }
157
+ else if (payload.$case === 'statusUpdate') {
158
+ const status = payload.value.status;
159
+ taskId = payload.value.taskId || taskId;
160
+ if (status?.state === TaskState.TASK_STATE_WORKING) {
161
+ onEvent?.({ phase: 'working', taskId, note: noteOf(status.message) });
162
+ }
163
+ else if (status) {
164
+ ended = status.state;
165
+ final = textOf(status.message?.parts) || final;
166
+ }
167
+ }
168
+ else if (payload.$case === 'artifactUpdate') {
169
+ const artifact = payload.value.artifact;
170
+ if (artifact) {
171
+ const text = textOf(artifact.parts);
172
+ const before = artifacts.get(artifact.artifactId) ?? '';
173
+ // Chunks are appended; the last chunk carries the whole answer again.
174
+ artifacts.set(artifact.artifactId, payload.value.append && !payload.value.lastChunk
175
+ ? before + text
176
+ : text);
177
+ }
178
+ }
179
+ else if (payload.$case === 'message') {
180
+ final = textOf(payload.value.parts);
181
+ ended = TaskState.TASK_STATE_COMPLETED;
182
+ }
183
+ }
184
+ if (ended !== TaskState.TASK_STATE_COMPLETED) {
185
+ const how = (ended !== undefined && ENDED_BADLY[ended]) || 'ended without an answer';
186
+ throw new Error(`${peer.card.name} ${how}${final ? `: ${final}` : '.'}`);
187
+ }
188
+ const answer = [...artifacts.values()].join('\n') || final;
189
+ onEvent?.({ phase: 'answered', taskId, answer });
190
+ return answer;
191
+ }
192
+ /** The input of a peer's tool: one request, in words, as its skill takes it. */
193
+ const REQUEST_SCHEMA = {
194
+ type: 'object',
195
+ properties: {
196
+ request: {
197
+ type: 'string',
198
+ description: 'What to ask, in one request that can be acted on without the rest of this conversation.',
199
+ },
200
+ },
201
+ required: ['request'],
202
+ };
203
+ /** The tool's description: the peer's skill, and what it may be asked. */
204
+ export function peerToolDescription(peer) {
205
+ const { card, skill } = peer;
206
+ const examples = skill.examples.length
207
+ ? ` For example: ${skill.examples.map(example => `"${example}"`).join('; ')}.`
208
+ : '';
209
+ return `Ask ${card.name}, over A2A: ${skill.description}${examples} It answers in words; report its answer as it gave it.`;
210
+ }
211
+ /**
212
+ * The tool an agent asks its peer with: a request in, the peer's answer out.
213
+ *
214
+ * A failed request is returned rather than thrown, so the agent can tell the
215
+ * person what the peer said instead of the whole run failing.
216
+ */
217
+ export function a2aPeerTool(options) {
218
+ const { peer, onEvent } = options;
219
+ return tool({
220
+ description: peerToolDescription(peer),
221
+ inputSchema: jsonSchema(REQUEST_SCHEMA),
222
+ execute: async (input, execution) => {
223
+ const { request } = (input ?? {});
224
+ if (!request?.trim()) {
225
+ return { error: `A request to ${peer.card.name} says what it asks.` };
226
+ }
227
+ try {
228
+ const answer = await askA2APeer(peer, request, {
229
+ signal: execution?.abortSignal,
230
+ onEvent,
231
+ });
232
+ return { answer };
233
+ }
234
+ catch (reason) {
235
+ return {
236
+ error: reason instanceof Error ? reason.message : String(reason),
237
+ };
238
+ }
239
+ },
240
+ });
241
+ }
@@ -642,6 +642,57 @@ export const SERVER_ACTIONS = {
642
642
  * to reproduce.
643
643
  */
644
644
  export const APP_BEHAVIOURS = {
645
+ accounting: {
646
+ 'odoo-accounting.odoo_accounting_aged_balance': 'do_it',
647
+ 'odoo-accounting.odoo_accounting_bank_status': 'do_it',
648
+ 'odoo-accounting.odoo_accounting_book_bank_line': 'leave_to_me',
649
+ 'odoo-accounting.odoo_accounting_check_tax_period': 'do_it',
650
+ 'odoo-accounting.odoo_accounting_create_customer_invoice': 'leave_to_me',
651
+ 'odoo-accounting.odoo_accounting_create_draft_invoice': 'leave_to_me',
652
+ 'odoo-accounting.odoo_accounting_create_draft_journal_entry': 'leave_to_me',
653
+ 'odoo-accounting.odoo_accounting_create_matching_model': 'leave_to_me',
654
+ 'odoo-accounting.odoo_accounting_create_vendor_bill': 'leave_to_me',
655
+ 'odoo-accounting.odoo_accounting_explain_account_balance': 'do_it',
656
+ 'odoo-accounting.odoo_accounting_general_ledger': 'do_it',
657
+ 'odoo-accounting.odoo_accounting_get_asset': 'do_it',
658
+ 'odoo-accounting.odoo_accounting_get_bank_line': 'do_it',
659
+ 'odoo-accounting.odoo_accounting_get_invoice': 'do_it',
660
+ 'odoo-accounting.odoo_accounting_get_journal_entry': 'do_it',
661
+ 'odoo-accounting.odoo_accounting_get_lock_dates': 'do_it',
662
+ 'odoo-accounting.odoo_accounting_get_tax_return': 'do_it',
663
+ 'odoo-accounting.odoo_accounting_get_tax_settings': 'do_it',
664
+ 'odoo-accounting.odoo_accounting_link_bank_feed': 'leave_to_me',
665
+ 'odoo-accounting.odoo_accounting_list_accounts': 'do_it',
666
+ 'odoo-accounting.odoo_accounting_list_assets': 'do_it',
667
+ 'odoo-accounting.odoo_accounting_list_bank_feeds': 'do_it',
668
+ 'odoo-accounting.odoo_accounting_list_bank_lines': 'do_it',
669
+ 'odoo-accounting.odoo_accounting_list_currency_rates': 'do_it',
670
+ 'odoo-accounting.odoo_accounting_list_invoices': 'do_it',
671
+ 'odoo-accounting.odoo_accounting_list_journal_entries': 'do_it',
672
+ 'odoo-accounting.odoo_accounting_list_journals': 'do_it',
673
+ 'odoo-accounting.odoo_accounting_list_open_balances': 'do_it',
674
+ 'odoo-accounting.odoo_accounting_list_partners': 'do_it',
675
+ 'odoo-accounting.odoo_accounting_list_tax_grid_lines': 'do_it',
676
+ 'odoo-accounting.odoo_accounting_list_tax_returns': 'do_it',
677
+ 'odoo-accounting.odoo_accounting_list_taxes': 'do_it',
678
+ 'odoo-accounting.odoo_accounting_list_untagged_tax_lines': 'do_it',
679
+ 'odoo-accounting.odoo_accounting_match_bank_line': 'leave_to_me',
680
+ 'odoo-accounting.odoo_accounting_partner_ledger': 'do_it',
681
+ 'odoo-accounting.odoo_accounting_post_invoice': 'leave_to_me',
682
+ 'odoo-accounting.odoo_accounting_post_journal_entry': 'leave_to_me',
683
+ 'odoo-accounting.odoo_accounting_reconcile_lines': 'leave_to_me',
684
+ 'odoo-accounting.odoo_accounting_remove_duplicate_bank_lines': 'leave_to_me',
685
+ 'odoo-accounting.odoo_accounting_reset_bank_line': 'leave_to_me',
686
+ 'odoo-accounting.odoo_accounting_set_currency_rates': 'leave_to_me',
687
+ 'odoo-accounting.odoo_accounting_set_lock_date': 'leave_to_me',
688
+ 'odoo-accounting.odoo_accounting_set_tax_grids': 'leave_to_me',
689
+ 'odoo-accounting.odoo_accounting_set_tax_periodicity': 'leave_to_me',
690
+ 'odoo-accounting.odoo_accounting_suggest_bank_matches': 'do_it',
691
+ 'odoo-accounting.odoo_accounting_suggest_reconciliations': 'do_it',
692
+ 'odoo-accounting.odoo_accounting_tax_grids': 'do_it',
693
+ 'odoo-accounting.odoo_accounting_trial_balance': 'do_it',
694
+ 'odoo-accounting.odoo_accounting_unreconcile_lines': 'leave_to_me',
695
+ },
645
696
  'customer-interview': {},
646
697
  'data-quality': {},
647
698
  decide: {},
@@ -771,6 +822,7 @@ export const APP_BEHAVIOURS = {
771
822
  'pipeline-report': {},
772
823
  'quote-calculator': {},
773
824
  'report-from-a-file': {},
825
+ sales: {},
774
826
  'ship-or-fix': {},
775
827
  'supplier-comparison': {},
776
828
  'support-desk': {},
@@ -787,6 +839,7 @@ export const APP_BEHAVIOURS = {
787
839
  * application and tool, each condition and the behaviour when it holds.
788
840
  */
789
841
  export const APP_ESCALATIONS = {
842
+ accounting: {},
790
843
  'customer-interview': {},
791
844
  'data-quality': {},
792
845
  decide: {},
@@ -828,6 +881,7 @@ export const APP_ESCALATIONS = {
828
881
  'pipeline-report': {},
829
882
  'quote-calculator': {},
830
883
  'report-from-a-file': {},
884
+ sales: {},
831
885
  'ship-or-fix': {},
832
886
  'supplier-comparison': {},
833
887
  'support-desk': {},
@@ -8,6 +8,7 @@
8
8
  * DO NOT EDIT MANUALLY - run 'make specs' to regenerate.
9
9
  */
10
10
  import type { AppBuilt, AppKind, AppSpec } from '../types/agentspecs';
11
+ export declare const ACCOUNTING_APP_0_0_1: AppSpec;
11
12
  export declare const CUSTOMER_INTERVIEW_APP_0_0_1: AppSpec;
12
13
  export declare const DATA_QUALITY_APP_0_0_1: AppSpec;
13
14
  export declare const DECIDE_APP_0_0_1: AppSpec;
@@ -16,6 +17,7 @@ export declare const MODEL_CHOICE_APP_0_0_1: AppSpec;
16
17
  export declare const PIPELINE_REPORT_APP_0_0_1: AppSpec;
17
18
  export declare const QUOTE_CALCULATOR_APP_0_0_1: AppSpec;
18
19
  export declare const REPORT_FROM_A_FILE_APP_0_0_1: AppSpec;
20
+ export declare const SALES_APP_0_0_1: AppSpec;
19
21
  export declare const SHIP_OR_FIX_APP_0_0_1: AppSpec;
20
22
  export declare const SUPPLIER_COMPARISON_APP_0_0_1: AppSpec;
21
23
  export declare const SUPPORT_DESK_APP_0_0_1: AppSpec;