chorusgraph 1.0.1__py3-none-any.whl

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 (156) hide show
  1. chorusgraph/__init__.py +57 -0
  2. chorusgraph/adapter/__init__.py +5 -0
  3. chorusgraph/adapter/wrap.py +249 -0
  4. chorusgraph/agents/__init__.py +43 -0
  5. chorusgraph/agents/agent.py +69 -0
  6. chorusgraph/agents/agent_node.py +82 -0
  7. chorusgraph/agents/loop.py +137 -0
  8. chorusgraph/agents/plan_solve.py +50 -0
  9. chorusgraph/agents/plan_utils.py +98 -0
  10. chorusgraph/agents/policy.py +100 -0
  11. chorusgraph/agents/react.py +48 -0
  12. chorusgraph/agents/react_utils.py +31 -0
  13. chorusgraph/agents/reflection.py +54 -0
  14. chorusgraph/agents/strategies/__init__.py +20 -0
  15. chorusgraph/agents/strategies/base.py +53 -0
  16. chorusgraph/agents/strategies/plan_solve_strategy.py +141 -0
  17. chorusgraph/agents/strategies/react_strategy.py +141 -0
  18. chorusgraph/agents/strategies/reflection_strategy.py +64 -0
  19. chorusgraph/cache_gate/__init__.py +23 -0
  20. chorusgraph/cache_gate/backend.py +251 -0
  21. chorusgraph/cache_gate/decision.py +33 -0
  22. chorusgraph/cache_gate/gate.py +168 -0
  23. chorusgraph/cache_gate/scope.py +34 -0
  24. chorusgraph/cache_gate/seed_policy.py +56 -0
  25. chorusgraph/cache_gate/sidecar.py +220 -0
  26. chorusgraph/cache_gate/thresholds.py +49 -0
  27. chorusgraph/checkpoint/__init__.py +27 -0
  28. chorusgraph/checkpoint/prism.py +93 -0
  29. chorusgraph/compat/__init__.py +0 -0
  30. chorusgraph/compat/checkpoint_import.py +75 -0
  31. chorusgraph/compat/langgraph.py +160 -0
  32. chorusgraph/compat/otel_exporter.py +36 -0
  33. chorusgraph/compat/tool_node.py +24 -0
  34. chorusgraph/compose/__init__.py +47 -0
  35. chorusgraph/compose/adapters/__init__.py +13 -0
  36. chorusgraph/compose/adapters/keyword_retrieval.py +51 -0
  37. chorusgraph/compose/adapters/memory.py +47 -0
  38. chorusgraph/compose/adapters/prism_cache.py +100 -0
  39. chorusgraph/compose/adapters/prismrag_retrieval.py +228 -0
  40. chorusgraph/compose/adapters/redis_cache.py +178 -0
  41. chorusgraph/compose/defaults.py +58 -0
  42. chorusgraph/compose/ports.py +129 -0
  43. chorusgraph/compose/stack.py +172 -0
  44. chorusgraph/core/__init__.py +22 -0
  45. chorusgraph/core/bus.py +93 -0
  46. chorusgraph/core/cache_interceptor.py +136 -0
  47. chorusgraph/core/channels.py +255 -0
  48. chorusgraph/core/constants.py +6 -0
  49. chorusgraph/core/envelope.py +59 -0
  50. chorusgraph/core/graph.py +261 -0
  51. chorusgraph/core/ir.py +155 -0
  52. chorusgraph/core/node.py +172 -0
  53. chorusgraph/core/pending_writes.py +128 -0
  54. chorusgraph/core/persistence.py +324 -0
  55. chorusgraph/core/scheduler.py +1613 -0
  56. chorusgraph/core/send.py +177 -0
  57. chorusgraph/core/subgraph.py +180 -0
  58. chorusgraph/core/subgraph_transport.py +208 -0
  59. chorusgraph/core/trace.py +226 -0
  60. chorusgraph/core/transport_router.py +140 -0
  61. chorusgraph/embedders.py +51 -0
  62. chorusgraph/examples/__init__.py +0 -0
  63. chorusgraph/examples/demo_graph.py +129 -0
  64. chorusgraph/examples/finance_agent/__init__.py +0 -0
  65. chorusgraph/examples/finance_agent/gemini_client.py +108 -0
  66. chorusgraph/examples/finance_agent/graph.py +98 -0
  67. chorusgraph/examples/finance_agent/nodes.py +480 -0
  68. chorusgraph/examples/finance_agent/pattern_nodes.py +194 -0
  69. chorusgraph/examples/finance_agent/patterns_graph.py +201 -0
  70. chorusgraph/examples/finance_agent/run.py +64 -0
  71. chorusgraph/examples/finance_agent/run_memory_demo.py +193 -0
  72. chorusgraph/examples/finance_agent/run_patterns_demo.py +92 -0
  73. chorusgraph/examples/finance_agent/runtime.py +126 -0
  74. chorusgraph/examples/multi_agent_graph.py +54 -0
  75. chorusgraph/func.py +84 -0
  76. chorusgraph/ledger/__init__.py +15 -0
  77. chorusgraph/ledger/instrument.py +57 -0
  78. chorusgraph/ledger/models.py +46 -0
  79. chorusgraph/ledger/query.py +24 -0
  80. chorusgraph/ledger/sink.py +267 -0
  81. chorusgraph/memory/__init__.py +16 -0
  82. chorusgraph/memory/async_digest.py +80 -0
  83. chorusgraph/memory/cortex_compat.py +52 -0
  84. chorusgraph/memory/cortex_service.py +195 -0
  85. chorusgraph/memory/recall.py +28 -0
  86. chorusgraph/memory/structured_recall.py +59 -0
  87. chorusgraph/nodes/__init__.py +33 -0
  88. chorusgraph/nodes/retrieve.py +110 -0
  89. chorusgraph/nodes/roles.py +102 -0
  90. chorusgraph/nodes/tool.py +207 -0
  91. chorusgraph/observability/__init__.py +18 -0
  92. chorusgraph/observability/health.py +60 -0
  93. chorusgraph/observability/logging.py +58 -0
  94. chorusgraph/observability/metrics.py +51 -0
  95. chorusgraph/observability/otel.py +44 -0
  96. chorusgraph/persistence/__init__.py +22 -0
  97. chorusgraph/persistence/backup.py +63 -0
  98. chorusgraph/persistence/cortex_factory.py +66 -0
  99. chorusgraph/persistence/lifecycle.py +107 -0
  100. chorusgraph/persistence/migrations.py +114 -0
  101. chorusgraph/persistence/sqlite_graph_store.py +126 -0
  102. chorusgraph/policy/__init__.py +5 -0
  103. chorusgraph/policy/embedder_guard.py +74 -0
  104. chorusgraph/public.py +62 -0
  105. chorusgraph/resilience/__init__.py +23 -0
  106. chorusgraph/resilience/circuit_breaker.py +104 -0
  107. chorusgraph/resilience/errors.py +72 -0
  108. chorusgraph/resilience/executor.py +71 -0
  109. chorusgraph/resilience/idempotency.py +29 -0
  110. chorusgraph/resilience/partial.py +34 -0
  111. chorusgraph/resilience/policy.py +42 -0
  112. chorusgraph/sections/__init__.py +5 -0
  113. chorusgraph/sections/models.py +46 -0
  114. chorusgraph/sections/profiles.py +46 -0
  115. chorusgraph/security/__init__.py +21 -0
  116. chorusgraph/security/auth.py +42 -0
  117. chorusgraph/security/cache.py +47 -0
  118. chorusgraph/security/pii.py +35 -0
  119. chorusgraph/security/tools.py +71 -0
  120. chorusgraph/security/transport.py +43 -0
  121. chorusgraph/shadow/__init__.py +19 -0
  122. chorusgraph/shadow/dataset/__init__.py +0 -0
  123. chorusgraph/shadow/harness.py +182 -0
  124. chorusgraph/shadow/replay/__init__.py +19 -0
  125. chorusgraph/shadow/replay/cli.py +43 -0
  126. chorusgraph/shadow/replay/ingest.py +30 -0
  127. chorusgraph/shadow/replay/policies.py +18 -0
  128. chorusgraph/shadow/replay/replay.py +166 -0
  129. chorusgraph/shadow/replay/report.py +116 -0
  130. chorusgraph/shadow/replay/schema.py +31 -0
  131. chorusgraph/shadow/replay/stats.py +69 -0
  132. chorusgraph/shadow/report.py +96 -0
  133. chorusgraph/shadow/results_store.py +107 -0
  134. chorusgraph/tenant/__init__.py +17 -0
  135. chorusgraph/tenant/context.py +27 -0
  136. chorusgraph/tenant/isolation.py +33 -0
  137. chorusgraph/tenant/limits.py +59 -0
  138. chorusgraph/transforms/__init__.py +17 -0
  139. chorusgraph/transforms/cortex_projector.py +61 -0
  140. chorusgraph/transforms/intent.py +59 -0
  141. chorusgraph/transforms/projector.py +49 -0
  142. chorusgraph/transforms/templates.py +129 -0
  143. chorusgraph/transport/__init__.py +30 -0
  144. chorusgraph/transport/chorus.py +222 -0
  145. chorusgraph/transport/context.py +63 -0
  146. chorusgraph/transport/envelope.py +79 -0
  147. chorusgraph/transport/inproc.py +61 -0
  148. chorusgraph/transport/modes.py +23 -0
  149. chorusgraph/transport/prismapi.py +164 -0
  150. chorusgraph/transport/spine.py +66 -0
  151. chorusgraph-1.0.1.dist-info/METADATA +283 -0
  152. chorusgraph-1.0.1.dist-info/RECORD +156 -0
  153. chorusgraph-1.0.1.dist-info/WHEEL +5 -0
  154. chorusgraph-1.0.1.dist-info/entry_points.txt +9 -0
  155. chorusgraph-1.0.1.dist-info/licenses/LICENSE +17 -0
  156. chorusgraph-1.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,57 @@
