跳转至

会话日志:模型看到的每个字都记账

pi 的会话存成一个 JSONL 文件,扩展却能在每次请求前用 context、context_with_system 改写要发的消息,改写只对这一次请求生效、不落盘1——模型当时看到了什么,事后从会话文件拼不回来。dsh 反过来:只追加的事件日志是唯一真源,模型历史每一步从日志现算。下面每节贴源码模版和实跑效果;模型回复是项目测试里的 MockAdapter 脚本(构造的),其余都是 dsh 自己的代码。

只追加的事件日志,模型视图从它折叠出来

问题:可变的消息数组加上事后补写的日志,是两份真相,迟早对不上。项目的决策记录2在否决这条路时写道:

可变消息数组 + 事件仅作通知发出:更简单,但状态与日志可能分歧;采用事件溯源后,日志本身即是状态,分歧在结构上不可能发生。

会话是一串带连续 seq 的事件,Session.append 先做一份无损 JSON 快照、深度冻结再提交。能进模型历史的只有五类消息事件,每条都标明自己怎么进模型视图——项目代码里叫 surface:

模版 · packages/core/session/src/types.ts:450-465
/**
 * How a session event entered the ordered surface. Only valid on
 * {@link SurfaceEventType} events.
 *
 * - `'append'`: added to the tail — normal path for user/assistant/tool
 *   messages.
 * - `{ op: 'replace', startSeq, endSeq }`: replaces surface nodes from `startSeq`
 *   (inclusive) through `endSeq` (inclusive) with this node. Both must exist as
 *   surface nodes in the current surface. `startSeq === endSeq` replaces a single
 *   node. The node's {@link SessionEvent.sourceEventSeqs} must include every
 *   shadowed surface node. Used by compaction; any surface-replacing producer
 *   may use it.
 */
export type SurfaceOp =
  | 'append'
  | { op: 'replace'; startSeq: SessionSeq; endSeq: SessionSeq }

模型视图就是一列 seq,append 往末尾加一个,replace 把连续一段换成一个:

模版 · packages/core/session/src/surface.ts:563-585
function applySurfacePlan(
  state: SurfaceFoldState,
  plan: SurfacePlan | undefined,
): SurfaceFoldReplacement | undefined {
  if (plan?.kind === 'append') {
    state.nodes.push(plan.seq)
  } else if (plan?.kind === 'replace') {
    state.nodes.splice(plan.startIdx, plan.endIdx - plan.startIdx + 1, plan.seq)
    state.replaceGeneration += 1
    state.contentGeneration += 1
  } else if (plan?.kind === 'project') {
    for (const [seq, message] of plan.messages) state.projectedMessages.set(seq, message)
    state.projections.add(plan.projection)
    state.contentGeneration += 1
  }
  if (plan?.kind !== 'replace') return
  return {
    seq: plan.seq,
    start: plan.start,
    end: plan.end,
    shadowedSeqs: plan.shadowedSeqs,
  }
}

用 dsh 的 agent 循环跑四轮:第 1 轮调一次工具;第 2 轮第一次请求失败、重试成功;第 3 轮写到一半被用户取消;第 4 轮接着问。日志全貌如下(第三列是进模型视图的方式,空着的就是只留档):

效果 · 四轮会话的完整日志(模型回复是构造的)
 0  agent/inbox/spliced         
 1  turn/start                 turn 1
 2  agent/inbox/spliced         
 3  step/start                 turn 1 step 1
 4  system/message     append  "You are an AI agent powered by DeepSeek
 5  user/message       append  user: "go"
 6  request/header             reason=initial
 7  request/context            
 8  assistant/message  append  "calling echo" + tool-call echo
 9  tool/call                  echo
10  tool/result        append  "echo: one"
11  step/end                   turn 1 step 1
12  step/start                 turn 1 step 2
13  assistant/message  append  "done"
14  step/end                   turn 1 step 2
15  turn/end                   reason=completed
16  agent/inbox/spliced         
17  turn/start                 turn 2
18  agent/inbox/spliced         
19  step/start                 turn 2 step 1
20  user/message       append  user: "again"
21  assistant/attempt          stream 1 段,不进模型视图
22  assistant/message  append  "ok"
23  step/end                   turn 2 step 1
24  turn/end                   reason=completed
25  agent/inbox/spliced         
26  turn/start                 turn 3
27  agent/inbox/spliced         
28  step/start                 turn 3 step 1
29  user/message       append  user: "讲讲三点"
30  assistant/message  append  "第一点是"  interrupted
31  step/end                   turn 3 step 1
32  turn/end                   reason=aborted
33  agent/inbox/spliced         
34  turn/start                 turn 4
35  agent/inbox/spliced         
36  step/start                 turn 4 step 1
37  user/message       append  user: "第二点展开讲讲"
38  assistant/message  append  "第二点是……"
39  step/end                   turn 4 step 1
40  turn/end                   reason=completed

41 条事件里只有 11 条进了模型视图:

效果 · foldSurface 折出的模型视图
nodes: [4, 5, 8, 10, 13, 20, 22, 29, 30, 37, 38]
derived roles: system → user → assistant → tool → assistant → user → assistant → user → assistant → user → assistant

replace 是压缩用的操作。下面构造一条替换,把第 1 轮那一段换成一句摘要,再试两条护栏:

效果 · 构造的 replace 过 Session.append
seeded 41 events; Session.create appended session/end-seed @41
before: nodes [4, 5, 8, 10, 13, 20, 22, 29, 30, 37, 38]
  system    "You are an AI agent powered by DeepSeek Harness.\
  user      "go"
  assistant "calling echo[tool-call echo]"
  tool      "echo: one"
  assistant "done"
  user      "again"
  assistant "ok"
  user      "讲讲三点"
  assistant "第一点是"
  user      "第二点展开讲讲"
  assistant "第二点是……"

append user/message @42 with surfaceOp replace 5..13, sourceEventSeqs [5, 8, 10, 13]
after:  nodes [4, 42, 20, 22, 29, 30, 37, 38]
  system    "You are an AI agent powered by DeepSeek Harness.\
  user      "SUMMARY: user said go; echo returned one; assista
  user      "again"
  assistant "ok"
  user      "讲讲三点"
  assistant "第一点是"
  user      "第二点展开讲讲"
  assistant "第二点是……"
log still holds seq 8: assistant/message "calling echo[tool-call echo]"
log length 43; foldSurface(log).nodes = [4, 42, 20, 22, 29, 30, 37, 38]

replace node 0 (system prompt) with a user/message: surface replace: node 0 holds the system prompt and may be rewritten only by a system/message over exactly that node
replace 20..22 without citing them: surface replace: sourceEventSeqs must include every shadowed surface node; missing 20, 22

