@siftd/connect-agent 0.2.36 → 0.2.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/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/orchestrator.d.ts +46 -0
- package/dist/orchestrator.js +874 -26
- package/dist/tools/bash.js +4 -1
- 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,9 +11,11 @@ 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';
|
|
@@ -90,11 +92,21 @@ TOOL RULES:
|
|
|
90
92
|
- Building, testing, deploying
|
|
91
93
|
- Any filesystem modification
|
|
92
94
|
|
|
95
|
+
ATTACHMENTS:
|
|
96
|
+
- If the user message includes "📎 Attached files:", include those URLs verbatim in any worker task.
|
|
97
|
+
- Workers should download the files from the URLs and proceed without asking for the content again.
|
|
98
|
+
|
|
93
99
|
MEMORY:
|
|
94
100
|
- Search memory before starting work (context is valuable)
|
|
95
101
|
- Remember important things after significant actions
|
|
96
102
|
- Learn from failures - what went wrong, how to avoid it
|
|
97
103
|
|
|
104
|
+
CONTEXT GRAPH:
|
|
105
|
+
- Use remember_entity to store people, teams, projects, tools, tasks
|
|
106
|
+
- Use remember_relation to link entities (owns, works_on, depends_on, etc.)
|
|
107
|
+
- Use search_context to retrieve team context before asking users to repeat
|
|
108
|
+
- If user asks for "/context", call search_context and summarize results
|
|
109
|
+
|
|
98
110
|
YOUR HUB: ~/Lia-Hub/
|
|
99
111
|
- **CLAUDE.md** - User's instructions (always follow these)
|
|
100
112
|
- **LANDMARKS.md** - Current state and context
|
|
@@ -109,22 +121,52 @@ The Lia interface has a built-in gallery that shows worker outputs automatically
|
|
|
109
121
|
✅ Workers write to ~/Lia-Hub/shared/outputs/
|
|
110
122
|
✅ Gallery displays assets in-app automatically
|
|
111
123
|
|
|
124
|
+
FILES BROWSER:
|
|
125
|
+
Users can type /files to open the Finder UI (cloud mode).
|
|
126
|
+
When asked to browse or locate files, point them to /files.
|
|
127
|
+
Refer to /files instead of internal Lia-Hub paths in responses.
|
|
128
|
+
|
|
129
|
+
FILES SCOPES:
|
|
130
|
+
- "My Files" are private to the user (default /files view)
|
|
131
|
+
- "Team Files" are shared across the org (use /files team or the Team Files location)
|
|
132
|
+
- If a user asks to share with the team, store outputs in Team Files and mention that it is shared
|
|
133
|
+
|
|
134
|
+
CALENDAR + TODO (Lia-managed data):
|
|
135
|
+
- /cal reads ~/Lia-Hub/shared/outputs/.lia/calendar.json
|
|
136
|
+
- /todo reads ~/Lia-Hub/shared/outputs/.lia/todos.json
|
|
137
|
+
- When users add, edit, or delete events, use calendar_upsert_events
|
|
138
|
+
- When users add, edit, or delete tasks, use todo_upsert_items
|
|
139
|
+
- Keep these files hidden; refer users to /cal or /todo in responses
|
|
140
|
+
- Schemas:
|
|
141
|
+
- calendar.json: { "version": 1, "calendars": [...], "events": [...] }
|
|
142
|
+
- todos.json: { "version": 1, "items": [...] }
|
|
143
|
+
|
|
112
144
|
WORKFLOW:
|
|
113
145
|
Before complex work: Check CLAUDE.md → Read LANDMARKS.md → Search memory
|
|
114
146
|
After completing work: Update LANDMARKS.md → Remember learnings
|
|
115
147
|
|
|
116
|
-
You orchestrate through workers. You remember through memory. You never do file operations directly.`;
|
|
148
|
+
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
149
|
export class MasterOrchestrator {
|
|
118
150
|
client;
|
|
119
151
|
model;
|
|
120
152
|
maxTokens;
|
|
121
153
|
memory;
|
|
154
|
+
contextGraph;
|
|
155
|
+
orgMemory;
|
|
156
|
+
orgContextGraph;
|
|
157
|
+
orgId;
|
|
158
|
+
orgRole;
|
|
159
|
+
instanceMode;
|
|
160
|
+
userOrgMemories;
|
|
161
|
+
userOrgContextGraphs;
|
|
122
162
|
scheduler;
|
|
123
163
|
indexer;
|
|
124
164
|
jobs = new Map();
|
|
125
165
|
jobCounter = 0;
|
|
126
166
|
workerStatusCallback = null;
|
|
167
|
+
workerLogCallback = null;
|
|
127
168
|
workerStatusInterval = null;
|
|
169
|
+
attachmentContext = null;
|
|
128
170
|
galleryCallback = null;
|
|
129
171
|
userId;
|
|
130
172
|
workspaceDir;
|
|
@@ -134,6 +176,7 @@ export class MasterOrchestrator {
|
|
|
134
176
|
bashTool;
|
|
135
177
|
webTools;
|
|
136
178
|
workerTools;
|
|
179
|
+
calendarTools;
|
|
137
180
|
sharedState;
|
|
138
181
|
verboseMode = new Map(); // per-user verbose mode
|
|
139
182
|
constructor(options) {
|
|
@@ -141,10 +184,16 @@ export class MasterOrchestrator {
|
|
|
141
184
|
this.model = options.model || 'claude-sonnet-4-20250514';
|
|
142
185
|
this.maxTokens = options.maxTokens || 4096;
|
|
143
186
|
this.userId = options.userId;
|
|
187
|
+
this.orgId = options.orgId;
|
|
188
|
+
this.orgRole = options.orgRole;
|
|
189
|
+
this.instanceMode = options.instanceMode || 'personal';
|
|
144
190
|
this.workspaceDir = options.workspaceDir || process.env.HOME || '/tmp';
|
|
191
|
+
this.userOrgMemories = new Map();
|
|
192
|
+
this.userOrgContextGraphs = new Map();
|
|
145
193
|
// Find claude binary - critical for worker delegation
|
|
146
194
|
this.claudePath = this.findClaudeBinary();
|
|
147
195
|
console.log(`[ORCHESTRATOR] Claude binary: ${this.claudePath}`);
|
|
196
|
+
console.log(`[ORCHESTRATOR] Instance mode: ${this.instanceMode}`);
|
|
148
197
|
// Initialize memory - use Postgres if DATABASE_URL is set, otherwise SQLite
|
|
149
198
|
if (isPostgresConfigured()) {
|
|
150
199
|
console.log('[ORCHESTRATOR] DATABASE_URL detected, using PostgreSQL + pgvector');
|
|
@@ -160,6 +209,44 @@ export class MasterOrchestrator {
|
|
|
160
209
|
voyageApiKey: options.voyageApiKey
|
|
161
210
|
});
|
|
162
211
|
}
|
|
212
|
+
this.contextGraph = new ContextGraph(this.memory, { scope: options.userId });
|
|
213
|
+
// Initialize org memory for team mode or when orgId is provided
|
|
214
|
+
if (this.orgId) {
|
|
215
|
+
if (isPostgresConfigured()) {
|
|
216
|
+
this.orgMemory = new PostgresMemoryStore({
|
|
217
|
+
connectionString: process.env.DATABASE_URL,
|
|
218
|
+
userId: `org:${this.orgId}`
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
this.orgMemory = new AdvancedMemoryStore({
|
|
223
|
+
dbPath: `memories_org_${this.orgId}.db`,
|
|
224
|
+
vectorPath: `./memory_vectors_org_${this.orgId}`,
|
|
225
|
+
voyageApiKey: options.voyageApiKey
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
this.orgContextGraph = new ContextGraph(this.orgMemory, { scope: `org:${this.orgId}` });
|
|
229
|
+
}
|
|
230
|
+
// For personal mode: initialize memory stores for all user's teams (for reading)
|
|
231
|
+
if (this.instanceMode === 'personal' && options.userOrgIds) {
|
|
232
|
+
for (const orgId of options.userOrgIds) {
|
|
233
|
+
if (isPostgresConfigured()) {
|
|
234
|
+
this.userOrgMemories.set(orgId, new PostgresMemoryStore({
|
|
235
|
+
connectionString: process.env.DATABASE_URL,
|
|
236
|
+
userId: `org:${orgId}`
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
this.userOrgMemories.set(orgId, new AdvancedMemoryStore({
|
|
241
|
+
dbPath: `memories_org_${orgId}.db`,
|
|
242
|
+
vectorPath: `./memory_vectors_org_${orgId}`,
|
|
243
|
+
voyageApiKey: options.voyageApiKey
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
this.userOrgContextGraphs.set(orgId, new ContextGraph(this.userOrgMemories.get(orgId), { scope: `org:${orgId}` }));
|
|
247
|
+
}
|
|
248
|
+
console.log(`[ORCHESTRATOR] Personal mode: initialized ${this.userOrgMemories.size} team memory stores for reading`);
|
|
249
|
+
}
|
|
163
250
|
this.scheduler = new TaskScheduler(`scheduled_${options.userId}.json`);
|
|
164
251
|
// Create indexer (will index on first initialize call)
|
|
165
252
|
this.indexer = new SystemIndexer(this.memory);
|
|
@@ -167,7 +254,13 @@ export class MasterOrchestrator {
|
|
|
167
254
|
this.bashTool = new BashTool(this.workspaceDir);
|
|
168
255
|
this.webTools = new WebTools();
|
|
169
256
|
this.workerTools = new WorkerTools(this.workspaceDir);
|
|
257
|
+
this.calendarTools = new CalendarTools();
|
|
170
258
|
this.sharedState = new SharedState(this.workspaceDir);
|
|
259
|
+
this.workerTools.setLogCallback((workerId, line, stream) => {
|
|
260
|
+
if (this.workerLogCallback) {
|
|
261
|
+
this.workerLogCallback(workerId, line, stream);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
171
264
|
}
|
|
172
265
|
/**
|
|
173
266
|
* Initialize the orchestrator - indexes filesystem on first call
|
|
@@ -208,6 +301,12 @@ export class MasterOrchestrator {
|
|
|
208
301
|
}
|
|
209
302
|
}
|
|
210
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* Set callback for worker log streaming
|
|
306
|
+
*/
|
|
307
|
+
setWorkerLogCallback(callback) {
|
|
308
|
+
this.workerLogCallback = callback;
|
|
309
|
+
}
|
|
211
310
|
/**
|
|
212
311
|
* Set callback for gallery updates (worker assets for UI gallery view)
|
|
213
312
|
*/
|
|
@@ -412,6 +511,35 @@ export class MasterOrchestrator {
|
|
|
412
511
|
}
|
|
413
512
|
return null;
|
|
414
513
|
}
|
|
514
|
+
extractAttachmentContext(message) {
|
|
515
|
+
const marker = '📎 Attached files:';
|
|
516
|
+
const index = message.indexOf(marker);
|
|
517
|
+
if (index === -1)
|
|
518
|
+
return null;
|
|
519
|
+
const raw = message.slice(index + marker.length);
|
|
520
|
+
const lines = raw
|
|
521
|
+
.split('\n')
|
|
522
|
+
.map(line => line.trim())
|
|
523
|
+
.filter(Boolean);
|
|
524
|
+
if (lines.length === 0)
|
|
525
|
+
return null;
|
|
526
|
+
return lines.join('\n');
|
|
527
|
+
}
|
|
528
|
+
withAttachments(task, context) {
|
|
529
|
+
if (!this.attachmentContext)
|
|
530
|
+
return { task, context };
|
|
531
|
+
const attachmentBlock = this.attachmentContext;
|
|
532
|
+
const hasAttachments = task.includes(attachmentBlock) ||
|
|
533
|
+
task.includes('Attached files') ||
|
|
534
|
+
(context && (context.includes(attachmentBlock) || context.includes('Attached files')));
|
|
535
|
+
if (hasAttachments)
|
|
536
|
+
return { task, context };
|
|
537
|
+
const attachmentNote = `Attached files (download these URLs and use their contents; do not ask the user to resend):\n${attachmentBlock}`;
|
|
538
|
+
if (context && context.trim().length > 0) {
|
|
539
|
+
return { task, context: `${context}\n\n${attachmentNote}` };
|
|
540
|
+
}
|
|
541
|
+
return { task: `${task}\n\n${attachmentNote}` };
|
|
542
|
+
}
|
|
415
543
|
/**
|
|
416
544
|
* Check if verbose mode is enabled
|
|
417
545
|
*/
|
|
@@ -428,9 +556,15 @@ export class MasterOrchestrator {
|
|
|
428
556
|
if (slashResponse) {
|
|
429
557
|
return slashResponse;
|
|
430
558
|
}
|
|
559
|
+
// Deterministic calendar/todo writes (do not rely on the model calling tools)
|
|
560
|
+
const quickWrite = this.tryHandleCalendarTodo(message);
|
|
561
|
+
if (quickWrite) {
|
|
562
|
+
return quickWrite;
|
|
563
|
+
}
|
|
431
564
|
// Load hub context (AGENTS.md identity, LANDMARKS.md state, project bio if relevant)
|
|
432
565
|
const hubContext = loadHubContext(message);
|
|
433
566
|
const hubContextStr = formatHubContext(hubContext);
|
|
567
|
+
this.attachmentContext = this.extractAttachmentContext(message);
|
|
434
568
|
// Build context from memory
|
|
435
569
|
const memoryContext = await this.getMemoryContext(message);
|
|
436
570
|
// Build system prompt with hub context, genesis knowledge, and memory context
|
|
@@ -467,6 +601,7 @@ ${hubContextStr}
|
|
|
467
601
|
const response = await this.runAgentLoop(messages, systemWithContext, sendMessage, apiKey);
|
|
468
602
|
// Auto-remember important things from the conversation
|
|
469
603
|
await this.autoRemember(message, response);
|
|
604
|
+
void this.autoCaptureContext(message);
|
|
470
605
|
// Log significant actions to hub
|
|
471
606
|
if (response.length > 100) {
|
|
472
607
|
const action = message.length > 50 ? message.slice(0, 50) + '...' : message;
|
|
@@ -478,16 +613,239 @@ ${hubContextStr}
|
|
|
478
613
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
479
614
|
return `Error: ${errorMessage}`;
|
|
480
615
|
}
|
|
616
|
+
finally {
|
|
617
|
+
this.attachmentContext = null;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
tryHandleCalendarTodo(message) {
|
|
621
|
+
const todoItems = this.extractTodoItems(message);
|
|
622
|
+
const calendarEvents = this.extractCalendarEvents(message);
|
|
623
|
+
if (todoItems.length === 0 && calendarEvents.length === 0)
|
|
624
|
+
return null;
|
|
625
|
+
const results = [];
|
|
626
|
+
if (todoItems.length > 0) {
|
|
627
|
+
const todoRes = this.calendarTools.upsertTodoItems(todoItems);
|
|
628
|
+
if (!todoRes.success) {
|
|
629
|
+
return `Failed to update /todo: ${todoRes.error || todoRes.output || 'unknown error'}`;
|
|
630
|
+
}
|
|
631
|
+
results.push(`Updated /todo with ${todoItems.length} item(s).`);
|
|
632
|
+
}
|
|
633
|
+
if (calendarEvents.length > 0) {
|
|
634
|
+
const calRes = this.calendarTools.upsertCalendarEvents(calendarEvents);
|
|
635
|
+
if (!calRes.success) {
|
|
636
|
+
return `Failed to update /cal: ${calRes.error || calRes.output || 'unknown error'}`;
|
|
637
|
+
}
|
|
638
|
+
results.push(`Updated /cal with ${calendarEvents.length} event(s).`);
|
|
639
|
+
}
|
|
640
|
+
return `${results.join(' ')} Open /todo or /cal to view.`;
|
|
641
|
+
}
|
|
642
|
+
extractTodoItems(message) {
|
|
643
|
+
const lower = message.toLowerCase();
|
|
644
|
+
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)) &&
|
|
645
|
+
/\b(add|put|save|store|track)\b/i.test(message);
|
|
646
|
+
if (!hasTodoIntent)
|
|
647
|
+
return [];
|
|
648
|
+
const items = [];
|
|
649
|
+
const wantSteps = /\b(step\s*[- ]?by\s*[- ]?step|break\s*down|smaller\s*steps|parse\s*it)\b/i.test(message);
|
|
650
|
+
const hasRag = /\b(rag|retrieval augmented|retrieval-augmented|embedding|embeddings|pierce)\b/i.test(message);
|
|
651
|
+
const hasWhenIsGood = /\b(whenisgood|when\s+is\s+good|availability|unavailable|schedule|scheduling|lab\s+meeting)\b/i.test(message);
|
|
652
|
+
if (wantSteps && hasRag) {
|
|
653
|
+
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' });
|
|
654
|
+
}
|
|
655
|
+
if (wantSteps && hasWhenIsGood) {
|
|
656
|
+
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' });
|
|
657
|
+
}
|
|
658
|
+
if (!wantSteps || (items.length === 0 && (hasRag || hasWhenIsGood))) {
|
|
659
|
+
const raw = message.trim().slice(0, 240);
|
|
660
|
+
items.push({
|
|
661
|
+
title: `[Todo] ${raw}${message.trim().length > 240 ? '…' : ''}`,
|
|
662
|
+
priority: 'medium',
|
|
663
|
+
notes: message.trim()
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
return items;
|
|
667
|
+
}
|
|
668
|
+
extractCalendarEvents(message) {
|
|
669
|
+
const hasCalendarIntent = /\bto\s+my\s+cal\b/i.test(message) ||
|
|
670
|
+
/\bto\s+my\s+calendar\b/i.test(message) ||
|
|
671
|
+
/(\s|^)\/cal\b/i.test(message) ||
|
|
672
|
+
/(\s|^)\/calendar\b/i.test(message);
|
|
673
|
+
const hasAddIntent = /\b(add|put|schedule|book)\b/i.test(message);
|
|
674
|
+
if (!hasAddIntent)
|
|
675
|
+
return [];
|
|
676
|
+
const candidates = message
|
|
677
|
+
.split(/\n\s*\n/)
|
|
678
|
+
.map((chunk) => chunk.trim())
|
|
679
|
+
.filter(Boolean);
|
|
680
|
+
const events = [];
|
|
681
|
+
for (const chunk of candidates) {
|
|
682
|
+
const parsed = this.parseCalendarEvent(chunk, hasCalendarIntent);
|
|
683
|
+
if (parsed)
|
|
684
|
+
events.push(parsed);
|
|
685
|
+
}
|
|
686
|
+
return events;
|
|
687
|
+
}
|
|
688
|
+
parseCalendarEvent(message, hasCalendarIntent) {
|
|
689
|
+
const explicit = this.parseExplicitDateTimeEvent(message, hasCalendarIntent);
|
|
690
|
+
if (explicit)
|
|
691
|
+
return explicit;
|
|
692
|
+
const weekdayMatch = message.match(/\b(mon(?:day)?|tue(?:sday)?|wed(?:nesday)?|thu(?:rsday)?|fri(?:day)?|sat(?:urday)?|sun(?:day)?)\b/i);
|
|
693
|
+
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);
|
|
694
|
+
if (!weekdayMatch || !timeMatch)
|
|
695
|
+
return null;
|
|
696
|
+
const weekday = weekdayMatch[1].slice(0, 3).toLowerCase();
|
|
697
|
+
const weekdayIndex = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 }[weekday];
|
|
698
|
+
const startTime = this.to24h(timeMatch[1], timeMatch[2], timeMatch[3]);
|
|
699
|
+
const endTime = this.to24h(timeMatch[4], timeMatch[5], timeMatch[6]);
|
|
700
|
+
const startDate = this.nextOccurrence(weekdayIndex, startTime);
|
|
701
|
+
const dateKey = this.toDateKey(startDate);
|
|
702
|
+
const title = message
|
|
703
|
+
.replace(/^\s*add\s+/i, '')
|
|
704
|
+
.replace(/\s+to\s+\b(mon(?:day)?|tue(?:sday)?|wed(?:nesday)?|thu(?:rsday)?|fri(?:day)?|sat(?:urday)?|sun(?:day)?)\b[\s\S]*$/i, '')
|
|
705
|
+
.trim();
|
|
706
|
+
if (!title)
|
|
707
|
+
return null;
|
|
708
|
+
return {
|
|
709
|
+
title,
|
|
710
|
+
date: dateKey,
|
|
711
|
+
startTime,
|
|
712
|
+
endTime,
|
|
713
|
+
calendarId: 'primary'
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
parseExplicitDateTimeEvent(message, hasCalendarIntent) {
|
|
717
|
+
if (!hasCalendarIntent)
|
|
718
|
+
return null;
|
|
719
|
+
// e.g. "Tuesday, January 13, 3:30 pm, A211 PDB—TITLE, \"Talk...\" ... Host: ..."
|
|
720
|
+
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);
|
|
721
|
+
if (!monthMatch)
|
|
722
|
+
return null;
|
|
723
|
+
const monthText = monthMatch[1].slice(0, 3).toLowerCase();
|
|
724
|
+
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];
|
|
725
|
+
const dayMatch = message.match(new RegExp(`${monthMatch[1]}\\s+(\\d{1,2})`, 'i'));
|
|
726
|
+
const timeMatch = message.match(/\b(\d{1,2})(?::(\d{2}))?\s*(a|am|p|pm)\b/i);
|
|
727
|
+
if (!dayMatch || !timeMatch)
|
|
728
|
+
return null;
|
|
729
|
+
const dayOfMonth = Number(dayMatch[1]);
|
|
730
|
+
const startTime = this.to24h(timeMatch[1], timeMatch[2], timeMatch[3]);
|
|
731
|
+
const yearMatch = message.match(/\b(20\d{2})\b/);
|
|
732
|
+
const now = new Date();
|
|
733
|
+
const year = yearMatch ? Number(yearMatch[1]) : now.getFullYear();
|
|
734
|
+
const date = new Date(year, monthIndex, dayOfMonth, 0, 0, 0, 0);
|
|
735
|
+
const dateKey = this.toDateKey(date);
|
|
736
|
+
const cleaned = message
|
|
737
|
+
.replace(/^\s*add\s+(these\s+)?to\s+my\s+(cal|calendar)\b[:\s-]*/i, '')
|
|
738
|
+
.trim();
|
|
739
|
+
const parts = cleaned.split(/—|--|–/);
|
|
740
|
+
const head = parts[0] || cleaned;
|
|
741
|
+
const after = parts.slice(1).join('—').trim();
|
|
742
|
+
// Try to extract a succinct title from the segment after the location delimiter, otherwise use head.
|
|
743
|
+
const titleCandidate = after ? after : head;
|
|
744
|
+
const title = titleCandidate
|
|
745
|
+
.replace(/^[^A-Za-z0-9]*\b(mon(?:day)?|tue(?:sday)?|wed(?:nesday)?|thu(?:rsday)?|fri(?:day)?|sat(?:urday)?|sun(?:day)?)\b[, ]*/i, '')
|
|
746
|
+
.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, '')
|
|
747
|
+
.replace(/\b\d{1,2}(?::\d{2})?\s*(a|am|p|pm)\b/i, '')
|
|
748
|
+
.replace(/^[,\s]+|[,\s]+$/g, '')
|
|
749
|
+
.slice(0, 160);
|
|
750
|
+
if (!title)
|
|
751
|
+
return null;
|
|
752
|
+
const notes = cleaned.length > title.length ? cleaned : undefined;
|
|
753
|
+
return {
|
|
754
|
+
title,
|
|
755
|
+
date: dateKey,
|
|
756
|
+
startTime,
|
|
757
|
+
calendarId: 'primary',
|
|
758
|
+
notes
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
to24h(hourText, minuteText, meridiemText) {
|
|
762
|
+
const hour = Number(hourText);
|
|
763
|
+
const minute = Number(minuteText || '0');
|
|
764
|
+
const meridiem = meridiemText.toLowerCase();
|
|
765
|
+
let h = hour % 12;
|
|
766
|
+
if (meridiem.startsWith('p'))
|
|
767
|
+
h += 12;
|
|
768
|
+
return `${String(h).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
|
|
769
|
+
}
|
|
770
|
+
nextOccurrence(weekdayIndex, startTime) {
|
|
771
|
+
const now = new Date();
|
|
772
|
+
const candidate = new Date(now);
|
|
773
|
+
candidate.setHours(0, 0, 0, 0);
|
|
774
|
+
const delta = (weekdayIndex - candidate.getDay() + 7) % 7;
|
|
775
|
+
candidate.setDate(candidate.getDate() + delta);
|
|
776
|
+
if (startTime) {
|
|
777
|
+
const [h, m] = startTime.split(':').map(Number);
|
|
778
|
+
candidate.setHours(h, m, 0, 0);
|
|
779
|
+
if (candidate.getTime() <= now.getTime()) {
|
|
780
|
+
candidate.setDate(candidate.getDate() + 7);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return candidate;
|
|
784
|
+
}
|
|
785
|
+
toDateKey(date) {
|
|
786
|
+
const year = date.getFullYear();
|
|
787
|
+
const month = `${date.getMonth() + 1}`.padStart(2, '0');
|
|
788
|
+
const day = `${date.getDate()}`.padStart(2, '0');
|
|
789
|
+
return `${year}-${month}-${day}`;
|
|
481
790
|
}
|
|
482
791
|
/**
|
|
483
792
|
* Get relevant memory context for a message
|
|
793
|
+
* Routes based on instanceMode:
|
|
794
|
+
* - personal: search personal memories + all team memories
|
|
795
|
+
* - team: search ONLY team memory (never personal - team Lia is isolated)
|
|
484
796
|
*/
|
|
485
797
|
async getMemoryContext(message) {
|
|
486
798
|
try {
|
|
799
|
+
const sections = [];
|
|
800
|
+
// Team mode: ONLY search team memory - never personal
|
|
801
|
+
if (this.instanceMode === 'team') {
|
|
802
|
+
if (!this.orgMemory) {
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
|
+
const teamMemories = await this.orgMemory.search(message, { limit: 10 });
|
|
806
|
+
const teamMemoryLines = teamMemories.map(m => `[team:${m.type}] ${m.content}`).join('\n');
|
|
807
|
+
const teamContextSummary = this.orgContextGraph
|
|
808
|
+
? await this.orgContextGraph.getContextSummary(message, { limit: 5 })
|
|
809
|
+
: null;
|
|
810
|
+
if (teamMemoryLines)
|
|
811
|
+
sections.push(`TEAM MEMORIES:\n${teamMemoryLines}`);
|
|
812
|
+
if (teamContextSummary)
|
|
813
|
+
sections.push(`TEAM CONTEXT:\n${teamContextSummary}`);
|
|
814
|
+
if (sections.length === 0)
|
|
815
|
+
return null;
|
|
816
|
+
return sections.join('\n\n');
|
|
817
|
+
}
|
|
818
|
+
// Personal mode: search personal + all team memories
|
|
487
819
|
const memories = await this.memory.search(message, { limit: 5 });
|
|
488
|
-
|
|
820
|
+
const memoryLines = memories.map(m => `[${m.type}] ${m.content}`).join('\n');
|
|
821
|
+
const contextSummary = await this.contextGraph.getContextSummary(message, { limit: 3 });
|
|
822
|
+
if (memoryLines)
|
|
823
|
+
sections.push(`YOUR MEMORIES:\n${memoryLines}`);
|
|
824
|
+
if (contextSummary)
|
|
825
|
+
sections.push(`YOUR CONTEXT:\n${contextSummary}`);
|
|
826
|
+
// In personal mode, also include all team memories for reading
|
|
827
|
+
for (const [orgId, orgMem] of this.userOrgMemories) {
|
|
828
|
+
try {
|
|
829
|
+
const teamMems = await orgMem.search(message, { limit: 3 });
|
|
830
|
+
if (teamMems.length > 0) {
|
|
831
|
+
const teamLines = teamMems.map(m => `[${m.type}] ${m.content}`).join('\n');
|
|
832
|
+
sections.push(`TEAM (${orgId}) MEMORIES:\n${teamLines}`);
|
|
833
|
+
}
|
|
834
|
+
const teamGraph = this.userOrgContextGraphs.get(orgId);
|
|
835
|
+
if (teamGraph) {
|
|
836
|
+
const teamContext = await teamGraph.getContextSummary(message, { limit: 2 });
|
|
837
|
+
if (teamContext) {
|
|
838
|
+
sections.push(`TEAM (${orgId}) CONTEXT:\n${teamContext}`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
catch {
|
|
843
|
+
// Skip failed team memory access
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
if (sections.length === 0)
|
|
489
847
|
return null;
|
|
490
|
-
return
|
|
848
|
+
return sections.join('\n\n');
|
|
491
849
|
}
|
|
492
850
|
catch {
|
|
493
851
|
return null;
|
|
@@ -495,6 +853,9 @@ ${hubContextStr}
|
|
|
495
853
|
}
|
|
496
854
|
/**
|
|
497
855
|
* Auto-remember important information from conversations
|
|
856
|
+
* Routes based on instanceMode:
|
|
857
|
+
* - personal: write to personal memory
|
|
858
|
+
* - team: write to team memory
|
|
498
859
|
*/
|
|
499
860
|
async autoRemember(userMessage, response) {
|
|
500
861
|
// Look for preference indicators
|
|
@@ -502,16 +863,140 @@ ${hubContextStr}
|
|
|
502
863
|
const lower = userMessage.toLowerCase();
|
|
503
864
|
for (const indicator of prefIndicators) {
|
|
504
865
|
if (lower.includes(indicator)) {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
866
|
+
// Route to correct memory store based on instanceMode
|
|
867
|
+
const targetMemory = this.instanceMode === 'team' ? this.orgMemory : this.memory;
|
|
868
|
+
if (targetMemory) {
|
|
869
|
+
await targetMemory.remember(userMessage, {
|
|
870
|
+
type: 'semantic',
|
|
871
|
+
source: 'auto-extract',
|
|
872
|
+
importance: 0.7,
|
|
873
|
+
tags: ['preference', this.instanceMode === 'team' ? 'team' : 'user']
|
|
874
|
+
});
|
|
875
|
+
}
|
|
511
876
|
break;
|
|
512
877
|
}
|
|
513
878
|
}
|
|
514
879
|
}
|
|
880
|
+
async autoCaptureContext(userMessage) {
|
|
881
|
+
const marker = '📎 Attached files:';
|
|
882
|
+
const base = userMessage.split(marker)[0]?.trim() || '';
|
|
883
|
+
if (!base || base.startsWith('/'))
|
|
884
|
+
return;
|
|
885
|
+
const text = base.replace(/\s+/g, ' ').trim();
|
|
886
|
+
const shareToOrg = !!this.orgContextGraph &&
|
|
887
|
+
/\b(share with team|team files|shared with team|team-wide|org-wide|company-wide)\b/i.test(text);
|
|
888
|
+
const graphs = shareToOrg && this.orgContextGraph
|
|
889
|
+
? [this.contextGraph, this.orgContextGraph]
|
|
890
|
+
: [this.contextGraph];
|
|
891
|
+
const seenEntities = new Set();
|
|
892
|
+
const tasks = [];
|
|
893
|
+
const rememberEntity = (kind, name, attributes) => {
|
|
894
|
+
const cleaned = this.normalizeEntityName(name);
|
|
895
|
+
if (!cleaned || this.isIgnoredEntity(cleaned))
|
|
896
|
+
return;
|
|
897
|
+
for (const graph of graphs) {
|
|
898
|
+
const scopeLabel = graph === this.orgContextGraph ? 'org' : 'user';
|
|
899
|
+
const key = `${scopeLabel}:${kind}:${cleaned.toLowerCase()}`;
|
|
900
|
+
if (seenEntities.has(key))
|
|
901
|
+
continue;
|
|
902
|
+
seenEntities.add(key);
|
|
903
|
+
tasks.push(graph.upsertEntity({
|
|
904
|
+
kind,
|
|
905
|
+
name: cleaned,
|
|
906
|
+
attributes,
|
|
907
|
+
tags: ['ctx:auto']
|
|
908
|
+
}));
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
const rememberRelation = (from, to, type, fromKind, toKind) => {
|
|
912
|
+
const fromClean = this.normalizeEntityName(from);
|
|
913
|
+
const toClean = this.normalizeEntityName(to);
|
|
914
|
+
if (!fromClean || !toClean || this.isIgnoredEntity(fromClean) || this.isIgnoredEntity(toClean))
|
|
915
|
+
return;
|
|
916
|
+
for (const graph of graphs) {
|
|
917
|
+
tasks.push(graph.linkEntities({
|
|
918
|
+
from: fromClean,
|
|
919
|
+
to: toClean,
|
|
920
|
+
type,
|
|
921
|
+
fromKind,
|
|
922
|
+
toKind,
|
|
923
|
+
source: 'auto-capture'
|
|
924
|
+
}));
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
const emailMatches = text.match(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi) || [];
|
|
928
|
+
for (const email of emailMatches.slice(0, 5)) {
|
|
929
|
+
rememberEntity('person', email, { email });
|
|
930
|
+
}
|
|
931
|
+
for (const match of text.matchAll(/\bproject\s+([A-Za-z0-9][\w .'-]{1,60})/gi)) {
|
|
932
|
+
rememberEntity('project', match[1]);
|
|
933
|
+
}
|
|
934
|
+
for (const match of text.matchAll(/\bteam\s+([A-Za-z0-9][\w .'-]{1,50})/gi)) {
|
|
935
|
+
rememberEntity('team', match[1]);
|
|
936
|
+
}
|
|
937
|
+
for (const match of text.matchAll(/\b([A-Za-z0-9][\w .'-]{1,50})\s+team\b/gi)) {
|
|
938
|
+
rememberEntity('team', match[1]);
|
|
939
|
+
}
|
|
940
|
+
for (const match of text.matchAll(/\b(company|org|organization)\s+([A-Za-z0-9][\w .'-]{1,60})/gi)) {
|
|
941
|
+
rememberEntity('org', match[2]);
|
|
942
|
+
}
|
|
943
|
+
for (const match of text.matchAll(/\b(tool|platform|stack|using)\s+([A-Za-z0-9][\w .'-]{1,40})/gi)) {
|
|
944
|
+
rememberEntity('tool', match[2]);
|
|
945
|
+
}
|
|
946
|
+
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)) {
|
|
947
|
+
rememberRelation(match[1], match[3], 'works_on', 'person', 'project');
|
|
948
|
+
}
|
|
949
|
+
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)) {
|
|
950
|
+
rememberRelation(match[1], match[2], 'owns', 'person', 'project');
|
|
951
|
+
}
|
|
952
|
+
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)) {
|
|
953
|
+
rememberRelation(match[1], match[2], 'leads', 'person', 'team');
|
|
954
|
+
}
|
|
955
|
+
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)) {
|
|
956
|
+
rememberRelation(match[1], match[2], 'member_of', 'person', 'team');
|
|
957
|
+
}
|
|
958
|
+
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)) {
|
|
959
|
+
rememberRelation(match[1], match[2], 'depends_on', 'project', 'project');
|
|
960
|
+
}
|
|
961
|
+
for (const match of text.matchAll(/([A-Za-z][\w .'-]{1,50})\s+reports to\s+([A-Za-z0-9][\w .'-]{1,50})/gi)) {
|
|
962
|
+
rememberRelation(match[1], match[2], 'reports_to', 'person', 'person');
|
|
963
|
+
}
|
|
964
|
+
for (const match of text.matchAll(/([A-Za-z][\w .'-]{1,50})\s+uses\s+([A-Za-z0-9][\w .'-]{1,40})/gi)) {
|
|
965
|
+
const subject = match[1];
|
|
966
|
+
const tool = match[2];
|
|
967
|
+
const lowered = subject.toLowerCase();
|
|
968
|
+
let subjectKind = 'person';
|
|
969
|
+
let subjectName = subject;
|
|
970
|
+
if (lowered.includes('team')) {
|
|
971
|
+
subjectKind = 'team';
|
|
972
|
+
subjectName = subject.replace(/team/gi, '').trim();
|
|
973
|
+
}
|
|
974
|
+
else if (lowered.includes('project')) {
|
|
975
|
+
subjectKind = 'project';
|
|
976
|
+
subjectName = subject.replace(/project/gi, '').trim();
|
|
977
|
+
}
|
|
978
|
+
rememberEntity('tool', tool);
|
|
979
|
+
rememberRelation(subjectName, tool, 'uses', subjectKind, 'tool');
|
|
980
|
+
}
|
|
981
|
+
if (tasks.length > 0) {
|
|
982
|
+
try {
|
|
983
|
+
await Promise.all(tasks);
|
|
984
|
+
}
|
|
985
|
+
catch {
|
|
986
|
+
// Best-effort; context capture should never block responses.
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
normalizeEntityName(name) {
|
|
991
|
+
return name.replace(/["'`]/g, '').replace(/\s+/g, ' ').trim();
|
|
992
|
+
}
|
|
993
|
+
isIgnoredEntity(name) {
|
|
994
|
+
if (!name || name.length < 2 || name.length > 80)
|
|
995
|
+
return true;
|
|
996
|
+
const lower = name.toLowerCase();
|
|
997
|
+
const ignored = new Set(['i', 'we', 'you', 'they', 'he', 'she', 'it', 'our', 'my', 'your', 'their']);
|
|
998
|
+
return ignored.has(lower);
|
|
999
|
+
}
|
|
515
1000
|
/**
|
|
516
1001
|
* Run the agentic loop
|
|
517
1002
|
* @param apiKey Optional per-request API key (creates temporary client)
|
|
@@ -521,17 +1006,28 @@ ${hubContextStr}
|
|
|
521
1006
|
let currentMessages = [...messages];
|
|
522
1007
|
let iterations = 0;
|
|
523
1008
|
const maxIterations = 30; // Increased for complex multi-tool tasks
|
|
1009
|
+
const requestTimeoutMs = 60000;
|
|
524
1010
|
// Use per-request client if apiKey provided, otherwise use default
|
|
525
1011
|
const client = apiKey ? new Anthropic({ apiKey }) : this.client;
|
|
526
1012
|
while (iterations < maxIterations) {
|
|
527
1013
|
iterations++;
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
1014
|
+
const requestStart = Date.now();
|
|
1015
|
+
console.log(`[ORCHESTRATOR] Anthropic request ${iterations}/${maxIterations} (model: ${this.model})`);
|
|
1016
|
+
const response = await Promise.race([
|
|
1017
|
+
client.messages.create({
|
|
1018
|
+
model: this.model,
|
|
1019
|
+
max_tokens: this.maxTokens,
|
|
1020
|
+
system,
|
|
1021
|
+
tools,
|
|
1022
|
+
messages: currentMessages
|
|
1023
|
+
}),
|
|
1024
|
+
new Promise((_, reject) => {
|
|
1025
|
+
setTimeout(() => {
|
|
1026
|
+
reject(new Error(`Anthropic request timed out after ${requestTimeoutMs}ms`));
|
|
1027
|
+
}, requestTimeoutMs);
|
|
1028
|
+
})
|
|
1029
|
+
]);
|
|
1030
|
+
console.log(`[ORCHESTRATOR] Anthropic response ${iterations}/${maxIterations} in ${Date.now() - requestStart}ms`);
|
|
535
1031
|
// Check if done
|
|
536
1032
|
if (response.stop_reason === 'end_turn' || !this.hasToolUse(response.content)) {
|
|
537
1033
|
return this.extractText(response.content);
|
|
@@ -576,6 +1072,87 @@ For ANY file creation, editing, or system modification, use delegate_to_worker i
|
|
|
576
1072
|
required: ['command']
|
|
577
1073
|
}
|
|
578
1074
|
},
|
|
1075
|
+
{
|
|
1076
|
+
name: 'calendar_upsert_events',
|
|
1077
|
+
description: `Create or update calendar events for /cal.
|
|
1078
|
+
|
|
1079
|
+
Writes ONLY to ~/Lia-Hub/shared/outputs/.lia/calendar.json (creates the directory/file if missing).`,
|
|
1080
|
+
input_schema: {
|
|
1081
|
+
type: 'object',
|
|
1082
|
+
properties: {
|
|
1083
|
+
events: {
|
|
1084
|
+
type: 'array',
|
|
1085
|
+
items: {
|
|
1086
|
+
type: 'object',
|
|
1087
|
+
properties: {
|
|
1088
|
+
id: { type: 'string' },
|
|
1089
|
+
title: { type: 'string' },
|
|
1090
|
+
date: { type: 'string', description: 'YYYY-MM-DD' },
|
|
1091
|
+
startTime: { type: 'string', description: 'HH:MM (24h) or ISO timestamp' },
|
|
1092
|
+
endTime: { type: 'string', description: 'HH:MM (24h) or ISO timestamp' },
|
|
1093
|
+
calendarId: { type: 'string' },
|
|
1094
|
+
notes: { type: 'string' }
|
|
1095
|
+
},
|
|
1096
|
+
required: ['title']
|
|
1097
|
+
}
|
|
1098
|
+
},
|
|
1099
|
+
calendars: {
|
|
1100
|
+
type: 'array',
|
|
1101
|
+
items: {
|
|
1102
|
+
type: 'object',
|
|
1103
|
+
properties: {
|
|
1104
|
+
id: { type: 'string' },
|
|
1105
|
+
name: { type: 'string' },
|
|
1106
|
+
color: { type: 'string' }
|
|
1107
|
+
},
|
|
1108
|
+
required: ['id', 'name']
|
|
1109
|
+
},
|
|
1110
|
+
description: 'Optional calendar definitions; omitted to keep existing.'
|
|
1111
|
+
}
|
|
1112
|
+
},
|
|
1113
|
+
required: ['events']
|
|
1114
|
+
}
|
|
1115
|
+
},
|
|
1116
|
+
{
|
|
1117
|
+
name: 'todo_upsert_items',
|
|
1118
|
+
description: `Create or update todo items for /todo.
|
|
1119
|
+
|
|
1120
|
+
Writes ONLY to ~/Lia-Hub/shared/outputs/.lia/todos.json (creates the directory/file if missing).`,
|
|
1121
|
+
input_schema: {
|
|
1122
|
+
type: 'object',
|
|
1123
|
+
properties: {
|
|
1124
|
+
items: {
|
|
1125
|
+
type: 'array',
|
|
1126
|
+
items: {
|
|
1127
|
+
type: 'object',
|
|
1128
|
+
properties: {
|
|
1129
|
+
id: { type: 'string' },
|
|
1130
|
+
title: { type: 'string' },
|
|
1131
|
+
done: { type: 'boolean' },
|
|
1132
|
+
due: { type: 'string' },
|
|
1133
|
+
priority: { type: 'string', enum: ['low', 'medium', 'high'] },
|
|
1134
|
+
notes: { type: 'string' },
|
|
1135
|
+
subtasks: {
|
|
1136
|
+
type: 'array',
|
|
1137
|
+
items: {
|
|
1138
|
+
type: 'object',
|
|
1139
|
+
properties: {
|
|
1140
|
+
id: { type: 'string' },
|
|
1141
|
+
title: { type: 'string' },
|
|
1142
|
+
done: { type: 'boolean' },
|
|
1143
|
+
notes: { type: 'string' }
|
|
1144
|
+
},
|
|
1145
|
+
required: ['title']
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
},
|
|
1149
|
+
required: ['title']
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
},
|
|
1153
|
+
required: ['items']
|
|
1154
|
+
}
|
|
1155
|
+
},
|
|
579
1156
|
// Web tools
|
|
580
1157
|
{
|
|
581
1158
|
name: 'web_search',
|
|
@@ -759,6 +1336,110 @@ Be specific about what you want done.`,
|
|
|
759
1336
|
required: ['content']
|
|
760
1337
|
}
|
|
761
1338
|
},
|
|
1339
|
+
{
|
|
1340
|
+
name: 'remember_entity',
|
|
1341
|
+
description: 'Store or update a team context entity (person, team, project, tool, task, doc).',
|
|
1342
|
+
input_schema: {
|
|
1343
|
+
type: 'object',
|
|
1344
|
+
properties: {
|
|
1345
|
+
kind: {
|
|
1346
|
+
type: 'string',
|
|
1347
|
+
enum: ContextGraphKinds.entities,
|
|
1348
|
+
description: 'Entity kind'
|
|
1349
|
+
},
|
|
1350
|
+
name: {
|
|
1351
|
+
type: 'string',
|
|
1352
|
+
description: 'Entity name'
|
|
1353
|
+
},
|
|
1354
|
+
description: {
|
|
1355
|
+
type: 'string',
|
|
1356
|
+
description: 'Short description or role'
|
|
1357
|
+
},
|
|
1358
|
+
attributes: {
|
|
1359
|
+
type: 'object',
|
|
1360
|
+
description: 'Optional key/value attributes',
|
|
1361
|
+
additionalProperties: { type: 'string' }
|
|
1362
|
+
},
|
|
1363
|
+
tags: {
|
|
1364
|
+
type: 'array',
|
|
1365
|
+
items: { type: 'string' },
|
|
1366
|
+
description: 'Optional tags'
|
|
1367
|
+
},
|
|
1368
|
+
scope: {
|
|
1369
|
+
type: 'string',
|
|
1370
|
+
enum: ['user', 'org'],
|
|
1371
|
+
description: 'Where to store the entity (default: user)'
|
|
1372
|
+
}
|
|
1373
|
+
},
|
|
1374
|
+
required: ['kind', 'name']
|
|
1375
|
+
}
|
|
1376
|
+
},
|
|
1377
|
+
{
|
|
1378
|
+
name: 'remember_relation',
|
|
1379
|
+
description: 'Link two entities with a relationship (depends_on, works_on, member_of, etc).',
|
|
1380
|
+
input_schema: {
|
|
1381
|
+
type: 'object',
|
|
1382
|
+
properties: {
|
|
1383
|
+
from: {
|
|
1384
|
+
type: 'string',
|
|
1385
|
+
description: 'Entity name or key'
|
|
1386
|
+
},
|
|
1387
|
+
to: {
|
|
1388
|
+
type: 'string',
|
|
1389
|
+
description: 'Entity name or key'
|
|
1390
|
+
},
|
|
1391
|
+
type: {
|
|
1392
|
+
type: 'string',
|
|
1393
|
+
enum: ContextGraphKinds.relations,
|
|
1394
|
+
description: 'Relationship type'
|
|
1395
|
+
},
|
|
1396
|
+
from_kind: {
|
|
1397
|
+
type: 'string',
|
|
1398
|
+
enum: ContextGraphKinds.entities,
|
|
1399
|
+
description: 'Optional kind hint for from'
|
|
1400
|
+
},
|
|
1401
|
+
to_kind: {
|
|
1402
|
+
type: 'string',
|
|
1403
|
+
enum: ContextGraphKinds.entities,
|
|
1404
|
+
description: 'Optional kind hint for to'
|
|
1405
|
+
},
|
|
1406
|
+
description: {
|
|
1407
|
+
type: 'string',
|
|
1408
|
+
description: 'Optional relation detail'
|
|
1409
|
+
},
|
|
1410
|
+
scope: {
|
|
1411
|
+
type: 'string',
|
|
1412
|
+
enum: ['user', 'org'],
|
|
1413
|
+
description: 'Where to store the relation (default: user)'
|
|
1414
|
+
}
|
|
1415
|
+
},
|
|
1416
|
+
required: ['from', 'to', 'type']
|
|
1417
|
+
}
|
|
1418
|
+
},
|
|
1419
|
+
{
|
|
1420
|
+
name: 'search_context',
|
|
1421
|
+
description: 'Search the context graph for entities and relationships.',
|
|
1422
|
+
input_schema: {
|
|
1423
|
+
type: 'object',
|
|
1424
|
+
properties: {
|
|
1425
|
+
query: {
|
|
1426
|
+
type: 'string',
|
|
1427
|
+
description: 'What to search for'
|
|
1428
|
+
},
|
|
1429
|
+
kind: {
|
|
1430
|
+
type: 'string',
|
|
1431
|
+
enum: ContextGraphKinds.entities,
|
|
1432
|
+
description: 'Optional entity kind filter'
|
|
1433
|
+
},
|
|
1434
|
+
scope: {
|
|
1435
|
+
type: 'string',
|
|
1436
|
+
enum: ['user', 'org'],
|
|
1437
|
+
description: 'Which context scope to search (default: user)'
|
|
1438
|
+
}
|
|
1439
|
+
},
|
|
1440
|
+
required: ['query']
|
|
1441
|
+
}
|
|
1442
|
+
},
|
|
762
1443
|
{
|
|
763
1444
|
name: 'search_memory',
|
|
764
1445
|
description: 'Search your memory for relevant information. Use before delegating to include user context.',
|
|
@@ -882,6 +1563,12 @@ Be specific about what you want done.`,
|
|
|
882
1563
|
case 'bash':
|
|
883
1564
|
result = await this.bashTool.execute(input.command, input.timeout);
|
|
884
1565
|
break;
|
|
1566
|
+
case 'calendar_upsert_events':
|
|
1567
|
+
result = this.calendarTools.upsertCalendarEvents(input.events, input.calendars);
|
|
1568
|
+
break;
|
|
1569
|
+
case 'todo_upsert_items':
|
|
1570
|
+
result = this.calendarTools.upsertTodoItems(input.items);
|
|
1571
|
+
break;
|
|
885
1572
|
case 'web_search':
|
|
886
1573
|
result = await this.webTools.webSearch(input.query, { numResults: input.num_results });
|
|
887
1574
|
break;
|
|
@@ -892,12 +1579,14 @@ Be specific about what you want done.`,
|
|
|
892
1579
|
});
|
|
893
1580
|
break;
|
|
894
1581
|
// Worker tools
|
|
895
|
-
case 'spawn_worker':
|
|
896
|
-
|
|
1582
|
+
case 'spawn_worker': {
|
|
1583
|
+
const { task } = this.withAttachments(input.task);
|
|
1584
|
+
result = await this.workerTools.spawnWorker(task, {
|
|
897
1585
|
timeout: input.timeout,
|
|
898
1586
|
priority: input.priority
|
|
899
1587
|
});
|
|
900
1588
|
break;
|
|
1589
|
+
}
|
|
901
1590
|
case 'check_worker':
|
|
902
1591
|
result = await this.workerTools.checkWorker(input.job_id);
|
|
903
1592
|
break;
|
|
@@ -911,12 +1600,68 @@ Be specific about what you want done.`,
|
|
|
911
1600
|
result = await this.workerTools.cancelWorker(input.job_id);
|
|
912
1601
|
break;
|
|
913
1602
|
// Legacy delegate tool
|
|
914
|
-
case 'delegate_to_worker':
|
|
915
|
-
|
|
1603
|
+
case 'delegate_to_worker': {
|
|
1604
|
+
const { task, context } = this.withAttachments(input.task, input.context);
|
|
1605
|
+
result = await this.delegateToWorker(task, context, input.working_directory);
|
|
916
1606
|
break;
|
|
1607
|
+
}
|
|
917
1608
|
case 'remember':
|
|
918
1609
|
result = await this.executeRemember(input.content, input.type, input.importance);
|
|
919
1610
|
break;
|
|
1611
|
+
case 'remember_entity': {
|
|
1612
|
+
const scope = input.scope === 'org' ? 'org' : 'user';
|
|
1613
|
+
const graph = scope === 'org' ? this.orgContextGraph : this.contextGraph;
|
|
1614
|
+
if (!graph) {
|
|
1615
|
+
result = { success: false, output: 'Org context is not configured for this agent.' };
|
|
1616
|
+
break;
|
|
1617
|
+
}
|
|
1618
|
+
const entity = await graph.upsertEntity({
|
|
1619
|
+
kind: input.kind,
|
|
1620
|
+
name: input.name,
|
|
1621
|
+
description: input.description,
|
|
1622
|
+
attributes: input.attributes,
|
|
1623
|
+
tags: input.tags
|
|
1624
|
+
});
|
|
1625
|
+
result = { success: true, output: `Stored ${scope} entity: ${entity.kind} ${entity.name} (${entity.key})` };
|
|
1626
|
+
break;
|
|
1627
|
+
}
|
|
1628
|
+
case 'remember_relation': {
|
|
1629
|
+
const scope = input.scope === 'org' ? 'org' : 'user';
|
|
1630
|
+
const graph = scope === 'org' ? this.orgContextGraph : this.contextGraph;
|
|
1631
|
+
if (!graph) {
|
|
1632
|
+
result = { success: false, output: 'Org context is not configured for this agent.' };
|
|
1633
|
+
break;
|
|
1634
|
+
}
|
|
1635
|
+
const relation = await graph.linkEntities({
|
|
1636
|
+
from: input.from,
|
|
1637
|
+
to: input.to,
|
|
1638
|
+
type: input.type,
|
|
1639
|
+
description: input.description,
|
|
1640
|
+
fromKind: input.from_kind,
|
|
1641
|
+
toKind: input.to_kind
|
|
1642
|
+
});
|
|
1643
|
+
result = { success: true, output: `Linked ${scope}: ${relation.from} ${relation.type} ${relation.to}` };
|
|
1644
|
+
break;
|
|
1645
|
+
}
|
|
1646
|
+
case 'search_context': {
|
|
1647
|
+
const scope = input.scope === 'org' ? 'org' : 'user';
|
|
1648
|
+
const graph = scope === 'org' ? this.orgContextGraph : this.contextGraph;
|
|
1649
|
+
if (!graph) {
|
|
1650
|
+
result = { success: false, output: 'Org context is not configured for this agent.' };
|
|
1651
|
+
break;
|
|
1652
|
+
}
|
|
1653
|
+
const entities = await graph.searchEntities(input.query, { kind: input.kind, limit: 6 });
|
|
1654
|
+
if (entities.length === 0) {
|
|
1655
|
+
result = { success: true, output: 'No context entities found.' };
|
|
1656
|
+
break;
|
|
1657
|
+
}
|
|
1658
|
+
const lines = entities.map(entity => {
|
|
1659
|
+
const desc = entity.description ? ` - ${entity.description}` : '';
|
|
1660
|
+
return `[${entity.kind}] ${entity.name}${desc} (${entity.key})`;
|
|
1661
|
+
});
|
|
1662
|
+
result = { success: true, output: lines.join('\n') };
|
|
1663
|
+
break;
|
|
1664
|
+
}
|
|
920
1665
|
case 'search_memory':
|
|
921
1666
|
result = await this.executeSearchMemory(input.query, input.type);
|
|
922
1667
|
break;
|
|
@@ -999,10 +1744,31 @@ Be specific about what you want done.`,
|
|
|
999
1744
|
};
|
|
1000
1745
|
// Escape single quotes in prompt for shell safety
|
|
1001
1746
|
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
|
1747
|
+
const claudeCmd = `${this.claudePath} -p '${escapedPrompt}' --output-format text --dangerously-skip-permissions`;
|
|
1748
|
+
const isRoot = process.getuid?.() === 0;
|
|
1749
|
+
const shellCmd = isRoot
|
|
1750
|
+
? `runuser -u lia -m -- /bin/bash -l -c "${claudeCmd.replace(/"/g, '\\"')}"`
|
|
1751
|
+
: claudeCmd;
|
|
1752
|
+
let workerUser = process.env.USER || 'lia';
|
|
1753
|
+
let workerLogname = process.env.LOGNAME || workerUser;
|
|
1754
|
+
if (isRoot) {
|
|
1755
|
+
workerUser = 'lia';
|
|
1756
|
+
workerLogname = 'lia';
|
|
1757
|
+
}
|
|
1758
|
+
const spawnEnv = {
|
|
1759
|
+
...process.env,
|
|
1760
|
+
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
|
|
1761
|
+
GH_TOKEN: process.env.GH_TOKEN,
|
|
1762
|
+
GITHUB_TOKEN: process.env.GITHUB_TOKEN,
|
|
1763
|
+
HOME: cwd,
|
|
1764
|
+
LIA_WORKSPACE: cwd,
|
|
1765
|
+
USER: workerUser,
|
|
1766
|
+
LOGNAME: workerLogname
|
|
1767
|
+
};
|
|
1002
1768
|
// Spawn worker
|
|
1003
|
-
const child = spawn('/bin/bash', ['-l', '-c',
|
|
1769
|
+
const child = spawn('/bin/bash', ['-l', '-c', shellCmd], {
|
|
1004
1770
|
cwd,
|
|
1005
|
-
env:
|
|
1771
|
+
env: spawnEnv,
|
|
1006
1772
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
1007
1773
|
});
|
|
1008
1774
|
job.process = child;
|
|
@@ -1024,12 +1790,18 @@ Be specific about what you want done.`,
|
|
|
1024
1790
|
job.output += text;
|
|
1025
1791
|
// Stream output to console
|
|
1026
1792
|
process.stdout.write(`[${id}] ${text}`);
|
|
1793
|
+
if (this.workerLogCallback) {
|
|
1794
|
+
this.workerLogCallback(id, text, 'stdout');
|
|
1795
|
+
}
|
|
1027
1796
|
});
|
|
1028
1797
|
child.stderr?.on('data', (data) => {
|
|
1029
1798
|
const text = data.toString();
|
|
1030
1799
|
if (!text.includes('Checking') && !text.includes('Connected')) {
|
|
1031
1800
|
job.output += text;
|
|
1032
1801
|
}
|
|
1802
|
+
if (this.workerLogCallback) {
|
|
1803
|
+
this.workerLogCallback(id, text, 'stderr');
|
|
1804
|
+
}
|
|
1033
1805
|
});
|
|
1034
1806
|
child.on('close', async (code) => {
|
|
1035
1807
|
clearTimeout(timeout);
|
|
@@ -1196,9 +1968,24 @@ Be specific about what you want done.`,
|
|
|
1196
1968
|
}
|
|
1197
1969
|
/**
|
|
1198
1970
|
* Execute remember tool
|
|
1971
|
+
* Routes writes based on instanceMode:
|
|
1972
|
+
* - personal: write to personal memory only
|
|
1973
|
+
* - team: write to team memory only
|
|
1199
1974
|
*/
|
|
1200
1975
|
async executeRemember(content, type, importance) {
|
|
1201
1976
|
try {
|
|
1977
|
+
// Team mode: write to org memory only
|
|
1978
|
+
if (this.instanceMode === 'team') {
|
|
1979
|
+
if (!this.orgMemory) {
|
|
1980
|
+
return { success: false, output: 'Team memory not configured. Cannot store team memory.' };
|
|
1981
|
+
}
|
|
1982
|
+
const id = await this.orgMemory.remember(content, {
|
|
1983
|
+
type: type || undefined,
|
|
1984
|
+
importance
|
|
1985
|
+
});
|
|
1986
|
+
return { success: true, output: `Remembered in team memory (${id})` };
|
|
1987
|
+
}
|
|
1988
|
+
// Personal mode: write to personal memory only
|
|
1202
1989
|
const id = await this.memory.remember(content, {
|
|
1203
1990
|
type: type || undefined,
|
|
1204
1991
|
importance
|
|
@@ -1211,17 +1998,53 @@ Be specific about what you want done.`,
|
|
|
1211
1998
|
}
|
|
1212
1999
|
/**
|
|
1213
2000
|
* Execute search memory tool
|
|
2001
|
+
* Routes searches based on instanceMode:
|
|
2002
|
+
* - personal: search personal + all team memories
|
|
2003
|
+
* - team: search team memory only (never personal)
|
|
1214
2004
|
*/
|
|
1215
2005
|
async executeSearchMemory(query, type) {
|
|
1216
2006
|
try {
|
|
1217
|
-
const
|
|
2007
|
+
const searchOpts = {
|
|
1218
2008
|
type: type || undefined,
|
|
1219
2009
|
limit: 10
|
|
1220
|
-
}
|
|
1221
|
-
|
|
2010
|
+
};
|
|
2011
|
+
// Team mode: search only team memory
|
|
2012
|
+
if (this.instanceMode === 'team') {
|
|
2013
|
+
if (!this.orgMemory) {
|
|
2014
|
+
return { success: false, output: 'Team memory not configured.' };
|
|
2015
|
+
}
|
|
2016
|
+
const memories = await this.orgMemory.search(query, searchOpts);
|
|
2017
|
+
if (memories.length === 0) {
|
|
2018
|
+
return { success: true, output: 'No relevant team memories found.' };
|
|
2019
|
+
}
|
|
2020
|
+
const output = memories.map(m => `[team:${m.type}] (${(m.importance * 100).toFixed(0)}%) ${m.content}`).join('\n\n');
|
|
2021
|
+
return { success: true, output };
|
|
2022
|
+
}
|
|
2023
|
+
// Personal mode: search personal + all team memories
|
|
2024
|
+
const allMemories = [];
|
|
2025
|
+
// Search personal memories
|
|
2026
|
+
const personalMemories = await this.memory.search(query, { ...searchOpts, limit: 5 });
|
|
2027
|
+
for (const m of personalMemories) {
|
|
2028
|
+
allMemories.push({ source: 'personal', memory: m });
|
|
2029
|
+
}
|
|
2030
|
+
// Search all team memories user belongs to
|
|
2031
|
+
for (const [orgId, orgMem] of this.userOrgMemories) {
|
|
2032
|
+
try {
|
|
2033
|
+
const teamMems = await orgMem.search(query, { ...searchOpts, limit: 3 });
|
|
2034
|
+
for (const m of teamMems) {
|
|
2035
|
+
allMemories.push({ source: `team:${orgId}`, memory: m });
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
catch {
|
|
2039
|
+
// Skip failed team memory searches
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
if (allMemories.length === 0) {
|
|
1222
2043
|
return { success: true, output: 'No relevant memories found.' };
|
|
1223
2044
|
}
|
|
1224
|
-
|
|
2045
|
+
// Sort by importance and format
|
|
2046
|
+
allMemories.sort((a, b) => b.memory.importance - a.memory.importance);
|
|
2047
|
+
const output = allMemories.map(({ source, memory: m }) => `[${source}:${m.type}] (${(m.importance * 100).toFixed(0)}%) ${m.content}`).join('\n\n');
|
|
1225
2048
|
return { success: true, output };
|
|
1226
2049
|
}
|
|
1227
2050
|
catch (error) {
|
|
@@ -1396,6 +2219,12 @@ Be specific about what you want done.`,
|
|
|
1396
2219
|
return `Delegating: ${input.task.slice(0, 60)}...`;
|
|
1397
2220
|
case 'remember':
|
|
1398
2221
|
return `Remembering: ${input.content.slice(0, 50)}...`;
|
|
2222
|
+
case 'remember_entity':
|
|
2223
|
+
return `Remembering entity: ${input.name}`;
|
|
2224
|
+
case 'remember_relation':
|
|
2225
|
+
return `Linking context: ${input.from} -> ${input.type} -> ${input.to}`;
|
|
2226
|
+
case 'search_context':
|
|
2227
|
+
return `Searching context: ${input.query}`;
|
|
1399
2228
|
case 'search_memory':
|
|
1400
2229
|
return `Searching memory: ${input.query}`;
|
|
1401
2230
|
case 'schedule_task':
|
|
@@ -1438,5 +2267,24 @@ Be specific about what you want done.`,
|
|
|
1438
2267
|
else {
|
|
1439
2268
|
this.memory.close();
|
|
1440
2269
|
}
|
|
2270
|
+
if (this.orgMemory) {
|
|
2271
|
+
if (this.orgMemory instanceof PostgresMemoryStore) {
|
|
2272
|
+
await this.orgMemory.close();
|
|
2273
|
+
}
|
|
2274
|
+
else {
|
|
2275
|
+
this.orgMemory.close();
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
// Close all user org memory stores (for personal mode reading team memories)
|
|
2279
|
+
for (const [, orgMem] of this.userOrgMemories) {
|
|
2280
|
+
if (orgMem instanceof PostgresMemoryStore) {
|
|
2281
|
+
await orgMem.close();
|
|
2282
|
+
}
|
|
2283
|
+
else {
|
|
2284
|
+
orgMem.close();
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
this.userOrgMemories.clear();
|
|
2288
|
+
this.userOrgContextGraphs.clear();
|
|
1441
2289
|
}
|
|
1442
2290
|
}
|