@meetopenbot/openbot 1.2.17 → 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.
- package/dist/build-tools.js +2 -0
- package/dist/index.js +2 -0
- package/dist/system-prompt.js +3 -3
- package/dist/tools/ask-agent.js +0 -9
- package/dist/tools/preview.js +0 -40
- package/dist/tools/suggest-space.js +115 -0
- package/package.json +1 -1
package/dist/build-tools.js
CHANGED
|
@@ -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;
|
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**: 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` —
|
|
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,
|
|
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.",
|
|
@@ -61,7 +61,7 @@ export const OPENBOT_SYSTEM_PROMPT = [
|
|
|
61
61
|
"This runtime is a remote cloud computer. The human is in the browser at openbot.one and cannot open localhost, 127.0.0.1, or any port on this machine.",
|
|
62
62
|
"- Never tell them to visit localhost. They need a public HTTPS URL.",
|
|
63
63
|
"- When you ask a coding agent to run a dev server, preview, or any local process the human should open, tell them to expose it with `cloudflared tunnel --url http://127.0.0.1:<port>` and to add `cloudflared` as a **project devDependency** (not a global install — this image is ephemeral and only workspace files persist).",
|
|
64
|
-
"-
|
|
64
|
+
"- After the specialist finishes, inspect the URL they report, share the `https://*.trycloudflare.com` URL, and call `open_preview` with it so it opens in the space browser. Quick tunnels change on restart — pass the new one through. If they report a localhost URL, ask them to tunnel it.",
|
|
65
65
|
"- For any other public page the human should see, call `open_preview` with that http(s) URL too. Never pass localhost, 127.0.0.1, or a port on this machine.",
|
|
66
66
|
"",
|
|
67
67
|
"# SPACE VARIABLES",
|
package/dist/tools/ask-agent.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { loadAgentModelCatalogs, validateAskModel } from "../agent-models.js";
|
|
4
|
-
import { previewEventsForTunnelUrls } from "./preview.js";
|
|
5
4
|
/**
|
|
6
5
|
* `ask_agent` — OpenBot sends a message to another agent in its own harness.
|
|
7
6
|
* Only the orchestrator may ask. Child events stream into this thread so the
|
|
@@ -104,8 +103,6 @@ async function runAskedAgent(ctx, args) {
|
|
|
104
103
|
resolveNext = null;
|
|
105
104
|
}
|
|
106
105
|
});
|
|
107
|
-
const seenPreviewUrls = new Set();
|
|
108
|
-
const threadId = ctx.threadId ?? ctx.state.threadId;
|
|
109
106
|
while (!isFinished || eventQueue.length > 0) {
|
|
110
107
|
if (eventQueue.length === 0) {
|
|
111
108
|
await new Promise((r) => {
|
|
@@ -115,12 +112,6 @@ async function runAskedAgent(ctx, args) {
|
|
|
115
112
|
while (eventQueue.length > 0) {
|
|
116
113
|
const nextEvent = eventQueue.shift();
|
|
117
114
|
await ctx.emit(nextEvent);
|
|
118
|
-
for (const preview of previewEventsForTunnelUrls(nextEvent, seenPreviewUrls, {
|
|
119
|
-
agentId: ctx.agentId,
|
|
120
|
-
threadId,
|
|
121
|
-
})) {
|
|
122
|
-
await ctx.emit(preview);
|
|
123
|
-
}
|
|
124
115
|
}
|
|
125
116
|
}
|
|
126
117
|
await runPromise;
|
package/dist/tools/preview.js
CHANGED
|
@@ -25,35 +25,6 @@ export function publicPreviewUrl(input) {
|
|
|
25
25
|
return null;
|
|
26
26
|
return url.href;
|
|
27
27
|
}
|
|
28
|
-
const TUNNEL_URL = /https:\/\/[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*\.trycloudflare\.com(?::\d+)?(?:\/[^\s<>"'`)\\\]]*)?/gi;
|
|
29
|
-
const MAX_SCAN_CHARS = 250000;
|
|
30
|
-
function cleanMatch(raw) {
|
|
31
|
-
return raw.replace(/[.,;:!?)]+$/g, '');
|
|
32
|
-
}
|
|
33
|
-
/** Public Cloudflare quick-tunnel URLs mentioned in specialist output. */
|
|
34
|
-
export function tunnelPreviewUrls(value) {
|
|
35
|
-
let text;
|
|
36
|
-
try {
|
|
37
|
-
text = typeof value === 'string' ? value : JSON.stringify(value) ?? '';
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
return [];
|
|
41
|
-
}
|
|
42
|
-
if (!text)
|
|
43
|
-
return [];
|
|
44
|
-
if (text.length > MAX_SCAN_CHARS)
|
|
45
|
-
text = text.slice(0, MAX_SCAN_CHARS);
|
|
46
|
-
const seen = new Set();
|
|
47
|
-
const urls = [];
|
|
48
|
-
for (const match of text.matchAll(TUNNEL_URL)) {
|
|
49
|
-
const url = publicPreviewUrl(cleanMatch(match[0]));
|
|
50
|
-
if (!url || seen.has(url))
|
|
51
|
-
continue;
|
|
52
|
-
seen.add(url);
|
|
53
|
-
urls.push(url);
|
|
54
|
-
}
|
|
55
|
-
return urls;
|
|
56
|
-
}
|
|
57
28
|
export function previewWidgetEvent(args) {
|
|
58
29
|
return {
|
|
59
30
|
type: 'client:ui:widget',
|
|
@@ -80,17 +51,6 @@ export function previewWidgetEvent(args) {
|
|
|
80
51
|
},
|
|
81
52
|
};
|
|
82
53
|
}
|
|
83
|
-
/** Preview widgets for tunnel URLs in `value` that `seen` has not emitted yet. */
|
|
84
|
-
export function previewEventsForTunnelUrls(value, seen, meta) {
|
|
85
|
-
const events = [];
|
|
86
|
-
for (const url of tunnelPreviewUrls(value)) {
|
|
87
|
-
if (seen.has(url))
|
|
88
|
-
continue;
|
|
89
|
-
seen.add(url);
|
|
90
|
-
events.push(previewWidgetEvent({ url, ...meta }));
|
|
91
|
-
}
|
|
92
|
-
return events;
|
|
93
|
-
}
|
|
94
54
|
async function openPreview(rawArgs, ctx) {
|
|
95
55
|
const channelId = ctx.channelId ?? ctx.state.channelId ?? '';
|
|
96
56
|
const threadId = ctx.threadId ?? ctx.state.threadId;
|
|
@@ -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
|
+
};
|