@meetopenbot/openbot 1.8.0 → 1.9.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.
@@ -5,13 +5,23 @@ import { startThreadTools } from "./tools/start-thread.js";
5
5
  import { askAgentTools } from "./tools/ask-agent.js";
6
6
  import { askUserTools } from "./tools/ask-user.js";
7
7
  import { installAgentTools } from "./tools/install-agent.js";
8
- import { threadStatusTools } from "./tools/thread-status.js";
9
8
  import { threadTitleTools } from "./tools/thread-title.js";
10
9
  import { previewTools } from "./tools/preview.js";
11
10
  import { variablesRequestTools } from "./tools/variables-request.js";
12
11
  import { renderWidgetTools } from "./tools/render-widget.js";
13
12
  import { wrapToolsWithApproval } from "./tools/approval.js";
13
+ import { isConversationThread } from "./conversation.js";
14
14
  export const DEFAULT_APPROVAL_ACTIONS = ["update_channel_spec", "start_thread"];
15
+ /**
16
+ * A Space conversation answers inline or starts tasks; specialist work, todos,
17
+ * and job title/status belong to the task.
18
+ */
19
+ export const CONVERSATION_EXCLUDED_TOOLS = [
20
+ "ask_agent",
21
+ "todo_write",
22
+ "todo_read",
23
+ "set_thread_title",
24
+ ];
15
25
  /**
16
26
  * Merge first-party OpenBot coordinator tools and wrap configured actions
17
27
  * with approval.
@@ -25,7 +35,6 @@ export function buildOpenbotTools(args) {
25
35
  ...askAgentTools,
26
36
  ...askUserTools,
27
37
  ...installAgentTools,
28
- ...threadStatusTools,
29
38
  ...threadTitleTools,
30
39
  ...previewTools,
31
40
  ...variablesRequestTools,
@@ -33,5 +42,12 @@ export function buildOpenbotTools(args) {
33
42
  ...args.context.tools,
34
43
  };
35
44
  const approvalConfig = args.context.config?.approval;
36
- return wrapToolsWithApproval(tools, approvalConfig?.actions ?? DEFAULT_APPROVAL_ACTIONS, args.context);
45
+ const conversation = isConversationThread(args.threadId);
46
+ if (conversation) {
47
+ for (const name of CONVERSATION_EXCLUDED_TOOLS)
48
+ delete tools[name];
49
+ }
50
+ // Starting tasks is what a Space conversation is for, so it does not ask first.
51
+ const actions = (approvalConfig?.actions ?? DEFAULT_APPROVAL_ACTIONS).filter((action) => !(action === "start_thread" && conversation));
52
+ return wrapToolsWithApproval(tools, actions, args.context);
37
53
  }
package/dist/context.js CHANGED
@@ -2,6 +2,7 @@ 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";
5
6
  export const DEFAULT_CONTEXT_BUDGET = 8000;
6
7
  export const MAX_CONTEXT_FILES = 50;
7
8
  /**
@@ -63,23 +64,8 @@ export async function buildContext(state, storage, host) {
63
64
  env += `- Thread: ${threadDetails?.name || threadId}\n`;
64
65
  }
65
66
  sections.push(env);
66
- if (threadId) {
67
- const threadState = threadDetails?.state && typeof threadDetails.state === "object"
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);
67
+ if (isConversationThread(threadId)) {
68
+ sections.push(CONVERSATION_INSTRUCTIONS);
83
69
  }
84
70
  const invokeData = state.triggerEvent?.type === "agent:invoke"
85
71
  ? state.triggerEvent.data
@@ -0,0 +1,78 @@
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
+ '- **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.',
15
+ '- 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.',
16
+ '- 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.',
17
+ '- If you are unsure whether the human wants a task, ask in one line instead of starting one.',
18
+ ].join('\n');
19
+ /** Human turns kept verbatim when a Space conversation is replayed to the model. */
20
+ export const CONVERSATION_KEPT_TURNS = 20;
21
+ const DIGEST_MESSAGE_LIMIT = 30;
22
+ const DIGEST_TEXT_LIMIT = 200;
23
+ function isHumanTurn(event) {
24
+ if (event.type !== 'agent:invoke' || event.meta?.parentToolCallId)
25
+ return false;
26
+ return event.data?.role === 'user';
27
+ }
28
+ function clip(text) {
29
+ const flat = text.replace(/\s+/g, ' ').trim();
30
+ return flat.length > DIGEST_TEXT_LIMIT ? `${flat.slice(0, DIGEST_TEXT_LIMIT)}…` : flat;
31
+ }
32
+ function startedTask(event) {
33
+ if (event.type !== 'client:ui:widget')
34
+ return null;
35
+ const data = event.data;
36
+ if (data?.metadata?.type !== 'thread_started' || data.state !== 'submitted')
37
+ return null;
38
+ const { channelId, threadId, title } = data.metadata;
39
+ if (typeof channelId !== 'string' || typeof threadId !== 'string')
40
+ return null;
41
+ return { channelId, threadId, ...(typeof title === 'string' && title ? { title } : {}) };
42
+ }
43
+ /**
44
+ * Keeps the last {@link CONVERSATION_KEPT_TURNS} human turns (and everything
45
+ * after the first of them) and folds older history into a plain-text digest:
46
+ * what the human asked and which tasks were started. No model call.
47
+ */
48
+ export function compactConversation(events, keptTurns = CONVERSATION_KEPT_TURNS) {
49
+ const turnStarts = [];
50
+ events.forEach((event, index) => {
51
+ if (isHumanTurn(event))
52
+ turnStarts.push(index);
53
+ });
54
+ if (turnStarts.length <= keptTurns)
55
+ return { events, digest: '' };
56
+ const cut = turnStarts[turnStarts.length - keptTurns];
57
+ const dropped = events.slice(0, cut);
58
+ const asked = dropped
59
+ .filter(isHumanTurn)
60
+ .map((event) => clip(String(event.data.content ?? '')))
61
+ .filter(Boolean)
62
+ .slice(-DIGEST_MESSAGE_LIMIT);
63
+ const tasks = dropped
64
+ .map(startedTask)
65
+ .filter((task) => task !== null)
66
+ .slice(-DIGEST_MESSAGE_LIMIT);
67
+ const lines = [
68
+ '## EARLIER IN THIS CONVERSATION',
69
+ 'Older messages were compacted. This is what remains of them; ask the human if you need more.',
70
+ ];
71
+ if (asked.length > 0) {
72
+ lines.push('Human asked:', ...asked.map((text) => `- ${text}`));
73
+ }
74
+ if (tasks.length > 0) {
75
+ lines.push('Tasks started:', ...tasks.map((task) => `- ${task.title ?? 'Untitled'} — #${task.channelId}, thread ${task.threadId}`));
76
+ }
77
+ return { events: events.slice(cut), digest: lines.join('\n') };
78
+ }
@@ -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 and set ready_for_review.',
18
- 'If they are blocked on the human, set needs_input.',
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
- if (!readInvokeSurface(state.triggerEvent)) {
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
- events = await storage.getEvents({
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
@@ -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. 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
+ "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.",
@@ -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: { type: 'thread_started', channelId: args.channelId || undefined },
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 another existing Space and hand the human's request to that Space's agent. Use when work asked here clearly belongs in a different Space from `## SPACES`. The new thread runs on its own; the human follows it there. Never use it for the Space you are already 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
- if (targetChannelId === (ctx.channelId ?? ctx.state.channelId)) {
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,6 +92,7 @@ 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',
@@ -103,6 +112,7 @@ export const startThreadTools = {
103
112
  await emitStartThreadWidget(ctx, {
104
113
  widgetId,
105
114
  channelId: targetChannelId,
115
+ threadId,
106
116
  title: threadTitle,
107
117
  body: `Started a thread in #${targetChannelId}.`,
108
118
  state: 'submitted',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meetopenbot/openbot",
3
- "version": "1.8.0",
3
+ "version": "1.9.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",
@@ -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;