kernelbot 1.0.37 → 1.0.38
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/bin/kernel.js +389 -23
- package/config.example.yaml +17 -0
- package/package.json +2 -1
- package/src/agent.js +355 -82
- package/src/bot.js +724 -12
- package/src/character.js +406 -0
- package/src/characters/builder.js +174 -0
- package/src/characters/builtins.js +421 -0
- package/src/conversation.js +17 -2
- package/src/dashboard/agents.css +469 -0
- package/src/dashboard/agents.html +184 -0
- package/src/dashboard/agents.js +873 -0
- package/src/dashboard/dashboard.css +281 -0
- package/src/dashboard/dashboard.js +579 -0
- package/src/dashboard/index.html +366 -0
- package/src/dashboard/server.js +521 -0
- package/src/dashboard/shared.css +700 -0
- package/src/dashboard/shared.js +209 -0
- package/src/life/engine.js +28 -20
- package/src/life/evolution.js +7 -5
- package/src/life/journal.js +5 -4
- package/src/life/memory.js +12 -9
- package/src/life/share-queue.js +7 -5
- package/src/prompts/orchestrator.js +76 -14
- package/src/prompts/workers.js +22 -0
- package/src/self.js +17 -5
- package/src/services/linkedin-api.js +190 -0
- package/src/services/stt.js +8 -2
- package/src/services/tts.js +32 -2
- package/src/services/x-api.js +141 -0
- package/src/swarm/worker-registry.js +7 -0
- package/src/tools/categories.js +4 -0
- package/src/tools/index.js +6 -0
- package/src/tools/linkedin.js +264 -0
- package/src/tools/orchestrator-tools.js +337 -2
- package/src/tools/x.js +256 -0
- package/src/utils/config.js +104 -57
- package/src/utils/display.js +73 -12
- package/src/utils/temporal-awareness.js +24 -10
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { LinkedInAPI } from '../services/linkedin-api.js';
|
|
2
|
+
import { getLogger } from '../utils/logger.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get a configured LinkedIn API client from the tool context.
|
|
6
|
+
* Expects context.config.linkedin.access_token and context.config.linkedin.person_urn
|
|
7
|
+
* to be injected at dispatch time.
|
|
8
|
+
*/
|
|
9
|
+
function getClient(context) {
|
|
10
|
+
const token = context.config.linkedin?.access_token;
|
|
11
|
+
if (!token) throw new Error('LinkedIn not connected. Use /linkedin link to connect your account.');
|
|
12
|
+
return new LinkedInAPI(token);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getPersonUrn(context) {
|
|
16
|
+
const urn = context.config.linkedin?.person_urn;
|
|
17
|
+
if (!urn) throw new Error('LinkedIn person URN not available. Try /linkedin link again.');
|
|
18
|
+
return urn;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const definitions = [
|
|
22
|
+
{
|
|
23
|
+
name: 'linkedin_create_post',
|
|
24
|
+
description: 'Create a LinkedIn post. Can be text-only or include an article link.',
|
|
25
|
+
input_schema: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
text: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
description: 'The post content/commentary text',
|
|
31
|
+
},
|
|
32
|
+
visibility: {
|
|
33
|
+
type: 'string',
|
|
34
|
+
enum: ['PUBLIC', 'CONNECTIONS'],
|
|
35
|
+
description: 'Post visibility (default: PUBLIC)',
|
|
36
|
+
},
|
|
37
|
+
article_url: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
description: 'Optional URL to share as an article attachment',
|
|
40
|
+
},
|
|
41
|
+
article_title: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
description: 'Optional title for the shared article',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
required: ['text'],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'linkedin_get_my_posts',
|
|
51
|
+
description: 'Get the user\'s recent LinkedIn posts. NOTE: Requires r_member_social permission (restricted). May return 403 if the LinkedIn app is not approved for this scope.',
|
|
52
|
+
input_schema: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: {
|
|
55
|
+
count: {
|
|
56
|
+
type: 'number',
|
|
57
|
+
description: 'Number of posts to fetch (default 10)',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'linkedin_get_post',
|
|
64
|
+
description: 'Get a specific LinkedIn post by its URN. NOTE: Requires r_member_social permission (restricted). May return 403 if the LinkedIn app is not approved for this scope.',
|
|
65
|
+
input_schema: {
|
|
66
|
+
type: 'object',
|
|
67
|
+
properties: {
|
|
68
|
+
post_urn: {
|
|
69
|
+
type: 'string',
|
|
70
|
+
description: 'The LinkedIn post URN (e.g. urn:li:share:12345)',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
required: ['post_urn'],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'linkedin_comment_on_post',
|
|
78
|
+
description: 'Add a comment to a LinkedIn post.',
|
|
79
|
+
input_schema: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
82
|
+
post_urn: {
|
|
83
|
+
type: 'string',
|
|
84
|
+
description: 'The LinkedIn post URN to comment on',
|
|
85
|
+
},
|
|
86
|
+
comment: {
|
|
87
|
+
type: 'string',
|
|
88
|
+
description: 'The comment text',
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
required: ['post_urn', 'comment'],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'linkedin_get_comments',
|
|
96
|
+
description: 'Get comments on a LinkedIn post. NOTE: Requires r_member_social permission (restricted). May return 403 if the LinkedIn app is not approved for this scope.',
|
|
97
|
+
input_schema: {
|
|
98
|
+
type: 'object',
|
|
99
|
+
properties: {
|
|
100
|
+
post_urn: {
|
|
101
|
+
type: 'string',
|
|
102
|
+
description: 'The LinkedIn post URN',
|
|
103
|
+
},
|
|
104
|
+
count: {
|
|
105
|
+
type: 'number',
|
|
106
|
+
description: 'Number of comments to fetch (default 10)',
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
required: ['post_urn'],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'linkedin_like_post',
|
|
114
|
+
description: 'Like a LinkedIn post.',
|
|
115
|
+
input_schema: {
|
|
116
|
+
type: 'object',
|
|
117
|
+
properties: {
|
|
118
|
+
post_urn: {
|
|
119
|
+
type: 'string',
|
|
120
|
+
description: 'The LinkedIn post URN to like',
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
required: ['post_urn'],
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'linkedin_get_profile',
|
|
128
|
+
description: 'Get the linked LinkedIn profile information. NOTE: Requires "Sign in with LinkedIn" product (openid + profile scopes). Returns limited info if only w_member_social is available.',
|
|
129
|
+
input_schema: {
|
|
130
|
+
type: 'object',
|
|
131
|
+
properties: {},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'linkedin_delete_post',
|
|
136
|
+
description: 'Delete a LinkedIn post.',
|
|
137
|
+
input_schema: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
properties: {
|
|
140
|
+
post_urn: {
|
|
141
|
+
type: 'string',
|
|
142
|
+
description: 'The LinkedIn post URN to delete',
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
required: ['post_urn'],
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
export const handlers = {
|
|
151
|
+
linkedin_create_post: async (params, context) => {
|
|
152
|
+
try {
|
|
153
|
+
const client = getClient(context);
|
|
154
|
+
const authorUrn = getPersonUrn(context);
|
|
155
|
+
const visibility = params.visibility || 'PUBLIC';
|
|
156
|
+
|
|
157
|
+
let result;
|
|
158
|
+
if (params.article_url) {
|
|
159
|
+
result = await client.createArticlePost(authorUrn, params.text, params.article_url, params.article_title);
|
|
160
|
+
} else {
|
|
161
|
+
result = await client.createTextPost(authorUrn, params.text, visibility);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { success: true, message: 'Post created successfully', post: result };
|
|
165
|
+
} catch (err) {
|
|
166
|
+
getLogger().error(`linkedin_create_post failed: ${err.message}`);
|
|
167
|
+
return { error: err.response?.data?.message || err.message };
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
linkedin_get_my_posts: async (params, context) => {
|
|
172
|
+
try {
|
|
173
|
+
const client = getClient(context);
|
|
174
|
+
const authorUrn = getPersonUrn(context);
|
|
175
|
+
const posts = await client.getMyPosts(authorUrn, params.count || 10);
|
|
176
|
+
return { posts, count: posts.length };
|
|
177
|
+
} catch (err) {
|
|
178
|
+
getLogger().error(`linkedin_get_my_posts failed: ${err.message}`);
|
|
179
|
+
if (err.response?.status === 403) {
|
|
180
|
+
return { error: 'Access denied — reading posts requires r_member_social permission which is restricted to approved LinkedIn apps. Your token only has w_member_social (write-only: post, comment, like).' };
|
|
181
|
+
}
|
|
182
|
+
return { error: err.response?.data?.message || err.message };
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
linkedin_get_post: async (params, context) => {
|
|
187
|
+
try {
|
|
188
|
+
const client = getClient(context);
|
|
189
|
+
const post = await client.getPost(params.post_urn);
|
|
190
|
+
return { post };
|
|
191
|
+
} catch (err) {
|
|
192
|
+
getLogger().error(`linkedin_get_post failed: ${err.message}`);
|
|
193
|
+
if (err.response?.status === 403) {
|
|
194
|
+
return { error: 'Access denied — reading posts requires r_member_social permission which is restricted to approved LinkedIn apps.' };
|
|
195
|
+
}
|
|
196
|
+
return { error: err.response?.data?.message || err.message };
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
linkedin_comment_on_post: async (params, context) => {
|
|
201
|
+
try {
|
|
202
|
+
const client = getClient(context);
|
|
203
|
+
const actorUrn = getPersonUrn(context);
|
|
204
|
+
const result = await client.addComment(params.post_urn, params.comment, actorUrn);
|
|
205
|
+
return { success: true, message: 'Comment posted', comment: result };
|
|
206
|
+
} catch (err) {
|
|
207
|
+
getLogger().error(`linkedin_comment_on_post failed: ${err.message}`);
|
|
208
|
+
return { error: err.response?.data?.message || err.message };
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
linkedin_get_comments: async (params, context) => {
|
|
213
|
+
try {
|
|
214
|
+
const client = getClient(context);
|
|
215
|
+
const comments = await client.getComments(params.post_urn, params.count || 10);
|
|
216
|
+
return { comments, count: comments.length };
|
|
217
|
+
} catch (err) {
|
|
218
|
+
getLogger().error(`linkedin_get_comments failed: ${err.message}`);
|
|
219
|
+
if (err.response?.status === 403) {
|
|
220
|
+
return { error: 'Access denied — reading comments requires r_member_social permission which is restricted to approved LinkedIn apps.' };
|
|
221
|
+
}
|
|
222
|
+
return { error: err.response?.data?.message || err.message };
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
linkedin_like_post: async (params, context) => {
|
|
227
|
+
try {
|
|
228
|
+
const client = getClient(context);
|
|
229
|
+
const actorUrn = getPersonUrn(context);
|
|
230
|
+
const result = await client.likePost(params.post_urn, actorUrn);
|
|
231
|
+
return { success: true, message: 'Post liked', result };
|
|
232
|
+
} catch (err) {
|
|
233
|
+
getLogger().error(`linkedin_like_post failed: ${err.message}`);
|
|
234
|
+
return { error: err.response?.data?.message || err.message };
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
linkedin_get_profile: async (params, context) => {
|
|
239
|
+
try {
|
|
240
|
+
const client = getClient(context);
|
|
241
|
+
const profile = await client.getProfile();
|
|
242
|
+
if (!profile) {
|
|
243
|
+
// No openid+profile scopes — return what we know from config
|
|
244
|
+
const urn = context.config.linkedin?.person_urn;
|
|
245
|
+
return { profile: { person_urn: urn }, note: 'Full profile unavailable — LinkedIn app only has w_member_social scope. Add "Sign in with LinkedIn" product for full profile access.' };
|
|
246
|
+
}
|
|
247
|
+
return { profile };
|
|
248
|
+
} catch (err) {
|
|
249
|
+
getLogger().error(`linkedin_get_profile failed: ${err.message}`);
|
|
250
|
+
return { error: err.response?.data?.message || err.message };
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
linkedin_delete_post: async (params, context) => {
|
|
255
|
+
try {
|
|
256
|
+
const client = getClient(context);
|
|
257
|
+
await client.deletePost(params.post_urn);
|
|
258
|
+
return { success: true, message: 'Post deleted' };
|
|
259
|
+
} catch (err) {
|
|
260
|
+
getLogger().error(`linkedin_delete_post failed: ${err.message}`);
|
|
261
|
+
return { error: err.response?.data?.message || err.message };
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
};
|
|
@@ -5,7 +5,35 @@ import { getLogger } from '../utils/logger.js';
|
|
|
5
5
|
const workerTypeEnum = Object.keys(WORKER_TYPES);
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Tokenize a task string for similarity comparison.
|
|
9
|
+
* Lowercase, strip punctuation, filter words ≤2 chars.
|
|
10
|
+
*/
|
|
11
|
+
function tokenize(text) {
|
|
12
|
+
return text
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.replace(/[^\w\s]/g, ' ')
|
|
15
|
+
.split(/\s+/)
|
|
16
|
+
.filter(w => w.length > 2);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Jaccard word-overlap similarity between two task strings.
|
|
21
|
+
* Returns a value between 0 and 1.
|
|
22
|
+
*/
|
|
23
|
+
function taskSimilarity(a, b) {
|
|
24
|
+
const setA = new Set(tokenize(a));
|
|
25
|
+
const setB = new Set(tokenize(b));
|
|
26
|
+
if (setA.size === 0 && setB.size === 0) return 1;
|
|
27
|
+
if (setA.size === 0 || setB.size === 0) return 0;
|
|
28
|
+
let intersection = 0;
|
|
29
|
+
for (const word of setA) {
|
|
30
|
+
if (setB.has(word)) intersection++;
|
|
31
|
+
}
|
|
32
|
+
return intersection / (setA.size + setB.size - intersection);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Tool definitions for the orchestrator's meta-tools.
|
|
9
37
|
*/
|
|
10
38
|
export const orchestratorToolDefinitions = [
|
|
11
39
|
{
|
|
@@ -58,6 +86,20 @@ export const orchestratorToolDefinitions = [
|
|
|
58
86
|
required: ['job_id'],
|
|
59
87
|
},
|
|
60
88
|
},
|
|
89
|
+
{
|
|
90
|
+
name: 'check_job',
|
|
91
|
+
description: 'Get detailed diagnostics for a specific job — status, elapsed time, activity log, and stuck detection. Use this to investigate workers that seem slow or unresponsive.',
|
|
92
|
+
input_schema: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
properties: {
|
|
95
|
+
job_id: {
|
|
96
|
+
type: 'string',
|
|
97
|
+
description: 'The ID of the job to check.',
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
required: ['job_id'],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
61
103
|
{
|
|
62
104
|
name: 'create_automation',
|
|
63
105
|
description: 'Create a recurring automation that runs on a schedule. The task description will be executed as a standalone prompt each time it fires.',
|
|
@@ -176,6 +218,60 @@ export const orchestratorToolDefinitions = [
|
|
|
176
218
|
required: ['emoji'],
|
|
177
219
|
},
|
|
178
220
|
},
|
|
221
|
+
{
|
|
222
|
+
name: 'recall_memories',
|
|
223
|
+
description: 'Search your episodic and semantic memory for information about a topic, event, or past experience. Use this when the user references something from the past or you need context about a specific subject.',
|
|
224
|
+
input_schema: {
|
|
225
|
+
type: 'object',
|
|
226
|
+
properties: {
|
|
227
|
+
query: {
|
|
228
|
+
type: 'string',
|
|
229
|
+
description: 'Keywords or topic to search memories for (e.g. "kubernetes", "birthday", "project deadline").',
|
|
230
|
+
},
|
|
231
|
+
time_range_hours: {
|
|
232
|
+
type: 'number',
|
|
233
|
+
description: 'Optional: limit episodic search to the last N hours. Defaults to searching all available memories.',
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
required: ['query'],
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: 'recall_user_history',
|
|
241
|
+
description: 'Retrieve memories specifically about a user — past interactions, preferences, things they told you. Use when you need to remember what a specific person has shared or discussed.',
|
|
242
|
+
input_schema: {
|
|
243
|
+
type: 'object',
|
|
244
|
+
properties: {
|
|
245
|
+
user_id: {
|
|
246
|
+
type: 'string',
|
|
247
|
+
description: 'The user ID to retrieve memories for.',
|
|
248
|
+
},
|
|
249
|
+
limit: {
|
|
250
|
+
type: 'number',
|
|
251
|
+
description: 'Maximum number of memories to return. Default: 10.',
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
required: ['user_id'],
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: 'search_conversations',
|
|
259
|
+
description: 'Search past messages in the current (or specified) chat for a keyword or phrase. Use when you need to find something specific that was said in conversation.',
|
|
260
|
+
input_schema: {
|
|
261
|
+
type: 'object',
|
|
262
|
+
properties: {
|
|
263
|
+
query: {
|
|
264
|
+
type: 'string',
|
|
265
|
+
description: 'Keyword or phrase to search for in conversation history.',
|
|
266
|
+
},
|
|
267
|
+
chat_id: {
|
|
268
|
+
type: 'string',
|
|
269
|
+
description: 'Optional: chat ID to search. Defaults to the current chat.',
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
required: ['query'],
|
|
273
|
+
},
|
|
274
|
+
},
|
|
179
275
|
];
|
|
180
276
|
|
|
181
277
|
/**
|
|
@@ -259,6 +355,26 @@ export async function executeOrchestratorTool(name, input, context) {
|
|
|
259
355
|
}
|
|
260
356
|
}
|
|
261
357
|
|
|
358
|
+
// Duplicate task detection — compare against running + queued jobs in this chat
|
|
359
|
+
const activeJobs = jobManager.getJobsForChat(chatId)
|
|
360
|
+
.filter(j => (j.status === 'running' || j.status === 'queued') && j.workerType === worker_type);
|
|
361
|
+
|
|
362
|
+
for (const existing of activeJobs) {
|
|
363
|
+
const sim = taskSimilarity(task, existing.task);
|
|
364
|
+
if (sim > 0.7) {
|
|
365
|
+
logger.warn(`[dispatch_task] Duplicate blocked — ${(sim * 100).toFixed(0)}% similar to job ${existing.id}: "${existing.task.slice(0, 80)}"`);
|
|
366
|
+
return {
|
|
367
|
+
error: `A very similar ${worker_type} task is already ${existing.status} (job ${existing.id}, ${(sim * 100).toFixed(0)}% match). Wait for it to finish or cancel it first.`,
|
|
368
|
+
existing_job_id: existing.id,
|
|
369
|
+
similarity: Math.round(sim * 100),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
if (sim > 0.4) {
|
|
373
|
+
logger.info(`[dispatch_task] Similar task warning — ${(sim * 100).toFixed(0)}% overlap with job ${existing.id}`);
|
|
374
|
+
credentialWarning = [credentialWarning, `Warning: This task is ${(sim * 100).toFixed(0)}% similar to an active ${worker_type} job (${existing.id}). Proceeding anyway.`].filter(Boolean).join('\n');
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
262
378
|
// Create the job with context and dependencies
|
|
263
379
|
const job = jobManager.createJob(chatId, worker_type, task);
|
|
264
380
|
job.context = [taskContext, credentialWarning].filter(Boolean).join('\n\n') || null;
|
|
@@ -288,12 +404,21 @@ export async function executeOrchestratorTool(name, input, context) {
|
|
|
288
404
|
}
|
|
289
405
|
});
|
|
290
406
|
|
|
291
|
-
|
|
407
|
+
// Collect sibling active jobs for awareness
|
|
408
|
+
const siblingJobs = jobManager.getJobsForChat(chatId)
|
|
409
|
+
.filter(j => j.id !== job.id && (j.status === 'running' || j.status === 'queued'))
|
|
410
|
+
.map(j => ({ id: j.id, worker_type: j.workerType, status: j.status, task: j.task.slice(0, 80) }));
|
|
411
|
+
|
|
412
|
+
const result = {
|
|
292
413
|
job_id: job.id,
|
|
293
414
|
worker_type,
|
|
294
415
|
status: 'dispatched',
|
|
295
416
|
message: `${workerConfig.emoji} ${workerConfig.label} started.`,
|
|
296
417
|
};
|
|
418
|
+
if (siblingJobs.length > 0) {
|
|
419
|
+
result.other_active_jobs = siblingJobs;
|
|
420
|
+
}
|
|
421
|
+
return result;
|
|
297
422
|
}
|
|
298
423
|
|
|
299
424
|
case 'list_jobs': {
|
|
@@ -345,6 +470,68 @@ export async function executeOrchestratorTool(name, input, context) {
|
|
|
345
470
|
};
|
|
346
471
|
}
|
|
347
472
|
|
|
473
|
+
case 'check_job': {
|
|
474
|
+
const { job_id } = input;
|
|
475
|
+
logger.info(`[check_job] Checking job ${job_id}`);
|
|
476
|
+
const job = jobManager.getJob(job_id);
|
|
477
|
+
if (!job) {
|
|
478
|
+
return { error: `Job ${job_id} not found.` };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const now = Date.now();
|
|
482
|
+
const elapsed = job.startedAt ? Math.round((now - job.startedAt) / 1000) : 0;
|
|
483
|
+
const timeoutSec = job.timeoutMs ? Math.round(job.timeoutMs / 1000) : null;
|
|
484
|
+
const timeRemaining = (timeoutSec && job.startedAt) ? Math.max(0, timeoutSec - elapsed) : null;
|
|
485
|
+
|
|
486
|
+
const result = {
|
|
487
|
+
job_id: job.id,
|
|
488
|
+
worker_type: job.workerType,
|
|
489
|
+
status: job.status,
|
|
490
|
+
task: job.task.slice(0, 200),
|
|
491
|
+
elapsed_seconds: elapsed,
|
|
492
|
+
timeout_seconds: timeoutSec,
|
|
493
|
+
time_remaining_seconds: timeRemaining,
|
|
494
|
+
llm_calls: job.llmCalls,
|
|
495
|
+
tool_calls: job.toolCalls,
|
|
496
|
+
last_thinking: job.lastThinking ? job.lastThinking.slice(0, 300) : null,
|
|
497
|
+
recent_activity: job.progress.slice(-10),
|
|
498
|
+
last_activity_seconds_ago: job.lastActivity ? Math.round((now - job.lastActivity) / 1000) : null,
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
// Stuck detection diagnostics (only for running jobs)
|
|
502
|
+
// Long-running workers (coding, devops) get higher thresholds
|
|
503
|
+
if (job.status === 'running') {
|
|
504
|
+
const diagnostics = [];
|
|
505
|
+
const isLongRunning = ['coding', 'devops'].includes(job.workerType);
|
|
506
|
+
const idleThreshold = isLongRunning ? 600 : 120;
|
|
507
|
+
const loopLlmThreshold = isLongRunning ? 50 : 15;
|
|
508
|
+
|
|
509
|
+
const idleSec = job.lastActivity ? Math.round((now - job.lastActivity) / 1000) : elapsed;
|
|
510
|
+
if (idleSec > idleThreshold) {
|
|
511
|
+
diagnostics.push(`IDLE for ${idleSec}s — no activity detected`);
|
|
512
|
+
}
|
|
513
|
+
if (job.llmCalls > loopLlmThreshold && job.toolCalls < 3) {
|
|
514
|
+
diagnostics.push(`POSSIBLY LOOPING — ${job.llmCalls} LLM calls but only ${job.toolCalls} tool calls`);
|
|
515
|
+
}
|
|
516
|
+
if (timeoutSec && elapsed > timeoutSec * 0.75) {
|
|
517
|
+
const pct = Math.round((elapsed / timeoutSec) * 100);
|
|
518
|
+
diagnostics.push(`${pct}% of timeout used (${elapsed}s / ${timeoutSec}s)`);
|
|
519
|
+
}
|
|
520
|
+
if (diagnostics.length > 0) {
|
|
521
|
+
result.warnings = diagnostics;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (job.dependsOn.length > 0) result.depends_on = job.dependsOn;
|
|
526
|
+
if (job.error) result.error = job.error;
|
|
527
|
+
if (job.structuredResult) {
|
|
528
|
+
result.result_summary = job.structuredResult.summary;
|
|
529
|
+
result.result_status = job.structuredResult.status;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return result;
|
|
533
|
+
}
|
|
534
|
+
|
|
348
535
|
case 'create_automation': {
|
|
349
536
|
if (!automationManager) return { error: 'Automation system not available.' };
|
|
350
537
|
|
|
@@ -479,6 +666,154 @@ export async function executeOrchestratorTool(name, input, context) {
|
|
|
479
666
|
}
|
|
480
667
|
}
|
|
481
668
|
|
|
669
|
+
case 'recall_memories': {
|
|
670
|
+
const { memoryManager } = context;
|
|
671
|
+
if (!memoryManager) return { error: 'Memory system not available.' };
|
|
672
|
+
|
|
673
|
+
const { query, time_range_hours } = input;
|
|
674
|
+
logger.info(`[recall_memories] Searching for: "${query}" (time_range=${time_range_hours || 'all'})`);
|
|
675
|
+
|
|
676
|
+
const episodic = memoryManager.searchEpisodic(query, 10);
|
|
677
|
+
const semantic = memoryManager.searchSemantic(query, 5);
|
|
678
|
+
|
|
679
|
+
// Filter by time range if specified
|
|
680
|
+
const now = Date.now();
|
|
681
|
+
const filteredEpisodic = time_range_hours
|
|
682
|
+
? episodic.filter(m => (now - m.timestamp) <= time_range_hours * 3600_000)
|
|
683
|
+
: episodic;
|
|
684
|
+
|
|
685
|
+
const formatAge = (ts) => {
|
|
686
|
+
const ageH = Math.round((now - ts) / 3600_000);
|
|
687
|
+
if (ageH < 1) return 'just now';
|
|
688
|
+
if (ageH < 24) return `${ageH}h ago`;
|
|
689
|
+
const ageD = Math.round(ageH / 24);
|
|
690
|
+
return `${ageD}d ago`;
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
const episodicResults = filteredEpisodic.map(m => ({
|
|
694
|
+
summary: m.summary,
|
|
695
|
+
age: formatAge(m.timestamp),
|
|
696
|
+
importance: m.importance,
|
|
697
|
+
tags: m.tags,
|
|
698
|
+
type: m.type,
|
|
699
|
+
}));
|
|
700
|
+
|
|
701
|
+
const semanticResults = semantic.map(s => ({
|
|
702
|
+
topic: s.topic,
|
|
703
|
+
summary: s.summary,
|
|
704
|
+
learned: formatAge(s.learnedAt),
|
|
705
|
+
related: s.relatedTopics,
|
|
706
|
+
}));
|
|
707
|
+
|
|
708
|
+
logger.info(`[recall_memories] Found ${episodicResults.length} episodic, ${semanticResults.length} semantic results for query="${query}"`);
|
|
709
|
+
|
|
710
|
+
// Detailed logging — show what was actually recalled
|
|
711
|
+
for (const m of episodicResults) {
|
|
712
|
+
logger.info(`[recall_memories] 📝 [${m.age}, imp=${m.importance}] ${m.summary.slice(0, 120)}`);
|
|
713
|
+
}
|
|
714
|
+
for (const s of semanticResults) {
|
|
715
|
+
logger.info(`[recall_memories] 🧠 [${s.topic}] ${s.summary.slice(0, 120)}`);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (episodicResults.length === 0 && semanticResults.length === 0) {
|
|
719
|
+
logger.info(`[recall_memories] No results found for query="${query}"`);
|
|
720
|
+
return { message: `No memories found matching "${query}".` };
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return {
|
|
724
|
+
query,
|
|
725
|
+
episodic: episodicResults,
|
|
726
|
+
semantic: semanticResults,
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
case 'recall_user_history': {
|
|
731
|
+
const { memoryManager } = context;
|
|
732
|
+
if (!memoryManager) return { error: 'Memory system not available.' };
|
|
733
|
+
|
|
734
|
+
const { user_id, limit } = input;
|
|
735
|
+
logger.info(`[recall_user_history] Retrieving memories for user: ${user_id} (limit=${limit || 10})`);
|
|
736
|
+
|
|
737
|
+
const memories = memoryManager.getMemoriesAboutUser(user_id, limit || 10);
|
|
738
|
+
|
|
739
|
+
const now = Date.now();
|
|
740
|
+
const results = memories.map(m => ({
|
|
741
|
+
summary: m.summary,
|
|
742
|
+
age: (() => {
|
|
743
|
+
const ageH = Math.round((now - m.timestamp) / 3600_000);
|
|
744
|
+
if (ageH < 1) return 'just now';
|
|
745
|
+
if (ageH < 24) return `${ageH}h ago`;
|
|
746
|
+
return `${Math.round(ageH / 24)}d ago`;
|
|
747
|
+
})(),
|
|
748
|
+
importance: m.importance,
|
|
749
|
+
type: m.type,
|
|
750
|
+
tags: m.tags,
|
|
751
|
+
}));
|
|
752
|
+
|
|
753
|
+
logger.info(`[recall_user_history] Found ${results.length} memories for user ${user_id}`);
|
|
754
|
+
|
|
755
|
+
// Detailed logging — show each recalled memory
|
|
756
|
+
for (const m of results) {
|
|
757
|
+
logger.info(`[recall_user_history] 📝 [${m.age}, imp=${m.importance}, ${m.type}] ${m.summary.slice(0, 120)}`);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (results.length === 0) {
|
|
761
|
+
logger.info(`[recall_user_history] No memories found for user ${user_id}`);
|
|
762
|
+
return { message: `No memories found for user ${user_id}.` };
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return { user_id, memories: results };
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
case 'search_conversations': {
|
|
769
|
+
const { conversationManager } = context;
|
|
770
|
+
if (!conversationManager) return { error: 'Conversation system not available.' };
|
|
771
|
+
|
|
772
|
+
const { query, chat_id } = input;
|
|
773
|
+
const targetChatId = chat_id || chatId;
|
|
774
|
+
logger.info(`[search_conversations] Searching chat ${targetChatId} for: "${query}"`);
|
|
775
|
+
|
|
776
|
+
const history = conversationManager.getHistory(targetChatId);
|
|
777
|
+
const queryLower = query.toLowerCase();
|
|
778
|
+
|
|
779
|
+
const matches = [];
|
|
780
|
+
for (let i = 0; i < history.length; i++) {
|
|
781
|
+
const msg = history[i];
|
|
782
|
+
const content = typeof msg.content === 'string' ? msg.content : '';
|
|
783
|
+
if (content.toLowerCase().includes(queryLower)) {
|
|
784
|
+
matches.push({
|
|
785
|
+
role: msg.role,
|
|
786
|
+
snippet: content.length > 300 ? content.slice(0, 300) + '...' : content,
|
|
787
|
+
age: msg.timestamp
|
|
788
|
+
? (() => {
|
|
789
|
+
const ageH = Math.round((Date.now() - msg.timestamp) / 3600_000);
|
|
790
|
+
if (ageH < 1) return 'just now';
|
|
791
|
+
if (ageH < 24) return `${ageH}h ago`;
|
|
792
|
+
return `${Math.round(ageH / 24)}d ago`;
|
|
793
|
+
})()
|
|
794
|
+
: 'unknown',
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Return last 10 matches (most recent)
|
|
800
|
+
const results = matches.slice(-10);
|
|
801
|
+
|
|
802
|
+
logger.info(`[search_conversations] Found ${matches.length} total matches in chat ${targetChatId}, returning last ${results.length}`);
|
|
803
|
+
|
|
804
|
+
// Detailed logging — show matched conversation snippets
|
|
805
|
+
for (const m of results) {
|
|
806
|
+
logger.info(`[search_conversations] 💬 [${m.role}, ${m.age}] ${m.snippet.slice(0, 100)}`);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (results.length === 0) {
|
|
810
|
+
logger.info(`[search_conversations] No matches for "${query}" in chat ${targetChatId}`);
|
|
811
|
+
return { message: `No messages found matching "${query}" in this chat.` };
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return { query, chat_id: targetChatId, matches: results, total_matches: matches.length };
|
|
815
|
+
}
|
|
816
|
+
|
|
482
817
|
default:
|
|
483
818
|
return { error: `Unknown orchestrator tool: ${name}` };
|
|
484
819
|
}
|