图解 Pi AgentPi Agent, Illustratedv1.1.0
包Packages· telemetry
演示Slides
Infra已完成Donepackages/telemetry · v1.1.0

telemetry@earendil-works/pi-telemetry

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

1.1.0
版本version
6
源文件src files
935
源码行src LOC
2
测试文件test files
6
测试用例test cases
2
导出路径export paths
依赖Depends on无内部依赖no internal deps被依赖Used bypi-ai

这个包是什么What this package ispi-telemetry 是 pi 的遥测契约,而不是遥测系统:一个回调式的 TelemetryContext.startSpan(options, callback),一个同时充当子上下文的 TelemetrySpan(属性、事件、状态),两个参考实现(共享的 NOOP_TELEMETRY_CONTEXT 与确定性的 InMemoryTelemetryContext),一套与测试框架无关的适配器一致性用例,以及把可序列化 schema 变成精确 TypeScript 类型的工具。它不导出任何数据、没有运行时依赖、不用环境上下文;真正对接 OpenTelemetry 或 Sentry 的是适配器。pi-telemetry is pi's telemetry contract, not a telemetry system: a callback-style TelemetryContext.startSpan(options, callback), a TelemetrySpan that doubles as the child context (attributes, events, status), two reference implementations (the shared NOOP_TELEMETRY_CONTEXT and the deterministic InMemoryTelemetryContext), a runner-independent adapter conformance suite, and utilities that turn serializable schemas into exact TypeScript types. It exports no data, has no runtime dependencies and uses no ambient context; adapters do the actual OpenTelemetry or Sentry work.

935 行 TypeScript,两个导入路径(. 与 ./testing),零运行时依赖。在 v1.1.0 里唯一的使用者是 pi-ai:它在 ProviderRequestOptions 上接受 telemetryContext 并原样传给 provider。README 提到的 agent-core schema(AGENT_TELEMETRY_SCHEMAS)在这个标签上还不存在。整个包在 0.84.0 一次性加入,1.1.0 没有变化。935 lines of TypeScript, two import paths (. and ./testing), zero runtime dependencies. At v1.1.0 its only user is pi-ai, which accepts telemetryContext on ProviderRequestOptions and hands it to providers unchanged. The agent-core schemas the README mentions (AGENT_TELEMETRY_SCHEMAS) do not exist at this tag yet. The whole package arrived in 0.84.0; 1.1.0 changes nothing.

01Infra

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

一句话结论Takeawayindex.ts 声明契约类型、schema 定义与全部类型推导,以及 createTypedSpanStarter();noop.ts 是一个冻结的惰性 span;memory.ts 是记录到内存的参考适配器,在父 span 已结束或记录失败时退回 no-op。./testing 子路径放适配器一致性套件,它是包里唯一使用 Node API(node:assert/strict)的部分。index.ts declares the contract types, the schema definitions with all their type inference, and createTypedSpanStarter(); noop.ts is one frozen inert span; memory.ts is the reference adapter that records in memory and falls back to the no-op when the parent has settled or recording fails. The ./testing subpath holds the adapter conformance suite, the only part of the package that uses a Node API (node:assert/strict).

pi-telemetry 的结构:契约、两个参考上下文与 ./testingpi-telemetry: the contract, two reference contexts and ./testing
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
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

要点Key points

  • 根入口只有 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.
  • 没有运行时依赖:package.json 只有 @types/node 与 vitest 两个 devDependencies。No runtime dependencies: package.json has just two devDependencies, @types/node and vitest.
  • 别和 coding-agent 的 core/telemetry.ts 混淆:那是“安装遥测”开关(PI_TELEMETRY 或设置项),与这个包无关。Don't confuse it with coding-agent's core/telemetry.ts: that is the install-telemetry switch (PI_TELEMETRY or a setting) and has nothing to do with this package.

