openbot 0.5.11 → 0.5.13

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/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.11');
21
+ program.name('openbot').description('OpenBot CLI').version('0.5.13');
22
22
  program
23
23
  .command('start')
24
24
  .description('Start the OpenBot harness')
@@ -0,0 +1,48 @@
1
+ import { getInvokeConfigOverrides } from '../harness/merge-plugin-config.js';
2
+ import { storageService } from '../plugins/storage/service.js';
3
+ /** Thread `state.json` key for sticky composer override values. */
4
+ export const THREAD_COMPOSER_CONFIG_KEY = 'composerConfig';
5
+ const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
6
+ export const readThreadComposerConfig = (state) => {
7
+ if (!isPlainObject(state))
8
+ return {};
9
+ const value = state[THREAD_COMPOSER_CONFIG_KEY];
10
+ return isPlainObject(value) ? value : {};
11
+ };
12
+ export const mergeComposerConfigMaps = (...layers) => Object.assign({}, ...layers.filter(isPlainObject));
13
+ /**
14
+ * Persists user invoke overrides on the thread and merges sticky config into the event
15
+ * so later messages without `data.config` still use the thread defaults.
16
+ */
17
+ export async function applyThreadComposerConfigToInvoke(options) {
18
+ const { channelId, threadId, event } = options;
19
+ if (event.type !== 'agent:invoke')
20
+ return event;
21
+ const invokeConfig = getInvokeConfigOverrides(event);
22
+ const details = await storageService.getThreadDetails({ channelId, threadId });
23
+ const sticky = readThreadComposerConfig(details.state);
24
+ if (invokeConfig) {
25
+ const nextSticky = mergeComposerConfigMaps(sticky, invokeConfig);
26
+ await storageService.patchThreadState({
27
+ channelId,
28
+ threadId,
29
+ state: { [THREAD_COMPOSER_CONFIG_KEY]: nextSticky },
30
+ });
31
+ return {
32
+ ...event,
33
+ data: {
34
+ ...event.data,
35
+ config: nextSticky,
36
+ },
37
+ };
38
+ }
39
+ if (Object.keys(sticky).length === 0)
40
+ return event;
41
+ return {
42
+ ...event,
43
+ data: {
44
+ ...event.data,
45
+ config: sticky,
46
+ },
47
+ };
48
+ }
@@ -7,7 +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
+ import { getEventInvokeConfig, getInvokeConfigOverrides, mergePluginConfig, readThreadInvokeConfig, THREAD_INVOKE_CONFIG_KEY, } from './merge-plugin-config.js';
11
11
  export { STATE_AGENT_ID, ORCHESTRATOR_AGENT_ID };
12
12
  async function emitEvent(chunk, state, { persistEvents, onEvent, parentAgentId, parentToolCallId, }) {
13
13
  ensureEventId(chunk);
@@ -50,6 +50,15 @@ export async function runAgent(options) {
50
50
  agentId,
51
51
  event,
52
52
  });
53
+ const storedInvokeConfig = readThreadInvokeConfig(state.threadDetails?.state);
54
+ const eventInvokeConfig = getEventInvokeConfig(event);
55
+ if (state.channelId && state.threadId && eventInvokeConfig) {
56
+ await storageService.patchThreadState({
57
+ channelId: state.channelId,
58
+ threadId: state.threadId,
59
+ state: { [THREAD_INVOKE_CONFIG_KEY]: eventInvokeConfig },
60
+ });
61
+ }
53
62
  // Shared per-thread abort signal so a stop request cancels this run and any
54
63
  // delegated sub-agent runs (which execute in the same channel/thread).
55
64
  const runKey = state.channelId ? abortKey(state.channelId, state.threadId) : runId;
@@ -69,7 +78,7 @@ export async function runAgent(options) {
69
78
  }
70
79
  const builder = melony().initialState(state);
71
80
  const pluginHost = createPluginHost(runAgent);
