@pmndrs/pine 0.0.0-stage → 0.1.1
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 +180 -2
- package/bin/pine.js +27 -0
- package/docs/SKILL.md +189 -0
- package/docs/references/agents.md +368 -0
- package/docs/references/coding.md +126 -0
- package/docs/references/configuration.md +229 -0
- package/docs/references/events.md +263 -0
- package/docs/references/mcp.md +257 -0
- package/docs/references/operations.md +280 -0
- package/docs/references/overview.md +186 -0
- package/docs/references/security.md +489 -0
- package/docs/references/setup.md +181 -0
- package/docs/references/sources.md +768 -0
- package/docs/references/stack.md +105 -0
- package/docs/references/tui.md +348 -0
- package/examples/home/.env.example +5 -0
- package/examples/home/USER.md +4 -0
- package/examples/home/agents/main/agent.json +4 -0
- package/examples/home/agents/main/workspace/AGENTS.md +14 -0
- package/examples/home/agents/main/workspace/MEMORY.md +4 -0
- package/examples/home/agents/main/workspace/SOUL.md +4 -0
- package/examples/home/models.json +62 -0
- package/examples/home/pine.json +30 -0
- package/examples/home/skills/README.txt +7 -0
- package/examples/launchd/com.pine.daemon.plist +32 -0
- package/mcp/browser.d.ts +34 -0
- package/mcp/browser.js +228 -0
- package/mcp/commands.d.ts +20 -0
- package/mcp/commands.js +88 -0
- package/mcp/lib.d.ts +13 -0
- package/mcp/lib.js +50 -0
- package/mcp/web.d.ts +6 -0
- package/mcp/web.js +140 -0
- package/package.json +32 -4
- package/src/agent-admin.d.ts +24 -0
- package/src/agent-admin.js +55 -0
- package/src/agents.d.ts +343 -0
- package/src/agents.js +1587 -0
- package/src/cli.d.ts +2 -0
- package/src/cli.js +526 -0
- package/src/config.d.ts +351 -0
- package/src/config.js +514 -0
- package/src/dev/claude-model.d.ts +28 -0
- package/src/dev/claude-model.js +174 -0
- package/src/env-file.d.ts +4 -0
- package/src/env-file.js +40 -0
- package/src/http.d.ts +46 -0
- package/src/http.js +407 -0
- package/src/limiter.d.ts +32 -0
- package/src/limiter.js +84 -0
- package/src/login.d.ts +21 -0
- package/src/login.js +126 -0
- package/src/manual.d.ts +61 -0
- package/src/manual.js +425 -0
- package/src/mcp-config.d.ts +40 -0
- package/src/mcp-config.js +132 -0
- package/src/net.d.ts +36 -0
- package/src/net.js +122 -0
- package/src/pine.d.ts +32 -0
- package/src/pine.js +142 -0
- package/src/router.d.ts +56 -0
- package/src/router.js +164 -0
- package/src/sandbox-fs.d.ts +27 -0
- package/src/sandbox-fs.js +172 -0
- package/src/security.d.ts +121 -0
- package/src/security.js +363 -0
- package/src/skills.d.ts +67 -0
- package/src/skills.js +163 -0
- package/src/source-types.d.ts +61 -0
- package/src/source-types.js +1 -0
- package/src/sources/command.d.ts +4 -0
- package/src/sources/command.js +77 -0
- package/src/sources/folder.d.ts +6 -0
- package/src/sources/folder.js +116 -0
- package/src/sources/ftp.d.ts +4 -0
- package/src/sources/ftp.js +307 -0
- package/src/sources/http.d.ts +4 -0
- package/src/sources/http.js +201 -0
- package/src/sources/mqtt.d.ts +18 -0
- package/src/sources/mqtt.js +97 -0
- package/src/sources/telegram.d.ts +22 -0
- package/src/sources/telegram.js +269 -0
- package/src/sources/webhook.d.ts +4 -0
- package/src/sources/webhook.js +52 -0
- package/src/sources.d.ts +339 -0
- package/src/sources.js +955 -0
- package/src/store.d.ts +89 -0
- package/src/store.js +251 -0
- package/src/timers.d.ts +83 -0
- package/src/timers.js +318 -0
- package/src/tools.d.ts +6 -0
- package/src/tools.js +777 -0
- package/src/tui/app.d.ts +209 -0
- package/src/tui/app.js +2223 -0
- package/src/tui/client.d.ts +178 -0
- package/src/tui/client.js +116 -0
- package/src/tui/code-pane.d.ts +51 -0
- package/src/tui/code-pane.js +398 -0
- package/src/tui/highlight.d.ts +8 -0
- package/src/tui/highlight.js +86 -0
- package/src/tui/picture.d.ts +141 -0
- package/src/tui/picture.js +571 -0
- package/src/tui/run.d.ts +8 -0
- package/src/tui/run.js +97 -0
- package/src/tui/theme.d.ts +32 -0
- package/src/tui/theme.js +128 -0
- package/src/types.d.ts +124 -0
- package/src/types.js +1 -0
- package/src/update.d.ts +25 -0
- package/src/update.js +110 -0
- package/src/util.d.ts +32 -0
- package/src/util.js +116 -0
- package/src/watching.d.ts +20 -0
- package/src/watching.js +68 -0
- package/src/workspace-view.d.ts +38 -0
- package/src/workspace-view.js +160 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
Everything pine needs is in its home directory: `~/.pine` by default, or `$PINE_HOME`, or
|
|
4
|
+
`--home <dir>` on the command line.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
~/.pine/
|
|
8
|
+
pine.json global settings, routes, schedules, global MCP servers and event sources
|
|
9
|
+
models.json optional: your own model servers, in pi's format
|
|
10
|
+
.env secrets, referenced from the JSON files as ${NAME}
|
|
11
|
+
auth.json provider credentials, written by `pine login`
|
|
12
|
+
USER.md what all agents know about you; in every agent's prompt, kept by the main agent
|
|
13
|
+
agents/
|
|
14
|
+
main/
|
|
15
|
+
agent.json this agent's settings
|
|
16
|
+
workspace/ its working folder, unless agent.json points elsewhere
|
|
17
|
+
AGENTS.md its purpose and how it works ┐ read into its prompt,
|
|
18
|
+
SOUL.md who it is │ written by the agent itself
|
|
19
|
+
MEMORY.md what should outlive a talk ┘ (agents.md)
|
|
20
|
+
AGENTS.md optional: rules of yours the agent cannot change
|
|
21
|
+
skills/ skills only this agent has
|
|
22
|
+
conversations/ what was set up for one conversation alone (its skills); made when needed
|
|
23
|
+
extensions/ pi extensions only this agent loads
|
|
24
|
+
sessions/ conversation history (written by pine)
|
|
25
|
+
skills/ skills for all agents; no entry needed (the main agent adds them with add_skill)
|
|
26
|
+
sources/ source types the main agent registered (pine's own copies)
|
|
27
|
+
sandbox/ per-agent private home and generated sandbox profiles (written by pine)
|
|
28
|
+
data/ event log, task queue, media, control token, files of event sources (written by pine)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`pine check` validates names, references, paths, models and the entries of sources and
|
|
32
|
+
schedules, prints what each agent ends up with, and lists anything that
|
|
33
|
+
weakens the defences. Problems are reported together, each with the file it is in.
|
|
34
|
+
|
|
35
|
+
Paths in `pine.json` are relative to the home directory; paths in `agent.json` are relative to
|
|
36
|
+
that agent's folder. `~` works in both.
|
|
37
|
+
|
|
38
|
+
## Secrets
|
|
39
|
+
|
|
40
|
+
Write `${NAME}` or `${NAME:-fallback}` anywhere in a string in `pine.json` or `agent.json`.
|
|
41
|
+
Values come from `<home>/.env` first, then from the environment of the daemon. A missing
|
|
42
|
+
variable without a fallback is a configuration error.
|
|
43
|
+
|
|
44
|
+
`.env` is read by pine only. Its values are not exported to the daemon's environment, so
|
|
45
|
+
agent shells and MCP servers do not inherit them; they are also removed from events, messages
|
|
46
|
+
and log lines if they ever appear there (values of 8 characters or more). Keep the file at
|
|
47
|
+
mode 600 (`pine init` sets that).
|
|
48
|
+
|
|
49
|
+
pine writes to `.env` itself in one case: when an agent sets up an event source that has a
|
|
50
|
+
password or secret, the value is stored there as `PINE_SOURCE_<NAME>_<AGENT>_<OPTION>` (without
|
|
51
|
+
the agent for a source of all agents), and the configuration holds only the `${...}` reference.
|
|
52
|
+
Removing the source removes the entry. See [sources.md](sources.md).
|
|
53
|
+
|
|
54
|
+
## pine.json
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"main": "main",
|
|
59
|
+
"logLevel": "info",
|
|
60
|
+
"defaults": { "model": "local/deepseek-v4.1-flash", "thinking": "off" },
|
|
61
|
+
"limits": { "maxConcurrentRuns": 2 },
|
|
62
|
+
"security": { "approvalTimeout": "5m" },
|
|
63
|
+
"http": { "host": "127.0.0.1", "port": 7463, "hooks": {} },
|
|
64
|
+
"mcp": {},
|
|
65
|
+
"sources": {},
|
|
66
|
+
"sourceTypes": {},
|
|
67
|
+
"subscribe": [],
|
|
68
|
+
"extensions": [],
|
|
69
|
+
"skills": [],
|
|
70
|
+
"routes": [],
|
|
71
|
+
"schedules": []
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
| Key | Default | Meaning |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `main` | none | Id of the main agent. It may delegate to every other agent unless its `agent.json` says otherwise, and it is the only one that can create agents and set things up for all agents ([setup.md](setup.md)). Without this key, the agent called `main` (or else the first one) has those setup rights, but nobody delegates by default. |
|
|
78
|
+
| `logLevel` | `info` | `debug`, `info`, `warn`, `error`, `silent`. Logs go to stderr; a daemon started inside the terminal UI logs to `data/pine.log`. |
|
|
79
|
+
| `dataDir` | `data` | Where the database, media and control token live. |
|
|
80
|
+
| `defaults.model` | none | `provider/model-id` from `models.json`: the model of every agent whose `agent.json` names none. There is no built-in fallback: an agent with neither is a configuration error, which `pine check` and the start of pine report. |
|
|
81
|
+
| `defaults.thinking` | `off` | Reasoning level for models that can reason: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. |
|
|
82
|
+
| `defaults.mode` | `auto` | `manual`, `auto` or `bypass`: how much agents do without asking. See [security.md](security.md). |
|
|
83
|
+
| `defaults.bashTimeoutSec` | `300` | Longest a shell command may run. The model cannot raise it. |
|
|
84
|
+
| `defaults.taskTimeoutSec` | `1800` | Longest a task may run before it is aborted. |
|
|
85
|
+
| `defaults.idleMs` | `15m` | A live session without work is closed after this (and resumed from disk later). |
|
|
86
|
+
| `defaults.settings` | `{}` | pi settings applied to every agent, for example `{ "compaction": { "reserveTokens": 8000 } }`. |
|
|
87
|
+
| `limits.maxConcurrentRuns` | `2` | Model slots: how many runs talk to the model server at once. Match it to the server's parallel slots. |
|
|
88
|
+
| `limits.maxDelegationDepth` | `3` | How deep a chain of delegations may go. |
|
|
89
|
+
| `limits.maxDownloadMb` | `200` | Largest file the `download` tool fetches. |
|
|
90
|
+
| `limits.maxEventHops` | `8` | Events further than this from an outside trigger are not routed. |
|
|
91
|
+
| `limits.maxQueuedPerAgent` | `50` | New tasks are refused when an agent has this many waiting. |
|
|
92
|
+
| `limits.maxEventChars` | `20000` | Longest text of an untrusted event in a prompt (at least 1000). Your own messages and delegated tasks are cut at ten times this. |
|
|
93
|
+
| `limits.modelHeadersTimeoutMs` | `10m` | How long the model server may take to start answering. |
|
|
94
|
+
| `limits.modelStreamIdleMs` | `5m` | How long a started answer may go silent before the request is dropped. |
|
|
95
|
+
| `limits.eventRetentionDays` | `30` | Older events are deleted, and so are the copies kept of conversations that were reset. |
|
|
96
|
+
| `limits.conversationDays` | `7` | A conversation in which nothing was said for this long is removed with its history, unless it is an agent's main conversation, pinned, open in a tab, subscribed to an event source, or has a schedule of its own. `0`: never. |
|
|
97
|
+
| `security.approvalTimeout` | `5m` | An unanswered approval request counts as "no" after this. |
|
|
98
|
+
| `security.taint` | `session` | How long a session stays tainted: until reset (`session`), until the task ends (`run`), or never (`off`). See [security.md](security.md). |
|
|
99
|
+
| `security.taintTools` | `[]` | Tool name globs whose results count as outside content, besides MCP servers marked `taints`. |
|
|
100
|
+
| `security.bypassWhenTainted` | `false` | Keep bypass mode in force in a session that has read outside content. Off, such a session asks again. |
|
|
101
|
+
| `http` | on, loopback, port 7463 | Control API and webhooks. `false` turns it off (the CLI's client commands then do not work). |
|
|
102
|
+
| `http.host`, `http.port` | `127.0.0.1`, `7463` | Where the control API listens. Anything but loopback is reported by `pine check`. |
|
|
103
|
+
| `http.token` | generated | Bearer token of the control API. When unset, pine generates one into `data/http-token`. |
|
|
104
|
+
| `http.hooks.<name>` | none | A webhook at `POST /hook/<name>`: `{ "secret": "...", "topic": "hook/<name>" }`, or `"open": true` instead of a secret to accept anyone who can reach the port. See [events.md](events.md). For a webhook an agent subscribes to, use a `webhook` source instead. |
|
|
105
|
+
| `mcp` | `{}` | Global MCP servers. See [mcp.md](mcp.md). |
|
|
106
|
+
| `sources` | `{}` | Event sources every agent can subscribe to. See [sources.md](sources.md). |
|
|
107
|
+
| `sourceTypes` | `{}` | Source types of your own: a name and the module that implements it, e.g. `{ "doorbell": "./sources/doorbell.ts" }`. The module says which options it takes, and agents can then set up sources of that type by tool call. The main agent adds entries here with `add_source_type`, pointing at the copy pine keeps in `sources/` of its home. |
|
|
108
|
+
| `subscribe` | `[]` | Subscriptions for all agents: every agent that can use the source is woken in its main conversation. |
|
|
109
|
+
| `extensions` | `[]` | Paths of pi extensions loaded into every agent's sessions. |
|
|
110
|
+
| `skills` | `[]` | Further folders of skills for every agent, for skills that lie outside pine's home. Not needed for `skills/` in pine's home: what is there is for every agent without an entry, and so is what the main agent adds with `add_skill`. Agents cannot replace or remove a skill that is named here. |
|
|
111
|
+
| `routes` | `[]` | See [events.md](events.md). |
|
|
112
|
+
| `schedules` | `[]` | What happens at a time. One with a `task` wakes every agent with it; one without only publishes an event. The main agent adds entries with `add_schedule`. See [events.md](events.md). |
|
|
113
|
+
|
|
114
|
+
Two things that older setups had are configuration errors now, each reported with what to
|
|
115
|
+
write instead: a `plugins` list that is not empty, and an event source of `"type": "timer"`
|
|
116
|
+
(that is a schedule).
|
|
117
|
+
|
|
118
|
+
Durations are a number of milliseconds or a string such as `"90s"`, `"15m"`, `"2h"`, `"1d"`.
|
|
119
|
+
|
|
120
|
+
## agent.json
|
|
121
|
+
|
|
122
|
+
Every key is optional; `{}` is a valid agent.
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"description": "Contract law; knows the case files",
|
|
127
|
+
"workspace": "~/dev/gonzo",
|
|
128
|
+
"model": "local/deepseek-v4.1-flash",
|
|
129
|
+
"tools": ["read", "bash", "edit", "write", "grep", "find", "ls"],
|
|
130
|
+
"mcp": { "library": { "command": "library-mcp" } },
|
|
131
|
+
"globalMcp": ["web"],
|
|
132
|
+
"delegates": [],
|
|
133
|
+
"subagents": { "maxParallel": 3 },
|
|
134
|
+
"sandbox": { "network": false, "read": ["~/Documents/law-library"] }
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Identity and model
|
|
139
|
+
|
|
140
|
+
| Key | Default | Meaning |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| `description` | empty | One line other agents see when they choose whom to delegate to. Write it for them. |
|
|
143
|
+
| `workspace` | `agents/<id>/workspace` | The agent's working folder. `AGENTS.md`, `SOUL.md` and `MEMORY.md` at its top are the agent's instructions and are loaded into its prompt. Must exist when you set it. |
|
|
144
|
+
| `context` | `[]` | Further instruction files to load into the system prompt. |
|
|
145
|
+
| `model`, `thinking` | from `defaults` | Per-agent model and thinking level. |
|
|
146
|
+
| `settings` | from `defaults.settings` | pi settings for this agent. `agents/<id>/settings.json` is honoured as well. |
|
|
147
|
+
| `taskTimeoutSec`, `idleMs` | from `defaults` | Per-agent overrides. |
|
|
148
|
+
|
|
149
|
+
### Tools and extensions
|
|
150
|
+
|
|
151
|
+
| Key | Default | Meaning |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| `tools` | `read`, `bash`, `edit`, `write` | Which of pi's built-in tools the agent gets: any of `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`. |
|
|
154
|
+
| `mcp` | `{}` | MCP servers only this agent has. An entry with the name of a global server replaces it for this agent. An entry with `"session": "<conversation>"` was added for that conversation alone and is removed with it ([setup.md](setup.md)). |
|
|
155
|
+
| `globalMcp` | `true` | `true`: every global server that includes this agent. `false`: none. A list of names: exactly those. |
|
|
156
|
+
| `sources` | `{}` | Event sources only this agent can subscribe to. An entry with `"session": "<conversation>"` was set up for that conversation alone and is removed with it. See [sources.md](sources.md). |
|
|
157
|
+
| `subscribe` | `[]` | What this agent is subscribed to: a list of names, an object with `note`, `when`, `throttle`, `debounce` per source, or a list of entries with `source` and, for one conversation only, `session`. The agent's `subscribe` tool and `pine subscribe` write here. |
|
|
158
|
+
| `schedules` | `[]` | What this agent does at set times: `{ "name", "cron" or "every" or "at", "task" }`, with `"session": "<conversation>"` for one conversation. The agent adds entries with `add_schedule`. See [events.md](events.md). |
|
|
159
|
+
| `extensions`, `skills` | `[]` | Further pi extension files and skill folders for this agent. Its own `skills/` folder needs no entry. |
|
|
160
|
+
| `bashTimeoutSec` | from `defaults` | Longest shell command. |
|
|
161
|
+
| `bashDeny` | `[]` | Regular expressions; a shell command matching one is refused. A convenience, not a boundary: the sandbox is the boundary. |
|
|
162
|
+
|
|
163
|
+
### Working with other agents
|
|
164
|
+
|
|
165
|
+
| Key | Default | Meaning |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| `delegates` | `["*"]` for the agent that `main` in `pine.json` names, else `[]` | Agents this one may hand tasks to. |
|
|
168
|
+
| `subagents` | on, 4 in parallel | `false`, or `{ "maxParallel": 4, "tools": [...], "mcp": true }`. `tools` narrows what sub-agents get. |
|
|
169
|
+
| `publish` | `[]` | Topic patterns the `publish_event` tool may publish on. Empty: the agent has no such tool. |
|
|
170
|
+
| `state.read` | `["#"]` | State keys the agent may read. |
|
|
171
|
+
| `state.write` | `["agent/<id>/#"]` | State keys the agent may write. |
|
|
172
|
+
| `state.inject` | `[]` | State keys shown to the agent in every turn. |
|
|
173
|
+
|
|
174
|
+
### Containment
|
|
175
|
+
|
|
176
|
+
| Key | Default | Meaning |
|
|
177
|
+
|---|---|---|
|
|
178
|
+
| `mode` | from `defaults` | `manual`: every action waits for approval. `auto`: works inside the sandbox and asks for the rest. `bypass`: never asks, and reaches your whole account. |
|
|
179
|
+
| `sandbox` | on | `false` runs shell and file tools with the daemon's full rights. Do not, unless the machine itself is disposable. |
|
|
180
|
+
| `sandbox.network` | `false` | Shell commands may use the network. |
|
|
181
|
+
| `sandbox.read`, `sandbox.write` | `[]` | Further folders the agent may read / write, besides its workspace. |
|
|
182
|
+
| `sandbox.sharedTmp` | `true` | Allow `/tmp` and the user's temp folder. |
|
|
183
|
+
| `sandbox.env` | `{}` | Extra environment variables for the agent's shell. Do not put secrets here; the agent can read them. |
|
|
184
|
+
| `trustWorkspace` | `false` | `true` lets pi load `.pi/extensions`, `.pi/settings.json` and skills from the workspace. That is code execution inside the daemon for whoever can write there, the agent included. |
|
|
185
|
+
| `approve` | `[]` | Tool calls that always need your approval: `"tool"` or `"tool:argument-glob"`, for example `"bash:*git push*"`. |
|
|
186
|
+
| `approveWhenTainted` | `["publish_event", "send"]` | Tool calls that need approval once the session has seen outside content. |
|
|
187
|
+
| `untrusted` | read-only profile | What a run started by an untrusted event may use: `{ "tools": ["read","grep","find","ls"], "mcp": [], "delegates": [], "publish": [], "stateWrite": false, "subagents": false }`. `false` refuses such runs. |
|
|
188
|
+
|
|
189
|
+
## models.json
|
|
190
|
+
|
|
191
|
+
This is pi's own file; pine passes it through. It is only needed for model servers of your own:
|
|
192
|
+
pi already knows the hosted providers (OpenCode, Anthropic, OpenAI and many more), and
|
|
193
|
+
`pine login <provider>` supplies their credentials. `pine init` writes two providers: `dev`
|
|
194
|
+
(Claude through `claude -p`, served by `pine dev-model`; the example's `defaults.model` is
|
|
195
|
+
`dev/claude-haiku`) and `local` for llama.cpp. The `local` entry:
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
{
|
|
199
|
+
"providers": {
|
|
200
|
+
"local": {
|
|
201
|
+
"name": "llama-server",
|
|
202
|
+
"baseUrl": "http://127.0.0.1:8080/v1",
|
|
203
|
+
"api": "openai-completions",
|
|
204
|
+
"apiKey": "none",
|
|
205
|
+
"models": [
|
|
206
|
+
{
|
|
207
|
+
"id": "deepseek-v4.1-flash",
|
|
208
|
+
"contextWindow": 65536,
|
|
209
|
+
"maxTokens": 8192,
|
|
210
|
+
"input": ["text"],
|
|
211
|
+
"reasoning": true,
|
|
212
|
+
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
|
|
213
|
+
"compat": { "supportsStore": false, "supportsDeveloperRole": false, "maxTokensField": "max_tokens", "thinkingFormat": "deepseek" }
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
- Refer to a model as `provider/id`, here `local/deepseek-v4.1-flash`.
|
|
222
|
+
- `contextWindow` must match what the server was started with (`-c`), or compaction triggers at
|
|
223
|
+
the wrong time.
|
|
224
|
+
- Add `"image"` to `input` only for a model that accepts images; agents then receive the
|
|
225
|
+
pictures that Telegram and watched folders deliver.
|
|
226
|
+
- The `compat` flags stop pi from sending fields a plain llama-server may reject. They have not
|
|
227
|
+
been verified against a real llama-server yet; see [stack.md](stack.md).
|
|
228
|
+
- pi's documentation for this file is in
|
|
229
|
+
`node_modules/@earendil-works/pi-coding-agent/docs/models-configuration.md`.
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# Events, routes and schedules
|
|
2
|
+
|
|
3
|
+
> To let an agent react to a folder, a device or a web request, you usually do not need this
|
|
4
|
+
> page: ask it, or configure a [source](sources.md) and have the agent subscribe. To have it do
|
|
5
|
+
> something at a time, ask it too: see "Schedules" below. Routes are for rules of your own:
|
|
6
|
+
> setting state, chaining events, single model calls.
|
|
7
|
+
|
|
8
|
+
## Topics
|
|
9
|
+
|
|
10
|
+
A topic is a slash-separated name. Patterns use `+` for exactly one level and `#` for the rest:
|
|
11
|
+
|
|
12
|
+
| Pattern | Matches |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `cam/front/motion` | only that topic |
|
|
15
|
+
| `cam/+/motion` | `cam/front/motion`, `cam/back/motion` |
|
|
16
|
+
| `source/#` | everything below `source`, and `source` itself |
|
|
17
|
+
| `#` | everything |
|
|
18
|
+
|
|
19
|
+
Where events come from:
|
|
20
|
+
|
|
21
|
+
| Source | Topics | Trust |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| Webhook `POST /hook/<name>` | `hook/<name>` (or the hook's `topic`) | untrusted |
|
|
24
|
+
| Event sources ([sources.md](sources.md)) | `source/<name>/<event>`, or `source/<name>@<agent>/<event>` for an agent's own | untrusted; owner for a source whose entry says `"trust": "owner"` (`add_source` writes that for `telegram`, whose account you pair) |
|
|
25
|
+
| `pine emit`, `POST /events` | whatever you name | owner |
|
|
26
|
+
| Schedules | `timer/<name>`, or `timer/<name>@<agent>` for an agent's own | owner |
|
|
27
|
+
| Agents (`publish_event`) | topics in the agent's `publish` list | owner, or untrusted if the run was restricted |
|
|
28
|
+
| pine | `agent/...`, `state/...`, `approval/...`, `pine/...` | untrusted |
|
|
29
|
+
|
|
30
|
+
Watch them:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pine events -f
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pine events --topic "source/#" --limit 50
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Routes
|
|
41
|
+
|
|
42
|
+
A route is an entry in `routes` in `pine.json`.
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"name": "front-door",
|
|
47
|
+
"on": "source/house/message",
|
|
48
|
+
"when": { "payload.topic": "house/door/front", "payload.value.open": true, "state.house/mode": "away" },
|
|
49
|
+
"throttleMs": "5m",
|
|
50
|
+
"agent": "house",
|
|
51
|
+
"prompt": "The front door was opened at {{time}} while the house is in away mode.",
|
|
52
|
+
"session": "new",
|
|
53
|
+
"reply": { "topic": "out/source/telegram" }
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Here `house` is an `mqtt` source and `telegram` a `telegram` source, both in `pine.json`
|
|
58
|
+
([sources.md](sources.md)). For a source an agent set up for itself the topic carries the
|
|
59
|
+
agent's name: `out/source/telegram@main`.
|
|
60
|
+
A route does not subscribe to anything, so a source it relies on needs `"start": "always"`
|
|
61
|
+
unless a conversation is subscribed to it anyway.
|
|
62
|
+
|
|
63
|
+
| Key | Meaning |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `name` | For logs and errors. Default `route<n>`. |
|
|
66
|
+
| `on` | A topic pattern or a list of them. |
|
|
67
|
+
| `when` | A filter; every entry must hold. See below. |
|
|
68
|
+
| `debounceMs` | Wait until no matching event has arrived for this long, then handle the last one. For bursts (a camera uploading five images). |
|
|
69
|
+
| `throttleMs` | Handle one event, then ignore matching ones for this long. For repeating alarms. |
|
|
70
|
+
| `key` | Template for what counts as "the same" for debounce and throttle. Default `{{topic}}`. |
|
|
71
|
+
| `set` | Write shared state: `{ "house/mode": "{{payload.value}}" }`. `null`, or a value that is exactly one `{{path}}` that finds nothing, deletes the key. |
|
|
72
|
+
| `publish` | Publish another event: `{ "topic": "...", "payload": {...} }`. Without `payload` the original payload is passed on; after a `call` it is `{ "text": <the answer>, "of": <the original payload> }`. |
|
|
73
|
+
| `call` | One model call: `{ "prompt": "...", "system": "...", "model": "provider/id" }`. Its answer is `{{result}}`. |
|
|
74
|
+
| `agent` | Give an agent a task. |
|
|
75
|
+
| `prompt` | The task text. Default: the payload's `text`, or the payload as JSON. |
|
|
76
|
+
| `session` | Session key template. Default `main`. `new` for one-off work. |
|
|
77
|
+
| `whenBusy` | `queue` (default) or `steer`. |
|
|
78
|
+
| `priority` | Higher runs first when model slots are scarce. Default 10 for owner events, 0 otherwise. |
|
|
79
|
+
| `reply` | Where the agent's answer goes: `{ "topic": "...", "payload": {...} }`. Default: the event's own reply target. `false`: nowhere. |
|
|
80
|
+
| `trust` | Override the event's trust. `"owner"` gives events on this route the agent's full tool set: only for sources nobody else can write to. `pine check` lists such routes. |
|
|
81
|
+
|
|
82
|
+
A route needs at least one of `set`, `publish`, `call`, `agent`. They run in the order `set`,
|
|
83
|
+
`call`, `publish`, `agent`, so one route can classify with `call` and pass the result on with
|
|
84
|
+
`publish`. `call` and `agent` cannot be in the same route: publish the result and let a second
|
|
85
|
+
route start the agent, as in the example below.
|
|
86
|
+
|
|
87
|
+
### Templates
|
|
88
|
+
|
|
89
|
+
`{{path}}` is replaced in `prompt`, `session`, `key`, `set`, `publish`, `reply` and `call`.
|
|
90
|
+
|
|
91
|
+
| Path | Value |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `payload`, `payload.a.b` | The event's payload or a field of it |
|
|
94
|
+
| `topic`, `source`, `id`, `ts`, `time` | Event fields; `time` is local, `YYYY-MM-DD HH:MM:SS` |
|
|
95
|
+
| `state.<key>` | Shared state, for example `{{state.house/mode}}` |
|
|
96
|
+
| `result` | The answer of the route's `call` |
|
|
97
|
+
|
|
98
|
+
A value that is exactly one `{{path}}` keeps its type (a number stays a number); inside longer
|
|
99
|
+
text, objects become JSON and missing values become empty.
|
|
100
|
+
|
|
101
|
+
### Filters
|
|
102
|
+
|
|
103
|
+
`when` maps a path to a value (equality) or to conditions:
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"payload.value.temperature": { "gt": 30 },
|
|
108
|
+
"payload.value.room": { "in": ["kitchen", "boiler"] },
|
|
109
|
+
"payload.text": { "regex": "^alarm", "flags": "i" },
|
|
110
|
+
"state.house/mode": { "ne": "home" },
|
|
111
|
+
"payload.value.battery": { "exists": true }
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Conditions: `eq`, `ne`, `in`, `gt`, `gte`, `lt`, `lte`, `exists`, `contains`, `regex` (with
|
|
116
|
+
optional `flags`).
|
|
117
|
+
|
|
118
|
+
### The cheapest handler that is enough
|
|
119
|
+
|
|
120
|
+
A full agent turn costs seconds of model time. Most events do not need one.
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
[
|
|
124
|
+
{ "on": "source/house/message", "when": { "payload.topic": "house/mode" }, "set": { "house/mode": "{{payload.value}}" } },
|
|
125
|
+
|
|
126
|
+
{ "on": "source/cam/added",
|
|
127
|
+
"debounceMs": "4s",
|
|
128
|
+
"call": { "system": "You look at one camera image. Answer in one line: PERSON, VEHICLE, ANIMAL or NOTHING, then a short description.",
|
|
129
|
+
"prompt": "Camera file {{payload.file}}" },
|
|
130
|
+
"publish": { "topic": "cam/classified", "payload": { "file": "{{payload.file}}", "verdict": "{{result}}" } } },
|
|
131
|
+
|
|
132
|
+
{ "on": "cam/classified",
|
|
133
|
+
"when": { "payload.verdict": { "regex": "^(PERSON|VEHICLE)" }, "state.house/mode": "away" },
|
|
134
|
+
"agent": "house", "session": "new",
|
|
135
|
+
"prompt": "Camera saw: {{payload.verdict}} ({{payload.file}}). The house is in away mode.",
|
|
136
|
+
"reply": { "topic": "out/source/telegram" } }
|
|
137
|
+
]
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The first route never calls the model. The second makes one call per burst of images (a `call`
|
|
141
|
+
receives the event's images if the model accepts them). Only a person or a vehicle while nobody
|
|
142
|
+
is home reaches an agent.
|
|
143
|
+
|
|
144
|
+
Text from untrusted events is prefixed for the agent with a line saying where it came from and
|
|
145
|
+
that it is data, and is cut at `limits.maxEventChars`. That line is a courtesy; the protection
|
|
146
|
+
is that the run has fewer tools (see [security.md](security.md)).
|
|
147
|
+
|
|
148
|
+
## Schedules
|
|
149
|
+
|
|
150
|
+
A schedule is something that happens because of the time: again and again, or once. It is the
|
|
151
|
+
fourth thing an agent can be given by asking, next to event sources, MCP servers and skills,
|
|
152
|
+
and it has the same three scopes ([setup.md](setup.md)).
|
|
153
|
+
|
|
154
|
+
### Ask the agent
|
|
155
|
+
|
|
156
|
+
> Every weekday at seven, check my mail and tell me on Telegram what matters.
|
|
157
|
+
|
|
158
|
+
> Remind me at five to call the dentist.
|
|
159
|
+
|
|
160
|
+
> In twenty minutes, look whether the build is through.
|
|
161
|
+
|
|
162
|
+
The agent calls its `add_schedule` tool with a name, the time and the task in its own words:
|
|
163
|
+
`cron` for set times ("every weekday at six in the evening" is `"0 18 * * 1-5"`), `every` for an
|
|
164
|
+
interval, `at` or `in` for a reminder that happens once. When it is time, it is woken with that
|
|
165
|
+
task, also when you are not there, and does it. What it starts
|
|
166
|
+
counts as your wish: the agent has its usual tools, and nothing is marked as outside content.
|
|
167
|
+
|
|
168
|
+
| Said | Who is woken | You are asked |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| in a side conversation | That conversation; the schedule goes when the conversation is removed | No |
|
|
171
|
+
| in the agent's main conversation, or "as an agent" | The agent's main conversation | Only from a side conversation |
|
|
172
|
+
| "all agents" | Every agent, each in its main conversation. Only the main agent can do this | Yes |
|
|
173
|
+
|
|
174
|
+
In a conversation that has read outside content you are asked in every case, and the dialog
|
|
175
|
+
shows the task: what a web page says must not become something that is done every hour.
|
|
176
|
+
|
|
177
|
+
| Tool | |
|
|
178
|
+
|---|---|
|
|
179
|
+
| `schedules` | What is scheduled for this agent, in which conversation, and when each is next due. |
|
|
180
|
+
| `add_schedule` `name` `task` + one of `cron`, `every`, `at`, `in` [`scope`] | Schedules it. `cron`: set times; `every`: an interval, at least a minute; `at`: once, at a local time (`2026-12-24 17:00`); `in`: once, after this long (`20m`). |
|
|
181
|
+
| `remove_schedule` `name` | Ends one. This conversation's own goes without a question. |
|
|
182
|
+
|
|
183
|
+
A schedule that happens once removes itself when it has fired. A one-off, or a `cron` schedule
|
|
184
|
+
with a task, that was due while pine was not running is done once when pine starts again, and
|
|
185
|
+
the agent is told that it comes late. An `every` schedule simply starts counting again. `pine
|
|
186
|
+
schedules` lists everything that is scheduled.
|
|
187
|
+
|
|
188
|
+
A name is unique among all schedules; a task has at most 4000 characters; an interval an agent
|
|
189
|
+
sets is at least a minute; a time in the past is refused. In manual mode you are asked for
|
|
190
|
+
every `add_schedule` and `remove_schedule`.
|
|
191
|
+
|
|
192
|
+
### In the files
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
"schedules": [
|
|
196
|
+
{ "name": "brief", "cron": "30 7 * * 1-5", "task": "Prepare the morning brief and send it to me on Telegram." },
|
|
197
|
+
{ "name": "dentist", "at": "2026-11-12T17:00", "task": "Remind me to call the dentist." },
|
|
198
|
+
{ "name": "printer-check", "every": "10m" }
|
|
199
|
+
]
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
In an `agent.json` this is that agent's schedule (add `"session": "<conversation>"` for one
|
|
203
|
+
conversation, otherwise its main one). In `pine.json` a schedule with a task wakes every
|
|
204
|
+
agent; one without a task only publishes an event.
|
|
205
|
+
|
|
206
|
+
| Key | Meaning |
|
|
207
|
+
|---|---|
|
|
208
|
+
| `name` | Required. Starts with a lowercase letter, then lowercase letters, digits, `-` and `_`; 40 characters at most. The event is published on `timer/<name>` (`timer/<name>@<agent>` for an agent's own) unless `topic` says otherwise. |
|
|
209
|
+
| `cron` | Five fields, local time: minute, hour, day of month, month, day of week (0 or 7 is Sunday). Lists, ranges and steps work: `*/15 8-18 * * 1-5`. |
|
|
210
|
+
| `every` | A fixed interval instead. |
|
|
211
|
+
| `at` | Once, at this local time. The entry is removed when it has fired. If pine was not running at that time, it fires when pine starts again, and the agent is told that it comes late. |
|
|
212
|
+
| `task` | What the agent is to do. Required in an `agent.json`. |
|
|
213
|
+
| `session` | In an `agent.json`: the conversation that is woken. Default: the main one. |
|
|
214
|
+
| `payload` | Fields added to the event's payload (`name` and `scheduled` are always there; `late` when it comes late, `task` when it has one). |
|
|
215
|
+
| `catchUp` | For `cron`: fire once at startup if a time passed while the daemon was down, marked as late. Default: `true` with a task, `false` without. (`at` always catches up; `every` never does.) |
|
|
216
|
+
|
|
217
|
+
### Schedules and routes
|
|
218
|
+
|
|
219
|
+
Every schedule also publishes an event, trusted as yours, so a route can act on it. For a
|
|
220
|
+
schedule without a task that is all it does:
|
|
221
|
+
|
|
222
|
+
```json
|
|
223
|
+
{ "on": "timer/printer-check", "call": { "prompt": "..." } }
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
A route is the way to do something other than an agent's turn at a time: set state, publish,
|
|
227
|
+
or one model call without tools. For "wake the agent and let it do this", give the schedule a
|
|
228
|
+
task instead; no route is needed.
|
|
229
|
+
|
|
230
|
+
## Webhooks
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
"http": { "hooks": { "doorbell": { "secret": "${DOORBELL_SECRET}" } } }
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`POST http://127.0.0.1:7463/hook/doorbell` then publishes `hook/doorbell` with the request body
|
|
237
|
+
as payload (parsed JSON, or `{ "text": ... }`). The caller proves itself with either
|
|
238
|
+
|
|
239
|
+
- `Authorization: Bearer <secret>`, or
|
|
240
|
+
- an HMAC-SHA256 of the body with the secret, hex-encoded, in `X-Hub-Signature-256: sha256=<hex>`
|
|
241
|
+
(the GitHub convention) or `X-Signature`.
|
|
242
|
+
|
|
243
|
+
A hook needs a secret; `"open": true` instead of a secret accepts anyone who can reach the port
|
|
244
|
+
and is reported by `pine check`. Unknown hook names get 404. Each hook accepts at most 120
|
|
245
|
+
requests a minute and bodies up to 1 MB. Webhook events are untrusted even when the signature
|
|
246
|
+
is right: the signature says who sent it, not that its content is safe to obey.
|
|
247
|
+
|
|
248
|
+
To receive webhooks from outside your network, put a tunnel or reverse proxy in front and
|
|
249
|
+
forward only `/hook/`.
|
|
250
|
+
|
|
251
|
+
A hook here only publishes an event; a route decides what happens. For a webhook that wakes an
|
|
252
|
+
agent directly, use a source of type `webhook` ([sources.md](sources.md)): it answers under
|
|
253
|
+
`/hook/` too, while an agent is subscribed. A hook in `http.hooks` with the same name comes
|
|
254
|
+
first.
|
|
255
|
+
|
|
256
|
+
## When nothing happens
|
|
257
|
+
|
|
258
|
+
1. `pine events --topic "<pattern>"`: did the event arrive, and on which topic?
|
|
259
|
+
2. `pine tasks`: was a task created? `pine task <id>` shows its prompt, result and error.
|
|
260
|
+
3. `pine events --topic "pine/#"`: `pine/route-failed` carries the reason a route could not act,
|
|
261
|
+
for example an agent that refuses untrusted events or a full queue.
|
|
262
|
+
4. Daemon log (stderr): a line `dropped ... after N hops` means a loop was cut.
|
|
263
|
+
5. A filter that never matches is usually a wrong path: look at the real payload in step 1.
|