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.
- 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 +0 -19
- package/dist/app/server.js +8 -14
- package/dist/assets/icon.svg +9 -3
- package/dist/bus/services.js +78 -132
- 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 +98 -45
- 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 +91 -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 +105 -149
- package/dist/plugins/delegation/index.js +119 -32
- package/dist/plugins/memory/index.js +103 -14
- package/dist/plugins/memory/service.js +152 -0
- package/dist/plugins/openbot/context.js +80 -0
- package/dist/plugins/openbot/history.js +98 -0
- package/dist/plugins/openbot/index.js +31 -0
- package/dist/plugins/openbot/runtime.js +317 -0
- package/dist/plugins/openbot/system-prompt.js +5 -0
- package/dist/plugins/plugin-manager/index.js +105 -0
- package/dist/plugins/storage/index.js +573 -0
- package/dist/plugins/storage/service.js +1159 -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 +2 -32
- package/dist/registry/plugins.js +3 -9
- package/dist/services/plugins/domain.js +1 -0
- package/dist/services/plugins/plugin-cache.js +9 -0
- package/dist/services/plugins/registry.js +110 -0
- package/dist/services/plugins/service.js +177 -0
- package/dist/services/plugins/types.js +1 -0
- package/dist/services/process.js +29 -0
- package/dist/services/storage.js +41 -15
- package/dist/services/thread-naming.js +81 -0
- package/docs/agents.md +16 -10
- package/docs/architecture.md +2 -2
- package/docs/plugins.md +6 -15
- package/docs/templates/AGENT.example.md +7 -13
- 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 +1 -31
- package/src/app/server.ts +8 -16
- package/src/app/types.ts +70 -190
- package/src/assets/icon.svg +9 -3
- package/src/harness/index.ts +145 -0
- package/src/plugins/approval/index.ts +91 -189
- package/src/plugins/delegation/index.ts +136 -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 +91 -0
- package/src/plugins/openbot/history.ts +107 -0
- package/src/plugins/openbot/index.ts +37 -0
- package/src/plugins/openbot/runtime.ts +384 -0
- package/src/plugins/openbot/system-prompt.ts +7 -0
- package/src/plugins/plugin-manager/index.ts +122 -0
- package/src/plugins/shell/index.ts +1 -1
- package/src/plugins/storage/index.ts +633 -0
- package/src/{services/storage.ts → plugins/storage/service.ts} +257 -72
- package/src/{bus/types.ts → services/plugins/domain.ts} +20 -7
- package/src/services/plugins/plugin-cache.ts +13 -0
- package/src/{registry/plugins.ts → services/plugins/registry.ts} +25 -27
- package/src/services/{plugins.ts → plugins/service.ts} +96 -2
- package/src/{bus/plugin.ts → services/plugins/types.ts} +3 -3
- package/src/bus/services.ts +0 -908
- package/src/harness/context.ts +0 -356
- 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/storage-tools/index.ts +0 -90
- package/src/plugins/todo/index.ts +0 -64
- package/src/plugins/ui/index.ts +0 -227
- /package/src/{harness → services}/process.ts +0 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { OpenBotState } from '../../app/types.js';
|
|
2
|
+
import { Storage } from '../../services/plugins/domain.js';
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_CONTEXT_BUDGET = 8000;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns the known context window budget (in tokens) for a given model string.
|
|
8
|
+
*/
|
|
9
|
+
export const getContextBudgetForModel = (modelString: string): number => {
|
|
10
|
+
const budgets: Record<string, number> = {
|
|
11
|
+
'openai/gpt-4o': 128000,
|
|
12
|
+
'openai/gpt-4o-mini': 128000,
|
|
13
|
+
'openai/o1-preview': 128000,
|
|
14
|
+
'openai/o1-mini': 128000,
|
|
15
|
+
'anthropic/claude-3-5-sonnet-20240620': 200000,
|
|
16
|
+
'anthropic/claude-3-5-sonnet-latest': 200000,
|
|
17
|
+
'anthropic/claude-3-opus-20240229': 200000,
|
|
18
|
+
'anthropic/claude-3-sonnet-20240229': 200000,
|
|
19
|
+
'anthropic/claude-3-haiku-20240307': 200000,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return budgets[modelString] || DEFAULT_CONTEXT_BUDGET;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Built-in orchestrator agent id. */
|
|
26
|
+
export const ORCHESTRATOR_AGENT_ID = 'system';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if a channel is a solo DM (only the agent is present).
|
|
30
|
+
*/
|
|
31
|
+
export function isDmSoloChannel(participants: string[], agentId: string): boolean {
|
|
32
|
+
return participants.length === 0 || (participants.length === 1 && participants[0] === agentId);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Simplified context builder for MVP.
|
|
37
|
+
*/
|
|
38
|
+
export async function buildContext(state: OpenBotState, storage?: Storage): Promise<string> {
|
|
39
|
+
const { channelId, threadId, channelDetails, agentId, threadDetails, agentDetails } = state;
|
|
40
|
+
const participants = channelDetails?.participants || [];
|
|
41
|
+
const isDm = isDmSoloChannel(participants, agentId);
|
|
42
|
+
|
|
43
|
+
const sections: string[] = [];
|
|
44
|
+
|
|
45
|
+
// 1. Environment
|
|
46
|
+
let env = '## ENVIRONMENT\n';
|
|
47
|
+
if (isDm) {
|
|
48
|
+
env += '- Mode: Direct Message (Solo)\n';
|
|
49
|
+
} else {
|
|
50
|
+
const channelName = channelDetails?.name || channelId;
|
|
51
|
+
env += `- Mode: Channel (#${channelName})\n`;
|
|
52
|
+
if (threadId) {
|
|
53
|
+
env += `- Thread: ${threadDetails?.name || threadId}\n`;
|
|
54
|
+
}
|
|
55
|
+
const peerIds = participants.filter((id: string) => id !== agentId);
|
|
56
|
+
if (peerIds.length > 0) {
|
|
57
|
+
env += `- Participants: ${peerIds.join(', ')}\n`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
sections.push(env);
|
|
61
|
+
|
|
62
|
+
// 2. Channel Spec
|
|
63
|
+
const spec = channelDetails?.spec?.trim();
|
|
64
|
+
if (spec) {
|
|
65
|
+
sections.push(`## CHANNEL SPECIFICATION\n${spec}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 3. Agent Instructions
|
|
69
|
+
if (agentDetails?.instructions) {
|
|
70
|
+
sections.push(`## AGENT: ${agentDetails?.name}\n${agentDetails.instructions}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 4. Memories
|
|
74
|
+
if (storage?.listMemories) {
|
|
75
|
+
try {
|
|
76
|
+
const scopes = ['global', `agent:${agentId}`];
|
|
77
|
+
if (channelId) scopes.push(`channel:${channelId}`);
|
|
78
|
+
const records = await storage.listMemories({ scopes, limit: 20 });
|
|
79
|
+
if (records.length > 0) {
|
|
80
|
+
const formatted = records
|
|
81
|
+
.map((r: any) => `- (${r.scope}) ${r.content}`)
|
|
82
|
+
.join('\n');
|
|
83
|
+
sections.push(`## MEMORIES\n${formatted}`);
|
|
84
|
+
}
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.warn('[context] Failed to fetch memories:', error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return sections.join('\n\n');
|
|
91
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { OpenBotEvent } from '../../app/types.js';
|
|
2
|
+
import { ToolResultPart, type ModelMessage } from 'ai';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Converts a raw event log into a valid chain of ModelMessages for the AI SDK.
|
|
6
|
+
*
|
|
7
|
+
* This is a basic implementation that maps events to messages and filters out
|
|
8
|
+
* events from sub-processes (delegation) to avoid duplication in history.
|
|
9
|
+
*/
|
|
10
|
+
export function eventsToModelMessages(events: OpenBotEvent[]): ModelMessage[] {
|
|
11
|
+
const messages: ModelMessage[] = [];
|
|
12
|
+
|
|
13
|
+
for (const event of events) {
|
|
14
|
+
// Skip events that belong to a sub-process (like delegation)
|
|
15
|
+
// so they don't pollute the main conversation history.
|
|
16
|
+
if (event.meta?.parentToolCallId) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
switch (event.type) {
|
|
21
|
+
case 'agent:output': {
|
|
22
|
+
const last = messages[messages.length - 1];
|
|
23
|
+
if (last && last.role === 'assistant' && typeof last.content === 'string') {
|
|
24
|
+
last.content += event.data.content;
|
|
25
|
+
} else {
|
|
26
|
+
messages.push({ role: 'assistant', content: event.data.content });
|
|
27
|
+
}
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
case 'agent:invoke': {
|
|
32
|
+
const invokeEvent = event as any;
|
|
33
|
+
if (invokeEvent.data?.content && invokeEvent.data?.role) {
|
|
34
|
+
messages.push({
|
|
35
|
+
role: invokeEvent.data.role,
|
|
36
|
+
content: invokeEvent.data.content
|
|
37
|
+
} as ModelMessage);
|
|
38
|
+
}
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
default:
|
|
43
|
+
// Handle tool calls (action:*)
|
|
44
|
+
if (event.type.startsWith('action:') && !event.type.endsWith(':result')) {
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
const toolName = event.type.slice(7);
|
|
48
|
+
const toolCallId = event.meta?.toolCallId;
|
|
49
|
+
if (!toolCallId) break;
|
|
50
|
+
|
|
51
|
+
const toolCall = {
|
|
52
|
+
type: 'tool-call' as const,
|
|
53
|
+
toolCallId,
|
|
54
|
+
toolName,
|
|
55
|
+
input: (event as any).data,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const last = messages[messages.length - 1];
|
|
59
|
+
|
|
60
|
+
if (last && last.role === 'assistant') {
|
|
61
|
+
if (typeof last.content === 'string') {
|
|
62
|
+
last.content = [
|
|
63
|
+
{ type: 'text', text: last.content },
|
|
64
|
+
toolCall,
|
|
65
|
+
];
|
|
66
|
+
} else if (Array.isArray(last.content)) {
|
|
67
|
+
(last.content as any[]).push(toolCall);
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
messages.push({
|
|
71
|
+
role: 'assistant',
|
|
72
|
+
content: [toolCall],
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Handle tool results (action:*:result)
|
|
77
|
+
else if (event.type.startsWith('action:') && event.type.endsWith(':result')) {
|
|
78
|
+
const toolName = event.type.slice(7, -7);
|
|
79
|
+
const toolCallId = event.meta?.toolCallId;
|
|
80
|
+
if (!toolCallId) break;
|
|
81
|
+
|
|
82
|
+
const toolResult: ToolResultPart = {
|
|
83
|
+
type: 'tool-result' as const,
|
|
84
|
+
toolCallId,
|
|
85
|
+
toolName,
|
|
86
|
+
output: {
|
|
87
|
+
type: 'text',
|
|
88
|
+
value: (event as any)?.data?.output || "No output", // ?.output is from delegation result
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const last = messages[messages.length - 1];
|
|
93
|
+
if (last && last.role === 'tool' && Array.isArray(last.content)) {
|
|
94
|
+
(last.content as any[]).push(toolResult);
|
|
95
|
+
} else {
|
|
96
|
+
messages.push({
|
|
97
|
+
role: 'tool',
|
|
98
|
+
content: [toolResult],
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return messages;
|
|
107
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Plugin } from '../../services/plugins/types.js';
|
|
2
|
+
import { openbotRuntime } from './runtime.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `openbot` — the standard, opinionated OpenBot agent runtime.
|
|
6
|
+
*
|
|
7
|
+
* This is the canonical execution loop for OpenBot agents. It handles
|
|
8
|
+
* `agent:invoke`, manages short-term memory, assembles context, and
|
|
9
|
+
* orchestrates tool calls.
|
|
10
|
+
*/
|
|
11
|
+
export const openbotPlugin: Plugin = {
|
|
12
|
+
id: 'openbot',
|
|
13
|
+
name: 'OpenBot Agent',
|
|
14
|
+
description:
|
|
15
|
+
'The standard, opinionated OpenBot agent runtime. Handles the core execution loop and tool orchestration.',
|
|
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: ({ config, storage, tools }) => {
|
|
28
|
+
|
|
29
|
+
return openbotRuntime({
|
|
30
|
+
model: config?.model as string,
|
|
31
|
+
storage,
|
|
32
|
+
toolDefinitions: tools,
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default openbotPlugin;
|
|
@@ -0,0 +1,384 @@
|
|
|
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
|
+
getContextBudgetForModel,
|
|
12
|
+
buildContext,
|
|
13
|
+
} from './context.js';
|
|
14
|
+
import { saveConfig } from '../../app/config.js';
|
|
15
|
+
import { API_KEY_SETUP_MESSAGE, OPENBOT_SYSTEM_PROMPT } from './system-prompt.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
|
+
storage?: Storage;
|
|
21
|
+
/** Tool definitions merged from all tool plugins attached to this agent. */
|
|
22
|
+
toolDefinitions?: Record<string, ToolDefinition>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveModel(modelString: string): LanguageModel {
|
|
26
|
+
const [provider, ...rest] = modelString.split('/');
|
|
27
|
+
const modelId = rest.join('/');
|
|
28
|
+
if (!modelId) {
|
|
29
|
+
throw new Error(`Invalid model string: "${modelString}". Expected "provider/model-id".`);
|
|
30
|
+
}
|
|
31
|
+
switch (provider) {
|
|
32
|
+
case 'openai':
|
|
33
|
+
return openai(modelId);
|
|
34
|
+
case 'anthropic':
|
|
35
|
+
return anthropic(modelId);
|
|
36
|
+
default:
|
|
37
|
+
throw new Error(`Unsupported AI provider: "${provider}"`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function buildSystemPrompt(
|
|
42
|
+
state: OpenBotState,
|
|
43
|
+
storage: Storage | undefined,
|
|
44
|
+
): Promise<string> {
|
|
45
|
+
const context = await buildContext(state, storage);
|
|
46
|
+
|
|
47
|
+
const instructions =
|
|
48
|
+
state.agentId === ORCHESTRATOR_AGENT_ID
|
|
49
|
+
? (state.agentDetails?.instructions?.trim() || OPENBOT_SYSTEM_PROMPT)
|
|
50
|
+
: OPENBOT_SYSTEM_PROMPT;
|
|
51
|
+
|
|
52
|
+
const sections = [instructions, '', context];
|
|
53
|
+
|
|
54
|
+
// Hardcoded naming hint logic
|
|
55
|
+
const threadState = state.threadDetails?.state as any;
|
|
56
|
+
if (!threadState?.isSmartNamed) {
|
|
57
|
+
sections.push(
|
|
58
|
+
'',
|
|
59
|
+
'## SYSTEM HINT',
|
|
60
|
+
'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.',
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return sections.join('\n');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Tracks tool-call IDs from one LLM turn until matching `:result` events arrive.
|
|
69
|
+
*
|
|
70
|
+
* Melony runs yielded `action:*` events depth-first, so parallel tool calls from
|
|
71
|
+
* a single `generateText` response execute one-by-one. We must wait for every ID
|
|
72
|
+
* in the batch before calling the LLM again — not after the first result.
|
|
73
|
+
*/
|
|
74
|
+
function createToolBatchTracker() {
|
|
75
|
+
let pending: Set<string> | null = null;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
startBatch(toolCallIds: string[]) {
|
|
79
|
+
pending = new Set(toolCallIds);
|
|
80
|
+
},
|
|
81
|
+
clear() {
|
|
82
|
+
pending = null;
|
|
83
|
+
},
|
|
84
|
+
/** Returns true when this result completes the batch (time to call the LLM again). */
|
|
85
|
+
recordResult(toolCallId: string): boolean {
|
|
86
|
+
if (!pending?.has(toolCallId)) return false;
|
|
87
|
+
pending.delete(toolCallId);
|
|
88
|
+
if (pending.size > 0) return false;
|
|
89
|
+
pending = null;
|
|
90
|
+
return true;
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* OpenBot agent runtime.
|
|
97
|
+
*
|
|
98
|
+
* - One `generateText` call per `runLLM` (tools have no `execute`; SDK stops at 1 step).
|
|
99
|
+
* - Tool calls become `action:*` events; plugins emit `:result` when done.
|
|
100
|
+
* - When a full batch of results is in, `runLLM` runs again with updated history.
|
|
101
|
+
*/
|
|
102
|
+
export const openbotRuntime =
|
|
103
|
+
(options: OpenBotRuntimeOptions): MelonyPlugin<OpenBotState, OpenBotEvent> =>
|
|
104
|
+
(builder) => {
|
|
105
|
+
const {
|
|
106
|
+
model: modelString = 'openai/gpt-4o-mini',
|
|
107
|
+
storage,
|
|
108
|
+
toolDefinitions = {},
|
|
109
|
+
} = options;
|
|
110
|
+
|
|
111
|
+
let currentModelString = modelString;
|
|
112
|
+
let model = resolveModel(currentModelString);
|
|
113
|
+
const toolBatch = createToolBatchTracker();
|
|
114
|
+
|
|
115
|
+
const runLLM = async function* (
|
|
116
|
+
context: RuntimeContext<OpenBotState, OpenBotEvent>,
|
|
117
|
+
threadId?: string,
|
|
118
|
+
trigger?: AgentInvokeEvent,
|
|
119
|
+
): AsyncGenerator<OpenBotEvent> {
|
|
120
|
+
if (!storage) return;
|
|
121
|
+
|
|
122
|
+
// Capture parent metadata for event enrichment
|
|
123
|
+
const triggerEvent = trigger || context.state.triggerEvent;
|
|
124
|
+
const parentAgentId = triggerEvent?.meta?.parentAgentId;
|
|
125
|
+
const parentToolCallId = triggerEvent?.meta?.parentToolCallId;
|
|
126
|
+
|
|
127
|
+
context.state.model = currentModelString;
|
|
128
|
+
|
|
129
|
+
const systemPrompt = await buildSystemPrompt(context.state, storage);
|
|
130
|
+
|
|
131
|
+
const events = await storage.getEvents({
|
|
132
|
+
channelId: context.state.channelId,
|
|
133
|
+
threadId: context.state.threadId,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const messages = eventsToModelMessages(events);
|
|
137
|
+
|
|
138
|
+
// console.log('systemPrompt:::::::\n', systemPrompt);
|
|
139
|
+
// console.log('messages:::::::\n', JSON.stringify(messages, null, 2));
|
|
140
|
+
// console.log('toolDefinitions:::::::\n', JSON.stringify(toolDefinitions, null, 2));
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
// Single LLM request — tool execution happens externally via action:* handlers.
|
|
144
|
+
const result = await generateText({
|
|
145
|
+
model,
|
|
146
|
+
system: systemPrompt,
|
|
147
|
+
messages,
|
|
148
|
+
tools: toolDefinitions as Record<string, { description: string; inputSchema: any }>,
|
|
149
|
+
stopWhen: ({ steps }) => steps.length === 1,
|
|
150
|
+
allowSystemInMessages: true,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const toolCalls = result.toolCalls ?? [];
|
|
154
|
+
|
|
155
|
+
// if (result.usage) {
|
|
156
|
+
// const usage = result.usage;
|
|
157
|
+
// yield {
|
|
158
|
+
// type: 'agent:usage',
|
|
159
|
+
// data: {
|
|
160
|
+
// usage: {
|
|
161
|
+
// promptTokens: usage.inputTokens,
|
|
162
|
+
// completionTokens: usage.outputTokens,
|
|
163
|
+
// totalTokens: usage.totalTokens,
|
|
164
|
+
// currentContextTokens: usage.inputTokens,
|
|
165
|
+
// contextBudget: getContextBudgetForModel(currentModelString),
|
|
166
|
+
// },
|
|
167
|
+
// model: currentModelString,
|
|
168
|
+
// },
|
|
169
|
+
// meta: {
|
|
170
|
+
// agentId: context.state.agentId,
|
|
171
|
+
// threadId,
|
|
172
|
+
// runId: context.state.runId,
|
|
173
|
+
// },
|
|
174
|
+
// } as OpenBotEvent;
|
|
175
|
+
// }
|
|
176
|
+
|
|
177
|
+
const outputMeta = {
|
|
178
|
+
agentId: context.state.agentId,
|
|
179
|
+
threadId,
|
|
180
|
+
parentAgentId,
|
|
181
|
+
parentToolCallId,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Text before actions so history/UI show the model's intent first.
|
|
185
|
+
if (result.text) {
|
|
186
|
+
yield {
|
|
187
|
+
type: 'agent:output',
|
|
188
|
+
data: { content: result.text },
|
|
189
|
+
meta: outputMeta,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (toolCalls.length > 0) {
|
|
194
|
+
// when multiple tool calls are made, Melony runtime handles them one by one, thats why we need to start a new batch
|
|
195
|
+
toolBatch.startBatch(toolCalls.map((tc) => tc.toolCallId));
|
|
196
|
+
|
|
197
|
+
for (const toolCall of toolCalls) {
|
|
198
|
+
yield {
|
|
199
|
+
type: `action:${toolCall.toolName}` as OpenBotEvent['type'],
|
|
200
|
+
data: toolCall.input,
|
|
201
|
+
meta: {
|
|
202
|
+
toolCallId: toolCall.toolCallId,
|
|
203
|
+
...outputMeta,
|
|
204
|
+
},
|
|
205
|
+
} as unknown as OpenBotEvent;
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
// clear the tool batch if there are no tool calls
|
|
209
|
+
toolBatch.clear();
|
|
210
|
+
}
|
|
211
|
+
} catch (error: unknown) {
|
|
212
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
213
|
+
const isApiKeyError =
|
|
214
|
+
errorMessage.includes('API key') ||
|
|
215
|
+
errorMessage.includes('401') ||
|
|
216
|
+
errorMessage.includes('Unauthorized') ||
|
|
217
|
+
errorMessage.includes('authentication');
|
|
218
|
+
|
|
219
|
+
if (isApiKeyError) {
|
|
220
|
+
const [currentProvider, ...rest] = currentModelString.split('/');
|
|
221
|
+
const currentModelId = rest.join('/');
|
|
222
|
+
|
|
223
|
+
yield {
|
|
224
|
+
type: 'client:ui:widget',
|
|
225
|
+
data: {
|
|
226
|
+
kind: 'form',
|
|
227
|
+
widgetId: `api_key_request_${Date.now()}`,
|
|
228
|
+
title: `AI Provider API Key Required`,
|
|
229
|
+
description: API_KEY_SETUP_MESSAGE,
|
|
230
|
+
fields: [
|
|
231
|
+
{
|
|
232
|
+
id: 'provider',
|
|
233
|
+
label: 'Provider',
|
|
234
|
+
type: 'select',
|
|
235
|
+
required: true,
|
|
236
|
+
options: [
|
|
237
|
+
{ label: 'OpenAI', value: 'openai' },
|
|
238
|
+
{ label: 'Anthropic', value: 'anthropic' },
|
|
239
|
+
],
|
|
240
|
+
defaultValue: currentProvider === 'anthropic' ? 'anthropic' : 'openai',
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
id: 'model',
|
|
244
|
+
label: 'Model',
|
|
245
|
+
type: 'text',
|
|
246
|
+
description:
|
|
247
|
+
'Model name without the provider prefix (e.g. `gpt-4o-mini` or `claude-3-5-sonnet-20240620`).',
|
|
248
|
+
placeholder: 'gpt-4o-mini',
|
|
249
|
+
required: true,
|
|
250
|
+
defaultValue: currentModelId,
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
id: 'apiKey',
|
|
254
|
+
label: 'API Key',
|
|
255
|
+
type: 'text',
|
|
256
|
+
placeholder: `sk-...`,
|
|
257
|
+
required: true,
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
submitLabel: 'Save & Continue',
|
|
261
|
+
metadata: {
|
|
262
|
+
type: 'api_key_request',
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
meta: { agentId: context.state.agentId, threadId },
|
|
266
|
+
} as OpenBotEvent;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
builder.on('agent:invoke', async function* (event, context) {
|
|
275
|
+
const routedTo = (event as { data?: { agentId?: string } }).data?.agentId;
|
|
276
|
+
if (typeof routedTo === 'string' && routedTo && routedTo !== context.state.agentId) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// clear the tool batch if the agent is invoked
|
|
281
|
+
// this is to prevent the tool batch from being used for a new agent invocation
|
|
282
|
+
toolBatch.clear();
|
|
283
|
+
|
|
284
|
+
const threadId = event.meta?.threadId || context.state.threadId;
|
|
285
|
+
yield* runLLM(context, threadId, event as AgentInvokeEvent);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// this is to handle the tool results from the tool calls
|
|
289
|
+
// because Melony runtime handles them one by one, thats why we need to record the result
|
|
290
|
+
builder.on('*', async function* (event, context) {
|
|
291
|
+
if (!event.type.endsWith(':result')) return;
|
|
292
|
+
if (event.meta?.agentId !== context.state.agentId) return;
|
|
293
|
+
|
|
294
|
+
const toolCallId = event.meta?.toolCallId;
|
|
295
|
+
// record the result of the tool call
|
|
296
|
+
if (!toolCallId || !toolBatch.recordResult(toolCallId)) return;
|
|
297
|
+
|
|
298
|
+
const threadId = event.meta?.threadId || context.state.threadId;
|
|
299
|
+
yield* runLLM(context, threadId);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
builder.on('client:ui:widget:response', async function* (event, context) {
|
|
303
|
+
const { metadata, values } = event.data;
|
|
304
|
+
if (metadata?.type !== 'api_key_request') return;
|
|
305
|
+
if (!values?.apiKey || !values?.provider || !values?.model) return;
|
|
306
|
+
|
|
307
|
+
const provider = String(values.provider);
|
|
308
|
+
const modelId = String(values.model).trim();
|
|
309
|
+
const apiKey = String(values.apiKey);
|
|
310
|
+
|
|
311
|
+
if (provider !== 'openai' && provider !== 'anthropic') {
|
|
312
|
+
yield {
|
|
313
|
+
type: 'agent:output',
|
|
314
|
+
data: { content: `Unsupported provider: ${provider}` },
|
|
315
|
+
meta: { agentId: context.state.agentId },
|
|
316
|
+
};
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const envVar = provider === 'openai' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY';
|
|
321
|
+
const newModelString = `${provider}/${modelId}`;
|
|
322
|
+
|
|
323
|
+
if (!storage) return;
|
|
324
|
+
try {
|
|
325
|
+
await storage.createVariable({ key: envVar, value: apiKey, secret: true });
|
|
326
|
+
process.env[envVar] = apiKey;
|
|
327
|
+
|
|
328
|
+
currentModelString = newModelString;
|
|
329
|
+
model = resolveModel(currentModelString);
|
|
330
|
+
try {
|
|
331
|
+
saveConfig({ model: currentModelString });
|
|
332
|
+
|
|
333
|
+
// Also update the agent's AGENT.md if it has an openbot plugin config
|
|
334
|
+
const details = await storage.getAgentDetails({ agentId: context.state.agentId });
|
|
335
|
+
const updatedPlugins = details.pluginRefs.map((ref) => {
|
|
336
|
+
if (ref.id === 'openbot') {
|
|
337
|
+
return {
|
|
338
|
+
...ref,
|
|
339
|
+
config: { ...ref.config, model: currentModelString },
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
return ref;
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
await storage.updateAgent({
|
|
346
|
+
agentId: context.state.agentId,
|
|
347
|
+
plugins: updatedPlugins,
|
|
348
|
+
});
|
|
349
|
+
} catch {
|
|
350
|
+
// best-effort: config persistence failure shouldn't block the conversation
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
yield {
|
|
354
|
+
type: 'agent:output',
|
|
355
|
+
data: {
|
|
356
|
+
content: `Saved ${provider} API key and set model to \`${newModelString}\`.`,
|
|
357
|
+
},
|
|
358
|
+
meta: { agentId: context.state.agentId },
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
yield {
|
|
362
|
+
type: 'client:ui:widget',
|
|
363
|
+
data: {
|
|
364
|
+
widgetId: event.data.widgetId,
|
|
365
|
+
kind: 'message',
|
|
366
|
+
title: 'API Key Saved',
|
|
367
|
+
body: `Successfully saved ${provider} API key and selected model \`${newModelString}\`. You can now continue your conversation.`,
|
|
368
|
+
state: 'submitted',
|
|
369
|
+
actions: [{ id: 'ok', label: 'Got it', variant: 'primary' }],
|
|
370
|
+
},
|
|
371
|
+
meta: { agentId: context.state.agentId },
|
|
372
|
+
};
|
|
373
|
+
} catch (error) {
|
|
374
|
+
yield {
|
|
375
|
+
type: 'agent:output',
|
|
376
|
+
data: {
|
|
377
|
+
content: `Failed to save API key: ${error instanceof Error ? error.message : 'Unknown error'
|
|
378
|
+
}`,
|
|
379
|
+
},
|
|
380
|
+
meta: { agentId: context.state.agentId },
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const OPENBOT_SYSTEM_PROMPT = [
|
|
2
|
+
'You are a helpful AI assistant for your human. Your job is to help the user with their questions and tasks.',
|
|
3
|
+
].join('\n');
|
|
4
|
+
|
|
5
|
+
/** Shown in the API key setup form when no provider credentials are configured. */
|
|
6
|
+
export const API_KEY_SETUP_MESSAGE =
|
|
7
|
+
'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.';
|