feat(agent): budget reminders, usage while waiting, budget-exceeded status and dedupe
- tool results carry a budget reminder near limits; text before the last tool call is dropped as report when cut by budget/abort - usage persisted when a run suspends, so waiting sessions show spent tokens - task page shows budget_exceeded via business.outcome instead of completed - budget-exceeded decisions merge by root cause (decision / schedule / goal template); new summaries added to investigate allowlist Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
4cd9ec8390
commit
a00a9eaf5d
14 files changed
+205
-21
No files matched your search
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { engineRunStatus } from "./labels.ts";
|
||||
|
||||
describe("任务行状态", () => {
|
||||
const agent = (outcome: string | null) => ({
|
||||
kind: "agent" as const,
|
||||
refId: "r1",
|
||||
title: null,
|
||||
triggeredBy: null,
|
||||
outcome,
|
||||
});
|
||||
it("引擎跑完但 Agent 会话超出预算 / 失败:显示业务结果,不显示「完成」", () => {
|
||||
expect(engineRunStatus({ status: "completed", business: agent("budget_exceeded") })).toEqual({
|
||||
label: "超出预算",
|
||||
tone: "warning",
|
||||
});
|
||||
expect(engineRunStatus({ status: "completed", business: agent("failed") }).label).toBe("失败");
|
||||
expect(engineRunStatus({ status: "completed", business: agent("succeeded") }).label).toBe("完成");
|
||||
});
|
||||
it("引擎自身失败 / 进行中照旧;没有业务对象照旧", () => {
|
||||
expect(engineRunStatus({ status: "failed", business: agent("budget_exceeded") }).label).toBe("失败");
|
||||
expect(engineRunStatus({ status: "running", business: agent("running") }).label).toBe("进行中");
|
||||
expect(engineRunStatus({ status: "completed", business: null }).label).toBe("完成");
|
||||
});
|
||||
});
|
||||
@@ -69,6 +69,22 @@ const RUN_STATUS: Record<string, { label: string; tone: StatusTone }> = {
|
||||
canceled: { label: "已取消", tone: "warning" },
|
||||
};
|
||||
export const runStatus = (s: string) => RUN_STATUS[s] ?? { label: s, tone: "neutral" as StatusTone };
|
||||
/**
|
||||
* 任务行状态:引擎 run 正常结束但业务结果不是成功时以业务结果为准(Agent 会话超出预算 / 失败 / 已停止),
|
||||
* 不把"引擎跑完"显示成「完成」
|
||||
*/
|
||||
const OUTCOME_STATUS: Record<string, { label: string; tone: StatusTone }> = {
|
||||
budget_exceeded: { label: "超出预算", tone: "warning" },
|
||||
failed: { label: "失败", tone: "destructive" },
|
||||
cancelled: { label: "已停止", tone: "warning" },
|
||||
waiting: { label: "等待", tone: "neutral" },
|
||||
};
|
||||
export function engineRunStatus(r: Pick<EngineRun, "status" | "business">) {
|
||||
const outcome = r.business?.outcome;
|
||||
if (outcome && (r.status === "completed" || r.status === "succeeded") && OUTCOME_STATUS[outcome])
|
||||
return OUTCOME_STATUS[outcome];
|
||||
return runStatus(r.status);
|
||||
}
|
||||
export const RUN_STATUS_OPTIONS = ["pending", "running", "completed", "failed", "canceled"].map((value) => ({
|
||||
value,
|
||||
label: runStatus(value).label,
|
||||
|
||||
@@ -15,7 +15,15 @@ import { Button } from "~/components/ui/button.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { formatDuration, formatRelative, spanSec } from "~/lib/labels.ts";
|
||||
import { cn, formatDateTime } from "~/lib/utils.ts";
|
||||
import { BUSINESS_KIND_LABEL, businessLink, runStatus, SOURCE_LABEL, sourceOf, workflowLabel } from "./labels.ts";
|
||||
import {
|
||||
BUSINESS_KIND_LABEL,
|
||||
businessLink,
|
||||
engineRunStatus,
|
||||
runStatus,
|
||||
SOURCE_LABEL,
|
||||
sourceOf,
|
||||
workflowLabel,
|
||||
} from "./labels.ts";
|
||||
|
||||
// 任务详情:业务摘要 → 时间线 → 引擎步骤(折叠)→ 操作。任务页抽屉与 /tasks/:id 共用。
|
||||
|
||||
@@ -125,7 +133,7 @@ function businessTimeline(r: EngineRunDetail): TimelineItem[] {
|
||||
});
|
||||
}
|
||||
if (r.finishedAt) {
|
||||
const s = runStatus(r.status);
|
||||
const s = engineRunStatus(r);
|
||||
items.push({
|
||||
id: "finished",
|
||||
at: r.finishedAt,
|
||||
@@ -337,7 +345,7 @@ export function TaskDetailBody({ r, onOpenRun }: { r: EngineRunDetail; onOpenRun
|
||||
|
||||
/** 头部一行:状态 + 业务类型 */
|
||||
export function TaskStatusLine({ r }: { r: EngineRunDetail }) {
|
||||
const s = runStatus(r.status);
|
||||
const s = engineRunStatus(r);
|
||||
return (
|
||||
<span className="flex flex-wrap items-center gap-2">
|
||||
<StatusBadge tone={s.tone}>{s.label}</StatusBadge>
|
||||
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
BUSINESS_KINDS,
|
||||
businessLink,
|
||||
ENGINE_ACTIVE,
|
||||
engineRunStatus,
|
||||
RUN_STATUS_OPTIONS,
|
||||
runStatus,
|
||||
runTitle,
|
||||
SOURCE_LABEL,
|
||||
SOURCE_TRIGGERED_BY,
|
||||
@@ -247,7 +247,7 @@ export function TaskStream({
|
||||
key: "status",
|
||||
header: "状态",
|
||||
cell: (r) => {
|
||||
const s = runStatus(r.status);
|
||||
const s = engineRunStatus(r);
|
||||
return <StatusBadge tone={s.tone}>{s.label}</StatusBadge>;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -35,7 +35,7 @@ import { StatusBadge, type StatusTone } from "~/components/console/status-badge.
|
||||
import { useVolumeLabel } from "~/components/downloads/cache-volumes.tsx";
|
||||
import { countdown, RUN_STATUS } from "~/components/schedules/labels.ts";
|
||||
import { useLiveSchedules, useNow } from "~/components/schedules/schedule-board.tsx";
|
||||
import { runStatus, runTitle, workflowLabel } from "~/components/tasks/labels.ts";
|
||||
import { engineRunStatus, runTitle, workflowLabel } from "~/components/tasks/labels.ts";
|
||||
import { useLiveTasks } from "~/components/tasks/task-stream.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { Switch } from "~/components/ui/switch.tsx";
|
||||
@@ -422,7 +422,7 @@ function RecentRuns() {
|
||||
) : (
|
||||
<ul className="divide-y divide-border/60">
|
||||
{rows.map((r) => {
|
||||
const s = runStatus(r.status);
|
||||
const s = engineRunStatus(r);
|
||||
return (
|
||||
<Row key={r.id}>
|
||||
<Link to={`/console/tasks?run=${encodeURIComponent(r.id)}`} className="min-w-0 flex-1 hover:underline">
|
||||
|
||||
Reference in new issue
Block a user