openbot 0.2.3 → 0.2.6

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 (49) hide show
  1. package/README.md +1 -1
  2. package/dist/agents/agent-creator.js +58 -19
  3. package/dist/agents/os-agent.js +1 -4
  4. package/dist/agents/planner-agent.js +32 -0
  5. package/dist/agents/topic-agent.js +1 -1
  6. package/dist/architecture/contracts.js +1 -0
  7. package/dist/architecture/execution-engine.js +151 -0
  8. package/dist/architecture/intent-classifier.js +26 -0
  9. package/dist/architecture/planner.js +106 -0
  10. package/dist/automation-worker.js +121 -0
  11. package/dist/automations.js +52 -0
  12. package/dist/cli.js +116 -146
  13. package/dist/config.js +20 -0
  14. package/dist/core/agents.js +41 -0
  15. package/dist/core/delegation.js +124 -0
  16. package/dist/core/manager.js +73 -0
  17. package/dist/core/plugins.js +77 -0
  18. package/dist/core/router.js +40 -0
  19. package/dist/installers.js +156 -0
  20. package/dist/marketplace.js +80 -0
  21. package/dist/open-bot.js +34 -157
  22. package/dist/orchestrator.js +247 -51
  23. package/dist/plugins/approval/index.js +107 -3
  24. package/dist/plugins/brain/index.js +17 -86
  25. package/dist/plugins/brain/memory.js +1 -1
  26. package/dist/plugins/brain/prompt.js +8 -13
  27. package/dist/plugins/brain/types.js +0 -15
  28. package/dist/plugins/file-system/index.js +8 -8
  29. package/dist/plugins/llm/context-shaping.js +177 -0
  30. package/dist/plugins/llm/index.js +223 -49
  31. package/dist/plugins/memory/index.js +220 -0
  32. package/dist/plugins/memory/memory.js +122 -0
  33. package/dist/plugins/memory/prompt.js +55 -0
  34. package/dist/plugins/memory/types.js +45 -0
  35. package/dist/plugins/shell/index.js +3 -3
  36. package/dist/plugins/skills/index.js +9 -9
  37. package/dist/registry/index.js +1 -4
  38. package/dist/registry/plugin-loader.js +361 -56
  39. package/dist/registry/plugin-registry.js +21 -4
  40. package/dist/registry/ts-agent-loader.js +4 -4
  41. package/dist/registry/yaml-agent-loader.js +78 -20
  42. package/dist/runtime/execution-trace.js +41 -0
  43. package/dist/runtime/intent-routing.js +26 -0
  44. package/dist/runtime/openbot-runtime.js +354 -0
  45. package/dist/server.js +513 -41
  46. package/dist/ui/widgets/approval-card.js +22 -2
  47. package/dist/ui/widgets/delegation.js +29 -0
  48. package/dist/version.js +62 -0
  49. package/package.json +4 -1
@@ -1,7 +1,6 @@
1
1
  import { ui } from "@melony/ui-kit/server";
2
2
  import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
- import { createIdentityModule } from "./identity.js";
5
4
  import { createMemoryModule } from "./memory.js";
6
5
  import { buildBrainPrompt } from "./prompt.js";
7
6
  import { statusWidget } from "../../ui/widgets/status.js";
@@ -16,44 +15,40 @@ function expandPath(p) {
16
15
  }
17
16
  /**
18
17
  * Create a prompt-builder function bound to a baseDir.
19
- * Returns the brain's portion of the system prompt (identity + memory).
18
+ * Returns the brain's portion of the system prompt (base context + memory).
20
19
  */
