openbot 0.4.5 → 0.4.7
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/channel-ids.js +3 -0
- package/dist/app/cli.js +1 -1
- package/dist/app/responding-agent.js +32 -0
- package/dist/app/server.js +33 -5
- package/dist/plugins/openbot/context.js +6 -25
- package/dist/plugins/openbot/index.js +2 -1
- package/dist/plugins/openbot/runtime.js +116 -81
- package/dist/plugins/openbot/system-prompt.js +4 -7
- package/dist/plugins/plugin-manager/index.js +3 -45
- package/dist/plugins/storage/index.js +2 -41
- package/dist/plugins/storage/service.js +54 -14
- package/dist/services/plugins/service.js +0 -4
- package/package.json +2 -1
- package/src/app/channel-ids.ts +5 -0
- package/src/app/cli.ts +1 -1
- package/src/app/responding-agent.ts +46 -0
- package/src/app/server.ts +41 -6
- package/src/app/types.ts +7 -10
- package/src/plugins/openbot/context.ts +7 -26
- package/src/plugins/openbot/index.ts +2 -1
- package/src/plugins/openbot/runtime.ts +137 -89
- package/src/plugins/openbot/system-prompt.ts +4 -9
- package/src/plugins/plugin-manager/index.ts +2 -50
- package/src/plugins/storage/index.ts +4 -46
- package/src/plugins/storage/service.ts +78 -14
- package/src/services/plugins/domain.ts +7 -4
- package/src/services/plugins/service.ts +0 -6
- package/dist/plugins/thread-naming/generate-title.js +0 -44
- package/dist/plugins/thread-naming/index.js +0 -103
- package/dist/services/thread-naming.js +0 -81
|
@@ -2,6 +2,7 @@ import { MelonyPlugin, RuntimeContext } from 'melony';
|
|
|
2
2
|
import { generateText, type LanguageModel } from 'ai';
|
|
3
3
|
import { openai } from '@ai-sdk/openai';
|
|
4
4
|
import { anthropic } from '@ai-sdk/anthropic';
|
|
5
|
+
import { google } from '@ai-sdk/google';
|
|
5
6
|
import { OpenBotEvent, OpenBotState, AgentInvokeEvent } from '../../app/types.js';
|
|
6
7
|
import { eventsToModelMessages } from './history.js';
|
|
7
8
|
import { Storage } from '../../services/plugins/domain.js';
|
|
@@ -10,8 +11,33 @@ import {
|
|
|
10
11
|
ORCHESTRATOR_AGENT_ID,
|
|
11
12
|
buildContext,
|
|
12
13
|
} from './context.js';
|
|
13
|
-
import { saveConfig } from '../../app/config.js';
|
|
14
|
-
import {
|
|
14
|
+
import { saveConfig, DEFAULT_MARKETPLACE_REGISTRY_URL } from '../../app/config.js';
|
|
15
|
+
import { OPENBOT_SYSTEM_PROMPT } from './system-prompt.js';
|
|
16
|
+
|
|
17
|
+
interface ModelRegistry {
|
|
18
|
+
providers: Record<
|
|
19
|
+
string,
|
|
20
|
+
{
|
|
21
|
+
label: string;
|
|
22
|
+
models: Array<{ id: string; label: string; description: string }>;
|
|
23
|
+
}
|
|
24
|
+
>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let cachedRegistry: ModelRegistry | null = null;
|
|
28
|
+
|
|
29
|
+
async function fetchRegistry(): Promise<ModelRegistry | null> {
|
|
30
|
+
if (cachedRegistry) return cachedRegistry;
|
|
31
|
+
try {
|
|
32
|
+
const response = await fetch(DEFAULT_MARKETPLACE_REGISTRY_URL);
|
|
33
|
+
if (!response.ok) throw new Error(`Failed to fetch registry: ${response.statusText}`);
|
|
34
|
+
cachedRegistry = (await response.json()) as ModelRegistry;
|
|
35
|
+
return cachedRegistry;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('[openbot] Failed to fetch model registry:', error);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
15
41
|
|
|
16
42
|
export interface OpenBotRuntimeOptions {
|
|
17
43
|
/** Provider model string (e.g. `openai/gpt-4o-mini`, `anthropic/claude-3-5-sonnet-20240620`). */
|
|
@@ -34,6 +60,8 @@ function resolveModel(modelString: string): LanguageModel {
|
|
|
34
60
|
return openai(modelId);
|
|
35
61
|
case 'anthropic':
|
|
36
62
|
return anthropic(modelId);
|
|
63
|
+
case 'google':
|
|
64
|
+
return google(modelId);
|
|
37
65
|
default:
|
|
38
66
|
throw new Error(`Unsupported AI provider: "${provider}"`);
|
|
39
67
|
}
|
|
@@ -246,49 +274,20 @@ export const openbotRuntime =
|
|
|
246
274
|
errorMessage.includes('authentication');
|
|
247
275
|
|
|
248
276
|
if (isApiKeyError) {
|
|
249
|
-
const [currentProvider, ...rest] = currentModelString.split('/');
|
|
250
|
-
const currentModelId = rest.join('/');
|
|
251
|
-
|
|
252
277
|
yield {
|
|
253
278
|
type: 'client:ui:widget',
|
|
254
279
|
data: {
|
|
255
|
-
kind: '
|
|
256
|
-
widgetId: `
|
|
257
|
-
title: `AI Provider
|
|
258
|
-
description:
|
|
259
|
-
|
|
260
|
-
{
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
type: 'select',
|
|
264
|
-
required: true,
|
|
265
|
-
options: [
|
|
266
|
-
{ label: 'OpenAI', value: 'openai' },
|
|
267
|
-
{ label: 'Anthropic', value: 'anthropic' },
|
|
268
|
-
],
|
|
269
|
-
defaultValue: currentProvider === 'anthropic' ? 'anthropic' : 'openai',
|
|
270
|
-
},
|
|
271
|
-
{
|
|
272
|
-
id: 'model',
|
|
273
|
-
label: 'Model',
|
|
274
|
-
type: 'text',
|
|
275
|
-
description:
|
|
276
|
-
'Model name without the provider prefix (e.g. `gpt-4o-mini` or `claude-3-5-sonnet-20240620`).',
|
|
277
|
-
placeholder: 'gpt-4o-mini',
|
|
278
|
-
required: true,
|
|
279
|
-
defaultValue: currentModelId,
|
|
280
|
-
},
|
|
281
|
-
{
|
|
282
|
-
id: 'apiKey',
|
|
283
|
-
label: 'API Key',
|
|
284
|
-
type: 'text',
|
|
285
|
-
placeholder: `sk-...`,
|
|
286
|
-
required: true,
|
|
287
|
-
},
|
|
280
|
+
kind: 'choice',
|
|
281
|
+
widgetId: `api_provider_selection_${Date.now()}`,
|
|
282
|
+
title: `Setup AI Provider`,
|
|
283
|
+
description: `Select a provider to continue.`,
|
|
284
|
+
actions: [
|
|
285
|
+
{ id: 'openai', label: 'OpenAI', variant: 'primary' },
|
|
286
|
+
{ id: 'anthropic', label: 'Anthropic', variant: 'primary' },
|
|
287
|
+
{ id: 'google', label: 'Google', variant: 'primary' },
|
|
288
288
|
],
|
|
289
|
-
submitLabel: 'Save & Continue',
|
|
290
289
|
metadata: {
|
|
291
|
-
type: '
|
|
290
|
+
type: 'api_provider_selection',
|
|
292
291
|
},
|
|
293
292
|
},
|
|
294
293
|
meta: { agentId: context.state.agentId, threadId },
|
|
@@ -315,49 +314,6 @@ export const openbotRuntime =
|
|
|
315
314
|
|
|
316
315
|
const threadId = event.meta?.threadId || context.state.threadId;
|
|
317
316
|
|
|
318
|
-
// Auto-add participants if tagged in the prompt
|
|
319
|
-
const content = (event as AgentInvokeEvent).data?.content;
|
|
320
|
-
if (content && storage) {
|
|
321
|
-
try {
|
|
322
|
-
const allAgents = await storage.getAgents();
|
|
323
|
-
const tags = content.match(/@([\w-]+)/g);
|
|
324
|
-
if (tags) {
|
|
325
|
-
const taggedAgentIds = tags.map((t) => t.slice(1));
|
|
326
|
-
const validAgentIds = taggedAgentIds.filter((id) =>
|
|
327
|
-
allAgents.some((a) => a.id === id),
|
|
328
|
-
);
|
|
329
|
-
|
|
330
|
-
const currentParticipants = context.state.channelDetails?.participants || [];
|
|
331
|
-
const newParticipants = [...new Set([...currentParticipants, ...validAgentIds])];
|
|
332
|
-
|
|
333
|
-
if (newParticipants.length > currentParticipants.length) {
|
|
334
|
-
// Update storage
|
|
335
|
-
await storage.patchChannelState({
|
|
336
|
-
channelId: context.state.channelId,
|
|
337
|
-
state: { participants: newParticipants },
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
// Refresh local state
|
|
341
|
-
context.state.channelDetails = await storage.getChannelDetails({
|
|
342
|
-
channelId: context.state.channelId,
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
// Notify UI/others about the change
|
|
346
|
-
yield {
|
|
347
|
-
type: 'action:patch_channel_details:result',
|
|
348
|
-
data: { success: true, updatedFields: ['participants'] },
|
|
349
|
-
meta: {
|
|
350
|
-
agentId: context.state.agentId,
|
|
351
|
-
threadId,
|
|
352
|
-
},
|
|
353
|
-
} as OpenBotEvent;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
} catch (error) {
|
|
357
|
-
console.warn('[openbot] Failed to auto-add participants from tags:', error);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
317
|
// clear the tool batch if the agent is invoked
|
|
362
318
|
// this is to prevent the tool batch from being used for a new agent invocation
|
|
363
319
|
await createToolBatchTracker(
|
|
@@ -394,15 +350,98 @@ export const openbotRuntime =
|
|
|
394
350
|
});
|
|
395
351
|
|
|
396
352
|
builder.on('client:ui:widget:response', async function* (event, context) {
|
|
397
|
-
const { metadata, values } = event.data;
|
|
353
|
+
const { metadata, values, actionId } = event.data;
|
|
354
|
+
const threadId = event.meta?.threadId || context.state.threadId;
|
|
355
|
+
|
|
356
|
+
if (metadata?.type === 'api_provider_selection') {
|
|
357
|
+
const provider = actionId;
|
|
358
|
+
const [_, ...rest] = currentModelString.split('/');
|
|
359
|
+
const currentModelId = rest.join('/');
|
|
360
|
+
|
|
361
|
+
const registry = await fetchRegistry();
|
|
362
|
+
const providerData = registry?.providers[provider as string];
|
|
363
|
+
|
|
364
|
+
const providerLinks: Record<string, string> = {
|
|
365
|
+
openai: 'https://platform.openai.com/api-keys',
|
|
366
|
+
anthropic: 'https://console.anthropic.com/settings/keys',
|
|
367
|
+
google: 'https://aistudio.google.com/app/apikey',
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const label = providerData?.label || (provider as string);
|
|
371
|
+
const link = providerLinks[provider as string] || '';
|
|
372
|
+
|
|
373
|
+
const modelOptions = providerData?.models.map((m) => ({
|
|
374
|
+
label: m.label,
|
|
375
|
+
value: m.id,
|
|
376
|
+
}));
|
|
377
|
+
|
|
378
|
+
const defaultModel = modelOptions?.[0]?.value || 'gpt-4o-mini';
|
|
379
|
+
const defaultValue =
|
|
380
|
+
modelOptions?.find((m) => m.value === currentModelId)?.value ||
|
|
381
|
+
currentModelId ||
|
|
382
|
+
defaultModel;
|
|
383
|
+
|
|
384
|
+
yield {
|
|
385
|
+
type: 'client:ui:widget',
|
|
386
|
+
data: {
|
|
387
|
+
widgetId: event.data.widgetId,
|
|
388
|
+
kind: 'message',
|
|
389
|
+
title: 'Provider Selected',
|
|
390
|
+
body: `${label} provider was selected.`,
|
|
391
|
+
state: 'submitted',
|
|
392
|
+
display: 'collapsed',
|
|
393
|
+
disabled: true,
|
|
394
|
+
actions: [],
|
|
395
|
+
},
|
|
396
|
+
meta: { agentId: context.state.agentId, threadId },
|
|
397
|
+
} as OpenBotEvent;
|
|
398
|
+
|
|
399
|
+
yield {
|
|
400
|
+
type: 'client:ui:widget',
|
|
401
|
+
data: {
|
|
402
|
+
kind: 'form',
|
|
403
|
+
widgetId: `api_key_request_${Date.now()}`,
|
|
404
|
+
title: `${label} Setup`,
|
|
405
|
+
description: `Enter your API key and select a model.`,
|
|
406
|
+
fields: [
|
|
407
|
+
{
|
|
408
|
+
id: 'model',
|
|
409
|
+
label: 'Model',
|
|
410
|
+
type: modelOptions ? 'select' : 'text',
|
|
411
|
+
description: modelOptions ? undefined : `Model name (e.g. \`${defaultModel}\`).`,
|
|
412
|
+
options: modelOptions,
|
|
413
|
+
placeholder: defaultModel,
|
|
414
|
+
required: true,
|
|
415
|
+
defaultValue,
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
id: 'apiKey',
|
|
419
|
+
label: 'API Key',
|
|
420
|
+
type: 'password',
|
|
421
|
+
description: `Get your key here: [${link}](${link})`,
|
|
422
|
+
placeholder: `sk-...`,
|
|
423
|
+
required: true,
|
|
424
|
+
},
|
|
425
|
+
],
|
|
426
|
+
submitLabel: 'Save & Continue',
|
|
427
|
+
metadata: {
|
|
428
|
+
type: 'api_key_request',
|
|
429
|
+
provider,
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
meta: { agentId: context.state.agentId, threadId },
|
|
433
|
+
} as OpenBotEvent;
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
398
437
|
if (metadata?.type !== 'api_key_request') return;
|
|
399
|
-
if (!values?.apiKey || !values?.
|
|
438
|
+
if (!values?.apiKey || !values?.model) return;
|
|
400
439
|
|
|
401
|
-
const provider = String(values.provider);
|
|
440
|
+
const provider = String(values.provider || metadata.provider);
|
|
402
441
|
const modelId = String(values.model).trim();
|
|
403
442
|
const apiKey = String(values.apiKey);
|
|
404
443
|
|
|
405
|
-
if (provider !== 'openai' && provider !== 'anthropic') {
|
|
444
|
+
if (provider !== 'openai' && provider !== 'anthropic' && provider !== 'google') {
|
|
406
445
|
yield {
|
|
407
446
|
type: 'agent:output',
|
|
408
447
|
data: { content: `Unsupported provider: ${provider}` },
|
|
@@ -411,7 +450,12 @@ export const openbotRuntime =
|
|
|
411
450
|
return;
|
|
412
451
|
}
|
|
413
452
|
|
|
414
|
-
const envVar =
|
|
453
|
+
const envVar =
|
|
454
|
+
provider === 'openai'
|
|
455
|
+
? 'OPENAI_API_KEY'
|
|
456
|
+
: provider === 'anthropic'
|
|
457
|
+
? 'ANTHROPIC_API_KEY'
|
|
458
|
+
: 'GOOGLE_GENERATIVE_AI_API_KEY';
|
|
415
459
|
const newModelString = `${provider}/${modelId}`;
|
|
416
460
|
|
|
417
461
|
if (!storage) return;
|
|
@@ -460,10 +504,14 @@ export const openbotRuntime =
|
|
|
460
504
|
title: 'API Key Saved',
|
|
461
505
|
body: `Successfully saved ${provider} API key and selected model \`${newModelString}\`. You can now continue your conversation.`,
|
|
462
506
|
state: 'submitted',
|
|
463
|
-
|
|
507
|
+
display: 'collapsed',
|
|
508
|
+
disabled: true,
|
|
509
|
+
actions: [],
|
|
464
510
|
},
|
|
465
511
|
meta: { agentId: context.state.agentId },
|
|
466
512
|
};
|
|
513
|
+
|
|
514
|
+
yield* runLLM(context, threadId);
|
|
467
515
|
} catch (error) {
|
|
468
516
|
yield {
|
|
469
517
|
type: 'agent:output',
|
|
@@ -7,21 +7,16 @@ export const OPENBOT_SYSTEM_PROMPT = [
|
|
|
7
7
|
'- **Credential Guidance**: If an agent or tool requires credentials, inform the user they can be managed under "Settings > Variables".',
|
|
8
8
|
'',
|
|
9
9
|
'# CORE MISSION',
|
|
10
|
-
'You almost never execute tasks yourself. Instead, you delegate tasks to specialized agents
|
|
10
|
+
'You almost never execute tasks yourself. Instead, you delegate tasks to specialized agents. You act as a high-level manager, ensuring the right agent is working on the right task.',
|
|
11
11
|
'',
|
|
12
12
|
'# OPERATIONAL GUIDELINES',
|
|
13
13
|
'- **Channel and Threads**: The main and only way to communicate and act is through channels and threads. There might be a channel called "uncategorized" for general purpose communication.',
|
|
14
|
-
'- **
|
|
15
|
-
'- **
|
|
16
|
-
'- **Bash Tool Usage**: You should use the `bash` tool very rarely. Only use it when the user explicitly requests a command to be run or when it is absolutely necessary for a task that no other participant can handle.',
|
|
14
|
+
'- **Delegation**: You can delegate tasks to any specialized agent in the `INSTALLED AGENTS` list.',
|
|
15
|
+
'- **Bash Tool Usage**: You should use the `bash` tool very rarely. Only use it when the user explicitly requests a command to be run or when it is absolutely necessary for a task that no other agent can handle.',
|
|
17
16
|
'- **Context Awareness**: Use the provided ENVIRONMENT, CHANNEL SPECIFICATION, and MEMORIES to maintain continuity. Do not ask for information already present in these sections.',
|
|
18
17
|
'- **Durable Memory**: Use the `remember` tool to store important facts, preferences, or project details that should persist across sessions.',
|
|
19
|
-
'- **
|
|
18
|
+
'- **Hub-and-Spoke**: Specialized agents cannot communicate directly; as coordinator, you must pass relevant data from one agent to another.',
|
|
20
19
|
'',
|
|
21
20
|
'# COMMUNICATION STYLE',
|
|
22
21
|
'- Be always concise, professional, and proactive.',
|
|
23
22
|
].join('\n');
|
|
24
|
-
|
|
25
|
-
/** Shown in the API key setup form when no provider credentials are configured. */
|
|
26
|
-
export const API_KEY_SETUP_MESSAGE =
|
|
27
|
-
'OpenBot runs AI agents locally with tools, memory, and delegation. Bring your own OpenAI or Anthropic key — it stays on your machine. Use the form below to get started.';
|
|
@@ -67,10 +67,9 @@ export const pluginManagerPlugin: Plugin = {
|
|
|
67
67
|
const {
|
|
68
68
|
channelId: instanceId,
|
|
69
69
|
name: templateName,
|
|
70
|
-
participants: customParticipants,
|
|
71
70
|
initialState: customInitialState,
|
|
72
71
|
} = event.data;
|
|
73
|
-
const {
|
|
72
|
+
const { channels } = await resolveMarketplaceRegistry();
|
|
74
73
|
|
|
75
74
|
// Try to find the template by ID or Name
|
|
76
75
|
const channelListing =
|
|
@@ -78,64 +77,17 @@ export const pluginManagerPlugin: Plugin = {
|
|
|
78
77
|
channels.find((c) => c.name === templateName);
|
|
79
78
|
|
|
80
79
|
const channelId = instanceId;
|
|
81
|
-
const participants = customParticipants || channelListing?.participants || [];
|
|
82
80
|
const initialState = {
|
|
83
81
|
...(channelListing?.initialState || {}),
|
|
84
82
|
...(customInitialState || {}),
|
|
85
83
|
};
|
|
86
84
|
const spec = channelListing?.spec || '';
|
|
87
85
|
|
|
88
|
-
// 1. Auto-install participant agents if missing
|
|
89
|
-
for (const agentId of participants) {
|
|
90
|
-
const existingAgents = await storage.getAgents();
|
|
91
|
-
if (existingAgents.some((a) => a.id === agentId)) {
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Not found locally, look in marketplace
|
|
96
|
-
const agentListing = marketplaceAgents.find((a) => a.id === agentId);
|
|
97
|
-
if (agentListing) {
|
|
98
|
-
console.log(`[plugin-manager] Auto-installing agent ${agentId} for channel ${channelId}`);
|
|
99
|
-
|
|
100
|
-
// Install plugins for this agent
|
|
101
|
-
for (const ref of agentListing.plugins) {
|
|
102
|
-
const installed = await pluginService.isInstalled(ref.id);
|
|
103
|
-
if (
|
|
104
|
-
!installed &&
|
|
105
|
-
ref.id.includes('/') === false &&
|
|
106
|
-
ref.id.includes('-plugin-') === false
|
|
107
|
-
) {
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
if (!installed) {
|
|
111
|
-
try {
|
|
112
|
-
await pluginService.install({ packageName: ref.id });
|
|
113
|
-
} catch (err) {
|
|
114
|
-
console.warn(`[plugins] Failed to pre-install plugin ${ref.id}`, err);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Create the agent
|
|
120
|
-
await storage.createAgent({
|
|
121
|
-
agentId: agentListing.id,
|
|
122
|
-
name: agentListing.name,
|
|
123
|
-
description: agentListing.description,
|
|
124
|
-
image: agentListing.image,
|
|
125
|
-
instructions: agentListing.instructions,
|
|
126
|
-
plugins: agentListing.plugins,
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
86
|
// 2. Create the channel
|
|
132
87
|
await storage.createChannel({
|
|
133
88
|
channelId,
|
|
134
89
|
spec,
|
|
135
|
-
initialState
|
|
136
|
-
...initialState,
|
|
137
|
-
participants,
|
|
138
|
-
},
|
|
90
|
+
initialState,
|
|
139
91
|
});
|
|
140
92
|
|
|
141
93
|
const channelUrl = `/channels/${channelId}`;
|
|
@@ -48,20 +48,13 @@ const storageToolDefinitions = {
|
|
|
48
48
|
'Markdown content for the channel specification (SPEC.md). Use for goals and rules.',
|
|
49
49
|
),
|
|
50
50
|
cwd: z.string().optional().describe('Current working directory for the channel.'),
|
|
51
|
-
participants: z
|
|
52
|
-
.array(z.string())
|
|
53
|
-
.optional()
|
|
54
|
-
.describe(
|
|
55
|
-
'List of agent IDs that are participants in this channel. When a user tags an agent (e.g. @agent-id), you should ensure they are added to this list if they are not already there.',
|
|
56
|
-
),
|
|
57
51
|
})
|
|
58
52
|
.refine(
|
|
59
53
|
(value) =>
|
|
60
54
|
value.state !== undefined ||
|
|
61
55
|
value.spec !== undefined ||
|
|
62
|
-
value.cwd !== undefined
|
|
63
|
-
|
|
64
|
-
{ message: 'Provide at least one of state, spec, cwd, or participants.' },
|
|
56
|
+
value.cwd !== undefined,
|
|
57
|
+
{ message: 'Provide at least one of state, spec, or cwd.' },
|
|
65
58
|
),
|
|
66
59
|
},
|
|
67
60
|
patch_thread_details: {
|
|
@@ -155,7 +148,7 @@ export const storagePlugin: Plugin = {
|
|
|
155
148
|
});
|
|
156
149
|
|
|
157
150
|
builder.on('action:create_channel', async function* (event, context) {
|
|
158
|
-
const { channelId, spec, initialState, cwd
|
|
151
|
+
const { channelId, spec, initialState, cwd } = (event as any).data;
|
|
159
152
|
const rawChannelId = (channelId || '').trim();
|
|
160
153
|
const channelSpec = typeof spec === 'string' ? spec : '';
|
|
161
154
|
|
|
@@ -173,15 +166,6 @@ export const storagePlugin: Plugin = {
|
|
|
173
166
|
const channelUrl = `/channels/${rawChannelId}`;
|
|
174
167
|
|
|
175
168
|
const mergedInitial: Record<string, unknown> = { ...(initialState || {}) };
|
|
176
|
-
if (participants !== undefined) {
|
|
177
|
-
const normalized = Array.isArray(participants)
|
|
178
|
-
? participants
|
|
179
|
-
.filter((x: unknown): x is string => typeof x === 'string')
|
|
180
|
-
.map((s: string) => s.trim())
|
|
181
|
-
.filter(Boolean)
|
|
182
|
-
: [];
|
|
183
|
-
mergedInitial.participants = normalized;
|
|
184
|
-
}
|
|
185
169
|
|
|
186
170
|
try {
|
|
187
171
|
await storage.createChannel({
|
|
@@ -254,7 +238,6 @@ export const storagePlugin: Plugin = {
|
|
|
254
238
|
channelId?: string;
|
|
255
239
|
name?: string;
|
|
256
240
|
cwd?: string;
|
|
257
|
-
participants?: string[];
|
|
258
241
|
};
|
|
259
242
|
const targetChannelId = (data.channelId || context.state.channelId || '').trim();
|
|
260
243
|
const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
|
|
@@ -279,17 +262,6 @@ export const storagePlugin: Plugin = {
|
|
|
279
262
|
patch.cwd = data.cwd.trim();
|
|
280
263
|
updatedFields.push('cwd');
|
|
281
264
|
}
|
|
282
|
-
if (data.participants !== undefined) {
|
|
283
|
-
if (Array.isArray(data.participants)) {
|
|
284
|
-
patch.participants = data.participants
|
|
285
|
-
.filter((x): x is string => typeof x === 'string')
|
|
286
|
-
.map((s) => s.trim())
|
|
287
|
-
.filter(Boolean);
|
|
288
|
-
} else {
|
|
289
|
-
patch.participants = [];
|
|
290
|
-
}
|
|
291
|
-
updatedFields.push('participants');
|
|
292
|
-
}
|
|
293
265
|
|
|
294
266
|
try {
|
|
295
267
|
if (updatedFields.length > 0) {
|
|
@@ -317,13 +289,12 @@ export const storagePlugin: Plugin = {
|
|
|
317
289
|
});
|
|
318
290
|
|
|
319
291
|
builder.on('action:patch_channel_details', async function* (event, context) {
|
|
320
|
-
const updatedFields: ('state' | 'spec' | 'cwd'
|
|
292
|
+
const updatedFields: ('state' | 'spec' | 'cwd')[] = [];
|
|
321
293
|
const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
|
|
322
294
|
const data = (event.data || {}) as {
|
|
323
295
|
state?: Record<string, unknown>;
|
|
324
296
|
spec?: string;
|
|
325
297
|
cwd?: string;
|
|
326
|
-
participants?: string[];
|
|
327
298
|
};
|
|
328
299
|
try {
|
|
329
300
|
if (data.state !== undefined) {
|
|
@@ -347,19 +318,6 @@ export const storagePlugin: Plugin = {
|
|
|
347
318
|
});
|
|
348
319
|
updatedFields.push('cwd');
|
|
349
320
|
}
|
|
350
|
-
if (data.participants !== undefined) {
|
|
351
|
-
const normalized = Array.isArray(data.participants)
|
|
352
|
-
? data.participants
|
|
353
|
-
.filter((x): x is string => typeof x === 'string')
|
|
354
|
-
.map((s) => s.trim())
|
|
355
|
-
.filter(Boolean)
|
|
356
|
-
: [];
|
|
357
|
-
await storage.patchChannelState({
|
|
358
|
-
channelId: context.state.channelId,
|
|
359
|
-
state: { participants: normalized },
|
|
360
|
-
});
|
|
361
|
-
updatedFields.push('participants');
|
|
362
|
-
}
|
|
363
321
|
|
|
364
322
|
context.state.channelDetails = await storage.getChannelDetails({
|
|
365
323
|
channelId: context.state.channelId,
|
|
@@ -418,20 +418,20 @@ const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
|
418
418
|
/** Display-oriented fields persisted in a channel's `state.json`. */
|
|
419
419
|
const readChannelStateFileFields = (
|
|
420
420
|
parsed: unknown,
|
|
421
|
-
): { name?: string; cwd?: string
|
|
421
|
+
): { name?: string; cwd?: string } => {
|
|
422
422
|
if (!isRecord(parsed)) {
|
|
423
|
-
return {
|
|
423
|
+
return {};
|
|
424
424
|
}
|
|
425
425
|
const name =
|
|
426
426
|
typeof parsed.name === 'string' && parsed.name.trim() ? parsed.name.trim() : undefined;
|
|
427
427
|
const cwd = typeof parsed.cwd === 'string' ? parsed.cwd : undefined;
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
434
|
-
return
|
|
428
|
+
return { name, cwd };
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const isChannelProvisioned = async (channelId: string): Promise<boolean> => {
|
|
432
|
+
const statePath = `${getConversationDir(channelId)}/state.json`;
|
|
433
|
+
const state = await readJsonFile(statePath, {});
|
|
434
|
+
return !!readChannelStateFileFields(state).cwd;
|
|
435
435
|
};
|
|
436
436
|
|
|
437
437
|
/**
|
|
@@ -507,24 +507,25 @@ export const storageService = {
|
|
|
507
507
|
const statePath = path.join(channelDir, 'state.json');
|
|
508
508
|
let cwd: string | undefined;
|
|
509
509
|
let displayName = name;
|
|
510
|
-
let participants: string[] = [];
|
|
511
510
|
|
|
512
511
|
try {
|
|
513
512
|
const parsed = await readJsonFile(statePath, {});
|
|
514
513
|
const fields = readChannelStateFileFields(parsed);
|
|
515
514
|
cwd = fields.cwd;
|
|
516
515
|
displayName = fields.name ?? name;
|
|
517
|
-
participants = fields.participants;
|
|
518
516
|
} catch {
|
|
519
517
|
// ignore
|
|
520
518
|
}
|
|
521
519
|
|
|
520
|
+
if (!cwd) {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
|
|
522
524
|
const channel: Channel = {
|
|
523
525
|
id: name,
|
|
524
526
|
name: displayName,
|
|
525
527
|
description: '',
|
|
526
528
|
cwd,
|
|
527
|
-
participants,
|
|
528
529
|
createdAt: stats.birthtime,
|
|
529
530
|
updatedAt: stats.mtime,
|
|
530
531
|
};
|
|
@@ -554,7 +555,9 @@ export const storageService = {
|
|
|
554
555
|
}),
|
|
555
556
|
);
|
|
556
557
|
|
|
557
|
-
return channels
|
|
558
|
+
return channels
|
|
559
|
+
.filter((channel): channel is Channel => channel !== null)
|
|
560
|
+
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
558
561
|
},
|
|
559
562
|
createChannel: async ({
|
|
560
563
|
channelId,
|
|
@@ -606,6 +609,64 @@ export const storageService = {
|
|
|
606
609
|
);
|
|
607
610
|
await writeJsonFileAtomically(statePath, finalState);
|
|
608
611
|
},
|
|
612
|
+
ensureChannel: async ({
|
|
613
|
+
channelId,
|
|
614
|
+
spec,
|
|
615
|
+
initialState,
|
|
616
|
+
cwd,
|
|
617
|
+
}: {
|
|
618
|
+
channelId: string;
|
|
619
|
+
spec?: string;
|
|
620
|
+
initialState?: Record<string, unknown>;
|
|
621
|
+
cwd?: string;
|
|
622
|
+
}): Promise<void> => {
|
|
623
|
+
const normalizedChannelId = channelId.trim();
|
|
624
|
+
if (!normalizedChannelId) {
|
|
625
|
+
throw new Error('channelId is required');
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const channelDir = getConversationDir(normalizedChannelId);
|
|
629
|
+
const specPath = `${channelDir}/SPEC.md`;
|
|
630
|
+
const statePath = `${channelDir}/state.json`;
|
|
631
|
+
|
|
632
|
+
const existingState = await readJsonFile<Record<string, unknown>>(statePath, {});
|
|
633
|
+
const existingFields = readChannelStateFileFields(existingState);
|
|
634
|
+
if (existingFields.cwd) {
|
|
635
|
+
await fs.mkdir(resolvePath(existingFields.cwd), { recursive: true });
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const finalState: Record<string, unknown> = {
|
|
640
|
+
...existingState,
|
|
641
|
+
...(initialState || {}),
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
const rawCwd =
|
|
645
|
+
(typeof cwd === 'string' && cwd.trim()) ||
|
|
646
|
+
(typeof finalState.cwd === 'string' && finalState.cwd.trim()) ||
|
|
647
|
+
getDefaultChannelCwd(normalizedChannelId);
|
|
648
|
+
|
|
649
|
+
const resolvedCwd = resolvePath(rawCwd);
|
|
650
|
+
finalState.cwd = resolvedCwd;
|
|
651
|
+
|
|
652
|
+
await fs.mkdir(resolvedCwd, { recursive: true });
|
|
653
|
+
await fs.mkdir(channelDir, { recursive: true });
|
|
654
|
+
|
|
655
|
+
try {
|
|
656
|
+
await fs.access(specPath);
|
|
657
|
+
} catch (error: unknown) {
|
|
658
|
+
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
|
|
659
|
+
await fs.writeFile(
|
|
660
|
+
specPath,
|
|
661
|
+
spec?.trim() || `# ${normalizedChannelId}\n\n`,
|
|
662
|
+
);
|
|
663
|
+
} else {
|
|
664
|
+
throw error;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
await writeJsonFileAtomically(statePath, finalState);
|
|
669
|
+
},
|
|
609
670
|
deleteChannel: async ({ channelId }: { channelId: string }): Promise<void> => {
|
|
610
671
|
const normalizedChannelId = channelId.trim();
|
|
611
672
|
if (!normalizedChannelId) {
|
|
@@ -808,7 +869,6 @@ export const storageService = {
|
|
|
808
869
|
spec,
|
|
809
870
|
state,
|
|
810
871
|
cwd,
|
|
811
|
-
participants: diskFields.participants,
|
|
812
872
|
};
|
|
813
873
|
|
|
814
874
|
details.threads = await storageService.getThreads({ channelId });
|
|
@@ -1290,6 +1350,10 @@ export const storageService = {
|
|
|
1290
1350
|
event: OpenBotEvent;
|
|
1291
1351
|
}): Promise<void> => {
|
|
1292
1352
|
try {
|
|
1353
|
+
if (!(await isChannelProvisioned(channelId))) {
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1293
1357
|
const threadDir = getConversationDir(channelId, threadId);
|
|
1294
1358
|
if (threadId) {
|
|
1295
1359
|
let exists = false;
|