openbot 0.3.6 → 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 (96) 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/bus/services.js +34 -124
  7. package/dist/harness/agent-invoke-run.js +44 -0
  8. package/dist/harness/agent-turn.js +99 -0
  9. package/dist/harness/channel-participants.js +40 -0
  10. package/dist/harness/constants.js +2 -0
  11. package/dist/harness/context-meter.js +97 -0
  12. package/dist/harness/context.js +95 -47
  13. package/dist/harness/dispatch.js +144 -0
  14. package/dist/harness/dispatcher.js +45 -156
  15. package/dist/harness/history.js +177 -0
  16. package/dist/harness/index.js +91 -0
  17. package/dist/harness/orchestration.js +88 -0
  18. package/dist/harness/participants.js +22 -0
  19. package/dist/harness/run-harness.js +154 -0
  20. package/dist/harness/run.js +98 -0
  21. package/dist/harness/runtime-factory.js +0 -34
  22. package/dist/harness/runtime.js +57 -0
  23. package/dist/harness/todo-dispatch.js +51 -0
  24. package/dist/harness/todos.js +5 -0
  25. package/dist/harness/turn.js +79 -0
  26. package/dist/plugins/approval/index.js +105 -149
  27. package/dist/plugins/delegation/index.js +119 -32
  28. package/dist/plugins/memory/index.js +103 -14
  29. package/dist/plugins/memory/service.js +152 -0
  30. package/dist/plugins/openbot/context.js +80 -0
  31. package/dist/plugins/openbot/history.js +98 -0
  32. package/dist/plugins/openbot/index.js +31 -0
  33. package/dist/plugins/openbot/runtime.js +317 -0
  34. package/dist/plugins/openbot/system-prompt.js +5 -0
  35. package/dist/plugins/plugin-manager/index.js +105 -0
  36. package/dist/plugins/storage/index.js +573 -0
  37. package/dist/plugins/storage/service.js +1159 -0
  38. package/dist/plugins/storage-tools/index.js +2 -2
  39. package/dist/plugins/thread-namer/index.js +72 -0
  40. package/dist/plugins/thread-naming/generate-title.js +44 -0
  41. package/dist/plugins/thread-naming/index.js +103 -0
  42. package/dist/plugins/threads/index.js +114 -0
  43. package/dist/plugins/todo/index.js +24 -25
  44. package/dist/plugins/ui/index.js +2 -32
  45. package/dist/registry/plugins.js +3 -9
  46. package/dist/services/plugins/domain.js +1 -0
  47. package/dist/services/plugins/plugin-cache.js +9 -0
  48. package/dist/services/plugins/registry.js +110 -0
  49. package/dist/services/plugins/service.js +177 -0
  50. package/dist/services/plugins/types.js +1 -0
  51. package/dist/services/process.js +29 -0
  52. package/dist/services/storage.js +11 -10
  53. package/dist/services/thread-naming.js +81 -0
  54. package/docs/agents.md +16 -10
  55. package/docs/architecture.md +2 -2
  56. package/docs/plugins.md +6 -15
  57. package/docs/templates/AGENT.example.md +7 -13
  58. package/package.json +1 -2
  59. package/src/app/agent-ids.ts +5 -0
  60. package/src/app/cli.ts +1 -1
  61. package/src/app/config.ts +1 -31
  62. package/src/app/server.ts +8 -16
  63. package/src/app/types.ts +63 -189
  64. package/src/harness/index.ts +145 -0
  65. package/src/plugins/approval/index.ts +91 -189
  66. package/src/plugins/delegation/index.ts +136 -39
  67. package/src/plugins/memory/index.ts +112 -15
  68. package/src/{services/memory.ts → plugins/memory/service.ts} +1 -1
  69. package/src/plugins/openbot/context.ts +91 -0
  70. package/src/plugins/openbot/history.ts +107 -0
  71. package/src/plugins/openbot/index.ts +37 -0
  72. package/src/plugins/openbot/runtime.ts +384 -0
  73. package/src/plugins/openbot/system-prompt.ts +7 -0
  74. package/src/plugins/plugin-manager/index.ts +122 -0
  75. package/src/plugins/shell/index.ts +1 -1
  76. package/src/plugins/storage/index.ts +633 -0
  77. package/src/{services/storage.ts → plugins/storage/service.ts} +224 -67
  78. package/src/{bus/types.ts → services/plugins/domain.ts} +16 -7
  79. package/src/services/plugins/plugin-cache.ts +13 -0
  80. package/src/{registry/plugins.ts → services/plugins/registry.ts} +25 -27
  81. package/src/services/{plugins.ts → plugins/service.ts} +96 -2
  82. package/src/{bus/plugin.ts → services/plugins/types.ts} +3 -3
  83. package/src/bus/services.ts +0 -954
  84. package/src/harness/context.ts +0 -365
  85. package/src/harness/dispatcher.ts +0 -379
  86. package/src/harness/mcp.ts +0 -78
  87. package/src/harness/runtime-factory.ts +0 -129
  88. package/src/harness/todo-advance.ts +0 -128
  89. package/src/plugins/ai-sdk/index.ts +0 -41
  90. package/src/plugins/ai-sdk/runtime.ts +0 -468
  91. package/src/plugins/ai-sdk/system-prompt.ts +0 -18
  92. package/src/plugins/mcp/index.ts +0 -128
  93. package/src/plugins/storage-tools/index.ts +0 -90
  94. package/src/plugins/todo/index.ts +0 -64
  95. package/src/plugins/ui/index.ts +0 -227
  96. /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
