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
106 changes: 96 additions & 10 deletions docs/design-docs/autonomy.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,48 @@ The cortex assembles the autonomy channel's context before each wake. It gets:
- **Task state** — all active tasks: ready, in-progress, backlog, pending_approval. Full detail on each, including all comments.
- **Goals** — all active goals with descriptions and notes. Background context and direction, not a work queue. See [`goals.md`](goals.md).
- **Active workers** — what's currently running so it doesn't duplicate work.
- **Last few run summaries** — the `autonomy_complete` output from its previous runs, with timestamps. This is the primary continuity mechanism.
- **Its own prior transcript** — the channel's persisted history, where each finished run has compacted to its `autonomy_complete` summary. This is the continuity mechanism; see [Continuity Between Runs](#continuity-between-runs).

The last run summaries are surfaced up front: "Last run (2h ago): enriched tasks X and Y, created tasks Z for backlog." The autonomy channel wakes with spatial awareness of where things stand and what it did recently.
Continuity arrives as the channel's own history rather than as injected run summaries — the run store stays the queryable index and provenance record, not a second delivery path for the same content. The autonomy channel wakes with spatial awareness of where things stand and what it did recently.

### The briefing is the system prompt, not a message

All of the above renders into the channel's system prompt, re-rendered on each wake. It is not delivered as an inbound message.

This matters because the autonomy channel is one conversation that spans every run — a single conversation id, one persistent transcript. Anything delivered as a message is written into that transcript and stays there. A briefing sent as a message means run fifty wakes to forty-nine copies of its own instructions, with its actual work crowded out between them. Rendering the same content as the system prompt costs nothing extra, since the cortex already assembles it fresh each wake, and it leaves no residue. It is also the honest representation: a briefing stored with a user role and a system sender describes a participant who does not exist.

Two things legitimately arrive mid-run and so must be messages: the soft wrap-up warning at `warn_secs` and the hard timeout notice at `timeout_secs`. Both are ephemeral — present in the live run's context, never persisted to the transcript. They are scaffolding for one run, not history.

The rule the channel is built around: **the transcript holds the agent's own output.** Everything the system tells it is either the system prompt, re-rendered per wake, or an ephemeral mid-run injection. Nothing the system says accumulates.

### Journaled events

That rule governs scaffolding. It does not exclude the agent's own actions — and some of those leave the transcript entirely. When a run sends a message to the home channel, the effect lands on a human, not in the conversation the run is having with itself. The next wake has no way to know it happened.

So actions with effects outside the transcript are journaled into it as they occur:

```text
Sent to home channel (telegram:8659410676):
"Found three open issues on the repo you cloned — the oldest looks like a quick win."
```

**These are recorded as the agent's own turn.** Not as a system row: `log_system_message` persists `role = "system"`, and rehydration keeps only `user` and `assistant` rows (`render_conversation_history_backfill`). A system row is visible in the dashboard and invisible to the agent, which is precisely backwards for a journal entry — the dashboard is not who needs to read it.

What qualifies is decided by one test: **journal only what the next wake cannot re-derive.** Task state, goals, worker status, and run counts are all re-rendered into the system prompt on every wake, so journaling them duplicates content that is already arriving fresh. An outbound message fails that test on both counts — nothing regenerates it, and it cannot be undone. The agent has to know it already spoke.

Held to that test, the set stays small: things a human perceived, and writes to the world outside the agent. Internal state transitions stay out. The failure mode to avoid is a transcript that degrades from a train of thought into a syslog, which is the same pollution the briefing rule exists to prevent, arriving through a different door.

**Journaling is independent of waking.** Two axes that happen to share a vocabulary:

| | Wakes the agent | Appears in the transcript |
|---|---|---|
| Registered as a wake trigger | yes | yes, via the run it causes |
| Journal-only | no | yes, on the next wake |
| Neither | no | no |

An event is journaled because the agent needs to remember it, and it triggers a wake because it needs acting on *now*. Most things are one or the other. This is what several declared-but-unproduced `SystemEvent` variants are reaching for: `cortex.observation` wants to be journal-only.

