openbot 0.2.14 → 0.3.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/dist/agents/openbot/index.js +76 -0
- package/dist/agents/openbot/middleware/approval.js +132 -0
- package/dist/agents/openbot/runtime.js +289 -0
- package/dist/agents/openbot/system-prompt.js +32 -0
- package/dist/agents/openbot/tools/delegation.js +78 -0
- package/dist/agents/openbot/tools/mcp.js +99 -0
- package/dist/agents/openbot/tools/shell.js +91 -0
- package/dist/agents/openbot/tools/storage.js +75 -0
- package/dist/agents/openbot/tools/ui.js +176 -0
- package/dist/agents/system.js +20 -93
- package/dist/app/cli.js +0 -0
- package/dist/app/config.js +4 -1
- package/dist/app/server.js +15 -8
- package/dist/bus/agent-package.js +1 -0
- package/dist/bus/plugin.js +1 -0
- package/dist/bus/services.js +600 -0
- package/dist/bus/types.js +1 -0
- package/dist/harness/context.js +131 -0
- package/dist/harness/event-normalizer.js +59 -0
- package/dist/harness/orchestrator.js +27 -227
- package/dist/harness/process.js +25 -3
- package/dist/harness/queue-processor.js +227 -0
- package/dist/harness/runtime-factory.js +103 -0
- package/dist/plugins/ai-sdk/index.js +37 -0
- package/dist/plugins/ai-sdk/runtime.js +330 -0
- package/dist/plugins/ai-sdk/system-prompt.js +3 -0
- package/dist/plugins/ai-sdk.js +277 -87
- package/dist/plugins/approval/index.js +159 -0
- package/dist/plugins/approval.js +163 -0
- package/dist/plugins/delegation/index.js +79 -0
- package/dist/plugins/delegation.js +67 -11
- package/dist/plugins/mcp/index.js +108 -0
- package/dist/plugins/shell/index.js +99 -0
- package/dist/plugins/shell.js +123 -0
- package/dist/plugins/storage-tools/index.js +85 -0
- package/dist/plugins/storage.js +240 -5
- package/dist/plugins/ui/index.js +184 -0
- package/dist/plugins/ui.js +185 -21
- package/dist/registry/agents.js +138 -0
- package/dist/registry/plugins.js +91 -50
- package/dist/services/agent-packages.js +103 -0
- package/dist/services/plugins.js +98 -0
- package/dist/services/storage.js +360 -94
- package/docs/agents.md +39 -66
- package/docs/architecture.md +1 -1
- package/docs/plugins.md +70 -58
- package/docs/templates/AGENT.example.md +57 -0
- package/package.json +8 -7
- package/src/app/cli.ts +1 -1
- package/src/app/config.ts +14 -4
- package/src/app/server.ts +23 -10
- package/src/app/types.ts +385 -16
- package/src/assets/icon.svg +4 -1
- package/src/bus/plugin.ts +67 -0
- package/src/bus/services.ts +666 -0
- package/src/bus/types.ts +147 -0
- package/src/harness/context.ts +160 -0
- package/src/harness/event-normalizer.ts +82 -0
- package/src/harness/orchestrator.ts +35 -273
- package/src/harness/process.ts +28 -4
- package/src/harness/queue-processor.ts +309 -0
- package/src/harness/runtime-factory.ts +125 -0
- package/src/plugins/ai-sdk/index.ts +44 -0
- package/src/plugins/ai-sdk/runtime.ts +410 -0
- package/src/plugins/ai-sdk/system-prompt.ts +4 -0
- package/src/plugins/approval/index.ts +228 -0
- package/src/plugins/delegation/index.ts +94 -0
- package/src/plugins/mcp/index.ts +128 -0
- package/src/plugins/shell/index.ts +123 -0
- package/src/plugins/storage-tools/index.ts +101 -0
- package/src/plugins/ui/index.ts +227 -0
- package/src/registry/plugins.ts +106 -55
- package/src/services/plugins.ts +133 -0
- package/src/services/storage.ts +465 -137
- package/src/agents/system.ts +0 -112
- package/src/plugins/ai-sdk.ts +0 -197
- package/src/plugins/delegation.ts +0 -60
- package/src/plugins/mcp.ts +0 -154
- package/src/plugins/storage.ts +0 -725
- package/src/plugins/ui.ts +0 -57
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { melony, MelonyPlugin, Runtime } from 'melony';
|
|
2
|
+
import { OpenBotEvent, OpenBotState } from '../app/types.js';
|
|
3
|
+
import type { Plugin, PluginContext, ToolDefinition } from '../bus/plugin.js';
|
|
4
|
+
import { resolvePlugin } from '../registry/plugins.js';
|
|
5
|
+
import { storageService } from '../services/storage.js';
|
|
6
|
+
import { busServicesPlugin } from '../bus/services.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Enhances the agent's instructions with a list of other available agents the
|
|
10
|
+
* orchestrator can hand off / delegate to. Agents that include the
|
|
11
|
+
* `delegation` plugin will surface peers; agents without it can ignore this.
|
|
12
|
+
*/
|
|
13
|
+
export async function enhanceInstructions(state: OpenBotState) {
|
|
14
|
+
const { agentId, agentDetails } = state;
|
|
15
|
+
if (!agentDetails) return;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const agents = await storageService.getAgents();
|
|
19
|
+
const otherAgents = agents.filter((a) => a.id !== agentId);
|
|
20
|
+
if (otherAgents.length === 0) return;
|
|
21
|
+
|
|
22
|
+
const agentsList = otherAgents
|
|
23
|
+
.map((a) => `- **${a.id}**${a.description ? `: ${a.description}` : ''}`)
|
|
24
|
+
.join('\n');
|
|
25
|
+
|
|
26
|
+
const header = '### Available Agents for Handoff/Delegation:';
|
|
27
|
+
if (!agentDetails.instructions.includes(header)) {
|
|
28
|
+
agentDetails.instructions +=
|
|
29
|
+
`\n\n${header}\n${agentsList}\n\n` +
|
|
30
|
+
'Use `handoff` to transfer control to another agent. ' +
|
|
31
|
+
'Use `delegate` when you need a sub-result from another agent and want to continue after it returns.';
|
|
32
|
+
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.warn('[agent] Failed to enhance instructions', error);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const composeMelonyPlugin = (
|
|
39
|
+
...plugins: MelonyPlugin<OpenBotState, OpenBotEvent>[]
|
|
40
|
+
): MelonyPlugin<OpenBotState, OpenBotEvent> => {
|
|
41
|
+
return (builder) => {
|
|
42
|
+
for (const plugin of plugins) {
|
|
43
|
+
plugin(builder);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build the Melony runtime that drives a single agent run on the OpenBot bus.
|
|
50
|
+
*
|
|
51
|
+
* The runtime always wires:
|
|
52
|
+
* 1. `busServicesPlugin` — bus-level services (storage, channels, threads,
|
|
53
|
+
* plugin install/marketplace) shared by every agent.
|
|
54
|
+
* 2. Every Plugin referenced by the agent's `plugins[]` frontmatter, in
|
|
55
|
+
* order. Tool definitions from each plugin are merged into a single map
|
|
56
|
+
* and passed to every plugin via `PluginContext.tools`. Runtime plugins
|
|
57
|
+
* (those that handle `agent:invoke`) consume the merged map; tool plugins
|
|
58
|
+
* ignore it.
|
|
59
|
+
*
|
|
60
|
+
* Tool name collisions across plugins log a warning; the first plugin wins.
|
|
61
|
+
*/
|
|
62
|
+
export async function createAgentRuntime(
|
|
63
|
+
state: OpenBotState,
|
|
64
|
+
): Promise<Runtime<OpenBotState, OpenBotEvent>> {
|
|
65
|
+
await enhanceInstructions(state);
|
|
66
|
+
|
|
67
|
+
const runtime = melony<OpenBotState, OpenBotEvent>({
|
|
68
|
+
initialState: state,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
runtime.use(busServicesPlugin({ storage: storageService }));
|
|
72
|
+
|
|
73
|
+
const refs = state.agentDetails?.pluginRefs || [];
|
|
74
|
+
if (refs.length === 0) {
|
|
75
|
+
console.warn(
|
|
76
|
+
`[agent] Agent "${state.agentId}" has no plugins; only bus services will be active.`,
|
|
77
|
+
);
|
|
78
|
+
return runtime.build();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Resolve all plugins first so we can merge tool definitions before factory calls.
|
|
82
|
+
const resolved: Array<{ ref: { id: string; config?: Record<string, unknown> }; plugin: Plugin }> = [];
|
|
83
|
+
for (const ref of refs) {
|
|
84
|
+
const plugin = await resolvePlugin(ref.id);
|
|
85
|
+
if (!plugin) {
|
|
86
|
+
console.warn(
|
|
87
|
+
`[agent] Plugin "${ref.id}" for agent "${state.agentId}" could not be resolved.`,
|
|
88
|
+
);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
resolved.push({ ref, plugin });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Merge tool definitions; first plugin wins on collision.
|
|
95
|
+
const tools: Record<string, ToolDefinition> = {};
|
|
96
|
+
for (const { plugin } of resolved) {
|
|
97
|
+
if (!plugin.toolDefinitions) continue;
|
|
98
|
+
for (const [name, def] of Object.entries(plugin.toolDefinitions)) {
|
|
99
|
+
if (tools[name]) {
|
|
100
|
+
console.warn(
|
|
101
|
+
`[agent] Tool name collision for "${name}" while loading plugin "${plugin.id}"; keeping first registration.`,
|
|
102
|
+
);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
tools[name] = def;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Compose all plugin factories with the shared context.
|
|
110
|
+
const pluginPlugins: MelonyPlugin<OpenBotState, OpenBotEvent>[] = [];
|
|
111
|
+
for (const { ref, plugin } of resolved) {
|
|
112
|
+
const context: PluginContext = {
|
|
113
|
+
agentId: state.agentId,
|
|
114
|
+
agentDetails: state.agentDetails!,
|
|
115
|
+
config: ref.config || {},
|
|
116
|
+
storage: storageService,
|
|
117
|
+
tools,
|
|
118
|
+
};
|
|
119
|
+
pluginPlugins.push(plugin.factory(context));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
runtime.use(composeMelonyPlugin(...pluginPlugins));
|
|
123
|
+
|
|
124
|
+
return runtime.build();
|
|
125
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Plugin } from '../../bus/plugin.js';
|
|
2
|
+
import { aiSdkRuntime } from './runtime.js';
|
|
3
|
+
import { AI_SDK_SYSTEM_PROMPT } from './system-prompt.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `ai-sdk` — generic LLM runtime plugin built on the Vercel AI SDK.
|
|
7
|
+
*
|
|
8
|
+
* Owns `agent:invoke` and consumes the merged `tools` map provided by the
|
|
9
|
+
* agent loader (collected from every tool plugin attached to the same agent).
|
|
10
|
+
* Pair with tool plugins like `shell`, `mcp`, `delegation`, etc.
|
|
11
|
+
*/
|
|
12
|
+
export const aiSdkPlugin: Plugin = {
|
|
13
|
+
id: 'ai-sdk',
|
|
14
|
+
name: 'AI SDK Runtime',
|
|
15
|
+
description:
|
|
16
|
+
'Generic LLM runtime built on the Vercel AI SDK. Consumes tools contributed by other plugins.',
|
|
17
|
+
defaultInstructions: AI_SDK_SYSTEM_PROMPT,
|
|
18
|
+
configSchema: {
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
model: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
description:
|
|
24
|
+
'Provider model string, e.g. openai/gpt-4o-mini, anthropic/claude-3-5-sonnet-20240620',
|
|
25
|
+
default: 'openai/gpt-4o-mini',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
factory: ({ agentDetails, config, storage, tools }) => {
|
|
30
|
+
const model =
|
|
31
|
+
typeof config.model === 'string' && config.model
|
|
32
|
+
? config.model
|
|
33
|
+
: 'openai/gpt-4o-mini';
|
|
34
|
+
|
|
35
|
+
return aiSdkRuntime({
|
|
36
|
+
model,
|
|
37
|
+
system: agentDetails.instructions || AI_SDK_SYSTEM_PROMPT,
|
|
38
|
+
storage,
|
|
39
|
+
toolDefinitions: tools,
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default aiSdkPlugin;
|
|
@@ -0,0 +1,410 @@
|
|
|
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
|
+
/** Static or dynamic system prompt. */
|
|
15
|
+
system?: string | ((context: RuntimeContext) => string | Promise<string>);
|
|
16
|
+
storage?: Storage;
|
|
17
|
+
contextEngine?: {
|
|
18
|
+
buildContext: (state: OpenBotState, storage?: Storage) => Promise<string>;
|
|
19
|
+
};
|
|
20
|
+
/** Tool definitions merged from all tool plugins attached to this agent. */
|
|
21
|
+
toolDefinitions?: Record<string, ToolDefinition>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resolveModel(modelString: string): LanguageModel {
|
|
25
|
+
const [provider, ...rest] = modelString.split('/');
|
|
26
|
+
const modelId = rest.join('/');
|
|
27
|
+
if (!modelId) {
|
|
28
|
+
throw new Error(`Invalid model string: "${modelString}". Expected "provider/model-id".`);
|
|
29
|
+
}
|
|
30
|
+
switch (provider) {
|
|
31
|
+
case 'openai':
|
|
32
|
+
return openai(modelId);
|
|
33
|
+
case 'anthropic':
|
|
34
|
+
return anthropic(modelId);
|
|
35
|
+
default:
|
|
36
|
+
throw new Error(`Unsupported AI provider: "${provider}"`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const asRecord = (value: unknown): Record<string, unknown> =>
|
|
41
|
+
value && typeof value === 'object' && !Array.isArray(value)
|
|
42
|
+
? (value as Record<string, unknown>)
|
|
43
|
+
: {};
|
|
44
|
+
|
|
45
|
+
const readPersistedShortTermMessages = (state: OpenBotState): ShortTermMessage[] => {
|
|
46
|
+
const source = state.threadDetails?.state ?? state.channelDetails?.state;
|
|
47
|
+
const record = asRecord(source);
|
|
48
|
+
const raw = record.shortTermMessages;
|
|
49
|
+
return Array.isArray(raw) ? (raw as ShortTermMessage[]) : [];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const persistShortTermMessages = async (
|
|
53
|
+
state: OpenBotState,
|
|
54
|
+
storage: Storage | undefined,
|
|
55
|
+
): Promise<void> => {
|
|
56
|
+
if (!storage) return;
|
|
57
|
+
const shortTermMessages = state.shortTermMessages ?? [];
|
|
58
|
+
if (state.threadId) {
|
|
59
|
+
await storage.patchThreadState({
|
|
60
|
+
channelId: state.channelId,
|
|
61
|
+
threadId: state.threadId,
|
|
62
|
+
state: { shortTermMessages },
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
await storage.patchChannelState({
|
|
67
|
+
channelId: state.channelId,
|
|
68
|
+
state: { shortTermMessages },
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
async function buildSystemPrompt(
|
|
73
|
+
state: OpenBotState,
|
|
74
|
+
system?: string | ((context: RuntimeContext) => string | Promise<string>),
|
|
75
|
+
context?: RuntimeContext,
|
|
76
|
+
storage?: Storage,
|
|
77
|
+
contextEngine?: {
|
|
78
|
+
buildContext: (state: OpenBotState, storage?: Storage) => Promise<string>;
|
|
79
|
+
},
|
|
80
|
+
): Promise<string> {
|
|
81
|
+
const sections: string[] = [];
|
|
82
|
+
if (system && typeof system === 'string') sections.push(system);
|
|
83
|
+
if (system && typeof system === 'function' && context) sections.push(await system(context));
|
|
84
|
+
if (contextEngine) sections.push(await contextEngine.buildContext(state, storage));
|
|
85
|
+
return sections.join('\n\n');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Generic ai-sdk runtime plugin.
|
|
90
|
+
*
|
|
91
|
+
* Owns `agent:invoke`, runs the LLM, emits tool-call events, and stitches tool
|
|
92
|
+
* results back into the conversation. Tools are supplied externally by the
|
|
93
|
+
* loader (merged from every tool plugin attached to the same agent).
|
|
94
|
+
*/
|
|
95
|
+
export const aiSdkRuntime =
|
|
96
|
+
(options: AiSdkRuntimeOptions): MelonyPlugin<OpenBotState, OpenBotEvent> =>
|
|
97
|
+
(builder) => {
|
|
98
|
+
const {
|
|
99
|
+
model: modelString = 'openai/gpt-4o-mini',
|
|
100
|
+
system,
|
|
101
|
+
storage,
|
|
102
|
+
contextEngine = createDefaultContextEngine(),
|
|
103
|
+
toolDefinitions = {},
|
|
104
|
+
} = options;
|
|
105
|
+
|
|
106
|
+
let currentModelString = modelString;
|
|
107
|
+
let model = resolveModel(currentModelString);
|
|
108
|
+
|
|
109
|
+
const ensureShortTermMessages = (state: OpenBotState) => {
|
|
110
|
+
if (!state.shortTermMessages || state.shortTermMessages.length === 0) {
|
|
111
|
+
state.shortTermMessages = readPersistedShortTermMessages(state);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const mapToCoreMessages = (messages: ShortTermMessage[]): ModelMessage[] => {
|
|
116
|
+
return messages.map((m): ModelMessage => {
|
|
117
|
+
if (m.role === 'assistant' && m.toolCalls) {
|
|
118
|
+
return {
|
|
119
|
+
role: 'assistant',
|
|
120
|
+
content: [
|
|
121
|
+
{ type: 'text', text: m.content || '' },
|
|
122
|
+
...m.toolCalls.map((tc) => ({
|
|
123
|
+
type: 'tool-call' as const,
|
|
124
|
+
toolCallId: tc.id,
|
|
125
|
+
toolName: tc.function.name,
|
|
126
|
+
input: JSON.parse(tc.function.arguments),
|
|
127
|
+
})),
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (m.role === 'assistant') {
|
|
132
|
+
return { role: 'assistant', content: m.content || '' };
|
|
133
|
+
}
|
|
134
|
+
if (m.role === 'tool') {
|
|
135
|
+
return {
|
|
136
|
+
role: 'tool',
|
|
137
|
+
content: [
|
|
138
|
+
{
|
|
139
|
+
type: 'tool-result',
|
|
140
|
+
toolCallId: m.toolCallId,
|
|
141
|
+
toolName: m.toolName,
|
|
142
|
+
output: { type: 'text', value: JSON.stringify(m.content) },
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return m;
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const runLLM = async function* (
|
|
152
|
+
context: RuntimeContext<OpenBotState, OpenBotEvent>,
|
|
153
|
+
threadId?: string,
|
|
154
|
+
): AsyncGenerator<OpenBotEvent> {
|
|
155
|
+
ensureShortTermMessages(context.state);
|
|
156
|
+
const systemPrompt = await buildSystemPrompt(
|
|
157
|
+
context.state,
|
|
158
|
+
system,
|
|
159
|
+
context,
|
|
160
|
+
storage,
|
|
161
|
+
contextEngine,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const coreMessages = mapToCoreMessages(context.state.shortTermMessages || []);
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const result = await generateText({
|
|
168
|
+
model,
|
|
169
|
+
system: systemPrompt,
|
|
170
|
+
messages: coreMessages,
|
|
171
|
+
tools: toolDefinitions as Record<string, { description: string; inputSchema: any }>,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const toolCalls = result.toolCalls ?? [];
|
|
175
|
+
|
|
176
|
+
if (toolCalls.length > 0) {
|
|
177
|
+
context.state.shortTermMessages = [
|
|
178
|
+
...(context.state.shortTermMessages ?? []),
|
|
179
|
+
{
|
|
180
|
+
role: 'assistant',
|
|
181
|
+
content: result.text || '',
|
|
182
|
+
toolCalls: toolCalls.map((tc) => ({
|
|
183
|
+
id: tc.toolCallId,
|
|
184
|
+
type: 'function',
|
|
185
|
+
function: {
|
|
186
|
+
name: tc.toolName,
|
|
187
|
+
arguments: JSON.stringify(tc.input),
|
|
188
|
+
},
|
|
189
|
+
})),
|
|
190
|
+
},
|
|
191
|
+
];
|
|
192
|
+
await persistShortTermMessages(context.state, storage);
|
|
193
|
+
|
|
194
|
+
for (const toolCall of toolCalls) {
|
|
195
|
+
yield {
|
|
196
|
+
type: `action:${toolCall.toolName}` as OpenBotEvent['type'],
|
|
197
|
+
data: toolCall.input,
|
|
198
|
+
meta: {
|
|
199
|
+
toolCallId: toolCall.toolCallId,
|
|
200
|
+
agentId: context.state.agentId,
|
|
201
|
+
threadId,
|
|
202
|
+
},
|
|
203
|
+
} as unknown as OpenBotEvent;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (result.text) {
|
|
208
|
+
if (toolCalls.length === 0) {
|
|
209
|
+
context.state.shortTermMessages = [
|
|
210
|
+
...(context.state.shortTermMessages ?? []),
|
|
211
|
+
{ role: 'assistant', content: result.text },
|
|
212
|
+
];
|
|
213
|
+
await persistShortTermMessages(context.state, storage);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
yield {
|
|
217
|
+
type: 'agent:output',
|
|
218
|
+
data: { content: result.text },
|
|
219
|
+
meta: { agentId: context.state.agentId, threadId },
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
} catch (error: unknown) {
|
|
223
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
224
|
+
const isApiKeyError =
|
|
225
|
+
errorMessage.includes('API key') ||
|
|
226
|
+
errorMessage.includes('401') ||
|
|
227
|
+
errorMessage.includes('Unauthorized') ||
|
|
228
|
+
errorMessage.includes('authentication');
|
|
229
|
+
|
|
230
|
+
if (isApiKeyError) {
|
|
231
|
+
const [currentProvider, ...rest] = currentModelString.split('/');
|
|
232
|
+
const currentModelId = rest.join('/');
|
|
233
|
+
yield {
|
|
234
|
+
type: 'client:ui:widget',
|
|
235
|
+
data: {
|
|
236
|
+
kind: 'form',
|
|
237
|
+
widgetId: `api_key_request_${Date.now()}`,
|
|
238
|
+
title: `AI Provider API Key Required`,
|
|
239
|
+
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.`,
|
|
240
|
+
fields: [
|
|
241
|
+
{
|
|
242
|
+
id: 'provider',
|
|
243
|
+
label: 'Provider',
|
|
244
|
+
type: 'select',
|
|
245
|
+
required: true,
|
|
246
|
+
options: [
|
|
247
|
+
{ label: 'OpenAI', value: 'openai' },
|
|
248
|
+
{ label: 'Anthropic', value: 'anthropic' },
|
|
249
|
+
],
|
|
250
|
+
defaultValue: currentProvider === 'anthropic' ? 'anthropic' : 'openai',
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
id: 'model',
|
|
254
|
+
label: 'Model',
|
|
255
|
+
type: 'text',
|
|
256
|
+
description:
|
|
257
|
+
'Model name without the provider prefix (e.g. `gpt-4o-mini` or `claude-3-5-sonnet-20240620`).',
|
|
258
|
+
placeholder: 'gpt-4o-mini',
|
|
259
|
+
required: true,
|
|
260
|
+
defaultValue: currentModelId,
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
id: 'apiKey',
|
|
264
|
+
label: 'API Key',
|
|
265
|
+
type: 'text',
|
|
266
|
+
placeholder: `sk-...`,
|
|
267
|
+
required: true,
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
submitLabel: 'Save & Continue',
|
|
271
|
+
metadata: {
|
|
272
|
+
type: 'api_key_request',
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
meta: { agentId: context.state.agentId, threadId },
|
|
276
|
+
} as OpenBotEvent;
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
builder.on('agent:invoke', async function* (event, context) {
|
|
285
|
+
const routedTo = (event as { data?: { agentId?: string } }).data?.agentId;
|
|
286
|
+
if (typeof routedTo === 'string' && routedTo && routedTo !== context.state.agentId) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const threadId = event.meta?.threadId || context.state.threadId;
|
|
291
|
+
|
|
292
|
+
ensureShortTermMessages(context.state);
|
|
293
|
+
context.state.shortTermMessages = [
|
|
294
|
+
...(context.state.shortTermMessages ?? []),
|
|
295
|
+
{
|
|
296
|
+
role: event.data?.role || 'user',
|
|
297
|
+
content: event?.data?.content || '',
|
|
298
|
+
},
|
|
299
|
+
];
|
|
300
|
+
await persistShortTermMessages(context.state, storage);
|
|
301
|
+
|
|
302
|
+
yield* runLLM(context, threadId);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
builder.on('*', async function* (event, context) {
|
|
306
|
+
if (!event.type.endsWith(':result')) return;
|
|
307
|
+
if (event.meta?.agentId !== context.state.agentId) return;
|
|
308
|
+
const toolCallId = event.meta?.toolCallId;
|
|
309
|
+
if (!toolCallId) return;
|
|
310
|
+
ensureShortTermMessages(context.state);
|
|
311
|
+
|
|
312
|
+
const toolName = event.type.replace(/^action:/, '').replace(/:result$/, '');
|
|
313
|
+
const resultData = (event as { data?: unknown }).data;
|
|
314
|
+
const content = typeof resultData === 'string' ? resultData : JSON.stringify(resultData);
|
|
315
|
+
|
|
316
|
+
context.state.shortTermMessages = [
|
|
317
|
+
...(context.state.shortTermMessages ?? []),
|
|
318
|
+
{ role: 'tool', content, toolCallId, toolName },
|
|
319
|
+
];
|
|
320
|
+
await persistShortTermMessages(context.state, storage);
|
|
321
|
+
|
|
322
|
+
const lastAssistant = [...(context.state.shortTermMessages ?? [])]
|
|
323
|
+
.reverse()
|
|
324
|
+
.find(
|
|
325
|
+
(m): m is Extract<ShortTermMessage, { role: 'assistant' }> =>
|
|
326
|
+
m.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length > 0,
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
if (lastAssistant && lastAssistant.toolCalls) {
|
|
330
|
+
const allFulfilled = lastAssistant.toolCalls.every((tc) =>
|
|
331
|
+
context.state.shortTermMessages?.some(
|
|
332
|
+
(m) => m.role === 'tool' && m.toolCallId === tc.id,
|
|
333
|
+
),
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
if (allFulfilled) {
|
|
337
|
+
if (toolName === 'handoff') return;
|
|
338
|
+
const threadId = event.meta?.threadId || context.state.threadId;
|
|
339
|
+
yield* runLLM(context, threadId);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
builder.on('client:ui:widget:response', async function* (event, context) {
|
|
345
|
+
const { metadata, values } = event.data;
|
|
346
|
+
if (metadata?.type !== 'api_key_request') return;
|
|
347
|
+
if (!values?.apiKey || !values?.provider || !values?.model) return;
|
|
348
|
+
|
|
349
|
+
const provider = String(values.provider);
|
|
350
|
+
const modelId = String(values.model).trim();
|
|
351
|
+
const apiKey = String(values.apiKey);
|
|
352
|
+
|
|
353
|
+
if (provider !== 'openai' && provider !== 'anthropic') {
|
|
354
|
+
yield {
|
|
355
|
+
type: 'agent:output',
|
|
356
|
+
data: { content: `Unsupported provider: ${provider}` },
|
|
357
|
+
meta: { agentId: context.state.agentId },
|
|
358
|
+
};
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const envVar = provider === 'openai' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY';
|
|
363
|
+
const newModelString = `${provider}/${modelId}`;
|
|
364
|
+
|
|
365
|
+
if (!storage) return;
|
|
366
|
+
try {
|
|
367
|
+
await storage.createVariable({ key: envVar, value: apiKey, secret: true });
|
|
368
|
+
process.env[envVar] = apiKey;
|
|
369
|
+
|
|
370
|
+
currentModelString = newModelString;
|
|
371
|
+
model = resolveModel(currentModelString);
|
|
372
|
+
try {
|
|
373
|
+
saveConfig({ model: currentModelString });
|
|
374
|
+
} catch {
|
|
375
|
+
// best-effort: config persistence failure shouldn't block the conversation
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
yield {
|
|
379
|
+
type: 'agent:output',
|
|
380
|
+
data: {
|
|
381
|
+
content: `Saved ${provider} API key and set model to \`${newModelString}\`.`,
|
|
382
|
+
},
|
|
383
|
+
meta: { agentId: context.state.agentId },
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
yield {
|
|
387
|
+
type: 'client:ui:widget',
|
|
388
|
+
data: {
|
|
389
|
+
widgetId: event.data.widgetId,
|
|
390
|
+
kind: 'message',
|
|
391
|
+
title: 'API Key Saved',
|
|
392
|
+
body: `Successfully saved ${provider} API key and selected model \`${newModelString}\`. You can now continue your conversation.`,
|
|
393
|
+
state: 'submitted',
|
|
394
|
+
actions: [{ id: 'ok', label: 'Got it', variant: 'primary' }],
|
|
395
|
+
},
|
|
396
|
+
meta: { agentId: context.state.agentId },
|
|
397
|
+
};
|
|
398
|
+
} catch (error) {
|
|
399
|
+
yield {
|
|
400
|
+
type: 'agent:output',
|
|
401
|
+
data: {
|
|
402
|
+
content: `Failed to save API key: ${
|
|
403
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
404
|
+
}`,
|
|
405
|
+
},
|
|
406
|
+
meta: { agentId: context.state.agentId },
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
};
|