@madh-io/alfred-ai 0.7.0 → 0.7.1

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.
Files changed (2) hide show
  1. package/bundle/index.js +279 -17
  2. package/package.json +1 -1
package/bundle/index.js CHANGED
@@ -2526,20 +2526,120 @@ var init_skill_registry = __esm({
2526
2526
  });
2527
2527
 
2528
2528
  // ../skills/dist/skill-sandbox.js
2529
- var DEFAULT_TIMEOUT_MS, SkillSandbox;
2529
+ var DEFAULT_TIMEOUT_MS, INACTIVITY_THRESHOLD_MS, POLL_INTERVAL_MS, MAX_TOTAL_TIME_MS, SkillSandbox;
2530
2530
  var init_skill_sandbox = __esm({
2531
2531
  "../skills/dist/skill-sandbox.js"() {
2532
2532
  "use strict";
2533
2533
  DEFAULT_TIMEOUT_MS = 3e4;
2534
+ INACTIVITY_THRESHOLD_MS = 12e4;
2535
+ POLL_INTERVAL_MS = 1e4;
2536
+ MAX_TOTAL_TIME_MS = 20 * 6e4;
2534
2537
  SkillSandbox = class {
2535
2538
  logger;
2536
2539
  constructor(logger) {
2537
2540
  this.logger = logger;
2538
2541
  }
2539
- async execute(skill, input2, context, timeoutMs) {
2542
+ /**
2543
+ * Execute a skill with timeout protection.
2544
+ *
2545
+ * If an ActivityTracker is provided, uses an inactivity-based timeout:
2546
+ * the skill keeps running as long as the tracker receives pings.
2547
+ * Only kills the skill when it goes silent for INACTIVITY_THRESHOLD_MS.
2548
+ *
2549
+ * Without a tracker, falls back to a simple hard timeout.
2550
+ */
2551
+ async execute(skill, input2, context, timeoutMs, tracker) {
2540
2552
  timeoutMs = timeoutMs ?? skill.metadata.timeoutMs ?? DEFAULT_TIMEOUT_MS;
2541
2553
  const { name } = skill.metadata;
2542
2554
  this.logger.info({ skill: name, input: input2 }, "Skill execution started");
2555
+ if (tracker) {
2556
+ return this.executeWithTracker(skill, input2, context, name, timeoutMs, tracker);
2557
+ }
2558
+ return this.executeWithHardTimeout(skill, input2, context, name, timeoutMs);
2559
+ }
2560
+ /**
2561
+ * Activity-aware timeout: polls the tracker and only kills
2562
+ * the skill when it has been inactive for too long.
2563
+ */
2564
+ async executeWithTracker(skill, input2, context, name, initialTimeoutMs, tracker) {
2565
+ return new Promise((resolve) => {
2566
+ let settled = false;
2567
+ let pollTimer;
2568
+ let safetyTimer;
2569
+ let initialTimer;
2570
+ const cleanup = () => {
2571
+ if (pollTimer)
2572
+ clearInterval(pollTimer);
2573
+ if (safetyTimer)
2574
+ clearTimeout(safetyTimer);
2575
+ if (initialTimer)
2576
+ clearTimeout(initialTimer);
2577
+ };
2578
+ const finish = (result) => {
2579
+ if (settled)
2580
+ return;
2581
+ settled = true;
2582
+ cleanup();
2583
+ resolve(result);
2584
+ };
2585
+ skill.execute(input2, context).then((result) => {
2586
+ this.logger.info({ skill: name, success: result.success }, "Skill execution completed");
2587
+ finish(result);
2588
+ }, (error) => {
2589
+ const message = error instanceof Error ? error.message : String(error);
2590
+ this.logger.error({ skill: name, error: message }, "Skill execution failed");
2591
+ finish({ success: false, error: message });
2592
+ });
2593
+ initialTimer = setTimeout(() => {
2594
+ if (settled)
2595
+ return;
2596
+ const idleMs = tracker.getIdleMs();
2597
+ if (idleMs >= INACTIVITY_THRESHOLD_MS) {
2598
+ const snapshot2 = tracker.getSnapshot();
2599
+ this.logger.warn({ skill: name, idleMs, state: snapshot2.state, iteration: snapshot2.iteration }, "Agent inactive after initial timeout \u2014 aborting");
2600
+ finish({
2601
+ success: false,
2602
+ error: `Skill "${name}" timed out \u2014 inactive for ${Math.round(idleMs / 1e3)}s (last state: ${snapshot2.state})`
2603
+ });
2604
+ return;
2605
+ }
2606
+ const snapshot = tracker.getSnapshot();
2607
+ this.logger.info({ skill: name, idleMs, state: snapshot.state, iteration: snapshot.iteration, totalMs: snapshot.totalElapsedMs }, "Initial timeout reached but agent is active \u2014 extending");
2608
+ pollTimer = setInterval(() => {
2609
+ if (settled) {
2610
+ cleanup();
2611
+ return;
2612
+ }
2613
+ const currentIdleMs = tracker.getIdleMs();
2614
+ const snap = tracker.getSnapshot();
2615
+ if (currentIdleMs >= INACTIVITY_THRESHOLD_MS) {
2616
+ this.logger.warn({ skill: name, idleMs: currentIdleMs, state: snap.state, iteration: snap.iteration, totalMs: snap.totalElapsedMs }, "Agent went inactive \u2014 aborting");
2617
+ finish({
2618
+ success: false,
2619
+ error: `Skill "${name}" killed \u2014 inactive for ${Math.round(currentIdleMs / 1e3)}s (last state: ${snap.state})`
2620
+ });
2621
+ } else {
2622
+ this.logger.debug({ skill: name, idleMs: currentIdleMs, state: snap.state, iteration: snap.iteration }, "Agent still active, continuing...");
2623
+ }
2624
+ }, POLL_INTERVAL_MS);
2625
+ }, initialTimeoutMs);
2626
+ safetyTimer = setTimeout(() => {
2627
+ if (settled)
2628
+ return;
2629
+ const snap = tracker.getSnapshot();
2630
+ this.logger.error({ skill: name, totalMs: snap.totalElapsedMs, state: snap.state, iteration: snap.iteration }, "Absolute time limit reached \u2014 force killing agent");
2631
+ finish({
2632
+ success: false,
2633
+ error: `Skill "${name}" force-killed after ${Math.round(MAX_TOTAL_TIME_MS / 6e4)} minutes (safety limit)`
2634
+ });
2635
+ }, MAX_TOTAL_TIME_MS);
2636
+ });
2637
+ }
2638
+ /**
2639
+ * Simple hard timeout for skills that don't use a tracker.
2640
+ * This is the legacy behavior.
2641
+ */
2642
+ async executeWithHardTimeout(skill, input2, context, name, timeoutMs) {
2543
2643
  try {
2544
2644
  const result = await Promise.race([
2545
2645
  skill.execute(input2, context),
@@ -2562,6 +2662,90 @@ var init_skill_sandbox = __esm({
2562
2662
  }
2563
2663
  });
2564
2664
 
2665
+ // ../skills/dist/activity-tracker.js
2666
+ var ActivityTracker;
2667
+ var init_activity_tracker = __esm({
2668
+ "../skills/dist/activity-tracker.js"() {
2669
+ "use strict";
2670
+ ActivityTracker = class {
2671
+ state = "starting";
2672
+ iteration = 0;
2673
+ maxIterations = 0;
2674
+ currentTool;
2675
+ lastPingAt;
2676
+ startedAt;
2677
+ history = [];
2678
+ onProgress;
2679
+ constructor(onProgress) {
2680
+ this.startedAt = Date.now();
2681
+ this.lastPingAt = Date.now();
2682
+ this.onProgress = onProgress;
2683
+ }
2684
+ /**
2685
+ * Called by the agent at every meaningful step.
2686
+ * Resets the inactivity timer and reports status upward.
2687
+ */
2688
+ ping(state, meta) {
2689
+ this.state = state;
2690
+ this.lastPingAt = Date.now();
2691
+ if (meta?.iteration !== void 0)
2692
+ this.iteration = meta.iteration;
2693
+ if (meta?.maxIterations !== void 0)
2694
+ this.maxIterations = meta.maxIterations;
2695
+ this.currentTool = meta?.tool;
2696
+ this.history.push({
2697
+ state,
2698
+ tool: meta?.tool,
2699
+ iteration: this.iteration,
2700
+ timestamp: this.lastPingAt
2701
+ });
2702
+ if (this.onProgress) {
2703
+ this.onProgress(this.formatStatus());
2704
+ }
2705
+ }
2706
+ /** How long since last activity, in ms. */
2707
+ getIdleMs() {
2708
+ return Date.now() - this.lastPingAt;
2709
+ }
2710
+ /** Total elapsed time since agent started. */
2711
+ getTotalElapsedMs() {
2712
+ return Date.now() - this.startedAt;
2713
+ }
2714
+ /** Current human-readable status string. */
2715
+ formatStatus() {
2716
+ const iter = this.maxIterations > 0 ? ` (${this.iteration}/${this.maxIterations})` : "";
2717
+ switch (this.state) {
2718
+ case "starting":
2719
+ return `Sub-agent starting...`;
2720
+ case "llm_call":
2721
+ return `Sub-agent thinking...${iter}`;
2722
+ case "tool_call":
2723
+ return this.currentTool ? `Sub-agent using ${this.currentTool}${iter}` : `Sub-agent using tool...${iter}`;
2724
+ case "processing":
2725
+ return `Sub-agent processing...${iter}`;
2726
+ case "done":
2727
+ return `Sub-agent done${iter}`;
2728
+ default:
2729
+ return `Sub-agent working...${iter}`;
2730
+ }
2731
+ }
2732
+ /** Full snapshot for logging / debugging. */
2733
+ getSnapshot() {
2734
+ return {
2735
+ state: this.state,
2736
+ iteration: this.iteration,
2737
+ maxIterations: this.maxIterations,
2738
+ lastPingAt: this.lastPingAt,
2739
+ idleMs: this.getIdleMs(),
2740
+ currentTool: this.currentTool,
2741
+ totalElapsedMs: this.getTotalElapsedMs(),
2742
+ history: [...this.history]
2743
+ };
2744
+ }
2745
+ };
2746
+ }
2747
+ });
2748
+
2565
2749
  // ../skills/dist/plugin-loader.js
2566
2750
  import fs3 from "node:fs";
2567
2751
  import path3 from "node:path";
@@ -3576,12 +3760,15 @@ ${results.map((r) => `- ${r.key}: "${r.value}" (score: ${r.score.toFixed(2)})`).
3576
3760
  });
3577
3761
 
3578
3762
  // ../skills/dist/built-in/delegate.js
3579
- var MAX_SUB_AGENT_ITERATIONS, DelegateSkill;
3763
+ var DEFAULT_MAX_ITERATIONS, MAX_ALLOWED_ITERATIONS, INITIAL_TIMEOUT_MS, DelegateSkill;
3580
3764
  var init_delegate = __esm({
3581
3765
  "../skills/dist/built-in/delegate.js"() {
3582
3766
  "use strict";
3583
3767
  init_skill();
3584
- MAX_SUB_AGENT_ITERATIONS = 5;
3768
+ init_activity_tracker();
3769
+ DEFAULT_MAX_ITERATIONS = 5;
3770
+ MAX_ALLOWED_ITERATIONS = 15;
3771
+ INITIAL_TIMEOUT_MS = 12e4;
3585
3772
  DelegateSkill = class extends Skill {
3586
3773
  llm;
3587
3774
  skillRegistry;
@@ -3589,11 +3776,10 @@ var init_delegate = __esm({
3589
3776
  securityManager;
3590
3777
  metadata = {
3591
3778
  name: "delegate",
3592
- description: 'Delegate a complex sub-task to an autonomous sub-agent that has full tool access. The sub-agent can use shell, web search, calculator, memory, email, and all other tools. Use when a task is independent enough to run in parallel or when it requires a focused, multi-step workflow (e.g. "research X and summarize", "find all TODO files and list them", "check the weather and draft a packing list"). The sub-agent runs up to 5 tool iterations autonomously.',
3779
+ description: 'Delegate a complex sub-task to an autonomous sub-agent that has full tool access. The sub-agent can use shell, web search, calculator, memory, email, and all other tools. Use when a task is independent enough to run in parallel or when it requires a focused, multi-step workflow (e.g. "research X and summarize", "find all TODO files and list them", "check the weather and draft a packing list"). Control depth with max_iterations (default 5, max 15).',
3593
3780
  riskLevel: "write",
3594
- version: "2.0.0",
3595
- timeoutMs: 12e4,
3596
- // 2 minutes — delegate chains multiple LLM calls + tool executions
3781
+ version: "3.0.0",
3782
+ timeoutMs: INITIAL_TIMEOUT_MS,
3597
3783
  inputSchema: {
3598
3784
  type: "object",
3599
3785
  properties: {
@@ -3604,11 +3790,16 @@ var init_delegate = __esm({
3604
3790
  context: {
3605
3791
  type: "string",
3606
3792
  description: "Additional context the sub-agent needs (optional)"
3793
+ },
3794
+ max_iterations: {
3795
+ type: "number",
3796
+ description: "Max tool iterations (1-15). Use higher values for complex multi-step tasks. Default: 5."
3607
3797
  }
3608
3798
  },
3609
3799
  required: ["task"]
3610
3800
  }
3611
3801
  };
3802
+ onProgress;
3612
3803
  constructor(llm, skillRegistry, skillSandbox, securityManager) {
3613
3804
  super();
3614
3805
  this.llm = llm;
@@ -3616,6 +3807,21 @@ var init_delegate = __esm({
3616
3807
  this.skillSandbox = skillSandbox;
3617
3808
  this.securityManager = securityManager;
3618
3809
  }
3810
+ /**
3811
+ * Set a progress callback before execution.
3812
+ * The pipeline calls this so the user sees live status updates
3813
+ * like "Sub-agent using web_search (2/5)".
3814
+ */
3815
+ setProgressCallback(cb) {
3816
+ this.onProgress = cb;
3817
+ }
3818
+ /**
3819
+ * Create an ActivityTracker for this execution.
3820
+ * The sandbox uses this to decide whether to extend or kill.
3821
+ */
3822
+ createTracker() {
3823
+ return new ActivityTracker(this.onProgress);
3824
+ }
3619
3825
  async execute(input2, context) {
3620
3826
  const task = input2.task;
3621
3827
  const additionalContext = input2.context;
@@ -3625,6 +3831,10 @@ var init_delegate = __esm({
3625
3831
  error: 'Missing required field "task"'
3626
3832
  };
3627
3833
  }
3834
+ const requestedIterations = input2.max_iterations;
3835
+ const maxIterations = requestedIterations ? Math.max(1, Math.min(MAX_ALLOWED_ITERATIONS, Math.round(requestedIterations))) : DEFAULT_MAX_ITERATIONS;
3836
+ const tracker = new ActivityTracker(this.onProgress);
3837
+ tracker.ping("starting", { maxIterations });
3628
3838
  const tools = this.buildSubAgentTools();
3629
3839
  const systemPrompt = "You are a sub-agent of Alfred, a personal AI assistant. Complete the assigned task using the tools available to you. Work step by step: use tools to gather information, then synthesize a clear result. Be concise and return only the final answer when done.";
3630
3840
  let userContent = task;
@@ -3641,6 +3851,7 @@ Additional context: ${additionalContext}`;
3641
3851
  let totalInputTokens = 0;
3642
3852
  let totalOutputTokens = 0;
3643
3853
  while (true) {
3854
+ tracker.ping("llm_call", { iteration, maxIterations });
3644
3855
  const response = await this.llm.complete({
3645
3856
  messages,
3646
3857
  system: systemPrompt,
@@ -3649,7 +3860,9 @@ Additional context: ${additionalContext}`;
3649
3860
  });
3650
3861
  totalInputTokens += response.usage.inputTokens;
3651
3862
  totalOutputTokens += response.usage.outputTokens;
3652
- if (!response.toolCalls || response.toolCalls.length === 0 || iteration >= MAX_SUB_AGENT_ITERATIONS) {
3863
+ tracker.ping("processing", { iteration, maxIterations });
3864
+ if (!response.toolCalls || response.toolCalls.length === 0 || iteration >= maxIterations) {
3865
+ tracker.ping("done", { iteration, maxIterations });
3653
3866
  return {
3654
3867
  success: true,
3655
3868
  data: {
@@ -3676,6 +3889,7 @@ Additional context: ${additionalContext}`;
3676
3889
  messages.push({ role: "assistant", content: assistantContent });
3677
3890
  const toolResultBlocks = [];
3678
3891
  for (const toolCall of response.toolCalls) {
3892
+ tracker.ping("tool_call", { iteration, maxIterations, tool: toolCall.name });
3679
3893
  const result = await this.executeSubAgentTool(toolCall, context);
3680
3894
  toolResultBlocks.push({
3681
3895
  type: "tool_result",
@@ -5673,6 +5887,7 @@ var init_dist6 = __esm({
5673
5887
  init_skill();
5674
5888
  init_skill_registry();
5675
5889
  init_skill_sandbox();
5890
+ init_activity_tracker();
5676
5891
  init_plugin_loader();
5677
5892
  init_calculator();
5678
5893
  init_system_info();
@@ -5746,6 +5961,9 @@ var init_message_pipeline = __esm({
5746
5961
  speechTranscriber;
5747
5962
  inboxPath;
5748
5963
  embeddingService;
5964
+ /** Registry of currently running delegate agents, keyed by a unique agent ID. */
5965
+ activeAgents = /* @__PURE__ */ new Map();
5966
+ agentIdCounter = 0;
5749
5967
  constructor(options) {
5750
5968
  this.llm = options.llm;
5751
5969
  this.conversationManager = options.conversationManager;
@@ -5806,11 +6024,15 @@ var init_message_pipeline = __esm({
5806
6024
  }
5807
6025
  const skillMetas = this.skillRegistry ? this.skillRegistry.getAll().map((s) => s.metadata) : void 0;
5808
6026
  const tools = skillMetas ? this.promptBuilder.buildTools(skillMetas) : void 0;
5809
- const system = this.promptBuilder.buildSystemPrompt({
6027
+ let system = this.promptBuilder.buildSystemPrompt({
5810
6028
  memories,
5811
6029
  skills: skillMetas,
5812
6030
  userProfile
5813
6031
  });
6032
+ const agentStatusBlock = this.buildActiveAgentStatus();
6033
+ if (agentStatusBlock) {
6034
+ system += "\n\n" + agentStatusBlock;
6035
+ }
5814
6036
  const allMessages = this.promptBuilder.buildMessages(history);
5815
6037
  const userContent = await this.buildUserContent(message, onProgress);
5816
6038
  allMessages.push({ role: "user", content: userContent });
@@ -5855,7 +6077,7 @@ var init_message_pipeline = __esm({
5855
6077
  chatType: message.chatType,
5856
6078
  platform: message.platform,
5857
6079
  conversationId: conversation.id
5858
- });
6080
+ }, onProgress);
5859
6081
  toolResultBlocks.push({
5860
6082
  type: "tool_result",
5861
6083
  tool_use_id: toolCall.id,
@@ -5885,7 +6107,7 @@ var init_message_pipeline = __esm({
5885
6107
  throw error;
5886
6108
  }
5887
6109
  }
5888
- async executeToolCall(toolCall, context) {
6110
+ async executeToolCall(toolCall, context, onProgress) {
5889
6111
  const skill = this.skillRegistry?.get(toolCall.name);
5890
6112
  if (!skill) {
5891
6113
  this.logger.warn({ tool: toolCall.name }, "Unknown skill requested");
@@ -5909,11 +6131,33 @@ var init_message_pipeline = __esm({
5909
6131
  }
5910
6132
  }
5911
6133
  if (this.skillSandbox) {
5912
- const result = await this.skillSandbox.execute(skill, toolCall.input, context);
5913
- return {
5914
- content: result.display ?? (result.success ? JSON.stringify(result.data) : result.error ?? "Unknown error"),
5915
- isError: !result.success
5916
- };
6134
+ let tracker;
6135
+ let agentId;
6136
+ if (toolCall.name === "delegate" && "setProgressCallback" in skill && "createTracker" in skill) {
6137
+ const delegateSkill = skill;
6138
+ if (onProgress) {
6139
+ delegateSkill.setProgressCallback(onProgress);
6140
+ }
6141
+ tracker = delegateSkill.createTracker();
6142
+ agentId = `agent-${++this.agentIdCounter}`;
6143
+ this.activeAgents.set(agentId, {
6144
+ chatId: context.chatId,
6145
+ task: String(toolCall.input.task ?? "").slice(0, 200),
6146
+ tracker,
6147
+ startedAt: Date.now()
6148
+ });
6149
+ }
6150
+ try {
6151
+ const result = await this.skillSandbox.execute(skill, toolCall.input, context, void 0, tracker);
6152
+ return {
6153
+ content: result.display ?? (result.success ? JSON.stringify(result.data) : result.error ?? "Unknown error"),
6154
+ isError: !result.success
6155
+ };
6156
+ } finally {
6157
+ if (agentId) {
6158
+ this.activeAgents.delete(agentId);
6159
+ }
6160
+ }
5917
6161
  }
5918
6162
  try {
5919
6163
  const result = await skill.execute(toolCall.input, context);
@@ -5966,6 +6210,24 @@ var init_message_pipeline = __esm({
5966
6210
  return `Using ${toolName}...`;
5967
6211
  }
5968
6212
  }
6213
+ /**
6214
+ * Build a status block describing currently running delegate agents.
6215
+ * Injected into the system prompt so the LLM can answer user questions
6216
+ * like "What is the agent doing right now?".
6217
+ */
6218
+ buildActiveAgentStatus() {
6219
+ if (this.activeAgents.size === 0)
6220
+ return void 0;
6221
+ const lines = ["## Currently running sub-agents"];
6222
+ for (const [id, agent] of this.activeAgents) {
6223
+ const snapshot = agent.tracker.getSnapshot();
6224
+ const elapsedSec = Math.round(snapshot.totalElapsedMs / 1e3);
6225
+ lines.push(`- **${id}**: "${agent.task}"`, ` Status: ${agent.tracker.formatStatus()}`, ` Running for ${elapsedSec}s | Last activity ${Math.round(snapshot.idleMs / 1e3)}s ago`);
6226
+ }
6227
+ lines.push("");
6228
+ lines.push("If the user asks what you or the agent is doing, describe the above status in natural language.");
6229
+ return lines.join("\n");
6230
+ }
5969
6231
  /**
5970
6232
  * Trim messages to fit within the LLM's context window.
5971
6233
  * Keeps the system prompt, the latest user message, and as many
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@madh-io/alfred-ai",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Alfred — Personal AI Assistant across Telegram, Discord, WhatsApp, Matrix & Signal",
5
5
  "type": "module",
6
6
  "bin": {