Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,53 @@ All notable changes to AgentDescent are documented here. The format follows

### Added

- **`Investigator` — a read-only delegation whose findings persist into the record.**
The Genesis reference implementation has an `:read` investigator role
(`agents/investigator.ex`) that a manager hands a question and that may write only
`CONTEXT.md`. `--investigate` ports it: a manager marks a delegation `"investigate":
true`, the delegation runs `Investigator` (`examples/genesis/_investigator.py`) instead
of the recursion, and what it finds is folded into the node's `CONTEXT.md` under
`## Findings` — so it enters the version and the next agent inherits it.

It exists because of a blind spot in the port, not to complete a roster of roles:
`_MANAGER_PROMPT` carries the `CONTEXT.md` chain and a *listing* of the files at a
node, never their contents, since this port's agents have no read tool. A manager
deciding from the code is deciding blind, and the investigator is the one actor
allowed to read a node and leave a note about it.

Deliberately narrow. The role's fan-out to `subagent_investigator` is **not** ported,
because fanning out read-only sub-agents is what a manager with read tools of its own
is for, and that is the port's session work. The one write it may make is the record
at the node it was sent to; source edits and writes to an ancestor's record are
refused and counted.

14 tests in `tests/test_genesis_investigator.py`, offline and deterministic.

- **`SkillExtractor` — a skill is distilled from an accepted contribution, and the run
inherits it.** `LocalWorld.skills()` already collected `.agents/skills/` along the node
chain — `hierarchical_skill_names/2`, and what the paper lists among what an
accepted version carries (§3.1) — the read half and nothing else.
`--extract-skills` adds the write half: `SkillExtractor` (`examples/genesis/_skills.py`)
distils the contribution a root just accepted into a skill file, checks the skills that
already exist so it does not duplicate one, and places it at the node where it is
relevant.

It runs at the **root episode boundary**, not after the merge. A post-merge version
is one no later episode of the run ever sees, so the loop would still be open; inside
the episode the skill rides in the same proposal and the next agent inherits it.

The flag also turns on `LocalWorld.skill_bodies`, which puts inherited skill *bodies*
in the brief. Without it the feature is decorative — the brief otherwise carries only
names, and fetching a body needs a `skill_read` tool this port's agents do not have, so
"read one before using it" was an instruction none of them could follow.

`skill_add`'s frontmatter is rendered here rather than emitted by the model — the
format is the tool's, not the model's. Skills get their own trust-region bucket
(`max_skill_edits=2` beside `max_edits=4`), because a lesson trimmed to make room for
the file it distils is self-defeating. `skills=None` is the old behaviour exactly.

22 tests in `tests/test_genesis_skills.py`, offline and deterministic.

- **`evolve(max_tokens=...)`: a budget in the unit that maps to the bill.**
`max_calls` and `max_rollouts` count invocations, and a reasoning model can
spend 40k tokens on hidden thinking in a single one -- so a 20-round run with
Expand Down
43 changes: 32 additions & 11 deletions docs/algo-genesis.md
Original file line number Diff line number Diff line change
Expand Up @@ -965,13 +965,23 @@ so would a reader of this page without this paragraph.
propose concurrently against one accepted version and `OctopusConflict` does the
three-way merge in the parent. The observable consequence is the same — two children
editing one file both survive — and the measurement of that is in the table above.
* **Skills are inherited but never extracted.** `LocalWorld.skills()` collects
`.agents/skills/` along the node chain and puts the *names* in the brief, which
is what `hierarchical_skill_names/2` does upstream and what the paper means by
listing "reusable skills" among what an accepted version carries (§3.1). What
is missing is the other end: upstream's `SkillExtractor` distils a completed
contribution into a new skill, and nothing here does. The domain ships one
human-written skill so the inheritance path is live rather than decorative.
* **Skills are inherited, and now extracted, with `--extract-skills`.** `LocalWorld.skills()`
collects `.agents/skills/` along the node chain, which is what
`hierarchical_skill_names/2` does upstream and what the paper means by listing
"reusable skills" among what an accepted version carries (§3.1). This port had only
that half. The other half is upstream's `SkillExtractor` (`agents/skill_extractor.ex`,
a `:read_write`, `delegation_level :low` role): it distils a completed contribution
into new skills, checks what already exists so it does not duplicate one, places each
at the Context Tree node where it is relevant, and is explicitly allowed to find
nothing. `SkillExtractor` in `examples/genesis/_skills.py` is that role, run at the
**root episode boundary** on the contribution the episode accepted -- upstream fires
it after the PR merges, but on this engine a post-merge version is one no later
episode of the same run sees, so the loop would still not close. Two things are
adapted rather than ported: the model returns the skill's node and body and this
side renders `skill_add`'s frontmatter, and the *bodies* travel in the brief once the
flag is on. The second one is the whole point -- `skill_read` is a tool, this port's
agents have none, and a name line saying "read one before using it" is an instruction
nobody can follow, which is why inheritance was inert before.
* **An episode here is one model call; upstream it is a session.** The paper is
explicit — "the agent can execute multiple model–tool turns during one supervised
episode" — and the runs are configured at **2 048 root turns and 128 turns per
Expand All @@ -984,10 +994,21 @@ so would a reader of this page without this paragraph.
rather than another turn. It is the largest single distance between this port and
the system it ports, and it is a property of the harness rather than of the
algorithm: `evolve()` proposes with one completion per rollout.
* **Only two roles are ported.** The released code has ten agent modules; the
paper's appendix §1.3 says the model has two — manager and leaf executor — and
that "codebase lead / investigator / task scheduler are implementation labels,
not additional roles". Porting ten would be porting an implementation.
* **Only two roles are ported, plus one capability the paper calls a label.** The
released code has ten agent modules; the paper's appendix §1.3 says the model has two
— manager and leaf executor — and that "codebase lead / investigator / task scheduler
are implementation labels, not additional roles". Porting ten would be porting an
implementation. The one exception is the **investigator**, and it is ported as a
capability rather than a role: `--investigate` lets a manager mark a delegation
read-only, and `Investigator` (`examples/genesis/_investigator.py`) reads that node's
files and records what it finds under `## Findings` in the node's `CONTEXT.md`. The
reason it earns its place is the port's own blind spot — `_MANAGER_PROMPT` carries the
`CONTEXT.md` chain and a listing of files but never their contents, because this
port's agents have no read tool, so the investigator is the one actor that can read a
node's code and leave a note about it for the next agent. It is deliberately **not**
the upstream role in full: there is no fan-out to `subagent_investigator`, because
fanning out read-only sub-agents is what a manager with read tools of its own is for,
and that is the port's session work.

## Run it

Expand Down
Loading
Loading