openbot 0.3.6 → 0.4.2
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 +10 -19
- package/dist/app/server.js +208 -17
- package/dist/bus/services.js +34 -124
- 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 +95 -47
- 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 +109 -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 +120 -149
- package/dist/plugins/bash/index.js +195 -0
- package/dist/plugins/delegation/index.js +121 -32
- package/dist/plugins/memory/index.js +103 -14
- package/dist/plugins/memory/service.js +152 -0
- package/dist/plugins/openbot/context.js +125 -0
- package/dist/plugins/openbot/history.js +144 -0
- package/dist/plugins/openbot/index.js +71 -0
- package/dist/plugins/openbot/runtime.js +381 -0
- package/dist/plugins/openbot/system-prompt.js +25 -0
- package/dist/plugins/plugin-manager/index.js +189 -0
- package/dist/plugins/shell/index.js +2 -1
- package/dist/plugins/storage/files.js +67 -0
- package/dist/plugins/storage/index.js +750 -0
- package/dist/plugins/storage/service.js +1316 -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 +109 -180
- package/dist/registry/plugins.js +3 -9
- package/dist/services/abort.js +43 -0
- package/dist/services/plugins/domain.js +1 -0
- package/dist/services/plugins/plugin-cache.js +9 -0
- package/dist/services/plugins/registry.js +112 -0
- package/dist/services/plugins/service.js +232 -0
- package/dist/services/plugins/types.js +1 -0
- package/dist/services/process.js +29 -0
- package/dist/services/storage.js +11 -10
- package/dist/services/thread-naming.js +81 -0
- package/docs/agents.md +15 -12
- package/docs/architecture.md +2 -2
- package/docs/plugins.md +29 -17
- package/docs/templates/AGENT.example.md +8 -14
- 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 +14 -31
- package/src/app/server.ts +243 -19
- package/src/app/types.ts +331 -187
- package/src/harness/index.ts +166 -0
- package/src/plugins/approval/index.ts +107 -188
- package/src/plugins/bash/index.ts +232 -0
- package/src/plugins/delegation/index.ts +139 -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 +140 -0
- package/src/plugins/openbot/history.ts +158 -0
- package/src/plugins/openbot/index.ts +79 -0
- package/src/plugins/openbot/runtime.ts +478 -0
- package/src/plugins/openbot/system-prompt.ts +27 -0
- package/src/plugins/plugin-manager/index.ts +224 -0
- package/src/plugins/storage/files.ts +81 -0
- package/src/plugins/storage/index.ts +823 -0
- package/src/{services/storage.ts → plugins/storage/service.ts} +485 -105
- package/src/plugins/ui/index.ts +117 -221
- package/src/services/abort.ts +46 -0
- package/src/{bus/types.ts → services/plugins/domain.ts} +50 -8
- package/src/services/plugins/plugin-cache.ts +13 -0
- package/src/{registry/plugins.ts → services/plugins/registry.ts} +28 -28
- package/src/services/plugins/service.ts +318 -0
- package/src/{bus/plugin.ts → services/plugins/types.ts} +7 -3
- package/src/bus/services.ts +0 -954
- package/src/harness/context.ts +0 -365
- 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/shell/index.ts +0 -123
- package/src/plugins/storage-tools/index.ts +0 -90
- package/src/plugins/todo/index.ts +0 -64
- package/src/services/plugins.ts +0 -133
- /package/src/{harness → services}/process.ts +0 -0
|
@@ -1,13 +1,15 @@
|
|
|
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,
|
|
4
5
|
DEFAULT_BASE_DIR,
|
|
5
6
|
DEFAULT_CHANNELS_DIR,
|
|
7
|
+
getDefaultChannelCwd,
|
|
6
8
|
loadConfig,
|
|
7
9
|
resolvePath,
|
|
8
10
|
StoredVariable,
|
|
9
11
|
VARIABLES_FILE,
|
|
10
|
-
} from '
|
|
12
|
+
} from '../../app/config.js';
|
|
11
13
|
import fs from 'node:fs/promises';
|
|
12
14
|
import { readFileSync } from 'node:fs';
|
|
13
15
|
import path from 'node:path';
|
|
@@ -22,14 +24,18 @@ import {
|
|
|
22
24
|
PluginDescriptor,
|
|
23
25
|
Thread,
|
|
24
26
|
ThreadDetails,
|
|
25
|
-
} from '
|
|
26
|
-
import type { PluginRef } from '
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
27
|
+
} from '../../services/plugins/domain.js';
|
|
28
|
+
import type { PluginRef } from '../../services/plugins/types.js';
|
|
29
|
+
import { openbotPlugin } from '../openbot/index.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';
|
|
34
|
+
import {
|
|
35
|
+
guessMimeType,
|
|
36
|
+
resolveChannelFile,
|
|
37
|
+
statChannelFile,
|
|
38
|
+
} from './files.js';
|
|
33
39
|
|
|
34
40
|
const resolveBaseDir = () => {
|
|
35
41
|
const config = loadConfig();
|
|
@@ -49,7 +55,10 @@ function getBundledSystemAgentImage(): string | undefined {
|
|
|
49
55
|
if (bundledSystemAgentImageLoaded) return bundledSystemAgentImage;
|
|
50
56
|
bundledSystemAgentImageLoaded = true;
|
|
51
57
|
try {
|
|
52
|
-
const iconPath = path.join(
|
|
58
|
+
const iconPath = path.join(
|
|
59
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
60
|
+
'../../assets/icon.svg',
|
|
61
|
+
);
|
|
53
62
|
const trimmed = readFileSync(iconPath, 'utf-8').trim();
|
|
54
63
|
if (!trimmed.startsWith('<svg')) return undefined;
|
|
55
64
|
bundledSystemAgentImage = toSvgDataUrl(trimmed);
|
|
@@ -103,27 +112,37 @@ const getConversationDir = (channelId: string, threadId?: string) => {
|
|
|
103
112
|
};
|
|
104
113
|
|
|
105
114
|
/** Built-in orchestrator agent id. Not creatable as a normal disk agent. */
|
|
106
|
-
const SYSTEM_AGENT_ID =
|
|
115
|
+
const SYSTEM_AGENT_ID = ORCHESTRATOR_AGENT_ID;
|
|
107
116
|
|
|
108
117
|
const SYSTEM_DEFAULT_PLUGINS: PluginRef[] = [
|
|
109
|
-
{
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
{
|
|
119
|
+
id: 'openbot',
|
|
120
|
+
config: {
|
|
121
|
+
model: 'openai/gpt-5.4-mini',
|
|
122
|
+
approval: {
|
|
123
|
+
actions: ['action:bash', 'action:create_channel', 'action:delete_channel'],
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
117
127
|
];
|
|
118
128
|
|
|
129
|
+
/** No `openbot` / `bash` — storage-side effects and infra plugins only. */
|
|
130
|
+
const STATE_DEFAULT_PLUGINS: PluginRef[] = [
|
|
131
|
+
{ id: 'storage' },
|
|
132
|
+
{ id: 'plugin-manager' },
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
const STATE_AGENT_INSTRUCTIONS =
|
|
136
|
+
'Built-in infra agent for deterministic state reads. No conversational model is attached; handle storage, approvals, memory, and plugin marketplace events.';
|
|
137
|
+
|
|
119
138
|
function getSystemAgentDetails(overrides?: Partial<AgentDetails>): AgentDetails {
|
|
120
139
|
const defaults: AgentDetails = {
|
|
121
140
|
id: SYSTEM_AGENT_ID,
|
|
122
141
|
name: 'OpenBot',
|
|
123
142
|
image: getBundledSystemAgentImage(),
|
|
124
143
|
description:
|
|
125
|
-
'First-party orchestration agent for OpenBot.
|
|
126
|
-
instructions:
|
|
144
|
+
'First-party orchestration agent for OpenBot.',
|
|
145
|
+
instructions: '',
|
|
127
146
|
plugins: SYSTEM_DEFAULT_PLUGINS.map((ref) => ref.id),
|
|
128
147
|
pluginRefs: SYSTEM_DEFAULT_PLUGINS,
|
|
129
148
|
createdAt: new Date(),
|
|
@@ -136,10 +155,15 @@ function getSystemAgentDetails(overrides?: Partial<AgentDetails>): AgentDetails
|
|
|
136
155
|
? overrides.pluginRefs
|
|
137
156
|
: defaults.pluginRefs;
|
|
138
157
|
|
|
158
|
+
const diskInstructions = overrides.instructions?.trim();
|
|
159
|
+
const instructions =
|
|
160
|
+
diskInstructions && diskInstructions.length > 0 ? diskInstructions : defaults.instructions;
|
|
161
|
+
|
|
139
162
|
return {
|
|
140
163
|
...defaults,
|
|
141
164
|
...overrides,
|
|
142
165
|
id: SYSTEM_AGENT_ID,
|
|
166
|
+
instructions,
|
|
143
167
|
image: overrides.image || defaults.image,
|
|
144
168
|
plugins: refs.map((ref) => ref.id),
|
|
145
169
|
pluginRefs: refs,
|
|
@@ -147,18 +171,65 @@ function getSystemAgentDetails(overrides?: Partial<AgentDetails>): AgentDetails
|
|
|
147
171
|
};
|
|
148
172
|
}
|
|
149
173
|
|
|
150
|
-
|
|
151
|
-
|
|
174
|
+
function getStateAgentDetails(overrides?: Partial<AgentDetails>): AgentDetails {
|
|
175
|
+
const defaults: AgentDetails = {
|
|
176
|
+
id: STATE_AGENT_ID,
|
|
177
|
+
name: 'State',
|
|
178
|
+
image: getBundledSystemAgentImage(),
|
|
179
|
+
description: 'Infrastructure agent for OpenBot — storage and hooks without an LLM.',
|
|
180
|
+
instructions: STATE_AGENT_INSTRUCTIONS,
|
|
181
|
+
plugins: STATE_DEFAULT_PLUGINS.map((ref) => ref.id),
|
|
182
|
+
pluginRefs: STATE_DEFAULT_PLUGINS,
|
|
183
|
+
hidden: true,
|
|
184
|
+
createdAt: new Date(),
|
|
185
|
+
updatedAt: new Date(),
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
if (!overrides) return defaults;
|
|
189
|
+
|
|
190
|
+
const refs = overrides.pluginRefs && overrides.pluginRefs.length > 0
|
|
191
|
+
? overrides.pluginRefs
|
|
192
|
+
: defaults.pluginRefs;
|
|
193
|
+
|
|
194
|
+
const diskInstructions = overrides.instructions?.trim();
|
|
195
|
+
const instructions =
|
|
196
|
+
diskInstructions && diskInstructions.length > 0 ? diskInstructions : defaults.instructions;
|
|
152
197
|
|
|
153
|
-
|
|
198
|
+
return {
|
|
199
|
+
...defaults,
|
|
200
|
+
...overrides,
|
|
201
|
+
id: STATE_AGENT_ID,
|
|
202
|
+
instructions,
|
|
203
|
+
image: overrides.image || defaults.image,
|
|
204
|
+
hidden: overrides.hidden !== undefined ? overrides.hidden : defaults.hidden,
|
|
205
|
+
plugins: refs.map((ref) => ref.id),
|
|
206
|
+
pluginRefs: refs,
|
|
207
|
+
updatedAt: new Date(),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
154
210
|
|
|
155
|
-
const
|
|
211
|
+
const agentSummaryFromDetails = (details: AgentDetails): Agent => ({
|
|
212
|
+
id: details.id,
|
|
213
|
+
name: details.name || details.id,
|
|
214
|
+
description: details.description || '',
|
|
215
|
+
image: details.image,
|
|
216
|
+
plugins: details.plugins,
|
|
217
|
+
hidden: details.hidden,
|
|
218
|
+
createdAt: details.createdAt,
|
|
219
|
+
updatedAt: details.updatedAt,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Suppress unused warning until system agent customization re-uses openbotPlugin metadata.
|
|
223
|
+
void openbotPlugin;
|
|
224
|
+
|
|
225
|
+
/** Built-in agents may persist optional `agents/<id>/AGENT.md` overlays; read path merges them with defaults. */
|
|
226
|
+
const isBuiltinOverlayAgentId = (agentId: string): boolean =>
|
|
227
|
+
agentId === SYSTEM_AGENT_ID || agentId === STATE_AGENT_ID;
|
|
228
|
+
|
|
229
|
+
const assertAgentIdFormat = (agentId: string): void => {
|
|
156
230
|
if (!agentId || typeof agentId !== 'string') {
|
|
157
231
|
throw new Error('agentId is required');
|
|
158
232
|
}
|
|
159
|
-
if (RESERVED_DISK_AGENT_IDS.has(agentId)) {
|
|
160
|
-
throw new Error(`Agent id "${agentId}" is reserved`);
|
|
161
|
-
}
|
|
162
233
|
if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) {
|
|
163
234
|
throw new Error('agentId must contain only letters, digits, underscores, and hyphens');
|
|
164
235
|
}
|
|
@@ -169,14 +240,31 @@ const getAgentsRootDir = () => path.join(resolveBaseDir(), DEFAULT_AGENTS_DIR);
|
|
|
169
240
|
const getLastReadFilePath = () =>
|
|
170
241
|
path.join(resolvePath(resolveBaseDir() + '/' + DEFAULT_CHANNELS_DIR), '_meta', 'last-read.json');
|
|
171
242
|
|
|
243
|
+
/** Sentinel key for channel-root events (no threadId). */
|
|
244
|
+
export const ROOT_THREAD_KEY = '__root__';
|
|
245
|
+
|
|
246
|
+
export type LastReadMap = Record<string, Record<string, string>>;
|
|
247
|
+
|
|
248
|
+
const readLastReadMap = async (): Promise<LastReadMap> => {
|
|
249
|
+
const raw = await readJsonFile<Record<string, any>>(getLastReadFilePath(), {});
|
|
250
|
+
const map: LastReadMap = {};
|
|
251
|
+
for (const [channelId, value] of Object.entries(raw)) {
|
|
252
|
+
if (typeof value === 'string') {
|
|
253
|
+
// Migrate old format
|
|
254
|
+
map[channelId] = { [ROOT_THREAD_KEY]: value };
|
|
255
|
+
} else if (value && typeof value === 'object') {
|
|
256
|
+
map[channelId] = value as Record<string, string>;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return map;
|
|
260
|
+
};
|
|
261
|
+
|
|
172
262
|
const THREAD_TITLE_MAX_LENGTH = 80;
|
|
173
263
|
|
|
174
264
|
const buildThreadTitleFromEvent = (event: OpenBotEvent): string | undefined => {
|
|
175
265
|
let rawContent = '';
|
|
176
266
|
|
|
177
|
-
if (
|
|
178
|
-
rawContent = event.data.content;
|
|
179
|
-
} else if (
|
|
267
|
+
if (
|
|
180
268
|
event.type === 'agent:invoke' &&
|
|
181
269
|
event.data?.role === 'user' &&
|
|
182
270
|
typeof event.data.content === 'string'
|
|
@@ -196,9 +284,12 @@ const buildThreadTitleFromEvent = (event: OpenBotEvent): string | undefined => {
|
|
|
196
284
|
|
|
197
285
|
const readJsonFile = async <T>(filePath: string, fallback: T): Promise<T> => {
|
|
198
286
|
try {
|
|
199
|
-
|
|
287
|
+
const content = (await fs.readFile(filePath, 'utf-8')).trim();
|
|
288
|
+
if (!content) return fallback;
|
|
289
|
+
return JSON.parse(content) as T;
|
|
200
290
|
} catch (e: unknown) {
|
|
201
291
|
if ((e as { code?: string })?.code === 'ENOENT') return fallback;
|
|
292
|
+
if (e instanceof SyntaxError) return fallback;
|
|
202
293
|
throw e;
|
|
203
294
|
}
|
|
204
295
|
};
|
|
@@ -339,6 +430,12 @@ const readChannelStateFileFields = (
|
|
|
339
430
|
* Parse the `plugins:` array from AGENT.md frontmatter. Each entry must have an
|
|
340
431
|
* `id`; `config` is optional. Strings are accepted as a shorthand for `{ id }`.
|
|
341
432
|
*/
|
|
433
|
+
const parseHiddenFlag = (raw: unknown): boolean | undefined => {
|
|
434
|
+
if (raw === true) return true;
|
|
435
|
+
if (raw === false) return false;
|
|
436
|
+
return undefined;
|
|
437
|
+
};
|
|
438
|
+
|
|
342
439
|
const parsePluginRefs = (raw: unknown): PluginRef[] => {
|
|
343
440
|
if (!Array.isArray(raw)) return [];
|
|
344
441
|
const refs: PluginRef[] = [];
|
|
@@ -359,22 +456,27 @@ const serializePluginRefs = (refs: PluginRef[]): unknown[] =>
|
|
|
359
456
|
refs.map((ref) => (ref.config ? { id: ref.id, config: ref.config } : { id: ref.id }));
|
|
360
457
|
|
|
361
458
|
export const storageService = {
|
|
362
|
-
|
|
363
|
-
return
|
|
459
|
+
getLastReadMap: async (): Promise<LastReadMap> => {
|
|
460
|
+
return readLastReadMap();
|
|
364
461
|
},
|
|
365
462
|
|
|
366
|
-
|
|
463
|
+
setLastRead: async ({
|
|
367
464
|
channelId,
|
|
465
|
+
threadId,
|
|
368
466
|
lastReadEventId,
|
|
369
467
|
}: {
|
|
370
468
|
channelId: string;
|
|
469
|
+
threadId?: string;
|
|
371
470
|
lastReadEventId: string;
|
|
372
471
|
}): Promise<void> => {
|
|
373
472
|
const p = getLastReadFilePath();
|
|
374
473
|
await fs.mkdir(path.dirname(p), { recursive: true });
|
|
375
|
-
const map = await
|
|
376
|
-
map[channelId] =
|
|
377
|
-
|
|
474
|
+
const map = await readLastReadMap();
|
|
475
|
+
if (!map[channelId]) map[channelId] = {};
|
|
476
|
+
map[channelId][threadId || ROOT_THREAD_KEY] = lastReadEventId;
|
|
477
|
+
const tmp = `${p}.tmp`;
|
|
478
|
+
await fs.writeFile(tmp, JSON.stringify(map, null, 2), 'utf-8');
|
|
479
|
+
await fs.rename(tmp, p);
|
|
378
480
|
},
|
|
379
481
|
|
|
380
482
|
getChannels: async (): Promise<Channel[]> => {
|
|
@@ -388,11 +490,12 @@ export const storageService = {
|
|
|
388
490
|
const channelNames = (await fs.readdir(channelsDir)).filter(
|
|
389
491
|
(name) => !name.startsWith('.') && name !== '_meta',
|
|
390
492
|
);
|
|
391
|
-
const
|
|
493
|
+
const lastReadMap = await storageService.getLastReadMap();
|
|
392
494
|
|
|
393
495
|
const channels = await Promise.all(
|
|
394
496
|
channelNames.map(async (name) => {
|
|
395
497
|
const channelDir = getConversationDir(name);
|
|
498
|
+
const stats = await fs.stat(channelDir);
|
|
396
499
|
const statePath = path.join(channelDir, 'state.json');
|
|
397
500
|
let cwd: string | undefined;
|
|
398
501
|
let displayName = name;
|
|
@@ -415,24 +518,28 @@ export const storageService = {
|
|
|
415
518
|
description: '',
|
|
416
519
|
cwd,
|
|
417
520
|
participants,
|
|
418
|
-
createdAt:
|
|
419
|
-
updatedAt:
|
|
521
|
+
createdAt: stats.birthtime,
|
|
522
|
+
updatedAt: stats.mtime,
|
|
420
523
|
};
|
|
421
|
-
const rid = lastReadByChannel[name];
|
|
422
|
-
try {
|
|
423
|
-
const events = await storageService.getEvents({ channelId: name });
|
|
424
|
-
const latestId = events[events.length - 1]?.id;
|
|
425
|
-
channel.hasUnseenMessages = !!(latestId && latestId !== rid);
|
|
426
|
-
} catch {
|
|
427
|
-
channel.hasUnseenMessages = false;
|
|
428
|
-
}
|
|
429
524
|
|
|
525
|
+
const channelLastRead = lastReadMap[name] || {};
|
|
526
|
+
|
|
430
527
|
try {
|
|
528
|
+
// Check root unread
|
|
529
|
+
const rootEvents = await storageService.getEvents({ channelId: name });
|
|
530
|
+
const rootLatestId = rootEvents[rootEvents.length - 1]?.id;
|
|
531
|
+
const rootUnseen = !!(rootLatestId && rootLatestId !== channelLastRead[ROOT_THREAD_KEY]);
|
|
532
|
+
|
|
533
|
+
// Check threads unread
|
|
431
534
|
const allThreads = await storageService.getThreads({ channelId: name });
|
|
432
535
|
channel.recentThreads = allThreads
|
|
433
536
|
.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())
|
|
434
537
|
.slice(0, 5);
|
|
538
|
+
|
|
539
|
+
const threadsUnseen = allThreads.some(t => t.hasUnseenMessages);
|
|
540
|
+
channel.hasUnseenMessages = rootUnseen || threadsUnseen;
|
|
435
541
|
} catch {
|
|
542
|
+
channel.hasUnseenMessages = false;
|
|
436
543
|
channel.recentThreads = [];
|
|
437
544
|
}
|
|
438
545
|
|
|
@@ -440,7 +547,7 @@ export const storageService = {
|
|
|
440
547
|
}),
|
|
441
548
|
);
|
|
442
549
|
|
|
443
|
-
return channels;
|
|
550
|
+
return channels.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
444
551
|
},
|
|
445
552
|
createChannel: async ({
|
|
446
553
|
channelId,
|
|
@@ -472,22 +579,63 @@ export const storageService = {
|
|
|
472
579
|
}
|
|
473
580
|
}
|
|
474
581
|
|
|
475
|
-
const finalState = {
|
|
582
|
+
const finalState: Record<string, unknown> = {
|
|
476
583
|
...(initialState || {}),
|
|
477
584
|
};
|
|
478
585
|
|
|
479
|
-
|
|
480
|
-
(
|
|
481
|
-
|
|
586
|
+
const rawCwd =
|
|
587
|
+
(typeof cwd === 'string' && cwd.trim()) ||
|
|
588
|
+
(typeof finalState.cwd === 'string' && finalState.cwd.trim()) ||
|
|
589
|
+
getDefaultChannelCwd(normalizedChannelId);
|
|
482
590
|
|
|
591
|
+
const resolvedCwd = resolvePath(rawCwd);
|
|
592
|
+
finalState.cwd = resolvedCwd;
|
|
593
|
+
await fs.mkdir(resolvedCwd, { recursive: true });
|
|
483
594
|
await fs.mkdir(channelDir, { recursive: true });
|
|
484
595
|
await fs.writeFile(
|
|
485
596
|
specPath,
|
|
486
597
|
spec?.trim() ||
|
|
487
|
-
`# ${normalizedChannelId}\n\
|
|
598
|
+
`# ${normalizedChannelId}\n\n`,
|
|
488
599
|
);
|
|
489
600
|
await fs.writeFile(statePath, JSON.stringify(finalState, null, 2));
|
|
490
601
|
},
|
|
602
|
+
deleteChannel: async ({ channelId }: { channelId: string }): Promise<void> => {
|
|
603
|
+
const normalizedChannelId = channelId.trim();
|
|
604
|
+
if (!normalizedChannelId) {
|
|
605
|
+
throw new Error('channelId is required');
|
|
606
|
+
}
|
|
607
|
+
if (normalizedChannelId === '_meta') {
|
|
608
|
+
throw new Error('Cannot delete reserved channel path');
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const channelDir = getConversationDir(normalizedChannelId);
|
|
612
|
+
try {
|
|
613
|
+
await fs.access(channelDir);
|
|
614
|
+
} catch (error: unknown) {
|
|
615
|
+
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
|
|
616
|
+
const err = new Error(`Channel "${normalizedChannelId}" does not exist.`);
|
|
617
|
+
(err as Error & { code?: string }).code = 'CHANNEL_NOT_FOUND';
|
|
618
|
+
throw err;
|
|
619
|
+
}
|
|
620
|
+
throw error;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
await fs.rm(channelDir, { recursive: true, force: true });
|
|
624
|
+
|
|
625
|
+
try {
|
|
626
|
+
const lastReadPath = getLastReadFilePath();
|
|
627
|
+
const map = await readJsonFile<Record<string, string>>(lastReadPath, {});
|
|
628
|
+
if (normalizedChannelId in map) {
|
|
629
|
+
delete map[normalizedChannelId];
|
|
630
|
+
await fs.mkdir(path.dirname(lastReadPath), { recursive: true });
|
|
631
|
+
const tmp = `${lastReadPath}.tmp`;
|
|
632
|
+
await fs.writeFile(tmp, JSON.stringify(map, null, 2), 'utf-8');
|
|
633
|
+
await fs.rename(tmp, lastReadPath);
|
|
634
|
+
}
|
|
635
|
+
} catch {
|
|
636
|
+
// ignore last-read cleanup failures
|
|
637
|
+
}
|
|
638
|
+
},
|
|
491
639
|
createThread: async ({
|
|
492
640
|
channelId,
|
|
493
641
|
threadId,
|
|
@@ -522,7 +670,7 @@ export const storageService = {
|
|
|
522
670
|
|
|
523
671
|
const baseState: Record<string, unknown> = { ...(initialState || {}) };
|
|
524
672
|
if (threadTitle?.trim()) {
|
|
525
|
-
baseState.
|
|
673
|
+
baseState.name = threadTitle.trim();
|
|
526
674
|
}
|
|
527
675
|
|
|
528
676
|
await fs.mkdir(threadDir, { recursive: true });
|
|
@@ -538,6 +686,8 @@ export const storageService = {
|
|
|
538
686
|
return [];
|
|
539
687
|
}
|
|
540
688
|
|
|
689
|
+
const lastReadMap = await storageService.getLastReadMap();
|
|
690
|
+
const channelLastRead = lastReadMap[channelId] || {};
|
|
541
691
|
const threadNames = (await fs.readdir(threadsDir)).filter((name) => !name.startsWith('.'));
|
|
542
692
|
|
|
543
693
|
const threads = await Promise.all(
|
|
@@ -550,10 +700,10 @@ export const storageService = {
|
|
|
550
700
|
try {
|
|
551
701
|
const threadStateRaw = await fs.readFile(threadStatePath, 'utf-8');
|
|
552
702
|
const threadState = JSON.parse(threadStateRaw) as Record<string, unknown>;
|
|
553
|
-
const
|
|
554
|
-
typeof threadState.
|
|
555
|
-
if (
|
|
556
|
-
threadDisplayName =
|
|
703
|
+
const threadName =
|
|
704
|
+
typeof threadState.name === 'string' ? threadState.name.trim() : '';
|
|
705
|
+
if (threadName) {
|
|
706
|
+
threadDisplayName = threadName;
|
|
557
707
|
}
|
|
558
708
|
} catch (error: unknown) {
|
|
559
709
|
if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') {
|
|
@@ -564,12 +714,22 @@ export const storageService = {
|
|
|
564
714
|
}
|
|
565
715
|
}
|
|
566
716
|
|
|
717
|
+
let hasUnseen = false;
|
|
718
|
+
try {
|
|
719
|
+
const events = await storageService.getEvents({ channelId, threadId: name });
|
|
720
|
+
const latestId = events[events.length - 1]?.id;
|
|
721
|
+
hasUnseen = !!(latestId && latestId !== channelLastRead[name]);
|
|
722
|
+
} catch {
|
|
723
|
+
// ignore
|
|
724
|
+
}
|
|
725
|
+
|
|
567
726
|
return {
|
|
568
727
|
id: name,
|
|
569
728
|
name: threadDisplayName,
|
|
570
729
|
channelId,
|
|
571
730
|
createdAt: stats.birthtime,
|
|
572
731
|
updatedAt: stats.mtime,
|
|
732
|
+
hasUnseenMessages: hasUnseen,
|
|
573
733
|
};
|
|
574
734
|
}),
|
|
575
735
|
);
|
|
@@ -599,14 +759,14 @@ export const storageService = {
|
|
|
599
759
|
}
|
|
600
760
|
}
|
|
601
761
|
|
|
602
|
-
const
|
|
603
|
-
isRecord(state) && typeof state.
|
|
604
|
-
? state.
|
|
762
|
+
const threadName =
|
|
763
|
+
isRecord(state) && typeof state.name === 'string'
|
|
764
|
+
? state.name.trim()
|
|
605
765
|
: '';
|
|
606
766
|
|
|
607
767
|
return {
|
|
608
768
|
id: threadId,
|
|
609
|
-
name:
|
|
769
|
+
name: threadName || threadId,
|
|
610
770
|
channelId,
|
|
611
771
|
state,
|
|
612
772
|
};
|
|
@@ -741,15 +901,7 @@ export const storageService = {
|
|
|
741
901
|
agentIds.map(async (id) => {
|
|
742
902
|
try {
|
|
743
903
|
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;
|
|
904
|
+
return agentSummaryFromDetails(details);
|
|
753
905
|
} catch {
|
|
754
906
|
return {
|
|
755
907
|
id,
|
|
@@ -764,23 +916,19 @@ export const storageService = {
|
|
|
764
916
|
);
|
|
765
917
|
|
|
766
918
|
const system = await storageService.getAgentDetails({ agentId: SYSTEM_AGENT_ID });
|
|
767
|
-
const builtInSystemAgent
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
image: system.image,
|
|
772
|
-
plugins: system.plugins,
|
|
773
|
-
createdAt: system.createdAt,
|
|
774
|
-
updatedAt: system.updatedAt,
|
|
775
|
-
};
|
|
919
|
+
const builtInSystemAgent = agentSummaryFromDetails(system);
|
|
920
|
+
|
|
921
|
+
const builtInStateRow = await storageService.getAgentDetails({ agentId: STATE_AGENT_ID });
|
|
922
|
+
const builtInStateAgent = agentSummaryFromDetails(builtInStateRow);
|
|
776
923
|
|
|
777
924
|
const deduped = new Map<string, Agent>();
|
|
778
925
|
deduped.set(builtInSystemAgent.id, builtInSystemAgent);
|
|
926
|
+
deduped.set(builtInStateAgent.id, builtInStateAgent);
|
|
779
927
|
for (const agent of agents) {
|
|
780
928
|
if (!deduped.has(agent.id)) deduped.set(agent.id, agent);
|
|
781
929
|
}
|
|
782
930
|
|
|
783
|
-
return Array.from(deduped.values());
|
|
931
|
+
return Array.from(deduped.values()).filter((agent) => !agent.hidden);
|
|
784
932
|
},
|
|
785
933
|
getPlugins: async (): Promise<PluginDescriptor[]> => {
|
|
786
934
|
const [builtIn, fromDisk] = await Promise.all([
|
|
@@ -824,16 +972,17 @@ export const storageService = {
|
|
|
824
972
|
pluginRefs,
|
|
825
973
|
description: typeof data.description === 'string' ? data.description : '',
|
|
826
974
|
image: frontmatterImage || discoveredImage || undefined,
|
|
975
|
+
hidden: parseHiddenFlag(data.hidden),
|
|
827
976
|
createdAt: stats.birthtime,
|
|
828
977
|
updatedAt: stats.mtime,
|
|
829
978
|
};
|
|
830
979
|
} catch (error) {
|
|
831
|
-
if (agentId !== SYSTEM_AGENT_ID) {
|
|
980
|
+
if (agentId !== SYSTEM_AGENT_ID && agentId !== STATE_AGENT_ID) {
|
|
832
981
|
const err = new Error(`Agent "${agentId}" does not exist.`);
|
|
833
982
|
(err as Error & { code?: string }).code = 'AGENT_NOT_FOUND';
|
|
834
983
|
throw err;
|
|
835
984
|
}
|
|
836
|
-
// swallow:
|
|
985
|
+
// swallow: built-in agents have optional `agents/<id>/AGENT.md` overrides
|
|
837
986
|
void error;
|
|
838
987
|
}
|
|
839
988
|
|
|
@@ -841,6 +990,10 @@ export const storageService = {
|
|
|
841
990
|
return getSystemAgentDetails(diskDetails);
|
|
842
991
|
}
|
|
843
992
|
|
|
993
|
+
if (agentId === STATE_AGENT_ID) {
|
|
994
|
+
return getStateAgentDetails(diskDetails);
|
|
995
|
+
}
|
|
996
|
+
|
|
844
997
|
if (!diskDetails) {
|
|
845
998
|
const error = new Error(`Agent "${agentId}" does not exist.`);
|
|
846
999
|
(error as Error & { code?: string }).code = 'AGENT_NOT_FOUND';
|
|
@@ -854,6 +1007,7 @@ export const storageService = {
|
|
|
854
1007
|
name,
|
|
855
1008
|
description = '',
|
|
856
1009
|
image,
|
|
1010
|
+
hidden,
|
|
857
1011
|
instructions,
|
|
858
1012
|
plugins,
|
|
859
1013
|
}: {
|
|
@@ -861,10 +1015,11 @@ export const storageService = {
|
|
|
861
1015
|
name: string;
|
|
862
1016
|
description?: string;
|
|
863
1017
|
image?: string;
|
|
1018
|
+
hidden?: boolean;
|
|
864
1019
|
instructions: string;
|
|
865
1020
|
plugins: PluginRef[];
|
|
866
1021
|
}): Promise<void> => {
|
|
867
|
-
|
|
1022
|
+
assertAgentIdFormat(agentId);
|
|
868
1023
|
const agentDir = resolvePath(path.join(getAgentsRootDir(), agentId));
|
|
869
1024
|
const agentMdPath = path.join(agentDir, 'AGENT.md');
|
|
870
1025
|
|
|
@@ -892,6 +1047,9 @@ export const storageService = {
|
|
|
892
1047
|
if (typeof image === 'string' && image.trim() !== '') {
|
|
893
1048
|
data.image = image.trim();
|
|
894
1049
|
}
|
|
1050
|
+
if (hidden === true) {
|
|
1051
|
+
data.hidden = true;
|
|
1052
|
+
}
|
|
895
1053
|
|
|
896
1054
|
const body = matter.stringify(`${instructions.trim()}\n`, data);
|
|
897
1055
|
await fs.writeFile(agentMdPath, body, 'utf-8');
|
|
@@ -901,6 +1059,7 @@ export const storageService = {
|
|
|
901
1059
|
name,
|
|
902
1060
|
description,
|
|
903
1061
|
image,
|
|
1062
|
+
hidden,
|
|
904
1063
|
instructions,
|
|
905
1064
|
plugins,
|
|
906
1065
|
}: {
|
|
@@ -908,10 +1067,11 @@ export const storageService = {
|
|
|
908
1067
|
name?: string;
|
|
909
1068
|
description?: string;
|
|
910
1069
|
image?: string;
|
|
1070
|
+
hidden?: boolean;
|
|
911
1071
|
instructions?: string;
|
|
912
1072
|
plugins?: PluginRef[];
|
|
913
1073
|
}): Promise<void> => {
|
|
914
|
-
|
|
1074
|
+
assertAgentIdFormat(agentId);
|
|
915
1075
|
const agentDir = resolvePath(path.join(getAgentsRootDir(), agentId));
|
|
916
1076
|
const agentMdPath = path.join(agentDir, 'AGENT.md');
|
|
917
1077
|
|
|
@@ -920,14 +1080,18 @@ export const storageService = {
|
|
|
920
1080
|
raw = await fs.readFile(agentMdPath, 'utf-8');
|
|
921
1081
|
} catch (error: unknown) {
|
|
922
1082
|
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1083
|
+
if (!isBuiltinOverlayAgentId(agentId)) {
|
|
1084
|
+
const err = new Error(`Agent "${agentId}" does not exist.`);
|
|
1085
|
+
(err as Error & { code?: string }).code = 'AGENT_NOT_FOUND';
|
|
1086
|
+
throw err;
|
|
1087
|
+
}
|
|
1088
|
+
raw = '';
|
|
1089
|
+
} else {
|
|
1090
|
+
throw error;
|
|
926
1091
|
}
|
|
927
|
-
throw error;
|
|
928
1092
|
}
|
|
929
1093
|
|
|
930
|
-
const parsed = matter(raw);
|
|
1094
|
+
const parsed = raw === '' ? { data: {}, content: '' } : matter(raw);
|
|
931
1095
|
const nextData: Record<string, unknown> = { ...parsed.data };
|
|
932
1096
|
if (name !== undefined) nextData.name = name;
|
|
933
1097
|
if (description !== undefined) nextData.description = description;
|
|
@@ -939,17 +1103,84 @@ export const storageService = {
|
|
|
939
1103
|
delete nextData.image;
|
|
940
1104
|
}
|
|
941
1105
|
}
|
|
1106
|
+
if (hidden !== undefined) {
|
|
1107
|
+
if (hidden) {
|
|
1108
|
+
nextData.hidden = true;
|
|
1109
|
+
} else {
|
|
1110
|
+
delete nextData.hidden;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
let nextContent = instructions !== undefined ? instructions : parsed.content;
|
|
1115
|
+
|
|
1116
|
+
// Built-in agents merge disk overlays with code defaults on read; on write, partial
|
|
1117
|
+
// updates (e.g. plugins-only) must still persist a complete AGENT.md.
|
|
1118
|
+
if (isBuiltinOverlayAgentId(agentId)) {
|
|
1119
|
+
const pluginRefs =
|
|
1120
|
+
plugins ??
|
|
1121
|
+
(nextData.plugins !== undefined ? parsePluginRefs(nextData.plugins) : undefined);
|
|
1122
|
+
const diskOverrides: Partial<AgentDetails> = {};
|
|
1123
|
+
if (typeof nextData.name === 'string' && nextData.name.trim() !== '') {
|
|
1124
|
+
diskOverrides.name = nextData.name;
|
|
1125
|
+
}
|
|
1126
|
+
if (typeof nextData.description === 'string') {
|
|
1127
|
+
diskOverrides.description = nextData.description;
|
|
1128
|
+
}
|
|
1129
|
+
const trimmedContent = String(nextContent).trim();
|
|
1130
|
+
if (trimmedContent) {
|
|
1131
|
+
diskOverrides.instructions = trimmedContent;
|
|
1132
|
+
}
|
|
1133
|
+
if (pluginRefs && pluginRefs.length > 0) {
|
|
1134
|
+
diskOverrides.pluginRefs = pluginRefs;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
const effective =
|
|
1138
|
+
agentId === SYSTEM_AGENT_ID
|
|
1139
|
+
? getSystemAgentDetails(diskOverrides)
|
|
1140
|
+
: getStateAgentDetails(diskOverrides);
|
|
1141
|
+
|
|
1142
|
+
if (name === undefined) nextData.name = effective.name;
|
|
1143
|
+
if (description === undefined) nextData.description = effective.description;
|
|
1144
|
+
if (instructions === undefined && !String(nextContent).trim()) {
|
|
1145
|
+
nextContent = effective.instructions;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
942
1148
|
|
|
943
|
-
const nextContent = instructions !== undefined ? instructions : parsed.content;
|
|
944
1149
|
const body = matter.stringify(`${String(nextContent).trim()}\n`, nextData);
|
|
1150
|
+
await fs.mkdir(path.dirname(agentMdPath), { recursive: true });
|
|
945
1151
|
await fs.writeFile(agentMdPath, body, 'utf-8');
|
|
946
1152
|
},
|
|
947
1153
|
deleteAgent: async ({ agentId }: { agentId: string }): Promise<void> => {
|
|
948
|
-
|
|
1154
|
+
assertAgentIdFormat(agentId);
|
|
949
1155
|
const agentDir = resolvePath(path.join(getAgentsRootDir(), agentId));
|
|
950
1156
|
const agentMdPath = path.join(agentDir, 'AGENT.md');
|
|
951
1157
|
const packageJsonPath = path.join(agentDir, 'package.json');
|
|
952
1158
|
|
|
1159
|
+
if (isBuiltinOverlayAgentId(agentId)) {
|
|
1160
|
+
try {
|
|
1161
|
+
await fs.access(agentMdPath);
|
|
1162
|
+
} catch (error: unknown) {
|
|
1163
|
+
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
|
|
1164
|
+
const err = new Error(
|
|
1165
|
+
`Agent "${agentId}" has no AGENT.md on disk; nothing to remove (defaults already apply).`,
|
|
1166
|
+
);
|
|
1167
|
+
(err as Error & { code?: string }).code = 'AGENT_NOT_FOUND';
|
|
1168
|
+
throw err;
|
|
1169
|
+
}
|
|
1170
|
+
throw error;
|
|
1171
|
+
}
|
|
1172
|
+
await fs.unlink(agentMdPath);
|
|
1173
|
+
try {
|
|
1174
|
+
const remaining = await fs.readdir(agentDir);
|
|
1175
|
+
if (remaining.length === 0) {
|
|
1176
|
+
await fs.rmdir(agentDir);
|
|
1177
|
+
}
|
|
1178
|
+
} catch {
|
|
1179
|
+
// ignore cleanup failures
|
|
1180
|
+
}
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
953
1184
|
try {
|
|
954
1185
|
await fs.access(agentDir);
|
|
955
1186
|
} catch (error: unknown) {
|
|
@@ -1060,8 +1291,10 @@ export const storageService = {
|
|
|
1060
1291
|
try {
|
|
1061
1292
|
const threadDir = getConversationDir(channelId, threadId);
|
|
1062
1293
|
if (threadId) {
|
|
1294
|
+
let exists = false;
|
|
1063
1295
|
try {
|
|
1064
1296
|
await fs.access(threadDir);
|
|
1297
|
+
exists = true;
|
|
1065
1298
|
} catch (error: unknown) {
|
|
1066
1299
|
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
|
|
1067
1300
|
const threadTitle = buildThreadTitleFromEvent(event);
|
|
@@ -1074,6 +1307,23 @@ export const storageService = {
|
|
|
1074
1307
|
throw error;
|
|
1075
1308
|
}
|
|
1076
1309
|
}
|
|
1310
|
+
|
|
1311
|
+
if (exists) {
|
|
1312
|
+
// If the thread already exists, check if it has a name.
|
|
1313
|
+
// This handles threads created via action:create_thread without a title.
|
|
1314
|
+
const threadDetails = await storageService.getThreadDetails({ channelId, threadId });
|
|
1315
|
+
const currentState = (threadDetails.state as Record<string, unknown>) || {};
|
|
1316
|
+
if (!currentState.name) {
|
|
1317
|
+
const threadTitle = buildThreadTitleFromEvent(event);
|
|
1318
|
+
if (threadTitle) {
|
|
1319
|
+
await storageService.patchThreadState({
|
|
1320
|
+
channelId,
|
|
1321
|
+
threadId,
|
|
1322
|
+
state: { name: threadTitle },
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1077
1327
|
} else {
|
|
1078
1328
|
await fs.mkdir(threadDir, { recursive: true });
|
|
1079
1329
|
}
|
|
@@ -1200,12 +1450,9 @@ export const storageService = {
|
|
|
1200
1450
|
throw new Error('Channel has no CWD configured');
|
|
1201
1451
|
}
|
|
1202
1452
|
|
|
1203
|
-
const
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
if (!targetDir.startsWith(resolvedBase)) {
|
|
1207
|
-
throw new Error('Access denied: directory escape');
|
|
1208
|
-
}
|
|
1453
|
+
const targetDir = subPath
|
|
1454
|
+
? resolveChannelFile(baseCwd, subPath)
|
|
1455
|
+
: resolvePath(baseCwd);
|
|
1209
1456
|
|
|
1210
1457
|
const entries = await fs.readdir(targetDir, { withFileTypes: true });
|
|
1211
1458
|
return entries
|
|
@@ -1230,14 +1477,142 @@ export const storageService = {
|
|
|
1230
1477
|
throw new Error('Channel has no CWD configured');
|
|
1231
1478
|
}
|
|
1232
1479
|
|
|
1233
|
-
const
|
|
1234
|
-
|
|
1480
|
+
const targetFile = resolveChannelFile(baseCwd, filePath);
|
|
1481
|
+
return fs.readFile(targetFile, 'utf-8');
|
|
1482
|
+
},
|
|
1235
1483
|
|
|
1236
|
-
|
|
1237
|
-
|
|
1484
|
+
readChannelFile: async ({
|
|
1485
|
+
channelId,
|
|
1486
|
+
path: filePath,
|
|
1487
|
+
encoding = 'utf8',
|
|
1488
|
+
}: {
|
|
1489
|
+
channelId: string;
|
|
1490
|
+
path: string;
|
|
1491
|
+
encoding?: 'utf8' | 'base64';
|
|
1492
|
+
}): Promise<{ content: string; mimeType: string; size: number }> => {
|
|
1493
|
+
const details = await storageService.getChannelDetails({ channelId });
|
|
1494
|
+
const baseCwd = details.cwd;
|
|
1495
|
+
|
|
1496
|
+
if (!baseCwd) {
|
|
1497
|
+
throw new Error('Channel has no CWD configured');
|
|
1238
1498
|
}
|
|
1239
1499
|
|
|
1240
|
-
|
|
1500
|
+
const targetFile = resolveChannelFile(baseCwd, filePath);
|
|
1501
|
+
const buf = await fs.readFile(targetFile);
|
|
1502
|
+
const content =
|
|
1503
|
+
encoding === 'base64' ? buf.toString('base64') : buf.toString('utf-8');
|
|
1504
|
+
|
|
1505
|
+
return {
|
|
1506
|
+
content,
|
|
1507
|
+
mimeType: guessMimeType(targetFile),
|
|
1508
|
+
size: buf.length,
|
|
1509
|
+
};
|
|
1510
|
+
},
|
|
1511
|
+
|
|
1512
|
+
writeChannelFile: async ({
|
|
1513
|
+
channelId,
|
|
1514
|
+
path: filePath,
|
|
1515
|
+
content,
|
|
1516
|
+
encoding = 'utf8',
|
|
1517
|
+
overwrite = false,
|
|
1518
|
+
}: {
|
|
1519
|
+
channelId: string;
|
|
1520
|
+
path: string;
|
|
1521
|
+
content: string;
|
|
1522
|
+
encoding?: 'utf8' | 'base64';
|
|
1523
|
+
overwrite?: boolean;
|
|
1524
|
+
}): Promise<{ path: string; size: number; mimeType: string }> => {
|
|
1525
|
+
const details = await storageService.getChannelDetails({ channelId });
|
|
1526
|
+
const baseCwd = details.cwd;
|
|
1527
|
+
|
|
1528
|
+
if (!baseCwd) {
|
|
1529
|
+
throw new Error('Channel has no CWD configured');
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
const abs = resolveChannelFile(baseCwd, filePath);
|
|
1533
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
1534
|
+
|
|
1535
|
+
if (!overwrite) {
|
|
1536
|
+
try {
|
|
1537
|
+
await fs.access(abs);
|
|
1538
|
+
throw new Error('File already exists');
|
|
1539
|
+
} catch (error: unknown) {
|
|
1540
|
+
const code = (error as NodeJS.ErrnoException)?.code;
|
|
1541
|
+
if (code !== 'ENOENT') {
|
|
1542
|
+
throw error;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
const buf =
|
|
1548
|
+
encoding === 'base64' ? Buffer.from(content, 'base64') : Buffer.from(content, 'utf8');
|
|
1549
|
+
await fs.writeFile(abs, buf);
|
|
1550
|
+
|
|
1551
|
+
return {
|
|
1552
|
+
path: filePath,
|
|
1553
|
+
size: buf.length,
|
|
1554
|
+
mimeType: guessMimeType(abs),
|
|
1555
|
+
};
|
|
1556
|
+
},
|
|
1557
|
+
|
|
1558
|
+
uploadChannelFile: async ({
|
|
1559
|
+
channelId,
|
|
1560
|
+
path: filePath,
|
|
1561
|
+
body,
|
|
1562
|
+
overwrite = false,
|
|
1563
|
+
}: {
|
|
1564
|
+
channelId: string;
|
|
1565
|
+
path: string;
|
|
1566
|
+
body: Buffer;
|
|
1567
|
+
overwrite?: boolean;
|
|
1568
|
+
}): Promise<{ path: string; size: number; mimeType: string }> => {
|
|
1569
|
+
const details = await storageService.getChannelDetails({ channelId });
|
|
1570
|
+
const baseCwd = details.cwd;
|
|
1571
|
+
|
|
1572
|
+
if (!baseCwd) {
|
|
1573
|
+
throw new Error('Channel has no CWD configured');
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
const abs = resolveChannelFile(baseCwd, filePath);
|
|
1577
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
1578
|
+
|
|
1579
|
+
if (!overwrite) {
|
|
1580
|
+
try {
|
|
1581
|
+
await fs.access(abs);
|
|
1582
|
+
throw new Error('File already exists');
|
|
1583
|
+
} catch (error: unknown) {
|
|
1584
|
+
const code = (error as NodeJS.ErrnoException)?.code;
|
|
1585
|
+
if (code !== 'ENOENT') {
|
|
1586
|
+
throw error;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
await fs.writeFile(abs, body);
|
|
1592
|
+
|
|
1593
|
+
return {
|
|
1594
|
+
path: filePath,
|
|
1595
|
+
size: body.length,
|
|
1596
|
+
mimeType: guessMimeType(abs),
|
|
1597
|
+
};
|
|
1598
|
+
},
|
|
1599
|
+
|
|
1600
|
+
getChannelFileStat: async ({
|
|
1601
|
+
channelId,
|
|
1602
|
+
path: filePath,
|
|
1603
|
+
}: {
|
|
1604
|
+
channelId: string;
|
|
1605
|
+
path: string;
|
|
1606
|
+
}): Promise<{ abs: string; size: number; mimeType: string }> => {
|
|
1607
|
+
const details = await storageService.getChannelDetails({ channelId });
|
|
1608
|
+
const baseCwd = details.cwd;
|
|
1609
|
+
|
|
1610
|
+
if (!baseCwd) {
|
|
1611
|
+
throw new Error('Channel has no CWD configured');
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
const { abs, size } = await statChannelFile(baseCwd, filePath);
|
|
1615
|
+
return { abs, size, mimeType: guessMimeType(abs) };
|
|
1241
1616
|
},
|
|
1242
1617
|
|
|
1243
1618
|
appendMemory: memoryService.appendMemory,
|
|
@@ -1286,12 +1661,17 @@ export const storageService = {
|
|
|
1286
1661
|
}
|
|
1287
1662
|
}
|
|
1288
1663
|
|
|
1664
|
+
const threadState = (threadDetails?.state as Record<string, unknown>) || {};
|
|
1665
|
+
|
|
1289
1666
|
return {
|
|
1290
1667
|
runId,
|
|
1291
1668
|
agentId,
|
|
1292
1669
|
channelId,
|
|
1293
1670
|
threadId,
|
|
1294
1671
|
triggerEvent: event,
|
|
1672
|
+
pendingToolCallIds: Array.isArray(threadState.pendingToolCallIds)
|
|
1673
|
+
? (threadState.pendingToolCallIds as string[])
|
|
1674
|
+
: undefined,
|
|
1295
1675
|
agentDetails: {
|
|
1296
1676
|
id: agentDetails.id,
|
|
1297
1677
|
name: agentDetails.name,
|