尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證The batch engine for AI agents.
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗The batch engine for AI agents.
Funky is your always-on task force for running parallel, long-lived agent workloads at scale.
Define your agents, give them sandboxed environments, and send them work. Funky runs sessions in parallel across stateless workers and records every session as an append-only log in Postgres. A worker can die mid-run (SIGKILL, OOM, a deploy) and a fresh one resumes from the log, keeping long-lived workloads moving without repeating side effects.
Requires Docker, an E2B API key, and a key for at least one model provider: Anthropic, OpenAI, Google, Together AI, or xAI.
git clone https://github.com/funkyhq/funky && cd funky
cp .env.example .env # the auth token, a model-provider key, and the E2B key
docker compose up --build
You can interact with the Funky service through Web UI or CURL.
The console, at http://localhost:5173. Define an agent and its sandbox, give it a task, and watch its log grow — all in the browser. It proxies the api and attaches the token itself, so no credential ever reaches the page.
The api, at localhost:3000 — bearer-authed, and the same steps with curl:
export TOKEN=<your FUNKY_AUTH_TOKEN>
export H="Authorization: Bearer $TOKEN"
export J="content-type: application/json"
# 1. an agent config: the model, the system prompt, and its sandbox's network, packages, keys,
# setup commands and size
AID=$(curl -s localhost:3000/v1/agent-configs -H "$H" -H "$J" -d '{
"inference": { "provider": "anthropic", "model": "claude-sonnet-5", "maxTokens": 2048 },
"systemPrompt": "You are an autonomous agent in a fresh Linux sandbox.",
"network": { "type": "unrestricted" },
"packages": {},
"credentials": [],
"setupCommands": [],
"resources": {}
}' | jq -r .id)
# 2. a task: the work, handed to the agent config. It starts a session running it, and the
# answer's sessionId is where that session's log lives
SID=$(curl -s localhost:3000/v1/tasks -H "$H" -H "$J" -d "{
\"agentConfigId\": \"$AID\",
\"content\": \"Run uname -a in your sandbox and tell me what you see.\"
}" | jq -r .sessionId)
# 3. follow the log: each poll asks only for what is newer than the last seq seen
# (an answer that isn't a list of entries is an error: print it and stop)
Q=""
while sleep 1; do
NEW=$(curl -s -H "$H" "localhost:3000/v1/sessions/$SID/entries$Q")
echo "$NEW" | jq -e 'type == "array"' >/dev/null || { echo "$NEW" >&2; break; }
echo "$NEW" | jq -c '.[]'
LAST=$(echo "$NEW" | jq '.[-1].seq // empty')
[ -n "$LAST" ] && Q="?after=$LAST"
done
Agent configs can be updated with POST /v1/agent-configs/$AID. Send only the fields to
replace; omitted fields are preserved. Responses include a monotonic version: include the
current value in an update for optimistic concurrency (a stale value returns 409), or omit it
for an unconditional last-write-wins update. A task pins the version current when it was
created — behavior and sandbox together — so an update reaches only tasks created after it.
GET /v1/agent-configs/$AID?version=N reads a pinned version; without version it is the
current one.
packages are installed into the sandbox before the agent's first tool call; see
Packages. credentials let the agent use API keys it can't read; see
Credentials.
The task answers 201 with its id and the sessionId it started — accepted, not done. A
worker claims the run, provisions an E2B sandbox the moment a tool actually executes (an
inference-only turn never pays for one), and every committed step lands in the session's log
as one entry — seq is its place in the log, and GET /entries?after=<seq> answers
everything after it, in order:
{"seq":0,"type":"message","message":{"role":"user","content":[...]}}
{"seq":1,"type":"message","message":{"role":"assistant","content":[{"type":"toolCall","name":"bash",...}],"stopReason":"tool_use"}}
{"seq":2,"type":"message","message":{"role":"toolResult","toolCallId":"...","content":[...]}}
{"seq":3,"type":"message","message":{"role":"assistant","content":[{"type":"text","text":"It's a Linux system..."}],"stopReason":"end_turn"}}
There is no push: Funky runs deferrable work, so you poll as often as you care to, or check
back hours later. The cursor is just the last seq you hold, so a client that stops and
comes back misses nothing. Ctrl-C the loop when done; docker compose down stops the stack
(-v also wipes the database).
Scale workers with docker compose up -d --scale worker=3 — claiming is the only
scheduler; nothing else changes. A worker runs one step at a time by default;
FUNKY_CONCURRENCY=8 in .env has each hold eight claims at once (one process, one pool,
one drain), so a small container serves many sessions while their steps wait on a model or
a sandbox. Scale down the same way: a removed worker stops claiming, finishes a short step
or hands a long one back, and exits.
A task creates a session on an agent config and gives it its first message in one call, so
a failure never leaves an empty session behind. The session is an ordinary one: it takes
follow-up messages with POST /v1/sessions/$SID/messages, and the log above is its log.
GET /v1/tasks lists a namespace's tasks, newest first; GET /v1/tasks/:id reads one back;
POST /v1/tasks/:id/cancel cancels its session's run. POST /v1/sessions still creates a
bare session for experiments, but the console starts work only as tasks.
GET /v1/sessions/$SID/usage/summary answers what a session has used so far:
{"tokens":{"input":50320,"output":2262,"cacheRead":38000,"cacheWrite":4000},"runtimeSeconds":41.7}
tokens sums the usage the provider reported on each assistant message; input includes
the cache reads and writes. An errored or aborted message carries none, so it adds nothing.
GET /v1/sessions/$SID/usage lists the same counts per message. runtimeSeconds is how
long the session's runs have taken, each from its first message to its end. Idle time
between runs isn't counted, and a run still going counts up to now. It measures Funky
working on the session, not what the sandbox provider bills: an E2B sandbox runs from the
run's first tool call until the worker pauses it at the run's end (or, should that pause
never happen, until its timeout, FUNKY_SANDBOX_TIMEOUT_MS, 30 minutes by default). The
console shows a session's token total beside its log.
A config's inference.provider picks the vendor that serves it. At boot the worker wires
one inference adapter per model-provider key it finds and routes each step to the adapter
the session's config names, so one stack runs sessions on different vendors side by side:
inference.provider | key |
|---|---|
anthropic | ANTHROPIC_API_KEY |
openai | OPENAI_API_KEY |
google | GOOGLE_GENERATIVE_AI_API_KEY |
togetherai | TOGETHER_API_KEY |
xai | XAI_API_KEY |
Inference goes through the Vercel AI SDK behind a vendor-neutral port.
Every Anthropic call opts into prompt caching (Anthropic's automatic caching, 5-minute
lifetime). A run resends its conversation on each step, so when steps follow within five
minutes, most of a step's input is a cache read, which Anthropic bills at a small fraction
of the input price. Writing the cache costs more than plain input (1.25x), so a single-step
run, or a step after a gap of more than five minutes, pays a little more, not less. Prompts
under the model's minimum cacheable length (512 tokens on the 5.5 models) are not cached.
The usage counts above show it as cacheRead and cacheWrite. OpenAI and Gemini cache
repeated prefixes on their own.
packages maps a package manager to specs in its own syntax:
"packages": { "apt": ["graphviz"], "pip": ["pandas==2.2.0"], "npm": ["[email protected]"] }
Supported on E2B: apt, pip, npm. Each package set is built once into an E2B template
and shared by every session that uses it, so packages work under any network policy.
"error", with the reason in the tool result.setupCommands run after the packages install, in the same build:
"setupCommands": ["echo 'hello world' > ~/hello.txt"]
"error", naming it. Give downloads their own retries.resources sizes the sandbox: cpuCount vCPUs and memoryMB MiB of memory. A field left
out is the default, 2 vCPUs and 1024 MiB, so {} is 2 and 1024:
"resources": { "cpuCount": 4, "memoryMB": 4096 }
git checkout, can outgrow the default. When memory runs
out, commands hang until they time out (exit code 124)."error" with its reason.Most setups skip this. It's only for API keys your agent's tools use, such as a Stripe or
GitHub key: Funky stores them by name (/v1/secrets) in Infisical
rather than Postgres, and agent configs hand them to sandboxes as
credentials. Without it, those routes answer 404 "no secret store configured".
In Infisical Cloud (the free plan is enough), create a project, and under its Access Control → Machine Identities create an identity with the Member role.
On the identity's page, copy the Client ID under Universal Auth (not the Identity ID), and add a client secret.
Put them in .env with the project's ID (Project Settings → Copy Project ID), and run
docker compose up -d again:
INFISICAL_CLIENT_ID=...
INFISICAL_CLIENT_SECRET=...
INFISICAL_PROJECT_ID=...
INFISICAL_ENVIRONMENT=dev
PUT /v1/secrets/stripe-live with {"value": "sk_live_…"} stores a key, GET /v1/secrets lists
the names (never the values), and DELETE /v1/secrets/stripe-live removes one.
A credential lets the agent use a stored key it can't read:
"credentials": [
{
"type": "injected",
"env": "STRIPE_API_KEY",
"secret": "stripe-live",
"domains": ["api.stripe.com"],
"header": "Authorization",
"value": "Bearer {secret}"
}
]
Outside the sandbox, E2B's egress proxy sets Authorization: Bearer <the stripe-live key> on
HTTPS requests to api.stripe.com. Inside, STRIPE_API_KEY holds a placeholder, funky-injected.
domains that echoes request headers hands the key back to the agent.Most runtimes keep the agent's loop, memory, and state in one process; when it dies, in-flight work dies with it. Funky decouples them:
| Path | What it is |
|---|---|
packages/core | The vocabulary: zod schemas for messages, entries, configs, and tool specs |
packages/agent | The engine and driver: inference fold, tool execution, the claim → step → commit loop; the ports |
packages/adapters | The port implementations: Postgres store (drizzle), AI SDK inference, E2B sandboxes, Infisical secrets |
apps/api | The HTTP surface (Hono): configs, secrets, tasks, sessions, intake, cancel, inspection |
apps/worker | The runDriver host — the process a container runs; reads credentials' keys when a sandbox is created |
apps/web | The browser console over the api (React + Vite); compose serves it on :5173 |
pnpm install
pnpm test # unit + PGlite-backed suites; no network, no keys
pnpm typecheck
pnpm format:check
The store conformance suite also runs against a real Postgres when
STORE_TEST_DATABASE_URL is set (CI provisions one). An opt-in end-to-end suite — real
model, real sandbox, kill -9 recovery, and a two-process api+worker run observed entirely
over HTTP — is gated on E2E_DATABASE_URL (a scratch database), ANTHROPIC_API_KEY, and
E2B_API_KEY:
E2E_DATABASE_URL=postgres://... pnpm -F worker test # with the keys in the environment
Early-stage. The best contribution right now is feedback on the interfaces — open an issue to discuss the protocol, a missing method, or a backend you'd want to plug in.