ghostykit 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -57
- package/dist/chunks/chunk-DXFWACUL.js +85 -0
- package/dist/chunks/chunk-HPTM2BCM.js +472 -0
- package/dist/core/index.d.ts +2 -3
- package/dist/core/index.js +13 -23
- package/dist/core/reducer.d.ts +27 -45
- package/dist/core/steps.d.ts +9 -1
- package/dist/core/types.d.ts +36 -160
- package/dist/react/Bubble.d.ts +2 -6
- package/dist/react/Composer.d.ts +6 -1
- package/dist/react/ConfirmCard.d.ts +3 -4
- package/dist/react/Piloto.d.ts +3 -10
- package/dist/react/Status.d.ts +1 -13
- package/dist/react/index.d.ts +6 -6
- package/dist/react/index.js +218 -412
- package/dist/react/strings.d.ts +2 -18
- package/dist/react/usePilot.d.ts +31 -43
- package/dist/styles.css +2 -103
- package/dist/transport/index.d.ts +40 -30
- package/dist/transport/index.js +3 -13
- package/package.json +17 -9
- package/dist/chunks/chunk-O6FCE4CS.js +0 -670
- package/dist/chunks/chunk-XZHX2NZT.js +0 -375
- package/dist/core/reconcile.d.ts +0 -7
- package/dist/react/connectingGate.d.ts +0 -17
- package/dist/transport/postStream.d.ts +0 -44
- package/dist/transport/types.d.ts +0 -47
package/README.md
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Ghosty Autopilot · `ghostykit`
|
|
2
2
|
|
|
3
|
-
**An
|
|
3
|
+
**An autopilot for your React app, not a copilot or kit.** You don't drive: you say where you're going and
|
|
4
|
+
the agent flies the app for you, stopping only to ask before anything risky. Ghosty Autopilot is the agent chat from
|
|
4
5
|
[Ghosty Studio](https://www.ghosty.studio) (`/c`), packaged as a library: same components, same
|
|
5
6
|
markup, same look ([side by side](https://raw.githubusercontent.com/blissito/ghostykit/main/docs/screenshots/parity/desktop-done-gs-vs-ghostykit.png)). You drop it into an app you already
|
|
6
7
|
have. It sees what the user has on screen, streams what the agent says and does, and **stops to ask
|
|
7
8
|
before anything risky** (human in the loop).
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
It speaks **[AG-UI](https://docs.ag-ui.com)**, the open agent ↔ app protocol, so it works with
|
|
11
|
+
**LangGraph**, Mastra, CrewAI, Pydantic AI, Google ADK, or your own backend. No adapter of ours in
|
|
12
|
+
between.
|
|
13
|
+
|
|
14
|
+
> El piloto de tu app, no el copiloto: tú no manejas, vas de pasajero y le dices a dónde ir. Es el chat de Ghosty, probado en producción, como biblioteca. Español por default, inglés incluido.
|
|
10
15
|
|
|
11
16
|
**[Open the playground →](https://blissito.github.io/ghostykit/)** (a fake backend, a real chat)
|
|
12
17
|
|
|
@@ -18,9 +23,13 @@ before anything risky** (human in the loop).
|
|
|
18
23
|
- **Agent steps**: every tool call as a line ("Read 2 files", "Searched the web"). Steps fold into a card when the turn ends, and you can show your own labels and icons.
|
|
19
24
|
- **The permission pass**: risky actions arrive as a card with a countdown, *Approve* and *Reject*. A stamp lands when you answer, and the decision stays recorded under the reply.
|
|
20
25
|
- **Context and references**: what the user is looking at travels with every turn. Rows from your app can be "@-mentioned" into the message.
|
|
21
|
-
- **Resilience**:
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
- **Resilience**: the thread lives on your server. A dropped connection or a reload asks for it again
|
|
27
|
+
and repaints from `MESSAGES_SNAPSHOT`, pending approvals included; a deploy (`503`) is retried with
|
|
28
|
+
the same run.
|
|
29
|
+
- **The composer**: attachments (you provide `upload`), voice notes (you provide `transcribe`), stop and reply-option buttons.
|
|
30
|
+
- **Solid core**: the whole chat state is a pure reducer over AG-UI events, `reduceAgUiEvent(state, event)`, tested by replaying streams recorded from a real LangGraph agent.
|
|
31
|
+
|
|
32
|
+
> **En español:** Ghosty Autopilot le pone piloto a tu app con un solo componente: la persona dice qué quiere y el agente lo hace por ella. Tus usuarios ven la respuesta escribirse en vivo y cada paso del agente («Leí 2 archivos»), y aprueban o rechazan cualquier acción riesgosa con una tarjeta antes de que ocurra. El agente sabe qué pantalla y qué registro está viendo la persona. Habla AG-UI, el protocolo abierto entre agentes y apps, así que funciona con LangGraph, Mastra, CrewAI o tu propio backend. El chat se recupera solo de cortes de red y de deploys, y trae adjuntos, notas de voz y botón para detener. Por dentro, todo el estado es una función pura probada con sesiones reales, así que se puede actualizar sin miedo.
|
|
24
33
|
|
|
25
34
|
## 30-second quickstart
|
|
26
35
|
|
|
@@ -29,70 +38,47 @@ npm i ghostykit motion streamdown
|
|
|
29
38
|
```
|
|
30
39
|
|
|
31
40
|
```tsx
|
|
32
|
-
import { Piloto,
|
|
41
|
+
import { Piloto, createAgUiTransport } from "ghostykit";
|
|
33
42
|
import "ghostykit/styles.css";
|
|
34
|
-
import "ghostykit/fonts.css"; // optional: the house typefaces
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
const transport = createAgUiTransport({ url: "/api/agent" }); // any AG-UI endpoint
|
|
45
|
+
|
|
46
|
+
export function Pilot() {
|
|
38
47
|
return <Piloto transport={transport} />;
|
|
39
48
|
}
|
|
40
49
|
```
|
|
41
50
|
|
|
42
51
|
`<Piloto transport={…} />` works with nothing else. Give its container a height: Piloto fills it.
|
|
52
|
+
The product is **Ghosty Autopilot**; the component is `<Piloto>`.
|
|
43
53
|
|
|
44
|
-
##
|
|
45
|
-
|
|
46
|
-
A transport is how Piloto talks to your agent. It is a small interface: `subscribe`, `send`, `cancel`,
|
|
47
|
-
plus optional `setModel`, `respondPermission` and `history`. Two are included.
|
|
48
|
-
|
|
49
|
-
### 1. Split stream: `createSseTransport(base)` (Ghosty's contract)
|
|
50
|
-
|
|
51
|
-
The event stream stays open (`EventSource`) and messages go by POST.
|
|
54
|
+
## Works with LangGraph (and any AG-UI agent)
|
|
52
55
|
|
|
53
|
-
|
|
54
|
-
|---|---|
|
|
55
|
-
| `GET {base}/events` | Named SSE events: `started`, `status`, `chunk`, `thought`, `tool`, `permission`, `permission-resolved`, `usage`, `models`, `title`, `done{reposo?,interrumpido?,ultimo?}`, `error`… |
|
|
56
|
-
| `POST {base}/messages` | `{content, images, primerTurno, retomar?, context?}` → `{inyectado?, estado?, turnId?}` |
|
|
57
|
-
| `POST {base}/cancel` · `/model` · `/permission` | stop, switch model, answer a card |
|
|
56
|
+
Serve your LangGraph graph with its official AG-UI adapter and point Piloto at it:
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
```ts
|
|
59
|
+
// server: one POST endpoint (full file in examples/langgraph/src/server.ts)
|
|
60
|
+
import { LangGraphAgent } from "@ag-ui/langgraph";
|
|
61
|
+
import { EventEncoder } from "@ag-ui/encoder";
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
| | |
|
|
67
|
-
|---|---|
|
|
68
|
-
| `POST {url}` | `{text, context?, turnId, continueTurnId?}` → SSE `started`, `chunk`, `tool{title,status}`, `permission{…, expiresAt}`, `done{turnId,text}`, `error`, `capacity`, `interrupted{turnId,canContinue}`, `idle` |
|
|
69
|
-
| `GET {url}?attach=1` | `started`, one `permission` per open card, `snapshot{turnId,text,tools}`, then live |
|
|
70
|
-
| `GET {url}` | `{messages:[{role,text}]}`, loaded as the thread |
|
|
71
|
-
|
|
72
|
-
A 503 (deploy draining) is retried with the same `turnId`. A 409 `turn_busy` re-attaches to the turn
|
|
73
|
-
that is already running. Approvals usually go through your own route:
|
|
63
|
+
const agent = new LangGraphAgent({ graphId: "crons", deploymentUrl: "http://localhost:2024", emitInterruptOutcome: true });
|
|
64
|
+
// POST /agent → agent.run(runAgentInput).subscribe(e => res.write(new EventEncoder().encode(e)))
|
|
65
|
+
```
|
|
74
66
|
|
|
75
67
|
```tsx
|
|
76
|
-
<Piloto
|
|
77
|
-
transport={useMemo(() => createPostStreamTransport({ url: "/api/v2/admin/copilot/chat" }), [])}
|
|
78
|
-
context={{ page: "crons", job: selectedJob }}
|
|
79
|
-
onPermission={async (id, optionId) => {
|
|
80
|
-
const res = await fetch(`/api/v2/admin/agent-actions/${id}`, {
|
|
81
|
-
method: "POST",
|
|
82
|
-
headers: { "content-type": "application/json" },
|
|
83
|
-
body: JSON.stringify({ decision: optionId === "approve" ? "approve" : "reject" }),
|
|
84
|
-
});
|
|
85
|
-
if (res.status === 409 || res.status === 404) return { resolved: false }; // expired or already answered
|
|
86
|
-
if (!res.ok) throw new Error("approval failed");
|
|
87
|
-
return { resolved: true, detail: (await res.json()).status };
|
|
88
|
-
}}
|
|
89
|
-
/>
|
|
68
|
+
<Piloto transport={createAgUiTransport({ url: "http://localhost:8000/agent" })} />
|
|
90
69
|
```
|
|
91
70
|
|
|
92
|
-
|
|
71
|
+
`interrupt()` in your graph becomes the approval card; *Approve*/*Reject* resume it
|
|
72
|
+
(`Command(resume={approved})` on the graph side). A runnable example with a tool and an approval is in
|
|
73
|
+
[`examples/langgraph`](examples/langgraph); the playground can point at it with
|
|
74
|
+
`?backend=agui&url=http://localhost:8000/agent`.
|
|
75
|
+
|
|
76
|
+
## The contract
|
|
93
77
|
|
|
94
|
-
|
|
95
|
-
|
|
78
|
+
One `POST <url>` with an AG-UI `RunAgentInput`, answered by a stream of standard AG-UI events. That's
|
|
79
|
+
all a backend serves. **[docs/protocol.md](docs/protocol.md)** is the exact spec: request, events,
|
|
80
|
+
approvals (`RUN_FINISHED.outcome: interrupt` + `resume`), reopening a thread (`MESSAGES_SNAPSHOT`),
|
|
81
|
+
stopping and errors.
|
|
96
82
|
|
|
97
83
|
## Make it yours
|
|
98
84
|
|
|
@@ -109,7 +95,8 @@ const ref = useRef<PilotoHandle>(null);
|
|
|
109
95
|
welcome="How can I help with your crons?"
|
|
110
96
|
suggestions={["Why did the monitor fail?"]}
|
|
111
97
|
draft={{ text: "Why did the monitor fail?", key: row.id }} // prefilled + focused, not sent
|
|
112
|
-
|
|
98
|
+
threadId={conversationId} // reopen a stored thread
|
|
99
|
+
context={() => ({ screen: "crons", selected })} // AG-UI `context`, every run
|
|
113
100
|
toolVerbs={{ run_cron: { one: "Ran a cron", many: "Ran %n crons", running: "Running the cron…" } }}
|
|
114
101
|
upload={async (file) => ({ url: await myUpload(file) })}
|
|
115
102
|
transcribe={async (audio) => myWhisper(audio)}
|
|
@@ -122,9 +109,9 @@ ref.current?.send("Why did this fail?"); // ask on the user'
|
|
|
122
109
|
```
|
|
123
110
|
|
|
124
111
|
Other options: `strings`, `toolIcons`, `formatUserText`, `markdown`, `features`
|
|
125
|
-
(`attachments`, `retry`, `copy`, `usage`, `
|
|
112
|
+
(`attachments`, `retry`, `copy`, `usage`, `replyOptions`, `duration`), `references` /
|
|
126
113
|
`onReferencesChange`, `voiceNoteText`, `placeholder`, `avatar`, `initialTurns`, and the callbacks
|
|
127
|
-
`onTurnStart`, `onTitle`, `
|
|
114
|
+
`onTurnStart`, `onTitle`, `onEvent` (every raw AG-UI event: STATE_*, CUSTOM…), `onSend`. To read the state
|
|
128
115
|
outside the chat, call `usePilot(transport)` yourself and pass the result as `pilot`. Every piece is
|
|
129
116
|
exported (`AgentBubble`, `AgentSteps`, `Composer`, `ConfirmCard`, `Markdown`, …) if you want to build
|
|
130
117
|
your own layout.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// src/transport/index.ts
|
|
2
|
+
var PilotHttpError = class extends Error {
|
|
3
|
+
constructor(message, status, code, retryAfter) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "PilotHttpError";
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.retryAfter = retryAfter;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
async function readSse(body, onMessage) {
|
|
12
|
+
const reader = body.getReader();
|
|
13
|
+
const decoder = new TextDecoder();
|
|
14
|
+
let buffer = "";
|
|
15
|
+
let event = "message";
|
|
16
|
+
let data = [];
|
|
17
|
+
const flush = () => {
|
|
18
|
+
if (data.length) onMessage({ event, data: data.join("\n") });
|
|
19
|
+
event = "message";
|
|
20
|
+
data = [];
|
|
21
|
+
};
|
|
22
|
+
for (; ; ) {
|
|
23
|
+
const { value, done } = await reader.read();
|
|
24
|
+
if (done) break;
|
|
25
|
+
buffer += decoder.decode(value, { stream: true });
|
|
26
|
+
let nl;
|
|
27
|
+
while ((nl = buffer.search(/\r\n|\r|\n/)) >= 0) {
|
|
28
|
+
const line = buffer.slice(0, nl);
|
|
29
|
+
buffer = buffer.slice(nl + (buffer[nl] === "\r" && buffer[nl + 1] === "\n" ? 2 : 1));
|
|
30
|
+
if (line === "") flush();
|
|
31
|
+
else if (line.startsWith(":")) continue;
|
|
32
|
+
else {
|
|
33
|
+
const i = line.indexOf(":");
|
|
34
|
+
const field = i < 0 ? line : line.slice(0, i);
|
|
35
|
+
const val = i < 0 ? "" : line.slice(i + 1).replace(/^ /, "");
|
|
36
|
+
if (field === "event") event = val;
|
|
37
|
+
else if (field === "data") data.push(val);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
flush();
|
|
42
|
+
}
|
|
43
|
+
function createAgUiTransport(opts) {
|
|
44
|
+
const doFetch = (...a) => (opts.fetch ?? fetch)(...a);
|
|
45
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
46
|
+
const retries = opts.retries ?? 6;
|
|
47
|
+
return {
|
|
48
|
+
async run(input, onEvent, signal) {
|
|
49
|
+
const body = JSON.stringify(input);
|
|
50
|
+
for (let attempt = 0; ; attempt++) {
|
|
51
|
+
const res = await doFetch(opts.url, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
credentials: opts.credentials ?? "same-origin",
|
|
54
|
+
headers: { "content-type": "application/json", accept: "text/event-stream", ...opts.headers },
|
|
55
|
+
body,
|
|
56
|
+
signal
|
|
57
|
+
});
|
|
58
|
+
const retryAfter = Number(res.headers.get("Retry-After")) || void 0;
|
|
59
|
+
if (res.status === 503 && attempt < retries) {
|
|
60
|
+
await sleep((retryAfter ?? 3) * 1e3);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
const json = await res.json().catch(() => null);
|
|
65
|
+
throw new PilotHttpError(json?.message ?? json?.error ?? `Error ${res.status}`, res.status, json?.error, retryAfter);
|
|
66
|
+
}
|
|
67
|
+
if (!res.body) return;
|
|
68
|
+
await readSse(res.body, ({ data }) => {
|
|
69
|
+
if (!data) return;
|
|
70
|
+
try {
|
|
71
|
+
onEvent(JSON.parse(data));
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export {
|
|
82
|
+
PilotHttpError,
|
|
83
|
+
readSse,
|
|
84
|
+
createAgUiTransport
|
|
85
|
+
};
|