Skip to content
C.W.K.
Stream
Lesson 05 of 05 · published

Any Brain Closes the Loop

~16 min · tool-use, multi-agent, vendor-independence, facade

Level 0Loose Note
0 XP0/41 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

Sixteen Requests Looking for a Verb

Track 4's last lesson made every brain a possible worker. This one makes every brain a possible delegator, the Pippa who adds a todo, picks a worker, reads the report and closes the work, all through the control facade from lesson 3. It starts with the evening that showed the facade could not do that.

On 2026-10-17 Dad switched Master Chief, his own conversation with Pippa, to run on Ollama. His turns from the phone reached a vessel that carried no coop tool at all: on every non-Claude vessel, a turn sent without an explicit tool list got no family tools, while the Claude vessel carried them all. The model could see earlier coop calls in the conversation, so it went looking for another way in. It fell back to the command-line tool with --as dad, found no verb there for assigning, and kept digging through Coop's source for a way to do what it had been asked. Dad stopped it after sixteen model requests.

The missing schema was a cwkPippa bug, and it was fixed first: every vessel now carries every family tool that is not gated. But the evening exposed a deeper gap in Coop itself. Even with the tool in hand, a chief could add, run, reply and cancel, and could not close. The Claude chief had been closing finished work through a shell, as --as dad --force. A brain without that habit simply could not finish the loop.

The Whole Loop Through One Tool

