npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tasklens/protocol

v0.1.0

Published

TaskLens 事件协议:类型定义、运行时校验、事件流工具与状态派生(零依赖,框架无关)

Readme

@tasklens/protocol

TaskLens 事件协议。 Agent 的执行过程(计划 / 步骤 / 工具调用 / 人工审批 / 重试)在这里被定义成一份只追加的事件流,界面上看到的一切都由它派生。

零运行时依赖、纯 TypeScript、Node 与浏览器双端可用。框架无关 —— 这是整个 TaskLens 唯一的"真相来源"。

pnpm add @tasklens/protocol

事件信封(必填)

每条事件都是一个 JSON 对象,下面 6 个字段必填,缺任何一个都会被判定为畸形事件:

{
  id: 'e_7',            // 全局唯一,去重键
  runId: 'run_abc',     // 一次执行
  seq: 7,               // 单调递增,排序键(不是 at)
  at: 1735689600000,    // 时间戳,仅用于展示
  schemaVersion: 1,     // 协议版本,当前 1
  type: 'tool:call',    // 事件类型
  // …该类型的载荷
}

seq 是排序键,不是 at。 多进程 / 跨机器的墙上时钟不可信,只按时间戳排序会让时间线错乱。 用 createRun()(@tasklens/core)时这些字段全部自动生成,你不需要手写。

不要手写事件——用宽容解析

真实场景里,"手写一份 JSON"几乎一定会漏掉信封字段。与其让用户先读懂规范, 不如在入口处补全(协议本身不放宽:去重靠 id、排序靠 seq、审计靠 at,这些不能丢):

import { toRunEvents } from '@tasklens/protocol'

// 只有 type 和业务字段 —— 完全不合规,但这就是手写日志的真实样子
const handwritten = [
  { type: 'run:start', goal: '给 20 位未激活用户发激活邮件' },
  { type: 'plan', steps: ['查询未激活用户', '生成邮件草稿', '发送'] },
  { type: 'step:start', stepId: 'step_1' },
  { type: 'tool:call', stepId: 'step_1', name: 'db.query', args: { table: 'users' } },
  { type: 'tool:result', ok: true, result: { rows: 20 }, ms: 41 },
  { type: 'step:done', stepId: 'step_1' },
]

const { events, renderable, malformed, issues, meta } = toRunEvents(handwritten)
// events       校验通过的事件(可直接派生出完整面板状态)
// renderable   给面板用的完整列表:合规事件 + 被包成合法信封的坏行,**保持输入顺序**
// malformed    修不好的原始行(原样保留,不丢)
// issues       我改了你哪些地方 —— 别丢,见下面「透明」一节
// meta         行数统计、runId 列表、解析出的 goal

它做四件事:

| 步骤 | 内容 | | --- | --- | | 拆包 | 数组 / { events: [...] } / 单条对象都能进 | | 补信封 | id runId seq at schemaVersion 缺哪个补哪个 | | 修载荷 | 只做无歧义推断,见下表 | | 报告 | 每处改动进 issues;修不好的行降级保留 |

自动推断的全部清单(每一项都无歧义,不涉及语义猜测):

| 推断 | 依据 | | --- | --- | | tool_call / toolCall / ToolCall → tool:call | 命名风格归一 | | step_end / step_finish → step:done | 少量固定别名 | | plan.steps 传字符串数组 → { id, title } | 补 step_N 作为 id | | tool:result 缺 callId | 与最近一次未配对的 tool:call 配对 | | tool:result 缺 ok | 有 error 就是 false,否则 true | | tool:call / approval:request 缺 stepId | 归给唯一"进行中"的步骤;否则不猜 | | approval:decision 缺 approvalId | 与待决策的那条审批配对 | | decision: 'approved' / 'ok' / 'denied' … | 同义词收敛到 approve / edit / reject | | approval:request 缺 risk / summary | 补中性值 medium / 取同义字段(title message reason) | | step:error 缺 retryable | 补 false("不自动重试"是安全的一侧) | | run:paused 缺 reason | 有待决策审批就是 approval,否则 user | | 整份日志没有 run:start | 补一条,面板才有运行头部(可用 synthesizeRunStart: false 关掉) | | 部分行缺 seq | 整体按数组顺序重编号;全部都有 seq 时一个都不动(审计保真) | | id 撞车但内容不同 | 重新分配;内容完全相同的重复投递则保留(交给去重层) |

不猜 —— 这是最重要的设计约束

宁可让用户看到一条折叠的原始 JSON,也不要让面板显示一个"看起来正常、其实是编出来的"执行过程。

