@madh-io/alfred-ai 0.7.0 → 0.7.2

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 +417 -31
  2. package/package.json +1 -1
package/bundle/index.js CHANGED
@@ -2035,6 +2035,29 @@ For complex tasks, work through multiple steps:
2035
2035
  - Documents: ${homeDir}/Documents
2036
2036
  - Desktop: ${homeDir}/Desktop
2037
2037
  - Downloads: ${homeDir}/Downloads`;
2038
+ const serverTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
2039
+ const effectiveTimezone = userProfile?.timezone || serverTimezone;
2040
+ const now = /* @__PURE__ */ new Date();
2041
+ const timeStr = now.toLocaleTimeString("en-GB", {
2042
+ timeZone: effectiveTimezone,
2043
+ hour: "2-digit",
2044
+ minute: "2-digit"
2045
+ });
2046
+ const dateStr = now.toLocaleDateString("en-CA", { timeZone: effectiveTimezone });
2047
+ const dayStr = now.toLocaleDateString("en-US", { timeZone: effectiveTimezone, weekday: "long" });
2048
+ prompt += `
2049
+
2050
+ ## Current date & time`;
2051
+ prompt += `
2052
+ - Timezone: ${effectiveTimezone}`;
2053
+ prompt += `
2054
+ - Date: ${dateStr} (${dayStr})`;
2055
+ prompt += `
2056
+ - Time: ${timeStr}`;
2057
+ if (userProfile?.timezone && userProfile.timezone !== serverTimezone) {
2058
+ prompt += `
2059
+ - Server timezone: ${serverTimezone}`;
2060
+ }
2038
2061
  if (skills && skills.length > 0) {
2039
2062
  prompt += "\n\n## Available tools\n";
2040
2063
  for (const s of skills) {
@@ -2049,13 +2072,8 @@ For complex tasks, work through multiple steps:
2049
2072
  - Name: ${userProfile.displayName}`;
2050
2073
  }
2051
2074
  if (userProfile.timezone) {
2052
- const now = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB", {
2053
- timeZone: userProfile.timezone,
2054
- hour: "2-digit",
2055
- minute: "2-digit"
2056
- });
2057
2075
  prompt += `
2058
- - Timezone: ${userProfile.timezone} (Current local time: ${now})`;
2076
+ - Timezone: ${userProfile.timezone}`;
2059
2077
  }
2060
2078
  if (userProfile.language) {
2061
2079
  prompt += `
@@ -2526,20 +2544,120 @@ var init_skill_registry = __esm({
2526
2544
  });
2527
2545
 
2528
2546
  // ../skills/dist/skill-sandbox.js
2529
- var DEFAULT_TIMEOUT_MS, SkillSandbox;
2547
+ var DEFAULT_TIMEOUT_MS, INACTIVITY_THRESHOLD_MS, POLL_INTERVAL_MS, MAX_TOTAL_TIME_MS, SkillSandbox;
2530
2548
  var init_skill_sandbox = __esm({
2531
2549
  "../skills/dist/skill-sandbox.js"() {
2532
2550
  "use strict";
2533
2551
  DEFAULT_TIMEOUT_MS = 3e4;
2552
+ INACTIVITY_THRESHOLD_MS = 12e4;
2553
+ POLL_INTERVAL_MS = 1e4;
2554
+ MAX_TOTAL_TIME_MS = 20 * 6e4;
2534
2555
  SkillSandbox = class {
2535
2556
  logger;
2536
2557
  constructor(logger) {
2537
2558
  this.logger = logger;
2538
2559
  }
2539
- async execute(skill, input2, context, timeoutMs) {
2560
+ /**
2561
+ * Execute a skill with timeout protection.
2562
+ *
2563
+ * If an ActivityTracker is provided, uses an inactivity-based timeout:
2564
+ * the skill keeps running as long as the tracker receives pings.
2565
+ * Only kills the skill when it goes silent for INACTIVITY_THRESHOLD_MS.
2566
+ *
2567
+ * Without a tracker, falls back to a simple hard timeout.
2568
+ */
2569
+ async execute(skill, input2, context, timeoutMs, tracker) {
2540
2570
  timeoutMs = timeoutMs ?? skill.metadata.timeoutMs ?? DEFAULT_TIMEOUT_MS;
2541
2571
  const { name } = skill.metadata;
2542
2572
  this.logger.info({ skill: name, input: input2 }, "Skill execution started");
2573
+ if (tracker) {
2574
+ return this.executeWithTracker(skill, input2, context, name, timeoutMs, tracker);
2575
+ }
2576
+ return this.executeWithHardTimeout(skill, input2, context, name, timeoutMs);
2577
+ }
2578
+ /**
2579
+ * Activity-aware timeout: polls the tracker and only kills
2580
+ * the skill when it has been inactive for too long.
2581
+ */
2582
+ async executeWithTracker(skill, input2, context, name, initialTimeoutMs, tracker) {
2583
+ return new Promise((resolve) => {
2584
+ let settled = false;
2585
+ let pollTimer;
2586
+ let safetyTimer;
2587
+ let initialTimer;
2588
+ const cleanup = () => {
2589
+ if (pollTimer)
2590
+ clearInterval(pollTimer);
2591
+ if (safetyTimer)
2592
+ clearTimeout(safetyTimer);
2593
+ if (initialTimer)
2594
+ clearTimeout(initialTimer);
2595
+ };
2596
+ const finish = (result) => {
2597
+ if (settled)
2598
+ return;
2599
+ settled = true;
2600
+ cleanup();
2601
+ resolve(result);
2602
+ };
2603
+ skill.execute(input2, context).then((result) => {
2604
+ this.logger.info({ skill: name, success: result.success }, "Skill execution completed");
2605
+ finish(result);
2606
+ }, (error) => {
2607
+ const message = error instanceof Error ? error.message : String(error);
2608
+ this.logger.error({ skill: name, error: message }, "Skill execution failed");
2609
+ finish({ success: false, error: message });
2610
+ });
2611
+ initialTimer = setTimeout(() => {
2612
+ if (settled)
2613
+ return;
2614
+ const idleMs = tracker.getIdleMs();
2615
+ if (idleMs >= INACTIVITY_THRESHOLD_MS) {
2616
+ const snapshot2 = tracker.getSnapshot();
2617
+ this.logger.warn({ skill: name, idleMs, state: snapshot2.state, iteration: snapshot2.iteration }, "Agent inactive after initial timeout \u2014 aborting");
2618
+ finish({
2619
+ success: false,
2620
+ error: `Skill "${name}" timed out \u2014 inactive for ${Math.round(idleMs / 1e3)}s (last state: ${snapshot2.state})`
2621
+ });
2622
+ return;
2623
+ }
2624
+ const snapshot = tracker.getSnapshot();
2625
+ this.logger.info({ skill: name, idleMs, state: snapshot.state, iteration: snapshot.iteration, totalMs: snapshot.totalElapsedMs }, "Initial timeout reached but agent is active \u2014 extending");
2626
+ pollTimer = setInterval(() => {
2627
+ if (settled) {
2628
+ cleanup();
2629
+ return;
2630
+ }
2631
+ const currentIdleMs = tracker.getIdleMs();
2632
+ const snap = tracker.getSnapshot();
2633
+ if (currentIdleMs >= INACTIVITY_THRESHOLD_MS) {
2634
+ this.logger.warn({ skill: name, idleMs: currentIdleMs, state: snap.state, iteration: snap.iteration, totalMs: snap.totalElapsedMs }, "Agent went inactive \u2014 aborting");
2635
+ finish({
2636
+ success: false,
2637
+ error: `Skill "${name}" killed \u2014 inactive for ${Math.round(currentIdleMs / 1e3)}s (last state: ${snap.state})`
2638
+ });
2639
+ } else {
2640
+ this.logger.debug({ skill: name, idleMs: currentIdleMs, state: snap.state, iteration: snap.iteration }, "Agent still active, continuing...");
2641
+ }
2642
+ }, POLL_INTERVAL_MS);
2643
+ }, initialTimeoutMs);
2644
+ safetyTimer = setTimeout(() => {
2645
+ if (settled)
2646
+ return;
2647
+ const snap = tracker.getSnapshot();
2648
+ this.logger.error({ skill: name, totalMs: snap.totalElapsedMs, state: snap.state, iteration: snap.iteration }, "Absolute time limit reached \u2014 force killing agent");
2649
+ finish({
2650
+ success: false,
2651
+ error: `Skill "${name}" force-killed after ${Math.round(MAX_TOTAL_TIME_MS / 6e4)} minutes (safety limit)`
2652
+ });
2653
+ }, MAX_TOTAL_TIME_MS);
2654
+ });
2655
+ }
2656
+ /**
2657
+ * Simple hard timeout for skills that don't use a tracker.
2658
+ * This is the legacy behavior.
2659
+ */
2660
+ async executeWithHardTimeout(skill, input2, context, name, timeoutMs) {
2543
2661
  try {
2544
2662
  const result = await Promise.race([
2545
2663
  skill.execute(input2, context),
@@ -2562,6 +2680,90 @@ var init_skill_sandbox = __esm({
2562
2680
  }
2563
2681
  });
2564
2682
 
2683
+ // ../skills/dist/activity-tracker.js
2684
+ var ActivityTracker;
2685
+ var init_activity_tracker = __esm({
2686
+ "../skills/dist/activity-tracker.js"() {
2687
+ "use strict";
2688
+ ActivityTracker = class {
2689
+ state = "starting";
2690
+ iteration = 0;
2691
+ maxIterations = 0;
2692
+ currentTool;
2693
+ lastPingAt;
2694
+ startedAt;
2695
+ history = [];
2696
+ onProgress;
2697
+ constructor(onProgress) {
2698
+ this.startedAt = Date.now();
2699
+ this.lastPingAt = Date.now();
2700
+ this.onProgress = onProgress;
2701
+ }
2702
+ /**
2703
+ * Called by the agent at every meaningful step.
2704
+ * Resets the inactivity timer and reports status upward.
2705
+ */
2706
+ ping(state, meta) {
2707
+ this.state = state;
2708
+ this.lastPingAt = Date.now();
2709
+ if (meta?.iteration !== void 0)
2710
+ this.iteration = meta.iteration;
2711
+ if (meta?.maxIterations !== void 0)
2712
+ this.maxIterations = meta.maxIterations;
2713
+ this.currentTool = meta?.tool;
2714
+ this.history.push({
2715
+ state,
2716
+ tool: meta?.tool,
2717
+ iteration: this.iteration,
2718
+ timestamp: this.lastPingAt
2719
+ });
2720
+ if (this.onProgress) {
2721
+ this.onProgress(this.formatStatus());
2722
+ }
2723
+ }
2724
+ /** How long since last activity, in ms. */
2725
+ getIdleMs() {
2726
+ return Date.now() - this.lastPingAt;
2727
+ }
2728
+ /** Total elapsed time since agent started. */
2729
+ getTotalElapsedMs() {
2730
+ return Date.now() - this.startedAt;
2731
+ }
2732
+ /** Current human-readable status string. */
2733
+ formatStatus() {
2734
+ const iter = this.maxIterations > 0 ? ` (${this.iteration}/${this.maxIterations})` : "";
2735
+ switch (this.state) {
2736
+ case "starting":
2737
+ return `Sub-agent starting...`;
2738
+ case "llm_call":
2739
+ return `Sub-agent thinking...${iter}`;
2740
+ case "tool_call":
2741
+ return this.currentTool ? `Sub-agent using ${this.currentTool}${iter}` : `Sub-agent using tool...${iter}`;
2742
+ case "processing":
2743
+ return `Sub-agent processing...${iter}`;
2744
+ case "done":
2745
+ return `Sub-agent done${iter}`;
2746
+ default:
2747
+ return `Sub-agent working...${iter}`;
2748
+ }
2749
+ }
2750
+ /** Full snapshot for logging / debugging. */
2751
+ getSnapshot() {
2752
+ return {
2753
+ state: this.state,
2754
+ iteration: this.iteration,
2755
+ maxIterations: this.maxIterations,
2756
+ lastPingAt: this.lastPingAt,
2757
+ idleMs: this.getIdleMs(),
2758
+ currentTool: this.currentTool,
2759
+ totalElapsedMs: this.getTotalElapsedMs(),
2760
+ history: [...this.history]
2761
+ };
2762
+ }
2763
+ };
2764
+ }
2765
+ });
2766
+
2565
2767
  // ../skills/dist/plugin-loader.js
2566
2768
  import fs3 from "node:fs";
2567
2769
  import path3 from "node:path";
@@ -2974,9 +3176,9 @@ var init_reminder = __esm({
2974
3176
  reminderRepo;
2975
3177
  metadata = {
2976
3178
  name: "reminder",
2977
- description: 'Set timed reminders that notify the user later. Use when the user says "remind me", "erinnere mich", or asks to be notified about something at a specific time.',
3179
+ description: 'Set timed reminders that notify the user later. Use when the user says "remind me", "erinnere mich", or asks to be notified about something at a specific time. Prefer triggerAt (absolute time like "14:30" or "2026-02-28 09:00") over delayMinutes \u2014 it is more precise and avoids calculation errors.',
2978
3180
  riskLevel: "write",
2979
- version: "2.0.0",
3181
+ version: "3.0.0",
2980
3182
  inputSchema: {
2981
3183
  type: "object",
2982
3184
  properties: {
@@ -2989,9 +3191,13 @@ var init_reminder = __esm({
2989
3191
  type: "string",
2990
3192
  description: "The reminder message (required for set)"
2991
3193
  },
3194
+ triggerAt: {
3195
+ type: "string",
3196
+ description: 'Absolute time for the reminder. Accepts "HH:MM" for today or "YYYY-MM-DD HH:MM" for a specific date. Preferred over delayMinutes for time-specific reminders.'
3197
+ },
2992
3198
  delayMinutes: {
2993
3199
  type: "number",
2994
- description: "Minutes until the reminder triggers (required for set)"
3200
+ description: "Minutes until the reminder triggers. Use triggerAt instead when the user specifies a clock time."
2995
3201
  },
2996
3202
  reminderId: {
2997
3203
  type: "string",
@@ -3023,6 +3229,7 @@ var init_reminder = __esm({
3023
3229
  }
3024
3230
  setReminder(input2, context) {
3025
3231
  const message = input2.message;
3232
+ const triggerAtStr = input2.triggerAt;
3026
3233
  const delayMinutes = input2.delayMinutes;
3027
3234
  if (!message || typeof message !== "string") {
3028
3235
  return {
@@ -3030,20 +3237,119 @@ var init_reminder = __esm({
3030
3237
  error: 'Missing required field "message" for set action'
3031
3238
  };
3032
3239
  }
3033
- if (delayMinutes === void 0 || typeof delayMinutes !== "number" || delayMinutes <= 0) {
3240
+ let triggerAt;
3241
+ if (triggerAtStr && typeof triggerAtStr === "string") {
3242
+ const parsed = this.parseTriggerAt(triggerAtStr, context.timezone);
3243
+ if (!parsed) {
3244
+ return {
3245
+ success: false,
3246
+ error: `Could not parse triggerAt "${triggerAtStr}". Use "HH:MM" for today or "YYYY-MM-DD HH:MM" for a specific date.`
3247
+ };
3248
+ }
3249
+ if (parsed.getTime() <= Date.now()) {
3250
+ return {
3251
+ success: false,
3252
+ error: `The time "${triggerAtStr}" is in the past. Please specify a future time.`
3253
+ };
3254
+ }
3255
+ triggerAt = parsed;
3256
+ } else if (delayMinutes !== void 0 && typeof delayMinutes === "number" && delayMinutes > 0) {
3257
+ triggerAt = new Date(Date.now() + delayMinutes * 60 * 1e3);
3258
+ } else {
3034
3259
  return {
3035
3260
  success: false,
3036
- error: 'Missing or invalid "delayMinutes" for set action (must be a positive number)'
3261
+ error: 'Provide either "triggerAt" (e.g. "14:30") or "delayMinutes" (positive number) for set action.'
3037
3262
  };
3038
3263
  }
3039
- const triggerAt = new Date(Date.now() + delayMinutes * 60 * 1e3);
3040
3264
  const entry = this.reminderRepo.create(context.userId, context.platform, context.chatId, message, triggerAt);
3265
+ const delayMs = triggerAt.getTime() - Date.now();
3266
+ const mins = Math.round(delayMs / 6e4);
3267
+ const timeLabel = triggerAt.toLocaleTimeString("en-GB", {
3268
+ hour: "2-digit",
3269
+ minute: "2-digit",
3270
+ ...context.timezone ? { timeZone: context.timezone } : {}
3271
+ });
3041
3272
  return {
3042
3273
  success: true,
3043
3274
  data: { reminderId: entry.id, message, triggerAt: entry.triggerAt },
3044
- display: `Reminder set (${entry.id}): "${message}" in ${delayMinutes} minute(s)`
3275
+ display: `Reminder set (${entry.id}): "${message}" at ${timeLabel} (in ${mins} min)`
3045
3276
  };
3046
3277
  }
3278
+ /**
3279
+ * Parse a trigger time string into a Date.
3280
+ *
3281
+ * Supported formats:
3282
+ * - "HH:MM" → today at that time in the given timezone
3283
+ * - "YYYY-MM-DD HH:MM" → specific date+time
3284
+ */
3285
+ parseTriggerAt(str, timezone) {
3286
+ const trimmed = str.trim();
3287
+ const timeOnly = /^(\d{1,2}):(\d{2})$/.exec(trimmed);
3288
+ if (timeOnly) {
3289
+ const hours = parseInt(timeOnly[1], 10);
3290
+ const minutes = parseInt(timeOnly[2], 10);
3291
+ if (hours > 23 || minutes > 59)
3292
+ return void 0;
3293
+ return this.buildDateInTimezone(hours, minutes, void 0, timezone);
3294
+ }
3295
+ const dateTime = /^(\d{4})-(\d{2})-(\d{2})\s+(\d{1,2}):(\d{2})$/.exec(trimmed);
3296
+ if (dateTime) {
3297
+ const year = parseInt(dateTime[1], 10);
3298
+ const month = parseInt(dateTime[2], 10) - 1;
3299
+ const day = parseInt(dateTime[3], 10);
3300
+ const hours = parseInt(dateTime[4], 10);
3301
+ const minutes = parseInt(dateTime[5], 10);
3302
+ if (hours > 23 || minutes > 59 || month > 11 || day > 31)
3303
+ return void 0;
3304
+ return this.buildDateInTimezone(hours, minutes, { year, month, day }, timezone);
3305
+ }
3306
+ return void 0;
3307
+ }
3308
+ /**
3309
+ * Build a Date object for a given time in the user's timezone.
3310
+ * Uses iterative offset correction to handle DST edge cases.
3311
+ */
3312
+ buildDateInTimezone(hours, minutes, date, timezone) {
3313
+ if (!timezone) {
3314
+ const d = date ? new Date(date.year, date.month, date.day, hours, minutes, 0, 0) : /* @__PURE__ */ new Date();
3315
+ if (!date) {
3316
+ d.setHours(hours, minutes, 0, 0);
3317
+ }
3318
+ return d;
3319
+ }
3320
+ const now = /* @__PURE__ */ new Date();
3321
+ const refDate = date ? new Date(Date.UTC(date.year, date.month, date.day, hours, minutes, 0)) : new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), hours, minutes, 0));
3322
+ const formatter = new Intl.DateTimeFormat("en-CA", {
3323
+ timeZone: timezone,
3324
+ year: "numeric",
3325
+ month: "2-digit",
3326
+ day: "2-digit",
3327
+ hour: "2-digit",
3328
+ minute: "2-digit",
3329
+ second: "2-digit",
3330
+ hour12: false
3331
+ });
3332
+ if (!date) {
3333
+ const parts = formatter.formatToParts(now);
3334
+ const tzYear = parseInt(parts.find((p) => p.type === "year").value, 10);
3335
+ const tzMonth = parseInt(parts.find((p) => p.type === "month").value, 10) - 1;
3336
+ const tzDay = parseInt(parts.find((p) => p.type === "day").value, 10);
3337
+ let guess2 = new Date(Date.UTC(tzYear, tzMonth, tzDay, hours, minutes, 0));
3338
+ const guessParts2 = formatter.formatToParts(guess2);
3339
+ const guessHour2 = parseInt(guessParts2.find((p) => p.type === "hour").value, 10);
3340
+ const guessMinute2 = parseInt(guessParts2.find((p) => p.type === "minute").value, 10);
3341
+ const diffMinutes2 = (hours - guessHour2) * 60 + (minutes - guessMinute2);
3342
+ guess2 = new Date(guess2.getTime() + diffMinutes2 * 6e4);
3343
+ return guess2;
3344
+ }
3345
+ let guess = refDate;
3346
+ const guessParts = formatter.formatToParts(guess);
3347
+ const guessHour = parseInt(guessParts.find((p) => p.type === "hour").value, 10);
3348
+ const guessMinute = parseInt(guessParts.find((p) => p.type === "minute").value, 10);
3349
+ const diffMinutes = (hours - guessHour) * 60 + (minutes - guessMinute);
3350
+ guess = new Date(guess.getTime() + diffMinutes * 6e4);
3351
+ return guess;
3352
+ }
3047
3353
  listReminders(context) {
3048
3354
  const reminders = this.reminderRepo.getByUser(context.userId);
3049
3355
  const reminderList = reminders.map((r) => ({
@@ -3576,12 +3882,15 @@ ${results.map((r) => `- ${r.key}: "${r.value}" (score: ${r.score.toFixed(2)})`).
3576
3882
  });
3577
3883
 
3578
3884
  // ../skills/dist/built-in/delegate.js
3579
- var MAX_SUB_AGENT_ITERATIONS, DelegateSkill;
3885
+ var DEFAULT_MAX_ITERATIONS, MAX_ALLOWED_ITERATIONS, INITIAL_TIMEOUT_MS, DelegateSkill;
3580
3886
  var init_delegate = __esm({
3581
3887
  "../skills/dist/built-in/delegate.js"() {
3582
3888
  "use strict";
3583
3889
  init_skill();
3584
- MAX_SUB_AGENT_ITERATIONS = 5;
3890
+ init_activity_tracker();
3891
+ DEFAULT_MAX_ITERATIONS = 5;
3892
+ MAX_ALLOWED_ITERATIONS = 15;
3893
+ INITIAL_TIMEOUT_MS = 12e4;
3585
3894
  DelegateSkill = class extends Skill {
3586
3895
  llm;
3587
3896
  skillRegistry;
@@ -3589,11 +3898,10 @@ var init_delegate = __esm({
3589
3898
  securityManager;
3590
3899
  metadata = {
3591
3900
  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.',
3901
+ 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
3902
  riskLevel: "write",
3594
- version: "2.0.0",
3595
- timeoutMs: 12e4,
3596
- // 2 minutes — delegate chains multiple LLM calls + tool executions
3903
+ version: "3.0.0",
3904
+ timeoutMs: INITIAL_TIMEOUT_MS,
3597
3905
  inputSchema: {
3598
3906
  type: "object",
3599
3907
  properties: {
@@ -3604,11 +3912,16 @@ var init_delegate = __esm({
3604
3912
  context: {
3605
3913
  type: "string",
3606
3914
  description: "Additional context the sub-agent needs (optional)"
3915
+ },
3916
+ max_iterations: {
3917
+ type: "number",
3918
+ description: "Max tool iterations (1-15). Use higher values for complex multi-step tasks. Default: 5."
3607
3919
  }
3608
3920
  },
3609
3921
  required: ["task"]
3610
3922
  }
3611
3923
  };
3924
+ onProgress;
3612
3925
  constructor(llm, skillRegistry, skillSandbox, securityManager) {
3613
3926
  super();
3614
3927
  this.llm = llm;
@@ -3616,6 +3929,21 @@ var init_delegate = __esm({
3616
3929
  this.skillSandbox = skillSandbox;
3617
3930
  this.securityManager = securityManager;
3618
3931
  }
3932
+ /**
3933
+ * Set a progress callback before execution.
3934
+ * The pipeline calls this so the user sees live status updates
3935
+ * like "Sub-agent using web_search (2/5)".
3936
+ */
3937
+ setProgressCallback(cb) {
3938
+ this.onProgress = cb;
3939
+ }
3940
+ /**
3941
+ * Create an ActivityTracker for this execution.
3942
+ * The sandbox uses this to decide whether to extend or kill.
3943
+ */
3944
+ createTracker() {
3945
+ return new ActivityTracker(this.onProgress);
3946
+ }
3619
3947
  async execute(input2, context) {
3620
3948
  const task = input2.task;
3621
3949
  const additionalContext = input2.context;
@@ -3625,6 +3953,10 @@ var init_delegate = __esm({
3625
3953
  error: 'Missing required field "task"'
3626
3954
  };
3627
3955
  }
3956
+ const requestedIterations = input2.max_iterations;
3957
+ const maxIterations = requestedIterations ? Math.max(1, Math.min(MAX_ALLOWED_ITERATIONS, Math.round(requestedIterations))) : DEFAULT_MAX_ITERATIONS;
3958
+ const tracker = new ActivityTracker(this.onProgress);
3959
+ tracker.ping("starting", { maxIterations });
3628
3960
  const tools = this.buildSubAgentTools();
3629
3961
  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
3962
  let userContent = task;
@@ -3641,6 +3973,7 @@ Additional context: ${additionalContext}`;
3641
3973
  let totalInputTokens = 0;
3642
3974
  let totalOutputTokens = 0;
3643
3975
  while (true) {
3976
+ tracker.ping("llm_call", { iteration, maxIterations });
3644
3977
  const response = await this.llm.complete({
3645
3978
  messages,
3646
3979
  system: systemPrompt,
@@ -3649,7 +3982,9 @@ Additional context: ${additionalContext}`;
3649
3982
  });
3650
3983
  totalInputTokens += response.usage.inputTokens;
3651
3984
  totalOutputTokens += response.usage.outputTokens;
3652
- if (!response.toolCalls || response.toolCalls.length === 0 || iteration >= MAX_SUB_AGENT_ITERATIONS) {
3985
+ tracker.ping("processing", { iteration, maxIterations });
3986
+ if (!response.toolCalls || response.toolCalls.length === 0 || iteration >= maxIterations) {
3987
+ tracker.ping("done", { iteration, maxIterations });
3653
3988
  return {
3654
3989
  success: true,
3655
3990
  data: {
@@ -3676,6 +4011,7 @@ Additional context: ${additionalContext}`;
3676
4011
  messages.push({ role: "assistant", content: assistantContent });
3677
4012
  const toolResultBlocks = [];
3678
4013
  for (const toolCall of response.toolCalls) {
4014
+ tracker.ping("tool_call", { iteration, maxIterations, tool: toolCall.name });
3679
4015
  const result = await this.executeSubAgentTool(toolCall, context);
3680
4016
  toolResultBlocks.push({
3681
4017
  type: "tool_result",
@@ -5673,6 +6009,7 @@ var init_dist6 = __esm({
5673
6009
  init_skill();
5674
6010
  init_skill_registry();
5675
6011
  init_skill_sandbox();
6012
+ init_activity_tracker();
5676
6013
  init_plugin_loader();
5677
6014
  init_calculator();
5678
6015
  init_system_info();
@@ -5746,6 +6083,9 @@ var init_message_pipeline = __esm({
5746
6083
  speechTranscriber;
5747
6084
  inboxPath;
5748
6085
  embeddingService;
6086
+ /** Registry of currently running delegate agents, keyed by a unique agent ID. */
6087
+ activeAgents = /* @__PURE__ */ new Map();
6088
+ agentIdCounter = 0;
5749
6089
  constructor(options) {
5750
6090
  this.llm = options.llm;
5751
6091
  this.conversationManager = options.conversationManager;
@@ -5804,13 +6144,18 @@ var init_message_pipeline = __esm({
5804
6144
  }
5805
6145
  } catch {
5806
6146
  }
6147
+ const resolvedTimezone = userProfile?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
5807
6148
  const skillMetas = this.skillRegistry ? this.skillRegistry.getAll().map((s) => s.metadata) : void 0;
5808
6149
  const tools = skillMetas ? this.promptBuilder.buildTools(skillMetas) : void 0;
5809
- const system = this.promptBuilder.buildSystemPrompt({
6150
+ let system = this.promptBuilder.buildSystemPrompt({
5810
6151
  memories,
5811
6152
  skills: skillMetas,
5812
6153
  userProfile
5813
6154
  });
6155
+ const agentStatusBlock = this.buildActiveAgentStatus();
6156
+ if (agentStatusBlock) {
6157
+ system += "\n\n" + agentStatusBlock;
6158
+ }
5814
6159
  const allMessages = this.promptBuilder.buildMessages(history);
5815
6160
  const userContent = await this.buildUserContent(message, onProgress);
5816
6161
  allMessages.push({ role: "user", content: userContent });
@@ -5854,8 +6199,9 @@ var init_message_pipeline = __esm({
5854
6199
  chatId: message.chatId,
5855
6200
  chatType: message.chatType,
5856
6201
  platform: message.platform,
5857
- conversationId: conversation.id
5858
- });
6202
+ conversationId: conversation.id,
6203
+ timezone: resolvedTimezone
6204
+ }, onProgress);
5859
6205
  toolResultBlocks.push({
5860
6206
  type: "tool_result",
5861
6207
  tool_use_id: toolCall.id,
@@ -5885,7 +6231,7 @@ var init_message_pipeline = __esm({
5885
6231
  throw error;
5886
6232
  }
5887
6233
  }
5888
- async executeToolCall(toolCall, context) {
6234
+ async executeToolCall(toolCall, context, onProgress) {
5889
6235
  const skill = this.skillRegistry?.get(toolCall.name);
5890
6236
  if (!skill) {
5891
6237
  this.logger.warn({ tool: toolCall.name }, "Unknown skill requested");
@@ -5909,11 +6255,33 @@ var init_message_pipeline = __esm({
5909
6255
  }
5910
6256
  }
5911
6257
  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
- };
6258
+ let tracker;
6259
+ let agentId;
6260
+ if (toolCall.name === "delegate" && "setProgressCallback" in skill && "createTracker" in skill) {
6261
+ const delegateSkill = skill;
6262
+ if (onProgress) {
6263
+ delegateSkill.setProgressCallback(onProgress);
6264
+ }
6265
+ tracker = delegateSkill.createTracker();
6266
+ agentId = `agent-${++this.agentIdCounter}`;
6267
+ this.activeAgents.set(agentId, {
6268
+ chatId: context.chatId,
6269
+ task: String(toolCall.input.task ?? "").slice(0, 200),
6270
+ tracker,
6271
+ startedAt: Date.now()
6272
+ });
6273
+ }
6274
+ try {
6275
+ const result = await this.skillSandbox.execute(skill, toolCall.input, context, void 0, tracker);
6276
+ return {
6277
+ content: result.display ?? (result.success ? JSON.stringify(result.data) : result.error ?? "Unknown error"),
6278
+ isError: !result.success
6279
+ };
6280
+ } finally {
6281
+ if (agentId) {
6282
+ this.activeAgents.delete(agentId);
6283
+ }
6284
+ }
5917
6285
  }
5918
6286
  try {
5919
6287
  const result = await skill.execute(toolCall.input, context);
@@ -5966,6 +6334,24 @@ var init_message_pipeline = __esm({
5966
6334
  return `Using ${toolName}...`;
5967
6335
  }
5968
6336
  }
6337
+ /**
6338
+ * Build a status block describing currently running delegate agents.
6339
+ * Injected into the system prompt so the LLM can answer user questions
6340
+ * like "What is the agent doing right now?".
6341
+ */
6342
+ buildActiveAgentStatus() {
6343
+ if (this.activeAgents.size === 0)
6344
+ return void 0;
6345
+ const lines = ["## Currently running sub-agents"];
6346
+ for (const [id, agent] of this.activeAgents) {
6347
+ const snapshot = agent.tracker.getSnapshot();
6348
+ const elapsedSec = Math.round(snapshot.totalElapsedMs / 1e3);
6349
+ lines.push(`- **${id}**: "${agent.task}"`, ` Status: ${agent.tracker.formatStatus()}`, ` Running for ${elapsedSec}s | Last activity ${Math.round(snapshot.idleMs / 1e3)}s ago`);
6350
+ }
6351
+ lines.push("");
6352
+ lines.push("If the user asks what you or the agent is doing, describe the above status in natural language.");
6353
+ return lines.join("\n");
6354
+ }
5969
6355
  /**
5970
6356
  * Trim messages to fit within the LLM's context window.
5971
6357
  * 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.2",
4
4
  "description": "Alfred — Personal AI Assistant across Telegram, Discord, WhatsApp, Matrix & Signal",
5
5
  "type": "module",
6
6
  "bin": {