工作原理How it works

  1. 可选遥测:函数参数默认 telemetryContext = NOOP_TELEMETRY_CONTEXT,调用方想要时再传真实的上下文。Optional telemetry: default a parameter to telemetryContext = NOOP_TELEMETRY_CONTEXT and let callers pass a real context when they want one.
  2. 测试:new InMemoryTelemetryContext(),跑被测代码,再断言 getSpans()。Tests: new InMemoryTelemetryContext(), run the code under test, then assert on getSpans().
  3. 写适配器:实现 TelemetryContext,再用 ./testing 的 createTelemetryAdapterConformance() 验证。Writing an adapter: implement TelemetryContext, then verify it with createTelemetryAdapterConformance() from ./testing.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/telemetry/src/index.tsL14TelemetryContext契约the contract
packages/telemetry/src/noop.tsL20NOOP_TELEMETRY_CONTEXT共享的惰性上下文the shared inert context
packages/telemetry/src/memory.tsL192InMemoryTelemetryContext参考适配器the reference adapter
packages/telemetry/src/testing/index.tsL1./testing一致性套件入口conformance entry
packages/telemetry/package.jsonL8exports两个导入路径two import paths
02Infra

公共 APIPublic API#

一句话结论Takeaway2 个导入路径。根入口 index.ts 自己声明了大部分 API,并从 noop.ts / memory.ts 再导出两个上下文:合计 4 个值和 30 个类型——值只有两个上下文与两个 schema 函数,其余全是类型(契约、schema 形状与推导工具)。2 import paths. The root index.ts declares most of the API itself and re-exports the two contexts from noop.ts / memory.ts: 4 values and 30 types in all — the values are just the two contexts and the two schema functions; everything else is a type (the contract, schema shapes and inference utilities).

导入路径Import path目标Target说明Notes
@earendil-works/pi-telemetry./dist/index.js契约类型、no-op 与内存上下文、schema 定义与类型化启动器contract types, the no-op and in-memory contexts, schema definitions and typed starters
@earendil-works/pi-telemetry/testing./dist/testing/index.js与测试框架无关的适配器一致性套件(使用 node:assert)the runner-independent adapter conformance suite (uses node:assert)

package.json · exportsL8

常用导出Key exports

模块Module常用导出Key exports用途Use
packages/telemetry/src/index.tsTelemetryContext TelemetrySpan SpanOptions SpanStatus回调式的 span 契约the callback span contract
packages/telemetry/src/noop.tsNOOP_TELEMETRY_CONTEXT遥测关闭时的默认上下文the default context when telemetry is off
packages/telemetry/src/memory.tsInMemoryTelemetryContext RecordedTelemetrySpan确定性的内存记录与快照deterministic in-memory recording and snapshots
packages/telemetry/src/index.tsdefineTelemetrySchema TelemetrySchemaDefinition可序列化的 schemaserializable schemas
packages/telemetry/src/index.tscreateTypedSpanStarter TypedSpanStarter SchemaTelemetrySpan按 schema 检查的 span 启动器span starters checked against schemas

子路径 ./testingSubpath ./testing

模块Module常用导出Key exports用途Use
packages/telemetry/src/testing/conformance.tscreateTelemetryAdapterConformance9 个一致性用例the 9 conformance cases
packages/telemetry/src/testing/types.tsTelemetryAdapterFixture TelemetryAdapterFixtureFactory TelemetryAdapterConformanceCasefixture 与用例的类型fixture and case types

根入口的全部导出(按声明所在模块,逐行链接)Every root export, by declaring module (line-linked)

由构建脚本扫描 index.ts 自身的 export 声明并展开 export { … } from 生成;每个符号链接到 v1.1.0 中的定义行。Generated at build time by scanning index.ts's own export declarations and expanding its export { … } from statements; each symbol links to its defining line at v1.1.0.

03Infra

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

