fastagent AGENTS.md

Project instructions for fastagent, a tool that turns file-defined agents into live services for apps, GitHub, Telegram, or other channels. An agent is defined by files such as its identity, skills, tools, and project context.

In plain words
What is it for?
Use them when implementing agent handlers, chat-channel routing, integrations, documentation, or contributor workflows.
Why use it?
They identify the main design contract, source files, and channel-routing rules. This helps developers change the system without breaking how agents answer or remember conversations.

Instructions file for CodexOpenCode

Install

Getting it into your agent

One page per mod, every tool's command on it. A separate URL per tool would split the same page into five that compete with each other.

agentmods
npx agentmods add instructions/fastagent-sh/fastagent/agents-md
Clone the repo
git clone --depth 1 https://github.com/fastagent-sh/fastagent

Made for: Codex, OpenCode.

Per session 6,614 This file is loaded in full into every session.
When invoked 6,614 The same file — it is already loaded in full.
Security scan A 0 findings. Scan, not verified.
Origin original No closer match found in the catalogue.
Token cost

What it costs to keep this loaded

Counted locally with the o200k_base tokenizer, which is exact for GPT models; Claude uses its own tokenizer and its counts differ. Treat this as one consistent yardstick across the catalogue rather than a bill. Prices are per million input tokens.

ModelPer sessionOnce invoked
Fable 5 $0.06614 $0.06614
Opus 5 $0.03307 $0.03307
Sonnet 5 $0.01323 $0.01323
Haiku 4.5 $0.00661 $0.00661

Measured 2d ago against content hash b48f5a92ef47, method: parsed. Prices are Anthropic first-party input rates as of 2026-08-30, from the pricing page.

Security

Grade A, and why

fastagent AGENTS.md scanned grade A with 0 findings against 26 rules in 11 categories — prompt injection, anti-refusal, data exfiltration, privilege escalation, supply chain, agent snooping, system-prompt leakage, SSRF and excessive agency — measured 2d ago.

A static scan of the body, not an audit. Every finding is printed with the line that produced it so you can judge whether it matters here. A mod is markdown that instructs an agent; that is exactly why what it instructs is worth reading.

Nothing flagged

None of the 26 patterns this scan looks for appear in this file: no shell pipes, no recursive deletes, no credential paths, no hidden text, no instruction-override or anti-refusal phrasing, no agent-config snooping. That is not a guarantee, it is the absence of the things that are checkable.

AGENTS.md · 270 lines

How it starts

The opening of the file, as written. The whole thing — 270 lines — stays where its author put it; the contents beside it link to each section on GitHub.

fastagent — Agent Guide

What this is

fastagent is "Vibe first. Then FastAgent" for agent directories: it turns a file-defined agent (persona.md identity, skills/, tools, and existing AGENTS.md project context) into a live service inside an app, on GitHub, in Telegram, or behind a custom channel without a new authoring DSL.

The stable design center is the engine-neutral Agent Handler contract (docs/SPEC.md); pi (@earendil-works/pi-*) is the reference implementation.

Source of truth

Document Purpose
docs/SPEC.md The locked v0.1 Agent Handler contract. Do not change its semantics without an explicit decision.
docs/design/core.md The pi reference implementation and current architecture.
docs/design/participant-model.md When a chat channel speaks, where it answers, what it remembers. Authority for Feishu/Lark + Slack routing.
docs/overview.md, docs/README.md Product overview and documentation index.
CONTRIBUTING.md The full GitHub workflow (branch model, PR loop, merge strategy, review policy).

Code truth is src/.

Repo map

