openbot 0.3.6 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -16
- package/dist/app/agent-ids.js +4 -0
- package/dist/app/cli.js +1 -1
- package/dist/app/config.js +0 -19
- package/dist/app/server.js +8 -14
- package/dist/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 +91 -0
- package/dist/harness/orchestration.js +88 -0
- package/dist/harness/participants.js +22 -0
- package/dist/harness/run-harness.js +154 -0
- package/dist/harness/run.js +98 -0
- package/dist/harness/runtime-factory.js +0 -34
- package/dist/harness/runtime.js +57 -0
- package/dist/harness/todo-dispatch.js +51 -0
- package/dist/harness/todos.js +5 -0
- package/dist/harness/turn.js +79 -0
- package/dist/plugins/approval/index.js +105 -149
- package/dist/plugins/delegation/index.js +119 -32
- package/dist/plugins/memory/index.js +103 -14
- package/dist/plugins/memory/service.js +152 -0
- package/dist/plugins/openbot/context.js +80 -0
- package/dist/plugins/openbot/history.js +98 -0
- package/dist/plugins/openbot/index.js +31 -0
- package/dist/plugins/openbot/runtime.js +317 -0
- package/dist/plugins/openbot/system-prompt.js +5 -0
- package/dist/plugins/plugin-manager/index.js +105 -0
- package/dist/plugins/storage/index.js +573 -0
- package/dist/plugins/storage/service.js +1159 -0
- package/dist/plugins/storage-tools/index.js +2 -2
- package/dist/plugins/thread-namer/index.js +72 -0
- package/dist/plugins/thread-naming/generate-title.js +44 -0
- package/dist/plugins/thread-naming/index.js +103 -0
- package/dist/plugins/threads/index.js +114 -0
- package/dist/plugins/todo/index.js +24 -25
- package/dist/plugins/ui/index.js +2 -32
- package/dist/registry/plugins.js +3 -9
- package/dist/services/plugins/domain.js +1 -0
- package/dist/services/plugins/plugin-cache.js +9 -0
- package/dist/services/plugins/registry.js +110 -0
- package/dist/services/plugins/service.js +177 -0
- package/dist/services/plugins/types.js +1 -0
- package/dist/services/process.js +29 -0
- package/dist/services/storage.js +11 -10
- package/dist/services/thread-naming.js +81 -0
- package/docs/agents.md +16 -10
- package/docs/architecture.md +2 -2
- package/docs/plugins.md +6 -15
- package/docs/templates/AGENT.example.md +7 -13
- package/package.json +1 -2
- package/src/app/agent-ids.ts +5 -0
- package/src/app/cli.ts +1 -1
- package/src/app/config.ts +1 -31
- package/src/app/server.ts +8 -16
- package/src/app/types.ts +63 -189
- package/src/harness/index.ts +145 -0
- package/src/plugins/approval/index.ts +91 -189
- package/src/plugins/delegation/index.ts +136 -39
- package/src/plugins/memory/index.ts +112 -15
- package/src/{services/memory.ts → plugins/memory/service.ts} +1 -1
- package/src/plugins/openbot/context.ts +91 -0
- package/src/plugins/openbot/history.ts +107 -0
- package/src/plugins/openbot/index.ts +37 -0
- package/src/plugins/openbot/runtime.ts +384 -0
- package/src/plugins/openbot/system-prompt.ts +7 -0
- package/src/plugins/plugin-manager/index.ts +122 -0
- package/src/plugins/shell/index.ts +1 -1
- package/src/plugins/storage/index.ts +633 -0
- package/src/{services/storage.ts → plugins/storage/service.ts} +224 -67
- package/src/{bus/types.ts → services/plugins/domain.ts} +16 -7
- package/src/services/plugins/plugin-cache.ts +13 -0
- package/src/{registry/plugins.ts → services/plugins/registry.ts} +25 -27
- package/src/services/{plugins.ts → plugins/service.ts} +96 -2
- package/src/{bus/plugin.ts → services/plugins/types.ts} +3 -3
- package/src/bus/services.ts +0 -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/storage-tools/index.ts +0 -90
- package/src/plugins/todo/index.ts +0 -64
- package/src/plugins/ui/index.ts +0 -227
- /package/src/{harness → services}/process.ts +0 -0
|
@@ -32,7 +32,7 @@ const storageToolDefinitions = {
|
|
|
32
32
|
state: z
|
|
33
33
|
.record(z.string(), z.unknown())
|
|
34
34
|
.optional()
|
|
35
|
-
.describe('JSON state object for the channel. Use for structured
|
|
35
|
+
.describe('JSON state object for the channel. Use for structured metadata.'),
|
|
36
36
|
spec: z
|
|
37
37
|
.string()
|
|
38
38
|
.optional()
|
|
@@ -46,7 +46,7 @@ const storageToolDefinitions = {
|
|
|
46
46
|
inputSchema: z.object({
|
|
47
47
|
state: z
|
|
48
48
|
.record(z.string(), z.unknown())
|
|
49
|
-
.describe('JSON state object for the thread. Use for structured
|
|
49
|
+
.describe('JSON state object for the thread. Use for structured progress or metadata.'),
|
|
50
50
|
}),
|
|
51
51
|
},
|
|
52
52
|
create_variable: {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { generateText } from 'ai';
|
|
2
|
+
import { openai } from '@ai-sdk/openai';
|
|
3
|
+
import { anthropic } from '@ai-sdk/anthropic';
|
|
4
|
+
import { THREAD_NAMER_AGENT_ID } from '../../app/agent-ids.js';
|
|
5
|
+
import { loadConfig } from '../../app/config.js';
|
|
6
|
+
const DEFAULT_MODEL = 'openai/gpt-4o-mini';
|
|
7
|
+
const TITLE_SYSTEM_PROMPT = 'Generate a concise thread title from the user message. Reply with the title only: no quotes, no punctuation at the end, max 8 words.';
|
|
8
|
+
function resolveModel(modelString) {
|
|
9
|
+
const [provider, ...rest] = modelString.split('/');
|
|
10
|
+
const modelId = rest.join('/');
|
|
11
|
+
if (!modelId) {
|
|
12
|
+
throw new Error(`Invalid model string: "${modelString}". Expected "provider/model-id".`);
|
|
13
|
+
}
|
|
14
|
+
switch (provider) {
|
|
15
|
+
case 'openai':
|
|
16
|
+
return openai(modelId);
|
|
17
|
+
case 'anthropic':
|
|
18
|
+
return anthropic(modelId);
|
|
19
|
+
default:
|
|
20
|
+
throw new Error(`Unsupported AI provider: "${provider}"`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* `thread-namer` — internal agent plugin that generates short thread titles via LLM.
|
|
25
|
+
*/
|
|
26
|
+
export const threadNamerPlugin = {
|
|
27
|
+
id: 'thread-namer',
|
|
28
|
+
name: 'Thread namer',
|
|
29
|
+
description: 'Generates concise thread titles from seed messages.',
|
|
30
|
+
configSchema: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
model: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
description: 'Provider model string, e.g. openai/gpt-4o-mini',
|
|
36
|
+
default: DEFAULT_MODEL,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
factory: ({ agentId, config }) => {
|
|
41
|
+
if (agentId !== THREAD_NAMER_AGENT_ID) {
|
|
42
|
+
return () => { };
|
|
43
|
+
}
|
|
44
|
+
const configuredModel = typeof config?.model === 'string' && config.model.trim()
|
|
45
|
+
? config.model.trim()
|
|
46
|
+
: loadConfig().model?.trim() || DEFAULT_MODEL;
|
|
47
|
+
return (builder) => {
|
|
48
|
+
builder.on('thread:title:generate', async function* (event) {
|
|
49
|
+
const data = (event.data || {});
|
|
50
|
+
const seedMessage = typeof data.seedMessage === 'string' ? data.seedMessage.trim() : '';
|
|
51
|
+
if (!seedMessage) {
|
|
52
|
+
yield {
|
|
53
|
+
type: 'thread:title:generated',
|
|
54
|
+
data: { title: 'New thread', channelId: data.channelId },
|
|
55
|
+
};
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const result = await generateText({
|
|
59
|
+
model: resolveModel(configuredModel),
|
|
60
|
+
system: TITLE_SYSTEM_PROMPT,
|
|
61
|
+
prompt: seedMessage,
|
|
62
|
+
});
|
|
63
|
+
const title = result.text.replace(/\s+/g, ' ').trim() || 'New thread';
|
|
64
|
+
yield {
|
|
65
|
+
type: 'thread:title:generated',
|
|
66
|
+
data: { title, channelId: data.channelId },
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
export default threadNamerPlugin;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { generateText } from 'ai';
|
|
2
|
+
import { openai } from '@ai-sdk/openai';
|
|
3
|
+
import { anthropic } from '@ai-sdk/anthropic';
|
|
4
|
+
const THREAD_TITLE_MAX_LENGTH = 80;
|
|
5
|
+
function resolveModel(modelString) {
|
|
6
|
+
const [provider, ...rest] = modelString.split('/');
|
|
7
|
+
const modelId = rest.join('/');
|
|
8
|
+
if (!modelId) {
|
|
9
|
+
throw new Error(`Invalid model string: "${modelString}". Expected "provider/model-id".`);
|
|
10
|
+
}
|
|
11
|
+
switch (provider) {
|
|
12
|
+
case 'openai':
|
|
13
|
+
return openai(modelId);
|
|
14
|
+
case 'anthropic':
|
|
15
|
+
return anthropic(modelId);
|
|
16
|
+
default:
|
|
17
|
+
throw new Error(`Unsupported AI provider: "${provider}"`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function normalizeTitle(raw) {
|
|
21
|
+
let title = raw
|
|
22
|
+
.replace(/^["'`]+|["'`]+$/g, '')
|
|
23
|
+
.replace(/[.!?]+$/g, '')
|
|
24
|
+
.replace(/\s+/g, ' ')
|
|
25
|
+
.trim();
|
|
26
|
+
if (!title)
|
|
27
|
+
return '';
|
|
28
|
+
if (title.length > THREAD_TITLE_MAX_LENGTH) {
|
|
29
|
+
title = `${title.slice(0, THREAD_TITLE_MAX_LENGTH).trimEnd()}...`;
|
|
30
|
+
}
|
|
31
|
+
return title;
|
|
32
|
+
}
|
|
33
|
+
export async function generateThreadTitle(content, modelString) {
|
|
34
|
+
const normalized = content.replace(/\s+/g, ' ').trim();
|
|
35
|
+
if (!normalized)
|
|
36
|
+
return undefined;
|
|
37
|
+
const result = await generateText({
|
|
38
|
+
model: resolveModel(modelString),
|
|
39
|
+
system: 'You name chat threads. Reply with ONLY a short title (3-6 words). No quotes, no trailing punctuation.',
|
|
40
|
+
prompt: normalized.slice(0, 500),
|
|
41
|
+
maxOutputTokens: 20,
|
|
42
|
+
});
|
|
43
|
+
return normalizeTitle(result.text) || undefined;
|
|
44
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ORCHESTRATOR_AGENT_ID } from '../../app/agent-ids.js';
|
|
2
|
+
import { loadConfig } from '../../app/config.js';
|
|
3
|
+
import { generateThreadTitle } from './generate-title.js';
|
|
4
|
+
const namingInFlight = new Set();
|
|
5
|
+
function resolveNamingModel(pluginConfig, agentPluginRefs) {
|
|
6
|
+
const fromPlugin = typeof pluginConfig.model === 'string' ? pluginConfig.model.trim() : '';
|
|
7
|
+
if (fromPlugin)
|
|
8
|
+
return fromPlugin;
|
|
9
|
+
const openbotRef = agentPluginRefs?.find((ref) => ref.id === 'openbot');
|
|
10
|
+
const fromOpenbot = typeof openbotRef?.config?.model === 'string' ? openbotRef.config.model.trim() : '';
|
|
11
|
+
if (fromOpenbot)
|
|
12
|
+
return fromOpenbot;
|
|
13
|
+
return loadConfig().model || 'openai/gpt-4o-mini';
|
|
14
|
+
}
|
|
15
|
+
async function maybeGenerateThreadName(args) {
|
|
16
|
+
const details = await args.storage.getThreadDetails({
|
|
17
|
+
channelId: args.channelId,
|
|
18
|
+
threadId: args.threadId,
|
|
19
|
+
});
|
|
20
|
+
const state = details.state || {};
|
|
21
|
+
if (state.nameStatus === 'llm' || state.nameStatus === 'manual')
|
|
22
|
+
return;
|
|
23
|
+
const title = await generateThreadTitle(args.content, args.model);
|
|
24
|
+
if (!title)
|
|
25
|
+
return;
|
|
26
|
+
await args.storage.patchThreadState({
|
|
27
|
+
channelId: args.channelId,
|
|
28
|
+
threadId: args.threadId,
|
|
29
|
+
state: { generatedName: title, nameStatus: 'llm' },
|
|
30
|
+
});
|
|
31
|
+
if (!args.emitEvent)
|
|
32
|
+
return;
|
|
33
|
+
await args.emitEvent({
|
|
34
|
+
type: 'client:ui:thread:updated',
|
|
35
|
+
data: {
|
|
36
|
+
channelId: args.channelId,
|
|
37
|
+
threadId: args.threadId,
|
|
38
|
+
name: title,
|
|
39
|
+
},
|
|
40
|
+
meta: {
|
|
41
|
+
agentId: ORCHESTRATOR_AGENT_ID,
|
|
42
|
+
channelId: args.channelId,
|
|
43
|
+
threadId: args.threadId,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* `thread-naming` — generates short LLM titles for new threads on the system agent.
|
|
49
|
+
* Runs in the background on the first user message so the main turn is not blocked.
|
|
50
|
+
*/
|
|
51
|
+
export const threadNamingPlugin = {
|
|
52
|
+
id: 'thread-naming',
|
|
53
|
+
name: 'Thread naming',
|
|
54
|
+
description: 'Automatically generates short LLM titles for new conversation threads.',
|
|
55
|
+
configSchema: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: {
|
|
58
|
+
model: {
|
|
59
|
+
type: 'string',
|
|
60
|
+
description: 'Provider model string for title generation. Defaults to the openbot plugin model, then workspace config.',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
factory: ({ agentId, agentDetails, config, storage, emitEvent }) => {
|
|
65
|
+
if (agentId !== ORCHESTRATOR_AGENT_ID) {
|
|
66
|
+
return () => { };
|
|
67
|
+
}
|
|
68
|
+
const model = resolveNamingModel(config, agentDetails.pluginRefs);
|
|
69
|
+
return (builder) => {
|
|
70
|
+
builder.on('agent:invoke', async function* (event, context) {
|
|
71
|
+
const invoke = event;
|
|
72
|
+
if (invoke.data?.role && invoke.data.role !== 'user')
|
|
73
|
+
return;
|
|
74
|
+
const threadId = context.state.threadId;
|
|
75
|
+
const channelId = context.state.channelId;
|
|
76
|
+
if (!threadId || !channelId)
|
|
77
|
+
return;
|
|
78
|
+
const content = typeof invoke.data?.content === 'string' ? invoke.data.content : '';
|
|
79
|
+
if (!content.trim())
|
|
80
|
+
return;
|
|
81
|
+
const key = `${channelId}:${threadId}`;
|
|
82
|
+
if (namingInFlight.has(key))
|
|
83
|
+
return;
|
|
84
|
+
namingInFlight.add(key);
|
|
85
|
+
void maybeGenerateThreadName({
|
|
86
|
+
storage,
|
|
87
|
+
channelId,
|
|
88
|
+
threadId,
|
|
89
|
+
content,
|
|
90
|
+
model,
|
|
91
|
+
emitEvent,
|
|
92
|
+
})
|
|
93
|
+
.catch((error) => {
|
|
94
|
+
console.warn('[thread-naming] Failed to generate thread name:', error);
|
|
95
|
+
})
|
|
96
|
+
.finally(() => {
|
|
97
|
+
namingInFlight.delete(key);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
export default threadNamingPlugin;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { generateId } from 'melony';
|
|
2
|
+
import { STATE_AGENT_ID, THREAD_NAMER_AGENT_ID } from '../../app/agent-ids.js';
|
|
3
|
+
import { runAgent } from '../../harness/index.js';
|
|
4
|
+
const THREAD_TITLE_MAX_LENGTH = 80;
|
|
5
|
+
function buildFallbackTitle(seedMessage) {
|
|
6
|
+
const normalized = seedMessage.replace(/\s+/g, ' ').trim();
|
|
7
|
+
if (!normalized)
|
|
8
|
+
return 'New thread';
|
|
9
|
+
if (normalized.length <= THREAD_TITLE_MAX_LENGTH)
|
|
10
|
+
return normalized;
|
|
11
|
+
return `${normalized.slice(0, THREAD_TITLE_MAX_LENGTH).trimEnd()}...`;
|
|
12
|
+
}
|
|
13
|
+
async function requestTitleFromSystem(args) {
|
|
14
|
+
let title;
|
|
15
|
+
await runAgent({
|
|
16
|
+
runId: `tn_${generateId()}`,
|
|
17
|
+
agentId: THREAD_NAMER_AGENT_ID,
|
|
18
|
+
channelId: args.channelId,
|
|
19
|
+
threadId: args.threadId,
|
|
20
|
+
persistEvents: false,
|
|
21
|
+
event: {
|
|
22
|
+
type: 'thread:title:generate',
|
|
23
|
+
data: {
|
|
24
|
+
seedMessage: args.seedMessage,
|
|
25
|
+
channelId: args.channelId,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
onEvent: async (chunk) => {
|
|
29
|
+
if (chunk.type === 'thread:title:generated') {
|
|
30
|
+
const generated = chunk.data.title;
|
|
31
|
+
if (typeof generated === 'string' && generated.trim()) {
|
|
32
|
+
title = generated.trim();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
if (!title) {
|
|
38
|
+
throw new Error('Thread namer did not return a title');
|
|
39
|
+
}
|
|
40
|
+
return title;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* `threads` — orchestrates explicit thread creation on the state agent.
|
|
44
|
+
*/
|
|
45
|
+
export const threadsPlugin = {
|
|
46
|
+
id: 'threads',
|
|
47
|
+
name: 'Threads',
|
|
48
|
+
description: 'Creates threads with LLM-generated titles and notifies clients.',
|
|
49
|
+
factory: ({ agentId, storage }) => {
|
|
50
|
+
if (agentId !== STATE_AGENT_ID) {
|
|
51
|
+
return () => { };
|
|
52
|
+
}
|
|
53
|
+
return (builder) => {
|
|
54
|
+
builder.on('thread:create', async function* (event, context) {
|
|
55
|
+
const data = (event.data || {});
|
|
56
|
+
const channelId = (data.channelId || context.state.channelId || '').trim();
|
|
57
|
+
const threadId = (data.threadId || '').trim();
|
|
58
|
+
if (!channelId || !threadId) {
|
|
59
|
+
yield {
|
|
60
|
+
type: 'thread:create:failed',
|
|
61
|
+
data: {
|
|
62
|
+
channelId,
|
|
63
|
+
threadId,
|
|
64
|
+
error: 'channelId and threadId are required',
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const seedMessage = typeof data.seedMessage === 'string' ? data.seedMessage.trim() : '';
|
|
70
|
+
let title = threadId;
|
|
71
|
+
if (seedMessage) {
|
|
72
|
+
try {
|
|
73
|
+
title = await requestTitleFromSystem({ channelId, threadId, seedMessage });
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
console.warn('[threads] Title generation failed, using fallback', error);
|
|
77
|
+
title = buildFallbackTitle(seedMessage);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
await storage.createThread({
|
|
82
|
+
channelId,
|
|
83
|
+
threadId,
|
|
84
|
+
threadTitle: title,
|
|
85
|
+
initialState: data.initialState,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
90
|
+
if (message.includes('already exists')) {
|
|
91
|
+
const existing = await storage.getThreadDetails({ channelId, threadId });
|
|
92
|
+
title = existing.name || title;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
yield {
|
|
96
|
+
type: 'thread:create:failed',
|
|
97
|
+
data: { channelId, threadId, error: message },
|
|
98
|
+
};
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
yield {
|
|
103
|
+
type: 'thread:created',
|
|
104
|
+
data: { channelId, threadId, title },
|
|
105
|
+
};
|
|
106
|
+
yield {
|
|
107
|
+
type: 'client:invalidate',
|
|
108
|
+
data: { channelId, scopes: ['threads'], threadId },
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
export default threadsPlugin;
|
|
@@ -1,54 +1,53 @@
|
|
|
1
1
|
import z from 'zod';
|
|
2
|
-
/**
|
|
3
|
-
* `todo` — shared, per-thread task list for autonomous multi-agent flows.
|
|
4
|
-
*
|
|
5
|
-
* Todos live in `threadDetails.state.todos` and are owned by the system
|
|
6
|
-
* (handlers in `bus/services.ts`). Any agent in the thread can read the
|
|
7
|
-
* list via context, and propose mutations through these tools. Each item
|
|
8
|
-
* may carry an `assignee` agent id; combine with `handoff` to drive an
|
|
9
|
-
* autonomous, multi-step plan across agents.
|
|
10
|
-
*
|
|
11
|
-
* Keep the surface minimal: two tools (replace-all, patch-one) cover plan
|
|
12
|
-
* authoring, status transitions, and reassignment.
|
|
13
|
-
*/
|
|
14
2
|
const todoStatus = z.enum(['pending', 'in_progress', 'done', 'cancelled']);
|
|
15
3
|
const todoToolDefinitions = {
|
|
16
4
|
todo_write: {
|
|
17
|
-
description: '
|
|
5
|
+
description: 'Manage the shared todo list (create, update, append, remove).',
|
|
18
6
|
inputSchema: z.object({
|
|
19
7
|
todos: z
|
|
20
8
|
.array(z.object({
|
|
21
9
|
id: z
|
|
22
10
|
.string()
|
|
23
11
|
.optional()
|
|
24
|
-
.describe('Stable id. Reuse existing ids to
|
|
25
|
-
content: z.string().min(1).describe('What needs to be done.
|
|
12
|
+
.describe('Stable id. Reuse existing ids to update; omit to create.'),
|
|
13
|
+
content: z.string().min(1).optional().describe('What needs to be done.'),
|
|
26
14
|
status: todoStatus.optional().describe('Defaults to `pending`.'),
|
|
27
15
|
assignee: z
|
|
28
16
|
.string()
|
|
29
17
|
.optional()
|
|
30
|
-
.describe('
|
|
18
|
+
.describe('Suggested agent id for this step (plain id, no @ prefix).'),
|
|
19
|
+
deleted: z.boolean().optional().describe('If true, remove this item.'),
|
|
31
20
|
}))
|
|
32
|
-
.describe('
|
|
21
|
+
.describe('List of todo items to write or patch.'),
|
|
22
|
+
merge: z
|
|
23
|
+
.boolean()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe('If true (default), patches existing items by id and appends new ones. If false, replaces the entire list.'),
|
|
33
26
|
}),
|
|
34
27
|
},
|
|
35
|
-
|
|
36
|
-
description: '
|
|
28
|
+
delegate_to_agent: {
|
|
29
|
+
description: 'Run a worker agent on a self-contained task and return their output. ' +
|
|
30
|
+
'Call when a todo step should be executed by a participant; review the result and update todos before delegating again or replying to the user.',
|
|
37
31
|
inputSchema: z.object({
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
32
|
+
agentId: z
|
|
33
|
+
.string()
|
|
34
|
+
.min(1)
|
|
35
|
+
.describe('Worker agent id from channel participants (plain id, no @ prefix).'),
|
|
36
|
+
task: z
|
|
37
|
+
.string()
|
|
38
|
+
.min(1)
|
|
39
|
+
.describe('Complete instruction for the worker — they do not see the full todo plan.'),
|
|
40
|
+
todoId: z.string().optional().describe('Optional todo id this step relates to.'),
|
|
42
41
|
}),
|
|
43
42
|
},
|
|
44
43
|
};
|
|
45
44
|
export const todoPlugin = {
|
|
46
45
|
id: 'todo',
|
|
47
46
|
name: 'Todo',
|
|
48
|
-
description: 'Shared
|
|
47
|
+
description: 'Shared todo list and worker delegation for multi-step orchestration.',
|
|
49
48
|
toolDefinitions: todoToolDefinitions,
|
|
50
49
|
factory: () => () => {
|
|
51
|
-
// Handlers live in bus/services.ts
|
|
50
|
+
// Handlers live in bus/services.ts.
|
|
52
51
|
},
|
|
53
52
|
};
|
|
54
53
|
export default todoPlugin;
|
package/dist/plugins/ui/index.js
CHANGED
|
@@ -61,7 +61,7 @@ const renderWidgetSchema = z.union([
|
|
|
61
61
|
actions: z.array(actionSchema).optional(),
|
|
62
62
|
}),
|
|
63
63
|
z.object({
|
|
64
|
-
kind: z.
|
|
64
|
+
kind: z.literal('approval').describe('Legacy preset. Prefer choice or list.'),
|
|
65
65
|
widgetId: z.string().optional(),
|
|
66
66
|
title: z.string().optional(),
|
|
67
67
|
props: z.record(z.string(), z.unknown()).optional(),
|
|
@@ -72,21 +72,6 @@ const isRecord = (value) => !!value && typeof value === 'object' && !Array.isArr
|
|
|
72
72
|
const readString = (value) => typeof value === 'string' && value.trim() ? value : undefined;
|
|
73
73
|
const asFields = (value) => Array.isArray(value) ? value : undefined;
|
|
74
74
|
const asListItems = (value) => Array.isArray(value) ? value : undefined;
|
|
75
|
-
const todoToListItem = (todo, index) => {
|
|
76
|
-
if (!isRecord(todo)) {
|
|
77
|
-
return { id: `todo_${index + 1}`, label: String(todo) };
|
|
78
|
-
}
|
|
79
|
-
return {
|
|
80
|
-
id: readString(todo.id) || `todo_${index + 1}`,
|
|
81
|
-
label: readString(todo.label) ||
|
|
82
|
-
readString(todo.task) ||
|
|
83
|
-
readString(todo.title) ||
|
|
84
|
-
`Todo ${index + 1}`,
|
|
85
|
-
description: readString(todo.description),
|
|
86
|
-
status: readString(todo.status),
|
|
87
|
-
metadata: todo,
|
|
88
|
-
};
|
|
89
|
-
};
|
|
90
75
|
const createWidgetId = (data, toolCallId) => {
|
|
91
76
|
if ('widgetId' in data && data.widgetId)
|
|
92
77
|
return data.widgetId;
|
|
@@ -116,21 +101,6 @@ const normalizeWidget = (data, state, toolCallId) => {
|
|
|
116
101
|
],
|
|
117
102
|
};
|
|
118
103
|
}
|
|
119
|
-
if (data.kind === 'todo_list') {
|
|
120
|
-
const props = data.props || {};
|
|
121
|
-
const stateTodos = isRecord(state.threadDetails?.state)
|
|
122
|
-
? state.threadDetails.state.todos
|
|
123
|
-
: undefined;
|
|
124
|
-
const todos = asListItems(props.todos) || asListItems(stateTodos) || [];
|
|
125
|
-
return {
|
|
126
|
-
widgetId,
|
|
127
|
-
kind: 'list',
|
|
128
|
-
title: data.title || readString(props.title) || 'Task List',
|
|
129
|
-
description: readString(props.description),
|
|
130
|
-
metadata: { ...(data.metadata || {}), legacyKind: 'todo_list' },
|
|
131
|
-
items: todos.map(todoToListItem),
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
104
|
if (data.kind === 'form') {
|
|
135
105
|
const propsSource = data.props;
|
|
136
106
|
const props = isRecord(propsSource) ? propsSource : {};
|
|
@@ -160,7 +130,7 @@ const normalizeWidget = (data, state, toolCallId) => {
|
|
|
160
130
|
};
|
|
161
131
|
const uiToolDefinitions = {
|
|
162
132
|
render_ui_widget: {
|
|
163
|
-
description: 'Render a small server-driven UI widget in the conversation. Prefer primitive kinds: message, choice, form, or list. Legacy
|
|
133
|
+
description: 'Render a small server-driven UI widget in the conversation. Prefer primitive kinds: message, choice, form, or list. Legacy preset approval is accepted.',
|
|
164
134
|
inputSchema: renderWidgetSchema,
|
|
165
135
|
},
|
|
166
136
|
};
|
package/dist/registry/plugins.js
CHANGED
|
@@ -1,29 +1,23 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
|
-
import {
|
|
4
|
+
import { openbotPlugin } from '../plugins/openbot/index.js';
|
|
5
5
|
import { shellPlugin } from '../plugins/shell/index.js';
|
|
6
|
-
import { mcpPlugin } from '../plugins/mcp/index.js';
|
|
7
|
-
import { delegationPlugin } from '../plugins/delegation/index.js';
|
|
8
6
|
import { storageToolsPlugin } from '../plugins/storage-tools/index.js';
|
|
9
7
|
import { uiPlugin } from '../plugins/ui/index.js';
|
|
10
8
|
import { approvalPlugin } from '../plugins/approval/index.js';
|
|
11
9
|
import { memoryPlugin } from '../plugins/memory/index.js';
|
|
12
|
-
import { todoPlugin } from '../plugins/todo/index.js';
|
|
13
10
|
import { DEFAULT_PLUGINS_DIR, DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../app/config.js';
|
|
14
11
|
let pluginsDir = null;
|
|
15
12
|
const loadedPlugins = new Set();
|
|
16
13
|
const cache = new Map();
|
|
17
14
|
const BUILT_IN = {
|
|
18
|
-
[
|
|
15
|
+
[openbotPlugin.id]: openbotPlugin,
|
|
19
16
|
[shellPlugin.id]: shellPlugin,
|
|
20
|
-
[mcpPlugin.id]: mcpPlugin,
|
|
21
|
-
[delegationPlugin.id]: delegationPlugin,
|
|
22
17
|
[storageToolsPlugin.id]: storageToolsPlugin,
|
|
23
18
|
[uiPlugin.id]: uiPlugin,
|
|
24
19
|
[approvalPlugin.id]: approvalPlugin,
|
|
25
20
|
[memoryPlugin.id]: memoryPlugin,
|
|
26
|
-
[todoPlugin.id]: todoPlugin,
|
|
27
21
|
};
|
|
28
22
|
/** Normalize a dynamically imported plugin module. Supports `plugin`, `default`. */
|
|
29
23
|
export function parsePluginModule(module) {
|
|
@@ -61,7 +55,7 @@ export function initPlugins(dir) {
|
|
|
61
55
|
}
|
|
62
56
|
/**
|
|
63
57
|
* Resolve a Plugin by id. The id is either:
|
|
64
|
-
* - a built-in id (e.g. "
|
|
58
|
+
* - a built-in id (e.g. "openbot", "shell"), or
|
|
65
59
|
* - an npm package name (e.g. "openbot-plugin-foo" or "@scope/foo"),
|
|
66
60
|
* in which case the folder layout is `plugins/<id>/dist/index.js`.
|
|
67
61
|
*/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Resolved plugins (built-in + community); shared with registry resolution. */
|
|
2
|
+
export const resolvedPluginCache = new Map();
|
|
3
|
+
/** Community plugin ids that have already been logged as loaded once. */
|
|
4
|
+
export const loadedCommunityPlugins = new Set();
|
|
5
|
+
/** Drop a single id from the in-memory resolver cache (e.g. after install/uninstall). */
|
|
6
|
+
export function invalidatePlugin(id) {
|
|
7
|
+
resolvedPluginCache.delete(id);
|
|
8
|
+
loadedCommunityPlugins.delete(id);
|
|
9
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { openbotPlugin } from '../../plugins/openbot/index.js';
|
|
5
|
+
import { shellPlugin } from '../../plugins/shell/index.js';
|
|
6
|
+
import { storagePlugin } from '../../plugins/storage/index.js';
|
|
7
|
+
import { approvalPlugin } from '../../plugins/approval/index.js';
|
|
8
|
+
import { memoryPlugin } from '../../plugins/memory/index.js';
|
|
9
|
+
import { delegationPlugin } from '../../plugins/delegation/index.js';
|
|
10
|
+
import { pluginManagerPlugin } from '../../plugins/plugin-manager/index.js';
|
|
11
|
+
import { DEFAULT_PLUGINS_DIR, DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../../app/config.js';
|
|
12
|
+
import { invalidatePlugin as clearResolvedPluginEntry, loadedCommunityPlugins, resolvedPluginCache, } from './plugin-cache.js';
|
|
13
|
+
let pluginsDir = null;
|
|
14
|
+
const BUILT_IN = {
|
|
15
|
+
[openbotPlugin.id]: openbotPlugin,
|
|
16
|
+
[shellPlugin.id]: shellPlugin,
|
|
17
|
+
[storagePlugin.id]: storagePlugin,
|
|
18
|
+
[approvalPlugin.id]: approvalPlugin,
|
|
19
|
+
[memoryPlugin.id]: memoryPlugin,
|
|
20
|
+
[delegationPlugin.id]: delegationPlugin,
|
|
21
|
+
[pluginManagerPlugin.id]: pluginManagerPlugin,
|
|
22
|
+
};
|
|
23
|
+
/** Normalize a dynamically imported plugin module. Supports `plugin`, `default`. */
|
|
24
|
+
export function parsePluginModule(module) {
|
|
25
|
+
const raw = module.plugin ??
|
|
26
|
+
module.default;
|
|
27
|
+
if (!raw || typeof raw !== 'object')
|
|
28
|
+
return null;
|
|
29
|
+
const factory = raw.factory;
|
|
30
|
+
if (typeof factory !== 'function')
|
|
31
|
+
return null;
|
|
32
|
+
const name = typeof raw.name === 'string' ? raw.name : '';
|
|
33
|
+
const description = typeof raw.description === 'string' ? raw.description : '';
|
|
34
|
+
const image = typeof raw.image === 'string' ? raw.image : undefined;
|
|
35
|
+
const configSchema = raw.configSchema;
|
|
36
|
+
const toolDefinitions = raw.toolDefinitions;
|
|
37
|
+
return {
|
|
38
|
+
name,
|
|
39
|
+
description,
|
|
40
|
+
image,
|
|
41
|
+
configSchema,
|
|
42
|
+
toolDefinitions,
|
|
43
|
+
factory: factory,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Initialize the on-disk plugins directory (defaults to ~/.openbot/plugins). */
|
|
47
|
+
export function initPlugins(dir) {
|
|
48
|
+
if (dir) {
|
|
49
|
+
pluginsDir = dir;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const config = loadConfig();
|
|
53
|
+
const baseDir = config.baseDir || DEFAULT_BASE_DIR;
|
|
54
|
+
pluginsDir = path.join(resolvePath(baseDir), DEFAULT_PLUGINS_DIR);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolve a Plugin by id. The id is either:
|
|
59
|
+
* - a built-in id (e.g. "openbot", "shell"), or
|
|
60
|
+
* - an npm package name (e.g. "openbot-plugin-foo" or "@scope/foo"),
|
|
61
|
+
* in which case the folder layout is `plugins/<id>/dist/index.js`.
|
|
62
|
+
*/
|
|
63
|
+
export async function resolvePlugin(id) {
|
|
64
|
+
if (resolvedPluginCache.has(id))
|
|
65
|
+
return resolvedPluginCache.get(id);
|
|
66
|
+
if (BUILT_IN[id]) {
|
|
67
|
+
resolvedPluginCache.set(id, BUILT_IN[id]);
|
|
68
|
+
return BUILT_IN[id];
|
|
69
|
+
}
|
|
70
|
+
if (!pluginsDir) {
|
|
71
|
+
initPlugins();
|
|
72
|
+
}
|
|
73
|
+
if (!pluginsDir)
|
|
74
|
+
return null;
|
|
75
|
+
const distPath = path.join(pluginsDir, id, 'dist', 'index.js');
|
|
76
|
+
if (!fs.existsSync(distPath)) {
|
|
77
|
+
console.warn(`[plugins] Plugin "${id}" not found at ${distPath}.`);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const module = await import(pathToFileURL(distPath).href);
|
|
82
|
+
const parsed = parsePluginModule(module);
|
|
83
|
+
if (!parsed) {
|
|
84
|
+
console.warn(`[plugins] Plugin "${id}" at ${distPath} has no recognizable export.`);
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const plugin = { id, ...parsed, name: parsed.name || id };
|
|
88
|
+
resolvedPluginCache.set(id, plugin);
|
|
89
|
+
if (!loadedCommunityPlugins.has(id)) {
|
|
90
|
+
console.log(`[plugins] Loaded community plugin "${id}" from ${distPath}`);
|
|
91
|
+
loadedCommunityPlugins.add(id);
|
|
92
|
+
}
|
|
93
|
+
return plugin;
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
console.warn(`[plugins] Failed to load plugin "${id}" from ${distPath}:`, e);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Drop a single id from the in-memory cache (e.g. after fresh install). */
|
|
101
|
+
export function invalidatePlugin(id) {
|
|
102
|
+
clearResolvedPluginEntry(id);
|
|
103
|
+
}
|
|
104
|
+
/** List built-in plugins (for marketplace/registry views). */
|
|
105
|
+
export function listBuiltInPlugins() {
|
|
106
|
+
return Object.values(BUILT_IN);
|
|
107
|
+
}
|
|
108
|
+
export function getPluginsDir() {
|
|
109
|
+
return pluginsDir;
|
|
110
|
+
}
|