diff --git a/backend/api/server.py b/backend/api/server.py index 87745ce..e51e3ed 100644 --- a/backend/api/server.py +++ b/backend/api/server.py @@ -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 {}, } diff --git a/frontend/app/app/api/claw/sessions/route.ts b/frontend/app/app/api/claw/sessions/route.ts index 3f8e820..b952592 100644 --- a/frontend/app/app/api/claw/sessions/route.ts +++ b/frontend/app/app/api/claw/sessions/route.ts @@ -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", }); diff --git a/frontend/app/app/assistant.tsx b/frontend/app/app/assistant.tsx index e21b107..64c7395 100644 --- a/frontend/app/app/assistant.tsx +++ b/frontend/app/app/assistant.tsx @@ -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; 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(() => - readActiveSessionId(), + const [activeSessionId, setActiveSessionId] = useState( + () => 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 (
{showPipeline ? ( -
退出训练模式 -
+ ) : 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[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(() => - readActiveSessionId(), + const [activeSessionId, setActiveSessionId] = useState( + () => readSessionIdFromUrl() ?? readActiveSessionId(), ); const liveRunStatus = useLiveRunStatus(activeSessionId, mode); const messageItems = useMemo( @@ -203,18 +210,23 @@ export function ActivityPanel({ asDrawer = false }: { asDrawer?: boolean } = {}) const prevLatestIdRef = useRef(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; + 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; + const metadata = + record.metadata && typeof record.metadata === "object" + ? (record.metadata as Record) + : 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 => + Boolean(item && typeof item === "object"), + ) + : []; + const batches = Array.isArray(metadata.delegate_batches) + ? metadata.delegate_batches.filter( + (item): item is Record => + 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; diff --git a/frontend/app/components/assistant-ui/thread.tsx b/frontend/app/components/assistant-ui/thread.tsx index 0f232ac..53d2d59 100644 --- a/frontend/app/components/assistant-ui/thread.tsx +++ b/frontend/app/components/assistant-ui/thread.tsx @@ -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(() => 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; diff --git a/frontend/app/lib/claw-session-url.ts b/frontend/app/lib/claw-session-url.ts index 4beb3d2..4a78098 100644 --- a/frontend/app/lib/claw-session-url.ts +++ b/frontend/app/lib/claw-session-url.ts @@ -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)}`; diff --git a/skills/report-briefing/SKILL.md b/skills/report-briefing/SKILL.md new file mode 100644 index 0000000..3a69f8f --- /dev/null +++ b/skills/report-briefing/SKILL.md @@ -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)`。 +- `GSB:30:54:16` 这种人工对比结论不是百分比指标,可以保持原样;如有采样量,写 `采样 100 条,GSB:30: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>:-> + - <评测集 B>:-> + - 上线指标: + - <线上回放/大盘集>:<指标%(a/b)>,<采样量和 GSB 如有> + - 后续 ToDo: + - <动作 + 对象 + 验收标准> +``` + +### 数据/评测集建设 + +```markdown +- <评测集/训练集建设>: + - <数据集名>( 条),<基线或当前模型>: + - 覆盖范围: + - <类型 A>:<一句定义> + - <例子 1> + - <例子 2> + - <类型 B>:<一句定义> + - <例子 1> + - 后续:<抽审/补齐/清洗/上线评估> +``` + +### 策略评估/下线 + +```markdown +- <策略评估/下线标题> + - 数据分析:<链接> + - <数据集 A>,线上样本 条: + - <分桶 1>: + - <分桶 2>: + - <可优化/可下线/需评测>: + - <数据集 B>,线上样本 条: + - <分桶 1>: + - <分桶 2>: + - <可优化/可下线/需评测>: + - 后续 ToDo: + - <动作 1>:<数量 + 判断标准> + - <动作 2>:<数量 + 覆盖范围> +``` + +字段命名要贴近汇报对象: + +- `模型可优化候选` / `初筛正例评测集(模型可优化)`:用于说明模型能吃掉哪部分。 +- `模型推理非复杂/无可用历史`:用于说明为什么不能直接当正例。 +- `topQuery/缺 prompt`:用于说明是配置或链路口径,需单独治理。 +- `上线指标`:用于说明收益是否安全。 +- `覆盖范围`:用于说明数据集不是随机堆样本,而是覆盖明确问题类型。 + +## topQuery 口径 + +topQuery 不要简单写成“清理 = 不进优化集”。先写结论: + +```markdown +- topQuery 当前不能直接归入模型优化,需要进一步按 query 分析并清理配置。 +``` + +分析时按“配置去留”而不是“是不是复杂多轮”判断: + +- 意图明确的 topQuery:可能保留配置,不作为模型优化目标。例如 `播放音乐`、`关闭音乐`、`打开座椅通风`。 +- 依赖上下文的 topQuery:建议下线固定配置;如果业务仍需召回,再由模型或上下文规则补。例如 `不走高速`、`走高速`、`第二个`、`添加途经点`、`停车场`、`西门`。 + +如果要给领导看,先给覆盖集中度: + +```markdown +- topQuery 影响较大,占全量 ,去重后 个 query;分布集中,Top10 覆盖 ,Top20 覆盖 。 +``` + +## 粒度规则 + +好的汇报句子: + +- `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 条评估 GSB:30:54:16。` +- `高置信正例评测集从 2,431 条模型可优化候选里抽审 500 条,覆盖当前轮复杂、历史复杂后的确认/选择、目的地补槽、路线修改、地图信息承接。` + +避免的写法: + +- 只说“剩余大多是噪声”,但不给占比。 +- 写 `50%` 但没有 `(100/200)`。 +- 一上来写结论,没说当前在评估什么工作。 +- 把 topQuery 直接等同于“不能优化”或“应该删除”。 +- 列很多 query 类型但不说明它们对应什么决策。 +- 写长段解释,让用户再帮忙压缩。 + +## 自检清单 + +输出前检查: + +- 是否先说明了任务背景? +- 所有指标是否都是 `pct%(a/b)` 格式? +- 对比指标是否都是 `old%(a/b)->new%(c/d)` 格式? +- 每个数据集是否有总量、分桶和占比? +- 是否解释了模型优化、策略处理、数据清洗各自的边界? +- topQuery 等配置类问题是否用“配置去留 + 是否需要模型补召回”的视角? +- ToDo 是否能直接变成后续工作项? +- 是否删除了重复解释和过深嵌套? diff --git a/src/agent_runtime.py b/src/agent_runtime.py index 8091fe2..20fdf11 100644 --- a/src/agent_runtime.py +++ b/src/agent_runtime.py @@ -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, diff --git a/src/session_store.py b/src/session_store.py index a53067e..32f1afc 100644 --- a/src/session_store.py +++ b/src/session_store.py @@ -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 {} + ), )