@meetopenbot/openbot 1.8.0 → 1.10.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 +21 -3
- package/dist/context.js +15 -17
- package/dist/conversation.js +79 -0
- package/dist/fast-forward.js +2 -2
- package/dist/index.js +1 -3
- package/dist/runtime.js +17 -7
- package/dist/system-prompt.js +1 -8
- package/dist/tasks.js +169 -0
- package/dist/tools/start-thread.js +19 -4
- package/dist/tools/tasks.js +89 -0
- package/package.json +2 -2
- package/dist/tools/thread-status.js +0 -75
package/dist/build-tools.js
CHANGED
|
@@ -2,16 +2,27 @@ import { memoryTools } from "./tools/memory.js";
|
|
|
2
2
|
import { todoTools } from "./tools/todo.js";
|
|
3
3
|
import { storageTools } from "./tools/storage.js";
|
|
4
4
|
import { startThreadTools } from "./tools/start-thread.js";
|
|
5
|
+
import { tasksTools } from "./tools/tasks.js";
|
|
5
6
|
import { askAgentTools } from "./tools/ask-agent.js";
|
|
6
7
|
import { askUserTools } from "./tools/ask-user.js";
|
|
7
8
|
import { installAgentTools } from "./tools/install-agent.js";
|
|
8
|
-
import { threadStatusTools } from "./tools/thread-status.js";
|
|
9
9
|
import { threadTitleTools } from "./tools/thread-title.js";
|
|
10
10
|
import { previewTools } from "./tools/preview.js";
|
|
11
11
|
import { variablesRequestTools } from "./tools/variables-request.js";
|
|
12
12
|
import { renderWidgetTools } from "./tools/render-widget.js";
|
|
13
13
|
import { wrapToolsWithApproval } from "./tools/approval.js";
|
|
14
|
+
import { isConversationThread } from "./conversation.js";
|
|
14
15
|
export const DEFAULT_APPROVAL_ACTIONS = ["update_channel_spec", "start_thread"];
|
|
16
|
+
/**
|
|
17
|
+
* A Space conversation answers inline or starts tasks; specialist work, todos,
|
|
18
|
+
* and job title/status belong to the task.
|
|
19
|
+
*/
|
|
20
|
+
export const CONVERSATION_EXCLUDED_TOOLS = [
|
|
21
|
+
"ask_agent",
|
|
22
|
+
"todo_write",
|
|
23
|
+
"todo_read",
|
|
24
|
+
"set_thread_title",
|
|
25
|
+
];
|
|
15
26
|
/**
|
|
16
27
|
* Merge first-party OpenBot coordinator tools and wrap configured actions
|
|
17
28
|
* with approval.
|
|
@@ -22,10 +33,10 @@ export function buildOpenbotTools(args) {
|
|
|
22
33
|
...todoTools,
|
|
23
34
|
...storageTools,
|
|
24
35
|
...startThreadTools,
|
|
36
|
+
...tasksTools,
|
|
25
37
|
...askAgentTools,
|
|
26
38
|
...askUserTools,
|
|
27
39
|
...installAgentTools,
|
|
28
|
-
...threadStatusTools,
|
|
29
40
|
...threadTitleTools,
|
|
30
41
|
...previewTools,
|
|
31
42
|
...variablesRequestTools,
|
|
@@ -33,5 +44,12 @@ export function buildOpenbotTools(args) {
|
|
|
33
44
|
...args.context.tools,
|
|
34
45
|
};
|
|
35
46
|
const approvalConfig = args.context.config?.approval;
|
|
36
|
-
|
|
47
|
+
const conversation = isConversationThread(args.threadId);
|
|
48
|
+
if (conversation) {
|
|
49
|
+
for (const name of CONVERSATION_EXCLUDED_TOOLS)
|
|
50
|
+
delete tools[name];
|
|
51
|
+
}
|
|
52
|
+
// Starting tasks is what a Space conversation is for, so it does not ask first.
|
|
53
|
+
const actions = (approvalConfig?.actions ?? DEFAULT_APPROVAL_ACTIONS).filter((action) => !(action === "start_thread" && conversation));
|
|
54
|
+
return wrapToolsWithApproval(tools, actions, args.context);
|
|
37
55
|
}
|
package/dist/context.js
CHANGED
|
@@ -2,6 +2,8 @@ import { formatGitIdentity, readGitIdentity, } from "@meetopenbot/plugin-sdk";
|
|
|
2
2
|
import { formatAvailableAgentsSection, formatInstalledAgentsSection, loadAgentModelCatalogs, availableMarketplaceAgents, } from "./agent-models.js";
|
|
3
3
|
import { todoService } from "./tools/todo-service.js";
|
|
4
4
|
import { readInvokeSurface, surfaceSection } from "./surface.js";
|
|
5
|
+
import { CONVERSATION_INSTRUCTIONS, isConversationThread } from "./conversation.js";
|
|
6
|
+
import { buildTasksSection } from "./tasks.js";
|
|
5
7
|
export const DEFAULT_CONTEXT_BUDGET = 8000;
|
|
6
8
|
export const MAX_CONTEXT_FILES = 50;
|
|
7
9
|
/**
|
|
@@ -63,23 +65,8 @@ export async function buildContext(state, storage, host) {
|
|
|
63
65
|
env += `- Thread: ${threadDetails?.name || threadId}\n`;
|
|
64
66
|
}
|
|
65
67
|
sections.push(env);
|
|
66
|
-
if (threadId) {
|
|
67
|
-
|
|
68
|
-
? threadDetails.state
|
|
69
|
-
: undefined;
|
|
70
|
-
const threadStatus = typeof threadState?.status === "string" && threadState.status.trim()
|
|
71
|
-
? threadState.status.trim()
|
|
72
|
-
: "completed";
|
|
73
|
-
const threadStatusReason = typeof threadState?.statusReason === "string" && threadState.statusReason.trim()
|
|
74
|
-
? threadState.statusReason.trim()
|
|
75
|
-
: undefined;
|
|
76
|
-
let statusSection = `## THREAD STATUS\n- Status: ${threadStatus}`;
|
|
77
|
-
if (threadStatusReason) {
|
|
78
|
-
statusSection += `\n- Reason: ${threadStatusReason}`;
|
|
79
|
-
}
|
|
80
|
-
statusSection +=
|
|
81
|
-
"\nUse `set_thread_status` to update this. needs_input when blocked on the human; ready_for_review when you believe the job is done; working when the job is still in progress. Never set completed or archived.";
|
|
82
|
-
sections.push(statusSection);
|
|
68
|
+
if (isConversationThread(threadId)) {
|
|
69
|
+
sections.push(CONVERSATION_INSTRUCTIONS);
|
|
83
70
|
}
|
|
84
71
|
const invokeData = state.triggerEvent?.type === "agent:invoke"
|
|
85
72
|
? state.triggerEvent.data
|
|
@@ -111,6 +98,17 @@ export async function buildContext(state, storage, host) {
|
|
|
111
98
|
console.warn("[context] Failed to fetch todos:", error);
|
|
112
99
|
}
|
|
113
100
|
}
|
|
101
|
+
// 2.55 Tasks this thread started
|
|
102
|
+
if (!surface && channelId && threadId) {
|
|
103
|
+
try {
|
|
104
|
+
const tasks = await buildTasksSection(storage, host, { channelId, threadId });
|
|
105
|
+
if (tasks)
|
|
106
|
+
sections.push(tasks);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
console.warn("[context] Failed to fetch tasks:", error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
114
112
|
// 2.6 Installed Agents
|
|
115
113
|
const otherAgents = allAgents.filter((agent) => agent.id !== agentId);
|
|
116
114
|
if (otherAgents.length > 0) {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reserved thread that holds a Space's own conversation with OpenBot. The
|
|
3
|
+
* runtime keeps the same id; task listings skip it.
|
|
4
|
+
*/
|
|
5
|
+
export const CONVERSATION_THREAD_ID = 'conversation';
|
|
6
|
+
export function isConversationThread(threadId) {
|
|
7
|
+
return threadId === CONVERSATION_THREAD_ID;
|
|
8
|
+
}
|
|
9
|
+
/** Added to a Space conversation's context: it is a chat, not a task. */
|
|
10
|
+
export const CONVERSATION_INSTRUCTIONS = [
|
|
11
|
+
'## CONVERSATION',
|
|
12
|
+
"This is the Space's main conversation with the human, not a task. It has no title and keeps going across many requests. The JOB TITLE rules do not apply here.",
|
|
13
|
+
'- **Answer inline** when the reply needs no specialist and takes no lasting work: questions, explanations, advice, short drafts, status of Spaces or tasks, quick lookups, and setup questions.',
|
|
14
|
+
'- **Task status**: when the human asks how a task went, answer from `## TASKS`; call `get_task` for its full last reply, or `list_tasks` when it is not listed. Never say you cannot see a task before checking.',
|
|
15
|
+
'- **Start a task** with `start_thread` when the request needs a specialist (build, code, files, design, research, ops), runs long, or should be tracked on its own. Pick the Space it belongs to from `## SPACES`; the current Space is a valid target. You cannot ask specialists from here — the task does that.',
|
|
16
|
+
'- One request, one task. Several independent requests become several tasks. Follow-ups to an existing task belong in that task; tell the human to continue there instead of starting a duplicate.',
|
|
17
|
+
'- After `start_thread`, reply in one short sentence. The human sees a live card for the task; do not restate the request or promise progress updates.',
|
|
18
|
+
'- If you are unsure whether the human wants a task, ask in one line instead of starting one.',
|
|
19
|
+
].join('\n');
|
|
20
|
+
/** Human turns kept verbatim when a Space conversation is replayed to the model. */
|
|
21
|
+
export const CONVERSATION_KEPT_TURNS = 20;
|
|
22
|
+
const DIGEST_MESSAGE_LIMIT = 30;
|
|
23
|
+
const DIGEST_TEXT_LIMIT = 200;
|
|
24
|
+
function isHumanTurn(event) {
|
|
25
|
+
if (event.type !== 'agent:invoke' || event.meta?.parentToolCallId)
|
|
26
|
+
return false;
|
|
27
|
+
return event.data?.role === 'user';
|
|
28
|
+
}
|
|
29
|
+
function clip(text) {
|
|
30
|
+
const flat = text.replace(/\s+/g, ' ').trim();
|
|
31
|
+
return flat.length > DIGEST_TEXT_LIMIT ? `${flat.slice(0, DIGEST_TEXT_LIMIT)}…` : flat;
|
|
32
|
+
}
|
|
33
|
+
function startedTask(event) {
|
|
34
|
+
if (event.type !== 'client:ui:widget')
|
|
35
|
+
return null;
|
|
36
|
+
const data = event.data;
|
|
37
|
+
if (data?.metadata?.type !== 'thread_started' || data.state !== 'submitted')
|
|
38
|
+
return null;
|
|
39
|
+
const { channelId, threadId, title } = data.metadata;
|
|
40
|
+
if (typeof channelId !== 'string' || typeof threadId !== 'string')
|
|
41
|
+
return null;
|
|
42
|
+
return { channelId, threadId, ...(typeof title === 'string' && title ? { title } : {}) };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Keeps the last {@link CONVERSATION_KEPT_TURNS} human turns (and everything
|
|
46
|
+
* after the first of them) and folds older history into a plain-text digest:
|
|
47
|
+
* what the human asked and which tasks were started. No model call.
|
|
48
|
+
*/
|
|
49
|
+
export function compactConversation(events, keptTurns = CONVERSATION_KEPT_TURNS) {
|
|
50
|
+
const turnStarts = [];
|
|
51
|
+
events.forEach((event, index) => {
|
|
52
|
+
if (isHumanTurn(event))
|
|
53
|
+
turnStarts.push(index);
|
|
54
|
+
});
|
|
55
|
+
if (turnStarts.length <= keptTurns)
|
|
56
|
+
return { events, digest: '' };
|
|
57
|
+
const cut = turnStarts[turnStarts.length - keptTurns];
|
|
58
|
+
const dropped = events.slice(0, cut);
|
|
59
|
+
const asked = dropped
|
|
60
|
+
.filter(isHumanTurn)
|
|
61
|
+
.map((event) => clip(String(event.data.content ?? '')))
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.slice(-DIGEST_MESSAGE_LIMIT);
|
|
64
|
+
const tasks = dropped
|
|
65
|
+
.map(startedTask)
|
|
66
|
+
.filter((task) => task !== null)
|
|
67
|
+
.slice(-DIGEST_MESSAGE_LIMIT);
|
|
68
|
+
const lines = [
|
|
69
|
+
'## EARLIER IN THIS CONVERSATION',
|
|
70
|
+
'Older messages were compacted. This is what remains of them; ask the human if you need more.',
|
|
71
|
+
];
|
|
72
|
+
if (asked.length > 0) {
|
|
73
|
+
lines.push('Human asked:', ...asked.map((text) => `- ${text}`));
|
|
74
|
+
}
|
|
75
|
+
if (tasks.length > 0) {
|
|
76
|
+
lines.push('Tasks started:', ...tasks.map((task) => `- ${task.title ?? 'Untitled'} — #${task.channelId}, thread ${task.threadId}`));
|
|
77
|
+
}
|
|
78
|
+
return { events: events.slice(cut), digest: lines.join('\n') };
|
|
79
|
+
}
|
package/dist/fast-forward.js
CHANGED
|
@@ -14,8 +14,8 @@ const SELF_ANSWER_QUESTION = 'openbot_should_answer';
|
|
|
14
14
|
export const FAST_FORWARD_CLOSE_HINT = [
|
|
15
15
|
'## THIS TURN',
|
|
16
16
|
'You already asked the specialist. Do not call ask_agent again.',
|
|
17
|
-
'If they finished, close in one or two sentences
|
|
18
|
-
'If they are blocked on the human,
|
|
17
|
+
'If they finished, close in one or two sentences.',
|
|
18
|
+
'If they are blocked on the human, ask with `ask_user`.',
|
|
19
19
|
'If they failed, say what blocked them.',
|
|
20
20
|
'Do not restate a successful reply.',
|
|
21
21
|
].join('\n');
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,6 @@ import { todoTools } from './tools/todo.js';
|
|
|
8
8
|
import { storageTools } from './tools/storage.js';
|
|
9
9
|
import { askAgentTools } from './tools/ask-agent.js';
|
|
10
10
|
import { installAgentTools } from './tools/install-agent.js';
|
|
11
|
-
import { threadStatusTools } from './tools/thread-status.js';
|
|
12
11
|
import { threadTitleTools } from './tools/thread-title.js';
|
|
13
12
|
import { previewTools } from './tools/preview.js';
|
|
14
13
|
import { variablesRequestTools } from './tools/variables-request.js';
|
|
@@ -50,7 +49,6 @@ export const openbotPlugin = defineOpenbotPlugin({
|
|
|
50
49
|
...storageTools,
|
|
51
50
|
...askAgentTools,
|
|
52
51
|
...installAgentTools,
|
|
53
|
-
...threadStatusTools,
|
|
54
52
|
...threadTitleTools,
|
|
55
53
|
...previewTools,
|
|
56
54
|
...variablesRequestTools,
|
|
@@ -61,7 +59,7 @@ export const openbotPlugin = defineOpenbotPlugin({
|
|
|
61
59
|
const authMode = context.host.isCloudSystemAgent(context.agentId)
|
|
62
60
|
? context.host.parseOpenbotAuthMode(context.config?.authMode)
|
|
63
61
|
: 'byok';
|
|
64
|
-
const toolDefinitions = buildOpenbotTools({ context });
|
|
62
|
+
const toolDefinitions = buildOpenbotTools({ context, threadId: args.threadId });
|
|
65
63
|
yield* runOpenbotTurn(args, {
|
|
66
64
|
model: context.config?.model || AUTO_MODEL_ID,
|
|
67
65
|
authMode,
|
package/dist/runtime.js
CHANGED
|
@@ -12,6 +12,7 @@ import { reasoningForModel, resolveModel } from './model.js';
|
|
|
12
12
|
import { errorFromStreamPart } from './stream-error.js';
|
|
13
13
|
import { createAgentOutputBuffer, isOutputBoundaryPart } from './output-buffer.js';
|
|
14
14
|
import { threadNamingHint } from './thread-naming.js';
|
|
15
|
+
import { compactConversation, isConversationThread } from './conversation.js';
|
|
15
16
|
import { readInvokeSurface, RESPOND_TOOL, surfaceTools } from './surface.js';
|
|
16
17
|
async function buildSystemPrompt(state, storage, instructions, host) {
|
|
17
18
|
const context = await buildContext(state, storage, host);
|
|
@@ -20,7 +21,8 @@ async function buildSystemPrompt(state, storage, instructions, host) {
|
|
|
20
21
|
'',
|
|
21
22
|
context,
|
|
22
23
|
];
|
|
23
|
-
|
|
24
|
+
// A Space conversation is a chat, not a job: it never gets a title.
|
|
25
|
+
if (!readInvokeSurface(state.triggerEvent) && !isConversationThread(state.threadId)) {
|
|
24
26
|
const threadState = state.threadDetails?.state;
|
|
25
27
|
sections.push(...threadNamingHint(threadState));
|
|
26
28
|
}
|
|
@@ -202,7 +204,6 @@ async function* attemptFastForward(args, options) {
|
|
|
202
204
|
data: { content: `Asking ${candidate.agentName}.` },
|
|
203
205
|
meta: { agentId: args.context.agentId, threadId: args.threadId },
|
|
204
206
|
};
|
|
205
|
-
await executeCoordinatorTool(args, options.tools, 'set_thread_status', { status: 'working' });
|
|
206
207
|
const titleTask = (async () => {
|
|
207
208
|
const threadState = args.state.threadDetails?.state;
|
|
208
209
|
if (!threadNeedsGeneratedTitle(threadState))
|
|
@@ -239,12 +240,21 @@ export async function* runOpenbotTurn(args, options) {
|
|
|
239
240
|
state.model = configuredModelString;
|
|
240
241
|
const threadId = args.threadId || state.threadId;
|
|
241
242
|
let systemPrompt = await buildSystemPrompt(state, storage, instructions, host);
|
|
243
|
+
// A Space conversation runs indefinitely; replay only its recent turns plus a digest.
|
|
244
|
+
const compactHistory = (loaded) => {
|
|
245
|
+
if (surface || !isConversationThread(threadId))
|
|
246
|
+
return loaded;
|
|
247
|
+
const compacted = compactConversation(loaded);
|
|
248
|
+
if (compacted.digest)
|
|
249
|
+
systemPrompt = `${systemPrompt}\n\n${compacted.digest}`;
|
|
250
|
+
return compacted.events;
|
|
251
|
+
};
|
|
242
252
|
let events = surface
|
|
243
253
|
? [args.event]
|
|
244
|
-
: await storage.getEvents({
|
|
254
|
+
: compactHistory(await storage.getEvents({
|
|
245
255
|
channelId: state.channelId ?? '',
|
|
246
256
|
threadId,
|
|
247
|
-
});
|
|
257
|
+
}));
|
|
248
258
|
const hasAttachments = eventsHaveAttachments(events);
|
|
249
259
|
const candidates = chooseTurnCandidates({ model: configuredModelString, events, authMode });
|
|
250
260
|
const readImage = async (name) => {
|
|
@@ -293,11 +303,11 @@ export async function* runOpenbotTurn(args, options) {
|
|
|
293
303
|
if (abortSignal?.aborted)
|
|
294
304
|
return;
|
|
295
305
|
if (forwarded) {
|
|
296
|
-
|
|
306
|
+
systemPrompt = `${await buildSystemPrompt(state, storage, instructions, host)}\n\n${FAST_FORWARD_CLOSE_HINT}`;
|
|
307
|
+
events = compactHistory(await storage.getEvents({
|
|
297
308
|
channelId: state.channelId ?? '',
|
|
298
309
|
threadId,
|
|
299
|
-
});
|
|
300
|
-
systemPrompt = `${await buildSystemPrompt(state, storage, instructions, host)}\n\n${FAST_FORWARD_CLOSE_HINT}`;
|
|
310
|
+
}));
|
|
301
311
|
}
|
|
302
312
|
// Messages the human sent while this run is going. They join the conversation
|
|
303
313
|
// before the next model step, like a reply typed between tool calls, and stay in
|
package/dist/system-prompt.js
CHANGED
|
@@ -15,7 +15,7 @@ export const OPENBOT_SYSTEM_PROMPT = [
|
|
|
15
15
|
"# THE AGENT LOOP",
|
|
16
16
|
"You operate in an iterative loop:",
|
|
17
17
|
"1. **Analyze Events**: Understand the human, mentioned people, the current Space, and `## SPACES`.",
|
|
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.
|
|
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. Inside a task, never start a thread in the Space you are already in; otherwise continue there with `ask_agent`. In a Space conversation (`## CONVERSATION`), follow its rules instead: answer inline or start a task with `start_thread`. Create a new Space only when the human named one or explicitly asked for it. Never invent placeholder ids (`noop`, `temp`, `misc`, `untitled`).",
|
|
19
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.",
|
|
20
20
|
"4. **Wait**: Let the asked agent run in its own harness. Read their reply, then continue or ask someone else.",
|
|
21
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.",
|
|
@@ -47,13 +47,6 @@ export const OPENBOT_SYSTEM_PROMPT = [
|
|
|
47
47
|
"- Call `set_thread_title` again only if the topic clearly changes.",
|
|
48
48
|
"- Never re-title a job the user renamed.",
|
|
49
49
|
"",
|
|
50
|
-
"# JOB STATUS",
|
|
51
|
-
"- Every thread is a job. Status is `working` (in progress), `needs_input`, `ready_for_review`, `completed`, or `archived`.",
|
|
52
|
-
"- Status is the job's workflow state, not whether an agent is currently executing.",
|
|
53
|
-
"- `ask_user`, `request_variables`, and connect cards set `needs_input` while they wait; do not set it yourself for those.",
|
|
54
|
-
"- When you believe the job is done, set `ready_for_review`. Never set `completed` or `archived` — only the human can.",
|
|
55
|
-
"- The current status is injected each turn as `## THREAD STATUS`.",
|
|
56
|
-
"",
|
|
57
50
|
"# OPERATIONAL GUIDELINES",
|
|
58
51
|
'- **Spaces**: Work lives in Spaces (product name for channels). `#general` is the default Space and works like any other. Prefer existing Spaces. When the request clearly belongs to another existing Space, explain the appropriate Space in text, then let them continue there or hand it over with `start_thread`. Never auto-move threads or messages; `start_thread` opens a new thread and leaves this one in place. Create a new Space only when the human asked for one, or they agreed after you suggested it.',
|
|
59
52
|
"- **Asking**: You ask specialists with `ask_agent`. If `## MENTIONED AGENTS` is present, you MUST ask those people. You may also ask others if needed. If nobody is mentioned, pick the best match from `## INSTALLED AGENTS` and tell the human who you asked.",
|
package/dist/tasks.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/** Tasks listed in `## TASKS`; older ones are a `list_tasks` call away. */
|
|
2
|
+
export const TASKS_SECTION_LIMIT = 10;
|
|
3
|
+
const SECTION_RESULT_CHARS = 200;
|
|
4
|
+
function clip(text, max) {
|
|
5
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
6
|
+
return flat.length > max ? `${flat.slice(0, max).trimEnd()}…` : flat;
|
|
7
|
+
}
|
|
8
|
+
export function withRunning(thread, host) {
|
|
9
|
+
const running = host?.isThreadRunning?.({
|
|
10
|
+
channelId: thread.channelId,
|
|
11
|
+
threadId: thread.id,
|
|
12
|
+
});
|
|
13
|
+
return typeof running === "boolean" ? { ...thread, running } : thread;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Where the task stands, in words. A task stays `working` after its run ends
|
|
17
|
+
* until someone moves it on, so the last run's outcome decides what that means.
|
|
18
|
+
*/
|
|
19
|
+
export function describeTaskState(task) {
|
|
20
|
+
const reason = task.statusReason ? `: ${task.statusReason}` : "";
|
|
21
|
+
// A run waiting on the human's answer is still "running"; the wait matters more.
|
|
22
|
+
if (task.status === "needs_input")
|
|
23
|
+
return `waiting for the human${reason}`;
|
|
24
|
+
if (task.running)
|
|
25
|
+
return "running now";
|
|
26
|
+
switch (task.status) {
|
|
27
|
+
case "ready_for_review":
|
|
28
|
+
return `ready for review${reason}`;
|
|
29
|
+
case "completed":
|
|
30
|
+
return "completed";
|
|
31
|
+
case "archived":
|
|
32
|
+
return "archived";
|
|
33
|
+
}
|
|
34
|
+
if (task.running === undefined)
|
|
35
|
+
return "working";
|
|
36
|
+
const result = task.result;
|
|
37
|
+
if (!result)
|
|
38
|
+
return "not running, no result recorded";
|
|
39
|
+
if (result.error)
|
|
40
|
+
return `last run failed: ${clip(result.error, SECTION_RESULT_CHARS)}`;
|
|
41
|
+
if (result.stopped)
|
|
42
|
+
return "stopped by the human";
|
|
43
|
+
return "last run finished";
|
|
44
|
+
}
|
|
45
|
+
function formatTaskLine(task) {
|
|
46
|
+
const title = task.name && task.name !== task.id ? task.name : "Untitled";
|
|
47
|
+
const lines = [
|
|
48
|
+
`- ${title} — #${task.channelId}, thread ${task.id} — ${describeTaskState(task)}`,
|
|
49
|
+
];
|
|
50
|
+
const summary = task.result?.summary;
|
|
51
|
+
if (summary && !task.running) {
|
|
52
|
+
lines.push(` Last reply: ${clip(summary, SECTION_RESULT_CHARS)}`);
|
|
53
|
+
}
|
|
54
|
+
return lines.join("\n");
|
|
55
|
+
}
|
|
56
|
+
export function formatTasksSection(tasks) {
|
|
57
|
+
if (tasks.length === 0)
|
|
58
|
+
return "";
|
|
59
|
+
return [
|
|
60
|
+
"## TASKS",
|
|
61
|
+
"Tasks started from this conversation, most recently active first. Answer questions about them from this list. Call `get_task` for a task's full last reply, and `list_tasks` for older tasks or tasks in other Spaces.",
|
|
62
|
+
...tasks.map(formatTaskLine),
|
|
63
|
+
].join("\n");
|
|
64
|
+
}
|
|
65
|
+
export function isFromOrigin(task, origin) {
|
|
66
|
+
return task.origin?.channelId === origin.channelId && task.origin?.threadId === origin.threadId;
|
|
67
|
+
}
|
|
68
|
+
export async function listTasks(storage, host, args) {
|
|
69
|
+
const page = await storage.getThreads({ sort: "updatedAt", order: "desc", ...args });
|
|
70
|
+
return { tasks: page.items.map((thread) => withRunning(thread, host)), total: page.total };
|
|
71
|
+
}
|
|
72
|
+
/** The `## TASKS` section for a thread, or "" when it started none. */
|
|
73
|
+
export async function buildTasksSection(storage, host, origin) {
|
|
74
|
+
if (!storage?.getThreads)
|
|
75
|
+
return "";
|
|
76
|
+
const { tasks } = await listTasks(storage, host, {
|
|
77
|
+
origin,
|
|
78
|
+
limit: TASKS_SECTION_LIMIT,
|
|
79
|
+
});
|
|
80
|
+
// Older runtimes ignore the origin filter and list every thread.
|
|
81
|
+
return formatTasksSection(tasks.filter((task) => isFromOrigin(task, origin)));
|
|
82
|
+
}
|
|
83
|
+
const STATUSES = [
|
|
84
|
+
"working",
|
|
85
|
+
"needs_input",
|
|
86
|
+
"ready_for_review",
|
|
87
|
+
"completed",
|
|
88
|
+
"archived",
|
|
89
|
+
];
|
|
90
|
+
function isRecord(value) {
|
|
91
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
92
|
+
}
|
|
93
|
+
function text(value) {
|
|
94
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
95
|
+
}
|
|
96
|
+
/** Reads a task from its thread `state.json`, as `getThreadDetails` returns it. */
|
|
97
|
+
export function taskFromState(channelId, threadId, rawState) {
|
|
98
|
+
const state = isRecord(rawState) ? rawState : {};
|
|
99
|
+
const status = STATUSES.find((value) => value === state.status) ?? "completed";
|
|
100
|
+
const statusReason = text(state.statusReason);
|
|
101
|
+
const origin = isRecord(state.origin) ? state.origin : undefined;
|
|
102
|
+
const originChannelId = text(origin?.channelId);
|
|
103
|
+
const originThreadId = text(origin?.threadId);
|
|
104
|
+
const result = isRecord(state.result) ? state.result : undefined;
|
|
105
|
+
const endedAt = text(result?.endedAt);
|
|
106
|
+
const error = text(result?.error);
|
|
107
|
+
const preview = isRecord(state.preview) ? state.preview : undefined;
|
|
108
|
+
const firstMessage = text(preview?.content);
|
|
109
|
+
const latestAgentOutput = text(preview?.latestAgentOutput);
|
|
110
|
+
const updatedAt = text(state.statusUpdatedAt);
|
|
111
|
+
return {
|
|
112
|
+
id: threadId,
|
|
113
|
+
name: text(state.name) ?? threadId,
|
|
114
|
+
channelId,
|
|
115
|
+
createdAt: new Date(0),
|
|
116
|
+
updatedAt: updatedAt ? new Date(updatedAt) : new Date(0),
|
|
117
|
+
status,
|
|
118
|
+
...(statusReason ? { statusReason } : {}),
|
|
119
|
+
...(originChannelId && originThreadId
|
|
120
|
+
? { origin: { channelId: originChannelId, threadId: originThreadId } }
|
|
121
|
+
: {}),
|
|
122
|
+
...(result && endedAt
|
|
123
|
+
? {
|
|
124
|
+
result: {
|
|
125
|
+
summary: text(result.summary) ?? "",
|
|
126
|
+
endedAt,
|
|
127
|
+
...(error ? { error } : {}),
|
|
128
|
+
...(result.stopped === true ? { stopped: true } : {}),
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
: {}),
|
|
132
|
+
...(firstMessage
|
|
133
|
+
? {
|
|
134
|
+
preview: {
|
|
135
|
+
content: firstMessage,
|
|
136
|
+
...(latestAgentOutput ? { latestAgentOutput } : {}),
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
: {}),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
export function formatTaskList(tasks, total) {
|
|
143
|
+
if (tasks.length === 0)
|
|
144
|
+
return "No tasks match.";
|
|
145
|
+
const more = total > tasks.length ? `\n(${total - tasks.length} more not shown.)` : "";
|
|
146
|
+
return `${tasks.map(formatTaskLine).join("\n")}${more}`;
|
|
147
|
+
}
|
|
148
|
+
/** Everything `get_task` reports: state, the request, the last run's reply, and todos. */
|
|
149
|
+
export function formatTaskDetail(task, todos = []) {
|
|
150
|
+
const title = task.name && task.name !== task.id ? task.name : "Untitled";
|
|
151
|
+
const lines = [
|
|
152
|
+
`Task: ${title} — #${task.channelId}, thread ${task.id}`,
|
|
153
|
+
`State: ${describeTaskState(task)}`,
|
|
154
|
+
];
|
|
155
|
+
if (task.preview?.content)
|
|
156
|
+
lines.push(`Request: ${clip(task.preview.content, 600)}`);
|
|
157
|
+
if (task.result) {
|
|
158
|
+
lines.push(`Last run ended: ${task.result.endedAt}`);
|
|
159
|
+
if (task.result.summary)
|
|
160
|
+
lines.push(`Last reply:\n${task.result.summary}`);
|
|
161
|
+
}
|
|
162
|
+
else if (task.preview?.latestAgentOutput) {
|
|
163
|
+
lines.push(`Latest reply so far:\n${task.preview.latestAgentOutput}`);
|
|
164
|
+
}
|
|
165
|
+
if (todos.length > 0) {
|
|
166
|
+
lines.push("Todos:", ...todos.map((todo) => `- [${todo.status}] ${todo.content}`));
|
|
167
|
+
}
|
|
168
|
+
return lines.join("\n");
|
|
169
|
+
}
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import z from 'zod';
|
|
3
3
|
import { isPlaceholderSpaceId } from '../space-id.js';
|
|
4
4
|
import { normalizeChannelId } from './storage.js';
|
|
5
|
+
import { isConversationThread } from '../conversation.js';
|
|
5
6
|
async function emitStartThreadWidget(ctx, args) {
|
|
6
7
|
const actions = args.state === 'submitted' && args.threadUrl
|
|
7
8
|
? [
|
|
@@ -24,7 +25,13 @@ async function emitStartThreadWidget(ctx, args) {
|
|
|
24
25
|
display: args.state === 'submitted' ? 'expanded' : 'collapsed',
|
|
25
26
|
state: args.state,
|
|
26
27
|
...(actions ? { actions } : {}),
|
|
27
|
-
metadata: {
|
|
28
|
+
metadata: {
|
|
29
|
+
type: 'thread_started',
|
|
30
|
+
channelId: args.channelId || undefined,
|
|
31
|
+
// The task card follows this thread's live status.
|
|
32
|
+
...(args.threadId ? { threadId: args.threadId } : {}),
|
|
33
|
+
...(args.title ? { title: args.title } : {}),
|
|
34
|
+
},
|
|
28
35
|
},
|
|
29
36
|
meta: {
|
|
30
37
|
agentId: ctx.agentId,
|
|
@@ -35,7 +42,7 @@ async function emitStartThreadWidget(ctx, args) {
|
|
|
35
42
|
}
|
|
36
43
|
export const startThreadTools = {
|
|
37
44
|
start_thread: {
|
|
38
|
-
description: "Start a new thread in
|
|
45
|
+
description: "Start a new thread (task) in an existing Space and hand the human's request to that Space's agent. From a Space conversation (`## CONVERSATION`), use it for every real piece of work, in the current Space or another one. From inside a task, use it only when work clearly belongs in a different Space from `## SPACES`. The new thread runs on its own; the human follows it there.",
|
|
39
46
|
inputSchema: z.object({
|
|
40
47
|
channelId: z.string().describe('Existing Space id from `## SPACES` (e.g. backend-platform).'),
|
|
41
48
|
prompt: z
|
|
@@ -69,7 +76,8 @@ export const startThreadTools = {
|
|
|
69
76
|
if (isPlaceholderSpaceId(targetChannelId)) {
|
|
70
77
|
return fail(`Refused to start a thread in #${targetChannelId}: that is not a real Space.`);
|
|
71
78
|
}
|
|
72
|
-
|
|
79
|
+
const fromConversation = isConversationThread(ctx.threadId ?? ctx.state.threadId);
|
|
80
|
+
if (!fromConversation && targetChannelId === (ctx.channelId ?? ctx.state.channelId)) {
|
|
73
81
|
return fail(`You are already in #${targetChannelId}. Continue here instead of starting a thread.`);
|
|
74
82
|
}
|
|
75
83
|
if (!ctx.host.startThreadRun) {
|
|
@@ -84,16 +92,22 @@ export const startThreadTools = {
|
|
|
84
92
|
await emitStartThreadWidget(ctx, {
|
|
85
93
|
widgetId,
|
|
86
94
|
channelId: targetChannelId,
|
|
95
|
+
threadId,
|
|
87
96
|
title: threadTitle,
|
|
88
97
|
body: 'Starting…',
|
|
89
98
|
state: 'open',
|
|
90
99
|
});
|
|
100
|
+
const originChannelId = ctx.channelId ?? ctx.state.channelId;
|
|
101
|
+
const originThreadId = ctx.threadId ?? ctx.state.threadId;
|
|
91
102
|
try {
|
|
92
103
|
await ctx.storage.createThread({
|
|
93
104
|
channelId: targetChannelId,
|
|
94
105
|
threadId,
|
|
95
106
|
...(threadTitle ? { threadTitle } : {}),
|
|
96
|
-
|
|
107
|
+
// Lets the thread that started it list it under `## TASKS`.
|
|
108
|
+
initialState: originChannelId && originThreadId
|
|
109
|
+
? { origin: { channelId: originChannelId, threadId: originThreadId } }
|
|
110
|
+
: {},
|
|
97
111
|
});
|
|
98
112
|
const { agentId } = await ctx.host.startThreadRun({
|
|
99
113
|
channelId: targetChannelId,
|
|
@@ -103,6 +117,7 @@ export const startThreadTools = {
|
|
|
103
117
|
await emitStartThreadWidget(ctx, {
|
|
104
118
|
widgetId,
|
|
105
119
|
channelId: targetChannelId,
|
|
120
|
+
threadId,
|
|
106
121
|
title: threadTitle,
|
|
107
122
|
body: `Started a thread in #${targetChannelId}.`,
|
|
108
123
|
state: 'submitted',
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import { normalizeChannelId } from "./storage.js";
|
|
3
|
+
import { todoService } from "./todo-service.js";
|
|
4
|
+
import { formatTaskDetail, formatTaskList, listTasks, taskFromState, withRunning, } from "../tasks.js";
|
|
5
|
+
const LIST_DEFAULT_LIMIT = 20;
|
|
6
|
+
const LIST_MAX_LIMIT = 50;
|
|
7
|
+
const STATUS_VALUES = [
|
|
8
|
+
"working",
|
|
9
|
+
"needs_input",
|
|
10
|
+
"ready_for_review",
|
|
11
|
+
"completed",
|
|
12
|
+
"archived",
|
|
13
|
+
];
|
|
14
|
+
export const tasksTools = {
|
|
15
|
+
list_tasks: {
|
|
16
|
+
description: "List tasks (threads) with their state and last reply, most recently active first. Use it when a task the human asks about is not in `## TASKS`, or to see tasks in a Space. Read-only.",
|
|
17
|
+
inputSchema: z.object({
|
|
18
|
+
channelId: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("Space id from `## SPACES`. Omit to list tasks in every Space."),
|
|
22
|
+
fromThisThread: z
|
|
23
|
+
.boolean()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("Only tasks started from the current thread with `start_thread`."),
|
|
26
|
+
status: z
|
|
27
|
+
.array(z.enum(STATUS_VALUES))
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("Only these statuses. Archived tasks are left out unless listed here."),
|
|
30
|
+
limit: z.number().int().min(1).max(LIST_MAX_LIMIT).optional(),
|
|
31
|
+
}),
|
|
32
|
+
execute: async (raw, ctx) => {
|
|
33
|
+
const args = (raw ?? {});
|
|
34
|
+
try {
|
|
35
|
+
const channelId = args.channelId ? normalizeChannelId(args.channelId) : undefined;
|
|
36
|
+
const currentChannelId = ctx.channelId ?? ctx.state.channelId;
|
|
37
|
+
const currentThreadId = ctx.threadId ?? ctx.state.threadId;
|
|
38
|
+
const origin = args.fromThisThread && currentChannelId && currentThreadId
|
|
39
|
+
? { channelId: currentChannelId, threadId: currentThreadId }
|
|
40
|
+
: undefined;
|
|
41
|
+
const { tasks, total } = await listTasks(ctx.storage, ctx.host, {
|
|
42
|
+
...(channelId ? { channelId } : {}),
|
|
43
|
+
...(origin ? { origin } : {}),
|
|
44
|
+
...(args.status?.length ? { status: args.status, excludeStatus: [] } : {}),
|
|
45
|
+
limit: Math.min(args.limit ?? LIST_DEFAULT_LIMIT, LIST_MAX_LIMIT),
|
|
46
|
+
});
|
|
47
|
+
return { success: true, total, output: formatTaskList(tasks, total) };
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
51
|
+
return { success: false, error: message, output: `Failed to list tasks: ${message}` };
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
get_task: {
|
|
56
|
+
description: "Read one task: its state, the request, the full last reply, and its todos. Use it to answer how a task went. Read-only.",
|
|
57
|
+
inputSchema: z.object({
|
|
58
|
+
channelId: z.string().describe("The task's Space id."),
|
|
59
|
+
threadId: z.string().describe("The task's thread id."),
|
|
60
|
+
}),
|
|
61
|
+
execute: async (raw, ctx) => {
|
|
62
|
+
const args = (raw ?? {});
|
|
63
|
+
const channelId = normalizeChannelId(args.channelId ?? "");
|
|
64
|
+
const threadId = typeof args.threadId === "string" ? args.threadId.trim() : "";
|
|
65
|
+
if (!channelId || !threadId) {
|
|
66
|
+
const error = "channelId and threadId are required";
|
|
67
|
+
return { success: false, error, output: error };
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const details = await ctx.storage.getThreadDetails({ channelId, threadId });
|
|
71
|
+
const state = details.state;
|
|
72
|
+
if (!state || typeof state !== "object" || Object.keys(state).length === 0) {
|
|
73
|
+
const error = `No task ${threadId} in #${channelId}.`;
|
|
74
|
+
return { success: false, error, output: error };
|
|
75
|
+
}
|
|
76
|
+
const task = withRunning(taskFromState(channelId, threadId, state), ctx.host);
|
|
77
|
+
const todos = await todoService
|
|
78
|
+
.getTodos({ channelId, threadId })
|
|
79
|
+
.then((list) => list.items)
|
|
80
|
+
.catch(() => []);
|
|
81
|
+
return { success: true, output: formatTaskDetail(task, todos) };
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
85
|
+
return { success: false, error: message, output: `Failed to read task: ${message}` };
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meetopenbot/openbot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.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.5.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/node": "^20.10.1",
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import z from "zod";
|
|
2
|
-
const AGENT_THREAD_STATUSES = [
|
|
3
|
-
"working",
|
|
4
|
-
"needs_input",
|
|
5
|
-
"ready_for_review",
|
|
6
|
-
];
|
|
7
|
-
function formatStatusOutput(args) {
|
|
8
|
-
const reason = args.reason?.trim();
|
|
9
|
-
return reason
|
|
10
|
-
? `Job status set to ${args.status}: ${reason}`
|
|
11
|
-
: `Job status set to ${args.status}.`;
|
|
12
|
-
}
|
|
13
|
-
export const toolDefinitions = {
|
|
14
|
-
set_thread_status: {
|
|
15
|
-
description: "Set this job's workflow status. Use needs_input when blocked on the human, ready_for_review when you believe the work is done, working when the job is still in progress. Do not use working to mean you are currently executing — that is tracked automatically. Never mark completed or archived — only the human can.",
|
|
16
|
-
inputSchema: z.object({
|
|
17
|
-
status: z
|
|
18
|
-
.enum(AGENT_THREAD_STATUSES)
|
|
19
|
-
.describe("working — job is in progress; needs_input — blocked on the human; ready_for_review — you believe the job is done."),
|
|
20
|
-
reason: z
|
|
21
|
-
.string()
|
|
22
|
-
.optional()
|
|
23
|
-
.describe("Short reason shown in the job list, e.g. what you need from the human."),
|
|
24
|
-
}),
|
|
25
|
-
},
|
|
26
|
-
};
|
|
27
|
-
export const tools = {
|
|
28
|
-
set_thread_status: {
|
|
29
|
-
...toolDefinitions.set_thread_status,
|
|
30
|
-
execute: async (rawArgs, ctx) => {
|
|
31
|
-
try {
|
|
32
|
-
const channelId = ctx.channelId ?? ctx.state.channelId ?? '';
|
|
33
|
-
const threadId = ctx.threadId ?? ctx.state.threadId;
|
|
34
|
-
if (!channelId || !threadId) {
|
|
35
|
-
throw new Error("Missing channelId or threadId for set_thread_status");
|
|
36
|
-
}
|
|
37
|
-
const data = (rawArgs ?? {});
|
|
38
|
-
const status = data.status;
|
|
39
|
-
if (status !== "working" &&
|
|
40
|
-
status !== "needs_input" &&
|
|
41
|
-
status !== "ready_for_review") {
|
|
42
|
-
throw new Error("Agents may only set working, needs_input, or ready_for_review. The human marks completed and archived.");
|
|
43
|
-
}
|
|
44
|
-
const reason = typeof data.reason === "string" && data.reason.trim()
|
|
45
|
-
? data.reason.trim()
|
|
46
|
-
: "";
|
|
47
|
-
await ctx.storage.patchThreadState({
|
|
48
|
-
channelId,
|
|
49
|
-
threadId,
|
|
50
|
-
state: {
|
|
51
|
-
status,
|
|
52
|
-
statusUpdatedAt: new Date().toISOString(),
|
|
53
|
-
statusReason: reason,
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
ctx.state.threadDetails = await ctx.storage.getThreadDetails({
|
|
57
|
-
channelId,
|
|
58
|
-
threadId,
|
|
59
|
-
});
|
|
60
|
-
const output = formatStatusOutput({ status, reason });
|
|
61
|
-
return {
|
|
62
|
-
success: true,
|
|
63
|
-
status,
|
|
64
|
-
...(reason ? { reason } : {}),
|
|
65
|
-
output,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
catch (error) {
|
|
69
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
70
|
-
return { success: false, error: message, output: message };
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
};
|
|
75
|
-
export const threadStatusTools = tools;
|