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.
- package/README.md +15 -16
- package/dist/app/agent-ids.js +4 -0
- package/dist/app/cli.js +1 -1
- package/dist/app/config.js +0 -19
- package/dist/app/server.js +8 -14
- package/dist/assets/icon.svg +9 -3
- package/dist/bus/services.js +78 -132
- package/dist/harness/agent-invoke-run.js +44 -0
- package/dist/harness/agent-turn.js +99 -0
- package/dist/harness/channel-participants.js +40 -0
- package/dist/harness/constants.js +2 -0
- package/dist/harness/context-meter.js +97 -0
- package/dist/harness/context.js +98 -45
- package/dist/harness/dispatch.js +144 -0
- package/dist/harness/dispatcher.js +45 -156
- package/dist/harness/history.js +177 -0
- package/dist/harness/index.js +91 -0
- package/dist/harness/orchestration.js +88 -0
- package/dist/harness/participants.js +22 -0
- package/dist/harness/run-harness.js +154 -0
- package/dist/harness/run.js +98 -0
- package/dist/harness/runtime-factory.js +0 -34
- package/dist/harness/runtime.js +57 -0
- package/dist/harness/todo-dispatch.js +51 -0
- package/dist/harness/todos.js +5 -0
- package/dist/harness/turn.js +79 -0
- package/dist/plugins/approval/index.js +105 -149
- package/dist/plugins/delegation/index.js +119 -32
- package/dist/plugins/memory/index.js +103 -14
- package/dist/plugins/memory/service.js +152 -0
- package/dist/plugins/openbot/context.js +80 -0
- package/dist/plugins/openbot/history.js +98 -0
- package/dist/plugins/openbot/index.js +31 -0
- package/dist/plugins/openbot/runtime.js +317 -0
- package/dist/plugins/openbot/system-prompt.js +5 -0
- package/dist/plugins/plugin-manager/index.js +105 -0
- package/dist/plugins/storage/index.js +573 -0
- package/dist/plugins/storage/service.js +1159 -0
- package/dist/plugins/storage-tools/index.js +2 -2
- package/dist/plugins/thread-namer/index.js +72 -0
- package/dist/plugins/thread-naming/generate-title.js +44 -0
- package/dist/plugins/thread-naming/index.js +103 -0
- package/dist/plugins/threads/index.js +114 -0
- package/dist/plugins/todo/index.js +24 -25
- package/dist/plugins/ui/index.js +2 -32
- package/dist/registry/plugins.js +3 -9
- package/dist/services/plugins/domain.js +1 -0
- package/dist/services/plugins/plugin-cache.js +9 -0
- package/dist/services/plugins/registry.js +110 -0
- package/dist/services/plugins/service.js +177 -0
- package/dist/services/plugins/types.js +1 -0
- package/dist/services/process.js +29 -0
- package/dist/services/storage.js +41 -15
- package/dist/services/thread-naming.js +81 -0
- package/docs/agents.md +16 -10
- package/docs/architecture.md +2 -2
- package/docs/plugins.md +6 -15
- package/docs/templates/AGENT.example.md +7 -13
- package/package.json +1 -2
- package/src/app/agent-ids.ts +5 -0
- package/src/app/cli.ts +1 -1
- package/src/app/config.ts +1 -31
- package/src/app/server.ts +8 -16
- package/src/app/types.ts +70 -190
- package/src/assets/icon.svg +9 -3
- package/src/harness/index.ts +145 -0
- package/src/plugins/approval/index.ts +91 -189
- package/src/plugins/delegation/index.ts +136 -39
- package/src/plugins/memory/index.ts +112 -15
- package/src/{services/memory.ts → plugins/memory/service.ts} +1 -1
- package/src/plugins/openbot/context.ts +91 -0
- package/src/plugins/openbot/history.ts +107 -0
- package/src/plugins/openbot/index.ts +37 -0
- package/src/plugins/openbot/runtime.ts +384 -0
- package/src/plugins/openbot/system-prompt.ts +7 -0
- package/src/plugins/plugin-manager/index.ts +122 -0
- package/src/plugins/shell/index.ts +1 -1
- package/src/plugins/storage/index.ts +633 -0
- package/src/{services/storage.ts → plugins/storage/service.ts} +257 -72
- package/src/{bus/types.ts → services/plugins/domain.ts} +20 -7
- package/src/services/plugins/plugin-cache.ts +13 -0
- package/src/{registry/plugins.ts → services/plugins/registry.ts} +25 -27
- package/src/services/{plugins.ts → plugins/service.ts} +96 -2
- package/src/{bus/plugin.ts → services/plugins/types.ts} +3 -3
- package/src/bus/services.ts +0 -908
- package/src/harness/context.ts +0 -356
- package/src/harness/dispatcher.ts +0 -379
- package/src/harness/mcp.ts +0 -78
- package/src/harness/runtime-factory.ts +0 -129
- package/src/harness/todo-advance.ts +0 -128
- package/src/plugins/ai-sdk/index.ts +0 -41
- package/src/plugins/ai-sdk/runtime.ts +0 -468
- package/src/plugins/ai-sdk/system-prompt.ts +0 -18
- package/src/plugins/mcp/index.ts +0 -128
- package/src/plugins/storage-tools/index.ts +0 -90
- package/src/plugins/todo/index.ts +0 -64
- package/src/plugins/ui/index.ts +0 -227
- /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 '
|
|
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 '
|
|
26
|
-
import type { PluginRef } from '
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import { listBuiltInPlugins, parsePluginModule } from '
|
|
30
|
-
import { OpenBotEvent, OpenBotState } from '
|
|
31
|
-
import { processService } from '
|
|
32
|
-
import { memoryService } from '
|
|
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(
|
|
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 =
|
|
110
|
+
const SYSTEM_AGENT_ID = ORCHESTRATOR_AGENT_ID;
|
|
107
111
|
|
|
108
112
|
const SYSTEM_DEFAULT_PLUGINS: PluginRef[] = [
|
|
109
|
-
{ id: '
|
|
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.
|
|
126
|
-
instructions:
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
|
383
|
-
|
|
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:
|
|
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.
|
|
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
|
|
529
|
-
typeof threadState.
|
|
530
|
-
if (
|
|
531
|
-
threadDisplayName =
|
|
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
|
|
578
|
-
isRecord(state) && typeof state.
|
|
579
|
-
? state.
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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
|
-
|
|
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 '
|
|
2
|
-
import type { PluginRef } from './
|
|
3
|
-
import type { MemoryRecord, ListMemoriesArgs } from '
|
|
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
|
|
6
|
+
* Public data types exposed by the OpenBot platform.
|
|
7
7
|
*
|
|
8
|
-
* The
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
+
}
|