@stevederico/dotbot 0.16.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/CHANGELOG.md +136 -0
- package/README.md +380 -0
- package/bin/dotbot.js +461 -0
- package/core/agent.js +779 -0
- package/core/compaction.js +261 -0
- package/core/cron_handler.js +262 -0
- package/core/events.js +229 -0
- package/core/failover.js +193 -0
- package/core/gptoss_tool_parser.js +173 -0
- package/core/init.js +154 -0
- package/core/normalize.js +324 -0
- package/core/trigger_handler.js +148 -0
- package/docs/core.md +103 -0
- package/docs/protected-files.md +59 -0
- package/examples/sqlite-session-example.js +69 -0
- package/index.js +341 -0
- package/observer/index.js +164 -0
- package/package.json +42 -0
- package/storage/CronStore.js +145 -0
- package/storage/EventStore.js +71 -0
- package/storage/MemoryStore.js +175 -0
- package/storage/MongoAdapter.js +291 -0
- package/storage/MongoCronAdapter.js +347 -0
- package/storage/MongoTaskAdapter.js +242 -0
- package/storage/MongoTriggerAdapter.js +158 -0
- package/storage/SQLiteAdapter.js +382 -0
- package/storage/SQLiteCronAdapter.js +562 -0
- package/storage/SQLiteEventStore.js +300 -0
- package/storage/SQLiteMemoryAdapter.js +240 -0
- package/storage/SQLiteTaskAdapter.js +419 -0
- package/storage/SQLiteTriggerAdapter.js +262 -0
- package/storage/SessionStore.js +149 -0
- package/storage/TaskStore.js +100 -0
- package/storage/TriggerStore.js +90 -0
- package/storage/cron_constants.js +48 -0
- package/storage/index.js +21 -0
- package/tools/appgen.js +311 -0
- package/tools/browser.js +634 -0
- package/tools/code.js +101 -0
- package/tools/events.js +145 -0
- package/tools/files.js +201 -0
- package/tools/images.js +253 -0
- package/tools/index.js +97 -0
- package/tools/jobs.js +159 -0
- package/tools/memory.js +332 -0
- package/tools/messages.js +135 -0
- package/tools/notify.js +42 -0
- package/tools/tasks.js +404 -0
- package/tools/triggers.js +159 -0
- package/tools/weather.js +82 -0
- package/tools/web.js +283 -0
- package/utils/providers.js +136 -0
package/core/init.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified initialization for dotbot.
|
|
3
|
+
*
|
|
4
|
+
* Provides a single entry point that creates and wires all stores, handlers,
|
|
5
|
+
* and the agent instance. Host apps only need to provide hooks for host-specific
|
|
6
|
+
* behavior (notifications, preferences).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { SQLiteSessionStore } from '../storage/SQLiteAdapter.js';
|
|
10
|
+
import { SQLiteCronStore } from '../storage/SQLiteCronAdapter.js';
|
|
11
|
+
import { SQLiteTaskStore } from '../storage/SQLiteTaskAdapter.js';
|
|
12
|
+
import { SQLiteTriggerStore } from '../storage/SQLiteTriggerAdapter.js';
|
|
13
|
+
import { SQLiteMemoryStore } from '../storage/SQLiteMemoryAdapter.js';
|
|
14
|
+
import { coreTools } from '../tools/index.js';
|
|
15
|
+
import { createCronHandler } from './cron_handler.js';
|
|
16
|
+
import { createTriggerHandler } from './trigger_handler.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Initialize dotbot with unified configuration.
|
|
20
|
+
*
|
|
21
|
+
* @param {Object} options - Configuration options
|
|
22
|
+
* @param {string} options.dbPath - Path to SQLite database file
|
|
23
|
+
* @param {boolean} [options.storesOnly=false] - If true, only initialize stores (no agent, cron, triggers)
|
|
24
|
+
* @param {Object} [options.sessionStore] - Custom session store (default: SQLiteSessionStore)
|
|
25
|
+
* @param {Object} [options.providers] - Provider API keys: { anthropic: { apiKey }, openai: { apiKey }, xai: { apiKey } }
|
|
26
|
+
* @param {Array} [options.tools] - Tool definitions (default: coreTools)
|
|
27
|
+
* @param {number} [options.staleThresholdMs=86400000] - Skip heartbeat if user idle longer than this (default: 24h)
|
|
28
|
+
* @param {Function} [options.systemPrompt] - System prompt builder function
|
|
29
|
+
* @param {Function} [options.screenshotUrlPattern] - Screenshot URL pattern function
|
|
30
|
+
* @param {Object} [options.compaction] - Compaction settings
|
|
31
|
+
* @param {Object} [options.hooks] - Host-specific hooks
|
|
32
|
+
* @param {Function} [options.hooks.onNotification] - async (userId, { title, body, type }) => void
|
|
33
|
+
* @param {Function} [options.hooks.prefsFetcher] - async (userId) => { agentName, agentPersonality }
|
|
34
|
+
* @param {Function} [options.hooks.taskFetcher] - async (userId, taskId) => task object
|
|
35
|
+
* @param {Function} [options.hooks.tasksFinder] - async (userId, filter) => tasks array
|
|
36
|
+
* @returns {Promise<Object>} { agent, stores, fireTrigger, shutdown } or { stores, shutdown } if storesOnly
|
|
37
|
+
*/
|
|
38
|
+
export async function init({
|
|
39
|
+
dbPath,
|
|
40
|
+
storesOnly = false,
|
|
41
|
+
sessionStore: customSessionStore,
|
|
42
|
+
providers = {},
|
|
43
|
+
tools = coreTools,
|
|
44
|
+
staleThresholdMs = 24 * 60 * 60 * 1000,
|
|
45
|
+
systemPrompt,
|
|
46
|
+
screenshotUrlPattern,
|
|
47
|
+
compaction = { enabled: true },
|
|
48
|
+
hooks = {},
|
|
49
|
+
} = {}) {
|
|
50
|
+
if (!dbPath) {
|
|
51
|
+
throw new Error('init() requires a dbPath');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Initialize stores
|
|
55
|
+
const taskStore = new SQLiteTaskStore();
|
|
56
|
+
const triggerStore = new SQLiteTriggerStore();
|
|
57
|
+
const memoryStore = new SQLiteMemoryStore();
|
|
58
|
+
|
|
59
|
+
// Initialize task, trigger, and memory stores (always needed)
|
|
60
|
+
await taskStore.init({ dbPath });
|
|
61
|
+
await triggerStore.init({ dbPath });
|
|
62
|
+
await memoryStore.init({ dbPath });
|
|
63
|
+
|
|
64
|
+
// Bundle stores
|
|
65
|
+
const stores = {
|
|
66
|
+
task: taskStore,
|
|
67
|
+
trigger: triggerStore,
|
|
68
|
+
memory: memoryStore,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// For stores-only mode (e.g., dottie-desktop), skip session/cron/agent setup
|
|
72
|
+
if (storesOnly) {
|
|
73
|
+
return {
|
|
74
|
+
stores,
|
|
75
|
+
shutdown: async () => {},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Full initialization with sessions, cron, and agent
|
|
80
|
+
const sessionStore = customSessionStore || new SQLiteSessionStore();
|
|
81
|
+
const cronStore = new SQLiteCronStore();
|
|
82
|
+
|
|
83
|
+
stores.session = sessionStore;
|
|
84
|
+
stores.cron = cronStore;
|
|
85
|
+
|
|
86
|
+
// Initialize session store with hooks
|
|
87
|
+
if (!customSessionStore) {
|
|
88
|
+
await sessionStore.init(dbPath, {
|
|
89
|
+
prefsFetcher: hooks.prefsFetcher,
|
|
90
|
+
heartbeatEnsurer: async (userId) => {
|
|
91
|
+
return await cronStore.ensureHeartbeat(userId);
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Build cron handler with extracted logic
|
|
97
|
+
const handleCronTask = createCronHandler({
|
|
98
|
+
sessionStore,
|
|
99
|
+
cronStore,
|
|
100
|
+
taskStore,
|
|
101
|
+
memoryStore,
|
|
102
|
+
providers,
|
|
103
|
+
staleThresholdMs,
|
|
104
|
+
hooks,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Initialize cron store with the handler
|
|
108
|
+
await cronStore.init(dbPath, { onTaskFire: handleCronTask });
|
|
109
|
+
|
|
110
|
+
// Import createAgent dynamically to avoid circular dependency
|
|
111
|
+
const { createAgent } = await import('../index.js');
|
|
112
|
+
|
|
113
|
+
// Create the agent
|
|
114
|
+
const agent = createAgent({
|
|
115
|
+
sessionStore,
|
|
116
|
+
cronStore,
|
|
117
|
+
taskStore,
|
|
118
|
+
triggerStore,
|
|
119
|
+
memoryStore,
|
|
120
|
+
providers,
|
|
121
|
+
tools,
|
|
122
|
+
systemPrompt,
|
|
123
|
+
screenshotUrlPattern,
|
|
124
|
+
compaction,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Wire agent to cron handler (resolves chicken-and-egg dependency)
|
|
128
|
+
handleCronTask.setAgent(agent);
|
|
129
|
+
|
|
130
|
+
// Create trigger handler
|
|
131
|
+
const fireTrigger = createTriggerHandler({
|
|
132
|
+
agent,
|
|
133
|
+
sessionStore,
|
|
134
|
+
triggerStore,
|
|
135
|
+
memoryStore,
|
|
136
|
+
providers,
|
|
137
|
+
hooks,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Gracefully shut down all stores and handlers.
|
|
142
|
+
*/
|
|
143
|
+
async function shutdown() {
|
|
144
|
+
cronStore.stop();
|
|
145
|
+
// Add any other cleanup needed
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
agent,
|
|
150
|
+
stores,
|
|
151
|
+
fireTrigger,
|
|
152
|
+
shutdown,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// core/normalize.js
|
|
2
|
+
// Standardize message formats across providers (Anthropic, OpenAI, xAI, Ollama).
|
|
3
|
+
// Provides bidirectional conversion between provider-specific formats and a
|
|
4
|
+
// unified schema that apps can consume without coupling to provider quirks.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Standard message schema (provider-agnostic):
|
|
8
|
+
*
|
|
9
|
+
* User message:
|
|
10
|
+
* {
|
|
11
|
+
* role: "user",
|
|
12
|
+
* content: string,
|
|
13
|
+
* _ts?: number
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* Assistant message:
|
|
17
|
+
* {
|
|
18
|
+
* role: "assistant",
|
|
19
|
+
* content: string,
|
|
20
|
+
* toolCalls?: [{ id, name, input, result?, status: "pending"|"done"|"error" }],
|
|
21
|
+
* thinking?: string,
|
|
22
|
+
* images?: [{ url, prompt }],
|
|
23
|
+
* _ts?: number
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* System message:
|
|
27
|
+
* {
|
|
28
|
+
* role: "system",
|
|
29
|
+
* content: string
|
|
30
|
+
* }
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Convert provider-specific messages to standard format.
|
|
35
|
+
* Collapses assistant + tool_result pairs into single messages with toolCalls array.
|
|
36
|
+
*
|
|
37
|
+
* @param {Array} messages - Raw messages in Anthropic or OpenAI format
|
|
38
|
+
* @returns {Array} Normalized messages in standard format
|
|
39
|
+
*/
|
|
40
|
+
export function toStandardFormat(messages) {
|
|
41
|
+
// Build tool result lookup (tool_use_id/tool_call_id → result content)
|
|
42
|
+
const toolResults = new Map();
|
|
43
|
+
|
|
44
|
+
for (const msg of messages) {
|
|
45
|
+
// Anthropic tool results (role: user, content: [{type: "tool_result", ...}])
|
|
46
|
+
if (msg.role === 'user' && Array.isArray(msg.content)) {
|
|
47
|
+
for (const block of msg.content) {
|
|
48
|
+
if (block.type === 'tool_result') {
|
|
49
|
+
toolResults.set(block.tool_use_id, block.content);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// OpenAI tool results (role: tool)
|
|
54
|
+
if (msg.role === 'tool' && msg.tool_call_id) {
|
|
55
|
+
toolResults.set(msg.tool_call_id, msg.content);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const normalized = [];
|
|
60
|
+
|
|
61
|
+
for (const msg of messages) {
|
|
62
|
+
// Skip tool-result-only user messages (Anthropic)
|
|
63
|
+
if (msg.role === 'user' && Array.isArray(msg.content) &&
|
|
64
|
+
msg.content.length > 0 && msg.content.every(b => b.type === 'tool_result')) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
// Skip OpenAI tool messages
|
|
68
|
+
if (msg.role === 'tool') continue;
|
|
69
|
+
|
|
70
|
+
// System messages
|
|
71
|
+
if (msg.role === 'system') {
|
|
72
|
+
normalized.push({ role: 'system', content: msg.content });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// User messages
|
|
77
|
+
if (msg.role === 'user') {
|
|
78
|
+
normalized.push({
|
|
79
|
+
role: 'user',
|
|
80
|
+
content: typeof msg.content === 'string' ? msg.content : '',
|
|
81
|
+
...(msg._ts && { _ts: msg._ts })
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Assistant messages
|
|
87
|
+
if (msg.role === 'assistant') {
|
|
88
|
+
const standard = {
|
|
89
|
+
role: 'assistant',
|
|
90
|
+
content: '',
|
|
91
|
+
...(msg._ts && { _ts: msg._ts })
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// OpenAI format (string content + tool_calls array)
|
|
95
|
+
if (!Array.isArray(msg.content)) {
|
|
96
|
+
standard.content = typeof msg.content === 'string' ? msg.content : '';
|
|
97
|
+
|
|
98
|
+
if (msg.tool_calls) {
|
|
99
|
+
standard.toolCalls = msg.tool_calls.map(tc => {
|
|
100
|
+
const result = toolResults.get(tc.id);
|
|
101
|
+
let input = {};
|
|
102
|
+
try {
|
|
103
|
+
input = JSON.parse(tc.function?.arguments || '{}');
|
|
104
|
+
} catch {}
|
|
105
|
+
|
|
106
|
+
const toolCall = {
|
|
107
|
+
id: tc.id,
|
|
108
|
+
name: tc.function?.name,
|
|
109
|
+
input,
|
|
110
|
+
status: 'done'
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (result !== undefined) {
|
|
114
|
+
toolCall.result = result;
|
|
115
|
+
// Check if result is an image
|
|
116
|
+
try {
|
|
117
|
+
const parsed = JSON.parse(result);
|
|
118
|
+
if (parsed.type === 'image' && parsed.url) {
|
|
119
|
+
if (!standard.images) standard.images = [];
|
|
120
|
+
standard.images.push({ url: parsed.url, prompt: parsed.prompt });
|
|
121
|
+
}
|
|
122
|
+
} catch {}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return toolCall;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Anthropic format (content block array)
|
|
130
|
+
else {
|
|
131
|
+
const toolCalls = [];
|
|
132
|
+
|
|
133
|
+
for (const block of msg.content) {
|
|
134
|
+
if (block.type === 'text') {
|
|
135
|
+
standard.content += block.text;
|
|
136
|
+
}
|
|
137
|
+
else if (block.type === 'tool_use') {
|
|
138
|
+
const result = toolResults.get(block.id);
|
|
139
|
+
const toolCall = {
|
|
140
|
+
id: block.id,
|
|
141
|
+
name: block.name,
|
|
142
|
+
input: block.input || {},
|
|
143
|
+
status: 'done'
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if (result !== undefined) {
|
|
147
|
+
toolCall.result = result;
|
|
148
|
+
// Check if result is an image
|
|
149
|
+
try {
|
|
150
|
+
const parsed = JSON.parse(result);
|
|
151
|
+
if (parsed.type === 'image' && parsed.url) {
|
|
152
|
+
if (!standard.images) standard.images = [];
|
|
153
|
+
standard.images.push({ url: parsed.url, prompt: parsed.prompt });
|
|
154
|
+
}
|
|
155
|
+
} catch {}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
toolCalls.push(toolCall);
|
|
159
|
+
}
|
|
160
|
+
else if (block.type === 'thinking') {
|
|
161
|
+
standard.thinking = (standard.thinking || '') + (block.thinking || '');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (toolCalls.length > 0) {
|
|
166
|
+
standard.toolCalls = toolCalls;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Clean up empty arrays
|
|
171
|
+
if (standard.toolCalls?.length === 0) delete standard.toolCalls;
|
|
172
|
+
if (standard.images?.length === 0) delete standard.images;
|
|
173
|
+
|
|
174
|
+
normalized.push(standard);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Merge consecutive assistant messages (collapse tool-call phases)
|
|
179
|
+
const merged = [];
|
|
180
|
+
for (const msg of normalized) {
|
|
181
|
+
const prev = merged[merged.length - 1];
|
|
182
|
+
if (prev && prev.role === 'assistant' && msg.role === 'assistant') {
|
|
183
|
+
// Merge toolCalls
|
|
184
|
+
if (prev.toolCalls?.length) {
|
|
185
|
+
msg.toolCalls = [...prev.toolCalls, ...(msg.toolCalls || [])];
|
|
186
|
+
}
|
|
187
|
+
// Merge thinking
|
|
188
|
+
if (prev.thinking) {
|
|
189
|
+
msg.thinking = (prev.thinking || '') + (msg.thinking || '');
|
|
190
|
+
}
|
|
191
|
+
// Merge images
|
|
192
|
+
if (prev.images?.length) {
|
|
193
|
+
msg.images = [...(prev.images || []), ...(msg.images || [])];
|
|
194
|
+
}
|
|
195
|
+
// Keep first content if current is empty
|
|
196
|
+
if (!msg.content && prev.content) {
|
|
197
|
+
msg.content = prev.content;
|
|
198
|
+
}
|
|
199
|
+
// Keep earlier timestamp
|
|
200
|
+
if (prev._ts && !msg._ts) {
|
|
201
|
+
msg._ts = prev._ts;
|
|
202
|
+
}
|
|
203
|
+
merged[merged.length - 1] = msg;
|
|
204
|
+
} else {
|
|
205
|
+
merged.push(msg);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return merged;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Convert standard format messages to provider-specific format.
|
|
214
|
+
* Splits assistant messages with toolCalls into separate assistant + tool_result messages.
|
|
215
|
+
*
|
|
216
|
+
* @param {Array} messages - Normalized messages in standard format
|
|
217
|
+
* @param {"anthropic"|"openai"} targetFormat - Target provider format
|
|
218
|
+
* @returns {Array} Messages in provider-specific format
|
|
219
|
+
*/
|
|
220
|
+
export function toProviderFormat(messages, targetFormat) {
|
|
221
|
+
const result = [];
|
|
222
|
+
|
|
223
|
+
for (const msg of messages) {
|
|
224
|
+
// System and user messages stay the same
|
|
225
|
+
if (msg.role === 'system' || msg.role === 'user') {
|
|
226
|
+
result.push({ ...msg });
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Assistant messages with tool calls
|
|
231
|
+
if (msg.role === 'assistant') {
|
|
232
|
+
if (targetFormat === 'anthropic') {
|
|
233
|
+
// Anthropic: content block array
|
|
234
|
+
const contentBlocks = [];
|
|
235
|
+
|
|
236
|
+
if (msg.content) {
|
|
237
|
+
contentBlocks.push({ type: 'text', text: msg.content });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (msg.thinking) {
|
|
241
|
+
contentBlocks.push({ type: 'thinking', thinking: msg.thinking });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const toolUses = [];
|
|
245
|
+
if (msg.toolCalls) {
|
|
246
|
+
for (const tc of msg.toolCalls) {
|
|
247
|
+
contentBlocks.push({
|
|
248
|
+
type: 'tool_use',
|
|
249
|
+
id: tc.id,
|
|
250
|
+
name: tc.name,
|
|
251
|
+
input: tc.input
|
|
252
|
+
});
|
|
253
|
+
toolUses.push({ id: tc.id, result: tc.result });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
result.push({
|
|
258
|
+
role: 'assistant',
|
|
259
|
+
content: contentBlocks,
|
|
260
|
+
...(msg._ts && { _ts: msg._ts })
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Add tool results as separate user message
|
|
264
|
+
if (toolUses.length > 0) {
|
|
265
|
+
result.push({
|
|
266
|
+
role: 'user',
|
|
267
|
+
content: toolUses.map(tu => ({
|
|
268
|
+
type: 'tool_result',
|
|
269
|
+
tool_use_id: tu.id,
|
|
270
|
+
content: tu.result || ''
|
|
271
|
+
}))
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
// OpenAI: string content + tool_calls array
|
|
277
|
+
const assistantMsg = {
|
|
278
|
+
role: 'assistant',
|
|
279
|
+
content: msg.content || '',
|
|
280
|
+
...(msg._ts && { _ts: msg._ts })
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
if (msg.toolCalls) {
|
|
284
|
+
assistantMsg.tool_calls = msg.toolCalls.map(tc => ({
|
|
285
|
+
id: tc.id,
|
|
286
|
+
type: 'function',
|
|
287
|
+
function: {
|
|
288
|
+
name: tc.name,
|
|
289
|
+
arguments: typeof tc.input === 'string' ? tc.input : JSON.stringify(tc.input)
|
|
290
|
+
}
|
|
291
|
+
}));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
result.push(assistantMsg);
|
|
295
|
+
|
|
296
|
+
// Add tool results as separate tool messages
|
|
297
|
+
if (msg.toolCalls) {
|
|
298
|
+
for (const tc of msg.toolCalls) {
|
|
299
|
+
if (tc.result !== undefined) {
|
|
300
|
+
result.push({
|
|
301
|
+
role: 'tool',
|
|
302
|
+
tool_call_id: tc.id,
|
|
303
|
+
content: tc.result
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return result;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Normalize messages for frontend display.
|
|
317
|
+
* Alias for toStandardFormat() for backward compatibility.
|
|
318
|
+
*
|
|
319
|
+
* @param {Array} messages - Raw provider-specific messages
|
|
320
|
+
* @returns {Array} Normalized messages
|
|
321
|
+
*/
|
|
322
|
+
export function normalizeMessages(messages) {
|
|
323
|
+
return toStandardFormat(messages);
|
|
324
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trigger handler for dotbot.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from dottie-os server.js to provide a reusable trigger executor
|
|
5
|
+
* that handles event matching, firing, and notification hooks.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { compactMessages } from './compaction.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a trigger handler function.
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} options
|
|
14
|
+
* @param {Object} options.agent - Agent instance with chat() method
|
|
15
|
+
* @param {Object} options.sessionStore - Session store instance
|
|
16
|
+
* @param {Object} options.triggerStore - Trigger store instance
|
|
17
|
+
* @param {Object} options.memoryStore - Memory store instance (optional)
|
|
18
|
+
* @param {Object} options.providers - Provider API keys for compaction
|
|
19
|
+
* @param {Object} [options.hooks] - Host-specific hooks
|
|
20
|
+
* @param {Function} [options.hooks.onNotification] - async (userId, { title, body, type }) => void
|
|
21
|
+
* @returns {Function} Async function: (eventType, userId, eventData?) => Promise<void>
|
|
22
|
+
*/
|
|
23
|
+
export function createTriggerHandler({
|
|
24
|
+
agent,
|
|
25
|
+
sessionStore,
|
|
26
|
+
triggerStore,
|
|
27
|
+
memoryStore,
|
|
28
|
+
providers = {},
|
|
29
|
+
hooks = {},
|
|
30
|
+
}) {
|
|
31
|
+
/**
|
|
32
|
+
* Fire triggers for an event.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} eventType - Event type (e.g., "user_login", "app_opened")
|
|
35
|
+
* @param {string} userId - User ID
|
|
36
|
+
* @param {Object} [eventData] - Optional event payload
|
|
37
|
+
*/
|
|
38
|
+
async function fireTrigger(eventType, userId, eventData = {}) {
|
|
39
|
+
if (!userId) {
|
|
40
|
+
console.warn(`[triggers] fireTrigger called without userId for event ${eventType}`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// Find matching triggers for this user and event
|
|
46
|
+
const triggers = await triggerStore.findMatchingTriggers(userId, eventType);
|
|
47
|
+
if (triggers.length === 0) {
|
|
48
|
+
console.log(`[triggers] no matching triggers for ${eventType} (user=${userId})`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log(`[triggers] found ${triggers.length} trigger(s) for ${eventType}`);
|
|
53
|
+
|
|
54
|
+
// Get or create the user's session
|
|
55
|
+
const session = await sessionStore.getOrCreateDefaultSession(userId);
|
|
56
|
+
if (!session) {
|
|
57
|
+
console.warn(`[triggers] could not get/create session for user ${userId}`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Process each matching trigger
|
|
62
|
+
for (const trigger of triggers) {
|
|
63
|
+
await executeTrigger(trigger, eventType, session, eventData);
|
|
64
|
+
}
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error(`[triggers] error handling ${eventType}:`, err.message);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Execute a single trigger.
|
|
72
|
+
*
|
|
73
|
+
* @param {Object} trigger - Trigger object
|
|
74
|
+
* @param {string} eventType - Event type
|
|
75
|
+
* @param {Object} session - User session
|
|
76
|
+
* @param {Object} eventData - Event payload
|
|
77
|
+
*/
|
|
78
|
+
async function executeTrigger(trigger, eventType, session, eventData) {
|
|
79
|
+
// Mark trigger as fired (handles cooldown tracking)
|
|
80
|
+
await triggerStore.markTriggerFired(trigger.id);
|
|
81
|
+
|
|
82
|
+
// Build the message content
|
|
83
|
+
let messageContent = `[Event: ${eventType}] ${trigger.prompt}`;
|
|
84
|
+
if (Object.keys(eventData).length > 0) {
|
|
85
|
+
messageContent += `\n\nEvent data: ${JSON.stringify(eventData)}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Add message to session
|
|
89
|
+
await sessionStore.addMessage(session.id, {
|
|
90
|
+
role: 'user',
|
|
91
|
+
content: messageContent,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Re-fetch session to pick up the added message
|
|
95
|
+
const updatedSession = await sessionStore.getSessionInternal(session.id);
|
|
96
|
+
const providerId = updatedSession.provider || 'ollama';
|
|
97
|
+
|
|
98
|
+
// Compact old messages before running agent loop
|
|
99
|
+
const compacted = await compactMessages(updatedSession.messages, {
|
|
100
|
+
providerId,
|
|
101
|
+
providers,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (compacted.compacted) {
|
|
105
|
+
updatedSession.messages = compacted.messages;
|
|
106
|
+
updatedSession.messages.push({
|
|
107
|
+
role: 'user',
|
|
108
|
+
content: '[Compaction] Compressed conversation history',
|
|
109
|
+
_ts: Date.now(),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Run the agent chat loop
|
|
114
|
+
let finalText = '';
|
|
115
|
+
for await (const event of agent.chat({
|
|
116
|
+
sessionId: updatedSession.id,
|
|
117
|
+
message: '', // Message already added to session
|
|
118
|
+
provider: providerId,
|
|
119
|
+
model: updatedSession.model,
|
|
120
|
+
context: {
|
|
121
|
+
userID: updatedSession.owner,
|
|
122
|
+
sessionId: updatedSession.id,
|
|
123
|
+
memoryStore,
|
|
124
|
+
},
|
|
125
|
+
})) {
|
|
126
|
+
if (event.type === 'text_delta' && event.text) {
|
|
127
|
+
finalText += event.text;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Create notification if the agent produced meaningful output
|
|
132
|
+
const trimmed = finalText.trim();
|
|
133
|
+
if (trimmed && trimmed.length > 10 && updatedSession.owner && hooks.onNotification) {
|
|
134
|
+
try {
|
|
135
|
+
await hooks.onNotification(updatedSession.owner, {
|
|
136
|
+
title: 'Dottie',
|
|
137
|
+
body: trimmed.slice(0, 500),
|
|
138
|
+
type: 'trigger',
|
|
139
|
+
});
|
|
140
|
+
console.log(`[triggers] notification created for ${updatedSession.owner} (${trimmed.length} chars)`);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
console.error(`[triggers] failed to create notification for ${eventType}:`, err.message);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return fireTrigger;
|
|
148
|
+
}
|