@siftd/connect-agent 0.2.38 → 0.2.40

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.
@@ -20,6 +20,7 @@ import { SharedState } from './workers/shared-state.js';
20
20
  import { getKnowledgeForPrompt } from './genesis/index.js';
21
21
  import { loadHubContext, formatHubContext, logAction, logWorker } from './core/hub.js';
22
22
  import { buildWorkerPrompt } from './prompts/worker-system.js';
23
+ import { LiaTaskQueue } from './core/task-queue.js';
23
24
  /**
24
25
  * Extract file paths from worker output
25
26
  * Workers naturally mention files they create: "Created /tmp/foo.html", "Saved to /path/file"
@@ -74,6 +75,21 @@ YOUR IDENTITY:
74
75
  HOW YOU WORK:
75
76
  You are the orchestrator, not the executor. You coordinate and remember while workers do the hands-on work.
76
77
 
78
+ CRITICAL - DATE AWARENESS:
79
+ ALWAYS run \`date\` via bash FIRST before referencing ANY temporal context from memory.
80
+ - Before mentioning meetings, deadlines, or scheduled items: check today's date
81
+ - Cross-reference memory timestamps with current date
82
+ - Only surface RELEVANT, CURRENT information - never reference past events as if they're upcoming
83
+ - A personal assistant that gives outdated information is worse than useless - it's actively misleading
84
+
85
+ TASK QUEUE (Always-Listening):
86
+ You have an internal task queue. Users can keep sending messages while you work - you're interruptible.
87
+ - Use lia_plan to break down complex goals into steps
88
+ - Use lia_add_task to queue follow-up work or new requests that come in
89
+ - Use lia_get_queue to see what's pending
90
+ - When you receive multiple requests, acknowledge them and queue them: "Got it, adding to my queue..."
91
+ - You can work on one task while others wait - always stay responsive
92
+
77
93
  TOOL RULES:
78
94
 
79
95
  ✅ bash - READ-ONLY operations only:
@@ -141,6 +157,23 @@ CALENDAR + TODO (Lia-managed data):
141
157
  - calendar.json: { "version": 1, "calendars": [...], "events": [...] }
142
158
  - todos.json: { "version": 1, "items": [...] }
143
159
 
160
+ COMPLEX TODO/CALENDAR OPERATIONS:
161
+ When users ask to "break down", "split", or "parse" a todo item:
162
+ 1. READ THE NOTES FIELD - the TODO SNAPSHOT includes notes with full content
163
+ 2. Parse the notes into distinct actionable items (grants, meetings, tasks, etc.)
164
+ 3. Call todo_upsert_items with MULTIPLE new items extracted from the notes
165
+ 4. Mark the original item as done: true OR delete by not including it in upsert
166
+ 5. Each new item should have: id (unique), title, priority, notes (if details needed), due (if deadline mentioned)
167
+
168
+ Example: If a todo has notes listing "NIH R01 deadline Jan 25, AASLD pilot Feb, ADA Mar":
169
+ → Create 3 separate todos: one for NIH R01 (due: 2026-01-25), one for AASLD, one for ADA
170
+ → Extract deadlines, descriptions, requirements from the original notes
171
+
172
+ When users reference a todo by number (e.g., "#3", "item 3", "the third one"):
173
+ - Match it to the items in the TODO SNAPSHOT (which is ordered)
174
+ - READ its notes field carefully - that's where the real content lives
175
+ - Don't guess what it contains - the snapshot shows you exactly what's there
176
+
144
177
  WORKFLOW:
145
178
  Before complex work: Check CLAUDE.md → Read LANDMARKS.md → Search memory
146
179
  After completing work: Update LANDMARKS.md → Remember learnings
@@ -179,9 +212,16 @@ export class MasterOrchestrator {
179
212
  calendarTools;
180
213
  sharedState;
181
214
  verboseMode = new Map(); // per-user verbose mode
215
+ // Lia's internal task queue - allows async message processing
216
+ taskQueue;
217
+ taskUpdateCallback;
218
+ planUpdateCallback;
219
+ // Lia's visible todo list (displayed inline in chat)
220
+ currentTodos = [];
221
+ todoUpdateCallback;
182
222
  constructor(options) {
183
223
  this.client = new Anthropic({ apiKey: options.apiKey });
184
- this.model = options.model || 'claude-sonnet-4-20250514';
224
+ this.model = options.model || process.env.ANTHROPIC_MODEL || 'claude-sonnet-4-20250514';
185
225
  this.maxTokens = options.maxTokens || 4096;
186
226
  this.userId = options.userId;
187
227
  this.orgId = options.orgId;
@@ -261,6 +301,12 @@ export class MasterOrchestrator {
261
301
  this.workerLogCallback(workerId, line, stream);
262
302
  }
263
303
  });
304
+ // Initialize Lia's internal task queue
305
+ this.taskQueue = new LiaTaskQueue({
306
+ onTaskUpdate: (task) => this.taskUpdateCallback?.(task),
307
+ onPlanUpdate: (plan) => this.planUpdateCallback?.(plan),
308
+ processTask: async (task) => this.processQueuedTask(task),
309
+ });
264
310
  }
265
311
  /**
266
312
  * Initialize the orchestrator - indexes filesystem on first call
@@ -318,6 +364,80 @@ export class MasterOrchestrator {
318
364
  this.broadcastGalleryUpdate();
319
365
  } : null);
320
366
  }
367
+ /**
368
+ * Set callback for task queue updates (for real-time UI updates)
369
+ */
370
+ setTaskUpdateCallback(callback) {
371
+ this.taskUpdateCallback = callback || undefined;
372
+ }
373
+ /**
374
+ * Set callback for plan updates (for real-time UI updates)
375
+ */
376
+ setPlanUpdateCallback(callback) {
377
+ this.planUpdateCallback = callback || undefined;
378
+ }
379
+ /**
380
+ * Set callback for visible todo list updates (displayed inline in chat)
381
+ */
382
+ setTodoUpdateCallback(callback) {
383
+ this.todoUpdateCallback = callback || undefined;
384
+ }
385
+ /**
386
+ * Queue a message for async processing (non-blocking)
387
+ * Returns immediately with task ID - use callbacks or getTaskQueueStatus for results
388
+ */
389
+ queueMessage(message, options) {
390
+ return this.taskQueue.addTask({
391
+ type: 'user_message',
392
+ content: message,
393
+ priority: options?.priority || 'normal',
394
+ metadata: {
395
+ userId: this.userId,
396
+ messageId: options?.messageId,
397
+ context: this.instanceMode,
398
+ orgId: this.orgId,
399
+ },
400
+ });
401
+ }
402
+ /**
403
+ * Process a task from the queue (called by LiaTaskQueue)
404
+ */
405
+ async processQueuedTask(task) {
406
+ // This is where the actual work happens
407
+ return this.processMessage(task.content, [], // Fresh conversation for each task
408
+ undefined // No sendMessage callback for queued tasks
409
+ );
410
+ }
411
+ /**
412
+ * Get task queue status (for UI)
413
+ */
414
+ getTaskQueueStatus() {
415
+ return this.taskQueue.getStatus();
416
+ }
417
+ /**
418
+ * Get pending tasks
419
+ */
420
+ getPendingTasks() {
421
+ return this.taskQueue.getPendingTasks();
422
+ }
423
+ /**
424
+ * Get task by ID
425
+ */
426
+ getTask(taskId) {
427
+ return this.taskQueue.getTask(taskId);
428
+ }
429
+ /**
430
+ * Cancel a pending task
431
+ */
432
+ cancelTask(taskId) {
433
+ return this.taskQueue.cancelTask(taskId);
434
+ }
435
+ /**
436
+ * Get Lia's internal todo list formatted for context
437
+ */
438
+ getTaskQueueContext() {
439
+ return this.taskQueue.formatAsTodoList();
440
+ }
321
441
  /**
322
442
  * Get gallery workers with their assets
323
443
  */
