@gizmo-ai/client 0.2.2
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/dist/__tests__/client.test.d.ts +2 -0
- package/dist/__tests__/client.test.d.ts.map +1 -0
- package/dist/__tests__/subscribe.test.d.ts +2 -0
- package/dist/__tests__/subscribe.test.d.ts.map +1 -0
- package/dist/client.d.ts +15 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +835 -0
- package/dist/subscribe.d.ts +18 -0
- package/dist/subscribe.d.ts.map +1 -0
- package/dist/types.d.ts +218 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +33 -0
- package/src/__tests__/client.test.ts +412 -0
- package/src/__tests__/subscribe.test.ts +333 -0
- package/src/client.ts +214 -0
- package/src/errors.ts +16 -0
- package/src/index.ts +63 -0
- package/src/subscribe.ts +87 -0
- package/src/types.ts +271 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { describe, expect, test, mock, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { subscribe } from "../subscribe.ts";
|
|
3
|
+
import type {
|
|
4
|
+
InitialStateEvent,
|
|
5
|
+
StateUpdateEvent,
|
|
6
|
+
ExecutionStatusEvent,
|
|
7
|
+
HeartbeatEvent,
|
|
8
|
+
CustomEvent,
|
|
9
|
+
StateSnapshot,
|
|
10
|
+
} from "../types.ts";
|
|
11
|
+
|
|
12
|
+
// ── Mock EventSource ───────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
type EventHandler = (event: MessageEvent) => void;
|
|
15
|
+
|
|
16
|
+
class MockEventSource {
|
|
17
|
+
url: string;
|
|
18
|
+
listeners: Map<string, EventHandler[]> = new Map();
|
|
19
|
+
closed = false;
|
|
20
|
+
|
|
21
|
+
constructor(url: string) {
|
|
22
|
+
this.url = url;
|
|
23
|
+
// Make this instance accessible to tests
|
|
24
|
+
MockEventSource.instances.push(this);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
addEventListener(event: string, handler: EventHandler) {
|
|
28
|
+
const handlers = this.listeners.get(event) ?? [];
|
|
29
|
+
handlers.push(handler);
|
|
30
|
+
this.listeners.set(event, handlers);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
close() {
|
|
34
|
+
this.closed = true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Test helper: emit a named event
|
|
38
|
+
emit(eventType: string, data: unknown) {
|
|
39
|
+
const handlers = this.listeners.get(eventType) ?? [];
|
|
40
|
+
const messageEvent = new MessageEvent(eventType, {
|
|
41
|
+
data: JSON.stringify(data),
|
|
42
|
+
});
|
|
43
|
+
for (const handler of handlers) {
|
|
44
|
+
handler(messageEvent);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static instances: MockEventSource[] = [];
|
|
49
|
+
static reset() {
|
|
50
|
+
MockEventSource.instances = [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Install mock EventSource globally
|
|
55
|
+
const OriginalEventSource = globalThis.EventSource;
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
MockEventSource.reset();
|
|
59
|
+
(globalThis as unknown as Record<string, unknown>).EventSource = MockEventSource as unknown as typeof EventSource;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
(globalThis as unknown as Record<string, unknown>).EventSource = OriginalEventSource;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
function lastES(): MockEventSource {
|
|
67
|
+
return MockEventSource.instances[MockEventSource.instances.length - 1];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Tests ──────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
describe("subscribe()", () => {
|
|
73
|
+
test("connects to the correct URL", () => {
|
|
74
|
+
subscribe("http://localhost:3001", () => {});
|
|
75
|
+
expect(lastES().url).toBe("http://localhost:3001/events");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("uses custom endpoint from options", () => {
|
|
79
|
+
subscribe("http://localhost:3001", () => {}, { endpoint: "/stream/state" });
|
|
80
|
+
expect(lastES().url).toBe("http://localhost:3001/stream/state");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("state.initial", () => {
|
|
84
|
+
test("sets local state and fires callback with snapshot", () => {
|
|
85
|
+
const snapshots: StateSnapshot[] = [];
|
|
86
|
+
subscribe("http://localhost:3001", (s) => snapshots.push(s));
|
|
87
|
+
|
|
88
|
+
const event: InitialStateEvent = {
|
|
89
|
+
type: "state.initial",
|
|
90
|
+
slices: { agent: { conversation: [] }, execution: { id: null } },
|
|
91
|
+
timestamp: 1000,
|
|
92
|
+
};
|
|
93
|
+
lastES().emit("state.initial", event);
|
|
94
|
+
|
|
95
|
+
expect(snapshots).toHaveLength(1);
|
|
96
|
+
expect(snapshots[0].slices).toEqual({
|
|
97
|
+
agent: { conversation: [] },
|
|
98
|
+
execution: { id: null },
|
|
99
|
+
});
|
|
100
|
+
expect(snapshots[0].event).toEqual(event);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("replaces previous state on new initial event", () => {
|
|
104
|
+
const snapshots: StateSnapshot[] = [];
|
|
105
|
+
subscribe("http://localhost:3001", (s) => snapshots.push(s));
|
|
106
|
+
|
|
107
|
+
// First initial
|
|
108
|
+
lastES().emit("state.initial", {
|
|
109
|
+
type: "state.initial",
|
|
110
|
+
slices: { agent: { conversation: ["a"] }, old: { data: true } },
|
|
111
|
+
timestamp: 1000,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Second initial (reconnect scenario)
|
|
115
|
+
lastES().emit("state.initial", {
|
|
116
|
+
type: "state.initial",
|
|
117
|
+
slices: { agent: { conversation: [] } },
|
|
118
|
+
timestamp: 2000,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(snapshots).toHaveLength(2);
|
|
122
|
+
// Old slices should be gone
|
|
123
|
+
expect(snapshots[1].slices).toEqual({ agent: { conversation: [] } });
|
|
124
|
+
expect("old" in snapshots[1].slices).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("state.update with patches", () => {
|
|
129
|
+
test("applies JSON Patches correctly", () => {
|
|
130
|
+
const snapshots: StateSnapshot[] = [];
|
|
131
|
+
subscribe("http://localhost:3001", (s) => snapshots.push(s));
|
|
132
|
+
|
|
133
|
+
// Set initial state
|
|
134
|
+
lastES().emit("state.initial", {
|
|
135
|
+
type: "state.initial",
|
|
136
|
+
slices: { agent: { conversation: [], loopCount: 0 } },
|
|
137
|
+
timestamp: 1000,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Apply patch
|
|
141
|
+
const updateEvent: StateUpdateEvent = {
|
|
142
|
+
type: "state.update",
|
|
143
|
+
slice: "agent",
|
|
144
|
+
patches: [
|
|
145
|
+
{ op: "replace", path: "/loopCount", value: 1 },
|
|
146
|
+
{ op: "add", path: "/conversation/-", value: "Hello" },
|
|
147
|
+
],
|
|
148
|
+
timestamp: 2000,
|
|
149
|
+
};
|
|
150
|
+
lastES().emit("state.update", updateEvent);
|
|
151
|
+
|
|
152
|
+
expect(snapshots).toHaveLength(2);
|
|
153
|
+
const agentState = snapshots[1].slices.agent as Record<string, unknown>;
|
|
154
|
+
expect(agentState.loopCount).toBe(1);
|
|
155
|
+
expect(agentState.conversation).toEqual(["Hello"]);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("state.update with value replacement", () => {
|
|
160
|
+
test("replaces slice with value", () => {
|
|
161
|
+
const snapshots: StateSnapshot[] = [];
|
|
162
|
+
subscribe("http://localhost:3001", (s) => snapshots.push(s));
|
|
163
|
+
|
|
164
|
+
// Set initial state
|
|
165
|
+
lastES().emit("state.initial", {
|
|
166
|
+
type: "state.initial",
|
|
167
|
+
slices: { agent: { conversation: [] } },
|
|
168
|
+
timestamp: 1000,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Full value replacement
|
|
172
|
+
const updateEvent: StateUpdateEvent = {
|
|
173
|
+
type: "state.update",
|
|
174
|
+
slice: "agent",
|
|
175
|
+
value: { conversation: ["msg1"], loopCount: 5 },
|
|
176
|
+
timestamp: 2000,
|
|
177
|
+
};
|
|
178
|
+
lastES().emit("state.update", updateEvent);
|
|
179
|
+
|
|
180
|
+
expect(snapshots).toHaveLength(2);
|
|
181
|
+
expect(snapshots[1].slices.agent).toEqual({ conversation: ["msg1"], loopCount: 5 });
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("multiple updates accumulate state", () => {
|
|
186
|
+
test("state accumulates across multiple events", () => {
|
|
187
|
+
const snapshots: StateSnapshot[] = [];
|
|
188
|
+
subscribe("http://localhost:3001", (s) => snapshots.push(s));
|
|
189
|
+
|
|
190
|
+
// Initial
|
|
191
|
+
lastES().emit("state.initial", {
|
|
192
|
+
type: "state.initial",
|
|
193
|
+
slices: { counter: { value: 0 } },
|
|
194
|
+
timestamp: 1000,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Update 1
|
|
198
|
+
lastES().emit("state.update", {
|
|
199
|
+
type: "state.update",
|
|
200
|
+
slice: "counter",
|
|
201
|
+
patches: [{ op: "replace", path: "/value", value: 1 }],
|
|
202
|
+
timestamp: 2000,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Update 2
|
|
206
|
+
lastES().emit("state.update", {
|
|
207
|
+
type: "state.update",
|
|
208
|
+
slice: "counter",
|
|
209
|
+
patches: [{ op: "replace", path: "/value", value: 2 }],
|
|
210
|
+
timestamp: 3000,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Update 3 — new slice via value
|
|
214
|
+
lastES().emit("state.update", {
|
|
215
|
+
type: "state.update",
|
|
216
|
+
slice: "execution",
|
|
217
|
+
value: { id: "exec-1", state: "running" },
|
|
218
|
+
timestamp: 4000,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
expect(snapshots).toHaveLength(4);
|
|
222
|
+
|
|
223
|
+
// Final state should have both slices
|
|
224
|
+
const final = snapshots[3].slices;
|
|
225
|
+
expect(final.counter).toEqual({ value: 2 });
|
|
226
|
+
expect(final.execution).toEqual({ id: "exec-1", state: "running" });
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("non-state events", () => {
|
|
231
|
+
test("execution.status fires callback without mutating state", () => {
|
|
232
|
+
const snapshots: StateSnapshot[] = [];
|
|
233
|
+
subscribe("http://localhost:3001", (s) => snapshots.push(s));
|
|
234
|
+
|
|
235
|
+
// Initial
|
|
236
|
+
lastES().emit("state.initial", {
|
|
237
|
+
type: "state.initial",
|
|
238
|
+
slices: { agent: { value: 1 } },
|
|
239
|
+
timestamp: 1000,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Execution status
|
|
243
|
+
const statusEvent: ExecutionStatusEvent = {
|
|
244
|
+
type: "execution.status",
|
|
245
|
+
status: "started",
|
|
246
|
+
executionId: "exec-1",
|
|
247
|
+
timestamp: 2000,
|
|
248
|
+
};
|
|
249
|
+
lastES().emit("execution.status", statusEvent);
|
|
250
|
+
|
|
251
|
+
expect(snapshots).toHaveLength(2);
|
|
252
|
+
// State should be unchanged
|
|
253
|
+
expect(snapshots[1].slices).toEqual({ agent: { value: 1 } });
|
|
254
|
+
expect(snapshots[1].event).toEqual(statusEvent);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("heartbeat fires callback without mutating state", () => {
|
|
258
|
+
const snapshots: StateSnapshot[] = [];
|
|
259
|
+
subscribe("http://localhost:3001", (s) => snapshots.push(s));
|
|
260
|
+
|
|
261
|
+
lastES().emit("state.initial", {
|
|
262
|
+
type: "state.initial",
|
|
263
|
+
slices: {},
|
|
264
|
+
timestamp: 1000,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const heartbeat: HeartbeatEvent = { type: "heartbeat", timestamp: 2000 };
|
|
268
|
+
lastES().emit("heartbeat", heartbeat);
|
|
269
|
+
|
|
270
|
+
expect(snapshots).toHaveLength(2);
|
|
271
|
+
expect(snapshots[1].event).toEqual(heartbeat);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("custom-event fires callback without mutating state", () => {
|
|
275
|
+
const snapshots: StateSnapshot[] = [];
|
|
276
|
+
subscribe("http://localhost:3001", (s) => snapshots.push(s));
|
|
277
|
+
|
|
278
|
+
lastES().emit("state.initial", {
|
|
279
|
+
type: "state.initial",
|
|
280
|
+
slices: { x: 1 },
|
|
281
|
+
timestamp: 1000,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const custom: CustomEvent = {
|
|
285
|
+
type: "custom-event",
|
|
286
|
+
data: { foo: "bar" },
|
|
287
|
+
timestamp: 2000,
|
|
288
|
+
};
|
|
289
|
+
lastES().emit("custom-event", custom);
|
|
290
|
+
|
|
291
|
+
expect(snapshots).toHaveLength(2);
|
|
292
|
+
expect(snapshots[1].slices).toEqual({ x: 1 });
|
|
293
|
+
expect(snapshots[1].event).toEqual(custom);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe("unsubscribe", () => {
|
|
298
|
+
test("closes EventSource", () => {
|
|
299
|
+
const unsub = subscribe("http://localhost:3001", () => {});
|
|
300
|
+
expect(lastES().closed).toBe(false);
|
|
301
|
+
|
|
302
|
+
unsub();
|
|
303
|
+
expect(lastES().closed).toBe(true);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe("snapshot isolation", () => {
|
|
308
|
+
test("each callback receives a separate slices object", () => {
|
|
309
|
+
const snapshots: StateSnapshot[] = [];
|
|
310
|
+
subscribe("http://localhost:3001", (s) => snapshots.push(s));
|
|
311
|
+
|
|
312
|
+
lastES().emit("state.initial", {
|
|
313
|
+
type: "state.initial",
|
|
314
|
+
slices: { a: 1 },
|
|
315
|
+
timestamp: 1000,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
lastES().emit("state.update", {
|
|
319
|
+
type: "state.update",
|
|
320
|
+
slice: "a",
|
|
321
|
+
value: 2,
|
|
322
|
+
timestamp: 2000,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Mutating the first snapshot should not affect the second
|
|
326
|
+
expect(snapshots[0].slices.a).toBe(1);
|
|
327
|
+
expect(snapshots[1].slices.a).toBe(2);
|
|
328
|
+
|
|
329
|
+
// They should be different objects
|
|
330
|
+
expect(snapshots[0].slices).not.toBe(snapshots[1].slices);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
});
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createClient — factory for the typed Gizmo agent SDK
|
|
3
|
+
*
|
|
4
|
+
* One dependency, one createClient(url) call, every operation becomes a
|
|
5
|
+
* thin typed wrapper over the HTTP/SSE API.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { GizmoClientError } from "./errors.ts";
|
|
9
|
+
import { subscribe as sseSubscribe } from "./subscribe.ts";
|
|
10
|
+
import type {
|
|
11
|
+
GizmoManifest,
|
|
12
|
+
InvokeResponse,
|
|
13
|
+
AbortResponse,
|
|
14
|
+
RunSummary,
|
|
15
|
+
RunDetails,
|
|
16
|
+
RunsResponse,
|
|
17
|
+
RunListParams,
|
|
18
|
+
HydrateOptions,
|
|
19
|
+
HydrateResponse,
|
|
20
|
+
HealthResponse,
|
|
21
|
+
DispatchRequest,
|
|
22
|
+
Approval,
|
|
23
|
+
ApprovalsListResponse,
|
|
24
|
+
ApprovalResponse,
|
|
25
|
+
HistoryResponse,
|
|
26
|
+
SubscribeCallback,
|
|
27
|
+
SubscribeOptions,
|
|
28
|
+
Unsubscribe,
|
|
29
|
+
ClientOptions,
|
|
30
|
+
GizmoClient,
|
|
31
|
+
} from "./types.ts";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create a typed client for a Gizmo agent.
|
|
35
|
+
*
|
|
36
|
+
* @param baseUrl - The agent's base URL (e.g., "http://localhost:3001")
|
|
37
|
+
* @param options - Optional fetch implementation and default headers
|
|
38
|
+
*/
|
|
39
|
+
export function createClient(
|
|
40
|
+
baseUrl: string,
|
|
41
|
+
options?: ClientOptions,
|
|
42
|
+
): GizmoClient {
|
|
43
|
+
const _fetch = options?.fetch ?? globalThis.fetch;
|
|
44
|
+
const _headers = options?.headers ?? {};
|
|
45
|
+
|
|
46
|
+
// Strip trailing slash from baseUrl
|
|
47
|
+
const base = baseUrl.replace(/\/$/, "");
|
|
48
|
+
|
|
49
|
+
// ── Internal helpers ───────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
async function get<T>(path: string): Promise<T> {
|
|
52
|
+
const res = await _fetch(`${base}${path}`, {
|
|
53
|
+
method: "GET",
|
|
54
|
+
headers: { ...headers(), Accept: "application/json" },
|
|
55
|
+
});
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
const body = await safeJson(res);
|
|
58
|
+
throw new GizmoClientError(res.status, errorMessage(res, body), body);
|
|
59
|
+
}
|
|
60
|
+
return res.json() as Promise<T>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function post<T>(path: string, body?: unknown): Promise<T> {
|
|
64
|
+
const res = await _fetch(`${base}${path}`, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: { ...headers(), "Content-Type": "application/json", Accept: "application/json" },
|
|
67
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
68
|
+
});
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
const errBody = await safeJson(res);
|
|
71
|
+
throw new GizmoClientError(res.status, errorMessage(res, errBody), errBody);
|
|
72
|
+
}
|
|
73
|
+
return res.json() as Promise<T>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function headers(): Record<string, string> {
|
|
77
|
+
return { ..._headers };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Manifest caching ──────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
let manifestCache: GizmoManifest | null = null;
|
|
83
|
+
|
|
84
|
+
// ── Runs namespace ────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
const runs = {
|
|
87
|
+
async list(params?: RunListParams): Promise<RunSummary[]> {
|
|
88
|
+
const qs = buildQuery(params as Record<string, unknown> | undefined);
|
|
89
|
+
const res = await get<RunsResponse>(`/runs${qs}`);
|
|
90
|
+
return res.runs;
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
async get(executionId: string): Promise<RunDetails> {
|
|
94
|
+
return get<RunDetails>(`/runs/${encodeURIComponent(executionId)}`);
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
async actions(executionId: string): Promise<RunDetails["actions"]> {
|
|
98
|
+
const run = await runs.get(executionId);
|
|
99
|
+
return run.actions;
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
async cancel(executionId: string): Promise<{ status: string; executionId: string }> {
|
|
103
|
+
return post<{ status: string; executionId: string }>(
|
|
104
|
+
`/runs/${encodeURIComponent(executionId)}/cancel`,
|
|
105
|
+
);
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
async hydrate(executionId: string, opts?: HydrateOptions): Promise<HydrateResponse> {
|
|
109
|
+
return post<HydrateResponse>("/hydrate", { executionId, ...opts });
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// ── Approvals namespace ───────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
const approvals = {
|
|
116
|
+
async list(): Promise<Approval[]> {
|
|
117
|
+
const res = await get<ApprovalsListResponse>("/approvals");
|
|
118
|
+
return res.approvals;
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
async history(): Promise<Approval[]> {
|
|
122
|
+
const res = await get<HistoryResponse>("/approvals/history");
|
|
123
|
+
return res.history;
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
async get(id: string): Promise<Approval> {
|
|
127
|
+
const res = await get<ApprovalResponse>(`/approvals/${encodeURIComponent(id)}`);
|
|
128
|
+
return res.approval;
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
async approve(id: string, reason?: string): Promise<Approval> {
|
|
132
|
+
const res = await post<ApprovalResponse>(
|
|
133
|
+
`/approvals/${encodeURIComponent(id)}`,
|
|
134
|
+
{ decision: "approve", reason },
|
|
135
|
+
);
|
|
136
|
+
return res.approval;
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
async reject(id: string, reason?: string): Promise<Approval> {
|
|
140
|
+
const res = await post<ApprovalResponse>(
|
|
141
|
+
`/approvals/${encodeURIComponent(id)}`,
|
|
142
|
+
{ decision: "reject", reason },
|
|
143
|
+
);
|
|
144
|
+
return res.approval;
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// ── Public API ────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
async discover(): Promise<GizmoManifest> {
|
|
152
|
+
if (manifestCache) return manifestCache;
|
|
153
|
+
manifestCache = await get<GizmoManifest>("/.well-known/manifest.json");
|
|
154
|
+
return manifestCache;
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
async invoke(input: string): Promise<InvokeResponse> {
|
|
158
|
+
return post<InvokeResponse>("/invoke", { input });
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
async abort(): Promise<AbortResponse> {
|
|
162
|
+
return post<AbortResponse>("/abort");
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
async state(slice?: string): Promise<unknown> {
|
|
166
|
+
const path = slice ? `/state/${encodeURIComponent(slice)}` : "/state";
|
|
167
|
+
return get<unknown>(path);
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
subscribe(callback: SubscribeCallback, opts?: SubscribeOptions): Unsubscribe {
|
|
171
|
+
return sseSubscribe(base, callback, opts);
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
async health(): Promise<HealthResponse> {
|
|
175
|
+
return get<HealthResponse>("/health");
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
async dispatch(action: DispatchRequest): Promise<unknown> {
|
|
179
|
+
return post<unknown>("/dispatch", action);
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
runs,
|
|
183
|
+
approvals,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Utilities ──────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
function buildQuery(params?: Record<string, unknown>): string {
|
|
190
|
+
if (!params) return "";
|
|
191
|
+
const entries = Object.entries(params).filter(
|
|
192
|
+
([, v]) => v !== undefined && v !== null,
|
|
193
|
+
);
|
|
194
|
+
if (entries.length === 0) return "";
|
|
195
|
+
const qs = entries
|
|
196
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
|
|
197
|
+
.join("&");
|
|
198
|
+
return `?${qs}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function safeJson(res: Response): Promise<unknown> {
|
|
202
|
+
try {
|
|
203
|
+
return await res.json();
|
|
204
|
+
} catch {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function errorMessage(res: Response, body: unknown): string {
|
|
210
|
+
if (body && typeof body === "object" && "error" in body) {
|
|
211
|
+
return String((body as { error: string }).error);
|
|
212
|
+
}
|
|
213
|
+
return `HTTP ${res.status}`;
|
|
214
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GizmoClientError
|
|
3
|
+
*
|
|
4
|
+
* Thrown when the server returns a non-OK HTTP response.
|
|
5
|
+
*/
|
|
6
|
+
export class GizmoClientError extends Error {
|
|
7
|
+
readonly status: number;
|
|
8
|
+
readonly body?: unknown;
|
|
9
|
+
|
|
10
|
+
constructor(status: number, message: string, body?: unknown) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "GizmoClientError";
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.body = body;
|
|
15
|
+
}
|
|
16
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @gizmo-ai/client
|
|
3
|
+
*
|
|
4
|
+
* Typed SDK for Gizmo agents — discover, invoke, subscribe, and manage runs.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { createClient } from "@gizmo-ai/client";
|
|
9
|
+
*
|
|
10
|
+
* const client = createClient("http://localhost:3001");
|
|
11
|
+
*
|
|
12
|
+
* const manifest = await client.discover();
|
|
13
|
+
* const { executionId } = await client.invoke("Hello!");
|
|
14
|
+
* const unsub = client.subscribe(({ slices, event }) => {
|
|
15
|
+
* console.log(event.type, slices);
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export { createClient } from "./client.ts";
|
|
21
|
+
export { GizmoClientError } from "./errors.ts";
|
|
22
|
+
|
|
23
|
+
export type {
|
|
24
|
+
// Client
|
|
25
|
+
GizmoClient,
|
|
26
|
+
ClientOptions,
|
|
27
|
+
|
|
28
|
+
// Manifest
|
|
29
|
+
GizmoManifest,
|
|
30
|
+
|
|
31
|
+
// SSE Events
|
|
32
|
+
SSEEvent,
|
|
33
|
+
InitialStateEvent,
|
|
34
|
+
StateUpdateEvent,
|
|
35
|
+
ExecutionStatusEvent,
|
|
36
|
+
HeartbeatEvent,
|
|
37
|
+
CustomEvent,
|
|
38
|
+
|
|
39
|
+
// API Types
|
|
40
|
+
InvokeResponse,
|
|
41
|
+
AbortResponse,
|
|
42
|
+
RunSummary,
|
|
43
|
+
RunDetails,
|
|
44
|
+
RunsResponse,
|
|
45
|
+
RunListParams,
|
|
46
|
+
HydrateOptions,
|
|
47
|
+
HydrateResponse,
|
|
48
|
+
HealthResponse,
|
|
49
|
+
DispatchRequest,
|
|
50
|
+
|
|
51
|
+
// Approvals
|
|
52
|
+
ApprovalStatus,
|
|
53
|
+
Approval,
|
|
54
|
+
ApprovalsListResponse,
|
|
55
|
+
ApprovalResponse,
|
|
56
|
+
HistoryResponse,
|
|
57
|
+
|
|
58
|
+
// Subscribe
|
|
59
|
+
StateSnapshot,
|
|
60
|
+
SubscribeCallback,
|
|
61
|
+
SubscribeOptions,
|
|
62
|
+
Unsubscribe,
|
|
63
|
+
} from "./types.ts";
|