@rubytech/taskmaster 1.0.4 → 1.0.6

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/README.md CHANGED
@@ -14,7 +14,7 @@ taskmaster provision
14
14
  Or one command on a fresh device:
15
15
 
16
16
  ```bash
17
- curl -fsSL https://taskmaster.bot/install.sh | bash
17
+ curl -fsSL https://taskmaster.bot/install.sh | sudo bash
18
18
  ```
19
19
 
20
20
  After install, open the setup wizard in your browser:
@@ -122,7 +122,7 @@ export function buildAgentSystemPrompt(params) {
122
122
  browser: "Control web browser",
123
123
  canvas: "Present/eval/snapshot the Canvas",
124
124
  nodes: "List/describe/notify/camera/screen on paired nodes",
125
- cron: "Manage cron jobs and wake events (use for reminders; when scheduling a reminder, write the systemEvent text as something that will read like a reminder when it fires, and mention that it is a reminder depending on the time gap between setting and firing; include recent context in reminder text if appropriate)",
125
+ cron: "Manage scheduled events and wake events (use for reminders; when scheduling a reminder, write the systemEvent text as something that will read like a reminder when it fires, and mention that it is a reminder depending on the time gap between setting and firing; include recent context in reminder text if appropriate)",
126
126
  message: "Send messages and channel actions",
127
127
  gateway: "Restart, apply config, or run updates on the running Taskmaster process",
128
128
  agents_list: "List agent ids allowed for sessions_spawn",
@@ -263,7 +263,7 @@ export function buildAgentSystemPrompt(params) {
263
263
  "- browser: control Taskmaster's dedicated browser",
264
264
  "- canvas: present/eval/snapshot the Canvas",
265
265
  "- nodes: list/describe/notify/camera/screen on paired nodes",
266
- "- cron: manage cron jobs and wake events (use for reminders; when scheduling a reminder, write the systemEvent text as something that will read like a reminder when it fires, and mention that it is a reminder depending on the time gap between setting and firing; include recent context in reminder text if appropriate)",
266
+ "- cron: manage scheduled events and wake events (use for reminders; when scheduling a reminder, write the systemEvent text as something that will read like a reminder when it fires, and mention that it is a reminder depending on the time gap between setting and firing; include recent context in reminder text if appropriate)",
267
267
  "- sessions_list: list sessions",
268
268
  "- sessions_history: fetch session history",
269
269
  "- sessions_send: send to another session",
@@ -145,7 +145,7 @@
145
145
  },
146
146
  "cron": {
147
147
  "emoji": "⏰",
148
- "title": "Cron",
148
+ "title": "Events",
149
149
  "actions": {
150
150
  "status": { "label": "status" },
151
151
  "list": { "label": "list" },
@@ -113,21 +113,21 @@ async function buildReminderContextLines(params) {
113
113
  }
114
114
  export function createCronTool(opts) {
115
115
  return {
116
- label: "Cron",
116
+ label: "Events",
117
117
  name: "cron",
118
- description: `Manage Gateway cron jobs (status/list/add/update/remove/run/runs) and send wake events.
118
+ description: `Manage scheduled events (status/list/add/update/remove/run/runs) and send wake events.
119
119
 
120
120
  ACTIONS:
121
- - status: Check cron scheduler status
122
- - list: List jobs (use includeDisabled:true to include disabled)
123
- - add: Create job (requires job object, see schema below)
124
- - update: Modify job (requires jobId + patch object)
125
- - remove: Delete job (requires jobId)
126
- - run: Trigger job immediately (requires jobId)
127
- - runs: Get job run history (requires jobId)
121
+ - status: Check event scheduler status
122
+ - list: List events (use includeDisabled:true to include disabled). Returns all events for your account.
123
+ - add: Create event (requires job object, see schema below)
124
+ - update: Modify event (requires jobId + patch object)
125
+ - remove: Delete event (requires jobId)
126
+ - run: Trigger event immediately (requires jobId)
127
+ - runs: Get event run history (requires jobId)
128
128
  - wake: Send wake event (requires text, optional mode)
129
129
 
130
- JOB SCHEMA (for add action):
130
+ EVENT SCHEMA (for add action):
131
131
  {
132
132
  "name": "string (optional)",
133
133
  "schedule": { ... }, // Required: when to run
@@ -158,7 +158,7 @@ WAKE MODES (for wake action):
158
158
  - "next-heartbeat" (default): Wake on next heartbeat
159
159
  - "now": Wake immediately
160
160
 
161
- Use jobId as the canonical identifier; id is accepted for compatibility. Use contextMessages (0-10) to add previous messages as context to the job text.`,
161
+ Use jobId as the canonical identifier; id is accepted for compatibility. Use contextMessages (0-10) to add previous messages as context to the event text.`,
162
162
  parameters: CronToolSchema,
163
163
  execute: async (_toolCallId, args) => {
164
164
  const params = args;
@@ -171,10 +171,15 @@ Use jobId as the canonical identifier; id is accepted for compatibility. Use con
171
171
  switch (action) {
172
172
  case "status":
173
173
  return jsonResult(await callGatewayTool("cron.status", gatewayOpts, {}));
174
- case "list":
175
- return jsonResult(await callGatewayTool("cron.list", gatewayOpts, {
174
+ case "list": {
175
+ const listParams = {
176
176
  includeDisabled: Boolean(params.includeDisabled),
177
- }));
177
+ };
178
+ const accountId = opts?.agentAccountId;
179
+ if (accountId)
180
+ listParams.accountId = accountId;
181
+ return jsonResult(await callGatewayTool("cron.list", gatewayOpts, listParams));
182
+ }
178
183
  case "add": {
179
184
  if (!params.job || typeof params.job !== "object") {
180
185
  throw new Error("job required");
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.4",
3
- "commit": "a317fdb599ad59d1366f5d5352244c8dff8f44a8",
4
- "builtAt": "2026-02-14T19:55:48.406Z"
2
+ "version": "1.0.6",
3
+ "commit": "f50bf4225d9f628ae369ac7b6d520687095a688d",
4
+ "builtAt": "2026-02-15T08:03:07.343Z"
5
5
  }
@@ -87,7 +87,11 @@ export function buildDefaultAgentList(workspaceRoot) {
87
87
  ],
88
88
  },
89
89
  write: {
90
- include: ["memory/users/{peer}/**", "memory/groups/{peer}/**"],
90
+ include: [
91
+ "memory/users/{peer}/**",
92
+ "memory/groups/{peer}/**",
93
+ "memory/shared/events/**",
94
+ ],
91
95
  },
92
96
  },
93
97
  },
@@ -13,11 +13,21 @@ export function parseTaskmasterVersion(raw) {
13
13
  revision: revision ? Number.parseInt(revision, 10) : 0,
14
14
  };
15
15
  }
16
+ /**
17
+ * Date-based versions (e.g. 2026.1.25) have major >= 2000.
18
+ * Semver versions (e.g. 1.0.4) have major < 100.
19
+ * Comparing across formats is meaningless — return null.
20
+ */
21
+ function isDateVersion(v) {
22
+ return v.major >= 2000;
23
+ }
16
24
  export function compareTaskmasterVersions(a, b) {
17
25
  const parsedA = parseTaskmasterVersion(a);
18
26
  const parsedB = parseTaskmasterVersion(b);
19
27
  if (!parsedA || !parsedB)
20
28
  return null;
29
+ if (isDateVersion(parsedA) !== isDateVersion(parsedB))
30
+ return null;
21
31
  if (parsedA.major !== parsedB.major)
22
32
  return parsedA.major < parsedB.major ? -1 : 1;
23
33
  if (parsedA.minor !== parsedB.minor)
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Filler configuration resolution.
3
+ */
4
+ /** Default filler configuration values. */
5
+ export const FILLER_DEFAULTS = {
6
+ enabled: false,
7
+ channels: [
8
+ "whatsapp",
9
+ "telegram",
10
+ "discord",
11
+ "slack",
12
+ "signal",
13
+ "googlechat",
14
+ "imessage",
15
+ "webchat",
16
+ ],
17
+ maxWaitMs: 3000,
18
+ model: "claude-3-haiku-20240307",
19
+ maxWords: 15,
20
+ };
21
+ /** Resolve filler config with defaults applied. */
22
+ export function resolveFillerConfig(cfg) {
23
+ return {
24
+ ...FILLER_DEFAULTS,
25
+ ...cfg?.filler,
26
+ // Ensure channels array is always present
27
+ channels: cfg?.filler?.channels ?? FILLER_DEFAULTS.channels,
28
+ };
29
+ }
30
+ /**
31
+ * Resolve whether filler is enabled, checking session and agent-level config.
32
+ *
33
+ * Priority:
34
+ * 1. Session override (from /filler command)
35
+ * 2. Per-agent config (agents.list[].fillerEnabled)
36
+ * 3. Agent defaults (agents.defaults.fillerEnabled)
37
+ * 4. Global filler config (filler.enabled)
38
+ * 5. Default (false)
39
+ */
40
+ export function resolveFillerEnabled(params) {
41
+ const { cfg, agentId, sessionFillerEnabled } = params;
42
+ // Session-level override takes precedence (from /filler command)
43
+ if (sessionFillerEnabled !== undefined) {
44
+ return sessionFillerEnabled;
45
+ }
46
+ // Check per-agent config
47
+ if (agentId && cfg?.agents?.list) {
48
+ const agentConfig = cfg.agents.list.find((a) => a.id === agentId);
49
+ if (agentConfig?.fillerEnabled !== undefined) {
50
+ return agentConfig.fillerEnabled;
51
+ }
52
+ }
53
+ // Check agent defaults
54
+ if (cfg?.agents?.defaults?.fillerEnabled !== undefined) {
55
+ return cfg.agents.defaults.fillerEnabled;
56
+ }
57
+ // Fall back to global filler config
58
+ return cfg?.filler?.enabled ?? FILLER_DEFAULTS.enabled;
59
+ }
60
+ /** Check if filler is enabled for a specific channel. */
61
+ export function isFillerEnabledForChannel(params) {
62
+ const { cfg, channel } = params;
63
+ if (!resolveFillerEnabled(params))
64
+ return false;
65
+ if (!channel)
66
+ return false;
67
+ const fillerConfig = resolveFillerConfig(cfg);
68
+ return fillerConfig.channels.includes(channel);
69
+ }
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Filler message generation using a lightweight model (Haiku).
3
+ *
4
+ * Uses pi-ai complete() for OAuth-compatible inference.
5
+ */
6
+ import path from "node:path";
7
+ import { complete } from "@mariozechner/pi-ai";
8
+ import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
9
+ import { getApiKeyForModel, requireApiKey } from "../agents/model-auth.js";
10
+ import { ensureTaskmasterModelsJson } from "../agents/models-config.js";
11
+ const FILLER_SYSTEM_PROMPT = `You generate a brief, first-person filler while processing a request. Output ONLY the filler text.
12
+
13
+ Rules:
14
+ 1. For trivial messages (greetings, "yes", "ok", "thanks", single-word replies), output: SKIP
15
+ 2. Always use first person ("I'll...", "I need to...", "I'm...")
16
+ 3. Match the filler to what you're doing (shown in thinking)
17
+ 4. Keep it short (2-6 words) and casual
18
+ 5. Vary your responses every time
19
+
20
+ Quick acknowledgments (no lookup needed):
21
+ - "one moment please", "just a moment", "one sec", "OK", "sure thing", "got it"
22
+
23
+ When looking something up / searching:
24
+ - "I need to look that up", "I'll check on that", "I need to search for that", "I'm looking into it"
25
+
26
+ When checking / verifying:
27
+ - "I'll check", "I'm checking now", "I need to verify that"
28
+
29
+ When thinking / working out:
30
+ - "hmm", "I'm thinking", "I need to work this out"
31
+
32
+ IMPORTANT: Vary every response. Never repeat the same phrase twice in a row.`;
33
+ const TOOL_PROGRESS_SYSTEM_PROMPT = `You generate a brief, first-person progress update while a tool is running. Output ONLY the progress text.
34
+
35
+ Rules:
36
+ 1. Always use first person ("I'm...", "Just...", "Let me...")
37
+ 2. Be specific about what you're doing based on the tool and its arguments
38
+ 3. Keep it short (3-8 words) and natural
39
+ 4. Sound like a human assistant giving a quick status update
40
+
41
+ Examples:
42
+ - exec with "git status" → "Just checking the git status"
43
+ - exec with "npm install" → "Installing the dependencies"
44
+ - memory_search with query "invoice" → "Searching my notes for invoices"
45
+ - read with path "/config.json" → "Reading the config file"
46
+ - web_search with query "weather" → "Looking up the weather"
47
+
48
+ Output ONLY the progress message, nothing else.`;
49
+ /** Token returned when filler should not be sent. */
50
+ export const FILLER_SKIP_TOKEN = "SKIP";
51
+ /**
52
+ * Generate a filler message using pi-ai complete().
53
+ *
54
+ * Works with both API keys and OAuth tokens.
55
+ */
56
+ export async function generateFiller(params) {
57
+ const { userMessage, thinking, model: modelId, maxWords, timeoutMs = 3000, abortSignal, cfg, agentDir, } = params;
58
+ // Check if already aborted before starting
59
+ if (abortSignal?.aborted) {
60
+ return { ok: false, error: new Error("Filler generation aborted") };
61
+ }
62
+ try {
63
+ // Set up model discovery with auth
64
+ await ensureTaskmasterModelsJson(cfg, agentDir);
65
+ const authStorage = agentDir
66
+ ? new AuthStorage(path.join(agentDir, "auth.json"))
67
+ : new AuthStorage();
68
+ const modelRegistry = agentDir
69
+ ? new ModelRegistry(authStorage, path.join(agentDir, "models.json"))
70
+ : new ModelRegistry(authStorage);
71
+ // Find Haiku model
72
+ const model = modelRegistry.find("anthropic", modelId);
73
+ if (!model) {
74
+ return { ok: false, error: new Error(`Filler model not found: anthropic/${modelId}`) };
75
+ }
76
+ // Resolve and set API key
77
+ const apiKeyInfo = await getApiKeyForModel({
78
+ model,
79
+ cfg,
80
+ agentDir,
81
+ });
82
+ const apiKey = requireApiKey(apiKeyInfo, model.provider);
83
+ authStorage.setRuntimeApiKey(model.provider, apiKey);
84
+ // Truncate thinking if too long (keep last 400 chars for context)
85
+ const truncatedThinking = thinking
86
+ ? thinking.length > 400
87
+ ? `...${thinking.slice(-400)}`
88
+ : thinking
89
+ : "";
90
+ // Build the prompt with both user message and thinking
91
+ let prompt = `User's message: "${userMessage}"`;
92
+ if (truncatedThinking) {
93
+ prompt += `\n\nAssistant's thinking so far:\n---\n${truncatedThinking}\n---`;
94
+ }
95
+ prompt += `\n\nGenerate a brief filler (max ${maxWords} words) that fits what the assistant is doing:`;
96
+ // console.log(`[filler] === FILLER GENERATION ===`);
97
+ // console.log(`[filler] User message: "${userMessage}"`);
98
+ // console.log(
99
+ // `[filler] Thinking (${truncatedThinking.length} chars): "${truncatedThinking.slice(0, 100)}..."`,
100
+ // );
101
+ // Build context with system prompt and user message
102
+ const context = {
103
+ systemPrompt: FILLER_SYSTEM_PROMPT,
104
+ messages: [
105
+ {
106
+ role: "user",
107
+ content: prompt,
108
+ timestamp: Date.now(),
109
+ },
110
+ ],
111
+ };
112
+ // Create timeout promise
113
+ const timeoutPromise = new Promise((_, reject) => {
114
+ const timer = setTimeout(() => reject(new Error("Filler generation timeout")), timeoutMs);
115
+ abortSignal?.addEventListener("abort", () => {
116
+ clearTimeout(timer);
117
+ reject(new Error("Filler generation aborted"));
118
+ });
119
+ });
120
+ // Race completion against timeout
121
+ const message = (await Promise.race([
122
+ complete(model, context, {
123
+ apiKey,
124
+ maxTokens: 50,
125
+ temperature: 0.9, // Higher temperature for variety
126
+ }),
127
+ timeoutPromise,
128
+ ]));
129
+ // Extract text from response
130
+ const content = message.content?.[0];
131
+ if (!content || content.type !== "text" || !content.text) {
132
+ return { ok: false, skip: true };
133
+ }
134
+ const text = content.text.trim();
135
+ // console.log(`[filler] Generated output: "${text}"`);
136
+ // Check for SKIP token
137
+ if (text.toUpperCase() === FILLER_SKIP_TOKEN) {
138
+ // logVerbose(`[filler] Skipping (model returned SKIP)`);
139
+ return { ok: false, skip: true };
140
+ }
141
+ // Don't truncate - mid-sentence cuts look bad. The prompt already
142
+ // instructs Haiku to keep it brief (2-6 words).
143
+ return { ok: true, text };
144
+ }
145
+ catch (err) {
146
+ const error = err instanceof Error ? err : new Error(String(err));
147
+ // logVerbose(`Filler generation failed: ${error.message}`);
148
+ return { ok: false, error };
149
+ }
150
+ }
151
+ /**
152
+ * Generate a tool progress message using Haiku.
153
+ */
154
+ export async function generateToolProgress(params) {
155
+ const { toolName, args, model: modelId, timeoutMs = 2000, cfg, agentDir } = params;
156
+ try {
157
+ // Set up model discovery with auth
158
+ await ensureTaskmasterModelsJson(cfg, agentDir);
159
+ const authStorage = agentDir
160
+ ? new AuthStorage(path.join(agentDir, "auth.json"))
161
+ : new AuthStorage();
162
+ const modelRegistry = agentDir
163
+ ? new ModelRegistry(authStorage, path.join(agentDir, "models.json"))
164
+ : new ModelRegistry(authStorage);
165
+ // Find Haiku model
166
+ const model = modelRegistry.find("anthropic", modelId);
167
+ if (!model) {
168
+ return { ok: false, error: new Error(`Filler model not found: anthropic/${modelId}`) };
169
+ }
170
+ // Resolve and set API key
171
+ const apiKeyInfo = await getApiKeyForModel({
172
+ model,
173
+ cfg,
174
+ agentDir,
175
+ });
176
+ const apiKey = requireApiKey(apiKeyInfo, model.provider);
177
+ authStorage.setRuntimeApiKey(model.provider, apiKey);
178
+ // Build prompt with tool info
179
+ let prompt = `Tool: ${toolName}`;
180
+ if (args && Object.keys(args).length > 0) {
181
+ // Include relevant args, truncating long values
182
+ const relevantArgs = {};
183
+ for (const [key, value] of Object.entries(args)) {
184
+ if (typeof value === "string" && value.length > 100) {
185
+ relevantArgs[key] = value.slice(0, 100) + "...";
186
+ }
187
+ else if (typeof value === "string" ||
188
+ typeof value === "number" ||
189
+ typeof value === "boolean") {
190
+ relevantArgs[key] = value;
191
+ }
192
+ }
193
+ if (Object.keys(relevantArgs).length > 0) {
194
+ prompt += `\nArguments: ${JSON.stringify(relevantArgs)}`;
195
+ }
196
+ }
197
+ prompt += "\n\nGenerate a brief progress update:";
198
+ // console.log(`[filler] === TOOL PROGRESS GENERATION ===`);
199
+ // console.log(`[filler] Tool: ${toolName}, args: ${JSON.stringify(args ?? {}).slice(0, 100)}`);
200
+ const context = {
201
+ systemPrompt: TOOL_PROGRESS_SYSTEM_PROMPT,
202
+ messages: [
203
+ {
204
+ role: "user",
205
+ content: prompt,
206
+ timestamp: Date.now(),
207
+ },
208
+ ],
209
+ };
210
+ // Create timeout promise
211
+ const timeoutPromise = new Promise((_, reject) => {
212
+ setTimeout(() => reject(new Error("Tool progress generation timeout")), timeoutMs);
213
+ });
214
+ // Race completion against timeout
215
+ const message = (await Promise.race([
216
+ complete(model, context, {
217
+ apiKey,
218
+ maxTokens: 30,
219
+ temperature: 0.7,
220
+ }),
221
+ timeoutPromise,
222
+ ]));
223
+ // Extract text from response
224
+ const content = message.content?.[0];
225
+ if (!content || content.type !== "text" || !content.text) {
226
+ return { ok: false, skip: true };
227
+ }
228
+ const text = content.text.trim();
229
+ // console.log(`[filler] Tool progress: "${text}"`);
230
+ return { ok: true, text };
231
+ }
232
+ catch (err) {
233
+ const error = err instanceof Error ? err : new Error(String(err));
234
+ console.warn(`[filler] Tool progress generation failed: ${error.message}`);
235
+ return { ok: false, error };
236
+ }
237
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Filler message system - reduces perceived latency by emitting brief
3
+ * acknowledgments while the main AI response generates.
4
+ */
5
+ export * from "./types.js";
6
+ export { FILLER_DEFAULTS, resolveFillerConfig, resolveFillerEnabled, isFillerEnabledForChannel, } from "./config.js";
7
+ export { createFillerTrigger } from "./trigger.js";
8
+ export { generateFiller, FILLER_SKIP_TOKEN } from "./generator.js";
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Filler trigger: generates and delivers a brief acknowledgment message
3
+ * while the main AI response is being generated.
4
+ *
5
+ * Waits briefly for initial thinking to accumulate, then generates a
6
+ * context-aware filler using both the user message and model's thinking.
7
+ *
8
+ * Also sends progress updates during tool calls (throttled).
9
+ */
10
+ import { isFillerEnabledForChannel, resolveFillerConfig } from "./config.js";
11
+ import { generateFiller, generateToolProgress } from "./generator.js";
12
+ /** How long to wait for thinking before generating filler (ms) */
13
+ const THINKING_WAIT_MS = 350;
14
+ /** Minimum time between tool progress updates (ms) */
15
+ const TOOL_PROGRESS_THROTTLE_MS = 8000;
16
+ /**
17
+ * Check if a tool should trigger a progress update.
18
+ * Messaging tools are skipped since they're the final output.
19
+ */
20
+ function shouldSkipToolProgress(toolName) {
21
+ const normalized = toolName.toLowerCase().trim();
22
+ return (normalized === "message" ||
23
+ normalized === "send" ||
24
+ normalized === "send_message" ||
25
+ normalized === "sessions_send");
26
+ }
27
+ /**
28
+ * Create a filler trigger for an agent run.
29
+ *
30
+ * Returns null if filler is disabled or not enabled for this channel.
31
+ * Waits briefly for thinking to accumulate before generating filler.
32
+ */
33
+ export function createFillerTrigger(params) {
34
+ const config = resolveFillerConfig(params.cfg);
35
+ // Check if filler is enabled for this channel (respects session and per-agent config)
36
+ if (!isFillerEnabledForChannel({
37
+ cfg: params.cfg,
38
+ agentId: params.agentId,
39
+ channel: params.channel,
40
+ sessionFillerEnabled: params.sessionFillerEnabled,
41
+ })) {
42
+ return null;
43
+ }
44
+ // console.log(`[filler] waiting for thinking (${THINKING_WAIT_MS}ms) for run ${params.runId}`);
45
+ let cancelled = false;
46
+ let fillerSent = false;
47
+ const abortController = new AbortController();
48
+ const thinkingChunks = [];
49
+ let generatePromise = null;
50
+ let lastProgressAt = 0;
51
+ // Collect thinking chunks
52
+ const feedThinking = (text) => {
53
+ if (cancelled || fillerSent || !text)
54
+ return;
55
+ thinkingChunks.push(text);
56
+ };
57
+ // Send progress update for tool calls (throttled, uses Haiku to generate)
58
+ const feedToolCall = (toolName, args) => {
59
+ if (cancelled)
60
+ return;
61
+ // Skip messaging tools (they're the final output)
62
+ if (shouldSkipToolProgress(toolName)) {
63
+ // console.log(`[filler] skipping progress for tool ${toolName}`);
64
+ return;
65
+ }
66
+ // Throttle progress updates
67
+ const now = Date.now();
68
+ if (now - lastProgressAt < TOOL_PROGRESS_THROTTLE_MS) {
69
+ // console.log(`[filler] throttled tool progress for ${toolName}`);
70
+ return;
71
+ }
72
+ lastProgressAt = now;
73
+ // console.log(`[filler] generating tool progress for ${toolName}`);
74
+ // Generate and send progress update via Haiku
75
+ void generateToolProgress({
76
+ toolName,
77
+ args,
78
+ model: config.model,
79
+ timeoutMs: 2000,
80
+ cfg: params.cfg,
81
+ agentDir: params.agentDir,
82
+ })
83
+ .then((result) => {
84
+ if (cancelled) {
85
+ // console.log(`[filler] cancelled, discarding tool progress`);
86
+ return;
87
+ }
88
+ if (result.ok) {
89
+ // console.log(`[filler] sending tool progress: "${result.text}" (${toolName})`);
90
+ return params.onFiller(result.text);
91
+ }
92
+ })
93
+ .catch((err) => {
94
+ console.warn(`[filler] tool progress failed: ${err instanceof Error ? err.message : String(err)}`);
95
+ });
96
+ };
97
+ // Start generation after brief delay to collect thinking
98
+ const thinkingTimer = setTimeout(() => {
99
+ if (cancelled)
100
+ return;
101
+ generatePromise = startGeneration();
102
+ }, THINKING_WAIT_MS);
103
+ async function startGeneration() {
104
+ if (cancelled)
105
+ return;
106
+ const thinking = thinkingChunks.join("").trim();
107
+ // console.log(`[filler] generating with ${thinking.length} chars of thinking`);
108
+ try {
109
+ const result = await generateFiller({
110
+ userMessage: params.userMessage || "",
111
+ thinking: thinking || undefined,
112
+ model: config.model,
113
+ maxWords: config.maxWords,
114
+ timeoutMs: config.maxWaitMs,
115
+ abortSignal: abortController.signal,
116
+ cfg: params.cfg,
117
+ agentDir: params.agentDir,
118
+ });
119
+ if (cancelled) {
120
+ // console.log(`[filler] cancelled during generation, discarding`);
121
+ return;
122
+ }
123
+ if (result.ok) {
124
+ fillerSent = true;
125
+ // console.log(`[filler] delivering: "${result.text}"`);
126
+ await params.onFiller(result.text);
127
+ }
128
+ else if ("skip" in result && result.skip) {
129
+ // console.log(`[filler] model returned SKIP`);
130
+ }
131
+ else if ("error" in result) {
132
+ console.warn(`[filler] generation error: ${result.error.message}`);
133
+ }
134
+ }
135
+ catch (err) {
136
+ // Fail silently - filler is non-critical
137
+ console.warn(`[filler] error: ${err instanceof Error ? err.message : String(err)}`);
138
+ }
139
+ }
140
+ const cancel = () => {
141
+ if (cancelled)
142
+ return;
143
+ cancelled = true;
144
+ clearTimeout(thinkingTimer);
145
+ abortController.abort();
146
+ console.warn(`[filler] cancelled for run ${params.runId}`);
147
+ };
148
+ const flush = async () => {
149
+ clearTimeout(thinkingTimer);
150
+ if (!generatePromise && !cancelled && !fillerSent) {
151
+ generatePromise = startGeneration();
152
+ }
153
+ if (generatePromise) {
154
+ await generatePromise;
155
+ }
156
+ };
157
+ return {
158
+ feedThinking,
159
+ feedToolCall,
160
+ cancel,
161
+ flush,
162
+ };
163
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Filler message generation types.
3
+ *
4
+ * The filler system emits brief acknowledgments while the main AI response
5
+ * is generating, reducing perceived latency for channels like WhatsApp.
6
+ */
7
+ export {};
@@ -66,6 +66,7 @@ export const CronJobStateSchema = Type.Object({
66
66
  export const CronJobSchema = Type.Object({
67
67
  id: NonEmptyString,
68
68
  agentId: Type.Optional(NonEmptyString),
69
+ accountId: Type.Optional(NonEmptyString),
69
70
  name: NonEmptyString,
70
71
  description: Type.Optional(Type.String()),
71
72
  enabled: Type.Boolean(),
@@ -82,11 +83,13 @@ export const CronJobSchema = Type.Object({
82
83
  export const CronListParamsSchema = Type.Object({
83
84
  includeDisabled: Type.Optional(Type.Boolean()),
84
85
  agentIds: Type.Optional(Type.Array(Type.String())),
86
+ accountId: Type.Optional(Type.String()),
85
87
  }, { additionalProperties: false });
86
88
  export const CronStatusParamsSchema = Type.Object({}, { additionalProperties: false });
87
89
  export const CronAddParamsSchema = Type.Object({
88
90
  name: NonEmptyString,
89
91
  agentId: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
92
+ accountId: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
90
93
  description: Type.Optional(Type.String()),
91
94
  enabled: Type.Optional(Type.Boolean()),
92
95
  deleteAfterRun: Type.Optional(Type.Boolean()),
@@ -99,6 +102,7 @@ export const CronAddParamsSchema = Type.Object({
99
102
  export const CronJobPatchSchema = Type.Object({
100
103
  name: Type.Optional(NonEmptyString),
101
104
  agentId: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
105
+ accountId: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
102
106
  description: Type.Optional(Type.String()),
103
107
  enabled: Type.Optional(Type.Boolean()),
104
108
  deleteAfterRun: Type.Optional(Type.Boolean()),