LittleCLIP is a lightweight multi-agent system (MAS) orchestrator and monitoring tool.
It provides a Kanban-based UI for managing agent workflows, real-time execution logs, a communication graph, and basic cost metrics.
![]() |
![]() |
Agents are process-based: the orchestrator spawns each agent as a child process, writes a JSON input to its stdin, and reads its JSON output from stdout.
This means agents can be written in any language (Node.js, Python, C#, Bash, etc.) with zero coupling to the orchestrator codebase.
This repository contains only the orchestrator framework. It does not ship any agents. It is designed for developers who want to build and orchestrate their own multi-agent systems of any kind.
A SKILL.md file is included at skill/SKILL.md to give your code assistant full context on how to build agents for LittleCLIP.
LittleCLIP sends an AgentInput (see below) to your agent containing context about the current task, parent tasks, sibling tasks, its own previous executions of the task, and available agents. The agent uses this information to perform its work.
In return, LittleCLIP expects an AgentOutput (see below) to handle the next steps. Through this, agents can:
- Assign tasks to each other
- Wait for sub-tasks to finish before continuing the main task
- Complete their own tasks
- Update their own task status
- Node.js v20 or later
- npm, yarn, or pnpm
npm install
# or: pnpm install / yarn installNote for pnpm users:
better-sqlite3is a native binary module compiled during installation. It is already pre-configured inpackage.jsonunderpnpm.onlyBuiltDependencies.
LittleCLIP uses a local SQLite database file (lite-clip.db at the repository root) managed via Drizzle ORM and better-sqlite3 with WAL mode enabled.
To create the SQLite database and initialize all required tables (tasks, agents, settings), apply the migration files from the drizzle/ directory:
npm run db:migrate
# or: pnpm db:migrateThis will automatically create lite-clip.db if it does not exist yet.
The following database scripts are provided in package.json:
| Command | Tool Command | Description |
|---|---|---|
npm run db:migrate |
drizzle-kit migrate |
Applies all pending migrations from ./drizzle to lite-clip.db |
npm run db:generate |
drizzle-kit generate |
Generates new SQL migration files in ./drizzle from src/db/schema.ts |
npx drizzle-kit push |
drizzle-kit push |
Pushes the schema from src/db/schema.ts directly into lite-clip.db without generating migration files (useful for prototyping) |
npm run dev
# or: pnpm devOpen http://localhost:3000 in your browser to access the dashboard.
- Create an
agents/folder at the project root. - Inside it, create a folder for your agent. The folder name becomes the agent name (e.g.
agents/my-agent/). - Add your executable script (e.g.
run.js,run.py) and any dependency files your agent needs (package.json,requirements.txt, etc.). - In the UI, navigate to the Agents page and click Register Agent.
- Path — working directory for the process (project root by default).
- Command — the shell command to run the agent (e.g.
node agents/my-agent/run.jsorpython agents/my-agent/run.py). - Description — a one-line summary. This description is provided to all other agents so they know when to delegate tasks to yours.
- Bypass Tick (optional) — child tasks assigned to this agent run immediately upon creation instead of waiting for the next scheduler tick.
- Retrigger Parent (optional) — immediately re-invokes the parent task when all sibling tasks finish (
doneorerror), without waiting for the next tick. - Max Chain Calls / min (optional) — per-agent execution rate limit (falls back to global project setting).
- Timeout (optional) — execution timeout in milliseconds (defaults to 60,000 ms / 60 seconds).
- Environment Variables (optional) — JSON object of custom environment variables injected into the agent process at runtime (the
TASK_IDenv var is always injected automatically).
Once registered, the agent is ready to receive tasks.
The orchestrator executes tasks through an automated tick engine with real-time UI synchronization:
- Configurable Tick Interval: Runs on a configurable interval (default: 10 seconds, adjustable in the Settings page). On each tick, all tasks with status
"todo"are scheduled and executed. - Server Lifecycle Hook: The engine starts cleanly on server initialization via Next.js
instrumentation.ts. - Supervisory Watchdog: A background watchdog monitors scheduled ticks every 60 seconds to detect and recover from timer stalls or clock drift, automatically rescheduling overdue cycles.
- Live UI Synchronization: The dashboard header displays an active countdown (
14s,2m 10s, ornow) synced withgetTickState(), accompanied by an interactive play/pause toggle. - Manual Execution: Any assigned task can also be triggered immediately on demand via the Run button in the task detail dialog, without waiting for the next tick.
- Bypass Tick: Agents with Bypass Tick enabled have their child tasks executed immediately upon creation, eliminating scheduler latency in multi-step workflows.
- Retrigger Parent: Agents with Retrigger Parent enabled cause the orchestrator to immediately re-invoke the parent task once all sibling tasks reach a terminal state (
doneorerror), without waiting for the next tick. Combining Bypass Tick on the parent with Retrigger Parent on workers enables fully synchronous delegation round-trips.
todo ──> runner spawns agent ──> agent prints AgentOutput
|
+────────────┴────────────+
has next_tasks no next_tasks
| |
stays "doing" uses agent-provided status
| (default: "done")
children execute...
|
all children reach terminal state
|
agent re-invoked <── with child_tasks populated
|
(loop continues until agent returns no next_tasks)
- Returning
next_taskskeeps the task in"doing"and triggers a re-invocation loop — the orchestrator overrides the agent'sstatusto"doing"when children are present. - When there are no
next_tasks, the orchestrator uses the agent-providedstatusfield. If omitted, it defaults to"done". - An agent can return
status: "error"to self-report failure even on exit code 0, orstatus: "doing"with no children to keep itself alive for external re-invocation. - The agent is re-invoked with
child_taskspopulated once all sub-tasks reach a terminal state (doneorerror). If a child agent has Retrigger Parent enabled, re-invocation happens immediately; otherwise it occurs on the next tick. - On every re-invocation, the agent also receives its own previous runs of the task (status, output, cost, trigger, ...) in
execution_history, so it can tell a retry from a first run or resume from its lastoutput. - Humans can post messages on a task at any time (stored in the
messagestable). Unread messages are sent inhuman_messageson the next run, recorded on that run's execution entry, and marked read once the run is recorded (including failed runs). A rate-limited (deferred) run consumes nothing, and a message posted while the agent is running waits for the next run. - The agent must eventually return with no
next_tasksto complete, otherwise it loops indefinitely. - Exit code
0means process success. Any non-zero exit code marks the task as"error". - Incremental cost reported by each invocation is accumulated by the runner into the task's total cost (
totalCost = previousCost + incrementalCost).
backlog → todo → doing → done | error
| Status | Meaning |
|---|---|
backlog |
Created but no agent assigned |
todo |
Agent assigned, waiting for execution (tick, manual run, or bypass) |
doing |
Currently running or waiting on sub-tasks |
done |
Completed successfully |
error |
Failed (non-zero exit code, unparseable output, or timeout) |
Agents have exactly two touchpoints with LittleCLIP: an input JSON (delivered via stdin) and an output JSON (printed to stdout).
The orchestrator writes a single UTF-8 JSON document to the agent's stdin (preventing OS command-line argument length limits such as ENAMETOOLONG on Windows). For local debugging, you may also pass the same JSON as a CLI argument (process.argv[2] in Node.js, sys.argv[1] in Python).
{
"task_id": "01J5KXYZ...",
"task_body": "The task instructions written by a human or parent agent",
"system_prompt": "Shared orchestration prompt from the settings page",
"parent_context": [
{
"id": "01J5KX...",
"title": "Parent task title",
"body": "Parent task body",
"output": "Parent task output (null if not yet completed)",
"agent": "coordinator"
}
],
"child_tasks": [
{
"id": "01J5KY...",
"title": "Previously created sub-task",
"body": "Sub-task instructions",
"status": "done",
"output": "Sub-task result",
"agent": "worker"
}
],
"sibling_tasks": [
{
"id": "01J5KZ...",
"title": "Sibling task under the same parent",
"body": "Sibling instructions",
"status": "done",
"output": "Sibling result",
"agent": "researcher"
}
],
"execution_history": [
{
"run": 1,
"started_at": "2026-10-02T14:00:00.000Z",
"finished_at": "2026-10-02T14:00:03.120Z",
"duration_ms": 3120,
"cost": 0.002,
"status": "doing",
"exit_code": 0,
"output": "Output returned by this agent on its previous run",
"agent_message": "Reasoning or notes returned on that run, or null",
"spawned_children": 2,
"trigger": { "kind": "tick" },
"human_messages": [
{ "id": "01J5M0...", "content": "Message delivered on that run", "posted_at": "2026-10-02T13:59:00.000Z" }
]
}
],
"available_agents": [
{ "name": "coordinator", "description": "High-level strategist" },
{ "name": "worker", "description": "Executes concrete tasks" }
],
"human_messages": [
{ "id": "01J5M1...", "content": "Message posted by a human on this task", "posted_at": "2026-10-02T14:05:00.000Z" }
]
}| Field | Type | Description |
|---|---|---|
task_id |
string | ULID of the current task |
task_body |
string | The task instructions |
system_prompt |
string | Shared system prompt (configurable in settings) |
parent_context |
array | Ancestor task chain, nearest parent first (up to 10). Body and output fields may be truncated |
child_tasks |
array | Up to 20 most recent sub-tasks with their current status and output. Body and output fields may be truncated |
sibling_tasks |
array | Other tasks sharing the same parent (up to 20 most recent, excludes self) |
execution_history |
array | Previous runs of this task (not parents, children or siblings), oldest first, up to 20 most recent. Empty on the first run. Output field may be truncated |
available_agents |
array | All registered agents with name and description |
human_messages |
array | Unread human messages posted on this task (id, content, posted_at), oldest first. Each message is delivered on exactly one run, then marked read; earlier messages are in execution_history[].human_messages |
Each execution_history entry describes one past invocation:
| Field | Type | Description |
|---|---|---|
run |
number | 1-based run number of this task |
started_at |
string | ISO timestamp when the run started |
finished_at |
string | ISO timestamp when the run finished |
duration_ms |
number | Run duration in milliseconds |
cost |
number | Incremental cost reported by that run (0 on failures) |
status |
string | Task status the run resolved to (done, doing or error) |
exit_code |
number or null | Process exit code (null on spawn failure or timeout) |
output |
string or null | output returned by that run (up to 1000 chars), raw stdout on parse failure, null on non-zero exit |
agent_message |
string or null | message returned by that run (up to 1000 chars), null when none was returned |
spawned_children |
number | Number of next_tasks the run created |
trigger |
object or null | What caused the run: kind is manual, tick, bypass, retrigger or children_done; by_task_id is the child task that caused it, when there is one. null for runs recorded before triggers were tracked |
human_messages |
array | Human messages delivered to that run (same shape as the top-level field). Empty when none were delivered |
The agent must print a valid JSON object to stdout. The orchestrator extracts the last valid JSON object from the full stdout stream, so debug logs before it are ignored.
{
"output": "Summary of what the agent accomplished",
"message": "Why I split the work this way, and what I'm unsure about",
"cost": 0.003,
"status": "done",
"next_tasks": [
{
"title": "Sub-task title",
"body": "Detailed instructions for the sub-task",
"agent": "worker",
"status": "todo"
}
]
}| Field | Type | Required | Description |
|---|---|---|---|
output |
string | yes | Result summary visible to parent agents and humans |
message |
string | no | Informational message for humans (reasoning, notes, caveats). Recorded on the run, shown in the task conversation as a note, and returned in execution_history[].agent_message. Never used as the task's output |
cost |
number | no | Incremental execution cost in dollars (e.g. LLM API cost). The orchestrator accumulates this across re-invocations. Defaults to 0 |
status |
string | recommended | Controls the task's final status. Valid values: "done" (default), "doing", "error". Setting "doing" keeps the task alive for re-invocation on subsequent ticks without children. When next_tasks are present, the orchestrator overrides to "doing". Defaults to "done" if omitted |
next_tasks |
array | no | Sub-tasks to create. Omit or pass [] when the work is complete |
next_tasks[].title |
string | yes | Task card title |
next_tasks[].body |
string | no | Detailed specification for the sub-task |
next_tasks[].agent |
string or null | no | Agent name to assign (must match a name from available_agents) |
next_tasks[].status |
string | no | "todo" when an agent is assigned, "backlog" when agent is null |
async function readInput() {
const fromArg = process.argv[2];
if (fromArg && fromArg !== "--stdin") return fromArg;
const chunks = [];
for await (const chunk of process.stdin) chunks.push(chunk);
return Buffer.concat(chunks).toString("utf8");
}
(async () => {
const input = JSON.parse(await readInput());
const { task_body, child_tasks, available_agents } = input;
// Your logic here
const result = {
output: "What this agent accomplished",
cost: 0,
status: "done", // optional: "done" | "doing" | "error"
// next_tasks: [] // optional: delegate sub-tasks
};
console.log(JSON.stringify(result));
})();import sys, json
def read_input():
if len(sys.argv) > 1 and sys.argv[1] != "--stdin":
return sys.argv[1]
return sys.stdin.read()
def main():
inp = json.loads(read_input())
task_body = inp["task_body"]
child_tasks = inp.get("child_tasks", [])
available_agents = inp.get("available_agents", [])
# Your logic here
result = {
"output": "What this agent accomplished",
"cost": 0,
"status": "done", # optional: "done" | "doing" | "error"
# "next_tasks": [] # optional: delegate sub-tasks
}
print(json.dumps(result))
if __name__ == "__main__":
main()A chain-call limit prevents runaway loops. The global default is 10 calls per minute (configurable via the max_chain_calls_per_minute setting). Each agent can also override this with its own Max Chain Calls / min setting in its configuration.
When an agent hits its rate limit, task execution is deferred (the task remains in "todo" status) and deferred executions are logged. As soon as the rolling 1-minute window clears, the task is executed on the subsequent cycle. This prevents broken workflows while maintaining protection against infinite loops.
- Read the
AgentInputJSON from stdin. For local debugging, you may also pass it asargv[2](Node.js) orargv[1](Python). - Print the
AgentOutputJSON as the last thing to stdout. The orchestrator extracts the last valid JSON object from the full output. - Debug logs are fine — print anything you want before the final JSON. The orchestrator only parses the last JSON object.
- Exit with code
0for success. Non-zero marks the task as"error". - Never import orchestrator code. Agents are standalone processes. All context arrives via the JSON input.
- Environment variables from the agent's configuration are injected at spawn time. Use them for API keys. The
TASK_IDenv var is also injected automatically. - Default timeout is 60 seconds, configurable per agent.
- Next.js — full-stack React framework (app router)
- SQLite via better-sqlite3 — local database acting as the system ledger
- Drizzle ORM — type-safe, code-first database schema and queries
- Tailwind CSS — utility-first styling
- Recharts and D3 — metrics and agent communication graph
Feel free to fork, reuse, and submit change requests.
MIT


