openbot 0.3.6 → 0.4.2
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/README.md +15 -16
- package/dist/app/agent-ids.js +4 -0
- package/dist/app/cli.js +1 -1
- package/dist/app/config.js +10 -19
- package/dist/app/server.js +208 -17
- package/dist/bus/services.js +34 -124
- package/dist/harness/agent-invoke-run.js +44 -0
- package/dist/harness/agent-turn.js +99 -0
- package/dist/harness/channel-participants.js +40 -0
- package/dist/harness/constants.js +2 -0
- package/dist/harness/context-meter.js +97 -0
- package/dist/harness/context.js +95 -47
- package/dist/harness/dispatch.js +144 -0
- package/dist/harness/dispatcher.js +45 -156
- package/dist/harness/history.js +177 -0
- package/dist/harness/index.js +109 -0
- package/dist/harness/orchestration.js +88 -0
- package/dist/harness/participants.js +22 -0
- package/dist/harness/run-harness.js +154 -0
- package/dist/harness/run.js +98 -0
- package/dist/harness/runtime-factory.js +0 -34
- package/dist/harness/runtime.js +57 -0
- package/dist/harness/todo-dispatch.js +51 -0
- package/dist/harness/todos.js +5 -0
- package/dist/harness/turn.js +79 -0
- package/dist/plugins/approval/index.js +120 -149
- package/dist/plugins/bash/index.js +195 -0
- package/dist/plugins/delegation/index.js +121 -32
- package/dist/plugins/memory/index.js +103 -14
- package/dist/plugins/memory/service.js +152 -0
- package/dist/plugins/openbot/context.js +125 -0
- package/dist/plugins/openbot/history.js +144 -0
- package/dist/plugins/openbot/index.js +71 -0
- package/dist/plugins/openbot/runtime.js +381 -0
- package/dist/plugins/openbot/system-prompt.js +25 -0
- package/dist/plugins/plugin-manager/index.js +189 -0
- package/dist/plugins/shell/index.js +2 -1
- package/dist/plugins/storage/files.js +67 -0
- package/dist/plugins/storage/index.js +750 -0
- package/dist/plugins/storage/service.js +1316 -0
- package/dist/plugins/storage-tools/index.js +2 -2
- package/dist/plugins/thread-namer/index.js +72 -0
- package/dist/plugins/thread-naming/generate-title.js +44 -0
- package/dist/plugins/thread-naming/index.js +103 -0
- package/dist/plugins/threads/index.js +114 -0
- package/dist/plugins/todo/index.js +24 -25
- package/dist/plugins/ui/index.js +109 -180
- package/dist/registry/plugins.js +3 -9
- package/dist/services/abort.js +43 -0
- package/dist/services/plugins/domain.js +1 -0
- package/dist/services/plugins/plugin-cache.js +9 -0
- package/dist/services/plugins/registry.js +112 -0
- package/dist/services/plugins/service.js +232 -0
- package/dist/services/plugins/types.js +1 -0
- package/dist/services/process.js +29 -0
- package/dist/services/storage.js +11 -10
- package/dist/services/thread-naming.js +81 -0
- package/docs/agents.md +15 -12
- package/docs/architecture.md +2 -2
- package/docs/plugins.md +29 -17
- package/docs/templates/AGENT.example.md +8 -14
- package/package.json +1 -2
- package/src/app/agent-ids.ts +5 -0
- package/src/app/cli.ts +1 -1
- package/src/app/config.ts +14 -31
- package/src/app/server.ts +243 -19
- package/src/app/types.ts +331 -187
- package/src/harness/index.ts +166 -0
- package/src/plugins/approval/index.ts +107 -188
- package/src/plugins/bash/index.ts +232 -0
- package/src/plugins/delegation/index.ts +139 -39
- package/src/plugins/memory/index.ts +112 -15
- package/src/{services/memory.ts → plugins/memory/service.ts} +1 -1
- package/src/plugins/openbot/context.ts +140 -0
- package/src/plugins/openbot/history.ts +158 -0
- package/src/plugins/openbot/index.ts +79 -0
- package/src/plugins/openbot/runtime.ts +478 -0
- package/src/plugins/openbot/system-prompt.ts +27 -0
- package/src/plugins/plugin-manager/index.ts +224 -0
- package/src/plugins/storage/files.ts +81 -0
- package/src/plugins/storage/index.ts +823 -0
- package/src/{services/storage.ts → plugins/storage/service.ts} +485 -105
- package/src/plugins/ui/index.ts +117 -221
- package/src/services/abort.ts +46 -0
- package/src/{bus/types.ts → services/plugins/domain.ts} +50 -8
- package/src/services/plugins/plugin-cache.ts +13 -0
- package/src/{registry/plugins.ts → services/plugins/registry.ts} +28 -28
- package/src/services/plugins/service.ts +318 -0
- package/src/{bus/plugin.ts → services/plugins/types.ts} +7 -3
- package/src/bus/services.ts +0 -954
- package/src/harness/context.ts +0 -365
- package/src/harness/dispatcher.ts +0 -379
- package/src/harness/mcp.ts +0 -78
- package/src/harness/runtime-factory.ts +0 -129
- package/src/harness/todo-advance.ts +0 -128
- package/src/plugins/ai-sdk/index.ts +0 -41
- package/src/plugins/ai-sdk/runtime.ts +0 -468
- package/src/plugins/ai-sdk/system-prompt.ts +0 -18
- package/src/plugins/mcp/index.ts +0 -128
- package/src/plugins/shell/index.ts +0 -123
- package/src/plugins/storage-tools/index.ts +0 -90
- package/src/plugins/todo/index.ts +0 -64
- package/src/services/plugins.ts +0 -133
- /package/src/{harness → services}/process.ts +0 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import { MelonyPlugin, RuntimeContext } from 'melony';
|
|
2
|
+
import { generateText, type LanguageModel } from 'ai';
|
|
3
|
+
import { openai } from '@ai-sdk/openai';
|
|
4
|
+
import { anthropic } from '@ai-sdk/anthropic';
|
|
5
|
+
import { OpenBotEvent, OpenBotState, AgentInvokeEvent } from '../../app/types.js';
|
|
6
|
+
import { eventsToModelMessages } from './history.js';
|
|
7
|
+
import { Storage } from '../../services/plugins/domain.js';
|
|
8
|
+
import type { ToolDefinition } from '../../services/plugins/types.js';
|
|
9
|
+
import {
|
|
10
|
+
ORCHESTRATOR_AGENT_ID,
|
|
11
|
+
buildContext,
|
|
12
|
+
} from './context.js';
|
|
13
|
+
import { saveConfig } from '../../app/config.js';
|
|
14
|
+
import { API_KEY_SETUP_MESSAGE, OPENBOT_SYSTEM_PROMPT } from './system-prompt.js';
|
|
15
|
+
|
|
16
|
+
export interface OpenBotRuntimeOptions {
|
|
17
|
+
/** Provider model string (e.g. `openai/gpt-4o-mini`, `anthropic/claude-3-5-sonnet-20240620`). */
|
|
18
|
+
model?: string;
|
|
19
|
+
storage?: Storage;
|
|
20
|
+
/** Tool definitions merged from all tool plugins attached to this agent. */
|
|
21
|
+
toolDefinitions?: Record<string, ToolDefinition>;
|
|
22
|
+
/** Fires when the run is stopped; cancels the in-flight LLM call. */
|
|
23
|
+
abortSignal?: AbortSignal;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolveModel(modelString: string): LanguageModel {
|
|
27
|
+
const [provider, ...rest] = modelString.split('/');
|
|
28
|
+
const modelId = rest.join('/');
|
|
29
|
+
if (!modelId) {
|
|
30
|
+
throw new Error(`Invalid model string: "${modelString}". Expected "provider/model-id".`);
|
|
31
|
+
}
|
|
32
|
+
switch (provider) {
|
|
33
|
+
case 'openai':
|
|
34
|
+
return openai(modelId);
|
|
35
|
+
case 'anthropic':
|
|
36
|
+
return anthropic(modelId);
|
|
37
|
+
default:
|
|
38
|
+
throw new Error(`Unsupported AI provider: "${provider}"`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function buildSystemPrompt(
|
|
43
|
+
state: OpenBotState,
|
|
44
|
+
storage: Storage | undefined,
|
|
45
|
+
): Promise<string> {
|
|
46
|
+
const context = await buildContext(state, storage);
|
|
47
|
+
|
|
48
|
+
const sections = [OPENBOT_SYSTEM_PROMPT, '', context];
|
|
49
|
+
|
|
50
|
+
// Hardcoded naming hint logic
|
|
51
|
+
const threadState = state.threadDetails?.state as any;
|
|
52
|
+
if (!threadState?.isSmartNamed) {
|
|
53
|
+
sections.push(
|
|
54
|
+
'',
|
|
55
|
+
'## SYSTEM HINT',
|
|
56
|
+
'This thread is unnamed. Please use the `patch_thread_details` tool to set a concise, descriptive, and regular `name` (e.g., "Project Brainstorming" instead of "project-brainstorm") in the thread state and set `isSmartNamed: true` in the same patch. Only do this once.',
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return sections.join('\n');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Tracks tool-call IDs from one LLM turn until matching `:result` events arrive.
|
|
65
|
+
*
|
|
66
|
+
* Melony runs yielded `action:*` events depth-first, so parallel tool calls from
|
|
67
|
+
* a single `generateText` response execute one-by-one. We must wait for every ID
|
|
68
|
+
* in the batch before calling the LLM again — not after the first result.
|
|
69
|
+
*/
|
|
70
|
+
function createToolBatchTracker(
|
|
71
|
+
state: OpenBotState,
|
|
72
|
+
storage?: Storage,
|
|
73
|
+
channelId?: string,
|
|
74
|
+
threadId?: string,
|
|
75
|
+
) {
|
|
76
|
+
const save = async (ids?: string[]) => {
|
|
77
|
+
if (!storage || !channelId || !threadId) return;
|
|
78
|
+
try {
|
|
79
|
+
await storage.patchThreadState({
|
|
80
|
+
channelId,
|
|
81
|
+
threadId,
|
|
82
|
+
state: { pendingToolCallIds: ids },
|
|
83
|
+
});
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error('[openbot] Failed to persist pendingToolCallIds:', error);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
async startBatch(toolCallIds: string[]) {
|
|
91
|
+
state.pendingToolCallIds = [...toolCallIds];
|
|
92
|
+
await save(state.pendingToolCallIds);
|
|
93
|
+
},
|
|
94
|
+
async clear() {
|
|
95
|
+
state.pendingToolCallIds = undefined;
|
|
96
|
+
await save(undefined);
|
|
97
|
+
},
|
|
98
|
+
/** Returns true when this result completes the batch (time to call the LLM again). */
|
|
99
|
+
async recordResult(toolCallId: string): Promise<boolean> {
|
|
100
|
+
if (!state.pendingToolCallIds?.includes(toolCallId)) return false;
|
|
101
|
+
state.pendingToolCallIds = state.pendingToolCallIds.filter((id) => id !== toolCallId);
|
|
102
|
+
const done = state.pendingToolCallIds.length === 0;
|
|
103
|
+
if (done) {
|
|
104
|
+
state.pendingToolCallIds = undefined;
|
|
105
|
+
}
|
|
106
|
+
await save(state.pendingToolCallIds);
|
|
107
|
+
return done;
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* OpenBot agent runtime.
|
|
114
|
+
*
|
|
115
|
+
* - One `generateText` call per `runLLM` (tools have no `execute`; SDK stops at 1 step).
|
|
116
|
+
* - Tool calls become `action:*` events; plugins emit `:result` when done.
|
|
117
|
+
* - When a full batch of results is in, `runLLM` runs again with updated history.
|
|
118
|
+
*/
|
|
119
|
+
export const openbotRuntime =
|
|
120
|
+
(options: OpenBotRuntimeOptions): MelonyPlugin<OpenBotState, OpenBotEvent> =>
|
|
121
|
+
(builder) => {
|
|
122
|
+
const {
|
|
123
|
+
model: modelString = 'openai/gpt-4o-mini',
|
|
124
|
+
storage,
|
|
125
|
+
toolDefinitions = {},
|
|
126
|
+
abortSignal,
|
|
127
|
+
} = options;
|
|
128
|
+
|
|
129
|
+
let currentModelString = modelString;
|
|
130
|
+
let model = resolveModel(currentModelString);
|
|
131
|
+
|
|
132
|
+
const runLLM = async function* (
|
|
133
|
+
context: RuntimeContext<OpenBotState, OpenBotEvent>,
|
|
134
|
+
threadId?: string,
|
|
135
|
+
trigger?: AgentInvokeEvent,
|
|
136
|
+
): AsyncGenerator<OpenBotEvent> {
|
|
137
|
+
if (!storage) return;
|
|
138
|
+
if (abortSignal?.aborted) return;
|
|
139
|
+
|
|
140
|
+
const toolBatch = createToolBatchTracker(
|
|
141
|
+
context.state,
|
|
142
|
+
storage,
|
|
143
|
+
context.state.channelId,
|
|
144
|
+
threadId || context.state.threadId,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// Capture parent metadata for event enrichment
|
|
148
|
+
const triggerEvent = trigger || context.state.triggerEvent;
|
|
149
|
+
const parentAgentId = triggerEvent?.meta?.parentAgentId;
|
|
150
|
+
const parentToolCallId = triggerEvent?.meta?.parentToolCallId;
|
|
151
|
+
|
|
152
|
+
context.state.model = currentModelString;
|
|
153
|
+
|
|
154
|
+
const systemPrompt = await buildSystemPrompt(context.state, storage);
|
|
155
|
+
|
|
156
|
+
const events = await storage.getEvents({
|
|
157
|
+
channelId: context.state.channelId,
|
|
158
|
+
threadId: context.state.threadId,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const messages = eventsToModelMessages(events);
|
|
162
|
+
|
|
163
|
+
// console.log('systemPrompt:::::::\n', systemPrompt);
|
|
164
|
+
// console.log('messages:::::::\n', JSON.stringify(messages));
|
|
165
|
+
// console.log('toolDefinitions:::::::\n', JSON.stringify(toolDefinitions));
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
// Single LLM request — tool execution happens externally via action:* handlers.
|
|
169
|
+
const result = await generateText({
|
|
170
|
+
model,
|
|
171
|
+
system: systemPrompt,
|
|
172
|
+
messages,
|
|
173
|
+
tools: toolDefinitions as Record<string, { description: string; inputSchema: any }>,
|
|
174
|
+
stopWhen: ({ steps }) => steps.length === 1,
|
|
175
|
+
allowSystemInMessages: true,
|
|
176
|
+
abortSignal,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const toolCalls = result.toolCalls ?? [];
|
|
180
|
+
|
|
181
|
+
// if (result.usage) {
|
|
182
|
+
// const usage = result.usage;
|
|
183
|
+
// yield {
|
|
184
|
+
// type: 'agent:usage',
|
|
185
|
+
// data: {
|
|
186
|
+
// usage: {
|
|
187
|
+
// promptTokens: usage.inputTokens,
|
|
188
|
+
// completionTokens: usage.outputTokens,
|
|
189
|
+
// totalTokens: usage.totalTokens,
|
|
190
|
+
// currentContextTokens: usage.inputTokens,
|
|
191
|
+
// contextBudget: getContextBudgetForModel(currentModelString),
|
|
192
|
+
// },
|
|
193
|
+
// model: currentModelString,
|
|
194
|
+
// },
|
|
195
|
+
// meta: {
|
|
196
|
+
// agentId: context.state.agentId,
|
|
197
|
+
// threadId,
|
|
198
|
+
// runId: context.state.runId,
|
|
199
|
+
// },
|
|
200
|
+
// } as OpenBotEvent;
|
|
201
|
+
// }
|
|
202
|
+
|
|
203
|
+
const outputMeta = {
|
|
204
|
+
agentId: context.state.agentId,
|
|
205
|
+
threadId,
|
|
206
|
+
parentAgentId,
|
|
207
|
+
parentToolCallId,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Text before actions so history/UI show the model's intent first.
|
|
211
|
+
if (result.text) {
|
|
212
|
+
yield {
|
|
213
|
+
type: 'agent:output',
|
|
214
|
+
data: { content: result.text },
|
|
215
|
+
meta: outputMeta,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (toolCalls.length > 0) {
|
|
220
|
+
// when multiple tool calls are made, Melony runtime handles them one by one, thats why we need to start a new batch
|
|
221
|
+
await toolBatch.startBatch(toolCalls.map((tc) => tc.toolCallId));
|
|
222
|
+
|
|
223
|
+
for (const toolCall of toolCalls) {
|
|
224
|
+
yield {
|
|
225
|
+
type: `action:${toolCall.toolName}` as OpenBotEvent['type'],
|
|
226
|
+
data: toolCall.input,
|
|
227
|
+
meta: {
|
|
228
|
+
toolCallId: toolCall.toolCallId,
|
|
229
|
+
...outputMeta,
|
|
230
|
+
},
|
|
231
|
+
} as unknown as OpenBotEvent;
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
// clear the tool batch if there are no tool calls
|
|
235
|
+
await toolBatch.clear();
|
|
236
|
+
}
|
|
237
|
+
} catch (error: unknown) {
|
|
238
|
+
// Run was stopped — unwind quietly without surfacing an error.
|
|
239
|
+
if (abortSignal?.aborted) return;
|
|
240
|
+
|
|
241
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
242
|
+
const isApiKeyError =
|
|
243
|
+
errorMessage.includes('API key') ||
|
|
244
|
+
errorMessage.includes('401') ||
|
|
245
|
+
errorMessage.includes('Unauthorized') ||
|
|
246
|
+
errorMessage.includes('authentication');
|
|
247
|
+
|
|
248
|
+
if (isApiKeyError) {
|
|
249
|
+
const [currentProvider, ...rest] = currentModelString.split('/');
|
|
250
|
+
const currentModelId = rest.join('/');
|
|
251
|
+
|
|
252
|
+
yield {
|
|
253
|
+
type: 'client:ui:widget',
|
|
254
|
+
data: {
|
|
255
|
+
kind: 'form',
|
|
256
|
+
widgetId: `api_key_request_${Date.now()}`,
|
|
257
|
+
title: `AI Provider API Key Required`,
|
|
258
|
+
description: API_KEY_SETUP_MESSAGE,
|
|
259
|
+
fields: [
|
|
260
|
+
{
|
|
261
|
+
id: 'provider',
|
|
262
|
+
label: 'Provider',
|
|
263
|
+
type: 'select',
|
|
264
|
+
required: true,
|
|
265
|
+
options: [
|
|
266
|
+
{ label: 'OpenAI', value: 'openai' },
|
|
267
|
+
{ label: 'Anthropic', value: 'anthropic' },
|
|
268
|
+
],
|
|
269
|
+
defaultValue: currentProvider === 'anthropic' ? 'anthropic' : 'openai',
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
id: 'model',
|
|
273
|
+
label: 'Model',
|
|
274
|
+
type: 'text',
|
|
275
|
+
description:
|
|
276
|
+
'Model name without the provider prefix (e.g. `gpt-4o-mini` or `claude-3-5-sonnet-20240620`).',
|
|
277
|
+
placeholder: 'gpt-4o-mini',
|
|
278
|
+
required: true,
|
|
279
|
+
defaultValue: currentModelId,
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
id: 'apiKey',
|
|
283
|
+
label: 'API Key',
|
|
284
|
+
type: 'text',
|
|
285
|
+
placeholder: `sk-...`,
|
|
286
|
+
required: true,
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
submitLabel: 'Save & Continue',
|
|
290
|
+
metadata: {
|
|
291
|
+
type: 'api_key_request',
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
meta: { agentId: context.state.agentId, threadId },
|
|
295
|
+
} as OpenBotEvent;
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
builder.on('agent:invoke', async function* (event, context) {
|
|
304
|
+
const routedTo = (event as { data?: { agentId?: string } }).data?.agentId;
|
|
305
|
+
if (typeof routedTo === 'string' && routedTo && routedTo !== context.state.agentId) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Capture user info from meta if available
|
|
310
|
+
if (event.meta?.userName) {
|
|
311
|
+
context.state.currentUser = {
|
|
312
|
+
userName: event.meta.userName,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const threadId = event.meta?.threadId || context.state.threadId;
|
|
317
|
+
|
|
318
|
+
// Auto-add participants if tagged in the prompt
|
|
319
|
+
const content = (event as AgentInvokeEvent).data?.content;
|
|
320
|
+
if (content && storage) {
|
|
321
|
+
try {
|
|
322
|
+
const allAgents = await storage.getAgents();
|
|
323
|
+
const tags = content.match(/@([\w-]+)/g);
|
|
324
|
+
if (tags) {
|
|
325
|
+
const taggedAgentIds = tags.map((t) => t.slice(1));
|
|
326
|
+
const validAgentIds = taggedAgentIds.filter((id) =>
|
|
327
|
+
allAgents.some((a) => a.id === id),
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const currentParticipants = context.state.channelDetails?.participants || [];
|
|
331
|
+
const newParticipants = [...new Set([...currentParticipants, ...validAgentIds])];
|
|
332
|
+
|
|
333
|
+
if (newParticipants.length > currentParticipants.length) {
|
|
334
|
+
// Update storage
|
|
335
|
+
await storage.patchChannelState({
|
|
336
|
+
channelId: context.state.channelId,
|
|
337
|
+
state: { participants: newParticipants },
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Refresh local state
|
|
341
|
+
context.state.channelDetails = await storage.getChannelDetails({
|
|
342
|
+
channelId: context.state.channelId,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Notify UI/others about the change
|
|
346
|
+
yield {
|
|
347
|
+
type: 'action:patch_channel_details:result',
|
|
348
|
+
data: { success: true, updatedFields: ['participants'] },
|
|
349
|
+
meta: {
|
|
350
|
+
agentId: context.state.agentId,
|
|
351
|
+
threadId,
|
|
352
|
+
},
|
|
353
|
+
} as OpenBotEvent;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
} catch (error) {
|
|
357
|
+
console.warn('[openbot] Failed to auto-add participants from tags:', error);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// clear the tool batch if the agent is invoked
|
|
362
|
+
// this is to prevent the tool batch from being used for a new agent invocation
|
|
363
|
+
await createToolBatchTracker(
|
|
364
|
+
context.state,
|
|
365
|
+
storage,
|
|
366
|
+
context.state.channelId,
|
|
367
|
+
threadId,
|
|
368
|
+
).clear();
|
|
369
|
+
|
|
370
|
+
yield* runLLM(context, threadId, event as AgentInvokeEvent);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// this is to handle the tool results from the tool calls
|
|
374
|
+
// because Melony runtime handles them one by one, thats why we need to record the result
|
|
375
|
+
builder.on('*', async function* (event, context) {
|
|
376
|
+
if (!event.type.endsWith(':result')) return;
|
|
377
|
+
if (event.meta?.agentId !== context.state.agentId) return;
|
|
378
|
+
|
|
379
|
+
const toolCallId = event.meta?.toolCallId;
|
|
380
|
+
// record the result of the tool call
|
|
381
|
+
if (
|
|
382
|
+
!toolCallId ||
|
|
383
|
+
!(await createToolBatchTracker(
|
|
384
|
+
context.state,
|
|
385
|
+
storage,
|
|
386
|
+
context.state.channelId,
|
|
387
|
+
event.meta?.threadId || context.state.threadId,
|
|
388
|
+
).recordResult(toolCallId))
|
|
389
|
+
)
|
|
390
|
+
return;
|
|
391
|
+
|
|
392
|
+
const threadId = event.meta?.threadId || context.state.threadId;
|
|
393
|
+
yield* runLLM(context, threadId);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
builder.on('client:ui:widget:response', async function* (event, context) {
|
|
397
|
+
const { metadata, values } = event.data;
|
|
398
|
+
if (metadata?.type !== 'api_key_request') return;
|
|
399
|
+
if (!values?.apiKey || !values?.provider || !values?.model) return;
|
|
400
|
+
|
|
401
|
+
const provider = String(values.provider);
|
|
402
|
+
const modelId = String(values.model).trim();
|
|
403
|
+
const apiKey = String(values.apiKey);
|
|
404
|
+
|
|
405
|
+
if (provider !== 'openai' && provider !== 'anthropic') {
|
|
406
|
+
yield {
|
|
407
|
+
type: 'agent:output',
|
|
408
|
+
data: { content: `Unsupported provider: ${provider}` },
|
|
409
|
+
meta: { agentId: context.state.agentId },
|
|
410
|
+
};
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const envVar = provider === 'openai' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY';
|
|
415
|
+
const newModelString = `${provider}/${modelId}`;
|
|
416
|
+
|
|
417
|
+
if (!storage) return;
|
|
418
|
+
try {
|
|
419
|
+
await storage.createVariable({ key: envVar, value: apiKey, secret: true });
|
|
420
|
+
process.env[envVar] = apiKey;
|
|
421
|
+
|
|
422
|
+
currentModelString = newModelString;
|
|
423
|
+
model = resolveModel(currentModelString);
|
|
424
|
+
try {
|
|
425
|
+
saveConfig({ model: currentModelString });
|
|
426
|
+
|
|
427
|
+
// Also update the agent's AGENT.md if it has an openbot plugin config
|
|
428
|
+
const details = await storage.getAgentDetails({ agentId: context.state.agentId });
|
|
429
|
+
const updatedPlugins = details.pluginRefs.map((ref) => {
|
|
430
|
+
if (ref.id === 'openbot') {
|
|
431
|
+
return {
|
|
432
|
+
...ref,
|
|
433
|
+
config: { ...ref.config, model: currentModelString },
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
return ref;
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
await storage.updateAgent({
|
|
440
|
+
agentId: context.state.agentId,
|
|
441
|
+
plugins: updatedPlugins,
|
|
442
|
+
});
|
|
443
|
+
} catch {
|
|
444
|
+
// best-effort: config persistence failure shouldn't block the conversation
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
yield {
|
|
448
|
+
type: 'agent:output',
|
|
449
|
+
data: {
|
|
450
|
+
content: `Saved ${provider} API key and set model to \`${newModelString}\`.`,
|
|
451
|
+
},
|
|
452
|
+
meta: { agentId: context.state.agentId },
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
yield {
|
|
456
|
+
type: 'client:ui:widget',
|
|
457
|
+
data: {
|
|
458
|
+
widgetId: event.data.widgetId,
|
|
459
|
+
kind: 'message',
|
|
460
|
+
title: 'API Key Saved',
|
|
461
|
+
body: `Successfully saved ${provider} API key and selected model \`${newModelString}\`. You can now continue your conversation.`,
|
|
462
|
+
state: 'submitted',
|
|
463
|
+
actions: [{ id: 'ok', label: 'Got it', variant: 'primary' }],
|
|
464
|
+
},
|
|
465
|
+
meta: { agentId: context.state.agentId },
|
|
466
|
+
};
|
|
467
|
+
} catch (error) {
|
|
468
|
+
yield {
|
|
469
|
+
type: 'agent:output',
|
|
470
|
+
data: {
|
|
471
|
+
content: `Failed to save API key: ${error instanceof Error ? error.message : 'Unknown error'
|
|
472
|
+
}`,
|
|
473
|
+
},
|
|
474
|
+
meta: { agentId: context.state.agentId },
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const OPENBOT_SYSTEM_PROMPT = [
|
|
2
|
+
'# ROLE',
|
|
3
|
+
'You are an OpenBot, the main coordinator and router agent. Your primary role is to orchestrate specialized agents to help the human achieve their goals.',
|
|
4
|
+
'',
|
|
5
|
+
'# SECURITY POLICY',
|
|
6
|
+
'- **CRITICAL**: Never request API keys, passwords, or sensitive credentials via text or UI widgets; these are managed deterministically via secure forms and must never enter your context.',
|
|
7
|
+
'- **Credential Guidance**: If an agent or tool requires credentials, inform the user they can be managed under "Settings > Variables".',
|
|
8
|
+
'',
|
|
9
|
+
'# CORE MISSION',
|
|
10
|
+
'You almost never execute tasks yourself. Instead, you delegate tasks to specialized agents (channel participants). You act as a high-level manager, ensuring the right agent is working on the right task.',
|
|
11
|
+
'',
|
|
12
|
+
'# OPERATIONAL GUIDELINES',
|
|
13
|
+
'- **Channel and Threads**: The main and only way to communicate and act is through channels and threads. There might be a channel called "uncategorized" for general purpose communication.',
|
|
14
|
+
'- **Agent Participation**: ONLY add an agent via `patch_channel_details` if the user manually tags them (e.g., `@name`) AND they are missing from the `Participants` list in `ENVIRONMENT`.',
|
|
15
|
+
'- **Delegation**: NEVER delegate to an agent who is not a participant. Only if existing participants clearly cannot handle a task should you suggest relevant agents from the `INSTALLED AGENTS` list.',
|
|
16
|
+
'- **Bash Tool Usage**: You should use the `bash` tool very rarely. Only use it when the user explicitly requests a command to be run or when it is absolutely necessary for a task that no other participant can handle.',
|
|
17
|
+
'- **Context Awareness**: Use the provided ENVIRONMENT, CHANNEL SPECIFICATION, and MEMORIES to maintain continuity. Do not ask for information already present in these sections.',
|
|
18
|
+
'- **Durable Memory**: Use the `remember` tool to store important facts, preferences, or project details that should persist across sessions.',
|
|
19
|
+
'- **Structured Interaction**: Use the `render_widget` tool to collect information via forms, offer choices, or display lists. This is preferred over asking multiple separate questions in plain text.',
|
|
20
|
+
'',
|
|
21
|
+
'# COMMUNICATION STYLE',
|
|
22
|
+
'- Be always concise, professional, and proactive.',
|
|
23
|
+
].join('\n');
|
|
24
|
+
|
|
25
|
+
/** Shown in the API key setup form when no provider credentials are configured. */
|
|
26
|
+
export const API_KEY_SETUP_MESSAGE =
|
|
27
|
+
'OpenBot runs AI agents locally with tools, memory, and delegation. Bring your own OpenAI or Anthropic key — it stays on your machine. Use the form below to get started.';
|