v1.1.0Pi v1.1.0 源码分析Analysis of Pi v1.1.0 · 2026-10-07

telemetrytelemetry

与厂商无关的遥测契约与类型化 schema 工具Vendor-neutral telemetry contracts and typed schema utilities for pi

Infra

← → 翻页 · F 全屏 · O 目录 · T 明暗 · L 语言← → navigate · F fullscreen · O overview · T theme · L language

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.001
01 · 一个契约、两个参考实现、一套一致性测试01 · One contract, two reference contexts, one conformance suiteInfra

一个契约、两个参考实现、一套一致性测试One contract, two reference contexts, one conformance suite

pi-telemetry: 935 lines — a contract, two reference contexts and a conformance suitepackages/telemetry/srctesting/./testing依赖与使用者Dependencies and users357index.ts契约类型 · schema 定义与类型推导 · createTypedSpanStarter · 重新导出两个 contextcontract types · schema definitions and inference · createTypedSpanStarter · re-exports both contexts20noop.tsNOOP_TELEMETRY_CONTEXT一个冻结的惰性 spanone frozen inert span219memory.tsInMemoryTelemetryContext确定性的内存记录deterministic recording6index.ts子路径入口subpath entry18types.tsFixture · Case 类型Fixture · Case types315conformance.ts9 个用例 · node:assert9 cases · node:assertdefineTelemetrySchemacreateTypedSpanStarterNOOP_TELEMETRY_CONTEXTInMemoryTelemetryContextTelemetryContextTelemetrySpanSpanStatusTelemetrySchemaDefinitionTypedSpanStarterSchemaTelemetrySpanInfer*Attributes1pi-aiProviderRequestOptions.telemetryContext只传递,不拥有任何 schemapropagates it; owns no schemapi-agent-coreAGENT_TELEMETRY_SCHEMAS · startAiSpan …agent-core 既不依赖也不导出它们agent-core neither depends on nor exports themcoding-agent · core/telemetry.ts安装遥测:PI_TELEMETRY 或设置项install telemetry: PI_TELEMETRY or a setting不导入 pi-telemetrydoes not import pi-telemetry再导出re-export再导出re-export父 span 已结束或记录失败时,memory.ts 退回 NOOP 上下文。memory.ts falls back to the NOOP context when the parent has settled or recording fails.只有这里用 Node 的Only this subpath uses断言 API;根包保持Node assertion APIs; the运行时无关。root stays runtime-neutral.根入口导出 4 个值The root exports 4 values… 和 30 个类型,例如… and 30 types, such as运行时依赖Runtime dependencies无:package.json 只有 devDependencies(@types/node、vitest)none: package.json has only devDependencies (@types/node, vitest)被导入(src 中导入它的文件数)Imported by (files in src)README 里写到、但 v1.1.0 源码里没有In the README, but not in the v1.1.0 source同名但无关Same name, unrelatedpi-telemetry 只定义契约;pi-telemetry only defines the contract;导出到 OpenTelemetry、Sentry 等由适配器负责。exporting to OpenTelemetry, Sentry … is the adapter's job.CHANGELOG:整个包在 0.84.0 一次性加入(回调式上下文契约、no-op、内存参考适配器、一致性套件、类型化 schema);1.1.0 没有新条目。CHANGELOG: the whole package arrived in 0.84.0 (callback context contract, no-op, in-memory reference adapter, conformance suite, typed schemas); 1.1.0 has no entries.标签 = 行数 / 文件数tag = lines / files契约与参考实现contract & reference contexts./testing使用者users导出 / 回退export / fallback

根入口只有 4 个值导出:defineTelemetrySchema、createTypedSpanStarter、NOOP_TELEMETRY_CONTEXT 与 InMemoryTelemetryContext,其余全是类型。The root has only 4 value exports — defineTelemetrySchema, createTypedSpanStarter, NOOP_TELEMETRY_CONTEXT and InMemoryTelemetryContext; everything else is a type.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.002
02 · span、属性、事件、状态与上下文02 · Spans, attributes, events, status and contextInfra

