@tai42/api-client 0.1.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.
Files changed (48) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +28 -0
  3. package/dist/agents.d.ts +265 -0
  4. package/dist/agents.d.ts.map +1 -0
  5. package/dist/agents.js +200 -0
  6. package/dist/agents.js.map +1 -0
  7. package/dist/client.d.ts +1316 -0
  8. package/dist/client.d.ts.map +1 -0
  9. package/dist/client.js +468 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/errors.d.ts +43 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/errors.js +63 -0
  14. package/dist/errors.js.map +1 -0
  15. package/dist/fleet-report.d.ts +61 -0
  16. package/dist/fleet-report.d.ts.map +1 -0
  17. package/dist/fleet-report.js +42 -0
  18. package/dist/fleet-report.js.map +1 -0
  19. package/dist/http.d.ts +69 -0
  20. package/dist/http.d.ts.map +1 -0
  21. package/dist/http.js +152 -0
  22. package/dist/http.js.map +1 -0
  23. package/dist/index.d.ts +23 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +16 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/schemas.d.ts +5469 -0
  28. package/dist/schemas.d.ts.map +1 -0
  29. package/dist/schemas.js +1103 -0
  30. package/dist/schemas.js.map +1 -0
  31. package/dist/sse.d.ts +27 -0
  32. package/dist/sse.d.ts.map +1 -0
  33. package/dist/sse.js +84 -0
  34. package/dist/sse.js.map +1 -0
  35. package/dist/tool-runs.d.ts +112 -0
  36. package/dist/tool-runs.d.ts.map +1 -0
  37. package/dist/tool-runs.js +70 -0
  38. package/dist/tool-runs.js.map +1 -0
  39. package/package.json +51 -0
  40. package/src/agents.ts +248 -0
  41. package/src/client.ts +837 -0
  42. package/src/errors.ts +67 -0
  43. package/src/fleet-report.ts +94 -0
  44. package/src/http.ts +198 -0
  45. package/src/index.ts +142 -0
  46. package/src/schemas.ts +1341 -0
  47. package/src/sse.ts +90 -0
  48. package/src/tool-runs.ts +103 -0
