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.
Files changed (96) hide show
  1. package/README.md +15 -16
  2. package/dist/app/agent-ids.js +4 -0
  3. package/dist/app/cli.js +1 -1
  4. package/dist/app/config.js +0 -19
  5. package/dist/app/server.js +8 -14
  6. package/dist/bus/services.js +34 -124
  7. package/dist/harness/agent-invoke-run.js +44 -0
  8. package/dist/harness/agent-turn.js +99 -0
  9. package/dist/harness/channel-participants.js +40 -0
  10. package/dist/harness/constants.js +2 -0
  11. package/dist/harness/context-meter.js +97 -0
  12. package/dist/harness/context.js +95 -47
  13. package/dist/harness/dispatch.js +144 -0
  14. package/dist/harness/dispatcher.js +45 -156
  15. package/dist/harness/history.js +177 -0
  16. package/dist/harness/index.js +91 -0
  17. package/dist/harness/orchestration.js +88 -0
  18. package/dist/harness/participants.js +22 -0
  19. package/dist/harness/run-harness.js +154 -0
  20. package/dist/harness/run.js +98 -0
  21. package/dist/harness/runtime-factory.js +0 -34
  22. package/dist/harness/runtime.js +57 -0
  23. package/dist/harness/todo-dispatch.js +51 -0
  24. package/dist/harness/todos.js +5 -0
  25. package/dist/harness/turn.js +79 -0
  26. package/dist/plugins/approval/index.js +105 -149
  27. package/dist/plugins/delegation/index.js +119 -32
  28. package/dist/plugins/memory/index.js +103 -14
  29. package/dist/plugins/memory/service.js +152 -0
  30. package/dist/plugins/openbot/context.js +80 -0
  31. package/dist/plugins/openbot/history.js +98 -0
  32. package/dist/plugins/openbot/index.js +31 -0
  33. package/dist/plugins/openbot/runtime.js +317 -0
  34. package/dist/plugins/openbot/system-prompt.js +5 -0
  35. package/dist/plugins/plugin-manager/index.js +105 -0
  36. package/dist/plugins/storage/index.js +573 -0
  37. package/dist/plugins/storage/service.js +1159 -0
  38. package/dist/plugins/storage-tools/index.js +2 -2
  39. package/dist/plugins/thread-namer/index.js +72 -0
  40. package/dist/plugins/thread-naming/generate-title.js +44 -0
  41. package/dist/plugins/thread-naming/index.js +103 -0
  42. package/dist/plugins/threads/index.js +114 -0
  43. package/dist/plugins/todo/index.js +24 -25
  44. package/dist/plugins/ui/index.js +2 -32
  45. package/dist/registry/plugins.js +3 -9
  46. package/dist/services/plugins/domain.js +1 -0
  47. package/dist/services/plugins/plugin-cache.js +9 -0
  48. package/dist/services/plugins/registry.js +110 -0
  49. package/dist/services/plugins/service.js +177 -0
  50. package/dist/services/plugins/types.js +1 -0
  51. package/dist/services/process.js +29 -0
  52. package/dist/services/storage.js +11 -10
  53. package/dist/services/thread-naming.js +81 -0
  54. package/docs/agents.md +16 -10
  55. package/docs/architecture.md +2 -2
  56. package/docs/plugins.md +6 -15
  57. package/docs/templates/AGENT.example.md +7 -13
  58. package/package.json +1 -2
  59. package/src/app/agent-ids.ts +5 -0
  60. package/src/app/cli.ts +1 -1
  61. package/src/app/config.ts +1 -31
  62. package/src/app/server.ts +8 -16
  63. package/src/app/types.ts +63 -189
  64. package/src/harness/index.ts +145 -0
  65. package/src/plugins/approval/index.ts +91 -189
  66. package/src/plugins/delegation/index.ts +136 -39
  67. package/src/plugins/memory/index.ts +112 -15
  68. package/src/{services/memory.ts → plugins/memory/service.ts} +1 -1
  69. package/src/plugins/openbot/context.ts +91 -0
  70. package/src/plugins/openbot/history.ts +107 -0
  71. package/src/plugins/openbot/index.ts +37 -0
  72. package/src/plugins/openbot/runtime.ts +384 -0
  73. package/src/plugins/openbot/system-prompt.ts +7 -0
  74. package/src/plugins/plugin-manager/index.ts +122 -0
  75. package/src/plugins/shell/index.ts +1 -1
  76. package/src/plugins/storage/index.ts +633 -0
  77. package/src/{services/storage.ts → plugins/storage/service.ts} +224 -67
  78. package/src/{bus/types.ts → services/plugins/domain.ts} +16 -7
  79. package/src/services/plugins/plugin-cache.ts +13 -0
  80. package/src/{registry/plugins.ts → services/plugins/registry.ts} +25 -27
  81. package/src/services/{plugins.ts → plugins/service.ts} +96 -2
  82. package/src/{bus/plugin.ts → services/plugins/types.ts} +3 -3
  83. package/src/bus/services.ts +0 -954
  84. package/src/harness/context.ts +0 -365
  85. package/src/harness/dispatcher.ts +0 -379
  86. package/src/harness/mcp.ts +0 -78
  87. package/src/harness/runtime-factory.ts +0 -129
  88. package/src/harness/todo-advance.ts +0 -128
  89. package/src/plugins/ai-sdk/index.ts +0 -41
  90. package/src/plugins/ai-sdk/runtime.ts +0 -468
  91. package/src/plugins/ai-sdk/system-prompt.ts +0 -18
  92. package/src/plugins/mcp/index.ts +0 -128
  93. package/src/plugins/storage-tools/index.ts +0 -90
  94. package/src/plugins/todo/index.ts +0 -64
  95. package/src/plugins/ui/index.ts +0 -227
  96. /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 data like `todos` or metadata.'),
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 data like `todos` or progress.'),
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: 'Author or rewrite the shared todo plan for the current thread. Pass the full ordered list missing items are removed. Use at the start of multi-step work, or whenever the plan changes shape. For status flips, prefer `todo_update`.',
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 preserve history; omit to create.'),
25
- content: z.string().min(1).describe('What needs to be done. One concrete step.'),
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('Agent id responsible for this step. Pair with `handoff` to delegate.'),
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('The complete, ordered plan.'),
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
- todo_update: {
36
- description: 'Patch a single todo by id. Use to mark progress (`in_progress`, `done`, `cancelled`), rephrase, or reassign without rewriting the whole list.',
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
- id: z.string().describe('Todo id from `todo_write` or the rendered list.'),
39
- status: todoStatus.optional(),
40
- content: z.string().min(1).optional(),
41
- assignee: z.string().optional().describe('Use empty string to clear.'),
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 per-thread task list for coordinating multi-step, multi-agent work.',
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; this plugin only contributes tool definitions.
50
+ // Handlers live in bus/services.ts.
52
51
  },
53
52
  };
54
53
  export default todoPlugin;
@@ -61,7 +61,7 @@ const renderWidgetSchema = z.union([
61
61
  actions: z.array(actionSchema).optional(),
62
62
  }),
63
63
  z.object({
64
- kind: z.enum(['approval', 'todo_list']).describe('Legacy preset. Prefer choice or list.'),
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 presets approval and todo_list are accepted.',
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
  };
@@ -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 { aiSdkPlugin } from '../plugins/ai-sdk/index.js';
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
- [aiSdkPlugin.id]: aiSdkPlugin,
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. "ai-sdk", "shell"), or
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
+ }