一句话结论Takeaway一个 span 是一次操作的计时记录,带属性(事实)、事件(时刻)和状态(ok / error)。TelemetryContext 只有一个方法 startSpan();回调拿到的 TelemetrySpan 继承自 TelemetryContext,所以它本身就是子 span 的父上下文。属性值只能是字符串、数字、布尔或它们的只读数组。A span is a timed record of one operation, carrying attributes (facts), events (moments) and a status (ok / error). TelemetryContext has a single method, startSpan(); the TelemetrySpan the callback receives extends TelemetryContext, so it is itself the parent context for child spans. Attribute values are strings, numbers, booleans or readonly arrays of them.

span、属性、事件、状态,以及作为上下文的 TelemetrySpanSpans, attributes, events, status, and TelemetrySpan as the context
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
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

要点Key points

  • 没有 end():startSpan() 拥有结束时机,回调的返回值或 promise 落定时 span 才结束。There is no end(): startSpan() owns settlement, and the span ends when the callback's value or promise settles.
  • 父子关系完全靠显式传参:把回调里的 span 传给下层代码。包里没有 AsyncLocalStorage。Parentage is purely explicit: pass the callback's span down to lower-level code. There is no AsyncLocalStorage in the package.
  • span 是诊断数据而不是业务状态:记录与否不能改变操作是否执行、成功或失败。A span is diagnostic data, not business state: recording must not change whether an operation runs, succeeds or fails.

工作原理How it works

  1. ctx.startSpan({ name, attributes }, async (span) => { … span.setAttributes({ found }) … })。ctx.startSpan({ name, attributes }, async (span) => { … span.setAttributes({ found }) … }).
  2. 子 span:在回调里 span.startSpan({ name: 'child' }, …)。A child: span.startSpan({ name: 'child' }, …) inside the callback.
  3. 预期失败(返回 { ok: false })要显式 span.setStatus({ status: 'error', error: { name, message } })。An expected failure (returning { ok: false }) needs an explicit span.setStatus({ status: 'error', error: { name, message } }).

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/telemetry/src/index.tsL1AttributeValue属性值attribute values
packages/telemetry/src/index.tsL7SpanOptions名字与开始属性name and start attributes
packages/telemetry/src/index.tsL12SpanStatusok / errorok / error
packages/telemetry/src/index.tsL18TelemetrySpanspan 即上下文a span is a context
04Infra

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

一句话结论TakeawaystartSpan() 同步调用回调恰好一次。回调正常返回或 promise 兑现 → 状态保持 ok;同步抛错或 promise 拒绝 → 自动设为 error { name, message }——除非回调已经显式 setStatus()。settleSpan() 只执行一次,记下结束序号;返回的 promise 以同一个值兑现或拒绝。结束之后的记录调用全部静默无效。startSpan() calls the callback synchronously, exactly once. A normal return or a resolved promise keeps the status ok; a synchronous throw or a rejection sets error { name, message } automatically — unless the callback already called setStatus(). settleSpan() runs once and records the end sequence; the returned promise resolves or rejects with the same value. Recording calls after settlement are silent no-ops.

一次 startSpan():进入回调、自动与显式状态、结束One startSpan(): admission, automatic and explicit status, settlement
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
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

要点Key points

  • 显式状态优先于自动状态:先 setStatus({ status: 'ok' }) 再抛错,span 仍是 ok。Explicit status beats the automatic one: setStatus({ status: 'ok' }) and then throw, and the span stays ok.
  • 父 span 已结束时再开子 span,或创建记录本身失败,都会走 NOOP_TELEMETRY_CONTEXT:业务回调照常执行,只是不记录。Starting a child under a settled parent, or a failure while creating the record, goes through NOOP_TELEMETRY_CONTEXT: the business callback still runs, it just isn't recorded.
  • 错误详情的读取是被动的:读 error.name 抛错时,状态退回为不带详情的 error。Reading error details is passive: if reading error.name throws, the status falls back to error without details.

