react-agent-harness 0.3.0__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. {react_agent_harness-0.3.0/react_agent_harness.egg-info → react_agent_harness-0.3.2}/PKG-INFO +1 -1
  2. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/README.md +17 -0
  3. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/agents/base.py +7 -5
  4. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/harness/runtime.py +3 -2
  5. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/harness/steering.py +29 -16
  6. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/pyproject.toml +1 -1
  7. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2/react_agent_harness.egg-info}/PKG-INFO +1 -1
  8. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tests/test_steering.py +73 -22
  9. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tests/test_streaming.py +64 -23
  10. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/LICENSE +0 -0
  11. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/agents/__init__.py +0 -0
  12. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/harness/__init__.py +0 -0
  13. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/harness/annotation.py +0 -0
  14. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/harness/checkpoint.py +0 -0
  15. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/harness/cli.py +0 -0
  16. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/harness/events.py +0 -0
  17. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/harness/executor_bridge.py +0 -0
  18. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/harness/hitl.py +0 -0
  19. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/harness/llm/__init__.py +0 -0
  20. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/harness/llm/_streaming.py +0 -0
  21. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/harness/llm/auth.py +0 -0
  22. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/harness/llm/claude_code.py +0 -0
  23. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/harness/llm/openai.py +0 -0
  24. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/harness/llm/openai_codex.py +0 -0
  25. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/harness/otel.py +0 -0
  26. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/harness/utils.py +0 -0
  27. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/memory/__init__.py +0 -0
  28. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/memory/episodic_lance.py +0 -0
  29. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/memory/manager.py +0 -0
  30. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/memory/redis_store.py +0 -0
  31. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/memory/stores.py +0 -0
  32. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/memory/working.py +0 -0
  33. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/orchestrator/__init__.py +0 -0
  34. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/orchestrator/planner.py +0 -0
  35. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/react_agent_harness.egg-info/SOURCES.txt +0 -0
  36. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/react_agent_harness.egg-info/dependency_links.txt +0 -0
  37. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/react_agent_harness.egg-info/entry_points.txt +0 -0
  38. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/react_agent_harness.egg-info/requires.txt +0 -0
  39. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/react_agent_harness.egg-info/top_level.txt +0 -0
  40. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/setup.cfg +0 -0
  41. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tests/test_agents_base.py +0 -0
  42. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tests/test_annotation.py +0 -0
  43. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tests/test_checkpoint_resume.py +0 -0
  44. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tests/test_claude_code_llm.py +0 -0
  45. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tests/test_cli.py +0 -0
  46. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tests/test_executor_bridge.py +0 -0
  47. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tests/test_http_fetch.py +0 -0
  48. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tests/test_llm_auth.py +0 -0
  49. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tests/test_mcp_adapter.py +0 -0
  50. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tests/test_memory.py +0 -0
  51. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tests/test_openai_codex_llm.py +0 -0
  52. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tests/test_openai_llm.py +0 -0
  53. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tests/test_orchestrator.py +0 -0
  54. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tests/test_otel.py +0 -0
  55. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tests/test_parse_action_json.py +0 -0
  56. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tests/test_redis_store.py +0 -0
  57. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tests/test_utils.py +0 -0
  58. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tests/test_vision.py +0 -0
  59. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tests/test_working_memory.py +0 -0
  60. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tools/__init__.py +0 -0
  61. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tools/builtin/__init__.py +0 -0
  62. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tools/builtin/fetch_image.py +0 -0
  63. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tools/builtin/http_fetch.py +0 -0
  64. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tools/mcp/__init__.py +0 -0
  65. {react_agent_harness-0.3.0 → react_agent_harness-0.3.2}/tools/mcp/adapter.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: react-agent-harness
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Multi-agent LLM orchestration: hybrid DAG planning, two-tier memory, streaming
5
5
  Requires-Python: >=3.10
6
6
  License-File: LICENSE
