复制安装命令
用 Codex 或 Claude 安装复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它先审查 Skill 页面再帮你安装。
复制前请先查看来源、License 和安全提示。
agentic coding in 30 loc. a loop, two tools, and an llm.
用 Codex 或 Claude 安装复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它先审查 Skill 页面再帮你安装。
复制前请先查看来源、License 和安全提示。
来源文件:README.md
agentic coding in 30 loc. a loop, two tools, and an llm.
bash tool gives full system access: git, curl, compilers, file I/O (cat, sed -i, heredocs); optional timeout=<ms> and bg=truthy for background tasksdelegate tool spawns a mi subagent with a prompt — runs in the same directory, same API, streams output in real-time; optional timeout=<ms>goal tool pursues a high-level goal by iterating subagents until a bash check command exits 0 — uses a progress file so iterations build on prior work (up to 128 by default)delegate and goal tools spawn sub-agents natively; any tool can also call mi as a child processself skillskill tool loads markdown playbooks from skills/ and ~/.agents/skills/ (auto-advertised in system prompt)plan, tasks, delegate, explore, refactor, review, verify, debug, tdd, new-skill, self.mjs files in tools/ (hot-loaded before each model call)--sandbox mode: run inside a Docker container with PWD mounted — safe for destructive experiments. image ships with node, python, uv, git, GNU coreutils/grep/sed/awk/find, curl, jq, ssh, rsync, vim, less, procps, util-linux, bind-tools (dig), iputils (ping), treeAGENTS.md ingestion from current directory for repo-specific context/reset (aliases: /new, /clear) and error recovery-p 'prompt' for scripting and CIecho "do this" | mi or cat file | mi-f <file> argumentSIGINT handling for bash child processes~/.mi/config.json config file (env vars always override)# run directly
npx @avcodes/mi
# or install globally
npm i -g @avcodes/mi
mi
# or run via docker
docker run --rm -it -e OPENAI_API_KEY ghcr.io/av/mi
# interactive repl (type /reset, /new, or /clear to clear history)
OPENAI_API_KEY=sk-... mi
# one-shot (run once, exit)
mi -p 'refactor auth.js to use bcrypt'
# load additional context from a file
mi -f error.log -p 'why is this crashing?'
# pipe stdin to the agent
echo "write a python script that prints hello world" | mi
# local models via any openai-compatible api
MODEL=qwen3.5:4b OPENAI_BASE_URL=http://localhost:33821 mi
# sandbox mode: run in a docker container with current dir mounted
mi --sandbox -p 'rm -rf node_modules && reinstall everything'
# or always run sandboxed via env var
export MI_SANDBOX=1
mi
~/.mi/config.json is an optional JSON config file. keys become env var defaults — your shell env always wins.
{
"MODEL": "o3",
"OPENAI_BASE_URL": "http://localhost:11434",
"REASONING_EFFORT": "high"
}
any env var that mi reads can be set here: OPENAI_API_KEY, MODEL, OPENAI_BASE_URL, REASONING_EFFORT, SYSTEM_PROMPT. the config directory can be overridden with MI_HOME.
| var | default | what |
|---|---|---|
OPENAI_API_KEY | (none) | api key |
OPENAI_BASE_URL | https://api.openai.com | api base url (ollama, lmstudio, litellm, etc) |
MODEL | gpt-5.4 | model name |
REASONING_EFFORT | (unset) | optional reasoning effort for compatible models (minimal, low, medium, high) |
SYSTEM_PROMPT | built-in agent prompt | override the system prompt entirely |
MI_HOME | ~/.mi | config directory (reads config.json) |
MI_SANDBOX | (unset) | truthy = always run in Docker container |
MI_API_PARAMS | (unset) | JSON object merged into chat completion payload (e.g. {"temperature":0}) |
MI_IMAGE | ghcr.io/av/mi:latest | docker image for sandbox mode |
an agentic harness is surprisingly simple. it's a loop that calls an llm, checks if it wants to use tools, executes them, feeds results back, and repeats. here's how each part works.
the agent needs to affect the outside world. tools are just functions that take structured args and return a string. each tool lives in tools/<name>.mjs and exports name, description, parameters, and handler:
// tools/bash.mjs
export default { name: 'bash', description: '...', parameters: {...}, handler: ({command, timeout, bg}) => {
// run shell command, return output
}};
the harness hot-loads tools before each model call by scanning tools/*.mjs. two tools ship by default:
bash gives the agent access to the entire system: git, curl, compilers, package managers, and file I/O (via cat, sed -n, sed -i, heredocs; the system prompt teaches the patterns). optional timeout=<ms> kills the process after the given delay and resolves with [timeout]. optional bg=truthy runs the command detached and returns pid:X log:/tmp/mi-*.log immediately.skill gives the agent specialized workflows loaded on demand from markdown playbooks in bundled skills/ or ~/.agents/skills/.every tool returns a string because that's what goes back into the conversation.
the llm doesn't see your functions. it sees json schemas that describe what tools are available and what arguments they accept. each tool module exports these directly:
// tools/bash.mjs
export default {
name: 'bash',
description: 'run bash cmd',
parameters: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] },
handler: ...
};
the harness builds the tools array from all discovered modules and sends it with every api call so the model knows what it can do.
the conversation is a flat array of message objects. each message has a role (system, user, assistant, or tool) and content. this array is the agent's entire memory:
const hist = [{ role: 'system', content: SYSTEM }];
// user says something
hist.push({ role: 'user', content: 'fix the bug in server.js' });
// assistant replies (pushed inside the loop)
// tool results get pushed too (role: 'tool')
the system message sets the agent's personality and context (working directory, date). every user message, assistant response, and tool result gets appended. the model sees the full history on each call, which is how it maintains context across multiple tool uses.
each iteration makes a single call to the chat completions endpoint. the model receives the full message history and the tool definitions, and we ask for an SSE stream so tokens arrive incrementally:
const res = await fetch(`${base}/v1/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` },
body: JSON.stringify({ model, messages: msgs, tools: defs, stream: true }),
});
// iterate res.body, parse `data: {...}` events, accumulate deltas into one message
the stream emits delta chunks: delta.content is partial text (write straight to stdout as it arrives), delta.tool_calls[i] are partial tool-call fragments (id/name first, then arguments in pieces; merge by index). once [DONE] arrives, the assembled message either has content (a text reply) or tool_calls (the model wants to use tools). this is the decision point that drives the whole loop.
this is the core of the harness. it's a while (true) that keeps calling the llm until it responds with text instead of tool calls:
async function run(msgs) {
while (true) {
const msg = await streamLLM(msgs); // stream tokens to stdout, return assembled message
msgs.push(msg); // add assistant response to history
if (!msg.tool_calls) return; // no tools? we're done (text already streamed)
// otherwise, execute tools and continue...
}
}
the loop exits only when the model decides it has enough information to respond directly. the model might call tools once or twenty times, it drives its own execution. this is what makes it agentic: the llm decides when it's done, not the code. note that text content is written to stdout during the stream, so run() doesn't return it; the user already saw it.
when the model returns tool_calls, the harness executes each one and pushes the result back into the message history as a tool message:
for (const t of msg.tool_calls) {
const { name } = t.function;
const args = JSON.parse(t.function.arguments);
const result = String(await tools[name](args));
msgs.push({ role: 'tool', tool_call_id: t.id, content: result });
}
each tool result is tagged with the tool_call_id so the model knows which call it corresponds to. after all tool results are pushed, the loop goes back to the top and calls the llm again, now with the tool outputs in context.
the outer shell is a simple read-eval-print loop. it reads user input, pushes it as a user message, and calls run(), which streams the response to stdout itself:
while (true) {
const input = await ask('\n> ');
if (input.trim()) {
hist.push({ role: 'user', content: input });
try { await run(hist); }
catch (e) { console.error('✗ ' + e.message); hist.pop(); }
}
}
there's also a one-shot mode (-p 'prompt') that skips the repl and exits after a single run. both modes use the same run() function. streaming works the same way; tokens just go to a piped stdout instead of a terminal. the agentic loop doesn't care where the prompt came from.
the full flow looks like this:
user prompt → [system, user] → llm → tool_calls? → execute tools → [tool results] → llm → ... → text response
two built-in tools extend the single-agent loop into multi-agent workflows.
delegate spawns a fresh mi subagent with a prompt. the child runs in the same directory against the same API. its output streams to the terminal in real-time (the parent REPL is blocked during tool execution, so there's no interleaving). when the child exits, the full output goes back to the parent model as the tool result.
⟡ delegate({"prompt":"fix the failing test in auth.js"})
[subagent output streams here — tool calls, content, everything]
⟡ bash({"command":"npm test"})
all tests pass.
done.
optional timeout (ms) kills the subagent if it runs too long. ctrl-c kills the child process group.
goal wraps delegate in a retry loop. it takes a goal (what to achieve), a check (bash command that exits 0 on success), and an optional max (iteration limit, default 128). before starting, it runs the check — if it already passes, it returns immediately. otherwise it loops:
/tmp/mi-goal-<ts>.md) with the goal, check command, and pre-check outputthe progress file is key: it prevents subagents from re-inventing the world. each iteration reads what was tried before and what the check output was, then picks up where the last one left off.
⟡ goal({"goal":"make all tests pass","check":"npm test"})
── goal 1/128 ──
[subagent 1 streams here]
── ✗ ──
── goal 2/128 ──
[subagent 2 reads progress file, continues from where 1 left off]
── ✓ ──
goal achieved in 2 iterations.
more sophisticated agents add things like memory, retries, parallel tool calls, or multi-agent delegation, but the core is always: loop, call, check for tools, execute, repeat.
name: self
description: Answer questions about how 'mi' works, write new tools, or modify the harness. Use for "how do you work", "write a tool", "add a tool", "create a tool", "extend yourself", "edit yourself", "what tools do you have", or any introspection/modification of the running agent.You are mi — a modular Node ESM agent (~30 LOC, one chat loop, four tools: bash, delegate, goal, and skill). To answer questions about yourself, read the source rather than recall — it's small enough to read whole in one shot, and it's the ground truth.
The harness sets MI_PATH to the running index.mjs at startup. From it you can derive everything else:
$MI_PATH — the main harness file.$(dirname $MI_PATH)/tools/*.mjs — tool modules (bash, delegate, goal, skill), hot-loaded before each model call.$(dirname $MI_PATH) — the package root: README.md, package.json, AGENTS.md, skills/, tools/, tests/, scripts/.$(dirname $MI_PATH)/skills/<name>/SKILL.md — bundled skills.~/.agents/skills/<name>/SKILL.md — user skills (same format, optional).$PWD/AGENTS.md — auto-appended to your system prompt at startup, when present. It's the per-repo context channel.$MI_HOME/config.json (default ~/.mi/config.json) — optional JSON config file, loaded at startup.~/.mi/config.json is an optional JSON file. Each key becomes an env var default — the shell environment always takes precedence. The config directory can be relocated via MI_HOME.
{
"MODEL": "o3",
"OPENAI_BASE_URL": "http://localhost:11434",
"REASONING_EFFORT": "high"
}
Any env var that mi reads can be set here. To inspect the active config: cat ${MI_HOME:-~/.mi}/config.json 2>/dev/null || echo '(no config file)'.
| var | default | what |
|---|---|---|
OPENAI_API_KEY | (none) | API key (required) |
OPENAI_BASE_URL | https://api.openai.com | API base URL (ollama, lmstudio, litellm, etc) |
MODEL | gpt-5.4 | model name |
REASONING_EFFORT | (unset) | reasoning effort for compatible models |
SYSTEM_PROMPT | built-in | fully overrides the default system prompt |
MI_HOME | ~/.mi | config directory (reads config.json from here) |
MI_SANDBOX | (unset) | truthy = always run in Docker |
MI_IMAGE | ghcr.io/av/mi:latest | Docker image for sandbox mode |
mi (REPL) · mi -p '<prompt>' (one-shot) · mi -f <file> (prepend file to system) · mi --sandbox (run in Docker) · mi -v (version) · mi -h (help). Stdin pipes work: echo ... | mi./reset clears history (keeps system prompt). Aliases: /new, /clear.cat $MI_PATH and cat $(dirname $MI_PATH)/tools/*.mjs — the harness plus tools are ~30 lines total. For a specific concern: grep -rn <keyword> $MI_PATH $(dirname $MI_PATH)/tools/.cat $(dirname $MI_PATH)/README.md.cat $(dirname $MI_PATH)/AGENTS.md — note the "30 loc is load-bearing" rule.skill tool with no name arg (returns - name: description bullets from both skill dirs).cat $(dirname $MI_PATH)/skills/<name>/SKILL.md (or ~/.agents/skills/<name>/SKILL.md for user skills).node -p "require('$(dirname $MI_PATH)/package.json').version".index.mjs and tools/*.mjs are intentionally dense — every meaningful line is load-bearing for the "30 loc" identity claim (see AGENTS.md). Before editing:
AGENTS.md and the current source.cd $(dirname $MI_PATH) && npm run lines — line count should not regress.node $MI_PATH -h (loads the module without needing an API key).Tools are code — they give you new capabilities. Skills are markdown — they teach you procedures. To add a new tool:
fetch, grep, db).cat > $(dirname $MI_PATH)/tools/<name>.mjs <<'EOF'
export default {
name: '<name>',
description: '<what it does — shown to LLM>',
parameters: {
type: 'object',
properties: { arg: { type: 'string' } },
required: ['arg']
},
handler: async ({arg}) => {
// your code here
return 'result string';
}
};
EOF
Available globals (no import needed): spawn, readFileSync, existsSync, readdirSync, homedir. Handler must return a string. For reference, read existing tools: cat $(dirname $MI_PATH)/tools/*.mjs.
A tool that spawns mi as a sub-agent:
export default {
name: 'delegate',
description: 'Run a subtask in a separate mi instance',
parameters: { type: 'object', properties: { task: { type: 'string' } }, required: ['task'] },
handler: ({task}) => new Promise(resolve => {
const child = spawn('mi', ['-p', task], { stdio: ['ignore', 'pipe', 'pipe'] });
let out = ''; child.stdout.on('data', d => out += d); child.stderr.on('data', d => out += d);
child.on('exit', () => resolve(out));
})
};
The sub-agent inherits env vars (OPENAI_API_KEY, MODEL) and runs independently with its own context.
评论 (0)
暂无评论,成为第一个评论者吧!