工作原理How it works

  1. startInMemorySpan():检查父 span → createSpan() → callback(span) → Promise.resolve(result).then(settle ok, settle error)。startInMemorySpan(): check the parent → createSpan() → callback(span) → Promise.resolve(result).then(settle ok, settle error).
  2. 同步抛错:先 settleSpan(…, true, error),再 return Promise.reject(error)。A synchronous throw: settleSpan(…, true, error) first, then return Promise.reject(error).
  3. 每个记录方法开头都是 if (recordedSpan.settled) return;,主体包在 try { … } catch {} 里。Every recording method starts with if (recordedSpan.settled) return; and wraps its body in try { … } catch {}.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/telemetry/src/memory.tsL120startInMemorySpan()一次 span 的全过程one span, start to end
packages/telemetry/src/memory.tsL89settleSpan()只结束一次settles once
packages/telemetry/src/memory.tsL78automaticErrorStatus()自动 error 状态automatic error status
packages/telemetry/src/noop.tsL3startNoopSpan()no-op 的同一套语义the same semantics, no recording
05Infra

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

一句话结论TakeawayREADME 为适配器列出 9 条规则:同步且只调用一次回调、保留返回值与拒绝值、promise 落定前保持 span 打开、自动与显式状态、setStatus 最后一次生效、setAttributes 合并、记录方法被动不抛错、结束后调用无效、失败的记录调用整体忽略。./testing 的 createTelemetryAdapterConformance(factory) 把这些写成 5 组共 9 个与测试框架无关的用例。The README lists 9 rules for adapters: call the callback synchronously and once, preserve return and rejection values, keep the span open until the promise settles, automatic and explicit status, last-write-wins setStatus, merging setAttributes, passive non-throwing recording, inert calls after settlement, and ignoring a failed recording call whole. createTelemetryAdapterConformance(factory) in ./testing turns them into 9 runner-independent cases in 5 groups.

适配器契约的 9 条规则与 9 个一致性用例The 9 adapter-contract rules and the 9 conformance cases
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
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)

要点Key points

  • 用例是普通对象 { 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.
  • 被动性用 Proxy 检验:读取、枚举、取原型时都抛错的载荷不能让适配器抛错,也不能留下半条记录。Passivity is tested with a Proxy that throws on read, enumeration and prototype access: the adapter must not throw and must not keep half a record.
  • fixture 的 getSpans() 返回规范化的 RecordedTelemetrySpan[],可以先 flush 异步导出器。A fixture's getSpans() returns normalized RecordedTelemetrySpan[] and may flush an async exporter first.

工作原理How it works

  1. createTelemetryAdapterConformance(async () => ({ context, getSpans, [Symbol.asyncDispose] }))。createTelemetryAdapterConformance(async () => ({ context, getSpans, [Symbol.asyncDispose] })).
  2. for (const group of new Set(cases.map(c => c.group))) describe(group, () => …it(c.name, () => c.run()))。for (const group of new Set(cases.map(c => c.group))) describe(group, () => …it(c.name, () => c.run())).
  3. 每个用例 await using fixture = await factory():一个用例一个全新的适配器,结束时自动释放。Each case does await using fixture = await factory(): a fresh adapter per case, disposed automatically.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/telemetry/README.mdL118README · Adapter Contract9 条规则the 9 rules
packages/telemetry/src/testing/conformance.tsL61createTelemetryAdapterConformance()9 个用例the 9 cases
packages/telemetry/src/testing/conformance.tsL43unreadable()读取即抛错的载荷payloads that throw on read
packages/telemetry/src/testing/types.tsL5TelemetryAdapterFixturefixture 契约the fixture contract
packages/telemetry/test/conformance.test.tsL5conformance.test.ts跑在内存适配器上run against the in-memory adapter
06Infra

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

一句话结论TakeawayInMemoryTelemetryContext 每个实例有一份状态:span 数组、nextSpanId 与 nextEndSequence。id 按开始顺序分配,endSequence 按结束顺序分配,所以嵌套与并发的结果是确定的,不需要时间戳。getSpans() 返回分离的快照:属性、数组、事件和状态全部复制,内部的 explicitStatus 不暴露。Each InMemoryTelemetryContext instance has one state: the span array, nextSpanId and nextEndSequence. Ids follow start order and endSequence follows end order, so nesting and concurrency produce deterministic results without timestamps. getSpans() returns detached snapshots: attributes, arrays, events and status are all copied, and the internal explicitStatus stays hidden.

InMemoryTelemetryContext:id、结束序号与分离的快照InMemoryTelemetryContext: ids, end sequence and detached snapshots
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
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

要点Key points

  • 一致性用例里,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).
  • 回调运行中调用 getSpans():看到 settled: false 且没有 endSequence。Calling getSpans() while a callback runs shows settled: false and no endSequence.
  • 存储无上限且只在本进程内:用于测试与本地诊断,每个测试 new 一个实例。Storage is unbounded and process-local: it is meant for tests and local diagnostics, one fresh instance per test.

