feat(agent): adaptive tool timing with slow tool notification

Track per-tool execution time statistics in session metadata. When a tool
call exceeds its adaptive threshold (2x historical max, min 120s default),
send a direct outbound notification to the user.

- ToolTimingMap: parse/serialize/record/threshold from session metadata
- StartSlowTimer: fires once per tool call, auto-cancels on completion
- Team config: slow_tool toggle (default on, always direct, never leader)
- UI: toggle in team settings with i18n (en/vi/zh)
- Store: add GetSessionMetadata to session store interface
This commit is contained in:
viettranx
2026-03-19 11:17:32 +07:00
parent 0df619023c
commit 4e9f155a4c
12 changed files with 236 additions and 1 deletions
+2
View File
@@ -189,6 +189,8 @@
"notifyProgressHint": "Notify when a member updates task progress.",
"notifyFailed": "Task failed",
"notifyFailedHint": "Notify when a task fails.",
"notifySlowTool": "Slow tool alert",
"notifySlowToolHint": "System alert when a tool call takes abnormally long. Always direct — never through leader.",
"notifyMode": "Delivery mode",
"notifyModeHint": "How notifications are delivered to the chat channel.",
"notifyModeDirect": "Direct",
+2
View File
@@ -189,6 +189,8 @@
"notifyProgressHint": "Thông báo khi thành viên cập nhật tiến trình.",
"notifyFailed": "Thất bại",
"notifyFailedHint": "Thông báo khi nhiệm vụ thất bại.",
"notifySlowTool": "Cảnh báo tool chạy lâu",
"notifySlowToolHint": "Thông báo hệ thống khi tool chạy lâu bất thường. Luôn gửi trực tiếp — không qua leader.",
"notifyMode": "Chế độ gửi",
"notifyModeHint": "Cách thông báo được gửi đến kênh chat.",
"notifyModeDirect": "Trực tiếp",
+2
View File
@@ -189,6 +189,8 @@
"notifyProgressHint": "成员更新进度时通知。",
"notifyFailed": "任务失败",
"notifyFailedHint": "任务失败时通知。",
"notifySlowTool": "工具慢速警报",
"notifySlowToolHint": "工具调用异常耗时时发送系统警报。始终直接发送,不通过 leader。",
"notifyMode": "发送方式",
"notifyModeHint": "通知如何发送到聊天频道。",
"notifyModeDirect": "直接发送",
+11 -1
View File
@@ -75,6 +75,7 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps)
const [notifyDispatched, setNotifyDispatched] = useState(initNotify.dispatched ?? true);
const [notifyProgress, setNotifyProgress] = useState(initNotify.progress ?? true);
const [notifyFailed, setNotifyFailed] = useState(initNotify.failed ?? true);
const [notifySlowTool, setNotifySlowTool] = useState(initNotify.slow_tool ?? true);
const [notifyMode, setNotifyMode] = useState<"direct" | "leader">(initNotify.mode ?? "direct");
const [escalationMode, setEscalationMode] = useState<EscalationMode | "">(initial.escalation_mode ?? "");
const [escalationActions, setEscalationActions] = useState<EscalationAction[]>(initial.escalation_actions ?? []);
@@ -105,6 +106,7 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps)
setNotifyDispatched(sn.dispatched ?? true);
setNotifyProgress(sn.progress ?? true);
setNotifyFailed(sn.failed ?? true);
setNotifySlowTool(sn.slow_tool ?? true);
setNotifyMode(sn.mode ?? "direct");
setEscalationMode(s.escalation_mode ?? "");
setEscalationActions(s.escalation_actions ?? []);
@@ -129,6 +131,7 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps)
dispatched: notifyDispatched,
progress: notifyProgress,
failed: notifyFailed,
slow_tool: notifySlowTool,
mode: notifyMode,
};
if (escalationMode) {
@@ -148,7 +151,7 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps)
} finally {
setSaving(false);
}
}, [teamId, version, allowUserIds, denyUserIds, allowChannels, denyChannels, notifyDispatched, notifyProgress, notifyFailed, notifyMode, escalationMode, escalationActions, followupInterval, followupMaxReminders, workspaceScope, updateTeamSettings, onSaved, t]);
}, [teamId, version, allowUserIds, denyUserIds, allowChannels, denyChannels, notifyDispatched, notifyProgress, notifyFailed, notifySlowTool, notifyMode, escalationMode, escalationActions, followupInterval, followupMaxReminders, workspaceScope, updateTeamSettings, onSaved, t]);
const userOptions = knownUsers.map((u) => ({ value: u, label: u }));
const channelOptions = CHANNEL_TYPES.map((c) => ({ value: c.value, label: c.label }));
@@ -235,6 +238,13 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps)
</div>
<Switch checked={notifyFailed} onCheckedChange={setNotifyFailed} />
</div>
<div className="flex items-center justify-between">
<div>
<span className="text-sm font-semibold">{t("settings.notifySlowTool")}</span>
<p className="text-xs text-muted-foreground">{t("settings.notifySlowToolHint")}</p>
</div>
<Switch checked={notifySlowTool} onCheckedChange={setNotifySlowTool} />
</div>
<div className="border-t pt-3 space-y-2">
<span className="text-sm font-semibold">{t("settings.notifyMode")}</span>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
+1
View File
@@ -9,6 +9,7 @@ export interface TeamNotifyConfig {
dispatched?: boolean;
progress?: boolean;
failed?: boolean;
slow_tool?: boolean;
mode?: "direct" | "leader";
}