openbot 0.5.12 → 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.12');
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbot",
3
- "version": "0.5.12",
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.12"
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.12');
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,
@@ -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;