package/src/agents.ts ADDED
@@ -0,0 +1,248 @@
1
+ /**
2
+ * The agents transport: the `GET /api/agents` list schema and the
3
+ * `POST /api/agents/{name}/runs` streaming run, plus the zod discriminated union
4
+ * over the agent `StreamEvent` taxonomy.
5
+ *
6
+ * This is a SELF-CONTAINED api-client module (its own zod, not the shared
7
+ * `schemas.ts`), so the agents feature owns its wire types end to end. The
8
+ * `createApiClient` object wires two thin methods onto it (`listAgents`,
9
+ * `streamAgentRun`) since only it holds the `ApiConfig` (base URL + auth token).
10
+ *
11
+ * Frame discriminator: every event carries a stable `type`, which is also the
12
+ * client's switch. A frame whose `type` is unknown (a newer server event) is
13
+ * NEVER dropped — it is surfaced as an `unknown` parsed event so the UI can show
14
+ * it verbatim; a malformed frame is surfaced the same way rather than crashing
15
+ * the stream.
16
+ */
17
+ import { z } from 'zod';
18
+
19
+ import { ApiError, ApiUnauthorizedError } from './errors';
20
+ import { encodeSegment, extractError, type ApiConfig } from './http';
21
+ import { SseFrameParser, readSseFrames, type SseFrame } from './sse';
22
+
23
+ // -- list schema -------------------------------------------------------------
24
+
25
+ /** One registered agent as `GET /api/agents` returns it. */
26
+ export const agentSummary = z.object({
27
+ name: z.string(),
28
+ description: z.string().default(''),
29
+ tool_name: z.string(),
30
+ // The agent's `ToolInput` JSON schema (the same schema its run tool exposes).
31
+ input_schema: z.record(z.unknown()).default({}),
32
+ // The agent's own authorability marker (read, never inferred): `true` means its
33
+ // `ToolInput` accepts the composable spec fields and it can be authored into a
34
+ // versioned agent. A code role-agent reports `false`. The same item shape is
35
+ // returned by `GET /api/agents/spec-runnable`, filtered to the `true` rows.
36
+ spec_runnable: z.boolean().default(false),
37
+ });
38
+
39
+ export const agentList = z.object({
40
+ items: z.array(agentSummary),
41
+ total: z.number(),
42
+ });
43
+
44
+ export type AgentSummary = z.infer<typeof agentSummary>;
45
+
46
+ // -- event union (mirrors the contract StreamEvent taxonomy) -----------------
47
+
48
+ const reasoningStep = z.object({
49
+ type: z.literal('reasoning_step'),
50
+ final: z.boolean().default(false),
51
+ text: z.string(),
52
+ });
53
+ const toolCallStep = z.object({
54
+ type: z.literal('tool_call_step'),
55
+ final: z.boolean().default(false),
56
+ tool: z.string(),
57
+ args: z.record(z.unknown()).default({}),
58
+ call_id: z.string(),
59
+ });
60
+ const toolResultStep = z.object({
61
+ type: z.literal('tool_result_step'),
62
+ final: z.boolean().default(false),
63
+ tool: z.string(),
64
+ call_id: z.string(),
65
+ result: z.unknown(),
66
+ is_error: z.boolean().default(false),
67
+ });
68
+ const messageDelta = z.object({
69
+ type: z.literal('message_delta'),
70
+ final: z.boolean().default(false),
71
+ text: z.string(),
72
+ });
73
+ const messageFinal = z.object({
74
+ type: z.literal('message_final'),
75
+ final: z.boolean().default(true),
76
+ text: z.string(),
77
+ });
78
+ const runUsage = z.object({
79
+ type: z.literal('run_usage'),
80
+ final: z.boolean().default(false),
81
+ input_tokens: z.number().nullable().default(null),
82
+ output_tokens: z.number().nullable().default(null),
83
+ total_tokens: z.number().nullable().default(null),
84
+ model: z.string().nullable().default(null),
85
+ });
86
+ const structuredFinal = z.object({
87
+ type: z.literal('structured_final'),
88
+ final: z.boolean().default(true),
89
+ data: z.unknown(),
90
+ });
91
+ const interruptFinal = z.object({
92
+ type: z.literal('interrupt_final'),
93
+ final: z.boolean().default(true),
94
+ interrupt_id: z.string(),
95
+ payload: z.unknown(),
96
+ reason: z.string().nullable().default(null),
97
+ });
98
+ // The two SSE-layer terminal frames the run route emits (not contract events).
99
+ const streamEnd = z.object({ type: z.literal('stream.end') });
100
+ const streamError = z.object({ type: z.literal('stream.error'), message: z.string() });
101
+
102
+ /** The discriminated union of every KNOWN agent frame. */
103
+ export const agentEventSchema = z.discriminatedUnion('type', [
104
+ reasoningStep,
105
+ toolCallStep,
106
+ toolResultStep,
107
+ messageDelta,
108
+ messageFinal,
109
+ runUsage,
110
+ structuredFinal,
111
+ interruptFinal,
112
+ streamEnd,
113
+ streamError,
114
+ ]);
115
+
116
+ /** A known, validated agent frame. */
117
+ export type AgentEvent = z.infer<typeof agentEventSchema>;
118
+
119
+ /**
120
+ * A frame the client could not validate against a known event — an unrecognized
121
+ * `type` (forward-compat) or a malformed payload. It is kept, never dropped, so
122
+ * the UI renders it visibly as a labeled raw row.
123
+ */
124
+ export interface UnknownAgentEvent {
125
+ readonly type: string;
126
+ readonly raw: unknown;
127
+ }
128
+
129
+ /** The result of parsing one SSE frame: a known event or an `unknown` fallback. */
130
+ export type ParsedAgentEvent =
131
+ | { readonly known: true; readonly event: AgentEvent }
132
+ | { readonly known: false; readonly unknown: UnknownAgentEvent };
133
+
134
+ /**
135
+ * Parse one SSE frame's `data` into a typed event. Unparseable JSON, a
136
+ * non-object payload, an unknown `type`, or a payload that fails its schema all
137
+ * resolve to an `unknown` fallback (visible, never silently skipped) rather than
138
+ * throwing and tearing down the stream.
139
+ */
140
+ export function parseAgentFrame(frame: SseFrame): ParsedAgentEvent {
141
+ let payload: unknown;
142
+ try {
143
+ payload = JSON.parse(frame.data);
144
+ } catch {
145
+ return { known: false, unknown: { type: '(unparseable)', raw: frame.data } };
146
+ }
147
+ const result = agentEventSchema.safeParse(payload);
148
+ if (result.success) return { known: true, event: result.data };
149
+ const rawType =
150
+ typeof payload === 'object' &&
151
+ payload !== null &&
152
+ typeof (payload as { type?: unknown }).type === 'string'
153
+ ? (payload as { type: string }).type
154
+ : '(unknown)';
155
+ return { known: false, unknown: { type: rawType, raw: payload } };
156
+ }
157
+
158
+ // -- streaming run -----------------------------------------------------------
159
+
160
+ async function* parseFrames(frames: AsyncGenerator<SseFrame>): AsyncGenerator<ParsedAgentEvent> {
161
+ for await (const frame of frames) {
162
+ yield parseAgentFrame(frame);
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Open an authed agent-run SSE stream over `fetch` + `ReadableStream` (never
168
+ * `EventSource`, which cannot send the auth header). The POST body is the run
169
+ * input; a non-2xx open throws loudly (a body that parsed to zero frames would
170
+ * otherwise look like a silently-empty run). Aborting `signal` closes the stream,
171
+ * which the server treats as a disconnect and cancels the run. The path is the
172
+ * ONLY difference between a plain-agent run and an authored-agent run, so both
173
+ * faces share this opener.
174
+ */
175
+ async function openAgentStream(
176
+ config: ApiConfig,
177
+ path: string,
178
+ input: unknown,
179
+ signal?: AbortSignal,
180
+ ): Promise<AsyncGenerator<ParsedAgentEvent>> {
181
+ const doFetch = config.fetch ?? globalThis.fetch;
182
+ const token = config.getToken();
183
+ const headers: Record<string, string> = {
184
+ accept: 'text/event-stream',
185
+ 'content-type': 'application/json',
186
+ };
187
+ if (token) headers['x-api-key'] = token;
188
+ const response = await doFetch(`${config.baseUrl ?? ''}${path}`, {
189
+ method: 'POST',
190
+ headers,
191
+ body: JSON.stringify(input),
192
+ signal,
193
+ });
194
+ if (response.status === 401) throw new ApiUnauthorizedError();
195
+ if (!response.ok) {
196
+ // Read the `{ "error" }` envelope so a load-bearing server message (e.g. an
197
+ // authored-run "cannot override the fixed field ..." 400) surfaces verbatim
198
+ // instead of collapsing to the bare status text. Both run doors share this
199
+ // opener, so both surface the server's message.
200
+ let payload: unknown;
201
+ try {
202
+ payload = await response.json();
203
+ } catch {
204
+ throw new ApiError(response.statusText || 'agent run failed', response.status);
205
+ }
206
+ const { message, code } = extractError(payload);
207
+ throw new ApiError(
208
+ message ?? (response.statusText || 'agent run failed'),
209
+ response.status,
210
+ code,
211
+ );
212
+ }
213
+ return parseFrames(readSseFrames(response, signal));
214
+ }
215
+
216
+ /** Stream a plain registered agent's run (`POST /api/agents/{name}/runs`). */
217
+ export async function streamAgentRun(
218
+ config: ApiConfig,
219
+ name: string,
220
+ input: unknown,
221
+ signal?: AbortSignal,
222
+ ): Promise<AsyncGenerator<ParsedAgentEvent>> {
223
+ return openAgentStream(config, `/api/agents/${encodeSegment(name)}/runs`, input, signal);
224
+ }
225
+
226
+ /**
227
+ * Stream an AUTHORED agent's run (`POST /api/agents/authored/{name}/runs`). Same
228
+ * SSE framing as `streamAgentRun`; the baked spec is resolved server-side, so the
229
+ * POST body carries ONLY the non-baked `ToolInput` fields (the actual query/user
230
+ * input). Naming a baked field is a loud server 400, surfaced by the caller.
231
+ */
232
+ export async function streamAuthoredAgentRun(
233
+ config: ApiConfig,
234
+ name: string,
235
+ input: unknown,
236
+ signal?: AbortSignal,
237
+ ): Promise<AsyncGenerator<ParsedAgentEvent>> {
238
+ return openAgentStream(config, `/api/agents/authored/${encodeSegment(name)}/runs`, input, signal);
239
+ }
240
+
241
+ /**
242
+ * Parse a whole hand-authored SSE transcript into its ordered parsed events.
243
+ * Reuses the wire `SseFrameParser`, so a test fixture exercises the real
244
+ * frame-splitting + event-parsing path end to end.
245
+ */
246
+ export function parseAgentTranscript(transcript: string): ParsedAgentEvent[] {
247
+ return new SseFrameParser().push(transcript).map(parseAgentFrame);
248
+ }