跳到主要内容

会话状态与可观测性:Chat、Session 与 Dev UI

30 秒导读: 前几章讲的 generate(03)和工具循环 (04)都是单次调用——喊一声、答一句、忘干净。本章补上 把 demo 变成产品的两条能力:① 让多轮对话记得住(Session + SessionStore 用快照持久化 消息和自定义状态);② 让开发者看得见(每个 Action 自动生成 OpenTelemetry span,汇成 trace, 再由 ReflectionServer 喂给本地 Dev UI 可视化调试)。


1. 这是什么(零基础也能懂)

一个问题起头: 你用 03 章ai.generate() 写了个聊天机器人。 用户问"我叫小明",模型回"你好小明";用户再问"我叫什么",模型答"我不知道"。因为每次 generate 都是全新的,上一句根本没带过来。

  • 一句话定义: 「会话(Session)」= 一个把多轮消息自定义状态跨多次 generate 攒起来、并能存盘/读盘的容器;「可观测性」= 让你在浏览器里逐帧回放一次调用到底 经过了哪些 prompt、工具、模型。

  • 解决什么问题 / 给谁用:

    • 做多轮 Agent 的人:不想每轮手工拼 messages 数组、不想自己写数据库存对话。
    • 调 Agent 卡壳的人:模型为什么突然罢工?工具传了什么参数?肉眼看 log 太痛苦, 要一张可点开的调用树
  • 它能做什么(功能):

    • 跨多次生成累积消息、维护一坨类型安全的自定义状态(custom,如"当前订单")。
    • 把每一轮的状态存成快照(snapshot),进程重启后还能sessionId 接着聊
    • 每个 Action(flow / prompt / tool / model 调用)自动产出一个 span,零埋点。
    • 本地跑 genkit start,浏览器打开 Dev UI,列出所有 Action、点一下就能跑、看 trace。
  • 用起来什么样: 一个最小的有状态 Agent(源码里 defineAgent 的 doc 示例, js/genkit/src/genkit-beta.ts:154-162):

    // 示意,非源码(改编自 defineAgent 的文档示例)
    const chatAgent = ai.defineAgent({
    name: 'chatAgent',
    model: 'googleai/gemini-2.5-flash',
    system: 'Talk like a pirate.',
    store: new FileSessionStore('./.snapshots'), // ← 有 store 就自动存盘
    });

    const chat = chatAgent.chat(); // 开一轮新会话
    await chat.send('我叫小明'); // 第 1 轮:记住了
    await chat.send('我叫什么?'); // 第 2 轮:答"小明"——历史被带上了
    console.log(chat.sessionId); // 这个 id 下次可以 loadChat 接着聊
  • 一句话直觉/类比:Session 当"这次对话的工作内存",把 SessionStore 当"磁盘"; 每轮结束拍一张快照存进磁盘,下次开机把最新快照读回内存,对话就无缝续上。可观测性那半边, 就像给整个程序装了行车记录仪:每个动作自动录一段,事后能倒带看。

本节不碰底层。记住两个词:Session(内存里的对话状态)trace(一次调用的录像)


2. 顶层全景(它大概怎么转)

这套东西分两个几乎正交的子系统,共享同一个底座——01 章Action + 注册表。先看它们怎么拼:

┌─────────────────────── 你的进程 ───────────────────────┐
│ │
用户输入 ──▶ │ AgentChat.send() │
│ │ │
│ ▼ │
│ ┌────────────┐ 每轮读→改→存 ┌──────────────┐ │
│ │ SessionRunner│◀────────────▶│ SessionStore │─┐ │
│ │ + Session │ 快照 │ (内存/文件) │ │存盘 │
│ └─────┬──────┘ └──────────────┘ │ │
│ │ runWithSession 绑定上下文 ▼ │
│ ▼ .snapshots/ │
│ ai.generate() ← getCurrentSession() 读历史 │
│ │ │
│ ┌─────┴───────────── 每个 Action 自动开 span ────────┐ │
│ │ runInNewSpan → OpenTelemetry startActiveSpan │ │
│ └─────┬─────────────────────────────────────────────┘ │
│ │ 导出 span │
└────────┼───────────────────────────────────────────────┘

