@datalayer/agent-runtimes 1.3.95 → 1.3.96

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.
@@ -55,6 +55,18 @@ export type SandboxTargetSpec = {
55
55
  };
56
56
  /** The four, in the order the control shows them: nearest first. */
57
57
  export declare const SANDBOX_TARGETS: readonly SandboxTarget[];
58
+ /**
59
+ * Why a target is shown but not offered here — said on it — or nothing when
60
+ * it is offered.
61
+ *
62
+ * *Local* is a local agent with the Jupyter server it starts beside itself,
63
+ * and a web page has no machine of the reader's to start them on. Decided
64
+ * 2026-10-10 (Eric): not offered on the web for now, and left in the switch,
65
+ * held, so that a reader sees what exists and why it is not here. Inside
66
+ * JupyterLab it is offered as before — the same line the code-sandbox picker
67
+ * draws for a Jupyter Server's kernels (`codeSandboxProviders`).
68
+ */
69
+ export declare function heldTargetReason(target: SandboxTarget): string | undefined;
58
70
  export declare const TARGET_SPECS: Record<SandboxTarget, SandboxTargetSpec>;
59
71
  /** Whether an agent runs on this target — the question the chat asks. */
60
72
  export declare function targetHasAgent(target: SandboxTarget): boolean;
@@ -22,6 +22,7 @@
22
22
  * @module apps/plugins/agents/switchable
23
23
  */
24
24
  import { computed, signal, } from '@datalayer/reactor';
25
+ import { insideJupyterLab } from './host';
25
26
  import { browserSource, createBrowserSandboxService, } from './browserService';
26
27
  import { createServerSandboxService, } from './service';
