@womp/spark-agent 5.1.0 → 5.2.1

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 CHANGED
@@ -117,6 +117,9 @@ next model request. Consume the run's stream while awaiting its result; the resu
117
117
  the terminal checkpoint. A host run owner must keep consuming when a view detaches.
118
118
  A failed barrier aborts the loop. The default model-step limit is 100; a limit returns
119
119
  `status: "incomplete"`. A host must not automatically restart to bypass that limit.
120
+ A generation that ends on any other non-`stop` reason (`length`, `content-filter`, `error`,
121
+ `other`) rejects the result and ends the run `failed`: its error names the unified reason and the
122
+ provider's raw one, and its diagnostics carry the unified reason as `code`.
120
123
 
121
124
  Spark does not estimate or limit input tokens or automatically shorten conversation history.
122
125
  The provider enforces its context limit; rejections follow the normal provider-error path.
package/dist/agent.js CHANGED
@@ -7,6 +7,29 @@ import { contextWindowFor, fitMessagesToContext } from "./context-budget.js";
7
7
  function errorMessage(error) {
8
8
  return sparkErrorDiagnostics(error).message;
9
9
  }
10
+ /**
11
+ * `stop` completes a run and `tool-calls` means the step budget ran out
12
+ * (`stopWhen` is the only other way the loop ends), which leaves it
13
+ * incomplete. Any other final reason (`length`, `content-filter`, `error`,
14
+ * `other`) cut the model's last response short, and the run fails with it.
15
+ */
16
+ function cutShort(finishReason) {
17
+ return finishReason !== "stop" && finishReason !== "tool-calls";
18
+ }
19
+ /**
20
+ * The failure for a generation cut short, in the browser loop's words. The
21
+ * AI SDK runs a step's tool calls only when the step ends with `stop` or
22
+ * `tool-calls`, so the calls of the step cut short were not executed. The
23
+ * unified reason goes in `code` and the provider's raw reason in the message:
24
+ * the unified one alone hides which provider reason it stands for (Anthropic's
25
+ * `max_tokens` and `model_context_window_exceeded` are both `length`).
26
+ */
27
+ function finishReasonError(finishReason, rawFinishReason, responseHeaders) {
28
+ const raw = rawFinishReason === undefined || rawFinishReason === finishReason
29
+ ? ""
30
+ : ` (provider reason: ${rawFinishReason})`;
31
+ return Object.assign(new Error(`Generation ended with ${finishReason}${raw}; its partial response was not executed.`), { name: "SparkFinishReasonError", code: finishReason, responseHeaders });
32
+ }
10
33
  export function runSparkAgent(options) {
11
34
  const maxSteps = options.maxSteps ?? 100;
12
35
  const contextWindow = options.contextWindow ?? contextWindowFor(options.modelId);
@@ -178,20 +201,30 @@ export function runSparkAgent(options) {
178
201
  if (part.type === "start")
179
202
  return metadata("running");
180
203
  if (part.type === "finish") {
181
- const completed = part.finishReason === "stop";
182
- return metadata(completed ? "completed" : "incomplete", part.totalUsage);
204
+ // A run cut short is marked failed here already; its error and
205
+ // diagnostics follow once the generation has settled.
206
+ const status = cutShort(part.finishReason)
207
+ ? "failed"
208
+ : part.finishReason === "stop"
209
+ ? "completed"
210
+ : "incomplete";
211
+ return metadata(status, part.totalUsage);
183
212
  }
184
213
  return undefined;
185
214
  },
186
215
  }));
