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.
- cave_teams-0.1.0/LICENSE +21 -0
- cave_teams-0.1.0/PKG-INFO +85 -0
- cave_teams-0.1.0/README.md +60 -0
- cave_teams-0.1.0/cave_teams/__init__.py +99 -0
- cave_teams-0.1.0/cave_teams/_chain_ontology_vendored.py +395 -0
- cave_teams-0.1.0/cave_teams/adaptor.py +120 -0
- cave_teams-0.1.0/cave_teams/algebra.py +126 -0
- cave_teams-0.1.0/cave_teams/blackboard.py +107 -0
- cave_teams-0.1.0/cave_teams/cave.py +349 -0
- cave_teams-0.1.0/cave_teams/chain_ontology.py +28 -0
- cave_teams-0.1.0/cave_teams/concurrent.py +68 -0
- cave_teams-0.1.0/cave_teams/conditions.py +68 -0
- cave_teams-0.1.0/cave_teams/context_engineering.py +58 -0
- cave_teams-0.1.0/cave_teams/conversation.py +138 -0
- cave_teams-0.1.0/cave_teams/dag.py +91 -0
- cave_teams-0.1.0/cave_teams/dovetail.py +128 -0
- cave_teams-0.1.0/cave_teams/dsl.py +62 -0
- cave_teams-0.1.0/cave_teams/events.py +123 -0
- cave_teams-0.1.0/cave_teams/evolve.py +73 -0
- cave_teams-0.1.0/cave_teams/frontend.py +265 -0
- cave_teams-0.1.0/cave_teams/gameworld.py +87 -0
- cave_teams-0.1.0/cave_teams/harness.py +371 -0
- cave_teams-0.1.0/cave_teams/heaven_minimax.py +145 -0
- cave_teams-0.1.0/cave_teams/jobworld.py +413 -0
- cave_teams-0.1.0/cave_teams/leader.py +173 -0
- cave_teams-0.1.0/cave_teams/links.py +57 -0
- cave_teams-0.1.0/cave_teams/metacog.py +74 -0
- cave_teams-0.1.0/cave_teams/npc.py +51 -0
- cave_teams-0.1.0/cave_teams/orchestrator.py +238 -0
- cave_teams-0.1.0/cave_teams/primitives.py +354 -0
- cave_teams-0.1.0/cave_teams/runtime.py +113 -0
- cave_teams-0.1.0/cave_teams/sdna_bridge.py +91 -0
- cave_teams-0.1.0/cave_teams/season.py +81 -0
- cave_teams-0.1.0/cave_teams/sim.py +67 -0
- cave_teams-0.1.0/cave_teams/topologies.py +173 -0
- cave_teams-0.1.0/cave_teams/workflow.py +128 -0
- cave_teams-0.1.0/cave_teams.egg-info/PKG-INFO +85 -0
- cave_teams-0.1.0/cave_teams.egg-info/SOURCES.txt +41 -0
- cave_teams-0.1.0/cave_teams.egg-info/dependency_links.txt +1 -0
- cave_teams-0.1.0/cave_teams.egg-info/requires.txt +3 -0
- cave_teams-0.1.0/cave_teams.egg-info/top_level.txt +1 -0
- cave_teams-0.1.0/pyproject.toml +43 -0
- cave_teams-0.1.0/setup.cfg +4 -0
cave_teams-0.1.0/LICENSE
ADDED
|
@@ -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)}"
|