@@ -909,3 +909,20 @@ key-bindings (like Enter-submits and Alt-Enter/Ctrl-J-newline) across both paths
909
909
 
910
910
  See `examples/complex_sysaudit_demo.py` for stdin steering across three
911
911
  agents alongside HITL on the shell tool.
912
+
913
+ ## AgentConfig reference
914
+
915
+ | Field | Default | Description |
916
+ |---|---|---|
917
+ | `agent_id` | required | Unique identifier for the agent |
918
+ | `role` | required | Plain-English description used by the planner for agent selection |
919
+ | `system_prompt` | required | Base system prompt for the agent |
920
+ | `allowed_tools` | required | Tool names the agent may call |
921
+ | `max_steps` | `10` | Maximum ReAct iterations before the run is terminated |
922
+ | `max_wall_time_seconds` | (guardrail) | See `GuardrailConfig` |
923
+ | `memory_context_enabled` | `True` | Prepend relevant long-term memory to the system prompt |
924
+ | `confidence_from_llm` | `True` | Use the `confidence` field from the LLM response; set `False` to always return `1.0` |
925
+ | `working_memory_max_tokens` | `8000` | Token budget for in-context working memory before rolling summarisation kicks in |
926
+ | `hitl_tools` | `[]` | Tool names that require human approval before execution |
927
+ | `checkpoint_every` | `0` | Write a crash-resumable checkpoint every N steps; `0` disables periodic checkpoints |
928
+ | `stream_tokens` | `False` | Emit `TOKEN` events as the LLM streams. Disabled by default — enable if you want to render partial output in real time: `AgentConfig(..., stream_tokens=True)` |
@@ -61,6 +61,7 @@ class AgentConfig:
61
61
  max_steps: int = 10
62
62
  memory_context_enabled: bool = True
63
63
  confidence_from_llm: bool = True # if False, confidence=1.0 on success
64
+ stream_tokens: bool = False # if True, TOKEN events are emitted as the LLM streams
64
65
  working_memory_max_tokens: int = 8000 # WorkingMemory eviction threshold; tune per agent
65
66
  hitl_tools: list[str] = None # tools requiring human approval; None = no HITL
66
67
  checkpoint_every: int = 0 # write a resumable checkpoint every N steps; 0 = disabled
@@ -649,11 +650,12 @@ class BaseAgent:
649
650
  messages=messages,
650
651
  ):
651
652
  accumulated += token
652
- yield BusEvent(
653
- type=EventType.TOKEN,
654
- agent_id=self.config.agent_id,
655
- token=token,
656
- )
653
+ if self.config.stream_tokens:
654
+ yield BusEvent(
655
+ type=EventType.TOKEN,
656
+ agent_id=self.config.agent_id,
657
+ token=token,
658
+ )
657
659
  response = _parse_action_json(accumulated)
658
660
  if response is None:
659
661
  logger.warning(
@@ -781,8 +781,9 @@ class AgentRuntime:
781
781
  run_id = str(uuid.uuid4())
782
782
  tracer.start_run(run_id, task)
783
783
  try:
784
- async for event in self._run_agent_with_tracer(agent_id, task, tracer, run_id):
785
- yield event
784
+ async with self._steering_lifecycle():
785
+ async for event in self._run_agent_with_tracer(agent_id, task, tracer, run_id):
786
+ yield event
786
787
  finally:
787
788
  tracer.end_run()
788
789
 
@@ -150,7 +150,6 @@ class StdinRouter:
150
150
  input_: Any | None = None,
151
151
  output: Any | None = None,
152
152
  history: Any | None = None,
153
- patch_stdout_: bool = True,
154
153
  ) -> None:
155
154
  self._task: asyncio.Task | None = None
156
155
  self._stop = asyncio.Event()
@@ -159,8 +158,6 @@ class StdinRouter:
159
158
  # subscription_id → (prefix, callback). prefix=None is catch-all.
