@womp/spark-agent 4.1.1 → 4.1.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/agent.js CHANGED
@@ -1,9 +1,10 @@
1
+ import { sparkErrorDiagnostics } from "./error-diagnostics.js";
1
2
  import { createUIMessageStream, stepCountIs, streamText, } from "ai";
2
3
  import { createSparkTools } from "./agent-tools.js";
3
4
  import { SPARK_AGENT_PROMPT } from "./prompt.js";
4
5
  import { calculateStepCost } from "./pricing.js";
5
6
  function errorMessage(error) {
6
- return error instanceof Error ? error.message : String(error);
7
+ return sparkErrorDiagnostics(error).message;
7
8
  }
8
9
  export function runSparkAgent(options) {
9
10
  const maxSteps = options.maxSteps ?? 100;
@@ -187,7 +188,15 @@ export function runSparkAgent(options) {
187
188
  const status = options.signal?.aborted ? "cancelled" : "failed";
188
189
  writer.write({
189
190
  type: "message-metadata",
190
- messageMetadata: { ...metadata(status), error: errorMessage(failure) },
191
+ messageMetadata: {
192
+ ...metadata(status),
193
+ error: errorMessage(failure),
194
+ ...(status === "cancelled"
195
+ ? {}
196
+ : {
197
+ errorDiagnostics: sparkErrorDiagnostics(failure),
198
+ }),
199
+ },
191
200
  });
192
201
  rejectResult(failure);
193
202
  throw failure;
@@ -0,0 +1,32 @@
1
+ import { z } from "zod";
2
+ /** Allowlisted diagnostics only; never serialize an SDK error or request wholesale. */
3
+ export type SparkErrorDiagnostics = {
4
+ message: string;
5
+ name?: string;
6
+ code?: string;
7
+ status?: number;
8
+ requestId?: string;
9
+ cause?: SparkErrorDiagnostics;
10
+ };
11
+ export declare const sparkErrorDiagnosticsSchema: z.ZodObject<{
12
+ cause: z.ZodOptional<z.ZodObject<{
13
+ message: z.ZodString;
14
+ name: z.ZodOptional<z.ZodString>;
15
+ code: z.ZodOptional<z.ZodString>;
16
+ status: z.ZodOptional<z.ZodNumber>;
17
+ requestId: z.ZodOptional<z.ZodString>;
18
+ cause: z.ZodOptional<z.ZodObject<{
19
+ message: z.ZodString;
20
+ name: z.ZodOptional<z.ZodString>;
21
+ code: z.ZodOptional<z.ZodString>;
22
+ status: z.ZodOptional<z.ZodNumber>;
23
+ requestId: z.ZodOptional<z.ZodString>;
24
+ }, z.core.$strip>>;
25
+ }, z.core.$strip>>;
26
+ message: z.ZodString;
27
+ name: z.ZodOptional<z.ZodString>;
28
+ code: z.ZodOptional<z.ZodString>;
29
+ status: z.ZodOptional<z.ZodNumber>;
30
+ requestId: z.ZodOptional<z.ZodString>;
31
+ }, z.core.$strip>;
32
+ export declare function sparkErrorDiagnostics(error: unknown): SparkErrorDiagnostics;
@@ -0,0 +1,80 @@
1
+ import { z } from "zod";
2
+ const fields = {
3
+ message: z.string().max(1024),
4
+ name: z.string().max(128).optional(),
5
+ code: z.string().max(128).optional(),
6
+ status: z.number().int().min(100).max(599).optional(),
7
+ requestId: z.string().max(128).optional(),
8
+ };
9
+ const causeSchema = z.object(fields);
10
+ export const sparkErrorDiagnosticsSchema = z.object({
11
+ ...fields,
12
+ cause: causeSchema.extend({ cause: causeSchema.optional() }).optional(),
13
+ });
14
+ function record(value) {
15
+ return typeof value === "object" && value !== null ? value : {};
16
+ }
17
+ function responseData(value) {
18
+ if (typeof value !== "string" || value.length > 16384)
19
+ return {};
20
+ try {
21
+ return record(JSON.parse(value));
22
+ }
23
+ catch {
24
+ return {};
25
+ }
26
+ }
27
+ function text(value, limit = 1024) {
28
+ if (typeof value !== "string" && typeof value !== "number")
29
+ return undefined;
30
+ // Bound work as well as storage. Embedded JSON/request dumps are not diagnostics.
31
+ return String(value)
32
+ .slice(0, 8192)
33
+ .replace(/\{[\s\S]*|\[(?=\s*["{0-9])[\s\S]*/u, "[payload omitted]")
34
+ .replace(/\b(?:request(?:\s*body)?|response\s*body|payload|headers|image|prompt)\s*[:=][\s\S]*/giu, "[payload omitted]")
35
+ .replace(/data:[^\s]+/giu, "[data omitted]")
36
+ .replace(/https?:\/\/[^\s]+/giu, "[url omitted]")
37
+ .replace(/\b(?:authorization|proxy-authorization|cookie|set-cookie|x-api-key|api[_ -]?key|access[_ -]?token|refresh[_ -]?token|token|password|(?:client[_ -]?)?secret)\b\s*[:=]\s*[^\r\n,;]+/giu, "[credential omitted]")
38
+ .replace(/\b(?:Bearer|Basic)\s+[^\s,;]+/giu, "[credential omitted]")
39
+ .replace(/\b(?:sk-|sk_|AIza)[A-Za-z0-9_-]+/gu, "[credential omitted]")
40
+ .replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/gu, "[credential omitted]")
41
+ .replace(/[A-Za-z0-9+/=_-]{200,}/gu, "[data omitted]")
42
+ .slice(0, limit);
43
+ }
44
+ export function sparkErrorDiagnostics(error) {
45
+ const seen = new Set();
46
+ const visit = (value, depth) => {
47
+ seen.add(value);
48
+ const source = record(value);
49
+ const data = record(source.data);
50
+ const body = responseData(source.responseBody);
51
+ const provider = record(data.error ?? source.error ?? body.error ?? body);
52
+ const headers = record(source.responseHeaders);
53
+ const status = source.statusCode ?? source.status ?? provider.status;
54
+ const result = {
55
+ message: text(source.message ?? provider.message ?? value) || "An error occurred.",
56
+ };
57
+ const name = text(source.name, 128);
58
+ const code = text(source.code ?? provider.code ?? data.code, 128);
59
+ const requestId = text(source.requestId ??
60
+ source.request_id ??
61
+ provider.request_id ??
62
+ body.request_id ??
63
+ headers["x-request-id"] ??
64
+ headers["request-id"] ??
65
+ headers["x-amzn-requestid"], 128);
66
+ if (name)
67
+ result.name = name;
68
+ if (code)
69
+ result.code = code;
70
+ if (typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599)
71
+ result.status = status;
72
+ if (requestId)
73
+ result.requestId = requestId;
74
+ const cause = source.cause ?? source.lastError;
75
+ if (depth < 2 && cause !== undefined && !seen.has(cause))
76
+ result.cause = visit(cause, depth + 1);
77
+ return result;
78
+ };
79
+ return visit(error, 0);
80
+ }
@@ -1,3 +1,4 @@
1
+ import { sparkErrorDiagnostics } from "../error-diagnostics.js";
1
2
  import { readUIMessageStream, validateUIMessages } from "ai";
2
3
  import { sparkDataPartSchemas, sparkMessageMetadataSchema, } from "../ui.js";
3
4
  const TERMINAL_STATUSES = new Set([
@@ -102,7 +103,7 @@ export class SparkAgentService {
102
103
  }
103
104
  async deleteConversation(ownerId, id) {
104
105
  await this.getConversation(ownerId, id);
105
- if (await this.store.deleteConversation(id) === false) {
106
+ if ((await this.store.deleteConversation(id)) === false) {
106
107
  throw new SparkServiceError(409, "Stop the active run before deleting this conversation.");
107
108
  }
108
109
  }
@@ -209,7 +210,9 @@ export class SparkAgentService {
209
210
  this.controllers.set(run.id, controller);
210
211
  const execution = this.runInBackground(run, conversation, controller);
211
212
  this.executions.set(run.id, execution);
212
- void execution.finally(() => this.executions.delete(run.id)).catch((error) => {
213
+ void execution
214
+ .finally(() => this.executions.delete(run.id))
215
+ .catch((error) => {
213
216
  console.error("Spark run persistence failed", error);
214
217
  });
215
218
  return run;
@@ -314,7 +317,7 @@ export class SparkAgentService {
314
317
  continue;
315
318
  await this.appendChunk(run, chunk);
316
319
  }
317
- })();
320
+ })().then(() => undefined, (error) => error);
318
321
  let executionError;
319
322
  let result;
320
323
  try {
@@ -323,9 +326,11 @@ export class SparkAgentService {
323
326
  catch (error) {
324
327
  executionError = error;
325
328
  }
326
- await consume;
329
+ const streamError = await consume;
327
330
  if (executionError !== undefined)
328
331
  throw executionError;
332
+ if (streamError !== undefined)
333
+ throw streamError;
329
334
  if (controller.signal.aborted) {
330
335
  await this.appendStatus(run, "cancelled");
331
336
  await this.finishRun(run, conversation, "cancelled");
@@ -341,25 +346,26 @@ export class SparkAgentService {
341
346
  await this.finishRun(run, conversation, "cancelled");
342
347
  return;
343
348
  }
344
- const message = error instanceof Error ? error.message : String(error);
349
+ const diagnostics = sparkErrorDiagnostics(error);
350
+ const message = diagnostics.message;
345
351
  const failedChunk = {
346
352
  type: "data-run-status",
347
353
  id: `run-status-${run.id}`,
348
- data: { runId: run.id, status: "failed", error: message },
354
+ data: { runId: run.id, status: "failed", error: message, errorDiagnostics: diagnostics },
349
355
  };
350
356
  await this.appendChunk(run, { type: "error", errorText: message });
351
357
  await this.appendChunk(run, failedChunk);
352
- await this.finishRun(run, conversation, "failed", message);
358
+ await this.finishRun(run, conversation, "failed", message, diagnostics);
353
359
  }
354
360
  finally {
355
361
  this.controllers.delete(run.id);
356
362
  }
357
363
  }
358
- async finishRun(run, conversation, status, error) {
364
+ async finishRun(run, conversation, status, error, errorDiagnostics) {
359
365
  const completedAt = now();
360
366
  const storedChunks = await this.store.listRunChunks(run.id, 0);
361
367
  const chunks = storedChunks.map(({ chunk }) => chunk);
362
- const assistant = await this.materializeMessage(run, chunks, status, completedAt, error);
368
+ const assistant = await this.materializeMessage(run, chunks, status, completedAt, error, errorDiagnostics);
363
369
  if (assistant !== undefined) {
364
370
  await this.appendChunk(run, {
365
371
  type: "message-metadata",
@@ -379,12 +385,13 @@ export class SparkAgentService {
379
385
  ...run,
380
386
  status,
381
387
  ...(error === undefined ? {} : { error }),
388
+ ...(errorDiagnostics === undefined ? {} : { errorDiagnostics }),
382
389
  updatedAt: completedAt,
383
390
  completedAt,
384
391
  });
385
392
  this.wakeRun(run.id);
386
393
  }
387
- async materializeMessage(run, chunks, status, completedAt, error) {
394
+ async materializeMessage(run, chunks, status, completedAt, error, errorDiagnostics) {
388
395
  let message;
389
396
  try {
390
397
  for await (const current of readUIMessageStream({
@@ -412,6 +419,7 @@ export class SparkAgentService {
412
419
  modelId: run.settings.modelId,
413
420
  ...(message.metadata?.usage === undefined ? {} : { usage: message.metadata.usage }),
414
421
  ...(error === undefined ? {} : { error }),
422
+ ...(errorDiagnostics === undefined ? {} : { errorDiagnostics }),
415
423
  },
416
424
  };
417
425
  }
@@ -423,6 +431,23 @@ export class SparkAgentService {
423
431
  });
424
432
  }
425
433
  async appendChunk(run, chunk) {
434
+ if (chunk.type === "error")
435
+ chunk = { ...chunk, errorText: sparkErrorDiagnostics(chunk.errorText).message };
436
+ if (chunk.type === "message-metadata" || chunk.type === "data-run-status") {
437
+ const data = chunk.type === "message-metadata" ? chunk.messageMetadata : chunk.data;
438
+ const safe = {
439
+ ...(data.error === undefined ? {} : { error: sparkErrorDiagnostics(data.error).message }),
440
+ ...(data.errorDiagnostics === undefined
441
+ ? {}
442
+ : {
443
+ errorDiagnostics: sparkErrorDiagnostics(data.errorDiagnostics),
444
+ }),
445
+ };
446
+ chunk =
447
+ chunk.type === "message-metadata"
448
+ ? { ...chunk, messageMetadata: { ...chunk.messageMetadata, ...safe } }
449
+ : { ...chunk, data: { ...chunk.data, ...safe } };
450
+ }
426
451
  const stored = await this.store.appendRunChunk(run.id, run.conversationId, chunk);
427
452
  for (const listener of this.listeners.get(run.id) ?? [])
428
453
  listener(stored);
@@ -1,3 +1,4 @@
1
+ import type { SparkErrorDiagnostics } from "../error-diagnostics.js";
1
2
  import type { SparkAgentRun } from "../agent.js";
2
3
  import type { ReasoningEffort } from "../models.js";
3
4
  import type { SparkRunStatus, SparkUIMessage, StoredSparkChunk } from "../ui.js";
@@ -26,6 +27,7 @@ export type SparkRun = {
26
27
  status: SparkRunStatus;
27
28
  settings: SparkRunSettings;
28
29
  error?: string;
30
+ errorDiagnostics?: SparkErrorDiagnostics;
29
31
  createdAt: string;
30
32
  updatedAt: string;
31
33
  startedAt?: string;
package/dist/ui.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { type SparkErrorDiagnostics } from "./error-diagnostics.js";
2
+ export { sparkErrorDiagnostics, sparkErrorDiagnosticsSchema, type SparkErrorDiagnostics, } from "./error-diagnostics.js";
1
3
  import type { InferUIMessageChunk, UIMessage } from "ai";
2
4
  import type { SceneScreenshotOptions } from "./capabilities/scene.js";
3
5
  import { z } from "zod";
@@ -24,6 +26,7 @@ export type SparkMessageMetadata = {
24
26
  modelId?: string;
25
27
  usage?: unknown;
26
28
  error?: string;
29
+ errorDiagnostics?: SparkErrorDiagnostics;
27
30
  };
28
31
  export type SparkConsoleData = {
29
32
  stream: "stdout" | "stderr";
@@ -33,6 +36,7 @@ export type SparkRunStatusData = {
33
36
  runId: string;
34
37
  status: SparkRunStatus;
35
38
  error?: string;
39
+ errorDiagnostics?: SparkErrorDiagnostics;
36
40
  label?: string;
37
41
  };
38
42
  export declare const sparkStepCostSchema: z.ZodObject<{
@@ -157,6 +161,27 @@ export declare const sparkMessageMetadataSchema: z.ZodObject<{
157
161
  modelId: z.ZodOptional<z.ZodString>;
158
162
  usage: z.ZodOptional<z.ZodUnknown>;
159
163
  error: z.ZodOptional<z.ZodString>;
164
+ errorDiagnostics: z.ZodOptional<z.ZodObject<{
165
+ cause: z.ZodOptional<z.ZodObject<{
166
+ message: z.ZodString;
167
+ name: z.ZodOptional<z.ZodString>;
168
+ code: z.ZodOptional<z.ZodString>;
169
+ status: z.ZodOptional<z.ZodNumber>;
170
+ requestId: z.ZodOptional<z.ZodString>;
171
+ cause: z.ZodOptional<z.ZodObject<{
172
+ message: z.ZodString;
173
+ name: z.ZodOptional<z.ZodString>;
174
+ code: z.ZodOptional<z.ZodString>;
175
+ status: z.ZodOptional<z.ZodNumber>;
176
+ requestId: z.ZodOptional<z.ZodString>;
177
+ }, z.core.$strip>>;
178
+ }, z.core.$strip>>;
179
+ message: z.ZodString;
180
+ name: z.ZodOptional<z.ZodString>;
181
+ code: z.ZodOptional<z.ZodString>;
182
+ status: z.ZodOptional<z.ZodNumber>;
183
+ requestId: z.ZodOptional<z.ZodString>;
184
+ }, z.core.$strip>>;
160
185
  }, z.core.$strip>;
161
186
  export declare const sparkDataPartSchemas: {
162
187
  readonly "tool-input-label": z.ZodObject<{
@@ -211,6 +236,27 @@ export declare const sparkDataPartSchemas: {
211
236
  incomplete: "incomplete";
212
237
  }>;
213
238
  error: z.ZodOptional<z.ZodString>;
239
+ errorDiagnostics: z.ZodOptional<z.ZodObject<{
240
+ cause: z.ZodOptional<z.ZodObject<{
241
+ message: z.ZodString;
242
+ name: z.ZodOptional<z.ZodString>;
243
+ code: z.ZodOptional<z.ZodString>;
244
+ status: z.ZodOptional<z.ZodNumber>;
245
+ requestId: z.ZodOptional<z.ZodString>;
246
+ cause: z.ZodOptional<z.ZodObject<{
247
+ message: z.ZodString;
248
+ name: z.ZodOptional<z.ZodString>;
249
+ code: z.ZodOptional<z.ZodString>;
250
+ status: z.ZodOptional<z.ZodNumber>;
251
+ requestId: z.ZodOptional<z.ZodString>;
252
+ }, z.core.$strip>>;
253
+ }, z.core.$strip>>;
254
+ message: z.ZodString;
255
+ name: z.ZodOptional<z.ZodString>;
256
+ code: z.ZodOptional<z.ZodString>;
257
+ status: z.ZodOptional<z.ZodNumber>;
258
+ requestId: z.ZodOptional<z.ZodString>;
259
+ }, z.core.$strip>>;
214
260
  label: z.ZodOptional<z.ZodString>;
215
261
  }, z.core.$strip>;
216
262
  readonly "step-metrics": z.ZodObject<{
package/dist/ui.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { sparkErrorDiagnosticsSchema } from "./error-diagnostics.js";
2
+ export { sparkErrorDiagnostics, sparkErrorDiagnosticsSchema, } from "./error-diagnostics.js";
1
3
  import { z } from "zod";
2
4
  export { DEFAULT_MODEL_ID, getSparkModel, REASONING_EFFORT_LABELS, resolveReasoningEffort, SPARK_MODELS, } from "./models.js";
3
5
  export const SPARK_RUN_STATUSES = [
@@ -48,9 +50,14 @@ export const sparkMessageMetadataSchema = z.object({
48
50
  modelId: z.string().optional(),
49
51
  usage: z.unknown().optional(),
50
52
  error: z.string().optional(),
53
+ errorDiagnostics: sparkErrorDiagnosticsSchema.optional(),
51
54
  });
52
55
  export const sparkDataPartSchemas = {
53
- "tool-input-label": z.object({ toolCallId: z.string(), field: z.enum(["name", "purpose"]), value: z.string() }),
56
+ "tool-input-label": z.object({
57
+ toolCallId: z.string(),
58
+ field: z.enum(["name", "purpose"]),
59
+ value: z.string(),
60
+ }),
54
61
  "execution-receipt": z.object({ toolCallId: z.string(), receipt: sparkExecutionReceiptSchema }),
55
62
  console: z.object({
56
63
  stream: z.enum(["stdout", "stderr"]),
@@ -61,6 +68,7 @@ export const sparkDataPartSchemas = {
61
68
  runId: z.string().min(1),
62
69
  status: z.enum(SPARK_RUN_STATUSES),
63
70
  error: z.string().optional(),
71
+ errorDiagnostics: sparkErrorDiagnosticsSchema.optional(),
64
72
  label: z.string().optional(),
65
73
  }),
66
74
  "step-metrics": z.object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@womp/spark-agent",
3
- "version": "4.1.1",
3
+ "version": "4.1.2",
4
4
  "description": "Portable skills-based AI agent with local and durable chat transports for Womp",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",