fix session routing and subagent visibility

This commit is contained in:
wuyang6
2026-06-12 11:06:04 +08:00
parent a6e7d2260b
commit 40d7c92a1d
9 changed files with 447 additions and 45 deletions
+16 -1
View File
@@ -1659,7 +1659,10 @@ def create_app(state: AgentState) -> FastAPI:
# ------------- sessions --------------------------------------------------
@app.get('/api/sessions')
async def list_sessions(account_id: str | None = None) -> list[dict[str, Any]]:
async def list_sessions(
account_id: str | None = None,
include_children: bool = False,
) -> list[dict[str, Any]]:
directory = state.account_paths(account_id)['sessions']
if not directory.exists():
return []
@@ -1670,6 +1673,16 @@ def create_app(state: AgentState) -> FastAPI:
except (OSError, json.JSONDecodeError):
continue
session_id = str(data.get('session_id', _session_id_from_path(path)))
session_metadata = (
data.get('session_metadata')
if isinstance(data.get('session_metadata'), dict)
else {}
)
if (
not include_children
and session_metadata.get('visibility') == 'child'
):
continue
usage = data.get('usage') if isinstance(data.get('usage'), dict) else {}
model_config = (
data.get('model_config')
@@ -1695,6 +1708,7 @@ def create_app(state: AgentState) -> FastAPI:
'model': model_config.get('model'),
'usage': usage,
'is_training': bool(data.get('is_training', False)),
'session_metadata': session_metadata,
}
)
return results
@@ -2769,6 +2783,7 @@ def _serialize_stored_session(stored: StoredAgentSession) -> dict[str, Any]:
'total_cost_usd': stored.total_cost_usd,
'model': stored.model_config.get('model'),
'is_training': stored.is_training,
'session_metadata': stored.session_metadata or {},
}
+6 -1
View File
@@ -2,12 +2,17 @@ import { getCurrentAccount } from "@/lib/claw-auth";
const CLAW_API_URL = process.env.CLAW_API_URL ?? "http://127.0.0.1:8765";
export async function GET() {
export async function GET(request: Request) {
const account = await getCurrentAccount();
if (!account) return Response.json([], { status: 401 });
const incoming = new URL(request.url);
const url = new URL(`${CLAW_API_URL}/api/sessions`);
url.searchParams.set("account_id", account.id);
const includeChildren = incoming.searchParams.get("include_children");
if (includeChildren) {
url.searchParams.set("include_children", includeChildren);
}
const response = await fetch(url, {
cache: "no-store",
});
+24 -19
View File
@@ -1,10 +1,7 @@
"use client";
import type { ExportedMessageRepository } from "@assistant-ui/core";
import {
AssistantRuntimeProvider,
useAui,
} from "@assistant-ui/react";
import { AssistantRuntimeProvider, useAui } from "@assistant-ui/react";
import {
AssistantChatTransport,
useChatRuntime,
@@ -38,14 +35,14 @@ import {
writeActiveSessionId,
} from "@/lib/claw-active-session";
import { ClawSessionReplayProvider } from "@/lib/claw-session-replay";
import { pushSessionUrl } from "@/lib/claw-session-url";
import { cn } from "@/lib/utils";
import { pushSessionUrl, readSessionIdFromUrl } from "@/lib/claw-session-url";
import {
SKILL_TOGGLED_EVENT,
type SkillToggledDetail,
useAppliedTraining,
useModelIterationEnabled,
} from "@/lib/use-training-mode";
import { cn } from "@/lib/utils";
import { ClawAccountGate, useClawAccount } from "./claw-account-gate";
type AssistantProps = {
@@ -62,10 +59,16 @@ export const Assistant = ({ initialSessionId }: AssistantProps = {}) => {
prepareSendMessagesRequest: async (options) => {
const body = options.body as Record<string, unknown>;
const messages = options.messages as UIMessage[];
const selectedSessionId = readActiveSessionId();
const selectedSessionId =
readSessionIdFromUrl() ??
readActiveSessionId() ??
initialSessionId?.trim() ??
null;
const lastSessionId = getLastSessionId(messages);
const pendingWorkspaceSessionId =
messages.length <= 1 ? readPendingWorkspaceSessionId() : null;
!selectedSessionId && messages.length <= 1
? readPendingWorkspaceSessionId()
: null;
const selectedResumeSessionId = selectedSessionId;
const outgoingSessionId =
lastSessionId ??
@@ -89,7 +92,7 @@ export const Assistant = ({ initialSessionId }: AssistantProps = {}) => {
};
},
}),
[],
[initialSessionId],
);
const runtime = useChatRuntime({
transport,
@@ -195,11 +198,10 @@ function getDraftKey(activeSessionId: string | null): string {
return activeSessionId;
}
function AssistantWorkspace() {
const aui = useAui();
const [activeSessionId, setActiveSessionId] = useState<string | null>(() =>
readActiveSessionId(),
const [activeSessionId, setActiveSessionId] = useState<string | null>(
() => readSessionIdFromUrl() ?? readActiveSessionId(),
);
const skill = useModelIterationEnabled();
const { applied, setApplied } = useAppliedTraining(activeSessionId);
@@ -250,14 +252,15 @@ function AssistantWorkspace() {
useEffect(() => {
const update = () => {
const next = readActiveSessionId();
const next = readSessionIdFromUrl() ?? readActiveSessionId();
console.log("[active-session] focus-update", { next });
setActiveSessionId(next);
};
const handleChanged = (event: Event) => {
const detail = (event as CustomEvent<{ sessionId?: string | null }>)
.detail;
const next = detail?.sessionId ?? readActiveSessionId();
const next =
detail?.sessionId ?? readSessionIdFromUrl() ?? readActiveSessionId();
console.log("[active-session] event-changed", {
detailSessionId: detail?.sessionId,
next,
@@ -266,10 +269,12 @@ function AssistantWorkspace() {
};
window.addEventListener(ACTIVE_SESSION_CHANGED_EVENT, handleChanged);
window.addEventListener("focus", update);
window.addEventListener("popstate", update);
update();
return () => {
window.removeEventListener(ACTIVE_SESSION_CHANGED_EVENT, handleChanged);
window.removeEventListener("focus", update);
window.removeEventListener("popstate", update);
};
}, []);
@@ -294,8 +299,7 @@ function AssistantWorkspace() {
"textarea.aui-composer-input",
);
const composerState = aui.composer().getState();
const savingText =
composerEl?.value ?? composerState.text ?? "";
const savingText = composerEl?.value ?? composerState.text ?? "";
cache.set(prevKey, savingText);
// newTask 里点工作区会**新生成**一个 LOCALID 并切过去,用户视角里
// 这是同一份草稿的延续——把 newTask 的文本继承到 LOCALID 名下,
@@ -326,6 +330,7 @@ function AssistantWorkspace() {
prevSessionKeyRef.current = nextKey;
}, [activeSessionId, aui]);
// biome-ignore lint/correctness/useExhaustiveDependencies: close the activity drawer whenever training mode toggles.
useEffect(() => {
closeActivityPanel();
}, [showPipeline, closeActivityPanel]);
@@ -349,7 +354,8 @@ function AssistantWorkspace() {
return (
<div className="flex min-h-0 flex-1 overflow-hidden">
{showPipeline ? (
<div
<section
aria-label="训练模式面板"
ref={pipelineContainerRef}
onMouseMove={handlePipelineMouseMove}
onMouseLeave={handlePipelineMouseLeave}
@@ -369,7 +375,7 @@ function AssistantWorkspace() {
<span>退</span>
</button>
<TrainingPipelinePanel sessionId={activeSessionId} />
</div>
</section>
) : null}
<div
className={cn(
@@ -387,7 +393,6 @@ function AssistantWorkspace() {
);
}
function ActivityDrawerTrigger() {
const { open, openActivity } = useActivityPanel();
if (open) return null;
@@ -47,6 +47,7 @@ import {
readActiveSessionId,
writeActiveSessionId,
} from "@/lib/claw-active-session";
import { readSessionIdFromUrl } from "@/lib/claw-session-url";
import { cn } from "@/lib/utils";
type ActivityContextValue = {
@@ -81,7 +82,9 @@ export function ActivityProvider({ children }: { children: ReactNode }) {
}, []);
const openFiles = useCallback((nextSessionId?: string | null) => {
setMode("files");
setSessionId(nextSessionId ?? readActiveSessionId());
setSessionId(
nextSessionId ?? readSessionIdFromUrl() ?? readActiveSessionId(),
);
setFilesRefreshToken((value) => value + 1);
setOpen(true);
}, []);
@@ -141,7 +144,11 @@ type ActivityItem = {
type LiveRunEvent = NonNullable<ClawRunStatus["events"]>[number];
export function ActivityPanel({ asDrawer = false }: { asDrawer?: boolean } = {}) {
export function ActivityPanel({
asDrawer = false,
}: {
asDrawer?: boolean;
} = {}) {
const {
open,
mode,
@@ -153,8 +160,8 @@ export function ActivityPanel({ asDrawer = false }: { asDrawer?: boolean } = {})
close,
} = useActivityPanel();
const messages = useAuiState((s) => s.thread.messages);
const [activeSessionId, setActiveSessionId] = useState<string | null>(() =>
readActiveSessionId(),
const [activeSessionId, setActiveSessionId] = useState<string | null>(
() => readSessionIdFromUrl() ?? readActiveSessionId(),
);
const liveRunStatus = useLiveRunStatus(activeSessionId, mode);
const messageItems = useMemo(
@@ -203,18 +210,23 @@ export function ActivityPanel({ asDrawer = false }: { asDrawer?: boolean } = {})
const prevLatestIdRef = useRef<string | null>(null);
useEffect(() => {
const update = () => setActiveSessionId(readActiveSessionId());
const update = () =>
setActiveSessionId(readSessionIdFromUrl() ?? readActiveSessionId());
const handleChanged = (event: Event) => {
const detail = (event as CustomEvent<{ sessionId?: string | null }>)
.detail;
setActiveSessionId(detail?.sessionId ?? readActiveSessionId());
setActiveSessionId(
detail?.sessionId ?? readSessionIdFromUrl() ?? readActiveSessionId(),
);
};
window.addEventListener(ACTIVE_SESSION_CHANGED_EVENT, handleChanged);
window.addEventListener("focus", update);
window.addEventListener("popstate", update);
update();
return () => {
window.removeEventListener(ACTIVE_SESSION_CHANGED_EVENT, handleChanged);
window.removeEventListener("focus", update);
window.removeEventListener("popstate", update);
};
}, []);
@@ -373,7 +385,7 @@ function SessionFilesPanel({
let cancelled = false;
async function loadFiles() {
const activeSessionId = normalizeSessionId(
sessionId ?? readActiveSessionId(),
sessionId ?? readSessionIdFromUrl() ?? readActiveSessionId(),
);
setStatus("正在读取聊天中的文件...");
try {
@@ -1019,6 +1031,86 @@ function collectLiveRunItems(runStatus: ClawRunStatus | null) {
),
});
}
items.push(...collectLiveDelegateItems(runStatus, events));
return items;
}
function collectLiveDelegateItems(
runStatus: ClawRunStatus,
events: readonly LiveRunEvent[],
): ActivityItem[] {
const items: ActivityItem[] = [];
const prefix = `live:${runStatus.run_id ?? runStatus.session_id ?? "active"}`;
for (const event of events) {
const record = event as Record<string, unknown>;
if (event.type === "delegate_group_result") {
const total = asNumber(record.subtask_count);
const completed = asNumber(record.completed_children);
const failed = asNumber(record.failed_children);
const skipped = asNumber(record.dependency_skips);
const batchCount = asNumber(record.batch_count);
items.push({
id: `${prefix}:${event.tool_call_id ?? "delegate"}:group`,
kind: "reasoning",
title: "子 Agent 汇总",
summary: [
total ? `${total} 个子任务` : "子任务完成",
`完成 ${completed}`,
failed ? `失败 ${failed}` : null,
skipped ? `跳过 ${skipped}` : null,
batchCount ? `${batchCount}` : null,
asText(record.group_status)
? `状态 ${asText(record.group_status)}`
: null,
]
.filter(Boolean)
.join(""),
status: failed ? "incomplete" : "complete",
});
}
if (event.type === "delegate_batch_result") {
const labels = Array.isArray(record.labels)
? record.labels.map((item) => String(item)).join(", ")
: "";
const failed = asNumber(record.failed_children);
items.push({
id: `${prefix}:${event.tool_call_id ?? "delegate"}:batch:${asText(record.batch_index) || items.length}`,
kind: "reasoning",
title: `子 Agent 批次 ${asText(record.batch_index) || ""}`.trim(),
summary: [
asText(record.status) || "completed",
labels,
`完成 ${asNumber(record.completed_children)}`,
failed ? `失败 ${failed}` : null,
asNumber(record.skipped_children)
? `跳过 ${asNumber(record.skipped_children)}`
: null,
]
.filter(Boolean)
.join(""),
status: failed ? "incomplete" : "complete",
});
}
if (event.type === "delegate_subtask_result") {
const failed = asText(record.stop_reason) === "backend_error";
items.push({
id: `${prefix}:${event.tool_call_id ?? "delegate"}:child:${asText(record.index) || items.length}`,
kind: "tool",
title: `子 Agent${asText(record.label) || "subtask"}`,
summary: [
asText(record.stop_reason) || "stop",
`turns=${asNumber(record.turns)}`,
`tools=${asNumber(record.tool_calls)}`,
asText(record.session_id)
? `session=${asText(record.session_id)}`
: null,
]
.filter(Boolean)
.join(""),
status: failed ? "incomplete" : "complete",
});
}
}
return items;
}
@@ -1031,21 +1123,13 @@ function mergeActivityItems(
const merged = [...messageItems];
const seen = new Set(
messageItems.map((item) =>
[
item.kind,
item.title,
item.argsText ?? "",
item.summary,
].join("\u0000"),
[item.kind, item.title, item.argsText ?? "", item.summary].join("\u0000"),
),
);
for (const item of liveItems) {
const key = [
item.kind,
item.title,
item.argsText ?? "",
item.summary,
].join("\u0000");
const key = [item.kind, item.title, item.argsText ?? "", item.summary].join(
"\u0000",
);
if (seen.has(key)) continue;
seen.add(key);
merged.push(item);
@@ -1251,6 +1335,11 @@ function summarizeTool(
part: ToolCallMessagePart,
) {
const status = getPartStatus(message, part);
if (isDelegateTool(part.toolName)) {
if (status === "running") return "子 Agent 执行中";
const delegateSummary = summarizeDelegateToolResult(part.result);
if (delegateSummary) return delegateSummary.compact;
}
if (status === "running") return "调用工具中";
if (status === "incomplete") return "工具调用未完成";
if (part.result === undefined) return "已提交工具参数";
@@ -1276,6 +1365,8 @@ function formatValue(value: unknown) {
function summarizeToolResult(value: unknown) {
const decoded = decodeJsonString(value);
if (decoded === undefined) return undefined;
const delegateSummary = summarizeDelegateToolResult(decoded);
if (delegateSummary) return delegateSummary.detail;
if (typeof decoded === "string") return trimText(decoded, 600);
if (!decoded || typeof decoded !== "object") return String(decoded);
@@ -1291,6 +1382,99 @@ function summarizeToolResult(value: unknown) {
return parts.join("\n");
}
function isDelegateTool(toolName?: string) {
return toolName === "Agent" || toolName === "delegate_agent";
}
function summarizeDelegateToolResult(value: unknown) {
const decoded = decodeJsonString(value);
if (!decoded || typeof decoded !== "object") return null;
const record = decoded as Record<string, unknown>;
const metadata =
record.metadata && typeof record.metadata === "object"
? (record.metadata as Record<string, unknown>)
: record;
if (metadata.action !== "Agent" && metadata.action !== "delegate_agent") {
return null;
}
const childResults = Array.isArray(metadata.child_results)
? metadata.child_results.filter((item): item is Record<string, unknown> =>
Boolean(item && typeof item === "object"),
)
: [];
const batches = Array.isArray(metadata.delegate_batches)
? metadata.delegate_batches.filter(
(item): item is Record<string, unknown> =>
Boolean(item && typeof item === "object"),
)
: [];
const total =
asNumber(metadata.subtask_count) ||
childResults.length ||
asNumber(metadata.completed_children) + asNumber(metadata.failed_children);
const completed = asNumber(metadata.completed_children);
const failed = asNumber(metadata.failed_children);
const skipped = asNumber(metadata.dependency_skips);
const groupStatus = asText(metadata.group_status);
const strategy = asText(metadata.strategy);
const compactParts = [
total ? `${total} 个子 Agent` : "子 Agent",
completed || failed || skipped
? `完成 ${completed} / 失败 ${failed} / 跳过 ${skipped}`
: null,
groupStatus ? `状态 ${groupStatus}` : null,
].filter(Boolean);
const compact = compactParts.join("");
const lines = [compact];
if (strategy) lines.push(`策略:${strategy}`);
if (batches.length) {
lines.push(`批次:${batches.length}`);
for (const batch of batches) {
const labels = Array.isArray(batch.labels)
? batch.labels.map((item) => String(item)).join(", ")
: "";
lines.push(
`- batch ${asText(batch.batch_index) || "?"}: ${asText(batch.status) || "unknown"}${labels ? ` · ${labels}` : ""}`,
);
}
}
if (childResults.length) {
lines.push("子任务:");
for (const child of childResults) {
const label =
asText(child.label) || `subtask_${asText(child.index) || "?"}`;
const sessionId = asText(child.session_id);
const stopReason = asText(child.stop_reason) || "stop";
const turns = asNumber(child.turns);
const toolCalls = asNumber(child.tool_calls);
const preview = asText(child.output_preview);
lines.push(
[
`- ${label}: ${stopReason}`,
turns ? `turns=${turns}` : null,
toolCalls ? `tools=${toolCalls}` : null,
sessionId ? `session=${sessionId}` : null,
]
.filter(Boolean)
.join(""),
);
if (preview) lines.push(` ${trimText(preview, 180)}`);
}
}
return { compact, detail: lines.join("\n") };
}
function asText(value: unknown) {
if (value === null || value === undefined) return "";
return typeof value === "string" ? value : String(value);
}
function asNumber(value: unknown) {
return typeof value === "number" && Number.isFinite(value) ? value : 0;
}
function trimText(text: string, maxLength: number) {
const normalized = text.trim();
if (normalized.length <= maxLength) return normalized;
@@ -92,8 +92,9 @@ import {
writePendingWorkspaceSessionId,
} from "@/lib/claw-active-session";
import { useClawSessionReplay } from "@/lib/claw-session-replay";
import { cn } from "@/lib/utils";
import { readSessionIdFromUrl } from "@/lib/claw-session-url";
import { dispatchSkillToggled } from "@/lib/use-training-mode";
import { cn } from "@/lib/utils";
type ComposerInsertEvent = CustomEvent<{ text: string }>;
@@ -396,21 +397,25 @@ function usePendingWorkspaceSessionId() {
function useActiveSessionId() {
const [sessionId, setSessionId] = useState<string | null>(() =>
typeof window !== "undefined"
? normalizeSessionId(readActiveSessionId())
? normalizeSessionId(readSessionIdFromUrl() ?? readActiveSessionId())
: null,
);
useEffect(() => {
const refresh = () => {
setSessionId(normalizeSessionId(readActiveSessionId()));
setSessionId(
normalizeSessionId(readSessionIdFromUrl() ?? readActiveSessionId()),
);
};
window.addEventListener(ACTIVE_SESSION_CHANGED_EVENT, refresh);
window.addEventListener("storage", refresh);
window.addEventListener("focus", refresh);
window.addEventListener("popstate", refresh);
return () => {
window.removeEventListener(ACTIVE_SESSION_CHANGED_EVENT, refresh);
window.removeEventListener("storage", refresh);
window.removeEventListener("focus", refresh);
window.removeEventListener("popstate", refresh);
};
}, []);
@@ -1349,7 +1354,9 @@ function useComposerContextStatus() {
const statePayload = stateResponse.ok
? ((await stateResponse.json()) as ClawState)
: {};
const activeSessionId = normalizeSessionId(readActiveSessionId());
const activeSessionId = normalizeSessionId(
readSessionIdFromUrl() ?? readActiveSessionId(),
);
const latestMessageSessionId = normalizeSessionId(latestSessionId);
let sessionId = normalizeSessionId(
isRunning
@@ -2088,7 +2095,10 @@ const ActivityChainSummary: FC<{
liveStartedAtRef.current = authoritativeStartedAt;
} else if (liveStartedAtRef.current === null && storedElapsedMs !== null) {
liveStartedAtRef.current = Date.now() - storedElapsedMs;
} else if (liveStartedAtRef.current === null && messageCreatedAtMs !== null) {
} else if (
liveStartedAtRef.current === null &&
messageCreatedAtMs !== null
) {
liveStartedAtRef.current = messageCreatedAtMs;
}
const startedAt = liveStartedAtRef.current;
+11
View File
@@ -1,5 +1,16 @@
"use client";
export function readSessionIdFromUrl() {
if (typeof window === "undefined") return null;
const match = window.location.pathname.match(/^\/session\/([^/]+)$/);
if (!match?.[1]) return null;
try {
return decodeURIComponent(match[1]).trim() || null;
} catch {
return match[1].trim() || null;
}
}
export function pushSessionUrl(sessionId: string) {
if (typeof window === "undefined") return;
const target = `/session/${encodeURIComponent(sessionId)}`;
+155
View File
@@ -0,0 +1,155 @@
---
name: report-briefing
description: 将数据分析、模型优化、策略下线、评测集建设、上线指标、badcase 复盘等内容整理成领导汇报版本。适合用户要求“汇报一下”“30 秒版本”“简洁结论”“给领导看”“整理回报/汇报 skill”,或需要把复杂分析产物压缩成背景、关键数据、例子、结论和后续 ToDo。
when_to_use: 用户希望把数据分析或项目进展转成汇报口径、复盘为什么之前表达不适合汇报、统一不同类型周报/项目回报格式、或需要一版面向决策者的简洁分点材料时使用。
aliases: leadership-report, exec-brief, report-summary, 汇报, 回报, 领导汇报
allowed_tools: read_file, write_file, edit_file, grep_search, glob_search, python_exec
---
# Report Briefing Skill
使用这个 skill 把分析结果压缩成“领导能快速判断进展、收益、风险和下一步”的汇报,不写长篇分析报告。
## 核心原则
1. 先交代“我们在做什么工作”和当前状态,再给数据和结论。不要一上来只写“结论”。
2. 任何指标必须同时给百分比和分子分母,格式强制为 `50%(100/200)`;对比指标写成 `old%(a/b)->new%(c/d)`
3. 区分“指标”和“规模”:准确率、召回率、diff 率、基线、覆盖率是指标,必须带分子分母;训练集 578 条、评测集 935 条是规模,可只写数量。
4. 用少量关键数据支撑判断,不展开所有中间分析。每个数据集或评测集只保留最能支持决策的 2-4 个数。
5. 例子只服务于解释覆盖范围或典型问题,每类 1-2 个。不要把样例堆成样本列表。
6. 结构可以因任务变化,但每段都要回答一个决策问题:做了什么、效果如何、风险是什么、下一步做什么。
7. ToDo 用业务动作命名:清洗、抽审、构建评测集、模型优化、上线评估、离线对比、下线评估。不要写泛泛的“继续分析”。
## 指标格式硬约束
所有带百分比的指标必须写成:
```text
指标名:50%(100/200)
指标名:50%(100/200)->80%(160/200)
指标名:50%(100/200)->80%(160/200)+30%
```
注意:
- 分子分母之间不要省略;`58.18%->73.9%` 这种不合格,除非原始材料确实没有分子分母,此时标注为 `58.18%->73.9%(缺分子分母)`
- 同一行内有多个指标时,每个指标都要带自己的 `(a/b)`
- `GSB30:54:16` 这种人工对比结论不是百分比指标,可以保持原样;如有采样量,写 `采样 100 条,GSB30:54:16`
- 空提升或从 0 起步写 `0%(0/n)->93.72%(388/414)`,不要写 `0->93.72%`
## 汇报的“魂”
无论内容是模型优化、策略下线、评测集建设还是上线评估,优先抽成下面几个信息块,按需要取舍:
- **工作项**:一句话说明项目目标和当前状态,例如“快慢分发问题优化,模型已上线”。
- **关键收益**:列最重要的离线/线上指标变化,使用 `old%(a/b)->new%(c/d)`
- **安全性/上线风险**:列大盘集、TOP diff、Random diff、GSB 等兜底指标。
- **数据建设**:写清评测集/训练集规模、当前基线、覆盖范围、典型例子。
- **问题定位**:用 1-3 个分桶说明不能优化、需要策略处理、需要人工清洗的原因。
- **后续 ToDo**:每项有动作、对象、数量或验收标准。
## 通用输出骨架
### 项目进展/模型优化
```markdown
- <项目/问题>
- <进展状态><已完成训练/已上线/preview 评测中/待下线评估>
- 模型优化:
- <评测集 A><old%(a/b)>-><new%(c/d)>
- <评测集 B><old%(a/b)>-><new%(c/d)>
- 上线指标:
- <线上回放/大盘集><指标%(a/b)><采样量和 GSB 如有>
- 后续 ToDo
- <动作 + 对象 + 验收标准>
```
### 数据/评测集建设
```markdown
- <评测集/训练集建设>
- <数据集名><N> 条),<基线或当前模型><pct%(a/b)>
- 覆盖范围:
- <类型 A><一句定义>
- <例子 1>
- <例子 2>
- <类型 B><一句定义>
- <例子 1>
- 后续:<抽审/补齐/清洗/上线评估>
```
### 策略评估/下线
```markdown
- <策略评估/下线标题>
- 数据分析:<链接>
- <数据集 A>,线上样本 <N> 条:
- <分桶 1><p%(n/N)>
- <分桶 2><p%(n/N)>
- <可优化/可下线/需评测><p%(n/N)>
- <数据集 B>,线上样本 <N> 条:
- <分桶 1><p%(n/N)>
- <分桶 2><p%(n/N)>
- <可优化/可下线/需评测><p%(n/N)>
- 后续 ToDo
- <动作 1><数量 + 判断标准>
- <动作 2><数量 + 覆盖范围>
```
字段命名要贴近汇报对象:
- `模型可优化候选` / `初筛正例评测集(模型可优化)`:用于说明模型能吃掉哪部分。
- `模型推理非复杂/无可用历史`:用于说明为什么不能直接当正例。
- `topQuery/缺 prompt`:用于说明是配置或链路口径,需单独治理。
- `上线指标`:用于说明收益是否安全。
- `覆盖范围`:用于说明数据集不是随机堆样本,而是覆盖明确问题类型。
## topQuery 口径
topQuery 不要简单写成“清理 = 不进优化集”。先写结论:
```markdown
- topQuery 当前不能直接归入模型优化,需要进一步按 query 分析并清理配置。
```
分析时按“配置去留”而不是“是不是复杂多轮”判断:
- 意图明确的 topQuery:可能保留配置,不作为模型优化目标。例如 `播放音乐``关闭音乐``打开座椅通风`
- 依赖上下文的 topQuery:建议下线固定配置;如果业务仍需召回,再由模型或上下文规则补。例如 `不走高速``走高速``第二个``添加途经点``停车场``西门`
如果要给领导看,先给覆盖集中度:
```markdown
- topQuery 影响较大,占全量 <p%(n/N)>,去重后 <k> 个 query;分布集中,Top10 覆盖 <p10%(n10/n)>Top20 覆盖 <p20%(n20/n)>。
```
## 粒度规则
好的汇报句子:
- `processing 策略召回,线上样本 7,460 条:topQuery 22.4%(1,671/7,460),模型推理非复杂 57.8%(4,310/7,460),模型可优化 19.8%(1,479/7,460)。`
- `车控意向性评测集:56.88%(372/654)->95.26%(605/654)。`
- `TOP diff 率:3.96%(485/12,260),采样 100 条评估 GSB30:54:16。`
- `高置信正例评测集从 2,431 条模型可优化候选里抽审 500 条,覆盖当前轮复杂、历史复杂后的确认/选择、目的地补槽、路线修改、地图信息承接。`
避免的写法:
- 只说“剩余大多是噪声”,但不给占比。
-`50%` 但没有 `(100/200)`
- 一上来写结论,没说当前在评估什么工作。
- 把 topQuery 直接等同于“不能优化”或“应该删除”。
- 列很多 query 类型但不说明它们对应什么决策。
- 写长段解释,让用户再帮忙压缩。
## 自检清单
输出前检查:
- 是否先说明了任务背景?
- 所有指标是否都是 `pct%(a/b)` 格式?
- 对比指标是否都是 `old%(a/b)->new%(c/d)` 格式?
- 每个数据集是否有总量、分桶和占比?
- 是否解释了模型优化、策略处理、数据清洗各自的边界?
- topQuery 等配置类问题是否用“配置去留 + 是否需要模型补召回”的视角?
- ToDo 是否能直接变成后续工作项?
- 是否删除了重复解释和过深嵌套?
+11
View File
@@ -282,6 +282,7 @@ class LocalCodingAgent:
managed_group_id: str | None = None
managed_child_index: int | None = None
managed_label: str | None = None
session_metadata: dict[str, object] = field(default_factory=dict)
plugin_runtime: PluginRuntime | None = None
hook_policy_runtime: HookPolicyRuntime | None = None
mcp_runtime: MCPRuntime | None = None
@@ -3445,6 +3446,15 @@ class LocalCodingAgent:
managed_group_id=group_id,
managed_child_index=index,
managed_label=subtask_label,
session_metadata={
'visibility': 'child',
'parent_session_id': self.active_session_id or '',
'subagent_type': agent_def.agent_type,
'delegate_label': subtask_label,
'delegate_index': index,
'delegate_batch_index': batch_index,
**({'delegate_group_id': group_id} if group_id is not None else {}),
},
)
if self.tool_context.jupyter_runtime is not None:
child_agent.tool_context = replace(
@@ -4242,6 +4252,7 @@ class LocalCodingAgent:
else {}
),
scratchpad_directory=result.scratchpad_directory,
session_metadata=dict(self.session_metadata),
)
path = save_agent_session(
stored,
+6
View File
@@ -69,6 +69,7 @@ class StoredAgentSession:
plugin_state: JSONDict
scratchpad_directory: str | None = None
is_training: bool = False
session_metadata: JSONDict | None = None
def save_agent_session(session: StoredAgentSession, directory: Path | None = None) -> Path:
@@ -120,6 +121,11 @@ def load_agent_session(session_id: str, directory: Path | None = None) -> Stored
else None
),
is_training=bool(data.get('is_training', False)),
session_metadata=(
dict(data.get('session_metadata', {}))
if isinstance(data.get('session_metadata'), dict)
else {}
),
)