@meetopenbot/openbot 0.1.14 → 0.2.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.
@@ -0,0 +1,262 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { z } from "zod";
3
+ import { GENERAL_SPACE_ID, isPlaceholderSpaceId, placeholderSpaceError, } from "../space-id.js";
4
+ import { asActionBuilder, } from "../types.js";
5
+ function normalizeSpaceId(raw) {
6
+ return raw
7
+ .trim()
8
+ .toLowerCase()
9
+ .replace(/[^a-z0-9]+/g, "-")
10
+ .replace(/(^-|-$)/g, "");
11
+ }
12
+ function startWorkWidget(args) {
13
+ return {
14
+ type: "client:ui:widget",
15
+ data: {
16
+ widgetId: args.widgetId,
17
+ kind: "message",
18
+ title: "start_work",
19
+ description: args.spaceId || undefined,
20
+ body: args.body,
21
+ display: "collapsed",
22
+ state: args.state,
23
+ },
24
+ meta: args.meta,
25
+ };
26
+ }
27
+ const startWorkToolDefinitions = {
28
+ start_work: {
29
+ description: "Start the human's ask in an existing Space. Use from #general for real project work only — not questions, listings, or setup. If the Space does not exist, do not invent one: propose it to the human, then create_channel after they agree. Do not call this on work that was already forwarded.",
30
+ inputSchema: z.object({
31
+ spaceId: z
32
+ .string()
33
+ .describe("Target Space id (channel id), e.g. checkout, website, backend-platform."),
34
+ prompt: z
35
+ .string()
36
+ .describe("The human's ask, restated for the thread in that Space. Include enough context to start."),
37
+ title: z
38
+ .string()
39
+ .optional()
40
+ .describe("Optional thread title in the target Space."),
41
+ mentionedAgentIds: z
42
+ .array(z.string())
43
+ .optional()
44
+ .describe("Agents the human named; OpenBot in the target Space must ask them."),
45
+ }),
46
+ },
47
+ };
48
+ export const startWorkPlugin = {
49
+ id: "start-work",
50
+ name: "Start work",
51
+ description: "Forwards an ask into the right Space and starts OpenBot there.",
52
+ toolDefinitions: startWorkToolDefinitions,
53
+ factory: (pluginContext) => (builder) => {
54
+ const actions = asActionBuilder(builder);
55
+ actions.on("start_work", async function* (event, context) {
56
+ const data = event.data;
57
+ const toolCallId = event.meta?.toolCallId;
58
+ const resultMeta = {
59
+ ...(event.meta || {}),
60
+ agentId: context.state.agentId,
61
+ threadId: event.meta?.threadId || context.state.threadId,
62
+ };
63
+ const widgetId = typeof toolCallId === "string"
64
+ ? toolCallId
65
+ : `start_work:${Date.now()}`;
66
+ if (context.state.agentId !== pluginContext.host.orchestratorAgentId) {
67
+ const error = "Only OpenBot can start work in another Space.";
68
+ yield startWorkWidget({
69
+ widgetId,
70
+ spaceId: "",
71
+ body: error,
72
+ state: "error",
73
+ meta: resultMeta,
74
+ });
75
+ yield {
76
+ type: "action:start_work:result",
77
+ data: { success: false, error, output: error },
78
+ meta: resultMeta,
79
+ };
80
+ return;
81
+ }
82
+ const invokeData = context.state.triggerEvent?.type === "agent:invoke"
83
+ ? context.state.triggerEvent.data
84
+ : undefined;
85
+ if (typeof invokeData?.forwardedFromSpaceId === "string") {
86
+ const error = "This thread was already forwarded. Stay here and ask agents; do not start_work again.";
87
+ yield startWorkWidget({
88
+ widgetId,
89
+ spaceId: "",
90
+ body: error,
91
+ state: "error",
92
+ meta: resultMeta,
93
+ });
94
+ yield {
95
+ type: "action:start_work:result",
96
+ data: { success: false, error, output: error },
97
+ meta: resultMeta,
98
+ };
99
+ return;
100
+ }
101
+ const spaceId = normalizeSpaceId(data.spaceId || "");
102
+ const prompt = typeof data.prompt === "string" ? data.prompt.trim() : "";
103
+ if (spaceId && isPlaceholderSpaceId(spaceId)) {
104
+ const error = placeholderSpaceError(spaceId);
105
+ yield startWorkWidget({
106
+ widgetId,
107
+ spaceId,
108
+ body: error,
109
+ state: "error",
110
+ meta: resultMeta,
111
+ });
112
+ yield {
113
+ type: "action:start_work:result",
114
+ data: { success: false, spaceId, error, output: error },
115
+ meta: resultMeta,
116
+ };
117
+ return;
118
+ }
119
+ if (!spaceId || !prompt) {
120
+ const error = "spaceId and prompt are required";
121
+ yield startWorkWidget({
122
+ widgetId,
123
+ spaceId,
124
+ body: error,
125
+ state: "error",
126
+ meta: resultMeta,
127
+ });
128
+ yield {
129
+ type: "action:start_work:result",
130
+ data: { success: false, spaceId, error, output: error },
131
+ meta: resultMeta,
132
+ };
133
+ return;
134
+ }
135
+ if (spaceId === context.state.channelId) {
136
+ const error = `Already in #${spaceId}. Ask an agent here instead of start_work.`;
137
+ yield startWorkWidget({
138
+ widgetId,
139
+ spaceId,
140
+ body: error,
141
+ state: "error",
142
+ meta: resultMeta,
143
+ });
144
+ yield {
145
+ type: "action:start_work:result",
146
+ data: { success: false, spaceId, error, output: error },
147
+ meta: resultMeta,
148
+ };
149
+ return;
150
+ }
151
+ const storage = pluginContext.storage;
152
+ const existing = (await storage.getChannels?.().catch(() => []));
153
+ const exists = Array.isArray(existing) &&
154
+ existing.some((space) => space.id === spaceId);
155
+ if (!exists) {
156
+ const error = `Space #${spaceId} does not exist. Propose it to the human (name + why). After they agree, use create_channel, then start_work. Do not invent a placeholder Space.`;
157
+ yield startWorkWidget({
158
+ widgetId,
159
+ spaceId,
160
+ body: error,
161
+ state: "error",
162
+ meta: resultMeta,
163
+ });
164
+ yield {
165
+ type: "action:start_work:result",
166
+ data: { success: false, spaceId, error, output: error },
167
+ meta: resultMeta,
168
+ };
169
+ return;
170
+ }
171
+ const threadId = randomUUID();
172
+ const threadTitle = typeof data.title === "string" && data.title.trim()
173
+ ? data.title.trim()
174
+ : undefined;
175
+ const threadUrl = `/channels/${encodeURIComponent(spaceId)}/threads/${encodeURIComponent(threadId)}`;
176
+ const fromSpace = context.state.channelId || GENERAL_SPACE_ID;
177
+ yield startWorkWidget({
178
+ widgetId,
179
+ spaceId,
180
+ body: `Starting in #${spaceId}…`,
181
+ state: "open",
182
+ meta: resultMeta,
183
+ });
184
+ try {
185
+ await storage.createThread({
186
+ channelId: spaceId,
187
+ threadId,
188
+ threadTitle,
189
+ initialState: {
190
+ respondingAgentId: pluginContext.host.orchestratorAgentId,
191
+ ...(threadTitle ? { name: threadTitle } : {}),
192
+ },
193
+ });
194
+ const mentionedAgentIds = Array.isArray(data.mentionedAgentIds)
195
+ ? data.mentionedAgentIds.filter((id) => typeof id === "string" && id.length > 0)
196
+ : [];
197
+ const invokeEvent = {
198
+ type: "agent:invoke",
199
+ data: {
200
+ role: "user",
201
+ content: prompt,
202
+ ...(mentionedAgentIds.length > 0 ? { mentionedAgentIds } : {}),
203
+ forwardedFromSpaceId: fromSpace,
204
+ },
205
+ meta: {
206
+ channelId: spaceId,
207
+ threadId,
208
+ userId: context.state.triggerEvent?.meta?.userId,
209
+ userName: context.state.triggerEvent?.meta?.userName,
210
+ userAvatarUrl: context.state.triggerEvent?.meta?.userAvatarUrl,
211
+ },
212
+ };
213
+ await pluginContext.host.runAgent({
214
+ runId: `sw_${randomUUID()}`,
215
+ agentId: pluginContext.host.orchestratorAgentId,
216
+ event: invokeEvent,
217
+ publicBaseUrl: pluginContext.publicBaseUrl,
218
+ persistEvents: true,
219
+ onEvent: async () => {
220
+ // Persist in the target Space via the inner harness. Do not echo into this thread.
221
+ },
222
+ });
223
+ const output = `Started work in #${spaceId}. Open ${threadUrl}`;
224
+ yield startWorkWidget({
225
+ widgetId,
226
+ spaceId,
227
+ body: output,
228
+ state: "submitted",
229
+ meta: resultMeta,
230
+ });
231
+ yield {
232
+ type: "action:start_work:result",
233
+ data: {
234
+ success: true,
235
+ spaceId,
236
+ threadId,
237
+ threadUrl,
238
+ output,
239
+ },
240
+ meta: resultMeta,
241
+ };
242
+ }
243
+ catch (error) {
244
+ const message = error instanceof Error ? error.message : "Unknown error";
245
+ const output = `Failed to start work in #${spaceId}: ${message}`;
246
+ yield startWorkWidget({
247
+ widgetId,
248
+ spaceId,
249
+ body: output,
250
+ state: "error",
251
+ meta: resultMeta,
252
+ });
253
+ yield {
254
+ type: "action:start_work:result",
255
+ data: { success: false, spaceId, threadId, error: message, output },
256
+ meta: resultMeta,
257
+ };
258
+ }
259
+ });
260
+ },
261
+ };
262
+ export default startWorkPlugin;