72
- const invokeConfigOverrides = getInvokeConfigOverrides(event);
81
+ const invokeConfigOverrides = getInvokeConfigOverrides(event, storedInvokeConfig);
73
82
  const pluginConfigs = Object.fromEntries(pluginRefs.map((ref) => [
74
83
  ref.id,
75
84
  mergePluginConfig(ref.config, invokeConfigOverrides, ref.id),
@@ -1,10 +1,20 @@
1
+ /** Thread `state.json` key for sticky per-thread `agent:invoke` `data.config`. */
2
+ export const THREAD_INVOKE_CONFIG_KEY = 'invokeConfig';
1
3
  function isPlainObject(value) {
2
4
  return typeof value === 'object' && value !== null && !Array.isArray(value);
3
5
  }
4
6
  function isPluginKeyedConfig(config) {
5
7
  return Object.values(config).every(isPlainObject);
6
8
  }
7
- export function getInvokeConfigOverrides(event) {
9
+ export function readThreadInvokeConfig(threadState) {
10
+ if (!isPlainObject(threadState))
11
+ return undefined;
12
+ const config = threadState[THREAD_INVOKE_CONFIG_KEY];
13
+ if (!isPlainObject(config) || Object.keys(config).length === 0)
14
+ return undefined;
15
+ return config;
16
+ }
17
+ export function getEventInvokeConfig(event) {
8
18
  if (event.type !== 'agent:invoke')
9
19
  return undefined;
10
20
  const config = event.data?.config;
@@ -12,6 +22,9 @@ export function getInvokeConfigOverrides(event) {
12
22
  return undefined;
13
23
  return config;
14
24
  }
25
+ export function getInvokeConfigOverrides(event, storedConfig) {
26
+ return getEventInvokeConfig(event) ?? storedConfig;
27
+ }
15
28
  export function mergePluginConfig(base, overrides, pluginId) {
16
29
  const baseConfig = base ?? {};
17
30
  if (!overrides)
@@ -4,11 +4,11 @@ import { DEFAULT_PLUGINS_DIR, DEFAULT_AGENTS_DIR, DEFAULT_CHANNELS_DIR, getBaseD
4
4
  import fs from 'node:fs/promises';
5
5
  import { readFileSync } from 'node:fs';
6
6
  import path from 'node:path';
7
- import { fileURLToPath, pathToFileURL } from 'node:url';
7
+ import { fileURLToPath } from 'node:url';
8
8
  import crypto from 'node:crypto';
9
9
  import matter from 'gray-matter';
10
10
  import { OPENBOT_PLUGIN_ID } from '../../app/openbot-plugin.js';
11
- import { listBuiltInPlugins, parsePluginModule } from '../../services/plugins/registry.js';
11
+ import { listBuiltInPlugins, resolvePlugin } from '../../services/plugins/registry.js';
12
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';
@@ -372,24 +372,22 @@ const listPluginsFromDisk = async () => {
372
372
  const ids = await listInstalledPluginIds(pluginsDir);
373
373
  const descriptors = await Promise.all(ids.map(async (id) => {
374
374
  try {
375
- const pluginDir = path.join(pluginsDir, id);
376
- const distPath = path.join(pluginDir, 'dist', 'index.js');
377
- const module = await import(pathToFileURL(distPath).href);
378
- const parsed = parsePluginModule(module);
379
- if (!parsed)
375
+ const plugin = await resolvePlugin(id);
376
+ if (!plugin)
380
377
  return null;
378
+ const pluginDir = path.join(pluginsDir, id);
381
379
  const [image, version] = await Promise.all([
382
380
  resolveEntityImageDataUrl(pluginDir),
383
381
  readPluginPackageVersion(pluginDir),
384
382
  ]);
385
383
  return {
386
384
  id,
387
- name: parsed.name || id,
388
- description: parsed.description || '',
385
+ name: plugin.name || id,
386
+ description: plugin.description || '',
389
387
  builtIn: false,
390
388
  version,
391
- image: parsed.image || image,
392
- configSchema: parsed.configSchema,
389
+ image: plugin.image || image,
390
+ configSchema: plugin.configSchema,
393
391
  createdAt: new Date(),
394
392
  updatedAt: new Date(),
395
393
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbot",
3
- "version": "0.5.11",
3
+ "version": "0.5.13",
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.11"
14
+ "push": "fly deploy --build-only --push --config deploy/fly.toml -a openbot-runtime --image-label v0.5.13"
15
15
  },
16
16
  "bin": {
17
17
  "openbot": "./dist/app/cli.js"
package/src/app/cli.ts CHANGED
@@ -27,7 +27,7 @@ function checkNodeVersion() {
27
27
 
28
28
  checkNodeVersion();
29
29
 
30
- program.name('openbot').description('OpenBot CLI').version('0.5.11');
30
+ program.name('openbot').description('OpenBot CLI').version('0.5.13');
31
31
 
32
32
  program
33
33
  .command('start')
@@ -9,7 +9,13 @@ 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
+ import {
13
+ getEventInvokeConfig,
14
+ getInvokeConfigOverrides,
15
+ mergePluginConfig,
16
+ readThreadInvokeConfig,
17
+ THREAD_INVOKE_CONFIG_KEY,
18
+ } from './merge-plugin-config.js';
13
19
 
14
20
  export { STATE_AGENT_ID, ORCHESTRATOR_AGENT_ID };
15
21
 
@@ -87,6 +93,16 @@ export async function runAgent(options: RunAgentOptions): Promise<void> {
87
93
  event,
88
94
  });
89
95
 
96
+ const storedInvokeConfig = readThreadInvokeConfig(state.threadDetails?.state);
97
+ const eventInvokeConfig = getEventInvokeConfig(event);
98
+ if (state.channelId && state.threadId && eventInvokeConfig) {
99
+ await storageService.patchThreadState({
100
+ channelId: state.channelId,
101
+ threadId: state.threadId,
102
+ state: { [THREAD_INVOKE_CONFIG_KEY]: eventInvokeConfig },
103
+ });
104
+ }
105
+
90
106
  // Shared per-thread abort signal so a stop request cancels this run and any
91
107
  // delegated sub-agent runs (which execute in the same channel/thread).
92
108
  const runKey = state.channelId ? abortKey(state.channelId, state.threadId) : runId;
@@ -115,7 +131,7 @@ export async function runAgent(options: RunAgentOptions): Promise<void> {
115
131
  const builder = melony<OpenBotState, OpenBotEvent>().initialState(state);
116
132
 
117
133
  const pluginHost = createPluginHost(runAgent);
118
- const invokeConfigOverrides = getInvokeConfigOverrides(event);
134
+ const invokeConfigOverrides = getInvokeConfigOverrides(event, storedInvokeConfig);
119
135
  const pluginConfigs = Object.fromEntries(
120
136
  pluginRefs.map((ref) => [
121
137
  ref.id,
@@ -1,5 +1,8 @@
1
1
  import { OpenBotEvent } from '../app/types.js';
2
2
 
3
+ /** Thread `state.json` key for sticky per-thread `agent:invoke` `data.config`. */
4
+ export const THREAD_INVOKE_CONFIG_KEY = 'invokeConfig';
5
+
3
6
  function isPlainObject(value: unknown): value is Record<string, unknown> {
4
7
  return typeof value === 'object' && value !== null && !Array.isArray(value);
5
8
  }
@@ -8,7 +11,16 @@ function isPluginKeyedConfig(config: Record<string, unknown>): boolean {
8
11
  return Object.values(config).every(isPlainObject);
9
12
  }
10
13
 
11
- export function getInvokeConfigOverrides(event: OpenBotEvent): Record<string, unknown> | undefined {
14
+ export function readThreadInvokeConfig(threadState: unknown): Record<string, unknown> | undefined {
15
+ if (!isPlainObject(threadState)) return undefined;
16
+
17
+ const config = threadState[THREAD_INVOKE_CONFIG_KEY];
18
+ if (!isPlainObject(config) || Object.keys(config).length === 0) return undefined;
19
+
20
+ return config;
21
+ }
22
+
23
+ export function getEventInvokeConfig(event: OpenBotEvent): Record<string, unknown> | undefined {
12
24
  if (event.type !== 'agent:invoke') return undefined;
13
25
 
14
26
  const config = event.data?.config;
@@ -17,6 +29,13 @@ export function getInvokeConfigOverrides(event: OpenBotEvent): Record<string, un
17
29
  return config;
18
30
  }
19
31
 
32
+ export function getInvokeConfigOverrides(
33
+ event: OpenBotEvent,
34
+ storedConfig?: Record<string, unknown>,
35
+ ): Record<string, unknown> | undefined {
36
+ return getEventInvokeConfig(event) ?? storedConfig;
37
+ }
38
+
20
39
  export function mergePluginConfig(
21
40
  base: Record<string, unknown> | undefined,
22
41
  overrides: Record<string, unknown> | undefined,
@@ -17,7 +17,7 @@ import {
17
17
  import fs from 'node:fs/promises';
18
18
  import { readFileSync } from 'node:fs';
19
19
  import path from 'node:path';
20
- import { fileURLToPath, pathToFileURL } from 'node:url';
20
+ import { fileURLToPath } from 'node:url';
21
21
  import crypto from 'node:crypto';
22
22
  import matter from 'gray-matter';
23
23
  import {
@@ -32,7 +32,7 @@ import {
32
32
  } from '../../services/plugins/domain.js';
33
33
  import type { PluginRef } from '../../services/plugins/types.js';
34
34
  import { OPENBOT_PLUGIN_ID } from '../../app/openbot-plugin.js';
35
- import { listBuiltInPlugins, parsePluginModule } from '../../services/plugins/registry.js';
35
+ import { listBuiltInPlugins, resolvePlugin } from '../../services/plugins/registry.js';
36
36
  import { enrichOpenbotPluginDescriptor } from '../../services/plugins/enrich-openbot-plugin.js';
37
37
  import { OpenBotEvent, OpenBotState } from '../../app/types.js';
38
38
  import { processService } from '../../services/process.js';
@@ -456,23 +456,21 @@ const listPluginsFromDisk = async (): Promise<PluginDescriptor[]> => {
456
456
  const descriptors = await Promise.all(
457
457
  ids.map(async (id): Promise<PluginDescriptor | null> => {
458
458
  try {
459
+ const plugin = await resolvePlugin(id);
460
+ if (!plugin) return null;
459
461
  const pluginDir = path.join(pluginsDir, id);
460
- const distPath = path.join(pluginDir, 'dist', 'index.js');
461
- const module = await import(pathToFileURL(distPath).href);
462
- const parsed = parsePluginModule(module as Record<string, unknown>);
463
- if (!parsed) return null;
464
462
  const [image, version] = await Promise.all([
465
463
  resolveEntityImageDataUrl(pluginDir),
466
464
  readPluginPackageVersion(pluginDir),
467
465
  ]);
468
466
  return {
469
467
  id,
470
- name: parsed.name || id,
471
- description: parsed.description || '',
468
+ name: plugin.name || id,
469
+ description: plugin.description || '',
472
470
  builtIn: false,
473
471
  version,
474
- image: parsed.image || image,
475
- configSchema: parsed.configSchema,
472
+ image: plugin.image || image,
473
+ configSchema: plugin.configSchema,
476
474
  createdAt: new Date(),
477
475
  updatedAt: new Date(),
478
476
  };
@@ -88,6 +88,8 @@ export type ThreadState = {
88
88
  name?: string;
89
89
  /** Sticky agent id for this thread (`system` = orchestrator). Set once, then enforced on publish. */
90
90
  respondingAgentId?: string;
91
+ /** Last `agent:invoke` `data.config` for this thread; reused when omitted on later invokes. */
92
+ invokeConfig?: Record<string, unknown>;
91
93
  pendingToolCallIds?: string[];
92
94
  usage?: {
93
95
  promptTokens?: number;