cave-teams 0.1.0__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 (43) hide show
  1. cave_teams-0.1.0/LICENSE +21 -0
  2. cave_teams-0.1.0/PKG-INFO +85 -0
  3. cave_teams-0.1.0/README.md +60 -0
  4. cave_teams-0.1.0/cave_teams/__init__.py +99 -0
  5. cave_teams-0.1.0/cave_teams/_chain_ontology_vendored.py +395 -0
  6. cave_teams-0.1.0/cave_teams/adaptor.py +120 -0
  7. cave_teams-0.1.0/cave_teams/algebra.py +126 -0
  8. cave_teams-0.1.0/cave_teams/blackboard.py +107 -0
  9. cave_teams-0.1.0/cave_teams/cave.py +349 -0
  10. cave_teams-0.1.0/cave_teams/chain_ontology.py +28 -0
  11. cave_teams-0.1.0/cave_teams/concurrent.py +68 -0
  12. cave_teams-0.1.0/cave_teams/conditions.py +68 -0
  13. cave_teams-0.1.0/cave_teams/context_engineering.py +58 -0
  14. cave_teams-0.1.0/cave_teams/conversation.py +138 -0
  15. cave_teams-0.1.0/cave_teams/dag.py +91 -0
  16. cave_teams-0.1.0/cave_teams/dovetail.py +128 -0
  17. cave_teams-0.1.0/cave_teams/dsl.py +62 -0
  18. cave_teams-0.1.0/cave_teams/events.py +123 -0
  19. cave_teams-0.1.0/cave_teams/evolve.py +73 -0
  20. cave_teams-0.1.0/cave_teams/frontend.py +265 -0
  21. cave_teams-0.1.0/cave_teams/gameworld.py +87 -0
  22. cave_teams-0.1.0/cave_teams/harness.py +371 -0
  23. cave_teams-0.1.0/cave_teams/heaven_minimax.py +145 -0
  24. cave_teams-0.1.0/cave_teams/jobworld.py +413 -0
  25. cave_teams-0.1.0/cave_teams/leader.py +173 -0
  26. cave_teams-0.1.0/cave_teams/links.py +57 -0
  27. cave_teams-0.1.0/cave_teams/metacog.py +74 -0
  28. cave_teams-0.1.0/cave_teams/npc.py +51 -0
  29. cave_teams-0.1.0/cave_teams/orchestrator.py +238 -0
  30. cave_teams-0.1.0/cave_teams/primitives.py +354 -0
  31. cave_teams-0.1.0/cave_teams/runtime.py +113 -0
  32. cave_teams-0.1.0/cave_teams/sdna_bridge.py +91 -0
  33. cave_teams-0.1.0/cave_teams/season.py +81 -0
  34. cave_teams-0.1.0/cave_teams/sim.py +67 -0
  35. cave_teams-0.1.0/cave_teams/topologies.py +173 -0
  36. cave_teams-0.1.0/cave_teams/workflow.py +128 -0
  37. cave_teams-0.1.0/cave_teams.egg-info/PKG-INFO +85 -0
  38. cave_teams-0.1.0/cave_teams.egg-info/SOURCES.txt +41 -0
  39. cave_teams-0.1.0/cave_teams.egg-info/dependency_links.txt +1 -0
  40. cave_teams-0.1.0/cave_teams.egg-info/requires.txt +3 -0
  41. cave_teams-0.1.0/cave_teams.egg-info/top_level.txt +1 -0
  42. cave_teams-0.1.0/pyproject.toml +43 -0
  43. cave_teams-0.1.0/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sancovp
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: cave-teams
3
+ Version: 0.1.0
4
+ Summary: Connect AI agents like code — compose any agents into teams with a tiny algebra, all topologies, a programmable message state machine, and a proven-team library. CAVE = Coding Agent Virtualization Environment.
5
+ Author: sancovp
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/sancovp/cave-teams
8
+ Project-URL: Repository, https://github.com/sancovp/cave-teams
9
+ Keywords: ai-agents,agents,multi-agent,multi-agent-systems,agent-orchestration,agent-framework,ai-agent-framework,llm,llm-agents,llm-orchestration,agentic,agentic-ai,agentic-workflow,claude,claude-code,codex,minimax,orchestration,agent-teams,provider-agnostic,dsl,cave
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Classifier: Operating System :: OS Independent
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Provides-Extra: minimax
23
+ Requires-Dist: anthropic>=0.40; extra == "minimax"
24
+ Dynamic: license-file
25
+
26
+ # cave-teams
27
+
28
+ **Connect AI agents like code.** Wire your whole team of AI agents with one line instead of hundreds
29
+ of lines of glue — add one, swap one, or reuse a whole team just by changing a word.
30
+
31
+ cave-teams is the **programmable, provider-agnostic** version of Claude Code Teams. The agents
32
+ underneath can be anything (Claude Code, Codex, MiniMax, a model call, a shell command, a Python
33
+ function); you control the *flow* with code. **CAVE = Coding Agent Virtualization Environment.**
34
+
35
+ ```bash
36
+ pip install cave-teams # zero-dependency core
37
+ ```
38
+
39
+ ## The one idea
40
+
41
+ Everything is the same shape — a building block. An agent is one, a team is one, a whole world is
42
+ one. A composition of building blocks *is* a building block, so teams nest inside teams forever
43
+ (`agent = team = world`). That is why two operators wire anything:
44
+
45
+ ```python
46
+ import cave_teams
47
+ from cave_teams import AgentLink
48
+
49
+ research = AgentLink("research", "Find 3 key facts.")
50
+ writer = AgentLink("writer", "Turn the facts into a paragraph.")
51
+
52
+ team = research >> writer # >> run in order
53
+ flow = research >> (security | perf | tests) >> ship # | run at the same time
54
+
55
+ result = await flow.execute({"goal": "ship the feature"})
56
+ ```
57
+
58
+ ## What it does
59
+
60
+ - **Program any control flow** — agents fire on conditions you write (`when_flag`, `after`, any
61
+ predicate). The message state machine, not a markdown to-do list.
62
+ - **Any agents** — `AgentLink` (Claude Code / MiniMax), `HeavenMiniMaxLink` (a real coding agent
63
+ with bash + file-edit), or `lift()` any function / callable.
64
+ - **Every topology** — sequential, parallel, branch, loop-until-approved, join (DAG), typed
65
+ hand-off, shared-workspace arena, tournament, evolve, season — and they nest.
66
+ - **Provable wiring** — termination, gate-soundness, and distribution are mechanically tested.
67
+ - **A whole world of agents** — `GameWorld`, an economic crafter sim, agents that compete and evolve.
68
+ - **Build once, reuse forever** — save a proven team to your golden library and drop it into any
69
+ project as one building block.
70
+
71
+ ## Two layers
72
+
73
+ - **The native API** is how you program — the `>>` / `|` DSL and the pattern functions.
74
+ - **`cave()`** is the metacontrol function on top: it drives the whole API from a data spec, in any
75
+ sequence — serialize a team, run it from JSON, save/reuse a proven team, federate caves.
76
+
77
+ ## Claude Code plugin
78
+
79
+ This repo is also a Claude Code plugin (`plugin/`). It ships a skill per pattern — the language
80
+ (`cave-teams`), the metacontrol (`cave`), and one each for sequential / parallel / branch / gate /
81
+ conditions / dovetail / dag / blackboard / tournament / evolve / season / world / sim / metacog.
82
+ `.claude/skills`, `.codex/skills`, and `.agent/skills` symlink to the same `plugin/skills`, so any
83
+ coding agent that clones the repo picks them up.
84
+
85
+ MIT.
@@ -0,0 +1,60 @@
1
+ # cave-teams
2
+
3
+ **Connect AI agents like code.** Wire your whole team of AI agents with one line instead of hundreds
4
+ of lines of glue — add one, swap one, or reuse a whole team just by changing a word.
5
+
6
+ cave-teams is the **programmable, provider-agnostic** version of Claude Code Teams. The agents
7
+ underneath can be anything (Claude Code, Codex, MiniMax, a model call, a shell command, a Python
8
+ function); you control the *flow* with code. **CAVE = Coding Agent Virtualization Environment.**
9
+
10
+ ```bash
11
+ pip install cave-teams # zero-dependency core
12
+ ```
13
+
14
+ ## The one idea
15
+
16
+ Everything is the same shape — a building block. An agent is one, a team is one, a whole world is
17
+ one. A composition of building blocks *is* a building block, so teams nest inside teams forever
18
+ (`agent = team = world`). That is why two operators wire anything:
19
+
20
+ ```python
21
+ import cave_teams
22
+ from cave_teams import AgentLink
23
+
24
+ research = AgentLink("research", "Find 3 key facts.")
25
+ writer = AgentLink("writer", "Turn the facts into a paragraph.")
26
+
27
+ team = research >> writer # >> run in order
28
+ flow = research >> (security | perf | tests) >> ship # | run at the same time
29
+
30
+ result = await flow.execute({"goal": "ship the feature"})
31
+ ```
32
+
33
+ ## What it does
34
+
35
+ - **Program any control flow** — agents fire on conditions you write (`when_flag`, `after`, any
36
+ predicate). The message state machine, not a markdown to-do list.
37
+ - **Any agents** — `AgentLink` (Claude Code / MiniMax), `HeavenMiniMaxLink` (a real coding agent
38
+ with bash + file-edit), or `lift()` any function / callable.
39
+ - **Every topology** — sequential, parallel, branch, loop-until-approved, join (DAG), typed
40
+ hand-off, shared-workspace arena, tournament, evolve, season — and they nest.
41
+ - **Provable wiring** — termination, gate-soundness, and distribution are mechanically tested.
42
+ - **A whole world of agents** — `GameWorld`, an economic crafter sim, agents that compete and evolve.
43
+ - **Build once, reuse forever** — save a proven team to your golden library and drop it into any
44
+ project as one building block.
45
+
46
+ ## Two layers
47
+
48
+ - **The native API** is how you program — the `>>` / `|` DSL and the pattern functions.
49
+ - **`cave()`** is the metacontrol function on top: it drives the whole API from a data spec, in any
50
+ sequence — serialize a team, run it from JSON, save/reuse a proven team, federate caves.
51
+
52
+ ## Claude Code plugin
53
+
54
+ This repo is also a Claude Code plugin (`plugin/`). It ships a skill per pattern — the language
55
+ (`cave-teams`), the metacontrol (`cave`), and one each for sequential / parallel / branch / gate /
56
+ conditions / dovetail / dag / blackboard / tournament / evolve / season / world / sim / metacog.
57
+ `.claude/skills`, `.codex/skills`, and `.agent/skills` symlink to the same `plugin/skills`, so any
58
+ coding agent that clones the repo picks them up.
59
+
60
+ MIT.
@@ -0,0 +1,99 @@
1
+ """
2
+ CAVE Teams — Programmable agent teams with persistent conversations.
3
+
4
+ Core:
5
+ Conversation — Persistent agent conversation (remembers everything)
6
+ Harness — File watcher that delivers messages between agents
7
+ TeamLeader — Opus/MiniMax leader that orchestrates by reasoning
8
+
9
+ Primitives:
10
+ run_opus() — Single claude -p call with Opus 4.6 1M
11
+ continue_opus() — Continue a prior claude -p conversation
12
+ run_minimax() — Direct MiniMax call
13
+
14
+ Seam + frontend + adaptor (the 2026-06 build — events OUT, decoupled frontend):
15
+ EventBus, TeamEvent — the on_event seam every Harness now emits through
16
+ TeamGalleryServer — the live web gallery (/ws + /emit + page)
17
+ FrontendListener / HttpFrontendListener — push a team's events to the gallery
18
+ build_team / spawn_team — the CAVE adaptor: spin up a team on the fly, wired live
19
+
20
+ Legacy (stateless, being replaced):
21
+ Team, TeamAgent — Old stateless orchestrator
22
+ """
23
+
24
+ from .primitives import run_opus, continue_opus, run_minimax, generate_image, AgentResult, ImageResult
25
+ from .conversation import Conversation
26
+ from .events import EventBus, TeamEvent, FileListener, CallbackListener
27
+ from .harness import Harness, Condition
28
+ from .conditions import (
29
+ when_flag, when_not_flag, after, when, all_of, any_of, wrap_cave_automation,
30
+ )
31
+ # the REAL chain ontology (vendored from SDNA — pure stdlib) + the typed Dovetail data-plane +
32
+ # cave-teams' ConcurrentChain. cave-teams is chain-ontology NATIVE: every agent type is a Link.
33
+ from .chain_ontology import (
34
+ Link, LinkResult, LinkStatus, Chain, EvalChain, Compiler, ConfigLink, LinkConfig,
35
+ )
36
+ from .concurrent import ConcurrentChain
37
+ from .dovetail import DovetailModel, HermesConfigInput
38
+ from .links import AgentLink
39
+ # A MiniMax agent via the heaven framework (the onionmorph/Conductor path — self-auths, no env key).
40
+ # heaven_base/cave are imported lazily inside execute(), so this import is host-safe (zero heaven dep).
41
+ from .heaven_minimax import HeavenMiniMaxLink, build_minimax_config, minimax_coords
42
+ # The metacontrol function: ONE super-compiled top that can drive the whole native API from a
43
+ # data spec, in any sequence. NOT how you program (use the native API for that) — the universal
44
+ # driver over it, extensible via register() (canonical/goldenized ops). See cave.py.
45
+ from .cave import (
46
+ cave, golden, register, register_fn, registered_ops, registered_fns,
47
+ scan_caves, scan_library, _build as build_from_spec,
48
+ )
49
+ from . import topologies
50
+ from .topologies import (
51
+ pipeline, chain, fan_out, broadcast, synthesis_gate, map_reduce, tournament,
52
+ eval_chain, loop_refine, duo, round_robin, Router, router, chain_from_spec,
53
+ )
54
+ # a team AS an agent (homoiconic: agent = team = agent = Link) → runtime stacking + override-run
55
+ from .runtime import TeamRuntime, as_agent
56
+ # Phase 2: the SDNA/heaven agent zoo as leaf Links (as_link adapts any runnable; SDNAC is already a Link)
57
+ from .sdna_bridge import as_link, RunnableLink, SDNA_AVAILABLE
58
+ from .chain_ontology import CHAIN_ONTOLOGY_SOURCE
59
+ # Phase 3: the agent-composition ALGEBRA over Links (laws in LAWS.md, verified in test_algebra_laws.py)
60
+ from . import algebra
61
+ from .algebra import seq, par, choice, gate, dovetail, skip, team, lift, Skip, Subgraph
62
+ # Phase 4: literal algebra NOTATION (a >> b, a | b, >> dove(D) >>) — installs >>/| on Link
63
+ from . import dsl
64
+ from .dsl import dove
65
+ # Team topology: the general partial-order scheduler (blockedBy DAG) — seq/par are its limits
66
+ from .dag import dag, DagChain
67
+ # Team topology: the arena / stigmergy blackboard (the gameworld core) — N agents ↔ shared state ↔ deity
68
+ from .blackboard import blackboard, Blackboard
69
+ # The genetic operator: copy a winner's dir (its AIOS) + wipe session memory → next generation
70
+ from .evolve import evolve, evolve_dir, select_winners
71
+ # The Economic Crafter Sim: compete → user-buys → select → evolve over generations (compiler + lab)
72
+ from .sim import crafter_sim
73
+ # The bounded epoch: carry / reset / ratchet boundary (the WoS season — the standard climbs)
74
+ from .season import season, Season, carry_reset_ratchet
75
+ # A WHOLE gameworld as one composable object: a program, and a class (instantiable / from_spec / subclassable)
76
+ from .gameworld import GameWorld, world_as_agent
77
+ # Workflow-tool parity: pipeline (no-barrier streaming), parallel (capped), run_until, content-hash
78
+ # resume (Memo), schema-output (with_schema). `pipeline` stays module-qualified (cave_teams.workflow.pipeline)
79
+ # to avoid clashing with topologies.pipeline (the sequential Chain builder).
80
+ from . import workflow
81
+ from .workflow import parallel, run_until, Memo, content_key, with_schema, SchemaError
82
+ # NPCs: agents in the world callable by players via a skill (an NPC can be a whole GameWorld)
83
+ from .npc import npc_mutator
84
+ # Context surgery (inject / weave / dovetail) — the context-assembly plane (native + sdna re-export)
85
+ from . import context_engineering
86
+ from .context_engineering import compose_context, weave_text, inject_context, weave_context, CONTEXT_ENGINEERING_SOURCE
87
+ # Metacog shell (a SEPARATE meta-pattern): executor→observer→meta[STATIC]→skill_editor, compounds
88
+ from .metacog import metacog_shell, MetacogShell
89
+ from .leader import TeamLeader
90
+ from .orchestrator import Team, TeamAgent, AgentBackend, Task
91
+ from .jobworld import JobworldTeam
92
+ from .adaptor import build_team, spawn_team
93
+
94
+ # Frontend imports fastapi/uvicorn lazily — keep them optional so the core library
95
+ # (agents + seam + adaptor) imports cleanly even where the web deps aren't installed.
96
+ try:
97
+ from .frontend import TeamGalleryServer, FrontendListener, HttpFrontendListener
98
+ except Exception: # pragma: no cover
99
+ pass
@@ -0,0 +1,395 @@
1
+ # VENDORED VERBATIM from base/sdna/sdna/chain_ontology.py (pure stdlib — abc/dataclasses/enum/typing,
2
+ # zero heaven/LLM coupling). Copied (not reimplemented) into cave-teams so the library is chain-ontology
3
+ # NATIVE while staying standalone (no `import sdna`, which would trigger heaven-coupled sdna/__init__).
4
+ # This is THE ontology — keep it in sync if SDNA's changes. cave-teams adds ConcurrentChain in concurrent.py.
5
+ """Universal Chain Ontology — Link and Chain homoiconic primitives.
6
+
7
+ The entire SDNA hierarchy reduces to two concepts:
8
+
9
+ Link: atomic unit of execution (wraps a config)
10
+ Chain(Link): sequence of Links — which IS ALSO a Link
11
+
12
+ This gives homoiconic composition: a Chain can be a Link in another Chain,
13
+ so SDNAC, SDNAFlow, SDNAFlowchain, DUOChain all become specializations of
14
+ the same two primitives.
15
+
16
+ SDNAC = Link (ariadne + hermes config)
17
+ SDNAFlow = Chain (sequential links)
18
+ SDNAFlowchain = EvalChain (chain + OVP link in a loop)
19
+ DUOChain = DUOChain (A→P alternation chain)
20
+ CompiledAgent = Link | Chain (pipeline output)
21
+
22
+ Source: Heaven core/chains/base/link.py + base_chain.py
23
+ Extracted from llegos actor model — we keep only the composition,
24
+ not the message-passing substrate.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from abc import ABC, abstractmethod
30
+ from dataclasses import dataclass, field
31
+ from enum import Enum
32
+ from typing import Any, Dict, List, Optional, Union
33
+
34
+
35
+ # =============================================================================
36
+ # Result protocol
37
+ # =============================================================================
38
+
39
+ class LinkStatus(str, Enum):
40
+ SUCCESS = "success"
41
+ BLOCKED = "blocked"
42
+ ERROR = "error"
43
+ AWAITING_INPUT = "awaiting_input"
44
+
45
+
46
+ @dataclass
47
+ class LinkResult:
48
+ """Result of executing a Link."""
49
+ status: LinkStatus
50
+ context: Dict[str, Any] = field(default_factory=dict)
51
+ error: Optional[str] = None
52
+ resume_path: Optional[List[int]] = None
53
+
54
+
55
+ # =============================================================================
56
+ # Link — atomic unit
57
+ # =============================================================================
58
+
59
+ class Link(ABC):
60
+ """Atomic unit of execution in the universal chain ontology.
61
+
62
+ A Link wraps a configuration and knows how to execute itself.
63
+ Everything in SDNA is a Link — single units, flows, flowchains.
64
+
65
+ The homoiconic property: Chain extends Link, so a Chain
66
+ can appear as a Link inside another Chain. This is the
67
+ entire composition model.
68
+
69
+ Contract:
70
+ - name: str (attribute or property — both work)
71
+ - execute(context) → LinkResult (or compatible result type)
72
+
73
+ SDNA compatibility: SDNAC sets self.name in __init__ (attribute),
74
+ which satisfies this contract. Subclasses may also use @property.
75
+ """
76
+
77
+ # name can be set as attribute in __init__ or as @property
78
+ # We don't make it @abstractmethod to allow plain attributes
79
+ name: str
80
+
81
+ @abstractmethod
82
+ async def execute(self, context: Optional[Dict[str, Any]] = None, **kwargs):
83
+ """Execute this link with the given context.
84
+
85
+ Args:
86
+ context: Shared mutable context dictionary.
87
+ **kwargs: Subclass-specific params (e.g. on_message for SDNAC).
88
+
89
+ Returns:
90
+ Result with status and updated context.
91
+ Type varies by subclass (SDNAResult, SDNAFlowchainResult, etc.)
92
+ """
93
+ ...
94
+
95
+ def describe(self, depth: int = 0) -> str:
96
+ """Return an LLM-readable description of this link.
97
+
98
+ This is the metaprogramming surface. LLMs call this to inspect
99
+ the chain structure they just built. Without this, homoiconic
100
+ composition has no consumer.
101
+
102
+ EVENTUAL BEHAVIOR (not yet implemented):
103
+ 1. Send this object's AST to ontologization (Carton/YOUKNOW)
104
+ if not already represented in the ontology
105
+ 2. Query the ontology for what it knows about this Link
106
+ 3. Return the ontology CLI info — effectively starting a
107
+ non-interactive persistent session about this object
108
+
109
+ Goes as deep as the callgraph but doesn't show everything
110
+ all at once — progressive pagination. The LLM can drill
111
+ deeper by following references, not by receiving a wall.
112
+
113
+ So: str(link) becomes a live ontology query, not a static string.
114
+ The object describes itself through the knowledge graph.
115
+
116
+ Current: static format string, overridden per subclass.
117
+ Future: ontology-backed self-description with pagination.
118
+ """
119
+ indent = " " * depth
120
+ return f"{indent}Link \"{self.name}\""
121
+
122
+ def __str__(self) -> str:
123
+ """String representation = ontology CLI session about this object.
124
+
125
+ Eventually: calling str() on any Link starts an ontology CLI
126
+ non-interactive persistent session about that thing. The AST
127
+ gets sent to ontologization if not in ontology, the ontology
128
+ gets queried, and the CLI info comes back.
129
+
130
+ Progressive pagination: shows the top level, with references
131
+ the LLM can follow to go deeper. Never dumps the full tree.
132
+
133
+ For now: delegates to describe().
134
+ """
135
+ return self.describe()
136
+
137
+
138
+ # =============================================================================
139
+ # Chain(Link) — homoiconic sequence
140
+ # =============================================================================
141
+
142
+ class Chain(Link):
143
+ """Sequence of Links — which IS ALSO a Link.
144
+
145
+ This is the homoiconic composition primitive.
146
+ A Chain can be a Link in another Chain, giving recursive nesting:
147
+
148
+ Chain([Link, Chain([Link, Link]), Link])
149
+
150
+ Execution is sequential: each link gets the context from the previous.
151
+ Stops on first non-SUCCESS.
152
+ """
153
+
154
+ def __init__(self, chain_name: str, links: Optional[List[Link]] = None):
155
+ self._name = chain_name
156
+ self.links: List[Link] = links or []
157
+
158
+ @property
159
+ def name(self) -> str:
160
+ return self._name
161
+
162
+ def add(self, link: Link) -> "Chain":
163
+ """Add a link to the chain. Returns self for fluent API."""
164
+ self.links.append(link)
165
+ return self
166
+
167
+ async def execute(self, context: Optional[Dict[str, Any]] = None, **kwargs):
168
+ """Execute links sequentially. Stop on first failure."""
169
+ ctx = dict(context) if context else {}
170
+
171
+ for i, link in enumerate(self.links):
172
+ result = await link.execute(ctx)
173
+ ctx = result.context
174
+
175
+ if result.status != LinkStatus.SUCCESS:
176
+ result.resume_path = [i] + (result.resume_path or [])
177
+ return result
178
+
179
+ return LinkResult(status=LinkStatus.SUCCESS, context=ctx)
180
+
181
+ def __len__(self) -> int:
182
+ return len(self.links)
183
+
184
+ def __getitem__(self, index: int) -> Link:
185
+ return self.links[index]
186
+
187
+ def describe(self, depth: int = 0) -> str:
188
+ """Recursive tree description of the chain."""
189
+ indent = " " * depth
190
+ lines = [f"{indent}Chain \"{self.name}\" ({len(self.links)} links):"]
191
+ for i, link in enumerate(self.links):
192
+ connector = "└──" if i == len(self.links) - 1 else "├──"
193
+ child_desc = link.describe(depth + 1).lstrip()
194
+ lines.append(f"{indent} {connector} {child_desc}")
195
+ return "\n".join(lines)
196
+
197
+
198
+ # =============================================================================
199
+ # EvalChain(Chain) — chain + evaluator in a loop
200
+ # =============================================================================
201
+
202
+ class EvalChain(Chain):
203
+ """Chain with an evaluator Link in a loop.
204
+
205
+ Runs the inner chain, then runs the evaluator.
206
+ If evaluator approves → done. Otherwise loop (max_cycles).
207
+
208
+ This is SDNAFlowchain / OVP pattern.
209
+ """
210
+
211
+ def __init__(
212
+ self,
213
+ chain_name: str,
214
+ links: Optional[List[Link]] = None,
215
+ evaluator: Optional[Link] = None,
216
+ max_cycles: int = 3,
217
+ approval_key: str = "approved",
218
+ ):
219
+ super().__init__(chain_name, links)
220
+ self.evaluator = evaluator
221
+ self.max_cycles = max_cycles
222
+ self.approval_key = approval_key
223
+
224
+ async def execute(self, context: Optional[Dict[str, Any]] = None, **kwargs):
225
+ """Run chain → evaluate → loop."""
226
+ ctx = dict(context) if context else {}
227
+
228
+ for cycle in range(self.max_cycles):
229
+ ctx["cycle"] = cycle + 1
230
+
231
+ # Run inner chain
232
+ result = await super().execute(ctx)
233
+ ctx = result.context
234
+
235
+ if result.status != LinkStatus.SUCCESS:
236
+ return result
237
+
238
+ # If no evaluator, single pass
239
+ if not self.evaluator:
240
+ return result
241
+
242
+ # Evaluate
243
+ eval_result = await self.evaluator.execute(ctx)
244
+ ctx = eval_result.context
245
+
246
+ if eval_result.status != LinkStatus.SUCCESS:
247
+ return eval_result
248
+
249
+ if ctx.get(self.approval_key):
250
+ return LinkResult(status=LinkStatus.SUCCESS, context=ctx)
251
+
252
+ return LinkResult(
253
+ status=LinkStatus.BLOCKED,
254
+ context=ctx,
255
+ error=f"Max cycles ({self.max_cycles}) reached",
256
+ )
257
+
258
+ def describe(self, depth: int = 0) -> str:
259
+ indent = " " * depth
260
+ lines = [f"{indent}EvalChain \"{self.name}\" ({len(self.links)} links, max_cycles={self.max_cycles}):"]
261
+ for i, link in enumerate(self.links):
262
+ connector = "├──" if i < len(self.links) - 1 or self.evaluator else "└──"
263
+ child_desc = link.describe(depth + 1).lstrip()
264
+ lines.append(f"{indent} {connector} {child_desc}")
265
+ if self.evaluator:
266
+ eval_desc = self.evaluator.describe(depth + 1).lstrip()
267
+ lines.append(f"{indent} └── [evaluator] {eval_desc}")
268
+ return "\n".join(lines)
269
+
270
+
271
+ # =============================================================================
272
+ # Compiler(Chain) — chain that produces new Links/Chains
273
+ # =============================================================================
274
+
275
+ class Compiler(Chain):
276
+ """A Chain whose output is a Link or Chain.
277
+
278
+ This is the D:D→D type in the hierarchy:
279
+
280
+ Link — executes
281
+ Chain(Link) — executes Links in sequence
282
+ Compiler(Chain) — executes Links that PRODUCE new Links
283
+
284
+ A Compiler takes a specification (as context) and produces
285
+ executable structure (a Link/Chain) as output. The produced
286
+ structure is stored in context under the 'compiled' key.
287
+
288
+ This is what makes the system self-compiling: a Compiler
289
+ can compile another Compiler, and the output is always
290
+ something that can be composed and executed.
291
+ """
292
+
293
+ def __init__(
294
+ self,
295
+ chain_name: str,
296
+ links: Optional[List[Link]] = None,
297
+ output_key: str = "compiled",
298
+ ):
299
+ super().__init__(chain_name, links)
300
+ self.output_key = output_key
301
+
302
+ def get_compiled(self, context: Dict[str, Any]) -> Optional[Link]:
303
+ """Extract the compiled Link/Chain from context."""
304
+ return context.get(self.output_key)
305
+
306
+ def describe(self, depth: int = 0) -> str:
307
+ indent = " " * depth
308
+ lines = [f"{indent}Compiler \"{self.name}\" ({len(self.links)} links):"]
309
+ for i, link in enumerate(self.links):
310
+ connector = "└──" if i == len(self.links) - 1 else "├──"
311
+ child_desc = link.describe(depth + 1).lstrip()
312
+ lines.append(f"{indent} {connector} {child_desc}")
313
+ lines.append(f"{indent} → produces Link via '{self.output_key}'")
314
+ return "\n".join(lines)
315
+
316
+
317
+ # =============================================================================
318
+ # ConfigLink — concrete Link wrapping a config dict
319
+ # =============================================================================
320
+
321
+ @dataclass
322
+ class LinkConfig:
323
+ """Configuration for a Link — the serializable part.
324
+
325
+ This is what the compiler produces. At runtime, a LinkConfig
326
+ becomes a Link via a factory/bridge.
327
+
328
+ Maps to HermesConfig fields:
329
+ - name → identity
330
+ - goal → input prompt (what to do)
331
+ - system_prompt → behavioral frame
332
+ - model → which LLM
333
+ - provider → which API
334
+ - temperature → creativity
335
+ - max_turns → execution limit
336
+ - permission_mode → trust boundary
337
+ - allowed_tools → tool surface
338
+ - mcp_servers → MCP configs
339
+ - skills → injected skill context
340
+ """
341
+ name: str = ""
342
+ goal: str = ""
343
+ system_prompt: str = ""
344
+ model: str = ""
345
+ provider: str = ""
346
+ temperature: float = 0.7
347
+ max_turns: int = 10
348
+ permission_mode: str = "default"
349
+ allowed_tools: List[str] = field(default_factory=list)
350
+ mcp_servers: Dict[str, Any] = field(default_factory=dict)
351
+ skills: str = "" # injected context
352
+ passthrough: Dict[str, Any] = field(default_factory=dict)
353
+
354
+
355
+ class ConfigLink(Link):
356
+ """Concrete Link that wraps a LinkConfig.
357
+
358
+ This is the leaf node — the thing that actually holds the config
359
+ the compiler produced. At runtime, this gets converted to an
360
+ SDNAC with the appropriate AriadneChain and HermesConfig.
361
+ """
362
+
363
+ def __init__(self, config: LinkConfig):
364
+ self.config = config
365
+
366
+ @property
367
+ def name(self) -> str:
368
+ return self.config.name
369
+
370
+ async def execute(self, context: Optional[Dict[str, Any]] = None, **kwargs):
371
+ """Placeholder execute — real execution creates an SDNAC and runs it.
372
+
373
+ In test/dry-run mode, this just passes through.
374
+ In production, the config gets instantiated as an SDNAC.
375
+ """
376
+ ctx = dict(context) if context else {}
377
+ ctx["_link_config"] = self.config
378
+ ctx["_link_name"] = self.config.name
379
+ return LinkResult(status=LinkStatus.SUCCESS, context=ctx)
380
+
381
+ def describe(self, depth: int = 0) -> str:
382
+ indent = " " * depth
383
+ parts = [f'Link "{self.config.name}"']
384
+ if self.config.model:
385
+ parts.append(f"model={self.config.model}")
386
+ if self.config.temperature != 0.7:
387
+ parts.append(f"temp={self.config.temperature}")
388
+ if self.config.goal:
389
+ goal_preview = self.config.goal[:60]
390
+ if len(self.config.goal) > 60:
391
+ goal_preview += "..."
392
+ parts.append(f'goal="{goal_preview}"')
393
+ if self.config.allowed_tools:
394
+ parts.append(f"tools=[{', '.join(self.config.allowed_tools[:5])}]")
395
+ return f"{indent}{' | '.join(parts)}"