@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 +1 -1
- package/dist/agents/system-prompt.js +2 -2
- package/dist/agents/tool-display.json +1 -1
- package/dist/agents/tools/cron-tool.js +19 -14
- package/dist/build-info.json +3 -3
- package/dist/cli/provision-seed.js +5 -1
- package/dist/config/version.js +10 -0
- package/dist/filler/config.js +69 -0
- package/dist/filler/generator.js +237 -0
- package/dist/filler/index.js +8 -0
- package/dist/filler/trigger.js +163 -0
- package/dist/filler/types.js +7 -0
- package/dist/gateway/protocol/schema/cron.js +4 -0
- package/dist/gateway/server-methods/cron.js +4 -0
- package/dist/gateway/server.impl.js +12 -0
- package/dist/license/device-id.js +61 -0
- package/dist/license/keys.js +61 -0
- package/dist/license/revalidation.js +52 -0
- package/dist/license/state.js +12 -0
- package/dist/license/validate.js +59 -0
- package/dist/logging/logger.js +23 -11
- package/dist/records/records-manager.js +92 -0
- package/package.json +5 -2
- package/scripts/install.sh +2 -2
- package/scripts/postinstall.js +7 -1
- package/skills/business-assistant/SKILL.md +2 -2
- package/skills/event-management/SKILL.md +15 -0
- package/skills/event-management/references/events.md +120 -0
- package/taskmaster-docs/USER-GUIDE.md +3 -3
- package/templates/taskmaster/agents/admin/AGENTS.md +27 -6
- package/templates/taskmaster/skills/business-assistant/SKILL.md +80 -0
package/README.md
CHANGED
|
@@ -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
|
|
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
|
|
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",
|
|
@@ -113,21 +113,21 @@ async function buildReminderContextLines(params) {
|
|
|
113
113
|
}
|
|
114
114
|
export function createCronTool(opts) {
|
|
115
115
|
return {
|
|
116
|
-
label: "
|
|
116
|
+
label: "Events",
|
|
117
117
|
name: "cron",
|
|
118
|
-
description: `Manage
|
|
118
|
+
description: `Manage scheduled events (status/list/add/update/remove/run/runs) and send wake events.
|
|
119
119
|
|
|
120
120
|
ACTIONS:
|
|
121
|
-
- status: Check
|
|
122
|
-
- list: List
|
|
123
|
-
- add: Create
|
|
124
|
-
- update: Modify
|
|
125
|
-
- remove: Delete
|
|
126
|
-
- run: Trigger
|
|
127
|
-
- runs: Get
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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");
|
package/dist/build-info.json
CHANGED
|
@@ -87,7 +87,11 @@ export function buildDefaultAgentList(workspaceRoot) {
|
|
|
87
87
|
],
|
|
88
88
|
},
|
|
89
89
|
write: {
|
|
90
|
-
include: [
|
|
90
|
+
include: [
|
|
91
|
+
"memory/users/{peer}/**",
|
|
92
|
+
"memory/groups/{peer}/**",
|
|
93
|
+
"memory/shared/events/**",
|
|
94
|
+
],
|
|
91
95
|
},
|
|
92
96
|
},
|
|
93
97
|
},
|
package/dist/config/version.js
CHANGED
|
@@ -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
|
+
}
|
|
@@ -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()),
|