@meetopenbot/openbot 1.6.3 → 1.7.0
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/build-tools.js +2 -0
- package/dist/history.js +10 -1
- package/dist/index.js +2 -0
- package/dist/runtime.js +13 -2
- package/dist/system-prompt.js +6 -0
- package/dist/tools/render-widget.js +317 -0
- package/package.json +2 -2
package/dist/build-tools.js
CHANGED
|
@@ -8,6 +8,7 @@ import { threadStatusTools } from "./tools/thread-status.js";
|
|
|
8
8
|
import { threadTitleTools } from "./tools/thread-title.js";
|
|
9
9
|
import { previewTools } from "./tools/preview.js";
|
|
10
10
|
import { variablesRequestTools } from "./tools/variables-request.js";
|
|
11
|
+
import { renderWidgetTools } from "./tools/render-widget.js";
|
|
11
12
|
import { wrapToolsWithApproval } from "./tools/approval.js";
|
|
12
13
|
export const DEFAULT_APPROVAL_ACTIONS = ["update_channel_spec", "start_thread"];
|
|
13
14
|
/**
|
|
@@ -26,6 +27,7 @@ export function buildOpenbotTools(args) {
|
|
|
26
27
|
...threadTitleTools,
|
|
27
28
|
...previewTools,
|
|
28
29
|
...variablesRequestTools,
|
|
30
|
+
...renderWidgetTools,
|
|
29
31
|
...args.context.tools,
|
|
30
32
|
};
|
|
31
33
|
const approvalConfig = args.context.config?.approval;
|
package/dist/history.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { compactRenderWidgetInput, renderedWidgetReplyText } from './tools/render-widget.js';
|
|
1
2
|
function asReasoningPart(part) {
|
|
2
3
|
const text = typeof part.text === 'string' ? part.text : '';
|
|
3
4
|
if (!text.trim() && !part.providerOptions)
|
|
@@ -201,6 +202,12 @@ export function eventsToModelMessages(events, options) {
|
|
|
201
202
|
}
|
|
202
203
|
break;
|
|
203
204
|
}
|
|
205
|
+
case 'client:ui:widget:response': {
|
|
206
|
+
const reply = renderedWidgetReplyText(event.data);
|
|
207
|
+
if (reply)
|
|
208
|
+
messages.push({ role: 'user', content: reply });
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
204
211
|
default:
|
|
205
212
|
// Handle tool calls (action:*)
|
|
206
213
|
if (event.type.startsWith('action:') && !event.type.endsWith(':result')) {
|
|
@@ -212,7 +219,9 @@ export function eventsToModelMessages(events, options) {
|
|
|
212
219
|
type: 'tool-call',
|
|
213
220
|
toolCallId,
|
|
214
221
|
toolName,
|
|
215
|
-
input:
|
|
222
|
+
input: toolName === 'render_widget'
|
|
223
|
+
? compactRenderWidgetInput(event.data)
|
|
224
|
+
: event.data,
|
|
216
225
|
};
|
|
217
226
|
const last = messages[messages.length - 1];
|
|
218
227
|
if (last && last.role === 'assistant') {
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { threadStatusTools } from './tools/thread-status.js';
|
|
|
12
12
|
import { threadTitleTools } from './tools/thread-title.js';
|
|
13
13
|
import { previewTools } from './tools/preview.js';
|
|
14
14
|
import { variablesRequestTools } from './tools/variables-request.js';
|
|
15
|
+
import { renderWidgetTools } from './tools/render-widget.js';
|
|
15
16
|
import { buildOpenbotTools } from './build-tools.js';
|
|
16
17
|
export const OPENBOT_PLUGIN_ID = '@meetopenbot/openbot';
|
|
17
18
|
const modelField = await modelConfigField({
|
|
@@ -53,6 +54,7 @@ export const openbotPlugin = defineOpenbotPlugin({
|
|
|
53
54
|
...threadTitleTools,
|
|
54
55
|
...previewTools,
|
|
55
56
|
...variablesRequestTools,
|
|
57
|
+
...renderWidgetTools,
|
|
56
58
|
},
|
|
57
59
|
async *run(args) {
|
|
58
60
|
const { context } = args;
|
package/dist/runtime.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { generateText, hasToolCall, streamText, stepCountIs } from 'ai';
|
|
3
3
|
import { BYOK_ENV_KEYS, isAuthErrorMessage, isCreditsErrorMessage, fetchModelRegistry, getProviderModelOptions, listApiKeyProviders, PROVIDER_API_KEY_LINKS, } from '@meetopenbot/plugin-sdk';
|
|
4
4
|
import { eventsToModelMessages } from './history.js';
|
|
5
|
+
import { RENDER_WIDGET_METADATA_TYPE, settledRenderedWidget } from './tools/render-widget.js';
|
|
5
6
|
import { addAttachmentParts, chooseTurnCandidates, eventsHaveAttachments, modelSupportsImages, } from './images.js';
|
|
6
7
|
import { buildContext } from './context.js';
|
|
7
8
|
import { OPENBOT_SYSTEM_PROMPT, withGithubGitInstructions, withUserInstructions, } from './system-prompt.js';
|
|
@@ -438,6 +439,18 @@ export async function* handleOpenbotWidgetResponse(args, options) {
|
|
|
438
439
|
const { host, storage } = options;
|
|
439
440
|
let configuredModelString = options.model ?? AUTO_MODEL_ID;
|
|
440
441
|
const state = args.state;
|
|
442
|
+
// A click on a card the model rendered is the human's next message: the
|
|
443
|
+
// harness already stored it, and history replays it as a user turn.
|
|
444
|
+
if (metadata?.type === RENDER_WIDGET_METADATA_TYPE) {
|
|
445
|
+
if (storage) {
|
|
446
|
+
const events = await storage.getEvents({ channelId: state.channelId ?? '', threadId });
|
|
447
|
+
const settled = settledRenderedWidget(events, event.data);
|
|
448
|
+
if (settled)
|
|
449
|
+
yield settled;
|
|
450
|
+
}
|
|
451
|
+
yield* runOpenbotTurn(args, options);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
441
454
|
if (host.isCloudSystemAgent(state.agentId) && options.authMode === 'credits')
|
|
442
455
|
return;
|
|
443
456
|
if (metadata?.type === 'api_provider_selection') {
|
|
@@ -459,7 +472,6 @@ export async function* handleOpenbotWidgetResponse(args, options) {
|
|
|
459
472
|
body: `${label} provider was selected.`,
|
|
460
473
|
state: 'submitted',
|
|
461
474
|
display: 'collapsed',
|
|
462
|
-
disabled: true,
|
|
463
475
|
actions: [],
|
|
464
476
|
},
|
|
465
477
|
meta: { agentId: state.agentId, threadId },
|
|
@@ -536,7 +548,6 @@ export async function* handleOpenbotWidgetResponse(args, options) {
|
|
|
536
548
|
body: `${modelId} was selected.`,
|
|
537
549
|
state: 'submitted',
|
|
538
550
|
display: 'collapsed',
|
|
539
|
-
disabled: true,
|
|
540
551
|
actions: [],
|
|
541
552
|
},
|
|
542
553
|
meta: { agentId: state.agentId, threadId },
|
package/dist/system-prompt.js
CHANGED
|
@@ -28,6 +28,12 @@ export const OPENBOT_SYSTEM_PROMPT = [
|
|
|
28
28
|
"- The current list is injected into context each turn as `## TODOS`; call `todo_read` only if you need an explicit refresh.",
|
|
29
29
|
"- Do not stop with open `pending` or `in_progress` items unless you are blocked and have told the user why.",
|
|
30
30
|
"",
|
|
31
|
+
"# WIDGETS",
|
|
32
|
+
"- Call `render_widget` when your own answer is structured: 3+ similar records (Spaces, agents, options, statuses), a side-by-side comparison, a decision with fixed choices, or a few fields you need from the human. Use plain text for everything else.",
|
|
33
|
+
"- Never use it to restate or re-format what a specialist already said in this thread; that rule from Close still applies.",
|
|
34
|
+
"- Do not repeat the widget's contents in text. A click or form submission arrives as the human's next message, quoting what they picked.",
|
|
35
|
+
"- Forms never ask for secrets; use `request_variables` for those.",
|
|
36
|
+
"",
|
|
31
37
|
"# JOB TITLE",
|
|
32
38
|
"- Unnamed jobs get a system hint to call `set_thread_title` once with a concise, human-readable title.",
|
|
33
39
|
"- Call `set_thread_title` again only if the topic clearly changes.",
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import z from 'zod';
|
|
3
|
+
import { uiWidgetIssue, } from '@meetopenbot/plugin-sdk';
|
|
4
|
+
/**
|
|
5
|
+
* `render_widget` — the coordinator shows structured data as an inline card
|
|
6
|
+
* instead of markdown. Clicks and form submits come back as a new turn
|
|
7
|
+
* (`handleOpenbotWidgetResponse` → `history.ts`), tagged by this metadata type.
|
|
8
|
+
*/
|
|
9
|
+
export const RENDER_WIDGET_METADATA_TYPE = 'render_widget';
|
|
10
|
+
const MAX_COLUMNS = 12;
|
|
11
|
+
const MAX_ROWS = 200;
|
|
12
|
+
const MAX_ITEMS = 50;
|
|
13
|
+
const MAX_ACTIONS = 6;
|
|
14
|
+
/** Ids the client runs itself (navigate, open_url, …); a model action must not collide. */
|
|
15
|
+
const RESERVED_ACTION_IDS = new Set(['open_preview', 'open_url', 'navigate', 'start_in_space']);
|
|
16
|
+
const actionSchema = z.object({
|
|
17
|
+
id: z.string().min(1).max(64).describe('Stable id returned to you when clicked, e.g. "approve".'),
|
|
18
|
+
label: z.string().min(1).max(40).describe('Button text.'),
|
|
19
|
+
variant: z.enum(['primary', 'secondary', 'danger']).optional(),
|
|
20
|
+
});
|
|
21
|
+
const cellSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
|
22
|
+
const inputSchema = z.object({
|
|
23
|
+
kind: z
|
|
24
|
+
.enum(['table', 'list', 'choice', 'form', 'media'])
|
|
25
|
+
.describe('table: rows × columns of data. list: records with status/badge and optional per-item buttons. choice: a question answered with buttons. form: structured input. media: images/videos/files.'),
|
|
26
|
+
title: z.string().min(1).max(120),
|
|
27
|
+
description: z.string().max(400).optional().describe('One short line under the title.'),
|
|
28
|
+
summary: z
|
|
29
|
+
.string()
|
|
30
|
+
.min(1)
|
|
31
|
+
.max(400)
|
|
32
|
+
.describe('One or two sentences on what the widget shows. Kept in history in place of the full data, so include what you will need to refer back to.'),
|
|
33
|
+
columns: z
|
|
34
|
+
.array(z.object({
|
|
35
|
+
id: z.string().min(1),
|
|
36
|
+
label: z.string(),
|
|
37
|
+
align: z.enum(['left', 'right', 'center']).optional().describe('Use right for numbers.'),
|
|
38
|
+
}))
|
|
39
|
+
.max(MAX_COLUMNS)
|
|
40
|
+
.optional()
|
|
41
|
+
.describe('table only.'),
|
|
42
|
+
rows: z
|
|
43
|
+
.array(z.array(cellSchema))
|
|
44
|
+
.max(MAX_ROWS)
|
|
45
|
+
.optional()
|
|
46
|
+
.describe('table only. Each row lists one cell per column, in column order.'),
|
|
47
|
+
items: z
|
|
48
|
+
.array(z.object({
|
|
49
|
+
id: z.string().min(1),
|
|
50
|
+
label: z.string().min(1),
|
|
51
|
+
description: z.string().optional(),
|
|
52
|
+
badge: z.string().optional(),
|
|
53
|
+
status: z.string().optional(),
|
|
54
|
+
statusVariant: z.enum(['default', 'success', 'warning', 'danger', 'info']).optional(),
|
|
55
|
+
actions: z.array(actionSchema).max(3).optional(),
|
|
56
|
+
}))
|
|
57
|
+
.max(MAX_ITEMS)
|
|
58
|
+
.optional()
|
|
59
|
+
.describe('list only.'),
|
|
60
|
+
fields: z
|
|
61
|
+
.array(z.object({
|
|
62
|
+
id: z.string().min(1),
|
|
63
|
+
label: z.string().min(1),
|
|
64
|
+
type: z.enum(['text', 'textarea', 'number', 'boolean', 'select', 'multiselect', 'date']),
|
|
65
|
+
description: z.string().optional(),
|
|
66
|
+
placeholder: z.string().optional(),
|
|
67
|
+
required: z.boolean().optional(),
|
|
68
|
+
options: z.array(z.object({ label: z.string(), value: z.string() })).optional(),
|
|
69
|
+
}))
|
|
70
|
+
.max(12)
|
|
71
|
+
.optional()
|
|
72
|
+
.describe('form only. Never ask for secrets.'),
|
|
73
|
+
submitLabel: z.string().max(40).optional().describe('form only.'),
|
|
74
|
+
media: z
|
|
75
|
+
.array(z.object({
|
|
76
|
+
type: z.enum(['image', 'video', 'audio', 'file']),
|
|
77
|
+
url: z.string().url(),
|
|
78
|
+
title: z.string().optional(),
|
|
79
|
+
alt: z.string().optional(),
|
|
80
|
+
}))
|
|
81
|
+
.max(20)
|
|
82
|
+
.optional()
|
|
83
|
+
.describe('media only. Public https URLs.'),
|
|
84
|
+
actions: z
|
|
85
|
+
.array(actionSchema)
|
|
86
|
+
.max(MAX_ACTIONS)
|
|
87
|
+
.optional()
|
|
88
|
+
.describe('Buttons under the widget. Required for choice.'),
|
|
89
|
+
});
|
|
90
|
+
function actionIdError(actions = []) {
|
|
91
|
+
const reserved = actions.find((action) => RESERVED_ACTION_IDS.has(action.id));
|
|
92
|
+
return reserved ? `Action id "${reserved.id}" is reserved; pick another id.` : null;
|
|
93
|
+
}
|
|
94
|
+
function labelsById(input) {
|
|
95
|
+
const labels = {};
|
|
96
|
+
for (const action of input.actions ?? [])
|
|
97
|
+
labels[action.id] = action.label;
|
|
98
|
+
for (const item of input.items ?? []) {
|
|
99
|
+
for (const action of item.actions ?? [])
|
|
100
|
+
labels[action.id] = action.label;
|
|
101
|
+
}
|
|
102
|
+
return labels;
|
|
103
|
+
}
|
|
104
|
+
/** Validate the flat tool input for its kind and map it onto the shared widget shape. */
|
|
105
|
+
export function buildRenderedWidget(input, widgetId) {
|
|
106
|
+
const base = {
|
|
107
|
+
widgetId,
|
|
108
|
+
title: input.title,
|
|
109
|
+
...(input.description ? { description: input.description } : {}),
|
|
110
|
+
display: 'expanded',
|
|
111
|
+
state: 'open',
|
|
112
|
+
metadata: {
|
|
113
|
+
type: RENDER_WIDGET_METADATA_TYPE,
|
|
114
|
+
title: input.title,
|
|
115
|
+
summary: input.summary,
|
|
116
|
+
labels: labelsById(input),
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
const actions = input.actions?.length ? input.actions : undefined;
|
|
120
|
+
const reserved = actionIdError(input.actions) ??
|
|
121
|
+
actionIdError((input.items ?? []).flatMap((item) => item.actions ?? []));
|
|
122
|
+
if (reserved)
|
|
123
|
+
return { error: reserved };
|
|
124
|
+
let widget;
|
|
125
|
+
switch (input.kind) {
|
|
126
|
+
case 'table': {
|
|
127
|
+
const columns = input.columns ?? [];
|
|
128
|
+
const rows = input.rows ?? [];
|
|
129
|
+
if (columns.length === 0)
|
|
130
|
+
return { error: 'table needs at least one column.' };
|
|
131
|
+
const badRow = rows.findIndex((row) => row.length !== columns.length);
|
|
132
|
+
if (badRow !== -1) {
|
|
133
|
+
return {
|
|
134
|
+
error: `table row ${badRow} has ${rows[badRow].length} cells; expected ${columns.length} (one per column).`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
widget = {
|
|
138
|
+
...base,
|
|
139
|
+
kind: 'table',
|
|
140
|
+
columns,
|
|
141
|
+
rows: rows.map((row) => Object.fromEntries(columns.map((column, i) => [column.id, row[i]]))),
|
|
142
|
+
...(actions ? { actions } : {}),
|
|
143
|
+
};
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
case 'list': {
|
|
147
|
+
if (!input.items?.length)
|
|
148
|
+
return { error: 'list needs at least one item.' };
|
|
149
|
+
widget = {
|
|
150
|
+
...base,
|
|
151
|
+
kind: 'list',
|
|
152
|
+
items: input.items.map((item) => ({
|
|
153
|
+
...item,
|
|
154
|
+
...(item.actions?.length
|
|
155
|
+
? {
|
|
156
|
+
actions: item.actions.map((action) => ({
|
|
157
|
+
...action,
|
|
158
|
+
value: { itemId: item.id, itemLabel: item.label },
|
|
159
|
+
})),
|
|
160
|
+
}
|
|
161
|
+
: {}),
|
|
162
|
+
})),
|
|
163
|
+
...(actions ? { actions } : {}),
|
|
164
|
+
};
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case 'choice':
|
|
168
|
+
if (!actions)
|
|
169
|
+
return { error: 'choice needs at least one action.' };
|
|
170
|
+
widget = { ...base, kind: 'choice', actions };
|
|
171
|
+
break;
|
|
172
|
+
case 'form':
|
|
173
|
+
if (!input.fields?.length)
|
|
174
|
+
return { error: 'form needs at least one field.' };
|
|
175
|
+
widget = {
|
|
176
|
+
...base,
|
|
177
|
+
kind: 'form',
|
|
178
|
+
fields: input.fields,
|
|
179
|
+
...(input.submitLabel ? { submitLabel: input.submitLabel } : {}),
|
|
180
|
+
};
|
|
181
|
+
break;
|
|
182
|
+
case 'media':
|
|
183
|
+
if (!input.media?.length)
|
|
184
|
+
return { error: 'media needs at least one item.' };
|
|
185
|
+
widget = {
|
|
186
|
+
...base,
|
|
187
|
+
kind: 'media',
|
|
188
|
+
items: input.media,
|
|
189
|
+
layout: input.media.length > 1 ? 'grid' : 'single',
|
|
190
|
+
...(actions ? { actions } : {}),
|
|
191
|
+
};
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
const issue = uiWidgetIssue(widget);
|
|
195
|
+
return issue ? { error: issue } : { widget };
|
|
196
|
+
}
|
|
197
|
+
function asRecord(value) {
|
|
198
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
199
|
+
? value
|
|
200
|
+
: undefined;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* How a click on a `render_widget` card reads in the model's history, or
|
|
204
|
+
* undefined for widget responses that came from anything else.
|
|
205
|
+
*/
|
|
206
|
+
export function renderedWidgetReplyText(data) {
|
|
207
|
+
const metadata = asRecord(data.metadata);
|
|
208
|
+
if (metadata?.type !== RENDER_WIDGET_METADATA_TYPE)
|
|
209
|
+
return undefined;
|
|
210
|
+
const title = typeof metadata.title === 'string' ? metadata.title : 'widget';
|
|
211
|
+
const values = { ...(data.values ?? {}) };
|
|
212
|
+
if (data.actionId === 'submit' && !asRecord(metadata.labels)?.submit) {
|
|
213
|
+
return `[Widget "${title}"] Submitted: ${JSON.stringify(values)}`;
|
|
214
|
+
}
|
|
215
|
+
const label = asRecord(metadata.labels)?.[data.actionId];
|
|
216
|
+
const clicked = typeof label === 'string' ? label : data.actionId;
|
|
217
|
+
const itemLabel = values.itemLabel;
|
|
218
|
+
delete values.itemId;
|
|
219
|
+
delete values.itemLabel;
|
|
220
|
+
const target = typeof itemLabel === 'string' ? ` on "${itemLabel}"` : '';
|
|
221
|
+
const extra = Object.keys(values).length > 0 ? ` with ${JSON.stringify(values)}` : '';
|
|
222
|
+
return `[Widget "${title}"] Clicked "${clicked}"${target}${extra} (action id: ${data.actionId})`;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* A choice or form answers once: re-emit it as submitted, with the picked
|
|
226
|
+
* button highlighted or the submitted values filled in. Lists, tables and
|
|
227
|
+
* media stay open for further clicks.
|
|
228
|
+
*/
|
|
229
|
+
export function settledRenderedWidget(events, response) {
|
|
230
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
231
|
+
const event = events[i];
|
|
232
|
+
if (event.type !== 'client:ui:widget' || event.data.widgetId !== response.widgetId)
|
|
233
|
+
continue;
|
|
234
|
+
const widget = event.data;
|
|
235
|
+
if (widget.metadata?.type !== RENDER_WIDGET_METADATA_TYPE)
|
|
236
|
+
return undefined;
|
|
237
|
+
if (widget.kind === 'choice') {
|
|
238
|
+
return {
|
|
239
|
+
...event,
|
|
240
|
+
data: {
|
|
241
|
+
...widget,
|
|
242
|
+
state: 'submitted',
|
|
243
|
+
actions: widget.actions.map((action) => ({
|
|
244
|
+
...action,
|
|
245
|
+
variant: action.id === response.actionId ? 'primary' : 'secondary',
|
|
246
|
+
})),
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
if (widget.kind === 'form') {
|
|
251
|
+
const values = response.values ?? {};
|
|
252
|
+
return {
|
|
253
|
+
...event,
|
|
254
|
+
data: {
|
|
255
|
+
...widget,
|
|
256
|
+
state: 'submitted',
|
|
257
|
+
fields: widget.fields.map((field) => field.id in values ? { ...field, defaultValue: values[field.id] } : field),
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
/** History stand-in for a `render_widget` call: what it showed, without the data. */
|
|
266
|
+
export function compactRenderWidgetInput(input) {
|
|
267
|
+
const record = asRecord(input);
|
|
268
|
+
if (!record)
|
|
269
|
+
return input;
|
|
270
|
+
const { kind, title, summary } = record;
|
|
271
|
+
return { kind, title, summary };
|
|
272
|
+
}
|
|
273
|
+
function isInteractive(widget) {
|
|
274
|
+
if (widget.kind === 'choice' || widget.kind === 'form')
|
|
275
|
+
return true;
|
|
276
|
+
if (widget.actions?.length)
|
|
277
|
+
return true;
|
|
278
|
+
return widget.kind === 'list' && widget.items.some((item) => item.actions?.length);
|
|
279
|
+
}
|
|
280
|
+
async function renderWidget(rawArgs, ctx) {
|
|
281
|
+
const parsed = inputSchema.safeParse(rawArgs ?? {});
|
|
282
|
+
if (!parsed.success) {
|
|
283
|
+
const message = parsed.error.issues[0]?.message ?? 'Invalid render_widget input.';
|
|
284
|
+
return { success: false, error: message, output: message };
|
|
285
|
+
}
|
|
286
|
+
const threadId = ctx.threadId ?? ctx.state.threadId;
|
|
287
|
+
const built = buildRenderedWidget(parsed.data, ctx.toolCallId ?? `render:${randomUUID()}`);
|
|
288
|
+
if ('error' in built) {
|
|
289
|
+
return { success: false, error: built.error, output: built.error };
|
|
290
|
+
}
|
|
291
|
+
await ctx.emit({
|
|
292
|
+
type: 'client:ui:widget',
|
|
293
|
+
data: built.widget,
|
|
294
|
+
meta: {
|
|
295
|
+
agentId: ctx.agentId,
|
|
296
|
+
...(threadId ? { threadId } : {}),
|
|
297
|
+
...(ctx.toolCallId ? { toolCallId: ctx.toolCallId } : {}),
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
const output = isInteractive(built.widget)
|
|
301
|
+
? 'Shown to the human. Do not repeat its contents in text. Their click or submission arrives as their next message; end your turn unless you have something new to add.'
|
|
302
|
+
: 'Shown to the human. Do not repeat its contents in text; add only a short takeaway if useful.';
|
|
303
|
+
return { success: true, widgetId: built.widget.widgetId, output };
|
|
304
|
+
}
|
|
305
|
+
export const toolDefinitions = {
|
|
306
|
+
render_widget: {
|
|
307
|
+
description: 'Show structured data to the human as an inline card instead of markdown: a table, a list of records with status, a choice with buttons, a short form, or media. Use it when the answer is a collection of 3+ similar records, a comparison, or a decision or input you need from the human. Do not use it for prose, single facts, or to restate what a specialist already said in this thread.',
|
|
308
|
+
inputSchema,
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
export const tools = {
|
|
312
|
+
render_widget: {
|
|
313
|
+
...toolDefinitions.render_widget,
|
|
314
|
+
execute: renderWidget,
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
export const renderWidgetTools = tools;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meetopenbot/openbot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "OpenBot coordinator runtime: ask specialists, route work into Spaces, and track todos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"@ai-sdk/openai": "^4.0.72",
|
|
26
26
|
"ai": "^7.0.111",
|
|
27
27
|
"zod": "^4.3.5",
|
|
28
|
-
"@meetopenbot/plugin-sdk": "^2.
|
|
28
|
+
"@meetopenbot/plugin-sdk": "^2.3.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/node": "^20.10.1",
|