160
159
  self._subs: dict[int, tuple[str | None, Callable[[str], None]]] = {}
161
160
  self._next_sub_id: int = 0
162
- # Tests turn off patch_stdout to avoid interfering with pytest capture.
163
- self._patch_stdout = patch_stdout_
164
161
  self._session: PromptSession = PromptSession(
165
162
  history=history or InMemoryHistory(),
166
163
  input=input_,
@@ -282,18 +279,13 @@ class StdinRouter:
282
279
  # ── Internals ─────────────────────────────────────────────────────────────
283
280
 
284
281
  async def _run(self) -> None:
285
- # patch_stdout makes prints from other tasks scroll above the prompt
286
- # instead of corrupting the input line. Tests skip it because it
287
- # interferes with pytest's stdout capture.
288
- cm = patch_stdout(raw=True) if self._patch_stdout else contextlib.nullcontext()
289
- with cm:
290
- while not self._stop.is_set():
291
- claim = self._hitl_claim
292
- if claim is not None:
293
- await self._serve_hitl(*claim)
294
- self._hitl_claim = None
295
- else:
296
- await self._serve_steering()
282
+ while not self._stop.is_set():
283
+ claim = self._hitl_claim
284
+ if claim is not None:
285
+ await self._serve_hitl(*claim)
286
+ self._hitl_claim = None
287
+ else:
288
+ await self._serve_steering()
297
289
 
298
290
  async def _serve_steering(self) -> None:
299
291
  try:
@@ -575,10 +567,13 @@ class _StdinSteeringFactory:
575
567
  self,
576
568
  router: StdinRouter | None = None,
577
569
  prefix_template: str = "{agent_id}",
570
+ patch_stdout_: bool = True,
578
571
  ) -> None:
579
572
  self._router = router or StdinRouter()
580
573
  self._owned = router is None
581
574
  self._prefix_template = prefix_template
575
+ self._patch_stdout = patch_stdout_ and router is None # only patch when we own the router
576
+ self._patch_stdout_cm: Any | None = None
582
577
  # Ref-counted lifecycle: nested AgentRuntime wraps (dispatch_stream
583
578
  # → run_stream) re-enter the factory; only the outermost
584
579
  # enter/exit actually starts/stops the router.
@@ -590,6 +585,9 @@ class _StdinSteeringFactory:
590
585
 
591
586
  async def __aenter__(self) -> _StdinSteeringFactory:
592
587
  if self._owned and self._enter_count == 0:
588
+ if self._patch_stdout:
589
+ self._patch_stdout_cm = patch_stdout(raw=True)
590
+ self._patch_stdout_cm.__enter__()
593
591
  await self._router.__aenter__()
594
592
  self._enter_count += 1
595
593
  return self
@@ -598,11 +596,15 @@ class _StdinSteeringFactory:
598
596
  self._enter_count = max(0, self._enter_count - 1)
599
597
  if self._owned and self._enter_count == 0:
600
598
  await self._router.__aexit__(exc_type, exc, tb)
599
+ if self._patch_stdout_cm is not None:
600
+ self._patch_stdout_cm.__exit__(exc_type, exc, tb)
601
+ self._patch_stdout_cm = None
601
602
 
602
603
 
603
604
  def stdin_steering_factory(
604
605
  router: StdinRouter | None = None,
605
606
  prefix_template: str = "{agent_id}",
607
+ patch_stdout_: bool = True,
606
608
  ) -> _StdinSteeringFactory:
607
609
  """Return a steering factory that lifecycles its own StdinRouter.
608
610
 
@@ -616,7 +618,9 @@ def stdin_steering_factory(
616
618
  `prefix_template` may reference `{agent_id}`; default subscribes
617
619
  each agent to its own `agent_id`.
618
620
  """
619
- return _StdinSteeringFactory(router=router, prefix_template=prefix_template)
621
+ return _StdinSteeringFactory(
622
+ router=router, prefix_template=prefix_template, patch_stdout_=patch_stdout_
623
+ )
620
624
 
