openbot 0.5.8 → 0.5.10
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/CLAUDE.md +1 -1
- package/dist/app/active-runs.js +70 -0
- package/dist/app/cli.js +1 -1
- package/dist/app/lifecycle-events.js +27 -0
- package/dist/app/run-event-handler.js +37 -0
- package/dist/app/server.js +127 -123
- package/dist/app/slack-webhook.js +81 -0
- package/dist/app/webhook-routing.js +52 -0
- package/dist/harness/index.js +18 -15
- package/dist/harness/merge-plugin-config.js +26 -0
- package/dist/plugins/slack-webhook/index.js +51 -0
- package/dist/plugins/storage/index.js +72 -14
- package/dist/plugins/storage/service.js +11 -6
- package/dist/services/plugins/enrich-openbot-plugin.js +38 -0
- package/dist/services/plugins/host.js +0 -3
- package/dist/services/plugins/model-registry.js +1 -1
- package/docs/plugins.md +27 -0
- package/package.json +2 -2
- package/src/app/active-runs.ts +95 -0
- package/src/app/cli.ts +1 -1
- package/src/app/lifecycle-events.ts +46 -0
- package/src/app/run-event-handler.ts +55 -0
- package/src/app/server.ts +147 -154
- package/src/app/types.ts +33 -4
- package/src/app/webhook-routing.ts +70 -0
- package/src/harness/index.ts +19 -20
- package/src/harness/merge-plugin-config.ts +35 -0
- package/src/plugins/storage/index.ts +432 -372
- package/src/plugins/storage/service.ts +13 -13
- package/src/services/plugins/enrich-openbot-plugin.ts +41 -0
- package/src/services/plugins/host.ts +0 -6
- package/src/services/plugins/types.ts +0 -8
- package/src/services/plugins/model-registry.ts +0 -178
package/dist/harness/index.js
CHANGED
|
@@ -7,8 +7,9 @@ import { abortRegistry, abortKey } from '../services/abort.js';
|
|
|
7
7
|
import { loadConfig } from '../app/config.js';
|
|
8
8
|
import { getPublicBaseUrl } from '../plugins/storage/files.js';
|
|
9
9
|
import { createPluginHost } from '../services/plugins/host.js';
|
|
10
|
+
import { getInvokeConfigOverrides, mergePluginConfig } from './merge-plugin-config.js';
|
|
10
11
|
export { STATE_AGENT_ID, ORCHESTRATOR_AGENT_ID };
|
|
11
|
-
async function emitEvent(chunk, state, { persistEvents,
|
|
12
|
+
async function emitEvent(chunk, state, { persistEvents, onEvent, parentAgentId, parentToolCallId, }) {
|
|
12
13
|
ensureEventId(chunk);
|
|
13
14
|
// Enrich event with parent metadata if not already present
|
|
14
15
|
if (parentAgentId || parentToolCallId) {
|
|
@@ -18,10 +19,11 @@ async function emitEvent(chunk, state, { persistEvents, channelId, threadId, onE
|
|
|
18
19
|
parentToolCallId: chunk.meta?.parentToolCallId || parentToolCallId,
|
|
19
20
|
};
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
+
const channelId = state?.channelId;
|
|
23
|
+
if (persistEvents && channelId) {
|
|
22
24
|
await storageService.storeEvent({
|
|
23
|
-
channelId
|
|
24
|
-
threadId: state?.threadId
|
|
25
|
+
channelId,
|
|
26
|
+
threadId: state?.threadId,
|
|
25
27
|
event: chunk,
|
|
26
28
|
});
|
|
27
29
|
}
|
|
@@ -32,7 +34,7 @@ async function emitEvent(chunk, state, { persistEvents, channelId, threadId, onE
|
|
|
32
34
|
* Fire and forget.
|
|
33
35
|
*/
|
|
34
36
|
export async function runAgent(options) {
|
|
35
|
-
const { runId, agentId, event,
|
|
37
|
+
const { runId, agentId, event, onEvent } = options;
|
|
36
38
|
const persistEvents = options.persistEvents !== false;
|
|
37
39
|
let publicBaseUrl = options.publicBaseUrl;
|
|
38
40
|
if (!publicBaseUrl) {
|
|
@@ -46,18 +48,16 @@ export async function runAgent(options) {
|
|
|
46
48
|
const state = await storageService.getOpenBotState({
|
|
47
49
|
runId,
|
|
48
50
|
agentId,
|
|
49
|
-
channelId,
|
|
50
|
-
threadId,
|
|
51
51
|
event,
|
|
52
52
|
});
|
|
53
53
|
// Shared per-thread abort signal so a stop request cancels this run and any
|
|
54
54
|
// delegated sub-agent runs (which execute in the same channel/thread).
|
|
55
|
-
const runKey = abortKey(channelId, threadId);
|
|
55
|
+
const runKey = state.channelId ? abortKey(state.channelId, state.threadId) : runId;
|
|
56
56
|
const abortSignal = abortRegistry.acquire(runKey);
|
|
57
57
|
await emitEvent({
|
|
58
58
|
type: 'agent:run:start',
|
|
59
|
-
data: { runId, agentId, channelId, threadId },
|
|
60
|
-
}, state, { persistEvents,
|
|
59
|
+
data: { runId, agentId, channelId: state.channelId, threadId: state.threadId },
|
|
60
|
+
}, state, { persistEvents, onEvent, parentAgentId, parentToolCallId });
|
|
61
61
|
try {
|
|
62
62
|
const pluginRefs = agentDetails.pluginRefs ?? [];
|
|
63
63
|
const tools = {};
|
|
@@ -69,6 +69,11 @@ export async function runAgent(options) {
|
|
|
69
69
|
}
|
|
70
70
|
const builder = melony().initialState(state);
|
|
71
71
|
const pluginHost = createPluginHost(runAgent);
|
|
72
|
+
const invokeConfigOverrides = getInvokeConfigOverrides(event);
|
|
73
|
+
const pluginConfigs = Object.fromEntries(pluginRefs.map((ref) => [
|
|
74
|
+
ref.id,
|
|
75
|
+
mergePluginConfig(ref.config, invokeConfigOverrides, ref.id),
|
|
76
|
+
]));
|
|
72
77
|
for (const ref of pluginRefs) {
|
|
73
78
|
const plugin = await resolvePlugin(ref.id);
|
|
74
79
|
if (!plugin)
|
|
@@ -76,7 +81,7 @@ export async function runAgent(options) {
|
|
|
76
81
|
builder.use(plugin.factory({
|
|
77
82
|
agentId,
|
|
78
83
|
agentDetails,
|
|
79
|
-
config: ref.
|
|
84
|
+
config: pluginConfigs[ref.id],
|
|
80
85
|
storage: storageService,
|
|
81
86
|
tools,
|
|
82
87
|
publicBaseUrl,
|
|
@@ -91,8 +96,6 @@ export async function runAgent(options) {
|
|
|
91
96
|
break;
|
|
92
97
|
await emitEvent(outputEvent, state, {
|
|
93
98
|
persistEvents,
|
|
94
|
-
channelId,
|
|
95
|
-
threadId,
|
|
96
99
|
onEvent,
|
|
97
100
|
parentAgentId,
|
|
98
101
|
parentToolCallId,
|
|
@@ -106,7 +109,7 @@ export async function runAgent(options) {
|
|
|
106
109
|
abortRegistry.release(runKey);
|
|
107
110
|
await emitEvent({
|
|
108
111
|
type: 'agent:run:end',
|
|
109
|
-
data: { runId, agentId, channelId, threadId },
|
|
110
|
-
}, state, { persistEvents,
|
|
112
|
+
data: { runId, agentId, channelId: state.channelId, threadId: state.threadId },
|
|
113
|
+
}, state, { persistEvents, onEvent, parentAgentId, parentToolCallId });
|
|
111
114
|
}
|
|
112
115
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
function isPlainObject(value) {
|
|
2
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function isPluginKeyedConfig(config) {
|
|
5
|
+
return Object.values(config).every(isPlainObject);
|
|
6
|
+
}
|
|
7
|
+
export function getInvokeConfigOverrides(event) {
|
|
8
|
+
if (event.type !== 'agent:invoke')
|
|
9
|
+
return undefined;
|
|
10
|
+
const config = event.data?.config;
|
|
11
|
+
if (!isPlainObject(config) || Object.keys(config).length === 0)
|
|
12
|
+
return undefined;
|
|
13
|
+
return config;
|
|
14
|
+
}
|
|
15
|
+
export function mergePluginConfig(base, overrides, pluginId) {
|
|
16
|
+
const baseConfig = base ?? {};
|
|
17
|
+
if (!overrides)
|
|
18
|
+
return baseConfig;
|
|
19
|
+
if (isPluginKeyedConfig(overrides)) {
|
|
20
|
+
const pluginOverride = overrides[pluginId];
|
|
21
|
+
if (!isPlainObject(pluginOverride))
|
|
22
|
+
return baseConfig;
|
|
23
|
+
return { ...baseConfig, ...pluginOverride };
|
|
24
|
+
}
|
|
25
|
+
return { ...baseConfig, ...overrides };
|
|
26
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { parseSlackWebhookPayload, SLACK_WEBHOOK_PROVIDER } from '../../app/slack-webhook.js';
|
|
2
|
+
/**
|
|
3
|
+
* Handles `action:webhook` for the Slack Events API: URL verification challenges and
|
|
4
|
+
* `message` events transformed into `agent:invoke` turns.
|
|
5
|
+
*/
|
|
6
|
+
export const slackWebhookPlugin = {
|
|
7
|
+
id: 'slack-webhook',
|
|
8
|
+
name: 'Slack webhook',
|
|
9
|
+
description: 'Parses inbound Slack Events API webhooks and forwards user messages to the runtime.',
|
|
10
|
+
factory: ({ agentId }) => (builder) => {
|
|
11
|
+
builder.on('action:webhook', async function* (event) {
|
|
12
|
+
if (event.data?.provider !== SLACK_WEBHOOK_PROVIDER)
|
|
13
|
+
return;
|
|
14
|
+
const rawBody = Buffer.from(event.data.rawBody, 'base64');
|
|
15
|
+
const parsed = parseSlackWebhookPayload(rawBody);
|
|
16
|
+
if (!parsed)
|
|
17
|
+
return;
|
|
18
|
+
if (parsed.kind === 'url_verification') {
|
|
19
|
+
yield {
|
|
20
|
+
type: 'action:webhook:http-response',
|
|
21
|
+
data: {
|
|
22
|
+
status: 200,
|
|
23
|
+
body: { challenge: parsed.challenge },
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (parsed.kind === 'ignored') {
|
|
29
|
+
yield {
|
|
30
|
+
type: 'action:webhook:http-response',
|
|
31
|
+
data: { status: 200, body: { ok: true, ignored: parsed.reason } },
|
|
32
|
+
};
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const userLabel = parsed.userId ? `<@${parsed.userId}>` : 'Slack user';
|
|
36
|
+
yield {
|
|
37
|
+
type: 'agent:invoke',
|
|
38
|
+
data: {
|
|
39
|
+
role: 'user',
|
|
40
|
+
content: `${userLabel}: ${parsed.text}`,
|
|
41
|
+
},
|
|
42
|
+
meta: {
|
|
43
|
+
agentId,
|
|
44
|
+
slackChannelId: parsed.slackChannelId,
|
|
45
|
+
slackThreadTs: parsed.threadTs,
|
|
46
|
+
slackUserId: parsed.userId,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -43,7 +43,8 @@ export const storagePlugin = {
|
|
|
43
43
|
});
|
|
44
44
|
builder.on('agent:usage', async function* (event, context) {
|
|
45
45
|
const { usage } = event.data;
|
|
46
|
-
|
|
46
|
+
const channelId = context.state.channelId;
|
|
47
|
+
if (!channelId || !context.state.threadId)
|
|
47
48
|
return;
|
|
48
49
|
const currentState = context.state.threadDetails?.state || {};
|
|
49
50
|
const currentUsage = currentState.usage || {
|
|
@@ -57,12 +58,12 @@ export const storagePlugin = {
|
|
|
57
58
|
totalTokens: (currentUsage.totalTokens || 0) + usage.totalTokens,
|
|
58
59
|
};
|
|
59
60
|
await storage.patchThreadState({
|
|
60
|
-
channelId
|
|
61
|
+
channelId,
|
|
61
62
|
threadId: context.state.threadId,
|
|
62
63
|
state: { usage: nextUsage },
|
|
63
64
|
});
|
|
64
65
|
context.state.threadDetails = await storage.getThreadDetails({
|
|
65
|
-
channelId
|
|
66
|
+
channelId,
|
|
66
67
|
threadId: context.state.threadId,
|
|
67
68
|
});
|
|
68
69
|
});
|
|
@@ -76,9 +77,17 @@ export const storagePlugin = {
|
|
|
76
77
|
});
|
|
77
78
|
builder.on('action:storage:set-last-read', async function* (event, context) {
|
|
78
79
|
const { channelId, threadId, lastReadEventId } = event.data;
|
|
80
|
+
const resolvedChannelId = channelId || context.state.channelId;
|
|
81
|
+
if (!resolvedChannelId) {
|
|
82
|
+
yield {
|
|
83
|
+
type: 'action:storage:set-last-read-result',
|
|
84
|
+
data: { success: false, error: 'channelId is required' },
|
|
85
|
+
};
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
79
88
|
try {
|
|
80
89
|
await storage.setLastRead({
|
|
81
|
-
channelId:
|
|
90
|
+
channelId: resolvedChannelId,
|
|
82
91
|
threadId: threadId || context.state.threadId,
|
|
83
92
|
lastReadEventId,
|
|
84
93
|
});
|
|
@@ -95,15 +104,29 @@ export const storagePlugin = {
|
|
|
95
104
|
}
|
|
96
105
|
});
|
|
97
106
|
builder.on('action:storage:get-channel-details', async function* (_, state) {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
107
|
+
const channelId = state.state.channelId;
|
|
108
|
+
if (!channelId) {
|
|
109
|
+
yield {
|
|
110
|
+
type: 'action:storage:get-channel-details-result',
|
|
111
|
+
data: { channelDetails: null, error: 'channelId is required' },
|
|
112
|
+
};
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const channelDetails = await storage.getChannelDetails({ channelId });
|
|
101
116
|
yield { type: 'action:storage:get-channel-details-result', data: { channelDetails } };
|
|
102
117
|
});
|
|
103
118
|
builder.on('action:storage:get-thread-details', async function* (_, state) {
|
|
119
|
+
const channelId = state.state.channelId;
|
|
104
120
|
const threadId = state.state.threadId;
|
|
121
|
+
if (!channelId) {
|
|
122
|
+
yield {
|
|
123
|
+
type: 'action:storage:get-thread-details-result',
|
|
124
|
+
data: { threadDetails: null, error: 'channelId is required' },
|
|
125
|
+
};
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
105
128
|
const threadDetails = threadId
|
|
106
|
-
? await storage.getThreadDetails({ channelId
|
|
129
|
+
? await storage.getThreadDetails({ channelId, threadId })
|
|
107
130
|
: null;
|
|
108
131
|
yield { type: 'action:storage:get-thread-details-result', data: { threadDetails } };
|
|
109
132
|
});
|
|
@@ -210,7 +233,12 @@ export const storagePlugin = {
|
|
|
210
233
|
}
|
|
211
234
|
});
|
|
212
235
|
builder.on('action:storage:get-events', async function* (_, state) {
|
|
213
|
-
const
|
|
236
|
+
const channelId = state.state.channelId;
|
|
237
|
+
if (!channelId) {
|
|
238
|
+
yield { type: 'action:storage:get-events-result', data: { events: [] } };
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const events = await storage.getEvents({ channelId, threadId: state.state.threadId });
|
|
214
242
|
yield { type: 'action:storage:get-events-result', data: { events } };
|
|
215
243
|
});
|
|
216
244
|
builder.on('action:storage:get-variables', async function* () {
|
|
@@ -261,9 +289,14 @@ export const storagePlugin = {
|
|
|
261
289
|
}
|
|
262
290
|
});
|
|
263
291
|
builder.on('action:storage:patch-channel-state', async function* (event, state) {
|
|
292
|
+
const channelId = state.state.channelId;
|
|
293
|
+
if (!channelId) {
|
|
294
|
+
yield { type: 'action:storage:patch-channel-state-result', data: { success: false } };
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
264
297
|
try {
|
|
265
298
|
await storage.patchChannelState({
|
|
266
|
-
channelId
|
|
299
|
+
channelId,
|
|
267
300
|
state: event.data.state,
|
|
268
301
|
});
|
|
269
302
|
yield { type: 'action:storage:patch-channel-state-result', data: { success: true } };
|
|
@@ -273,12 +306,13 @@ export const storagePlugin = {
|
|
|
273
306
|
}
|
|
274
307
|
});
|
|
275
308
|
builder.on('action:storage:patch-thread-state', async function* (event, state) {
|
|
309
|
+
const channelId = state.state.channelId;
|
|
276
310
|
try {
|
|
277
|
-
if (!state.state.threadId) {
|
|
278
|
-
throw new Error('Missing threadId in state for patch-thread-state');
|
|
311
|
+
if (!channelId || !state.state.threadId) {
|
|
312
|
+
throw new Error('Missing channelId or threadId in state for patch-thread-state');
|
|
279
313
|
}
|
|
280
314
|
await storage.patchThreadState({
|
|
281
|
-
channelId
|
|
315
|
+
channelId,
|
|
282
316
|
threadId: state.state.threadId,
|
|
283
317
|
state: event.data.state,
|
|
284
318
|
});
|
|
@@ -291,6 +325,13 @@ export const storagePlugin = {
|
|
|
291
325
|
builder.on('action:storage:list-files', async function* (event, context) {
|
|
292
326
|
const channelId = context.state.channelId;
|
|
293
327
|
const subPath = event.data?.path || '';
|
|
328
|
+
if (!channelId) {
|
|
329
|
+
yield {
|
|
330
|
+
type: 'action:storage:list-files:result',
|
|
331
|
+
data: { success: false, files: [], error: 'channelId is required' },
|
|
332
|
+
};
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
294
335
|
try {
|
|
295
336
|
const files = await storage.listFiles({ channelId, path: subPath });
|
|
296
337
|
yield {
|
|
@@ -321,6 +362,13 @@ export const storagePlugin = {
|
|
|
321
362
|
};
|
|
322
363
|
return;
|
|
323
364
|
}
|
|
365
|
+
if (!channelId) {
|
|
366
|
+
yield {
|
|
367
|
+
type: 'action:storage:read-file:result',
|
|
368
|
+
data: { success: false, path: filePath, error: 'channelId is required' },
|
|
369
|
+
};
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
324
372
|
try {
|
|
325
373
|
if (encoding === 'utf8') {
|
|
326
374
|
const content = await storage.readFile({ channelId, path: filePath });
|
|
@@ -361,8 +409,18 @@ export const storagePlugin = {
|
|
|
361
409
|
};
|
|
362
410
|
return;
|
|
363
411
|
}
|
|
412
|
+
if (!channelId) {
|
|
413
|
+
yield {
|
|
414
|
+
type: 'action:storage:get-file-url:result',
|
|
415
|
+
data: { success: false, path: filePath, error: 'channelId is required' },
|
|
416
|
+
};
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
364
419
|
try {
|
|
365
|
-
const { size, mimeType } = await storage.getChannelFileStat({
|
|
420
|
+
const { size, mimeType } = await storage.getChannelFileStat({
|
|
421
|
+
channelId,
|
|
422
|
+
path: filePath,
|
|
423
|
+
});
|
|
366
424
|
const url = buildWorkspaceFileUrl({
|
|
367
425
|
baseUrl: resolvePublicBaseUrl(),
|
|
368
426
|
channelId,
|
|
@@ -9,7 +9,7 @@ import crypto from 'node:crypto';
|
|
|
9
9
|
import matter from 'gray-matter';
|
|
10
10
|
import { OPENBOT_PLUGIN_ID } from '../../app/openbot-plugin.js';
|
|
11
11
|
import { listBuiltInPlugins, parsePluginModule } from '../../services/plugins/registry.js';
|
|
12
|
-
import { enrichOpenbotPluginDescriptor
|
|
12
|
+
import { enrichOpenbotPluginDescriptor } from '../../services/plugins/enrich-openbot-plugin.js';
|
|
13
13
|
import { processService } from '../../services/process.js';
|
|
14
14
|
import { memoryService } from '../memory/service.js';
|
|
15
15
|
import { guessMimeType, resolveChannelFile, statChannelFile } from './files.js';
|
|
@@ -837,13 +837,12 @@ export const storageService = {
|
|
|
837
837
|
return Array.from(deduped.values()).filter((agent) => !agent.hidden);
|
|
838
838
|
},
|
|
839
839
|
getPlugins: async () => {
|
|
840
|
-
const [builtIn, fromDisk
|
|
840
|
+
const [builtIn, fromDisk] = await Promise.all([
|
|
841
841
|
listBuiltInPluginDescriptors(),
|
|
842
842
|
listPluginsFromDisk(),
|
|
843
|
-
listRegistryModelOptions(),
|
|
844
843
|
]);
|
|
845
|
-
const
|
|
846
|
-
const merged = [...
|
|
844
|
+
const enrichPlugin = (plugin) => enrichOpenbotPluginDescriptor(plugin);
|
|
845
|
+
const merged = [...builtIn.map(enrichPlugin), ...fromDisk.map(enrichPlugin)];
|
|
847
846
|
const deduped = new Map();
|
|
848
847
|
for (const plugin of merged) {
|
|
849
848
|
if (!deduped.has(plugin.id)) {
|
|
@@ -1361,7 +1360,13 @@ export const storageService = {
|
|
|
1361
1360
|
* Hydrates the full OpenBot state from disk/storage before a run.
|
|
1362
1361
|
*/
|
|
1363
1362
|
getOpenBotState: async (options) => {
|
|
1364
|
-
const { runId, agentId,
|
|
1363
|
+
const { runId, agentId, event } = options;
|
|
1364
|
+
const channelId = typeof event.meta?.channelId === 'string'
|
|
1365
|
+
? event.meta.channelId.trim() || undefined
|
|
1366
|
+
: undefined;
|
|
1367
|
+
const threadId = typeof event.meta?.threadId === 'string'
|
|
1368
|
+
? event.meta.threadId.trim() || undefined
|
|
1369
|
+
: undefined;
|
|
1365
1370
|
let agentDetails;
|
|
1366
1371
|
try {
|
|
1367
1372
|
agentDetails = await storageService.getAgentDetails({ agentId });
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { OPENBOT_PLUGIN_ID } from '../../app/openbot-plugin.js';
|
|
2
|
+
import { DEFAULT_CLOUD_OPENBOT_AUTH_MODE, isCloudMode } from '../../app/cloud-mode.js';
|
|
3
|
+
/** Injects cloud platform fields into the openbot plugin descriptor for settings UI. */
|
|
4
|
+
export function enrichOpenbotPluginDescriptor(descriptor) {
|
|
5
|
+
if (descriptor.id !== OPENBOT_PLUGIN_ID || !descriptor.configSchema)
|
|
6
|
+
return descriptor;
|
|
7
|
+
if (!isCloudMode())
|
|
8
|
+
return descriptor;
|
|
9
|
+
const { authMode: _authMode, ...otherProperties } = descriptor.configSchema.properties;
|
|
10
|
+
const cloudAuthModeProperty = {
|
|
11
|
+
type: 'string',
|
|
12
|
+
description: 'Credits uses OpenBot platform billing. BYOK uses your own API keys.',
|
|
13
|
+
enum: ['credits', 'byok'],
|
|
14
|
+
default: DEFAULT_CLOUD_OPENBOT_AUTH_MODE,
|
|
15
|
+
options: [
|
|
16
|
+
{
|
|
17
|
+
label: 'Credits',
|
|
18
|
+
value: 'credits',
|
|
19
|
+
description: 'Use OpenBot platform credits.',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
label: 'Bring your own key',
|
|
23
|
+
value: 'byok',
|
|
24
|
+
description: 'Use your own provider API keys.',
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
...descriptor,
|
|
30
|
+
configSchema: {
|
|
31
|
+
...descriptor.configSchema,
|
|
32
|
+
properties: {
|
|
33
|
+
authMode: cloudAuthModeProperty,
|
|
34
|
+
...otherProperties,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -2,15 +2,12 @@ import { DEFAULT_CLOUD_OPENBOT_AUTH_MODE, isCloudMode, isCloudSystemAgent, parse
|
|
|
2
2
|
import { ORCHESTRATOR_AGENT_ID } from '../../app/agent-ids.js';
|
|
3
3
|
import { OPENBOT_PLUGIN_ID } from '../../app/openbot-plugin.js';
|
|
4
4
|
import { getBaseDir, resolvePath, saveConfig } from '../../app/config.js';
|
|
5
|
-
import { listApiKeyProvidersFromRegistry, resolveModelRegistry, } from './model-registry.js';
|
|
6
5
|
export function createPluginHost(runAgent) {
|
|
7
6
|
return {
|
|
8
7
|
runAgent,
|
|
9
8
|
isCloudSystemAgent,
|
|
10
9
|
isCloudMode,
|
|
11
10
|
parseOpenbotAuthMode,
|
|
12
|
-
resolveModelRegistry,
|
|
13
|
-
listApiKeyProvidersFromRegistry,
|
|
14
11
|
saveConfig,
|
|
15
12
|
getBaseDir,
|
|
16
13
|
resolvePath,
|
|
@@ -74,7 +74,7 @@ export function enrichOpenbotPluginDescriptor(descriptor, modelOptions) {
|
|
|
74
74
|
: pickDefaultModelValue(modelOptions) ?? staticDefault;
|
|
75
75
|
const nextModelProperty = {
|
|
76
76
|
...modelProperty,
|
|
77
|
-
description: 'Model from the hosted marketplace registry.',
|
|
77
|
+
description: modelProperty.description ?? 'Model from the hosted marketplace registry.',
|
|
78
78
|
};
|
|
79
79
|
if (values.length > 0) {
|
|
80
80
|
nextModelProperty.enum = values;
|
package/docs/plugins.md
CHANGED
|
@@ -60,6 +60,33 @@ model as the tool result (`history.ts`). Structured fields (e.g. `list`,
|
|
|
60
60
|
| `todo` | Tool | `todo_write`, `todo_read` (inbuilt in `openbot`) |
|
|
61
61
|
| `plugin-manager` | Infra | Marketplace list, npm plugin install/uninstall, agent install |
|
|
62
62
|
|
|
63
|
+
## Inbound webhooks
|
|
64
|
+
|
|
65
|
+
`POST /api/webhooks/:provider` forwards third-party HTTP payloads onto the bus as
|
|
66
|
+
`action:webhook`. The URL segment is used as the target agent id when
|
|
67
|
+
`~/.openbot/agents/<provider>/AGENT.md` exists; otherwise the request falls back
|
|
68
|
+
to the `system` agent. Attach a plugin to the routed agent and handle
|
|
69
|
+
verification, channel/thread routing, transforms, and HTTP responses there.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
builder.on('action:webhook', async function* (event) {
|
|
73
|
+
if (event.data?.provider !== 'slack') return;
|
|
74
|
+
|
|
75
|
+
const rawBody = Buffer.from(event.data.rawBody, 'base64');
|
|
76
|
+
|
|
77
|
+
// Optional: control the HTTP response (URL challenges, 401s, custom acks)
|
|
78
|
+
yield {
|
|
79
|
+
type: 'action:webhook:http-response',
|
|
80
|
+
data: { status: 200, body: { ok: true } },
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
yield {
|
|
84
|
+
type: 'agent:invoke',
|
|
85
|
+
data: { role: 'user', content: '...' },
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
63
90
|
## Batteries-included: `openbot` runtime
|
|
64
91
|
|
|
65
92
|
The `openbot` plugin is the standard runtime for OpenBot agents. It is designed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openbot",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.10",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"build": "tsc && mkdir -p dist/assets && cp src/assets/icon.svg dist/assets/icon.svg",
|
|
12
12
|
"start": "node dist/app/cli.js start",
|
|
13
13
|
"format": "prettier --write .",
|
|
14
|
-
"push": "fly deploy --build-only --push --config deploy/fly.toml -a openbot-runtime --image-label v0.5.
|
|
14
|
+
"push": "fly deploy --build-only --push --config deploy/fly.toml -a openbot-runtime --image-label v0.5.10"
|
|
15
15
|
},
|
|
16
16
|
"bin": {
|
|
17
17
|
"openbot": "./dist/app/cli.js"
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { ActiveRunsSnapshotEvent } from './types.js';
|
|
2
|
+
|
|
3
|
+
export type ActiveRunRecord = {
|
|
4
|
+
runId: string;
|
|
5
|
+
channelId: string;
|
|
6
|
+
threadId?: string;
|
|
7
|
+
agentId: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type Bucket = {
|
|
11
|
+
channelId: string;
|
|
12
|
+
threadId?: string;
|
|
13
|
+
activeCount: number;
|
|
14
|
+
agentIds: Set<string>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function getRunKey(
|
|
18
|
+
runId: string,
|
|
19
|
+
agentId: string,
|
|
20
|
+
channelId: string,
|
|
21
|
+
threadId?: string,
|
|
22
|
+
): string {
|
|
23
|
+
return `${runId}:${agentId}:${channelId}:${threadId || ''}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class ActiveRunsTracker {
|
|
27
|
+
private runs = new Map<string, ActiveRunRecord>();
|
|
28
|
+
|
|
29
|
+
trackStart(data: ActiveRunRecord): void {
|
|
30
|
+
this.runs.set(getRunKey(data.runId, data.agentId, data.channelId, data.threadId), data);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
trackEnd(data: ActiveRunRecord): void {
|
|
34
|
+
this.runs.delete(getRunKey(data.runId, data.agentId, data.channelId, data.threadId));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
buildSnapshot(): ActiveRunsSnapshotEvent {
|
|
38
|
+
const byBucket = new Map<string, Bucket>();
|
|
39
|
+
for (const run of this.runs.values()) {
|
|
40
|
+
const threadId = run.threadId || undefined;
|
|
41
|
+
const key = JSON.stringify([run.channelId, threadId ?? null]);
|
|
42
|
+
let bucket = byBucket.get(key);
|
|
43
|
+
if (!bucket) {
|
|
44
|
+
bucket = {
|
|
45
|
+
channelId: run.channelId,
|
|
46
|
+
threadId,
|
|
47
|
+
activeCount: 0,
|
|
48
|
+
agentIds: new Set<string>(),
|
|
49
|
+
};
|
|
50
|
+
byBucket.set(key, bucket);
|
|
51
|
+
}
|
|
52
|
+
bucket.activeCount += 1;
|
|
53
|
+
bucket.agentIds.add(run.agentId);
|
|
54
|
+
}
|
|
55
|
+
const channels = Array.from(byBucket.values())
|
|
56
|
+
.sort((a, b) => {
|
|
57
|
+
const c = a.channelId.localeCompare(b.channelId);
|
|
58
|
+
if (c !== 0) return c;
|
|
59
|
+
return (a.threadId ?? '').localeCompare(b.threadId ?? '');
|
|
60
|
+
})
|
|
61
|
+
.map(({ channelId, threadId, activeCount, agentIds }) => {
|
|
62
|
+
const row: ActiveRunsSnapshotEvent['data']['channels'][number] = {
|
|
63
|
+
channelId,
|
|
64
|
+
activeCount,
|
|
65
|
+
agentIds: Array.from(agentIds),
|
|
66
|
+
};
|
|
67
|
+
if (threadId !== undefined) {
|
|
68
|
+
row.threadId = threadId;
|
|
69
|
+
}
|
|
70
|
+
return row;
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
type: 'agent:active-runs:snapshot',
|
|
74
|
+
data: { channels },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Drop every tracked run for a channel/thread. Returns the purged records so
|
|
80
|
+
* callers can emit synthetic `agent:run:end` events to SSE clients.
|
|
81
|
+
*/
|
|
82
|
+
purgeForThread(channelId: string, threadId?: string): ActiveRunRecord[] {
|
|
83
|
+
const target = threadId || undefined;
|
|
84
|
+
const removed: ActiveRunRecord[] = [];
|
|
85
|
+
|
|
86
|
+
for (const [key, run] of this.runs) {
|
|
87
|
+
if (run.channelId === channelId && (run.threadId || undefined) === target) {
|
|
88
|
+
removed.push(run);
|
|
89
|
+
this.runs.delete(key);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return removed;
|
|
94
|
+
}
|
|
95
|
+
}
|
package/src/app/cli.ts
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { storageService } from '../plugins/storage/service.js';
|
|
2
|
+
import { OpenBotEvent } from './types.js';
|
|
3
|
+
import { ensureEventId } from './utils.js';
|
|
4
|
+
|
|
5
|
+
export function persistLifecycleEvent(
|
|
6
|
+
event: OpenBotEvent,
|
|
7
|
+
targetChannelId: string,
|
|
8
|
+
targetThreadId?: string,
|
|
9
|
+
): void {
|
|
10
|
+
ensureEventId(event);
|
|
11
|
+
storageService
|
|
12
|
+
.storeEvent({
|
|
13
|
+
channelId: targetChannelId,
|
|
14
|
+
threadId: targetThreadId,
|
|
15
|
+
event,
|
|
16
|
+
})
|
|
17
|
+
.catch((error) => {
|
|
18
|
+
console.error('[server] Failed to persist lifecycle event', {
|
|
19
|
+
type: event.type,
|
|
20
|
+
channelId: targetChannelId,
|
|
21
|
+
threadId: targetThreadId,
|
|
22
|
+
error,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type LifecycleBroadcastDeps = {
|
|
28
|
+
sendToClientKey: (clientKey: string, event: OpenBotEvent) => void;
|
|
29
|
+
getClientKey: (channelId: string, threadId?: string) => string;
|
|
30
|
+
globalChannelId: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function broadcastLifecycleEvent(
|
|
34
|
+
event: OpenBotEvent,
|
|
35
|
+
channelId: string,
|
|
36
|
+
threadId: string | undefined,
|
|
37
|
+
deps: LifecycleBroadcastDeps,
|
|
38
|
+
options?: { persist?: boolean },
|
|
39
|
+
): void {
|
|
40
|
+
ensureEventId(event);
|
|
41
|
+
if (options?.persist !== false) {
|
|
42
|
+
persistLifecycleEvent(event, channelId, threadId);
|
|
43
|
+
}
|
|
44
|
+
deps.sendToClientKey(deps.getClientKey(channelId, threadId), event);
|
|
45
|
+
deps.sendToClientKey(deps.globalChannelId, event);
|
|
46
|
+
}
|