span、属性、事件、状态与上下文Spans, attributes, events, status and context

The model: a span tree with attributes, events and status; a TelemetrySpan is itself the context for children一次运行产生的遥测(README 的例子)Telemetry from one run (the README example)核心类型(index.ts)Core types (index.ts)telemetryContext调用方传进来的父上下文the parent passed in by the callerspanexample.account.loadspan:一次操作的计时记录span: a timed record of one operationattributesaccount.id=123 · found=true关于 span 的事实facts about the spaneventexample.cache.lookup { cache.hit: false }span 期间的一个时刻a moment during the spanstatusok最终结果:ok 或 errorthe outcome: ok or errorchild spanexample.db.queryspan.startSpan() → 子节点span.startSpan() → a child«interface»TelemetryContextstartSpan<T>(options, callback): Promise<T>«interface» extends TelemetryContextTelemetrySpanaddEvent(name, attributes?): voidsetAttributes(attributes): voidsetStatus(status): voidSpanOptionsname: stringattributes?: SpanAttributes«union»SpanStatus{ status: "ok" }{ status: "error"; error?: { name, message } }startSpan()子 span 只能经由显式传下去的父 span 创建:没有环境上下文(AsyncLocalStorage)。Children are created only through the explicitly passed parent span: no ambient context (AsyncLocalStorage).AttributeValue = string | number | boolean | 三种只读数组AttributeValue = string | number | boolean | three readonly arraysSpanAttributes = { [name]: AttributeValue | undefined }没有公开的 end():startSpan() 拥有结束时机——回调的值或 promise 落定时 span 才结束。span 是诊断数据,不是业务状态:记录与否不能改变业务结果。There is no public end(): startSpan() owns settlement — the span ends when the callback's value or promise settles. A span is diagnostic data, not business state: recording must not change the outcome.span 与契约spans & contract事件event状态status开始 spanstart a span包含contains

没有 end():startSpan() 拥有结束时机,回调的返回值或 promise 落定时 span 才结束。There is no end(): startSpan() owns settlement, and the span ends when the callback's value or promise settles.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.003
03 · 一次 startSpan() 的生命周期03 · The life of one startSpan()Infra

一次 startSpan() 的生命周期The life of one startSpan()

One startSpan(): the callback runs once, synchronously; the span ends when it settlesspan 的方法Span methodsstartSpan(opts, cb)父 span 已结束?Parent settled?或 createSpan 抛错or createSpan throwscreateSpan()复制属性 · id = nextSpanId++copy attributes · id = nextSpanId++callback(span)同步调用,恰好一次;span 打开called synchronously, exactly once; span openNOOP_TELEMETRY_CONTEXT回调照常执行,不记录callback still runs, nothing recorded返回值 / promise 兑现Returns / promise resolvesstatus 保持 ok(除非显式设置)status stays ok (unless set explicitly)同步抛错 / promise 拒绝Throws / promise rejects自动 error { name, message }automatic error { name, message }settleSpan()settled = true · endSequence = nextEndSequence++ · 只执行一次settled = true · endSequence = nextEndSequence++ · runs once返回的 promiseThe returned promise以同一个值兑现,或以同一个值拒绝(同步抛错也变成拒绝)resolves with the same value or rejects with the same value (a sync throw becomes a rejection)否no是yessetAttributes(a)合并;后写的定义值覆盖,undefined 被忽略merge; later defined values win, undefined ignoredaddEvent(name, a?)按顺序追加事件append an event, in ordersetStatus(s)最后一次生效;标记 explicitStatuslast write wins; marks explicitStatusstartSpan(opts, cb)子 span,parentId = 这个 spana child span, parentId = this span结束之后:前三个方法静默无效;startSpan 走 NOOP(回调仍执行)。After settlement: the first three are silent no-ops; startSpan goes to NOOP (callback still runs).任何记录调用都同步、被动、不抛错;坏数据被整体忽略。Every recording call is synchronous, passive, never throws; bad payloads are ignored whole.显式状态优先:回调先 setStatus({ status: "ok" }) 再抛错,span 仍是 ok;返回 { ok: false } 这类“预期失败”要自己 setStatus(error)。Explicit status wins: setStatus({ status: "ok" }) and then throw, and the span is still ok; an expected failure returned as { ok: false } must setStatus(error) itself.NOOP 上下文走同样的路径,只是不记录:一个共享的冻结 span,嵌套 span 也用它。The NOOP context follows the same path without recording: one shared frozen span, nested spans included.正常normal抛错 / 拒绝throw / rejectmemory.ts结束settlement