**Journal entries must survive compaction.** A run's detail collapses into its summary on exit, and "have I already told them this?" is a question spanning days — exactly the range compaction removes. An outbound-message record that lives only in run detail works for one wake and then silently stops. Outward-facing actions are promoted into the run summary rather than discarded with the rest of the detail.

---

Expand All @@ -60,15 +99,54 @@ During a run it can:
- **Execute ready tasks** — tasks the user has approved. Uses execution tools directly (shell, file, browser) with no forced delegation. Workers available for genuine parallelism.
- **Create new tasks** — identifies follow-on work and adds it to `pending_approval`. The agent proposes; the user decides.
- **Update task metadata** — priority, blockers, progress notes.
- **Record what it notices** — the channel holds `memory_save` and `memory_recall` directly (it does not branch, so there is no persistence branch behind it). Findings are written as they are found, not batched at the end, because a run can time out and lose them.

Recording is licensed, not quota'd. A run that genuinely learned nothing records nothing, and the briefing says so in as many words. An agent told to always produce an observation will produce one — restating what it was already given, or narrating its own activity as a discovery — and manufactured memories are worse than none, because they degrade every future recall that has to sift past them.

What it **cannot** do:
- Reply to users (no `reply` tool)
- Hold a conversation. There is no `reply` tool: the channel has no inbound turn to answer, and a user who replies to something it sent is answered by the normal user channel for that conversation. Delivery to a configured target is not conversation — see **Reaching Out** below.
- Execute tasks that are still in `pending_approval`
- Create cron jobs
- Spawn other autonomy channels

---

## The Empty Instance

A fresh instance has no tasks, no goals, and no history. The default outcome is a run that surveys nothing, concludes "nothing new here", and exits — and because nothing changed, the next wake reaches the same conclusion. An agent that idles until someone gives it work is not autonomous; it is a queue consumer with a timer.

The survey already knows when it came back empty, so the briefing branches on it rather than leaving the agent to notice. The template is already conditional on wake events, run history, goals, workers, and level; empty state is one more branch, and it fires deterministically. That matters more than it sounds: routing this through a skill the agent chooses to invoke reintroduces the exact failure being fixed, because the run that fails to reach for the skill is indistinguishable from the run that had nothing to do.

The empty branch is built on one claim: **on an empty instance, learning the user and the system is the highest-value work available, not filler while waiting for real work.**

- **Read what is actually here.** The workspace, registered projects, whatever the user has already done. A cloned repository is a statement of intent.
- **Record what it learns**, under the rules above.
- **Find capability gaps** via `spacebot_docs` — features that fit what the user appears to be doing and that they have not set up.
- **Ask one good question.** If there is a single thing the user could say that would unlock the most, ask that.

The last one is the point. A question that gets answered converts an empty instance into a non-empty one and compounds into every later run. Ten manufactured observations compound into nothing. When the empty branch is deciding what is worth doing, one good question outranks a full survey of an empty system.

`spacebot_docs` is currently registered only on the branch and cortex tool servers (`create_branch_tool_server`, `create_cortex_tool_server`), not in `add_direct_mode_tools` — which is what the autonomy channel receives, and it does not branch. It has to be added there before any of this is reachable.

---

## Reaching Out

At `suggest` and above, a run may send to the home channel ([`home-channel.md`](home-channel.md)). This is the one place autonomous work becomes visible to a human without them going looking, so the bar is deliberately high — an agent that reports in every interval gets muted, and a muted agent is worth less than a silent one.

Send when:

- It needs something only the user can provide — a decision, access, a credential, missing context that blocks otherwise-ready work.
- It found something time-sensitive, where waiting until the user next opens a channel has a cost.

Do not send to report activity. "Here is what I did this run" is what run history is for, and it is visible on demand rather than pushed.

Every send is journaled into the transcript as the agent's own turn, so the next run can see that it already raised something and decide against repeating it. That judgment is the primary control; the content-key backstop exists for loops, not for taste. An unanswered question asked twice in a week is a worse outcome than one asked once and left standing.

