@chalky/chalky 0.1.3 → 0.1.5
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 +10 -6
- package/{app → agent}/SYSTEM.md +16 -11
- package/agent/extensions/app/README.md +68 -0
- package/{app/extensions/ui → agent/extensions/app}/agents.js +22 -16
- package/agent/extensions/app/guide.md +5 -0
- package/agent/extensions/app/index.ts +66 -0
- package/{app/extensions/ui/skills/agent-apps → agent/extensions/app/skills/app-agents}/SKILL.md +17 -18
- package/agent/extensions/board/browser.js +359 -0
- package/agent/extensions/board/guide.md +4 -0
- package/agent/extensions/board/index.ts +100 -0
- package/agent/extensions/card/README.md +88 -0
- package/agent/extensions/card/blocks/Button.js +34 -0
- package/{app/extensions/ui/components → agent/extensions/card/blocks}/Column.js +2 -2
- package/agent/extensions/card/blocks/Markdown.js +37 -0
- package/{app/extensions/ui/components/Card.js → agent/extensions/card/blocks/Panel.js} +3 -3
- package/{app/extensions/ui/components → agent/extensions/card/blocks}/Row.js +2 -2
- package/{app/extensions/ui/components → agent/extensions/card/blocks}/TextField.js +3 -3
- package/{app/extensions/ui → agent/extensions/card}/browser.js +155 -128
- package/agent/extensions/card/guide.md +13 -0
- package/agent/extensions/card/index.ts +370 -0
- package/agent/extensions/command/browser.js +13 -0
- package/agent/extensions/command/guide.md +2 -0
- package/agent/extensions/command/index.ts +94 -0
- package/agent/extensions/doodle/README.md +57 -0
- package/agent/extensions/doodle/browser.js +1013 -0
- package/agent/extensions/doodle/guide.md +22 -0
- package/agent/extensions/doodle/index.ts +274 -0
- package/agent/extensions/file/browser.js +143 -0
- package/agent/extensions/file/guide.md +5 -0
- package/{app/extensions/files → agent/extensions/file}/index.ts +34 -16
- package/{app/extensions/frames → agent/extensions/frame}/README.md +13 -13
- package/{app/extensions/frames → agent/extensions/frame}/browser.js +24 -19
- package/agent/extensions/frame/guide.md +3 -0
- package/agent/extensions/frame/index.ts +81 -0
- package/{app/extensions/ui_history → agent/extensions/history}/browser.js +18 -18
- package/{app/extensions/ui_history → agent/extensions/history}/index.ts +32 -33
- package/{app → agent}/extensions/mcp/browser.js +1 -1
- package/agent/extensions/mcp/guide.md +3 -0
- package/{app → agent}/extensions/mcp/index.ts +2 -4
- package/agent/extensions/model/browser.js +363 -0
- package/{app → agent}/extensions/model/index.ts +8 -4
- package/agent/extensions/model/login.ts +215 -0
- package/agent/extensions/note/README.md +35 -0
- package/agent/extensions/note/browser.js +356 -0
- package/agent/extensions/note/guide.md +8 -0
- package/agent/extensions/note/index.ts +192 -0
- package/{app → agent}/extensions/page/index.ts +2 -2
- package/{app → agent}/extensions/page/page.html +55 -49
- package/agent/extensions/prompt/index.ts +38 -0
- package/{app → agent}/extensions/reload/index.ts +3 -3
- package/{app → agent}/extensions/screen/browser.js +4 -4
- package/{app → agent}/extensions/screen/index.ts +11 -11
- package/{app/extensions/steps → agent/extensions/step}/index.ts +1 -1
- package/{app/extensions/swarm → agent/extensions/team}/README.md +19 -19
- package/{app/extensions/swarm → agent/extensions/team}/guide.md +4 -4
- package/{app/extensions/swarm → agent/extensions/team}/index.ts +41 -66
- package/{app/extensions/versions → agent/extensions/version}/browser.js +10 -10
- package/{app/extensions/versions → agent/extensions/version}/index.ts +39 -54
- package/{app → agent}/extensions/viewer/README.md +4 -4
- package/{app → agent}/extensions/viewer/browser.js +8 -8
- package/agent/extensions/viewer/guide.md +3 -0
- package/{app → agent}/extensions/viewer/index.ts +22 -17
- package/agent/extensions/voice/browser.js +72 -0
- package/agent/extensions/voice/index.ts +39 -0
- package/{app/extensions/ui → agent/extensions/voice}/voice.js +5 -5
- package/agent/settings.json +8 -0
- package/{app/extensions/ui/skills/ui-check → agent/skills/browser-check}/SKILL.md +4 -4
- package/{app/extensions/ui/skills/ui-check → agent/skills/browser-check}/scripts/check.mjs +1 -1
- package/{app/extensions/ui/skills/ui-design → agent/skills/design}/SKILL.md +14 -14
- package/agent/skills/extension-map/SKILL.md +56 -0
- package/{app → agent}/skills/write-skill/SKILL.md +1 -1
- package/bin/chalky.mjs +3 -3
- package/package.json +8 -8
- package/server.ts +162 -63
- package/app/extensions/appdata/README.md +0 -10
- package/app/extensions/appdata/guide.md +0 -4
- package/app/extensions/appdata/index.ts +0 -47
- package/app/extensions/doodle/README.md +0 -15
- package/app/extensions/doodle/browser.js +0 -388
- package/app/extensions/doodle/guide.md +0 -6
- package/app/extensions/doodle/index.ts +0 -54
- package/app/extensions/files/browser.js +0 -96
- package/app/extensions/files/guide.md +0 -7
- package/app/extensions/frames/guide.md +0 -7
- package/app/extensions/frames/index.ts +0 -76
- package/app/extensions/mcp/guide.md +0 -5
- package/app/extensions/point/README.md +0 -22
- package/app/extensions/point/browser.js +0 -245
- package/app/extensions/point/guide.md +0 -6
- package/app/extensions/point/index.ts +0 -97
- package/app/extensions/prompt/index.ts +0 -19
- package/app/extensions/ui/README.md +0 -69
- package/app/extensions/ui/components/App.js +0 -28
- package/app/extensions/ui/components/Button.js +0 -28
- package/app/extensions/ui/components/Markdown.js +0 -37
- package/app/extensions/ui/guide.md +0 -7
- package/app/extensions/ui/index.ts +0 -271
- package/app/extensions/viewer/guide.md +0 -5
- package/app/mcp.json +0 -3
- package/app/skills/plugin-map/SKILL.md +0 -49
- package/app/ui/apps/.keep +0 -0
- /package/{app/extensions/ui/components → agent/extensions/card/blocks}/Table.js +0 -0
- /package/{app/extensions/ui/components → agent/extensions/card/blocks}/Text.js +0 -0
package/README.md
CHANGED
|
@@ -26,7 +26,6 @@ Then open http://127.0.0.1:3000.
|
|
|
26
26
|
| `MODEL_BASE_URL` | the endpoint, up to `/v1` |
|
|
27
27
|
| `MODEL_API_KEY` | its key |
|
|
28
28
|
| `MODEL_NAME` | the model (default: `default`) |
|
|
29
|
-
| `MODEL_THINKING` | optional thinking level: `minimal`, `low`, `medium`, `high` |
|
|
30
29
|
| `CHALKY_HOME` | where Chalky keeps its things (default: `~/.chalky`) |
|
|
31
30
|
| `UI_HOST` | the address to serve on (default: `127.0.0.1`, this machine only) |
|
|
32
31
|
| `PORT` | the port to serve on (default: `3000`) |
|
|
@@ -35,13 +34,18 @@ Then open http://127.0.0.1:3000.
|
|
|
35
34
|
|
|
36
35
|
`~/.chalky` (or `CHALKY_HOME`) holds:
|
|
37
36
|
|
|
38
|
-
- `
|
|
39
|
-
|
|
40
|
-
- `
|
|
41
|
-
- `
|
|
37
|
+
- `agent/`: what the agents are: their prompt, settings and extensions (the page among them), copied from the
|
|
38
|
+
package on the first run. The agents edit it. A newer version of the package doesn't overwrite it.
|
|
39
|
+
- `built/`: what the agents made for you: the cards on your boards, and the apps they wrote.
|
|
40
|
+
- `data/`: what is kept while running: conversations, what you typed into cards and apps, notes, undo history.
|
|
41
|
+
- `workspace/`: the agents' working folder; `workspace/drops/` holds your files.
|
|
42
|
+
|
|
43
|
+
`agent/` and `built/` share one version history: each agent turn that changes them is saved, and a rollback puts
|
|
44
|
+
both back. A home from before this layout (`app/`, `state/`) is moved into it on the first start; the old code
|
|
45
|
+
stays in `app-before-reorg/`.
|
|
42
46
|
|
|
43
47
|
## MCP servers
|
|
44
48
|
|
|
45
|
-
Tools from MCP servers go in `~/.chalky/
|
|
49
|
+
Tools from MCP servers go in `~/.chalky/agent/mcp.json`, in pi's format (stdio or streamable HTTP; see pi's
|
|
46
50
|
`docs/mcp.md`). Type `/mcp` on the page to choose which servers and tools the agents may use. Keep secrets in
|
|
47
51
|
environment variables and refer to them as `${NAME}` in `headers` or `env`.
|
package/{app → agent}/SYSTEM.md
RENAMED
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
You are an agent in Chalky. You build what the user sees: the page is a workspace where what you build (
|
|
2
|
-
|
|
1
|
+
You are an agent in Chalky. You build what the user sees: the page is a workspace where what you build (cards on
|
|
2
|
+
boards, each showing blocks or an app) fills the screen, and the conversation is a side panel. The user says what they want; you put a
|
|
3
3
|
working first version on screen; then they point at things on it and leave notes, and you change exactly that.
|
|
4
4
|
- Build, don't describe. A first version they can point at beats questions you could answer by building.
|
|
5
5
|
- Your final reply is the only text they read in full: one or two sentences on what changed.
|
|
6
6
|
- The page around your work is the user's: change it only when asked. Build inside the main space.
|
|
7
7
|
|
|
8
8
|
What follows this: the skills you can load, your working folder (<cwd>: bash runs there), one short section for
|
|
9
|
-
each part of you that has something to say (each comes from an extension), and <paths>:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
each part of you that has something to say (each comes from an extension), and <paths>: your four folders, the
|
|
10
|
+
host and pi's docs. In README.md files and skills, {agent}/..., {built}/..., {data}/... and {workspace}/... are paths
|
|
11
|
+
in those folders. A section that names a README.md is the short version of its extension: read that file when the
|
|
12
|
+
section says, once (it stays in your conversation). Some tools are loaded only when needed: tool_search finds and
|
|
13
|
+
loads them (card_blocks, viewer_read, screen_see, team_reset, MCP servers' tools). Tools you already have need no
|
|
14
|
+
search.
|
|
15
|
+
Two kinds of change, two places: what the user asks you to make (a card, an app) goes in {built}; a change to how
|
|
16
|
+
Chalky itself works is a change to you, in {agent}.
|
|
13
17
|
|
|
14
18
|
Your tools:
|
|
15
|
-
- read to look at files and pictures (not cat or sed); bash for ls,
|
|
19
|
+
- read to look at files and pictures (not cat or sed); bash for ls, grep, find and running things.
|
|
16
20
|
- edit for precise changes: each edits[].oldText must match the file exactly and only once; keep it as small as
|
|
17
21
|
that allows. Several changes in one file: one edit call with several entries, each matched against the original
|
|
18
22
|
file, none overlapping. An edit whose newText equals its oldText changes nothing and is refused.
|
|
@@ -20,10 +24,11 @@ Your tools:
|
|
|
20
24
|
- Name files by their full path when you mention them.
|
|
21
25
|
|
|
22
26
|
Changing yourself: when the work needs it, you can change any part of you, the page included. You are pi
|
|
23
|
-
extensions, one folder each: {
|
|
24
|
-
ExtensionAPI) { ... }`, which registers tools (pi.registerTool)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
extensions, one folder each: {agent}/extensions/<name>/. In it, index.ts is `export default function (pi:
|
|
28
|
+
ExtensionAPI) { ... }`, which registers tools (pi.registerTool) and event handlers (pi.on); and as the extension
|
|
29
|
+
needs them, guide.md (its prompt section, found by itself), README.md (the whole of it), browser.js (its part of
|
|
30
|
+
the page, found by itself) and skills/. The page itself is the page extension; this text is {agent}/SYSTEM.md;
|
|
31
|
+
pi's settings (e.g. compaction) are {agent}/settings.json (pi's docs/settings.md). Extensions reach
|
|
27
32
|
shared state and the web server through globalThis.chalky: the host, the fixed core under them (don't edit it).
|
|
28
33
|
Read pi's extension docs (<paths>) before writing or changing an extension. After editing, call the reload tool:
|
|
29
34
|
your extensions load again when your turn ends, and your conversation is kept. A changed page or browser.js shows
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Making an app (the app extension)
|
|
2
|
+
Everything a usual app needs is here: for one, you don't need the card README or the design and browser-check skills.
|
|
3
|
+
|
|
4
|
+
## Write it, show it
|
|
5
|
+
- Write {built}/apps/<name>/index.html (other files next to it), then card_show(card=<id>, app=<name>, about="...").
|
|
6
|
+
It is served at {page}/built/apps/<name>/. card_show checks it on the board and reports errors and whether it
|
|
7
|
+
fits its card: fix those and show it again. height=<px>: its height when nothing else sizes it (default 400).
|
|
8
|
+
- about: one or two sentences on what it is for and any decision a later change should respect (its back side).
|
|
9
|
+
- Name what the user may point at: ids or data-name on elements ("habit-Read", not "row3"). A canvas: give the
|
|
10
|
+
page's window a partAt(x, y) returning {"name", ...} for what is at that spot.
|
|
11
|
+
- The user moves and resizes the card: fill the space (%, flex, grid, an SVG with a viewBox), not fixed px.
|
|
12
|
+
- A pinned card (the user held its label): card_show on it fails. Make a new card (made_from=[it]) or ask.
|
|
13
|
+
- Changing it later: edit the part (the edit tool), not a rewrite. App files show when your turn ends.
|
|
14
|
+
|
|
15
|
+
## Keep its data
|
|
16
|
+
`import agents from "/app/agents.js"` (a module script). `await agents.load()` gives the saved data ({} at first);
|
|
17
|
+
`agents.save(data)` replaces it after every change (on the server, {data}/app/<name>.json: kept across reloads, the
|
|
18
|
+
same in every browser, readable by you with cat). Other cards showing the same app hear each save:
|
|
19
|
+
`agents.on("app_data", ({app, data}) => { if (app === agents.app) ... })` (the field is `data`). Never localStorage
|
|
20
|
+
(one browser only, invisible to you). To change it yourself: curl -X PUT --data @new.json {page}/app/data/<name>.
|
|
21
|
+
|
|
22
|
+
## An app that asks an AI (a plan from a goal, a summary, an answer)
|
|
23
|
+
A real agent does it, never a canned sample. While building, spawn one for the card: team_spawn(role, name="planner",
|
|
24
|
+
app=<card id>), its role ending "Answer only with JSON like {...}. Don't use tools." In the app:
|
|
25
|
+
```
|
|
26
|
+
const asked = Date.now() / 1000; agents.send("planner", goal);
|
|
27
|
+
agents.on("message", (ev) => { // events replay on connect: take only the answer to this question
|
|
28
|
+
if (ev.agent !== "planner" || ev.role !== "assistant" || ev.tool_calls?.length || ev.ts < asked) return;
|
|
29
|
+
data.plan = JSON.parse(ev.text.slice(ev.text.indexOf("{"), ev.text.lastIndexOf("}") + 1)); agents.save(data); draw();
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
Show that it is thinking while it waits; agents.send is limited per minute. Agents with personalities that talk to
|
|
33
|
+
each other (a debate, a simulation): load the app-agents skill.
|
|
34
|
+
|
|
35
|
+
## Apps that work together
|
|
36
|
+
Each card is its own page: a click in one never reaches another. One app saves what was picked
|
|
37
|
+
(`agents.save({...data, picked: "Goblet squat"})`); the other hears every app's saves:
|
|
38
|
+
`agents.on("app_data", ({app, data}) => { if (app === "fitflow") show(data.picked) })`, and can read the first one's
|
|
39
|
+
data at start with `fetch("/app/data/fitflow").then((r) => r.json())`.
|
|
40
|
+
|
|
41
|
+
## Look
|
|
42
|
+
The page is light, glassy and quiet; match it unless the user asks for another look:
|
|
43
|
+
- Transparent background; the system font (`font: inherit` or system-ui); text #1d1d1f, muted rgba(60,60,67,.55),
|
|
44
|
+
accent #0071e3, danger #e5484d, lines rgba(0,0,0,.06), glass rgba(255,255,255,.7), radius 16px.
|
|
45
|
+
- Show just the thing: no frame or panel around it unless it needs one. Group with flex/grid and gap.
|
|
46
|
+
- Open in a working state (its real data, or a marked example), never an empty shell; `tabular-nums` for digits.
|
|
47
|
+
- Buttons say what happens ("Add habit"); short plain words; status colours always with a word or icon.
|
|
48
|
+
- Avoid the generic AI look: cream and terracotta, purple gradients, emoji as markers, the same shadowed card on
|
|
49
|
+
everything. For a chart, a diagram, a game's art or a restyle, load the design skill.
|
|
50
|
+
|
|
51
|
+
## Check it works
|
|
52
|
+
card_show covers errors and fit. Before telling the user what it does, try its main action and a reload with
|
|
53
|
+
{agent}/skills/browser-check/scripts/check.mjs:
|
|
54
|
+
```
|
|
55
|
+
node <that check.mjs> {page}/built/apps/<name>/index.html "eval:(()=>{const i=document.querySelector('#new'); i.value='Test-1';
|
|
56
|
+
i.dispatchEvent(new Event('input',{bubbles:true})); i.form.requestSubmit(); return 'ok'})()" wait:800 \
|
|
57
|
+
"eval:document.body.innerText.includes('Test-1')"
|
|
58
|
+
```
|
|
59
|
+
Set values with eval (`type:` adds to what a field already holds). Run it again to see what a reload keeps.
|
|
60
|
+
Your test writes the user's real data: note it first (cat {data}/app/<name>.json) and put it back afterwards
|
|
61
|
+
(PUT, above). Say only what you saw working.
|
|
62
|
+
|
|
63
|
+
## More
|
|
64
|
+
- Calculations (totals, streaks, dates) belong in the page's code, never worked out by you and typed in.
|
|
65
|
+
- Voice input: import /voice/voice.js (read its header), not the browser's speech API.
|
|
66
|
+
- Libraries: put an ES-module build in {agent}/extensions/card/lib/<pkg>/ and import /card/lib/<pkg>/<file>.js.
|
|
67
|
+
- agents.action(name, context) wakes the agent that owns the card (for what truly needs you); the rest of the
|
|
68
|
+
bridge: the header of {agent}/extensions/app/agents.js.
|
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
// Bridge from an app ({
|
|
2
|
-
// import agents from "/
|
|
1
|
+
// Bridge from an app ({built}/apps/<name>/, shown on a card) to the agents. An app imports it:
|
|
2
|
+
// import agents from "/app/agents.js";
|
|
3
3
|
// agents.send(to, text) a message to an agent, from "app"; its answer comes as a "message" event
|
|
4
4
|
// (role "assistant", no tool_calls). Each one is a model call: limited per minute.
|
|
5
|
-
// agents.action(name, context) an action for the agent that owns this app's
|
|
5
|
+
// agents.action(name, context) an action for the agent that owns this app's card, like a Button
|
|
6
6
|
// agents.on(type, fn) events as the page gets them, past ones first (replayed on connect), then live:
|
|
7
|
-
// "message" {agent, role, content, tool_calls, ts}: every message of every agent
|
|
8
|
-
//
|
|
7
|
+
// "message" {agent, role, content, tool_calls, ts, to, text}: every message of every agent;
|
|
8
|
+
// to and text: its "[to: <name>]" tag ("all" without one) and the words after it;
|
|
9
|
+
// incl. its team_ask / team_tell calls (who talks to whom); "status" {agents}: who is busy
|
|
9
10
|
// agents.list() {name: {role, app, state, doing, queued}}, from the latest "status" event
|
|
10
|
-
// agents.reset() empty the memories of this app's agents (spawned with app=<this
|
|
11
|
+
// agents.reset() empty the memories of this app's agents (spawned with app=<this card>),
|
|
11
12
|
// roles kept: a fresh round. Refused (an "error" event) while one is busy.
|
|
12
|
-
// agents.load() this app's saved data ({} at first),
|
|
13
|
+
// agents.load() this app's saved data ({} at first), kept by the app extension (on the server)
|
|
13
14
|
// agents.save(data) replace it; every page and app frame then gets "app_data" {app, data}, so
|
|
14
|
-
// the same app
|
|
15
|
-
// agents.app this app's name: its folder in {
|
|
15
|
+
// the same app on other cards updates (check ev.app === agents.app)
|
|
16
|
+
// agents.app this app's name: its folder in {built}/apps
|
|
16
17
|
// It opens its own WebSocket to /ws, the page's protocol (the page extension).
|
|
17
18
|
const ws = new WebSocket((location.protocol === "https:" ? "wss://" : "ws://") + location.host + "/ws");
|
|
18
19
|
const open = new Promise((resolve) => ws.addEventListener("open", resolve));
|
|
@@ -22,23 +23,28 @@ let status = {};
|
|
|
22
23
|
ws.onmessage = (e) => {
|
|
23
24
|
const ev = JSON.parse(e.data);
|
|
24
25
|
if (ev.type === "status") status = ev.agents;
|
|
26
|
+
if (ev.type === "message" && typeof ev.content === "string") { // an app agent's "[to: <name>] ..." answer, read once here
|
|
27
|
+
const tag = /^\s*\[to:\s*([^\]]+)\]\s*/i.exec(ev.content);
|
|
28
|
+
ev.to = tag ? tag[1].trim() : "all";
|
|
29
|
+
ev.text = tag ? ev.content.slice(tag[0].length) : ev.content;
|
|
30
|
+
}
|
|
25
31
|
for (const fn of listeners[ev.type] || []) fn(ev);
|
|
26
32
|
};
|
|
27
33
|
|
|
28
|
-
// In the
|
|
34
|
+
// In the version extension's screenshot (/?shot) the app is only looked at: send nothing, no model calls.
|
|
29
35
|
const shot = new URLSearchParams(window.top.location.search).has("shot");
|
|
30
36
|
const post = async (obj) => { if (shot) return; await open; ws.send(JSON.stringify(obj)); };
|
|
31
|
-
const app = location.pathname.split("/")[3]; // /
|
|
32
|
-
const store = `/
|
|
33
|
-
// the
|
|
34
|
-
const
|
|
37
|
+
const app = location.pathname.split("/")[3]; // /built/apps/<name>/...
|
|
38
|
+
const store = `/app/data/${app}`;
|
|
39
|
+
// the card this app is shown on (its frame sits inside the card on the page)
|
|
40
|
+
const card = () => window.frameElement?.closest("[data-card]")?.dataset.card;
|
|
35
41
|
|
|
36
42
|
export default {
|
|
37
43
|
send: (to, text) => post({ type: "app_message", to, text }),
|
|
38
|
-
action: (name, context = {}) => post({ type: "
|
|
44
|
+
action: (name, context = {}) => post({ type: "card_action", card: card(), block: "app", name, context }),
|
|
39
45
|
on: (type, fn) => (listeners[type] ??= []).push(fn),
|
|
40
46
|
list: () => status,
|
|
41
|
-
reset: () => post({ type: "app_reset",
|
|
47
|
+
reset: () => post({ type: "app_reset", card: card() }),
|
|
42
48
|
app,
|
|
43
49
|
load: async () => (await fetch(store, { cache: "no-store" })).json(),
|
|
44
50
|
save: async (data) => { if (!shot) await fetch(store, { method: "PUT", body: JSON.stringify(data) }); },
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Apps
|
|
2
|
+
An app is a page you write in {built}/apps/<name>/ and show on a card (card_show with app). It keeps what the user
|
|
3
|
+
makes in it (notes, lists, picks) with /app/agents.js (agents.load, agents.save), on the server and shared by every
|
|
4
|
+
card showing it; never localStorage (one browser only, and you can't see it).
|
|
5
|
+
Before writing or changing an app, read README.md: it has all an app needs (showing it, its data, its look, checking it).
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Apps: pages the agents write in built/apps/<name>/ (the host serves them at /built/apps/<name>/), shown on a card
|
|
2
|
+
// (the card extension draws the frame). This extension gives apps what a plain page can't have:
|
|
3
|
+
// - their data, kept on the server: one JSON value per app in data/app/<name>.json, so it outlives the page, is
|
|
4
|
+
// the same in every browser, and the agents can read it. Whole value replaced each time: the last save wins
|
|
5
|
+
// (keep one app's data in one value). The same app on several cards shares it: every save is an "app_data" event;
|
|
6
|
+
// - agents.js, their bridge to the agents (load, save, send, action, reset, on: read its header);
|
|
7
|
+
// - the app-agents skill (apps with agents inside, e.g. a simulation's members).
|
|
8
|
+
// GET /app/data/<name> the app's data ({} if none yet) GET /app/agents.js the bridge
|
|
9
|
+
// PUT /app/data/<name> replace it (JSON, at most MAX_BYTES); event "app_data" {app, data}
|
|
10
|
+
// page messages app_message {to, text}: to an agent, from "app" (at most MESSAGES_PER_MINUTE, all apps together)
|
|
11
|
+
// app_reset {card}: empty the memories of the agents spawned for the app on that card (the team's reset)
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
|
|
16
|
+
const host = (globalThis as any).chalky;
|
|
17
|
+
const DIR = path.join(host.data, "app");
|
|
18
|
+
const MAX_BYTES = 1024 * 1024;
|
|
19
|
+
const MESSAGES_PER_MINUTE = 30;
|
|
20
|
+
const sent: number[] = host.shared.get("app-sent") ?? []; // when apps sent messages (on the host: outlives reloads)
|
|
21
|
+
host.shared.set("app-sent", sent);
|
|
22
|
+
const json = (res: any, v: unknown, status = 200) => res.writeHead(status, { "content-type": "application/json" }).end(JSON.stringify(v));
|
|
23
|
+
|
|
24
|
+
export default function (pi: ExtensionAPI) {
|
|
25
|
+
pi.on("resources_discover", () => ({ skillPaths: [path.join(import.meta.dirname, "skills")] }));
|
|
26
|
+
|
|
27
|
+
host.routes.set("/app/agents.js", (_req: any, res: any) => host.file(res, path.join(import.meta.dirname, "agents.js")));
|
|
28
|
+
host.routes.set("/app/data/", (req: any, res: any) => {
|
|
29
|
+
const name = decodeURIComponent(new URL(req.url, "http://x").pathname.slice("/app/data/".length));
|
|
30
|
+
if (!/^[\w-]+$/.test(name)) return json(res, { error: "app names are letters, digits, _ and -" }, 400);
|
|
31
|
+
const p = path.join(DIR, name + ".json");
|
|
32
|
+
if (req.method === "GET") {
|
|
33
|
+
if (!fs.existsSync(p)) return json(res, {});
|
|
34
|
+
return res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" }).end(fs.readFileSync(p));
|
|
35
|
+
}
|
|
36
|
+
if (req.method !== "PUT") return res.writeHead(405).end();
|
|
37
|
+
const chunks: Buffer[] = [];
|
|
38
|
+
let size = 0;
|
|
39
|
+
req.on("data", (c: Buffer) => { size += c.length; if (size <= MAX_BYTES) chunks.push(c); });
|
|
40
|
+
req.on("end", () => {
|
|
41
|
+
if (size > MAX_BYTES) return json(res, { error: `over ${MAX_BYTES / 1024} KB` }, 400);
|
|
42
|
+
let data;
|
|
43
|
+
try { data = JSON.parse(Buffer.concat(chunks).toString("utf8")); } catch (e: any) { return json(res, { error: `not JSON: ${e.message}` }, 400); }
|
|
44
|
+
fs.mkdirSync(DIR, { recursive: true });
|
|
45
|
+
fs.writeFileSync(p + ".tmp", JSON.stringify(data));
|
|
46
|
+
fs.renameSync(p + ".tmp", p);
|
|
47
|
+
host.broadcast("app_data", { app: name, data });
|
|
48
|
+
json(res, { saved: true });
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
host.pageHandlers.set("app", async (msg: any) => {
|
|
53
|
+
const team = host.api.team, error = (text: string) => host.broadcast("error", { agent: "app", text });
|
|
54
|
+
if (msg.type === "app_reset" && team) {
|
|
55
|
+
const names = [...host.agents.values()].filter((x: any) => x.app === msg.card).map((x: any) => x.name);
|
|
56
|
+
const problem = await team.reset(names);
|
|
57
|
+
if (problem) error(`reset of ${msg.card}: ${problem}`);
|
|
58
|
+
} else if (msg.type === "app_message" && team) {
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
sent.splice(0, sent.length, ...sent.filter((t) => now - t < 60000));
|
|
61
|
+
if (!host.agents.has(msg.to)) error(`app message to unknown agent '${msg.to}'`);
|
|
62
|
+
else if (sent.length >= MESSAGES_PER_MINUTE) error(`app message to ${msg.to} dropped: more than ${MESSAGES_PER_MINUTE} a minute`);
|
|
63
|
+
else { sent.push(now); team.send("app", msg.to, msg.text ?? ""); }
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
package/{app/extensions/ui/skills/agent-apps → agent/extensions/app/skills/app-agents}/SKILL.md
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description: Build an app whose members are real AI agents (a simulated board meeting, a debate, a negotiation, a panel of experts, game characters), run and shown by the app itself, in turns or all at once. Use when the user wants agents inside what you build, as opposed to agents helping you build it.
|
|
2
|
+
name: app-agents
|
|
3
|
+
description: Build an app whose members are real AI agents (a simulated board meeting, a debate, a negotiation, a panel of experts, game characters), run and shown by the app itself, in turns or all at once. Use when the user wants agents inside what you build, as opposed to agents helping you build it. (One agent that just answers the app's questions: the app README's recipe is enough.)
|
|
4
4
|
---
|
|
5
5
|
# Apps with real agents inside
|
|
6
6
|
|
|
@@ -8,8 +8,8 @@ description: Build an app whose members are real AI agents (a simulated board me
|
|
|
8
8
|
| | Worker agents | App agents |
|
|
9
9
|
|-------------------|---------------------------------|----------------------------------------------|
|
|
10
10
|
| What for | help you build or fix the app | are part of the app (a board member, a debater) |
|
|
11
|
-
| Spawn | `
|
|
12
|
-
| Who talks to them | you (`
|
|
11
|
+
| Spawn | `team_spawn(role)` | `team_spawn(role, name, app="<card>")` |
|
|
12
|
+
| Who talks to them | you (`team_tell`, `team_ask`) | the app, through `/app/agents.js` |
|
|
13
13
|
| Messages panel | shown | hidden: the app shows their talk |
|
|
14
14
|
| When they run | while you build | each time the user runs the app |
|
|
15
15
|
|
|
@@ -19,7 +19,7 @@ is what keeps app agents out of the user's messages panel and lets the app reset
|
|
|
19
19
|
## Why the app runs the activity, not you
|
|
20
20
|
Your context is large (system prompt plus everything you did while building): each of your model calls
|
|
21
21
|
costs all of it, often 50k tokens or more. If you relay a meeting yourself (ask the chair who's next, ask
|
|
22
|
-
that member, `
|
|
22
|
+
that member, `card_update` the transcript), every line spoken costs two or three of your calls. The app
|
|
23
23
|
can talk to its agents directly, and their contexts are small. So you build, fix, and answer the user;
|
|
24
24
|
at run time you make no calls at all.
|
|
25
25
|
|
|
@@ -28,17 +28,17 @@ at run time you make no calls at all.
|
|
|
28
28
|
stance and personality. End each role with: "You are a character in <app>. Answer in plain text,
|
|
29
29
|
briefly, starting with [to: <name>] or [to: all]. Don't use tools." App agents have the same tools
|
|
30
30
|
you have; this keeps them in character, fast, and gives the app a "to" to draw.
|
|
31
|
-
2. **Build
|
|
32
|
-
`
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
string as the surface id. In the app, check `agents.list()`: if a member is missing, show that
|
|
31
|
+
2. **Build it as an app**: `{built}/apps/<name>/index.html` and its scripts, shown on a card with
|
|
32
|
+
`card_show(card="<card>", app="<name>")`.
|
|
33
|
+
An app can import `/app/agents.js`; the bridge finds the app's card from the frame it runs in.
|
|
34
|
+
3. **Spawn the members once**, while building: `team_spawn(role, name=..., app="<card>")`, the same
|
|
35
|
+
string as the card id. In the app, check `agents.list()`: if a member is missing, show that
|
|
37
36
|
instead of failing.
|
|
38
|
-
4. **Wire the app with `/
|
|
37
|
+
4. **Wire the app with `/app/agents.js`** (read its header: {agent}/extensions/app/agents.js):
|
|
39
38
|
- `send(to, text)`: ask a member. Its answer arrives as a `"message"` event: `agent` is that member,
|
|
40
39
|
`role` is `"assistant"`, and there are no `tool_calls` (its final answer).
|
|
41
|
-
- `on("message", fn)`: draw each answer as it arrives (speaker,
|
|
40
|
+
- `on("message", fn)`: draw each answer as it arrives: `ev.agent` (the speaker), `ev.to` (its target,
|
|
41
|
+
"all" when it gave none) and `ev.text` (the answer without its `[to: …]` tag; show this, not `ev.content`).
|
|
42
42
|
- `reset()`: a fresh round, members' memories empty and roles kept. Call it before each run if the
|
|
43
43
|
user wants rounds to start fresh.
|
|
44
44
|
- `action(name, context)`: only for what needs you, e.g. "add a member" (you spawn it).
|
|
@@ -48,11 +48,11 @@ at run time you make no calls at all.
|
|
|
48
48
|
- All at once: send to several members together; answers come back in any order, so draw each one
|
|
49
49
|
as it arrives.
|
|
50
50
|
Members only know what you send them: include the transcript so far (or its last lines) each time.
|
|
51
|
-
6. **Check it** with the
|
|
51
|
+
6. **Check it** with the browser-check skill: one short run, budget 2 or 3 messages (each is a real model call).
|
|
52
52
|
|
|
53
53
|
## The ask helper (copy into the app)
|
|
54
54
|
```js
|
|
55
|
-
import agents from "/
|
|
55
|
+
import agents from "/app/agents.js";
|
|
56
56
|
const waiting = {}; // member -> resolve() for its next final answer
|
|
57
57
|
agents.on("message", (ev) => {
|
|
58
58
|
if (ev.role === "assistant" && !ev.tool_calls?.length && waiting[ev.agent]) {
|
|
@@ -76,7 +76,7 @@ answer from an earlier run must not resolve a new question.
|
|
|
76
76
|
with play, pause and a scrubber.
|
|
77
77
|
- After a reload, past events replay: rebuild earlier runs from them (only your members' messages).
|
|
78
78
|
A member's `"system"` message means a fresh memory, i.e. a new round began.
|
|
79
|
-
- Don't keep the transcript in the
|
|
79
|
+
- Don't keep the transcript in the card's data with `card_update`: that is you relaying again.
|
|
80
80
|
|
|
81
81
|
## Limits
|
|
82
82
|
- App messages: at most 30 a minute (each is a model call); more are dropped with an `"error"` event.
|
|
@@ -86,6 +86,5 @@ answer from an earlier run must not resolve a new question.
|
|
|
86
86
|
|
|
87
87
|
## Pitfalls
|
|
88
88
|
- Disable Start while a run is going: a double click starts two runs.
|
|
89
|
-
- An answer without a `[to: …]` tag: treat it as to all.
|
|
90
89
|
- If the app shows nothing, the panel won't help (app agents are hidden there): check the app with
|
|
91
|
-
|
|
90
|
+
browser-check, e.g. `eval:` the length of its transcript, and look for `"error"` events.
|