Publish portable agent state market research

This commit is contained in:
wuyang6
2026-08-22 23:13:07 +08:00
parent 8872404966
commit 0eaf39a222
6 changed files with 668 additions and 0 deletions
@@ -0,0 +1,58 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="820" viewBox="0 0 1280 820" role="img" aria-labelledby="title desc">
<title id="title">可移植 Agent 状态市场机会图</title>
<desc id="desc">横轴是对协议网络效应的依赖,纵轴是近期付费意愿。兼容迁移和企业治理位于高付费区,第五个新格式与代币网络位于高网络效应、低近期付费区。</desc>
<defs>
<linearGradient id="bg2" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#f8fafc"/><stop offset="1" stop-color="#eef2ff"/></linearGradient>
<filter id="soft"><feDropShadow dx="0" dy="8" stdDeviation="9" flood-color="#475569" flood-opacity=".16"/></filter>
<marker id="arr2" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto"><path d="M0 0 L9 4.5 L0 9Z" fill="#475569"/></marker>
</defs>
<rect width="1280" height="820" rx="32" fill="url(#bg2)"/>
<text x="70" y="70" fill="#0f172a" font-size="32" font-weight="800" font-family="system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">先卖迁移与治理,再争协议网络效应</text>
<text x="70" y="104" fill="#64748b" font-size="18" font-family="system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">位置是基于公开定价与本次技术审计的判断,不是市场规模预测</text>
<g transform="translate(132 148)" font-family="system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">
<rect x="0" y="0" width="1010" height="560" rx="22" fill="#ffffff" stroke="#cbd5e1"/>
<rect x="0" y="0" width="505" height="280" fill="#ecfeff" opacity=".72"/>
<rect x="505" y="0" width="505" height="280" fill="#f5f3ff" opacity=".75"/>
<rect x="0" y="280" width="505" height="280" fill="#f8fafc"/>
<rect x="505" y="280" width="505" height="280" fill="#fff7ed" opacity=".72"/>
<line x1="505" y1="0" x2="505" y2="560" stroke="#cbd5e1" stroke-dasharray="8 8"/>
<line x1="0" y1="280" x2="1010" y2="280" stroke="#cbd5e1" stroke-dasharray="8 8"/>
<line x1="0" y1="560" x2="1038" y2="560" stroke="#475569" stroke-width="2" marker-end="url(#arr2)"/>
<line x1="0" y1="560" x2="0" y2="-26" stroke="#475569" stroke-width="2" marker-end="url(#arr2)"/>
<text x="405" y="612" fill="#334155" font-size="18" font-weight="700">对协议网络效应的依赖 →</text>
<text transform="translate(-72 385) rotate(-90)" fill="#334155" font-size="18" font-weight="700">近期付费意愿 →</text>
<text x="22" y="34" fill="#0891b2" font-size="15" font-weight="700">先做:价值可独立交付</text>
<text x="527" y="34" fill="#7c3aed" font-size="15" font-weight="700">再做:需要生态协同</text>
<g filter="url(#soft)">
<rect x="72" y="84" width="300" height="112" rx="20" fill="#ffffff" stroke="#06b6d4" stroke-width="2"/>
<circle cx="108" cy="120" r="16" fill="#06b6d4"/>
<text x="138" y="124" fill="#0f172a" font-size="22" font-weight="800">兼容迁移 + loss report</text>
<text x="96" y="158" fill="#475569" font-size="16">CLI、adapter、迁移评估、回滚</text>
<rect x="610" y="66" width="318" height="124" rx="20" fill="#ffffff" stroke="#8b5cf6" stroke-width="2"/>
<circle cx="648" cy="105" r="16" fill="#8b5cf6"/>
<text x="678" y="110" fill="#0f172a" font-size="22" font-weight="800">企业治理 gateway</text>
<text x="634" y="145" fill="#475569" font-size="16">授权、撤销、审计、合规策略</text>
<rect x="278" y="218" width="280" height="108" rx="20" fill="#ffffff" stroke="#14b8a6" stroke-width="2"/>
<circle cx="314" cy="254" r="15" fill="#14b8a6"/>
<text x="344" y="259" fill="#0f172a" font-size="21" font-weight="800">托管同步 / 恢复</text>
<text x="302" y="292" fill="#475569" font-size="16">按写入、存储、SLA 收费</text>
<rect x="642" y="328" width="272" height="106" rx="20" fill="#ffffff" stroke="#f59e0b" stroke-width="2"/>
<circle cx="678" cy="364" r="15" fill="#f59e0b"/>
<text x="708" y="369" fill="#0f172a" font-size="21" font-weight="800">第五个新格式</text>
<text x="666" y="402" fill="#475569" font-size="16">先有生态,后有收入</text>
<rect x="768" y="450" width="204" height="78" rx="20" fill="#ffffff" stroke="#fb7185" stroke-width="2"/>
<text x="798" y="483" fill="#be123c" font-size="20" font-weight="800">代币 / 状态链</text>
<text x="798" y="510" fill="#64748b" font-size="14">高监管与冷启动风险</text>
<rect x="90" y="380" width="264" height="92" rx="20" fill="#ffffff" stroke="#94a3b8"/>
<text x="122" y="416" fill="#334155" font-size="20" font-weight="800">单一 memory SDK</text>
<text x="122" y="446" fill="#64748b" font-size="15">红海;已有 Mem0/Letta 等</text>
</g>
</g>
<text x="70" y="775" fill="#64748b" font-size="15" font-family="system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">图 2|最值得下注的不是“拥有协议名字”,而是成为迁移、兼容与治理的默认实现。</text>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

