@womp/spark-agent 5.1.0 → 5.2.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.
- package/README.md +3 -0
- package/dist/agent.js +36 -3
- package/dist/error-diagnostics.js +47 -5
- package/package.json +1 -1
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
|
-
|
|
182
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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:
|
|
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 ??
|