# Cumulus Agentic Harness — A Primer (for OSS models like Kimi)

> A plain-language explanation of how Cumulus turns a _plain_ language model into an
> _agent_ that can read files, run tools, ask questions, and work through multi-step
> tasks — without relying on the Claude CLI. No code; concepts only.

---

## 1. Why this exists

Cumulus originally drove every turn by spawning Anthropic's `claude` command-line
tool. That CLI hides a lot of machinery: it decides when to call tools, when to stop,
how to stream output, how to recover from truncation. It only works with Claude.

The **agentic harness** is Cumulus owning that machinery itself. It is a single loop
that can drive _any_ model — Claude through its API, or an open-weight model like
**Kimi**, **GLM**, or **DeepSeek** served over HuggingFace — through the same
tool-using, multi-step behavior. The model supplies intelligence; the harness supplies
the structure that makes that intelligence _agentic_.

The one-sentence version:

> **The harness repeatedly asks the model "what next?", does whatever the model asks
> for, feeds the result back, and repeats — until the model says it's finished.**

---

## 2. The core loop

Everything centers on one cycle. Think of it as a conversation between the **harness**
(the orchestrator) and the **model** (the brain):

1. **Ask the model.** The harness sends the system prompt, the conversation so far, and
   the list of available tools. It streams the model's reply back token by token, so the
   user sees text appear live.
2. **Look at _why_ the model stopped.** Every model reply ends with a _stop reason_. Two
   matter most:
   - _"I want to use a tool."_ → the harness must run the tool(s) and continue.
   - _"I'm done"_ (or any other reason) → the harness exits the loop and the turn ends.
3. **Run the requested tools.** If the model asked to use one or more tools, the harness
   executes them, capturing each result (or error).
4. **Feed results back.** The tool results are added to the conversation as the next
   message, exactly as if a user had pasted them in.
5. **Repeat from step 1.** The model now sees the results and decides the next move —
   another tool, more tools, or a final answer.

This repeats until the model stops asking for tools. A single user message can therefore
trigger many internal round-trips (read a file → search it → edit it → confirm), all
invisible to the user except for the streamed narration.

```
user message
   │
   ▼
┌──────────────────────────────────────────────┐
│  ask model  →  stream reply  →  stop reason?  │◄────┐
└──────────────────────────────────────────────┘     │
   │ "use tools"            │ "done"                  │
   ▼                        ▼                         │
 run tools             finish turn                    │
   │                                                  │
   └──── feed results back as a new message ──────────┘
```

### A worked example

User asks: _"What PDF library does `read_file` use?"_

- **Round 1** — model: "I should read the tool handler." → asks for `read_file`.
  Harness runs it, returns the file summary + chunk list.
- **Round 2** — model: "The relevant part is chunk 3." → asks for `read_content_chunk`.
  Harness returns that chunk.
- **Round 3** — model now has what it needs → writes the final answer, stop reason "done".
  Loop exits.

Three round-trips, one user-visible answer.

---

## 3. The provider abstraction

The loop never talks to a specific model vendor directly. It talks to a **provider** — a
thin adapter that knows how to (a) send a request and (b) stream back events. Swapping
the model is just swapping the provider:

| Provider             | Used for                                    | Notes                            |
| -------------------- | ------------------------------------------- | -------------------------------- |
| **Claude (default)** | `model: "claude"` threads                   | The historical path.             |
| **HuggingFace**      | Open-weight models (Kimi, GLM, DeepSeek, …) | OpenAI-compatible streaming API. |

Because the loop only depends on the abstract provider contract, **everything in this
primer applies identically to Kimi and to Claude.** The harness doesn't special-case the
model — it special-cases _behaviors_ (truncation, tool support), which is what the rest
of this document describes.

---

## 4. Tools: eager vs. deferred (the ToolSearch trick)

Models pay a token cost for every tool definition you show them. Cumulus exposes _many_
tools (file reading, content search, inter-agent messaging, email, scheduling, media
upload, …). Sending every full schema on every turn would waste thousands of tokens each
round-trip and crowd the context.

The harness solves this with a **two-tier tool system**:

- **Eager tools (~10).** The handful used constantly. Their full definitions are sent on
  every turn so the model can call them instantly.
- **Deferred tools (everything else).** Only their _name and one-line description_ are
  listed — not their full schema. They are real, callable tools; they just aren't
  "loaded" yet.

When the model wants a deferred tool, it first calls a special tool named **ToolSearch**,
which returns the full schema for the tool(s) it named. Now the model can call the tool
for real.

```
deferred list (cheap):   "- send_email: send an email via Resend"
        │
        ▼  model calls ToolSearch("select:send_email")
full schema returned (expensive, but only when needed)
        │
        ▼  model now calls send_email with correct arguments
```

Two ways to query ToolSearch:

- **Exact lookup** — `select:tool_a,tool_b` returns those specific schemas.
- **Keyword search** — plain words fuzzy-match against tool names and descriptions.

**Crucial detail for OSS models:** the deferred schemas are _re-deferred every turn_ —
they are never permanently accumulated into the context. This keeps the per-turn token
count **flat** no matter how many tools exist or how long the task runs. For a model with
a smaller or pricier context window, this is what keeps long agentic sessions affordable.

> Practical note: a tool a model "doesn't see" may simply be deferred, not missing.
> The correct move is always to call ToolSearch first, never to assume a capability is
> unavailable.

---

## 5. Running tools: concurrent and bounded

When the model requests several tools in one turn, the harness runs them **all at once**
rather than one after another, then collects every result before replying. Independent
reads and searches finish in parallel instead of stacking up.