工作原理How it works

  1. createSpan():id: state.nextSpanId++、parentId: parent?.id ?? null、复制开始属性(丢弃 undefined)。createSpan(): id: state.nextSpanId++, parentId: parent?.id ?? null, copied start attributes (dropping undefined).
  2. mergeAttributes() 先复制再合并;中途抛错时整个调用被忽略,原属性不变。mergeAttributes() copies, then merges; if it throws midway the whole call is ignored and the attributes stay unchanged.
  3. getSpans() 只在结束过的 span 上加 endSequence 字段。getSpans() adds the endSequence field only to settled spans.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/telemetry/src/memory.tsL16RecordedTelemetrySpan快照形状the snapshot shape
packages/telemetry/src/memory.tsL101createSpan()分配 idassigns ids
packages/telemetry/src/memory.tsL63mergeAttributes()合并属性merges attributes
packages/telemetry/src/memory.tsL204getSpans()分离的快照detached snapshots
packages/telemetry/test/conformance.test.tsL23detached snapshots test快照不可变snapshots don't leak
07pi-agent-core

可序列化的 schemaSerializable schemas#

一句话结论TakeawayTelemetrySchemaDefinition 是普通 JSON:version 加 spans 表。每个 span 定义描述、父子规则(any / root_or_external / 列出的 span)、开始属性(每个都声明 required)、结束属性(总是可选)、事件(属性声明 required)以及状态规则 { default: 'ok', errorWhen }。属性有 6 种类型,可带封闭集合、示例、sensitive 与 cardinality。A TelemetrySchemaDefinition is plain JSON: a version plus a spans table. Each span declares a description, a parent rule (any / root_or_external / listed spans), start attributes (each declares required), end attributes (always optional), events (whose attributes declare required) and a status rule { default: 'ok', errorWhen }. Attributes have 6 types and may carry closed sets, examples, sensitive and cardinality.

可序列化的 schema:span、属性、事件与规则Serializable schemas: spans, attributes, events and rules
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
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 (◆)

要点Key points

  • defineTelemetrySchema() 是类型化的恒等函数:原样返回参数,不做运行时校验,也不强制父子规则。defineTelemetrySchema() is a typed identity function: it returns its argument, with no runtime validation and no parent-rule enforcement.
  • start 与 end 只说明值“何时可知”,最后都落在同一个后端 span 的同一个属性袋里。start vs end only says when a value is known; both end up in the same attribute bag of the same backend span.
  • 父子规则、sensitive、cardinality 都是描述性元数据:适配器不需要理解 schema 对象。Parent rules, sensitive and cardinality are descriptive metadata: adapters never need to understand schema objects.

工作原理How it works

  1. 用 as const 写 schema(或内联),让字面量类型保留下来。Write the schema as const (or inline) so the literal types survive.
  2. values: ['account', 'project'] 让这个属性的类型变成 'account' | 'project';数组用 elementValues。values: ['account', 'project'] types the attribute as 'account' | 'project'; arrays use elementValues.
  3. 因为是 JSON,schema 可以导出给文档或外部校验工具。Being JSON, a schema can be exported to documentation or external validators.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/telemetry/src/index.tsL66TelemetrySchemaDefinition顶层形状top-level shape
