openbot 0.5.7 → 0.5.9
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 +1 -1
- package/dist/app/cli.js +1 -1
- package/dist/harness/index.js +7 -1
- package/dist/harness/merge-plugin-config.js +26 -0
- package/dist/plugins/storage/service.js +43 -26
- package/dist/services/plugins/enrich-openbot-plugin.js +38 -0
- package/dist/services/plugins/host.js +0 -3
- package/dist/services/plugins/model-registry.js +1 -1
- package/package.json +2 -2
- package/src/app/cli.ts +1 -1
- package/src/app/types.ts +2 -0
- package/src/harness/index.ts +9 -1
- package/src/harness/merge-plugin-config.ts +35 -0
- package/src/plugins/storage/service.ts +64 -64
- package/src/services/plugins/domain.ts +7 -1
- package/src/services/plugins/enrich-openbot-plugin.ts +41 -0
- package/src/services/plugins/host.ts +0 -6
- package/src/services/plugins/types.ts +0 -6
- package/src/services/plugins/model-registry.ts +0 -178
package/CLAUDE.md
CHANGED
|
@@ -31,7 +31,7 @@ OpenBot is a local-first, event-driven harness for running AI agents, built on t
|
|
|
31
31
|
|
|
32
32
|
Core built-ins: `storage` (infra handlers for `/api/state`), `plugin-manager` (marketplace + npm plugin install). The default agent runtime is **`@meetopenbot/openbot`**, a community npm package installed to `~/.openbot/plugins/@meetopenbot/openbot/` on first boot (`ensureDefaultStack`). Disk-installed plugins take precedence over built-ins in `resolvePlugin`.
|
|
33
33
|
|
|
34
|
-
A `Plugin`'s `factory(context)` returns a `MelonyPlugin`; `PluginContext` carries `agentId`, `agentDetails`, per-agent `config` (from `AGENT.md` frontmatter), the shared `storage` service, the merged `tools` map, `publicBaseUrl`, the run's `abortSignal`, and `host` (platform hooks: `runAgent`, cloud auth,
|
|
34
|
+
A `Plugin`'s `factory(context)` returns a `MelonyPlugin`; `PluginContext` carries `agentId`, `agentDetails`, per-agent `config` (from `AGENT.md` frontmatter), the shared `storage` service, the merged `tools` map, `publicBaseUrl`, the run's `abortSignal`, and `host` (platform hooks: `runAgent`, cloud auth, config).
|
|
35
35
|
|
|
36
36
|
### Agents
|
|
37
37
|
|
package/dist/app/cli.js
CHANGED
|
@@ -18,7 +18,7 @@ function checkNodeVersion() {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
checkNodeVersion();
|
|
21
|
-
program.name('openbot').description('OpenBot CLI').version('0.5.
|
|
21
|
+
program.name('openbot').description('OpenBot CLI').version('0.5.9');
|
|
22
22
|
program
|
|
23
23
|
.command('start')
|
|
24
24
|
.description('Start the OpenBot harness')
|
package/dist/harness/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { abortRegistry, abortKey } from '../services/abort.js';
|
|
|
7
7
|
import { loadConfig } from '../app/config.js';
|
|
8
8
|
import { getPublicBaseUrl } from '../plugins/storage/files.js';
|
|
9
9
|
import { createPluginHost } from '../services/plugins/host.js';
|
|
10
|
+
import { getInvokeConfigOverrides, mergePluginConfig } from './merge-plugin-config.js';
|
|
10
11
|
export { STATE_AGENT_ID, ORCHESTRATOR_AGENT_ID };
|
|
11
12
|
async function emitEvent(chunk, state, { persistEvents, channelId, threadId, onEvent, parentAgentId, parentToolCallId, }) {
|
|
12
13
|
ensureEventId(chunk);
|
|
@@ -69,6 +70,11 @@ export async function runAgent(options) {
|
|
|
69
70
|
}
|
|
70
71
|
const builder = melony().initialState(state);
|
|
71
72
|
const pluginHost = createPluginHost(runAgent);
|
|
73
|
+
const invokeConfigOverrides = getInvokeConfigOverrides(event);
|
|
74
|
+
const pluginConfigs = Object.fromEntries(pluginRefs.map((ref) => [
|
|
75
|
+
ref.id,
|
|
76
|
+
mergePluginConfig(ref.config, invokeConfigOverrides, ref.id),
|
|
77
|
+
]));
|
|
72
78
|
for (const ref of pluginRefs) {
|
|
73
79
|
const plugin = await resolvePlugin(ref.id);
|
|
74
80
|
if (!plugin)
|
|
@@ -76,7 +82,7 @@ export async function runAgent(options) {
|
|
|
76
82
|
builder.use(plugin.factory({
|
|
77
83
|
agentId,
|
|
78
84
|
agentDetails,
|
|
79
|
-
config: ref.
|
|
85
|
+
config: pluginConfigs[ref.id],
|
|
80
86
|
storage: storageService,
|
|
81
87
|
tools,
|
|
82
88
|
publicBaseUrl,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
function isPlainObject(value) {
|
|
2
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function isPluginKeyedConfig(config) {
|
|
5
|
+
return Object.values(config).every(isPlainObject);
|
|
6
|
+
}
|
|
7
|
+
export function getInvokeConfigOverrides(event) {
|
|
8
|
+
if (event.type !== 'agent:invoke')
|
|
9
|
+
return undefined;
|
|
10
|
+
const config = event.data?.config;
|
|
11
|
+
if (!isPlainObject(config) || Object.keys(config).length === 0)
|
|
12
|
+
return undefined;
|
|
13
|
+
return config;
|
|
14
|
+
}
|
|
15
|
+
export function mergePluginConfig(base, overrides, pluginId) {
|
|
16
|
+
const baseConfig = base ?? {};
|
|
17
|
+
if (!overrides)
|
|
18
|
+
return baseConfig;
|
|
19
|
+
if (isPluginKeyedConfig(overrides)) {
|
|
20
|
+
const pluginOverride = overrides[pluginId];
|
|
21
|
+
if (!isPlainObject(pluginOverride))
|
|
22
|
+
return baseConfig;
|
|
23
|
+
return { ...baseConfig, ...pluginOverride };
|
|
24
|
+
}
|
|
25
|
+
return { ...baseConfig, ...overrides };
|
|
26
|
+
}
|
|
@@ -9,10 +9,10 @@ import crypto from 'node:crypto';
|
|
|
9
9
|
import matter from 'gray-matter';
|
|
10
10
|
import { OPENBOT_PLUGIN_ID } from '../../app/openbot-plugin.js';
|
|
11
11
|
import { listBuiltInPlugins, parsePluginModule } from '../../services/plugins/registry.js';
|
|
12
|
-
import { enrichOpenbotPluginDescriptor
|
|
12
|
+
import { enrichOpenbotPluginDescriptor } from '../../services/plugins/enrich-openbot-plugin.js';
|
|
13
13
|
import { processService } from '../../services/process.js';
|
|
14
14
|
import { memoryService } from '../memory/service.js';
|
|
15
|
-
import { guessMimeType, resolveChannelFile, statChannelFile
|
|
15
|
+
import { guessMimeType, resolveChannelFile, statChannelFile } from './files.js';
|
|
16
16
|
const resolveBaseDir = () => getBaseDir();
|
|
17
17
|
const ENTITY_SVG_CANDIDATE_NAMES = ['avatar.svg', 'icon.svg', 'image.svg', 'logo.svg'];
|
|
18
18
|
const toSvgDataUrl = (svg) => `data:image/svg+xml;base64,${Buffer.from(svg, 'utf-8').toString('base64')}`;
|
|
@@ -115,10 +115,7 @@ function mergeCloudSystemPluginRefs(defaults, diskRefs) {
|
|
|
115
115
|
];
|
|
116
116
|
}
|
|
117
117
|
/** No `openbot` / `bash` — storage-side effects and infra plugins only. */
|
|
118
|
-
const STATE_DEFAULT_PLUGINS = [
|
|
119
|
-
{ id: 'storage' },
|
|
120
|
-
{ id: 'plugin-manager' },
|
|
121
|
-
];
|
|
118
|
+
const STATE_DEFAULT_PLUGINS = [{ id: 'storage' }, { id: 'plugin-manager' }];
|
|
122
119
|
const STATE_AGENT_INSTRUCTIONS = 'Built-in infra agent for deterministic state reads. No conversational model is attached; handle storage, approvals, memory, and plugin marketplace events.';
|
|
123
120
|
function getSystemAgentDetails(overrides) {
|
|
124
121
|
const defaultPlugins = isCloudMode() ? getCloudSystemDefaultPlugins() : SYSTEM_DEFAULT_PLUGINS;
|
|
@@ -289,12 +286,37 @@ const toVariablesRecord = (raw) => {
|
|
|
289
286
|
String(value ?? ''),
|
|
290
287
|
]));
|
|
291
288
|
};
|
|
289
|
+
let cachedRuntimeVersion;
|
|
290
|
+
const getRuntimeVersion = () => {
|
|
291
|
+
if (cachedRuntimeVersion !== undefined)
|
|
292
|
+
return cachedRuntimeVersion || undefined;
|
|
293
|
+
try {
|
|
294
|
+
const pkgPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '../../../package.json');
|
|
295
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
296
|
+
cachedRuntimeVersion = typeof pkg.version === 'string' ? pkg.version : '';
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
cachedRuntimeVersion = '';
|
|
300
|
+
}
|
|
301
|
+
return cachedRuntimeVersion || undefined;
|
|
302
|
+
};
|
|
303
|
+
const readPluginPackageVersion = async (pluginDir) => {
|
|
304
|
+
try {
|
|
305
|
+
const pkg = JSON.parse(await fs.readFile(path.join(pluginDir, 'package.json'), 'utf-8'));
|
|
306
|
+
return typeof pkg.version === 'string' ? pkg.version : undefined;
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
};
|
|
292
312
|
const listBuiltInPluginDescriptors = async () => {
|
|
313
|
+
const version = getRuntimeVersion();
|
|
293
314
|
return listBuiltInPlugins().map((plugin) => ({
|
|
294
315
|
id: plugin.id,
|
|
295
316
|
name: plugin.name,
|
|
296
317
|
description: plugin.description,
|
|
297
318
|
builtIn: true,
|
|
319
|
+
version,
|
|
298
320
|
image: plugin.image,
|
|
299
321
|
configSchema: plugin.configSchema,
|
|
300
322
|
createdAt: new Date(),
|
|
@@ -356,12 +378,16 @@ const listPluginsFromDisk = async () => {
|
|
|
356
378
|
const parsed = parsePluginModule(module);
|
|
357
379
|
if (!parsed)
|
|
358
380
|
return null;
|
|
359
|
-
const image = await
|
|
381
|
+
const [image, version] = await Promise.all([
|
|
382
|
+
resolveEntityImageDataUrl(pluginDir),
|
|
383
|
+
readPluginPackageVersion(pluginDir),
|
|
384
|
+
]);
|
|
360
385
|
return {
|
|
361
386
|
id,
|
|
362
387
|
name: parsed.name || id,
|
|
363
388
|
description: parsed.description || '',
|
|
364
389
|
builtIn: false,
|
|
390
|
+
version,
|
|
365
391
|
image: parsed.image || image,
|
|
366
392
|
configSchema: parsed.configSchema,
|
|
367
393
|
createdAt: new Date(),
|
|
@@ -480,7 +506,7 @@ export const storageService = {
|
|
|
480
506
|
channel.recentThreads = allThreads
|
|
481
507
|
.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())
|
|
482
508
|
.slice(0, 5);
|
|
483
|
-
const threadsUnseen = allThreads.some(t => t.hasUnseenMessages);
|
|
509
|
+
const threadsUnseen = allThreads.some((t) => t.hasUnseenMessages);
|
|
484
510
|
channel.hasUnseenMessages = rootUnseen || threadsUnseen;
|
|
485
511
|
}
|
|
486
512
|
catch {
|
|
@@ -521,8 +547,7 @@ export const storageService = {
|
|
|
521
547
|
finalState.cwd = resolvedCwd;
|
|
522
548
|
await fs.mkdir(resolvedCwd, { recursive: true });
|
|
523
549
|
await fs.mkdir(channelDir, { recursive: true });
|
|
524
|
-
await fs.writeFile(specPath, spec?.trim() ||
|
|
525
|
-
`# ${normalizedChannelId}\n\n`);
|
|
550
|
+
await fs.writeFile(specPath, spec?.trim() || `# ${normalizedChannelId}\n\n`);
|
|
526
551
|
await writeJsonFileAtomically(statePath, finalState);
|
|
527
552
|
},
|
|
528
553
|
ensureChannel: async ({ channelId, spec, initialState, cwd, }) => {
|
|
@@ -684,9 +709,7 @@ export const storageService = {
|
|
|
684
709
|
console.error(`Failed to read thread state for channel ${channelId} thread ${threadId}`, error);
|
|
685
710
|
}
|
|
686
711
|
}
|
|
687
|
-
const threadName = isRecord(state) && typeof state.name === 'string'
|
|
688
|
-
? state.name.trim()
|
|
689
|
-
: '';
|
|
712
|
+
const threadName = isRecord(state) && typeof state.name === 'string' ? state.name.trim() : '';
|
|
690
713
|
return {
|
|
691
714
|
id: threadId,
|
|
692
715
|
name: threadName || threadId,
|
|
@@ -814,13 +837,12 @@ export const storageService = {
|
|
|
814
837
|
return Array.from(deduped.values()).filter((agent) => !agent.hidden);
|
|
815
838
|
},
|
|
816
839
|
getPlugins: async () => {
|
|
817
|
-
const [builtIn, fromDisk
|
|
840
|
+
const [builtIn, fromDisk] = await Promise.all([
|
|
818
841
|
listBuiltInPluginDescriptors(),
|
|
819
842
|
listPluginsFromDisk(),
|
|
820
|
-
listRegistryModelOptions(),
|
|
821
843
|
]);
|
|
822
|
-
const
|
|
823
|
-
const merged = [...
|
|
844
|
+
const enrichPlugin = (plugin) => enrichOpenbotPluginDescriptor(plugin);
|
|
845
|
+
const merged = [...builtIn.map(enrichPlugin), ...fromDisk.map(enrichPlugin)];
|
|
824
846
|
const deduped = new Map();
|
|
825
847
|
for (const plugin of merged) {
|
|
826
848
|
if (!deduped.has(plugin.id)) {
|
|
@@ -840,9 +862,7 @@ export const storageService = {
|
|
|
840
862
|
const discoveredImage = await resolveEntityImageDataUrl(agentDir);
|
|
841
863
|
const stats = await fs.stat(agentMdPath);
|
|
842
864
|
const pluginRefs = parsePluginRefs(data.plugins);
|
|
843
|
-
const frontmatterImage = typeof data.image === 'string' && data.image.trim() !== ''
|
|
844
|
-
? data.image.trim()
|
|
845
|
-
: undefined;
|
|
865
|
+
const frontmatterImage = typeof data.image === 'string' && data.image.trim() !== '' ? data.image.trim() : undefined;
|
|
846
866
|
diskDetails = {
|
|
847
867
|
id: agentId,
|
|
848
868
|
name: typeof data.name === 'string' ? data.name : agentId,
|
|
@@ -964,8 +984,7 @@ export const storageService = {
|
|
|
964
984
|
// Built-in agents merge disk overlays with code defaults on read; on write, partial
|
|
965
985
|
// updates (e.g. plugins-only) must still persist a complete AGENT.md.
|
|
966
986
|
if (isBuiltinOverlayAgentId(agentId)) {
|
|
967
|
-
const pluginRefs = plugins ??
|
|
968
|
-
(nextData.plugins !== undefined ? parsePluginRefs(nextData.plugins) : undefined);
|
|
987
|
+
const pluginRefs = plugins ?? (nextData.plugins !== undefined ? parsePluginRefs(nextData.plugins) : undefined);
|
|
969
988
|
const diskOverrides = {};
|
|
970
989
|
if (typeof nextData.name === 'string' && nextData.name.trim() !== '') {
|
|
971
990
|
diskOverrides.name = nextData.name;
|
|
@@ -1170,7 +1189,7 @@ export const storageService = {
|
|
|
1170
1189
|
typeof raw === 'object' &&
|
|
1171
1190
|
'variables' in raw &&
|
|
1172
1191
|
Array.isArray(raw.variables)) {
|
|
1173
|
-
const entries =
|
|
1192
|
+
const entries = raw.variables
|
|
1174
1193
|
.filter((v) => typeof v?.key === 'string')
|
|
1175
1194
|
.map((v) => [v.key, { value: String(v.value ?? ''), secret: !!v.secret }]);
|
|
1176
1195
|
return Object.fromEntries(entries);
|
|
@@ -1236,9 +1255,7 @@ export const storageService = {
|
|
|
1236
1255
|
if (!baseCwd) {
|
|
1237
1256
|
throw new Error('Channel has no CWD configured');
|
|
1238
1257
|
}
|
|
1239
|
-
const targetDir = subPath
|
|
1240
|
-
? resolveChannelFile(baseCwd, subPath)
|
|
1241
|
-
: resolvePath(baseCwd);
|
|
1258
|
+
const targetDir = subPath ? resolveChannelFile(baseCwd, subPath) : resolvePath(baseCwd);
|
|
1242
1259
|
const entries = await fs.readdir(targetDir, { withFileTypes: true });
|
|
1243
1260
|
return entries
|
|
1244
1261
|
.filter((e) => !e.name.startsWith('.'))
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { OPENBOT_PLUGIN_ID } from '../../app/openbot-plugin.js';
|
|
2
|
+
import { DEFAULT_CLOUD_OPENBOT_AUTH_MODE, isCloudMode } from '../../app/cloud-mode.js';
|
|
3
|
+
/** Injects cloud platform fields into the openbot plugin descriptor for settings UI. */
|
|
4
|
+
export function enrichOpenbotPluginDescriptor(descriptor) {
|
|
5
|
+
if (descriptor.id !== OPENBOT_PLUGIN_ID || !descriptor.configSchema)
|
|
6
|
+
return descriptor;
|
|
7
|
+
if (!isCloudMode())
|
|
8
|
+
return descriptor;
|
|
9
|
+
const { authMode: _authMode, ...otherProperties } = descriptor.configSchema.properties;
|
|
10
|
+
const cloudAuthModeProperty = {
|
|
11
|
+
type: 'string',
|
|
12
|
+
description: 'Credits uses OpenBot platform billing. BYOK uses your own API keys.',
|
|
13
|
+
enum: ['credits', 'byok'],
|
|
14
|
+
default: DEFAULT_CLOUD_OPENBOT_AUTH_MODE,
|
|
15
|
+
options: [
|
|
16
|
+
{
|
|
17
|
+
label: 'Credits',
|
|
18
|
+
value: 'credits',
|
|
19
|
+
description: 'Use OpenBot platform credits.',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
label: 'Bring your own key',
|
|
23
|
+
value: 'byok',
|
|
24
|
+
description: 'Use your own provider API keys.',
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
...descriptor,
|
|
30
|
+
configSchema: {
|
|
31
|
+
...descriptor.configSchema,
|
|
32
|
+
properties: {
|
|
33
|
+
authMode: cloudAuthModeProperty,
|
|
34
|
+
...otherProperties,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -2,15 +2,12 @@ import { DEFAULT_CLOUD_OPENBOT_AUTH_MODE, isCloudMode, isCloudSystemAgent, parse
|
|
|
2
2
|
import { ORCHESTRATOR_AGENT_ID } from '../../app/agent-ids.js';
|
|
3
3
|
import { OPENBOT_PLUGIN_ID } from '../../app/openbot-plugin.js';
|
|
4
4
|
import { getBaseDir, resolvePath, saveConfig } from '../../app/config.js';
|
|
5
|
-
import { listApiKeyProvidersFromRegistry, resolveModelRegistry, } from './model-registry.js';
|
|
6
5
|
export function createPluginHost(runAgent) {
|
|
7
6
|
return {
|
|
8
7
|
runAgent,
|
|
9
8
|
isCloudSystemAgent,
|
|
10
9
|
isCloudMode,
|
|
11
10
|
parseOpenbotAuthMode,
|
|
12
|
-
resolveModelRegistry,
|
|
13
|
-
listApiKeyProvidersFromRegistry,
|
|
14
11
|
saveConfig,
|
|
15
12
|
getBaseDir,
|
|
16
13
|
resolvePath,
|
|
@@ -74,7 +74,7 @@ export function enrichOpenbotPluginDescriptor(descriptor, modelOptions) {
|
|
|
74
74
|
: pickDefaultModelValue(modelOptions) ?? staticDefault;
|
|
75
75
|
const nextModelProperty = {
|
|
76
76
|
...modelProperty,
|
|
77
|
-
description: 'Model from the hosted marketplace registry.',
|
|
77
|
+
description: modelProperty.description ?? 'Model from the hosted marketplace registry.',
|
|
78
78
|
};
|
|
79
79
|
if (values.length > 0) {
|
|
80
80
|
nextModelProperty.enum = values;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openbot",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"build": "tsc && mkdir -p dist/assets && cp src/assets/icon.svg dist/assets/icon.svg",
|
|
12
12
|
"start": "node dist/app/cli.js start",
|
|
13
13
|
"format": "prettier --write .",
|
|
14
|
-
"push": "fly deploy --build-only --push --config deploy/fly.toml -a openbot-runtime --image-label v0.5.
|
|
14
|
+
"push": "fly deploy --build-only --push --config deploy/fly.toml -a openbot-runtime --image-label v0.5.9"
|
|
15
15
|
},
|
|
16
16
|
"bin": {
|
|
17
17
|
"openbot": "./dist/app/cli.js"
|
package/src/app/cli.ts
CHANGED
package/src/app/types.ts
CHANGED
|
@@ -42,6 +42,8 @@ export type AgentInvokeEvent = BaseEvent & {
|
|
|
42
42
|
role?: 'user' | 'assistant' | 'system';
|
|
43
43
|
content: string;
|
|
44
44
|
agentId?: string;
|
|
45
|
+
/** Per-run config overrides: flat fields (e.g. model) or nested by plugin id. */
|
|
46
|
+
config?: Record<string, unknown>;
|
|
45
47
|
};
|
|
46
48
|
};
|
|
47
49
|
|
package/src/harness/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { abortRegistry, abortKey } from '../services/abort.js';
|
|
|
9
9
|
import { loadConfig } from '../app/config.js';
|
|
10
10
|
import { getPublicBaseUrl } from '../plugins/storage/files.js';
|
|
11
11
|
import { createPluginHost } from '../services/plugins/host.js';
|
|
12
|
+
import { getInvokeConfigOverrides, mergePluginConfig } from './merge-plugin-config.js';
|
|
12
13
|
|
|
13
14
|
export { STATE_AGENT_ID, ORCHESTRATOR_AGENT_ID };
|
|
14
15
|
|
|
@@ -121,6 +122,13 @@ export async function runAgent(options: RunAgentOptions): Promise<void> {
|
|
|
121
122
|
const builder = melony<OpenBotState, OpenBotEvent>().initialState(state);
|
|
122
123
|
|
|
123
124
|
const pluginHost = createPluginHost(runAgent);
|
|
125
|
+
const invokeConfigOverrides = getInvokeConfigOverrides(event);
|
|
126
|
+
const pluginConfigs = Object.fromEntries(
|
|
127
|
+
pluginRefs.map((ref) => [
|
|
128
|
+
ref.id,
|
|
129
|
+
mergePluginConfig(ref.config, invokeConfigOverrides, ref.id),
|
|
130
|
+
]),
|
|
131
|
+
);
|
|
124
132
|
|
|
125
133
|
for (const ref of pluginRefs) {
|
|
126
134
|
const plugin = await resolvePlugin(ref.id);
|
|
@@ -130,7 +138,7 @@ export async function runAgent(options: RunAgentOptions): Promise<void> {
|
|
|
130
138
|
plugin.factory({
|
|
131
139
|
agentId,
|
|
132
140
|
agentDetails,
|
|
133
|
-
config: ref.
|
|
141
|
+
config: pluginConfigs[ref.id],
|
|
134
142
|
storage: storageService,
|
|
135
143
|
tools,
|
|
136
144
|
publicBaseUrl,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { OpenBotEvent } from '../app/types.js';
|
|
2
|
+
|
|
3
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
4
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function isPluginKeyedConfig(config: Record<string, unknown>): boolean {
|
|
8
|
+
return Object.values(config).every(isPlainObject);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getInvokeConfigOverrides(event: OpenBotEvent): Record<string, unknown> | undefined {
|
|
12
|
+
if (event.type !== 'agent:invoke') return undefined;
|
|
13
|
+
|
|
14
|
+
const config = event.data?.config;
|
|
15
|
+
if (!isPlainObject(config) || Object.keys(config).length === 0) return undefined;
|
|
16
|
+
|
|
17
|
+
return config;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function mergePluginConfig(
|
|
21
|
+
base: Record<string, unknown> | undefined,
|
|
22
|
+
overrides: Record<string, unknown> | undefined,
|
|
23
|
+
pluginId: string,
|
|
24
|
+
): Record<string, unknown> {
|
|
25
|
+
const baseConfig = base ?? {};
|
|
26
|
+
if (!overrides) return baseConfig;
|
|
27
|
+
|
|
28
|
+
if (isPluginKeyedConfig(overrides)) {
|
|
29
|
+
const pluginOverride = overrides[pluginId];
|
|
30
|
+
if (!isPlainObject(pluginOverride)) return baseConfig;
|
|
31
|
+
return { ...baseConfig, ...pluginOverride };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { ...baseConfig, ...overrides };
|
|
35
|
+
}
|
|
@@ -33,18 +33,11 @@ import {
|
|
|
33
33
|
import type { PluginRef } from '../../services/plugins/types.js';
|
|
34
34
|
import { OPENBOT_PLUGIN_ID } from '../../app/openbot-plugin.js';
|
|
35
35
|
import { listBuiltInPlugins, parsePluginModule } from '../../services/plugins/registry.js';
|
|
36
|
-
import {
|
|
37
|
-
enrichOpenbotPluginDescriptor,
|
|
38
|
-
listRegistryModelOptions,
|
|
39
|
-
} from '../../services/plugins/model-registry.js';
|
|
36
|
+
import { enrichOpenbotPluginDescriptor } from '../../services/plugins/enrich-openbot-plugin.js';
|
|
40
37
|
import { OpenBotEvent, OpenBotState } from '../../app/types.js';
|
|
41
38
|
import { processService } from '../../services/process.js';
|
|
42
39
|
import { memoryService } from '../memory/service.js';
|
|
43
|
-
import {
|
|
44
|
-
guessMimeType,
|
|
45
|
-
resolveChannelFile,
|
|
46
|
-
statChannelFile,
|
|
47
|
-
} from './files.js';
|
|
40
|
+
import { guessMimeType, resolveChannelFile, statChannelFile } from './files.js';
|
|
48
41
|
|
|
49
42
|
const resolveBaseDir = () => getBaseDir();
|
|
50
43
|
|
|
@@ -142,10 +135,7 @@ function getCloudSystemDefaultPlugins(): PluginRef[] {
|
|
|
142
135
|
});
|
|
143
136
|
}
|
|
144
137
|
|
|
145
|
-
function mergeCloudSystemPluginRefs(
|
|
146
|
-
defaults: PluginRef[],
|
|
147
|
-
diskRefs: PluginRef[],
|
|
148
|
-
): PluginRef[] {
|
|
138
|
+
function mergeCloudSystemPluginRefs(defaults: PluginRef[], diskRefs: PluginRef[]): PluginRef[] {
|
|
149
139
|
const defaultOpenbot = defaults.find((ref) => ref.id === OPENBOT_PLUGIN_ID);
|
|
150
140
|
const diskOpenbot = diskRefs.find((ref) => ref.id === OPENBOT_PLUGIN_ID);
|
|
151
141
|
|
|
@@ -163,10 +153,7 @@ function mergeCloudSystemPluginRefs(
|
|
|
163
153
|
}
|
|
164
154
|
|
|
165
155
|
/** No `openbot` / `bash` — storage-side effects and infra plugins only. */
|
|
166
|
-
const STATE_DEFAULT_PLUGINS: PluginRef[] = [
|
|
167
|
-
{ id: 'storage' },
|
|
168
|
-
{ id: 'plugin-manager' },
|
|
169
|
-
];
|
|
156
|
+
const STATE_DEFAULT_PLUGINS: PluginRef[] = [{ id: 'storage' }, { id: 'plugin-manager' }];
|
|
170
157
|
|
|
171
158
|
const STATE_AGENT_INSTRUCTIONS =
|
|
172
159
|
'Built-in infra agent for deterministic state reads. No conversational model is attached; handle storage, approvals, memory, and plugin marketplace events.';
|
|
@@ -177,8 +164,7 @@ function getSystemAgentDetails(overrides?: Partial<AgentDetails>): AgentDetails
|
|
|
177
164
|
id: SYSTEM_AGENT_ID,
|
|
178
165
|
name: 'OpenBot',
|
|
179
166
|
image: getBundledSystemAgentImage(),
|
|
180
|
-
description:
|
|
181
|
-
'First-party orchestration agent for OpenBot.',
|
|
167
|
+
description: 'First-party orchestration agent for OpenBot.',
|
|
182
168
|
instructions: '',
|
|
183
169
|
plugins: defaultPlugins.map((ref) => ref.id),
|
|
184
170
|
pluginRefs: defaultPlugins,
|
|
@@ -211,9 +197,10 @@ function getSystemAgentDetails(overrides?: Partial<AgentDetails>): AgentDetails
|
|
|
211
197
|
};
|
|
212
198
|
}
|
|
213
199
|
|
|
214
|
-
const refs =
|
|
215
|
-
|
|
216
|
-
|
|
200
|
+
const refs =
|
|
201
|
+
overrides.pluginRefs && overrides.pluginRefs.length > 0
|
|
202
|
+
? overrides.pluginRefs
|
|
203
|
+
: defaults.pluginRefs;
|
|
217
204
|
|
|
218
205
|
return {
|
|
219
206
|
...defaults,
|
|
@@ -243,9 +230,10 @@ function getStateAgentDetails(overrides?: Partial<AgentDetails>): AgentDetails {
|
|
|
243
230
|
|
|
244
231
|
if (!overrides) return defaults;
|
|
245
232
|
|
|
246
|
-
const refs =
|
|
247
|
-
|
|
248
|
-
|
|
233
|
+
const refs =
|
|
234
|
+
overrides.pluginRefs && overrides.pluginRefs.length > 0
|
|
235
|
+
? overrides.pluginRefs
|
|
236
|
+
: defaults.pluginRefs;
|
|
249
237
|
|
|
250
238
|
const diskInstructions = overrides.instructions?.trim();
|
|
251
239
|
const instructions =
|
|
@@ -374,12 +362,42 @@ const toVariablesRecord = (raw: unknown): Record<string, string> => {
|
|
|
374
362
|
);
|
|
375
363
|
};
|
|
376
364
|
|
|
365
|
+
let cachedRuntimeVersion: string | undefined;
|
|
366
|
+
|
|
367
|
+
const getRuntimeVersion = (): string | undefined => {
|
|
368
|
+
if (cachedRuntimeVersion !== undefined) return cachedRuntimeVersion || undefined;
|
|
369
|
+
try {
|
|
370
|
+
const pkgPath = path.join(
|
|
371
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
372
|
+
'../../../package.json',
|
|
373
|
+
);
|
|
374
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { version?: unknown };
|
|
375
|
+
cachedRuntimeVersion = typeof pkg.version === 'string' ? pkg.version : '';
|
|
376
|
+
} catch {
|
|
377
|
+
cachedRuntimeVersion = '';
|
|
378
|
+
}
|
|
379
|
+
return cachedRuntimeVersion || undefined;
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const readPluginPackageVersion = async (pluginDir: string): Promise<string | undefined> => {
|
|
383
|
+
try {
|
|
384
|
+
const pkg = JSON.parse(await fs.readFile(path.join(pluginDir, 'package.json'), 'utf-8')) as {
|
|
385
|
+
version?: unknown;
|
|
386
|
+
};
|
|
387
|
+
return typeof pkg.version === 'string' ? pkg.version : undefined;
|
|
388
|
+
} catch {
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
377
393
|
const listBuiltInPluginDescriptors = async (): Promise<PluginDescriptor[]> => {
|
|
394
|
+
const version = getRuntimeVersion();
|
|
378
395
|
return listBuiltInPlugins().map((plugin) => ({
|
|
379
396
|
id: plugin.id,
|
|
380
397
|
name: plugin.name,
|
|
381
398
|
description: plugin.description,
|
|
382
399
|
builtIn: true,
|
|
400
|
+
version,
|
|
383
401
|
image: plugin.image,
|
|
384
402
|
configSchema: plugin.configSchema,
|
|
385
403
|
createdAt: new Date(),
|
|
@@ -443,12 +461,16 @@ const listPluginsFromDisk = async (): Promise<PluginDescriptor[]> => {
|
|
|
443
461
|
const module = await import(pathToFileURL(distPath).href);
|
|
444
462
|
const parsed = parsePluginModule(module as Record<string, unknown>);
|
|
445
463
|
if (!parsed) return null;
|
|
446
|
-
const image = await
|
|
464
|
+
const [image, version] = await Promise.all([
|
|
465
|
+
resolveEntityImageDataUrl(pluginDir),
|
|
466
|
+
readPluginPackageVersion(pluginDir),
|
|
467
|
+
]);
|
|
447
468
|
return {
|
|
448
469
|
id,
|
|
449
470
|
name: parsed.name || id,
|
|
450
471
|
description: parsed.description || '',
|
|
451
472
|
builtIn: false,
|
|
473
|
+
version,
|
|
452
474
|
image: parsed.image || image,
|
|
453
475
|
configSchema: parsed.configSchema,
|
|
454
476
|
createdAt: new Date(),
|
|
@@ -468,9 +490,7 @@ const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
|
468
490
|
!!value && typeof value === 'object' && !Array.isArray(value);
|
|
469
491
|
|
|
470
492
|
/** Display-oriented fields persisted in a channel's `state.json`. */
|
|
471
|
-
const readChannelStateFileFields = (
|
|
472
|
-
parsed: unknown,
|
|
473
|
-
): { name?: string; cwd?: string } => {
|
|
493
|
+
const readChannelStateFileFields = (parsed: unknown): { name?: string; cwd?: string } => {
|
|
474
494
|
if (!isRecord(parsed)) {
|
|
475
495
|
return {};
|
|
476
496
|
}
|
|
@@ -583,7 +603,7 @@ export const storageService = {
|
|
|
583
603
|
};
|
|
584
604
|
|
|
585
605
|
const channelLastRead = lastReadMap[name] || {};
|
|
586
|
-
|
|
606
|
+
|
|
587
607
|
try {
|
|
588
608
|
// Check root unread
|
|
589
609
|
const rootEvents = await storageService.getEvents({ channelId: name });
|
|
@@ -595,8 +615,8 @@ export const storageService = {
|
|
|
595
615
|
channel.recentThreads = allThreads
|
|
596
616
|
.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())
|
|
597
617
|
.slice(0, 5);
|
|
598
|
-
|
|
599
|
-
const threadsUnseen = allThreads.some(t => t.hasUnseenMessages);
|
|
618
|
+
|
|
619
|
+
const threadsUnseen = allThreads.some((t) => t.hasUnseenMessages);
|
|
600
620
|
channel.hasUnseenMessages = rootUnseen || threadsUnseen;
|
|
601
621
|
} catch {
|
|
602
622
|
channel.hasUnseenMessages = false;
|
|
@@ -654,11 +674,7 @@ export const storageService = {
|
|
|
654
674
|
finalState.cwd = resolvedCwd;
|
|
655
675
|
await fs.mkdir(resolvedCwd, { recursive: true });
|
|
656
676
|
await fs.mkdir(channelDir, { recursive: true });
|
|
657
|
-
await fs.writeFile(
|
|
658
|
-
specPath,
|
|
659
|
-
spec?.trim() ||
|
|
660
|
-
`# ${normalizedChannelId}\n\n`,
|
|
661
|
-
);
|
|
677
|
+
await fs.writeFile(specPath, spec?.trim() || `# ${normalizedChannelId}\n\n`);
|
|
662
678
|
await writeJsonFileAtomically(statePath, finalState);
|
|
663
679
|
},
|
|
664
680
|
ensureChannel: async ({
|
|
@@ -708,10 +724,7 @@ export const storageService = {
|
|
|
708
724
|
await fs.access(specPath);
|
|
709
725
|
} catch (error: unknown) {
|
|
710
726
|
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
|
|
711
|
-
await fs.writeFile(
|
|
712
|
-
specPath,
|
|
713
|
-
spec?.trim() || `# ${normalizedChannelId}\n\n`,
|
|
714
|
-
);
|
|
727
|
+
await fs.writeFile(specPath, spec?.trim() || `# ${normalizedChannelId}\n\n`);
|
|
715
728
|
} else {
|
|
716
729
|
throw error;
|
|
717
730
|
}
|
|
@@ -818,8 +831,7 @@ export const storageService = {
|
|
|
818
831
|
|
|
819
832
|
try {
|
|
820
833
|
const threadState = await readJsonFile<Record<string, unknown>>(threadStatePath, {});
|
|
821
|
-
const threadName =
|
|
822
|
-
typeof threadState.name === 'string' ? threadState.name.trim() : '';
|
|
834
|
+
const threadName = typeof threadState.name === 'string' ? threadState.name.trim() : '';
|
|
823
835
|
if (threadName) {
|
|
824
836
|
threadDisplayName = threadName;
|
|
825
837
|
}
|
|
@@ -876,10 +888,7 @@ export const storageService = {
|
|
|
876
888
|
}
|
|
877
889
|
}
|
|
878
890
|
|
|
879
|
-
const threadName =
|
|
880
|
-
isRecord(state) && typeof state.name === 'string'
|
|
881
|
-
? state.name.trim()
|
|
882
|
-
: '';
|
|
891
|
+
const threadName = isRecord(state) && typeof state.name === 'string' ? state.name.trim() : '';
|
|
883
892
|
|
|
884
893
|
return {
|
|
885
894
|
id: threadId,
|
|
@@ -1044,17 +1053,14 @@ export const storageService = {
|
|
|
1044
1053
|
return Array.from(deduped.values()).filter((agent) => !agent.hidden);
|
|
1045
1054
|
},
|
|
1046
1055
|
getPlugins: async (): Promise<PluginDescriptor[]> => {
|
|
1047
|
-
const [builtIn, fromDisk
|
|
1056
|
+
const [builtIn, fromDisk] = await Promise.all([
|
|
1048
1057
|
listBuiltInPluginDescriptors(),
|
|
1049
1058
|
listPluginsFromDisk(),
|
|
1050
|
-
listRegistryModelOptions(),
|
|
1051
1059
|
]);
|
|
1052
1060
|
|
|
1053
|
-
const
|
|
1054
|
-
enrichOpenbotPluginDescriptor(plugin, modelOptions),
|
|
1055
|
-
);
|
|
1061
|
+
const enrichPlugin = (plugin: PluginDescriptor) => enrichOpenbotPluginDescriptor(plugin);
|
|
1056
1062
|
|
|
1057
|
-
const merged = [...
|
|
1063
|
+
const merged = [...builtIn.map(enrichPlugin), ...fromDisk.map(enrichPlugin)];
|
|
1058
1064
|
const deduped = new Map<string, PluginDescriptor>();
|
|
1059
1065
|
for (const plugin of merged) {
|
|
1060
1066
|
if (!deduped.has(plugin.id)) {
|
|
@@ -1078,9 +1084,7 @@ export const storageService = {
|
|
|
1078
1084
|
|
|
1079
1085
|
const pluginRefs = parsePluginRefs(data.plugins);
|
|
1080
1086
|
const frontmatterImage =
|
|
1081
|
-
typeof data.image === 'string' && data.image.trim() !== ''
|
|
1082
|
-
? data.image.trim()
|
|
1083
|
-
: undefined;
|
|
1087
|
+
typeof data.image === 'string' && data.image.trim() !== '' ? data.image.trim() : undefined;
|
|
1084
1088
|
|
|
1085
1089
|
diskDetails = {
|
|
1086
1090
|
id: agentId,
|
|
@@ -1237,8 +1241,7 @@ export const storageService = {
|
|
|
1237
1241
|
// updates (e.g. plugins-only) must still persist a complete AGENT.md.
|
|
1238
1242
|
if (isBuiltinOverlayAgentId(agentId)) {
|
|
1239
1243
|
const pluginRefs =
|
|
1240
|
-
plugins ??
|
|
1241
|
-
(nextData.plugins !== undefined ? parsePluginRefs(nextData.plugins) : undefined);
|
|
1244
|
+
plugins ?? (nextData.plugins !== undefined ? parsePluginRefs(nextData.plugins) : undefined);
|
|
1242
1245
|
const diskOverrides: Partial<AgentDetails> = {};
|
|
1243
1246
|
if (typeof nextData.name === 'string' && nextData.name.trim() !== '') {
|
|
1244
1247
|
diskOverrides.name = nextData.name;
|
|
@@ -1472,7 +1475,7 @@ export const storageService = {
|
|
|
1472
1475
|
'variables' in raw &&
|
|
1473
1476
|
Array.isArray((raw as { variables: unknown }).variables)
|
|
1474
1477
|
) {
|
|
1475
|
-
const entries = (
|
|
1478
|
+
const entries = (raw as { variables: StoredVariable[] }).variables
|
|
1476
1479
|
.filter((v) => typeof v?.key === 'string')
|
|
1477
1480
|
.map((v) => [v.key, { value: String(v.value ?? ''), secret: !!v.secret }] as const);
|
|
1478
1481
|
return Object.fromEntries(entries);
|
|
@@ -1574,9 +1577,7 @@ export const storageService = {
|
|
|
1574
1577
|
throw new Error('Channel has no CWD configured');
|
|
1575
1578
|
}
|
|
1576
1579
|
|
|
1577
|
-
const targetDir = subPath
|
|
1578
|
-
? resolveChannelFile(baseCwd, subPath)
|
|
1579
|
-
: resolvePath(baseCwd);
|
|
1580
|
+
const targetDir = subPath ? resolveChannelFile(baseCwd, subPath) : resolvePath(baseCwd);
|
|
1580
1581
|
|
|
1581
1582
|
const entries = await fs.readdir(targetDir, { withFileTypes: true });
|
|
1582
1583
|
return entries
|
|
@@ -1623,8 +1624,7 @@ export const storageService = {
|
|
|
1623
1624
|
|
|
1624
1625
|
const targetFile = resolveChannelFile(baseCwd, filePath);
|
|
1625
1626
|
const buf = await fs.readFile(targetFile);
|
|
1626
|
-
const content =
|
|
1627
|
-
encoding === 'base64' ? buf.toString('base64') : buf.toString('utf-8');
|
|
1627
|
+
const content = encoding === 'base64' ? buf.toString('base64') : buf.toString('utf-8');
|
|
1628
1628
|
|
|
1629
1629
|
return {
|
|
1630
1630
|
content,
|
|
@@ -35,6 +35,8 @@ export type PluginDescriptor = {
|
|
|
35
35
|
description: string;
|
|
36
36
|
/** True when bundled with the core server (`src/registry/plugins`); false for ~/.openbot/plugins installs. */
|
|
37
37
|
builtIn: boolean;
|
|
38
|
+
/** Installed npm semver for disk plugins; runtime package version for built-ins. */
|
|
39
|
+
version?: string;
|
|
38
40
|
image?: string;
|
|
39
41
|
configSchema?: ConfigSchema;
|
|
40
42
|
createdAt: Date;
|
|
@@ -136,7 +138,11 @@ export interface Storage {
|
|
|
136
138
|
}) => Promise<void>;
|
|
137
139
|
getThreads: (args: { channelId: string }) => Promise<Thread[]>;
|
|
138
140
|
getThreadDetails: (args: { channelId: string; threadId: string }) => Promise<ThreadDetails>;
|
|
139
|
-
setLastRead: (args: {
|
|
141
|
+
setLastRead: (args: {
|
|
142
|
+
channelId: string;
|
|
143
|
+
threadId?: string;
|
|
144
|
+
lastReadEventId: string;
|
|
145
|
+
}) => Promise<void>;
|
|
140
146
|
/** User-facing agent list; excludes agents with `hidden: true` (e.g. built-in `state`). */
|
|
141
147
|
getAgents: () => Promise<Agent[]>;
|
|
142
148
|
getPlugins: () => Promise<PluginDescriptor[]>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { OPENBOT_PLUGIN_ID } from '../../app/openbot-plugin.js';
|
|
2
|
+
import { DEFAULT_CLOUD_OPENBOT_AUTH_MODE, isCloudMode } from '../../app/cloud-mode.js';
|
|
3
|
+
import type { ConfigSchema, PluginDescriptor } from './domain.js';
|
|
4
|
+
|
|
5
|
+
/** Injects cloud platform fields into the openbot plugin descriptor for settings UI. */
|
|
6
|
+
export function enrichOpenbotPluginDescriptor(descriptor: PluginDescriptor): PluginDescriptor {
|
|
7
|
+
if (descriptor.id !== OPENBOT_PLUGIN_ID || !descriptor.configSchema) return descriptor;
|
|
8
|
+
if (!isCloudMode()) return descriptor;
|
|
9
|
+
|
|
10
|
+
const { authMode: _authMode, ...otherProperties } = descriptor.configSchema.properties;
|
|
11
|
+
|
|
12
|
+
const cloudAuthModeProperty: ConfigSchema['properties'][string] = {
|
|
13
|
+
type: 'string',
|
|
14
|
+
description: 'Credits uses OpenBot platform billing. BYOK uses your own API keys.',
|
|
15
|
+
enum: ['credits', 'byok'],
|
|
16
|
+
default: DEFAULT_CLOUD_OPENBOT_AUTH_MODE,
|
|
17
|
+
options: [
|
|
18
|
+
{
|
|
19
|
+
label: 'Credits',
|
|
20
|
+
value: 'credits',
|
|
21
|
+
description: 'Use OpenBot platform credits.',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
label: 'Bring your own key',
|
|
25
|
+
value: 'byok',
|
|
26
|
+
description: 'Use your own provider API keys.',
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
...descriptor,
|
|
33
|
+
configSchema: {
|
|
34
|
+
...descriptor.configSchema,
|
|
35
|
+
properties: {
|
|
36
|
+
authMode: cloudAuthModeProperty,
|
|
37
|
+
...otherProperties,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -8,10 +8,6 @@ import {
|
|
|
8
8
|
import { ORCHESTRATOR_AGENT_ID } from '../../app/agent-ids.js';
|
|
9
9
|
import { OPENBOT_PLUGIN_ID } from '../../app/openbot-plugin.js';
|
|
10
10
|
import { getBaseDir, resolvePath, saveConfig } from '../../app/config.js';
|
|
11
|
-
import {
|
|
12
|
-
listApiKeyProvidersFromRegistry,
|
|
13
|
-
resolveModelRegistry,
|
|
14
|
-
} from './model-registry.js';
|
|
15
11
|
import type { PluginHost, RunAgentHostOptions } from './types.js';
|
|
16
12
|
|
|
17
13
|
export function createPluginHost(
|
|
@@ -22,8 +18,6 @@ export function createPluginHost(
|
|
|
22
18
|
isCloudSystemAgent,
|
|
23
19
|
isCloudMode,
|
|
24
20
|
parseOpenbotAuthMode,
|
|
25
|
-
resolveModelRegistry,
|
|
26
|
-
listApiKeyProvidersFromRegistry,
|
|
27
21
|
saveConfig,
|
|
28
22
|
getBaseDir,
|
|
29
23
|
resolvePath,
|
|
@@ -38,12 +38,6 @@ export interface PluginHost {
|
|
|
38
38
|
isCloudSystemAgent: (agentId: string) => boolean;
|
|
39
39
|
isCloudMode: () => boolean;
|
|
40
40
|
parseOpenbotAuthMode: (value: unknown) => 'credits' | 'byok';
|
|
41
|
-
resolveModelRegistry: () => ReturnType<
|
|
42
|
-
typeof import('./model-registry.js').resolveModelRegistry
|
|
43
|
-
>;
|
|
44
|
-
listApiKeyProvidersFromRegistry: (
|
|
45
|
-
registry: Awaited<ReturnType<PluginHost['resolveModelRegistry']>>,
|
|
46
|
-
) => Array<{ id: string; label: string }>;
|
|
47
41
|
saveConfig: (patch: Record<string, unknown>) => void;
|
|
48
42
|
getBaseDir: () => string;
|
|
49
43
|
resolvePath: (p: string) => string;
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DEFAULT_MARKETPLACE_REGISTRY_URL,
|
|
3
|
-
loadConfig,
|
|
4
|
-
} from '../../app/config.js';
|
|
5
|
-
import { OPENBOT_PLUGIN_ID } from '../../app/openbot-plugin.js';
|
|
6
|
-
import {
|
|
7
|
-
DEFAULT_CLOUD_OPENBOT_AUTH_MODE,
|
|
8
|
-
isCloudMode,
|
|
9
|
-
} from '../../app/cloud-mode.js';
|
|
10
|
-
import type { ConfigSchema, PluginDescriptor } from './domain.js';
|
|
11
|
-
|
|
12
|
-
export type RegistryProviderCatalog = Record<
|
|
13
|
-
string,
|
|
14
|
-
{
|
|
15
|
-
label: string;
|
|
16
|
-
models: Array<{ id: string; label: string; description: string }>;
|
|
17
|
-
}
|
|
18
|
-
>;
|
|
19
|
-
|
|
20
|
-
export type ModelRegistry = {
|
|
21
|
-
providers?: RegistryProviderCatalog;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export type RegistryModelOption = {
|
|
25
|
-
value: string;
|
|
26
|
-
label: string;
|
|
27
|
-
description?: string;
|
|
28
|
-
provider: string;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
let cachedRegistry: ModelRegistry | null = null;
|
|
32
|
-
let cacheUrl: string | null = null;
|
|
33
|
-
|
|
34
|
-
function getRegistryUrl(): string {
|
|
35
|
-
const { marketplaceRegistryUrl } = loadConfig();
|
|
36
|
-
return marketplaceRegistryUrl?.trim() || DEFAULT_MARKETPLACE_REGISTRY_URL;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export async function resolveModelRegistry(): Promise<ModelRegistry> {
|
|
40
|
-
const url = getRegistryUrl();
|
|
41
|
-
if (cachedRegistry && cacheUrl === url) return cachedRegistry;
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
const res = await fetch(url, {
|
|
45
|
-
headers: { Accept: 'application/json' },
|
|
46
|
-
signal: AbortSignal.timeout(15_000),
|
|
47
|
-
});
|
|
48
|
-
if (!res.ok) {
|
|
49
|
-
throw new Error(`Registry HTTP ${res.status} ${res.statusText}`);
|
|
50
|
-
}
|
|
51
|
-
cachedRegistry = (await res.json()) as ModelRegistry;
|
|
52
|
-
cacheUrl = url;
|
|
53
|
-
return cachedRegistry;
|
|
54
|
-
} catch (err) {
|
|
55
|
-
console.warn(
|
|
56
|
-
'[model-registry] fetch failed:',
|
|
57
|
-
err instanceof Error ? err.message : err,
|
|
58
|
-
);
|
|
59
|
-
return { providers: {} };
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function listModelOptionsFromRegistry(registry: ModelRegistry): RegistryModelOption[] {
|
|
64
|
-
const providers = registry.providers ?? {};
|
|
65
|
-
const out: RegistryModelOption[] = [];
|
|
66
|
-
|
|
67
|
-
for (const [providerId, provider] of Object.entries(providers)) {
|
|
68
|
-
for (const model of provider.models ?? []) {
|
|
69
|
-
out.push({
|
|
70
|
-
value: `${providerId}/${model.id}`,
|
|
71
|
-
label: `${provider.label} — ${model.label}`,
|
|
72
|
-
description: model.description,
|
|
73
|
-
provider: providerId,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return out;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export async function listRegistryModelOptions(): Promise<RegistryModelOption[]> {
|
|
82
|
-
const registry = await resolveModelRegistry();
|
|
83
|
-
return listModelOptionsFromRegistry(registry);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function listApiKeyProvidersFromRegistry(
|
|
87
|
-
registry: ModelRegistry,
|
|
88
|
-
): Array<{ id: string; label: string }> {
|
|
89
|
-
const providers = registry.providers ?? {};
|
|
90
|
-
return Object.entries(providers).map(([id, provider]) => ({
|
|
91
|
-
id,
|
|
92
|
-
label: provider.label,
|
|
93
|
-
}));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function pickDefaultModelValue(options: RegistryModelOption[]): string | undefined {
|
|
97
|
-
if (options.length === 0) return undefined;
|
|
98
|
-
const values = options.map((option) => option.value);
|
|
99
|
-
const preferred = values.find((value) => value.startsWith('openai/'));
|
|
100
|
-
return preferred ?? values[0];
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function enrichOpenbotPluginDescriptor(
|
|
104
|
-
descriptor: PluginDescriptor,
|
|
105
|
-
modelOptions: RegistryModelOption[],
|
|
106
|
-
): PluginDescriptor {
|
|
107
|
-
if (descriptor.id !== OPENBOT_PLUGIN_ID || !descriptor.configSchema) return descriptor;
|
|
108
|
-
|
|
109
|
-
const modelProperty = descriptor.configSchema.properties.model;
|
|
110
|
-
if (!modelProperty) return descriptor;
|
|
111
|
-
|
|
112
|
-
const values = modelOptions.map((option) => option.value);
|
|
113
|
-
const staticDefault =
|
|
114
|
-
typeof modelProperty.default === 'string' ? modelProperty.default : undefined;
|
|
115
|
-
const defaultModel =
|
|
116
|
-
staticDefault && values.includes(staticDefault)
|
|
117
|
-
? staticDefault
|
|
118
|
-
: pickDefaultModelValue(modelOptions) ?? staticDefault;
|
|
119
|
-
|
|
120
|
-
const nextModelProperty: ConfigSchema['properties'][string] = {
|
|
121
|
-
...modelProperty,
|
|
122
|
-
description: 'Model from the hosted marketplace registry.',
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
if (values.length > 0) {
|
|
126
|
-
nextModelProperty.enum = values;
|
|
127
|
-
nextModelProperty.options = modelOptions.map((option) => ({
|
|
128
|
-
label: option.label,
|
|
129
|
-
value: option.value,
|
|
130
|
-
description: option.description,
|
|
131
|
-
}));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (defaultModel) {
|
|
135
|
-
nextModelProperty.default = defaultModel;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const { model: _model, authMode: _authMode, ...otherProperties } =
|
|
139
|
-
descriptor.configSchema.properties;
|
|
140
|
-
|
|
141
|
-
const cloudAuthModeProperty: ConfigSchema['properties'][string] = {
|
|
142
|
-
type: 'string',
|
|
143
|
-
description: 'Credits uses OpenBot platform billing. BYOK uses your own API keys.',
|
|
144
|
-
enum: ['credits', 'byok'],
|
|
145
|
-
default: DEFAULT_CLOUD_OPENBOT_AUTH_MODE,
|
|
146
|
-
options: [
|
|
147
|
-
{
|
|
148
|
-
label: 'Credits',
|
|
149
|
-
value: 'credits',
|
|
150
|
-
description: 'Use OpenBot platform credits.',
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
label: 'Bring your own key',
|
|
154
|
-
value: 'byok',
|
|
155
|
-
description: 'Use your own provider API keys.',
|
|
156
|
-
},
|
|
157
|
-
],
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
const nextProperties: ConfigSchema['properties'] = isCloudMode()
|
|
161
|
-
? {
|
|
162
|
-
authMode: cloudAuthModeProperty,
|
|
163
|
-
model: nextModelProperty,
|
|
164
|
-
...otherProperties,
|
|
165
|
-
}
|
|
166
|
-
: {
|
|
167
|
-
model: nextModelProperty,
|
|
168
|
-
...otherProperties,
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
...descriptor,
|
|
173
|
-
configSchema: {
|
|
174
|
-
...descriptor.configSchema,
|
|
175
|
-
properties: nextProperties,
|
|
176
|
-
},
|
|
177
|
-
};
|
|
178
|
-
}
|