fix session routing and subagent visibility
This commit is contained in:
+16
-1
@@ -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 {},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)}`;
|
||||
|
||||
@@ -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>:<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 条评估 GSB:30:54:16。`
|
||||
- `高置信正例评测集从 2,431 条模型可优化候选里抽审 500 条,覆盖当前轮复杂、历史复杂后的确认/选择、目的地补槽、路线修改、地图信息承接。`
|
||||
|
||||
避免的写法:
|
||||
|
||||
- 只说“剩余大多是噪声”,但不给占比。
|
||||
- 写 `50%` 但没有 `(100/200)`。
|
||||
- 一上来写结论,没说当前在评估什么工作。
|
||||
- 把 topQuery 直接等同于“不能优化”或“应该删除”。
|
||||
- 列很多 query 类型但不说明它们对应什么决策。
|
||||
- 写长段解释,让用户再帮忙压缩。
|
||||
|
||||
## 自检清单
|
||||
|
||||
输出前检查:
|
||||
|
||||
- 是否先说明了任务背景?
|
||||
- 所有指标是否都是 `pct%(a/b)` 格式?
|
||||
- 对比指标是否都是 `old%(a/b)->new%(c/d)` 格式?
|
||||
- 每个数据集是否有总量、分桶和占比?
|
||||
- 是否解释了模型优化、策略处理、数据清洗各自的边界?
|
||||
- topQuery 等配置类问题是否用“配置去留 + 是否需要模型补召回”的视角?
|
||||
- ToDo 是否能直接变成后续工作项?
|
||||
- 是否删除了重复解释和过深嵌套?
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user