src/
├── agent.ts                 # the Agent Handler contract (pure types, no engine import)
├── service.ts               # THE PRODUCT AS ONE CALL: a directory becomes a live service
│                           # (createAgentService). Before it, only the CLI could keep fastagent's
│                           # "into a live service inside an app" promise — everything else was
│                           # parts an embedder had to assemble in the right order, and getting it
│                           # wrong is silent (a plane that 404s while advertising itself). The
│                           # assembly parts (routesFor / mountSessionControl / startSchedules) live
│                           # here rather than in cli/, because a public entry must not reach into a
│                           # directory that calls process.exit — guarded in package-boundary.test.
│                           # dev/start are callers of this, not a second implementation.
├── channel.ts               # the Channel contract — the TRIGGER side of the product boundary
│                           # (core.md §1), beside agent.ts and session.ts: ChannelModule / Routes /
│                           # ChannelHandler / LongConnection*. Pure types, no host, no framework:
│                           # a WebSocket ingress needs LongConnection and has no HTTP in it, and a
│                           # channel author must not pull node:http in behind a type import.
├── collect.ts               # caller-side stream helpers: collect (buffered consumption) + abortFirstIterator (shared cancellation protocol)
├── core.ts, node.ts,        # THE THREE LAYERS, split by what each costs to import: core (+session)
│   pi.ts                    # is engine- AND runtime-neutral with ZERO packages; node adds what needs
│                           # a Node runtime (the assembly, the http binding); pi names an engine.
│                           # Every layer's dependency list is asserted in package-boundary.test.ts.
├── index.ts                 # supported all-in-one entry (re-exports core + node + session + pi)
├── cli.ts                   # the THIN entry (import-free; lazy-loads cli/program.ts)
├── cli/                     # the CLI, built on clig.dev: kernel.ts (CommandSpec-as-data + the commander adapter — commander appears ONLY here; help/suggestions/exit-code policy: 0 ok, 1 runtime, 2 usage), program.ts (the spec registry — the CLI surface's single source of truth; lazy per-command imports), presenters (invoke-stream.ts `invoke` stream → exit code; models-view.ts/auth-view.ts `models`/auth-report output; add-feishu.ts `add feishu|lark` app onboarding), shared.ts/serve.ts (cross-command helpers: serve/bind reporting, tunnel — the ASSEMBLY lives in service.ts and the agentcore one in channels/agentcore-service.ts, since a public entry may not reach into cli/), fail.ts, commands/ (one module per command)
├── telegram.ts, github.ts,  # subpath-export shims (@fastagent-sh/fastagent/telegram etc.)
│   slack.ts, feishu.ts,
│   lark.ts
├── bind.ts                  # THE reading of a bind address, as the six DIFFERENT questions it is:
│                           # bindable (isBindAddress) / an address not a name (bindAddress, applied
│                           # where a value enters) / reach (classifyBind) / dialable by the NAME
│                           # localhost (answersLocalhost — NOT the same as reach: 127.0.0.2 is
│                           # loopback and --tunnel still cannot reach it) / how a message names it
│                           # (bindLabel) / what a client dials (clientHost). The flag, http.host
│                           # validation, serveNode, the ready lines, control.json and the deploy
│                           # pre-flight all read one through this — conflating any two of those
│                           # questions produces a silent failure, which is why they are separate.
├── log.ts                   # leveled logging singleton (dev=debug, start=info)
├── session.ts               # engine-neutral session-control contract (SessionControl: state/entries/events + dispatch, error codes)
├── session-remote.ts        # remote clients over /control/*: connectSessionControl (control plane) + connectAgent (data plane)
├── observe.ts               # turn-trace logging around an Agent
├── tunnel.ts                # `--tunnel`: cloudflared + per-channel webhook dispatch
├── dev-supervisor.ts        # `dev` supervisor: restart on code-input edits (definition is live-read per invoke)
├── proxy.ts                 # HTTPS_PROXY wiring
├── env.ts                   # `.env` → process.env loading (missing file is normal; anything else surfaces)
├── runtime.ts               # agent runtime/package-manager detection (node vs bun) + readPackageJson
├── loader.ts                # neutral ESM module discovery/loading for tools/ channels/ schedules/ config
├── paths.ts                # PLACEMENT + the path predicates everyone shares (isUnderDir):
│                           # resolvePlacement — ONE marker (`fastagent.config.*`, at any
│                           # NAME) and one rule: the workspace is the dir you point at, the agent is the
│                           # single config holder at it or one level inside + the machinery paths that follow
│                           # (.secrets/.state + env overrides), the containment guard, and the neutral
│                           # path helpers the CLI/deploy share (displayPath, exists). Engine-neutral,
│                           # so the scaffold/deploy/watcher/env consume it without touching engines/pi.
├── atomic-write.ts         # writeFileAtomic: the ONE synchronous "whole file or none of it" write
│                           # (temp + rename + mode), after five copies drifted apart. The fixed
│                           # `<path>.tmp` rests on one-writer-per-state-root, with slack onboarding
│                           # state as the documented exception, and is the seam channel tests use to
│                           # inject a write failure.
├── version.ts              # package version (deploy pins it into the image)
├── scaffold/                # `init` / `add <channel>` / `add skill` + templates/ (real files)
├── channels/
│   ├── serve.ts             # HOW a route table becomes a running server. Dispatch is a MAP LOOKUP
│   │                     # on literal paths — a deployment mounts a handful (one per channel, plus
│   │                     # health, plus the plane's prefix), and a routing library would answer the
│   │                     # same question through a pattern language we do not use, whose extra
│   │                     # semantics every collision check would then have to PREDICT. Prefix
│   │                     # owners are a separate mount argument, not a key spelling. Plus the
│   │                     # totality boundary and the node:http binding. Shared ground, NOT a
│   │                     # deployment target: every host in deploy/ runs this same process. Hono
│   │                     # lives INSIDE this file (overrideGlobalObjects: false keeps it there — an
│   │                     # embedder's globals are not ours to swap); the types stay pure Fetch.
│   ├── agentcore-service.ts # the AgentCore SERVING assembly — same product as service.ts, built
│   │                     # differently because the host is: the adapter is the surface, channels are
│   │                     # discovered LAZILY (the state mount at boot is pre-restore, so eager
│   │                     # discovery would cache that emptiness and clobber the restore), the clock
│   │                     # is external, resident connections cannot survive scale-to-zero. Returns an
│   │                     # AgentService, so `start` picks an assembly once and everything after is
│   │                     # common. Owns nothing process-global: the wake sink stays with the entry.
│   ├── http.ts              # HTTP/SSE channel (consumes only the Agent contract). Serving it is
│   │                     # serve.ts's job — this file knows only the contract and one stream's shape
│   ├── control.ts           # session-control transport: bearer-token /control/* routes (dispatch + SSE events with wire envelope + /control/invoke)
│   ├── discover.ts          # channels/ filesystem discovery (ChannelModule → Routes) — engine-neutral,
│   │                     # so it lives here and not under engines/ (#365)
│   ├── body.ts, respond.ts  # channel-authoring kit (body cap, responses)
│   ├── wait-health.ts       # SHARED readiness probe — channels AND deploy use it, so NOT in kit/
│   ├── registration.ts      # SHARED registrar outcome (registered|manual|failed) — same reason
│   ├── kit/                 # WRITING a channel — the parts every chat platform needs and none should
│   │   │                     # reinvent. The split from the mechanism beside it is a FACT about
│   │   │                     # imports, asserted in package-boundary.test.ts: every file here has
│   │   │                     # consumers only under channels/<platform>/, and serve/http/control/
│   │   │                     # discover have none there. Neither side may reach for the other.
│   │   ├── preview-kit.ts   # turn-view reducer (event → view state + line renderers) + preview policies
│   │   ├── invoke-turn-kit.ts # busy-retry stream loop around agent.invoke (onCompleted commit point)
│   │   ├── turn-queue.ts    # in-memory per-session serial turns (FIFO; telegram + slack + feishu)
│   │   ├── turn-store.ts    # generic durable turn intent (L1) — record shape/validator/order injected
│   │   ├── context-buffer.ts# generic durable un-summoned-discussion buffer (peek→completed→commit)
│   │   ├── thread-participants.ts # who the agent has HEARD in a thread (the summon rule)
│   │   ├── state.ts, seen.ts# atomic channel state + bounded durable delivery dedup
│   │   ├── signature.ts     # replay window for a signed webhook ingress (the LENGTH is the platform's)
│   │   ├── tasks.ts         # fire-and-forget side-task tracking — channels drain it in turnsIdle
│   │   ├── text.ts          # Unicode-safe code-point slicing (cards, preview kit)
│   │   ├── attachment-path.ts # where an attachment lands: the conversation id is ENCODED into a
│   │   │                     # directory (like piSessionId — an id belongs to the caller, so it is
│   │   │                     # never rejected), the file name is only reduced, since the model reads it
│   │   └── stop-command.ts  # the shared /stop parsing every chat channel accepts
│   ├── github/              # github channel (+ scaffold/ bundle)
│   ├── telegram/            # telegram channel: see docs/design/core.md §7
│   │   ├── telegram.ts      # Telegram wiring: ingress + per-turn lifecycle + composition (pure parsing → parse.ts, run one turn → invoke-turn.ts)
│   │   ├── parse.ts         # pure protocol parsing: field extraction, prompt envelope, summon/route policy (no state/IO)
│   │   ├── invoke-turn.ts   # run one turn: assemble inputs (resolve attachments: download/vision) + stream agent.invoke
│   │   ├── turn-store.ts    # telegram's record + update_id arrival order over the shared generic store
│   │   ├── context-buffer.ts# telegram's entry shape + attachment selection over the shared generic buffer
│   │   ├── preview.ts       # live-preview pump + terminal-write policy
│   │   ├── telegram-api.ts  # the single Bot API pipeline + HTML-aware split
│   │   ├── register-webhook.ts # --tunnel setWebhook registration
│   │   └── scaffold/        # `add telegram` bundle (channel.ts + send tool)
│   ├── slack/               # Slack Agent: native streams + inline tool traces, rotating bot auth, signed Events API ingress, durable threads/context, files + onboarding/scaffold
│   ├── feishu/              # CANONICAL Feishu channel engine — see docs/design/core.md
│   │   ├── feishu.ts        # ingress + per-turn lifecycle + composition; Lark binds this engine via a profile
│   │   ├── cloud.ts         # explicit Feishu-reference / Lark-compatibility capability profiles
│   │   ├── model.ts, normalize.ts, parse.ts, crypto.ts, card.ts # protocol model/content normalization/policy + security/card
│   │   ├── invoke-turn.ts, preview.ts # turn IO + streaming-card delivery
│   │   ├── context-buffer.ts# feishu's entry shape + resource selection over the shared generic buffer
│   │   ├── feishu-api.ts    # canonical Open API pipeline (token cache, retry, cardkit)
│   │   ├── register-app.ts  # `add feishu`: scan-to-create device flow
│   │   ├── register-webhook.ts, bootstrap-token.ts # event URL + token automation
│   │   └── scaffold/        # `add feishu` bundle
│   └── lark/                # Lark compatibility/degraded edges over the Feishu engine
│       ├── lark.ts          # thin branded adapter bound to LARK_COMPAT_CLOUD
│       ├── onboard.ts       # unbound launcher + credentials + manual config fallback
│       └── scaffold/        # `add lark` bundle
├── deploy/                  # `deploy docker|fly|railway`: host artifacts + runbook + `--run` CLI drive (docs/design/core.md §9)
│   │                        # LAYOUT: neutral kernel at top (horizontal) + one dir per host (vertical) — new host = new dir, copy fly/
│   ├── channel-ingress.ts   # HOW A RUNNING CHANNEL IS REACHED: default route, who can set that URL
│   │                     # end-to-end, the words when nobody can. The ONE answer to "which channels
│   │                     # have a webhook" — it was written per host (3 runbooks, 3 --run drivers, a
│   │                     # docker path table, the tunnel announcer) and the long-connection exception
│   │                     # reached only the feishu/lark branches, so a long-connection Telegram deploy
│   │                     # printed setWebhook and 409'd the channel it just deployed. Every function
│   │                     # filters the DeclaredChannel list ITSELF — a pre-filtered argument is how it
│   │                     # drifted. Consumed by every host AND by the serving path (src/tunnel.ts)
│   ├── registration-gate.ts # host-NEUTRAL step-7 gate policy: registrars report facts (registered|manual|failed), this owns gate-or-not
│   ├── preflight.ts         # host-NEUTRAL pre-flight: model-travel gate (modelTravelIssue), channel discovery, auth probe, container facts + warnings
│   ├── container.ts         # portable Dockerfile + .dockerignore (host-neutral) + the generated-marker predicate
│   ├── secrets.ts           # required-secret NAMES (runbook) + assembleSecrets VALUES (--run credential carry)
│   ├── runner.ts            # the shared host-CLI dispatcher seam (CliRunner + spawnRunner; faked in tests)
│   ├── docker/  { plan.ts, run.ts }  # Local Docker: Compose topology (agent + optional Quick Tunnel) + `--run` compose driver
│   ├── fly/     { plan.ts, run.ts }  # Fly: PLAN (artifacts + runbook, pure) + `--run` driver (drives flyctl behind the runner seam)
│   └── railway/ { plan.ts, run.ts }  # Railway: same two roles — NOT a copy of Fly (thin config, minted URL, no scriptable scale-to-zero)
├── schedule/               # the N axis, clock form: a time-trigger firing the agent on a cron (schedules/<name>.ts)
│   ├── schedule.ts         # defineSchedule({ cron, tz?, prompt }) authoring surface + types (no session field — it's runtime-derived)
│   ├── cron.ts             # the one place touching `croner` (zero-dep, IANA tz/DST): nextRun + cronError
│   ├── discover.ts         # schedules/ filesystem discovery (loadSchedules/discoverScheduleFiles), isolates a bad file (G2)
│   ├── scheduler.ts        # lifecycle + fire algorithm (overdue catch-up ONCE, claim-before-invoke) + stable per-schedule session + wake-up poll
│   ├── wakeups.ts          # the agent's self-scheduled wake-ups, one-shot + recurring (2nd producer): engine-neutral store + guardrails (min delay/gap, cap, claim/defer)
│   ├── audit.ts            # runs.jsonl append-only run audit (full reply) + `schedule history` reader — "did last night's run silently fail?"
│   └── state.ts            # atomic schedule state under <stateRoot>/schedule/ (fires.json + wakeups.json)
└── engines/pi/              # the pi reference implementation
    ├── create.ts            # reusable assembly ladder L1–L2 + engine assets/prompt
    ├── turn-kit.ts          # the turn mechanism's pi-CLASS-neutral half: lease (single-writer
    │                         # floor), terminals (settled message/thrown error → SPEC terminal +
    │                         # retryable), EventQueue (push→pull), prompt image prep, the SPEC
    │                         # projection, and the observation seam (RunControls + SessionObserver)
    ├── invoke-session.ts    # THE L0: pi's AgentSession, one per invoke, over the same durable
    │                         # record. Events translate ONCE into the rich SessionEvent vocabulary;
    │                         # the SPEC stream is its projection. Owns the run's identity, its
    │                         # controls, and exactly one settlement
    ├── agent-session-factory.ts # the engine binding: the assembly (model/prompt/skills/tools) bound
    │                         # to one record per invoke. services shared, session per turn. Carries
    │                         # the adaptations pi's TUI origins require — see its header
    ├── session-store.ts     # session records on pi's SessionManager: Caller ids encoded into names
    │                         # pi accepts, a record published on create (pi buffers until the first
    │                         # assistant message), crash reconciliation for interrupted tool calls
    ├── session-inheritance.ts # where a NEW thread starts from when it names a parent
    │                         # (participant-model.md §5): fork the parent's active path to the branch
    │                         # point, then bound the model's view with one mechanical compaction mark
    ├── session-control.ts   # the pi session-control hub: observation projections + dispatch (run modulation, boundary mutations, abortable compaction)
    ├── session-builder.ts   # definition-aware session builder: agent assembly → resident pi AgentSessionRuntime (chat TUI consumes it)
    ├── open.ts              # shared opener: directory → agent for dev/start/invoke
    ├── chat.ts              # `chat` channel: drive pi's interactive TUI with the assembled agent
    ├── tool.ts              # defineTool (Zod, incl. deferred: true) + tools/ filesystem discovery
    ├── tool-context.ts      # ToolContext.session + tool-activation bridge via AsyncLocalStorage (set around the turn; read in execute — the wake/search_tools seam)
    ├── search-tools.ts      # built-in search_tools loader for deferred tools (auto-mounted when any tool is deferred; author's wins)
    ├── wake-tool.ts         # the built-in `wake` tool (pi-coupled: defineTool): writes a wake-up into ToolContext.session; withWakeTool mounts it (serving path only)
    ├── definition.ts        # AGENTS.md + skills loading and bundling
    ├── config.ts            # fastagent.config.ts loading + model/precedence (placement lives in paths.ts)
    ├── auth.ts, login.ts    # credential store/resolution (project-level auth.json default) + `login` flow
    ├── models.ts            # Models collection wiring + the agent's OWN models.json (custom endpoints:
    │                         # definition-local so it travels; the machine-global ~/.pi one stays unread)
    └── report.ts            # startup report (auth/model/skills/tools surface)
test/                        # vitest; faux models by default + reusable SPEC conformance.
└── embedding.test.ts       # the docs/embedding.md snippets, run against REAL express/fastify (the
                             # only reason they are devDeps): that path crosses the Node/Fetch seam
                             # through code we do not own, so a swap underneath can keep every unit
                             # test green while breaking the paste-this-in promise
docs/                        # SPEC, guides, and maintainer design notes (design/core.md = architecture)

Read the full file on GitHub · 270 lines

Changes

What this file has done since we first saw it

Hashed on every crawl. A supply-chain change to an agent config is a question of when, not whether, so the history is kept rather than the latest state alone.

  1. 2d ago First seen · 270 lines · 6,614 tokens per session scan A b48f5a92ef47

Subscribe to this mod's changes

fastagent AGENTS.md is an instructions file published in the GitHub repository fastagent-sh/fastagent (57 stars, last pushed 2d ago), licensed MIT. It adds 6,614 tokens to every session, about $0.0331 per session on Opus 5. A static security scan graded it A with 0 findings. No closer match exists in the catalogue, so it is treated as the original; first seen 2026-08-30.