@meetopenbot/openbot 1.7.0 → 1.8.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/runtime.js +17 -1
- package/dist/system-prompt.js +13 -5
- package/dist/tools/ask-agent.js +20 -7
- package/dist/tools/ask-user.js +77 -0
- package/dist/tools/todo-items.js +43 -0
- package/dist/tools/todo-service.js +2 -48
- package/dist/tools/todo.js +6 -9
- package/dist/tools/variables-request.js +53 -4
- package/package.json +2 -2
package/dist/build-tools.js
CHANGED
|
@@ -3,6 +3,7 @@ import { todoTools } from "./tools/todo.js";
|
|
|
3
3
|
import { storageTools } from "./tools/storage.js";
|
|
4
4
|
import { startThreadTools } from "./tools/start-thread.js";
|
|
5
5
|
import { askAgentTools } from "./tools/ask-agent.js";
|
|
6
|
+
import { askUserTools } from "./tools/ask-user.js";
|
|
6
7
|
import { installAgentTools } from "./tools/install-agent.js";
|
|
7
8
|
import { threadStatusTools } from "./tools/thread-status.js";
|
|
8
9
|
import { threadTitleTools } from "./tools/thread-title.js";
|
|
@@ -22,6 +23,7 @@ export function buildOpenbotTools(args) {
|
|
|
22
23
|
...storageTools,
|
|
23
24
|
...startThreadTools,
|
|
24
25
|
...askAgentTools,
|
|
26
|
+
...askUserTools,
|
|
25
27
|
...installAgentTools,
|
|
26
28
|
...threadStatusTools,
|
|
27
29
|
...threadTitleTools,
|
package/dist/runtime.js
CHANGED
|
@@ -299,6 +299,21 @@ export async function* runOpenbotTurn(args, options) {
|
|
|
299
299
|
});
|
|
300
300
|
systemPrompt = `${await buildSystemPrompt(state, storage, instructions, host)}\n\n${FAST_FORWARD_CLOSE_HINT}`;
|
|
301
301
|
}
|
|
302
|
+
// Messages the human sent while this run is going. They join the conversation
|
|
303
|
+
// before the next model step, like a reply typed between tool calls, and stay in
|
|
304
|
+
// it for every later step (and a retry on another model).
|
|
305
|
+
const steered = [];
|
|
306
|
+
const takeSteering = !surface && !args.event?.meta?.parentToolCallId ? host.takeSteeringMessages : undefined;
|
|
307
|
+
const prepareStep = takeSteering
|
|
308
|
+
? ({ messages }) => {
|
|
309
|
+
const known = new Set([...events, ...steered].map((event) => event.id).filter(Boolean));
|
|
310
|
+
const fresh = takeSteering().filter((event) => !event.id || !known.has(event.id));
|
|
311
|
+
if (fresh.length === 0)
|
|
312
|
+
return undefined;
|
|
313
|
+
steered.push(...fresh);
|
|
314
|
+
return { messages: [...messages, ...eventsToModelMessages(fresh)] };
|
|
315
|
+
}
|
|
316
|
+
: undefined;
|
|
302
317
|
let lastError;
|
|
303
318
|
let started = false;
|
|
304
319
|
for (let i = 0; i < candidates.length; i++) {
|
|
@@ -311,7 +326,7 @@ export async function* runOpenbotTurn(args, options) {
|
|
|
311
326
|
};
|
|
312
327
|
const tools = toAiTools(toolDefinitions, args, flushPendingOutput);
|
|
313
328
|
try {
|
|
314
|
-
let messages = eventsToModelMessages(events);
|
|
329
|
+
let messages = eventsToModelMessages([...events, ...steered]);
|
|
315
330
|
if (hasAttachments) {
|
|
316
331
|
const includeImages = modelSupportsImages(candidate, { authMode });
|
|
317
332
|
messages = await addAttachmentParts(messages, events, {
|
|
@@ -330,6 +345,7 @@ export async function* runOpenbotTurn(args, options) {
|
|
|
330
345
|
reasoning: reasoningForModel(candidate),
|
|
331
346
|
stopWhen: surface ? [stepCountIs(8), hasToolCall(RESPOND_TOOL)] : stepCountIs(20),
|
|
332
347
|
allowSystemInMessages: true,
|
|
348
|
+
...(prepareStep ? { prepareStep } : {}),
|
|
333
349
|
});
|
|
334
350
|
for await (const part of result.fullStream) {
|
|
335
351
|
if (abortSignal?.aborted)
|
package/dist/system-prompt.js
CHANGED
|
@@ -4,12 +4,13 @@ export const OPENBOT_SYSTEM_PROMPT = [
|
|
|
4
4
|
"",
|
|
5
5
|
"# SECURITY POLICY",
|
|
6
6
|
"- **CRITICAL**: Never ask for API keys, passwords, or sensitive credentials in chat or through form widgets; values must never enter your context.",
|
|
7
|
-
"- **Credential Guidance**: Only ask for a variable after a specialist or tool actually reports it missing. Then call `request_variables` once with every exact name it reported (backend and build-time `VITE_*` alike), each with a short hint. The human fills them in an inline card that saves straight to Space variables and
|
|
7
|
+
"- **Credential Guidance**: Only ask for a variable after a specialist or tool actually reports it missing. Then call `request_variables` once with every exact name it reported (backend and build-time `VITE_*` alike), each with a short hint. The human fills them in an inline card that saves straight to Space variables; your run waits on the card and continues once they are saved — do not also link to Settings or list the names as links. Never guess, invent, or name an API key or provider that no agent or tool reported.",
|
|
8
8
|
"",
|
|
9
9
|
"# CORE MISSION",
|
|
10
10
|
"You are a high-level manager. Not every message is project work.",
|
|
11
11
|
"- Answer questions, listings, setup, and status directly from `## SPACES`, `## INSTALLED AGENTS`, and `## AVAILABLE AGENTS`. Do not create a Space to look anything up.",
|
|
12
12
|
"- For real project work (build, code, design, ops) you decide **who** (which installed agent), then ask that agent in the current Space, close without restating their work, and keep todos current.",
|
|
13
|
+
"- A quick question gets a plain answer and no todo list. A task gets a todo list first; the human follows progress there, not in your messages.",
|
|
13
14
|
"",
|
|
14
15
|
"# THE AGENT LOOP",
|
|
15
16
|
"You operate in an iterative loop:",
|
|
@@ -17,19 +18,26 @@ export const OPENBOT_SYSTEM_PROMPT = [
|
|
|
17
18
|
"2. **Right Space**: Every Space, `#general` included, is a place to do work. Do it in the current Space by default. Only if the human's request clearly belongs to a different existing Space from `## SPACES` — for example it names that project — explain in text which Space it belongs in and why. Then either wait for the human to continue there, or — when the request is clear enough to act on — call `start_thread` to open a thread in that Space with their request, written in their own first-person voice because it is posted as them; the human approves it before it starts. Never start a thread in the Space you are already in. Otherwise continue here with `ask_agent`. Create a new Space only when the human named one or explicitly asked for it. Never invent placeholder ids (`noop`, `temp`, `misc`, `untitled`).",
|
|
18
19
|
"3. **Ask people**: Use `ask_agent` to send a message to a specialist. Write it as a request to a colleague, not as a tool invocation.",
|
|
19
20
|
"4. **Wait**: Let the asked agent run in its own harness. Read their reply, then continue or ask someone else.",
|
|
20
|
-
"5. **Close**: The human already saw the specialist speak in this thread. Do not restate, re-bullet, or paraphrase their findings. Name who you asked, then add only what is new — a decision, a conflict, a next step, or a caveat. If nothing is new, close in one or two sentences.",
|
|
21
|
+
"5. **Close**: Write one short final message when the task is done or blocked. The human already saw the specialist speak in this thread. Do not restate, re-bullet, or paraphrase their findings. Name who you asked, then add only what is new — a decision, a conflict, a next step, or a caveat. If nothing is new, close in one or two sentences. Never post progress recaps between steps; the todo list shows progress.",
|
|
21
22
|
"6. **Enter Standby**: Idle until the next human message.",
|
|
23
|
+
"- The human can write while you work. Their message shows up in the conversation between your steps: read it, adjust the plan or the next ask to fit, and keep going. If they ask you to stop or change course, do that first.",
|
|
22
24
|
"",
|
|
23
25
|
"# TASK TRACKING (TODOS)",
|
|
24
26
|
"- For any complex multi-step task (3+ steps), you **MUST** maintain a todo list with `todo_write`.",
|
|
25
27
|
"- Write the full intended list at task start. Each call replaces the previous list (not a partial patch).",
|
|
26
|
-
"-
|
|
28
|
+
"- Mark an item `in_progress` while it is being worked on; when you ask several agents at once, each of their items is `in_progress`. Mark items `completed` immediately after finishing them.",
|
|
29
|
+
"- Each item is usually one `ask_agent` call. Mark it `in_progress` before you ask, and keep the same list across turns — update it, never start a second one.",
|
|
27
30
|
"- Use `cancelled` for steps that are no longer needed. When the overall task is done, mark all items `completed` and leave the list intact (do not clear with empty `items`).",
|
|
28
31
|
"- The current list is injected into context each turn as `## TODOS`; call `todo_read` only if you need an explicit refresh.",
|
|
29
32
|
"- Do not stop with open `pending` or `in_progress` items unless you are blocked and have told the user why.",
|
|
30
33
|
"",
|
|
34
|
+
"# ASKING THE HUMAN",
|
|
35
|
+
"- One way to ask: when a step needs a decision or answer from the human, call `ask_user`. It shows a card, pauses your run, and returns their answer, so you continue the same step. Never ask a blocking question in prose and never continue past it.",
|
|
36
|
+
"- Missing variables go through `request_variables`, which also waits. Missing agent accounts are the host's job: the host shows the connect card and the asked agent waits for it. Do not ask the human to connect or to paste keys for an agent that is already connected.",
|
|
37
|
+
"- Ask once. If an answer or card is already pending, wait for it instead of asking again.",
|
|
38
|
+
"",
|
|
31
39
|
"# WIDGETS",
|
|
32
|
-
"- Call `render_widget` when your own answer is structured: 3+ similar records (Spaces, agents, options, statuses)
|
|
40
|
+
"- Call `render_widget` when your own answer is structured: 3+ similar records (Spaces, agents, options, statuses) or a side-by-side comparison. Use plain text for everything else. Decisions go through `ask_user`, not `render_widget`.",
|
|
33
41
|
"- Never use it to restate or re-format what a specialist already said in this thread; that rule from Close still applies.",
|
|
34
42
|
"- 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
43
|
"- Forms never ask for secrets; use `request_variables` for those.",
|
|
@@ -42,7 +50,7 @@ export const OPENBOT_SYSTEM_PROMPT = [
|
|
|
42
50
|
"# JOB STATUS",
|
|
43
51
|
"- Every thread is a job. Status is `working` (in progress), `needs_input`, `ready_for_review`, `completed`, or `archived`.",
|
|
44
52
|
"- Status is the job's workflow state, not whether an agent is currently executing.",
|
|
45
|
-
"-
|
|
53
|
+
"- `ask_user`, `request_variables`, and connect cards set `needs_input` while they wait; do not set it yourself for those.",
|
|
46
54
|
"- When you believe the job is done, set `ready_for_review`. Never set `completed` or `archived` — only the human can.",
|
|
47
55
|
"- The current status is injected each turn as `## THREAD STATUS`.",
|
|
48
56
|
"",
|
package/dist/tools/ask-agent.js
CHANGED
|
@@ -21,16 +21,24 @@ export const toolDefinitions = {
|
|
|
21
21
|
}),
|
|
22
22
|
},
|
|
23
23
|
};
|
|
24
|
-
/**
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The host's connect card: the asked agent has no account yet. Newer runtimes
|
|
26
|
+
* wait on the card and close it once connected; `open` is false from then on.
|
|
27
|
+
*/
|
|
28
|
+
function connectionCard(event) {
|
|
26
29
|
if (event.type !== "client:ui:widget")
|
|
27
30
|
return undefined;
|
|
28
|
-
const
|
|
31
|
+
const data = event.data;
|
|
32
|
+
const metadata = data?.metadata;
|
|
29
33
|
if (metadata?.type !== "connection_request")
|
|
30
34
|
return undefined;
|
|
31
35
|
// `pluginName` is what runtimes before scoped connections send.
|
|
32
36
|
const name = metadata.name ?? metadata.pluginName;
|
|
33
|
-
return
|
|
37
|
+
return {
|
|
38
|
+
widgetId: data?.widgetId ?? "",
|
|
39
|
+
name: typeof name === "string" ? name : "This agent",
|
|
40
|
+
open: !data?.state || data.state === "open",
|
|
41
|
+
};
|
|
34
42
|
}
|
|
35
43
|
async function runAskedAgent(ctx, args) {
|
|
36
44
|
if (ctx.agentId !== ctx.host.orchestratorAgentId) {
|
|
@@ -61,7 +69,8 @@ async function runAskedAgent(ctx, args) {
|
|
|
61
69
|
}
|
|
62
70
|
const runId = `ask_${randomUUID()}`;
|
|
63
71
|
let lastAgentOutput = "";
|
|
64
|
-
|
|
72
|
+
// Latest state of each connect card the asked agent showed, by widgetId.
|
|
73
|
+
const connectCards = new Map();
|
|
65
74
|
const eventQueue = [];
|
|
66
75
|
let resolveNext = null;
|
|
67
76
|
let isFinished = false;
|
|
@@ -99,7 +108,9 @@ async function runAskedAgent(ctx, args) {
|
|
|
99
108
|
if (outEvent.type === "agent:output") {
|
|
100
109
|
lastAgentOutput = outEvent.data.content;
|
|
101
110
|
}
|
|
102
|
-
|
|
111
|
+
const card = connectionCard(outEvent);
|
|
112
|
+
if (card)
|
|
113
|
+
connectCards.set(card.widgetId, card);
|
|
103
114
|
if (resolveNext) {
|
|
104
115
|
resolveNext();
|
|
105
116
|
resolveNext = null;
|
|
@@ -128,8 +139,10 @@ async function runAskedAgent(ctx, args) {
|
|
|
128
139
|
}
|
|
129
140
|
}
|
|
130
141
|
await runPromise;
|
|
142
|
+
// A card still open when the run ended means the agent never got its account.
|
|
143
|
+
const needsConnection = [...connectCards.values()].find((card) => card.open)?.name;
|
|
131
144
|
if (needsConnection) {
|
|
132
|
-
const output = `${needsConnection} is not connected yet, so it did not run. The human
|
|
145
|
+
const output = `${needsConnection} is not connected yet, so it did not run. The human sees a ` +
|
|
133
146
|
"card to connect it; once they do, the conversation continues on its own. " +
|
|
134
147
|
"Do not ask them for keys or retry; say in one short line that it needs connecting.";
|
|
135
148
|
return { success: false, needsConnection: true, error: output, output };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
import { requestQuestion } from '@meetopenbot/plugin-sdk';
|
|
3
|
+
/**
|
|
4
|
+
* `ask_user` — OpenBot asks the human one question and waits for the answer.
|
|
5
|
+
* The run pauses on the card (thread status `needs_input`) and the answer is
|
|
6
|
+
* the tool result, so the plan continues from the same step.
|
|
7
|
+
*/
|
|
8
|
+
const MAX_OPTIONS = 6;
|
|
9
|
+
export const toolDefinitions = {
|
|
10
|
+
ask_user: {
|
|
11
|
+
description: 'Ask the human one question and wait for their answer before continuing. Use it for a decision the next step depends on (e.g. test or live mode, which option to pick). Give 2-6 short options for a choice; omit options for a free-text answer. Never ask the same question in prose.',
|
|
12
|
+
inputSchema: z.object({
|
|
13
|
+
question: z.string().min(1).describe('The question, one short sentence.'),
|
|
14
|
+
description: z.string().optional().describe('Optional one-line context under the question.'),
|
|
15
|
+
options: z
|
|
16
|
+
.array(z.string().min(1).max(40))
|
|
17
|
+
.max(MAX_OPTIONS)
|
|
18
|
+
.optional()
|
|
19
|
+
.describe('Button labels, recommended option first. Omit for a free-text answer.'),
|
|
20
|
+
}),
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
async function askUser(rawArgs, ctx) {
|
|
24
|
+
const args = (rawArgs ?? {});
|
|
25
|
+
const question = args.question?.trim();
|
|
26
|
+
if (!question) {
|
|
27
|
+
const message = 'Pass the question to ask.';
|
|
28
|
+
return { success: false, error: message, output: message };
|
|
29
|
+
}
|
|
30
|
+
const options = [...new Set((args.options ?? []).map((option) => option.trim()).filter(Boolean))].slice(0, MAX_OPTIONS);
|
|
31
|
+
const description = args.description?.trim() || undefined;
|
|
32
|
+
const hitl = {
|
|
33
|
+
emit: ctx.emit,
|
|
34
|
+
host: ctx.host,
|
|
35
|
+
abortSignal: ctx.abortSignal,
|
|
36
|
+
agentId: ctx.agentId,
|
|
37
|
+
threadId: ctx.threadId ?? ctx.state.threadId,
|
|
38
|
+
channelId: ctx.channelId ?? ctx.state.channelId,
|
|
39
|
+
storage: ctx.storage,
|
|
40
|
+
};
|
|
41
|
+
try {
|
|
42
|
+
const response = options.length > 0
|
|
43
|
+
? await requestQuestion({
|
|
44
|
+
...hitl,
|
|
45
|
+
title: question,
|
|
46
|
+
description,
|
|
47
|
+
actions: options.map((label, index) => ({
|
|
48
|
+
id: label,
|
|
49
|
+
label,
|
|
50
|
+
variant: index === 0 ? 'primary' : 'secondary',
|
|
51
|
+
})),
|
|
52
|
+
})
|
|
53
|
+
: await requestQuestion({
|
|
54
|
+
...hitl,
|
|
55
|
+
title: question,
|
|
56
|
+
description,
|
|
57
|
+
fields: [{ id: 'answer', label: question, type: 'textarea', required: true }],
|
|
58
|
+
});
|
|
59
|
+
const answer = options.length > 0 ? response.actionId : String(response.values?.answer ?? '').trim();
|
|
60
|
+
if (!answer || response.actionId === 'deny') {
|
|
61
|
+
const output = 'The human dismissed the question without answering.';
|
|
62
|
+
return { success: false, error: output, output };
|
|
63
|
+
}
|
|
64
|
+
return { success: true, answer, output: `The human answered: ${answer}` };
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
const output = 'The run stopped before the human answered.';
|
|
68
|
+
return { success: false, error: output, output };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export const tools = {
|
|
72
|
+
ask_user: {
|
|
73
|
+
...toolDefinitions.ask_user,
|
|
74
|
+
execute: askUser,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
export const askUserTools = tools;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Todo list shape and validation; no I/O, so it loads without the build. */
|
|
2
|
+
export const MAX_TODO_ITEMS = 20;
|
|
3
|
+
const TODO_STATUSES = new Set([
|
|
4
|
+
'pending',
|
|
5
|
+
'in_progress',
|
|
6
|
+
'completed',
|
|
7
|
+
'cancelled',
|
|
8
|
+
]);
|
|
9
|
+
/**
|
|
10
|
+
* Validate and normalize a todo list write. Throws on invalid input.
|
|
11
|
+
* Enforces: non-empty content, known statuses, unique ids, max size.
|
|
12
|
+
* Several items may be `in_progress`: steps asked in parallel run together.
|
|
13
|
+
*/
|
|
14
|
+
export function validateTodoItems(items) {
|
|
15
|
+
if (!Array.isArray(items)) {
|
|
16
|
+
throw new Error('todos must be an array');
|
|
17
|
+
}
|
|
18
|
+
if (items.length > MAX_TODO_ITEMS) {
|
|
19
|
+
throw new Error(`At most ${MAX_TODO_ITEMS} todos allowed`);
|
|
20
|
+
}
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
const normalized = [];
|
|
23
|
+
for (const raw of items) {
|
|
24
|
+
if (!raw || typeof raw !== 'object') {
|
|
25
|
+
throw new Error('Each todo must be an object');
|
|
26
|
+
}
|
|
27
|
+
const id = typeof raw.id === 'string' ? raw.id.trim() : '';
|
|
28
|
+
const content = typeof raw.content === 'string' ? raw.content.trim() : '';
|
|
29
|
+
const status = raw.status;
|
|
30
|
+
if (!id)
|
|
31
|
+
throw new Error('Each todo requires a non-empty id');
|
|
32
|
+
if (!content)
|
|
33
|
+
throw new Error(`Todo "${id}" requires non-empty content`);
|
|
34
|
+
if (!TODO_STATUSES.has(status)) {
|
|
35
|
+
throw new Error(`Todo "${id}" has invalid status "${String(status)}"; expected pending|in_progress|completed|cancelled`);
|
|
36
|
+
}
|
|
37
|
+
if (seen.has(id))
|
|
38
|
+
throw new Error(`Duplicate todo id "${id}"`);
|
|
39
|
+
seen.add(id);
|
|
40
|
+
normalized.push({ id, content, status: status });
|
|
41
|
+
}
|
|
42
|
+
return normalized;
|
|
43
|
+
}
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { DEFAULT_CHANNELS_DIR, getBaseDir, resolvePath } from '../utils/paths.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
'pending',
|
|
7
|
-
'in_progress',
|
|
8
|
-
'completed',
|
|
9
|
-
'cancelled',
|
|
10
|
-
]);
|
|
4
|
+
import { MAX_TODO_ITEMS, validateTodoItems } from './todo-items.js';
|
|
5
|
+
export { MAX_TODO_ITEMS, validateTodoItems };
|
|
11
6
|
const getThreadDir = (channelId, threadId) => {
|
|
12
7
|
const base = resolvePath(path.join(getBaseDir(), DEFAULT_CHANNELS_DIR, channelId));
|
|
13
8
|
return path.join(base, 'threads', threadId);
|
|
@@ -17,47 +12,6 @@ const emptyList = () => ({
|
|
|
17
12
|
items: [],
|
|
18
13
|
updatedAt: new Date().toISOString(),
|
|
19
14
|
});
|
|
20
|
-
/**
|
|
21
|
-
* Validate and normalize a todo list write. Throws on invalid input.
|
|
22
|
-
* Enforces: non-empty content, known statuses, unique ids, max size,
|
|
23
|
-
* and at most one `in_progress` item.
|
|
24
|
-
*/
|
|
25
|
-
export function validateTodoItems(items) {
|
|
26
|
-
if (!Array.isArray(items)) {
|
|
27
|
-
throw new Error('todos must be an array');
|
|
28
|
-
}
|
|
29
|
-
if (items.length > MAX_TODO_ITEMS) {
|
|
30
|
-
throw new Error(`At most ${MAX_TODO_ITEMS} todos allowed`);
|
|
31
|
-
}
|
|
32
|
-
const seen = new Set();
|
|
33
|
-
let inProgressCount = 0;
|
|
34
|
-
const normalized = [];
|
|
35
|
-
for (const raw of items) {
|
|
36
|
-
if (!raw || typeof raw !== 'object') {
|
|
37
|
-
throw new Error('Each todo must be an object');
|
|
38
|
-
}
|
|
39
|
-
const id = typeof raw.id === 'string' ? raw.id.trim() : '';
|
|
40
|
-
const content = typeof raw.content === 'string' ? raw.content.trim() : '';
|
|
41
|
-
const status = raw.status;
|
|
42
|
-
if (!id)
|
|
43
|
-
throw new Error('Each todo requires a non-empty id');
|
|
44
|
-
if (!content)
|
|
45
|
-
throw new Error(`Todo "${id}" requires non-empty content`);
|
|
46
|
-
if (!TODO_STATUSES.has(status)) {
|
|
47
|
-
throw new Error(`Todo "${id}" has invalid status "${String(status)}"; expected pending|in_progress|completed|cancelled`);
|
|
48
|
-
}
|
|
49
|
-
if (seen.has(id))
|
|
50
|
-
throw new Error(`Duplicate todo id "${id}"`);
|
|
51
|
-
seen.add(id);
|
|
52
|
-
if (status === 'in_progress')
|
|
53
|
-
inProgressCount += 1;
|
|
54
|
-
normalized.push({ id, content, status: status });
|
|
55
|
-
}
|
|
56
|
-
if (inProgressCount > 1) {
|
|
57
|
-
throw new Error('At most one todo may be in_progress');
|
|
58
|
-
}
|
|
59
|
-
return normalized;
|
|
60
|
-
}
|
|
61
15
|
export const todoService = {
|
|
62
16
|
getTodos: async (args) => {
|
|
63
17
|
const filePath = getTodosPath(args.channelId, args.threadId);
|
package/dist/tools/todo.js
CHANGED
|
@@ -35,8 +35,8 @@ function todoListWidget(args) {
|
|
|
35
35
|
return {
|
|
36
36
|
type: 'client:ui:widget',
|
|
37
37
|
data: {
|
|
38
|
-
// One
|
|
39
|
-
widgetId: `todos:${args.
|
|
38
|
+
// One plan per thread — every todo_write, in any run, updates it in place.
|
|
39
|
+
widgetId: `todos:${args.threadId}`,
|
|
40
40
|
kind: 'list',
|
|
41
41
|
layout: 'checklist',
|
|
42
42
|
title: 'Todos',
|
|
@@ -51,11 +51,11 @@ const todoItemSchema = z.object({
|
|
|
51
51
|
content: z.string().min(1).describe('Short imperative description of the step.'),
|
|
52
52
|
status: z
|
|
53
53
|
.enum(['pending', 'in_progress', 'completed', 'cancelled'])
|
|
54
|
-
.describe('Todo status.
|
|
54
|
+
.describe('Todo status. Every step being worked on is in_progress, including steps run in parallel.'),
|
|
55
55
|
});
|
|
56
56
|
export const toolDefinitions = {
|
|
57
57
|
todo_write: {
|
|
58
|
-
description: 'Replace the current thread todo list with the provided items. Use for multi-step tasks: write the plan first,
|
|
58
|
+
description: 'Replace the current thread todo list with the provided items. Use for multi-step tasks: write the plan first, mark each step in_progress while it is being worked on (several when they run in parallel), mark items completed immediately after finishing, and when done leave all items as completed (do not clear the list). Pass the full intended list each call (not a partial patch).',
|
|
59
59
|
inputSchema: z.object({
|
|
60
60
|
items: z
|
|
61
61
|
.array(todoItemSchema)
|
|
@@ -81,16 +81,13 @@ export const tools = {
|
|
|
81
81
|
const items = (rawArgs ?? {}).items;
|
|
82
82
|
const list = await todoService.writeTodos({ channelId, threadId, items });
|
|
83
83
|
const runId = ctx.runId ?? ctx.state.runId;
|
|
84
|
-
if (!runId) {
|
|
85
|
-
throw new Error('Missing runId for todo_write widget');
|
|
86
|
-
}
|
|
87
84
|
await ctx.emit(todoListWidget({
|
|
88
|
-
|
|
85
|
+
threadId,
|
|
89
86
|
list,
|
|
90
87
|
meta: {
|
|
91
88
|
agentId: ctx.agentId,
|
|
92
89
|
threadId,
|
|
93
|
-
runId,
|
|
90
|
+
...(runId ? { runId } : {}),
|
|
94
91
|
toolCallId: ctx.toolCallId,
|
|
95
92
|
},
|
|
96
93
|
}));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import z from 'zod';
|
|
2
|
+
import { markThreadNeedsInput, restoreThreadWorking } from '@meetopenbot/plugin-sdk';
|
|
2
3
|
/** `metadata.type` of the card the host shows to collect missing variables. */
|
|
3
4
|
export const VARIABLES_REQUEST_TYPE = 'variables_request';
|
|
4
5
|
const ENV_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
@@ -63,22 +64,70 @@ async function requestVariables(rawArgs, ctx) {
|
|
|
63
64
|
const message = 'Pass at least one variable name such as FIREBASE_PROJECT_ID.';
|
|
64
65
|
return { success: false, error: message, output: message };
|
|
65
66
|
}
|
|
66
|
-
|
|
67
|
+
const card = variablesRequestEvent({
|
|
67
68
|
variables,
|
|
68
69
|
reason: typeof data.reason === 'string' ? data.reason : undefined,
|
|
69
70
|
agentId: ctx.agentId,
|
|
70
71
|
threadId,
|
|
71
|
-
})
|
|
72
|
+
});
|
|
73
|
+
await ctx.emit(card);
|
|
74
|
+
const names = variables.map((variable) => variable.key).join(', ');
|
|
75
|
+
if (ctx.host?.resumesCardsInPlace)
|
|
76
|
+
return waitForVariables(ctx, card, names);
|
|
72
77
|
return {
|
|
73
78
|
success: true,
|
|
74
79
|
output: 'Shown a card asking the human for ' +
|
|
75
|
-
|
|
80
|
+
names +
|
|
76
81
|
'. They save the values there; you will get a message when they do. Do not ask them to paste values in chat.',
|
|
77
82
|
};
|
|
78
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Hold the run on the card until the human saves every key. The runtime's
|
|
86
|
+
* variables route resolves the wait and sends the closed card's body back.
|
|
87
|
+
* If the run stops first the card stays open: saving later resumes the thread
|
|
88
|
+
* with a message, also after a restart.
|
|
89
|
+
*/
|
|
90
|
+
async function waitForVariables(ctx, card, names) {
|
|
91
|
+
const data = card.data;
|
|
92
|
+
const hitl = {
|
|
93
|
+
emit: ctx.emit,
|
|
94
|
+
host: ctx.host,
|
|
95
|
+
agentId: ctx.agentId,
|
|
96
|
+
channelId: ctx.channelId ?? ctx.state.channelId,
|
|
97
|
+
threadId: ctx.threadId ?? ctx.state.threadId,
|
|
98
|
+
storage: ctx.storage,
|
|
99
|
+
};
|
|
100
|
+
const paused = await markThreadNeedsInput(hitl, `Add ${names}`);
|
|
101
|
+
let response;
|
|
102
|
+
try {
|
|
103
|
+
response = await ctx.host.awaitWidgetResponse(data.widgetId, ctx.abortSignal, { resumable: true });
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// The card stays open and the thread stays `needs_input`; saving later resumes it.
|
|
107
|
+
const output = `The run stopped before ${names} were saved. The card stays open; saving it continues the thread.`;
|
|
108
|
+
return { success: false, error: output, output };
|
|
109
|
+
}
|
|
110
|
+
const body = typeof response.metadata?.body === 'string' ? response.metadata.body : `Saved ${names}.`;
|
|
111
|
+
const scope = response.metadata?.scope;
|
|
112
|
+
await ctx.emit({
|
|
113
|
+
...card,
|
|
114
|
+
data: {
|
|
115
|
+
...data,
|
|
116
|
+
state: 'submitted',
|
|
117
|
+
body,
|
|
118
|
+
metadata: { ...data.metadata, ...(scope ? { scope } : {}) },
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
if (paused)
|
|
122
|
+
await restoreThreadWorking(hitl);
|
|
123
|
+
return {
|
|
124
|
+
success: true,
|
|
125
|
+
output: `${body} They are in the environment now; ask the specialist to restart whatever needed them.`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
79
128
|
export const toolDefinitions = {
|
|
80
129
|
request_variables: {
|
|
81
|
-
description: 'Ask the human for missing environment variables with an inline card in the thread. Values are saved as Space variables by default (or for all spaces if they choose) and never appear in chat. Pass every missing name at once, including build-time ones like VITE_*.',
|
|
130
|
+
description: 'Ask the human for missing environment variables with an inline card in the thread, and wait until they are saved. Values are saved as Space variables by default (or for all spaces if they choose) and never appear in chat. Pass every missing name at once, including build-time ones like VITE_*.',
|
|
82
131
|
inputSchema: z.object({
|
|
83
132
|
variables: z
|
|
84
133
|
.array(z.object({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meetopenbot/openbot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.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.4.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/node": "^20.10.1",
|