openbot 0.5.4 → 0.5.6
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 +5 -4
- package/deploy/README.md +2 -4
- package/dist/app/cli.js +3 -1
- package/dist/app/cloud-mode.js +5 -16
- package/dist/app/ensure-default-stack.js +54 -0
- package/dist/app/openbot-plugin.js +4 -0
- package/dist/app/server.js +2 -3
- package/dist/harness/index.js +3 -0
- package/dist/plugins/openbot/index.js +6 -5
- package/dist/plugins/openbot/model.js +2 -41
- package/dist/plugins/openbot/runtime.js +9 -4
- package/dist/plugins/storage/index.js +3 -325
- package/dist/plugins/storage/service.js +18 -21
- package/dist/services/plugins/host.js +21 -0
- package/dist/services/plugins/model-registry.js +34 -9
- package/dist/services/plugins/registry.js +26 -42
- package/dist/services/plugins/service.js +5 -1
- package/dist/services/todo/types.js +1 -0
- package/docs/plugins.md +14 -12
- package/docs/templates/AGENT.example.md +8 -14
- package/package.json +2 -2
- package/src/app/cli.ts +3 -1
- package/src/app/cloud-mode.ts +7 -22
- package/src/app/ensure-default-stack.ts +63 -0
- package/src/app/openbot-plugin.ts +5 -0
- package/src/app/server.ts +2 -5
- package/src/app/types.ts +4 -8
- package/src/harness/index.ts +4 -0
- package/src/plugins/storage/index.ts +3 -368
- package/src/plugins/storage/service.ts +17 -22
- package/src/services/plugins/host.ts +36 -0
- package/src/services/plugins/model-registry.ts +41 -9
- package/src/services/plugins/registry.ts +28 -43
- package/src/services/plugins/service.ts +13 -12
- package/src/services/plugins/types.ts +36 -2
- package/src/services/todo/types.ts +12 -0
- package/src/plugins/approval/index.ts +0 -147
- package/src/plugins/bash/index.ts +0 -545
- package/src/plugins/delegation/index.ts +0 -153
- package/src/plugins/memory/index.ts +0 -182
- package/src/plugins/openbot/context.ts +0 -137
- package/src/plugins/openbot/history.ts +0 -158
- package/src/plugins/openbot/index.ts +0 -95
- package/src/plugins/openbot/model.ts +0 -76
- package/src/plugins/openbot/runtime.ts +0 -504
- package/src/plugins/openbot/system-prompt.ts +0 -57
- package/src/plugins/preview/index.ts +0 -323
- package/src/plugins/todo/index.ts +0 -166
- package/src/plugins/todo/service.ts +0 -123
- package/src/plugins/ui/index.ts +0 -130
|
@@ -1,504 +0,0 @@
|
|
|
1
|
-
import { MelonyPlugin, RuntimeContext } from 'melony';
|
|
2
|
-
import { generateText } from 'ai';
|
|
3
|
-
import { OpenBotEvent, OpenBotState, AgentInvokeEvent } from '../../app/types.js';
|
|
4
|
-
import { eventsToModelMessages } from './history.js';
|
|
5
|
-
import { Storage } from '../../services/plugins/domain.js';
|
|
6
|
-
import type { ToolDefinition } from '../../services/plugins/types.js';
|
|
7
|
-
import { buildContext } from './context.js';
|
|
8
|
-
import { saveConfig } from '../../app/config.js';
|
|
9
|
-
import { isCloudSystemAgent } from '../../app/cloud-mode.js';
|
|
10
|
-
import { OPENBOT_SYSTEM_PROMPT } from './system-prompt.js';
|
|
11
|
-
import { resolveModel } from './model.js';
|
|
12
|
-
import {
|
|
13
|
-
listApiKeyProvidersFromRegistry,
|
|
14
|
-
resolveModelRegistry,
|
|
15
|
-
} from '../../services/plugins/model-registry.js';
|
|
16
|
-
|
|
17
|
-
export interface OpenBotRuntimeOptions {
|
|
18
|
-
/** Provider model string (e.g. `openai/gpt-4o-mini`, `anthropic/claude-3-5-sonnet-20240620`). */
|
|
19
|
-
model?: string;
|
|
20
|
-
agentId?: string;
|
|
21
|
-
storage?: Storage;
|
|
22
|
-
/** Tool definitions merged from all tool plugins attached to this agent. */
|
|
23
|
-
toolDefinitions?: Record<string, ToolDefinition>;
|
|
24
|
-
/** Fires when the run is stopped; cancels the in-flight LLM call. */
|
|
25
|
-
abortSignal?: AbortSignal;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function buildSystemPrompt(
|
|
29
|
-
state: OpenBotState,
|
|
30
|
-
storage: Storage | undefined,
|
|
31
|
-
): Promise<string> {
|
|
32
|
-
const context = await buildContext(state, storage);
|
|
33
|
-
|
|
34
|
-
const sections = [OPENBOT_SYSTEM_PROMPT, '', context];
|
|
35
|
-
|
|
36
|
-
// Hardcoded naming hint logic
|
|
37
|
-
const threadState = state.threadDetails?.state as any;
|
|
38
|
-
if (!threadState?.isSmartNamed) {
|
|
39
|
-
sections.push(
|
|
40
|
-
'',
|
|
41
|
-
'## SYSTEM HINT',
|
|
42
|
-
'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.',
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return sections.join('\n');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Tracks tool-call IDs from one LLM turn until matching `:result` events arrive.
|
|
51
|
-
*
|
|
52
|
-
* Melony runs yielded `action:*` events depth-first, so parallel tool calls from
|
|
53
|
-
* a single `generateText` response execute one-by-one. We must wait for every ID
|
|
54
|
-
* in the batch before calling the LLM again — not after the first result.
|
|
55
|
-
*/
|
|
56
|
-
function createToolBatchTracker(
|
|
57
|
-
state: OpenBotState,
|
|
58
|
-
storage?: Storage,
|
|
59
|
-
channelId?: string,
|
|
60
|
-
threadId?: string,
|
|
61
|
-
) {
|
|
62
|
-
const save = async (ids?: string[]) => {
|
|
63
|
-
if (!storage || !channelId || !threadId) return;
|
|
64
|
-
try {
|
|
65
|
-
await storage.patchThreadState({
|
|
66
|
-
channelId,
|
|
67
|
-
threadId,
|
|
68
|
-
state: { pendingToolCallIds: ids },
|
|
69
|
-
});
|
|
70
|
-
} catch (error) {
|
|
71
|
-
console.error('[openbot] Failed to persist pendingToolCallIds:', error);
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
async startBatch(toolCallIds: string[]) {
|
|
77
|
-
state.pendingToolCallIds = [...toolCallIds];
|
|
78
|
-
await save(state.pendingToolCallIds);
|
|
79
|
-
},
|
|
80
|
-
async clear() {
|
|
81
|
-
state.pendingToolCallIds = undefined;
|
|
82
|
-
await save(undefined);
|
|
83
|
-
},
|
|
84
|
-
/** Returns true when this result completes the batch (time to call the LLM again). */
|
|
85
|
-
async recordResult(toolCallId: string): Promise<boolean> {
|
|
86
|
-
if (!state.pendingToolCallIds?.includes(toolCallId)) return false;
|
|
87
|
-
state.pendingToolCallIds = state.pendingToolCallIds.filter((id) => id !== toolCallId);
|
|
88
|
-
const done = state.pendingToolCallIds.length === 0;
|
|
89
|
-
if (done) {
|
|
90
|
-
state.pendingToolCallIds = undefined;
|
|
91
|
-
}
|
|
92
|
-
await save(state.pendingToolCallIds);
|
|
93
|
-
return done;
|
|
94
|
-
},
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* OpenBot agent runtime.
|
|
100
|
-
*
|
|
101
|
-
* - One `generateText` call per `runLLM` (tools have no `execute`; SDK stops at 1 step).
|
|
102
|
-
* - Tool calls become `action:*` events; plugins emit `:result` when done.
|
|
103
|
-
* - When a full batch of results is in, `runLLM` runs again with updated history.
|
|
104
|
-
*/
|
|
105
|
-
export const openbotRuntime =
|
|
106
|
-
(options: OpenBotRuntimeOptions): MelonyPlugin<OpenBotState, OpenBotEvent> =>
|
|
107
|
-
(builder) => {
|
|
108
|
-
const {
|
|
109
|
-
model: modelString = 'openai/gpt-4o-mini',
|
|
110
|
-
agentId,
|
|
111
|
-
storage,
|
|
112
|
-
toolDefinitions = {},
|
|
113
|
-
abortSignal,
|
|
114
|
-
} = options;
|
|
115
|
-
|
|
116
|
-
let currentModelString = modelString;
|
|
117
|
-
let model = resolveModel(currentModelString, agentId);
|
|
118
|
-
|
|
119
|
-
const runLLM = async function* (
|
|
120
|
-
context: RuntimeContext<OpenBotState, OpenBotEvent>,
|
|
121
|
-
threadId?: string,
|
|
122
|
-
trigger?: AgentInvokeEvent,
|
|
123
|
-
): AsyncGenerator<OpenBotEvent> {
|
|
124
|
-
if (!storage) return;
|
|
125
|
-
if (abortSignal?.aborted) return;
|
|
126
|
-
|
|
127
|
-
const toolBatch = createToolBatchTracker(
|
|
128
|
-
context.state,
|
|
129
|
-
storage,
|
|
130
|
-
context.state.channelId,
|
|
131
|
-
threadId || context.state.threadId,
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
// Capture parent metadata for event enrichment
|
|
135
|
-
const triggerEvent = trigger || context.state.triggerEvent;
|
|
136
|
-
const parentAgentId = triggerEvent?.meta?.parentAgentId;
|
|
137
|
-
const parentToolCallId = triggerEvent?.meta?.parentToolCallId;
|
|
138
|
-
|
|
139
|
-
context.state.model = currentModelString;
|
|
140
|
-
|
|
141
|
-
const systemPrompt = await buildSystemPrompt(context.state, storage);
|
|
142
|
-
|
|
143
|
-
const events = await storage.getEvents({
|
|
144
|
-
channelId: context.state.channelId,
|
|
145
|
-
threadId: context.state.threadId,
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
const messages = eventsToModelMessages(events);
|
|
149
|
-
|
|
150
|
-
// console.log('systemPrompt:::::::\n', systemPrompt);
|
|
151
|
-
// console.log('messages:::::::\n', JSON.stringify(messages));
|
|
152
|
-
// console.log('toolDefinitions:::::::\n', JSON.stringify(toolDefinitions));
|
|
153
|
-
|
|
154
|
-
try {
|
|
155
|
-
// Single LLM request — tool execution happens externally via action:* handlers.
|
|
156
|
-
const result = await generateText({
|
|
157
|
-
model,
|
|
158
|
-
system: systemPrompt,
|
|
159
|
-
messages,
|
|
160
|
-
tools: toolDefinitions as Record<string, { description: string; inputSchema: any }>,
|
|
161
|
-
stopWhen: ({ steps }) => steps.length === 1,
|
|
162
|
-
allowSystemInMessages: true,
|
|
163
|
-
abortSignal,
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
const toolCalls = result.toolCalls ?? [];
|
|
167
|
-
|
|
168
|
-
// if (result.usage) {
|
|
169
|
-
// const usage = result.usage;
|
|
170
|
-
// yield {
|
|
171
|
-
// type: 'agent:usage',
|
|
172
|
-
// data: {
|
|
173
|
-
// usage: {
|
|
174
|
-
// promptTokens: usage.inputTokens,
|
|
175
|
-
// completionTokens: usage.outputTokens,
|
|
176
|
-
// totalTokens: usage.totalTokens,
|
|
177
|
-
// currentContextTokens: usage.inputTokens,
|
|
178
|
-
// contextBudget: getContextBudgetForModel(currentModelString),
|
|
179
|
-
// },
|
|
180
|
-
// model: currentModelString,
|
|
181
|
-
// },
|
|
182
|
-
// meta: {
|
|
183
|
-
// agentId: context.state.agentId,
|
|
184
|
-
// threadId,
|
|
185
|
-
// runId: context.state.runId,
|
|
186
|
-
// },
|
|
187
|
-
// } as OpenBotEvent;
|
|
188
|
-
// }
|
|
189
|
-
|
|
190
|
-
const outputMeta = {
|
|
191
|
-
agentId: context.state.agentId,
|
|
192
|
-
threadId,
|
|
193
|
-
parentAgentId,
|
|
194
|
-
parentToolCallId,
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
// Text before actions so history/UI show the model's intent first.
|
|
198
|
-
if (result.text) {
|
|
199
|
-
yield {
|
|
200
|
-
type: 'agent:output',
|
|
201
|
-
data: { content: result.text },
|
|
202
|
-
meta: outputMeta,
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (toolCalls.length > 0) {
|
|
207
|
-
// when multiple tool calls are made, Melony runtime handles them one by one, thats why we need to start a new batch
|
|
208
|
-
await toolBatch.startBatch(toolCalls.map((tc) => tc.toolCallId));
|
|
209
|
-
|
|
210
|
-
for (const toolCall of toolCalls) {
|
|
211
|
-
yield {
|
|
212
|
-
type: `action:${toolCall.toolName}` as OpenBotEvent['type'],
|
|
213
|
-
data: toolCall.input,
|
|
214
|
-
meta: {
|
|
215
|
-
toolCallId: toolCall.toolCallId,
|
|
216
|
-
...outputMeta,
|
|
217
|
-
},
|
|
218
|
-
} as unknown as OpenBotEvent;
|
|
219
|
-
}
|
|
220
|
-
} else {
|
|
221
|
-
// clear the tool batch if there are no tool calls
|
|
222
|
-
await toolBatch.clear();
|
|
223
|
-
}
|
|
224
|
-
} catch (error: unknown) {
|
|
225
|
-
// Run was stopped — unwind quietly without surfacing an error.
|
|
226
|
-
if (abortSignal?.aborted) return;
|
|
227
|
-
|
|
228
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
229
|
-
const isApiKeyError =
|
|
230
|
-
errorMessage.includes('API key') ||
|
|
231
|
-
errorMessage.includes('401') ||
|
|
232
|
-
errorMessage.includes('Unauthorized') ||
|
|
233
|
-
errorMessage.includes('authentication');
|
|
234
|
-
|
|
235
|
-
if (isApiKeyError && !isCloudSystemAgent(context.state.agentId)) {
|
|
236
|
-
const registry = await resolveModelRegistry();
|
|
237
|
-
const providerActions = listApiKeyProvidersFromRegistry(registry).map((provider) => ({
|
|
238
|
-
id: provider.id,
|
|
239
|
-
label: provider.label,
|
|
240
|
-
variant: 'primary' as const,
|
|
241
|
-
}));
|
|
242
|
-
|
|
243
|
-
yield {
|
|
244
|
-
type: 'client:ui:widget',
|
|
245
|
-
data: {
|
|
246
|
-
kind: 'choice',
|
|
247
|
-
widgetId: `api_provider_selection_${Date.now()}`,
|
|
248
|
-
title: `Setup AI Provider`,
|
|
249
|
-
description: `Select a provider to continue.`,
|
|
250
|
-
actions:
|
|
251
|
-
providerActions.length > 0
|
|
252
|
-
? providerActions
|
|
253
|
-
: [
|
|
254
|
-
{ id: 'openai', label: 'OpenAI', variant: 'primary' as const },
|
|
255
|
-
{ id: 'anthropic', label: 'Anthropic', variant: 'primary' as const },
|
|
256
|
-
{ id: 'google', label: 'Google', variant: 'primary' as const },
|
|
257
|
-
],
|
|
258
|
-
metadata: {
|
|
259
|
-
type: 'api_provider_selection',
|
|
260
|
-
},
|
|
261
|
-
},
|
|
262
|
-
meta: { agentId: context.state.agentId, threadId },
|
|
263
|
-
} as OpenBotEvent;
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
throw error;
|
|
268
|
-
}
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
builder.on('agent:invoke', async function* (event, context) {
|
|
272
|
-
const routedTo = (event as { data?: { agentId?: string } }).data?.agentId;
|
|
273
|
-
if (typeof routedTo === 'string' && routedTo && routedTo !== context.state.agentId) {
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Capture user info from meta if available
|
|
278
|
-
if (event.meta?.userName) {
|
|
279
|
-
context.state.currentUser = {
|
|
280
|
-
userName: event.meta.userName,
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const threadId = event.meta?.threadId || context.state.threadId;
|
|
285
|
-
|
|
286
|
-
// clear the tool batch if the agent is invoked
|
|
287
|
-
// this is to prevent the tool batch from being used for a new agent invocation
|
|
288
|
-
await createToolBatchTracker(
|
|
289
|
-
context.state,
|
|
290
|
-
storage,
|
|
291
|
-
context.state.channelId,
|
|
292
|
-
threadId,
|
|
293
|
-
).clear();
|
|
294
|
-
|
|
295
|
-
yield* runLLM(context, threadId, event as AgentInvokeEvent);
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
// this is to handle the tool results from the tool calls
|
|
299
|
-
// because Melony runtime handles them one by one, thats why we need to record the result
|
|
300
|
-
builder.on('*', async function* (event, context) {
|
|
301
|
-
if (!event.type.endsWith(':result')) return;
|
|
302
|
-
if (event.meta?.agentId !== context.state.agentId) return;
|
|
303
|
-
|
|
304
|
-
const toolCallId = event.meta?.toolCallId;
|
|
305
|
-
// record the result of the tool call
|
|
306
|
-
if (
|
|
307
|
-
!toolCallId ||
|
|
308
|
-
!(await createToolBatchTracker(
|
|
309
|
-
context.state,
|
|
310
|
-
storage,
|
|
311
|
-
context.state.channelId,
|
|
312
|
-
event.meta?.threadId || context.state.threadId,
|
|
313
|
-
).recordResult(toolCallId))
|
|
314
|
-
)
|
|
315
|
-
return;
|
|
316
|
-
|
|
317
|
-
const threadId = event.meta?.threadId || context.state.threadId;
|
|
318
|
-
yield* runLLM(context, threadId);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
builder.on('client:ui:widget:response', async function* (event, context) {
|
|
322
|
-
const { metadata, values, actionId } = event.data;
|
|
323
|
-
const threadId = event.meta?.threadId || context.state.threadId;
|
|
324
|
-
|
|
325
|
-
if (isCloudSystemAgent(context.state.agentId)) return;
|
|
326
|
-
|
|
327
|
-
if (metadata?.type === 'api_provider_selection') {
|
|
328
|
-
const provider = actionId;
|
|
329
|
-
const [_, ...rest] = currentModelString.split('/');
|
|
330
|
-
const currentModelId = rest.join('/');
|
|
331
|
-
|
|
332
|
-
const registry = await resolveModelRegistry();
|
|
333
|
-
const providerData = registry.providers?.[provider as string];
|
|
334
|
-
|
|
335
|
-
const providerLinks: Record<string, string> = {
|
|
336
|
-
openai: 'https://platform.openai.com/api-keys',
|
|
337
|
-
anthropic: 'https://console.anthropic.com/settings/keys',
|
|
338
|
-
google: 'https://aistudio.google.com/app/apikey',
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
const label = providerData?.label || (provider as string);
|
|
342
|
-
const link = providerLinks[provider as string] || '';
|
|
343
|
-
|
|
344
|
-
const modelOptions = providerData?.models.map((m) => ({
|
|
345
|
-
label: m.label,
|
|
346
|
-
value: m.id,
|
|
347
|
-
}));
|
|
348
|
-
|
|
349
|
-
if (!modelOptions || modelOptions.length === 0) {
|
|
350
|
-
yield {
|
|
351
|
-
type: 'agent:output',
|
|
352
|
-
data: {
|
|
353
|
-
content: `No models are listed for **${label}** in the marketplace registry.`,
|
|
354
|
-
},
|
|
355
|
-
meta: { agentId: context.state.agentId },
|
|
356
|
-
};
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const defaultModel = modelOptions[0].value;
|
|
361
|
-
const defaultValue =
|
|
362
|
-
modelOptions.find((m) => m.value === currentModelId)?.value || defaultModel;
|
|
363
|
-
|
|
364
|
-
yield {
|
|
365
|
-
type: 'client:ui:widget',
|
|
366
|
-
data: {
|
|
367
|
-
widgetId: event.data.widgetId,
|
|
368
|
-
kind: 'message',
|
|
369
|
-
title: 'Provider Selected',
|
|
370
|
-
body: `${label} provider was selected.`,
|
|
371
|
-
state: 'submitted',
|
|
372
|
-
display: 'collapsed',
|
|
373
|
-
disabled: true,
|
|
374
|
-
actions: [],
|
|
375
|
-
},
|
|
376
|
-
meta: { agentId: context.state.agentId, threadId },
|
|
377
|
-
} as OpenBotEvent;
|
|
378
|
-
|
|
379
|
-
yield {
|
|
380
|
-
type: 'client:ui:widget',
|
|
381
|
-
data: {
|
|
382
|
-
kind: 'form',
|
|
383
|
-
widgetId: `api_key_request_${Date.now()}`,
|
|
384
|
-
title: `${label} Setup`,
|
|
385
|
-
description: `Enter your API key and select a model.`,
|
|
386
|
-
fields: [
|
|
387
|
-
{
|
|
388
|
-
id: 'model',
|
|
389
|
-
label: 'Model',
|
|
390
|
-
type: 'select',
|
|
391
|
-
options: modelOptions,
|
|
392
|
-
required: true,
|
|
393
|
-
defaultValue,
|
|
394
|
-
},
|
|
395
|
-
{
|
|
396
|
-
id: 'apiKey',
|
|
397
|
-
label: 'API Key',
|
|
398
|
-
type: 'password',
|
|
399
|
-
description: `Get your key here: [${link}](${link})`,
|
|
400
|
-
placeholder: `sk-...`,
|
|
401
|
-
required: true,
|
|
402
|
-
},
|
|
403
|
-
],
|
|
404
|
-
submitLabel: 'Save & Continue',
|
|
405
|
-
metadata: {
|
|
406
|
-
type: 'api_key_request',
|
|
407
|
-
provider,
|
|
408
|
-
},
|
|
409
|
-
},
|
|
410
|
-
meta: { agentId: context.state.agentId, threadId },
|
|
411
|
-
} as OpenBotEvent;
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
if (metadata?.type !== 'api_key_request') return;
|
|
416
|
-
if (!values?.apiKey || !values?.model) return;
|
|
417
|
-
|
|
418
|
-
const provider = String(values.provider || metadata.provider);
|
|
419
|
-
const modelId = String(values.model).trim();
|
|
420
|
-
const apiKey = String(values.apiKey);
|
|
421
|
-
|
|
422
|
-
if (provider !== 'openai' && provider !== 'anthropic' && provider !== 'google') {
|
|
423
|
-
yield {
|
|
424
|
-
type: 'agent:output',
|
|
425
|
-
data: { content: `Unsupported provider: ${provider}` },
|
|
426
|
-
meta: { agentId: context.state.agentId },
|
|
427
|
-
};
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const envVar =
|
|
432
|
-
provider === 'openai'
|
|
433
|
-
? 'OPENAI_API_KEY'
|
|
434
|
-
: provider === 'anthropic'
|
|
435
|
-
? 'ANTHROPIC_API_KEY'
|
|
436
|
-
: 'GOOGLE_GENERATIVE_AI_API_KEY';
|
|
437
|
-
const newModelString = `${provider}/${modelId}`;
|
|
438
|
-
|
|
439
|
-
if (!storage) return;
|
|
440
|
-
try {
|
|
441
|
-
await storage.createVariable({ key: envVar, value: apiKey, secret: true });
|
|
442
|
-
process.env[envVar] = apiKey;
|
|
443
|
-
|
|
444
|
-
currentModelString = newModelString;
|
|
445
|
-
model = resolveModel(currentModelString, agentId);
|
|
446
|
-
try {
|
|
447
|
-
saveConfig({ model: currentModelString });
|
|
448
|
-
|
|
449
|
-
// Also update the agent's AGENT.md if it has an openbot plugin config
|
|
450
|
-
const details = await storage.getAgentDetails({ agentId: context.state.agentId });
|
|
451
|
-
const updatedPlugins = details.pluginRefs.map((ref) => {
|
|
452
|
-
if (ref.id === 'openbot') {
|
|
453
|
-
return {
|
|
454
|
-
...ref,
|
|
455
|
-
config: { ...ref.config, model: currentModelString },
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
return ref;
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
await storage.updateAgent({
|
|
462
|
-
agentId: context.state.agentId,
|
|
463
|
-
plugins: updatedPlugins,
|
|
464
|
-
});
|
|
465
|
-
} catch {
|
|
466
|
-
// best-effort: config persistence failure shouldn't block the conversation
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
yield {
|
|
470
|
-
type: 'agent:output',
|
|
471
|
-
data: {
|
|
472
|
-
content: `Saved ${provider} API key and set model to \`${newModelString}\`.`,
|
|
473
|
-
},
|
|
474
|
-
meta: { agentId: context.state.agentId },
|
|
475
|
-
};
|
|
476
|
-
|
|
477
|
-
yield {
|
|
478
|
-
type: 'client:ui:widget',
|
|
479
|
-
data: {
|
|
480
|
-
widgetId: event.data.widgetId,
|
|
481
|
-
kind: 'message',
|
|
482
|
-
title: 'API Key Saved',
|
|
483
|
-
body: `Successfully saved ${provider} API key and selected model \`${newModelString}\`. You can now continue your conversation.`,
|
|
484
|
-
state: 'submitted',
|
|
485
|
-
display: 'collapsed',
|
|
486
|
-
disabled: true,
|
|
487
|
-
actions: [],
|
|
488
|
-
},
|
|
489
|
-
meta: { agentId: context.state.agentId },
|
|
490
|
-
};
|
|
491
|
-
|
|
492
|
-
yield* runLLM(context, threadId);
|
|
493
|
-
} catch (error) {
|
|
494
|
-
yield {
|
|
495
|
-
type: 'agent:output',
|
|
496
|
-
data: {
|
|
497
|
-
content: `Failed to save API key: ${error instanceof Error ? error.message : 'Unknown error'
|
|
498
|
-
}`,
|
|
499
|
-
},
|
|
500
|
-
meta: { agentId: context.state.agentId },
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
});
|
|
504
|
-
};
|
|
@@ -1,57 +0,0 @@
|
|
|
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 and manage tasks methodically 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 act as a high-level manager, ensuring the right specialized agent is working on the right task. However, when orchestrating or when specialized agents are not available, you are capable of executing complex steps yourself using a highly structured, stateful, and disciplined approach.',
|
|
11
|
-
'',
|
|
12
|
-
'# THE AGENT LOOP',
|
|
13
|
-
'You operate in an iterative agent loop to complete user-assigned tasks step-by-step:',
|
|
14
|
-
'1. **Analyze Events**: Understand user needs and the current state through the chronological event stream (focusing on latest user messages and execution/observation results).',
|
|
15
|
-
'2. **Select Tools**: Choose the next tool call based on current state, todos, and environment constraints. To maximize precision, choose only one tool call per iteration for complex tasks, or parallel calls if they are independent.',
|
|
16
|
-
'3. **Wait for Execution**: Let the system execute the tool action and add observations/results back into the event stream.',
|
|
17
|
-
'4. **Iterate**: Patiently repeat the above steps, analyzing results at each turn, until all steps of the task are completed.',
|
|
18
|
-
'5. **Submit Results**: Message the human with clear, polished, and detailed outcomes, attaching any relevant files or deliverables.',
|
|
19
|
-
'6. **Enter Standby**: Enter an idle state and wait for new tasks.',
|
|
20
|
-
'',
|
|
21
|
-
'# TASK TRACKING (TODOS)',
|
|
22
|
-
'- For any complex multi-step task (3+ steps), you **MUST** maintain a todo list with `todo_write`.',
|
|
23
|
-
'- Write the full intended list at task start. Each call replaces the previous list (not a partial patch).',
|
|
24
|
-
'- Keep exactly one item `in_progress` while working. Mark items `completed` immediately after finishing them.',
|
|
25
|
-
'- Use `cancelled` for steps that are no longer needed. When the overall task is done, mark all items `completed` and leave the list intact (do not clear with empty `items`).',
|
|
26
|
-
'- The current list is injected into context each turn as `## TODOS`; call `todo_read` only if you need an explicit refresh.',
|
|
27
|
-
'- Do not stop with open `pending` or `in_progress` items unless you are blocked and have told the user why.',
|
|
28
|
-
'',
|
|
29
|
-
'# OPERATIONAL GUIDELINES',
|
|
30
|
-
'- **Channel and Threads**: The main way to communicate and act is through channels and threads. There might be a channel called "uncategorized" for general purpose communication.',
|
|
31
|
-
'- **Delegation**: You can delegate tasks to any specialized agent in the `INSTALLED AGENTS` list using the `delegate` tool.',
|
|
32
|
-
'- **Durable Memory**: Use the `remember` tool to store important facts, preferences, or project details that should persist across sessions.',
|
|
33
|
-
'- **Hub-and-Spoke**: Specialized agents cannot communicate directly; as coordinator, you must pass relevant data from one agent to another.',
|
|
34
|
-
'',
|
|
35
|
-
'# SHELL & FILE EXECUTION RULES',
|
|
36
|
-
'- **Stateful Sessions**: Use `shell_exec` with a session `id` (e.g. `default`, `server`). Reuse the same id to keep shell state (cwd, env) across commands.',
|
|
37
|
-
'- **Working Directory**: Always pass an absolute `exec_dir` (use the channel workspace path from ENVIRONMENT).',
|
|
38
|
-
'- **Command Discipline**: Avoid interactive prompts when possible; use `-y`, `-f`, or non-interactive flags. For prompts, use `shell_write_to_process`.',
|
|
39
|
-
'- **Long-Running Processes**: Start dev servers with `shell_exec` using `&` at the end (e.g. `pnpm dev &`). If you forget `&`, `shell_exec` returns after ~15s with partial logs — the server may still be running.',
|
|
40
|
-
'- **Polling dev servers**: After starting (or after a timeout), poll with `shell_wait` (2–5s) then `shell_view` until logs show ready (URL/port). Do not start a duplicate server; reuse the same session id.',
|
|
41
|
-
'- **Stop dev servers**: Use `shell_kill_process` before restarting or when finished.',
|
|
42
|
-
'- **Preview URLs**: After a dev server is ready, use `expose_port` with its port to generate a public preview URL (stored on channel details as `previewUrl`). Expose tunnels are temporary; close with `unexpose_port`.',
|
|
43
|
-
'- **Calculations**: Use `bc` or Python for math. Do not calculate complex math mentally.',
|
|
44
|
-
'- **Chaining**: Chain related commands with `&&` in a single `shell_exec` when they must run sequentially.',
|
|
45
|
-
'',
|
|
46
|
-
'# COMMUNICATION & WRITING STYLE',
|
|
47
|
-
'- Be concise, professional, proactive, and polite.',
|
|
48
|
-
'- Confirm receipt of user messages quickly and outline your high-level strategy brief before executing a long series of steps.',
|
|
49
|
-
'- Inform the user via messages when you change methods or find strategic issues.',
|
|
50
|
-
'- Write in clean, continuous prose. Avoid excessive bullet points or lists in final responses unless explicitly requested.',
|
|
51
|
-
'- All major deliverables and documents must be highly detailed and complete; do not summarize or truncate final outputs unless specified.',
|
|
52
|
-
'',
|
|
53
|
-
'# ERROR HANDLING',
|
|
54
|
-
'- Tool failures are provided as events. When an error occurs, do not panic or immediately ask the user for help.',
|
|
55
|
-
'- Carefully analyze the stderr or failure logs, verify tool/argument names, and attempt alternative approaches or parameter fixes.',
|
|
56
|
-
'- Only report failure reasons and ask the human for help after multiple alternative approaches have failed.',
|
|
57
|
-
].join('\n');
|