bots-garden/mini-mepublic Fork
⑂ feature/resume-acp
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP
🛟 Updated. 5b2f986 · on feature/resume-acp · k33g · 7d ago
control-agent-calls-with-hooks.md · 104 lines · 4.9 KBmarkdown
Blame HistoryOpen raw

How to control the hand-offs to other agents with hooks

This guide shows how an agent that asks other agents (ask_agent, /ask-<name>, mm --ask) can check, rewrite or refuse each question before it leaves, and check or replace each answer when it comes back. It assumes you have an agent with at least one entry in mmConfigs (see Let the agent call other agents), and that you know how to write a hook (see Guard the agent with hooks). The examples use Node.js.

The hooks go in the asking agent's file. The asked agent does not need to change, or even to know.

Steps

1. Pick the event

You want to… Event Reply
refuse a hand-off (a budget, an agent that must not be asked from here) preAsk {"action":"deny","reason":"…"}
mask secrets, or cut the conversation sent with with_context preAsk {"prompt":"…","transcript":"…"}
give the asked agent standing rules (language, length, format) preAsk {"context":"…"}
turn a failure into something the asking model can act on postAsk {"answer":"…"}
check the answer's format, add a note for the asking model postAsk {"context":"…"}
log every hand-off postAsk nothing

2. Write the hook

This preAsk hook adds standing rules to every question sent to the agent expert:

// hooks/house-rules.js
const input = JSON.parse(require("fs").readFileSync(0, "utf8"));

const rules = ["Answer in the language of the question.", "At most 120 words."];
// Through ask_agent, the answer is read by the asking model, not a person.
if (input.ask.via === "tool") rules.push("No greetings, no questions back.");

console.log(JSON.stringify({ context: rules.join("\n") }));

And this postAsk hook logs the hand-off and, when the asked agent failed, replaces the error with an instruction for the asking model:

// hooks/after-ask.js
const fs = require("fs");
const path = require("path");
const input = JSON.parse(fs.readFileSync(0, "utf8"));

const line = { agent: input.ask.agent, via: input.ask.via, prompt: input.ask.prompt, durationMs: input.durationMs, error: input.error };
fs.mkdirSync(path.join(input.cwd, ".mm"), { recursive: true });
fs.appendFileSync(path.join(input.cwd, ".mm", "ask-audit.jsonl"), JSON.stringify(line) + "\n");

if (input.error && !(input.answer || "").trim()) {
  console.log(JSON.stringify({ answer: `The ${input.ask.agent} agent is unavailable. Tell the user in one sentence; do not answer yourself.` }));
}

3. Try them without mm

echo '{"event":"preAsk","cwd":"/tmp","ask":{"agent":"expert","via":"tool","prompt":"What is an inode?"}}' \
  | node hooks/house-rules.js

You should see:

{"context":"Answer in the language of the question.\nAt most 120 words.\nNo greetings, no questions back."}

4. Declare them in the asking agent's file

mmConfigs:
  expert:
    path: agent.expert.yaml
    description: Technical expert. For every technical question.

hooks:
  preAsk:
    - matcher: ^expert$
      command: node "$MM_CONFIG_DIR/hooks/house-rules.js"
  postAsk:
    - command: node "$MM_CONFIG_DIR/hooks/after-ask.js"

matcher is a regular expression on the asked agent's name; without it, the hook runs for every agent.

5. Start the agent and check

mm agent.yaml

The banner ends with hooks: preAsk×1 postAsk×1. Ask a question the agent hands off, then look at .mm/ask-audit.jsonl: one line per hand-off, with "via":"tool". Type /ask-expert <question> and the line says "via":"command".

Variants

  • A budget per question. Declare the same script on preGeneration (reset a counter file) and on preAsk (count, and answer deny past the limit). Count only "via":"tool": a hand-off the user typed, or a script ran, is not the model's doing.
  • Mask secrets. On preAsk, rewrite input.ask.prompt and input.ask.transcript and print both. "transcript":"" sends no conversation at all.
  • A guard. Set onError: fail on a preAsk hook that must not be skipped: when it cannot run, the hand-off is denied.
  • With yolo: false, the user approves the ask_agent call first; preAsk runs after that approval, on the question the model wrote.
  • mm --ask from a skill. The hooks of the file it reads run too ("via":"cli"). With a postAsk hook, stdout carries the answer once the hooks have run, so the calling script reads the replacement.
  • The asked agent's own hooks. It runs the hooks of its own file, which see "asked":{"depth":1}. Use them for what belongs to that agent whoever asks it; use preAsk/postAsk for what belongs to this hand-off.

See also

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
# How to control the hand-offs to other agents with hooks