@@ -429,6 +549,22 @@ export class MasterOrchestrator {
429
549
  if (hasRunning || workers.length > 0) {
430
550
  this.workerStatusCallback(workers);
431
551
  }
552
+ // Update linked todos with worker progress
553
+ if (this.currentTodos.length > 0 && this.todoUpdateCallback) {
554
+ let todoUpdated = false;
555
+ for (const todo of this.currentTodos) {
556
+ if (todo.workerId && todo.status === 'in_progress') {
557
+ const worker = workers.find(w => w.id === todo.workerId);
558
+ if (worker && todo.progress !== worker.progress) {
559
+ todo.progress = worker.progress;
560
+ todoUpdated = true;
561
+ }
562
+ }
563
+ }
564
+ if (todoUpdated) {
565
+ this.todoUpdateCallback(this.currentTodos);
566
+ }
567
+ }
432
568
  // Clean up completed jobs after 3 seconds
433
569
  const now = Date.now();
434
570
  for (const [id, job] of this.jobs) {
@@ -548,9 +684,8 @@ export class MasterOrchestrator {
548
684
  }
549
685
  /**
550
686
  * Process a user message
551
- * @param apiKey Optional per-request API key (overrides default)
552
687
  */
553
- async processMessage(message, conversationHistory = [], sendMessage, apiKey) {
688
+ async processMessage(message, conversationHistory = [], sendMessage) {
554
689
  // Handle slash commands first
555
690
  const slashResponse = this.handleSlashCommand(message);
556
691
  if (slashResponse) {
@@ -598,7 +733,7 @@ ${hubContextStr}
598
733
  { role: 'user', content: message }
599
734
  ];
600
735
  try {
601
- const response = await this.runAgentLoop(messages, systemWithContext, sendMessage, apiKey);
736
+ const response = await this.runAgentLoop(messages, systemWithContext, sendMessage);
602
737
  // Auto-remember important things from the conversation
603
738
  await this.autoRemember(message, response);
604
739
  void this.autoCaptureContext(message);
@@ -999,22 +1134,19 @@ ${hubContextStr}
999
1134
  }
1000
1135
  /**
1001
1136
  * Run the agentic loop
1002
- * @param apiKey Optional per-request API key (creates temporary client)
1003
1137
  */
1004
- async runAgentLoop(messages, system, sendMessage, apiKey) {
1138
+ async runAgentLoop(messages, system, sendMessage) {
1005
1139
  const tools = this.getToolDefinitions();
1006
1140
  let currentMessages = [...messages];
1007
1141
  let iterations = 0;
1008
1142
  const maxIterations = 30; // Increased for complex multi-tool tasks
1009
1143
  const requestTimeoutMs = 60000;
1010
- // Use per-request client if apiKey provided, otherwise use default
1011
- const client = apiKey ? new Anthropic({ apiKey }) : this.client;
1012
1144
  while (iterations < maxIterations) {
1013
1145
  iterations++;
1014
1146
  const requestStart = Date.now();
1015
1147
  console.log(`[ORCHESTRATOR] Anthropic request ${iterations}/${maxIterations} (model: ${this.model})`);
1016
1148
  const response = await Promise.race([
1017
- client.messages.create({
1149
+ this.client.messages.create({
1018
1150
  model: this.model,
1019
1151
  max_tokens: this.maxTokens,
1020
1152
  system,
@@ -1117,7 +1249,18 @@ Writes ONLY to ~/Lia-Hub/shared/outputs/.lia/calendar.json (creates the director
1117
1249
  name: 'todo_upsert_items',
1118
1250
  description: `Create or update todo items for /todo.
1119
1251
 
1120
- Writes ONLY to ~/Lia-Hub/shared/outputs/.lia/todos.json (creates the directory/file if missing).`,
1252
+ Writes ONLY to ~/Lia-Hub/shared/outputs/.lia/todos.json (creates the directory/file if missing).
1253
+
1254
+ UPSERT BEHAVIOR:
1255
+ - Items with matching 'id' are REPLACED (use this to update or mark done)
1256
+ - Items with new 'id' are ADDED
1257
+ - To "delete" an item, set done: true or exclude it when replacing
1258
+
1259
+ BREAKING DOWN A TODO:
1260
+ When asked to split/parse a todo into multiple items:
1261
+ 1. Include the original item with done: true (marks it complete)
1262
+ 2. Add the new parsed items with unique ids
1263
+ 3. Extract deadlines, priorities, and notes from the original content`,
1121
1264
  input_schema: {
1122
1265
  type: 'object',
1123
1266
  properties: {
@@ -1540,6 +1683,85 @@ Be specific about what you want done.`,
1540
1683
  },
1541
1684
  required: ['port']
1542
1685
  }
1686
+ },
1687
+ // Lia's internal task management tools
1688
+ {
1689
+ name: 'lia_plan',
1690
+ 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.`,
1691
+ input_schema: {
1692
+ type: 'object',
1693
+ properties: {
1694
+ goal: {
1695
+ type: 'string',
1696
+ description: 'The overall goal you are trying to accomplish'
1697
+ },
1698
+ steps: {
1699
+ type: 'array',
1700
+ items: { type: 'string' },
1701
+ description: 'List of steps to accomplish the goal'
1702
+ }
1703
+ },
1704
+ required: ['goal', 'steps']
1705
+ }
1706
+ },
1707
+ {
1708
+ name: 'lia_add_task',
1709
+ 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.`,
1710
+ input_schema: {
1711
+ type: 'object',
1712
+ properties: {
1713
+ task: {
1714
+ type: 'string',
1715
+ description: 'The task description'
1716
+ },
1717
+ priority: {
1718
+ type: 'string',
1719
+ enum: ['urgent', 'high', 'normal', 'low'],
1720
+ description: 'Task priority (default: normal)'
1721
+ }
1722
+ },
1723
+ required: ['task']
1724
+ }
1725
+ },
1726
+ {
1727
+ name: 'lia_get_queue',
1728
+ description: 'Get your current task queue status. Shows what you are working on and what is pending.',
1729
+ input_schema: {
1730
+ type: 'object',
1731
+ properties: {},
1732
+ required: []
1733
+ }
1734
+ },
1735
+ {
1736
+ name: 'lia_todo_write',
1737
+ description: `Update your visible todo list displayed inline in the chat. Use this to show the user what you're working on.
1738
+
1739
+ Unlike lia_plan (internal only), this creates a VISIBLE todo list that appears in the chat conversation.
1740
+ - Use for multi-step tasks to show real-time progress
1741
+ - Update status as you complete each item
1742
+ - Only have ONE item as in_progress at a time
1743
+ - Link workerId to show actual worker progress percentage`,
1744
+ input_schema: {
1745
+ type: 'object',
1746
+ properties: {
1747
+ todos: {
1748
+ type: 'array',
1749
+ items: {
1750
+ type: 'object',
1751
+ properties: {
1752
+ id: { type: 'string', description: 'Unique identifier for the todo (optional, auto-generated if not provided)' },
1753
+ content: { type: 'string', description: 'Imperative form: "Run tests", "Build component"' },
1754
+ activeForm: { type: 'string', description: 'Present continuous: "Running tests", "Building component"' },
1755
+ status: { type: 'string', enum: ['pending', 'in_progress', 'completed'] },
1756
+ workerId: { type: 'string', description: 'Optional: link to a spawned worker ID for real progress tracking' }
1757
+ },
1758
+ required: ['content', 'activeForm', 'status']
1759
+ },
1760
+ description: 'The full todo list (replaces existing list)'
1761
+ }
1762
+ },
1763
+ required: ['todos']
1764
+ }
1543
1765
  }
1544
1766
  ];
1545
1767
  }
@@ -1683,6 +1905,19 @@ Be specific about what you want done.`,
1683
1905
  case 'stop_local_server':
1684
1906
  result = await this.executeStopServer(input.port);
1685
1907
  break;
1908
+ // Lia's internal task management tools
1909
+ case 'lia_plan':
1910
+ result = this.executeLiaPlan(input.goal, input.steps);
1911
+ break;
1912
+ case 'lia_add_task':
1913
+ result = this.executeLiaAddTask(input.task, input.priority);
1914
+ break;
1915
+ case 'lia_get_queue':
1916
+ result = this.executeLiaGetQueue();
1917
+ break;
1918
+ case 'lia_todo_write':
1919
+ result = this.executeLiaTodoWrite(input.todos);
1920
+ break;
1686
1921
  default:
1687
1922
  result = { success: false, output: `Unknown tool: ${toolUse.name}` };
1688
1923
  }
@@ -2186,6 +2421,102 @@ Be specific about what you want done.`,
2186
2421
  return { success: false, output: `No server running on port ${port}` };
2187
2422
  }
2188
2423
  }
2424
+ /**
2425
+ * Create a plan with steps (Lia's internal planning)
2426
+ */
2427
+ executeLiaPlan(goal, steps) {
2428
+ try {
2429
+ const plan = this.taskQueue.createPlan(goal, steps);
2430
+ console.log(`[ORCHESTRATOR] Created plan: ${plan.id} with ${steps.length} steps`);
2431
+ const stepsFormatted = steps.map((s, i) => `${i + 1}. ${s}`).join('\n');
2432
+ return {
2433
+ success: true,
2434
+ output: `Created plan "${goal}" with ${steps.length} steps:\n${stepsFormatted}\n\nPlan ID: ${plan.id}`
2435
+ };
2436
+ }
2437
+ catch (error) {
2438
+ return { success: false, output: `Failed to create plan: ${error}` };
2439
+ }
2440
+ }
2441
+ /**
2442
+ * Add a task to Lia's internal queue
2443
+ */
2444
+ executeLiaAddTask(task, priority) {
2445
+ try {
2446
+ const newTask = this.taskQueue.addTask({
2447
+ type: 'follow_up',
2448
+ content: task,
2449
+ priority: priority || 'normal',
2450
+ metadata: {
2451
+ userId: this.userId,
2452
+ context: this.instanceMode,
2453
+ orgId: this.orgId,
2454
+ },
2455
+ });
2456
+ console.log(`[ORCHESTRATOR] Added task to queue: ${newTask.id}`);
2457
+ const status = this.taskQueue.getStatus();
2458
+ return {
2459
+ success: true,
2460
+ output: `Added to my queue: "${task}" (priority: ${priority || 'normal'}). ${status.pendingCount} tasks pending.`
2461
+ };
2462
+ }
2463
+ catch (error) {
2464
+ return { success: false, output: `Failed to add task: ${error}` };
2465
+ }
2466
+ }
2467
+ /**
2468
+ * Get Lia's task queue status
2469
+ */
2470
+ executeLiaGetQueue() {
2471
+ try {
2472
+ const status = this.taskQueue.getStatus();
2473
+ const todoList = this.taskQueue.formatAsTodoList();
2474
+ let output = todoList;
2475
+ if (!status.isProcessing && status.pendingCount === 0) {
2476
+ output = 'My queue is empty - ready for new tasks.';
2477
+ }
2478
+ return { success: true, output };
2479
+ }
2480
+ catch (error) {
2481
+ return { success: false, output: `Failed to get queue: ${error}` };
2482
+ }
2483
+ }
2484
+ /**
2485
+ * Update visible todo list (displayed inline in chat)
2486
+ */
2487
+ executeLiaTodoWrite(todos) {
2488
+ try {
2489
+ // Assign IDs if missing
2490
+ this.currentTodos = todos.map((todo, i) => ({
2491
+ ...todo,
2492
+ id: todo.id || `todo_${Date.now()}_${i}`,
2493
+ }));
2494
+ // Update progress from linked workers
2495
+ const allWorkers = this.getWorkerStatus();
2496
+ for (const todo of this.currentTodos) {
2497
+ if (todo.workerId && todo.status === 'in_progress') {
2498
+ const workerStatus = allWorkers.find(w => w.id === todo.workerId);
2499
+ if (workerStatus) {
2500
+ todo.progress = workerStatus.progress;
2501
+ }
2502
+ }
2503
+ }
2504
+ // Broadcast to UI
2505
+ this.todoUpdateCallback?.(this.currentTodos);
2506
+ const counts = {
2507
+ pending: this.currentTodos.filter(t => t.status === 'pending').length,
2508
+ inProgress: this.currentTodos.filter(t => t.status === 'in_progress').length,
2509
+ completed: this.currentTodos.filter(t => t.status === 'completed').length,
2510
+ };
2511
+ return {
2512
+ success: true,
2513
+ output: `Todo list updated: ${counts.completed}/${this.currentTodos.length} completed, ${counts.inProgress} in progress`
2514
+ };
2515
+ }
2516
+ catch (error) {
2517
+ return { success: false, output: `Failed to update todo list: ${error}` };
2518
+ }
2519
+ }
2189
2520
  /**
2190
2521
  * Format tool preview for user
2191
2522
  */
@@ -2235,6 +2566,14 @@ Be specific about what you want done.`,
2235
2566
  return `Starting server on port ${input.port || 8080}...`;
2236
2567
  case 'stop_local_server':
2237
2568
  return `Stopping server on port ${input.port}...`;
2569
+ case 'lia_plan':
2570
+ return `Planning: ${input.goal}`;
2571
+ case 'lia_add_task':
2572
+ return `Queuing task: ${input.task.slice(0, 50)}...`;
2573
+ case 'lia_get_queue':
2574
+ return 'Checking my task queue...';
2575
+ case 'lia_todo_write':
2576
+ return null; // Don't show preview - the todo list itself is the UI
2238
2577
  default:
2239
2578
  return null;
2240
2579
  }
@@ -6,7 +6,7 @@
6
6
  * - Streams responses as they're generated
7
7
  * - Supports interruption and progress updates
8
8
  */
9
- import type { WorkerStatus } from './orchestrator.js';
9
+ import type { WorkerStatus, LiaTodoItem } from './orchestrator.js';
10
10
  import { AssetResponse } from './core/asset-api.js';
11
11
  export type MessageHandler = (message: WebSocketMessage) => Promise<void>;
12
12
  export type StreamHandler = (chunk: string) => void;
@@ -15,7 +15,6 @@ export interface WebSocketMessage {
15
15
  id?: string;
16
16
  content?: string;
17
17
  timestamp?: number;
18
- apiKey?: string;
19
18
  requestId?: string;
20
19
  assetId?: string;
21
20
  groupId?: string;
@@ -67,6 +66,10 @@ export declare class AgentWebSocket {
67
66
  * Send workers status update for progress bars
68
67
  */
69
68
  sendWorkersUpdate(workers: WorkerStatus[]): void;
69
+ /**
70
+ * Send todo list update for inline display in chat
71
+ */
72
+ sendTodoUpdate(todos: LiaTodoItem[]): void;
70
73
  /**
71
74
  * Send gallery command (AI controls the gallery)
72
75
  */
package/dist/websocket.js CHANGED
@@ -169,6 +169,16 @@ export class AgentWebSocket {
169
169
  workers
170
170
  });
171
171
  }
172
+ /**
173
+ * Send todo list update for inline display in chat
174
+ */
175
+ sendTodoUpdate(todos) {
176
+ this.sendToServer({
177
+ type: 'lia_todo_update',
178
+ todos,
179
+ timestamp: Date.now()
180
+ });
181
+ }
172
182
  /**
173
183
  * Send gallery command (AI controls the gallery)
174
184
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siftd/connect-agent",
3
- "version": "0.2.38",
3
+ "version": "0.2.40",
4
4
  "description": "Master orchestrator agent - control Claude Code remotely via web",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",