21
- export function createBrainPromptBuilder(baseDir) {
20
+ export function createBrainPromptBuilder(baseDir, baseContext) {
22
21
  const expandedBase = expandPath(baseDir);
23
22
  const modules = {
24
- identity: createIdentityModule(expandedBase),
25
23
  memory: createMemoryModule(expandedBase),
26
24
  };
27
- return async (context) => buildBrainPrompt(expandedBase, modules, context);
25
+ return async (context) => buildBrainPrompt(expandedBase, modules, baseContext || "", context);
28
26
  }
29
27
  // --- Plugin ---
30
28
  /**
31
29
  * Brain Plugin for Melony
32
30
  *
33
- * Provides the bot's "brain": identity and long-term memory with recall.
31
+ * Provides memory capabilities (long-term memory + recall).
34
32
  * Skills are managed by the separate skills plugin.
35
33
  *
36
- * Architecture: thin facade that delegates to focused sub-modules
37
- * (identity, memory) — each independently testable and replaceable.
34
+ * Architecture: thin facade over memory module.
38
35
  */
39
36
  export const brainPlugin = (options) => (builder) => {
40
37
  const { baseDir } = options;
41
38
  const expandedBase = expandPath(baseDir);
42
- // Create sub-modules
43
- const identity = createIdentityModule(expandedBase);
39
+ // Create sub-module
44
40
  const memory = createMemoryModule(expandedBase);
45
41
  // ─── Initialization ───────────────────────────────────────────────
46
- builder.on("init", async function* (_event, context) {
42
+ builder.on("init", async function* (_event) {
47
43
  yield {
48
44
  type: "brain:status",
49
- data: { message: "Initializing brain..." },
45
+ data: { message: "Initializing memory..." },
50
46
  };
51
47
  await fs.mkdir(expandedBase, { recursive: true, mode: 0o700 });
52
- await identity.initialize();
53
48
  await memory.initialize();
54
49
  yield {
55
50
  type: "brain:status",
56
- data: { message: "Brain initialized", severity: "success" },
51
+ data: { message: "Memory initialized", severity: "success" },
57
52
  };
58
53
  });
59
54
  // ─── Memory: Remember ─────────────────────────────────────────────
@@ -66,7 +61,7 @@ export const brainPlugin = (options) => (builder) => {
66
61
  data: { message: "Remembered", severity: "success" },
67
62
  };
68
63
  yield {
69
- type: "action:taskResult",
64
+ type: "action:result",
70
65
  data: {
71
66
  action: "remember",
72
67
  toolCallId,
@@ -87,7 +82,7 @@ export const brainPlugin = (options) => (builder) => {
87
82
  },
88
83
  };
89
84
  yield {
90
- type: "action:taskResult",
85
+ type: "action:result",
91
86
  data: {
92
87
  action: "remember",
93
88
  toolCallId,
@@ -102,7 +97,7 @@ export const brainPlugin = (options) => (builder) => {
102
97
  try {
103
98
  const results = await memory.recall(query, { tags, limit });
104
99
  yield {
105
- type: "action:taskResult",
100
+ type: "action:result",
106
101
  data: {
107
102
  action: "recall",
108
103
  toolCallId,
@@ -120,7 +115,7 @@ export const brainPlugin = (options) => (builder) => {
120
115
  }
121
116
  catch (error) {
122
117
  yield {
123
- type: "action:taskResult",
118
+ type: "action:result",
124
119
  data: {
125
120
  action: "recall",
126
121
  toolCallId,
@@ -142,7 +137,7 @@ export const brainPlugin = (options) => (builder) => {
142
137
  },
143
138
  };
144
139
  yield {
145
- type: "action:taskResult",
140
+ type: "action:result",
146
141
  data: {
147
142
  action: "forget",
148
143
  toolCallId,
@@ -157,7 +152,7 @@ export const brainPlugin = (options) => (builder) => {
157
152
  }
158
153
  catch (error) {
159
154
  yield {
160
- type: "action:taskResult",
155
+ type: "action:result",
161
156
  data: {
162
157
  action: "forget",
163
158
  toolCallId,
@@ -176,7 +171,7 @@ export const brainPlugin = (options) => (builder) => {
176
171
  data: { message: "Journal entry added", severity: "success" },
177
172
  };
178
173
  yield {
179
- type: "action:taskResult",
174
+ type: "action:result",
180
175
  data: {
181
176
  action: "journal",
182
177
  toolCallId,
@@ -193,7 +188,7 @@ export const brainPlugin = (options) => (builder) => {
193
188
  },
194
189
  };
195
190
  yield {
196
- type: "action:taskResult",
191
+ type: "action:result",
197
192
  data: {
198
193
  action: "journal",
199
194
  toolCallId,
@@ -202,70 +197,6 @@ export const brainPlugin = (options) => (builder) => {
202
197
  };
203
198
  }
204
199
  });
205
- // ─── Identity: Update ──────────────────────────────────────────────
206
- builder.on("action:updateIdentity", async function* (event) {
207
- const { content, toolCallId } = event.data;
208
- try {
209
- await identity.updateIdentity(content);
210
- yield {
211
- type: "brain:status",
212
- data: { message: "Identity updated", severity: "success" },
213
- };
214
- yield {
215
- type: "action:taskResult",
216
- data: {
217
- action: "updateIdentity",
218
- toolCallId,
219
- result: {
220
- success: true,
221
- message: "Identity updated. Changes will take effect on next initialization.",
222
- },
223
- },
224
- };
225
- }
226
- catch (error) {
227
- yield {
228
- type: "brain:status",
229
- data: {
230
- message: `Failed to update identity: ${error.message}`,
231
- severity: "error",
232
- },
233
- };
234
- yield {
235
- type: "action:taskResult",
236
- data: {
237
- action: "updateIdentity",
238
- toolCallId,
239
- result: { error: error.message },
240
- },
241
- };
242
- }
243
- });
244
- // ─── Identity: Read ────────────────────────────────────────────────
245
- builder.on("action:readIdentity", async function* (event) {
246
- const { file, toolCallId } = event.data;
247
- try {
248
- const content = await identity.readFile(file);
249
- yield {
250
- type: "action:taskResult",
251
- data: {
252
- action: "readIdentity",
253
- toolCallId,
254
- result: { file, content },
255
- },
256
- };
257
- }
258
- catch (error) {
259
- yield {
260
- type: "action:taskResult",
261
- data: {
262
- action: "readIdentity",
263
- toolCallId,
264
- result: { error: `Could not read ${file}: ${error.message}` },
265
- },
266
- };
267
- }
268
- });
269
200
  builder.on("brain:status", async function* (event) {
270
201
  yield ui.event(statusWidget(event.data.message, event.data.severity));
271
202
  });
@@ -114,7 +114,7 @@ export function createMemoryModule(baseDir) {
114
114
  },
115
115
  async getRecentFacts(limit = 3) {
116
116
  const index = await loadIndex();
117
- return index.entries.slice(-limit);
117
+ return (index?.entries ?? []).slice(-limit);
118
118
  },
119
119
  };
120
120
  }
@@ -4,14 +4,14 @@
4
4
  *
5
5
  * Includes only what the brain owns:
6
6
  * - Environment context
7
- * - Identity + Soul (small, static)
7
+ * - Base context from settings (optional)
8
8
  * - A handful of the most recent memories
9
- * - Brain capability instructions
9
+ * - Memory capability instructions
10
10
  *
11
11
  * Skills are handled by the separate skills plugin and composed
12
12
  * at the top level in open-bot.ts.
13
13
  */
14
- export async function buildBrainPrompt(baseDir, modules, context) {
14
+ export async function buildBrainPrompt(baseDir, modules, baseContext, context) {
15
15
  const parts = [];
16
16
  const state = context?.state;
17
17
  const currentCwd = state?.cwd || process.cwd();
@@ -22,13 +22,10 @@ export async function buildBrainPrompt(baseDir, modules, context) {
22
22
  - CWD: ${currentCwd}
23
23
  - Bot Home: ${baseDir}
24
24
  </environment>`);
25
- // 2. Identity (small, always included)
26
- const identity = await modules.identity.getIdentity();
27
- if (identity)
28
- parts.push(`<identity>\n${identity}\n</identity>`);
29
- const soul = await modules.identity.getSoul();
30
- if (soul)
31
- parts.push(`<soul>\n${soul}\n</soul>`);
25
+ // 2. Base context from settings (optional)
26
+ if (baseContext.trim()) {
27
+ parts.push(`<base_context>\n${baseContext.trim()}\n</base_context>`);
28
+ }
32
29
  // 3. Recent memories (lean — just a few to keep context fresh)
33
30
  const recentFacts = await modules.memory.getRecentFacts(5);
34
31
  if (recentFacts.length > 0) {
@@ -37,15 +34,13 @@ export async function buildBrainPrompt(baseDir, modules, context) {
37
34
  .join("\n");
38
35
  parts.push(`<recent_memories>\n${factsList}\n</recent_memories>`);
39
36
  }
40
- // 4. Brain capabilities
37
+ // 4. Memory capabilities
41
38
  parts.push(`<brain_tools>
42
39
  Use these to manage your persistent state:
43
40
  - \`remember(content, tags)\`: Store facts/preferences
44
41
  - \`recall(query, tags)\`: Search long-term memory
45
42
  - \`forget(memoryId)\`: Remove outdated info
46
43
  - \`journal(content)\`: Record session reflections
47
- - \`updateIdentity(content)\`: Refine your persona
48
- - \`readIdentity(file)\`: Inspect SOUL.md or IDENTITY.md
49
44
  </brain_tools>`);
50
45
  return `\n${parts.join("\n\n")}\n`;
51
46
  }
@@ -42,19 +42,4 @@ export const brainToolDefinitions = {
42
42
  content: z.string().describe("Journal entry content"),
43
43
  }),
44
44
  },
45
- // Identity tools
46
- updateIdentity: {
47
- description: "Update your identity file to refine your personality and traits. Start it with # Identity.",
48
- inputSchema: z.object({
49
- content: z.string().describe("New content for IDENTITY.md"),
50
- }),
51
- },
52
- readIdentity: {
53
- description: "Read your current identity or soul configuration.",
54
- inputSchema: z.object({
55
- file: z
56
- .enum(["IDENTITY.md", "SOUL.md"])
57
- .describe("Which identity file to read"),
58
- }),
59
- },
60
45
  };
@@ -56,7 +56,7 @@ export const fileSystemPlugin = (options = {}) => (builder) => {
56
56
  try {
57
57
  const content = await fs.readFile(resolvePath(filePath, state.cwd), "utf-8");
58
58
  yield {
59
- type: "action:taskResult",
59
+ type: "action:result",
60
60
  data: {
61
61
  action: "readFile",
62
62
  result: { content: truncate(content, maxFileReadLength) },
@@ -70,7 +70,7 @@ export const fileSystemPlugin = (options = {}) => (builder) => {
70
70
  }
71
71
  catch (error) {
72
72
  yield {
73
- type: "action:taskResult",
73
+ type: "action:result",
74
74
  data: { action: "readFile", result: { error: error.message }, toolCallId },
75
75
  };
76
76
  yield {
@@ -90,7 +90,7 @@ export const fileSystemPlugin = (options = {}) => (builder) => {
90
90
  await fs.mkdir(path.dirname(fullPath), { recursive: true });
91
91
  await fs.writeFile(fullPath, content, "utf-8");
92
92
  yield {
93
- type: "action:taskResult",
93
+ type: "action:result",
94
94
  data: { action: "writeFile", result: { success: true }, toolCallId },
95
95
  };
96
96
  yield {
@@ -100,7 +100,7 @@ export const fileSystemPlugin = (options = {}) => (builder) => {
100
100
  }
101
101
  catch (error) {
102
102
  yield {
103
- type: "action:taskResult",
103
+ type: "action:result",
104
104
  data: { action: "writeFile", result: { error: error.message }, toolCallId },
105
105
  };
106
106
  yield {
@@ -122,7 +122,7 @@ export const fileSystemPlugin = (options = {}) => (builder) => {
122
122
  data: { message: `Files listed successfully`, severity: "success" }
123
123
  };
124
124
  yield {
125
- type: "action:taskResult",
125
+ type: "action:result",
126
126
  data: { action: "listFiles", result: { files }, toolCallId },
127
127
  };
128
128
  }
@@ -132,7 +132,7 @@ export const fileSystemPlugin = (options = {}) => (builder) => {
132
132
  data: { message: `Files listing failed: ${error.message}`, severity: "error" }
133
133
  };
134
134
  yield {
135
- type: "action:taskResult",
135
+ type: "action:result",
136
136
  data: { action: "listFiles", result: { error: error.message }, toolCallId },
137
137
  };
138
138
  }
@@ -146,7 +146,7 @@ export const fileSystemPlugin = (options = {}) => (builder) => {
146
146
  try {
147
147
  await fs.unlink(resolvePath(filePath, state.cwd));
148
148
  yield {
149
- type: "action:taskResult",
149
+ type: "action:result",
150
150
  data: { action: "deleteFile", result: { success: true }, toolCallId },
151
151
  };
152
152
  yield {
@@ -156,7 +156,7 @@ export const fileSystemPlugin = (options = {}) => (builder) => {
156
156
  }
157
157
  catch (error) {
158
158
  yield {
159
- type: "action:taskResult",
159
+ type: "action:result",
160
160
  data: { action: "deleteFile", result: { error: error.message }, toolCallId },
161
161
  };
162
162
  yield {
@@ -0,0 +1,177 @@
1
+ const DEFAULT_OPTIONS = {
2
+ maxRecentRawMessages: 4,
3
+ maxRelevantMessages: 6,
4
+ maxContextChars: 12000,
5
+ maxTurnSummaries: 20,
6
+ maxConstraints: 8,
7
+ };
8
+ const STOP_WORDS = new Set([
9
+ "the",
10
+ "and",
11
+ "for",
12
+ "that",
13
+ "with",
14
+ "this",
15
+ "from",
16
+ "have",
17
+ "your",
18
+ "will",
19
+ "would",
20
+ "should",
21
+ "could",
22
+ "about",
23
+ "what",
24
+ "when",
25
+ "where",
26
+ "which",
27
+ "into",
28
+ "just",
29
+ "like",
30
+ "than",
31
+ "then",
32
+ "there",
33
+ "their",
34
+ "them",
35
+ "also",
36
+ "need",
37
+ "want",
38
+ "please",
39
+ ]);
40
+ function mergeOptions(overrides) {
41
+ return {
42
+ ...DEFAULT_OPTIONS,
43
+ ...(overrides ?? {}),
44
+ };
45
+ }
46
+ function clip(text, maxChars) {
47
+ if (text.length <= maxChars)
48
+ return text;
49
+ return `${text.slice(0, maxChars - 3)}...`;
50
+ }
51
+ function toSearchText(message) {
52
+ return message.content.toLowerCase();
53
+ }
54
+ function tokenize(text) {
55
+ return text
56
+ .toLowerCase()
57
+ .replace(/[^a-z0-9\s]/g, " ")
58
+ .split(/\s+/)
59
+ .filter((token) => token.length > 2 && !STOP_WORDS.has(token));
60
+ }
61
+ function overlapScore(messageText, queryTerms) {
62
+ if (queryTerms.length === 0)
63
+ return 0;
64
+ let matched = 0;
65
+ for (const term of queryTerms) {
66
+ if (messageText.includes(term))
67
+ matched += 1;
68
+ }
69
+ return matched / queryTerms.length;
70
+ }
71
+ function recencyBoost(index, total) {
72
+ if (total <= 1)
73
+ return 0;
74
+ return index / (total - 1);
75
+ }
76
+ function estimateChars(messages) {
77
+ return messages.reduce((sum, m) => sum + (m.content?.length ?? 0), 0);
78
+ }
79
+ function buildContextBrief(state, maxChars) {
80
+ if (!state)
81
+ return "";
82
+ const lines = [];
83
+ if (state.currentGoal)
84
+ lines.push(`Current objective: ${state.currentGoal}`);
85
+ if (state.constraints && state.constraints.length > 0) {
86
+ lines.push(`Constraints: ${state.constraints.join(" | ")}`);
87
+ }
88
+ if (state.rollingSummary)
89
+ lines.push(`Recent summary: ${state.rollingSummary}`);
90
+ if (lines.length === 0)
91
+ return "";
92
+ return clip(`System context for this turn:\n${lines.map((line) => `- ${line}`).join("\n")}`, maxChars);
93
+ }
94
+ export function buildShapedContext(input) {
95
+ const options = mergeOptions(input.options);
96
+ const messages = input.messages;
97
+ if (messages.length <= options.maxRecentRawMessages) {
98
+ const brief = buildContextBrief(input.contextState, 1500);
99
+ return brief
100
+ ? [{ role: "user", content: brief }, ...messages]
101
+ : messages;
102
+ }
103
+ const recentStart = Math.max(0, messages.length - options.maxRecentRawMessages);
104
+ const recent = messages.slice(recentStart);
105
+ const older = messages.slice(0, recentStart);
106
+ const latestUser = [...messages]
107
+ .reverse()
108
+ .find((message) => message.role === "user");
109
+ const queryTerms = latestUser ? tokenize(latestUser.content) : [];
110
+ const scored = older
111
+ .map((message, index) => {
112
+ const text = toSearchText(message);
113
+ const relevance = overlapScore(text, queryTerms);
114
+ const score = relevance * 0.8 + recencyBoost(index, older.length) * 0.2;
115
+ return { index, score };
116
+ })
117
+ .filter((row) => row.score > 0)
118
+ .sort((a, b) => b.score - a.score)
119
+ .slice(0, options.maxRelevantMessages)
120
+ .sort((a, b) => a.index - b.index);
121
+ const selectedOlder = scored.map((row) => older[row.index]);
122
+ const brief = buildContextBrief(input.contextState, 1500);
123
+ let selected = [
124
+ ...(brief ? [{ role: "user", content: brief }] : []),
125
+ ...selectedOlder,
126
+ ...recent,
127
+ ];
128
+ while (estimateChars(selected) > options.maxContextChars) {
129
+ const removableIndex = selected.findIndex((message, index) => {
130
+ if (brief && index === 0)
131
+ return false;
132
+ return index < selected.length - options.maxRecentRawMessages;
133
+ });
134
+ if (removableIndex === -1)
135
+ break;
136
+ selected.splice(removableIndex, 1);
137
+ }
138
+ return selected;
139
+ }
140
+ function extractConstraints(text, existing, maxConstraints) {
141
+ const hasConstraintLanguage = /\b(do not|don't|must|never|always|only)\b/i.test(text);
142
+ if (!hasConstraintLanguage)
143
+ return existing;
144
+ const normalized = clip(text.trim().replace(/\s+/g, " "), 180);
145
+ if (!normalized)
146
+ return existing;
147
+ if (existing.includes(normalized))
148
+ return existing;
149
+ const next = [...existing, normalized];
150
+ return next.slice(-maxConstraints);
151
+ }
152
+ export function updateContextState(input, optionsOverride) {
153
+ const options = mergeOptions(optionsOverride);
154
+ const existing = input.contextState ?? {};
155
+ const constraints = extractConstraints(input.latestUserMessage, existing.constraints ?? [], options.maxConstraints);
156
+ const compactUser = clip(input.latestUserMessage.trim().replace(/\s+/g, " "), 200);
157
+ const compactAssistant = clip(input.latestAssistantMessage.trim().replace(/\s+/g, " "), 240);
158
+ const turnSummary = compactUser || compactAssistant
159
+ ? `U: ${compactUser || "-"} | A: ${compactAssistant || "-"}`
160
+ : "";
161
+ const turnSummaries = turnSummary
162
+ ? [...(existing.turnSummaries ?? []), turnSummary].slice(-options.maxTurnSummaries)
163
+ : existing.turnSummaries ?? [];
164
+ const rollingSummary = turnSummaries.slice(-6).join(" || ");
165
+ const latestUserMessage = input.latestUserMessage.trim();
166
+ const isToolEcho = latestUserMessage.startsWith("System: Action ");
167
+ const currentGoal = !isToolEcho && latestUserMessage
168
+ ? clip(latestUserMessage, 240)
169
+ : existing.currentGoal;
170
+ return {
171
+ currentGoal,
172
+ constraints,
173
+ turnSummaries,
174
+ rollingSummary,
175
+ updatedAt: new Date().toISOString(),
176
+ };
177
+ }