需要语义判断的地方一律不补:不知道工具名就不补 name、不知道属于哪一步就不补 stepId、 decision: 'maybe' 就老老实实降级。修不好的行仍然带合法信封(所以能进时间线被折叠展示), 但永远不会被当成正常事件执行。

透明:改了就要说

issues 里每一条都是「哪一行、改了什么、为什么」,level 分四级:

issues.filter((i) => i.level === 'fixed')    // 已自动补全
issues.filter((i) => i.level === 'degraded') // 修不好,降级保留
issues.filter((i) => i.level === 'dropped')  // 整行无法解析,已丢弃
issues.filter((i) => i.level === 'info')     // 只是告知(未知类型会折叠等)

别把 issues 扔掉。 静默补全是最危险的一种"好用": 用户看到面板一切正常,就永远不知道自己那份日志本来是有问题的, 下次还会写出同样的错。<RunReplay> 会把改动直接显示在面板上方。

生成合规样例数据

想造一份完全合规的事件流(演示 / 测试 / 文档用),用内置生成器:

import { createMockRun } from '@tasklens/protocol'

const events = createMockRun({
  goal: '给 20 位未激活用户发激活邮件',
  stepCount: 4,
  withApproval: true,
  approvalOutcome: 'pending', // 事件流停在"等待审批",模拟一条正在进行的 run
  withFailure: true,          // 含一个可重试的失败步骤
  withUnknownEvent: false,    // 含一条未知类型事件(验证前向兼容折叠)
})

校验:不抛错、不丢弃

import { splitEvents, normalize, deriveRunState, deriveRunStateFromUnknown } from '@tasklens/protocol'

const { events, malformed, ok } = splitEvents(rawInputFromAnywhere)
//  events    通过校验的事件(含未知类型,前向兼容)
//  malformed 校验失败的事件 —— 原样保留,可以退化成原始 JSON 展示
//  ok        是否全部通过

const state = deriveRunState(normalize(events))   // normalize = 按 seq 排序 + 按 id 去重

// 连校验都懒得管、只想让它别炸的时候:
const state2 = deriveRunStateFromUnknown(rawInputFromAnywhere)

这是这套协议的核心承诺:脏数据不会让面板白屏。缺字段的事件退化成一行原始 JSON,未知类型折叠成"未知事件",而不是抛异常。

事件类型

| type | 说明 | | --- | --- | | run:start / run:done / run:error / run:paused | 执行生命周期 | | plan | 步骤计划(用于渲染"还剩几步") | | step:start / step:progress / step:done / step:error | 步骤状态,error 带 retryable | | tool:call / tool:result | 工具调用与结果(含 ms / tokens / cost) | | approval:request / approval:decision | 人工介入(summary + 结构化 preview + risk) | | retry:request | 步骤级重试锚点(fromEventId) |

主要 API

// 校验
validateEvent(v) / validateEnvelope(v) / validateEventPayload(v)   // → issues[] / EventValidation
isValidEvent(x) / isEnvelope(x) / assertEvent(x)
splitEvents(unknown)          // → { events, malformed, ok }
isKnownEventType(t) / RUN_EVENT_TYPES / APPROVAL_EVENT_TYPES

// 宽容解析(档 0:手写 JSON 直接出面板)
toRunEvents(input, options?)   // → { events, renderable, malformed, issues, meta }
isCoerceResult(value)          // 判定是不是上面那个结果(组件用它避免二次解析)

// 事件流工具(全部纯函数,不改原数组)
sortBySeq · dedupeById · normalize · appendEvent · appendEvents
filterByRun · sinceSeq · upToEventId · latestSeq · compareBySeq

// 状态派生
deriveRunState(events)               // → RunState
deriveRunStateFromUnknown(unknown)   // 不校验直接派生
createDeriver()                      // 增量推导(大事件流用,避免每次全量重算)
hasPendingApproval(state, stepId) · RISK_WEIGHT

// 测试与演示
createMockRun(options) · createMockEvents() · createMockEvent() · scenarios · getScenario

可选:zod 集成

zod 是可选的 peerDependency,只有用到时才需要装:

import { … } from '@tasklens/protocol/zod'

几条硬规则

| 规则 | 原因 | | --- | --- | | seq 是排序键,at 只用于展示 | 时钟不可信 | | 按 id 去重 | SSE 断线重连、Last-Event-ID 续传一定会重复推事件 | | 协议里没有 HTML、没有模型厂商字段 | args / result 是 unknown,渲染交给使用方 | | 未知事件类型只折叠、不崩 | 旧前端遇新事件、新前端遇旧事件都不该炸 | | 同一主版本内只允许新增可选字段 | 更高主版本按未知事件处理 | | approval:* 是一等公民 | 审批不是"工具调用的特例" |

许可

MIT © 李学丹 · 仓库与完整文档