显式状态优先于自动状态:先 setStatus({ status: 'ok' }) 再抛错,span 仍是 ok。Explicit status beats the automatic one: setStatus({ status: 'ok' }) and then throw, and the span stays ok.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.004
04 · 适配器契约与一致性套件04 · The adapter contract and its conformance suiteInfra

适配器契约与一致性套件The adapter contract and its conformance suite

The adapter contract: 9 rules in the README, 9 conformance cases in ./testing适配器必须……(README · Adapter Contract)An adapter must … (README · Adapter Contract)createTelemetryAdapterConformance(factory)1 · 2 · 42 · 44 · 56981 · 37 · 99同步调用回调,恰好一次Call the callback synchronously, exactly once并创建子 spanand create a child span保留返回值与拒绝值Preserve the return and rejection value同步抛错 → 以同一个值拒绝的 promisesync throw → a promise rejected with itpromise 落定前 span 保持打开Keep the span open until the promise settles原生 span 的结束由 startSpan 负责startSpan ends the native span正常 → ok,抛错 → errorNormal → ok, throw → error除非已显式设置状态unless a status was set explicitlysetStatus 最后一次生效setStatus is last-write-winssetAttributes 合并setAttributes merges后写的定义值覆盖,undefined 忽略later defined values win, undefined ignored记录方法同步、被动、不抛错Recording is synchronous, passive, non-throwing结束之后的调用被忽略Calls after settlement are ignored失败的记录调用被整体忽略A failed recording call is ignored whole后端错误被吞掉,业务回调仍只执行一次backend errors suppressed; callback still runs oncecallback lifecycleadmits once synchronously and preserves the resultpreserves synchronous and asynchronous rejection valuesstatususes last explicit status without automatic overwriterecordingmerges attributes and records ordered eventsignores failed attribute calls atomicallymakes calls after settlement inertparentagerecords nested and concurrent child relationshipspassivitysuppresses unreadable telemetry payload failuresignores failed status calls atomically右侧标签 = 用例覆盖的规则编号(本站的对应关系)tag = the rules a case covers (this site's mapping)createTelemetryAdapterConformance(async () => ({ context, getSpans: async () => normalizedSpans(), async [Symbol.asyncDispose]() { … } }))→ readonly { group, name, run() }[] · 注册到任何测试框架:for (group) describe(group, …) · it(name, () => run())每个用例用 await using 拿一个全新 fixture;getSpans() 可以先 flush 异步导出器。用 Proxy 构造“读取即抛错”的载荷来检验被动性。Each case takes a fresh fixture with await using; getSpans() may flush an async exporter first. Payloads that throw on read (a Proxy) test passivity.123456789契约规则contract rules一致性用例(按组)conformance cases (by group)

用例是普通对象 { group, name, run() },可以注册进 vitest、node:test 或任何框架;pi 自己的测试把它们挂到 InMemoryTelemetryContext 上。Cases are plain { group, name, run() } objects you can register with vitest, node:test or any runner; pi's own tests run them against InMemoryTelemetryContext.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.005
05 · 内存参考适配器05 · The in-memory reference adapterInfra

内存参考适配器The in-memory reference adapter

InMemoryTelemetryContext: deterministic ids and end sequence numbers, no timestamps每个实例的状态Per-instance statememory.ts一致性用例 “records nested and concurrent child relationships”Conformance case “records nested and concurrent child relationships”getSpans() 的结果What getSpans() returnsidparentIdnameattributeseventsstatussettledendSequence?explicitStatusendSequence 3endSequence 2endSequence 1state = {spans: MutableRecordedTelemetrySpan[]nextSpanId: 1nextEndSequence: 1}每条记录(可变,内部)Each record (mutable, internal)getSpans():按开始顺序返回分离的快照——getSpans(): detached snapshots in start order —属性、数组、事件、状态全部复制;attributes, arrays, events and status all copied;explicitStatus 不出现在快照里。explicitStatus is not part of the snapshot.改快照不影响记录(有专门的测试)。Mutating a snapshot never leaks back (tested).parentid 1first-childid 2 · parent 1second-childid 3 · parent 1时间 →time →开始顺序 = id 顺序start order = id ordersecond-child 同步返回 "done",先结束;second-child returns "done" synchronously and ends first;first-child 等闸门打开后才结束。first-child ends once its gate opens.idparentIdnamestatussettledendSequenceattributes / events1nullparentoktrue3{} / []21first-childoktrue2{} / []31second-childoktrue1{} / []回调还在运行时调用 getSpans():settled = false,没有 endSequence。getSpans() while a callback is still running: settled = false, no endSequence.用途:测试、本地诊断、只想在进程内收集的应用。存储无上限且只在本进程内;为隔离测试请每次 new 一个实例。Use it for tests, local diagnostics and apps that only want in-process capture. Storage is unbounded and process-local; create a fresh instance per test.根 spanroot span子 spanchild span结束序号end sequence

一致性用例里,second-child 同步返回、先结束(endSequence 1),first-child 等闸门打开才结束(2),parent 最后(3)。In the conformance case, second-child returns synchronously and ends first (endSequence 1), first-child ends once its gate opens (2), and parent last (3).

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.006
06 · 可序列化的 schema06 · Serializable schemaspi-agent-core

可序列化的 schemaSerializable schemas

A serializable schema: spans, attributes, events, parent and status rules — all plain JSON dataTelemetrySchemaDefinitionversion: numberspans: Record<name, SpanDefinition>TelemetrySpanDefinitiondescription: stringparents: TelemetryParentDefinitionstartAttributes: Record<name, StartAttributeDefinition>endAttributes: Record<name, AttributeDefinition>events?: Record<name, EventDefinition>status: { default: "ok"; errorWhen: string }TelemetryAttributeDefinitiondescription: stringsensitive?: booleancardinality?: "low" | "high"type: one of 6 (below)values? · elementValues?examples?TelemetryParentDefinition{ kind: "any" }{ kind: "root_or_external" }{ kind: "spans", spans }TelemetryEventDefinitiondescription: stringattributes: Record<…>type:string · number · boolean · string[] · number[] · boolean[]type: string · number · boolean · string[] · number[] · boolean[]values = 标量的封闭集合;elementValues = 数组元素的封闭集合values = a closed set for scalars; elementValues = for array elementsstart 与 event 属性另加 required: boolean;end 属性没有,永远可选start and event attributes add required: boolean; end attributes never doREADME 的例子(description 字段从略)The README example (descriptions omitted)defineTelemetrySchema({version: 1,spans: {'example.read': {parents: { kind: 'any' },startAttributes: {'example.resource': { type: 'string',required: true, values: ['account', 'project'] },},endAttributes: {'example.item_count': { type: 'number' },},events: {'example.cache': { attributes: {'example.cache.hit': { type: 'boolean',required: true } } },},status: { default: 'ok', errorWhen: '…' },},},} as const)何时可知When a value is knownstartAttributes:开始时传入 starter 的 attributes 参数startAttributes: passed to the starter when the span is createdendAttributes:之后用 span.setAttributes() 补充,可以不调endAttributes: added later with span.setAttributes(), or not at all两者落在同一个后端 span 的同一个属性袋里。Both land in the same attribute bag of the same backend span.defineTelemetrySchema() 是类型化的恒等函数:defineTelemetrySchema() is a typed identity function:原样返回参数,运行时不校验、不强制父子规则。it returns its argument; no runtime validation or parent rules.schema 类型schema types包含(◆)contains (◆)

defineTelemetrySchema() 是类型化的恒等函数:原样返回参数,不做运行时校验,也不强制父子规则。defineTelemetrySchema() is a typed identity function: it returns its argument, with no runtime validation and no parent-rule enforcement.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.007
07 · 类型化的 span 启动器07 · Typed span starterspi-agent-core

类型化的 span 启动器Typed span starters

createTypedSpanStarter: checked against the schemas at compile time, a thin startSpan wrapper at run time编译期(TypeScript)Compile time (TypeScript)运行时(bindTypedSpanStarter)Run time (bindTypedSpanStarter)createTypedSpanStarter(ctx, [A, B])一个父上下文 + 一组 schema(元组)one parent context + a tuple of schemas缺少 required 事件属性missing required event attribute封闭集合外的值value outside a closed set未声明的事件undeclared event空 end schema 的任何属性any attribute on an empty end schema未收窄的联合 span 名un-narrowed union span name多出未知键的变量a variable with unknown keys未知 span 名unknown span name属性取自别的 spanattributes from another span重复的 span 名duplicate span namesUniqueTelemetrySchemas跨 schema 的同名 span → 参数类型变成 "duplicate telemetry span names"a span name in two schemas → the argument type becomes "duplicate telemetry span names"TypedSpanStarter每个 span 名一个重载(UnionToIntersection);联合类型的名字必须先收窄one overload per span name (UnionToIntersection); union-typed names must be narrowedExactTelemetryAttributes多出来的键 → never;缺 required 的 start 属性 → 报错extra keys → never; a missing required start attribute → errorAttributeDefinitionValuevalues / elementValues → 字面量联合;否则 string / number / booleanvalues / elementValues → literal unions; otherwise string / number / booleanSchemaTelemetrySpanaddEvent 只收已声明的事件;setAttributes 只收 end 属性addEvent takes declared events only; setAttributes end attributes onlystartSpan(name, attributes, callback) =>telemetryContext.startSpan({ name, attributes },(span) => callback(span,bindTypedSpanStarter(span)))子 starter 绑定到回调的 span:显式传播,同一套 schema。The child starter is bound to the callback span: explicit propagation, same schemas.schema 值只用于类型推导:运行时不检查、不合并、不保留。Schema values only drive type inference: not inspected, merged or kept at run time.测试里的例子The example from the testsstartSpan("operation", { kind: "read" },(_op, startChild) => startChild("request",{ provider: "example" }, (req) => {req.setAttributes({ response: "cached" }); return 42; }))// → 42 · request.parentId === operation.idtelemetry.test.ts 用 9 处 @ts-expect-error 锁定这些拒绝:telemetry.test.ts pins these rejections with 9 @ts-expect-error lines:类型层type level运行时run time编译期拒绝compile-time rejection

回调签名是 (span, startChildSpan):span 只接受这个 span 声明的事件和结束属性,startChildSpan 已绑定到 span。The callback signature is (span, startChildSpan): span accepts only this span's declared events and end attributes, and startChildSpan is already bound to span.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.008
08 · 在 pi 里的位置08 · Where it sits in pipi-ai

在 pi 里的位置Where it sits in pi

In pi: pi-ai carries telemetryContext through to the provider and opens no span itselfpi-ai 的测试证明它在每条分派路径上都还是同一个对象pi-ai tests prove it is the same object on every dispatch pathtelemetry-options.test.ts按 README 的分工Ownership, per the README安全与可移植性Security and portability应用 / 宿主Application / host拥有适配器(OTel、Sentry…)owns the adapter (OTel, Sentry …){ telemetryContext }ProviderRequestOptions · types.tsbuildBaseOptions()simple-options.ts:SimpleStreamOptions → StreamOptions,原样带上simple-options.ts: SimpleStreamOptions → StreamOptions, passed as isprovider.streammodels.streamprovider.streamSimplemodels.streamSimpleprovider.fetchDeferredmodels.fetchDeferredprovider.cancelDeferredmodels.cancelDeferredgenerateImagesmodels.generateImagesv1.1.0 ✓v1.1.0 ✓尚未出现not yet流式与延迟分派共 8 处观察到的 options.telemetryContext 全部 === 传入的对象;图像分派单独一个用例。All 8 observed options.telemetryContext values for streaming and deferred dispatch === the one passed in; image dispatch has its own case.pi-telemetry契约、no-op 与内存上下文、schema 工具、一致性套件contract, no-op and in-memory contexts, schema utilities, conformancepi-ai接受并传递 telemetryContext,不拥有 schemaaccepts and propagates telemetryContext, owns no schemapi-agent-corepi.ai.* / pi.harness.* / pi.session.* schema 与 span 助手pi.ai.* / pi.harness.* / pi.session.* schemas and span helpers不要持久化 context 或 spanNever persist a context or span(记录、消息、快照、deferred handle)(records, messages, snapshots, deferred handles)属性只有标量与数组Attributes are scalars and arrays only避开提示词、输出、文件内容、凭据avoid prompts, outputs, file contents, credentials没有 AsyncLocalStorageNo AsyncLocalStorageNode.js、Bun、浏览器、worker 都能用works in Node.js, Bun, browsers and workerspi-ai 的 src 里 telemetryContext 只出现在 2 行(类型声明与 buildBaseOptions),没有一处 startSpan:真正开 span 的是拿到它的 provider 实现或上层代码。telemetryContext appears on just 2 lines of pi-ai's src (the type and buildBaseOptions), with no startSpan anywhere: spans are opened by the provider implementation or code above that receives it.传递passed alongpi-aipi-telemetry缺口 / 规则gap / rules

pi-ai 的 src 里没有一处 startSpan:开 span 的是拿到上下文的 provider 实现或更上层的代码。There is no startSpan anywhere in pi-ai's src: spans are opened by the provider implementation or higher-level code that receives the context.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.009
扩展点Extension pointspi-coding-agent

telemetry 的扩展点Where to plug into telemetry

1
TelemetryContextTelemetryContext

实现它就是一个适配器:把 startSpan 映射到 OpenTelemetry、Sentry、日志或任何后端。Implementing it makes an adapter: map startSpan onto OpenTelemetry, Sentry, logs or any backend.

2
createTelemetryAdapterConformance(factory)createTelemetryAdapterConformance(factory)

用 9 个一致性用例验证你的适配器,可注册到任何测试框架。Verify your adapter with the 9 conformance cases, registered with any test runner.

3
defineTelemetrySchema + createTypedSpanStarterdefineTelemetrySchema + createTypedSpanStarter

为自己的领域定义封闭的 span 词汇,在编译期检查名字与属性。Define a closed span vocabulary for your domain, with names and attributes checked at compile time.

4
ProviderRequestOptions.telemetryContextProviderRequestOptions.telemetryContext

把父 span 交给 pi-ai 的一次请求;provider 实现决定开哪些子 span。Hand a parent span to one pi-ai request; the provider implementation decides which child spans to open.

5
InMemoryTelemetryContextInMemoryTelemetryContext

在测试里断言你的代码产生了哪些 span、属性、事件与状态。Assert in tests which spans, attributes, events and statuses your code produced.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.010
←
1 / 10