packages/telemetry/src/index.tsL57TelemetrySpanDefinition一个 span 的定义one span's definition
packages/telemetry/src/index.tsL34TelemetryAttributeDefinition6 种属性类型6 attribute types
packages/telemetry/src/index.tsL52TelemetryParentDefinition父子规则parent rules
packages/telemetry/src/index.tsL72defineTelemetrySchema()恒等函数identity function
08pi-agent-core

类型化的 span 启动器Typed span starters#

一句话结论TakeawaycreateTypedSpanStarter(ctx, [A, B]) 把一个父上下文绑定到若干 schema 的合并词汇上。编译期:每个 span 名一个重载,开始属性必须精确(缺 required、多余的键、封闭集合外的值都报错),跨 schema 的重名 span 直接让参数类型失效。运行时它只是 ctx.startSpan({ name, attributes }, …),再把绑定到子 span 的新启动器交给回调。createTypedSpanStarter(ctx, [A, B]) binds one parent context to the combined vocabulary of several schemas. At compile time there is one overload per span name and start attributes must be exact (missing required keys, extra keys and values outside a closed set all fail), while a span name present in two schemas breaks the argument type outright. At run time it is just ctx.startSpan({ name, attributes }, …), handing the callback a new starter bound to the child span.

createTypedSpanStarter:编译期检查,运行时薄包装createTypedSpanStarter: compile-time checks, a thin run-time wrapper
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
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

