openbot 0.3.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +15 -16
  2. package/dist/app/agent-ids.js +4 -0
  3. package/dist/app/cli.js +1 -1
  4. package/dist/app/config.js +0 -19
  5. package/dist/app/server.js +8 -14
  6. package/dist/assets/icon.svg +9 -3
  7. package/dist/bus/services.js +78 -132
  8. package/dist/harness/agent-invoke-run.js +44 -0
  9. package/dist/harness/agent-turn.js +99 -0
  10. package/dist/harness/channel-participants.js +40 -0
  11. package/dist/harness/constants.js +2 -0
  12. package/dist/harness/context-meter.js +97 -0
  13. package/dist/harness/context.js +98 -45
  14. package/dist/harness/dispatch.js +144 -0
  15. package/dist/harness/dispatcher.js +45 -156
  16. package/dist/harness/history.js +177 -0
  17. package/dist/harness/index.js +91 -0
  18. package/dist/harness/orchestration.js +88 -0
  19. package/dist/harness/participants.js +22 -0
  20. package/dist/harness/run-harness.js +154 -0
  21. package/dist/harness/run.js +98 -0
  22. package/dist/harness/runtime-factory.js +0 -34
  23. package/dist/harness/runtime.js +57 -0
  24. package/dist/harness/todo-dispatch.js +51 -0
  25. package/dist/harness/todos.js +5 -0
  26. package/dist/harness/turn.js +79 -0
  27. package/dist/plugins/approval/index.js +105 -149
  28. package/dist/plugins/delegation/index.js +119 -32
  29. package/dist/plugins/memory/index.js +103 -14
  30. package/dist/plugins/memory/service.js +152 -0
  31. package/dist/plugins/openbot/context.js +80 -0
  32. package/dist/plugins/openbot/history.js +98 -0
  33. package/dist/plugins/openbot/index.js +31 -0
  34. package/dist/plugins/openbot/runtime.js +317 -0
  35. package/dist/plugins/openbot/system-prompt.js +5 -0
  36. package/dist/plugins/plugin-manager/index.js +105 -0
  37. package/dist/plugins/storage/index.js +573 -0
  38. package/dist/plugins/storage/service.js +1159 -0
  39. package/dist/plugins/storage-tools/index.js +2 -2
  40. package/dist/plugins/thread-namer/index.js +72 -0
  41. package/dist/plugins/thread-naming/generate-title.js +44 -0
  42. package/dist/plugins/thread-naming/index.js +103 -0
  43. package/dist/plugins/threads/index.js +114 -0
  44. package/dist/plugins/todo/index.js +24 -25
  45. package/dist/plugins/ui/index.js +2 -32
  46. package/dist/registry/plugins.js +3 -9
  47. package/dist/services/plugins/domain.js +1 -0
  48. package/dist/services/plugins/plugin-cache.js +9 -0
  49. package/dist/services/plugins/registry.js +110 -0
  50. package/dist/services/plugins/service.js +177 -0
  51. package/dist/services/plugins/types.js +1 -0
  52. package/dist/services/process.js +29 -0
  53. package/dist/services/storage.js +41 -15
  54. package/dist/services/thread-naming.js +81 -0
  55. package/docs/agents.md +16 -10
  56. package/docs/architecture.md +2 -2
  57. package/docs/plugins.md +6 -15
  58. package/docs/templates/AGENT.example.md +7 -13
  59. package/package.json +1 -2
  60. package/src/app/agent-ids.ts +5 -0
  61. package/src/app/cli.ts +1 -1
  62. package/src/app/config.ts +1 -31
  63. package/src/app/server.ts +8 -16
  64. package/src/app/types.ts +70 -190
  65. package/src/assets/icon.svg +9 -3
  66. package/src/harness/index.ts +145 -0
  67. package/src/plugins/approval/index.ts +91 -189
  68. package/src/plugins/delegation/index.ts +136 -39
  69. package/src/plugins/memory/index.ts +112 -15
  70. package/src/{services/memory.ts → plugins/memory/service.ts} +1 -1
  71. package/src/plugins/openbot/context.ts +91 -0
  72. package/src/plugins/openbot/history.ts +107 -0
  73. package/src/plugins/openbot/index.ts +37 -0
  74. package/src/plugins/openbot/runtime.ts +384 -0
  75. package/src/plugins/openbot/system-prompt.ts +7 -0
  76. package/src/plugins/plugin-manager/index.ts +122 -0
  77. package/src/plugins/shell/index.ts +1 -1
  78. package/src/plugins/storage/index.ts +633 -0
  79. package/src/{services/storage.ts → plugins/storage/service.ts} +257 -72
  80. package/src/{bus/types.ts → services/plugins/domain.ts} +20 -7
  81. package/src/services/plugins/plugin-cache.ts +13 -0
  82. package/src/{registry/plugins.ts → services/plugins/registry.ts} +25 -27
  83. package/src/services/{plugins.ts → plugins/service.ts} +96 -2
  84. package/src/{bus/plugin.ts → services/plugins/types.ts} +3 -3
  85. package/src/bus/services.ts +0 -908
  86. package/src/harness/context.ts +0 -356
  87. package/src/harness/dispatcher.ts +0 -379
  88. package/src/harness/mcp.ts +0 -78
  89. package/src/harness/runtime-factory.ts +0 -129
  90. package/src/harness/todo-advance.ts +0 -128
  91. package/src/plugins/ai-sdk/index.ts +0 -41
  92. package/src/plugins/ai-sdk/runtime.ts +0 -468
  93. package/src/plugins/ai-sdk/system-prompt.ts +0 -18
  94. package/src/plugins/mcp/index.ts +0 -128
  95. package/src/plugins/storage-tools/index.ts +0 -90
  96. package/src/plugins/todo/index.ts +0 -64
  97. package/src/plugins/ui/index.ts +0 -227
  98. /package/src/{harness → services}/process.ts +0 -0