被换掉的事件仍在日志里,只是不再进模型视图,项目把这叫「遮住」(shadowed)。替换必须在 sourceEventSeqs 里列出它遮住的每一条;第 0 个节点放的是 system prompt,只有 system 消息能替换它。

请求是日志的纯函数,算完冻结

问题:只要允许插件改单次请求,日志就解释不了请求,前缀缓存也会悄悄丢掉。决策记录3否决「每次 agent/request 都传一个可自由修改的配置」时说:

监听器可以零记账地逐次切换模型,悄然放弃本设计旨在保护的提供方缓存。

dsh 的规矩叫「模型可见 ⟺ 已持久引用」:每一步先把要进模型的东西写进日志,再从日志派生消息,然后连同请求头一起冻结:

模版 · packages/core/agent-loop/src/agent.ts:686-703
    // canonicalHeader is shallow; append logs a detached snapshot, not these local values.
    deepFreeze(header)
    const boundaryMessages = session.deriveMessages()
    for (const message of boundaryMessages) {
      if (this.frozenMessages.has(message)) continue
      deepFreeze(message)
      this.frozenMessages.add(message)
    }
    Object.freeze(boundaryMessages)
    const request = markAgentLoopRequest(Object.freeze({
      ...header.config,
      messages: boundaryMessages,
      toolHistory: session.toolHistory(),
      ...header.tools !== undefined ? { tools: header.tools } : {},
      sessionId: this.session.id,
      signal,
    }))
    return request

把上面那段会话里发出的 6 个请求,逐个只拿日志前缀重建一遍,再试着改其中一个:

效果 · 每个请求都能只凭日志前缀重建
request 1: 2 messages, settled as assistant/message @8; rebuilt from log[0..7] equal=true; extends previous=-; frozen=true
request 2: 4 messages, settled as assistant/message @13; rebuilt from log[0..12] equal=true; extends previous=true; frozen=true
request 3: 6 messages, settled as assistant/attempt @21; rebuilt from log[0..20] equal=true; extends previous=true; frozen=true
request 4: 6 messages, settled as assistant/message @22; rebuilt from log[0..21] equal=true; extends previous=true; frozen=true
request 5: 8 messages, settled as assistant/message @30; rebuilt from log[0..29] equal=true; extends previous=true; frozen=true
request 6: 10 messages, settled as assistant/message @38; rebuilt from log[0..37] equal=true; extends previous=true; frozen=true
last request carries the interrupted prefix: true
mutation: TypeError: Cannot add property 2, object is not extensible

6 个请求全都重建得一模一样,而且每个都是前一个的逐字前缀延长——前缀缓存能一路命中,靠的就是这一点。项目自己用一条测试钉住这件事,测试名直接叫 THEOREM:

