penguiflow 2.2.0__tar.gz → 2.2.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.

Potentially problematic release.


This version of penguiflow might be problematic. Click here for more details.

Files changed (58) hide show
  1. {penguiflow-2.2.0 → penguiflow-2.2.2}/PKG-INFO +42 -68
  2. {penguiflow-2.2.0 → penguiflow-2.2.2}/README.md +41 -67
  3. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/__init__.py +1 -1
  4. penguiflow-2.2.2/penguiflow/planner/__init__.py +27 -0
  5. penguiflow-2.2.2/penguiflow/planner/prompts.py +243 -0
  6. penguiflow-2.2.2/penguiflow/planner/react.py +1339 -0
  7. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow.egg-info/PKG-INFO +42 -68
  8. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow.egg-info/SOURCES.txt +3 -0
  9. {penguiflow-2.2.0 → penguiflow-2.2.2}/pyproject.toml +2 -2
  10. {penguiflow-2.2.0 → penguiflow-2.2.2}/LICENSE +0 -0
  11. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/admin.py +0 -0
  12. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/bus.py +0 -0
  13. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/catalog.py +0 -0
  14. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/core.py +0 -0
  15. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/debug.py +0 -0
  16. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/errors.py +0 -0
  17. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/metrics.py +0 -0
  18. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/middlewares.py +0 -0
  19. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/node.py +0 -0
  20. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/patterns.py +0 -0
  21. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/policies.py +0 -0
  22. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/registry.py +0 -0
  23. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/remote.py +0 -0
  24. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/state.py +0 -0
  25. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/streaming.py +0 -0
  26. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/testkit.py +0 -0
  27. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/types.py +0 -0
  28. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow/viz.py +0 -0
  29. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow.egg-info/dependency_links.txt +0 -0
  30. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow.egg-info/entry_points.txt +0 -0
  31. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow.egg-info/requires.txt +0 -0
  32. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow.egg-info/top_level.txt +0 -0
  33. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow_a2a/__init__.py +0 -0
  34. {penguiflow-2.2.0 → penguiflow-2.2.2}/penguiflow_a2a/server.py +0 -0
  35. {penguiflow-2.2.0 → penguiflow-2.2.2}/setup.cfg +0 -0
  36. {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_a2a_server.py +0 -0
  37. {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_budgets.py +0 -0
  38. {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_cancel.py +0 -0
  39. {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_catalog.py +0 -0
  40. {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_controller.py +0 -0
  41. {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_core.py +0 -0
  42. {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_distribution_hooks.py +0 -0
  43. {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_errors.py +0 -0
  44. {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_metadata.py +0 -0
  45. {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_metrics.py +0 -0
  46. {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_middlewares.py +0 -0
  47. {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_node.py +0 -0
  48. {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_patterns.py +0 -0
  49. {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_planner_prompts.py +0 -0
  50. {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_property_based.py +0 -0
  51. {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_react_planner.py +0 -0
  52. {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_registry.py +0 -0
  53. {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_remote.py +0 -0
  54. {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_routing_policy.py +0 -0
  55. {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_streaming.py +0 -0
  56. {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_testkit.py +0 -0
  57. {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_types.py +0 -0
  58. {penguiflow-2.2.0 → penguiflow-2.2.2}/tests/test_viz.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: penguiflow
3
- Version: 2.2.0
3
+ Version: 2.2.2
4
4
  Summary: Async agent orchestration primitives.
5
5
  Author: PenguiFlow Team
6
6
  License: MIT License
@@ -53,21 +53,11 @@ Dynamic: license-file
53
53
  </p>
54
54
 
55
55
  <p align="center">
56
- <a href="https://github.com/penguiflow/penguiflow/actions/workflows/ci.yml">
57
- <img src="https://github.com/penguiflow/penguiflow/actions/workflows/ci.yml/badge.svg" alt="CI Status">
58
- </a>
59
- <a href="https://github.com/penguiflow/penguiflow">
60
- <img src="https://img.shields.io/badge/coverage-85%25-brightgreen" alt="Coverage">
61
- </a>
62
- <a href="https://nightly.link/penguiflow/penguiflow/workflows/benchmarks/main/benchmarks.json.zip">
63
- <img src="https://img.shields.io/badge/benchmarks-latest-orange" alt="Benchmarks">
64
- </a>
65
- <a href="https://pypi.org/project/penguiflow/">
66
- <img src="https://img.shields.io/pypi/v/penguiflow.svg" alt="PyPI version">
67
- </a>
68
- <a href="https://github.com/penguiflow/penguiflow/blob/main/LICENSE">
69
- <img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License">
70
- </a>
56
+ <a href="https://github.com/hurtener/penguiflow/actions/workflows/ci.yml"><img src="https://github.com/hurtener/penguiflow/actions/workflows/ci.yml/badge.svg" alt="CI Status"></a>
57
+ <a href="https://github.com/hurtener/penguiflow"><img src="https://img.shields.io/badge/coverage-85%25-brightgreen" alt="Coverage"></a>
58
+ <a href="https://nightly.link/hurtener/penguiflow/workflows/benchmarks/main/benchmarks.json.zip"><img src="https://img.shields.io/badge/benchmarks-latest-orange" alt="Benchmarks"></a>
59
+ <a href="https://pypi.org/project/penguiflow/"><img src="https://img.shields.io/pypi/v/penguiflow.svg" alt="PyPI version"></a>
60
+ <a href="https://github.com/hurtener/penguiflow/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
71
61
  </p>
72
62
 
73
63
  **Async-first orchestration library for multi-agent and data pipelines**
@@ -81,6 +71,7 @@ It provides:
81
71
  * **Retries, timeouts, backpressure**
82
72
  * **Streaming chunks** (LLM-style token emission with `Context.emit_chunk`)
83
73
  * **Dynamic loops** (controller nodes)
74
+ * **LLM-driven orchestration** (`ReactPlanner` for autonomous multi-step workflows with tool selection, parallel execution, and pause/resume)
84
75
  * **Runtime playbooks** (callable subflows with shared metadata)
85
76
  * **Per-trace cancellation** (`PenguiFlow.cancel` with `TraceCancelled` surfacing in nodes)
86
77
  * **Deadlines & budgets** (`Message.deadline_s`, `WM.budget_hops`, and `WM.budget_tokens` guardrails that you can leave unset/`None`)
@@ -369,69 +360,52 @@ The new `penguiflow.testkit` module keeps unit tests tiny:
369
360
  The harness is covered by `tests/test_testkit.py` and demonstrated in
370
361
  `examples/testkit_demo/`.
371
362
 
372
- ### JSON-only ReAct planner (Phase A)
363
+ ### React Planner - LLM-Driven Orchestration
373
364
 
374
- Phase A introduces a lightweight planner loop that keeps PenguiFlow typed and
375
- deterministic:
365
+ Build autonomous agents that select and execute tools dynamically using the ReAct (Reasoning + Acting) pattern:
376
366
 
377
- * `penguiflow.catalog.NodeSpec` + `build_catalog` turn registered nodes into
378
- tool descriptors with JSON Schemas derived from your Pydantic models.
379
- * `penguiflow.planner.ReactPlanner` drives a JSON-only ReAct loop over those
380
- descriptors, validating every LLM action with Pydantic and replaying invalid
381
- steps to request corrections.
382
- * LiteLLM stays optional—install `penguiflow[planner]` or inject a custom
383
- `llm_client` for deterministic/offline runs.
384
-
385
- See `examples/react_minimal/` for a stubbed end-to-end run.
386
-
387
- ### Trajectory summarisation & pause/resume (Phase B)
388
-
389
- Phase B adds the tools you need for longer-running, approval-driven flows:
390
-
391
- * **Token-aware summaries** — `Trajectory.compress()` keeps a compact state and
392
- the planner can route summaries through a cheaper `summarizer_llm` before
393
- asking for the next action.
394
- * **`PlannerPause` contract** — nodes can call `await ctx.pause(...)` to return a
395
- typed pause payload. Resume the run later with `ReactPlanner.resume(token, user_input=...)`.
396
- * **Developer hints** — pass `planning_hints={...}` to enforce disallowed tools,
397
- preferred ordering, or parallelism ceilings.
367
+ ```python
368
+ from penguiflow import ReactPlanner, tool, build_catalog
398
369
 
399
- All three features are exercised in `examples/react_pause_resume/`, which runs
400
- entirely offline with stubbed LLM responses.
370
+ @tool(desc="Search documentation")
371
+ async def search_docs(args: Query, ctx) -> Documents:
372
+ return Documents(results=await search(args.text))
401
373
 
402
- ### Adaptive re-planning & budgets (Phase C)
374
+ @tool(desc="Summarize results")
375
+ async def summarize(args: Documents, ctx) -> Summary:
376
+ return Summary(text=await llm_summarize(args.results))
403
377
 
404
- Phase C closes the loop when things go sideways:
378
+ planner = ReactPlanner(
379
+ llm="gpt-4",
380
+ catalog=build_catalog([search_docs, summarize], registry),
381
+ max_iters=10
382
+ )
405
383
 
406
- * **Structured failure feedback** if a tool raises after exhausting its retries,
407
- the planner records `{failure: {node, args, error_code, suggestion}}` and feeds
408
- it back to the LLM, prompting a constrained re-plan instead of aborting.
409
- * **Hard guardrails** — configure wall-clock deadlines and hop budgets directly
410
- on `ReactPlanner`; attempts beyond the allotted hops surface deterministic
411
- violations and ultimately finish with `reason="budget_exhausted"` alongside a
412
- constraint snapshot.
413
- * **Typed exit reasons** — runs now finish with one of
414
- `answer_complete`, `no_path`, or `budget_exhausted`, keeping downstream code
415
- simple and machine-checkable.
384
+ result = await planner.run("Explain PenguiFlow routing")
385
+ print(result.payload) # LLM orchestrated search summarize automatically
386
+ ```
416
387
 
417
- The new `examples/react_replan/` sample shows a retrieval timeout automatically
418
- recover via a cached index without leaving the JSON-only contract.
388
+ **Key capabilities:**
419
389
 
420
- ### Parallel fan-out & joins (Phase D)
390
+ * **Autonomous tool selection** LLM decides which tools to call and in what order based on your query
391
+ * **Type-safe execution** — All tool inputs/outputs validated with Pydantic, JSON schemas auto-generated from models
392
+ * **Parallel execution** — LLM can fan out to multiple tools concurrently with automatic result joining
393
+ * **Pause/resume workflows** — Add approval gates with `await ctx.pause()`, resume later with user input
394
+ * **Adaptive replanning** — Tool failures feed structured error suggestions back to LLM for recovery
395
+ * **Constraint enforcement** — Set hop budgets, deadlines, and token limits to prevent runaway execution
396
+ * **Planning hints** — Guide LLM behavior with ordering preferences, parallel groups, and tool filters
421
397
 
422
- Phase D lets the planner propose sets of independent tool calls and join them
423
- without leaving the typed surface area:
398
+ **Model support:**
399
+ * Install `penguiflow[planner]` for LiteLLM integration (100+ models: OpenAI, Anthropic, Azure, etc.)
400
+ * Or inject a custom `llm_client` for deterministic/offline testing
424
401
 
425
- * **Parallel `plan` blocks** — the LLM can return `{"plan": [...]}` actions
426
- where each branch is validated against the catalog and executed concurrently.
427
- * **Typed joins** provide a `{"join": {"node": ...}}` descriptor and the
428
- planner will aggregate results, auto-populate fields like `expect`, `results`,
429
- or `failures`, and feed branch metadata through `ctx.meta` for the join node.
430
- * **Deterministic telemetry** — branch errors, pauses, and joins are recorded as
431
- structured observations so follow-up actions can re-plan or finish cleanly.
402
+ **Examples:**
403
+ * `examples/react_minimal/` Basic sequential flow with stub LLM
404
+ * `examples/react_parallel/`Parallel shard fan-out with join node
405
+ * `examples/react_pause_resume/` Approval workflow with planning hints
406
+ * `examples/react_replan/` Adaptive recovery from tool failures
432
407
 
433
- See `examples/react_parallel/` for a shard fan-out that merges responses in one
434
- round-trip.
408
+ See **manual.md Section 19** for complete documentation.
435
409
 
436
410
 
437
411
  ## 🧭 Repo Structure
@@ -5,21 +5,11 @@
5
5
  </p>
6
6
 
7
7
  <p align="center">
8
- <a href="https://github.com/penguiflow/penguiflow/actions/workflows/ci.yml">
9
- <img src="https://github.com/penguiflow/penguiflow/actions/workflows/ci.yml/badge.svg" alt="CI Status">
10
- </a>
11
- <a href="https://github.com/penguiflow/penguiflow">
12
- <img src="https://img.shields.io/badge/coverage-85%25-brightgreen" alt="Coverage">
13
- </a>
14
- <a href="https://nightly.link/penguiflow/penguiflow/workflows/benchmarks/main/benchmarks.json.zip">
15
- <img src="https://img.shields.io/badge/benchmarks-latest-orange" alt="Benchmarks">
16
- </a>
17
- <a href="https://pypi.org/project/penguiflow/">
18
- <img src="https://img.shields.io/pypi/v/penguiflow.svg" alt="PyPI version">
19
- </a>
20
- <a href="https://github.com/penguiflow/penguiflow/blob/main/LICENSE">
21
- <img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License">
22
- </a>
8
+ <a href="https://github.com/hurtener/penguiflow/actions/workflows/ci.yml"><img src="https://github.com/hurtener/penguiflow/actions/workflows/ci.yml/badge.svg" alt="CI Status"></a>
9
+ <a href="https://github.com/hurtener/penguiflow"><img src="https://img.shields.io/badge/coverage-85%25-brightgreen" alt="Coverage"></a>
10
+ <a href="https://nightly.link/hurtener/penguiflow/workflows/benchmarks/main/benchmarks.json.zip"><img src="https://img.shields.io/badge/benchmarks-latest-orange" alt="Benchmarks"></a>
11
+ <a href="https://pypi.org/project/penguiflow/"><img src="https://img.shields.io/pypi/v/penguiflow.svg" alt="PyPI version"></a>
12
+ <a href="https://github.com/hurtener/penguiflow/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
23
13
  </p>
24
14
 
25
15
  **Async-first orchestration library for multi-agent and data pipelines**
@@ -33,6 +23,7 @@ It provides:
33
23
  * **Retries, timeouts, backpressure**
34
24
  * **Streaming chunks** (LLM-style token emission with `Context.emit_chunk`)
35
25
  * **Dynamic loops** (controller nodes)
26
+ * **LLM-driven orchestration** (`ReactPlanner` for autonomous multi-step workflows with tool selection, parallel execution, and pause/resume)
36
27
  * **Runtime playbooks** (callable subflows with shared metadata)
37
28
  * **Per-trace cancellation** (`PenguiFlow.cancel` with `TraceCancelled` surfacing in nodes)
38
29
  * **Deadlines & budgets** (`Message.deadline_s`, `WM.budget_hops`, and `WM.budget_tokens` guardrails that you can leave unset/`None`)
@@ -321,69 +312,52 @@ The new `penguiflow.testkit` module keeps unit tests tiny:
321
312
  The harness is covered by `tests/test_testkit.py` and demonstrated in
322
313
  `examples/testkit_demo/`.
323
314
 
324
- ### JSON-only ReAct planner (Phase A)
315
+ ### React Planner - LLM-Driven Orchestration
325
316
 
326
- Phase A introduces a lightweight planner loop that keeps PenguiFlow typed and
327
- deterministic:
317
+ Build autonomous agents that select and execute tools dynamically using the ReAct (Reasoning + Acting) pattern:
328
318
 
329
- * `penguiflow.catalog.NodeSpec` + `build_catalog` turn registered nodes into
330
- tool descriptors with JSON Schemas derived from your Pydantic models.
331
- * `penguiflow.planner.ReactPlanner` drives a JSON-only ReAct loop over those
332
- descriptors, validating every LLM action with Pydantic and replaying invalid
333
- steps to request corrections.
334
- * LiteLLM stays optional—install `penguiflow[planner]` or inject a custom
335
- `llm_client` for deterministic/offline runs.
336
-
337
- See `examples/react_minimal/` for a stubbed end-to-end run.
338
-
339
- ### Trajectory summarisation & pause/resume (Phase B)
340
-
341
- Phase B adds the tools you need for longer-running, approval-driven flows:
342
-
343
- * **Token-aware summaries** — `Trajectory.compress()` keeps a compact state and
344
- the planner can route summaries through a cheaper `summarizer_llm` before
345
- asking for the next action.
346
- * **`PlannerPause` contract** — nodes can call `await ctx.pause(...)` to return a
347
- typed pause payload. Resume the run later with `ReactPlanner.resume(token, user_input=...)`.
348
- * **Developer hints** — pass `planning_hints={...}` to enforce disallowed tools,
349
- preferred ordering, or parallelism ceilings.
319
+ ```python
320
+ from penguiflow import ReactPlanner, tool, build_catalog
350
321
 
351
- All three features are exercised in `examples/react_pause_resume/`, which runs
352
- entirely offline with stubbed LLM responses.
322
+ @tool(desc="Search documentation")
323
+ async def search_docs(args: Query, ctx) -> Documents:
324
+ return Documents(results=await search(args.text))
353
325
 
354
- ### Adaptive re-planning & budgets (Phase C)
326
+ @tool(desc="Summarize results")
327
+ async def summarize(args: Documents, ctx) -> Summary:
328
+ return Summary(text=await llm_summarize(args.results))
355
329
 
356
- Phase C closes the loop when things go sideways:
330
+ planner = ReactPlanner(
331
+ llm="gpt-4",
332
+ catalog=build_catalog([search_docs, summarize], registry),
333
+ max_iters=10
334
+ )
357
335
 
358
- * **Structured failure feedback** if a tool raises after exhausting its retries,
359
- the planner records `{failure: {node, args, error_code, suggestion}}` and feeds
360
- it back to the LLM, prompting a constrained re-plan instead of aborting.
361
- * **Hard guardrails** — configure wall-clock deadlines and hop budgets directly
362
- on `ReactPlanner`; attempts beyond the allotted hops surface deterministic
363
- violations and ultimately finish with `reason="budget_exhausted"` alongside a
364
- constraint snapshot.
365
- * **Typed exit reasons** — runs now finish with one of
366
- `answer_complete`, `no_path`, or `budget_exhausted`, keeping downstream code
367
- simple and machine-checkable.
336
+ result = await planner.run("Explain PenguiFlow routing")
337
+ print(result.payload) # LLM orchestrated search summarize automatically
338
+ ```
368
339
 
369
- The new `examples/react_replan/` sample shows a retrieval timeout automatically
370
- recover via a cached index without leaving the JSON-only contract.
340
+ **Key capabilities:**
371
341
 
372
- ### Parallel fan-out & joins (Phase D)
342
+ * **Autonomous tool selection** LLM decides which tools to call and in what order based on your query
343
+ * **Type-safe execution** — All tool inputs/outputs validated with Pydantic, JSON schemas auto-generated from models
344
+ * **Parallel execution** — LLM can fan out to multiple tools concurrently with automatic result joining
345
+ * **Pause/resume workflows** — Add approval gates with `await ctx.pause()`, resume later with user input
346
+ * **Adaptive replanning** — Tool failures feed structured error suggestions back to LLM for recovery
347
+ * **Constraint enforcement** — Set hop budgets, deadlines, and token limits to prevent runaway execution
348
+ * **Planning hints** — Guide LLM behavior with ordering preferences, parallel groups, and tool filters
373
349
 
374
- Phase D lets the planner propose sets of independent tool calls and join them
375
- without leaving the typed surface area:
350
+ **Model support:**
351
+ * Install `penguiflow[planner]` for LiteLLM integration (100+ models: OpenAI, Anthropic, Azure, etc.)
352
+ * Or inject a custom `llm_client` for deterministic/offline testing
376
353
 
377
- * **Parallel `plan` blocks** — the LLM can return `{"plan": [...]}` actions
378
- where each branch is validated against the catalog and executed concurrently.
379
- * **Typed joins** provide a `{"join": {"node": ...}}` descriptor and the
380
- planner will aggregate results, auto-populate fields like `expect`, `results`,
381
- or `failures`, and feed branch metadata through `ctx.meta` for the join node.
382
- * **Deterministic telemetry** — branch errors, pauses, and joins are recorded as
383
- structured observations so follow-up actions can re-plan or finish cleanly.
354
+ **Examples:**
355
+ * `examples/react_minimal/` Basic sequential flow with stub LLM
356
+ * `examples/react_parallel/`Parallel shard fan-out with join node
357
+ * `examples/react_pause_resume/` Approval workflow with planning hints
358
+ * `examples/react_replan/` Adaptive recovery from tool failures
384
359
 
385
- See `examples/react_parallel/` for a shard fan-out that merges responses in one
386
- round-trip.
360
+ See **manual.md Section 19** for complete documentation.
387
361
 
388
362
 
389
363
  ## 🧭 Repo Structure
@@ -105,4 +105,4 @@ __all__ = [
105
105
  "TrajectoryStep",
106
106
  ]
107
107
 
108
- __version__ = "2.2.0"
108
+ __version__ = "2.2.2"
@@ -0,0 +1,27 @@
1
+ """Planner entry points."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .react import (
6
+ ParallelCall,
7
+ ParallelJoin,
8
+ PlannerAction,
9
+ PlannerFinish,
10
+ PlannerPause,
11
+ ReactPlanner,
12
+ Trajectory,
13
+ TrajectoryStep,
14
+ TrajectorySummary,
15
+ )
16
+
17
+ __all__ = [
18
+ "ParallelCall",
19
+ "ParallelJoin",
20
+ "PlannerAction",
21
+ "PlannerFinish",
22
+ "PlannerPause",
23
+ "ReactPlanner",
24
+ "Trajectory",
25
+ "TrajectoryStep",
26
+ "TrajectorySummary",
27
+ ]
@@ -0,0 +1,243 @@
1
+ """Prompt helpers for the React planner."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import Mapping, Sequence
7
+ from typing import Any
8
+
9
+
10
+ def render_summary(summary: Mapping[str, Any]) -> str:
11
+ return "Trajectory summary: " + _compact_json(summary)
12
+
13
+
14
+ def render_resume_user_input(user_input: str) -> str:
15
+ return f"Resume input: {user_input}"
16
+
17
+
18
+ def render_planning_hints(hints: Mapping[str, Any]) -> str:
19
+ lines: list[str] = []
20
+ constraints = hints.get("constraints")
21
+ if constraints:
22
+ lines.append(f"Respect the following constraints: {constraints}")
23
+ preferred = hints.get("preferred_order")
24
+ if preferred:
25
+ lines.append(f"Preferred order (if feasible): {preferred}")
26
+ parallels = hints.get("parallel_groups")
27
+ if parallels:
28
+ lines.append(f"Allowed parallel groups: {parallels}")
29
+ disallowed = hints.get("disallow_nodes")
30
+ if disallowed:
31
+ lines.append(f"Disallowed tools: {disallowed}")
32
+ preferred_nodes = hints.get("preferred_nodes")
33
+ if preferred_nodes:
34
+ lines.append(f"Preferred tools: {preferred_nodes}")
35
+ budget = hints.get("budget")
36
+ if budget:
37
+ lines.append(f"Budget hints: {budget}")
38
+ if not lines:
39
+ return ""
40
+ return "\n".join(lines)
41
+
42
+
43
+ def render_disallowed_node(node_name: str) -> str:
44
+ return (
45
+ f"tool '{node_name}' is not permitted by constraints. "
46
+ "Choose an allowed tool or revise the plan."
47
+ )
48
+
49
+
50
+ def render_ordering_hint_violation(expected: Sequence[str], proposed: str) -> str:
51
+ order = ", ".join(expected)
52
+ return (
53
+ "Ordering hint reminder: follow the preferred sequence "
54
+ f"[{order}]. Proposed: {proposed}. Revise the plan."
55
+ )
56
+
57
+
58
+ def render_parallel_limit(max_parallel: int) -> str:
59
+ return (
60
+ f"Parallel action exceeds max_parallel={max_parallel}. Reduce parallel fan-out."
61
+ )
62
+
63
+
64
+ def render_sequential_only(node_name: str) -> str:
65
+ return (
66
+ f"tool '{node_name}' must run sequentially. "
67
+ "Do not include it in a parallel plan."
68
+ )
69
+
70
+
71
+ def render_parallel_setup_error(errors: Sequence[str]) -> str:
72
+ detail = "; ".join(errors)
73
+ return f"Parallel plan invalid: {detail}. Revise the plan and retry."
74
+
75
+
76
+ def render_empty_parallel_plan() -> str:
77
+ return "Parallel plan must include at least one branch in 'plan'."
78
+
79
+
80
+ def render_parallel_with_next_node(next_node: str) -> str:
81
+ return (
82
+ f"Parallel plan cannot set next_node='{next_node}'. "
83
+ "Use 'join' to continue or finish the run explicitly."
84
+ )
85
+
86
+
87
+ def render_parallel_unknown_failure(node_name: str) -> str:
88
+ return (
89
+ f"tool '{node_name}' failed during parallel execution. "
90
+ "Investigate the tool and adjust the plan."
91
+ )
92
+
93
+
94
+ def build_summarizer_messages(
95
+ query: str,
96
+ history: Sequence[Mapping[str, Any]],
97
+ base_summary: Mapping[str, Any],
98
+ ) -> list[dict[str, str]]:
99
+ return [
100
+ {
101
+ "role": "system",
102
+ "content": (
103
+ "You are a summariser producing compact JSON state. "
104
+ "Respond with valid JSON matching the TrajectorySummary schema."
105
+ ),
106
+ },
107
+ {
108
+ "role": "user",
109
+ "content": _compact_json(
110
+ {
111
+ "query": query,
112
+ "history": list(history),
113
+ "current_summary": dict(base_summary),
114
+ }
115
+ ),
116
+ },
117
+ ]
118
+
119
+
120
+ def _compact_json(data: Any) -> str:
121
+ return json.dumps(data, ensure_ascii=False, sort_keys=True)
122
+
123
+
124
+ def render_tool(record: Mapping[str, Any]) -> str:
125
+ args_schema = _compact_json(record["args_schema"])
126
+ out_schema = _compact_json(record["out_schema"])
127
+ tags = ", ".join(record.get("tags", ()))
128
+ scopes = ", ".join(record.get("auth_scopes", ()))
129
+ parts = [
130
+ f"- name: {record['name']}",
131
+ f" desc: {record['desc']}",
132
+ f" side_effects: {record['side_effects']}",
133
+ f" args_schema: {args_schema}",
134
+ f" out_schema: {out_schema}",
135
+ ]
136
+ if tags:
137
+ parts.append(f" tags: {tags}")
138
+ if scopes:
139
+ parts.append(f" auth_scopes: {scopes}")
140
+ if record.get("cost_hint"):
141
+ parts.append(f" cost_hint: {record['cost_hint']}")
142
+ if record.get("latency_hint_ms") is not None:
143
+ parts.append(f" latency_hint_ms: {record['latency_hint_ms']}")
144
+ if record.get("safety_notes"):
145
+ parts.append(f" safety_notes: {record['safety_notes']}")
146
+ if record.get("extra"):
147
+ parts.append(f" extra: {_compact_json(record['extra'])}")
148
+ return "\n".join(parts)
149
+
150
+
151
+ def build_system_prompt(
152
+ catalog: Sequence[Mapping[str, Any]],
153
+ *,
154
+ extra: str | None = None,
155
+ planning_hints: Mapping[str, Any] | None = None,
156
+ ) -> str:
157
+ rendered_tools = "\n".join(render_tool(item) for item in catalog)
158
+ prompt = [
159
+ "You are PenguiFlow ReactPlanner, a JSON-only planner.",
160
+ "Follow these rules strictly:",
161
+ "1. Respond with valid JSON matching the PlannerAction schema.",
162
+ "2. Use the provided tools when necessary; never invent new tool names.",
163
+ "3. Keep 'thought' concise and factual.",
164
+ "4. When the task is complete, set 'next_node' to null "
165
+ "and include the final payload in 'args'.",
166
+ "5. Do not emit plain text outside JSON.",
167
+ "",
168
+ "Available tools:",
169
+ rendered_tools or "(none)",
170
+ ]
171
+ if extra:
172
+ prompt.extend(["", "Additional guidance:", extra])
173
+ if planning_hints:
174
+ rendered_hints = render_planning_hints(planning_hints)
175
+ if rendered_hints:
176
+ prompt.extend(["", rendered_hints])
177
+ return "\n".join(prompt)
178
+
179
+
180
+ def build_user_prompt(query: str, context_meta: Mapping[str, Any] | None = None) -> str:
181
+ if context_meta:
182
+ return _compact_json({"query": query, "context": dict(context_meta)})
183
+ return _compact_json({"query": query})
184
+
185
+
186
+ def render_observation(
187
+ *,
188
+ observation: Any | None,
189
+ error: str | None,
190
+ failure: Mapping[str, Any] | None = None,
191
+ ) -> str:
192
+ payload: dict[str, Any] = {}
193
+ if observation is not None:
194
+ payload["observation"] = observation
195
+ if error:
196
+ payload["error"] = error
197
+ if failure:
198
+ payload["failure"] = dict(failure)
199
+ if not payload:
200
+ payload["observation"] = None
201
+ return _compact_json(payload)
202
+
203
+
204
+ def render_hop_budget_violation(limit: int) -> str:
205
+ return (
206
+ "Hop budget exhausted; you have used all available tool calls. "
207
+ "Finish with the best answer so far or reply with no_path."
208
+ f" (limit={limit})"
209
+ )
210
+
211
+
212
+ def render_deadline_exhausted() -> str:
213
+ return (
214
+ "Deadline reached. Provide the best available conclusion or return no_path."
215
+ )
216
+
217
+
218
+ def render_validation_error(node_name: str, error: str) -> str:
219
+ return (
220
+ f"args for tool '{node_name}' did not validate: {error}. "
221
+ "Return corrected JSON."
222
+ )
223
+
224
+
225
+ def render_output_validation_error(node_name: str, error: str) -> str:
226
+ return (
227
+ f"tool '{node_name}' returned data that did not validate: {error}. "
228
+ "Ensure the tool output matches the declared schema."
229
+ )
230
+
231
+
232
+ def render_invalid_node(node_name: str, available: Sequence[str]) -> str:
233
+ options = ", ".join(sorted(available))
234
+ return (
235
+ f"tool '{node_name}' is not in the catalog. Choose one of: {options}."
236
+ )
237
+
238
+
239
+ def render_repair_message(error: str) -> str:
240
+ return (
241
+ "Previous response was invalid JSON or schema mismatch: "
242
+ f"{error}. Reply with corrected JSON only."
243
+ )