@meetopenbot/openbot 1.2.18 → 1.2.19

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.
@@ -6,6 +6,7 @@ import { installAgentTools } from "./tools/install-agent.js";
6
6
  import { threadStatusTools } from "./tools/thread-status.js";
7
7
  import { threadTitleTools } from "./tools/thread-title.js";
8
8
  import { previewTools } from "./tools/preview.js";
9
+ import { suggestSpaceTools } from "./tools/suggest-space.js";
9
10
  import { wrapToolsWithApproval } from "./tools/approval.js";
10
11
  export const DEFAULT_APPROVAL_ACTIONS = ["update_channel_spec"];
11
12
  /**
@@ -22,6 +23,7 @@ export function buildOpenbotTools(args) {
22
23
  ...threadStatusTools,
23
24
  ...threadTitleTools,
24
25
  ...previewTools,
26
+ ...suggestSpaceTools,
25
27
  ...args.context.tools,
26
28
  };
27
29
  const approvalConfig = args.context.config?.approval;
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ import { installAgentTools } from './tools/install-agent.js';
11
11
  import { threadStatusTools } from './tools/thread-status.js';
12
12
  import { threadTitleTools } from './tools/thread-title.js';
13
13
  import { previewTools } from './tools/preview.js';
14
+ import { suggestSpaceTools } from './tools/suggest-space.js';
14
15
  import { buildOpenbotTools } from './build-tools.js';
15
16
  export const OPENBOT_PLUGIN_ID = '@meetopenbot/openbot';
16
17
  const modelField = await modelConfigField({
@@ -51,6 +52,7 @@ export const openbotPlugin = defineOpenbotPlugin({
51
52
  ...threadStatusTools,
52
53
  ...threadTitleTools,
53
54
  ...previewTools,
55
+ ...suggestSpaceTools,
54
56
  },
55
57
  async *run(args) {
56
58
  const { context } = args;
@@ -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**: Use `## SPACES` to see where work belongs. If the human's intent clearly fits a different existing Space than the one you are in — especially project work posted in `#general` — do not move or copy their message. Reply in plain text: name the Space (e.g. `#checkout`), why it fits, and that they can open that Space and send the same ask there to start a thread. In `#general`, handle questions, listings, setup, and routing only; do not run project execution there. If they are already in the right Space, continue with `ask_agent`. If no Space fits, propose a concrete name and why, then wait for the human to agree before `create_channel` — unless they already named a new Space or explicitly asked you to create it. Never invent placeholder ids (`noop`, `temp`, `misc`, `untitled`).",
18
+ "2. **Right Space**: Use `## SPACES` to see where work belongs. If the human's intent clearly fits a different existing Space than the one you are in — especially project work posted in `#general` — call `suggest_space` with that Space id, a self-contained version of their ask, and a one-line reason, then stop. Do not also explain it in prose, and do not `ask_agent` in this thread. Do not move or copy their message. In `#general`, handle questions, listings, setup, and routing only; do not run project execution there. If they are already in the right Space, continue with `ask_agent`. If no Space fits, propose a concrete name and why, then wait for the human to agree before `create_channel` — unless they already named a new Space or explicitly asked you to create it. Never invent placeholder ids (`noop`, `temp`, `misc`, `untitled`). Never call `suggest_space` for a Space that is not in `## SPACES`.",
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**: 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.",
@@ -42,7 +42,7 @@ export const OPENBOT_SYSTEM_PROMPT = [
42
42
  "- The current status is injected each turn as `## THREAD STATUS`.",
43
43
  "",
44
44
  "# OPERATIONAL GUIDELINES",
45
- '- **Spaces**: Work lives in Spaces (product name for channels). `#general` is dispatch — setup, questions, listings, routing, and "what should we do next." Project, coding, design, and ops work belongs in a dedicated Space. Prefer existing Spaces. When the human is in the wrong Space, tell them which Space to use and let them post there; never auto-move threads or messages. Create a new Space only when the human asked for one, or they agreed after you suggested it.',
45
+ '- **Spaces**: Work lives in Spaces (product name for channels). `#general` is dispatch — setup, questions, listings, routing, and "what should we do next." Project, coding, design, and ops work belongs in a dedicated Space. Prefer existing Spaces. When the human is in the wrong existing Space, call `suggest_space` and stop so they can edit the Space and the message, then send it as a new thread. Never auto-move threads or messages. Create a new Space only when the human asked for one, or they agreed after you suggested it.',
46
46
  "- **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.",
47
47
  "- **Models**: When you `ask_agent`, pick `model` from that agent's list in `## INSTALLED AGENTS` using the descriptions — cheaper/faster for simple work, stronger for complex coding or reasoning. Omit `model` to use their default. Do not reuse another agent's ids.",
48
48
  "- **Closer**: After `ask_agent`, you are the closer, not a second author of their work. Don't repeat. Add or close. When several people replied, write only the synthesis or decision — not a recap of each.",
@@ -0,0 +1,115 @@
1
+ import z from 'zod';
2
+ import { isPlaceholderSpaceId } from '../space-id.js';
3
+ /** Widget metadata the client uses to render a Space handoff card. */
4
+ export const SPACE_HANDOFF_TYPE = 'space_handoff';
5
+ const MAX_MESSAGE_LENGTH = 8000;
6
+ const MAX_REASON_LENGTH = 240;
7
+ function normalizeChannelId(raw) {
8
+ return raw
9
+ .trim()
10
+ .toLowerCase()
11
+ .replace(/[^a-z0-9]+/g, '-')
12
+ .replace(/(^-|-$)/g, '');
13
+ }
14
+ function spaceOptionLabel(space) {
15
+ const name = space.name?.trim();
16
+ if (!name || name.toLowerCase() === space.id)
17
+ return `#${space.id}`;
18
+ return `#${space.id} · ${name}`;
19
+ }
20
+ function failure(channelId, error) {
21
+ return { success: false, channelId, error, output: error };
22
+ }
23
+ async function suggestSpace(rawArgs, ctx) {
24
+ const data = (rawArgs ?? {});
25
+ const channelId = normalizeChannelId(typeof data.channelId === 'string' ? data.channelId : '');
26
+ const message = typeof data.message === 'string' ? data.message.trim() : '';
27
+ const reason = typeof data.reason === 'string' ? data.reason.trim().slice(0, MAX_REASON_LENGTH) : '';
28
+ const currentId = normalizeChannelId(ctx.channelId ?? ctx.state.channelId ?? '');
29
+ if (!channelId)
30
+ return failure('', 'channelId is required');
31
+ if (isPlaceholderSpaceId(channelId)) {
32
+ return failure(channelId, `Refused to suggest #${channelId}: that looks like a placeholder, not a real Space.`);
33
+ }
34
+ if (!message)
35
+ return failure(channelId, 'message is required');
36
+ if (message.length > MAX_MESSAGE_LENGTH) {
37
+ return failure(channelId, `message must be ${MAX_MESSAGE_LENGTH} characters or fewer.`);
38
+ }
39
+ if (currentId && channelId === currentId) {
40
+ return failure(channelId, `Already in #${channelId}. Continue here with ask_agent.`);
41
+ }
42
+ const channels = (await ctx.storage.getChannels());
43
+ const target = channels.find((space) => normalizeChannelId(space.id) === channelId);
44
+ if (!target) {
45
+ return failure(channelId, `Space #${channelId} does not exist. Suggest a name and wait before create_channel.`);
46
+ }
47
+ const targetId = target.id;
48
+ const options = channels
49
+ .filter((space) => normalizeChannelId(space.id) !== currentId)
50
+ .map((space) => ({ label: spaceOptionLabel(space), value: space.id }));
51
+ const threadId = ctx.threadId ?? ctx.state.threadId;
52
+ const description = reason || `This belongs in #${targetId}.`;
53
+ await ctx.emit({
54
+ type: 'client:ui:widget',
55
+ data: {
56
+ widgetId: ctx.toolCallId ?? `suggest_space:${targetId}`,
57
+ kind: 'form',
58
+ title: `Run this in #${targetId}`,
59
+ description,
60
+ fields: [
61
+ {
62
+ id: 'channelId',
63
+ label: 'Space',
64
+ type: 'select',
65
+ required: true,
66
+ defaultValue: targetId,
67
+ options,
68
+ },
69
+ {
70
+ id: 'message',
71
+ label: 'Message',
72
+ type: 'textarea',
73
+ required: true,
74
+ defaultValue: message,
75
+ placeholder: 'What should OpenBot do there?',
76
+ },
77
+ ],
78
+ actions: [
79
+ {
80
+ id: 'start_in_space',
81
+ label: 'Send',
82
+ variant: 'primary',
83
+ value: { channelId: targetId },
84
+ },
85
+ ],
86
+ display: 'expanded',
87
+ state: 'open',
88
+ metadata: { type: SPACE_HANDOFF_TYPE, channelId: targetId },
89
+ },
90
+ meta: {
91
+ agentId: ctx.agentId,
92
+ ...(threadId ? { threadId } : {}),
93
+ },
94
+ });
95
+ const output = `Showed a handoff to #${targetId}. Stop. Do not ask_agent in this thread and do not explain the handoff in prose. The human sends the message there if they confirm.`;
96
+ return { success: true, channelId: targetId, output };
97
+ }
98
+ export const suggestSpaceTools = {
99
+ suggest_space: {
100
+ description: 'When the human is in the wrong existing Space, show a card with that Space and a self-contained version of their ask. They can edit both, then send it as a new thread there. Call this and stop. Do not ask_agent in this thread. Only use a Space id from ## SPACES, never the current Space, and never a Space you have not created yet.',
101
+ inputSchema: z.object({
102
+ channelId: z
103
+ .string()
104
+ .describe('Existing Space id from ## SPACES. Not the Space you are in.'),
105
+ message: z
106
+ .string()
107
+ .describe("The human's ask, rewritten so it stands alone in that Space. They can edit it before sending."),
108
+ reason: z
109
+ .string()
110
+ .optional()
111
+ .describe('One line on why this Space fits. Shown on the card.'),
112
+ }),
113
+ execute: suggestSpace,
114
+ },
115
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meetopenbot/openbot",
3
- "version": "1.2.18",
3
+ "version": "1.2.19",
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",