+ };
179
+
180
+ if (!overrides) return defaults;
181
+
182
+ const refs = overrides.pluginRefs && overrides.pluginRefs.length > 0
183
+ ? overrides.pluginRefs
184
+ : defaults.pluginRefs;
152
185
 
153
- const RESERVED_DISK_AGENT_IDS = new Set([SYSTEM_AGENT_ID]);
186
+ const diskInstructions = overrides.instructions?.trim();
187
+ const instructions =
188
+ diskInstructions && diskInstructions.length > 0 ? diskInstructions : defaults.instructions;
154
189
 
155
- const assertValidDiskAgentId = (agentId: string): void => {
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'
@@ -339,6 +400,12 @@ const readChannelStateFileFields = (
339
400
  * Parse the `plugins:` array from AGENT.md frontmatter. Each entry must have an
340
401
  * `id`; `config` is optional. Strings are accepted as a shorthand for `{ id }`.
341
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
+
342
409
  const parsePluginRefs = (raw: unknown): PluginRef[] => {
343
410
  if (!Array.isArray(raw)) return [];
344
411
  const refs: PluginRef[] = [];
@@ -522,7 +589,7 @@ export const storageService = {
522
589
 
523
590
  const baseState: Record<string, unknown> = { ...(initialState || {}) };
524
591
  if (threadTitle?.trim()) {
525
- baseState.generatedName = threadTitle.trim();
592
+ baseState.name = threadTitle.trim();
526
593
  }
527
594
 
528
595
  await fs.mkdir(threadDir, { recursive: true });
@@ -550,10 +617,10 @@ export const storageService = {
550
617
  try {
551
618
  const threadStateRaw = await fs.readFile(threadStatePath, 'utf-8');
552
619
  const threadState = JSON.parse(threadStateRaw) as Record<string, unknown>;
553
- const generatedName =
554
- typeof threadState.generatedName === 'string' ? threadState.generatedName.trim() : '';
555
- if (generatedName) {
556
- threadDisplayName = generatedName;
620
+ const threadName =
621
+ typeof threadState.name === 'string' ? threadState.name.trim() : '';
622
+ if (threadName) {
623
+ threadDisplayName = threadName;
557
624
  }
558
625
  } catch (error: unknown) {
559
626
  if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') {
@@ -599,14 +666,14 @@ export const storageService = {
599
666
  }
600
667
  }
601
668
 
602
- const generatedName =
603
- isRecord(state) && typeof state.generatedName === 'string'
604
- ? state.generatedName.trim()
669
+ const threadName =
670
+ isRecord(state) && typeof state.name === 'string'
671
+ ? state.name.trim()
605
672
  : '';
606
673
 
607
674
  return {
608
675
  id: threadId,
609
- name: generatedName || threadId,
676
+ name: threadName || threadId,
610
677
  channelId,
611
678
  state,
612
679
  };
@@ -741,15 +808,7 @@ export const storageService = {
741
808
  agentIds.map(async (id) => {
742
809
  try {
743
810
  const details = await storageService.getAgentDetails({ agentId: id });
744
- return {
745
- id,
746
- name: details.name || id,
747
- description: details.description || '',
748
- image: details.image,
749
- plugins: details.plugins,
750
- createdAt: details.createdAt,
751
- updatedAt: details.updatedAt,
752
- } satisfies Agent;
811
+ return agentSummaryFromDetails(details);
753
812
  } catch {
754
813
  return {
755
814
  id,
@@ -764,23 +823,19 @@ export const storageService = {
764
823
  );
765
824
 
766
825
  const system = await storageService.getAgentDetails({ agentId: SYSTEM_AGENT_ID });
767
- const builtInSystemAgent: Agent = {
768
- id: system.id,
769
- name: system.name,
770
- description: system.description || '',
771
- image: system.image,
772
- plugins: system.plugins,
773
- createdAt: system.createdAt,
774
- updatedAt: system.updatedAt,
775
- };
826
+ const builtInSystemAgent = agentSummaryFromDetails(system);
827
+
828
+ const builtInStateRow = await storageService.getAgentDetails({ agentId: STATE_AGENT_ID });
829
+ const builtInStateAgent = agentSummaryFromDetails(builtInStateRow);
776
830
 
777
831
  const deduped = new Map<string, Agent>();
778
832
  deduped.set(builtInSystemAgent.id, builtInSystemAgent);
833
+ deduped.set(builtInStateAgent.id, builtInStateAgent);
779
834
  for (const agent of agents) {
780
835
  if (!deduped.has(agent.id)) deduped.set(agent.id, agent);
781
836
  }
782
837
 
783
- return Array.from(deduped.values());
838
+ return Array.from(deduped.values()).filter((agent) => !agent.hidden);
784
839
  },
785
840
  getPlugins: async (): Promise<PluginDescriptor[]> => {
786
841
  const [builtIn, fromDisk] = await Promise.all([
@@ -824,16 +879,17 @@ export const storageService = {
824
879
  pluginRefs,
825
880
  description: typeof data.description === 'string' ? data.description : '',
826
881
  image: frontmatterImage || discoveredImage || undefined,
882
+ hidden: parseHiddenFlag(data.hidden),
827
883
  createdAt: stats.birthtime,
828
884
  updatedAt: stats.mtime,
829
885
  };
830
886
  } catch (error) {
831
- if (agentId !== SYSTEM_AGENT_ID) {
887
+ if (agentId !== SYSTEM_AGENT_ID && agentId !== STATE_AGENT_ID) {
832
888
  const err = new Error(`Agent "${agentId}" does not exist.`);
833
889
  (err as Error & { code?: string }).code = 'AGENT_NOT_FOUND';
834
890
  throw err;
835
891
  }
836
- // swallow: system agent has on-disk overrides optional
892
+ // swallow: built-in agents have optional `agents/<id>/AGENT.md` overrides
837
893
  void error;
838
894
  }
839
895
 
@@ -841,6 +897,10 @@ export const storageService = {
841
897
  return getSystemAgentDetails(diskDetails);
842
898
  }
843
899
 
900
+ if (agentId === STATE_AGENT_ID) {
901
+ return getStateAgentDetails(diskDetails);
902
+ }
903
+
844
904
  if (!diskDetails) {
845
905
  const error = new Error(`Agent "${agentId}" does not exist.`);
846
906
  (error as Error & { code?: string }).code = 'AGENT_NOT_FOUND';
@@ -854,6 +914,7 @@ export const storageService = {
854
914
  name,
855
915
  description = '',
856
916
  image,
917
+ hidden,
857
918
  instructions,
858
919
  plugins,
859
920
  }: {
@@ -861,10 +922,11 @@ export const storageService = {
861
922
  name: string;
862
923
  description?: string;
863
924
  image?: string;
925
+ hidden?: boolean;
864
926
  instructions: string;
865
927
  plugins: PluginRef[];
866
928
  }): Promise<void> => {
867
- assertValidDiskAgentId(agentId);
929
+ assertAgentIdFormat(agentId);
868
930
  const agentDir = resolvePath(path.join(getAgentsRootDir(), agentId));
869
931
  const agentMdPath = path.join(agentDir, 'AGENT.md');
870
932
 
@@ -892,6 +954,9 @@ export const storageService = {
892
954
  if (typeof image === 'string' && image.trim() !== '') {
893
955
  data.image = image.trim();
894
956
  }
957
+ if (hidden === true) {
958
+ data.hidden = true;
959
+ }
895
960
 
896
961
  const body = matter.stringify(`${instructions.trim()}\n`, data);
897
962
  await fs.writeFile(agentMdPath, body, 'utf-8');
@@ -901,6 +966,7 @@ export const storageService = {
901
966
  name,
902
967
  description,
903
968
  image,
969
+ hidden,
904
970
  instructions,
905
971
  plugins,
906
972
  }: {
@@ -908,10 +974,11 @@ export const storageService = {
908
974
  name?: string;
909
975
  description?: string;
910
976
  image?: string;
977
+ hidden?: boolean;
911
978
  instructions?: string;
912
979
  plugins?: PluginRef[];
913
980
  }): Promise<void> => {
914
- assertValidDiskAgentId(agentId);
981
+ assertAgentIdFormat(agentId);
915
982
  const agentDir = resolvePath(path.join(getAgentsRootDir(), agentId));
916
983
  const agentMdPath = path.join(agentDir, 'AGENT.md');
917
984
 
@@ -920,14 +987,18 @@ export const storageService = {
920
987
  raw = await fs.readFile(agentMdPath, 'utf-8');
921
988
  } catch (error: unknown) {
922
989
  if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
923
- const err = new Error(`Agent "${agentId}" does not exist.`);
924
- (err as Error & { code?: string }).code = 'AGENT_NOT_FOUND';
925
- 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;
926
998
  }
927
- throw error;
928
999
  }
929
1000
 
930
- const parsed = matter(raw);
1001
+ const parsed = raw === '' ? { data: {}, content: '' } : matter(raw);
931
1002
  const nextData: Record<string, unknown> = { ...parsed.data };
932
1003
  if (name !== undefined) nextData.name = name;
933
1004
  if (description !== undefined) nextData.description = description;
@@ -939,17 +1010,84 @@ export const storageService = {
939
1010
  delete nextData.image;
940
1011
  }
941
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
+ }
942
1055
 
943
- const nextContent = instructions !== undefined ? instructions : parsed.content;
944
1056
  const body = matter.stringify(`${String(nextContent).trim()}\n`, nextData);
1057
+ await fs.mkdir(path.dirname(agentMdPath), { recursive: true });
945
1058
  await fs.writeFile(agentMdPath, body, 'utf-8');
946
1059
  },
947
1060
  deleteAgent: async ({ agentId }: { agentId: string }): Promise<void> => {
948
- assertValidDiskAgentId(agentId);
1061
+ assertAgentIdFormat(agentId);
949
1062
  const agentDir = resolvePath(path.join(getAgentsRootDir(), agentId));
950
1063
  const agentMdPath = path.join(agentDir, 'AGENT.md');
951
1064
  const packageJsonPath = path.join(agentDir, 'package.json');
952
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
+
953
1091
  try {
954
1092
  await fs.access(agentDir);
955
1093
  } catch (error: unknown) {
@@ -1060,8 +1198,10 @@ export const storageService = {
1060
1198
  try {
1061
1199
  const threadDir = getConversationDir(channelId, threadId);
1062
1200
  if (threadId) {
1201
+ let exists = false;
1063
1202
  try {
1064
1203
  await fs.access(threadDir);
1204
+ exists = true;
1065
1205
  } catch (error: unknown) {
1066
1206
  if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
1067
1207
  const threadTitle = buildThreadTitleFromEvent(event);
@@ -1074,6 +1214,23 @@ export const storageService = {
1074
1214
  throw error;
1075
1215
  }
1076
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
+ }
1077
1234
  } else {
1078
1235
  await fs.mkdir(threadDir, { recursive: true });
1079
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
  };
@@ -110,27 +112,34 @@ export interface Storage {
110
112
  }) => Promise<void>;
111
113
  getThreads: (args: { channelId: string }) => Promise<Thread[]>;
112
114
  getThreadDetails: (args: { channelId: string; threadId: string }) => Promise<ThreadDetails>;
115
+ /** User-facing agent list; excludes agents with `hidden: true` (e.g. built-in `state`). */
113
116
  getAgents: () => Promise<Agent[]>;
114
117
  getPlugins: () => Promise<PluginDescriptor[]>;
115
118
  getAgentDetails: (args: { agentId: string }) => Promise<AgentDetails>;
119
+ /** Includes built-in `system` / `state` agents as optional AGENT.md overlays. */
116
120
  createAgent: (args: {
117
121
  agentId: string;
118
122
  name: string;
119
123
  description?: string;
120
124
  /** Avatar/logo URL or data URI; persisted in AGENT.md frontmatter. */
121
125
  image?: string;
126
+ /** When true, agent is omitted from `getAgents` / `action:storage:get-agents`. */
127
+ hidden?: boolean;
122
128
  instructions: string;
123
129
  plugins: PluginRef[];
124
130
  }) => Promise<void>;
131
+ /** Partial update; for `system` / `state`, creates overlay file if missing. */
125
132
  updateAgent: (args: {
126
133
  agentId: string;
127
134
  name?: string;
128
135
  description?: string;
129
136
  /** Omit to leave unchanged; empty string removes stored image. */
130
137
  image?: string;
138
+ hidden?: boolean;
131
139
  instructions?: string;
132
140
  plugins?: PluginRef[];
133
141
  }) => Promise<void>;
142
+ /** For `system` / `state`, removes only `AGENT.md` (reverts to code defaults). */
134
143
  deleteAgent: (args: { agentId: string }) => Promise<void>;
135
144
  getEvents: (args: { channelId: string; threadId?: string }) => Promise<OpenBotEvent[]>;
136
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
+ }