openbot 0.4.0 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/cli.js +1 -1
- package/dist/app/config.js +10 -0
- package/dist/app/server.js +200 -3
- package/dist/harness/index.js +18 -0
- package/dist/plugins/approval/index.js +35 -20
- package/dist/plugins/bash/index.js +195 -0
- package/dist/plugins/delegation/index.js +6 -2
- package/dist/plugins/openbot/context.js +54 -9
- package/dist/plugins/openbot/history.js +47 -1
- package/dist/plugins/openbot/index.js +43 -3
- package/dist/plugins/openbot/runtime.js +91 -27
- package/dist/plugins/openbot/system-prompt.js +21 -1
- package/dist/plugins/plugin-manager/index.js +87 -3
- package/dist/plugins/shell/index.js +2 -1
- package/dist/plugins/storage/files.js +67 -0
- package/dist/plugins/storage/index.js +184 -7
- package/dist/plugins/storage/service.js +215 -59
- package/dist/plugins/ui/index.js +109 -150
- package/dist/services/abort.js +43 -0
- package/dist/services/plugins/registry.js +5 -3
- package/dist/services/plugins/service.js +66 -11
- package/docs/agents.md +5 -8
- package/docs/architecture.md +1 -1
- package/docs/plugins.md +28 -7
- package/docs/templates/AGENT.example.md +4 -4
- package/package.json +7 -7
- package/src/app/cli.ts +1 -1
- package/src/app/config.ts +13 -0
- package/src/app/server.ts +235 -3
- package/src/app/types.ts +284 -14
- package/src/harness/index.ts +21 -0
- package/src/plugins/approval/index.ts +37 -20
- package/src/plugins/bash/index.ts +232 -0
- package/src/plugins/delegation/index.ts +7 -2
- package/src/plugins/openbot/context.ts +58 -9
- package/src/plugins/openbot/history.ts +52 -1
- package/src/plugins/openbot/index.ts +45 -3
- package/src/plugins/openbot/runtime.ts +121 -27
- package/src/plugins/openbot/system-prompt.ts +21 -1
- package/src/plugins/plugin-manager/index.ts +105 -3
- package/src/plugins/storage/files.ts +81 -0
- package/src/plugins/storage/index.ts +198 -8
- package/src/plugins/storage/service.ts +282 -59
- package/src/plugins/ui/index.ts +123 -0
- package/src/services/abort.ts +46 -0
- package/src/services/plugins/domain.ts +34 -1
- package/src/services/plugins/registry.ts +5 -3
- package/src/services/plugins/service.ts +136 -45
- package/src/services/plugins/types.ts +5 -1
- package/src/plugins/shell/index.ts +0 -123
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ORCHESTRATOR_AGENT_ID, STATE_AGENT_ID } from '../../app/agent-ids.js';
|
|
2
|
-
import { DEFAULT_PLUGINS_DIR, DEFAULT_AGENTS_DIR, DEFAULT_BASE_DIR, DEFAULT_CHANNELS_DIR, loadConfig, resolvePath, VARIABLES_FILE, } from '../../app/config.js';
|
|
2
|
+
import { DEFAULT_PLUGINS_DIR, DEFAULT_AGENTS_DIR, DEFAULT_BASE_DIR, DEFAULT_CHANNELS_DIR, getDefaultChannelCwd, loadConfig, resolvePath, VARIABLES_FILE, } from '../../app/config.js';
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
4
|
import { readFileSync } from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
@@ -7,10 +7,10 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
7
7
|
import crypto from 'node:crypto';
|
|
8
8
|
import matter from 'gray-matter';
|
|
9
9
|
import { openbotPlugin } from '../openbot/index.js';
|
|
10
|
-
import { OPENBOT_SYSTEM_PROMPT } from '../openbot/system-prompt.js';
|
|
11
10
|
import { listBuiltInPlugins, parsePluginModule } from '../../services/plugins/registry.js';
|
|
12
11
|
import { processService } from '../../services/process.js';
|
|
13
12
|
import { memoryService } from '../memory/service.js';
|
|
13
|
+
import { guessMimeType, resolveChannelFile, statChannelFile, } from './files.js';
|
|
14
14
|
const resolveBaseDir = () => {
|
|
15
15
|
const config = loadConfig();
|
|
16
16
|
return resolvePath(config.baseDir || DEFAULT_BASE_DIR);
|
|
@@ -80,14 +80,17 @@ const getConversationDir = (channelId, threadId) => {
|
|
|
80
80
|
/** Built-in orchestrator agent id. Not creatable as a normal disk agent. */
|
|
81
81
|
const SYSTEM_AGENT_ID = ORCHESTRATOR_AGENT_ID;
|
|
82
82
|
const SYSTEM_DEFAULT_PLUGINS = [
|
|
83
|
-
{
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
{
|
|
84
|
+
id: 'openbot',
|
|
85
|
+
config: {
|
|
86
|
+
model: 'openai/gpt-5.4-mini',
|
|
87
|
+
approval: {
|
|
88
|
+
actions: ['action:bash', 'action:create_channel', 'action:delete_channel'],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
89
92
|
];
|
|
90
|
-
/** No `openbot` / `
|
|
93
|
+
/** No `openbot` / `bash` — storage-side effects and infra plugins only. */
|
|
91
94
|
const STATE_DEFAULT_PLUGINS = [
|
|
92
95
|
{ id: 'storage' },
|
|
93
96
|
{ id: 'plugin-manager' },
|
|
@@ -99,7 +102,7 @@ function getSystemAgentDetails(overrides) {
|
|
|
99
102
|
name: 'OpenBot',
|
|
100
103
|
image: getBundledSystemAgentImage(),
|
|
101
104
|
description: 'First-party orchestration agent for OpenBot.',
|
|
102
|
-
instructions:
|
|
105
|
+
instructions: '',
|
|
103
106
|
plugins: SYSTEM_DEFAULT_PLUGINS.map((ref) => ref.id),
|
|
104
107
|
pluginRefs: SYSTEM_DEFAULT_PLUGINS,
|
|
105
108
|
createdAt: new Date(),
|
|
@@ -179,6 +182,22 @@ const assertAgentIdFormat = (agentId) => {
|
|
|
179
182
|
};
|
|
180
183
|
const getAgentsRootDir = () => path.join(resolveBaseDir(), DEFAULT_AGENTS_DIR);
|
|
181
184
|
const getLastReadFilePath = () => path.join(resolvePath(resolveBaseDir() + '/' + DEFAULT_CHANNELS_DIR), '_meta', 'last-read.json');
|
|
185
|
+
/** Sentinel key for channel-root events (no threadId). */
|
|
186
|
+
export const ROOT_THREAD_KEY = '__root__';
|
|
187
|
+
const readLastReadMap = async () => {
|
|
188
|
+
const raw = await readJsonFile(getLastReadFilePath(), {});
|
|
189
|
+
const map = {};
|
|
190
|
+
for (const [channelId, value] of Object.entries(raw)) {
|
|
191
|
+
if (typeof value === 'string') {
|
|
192
|
+
// Migrate old format
|
|
193
|
+
map[channelId] = { [ROOT_THREAD_KEY]: value };
|
|
194
|
+
}
|
|
195
|
+
else if (value && typeof value === 'object') {
|
|
196
|
+
map[channelId] = value;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return map;
|
|
200
|
+
};
|
|
182
201
|
const THREAD_TITLE_MAX_LENGTH = 80;
|
|
183
202
|
const buildThreadTitleFromEvent = (event) => {
|
|
184
203
|
let rawContent = '';
|
|
@@ -197,14 +216,25 @@ const buildThreadTitleFromEvent = (event) => {
|
|
|
197
216
|
};
|
|
198
217
|
const readJsonFile = async (filePath, fallback) => {
|
|
199
218
|
try {
|
|
200
|
-
|
|
219
|
+
const content = (await fs.readFile(filePath, 'utf-8')).trim();
|
|
220
|
+
if (!content)
|
|
221
|
+
return fallback;
|
|
222
|
+
return JSON.parse(content);
|
|
201
223
|
}
|
|
202
224
|
catch (e) {
|
|
203
225
|
if (e?.code === 'ENOENT')
|
|
204
226
|
return fallback;
|
|
227
|
+
if (e instanceof SyntaxError)
|
|
228
|
+
return fallback;
|
|
205
229
|
throw e;
|
|
206
230
|
}
|
|
207
231
|
};
|
|
232
|
+
const writeJsonFileAtomically = async (filePath, data) => {
|
|
233
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
234
|
+
const tmp = `${filePath}.tmp`;
|
|
235
|
+
await fs.writeFile(tmp, JSON.stringify(data, null, 2), 'utf-8');
|
|
236
|
+
await fs.rename(tmp, filePath);
|
|
237
|
+
};
|
|
208
238
|
const toVariablesRecord = (raw) => {
|
|
209
239
|
if (!raw || typeof raw !== 'object') {
|
|
210
240
|
return {};
|
|
@@ -352,15 +382,19 @@ const parsePluginRefs = (raw) => {
|
|
|
352
382
|
};
|
|
353
383
|
const serializePluginRefs = (refs) => refs.map((ref) => (ref.config ? { id: ref.id, config: ref.config } : { id: ref.id }));
|
|
354
384
|
export const storageService = {
|
|
355
|
-
|
|
356
|
-
return
|
|
385
|
+
getLastReadMap: async () => {
|
|
386
|
+
return readLastReadMap();
|
|
357
387
|
},
|
|
358
|
-
|
|
388
|
+
setLastRead: async ({ channelId, threadId, lastReadEventId, }) => {
|
|
359
389
|
const p = getLastReadFilePath();
|
|
360
390
|
await fs.mkdir(path.dirname(p), { recursive: true });
|
|
361
|
-
const map = await
|
|
362
|
-
map[channelId]
|
|
363
|
-
|
|
391
|
+
const map = await readLastReadMap();
|
|
392
|
+
if (!map[channelId])
|
|
393
|
+
map[channelId] = {};
|
|
394
|
+
map[channelId][threadId || ROOT_THREAD_KEY] = lastReadEventId;
|
|
395
|
+
const tmp = `${p}.tmp`;
|
|
396
|
+
await fs.writeFile(tmp, JSON.stringify(map, null, 2), 'utf-8');
|
|
397
|
+
await fs.rename(tmp, p);
|
|
364
398
|
},
|
|
365
399
|
getChannels: async () => {
|
|
366
400
|
const channelsDir = resolvePath(resolveBaseDir() + '/' + DEFAULT_CHANNELS_DIR);
|
|
@@ -371,16 +405,16 @@ export const storageService = {
|
|
|
371
405
|
await fs.mkdir(channelsDir, { recursive: true });
|
|
372
406
|
}
|
|
373
407
|
const channelNames = (await fs.readdir(channelsDir)).filter((name) => !name.startsWith('.') && name !== '_meta');
|
|
374
|
-
const
|
|
408
|
+
const lastReadMap = await storageService.getLastReadMap();
|
|
375
409
|
const channels = await Promise.all(channelNames.map(async (name) => {
|
|
376
410
|
const channelDir = getConversationDir(name);
|
|
411
|
+
const stats = await fs.stat(channelDir);
|
|
377
412
|
const statePath = path.join(channelDir, 'state.json');
|
|
378
413
|
let cwd;
|
|
379
414
|
let displayName = name;
|
|
380
415
|
let participants = [];
|
|
381
416
|
try {
|
|
382
|
-
const
|
|
383
|
-
const parsed = JSON.parse(stateContent);
|
|
417
|
+
const parsed = await readJsonFile(statePath, {});
|
|
384
418
|
const fields = readChannelStateFileFields(parsed);
|
|
385
419
|
cwd = fields.cwd;
|
|
386
420
|
displayName = fields.name ?? name;
|
|
@@ -395,30 +429,30 @@ export const storageService = {
|
|
|
395
429
|
description: '',
|
|
396
430
|
cwd,
|
|
397
431
|
participants,
|
|
398
|
-
createdAt:
|
|
399
|
-
updatedAt:
|
|
432
|
+
createdAt: stats.birthtime,
|
|
433
|
+
updatedAt: stats.mtime,
|
|
400
434
|
};
|
|
401
|
-
const
|
|
402
|
-
try {
|
|
403
|
-
const events = await storageService.getEvents({ channelId: name });
|
|
404
|
-
const latestId = events[events.length - 1]?.id;
|
|
405
|
-
channel.hasUnseenMessages = !!(latestId && latestId !== rid);
|
|
406
|
-
}
|
|
407
|
-
catch {
|
|
408
|
-
channel.hasUnseenMessages = false;
|
|
409
|
-
}
|
|
435
|
+
const channelLastRead = lastReadMap[name] || {};
|
|
410
436
|
try {
|
|
437
|
+
// Check root unread
|
|
438
|
+
const rootEvents = await storageService.getEvents({ channelId: name });
|
|
439
|
+
const rootLatestId = rootEvents[rootEvents.length - 1]?.id;
|
|
440
|
+
const rootUnseen = !!(rootLatestId && rootLatestId !== channelLastRead[ROOT_THREAD_KEY]);
|
|
441
|
+
// Check threads unread
|
|
411
442
|
const allThreads = await storageService.getThreads({ channelId: name });
|
|
412
443
|
channel.recentThreads = allThreads
|
|
413
444
|
.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())
|
|
414
445
|
.slice(0, 5);
|
|
446
|
+
const threadsUnseen = allThreads.some(t => t.hasUnseenMessages);
|
|
447
|
+
channel.hasUnseenMessages = rootUnseen || threadsUnseen;
|
|
415
448
|
}
|
|
416
449
|
catch {
|
|
450
|
+
channel.hasUnseenMessages = false;
|
|
417
451
|
channel.recentThreads = [];
|
|
418
452
|
}
|
|
419
453
|
return channel;
|
|
420
454
|
}));
|
|
421
|
-
return channels;
|
|
455
|
+
return channels.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
422
456
|
},
|
|
423
457
|
createChannel: async ({ channelId, spec, initialState, cwd, }) => {
|
|
424
458
|
const normalizedChannelId = channelId.trim();
|
|
@@ -441,13 +475,52 @@ export const storageService = {
|
|
|
441
475
|
const finalState = {
|
|
442
476
|
...(initialState || {}),
|
|
443
477
|
};
|
|
444
|
-
|
|
445
|
-
finalState.cwd
|
|
446
|
-
|
|
478
|
+
const rawCwd = (typeof cwd === 'string' && cwd.trim()) ||
|
|
479
|
+
(typeof finalState.cwd === 'string' && finalState.cwd.trim()) ||
|
|
480
|
+
getDefaultChannelCwd(normalizedChannelId);
|
|
481
|
+
const resolvedCwd = resolvePath(rawCwd);
|
|
482
|
+
finalState.cwd = resolvedCwd;
|
|
483
|
+
await fs.mkdir(resolvedCwd, { recursive: true });
|
|
447
484
|
await fs.mkdir(channelDir, { recursive: true });
|
|
448
485
|
await fs.writeFile(specPath, spec?.trim() ||
|
|
449
|
-
`# ${normalizedChannelId}\n\
|
|
450
|
-
await
|
|
486
|
+
`# ${normalizedChannelId}\n\n`);
|
|
487
|
+
await writeJsonFileAtomically(statePath, finalState);
|
|
488
|
+
},
|
|
489
|
+
deleteChannel: async ({ channelId }) => {
|
|
490
|
+
const normalizedChannelId = channelId.trim();
|
|
491
|
+
if (!normalizedChannelId) {
|
|
492
|
+
throw new Error('channelId is required');
|
|
493
|
+
}
|
|
494
|
+
if (normalizedChannelId === '_meta') {
|
|
495
|
+
throw new Error('Cannot delete reserved channel path');
|
|
496
|
+
}
|
|
497
|
+
const channelDir = getConversationDir(normalizedChannelId);
|
|
498
|
+
try {
|
|
499
|
+
await fs.access(channelDir);
|
|
500
|
+
}
|
|
501
|
+
catch (error) {
|
|
502
|
+
if (error?.code === 'ENOENT') {
|
|
503
|
+
const err = new Error(`Channel "${normalizedChannelId}" does not exist.`);
|
|
504
|
+
err.code = 'CHANNEL_NOT_FOUND';
|
|
505
|
+
throw err;
|
|
506
|
+
}
|
|
507
|
+
throw error;
|
|
508
|
+
}
|
|
509
|
+
await fs.rm(channelDir, { recursive: true, force: true });
|
|
510
|
+
try {
|
|
511
|
+
const lastReadPath = getLastReadFilePath();
|
|
512
|
+
const map = await readJsonFile(lastReadPath, {});
|
|
513
|
+
if (normalizedChannelId in map) {
|
|
514
|
+
delete map[normalizedChannelId];
|
|
515
|
+
await fs.mkdir(path.dirname(lastReadPath), { recursive: true });
|
|
516
|
+
const tmp = `${lastReadPath}.tmp`;
|
|
517
|
+
await fs.writeFile(tmp, JSON.stringify(map, null, 2), 'utf-8');
|
|
518
|
+
await fs.rename(tmp, lastReadPath);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
// ignore last-read cleanup failures
|
|
523
|
+
}
|
|
451
524
|
},
|
|
452
525
|
createThread: async ({ channelId, threadId, threadTitle, initialState, }) => {
|
|
453
526
|
const normalizedChannelId = channelId.trim();
|
|
@@ -472,8 +545,7 @@ export const storageService = {
|
|
|
472
545
|
if (threadTitle?.trim()) {
|
|
473
546
|
baseState.name = threadTitle.trim();
|
|
474
547
|
}
|
|
475
|
-
await
|
|
476
|
-
await fs.writeFile(statePath, JSON.stringify(baseState, null, 2));
|
|
548
|
+
await writeJsonFileAtomically(statePath, baseState);
|
|
477
549
|
},
|
|
478
550
|
getThreads: async ({ channelId }) => {
|
|
479
551
|
const threadsDir = resolvePath(resolveBaseDir() + '/' + DEFAULT_CHANNELS_DIR + '/' + channelId + '/threads');
|
|
@@ -483,6 +555,8 @@ export const storageService = {
|
|
|
483
555
|
catch {
|
|
484
556
|
return [];
|
|
485
557
|
}
|
|
558
|
+
const lastReadMap = await storageService.getLastReadMap();
|
|
559
|
+
const channelLastRead = lastReadMap[channelId] || {};
|
|
486
560
|
const threadNames = (await fs.readdir(threadsDir)).filter((name) => !name.startsWith('.'));
|
|
487
561
|
const threads = await Promise.all(threadNames.map(async (name) => {
|
|
488
562
|
const threadPath = path.join(threadsDir, name);
|
|
@@ -490,8 +564,7 @@ export const storageService = {
|
|
|
490
564
|
const threadStatePath = path.join(threadPath, 'state.json');
|
|
491
565
|
let threadDisplayName = name;
|
|
492
566
|
try {
|
|
493
|
-
const
|
|
494
|
-
const threadState = JSON.parse(threadStateRaw);
|
|
567
|
+
const threadState = await readJsonFile(threadStatePath, {});
|
|
495
568
|
const threadName = typeof threadState.name === 'string' ? threadState.name.trim() : '';
|
|
496
569
|
if (threadName) {
|
|
497
570
|
threadDisplayName = threadName;
|
|
@@ -502,12 +575,22 @@ export const storageService = {
|
|
|
502
575
|
console.error(`Failed to read thread state for channel ${channelId} thread ${name}`, error);
|
|
503
576
|
}
|
|
504
577
|
}
|
|
578
|
+
let hasUnseen = false;
|
|
579
|
+
try {
|
|
580
|
+
const events = await storageService.getEvents({ channelId, threadId: name });
|
|
581
|
+
const latestId = events[events.length - 1]?.id;
|
|
582
|
+
hasUnseen = !!(latestId && latestId !== channelLastRead[name]);
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
// ignore
|
|
586
|
+
}
|
|
505
587
|
return {
|
|
506
588
|
id: name,
|
|
507
589
|
name: threadDisplayName,
|
|
508
590
|
channelId,
|
|
509
591
|
createdAt: stats.birthtime,
|
|
510
592
|
updatedAt: stats.mtime,
|
|
593
|
+
hasUnseenMessages: hasUnseen,
|
|
511
594
|
};
|
|
512
595
|
}));
|
|
513
596
|
return threads;
|
|
@@ -517,8 +600,7 @@ export const storageService = {
|
|
|
517
600
|
const statePath = `${threadDir}/state.json`;
|
|
518
601
|
let state = {};
|
|
519
602
|
try {
|
|
520
|
-
|
|
521
|
-
state = JSON.parse(stateContent);
|
|
603
|
+
state = await readJsonFile(statePath, {});
|
|
522
604
|
}
|
|
523
605
|
catch (error) {
|
|
524
606
|
if (error?.code !== 'ENOENT') {
|
|
@@ -550,8 +632,7 @@ export const storageService = {
|
|
|
550
632
|
}
|
|
551
633
|
let state = {};
|
|
552
634
|
try {
|
|
553
|
-
|
|
554
|
-
state = JSON.parse(stateContent);
|
|
635
|
+
state = await readJsonFile(statePath, {});
|
|
555
636
|
}
|
|
556
637
|
catch (error) {
|
|
557
638
|
if (error?.code !== 'ENOENT') {
|
|
@@ -582,8 +663,7 @@ export const storageService = {
|
|
|
582
663
|
...currentState,
|
|
583
664
|
...patch,
|
|
584
665
|
};
|
|
585
|
-
await
|
|
586
|
-
await fs.writeFile(statePath, JSON.stringify(newState, null, 2));
|
|
666
|
+
await writeJsonFileAtomically(statePath, newState);
|
|
587
667
|
}
|
|
588
668
|
catch (error) {
|
|
589
669
|
console.error(`Failed to patch channel state for channel ${channelId}`, error);
|
|
@@ -600,8 +680,7 @@ export const storageService = {
|
|
|
600
680
|
...currentState,
|
|
601
681
|
...patch,
|
|
602
682
|
};
|
|
603
|
-
await
|
|
604
|
-
await fs.writeFile(statePath, JSON.stringify(newState, null, 2));
|
|
683
|
+
await writeJsonFileAtomically(statePath, newState);
|
|
605
684
|
}
|
|
606
685
|
catch (error) {
|
|
607
686
|
console.error(`Failed to patch thread state for channel ${channelId} thread ${threadId}`, error);
|
|
@@ -1074,11 +1153,9 @@ export const storageService = {
|
|
|
1074
1153
|
if (!baseCwd) {
|
|
1075
1154
|
throw new Error('Channel has no CWD configured');
|
|
1076
1155
|
}
|
|
1077
|
-
const
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
throw new Error('Access denied: directory escape');
|
|
1081
|
-
}
|
|
1156
|
+
const targetDir = subPath
|
|
1157
|
+
? resolveChannelFile(baseCwd, subPath)
|
|
1158
|
+
: resolvePath(baseCwd);
|
|
1082
1159
|
const entries = await fs.readdir(targetDir, { withFileTypes: true });
|
|
1083
1160
|
return entries
|
|
1084
1161
|
.filter((e) => !e.name.startsWith('.'))
|
|
@@ -1093,13 +1170,88 @@ export const storageService = {
|
|
|
1093
1170
|
if (!baseCwd) {
|
|
1094
1171
|
throw new Error('Channel has no CWD configured');
|
|
1095
1172
|
}
|
|
1096
|
-
const
|
|
1097
|
-
const targetFile = path.resolve(resolvedBase, filePath);
|
|
1098
|
-
if (!targetFile.startsWith(resolvedBase)) {
|
|
1099
|
-
throw new Error('Access denied: directory escape');
|
|
1100
|
-
}
|
|
1173
|
+
const targetFile = resolveChannelFile(baseCwd, filePath);
|
|
1101
1174
|
return fs.readFile(targetFile, 'utf-8');
|
|
1102
1175
|
},
|
|
1176
|
+
readChannelFile: async ({ channelId, path: filePath, encoding = 'utf8', }) => {
|
|
1177
|
+
const details = await storageService.getChannelDetails({ channelId });
|
|
1178
|
+
const baseCwd = details.cwd;
|
|
1179
|
+
if (!baseCwd) {
|
|
1180
|
+
throw new Error('Channel has no CWD configured');
|
|
1181
|
+
}
|
|
1182
|
+
const targetFile = resolveChannelFile(baseCwd, filePath);
|
|
1183
|
+
const buf = await fs.readFile(targetFile);
|
|
1184
|
+
const content = encoding === 'base64' ? buf.toString('base64') : buf.toString('utf-8');
|
|
1185
|
+
return {
|
|
1186
|
+
content,
|
|
1187
|
+
mimeType: guessMimeType(targetFile),
|
|
1188
|
+
size: buf.length,
|
|
1189
|
+
};
|
|
1190
|
+
},
|
|
1191
|
+
writeChannelFile: async ({ channelId, path: filePath, content, encoding = 'utf8', overwrite = false, }) => {
|
|
1192
|
+
const details = await storageService.getChannelDetails({ channelId });
|
|
1193
|
+
const baseCwd = details.cwd;
|
|
1194
|
+
if (!baseCwd) {
|
|
1195
|
+
throw new Error('Channel has no CWD configured');
|
|
1196
|
+
}
|
|
1197
|
+
const abs = resolveChannelFile(baseCwd, filePath);
|
|
1198
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
1199
|
+
if (!overwrite) {
|
|
1200
|
+
try {
|
|
1201
|
+
await fs.access(abs);
|
|
1202
|
+
throw new Error('File already exists');
|
|
1203
|
+
}
|
|
1204
|
+
catch (error) {
|
|
1205
|
+
const code = error?.code;
|
|
1206
|
+
if (code !== 'ENOENT') {
|
|
1207
|
+
throw error;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
const buf = encoding === 'base64' ? Buffer.from(content, 'base64') : Buffer.from(content, 'utf8');
|
|
1212
|
+
await fs.writeFile(abs, buf);
|
|
1213
|
+
return {
|
|
1214
|
+
path: filePath,
|
|
1215
|
+
size: buf.length,
|
|
1216
|
+
mimeType: guessMimeType(abs),
|
|
1217
|
+
};
|
|
1218
|
+
},
|
|
1219
|
+
uploadChannelFile: async ({ channelId, path: filePath, body, overwrite = false, }) => {
|
|
1220
|
+
const details = await storageService.getChannelDetails({ channelId });
|
|
1221
|
+
const baseCwd = details.cwd;
|
|
1222
|
+
if (!baseCwd) {
|
|
1223
|
+
throw new Error('Channel has no CWD configured');
|
|
1224
|
+
}
|
|
1225
|
+
const abs = resolveChannelFile(baseCwd, filePath);
|
|
1226
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
1227
|
+
if (!overwrite) {
|
|
1228
|
+
try {
|
|
1229
|
+
await fs.access(abs);
|
|
1230
|
+
throw new Error('File already exists');
|
|
1231
|
+
}
|
|
1232
|
+
catch (error) {
|
|
1233
|
+
const code = error?.code;
|
|
1234
|
+
if (code !== 'ENOENT') {
|
|
1235
|
+
throw error;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
await fs.writeFile(abs, body);
|
|
1240
|
+
return {
|
|
1241
|
+
path: filePath,
|
|
1242
|
+
size: body.length,
|
|
1243
|
+
mimeType: guessMimeType(abs),
|
|
1244
|
+
};
|
|
1245
|
+
},
|
|
1246
|
+
getChannelFileStat: async ({ channelId, path: filePath, }) => {
|
|
1247
|
+
const details = await storageService.getChannelDetails({ channelId });
|
|
1248
|
+
const baseCwd = details.cwd;
|
|
1249
|
+
if (!baseCwd) {
|
|
1250
|
+
throw new Error('Channel has no CWD configured');
|
|
1251
|
+
}
|
|
1252
|
+
const { abs, size } = await statChannelFile(baseCwd, filePath);
|
|
1253
|
+
return { abs, size, mimeType: guessMimeType(abs) };
|
|
1254
|
+
},
|
|
1103
1255
|
appendMemory: memoryService.appendMemory,
|
|
1104
1256
|
listMemories: memoryService.listMemories,
|
|
1105
1257
|
deleteMemory: memoryService.deleteMemory,
|
|
@@ -1135,12 +1287,16 @@ export const storageService = {
|
|
|
1135
1287
|
console.warn(`[storage] Failed to load thread details for channel ${channelId} thread: ${threadId}`, error);
|
|
1136
1288
|
}
|
|
1137
1289
|
}
|
|
1290
|
+
const threadState = threadDetails?.state || {};
|
|
1138
1291
|
return {
|
|
1139
1292
|
runId,
|
|
1140
1293
|
agentId,
|
|
1141
1294
|
channelId,
|
|
1142
1295
|
threadId,
|
|
1143
1296
|
triggerEvent: event,
|
|
1297
|
+
pendingToolCallIds: Array.isArray(threadState.pendingToolCallIds)
|
|
1298
|
+
? threadState.pendingToolCallIds
|
|
1299
|
+
: undefined,
|
|
1144
1300
|
agentDetails: {
|
|
1145
1301
|
id: agentDetails.id,
|
|
1146
1302
|
name: agentDetails.name,
|