feat: wire Stream Event v1 transport and renderer log - #43
Conversation
helsome
left a comment
There was a problem hiding this comment.
这组 #41 → #42 → #43 的方向是对的,但当前还不建议把协议定型进 main,有两个架构阻塞需要先解决:
messageId === runId不应作为 v1 固化。#34 已明确要求 message / generation / run 是不同身份:message 有稳定 id,assistant generation 绑定具体 run。现在把两者合并会直接给 edit/regenerate/fork 留下错误语义。建议让 message 级事件携带真实 messageId;run 级事件可只依赖 runId,或 messageId 可选,但不要把两者定义成同一身份。- ADR 写了 reconnect=
lastSequencereplay,但当前 stack 只有 parallel event channel + renderer log/dedupe,没有 replay source/API/buffer,也没有“明确无法恢复”的失败路径。也就是说 contract 文档承诺了实现尚不存在的行为。
另外 #43 自己说明 #41 的 StreamEvent 不是正确 discriminated union,要到 #43 才修,所以不要单独 merge #41/#42 后在 main 留一个已知有缺陷的协议类型。
建议:在本 stack 顶部统一修掉 identity contract,并至少实现一个明确的 reconnect 行为(内存 replay 也可以,或明确 terminal failure),然后补 Issue #27 要求的真实 Copilot E2E:model + tool + citation,分别验证 cancel 和 transport interruption。完成后这组三层可以按依赖顺序合。
Addresses helsome#43 review: messageId is no longer fused with runId (issue helsome#34 - message/generation/run are distinct identities). message-level events carry the real assistant messageId, pre-assigned per run in RunManager; run-level events omit it; idempotency key is now runId + sequence. Adds StreamEventHistory: in-memory per-run tail used by RunManager.replayStream(runId, lastSequence) for reconnect resume, with an explicit recoverable:false path when the run is unknown or the tail is non-contiguous (eviction).
|
Thank you for the detailed review. Both blockers are now addressed at the top of this stack (new commits just pushed): 1. Identity contract (#34) —
2. Reconnect — implemented a concrete in-memory replay plus an explicit unrecoverable path:
Verification: adapter (7, incl. messageId injection) + history (5) + kernel E2E regression (6) all pass; electron/core/ui/shared tsc show no new errors beyond the pre-existing local On the real Copilot E2E (model + tool + citation, cancel and transport interruption): I'll wire this next against the eval fixture infrastructure so it runs without external API keys — will follow up in this thread before we merge. |
作者已解决原 review 的两个实质 blocker(message/run identity 与 reconnect replay)。重新评估后,真实完整 E2E 不再作为当前合入前置条件;剩余仅需解决与最新 main 的 Git 冲突并通过基础 CI。
helsome
left a comment
There was a problem hiding this comment.
重新审查后通过代码方向。你已经解决了上一轮的两个核心 blocker:messageId / runId 身份拆分,以及明确的 in-memory replay / unrecoverable reconnect 路径。完整真实 Copilot E2E 可以作为后续增强,不再作为当前合入前置条件。现在只剩 Git 层面的 dirty:请把最终 #43 rebase 到最新 main,保留 Run Budget / Provider Router 等主干后续改动,并让基础 CI(unit/typecheck/secret scan)跑通;完成后可直接合。
f345099 to
d953606
Compare
Addresses helsome#43 review: messageId is no longer fused with runId (issue helsome#34 - message/generation/run are distinct identities). message-level events carry the real assistant messageId, pre-assigned per run in RunManager; run-level events omit it; idempotency key is now runId + sequence. Adds StreamEventHistory: in-memory per-run tail used by RunManager.replayStream(runId, lastSequence) for reconnect resume, with an explicit recoverable:false path when the run is unknown or the tail is non-contiguous (eviction).
d953606 to
05ae328
Compare
Addresses helsome#43 review: messageId is no longer fused with runId (issue helsome#34 - message/generation/run are distinct identities). message-level events carry the real assistant messageId, pre-assigned per run in RunManager; run-level events omit it; idempotency key is now runId + sequence. Adds StreamEventHistory: in-memory per-run tail used by RunManager.replayStream(runId, lastSequence) for reconnect resume, with an explicit recoverable:false path when the run is unknown or the tail is non-contiguous (eviction).
517434d to
b9e2cc5
Compare
审核标准已更新:Ready PR 在 APPROVE 前必须补齐可复现测试报告(至少 Bun 版本、OS、实际命令与结果)。原审批基于旧标准,先撤销,代码方向本身仍认可。
helsome
left a comment
There was a problem hiding this comment.
代码方向仍然认可,上一轮 identity/reconnect blocker 已修;这次 Changes Requested 只是同步当前统一验收规则。请在 clean rebase 最新 main 时,把测试报告补成可复现格式:Bun 版本、OS/平台、实际 focused test/typecheck 命令与 pass/fail;现有 protocol/renderer/kernel 测试范围够了,不需要补完整真实 Copilot E2E。rebase + 基础 CI + 报告字段齐全后即可恢复 APPROVE。
Introduce the pure-type foundation for the structured streaming event protocol (issue helsome#27): versioned envelope, 12 typed events, and the idempotency/cancel/reconnect contracts as types. Zero runtime change. Adds ADR 0001 documenting context, decision, migration path and open questions for maintainer review.
Emit Stream Event Protocol v1 events alongside the existing AgentEvent stream (issue helsome#27, ADR 0001 migration step 2). Adds toStreamEvents mapping (8 AgentEvent types -> 12 protocol events, cancel normalized to 'cancelled') and RunManager.subscribeStream. Existing AgentEvent consumers are untouched; the parallel channel only activates when a stream subscriber is registered.
StreamEvent was a single indexed-union instantiation (Tagged StreamEventEnvelope<StreamEventType>), so payload could not be narrowed by type in switch/if. Rewrite as a distributive mapped union; on-disk type shape is unchanged. Adjusts adapter unit-test helper accordingly.
KernelHost subscribes RunManager.subscribeStream (issue helsome#27) and forwards { sessionId, event } over IPC channel 'agent:stream'; preload exposes electronAPI.kernel.onStreamEvent. Legacy 'agent:event' delivery untouched. Transport only; renderer consumption follows.
Renderer-side data layer for issue helsome#27: KernelBridge subscribes client.kernel.onStreamEvent into a parallel StreamEvent log. reduceStreamLog keeps per-run events ordered by sequence, dedupes replays (drops), and flags gaps/out-of-order (anomalies) as a protocol health signal. FinagentClient and preload.cjs wire onStreamEvent; FinagentClient adds onStreamEvent contract with fallback noop. No visual change: existing AgentEvent rendering untouched.
Addresses helsome#43 review: messageId is no longer fused with runId (issue helsome#34 - message/generation/run are distinct identities). message-level events carry the real assistant messageId, pre-assigned per run in RunManager; run-level events omit it; idempotency key is now runId + sequence. Adds StreamEventHistory: in-memory per-run tail used by RunManager.replayStream(runId, lastSequence) for reconnect resume, with an explicit recoverable:false path when the run is unknown or the tail is non-contiguous (eviction).
Adds 'runs:stream-replay' handler (KernelHost.streamReplay -> RunManager.replayStream), preload (ts + cjs) streamReplay, finagentClient wiring and FinagentClient contract with fallback noop.
…plementation Envelope messageId is now optional (message-level events only); idempotency key runId+sequence; Reconnect row documents implemented StreamEventHistory + IPC with explicit unrecoverable path; open question 3 resolved.
run_started(seq 1)与 runtime 自产的首个事件(同样 seq 1)冲突, 导致 replay(runId, 0) 被判为不连续(recoverable: false),renderer 的 幂等去重也会误丢事件。RunManager 现为每个 run 持有 RunProtocol 计数器, 扇出前把所有 AgentEvent 统一重排为 1..N;messageId 随该对象传递, 崩溃/取消兜底合成的 terminal 事件也保住 helsome#34 的身份契约。replay 游标 超出已知最大 sequence 时,由静默视为已同步改为明确不可恢复。
kernel 级(stream-replay.e2e.test.ts):真实 LocalRuntimeAdapter + RunManager + 持久化。六个用例:完整 run 后全量补发、无实时订阅者时 事件仍入历史、按 lastSequence 中途断线补发、取消路径产生可补发的显式 cancelled 事件、带工具 run 的全量流、未知 run 与伪造游标返回明确的 不可恢复路径。 app 级(e2e/stream-replay.mjs):真实 Electron + preload IPC(CDP)。 四个用例:replay(runId, 0) 与实时投递逐字节一致、断线补发拼接还原 完整流、未知 run 跨 IPC 返回不可恢复、非法 lastSequence 被拒以 INVALID_ARGUMENT。
AgentKernelHost.attach wires the Stream Event v1 channel through kernel.runs.subscribeStream; the fake kernel in kernelHost.test.ts lacked the method, so the transport test crashed with TypeError before asserting. Align the fake with the kernel surface (subscribeStream + replayStream) so focused CI runs green again.
The Stream Event v1 IPC surface (streamReplay / onStreamEvent) landed in the kernel channel types; the test kernel client was not updated, which broke the ui + i18n + electron typecheck gates. Align the stub with the channel so typecheck is green again.
streamReplay returned an inferred StreamReplayResult that TS could not name portably across the @finagent/shared boundary (error TS2742). Export the type from the shared package and annotate the kernelHost surface so every package typecheck gate passes.
b9e2cc5 to
e0d46c4
Compare
|
这轮复查有两个更新:
合并策略调整为:#43 clean 后直接以 stack 顶部合入 #41→#42→#43 的完整协议增量,不再分别 squash #41/#42,避免重复历史;随后再处理 #76 的 persistence-only delta。 |
Summary
Wires the Stream Event Protocol v1 across transport and renderer (issue #27), additive to the existing
AgentEventpath:Transport (#43)
apps/electron/src/main/kernelHost.ts:attach()also subscribesRunManager.subscribeStreamand forwards{ sessionId, event }over a new IPC channelagent:stream(cleaned up on re-attach). Legacyagent:eventuntouched.apps/electron/src/preload/index.ts+index.cjs: exposekernel.onStreamEvent(cb) -> unsubscribe, mirroringonAgentEvent.Renderer data layer (#43)
packages/ui/src/atoms/streamAtoms.ts(new): purereduceStreamLogkeeps per-run events ordered bysequence, dedupes replays (idempotency requirement) into adropscounter, and flags gaps/out-of-order delivery asanomalies. Exposed as JotaistreamLogAtom/applyStreamEventAtom.KernelBridgesubscribesonStreamEventinto the log; the legacyagent:event→applyAgentEventAtompath is untouched.FinagentClientinterface + fallback noop +finagentClient.tswiring.Type fix (#43)
packages/core/src/stream-events.ts:StreamEventis now a true discriminated union (distributive mapped type) sopayloadnarrows bytype— the feat(core): add Stream Event Protocol v1 types (ADR 0001) #41 definition failed tsc narrowing. Shape unchanged.No visual change — renderer data layer only; rendering from
text_delta(and the explicitcancelledstate) is the follow-up, best after the protocol open questions settle.Verification
stream-events.test.ts+stream-event-adapter.test.ts→ 10 passedstreamAtoms.test.ts(5) +sessionAtoms.test.ts(5) passedagent-kernel.test.ts→ 6 passedtsc --noEmitfor electron/core/ui/shared → no new errors beyond the known pre-existingi18nextresolution issue (local bun 1.4 layout)Dependency
Builds on #42 (RunManager stream channel) → #41 (protocol types). The discriminated-union fix lives here so #41 merges as-is.
Open questions
Same as ADR 0001 (resume data source,
status.phase,messageIdvsrunId).