@fastagent-sh/fastagent 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/dist/agent.d.ts +72 -0
- package/dist/agent.js +14 -0
- package/dist/channels/body.d.ts +9 -0
- package/dist/channels/body.js +29 -0
- package/dist/channels/github/github.d.ts +31 -0
- package/dist/channels/github/github.js +80 -0
- package/dist/channels/github/scaffold/channel.ts +25 -0
- package/dist/channels/http.d.ts +13 -0
- package/dist/channels/http.js +161 -0
- package/dist/channels/respond.d.ts +5 -0
- package/dist/channels/respond.js +3 -0
- package/dist/channels/telegram/context-buffer.d.ts +59 -0
- package/dist/channels/telegram/context-buffer.js +139 -0
- package/dist/channels/telegram/invoke-turn.d.ts +53 -0
- package/dist/channels/telegram/invoke-turn.js +127 -0
- package/dist/channels/telegram/parse.d.ts +160 -0
- package/dist/channels/telegram/parse.js +153 -0
- package/dist/channels/telegram/preview.d.ts +22 -0
- package/dist/channels/telegram/preview.js +266 -0
- package/dist/channels/telegram/register-webhook.d.ts +13 -0
- package/dist/channels/telegram/register-webhook.js +73 -0
- package/dist/channels/telegram/scaffold/channel.ts +25 -0
- package/dist/channels/telegram/scaffold/telegram-send.ts +107 -0
- package/dist/channels/telegram/state.d.ts +9 -0
- package/dist/channels/telegram/state.js +58 -0
- package/dist/channels/telegram/telegram-api.d.ts +77 -0
- package/dist/channels/telegram/telegram-api.js +345 -0
- package/dist/channels/telegram/telegram.d.ts +35 -0
- package/dist/channels/telegram/telegram.js +352 -0
- package/dist/channels/telegram/turn-queue.d.ts +21 -0
- package/dist/channels/telegram/turn-queue.js +47 -0
- package/dist/channels/telegram/turn-store.d.ts +38 -0
- package/dist/channels/telegram/turn-store.js +150 -0
- package/dist/cli-auth.d.ts +24 -0
- package/dist/cli-auth.js +30 -0
- package/dist/cli-models.d.ts +11 -0
- package/dist/cli-models.js +20 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1288 -0
- package/dist/collect.d.ts +20 -0
- package/dist/collect.js +27 -0
- package/dist/core.d.ts +10 -0
- package/dist/core.js +8 -0
- package/dist/deploy/container.d.ts +56 -0
- package/dist/deploy/container.js +169 -0
- package/dist/deploy/fly/plan.d.ts +65 -0
- package/dist/deploy/fly/plan.js +142 -0
- package/dist/deploy/fly/run.d.ts +51 -0
- package/dist/deploy/fly/run.js +89 -0
- package/dist/deploy/preflight.d.ts +59 -0
- package/dist/deploy/preflight.js +191 -0
- package/dist/deploy/railway/plan.d.ts +48 -0
- package/dist/deploy/railway/plan.js +91 -0
- package/dist/deploy/railway/run.d.ts +81 -0
- package/dist/deploy/railway/run.js +176 -0
- package/dist/deploy/runner.d.ts +18 -0
- package/dist/deploy/runner.js +25 -0
- package/dist/deploy/secrets.d.ts +49 -0
- package/dist/deploy/secrets.js +88 -0
- package/dist/dev-supervisor.d.ts +14 -0
- package/dist/dev-supervisor.js +158 -0
- package/dist/engines/pi/auth.d.ts +17 -0
- package/dist/engines/pi/auth.js +118 -0
- package/dist/engines/pi/channel.d.ts +31 -0
- package/dist/engines/pi/channel.js +113 -0
- package/dist/engines/pi/chat.d.ts +19 -0
- package/dist/engines/pi/chat.js +200 -0
- package/dist/engines/pi/config.d.ts +110 -0
- package/dist/engines/pi/config.js +285 -0
- package/dist/engines/pi/create.d.ts +127 -0
- package/dist/engines/pi/create.js +222 -0
- package/dist/engines/pi/definition.d.ts +72 -0
- package/dist/engines/pi/definition.js +129 -0
- package/dist/engines/pi/harness.d.ts +50 -0
- package/dist/engines/pi/harness.js +41 -0
- package/dist/engines/pi/invoke.d.ts +28 -0
- package/dist/engines/pi/invoke.js +298 -0
- package/dist/engines/pi/login.d.ts +51 -0
- package/dist/engines/pi/login.js +125 -0
- package/dist/engines/pi/models.d.ts +41 -0
- package/dist/engines/pi/models.js +76 -0
- package/dist/engines/pi/report.d.ts +13 -0
- package/dist/engines/pi/report.js +21 -0
- package/dist/engines/pi/sessions.d.ts +15 -0
- package/dist/engines/pi/sessions.js +130 -0
- package/dist/engines/pi/tool-context.d.ts +17 -0
- package/dist/engines/pi/tool-context.js +13 -0
- package/dist/engines/pi/tool.d.ts +45 -0
- package/dist/engines/pi/tool.js +85 -0
- package/dist/engines/pi/wake-tool.d.ts +20 -0
- package/dist/engines/pi/wake-tool.js +110 -0
- package/dist/engines/pi/workspace.d.ts +53 -0
- package/dist/engines/pi/workspace.js +70 -0
- package/dist/env.d.ts +24 -0
- package/dist/env.js +57 -0
- package/dist/github.d.ts +3 -0
- package/dist/github.js +2 -0
- package/dist/host/node.d.ts +41 -0
- package/dist/host/node.js +50 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/invoke-stream.d.ts +10 -0
- package/dist/invoke-stream.js +26 -0
- package/dist/loader.d.ts +38 -0
- package/dist/loader.js +69 -0
- package/dist/log.d.ts +26 -0
- package/dist/log.js +51 -0
- package/dist/observe.d.ts +12 -0
- package/dist/observe.js +44 -0
- package/dist/pi.d.ts +16 -0
- package/dist/pi.js +12 -0
- package/dist/proxy.d.ts +9 -0
- package/dist/proxy.js +23 -0
- package/dist/runtime.d.ts +26 -0
- package/dist/runtime.js +29 -0
- package/dist/scaffold/add-channel.d.ts +46 -0
- package/dist/scaffold/add-channel.js +227 -0
- package/dist/scaffold/init.d.ts +54 -0
- package/dist/scaffold/init.js +249 -0
- package/dist/scaffold/templates/env.example +24 -0
- package/dist/scaffold/templates/fastagent.config.mjs +19 -0
- package/dist/scaffold/templates/gitignore +8 -0
- package/dist/scaffold/templates/gitignore.agentdir-root +5 -0
- package/dist/scaffold/templates/gitignore.kit +2 -0
- package/dist/scaffold/templates/persona.md +10 -0
- package/dist/scaffold/templates/skills/writing-great-skills/GLOSSARY.md +195 -0
- package/dist/scaffold/templates/skills/writing-great-skills/LICENSE +21 -0
- package/dist/scaffold/templates/skills/writing-great-skills/SKILL.md +82 -0
- package/dist/scaffold/templates/tools/fetch-url.ts +27 -0
- package/dist/scaffold/templates.d.ts +17 -0
- package/dist/scaffold/templates.js +56 -0
- package/dist/scaffold/vendor-skill.d.ts +24 -0
- package/dist/scaffold/vendor-skill.js +157 -0
- package/dist/schedule/audit.d.ts +18 -0
- package/dist/schedule/audit.js +57 -0
- package/dist/schedule/cron.d.ts +7 -0
- package/dist/schedule/cron.js +33 -0
- package/dist/schedule/discover.d.ts +17 -0
- package/dist/schedule/discover.js +64 -0
- package/dist/schedule/schedule.d.ts +35 -0
- package/dist/schedule/schedule.js +23 -0
- package/dist/schedule/scheduler.d.ts +36 -0
- package/dist/schedule/scheduler.js +253 -0
- package/dist/schedule/state.d.ts +14 -0
- package/dist/schedule/state.js +56 -0
- package/dist/schedule/wakeups.d.ts +82 -0
- package/dist/schedule/wakeups.js +196 -0
- package/dist/telegram.d.ts +2 -0
- package/dist/telegram.js +2 -0
- package/dist/tunnel.d.ts +31 -0
- package/dist/tunnel.js +139 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.js +13 -0
- package/dist/workspace.d.ts +9 -0
- package/dist/workspace.js +45 -0
- package/package.json +97 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 the fastagent authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://github.com/fastagent-sh/fastagent"><img src="https://raw.githubusercontent.com/fastagent-sh/fastagent/main/assets/hero.png" alt="FastAgent — Vibe first. Then FastAgent. An agent directory becomes a live service in your app, on GitHub, in Telegram, or any channel." width="860"></a>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
[](https://github.com/fastagent-sh/fastagent/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/@fastagent-sh/fastagent)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://nodejs.org)
|
|
9
|
+
[](https://pi.dev)
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<sub>Built on</sub>
|
|
13
|
+
<a href="https://pi.dev"><img src="https://pi.dev/logo-auto.svg" alt="pi" height="22" /></a>
|
|
14
|
+
<sub>— the agent harness & multi-provider LLM API under the hood</sub>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
A file-defined agent directory can become a live service. FastAgent takes it out of the terminal and serves it in your Next/Astro app, Telegram, GitHub/webhook events, an API endpoint, or your own channel.
|
|
18
|
+
|
|
19
|
+
Leave the terminal. Become a real service.
|
|
20
|
+
|
|
21
|
+
- **Add it to your app** — one route, your auth, your database, your host.
|
|
22
|
+
- **Run it as a live service** — Telegram support, GitHub PR review, webhook handler, API endpoint, or custom channel.
|
|
23
|
+
|
|
24
|
+
FastAgent is not a new agent-authoring DSL. You bring the existing definition and project layout; FastAgent provides the serving runtime and adapters around it.
|
|
25
|
+
|
|
26
|
+
<p align="center">
|
|
27
|
+
<img src="https://cdn.jsdelivr.net/gh/fastagent-sh/fastagent@main/assets/demo.svg" alt="fastagent dev boots an existing agent directory (AGENTS.md, skills/, tools/, channels/) into a live service; a GitHub pull_request.opened webhook arrives and the agent reviews PR #42 and posts inline comments." width="860">
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
## Why FastAgent
|
|
31
|
+
|
|
32
|
+
Coding agents made it cheap to vibe useful agent directories. The hard part is the next step: local agents live in terminals, but real services receive webhooks, join Telegram, serve product users, and expose stable APIs.
|
|
33
|
+
|
|
34
|
+
FastAgent is the missing bridge from local agent directory to real service.
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
- **Vibe first — a directory is an agent.** Point FastAgent at the `AGENTS.md` + `skills/` you already vibed in a coding agent. Markdown instructions, reusable skills, and TypeScript tools stay as files you inspect, edit, and commit — no new DSL, no framework rewrite.
|
|
39
|
+
- **Channels.** Serve the same agent as a GitHub PR reviewer, a Telegram bot, an HTTP/SSE endpoint, or your own adapter — verified webhooks, streaming replies, group-aware.
|
|
40
|
+
- **Models, tools & skills.** Any model provider (OpenAI, Anthropic, Google, …) via OAuth or API key; typed tools discovered from `tools/` (the filename is the name, Zod-validated); Agent Skills loaded on demand. Built on the open-source [pi](https://github.com/earendil-works/pi) harness.
|
|
41
|
+
- **App embedding — your stack, we plug in.** Like Flask/FastAPI for agents: FastAgent never owns your framework. Mount the agent in your Next / Astro / Hono / Bun / Node route with one handler — your auth, your database, your host.
|
|
42
|
+
- **Deploy anywhere.** Run the directory directly — no build step; `fastagent deploy fly|railway` generates the host config + a runbook and hands off (`--run` drives the deploy to completion). The generated container also runs on other Docker hosts.
|
|
43
|
+
|
|
44
|
+
Using a coding agent? Give it [`docs/ai-start.md`](docs/ai-start.md) for an AI-guided setup path.
|
|
45
|
+
|
|
46
|
+
## Design philosophy
|
|
47
|
+
|
|
48
|
+
FastAgent is built around a small serving contract, app-owned runtime concerns, typed boundaries, and composable adapters.
|
|
49
|
+
|
|
50
|
+
- **Small serving core** — `invoke` decouples channels, agents, engines, and hosts.
|
|
51
|
+
- **App-owned runtime** — no takeover of your auth, database, routes, or deployment.
|
|
52
|
+
- **Typed edges** — typed tools, explicit events, boundary validation.
|
|
53
|
+
- **Agent-native shape** — the directory is the deployable unit, and channels drive the same contract.
|
|
54
|
+
|
|
55
|
+
Read [Design principles](docs/principles.md) for the full rationale.
|
|
56
|
+
|
|
57
|
+
## What we didn't build
|
|
58
|
+
|
|
59
|
+
FastAgent stays a small serving layer, so it never dictates your stack. Capabilities other agent frameworks bake into a platform, we leave to your app, your host, or the agent itself — composed in, not locked in.
|
|
60
|
+
|
|
61
|
+
- **No platform to move to.** No dashboard, no control plane, no runtime you deploy *into* — run it locally, embed it in your app, or ship the directory to any host.
|
|
62
|
+
- **No new format or DSL.** `AGENTS.md`, Agent Skills, TypeScript tools, HTTP/SSE — FastAgent consumes the standards you already use instead of a parallel ecosystem.
|
|
63
|
+
- **No workflow engine.** The agent decides its own steps; for deterministic multi-step orchestration, call `invoke` from your own queue or workflow.
|
|
64
|
+
- **No model or cloud lock-in.** The Agent Handler contract is engine-neutral, with pi as the reference implementation; another engine can implement the same `Agent` contract without changing channels.
|
|
65
|
+
|
|
66
|
+
## Install
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm i -g @fastagent-sh/fastagent # CLI: fastagent init/dev/start/...
|
|
70
|
+
npm i @fastagent-sh/fastagent # library API for embedding or code tools
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Requires **Node >= 22.19** (the floor is inherited from the pi reference engine and `undici`), and also runs under **Bun** (smoke-tested in CI on Bun 1.3; its native fetch replaces the undici path). The npm package ships compiled JavaScript and type declarations.
|
|
74
|
+
|
|
75
|
+
## Quickstart
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
fastagent init my-agent
|
|
79
|
+
cd my-agent
|
|
80
|
+
fastagent dev
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Then send a local test turn:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
curl -N -X POST localhost:8787/invoke \
|
|
87
|
+
-H 'content-type: application/json' \
|
|
88
|
+
-d '{"session":"s1","text":"hello"}'
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
For production-style local serving:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
fastagent start
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
There is no FastAgent build step: the directory is the agent.
|
|
98
|
+
|
|
99
|
+
## Embed in an app
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { createInvokeHandler, createPiAgentFromDefinition } from "@fastagent-sh/fastagent";
|
|
103
|
+
|
|
104
|
+
const { agent } = await createPiAgentFromDefinition("./agent", {
|
|
105
|
+
model: "openai-codex/gpt-5.5",
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
export const POST = createInvokeHandler(agent); // Fetch-shaped handler
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
No directory? Assemble from typed parts:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { createPiAgent, defineTool, z } from "@fastagent-sh/fastagent";
|
|
115
|
+
|
|
116
|
+
const lookupOrder = defineTool({
|
|
117
|
+
name: "lookup-order",
|
|
118
|
+
description: "Look up an order by id.",
|
|
119
|
+
input: z.object({ orderId: z.string() }),
|
|
120
|
+
async execute({ orderId }) {
|
|
121
|
+
return await db.find(orderId);
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const agent = createPiAgent({
|
|
126
|
+
model: "openai-codex/gpt-5.5",
|
|
127
|
+
instructions: "You are a support assistant. Use lookup-order for order questions.",
|
|
128
|
+
tools: [lookupOrder],
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Documentation
|
|
133
|
+
|
|
134
|
+
| Document | Purpose |
|
|
135
|
+
|---|---|
|
|
136
|
+
| [docs/README.md](docs/README.md) | Documentation index |
|
|
137
|
+
| [docs/quickstart.md](docs/quickstart.md) | Scaffold, run, add a tool, and start |
|
|
138
|
+
| [docs/configuration.md](docs/configuration.md) | Configure model, auth, ports, sessions, tools, and channels |
|
|
139
|
+
| [docs/principles.md](docs/principles.md) | Design choices, core primitives, and non-goals |
|
|
140
|
+
| [docs/cli.md](docs/cli.md) | CLI reference |
|
|
141
|
+
| [docs/embedding.md](docs/embedding.md) | Use FastAgent as a library inside your own app |
|
|
142
|
+
| [docs/channels.md](docs/channels.md) | Add webhook/bot channels |
|
|
143
|
+
| [docs/deploy.md](docs/deploy.md) | Ship the directory to Fly, Railway, or any Docker host |
|
|
144
|
+
| [docs/github.md](docs/github.md) / [docs/telegram.md](docs/telegram.md) | First-party channel guides |
|
|
145
|
+
| [docs/channel-development.md](docs/channel-development.md) | Build custom channel adapters |
|
|
146
|
+
| [docs/api-reference.md](docs/api-reference.md) | Public TypeScript API reference |
|
|
147
|
+
| [docs/troubleshooting.md](docs/troubleshooting.md) | Common setup/runtime issues |
|
|
148
|
+
| [docs/SPEC.md](docs/SPEC.md) | Agent Handler protocol v0.1 |
|
|
149
|
+
| [docs/design/core.md](docs/design/core.md) | Maintainer architecture notes |
|
|
150
|
+
|
|
151
|
+
## Public API surface & stability
|
|
152
|
+
|
|
153
|
+
The root export intentionally contains the supported surface only.
|
|
154
|
+
|
|
155
|
+
| Area | Examples | Stability |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| Contract | `Agent`, `AgentEvent`, `collect` | Stable within SPEC v0.1 |
|
|
158
|
+
| Channels/host | `createInvokeHandler`, `nodeListener`, `serveNode`, `router`, `Routes` | Reference implementation, pre-1.0 |
|
|
159
|
+
| pi assembly | `createPiAgentFromWorkspace`, `createPiAgentFromDefinition`, `createPiAgent` | Usable now, may tighten before 1.0 |
|
|
160
|
+
| Tool/channel authoring | `defineTool`, `z`, `loadTools`, `loadChannels`, `ChannelModule` | Usable now, may tighten before 1.0 |
|
|
161
|
+
| Injection ports | `PiSessionStore`, `inMemorySessionStore`, `jsonlSessionStore`, `Lease`, `Provider`, `createProvider` | Public because options reference them |
|
|
162
|
+
| Not exported | L0 harness adapter, pi harness factory, prompt/config internals | Internal modules; no compatibility promise |
|
|
163
|
+
|
|
164
|
+
Subpath exports:
|
|
165
|
+
|
|
166
|
+
- `@fastagent-sh/fastagent/core` — engine-neutral contract, consumption helpers, channel/host kit, schedules;
|
|
167
|
+
- `@fastagent-sh/fastagent/pi` — the pi reference implementation;
|
|
168
|
+
- `@fastagent-sh/fastagent/github` — GitHub webhook channel;
|
|
169
|
+
- `@fastagent-sh/fastagent/telegram` — Telegram bot channel.
|
|
170
|
+
|
|
171
|
+
## Repository layout
|
|
172
|
+
|
|
173
|
+
```txt
|
|
174
|
+
src/ the npm package: CLI, library API, reference implementation
|
|
175
|
+
test/ vitest suite (faux models by default) + reusable SPEC conformance
|
|
176
|
+
docs/ user docs, SPEC, and maintainer design notes
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Single package, likely long-term; subpath exports (not sibling packages) are the module boundary.
|
|
180
|
+
A `packages/` workspace split is deliberately deferred until a second published artifact with
|
|
181
|
+
independent dependencies/versioning actually exists.
|
|
182
|
+
|
|
183
|
+
## Status
|
|
184
|
+
|
|
185
|
+
FastAgent is pre-1.0. The stable design center is the Agent Handler contract in `docs/SPEC.md`; the package API may still tighten before 1.0. Notable changes are recorded in the [GitHub Releases](https://github.com/fastagent-sh/fastagent/releases).
|
|
186
|
+
|
|
187
|
+
## Designed for more
|
|
188
|
+
|
|
189
|
+
The neutral contract leaves room for capabilities that are not complete product features yet:
|
|
190
|
+
|
|
191
|
+
- **Durable execution** — Telegram accepted turns replay at least once today; general durability and exactly-once execution remain future backend work.
|
|
192
|
+
- **Sandboxed execution** — `ExecutionEnv` is an assembly seam, but the pi coding tools and project-context loader are still local; a complete sandbox adapter is future work.
|
|
193
|
+
- **Observability export** — leveled logs and per-turn traces exist today; an OpenTelemetry exporter does not.
|
|
194
|
+
- **More reference bindings and channels** — pi is the reference implementation; another engine can implement the Agent contract, and community channels can use the channel kit.
|
|
195
|
+
- **More deploy targets** — Fly and Railway ship today; the generated container is the portable path for other hosts.
|
|
196
|
+
|
|
197
|
+
See [Contributing](CONTRIBUTING.md) if one of these is the problem you want to work on.
|
|
198
|
+
|
|
199
|
+
## Project
|
|
200
|
+
|
|
201
|
+
- [Contributing](CONTRIBUTING.md)
|
|
202
|
+
- [Security policy](SECURITY.md)
|
|
203
|
+
|
|
204
|
+
## Acknowledgements
|
|
205
|
+
|
|
206
|
+
FastAgent stands on open source. The reference implementation is built on **[pi](https://github.com/earendil-works/pi)** ([pi.dev](https://pi.dev)) — its agent harness, multi-provider LLM API, and the interactive TUI that `fastagent chat` drives.
|
|
207
|
+
|
|
208
|
+
It also depends on, and is grateful to, [zod](https://github.com/colinhacks/zod), [undici](https://github.com/nodejs/undici), [chokidar](https://github.com/paulmillr/chokidar), [giget](https://github.com/unjs/giget), [@clack/prompts](https://github.com/bombshell-dev/clack), [ignore](https://github.com/kaelzhang/node-ignore), and [octokit/webhooks](https://github.com/octokit/webhooks).
|
|
209
|
+
|
|
210
|
+
The scaffolded `writing-great-skills` skill is vendored from [mattpocock/skills](https://github.com/mattpocock/skills), with its license included.
|
|
211
|
+
|
|
212
|
+
## License
|
|
213
|
+
|
|
214
|
+
[MIT](LICENSE). Runtime dependencies use permissive open-source licenses and are installed as separate npm packages; the vendored `writing-great-skills` scaffold includes its own license.
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Handler protocol v0.1 — the engine-neutral contract (docs/SPEC.md). Pure types, zero
|
|
3
|
+
* dependencies. Importing any engine implementation here is forbidden (`@earendil-works/pi-*` may
|
|
4
|
+
* only appear under engines/).
|
|
5
|
+
*/
|
|
6
|
+
export type Json = null | boolean | number | string | Json[] | {
|
|
7
|
+
[k: string]: Json;
|
|
8
|
+
};
|
|
9
|
+
/** Base64-encoded image reference. */
|
|
10
|
+
export interface ImageRef {
|
|
11
|
+
mimeType: string;
|
|
12
|
+
data: string;
|
|
13
|
+
}
|
|
14
|
+
export interface Prompt {
|
|
15
|
+
text: string;
|
|
16
|
+
images?: ImageRef[];
|
|
17
|
+
}
|
|
18
|
+
/** Invocation scope. Core keeps only the `session` anchor; other fields are extensions (SPEC §8). */
|
|
19
|
+
export interface Scope {
|
|
20
|
+
/** Opaque session anchor: turns of the same logical conversation MUST reuse the same value. */
|
|
21
|
+
session: string;
|
|
22
|
+
}
|
|
23
|
+
export type AgentEvent = {
|
|
24
|
+
type: "text";
|
|
25
|
+
delta: string;
|
|
26
|
+
}
|
|
27
|
+
/** Model reasoning, streamed live. Process, NOT the answer: consumers MUST NOT fold it into the final text. */
|
|
28
|
+
| {
|
|
29
|
+
type: "thinking";
|
|
30
|
+
delta: string;
|
|
31
|
+
} | {
|
|
32
|
+
type: "tool_started";
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
args: Json;
|
|
36
|
+
} | {
|
|
37
|
+
type: "tool_ended";
|
|
38
|
+
id: string;
|
|
39
|
+
isError: boolean;
|
|
40
|
+
content: Json;
|
|
41
|
+
}
|
|
42
|
+
/** Terminal: success. `data` is attached only when the engine produces a structured result. */
|
|
43
|
+
| {
|
|
44
|
+
type: "completed";
|
|
45
|
+
data?: Json;
|
|
46
|
+
}
|
|
47
|
+
/** Terminal: failure. `retryable` means it is worth re-sending with the same session. `code` is the
|
|
48
|
+
* optional machine-readable failure subdivision (SPEC §8) — a stable discriminator a consumer can branch
|
|
49
|
+
* on without parsing `details` (human-facing prose). */
|
|
50
|
+
| {
|
|
51
|
+
type: "failed";
|
|
52
|
+
details: string;
|
|
53
|
+
retryable: boolean;
|
|
54
|
+
code?: string;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* The `failed.code` (SPEC §8 failure subdivision) the reference engine sets when a turn is rejected because
|
|
58
|
+
* the session is BUSY — another turn is already in flight, so THIS one never started and is replay-safe.
|
|
59
|
+
* It lives in the contract (a code VALUE, not an engine import) so a neutral consumer — the scheduler,
|
|
60
|
+
* which re-fires a busy wake-up but not one whose turn may have run side effects — branches on it without
|
|
61
|
+
* text-matching `details` or reaching into the engine. An internal fastagent seam (engine ↔ scheduler),
|
|
62
|
+
* not a public cross-engine mandate.
|
|
63
|
+
*/
|
|
64
|
+
export declare const SESSION_BUSY_CODE = "session_busy";
|
|
65
|
+
/**
|
|
66
|
+
* One turn = one invoke, returning a single async event stream. The stream MUST terminate with
|
|
67
|
+
* exactly one of completed / failed, or be cancelled by the caller (no terminal event). Any
|
|
68
|
+
* AsyncIterable producer that implements this conforms (interface, not base class).
|
|
69
|
+
*/
|
|
70
|
+
export interface Agent {
|
|
71
|
+
invoke(scope: Scope, prompt: Prompt): AsyncIterable<AgentEvent>;
|
|
72
|
+
}
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Handler protocol v0.1 — the engine-neutral contract (docs/SPEC.md). Pure types, zero
|
|
3
|
+
* dependencies. Importing any engine implementation here is forbidden (`@earendil-works/pi-*` may
|
|
4
|
+
* only appear under engines/).
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* The `failed.code` (SPEC §8 failure subdivision) the reference engine sets when a turn is rejected because
|
|
8
|
+
* the session is BUSY — another turn is already in flight, so THIS one never started and is replay-safe.
|
|
9
|
+
* It lives in the contract (a code VALUE, not an engine import) so a neutral consumer — the scheduler,
|
|
10
|
+
* which re-fires a busy wake-up but not one whose turn may have run side effects — branches on it without
|
|
11
|
+
* text-matching `details` or reaching into the engine. An internal fastagent seam (engine ↔ scheduler),
|
|
12
|
+
* not a public cross-engine mandate.
|
|
13
|
+
*/
|
|
14
|
+
export const SESSION_BUSY_CODE = "session_busy";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a request body with a hard byte cap (real bytes). A streaming cap is the only robust guard
|
|
3
|
+
* against an unbounded body (Content-Length is bypassable with chunked encoding). Web-streams only.
|
|
4
|
+
*/
|
|
5
|
+
export declare function readBodyCapped(req: Request, max: number): Promise<{
|
|
6
|
+
text: string;
|
|
7
|
+
} | {
|
|
8
|
+
tooLarge: true;
|
|
9
|
+
}>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a request body with a hard byte cap (real bytes). A streaming cap is the only robust guard
|
|
3
|
+
* against an unbounded body (Content-Length is bypassable with chunked encoding). Web-streams only.
|
|
4
|
+
*/
|
|
5
|
+
export async function readBodyCapped(req, max) {
|
|
6
|
+
if (!req.body)
|
|
7
|
+
return { text: "" };
|
|
8
|
+
const reader = req.body.getReader();
|
|
9
|
+
const chunks = [];
|
|
10
|
+
let received = 0;
|
|
11
|
+
for (;;) {
|
|
12
|
+
const { done, value } = await reader.read();
|
|
13
|
+
if (done)
|
|
14
|
+
break;
|
|
15
|
+
received += value.byteLength;
|
|
16
|
+
if (received > max) {
|
|
17
|
+
await reader.cancel();
|
|
18
|
+
return { tooLarge: true };
|
|
19
|
+
}
|
|
20
|
+
chunks.push(value);
|
|
21
|
+
}
|
|
22
|
+
const buf = new Uint8Array(received);
|
|
23
|
+
let offset = 0;
|
|
24
|
+
for (const c of chunks) {
|
|
25
|
+
buf.set(c, offset);
|
|
26
|
+
offset += c.byteLength;
|
|
27
|
+
}
|
|
28
|
+
return { text: new TextDecoder().decode(buf) };
|
|
29
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Schema } from "@octokit/webhooks-types";
|
|
2
|
+
import type { ChannelModule } from "../../host/node.ts";
|
|
3
|
+
/** A verified GitHub webhook event. Header fields plus the official typed payload. */
|
|
4
|
+
export interface GithubEvent {
|
|
5
|
+
/** `X-GitHub-Event` (e.g. "pull_request", "issue_comment"). */
|
|
6
|
+
event: string;
|
|
7
|
+
/** `payload.action` (e.g. "opened"), when present — the usual routing discriminant. */
|
|
8
|
+
action?: string;
|
|
9
|
+
/** `X-GitHub-Delivery` — unique per delivery. */
|
|
10
|
+
deliveryId: string;
|
|
11
|
+
/** The native payload (union of all events); narrow it, e.g. `if ("pull_request" in event.payload)`. */
|
|
12
|
+
payload: Schema;
|
|
13
|
+
}
|
|
14
|
+
/** What `on` returns per acted-on delivery: a session + the prompt text for the agent turn. */
|
|
15
|
+
export interface Intent {
|
|
16
|
+
session: string;
|
|
17
|
+
text: string;
|
|
18
|
+
}
|
|
19
|
+
export interface GithubChannelOptions {
|
|
20
|
+
/** Webhook secret — verifies inbound deliveries (HMAC-SHA256 over the raw body). */
|
|
21
|
+
secret: string;
|
|
22
|
+
/** Map a verified event to the intents this agent acts on (empty array = ignore). */
|
|
23
|
+
on: (event: GithubEvent) => Intent[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Build a GitHub webhook channel: policy options in, a {@link ChannelModule} out (mounts
|
|
27
|
+
* `POST /webhook`). `agent` arrives via the mount context. The adapter owns that route key; to serve
|
|
28
|
+
* it elsewhere, re-key the returned module:
|
|
29
|
+
* `(ctx) => ({ "POST /gh": githubChannel(opts)(ctx)["POST /webhook"]! })`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function githubChannel({ secret, on }: GithubChannelOptions): ChannelModule;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub webhook channel: verify → route via `on(event)` → fire-and-forget agent turns, ACK 202.
|
|
3
|
+
* The developer writes only `on`. Concurrency safety is the engine's per-session lease. A turn that
|
|
4
|
+
* fails after the 202, or an in-flight turn on shutdown, is lost (server log only).
|
|
5
|
+
*/
|
|
6
|
+
import { verify } from "@octokit/webhooks-methods";
|
|
7
|
+
import { collect } from "../../collect.js";
|
|
8
|
+
import { log } from "../../log.js";
|
|
9
|
+
import { readBodyCapped } from "../body.js";
|
|
10
|
+
import { text } from "../respond.js";
|
|
11
|
+
/** Raw body cap before verification — GitHub caps webhook payloads at 25 MB; reject larger early. */
|
|
12
|
+
const MAX_WEBHOOK_BYTES = 25 << 20;
|
|
13
|
+
/**
|
|
14
|
+
* Build a GitHub webhook channel: policy options in, a {@link ChannelModule} out (mounts
|
|
15
|
+
* `POST /webhook`). `agent` arrives via the mount context. The adapter owns that route key; to serve
|
|
16
|
+
* it elsewhere, re-key the returned module:
|
|
17
|
+
* `(ctx) => ({ "POST /gh": githubChannel(opts)(ctx)["POST /webhook"]! })`.
|
|
18
|
+
*/
|
|
19
|
+
export function githubChannel({ secret, on }) {
|
|
20
|
+
// A non-empty secret is mandatory: verify() against an empty key accepts a signature anyone can
|
|
21
|
+
// compute, so an unset secret must fail at construction, never silently run forgeable.
|
|
22
|
+
if (!secret) {
|
|
23
|
+
throw new Error("githubChannel requires a non-empty secret (the GitHub webhook secret, e.g. GITHUB_WEBHOOK_SECRET)");
|
|
24
|
+
}
|
|
25
|
+
return ({ agent }) => ({
|
|
26
|
+
"POST /webhook": async (req) => {
|
|
27
|
+
if (req.method !== "POST")
|
|
28
|
+
return text("POST only\n", 405);
|
|
29
|
+
// Cap before verify: a public endpoint must not buffer an unbounded body (chunked bypasses Content-Length).
|
|
30
|
+
const body = await readBodyCapped(req, MAX_WEBHOOK_BYTES);
|
|
31
|
+
if ("tooLarge" in body)
|
|
32
|
+
return text("payload too large\n", 413);
|
|
33
|
+
const raw = body.text;
|
|
34
|
+
// Fail closed: verify() throws on an empty/missing arg, so treat any verify error as a clean 401.
|
|
35
|
+
const signature = req.headers.get("x-hub-signature-256");
|
|
36
|
+
if (!signature || !(await verify(secret, raw, signature).catch(() => false))) {
|
|
37
|
+
return text("invalid signature\n", 401);
|
|
38
|
+
}
|
|
39
|
+
const eventName = req.headers.get("x-github-event") ?? "";
|
|
40
|
+
if (eventName === "ping")
|
|
41
|
+
return new Response(null, { status: 204 });
|
|
42
|
+
// x-www-form-urlencoded (GitHub's UI default) wraps the JSON in a `payload` field; json is the body.
|
|
43
|
+
let json = raw;
|
|
44
|
+
if ((req.headers.get("content-type") ?? "").includes("application/x-www-form-urlencoded")) {
|
|
45
|
+
const field = new URLSearchParams(raw).get("payload");
|
|
46
|
+
if (field === null)
|
|
47
|
+
return text("missing form payload\n", 400);
|
|
48
|
+
json = field;
|
|
49
|
+
}
|
|
50
|
+
let payload;
|
|
51
|
+
try {
|
|
52
|
+
payload = JSON.parse(json);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return text("invalid json\n", 400);
|
|
56
|
+
}
|
|
57
|
+
const event = {
|
|
58
|
+
event: eventName,
|
|
59
|
+
action: typeof payload.action === "string" ? payload.action : undefined,
|
|
60
|
+
deliveryId: req.headers.get("x-github-delivery") ?? "",
|
|
61
|
+
payload: payload, // trust boundary: the verified body is a GitHub event
|
|
62
|
+
};
|
|
63
|
+
// Fire each turn, return 202; the process runs them to completion. The lifecycle is logged to
|
|
64
|
+
// stderr — after the 202 there is no response body, so these lines are the operator's only signal
|
|
65
|
+
// (and the sink that keeps a post-ACK error from going unhandled).
|
|
66
|
+
const intents = on(event);
|
|
67
|
+
const label = event.action ? `${event.event}.${event.action}` : event.event;
|
|
68
|
+
for (let i = 0; i < intents.length; i++) {
|
|
69
|
+
const { session, text } = intents[i];
|
|
70
|
+
// Per-turn correlation id (deliveryId is unique per webhook; the index disambiguates fan-out),
|
|
71
|
+
// threaded through start/done/failed so a terminal line joins back to its start.
|
|
72
|
+
const turn = `${event.deliveryId}#${i}`;
|
|
73
|
+
log.info(`[github] turn start: turn=${turn} session=${session} event=${label}`);
|
|
74
|
+
const startedAt = Date.now();
|
|
75
|
+
void collect(agent.invoke({ session }, { text })).then(() => log.info(`[github] turn done: turn=${turn} session=${session} (${Date.now() - startedAt}ms)`), (error) => log.error(`[github] turn failed: turn=${turn} session=${session} (${Date.now() - startedAt}ms): ${String(error)}`));
|
|
76
|
+
}
|
|
77
|
+
return new Response(null, { status: 202 });
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { githubChannel } from "@fastagent-sh/fastagent/github";
|
|
2
|
+
|
|
3
|
+
// A channel = a third-party ADAPTER (githubChannel: verify + parse + ACK) configured with YOUR on() policy.
|
|
4
|
+
// fastagent discovers this file under channels/, mounts POST /webhook, and pipes the agent to the
|
|
5
|
+
// adapter — this file holds only policy. Set GITHUB_WEBHOOK_SECRET in .env (a missing secret fails at
|
|
6
|
+
// startup — an empty key would accept forged deliveries) and point a GitHub webhook (JSON) at POST /webhook.
|
|
7
|
+
export default githubChannel({
|
|
8
|
+
secret: process.env.GITHUB_WEBHOOK_SECRET ?? "",
|
|
9
|
+
// Map a verified event to the intents the agent acts on (empty array = ignore). Each review is
|
|
10
|
+
// INDEPENDENT and idempotent (it reconciles against the PR's existing comments), so use a
|
|
11
|
+
// distinct per-delivery session (event.deliveryId): overlapping deliveries then run on their
|
|
12
|
+
// own session without a shared-lease drop.
|
|
13
|
+
on: (event) => {
|
|
14
|
+
if (event.event === "pull_request" && event.action === "opened" && "pull_request" in event.payload) {
|
|
15
|
+
const { repository, pull_request } = event.payload;
|
|
16
|
+
return [
|
|
17
|
+
{
|
|
18
|
+
session: event.deliveryId,
|
|
19
|
+
text: `Review pull request #${pull_request.number} in ${repository.full_name}.`,
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
return [];
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { Agent } from "../agent.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Fetch-shaped invoke handler. Mount it at any route in the host app; it accepts POST only.
|
|
5
|
+
* Returns SSE (`text/event-stream`) with one `data:` line per AgentEvent.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createInvokeHandler(agent: Agent): (req: Request) => Promise<Response>;
|
|
8
|
+
/**
|
|
9
|
+
* node:http adapter for a Fetch handler. Bridges IncomingMessage → Request and pumps the
|
|
10
|
+
* Response body back to ServerResponse with backpressure; a client disconnect (`res` close)
|
|
11
|
+
* cancels both the request signal and the response stream (→ invoke cancellation).
|
|
12
|
+
*/
|
|
13
|
+
export declare function nodeListener(handler: (req: Request) => Promise<Response>): (req: IncomingMessage, res: ServerResponse) => void;
|