opal-zero 1.2.1 → 1.2.4
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 +553 -0
- package/dist/index.d.ts +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/useOpalZero.d.ts +2 -0
- package/dist/react/useOpalZero.js +101 -0
- package/dist/types.d.ts +2 -2
- package/opalzero-demo/package-lock.json +6 -0
- package/package.json +1 -1
- package/src/index.ts +2 -2
- package/src/react/index.ts +1 -1
- package/src/react/{useMission.ts → useOpalZero.ts} +3 -3
- package/src/types.ts +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
# opal-zero
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for [OpalZero](https://github.com/albertobarnabo/opal-zero-engine) — a self-hosted multi-agent intelligence kernel. Give it a plain-English intent; get back structured results streamed in real time.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install opal-zero
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## What is OpalZero?
|
|
12
|
+
|
|
13
|
+
OpalZero is a self-hosted server that turns an unstructured natural-language task into a dependency-ordered execution plan, dispatches specialist AI agents to fulfill it, validates every result through an autonomous quality gate (the Governor), and streams the entire process back to your application over SSE.
|
|
14
|
+
|
|
15
|
+
**Architecture at a glance:**
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Your app ──POST /api/v1/execute──▶ Planner
|
|
19
|
+
│ produces ordered task graph
|
|
20
|
+
Dispatcher
|
|
21
|
+
│ assigns agents by role
|
|
22
|
+
┌─────────┴──────────┐
|
|
23
|
+
WebSearcher Analyst / Coder …
|
|
24
|
+
│ │
|
|
25
|
+
Governor ◀─────────────┘
|
|
26
|
+
│ validates; may Expand / Retry / Refine
|
|
27
|
+
ContextBus
|
|
28
|
+
│ aggregates final state
|
|
29
|
+
◀─SSE stream── mission_complete
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Agents share a live ContextBus. The Governor scores every result across five criteria; if quality is insufficient it can inject additional tasks mid-run (`governor_expand` event), retry, or pause and ask you for clarification (`awaiting_feedback` event).
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install opal-zero # npm
|
|
40
|
+
yarn add opal-zero # yarn
|
|
41
|
+
pnpm add opal-zero # pnpm
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
React (`useOpalZero`) is a peer dependency — install React separately if you haven't already:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install react react-dom
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Quickstart — React
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { OpalZeroClient } from "opal-zero";
|
|
56
|
+
import { useOpalZero } from "opal-zero/react";
|
|
57
|
+
|
|
58
|
+
const client = new OpalZeroClient({ baseUrl: "http://localhost:8000" });
|
|
59
|
+
|
|
60
|
+
export function MissionRunner() {
|
|
61
|
+
const { run, status, cards, activeAgent, error, missionId, refine } =
|
|
62
|
+
useOpalZero({ client });
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div>
|
|
66
|
+
<button onClick={() => run("Compare the top 3 EVs under $60k")}>Run</button>
|
|
67
|
+
<button
|
|
68
|
+
disabled={!missionId}
|
|
69
|
+
onClick={() => refine(missionId!, "Add charging infrastructure scores")}
|
|
70
|
+
>
|
|
71
|
+
Refine
|
|
72
|
+
</button>
|
|
73
|
+
|
|
74
|
+
{activeAgent && (
|
|
75
|
+
<p>
|
|
76
|
+
{activeAgent.role} — {activeAgent.intent}
|
|
77
|
+
</p>
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
{status === "complete" &&
|
|
81
|
+
cards.map((card) => (
|
|
82
|
+
<div key={card.key}>
|
|
83
|
+
<h3>{card.widget}: {card.key}</h3>
|
|
84
|
+
{card.isRefined && <span>Updated</span>}
|
|
85
|
+
<pre>{JSON.stringify(card.props, null, 2)}</pre>
|
|
86
|
+
</div>
|
|
87
|
+
))}
|
|
88
|
+
|
|
89
|
+
{error && <p>Error: {error}</p>}
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Quickstart — Vanilla TypeScript / Node
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { OpalZeroClient } from "opal-zero";
|
|
101
|
+
|
|
102
|
+
const client = new OpalZeroClient({ baseUrl: "http://localhost:8000" });
|
|
103
|
+
|
|
104
|
+
for await (const event of client.execute("Summarise the latest Rust release notes")) {
|
|
105
|
+
switch (event.type) {
|
|
106
|
+
case "task_started":
|
|
107
|
+
console.log(`▶ [${event.role}] ${event.intent}`);
|
|
108
|
+
break;
|
|
109
|
+
case "task_completed":
|
|
110
|
+
console.log(`✅ ${event.slug}:`, event.result);
|
|
111
|
+
break;
|
|
112
|
+
case "governor_expand":
|
|
113
|
+
console.log(`🔁 Governor added ${event.new_task_count} tasks`);
|
|
114
|
+
break;
|
|
115
|
+
case "mission_complete":
|
|
116
|
+
console.log("Done:", event.mission_id, event.mission_state);
|
|
117
|
+
break;
|
|
118
|
+
case "mission_failed":
|
|
119
|
+
console.error("Failed:", event.error);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Running the server
|
|
128
|
+
|
|
129
|
+
### Docker (recommended)
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
docker run \
|
|
133
|
+
-e OPENAI_API_KEY=sk-... \
|
|
134
|
+
-e TAVILY_API_KEY=tvly-... `# optional — enables web search` \
|
|
135
|
+
-e OPALZERO_API_KEY=... `# optional — enables API key auth` \
|
|
136
|
+
-p 8000:8000 \
|
|
137
|
+
ghcr.io/albertobarnabo/opalzero-server:latest
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Docker Compose
|
|
141
|
+
|
|
142
|
+
```yaml
|
|
143
|
+
# docker-compose.yml
|
|
144
|
+
services:
|
|
145
|
+
opalzero-server:
|
|
146
|
+
image: ghcr.io/albertobarnabo/opalzero-server:latest
|
|
147
|
+
ports:
|
|
148
|
+
- "8000:8000"
|
|
149
|
+
environment:
|
|
150
|
+
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
|
151
|
+
- TAVILY_API_KEY=${TAVILY_API_KEY:-}
|
|
152
|
+
- OPALZERO_API_KEY=${OPALZERO_API_KEY:-}
|
|
153
|
+
volumes:
|
|
154
|
+
- opalzero-missions:/app/missions
|
|
155
|
+
- opalzero-uploads:/app/uploads
|
|
156
|
+
|
|
157
|
+
volumes:
|
|
158
|
+
opalzero-missions:
|
|
159
|
+
opalzero-uploads:
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### From source (Rust)
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
git clone https://github.com/albertobarnabo/opal-zero-engine
|
|
166
|
+
cd opal-zero-engine
|
|
167
|
+
export OPENAI_API_KEY=sk-...
|
|
168
|
+
PORT=8000 cargo run --release -p opalzero-server
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Environment variables
|
|
172
|
+
|
|
173
|
+
| Variable | Required | Description |
|
|
174
|
+
|---|---|---|
|
|
175
|
+
| `OPENAI_API_KEY` | Yes | Primary LLM provider key |
|
|
176
|
+
| `TAVILY_API_KEY` | No | Enables the `web_search` tool for real-time retrieval |
|
|
177
|
+
| `ALPHA_VANTAGE_API_KEY` | No | Enables financial data tools (stock prices, income statements, news sentiment) |
|
|
178
|
+
| `OPALZERO_API_KEY` | No | When set, all API requests must include `X-OpalZero-Key` matching this value |
|
|
179
|
+
| `PORT` | No | Listening port (default `8000`) |
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## API Reference
|
|
184
|
+
|
|
185
|
+
### `OpalZeroClient`
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
import { OpalZeroClient } from "opal-zero";
|
|
189
|
+
|
|
190
|
+
const client = new OpalZeroClient({
|
|
191
|
+
baseUrl: "http://localhost:8000", // required
|
|
192
|
+
apiKey?: string, // X-OpalZero-Key header
|
|
193
|
+
openAiKey?: string, // per-request OpenAI key override
|
|
194
|
+
tavilyKey?: string, // per-request Tavily key override
|
|
195
|
+
alphaVantageKey?: string, // per-request Alpha Vantage key override
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Per-request key overrides are useful when you want different callers to use their own API keys without the server needing a shared key configured.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
#### `client.execute(intent, model?)`
|
|
204
|
+
|
|
205
|
+
Starts a new mission. Returns an `AsyncGenerator<MissionEvent>` that yields SSE events until the mission completes or fails.
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
for await (const event of client.execute("Plan a 3-day trip to Rome")) {
|
|
209
|
+
// handle events (see SSE Events below)
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
The optional `model` parameter is passed directly to the server and may be used to select a specific LLM (e.g. `"gpt-4o"`, `"claude-opus-4-7"`).
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
#### `client.missions.list()`
|
|
218
|
+
|
|
219
|
+
Returns `MissionSummary[]` — all past missions with their id, intent, status, and creation timestamp.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
const missions = await client.missions.list();
|
|
223
|
+
// [{ id, intent, status, created_at }, ...]
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
#### `client.missions.get(id)`
|
|
229
|
+
|
|
230
|
+
Returns a full `MissionSnapshot` including the complete task plan, every task's result, and the final `MissionState`.
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
const snapshot = await client.missions.get("mission-uuid");
|
|
234
|
+
// { mission_id, intent, plan, mission_state, status, created_at }
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
#### `client.missions.refine(id, intent, model?)`
|
|
240
|
+
|
|
241
|
+
Streams a refinement on an existing mission. Agents pick up the context from the prior run; new results are merged into the existing state. Returns `AsyncGenerator<MissionEvent>`.
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
for await (const event of client.missions.refine(id, "Add a cost breakdown")) {
|
|
245
|
+
// same event types as execute()
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
#### `client.missions.export(id, format)`
|
|
252
|
+
|
|
253
|
+
Downloads the mission result. Returns a `Blob` ready to save or display.
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
const blob = await client.missions.export(id, "md"); // "md" | "csv" | "html"
|
|
257
|
+
const url = URL.createObjectURL(blob);
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
#### `client.missions.delete(id)`
|
|
263
|
+
|
|
264
|
+
Deletes a mission and its persisted data from the server.
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
await client.missions.delete(id);
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
#### `client.upload(file)`
|
|
273
|
+
|
|
274
|
+
Uploads a `File` object (CSV, JSON, image — max 10 MB) to make it available as context for agents during a subsequent `execute()` call.
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
const result = await client.upload(file);
|
|
278
|
+
// { filename: "abc123.csv", file_type: "data", original_name: "sales.csv" }
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
#### `client.configStatus()`
|
|
284
|
+
|
|
285
|
+
Returns which API keys are configured on the server. Useful for showing/hiding features in your UI.
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
const status = await client.configStatus();
|
|
289
|
+
// { openai: true, tavily: false, alpha_vantage: false }
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
### SSE Events
|
|
295
|
+
|
|
296
|
+
Every event has a `type` discriminant. Use a `switch` on `event.type` to handle them:
|
|
297
|
+
|
|
298
|
+
| `event.type` | Fields | When |
|
|
299
|
+
|---|---|---|
|
|
300
|
+
| `task_started` | `slug`, `role`, `intent` | An agent begins working on a task |
|
|
301
|
+
| `task_completed` | `slug`, `role`, `result` | An agent finishes; `result` is the agent's raw output string |
|
|
302
|
+
| `task_failed` | `slug`, `role` | An agent failed; the mission may still continue if other tasks can proceed |
|
|
303
|
+
| `governor_expand` | `new_task_count`, `descriptions[]` | The Governor rejected output and injected additional tasks to fill gaps |
|
|
304
|
+
| `mission_complete` | `mission_id`, `intent`, `task_count`, `expanded_task_count`, `layout_hint`, `mission_state?` | All tasks done; `mission_state` has the final structured result |
|
|
305
|
+
| `mission_failed` | `error` | Unrecoverable failure; `error` contains the reason |
|
|
306
|
+
| `mission_paused` | `question`, `mission_id` | The `feedback` tool paused execution awaiting human input |
|
|
307
|
+
| `awaiting_feedback` | `slug`, `question` | A specific task is paused for clarification |
|
|
308
|
+
|
|
309
|
+
**TypeScript event types:**
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
import type {
|
|
313
|
+
MissionEvent,
|
|
314
|
+
TaskStartedEvent,
|
|
315
|
+
TaskCompletedEvent,
|
|
316
|
+
TaskFailedEvent,
|
|
317
|
+
GovernorExpandEvent,
|
|
318
|
+
MissionCompleteEvent,
|
|
319
|
+
MissionFailedEvent,
|
|
320
|
+
MissionPausedEvent,
|
|
321
|
+
AwaitingFeedbackEvent,
|
|
322
|
+
} from "opal-zero";
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
### `useOpalZero(options)` · `opal-zero/react`
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
import { useOpalZero } from "opal-zero/react";
|
|
331
|
+
|
|
332
|
+
const {
|
|
333
|
+
run, // (intent: string, model?: string) => Promise<void>
|
|
334
|
+
refine, // (missionId: string, intent: string, model?: string) => Promise<void>
|
|
335
|
+
status, // "idle" | "running" | "complete" | "failed"
|
|
336
|
+
cards, // BentoCard[] — ready-to-render result cards
|
|
337
|
+
activeAgent, // { role: string; intent: string } | null
|
|
338
|
+
error, // string | null
|
|
339
|
+
missionId, // string | null — pass to refine()
|
|
340
|
+
missionState, // MissionState | null — raw payload for custom renderers
|
|
341
|
+
reset, // () => void — reset to idle
|
|
342
|
+
} = useOpalZero({
|
|
343
|
+
client, // OpalZeroClient instance
|
|
344
|
+
model?, // default model string
|
|
345
|
+
onEvent?, // (event: MissionEvent) => void — tap every event for side effects
|
|
346
|
+
});
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
**`run(intent, model?)`** — clears all previous state and starts a new mission.
|
|
350
|
+
|
|
351
|
+
**`refine(missionId, intent, model?)`** — leaves the existing card grid in place and merges new results. Cards added or updated by the refinement are marked `isRefined: true`. Does nothing if `status === "running"`.
|
|
352
|
+
|
|
353
|
+
**`reset()`** — resets to `idle`. Does not abort an in-flight stream — call it after the stream ends or as a UI affordance.
|
|
354
|
+
|
|
355
|
+
**`activeAgent`** — non-null while a `task_started` event has been received and no `task_completed` / `task_failed` has followed yet. Use this to show a live "Agent running…" indicator.
|
|
356
|
+
|
|
357
|
+
**`onEvent`** — called for every SSE event before the hook updates its own state. Use for side effects (trace logs, toast banners) that live outside the hook.
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
### `parseBentoCards(state, options?)`
|
|
362
|
+
|
|
363
|
+
Converts a raw `MissionState` into an ordered `BentoCard[]`. Used internally by `useOpalZero` — export it when building a custom renderer.
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
import { parseBentoCards } from "opal-zero";
|
|
367
|
+
|
|
368
|
+
const cards = parseBentoCards(missionState);
|
|
369
|
+
// or with refinement tracking:
|
|
370
|
+
const cards = parseBentoCards(missionState, { refinedKeys: new Set(["ev_range"]) });
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
**How it works:**
|
|
374
|
+
|
|
375
|
+
The server's Analyst agent produces a `MissionState` with two parts:
|
|
376
|
+
- `data_payload` — a key/value map of structured results (strings, numbers, objects, arrays)
|
|
377
|
+
- `suggested_widgets` — an ordered list of `"WidgetType:key"` strings that map payload entries to UI components
|
|
378
|
+
|
|
379
|
+
`parseBentoCards` joins these to produce `BentoCard[]`. When `suggested_widgets` is absent, widgets are inferred from the shape of each payload value (number → `MetricCard`, URL string → `ImageCard`, array of objects → `ComparisonTable`).
|
|
380
|
+
|
|
381
|
+
**Widget types:**
|
|
382
|
+
|
|
383
|
+
| Widget | When used |
|
|
384
|
+
|---|---|
|
|
385
|
+
| `MetricCard` | Scalar values — numbers, short strings |
|
|
386
|
+
| `ChartCard` | Arrays of data points or objects with chart data |
|
|
387
|
+
| `ComparisonTable` | Arrays of objects (rows), ideal for side-by-side comparisons |
|
|
388
|
+
| `Timeline` | Ordered sequences of events |
|
|
389
|
+
| `ImageCard` | URLs or objects with image metadata |
|
|
390
|
+
|
|
391
|
+
**`BentoCard` shape:**
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
interface BentoCard {
|
|
395
|
+
key: string; // data_payload key, e.g. "cheapest_ev_usd"
|
|
396
|
+
widget: string; // "MetricCard" | "ChartCard" | ...
|
|
397
|
+
props: Record<string, unknown>; // pass directly to your component
|
|
398
|
+
isRefined?: boolean; // true when added/updated by refine()
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## Advanced Topics
|
|
405
|
+
|
|
406
|
+
### Mission refinement
|
|
407
|
+
|
|
408
|
+
Refinement runs a second pass on an existing mission with a narrower intent. New results are merged into the original `data_payload` — keys that didn't exist before are added, keys that changed are updated. The `useOpalZero` hook marks changed cards `isRefined: true` so you can highlight them.
|
|
409
|
+
|
|
410
|
+
```tsx
|
|
411
|
+
// After a mission completes:
|
|
412
|
+
const { missionId, refine } = useOpalZero({ client });
|
|
413
|
+
|
|
414
|
+
// First run
|
|
415
|
+
await run("Compare the top 3 EVs under $60k");
|
|
416
|
+
|
|
417
|
+
// Then deepen — does not replace the existing card grid
|
|
418
|
+
await refine(missionId!, "Add charging network coverage data");
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Human-in-the-loop (HITL) feedback
|
|
422
|
+
|
|
423
|
+
Agents can call the built-in `feedback` tool to pause a mission and ask for human input. When this happens, the server emits a `mission_paused` event with a `question` string. Your application must call `POST /api/v1/clarify` with the answer to resume execution.
|
|
424
|
+
|
|
425
|
+
```ts
|
|
426
|
+
for await (const event of client.execute("Analyse this dataset")) {
|
|
427
|
+
if (event.type === "mission_paused") {
|
|
428
|
+
const answer = await promptUser(event.question); // your UI
|
|
429
|
+
await fetch(`${baseUrl}/api/v1/clarify`, {
|
|
430
|
+
method: "POST",
|
|
431
|
+
headers: { "Content-Type": "application/json" },
|
|
432
|
+
body: JSON.stringify({ mission_id: event.mission_id, answer }),
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Uploading files for agent use
|
|
439
|
+
|
|
440
|
+
Upload a file before calling `execute()`. The server stores it and agents can reference it by the returned `filename`.
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
const result = await client.upload(myFile);
|
|
444
|
+
// result.filename is the server-side path agents can access
|
|
445
|
+
|
|
446
|
+
for await (const event of client.execute(
|
|
447
|
+
`Analyse the uploaded CSV and find revenue trends — file: ${result.filename}`
|
|
448
|
+
)) { ... }
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Supported file types: CSV, JSON, PDF, and images (PNG, JPEG, WebP). Maximum size: 10 MB.
|
|
452
|
+
|
|
453
|
+
### Checking server capabilities before rendering UI
|
|
454
|
+
|
|
455
|
+
```ts
|
|
456
|
+
const { openai, tavily, alpha_vantage } = await client.configStatus();
|
|
457
|
+
|
|
458
|
+
if (!tavily) {
|
|
459
|
+
// hide "search the web" features in your UI
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### Observing every event (tracing / analytics)
|
|
464
|
+
|
|
465
|
+
Use `onEvent` to tap the SSE stream for cross-cutting concerns without coupling them to component state:
|
|
466
|
+
|
|
467
|
+
```tsx
|
|
468
|
+
useOpalZero({
|
|
469
|
+
client,
|
|
470
|
+
onEvent(event) {
|
|
471
|
+
analytics.track("opalzero_event", { type: event.type });
|
|
472
|
+
if (event.type === "task_completed") {
|
|
473
|
+
console.log("[trace]", event.slug, "→", event.result.slice(0, 120));
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## TypeScript
|
|
482
|
+
|
|
483
|
+
All public types are exported from the main entry point:
|
|
484
|
+
|
|
485
|
+
```ts
|
|
486
|
+
import type {
|
|
487
|
+
// Client
|
|
488
|
+
OpalZeroClientConfig,
|
|
489
|
+
|
|
490
|
+
// Mission lifecycle
|
|
491
|
+
MissionStatus, // "idle" | "running" | "complete" | "failed"
|
|
492
|
+
MissionState, // { data_payload, design_tokens?, layout_hint?, suggested_widgets? }
|
|
493
|
+
MissionSnapshot, // { mission_id, intent, plan, mission_state, status, created_at? }
|
|
494
|
+
MissionSummary, // { id, intent, status, created_at? }
|
|
495
|
+
Task, // { slug, role, intent, status, result? }
|
|
496
|
+
|
|
497
|
+
// SSE events (full union)
|
|
498
|
+
MissionEvent,
|
|
499
|
+
TaskStartedEvent,
|
|
500
|
+
TaskCompletedEvent,
|
|
501
|
+
TaskFailedEvent,
|
|
502
|
+
GovernorExpandEvent,
|
|
503
|
+
MissionCompleteEvent,
|
|
504
|
+
MissionFailedEvent,
|
|
505
|
+
MissionPausedEvent,
|
|
506
|
+
AwaitingFeedbackEvent,
|
|
507
|
+
UnknownEvent,
|
|
508
|
+
|
|
509
|
+
// Upload / config
|
|
510
|
+
UploadResult,
|
|
511
|
+
ConfigStatus,
|
|
512
|
+
|
|
513
|
+
// Bento / React hook
|
|
514
|
+
BentoCard,
|
|
515
|
+
UseOpalZeroOptions,
|
|
516
|
+
UseOpalZeroReturn,
|
|
517
|
+
} from "opal-zero";
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
## API Endpoints (HTTP reference)
|
|
523
|
+
|
|
524
|
+
For non-TypeScript clients or server-to-server use:
|
|
525
|
+
|
|
526
|
+
| Method | Path | Description |
|
|
527
|
+
|---|---|---|
|
|
528
|
+
| `POST` | `/api/v1/execute` | Start a mission; returns SSE stream |
|
|
529
|
+
| `POST` | `/api/v1/clarify` | Resume a paused mission with a human answer |
|
|
530
|
+
| `GET` | `/api/v1/missions` | List all past missions |
|
|
531
|
+
| `GET` | `/api/v1/missions/:id` | Get a mission snapshot |
|
|
532
|
+
| `DELETE` | `/api/v1/missions/:id` | Delete a mission |
|
|
533
|
+
| `POST` | `/api/v1/missions/:id/refine` | Refine a mission; returns SSE stream |
|
|
534
|
+
| `GET` | `/api/v1/missions/:id/export` | Export (`?format=md\|csv\|html`) |
|
|
535
|
+
| `POST` | `/api/v1/upload` | Upload a file (multipart/form-data) |
|
|
536
|
+
| `GET` | `/api/v1/config/status` | Check which API keys are configured |
|
|
537
|
+
| `GET` | `/health` | Health check |
|
|
538
|
+
|
|
539
|
+
**Auth header:** `X-OpalZero-Key: <your-key>` (only required when `OPALZERO_API_KEY` is set on the server).
|
|
540
|
+
|
|
541
|
+
**Per-request key overrides:**
|
|
542
|
+
|
|
543
|
+
| Header | Purpose |
|
|
544
|
+
|---|---|
|
|
545
|
+
| `X-OpenAI-Key` | Override the server's `OPENAI_API_KEY` for this request |
|
|
546
|
+
| `X-Tavily-Key` | Override the server's `TAVILY_API_KEY` for this request |
|
|
547
|
+
| `X-Alpha-Vantage-Key` | Override the server's `ALPHA_VANTAGE_API_KEY` for this request |
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
## License
|
|
552
|
+
|
|
553
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { OpalZeroClient } from "./client";
|
|
2
2
|
export { parseBentoCards } from "./parseBentoCards";
|
|
3
|
-
export type { OpalZeroClientConfig, MissionEvent, TaskStartedEvent, TaskCompletedEvent, TaskFailedEvent, GovernorExpandEvent, MissionCompleteEvent, MissionFailedEvent, MissionPausedEvent, AwaitingFeedbackEvent, UnknownEvent, MissionSnapshot, MissionSummary, MissionState, Task, MissionStatus, UploadResult, ConfigStatus, BentoCard,
|
|
3
|
+
export type { OpalZeroClientConfig, MissionEvent, TaskStartedEvent, TaskCompletedEvent, TaskFailedEvent, GovernorExpandEvent, MissionCompleteEvent, MissionFailedEvent, MissionPausedEvent, AwaitingFeedbackEvent, UnknownEvent, MissionSnapshot, MissionSummary, MissionState, Task, MissionStatus, UploadResult, ConfigStatus, BentoCard, UseOpalZeroOptions, UseOpalZeroReturn, } from "./types";
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { useOpalZero } from './useOpalZero';
|
package/dist/react/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { useOpalZero } from './useOpalZero';
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useState, useRef, useCallback } from 'react';
|
|
2
|
+
import { parseBentoCards } from '../parseBentoCards';
|
|
3
|
+
export function useOpalZero({ client, model: defaultModel, onEvent }) {
|
|
4
|
+
const [status, setStatus] = useState('idle');
|
|
5
|
+
const [cards, setCards] = useState([]);
|
|
6
|
+
const [activeAgent, setActiveAgent] = useState(null);
|
|
7
|
+
const [error, setError] = useState(null);
|
|
8
|
+
const [missionId, setMissionId] = useState(null);
|
|
9
|
+
const [missionState, setMissionState] = useState(null);
|
|
10
|
+
const refinedKeysRef = useRef(new Set());
|
|
11
|
+
function reset() {
|
|
12
|
+
setStatus('idle');
|
|
13
|
+
setCards([]);
|
|
14
|
+
setActiveAgent(null);
|
|
15
|
+
setError(null);
|
|
16
|
+
setMissionId(null);
|
|
17
|
+
setMissionState(null);
|
|
18
|
+
refinedKeysRef.current = new Set();
|
|
19
|
+
}
|
|
20
|
+
async function drainStream(stream, isRefinement, previousKeys) {
|
|
21
|
+
setStatus('running');
|
|
22
|
+
setActiveAgent(null);
|
|
23
|
+
setError(null);
|
|
24
|
+
try {
|
|
25
|
+
for await (const event of stream) {
|
|
26
|
+
onEvent?.(event);
|
|
27
|
+
switch (event.type) {
|
|
28
|
+
case 'task_started': {
|
|
29
|
+
const e = event;
|
|
30
|
+
setActiveAgent({ role: e.role, intent: e.intent });
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
case 'task_completed':
|
|
34
|
+
case 'task_failed':
|
|
35
|
+
setActiveAgent(null);
|
|
36
|
+
break;
|
|
37
|
+
case 'mission_complete': {
|
|
38
|
+
const e = event;
|
|
39
|
+
setMissionId(e.mission_id);
|
|
40
|
+
const state = e.mission_state ?? null;
|
|
41
|
+
setMissionState(state);
|
|
42
|
+
if (state) {
|
|
43
|
+
const ms = state;
|
|
44
|
+
if (isRefinement) {
|
|
45
|
+
const newKeys = new Set(Object.keys(ms.data_payload ?? {}));
|
|
46
|
+
previousKeys.forEach(k => newKeys.delete(k));
|
|
47
|
+
newKeys.forEach(k => refinedKeysRef.current.add(k));
|
|
48
|
+
}
|
|
49
|
+
const parsed = parseBentoCards(ms, { refinedKeys: refinedKeysRef.current });
|
|
50
|
+
if (isRefinement) {
|
|
51
|
+
setCards(prev => {
|
|
52
|
+
const merged = [...prev];
|
|
53
|
+
for (const card of parsed) {
|
|
54
|
+
const idx = merged.findIndex(c => c.key === card.key);
|
|
55
|
+
if (idx >= 0)
|
|
56
|
+
merged[idx] = card;
|
|
57
|
+
else
|
|
58
|
+
merged.push(card);
|
|
59
|
+
}
|
|
60
|
+
return merged;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
setCards(parsed);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
setStatus('complete');
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
case 'mission_failed': {
|
|
71
|
+
const e = event;
|
|
72
|
+
setError(e.error);
|
|
73
|
+
setStatus('failed');
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
81
|
+
setError(msg);
|
|
82
|
+
setStatus('failed');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const run = useCallback(async (intent, model) => {
|
|
86
|
+
reset();
|
|
87
|
+
refinedKeysRef.current = new Set();
|
|
88
|
+
const stream = client.execute(intent, model ?? defaultModel);
|
|
89
|
+
await drainStream(stream, false, new Set());
|
|
90
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
91
|
+
}, [client, defaultModel]);
|
|
92
|
+
const refine = useCallback(async (id, intent, model) => {
|
|
93
|
+
if (status === 'running')
|
|
94
|
+
return;
|
|
95
|
+
const previousKeys = new Set(Object.keys(missionState?.data_payload ?? {}));
|
|
96
|
+
const stream = client.missions.refine(id, intent, model ?? defaultModel);
|
|
97
|
+
await drainStream(stream, true, previousKeys);
|
|
98
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
99
|
+
}, [client, defaultModel, status, missionState]);
|
|
100
|
+
return { run, refine, status, cards, activeAgent, error, missionId, missionState, reset };
|
|
101
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -112,14 +112,14 @@ export interface BentoCard {
|
|
|
112
112
|
/** True if this card was added/updated by a refinement round */
|
|
113
113
|
isRefined?: boolean;
|
|
114
114
|
}
|
|
115
|
-
export interface
|
|
115
|
+
export interface UseOpalZeroOptions {
|
|
116
116
|
client: OpalZeroClient;
|
|
117
117
|
/** Default model to use. Can be overridden per-call in run(). */
|
|
118
118
|
model?: string;
|
|
119
119
|
/** Called for every SSE event before the hook updates its own state. Use this for side effects (trace logs, banners, etc.) that the hook doesn't need to manage. */
|
|
120
120
|
onEvent?: (event: MissionEvent) => void;
|
|
121
121
|
}
|
|
122
|
-
export interface
|
|
122
|
+
export interface UseOpalZeroReturn {
|
|
123
123
|
/** Execute a new mission. Clears previous state before starting. */
|
|
124
124
|
run: (intent: string, model?: string) => Promise<void>;
|
|
125
125
|
/** Refine an existing mission without clearing the card grid. */
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -20,7 +20,7 @@ export type {
|
|
|
20
20
|
UploadResult,
|
|
21
21
|
ConfigStatus,
|
|
22
22
|
BentoCard,
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
UseOpalZeroOptions,
|
|
24
|
+
UseOpalZeroReturn,
|
|
25
25
|
} from "./types";
|
|
26
26
|
// Note: useMission is NOT exported from the main entry — import from 'opal-zero/react'
|
package/src/react/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { useOpalZero } from './useOpalZero';
|
|
@@ -3,8 +3,8 @@ import type {
|
|
|
3
3
|
MissionStatus,
|
|
4
4
|
MissionState,
|
|
5
5
|
BentoCard,
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
UseOpalZeroOptions,
|
|
7
|
+
UseOpalZeroReturn,
|
|
8
8
|
MissionEvent,
|
|
9
9
|
TaskStartedEvent,
|
|
10
10
|
MissionCompleteEvent,
|
|
@@ -12,7 +12,7 @@ import type {
|
|
|
12
12
|
} from '../types';
|
|
13
13
|
import { parseBentoCards } from '../parseBentoCards';
|
|
14
14
|
|
|
15
|
-
export function
|
|
15
|
+
export function useOpalZero({ client, model: defaultModel, onEvent }: UseOpalZeroOptions): UseOpalZeroReturn {
|
|
16
16
|
const [status, setStatus] = useState<MissionStatus>('idle');
|
|
17
17
|
const [cards, setCards] = useState<BentoCard[]>([]);
|
|
18
18
|
const [activeAgent, setActiveAgent] = useState<{ role: string; intent: string } | null>(null);
|
package/src/types.ts
CHANGED
|
@@ -156,7 +156,7 @@ export interface BentoCard {
|
|
|
156
156
|
|
|
157
157
|
// ── useMission ────────────────────────────────────────────────────────────────
|
|
158
158
|
|
|
159
|
-
export interface
|
|
159
|
+
export interface UseOpalZeroOptions {
|
|
160
160
|
client: OpalZeroClient;
|
|
161
161
|
/** Default model to use. Can be overridden per-call in run(). */
|
|
162
162
|
model?: string;
|
|
@@ -164,7 +164,7 @@ export interface UseMissionOptions {
|
|
|
164
164
|
onEvent?: (event: MissionEvent) => void;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
export interface
|
|
167
|
+
export interface UseOpalZeroReturn {
|
|
168
168
|
/** Execute a new mission. Clears previous state before starting. */
|
|
169
169
|
run: (intent: string, model?: string) => Promise<void>;
|
|
170
170
|
/** Refine an existing mission without clearing the card grid. */
|