187
- const [text, usage, finishReason] = await Promise.all([
216
+ const [text, usage, finishReason, rawFinishReason, response] = await Promise.all([
188
217
  generation.text,
189
218
  generation.totalUsage,
190
219
  generation.finishReason,
220
+ generation.rawFinishReason,
221
+ generation.response,
191
222
  ]);
192
223
  controller.signal.throwIfAborted();
193
224
  if (generationError !== undefined)
194
225
  throw generationError;
226
+ if (cutShort(finishReason))
227
+ throw finishReasonError(finishReason, rawFinishReason, response.headers);
195
228
  const status = finishReason === "stop" ? "completed" : "incomplete";
196
229
  completedResult = { text, steps: stepCount, usage, status, finishReason };
197
230
  }
@@ -30,17 +30,48 @@ function text(value, limit = 1024) {
30
30
  // Bound work as well as storage. Embedded JSON/request dumps are not diagnostics.
31
31
  return String(value)
32
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]")
33
+ // An object, or an array of strings, objects or several numbers. An index
34
+ // such as `messages[2].content[0]` is not an array. `[[` is left to the
35
+ // inner bracket, so a placeholder after a `[` never matches on a rerun.
36
+ .replace(/\{[\s\S]*|\[(?=\s*(?:["'{]|-?\d[\d.eE+-]*\s*,))[\s\S]*/u, "[payload omitted]")
37
+ // A prompt, body or headers after its label, whatever follows.
38
+ .replace(/\b(?:request\s*body|response\s*body|headers|prompt)\s*[:=][\s\S]*/giu, "[payload omitted]")
39
+ // A request, payload or image only when a quoted value follows its label:
40
+ // "400 Bad Request: model not found" and "Invalid image: the image exceeds
41
+ // 5 MB" are the error, not a copy of the input. Objects and arrays after
42
+ // a label are already gone above.
43
+ .replace(/\b(?:request|payload|image)\s*[:=]\s*["'`\u201c\u201d\u2018\u2019][\s\S]*/giu, "[payload omitted]")
35
44
  .replace(/data:[^\s]+/giu, "[data omitted]")
36
45
  .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]")
46
+ // Userinfo on any other scheme (postgres, redis, mongodb+srv, ssh...);
47
+ // the scheme, host and path stay for staff. The last `@` before the host
48
+ // ends it, so a password containing `@` goes with it.
49
+ .replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/?#]+@/giu, "$1[credential omitted]@")
50
+ // A `user:password` whose password contains `/`, `?` or `#` (AWS-generated
51
+ // passwords can). The colon is required, so `s3://bucket/key@v2` stays, and
52
+ // up to five digits then `/` after it is a port and path, so
53
+ // `docker://registry:5000/repo@sha256:...` stays. A password of up to five
54
+ // digits then a `/` therefore survives.
55
+ .replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/?#:@]+:(?!\d{1,5}\/)[^\s@]*@/giu, "$1[credential omitted]@")
56
+ .replace(/\b(?:authorization|proxy-authorization|cookie|set-cookie|x-api-key|api[_ -]?key|access[_ -]?token|refresh[_ -]?token|(?:aws[_ -]?)?session[_ -]?token|x-amz-security-token|token|password|(?:aws[_ -]?)?secret[_ -]?access[_ -]?key|(?:client[_ -]?)?secret)\b\s*[:=]\s*[^\r\n,;]+/giu, "[credential omitted]")
38
57
  .replace(/\b(?:Bearer|Basic)\s+[^\s,;]+/giu, "[credential omitted]")
39
58
  .replace(/\b(?:sk-|sk_|AIza)[A-Za-z0-9_-]+/gu, "[credential omitted]")
59
+ // GitHub, Slack, Google OAuth access tokens and AWS access key ids.
60
+ .replace(/\b(?:gh[pousr]_|github_pat_)[A-Za-z0-9_]+/gu, "[credential omitted]")
61
+ .replace(/\bxox[abprs]-[A-Za-z0-9-]+/gu, "[credential omitted]")
62
+ .replace(/\bya29\.[A-Za-z0-9_-]+/gu, "[credential omitted]")
63
+ // No trailing `\b`: the length limit below can cut a longer run to exactly
64
+ // 16 characters, which a rerun would then match.
65
+ .replace(/\b(?:AKIA|ASIA)[A-Z0-9]{16}/gu, "[credential omitted]")
40
66
  .replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/gu, "[credential omitted]")
41
67
  .replace(/[A-Za-z0-9+/=_-]{200,}/gu, "[data omitted]")
42
68
  .slice(0, limit);
43
69
  }
70
+ const PLACEHOLDER = /\[(?:payload|data|url|credential) omitted\]/gu;
71
+ /** Whether redacted text still says anything once its placeholders are removed. */
72
+ function informative(value) {
73
+ return value !== undefined && /[\p{L}\p{N}]/u.test(value.replace(PLACEHOLDER, ""));
74
+ }
44
75
  export function sparkErrorDiagnostics(error) {
45
76
  const seen = new Set();
46
77
  const visit = (value, depth) => {
@@ -48,14 +79,25 @@ export function sparkErrorDiagnostics(error) {
48
79
  const source = record(value);
49
80
  const data = record(source.data);
50
81
  const body = responseData(source.responseBody);
82
+ const nested = record(data.error ?? source.error ?? body.error);
51
83
  const provider = record(data.error ?? source.error ?? body.error ?? body);
52
84
  const headers = record(source.responseHeaders);
53
85
  const status = source.statusCode ?? source.status ?? provider.status;
86
+ // A message redacted down to placeholders (a provider error whose message
87
+ // is its JSON body, say) gives way to the provider's own message, which is
88
+ // redacted the same way.
89
+ const own = text(source.message);
90
+ const fallback = text(provider.message ?? (source.message === undefined ? value : undefined));
54
91
  const result = {
55
- message: text(source.message ?? provider.message ?? value) || "An error occurred.",
92
+ message: (informative(own) ? own : informative(fallback) ? fallback : (own ?? fallback)) ||
93
+ "An error occurred.",
56
94
  };
95
+ // The provider's error type (Anthropic's `overloaded_error`, OpenAI's
96
+ // `invalid_request_error`) stands in for a missing code. The AI SDK puts
97
+ // a mid-stream provider error's type on the error itself.
98
+ const type = nested.type ?? (source.name === "AI_StreamProviderError" ? source.type : undefined);
57
99
  const name = text(source.name, 128);
58
- const code = text(source.code ?? provider.code ?? data.code, 128);
100
+ const code = text(source.code ?? provider.code ?? data.code ?? (typeof type === "string" ? type : undefined), 128);
59
101
  const requestId = text(source.requestId ??
60
102
  source.request_id ??
61
103
  provider.request_id ??
@@ -828,7 +828,7 @@ export function createWompSceneApi(context) {
828
828
  update(id, { lineHeight: clamp(finite(lineHeight, "lineHeight"), 0.5, 3) });
829
829
  },
830
830
  nodeGetDensity: (id) => requireCurve(id).density,
831
- nodeSetDensity: (id, density) => update(id, { density: clamp(Math.round(finite(density, "density")), 0, 250) }),
831
+ nodeSetDensity: (id, density) => update(id, { density: clamp(Math.round(finite(density, "density")), 1, 250) }),
832
832
  nodeGetRoundness: (id) => requireCurve(id).roundness,
833
833
  nodeSetRoundness: (id, roundness) => update(id, { roundness: clamp(Math.round(finite(roundness, "roundness")), 1, 10) }),
834
834
  nodeGetSmoothing: (id) => requireCurve(id).smoothing,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@womp/spark-agent",
3
- "version": "5.1.0",
3
+ "version": "5.2.1",
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",
@@ -110,7 +110,7 @@
110
110
  "@ai-sdk/openai": "4.0.63",
111
111
  "@jitl/quickjs-wasmfile-release-sync": "0.32.0",
112
112
  "@womp/flowir": "0.3.0",
113
- "@womp/kakapo-sdk": "0.7.0",
113
+ "@womp/kakapo-sdk": "0.7.1",
114
114
  "ai": "7.0.95",
115
115
  "eventsource-parser": "^3.0.6",
116
116
  "express": "^5.2.1",