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 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, UseMissionOptions, UseMissionReturn, } from "./types";
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";
@@ -1 +1 @@
1
- export { useMission } from './useMission';
1
+ export { useOpalZero } from './useOpalZero';
@@ -1 +1 @@
1
- export { useMission } from './useMission';
1
+ export { useOpalZero } from './useOpalZero';
@@ -0,0 +1,2 @@
1
+ import type { UseOpalZeroOptions, UseOpalZeroReturn } from '../types';
2
+ export declare function useOpalZero({ client, model: defaultModel, onEvent }: UseOpalZeroOptions): UseOpalZeroReturn;
@@ -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 UseMissionOptions {
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 UseMissionReturn {
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. */
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "opalzero-demo",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {}
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opal-zero",
3
- "version": "1.2.1",
3
+ "version": "1.2.4",
4
4
  "description": "TypeScript client SDK for the OpalZero Intelligence Kernel",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/index.ts CHANGED
@@ -20,7 +20,7 @@ export type {
20
20
  UploadResult,
21
21
  ConfigStatus,
22
22
  BentoCard,
23
- UseMissionOptions,
24
- UseMissionReturn,
23
+ UseOpalZeroOptions,
24
+ UseOpalZeroReturn,
25
25
  } from "./types";
26
26
  // Note: useMission is NOT exported from the main entry — import from 'opal-zero/react'
@@ -1 +1 @@
1
- export { useMission } from './useMission';
1
+ export { useOpalZero } from './useOpalZero';
@@ -3,8 +3,8 @@ import type {
3
3
  MissionStatus,
4
4
  MissionState,
5
5
  BentoCard,
6
- UseMissionOptions,
7
- UseMissionReturn,
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 useMission({ client, model: defaultModel, onEvent }: UseMissionOptions): UseMissionReturn {
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 UseMissionOptions {
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 UseMissionReturn {
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. */