With the dial at `observe`, or with no home channel configured, this section does not apply — findings are recorded and nothing is sent.

---

## Task Comments

Comments are the primary output of the enrichment loop. When the autonomy channel or a worker completes investigation on a task, findings are written as a comment — not appended to the task description, not stuffed into metadata. Comments are append-only and chronological. The task description remains the stable statement of what needs to be done; comments are everything that has been learned or decided since.
Expand Down Expand Up @@ -115,7 +193,7 @@ wake → survey pending_approval tasks
→ reason about worker findings
→ add_task_comment: synthesised finding + worker_id(s)
→ repeat for next task within turn budget
set_outcome → exit
autonomy_complete → exit
```

The autonomy channel system prompt instructs: investigate and comment freely; never execute a task still in `pending_approval`.
Expand Down Expand Up @@ -179,7 +257,7 @@ The cortex monitors elapsed time. At `warn_secs`, it injects an addendum into th

```
You have approximately 2 minutes remaining in this run.
Finish your current task, add any final comments, and call set_outcome.
Finish your current task, add any final comments, and call autonomy_complete.
Do not start a new task.
```

Expand All @@ -193,9 +271,15 @@ Delivery mechanism: a synthetic system message between turns, the same pattern t

**Task comments** — the primary record of what has been investigated and found. Persist indefinitely. The next run sees all prior comments when it reads task state on wake, so it does not duplicate completed investigation.

**Run summaries** — on exit, `autonomy_complete` records what was enriched, what was executed, what was created, and which wake events the run consumed. The next wake receives the last `run_history_count` summaries as part of its context, and the UI renders the consumed wakes as "woken by" provenance per run.
**Run summaries** — on exit, `autonomy_complete` records what was enriched, what was executed, what was created, and which wake events the run consumed. The summary is persisted as the run's assistant turn in the channel transcript, and the UI renders the consumed wakes as "woken by" provenance per run.

**The summary is the compaction unit.** During a run the channel carries full detail: tool calls, worker results, intermediate reasoning. When the run ends, that detail collapses to the summary. What persists is a stream of summaries — roughly five lines per run — plus the live detail of whichever run is currently executing. The transcript is therefore a continuous record of the agent's own thinking that stays bounded no matter how many times it wakes.

This makes the transcript itself the continuity mechanism, so `run_history_count` is a compaction window rather than a second delivery path. Persisting the transcript *and* injecting the last N summaries from the runs table would feed the same content twice by two mechanisms; the run store remains the queryable index and the provenance record, not a parallel context source.

One consequence worth stating: wake provenance lives in the run store and the UI, not in the transcript. Read on its own, the transcript is uninterrupted thought with no visible cause — "why did run 47 happen" is answered by run history, not by scrolling back.

Working memory provides broader system context. Run summaries provide the autonomy-specific thread.
Working memory provides broader system context. The transcript provides the autonomy-specific thread.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

---

Expand All @@ -208,9 +292,10 @@ Cortex tick
→ no autonomy channel currently running
→ autonomy.enabled = true
Cortex assembles context (identity + bulletin + working memory + tasks + goals + run summaries)
Cortex assembles context (identity + bulletin + working memory + tasks + goals)
→ rendered into the channel's system prompt, not sent as a message
Autonomy channel wakes with full context
Autonomy channel wakes with full context + its own prior transcript
├─ pending_approval tasks exist?
│ → enrich: spawn investigation workers, reason about findings, add_task_comment
Expand All @@ -222,7 +307,8 @@ Autonomy channel wakes with full context
└─ no tasks worth acting on?
→ create pending_approval tasks from goals, or exit with "nothing to do"
Calls set_outcome → summary recorded
Calls autonomy_complete → summary recorded in the run store
→ and persisted as the run's assistant turn; the run's detail compacts to it
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Channel exits → cortex records last_run_at, cleans up
```
Expand Down
Loading
Loading