1
+ """ChorusGraph — native Prism execution engine with cache, memory, and Route Ledger."""
2
+
3
+ __version__ = "1.0.1"
4
+
5
+ from chorusgraph.adapter import RunnableWithLedger, wrap
6
+ from chorusgraph.cache_gate import Decision, DecisionKind, SidecarStore, gate, seed_cache_entry
7
+ from chorusgraph.checkpoint import PrismCheckpointer, create_checkpointer, sqlite_checkpointer
8
+ from chorusgraph.compose import ChorusStack, RedisCacheBackend
9
+ from chorusgraph.core import END, START, CompiledGraph, Graph, NodeContext, NodeFn
10
+ from chorusgraph.transport import TransportMode, publish_hop
11
+ from chorusgraph.ledger import (
12
+ LedgerStep,
13
+ LedgerSink,
14
+ PostgresLedgerSink,
15
+ RouteLedger,
16
+ SqliteLedgerSink,
17
+ get_run,
18
+ list_runs,
19
+ )
20
+ from chorusgraph.memory import CortexMemoryService, get_cortex_service
21
+ from chorusgraph.sections import CachePolicy, Section
22
+ from chorusgraph.shadow import run_shadow_measurement
23
+
24
+ __all__ = [
25
+ "__version__",
26
+ "CachePolicy",
27
+ "CompiledGraph",
28
+ "CortexMemoryService",
29
+ "ChorusStack",
30
+ "Decision",
31
+ "DecisionKind",
32
+ "END",
33
+ "Graph",
34
+ "NodeContext",
35
+ "NodeFn",
36
+ "LedgerSink",
37
+ "LedgerStep",
38
+ "TransportMode",
39
+ "PrismCheckpointer",
40
+ "RedisCacheBackend",
41
+ "RouteLedger",
42
+ "RunnableWithLedger",
43
+ "Section",
44
+ "SidecarStore",
45
+ "SqliteLedgerSink",
46
+ "START",
47
+ "create_checkpointer",
48
+ "gate",
49
+ "get_cortex_service",
50
+ "get_run",
51
+ "list_runs",
52
+ "publish_hop",
53
+ "run_shadow_measurement",
54
+ "seed_cache_entry",
55
+ "sqlite_checkpointer",
56
+ "wrap",
57
+ ]
@@ -0,0 +1,5 @@
1
+ """LangGraph adapter — observe execution, emit Route Ledger."""
2
+
3
+ from chorusgraph.adapter.wrap import RunnableWithLedger, wrap
4
+
5
+ __all__ = ["RunnableWithLedger", "wrap"]
@@ -0,0 +1,249 @@
1
+ """Adapter — native CompiledGraph ledger + legacy LangGraph fallback."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from datetime import datetime, timezone
7
+ from typing import Any, Dict, List, Optional
8
+ from uuid import uuid4
9
+
10
+ from chorusgraph.ledger.models import LedgerStep, RouteLedger
11
+ from chorusgraph.ledger.sink import LedgerSink, SqliteLedgerSink
12
+
13
+ _APPEND_KEYS = frozenset(
14
+ {
15
+ "rule_chain",
16
+ "prism_sequence",
17
+ "hop_metrics",
18
+ "vector_hops",
19
+ "pipeline_trace",
20
+ "tool_calls",
21
+ "conversation_history",
22
+ "agent_trace",
23
+ }
24
+ )
25
+
26
+
27
+ def _merge_state(base: Dict[str, Any], update: Dict[str, Any]) -> Dict[str, Any]:
28
+ merged = dict(base)
29
+ for key, value in update.items():
30
+ if key in _APPEND_KEYS and key in merged:
31
+ left = merged[key]
32
+ if isinstance(left, list) and isinstance(value, list):
33
+ merged[key] = left + value
34
+ continue
35
+ if (
36
+ key not in _APPEND_KEYS
37
+ and key in merged
38
+ and isinstance(merged[key], list)
39
+ and isinstance(value, list)
40
+ ):
41
+ merged[key] = merged[key] + value
42
+ else:
43
+ merged[key] = value
44
+ return merged
45
+
46
+
47
+ def _parse_edge_from_triggers(triggers: Optional[List[str]]) -> Optional[str]:
48
+ if not triggers:
49
+ return None
50
+ for trigger in triggers:
51
+ if trigger.startswith("branch:to:"):
52
+ return trigger.split(":", 2)[-1]
53
+ return None
54
+
55
+
56
+ def _extract_grounding_score(
57
+ result: Dict[str, Any],
58
+ node_name: str,
59
+ *,
60
+ merged_state: Optional[Dict[str, Any]] = None,
61
+ ) -> Optional[float]:
62
+ if node_name != "writer":
63
+ return None
64
+ if result.get("memory_confidence") is not None:
65
+ return float(result["memory_confidence"])
66
+ if merged_state is not None and merged_state.get("memory_confidence") is not None:
67
+ return float(merged_state["memory_confidence"])
68
+ return None
69
+
70
+
71
+ def _append_agent_trace_steps(
72
+ ledger: RouteLedger,
73
+ node: str,
74
+ result: Dict[str, Any],
75
+ *,
76
+ timestamp: datetime,
77
+ ) -> None:
78
+ trace = result.get("agent_trace") or []
79
+ for step in trace:
80
+ if isinstance(step, dict):
81
+ kind = step.get("kind") or "step"
82
+ content = step.get("content") or ""
83
+ else:
84
+ kind = getattr(step, "kind", "step")
85
+ if hasattr(kind, "value"):
86
+ kind = kind.value
87
+ content = getattr(step, "content", str(step))
88
+ ledger.add_step(
89
+ LedgerStep(
90
+ node=f"{node}/{kind}",
91
+ rule_chain=[content[:500] if content else kind],
92
+ timestamp=timestamp,
93
+ )
94
+ )
95
+
96
+
97
+ def _extract_rule_chain(
98
+ result: Dict[str, Any],
99
+ node_name: str,
100
+ *,
101
+ merged_state: Optional[Dict[str, Any]] = None,
102
+ ) -> Optional[List[str]]:
103
+ chain = result.get("rule_chain")
104
+ if chain:
105
+ return list(chain)
106
+
107
+ sequence = result.get("prism_sequence") or []
108
+ if not sequence and merged_state is not None:
109
+ sequence = merged_state.get("prism_sequence") or []
110
+ for envelope in reversed(sequence):
111
+ if envelope.get("agent_id") == node_name and envelope.get("rule_chain"):
112
+ return list(envelope["rule_chain"])
113
+ return None
114
+
115
+
116
+ class RunnableWithLedger:
117
+ """Wraps a compiled graph and ensures Route Ledger persistence."""
118
+
119
+ def __init__(
120
+ self,
121
+ compiled_graph: Any,
122
+ *,
123
+ tenant_id: str,
124
+ graph_id: str,
125
+ sink: Optional[LedgerSink] = None,
126
+ ) -> None:
127
+ self._graph = compiled_graph
128
+ self._tenant_id = tenant_id
129
+ self._graph_id = graph_id
130
+ self._sink = sink or SqliteLedgerSink()
131
+ self.last_ledger: Optional[RouteLedger] = None
132
+
133
+ if getattr(compiled_graph, "_native", False):
134
+ compiled_graph.attach_ledger(
135
+ tenant_id=tenant_id,
136
+ graph_id=graph_id,
137
+ sink=self._sink,
138
+ )
139
+
140
+ @property
141
+ def graph(self) -> Any:
142
+ return self._graph
143
+
144
+ def invoke(self, input: Dict[str, Any], /, **kwargs: Any) -> Dict[str, Any]:
145
+ if getattr(self._graph, "_native", False):
146
+ result = self._graph.invoke(input, **kwargs)
147
+ self.last_ledger = self._graph.last_ledger
148
+ if self.last_ledger and result.get("agent_trace"):
149
+ ts = datetime.now(timezone.utc)
150
+ for node_name in ("react_agent", "plan_solve", "validator"):
151
+ if any(s.node == node_name for s in self.last_ledger.steps):
152
+ _append_agent_trace_steps(self.last_ledger, node_name, result, timestamp=ts)
153
+ break
154
+ return result
155
+ return self._run_legacy(input, kwargs, async_mode=False)
156
+
157
+ async def ainvoke(self, input: Dict[str, Any], /, **kwargs: Any) -> Dict[str, Any]:
158
+ if getattr(self._graph, "_native", False):
159
+ result = await self._graph.ainvoke(input, **kwargs)
160
+ self.last_ledger = self._graph.last_ledger
161
+ return result
162
+ return await self._run_legacy_async(input, kwargs)
163
+
164
+ def _run_legacy(self, input: Dict[str, Any], kwargs: Dict[str, Any], *, async_mode: bool) -> Dict[str, Any]:
165
+ run_id = str(uuid4())
166
+ turn_id = input.get("turn_id") or input.get("request_id")
167
+ ledger = RouteLedger(
168
+ run_id=run_id,
169
+ turn_id=str(turn_id) if turn_id else None,
170
+ tenant_id=self._tenant_id,
171
+ graph_id=self._graph_id,
172
+ )
173
+
174
+ state = dict(input)
175
+ final_state: Dict[str, Any] = state
176
+ pending: Dict[str, float] = {}
177
+
178
+ stream_kwargs = dict(kwargs)
179
+ stream_kwargs["stream_mode"] = "debug"
180
+
181
+ for event in self._graph.stream(state, **stream_kwargs):
182
+ if not isinstance(event, dict):
183
+ continue
184
+ event_type = event.get("type")
185
+ payload = event.get("payload") or {}
186
+
187
+ if event_type == "task":
188
+ node = payload.get("name")
189
+ if not node:
190
+ continue
191
+ pending[node] = time.perf_counter()
192
+ edge_taken = _parse_edge_from_triggers(payload.get("triggers"))
193
+ if edge_taken and ledger.steps:
194
+ ledger.steps[-1].edge_taken = edge_taken
195
+
196
+ elif event_type == "task_result":
197
+ node = payload.get("name")
198
+ if not node:
199
+ continue
200
+ started = pending.pop(node, time.perf_counter())
201
+ duration_ms = int((time.perf_counter() - started) * 1000)
202
+ result = payload.get("result") or {}
203
+ if isinstance(result, dict):
204
+ state = _merge_state(state, result)
205
+ final_state = state
206
+ ts_raw = event.get("timestamp")
207
+ timestamp = (
208
+ datetime.fromisoformat(ts_raw)
209
+ if isinstance(ts_raw, str)
210
+ else datetime.now(timezone.utc)
211
+ )
212
+ ledger.add_step(
213
+ LedgerStep(
214
+ node=node,
215
+ edge_taken=None,
216
+ rule_chain=_extract_rule_chain(result, node, merged_state=state),
217
+ duration_ms=duration_ms,
218
+ timestamp=timestamp,
219
+ grounding_score=_extract_grounding_score(result, node, merged_state=state),
220
+ )
221
+ )
222
+ if isinstance(result, dict) and result.get("agent_trace"):
223
+ _append_agent_trace_steps(ledger, node, result, timestamp=timestamp)
224
+
225
+ self._sink.write(ledger)
226
+ self.last_ledger = ledger
227
+ return final_state
228
+
229
+ async def _run_legacy_async(self, input: Dict[str, Any], kwargs: Dict[str, Any]) -> Dict[str, Any]:
230
+ return self._run_legacy(input, kwargs, async_mode=True)
231
+
232
+
233
+ def wrap(
234
+ compiled_graph: Any,
235
+ *,
236
+ tenant_id: str,
237
+ graph_id: str,
238
+ sink: Optional[LedgerSink] = None,
239
+ ) -> RunnableWithLedger:
240
+ """Attach Route Ledger tracking to a compiled graph."""
241
+ return RunnableWithLedger(
242
+ compiled_graph,
243
+ tenant_id=tenant_id,
244
+ graph_id=graph_id,
245
+ sink=sink,
246
+ )
247
+
248
+
249
+ __all__ = ["RunnableWithLedger", "wrap"]
@@ -0,0 +1,43 @@
1
+ """Agent execution patterns — unified Agent + strategies."""
2
+
3
+ from chorusgraph.agents.agent import Agent
4
+ from chorusgraph.agents.agent_node import AgentNode, agent_result_to_state, promote_to_agent
5
+ from chorusgraph.agents.loop import AgentTraceStep, LoopOutcome, TraceKind, run_agent_loop
6
+ from chorusgraph.agents.plan_solve import PlanSolveResult, run_plan_solve
7
+ from chorusgraph.agents.plan_utils import PlanStep, plan_tasks
8
+ from chorusgraph.agents.policy import (
9
+ BeliefPolicy,
10
+ BeliefPolicyNotCalibratedError,
11
+ PlanPolicy,
12
+ PlanSolveOpts,
13
+ ReActOpts,
14
+ ReflectionOpts,
15
+ )
16
+ from chorusgraph.agents.react import ReActResult, run_react
17
+ from chorusgraph.agents.reflection import ReflectionResult, ValidationVerdict, run_reflection
18
+
19
+ __all__ = [
20
+ "Agent",
21
+ "AgentNode",
22
+ "AgentTraceStep",
23
+ "BeliefPolicy",
24
+ "BeliefPolicyNotCalibratedError",
25
+ "LoopOutcome",
26
+ "PlanPolicy",
27
+ "PlanSolveOpts",
28
+ "PlanSolveResult",
29
+ "PlanStep",
30
+ "ReActOpts",
31
+ "ReActResult",
32
+ "ReflectionOpts",
33
+ "ReflectionResult",
34
+ "TraceKind",
35
+ "ValidationVerdict",
36
+ "agent_result_to_state",
37
+ "plan_tasks",
38
+ "promote_to_agent",
39
+ "run_agent_loop",
40
+ "run_plan_solve",
41
+ "run_react",
42
+ "run_reflection",
43
+ ]
@@ -0,0 +1,69 @@
1
+ """Unified configurable Agent type (DESIGN §7.8)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Callable, Dict, Optional
7
+
8
+ from chorusgraph.agents.policy import (
9
+ BeliefPolicy,
10
+ PATTERN_DEFAULTS,
11
+ PatternOpts,
12
+ PlanPolicy,
13
+ PlanSolveOpts,
14
+ ReActOpts,
15
+ ReflectionOpts,
16
+ )
17
+ from chorusgraph.agents.strategies import get_strategy
18
+ from chorusgraph.agents.strategies.base import AgentContext, AgentRunResult
19
+ from chorusgraph.nodes.roles import RoleTemplate
20
+ from chorusgraph.nodes.tool import ToolRegistry
21
+
22
+
23
+ @dataclass
24
+ class Agent:
25
+ """
26
+ One agent type — pattern selected via pluggable strategy config.
27
+
28
+ >>> agent = Agent(pattern="react", tools=registry, model=gemini.generate_json)
29
+ >>> agent.run("Compare USD/EUR and USD/GBP")
30
+ """
31
+
32
+ pattern: str
33
+ tools: ToolRegistry
34
+ model: Callable[[str, str], str]
35
+ role: Optional[RoleTemplate] = None
36
+ policy: PlanPolicy = field(default_factory=PlanPolicy)
37
+ pattern_opts: Optional[PatternOpts] = None
38
+ belief: BeliefPolicy = field(default_factory=BeliefPolicy)
39
+ llm_text: Optional[Callable[[str, str], str]] = None
40
+
41
+ def __post_init__(self) -> None:
42
+ if self.pattern_opts is None:
43
+ self.pattern_opts = PATTERN_DEFAULTS[self.pattern] # type: ignore[assignment]
44
+
45
+ def run(
46
+ self,
47
+ question: str,
48
+ /,
49
+ *,
50
+ scratchpad_prefix: str = "",
51
+ initial_draft: str = "",
52
+ validate: Optional[Callable[[str], Any]] = None,
53
+ revise: Optional[Callable[[str, Any], str]] = None,
54
+ ) -> AgentRunResult:
55
+ strategy = get_strategy(self.pattern)
56
+ ctx = AgentContext(
57
+ question=question,
58
+ registry=self.tools,
59
+ llm_json=self.model,
60
+ llm_text=self.llm_text,
61
+ policy=self.policy,
62
+ pattern_opts=self.pattern_opts or PATTERN_DEFAULTS[self.pattern], # type: ignore[arg-type]
63
+ belief=self.belief,
64
+ scratchpad_prefix=scratchpad_prefix,
65
+ initial_draft=initial_draft,
66
+ validate=validate,
67
+ revise=revise,
68
+ )
69
+ return strategy.run(ctx)
@@ -0,0 +1,82 @@
1
+ """AgentNode — Agent IS-A Node (DESIGN §7.7)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Callable, Dict, List, Optional
6
+
7
+ from chorusgraph.agents.agent import Agent
8
+ from chorusgraph.agents.strategies.base import AgentRunResult
9
+ from chorusgraph.nodes.roles import Node, RoleTemplate, promote
10
+
11
+
12
+ def _trace_dicts(result: AgentRunResult) -> List[Dict[str, Any]]:
13
+ return [s.to_dict() for s in result.trace]
14
+
15
+
16
+ def _rule_chain_from_result(result: AgentRunResult) -> List[str]:
17
+ return [s.to_rule() for s in result.trace]
18
+
19
+
20
+ def agent_result_to_state(result: AgentRunResult, *, pattern: str, prior: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
21
+ prior = prior or {}
22
+ tool_results = [obs for obs in result.observations if isinstance(obs, dict) and "rate" in obs]
23
+ if not tool_results and result.observations:
24
+ tool_results = [
25
+ (obs.get("data") if isinstance(obs, dict) else obs) for obs in result.observations
26
+ ]
27
+ update: Dict[str, Any] = {
28
+ "tool_calls": list(prior.get("tool_calls") or []) + result.tool_calls,
29
+ "tool_results": tool_results,
30
+ "tool_result": tool_results[-1] if tool_results else None,
31
+ "needs_tool": False,
32
+ "agent_trace": _trace_dicts(result),
33
+ "rule_chain": _rule_chain_from_result(result),
34
+ }
35
+ if pattern == "react":
36
+ update["research_plan"] = "ReAct via Agent"
37
+ if pattern == "plan_solve":
38
+ update["plan_steps"] = [
39
+ {"id": s.id, "description": s.description, "tool": s.tool} for s in result.plan
40
+ ]
41
+ if pattern == "reflection":
42
+ update["validation"] = result.validation
43
+ update["response"] = result.draft
44
+ update["draft_response"] = result.draft
45
+ update["reflection_pass"] = result.passes
46
+ return update
47
+
48
+
49
+ def AgentNode(
50
+ agent: Agent,
51
+ node_id: str = "agent",
52
+ *,
53
+ state_mapper: Optional[Callable[[Dict[str, Any], AgentRunResult], Dict[str, Any]]] = None,
54
+ ) -> Node:
55
+ """Wrap an Agent as a LangGraph-compatible Node."""
56
+
57
+ def handler(state: Dict[str, Any]) -> Dict[str, Any]:
58
+ if state.get("cache_hit") and state.get("tool_result") and agent.pattern == "react":
59
+ return {"agent_trace": [], "rule_chain": ["agent=skipped_cache_hit"]}
60
+ message = state.get("message") or ""
61
+ kwargs: Dict[str, Any] = {}
62
+ if agent.pattern == "reflection":
63
+ kwargs["initial_draft"] = state.get("draft_response") or ""
64
+ if state.get("_reflection_validate"):
65
+ kwargs["validate"] = state["_reflection_validate"]
66
+ if state.get("_reflection_revise"):
67
+ kwargs["revise"] = state["_reflection_revise"]
68
+ result = agent.run(message, **kwargs)
69
+ update = agent_result_to_state(result, pattern=agent.pattern, prior=state)
70
+ if state_mapper:
71
+ update = state_mapper(state, result) | update
72
+ return update
73
+
74
+ role = agent.role
75
+ node = Node(node_id=node_id, role=role, handler=handler)
76
+ return node
77
+
78
+
79
+ def promote_to_agent(node: Node, agent: Agent) -> Node:
80
+ """Promote a plain Node to an Agent-backed node."""
81
+ bound = AgentNode(agent, node_id=node.node_id)
82
+ return Node(node_id=node.node_id, role=agent.role or node.role, handler=bound.handler)
@@ -0,0 +1,137 @@
1
+ """Generic agent-loop substrate — reason ↔ act ↔ route with observable trace."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+ from typing import Any, Callable, Dict, List, Optional, Protocol
8
+
9
+ from chorusgraph.agents.policy import PlanPolicy
10
+
11
+
12
+ class TraceKind(str, Enum):
13
+ THOUGHT = "thought"
14
+ ACTION = "action"
15
+ OBSERVATION = "observation"
16
+ PLAN_STEP = "plan_step"
17
+ REVISION = "revision"
18
+ ROUTER = "router"
19
+
20
+
21
+ @dataclass
22
+ class AgentTraceStep:
23
+ kind: TraceKind
24
+ content: str
25
+ metadata: Dict[str, Any] = field(default_factory=dict)
26
+
27
+ def to_dict(self) -> Dict[str, Any]:
28
+ return {
29
+ "kind": self.kind.value,
30
+ "content": self.content,
31
+ "metadata": self.metadata,
32
+ }
33
+
34
+ def to_rule(self) -> str:
35
+ return f"{self.kind.value}={self.content[:240]}"
36
+
37
+
38
+ @dataclass
39
+ class LoopOutcome:
40
+ finished: bool
41
+ trace: List[AgentTraceStep] = field(default_factory=list)
42
+ observations: List[Any] = field(default_factory=list)
43
+ tokens_used: int = 0
44
+ steps_used: int = 0
45
+ finish_reason: Optional[str] = None
46
+
47
+
48
+ class Reasoner(Protocol):
49
+ def __call__(self, *, scratchpad: str, step: int) -> Dict[str, Any]: ...
50
+
51
+
52
+ class Actor(Protocol):
53
+ def __call__(self, action: Dict[str, Any]) -> Any: ...
54
+
55
+
56
+ class Router(Protocol):
57
+ def __call__(self, *, reason_result: Dict[str, Any], observations: List[Any]) -> str: ...
58
+
59
+
60
+ def run_agent_loop(
61
+ *,
62
+ policy: PlanPolicy,
63
+ reason: Reasoner,
64
+ act: Optional[Actor],
65
+ route: Router,
66
+ initial_scratchpad: str = "",
67
+ ) -> LoopOutcome:
68
+ """
69
+ Generic cyclic machinery: llm (reason) ↔ tool (act) ↔ router (act-or-finish).
70
+
71
+ Each cycle appends Thought / Action / Observation steps to the trace.
72
+ """
73
+ trace: List[AgentTraceStep] = []
74
+ observations: List[Any] = []
75
+ scratchpad = initial_scratchpad
76
+ tokens_used = 0
77
+ steps_used = 0
78
+ finished = False
79
+ finish_reason: Optional[str] = None
80
+
81
+ while steps_used < policy.max_steps and policy.within_budget(tokens_used):
82
+ reason_result = reason(scratchpad=scratchpad, step=steps_used)
83
+ tokens_used += int(reason_result.get("tokens_used") or 0)
84
+
85
+ thought = (reason_result.get("thought") or "").strip()
86
+ if thought:
87
+ trace.append(AgentTraceStep(TraceKind.THOUGHT, thought))
88
+
89
+ decision = route(reason_result=reason_result, observations=observations)
90
+ trace.append(AgentTraceStep(TraceKind.ROUTER, decision, metadata={"step": steps_used}))
91
+
92
+ if decision == "finish":
93
+ finished = True
94
+ finish_reason = reason_result.get("finish_reason") or "router_finish"
95
+ steps_used += 1
96
+ break
97
+
98
+ if decision == "continue":
99
+ scratchpad += f"\nThought: {thought}\n(Policy: continuing — tool required before finish)\n"
100
+ steps_used += 1
101
+ continue
102
+
103
+ if decision == "act" and act is not None:
104
+ action = reason_result.get("action") or {}
105
+ tool_name = action.get("tool") or action.get("name") or "unknown"
106
+ trace.append(
107
+ AgentTraceStep(
108
+ TraceKind.ACTION,
109
+ f"{tool_name}({action.get('args') or action.get('arguments') or {}})",
110
+ metadata={"tool": tool_name, "args": action.get("args") or action.get("arguments") or {}},
111
+ )
112
+ )
113
+ observation = act(action)
114
+ observations.append(observation)
115
+ obs_text = observation if isinstance(observation, str) else str(observation)
116
+ trace.append(AgentTraceStep(TraceKind.OBSERVATION, obs_text[:2000], metadata={"tool": tool_name}))
117
+ scratchpad += f"\nThought: {thought}\nAction: {tool_name}\nObservation: {obs_text}\n"
118
+ else:
119
+ scratchpad += f"\nThought: {thought}\n"
120
+ if reason_result.get("error"):
121
+ trace.append(AgentTraceStep(TraceKind.OBSERVATION, str(reason_result["error"])))
122
+
123
+ steps_used += 1
124
+
125
+ if not finished and steps_used >= policy.max_steps:
126
+ finish_reason = "max_steps"
127
+ elif not finished and not policy.within_budget(tokens_used):
128
+ finish_reason = "token_budget"
129
+
130
+ return LoopOutcome(
131
+ finished=finished or bool(observations),
132
+ trace=trace,
133
+ observations=observations,
134
+ tokens_used=tokens_used,
135
+ steps_used=steps_used,
136
+ finish_reason=finish_reason,
137
+ )
@@ -0,0 +1,50 @@
1
+ """Plan-Solve Agent shim."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Callable, Dict, List, Optional
7
+
8
+ from chorusgraph.agents.plan_utils import PlanStep, plan_tasks, _try_compute_cross
9
+ from chorusgraph.agents.policy import PlanPolicy, PlanSolveOpts
10
+ from chorusgraph.nodes.tool import ToolRegistry
11
+
12
+ __all__ = ["PlanStep", "PlanSolveResult", "plan_tasks", "run_plan_solve", "_try_compute_cross"]
13
+
14
+
15
+ @dataclass
16
+ class PlanSolveResult:
17
+ plan: List[PlanStep] = field(default_factory=list)
18
+ trace: List[Any] = field(default_factory=list)
19
+ observations: List[Any] = field(default_factory=list)
20
+ tool_calls: List[Dict[str, Any]] = field(default_factory=list)
21
+ failed_step: Optional[int] = None
22
+
23
+
24
+ def run_plan_solve(
25
+ *,
26
+ question: str,
27
+ registry: ToolRegistry,
28
+ llm_json: Callable[[str, str], str],
29
+ policy: Optional[PlanPolicy] = None,
30
+ pattern_opts: Optional[PlanSolveOpts] = None,
31
+ ) -> PlanSolveResult:
32
+ from chorusgraph.agents.agent import Agent
33
+
34
+ policy = policy or PlanPolicy(max_steps=10)
35
+ opts = pattern_opts or PlanSolveOpts(max_plan_steps=policy.max_steps, on_step_failure="abort")
36
+ agent = Agent(
37
+ pattern="plan_solve",
38
+ tools=registry,
39
+ model=llm_json,
40
+ policy=policy,
41
+ pattern_opts=opts,
42
+ )
43
+ result = agent.run(question)
44
+ return PlanSolveResult(
45
+ plan=result.plan,
46
+ trace=result.trace,
47
+ observations=result.observations,
48
+ tool_calls=result.tool_calls,
49
+ failed_step=result.failed_step,
50
+ )