openbot 0.3.5 → 0.4.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.
Files changed (98) hide show
  1. package/README.md +15 -16
  2. package/dist/app/agent-ids.js +4 -0
  3. package/dist/app/cli.js +1 -1
  4. package/dist/app/config.js +0 -19
  5. package/dist/app/server.js +8 -14
  6. package/dist/assets/icon.svg +9 -3
  7. package/dist/bus/services.js +78 -132
  8. package/dist/harness/agent-invoke-run.js +44 -0
  9. package/dist/harness/agent-turn.js +99 -0
  10. package/dist/harness/channel-participants.js +40 -0
  11. package/dist/harness/constants.js +2 -0
  12. package/dist/harness/context-meter.js +97 -0
  13. package/dist/harness/context.js +98 -45
  14. package/dist/harness/dispatch.js +144 -0
  15. package/dist/harness/dispatcher.js +45 -156
  16. package/dist/harness/history.js +177 -0
  17. package/dist/harness/index.js +91 -0
  18. package/dist/harness/orchestration.js +88 -0
  19. package/dist/harness/participants.js +22 -0
  20. package/dist/harness/run-harness.js +154 -0
  21. package/dist/harness/run.js +98 -0
  22. package/dist/harness/runtime-factory.js +0 -34
  23. package/dist/harness/runtime.js +57 -0
  24. package/dist/harness/todo-dispatch.js +51 -0
  25. package/dist/harness/todos.js +5 -0
  26. package/dist/harness/turn.js +79 -0
  27. package/dist/plugins/approval/index.js +105 -149
  28. package/dist/plugins/delegation/index.js +119 -32
  29. package/dist/plugins/memory/index.js +103 -14
  30. package/dist/plugins/memory/service.js +152 -0
  31. package/dist/plugins/openbot/context.js +80 -0
  32. package/dist/plugins/openbot/history.js +98 -0
  33. package/dist/plugins/openbot/index.js +31 -0
  34. package/dist/plugins/openbot/runtime.js +317 -0
  35. package/dist/plugins/openbot/system-prompt.js +5 -0
  36. package/dist/plugins/plugin-manager/index.js +105 -0
  37. package/dist/plugins/storage/index.js +573 -0
  38. package/dist/plugins/storage/service.js +1159 -0
  39. package/dist/plugins/storage-tools/index.js +2 -2
  40. package/dist/plugins/thread-namer/index.js +72 -0
  41. package/dist/plugins/thread-naming/generate-title.js +44 -0
  42. package/dist/plugins/thread-naming/index.js +103 -0
  43. package/dist/plugins/threads/index.js +114 -0
  44. package/dist/plugins/todo/index.js +24 -25
  45. package/dist/plugins/ui/index.js +2 -32
  46. package/dist/registry/plugins.js +3 -9
  47. package/dist/services/plugins/domain.js +1 -0
  48. package/dist/services/plugins/plugin-cache.js +9 -0
  49. package/dist/services/plugins/registry.js +110 -0
  50. package/dist/services/plugins/service.js +177 -0
  51. package/dist/services/plugins/types.js +1 -0
  52. package/dist/services/process.js +29 -0
  53. package/dist/services/storage.js +41 -15
  54. package/dist/services/thread-naming.js +81 -0
  55. package/docs/agents.md +16 -10
  56. package/docs/architecture.md +2 -2
  57. package/docs/plugins.md +6 -15
  58. package/docs/templates/AGENT.example.md +7 -13
  59. package/package.json +1 -2
  60. package/src/app/agent-ids.ts +5 -0
  61. package/src/app/cli.ts +1 -1
  62. package/src/app/config.ts +1 -31
  63. package/src/app/server.ts +8 -16
  64. package/src/app/types.ts +70 -190
  65. package/src/assets/icon.svg +9 -3
  66. package/src/harness/index.ts +145 -0
  67. package/src/plugins/approval/index.ts +91 -189
  68. package/src/plugins/delegation/index.ts +136 -39
  69. package/src/plugins/memory/index.ts +112 -15
  70. package/src/{services/memory.ts → plugins/memory/service.ts} +1 -1
  71. package/src/plugins/openbot/context.ts +91 -0
  72. package/src/plugins/openbot/history.ts +107 -0
  73. package/src/plugins/openbot/index.ts +37 -0
  74. package/src/plugins/openbot/runtime.ts +384 -0
  75. package/src/plugins/openbot/system-prompt.ts +7 -0
  76. package/src/plugins/plugin-manager/index.ts +122 -0
  77. package/src/plugins/shell/index.ts +1 -1
  78. package/src/plugins/storage/index.ts +633 -0
  79. package/src/{services/storage.ts → plugins/storage/service.ts} +257 -72
  80. package/src/{bus/types.ts → services/plugins/domain.ts} +20 -7
  81. package/src/services/plugins/plugin-cache.ts +13 -0
  82. package/src/{registry/plugins.ts → services/plugins/registry.ts} +25 -27
  83. package/src/services/{plugins.ts → plugins/service.ts} +96 -2
  84. package/src/{bus/plugin.ts → services/plugins/types.ts} +3 -3
  85. package/src/bus/services.ts +0 -908
  86. package/src/harness/context.ts +0 -356
  87. package/src/harness/dispatcher.ts +0 -379
  88. package/src/harness/mcp.ts +0 -78
  89. package/src/harness/runtime-factory.ts +0 -129
  90. package/src/harness/todo-advance.ts +0 -128
  91. package/src/plugins/ai-sdk/index.ts +0 -41
  92. package/src/plugins/ai-sdk/runtime.ts +0 -468
  93. package/src/plugins/ai-sdk/system-prompt.ts +0 -18
  94. package/src/plugins/mcp/index.ts +0 -128
  95. package/src/plugins/storage-tools/index.ts +0 -90
  96. package/src/plugins/todo/index.ts +0 -64
  97. package/src/plugins/ui/index.ts +0 -227
  98. /package/src/{harness → services}/process.ts +0 -0