This guide shows how an agent that asks other agents (`ask_agent`, `/ask-<name>`, `mm --ask`) can check, rewrite or refuse each question before it leaves, and check or replace each answer when it comes back. It assumes you have an agent with at least one entry in `mmConfigs` (see [Let the agent call other agents](let-the-agent-call-other-agents.md)), and that you know how to write a hook (see [Guard the agent with hooks](guard-the-agent-with-hooks.md)). The examples use Node.js.

The hooks go in the **asking** agent's file. The asked agent does not need to change, or even to know.

## Steps

### 1. Pick the event

| You want to… | Event | Reply |
|--------------|-------|-------|
| refuse a hand-off (a budget, an agent that must not be asked from here) | `preAsk` | `{"action":"deny","reason":"…"}` |
| mask secrets, or cut the conversation sent with `with_context` | `preAsk` | `{"prompt":"…","transcript":"…"}` |
| give the asked agent standing rules (language, length, format) | `preAsk` | `{"context":"…"}` |
| turn a failure into something the asking model can act on | `postAsk` | `{"answer":"…"}` |
| check the answer's format, add a note for the asking model | `postAsk` | `{"context":"…"}` |
| log every hand-off | `postAsk` | nothing |

### 2. Write the hook

This `preAsk` hook adds standing rules to every question sent to the agent `expert`:

```js
// hooks/house-rules.js
const input = JSON.parse(require("fs").readFileSync(0, "utf8"));

const rules = ["Answer in the language of the question.", "At most 120 words."];
// Through ask_agent, the answer is read by the asking model, not a person.
if (input.ask.via === "tool") rules.push("No greetings, no questions back.");

console.log(JSON.stringify({ context: rules.join("\n") }));
```

And this `postAsk` hook logs the hand-off and, when the asked agent failed, replaces the error with an instruction for the asking model:

```js
// hooks/after-ask.js
const fs = require("fs");
const path = require("path");
const input = JSON.parse(fs.readFileSync(0, "utf8"));

const line = { agent: input.ask.agent, via: input.ask.via, prompt: input.ask.prompt, durationMs: input.durationMs, error: input.error };
fs.mkdirSync(path.join(input.cwd, ".mm"), { recursive: true });
fs.appendFileSync(path.join(input.cwd, ".mm", "ask-audit.jsonl"), JSON.stringify(line) + "\n");

if (input.error && !(input.answer || "").trim()) {
  console.log(JSON.stringify({ answer: `The ${input.ask.agent} agent is unavailable. Tell the user in one sentence; do not answer yourself.` }));
}
```

### 3. Try them without mm

```bash
echo '{"event":"preAsk","cwd":"/tmp","ask":{"agent":"expert","via":"tool","prompt":"What is an inode?"}}' \
  | node hooks/house-rules.js
```

You should see:

```
{"context":"Answer in the language of the question.\nAt most 120 words.\nNo greetings, no questions back."}
```

### 4. Declare them in the asking agent's file

```yaml
mmConfigs:
  expert:
    path: agent.expert.yaml
    description: Technical expert. For every technical question.

hooks:
  preAsk:
    - matcher: ^expert$
      command: node "$MM_CONFIG_DIR/hooks/house-rules.js"
  postAsk:
    - command: node "$MM_CONFIG_DIR/hooks/after-ask.js"
```

`matcher` is a regular expression on the asked agent's name; without it, the hook runs for every agent.

### 5. Start the agent and check

```bash
mm agent.yaml
```

The banner ends with `hooks: preAsk×1 postAsk×1`. Ask a question the agent hands off, then look at `.mm/ask-audit.jsonl`: one line per hand-off, with `"via":"tool"`. Type `/ask-expert <question>` and the line says `"via":"command"`.

## Variants

- **A budget per question.** Declare the same script on `preGeneration` (reset a counter file) and on `preAsk` (count, and answer `deny` past the limit). Count only `"via":"tool"`: a hand-off the user typed, or a script ran, is not the model's doing.
- **Mask secrets.** On `preAsk`, rewrite `input.ask.prompt` and `input.ask.transcript` and print both. `"transcript":""` sends no conversation at all.
- **A guard.** Set `onError: fail` on a `preAsk` hook that must not be skipped: when it cannot run, the hand-off is denied.
- **With `yolo: false`**, the user approves the `ask_agent` call first; `preAsk` runs after that approval, on the question the model wrote.
- **`mm --ask` from a skill.** The hooks of the file it reads run too (`"via":"cli"`). With a `postAsk` hook, stdout carries the answer once the hooks have run, so the calling script reads the replacement.
- **The asked agent's own hooks.** It runs the hooks of its own file, which see `"asked":{"depth":1}`. Use them for what belongs to that agent whoever asks it; use `preAsk`/`postAsk` for what belongs to this hand-off.

## See also

- Every field and effect: [Hooks reference](../reference/hooks.md#preask)
- Why these hooks belong to the asking agent: [Hooks — explanation](../explanation/hooks.md#hooks-around-agent-calls)
- A complete example: `demos/hooks-ask/`