claudity 1.0.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/.env.example +2 -0
- package/README.md +32 -0
- package/install.sh +343 -0
- package/package.json +28 -0
- package/public/css/style.css +1672 -0
- package/public/favicon.svg +9 -0
- package/public/font/berkeley.woff2 +0 -0
- package/public/index.html +262 -0
- package/public/js/app.js +1240 -0
- package/setup.sh +134 -0
- package/src/db.js +162 -0
- package/src/index.js +61 -0
- package/src/routes/api.js +194 -0
- package/src/routes/connections.js +41 -0
- package/src/routes/relay.js +37 -0
- package/src/services/auth.js +88 -0
- package/src/services/chat.js +365 -0
- package/src/services/claude.js +353 -0
- package/src/services/connections.js +97 -0
- package/src/services/discord.js +126 -0
- package/src/services/heartbeat.js +106 -0
- package/src/services/imessage.js +200 -0
- package/src/services/memory.js +183 -0
- package/src/services/scheduler.js +32 -0
- package/src/services/signal.js +237 -0
- package/src/services/slack.js +113 -0
- package/src/services/telegram.js +94 -0
- package/src/services/tools.js +467 -0
- package/src/services/whatsapp.js +111 -0
- package/src/services/workspace.js +162 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
const { stmts } = require('../db');
|
|
3
|
+
|
|
4
|
+
let cachedCredentials = null;
|
|
5
|
+
|
|
6
|
+
function readKeychain() {
|
|
7
|
+
try {
|
|
8
|
+
const raw = execSync(
|
|
9
|
+
'security find-generic-password -s "Claude Code-credentials" -w',
|
|
10
|
+
{ encoding: 'utf8', timeout: 5000 }
|
|
11
|
+
).trim();
|
|
12
|
+
const parsed = JSON.parse(raw);
|
|
13
|
+
const creds = parsed.claudeAiOauth || parsed;
|
|
14
|
+
cachedCredentials = creds;
|
|
15
|
+
return creds;
|
|
16
|
+
} catch {
|
|
17
|
+
cachedCredentials = null;
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getApiKey() {
|
|
23
|
+
const row = stmts.getConfig.get('api_key');
|
|
24
|
+
return row ? row.value : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function setApiKey(key) {
|
|
28
|
+
stmts.setConfig.run('api_key', key);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function removeApiKey() {
|
|
32
|
+
stmts.deleteConfig.run('api_key');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getAccessToken() {
|
|
36
|
+
const apiKey = getApiKey();
|
|
37
|
+
if (apiKey) return apiKey;
|
|
38
|
+
const creds = readKeychain();
|
|
39
|
+
if (!creds) return null;
|
|
40
|
+
return creds.accessToken || null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getAuthStatus() {
|
|
44
|
+
const apiKey = getApiKey();
|
|
45
|
+
if (apiKey) {
|
|
46
|
+
return { authenticated: true, mode: 'api_key' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const creds = readKeychain();
|
|
50
|
+
if (!creds) {
|
|
51
|
+
return { authenticated: false, reason: 'no credentials found' };
|
|
52
|
+
}
|
|
53
|
+
if (!creds.accessToken) {
|
|
54
|
+
return { authenticated: false, reason: 'no access token in credentials' };
|
|
55
|
+
}
|
|
56
|
+
if (creds.expiresAt) {
|
|
57
|
+
const expiry = new Date(creds.expiresAt).getTime();
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
if (now >= expiry - 60000) {
|
|
60
|
+
return { authenticated: false, reason: 'token expired' };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { authenticated: true, mode: 'oauth' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getHeaders() {
|
|
67
|
+
const token = getAccessToken();
|
|
68
|
+
if (!token) return null;
|
|
69
|
+
return {
|
|
70
|
+
'x-api-key': token,
|
|
71
|
+
'content-type': 'application/json',
|
|
72
|
+
'anthropic-version': '2023-06-01'
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function writeSetupToken(token) {
|
|
77
|
+
const payload = JSON.stringify({ claudeAiOauth: { accessToken: token } });
|
|
78
|
+
try {
|
|
79
|
+
execSync('security delete-generic-password -s "Claude Code-credentials"', { stdio: 'ignore' });
|
|
80
|
+
} catch {}
|
|
81
|
+
execSync(
|
|
82
|
+
`security add-generic-password -s "Claude Code-credentials" -a "Claude Code" -w '${payload.replace(/'/g, "'\\''")}'`,
|
|
83
|
+
{ timeout: 5000 }
|
|
84
|
+
);
|
|
85
|
+
cachedCredentials = null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { getAccessToken, getAuthStatus, getHeaders, getApiKey, setApiKey, removeApiKey, readKeychain, writeSetupToken };
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
const { v4: uuid } = require('uuid');
|
|
2
|
+
const { stmts } = require('../db');
|
|
3
|
+
const claude = require('./claude');
|
|
4
|
+
const tools = require('./tools');
|
|
5
|
+
const memory = require('./memory');
|
|
6
|
+
const workspace = require('./workspace');
|
|
7
|
+
|
|
8
|
+
const agentStreams = new Map();
|
|
9
|
+
const messageQueues = new Map();
|
|
10
|
+
const processingAgents = new Set();
|
|
11
|
+
|
|
12
|
+
function addStream(agentId, res) {
|
|
13
|
+
if (!agentStreams.has(agentId)) agentStreams.set(agentId, []);
|
|
14
|
+
agentStreams.get(agentId).push(res);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function removeStream(agentId, res) {
|
|
18
|
+
const streams = agentStreams.get(agentId);
|
|
19
|
+
if (!streams) return;
|
|
20
|
+
const idx = streams.indexOf(res);
|
|
21
|
+
if (idx !== -1) streams.splice(idx, 1);
|
|
22
|
+
if (!streams.length) agentStreams.delete(agentId);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function emit(agentId, event, data) {
|
|
26
|
+
const streams = agentStreams.get(agentId);
|
|
27
|
+
if (!streams) return;
|
|
28
|
+
const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
29
|
+
for (const res of streams) {
|
|
30
|
+
res.write(payload);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildSystemPrompt(agent) {
|
|
35
|
+
if (agent.bootstrapped === 0) {
|
|
36
|
+
const bootstrap = workspace.readFile(agent.name, 'BOOTSTRAP.md');
|
|
37
|
+
if (bootstrap) {
|
|
38
|
+
const toolDefs = tools.getAllToolDefinitions();
|
|
39
|
+
const toolList = toolDefs
|
|
40
|
+
.filter(t => ['read_workspace', 'write_workspace', 'complete_bootstrap'].includes(t.name))
|
|
41
|
+
.map(t => `- ${t.name}: ${t.description}`).join('\n');
|
|
42
|
+
|
|
43
|
+
return `you are a new agent called ${agent.name}. you have not been set up yet.
|
|
44
|
+
|
|
45
|
+
you know nothing about the user. do not infer, guess, or use any name, username, file path, hostname, or environment variable to identify them. if you see a username in a path or system info, ignore it completely. you must ask the user who they are.
|
|
46
|
+
|
|
47
|
+
${bootstrap}
|
|
48
|
+
|
|
49
|
+
available tools:
|
|
50
|
+
${toolList}
|
|
51
|
+
|
|
52
|
+
your workspace is at data/agents/${workspace.sanitizeName(agent.name)}/. use write_workspace to create your files. do NOT use bash, read, glob, grep, or any other built-in tools during bootstrap.`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const soul = workspace.readFile(agent.name, 'SOUL.md');
|
|
57
|
+
const identity = workspace.readFile(agent.name, 'IDENTITY.md');
|
|
58
|
+
const user = workspace.readFile(agent.name, 'USER.md');
|
|
59
|
+
const memoryMd = workspace.readFile(agent.name, 'MEMORY.md');
|
|
60
|
+
const dailyLogs = workspace.getDailyLogs(agent.name);
|
|
61
|
+
|
|
62
|
+
const dbMemories = stmts.listMemories.all(agent.id);
|
|
63
|
+
const dbMemoryBlock = dbMemories.length
|
|
64
|
+
? dbMemories.map(m => `- ${m.summary}`).join('\n')
|
|
65
|
+
: '';
|
|
66
|
+
|
|
67
|
+
const toolDefs = tools.getAllToolDefinitions();
|
|
68
|
+
const toolList = toolDefs.map(t => `- ${t.name}: ${t.description}`).join('\n');
|
|
69
|
+
|
|
70
|
+
let prompt = '';
|
|
71
|
+
|
|
72
|
+
if (soul) {
|
|
73
|
+
prompt += soul + '\n\n';
|
|
74
|
+
} else {
|
|
75
|
+
prompt += `you are ${agent.name}, an ai agent.\n\n`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (identity) prompt += identity + '\n\n';
|
|
79
|
+
|
|
80
|
+
if (user) prompt += `user context:\n${user}\n\n`;
|
|
81
|
+
|
|
82
|
+
const memoryContent = memoryMd || dbMemoryBlock;
|
|
83
|
+
if (memoryContent) prompt += `your memories:\n${memoryContent}\n\n`;
|
|
84
|
+
|
|
85
|
+
if (dailyLogs) prompt += `recent context:\n${dailyLogs}\n\n`;
|
|
86
|
+
|
|
87
|
+
prompt += `adapt your tone and style naturally to match whoever you are talking to. be personable. you are not a task executor — you are a conversational agent who can also get things done when asked.
|
|
88
|
+
|
|
89
|
+
you have full machine access — bash, file read/write/edit, glob, grep — all available as built-in tools in your environment. use them freely to accomplish tasks: run commands, read/write files, explore the filesystem, execute scripts, etc.
|
|
90
|
+
|
|
91
|
+
available claudity tools:
|
|
92
|
+
${toolList}
|
|
93
|
+
|
|
94
|
+
use spawn_subagent to offload complex or time-consuming work (writing code, running multi-step commands, analysis) to an ephemeral subprocess. the subagent has full machine access but no claudity tools or memory.
|
|
95
|
+
|
|
96
|
+
use delegate to collaborate with other agents — send a message to another agent by name and get their response. useful when a task falls in another agent's domain.
|
|
97
|
+
|
|
98
|
+
your memories are automatically extracted from conversations and written to daily logs. use the remember tool for critical standing instructions or preferences you must never lose.
|
|
99
|
+
|
|
100
|
+
your workspace is at data/agents/${workspace.sanitizeName(agent.name)}/. you can read and write your own files using read_workspace and write_workspace. your soul, identity, memory, and heartbeat files are yours to evolve.
|
|
101
|
+
|
|
102
|
+
when using tools, just use them naturally as part of the conversation — no need to announce plans or ask permission. when interacting with external platforms, read their documentation first to understand the api.
|
|
103
|
+
|
|
104
|
+
if the user asks you to do something repeatedly or on a schedule, use the schedule_task tool to set it up. you will receive scheduled reminders as messages and should act on them autonomously.
|
|
105
|
+
|
|
106
|
+
when you receive a [scheduled reminder], just do the thing — no need to announce that it was a reminder. act naturally.
|
|
107
|
+
|
|
108
|
+
CRITICAL: users cannot see tool results. they only see your final text response. when you use tools, you MUST include every key detail from the results — urls, links, claim links, confirmation codes, registration links, usernames, error messages, anything actionable. if you don't include it in your response, the user will never see it. never summarize away actionable information.
|
|
109
|
+
|
|
110
|
+
you know nothing about the user until they tell you. do not infer, guess, or use any username, file path, hostname, or environment variable to identify them. if you see a username in a path or system info, ignore it completely. never address the user by name until they introduce themselves.`;
|
|
111
|
+
|
|
112
|
+
return prompt;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function getHistory(agentId, limit = 30) {
|
|
116
|
+
const rows = stmts.recentMessages.all(agentId, limit).reverse();
|
|
117
|
+
return rows.map(r => {
|
|
118
|
+
let content = r.content;
|
|
119
|
+
if (r.role === 'assistant' && r.tool_calls) {
|
|
120
|
+
try {
|
|
121
|
+
const calls = JSON.parse(r.tool_calls);
|
|
122
|
+
const toolSummary = calls.map(tc => {
|
|
123
|
+
const outputStr = typeof tc.output === 'string' ? tc.output : JSON.stringify(tc.output);
|
|
124
|
+
const truncated = outputStr.length > 500 ? outputStr.slice(0, 500) + '...' : outputStr;
|
|
125
|
+
return `[used ${tc.name}: ${truncated}]`;
|
|
126
|
+
}).join('\n');
|
|
127
|
+
content = content + '\n\n' + toolSummary;
|
|
128
|
+
} catch {}
|
|
129
|
+
}
|
|
130
|
+
return { role: r.role, content };
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function handleMessage(agentId, userContent, options = {}) {
|
|
135
|
+
const agent = stmts.getAgent.get(agentId);
|
|
136
|
+
if (!agent) throw new Error('agent not found');
|
|
137
|
+
|
|
138
|
+
const isHeartbeat = !!options.heartbeat;
|
|
139
|
+
const isScheduled = typeof userContent === 'string' && userContent.startsWith('[scheduled reminder]');
|
|
140
|
+
|
|
141
|
+
if (!isHeartbeat) {
|
|
142
|
+
const userMsgId = uuid();
|
|
143
|
+
stmts.createMessage.run(userMsgId, agentId, 'user', userContent, null);
|
|
144
|
+
emit(agentId, 'user_message', { id: userMsgId, content: userContent });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!isHeartbeat) {
|
|
148
|
+
processingAgents.add(agentId);
|
|
149
|
+
emit(agentId, 'typing', { active: true });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let responseComplete = false;
|
|
153
|
+
|
|
154
|
+
const systemPrompt = buildSystemPrompt(agent);
|
|
155
|
+
const toolDefs = tools.getAllToolDefinitions();
|
|
156
|
+
const isBootstrap = agent.bootstrapped === 0;
|
|
157
|
+
const sessionAgentId = (!isBootstrap && !isHeartbeat) ? agentId : null;
|
|
158
|
+
const model = agent.model || 'opus';
|
|
159
|
+
const thinking = (isBootstrap || isHeartbeat) ? 'low' : (agent.thinking || 'high');
|
|
160
|
+
|
|
161
|
+
let messages = isHeartbeat
|
|
162
|
+
? [{ role: 'user', content: userContent }]
|
|
163
|
+
: [...getHistory(agentId)];
|
|
164
|
+
|
|
165
|
+
let allToolCalls = [];
|
|
166
|
+
let intermediateTexts = [];
|
|
167
|
+
|
|
168
|
+
const wantsAck = !isHeartbeat && !isScheduled && agent.bootstrapped !== 0;
|
|
169
|
+
const ackPromise = wantsAck
|
|
170
|
+
? claude.generateQuickAck(userContent, agent.name).catch(() => null)
|
|
171
|
+
: Promise.resolve(null);
|
|
172
|
+
|
|
173
|
+
const mainPromise = claude.sendMessage({
|
|
174
|
+
system: systemPrompt,
|
|
175
|
+
messages,
|
|
176
|
+
tools: toolDefs,
|
|
177
|
+
maxTokens: 4096,
|
|
178
|
+
agentId: sessionAgentId,
|
|
179
|
+
model,
|
|
180
|
+
thinking,
|
|
181
|
+
noBuiltinTools: isBootstrap
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
let response;
|
|
186
|
+
|
|
187
|
+
if (wantsAck) {
|
|
188
|
+
const raceResult = await Promise.race([
|
|
189
|
+
mainPromise.then(r => ({ type: 'main', result: r })),
|
|
190
|
+
new Promise(resolve => setTimeout(() => resolve({ type: 'timeout' }), 8000))
|
|
191
|
+
]);
|
|
192
|
+
|
|
193
|
+
if (raceResult.type === 'main') {
|
|
194
|
+
response = raceResult.result;
|
|
195
|
+
} else {
|
|
196
|
+
const quickAck = await ackPromise;
|
|
197
|
+
if (quickAck) {
|
|
198
|
+
const ackMsgId = uuid();
|
|
199
|
+
stmts.createMessage.run(ackMsgId, agentId, 'assistant', quickAck, null);
|
|
200
|
+
emit(agentId, 'typing', { active: false });
|
|
201
|
+
emit(agentId, 'ack_message', { content: quickAck });
|
|
202
|
+
if (options.onAck) options.onAck(quickAck);
|
|
203
|
+
emit(agentId, 'typing', { active: true });
|
|
204
|
+
}
|
|
205
|
+
response = await mainPromise;
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
response = await mainPromise;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
while (claude.hasToolUse(response)) {
|
|
212
|
+
const calls = claude.extractToolUse(response);
|
|
213
|
+
let thinkingText = claude.extractText(response).trim();
|
|
214
|
+
thinkingText = thinkingText.replace(/```json\s*\n?\s*\{[\s\S]*?\}\s*\n?\s*```/g, '').replace(/\n{3,}/g, '\n\n').trim();
|
|
215
|
+
|
|
216
|
+
if (thinkingText) {
|
|
217
|
+
intermediateTexts.push(thinkingText);
|
|
218
|
+
emit(agentId, 'intermediate', { content: thinkingText });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
messages.push({ role: 'assistant', content: response.content });
|
|
222
|
+
|
|
223
|
+
const toolResults = [];
|
|
224
|
+
|
|
225
|
+
for (const call of calls) {
|
|
226
|
+
emit(agentId, 'tool_call', { name: call.name, input: call.input });
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const result = await tools.executeTool(call.name, call.input, { agentId });
|
|
230
|
+
emit(agentId, 'tool_result', { name: call.name, output: result });
|
|
231
|
+
allToolCalls.push({ name: call.name, input: call.input, output: result });
|
|
232
|
+
|
|
233
|
+
toolResults.push({
|
|
234
|
+
type: 'tool_result',
|
|
235
|
+
tool_use_id: call.id,
|
|
236
|
+
content: JSON.stringify(result)
|
|
237
|
+
});
|
|
238
|
+
} catch (err) {
|
|
239
|
+
emit(agentId, 'tool_result', { name: call.name, output: { error: err.message } });
|
|
240
|
+
allToolCalls.push({ name: call.name, input: call.input, output: { error: err.message } });
|
|
241
|
+
|
|
242
|
+
toolResults.push({
|
|
243
|
+
type: 'tool_result',
|
|
244
|
+
tool_use_id: call.id,
|
|
245
|
+
content: JSON.stringify({ error: err.message }),
|
|
246
|
+
is_error: true
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
messages.push({ role: 'user', content: toolResults });
|
|
252
|
+
|
|
253
|
+
response = await claude.sendMessage({
|
|
254
|
+
system: systemPrompt,
|
|
255
|
+
messages,
|
|
256
|
+
tools: toolDefs,
|
|
257
|
+
maxTokens: 4096,
|
|
258
|
+
agentId: sessionAgentId,
|
|
259
|
+
model,
|
|
260
|
+
thinking,
|
|
261
|
+
noBuiltinTools: isBootstrap
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
let rawText = claude.extractText(response);
|
|
266
|
+
if (/^\s*\{.*"type"\s*:\s*"result"/.test(rawText)) rawText = '';
|
|
267
|
+
let finalText = rawText.replace(/\n*\[used \w+:[\s\S]*$/, '').trim();
|
|
268
|
+
|
|
269
|
+
if (allToolCalls.length && !finalText) {
|
|
270
|
+
const lastCall = allToolCalls[allToolCalls.length - 1];
|
|
271
|
+
const outputStr = typeof lastCall.output === 'string' ? lastCall.output : JSON.stringify(lastCall.output, null, 2);
|
|
272
|
+
const truncated = outputStr.length > 10000 ? outputStr.slice(0, 10000) + '...' : outputStr;
|
|
273
|
+
finalText = `done (used ${lastCall.name})\n\n${truncated}`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let responseText = finalText;
|
|
277
|
+
if (intermediateTexts.length && finalText) {
|
|
278
|
+
const last = intermediateTexts[intermediateTexts.length - 1];
|
|
279
|
+
if (finalText === last || last.includes(finalText)) {
|
|
280
|
+
responseText = intermediateTexts.join('\n\n');
|
|
281
|
+
} else if (finalText.includes(last)) {
|
|
282
|
+
responseText = [...intermediateTexts.slice(0, -1), finalText].join('\n\n');
|
|
283
|
+
} else {
|
|
284
|
+
responseText = intermediateTexts.join('\n\n') + '\n\n' + finalText;
|
|
285
|
+
}
|
|
286
|
+
} else if (intermediateTexts.length) {
|
|
287
|
+
responseText = intermediateTexts.join('\n\n');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
responseText = responseText.replace(/\n{3,}/g, '\n\n').trim();
|
|
291
|
+
|
|
292
|
+
const assistantMsgId = uuid();
|
|
293
|
+
const toolCallsJson = allToolCalls.length ? JSON.stringify(allToolCalls) : null;
|
|
294
|
+
|
|
295
|
+
if (isHeartbeat) {
|
|
296
|
+
const stripped = responseText.replace(/\s+/g, ' ').trim();
|
|
297
|
+
const isOk = stripped.includes('HEARTBEAT_OK') && stripped.length <= 300;
|
|
298
|
+
if (isOk) {
|
|
299
|
+
responseComplete = true;
|
|
300
|
+
return { id: null, content: responseText, suppressed: true };
|
|
301
|
+
}
|
|
302
|
+
stmts.createHeartbeatMessage.run(assistantMsgId, agentId, 'assistant', responseText, toolCallsJson);
|
|
303
|
+
responseComplete = true;
|
|
304
|
+
emit(agentId, 'heartbeat_alert', {
|
|
305
|
+
id: assistantMsgId,
|
|
306
|
+
content: responseText,
|
|
307
|
+
tool_calls: allToolCalls.length ? allToolCalls : null
|
|
308
|
+
});
|
|
309
|
+
return { id: assistantMsgId, content: responseText, tool_calls: allToolCalls.length ? allToolCalls : null };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
stmts.createMessage.run(assistantMsgId, agentId, 'assistant', responseText, toolCallsJson);
|
|
313
|
+
|
|
314
|
+
if (isBootstrap && stmts.getAgent.get(agentId)?.bootstrapped === 0) {
|
|
315
|
+
const msgCount = stmts.listMessages.all(agentId).filter(m => m.role === 'user').length;
|
|
316
|
+
if (msgCount >= 4) {
|
|
317
|
+
stmts.setBootstrapped.run(1, agentId);
|
|
318
|
+
workspace.deleteFile(agent.name, 'BOOTSTRAP.md');
|
|
319
|
+
emit(agentId, 'bootstrap_complete', {});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (!isHeartbeat) {
|
|
324
|
+
memory.extractMemories(agentId, userContent, responseText).catch(err => {
|
|
325
|
+
console.error(`[memory] extraction failed for ${agentId}: ${err.message}`);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
responseComplete = true;
|
|
330
|
+
processingAgents.delete(agentId);
|
|
331
|
+
emit(agentId, 'typing', { active: false });
|
|
332
|
+
emit(agentId, 'assistant_message', {
|
|
333
|
+
id: assistantMsgId,
|
|
334
|
+
content: responseText,
|
|
335
|
+
tool_calls: allToolCalls.length ? allToolCalls : null
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
return { id: assistantMsgId, content: responseText, tool_calls: allToolCalls.length ? allToolCalls : null };
|
|
339
|
+
|
|
340
|
+
} catch (err) {
|
|
341
|
+
responseComplete = true;
|
|
342
|
+
processingAgents.delete(agentId);
|
|
343
|
+
if (!isHeartbeat) emit(agentId, 'typing', { active: false });
|
|
344
|
+
if (!isHeartbeat) emit(agentId, 'error', { error: err.message });
|
|
345
|
+
throw err;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function enqueueMessage(agentId, content, options = {}) {
|
|
350
|
+
const prior = messageQueues.get(agentId) || Promise.resolve();
|
|
351
|
+
const chained = prior.catch(() => undefined).then(() => handleMessage(agentId, content, options));
|
|
352
|
+
const tracked = chained.finally(() => {
|
|
353
|
+
if (messageQueues.get(agentId) === tracked) {
|
|
354
|
+
messageQueues.delete(agentId);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
messageQueues.set(agentId, tracked);
|
|
358
|
+
return chained;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function isProcessing(agentId) {
|
|
362
|
+
return processingAgents.has(agentId);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
module.exports = { handleMessage, enqueueMessage, addStream, removeStream, emit, isProcessing };
|