27
28
  /**
@@ -40,6 +41,23 @@ export const SANDBOX_TARGETS = [
40
41
  'jupyter',
41
42
  'datalayer',
42
43
  ];
44
+ /**
45
+ * Why a target is shown but not offered here — said on it — or nothing when
46
+ * it is offered.
47
+ *
48
+ * *Local* is a local agent with the Jupyter server it starts beside itself,
49
+ * and a web page has no machine of the reader's to start them on. Decided
50
+ * 2026-10-10 (Eric): not offered on the web for now, and left in the switch,
51
+ * held, so that a reader sees what exists and why it is not here. Inside
52
+ * JupyterLab it is offered as before — the same line the code-sandbox picker
53
+ * draws for a Jupyter Server's kernels (`codeSandboxProviders`).
54
+ */
55
+ export function heldTargetReason(target) {
56
+ if (target === 'local' && !insideJupyterLab()) {
57
+ return 'Not offered on the web for now: a local agent and the Jupyter server it starts run beside JupyterLab, not in a page.';
58
+ }
59
+ return undefined;
60
+ }
43
61
  export const TARGET_SPECS = {
44
62
  browser: {
45
63
  label: 'Browser',
@@ -298,6 +316,11 @@ export function createSwitchableSandboxService({ serverUrl, initialTarget = 'loc
298
316
  if (next === previous) {
299
317
  return;
300
318
  }
319
+ // Held here: refused whoever asks, not only by the switch's button.
320
+ const held = heldTargetReason(next);
321
+ if (held) {
322
+ throw new Error(held);
323
+ }
301
324
  // Disconnect first: two live sandboxes would race to report status, and
302
325
  // the reader would watch the header flicker between them.
303
326
  disconnect?.();
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The agent's toolbox, contributed (LOOP C-20): one plugin per source of
3
+ * parts — the servers of the catalogue a connection reaches, its skills, its
4
+ * models, and the rules, tests and events an agent answers to — each
5
+ * contributing to `loop.canvas.part`. The toolbox is what the enabled
6
+ * plugins contribute, as the palette is (`canvas-blocks`, C-12).
7
+ *
8
+ * @module apps/plugins/canvas-parts
9
+ */
10
+ import { type ReactorPlatform, type ReactorPlugin } from '@datalayer/reactor';
11
+ import { LoopCanvasPart, type CanvasPartContribution } from '../../core/canvasParts';
12
+ type PartsReader = {
13
+ getContributions: (point: typeof LoopCanvasPart) => ReadonlyArray<{
14
+ value: CanvasPartContribution;
15
+ }>;
16
+ };
17
+ /** The short ids an organization turns off, and the plugin each names. */
18
+ export declare const CANVAS_PART_SOURCES: readonly ["connections", "skills", "models", "rules", "tests", "events"];
19
+ export type CanvasPartSource = (typeof CANVAS_PART_SOURCES)[number];
20
+ export declare const canvasPartsPluginName: (source: CanvasPartSource) => string;
21
+ /** A connection to each server of the catalogue: read-only, as its owner, every tool. */
22
+ export declare const CONNECTION_PARTS: CanvasPartContribution[];
23
+ /** Each skill of the catalogue. */
24
+ export declare const SKILL_PARTS: CanvasPartContribution[];
25
+ /** Each chat model of the catalogue. */
26
+ export declare const MODEL_PARTS: CanvasPartContribution[];
27
+ /** The rules a person most often sets, each one of the four behaviours. */
28
+ export declare const RULE_PARTS: CanvasPartContribution[];
29
+ /** A test: a question, and what a good answer does. */
30
+ export declare const TEST_PARTS: CanvasPartContribution[];
31
+ /** The events an application answers (C-21): on a schedule, on an event, once. */
32
+ export declare const EVENT_PARTS: CanvasPartContribution[];
33
+ export declare const CANVAS_PART_PLUGINS: ReactorPlugin<Record<string, never>, unknown, unknown>[];
34
+ /** The toolbox's reactor, its named plugins disabled; a name no plugin has is refused. */
35
+ export declare function canvasPartsReactor(disabled?: readonly string[], plugins?: ReactorPlugin<any, any, any>[]): ReactorPlatform;
36
+ /**
37
+ * The toolbox for an organization's plugins off: the ids it turns off that
38
+ * name a source of parts (`skills`, …) take those parts off; the rest are the
39
+ * palette's.
40
+ */
41
+ export declare function canvasPartsReactorOf(pluginsOff: readonly string[]): ReactorPlatform;
42
+ /** What the toolbox offers: the parts the enabled plugins contribute, in their order. */
43
+ export declare function canvasPartsOf(reactor: PartsReader): CanvasPartContribution[];
44
+ export {};
@@ -0,0 +1,209 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * The agent's toolbox, contributed (LOOP C-20): one plugin per source of
7
+ * parts — the servers of the catalogue a connection reaches, its skills, its
8
+ * models, and the rules, tests and events an agent answers to — each
9
+ * contributing to `loop.canvas.part`. The toolbox is what the enabled
10
+ * plugins contribute, as the palette is (`canvas-blocks`, C-12).
11
+ *
12
+ * @module apps/plugins/canvas-parts
13
+ */
14
+ import { buildReactorFromPlugins, contribution, definePlugin, } from '@datalayer/reactor';
15
+ import { MCP_SERVER_LIBRARY } from '../../../specs/mcpServers';
16
+ import { listChatModels } from '../../../specs/models';
17
+ import { getSkillSpecs } from '../../../specs/skills';
18
+ import { LoopCanvasPart, } from '../../core/canvasParts';
19
+ /** The short ids an organization turns off, and the plugin each names. */
20
+ export const CANVAS_PART_SOURCES = [
21
+ 'connections',
22
+ 'skills',
23
+ 'models',
24
+ 'rules',
25
+ 'tests',
26
+ 'events',
27
+ ];
28
+ export const canvasPartsPluginName = (source) => `@datalayer/loop-plugin-parts-${source}`;
29
+ const partsPlugin = (source, displayName, parts) => definePlugin({
30
+ name: canvasPartsPluginName(source),
31
+ displayName,
32
+ description: `${displayName} in the agent's toolbox on the Canvas.`,
33
+ octicon: 'package',
34
+ contributes: parts.map(part => contribution(LoopCanvasPart, part, { id: part.id })),
35
+ });
36
+ /** A connection to each server of the catalogue: read-only, as its owner, every tool. */
37
+ export const CONNECTION_PARTS = Object.values(MCP_SERVER_LIBRARY).map(server => ({
38
+ id: `connection:${server.id}`,
39
+ kind: 'connection',
40
+ label: server.name,
41
+ says: server.description ?? '',
42
+ emoji: server.emoji,
43
+ example: { server: server.id, access: 'read', as: 'owner', only: [] },
44
+ enabled: server.enabled,
45
+ }));
46
+ /** Each skill of the catalogue. */
47
+ export const SKILL_PARTS = getSkillSpecs().map(skill => ({
48
+ id: `skill:${skill.id}`,
49
+ kind: 'skill',
50
+ label: skill.name,
51
+ says: skill.description,
52
+ emoji: skill.emoji,
53
+ example: skill.id,
54
+ enabled: skill.enabled,
55
+ }));
56
+ /** Each chat model of the catalogue. */
57
+ export const MODEL_PARTS = listChatModels().map(model => ({
58
+ id: `model:${model.id}`,
59
+ kind: 'model',
60
+ label: model.name,
61
+ says: model.description,
62
+ example: model.id,
63
+ // A model's `available` says whether its key is set where the catalogue
64
+ // was read, not whether it may be chosen: every chat model may.
65
+ enabled: true,
66
+ }));
67
+ /** The rules a person most often sets, each one of the four behaviours. */
68
+ export const RULE_PARTS = [
69
+ {
70
+ id: 'rule:ask-before-sending',
71
+ kind: 'rule',
72
+ label: 'Ask before sending',
73
+ says: 'It asks you before it sends anything on your behalf.',
74
+ example: {
75
+ action: 'Send on my behalf',
76
+ applies_to: ['send'],
77
+ behaviour: 'ask_first',
78
+ },
79
+ enabled: true,
80
+ },
81
+ {
82
+ id: 'rule:read-freely',
83
+ kind: 'rule',
84
+ label: 'Read freely',
85
+ says: 'It reads what its connections reach without asking.',
86
+ example: { action: 'Read', applies_to: ['read'], behaviour: 'do_it' },
87
+ enabled: true,
88
+ },
89
+ {
90
+ id: 'rule:never-delete',
91
+ kind: 'rule',
92
+ label: 'Leave deleting to me',
93
+ says: 'It never deletes: it tells you what it would.',
94
+ example: {
95
+ action: 'Delete',
96
+ applies_to: ['delete'],
97
+ behaviour: 'leave_to_me',
98
+ },
99
+ enabled: true,
100
+ },
101
+ {
102
+ id: 'rule:buy-if-asked',
103
+ kind: 'rule',
104
+ label: 'Buy only when asked',
105
+ says: 'It buys only when you ask it to, in so many words.',
106
+ example: { action: 'Buy', applies_to: ['buy'], behaviour: 'if_asked' },
107
+ enabled: true,
108
+ },
109
+ ];
110
+ /** A test: a question, and what a good answer does. */
111
+ export const TEST_PARTS = [
112
+ {
113
+ id: 'test:answers-from-sources',
114
+ kind: 'test',
115
+ label: 'Answers from its sources',
116
+ says: 'A question, and what a good answer does.',
117
+ example: {
118
+ ask: 'What is due this week?',
119
+ expect: 'It lists what is due this week, read from its sources, and says where.',
120
+ },
121
+ enabled: true,
122
+ },
123
+ {
124
+ id: 'test:says-when-it-cannot',
125
+ kind: 'test',
126
+ label: 'Says when it cannot',
127
+ says: 'A question it cannot answer, and it says so.',
128
+ example: {
129
+ ask: 'What will the price be next year?',
130
+ expect: 'It says it cannot know, and does not guess.',
131
+ },
132
+ enabled: true,
133
+ },
134
+ ];
135
+ /** The events an application answers (C-21): on a schedule, on an event, once. */
136
+ export const EVENT_PARTS = [
137
+ {
138
+ id: 'event:schedule',
139
+ kind: 'event',
140
+ label: 'On a schedule',
141
+ says: 'Every weekday at 9:00, or any time a schedule says.',
142
+ example: {
143
+ type: 'schedule',
144
+ cron: '0 9 * * 1-5',
145
+ description: 'Every weekday at 9:00',
146
+ prompt: 'Summarise what came in since yesterday.',
147
+ },
148
+ enabled: true,
149
+ },
150
+ {
151
+ id: 'event:email-received',
152
+ kind: 'event',
153
+ label: 'When an email arrives',
154
+ says: 'A message arriving in the inbox it reads.',
155
+ example: {
156
+ type: 'event',
157
+ event: 'email_received',
158
+ prompt: 'Read the email, and draft a reply for me to approve.',
159
+ },
160
+ enabled: true,
161
+ },
162
+ {
163
+ id: 'event:once',
164
+ kind: 'event',
165
+ label: 'Once, at a moment',
166
+ says: 'One run, at a date and a time.',
167
+ example: {
168
+ type: 'once',
169
+ at: '2026-12-31T09:00:00',
170
+ prompt: 'Close the year: list what is still open.',
171
+ },
172
+ enabled: true,
173
+ },
174
+ ];
175
+ export const CANVAS_PART_PLUGINS = [
176
+ partsPlugin('connections', 'Connections', CONNECTION_PARTS),
177
+ partsPlugin('skills', 'Skills', SKILL_PARTS),
178
+ partsPlugin('models', 'Models', MODEL_PARTS),
179
+ partsPlugin('rules', 'Rules', RULE_PARTS),
180
+ partsPlugin('tests', 'Tests', TEST_PARTS),
181
+ partsPlugin('events', 'Events', EVENT_PARTS),
182
+ ];
183
+ /** The toolbox's reactor, its named plugins disabled; a name no plugin has is refused. */
184
+ export function canvasPartsReactor(disabled = [], plugins = CANVAS_PART_PLUGINS) {
185
+ const reactor = buildReactorFromPlugins(plugins);
186
+ reactor.start();
187
+ for (const name of disabled) {
188
+ if (!reactor.hasPlugin(name)) {
189
+ throw new Error(`No plugin of the toolbox is named "${name}"; they are ${plugins.map(plugin => plugin.name).join(', ') || 'none'}.`);
190
+ }
191
+ reactor.disable(name);
192
+ }
193
+ return reactor;
194
+ }
195
+ /**
196
+ * The toolbox for an organization's plugins off: the ids it turns off that
197
+ * name a source of parts (`skills`, …) take those parts off; the rest are the
198
+ * palette's.
199
+ */
200
+ export function canvasPartsReactorOf(pluginsOff) {
201
+ const sources = new Set(CANVAS_PART_SOURCES);
202
+ return canvasPartsReactor(pluginsOff
203
+ .filter(id => sources.has(id))
204
+ .map(id => canvasPartsPluginName(id)));
205
+ }
206
+ /** What the toolbox offers: the parts the enabled plugins contribute, in their order. */
207
+ export function canvasPartsOf(reactor) {
208
+ return reactor.getContributions(LoopCanvasPart).map(entry => entry.value);
209
+ }
@@ -3,6 +3,7 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
  import { useCallback, useEffect, useState } from 'react';
6
+ import { agUiRefusal, refusalBody } from '../protocols/agUiRefusal';
6
7
  /**
7
8
  * Hook to manage chat state with AG-UI protocol.
8
9
  *
@@ -94,7 +95,9 @@ export function useAgUi(options = {}) {
94
95
  body: JSON.stringify(requestBody),
95
96
  });
96
97
  if (!response.ok) {
97
- throw new Error(`AG-UI request failed: ${response.status} ${response.statusText}`);
98
+ // Why, in the page's own words (F-15): a 404 at an agent's AG-UI
99
+ // address means the agent is not on this runtime.
100
+ throw new Error(agUiRefusal(response.status, response.statusText, await refusalBody(response)));
98
101
  }
99
102
  // Handle streaming response
100
103
  const reader = response.body?.getReader();
@@ -3,6 +3,7 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
  import { generateMessageId, createAssistantMessage } from '../types/messages';
6
+ import { agUiRefusal, refusalBody } from './agUiRefusal';
6
7
  import { BaseProtocolAdapter } from './BaseProtocolAdapter';
7
8
  /**
8
9
  * AG-UI protocol adapter
@@ -214,7 +215,10 @@ export class AGUIAdapter extends BaseProtocolAdapter {
214
215
  signal: controller.signal,
215
216
  });
216
217
  if (!response.ok) {
217
- throw new Error(`AG-UI request failed: ${response.status} ${response.statusText}`);
218
+ // Why, in the page's own words (F-15): a 404 at an agent's AG-UI
219
+ // address means the agent is not on this runtime, which the status
220
+ // alone never said.
221
+ throw new Error(agUiRefusal(response.status, response.statusText, await refusalBody(response)));
218
222
  }
219
223
  // Parse SSE stream
220
224
  await this.parseSSEStream(response);
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Why an AG-UI run was not answered, in the page's own words (LOOP F-15).
3
+ *
4
+ * What a person met before was `AG-UI request failed: 404` — the status and
5
+ * nothing else. A 404 at an agent's AG-UI address has one meaning: the agent
6
+ * is not on the runtime the page is addressing. The commonest way to get
7
+ * there is to open an application whose agent runs its code in the browser:
8
+ * the runtime refuses to start it (`browser_sandbox_refusal`, 422), the page
9
+ * addresses it anyway, and the address answers nothing. So the page says
10
+ * that, and says what to do about it.
11
+ *
12
+ * When the server said why — a refusal of the platform's own, carried as
13
+ * `detail` — that sentence is the better one and is used instead: it is
14
+ * about this agent, where ours is about any.
15
+ *
16
+ * Pure, so the hook, the adapter and the tests read a refusal the same way
17
+ * (the repo's jest/vitest rule: rules live outside components).
18
+ *
19
+ * @module protocols/agUiRefusal
20
+ */
21
+ /** What the page says when an agent's AG-UI address answers nothing. */
22
+ export declare const AG_UI_NOT_ON_RUNTIME: string;
23
+ /** What the page says when the address refused the caller. */
24
+ export declare const AG_UI_NOT_ALLOWED: string;
25
+ /**
26
+ * The `detail` a platform refusal carries, or `''`.
27
+ *
28
+ * A body that is not JSON — an HTML 404 from the page's own origin, which is
29
+ * what a page addressing a runtime it never got answers — carries none.
30
+ */
31
+ export declare function refusalDetail(body: string | undefined): string;
32
+ /**
33
+ * Why the run was not answered, as the page says it.
34
+ *
35
+ * @param status - The status the address answered with.
36
+ * @param statusText - Its text, for a status we have nothing better for.
37
+ * @param body - What came back, when it was read; a refusal's `detail` wins.
38
+ */
39
+ export declare function agUiRefusal(status: number, statusText: string, body?: string): string;
40
+ /**
41
+ * Read a refusal's body without letting the reading itself throw: a body
42
+ * already consumed, or a connection that went away, leaves us with the
43
+ * status alone, which is still better than nothing.
44
+ */
45
+ export declare function refusalBody(response: {
46
+ text: () => Promise<string>;
47
+ }): Promise<string | undefined>;
@@ -0,0 +1,87 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * Why an AG-UI run was not answered, in the page's own words (LOOP F-15).
7
+ *
8
+ * What a person met before was `AG-UI request failed: 404` — the status and
9
+ * nothing else. A 404 at an agent's AG-UI address has one meaning: the agent
10
+ * is not on the runtime the page is addressing. The commonest way to get
11
+ * there is to open an application whose agent runs its code in the browser:
12
+ * the runtime refuses to start it (`browser_sandbox_refusal`, 422), the page
13
+ * addresses it anyway, and the address answers nothing. So the page says
14
+ * that, and says what to do about it.
15
+ *
16
+ * When the server said why — a refusal of the platform's own, carried as
17
+ * `detail` — that sentence is the better one and is used instead: it is
18
+ * about this agent, where ours is about any.
19
+ *
20
+ * Pure, so the hook, the adapter and the tests read a refusal the same way
21
+ * (the repo's jest/vitest rule: rules live outside components).
22
+ *
23
+ * @module protocols/agUiRefusal
24
+ */
25
+ /** What the page says when an agent's AG-UI address answers nothing. */
26
+ export const AG_UI_NOT_ON_RUNTIME = 'This agent is not on the runtime the page is addressing: its AG-UI address answered nothing. ' +
27
+ 'An agent that runs its code in the browser is never started on a runtime — run it in the page, ' +
28
+ 'or give it a sandbox a runtime has (eval, jupyter-server).';
29
+ /** What the page says when the address refused the caller. */
30
+ export const AG_UI_NOT_ALLOWED = 'This agent did not answer the page: the run was refused. ' +
31
+ 'Sign in again, or open the application from its own address.';
32
+ /**
33
+ * The `detail` a platform refusal carries, or `''`.
34
+ *
35
+ * A body that is not JSON — an HTML 404 from the page's own origin, which is
36
+ * what a page addressing a runtime it never got answers — carries none.
37
+ */
38
+ export function refusalDetail(body) {
39
+ if (!body) {
40
+ return '';
41
+ }
42
+ let said;
43
+ try {
44
+ said = JSON.parse(body);
45
+ }
46
+ catch {
47
+ return '';
48
+ }
49
+ if (!said || typeof said !== 'object') {
50
+ return '';
51
+ }
52
+ const detail = said.detail;
53
+ return typeof detail === 'string' ? detail.trim() : '';
54
+ }
55
+ /**
56
+ * Why the run was not answered, as the page says it.
57
+ *
58
+ * @param status - The status the address answered with.
59
+ * @param statusText - Its text, for a status we have nothing better for.
60
+ * @param body - What came back, when it was read; a refusal's `detail` wins.
61
+ */
62
+ export function agUiRefusal(status, statusText, body) {
63
+ const detail = refusalDetail(body);
64
+ if (detail) {
65
+ return detail;
66
+ }
67
+ if (status === 404) {
68
+ return AG_UI_NOT_ON_RUNTIME;
69
+ }
70
+ if (status === 401 || status === 403) {
71
+ return AG_UI_NOT_ALLOWED;
72
+ }
73
+ return `AG-UI request failed: ${status} ${statusText}`.trim();
74
+ }
75
+ /**
76
+ * Read a refusal's body without letting the reading itself throw: a body
77
+ * already consumed, or a connection that went away, leaves us with the
78
+ * status alone, which is still better than nothing.
79
+ */
80
+ export async function refusalBody(response) {
81
+ try {
82
+ return await response.text();
83
+ }
84
+ catch {
85
+ return undefined;
86
+ }
87
+ }
package/lib/specs/apps.js CHANGED
@@ -893,7 +893,7 @@ export const DISASTER_ASSESSMENT_APP_0_0_1 = {
893
893
  owner: 'Datalayer <info@datalayer.io>',
894
894
  agent: 'worker-disaster-assessment:0.0.1',
895
895
  team: '',
896
- instructions: 'You assess disasters from satellite imagery. Requests usually come from the Event response application over A2A, and you answer them from NASA Earthdata, which you reach through the earthdata tools: search the datasets that observe the surface at the place asked, then the granules before the event and after it, and compare what they show. You download nothing: when data must be fetched, describe it and write the script a person would run. Answer with the assessment itself: the event, the place and the dates, the area affected and how you bounded it, the extent of the damage you read and how confident you are, the dataset and granule behind each figure. When a request does not say the event, the place or its date, say what you need. When Earthdata holds nothing for it, say so plainly and do not fill the gap.',
896
+ instructions: 'You assess disasters from satellite imagery. Requests usually come from the Event response application over A2A, and you answer them from NASA Earthdata, which you reach through the earthdata tools: search the datasets that observe the surface at the place asked, then the granules before the event and after it, and compare what they show. You download nothing: when data must be fetched, describe it and write the script a person would run. Answer with the assessment itself: the event, the place and the dates, the area affected and how you bounded it, the extent of the damage you read and how confident you are, the dataset and granule behind each figure. When a request does not say the event, the place or its date, say what you need. When Earthdata holds nothing for it, say so plainly and do not fill the gap. You send nothing yourself. Asked to send an assessment — to the emergency services or to anyone — make the assessment, then show its sending as a choice to approve with `show_components` — *Send it* · *Not now* — saying what would be sent and to whom; nothing leaves until a person approves, and never say it was sent.',
897
897
  model: '',
898
898
  skills: [],
899
899
  backendTools: [],
@@ -1045,7 +1045,7 @@ export const EVENT_RESPONSE_APP_0_0_1 = {
1045
1045
  owner: 'Datalayer <info@datalayer.io>',
1046
1046
  agent: 'worker-event-response:0.0.1',
1047
1047
  team: '',
1048
- instructions: 'You respond to events. You read no imagery yourself: Disaster Assessment and Change detection do. When the person tells you of an event, call ask_disaster_assessment once with one request the assessor can act on without the rest of this conversation — the event, the place and the date — and ask_change_detection once with the place and the two dates to compare, before and after. Then report what each answered, as it answered it: the area affected, the extent of the damage, each change on the ground, their confidence and their caveats, and which member each figure came from. Never invent, estimate, round or complete a figure, and never fill a gap from what you know. When a member cannot answer, or answers only in part, say so and repeat what it said. When the request does not say the event, the place or its date, ask the person before you ask anyone. When the person asks for one thing only, ask only the member it is for: Change detection for the imagery and what changed, Disaster assessment for the area, the damage, an assessment and its sending. Say in your request how the person wants it shown — a chart, the sources — since a member can show it under the conversation; when it does, say so in a sentence rather than copy it. Never say a thing was sent or done unless the member says it was. You change nothing anywhere: you ask, and you report.',
1048
+ instructions: "You respond to events. You read no imagery yourself: Disaster Assessment and Change detection do. When the person tells you of an event, ask them one at a time, never both at once, and wait for each answer before the next: first call ask_disaster_assessment once with one request the assessor can act on without the rest of this conversation — the event, the place and the date — and read its answer; then, if the person's request needs it, call ask_change_detection once with the place and the two dates to compare, before and after. Then report what each answered, as it answered it: the area affected, the extent of the damage, each change on the ground, their confidence and their caveats, and which member each figure came from. Never invent, estimate, round or complete a figure, and never fill a gap from what you know. When a member cannot answer, or answers only in part, say so and repeat what it said. **Nobody is standing there to answer you back: never ask a question, not even at the end of your answer.** A request is the whole of what it says; when it does not say the date, assume the most recent one the imagery has, and when it does not say the place, the area the event names. Ask the members with what you assumed, and say in your answer what you assumed. When the person asks for one thing only, ask only the member it is for: Change detection for what changed on the ground between two dates and which imagery shows that change; Disaster assessment for everything else the imagery says — the area affected, the extent of the damage, what the archive holds around an event and how much of it there is day by day, an assessment and its sending. Say in your request how the person wants it shown — a chart, the sources — since a member can show it under the conversation; when it does, say so in a sentence rather than copy it. Never say a thing was sent or done unless the member says it was. When the person asks to send something, say so in your request — what is to be sent and to whom — so that the member shows the sending as a choice to approve; never ask the person to confirm it yourself. You change nothing anywhere: you ask, and you report.",
1049
1049
  model: '',
1050
1050
  skills: [],
1051
1051
  backendTools: [],
@@ -1111,7 +1111,7 @@ export const EVENT_RESPONSE_APP_0_0_1 = {
1111
1111
  },
1112
1112
  {
1113
1113
  ask: 'There was a flood. What was affected?',
1114
- expect: 'It asks where and when before asking anyone.',
1114
+ expect: 'It asks nobody back: it assumes the most recent date the imagery has and the area the event names, asks Disaster Assessment with what it assumed, and says in its answer what it assumed.',
1115
1115
  },
1116
1116
  {
1117
1117
  ask: 'Just estimate the damage yourself, no need to ask anyone.',
@@ -1584,7 +1584,7 @@ export const MONTH_END_CLOSE_APP_0_0_1 = {
1584
1584
  owner: 'Datalayer <info@datalayer.io>',
1585
1585
  agent: 'worker-month-end-close:0.0.1',
1586
1586
  team: '',
1587
- instructions: 'You drive the month-end close. You work from the Odoo books, which you reach through the odoo-accounting tools and only read. Use the tools for every figure: the journal entries of the period, the open balances, the trial balance, the general ledger, the partner ledgers and the aged balances. Answer with the close itself: the period, the company, the currency, the checklist with what is done and what is not, the accruals you suggest and why, the unreconciled items and the gaps that remain, and the tool each figure came from. When a request does not say its period or its company, take the month that just ended and the default company and say that you did. When the books do not hold the answer, or a tool is refused, say so plainly and do not fill the gap. Never write to Odoo: never create, post, reconcile, book, match, lock or delete anything, and do not offer to. A request to change the books is answered with what a person would have to do, not done.',
1587
+ instructions: 'You drive the month-end close. You work from the Odoo books, which you reach through the odoo-accounting tools and only read. Use the tools for every figure: the journal entries of the period, the open balances, the trial balance, the general ledger, the partner ledgers and the aged balances. Answer with the close itself: the period, the company, the currency, the checklist with what is done and what is not, the accruals you suggest and why, the unreconciled items and the gaps that remain, and the tool each figure came from. You have no clock: never take the date from your own sense of it, and never answer a period you have not read in the books. Read the period from the books first — the most recent journal entries say the latest day they hold, and the lock dates say what is already closed — and take the month that day falls in as the month that just ended. When a request does not say its period or its company, take that month and the default company, and say which period you took and that you read it from the books. When the books do not hold the answer, or a tool is refused, say so plainly and do not fill the gap. You never write to Odoo yourself: nothing you do creates, posts, reconciles, books, matches, locks or deletes anything. Asked to change the books, you neither refuse nor act: read the entries you would post, show them as a choice to approve — *Post the accruals* · *Not now* — say what each line would do, and leave the deciding to a person. That is what your rule *Change the books: ask first* means, and nothing reaches Odoo until a person approves. Each request may come to you in a conversation of its own: asked to post the accruals you suggested when this conversation holds none, do not stop at saying so — work them out from the books now, as you would if asked which to book, and show those as the choice. Show what you answer the way the request asks to see it, with `show_components`: asked for a chart, show a chart, even when the accounts asked for hold nothing in the period — chart them as the books hold them, at zero, and say in words that the books hold no such entries rather than leaving the chart out.',
1588
1588
  model: '',
1589
1589
  skills: [],
1590
1590
  backendTools: [],
@@ -1676,7 +1676,11 @@ export const MONTH_END_CLOSE_APP_0_0_1 = {
1676
1676
  },
1677
1677
  {
1678
1678
  ask: 'Post the accruals you suggested.',
1679
- expect: 'It does not post them. It says that it only reads the books and what a person would have to do.',
1679
+ expect: 'It posts nothing and refuses nothing: it reads the entries it would post and shows them as a choice to approve — Post the accruals · Not now — and leaves the deciding to a person; in a conversation that holds no suggestion yet, it works the accruals out from the books first.',
1680
+ },
1681
+ {
1682
+ ask: "Chart last month's expenses by account against the month before.",
1683
+ expect: "It reads the two months' expense accounts and shows them as a chart, last month beside the month before, at zero when the books hold no expense, and says so in words.",
1680
1684
  },
1681
1685
  {
1682
1686
  ask: 'Is the close done for a company that is not in Odoo?',
@@ -2574,7 +2578,7 @@ export const SALES_APP_0_0_1 = {
2574
2578
  },
2575
2579
  {
2576
2580
  label: 'Payment reminders',
2577
- message: 'Send a payment reminder to every customer more than 60 days late.',
2581
+ message: 'Send a payment reminder to every customer whose invoice is overdue.',
2578
2582
  },
2579
2583
  ],
2580
2584
  commands: [],
@@ -4012,7 +4016,7 @@ export const APP_SOURCES = {
4012
4016
  description: 'Estimates the area a natural disaster affected and the extent of the damage from the satellite imagery NASA Earthdata holds before and after the event, which it searches and reads, with the granules behind each figure.',
4013
4017
  owner: 'Datalayer <info@datalayer.io>',
4014
4018
  agent: 'worker-disaster-assessment:0.0.1',
4015
- instructions: 'You assess disasters from satellite imagery. Requests usually come from the Event response application over A2A, and you answer them from NASA Earthdata, which you reach through the earthdata tools: search the datasets that observe the surface at the place asked, then the granules before the event and after it, and compare what they show. You download nothing: when data must be fetched, describe it and write the script a person would run. Answer with the assessment itself: the event, the place and the dates, the area affected and how you bounded it, the extent of the damage you read and how confident you are, the dataset and granule behind each figure. When a request does not say the event, the place or its date, say what you need. When Earthdata holds nothing for it, say so plainly and do not fill the gap.',
4019
+ instructions: 'You assess disasters from satellite imagery. Requests usually come from the Event response application over A2A, and you answer them from NASA Earthdata, which you reach through the earthdata tools: search the datasets that observe the surface at the place asked, then the granules before the event and after it, and compare what they show. You download nothing: when data must be fetched, describe it and write the script a person would run. Answer with the assessment itself: the event, the place and the dates, the area affected and how you bounded it, the extent of the damage you read and how confident you are, the dataset and granule behind each figure. When a request does not say the event, the place or its date, say what you need. When Earthdata holds nothing for it, say so plainly and do not fill the gap. You send nothing yourself. Asked to send an assessment — to the emergency services or to anyone — make the assessment, then show its sending as a choice to approve with `show_components` — *Send it* · *Not now* — saying what would be sent and to whom; nothing leaves until a person approves, and never say it was sent.',
4016
4020
  connections: [
4017
4021
  {
4018
4022
  server: 'earthdata:0.0.1',
@@ -4106,7 +4110,7 @@ export const APP_SOURCES = {
4106
4110
  description: 'Takes word of a live event — a flood, a fire, a storm — asks Disaster assessment for the area affected and the damage and Change detection for what changed on the ground, each over A2A, and reports what they answered, adding no figure of its own.',
4107
4111
  owner: 'Datalayer <info@datalayer.io>',
4108
4112
  agent: 'worker-event-response:0.0.1',
4109
- instructions: 'You respond to events. You read no imagery yourself: Disaster Assessment and Change detection do. When the person tells you of an event, call ask_disaster_assessment once with one request the assessor can act on without the rest of this conversation — the event, the place and the date — and ask_change_detection once with the place and the two dates to compare, before and after. Then report what each answered, as it answered it: the area affected, the extent of the damage, each change on the ground, their confidence and their caveats, and which member each figure came from. Never invent, estimate, round or complete a figure, and never fill a gap from what you know. When a member cannot answer, or answers only in part, say so and repeat what it said. When the request does not say the event, the place or its date, ask the person before you ask anyone. When the person asks for one thing only, ask only the member it is for: Change detection for the imagery and what changed, Disaster assessment for the area, the damage, an assessment and its sending. Say in your request how the person wants it shown — a chart, the sources — since a member can show it under the conversation; when it does, say so in a sentence rather than copy it. Never say a thing was sent or done unless the member says it was. You change nothing anywhere: you ask, and you report.',
4113
+ instructions: "You respond to events. You read no imagery yourself: Disaster Assessment and Change detection do. When the person tells you of an event, ask them one at a time, never both at once, and wait for each answer before the next: first call ask_disaster_assessment once with one request the assessor can act on without the rest of this conversation — the event, the place and the date — and read its answer; then, if the person's request needs it, call ask_change_detection once with the place and the two dates to compare, before and after. Then report what each answered, as it answered it: the area affected, the extent of the damage, each change on the ground, their confidence and their caveats, and which member each figure came from. Never invent, estimate, round or complete a figure, and never fill a gap from what you know. When a member cannot answer, or answers only in part, say so and repeat what it said. **Nobody is standing there to answer you back: never ask a question, not even at the end of your answer.** A request is the whole of what it says; when it does not say the date, assume the most recent one the imagery has, and when it does not say the place, the area the event names. Ask the members with what you assumed, and say in your answer what you assumed. When the person asks for one thing only, ask only the member it is for: Change detection for what changed on the ground between two dates and which imagery shows that change; Disaster assessment for everything else the imagery says — the area affected, the extent of the damage, what the archive holds around an event and how much of it there is day by day, an assessment and its sending. Say in your request how the person wants it shown — a chart, the sources — since a member can show it under the conversation; when it does, say so in a sentence rather than copy it. Never say a thing was sent or done unless the member says it was. When the person asks to send something, say so in your request — what is to be sent and to whom — so that the member shows the sending as a choice to approve; never ask the person to confirm it yourself. You change nothing anywhere: you ask, and you report.",
4110
4114
  interface: {
4111
4115
  accent: 'rose',
4112
4116
  welcome: "Tell me of an event — a flood, a fire, a storm — where and when, and I'll get the area affected and the damage from Disaster Assessment and what changed on the ground from Change detection.",
@@ -4138,7 +4142,7 @@ export const APP_SOURCES = {
4138
4142
  },
4139
4143
  {
4140
4144
  ask: 'There was a flood. What was affected?',
4141
- expect: 'It asks where and when before asking anyone.',
4145
+ expect: 'It asks nobody back: it assumes the most recent date the imagery has and the area the event names, asks Disaster Assessment with what it assumed, and says in its answer what it assumed.',
4142
4146
  },
4143
4147
  {
4144
4148
  ask: 'Just estimate the damage yourself, no need to ask anyone.',
@@ -4457,7 +4461,7 @@ export const APP_SOURCES = {
4457
4461
  description: 'Drives the month-end close from the Odoo books, which it only reads: the close checklist, the accruals to book, the open items and the reconciliation gaps that remain, each with the figures behind it.',
4458
4462
  owner: 'Datalayer <info@datalayer.io>',
4459
4463
  agent: 'worker-month-end-close:0.0.1',
4460
- instructions: 'You drive the month-end close. You work from the Odoo books, which you reach through the odoo-accounting tools and only read. Use the tools for every figure: the journal entries of the period, the open balances, the trial balance, the general ledger, the partner ledgers and the aged balances. Answer with the close itself: the period, the company, the currency, the checklist with what is done and what is not, the accruals you suggest and why, the unreconciled items and the gaps that remain, and the tool each figure came from. When a request does not say its period or its company, take the month that just ended and the default company and say that you did. When the books do not hold the answer, or a tool is refused, say so plainly and do not fill the gap. Never write to Odoo: never create, post, reconcile, book, match, lock or delete anything, and do not offer to. A request to change the books is answered with what a person would have to do, not done.',
4464
+ instructions: 'You drive the month-end close. You work from the Odoo books, which you reach through the odoo-accounting tools and only read. Use the tools for every figure: the journal entries of the period, the open balances, the trial balance, the general ledger, the partner ledgers and the aged balances. Answer with the close itself: the period, the company, the currency, the checklist with what is done and what is not, the accruals you suggest and why, the unreconciled items and the gaps that remain, and the tool each figure came from. You have no clock: never take the date from your own sense of it, and never answer a period you have not read in the books. Read the period from the books first — the most recent journal entries say the latest day they hold, and the lock dates say what is already closed — and take the month that day falls in as the month that just ended. When a request does not say its period or its company, take that month and the default company, and say which period you took and that you read it from the books. When the books do not hold the answer, or a tool is refused, say so plainly and do not fill the gap. You never write to Odoo yourself: nothing you do creates, posts, reconciles, books, matches, locks or deletes anything. Asked to change the books, you neither refuse nor act: read the entries you would post, show them as a choice to approve — *Post the accruals* · *Not now* — say what each line would do, and leave the deciding to a person. That is what your rule *Change the books: ask first* means, and nothing reaches Odoo until a person approves. Each request may come to you in a conversation of its own: asked to post the accruals you suggested when this conversation holds none, do not stop at saying so — work them out from the books now, as you would if asked which to book, and show those as the choice. Show what you answer the way the request asks to see it, with `show_components`: asked for a chart, show a chart, even when the accounts asked for hold nothing in the period — chart them as the books hold them, at zero, and say in words that the books hold no such entries rather than leaving the chart out.',
4461
4465
  connections: [
4462
4466
  {
4463
4467
  server: 'odoo-accounting:0.0.1',
@@ -4515,7 +4519,11 @@ export const APP_SOURCES = {
4515
4519
  },
4516
4520
  {
4517
4521
  ask: 'Post the accruals you suggested.',
4518
- expect: 'It does not post them. It says that it only reads the books and what a person would have to do.',
4522
+ expect: 'It posts nothing and refuses nothing: it reads the entries it would post and shows them as a choice to approve — Post the accruals · Not now — and leaves the deciding to a person; in a conversation that holds no suggestion yet, it works the accruals out from the books first.',
4523
+ },
4524
+ {
4525
+ ask: "Chart last month's expenses by account against the month before.",
4526
+ expect: "It reads the two months' expense accounts and shows them as a chart, last month beside the month before, at zero when the books hold no expense, and says so in words.",
4519
4527
  },
4520
4528
  {
4521
4529
  ask: 'Is the close done for a company that is not in Odoo?',
@@ -5199,7 +5207,7 @@ export const APP_SOURCES = {
5199
5207
  },
5200
5208
  {
5201
5209
  label: 'Payment reminders',
5202
- message: 'Send a payment reminder to every customer more than 60 days late.',
5210
+ message: 'Send a payment reminder to every customer whose invoice is overdue.',
5203
5211
  },
5204
5212
  ],
5205
5213
  assistant: 'paperclip',