perf: skip per-chunk live parsing for subagents · openclaw/openclaw@1876e3e
2026-06-23
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
@@ -39,6 +39,7 @@ import { runEmbeddedAgent, type EmbeddedAgentRunResult } from "../embedded-agent
|
39 | 39 | import { FailoverError } from "../failover-error.js"; |
40 | 40 | import { runAgentHarnessBeforeMessageWriteHook } from "../harness/hook-helpers.js"; |
41 | 41 | import { resolveAvailableAgentHarnessPolicy } from "../harness/selection.js"; |
| 42 | +import { AGENT_LANE_SUBAGENT } from "../lanes.js"; |
42 | 43 | import { resolveCliRuntimeExecutionProvider } from "../model-runtime-aliases.js"; |
43 | 44 | import { isCliProvider } from "../model-selection.js"; |
44 | 45 | import { resolveOpenAIRuntimeProvider } from "../openai-routing.js"; |
@@ -804,6 +805,9 @@ export function runAgentAttempt(params: {
|
804 | 805 | runId: params.runId, |
805 | 806 | lifecycleGeneration: params.lifecycleGeneration, |
806 | 807 | lane: params.opts.lane, |
| 808 | +// Subagents have no live stream consumer (result is read back from the |
| 809 | +// persisted transcript), so skip per-chunk live visible-text parsing. |
| 810 | +suppressLiveStreamOutput: params.opts.lane === AGENT_LANE_SUBAGENT, |
807 | 811 | abortSignal: params.opts.abortSignal, |
808 | 812 | extraSystemPrompt: params.opts.extraSystemPrompt, |
809 | 813 | bootstrapContextMode: params.opts.bootstrapContextMode, |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -27,6 +27,7 @@ function firstAttemptParams(): {
|
27 | 27 | modelRun?: boolean; |
28 | 28 | promptMode?: string; |
29 | 29 | promptCacheKey?: string; |
| 30 | +suppressLiveStreamOutput?: boolean; |
30 | 31 | } { |
31 | 32 | const call = mockedRunEmbeddedAttempt.mock.calls[0] as |
32 | 33 | | [ |
@@ -35,6 +36,7 @@ function firstAttemptParams(): {
|
35 | 36 | modelRun?: boolean; |
36 | 37 | promptMode?: string; |
37 | 38 | promptCacheKey?: string; |
| 39 | +suppressLiveStreamOutput?: boolean; |
38 | 40 | }, |
39 | 41 | ] |
40 | 42 | | undefined; |
@@ -187,4 +189,15 @@ describe("runEmbeddedAgent cron before_agent_reply seam", () => {
|
187 | 189 | |
188 | 190 | expect(firstAttemptParams().promptCacheKey).toBe("cron-cache-key"); |
189 | 191 | }); |
| 192 | + |
| 193 | +it("forwards suppressed live stream output into the embedded attempt", async () => { |
| 194 | +mockedRunEmbeddedAttempt.mockResolvedValueOnce(makeAttemptResult({ promptError: null })); |
| 195 | + |
| 196 | +await runEmbeddedAgent({ |
| 197 | + ...overflowBaseRunParams, |
| 198 | +suppressLiveStreamOutput: true, |
| 199 | +}); |
| 200 | + |
| 201 | +expect(firstAttemptParams().suppressLiveStreamOutput).toBe(true); |
| 202 | +}); |
190 | 203 | }); |
| Original file line number | Diff line number | Diff line change |
|---|
@@ -2181,6 +2181,7 @@ async function runEmbeddedAgentInternal(
|
2181 | 2181 | ownerNumbers: params.ownerNumbers, |
2182 | 2182 | enforceFinalTag: params.enforceFinalTag, |
2183 | 2183 | silentExpected: params.silentExpected, |
| 2184 | +suppressLiveStreamOutput: params.suppressLiveStreamOutput, |
2184 | 2185 | bootstrapContextMode: params.bootstrapContextMode, |
2185 | 2186 | bootstrapContextRunKind: params.bootstrapContextRunKind, |
2186 | 2187 | jobId: params.jobId, |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -3606,6 +3606,7 @@ export async function runEmbeddedAttempt(
|
3606 | 3606 | }, |
3607 | 3607 | enforceFinalTag: params.enforceFinalTag, |
3608 | 3608 | silentExpected: params.silentExpected, |
| 3609 | +suppressLiveStreamOutput: params.suppressLiveStreamOutput, |
3609 | 3610 | config: params.config, |
3610 | 3611 | sessionKey: sandboxSessionKey, |
3611 | 3612 | currentChannelId: params.currentChannelId, |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -256,6 +256,8 @@ export type RunEmbeddedAgentParams = {
|
256 | 256 | ownerNumbers?: string[]; |
257 | 257 | enforceFinalTag?: boolean; |
258 | 258 | silentExpected?: boolean; |
| 259 | +/** Skip per-chunk live visible-text parsing when no live stream consumer exists (e.g. subagents). */ |
| 260 | +suppressLiveStreamOutput?: boolean; |
259 | 261 | /** |
260 | 262 | * Treat a clean empty assistant stop as an intentional silent reply. |
261 | 263 | * Only set when the caller's prompt policy already allows an exact NO_REPLY |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -694,15 +694,23 @@ export function handleMessageUpdate(
|
694 | 694 | if (isPhasePendingOpenAiResponsesTextItem) { |
695 | 695 | return; |
696 | 696 | } |
| 697 | +// Subagents have no live consumer; their final result is delivered from |
| 698 | +// message_end. Keep accumulating deltaBuffer, but skip per-chunk visible-text |
| 699 | +// parsing so long parallel subagent streams do not monopolize the event loop. |
| 700 | +const skipLiveStream = ctx.params.suppressLiveStreamOutput === true; |
697 | 701 | const shouldUsePhaseAwareBlockReply = Boolean(deliveryPhase); |
698 | 702 | |
699 | 703 | if (chunk) { |
700 | 704 | ctx.state.deltaBuffer += chunk; |
701 | | -if (!shouldUsePhaseAwareBlockReply) { |
| 705 | +if (!skipLiveStream && !shouldUsePhaseAwareBlockReply) { |
702 | 706 | appendBlockReplyChunk(ctx, chunk); |
703 | 707 | } |
704 | 708 | } |
705 | 709 | |
| 710 | +if (skipLiveStream) { |
| 711 | +return; |
| 712 | +} |
| 713 | + |
706 | 714 | if (!suppressMessageToolOnlySourceReplyOutput && ctx.state.streamReasoning) { |
707 | 715 | // Handle partial <think> tags: stream whatever reasoning is visible so far. |
708 | 716 | ctx.emitReasoningStream(extractThinkingFromTaggedStream(ctx.state.deltaBuffer)); |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -359,6 +359,29 @@ describe("subscribeEmbeddedAgentSession", () => {
|
359 | 359 | }, |
360 | 360 | ); |
361 | 361 | |
| 362 | +it("suppressLiveStreamOutput skips per-chunk preview but still delivers final text", () => { |
| 363 | +const onAgentEvent = vi.fn(); |
| 364 | +const { emit } = createSubscribedHarness({ |
| 365 | +runId: "run", |
| 366 | + onAgentEvent, |
| 367 | +suppressLiveStreamOutput: true, |
| 368 | +}); |
| 369 | + |
| 370 | +emit({ type: "message_start", message: { role: "assistant" } }); |
| 371 | +emitAssistantTextDelta(emit, "Hello "); |
| 372 | +emitAssistantTextDelta(emit, "world"); |
| 373 | + |
| 374 | +// No live preview events while suppressed (the per-chunk parsing path is skipped). |
| 375 | +expect(extractAgentEventPayloads(onAgentEvent.mock.calls)).toHaveLength(0); |
| 376 | + |
| 377 | +const assistantMessage = { |
| 378 | +role: "assistant", |
| 379 | +content: [{ type: "text", text: "Hello world" }], |
| 380 | +} as AssistantMessage; |
| 381 | +emit({ type: "message_end", message: assistantMessage }); |
| 382 | +expectSingleAgentEventText(onAgentEvent.mock.calls, "Hello world"); |
| 383 | +}); |
| 384 | + |
362 | 385 | it("blocks local MEDIA urls from case-variant tool names in verbose output", async () => { |
363 | 386 | const onToolResult = vi.fn(); |
364 | 387 | const { emit } = createSubscribedHarness({ |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -95,6 +95,13 @@ export type SubscribeEmbeddedAgentSessionParams = {
|
95 | 95 | onBeforeLifecycleTerminal?: () => void | Promise<void>; |
96 | 96 | enforceFinalTag?: boolean; |
97 | 97 | silentExpected?: boolean; |
| 98 | +/** |
| 99 | + * Skip per-chunk live visible-text parsing in handleMessageUpdate. Set for runs |
| 100 | + * with no live stream consumer — notably subagents, whose result is read back |
| 101 | + * from the final message_end path. Suppressing intermediate passes does not |
| 102 | + * change final output. |
| 103 | + */ |
| 104 | +suppressLiveStreamOutput?: boolean; |
98 | 105 | config?: OpenClawConfig; |
99 | 106 | sessionKey?: string; |
100 | 107 | /** Current transport channel resolved for this run. */ |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。