Each tool result is also **size-bounded**: anything enormous (over ~100 KB) is truncated
before being handed back to the model, so one giant output can't blow up the context.
Errors are captured too — a failing tool returns an error message flagged as an error,
which the model can read and react to, rather than crashing the turn.

---

## 6. Surviving truncation (escalation & stitching)

Open-weight models, like all models, have an output-length ceiling per reply. When a
reply hits that ceiling it gets cut off mid-thought — the stop reason is _"ran out of
room."_ The harness handles this automatically, and the user ideally never notices. There
are two cases:

**Case A — cut off mid-sentence (plain text).**
The harness keeps the partial text, then sends a follow-up instruction: _"you were cut
off; continue exactly where you left off, here are your last ~200 characters, don't
repeat anything."_ Because the original text stream is still open, the continuation flows
in seamlessly. This is called **continuation stitching**.

**Case B — cut off mid-tool-call.**
If the cutoff landed inside a tool request, the tool arguments are now incomplete and
unusable. The harness **discards** that broken attempt entirely and **retries the whole
turn** with a larger output budget.

In both cases the budget grows along a fixed **escalation schedule** — roughly _16k →
32k → 65k tokens_ — giving the model progressively more room. Safety caps stop this from
running forever:

| Guardrail                           | Purpose                                              |
| ----------------------------------- | ---------------------------------------------------- |
| Max continuations (~3)              | Don't retry/continue endlessly.                      |
| Max cumulative output (~65k tokens) | Cap total tokens spent stitching one reply.          |
| Per-model output ceiling            | Never request more than the model actually supports. |

The escalation never exceeds what the specific model can produce — Kimi's ceiling is
respected just as Claude's is.

---

## 7. Pausing to ask the user a question

Sometimes the model genuinely needs a human decision mid-task (which approach? confirm
this destructive step?). For this there is a special tool, **AskUserQuestion**.

When the model calls it, the loop does something unusual: it **pauses and returns
control to the caller**, packaging up everything needed to resume later — the
conversation so far, which question is outstanding, and any _other_ tool results from the
same turn (those still run normally; only the question blocks progress).

The application shows the user the question (single choice, multi-select, or a small
carousel of questions, optionally with free-text). When the user answers, the harness is
called again with that answer slotted in as the tool's result, and the loop **resumes
exactly where it left off** — no work redone, no context lost.

So the lifecycle has three states, not two:

```
running ──► done
   │
   └──► needs_input ──(user answers)──► running ──► done
```

---

## 8. Working step-by-step: "todo mode"

Cumulus encourages the model to track multi-step work in `<todo>` checklists. Left
unchecked, some models — especially eager open-weight ones — try to do _everything_ in
one giant reply, batching many tool calls and huge output into a single turn. That is
hard to follow and easy to truncate.

When the harness detects todo-tracking is active, it **caps the output budget per turn**
(to a few thousand tokens). This nudges the model to work in smaller increments: do a
step, report progress, update the checklist, take the next step. The effect is steadier,
more legible execution and fewer runaway responses.

---

## 9. What this means for an OSS model like Kimi

If you are running Kimi (or GLM, DeepSeek, etc.) inside this harness, here is the mental
model to operate by:

- **You are in a loop.** Your reply isn't the end — if you request a tool, you'll be
  asked again with the result. Don't try to guess tool outputs; ask for the tool and wait.
- **Stop cleanly when finished.** End without requesting a tool and the loop ends. If you
  keep requesting tools, the loop keeps going (up to a turn limit, ~30).
- **Use ToolSearch before exotic tools.** Only the ~10 core tools are pre-loaded. For
  anything else, call ToolSearch by exact name or keyword first to get its schema, then
  call it. A tool not shown is _deferred_, not absent.
- **Emit complete tool calls.** A tool request cut off halfway is thrown away and the
  turn is retried — wasted effort. Prefer smaller, complete requests over one massive
  batch.
- **Truncation is recoverable, but avoid it.** If you're cut off you'll be asked to
  continue, but working in smaller steps (see todo mode) avoids the round-trips entirely.
- **Ask when genuinely unsure.** AskUserQuestion is a real escape hatch for decisions you
  shouldn't make alone. It pauses the whole task safely.
- **Tool calling is the main reliability variable.** The single biggest difference
  between a strong agentic run and a weak one on open models is disciplined, well-formed
  tool use: correct tool names, valid arguments, one clear intent per call.

---

## 10. The whole thing in one picture

```
            ┌─────────────────────────────────────────────┐
            │                 THE HARNESS                  │
            │                                              │
 user msg ─►│  build prompt + eager tools + deferred list  │
            │                     │                        │
            │                     ▼                        │
            │            ask model via PROVIDER            │ ◄── Claude OR Kimi/GLM/…
            │              (stream tokens out)             │
            │                     │                        │
            │              why did it stop?                │
            │        ┌────────────┼─────────────┐          │
            │     tool_use      max_tokens     done        │
            │        │             │             │         │
            │   run tools      escalate /     finish turn  │
            │  (concurrent)     stitch /          │        │
            │        │           retry           exit      │
            │        ▼             │                       │
            │  feed results back ──┘                       │
            │        │                                     │
            │        └──────────► loop again               │
            │                                              │
            │   (AskUserQuestion → pause → resume later)   │
            └─────────────────────────────────────────────┘
```

**In short:** the model is the brain; the harness is the nervous system. The harness
makes any capable model — Claude or open-weight — behave as a reliable, tool-using,
multi-step agent, while quietly handling the unglamorous realities of token budgets,
truncation, concurrency, and human-in-the-loop pauses.