要点Key points

  • 回调签名是 (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.
  • 类型为联合的名字('operation' | 'request')必须先收窄,名字与属性的对应关系才不会丢。A union-typed name ('operation' | 'request') must be narrowed first, so the name ↔ attributes correlation is never lost.
  • schema 值只参与类型推导:运行时不检查、不合并、不保留;传一个读取即抛错的数组也不会出错(有测试)。Schema values only drive inference: never inspected, merged or kept at run time; even an array that throws on read is fine (tested).

工作原理How it works

  1. const start = createTypedSpanStarter(ctx, [operationSchema, requestSchema])。const start = createTypedSpanStarter(ctx, [operationSchema, requestSchema]).
  2. await start('operation', { kind: 'read' }, (_op, child) => child('request', { provider: 'example' }, (req) => …))。await start('operation', { kind: 'read' }, (_op, child) => child('request', { provider: 'example' }, (req) => …)).
  3. 单独声明的 schema 数组要加 as const,才能保持元组类型。A separately declared schema array needs as const to keep its tuple type.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/telemetry/src/index.tsL349createTypedSpanStarter()入口entry point
packages/telemetry/src/index.tsL324bindTypedSpanStarter()运行时的全部实现the whole run-time part
packages/telemetry/src/index.tsL318TypedSpanStarter按名重载per-name overloads
packages/telemetry/src/index.tsL293UniqueTelemetrySchemas拒绝重名 spanrejects duplicate names
packages/telemetry/src/index.tsL140ExactTelemetryAttributes拒绝多余的键rejects extra keys
09pi-ai

在 pi 里的位置Where it sits in pi#

一句话结论TakeawayREADME 规定的分工是:pi-telemetry 拥有契约,pi-ai 只接受并传递 telemetryContext,pi-agent-core 拥有 pi.ai.* / pi.harness.* / pi.session.* 的 schema。在 v1.1.0 里前两条已经实现——ProviderRequestOptions.telemetryContext 经 buildBaseOptions() 原样到达每条分派路径——第三条还没有:agent-core 既不依赖这个包,也没有这些 schema。The README's division of labour: pi-telemetry owns the contract, pi-ai only accepts and propagates telemetryContext, and pi-agent-core owns the pi.ai.* / pi.harness.* / pi.session.* schemas. At v1.1.0 the first two are in place — ProviderRequestOptions.telemetryContext reaches every dispatch path unchanged through buildBaseOptions() — but the third is not: agent-core neither depends on the package nor has those schemas.

在 pi 里:pi-ai 传递 telemetryContext;分工与安全规则In pi: pi-ai carries telemetryContext; ownership and security rules
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
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

要点Key points

  • 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-ai 的测试检查了 8 条流式 / 延迟分派路径(provider 与 Models 各 4 条)和图像分派,拿到的都是同一个对象。pi-ai's tests check 8 streaming / deferred dispatch paths (4 each on the provider and on Models) plus image dispatch, and all receive the very same object.
  • 安全规则:不要把上下文或 span 存进记录、消息、快照或 deferred handle;属性避开提示词、输出、文件内容与凭据。Security rules: never store a context or span in records, messages, snapshots or deferred handles; keep prompts, outputs, file contents and credentials out of attributes.

工作原理How it works

  1. streamSimple(model, context, { telemetryContext: span }):把当前 span 作为这次请求的父上下文。streamSimple(model, context, { telemetryContext: span }): make the current span the parent of this request.
  2. 自定义 provider 在 stream(model, context, options) 里读 options.telemetryContext,自己决定开哪些子 span。A custom provider reads options.telemetryContext in stream(model, context, options) and decides which child spans to open.
  3. 适配器可以在内部激活后端的环境上下文以便自动埋点,但 pi 代码始终显式传递父上下文。An adapter may activate its backend's ambient context internally for auto-instrumentation, but pi code always passes the parent explicitly.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/ai/src/types.tsL141ProviderRequestOptions.telemetryContext选项字段the option field
packages/ai/src/api/simple-options.tsL48buildBaseOptions()原样传递passed through as is
packages/ai/test/telemetry-options.test.tsL75telemetry-options.test.ts每条分派路径every dispatch path
packages/telemetry/README.mdL365README · Pi Package Integration分工ownership
packages/telemetry/README.mdL385README · Security and Portability安全与可移植性security and portability
10Infra

扩展点Extension points#

TelemetryContext实现它就是一个适配器:把 startSpan 映射到 OpenTelemetry、Sentry、日志或任何后端。Implementing it makes an adapter: map startSpan onto OpenTelemetry, Sentry, logs or any backend.
createTelemetryAdapterConformance(factory)用 9 个一致性用例验证你的适配器,可注册到任何测试框架。Verify your adapter with the 9 conformance cases, registered with any test runner.
defineTelemetrySchema + createTypedSpanStarter为自己的领域定义封闭的 span 词汇,在编译期检查名字与属性。Define a closed span vocabulary for your domain, with names and attributes checked at compile time.
ProviderRequestOptions.telemetryContext把父 span 交给 pi-ai 的一次请求;provider 实现决定开哪些子 span。Hand a parent span to one pi-ai request; the provider implementation decides which child spans to open.
InMemoryTelemetryContext在测试里断言你的代码产生了哪些 span、属性、事件与状态。Assert in tests which spans, attributes, events and statuses your code produced.
11Infra

测试Tests#

一句话结论Takeaway两个测试文件:conformance.test.ts 把 9 个一致性用例跑在 InMemoryTelemetryContext 上,并检查快照是分离的;telemetry.test.ts 覆盖 schema 的类型推导(9 处 @ts-expect-error)、多 schema 启动器的父子绑定与 NOOP 上下文。pi-ai 另有 telemetry-options.test.ts 检查 telemetryContext 在每条分派路径上不变。Two test files: conformance.test.ts runs the 9 conformance cases against InMemoryTelemetryContext and checks that snapshots are detached; telemetry.test.ts covers schema type inference (9 @ts-expect-error lines), parent binding in multi-schema starters, and the NOOP context. pi-ai adds telemetry-options.test.ts, which checks that telemetryContext survives every dispatch path.

用例数 = 源码中 it( / test( 的出现次数(静态统计,不运行测试)。Cases = occurrences of it( / test( in the source (static count; tests were not run).

12Infra

交叉链接Cross-links#