@qilin-agent/session-telemetry 0.0.0-stage → 3.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.i18n.yaml +64 -0
- package/README.md +128 -2
- package/README.zh.md +129 -0
- package/lib/index.js +279 -0
- package/lib/types/coordinator.d.ts +102 -0
- package/lib/types/index.d.ts +163 -0
- package/package.json +39 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kkutysllb
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
|
|
2
|
+
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
|
3
|
+
# After editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/session/session-telemetry/README.md
|
|
5
|
+
/:
|
|
6
|
+
en: 5c553664bfb6ebaa
|
|
7
|
+
zh: 0ad10e1597cb65bc
|
|
8
|
+
/qilin-agent-session-telemetry:
|
|
9
|
+
en: 90a76261e0971942
|
|
10
|
+
zh: 95b8990c999aaef6
|
|
11
|
+
/qilin-agent-session-telemetry/summary:
|
|
12
|
+
en: 2ee6fcb4e68f6c9b
|
|
13
|
+
zh: 8ddc8d1f3c645757
|
|
14
|
+
/qilin-agent-session-telemetry/table-of-contents:
|
|
15
|
+
en: d152484eb41ac6b4
|
|
16
|
+
zh: 09388d293f9be9cb
|
|
17
|
+
/qilin-agent-session-telemetry/use-this-package:
|
|
18
|
+
en: f103da7913199503
|
|
19
|
+
zh: 623f7a102c451289
|
|
20
|
+
/qilin-agent-session-telemetry/use-this-package/choosing-and-mounting-a-backend:
|
|
21
|
+
en: cc1b92c9563ec9e9
|
|
22
|
+
zh: 6e80c5e002a6c83d
|
|
23
|
+
/qilin-agent-session-telemetry/use-this-package/the-backend-contract:
|
|
24
|
+
en: e3cc8ee7774a0ad5
|
|
25
|
+
zh: 3c039339870b8cf9
|
|
26
|
+
/qilin-agent-session-telemetry/use-this-package/what-gets-captured:
|
|
27
|
+
en: 5b7fbbe1c55997e8
|
|
28
|
+
zh: d62d9118fd8df2f4
|
|
29
|
+
/qilin-agent-session-telemetry/use-this-package/the-sharing-disclosure:
|
|
30
|
+
en: 5c325a8aaadf17c7
|
|
31
|
+
zh: 5c493d2333f82b28
|
|
32
|
+
/qilin-agent-session-telemetry/use-this-package/redacting-records:
|
|
33
|
+
en: 22ed345bf7746611
|
|
34
|
+
zh: a57d2eaa878c8cce
|
|
35
|
+
/qilin-agent-session-telemetry/understand-the-implementation:
|
|
36
|
+
en: cad1e9d195916553
|
|
37
|
+
zh: 2192f3ce5e2196bd
|
|
38
|
+
/qilin-agent-session-telemetry/understand-the-implementation/design-concept:
|
|
39
|
+
en: 5527f8d2222119fb
|
|
40
|
+
zh: 3173640a2f0a0676
|
|
41
|
+
/qilin-agent-session-telemetry/understand-the-implementation/source-map:
|
|
42
|
+
en: 5dc345ec3dda38b8
|
|
43
|
+
zh: 189d673734a2c20e
|
|
44
|
+
/qilin-agent-session-telemetry/understand-the-implementation/capture-flow:
|
|
45
|
+
en: f86c2777668ae89e
|
|
46
|
+
zh: 710c28c18f3f4402
|
|
47
|
+
/qilin-agent-session-telemetry/understand-the-implementation/the-handoff-cursor:
|
|
48
|
+
en: c213358e20f27173
|
|
49
|
+
zh: 84a5501335891f0b
|
|
50
|
+
/qilin-agent-session-telemetry/further-exploration:
|
|
51
|
+
en: 57e55389f43b5b28
|
|
52
|
+
zh: 1f7187573723e735
|
|
53
|
+
/qilin-agent-session-telemetry/model-experience:
|
|
54
|
+
en: 6a6b429828997572
|
|
55
|
+
zh: bde61df9dab8f840
|
|
56
|
+
/qilin-agent-session-telemetry/model-experience/kv-cache-effect:
|
|
57
|
+
en: 7593cf5d0d2cedd6
|
|
58
|
+
zh: f57cc48a1b5cbfa9
|
|
59
|
+
/qilin-agent-session-telemetry/known-limitations-and-deferred-work:
|
|
60
|
+
en: dc950b1f59a057ec
|
|
61
|
+
zh: 6f353d426b03abb1
|
|
62
|
+
/qilin-agent-session-telemetry/known-limitations-and-deferred-work/dev-note:
|
|
63
|
+
en: e67f633dc6fe7773
|
|
64
|
+
zh: 3bb0c20c93f16f98
|
package/README.md
CHANGED
|
@@ -1,3 +1,129 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
description: "Session-telemetry capture seam for deployments and backend authors choosing a reporting backend, mounting redaction rules, or implementing the backend contract."
|
|
3
|
+
kind: "package-library"
|
|
4
|
+
---
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
# @qilin-agent/session-telemetry
|
|
7
|
+
|
|
8
|
+
English | [中文](README.zh.md)
|
|
9
|
+
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
Session telemetry lets deployments send ordered copies of session activity for reporting while preserving the canonical session log. Deployments choose one reporting backend and can redact each outbound copy before delivery; without redaction rules, captured data leaves the process unchanged. The handoff is non-blocking, so reporting does not delay session processing. Delivery is best effort, and queued records may be lost if the process crashes.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Further Exploration](#further-exploration)
|
|
19
|
+
- [Model Experience](#model-experience)
|
|
20
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
21
|
+
- [Dev Note](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## Use this package
|
|
27
|
+
|
|
28
|
+
As a deployment, choose a backend, mount it, and add redaction rules when records must not leave the process as captured. As a backend author, implement the three-member contract and compose the coordinator with a capture mode.
|
|
29
|
+
|
|
30
|
+
### Choosing and mounting a backend
|
|
31
|
+
|
|
32
|
+
Load exactly one backend plugin; it registers `ctx.sessionTelemetry` with the capture coordinator and its delivery pipeline. A duplicate load throws. The required [`sharing` member](#the-sharing-disclosure) reports the deployment mode, not per-session admission or delivery. A consumer may report "not configured" only when no telemetry service is mounted. The `/feedback` command confirms recording without reading this policy.
|
|
33
|
+
|
|
34
|
+
### The backend contract
|
|
35
|
+
|
|
36
|
+
A backend implements three members: `emit(record)` must be a non-blocking enqueue because it runs synchronously on the session-event path; optional `flush()` is a fire-and-forget hint after a turn ends, which most backends omit in favor of their SDK's own batching schedule; `shutdown()` drains queued records and resolves when the SDK stops, and disposal awaits it. A backend that implements `flush()` must order concurrent flushes with the final `shutdown()` drain.
|
|
37
|
+
|
|
38
|
+
### What gets captured
|
|
39
|
+
|
|
40
|
+
Capture runs in one of two modes. `live` capture follows session events as they are appended, replays already-live sessions at mount time, and records lifecycle markers; `on-demand` capture reads the canonical session log only when the backend requests a prefix through `captureSession(session, throughSeq?)`. Coordinator options select whether stored history is included. Every canonical session event maps to one ledger record in order. An `assistant/message` or `assistant/attempt` record carries its complete embedded compact stream, including failed and retried output. Each ledger record also carries `session.id`, `session.format_version`, the numeric event identity, optional header facts, and a pre-mapped severity (`error` for `tool/result.isError`, `turn/end` error reasons, and `agent-error`; `info` otherwise).
|
|
41
|
+
|
|
42
|
+
Ledger records carry `sourceEvent` with the owning Session id and a copied event envelope excluding `data`. The redaction waterfall receives `body` as the only payload copy. Backends can reconstruct a complete exported event from the envelope and redacted body without bypassing redaction.
|
|
43
|
+
|
|
44
|
+
### The sharing disclosure
|
|
45
|
+
|
|
46
|
+
<a id="the-sharing-disclosure"></a>
|
|
47
|
+
|
|
48
|
+
Every backend discloses its deployment mode through `sharing`: `full`, `feedback-only`, or `disabled`. A backend may additionally restrict eligible Sessions. This property is not a delivery receipt; handoff is a non-blocking enqueue, and batching, retry, and loss policy belong to the backend SDK.
|
|
49
|
+
|
|
50
|
+
### Redacting records
|
|
51
|
+
|
|
52
|
+
<a id="the-redact-waterfall"></a>
|
|
53
|
+
|
|
54
|
+
Every outbound record passes the `sessionTelemetry/record` waterfall after the coordinator copies its canonical event. This package ships no rules: with no listener mounted, records reach the backend exactly as captured, so exported data is as clean as the rules a deployment mounts. Listeners stack by transforming `next()`'s return value; a throwing listener withholds that one record fail-closed. Redaction applies to the outbound copy only — the canonical session log is never rewritten.
|
|
55
|
+
|
|
56
|
+
-----
|
|
57
|
+
|
|
58
|
+
<a id="understand-the-implementation"></a>
|
|
59
|
+
## Understand the implementation
|
|
60
|
+
|
|
61
|
+
<details>
|
|
62
|
+
<summary>Implementation internals — click to expand</summary>
|
|
63
|
+
|
|
64
|
+
This section explains the capture design; the observable behavior is fully covered in [Use this package](#use-this-package).
|
|
65
|
+
|
|
66
|
+
### Design concept
|
|
67
|
+
|
|
68
|
+
The capture package owns complete event capture, redaction, and handoff cursors. Redaction rules must preserve `sourceEvent` for OTel upload; returning a fresh record without it withholds the event with a diagnostic. Its cloned envelope excludes `data`, which is carried only in `body`. The OTel backend owns byte/count scheduling and uses SDK transport/retries. The [revival Agent Note](../../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md) owns capture and redaction rationale.
|
|
69
|
+
|
|
70
|
+
### Source map
|
|
71
|
+
|
|
72
|
+
| File | Role |
|
|
73
|
+
|---|---|
|
|
74
|
+
| [`src/index.ts`](src/index.ts) | Service Definition: `SessionTelemetryBackend`/`SessionTelemetrySink` contract, record vocabulary, `session-telemetry/record` waterfall declaration |
|
|
75
|
+
| [`src/coordinator.ts`](src/coordinator.ts) | Capture: live listeners, lifecycle-local on-demand replay, redaction, handoff cursor, containment |
|
|
76
|
+
|
|
77
|
+
### Capture flow
|
|
78
|
+
|
|
79
|
+
Live capture registers Session events, flush hints, shutdown markers, and agent/error observers through the composing fiber’s effects. On-demand capture registers only the disposal effect and reads the requested canonical-log prefix under its history policy. Synchronous handlers contain failures so they cannot affect the agent loop or other listeners.
|
|
80
|
+
|
|
81
|
+
### The handoff cursor
|
|
82
|
+
|
|
83
|
+
A module-scope `WeakMap<Session, seq>` records the highest sequence handed off, not delivered. Re-adopting the same object resumes after that cursor. Capture starts at `firstLifecycleSeq`: a new fork includes its child-owned marker and closers, while a restored Session excludes its stored prefix, including prior child turns; explicit `includeHistory: true` starts an unhanded object at seq 0, including restored or fork history. The backend owns capture authorization. Stored history does not itself authorize capture; the OTel backend waits for new explicit feedback. Receivers deduplicate repeated records by `(session.id, session.format_version, event.seq)`.
|
|
84
|
+
|
|
85
|
+
</details>
|
|
86
|
+
|
|
87
|
+
-----
|
|
88
|
+
|
|
89
|
+
<a id="further-exploration"></a>
|
|
90
|
+
## Further Exploration
|
|
91
|
+
|
|
92
|
+
Read these pages when the seam contract is not enough. They move from the shipped backend to the subsystem reference and the decision evidence.
|
|
93
|
+
|
|
94
|
+
- [OpenTelemetry telemetry backend](../session-telemetry-otel/README.md) — the shipped backend deployments load, with mode and exporter configuration.
|
|
95
|
+
- [Session telemetry subsystem](../../../docs/subsystems/session-telemetry.md) — the capability split and type declarations.
|
|
96
|
+
- [Session telemetry revival decision](../../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md) — rationale, trade-offs, and rejected alternatives.
|
|
97
|
+
- [Session package map](../README.md) — adjacent persistence, projection, title, and telemetry packages.
|
|
98
|
+
|
|
99
|
+
-----
|
|
100
|
+
|
|
101
|
+
<a id="model-experience"></a>
|
|
102
|
+
## Model Experience
|
|
103
|
+
|
|
104
|
+
None, as the seam observes the session stream and hands redacted copies outward; it registers nothing model-facing.
|
|
105
|
+
|
|
106
|
+
#### KV Cache effect
|
|
107
|
+
|
|
108
|
+
None; the package neither assembles nor sends a provider request.
|
|
109
|
+
|
|
110
|
+
## Known Limitations and Deferred Work
|
|
111
|
+
|
|
112
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
These limits define the delivery and data-protection guarantees a deployment gets. They are current package constraints.
|
|
116
|
+
|
|
117
|
+
- **Best-effort delivery** — the cursor marks handed-off, not delivered; a session torn down inside a reload window cannot be re-adopted, and whatever sits in a backend queue at crash time is lost. A durable outbox (spool, per-sink cursors, at-least-once) is deferred until a deployment states a crash-loss requirement.
|
|
118
|
+
- **No built-in redaction rules** — with no `sessionTelemetry/record` listener mounted, records leave the process exactly as captured, including any credentials embedded in file contents or command output; a deployment exporting to a shared collector owns its rule set.
|
|
119
|
+
- **On-demand redaction uses current state** — uncaptured events exist only in the canonical session log; a later `captureSession()` deep-copies and redacts their current values with the policy mounted at that time, and there is no capture-time telemetry snapshot or durable pre-capture spool.
|
|
120
|
+
|
|
121
|
+
<a id="dev-note"></a>
|
|
122
|
+
### Dev Note
|
|
123
|
+
|
|
124
|
+
<details>
|
|
125
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
126
|
+
|
|
127
|
+
None.
|
|
128
|
+
|
|
129
|
+
</details>
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "面向部署方与后端作者的会话遥测捕获 seam 说明,用于选择上报后端、挂载脱敏规则或实现后端约定。"
|
|
3
|
+
kind: "package-library"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @qilin-agent/session-telemetry
|
|
7
|
+
|
|
8
|
+
[English](README.md) | 中文
|
|
9
|
+
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
会话遥测让部署方发送会话活动的有序副本用于上报,同时保留权威会话日志。部署方选择一个上报后端,并可在投递前脱敏每个外发副本;如果没有脱敏规则,捕获的数据将原样离开进程。交接以非阻塞方式完成,因此上报不会延迟会话处理。投递采用尽力而为方式;如果进程崩溃,队列中的记录可能丢失。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
15
|
+
|
|
16
|
+
- [使用本包](#use-this-package)
|
|
17
|
+
- [理解实现](#understand-the-implementation)
|
|
18
|
+
- [进一步探索](#further-exploration)
|
|
19
|
+
- [模型体验](#model-experience)
|
|
20
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
21
|
+
- [开发备注](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## 使用本包
|
|
27
|
+
|
|
28
|
+
作为部署方,选择一个后端并挂载它,当记录不能以捕获原样离开进程时添加脱敏规则。作为后端作者,实现三成员约定,并以一种捕获模式组装协调器。
|
|
29
|
+
|
|
30
|
+
### 选择并挂载后端
|
|
31
|
+
|
|
32
|
+
只加载一个后端插件;它把捕获协调器与投递流水线注册为 `ctx.sessionTelemetry`。重复加载会抛出异常。必需的 [`sharing` 成员](#the-sharing-disclosure) 报告部署模式,不代表会话准入或投递。只有在未挂载任何遥测服务时,消费方才可报告「未配置」。`/feedback` 命令确认记录,不读取此策略。
|
|
33
|
+
|
|
34
|
+
### 后端约定
|
|
35
|
+
|
|
36
|
+
后端实现三个成员:`emit(record)` 必须是非阻塞入队,因为它会在会话事件路径上同步执行;可选的 `flush()` 是轮次结束后的即发即忘提示,多数后端为了遵循 SDK 自身的批处理计划而省略它;`shutdown()` 排空已入队记录,并在 SDK 停止后结束,dispose(资源释放)会等待它。实现 `flush()` 的后端必须安排并发 flush 与最终 `shutdown()` 排空的先后顺序。
|
|
37
|
+
|
|
38
|
+
### 捕获内容
|
|
39
|
+
|
|
40
|
+
捕获以两种模式之一运行。`live` 捕获在追加时跟随会话事件、在挂载时回放已存活会话并记录生命周期标记;`on-demand` 捕获只在后端通过 `captureSession(session, throughSeq?)` 请求前缀时读取权威会话日志。协调器选项决定是否包含存储历史。每条权威会话事件都按顺序映射为一条 ledger 记录。`assistant/message` 或 `assistant/attempt` 记录会携带完整的嵌入式紧凑流,包括失败和重试输出。每条 ledger 记录还携带 `session.id`、`session.format_version`、数值型事件标识、可选 header 事实与预先映射的严重级别(`tool/result.isError`、`turn/end` 的错误原因与 `agent-error` 映射为 `error`;其余为 `info`)。
|
|
41
|
+
|
|
42
|
+
ledger 记录携带 `sourceEvent`,其中包含所属 Session id 和不含 `data` 的事件信封副本。脱敏 waterfall 接收的 `body` 是唯一的载荷副本。后端可从信封和脱敏后的 body 重建完整导出事件,而不绕过脱敏。
|
|
43
|
+
|
|
44
|
+
### 共享披露
|
|
45
|
+
|
|
46
|
+
<a id="the-sharing-disclosure"></a>
|
|
47
|
+
|
|
48
|
+
每个后端通过 `sharing` 披露部署模式:`full`、`feedback-only` 或 `disabled`。后端还可限制符合条件的会话。该属性不是投递回执;交接是非阻塞入队,批处理、重试与丢失策略属于后端 SDK。
|
|
49
|
+
|
|
50
|
+
### 脱敏记录
|
|
51
|
+
|
|
52
|
+
<a id="the-redact-waterfall"></a>
|
|
53
|
+
|
|
54
|
+
协调器复制权威事件后,每条外发记录都会立即经过 `sessionTelemetry/record` waterfall(瀑布式事件)。本包不带任何规则:未挂载监听器时,记录以捕获时的原样到达后端,因此导出数据能干净到什么程度,恰恰取决于部署方挂载了什么规则。监听器通过变换 `next()` 的返回值来堆叠;抛出异常的监听器以 fail-closed 方式拦下这一条记录。脱敏只作用于外发副本——权威会话日志永不改写。
|
|
55
|
+
|
|
56
|
+
-----
|
|
57
|
+
|
|
58
|
+
<a id="understand-the-implementation"></a>
|
|
59
|
+
## 理解实现
|
|
60
|
+
|
|
61
|
+
<details>
|
|
62
|
+
<summary>实现细节——点击展开</summary>
|
|
63
|
+
|
|
64
|
+
本节解释捕获设计;可观察行为已在[使用本包](#use-this-package)中完整说明。
|
|
65
|
+
|
|
66
|
+
### 设计理念
|
|
67
|
+
|
|
68
|
+
捕获包负责完整事件捕获、脱敏和交接游标。脱敏规则必须保留 `sourceEvent` 才能通过 OTel 上传;返回不含它的新记录会阻止该事件上传并产生诊断。复制的 envelope 不含 `data`,数据仅由 `body` 携带。OTel 后端负责字节/条数调度并使用 SDK 传输和重试。[恢复遥测决策记录](../../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md) 说明捕获和脱敏依据。
|
|
69
|
+
|
|
70
|
+
### 源码地图
|
|
71
|
+
|
|
72
|
+
| 文件 | 职责 |
|
|
73
|
+
|---|---|
|
|
74
|
+
| [`src/index.ts`](src/index.ts) | Service Definition:`SessionTelemetryBackend`/`SessionTelemetrySink` 约定、记录词汇、`session-telemetry/record` waterfall 声明 |
|
|
75
|
+
| [`src/coordinator.ts`](src/coordinator.ts) | 捕获:live 监听器、生命周期本地 on-demand 回放、脱敏、handoff 游标、异常隔离 |
|
|
76
|
+
|
|
77
|
+
### 捕获流程
|
|
78
|
+
|
|
79
|
+
实时捕获通过组合 fiber 的 effect 注册 Session 事件、刷新提示、关闭标记与 agent/error 观察器。按需捕获只注册释放 effect,并按历史策略读取请求的权威日志前缀。同步处理器隔离失败,避免影响 agent loop(智能体循环)或其他监听器。
|
|
80
|
+
|
|
81
|
+
### handoff 游标
|
|
82
|
+
|
|
83
|
+
模块级 `WeakMap<Session, seq>` 记录已经移交的最高序号,而不是已送达的最高序号。再次接管同一对象时从该游标之后继续。采集从 `firstLifecycleSeq` 开始:新 fork 包含子会话自有的标记与 fork 结束事件;恢复的 Session 排除已存储前缀,包括先前的子会话轮次。显式 `includeHistory: true` 会让尚未移交的对象从 seq 0 开始,包括恢复或 fork 历史。Backend 负责采集授权;已有存储历史本身不构成授权,OTel backend 等待新的显式反馈。接收方通过 `(session.id, session.format_version, event.seq)` 对重复记录去重。
|
|
84
|
+
|
|
85
|
+
</details>
|
|
86
|
+
|
|
87
|
+
-----
|
|
88
|
+
|
|
89
|
+
<a id="further-exploration"></a>
|
|
90
|
+
## 进一步探索
|
|
91
|
+
|
|
92
|
+
当 seam 约定不够用时阅读以下页面。它们从随附后端逐步进入子系统参考与决策证据。
|
|
93
|
+
|
|
94
|
+
- [OpenTelemetry 遥测后端](../session-telemetry-otel/README.zh.md)——部署方加载的随附后端,含模式与导出器配置。
|
|
95
|
+
- [会话遥测子系统](../../../docs/subsystems/session-telemetry.zh.md)——能力拆分与类型声明。
|
|
96
|
+
- [会话遥测复活决策](../../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md)——理由、权衡与被否决的替代方案。
|
|
97
|
+
- [会话包映射](../README.zh.md)——相邻的持久化、投影、标题与遥测包。
|
|
98
|
+
|
|
99
|
+
-----
|
|
100
|
+
|
|
101
|
+
<a id="model-experience"></a>
|
|
102
|
+
## 模型体验
|
|
103
|
+
|
|
104
|
+
无,因为该 seam 观察会话流并把脱敏后的副本交给外部;它不注册任何面向模型的内容。
|
|
105
|
+
|
|
106
|
+
#### KV Cache 影响
|
|
107
|
+
|
|
108
|
+
无;本包既不组装也不发送提供方请求。
|
|
109
|
+
|
|
110
|
+
## 已知限制与延期工作
|
|
111
|
+
|
|
112
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
这些限制定义部署方能得到的投递与数据保护保证。它们是当前包约束。
|
|
116
|
+
|
|
117
|
+
- **尽力而为的投递**——游标标记的是已交接而非已投递;在重载窗口内被拆除的会话无法重新收养,崩溃时留在后端队列中的内容会丢失。持久化 outbox(spool、每 sink 游标、at-least-once)推迟到有部署方提出明确的崩溃丢失要求时再实现。
|
|
118
|
+
- **不内置脱敏规则**——未挂载 `sessionTelemetry/record` 监听器时,记录以捕获时的原样离开进程,包括文件内容或命令输出中内嵌的任何凭据;向共享 collector 导出的部署方自行负责其规则集。
|
|
119
|
+
- **按需脱敏使用当前状态**——未捕获的事件只存在于权威会话日志中;后续的 `captureSession()` 会使用当时挂载的策略,深拷贝并脱敏其当前值,且不存在捕获时的遥测快照或持久化的捕获前 spool。
|
|
120
|
+
|
|
121
|
+
<a id="dev-note"></a>
|
|
122
|
+
### 开发备注
|
|
123
|
+
|
|
124
|
+
<details>
|
|
125
|
+
<summary>维护者的工作上下文——点击展开</summary>
|
|
126
|
+
|
|
127
|
+
无。
|
|
128
|
+
|
|
129
|
+
</details>
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { Service } from "@qilin-agent/kylin";
|
|
2
|
+
import { SessionLogOffset, SessionSeq } from "@qilin-agent/session";
|
|
3
|
+
//#region lib/types/coordinator.js
|
|
4
|
+
/**
|
|
5
|
+
* Capture coordinator for the telemetry capability. Live capture subscribes to
|
|
6
|
+
* the session firehose plus the one live-bus relay (`agent/error`). Both
|
|
7
|
+
* capture paths build one logical record per canonical Session event and run
|
|
8
|
+
* each through the
|
|
9
|
+
* `session-telemetry/record` waterfall (deployment-mounted redaction rules;
|
|
10
|
+
* pass-through when none), then hands the result to the backend. Live capture
|
|
11
|
+
* follows the session firehose; on-demand capture replays the canonical log
|
|
12
|
+
* only when requested. Every synchronous handler is self-contained so a
|
|
13
|
+
* failing backend can never starve other subscribers (cordis `emit` is
|
|
14
|
+
* stop-on-throw) or touch the agent loop. Composed by a backend in its
|
|
15
|
+
* constructor.
|
|
16
|
+
*
|
|
17
|
+
* @module @qilin-agent/session-telemetry/coordinator
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* The handoff cursor: per session, the highest `seq` handed to a backend.
|
|
21
|
+
* Deliberately MODULE-scope ambient state — a narrow, documented exception
|
|
22
|
+
* to the registrations-are-effects discipline: cordis has no HMR
|
|
23
|
+
* state-handover API, and keying by the `Session` object (which belongs to
|
|
24
|
+
* the session store and outlives any telemetry fiber) is the only in-process
|
|
25
|
+
* lifetime that lets a re-adopting fiber resume instead of re-handing
|
|
26
|
+
* history. Entries die with their sessions; a missing entry safely means
|
|
27
|
+
* "re-hand everything". Advanced only at emit time — the cursor marks
|
|
28
|
+
* handed-off, not delivered.
|
|
29
|
+
*/
|
|
30
|
+
const handoffCursor = /* @__PURE__ */ new WeakMap();
|
|
31
|
+
/**
|
|
32
|
+
* Install the telemetry capture side onto a context for one backend.
|
|
33
|
+
*
|
|
34
|
+
* Live capture registers its own `session/created` / `session/event` /
|
|
35
|
+
* `session/disposed` listener set plus the `agent/error` relay, all through
|
|
36
|
+
* `ctx.effect()`/`ctx.on()` on the composing fiber, and sweeps already-live
|
|
37
|
+
* sessions (a hot reload does not replay `session/created`). A `session/disposed` captures the session's `shutdown`
|
|
38
|
+
* operational record at its own termination edge and retires it from the
|
|
39
|
+
* adopted set. On-demand capture registers none of those continuous listeners;
|
|
40
|
+
* {@link captureSession} reads the canonical log explicitly and never creates
|
|
41
|
+
* operational records. Disposal captures shutdown markers for live-adopted
|
|
42
|
+
* sessions, then awaits the backend's `shutdown()`; a failure there warns
|
|
43
|
+
* instead of throwing — best-effort reporting must not fail application
|
|
44
|
+
* teardown.
|
|
45
|
+
*/
|
|
46
|
+
var SessionTelemetryCoordinator = class {
|
|
47
|
+
ctx;
|
|
48
|
+
backend;
|
|
49
|
+
options;
|
|
50
|
+
/**
|
|
51
|
+
* Sessions adopted by THIS fiber and still live, for double-adoption
|
|
52
|
+
* protection and the teardown sweep of unmarked sessions;
|
|
53
|
+
* `session/disposed` marks and retires entries.
|
|
54
|
+
*/
|
|
55
|
+
adopted = /* @__PURE__ */ new Set();
|
|
56
|
+
/**
|
|
57
|
+
* @param ctx - the composing backend's context; listeners bind to its fiber.
|
|
58
|
+
* @param backend - the backend receiving records; owned elsewhere, never disposed here beyond `shutdown()` forwarding.
|
|
59
|
+
* @param options - capture mode and history policy.
|
|
60
|
+
*/
|
|
61
|
+
constructor(ctx, backend, options = {}) {
|
|
62
|
+
this.ctx = ctx;
|
|
63
|
+
this.backend = backend;
|
|
64
|
+
this.options = options;
|
|
65
|
+
if ((options.capture ?? "live") === "live") {
|
|
66
|
+
ctx.on("session/created", (session) => {
|
|
67
|
+
this.adopt(session);
|
|
68
|
+
});
|
|
69
|
+
ctx.on("session/disposed", (session) => {
|
|
70
|
+
this.contain(() => {
|
|
71
|
+
if (!this.adopted.delete(session)) return;
|
|
72
|
+
this.deliver(session, { record: this.redact(shutdownRecord(session)) });
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
ctx.on("session/event", (session, event) => {
|
|
76
|
+
this.contain(() => {
|
|
77
|
+
this.captureEvent(session, event);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
ctx.on("session/flush", (session) => {
|
|
81
|
+
this.contain(() => {
|
|
82
|
+
this.hintFlush(session);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
ctx.on("agent/error", ({ agent, turn, step, error }) => {
|
|
86
|
+
this.contain(() => {
|
|
87
|
+
this.relayAgentError(agent, turn, step, error);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
for (const session of ctx.sessions.list()) this.adopt(session);
|
|
91
|
+
}
|
|
92
|
+
ctx.effect(() => async () => {
|
|
93
|
+
for (const session of this.adopted) this.contain(() => {
|
|
94
|
+
this.deliver(session, { record: this.redact(shutdownRecord(session)) });
|
|
95
|
+
});
|
|
96
|
+
try {
|
|
97
|
+
await this.backend.shutdown();
|
|
98
|
+
} catch (error) {
|
|
99
|
+
this.ctx.logger.warn(`telemetry: backend shutdown failed: ${String(error)}`);
|
|
100
|
+
}
|
|
101
|
+
}, "telemetry capture");
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Copy, redact, and hand over the canonical session-log suffix after the handoff
|
|
105
|
+
* cursor, optionally stopping at an inclusive sequence boundary. Redaction
|
|
106
|
+
* runs during this call, so an on-demand caller retains no copied records
|
|
107
|
+
* before requesting capture and uses the policy mounted at that time.
|
|
108
|
+
* Backend and policy failures remain contained per event and do not starve
|
|
109
|
+
* later events in the same replay.
|
|
110
|
+
* @param session - session whose current canonical-log prefix may be handed over.
|
|
111
|
+
* @param throughSeq - optional last sequence included in this capture.
|
|
112
|
+
*/
|
|
113
|
+
captureSession(session, throughSeq) {
|
|
114
|
+
const start = session.firstLifecycleSeq;
|
|
115
|
+
const cursor = handoffCursor.get(session) ?? (this.options.includeHistory === true || start === 0 ? -1 : SessionSeq(start - 1));
|
|
116
|
+
for (const event of session.snapshotEvents(SessionLogOffset(cursor + 1))) {
|
|
117
|
+
if (throughSeq !== void 0 && event.seq > throughSeq) break;
|
|
118
|
+
this.contain(() => {
|
|
119
|
+
this.captureEvent(session, event);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Adopt a session and replay after its handoff cursor, then follow live events.
|
|
125
|
+
* New fork objects include child-owned seed markers and closers. Restored
|
|
126
|
+
* objects start after the stored prefix, including restored forks. includeHistory starts either object at seq 0;
|
|
127
|
+
* re-adopting the same object resumes after its cursor.
|
|
128
|
+
* @param session - the live session to adopt; a second adoption is a no-op.
|
|
129
|
+
*/
|
|
130
|
+
adopt(session) {
|
|
131
|
+
if (this.adopted.has(session)) return;
|
|
132
|
+
this.adopted.add(session);
|
|
133
|
+
this.captureSession(session);
|
|
134
|
+
}
|
|
135
|
+
/** Copy, redact, and hand one canonical event to the backend. */
|
|
136
|
+
captureEvent(session, event) {
|
|
137
|
+
const { data, ...envelope } = event;
|
|
138
|
+
this.deliver(session, {
|
|
139
|
+
record: this.redact({
|
|
140
|
+
sourceEvent: {
|
|
141
|
+
sessionId: session.id,
|
|
142
|
+
envelope: structuredClone(envelope)
|
|
143
|
+
},
|
|
144
|
+
channel: "ledger",
|
|
145
|
+
time: event.time,
|
|
146
|
+
severity: severityOf(event),
|
|
147
|
+
attributes: identityOf(session, event),
|
|
148
|
+
body: structuredClone(data)
|
|
149
|
+
}),
|
|
150
|
+
seq: event.seq
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Run the `session-telemetry/record` waterfall at capture time. The innermost `next`
|
|
155
|
+
* passes the record through unchanged — this package ships no rules; exported
|
|
156
|
+
* data is as clean as the listeners a deployment mounts. Callers run inside
|
|
157
|
+
* {@link contain}, so a throwing rule withholds the record instead of
|
|
158
|
+
* reaching the loop (fail-closed). On-demand capture invokes this waterfall
|
|
159
|
+
* while reading the canonical session log, not when the event was appended.
|
|
160
|
+
*/
|
|
161
|
+
redact(record) {
|
|
162
|
+
return this.ctx.waterfall("session-telemetry/record", record, () => record);
|
|
163
|
+
}
|
|
164
|
+
/** Hand one redacted record to the backend, then advance its ledger cursor. */
|
|
165
|
+
deliver(session, pending) {
|
|
166
|
+
this.backend.emit(pending.record);
|
|
167
|
+
if (pending.seq !== void 0) handoffCursor.set(session, pending.seq);
|
|
168
|
+
}
|
|
169
|
+
/** Forward the turn-end boundary to the backend's optional flush hint. */
|
|
170
|
+
hintFlush(session) {
|
|
171
|
+
if (this.adopted.has(session)) this.backend.flush?.();
|
|
172
|
+
}
|
|
173
|
+
/** Relay one `agent/error` bus emission as an `agent-error` operational record. */
|
|
174
|
+
relayAgentError(agent, turn, step, error) {
|
|
175
|
+
const detail = errorDetail(error);
|
|
176
|
+
this.deliver(agent.session, { record: this.redact({
|
|
177
|
+
channel: "ops",
|
|
178
|
+
time: Date.now(),
|
|
179
|
+
severity: "error",
|
|
180
|
+
attributes: {
|
|
181
|
+
"telemetry.op": "agent-error",
|
|
182
|
+
"session.id": String(agent.session.id),
|
|
183
|
+
"agent.id": agent.id,
|
|
184
|
+
"error.name": detail.name,
|
|
185
|
+
turn,
|
|
186
|
+
step
|
|
187
|
+
},
|
|
188
|
+
body: detail
|
|
189
|
+
}) });
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Run one capture-side step with its exception contained: cordis `emit`
|
|
193
|
+
* is stop-on-throw, so a throwing listener would starve every subscriber
|
|
194
|
+
* registered after this plugin — nothing from the backend may escape.
|
|
195
|
+
*/
|
|
196
|
+
contain(step) {
|
|
197
|
+
try {
|
|
198
|
+
step();
|
|
199
|
+
} catch (error) {
|
|
200
|
+
this.ctx.logger.warn(`telemetry: capture step failed: ${String(error)}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
/**
|
|
205
|
+
* Build the per-session clean-exit marker: emitted at the session's own
|
|
206
|
+
* disposal edge, or at coordinator dispose for sessions still alive then.
|
|
207
|
+
*/
|
|
208
|
+
function shutdownRecord(session) {
|
|
209
|
+
return {
|
|
210
|
+
channel: "ops",
|
|
211
|
+
time: Date.now(),
|
|
212
|
+
severity: "info",
|
|
213
|
+
attributes: {
|
|
214
|
+
"telemetry.op": "shutdown",
|
|
215
|
+
"session.id": String(session.id)
|
|
216
|
+
},
|
|
217
|
+
body: { op: "shutdown" }
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
/** Map an event's own outcome flag to the pre-baked alerting severity. */
|
|
221
|
+
function severityOf(event) {
|
|
222
|
+
switch (event.type) {
|
|
223
|
+
case "tool/result": return event.data.message.isError === true ? "error" : "info";
|
|
224
|
+
case "turn/end": return event.data.reason.kind === "error" ? "error" : "info";
|
|
225
|
+
default: return "info";
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/** Normalize the live bus's arbitrary thrown value into the stable operational-record shape. */
|
|
229
|
+
function errorDetail(error) {
|
|
230
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
231
|
+
return {
|
|
232
|
+
name: normalized.name,
|
|
233
|
+
message: normalized.message
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/** Build the minimal identity attributes: envelope plus self-contained header facts. */
|
|
237
|
+
function identityOf(session, event) {
|
|
238
|
+
const attributes = {
|
|
239
|
+
"session.id": String(session.id),
|
|
240
|
+
"session.format_version": session.header.version,
|
|
241
|
+
"event.type": event.type,
|
|
242
|
+
"event.seq": event.seq
|
|
243
|
+
};
|
|
244
|
+
const { cwd, parentSession, isSeeded } = session.header;
|
|
245
|
+
if (cwd !== void 0) attributes["session.cwd"] = cwd;
|
|
246
|
+
if (parentSession !== void 0) attributes["session.parent_id"] = String(parentSession);
|
|
247
|
+
if (isSeeded) attributes["session.seed_length"] = session.inheritedEventCount;
|
|
248
|
+
return attributes;
|
|
249
|
+
}
|
|
250
|
+
//#endregion
|
|
251
|
+
//#region lib/types/index.js
|
|
252
|
+
/**
|
|
253
|
+
* SessionTelemetryBackend Service Definition for the QiLin.
|
|
254
|
+
*
|
|
255
|
+
* This package owns the CAPTURE side of session-event reporting — the complete
|
|
256
|
+
* one-record-per-event ledger mirror, what records carry, when
|
|
257
|
+
* they are captured (adoption, the per-append firehose, lifecycle
|
|
258
|
+
* forwarding), live versus on-demand canonical-log capture, and the HMR
|
|
259
|
+
* cursor. Everything downstream of
|
|
260
|
+
* {@link SessionTelemetryBackend.emit} — batching, retry, queueing, and loss policy — is the
|
|
261
|
+
* reporting SDK's territory and is deliberately not modelled here. The
|
|
262
|
+
* design and its trade-offs are pinned in
|
|
263
|
+
* .agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md.
|
|
264
|
+
*
|
|
265
|
+
* @module @qilin-agent/session-telemetry
|
|
266
|
+
*/
|
|
267
|
+
/**
|
|
268
|
+
* Loadable form of the backend contract: one implementation per context —
|
|
269
|
+
* the cordis `Service` registration under the `telemetry` key throws on a
|
|
270
|
+
* duplicate, cordis' standard behavior. A backend composes a
|
|
271
|
+
* {@link SessionTelemetryCoordinator} in its constructor to install the capture side.
|
|
272
|
+
*/
|
|
273
|
+
var SessionTelemetryBackend = class extends Service {
|
|
274
|
+
constructor(ctx) {
|
|
275
|
+
super(ctx, "sessionTelemetry");
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
//#endregion
|
|
279
|
+
export { SessionTelemetryBackend, SessionTelemetryCoordinator };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture coordinator for the telemetry capability. Live capture subscribes to
|
|
3
|
+
* the session firehose plus the one live-bus relay (`agent/error`). Both
|
|
4
|
+
* capture paths build one logical record per canonical Session event and run
|
|
5
|
+
* each through the
|
|
6
|
+
* `session-telemetry/record` waterfall (deployment-mounted redaction rules;
|
|
7
|
+
* pass-through when none), then hands the result to the backend. Live capture
|
|
8
|
+
* follows the session firehose; on-demand capture replays the canonical log
|
|
9
|
+
* only when requested. Every synchronous handler is self-contained so a
|
|
10
|
+
* failing backend can never starve other subscribers (cordis `emit` is
|
|
11
|
+
* stop-on-throw) or touch the agent loop. Composed by a backend in its
|
|
12
|
+
* constructor.
|
|
13
|
+
*
|
|
14
|
+
* @module @qilin-agent/session-telemetry/coordinator
|
|
15
|
+
*/
|
|
16
|
+
import type { Context } from '@qilin-agent/kylin';
|
|
17
|
+
import { type Session, type SessionSeq as SessionSeqType } from '@qilin-agent/session';
|
|
18
|
+
import type { SessionTelemetrySink } from './index.ts';
|
|
19
|
+
/** Whether capture follows live events or reads the canonical log only when requested. */
|
|
20
|
+
export type SessionTelemetryCapture = 'live' | 'on-demand';
|
|
21
|
+
/** Backend-selected capture mode and history policy. */
|
|
22
|
+
export interface SessionTelemetryCaptureOptions {
|
|
23
|
+
/** Follow live events, or wait for explicit capture; defaults to live. */
|
|
24
|
+
capture?: SessionTelemetryCapture;
|
|
25
|
+
/** Include inherited fork history and stored history from earlier lifecycles; defaults to false. */
|
|
26
|
+
includeHistory?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Install the telemetry capture side onto a context for one backend.
|
|
30
|
+
*
|
|
31
|
+
* Live capture registers its own `session/created` / `session/event` /
|
|
32
|
+
* `session/disposed` listener set plus the `agent/error` relay, all through
|
|
33
|
+
* `ctx.effect()`/`ctx.on()` on the composing fiber, and sweeps already-live
|
|
34
|
+
* sessions (a hot reload does not replay `session/created`). A `session/disposed` captures the session's `shutdown`
|
|
35
|
+
* operational record at its own termination edge and retires it from the
|
|
36
|
+
* adopted set. On-demand capture registers none of those continuous listeners;
|
|
37
|
+
* {@link captureSession} reads the canonical log explicitly and never creates
|
|
38
|
+
* operational records. Disposal captures shutdown markers for live-adopted
|
|
39
|
+
* sessions, then awaits the backend's `shutdown()`; a failure there warns
|
|
40
|
+
* instead of throwing — best-effort reporting must not fail application
|
|
41
|
+
* teardown.
|
|
42
|
+
*/
|
|
43
|
+
export declare class SessionTelemetryCoordinator {
|
|
44
|
+
private readonly ctx;
|
|
45
|
+
private readonly backend;
|
|
46
|
+
private readonly options;
|
|
47
|
+
/**
|
|
48
|
+
* Sessions adopted by THIS fiber and still live, for double-adoption
|
|
49
|
+
* protection and the teardown sweep of unmarked sessions;
|
|
50
|
+
* `session/disposed` marks and retires entries.
|
|
51
|
+
*/
|
|
52
|
+
private readonly adopted;
|
|
53
|
+
/**
|
|
54
|
+
* @param ctx - the composing backend's context; listeners bind to its fiber.
|
|
55
|
+
* @param backend - the backend receiving records; owned elsewhere, never disposed here beyond `shutdown()` forwarding.
|
|
56
|
+
* @param options - capture mode and history policy.
|
|
57
|
+
*/
|
|
58
|
+
constructor(ctx: Context, backend: SessionTelemetrySink, options?: SessionTelemetryCaptureOptions);
|
|
59
|
+
/**
|
|
60
|
+
* Copy, redact, and hand over the canonical session-log suffix after the handoff
|
|
61
|
+
* cursor, optionally stopping at an inclusive sequence boundary. Redaction
|
|
62
|
+
* runs during this call, so an on-demand caller retains no copied records
|
|
63
|
+
* before requesting capture and uses the policy mounted at that time.
|
|
64
|
+
* Backend and policy failures remain contained per event and do not starve
|
|
65
|
+
* later events in the same replay.
|
|
66
|
+
* @param session - session whose current canonical-log prefix may be handed over.
|
|
67
|
+
* @param throughSeq - optional last sequence included in this capture.
|
|
68
|
+
*/
|
|
69
|
+
captureSession(session: Session, throughSeq?: SessionSeqType): void;
|
|
70
|
+
/**
|
|
71
|
+
* Adopt a session and replay after its handoff cursor, then follow live events.
|
|
72
|
+
* New fork objects include child-owned seed markers and closers. Restored
|
|
73
|
+
* objects start after the stored prefix, including restored forks. includeHistory starts either object at seq 0;
|
|
74
|
+
* re-adopting the same object resumes after its cursor.
|
|
75
|
+
* @param session - the live session to adopt; a second adoption is a no-op.
|
|
76
|
+
*/
|
|
77
|
+
private adopt;
|
|
78
|
+
/** Copy, redact, and hand one canonical event to the backend. */
|
|
79
|
+
private captureEvent;
|
|
80
|
+
/**
|
|
81
|
+
* Run the `session-telemetry/record` waterfall at capture time. The innermost `next`
|
|
82
|
+
* passes the record through unchanged — this package ships no rules; exported
|
|
83
|
+
* data is as clean as the listeners a deployment mounts. Callers run inside
|
|
84
|
+
* {@link contain}, so a throwing rule withholds the record instead of
|
|
85
|
+
* reaching the loop (fail-closed). On-demand capture invokes this waterfall
|
|
86
|
+
* while reading the canonical session log, not when the event was appended.
|
|
87
|
+
*/
|
|
88
|
+
private redact;
|
|
89
|
+
/** Hand one redacted record to the backend, then advance its ledger cursor. */
|
|
90
|
+
private deliver;
|
|
91
|
+
/** Forward the turn-end boundary to the backend's optional flush hint. */
|
|
92
|
+
private hintFlush;
|
|
93
|
+
/** Relay one `agent/error` bus emission as an `agent-error` operational record. */
|
|
94
|
+
private relayAgentError;
|
|
95
|
+
/**
|
|
96
|
+
* Run one capture-side step with its exception contained: cordis `emit`
|
|
97
|
+
* is stop-on-throw, so a throwing listener would starve every subscriber
|
|
98
|
+
* registered after this plugin — nothing from the backend may escape.
|
|
99
|
+
*/
|
|
100
|
+
private contain;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=coordinator.d.ts.map
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionTelemetryBackend Service Definition for the QiLin.
|
|
3
|
+
*
|
|
4
|
+
* This package owns the CAPTURE side of session-event reporting — the complete
|
|
5
|
+
* one-record-per-event ledger mirror, what records carry, when
|
|
6
|
+
* they are captured (adoption, the per-append firehose, lifecycle
|
|
7
|
+
* forwarding), live versus on-demand canonical-log capture, and the HMR
|
|
8
|
+
* cursor. Everything downstream of
|
|
9
|
+
* {@link SessionTelemetryBackend.emit} — batching, retry, queueing, and loss policy — is the
|
|
10
|
+
* reporting SDK's territory and is deliberately not modelled here. The
|
|
11
|
+
* design and its trade-offs are pinned in
|
|
12
|
+
* .agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md.
|
|
13
|
+
*
|
|
14
|
+
* @module @qilin-agent/session-telemetry
|
|
15
|
+
*/
|
|
16
|
+
import { Context, Service } from '@qilin-agent/kylin';
|
|
17
|
+
import type { SessionEvent, SessionId } from '@qilin-agent/session';
|
|
18
|
+
declare module '@qilin-agent/kylin' {
|
|
19
|
+
interface Context {
|
|
20
|
+
sessionTelemetry: SessionTelemetryBackend;
|
|
21
|
+
}
|
|
22
|
+
interface Events {
|
|
23
|
+
/**
|
|
24
|
+
* Transform one outbound record before it reaches the backend. This
|
|
25
|
+
* waterfall is the Service Definition's redaction extension point. It ships NO rules
|
|
26
|
+
* of its own: the
|
|
27
|
+
* innermost `next()` passes the record through unchanged, and with no
|
|
28
|
+
* listener mounted records reach the backend as captured, so exported
|
|
29
|
+
* data is exactly as clean as the rules a deployment mounts. Listeners
|
|
30
|
+
* stack by transforming `next()`'s return value; returning without
|
|
31
|
+
* `next()` replaces everything beneath. Dispatched synchronously on the
|
|
32
|
+
* capture hot path inside the coordinator's containment: a throwing
|
|
33
|
+
* listener withholds that one record (fail-closed) and never reaches the
|
|
34
|
+
* agent loop. Live capture dispatches at append time; on-demand capture
|
|
35
|
+
* dispatches while reading the canonical log. Redaction applies to the
|
|
36
|
+
* exported copy only; the canonical session log is never rewritten.
|
|
37
|
+
* @param record - the candidate record, already the coordinator's own deep
|
|
38
|
+
* copy; listeners return a (possibly new) record and must not mutate it.
|
|
39
|
+
* @mode waterfall
|
|
40
|
+
*/
|
|
41
|
+
'session-telemetry/record'(record: SessionTelemetryRecord, next: () => SessionTelemetryRecord): SessionTelemetryRecord;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Severity of a telemetry record, pre-mapped at capture so a receiver can
|
|
46
|
+
* alert with zero configuration: `error` for events whose own outcome flag
|
|
47
|
+
* says so (the tool-result block's `isError`, `turn/end` error reasons) and for
|
|
48
|
+
* `agent-error` operational records. Captured events otherwise default to
|
|
49
|
+
* `info`; `warn` remains available to `session-telemetry/record` policies and
|
|
50
|
+
* backends.
|
|
51
|
+
*/
|
|
52
|
+
export type SessionTelemetrySeverity = 'info' | 'warn' | 'error';
|
|
53
|
+
/**
|
|
54
|
+
* One logical record handed to a backend — the capture contract's whole outbound
|
|
55
|
+
* vocabulary. Ledger records mirror session-log events one-to-one;
|
|
56
|
+
* operational records (`channel: 'ops'`) carry the two signals with no log
|
|
57
|
+
* home (`agent-error`, `shutdown`) and deliberately omit `event.seq`-style
|
|
58
|
+
* identity so they can never be mistaken for ledger rows.
|
|
59
|
+
*/
|
|
60
|
+
export interface SessionTelemetryRecord {
|
|
61
|
+
/** Canonical envelope without data; body carries the separately redacted payload. Absent for operational records. */
|
|
62
|
+
sourceEvent?: {
|
|
63
|
+
sessionId: SessionId;
|
|
64
|
+
envelope: Omit<SessionEvent, 'data'>;
|
|
65
|
+
};
|
|
66
|
+
/** Ledger (session-log mirror) or ops (operational signal) channel; backends keep the two under separate instrumentation scopes. */
|
|
67
|
+
channel: 'ledger' | 'ops';
|
|
68
|
+
/** Unix epoch milliseconds — the source event's append time for ledger records, the emission time for ops records. */
|
|
69
|
+
time: number;
|
|
70
|
+
/** Pre-mapped alerting severity; see {@link SessionTelemetrySeverity}. */
|
|
71
|
+
severity: SessionTelemetrySeverity;
|
|
72
|
+
/**
|
|
73
|
+
* Identity attributes, deliberately minimal: ledger records carry
|
|
74
|
+
* `session.id`, `session.format_version`, `event.type`, `event.seq`, plus optional
|
|
75
|
+
* `session.cwd` / `session.parent_id`; a seeded Session also carries
|
|
76
|
+
* `session.seed_length` from its exact inherited event count;
|
|
77
|
+
* ops records carry `telemetry.op`, `session.id`, and (for `agent-error`)
|
|
78
|
+
* `agent.id`, `turn`, `step`, `error.name`. Anything recoverable from the
|
|
79
|
+
* body is intentionally NOT duplicated here.
|
|
80
|
+
*/
|
|
81
|
+
attributes: Record<string, string | number>;
|
|
82
|
+
/**
|
|
83
|
+
* The complete payload: a deep copy of the session event's `data` for
|
|
84
|
+
* ledger records (JSON-serializable by `Session.append`'s own
|
|
85
|
+
* validation), or the op payload for ops records. Never mutated after
|
|
86
|
+
* handoff.
|
|
87
|
+
*/
|
|
88
|
+
body: unknown;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The minimum backend contract the coordinator requires. {@link SessionTelemetryBackend} is
|
|
92
|
+
* its service-registered form; tests compose the coordinator with a bare
|
|
93
|
+
* implementation of this interface.
|
|
94
|
+
*/
|
|
95
|
+
export interface SessionTelemetrySink {
|
|
96
|
+
/**
|
|
97
|
+
* Hand one record to the backend's pipeline. MUST be a non-blocking
|
|
98
|
+
* enqueue — the coordinator calls this synchronously from the
|
|
99
|
+
* `session/event` hot path or an explicit canonical-log capture, so anything
|
|
100
|
+
* slower than a queue push would tax the agent loop or feedback handling.
|
|
101
|
+
* Errors thrown here are contained by the coordinator and logged; they
|
|
102
|
+
* never reach the loop.
|
|
103
|
+
* @param record - the logical record to report; owned by the backend after the call.
|
|
104
|
+
*/
|
|
105
|
+
emit(record: SessionTelemetryRecord): void;
|
|
106
|
+
/**
|
|
107
|
+
* Optional hint that a turn ended. A backend may forward it to its SDK's
|
|
108
|
+
* flush so records are exported after each turn. Called
|
|
109
|
+
* fire-and-forget; implementations must not block and must not throw
|
|
110
|
+
* meaningfully (the coordinator contains exceptions). Most backends should
|
|
111
|
+
* leave this unimplemented and let their SDK's own batching cadence govern
|
|
112
|
+
* export timing: a backend that does implement it owns the interaction
|
|
113
|
+
* between its concurrent flushes and {@link shutdown}'s drain (the OTel
|
|
114
|
+
* backend leaves it unimplemented for exactly that hazard — see the
|
|
115
|
+
* revival Agent Note).
|
|
116
|
+
*/
|
|
117
|
+
flush?(): void;
|
|
118
|
+
/**
|
|
119
|
+
* Forward the fiber's disposal to the SDK: flush whatever is queued and
|
|
120
|
+
* reach quiescence, per the SDK's own shutdown contract. Everything
|
|
121
|
+
* emitted before this call must still be delivered — including records
|
|
122
|
+
* enqueued while a {@link flush} hint is in flight, so a backend whose SDK
|
|
123
|
+
* guards against concurrent flushes orders behind the outstanding one (the
|
|
124
|
+
* coordinator emits its dispose-time `shutdown` markers immediately before
|
|
125
|
+
* calling this). Awaited by the coordinator's dispose; a rejection is
|
|
126
|
+
* logged as a warning and never fails application teardown.
|
|
127
|
+
* The coordinator captures dispose-time shutdown markers immediately before
|
|
128
|
+
* this call for live capture; on-demand capture creates no ops records.
|
|
129
|
+
* @returns resolves when the backend's pipeline has quiesced.
|
|
130
|
+
*/
|
|
131
|
+
shutdown(): Promise<void>;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Deployment-selected session-sharing mode, not confirmation of SDK delivery.
|
|
135
|
+
*/
|
|
136
|
+
export type SessionTelemetrySharingStatus = 'full' | 'feedback-only' | 'disabled';
|
|
137
|
+
/**
|
|
138
|
+
* Loadable form of the backend contract: one implementation per context —
|
|
139
|
+
* the cordis `Service` registration under the `telemetry` key throws on a
|
|
140
|
+
* duplicate, cordis' standard behavior. A backend composes a
|
|
141
|
+
* {@link SessionTelemetryCoordinator} in its constructor to install the capture side.
|
|
142
|
+
*/
|
|
143
|
+
export declare abstract class SessionTelemetryBackend extends Service implements SessionTelemetrySink {
|
|
144
|
+
constructor(ctx: Context);
|
|
145
|
+
/**
|
|
146
|
+
* Deployment-selected sharing mode, independent of SDK delivery.
|
|
147
|
+
*/
|
|
148
|
+
abstract readonly sharing: SessionTelemetrySharingStatus;
|
|
149
|
+
/**
|
|
150
|
+
* See {@link SessionTelemetrySink.emit} — that declaration is the contract's one home.
|
|
151
|
+
* @param record - the logical record to report; owned by the backend after the call.
|
|
152
|
+
*/
|
|
153
|
+
abstract emit(record: SessionTelemetryRecord): void;
|
|
154
|
+
/** See {@link SessionTelemetrySink.flush}. */
|
|
155
|
+
flush?(): void;
|
|
156
|
+
/**
|
|
157
|
+
* See {@link SessionTelemetrySink.shutdown}.
|
|
158
|
+
* @returns resolves when the backend's pipeline has quiesced.
|
|
159
|
+
*/
|
|
160
|
+
abstract shutdown(): Promise<void>;
|
|
161
|
+
}
|
|
162
|
+
export { SessionTelemetryCoordinator, type SessionTelemetryCapture, type SessionTelemetryCaptureOptions } from './coordinator.ts';
|
|
163
|
+
//# sourceMappingURL=index.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qilin-agent/session-telemetry",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"description": "SessionTelemetryBackend seam for the QiLin: session-event capture, projection, redaction, and handoff to a reporting backend",
|
|
4
|
+
"version": "3.1.4",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/kkutysllb/QiLin.git",
|
|
11
|
+
"directory": "packages/session/session-telemetry"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "lib/index.js",
|
|
18
|
+
"types": "lib/types/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./lib/types/index.d.ts",
|
|
22
|
+
"default": "./lib/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./src/*": "./src/*",
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"lib/index.js",
|
|
29
|
+
"lib/types/**/*.d.ts"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@qilin-agent/kylin": "~4.0.3",
|
|
34
|
+
"@qilin-agent/agent": "3.1.4",
|
|
35
|
+
"@qilin-agent/session": "3.1.4"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@qilin-agent/kylin": "~4.0.3",
|
|
39
|
+
"@qilin-agent/session": "3.1.4",
|
|
40
|
+
"@qilin-agent/agent": "3.1.4"
|
|
41
|
+
}
|
|
6
42
|
}
|