621
625
 
622
626
  # ── Direct-use shims (no AgentRuntime / no factory) ───────────────────────────
@@ -639,6 +643,7 @@ class StdinSteer:
639
643
  agents: BaseAgent | list[BaseAgent],
640
644
  *,
641
645
  router: StdinRouter | None = None,
646
+ patch_stdout_: bool = True,
642
647
  ) -> None:
643
648
  if not isinstance(agents, list):
644
649
  agents = [agents]
@@ -647,9 +652,14 @@ class StdinSteer:
647
652
  self._agents = agents
648
653
  self._router = router or StdinRouter()
649
654
  self._owned_router = router is None
655
+ self._patch_stdout = patch_stdout_
656
+ self._patch_stdout_cm: Any | None = None
650
657
  self._sub_ids: list[int] = []
651
658
 
652
659
  async def __aenter__(self) -> StdinSteer:
660
+ if self._patch_stdout:
661
+ self._patch_stdout_cm = patch_stdout(raw=True)
662
+ self._patch_stdout_cm.__enter__()
653
663
  if self._owned_router:
654
664
  await self._router.start()
655
665
  # Always register one subscription per agent under its agent_id.
@@ -672,3 +682,6 @@ class StdinSteer:
672
682
  self._sub_ids.clear()
673
683
  if self._owned_router:
674
684
  await self._router.stop()
685
+ if self._patch_stdout_cm is not None:
686
+ self._patch_stdout_cm.__exit__(exc_type, exc, tb)
687
+ self._patch_stdout_cm = None
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "react-agent-harness"
7
- version = "0.3.0"
7
+ version = "0.3.2"
8
8
  description = "Multi-agent LLM orchestration: hybrid DAG planning, two-tier memory, streaming"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: react-agent-harness
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Multi-agent LLM orchestration: hybrid DAG planning, two-tier memory, streaming
5
5
  Requires-Python: >=3.10
6
6
  License-File: LICENSE
@@ -18,7 +18,14 @@ from prompt_toolkit.output import DummyOutput
18
18
 
19
19
  from agents.base import AgentConfig, BaseAgent
20
20
  from harness.events import EventType