@@ -1,3 +1,4 @@
1
+ import { ORCHESTRATOR_AGENT_ID, STATE_AGENT_ID } from '../../app/agent-ids.js';
1
2
  import {
2
3
  DEFAULT_PLUGINS_DIR,
3
4
  DEFAULT_AGENTS_DIR,
@@ -7,7 +8,7 @@ import {
7
8
  resolvePath,
8
9
  StoredVariable,
9
10
  VARIABLES_FILE,
10
- } from '../app/config.js';
11
+ } from '../../app/config.js';
11
12
  import fs from 'node:fs/promises';
12
13
  import { readFileSync } from 'node:fs';
13
14
  import path from 'node:path';
@@ -22,14 +23,14 @@ import {
22
23
  PluginDescriptor,
23
24
  Thread,
24
25
  ThreadDetails,
25
- } from '../bus/types.js';
26
- import type { PluginRef } from '../bus/plugin.js';
27
- import { aiSdkPlugin } from '../plugins/ai-sdk/index.js';
28
- import { AI_SDK_SYSTEM_PROMPT } from '../plugins/ai-sdk/system-prompt.js';
29
- import { listBuiltInPlugins, parsePluginModule } from '../registry/plugins.js';
30
- import { OpenBotEvent, OpenBotState } from '../app/types.js';
31
- import { processService } from '../harness/process.js';
32
- import { memoryService } from './memory.js';
26
+ } from '../../services/plugins/domain.js';
27
+ import type { PluginRef } from '../../services/plugins/types.js';
28
+ import { openbotPlugin } from '../openbot/index.js';
29
+ import { OPENBOT_SYSTEM_PROMPT } from '../openbot/system-prompt.js';
30
+ import { listBuiltInPlugins, parsePluginModule } from '../../services/plugins/registry.js';
31
+ import { OpenBotEvent, OpenBotState } from '../../app/types.js';
32
+ import { processService } from '../../services/process.js';
33
+ import { memoryService } from '../memory/service.js';
33
34
 