@@ -1,128 +0,0 @@
1
- import { TodoItem, TodoStatus } from '../app/types.js';
2
- import type { Storage } from '../bus/types.js';
3
-
4
- /** Stored on each todo and inlined into the next assignee's invoke payload. */
5
- const RESULT_MAX_CHARS = 12000;
6
-
7
- /**
8
- * Shared helpers that drive the autonomous todo loop. The queue processor
9
- * calls `advanceAfterRun` once per `agent:run:end`; that is the only place
10
- * todos are completed and dispatched, which keeps the autonomous flow
11
- * single-threaded and easy to reason about.
12
- */
13
-
14
- export const readTodosFromState = (state: unknown): TodoItem[] => {
15
- const raw = (state as Record<string, unknown> | undefined)?.todos;
16
- return Array.isArray(raw) ? (raw as TodoItem[]) : [];
17
- };
18
-
19
- export function truncateTodoResult(text: string, maxChars = RESULT_MAX_CHARS): string | undefined {
20
- const trimmed = text.trim();
21
- if (!trimmed) return undefined;
22
- if (trimmed.length <= maxChars) return trimmed;
23
- return `${trimmed.slice(0, maxChars)}\n…[truncated]`;
24
- }
25
-
26
- export interface AdvanceResult {
27
- /** Updated todo list (after marking finished + flipping next to in_progress). */
28
- todos: TodoItem[];
29
- /** Next agent to invoke, if any. */
30
- handoff: { agentId: string; content: string; todoId: string } | null;
31
- }
32
-
33
- /**
34
- * Apply a single advance step:
35
- * 1. If a todo is `in_progress` and `assignee` matches the agent whose run
36
- * just ended, mark it `done` and attach `result` from `lastOutput` when present.
37
- * 2. Pick the next `pending` todo with an `assignee` and flip it to
38
- * `in_progress`. That assignee gets handed off to; `invoke content` includes
39
- * the previous step output when available so agents without short-term
40
- * history still see prior work.
41
- *
42
- * If a todo is already `in_progress` and the just-ended agent wasn't its
43
- * assignee, leave it alone — someone else is working.
44
- */
45
- export function advanceTodos(
46
- todos: TodoItem[],
47
- endedAgentId: string,
48
- lastOutput?: string,
49
- ): AdvanceResult {
50
- const now = Date.now();
51
- const truncated = truncateTodoResult(lastOutput ?? '');
52
-
53
- let completedOutput: string | undefined;
54
-
55
- let working = todos.map((t) => {
56
- if (t.status === 'in_progress' && t.assignee === endedAgentId) {
57
- completedOutput = truncated;
58
- return {
59
- ...t,
60
- status: 'done' as TodoStatus,
61
- updatedAt: now,
62
- ...(truncated !== undefined ? { result: truncated } : {}),
63
- };
64
- }
65
- return t;
66
- });
67
-
68
- if (working.some((t) => t.status === 'in_progress')) {
69
- return { todos: working, handoff: null };
70
- }
71
-
72
- const idx = working.findIndex((t) => t.status === 'pending' && t.assignee);
73
- if (idx === -1) return { todos: working, handoff: null };
74
-
75
- const picked = working[idx];
76
- working = working.map((t, i) =>
77
- i === idx ? { ...t, status: 'in_progress' as TodoStatus, updatedAt: now } : t,
78
- );
79
-
80
- const invokeContent =
81
- completedOutput !== undefined && completedOutput !== ''
82
- ? `${picked.content}\n\n--- Output from previous step ---\n${completedOutput}`
83
- : picked.content;
84
-
85
- return {
86
- todos: working,
87
- handoff: {
88
- agentId: picked.assignee!,
89
- content: invokeContent,
90
- todoId: picked.id,
91
- },
92
- };
93
- }
94
-
95
- export async function advanceAfterRun(options: {
96
- storage: Storage;
97
- channelId: string;
98
- threadId?: string;
99
- endedAgentId: string;
100
- lastAgentOutput?: string;
101
- }): Promise<AdvanceResult['handoff']> {
102
- const { storage, channelId, threadId, endedAgentId, lastAgentOutput } = options;
103
- if (!threadId) return null;
104
-
105
- const details = await storage.getThreadDetails({ channelId, threadId });
106
- const todos = readTodosFromState(details?.state);
107
- if (todos.length === 0) return null;
108
-
109
- const { todos: nextList, handoff } = advanceTodos(todos, endedAgentId, lastAgentOutput);
110
-
111
- const changed =
112
- nextList.length !== todos.length ||
113
- nextList.some((t, i) => {
114
- const u = todos[i];
115
- if (!u) return true;
116
- return (
117
- t.status !== u.status ||
118
- t.updatedAt !== u.updatedAt ||
119
- t.result !== u.result ||
120
- t.assignee !== u.assignee ||
121
- t.content !== u.content
122
- );
123
- });
124
- if (changed) {
125
- await storage.patchThreadState({ channelId, threadId, state: { todos: nextList } });
126
- }
127
- return handoff;
128
- }
@@ -1,41 +0,0 @@
1
- import type { Plugin } from '../../bus/plugin.js';
2
- import { aiSdkRuntime } from './runtime.js';
3
-
4
- /**
5
- * `ai-sdk` — generic LLM runtime plugin built on the Vercel AI SDK.
6
- *
7
- * Owns `agent:invoke` and consumes the merged `tools` map provided by the
8
- * agent loader (collected from every tool plugin attached to the same agent).
9
- * Pair with tool plugins like `shell`, `mcp`, `delegation`, etc.
10
- */
11
- export const aiSdkPlugin: Plugin = {
12
- id: 'ai-sdk',
13
- name: 'AI SDK Runtime',
14
- description:
15
- 'Generic LLM runtime built on the Vercel AI SDK. Consumes tools contributed by other plugins.',
16
- configSchema: {
17
- type: 'object',
18
- properties: {
19
- model: {
20
- type: 'string',
21
- description:
22
- 'Provider model string, e.g. openai/gpt-4o-mini, anthropic/claude-3-5-sonnet-20240620',
23
- default: 'openai/gpt-4o-mini',
24
- },
25
- },
26
- },
27
- factory: ({ agentDetails, config, storage, tools }) => {
28
- const model =
29
- typeof config.model === 'string' && config.model
30
- ? config.model
31
- : 'openai/gpt-4o-mini';
32
-
33
- return aiSdkRuntime({
34
- model,
35
- storage,
36
- toolDefinitions: tools,
37
- });
38
- },
39
- };
40
-
41
- export default aiSdkPlugin;
@@ -1,468 +0,0 @@
1
- import { MelonyPlugin, RuntimeContext } from 'melony';
2
- import { generateText, type LanguageModel, type ModelMessage } from 'ai';
3
- import { openai } from '@ai-sdk/openai';
4
- import { anthropic } from '@ai-sdk/anthropic';
5
- import { OpenBotEvent, OpenBotState, ShortTermMessage } from '../../app/types.js';
6
- import { Storage } from '../../bus/types.js';
7
- import type { ToolDefinition } from '../../bus/plugin.js';
8
- import { createDefaultContextEngine } from '../../harness/context.js';
9
- import { saveConfig } from '../../app/config.js';
10
-
11
- export interface AiSdkRuntimeOptions {
12
- /** Provider model string (e.g. `openai/gpt-4o-mini`, `anthropic/claude-3-5-sonnet-20240620`). */
13
- model?: string;
14
- storage?: Storage;
15
- contextEngine?: {
16
- buildContext: (state: OpenBotState, storage?: Storage) => Promise<string>;
17
- };
18
- /** Tool definitions merged from all tool plugins attached to this agent. */
19
- toolDefinitions?: Record<string, ToolDefinition>;
20
- }
21
-
22
- function resolveModel(modelString: string): LanguageModel {
23
- const [provider, ...rest] = modelString.split('/');
24
- const modelId = rest.join('/');
25
- if (!modelId) {
26
- throw new Error(`Invalid model string: "${modelString}". Expected "provider/model-id".`);
27
- }
28
- switch (provider) {
29
- case 'openai':
30
- return openai(modelId);
31
- case 'anthropic':
32
- return anthropic(modelId);
33
- default:
34
- throw new Error(`Unsupported AI provider: "${provider}"`);
35
- }
36
- }
37
-
38
- const asRecord = (value: unknown): Record<string, unknown> =>
39
- value && typeof value === 'object' && !Array.isArray(value)
40
- ? (value as Record<string, unknown>)
41
- : {};
42
-
43
- /** Per-message hard cap (in characters) on tool-result payloads we feed back
44
- * to the model. Prevents one huge tool output from eating the context window;
45
- * the original event remains intact in storage. */
46
- const TOOL_RESULT_MAX_CHARS = 8000;
47
-
48
- /** Sliding window: max number of messages we replay to the model on each
49
- * invocation. Older turns stay on disk but are not sent. Keeps both the
50
- * recent prompts and the prompt token budget bounded. */
51
- const MAX_WINDOW_MESSAGES = 80;
52
-
53
- const truncateToolPayload = (raw: unknown): string => {
54
- const serialized = typeof raw === 'string' ? raw : JSON.stringify(raw);
55
- if (serialized.length <= TOOL_RESULT_MAX_CHARS) return serialized;
56
- const dropped = serialized.length - TOOL_RESULT_MAX_CHARS;
57
- return `${serialized.slice(0, TOOL_RESULT_MAX_CHARS)}\n…[truncated ${dropped} chars]`;
58
- };
59
-
60
- /**
61
- * Trim the message history to a sliding window while preserving tool-call
62
- * integrity. Drops any leading orphan `tool` messages whose matching
63
- * assistant call was sliced off, since most providers reject that.
64
- */
65
- const buildMessageWindow = (messages: ShortTermMessage[]): ShortTermMessage[] => {
66
- if (messages.length <= MAX_WINDOW_MESSAGES) return messages;
67
- const tail = messages.slice(-MAX_WINDOW_MESSAGES);
68
- const knownAssistantCallIds = new Set<string>();
69
- for (const m of tail) {
70
- if (m.role === 'assistant' && m.toolCalls) {
71
- for (const tc of m.toolCalls) knownAssistantCallIds.add(tc.id);
72
- }
73
- }
74
- return tail.filter((m) => m.role !== 'tool' || knownAssistantCallIds.has(m.toolCallId));
75
- };
76
-
77
- /**
78
- * Self-healing pass: every assistant tool_call must have a matching tool
79
- * result before the next user/assistant turn, or providers (OpenAI in
80
- * particular) reject the request with "Tool result is missing for tool call".
81
- *
82
- * This can happen when a handler emits a `:result` event without `meta`
83
- * (orphaning the call), the process restarts mid-run, or a tool handler
84
- * crashes. Rather than refuse to continue, we inject synthetic tool messages
85
- * with a clear error payload — the LLM can then explain the failure to the
86
- * user and proceed.
87
- */
88
- const repairOpenToolCalls = (messages: ShortTermMessage[]): ShortTermMessage[] => {
89
- const fulfilled = new Set<string>();
90
- for (const m of messages) {
91
- if (m.role === 'tool') fulfilled.add(m.toolCallId);
92
- }
93
-
94
- const repaired: ShortTermMessage[] = [];
95
- for (const m of messages) {
96
- repaired.push(m);
97
- if (m.role !== 'assistant' || !m.toolCalls) continue;
98
- for (const tc of m.toolCalls) {
99
- if (fulfilled.has(tc.id)) continue;
100
- repaired.push({
101
- role: 'tool',
102
- toolCallId: tc.id,
103
- toolName: tc.function.name,
104
- content: JSON.stringify({
105
- success: false,
106
- error: 'Tool result was lost (handler did not emit a matching :result event).',
107
- }),
108
- });
109
- fulfilled.add(tc.id);
110
- }
111
- }
112
- return repaired;
113
- };
114
-
115
- const readPersistedShortTermMessages = (state: OpenBotState): ShortTermMessage[] => {
116
- const source = state.threadDetails?.state ?? state.channelDetails?.state;
117
- const record = asRecord(source);
118
- const raw = record.shortTermMessages;
119
- return Array.isArray(raw) ? (raw as ShortTermMessage[]) : [];
120
- };
121
-
122
- const persistShortTermMessages = async (
123
- state: OpenBotState,
124
- storage: Storage | undefined,
125
- ): Promise<void> => {
126
- if (!storage) return;
127
- const shortTermMessages = state.shortTermMessages ?? [];
128
- if (state.threadId) {
129
- await storage.patchThreadState({
130
- channelId: state.channelId,
131
- threadId: state.threadId,
132
- state: { shortTermMessages },
133
- });
134
- return;
135
- }
136
- await storage.patchChannelState({
137
- channelId: state.channelId,
138
- state: { shortTermMessages },
139
- });
140
- };
141
-
142
- async function buildSystemPrompt(
143
- state: OpenBotState,
144
- storage: Storage | undefined,
145
- contextEngine: {
146
- buildContext: (state: OpenBotState, storage?: Storage) => Promise<string>;
147
- },
148
- ): Promise<string> {
149
- return contextEngine.buildContext(state, storage);
150
- }
151
-
152
- /**
153
- * Generic ai-sdk runtime plugin.
154
- *
155
- * Owns `agent:invoke`, runs the LLM, emits tool-call events, and stitches tool
156
- * results back into the conversation. Tools are supplied externally by the
157
- * loader (merged from every tool plugin attached to the same agent).
158
- */
159
- export const aiSdkRuntime =
160
- (options: AiSdkRuntimeOptions): MelonyPlugin<OpenBotState, OpenBotEvent> =>
161
- (builder) => {
162
- const {
163
- model: modelString = 'openai/gpt-4o-mini',
164
- storage,
165
- contextEngine = createDefaultContextEngine(),
166
- toolDefinitions = {},
167
- } = options;
168
-
169
- let currentModelString = modelString;
170
- let model = resolveModel(currentModelString);
171
-
172
- const ensureShortTermMessages = (state: OpenBotState) => {
173
- if (!state.shortTermMessages || state.shortTermMessages.length === 0) {
174
- state.shortTermMessages = readPersistedShortTermMessages(state);
175
- }
176
- };
177
-
178
- const mapToCoreMessages = (messages: ShortTermMessage[]): ModelMessage[] => {
179
- return messages.map((m): ModelMessage => {
180
- if (m.role === 'assistant' && m.toolCalls) {
181
- return {
182
- role: 'assistant',
183
- content: [
184
- { type: 'text', text: m.content || '' },
185
- ...m.toolCalls.map((tc) => ({
186
- type: 'tool-call' as const,
187
- toolCallId: tc.id,
188
- toolName: tc.function.name,
189
- input: JSON.parse(tc.function.arguments),
190
- })),
191
- ],
192
- };
193
- }
194
- if (m.role === 'assistant') {
195
- return { role: 'assistant', content: m.content || '' };
196
- }
197
- if (m.role === 'tool') {
198
- return {
199
- role: 'tool',
200
- content: [
201
- {
202
- type: 'tool-result',
203
- toolCallId: m.toolCallId,
204
- toolName: m.toolName,
205
- output: { type: 'text', value: JSON.stringify(m.content) },
206
- },
207
- ],
208
- };
209
- }
210
- return m;
211
- });
212
- };
213
-
214
- const runLLM = async function* (
215
- context: RuntimeContext<OpenBotState, OpenBotEvent>,
216
- threadId?: string,
217
- ): AsyncGenerator<OpenBotEvent> {
218
- ensureShortTermMessages(context.state);
219
- const systemPrompt = await buildSystemPrompt(context.state, storage, contextEngine);
220
-
221
- const coreMessages = mapToCoreMessages(
222
- buildMessageWindow(repairOpenToolCalls(context.state.shortTermMessages || [])),
223
- );
224
-
225
- try {
226
- const result = await generateText({
227
- model,
228
- system: systemPrompt,
229
- messages: coreMessages,
230
- tools: toolDefinitions as Record<string, { description: string; inputSchema: any }>,
231
- });
232
-
233
- const toolCalls = result.toolCalls ?? [];
234
-
235
- if (toolCalls.length > 0) {
236
- context.state.shortTermMessages = [
237
- ...(context.state.shortTermMessages ?? []),
238
- {
239
- role: 'assistant',
240
- content: result.text || '',
241
- toolCalls: toolCalls.map((tc) => ({
242
- id: tc.toolCallId,
243
- type: 'function',
244
- function: {
245
- name: tc.toolName,
246
- arguments: JSON.stringify(tc.input),
247
- },
248
- })),
249
- },
250
- ];
251
- await persistShortTermMessages(context.state, storage);
252
-
253
- for (const toolCall of toolCalls) {
254
- yield {
255
- type: `action:${toolCall.toolName}` as OpenBotEvent['type'],
256
- data: toolCall.input,
257
- meta: {
258
- toolCallId: toolCall.toolCallId,
259
- agentId: context.state.agentId,
260
- threadId,
261
- },
262
- } as unknown as OpenBotEvent;
263
- }
264
- }
265
-
266
- if (result.text) {
267
- if (toolCalls.length === 0) {
268
- context.state.shortTermMessages = [
269
- ...(context.state.shortTermMessages ?? []),
270
- { role: 'assistant', content: result.text },
271
- ];
272
- await persistShortTermMessages(context.state, storage);
273
- }
274
-
275
- yield {
276
- type: 'agent:output',
277
- data: { content: result.text },
278
- meta: { agentId: context.state.agentId, threadId },
279
- };
280
- }
281
- } catch (error: unknown) {
282
- const errorMessage = error instanceof Error ? error.message : String(error);
283
- const isApiKeyError =
284
- errorMessage.includes('API key') ||
285
- errorMessage.includes('401') ||
286
- errorMessage.includes('Unauthorized') ||
287
- errorMessage.includes('authentication');
288
-
289
- if (isApiKeyError) {
290
- const [currentProvider, ...rest] = currentModelString.split('/');
291
- const currentModelId = rest.join('/');
292
- yield {
293
- type: 'client:ui:widget',
294
- data: {
295
- kind: 'form',
296
- widgetId: `api_key_request_${Date.now()}`,
297
- title: `AI Provider API Key Required`,
298
- description: `The AI provider returned an authentication error. Select your provider, model, and provide a valid API key to continue. The key never leaves your local runtime.`,
299
- fields: [
300
- {
301
- id: 'provider',
302
- label: 'Provider',
303
- type: 'select',
304
- required: true,
305
- options: [
306
- { label: 'OpenAI', value: 'openai' },
307
- { label: 'Anthropic', value: 'anthropic' },
308
- ],
309
- defaultValue: currentProvider === 'anthropic' ? 'anthropic' : 'openai',
310
- },
311
- {
312
- id: 'model',
313
- label: 'Model',
314
- type: 'text',
315
- description:
316
- 'Model name without the provider prefix (e.g. `gpt-4o-mini` or `claude-3-5-sonnet-20240620`).',
317
- placeholder: 'gpt-4o-mini',
318
- required: true,
319
- defaultValue: currentModelId,
320
- },
321
- {
322
- id: 'apiKey',
323
- label: 'API Key',
324
- type: 'text',
325
- placeholder: `sk-...`,
326
- required: true,
327
- },
328
- ],
329
- submitLabel: 'Save & Continue',
330
- metadata: {
331
- type: 'api_key_request',
332
- },
333
- },
334
- meta: { agentId: context.state.agentId, threadId },
335
- } as OpenBotEvent;
336
- return;
337
- }
338
-
339
- throw error;
340
- }
341
- };
342
-
343
- builder.on('agent:invoke', async function* (event, context) {
344
- const routedTo = (event as { data?: { agentId?: string } }).data?.agentId;
345
- if (typeof routedTo === 'string' && routedTo && routedTo !== context.state.agentId) {
346
- return;
347
- }
348
-
349
- const threadId = event.meta?.threadId || context.state.threadId;
350
-
351
- ensureShortTermMessages(context.state);
352
- context.state.shortTermMessages = [
353
- ...(context.state.shortTermMessages ?? []),
354
- {
355
- role: event.data?.role || 'user',
356
- content: event?.data?.content || '',
357
- },
358
- ];
359
- await persistShortTermMessages(context.state, storage);
360
-
361
- yield* runLLM(context, threadId);
362
- });
363
-
364
- builder.on('*', async function* (event, context) {
365
- if (!event.type.endsWith(':result')) return;
366
- if (event.meta?.agentId !== context.state.agentId) return;
367
- const toolCallId = event.meta?.toolCallId;
368
- if (!toolCallId) return;
369
- ensureShortTermMessages(context.state);
370
-
371
- const toolName = event.type.replace(/^action:/, '').replace(/:result$/, '');
372
- const resultData = (event as { data?: unknown }).data;
373
- const content = truncateToolPayload(resultData);
374
-
375
- context.state.shortTermMessages = [
376
- ...(context.state.shortTermMessages ?? []),
377
- { role: 'tool', content, toolCallId, toolName },
378
- ];
379
- await persistShortTermMessages(context.state, storage);
380
-
381
- const lastAssistant = [...(context.state.shortTermMessages ?? [])]
382
- .reverse()
383
- .find(
384
- (m): m is Extract<ShortTermMessage, { role: 'assistant' }> =>
385
- m.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length > 0,
386
- );
387
-
388
- if (lastAssistant && lastAssistant.toolCalls) {
389
- const allFulfilled = lastAssistant.toolCalls.every((tc) =>
390
- context.state.shortTermMessages?.some(
391
- (m) => m.role === 'tool' && m.toolCallId === tc.id,
392
- ),
393
- );
394
-
395
- if (allFulfilled) {
396
- if (toolName === 'handoff') return;
397
- const threadId = event.meta?.threadId || context.state.threadId;
398
- yield* runLLM(context, threadId);
399
- }
400
- }
401
- });
402
-
403
- builder.on('client:ui:widget:response', async function* (event, context) {
404
- const { metadata, values } = event.data;
405
- if (metadata?.type !== 'api_key_request') return;
406
- if (!values?.apiKey || !values?.provider || !values?.model) return;
407
-
408
- const provider = String(values.provider);
409
- const modelId = String(values.model).trim();
410
- const apiKey = String(values.apiKey);
411
-
412
- if (provider !== 'openai' && provider !== 'anthropic') {
413
- yield {
414
- type: 'agent:output',
415
- data: { content: `Unsupported provider: ${provider}` },
416
- meta: { agentId: context.state.agentId },
417
- };
418
- return;
419
- }
420
-
421
- const envVar = provider === 'openai' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY';
422
- const newModelString = `${provider}/${modelId}`;
423
-
424
- if (!storage) return;
425
- try {
426
- await storage.createVariable({ key: envVar, value: apiKey, secret: true });
427
- process.env[envVar] = apiKey;
428
-
429
- currentModelString = newModelString;
430
- model = resolveModel(currentModelString);
431
- try {
432
- saveConfig({ model: currentModelString });
433
- } catch {
434
- // best-effort: config persistence failure shouldn't block the conversation
435
- }
436
-
437
- yield {
438
- type: 'agent:output',
439
- data: {
440
- content: `Saved ${provider} API key and set model to \`${newModelString}\`.`,
441
- },
442
- meta: { agentId: context.state.agentId },
443
- };
444
-
445
- yield {
446
- type: 'client:ui:widget',
447
- data: {
448
- widgetId: event.data.widgetId,
449
- kind: 'message',
450
- title: 'API Key Saved',
451
- body: `Successfully saved ${provider} API key and selected model \`${newModelString}\`. You can now continue your conversation.`,
452
- state: 'submitted',
453
- actions: [{ id: 'ok', label: 'Got it', variant: 'primary' }],
454
- },
455
- meta: { agentId: context.state.agentId },
456
- };
457
- } catch (error) {
458
- yield {
459
- type: 'agent:output',
460
- data: {
461
- content: `Failed to save API key: ${error instanceof Error ? error.message : 'Unknown error'
462
- }`,
463
- },
464
- meta: { agentId: context.state.agentId },
465
- };
466
- }
467
- });
468
- };
@@ -1,18 +0,0 @@
1
- export const AI_SDK_SYSTEM_PROMPT = [
2
- 'You are a helpful AI assistant on the OpenBot platform.',
3
- 'Use the tools available to you to help the user.',
4
- 'Be concise unless the user asks for depth.',
5
- '',
6
- '## Planning with todos',
7
- 'The current thread has a shared todo list (visible under "Shared todo plan" in context).',
8
- 'It is the single source of truth for multi-step work and is shared across every agent in the thread.',
9
- '',
10
- 'When planning:',
11
- '- For any task that needs more than one step, call `todo_write` ONCE with the full ordered plan, then stop. Do not call any other tool in the same turn.',
12
- '- The platform dispatches assignees automatically and completes their todo when their run ends. You do NOT need to call `handoff` to start the plan or `todo_update` to finish items.',
13
- '- Each item must be concrete and atomic (one verb, one outcome). Skip the list entirely for trivial single-step requests.',
14
- '',
15
- 'When you are an assignee (you have an `in_progress` todo addressed to you):',
16
- '- Just do the work and reply. The platform will mark your todo done and dispatch the next one.',
17
- '- If you genuinely cannot complete it, call `todo_update(id, status: "cancelled")` with a brief reason in your reply.',
18
- ].join('\n');