refactor(agent): Pi 会话运行时 - 接入原生 Agent 与 Skills 工具链

This commit is contained in:
ddcat666 committed 2026-09-29 08:21:11 +08:00
1 parent f9b8c5a43e
commit 387d3562ad
69 files changed
+11460 -207

No files matched your search

@@ -691,7 +691,6 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, se
const result = submission.parentRunId ? await sendAgentMessage(submission.parentRunId, request) : await createAgentRun(request);
accepted = true;
if (currentScope.current === scope) {
setContextUsage(emptyAgentContextUsage(result.run.id));
setRun(result.run);
}
await clearCloudAgentPendingSubmission(canvasId, activeConversationId);
@@ -1248,7 +1247,7 @@ function AgentContextRing({ view }: { view: AgentContextUsageView }) {
getPopupContainer={(trigger) => trigger.closest<HTMLElement>(".canvas-agent-panel") ?? document.body}
content={
<div className="agent-context-panel" data-phase={view.phase}>
<span className="agent-context-eyebrow">下一次请求</span>
<span className="agent-context-eyebrow">下一次请求 · 上下文窗口占用</span>
<div className="agent-context-panel-head">
<strong>{usageHeading}</strong>
{view.phase !== "ok" ? <span className={`agent-context-phase is-${view.phase}`}>{phaseLabel}</span> : null}
@@ -1268,7 +1267,7 @@ function AgentContextRing({ view }: { view: AgentContextUsageView }) {
)}
</div>
<div className="agent-context-progress-head">
<span>输入预算占用</span>
<span>上下文窗口占用</span>
<strong>{percent}</strong>
</div>
<div className="agent-context-progress" role="progressbar" aria-label={`上下文已用 ${percent}`} aria-valuemin={0} aria-valuemax={100} aria-valuenow={view.ratio === undefined ? undefined : Math.round(view.ratio * 100)}>
+21 -10
View File
@@ -87,15 +87,26 @@ export function mergeAgentCanvasEditor(previous: CanvasProject, incoming: Canvas
const changes = <T extends { id: string }>(before: T[], after: T[]): Change<T>[] => {
const byId = new Map(before.map((item) => [item.id, item]));
const afterIds = new Set(after.map((item) => item.id));
return [
...after.filter((item) => !equal(item, byId.get(item.id))).map((item) => ({ before: byId.get(item.id) ?? null, after: item })),
...before.filter((item) => !afterIds.has(item.id)).map((item) => ({ before: item, after: null })),
];
return [...after.filter((item) => !equal(item, byId.get(item.id))).map((item) => ({ before: byId.get(item.id) ?? null, after: item })), ...before.filter((item) => !afterIds.has(item.id)).map((item) => ({ before: item, after: null }))];
};
return applyAgentCanvasPatch({ ...previous, nodes, connections }, {
canvasId: previous.id,
updatedAt: incoming.updatedAt,
nodes: changes(previous.nodes, incoming.nodes),
connections: changes(previous.connections, incoming.connections),
});
const projected = applyAgentCanvasPatch(
{ ...previous, nodes, connections },
{
canvasId: previous.id,
updatedAt: incoming.updatedAt,
nodes: changes(previous.nodes, incoming.nodes),
connections: changes(previous.connections, incoming.connections),
},
);
const merged = { ...projected } as CanvasProject & Record<string, unknown>;
const before = previous as CanvasProject & Record<string, unknown>;
const after = incoming as CanvasProject & Record<string, unknown>;
const editor = { ...previous, nodes, connections } as CanvasProject & Record<string, unknown>;
for (const key of new Set([...Object.keys(before), ...Object.keys(after), ...Object.keys(editor)])) {
if (["id", "revision", "updatedAt", "remoteContentHash", "viewport", "nodes", "connections"].includes(key)) continue;
const value = mergeValue(editor[key], before[key], after[key]);
if (value === undefined) delete merged[key];
else merged[key] = value;
}
return { ...merged, revision: incoming.revision, updatedAt: incoming.updatedAt, viewport: editor.viewport } as CanvasProject;
}
+13 -14
View File
@@ -27,11 +27,11 @@ export type AgentContextBreakdownItem = {
export type AgentContextUsageView = {
phase: AgentContextPhase;
/** Share of the input budget, 0–1 when the model window is known. */
/** Share of the model context window, 0–1 when the window is known. */
ratio?: number;
/** Where semantic compaction starts, as a share of the same input budget. */
/** Compaction threshold as a share of the model context window. */
compactRatio?: number;
/** Ring fill. 1 means "at or past the compaction line", not "window is full". */
/** Ring fill against the full context window. */
ring: number;
label: string;
detail: string;
@@ -74,8 +74,8 @@ export function finiteContextNumber(value: unknown): number | undefined {
/** Returns a ratio only when the backend has a trustworthy configured model budget. */
export function contextPressureRatio(reading: Record<string, unknown> | null): number | undefined {
if (!reading || reading.modelLimitConfigured !== true) return undefined;
const usable = finiteContextNumber(reading.usableInputTokens);
if (!usable || usable <= 0) return undefined;
const contextWindow = finiteContextNumber(reading.contextWindowTokens);
if (!contextWindow || contextWindow <= 0) return undefined;
const ratio = reading.tokenSource === "provider"
? finiteContextNumber(reading.projectedPressureRatio)
: finiteContextNumber(reading.pressureRatio);
@@ -128,15 +128,14 @@ function formatContextTokens(tokens: number | undefined): string {
/**
* One view of the compaction mechanism.
* The ring fills against compactAtTokens (85% of the input budget): full means
* the next model call is about to pause and compact, not that the window is 100% used.
* All visual scales use the model context window; the marker shows the shared 80% compaction line.
*/
export function presentAgentContextUsage(usage: AgentContextUsage): AgentContextUsageView {
const reading = usage.reading;
const inputTokens = contextInputTokens(reading);
const usableTokens = finiteContextNumber(reading?.usableInputTokens);
const compactAtTokens = finiteContextNumber(reading?.compactAtTokens);
const contextWindowTokens = finiteContextNumber(reading?.contextWindowTokens);
const usableTokens = contextWindowTokens;
const compactAtTokens = finiteContextNumber(reading?.compactAtTokens);
const ratio = contextPressureRatio(reading);
const compactRatio = usableTokens && compactAtTokens ? Math.min(1, compactAtTokens / usableTokens) : undefined;
const tokenSource = reading?.tokenSource === "provider" ? "provider" : reading ? "estimate" : undefined;
@@ -168,21 +167,21 @@ export function presentAgentContextUsage(usage: AgentContextUsage): AgentContext
}
if (!reading) return base;
if (usage.readingStale) {
return { ...base, phase: "stale", ring: ratio === undefined ? 0 : Math.min(1, ratio / (compactRatio || 0.85)), label: "刚压缩", detail: "上一份读数是压缩前的;下一次模型调用会给出压缩后的占用。" };
return { ...base, phase: "stale", ring: ratio === undefined ? 0 : Math.min(1, ratio), label: "刚压缩", detail: "上一份读数是压缩前的;下一次模型调用会给出压缩后的占用。" };
}
if (ratio === undefined || usableTokens === undefined || usableTokens <= 0) {
const measured = inputTokens === undefined ? "窗口未知" : `约 ${formatContextTokens(inputTokens)} Token`;
return { ...base, phase: "unknown", label: measured, detail: "这个模型没有配置可确认的上下文窗口,不能给出占用百分比;对话过长时仍会按条数和体积压缩。" };
}
const line = compactRatio && compactRatio > 0 ? compactRatio : 0.85;
const ring = Math.max(0, Math.min(1, ratio / line));
const line = compactRatio && compactRatio > 0 ? compactRatio : 0.8;
const ring = Math.max(0, Math.min(1, ratio));
const percent = Math.round(ratio * 100);
const source = estimate ? "本地估算" : "模型实测校准";
if (ratio >= line) {
return { ...base, phase: "compress", ring: 1, label: `${percent}%`, detail: `已到压缩线(输入预算的 ${Math.round(line * 100)}%)。下一次调用前会暂停,把历史收成检查点后再继续。当前 ${formatContextTokens(inputTokens)} / ${formatContextTokens(usableTokens)}(${source})。` };
return { ...base, phase: "compress", ring, label: `${percent}%`, detail: `已到压缩线(模型窗口的 ${Math.round(line * 100)}%)。下一次调用前会暂停,把历史收成检查点后再继续。当前 ${formatContextTokens(inputTokens)} / ${formatContextTokens(usableTokens)}(${source})。` };
}
if (ring >= 0.72) {
return { ...base, phase: "watch", ring, label: `${percent}%`, detail: `接近压缩。当前 ${formatContextTokens(inputTokens)} / ${formatContextTokens(usableTokens)}(${source}),到 ${formatContextTokens(compactAtTokens)} 时开始压缩。` };
}
return { ...base, phase: "ok", ring, label: `${percent}%`, detail: `当前 ${formatContextTokens(inputTokens)} / ${formatContextTokens(usableTokens)}(${source})。满格表示到达压缩线,不是窗口已经 100% 用完。` };
return { ...base, phase: "ok", ring, label: `${percent}%`, detail: `当前 ${formatContextTokens(inputTokens)} / ${formatContextTokens(usableTokens)}(${source})。标记线为自动压缩阈值。` };
}