34
35
  const resolveBaseDir = () => {
35
36
  const config = loadConfig();
@@ -49,7 +50,10 @@ function getBundledSystemAgentImage(): string | undefined {
49
50
  if (bundledSystemAgentImageLoaded) return bundledSystemAgentImage;
50
51
  bundledSystemAgentImageLoaded = true;
51
52
  try {
52
- const iconPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '../assets/icon.svg');
53
+ const iconPath = path.join(
54
+ path.dirname(fileURLToPath(import.meta.url)),
55
+ '../../assets/icon.svg',
56
+ );
53
57
  const trimmed = readFileSync(iconPath, 'utf-8').trim();
54
58
  if (!trimmed.startsWith('<svg')) return undefined;
55
59
  bundledSystemAgentImage = toSvgDataUrl(trimmed);
@@ -103,27 +107,34 @@ const getConversationDir = (channelId: string, threadId?: string) => {
103
107
  };
104
108
 
105
109
  /** Built-in orchestrator agent id. Not creatable as a normal disk agent. */
106
- const SYSTEM_AGENT_ID = 'system';
110
+ const SYSTEM_AGENT_ID = ORCHESTRATOR_AGENT_ID;
107
111
 
108
112
  const SYSTEM_DEFAULT_PLUGINS: PluginRef[] = [
109
- { id: 'ai-sdk', config: { model: 'openai/gpt-5.4-nano' } },
110
- { id: 'storage-tools' },
111
- // { id: 'mcp' },
113
+ { id: 'openbot', config: { model: 'openai/gpt-5.4-mini' } },
112
114
  { id: 'shell' },
113
- { id: 'todo' },
114
- // { id: 'ui' },
115
115
  { id: 'approval' },
116
116
  { id: 'memory' },
117
+ { id: 'delegation' },
118
+ { id: 'storage' },
119
+ ];
120
+
121
+ /** No `openbot` / `shell` — storage-side effects and infra plugins only. */
122
+ const STATE_DEFAULT_PLUGINS: PluginRef[] = [
123
+ { id: 'storage' },
124
+ { id: 'plugin-manager' },
117
125
  ];
118
126
 
127
+ const STATE_AGENT_INSTRUCTIONS =
128
+ 'Built-in infra agent for deterministic state reads. No conversational model is attached; handle storage, approvals, memory, and plugin marketplace events.';
129
+
119
130
  function getSystemAgentDetails(overrides?: Partial<AgentDetails>): AgentDetails {
120
131
  const defaults: AgentDetails = {
121
132
  id: SYSTEM_AGENT_ID,
122
133
  name: 'OpenBot',
123
134
  image: getBundledSystemAgentImage(),
124
135
  description:
125
- 'First-party orchestration agent for OpenBot. Coordinates other agents via handoff.',
126
- instructions: AI_SDK_SYSTEM_PROMPT,
136
+ 'First-party orchestration agent for OpenBot.',
137
+ instructions: OPENBOT_SYSTEM_PROMPT,
127
138
  plugins: SYSTEM_DEFAULT_PLUGINS.map((ref) => ref.id),
128
139
  pluginRefs: SYSTEM_DEFAULT_PLUGINS,
129
140
  createdAt: new Date(),
@@ -136,10 +147,15 @@ function getSystemAgentDetails(overrides?: Partial<AgentDetails>): AgentDetails
136
147
  ? overrides.pluginRefs
137
148
  : defaults.pluginRefs;
138
149
 
150
+ const diskInstructions = overrides.instructions?.trim();
151
+ const instructions =
152
+ diskInstructions && diskInstructions.length > 0 ? diskInstructions : defaults.instructions;
153
+
139
154
  return {
140
155
  ...defaults,
141
156
  ...overrides,
142
157
  id: SYSTEM_AGENT_ID,
158
+ instructions,
143
159
  image: overrides.image || defaults.image,
144
160
  plugins: refs.map((ref) => ref.id),
145
161
  pluginRefs: refs,
@@ -147,18 +163,65 @@ function getSystemAgentDetails(overrides?: Partial<AgentDetails>): AgentDetails
147
163
  };
148
164
  }
149
165
 
150
- // Suppress unused warning until system agent customization re-uses aiSdkPlugin metadata.
151
- void aiSdkPlugin;
166
+ function getStateAgentDetails(overrides?: Partial<AgentDetails>): AgentDetails {
167
+ const defaults: AgentDetails = {
168
+ id: STATE_AGENT_ID,
169
+ name: 'State',
170
+ image: getBundledSystemAgentImage(),
171
+ description: 'Infrastructure agent for OpenBot — storage and hooks without an LLM.',
172
+ instructions: STATE_AGENT_INSTRUCTIONS,
173
+ plugins: STATE_DEFAULT_PLUGINS.map((ref) => ref.id),
174
+ pluginRefs: STATE_DEFAULT_PLUGINS,
175
+ hidden: true,
176
+ createdAt: new Date(),
177
+ updatedAt: new Date(),
178
+ };
152
179
 
153
- const RESERVED_DISK_AGENT_IDS = new Set([SYSTEM_AGENT_ID]);
180
+ if (!overrides) return defaults;
181
+
182
+ const refs = overrides.pluginRefs && overrides.pluginRefs.length > 0
183
+ ? overrides.pluginRefs
184
+ : defaults.pluginRefs;
154
185
 
155
- const assertValidDiskAgentId = (agentId: string): void => {
186
+ const diskInstructions = overrides.instructions?.trim();
187
+ const instructions =
188
+ diskInstructions && diskInstructions.length > 0 ? diskInstructions : defaults.instructions;
189
+
190
+ return {
191
+ ...defaults,
192
+ ...overrides,
193
+ id: STATE_AGENT_ID,
194
+ instructions,
195
+ image: overrides.image || defaults.image,
196
+ hidden: overrides.hidden !== undefined ? overrides.hidden : defaults.hidden,
197
+ plugins: refs.map((ref) => ref.id),
198
+ pluginRefs: refs,
199
+ updatedAt: new Date(),
200
+ };
201
+ }
202
+
203
+ const agentSummaryFromDetails = (details: AgentDetails): Agent => ({
204
+ id: details.id,
205
+ name: details.name || details.id,
206
+ description: details.description || '',
207
+ image: details.image,
208
+ plugins: details.plugins,
209
+ hidden: details.hidden,
210
+ createdAt: details.createdAt,
211
+ updatedAt: details.updatedAt,
212
+ });
213
+
214
+ // Suppress unused warning until system agent customization re-uses openbotPlugin metadata.
215
+ void openbotPlugin;
216
+
217
+ /** Built-in agents may persist optional `agents/<id>/AGENT.md` overlays; read path merges them with defaults. */
218
+ const isBuiltinOverlayAgentId = (agentId: string): boolean =>
219
+ agentId === SYSTEM_AGENT_ID || agentId === STATE_AGENT_ID;
220
+
221
+ const assertAgentIdFormat = (agentId: string): void => {
156
222
  if (!agentId || typeof agentId !== 'string') {
157
223
  throw new Error('agentId is required');
158
224
  }
159
- if (RESERVED_DISK_AGENT_IDS.has(agentId)) {
160
- throw new Error(`Agent id "${agentId}" is reserved`);
161
- }
162
225
  if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) {
163
226
  throw new Error('agentId must contain only letters, digits, underscores, and hyphens');
164
227
  }
@@ -174,9 +237,7 @@ const THREAD_TITLE_MAX_LENGTH = 80;
174
237
  const buildThreadTitleFromEvent = (event: OpenBotEvent): string | undefined => {
175
238
  let rawContent = '';
176
239
 
177
- if (event.type === 'user:input' && typeof event.data?.content === 'string') {
178
- rawContent = event.data.content;
179
- } else if (
240
+ if (
180
241
  event.type === 'agent:invoke' &&
181
242
  event.data?.role === 'user' &&
182
243
  typeof event.data.content === 'string'
@@ -316,10 +377,35 @@ const listPluginsFromDisk = async (): Promise<PluginDescriptor[]> => {
316
377
  const isRecord = (value: unknown): value is Record<string, unknown> =>
317
378
  !!value && typeof value === 'object' && !Array.isArray(value);
318
379
 
380
+ /** Display-oriented fields persisted in a channel's `state.json`. */
381
+ const readChannelStateFileFields = (
382
+ parsed: unknown,
383
+ ): { name?: string; cwd?: string; participants: string[] } => {
384
+ if (!isRecord(parsed)) {
385
+ return { participants: [] };
386
+ }
387
+ const name =
388
+ typeof parsed.name === 'string' && parsed.name.trim() ? parsed.name.trim() : undefined;
389
+ const cwd = typeof parsed.cwd === 'string' ? parsed.cwd : undefined;
390
+ const participants: string[] = [];
391
+ if (Array.isArray(parsed.participants)) {
392
+ for (const x of parsed.participants) {
393
+ if (typeof x === 'string' && x.trim()) participants.push(x.trim());
394
+ }
395
+ }
396
+ return { name, cwd, participants };
397
+ };
398
+
319
399
  /**
320
400
  * Parse the `plugins:` array from AGENT.md frontmatter. Each entry must have an
321
401
  * `id`; `config` is optional. Strings are accepted as a shorthand for `{ id }`.
322
402
  */
403
+ const parseHiddenFlag = (raw: unknown): boolean | undefined => {
404
+ if (raw === true) return true;
405
+ if (raw === false) return false;
406
+ return undefined;
407
+ };
408
+
323
409
  const parsePluginRefs = (raw: unknown): PluginRef[] => {
324
410
  if (!Array.isArray(raw)) return [];
325
411
  const refs: PluginRef[] = [];
@@ -376,20 +462,26 @@ export const storageService = {
376
462
  const channelDir = getConversationDir(name);
377
463
  const statePath = path.join(channelDir, 'state.json');
378
464
  let cwd: string | undefined;
465
+ let displayName = name;
466
+ let participants: string[] = [];
379
467
 
380
468
  try {
381
469
  const stateContent = await fs.readFile(statePath, 'utf-8');
382
- const state = JSON.parse(stateContent);
383
- cwd = typeof state.cwd === 'string' ? state.cwd : undefined;
470
+ const parsed = JSON.parse(stateContent);
471
+ const fields = readChannelStateFileFields(parsed);
472
+ cwd = fields.cwd;
473
+ displayName = fields.name ?? name;
474
+ participants = fields.participants;
384
475
  } catch {
385
476
  // ignore
386
477
  }
387
478
 
388
479
  const channel: Channel = {
389
480
  id: name,
390
- name: name,
481
+ name: displayName,
391
482
  description: '',
392
483
  cwd,
484
+ participants,
393
485
  createdAt: new Date(),
394
486
  updatedAt: new Date(),
395
487
  };
@@ -497,7 +589,7 @@ export const storageService = {
497
589
 
498
590
  const baseState: Record<string, unknown> = { ...(initialState || {}) };
499
591
  if (threadTitle?.trim()) {
500
- baseState.generatedName = threadTitle.trim();
592
+ baseState.name = threadTitle.trim();
501
593
  }
502
594
 
503
595
  await fs.mkdir(threadDir, { recursive: true });
@@ -525,10 +617,10 @@ export const storageService = {
525
617
  try {
526
618
  const threadStateRaw = await fs.readFile(threadStatePath, 'utf-8');
527
619
  const threadState = JSON.parse(threadStateRaw) as Record<string, unknown>;
528
- const generatedName =
529
- typeof threadState.generatedName === 'string' ? threadState.generatedName.trim() : '';
530
- if (generatedName) {
531
- threadDisplayName = generatedName;
620
+ const threadName =
621
+ typeof threadState.name === 'string' ? threadState.name.trim() : '';
622
+ if (threadName) {
623
+ threadDisplayName = threadName;
532
624
  }
533
625
  } catch (error: unknown) {
534
626
  if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') {
@@ -574,14 +666,14 @@ export const storageService = {
574
666
  }
575
667
  }
576
668
 
577
- const generatedName =
578
- isRecord(state) && typeof state.generatedName === 'string'
579
- ? state.generatedName.trim()
669
+ const threadName =
670
+ isRecord(state) && typeof state.name === 'string'
671
+ ? state.name.trim()
580
672
  : '';
581
673
 
582
674
  return {
583
675
  id: threadId,
584
- name: generatedName || threadId,
676
+ name: threadName || threadId,
585
677
  channelId,
586
678
  state,
587
679
  };
@@ -610,14 +702,17 @@ export const storageService = {
610
702
  }
611
703
  }
612
704
 
613
- const cwd = isRecord(state) && typeof state.cwd === 'string' ? state.cwd : undefined;
705
+ const diskFields = readChannelStateFileFields(state);
706
+ const cwd = diskFields.cwd;
707
+ const displayName = diskFields.name ?? channelId;
614
708
 
615
709
  const details: ChannelDetails = {
616
710
  id: channelId,
617
- name: channelId,
711
+ name: displayName,
618
712
  spec,
619
713
  state,
620
714
  cwd,
715
+ participants: diskFields.participants,
621
716
  };
622
717
 
623
718
  details.threads = await storageService.getThreads({ channelId });
@@ -713,15 +808,7 @@ export const storageService = {
713
808
  agentIds.map(async (id) => {
714
809
  try {
715
810
  const details = await storageService.getAgentDetails({ agentId: id });
716
- return {
717
- id,
718
- name: details.name || id,
719
- description: details.description || '',
720
- image: details.image,
721
- plugins: details.plugins,
722
- createdAt: details.createdAt,
723
- updatedAt: details.updatedAt,
724
- } satisfies Agent;
811
+ return agentSummaryFromDetails(details);
725
812
  } catch {
726
813
  return {
727
814
  id,
@@ -736,23 +823,19 @@ export const storageService = {
736
823
  );
737
824
 
738
825
  const system = await storageService.getAgentDetails({ agentId: SYSTEM_AGENT_ID });
739
- const builtInSystemAgent: Agent = {
740
- id: system.id,
741
- name: system.name,
742
- description: system.description || '',
743
- image: system.image,
744
- plugins: system.plugins,
745
- createdAt: system.createdAt,
746
- updatedAt: system.updatedAt,
747
- };
826
+ const builtInSystemAgent = agentSummaryFromDetails(system);
827
+
828
+ const builtInStateRow = await storageService.getAgentDetails({ agentId: STATE_AGENT_ID });
829
+ const builtInStateAgent = agentSummaryFromDetails(builtInStateRow);
748
830
 
749
831
  const deduped = new Map<string, Agent>();
750
832
  deduped.set(builtInSystemAgent.id, builtInSystemAgent);
833
+ deduped.set(builtInStateAgent.id, builtInStateAgent);
751
834
  for (const agent of agents) {
752
835
  if (!deduped.has(agent.id)) deduped.set(agent.id, agent);
753
836
  }
754
837
 
755
- return Array.from(deduped.values());
838
+ return Array.from(deduped.values()).filter((agent) => !agent.hidden);
756
839
  },
757
840
  getPlugins: async (): Promise<PluginDescriptor[]> => {
758
841
  const [builtIn, fromDisk] = await Promise.all([
@@ -796,16 +879,17 @@ export const storageService = {
796
879
  pluginRefs,
797
880
  description: typeof data.description === 'string' ? data.description : '',
798
881
  image: frontmatterImage || discoveredImage || undefined,
882
+ hidden: parseHiddenFlag(data.hidden),
799
883
  createdAt: stats.birthtime,
800
884
  updatedAt: stats.mtime,
801
885
  };
802
886
  } catch (error) {
803
- if (agentId !== SYSTEM_AGENT_ID) {
887
+ if (agentId !== SYSTEM_AGENT_ID && agentId !== STATE_AGENT_ID) {
804
888
  const err = new Error(`Agent "${agentId}" does not exist.`);
805
889
  (err as Error & { code?: string }).code = 'AGENT_NOT_FOUND';
806
890
  throw err;
807
891
  }
808
- // swallow: system agent has on-disk overrides optional
892
+ // swallow: built-in agents have optional `agents/<id>/AGENT.md` overrides
809
893
  void error;
810
894
  }
811
895
 
@@ -813,6 +897,10 @@ export const storageService = {
813
897
  return getSystemAgentDetails(diskDetails);
814
898
  }
815
899
 
900
+ if (agentId === STATE_AGENT_ID) {
901
+ return getStateAgentDetails(diskDetails);
902
+ }
903
+
816
904
  if (!diskDetails) {
817
905
  const error = new Error(`Agent "${agentId}" does not exist.`);
818
906
  (error as Error & { code?: string }).code = 'AGENT_NOT_FOUND';
@@ -826,6 +914,7 @@ export const storageService = {
826
914
  name,
827
915
  description = '',
828
916
  image,
917
+ hidden,
829
918
  instructions,
830
919
  plugins,
831
920
  }: {
@@ -833,10 +922,11 @@ export const storageService = {
833
922
  name: string;
834
923
  description?: string;
835
924
  image?: string;
925
+ hidden?: boolean;
836
926
  instructions: string;
837
927
  plugins: PluginRef[];
838
928
  }): Promise<void> => {
839
- assertValidDiskAgentId(agentId);
929
+ assertAgentIdFormat(agentId);
840
930
  const agentDir = resolvePath(path.join(getAgentsRootDir(), agentId));
841
931
  const agentMdPath = path.join(agentDir, 'AGENT.md');
842
932
 
@@ -864,6 +954,9 @@ export const storageService = {
864
954
  if (typeof image === 'string' && image.trim() !== '') {
865
955
  data.image = image.trim();
866
956
  }
957
+ if (hidden === true) {
958
+ data.hidden = true;
959
+ }
867
960
 
868
961
  const body = matter.stringify(`${instructions.trim()}\n`, data);
869
962
  await fs.writeFile(agentMdPath, body, 'utf-8');
@@ -873,6 +966,7 @@ export const storageService = {
873
966
  name,
874
967
  description,
875
968
  image,
969
+ hidden,
876
970
  instructions,
877
971
  plugins,
878
972
  }: {
@@ -880,10 +974,11 @@ export const storageService = {
880
974
  name?: string;
881
975
  description?: string;
882
976
  image?: string;
977
+ hidden?: boolean;
883
978
  instructions?: string;
884
979
  plugins?: PluginRef[];
885
980
  }): Promise<void> => {
886
- assertValidDiskAgentId(agentId);
981
+ assertAgentIdFormat(agentId);
887
982
  const agentDir = resolvePath(path.join(getAgentsRootDir(), agentId));
888
983
  const agentMdPath = path.join(agentDir, 'AGENT.md');
889
984
 
@@ -892,14 +987,18 @@ export const storageService = {
892
987
  raw = await fs.readFile(agentMdPath, 'utf-8');
893
988
  } catch (error: unknown) {
894
989
  if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
895
- const err = new Error(`Agent "${agentId}" does not exist.`);
896
- (err as Error & { code?: string }).code = 'AGENT_NOT_FOUND';
897
- throw err;
990
+ if (!isBuiltinOverlayAgentId(agentId)) {
991
+ const err = new Error(`Agent "${agentId}" does not exist.`);
992
+ (err as Error & { code?: string }).code = 'AGENT_NOT_FOUND';
993
+ throw err;
994
+ }
995
+ raw = '';
996
+ } else {
997
+ throw error;
898
998
  }
899
- throw error;
900
999
  }
901
1000
 
902
- const parsed = matter(raw);
1001
+ const parsed = raw === '' ? { data: {}, content: '' } : matter(raw);
903
1002
  const nextData: Record<string, unknown> = { ...parsed.data };
904
1003
  if (name !== undefined) nextData.name = name;
905
1004
  if (description !== undefined) nextData.description = description;
@@ -911,17 +1010,84 @@ export const storageService = {
911
1010
  delete nextData.image;
912
1011
  }
913
1012
  }
1013
+ if (hidden !== undefined) {
1014
+ if (hidden) {
1015
+ nextData.hidden = true;
1016
+ } else {
1017
+ delete nextData.hidden;
1018
+ }
1019
+ }
1020
+
1021
+ let nextContent = instructions !== undefined ? instructions : parsed.content;
1022
+
1023
+ // Built-in agents merge disk overlays with code defaults on read; on write, partial
1024
+ // updates (e.g. plugins-only) must still persist a complete AGENT.md.
1025
+ if (isBuiltinOverlayAgentId(agentId)) {
1026
+ const pluginRefs =
1027
+ plugins ??
1028
+ (nextData.plugins !== undefined ? parsePluginRefs(nextData.plugins) : undefined);
1029
+ const diskOverrides: Partial<AgentDetails> = {};
1030
+ if (typeof nextData.name === 'string' && nextData.name.trim() !== '') {
1031
+ diskOverrides.name = nextData.name;
1032
+ }
1033
+ if (typeof nextData.description === 'string') {
1034
+ diskOverrides.description = nextData.description;
1035
+ }
1036
+ const trimmedContent = String(nextContent).trim();
1037
+ if (trimmedContent) {
1038
+ diskOverrides.instructions = trimmedContent;
1039
+ }
1040
+ if (pluginRefs && pluginRefs.length > 0) {
1041
+ diskOverrides.pluginRefs = pluginRefs;
1042
+ }
1043
+
1044
+ const effective =
1045
+ agentId === SYSTEM_AGENT_ID
1046
+ ? getSystemAgentDetails(diskOverrides)
1047
+ : getStateAgentDetails(diskOverrides);
1048
+
1049
+ if (name === undefined) nextData.name = effective.name;
1050
+ if (description === undefined) nextData.description = effective.description;
1051
+ if (instructions === undefined && !String(nextContent).trim()) {
1052
+ nextContent = effective.instructions;
1053
+ }
1054
+ }
914
1055
 
915
- const nextContent = instructions !== undefined ? instructions : parsed.content;
916
1056
  const body = matter.stringify(`${String(nextContent).trim()}\n`, nextData);
1057
+ await fs.mkdir(path.dirname(agentMdPath), { recursive: true });
917
1058
  await fs.writeFile(agentMdPath, body, 'utf-8');
918
1059
  },
919
1060
  deleteAgent: async ({ agentId }: { agentId: string }): Promise<void> => {
920
- assertValidDiskAgentId(agentId);
1061
+ assertAgentIdFormat(agentId);
921
1062
  const agentDir = resolvePath(path.join(getAgentsRootDir(), agentId));
922
1063
  const agentMdPath = path.join(agentDir, 'AGENT.md');
923
1064
  const packageJsonPath = path.join(agentDir, 'package.json');
924
1065
 
1066
+ if (isBuiltinOverlayAgentId(agentId)) {
1067
+ try {
1068
+ await fs.access(agentMdPath);
1069
+ } catch (error: unknown) {
1070
+ if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
1071
+ const err = new Error(
1072
+ `Agent "${agentId}" has no AGENT.md on disk; nothing to remove (defaults already apply).`,
1073
+ );
1074
+ (err as Error & { code?: string }).code = 'AGENT_NOT_FOUND';
1075
+ throw err;
1076
+ }
1077
+ throw error;
1078
+ }
1079
+ await fs.unlink(agentMdPath);
1080
+ try {
1081
+ const remaining = await fs.readdir(agentDir);
1082
+ if (remaining.length === 0) {
1083
+ await fs.rmdir(agentDir);
1084
+ }
1085
+ } catch {
1086
+ // ignore cleanup failures
1087
+ }
1088
+ return;
1089
+ }
1090
+
925
1091
  try {
926
1092
  await fs.access(agentDir);
927
1093
  } catch (error: unknown) {
@@ -1032,8 +1198,10 @@ export const storageService = {
1032
1198
  try {
1033
1199
  const threadDir = getConversationDir(channelId, threadId);
1034
1200
  if (threadId) {
1201
+ let exists = false;
1035
1202
  try {
1036
1203
  await fs.access(threadDir);
1204
+ exists = true;
1037
1205
  } catch (error: unknown) {
1038
1206
  if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
1039
1207
  const threadTitle = buildThreadTitleFromEvent(event);
@@ -1046,6 +1214,23 @@ export const storageService = {
1046
1214
  throw error;
1047
1215
  }
1048
1216
  }
1217
+
1218
+ if (exists) {
1219
+ // If the thread already exists, check if it has a name.
1220
+ // This handles threads created via action:create_thread without a title.
1221
+ const threadDetails = await storageService.getThreadDetails({ channelId, threadId });
1222
+ const currentState = (threadDetails.state as Record<string, unknown>) || {};
1223
+ if (!currentState.name) {
1224
+ const threadTitle = buildThreadTitleFromEvent(event);
1225
+ if (threadTitle) {
1226
+ await storageService.patchThreadState({
1227
+ channelId,
1228
+ threadId,
1229
+ state: { name: threadTitle },
1230
+ });
1231
+ }
1232
+ }
1233
+ }
1049
1234
  } else {
1050
1235
  await fs.mkdir(threadDir, { recursive: true });
1051
1236
  }
@@ -1,13 +1,13 @@
1
- import type { OpenBotEvent } from '../app/types.js';
2
- import type { PluginRef } from './plugin.js';
3
- import type { MemoryRecord, ListMemoriesArgs } from '../services/memory.js';
1
+ import type { OpenBotEvent } from '../../app/types.js';
2
+ import type { PluginRef } from './types.js';
3
+ import type { MemoryRecord, ListMemoriesArgs } from '../../plugins/memory/service.js';
4
4
 
5
5
  /**
6
- * Public data types exposed by the OpenBot bus.
6
+ * Public data types exposed by the OpenBot platform.
7
7
  *
8
- * The bus is the platform layer that owns channels, threads, the agent registry,
9
- * and the event stream. Agents are composed entirely of Plugins (see
10
- * `bus/plugin.ts`); their internal implementation is opaque to the bus.
8
+ * The platform layer owns channels, threads, the agent registry, and the event
9
+ * stream. Agents are composed entirely of Plugins (see `./types.ts`); their
10
+ * internal implementation is opaque to the platform.
11
11
  */
12
12
 
13
13
  export type Agent = {
@@ -17,6 +17,8 @@ export type Agent = {
17
17
  image?: string;
18
18
  /** Plugin ids that compose this agent (mirrors AGENT.md `plugins[].id`). */
19
19
  plugins: string[];
20
+ /** When true, omitted from `action:storage:get-agents` (still available via get-agent-details). */
21
+ hidden?: boolean;
20
22
  createdAt: Date;
21
23
  updatedAt: Date;
22
24
  };
@@ -60,6 +62,8 @@ export type Channel = {
60
62
  name: string;
61
63
  description: string;
62
64
  cwd?: string;
65
+ /** Agent ids associated with this channel (from `state.json`). */
66
+ participants: string[];
63
67
  createdAt: Date;
64
68
  updatedAt: Date;
65
69
  hasUnseenMessages?: boolean;
@@ -87,6 +91,8 @@ export type ChannelDetails = {
87
91
  spec: string;
88
92
  state: unknown;
89
93
  cwd?: string;
94
+ /** Agent ids for this channel (from `state.json`). */
95
+ participants: string[];
90
96
  threads?: Thread[];
91
97
  };
92
98
 
@@ -106,27 +112,34 @@ export interface Storage {
106
112
  }) => Promise<void>;
107
113
  getThreads: (args: { channelId: string }) => Promise<Thread[]>;
108
114
  getThreadDetails: (args: { channelId: string; threadId: string }) => Promise<ThreadDetails>;
115
+ /** User-facing agent list; excludes agents with `hidden: true` (e.g. built-in `state`). */
109
116
  getAgents: () => Promise<Agent[]>;
110
117
  getPlugins: () => Promise<PluginDescriptor[]>;
111
118
  getAgentDetails: (args: { agentId: string }) => Promise<AgentDetails>;
119
+ /** Includes built-in `system` / `state` agents as optional AGENT.md overlays. */
112
120
  createAgent: (args: {
113
121
  agentId: string;
114
122
  name: string;
115
123
  description?: string;
116
124
  /** Avatar/logo URL or data URI; persisted in AGENT.md frontmatter. */
117
125
  image?: string;
126
+ /** When true, agent is omitted from `getAgents` / `action:storage:get-agents`. */
127
+ hidden?: boolean;
118
128
  instructions: string;
119
129
  plugins: PluginRef[];
120
130
  }) => Promise<void>;
131
+ /** Partial update; for `system` / `state`, creates overlay file if missing. */
121
132
  updateAgent: (args: {
122
133
  agentId: string;
123
134
  name?: string;
124
135
  description?: string;
125
136
  /** Omit to leave unchanged; empty string removes stored image. */
126
137
  image?: string;
138
+ hidden?: boolean;
127
139
  instructions?: string;
128
140
  plugins?: PluginRef[];
129
141
  }) => Promise<void>;
142
+ /** For `system` / `state`, removes only `AGENT.md` (reverts to code defaults). */
130
143
  deleteAgent: (args: { agentId: string }) => Promise<void>;
131
144
  getEvents: (args: { channelId: string; threadId?: string }) => Promise<OpenBotEvent[]>;
132
145
  getChannelDetails: (args: { channelId: string }) => Promise<ChannelDetails>;
@@ -0,0 +1,13 @@
1
+ import type { Plugin } from './types.js';
2
+
3
+ /** Resolved plugins (built-in + community); shared with registry resolution. */
4
+ export const resolvedPluginCache = new Map<string, Plugin>();
5
+
6
+ /** Community plugin ids that have already been logged as loaded once. */
7
+ export const loadedCommunityPlugins = new Set<string>();
8
+
9
+ /** Drop a single id from the in-memory resolver cache (e.g. after install/uninstall). */
10
+ export function invalidatePlugin(id: string): void {
11
+ resolvedPluginCache.delete(id);
12
+ loadedCommunityPlugins.delete(id);
13
+ }