21
- from harness.runtime import BudgetGuard, GuardrailConfig, Tracer
21
+ from harness.runtime import (
22
+ AgentRegistry,
23
+ AgentRuntime,
24
+ BudgetGuard,
25
+ GuardrailConfig,
26
+ ToolRegistry,
27
+ Tracer,
28
+ )
22
29
  from harness.steering import (
23
30
  FileSteer,
24
31
  StdinAgentSource,
@@ -57,7 +64,6 @@ def _piped_router():
57
64
  router = StdinRouter(
58
65
  input_=pipe_in,
59
66
  output=DummyOutput(),
60
- patch_stdout_=False,
61
67
  )
62
68
  yield router, pipe_in
63
69
 
@@ -322,20 +328,6 @@ async def test_router_routes_to_catchall_subscriber():
322
328
  assert received == ["plain line"]
323
329
 
324
330
 
325
- @pytest.mark.asyncio
326
- async def test_router_default_patch_stdout_context_starts():
327
- """Default patch_stdout path uses a sync context manager but still runs in async loop."""
328
- received: list[str] = []
329
- with create_pipe_input() as pipe_in:
330
- router = StdinRouter(input_=pipe_in, output=DummyOutput())
331
- router.subscribe(None, received.append)
332
- await router.start()
333
- pipe_in.send_text("plain line\r")
334
- await _drain()
335
- await router.stop()
336
- assert received == ["plain line"]
337
-
338
-
339
331
  @pytest.mark.asyncio
340
332
  async def test_router_routes_by_prefix():
341
333
  a_received: list[str] = []
@@ -441,7 +433,7 @@ async def test_router_claim_next_line_resolves_with_typed_answer():
441
433
 
442
434
 
443
435
  def test_router_rejects_star_as_subscription_prefix():
444
- router = StdinRouter(patch_stdout_=False)
436
+ router = StdinRouter()
445
437
  with pytest.raises(ValueError):
446
438
  router.subscribe("*", lambda _t: None)
447
439
 
@@ -455,7 +447,7 @@ async def test_stdin_single_agent_no_prefix_needed():
455
447
  with _piped_router() as (router, pipe_in):
456
448
  await router.start()
457
449
  try:
458
- async with StdinSteer(a, router=router):
450
+ async with StdinSteer(a, router=router, patch_stdout_=False):
459
451
  pipe_in.send_text("just do it\r")
460
452
  await _drain()
461
453
  finally:
@@ -469,7 +461,7 @@ async def test_stdin_single_agent_prefix_also_works():
469
461
  with _piped_router() as (router, pipe_in):
470
462
  await router.start()
471
463
  try:
472
- async with StdinSteer(a, router=router):
464
+ async with StdinSteer(a, router=router, patch_stdout_=False):
473
465
  pipe_in.send_text("a: explicit\r")
474
466
  await _drain()
475
467
  finally:
@@ -484,7 +476,7 @@ async def test_stdin_multi_agent_prefix_routes():
484
476
  with _piped_router() as (router, pipe_in):
485
477
  await router.start()
486
478
  try:
487
- async with StdinSteer([a, b], router=router):
479
+ async with StdinSteer([a, b], router=router, patch_stdout_=False):
488
480
  pipe_in.send_text("a: do A\r")
489
481
  await _drain()
490
482
  pipe_in.send_text("b: do B\r")
@@ -502,7 +494,7 @@ async def test_stdin_multi_agent_broadcast():
502
494
  with _piped_router() as (router, pipe_in):
503
495
  await router.start()
504
496
  try:
505
- async with StdinSteer([a, b], router=router):
497
+ async with StdinSteer([a, b], router=router, patch_stdout_=False):
506
498
  pipe_in.send_text("*: stop now\r")
507
499
  await _drain()
508
500
  finally:
@@ -518,7 +510,7 @@ async def test_stdin_steer_registers_as_active_router():
518
510
  await router.start()
519
511
  try:
520
512
  assert get_active_router() is None
521
- async with StdinSteer(a, router=router):
513
+ async with StdinSteer(a, router=router, patch_stdout_=False):
522
514
  assert get_active_router() is router
523
515
  assert get_active_router() is None
524
516
  finally:
@@ -661,3 +653,62 @@ async def test_stdin_steering_factory_subscribes_each_agent():
661
653
  # Verify it actually subscribed.
662
654
  assert "a" in router.active_prefixes()
663
655
  assert "a" not in router.active_prefixes()
656
+
657
+
658
+ # ── AgentRuntime.run_agent_stream steering lifecycle ─────────────────────────
659
+
660
+
661
+ @pytest.mark.asyncio
662
+ async def test_run_agent_stream_starts_steering_lifecycle(llm, memory):
663
+ """run_agent_stream must enter the steering lifecycle so the factory's
664
+ __aenter__/__aexit__ are called — regression test for the single-agent
665
+ steering bug where the lifecycle wrapper was missing."""
666
+ llm.routes = {
667
+ "react": lambda *_: {
668
+ "thought": "done",
669
+ "action": "finish",
670
+ "answer": "ok",
671
+ "confidence": 1.0,
672
+ }
673
+ }
674
+
675
+ entered = False
676
+ exited = False
677
+
678
+ class _LifecycleFactory:
679
+ """Looks like a stdin_steering_factory — has both lifecycle and per-agent call."""
680
+
681
+ async def __aenter__(self):
682
+ nonlocal entered
683
+ entered = True
684
+ return self
685
+
686
+ async def __aexit__(self, *exc):
687
+ nonlocal exited
688
+ exited = True
689
+
690
+ def __call__(self, agent: BaseAgent):
691
+ import contextlib
692
+
693
+ @contextlib.asynccontextmanager
694
+ async def _noop():
695
+ yield
696
+
697
+ return _noop()
698
+
699
+ config = AgentConfig(agent_id="solo", role="r", system_prompt="react", allowed_tools=[])
700
+ agent_reg = AgentRegistry()
701
+ agent_reg.register(config)
702
+
703
+ runtime = AgentRuntime(
704
+ agent_registry=agent_reg,
705
+ tool_registry=ToolRegistry(),
706
+ memory=memory,
707
+ llm=llm,
708
+ steering_source_factory=_LifecycleFactory(),
709
+ )
710
+
711
+ events = [ev async for ev in runtime.run_agent_stream("solo", "test task")]
712
+ assert any(ev.type == EventType.TASK_DONE for ev in events)
713
+ assert entered, "steering lifecycle __aenter__ was never called"
714
+ assert exited, "steering lifecycle __aexit__ was never called"
@@ -5,6 +5,7 @@ Verifies that BaseAgent.run_stream() and Orchestrator.run_stream() yield the
5
5
  expected BusEvent sequence, and that the blocking run() drains to the same
6
6
  result the stream's DONE event carries.
7
7
  """
8
+
8
9
  from __future__ import annotations
9
10
 
10
11
  from agents.base import AgentConfig
@@ -20,8 +21,11 @@ from tests.conftest import EchoTool, ScriptedLLM
20
21
  async def test_agent_run_stream_finish_yields_task_done(agent_factory):
21
22
  """Finish on first step → just one TASK_DONE event (no THOUGHT/ACTION pairs)."""
22
23
  cfg = AgentConfig(
23
- agent_id="a", role="r", system_prompt="finish.",
24
- allowed_tools=[], working_memory_max_tokens=2000,
24
+ agent_id="a",
25
+ role="r",
26
+ system_prompt="finish.",
27
+ allowed_tools=[],
28
+ working_memory_max_tokens=2000,
25
29
  )
26
30
  agent = agent_factory(cfg)
27
31
  events = [e async for e in agent.run_stream("hi")]
@@ -34,7 +38,8 @@ async def test_agent_run_stream_finish_yields_task_done(agent_factory):
34
38
 
35
39
 
36
40
  async def test_agent_run_stream_tool_call_yields_action_and_observation(
37
- agent_factory, llm: ScriptedLLM,
41
+ agent_factory,
42
+ llm: ScriptedLLM,
38
43
  ):
39
44
  """A tool-using step should yield THOUGHT → ACTION → OBSERVATION → ... → TASK_DONE."""
40
45
  step = {"n": 0}
@@ -47,7 +52,9 @@ async def test_agent_run_stream_tool_call_yields_action_and_observation(
47
52
 
48
53
  llm.routes = {"react": react}
49
54
  cfg = AgentConfig(
50
- agent_id="a", role="r", system_prompt="ReAct format.",
55
+ agent_id="a",
56
+ role="r",
57
+ system_prompt="ReAct format.",
51
58
  allowed_tools=["echo"],
52
59
  )
53
60
  agent = agent_factory(cfg, tools={"echo": EchoTool()})
@@ -68,7 +75,10 @@ async def test_agent_run_stream_tool_call_yields_action_and_observation(
68
75
  async def test_agent_run_is_drain_of_run_stream(agent_factory):
69
76
  """run() and the TASK_DONE payload from run_stream() must agree."""
70
77
  cfg = AgentConfig(
71
- agent_id="a", role="r", system_prompt="finish.", allowed_tools=[],
78
+ agent_id="a",
79
+ role="r",
80
+ system_prompt="finish.",
81
+ allowed_tools=[],
72
82
  )
73
83
  agent = agent_factory(cfg)
74
84
 
@@ -98,7 +108,11 @@ async def test_agent_forwards_token_events_when_llm_streams(agent_factory):
98
108
  yield tok
99
109
 
100
110
  cfg = AgentConfig(
101
- agent_id="a", role="r", system_prompt="ReAct.", allowed_tools=[],
111
+ agent_id="a",
112
+ role="r",
113
+ system_prompt="ReAct.",
114
+ allowed_tools=[],
115
+ stream_tokens=True,
102
116
  )
103
117
  agent = agent_factory(cfg)
104
118
  agent._llm = StreamingLLM()
@@ -119,10 +133,20 @@ def _orchestrator_routes():
119
133
  def planner(system, messages, kwargs):
120
134
  return {
121
135
  "tasks": [
122
- {"id": "t1", "agent_id": "analyst", "instruction": "do x",
123
- "depends_on": [], "on_failure": "skip"},
124
- {"id": "t2", "agent_id": "reporter", "instruction": "do y",
125
- "depends_on": ["t1"], "on_failure": "skip"},
136
+ {
137
+ "id": "t1",
138
+ "agent_id": "analyst",
139
+ "instruction": "do x",
140
+ "depends_on": [],
141
+ "on_failure": "skip",
142
+ },
143
+ {
144
+ "id": "t2",
145
+ "agent_id": "reporter",
146
+ "instruction": "do y",
147
+ "depends_on": ["t1"],
148
+ "on_failure": "skip",
149
+ },
126
150
  ],
127
151
  "rationale": "two tasks",
128
152
  }
@@ -132,8 +156,10 @@ def _orchestrator_routes():
132
156
 
133
157
  def extract(system, messages, kwargs):
134
158
  return {
135
- "semantic_facts": {}, "episodic_summary": "ok",
136
- "metadata": {}, "ttl_seconds": None,
159
+ "semantic_facts": {},
160
+ "episodic_summary": "ok",
161
+ "metadata": {},
162
+ "ttl_seconds": None,
137
163
  }
138
164
 
139
165
  return {
@@ -147,14 +173,24 @@ def _build_runtime(llm):
147
173
  tools = ToolRegistry().register(EchoTool())
148
174
  agents = (
149
175
  AgentRegistry()
150
- .register(AgentConfig(
151
- agent_id="analyst", role="r", system_prompt="ReAct.",
152
- allowed_tools=["echo"], max_steps=2,
153
- ))
154
- .register(AgentConfig(
155
- agent_id="reporter", role="r", system_prompt="ReAct.",
156
- allowed_tools=["echo"], max_steps=2,
157
- ))
176
+ .register(
177
+ AgentConfig(
178
+ agent_id="analyst",
179
+ role="r",
180
+ system_prompt="ReAct.",
181
+ allowed_tools=["echo"],
182
+ max_steps=2,
183
+ )
184
+ )
185
+ .register(
186
+ AgentConfig(
187
+ agent_id="reporter",
188
+ role="r",
189
+ system_prompt="ReAct.",
190
+ allowed_tools=["echo"],
191
+ max_steps=2,
192
+ )
193
+ )
158
194
  )
159
195
  memory = MemoryManager(
160
196
  semantic_store=InMemorySemanticStore(),
@@ -162,10 +198,15 @@ def _build_runtime(llm):
162
198
  llm=llm,
163
199
  )
164
200
  return AgentRuntime(
165
- agent_registry=agents, tool_registry=tools, memory=memory, llm=llm,
201
+ agent_registry=agents,
202
+ tool_registry=tools,
203
+ memory=memory,
204
+ llm=llm,
166
205
  guardrail_config=GuardrailConfig(
167
- max_total_cost_usd=5.0, max_wall_time_seconds=30,
168
- max_replan_count=1, confidence_threshold=0.5,
206
+ max_total_cost_usd=5.0,
207
+ max_wall_time_seconds=30,
208
+ max_replan_count=1,
209
+ confidence_threshold=0.5,
169
210
  ),
170
211
  )
171
212