@siftd/connect-agent 0.2.37 → 0.2.39
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/dist/agent.js +173 -13
- package/dist/api.d.ts +2 -0
- package/dist/cli.js +4 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.js +41 -0
- package/dist/core/assets.js +12 -1
- package/dist/core/context-graph.d.ts +94 -0
- package/dist/core/context-graph.js +247 -0
- package/dist/core/file-tracker.js +3 -1
- package/dist/core/hub.js +36 -1
- package/dist/core/memory-advanced.d.ts +5 -0
- package/dist/core/memory-advanced.js +27 -0
- package/dist/core/memory-postgres.d.ts +9 -0
- package/dist/core/memory-postgres.js +48 -0
- package/dist/core/preview-worker.js +3 -0
- package/dist/core/task-queue.d.ts +125 -0
- package/dist/core/task-queue.js +248 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/orchestrator.d.ts +108 -0
- package/dist/orchestrator.js +1086 -26
- package/dist/tools/calendar.d.ts +50 -0
- package/dist/tools/calendar.js +233 -0
- package/dist/tools/worker.d.ts +6 -1
- package/dist/tools/worker.js +6 -0
- package/dist/workers/manager.d.ts +7 -0
- package/dist/workers/manager.js +35 -5
- package/package.json +1 -1
package/dist/orchestrator.js
CHANGED
|
@@ -11,13 +11,16 @@ import { AdvancedMemoryStore } from './core/memory-advanced.js';
|
|
|
11
11
|
import { PostgresMemoryStore, isPostgresConfigured } from './core/memory-postgres.js';
|
|
12
12
|
import { TaskScheduler } from './core/scheduler.js';
|
|
13
13
|
import { SystemIndexer } from './core/system-indexer.js';
|
|
14
|
+
import { ContextGraph, ContextGraphKinds } from './core/context-graph.js';
|
|
14
15
|
import { BashTool } from './tools/bash.js';
|
|
15
16
|
import { WebTools } from './tools/web.js';
|
|
16
17
|
import { WorkerTools } from './tools/worker.js';
|
|
18
|
+
import { CalendarTools } from './tools/calendar.js';
|
|
17
19
|
import { SharedState } from './workers/shared-state.js';
|
|
18
20
|
import { getKnowledgeForPrompt } from './genesis/index.js';
|
|
19
21
|
import { loadHubContext, formatHubContext, logAction, logWorker } from './core/hub.js';
|
|
20
22
|
import { buildWorkerPrompt } from './prompts/worker-system.js';
|
|
23
|
+
import { LiaTaskQueue } from './core/task-queue.js';
|
|
21
24
|
/**
|
|
22
25
|
* Extract file paths from worker output
|
|
23
26
|
* Workers naturally mention files they create: "Created /tmp/foo.html", "Saved to /path/file"
|
|
@@ -72,6 +75,14 @@ YOUR IDENTITY:
|
|
|
72
75
|
HOW YOU WORK:
|
|
73
76
|
You are the orchestrator, not the executor. You coordinate and remember while workers do the hands-on work.
|
|
74
77
|
|
|
78
|
+
TASK QUEUE (Always-Listening):
|
|
79
|
+
You have an internal task queue. Users can keep sending messages while you work - you're interruptible.
|
|
80
|
+
- Use lia_plan to break down complex goals into steps
|
|
81
|
+
- Use lia_add_task to queue follow-up work or new requests that come in
|
|
82
|
+
- Use lia_get_queue to see what's pending
|
|
83
|
+
- When you receive multiple requests, acknowledge them and queue them: "Got it, adding to my queue..."
|
|
84
|
+
- You can work on one task while others wait - always stay responsive
|
|
85
|
+
|
|
75
86
|
TOOL RULES:
|
|
76
87
|
|
|
77
88
|
✅ bash - READ-ONLY operations only:
|
|
@@ -90,11 +101,21 @@ TOOL RULES:
|
|
|
90
101
|
- Building, testing, deploying
|
|
91
102
|
- Any filesystem modification
|
|
92
103
|
|
|
104
|
+
ATTACHMENTS:
|
|
105
|
+
- If the user message includes "📎 Attached files:", include those URLs verbatim in any worker task.
|
|
106
|
+
- Workers should download the files from the URLs and proceed without asking for the content again.
|
|
107
|
+
|
|
93
108
|
MEMORY:
|
|
94
109
|
- Search memory before starting work (context is valuable)
|
|
95
110
|
- Remember important things after significant actions
|
|
96
111
|
- Learn from failures - what went wrong, how to avoid it
|
|
97
112
|
|
|
113
|
+
CONTEXT GRAPH:
|
|
114
|
+
- Use remember_entity to store people, teams, projects, tools, tasks
|
|
115
|
+
- Use remember_relation to link entities (owns, works_on, depends_on, etc.)
|
|
116
|
+
- Use search_context to retrieve team context before asking users to repeat
|
|
117
|
+
- If user asks for "/context", call search_context and summarize results
|
|
118
|
+
|
|
98
119
|
YOUR HUB: ~/Lia-Hub/
|
|
99
120
|
- **CLAUDE.md** - User's instructions (always follow these)
|
|
100
121
|
- **LANDMARKS.md** - Current state and context
|
|
@@ -109,22 +130,52 @@ The Lia interface has a built-in gallery that shows worker outputs automatically
|
|
|
109
130
|
✅ Workers write to ~/Lia-Hub/shared/outputs/
|
|
110
131
|
✅ Gallery displays assets in-app automatically
|
|
111
132
|
|
|
133
|
+
FILES BROWSER:
|
|
134
|
+
Users can type /files to open the Finder UI (cloud mode).
|
|
135
|
+
When asked to browse or locate files, point them to /files.
|
|
136
|
+
Refer to /files instead of internal Lia-Hub paths in responses.
|
|
137
|
+
|
|
138
|
+
FILES SCOPES:
|
|
139
|
+
- "My Files" are private to the user (default /files view)
|
|
140
|
+
- "Team Files" are shared across the org (use /files team or the Team Files location)
|
|
141
|
+
- If a user asks to share with the team, store outputs in Team Files and mention that it is shared
|
|
142
|
+
|
|
143
|
+
CALENDAR + TODO (Lia-managed data):
|
|
144
|
+
- /cal reads ~/Lia-Hub/shared/outputs/.lia/calendar.json
|
|
145
|
+
- /todo reads ~/Lia-Hub/shared/outputs/.lia/todos.json
|
|
146
|
+
- When users add, edit, or delete events, use calendar_upsert_events
|
|
147
|
+
- When users add, edit, or delete tasks, use todo_upsert_items
|
|
148
|
+
- Keep these files hidden; refer users to /cal or /todo in responses
|
|
149
|
+
- Schemas:
|
|
150
|
+
- calendar.json: { "version": 1, "calendars": [...], "events": [...] }
|
|
151
|
+
- todos.json: { "version": 1, "items": [...] }
|
|
152
|
+
|
|
112
153
|
WORKFLOW:
|
|
113
154
|
Before complex work: Check CLAUDE.md → Read LANDMARKS.md → Search memory
|
|
114
155
|
After completing work: Update LANDMARKS.md → Remember learnings
|
|
115
156
|
|
|
116
|
-
You orchestrate through workers. You remember through memory. You never do file operations directly.`;
|
|
157
|
+
You orchestrate through workers. You remember through memory. You never do arbitrary file operations directly (calendar_upsert_events and todo_upsert_items are the safe exceptions).`;
|
|
117
158
|
export class MasterOrchestrator {
|
|
118
159
|
client;
|
|
119
160
|
model;
|
|
120
161
|
maxTokens;
|
|
121
162
|
memory;
|
|
163
|
+
contextGraph;
|
|
164
|
+
orgMemory;
|
|
165
|
+
orgContextGraph;
|
|
166
|
+
orgId;
|
|
167
|
+
orgRole;
|
|
168
|
+
instanceMode;
|
|
169
|
+
userOrgMemories;
|
|
170
|
+
userOrgContextGraphs;
|
|
122
171
|
scheduler;
|
|
123
172
|
indexer;
|
|
124
173
|
jobs = new Map();
|
|
125
174
|
jobCounter = 0;
|
|
126
175
|
workerStatusCallback = null;
|
|
176
|
+
workerLogCallback = null;
|
|
127
177
|
workerStatusInterval = null;
|
|
178
|
+
attachmentContext = null;
|
|
128
179
|
galleryCallback = null;
|
|
129
180
|
userId;
|
|
130
181
|
workspaceDir;
|
|
@@ -134,17 +185,28 @@ export class MasterOrchestrator {
|
|
|
134
185
|
bashTool;
|
|
135
186
|
webTools;
|
|
136
187
|
workerTools;
|
|
188
|
+
calendarTools;
|
|
137
189
|
sharedState;
|
|
138
190
|
verboseMode = new Map(); // per-user verbose mode
|
|
191
|
+
// Lia's internal task queue - allows async message processing
|
|
192
|
+
taskQueue;
|
|
193
|
+
taskUpdateCallback;
|
|
194
|
+
planUpdateCallback;
|
|
139
195
|
constructor(options) {
|
|
140
196
|
this.client = new Anthropic({ apiKey: options.apiKey });
|
|
141
197
|
this.model = options.model || 'claude-sonnet-4-20250514';
|
|
142
198
|
this.maxTokens = options.maxTokens || 4096;
|
|
143
199
|
this.userId = options.userId;
|
|
200
|
+
this.orgId = options.orgId;
|
|
201
|
+
this.orgRole = options.orgRole;
|
|
202
|
+
this.instanceMode = options.instanceMode || 'personal';
|
|
144
203
|
this.workspaceDir = options.workspaceDir || process.env.HOME || '/tmp';
|
|
204
|
+
this.userOrgMemories = new Map();
|
|
205
|
+
this.userOrgContextGraphs = new Map();
|
|
145
206
|
// Find claude binary - critical for worker delegation
|
|
146
207
|
this.claudePath = this.findClaudeBinary();
|
|
147
208
|
console.log(`[ORCHESTRATOR] Claude binary: ${this.claudePath}`);
|
|
209
|
+
console.log(`[ORCHESTRATOR] Instance mode: ${this.instanceMode}`);
|
|
148
210
|
// Initialize memory - use Postgres if DATABASE_URL is set, otherwise SQLite
|
|
149
211
|
if (isPostgresConfigured()) {
|
|
150
212
|
console.log('[ORCHESTRATOR] DATABASE_URL detected, using PostgreSQL + pgvector');
|
|
@@ -160,6 +222,44 @@ export class MasterOrchestrator {
|
|
|
160
222
|
voyageApiKey: options.voyageApiKey
|
|
161
223
|
});
|
|
162
224
|
}
|
|
225
|
+
this.contextGraph = new ContextGraph(this.memory, { scope: options.userId });
|
|
226
|
+
// Initialize org memory for team mode or when orgId is provided
|
|
227
|
+
if (this.orgId) {
|
|
228
|
+
if (isPostgresConfigured()) {
|
|
229
|
+
this.orgMemory = new PostgresMemoryStore({
|
|
230
|
+
connectionString: process.env.DATABASE_URL,
|
|
231
|
+
userId: `org:${this.orgId}`
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
this.orgMemory = new AdvancedMemoryStore({
|
|
236
|
+
dbPath: `memories_org_${this.orgId}.db`,
|
|
237
|
+
vectorPath: `./memory_vectors_org_${this.orgId}`,
|
|
238
|
+
voyageApiKey: options.voyageApiKey
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
this.orgContextGraph = new ContextGraph(this.orgMemory, { scope: `org:${this.orgId}` });
|
|
242
|
+
}
|
|
243
|
+
// For personal mode: initialize memory stores for all user's teams (for reading)
|
|
244
|
+
if (this.instanceMode === 'personal' && options.userOrgIds) {
|
|
245
|
+
for (const orgId of options.userOrgIds) {
|
|
246
|
+
if (isPostgresConfigured()) {
|
|
247
|
+
this.userOrgMemories.set(orgId, new PostgresMemoryStore({
|
|
248
|
+
connectionString: process.env.DATABASE_URL,
|
|
249
|
+
userId: `org:${orgId}`
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
this.userOrgMemories.set(orgId, new AdvancedMemoryStore({
|
|
254
|
+
dbPath: `memories_org_${orgId}.db`,
|
|
255
|
+
vectorPath: `./memory_vectors_org_${orgId}`,
|
|
256
|
+
voyageApiKey: options.voyageApiKey
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
this.userOrgContextGraphs.set(orgId, new ContextGraph(this.userOrgMemories.get(orgId), { scope: `org:${orgId}` }));
|
|
260
|
+
}
|
|
261
|
+
console.log(`[ORCHESTRATOR] Personal mode: initialized ${this.userOrgMemories.size} team memory stores for reading`);
|
|
262
|
+
}
|
|
163
263
|
this.scheduler = new TaskScheduler(`scheduled_${options.userId}.json`);
|
|
164
264
|
// Create indexer (will index on first initialize call)
|
|
165
265
|
this.indexer = new SystemIndexer(this.memory);
|
|
@@ -167,7 +267,19 @@ export class MasterOrchestrator {
|
|
|
167
267
|
this.bashTool = new BashTool(this.workspaceDir);
|
|
168
268
|
this.webTools = new WebTools();
|
|
169
269
|
this.workerTools = new WorkerTools(this.workspaceDir);
|
|
270
|
+
this.calendarTools = new CalendarTools();
|
|
170
271
|
this.sharedState = new SharedState(this.workspaceDir);
|
|
272
|
+
this.workerTools.setLogCallback((workerId, line, stream) => {
|
|
273
|
+
if (this.workerLogCallback) {
|
|
274
|
+
this.workerLogCallback(workerId, line, stream);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
// Initialize Lia's internal task queue
|
|
278
|
+
this.taskQueue = new LiaTaskQueue({
|
|
279
|
+
onTaskUpdate: (task) => this.taskUpdateCallback?.(task),
|
|
280
|
+
onPlanUpdate: (plan) => this.planUpdateCallback?.(plan),
|
|
281
|
+
processTask: async (task) => this.processQueuedTask(task),
|
|
282
|
+
});
|
|
171
283
|
}
|
|
172
284
|
/**
|
|
173
285
|
* Initialize the orchestrator - indexes filesystem on first call
|
|
@@ -208,6 +320,12 @@ export class MasterOrchestrator {
|
|
|
208
320
|
}
|
|
209
321
|
}
|
|
210
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Set callback for worker log streaming
|
|
325
|
+
*/
|
|
326
|
+
setWorkerLogCallback(callback) {
|
|
327
|
+
this.workerLogCallback = callback;
|
|
328
|
+
}
|
|
211
329
|
/**
|
|
212
330
|
* Set callback for gallery updates (worker assets for UI gallery view)
|
|
213
331
|
*/
|
|
@@ -219,6 +337,75 @@ export class MasterOrchestrator {
|
|
|
219
337
|
this.broadcastGalleryUpdate();
|
|
220
338
|
} : null);
|
|
221
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Set callback for task queue updates (for real-time UI updates)
|
|
342
|
+
*/
|
|
343
|
+
setTaskUpdateCallback(callback) {
|
|
344
|
+
this.taskUpdateCallback = callback || undefined;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Set callback for plan updates (for real-time UI updates)
|
|
348
|
+
*/
|
|
349
|
+
setPlanUpdateCallback(callback) {
|
|
350
|
+
this.planUpdateCallback = callback || undefined;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Queue a message for async processing (non-blocking)
|
|
354
|
+
* Returns immediately with task ID - use callbacks or getTaskQueueStatus for results
|
|
355
|
+
*/
|
|
356
|
+
queueMessage(message, options) {
|
|
357
|
+
return this.taskQueue.addTask({
|
|
358
|
+
type: 'user_message',
|
|
359
|
+
content: message,
|
|
360
|
+
priority: options?.priority || 'normal',
|
|
361
|
+
metadata: {
|
|
362
|
+
userId: this.userId,
|
|
363
|
+
messageId: options?.messageId,
|
|
364
|
+
apiKey: options?.apiKey,
|
|
365
|
+
context: this.instanceMode,
|
|
366
|
+
orgId: this.orgId,
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Process a task from the queue (called by LiaTaskQueue)
|
|
372
|
+
*/
|
|
373
|
+
async processQueuedTask(task) {
|
|
374
|
+
// This is where the actual work happens
|
|
375
|
+
return this.processMessage(task.content, [], // Fresh conversation for each task
|
|
376
|
+
undefined, // No sendMessage callback for queued tasks
|
|
377
|
+
task.metadata?.apiKey);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Get task queue status (for UI)
|
|
381
|
+
*/
|
|
382
|
+
getTaskQueueStatus() {
|
|
383
|
+
return this.taskQueue.getStatus();
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Get pending tasks
|
|
387
|
+
*/
|
|
388
|
+
getPendingTasks() {
|
|
389
|
+
return this.taskQueue.getPendingTasks();
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Get task by ID
|
|
393
|
+
*/
|
|
394
|
+
getTask(taskId) {
|
|
395
|
+
return this.taskQueue.getTask(taskId);
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Cancel a pending task
|
|
399
|
+
*/
|
|
400
|
+
cancelTask(taskId) {
|
|
401
|
+
return this.taskQueue.cancelTask(taskId);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Get Lia's internal todo list formatted for context
|
|
405
|
+
*/
|
|
406
|
+
getTaskQueueContext() {
|
|
407
|
+
return this.taskQueue.formatAsTodoList();
|
|
408
|
+
}
|
|
222
409
|
/**
|
|
223
410
|
* Get gallery workers with their assets
|
|
224
411
|
*/
|
|
@@ -412,6 +599,35 @@ export class MasterOrchestrator {
|
|
|
412
599
|
}
|
|
413
600
|
return null;
|
|
414
601
|
}
|
|
602
|
+
extractAttachmentContext(message) {
|
|
603
|
+
const marker = '📎 Attached files:';
|
|
604
|
+
const index = message.indexOf(marker);
|
|
605
|
+
if (index === -1)
|
|
606
|
+
return null;
|
|
607
|
+
const raw = message.slice(index + marker.length);
|
|
608
|
+
const lines = raw
|
|
609
|
+
.split('\n')
|
|
610
|
+
.map(line => line.trim())
|
|
611
|
+
.filter(Boolean);
|
|
612
|
+
if (lines.length === 0)
|
|
613
|
+
return null;
|
|
614
|
+
return lines.join('\n');
|
|
615
|
+
}
|
|
616
|
+
withAttachments(task, context) {
|
|
617
|
+
if (!this.attachmentContext)
|
|
618
|
+
return { task, context };
|
|
619
|
+
const attachmentBlock = this.attachmentContext;
|
|
620
|
+
const hasAttachments = task.includes(attachmentBlock) ||
|
|
621
|
+
task.includes('Attached files') ||
|
|
622
|
+
(context && (context.includes(attachmentBlock) || context.includes('Attached files')));
|
|
623
|
+
if (hasAttachments)
|
|
624
|
+
return { task, context };
|
|
625
|
+
const attachmentNote = `Attached files (download these URLs and use their contents; do not ask the user to resend):\n${attachmentBlock}`;
|
|
626
|
+
if (context && context.trim().length > 0) {
|
|
627
|
+
return { task, context: `${context}\n\n${attachmentNote}` };
|
|
628
|
+
}
|
|
629
|
+
return { task: `${task}\n\n${attachmentNote}` };
|
|
630
|
+
}
|
|
415
631
|
/**
|
|
416
632
|
* Check if verbose mode is enabled
|
|
417
633
|
*/
|
|
@@ -428,9 +644,15 @@ export class MasterOrchestrator {
|
|
|
428
644
|
if (slashResponse) {
|
|
429
645
|
return slashResponse;
|
|
430
646
|
}
|
|
647
|
+
// Deterministic calendar/todo writes (do not rely on the model calling tools)
|
|
648
|
+
const quickWrite = this.tryHandleCalendarTodo(message);
|
|
649
|
+
if (quickWrite) {
|
|
650
|
+
return quickWrite;
|
|
651
|
+
}
|
|
431
652
|
// Load hub context (AGENTS.md identity, LANDMARKS.md state, project bio if relevant)
|
|
432
653
|
const hubContext = loadHubContext(message);
|
|
433
654
|
const hubContextStr = formatHubContext(hubContext);
|
|
655
|
+
this.attachmentContext = this.extractAttachmentContext(message);
|
|
434
656
|
// Build context from memory
|
|
435
657
|
const memoryContext = await this.getMemoryContext(message);
|
|
436
658
|
// Build system prompt with hub context, genesis knowledge, and memory context
|
|
@@ -467,6 +689,7 @@ ${hubContextStr}
|
|
|
467
689
|
const response = await this.runAgentLoop(messages, systemWithContext, sendMessage, apiKey);
|
|
468
690
|
// Auto-remember important things from the conversation
|
|
469
691
|
await this.autoRemember(message, response);
|
|
692
|
+
void this.autoCaptureContext(message);
|
|
470
693
|
// Log significant actions to hub
|
|
471
694
|
if (response.length > 100) {
|
|
472
695
|
const action = message.length > 50 ? message.slice(0, 50) + '...' : message;
|
|
@@ -478,16 +701,239 @@ ${hubContextStr}
|
|
|
478
701
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
479
702
|
return `Error: ${errorMessage}`;
|
|
480
703
|
}
|
|
704
|
+
finally {
|
|
705
|
+
this.attachmentContext = null;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
tryHandleCalendarTodo(message) {
|
|
709
|
+
const todoItems = this.extractTodoItems(message);
|
|
710
|
+
const calendarEvents = this.extractCalendarEvents(message);
|
|
711
|
+
if (todoItems.length === 0 && calendarEvents.length === 0)
|
|
712
|
+
return null;
|
|
713
|
+
const results = [];
|
|
714
|
+
if (todoItems.length > 0) {
|
|
715
|
+
const todoRes = this.calendarTools.upsertTodoItems(todoItems);
|
|
716
|
+
if (!todoRes.success) {
|
|
717
|
+
return `Failed to update /todo: ${todoRes.error || todoRes.output || 'unknown error'}`;
|
|
718
|
+
}
|
|
719
|
+
results.push(`Updated /todo with ${todoItems.length} item(s).`);
|
|
720
|
+
}
|
|
721
|
+
if (calendarEvents.length > 0) {
|
|
722
|
+
const calRes = this.calendarTools.upsertCalendarEvents(calendarEvents);
|
|
723
|
+
if (!calRes.success) {
|
|
724
|
+
return `Failed to update /cal: ${calRes.error || calRes.output || 'unknown error'}`;
|
|
725
|
+
}
|
|
726
|
+
results.push(`Updated /cal with ${calendarEvents.length} event(s).`);
|
|
727
|
+
}
|
|
728
|
+
return `${results.join(' ')} Open /todo or /cal to view.`;
|
|
729
|
+
}
|
|
730
|
+
extractTodoItems(message) {
|
|
731
|
+
const lower = message.toLowerCase();
|
|
732
|
+
const hasTodoIntent = (/(\s|^)\/todo\b/i.test(message) || /\bto\s+my\s+todo\b/i.test(message) || /\bto\s+my\s+\/todo\b/i.test(message)) &&
|
|
733
|
+
/\b(add|put|save|store|track)\b/i.test(message);
|
|
734
|
+
if (!hasTodoIntent)
|
|
735
|
+
return [];
|
|
736
|
+
const items = [];
|
|
737
|
+
const wantSteps = /\b(step\s*[- ]?by\s*[- ]?step|break\s*down|smaller\s*steps|parse\s*it)\b/i.test(message);
|
|
738
|
+
const hasRag = /\b(rag|retrieval augmented|retrieval-augmented|embedding|embeddings|pierce)\b/i.test(message);
|
|
739
|
+
const hasWhenIsGood = /\b(whenisgood|when\s+is\s+good|availability|unavailable|schedule|scheduling|lab\s+meeting)\b/i.test(message);
|
|
740
|
+
if (wantSteps && hasRag) {
|
|
741
|
+
items.push({ title: '[RAG] Finish embedding Pierce 2025', priority: 'high' }, { title: '[RAG] Ingest/open-source genetics book content', priority: 'high' }, { title: '[RAG] Chunk + embed genetics book (verify coverage)', priority: 'high' }, { title: '[RAG] Build/refresh retrieval index + metadata', priority: 'medium' }, { title: '[RAG] Smoke test RAG queries + failure cases', priority: 'medium' }, { title: '[RAG] Create student access + usage instructions', priority: 'medium' }, { title: '[RAG] Grant access to students + announce', priority: 'medium' });
|
|
742
|
+
}
|
|
743
|
+
if (wantSteps && hasWhenIsGood) {
|
|
744
|
+
items.push({ title: '[Meetings] Review WhenIsGood results (unavailability)', priority: 'high' }, { title: '[Meetings] Build availability matrix per student', priority: 'high' }, { title: '[Meetings] Identify 2–3 candidate lab meeting slots', priority: 'medium' }, { title: '[Meetings] Decide lab meeting time + confirm with group', priority: 'medium' }, { title: '[Meetings] Pick individual meeting times for each student', priority: 'medium' }, { title: '[Meetings] Send invites/confirmations', priority: 'medium' }, { title: '[Meetings] Add meetings to /cal', priority: 'medium' });
|
|
745
|
+
}
|
|
746
|
+
if (!wantSteps || (items.length === 0 && (hasRag || hasWhenIsGood))) {
|
|
747
|
+
const raw = message.trim().slice(0, 240);
|
|
748
|
+
items.push({
|
|
749
|
+
title: `[Todo] ${raw}${message.trim().length > 240 ? '…' : ''}`,
|
|
750
|
+
priority: 'medium',
|
|
751
|
+
notes: message.trim()
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
return items;
|
|
755
|
+
}
|
|
756
|
+
extractCalendarEvents(message) {
|
|
757
|
+
const hasCalendarIntent = /\bto\s+my\s+cal\b/i.test(message) ||
|
|
758
|
+
/\bto\s+my\s+calendar\b/i.test(message) ||
|
|
759
|
+
/(\s|^)\/cal\b/i.test(message) ||
|
|
760
|
+
/(\s|^)\/calendar\b/i.test(message);
|
|
761
|
+
const hasAddIntent = /\b(add|put|schedule|book)\b/i.test(message);
|
|
762
|
+
if (!hasAddIntent)
|
|
763
|
+
return [];
|
|
764
|
+
const candidates = message
|
|
765
|
+
.split(/\n\s*\n/)
|
|
766
|
+
.map((chunk) => chunk.trim())
|
|
767
|
+
.filter(Boolean);
|
|
768
|
+
const events = [];
|
|
769
|
+
for (const chunk of candidates) {
|
|
770
|
+
const parsed = this.parseCalendarEvent(chunk, hasCalendarIntent);
|
|
771
|
+
if (parsed)
|
|
772
|
+
events.push(parsed);
|
|
773
|
+
}
|
|
774
|
+
return events;
|
|
775
|
+
}
|
|
776
|
+
parseCalendarEvent(message, hasCalendarIntent) {
|
|
777
|
+
const explicit = this.parseExplicitDateTimeEvent(message, hasCalendarIntent);
|
|
778
|
+
if (explicit)
|
|
779
|
+
return explicit;
|
|
780
|
+
const weekdayMatch = message.match(/\b(mon(?:day)?|tue(?:sday)?|wed(?:nesday)?|thu(?:rsday)?|fri(?:day)?|sat(?:urday)?|sun(?:day)?)\b/i);
|
|
781
|
+
const timeMatch = message.match(/\b(\d{1,2})(?::(\d{2}))?\s*(a|am|p|pm)\s*-\s*(\d{1,2})(?::(\d{2}))?\s*(a|am|p|pm)\b/i);
|
|
782
|
+
if (!weekdayMatch || !timeMatch)
|
|
783
|
+
return null;
|
|
784
|
+
const weekday = weekdayMatch[1].slice(0, 3).toLowerCase();
|
|
785
|
+
const weekdayIndex = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 }[weekday];
|
|
786
|
+
const startTime = this.to24h(timeMatch[1], timeMatch[2], timeMatch[3]);
|
|
787
|
+
const endTime = this.to24h(timeMatch[4], timeMatch[5], timeMatch[6]);
|
|
788
|
+
const startDate = this.nextOccurrence(weekdayIndex, startTime);
|
|
789
|
+
const dateKey = this.toDateKey(startDate);
|
|
790
|
+
const title = message
|
|
791
|
+
.replace(/^\s*add\s+/i, '')
|
|
792
|
+
.replace(/\s+to\s+\b(mon(?:day)?|tue(?:sday)?|wed(?:nesday)?|thu(?:rsday)?|fri(?:day)?|sat(?:urday)?|sun(?:day)?)\b[\s\S]*$/i, '')
|
|
793
|
+
.trim();
|
|
794
|
+
if (!title)
|
|
795
|
+
return null;
|
|
796
|
+
return {
|
|
797
|
+
title,
|
|
798
|
+
date: dateKey,
|
|
799
|
+
startTime,
|
|
800
|
+
endTime,
|
|
801
|
+
calendarId: 'primary'
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
parseExplicitDateTimeEvent(message, hasCalendarIntent) {
|
|
805
|
+
if (!hasCalendarIntent)
|
|
806
|
+
return null;
|
|
807
|
+
// e.g. "Tuesday, January 13, 3:30 pm, A211 PDB—TITLE, \"Talk...\" ... Host: ..."
|
|
808
|
+
const monthMatch = message.match(/\b(jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\b/i);
|
|
809
|
+
if (!monthMatch)
|
|
810
|
+
return null;
|
|
811
|
+
const monthText = monthMatch[1].slice(0, 3).toLowerCase();
|
|
812
|
+
const monthIndex = { jan: 0, feb: 1, mar: 2, apr: 3, may: 4, jun: 5, jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11 }[monthText];
|
|
813
|
+
const dayMatch = message.match(new RegExp(`${monthMatch[1]}\\s+(\\d{1,2})`, 'i'));
|
|
814
|
+
const timeMatch = message.match(/\b(\d{1,2})(?::(\d{2}))?\s*(a|am|p|pm)\b/i);
|
|
815
|
+
if (!dayMatch || !timeMatch)
|
|
816
|
+
return null;
|
|
817
|
+
const dayOfMonth = Number(dayMatch[1]);
|
|
818
|
+
const startTime = this.to24h(timeMatch[1], timeMatch[2], timeMatch[3]);
|
|
819
|
+
const yearMatch = message.match(/\b(20\d{2})\b/);
|
|
820
|
+
const now = new Date();
|
|
821
|
+
const year = yearMatch ? Number(yearMatch[1]) : now.getFullYear();
|
|
822
|
+
const date = new Date(year, monthIndex, dayOfMonth, 0, 0, 0, 0);
|
|
823
|
+
const dateKey = this.toDateKey(date);
|
|
824
|
+
const cleaned = message
|
|
825
|
+
.replace(/^\s*add\s+(these\s+)?to\s+my\s+(cal|calendar)\b[:\s-]*/i, '')
|
|
826
|
+
.trim();
|
|
827
|
+
const parts = cleaned.split(/—|--|–/);
|
|
828
|
+
const head = parts[0] || cleaned;
|
|
829
|
+
const after = parts.slice(1).join('—').trim();
|
|
830
|
+
// Try to extract a succinct title from the segment after the location delimiter, otherwise use head.
|
|
831
|
+
const titleCandidate = after ? after : head;
|
|
832
|
+
const title = titleCandidate
|
|
833
|
+
.replace(/^[^A-Za-z0-9]*\b(mon(?:day)?|tue(?:sday)?|wed(?:nesday)?|thu(?:rsday)?|fri(?:day)?|sat(?:urday)?|sun(?:day)?)\b[, ]*/i, '')
|
|
834
|
+
.replace(/\b(jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\b\s+\d{1,2}(?:,\s*\d{4})?/i, '')
|
|
835
|
+
.replace(/\b\d{1,2}(?::\d{2})?\s*(a|am|p|pm)\b/i, '')
|
|
836
|
+
.replace(/^[,\s]+|[,\s]+$/g, '')
|
|
837
|
+
.slice(0, 160);
|
|
838
|
+
if (!title)
|
|
839
|
+
return null;
|
|
840
|
+
const notes = cleaned.length > title.length ? cleaned : undefined;
|
|
841
|
+
return {
|
|
842
|
+
title,
|
|
843
|
+
date: dateKey,
|
|
844
|
+
startTime,
|
|
845
|
+
calendarId: 'primary',
|
|
846
|
+
notes
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
to24h(hourText, minuteText, meridiemText) {
|
|
850
|
+
const hour = Number(hourText);
|
|
851
|
+
const minute = Number(minuteText || '0');
|
|
852
|
+
const meridiem = meridiemText.toLowerCase();
|
|
853
|
+
let h = hour % 12;
|
|
854
|
+
if (meridiem.startsWith('p'))
|
|
855
|
+
h += 12;
|
|
856
|
+
return `${String(h).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
|
|
857
|
+
}
|
|
858
|
+
nextOccurrence(weekdayIndex, startTime) {
|
|
859
|
+
const now = new Date();
|
|
860
|
+
const candidate = new Date(now);
|
|
861
|
+
candidate.setHours(0, 0, 0, 0);
|
|
862
|
+
const delta = (weekdayIndex - candidate.getDay() + 7) % 7;
|
|
863
|
+
candidate.setDate(candidate.getDate() + delta);
|
|
864
|
+
if (startTime) {
|
|
865
|
+
const [h, m] = startTime.split(':').map(Number);
|
|
866
|
+
candidate.setHours(h, m, 0, 0);
|
|
867
|
+
if (candidate.getTime() <= now.getTime()) {
|
|
868
|
+
candidate.setDate(candidate.getDate() + 7);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
return candidate;
|
|
872
|
+
}
|
|
873
|
+
toDateKey(date) {
|
|
874
|
+
const year = date.getFullYear();
|
|
875
|
+
const month = `${date.getMonth() + 1}`.padStart(2, '0');
|
|
876
|
+
const day = `${date.getDate()}`.padStart(2, '0');
|
|
877
|
+
return `${year}-${month}-${day}`;
|
|
481
878
|
}
|
|
482
879
|
/**
|
|
483
880
|
* Get relevant memory context for a message
|
|
881
|
+
* Routes based on instanceMode:
|
|
882
|
+
* - personal: search personal memories + all team memories
|
|
883
|
+
* - team: search ONLY team memory (never personal - team Lia is isolated)
|
|
484
884
|
*/
|
|
485
885
|
async getMemoryContext(message) {
|
|
486
886
|
try {
|
|
887
|
+
const sections = [];
|
|
888
|
+
// Team mode: ONLY search team memory - never personal
|
|
889
|
+
if (this.instanceMode === 'team') {
|
|
890
|
+
if (!this.orgMemory) {
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
const teamMemories = await this.orgMemory.search(message, { limit: 10 });
|
|
894
|
+
const teamMemoryLines = teamMemories.map(m => `[team:${m.type}] ${m.content}`).join('\n');
|
|
895
|
+
const teamContextSummary = this.orgContextGraph
|
|
896
|
+
? await this.orgContextGraph.getContextSummary(message, { limit: 5 })
|
|
897
|
+
: null;
|
|
898
|
+
if (teamMemoryLines)
|
|
899
|
+
sections.push(`TEAM MEMORIES:\n${teamMemoryLines}`);
|
|
900
|
+
if (teamContextSummary)
|
|
901
|
+
sections.push(`TEAM CONTEXT:\n${teamContextSummary}`);
|
|
902
|
+
if (sections.length === 0)
|
|
903
|
+
return null;
|
|
904
|
+
return sections.join('\n\n');
|
|
905
|
+
}
|
|
906
|
+
// Personal mode: search personal + all team memories
|
|
487
907
|
const memories = await this.memory.search(message, { limit: 5 });
|
|
488
|
-
|
|
908
|
+
const memoryLines = memories.map(m => `[${m.type}] ${m.content}`).join('\n');
|
|
909
|
+
const contextSummary = await this.contextGraph.getContextSummary(message, { limit: 3 });
|
|
910
|
+
if (memoryLines)
|
|
911
|
+
sections.push(`YOUR MEMORIES:\n${memoryLines}`);
|
|
912
|
+
if (contextSummary)
|
|
913
|
+
sections.push(`YOUR CONTEXT:\n${contextSummary}`);
|
|
914
|
+
// In personal mode, also include all team memories for reading
|
|
915
|
+
for (const [orgId, orgMem] of this.userOrgMemories) {
|
|
916
|
+
try {
|
|
917
|
+
const teamMems = await orgMem.search(message, { limit: 3 });
|
|
918
|
+
if (teamMems.length > 0) {
|
|
919
|
+
const teamLines = teamMems.map(m => `[${m.type}] ${m.content}`).join('\n');
|
|
920
|
+
sections.push(`TEAM (${orgId}) MEMORIES:\n${teamLines}`);
|
|
921
|
+
}
|
|
922
|
+
const teamGraph = this.userOrgContextGraphs.get(orgId);
|
|
923
|
+
if (teamGraph) {
|
|
924
|
+
const teamContext = await teamGraph.getContextSummary(message, { limit: 2 });
|
|
925
|
+
if (teamContext) {
|
|
926
|
+
sections.push(`TEAM (${orgId}) CONTEXT:\n${teamContext}`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
catch {
|
|
931
|
+
// Skip failed team memory access
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
if (sections.length === 0)
|
|
489
935
|
return null;
|
|
490
|
-
return
|
|
936
|
+
return sections.join('\n\n');
|
|
491
937
|
}
|
|
492
938
|
catch {
|
|
493
939
|
return null;
|
|
@@ -495,6 +941,9 @@ ${hubContextStr}
|
|
|
495
941
|
}
|
|
496
942
|
/**
|
|
497
943
|
* Auto-remember important information from conversations
|
|
944
|
+
* Routes based on instanceMode:
|
|
945
|
+
* - personal: write to personal memory
|
|
946
|
+
* - team: write to team memory
|
|
498
947
|
*/
|
|
499
948
|
async autoRemember(userMessage, response) {
|
|
500
949
|
// Look for preference indicators
|
|
@@ -502,16 +951,140 @@ ${hubContextStr}
|
|
|
502
951
|
const lower = userMessage.toLowerCase();
|
|
503
952
|
for (const indicator of prefIndicators) {
|
|
504
953
|
if (lower.includes(indicator)) {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
954
|
+
// Route to correct memory store based on instanceMode
|
|
955
|
+
const targetMemory = this.instanceMode === 'team' ? this.orgMemory : this.memory;
|
|
956
|
+
if (targetMemory) {
|
|
957
|
+
await targetMemory.remember(userMessage, {
|
|
958
|
+
type: 'semantic',
|
|
959
|
+
source: 'auto-extract',
|
|
960
|
+
importance: 0.7,
|
|
961
|
+
tags: ['preference', this.instanceMode === 'team' ? 'team' : 'user']
|
|
962
|
+
});
|
|
963
|
+
}
|
|
511
964
|
break;
|
|
512
965
|
}
|
|
513
966
|
}
|
|
514
967
|
}
|
|
968
|
+
async autoCaptureContext(userMessage) {
|
|
969
|
+
const marker = '📎 Attached files:';
|
|
970
|
+
const base = userMessage.split(marker)[0]?.trim() || '';
|
|
971
|
+
if (!base || base.startsWith('/'))
|
|
972
|
+
return;
|
|
973
|
+
const text = base.replace(/\s+/g, ' ').trim();
|
|
974
|
+
const shareToOrg = !!this.orgContextGraph &&
|
|
975
|
+
/\b(share with team|team files|shared with team|team-wide|org-wide|company-wide)\b/i.test(text);
|
|
976
|
+
const graphs = shareToOrg && this.orgContextGraph
|
|
977
|
+
? [this.contextGraph, this.orgContextGraph]
|
|
978
|
+
: [this.contextGraph];
|
|
979
|
+
const seenEntities = new Set();
|
|
980
|
+
const tasks = [];
|
|
981
|
+
const rememberEntity = (kind, name, attributes) => {
|
|
982
|
+
const cleaned = this.normalizeEntityName(name);
|
|
983
|
+
if (!cleaned || this.isIgnoredEntity(cleaned))
|
|
984
|
+
return;
|
|
985
|
+
for (const graph of graphs) {
|
|
986
|
+
const scopeLabel = graph === this.orgContextGraph ? 'org' : 'user';
|
|
987
|
+
const key = `${scopeLabel}:${kind}:${cleaned.toLowerCase()}`;
|
|
988
|
+
if (seenEntities.has(key))
|
|
989
|
+
continue;
|
|
990
|
+
seenEntities.add(key);
|
|
991
|
+
tasks.push(graph.upsertEntity({
|
|
992
|
+
kind,
|
|
993
|
+
name: cleaned,
|
|
994
|
+
attributes,
|
|
995
|
+
tags: ['ctx:auto']
|
|
996
|
+
}));
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
const rememberRelation = (from, to, type, fromKind, toKind) => {
|
|
1000
|
+
const fromClean = this.normalizeEntityName(from);
|
|
1001
|
+
const toClean = this.normalizeEntityName(to);
|
|
1002
|
+
if (!fromClean || !toClean || this.isIgnoredEntity(fromClean) || this.isIgnoredEntity(toClean))
|
|
1003
|
+
return;
|
|
1004
|
+
for (const graph of graphs) {
|
|
1005
|
+
tasks.push(graph.linkEntities({
|
|
1006
|
+
from: fromClean,
|
|
1007
|
+
to: toClean,
|
|
1008
|
+
type,
|
|
1009
|
+
fromKind,
|
|
1010
|
+
toKind,
|
|
1011
|
+
source: 'auto-capture'
|
|
1012
|
+
}));
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
const emailMatches = text.match(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi) || [];
|
|
1016
|
+
for (const email of emailMatches.slice(0, 5)) {
|
|
1017
|
+
rememberEntity('person', email, { email });
|
|
1018
|
+
}
|
|
1019
|
+
for (const match of text.matchAll(/\bproject\s+([A-Za-z0-9][\w .'-]{1,60})/gi)) {
|
|
1020
|
+
rememberEntity('project', match[1]);
|
|
1021
|
+
}
|
|
1022
|
+
for (const match of text.matchAll(/\bteam\s+([A-Za-z0-9][\w .'-]{1,50})/gi)) {
|
|
1023
|
+
rememberEntity('team', match[1]);
|
|
1024
|
+
}
|
|
1025
|
+
for (const match of text.matchAll(/\b([A-Za-z0-9][\w .'-]{1,50})\s+team\b/gi)) {
|
|
1026
|
+
rememberEntity('team', match[1]);
|
|
1027
|
+
}
|
|
1028
|
+
for (const match of text.matchAll(/\b(company|org|organization)\s+([A-Za-z0-9][\w .'-]{1,60})/gi)) {
|
|
1029
|
+
rememberEntity('org', match[2]);
|
|
1030
|
+
}
|
|
1031
|
+
for (const match of text.matchAll(/\b(tool|platform|stack|using)\s+([A-Za-z0-9][\w .'-]{1,40})/gi)) {
|
|
1032
|
+
rememberEntity('tool', match[2]);
|
|
1033
|
+
}
|
|
1034
|
+
for (const match of text.matchAll(/([A-Za-z][\w .'-]{1,50})\s+(works on|working on)\s+(?:the\s+)?project\s+([A-Za-z0-9][\w .'-]{1,60})/gi)) {
|
|
1035
|
+
rememberRelation(match[1], match[3], 'works_on', 'person', 'project');
|
|
1036
|
+
}
|
|
1037
|
+
for (const match of text.matchAll(/([A-Za-z][\w .'-]{1,50})\s+owns\s+(?:the\s+)?project\s+([A-Za-z0-9][\w .'-]{1,60})/gi)) {
|
|
1038
|
+
rememberRelation(match[1], match[2], 'owns', 'person', 'project');
|
|
1039
|
+
}
|
|
1040
|
+
for (const match of text.matchAll(/([A-Za-z][\w .'-]{1,50})\s+leads\s+(?:the\s+)?([A-Za-z0-9][\w .'-]{1,50})\s+team/gi)) {
|
|
1041
|
+
rememberRelation(match[1], match[2], 'leads', 'person', 'team');
|
|
1042
|
+
}
|
|
1043
|
+
for (const match of text.matchAll(/([A-Za-z][\w .'-]{1,50})\s+is\s+on\s+the\s+([A-Za-z0-9][\w .'-]{1,50})\s+team/gi)) {
|
|
1044
|
+
rememberRelation(match[1], match[2], 'member_of', 'person', 'team');
|
|
1045
|
+
}
|
|
1046
|
+
for (const match of text.matchAll(/project\s+([A-Za-z0-9][\w .'-]{1,60})\s+depends on\s+project\s+([A-Za-z0-9][\w .'-]{1,60})/gi)) {
|
|
1047
|
+
rememberRelation(match[1], match[2], 'depends_on', 'project', 'project');
|
|
1048
|
+
}
|
|
1049
|
+
for (const match of text.matchAll(/([A-Za-z][\w .'-]{1,50})\s+reports to\s+([A-Za-z0-9][\w .'-]{1,50})/gi)) {
|
|
1050
|
+
rememberRelation(match[1], match[2], 'reports_to', 'person', 'person');
|
|
1051
|
+
}
|
|
1052
|
+
for (const match of text.matchAll(/([A-Za-z][\w .'-]{1,50})\s+uses\s+([A-Za-z0-9][\w .'-]{1,40})/gi)) {
|
|
1053
|
+
const subject = match[1];
|
|
1054
|
+
const tool = match[2];
|
|
1055
|
+
const lowered = subject.toLowerCase();
|
|
1056
|
+
let subjectKind = 'person';
|
|
1057
|
+
let subjectName = subject;
|
|
1058
|
+
if (lowered.includes('team')) {
|
|
1059
|
+
subjectKind = 'team';
|
|
1060
|
+
subjectName = subject.replace(/team/gi, '').trim();
|
|
1061
|
+
}
|
|
1062
|
+
else if (lowered.includes('project')) {
|
|
1063
|
+
subjectKind = 'project';
|
|
1064
|
+
subjectName = subject.replace(/project/gi, '').trim();
|
|
1065
|
+
}
|
|
1066
|
+
rememberEntity('tool', tool);
|
|
1067
|
+
rememberRelation(subjectName, tool, 'uses', subjectKind, 'tool');
|
|
1068
|
+
}
|
|
1069
|
+
if (tasks.length > 0) {
|
|
1070
|
+
try {
|
|
1071
|
+
await Promise.all(tasks);
|
|
1072
|
+
}
|
|
1073
|
+
catch {
|
|
1074
|
+
// Best-effort; context capture should never block responses.
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
normalizeEntityName(name) {
|
|
1079
|
+
return name.replace(/["'`]/g, '').replace(/\s+/g, ' ').trim();
|
|
1080
|
+
}
|
|
1081
|
+
isIgnoredEntity(name) {
|
|
1082
|
+
if (!name || name.length < 2 || name.length > 80)
|
|
1083
|
+
return true;
|
|
1084
|
+
const lower = name.toLowerCase();
|
|
1085
|
+
const ignored = new Set(['i', 'we', 'you', 'they', 'he', 'she', 'it', 'our', 'my', 'your', 'their']);
|
|
1086
|
+
return ignored.has(lower);
|
|
1087
|
+
}
|
|
515
1088
|
/**
|
|
516
1089
|
* Run the agentic loop
|
|
517
1090
|
* @param apiKey Optional per-request API key (creates temporary client)
|
|
@@ -521,17 +1094,28 @@ ${hubContextStr}
|
|
|
521
1094
|
let currentMessages = [...messages];
|
|
522
1095
|
let iterations = 0;
|
|
523
1096
|
const maxIterations = 30; // Increased for complex multi-tool tasks
|
|
1097
|
+
const requestTimeoutMs = 60000;
|
|
524
1098
|
// Use per-request client if apiKey provided, otherwise use default
|
|
525
1099
|
const client = apiKey ? new Anthropic({ apiKey }) : this.client;
|
|
526
1100
|
while (iterations < maxIterations) {
|
|
527
1101
|
iterations++;
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
1102
|
+
const requestStart = Date.now();
|
|
1103
|
+
console.log(`[ORCHESTRATOR] Anthropic request ${iterations}/${maxIterations} (model: ${this.model})`);
|
|
1104
|
+
const response = await Promise.race([
|
|
1105
|
+
client.messages.create({
|
|
1106
|
+
model: this.model,
|
|
1107
|
+
max_tokens: this.maxTokens,
|
|
1108
|
+
system,
|
|
1109
|
+
tools,
|
|
1110
|
+
messages: currentMessages
|
|
1111
|
+
}),
|
|
1112
|
+
new Promise((_, reject) => {
|
|
1113
|
+
setTimeout(() => {
|
|
1114
|
+
reject(new Error(`Anthropic request timed out after ${requestTimeoutMs}ms`));
|
|
1115
|
+
}, requestTimeoutMs);
|
|
1116
|
+
})
|
|
1117
|
+
]);
|
|
1118
|
+
console.log(`[ORCHESTRATOR] Anthropic response ${iterations}/${maxIterations} in ${Date.now() - requestStart}ms`);
|
|
535
1119
|
// Check if done
|
|
536
1120
|
if (response.stop_reason === 'end_turn' || !this.hasToolUse(response.content)) {
|
|
537
1121
|
return this.extractText(response.content);
|
|
@@ -576,6 +1160,87 @@ For ANY file creation, editing, or system modification, use delegate_to_worker i
|
|
|
576
1160
|
required: ['command']
|
|
577
1161
|
}
|
|
578
1162
|
},
|
|
1163
|
+
{
|
|
1164
|
+
name: 'calendar_upsert_events',
|
|
1165
|
+
description: `Create or update calendar events for /cal.
|
|
1166
|
+
|
|
1167
|
+
Writes ONLY to ~/Lia-Hub/shared/outputs/.lia/calendar.json (creates the directory/file if missing).`,
|
|
1168
|
+
input_schema: {
|
|
1169
|
+
type: 'object',
|
|
1170
|
+
properties: {
|
|
1171
|
+
events: {
|
|
1172
|
+
type: 'array',
|
|
1173
|
+
items: {
|
|
1174
|
+
type: 'object',
|
|
1175
|
+
properties: {
|
|
1176
|
+
id: { type: 'string' },
|
|
1177
|
+
title: { type: 'string' },
|
|
1178
|
+
date: { type: 'string', description: 'YYYY-MM-DD' },
|
|
1179
|
+
startTime: { type: 'string', description: 'HH:MM (24h) or ISO timestamp' },
|
|
1180
|
+
endTime: { type: 'string', description: 'HH:MM (24h) or ISO timestamp' },
|
|
1181
|
+
calendarId: { type: 'string' },
|
|
1182
|
+
notes: { type: 'string' }
|
|
1183
|
+
},
|
|
1184
|
+
required: ['title']
|
|
1185
|
+
}
|
|
1186
|
+
},
|
|
1187
|
+
calendars: {
|
|
1188
|
+
type: 'array',
|
|
1189
|
+
items: {
|
|
1190
|
+
type: 'object',
|
|
1191
|
+
properties: {
|
|
1192
|
+
id: { type: 'string' },
|
|
1193
|
+
name: { type: 'string' },
|
|
1194
|
+
color: { type: 'string' }
|
|
1195
|
+
},
|
|
1196
|
+
required: ['id', 'name']
|
|
1197
|
+
},
|
|
1198
|
+
description: 'Optional calendar definitions; omitted to keep existing.'
|
|
1199
|
+
}
|
|
1200
|
+
},
|
|
1201
|
+
required: ['events']
|
|
1202
|
+
}
|
|
1203
|
+
},
|
|
1204
|
+
{
|
|
1205
|
+
name: 'todo_upsert_items',
|
|
1206
|
+
description: `Create or update todo items for /todo.
|
|
1207
|
+
|
|
1208
|
+
Writes ONLY to ~/Lia-Hub/shared/outputs/.lia/todos.json (creates the directory/file if missing).`,
|
|
1209
|
+
input_schema: {
|
|
1210
|
+
type: 'object',
|
|
1211
|
+
properties: {
|
|
1212
|
+
items: {
|
|
1213
|
+
type: 'array',
|
|
1214
|
+
items: {
|
|
1215
|
+
type: 'object',
|
|
1216
|
+
properties: {
|
|
1217
|
+
id: { type: 'string' },
|
|
1218
|
+
title: { type: 'string' },
|
|
1219
|
+
done: { type: 'boolean' },
|
|
1220
|
+
due: { type: 'string' },
|
|
1221
|
+
priority: { type: 'string', enum: ['low', 'medium', 'high'] },
|
|
1222
|
+
notes: { type: 'string' },
|
|
1223
|
+
subtasks: {
|
|
1224
|
+
type: 'array',
|
|
1225
|
+
items: {
|
|
1226
|
+
type: 'object',
|
|
1227
|
+
properties: {
|
|
1228
|
+
id: { type: 'string' },
|
|
1229
|
+
title: { type: 'string' },
|
|
1230
|
+
done: { type: 'boolean' },
|
|
1231
|
+
notes: { type: 'string' }
|
|
1232
|
+
},
|
|
1233
|
+
required: ['title']
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
},
|
|
1237
|
+
required: ['title']
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
},
|
|
1241
|
+
required: ['items']
|
|
1242
|
+
}
|
|
1243
|
+
},
|
|
579
1244
|
// Web tools
|
|
580
1245
|
{
|
|
581
1246
|
name: 'web_search',
|
|
@@ -759,6 +1424,110 @@ Be specific about what you want done.`,
|
|
|
759
1424
|
required: ['content']
|
|
760
1425
|
}
|
|
761
1426
|
},
|
|
1427
|
+
{
|
|
1428
|
+
name: 'remember_entity',
|
|
1429
|
+
description: 'Store or update a team context entity (person, team, project, tool, task, doc).',
|
|
1430
|
+
input_schema: {
|
|
1431
|
+
type: 'object',
|
|
1432
|
+
properties: {
|
|
1433
|
+
kind: {
|
|
1434
|
+
type: 'string',
|
|
1435
|
+
enum: ContextGraphKinds.entities,
|
|
1436
|
+
description: 'Entity kind'
|
|
1437
|
+
},
|
|
1438
|
+
name: {
|
|
1439
|
+
type: 'string',
|
|
1440
|
+
description: 'Entity name'
|
|
1441
|
+
},
|
|
1442
|
+
description: {
|
|
1443
|
+
type: 'string',
|
|
1444
|
+
description: 'Short description or role'
|
|
1445
|
+
},
|
|
1446
|
+
attributes: {
|
|
1447
|
+
type: 'object',
|
|
1448
|
+
description: 'Optional key/value attributes',
|
|
1449
|
+
additionalProperties: { type: 'string' }
|
|
1450
|
+
},
|
|
1451
|
+
tags: {
|
|
1452
|
+
type: 'array',
|
|
1453
|
+
items: { type: 'string' },
|
|
1454
|
+
description: 'Optional tags'
|
|
1455
|
+
},
|
|
1456
|
+
scope: {
|
|
1457
|
+
type: 'string',
|
|
1458
|
+
enum: ['user', 'org'],
|
|
1459
|
+
description: 'Where to store the entity (default: user)'
|
|
1460
|
+
}
|
|
1461
|
+
},
|
|
1462
|
+
required: ['kind', 'name']
|
|
1463
|
+
}
|
|
1464
|
+
},
|
|
1465
|
+
{
|
|
1466
|
+
name: 'remember_relation',
|
|
1467
|
+
description: 'Link two entities with a relationship (depends_on, works_on, member_of, etc).',
|
|
1468
|
+
input_schema: {
|
|
1469
|
+
type: 'object',
|
|
1470
|
+
properties: {
|
|
1471
|
+
from: {
|
|
1472
|
+
type: 'string',
|
|
1473
|
+
description: 'Entity name or key'
|
|
1474
|
+
},
|
|
1475
|
+
to: {
|
|
1476
|
+
type: 'string',
|
|
1477
|
+
description: 'Entity name or key'
|
|
1478
|
+
},
|
|
1479
|
+
type: {
|
|
1480
|
+
type: 'string',
|
|
1481
|
+
enum: ContextGraphKinds.relations,
|
|
1482
|
+
description: 'Relationship type'
|
|
1483
|
+
},
|
|
1484
|
+
from_kind: {
|
|
1485
|
+
type: 'string',
|
|
1486
|
+
enum: ContextGraphKinds.entities,
|
|
1487
|
+
description: 'Optional kind hint for from'
|
|
1488
|
+
},
|
|
1489
|
+
to_kind: {
|
|
1490
|
+
type: 'string',
|
|
1491
|
+
enum: ContextGraphKinds.entities,
|
|
1492
|
+
description: 'Optional kind hint for to'
|
|
1493
|
+
},
|
|
1494
|
+
description: {
|
|
1495
|
+
type: 'string',
|
|
1496
|
+
description: 'Optional relation detail'
|
|
1497
|
+
},
|
|
1498
|
+
scope: {
|
|
1499
|
+
type: 'string',
|
|
1500
|
+
enum: ['user', 'org'],
|
|
1501
|
+
description: 'Where to store the relation (default: user)'
|
|
1502
|
+
}
|
|
1503
|
+
},
|
|
1504
|
+
required: ['from', 'to', 'type']
|
|
1505
|
+
}
|
|
1506
|
+
},
|
|
1507
|
+
{
|
|
1508
|
+
name: 'search_context',
|
|
1509
|
+
description: 'Search the context graph for entities and relationships.',
|
|
1510
|
+
input_schema: {
|
|
1511
|
+
type: 'object',
|
|
1512
|
+
properties: {
|
|
1513
|
+
query: {
|
|
1514
|
+
type: 'string',
|
|
1515
|
+
description: 'What to search for'
|
|
1516
|
+
},
|
|
1517
|
+
kind: {
|
|
1518
|
+
type: 'string',
|
|
1519
|
+
enum: ContextGraphKinds.entities,
|
|
1520
|
+
description: 'Optional entity kind filter'
|
|
1521
|
+
},
|
|
1522
|
+
scope: {
|
|
1523
|
+
type: 'string',
|
|
1524
|
+
enum: ['user', 'org'],
|
|
1525
|
+
description: 'Which context scope to search (default: user)'
|
|
1526
|
+
}
|
|
1527
|
+
},
|
|
1528
|
+
required: ['query']
|
|
1529
|
+
}
|
|
1530
|
+
},
|
|
762
1531
|
{
|
|
763
1532
|
name: 'search_memory',
|
|
764
1533
|
description: 'Search your memory for relevant information. Use before delegating to include user context.',
|
|
@@ -859,6 +1628,54 @@ Be specific about what you want done.`,
|
|
|
859
1628
|
},
|
|
860
1629
|
required: ['port']
|
|
861
1630
|
}
|
|
1631
|
+
},
|
|
1632
|
+
// Lia's internal task management tools
|
|
1633
|
+
{
|
|
1634
|
+
name: 'lia_plan',
|
|
1635
|
+
description: `Create a plan to break down a complex goal into steps. Use this when you receive multiple requests or need to organize work. The plan becomes your internal todo list.`,
|
|
1636
|
+
input_schema: {
|
|
1637
|
+
type: 'object',
|
|
1638
|
+
properties: {
|
|
1639
|
+
goal: {
|
|
1640
|
+
type: 'string',
|
|
1641
|
+
description: 'The overall goal you are trying to accomplish'
|
|
1642
|
+
},
|
|
1643
|
+
steps: {
|
|
1644
|
+
type: 'array',
|
|
1645
|
+
items: { type: 'string' },
|
|
1646
|
+
description: 'List of steps to accomplish the goal'
|
|
1647
|
+
}
|
|
1648
|
+
},
|
|
1649
|
+
required: ['goal', 'steps']
|
|
1650
|
+
}
|
|
1651
|
+
},
|
|
1652
|
+
{
|
|
1653
|
+
name: 'lia_add_task',
|
|
1654
|
+
description: `Add a task to your internal queue. Use when the user sends a new request while you are busy, or when you identify follow-up work. Tasks are processed in priority order.`,
|
|
1655
|
+
input_schema: {
|
|
1656
|
+
type: 'object',
|
|
1657
|
+
properties: {
|
|
1658
|
+
task: {
|
|
1659
|
+
type: 'string',
|
|
1660
|
+
description: 'The task description'
|
|
1661
|
+
},
|
|
1662
|
+
priority: {
|
|
1663
|
+
type: 'string',
|
|
1664
|
+
enum: ['urgent', 'high', 'normal', 'low'],
|
|
1665
|
+
description: 'Task priority (default: normal)'
|
|
1666
|
+
}
|
|
1667
|
+
},
|
|
1668
|
+
required: ['task']
|
|
1669
|
+
}
|
|
1670
|
+
},
|
|
1671
|
+
{
|
|
1672
|
+
name: 'lia_get_queue',
|
|
1673
|
+
description: 'Get your current task queue status. Shows what you are working on and what is pending.',
|
|
1674
|
+
input_schema: {
|
|
1675
|
+
type: 'object',
|
|
1676
|
+
properties: {},
|
|
1677
|
+
required: []
|
|
1678
|
+
}
|
|
862
1679
|
}
|
|
863
1680
|
];
|
|
864
1681
|
}
|
|
@@ -882,6 +1699,12 @@ Be specific about what you want done.`,
|
|
|
882
1699
|
case 'bash':
|
|
883
1700
|
result = await this.bashTool.execute(input.command, input.timeout);
|
|
884
1701
|
break;
|
|
1702
|
+
case 'calendar_upsert_events':
|
|
1703
|
+
result = this.calendarTools.upsertCalendarEvents(input.events, input.calendars);
|
|
1704
|
+
break;
|
|
1705
|
+
case 'todo_upsert_items':
|
|
1706
|
+
result = this.calendarTools.upsertTodoItems(input.items);
|
|
1707
|
+
break;
|
|
885
1708
|
case 'web_search':
|
|
886
1709
|
result = await this.webTools.webSearch(input.query, { numResults: input.num_results });
|
|
887
1710
|
break;
|
|
@@ -892,12 +1715,14 @@ Be specific about what you want done.`,
|
|
|
892
1715
|
});
|
|
893
1716
|
break;
|
|
894
1717
|
// Worker tools
|
|
895
|
-
case 'spawn_worker':
|
|
896
|
-
|
|
1718
|
+
case 'spawn_worker': {
|
|
1719
|
+
const { task } = this.withAttachments(input.task);
|
|
1720
|
+
result = await this.workerTools.spawnWorker(task, {
|
|
897
1721
|
timeout: input.timeout,
|
|
898
1722
|
priority: input.priority
|
|
899
1723
|
});
|
|
900
1724
|
break;
|
|
1725
|
+
}
|
|
901
1726
|
case 'check_worker':
|
|
902
1727
|
result = await this.workerTools.checkWorker(input.job_id);
|
|
903
1728
|
break;
|
|
@@ -911,12 +1736,68 @@ Be specific about what you want done.`,
|
|
|
911
1736
|
result = await this.workerTools.cancelWorker(input.job_id);
|
|
912
1737
|
break;
|
|
913
1738
|
// Legacy delegate tool
|
|
914
|
-
case 'delegate_to_worker':
|
|
915
|
-
|
|
1739
|
+
case 'delegate_to_worker': {
|
|
1740
|
+
const { task, context } = this.withAttachments(input.task, input.context);
|
|
1741
|
+
result = await this.delegateToWorker(task, context, input.working_directory);
|
|
916
1742
|
break;
|
|
1743
|
+
}
|
|
917
1744
|
case 'remember':
|
|
918
1745
|
result = await this.executeRemember(input.content, input.type, input.importance);
|
|
919
1746
|
break;
|
|
1747
|
+
case 'remember_entity': {
|
|
1748
|
+
const scope = input.scope === 'org' ? 'org' : 'user';
|
|
1749
|
+
const graph = scope === 'org' ? this.orgContextGraph : this.contextGraph;
|
|
1750
|
+
if (!graph) {
|
|
1751
|
+
result = { success: false, output: 'Org context is not configured for this agent.' };
|
|
1752
|
+
break;
|
|
1753
|
+
}
|
|
1754
|
+
const entity = await graph.upsertEntity({
|
|
1755
|
+
kind: input.kind,
|
|
1756
|
+
name: input.name,
|
|
1757
|
+
description: input.description,
|
|
1758
|
+
attributes: input.attributes,
|
|
1759
|
+
tags: input.tags
|
|
1760
|
+
});
|
|
1761
|
+
result = { success: true, output: `Stored ${scope} entity: ${entity.kind} ${entity.name} (${entity.key})` };
|
|
1762
|
+
break;
|
|
1763
|
+
}
|
|
1764
|
+
case 'remember_relation': {
|
|
1765
|
+
const scope = input.scope === 'org' ? 'org' : 'user';
|
|
1766
|
+
const graph = scope === 'org' ? this.orgContextGraph : this.contextGraph;
|
|
1767
|
+
if (!graph) {
|
|
1768
|
+
result = { success: false, output: 'Org context is not configured for this agent.' };
|
|
1769
|
+
break;
|
|
1770
|
+
}
|
|
1771
|
+
const relation = await graph.linkEntities({
|
|
1772
|
+
from: input.from,
|
|
1773
|
+
to: input.to,
|
|
1774
|
+
type: input.type,
|
|
1775
|
+
description: input.description,
|
|
1776
|
+
fromKind: input.from_kind,
|
|
1777
|
+
toKind: input.to_kind
|
|
1778
|
+
});
|
|
1779
|
+
result = { success: true, output: `Linked ${scope}: ${relation.from} ${relation.type} ${relation.to}` };
|
|
1780
|
+
break;
|
|
1781
|
+
}
|
|
1782
|
+
case 'search_context': {
|
|
1783
|
+
const scope = input.scope === 'org' ? 'org' : 'user';
|
|
1784
|
+
const graph = scope === 'org' ? this.orgContextGraph : this.contextGraph;
|
|
1785
|
+
if (!graph) {
|
|
1786
|
+
result = { success: false, output: 'Org context is not configured for this agent.' };
|
|
1787
|
+
break;
|
|
1788
|
+
}
|
|
1789
|
+
const entities = await graph.searchEntities(input.query, { kind: input.kind, limit: 6 });
|
|
1790
|
+
if (entities.length === 0) {
|
|
1791
|
+
result = { success: true, output: 'No context entities found.' };
|
|
1792
|
+
break;
|
|
1793
|
+
}
|
|
1794
|
+
const lines = entities.map(entity => {
|
|
1795
|
+
const desc = entity.description ? ` - ${entity.description}` : '';
|
|
1796
|
+
return `[${entity.kind}] ${entity.name}${desc} (${entity.key})`;
|
|
1797
|
+
});
|
|
1798
|
+
result = { success: true, output: lines.join('\n') };
|
|
1799
|
+
break;
|
|
1800
|
+
}
|
|
920
1801
|
case 'search_memory':
|
|
921
1802
|
result = await this.executeSearchMemory(input.query, input.type);
|
|
922
1803
|
break;
|
|
@@ -938,6 +1819,16 @@ Be specific about what you want done.`,
|
|
|
938
1819
|
case 'stop_local_server':
|
|
939
1820
|
result = await this.executeStopServer(input.port);
|
|
940
1821
|
break;
|
|
1822
|
+
// Lia's internal task management tools
|
|
1823
|
+
case 'lia_plan':
|
|
1824
|
+
result = this.executeLiaPlan(input.goal, input.steps);
|
|
1825
|
+
break;
|
|
1826
|
+
case 'lia_add_task':
|
|
1827
|
+
result = this.executeLiaAddTask(input.task, input.priority);
|
|
1828
|
+
break;
|
|
1829
|
+
case 'lia_get_queue':
|
|
1830
|
+
result = this.executeLiaGetQueue();
|
|
1831
|
+
break;
|
|
941
1832
|
default:
|
|
942
1833
|
result = { success: false, output: `Unknown tool: ${toolUse.name}` };
|
|
943
1834
|
}
|
|
@@ -999,10 +1890,31 @@ Be specific about what you want done.`,
|
|
|
999
1890
|
};
|
|
1000
1891
|
// Escape single quotes in prompt for shell safety
|
|
1001
1892
|
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
|
1893
|
+
const claudeCmd = `${this.claudePath} -p '${escapedPrompt}' --output-format text --dangerously-skip-permissions`;
|
|
1894
|
+
const isRoot = process.getuid?.() === 0;
|
|
1895
|
+
const shellCmd = isRoot
|
|
1896
|
+
? `runuser -u lia -m -- /bin/bash -l -c "${claudeCmd.replace(/"/g, '\\"')}"`
|
|
1897
|
+
: claudeCmd;
|
|
1898
|
+
let workerUser = process.env.USER || 'lia';
|
|
1899
|
+
let workerLogname = process.env.LOGNAME || workerUser;
|
|
1900
|
+
if (isRoot) {
|
|
1901
|
+
workerUser = 'lia';
|
|
1902
|
+
workerLogname = 'lia';
|
|
1903
|
+
}
|
|
1904
|
+
const spawnEnv = {
|
|
1905
|
+
...process.env,
|
|
1906
|
+
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
|
|
1907
|
+
GH_TOKEN: process.env.GH_TOKEN,
|
|
1908
|
+
GITHUB_TOKEN: process.env.GITHUB_TOKEN,
|
|
1909
|
+
HOME: cwd,
|
|
1910
|
+
LIA_WORKSPACE: cwd,
|
|
1911
|
+
USER: workerUser,
|
|
1912
|
+
LOGNAME: workerLogname
|
|
1913
|
+
};
|
|
1002
1914
|
// Spawn worker
|
|
1003
|
-
const child = spawn('/bin/bash', ['-l', '-c',
|
|
1915
|
+
const child = spawn('/bin/bash', ['-l', '-c', shellCmd], {
|
|
1004
1916
|
cwd,
|
|
1005
|
-
env:
|
|
1917
|
+
env: spawnEnv,
|
|
1006
1918
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
1007
1919
|
});
|
|
1008
1920
|
job.process = child;
|
|
@@ -1024,12 +1936,18 @@ Be specific about what you want done.`,
|
|
|
1024
1936
|
job.output += text;
|
|
1025
1937
|
// Stream output to console
|
|
1026
1938
|
process.stdout.write(`[${id}] ${text}`);
|
|
1939
|
+
if (this.workerLogCallback) {
|
|
1940
|
+
this.workerLogCallback(id, text, 'stdout');
|
|
1941
|
+
}
|
|
1027
1942
|
});
|
|
1028
1943
|
child.stderr?.on('data', (data) => {
|
|
1029
1944
|
const text = data.toString();
|
|
1030
1945
|
if (!text.includes('Checking') && !text.includes('Connected')) {
|
|
1031
1946
|
job.output += text;
|
|
1032
1947
|
}
|
|
1948
|
+
if (this.workerLogCallback) {
|
|
1949
|
+
this.workerLogCallback(id, text, 'stderr');
|
|
1950
|
+
}
|
|
1033
1951
|
});
|
|
1034
1952
|
child.on('close', async (code) => {
|
|
1035
1953
|
clearTimeout(timeout);
|
|
@@ -1196,9 +2114,24 @@ Be specific about what you want done.`,
|
|
|
1196
2114
|
}
|
|
1197
2115
|
/**
|
|
1198
2116
|
* Execute remember tool
|
|
2117
|
+
* Routes writes based on instanceMode:
|
|
2118
|
+
* - personal: write to personal memory only
|
|
2119
|
+
* - team: write to team memory only
|
|
1199
2120
|
*/
|
|
1200
2121
|
async executeRemember(content, type, importance) {
|
|
1201
2122
|
try {
|
|
2123
|
+
// Team mode: write to org memory only
|
|
2124
|
+
if (this.instanceMode === 'team') {
|
|
2125
|
+
if (!this.orgMemory) {
|
|
2126
|
+
return { success: false, output: 'Team memory not configured. Cannot store team memory.' };
|
|
2127
|
+
}
|
|
2128
|
+
const id = await this.orgMemory.remember(content, {
|
|
2129
|
+
type: type || undefined,
|
|
2130
|
+
importance
|
|
2131
|
+
});
|
|
2132
|
+
return { success: true, output: `Remembered in team memory (${id})` };
|
|
2133
|
+
}
|
|
2134
|
+
// Personal mode: write to personal memory only
|
|
1202
2135
|
const id = await this.memory.remember(content, {
|
|
1203
2136
|
type: type || undefined,
|
|
1204
2137
|
importance
|
|
@@ -1211,17 +2144,53 @@ Be specific about what you want done.`,
|
|
|
1211
2144
|
}
|
|
1212
2145
|
/**
|
|
1213
2146
|
* Execute search memory tool
|
|
2147
|
+
* Routes searches based on instanceMode:
|
|
2148
|
+
* - personal: search personal + all team memories
|
|
2149
|
+
* - team: search team memory only (never personal)
|
|
1214
2150
|
*/
|
|
1215
2151
|
async executeSearchMemory(query, type) {
|
|
1216
2152
|
try {
|
|
1217
|
-
const
|
|
2153
|
+
const searchOpts = {
|
|
1218
2154
|
type: type || undefined,
|
|
1219
2155
|
limit: 10
|
|
1220
|
-
}
|
|
1221
|
-
|
|
2156
|
+
};
|
|
2157
|
+
// Team mode: search only team memory
|
|
2158
|
+
if (this.instanceMode === 'team') {
|
|
2159
|
+
if (!this.orgMemory) {
|
|
2160
|
+
return { success: false, output: 'Team memory not configured.' };
|
|
2161
|
+
}
|
|
2162
|
+
const memories = await this.orgMemory.search(query, searchOpts);
|
|
2163
|
+
if (memories.length === 0) {
|
|
2164
|
+
return { success: true, output: 'No relevant team memories found.' };
|
|
2165
|
+
}
|
|
2166
|
+
const output = memories.map(m => `[team:${m.type}] (${(m.importance * 100).toFixed(0)}%) ${m.content}`).join('\n\n');
|
|
2167
|
+
return { success: true, output };
|
|
2168
|
+
}
|
|
2169
|
+
// Personal mode: search personal + all team memories
|
|
2170
|
+
const allMemories = [];
|
|
2171
|
+
// Search personal memories
|
|
2172
|
+
const personalMemories = await this.memory.search(query, { ...searchOpts, limit: 5 });
|
|
2173
|
+
for (const m of personalMemories) {
|
|
2174
|
+
allMemories.push({ source: 'personal', memory: m });
|
|
2175
|
+
}
|
|
2176
|
+
// Search all team memories user belongs to
|
|
2177
|
+
for (const [orgId, orgMem] of this.userOrgMemories) {
|
|
2178
|
+
try {
|
|
2179
|
+
const teamMems = await orgMem.search(query, { ...searchOpts, limit: 3 });
|
|
2180
|
+
for (const m of teamMems) {
|
|
2181
|
+
allMemories.push({ source: `team:${orgId}`, memory: m });
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
catch {
|
|
2185
|
+
// Skip failed team memory searches
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
if (allMemories.length === 0) {
|
|
1222
2189
|
return { success: true, output: 'No relevant memories found.' };
|
|
1223
2190
|
}
|
|
1224
|
-
|
|
2191
|
+
// Sort by importance and format
|
|
2192
|
+
allMemories.sort((a, b) => b.memory.importance - a.memory.importance);
|
|
2193
|
+
const output = allMemories.map(({ source, memory: m }) => `[${source}:${m.type}] (${(m.importance * 100).toFixed(0)}%) ${m.content}`).join('\n\n');
|
|
1225
2194
|
return { success: true, output };
|
|
1226
2195
|
}
|
|
1227
2196
|
catch (error) {
|
|
@@ -1363,6 +2332,66 @@ Be specific about what you want done.`,
|
|
|
1363
2332
|
return { success: false, output: `No server running on port ${port}` };
|
|
1364
2333
|
}
|
|
1365
2334
|
}
|
|
2335
|
+
/**
|
|
2336
|
+
* Create a plan with steps (Lia's internal planning)
|
|
2337
|
+
*/
|
|
2338
|
+
executeLiaPlan(goal, steps) {
|
|
2339
|
+
try {
|
|
2340
|
+
const plan = this.taskQueue.createPlan(goal, steps);
|
|
2341
|
+
console.log(`[ORCHESTRATOR] Created plan: ${plan.id} with ${steps.length} steps`);
|
|
2342
|
+
const stepsFormatted = steps.map((s, i) => `${i + 1}. ${s}`).join('\n');
|
|
2343
|
+
return {
|
|
2344
|
+
success: true,
|
|
2345
|
+
output: `Created plan "${goal}" with ${steps.length} steps:\n${stepsFormatted}\n\nPlan ID: ${plan.id}`
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
catch (error) {
|
|
2349
|
+
return { success: false, output: `Failed to create plan: ${error}` };
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
/**
|
|
2353
|
+
* Add a task to Lia's internal queue
|
|
2354
|
+
*/
|
|
2355
|
+
executeLiaAddTask(task, priority) {
|
|
2356
|
+
try {
|
|
2357
|
+
const newTask = this.taskQueue.addTask({
|
|
2358
|
+
type: 'follow_up',
|
|
2359
|
+
content: task,
|
|
2360
|
+
priority: priority || 'normal',
|
|
2361
|
+
metadata: {
|
|
2362
|
+
userId: this.userId,
|
|
2363
|
+
context: this.instanceMode,
|
|
2364
|
+
orgId: this.orgId,
|
|
2365
|
+
},
|
|
2366
|
+
});
|
|
2367
|
+
console.log(`[ORCHESTRATOR] Added task to queue: ${newTask.id}`);
|
|
2368
|
+
const status = this.taskQueue.getStatus();
|
|
2369
|
+
return {
|
|
2370
|
+
success: true,
|
|
2371
|
+
output: `Added to my queue: "${task}" (priority: ${priority || 'normal'}). ${status.pendingCount} tasks pending.`
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
catch (error) {
|
|
2375
|
+
return { success: false, output: `Failed to add task: ${error}` };
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
/**
|
|
2379
|
+
* Get Lia's task queue status
|
|
2380
|
+
*/
|
|
2381
|
+
executeLiaGetQueue() {
|
|
2382
|
+
try {
|
|
2383
|
+
const status = this.taskQueue.getStatus();
|
|
2384
|
+
const todoList = this.taskQueue.formatAsTodoList();
|
|
2385
|
+
let output = todoList;
|
|
2386
|
+
if (!status.isProcessing && status.pendingCount === 0) {
|
|
2387
|
+
output = 'My queue is empty - ready for new tasks.';
|
|
2388
|
+
}
|
|
2389
|
+
return { success: true, output };
|
|
2390
|
+
}
|
|
2391
|
+
catch (error) {
|
|
2392
|
+
return { success: false, output: `Failed to get queue: ${error}` };
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
1366
2395
|
/**
|
|
1367
2396
|
* Format tool preview for user
|
|
1368
2397
|
*/
|
|
@@ -1396,6 +2425,12 @@ Be specific about what you want done.`,
|
|
|
1396
2425
|
return `Delegating: ${input.task.slice(0, 60)}...`;
|
|
1397
2426
|
case 'remember':
|
|
1398
2427
|
return `Remembering: ${input.content.slice(0, 50)}...`;
|
|
2428
|
+
case 'remember_entity':
|
|
2429
|
+
return `Remembering entity: ${input.name}`;
|
|
2430
|
+
case 'remember_relation':
|
|
2431
|
+
return `Linking context: ${input.from} -> ${input.type} -> ${input.to}`;
|
|
2432
|
+
case 'search_context':
|
|
2433
|
+
return `Searching context: ${input.query}`;
|
|
1399
2434
|
case 'search_memory':
|
|
1400
2435
|
return `Searching memory: ${input.query}`;
|
|
1401
2436
|
case 'schedule_task':
|
|
@@ -1406,6 +2441,12 @@ Be specific about what you want done.`,
|
|
|
1406
2441
|
return `Starting server on port ${input.port || 8080}...`;
|
|
1407
2442
|
case 'stop_local_server':
|
|
1408
2443
|
return `Stopping server on port ${input.port}...`;
|
|
2444
|
+
case 'lia_plan':
|
|
2445
|
+
return `Planning: ${input.goal}`;
|
|
2446
|
+
case 'lia_add_task':
|
|
2447
|
+
return `Queuing task: ${input.task.slice(0, 50)}...`;
|
|
2448
|
+
case 'lia_get_queue':
|
|
2449
|
+
return 'Checking my task queue...';
|
|
1409
2450
|
default:
|
|
1410
2451
|
return null;
|
|
1411
2452
|
}
|
|
@@ -1438,5 +2479,24 @@ Be specific about what you want done.`,
|
|
|
1438
2479
|
else {
|
|
1439
2480
|
this.memory.close();
|
|
1440
2481
|
}
|
|
2482
|
+
if (this.orgMemory) {
|
|
2483
|
+
if (this.orgMemory instanceof PostgresMemoryStore) {
|
|
2484
|
+
await this.orgMemory.close();
|
|
2485
|
+
}
|
|
2486
|
+
else {
|
|
2487
|
+
this.orgMemory.close();
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
// Close all user org memory stores (for personal mode reading team memories)
|
|
2491
|
+
for (const [, orgMem] of this.userOrgMemories) {
|
|
2492
|
+
if (orgMem instanceof PostgresMemoryStore) {
|
|
2493
|
+
await orgMem.close();
|
|
2494
|
+
}
|
|
2495
|
+
else {
|
|
2496
|
+
orgMem.close();
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
this.userOrgMemories.clear();
|
|
2500
|
+
this.userOrgContextGraphs.clear();
|
|
1441
2501
|
}
|
|
1442
2502
|
}
|