



























@@ -743,6 +743,193 @@ describe("runAgentTurnWithFallback", () => {
743743expect(onPartialReply).not.toHaveBeenCalled();
744744});
745745746+it("bridges CLI assistant agent events into onReasoningStream for live reasoning preview (opus-4-7 text_delta path)", async () => {
747+state.isCliProviderMock.mockReturnValue(true);
748+state.runWithModelFallbackMock.mockImplementationOnce(async (params: FallbackRunnerParams) => ({
749+result: await params.run("claude-cli", "claude-opus-4-7"),
750+provider: "claude-cli",
751+model: "claude-opus-4-7",
752+attempts: [],
753+}));
754+state.runCliAgentMock.mockImplementationOnce(async (params: { runId: string }) => {
755+const realAgentEvents = await vi.importActual<typeof import("../../infra/agent-events.js")>(
756+"../../infra/agent-events.js",
757+);
758+realAgentEvents.emitAgentEvent({
759+runId: params.runId,
760+stream: "assistant",
761+data: { text: "Thinking", delta: "Thinking" },
762+});
763+realAgentEvents.emitAgentEvent({
764+runId: params.runId,
765+stream: "assistant",
766+data: { text: "Thinking about it", delta: " about it" },
767+});
768+return { payloads: [{ text: "Thinking about it" }], meta: {} };
769+});
770+771+const onReasoningStream = vi.fn<NonNullable<GetReplyOptions["onReasoningStream"]>>(
772+async (_payload) => undefined,
773+);
774+const runAgentTurnWithFallback = await getRunAgentTurnWithFallback();
775+const followupRun = createFollowupRun();
776+followupRun.run.provider = "claude-cli";
777+followupRun.run.model = "claude-opus-4-7";
778+779+await runAgentTurnWithFallback({
780+commandBody: "hi",
781+ followupRun,
782+sessionCtx: {
783+Provider: "telegram",
784+MessageSid: "msg",
785+} as unknown as TemplateContext,
786+opts: { onReasoningStream },
787+typingSignals: createMockTypingSignaler(),
788+blockReplyPipeline: null,
789+blockStreamingEnabled: false,
790+resolvedBlockStreamingBreak: "message_end",
791+applyReplyToMode: (payload) => payload,
792+shouldEmitToolResult: () => true,
793+shouldEmitToolOutput: () => false,
794+pendingToolTasks: new Set(),
795+resetSessionAfterCompactionFailure: async () => false,
796+resetSessionAfterRoleOrderingConflict: async () => false,
797+isHeartbeat: false,
798+sessionKey: "main",
799+getActiveSessionEntry: () => undefined,
800+resolvedVerboseLevel: "off",
801+});
802+803+const reasoningTexts = onReasoningStream.mock.calls.map((call) => call[0].text);
804+expect(reasoningTexts).toEqual(["Thinking", "Thinking about it"]);
805+});
806+807+it("does not bridge CLI assistant events to onReasoningStream when silentExpected is set", async () => {
808+state.isCliProviderMock.mockReturnValue(true);
809+state.runWithModelFallbackMock.mockImplementationOnce(async (params: FallbackRunnerParams) => ({
810+result: await params.run("claude-cli", "claude-opus-4-7"),
811+provider: "claude-cli",
812+model: "claude-opus-4-7",
813+attempts: [],
814+}));
815+state.runCliAgentMock.mockImplementationOnce(async (params: { runId: string }) => {
816+const realAgentEvents = await vi.importActual<typeof import("../../infra/agent-events.js")>(
817+"../../infra/agent-events.js",
818+);
819+realAgentEvents.emitAgentEvent({
820+runId: params.runId,
821+stream: "assistant",
822+data: { text: "heartbeat scratch text", delta: "heartbeat scratch text" },
823+});
824+realAgentEvents.emitAgentEvent({
825+runId: params.runId,
826+stream: "assistant",
827+data: { text: "NO_REPLY do not preview reasoning", delta: " do not preview reasoning" },
828+});
829+return { payloads: [{ text: "final" }], meta: {} };
830+});
831+832+const onReasoningStream = vi.fn<NonNullable<GetReplyOptions["onReasoningStream"]>>(
833+async (_payload) => undefined,
834+);
835+const runAgentTurnWithFallback = await getRunAgentTurnWithFallback();
836+const followupRun = createFollowupRun();
837+followupRun.run.provider = "claude-cli";
838+followupRun.run.model = "claude-opus-4-7";
839+followupRun.run.silentExpected = true;
840+841+await runAgentTurnWithFallback({
842+commandBody: "hi",
843+ followupRun,
844+sessionCtx: { Provider: "telegram", MessageSid: "msg" } as unknown as TemplateContext,
845+opts: { onReasoningStream },
846+typingSignals: createMockTypingSignaler(),
847+blockReplyPipeline: null,
848+blockStreamingEnabled: false,
849+resolvedBlockStreamingBreak: "message_end",
850+applyReplyToMode: (payload) => payload,
851+shouldEmitToolResult: () => true,
852+shouldEmitToolOutput: () => false,
853+pendingToolTasks: new Set(),
854+resetSessionAfterCompactionFailure: async () => false,
855+resetSessionAfterRoleOrderingConflict: async () => false,
856+isHeartbeat: false,
857+sessionKey: "main",
858+getActiveSessionEntry: () => undefined,
859+resolvedVerboseLevel: "off",
860+});
861+await new Promise((resolve) => setImmediate(resolve));
862+863+expect(onReasoningStream).not.toHaveBeenCalled();
864+});
865+866+it("does not double-fire onReasoningStream from the bridge when the API/native runtime path is active", async () => {
867+// API/native runtimes get reasoning content from real thinking_delta events
868+// already wired through the embedded harness. The CLI text_delta → reasoning
869+// bridge must be isCliProvider-gated so non-CLI runs don't see assistant
870+// text_delta forwarded as reasoning content (which would duplicate or
871+// pollute the reasoning lane).
872+state.isCliProviderMock.mockReturnValue(false);
873+state.runWithModelFallbackMock.mockImplementationOnce(async (params: FallbackRunnerParams) => ({
874+result: await params.run("anthropic", "claude-sonnet-4-7"),
875+provider: "anthropic",
876+model: "claude-sonnet-4-7",
877+attempts: [],
878+}));
879+state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => {
880+// Embedded harness emits assistant agent-events (the same bus the CLI
881+// bridge listens to). On the API path, those must NOT be re-emitted as
882+// reasoning content.
883+const realAgentEvents = await vi.importActual<typeof import("../../infra/agent-events.js")>(
884+"../../infra/agent-events.js",
885+);
886+realAgentEvents.emitAgentEvent({
887+runId: "api-run",
888+stream: "assistant",
889+data: { text: "assistant text from API run", delta: "assistant text from API run" },
890+});
891+await params.onAgentEvent?.({
892+stream: "assistant",
893+data: { text: "assistant text from API run", delta: "assistant text from API run" },
894+});
895+return { payloads: [{ text: "final" }], meta: {} };
896+});
897+898+const onReasoningStream = vi.fn<NonNullable<GetReplyOptions["onReasoningStream"]>>(
899+async (_payload) => undefined,
900+);
901+const runAgentTurnWithFallback = await getRunAgentTurnWithFallback();
902+const followupRun = createFollowupRun();
903+followupRun.run.provider = "anthropic";
904+followupRun.run.model = "claude-sonnet-4-7";
905+906+await runAgentTurnWithFallback({
907+commandBody: "hi",
908+ followupRun,
909+sessionCtx: { Provider: "telegram", MessageSid: "msg" } as unknown as TemplateContext,
910+opts: { onReasoningStream },
911+typingSignals: createMockTypingSignaler(),
912+blockReplyPipeline: null,
913+blockStreamingEnabled: false,
914+resolvedBlockStreamingBreak: "message_end",
915+applyReplyToMode: (payload) => payload,
916+shouldEmitToolResult: () => true,
917+shouldEmitToolOutput: () => false,
918+pendingToolTasks: new Set(),
919+resetSessionAfterCompactionFailure: async () => false,
920+resetSessionAfterRoleOrderingConflict: async () => false,
921+isHeartbeat: false,
922+sessionKey: "main",
923+getActiveSessionEntry: () => undefined,
924+resolvedVerboseLevel: "off",
925+});
926+await new Promise((resolve) => setImmediate(resolve));
927+928+// The bridge only runs in the CLI gate; the embedded API path does not
929+// call onReasoningStream from assistant text_delta.
930+expect(onReasoningStream).not.toHaveBeenCalled();
931+});
932+746933it("resolves CLI messageProvider from the live session surface when no origin channel is set", async () => {
747934state.isCliProviderMock.mockReturnValue(true);
748935state.runWithModelFallbackMock.mockImplementationOnce(async (params: FallbackRunnerParams) => ({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。