Feature: Executor.execute_stream() - token-by-token streaming delivery As the CleverThis router I want to call executor.execute_stream(message) and receive tokens one-by-one So that I can stream partial responses to end-users for a better UX on long LLM calls Background: Given the execute_stream test context is initialised # ── Executor.last_result attribute ─────────────────────────────────────── Scenario: Executor.last_result is None before any execute_stream call Given an Executor with a basic llm config (stream) Then executor.last_result should be None (stream) # ── LLM actor streaming ────────────────────────────────────────────────── Scenario: execute_stream on LLM actor yields string tokens and populates last_result Given an Executor with a basic llm config (stream) And a mock astream yielding tokens "Hello", " World", "!" with no usage (stream) When I call execute_stream with message "Hi" (stream) Then the collected tokens should be ["Hello", " World", "!"] (stream) And executor.last_result should be an ActorResult (stream) And executor.last_result.response should equal "Hello World!" (stream) Scenario: execute_stream populates last_result token counts from final chunk usage_metadata Given an Executor with a basic llm config (stream) And a mock astream yielding single token "Hi" with usage prompt=5 completion=10 (stream) When I call execute_stream with message "test" (stream) Then executor.last_result.prompt_tokens should be 5 (stream) And executor.last_result.completion_tokens should be 10 (stream) Scenario: execute_stream last_result token counts fall back to 0 when no usage_metadata Given an Executor with a basic llm config (stream) And a mock astream yielding single token "response" with no usage (stream) When I call execute_stream with message "test" (stream) Then executor.last_result.prompt_tokens should be 0 (stream) And executor.last_result.completion_tokens should be 0 (stream) Scenario: execute_stream last_result is cleared to None when a new stream starts Given an Executor with a basic llm config (stream) And a mock astream yielding tokens "Hello", " World" with no usage (stream) And a prior successful execute_stream has populated last_result (stream) When I start iterating execute_stream and stop after first token (stream) Then executor.last_result should be None (stream) # ── M1 fix: timeout_ms enforced on LLM streaming path (AC5) ────────────── Scenario: execute_stream on LLM actor raises ExecutionError when timeout_ms is exceeded Given an LLM Executor with timeout_ms 50 and a slow mock astream (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "timeout" should be raised (stream) Scenario: execute_stream on LLM actor raises ExecutionError for bool timeout_ms Given an LLM Executor with bool_true timeout_ms (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "timeout" should be raised (stream) Scenario: execute_stream on LLM actor raises ExecutionError for zero timeout_ms Given an LLM Executor with zero timeout_ms (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "timeout" should be raised (stream) Scenario: execute_stream on LLM actor raises ExecutionError for non-numeric timeout_ms Given an LLM Executor with string timeout_ms (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "timeout" should be raised (stream) Scenario: execute_stream on LLM actor completes normally when within timeout_ms Given an LLM Executor with timeout_ms 2000 and a fast mock astream (stream) When I call execute_stream with message "test" (stream) Then executor.last_result should be an ActorResult (stream) # ── M1 fix: max_cost_usd enforced on LLM streaming path (AC5) ──────────── Scenario: execute_stream on LLM actor raises ExecutionError when max_cost_usd is exceeded Given an LLM Executor with max_cost_usd 0.0 and pricing and a token-bearing mock astream (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "cost" should be raised (stream) And executor.last_result should be an ActorResult (stream) Scenario: execute_stream on LLM actor raises ExecutionError for bool max_cost_usd Given an LLM Executor with bool max_cost_usd and pricing (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "cost" should be raised (stream) And executor.last_result should be an ActorResult (stream) Scenario: execute_stream on LLM actor raises ExecutionError for non-numeric max_cost_usd Given an LLM Executor with non-numeric max_cost_usd and pricing (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "cost" should be raised (stream) And executor.last_result should be an ActorResult (stream) Scenario: execute_stream on LLM actor raises ExecutionError for missing provider pricing Given an LLM Executor with max_cost_usd 1.0 and missing provider pricing (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "cost" should be raised (stream) And executor.last_result should be an ActorResult (stream) Scenario: execute_stream on LLM actor raises ExecutionError for missing model pricing Given an LLM Executor with max_cost_usd 1.0 and missing model pricing (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "cost" should be raised (stream) And executor.last_result should be an ActorResult (stream) Scenario: execute_stream on LLM actor raises ExecutionError for incomplete pricing entry Given an LLM Executor with max_cost_usd 1.0 and incomplete pricing entry (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "cost" should be raised (stream) And executor.last_result should be an ActorResult (stream) Scenario: execute_stream on LLM actor raises ExecutionError for invalid pricing rate Given an LLM Executor with max_cost_usd 1.0 and invalid pricing rate (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "cost" should be raised (stream) And executor.last_result should be an ActorResult (stream) Scenario: execute_stream on LLM actor completes normally when cost is within max_cost_usd Given an LLM Executor with max_cost_usd 1.0 and pricing and a token-bearing mock astream (stream) When I call execute_stream with message "test" (stream) Then executor.last_result should be an ActorResult (stream) # ── m1 fix: executor.last_result populated when create_agent raises on LLM path ─ Scenario: execute_stream on LLM actor populates last_result with no_llm placeholder when create_agent raises ConfigurationError Given an LLM Executor where create_agent raises ConfigurationError (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then a ConfigurationError should be raised (stream) And executor.last_result should be an ActorResult (stream) And executor.last_result.nodes should contain a no_llm placeholder (stream) # ── m2 fix: billing integrity when build_chat_model raises ConfigurationError ─ Scenario: execute_stream on LLM actor populates last_result with zero tokens when build_chat_model raises ConfigurationError during lazy init Given an LLM Executor where build_chat_model raises ConfigurationError during lazy init (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then a ConfigurationError should be raised (stream) And executor.last_result should be an ActorResult (stream) And executor.last_result token counts should be zero (stream) # ── Graph actor streaming ───────────────────────────────────────────────── Scenario: execute_stream on graph actor yields tokens from terminal node Given an Executor with a graph config that has a single agent node (stream) And a mock astream yielding tokens "Graph", " response" with no usage (stream) When I call execute_stream with message "hello graph" (stream) Then the collected tokens should be ["Graph", " response"] (stream) And executor.last_result should be an ActorResult (stream) And executor.last_result.response should equal "Graph response" (stream) Scenario: execute_stream on graph actor populates last_result with node usages Given an Executor with a graph config that has a single agent node (stream) And a mock astream yielding single token "Answer" with no usage (stream) When I call execute_stream with message "test graph" (stream) Then executor.last_result should have at least one NodeUsage entry (stream) # NOTE: This scenario verifies the end-state only (final token list and # concatenated response), not the internal buffering behaviour. Both the # fast path (immediate yield) and the slow path (buffer-then-yield) produce # the same final token list, so a regression that removes the buffering code # and takes the fast path would not be caught here. The buffering path is # exercised by the _all_edges_unconditional=False branch in pure_graph.py; # the correctness of that branch is verified by the C1 fix assertion # (executor.last_result.response == "Buffered") which would fail if # full_response were not set from the LLM response. Scenario: execute_stream on terminal AGENT node with conditional edge buffers tokens before yielding Given an Executor with a graph config that has a terminal agent node with conditional edge (stream) And a mock astream yielding tokens "Buf", "fered" with no usage (stream) When I call execute_stream with message "test conditional" (stream) Then the collected tokens should be ["Buf", "fered"] (stream) And executor.last_result should be an ActorResult (stream) And executor.last_result.response should equal "Buffered" (stream) # ── LLMAgent.stream_message() ────────────────────────────────────────────── Scenario: LLMAgent.stream_message yields chunks from astream Given an LLMAgent with a mock astream that yields "tok1", "tok2", "tok3" When I call stream_message with a string message Then the yielded tokens should be ["tok1", "tok2", "tok3"] Scenario: LLMAgent.stream_message captures token counts from final chunk usage_metadata Given an LLMAgent with a mock astream whose last chunk has usage_metadata prompt=7 completion=13 When I call stream_message with a string message Then _last_token_usage should be (7, 13) after stream_message And last_token_usage_var was (7, 13) inside the async step Scenario: LLMAgent.stream_message resets token counts to (0,0) at the start Given an LLMAgent with stale _last_token_usage (100, 200) And a mock astream yielding empty sequence (stream-agent) When I call stream_message with a string message Then _last_token_usage should be zero after stream_message Scenario: LLMAgent.stream_message falls back to (0,0) with warning when no usage_metadata Given an LLMAgent with a mock astream that yields a chunk with no usage_metadata When I call stream_message with a string message Then _last_token_usage should be zero after stream_message And a no-usage warning was emitted during stream_message # N1: usage_metadata present but empty dict {} Scenario: LLMAgent.stream_message falls back to (0,0) when usage_metadata is empty dict Given an LLMAgent with a mock astream that yields a chunk with empty usage_metadata dict When I call stream_message with a string message Then _last_token_usage should be zero after stream_message And a no-usage warning was emitted during stream_message # N2: response_metadata present but not a dict Scenario: LLMAgent.stream_message falls back to (0,0) when response_metadata is not a dict Given an LLMAgent with a mock astream that yields a chunk with non-dict response_metadata When I call stream_message with a string message Then _last_token_usage should be zero after stream_message And a no-usage warning was emitted during stream_message # N2: response_metadata is a dict but has no token_usage key Scenario: LLMAgent.stream_message falls back to (0,0) when response_metadata has no token_usage key Given an LLMAgent with a mock astream that yields a chunk with response_metadata missing token_usage When I call stream_message with a string message Then _last_token_usage should be zero after stream_message And a no-usage warning was emitted during stream_message # n5: _CAUSE_RESPONSE_METADATA_MISSING branch — chunk has neither usage_metadata # nor response_metadata attributes (SimpleNamespace with content only) Scenario: LLMAgent.stream_message falls back to (0,0) when chunk has no response_metadata attribute Given an LLMAgent with a mock astream that yields a chunk with no response_metadata attribute When I call stream_message with a string message Then _last_token_usage should be zero after stream_message And a no-usage warning was emitted during stream_message # ── Limit enforcement for streaming ─────────────────────────────────────── Scenario: execute_stream on graph respects timeout_ms limit Given an Executor with a graph config and timeout_ms 50 (stream) And a mock astream that takes too long (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "timeout" should be raised (stream) Scenario: execute_stream on graph respects max_model_calls limit Given an Executor with a graph config and max_model_calls 0 (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "model_calls" should be raised (stream) # ── Tool/multi-actor not supported ───────────────────────────────────────── Scenario: execute_stream raises ConfigurationError for tool actor type Given an Executor with a tool actor config (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then a ConfigurationError should be raised about unsupported streaming type (stream) Scenario: execute_stream raises ConfigurationError for multi_actor type Given an Executor with a multi_actor config (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then a ConfigurationError should be raised about unsupported streaming type (stream) # ── Non-AGENT node path in _stream_from_node() ─────────────────────────── Scenario: execute_stream on graph with non-LLM terminal function node yields output Given an Executor with a pure function graph config (stream) When I call execute_stream with message "test function" (stream) Then executor.last_result should be an ActorResult (stream) And executor.last_result should have a no_llm placeholder node (stream) Scenario: execute_stream on graph with function then agent node exercises non-agent path Given an Executor with a function-then-agent graph config (stream) And a mock astream yielding single token "Final answer" with no usage (stream) When I call execute_stream with message "test mixed" (stream) Then executor.last_result should be an ActorResult (stream) And executor.last_result.response should equal "Final answer" (stream) # ── execute_stream with conversation history (messages param) ───────────── Scenario: execute_stream on LLM actor with conversation history messages Given an Executor with a basic llm config (stream) And a mock astream yielding single token "Reply" with no usage (stream) When I run execute_stream with history on executor (stream) Then the collected tokens should be ["Reply"] (stream) And executor.last_result should be an ActorResult (stream) # ── Node.stream_agent() error and non-LLM paths ────────────────────────── Scenario: Node._stream_agent() propagates exception when streaming fails Given a Node stream_agent that raises during streaming (stream) Then the exception should be propagated from stream_agent (stream) # ── _execute_llm_stream() with conversation history ─────────────────────── Scenario: _execute_llm_stream with messages conversation history sets last_result Given an Executor with a basic llm config (stream) And a mock astream yielding single token "ConvReply" with no usage (stream) When I run execute_stream with messages param on executor (stream) Then executor.last_result should be an ActorResult (stream) # ── execute_stream with initial_state ──────────────────────────────────── Scenario: execute_stream on graph with initial_state restores graph state Given an Executor with a graph config that has a single agent node (stream) And a mock astream yielding single token "Resumed" with no usage (stream) When I run execute_stream with initial_state on graph executor (stream) Then executor.last_result should be an ActorResult (stream) And executor.last_result.response should equal "Resumed" (stream) # ── Invalid timeout_ms in execute_stream ───────────────────────────────── Scenario: execute_stream with bool timeout_ms raises ExecutionError kind timeout Given an Executor with a graph config and bool_true timeout_ms (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "timeout" should be raised (stream) Scenario: execute_stream with zero timeout_ms raises ExecutionError kind timeout Given an Executor with a graph config and zero timeout_ms (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "timeout" should be raised (stream) # ── TOOL node limit enforcement in streaming ────────────────────────────── Scenario: execute_stream on graph with TOOL node respects max_tool_calls 0 limit Given an Executor with a tool-node graph and max_tool_calls 0 (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "tool_calls" should be raised (stream) # ── _execute_llm_stream() error paths ──────────────────────────────────── Scenario: _execute_llm_stream raises ExecutionError when stream_message raises Given an Executor with a basic llm config (stream) And a mock astream that raises RuntimeError (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError should be raised from _execute_llm_stream (stream) # ── Node.stream_agent() without current_message ────────────────────────── Scenario: Node.stream_agent() uses last user message when current_message absent Given a Node stream_agent without current_message in state (stream) Then stream_agent yields tokens from last user message context (stream) # ── execute_stream with graph and conversation_history ─────────────────── Scenario: execute_stream on graph with messages parameter passes history Given an Executor with a graph config that has a single agent node (stream) And a mock astream yielding single token "WithHistory" with no usage (stream) When I run execute_stream on graph with messages param (stream) Then executor.last_result should be an ActorResult (stream) And executor.last_result.response should equal "WithHistory" (stream) # ── Two-AGENT sequential (intermediate AGENT path) ─────────────────────── Scenario: execute_stream on two-AGENT sequential graph yields terminal tokens only Given an Executor with a two-agent sequential graph config (stream) And a mock astream yielding single token "TerminalToken" with no usage (stream) When I call execute_stream with message "test sequential" (stream) Then the collected tokens should be ["TerminalToken"] (stream) And executor.last_result should be an ActorResult (stream) # ── Parallel non-AGENT execution path ──────────────────────────────────── Scenario: execute_stream on parallel function graph exercises parallel non-agent path Given an Executor with a parallel function graph config (stream) When I call execute_stream with message "test parallel fn" (stream) Then executor.last_result should be an ActorResult (stream) # ── execute_stream() timing paths ──────────────────────────────────────── Scenario: execute_stream completes before timeout yields tokens normally Given an Executor with a graph config and timeout_ms 2000 (stream) And a mock astream yielding single token "FastToken" with no usage (stream) When I call execute_stream with message "fast" (stream) Then executor.last_result should be an ActorResult (stream) And executor.last_result.response should equal "FastToken" (stream) # ── _execute_llm_stream() config_block format ──────────────────────────── Scenario: _execute_llm_stream with config_block format covers fallback paths Given an Executor with a config_block format llm config (stream) And a mock astream yielding single token "BlockReply" with no usage (stream) When I call execute_stream with message "test block" (stream) Then executor.last_result should be an ActorResult (stream) # ── _execute_llm_stream() invalid config errors ─────────────────────────── Scenario: _execute_llm_stream with invalid temperature raises ConfigurationError Given an Executor with invalid temperature llm config (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then a ConfigurationError should be raised (stream) Scenario: _execute_llm_stream with invalid max_tokens raises ConfigurationError Given an Executor with invalid max_tokens llm config (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then a ConfigurationError should be raised (stream) # ── _execute_llm_stream() cleanup exception path ────────────────────────── Scenario: _execute_llm_stream logs warning when cleanup raises exception Given an Executor with a basic llm config (stream) And a mock astream yielding single token "CleanupTest" with no usage (stream) And the mock agent cleanup raises RuntimeError (stream) When I call execute_stream with message "test cleanup" (stream) Then executor.last_result should be an ActorResult (stream) # ── M2 fix: LLM path — executor.last_result populated on exception ──────── Scenario: _execute_llm_stream populates executor.last_result when ExecutionError is raised mid-stream Given an Executor with a basic llm config (stream) And a mock astream that raises ExecutionError mid-stream with usage prompt=10 completion=20 (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError should be raised (stream) And executor.last_result should be an ActorResult with llm token counts after the error (stream) # ── execute() with initial_state ───────────────────────────────────────── Scenario: PureLangGraph.execute() with initial_state restores metadata Given a PureLangGraph with a single agent node and initial_state (stream) And a mock astream that yields nothing (just for setup) (stream) When I execute the graph with initial_state and a message (stream) Then the execution result should contain the restored state key (stream) # ── LLMAgent.stream_message() memory_enabled path ──────────────────────── Scenario: stream_message() with memory_enabled config uses memory for history Given an LLMAgent with memory_enabled config and mock astream (stream) When I call stream_message with a string message Then the yielded tokens should be ["mem_token"] # ── More streaming depth/limit error paths ─────────────────────────────── Scenario: execute_stream with non-numeric timeout_ms raises ExecutionError kind timeout Given an Executor with a graph config and string timeout_ms (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "timeout" should be raised (stream) Scenario: execute_stream with valid max_depth limit executes normally Given an Executor with a graph config and max_depth 100 (stream) And a mock astream yielding single token "DepthOk" with no usage (stream) When I call execute_stream with message "test depth" (stream) Then executor.last_result should be an ActorResult (stream) Scenario: execute_stream with bool max_depth raises ExecutionError kind depth Given an Executor with a graph config and bool max_depth (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "depth" should be raised (stream) Scenario: execute_stream with non-numeric max_depth raises ExecutionError kind depth Given an Executor with a graph config and string max_depth (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "depth" should be raised (stream) # ── AGENT parallel streaming path ──────────────────────────────────────── Scenario: execute_stream on parallel AGENT graph exercises AGENT parallel path Given an Executor with a parallel AGENT graph config (stream) And a mock astream yielding single token "ParallelAgentToken" with no usage (stream) When I call execute_stream with message "test parallel agent" (stream) Then executor.last_result should be an ActorResult (stream) # ── Node.stream_agent() non-LLM agent path ─────────────────────────────── Scenario: Node.stream_agent() with ToolAgent falls back to process_message Given a Node stream_agent with a ToolAgent (stream) Then stream_agent yields the ToolAgent response (stream) # ── _execute_llm_stream() create_agent exception paths ─────────────────── Scenario: _execute_llm_stream raises ConfigurationError when create_agent fails Given an Executor with a basic llm config (stream) And the mock factory raises ConfigurationError on create_agent (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then a ConfigurationError should be raised (stream) # ── LLMAgent.stream_message() template path ────────────────────────────── Scenario: stream_message() with template config renders template for message Given an LLMAgent with template config and mock renderer (stream) When I call stream_message with a string message Then the yielded tokens should be ["template_token"] # ── _execute_graph_stream() actors key and alternate config ────────────── Scenario: _execute_graph_stream with context in config sets global_context Given an Executor with a graph config with actor context (stream) And a mock astream yielding single token "ContextToken" with no usage (stream) When I call execute_stream with message "test context" (stream) Then executor.last_result should be an ActorResult (stream) # ── execute_stream() already-running guard ────────────────────────────── Scenario: execute_stream raises RuntimeError when graph is already running Given a PureLangGraph already in running state (stream) When I call execute_stream on the running graph (stream) Then a RuntimeError should be raised from execute_stream (stream) # ── _stream_from_node() node not found path ───────────────────────────── Scenario: execute_stream on graph with missing node logs error and continues Given a PureLangGraph with a dangling edge to nonexistent node (stream) When I call execute_stream on the dangling graph (stream) Then the stream completes without error (stream) # ── _stream_from_node() AGENT streaming exception path ────────────────── Scenario: _stream_from_node re-raises exception from stream_agent as ExecutionError Given a PureLangGraph with AGENT node that has no agent configured (stream) When I call execute_stream on the bad-agent graph (stream) Then an ExecutionError should be raised (stream) # ── Node.stream_agent() edge cases ────────────────────────────────────── Scenario: stream_agent raises ValueError when no agent configured in node Given a Node stream_agent with no agent configured (stream) Then ValueError is raised from stream_agent (stream) Scenario: stream_agent raises ValueError when agent not found in agents dict Given a Node stream_agent with agent key missing from agents dict (stream) Then ValueError is raised from stream_agent (stream) Scenario: stream_agent handles empty state messages Given a Node stream_agent with empty state messages (stream) Then stream_agent yields from empty input (stream) Scenario: stream_agent handles long history with truncation Given a Node stream_agent with long conversation history (stream) Then stream_agent processes truncated history (stream) Scenario: stream_agent handles nested context dict in state metadata Given a Node stream_agent with nested context in metadata (stream) Then stream_agent processes nested context tokens (stream) # ── _execute_llm_stream() other exception in create_agent ─────────────── Scenario: _execute_llm_stream wraps RuntimeError from create_agent in ExecutionError Given an Executor with a basic llm config (stream) And the mock factory raises RuntimeError on create_agent (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError should be raised from _execute_llm_stream (stream) # ── execute_stream() with last_context fallback ───────────────────────── Scenario: execute_stream uses last_context when no global_context provided Given a PureLangGraph with last_context populated (stream) And a mock astream yielding single token "CtxToken" with no usage (stream) When I call execute_stream on graph with last context (stream) Then the stream completes with result (stream) # ── execute_stream() with context_manager ─────────────────────────────── Scenario: execute_stream with context_manager uses context_manager for setup Given a PureLangGraph with context_manager set (stream) And a mock astream yielding single token "CmToken" with no usage (stream) When I call execute_stream on graph with context manager (stream) Then the stream completes with result (stream) # ── execute_stream() conversation_history empty content skip ──────────── Scenario: execute_stream skips empty content entries in conversation_history Given a PureLangGraph with context_manager set (stream) And a mock astream yielding single token "HistToken" with no usage (stream) When I call execute_stream with empty content history entry (stream) Then the stream completes with result (stream) # ── _execute_llm_stream() with system_prompt at top level ──────────────── Scenario: _execute_llm_stream with top-level system_prompt covers sp branch Given an Executor with system_prompt at top level llm config (stream) And a mock astream yielding single token "SpToken" with no usage (stream) When I call execute_stream with message "sp test" (stream) Then executor.last_result should be an ActorResult (stream) # ── LLMAgent.stream_message() system prompt render exception ───────────── Scenario: stream_message falls back to original system_message on render exception Given an LLMAgent with failing render_string mock (stream) When I call stream_message with a string message Then the yielded tokens should be ["render_ex_tok"] # ── _execute_llm_stream() ExecutionError re-raise path ──────────────────── Scenario: _execute_llm_stream re-raises ExecutionError from stream_message Given an Executor with a basic llm config (stream) And a mock astream that raises ExecutionError (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError should be raised from _execute_llm_stream (stream) # ── Intermediate AGENT node coverage (M5 ainvoke path edge cases) ────────── Scenario: execute_stream on two-AGENT sequential graph with non-dict last_msg covers branch Given an Executor with a two-agent sequential graph config (stream) And a mock astream yielding single token "TerminalToken2" with no usage (stream) And the intermediate agent returns a non-dict last_msg (stream) When I call execute_stream with message "test non-dict" (stream) Then executor.last_result should be an ActorResult (stream) Scenario: execute_stream on two-AGENT sequential graph with no messages in result covers branch Given an Executor with a two-agent sequential graph config (stream) And a mock astream yielding single token "TerminalToken3" with no usage (stream) And the intermediate agent returns a result with no messages key (stream) When I call execute_stream with message "test no-messages" (stream) Then executor.last_result should be an ActorResult (stream) Scenario: execute_stream on two-AGENT sequential graph with non-dict ainvoke result covers branch Given an Executor with a two-agent sequential graph config (stream) And a mock astream yielding single token "TerminalToken4" with no usage (stream) And the intermediate agent returns a non-dict ainvoke result (stream) When I call execute_stream with message "test non-dict-result" (stream) Then executor.last_result should be an ActorResult (stream) Scenario: execute_stream on two-AGENT sequential graph with ainvoke exception covers branch Given an Executor with a two-agent sequential graph config (stream) And a mock astream yielding single token "TerminalToken5" with no usage (stream) And the intermediate agent ainvoke raises RuntimeError (stream) When I call execute_stream with message "test ainvoke-error" (stream) Then executor.last_result should be an ActorResult (stream) Scenario: execute_stream on function-then-agent graph with non-dict last_msg covers non-agent branch Given an Executor with a function-then-agent graph config (stream) And a mock astream yielding single token "FnAgentToken" with no usage (stream) And the function node returns a non-dict last_msg in messages (stream) When I call execute_stream with message "test fn-non-dict" (stream) Then executor.last_result should be an ActorResult (stream) Scenario: execute_stream on function-then-agent graph with no messages in function result covers branch Given an Executor with a function-then-agent graph config (stream) And a mock astream yielding single token "FnAgentToken2" with no usage (stream) And the function node returns a result with no messages key (stream) When I call execute_stream with message "test fn-no-messages" (stream) Then executor.last_result should be an ActorResult (stream) Scenario: execute_stream on function-then-agent graph with non-dict function result covers branch Given an Executor with a function-then-agent graph config (stream) And a mock astream yielding single token "FnAgentToken3" with no usage (stream) And the function node returns a non-dict result (stream) When I call execute_stream with message "test fn-non-dict-result" (stream) Then executor.last_result should be an ActorResult (stream) Scenario: execute_stream on function-then-agent graph with function exception covers branch Given an Executor with a function-then-agent graph config (stream) And a mock astream yielding single token "FnAgentToken4" with no usage (stream) And the function node raises RuntimeError (stream) When I call execute_stream with message "test fn-exception" (stream) Then executor.last_result should be an ActorResult (stream) Scenario: execute_stream on function-then-agent graph with function node token usage covers branch Given an Executor with a function-then-agent graph config (stream) And a mock astream yielding single token "FnAgentToken5" with no usage (stream) And the function node returns a result with _node_token_usage (stream) When I call execute_stream with message "test fn-token-usage" (stream) Then executor.last_result should be an ActorResult (stream) # ── stream_message() memory truncation and billing integrity (M2/m1 coverage) ─ Scenario: stream_message() with memory_enabled truncates history when over max_history Given an LLMAgent with memory_enabled and long history for truncation test (stream) When I call stream_message with a string message Then the yielded tokens should be ["mem_trunc_tok"] Scenario: stream_message() billing integrity preserves tokens when post-stream step raises Given an LLMAgent with memory_enabled that raises on update_memory (stream) When I call stream_message with a string message Then _last_token_usage should be (5, 10) after stream_message # ── _temperature_override support in stream_message() (n5 / M1 coverage) ── Scenario: stream_message() applies _temperature_override from context Given an LLMAgent with temperature 0.7 and mock astream for override test (stream) When I call stream_message with _temperature_override 0.1 in context (stream) Then the temperature was 0.1 during streaming and restored to 0.7 afterwards (stream) Scenario: stream_message() with invalid _temperature_override raises ConfigurationError Given an LLMAgent with temperature 0.7 and mock astream for override test (stream) When I call stream_message with _temperature_override "bad" in context (stream) Then a ConfigurationError is raised from stream_message (stream) # ── response_metadata fallback in stream_message() (n6 / M3 coverage) ───── Scenario: stream_message() falls back to response_metadata token_usage when usage_metadata absent Given an LLMAgent with a mock astream whose final chunk has response_metadata token_usage (stream) When I call stream_message with a string message Then _last_token_usage should be (3, 7) after stream_message # ── PureLangGraph.execute() with non-numeric timeout_ms ────────────────── Scenario: PureLangGraph.execute() with non-numeric timeout_ms raises ExecutionError Given a PureLangGraph with string timeout_ms limit (stream) When I call execute on the pure graph with a message (stream) Then an ExecutionError should be raised from execute (stream) # ── C1: auto_finish_active bypass in streaming loop detection ──────────── Scenario: execute_stream with auto_finish_active bypasses loop detection for repeated node visits Given a PureLangGraph with auto_finish_active set in state and a repeated-visit agent node (stream) And a mock astream yielding single token "SectionToken" with no usage (stream) When I call execute_stream on the auto_finish graph (stream) Then the stream completes with result (stream) # ── C2: Router-agent ping-pong detection in streaming path ─────────────── Scenario: execute_stream detects router-agent ping-pong and stops streaming Given a PureLangGraph with router-agent ping-pong setup for streaming (stream) When I call execute_stream on the ping-pong graph (stream) Then the stream completes without error (stream) # ── C3: No routing command → return to user guard in streaming path ─────── Scenario: execute_stream on agent-then-router graph with no routing command returns output to user Given an Executor with an agent-then-router graph config and no routing command (stream) And a mock astream yielding single token "DirectAnswer" with no usage (stream) When I call execute_stream with message "hello" (stream) Then executor.last_result should be an ActorResult (stream) And executor.last_result.response should equal "DirectAnswer" (stream) # ── M2: executor.last_result populated on exception path ───────────────── Scenario: execute_stream populates executor.last_result even when ExecutionError is raised mid-stream Given an Executor with a two-agent sequential graph and max_model_calls 1 (stream) And a mock astream yielding single token "PartialToken" with usage prompt=10 completion=20 (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "model_calls" should be raised (stream) And executor.last_result should be an ActorResult with token counts after the error (stream) Scenario: execute_stream handles state-capture failure gracefully on exception path Given an Executor with a graph that raises and has broken state capture (stream) When I attempt execute_stream with broken state capture expecting an error (stream) Then an ExecutionError should be raised (stream) # ── M3: Cost-limit enforcement in streaming path ───────────────────────── Scenario: execute_stream raises ExecutionError when cost limit is exceeded Given an Executor with a graph config and max_cost_usd 0.0 with pricing (stream) And a mock astream yielding single token "CostToken" with usage prompt=1000 completion=1000 (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "cost" should be raised (stream) Scenario: execute_stream raises ExecutionError when pricing rate is invalid (non-numeric) Given an Executor with a graph config and invalid pricing rate (stream) And a mock astream yielding single token "RateToken" with usage prompt=100 completion=100 (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "cost" should be raised (stream) Scenario: execute_stream raises ExecutionError when max_cost_usd is invalid (non-numeric string) Given an Executor with a graph config and non-numeric max_cost_usd (stream) And a mock astream yielding single token "BadCostToken" with usage prompt=100 completion=100 (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "cost" should be raised (stream) # ── M4: _collect_stream_tokens forwards depth ──────────────────────────── # The M4 fix forwards depth+1 to _collect_stream_tokens so that parallel # children receive the correct depth and can trigger the depth limit. # With max_depth=1 and a graph: start → agent_start → {agent_a, agent_b} → end, # agent_start is at depth 1 (≤ max_depth=1, OK), but agent_a and agent_b are # at depth 2 (> max_depth=1), triggering the depth error in the parallel branch. Scenario: execute_stream on parallel AGENT graph with max_depth 1 raises depth error in parallel children Given an Executor with a parallel AGENT graph config and max_depth 1 (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "depth" should be raised (stream) # ── C1 additional: loop detection stop path (auto_finish_active=False) ──── Scenario: _stream_from_node stops when node visited twice without auto_finish_active Given a PureLangGraph with repeated-visit agent node and no auto_finish_active (stream) When I call _stream_from_node on the repeated-visit graph (stream) Then the stream completes without error (stream) # ── C2 additional: ping-pong stop path ──────────────────────────────────── Scenario: _stream_from_node stops on router-agent ping-pong without auto_finish_active Given a PureLangGraph with router-agent ping-pong setup for streaming (stream) When I call execute_stream on the ping-pong graph (stream) Then the stream completes without error (stream) # ── C3 additional: no-routing-command guard in intermediate AGENT branch ── Scenario: execute_stream on intermediate-agent-then-router graph with no routing command returns output Given a PureLangGraph with intermediate agent then router and no routing command (stream) And a mock astream yielding single token "IntermediateAnswer" with no usage (stream) When I call execute_stream on the intermediate-router graph (stream) Then the stream completes with result (stream) # ── M3 additional: cost enforcement with missing pricing entry ──────────── Scenario: execute_stream raises ExecutionError when pricing entry is missing for provider Given an Executor with a graph config and max_cost_usd 1.0 with missing pricing (stream) And a mock astream yielding single token "CostToken2" with usage prompt=1000 completion=1000 (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "cost" should be raised (stream) # ── C3 intermediate branch: agent with non-END static successor routes to router ─ # Note: agent1 has edges to both "end" (static) and "router" (conditional:always). # Because "router" is a non-END static successor, _statically_terminal=False for # agent1, so this exercises the intermediate AGENT branch where the C3 guard fires. Scenario: _stream_from_node C3 guard fires in intermediate AGENT branch with conditional router edge Given a PureLangGraph with terminal agent and conditional router edge (stream) And a mock astream yielding single token "ConditionalAnswer" with no usage (stream) When I call execute_stream on the conditional-router graph (stream) Then the stream completes with result (stream) # ── C1 nested context: auto_finish_active in nested context dict ────────── Scenario: _stream_from_node C1 bypass works when auto_finish_active is in nested context Given a PureLangGraph with auto_finish_active in nested context and repeated-visit node (stream) When I call _stream_from_node on the repeated-visit graph (stream) Then the stream completes without error (stream) # ── C2 nested context: auto_finish_active in nested context for ping-pong ─ Scenario: _stream_from_node C2 bypass works when auto_finish_active is in nested context Given a PureLangGraph with auto_finish_active nested context and ping-pong setup (stream) When I call execute_stream on the ping-pong graph (stream) Then the stream completes without error (stream) # ── Intermediate AGENT dynamically terminal path ───────────────────────── Scenario: _stream_from_node intermediate AGENT yields when conditional edge resolves to END Given a PureLangGraph with intermediate agent and conditional edge to END (stream) When I call execute_stream on the conditional-end graph (stream) Then the stream completes with result (stream) And the collected tokens should be ["DynTerminalToken"] (stream) # ── Streaming bool/invalid limit enforcement ───────────────────────────── Scenario: execute_stream raises ExecutionError for bool max_model_calls in streaming Given an Executor with a graph config and bool max_model_calls (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "model_calls" should be raised (stream) Scenario: execute_stream raises ExecutionError for bool max_tool_calls in streaming Given an Executor with a tool-node graph and bool max_tool_calls (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "tool_calls" should be raised (stream) # ── M3 cost enforcement: missing model pricing entry ───────────────────── Scenario: execute_stream raises ExecutionError when model pricing entry is missing Given an Executor with a graph config and max_cost_usd 1.0 with missing model pricing (stream) And a mock astream yielding single token "CostToken3" with usage prompt=1000 completion=1000 (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "cost" should be raised (stream) # ── Streaming invalid (non-numeric) limit enforcement ──────────────────── Scenario: execute_stream raises ExecutionError for string max_model_calls in streaming Given an Executor with a graph config and string max_model_calls (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "model_calls" should be raised (stream) Scenario: execute_stream raises ExecutionError for string max_tool_calls in streaming Given an Executor with a tool-node graph and string max_tool_calls (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "tool_calls" should be raised (stream) # ── M3 cost enforcement: incomplete and invalid pricing ────────────────── Scenario: execute_stream raises ExecutionError when pricing entry is incomplete Given an Executor with a graph config and max_cost_usd 1.0 with incomplete pricing (stream) And a mock astream yielding single token "CostToken4" with usage prompt=1000 completion=1000 (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "cost" should be raised (stream) Scenario: execute_stream raises ExecutionError for bool max_cost_usd in streaming Given an Executor with a graph config and bool max_cost_usd with pricing (stream) And a mock astream yielding single token "CostToken5" with usage prompt=1000 completion=1000 (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "cost" should be raised (stream) # ── Intermediate AGENT parallel streaming path ─────────────────────────── Scenario: execute_stream on parallel intermediate AGENT graph yields tokens Given an Executor with a parallel intermediate AGENT graph config (stream) And a mock astream yielding single token "ParallelIntToken" with no usage (stream) When I call execute_stream with message "test parallel int" (stream) Then executor.last_result should be an ActorResult (stream) # ── Major #1 fix: max_cost_usd enforced for intermediate AGENT nodes ────── Scenario: Streaming graph exhausts budget via post-node check on first node Given an Executor with a two-agent sequential graph and max_cost_usd 0.40 with pricing (stream) And a mock astream yielding single token "BudgetNodeCost" with usage prompt=3000000 completion=0 (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "cost" and reason "budget_exhausted" should be raised (stream kind+reason) Scenario: Second streaming node blocked by pre-flight when first node exactly hits limit Given an Executor with a two-agent sequential graph and max_cost_usd 0.30 at 0.15/M prompt pricing (stream) And a mock astream yielding single token "ExactPreFlight" with usage prompt=2000000 completion=0 (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "cost" and reason "budget_exhausted" should be raised (stream kind+reason) Scenario: execute_stream raises ExecutionError when intermediate AGENT node exceeds max_cost_usd Given an Executor with a two-agent sequential graph and max_cost_usd 0.0 with pricing (stream) And a mock astream yielding single token "IntCostToken" with usage prompt=1000 completion=1000 (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "cost" should be raised (stream) Scenario: execute_stream raises ExecutionError when intermediate AGENT node has missing provider pricing Given an Executor with a two-agent sequential graph and max_cost_usd 1.0 with missing provider pricing (stream) And a mock astream yielding single token "IntCostToken2" with usage prompt=1000 completion=1000 (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "cost" should be raised (stream) Scenario: execute_stream raises ExecutionError when intermediate AGENT node has missing model pricing Given an Executor with a two-agent sequential graph and max_cost_usd 1.0 with missing model pricing (stream) And a mock astream yielding single token "IntCostToken3" with usage prompt=1000 completion=1000 (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "cost" should be raised (stream) Scenario: execute_stream raises ExecutionError when intermediate AGENT node has incomplete pricing Given an Executor with a two-agent sequential graph and max_cost_usd 1.0 with incomplete pricing (stream) And a mock astream yielding single token "IntCostToken4" with usage prompt=1000 completion=1000 (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "cost" should be raised (stream) Scenario: execute_stream raises ExecutionError when intermediate AGENT node has invalid pricing rate Given an Executor with a two-agent sequential graph and max_cost_usd 1.0 with invalid pricing rate (stream) And a mock astream yielding single token "IntCostToken5" with usage prompt=1000 completion=1000 (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "cost" should be raised (stream) Scenario: execute_stream raises ExecutionError when intermediate AGENT node has bool max_cost_usd Given an Executor with a two-agent sequential graph and bool max_cost_usd with pricing (stream) And a mock astream yielding single token "IntCostToken6" with usage prompt=1000 completion=1000 (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "cost" should be raised (stream) Scenario: execute_stream raises ExecutionError when intermediate AGENT node has non-numeric max_cost_usd Given an Executor with a two-agent sequential graph and non-numeric max_cost_usd with pricing (stream) And a mock astream yielding single token "IntCostToken7" with usage prompt=1000 completion=1000 (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "cost" should be raised (stream) # ── Major #2 fix: state not polluted on agent failure ──────────────────── Scenario: execute_stream does not persist user input as assistant message when streaming fails Given a PureLangGraph with terminal AGENT node that fails during streaming (stream) When I call execute_stream on the failing-agent graph (stream) Then the graph state messages should not contain the user input as an assistant message (stream) # ── M1 fix: graph path — executor.last_result populated for unexpected exceptions ── Scenario: execute_stream on graph actor populates executor.last_result when unexpected RuntimeError is raised from execute_stream Given an Executor with a graph config that has a single agent node (stream) And the graph execute_stream raises RuntimeError unexpectedly (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError should be raised (stream) And executor.last_result should be an ActorResult with no_llm placeholder (stream) Scenario: execute_stream on graph actor handles broken state capture gracefully on unexpected exception Given an Executor with a graph config that has a single agent node (stream) And the graph execute_stream raises RuntimeError with broken state capture (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError should be raised (stream) And executor.last_result should be an ActorResult with no_llm placeholder (stream) # ── M2 fix: graph path — executor.last_result populated when AgentCreationError ── Scenario: execute_stream on graph actor populates executor.last_result with no_llm placeholder when AgentFactory.create_agent raises ConfigurationError Given an Executor with a graph config that has a single agent node (stream) And the mock graph factory raises ConfigurationError on create_agent (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then a ConfigurationError should be raised (stream) And executor.last_result should be an ActorResult with no_llm placeholder (stream) # ── m4 fix: empty astream with memory_enabled=True ─────────────────────── Scenario: stream_message with memory_enabled and empty astream stores empty assistant entry Given an LLMAgent with memory_enabled and empty astream (stream) When I call stream_message with a string message Then the assistant memory entry should have empty content (stream) # ── m5 fix: partial-stream abandonment leaves _last_token_usage at (0, 0) ─ Scenario: execute_stream abandonment leaves _last_token_usage at (0, 0) Given an Executor with a basic llm config (stream) And a mock astream yielding tokens "Hello", " World" with no usage (stream) When I start iterating execute_stream and stop after first token (stream) Then executor.last_result should be None (stream) And the agent _last_token_usage should be (0, 0) after abandonment (stream) # ── m6 fix: executor.last_result.state verified in graph streaming success ─ Scenario: execute_stream on graph with initial_state populates last_result.state Given an Executor with a graph config that has a single agent node (stream) And a mock astream yielding single token "Resumed" with no usage (stream) When I run execute_stream with initial_state on graph executor (stream) Then executor.last_result should be an ActorResult (stream) And executor.last_result.state should be populated with initial_state keys (stream) # ── Issue 6: _execute_llm_stream generic-exception path (billing integrity) ─ # The except Exception block in _execute_llm_stream handles unexpected # non-ExecutionError exceptions and must populate executor.last_result before # re-raising as ExecutionError. This mirrors the graph path's coverage. Scenario: _execute_llm_stream populates executor.last_result when unexpected RuntimeError is raised during streaming Given an Executor with a basic llm config (stream) And a mock astream that raises RuntimeError (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError should be raised (stream) And executor.last_result should be an ActorResult (stream) # ── Issue 1: billing-integrity for early config-validation errors in _execute_llm_stream ─ # The temperature/max_tokens/timeout_ms validations fire before the main # try/except block. executor.last_result must be populated even for these # early errors to satisfy the billing-integrity guarantee. Scenario: _execute_llm_stream with invalid temperature populates executor.last_result Given an Executor with invalid temperature llm config (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then a ConfigurationError should be raised (stream) And executor.last_result should be an ActorResult (stream) Scenario: _execute_llm_stream with invalid max_tokens populates executor.last_result Given an Executor with invalid max_tokens llm config (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then a ConfigurationError should be raised (stream) And executor.last_result should be an ActorResult (stream) Scenario: _execute_llm_stream with bool timeout_ms populates executor.last_result Given an LLM Executor with bool_true timeout_ms (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "timeout" should be raised (stream) And executor.last_result should be an ActorResult (stream) # ── Issue 2: billing-integrity for early config-validation errors in _execute_graph_stream ─ # The node/edge validation loop fires before the main try/except block. # executor.last_result must be populated even for these early errors. Scenario: _execute_graph_stream with invalid node definition populates executor.last_result Given an Executor with a graph config that has an invalid node definition (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then a ConfigurationError should be raised (stream) And executor.last_result should be an ActorResult with no_llm placeholder (stream) Scenario: _execute_graph_stream with duplicate node ID populates executor.last_result Given an Executor with a graph config that has a duplicate node ID (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then a ConfigurationError should be raised (stream) And executor.last_result should be an ActorResult with no_llm placeholder (stream) # ── Issue 3: GOTO_/ROUTE_ routing commands parsed in streaming path ─────── # The streaming path must parse GOTO_*/ROUTE_* prefixes from agent output # and store the target in state.metadata["next_node"], mirroring # _execute_from_node. Without this, graphs relying on LLM-emitted routing # commands route correctly under execute() but incorrectly under # execute_stream(). Scenario: _stream_from_node parses GOTO_ routing command from terminal AGENT output Given a PureLangGraph with terminal AGENT node that emits a GOTO_ routing command (stream) When I call execute_stream on the GOTO-routing graph (stream) Then state.metadata next_node should be set from the GOTO_ command (stream) Scenario: _stream_from_node parses GOTO_ routing command from intermediate AGENT output Given a PureLangGraph with intermediate AGENT node that emits a GOTO_ routing command (stream) When I call execute_stream on the intermediate GOTO-routing graph (stream) Then state.metadata next_node should be set from the intermediate GOTO_ command (stream) # ── Issue 1 (review round 7): GOTO_/ROUTE_ parsing in non-AGENT branch ──── # The non-AGENT branch of _stream_from_node() was missing GOTO_/ROUTE_ parsing # that exists in _execute_from_node() for all node types. A function/tool node # that emits a routing command (e.g. "GOTO_validation:...") must set # state.metadata["next_node"] under execute_stream() just as it does under # execute(). Scenario: _stream_from_node parses GOTO_ routing command from non-AGENT function node output Given a PureLangGraph with non-AGENT function node that emits a GOTO_ routing command (stream) When I call execute_stream on the non-AGENT GOTO-routing graph (stream) Then state.metadata next_node should be set from the non-AGENT GOTO_ command (stream) # ── Issue 2 (review round 7): str(None) guard in intermediate AGENT branch ─ # When an intermediate AGENT node returns None content (e.g. last_msg["content"] # is None), the two yield sites in _stream_from_node() must yield "" instead of # the literal string "None". Scenario: _stream_from_node intermediate AGENT with None content yields empty string not "None" Given a PureLangGraph with intermediate AGENT node that returns None content (stream) When I call execute_stream on the None-content intermediate graph (stream) Then the collected tokens should not contain the literal string "None" (stream) # ── Issue 5 (review round 7): LangChainException arm in stream_message() ── # The except LangChainException arm added in review round 5 has no test. # A regression removing this arm would not be caught. This scenario injects # a LangChainException into astream() and asserts: # (a) ExecutionError is raised, (b) the captured log contains the streaming # error message. Scenario: stream_message raises ExecutionError when astream raises LangChainException Given an Executor with a basic llm config (stream) And a mock astream that raises LangChainException (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError should be raised (stream) And executor.last_result should be an ActorResult (stream) # ── Issue 4 (review round 7): resource-leak test verifies cleanup() called ─ # The existing abandonment test only checks last_result is None and # _last_token_usage == (0, 0) — both true regardless of whether cleanup() was # called. This scenario adds an explicit assertion that agent.cleanup was # awaited after the caller abandoned the stream. Scenario: execute_stream abandonment calls agent.cleanup() promptly Given an Executor with a basic llm config (stream) And a mock astream yielding tokens "Hello", " World" with no usage (stream) When I start iterating execute_stream and stop after first token verifying cleanup (stream) Then agent.cleanup should have been called (stream) # ── Issue 8 (review round 7): ExecutionError.reason asserted in cost scenarios ─ # The existing cost-error step only asserts error.kind. A regression that # always raises ExecutionError(kind="cost", reason="budget_exhausted") would # pass all cost scenarios. This scenario explicitly checks the reason field. Scenario: execute_stream raises ExecutionError with reason budget_exhausted when cost limit exceeded Given an Executor with a basic llm config and zero cost limit (stream) And a mock astream yielding single token "Hi" with usage prompt=10 completion=5 (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "cost" and reason "budget_exhausted" should be raised (stream kind+reason) Scenario: execute_stream raises ExecutionError with reason missing_pricing_entry when provider missing Given an Executor with a basic llm config and pricing but missing provider entry (stream) And a mock astream yielding single token "Hi" with usage prompt=10 completion=5 (stream) When I attempt execute_stream expecting an error with message "test" (stream) Then an ExecutionError with kind "cost" and reason "missing_pricing_entry" should be raised (stream kind+reason)