@rubytech/taskmaster 1.12.3 → 1.13.0
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/dist/agents/auth-profiles/consolidate.js +72 -0
- package/dist/agents/auth-profiles/oauth.js +0 -24
- package/dist/agents/auth-profiles/paths.js +4 -4
- package/dist/agents/auth-profiles/store.js +8 -100
- package/dist/agents/model-fallback.js +26 -1
- package/dist/agents/pi-embedded-runner/run/payloads.js +8 -0
- package/dist/agents/session-transcript-repair.js +3 -2
- package/dist/agents/system-prompt.js +1 -0
- package/dist/agents/taskmaster-tools.js +2 -0
- package/dist/agents/tool-policy.js +2 -0
- package/dist/agents/tools/opening-hours-tool.js +92 -0
- package/dist/agents/tools/web-fetch.js +8 -3
- package/dist/agents/tools/web-search.js +7 -4
- package/dist/agents/workspace-migrations.js +47 -0
- package/dist/build-info.json +3 -3
- package/dist/commands/agents.commands.add.js +1 -32
- package/dist/config/defaults.js +1 -1
- package/dist/config/legacy.migrations.part-3.js +25 -4
- package/dist/config/sessions/transcript.js +31 -0
- package/dist/config/types.business.js +1 -0
- package/dist/config/zod-schema.js +33 -0
- package/dist/control-ui/assets/{index-CpaEIgQy.css → index-B8I8lMfz.css} +1 -1
- package/dist/control-ui/assets/{index-CP9IoaZp.js → index-BWqMMgRV.js} +537 -425
- package/dist/control-ui/assets/index-BWqMMgRV.js.map +1 -0
- package/dist/control-ui/index.html +2 -2
- package/dist/gateway/config-reload.js +1 -0
- package/dist/gateway/server-close.js +8 -0
- package/dist/gateway/server-methods/business.js +31 -0
- package/dist/gateway/server-methods/network.js +19 -6
- package/dist/gateway/server-methods.js +5 -1
- package/dist/gateway/server.impl.js +42 -0
- package/dist/infra/heartbeat-infra-alert.js +54 -0
- package/dist/memory/manager.js +5 -5
- package/dist/web/auto-reply/monitor/process-message.js +24 -0
- package/dist/web/inbound/access-control.js +2 -1
- package/dist/web/inbound/monitor.js +32 -10
- package/dist/web/inbound/owner-mirror.js +35 -0
- package/package.json +1 -1
- package/skills/anthropic/SKILL.md +30 -0
- package/skills/anthropic/references/setup-guide.md +146 -0
- package/skills/google-ai/SKILL.md +3 -2
- package/skills/google-ai/references/setup-guide.md +94 -0
- package/skills/log-review/SKILL.md +45 -0
- package/skills/log-review/cron-template.json +21 -0
- package/skills/log-review/references/review-protocol.md +65 -0
- package/skills/openai/SKILL.md +28 -0
- package/skills/openai/references/setup-guide.md +122 -0
- package/taskmaster-docs/USER-GUIDE.md +31 -2
- package/templates/beagle-taxi/memory/public/investors-knowledge-base.md +230 -0
- package/templates/beagle-taxi/skills/beagle-taxi/SKILL.md +3 -1
- package/templates/customer/agents/admin/BOOTSTRAP.md +14 -2
- package/templates/customer/agents/public/AGENTS.md +15 -0
- package/templates/education-hero/agents/admin/BOOTSTRAP.md +14 -2
- package/templates/real-agent/agents/admin/AGENTS.md +139 -0
- package/templates/real-agent/agents/admin/HEARTBEAT.md +12 -0
- package/templates/real-agent/agents/admin/IDENTITY.md +11 -0
- package/templates/real-agent/agents/admin/SOUL.md +38 -0
- package/templates/real-agent/agents/public/AGENTS.md +183 -0
- package/templates/real-agent/agents/public/IDENTITY.md +8 -0
- package/templates/real-agent/agents/public/SOUL.md +75 -0
- package/templates/real-agent/memory/admin/.gitkeep +0 -0
- package/templates/real-agent/memory/public/contributors/adam-mackay.md +7 -0
- package/templates/real-agent/memory/public/contributors/alex-pelosi-buchanan.md +7 -0
- package/templates/real-agent/memory/public/contributors/jamie-fisher.md +7 -0
- package/templates/real-agent/memory/public/contributors/john-savage.md +7 -0
- package/templates/real-agent/memory/public/contributors/melanie-attwater.md +7 -0
- package/templates/real-agent/memory/public/contributors/regina-mangan.md +7 -0
- package/templates/real-agent/memory/public/contributors/richard-rawlings.md +7 -0
- package/templates/real-agent/memory/public/contributors/roger-black.md +7 -0
- package/templates/real-agent/memory/public/contributors/steve-backley.md +7 -0
- package/templates/real-agent/memory/public/courses/agency-blueprint/.gitkeep +0 -0
- package/templates/real-agent/memory/public/courses/podcast/.gitkeep +0 -0
- package/templates/real-agent/memory/public/courses/real-business/.gitkeep +0 -0
- package/templates/real-agent/memory/public/courses/real-coaching/.gitkeep +0 -0
- package/templates/real-agent/memory/public/courses/real-marketing/.gitkeep +0 -0
- package/templates/real-agent/memory/public/resources/.gitkeep +0 -0
- package/templates/real-agent/memory/shared/.gitkeep +0 -0
- package/templates/real-agent/memory/users/.gitkeep +0 -0
- package/templates/real-agent/skills/bespoke-coaching/SKILL.md +29 -0
- package/templates/real-agent/skills/bespoke-coaching/references/coaching-boundaries.md +56 -0
- package/templates/real-agent/skills/bespoke-coaching/references/feedback-framework.md +61 -0
- package/templates/real-agent/skills/bootstrap/SKILL.md +27 -0
- package/templates/real-agent/skills/bootstrap/references/onboarding-flow.md +63 -0
- package/templates/real-agent/skills/content-directory/SKILL.md +40 -0
- package/templates/real-agent/skills/content-directory/references/module-delivery.md +65 -0
- package/templates/real-agent/skills/content-directory/references/progress-tracking.md +47 -0
- package/templates/tradesupport/agents/admin/BOOTSTRAP.md +14 -2
- package/dist/control-ui/assets/index-CP9IoaZp.js.map +0 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveStateDir } from "../../config/paths.js";
|
|
4
|
+
import { AUTH_PROFILE_FILENAME } from "./constants.js";
|
|
5
|
+
import { resolveAuthStorePath } from "./paths.js";
|
|
6
|
+
import { ensureAuthProfileStore, saveAuthProfileStore } from "./store.js";
|
|
7
|
+
/**
|
|
8
|
+
* One-time migration: scan per-agent auth-profiles.json files, merge any
|
|
9
|
+
* fresher OAuth tokens into the global store, then rename per-agent files
|
|
10
|
+
* to `.bak`.
|
|
11
|
+
*
|
|
12
|
+
* Returns a list of human-readable change descriptions (empty if no work done).
|
|
13
|
+
*/
|
|
14
|
+
export function consolidateAuthProfileStores() {
|
|
15
|
+
const changes = [];
|
|
16
|
+
const mainPath = resolveAuthStorePath();
|
|
17
|
+
const mainStore = ensureAuthProfileStore();
|
|
18
|
+
const agentsDir = path.join(resolveStateDir(), "agents");
|
|
19
|
+
let agentDirs;
|
|
20
|
+
try {
|
|
21
|
+
agentDirs = fs.readdirSync(agentsDir);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return changes;
|
|
25
|
+
}
|
|
26
|
+
for (const dir of agentDirs) {
|
|
27
|
+
const agentAuthPath = path.join(agentsDir, dir, "agent", AUTH_PROFILE_FILENAME);
|
|
28
|
+
// Skip the main agent's store — it IS the global store.
|
|
29
|
+
if (path.resolve(agentAuthPath) === path.resolve(mainPath))
|
|
30
|
+
continue;
|
|
31
|
+
let raw;
|
|
32
|
+
try {
|
|
33
|
+
raw = fs.readFileSync(agentAuthPath, "utf8");
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
let agentStore;
|
|
39
|
+
try {
|
|
40
|
+
agentStore = JSON.parse(raw);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
let merged = false;
|
|
46
|
+
for (const [profileId, profile] of Object.entries(agentStore.profiles ?? {})) {
|
|
47
|
+
if (profile.type !== "oauth")
|
|
48
|
+
continue;
|
|
49
|
+
const mainProfile = mainStore.profiles[profileId];
|
|
50
|
+
const mainExpiry = mainProfile?.type === "oauth" ? (mainProfile.expires ?? 0) : 0;
|
|
51
|
+
const agentExpiry = profile.expires ?? 0;
|
|
52
|
+
if (agentExpiry > mainExpiry) {
|
|
53
|
+
mainStore.profiles[profileId] = profile;
|
|
54
|
+
merged = true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (merged) {
|
|
58
|
+
changes.push(`Consolidated fresher OAuth tokens from agent "${dir}".`);
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
fs.renameSync(agentAuthPath, `${agentAuthPath}.bak`);
|
|
62
|
+
changes.push(`Renamed ${dir}/agent/${AUTH_PROFILE_FILENAME} to .bak.`);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Best-effort rename — file may be locked or read-only.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (changes.length > 0) {
|
|
69
|
+
saveAuthProfileStore(mainStore);
|
|
70
|
+
}
|
|
71
|
+
return changes;
|
|
72
|
+
}
|
|
@@ -59,30 +59,6 @@ async function refreshOAuthTokenWithLock(params) {
|
|
|
59
59
|
type: "oauth",
|
|
60
60
|
};
|
|
61
61
|
saveAuthProfileStore(store, params.agentDir);
|
|
62
|
-
// Propagate refreshed credentials to the main store so auth.status and other agents
|
|
63
|
-
// see the fresh token. Without this, the main store retains a stale refresh token
|
|
64
|
-
// that Anthropic has already rotated, causing auth.status to permanently report
|
|
65
|
-
// "Connection expired" even though the agent is working fine.
|
|
66
|
-
const mainAuthPath = resolveAuthStorePath();
|
|
67
|
-
if (authPath !== mainAuthPath) {
|
|
68
|
-
try {
|
|
69
|
-
const mainStore = ensureAuthProfileStore();
|
|
70
|
-
const mainCred = mainStore.profiles[params.profileId];
|
|
71
|
-
const mainExpiry = mainCred?.type === "oauth" ? mainCred.expires : 0;
|
|
72
|
-
const freshExpiry = result.newCredentials.expires ?? 0;
|
|
73
|
-
if (freshExpiry > mainExpiry) {
|
|
74
|
-
mainStore.profiles[params.profileId] = {
|
|
75
|
-
...(mainCred ?? cred),
|
|
76
|
-
...result.newCredentials,
|
|
77
|
-
type: "oauth",
|
|
78
|
-
};
|
|
79
|
-
saveAuthProfileStore(mainStore);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
// Best-effort — don't fail the agent's own refresh if main store update fails
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
62
|
// Sync refreshed credentials back to Claude Code CLI if this is the claude-cli profile
|
|
87
63
|
// This ensures Claude Code continues to work after Taskmaster refreshes the token
|
|
88
64
|
if (params.profileId === CLAUDE_CLI_PROFILE_ID && cred.provider === "anthropic") {
|
|
@@ -4,12 +4,12 @@ import { saveJsonFile } from "../../infra/json-file.js";
|
|
|
4
4
|
import { resolveUserPath } from "../../utils.js";
|
|
5
5
|
import { resolveTaskmasterAgentDir } from "../agent-paths.js";
|
|
6
6
|
import { AUTH_PROFILE_FILENAME, AUTH_STORE_VERSION, LEGACY_AUTH_FILENAME } from "./constants.js";
|
|
7
|
-
export function resolveAuthStorePath(
|
|
8
|
-
const resolved = resolveUserPath(
|
|
7
|
+
export function resolveAuthStorePath(_agentDir) {
|
|
8
|
+
const resolved = resolveUserPath(resolveTaskmasterAgentDir());
|
|
9
9
|
return path.join(resolved, AUTH_PROFILE_FILENAME);
|
|
10
10
|
}
|
|
11
|
-
export function resolveLegacyAuthStorePath(
|
|
12
|
-
const resolved = resolveUserPath(
|
|
11
|
+
export function resolveLegacyAuthStorePath(_agentDir) {
|
|
12
|
+
const resolved = resolveUserPath(resolveTaskmasterAgentDir());
|
|
13
13
|
return path.join(resolved, LEGACY_AUTH_FILENAME);
|
|
14
14
|
}
|
|
15
15
|
export function resolveAuthStorePathForDisplay(agentDir) {
|
|
@@ -2,8 +2,8 @@ import fs from "node:fs";
|
|
|
2
2
|
import lockfile from "proper-lockfile";
|
|
3
3
|
import { resolveOAuthPath } from "../../config/paths.js";
|
|
4
4
|
import { loadJsonFile, saveJsonFile } from "../../infra/json-file.js";
|
|
5
|
-
import { AUTH_STORE_LOCK_OPTIONS, AUTH_STORE_VERSION,
|
|
6
|
-
import {
|
|
5
|
+
import { AUTH_STORE_LOCK_OPTIONS, AUTH_STORE_VERSION, log } from "./constants.js";
|
|
6
|
+
import { syncExternalCliCredentials } from "./external-cli-sync.js";
|
|
7
7
|
import { ensureAuthStoreFile, resolveAuthStorePath, resolveLegacyAuthStorePath } from "./paths.js";
|
|
8
8
|
function _syncAuthProfileStore(target, source) {
|
|
9
9
|
target.version = source.version;
|
|
@@ -104,70 +104,6 @@ function coerceAuthStore(raw) {
|
|
|
104
104
|
: undefined,
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
|
-
function mergeRecord(base, override) {
|
|
108
|
-
if (!base && !override)
|
|
109
|
-
return undefined;
|
|
110
|
-
if (!base)
|
|
111
|
-
return { ...override };
|
|
112
|
-
if (!override)
|
|
113
|
-
return { ...base };
|
|
114
|
-
return { ...base, ...override };
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Get the expiry timestamp from a credential if it has one.
|
|
118
|
-
* Returns 0 for credentials without expiry (API keys).
|
|
119
|
-
*/
|
|
120
|
-
function getCredentialExpiry(cred) {
|
|
121
|
-
if (cred.type === "oauth" || cred.type === "token") {
|
|
122
|
-
return cred.expires ?? 0;
|
|
123
|
-
}
|
|
124
|
-
return 0;
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Merge auth profiles, preferring the FRESHER credential when both have the same profile ID.
|
|
128
|
-
* This ensures OAuth tokens propagate from the shared location to all agents.
|
|
129
|
-
*/
|
|
130
|
-
function mergeAuthProfileStores(base, override) {
|
|
131
|
-
if (Object.keys(override.profiles).length === 0 &&
|
|
132
|
-
!override.order &&
|
|
133
|
-
!override.lastGood &&
|
|
134
|
-
!override.usageStats) {
|
|
135
|
-
return base;
|
|
136
|
-
}
|
|
137
|
-
// Merge profiles, preferring the fresher credential for OAuth/token types.
|
|
138
|
-
// For api_key profiles, prefer base (main store) — centralized API key
|
|
139
|
-
// management writes there, and agent stores only have inherited copies.
|
|
140
|
-
const mergedProfiles = { ...base.profiles };
|
|
141
|
-
for (const [profileId, overrideCred] of Object.entries(override.profiles)) {
|
|
142
|
-
const baseCred = base.profiles[profileId];
|
|
143
|
-
if (!baseCred) {
|
|
144
|
-
// No conflict — use override
|
|
145
|
-
mergedProfiles[profileId] = overrideCred;
|
|
146
|
-
}
|
|
147
|
-
else if (baseCred.type === "api_key" && overrideCred.type === "api_key") {
|
|
148
|
-
// API keys: base (main store) is authoritative — applyApiKeys() writes
|
|
149
|
-
// the centralized key there. Agent stores only have stale inherited
|
|
150
|
-
// copies. Always prefer base so key updates propagate immediately.
|
|
151
|
-
// (keep base — already in mergedProfiles)
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
// OAuth/token: prefer the one with later expiry (fresher token)
|
|
155
|
-
const baseExpiry = getCredentialExpiry(baseCred);
|
|
156
|
-
const overrideExpiry = getCredentialExpiry(overrideCred);
|
|
157
|
-
if (overrideExpiry >= baseExpiry) {
|
|
158
|
-
mergedProfiles[profileId] = overrideCred;
|
|
159
|
-
}
|
|
160
|
-
// else keep base (it has a later expiry = fresher token)
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return {
|
|
164
|
-
version: Math.max(base.version, override.version ?? base.version),
|
|
165
|
-
profiles: mergedProfiles,
|
|
166
|
-
order: mergeRecord(base.order, override.order),
|
|
167
|
-
lastGood: mergeRecord(base.lastGood, override.lastGood),
|
|
168
|
-
usageStats: mergeRecord(base.usageStats, override.usageStats),
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
107
|
function mergeOAuthFileIntoStore(store) {
|
|
172
108
|
const oauthPath = resolveOAuthPath();
|
|
173
109
|
const oauthRaw = loadJsonFile(oauthPath);
|
|
@@ -249,8 +185,8 @@ export function loadAuthProfileStore() {
|
|
|
249
185
|
syncExternalCliCredentials(store);
|
|
250
186
|
return store;
|
|
251
187
|
}
|
|
252
|
-
function loadAuthProfileStoreForAgent(
|
|
253
|
-
const authPath = resolveAuthStorePath(
|
|
188
|
+
function loadAuthProfileStoreForAgent(_agentDir, options) {
|
|
189
|
+
const authPath = resolveAuthStorePath();
|
|
254
190
|
const raw = loadJsonFile(authPath);
|
|
255
191
|
const asStore = coerceAuthStore(raw);
|
|
256
192
|
if (asStore) {
|
|
@@ -261,19 +197,7 @@ function loadAuthProfileStoreForAgent(agentDir, options) {
|
|
|
261
197
|
}
|
|
262
198
|
return asStore;
|
|
263
199
|
}
|
|
264
|
-
|
|
265
|
-
if (agentDir) {
|
|
266
|
-
const mainAuthPath = resolveAuthStorePath(); // without agentDir = main
|
|
267
|
-
const mainRaw = loadJsonFile(mainAuthPath);
|
|
268
|
-
const mainStore = coerceAuthStore(mainRaw);
|
|
269
|
-
if (mainStore && Object.keys(mainStore.profiles).length > 0) {
|
|
270
|
-
// Clone main store to subagent directory for auth inheritance
|
|
271
|
-
saveJsonFile(authPath, mainStore);
|
|
272
|
-
log.info("inherited auth-profiles from main agent", { agentDir });
|
|
273
|
-
return mainStore;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
const legacyRaw = loadJsonFile(resolveLegacyAuthStorePath(agentDir));
|
|
200
|
+
const legacyRaw = loadJsonFile(resolveLegacyAuthStorePath());
|
|
277
201
|
const legacy = coerceLegacyStore(legacyRaw);
|
|
278
202
|
const store = {
|
|
279
203
|
version: AUTH_STORE_VERSION,
|
|
@@ -324,7 +248,7 @@ function loadAuthProfileStoreForAgent(agentDir, options) {
|
|
|
324
248
|
// overwriting fresh OAuth creds with stale tokens (fixes #363). Delete only
|
|
325
249
|
// after we've successfully written auth-profiles.json.
|
|
326
250
|
if (shouldWrite && legacy !== null) {
|
|
327
|
-
const legacyPath = resolveLegacyAuthStorePath(
|
|
251
|
+
const legacyPath = resolveLegacyAuthStorePath();
|
|
328
252
|
try {
|
|
329
253
|
fs.unlinkSync(legacyPath);
|
|
330
254
|
}
|
|
@@ -339,24 +263,8 @@ function loadAuthProfileStoreForAgent(agentDir, options) {
|
|
|
339
263
|
}
|
|
340
264
|
return store;
|
|
341
265
|
}
|
|
342
|
-
export function ensureAuthProfileStore(
|
|
343
|
-
|
|
344
|
-
const authPath = resolveAuthStorePath(agentDir);
|
|
345
|
-
const mainAuthPath = resolveAuthStorePath();
|
|
346
|
-
if (!agentDir || authPath === mainAuthPath) {
|
|
347
|
-
return store;
|
|
348
|
-
}
|
|
349
|
-
const mainStore = loadAuthProfileStoreForAgent(undefined, options);
|
|
350
|
-
const merged = mergeAuthProfileStores(mainStore, store);
|
|
351
|
-
// Keep per-agent view clean even if the main store has codex-cli.
|
|
352
|
-
const codexProfile = merged.profiles[CODEX_CLI_PROFILE_ID];
|
|
353
|
-
if (codexProfile?.type === "oauth") {
|
|
354
|
-
const duplicateId = findDuplicateCodexProfile(merged, codexProfile);
|
|
355
|
-
if (duplicateId) {
|
|
356
|
-
delete merged.profiles[CODEX_CLI_PROFILE_ID];
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
return merged;
|
|
266
|
+
export function ensureAuthProfileStore(_agentDir, options) {
|
|
267
|
+
return loadAuthProfileStoreForAgent(undefined, options);
|
|
360
268
|
}
|
|
361
269
|
export function saveAuthProfileStore(store, agentDir) {
|
|
362
270
|
const authPath = resolveAuthStorePath(agentDir);
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { emitInfraAlertEvent } from "../infra/infra-alert-events.js";
|
|
2
|
+
import { createSubsystemLogger } from "../logging/subsystem.js";
|
|
1
3
|
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
|
|
2
4
|
import { coerceToFailoverError, describeFailoverError, isFailoverError, isTimeoutError, } from "./failover-error.js";
|
|
3
5
|
import { buildModelAliasIndex, modelKey, parseModelRef, resolveConfiguredModelRef, resolveModelRefFromString, } from "./model-selection.js";
|
|
6
|
+
const log = createSubsystemLogger("model-fallback");
|
|
4
7
|
function isAbortError(err) {
|
|
5
8
|
if (!err || typeof err !== "object")
|
|
6
9
|
return false;
|
|
@@ -173,6 +176,21 @@ export async function runWithModelFallback(params) {
|
|
|
173
176
|
status: described.status,
|
|
174
177
|
code: described.code,
|
|
175
178
|
});
|
|
179
|
+
const fallbackMsg = `[model-fallback] ${candidate.provider}/${candidate.model} failed (${described.reason ?? "unknown"}): ${described.message}` +
|
|
180
|
+
(i + 1 < candidates.length
|
|
181
|
+
? ` — falling back to ${candidates[i + 1].provider}/${candidates[i + 1].model}`
|
|
182
|
+
: " — no more fallback candidates");
|
|
183
|
+
log.warn(fallbackMsg);
|
|
184
|
+
console.warn(fallbackMsg);
|
|
185
|
+
if (i === 0) {
|
|
186
|
+
emitInfraAlertEvent({
|
|
187
|
+
category: "model-fallback",
|
|
188
|
+
message: `${candidate.provider}/${candidate.model} failed (${described.reason ?? "unknown"}): ${described.message}` +
|
|
189
|
+
(i + 1 < candidates.length
|
|
190
|
+
? `. Falling back to ${candidates[i + 1].provider}/${candidates[i + 1].model}.`
|
|
191
|
+
: ". No fallback available."),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
176
194
|
await params.onError?.({
|
|
177
195
|
provider: candidate.provider,
|
|
178
196
|
model: candidate.model,
|
|
@@ -219,11 +237,18 @@ export async function runWithImageModelFallback(params) {
|
|
|
219
237
|
if (shouldRethrowAbort(err))
|
|
220
238
|
throw err;
|
|
221
239
|
lastError = err;
|
|
240
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
222
241
|
attempts.push({
|
|
223
242
|
provider: candidate.provider,
|
|
224
243
|
model: candidate.model,
|
|
225
|
-
error:
|
|
244
|
+
error: errorMsg,
|
|
226
245
|
});
|
|
246
|
+
const imgFallbackMsg = `[model-fallback] image model ${candidate.provider}/${candidate.model} failed: ${errorMsg}` +
|
|
247
|
+
(i + 1 < candidates.length
|
|
248
|
+
? ` — falling back to ${candidates[i + 1].provider}/${candidates[i + 1].model}`
|
|
249
|
+
: " — no more fallback candidates");
|
|
250
|
+
log.warn(imgFallbackMsg);
|
|
251
|
+
console.warn(imgFallbackMsg);
|
|
227
252
|
await params.onError?.({
|
|
228
253
|
provider: candidate.provider,
|
|
229
254
|
model: candidate.model,
|
|
@@ -143,6 +143,14 @@ export function buildEmbeddedRunPayloads(params) {
|
|
|
143
143
|
});
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
|
+
// Last-resort safeguard: if the run errored and produced no user-facing content,
|
|
147
|
+
// inject a minimal fallback so the user isn't left with silence.
|
|
148
|
+
if (replyItems.length === 0 && lastAssistantErrored) {
|
|
149
|
+
replyItems.push({
|
|
150
|
+
text: "Sorry, I wasn't able to process that. Could you try again?",
|
|
151
|
+
isError: true,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
146
154
|
const hasAudioAsVoiceTag = replyItems.some((item) => item.audioAsVoice);
|
|
147
155
|
return replyItems
|
|
148
156
|
.map((item) => ({
|
|
@@ -28,14 +28,15 @@ function extractToolResultId(msg) {
|
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
function makeMissingToolResult(params) {
|
|
31
|
+
const name = params.toolName ?? "unknown";
|
|
31
32
|
return {
|
|
32
33
|
role: "toolResult",
|
|
33
34
|
toolCallId: params.toolCallId,
|
|
34
|
-
toolName:
|
|
35
|
+
toolName: name,
|
|
35
36
|
content: [
|
|
36
37
|
{
|
|
37
38
|
type: "text",
|
|
38
|
-
text:
|
|
39
|
+
text: `[taskmaster] Tool call "${name}" did not produce a result. The tool name may be misspelled or the call was dropped. Check the exact tool name and retry.`,
|
|
39
40
|
},
|
|
40
41
|
],
|
|
41
42
|
isError: true,
|
|
@@ -27,6 +27,7 @@ function buildMemorySection(params) {
|
|
|
27
27
|
return [
|
|
28
28
|
"## Memory Recall",
|
|
29
29
|
"Memory is your knowledge base — customer profiles, business data, preferences, prior interactions, lessons, and instructions all live there. Proactively use memory_search whenever a topic might have stored context: who a person is, what was discussed before, business rules, pricing, product details, or anything that could have been recorded. Do not rely on conversation history alone — it only covers the current session. Use memory_get to pull specific lines when you know the file. If a search returns no results, say you checked.",
|
|
30
|
+
"When skill files or workspace docs reference paths under `memory/` (e.g. `memory/public/data.md`), always access them with memory_get or memory_search — strip the leading `memory/` prefix to get the relative path (e.g. `public/data.md`). Never use read or skill_read for memory content.",
|
|
30
31
|
"",
|
|
31
32
|
];
|
|
32
33
|
}
|
|
@@ -37,6 +37,7 @@ import { createBrandSettingsTool } from "./tools/brand-settings-tool.js";
|
|
|
37
37
|
import { createChannelSettingsTool } from "./tools/channel-settings-tool.js";
|
|
38
38
|
import { createImageGenerateTool } from "./tools/image-generate-tool.js";
|
|
39
39
|
import { createLogsReadTool } from "./tools/logs-read-tool.js";
|
|
40
|
+
import { createOpeningHoursTool } from "./tools/opening-hours-tool.js";
|
|
40
41
|
import { createPublicChatSettingsTool } from "./tools/public-chat-settings-tool.js";
|
|
41
42
|
import { createSkillManageTool } from "./tools/skill-manage-tool.js";
|
|
42
43
|
import { createSoftwareUpdateTool } from "./tools/software-update-tool.js";
|
|
@@ -165,6 +166,7 @@ export function createTaskmasterTools(options) {
|
|
|
165
166
|
createUsageReportTool(),
|
|
166
167
|
createChannelSettingsTool(),
|
|
167
168
|
createBrandSettingsTool(),
|
|
169
|
+
createOpeningHoursTool(),
|
|
168
170
|
createPublicChatSettingsTool(),
|
|
169
171
|
createSkillManageTool(),
|
|
170
172
|
createLogsReadTool(),
|
|
@@ -52,6 +52,7 @@ export const TOOL_GROUPS = {
|
|
|
52
52
|
"usage_report",
|
|
53
53
|
"channel_settings",
|
|
54
54
|
"brand_settings",
|
|
55
|
+
"opening_hours",
|
|
55
56
|
"public_chat_settings",
|
|
56
57
|
"skill_manage",
|
|
57
58
|
"logs_read",
|
|
@@ -94,6 +95,7 @@ export const TOOL_GROUPS = {
|
|
|
94
95
|
"usage_report",
|
|
95
96
|
"channel_settings",
|
|
96
97
|
"brand_settings",
|
|
98
|
+
"opening_hours",
|
|
97
99
|
"public_chat_settings",
|
|
98
100
|
"skill_manage",
|
|
99
101
|
"logs_read",
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent tool for managing business opening hours.
|
|
3
|
+
*
|
|
4
|
+
* Wraps `config.get`/`config.patch` for reading and updating opening hours
|
|
5
|
+
* configuration, and `isWithinOpeningHours` for checking current status.
|
|
6
|
+
* Controls when the public agent responds to customer messages — outside
|
|
7
|
+
* opening hours, customer messages are received but the agent does not reply.
|
|
8
|
+
*/
|
|
9
|
+
import { Type } from "@sinclair/typebox";
|
|
10
|
+
import { isPublicAgentActive } from "../../business/opening-hours.js";
|
|
11
|
+
import { loadConfig } from "../../config/config.js";
|
|
12
|
+
import { stringEnum } from "../schema/typebox.js";
|
|
13
|
+
import { jsonResult, readStringParam } from "./common.js";
|
|
14
|
+
import { callGatewayTool } from "./gateway.js";
|
|
15
|
+
const ACTIONS = ["get", "set", "check"];
|
|
16
|
+
const OpeningHoursSchema = Type.Object({
|
|
17
|
+
action: stringEnum(ACTIONS, {
|
|
18
|
+
description: "get: retrieve current opening hours config. " +
|
|
19
|
+
"set: update opening hours (partial update supported). " +
|
|
20
|
+
"check: check if the business is currently open or closed.",
|
|
21
|
+
}),
|
|
22
|
+
enabled: Type.Optional(Type.Boolean({ description: "Enable/disable opening hours schedule." })),
|
|
23
|
+
publicAgentEnabled: Type.Optional(Type.Boolean({ description: "Enable/disable the public agent responding." })),
|
|
24
|
+
schedule: Type.Optional(Type.Object({
|
|
25
|
+
monday: Type.Optional(Type.Union([Type.Object({ start: Type.String(), end: Type.String() }), Type.Null()])),
|
|
26
|
+
tuesday: Type.Optional(Type.Union([Type.Object({ start: Type.String(), end: Type.String() }), Type.Null()])),
|
|
27
|
+
wednesday: Type.Optional(Type.Union([Type.Object({ start: Type.String(), end: Type.String() }), Type.Null()])),
|
|
28
|
+
thursday: Type.Optional(Type.Union([Type.Object({ start: Type.String(), end: Type.String() }), Type.Null()])),
|
|
29
|
+
friday: Type.Optional(Type.Union([Type.Object({ start: Type.String(), end: Type.String() }), Type.Null()])),
|
|
30
|
+
saturday: Type.Optional(Type.Union([Type.Object({ start: Type.String(), end: Type.String() }), Type.Null()])),
|
|
31
|
+
sunday: Type.Optional(Type.Union([Type.Object({ start: Type.String(), end: Type.String() }), Type.Null()])),
|
|
32
|
+
}, { description: "Per-weekday time windows. null = closed that day." })),
|
|
33
|
+
closedDates: Type.Optional(Type.Array(Type.String(), { description: "ISO dates (YYYY-MM-DD) to mark as closed." })),
|
|
34
|
+
});
|
|
35
|
+
export function createOpeningHoursTool() {
|
|
36
|
+
return {
|
|
37
|
+
label: "Opening Hours",
|
|
38
|
+
name: "opening_hours",
|
|
39
|
+
description: "Manage business opening hours. Controls when the public agent responds to customer messages. " +
|
|
40
|
+
"Outside opening hours, customer messages are received but the agent does not reply.",
|
|
41
|
+
parameters: OpeningHoursSchema,
|
|
42
|
+
execute: async (_toolCallId, args) => {
|
|
43
|
+
const params = args;
|
|
44
|
+
const action = readStringParam(params, "action", { required: true });
|
|
45
|
+
const gatewayOpts = {};
|
|
46
|
+
if (action === "check") {
|
|
47
|
+
const cfg = loadConfig();
|
|
48
|
+
const result = isPublicAgentActive(cfg.business);
|
|
49
|
+
return jsonResult({
|
|
50
|
+
currentlyOpen: result.open,
|
|
51
|
+
reason: result.reason,
|
|
52
|
+
publicAgentEnabled: cfg.business?.publicAgentEnabled !== false,
|
|
53
|
+
config: cfg.business?.openingHours ?? null,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (action === "get") {
|
|
57
|
+
const cfg = loadConfig();
|
|
58
|
+
return jsonResult({
|
|
59
|
+
publicAgentEnabled: cfg.business?.publicAgentEnabled !== false,
|
|
60
|
+
openingHours: cfg.business?.openingHours ?? null,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (action === "set") {
|
|
64
|
+
// Build a partial business patch.
|
|
65
|
+
const businessPatch = {};
|
|
66
|
+
// Public agent toggle lives at business.publicAgentEnabled.
|
|
67
|
+
if (typeof params.publicAgentEnabled === "boolean") {
|
|
68
|
+
businessPatch.publicAgentEnabled = params.publicAgentEnabled;
|
|
69
|
+
}
|
|
70
|
+
// Opening hours fields live at business.openingHours.
|
|
71
|
+
const ohPatch = {};
|
|
72
|
+
if (typeof params.enabled === "boolean")
|
|
73
|
+
ohPatch.enabled = params.enabled;
|
|
74
|
+
if (params.schedule)
|
|
75
|
+
ohPatch.schedule = params.schedule;
|
|
76
|
+
if (Array.isArray(params.closedDates))
|
|
77
|
+
ohPatch.closedDates = params.closedDates;
|
|
78
|
+
if (Object.keys(ohPatch).length > 0)
|
|
79
|
+
businessPatch.openingHours = ohPatch;
|
|
80
|
+
const snapshot = await callGatewayTool("config.get", gatewayOpts, {});
|
|
81
|
+
const baseHash = typeof snapshot?.hash === "string" ? snapshot.hash : undefined;
|
|
82
|
+
const result = await callGatewayTool("config.patch", gatewayOpts, {
|
|
83
|
+
raw: JSON.stringify({ business: businessPatch }),
|
|
84
|
+
baseHash,
|
|
85
|
+
note: "agent: update opening hours",
|
|
86
|
+
});
|
|
87
|
+
return jsonResult({ ok: true, result });
|
|
88
|
+
}
|
|
89
|
+
throw new Error(`Unknown action: ${action}`);
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -105,7 +105,7 @@ function isRedirectStatus(status) {
|
|
|
105
105
|
return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
|
|
106
106
|
}
|
|
107
107
|
async function fetchWithRedirects(params) {
|
|
108
|
-
const signal = withTimeout(
|
|
108
|
+
const signal = withTimeout(params.signal, params.timeoutSeconds * 1000);
|
|
109
109
|
const visited = new Set();
|
|
110
110
|
let currentUrl = params.url;
|
|
111
111
|
let redirectCount = 0;
|
|
@@ -184,7 +184,7 @@ export async function fetchFirecrawlContent(params) {
|
|
|
184
184
|
"Content-Type": "application/json",
|
|
185
185
|
},
|
|
186
186
|
body: JSON.stringify(body),
|
|
187
|
-
signal: withTimeout(
|
|
187
|
+
signal: withTimeout(params.signal, params.timeoutSeconds * 1000),
|
|
188
188
|
});
|
|
189
189
|
const payload = (await res.json());
|
|
190
190
|
if (!res.ok || payload?.success === false) {
|
|
@@ -230,6 +230,7 @@ async function runWebFetch(params) {
|
|
|
230
230
|
maxRedirects: params.maxRedirects,
|
|
231
231
|
timeoutSeconds: params.timeoutSeconds,
|
|
232
232
|
userAgent: params.userAgent,
|
|
233
|
+
signal: params.signal,
|
|
233
234
|
});
|
|
234
235
|
res = result.response;
|
|
235
236
|
finalUrl = result.finalUrl;
|
|
@@ -249,6 +250,7 @@ async function runWebFetch(params) {
|
|
|
249
250
|
proxy: params.firecrawlProxy,
|
|
250
251
|
storeInCache: params.firecrawlStoreInCache,
|
|
251
252
|
timeoutSeconds: params.firecrawlTimeoutSeconds,
|
|
253
|
+
signal: params.signal,
|
|
252
254
|
});
|
|
253
255
|
const truncated = truncateText(firecrawl.text, params.maxChars);
|
|
254
256
|
const payload = {
|
|
@@ -283,6 +285,7 @@ async function runWebFetch(params) {
|
|
|
283
285
|
proxy: params.firecrawlProxy,
|
|
284
286
|
storeInCache: params.firecrawlStoreInCache,
|
|
285
287
|
timeoutSeconds: params.firecrawlTimeoutSeconds,
|
|
288
|
+
signal: params.signal,
|
|
286
289
|
});
|
|
287
290
|
const truncated = truncateText(firecrawl.text, params.maxChars);
|
|
288
291
|
const payload = {
|
|
@@ -386,6 +389,7 @@ async function tryFirecrawlFallback(params) {
|
|
|
386
389
|
proxy: params.firecrawlProxy,
|
|
387
390
|
storeInCache: params.firecrawlStoreInCache,
|
|
388
391
|
timeoutSeconds: params.firecrawlTimeoutSeconds,
|
|
392
|
+
signal: params.signal,
|
|
389
393
|
});
|
|
390
394
|
return { text: firecrawl.text, title: firecrawl.title };
|
|
391
395
|
}
|
|
@@ -428,7 +432,7 @@ export function createWebFetchTool(options) {
|
|
|
428
432
|
name: "web_fetch",
|
|
429
433
|
description: "Fetch and extract readable content from a URL (HTML → markdown/text). Use for lightweight page access without browser automation.",
|
|
430
434
|
parameters: WebFetchSchema,
|
|
431
|
-
execute: async (_toolCallId, args) => {
|
|
435
|
+
execute: async (_toolCallId, args, signal) => {
|
|
432
436
|
const params = args;
|
|
433
437
|
const url = readStringParam(params, "url", { required: true });
|
|
434
438
|
const extractMode = readStringParam(params, "extractMode") === "text" ? "text" : "markdown";
|
|
@@ -450,6 +454,7 @@ export function createWebFetchTool(options) {
|
|
|
450
454
|
firecrawlProxy: "auto",
|
|
451
455
|
firecrawlStoreInCache: true,
|
|
452
456
|
firecrawlTimeoutSeconds,
|
|
457
|
+
signal,
|
|
453
458
|
});
|
|
454
459
|
return jsonResult(result);
|
|
455
460
|
},
|
|
@@ -233,7 +233,7 @@ async function runPerplexitySearch(params) {
|
|
|
233
233
|
},
|
|
234
234
|
],
|
|
235
235
|
}),
|
|
236
|
-
signal: withTimeout(
|
|
236
|
+
signal: withTimeout(params.signal, params.timeoutSeconds * 1000),
|
|
237
237
|
});
|
|
238
238
|
if (!res.ok) {
|
|
239
239
|
const detail = await readResponseText(res);
|
|
@@ -256,7 +256,7 @@ async function runTavilySearch(params) {
|
|
|
256
256
|
max_results: params.maxResults,
|
|
257
257
|
search_depth: params.searchDepth,
|
|
258
258
|
}),
|
|
259
|
-
signal: withTimeout(
|
|
259
|
+
signal: withTimeout(params.signal, params.timeoutSeconds * 1000),
|
|
260
260
|
});
|
|
261
261
|
if (!res.ok) {
|
|
262
262
|
const detail = await readResponseText(res);
|
|
@@ -283,6 +283,7 @@ async function runWebSearch(params) {
|
|
|
283
283
|
baseUrl: params.perplexityBaseUrl ?? DEFAULT_PERPLEXITY_BASE_URL,
|
|
284
284
|
model: params.perplexityModel ?? DEFAULT_PERPLEXITY_MODEL,
|
|
285
285
|
timeoutSeconds: params.timeoutSeconds,
|
|
286
|
+
signal: params.signal,
|
|
286
287
|
});
|
|
287
288
|
const payload = {
|
|
288
289
|
query: params.query,
|
|
@@ -302,6 +303,7 @@ async function runWebSearch(params) {
|
|
|
302
303
|
maxResults: params.count,
|
|
303
304
|
searchDepth: params.tavilySearchDepth ?? "basic",
|
|
304
305
|
timeoutSeconds: params.timeoutSeconds,
|
|
306
|
+
signal: params.signal,
|
|
305
307
|
});
|
|
306
308
|
const mapped = results.map((entry) => ({
|
|
307
309
|
title: entry.title ?? "",
|
|
@@ -346,7 +348,7 @@ async function runWebSearch(params) {
|
|
|
346
348
|
Accept: "application/json",
|
|
347
349
|
"X-Subscription-Token": params.apiKey,
|
|
348
350
|
},
|
|
349
|
-
signal: withTimeout(
|
|
351
|
+
signal: withTimeout(params.signal, params.timeoutSeconds * 1000),
|
|
350
352
|
});
|
|
351
353
|
if (!res.ok) {
|
|
352
354
|
const detail = await readResponseText(res);
|
|
@@ -388,7 +390,7 @@ export function createWebSearchTool(options) {
|
|
|
388
390
|
name: "web_search",
|
|
389
391
|
description,
|
|
390
392
|
parameters: WebSearchSchema,
|
|
391
|
-
execute: async (_toolCallId, args) => {
|
|
393
|
+
execute: async (_toolCallId, args, signal) => {
|
|
392
394
|
const perplexityAuth = provider === "perplexity" ? resolvePerplexityApiKey(perplexityConfig) : undefined;
|
|
393
395
|
const apiKey = provider === "perplexity"
|
|
394
396
|
? perplexityAuth?.apiKey
|
|
@@ -434,6 +436,7 @@ export function createWebSearchTool(options) {
|
|
|
434
436
|
perplexityBaseUrl: resolvePerplexityBaseUrl(perplexityConfig, perplexityAuth?.source, perplexityAuth?.apiKey),
|
|
435
437
|
perplexityModel: resolvePerplexityModel(perplexityConfig),
|
|
436
438
|
tavilySearchDepth: resolveTavilySearchDepth(tavilyConfig),
|
|
439
|
+
signal,
|
|
437
440
|
});
|
|
438
441
|
return jsonResult(result);
|
|
439
442
|
},
|