Since todo #500 the facade carries four more actions: close, release, abandon and edit, eleven in all. Lesson 3 said a chat door has no takeover, no force-close and no permission bypass; that is still true, and the way these four were added is why:

  • Under the caller's own name. A close, release or abandon is recorded under the calling brain's own coop identity, through the same canonical disposition path the Coop chief uses (Chief._transition). It is never Dad's identity borrowed, and never --force.
  • Through the binding, not around it. If a worker's binding holds the item, the disposition ends that binding with the reason given and resolves its obligation, in the same transaction. It never takes over the worker's session.
  • Offers are withdrawn. A launch still looking for its chat, or an invitation nobody accepted, would keep offering work that is over, so both are withdrawn first (the rule Track 6's last lesson calls _withdraw_offers).
  • Evidence the same way the chief gives it. close needs result, one self-contained line on what was done and how it was verified, and takes an optional evidence line and commit. A todo with replies needs one reply_outcomes answer per reply (Track 5), and show now always carries close_needs, empty when there is nothing to answer. Mind its edge: show returns at most twenty replies, and close_needs lists the ids of exactly those, so a todo with more replies than that still owes answers to the rest, and the close refuses, naming every missing id.
  • Finished stays finished. Closing a todo that is already done is refused with "a new todo carries new work". release applies only to work that is actually taken.

One small choice deserves a note: evidence is one line of text, not an object. Every vessel converts a tool's schema into its own provider's format, and a free-form object is exactly the thing some converters drop. A string survives all seven.

Right in One Call

The second half of the change was for a reader who has never seen the tool before, which, for a model, is every reader. run now takes brain, any of the seven or an alias such as codex or gpt, and routes it to its receiver with Track 4's route; vendor still names a receiver directly. The project may be left out, and Coop finds it from the work's scope, or it may be named by folder or by path. And every refusal names the next call: the missing reply ids together with the exact shape an answer takes, the projects to choose from, the fields an action accepts. Lesson 3's tip, let the error message carry the fix, became the facade's design rule, because a model reading its own tool result will either make the next call right or try spellings until one sticks.

Saying Is Not Doing

Then came the live acceptance from Track 4's last lesson: five sisters, each chiefing a fresh Pippa conversation through the tool alone, four turns each (add a todo, assign it to the next sister, read the report, close). In round one, the Kimi chief and the Grok chief each reached turn four and wrote, in effect, "closing… closed", with no call at all. The todos stayed open. The session logs were unambiguous: one provider round, no function call, tools present in the request.

The cause was in how every vessel replays a conversation. cwkPippa stores each reply's text and, beside it, the tool calls that reply made. The replay sent the text and dropped the calls. So four turns in, the chief was looking at its own history and seeing "Assigned to GLM." with nothing behind it: a reply that only said so. It had learned, from itself, that saying is doing.

The fix was measured before it was trusted. The exact point of failure was rebuilt offline: the stored history, the same prompt, the same tools, executed dry. With plain replay, Kimi made a close call in 3 of 16 tries, and Grok stopped on "I'll close it" without a call in 7 of 27. A first repair that added a text record of each reply's calls to Dad's next message helped Kimi only partly and Grok not at all. The repair that worked (cwkPippa 2cea1c80) replays each reply's calls in the wire's own call shape, ahead of the reply's text, exactly as each vessel sends calls inside a live turn: function-call items for Codex and Grok, assistant tool calls with tool messages for Kimi and Ollama, tool-use and tool-result blocks for GLM, function calls and responses for Gemini, and a calls field in Claude's prior-turn record. Kimi then closed 9 times in 9; Grok called the tool 14 times in 14. All six non-Claude providers accepted the replayed calls. Back in the round-one conversations, Kimi closed its open todo, and Grok closed its own on the second try.

Two Smaller Stalls

An empty round. A Gemini chief's first Coop turn came back with no text, no call and zero output tokens, and the adapter took it for a finished, silent turn. A round that says and does nothing is now asked once more (nothing can repeat, since nothing happened), and a second empty answer is a visible warning instead of silence. Telling the empty rounds apart later showed that some were refusals triggered by retrieved memory, so a refused round is asked once more without it, with a warning.

A report taken for a reply. The Kimi chief read the worker's report as a reply to the todo and invented a message id for reply_outcomes. The tool's description now says it in so many words: reply_outcomes is needed only when show lists close_needs, a worker's report is not a reply, and an action has happened only when the tool answered it.

Round two passed for all five: every todo was closed by its own chief, through the tool, most actions in a single call.

A model learns what doing looks like from its own history. If your replay keeps the words and drops the calls, every earlier turn teaches that a sentence is an action. Replay the calls in the shape the model actually makes them, and give every brain the same verbs, so finishing the loop never depends on which brain happens to be holding it.

Code

From Coop's control.py: eleven actions, and a close that goes through the chief's own path (trimmed excerpt)·python
FIELDS = {
    'list': {'scope', 'status', 'q'}, 'show': {'kind', 'id'},
    'options': {'scope', 'project', 'host'}, 'add': {'scope', 'title', 'body', 'priority'},
    'run': {'kind', 'id', 'brain', 'vendor', 'project', 'candidate', 'changes', 'host', 'effort'},
    'reply': {'kind', 'id', 'body'}, 'cancel': {'assignment_id'},
    'close': {'kind', 'id', 'result', 'evidence', 'reply_outcomes', 'commit'},
    'release': {'kind', 'id', 'reason'}, 'abandon': {'kind', 'id', 'reason'},
    'edit': {'kind', 'id', 'title', 'body', 'append', 'priority'},
}

def dispose(self, key, action, kind, item_id, params, metadata):
    """Close, release or abandon one item as the calling brain, on Dad's word in this conversation."""
    actor = metadata['id']
    ...
    def run(c):
        row = self.engine.row(c, KINDS[kind], item_id)
        finished = ('done', 'abandoned') if kind == 'todo' else ('closed',)
        if row['status'] in finished:
            raise Conflict(f"{kind} #{item_id} is already {row['status']}; a new todo carries new work")
        ...  # a todo with replies: every reply answered, or the refusal names the missing ids and the shape
        binding = c.execute("SELECT * FROM engine_bindings WHERE kind=? AND item_id=? AND state='active'", (kind, item_id)).fetchone()
        self.engine._withdraw_offers(c, kind, item_id, f'{action} by {actor}: {reason}')
        Chief._transition(action, kind, item_id, reason, actor, evidence or None)
        if binding:
            self.engine._record_outcome(c, dict(binding), action, reason,
                                        {'source': 'control', 'actor': actor, 'evidence': evidence})
        ...
A minimal sketch you can run: one stored history, replayed without and with its calls·python
"""A minimal sketch (not cwkPippa's code): the same stored history replayed
two ways. A reply's text and the calls it made are stored side by side; only
one replay lets the model see that saying came with doing."""
import json

history = [
    {"role": "user", "text": "Assign #506 to GLM, then close it when the report is in."},
    {"role": "assistant", "text": "Assigned to GLM.",
     "calls": [{"id": "c1", "name": "coop", "input": {"action": "run", "kind": "todo", "id": 506, "brain": "glm"},
                "answer": {"assignment": "prepared"}}]},
    {"role": "user", "text": "The report is in. Close it."},
    {"role": "assistant", "text": "Closed #506.",
     "calls": [{"id": "c2", "name": "coop", "input": {"action": "close", "kind": "todo", "id": 506,
                                                     "result": "Investigated as asked; report verified."},
                "answer": {"status": "done"}}]},
]


def plain(history):
    return [{"role": m["role"], "content": m["text"]} for m in history]


def with_calls(history):
    """Calls first, in the wire's own shape, then the reply's text."""
    out = []
    for m in history:
        for call in m.get("calls", []):
            out.append({"type": "function_call", "call_id": call["id"], "name": call["name"],
                        "arguments": json.dumps(call["input"])})
            out.append({"type": "function_call_output", "call_id": call["id"],
                        "output": json.dumps(call["answer"])})
        out.append({"role": m["role"], "content": m["text"]})
    return out


for name, replay in (("plain", plain(history)), ("with calls", with_calls(history))):
    calls = sum(1 for item in replay if item.get("type") == "function_call")
    print(f"{name:10} {len(replay)} items, {calls} calls the model can see")
    print("           last assistant turn as replayed:", [i for i in replay if i.get("role") == "assistant"][-1]["content"])

# Expected output:
# plain      4 items, 0 calls the model can see
#            last assistant turn as replayed: Closed #506.
# with calls 8 items, 2 calls the model can see
#            last assistant turn as replayed: Closed #506.

External links

Exercise

Take an agent of yours that uses tools across several turns. Dump exactly what it sends to the model on turn four: every message and item, as the provider receives it. Count the tool calls from earlier turns that are present in that payload. If the count is lower than the calls your agent actually made, run the sketch above and then change your replay so each earlier reply's calls are replayed in your provider's own call shape, ahead of the reply's text.
Hint
Look for a step that flattens history into role and content pairs; that is usually where calls are lost. Most providers accept earlier calls with stable ids and their outputs; give each replayed call an id derived from the stored message so the replay stays identical from turn to turn, which also keeps prompt caches warm.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue
💛 by Pippa— warm

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.