@timqi/pier 0.0.29 → 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/README.md +58 -125
- package/dist/agent/config.js +24 -11
- package/dist/agent/credentials.js +11 -23
- package/dist/agent/events.js +43 -62
- package/dist/agent/listing.js +113 -68
- package/dist/agent/pi.js +204 -211
- package/dist/boards/boards.js +19 -29
- package/dist/channels/attach.js +14 -42
- package/dist/channels/chains.js +33 -37
- package/dist/channels/chunk.js +8 -28
- package/dist/channels/commands.js +3 -14
- package/dist/channels/config.js +33 -52
- package/dist/channels/control.js +4 -13
- package/dist/channels/conversations.js +8 -25
- package/dist/channels/dedup.js +8 -17
- package/dist/channels/gatekeeper.js +13 -23
- package/dist/channels/lark-api.js +23 -63
- package/dist/channels/lark-outbound.js +12 -44
- package/dist/channels/lark-panel.js +8 -24
- package/dist/channels/lark-render.js +18 -62
- package/dist/channels/lark.js +52 -141
- package/dist/channels/lines.js +13 -15
- package/dist/channels/panel.js +16 -36
- package/dist/channels/receipts.js +29 -52
- package/dist/channels/routes.js +3 -9
- package/dist/channels/runtime.js +12 -23
- package/dist/channels/slack-api.js +34 -86
- package/dist/channels/slack-directory.js +7 -23
- package/dist/channels/slack-outbound.js +12 -56
- package/dist/channels/slack-panel.js +4 -13
- package/dist/channels/slack-render.js +23 -91
- package/dist/channels/slack-tool.js +48 -171
- package/dist/channels/slack.js +73 -239
- package/dist/channels/telegram-api.js +8 -20
- package/dist/channels/telegram-panel.js +5 -21
- package/dist/channels/telegram-render.js +13 -40
- package/dist/channels/telegram.js +54 -146
- package/dist/channels/types.js +5 -16
- package/dist/cli.js +17 -41
- package/dist/config-sync.js +87 -4
- package/dist/core/hub.js +7 -20
- package/dist/core/identity.js +20 -59
- package/dist/core/inbound-file.js +15 -49
- package/dist/core/inbox.js +13 -35
- package/dist/core/queue.js +3 -5
- package/dist/core/reply.js +41 -142
- package/dist/core/router.js +209 -260
- package/dist/core/types.js +17 -1
- package/dist/db.js +98 -252
- package/dist/drain.js +58 -51
- package/dist/extensions/index.js +3 -11
- package/dist/extensions/web/anthropic.js +3 -9
- package/dist/extensions/web/artifacts.js +2 -5
- package/dist/extensions/web/content.js +6 -14
- package/dist/extensions/web/http.js +2 -6
- package/dist/extensions/web/language.js +8 -18
- package/dist/extensions/web/openai.js +1 -1
- package/dist/extensions/web/provider.js +5 -18
- package/dist/extensions/web/tools.js +19 -63
- package/dist/lock.js +98 -0
- package/dist/log.js +9 -26
- package/dist/main.js +87 -179
- package/dist/paths.js +10 -26
- package/dist/secrets.js +19 -46
- package/dist/service.js +33 -75
- package/dist/settings.js +42 -65
- package/dist/tasks/agent.js +129 -114
- package/dist/tasks/callbacks.js +9 -19
- package/dist/tasks/command.js +29 -14
- package/dist/tasks/definitions.js +39 -62
- package/dist/tasks/execution.js +46 -42
- package/dist/tasks/groups.js +41 -35
- package/dist/tasks/messages.js +121 -182
- package/dist/tasks/outbox.js +61 -55
- package/dist/tasks/routes.js +5 -11
- package/dist/tasks/runs.js +14 -13
- package/dist/tasks/service.js +53 -54
- package/dist/tasks/store.js +53 -30
- package/dist/tasks/tool.js +132 -61
- package/dist/tools-task.js +20 -60
- package/dist/tools.js +100 -327
- package/dist/update.js +21 -44
- package/dist/web/auth.js +118 -179
- package/dist/web/config-sync.js +2 -2
- package/dist/web/config.js +3 -7
- package/dist/web/explorer.js +10 -21
- package/dist/web/fs.js +20 -42
- package/dist/web/instance.js +45 -85
- package/dist/web/providers.js +14 -13
- package/dist/web/public/assets/{activity-D3m4L2IL.js → activity-B89_hH7q.js} +2 -2
- package/dist/web/public/assets/activity-B89_hH7q.js.br +0 -0
- package/dist/web/public/assets/activity-B89_hH7q.js.gz +0 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js +1 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js.br +0 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js.gz +0 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js +4 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js.br +0 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js.gz +0 -0
- package/dist/web/public/assets/index-DzXDXra_.js +85 -0
- package/dist/web/public/assets/index-DzXDXra_.js.br +0 -0
- package/dist/web/public/assets/index-DzXDXra_.js.gz +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css +2 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.br +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.gz +0 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js +1 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js.br +0 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js.gz +0 -0
- package/dist/web/public/assets/settings-DzZLmujq.js +5 -0
- package/dist/web/public/assets/settings-DzZLmujq.js.br +0 -0
- package/dist/web/public/assets/settings-DzZLmujq.js.gz +0 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js +3 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js.br +0 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js.gz +0 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js +4 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js.br +0 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js.gz +0 -0
- package/dist/web/public/index.html +100 -130
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/manifest.webmanifest +2 -2
- package/dist/web/public/manifest.webmanifest.br +0 -0
- package/dist/web/public/manifest.webmanifest.gz +0 -0
- package/dist/web/public/sw.js +14 -2
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/push.js +55 -77
- package/dist/web/route.js +3 -7
- package/dist/web/server.js +131 -180
- package/dist/web/session-state.js +14 -54
- package/dist/web/types.js +2 -4
- package/dist/web/webpush.js +10 -25
- package/docs/deploy.md +115 -330
- package/package.json +2 -1
- package/skills/pier-boards/SKILL.md +81 -160
- package/skills/pier-help/SKILL.md +23 -20
- package/skills/pier-slack/SKILL.md +2 -2
- package/skills/pier-tasks/SKILL.md +153 -160
- package/dist/config-sync-fetch.js +0 -84
- package/dist/limits.js +0 -14
- package/dist/web/public/assets/activity-D3m4L2IL.js.br +0 -0
- package/dist/web/public/assets/activity-D3m4L2IL.js.gz +0 -0
- package/dist/web/public/assets/boards-BIObcQeX.js +0 -1
- package/dist/web/public/assets/boards-BIObcQeX.js.br +0 -0
- package/dist/web/public/assets/boards-BIObcQeX.js.gz +0 -0
- package/dist/web/public/assets/explorer-C_rSWPNB.js +0 -4
- package/dist/web/public/assets/explorer-C_rSWPNB.js.br +0 -0
- package/dist/web/public/assets/explorer-C_rSWPNB.js.gz +0 -0
- package/dist/web/public/assets/index-CX3fYZY5.css +0 -2
- package/dist/web/public/assets/index-CX3fYZY5.css.br +0 -0
- package/dist/web/public/assets/index-CX3fYZY5.css.gz +0 -0
- package/dist/web/public/assets/index-uFsZkKOQ.js +0 -85
- package/dist/web/public/assets/index-uFsZkKOQ.js.br +0 -0
- package/dist/web/public/assets/index-uFsZkKOQ.js.gz +0 -0
- package/dist/web/public/assets/runs-Ch6DZq6O.js +0 -1
- package/dist/web/public/assets/runs-Ch6DZq6O.js.br +0 -0
- package/dist/web/public/assets/runs-Ch6DZq6O.js.gz +0 -0
- package/dist/web/public/assets/settings-BWcEIEcv.js +0 -5
- package/dist/web/public/assets/settings-BWcEIEcv.js.br +0 -0
- package/dist/web/public/assets/settings-BWcEIEcv.js.gz +0 -0
- package/dist/web/public/assets/task-runs-DPkwv2UE.js +0 -3
- package/dist/web/public/assets/task-runs-DPkwv2UE.js.br +0 -0
- package/dist/web/public/assets/task-runs-DPkwv2UE.js.gz +0 -0
- package/dist/web/public/assets/tasks-DTiCi2mH.js +0 -4
- package/dist/web/public/assets/tasks-DTiCi2mH.js.br +0 -0
- package/dist/web/public/assets/tasks-DTiCi2mH.js.gz +0 -0
package/README.md
CHANGED
|
@@ -2,17 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
A self-hosted workspace for coding agents. Pier puts a web workbench and your
|
|
4
4
|
IM channels in front of [Pi](https://github.com/earendil-works/pi) sessions:
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
talk to the same agent from a browser, Slack, Telegram or Lark, steer a running
|
|
6
|
+
turn, schedule tasks, watch every session, and publish a static page when
|
|
7
|
+
something is worth showing.
|
|
8
8
|
|
|
9
9
|
One instance, one account, your own machine. The agent runs shell commands in
|
|
10
|
-
directories you name
|
|
11
|
-
control — not for a shared host.
|
|
10
|
+
directories you name — meant for a machine you own, not a shared host.
|
|
12
11
|
|
|
13
|
-
**Status: pre-release.** The version is `0.0.x
|
|
14
|
-
versioned from `0.0.1` on
|
|
15
|
-
|
|
12
|
+
**Status: pre-release.** The version is `0.0.x`; the database schema is
|
|
13
|
+
versioned from `0.0.1` on. Read `docs/deploy.md` before putting it anywhere
|
|
14
|
+
reachable.
|
|
16
15
|
|
|
17
16
|
## Requirements
|
|
18
17
|
|
|
@@ -35,75 +34,47 @@ It listens on `127.0.0.1:3141` (`PORT`, `HOST`) and keeps everything under
|
|
|
35
34
|
session transcripts (`~/.pier/pi`, unless `PI_CODING_AGENT_DIR` says
|
|
36
35
|
otherwise).
|
|
37
36
|
|
|
38
|
-
**The first start generates a password and prints it once.**
|
|
39
|
-
|
|
40
|
-
`
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- **Console → Settings** — everything the instance is configured with, one
|
|
45
|
-
tab per topic: Models (API-key/OAuth logins, and the menu of favored models
|
|
46
|
-
agents are advised with), Agent (Pi configuration, skills, extensions),
|
|
47
|
-
Channels (Slack Socket-Mode or Telegram bot tokens; chats are discovered
|
|
48
|
-
when the bot first sees traffic, gated by the mention/bind rules you set),
|
|
49
|
-
plus the public URL, password and master key
|
|
50
|
-
- **New session** — pick a directory; that is where the agent's shell runs
|
|
37
|
+
**The first start generates a password and prints it once.** Lost it?
|
|
38
|
+
`sqlite3 ~/.pier/db/pier.db 'DELETE FROM auth'` and restart. Open
|
|
39
|
+
`http://localhost:3141`, sign in; **Console → Settings** configures Models,
|
|
40
|
+
Agent, Channels, the public URL, password and master key; **New session** picks
|
|
41
|
+
the directory the agent's shell runs in.
|
|
51
42
|
|
|
52
43
|
## Configure Pi
|
|
53
44
|
|
|
54
|
-
Pier gives Pi
|
|
55
|
-
|
|
56
|
-
`PI_CODING_AGENT_DIR` before starting Pier to use another directory, including
|
|
57
|
-
an existing Pi setup:
|
|
45
|
+
Pier gives Pi its own agent directory, `$PIER_HOME/pi` by default; set
|
|
46
|
+
`PI_CODING_AGENT_DIR` to use another, including an existing Pi setup:
|
|
58
47
|
|
|
59
48
|
```sh
|
|
60
49
|
PI_CODING_AGENT_DIR="$HOME/.pi/agent" pier serve
|
|
61
50
|
```
|
|
62
51
|
|
|
63
|
-
Pier exports that variable
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
Anthropic one (OpenAI hosts no fetch tool), and no other key or service. The
|
|
90
|
-
page says which tools a switch adds and what each needs before you flip it.
|
|
91
|
-
Below it, the same tab switches on the command-line tools Pier manages —
|
|
92
|
-
`rtk`, `rg`, `fd`, `wt`, `jq`, or a tool of your own written as a
|
|
93
|
-
[ubix](https://github.com/timqi/ubix) block. A switch installs the binary
|
|
94
|
-
into `~/.pier/tools/bin`, which is first on the PATH every session and task
|
|
95
|
-
inherits, and a task you can read keeps them current.
|
|
96
|
-
|
|
97
|
-
On first credential access, Pier imports an existing `auth.json` into its sealed
|
|
98
|
-
store and renames the source to `auth.json.imported`. Literal provider keys left
|
|
99
|
-
in `models.json` are moved the same way, with the original retained as
|
|
100
|
-
`models.json.imported`. Use Providers for new secrets; the advanced editor will
|
|
101
|
-
not accept plaintext keys or header values.
|
|
102
|
-
|
|
103
|
-
Provider environment variables supported by Pi are inherited from the Pier
|
|
104
|
-
process. A service installed with `pier service install` does not inherit your
|
|
105
|
-
interactive shell, so put non-secret Pi environment settings in a systemd unit
|
|
106
|
-
override; for API keys, prefer the sealed Providers UI.
|
|
52
|
+
Pier exports that variable, so anything it starts inherits it. A second Pier
|
|
53
|
+
launched from there with its own `PIER_HOME` derives its own agent directory
|
|
54
|
+
unless `PI_CODING_AGENT_DIR` is set again on that command line.
|
|
55
|
+
|
|
56
|
+
Console → Settings:
|
|
57
|
+
|
|
58
|
+
- **Models** — endpoints, API-key/OAuth login, pinned models. Credentials are
|
|
59
|
+
sealed in Pier's SQLite database, never written back to `models.json`.
|
|
60
|
+
**Test** sends one real request and shows the body as sent and the reply.
|
|
61
|
+
- **Agent** — edits `SYSTEM.md`, `AGENTS.md`, `settings.json` and the
|
|
62
|
+
`models.json` structure, globally or per project scope (with that project's
|
|
63
|
+
`.pi/skills` and `.pi/extensions` listed). Changes apply when a session next
|
|
64
|
+
opens; saving recycles idle sessions; **Settings → Instance → Reload** does
|
|
65
|
+
the same for files changed elsewhere. Also here: the bundled extensions
|
|
66
|
+
(`web`: `web_search` on Anthropic or OpenAI, `web_fetch` on Anthropic; a
|
|
67
|
+
copy of yours registering the same tool makes Pier's stand down) and the
|
|
68
|
+
managed CLI tools (`rtk`, `rg`, `fd`, `wt`, `jq`, or your own as a
|
|
69
|
+
[ubix](https://github.com/timqi/ubix) block), installed into
|
|
70
|
+
`~/.pier/tools/bin`, first on every session's PATH.
|
|
71
|
+
|
|
72
|
+
On first credential access an existing `auth.json` is imported into the sealed
|
|
73
|
+
store and renamed `auth.json.imported`; literal keys in `models.json` likewise
|
|
74
|
+
(`models.json.imported`). The editor rejects plaintext keys or header values.
|
|
75
|
+
Pi's provider environment variables are inherited from the Pier process; a
|
|
76
|
+
systemd service does not inherit your shell, so put non-secret settings in a
|
|
77
|
+
unit override and API keys in the Providers UI.
|
|
107
78
|
|
|
108
79
|
## Run it as a service
|
|
109
80
|
|
|
@@ -118,32 +89,11 @@ pier update # latest release, then hard-stop/restart the service
|
|
|
118
89
|
pier tools sync # install/update the managed CLI tools by hand
|
|
119
90
|
```
|
|
120
91
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
Both commands target the installed systemd service. `pier update` is still a
|
|
127
|
-
hard stop because its separate updater replaces the installed code; let active
|
|
128
|
-
work finish before starting it.
|
|
129
|
-
|
|
130
|
-
Linux only, because it is systemd. It writes `~/.config/systemd/user/pier.service`
|
|
131
|
-
with the absolute path of the node you installed with (systemd's PATH would not
|
|
132
|
-
find a version-managed one), a memory drop-in it never rewrites afterwards, and
|
|
133
|
-
turns on linger so scheduled tasks survive your logout. Install also records the
|
|
134
|
-
exact npm executable in a separate updater unit. Re-run `pier service install
|
|
135
|
-
--force` after changing the service settings or its Node/npm installation; this
|
|
136
|
-
rewrites both units and restarts Pier. On macOS run `pier serve` in a terminal,
|
|
137
|
-
or under whatever supervisor you already use — `pier` on its own only prints
|
|
138
|
-
the usage.
|
|
139
|
-
|
|
140
|
-
`docs/deploy.md` is the same thing written out by hand, plus what the memory
|
|
141
|
-
limits mean, how updates work (and why the updater is a second unit), how to
|
|
142
|
-
read the first-run password out of the journal, and what to back up.
|
|
143
|
-
|
|
144
|
-
Exposing it needs two things: a reverse proxy or tunnel that terminates TLS
|
|
145
|
-
(Pier binds the loopback and expects `X-Forwarded-For`/`-Proto`), and the
|
|
146
|
-
understanding that whoever gets past the password gets a shell.
|
|
92
|
+
Linux only (systemd); on macOS run `pier serve` under your own supervisor.
|
|
93
|
+
`docs/deploy.md` is the runbook: units, memory limits, updates and rollback,
|
|
94
|
+
the first-run password, remote access, backups. Expose it only behind a
|
|
95
|
+
TLS-terminating proxy or tunnel (`X-Forwarded-For`/`-Proto`) — whoever gets
|
|
96
|
+
past the password gets a shell.
|
|
147
97
|
|
|
148
98
|
## Develop
|
|
149
99
|
|
|
@@ -157,40 +107,23 @@ npm run lint # oxlint
|
|
|
157
107
|
npm test # vitest
|
|
158
108
|
```
|
|
159
109
|
|
|
160
|
-
- `AGENTS.md` — the principles this codebase is held to
|
|
161
|
-
say when a change is too big. Read it before writing code here.
|
|
110
|
+
- `AGENTS.md` — the principles and budgets this codebase is held to
|
|
162
111
|
- `docs/architecture.md` — the seams, the areas, and what is deliberately absent
|
|
163
|
-
- `docs/design/` — one document per subsystem
|
|
112
|
+
- `docs/design/` — one document per subsystem
|
|
164
113
|
|
|
165
114
|
## Releases
|
|
166
115
|
|
|
167
|
-
Pier asks `registry.npmjs.org` at boot and every 30 minutes
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
time to finish, whatever the deadline still had to cut off written to the chat
|
|
179
|
-
it belonged to — and only then hand over. The automatic path additionally waits
|
|
180
|
-
for an idle instance: no turn streaming, no task run in flight.
|
|
181
|
-
|
|
182
|
-
Either way the updater writes `~/.pier/db/backups/pier.db.release-<version>.bak`
|
|
183
|
-
before npm touches the package — named for the release being replaced, which is
|
|
184
|
-
the one to reinstall beside it; the three newest are kept. Then it updates the
|
|
185
|
-
npm installation recorded when the service was installed, and starts Pier again.
|
|
186
|
-
|
|
187
|
-
`main` is the only development line. `just release [patch|minor|major]` runs the
|
|
188
|
-
checks, writes the tag and pushes it; the tag builds and publishes to npm and as
|
|
189
|
-
a GitHub Release. The version in the web footer is the one from `package.json`
|
|
190
|
-
— so the number on screen always names a commit.
|
|
191
|
-
Schema upgrades are one-way: a database migrated by a newer Pier is refused by
|
|
192
|
-
an older one. The release backup above is the way back; `docs/deploy.md` has the
|
|
193
|
-
restore procedure and the additional snapshots taken before schema migrations.
|
|
116
|
+
Pier asks `registry.npmjs.org` at boot and every 30 minutes; the footer version
|
|
117
|
+
becomes `v0.0.1 → 0.0.2` when a release is out and opens a panel: source link,
|
|
118
|
+
**Update now**, **Update automatically** (idle instance only). Both drain first
|
|
119
|
+
and hand the install to the updater unit; off systemd the panel says `pier
|
|
120
|
+
update`. The updater writes `~/.pier/db/backups/pier.db.release-<version>.bak`
|
|
121
|
+
(the release being replaced; three kept) first. Schema upgrades are one-way;
|
|
122
|
+
`docs/deploy.md` has the rollback.
|
|
123
|
+
|
|
124
|
+
`main` is the only development line. `just release [patch|minor|major]` checks,
|
|
125
|
+
tags and pushes; the tag builds and publishes to npm and a GitHub Release. The
|
|
126
|
+
footer version is `package.json`'s.
|
|
194
127
|
|
|
195
128
|
## License
|
|
196
129
|
|
package/dist/agent/config.js
CHANGED
|
@@ -10,16 +10,26 @@ import { mergeSnapshotProviders, normalizeAgentSnapshot, snapshotProviders } fro
|
|
|
10
10
|
const GLOBAL_FILES = ["SYSTEM.md", "AGENTS.md", "settings.json", "models.json"];
|
|
11
11
|
const PROJECT_FILES = ["AGENTS.md"];
|
|
12
12
|
// settings.json is on the list for two fields: the default model and its
|
|
13
|
-
// reasoning effort. Everything else in it is machine-local
|
|
14
|
-
// import untouched.
|
|
13
|
+
// reasoning effort. Everything else in it is machine-local.
|
|
15
14
|
const SNAPSHOT_FILES = ["SYSTEM.md", "AGENTS.md", "models.json", "settings.json"];
|
|
16
15
|
const RESOURCE_DEPTH = 3; // extensions/skills nest at most a couple of levels
|
|
17
|
-
/** Pier owns the Pi runtime dir
|
|
18
|
-
* repo, not `~/.pi`. main.ts exports this as PI_CODING_AGENT_DIR so the SDK's
|
|
19
|
-
* own path resolution (auth.json, sessions, bin) lands in the same place. */
|
|
16
|
+
/** Pier owns the Pi runtime dir; main.ts exports it as PI_CODING_AGENT_DIR. */
|
|
20
17
|
export const defaultAgentDir = () => process.env.PI_CODING_AGENT_DIR ?? pierPath("pi");
|
|
21
18
|
/** Stable mask: mapped back by field, without exposing key fragments. */
|
|
22
19
|
const maskKey = (_key) => "••••••••";
|
|
20
|
+
/** Pi reads the two highest levels off the model's own map. The rest of a
|
|
21
|
+
* hand-written map (an `off: null`) is left alone: the Console does not offer it. */
|
|
22
|
+
const effortOf = (map) => typeof map?.max === "string" ? "max" : typeof map?.xhigh === "string" ? "xhigh" : undefined;
|
|
23
|
+
function withEffort(map, effort) {
|
|
24
|
+
const next = { ...map };
|
|
25
|
+
delete next.xhigh;
|
|
26
|
+
delete next.max;
|
|
27
|
+
if (effort === "xhigh" || effort === "max")
|
|
28
|
+
next.xhigh = "xhigh";
|
|
29
|
+
if (effort === "max")
|
|
30
|
+
next.max = "max";
|
|
31
|
+
return Object.keys(next).length ? next : undefined;
|
|
32
|
+
}
|
|
23
33
|
const missing = (err) => err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
24
34
|
const readNullable = async (path) => {
|
|
25
35
|
try {
|
|
@@ -131,9 +141,11 @@ export class PiConfigStore {
|
|
|
131
141
|
if (typeof model !== "object" || model === null || typeof model.id !== "string") {
|
|
132
142
|
return [];
|
|
133
143
|
}
|
|
144
|
+
const effort = effortOf(asRecord(model.thinkingLevelMap));
|
|
134
145
|
return [{
|
|
135
146
|
id: model.id,
|
|
136
147
|
reasoning: model.reasoning === true,
|
|
148
|
+
...(effort ? { effort } : {}),
|
|
137
149
|
}];
|
|
138
150
|
})
|
|
139
151
|
: undefined;
|
|
@@ -180,6 +192,11 @@ export class PiConfigStore {
|
|
|
180
192
|
next.reasoning = true;
|
|
181
193
|
else
|
|
182
194
|
delete next.reasoning;
|
|
195
|
+
const map = withEffort(asRecord(next.thinkingLevelMap), model.effort);
|
|
196
|
+
if (map)
|
|
197
|
+
next.thinkingLevelMap = map;
|
|
198
|
+
else
|
|
199
|
+
delete next.thinkingLevelMap;
|
|
183
200
|
return next;
|
|
184
201
|
});
|
|
185
202
|
}
|
|
@@ -319,12 +336,8 @@ export class PiConfigStore {
|
|
|
319
336
|
return fs.readFile(path, "utf8");
|
|
320
337
|
}
|
|
321
338
|
}
|
|
322
|
-
/**
|
|
323
|
-
*
|
|
324
|
-
* Symlinks are followed (skills and extensions are routinely linked in from a
|
|
325
|
-
* checkout elsewhere) and everything reached through one is flagged, so the UI
|
|
326
|
-
* can say where it really came from. The depth bound is also the cycle guard.
|
|
327
|
-
*/
|
|
339
|
+
/** Symlinks are followed (skills are routinely linked in from elsewhere) and
|
|
340
|
+
* flagged; the depth bound is also the cycle guard. */
|
|
328
341
|
async function listDir(root, prefix = "", depth = RESOURCE_DEPTH, linked = false) {
|
|
329
342
|
if (depth === 0)
|
|
330
343
|
return [];
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
// Provider credentials
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// token back through here instead of a file. A stored credential wins over a
|
|
7
|
-
// models.json apiKey in pi-ai's resolution order, which is what lets the
|
|
8
|
-
// sweep below leave models.json purely structural — and safely syncable.
|
|
1
|
+
// Provider credentials at rest in pier.db, sealed by Secrets. Implements
|
|
2
|
+
// pi-ai's CredentialStore structurally (no SDK import), so an OAuth refresh
|
|
3
|
+
// writes back here instead of a file. A stored credential wins over a
|
|
4
|
+
// models.json apiKey in pi-ai's resolution order, which is what lets the sweep
|
|
5
|
+
// leave models.json purely structural and syncable.
|
|
9
6
|
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
10
7
|
import { join } from "node:path";
|
|
11
8
|
import { isDeepStrictEqual } from "node:util";
|
|
@@ -25,9 +22,7 @@ export class CredentialStore {
|
|
|
25
22
|
this.secrets = secrets;
|
|
26
23
|
this.agentDir = agentDir;
|
|
27
24
|
}
|
|
28
|
-
/**
|
|
29
|
-
* surface later as "provider is not configured". Called by pi.ts before
|
|
30
|
-
* every open; encrypt() throws the `secrets locked: ...` error we want. */
|
|
25
|
+
/** encrypt() throws the `secrets locked: ...` error a session open must fail with. */
|
|
31
26
|
assertUnlocked() {
|
|
32
27
|
if (this.secrets.state === "locked")
|
|
33
28
|
this.secrets.encrypt("");
|
|
@@ -93,10 +88,8 @@ export class CredentialStore {
|
|
|
93
88
|
"ON CONFLICT(key) DO UPDATE SET value = excluded.value")
|
|
94
89
|
.run(providerId, this.secrets.encrypt(JSON.stringify(credential)));
|
|
95
90
|
}
|
|
96
|
-
/**
|
|
97
|
-
*
|
|
98
|
-
* set only then, and #put is an idempotent upsert). The file is renamed,
|
|
99
|
-
* never deleted: auth.json.imported is the operator's receipt and way back. */
|
|
91
|
+
/** Lazy: sealing needs an unlocked Secrets, and #put is an idempotent upsert,
|
|
92
|
+
* so this retries until it succeeds. The file is renamed, never deleted. */
|
|
100
93
|
#ensureImported() {
|
|
101
94
|
if (this.#imported)
|
|
102
95
|
return;
|
|
@@ -123,14 +116,9 @@ export class CredentialStore {
|
|
|
123
116
|
this.#sweepModelsJson();
|
|
124
117
|
this.#imported = true;
|
|
125
118
|
}
|
|
126
|
-
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* database; `!command` and `$ENV` references are already not plaintext and
|
|
130
|
-
* stay — the SDK resolves those forms itself, and a sealed copy of a
|
|
131
|
-
* reference would freeze its meaning. The pre-sweep file is kept whole as
|
|
132
|
-
* models.json.imported: keys leave the file only with a receipt.
|
|
133
|
-
*/
|
|
119
|
+
/** models.json is what a config repo syncs, so literal keys move into the
|
|
120
|
+
* database. `!command` and `$ENV` references stay: a sealed copy would
|
|
121
|
+
* freeze their meaning. The pre-sweep file is kept as models.json.imported. */
|
|
134
122
|
#sweepModelsJson() {
|
|
135
123
|
const path = join(this.agentDir, "models.json");
|
|
136
124
|
if (!existsSync(path))
|
package/dist/agent/events.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// Pure Pi-event → SessionEventPayload translation. Structurally typed
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// events.test.ts is the mapping's spec; extend types.ts before adding events.
|
|
1
|
+
// Pure Pi-event → SessionEventPayload translation. Structurally typed: no Pi
|
|
2
|
+
// imports, so it is unit-testable without Pi and Pi types never leak past the
|
|
3
|
+
// seam. The golden table in events.test.ts is the mapping's spec.
|
|
5
4
|
import { isThinkingLevel, MAX_STEP_OUTPUT } from "../core/types.js";
|
|
6
|
-
|
|
5
|
+
/** An assistant message that calls a tool is work in progress, not a reply. */
|
|
6
|
+
export const hasToolCalls = (message) => Array.isArray(message?.content) && message.content.some((part) => part.type === "toolCall");
|
|
7
7
|
export function textOf(content) {
|
|
8
8
|
if (typeof content === "string")
|
|
9
9
|
return content;
|
|
@@ -25,9 +25,7 @@ function systemOrigin(message) {
|
|
|
25
25
|
typeof origin.runId !== "string" ||
|
|
26
26
|
(origin.sourceSessionId !== null && typeof origin.sourceSessionId !== "string"))
|
|
27
27
|
return null;
|
|
28
|
-
//
|
|
29
|
-
// half-valid one drawn by the card is an `undefined` in a chip, which reads
|
|
30
|
-
// as a bug in the card rather than as bad metadata.
|
|
28
|
+
// A half-valid `source` drawn by the card is an `undefined` in a chip.
|
|
31
29
|
const source = inputSource(raw);
|
|
32
30
|
const shape = { ...origin, ...(source ? { source } : {}) };
|
|
33
31
|
if (origin.kind === "task-delegation" || origin.kind === "task-callback") {
|
|
@@ -40,9 +38,6 @@ function systemOrigin(message) {
|
|
|
40
38
|
return shape;
|
|
41
39
|
return null;
|
|
42
40
|
}
|
|
43
|
-
/** What produced a system input, as read back off disk: the name is the whole
|
|
44
|
-
* point of it, the model and the effort are each kept only if they are the
|
|
45
|
-
* shape they claim to be (core/types.ts). */
|
|
46
41
|
function inputSource(value) {
|
|
47
42
|
if (!value || typeof value !== "object")
|
|
48
43
|
return undefined;
|
|
@@ -58,6 +53,9 @@ function inputSource(value) {
|
|
|
58
53
|
...(isThinkingLevel(thinking) ? { thinking } : {}),
|
|
59
54
|
};
|
|
60
55
|
}
|
|
56
|
+
/** `length`, `aborted` and `error` are Pi's to recover from (a truncated answer
|
|
57
|
+
* is compacted and asked again), so they stay on the `agent_end` path. */
|
|
58
|
+
const isAnswer = (m) => m?.role === "assistant" && m.stopReason === "stop" && !hasToolCalls(m);
|
|
61
59
|
function lastAssistant(messages) {
|
|
62
60
|
if (!messages)
|
|
63
61
|
return undefined;
|
|
@@ -67,17 +65,9 @@ function lastAssistant(messages) {
|
|
|
67
65
|
}
|
|
68
66
|
return undefined;
|
|
69
67
|
}
|
|
70
|
-
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* that at stream start, so callers with a real clock (live turn-end) pass
|
|
74
|
-
* their own; history accepts the approximation.
|
|
75
|
-
*
|
|
76
|
-
* `tokens` is the context size at that point, not a sum: each assistant
|
|
77
|
-
* message's `totalTokens` already covers the whole request (prompt + cache +
|
|
78
|
-
* output), so adding them up double-counts the context on every turn. Pi
|
|
79
|
-
* itself reads context usage off the last assistant message the same way.
|
|
80
|
-
*/
|
|
68
|
+
/** Pi stamps a message's timestamp at stream start, so a live turn-end passes
|
|
69
|
+
* its own `completedAt`. `tokens` is not a sum: each `totalTokens` already
|
|
70
|
+
* covers the whole request. */
|
|
81
71
|
export function turnMetaAt(messages, index, completedAt) {
|
|
82
72
|
const m = messages[index];
|
|
83
73
|
if (m?.role !== "assistant" || typeof m.timestamp !== "number")
|
|
@@ -101,11 +91,8 @@ export function turnMetaAt(messages, index, completedAt) {
|
|
|
101
91
|
}
|
|
102
92
|
return { completedAt: end, durationMs: Math.max(0, end - started), tokens };
|
|
103
93
|
}
|
|
104
|
-
/**
|
|
105
|
-
*
|
|
106
|
-
* (thinking + tool calls) that preceded each assistant answer. This is what
|
|
107
|
-
* makes a page reload show the real step counts instead of restarting at zero.
|
|
108
|
-
*/
|
|
94
|
+
/** The renderable transcript, activity included, so a reload shows the same
|
|
95
|
+
* step counts the live stream built. */
|
|
109
96
|
export function toChatTurns(messages) {
|
|
110
97
|
const turns = [];
|
|
111
98
|
let steps = []; // activity seen since the last emitted turn
|
|
@@ -117,9 +104,8 @@ export function toChatTurns(messages) {
|
|
|
117
104
|
turn.meta = meta;
|
|
118
105
|
if (origin)
|
|
119
106
|
turn.origin = origin;
|
|
120
|
-
// An assistant turn
|
|
121
|
-
// time at all after a reload
|
|
122
|
-
// stamp is gone.
|
|
107
|
+
// An assistant turn's meta says when it finished; user and system turns
|
|
108
|
+
// would have no time at all after a reload.
|
|
123
109
|
if (at !== undefined && role !== "assistant")
|
|
124
110
|
turn.at = at;
|
|
125
111
|
if (steps.length) {
|
|
@@ -132,8 +118,7 @@ export function toChatTurns(messages) {
|
|
|
132
118
|
if (m.role === "toolResult") {
|
|
133
119
|
const step = pendingTools.get(m.toolCallId ?? "");
|
|
134
120
|
if (step) {
|
|
135
|
-
// Capped
|
|
136
|
-
// long session's tool results are megabytes nobody ever sees.
|
|
121
|
+
// Capped here: a long session's tool results are megabytes nobody sees.
|
|
137
122
|
const output = textOf(m.content);
|
|
138
123
|
step.output = output.length > MAX_STEP_OUTPUT ? output.slice(0, MAX_STEP_OUTPUT) + "…" : output;
|
|
139
124
|
step.isError = m.isError ?? false;
|
|
@@ -201,18 +186,21 @@ export function toSessionEvents(e) {
|
|
|
201
186
|
switch (e.type) {
|
|
202
187
|
case "agent_start":
|
|
203
188
|
return [{ type: "state", state: "streaming" }, { type: "turn-start" }];
|
|
189
|
+
// One turn-end per answer, not per run: Pi drains a queued follow-up
|
|
190
|
+
// inside the run and emits a single agent_end for all of it.
|
|
191
|
+
case "turn_end":
|
|
192
|
+
return isAnswer(e.message) ? [{ type: "turn-end", text: textOf(e.message?.content) }] : [];
|
|
204
193
|
case "agent_end": {
|
|
205
|
-
// Pi
|
|
206
|
-
// per attempt. Only the last one ends the turn: translating the others
|
|
207
|
-
// posts a reply and an error per attempt for a failure Pi is still
|
|
208
|
-
// recovering from, and an `idle` the session is not in.
|
|
194
|
+
// Pi emits one agent_end per retry attempt; only the last ends the turn.
|
|
209
195
|
if (e.willRetry)
|
|
210
196
|
return [];
|
|
211
197
|
const final = lastAssistant(e.messages);
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
198
|
+
// An answer ended its own turn above; what is left is every way a run
|
|
199
|
+
// ends without one.
|
|
200
|
+
if (isAnswer(final))
|
|
201
|
+
return [];
|
|
202
|
+
// Carried twice: on turn-end because it is how the turn ended (what a
|
|
203
|
+
// task run settles on), and as the error event every chat surface reports.
|
|
216
204
|
const failure = final?.stopReason === "error"
|
|
217
205
|
? final.errorMessage || "unknown agent error"
|
|
218
206
|
: undefined;
|
|
@@ -223,31 +211,27 @@ export function toSessionEvents(e) {
|
|
|
223
211
|
out.push({ type: "error", message: failure });
|
|
224
212
|
return out;
|
|
225
213
|
}
|
|
226
|
-
// Pi
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
// rides on it rather than on the turn ending, so the seam's `state` getter
|
|
230
|
-
// and this stream stop being two derivations of one fact (§5): a waiter the
|
|
231
|
-
// idle wakes now re-reads `state` as idle instead of re-arming for an event
|
|
232
|
-
// that is already spent. It is also the truthful moment — Pi's
|
|
233
|
-
// auto-compaction and queued continuations run past `agent_end`.
|
|
214
|
+
// Pi clears `isStreaming` one statement before emitting this, many
|
|
215
|
+
// microtasks after the last `agent_end`; idle rides on it so the `state`
|
|
216
|
+
// getter and this stream agree. Auto-compaction runs past `agent_end` too.
|
|
234
217
|
case "agent_settled":
|
|
235
218
|
return [{ type: "state", state: "idle" }];
|
|
236
219
|
case "message_start": {
|
|
237
|
-
// Pi emits this for every message entering the context; the user ones are
|
|
238
|
-
// what a client can't know about (queued/steered messages, IM traffic).
|
|
239
220
|
const m = e.message;
|
|
240
221
|
if (!m)
|
|
241
222
|
return [];
|
|
242
223
|
if (m.role === "assistant")
|
|
243
224
|
return [{ type: "text-start" }];
|
|
244
225
|
const origin = systemOrigin(m);
|
|
245
|
-
|
|
246
|
-
if (origin)
|
|
247
|
-
return text ? [{ type: "system-input", text, origin }] : [];
|
|
248
|
-
if (m.role !== "user")
|
|
226
|
+
if (!origin && m.role !== "user")
|
|
249
227
|
return [];
|
|
250
|
-
|
|
228
|
+
// A message Pi drains mid-run opens a turn `agent_start` never announces.
|
|
229
|
+
// On the message, not its text: an attachment with no caption is still a turn.
|
|
230
|
+
const out = [{ type: "turn-start" }];
|
|
231
|
+
const text = textOf(m.content);
|
|
232
|
+
if (text)
|
|
233
|
+
out.push(origin ? { type: "system-input", text, origin } : { type: "user-message", text });
|
|
234
|
+
return out;
|
|
251
235
|
}
|
|
252
236
|
case "message_update": {
|
|
253
237
|
const ame = e.assistantMessageEvent;
|
|
@@ -277,10 +261,8 @@ export function toSessionEvents(e) {
|
|
|
277
261
|
},
|
|
278
262
|
];
|
|
279
263
|
case "compaction_end": {
|
|
280
|
-
// The only trace compaction leaves
|
|
281
|
-
//
|
|
282
|
-
// renders nothing for — so without this event the button's effect is
|
|
283
|
-
// invisible and the automatic one is invisible twice over (§5b).
|
|
264
|
+
// The only trace compaction leaves: `toChatTurns` renders nothing for the
|
|
265
|
+
// summary message (§5).
|
|
284
266
|
const r = e.result;
|
|
285
267
|
if (r && typeof r.tokensBefore === "number") {
|
|
286
268
|
return [{
|
|
@@ -289,9 +271,8 @@ export function toSessionEvents(e) {
|
|
|
289
271
|
after: r.estimatedTokensAfter ?? r.tokensBefore,
|
|
290
272
|
}];
|
|
291
273
|
}
|
|
292
|
-
// No result
|
|
293
|
-
//
|
|
294
|
-
// as well; an automatic one has no route, and this is all it has.
|
|
274
|
+
// No result: cancelled or failed. An automatic compaction has no route
|
|
275
|
+
// to report through; this is all it has.
|
|
295
276
|
return [{ type: "error", message: e.errorMessage ?? "compaction cancelled" }];
|
|
296
277
|
}
|
|
297
278
|
case "tool_execution_end":
|