模版 · packages/core/agent-loop/tests/request-reconstruction.spec.ts:880-890
    adapter.requests.forEach((request, index) => {
      const stepStart = stepStarts[index]!
      const settlement = events.find(e =>
        (e.type === 'assistant/message' || e.type === 'assistant/attempt')
        && e.data.turn === stepStart.data.turn
        && e.data.step === stepStart.data.step,
      )!
      // Messages: the entered batch is logged after step/start, so rebuild the
      // complete dispatch prefix through a completely fresh Session.
      const rebuilt = Session.create(SessionId(`rebuild-${index}`), structuredClone(events.slice(0, settlement.seq)))
      expect(structuredClone(request.messages)).toEqual(rebuilt.deriveMessages())
效果 · vitest 跑这个测试文件(摘出相关的几条)
✓ each step request within a turn append-extends the previous, frozen end to end 18ms
✓ a compaction replace rewrites the resend, and the log explains it 3ms
✓ a mutation attempt on the frozen request content throws into the step (loud, not silent) 2ms
✓ a delegating listener cannot mutate the seed through next() — the fold stays log-true 3ms
✓ THEOREM: every request rebuilds byte-equal from the session log alone 5ms
Test Files  1 passed (1)
Tests  30 passed (30)

代价也在这里。项目在 pi 扩展桥接提案里列了一张「比 pi 窄」的表4,第一行就是它:pi 的 context、before_provider_request 钩子能逐请求改写模型输入,dsh 给不了——「循环从日志派生每个请求并将其冻结」。插件想影响下一个请求,只能往日志里追加带来源的消息,见拼 prompt。

每次模型调用落一条结算记录

问题:只存最终消息,失败、重试、取消都没了痕迹;可取消时把半截扔掉也不对。决策记录5里举的例子:

后续的「第二点展开讲讲」之类追问会缺少用户已读到的文本,在该轮次上创建的分支也会继承这个缺口。

每次模型调用落且只落一条结算记录(settlement):成功是进模型视图的 assistant/message;失败或重试是只留档的 assistant/attempt,原始流照样嵌在里面。用户取消时,已经流出来的文字和推理照常记成 assistant/message,打上 interrupted;写到一半的工具调用整个丢掉——它从没执行,不能留一个没结果的调用:

模版 · packages/core/agent-loop/src/agent.ts:465-494(取消与流中断)
          if (signal.aborted) {
            const content = live.interruptedBlocks()
            if (content.length > 0) {
              live.settle('assistant/message', () => this.session.append('assistant/message', {
                turn,
                step,
                message: createAssistantMessage({
                  content,
                  source: {
                    provider: request.provider,
                    model: request.model,
                    ...live.replayState === undefined ? {} : { replayState: live.replayState },
                  },
                }),
                interrupted: true,
                ...live.usage === undefined ? {} : { usage: live.usage },
                stream: live.stream,
              }, { surfaceOp: 'append' }).seq)
            } else {
              live.settle(
                'assistant/attempt',
                () => this.session.append('assistant/attempt', { turn, step, stream: live.stream }).seq,
              )
            }
          } else {
            live.settle(
              'assistant/attempt',
              () => this.session.append('assistant/attempt', { turn, step, stream: live.stream }).seq,
            )
          }
模版 · packages/core/agent-loop/src/agent.ts:506-526(失败先留档,再问要不要重试)
        if (finish.kind === 'error' || finish.kind === 'aborted') {
          live.settle(
            'assistant/attempt',
            () => this.session.append('assistant/attempt', { turn, step, stream: live.stream }).seq,
          )
          const action = await this.dispatch.waterfall(
            'agent/request-error', {
              turn,
              step,
              provider: request.provider,
              failure: finish.failure,
              retryPolicy: preparedCall?.retryPolicy,
              signal,
            },
            () => Promise.resolve<RequestErrorAction>(undefined),
          )
          signal.throwIfAborted()
          if (action?.kind !== 'retry') {
            throw new LlmError(finish.failure.message, finish.failure.code, finish.failure)
          }
          continue

重试会回到循环开头,从同一个模型视图重新派生请求——上一节的 request 3 和 request 4 一模一样。日志里对应的两段:

效果 · 第 2 轮:失败一次再重试
20  user/message       append  user: "again"
21  assistant/attempt          stream 1 段,不进模型视图
22  assistant/message  append  "ok"
效果 · 第 3 轮:写到一半被取消
29  user/message       append  user: "讲讲三点"
30  assistant/message  append  "第一点是"  interrupted
31  step/end                   turn 3 step 1
32  turn/end                   reason=aborted

第 3 轮模型吐出「第一点是」,又开了一个参数只写一半的 echo 调用;日志只留下「第一点是」,那个调用连 tool/call 都没有。第 4 轮的请求带着这半截(上一节末行 carries the interrupted prefix: true),用户说「第二点展开讲讲」,模型知道第一点讲到了哪。

崩溃补齐:结果未知的工具调用别盲目重试

问题:进程在工具执行到一半时被杀,日志尾巴上就剩一个没有结果的工具调用。provider 不收悬空的调用;要是直接重放,有副作用的操作就会再跑一遍。另一条路是加载时整轮丢掉,项目否决了:「单个轮次可以包含大量真实工作」6。

恢复会话时,dsh 先给悬空的调用补错误结果,再补上 step/end 和 turn/end。错误结果分两种措辞:已经记下开始的叫「结果未知」,还没开始的叫「没开始,需要就重试」:

模版 · packages/core/session/src/repair.ts:31-41
/** Model-visible wording of the synthetic error tool results, keyed by cause. */
const CLOSER_TEXT = {
  interrupted: {
    started: 'The tool call was interrupted after it was recorded, but no result was durably recorded. Its outcome is unknown. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly.',
    notStarted: 'The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed.',
  },
  forked: {
    started: 'The history inherited by this branch records this tool call starting but does not include its result. The parent session may have completed it after the fork point. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly.',
    notStarted: 'The history inherited by this branch has no record of this tool call starting. The parent session may have executed it after the fork point. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly.',
  },
} as const
模版 · packages/core/agent-loop/src/index.ts:884-892(恢复时由 agent 层补,存储层不碰语义)
          // Semantic crash repair is the agent layer's job: persistence hands
          // back the physically valid log; an interrupted final turn receives
          // synthetic closers (missing tool errors, step/end, turn/end) that
          // are appended through the same handle as an ordinary batch.
          const coldRead = await handle.read(0, undefined, { signal: fused })
          fused.throwIfAborted()
          const persisted = coldRead.events
          const closers = interruptedTurnClosers(persisted)
          if (closers.length > 0) await handle.append(closers)

构造一个崩溃现场:模型一步里先要 deploy,再要 echo;工具默认一个接一个跑,deploy 开始执行后卡住不返回,这时把日志拿走(相当于进程被杀):

效果 · 构造的崩溃日志过 interruptedTurnClosers
persisted tail:
   6 request/header
   7 request/context
   8 assistant/message tool-call deploy(c1), tool-call echo(c2)
   9 tool/call c1

interruptedTurnClosers(persisted):
  10 tool/result c1 isError=true code=TOOL_OUTCOME_UNKNOWN sourceEventSeqs=[9]
     "The tool call was interrupted after it was recorded, but no result was durably recorded. Its outcome is unknown. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly."
  11 tool/result c2 isError=true code=TOOL_NOT_STARTED
     "The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed."
  12 step/end {"turn":1,"step":1}
  13 turn/end {"turn":1,"reason":{"kind":"interrupted"}}

model view after repair: system → user → assistant → tool → tool

deploy 已记下开始,补的结果让模型先判断有没有副作用,有就先核实或问用户;echo 没开始,可以直接重试。补完每个调用都有配对的结果,provider 能收。

fork 复制前缀,不在原日志里分叉

问题:用户想从某个旧位置换个走向;fork 出来的子 agent 要带着父会话的上下文开工,还要命中父会话的前缀缓存。

pi 的会话文件是一棵树,分支留在同一个文件里。dsh 的日志是线性的:fork 把父日志切一刀,前缀复制进一个新会话,末尾打一个 session/end-seed 标记;切点落在一个没收尾的轮次里,就照崩溃补齐的办法收尾,只是措辞换成「父会话在切点之后可能已经做完了」:

模版 · packages/core/session/src/fork.ts:20-30
 */
export function buildForkSeed(events: readonly SessionEvent[], boundary: SessionSeqType): SessionEvent[] {
  const prefix = events.slice(0, boundary + 1)
  prefix.push({
    type: 'session/end-seed', seq: SessionSeq(boundary + 1),
    // oxlint-disable-next-line typescript/no-non-null-assertion -- the caller validated this exact source event.
    time: events[boundary]!.time,
    data: { inherited: true },
  })
  return prefix.concat(openTurnClosers(prefix, { kind: 'forked' }))
}

在两份真实日志上各切一刀——A 切在第 1 轮结束处,B 切在 deploy 刚开始处:

效果 · buildForkSeed 的两种切法
A: buildForkSeed(log, 15) -> 17 events; tail:
  16 session/end-seed {"inherited":true}
  child messages start with the parent's 5 messages byte-for-byte: true
B: buildForkSeed(log, 9) -> 15 events; tail:
  10 session/end-seed {"inherited":true}
  11 tool/result c1 TOOL_OUTCOME_UNKNOWN: "The history inherited by this branch records this tool call starting but does not include its re…"
  12 tool/result c2 TOOL_NOT_STARTED: "The history inherited by this branch has no record of this tool call starting. The parent sessio…"
  13 step/end {"turn":1,"step":1}
  14 turn/end {"turn":1,"reason":{"kind":"forked"}}
  child messages start with the parent's 3 messages byte-for-byte: true

两种切法下,子会话派生出的消息开头都和父会话逐字节相同,fork 出来的子 agent 第一次请求就能命中父会话的缓存。界面上的「分支」按钮和 fork 型子 agent 都走这条,后者见多 agent 与长时工作。

格式升级不覆盖旧文件

问题:日志格式会变,已经发出去的旧版本会话还得能打开。整份读进内存再迁移扛不住大会话:决策记录7里,一份 116 MB 的真实会话在返回之前耗尽了 16 GB 的 Node 进程;改成逐行流式迁移后用了 6.2 秒、2.1 GB。

迁移拆成一串相邻的边,每条边只管 vN → vN+1,声明时就检查相邻:

模版 · packages/session/session-format/src/chain.ts:24-34
export function defineSessionFormatMigration(migration: SessionFormatMigration): SessionFormatMigration {
  if (typeof migration.name !== 'string' || migration.name.length === 0) {
    throw new SessionFormatError('Session migration name must be a non-empty string')
  }
  const from = sessionFormatVersion(migration.fromVersion, `${migration.name} fromVersion`)
  const to = sessionFormatVersion(migration.toVersion, `${migration.name} toVersion`)
  if (to !== from + 1) {
    throw new SessionFormatError(`${migration.name} must declare adjacent v${from}->v${from + 1}`)
  }
  return Object.freeze({ ...migration })
}

每条边建一个有状态的 stage,事件一条条流过去,不需要中间数组:

模版 · packages/session/session-format/src/types.ts:142-157
export interface SessionFormatMigrationStage {
  /** Target inherited cut when it is unchanged and known before EOF. */
  readonly headerInheritedEventCount?: number
  /** Transform one source event and synchronously emit every settled target item. */
  transformEvent(
    event: SessionFormatEvent,
    context: SessionFormatMigrationContext,
  ): void
  /** Transform one compact source run without requiring an intermediate expansion array. */
  transformRun(
    run: SessionFormatEventRun,
    context: SessionFormatMigrationContext,
  ): void
  /** Emit trailing target items and return the exact target inherited cut. */
  finish(context: SessionFormatMigrationContext): number
}

当前版本是 v4,四条边连成一条链:

模版 · packages/session/session-format-catalog/src/generated.ts:16-31
export const sessionFormatCatalogOptions: SessionFormatCatalogOptions = {
  currentVersion: 4,
  codecs: [
    releasedV0SessionFormatCodec,
    releasedV1SessionFormatCodec,
    releasedV2SessionFormatCodec,
    releasedV3SessionFormatCodec,
    releasedV4SessionFormatCodec,
  ],
  currentEncoder: releasedV4SessionFormatCodec,
  migrations: [
    sessionFormatV0ToV1,
    sessionFormatV1ToV2,
    sessionFormatV2ToV3,
    sessionFormatV3ToV4,
  ],

拿仓库快照里一份 v0 会话(snapshots/session/text-turn,占位符换回真值、补回快照去掉的 seq),放进临时会话目录,先只读打开、再写打开:

效果 · v0 会话被 JSONL 存储打开两次
stored: --tmp-demo-cwd--/text-turn-demo/session.jsonl  version=0  sha256=ee51b18a2487bf9d  rows=25
open(read):  17 current events (permission/preset, sandbox/mode, approval/policy, agent/inbox/spliced, turn/start, step/start, …); dir = session.jsonl
open(write): dir = session.jsonl, session.lock, session.v4.jsonl
  session.jsonl: 27347 bytes version=0 sha256=ee51b18a2487bf9d
  session.lock: 0 bytes
  session.v4.jsonl: 27338 bytes version=4 sha256=393ae90c28cc26fb
v0 file unchanged: true

只读打开只在内存里迁移,不写文件;写打开时在旁边发布 session.v4.jsonl,原来的 session.jsonl 一个字节没动。v0 的 25 行里有打包的流式分片,迁到 v4 折成 17 个事件。

谁管什么

  • 程序:追加、冻结、编号;折出模型视图;每步派生请求再冻结;落结算记录;补齐悬空调用;fork 复制前缀;迁移只发布新文件
  • 模型:只读派生出来的视图;补齐的错误结果交给它判断要不要重试、要不要先问用户
  • 用户:取消、分支、恢复旧会话
  • 插件:能往日志里追加带来源的事件,不能改已经派生出来的请求

实跑原件

脚本在 assets/sim/log/:loop.ts(四轮会话)、surface.ts(构造的 replace)、crash.ts(构造的崩溃)、fork.ts、migrate.ts、snapshot-events.py;实验设置见维护记录。

四轮会话的完整日志(loop-events.json)
[
  {
    "type": "agent/inbox/spliced",
    "seq": 0,
    "time": 1792499042229,
    "data": {
      "target": "next-turn",
      "start": 0,
      "inserted": [
        {
          "content": [
            {
              "type": "text",
              "text": "go"
            }
          ],
          "source": {
            "kind": "user"
          },
          "role": "user",
          "id": "562b50f5-c251-49ec-9700-af7e939e29c5"
        }
      ]
    }
  },
  {
    "type": "turn/start",
    "seq": 1,
    "time": 1792499042230,
    "data": {
      "turn": 1
    }
  },
  {
    "type": "agent/inbox/spliced",
    "seq": 2,
    "time": 1792499042230,
    "data": {
      "target": "next-turn",
      "start": 0,
      "removedCount": 1,
      "inserted": []
    }
  },
  {
    "type": "step/start",
    "seq": 3,
    "time": 1792499042230,
    "data": {
      "turn": 1,
      "step": 1
    }
  },
  {
    "type": "system/message",
    "seq": 4,
    "time": 1792499042231,
    "data": {
      "turn": 1,
      "step": 1,
      "message": {
        "role": "system",
        "content": [
          {
            "type": "text",
            "text": "You are an AI agent powered by DeepSeek Harness.\n\nYou are a test agent."
          }
        ],
        "source": {
          "kind": "system-prompt"
        },
        "id": "532b0b0c-7e13-4ee6-8234-a429ff8d40ae"
      }
    },
    "surfaceOp": "append"
  },
  {
    "type": "user/message",
    "seq": 5,
    "time": 1792499042231,
    "data": {
      "content": [
        {
          "type": "text",
          "text": "go"
        }
      ],
      "source": {
        "kind": "user"
      },
      "role": "user",
      "id": "562b50f5-c251-49ec-9700-af7e939e29c5"
    },
    "surfaceOp": "append"
  },
  {
    "type": "request/header",
    "seq": 6,
    "time": 1792499042231,
    "data": {
      "header": {
        "config": {
          "provider": "mock",
          "model": "mock"
        },
        "tools": [
          {
            "name": "echo",
            "description": "echo back",
            "parameters": {
              "type": "object",
              "properties": {
                "text": {
                  "type": "string"
                }
              }
            }
          }
        ]
      },
      "reason": "initial"
    }
  },
  {
    "type": "request/context",
    "seq": 7,
    "time": 1792499042232,
    "data": {
      "provider": "mock",
      "model": "mock"
    }
  },
  {
    "type": "assistant/message",
    "seq": 8,
    "time": 1792499042233,
    "data": {
      "turn": 1,
      "step": 1,
      "message": {
        "role": "assistant",
        "content": [
          {
            "type": "text",
            "text": "calling echo"
          },
          {
            "type": "tool-call",
            "id": "c1",
            "name": "echo",
            "arguments": "{\"text\":\"one\"}"
          }
        ],
        "source": {
          "kind": "model",
          "provider": "mock",
          "model": "mock"
        },
        "id": "5de51fe6-e57f-4bac-9a80-3fb6b0e22a22"
      },
      "usage": {
        "inputTokens": 10,
        "outputTokens": 5
      },
      "stream": [
        {
          "type": "chunk",
          "time": 1792499042232,
          "chunk": {
            "type": "block-start",
            "index": 0,
            "blockType": "text"
          }
        },
        {
          "type": "text-chunks",
          "time0": 1792499042232,
          "index": 0,
          "dt": [],
          "texts": [
            "calling echo"
          ]
        },
        {
          "type": "chunk",
          "time": 1792499042232,
          "chunk": {
            "type": "block-end",
            "index": 0,
            "block": {
              "type": "text",
              "text": "calling echo"
            }
          }
        },
        {
          "type": "chunk",
          "time": 1792499042232,
          "chunk": {
            "type": "block-start",
            "index": 1,
            "blockType": "tool-call"
          }
        },
        {
          "type": "tool-call-chunks",
          "time0": 1792499042232,
          "index": 1,
          "dt": [],
          "id": "c1",
          "name": "echo",
          "args": [
            "{\"tex"
          ]
        },
        {
          "type": "tool-call-chunks",
          "time0": 1792499042232,
          "index": 1,
          "dt": [],
          "id": "c1",
          "args": [
            "t\":\"one\"}"
          ]
        },
        {
          "type": "chunk",
          "time": 1792499042232,
          "chunk": {
            "type": "block-end",
            "index": 1,
            "block": {
              "type": "tool-call",
              "id": "c1",
              "name": "echo",
              "arguments": "{\"text\":\"one\"}"
            }
          }
        },
        {
          "type": "chunk",
          "time": 1792499042232,
          "chunk": {
            "type": "usage",
            "usage": {
              "inputTokens": 10,
              "outputTokens": 5
            }
          }
        },
        {
          "type": "chunk",
          "time": 1792499042232,
          "chunk": {
            "type": "finish",
            "reason": {
              "kind": "tool-calls"
            }
          }
        }
      ]
    },
    "surfaceOp": "append"
  },
  {
    "type": "tool/call",
    "seq": 9,
    "time": 1792499042233,
    "data": {
      "turn": 1,
      "step": 1,
      "callId": "c1",
      "name": "echo",
      "arguments": "{\"text\":\"one\"}"
    }
  },
  {
    "type": "tool/result",
    "seq": 10,
    "time": 1792499042234,
    "data": {
      "turn": 1,
      "step": 1,
      "message": {
        "role": "tool",
        "source": {
          "kind": "tool",
          "callId": "c1"
        },
        "toolCallId": "c1",
        "content": [
          {
            "type": "text",
            "text": "echo: one"
          }
        ],
        "isError": false,
        "id": "280c8346-5fd4-424e-8ea1-f6ff3e87b2e4"
      }
    },
    "sourceEventSeqs": [
      9
    ],
    "surfaceOp": "append"
  },
  {
    "type": "step/end",
    "seq": 11,
    "time": 1792499042234,
    "data": {
      "turn": 1,
      "step": 1
    }
  },
  {
    "type": "step/start",
    "seq": 12,
    "time": 1792499042235,
    "data": {
      "turn": 1,
      "step": 2
    }
  },
  {
    "type": "assistant/message",
    "seq": 13,
    "time": 1792499042235,
    "data": {
      "turn": 1,
      "step": 2,
      "message": {
        "role": "assistant",
        "content": [
          {
            "type": "text",
            "text": "done"
          }
        ],
        "source": {
          "kind": "model",
          "provider": "mock",
          "model": "mock"
        },
        "id": "d056df26-feb1-4614-b98a-8d686b834872"
      },
      "usage": {
        "inputTokens": 10,
        "outputTokens": 4
      },
      "stream": [
        {
          "type": "chunk",
          "time": 1792499042235,
          "chunk": {
            "type": "block-start",
            "index": 0,
            "blockType": "text"
          }
        },
        {
          "type": "text-chunks",
          "time0": 1792499042235,
          "index": 0,
          "dt": [
            0,
            0,
            0
          ],
          "texts": [
            "d",
            "o",
            "n",
            "e"
          ]
        },
        {
          "type": "chunk",
          "time": 1792499042235,
          "chunk": {
            "type": "block-end",
            "index": 0,
            "block": {
              "type": "text",
              "text": "done"
            }
          }
        },
        {
          "type": "chunk",
          "time": 1792499042235,
          "chunk": {
            "type": "usage",
            "usage": {
              "inputTokens": 10,
              "outputTokens": 4
            }
          }
        },
        {
          "type": "chunk",
          "time": 1792499042235,
          "chunk": {
            "type": "finish",
            "reason": {
              "kind": "stop"
            }
          }
        }
      ]
    },
    "surfaceOp": "append"
  },
  {
    "type": "step/end",
    "seq": 14,
    "time": 1792499042235,
    "data": {
      "turn": 1,
      "step": 2
    }
  },
  {
    "type": "turn/end",
    "seq": 15,
    "time": 1792499042235,
    "data": {
      "turn": 1,
      "reason": {
        "kind": "completed"
      }
    }
  },
  {
    "type": "agent/inbox/spliced",
    "seq": 16,
    "time": 1792499042236,
    "data": {
      "target": "next-turn",
      "start": 0,
      "inserted": [
        {
          "content": [
            {
              "type": "text",
              "text": "again"
            }
          ],
          "source": {
            "kind": "user"
          },
          "role": "user",
          "id": "35e2a228-9379-41e2-afea-e7350961d14a"
        }
      ]
    }
  },
  {
    "type": "turn/start",
    "seq": 17,
    "time": 1792499042236,
    "data": {
      "turn": 2
    }
  },
  {
    "type": "agent/inbox/spliced",
    "seq": 18,
    "time": 1792499042236,
    "data": {
      "target": "next-turn",
      "start": 0,
      "removedCount": 1,
      "inserted": []
    }
  },
  {
    "type": "step/start",
    "seq": 19,
    "time": 1792499042236,
    "data": {
      "turn": 2,
      "step": 1
    }
  },
  {
    "type": "user/message",
    "seq": 20,
    "time": 1792499042236,
    "data": {
      "content": [
        {
          "type": "text",
          "text": "again"
        }
      ],
      "source": {
        "kind": "user"
      },
      "role": "user",
      "id": "35e2a228-9379-41e2-afea-e7350961d14a"
    },
    "surfaceOp": "append"
  },
  {
    "type": "assistant/attempt",
    "seq": 21,
    "time": 1792499042236,
    "data": {
      "turn": 2,
      "step": 1,
      "stream": [
        {
          "type": "chunk",
          "time": 1792499042236,
          "chunk": {
            "type": "finish",
            "reason": {
              "kind": "error",
              "failure": {
                "message": "busy",
                "code": "RATE_LIMIT"
              }
            }
          }
        }
      ]
    }
  },
  {
    "type": "assistant/message",
    "seq": 22,
    "time": 1792499042236,
    "data": {
      "turn": 2,
      "step": 1,
      "message": {
        "role": "assistant",
        "content": [
          {
            "type": "text",
            "text": "ok"
          }
        ],
        "source": {
          "kind": "model",
          "provider": "mock",
          "model": "mock"
        },
        "id": "0816cbc8-f9e3-4c36-afd0-939f167c571f"
      },
      "usage": {
        "inputTokens": 10,
        "outputTokens": 2
      },
      "stream": [
        {
          "type": "chunk",
          "time": 1792499042236,
          "chunk": {
            "type": "block-start",
            "index": 0,
            "blockType": "text"
          }
        },
        {
          "type": "text-chunks",
          "time0": 1792499042236,
          "index": 0,
          "dt": [
            0
          ],
          "texts": [
            "o",
            "k"
          ]
        },
        {
          "type": "chunk",
          "time": 1792499042236,
          "chunk": {
            "type": "block-end",
            "index": 0,
            "block": {
              "type": "text",
              "text": "ok"
            }
          }
        },
        {
          "type": "chunk",
          "time": 1792499042236,
          "chunk": {
            "type": "usage",
            "usage": {
              "inputTokens": 10,
              "outputTokens": 2
            }
          }
        },
        {
          "type": "chunk",
          "time": 1792499042236,
          "chunk": {
            "type": "finish",
            "reason": {
              "kind": "stop"
            }
          }
        }
      ]
    },
    "surfaceOp": "append"
  },
  {
    "type": "step/end",
    "seq": 23,
    "time": 1792499042236,
    "data": {
      "turn": 2,
      "step": 1
    }
  },
  {
    "type": "turn/end",
    "seq": 24,
    "time": 1792499042237,
    "data": {
      "turn": 2,
      "reason": {
        "kind": "completed"
      }
    }
  },
  {
    "type": "agent/inbox/spliced",
    "seq": 25,
    "time": 1792499042237,
    "data": {
      "target": "next-turn",
      "start": 0,
      "inserted": [
        {
          "content": [
            {
              "type": "text",
              "text": "讲讲三点"
            }
          ],
          "source": {
            "kind": "user"
          },
          "role": "user",
          "id": "47e9f7cc-b406-4ede-a4d7-46ce7e92ef1a"
        }
      ]
    }
  },
  {
    "type": "turn/start",
    "seq": 26,
    "time": 1792499042237,
    "data": {
      "turn": 3
    }
  },
  {
    "type": "agent/inbox/spliced",
    "seq": 27,
    "time": 1792499042237,
    "data": {
      "target": "next-turn",
      "start": 0,
      "removedCount": 1,
      "inserted": []
    }
  },
  {
    "type": "step/start",
    "seq": 28,
    "time": 1792499042237,
    "data": {
      "turn": 3,
      "step": 1
    }
  },
  {
    "type": "user/message",
    "seq": 29,
    "time": 1792499042237,
    "data": {
      "content": [
        {
          "type": "text",
          "text": "讲讲三点"
        }
      ],
      "source": {
        "kind": "user"
      },
      "role": "user",
      "id": "47e9f7cc-b406-4ede-a4d7-46ce7e92ef1a"
    },
    "surfaceOp": "append"
  },
  {
    "type": "assistant/message",
    "seq": 30,
    "time": 1792499042268,
    "data": {
      "turn": 3,
      "step": 1,
      "message": {
        "role": "assistant",
        "content": [
          {
            "type": "text",
            "text": "第一点是"
          }
        ],
        "source": {
          "kind": "model",
          "provider": "mock",
          "model": "mock"
        },
        "id": "1efaba76-8071-4560-b571-b30599162a95"
      },
      "interrupted": true,
      "stream": [
        {
          "type": "chunk",
          "time": 1792499042237,
          "chunk": {
            "type": "block-start",
            "index": 0,
            "blockType": "text"
          }
        },
        {
          "type": "text-chunks",
          "time0": 1792499042237,
          "index": 0,
          "dt": [],
          "texts": [
            "第一点是"
          ]
        },
        {
          "type": "chunk",
          "time": 1792499042237,
          "chunk": {
            "type": "block-end",
            "index": 0,
            "block": {
              "type": "text",
              "text": "第一点是"
            }
          }
        },
        {
          "type": "chunk",
          "time": 1792499042237,
          "chunk": {
            "type": "block-start",
            "index": 1,
            "blockType": "tool-call"
          }
        },
        {
          "type": "tool-call-chunks",
          "time0": 1792499042237,
          "index": 1,
          "dt": [],
          "id": "c2",
          "name": "echo",
          "args": [
            "{\"te"
          ]
        }
      ]
    },
    "surfaceOp": "append"
  },
  {
    "type": "step/end",
    "seq": 31,
    "time": 1792499042268,
    "data": {
      "turn": 3,
      "step": 1
    }
  },
  {
    "type": "turn/end",
    "seq": 32,
    "time": 1792499042268,
    "data": {
      "turn": 3,
      "reason": {
        "kind": "aborted",
        "reason": {
          "kind": "user"
        }
      }
    }
  },
  {
    "type": "agent/inbox/spliced",
    "seq": 33,
    "time": 1792499042268,
    "data": {
      "target": "next-turn",
      "start": 0,
      "inserted": [
        {
          "content": [
            {
              "type": "text",
              "text": "第二点展开讲讲"
            }
          ],
          "source": {
            "kind": "user"
          },
          "role": "user",
          "id": "1d99f704-030c-4cd1-a9fc-94ae294efabd"
        }
      ]
    }
  },
  {
    "type": "turn/start",
    "seq": 34,
    "time": 1792499042268,
    "data": {
      "turn": 4
    }
  },
  {
    "type": "agent/inbox/spliced",
    "seq": 35,
    "time": 1792499042268,
    "data": {
      "target": "next-turn",
      "start": 0,
      "removedCount": 1,
      "inserted": []
    }
  },
  {
    "type": "step/start",
    "seq": 36,
    "time": 1792499042268,
    "data": {
      "turn": 4,
      "step": 1
    }
  },
  {
    "type": "user/message",
    "seq": 37,
    "time": 1792499042269,
    "data": {
      "content": [
        {
          "type": "text",
          "text": "第二点展开讲讲"
        }
      ],
      "source": {
        "kind": "user"
      },
      "role": "user",
      "id": "1d99f704-030c-4cd1-a9fc-94ae294efabd"
    },
    "surfaceOp": "append"
  },
  {
    "type": "assistant/message",
    "seq": 38,
    "time": 1792499042269,
    "data": {
      "turn": 4,
      "step": 1,
      "message": {
        "role": "assistant",
        "content": [
          {
            "type": "text",
            "text": "第二点是……"
          }
        ],
        "source": {
          "kind": "model",
          "provider": "mock",
          "model": "mock"
        },
        "id": "afc335c5-b171-42f9-8f19-f4b311e6e9d8"
      },
      "usage": {
        "inputTokens": 10,
        "outputTokens": 6
      },
      "stream": [
        {
          "type": "chunk",
          "time": 1792499042269,
          "chunk": {
            "type": "block-start",
            "index": 0,
            "blockType": "text"
          }
        },
        {
          "type": "text-chunks",
          "time0": 1792499042269,
          "index": 0,
          "dt": [
            0,
            0,
            0,
            0,
            0
          ],
          "texts": [
            "第",
            "二",
            "点",
            "是",
            "…",
            "…"
          ]
        },
        {
          "type": "chunk",
          "time": 1792499042269,
          "chunk": {
            "type": "block-end",
            "index": 0,
            "block": {
              "type": "text",
              "text": "第二点是……"
            }
          }
        },
        {
          "type": "chunk",
          "time": 1792499042269,
          "chunk": {
            "type": "usage",
            "usage": {
              "inputTokens": 10,
              "outputTokens": 6
            }
          }
        },
        {
          "type": "chunk",
          "time": 1792499042269,
          "chunk": {
            "type": "finish",
            "reason": {
              "kind": "stop"
            }
          }
        }
      ]
    },
    "surfaceOp": "append"
  },
  {
    "type": "step/end",
    "seq": 39,
    "time": 1792499042269,
    "data": {
      "turn": 4,
      "step": 1
    }
  },
  {
    "type": "turn/end",
    "seq": 40,
    "time": 1792499042269,
    "data": {
      "turn": 4,
      "reason": {
        "kind": "completed"
      }
    }
  }
]
崩溃那一刻的日志和补上的事件(crash-persisted.json、crash-closers.json)
[
  {
    "type": "agent/inbox/spliced",
    "seq": 0,
    "time": 1792499131141,
    "data": {
      "target": "next-turn",
      "start": 0,
      "inserted": [
        {
          "content": [
            {
              "type": "text",
              "text": "deploy to prod, then confirm"
            }
          ],
          "source": {
            "kind": "user"
          },
          "role": "user",
          "id": "12726a0f-10cb-4d96-ab91-6dc017182c76"
        }
      ]
    }
  },
  {
    "type": "turn/start",
    "seq": 1,
    "time": 1792499131141,
    "data": {
      "turn": 1
    }
  },
  {
    "type": "agent/inbox/spliced",
    "seq": 2,
    "time": 1792499131142,
    "data": {
      "target": "next-turn",
      "start": 0,
      "removedCount": 1,
      "inserted": []
    }
  },
  {
    "type": "step/start",
    "seq": 3,
    "time": 1792499131142,
    "data": {
      "turn": 1,
      "step": 1
    }
  },
  {
    "type": "system/message",
    "seq": 4,
    "time": 1792499131143,
    "data": {
      "turn": 1,
      "step": 1,
      "message": {
        "role": "system",
        "content": [
          {
            "type": "text",
            "text": "You are an AI agent powered by DeepSeek Harness.\n\nYou are a test agent."
          }
        ],
        "source": {
          "kind": "system-prompt"
        },
        "id": "c3486ecb-ea11-4a8c-a8be-f6f17fd6b10a"
      }
    },
    "surfaceOp": "append"
  },
  {
    "type": "user/message",
    "seq": 5,
    "time": 1792499131143,
    "data": {
      "content": [
        {
          "type": "text",
          "text": "deploy to prod, then confirm"
        }
      ],
      "source": {
        "kind": "user"
      },
      "role": "user",
      "id": "12726a0f-10cb-4d96-ab91-6dc017182c76"
    },
    "surfaceOp": "append"
  },
  {
    "type": "request/header",
    "seq": 6,
    "time": 1792499131143,
    "data": {
      "header": {
        "config": {
          "provider": "mock",
          "model": "mock"
        },
        "tools": [
          {
            "name": "deploy",
            "description": "deploy the service",
            "parameters": {
              "type": "object",
              "properties": {
                "env": {
                  "type": "string"
                }
              }
            }
          },
          {
            "name": "echo",
            "description": "echo back",
            "parameters": {
              "type": "object",
              "properties": {
                "text": {
                  "type": "string"
                }
              }
            }
          }
        ]
      },
      "reason": "initial"
    }
  },
  {
    "type": "request/context",
    "seq": 7,
    "time": 1792499131143,
    "data": {
      "provider": "mock",
      "model": "mock"
    }
  },
  {
    "type": "assistant/message",
    "seq": 8,
    "time": 1792499131144,
    "data": {
      "turn": 1,
      "step": 1,
      "message": {
        "role": "assistant",
        "content": [
          {
            "type": "tool-call",
            "id": "c1",
            "name": "deploy",
            "arguments": "{\"env\":\"prod\"}"
          },
          {
            "type": "tool-call",
            "id": "c2",
            "name": "echo",
            "arguments": "{\"text\":\"done?\"}"
          }
        ],
        "source": {
          "kind": "model",
          "provider": "mock",
          "model": "mock"
        },
        "id": "bee9714f-5d34-4bcd-90f9-17d793493200"
      },
      "usage": {
        "inputTokens": 10,
        "outputTokens": 5
      },
      "stream": [
        {
          "type": "chunk",
          "time": 1792499131144,
          "chunk": {
            "type": "block-start",
            "index": 0,
            "blockType": "tool-call"
          }
        },
        {
          "type": "tool-call-chunks",
          "time0": 1792499131144,
          "index": 0,
          "dt": [],
          "id": "c1",
          "name": "deploy",
          "args": [
            "{\"env\":\"prod\"}"
          ]
        },
        {
          "type": "chunk",
          "time": 1792499131144,
          "chunk": {
            "type": "block-end",
            "index": 0,
            "block": {
              "type": "tool-call",
              "id": "c1",
              "name": "deploy",
              "arguments": "{\"env\":\"prod\"}"
            }
          }
        },
        {
          "type": "chunk",
          "time": 1792499131144,
          "chunk": {
            "type": "block-start",
            "index": 1,
            "blockType": "tool-call"
          }
        },
        {
          "type": "tool-call-chunks",
          "time0": 1792499131144,
          "index": 1,
          "dt": [],
          "id": "c2",
          "name": "echo",
          "args": [
            "{\"text\":\"done?\"}"
          ]
        },
        {
          "type": "chunk",
          "time": 1792499131144,
          "chunk": {
            "type": "block-end",
            "index": 1,
            "block": {
              "type": "tool-call",
              "id": "c2",
              "name": "echo",
              "arguments": "{\"text\":\"done?\"}"
            }
          }
        },
        {
          "type": "chunk",
          "time": 1792499131144,
          "chunk": {
            "type": "usage",
            "usage": {
              "inputTokens": 10,
              "outputTokens": 5
            }
          }
        },
        {
          "type": "chunk",
          "time": 1792499131144,
          "chunk": {
            "type": "finish",
            "reason": {
              "kind": "tool-calls"
            }
          }
        }
      ]
    },
    "surfaceOp": "append"
  },
  {
    "type": "tool/call",
    "seq": 9,
    "time": 1792499131145,
    "data": {
      "turn": 1,
      "step": 1,
      "callId": "c1",
      "name": "deploy",
      "arguments": "{\"env\":\"prod\"}"
    }
  }
]
[
  {
    "type": "tool/result",
    "seq": 10,
    "time": 1792499131145,
    "data": {
      "turn": 1,
      "step": 1,
      "message": {
        "id": "interrupted-tool-result-c1-10",
        "role": "tool",
        "toolCallId": "c1",
        "isError": true,
        "source": {
          "kind": "tool",
          "callId": "c1"
        },
        "content": [
          {
            "type": "text",
            "text": "The tool call was interrupted after it was recorded, but no result was durably recorded. Its outcome is unknown. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly."
          }
        ]
      },
      "error": {
        "name": "ToolOutcomeUnknownError",
        "code": "TOOL_OUTCOME_UNKNOWN"
      }
    },
    "surfaceOp": "append",
    "sourceEventSeqs": [
      9
    ]
  },
  {
    "type": "tool/result",
    "seq": 11,
    "time": 1792499131145,
    "data": {
      "turn": 1,
      "step": 1,
      "message": {
        "id": "interrupted-tool-result-c2-11",
        "role": "tool",
        "toolCallId": "c2",
        "isError": true,
        "source": {
          "kind": "tool",
          "callId": "c2"
        },
        "content": [
          {
            "type": "text",
            "text": "The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed."
          }
        ]
      },
      "error": {
        "name": "ToolNotStartedError",
        "code": "TOOL_NOT_STARTED"
      }
    },
    "surfaceOp": "append"
  },
  {
    "type": "step/end",
    "seq": 12,
    "time": 1792499131145,
    "data": {
      "turn": 1,
      "step": 1
    }
  },
  {
    "type": "turn/end",
    "seq": 13,
    "time": 1792499131145,
    "data": {
      "turn": 1,
      "reason": {
        "kind": "interrupted"
      }
    }
  }
]
request-reconstruction.spec.ts 的完整测试输出
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request stability across the loop > each step request within a turn append-extends the previous, frozen end to end 18ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request stability across the loop > a later turn append-extends the previous turn (one conversation, one log) 4ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request stability across the loop > starts a new request series only when the admitted step explicitly asks for one 4ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request stability across the loop > retains the explicit series boundary when that request also changes its header 4ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request stability across the loop > keeps the series declaration when an outer listener rebuilds the enter decision 5ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request stability across the loop > logs adapter defaults, supports per-turn effort changes, and restores the effective value 11ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request stability across the loop > logs an adapter-owned maxTokens default before dispatch 3ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request stability across the loop > rematerializes the selected adapter maxTokens default after a provider switch 4ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request stability across the loop > preserves an explicit agent maxTokens cap across a provider switch 4ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request stability across the loop > keeps exact-model resolution, request logging, and dispatch on one adapter registration 3ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request stability across the loop > aborts a blocked reasoning lookup before quiescent disposal completes 3ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request stability across the loop > does not swallow a plain error from exact-model resolution 2ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request stability across the loop > does not swallow a LLM error from exact-model resolution 2ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request stability across the loop > lets a short-circuiting llm/stream listener own an unregistered route 2ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request stability across the loop > a compaction replace rewrites the resend, and the log explains it 3ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request stability across the loop > starts a new request series when compaction rewrites a retry in the same step 3ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request stability across the loop > a system-prompt change replaces surface node 0 and starts a new series under the same header 4ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request stability across the loop > on an in-history route a system-prompt change appends after the cached history under the same header 5ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request stability across the loop > on an in-history route a series start folds a prompt change into node 0 and empties later system nodes 4ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request stability across the loop > on an in-history route a compaction replace since the last request re-baselines node 0 3ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request stability across the loop > on an in-history route a tool-schema change re-baselines node 0 together with the changed header 3ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request stability across the loop > an inject() during the agent/request waterfall joins the NEXT request (the step/start boundary) 3ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request stability across the loop > a mutation attempt on the frozen request content throws into the step (loud, not silent) 2ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request stability across the loop > a fresh loop instance over a seeded log anchors with a resume snapshot and stays cache-aligned 6ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request stability across the loop > a delegating listener cannot mutate the seed through next() — the fold stays log-true 3ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request stability across the loop > THEOREM: every request rebuilds byte-equal from the session log alone 5ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request/context capacity records > records capacity once and skips it while the route is unchanged 3ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request/context capacity records > records a second capacity when the route changes mid-session 3ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request/context capacity records > records and deduplicates a route whose adapter advertises no capacity 3ms
✓ |thread-safe| packages/core/agent-loop/tests/request-reconstruction.spec.ts > request/context capacity records > clears a previous capacity when the next route advertises none 3ms
Test Files  1 passed (1)
Tests  30 passed (30)
对照:项目快照里一份 headless 组合的会话日志,摘成一行一个事件

多了产品组合会带上的东西:开头三条权限状态、一条 source=runtime-context 的运行时上下文、bash 作为后台任务登记和结算。

(header) version=4
permission/preset
sandbox/mode
approval/policy
agent/inbox/spliced
turn/start
agent/inbox/spliced
step/start
system/message       append  "{{system}}"
user/message         append  source=user: "Use the bash tool to run exactly: echo TERMINAL_OK. The
user/message         append  source=runtime-context: "Current runtime context. This snapshot supersedes earli
request/header               reason=initial
request/context
session/title
assistant/message    append  reasoning + tool-call bash
tool/call
job/registered
job/settled
tool/result          append  "TERMINAL_OK\n"
step/end
step/start
assistant/message    append  reasoning + text
step/end
turn/end                     reason=completed

  1. pi 的扩展文档(context、context_with_system 两个事件)和会话格式,经 Context7 读取,2026-10-20 读。 ↩

  2. dsh 的决策记录事件溯源的会话与派生消息历史,2026-10-20 读。 ↩

  3. dsh 的决策记录每个 LLM 请求都可从会话日志重建,2026-10-20 读。 ↩

  4. dsh 的决策记录Pi 扩展桥接,提案,2026-10-20 读。 ↩

  5. dsh 的决策记录被取消的流定稿其已送达前缀,2026-10-20 读。 ↩

  6. dsh 的决策记录加载时截断被中断的最终轮次,已否决,2026-10-20 读。 ↩

  7. dsh 的决策记录已发布 Session 格式通过有状态流式 Stage 迁移,2026-10-20 读。 ↩