┌──────────────┐ /api/actions /runAction /notify ┌────────┐
│ReflectionServer│◀────────────────────────────────▶│ genkit │
│ (:3100) │ .genkit/runtimes/*.json │ CLI │
└──────┬───────┘ │ +Dev UI│
│ trace 存进 telemetry-server └────────┘

/api/traces ◀── Dev UI 拉取并渲染调用树

怎么读这张图: 左半边(上)是状态子系统——对话记忆如何存盘读盘;下半边是 可观测子系统——每个 Action 自动录 span;右边是开发者面板——CLI/Dev UI 通过 ReflectionServer 的 HTTP 接口把两者可视化。

部件一句话职责:

部件干什么在哪个文件
Session内存里持有本次对话的 messages / custom / artifactsjs/ai/src/session.ts:152
SessionStore快照的持久化接口(存/读)js/ai/src/session.ts:99
InMemory / FileSessionStore两个内置实现:内存 Map / 磁盘 JSON 文件js/ai/src/session-stores.ts:122,290
SessionRunner逐轮跑 handler、每轮结束存快照的执行器js/ai/src/agent.ts:279
runWithSession / getCurrentSession把 Session 绑进异步上下文,供 generate 取历史js/ai/src/session.ts:279,290
runInNewSpan把一段执行包进一个 OpenTelemetry spanjs/core/src/tracing/instrumentation.ts:81
ReflectionServer开发期 HTTP 服务,暴露 /api/* 给 CLI/Dev UIjs/core/src/reflection.ts:73
RuntimeManager(工具侧)发现运行时、调 /api/*、拉 tracegenkit-tools/common/src/manager/manager.ts

主线走一遍(高层):

  1. AgentChat.send('...') 发起一轮 → SessionRunner.run() 把输入消息 append 进 Session
  2. Runner 用 runWithSession 把这个 Session 绑进异步上下文,再调你的 handler(内部一般是 generate)。
  3. generate 通过 getCurrentSession() 拿到历史消息,拼进请求发给模型。
  4. 这一整套调用里,每个 Action 都被 runInNewSpan 包住,自动生成 span,层层嵌套成一棵 trace。
  5. 一轮结束,Runner 把 Session 的当前状态拍成快照写进 SessionStore(maybeSnapshot)。
  6. 与此同时,ReflectionServer 把 span 导出到 telemetry-server;Dev UI 通过 /api/traces 拉回来渲染。

3. 核心原理(逐个机制,由浅入深)

3.1 Session:一次对话的"工作内存"

  • 它要解决的小问题: generate 是无状态的,总得有一个东西把这一轮产生的消息、 以及业务自定义的状态(比如"购物车里有啥")攒在一起,并且别让 handler 反手改坏了调用者的对象

  • 思路/直觉: Session 就是一个状态容器 + 版本号。它对外只给深拷贝(读), 改动走明确的方法(addMessages / updateCustom / addArtifacts),每改一次 version++。 版本号是后面"要不要存快照"的判据。

  • 原理演示:

    // 示意,非源码:Session 的心智模型
    class Session {
    private state; // { sessionId, messages, custom, artifacts }
    private version = 0;
    getMessages() { return structuredClone(this.state.messages); } // 只给拷贝
    addMessages(m) { this.state.messages.push(...m); this.version++; }
    updateCustom(fn) { this.state.custom = fn(this.state.custom); this.version++; }
    }
  • 真实实现: Session 类在 js/ai/src/session.ts:152。注意构造时的防御—— structuredClone(initialState)(session.ts:164),注释点破为什么:"Clone so we never alias (or mutate) the caller's object"。读取方法 getState/getMessages 一律 structuredClone(session.ts:173,183),写入方法 addMessages(:190)、 updateCustom(:213)、addArtifacts(:230)都 version++

  • 关键细节/坑:

    • artifacts 按 name 去重:addArtifacts(session.ts:230-258)对同名 artifact 做替换而非追加,并分别 emit artifactAdded / artifactUpdated 事件——这让流式 UI 能区分"新产物"和"产物被覆盖"。
    • 状态的 schema 定义在别处:SessionState(js/ai/src/agent-types.ts:151)—— 只有 sessionId? / messages? / custom? / artifacts? 四个字段,custom 是泛型 S, 调用者可自带 Zod schema 做类型约束。

3.2 runWithSession / getCurrentSession:让 generate "隐式"拿到历史

  • 它要解决的小问题: handler 里写的是 ai.generate({ prompt }),并没把 Session 当参数传进去。 那 generate 怎么知道要把历史消息拼上?

  • 思路/直觉: 用 Node 的 AsyncLocalStorage(异步上下文,一种"隐形的线程局部变量")。 Runner 在调 handler 前,把 Session 塞进一个键为 'ai.session' 的异步上下文;handler 内部 任意深处调 getCurrentSession() 都能取回来——不用一路手动透传。

  • 图示(上下文如何传递):

    runWithSession(session, () => { ┌─ 异步上下文: 'ai.session' = session ─┐
    yourHandler(...) │ │
    └─ ai.generate(...) │ getCurrentSession() ──▶ 拿到 session│
    └─ prompt 渲染 │ → 把 session.messages 拼进请求 │
    }) └─────────────────────────────────────┘
  • 真实实现:

    • runWithSession(js/ai/src/session.ts:279)一行:getAsyncContext().run('ai.session', session, fn)
    • getCurrentSession(js/ai/src/session.ts:290):getAsyncContext().getStore('ai.session')
    • Session.run(session.ts:263)是同一件事的实例方法版。
    • 谁在读它?比如顶层门面 ai.currentSession()(js/ai/src/genkit-ai.ts:353), 不在会话里就抛 FAILED_PRECONDITION;prompt 渲染也会 getCurrentSession (js/ai/src/prompt.ts:277)以注入历史。
  • 关键细节: 这套异步上下文机制正是 01 章 Action 上下文的同款 底座(js/core/src/async-context.ts),会话状态和 span 上下文都靠它跨 await 传递。

3.3 SessionStore 与快照:把内存状态落到磁盘

  • 它要解决的小问题: 进程重启,内存里的 Session 就没了。得把每轮状态存下来, 且下次只给个 sessionId 就能读回最新那一轮

  • 思路/直觉: 不是"一个会话一条记录、原地覆盖",而是追加式快照链——每轮存一张 新快照,快照用 parentId 指向上一张,形成一条时间线。读的时候有两种寻址:

    • snapshotId → 精确读那一张;
    • sessionId → 找这条链的叶子(leaf,没有别的快照以它为 parent 的那张)= 最新一轮。
  • 图示(快照链与"取叶子"):

    第1轮 第2轮 第3轮(最新=leaf)
    ┌──────┐ ┌──────┐ ┌──────┐
    │ snapA│◀───│ snapB│◀───│ snapC│ getSnapshot({sessionId}) → snapC
    └──────┘ parentId parentId└──────┘ getSnapshot({snapshotId:'B'}) → snapB
  • 接口长这样(js/ai/src/session.ts:99-147):

    方法作用
    getSnapshot({snapshotId | sessionId})读一张(精确 / 或该会话最新叶子)
    saveSnapshot(id, mutator, opts)读→改→写原子存盘;mutator 拿到旧快照返回新快照
    onSnapshotStateChange?(...)订阅某快照变化(给后台/中断路径用)

    为什么 saveSnapshot 不是简单 put(value) 而是传一个 mutator 函数 (SnapshotMutator,session.ts:92)?因为存盘要原子地先看一眼当前状态—— 典型场景:一个后台轮次正常写 completed 时,可能有人并发把它 aborted 了,mutator 能"看到 current 是 aborted 就返回 null 跳过写",避免 completed 覆盖 aborted。

  • 真实实现——两个内置 store:

    • InMemorySessionStore(js/ai/src/session-stores.ts:122):一个 Map<snapshotId, snapshot>sessionId 查询就遍历所有快照挑叶子(selectLeafSnapshot,session-stores.ts:76)。
    • FileSessionStore(js/ai/src/session-stores.ts:290):每张快照一个 JSON 文件 dirPath/<prefix>/<snapshotId>.json。三个值得学的工程细节见 §5。
  • 关键细节/坑:

    • 分叉(branching):regenerate 会造出多个叶子。默认取"最近创建的那个" (selectLeafSnapshot 里按 createdAt 取最大,session-stores.ts:114-116);设 rejectBranchingSessions: true 则直接抛 FAILED_PRECONDITION,逼你按 snapshotId 恢复。
    • parentId 只是给 UI/调试看的血缘,不参与"解析最新快照"(见 SessionSnapshot.parentId 的注释,agent-types.ts:376-378)——叶子判定靠"谁没被别人当 parent",不是靠链表遍历。

3.4 SessionRunner:把上面三件事串成一轮

  • 它要解决的小问题: 谁来"append 输入 → 绑上下文 → 跑 handler → 存快照",把 §3.1–3.3 编排起来?

  • 思路/直觉: SessionRunner逐轮执行器。它从一个输入通道 inputChfor await 取轮次,每轮:①把用户消息塞进 Session;②预留(reserve)本轮 snapshotId; ③跑 handler;④maybeSnapshot('completed') 存盘。

  • 真实实现: SessionRunner.run(js/ai/src/agent.ts:430)。核心节奏:

    // 真实源码骨架,见 js/ai/src/agent.ts:433-495(已省略)
    for await (const input of this.inputCh) {
    if (input.message) this.session.addMessages([input.message]); // ①
    if (this.store && !this.newSnapshotId)
    this.newSnapshotId = reserveSnapshotId(); // ②预留 id
    await run(`runTurn-${this.turnIndex + 1}`, input, async () => {
    await fn(input, turnContext); // ③跑 handler
    const snapshotId = await this.maybeSnapshot('completed', ...); // ④存盘
    });
    }
  • 为什么"预留 snapshotId"很妙: id 在跑之前就定了(reserveSnapshotId,session.ts:331), 于是 handler 能拿这个 id 去命名外部资源——注释举的例子是"用快照 id 命名一个 git 分支/worktree" (agent.ts:152-156),轮末快照就存在这个 id 下,将来回滚到该快照能连外部状态一起还原。

  • 存盘那一步(maybeSnapshot,agent.ts:538)的门道:

    • 只在状态真的变了才写:比对 session.getVersion() 和上次快照的版本(agent.ts:550-553), 没变且无显式 status 就跳过——省掉无谓写盘。
    • 组装快照时盖上 sessionIdparentId: this.lastSnapshot?.snapshotId(agent.ts:563,567), 默认 status: 'completed'(可续)。
    • 落盘走 abortAwareMutator(agent.ts:224)——就是 §3.3 说的"被并发 abort 就跳过写"。

3.5 每个 Action 自动生成 span → 汇成 trace

  • 它要解决的小问题: 一次 chat.send 里嵌套了 flow→prompt→model→tool→model… 怎么零埋点地把这棵调用树录下来,还能标出谁失败了?

  • 思路/直觉: 01 章说"一切皆 Action";这里的关键补充是 Action 的执行入口统一被 runInNewSpan 包住。span 天然嵌套(父 span 里开子 span), 一棵嵌套 span = 一条 trace。Genkit 只是在标准 OpenTelemetry span 上盖一层 genkit:* 属性(类型、路径、输入输出)。

  • 图示(span 如何嵌套成 trace):

    trace (traceId = 一次 chat.send)
    └─ span: chatAgent genkit:type=action, subtype=agent
    └─ span: runTurn-1
    └─ span: generate genkit:type=action, subtype=model
    ├─ span: prompt 渲染
    └─ span: weatherTool genkit:type=action, subtype=tool ← 失败会被标 isFailureSource
  • 真实实现——两处配合:

    • Action 侧:js/core/src/action.ts:528 把整个 action 执行体交给 runInNewSpan, 并打标签 [SPAN_TYPE_ATTR]: 'action' + genkit:metadata:subtype(action.ts:533-536)。 SPAN_TYPE_ATTR 常量 = 'genkit:type'(js/core/src/tracing/instrumentation.ts:35)。
    • span 侧:runInNewSpan(instrumentation.ts:81)内部调 OpenTelemetry 的 tracer.startActiveSpan(instrumentation.ts:110),把 Genkit 的 SpanMetadata 通过 metadataToAttributes 摊成 genkit:name / genkit:input / genkit:path 等属性 (instrumentation.ts:221-243),结束时 otSpan.end()
  • 关键细节/坑:

    • 失败溯源:第一个抛错的 span 被标 isFailureSource = true,并给异常打 ignoreFailedSpan 标记,防止上层 catch-rethrow 的父 span 也认领"我是错误源" (instrumentation.ts:164-172)。Dev UI 就是靠这个把"真正炸的那一格"高亮出来。
    • 根 span 检测:没有父 step 时把自己标成 isRoot(instrumentation.ts:99-100), 这决定了新 trace 的边界;可用 disableOTelRootSpanDetection 关掉(:368)。
    • 轮次关联快照:成功一轮后,Runner 把 agent:snapshotId 作为自定义属性打到 turn span 上 (agent.ts:485-486,setCustomMetadataAttribute),于是 trace 能反查到它对应哪张快照。

3.6 ReflectionServer:把这一切喂给 CLI 与 Dev UI

  • 它要解决的小问题: 上面产生的 Action 列表和 trace 都在你的进程里。Dev UI 是个独立的 浏览器界面,它怎么"看进"你的进程?

  • 思路/直觉: 开发期你的进程里跑一个小 Express 服务(ReflectionServer,默认 :3100), 暴露一组 /api/* 反射接口;同时把自己的坐标写进一个运行时文件 .genkit/runtimes/<id>.jsongenkit CLI 监视这个目录,发现新运行时就连上去, Dev UI 便能列 Action、跑 Action、拉 trace。

  • 图示(三方握手):

    你的进程 文件系统 genkit CLI / Dev UI
    ReflectionServer.start()
    │ writeRuntimeFile() ──────▶ .genkit/runtimes/*.json ──▶ chokidar 监视到新文件
    │ │
    │◀──────────── GET /api/actions (列出所有 Action) ────────┤
    │◀──────────── POST /api/runAction?stream=true (跑一个) ───┤
    │──── POST /api/notify(告知 telemetry server 地址)────────▶│
    │ │
    span ──▶ telemetry-server ◀──── GET /api/traces(拉调用树)────┘
  • 真实实现——服务端路由(js/core/src/reflection.ts):

    路由干什么行号
    GET /api/actions列出注册表里所有可解析 Action + 其 JSON schemareflection.ts:207
    POST /api/runActionkey 跑一个 Action,支持 ?stream=true 流式reflection.ts:240
    POST /api/notifyCLI 告知 telemetry server URL,并校验 API 版本reflection.ts:395
    POST /api/cancelAction按 traceId 中止在跑的 Actionreflection.ts:370
    GET /api/__health健康检查reflection.ts:159

    runAction 里有个巧思:通过 onTraceStart 回调提前X-Genkit-Trace-Id 写进响应头 (reflection.ts:255-280),这样 Dev UI 拿到流式响应的第一时间就知道该去查哪条 trace。

  • 真实实现——工具侧(genkit-tools/common/src/manager/manager.ts):

    • RuntimeManagerchokidar 监视 .genkit/runtimes/(setupRuntimesWatcher, manager.ts:726;由构造流程在 manager.ts:343 调起),发现运行时后 GET /api/actions(manager.ts:450)、跑动作走 /api/runAction (manager.ts:515,596)、POST /api/notify(manager.ts:714)。
    • trace 不走 ReflectionServer,而是走独立的 telemetry-server:GET /api/traces/... (manager.ts:130,178,206)。telemetry-server 的落盘实现见 genkit-tools/telemetry-server/src/file-trace-store.ts
  • 关键细节: 客户端/服务端靠 GENKIT_REFLECTION_API_SPEC_VERSION(当前 = 1, js/core/src/index.ts:28)对版本;/api/notify 里若发现 CLI 比运行时旧/新会各自 打 warning(reflection.ts:405-420)。接口契约本身固化在 genkit-tools/reflectionApi.yaml 这份 OpenAPI 里,是 runtime 与 tools 之间的单一事实源


4. 深入实现:一次有状态、可观测的 chat.send 全链路

把前面拆开的机制端到端串一遍(server-managed,即配了 store):

  1. 发起 chat.send('我叫什么?')AgentChatImpl.send(js/ai/src/agent-core.ts:607 起)。 它维护客户端侧聚合(messages/artifacts/clientState),用 buildInit() (agent-core.ts:658)决定本轮 init:有 snapshotId{snapshotId}(续接), 否则带 {state}(客户端托管)。

  2. 进 Agent Action → 内部起(或复用)一个 SessionRunner,把 handler 交给它 (agent.ts:1087-1125 附近)。若带的是 snapshotId/sessionId,先从 store getSnapshot 把历史读回,new Session(snapshot.state) 重建内存状态(agent.ts:817,860)。

  3. 绑上下文跑 handlerrunWithSession(registry, session, () => handler()) (agent.ts:1229)。handler 内部的 ai.generate 通过 getCurrentSession 取到历史消息, 拼进模型请求(03 章的生成+工具循环在此发生)。

  4. 全程录 span → 第 2、3 步的每个 Action 调用都被 runInNewSpan 包住, 自动嵌套成一棵 trace(§3.5)。

  5. 轮末存盘maybeSnapshot('completed', …)(agent.ts:538)把 Session 当前状态 拍成新快照,parentId 指向上一张,写进 SessionStore;lastGoodSnapshotId 更新 (agent.ts:480),供失败时回退。

  6. 回传AgentResponse 带回 snapshotId/sessionId/state(agent-core.ts:160-179); 下一次 send 就凭这个 snapshotId 无缝续接。

  7. 可视化(旁路) → span 导出到 telemetry-server;Dev UI 通过 /api/traces 拉回, 按 genkit:path 渲染成可点开的调用树,失败格用 isFailureSource 高亮。

客户端托管(无 store)对照: 不配 store 时没有快照,状态通过每轮的 AgentInit.state 进、AgentOutput.state 出,由调用方自己存(AgentStateManagement = 'client', agent-types.ts:460)。可观测那半边完全一样——span 不依赖 store。


5. 巧妙之处(可借鉴的技术)

  1. 快照默认就存,且"没变就不存"。 配了 store 后每轮自动快照(不再是 opt-in, agent.ts:531-536 注释),但 maybeSnapshot 用 Session 的 version 做脏检查 (agent.ts:550-553)——状态没动就跳过写盘。既省心又省 IO。

  2. saveSnapshot 用 mutator 而非直接 put,把并发原子性交给 store。 传函数 (SnapshotMutator,session.ts:92)让"读当前→决定写什么"发生在 store 的临界区里, 这是 abort 竞态(completed 覆盖 aborted)唯一干净的解法。

  3. FileSessionStore 的三层稳健工程(session-stores.ts),值得直接抄:

    • 原子写:先写 *.tmprename 覆盖(atomicWrite,:698)——rename 在 POSIX/Windows 都原子,读者绝不会看到写一半的文件。
    • 指针文件加速 sessionId 查询:每会话一个 .pointers/<sessionId>.json 记录当前叶子(:255-272),把"扫全目录挑叶子"降成"一次指针读+一次快照读"; 指针缺失/损坏就自愈回退到扫描并重写指针(getLatestSnapshotForSession,:553)。
    • 路径逃逸防御:id 可能直接来自网络请求,assertSafeId(:230)挡住 ../ 之类, 再叠一层"解析后必须仍在前缀目录内"的兜底(:372-380)。
  4. 失败溯源不被父 span 抢功。 用异常上的 ignoreFailedSpan 标记,让只有第一个 抛错的 span 认领 isFailureSource(instrumentation.ts:164-172)——Dev UI 因此能精确 指到"真正炸的那一格",而不是最外层。

  5. 提前吐 traceId。 /api/runAction 在动作刚起 span、还没出结果时就把 traceId 写进响应头 (reflection.ts:255-280),让 Dev UI 边流式看输出边准备好那条 trace 的链接。


6. 边界与局限(诚实)

  • 快照 API 仍是 beta。 会话相关能力挂在 GenkitBeta 上(genkit-beta.ts:103),文件顶部 明写 "these APIs are considered unstable and subject to frequent breaking changes" (genkit-beta.ts:85)。命名也在演进——面向用户的对话对象叫 AgentChat (agent-core.ts:95),不是老文档里的 Chat

  • 指针可能"落后于"真叶子。 FileSessionStore 的快查明确标注了已知局限:若 writePointer 失败或两次保存竞态,指针可能短暂指向一个"有效但更旧"的同会话快照,直到下次保存推进指针 (session-stores.ts:540-551)。要强一致就按 snapshotId 恢复,或开 rejectBranchingSessions(总是扫描)。

  • ReflectionServer 只面向开发期。 类注释直书 "for use in development environments" (reflection.ts:69);沙箱运行时会跳过启动(reflection.ts:133)。生产可观测性走 telemetry 插件:core 的 checkFirebaseMonitoringAutoInit(js/core/src/tracing.ts:50-71) 在 ENABLE_FIREBASE_MONITORING=true 时按需动态加载并调用 @genkit-ai/firebaseenableFirebaseTelemetry(js/plugins/firebase/src/index.ts:40),而不是这个反射服务。

  • 可观测依赖 OTel 初始化。 runInNewSpan 前会 ensureBasicTelemetryInstrumentation (instrumentation.ts:95);若用 disableGenkitOTelInitialization(js/core/src/tracing.ts:131) 自管 OTel,得自己接好导出,否则 Dev UI 看不到 trace。


7. 横向对比

  • 同 shelf(ai-agent-reference)其它 Agent 框架: 多数框架也需要"对话记忆 + 可观测"这对能力。 Genkit 的特色是把它俩都架在同一套 Action/注册表底座上——记忆靠 Session 绑异步上下文, 可观测靠 Action 入口统一开 span,零散埋点几乎不需要。可对照总库 doc 里"状态持久化 / 可观测性" 两个关切,看别家是用中间件、装饰器还是外置追踪。

  • 本章在 Genkit 内部的位置:03 生成引擎04 工具与中断之上的**"产品化"层**——把单次调用变成 可续接、可回放的对话;底座是01 Action 与注册表

  • 多语言镜像(呼应 index 的横向视角): 同一模型在 Genkit 各语言运行时里平行实现—— 它们共用一份 reflectionApi.yaml 契约,所以同一个 Dev UI 能连任意语言的运行时:

    语言会话追踪反射服务
    JS(本章)js/ai/src/session.tsjs/core/src/tracing/instrumentation.tsjs/core/src/reflection.ts
    Gogo/ai/exp/session.go(+ Firestore store)go/core/tracing/tracing.gogo/genkit/reflection.go
    Python(py/packages/genkit)py/packages/genkit/src/genkit/_core/_tracing.pypy/packages/genkit/src/genkit/_core/_reflection.py

    说明:本克隆里未见独立的 Dart 运行时目录;上表只列出树内实际存在的 JS / Go / Python 三家 (三者都还有 reflection-v2.ts / reflection_v2.go / _reflection_v2.py 的新版实现在并行演进)。


8. 代码地图(导航索引)

用符号名 grep 比行号抗漂移;下表按"想看什么"组织。

想看什么文件路径关键符号
会话内存容器js/ai/src/session.tsSessionupdateCustomaddArtifactsgetVersion
异步上下文绑定js/ai/src/session.tsrunWithSessiongetCurrentSessionreserveSnapshotId
快照存储接口js/ai/src/session.tsSessionStoreSnapshotMutatorgetSnapshotsaveSnapshot
会话状态/快照 schemajs/ai/src/agent-types.tsSessionStateSessionSnapshotSnapshotStatusSchemaAgentInit
内置两种 storejs/ai/src/session-stores.tsInMemorySessionStoreFileSessionStoreselectLeafSnapshotatomicWrite
逐轮执行 + 存盘js/ai/src/agent.tsSessionRunnerrunmaybeSnapshotabortAwareMutator
面向用户的对话对象js/ai/src/agent-core.tsAgentChatAgentChatImplsendsendStreambuildInit
定义有状态 Agentjs/genkit/src/genkit-beta.tsGenkitBeta.defineAgentstore 选项
顶层取当前会话js/ai/src/genkit-ai.tscurrentSession
Action 自动开 spanjs/core/src/action.tsrunInNewSpan 调用点、SPAN_TYPE_ATTR: 'action'
span 实现与属性js/core/src/tracing/instrumentation.tsrunInNewSpanSPAN_TYPE_ATTRstartActiveSpanmetadataToAttributesisFailureSource
OTel 初始化/flushjs/core/src/tracing.tsenableTelemetryflushTracingensureBasicTelemetryInstrumentationcheckFirebaseMonitoringAutoInit
开发期反射服务js/core/src/reflection.tsReflectionServer/api/actions/api/runAction/api/notifywriteRuntimeFile
工具侧连运行时genkit-tools/common/src/manager/manager.tsRuntimeManagersetupRuntimesWatcher/api/traces
反射 API 契约genkit-tools/reflectionApi.yamlGenkit Reflection API(OpenAPI)