@@ -0,0 +1,38 @@
{
"experiment": "portable-state-probe",
"date": "2026-08-22",
"runtime": "Node.js v26.5.0",
"tests": {
"passed": 10,
"failed": 0
},
"summary": {
"canonical_state_fields": 4,
"converged_state_hash": "d96f9b67696866926cb8a495cac7f23d3d468d2cc9af7de2fec8b07534e35688",
"tamper_rejections": 1,
"revocation_rejections": 1,
"explicit_loss_items": 1,
"omitted_secret_classes": 1
},
"agent_file_audit": {
"agents": 1,
"blocks": 9,
"tools": 9,
"messages": 3,
"contains_secret_values": false
},
"demonstrated": [
"cross-harness preference, memory and task import",
"signed event verification",
"delivery-order-independent convergence",
"post-revocation write rejection",
"secret omission",
"explicit semantic loss reporting"
],
"not_demonstrated": [
"behavioral equivalence across real model runtimes",
"large-scale sync performance",
"key recovery",
"multi-user policy composition"
]
}
@@ -0,0 +1,193 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
function canonical(value) {
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
if (value && typeof value === "object") {
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
}
return JSON.stringify(value);
}
function digest(value) {
return crypto.createHash("sha256").update(canonical(value)).digest("hex");
}
function device(name) {
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
const publicPem = publicKey.export({ type: "spki", format: "pem" });
return { name, did: `did:key:${digest(publicPem).slice(0, 32)}`, publicKey, privateKey };
}
function createEvent(signer, body) {
const id = digest(body);
const signature = crypto.sign(null, Buffer.from(canonical(body)), signer.privateKey).toString("base64");
return { id, body, signature };
}
function verifyEvent(event, keys) {
if (digest(event.body) !== event.id) return false;
const key = keys.get(event.body.writer);
return Boolean(key && crypto.verify(null, Buffer.from(canonical(event.body)), key, Buffer.from(event.signature, "base64")));
}
function eventOrder(a, b) {
return a.body.clock - b.body.clock || a.id.localeCompare(b.id);
}
function reduce(events, keys, ownerDid) {
const unique = [...new Map(events.map((event) => [event.id, event])).values()].sort(eventOrder);
const rejected = [];
const grants = new Map();
const revocations = new Map();
for (const event of unique) {
if (!verifyEvent(event, keys)) {
rejected.push({ id: event.id, reason: "bad-signature-or-id" });
continue;
}
const { kind, writer, payload, clock } = event.body;
if (kind === "grant.issue") {
if (writer !== ownerDid) rejected.push({ id: event.id, reason: "non-owner-grant" });
else grants.set(payload.grant_id, payload);
}
if (kind === "grant.revoke") {
if (writer !== ownerDid) rejected.push({ id: event.id, reason: "non-owner-revocation" });
else revocations.set(payload.grant_id, clock);
}
}
const registers = new Map();
for (const event of unique) {
if (!verifyEvent(event, keys) || event.body.kind !== "state.put") continue;
const { writer, clock, payload, grant_id: grantId } = event.body;
const grant = grants.get(grantId);
const revokedAt = revocations.get(grantId);
let reason = null;
if (!grant) reason = "missing-grant";
else if (grant.grantee !== writer) reason = "wrong-grantee";
else if (!grant.actions.includes("state.put")) reason = "action-not-allowed";
else if (clock > grant.expires_at) reason = "expired-grant";
else if (revokedAt !== undefined && clock >= revokedAt) reason = "revoked-grant";
if (reason) {
rejected.push({ id: event.id, reason });
continue;
}
const current = registers.get(payload.path);
if (!current || eventOrder(current.event, event) < 0) registers.set(payload.path, { value: payload.value, event });
}
const state = Object.fromEntries([...registers.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([path, item]) => [path, item.value]));
return { state, state_hash: digest(state), accepted_events: unique.length - rejected.length, rejected };
}
function exportHarnessA(internal, signer, ownerDid, grantId, startClock) {
const events = [];
let clock = startClock;
const put = (path, value) => events.push(createEvent(signer, {
version: "probe-0.1", owner: ownerDid, writer: signer.did, clock: clock++, kind: "state.put", grant_id: grantId, payload: { path, value }
}));
for (const [key, value] of Object.entries(internal.user.preferences)) put(`profile.preferences.${key}`, value);
for (const memory of internal.memories) put(`memory.${memory.id}`, { text: memory.text, provenance: memory.source });
put("task.current", internal.task);
return {
events,
loss: ["tool_rules: target-independent execution semantics undefined"],
omitted: ["private_material: never serialized"]
};
}
function importHarnessB(state) {
const profile = { preferences: {} };
const recollections = {};
let work = null;
for (const [path, value] of Object.entries(state)) {
if (path.startsWith("profile.preferences.")) profile.preferences[path.split(".").at(-1)] = value;
else if (path.startsWith("memory.")) recollections[path.split(".").at(-1)] = value;
else if (path === "task.current") work = value;
}
return { profile, recollections, work, vault: "local-only" };
}
function auditAgentFile(path) {
if (!path) return null;
const file = JSON.parse(fs.readFileSync(path, "utf8"));
const agents = file.agents ?? [];
return {
format: "Letta Agent File",
agents: agents.length,
blocks: (file.blocks ?? []).length,
tools: (file.tools ?? []).length,
messages: agents.reduce((sum, agent) => sum + (agent.messages ?? []).length, 0),
contains_secret_values: agents.some((agent) => Object.values(agent.secrets ?? {}).some((value) => value !== null)),
known_portability_gaps: ["archival passages not represented", "framework-specific blocks/tool rules require adapters", "schema migration is roadmap work"]
};
}
const owner = device("owner");
const harnessA = device("harness-a");
const harnessB = device("harness-b");
const keys = new Map([[owner.did, owner.publicKey], [harnessA.did, harnessA.publicKey], [harnessB.did, harnessB.publicKey]]);
const grantA = createEvent(owner, { version: "probe-0.1", owner: owner.did, writer: owner.did, clock: 1, kind: "grant.issue", payload: { grant_id: "grant-a", grantee: harnessA.did, actions: ["state.put"], expires_at: 100 } });
const grantB = createEvent(owner, { version: "probe-0.1", owner: owner.did, writer: owner.did, clock: 2, kind: "grant.issue", payload: { grant_id: "grant-b", grantee: harnessB.did, actions: ["state.put"], expires_at: 100 } });
const internalA = {
user: { preferences: { language: "zh-CN", answer_style: "direct" } },
memories: [{ id: "m1", text: "User works in Beijing", source: "user-stated" }],
task: { id: "research-1", status: "active", title: "portable state research" },
tool_rules: [{ tool: "shell", allow: "workspace-only" }],
private_material: { demo_credential: "redacted-local-only" }
};
const exportedA = exportHarnessA(internalA, harnessA, owner.did, "grant-a", 10);
const base = reduce([grantA, grantB, ...exportedA.events], keys, owner.did);
const importedB = importHarnessB(base.state);
const concurrentA = createEvent(harnessA, { version: "probe-0.1", owner: owner.did, writer: harnessA.did, clock: 20, kind: "state.put", grant_id: "grant-a", payload: { path: "profile.preferences.theme", value: "dark" } });
const concurrentB = createEvent(harnessB, { version: "probe-0.1", owner: owner.did, writer: harnessB.did, clock: 20, kind: "state.put", grant_id: "grant-b", payload: { path: "profile.preferences.theme", value: "light" } });
const mergedAB = reduce([grantA, grantB, ...exportedA.events, concurrentA, concurrentB], keys, owner.did);
const mergedBA = reduce([concurrentB, ...exportedA.events, grantB, concurrentA, grantA], keys, owner.did);
const tampered = structuredClone(exportedA.events[0]);
tampered.body.payload.value = "tampered";
const tamperResult = reduce([grantA, tampered], keys, owner.did);
const revokeB = createEvent(owner, { version: "probe-0.1", owner: owner.did, writer: owner.did, clock: 30, kind: "grant.revoke", payload: { grant_id: "grant-b" } });
const postRevoke = createEvent(harnessB, { version: "probe-0.1", owner: owner.did, writer: harnessB.did, clock: 31, kind: "state.put", grant_id: "grant-b", payload: { path: "profile.preferences.language", value: "en-US" } });
const revokeResult = reduce([grantA, grantB, ...exportedA.events, revokeB, postRevoke], keys, owner.did);
const tests = [];
const test = (name, fn) => { fn(); tests.push({ name, status: "pass" }); };
test("cross-harness preference import", () => assert.equal(importedB.profile.preferences.language, "zh-CN"));
test("cross-harness memory import", () => assert.equal(importedB.recollections.m1.text, "User works in Beijing"));
test("cross-harness task import", () => assert.equal(importedB.work.status, "active"));
test("merge converges independent of delivery order", () => assert.equal(mergedAB.state_hash, mergedBA.state_hash));
test("concurrent conflict resolves deterministically", () => assert.equal(mergedAB.state["profile.preferences.theme"], mergedBA.state["profile.preferences.theme"]));
test("tampering is rejected", () => assert.equal(tamperResult.rejected[0].reason, "bad-signature-or-id"));
test("revoked grant blocks later write", () => assert.ok(revokeResult.rejected.some((item) => item.reason === "revoked-grant")));
test("pre-revocation state remains valid", () => assert.equal(revokeResult.state["profile.preferences.language"], "zh-CN"));
test("secrets are omitted", () => assert.ok(!canonical(exportedA).includes("redacted-local-only")));
test("semantic loss is explicit", () => assert.ok(exportedA.loss[0].includes("tool_rules")));
const result = {
experiment: "portable-state-probe",
runtime: process.version,
tests,
summary: {
passed: tests.length,
failed: 0,
canonical_state_fields: Object.keys(base.state).length,
converged_state_hash: mergedAB.state_hash,
tamper_rejections: tamperResult.rejected.length,
revocation_rejections: revokeResult.rejected.filter((item) => item.reason === "revoked-grant").length,
explicit_loss_items: exportedA.loss.length,
omitted_secret_classes: exportedA.omitted.length
},
interpretation: {
demonstrated: ["signed event portability", "deterministic merge", "capability revocation", "secret omission", "loss reporting"],
not_demonstrated: ["behavioral equivalence across real model runtimes", "large-scale sync performance", "key recovery", "multi-user policy composition"]
},
agent_file_audit: auditAgentFile(process.argv[2])
};
console.log(JSON.stringify(result, null, 2));
@@ -0,0 +1,93 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="900" viewBox="0 0 1440 900" role="img" aria-labelledby="title desc">
<title id="title">可移植 Agent 状态层与现有协议的关系</title>
<desc id="desc">现有 MCP、A2A 和 AG-UI 位于交互接口侧;Agent File、Agent Spec 等位于适配层;真正缺少的是包含语义、损失声明、授权、签名事件和同步的用户状态公共层。</desc>
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#071426"/>
<stop offset="1" stop-color="#11102b"/>
</linearGradient>
<linearGradient id="core" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#0ea5e9"/>
<stop offset="0.55" stop-color="#7c3aed"/>
<stop offset="1" stop-color="#f97372"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="12" stdDeviation="14" flood-color="#020617" flood-opacity=".42"/>
</filter>
<marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto">
<path d="M0,0 L10,5 L0,10 Z" fill="#64748b"/>
</marker>
</defs>
<rect width="1440" height="900" rx="36" fill="url(#bg)"/>
<text x="80" y="76" fill="#f8fafc" font-size="34" font-weight="700" font-family="system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">跨 harness 公共层不是另一个 harness</text>
<text x="80" y="112" fill="#94a3b8" font-size="18" font-family="system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">状态归用户;运行时只通过 adapter 读取、写入和声明损失</text>
<g filter="url(#shadow)" font-family="system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">
<rect x="96" y="158" width="930" height="118" rx="24" fill="#111d33" stroke="#334155"/>
<text x="126" y="193" fill="#7dd3fc" font-size="16" font-weight="700">HARNESSES / 运行时</text>
<g fill="#e2e8f0" font-size="22" font-weight="600">
<rect x="126" y="214" width="190" height="42" rx="12" fill="#162941"/><text x="158" y="242">DeepSeek Harness</text>
<rect x="334" y="214" width="160" height="42" rx="12" fill="#162941"/><text x="366" y="242">LangGraph</text>
<rect x="512" y="214" width="140" height="42" rx="12" fill="#162941"/><text x="554" y="242">Letta</text>
<rect x="670" y="214" width="180" height="42" rx="12" fill="#162941"/><text x="706" y="242">自研 runtime</text>
<rect x="868" y="214" width="128" height="42" rx="12" fill="#162941"/><text x="900" y="242">未来实现</text>
</g>
<rect x="96" y="314" width="930" height="118" rx="24" fill="#121e35" stroke="#38bdf8" stroke-opacity=".52"/>
<text x="126" y="349" fill="#7dd3fc" font-size="16" font-weight="700">ADAPTER / 兼容与能力协商</text>
<g fill="#e2e8f0" font-size="19" font-weight="600">
<rect x="126" y="370" width="160" height="42" rx="12" fill="#17304b"/><text x="158" y="398">Agent File</text>
<rect x="304" y="370" width="176" height="42" rx="12" fill="#17304b"/><text x="336" y="398">Agent Spec</text>
<rect x="498" y="370" width="184" height="42" rx="12" fill="#17304b"/><text x="530" y="398">Agent Format</text>
<rect x="700" y="370" width="166" height="42" rx="12" fill="#17304b"/><text x="732" y="398">memorywire</text>
<rect x="884" y="370" width="112" height="42" rx="12" fill="#17304b"/><text x="912" y="398">转换器</text>
</g>
<rect x="96" y="470" width="930" height="170" rx="28" fill="#11182e" stroke="url(#core)" stroke-width="3"/>
<rect x="96" y="470" width="12" height="170" rx="6" fill="url(#core)"/>
<text x="130" y="508" fill="#f8fafc" font-size="18" font-weight="800">用户拥有的可移植状态语义</text>
<g fill="#e2e8f0" font-size="20" font-weight="600">
<rect x="130" y="532" width="150" height="42" rx="13" fill="#1e2a4a"/><text x="162" y="560">偏好 / 身份</text>
<rect x="296" y="532" width="146" height="42" rx="13" fill="#1e2a4a"/><text x="328" y="560">长期 memory</text>
<rect x="458" y="532" width="146" height="42" rx="13" fill="#1e2a4a"/><text x="490" y="560">任务 checkpoint</text>
<rect x="620" y="532" width="160" height="42" rx="13" fill="#1e2a4a"/><text x="652" y="560">授权 / 撤销</text>
<rect x="796" y="532" width="198" height="42" rx="13" fill="#1e2a4a"/><text x="828" y="560">来源 / 审计</text>
<rect x="130" y="586" width="240" height="34" rx="11" fill="#3a2043"/><text x="158" y="610" fill="#fda4af" font-size="17">必须输出 loss manifest</text>
<text x="392" y="610" fill="#94a3b8" font-size="17" font-weight="400">语法可迁移,不承诺不同模型与工具执行完全等价</text>
</g>
<rect x="96" y="678" width="930" height="126" rx="24" fill="#101b2f" stroke="#334155"/>
<text x="126" y="713" fill="#a78bfa" font-size="16" font-weight="700">CONTROL + SYNC / 可复用的成熟部件</text>
<g fill="#e2e8f0" font-size="18" font-weight="600">
<rect x="126" y="734" width="150" height="46" rx="12" fill="#261f45"/><text x="158" y="763">DID / device key</text>
<rect x="294" y="734" width="160" height="46" rx="12" fill="#261f45"/><text x="326" y="763">UCAN capability</text>
<rect x="472" y="734" width="164" height="46" rx="12" fill="#261f45"/><text x="504" y="763">签名事件日志</text>
<rect x="654" y="734" width="148" height="46" rx="12" fill="#261f45"/><text x="686" y="763">Automerge</text>
<rect x="820" y="734" width="176" height="46" rx="12" fill="#261f45"/><text x="852" y="763">Solid / 对象存储</text>
</g>
</g>
<g font-family="system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">
<rect x="1080" y="158" width="264" height="646" rx="26" fill="#0e1a2d" stroke="#334155"/>
<text x="1112" y="198" fill="#f8fafc" font-size="20" font-weight="700">相邻协议</text>
<text x="1112" y="226" fill="#94a3b8" font-size="15">需要组合,但不会替代状态层</text>
<rect x="1112" y="266" width="200" height="104" rx="18" fill="#162941" stroke="#38bdf8" stroke-opacity=".45"/>
<text x="1138" y="304" fill="#7dd3fc" font-size="24" font-weight="800">MCP</text>
<text x="1138" y="334" fill="#cbd5e1" font-size="16">工具、资源、OAuth</text>
<rect x="1112" y="394" width="200" height="104" rx="18" fill="#211d3c" stroke="#8b5cf6" stroke-opacity=".52"/>
<text x="1138" y="432" fill="#c4b5fd" font-size="24" font-weight="800">A2A</text>
<text x="1138" y="462" fill="#cbd5e1" font-size="16">Agent 消息、任务、产物</text>
<rect x="1112" y="522" width="200" height="104" rx="18" fill="#302033" stroke="#fb7185" stroke-opacity=".5"/>
<text x="1138" y="560" fill="#fda4af" font-size="24" font-weight="800">AG-UI</text>
<text x="1138" y="590" fill="#cbd5e1" font-size="16">前端事件与共享视图</text>
<rect x="1112" y="650" width="200" height="104" rx="18" fill="#1a2837" stroke="#64748b"/>
<text x="1138" y="688" fill="#cbd5e1" font-size="22" font-weight="800">模型 API</text>
<text x="1138" y="718" fill="#94a3b8" font-size="16">推理兼容,不搬状态</text>
</g>
<g stroke="#64748b" stroke-width="2" fill="none" marker-end="url(#arrow)" opacity=".8">
<path d="M1028 328 C1060 328 1060 318 1098 318"/>
<path d="M1028 372 C1060 372 1060 446 1098 446"/>
<path d="M1028 526 C1060 526 1060 574 1098 574"/>
</g>
<text x="80" y="855" fill="#64748b" font-size="15" font-family="system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">图 1|建议架构:已有协议负责调用和通信;新工作的核心是可移植语义、损失声明与一致性测试。</text>
</svg>

After

Width:  |  Height:  |  Size: 7.7 KiB