@womp/spark-agent 6.5.0 → 7.0.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/CHANGELOG.md +50 -0
- package/README.md +7 -4
- package/dist/agent.d.ts +3 -6
- package/dist/agent.js +41 -23
- package/dist/compaction.d.ts +41 -8
- package/dist/compaction.js +241 -77
- package/dist/context-window.d.ts +23 -0
- package/dist/context-window.js +141 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/ui.d.ts +7 -0
- package/dist/ui.js +1 -0
- package/package.json +1 -1
- package/dist/context-budget.d.ts +0 -65
- package/dist/context-budget.js +0 -364
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,56 @@ Notable changes to `@womp/spark-agent`, newest first. Versions follow
|
|
|
4
4
|
[Semantic Versioning](https://semver.org/). Add an entry with each version bump; describe what
|
|
5
5
|
changed for hosts and users, and how to migrate across breaking changes.
|
|
6
6
|
|
|
7
|
+
## 7.0.0 — 2026-10-12
|
|
8
|
+
|
|
9
|
+
Compaction is now the only thing that takes messages out of what the model reads, and it can run
|
|
10
|
+
before any step of a turn, not just the first. The per-step trimming that replaced earlier images,
|
|
11
|
+
tool results and whole turns with placeholder text is gone: it rewrote the start of the prompt
|
|
12
|
+
whenever it fired, which threw away the provider's prompt cache on every later step. A request
|
|
13
|
+
that is still over the model's context window after compaction is sent as it is and fails with
|
|
14
|
+
the provider's error.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- A run given `history` checks the conversation before every step. Once it reaches the compaction
|
|
19
|
+
threshold during a turn, everything before the current user message and the turn's steps before
|
|
20
|
+
its most recent `COMPACTION_KEEP_RECENT_TOKENS` are summarized; the user's message is kept
|
|
21
|
+
verbatim. The summary request is the first kept step's request plus one instruction, so it is
|
|
22
|
+
served from the prompt cache. A compaction that would save less than
|
|
23
|
+
`COMPACTION_MIN_SAVED_RATIO` of the threshold is skipped, so a turn whose own message is over
|
|
24
|
+
the threshold does not summarize on every step.
|
|
25
|
+
- A summary covers only the messages it replaces, before a turn as well as during one. It used to
|
|
26
|
+
tell of the kept messages too, and the model, reading them again after it, took that work for
|
|
27
|
+
done twice.
|
|
28
|
+
- `SparkCompaction.firstKeptStep` records a compaction written during its turn: the first step of
|
|
29
|
+
the recording assistant message the model still reads. `applyCompaction` (and so
|
|
30
|
+
`convertSparkMessages`) leaves the earlier steps out, so the next turn rebuilds exactly the
|
|
31
|
+
messages the last step was sent. A run may now record more than one `data-compaction` part; the
|
|
32
|
+
latest is the one in force. Hosts must store and reload message parts in order, including
|
|
33
|
+
`step-start`.
|
|
34
|
+
- When to compact is measured from the provider's count for the conversation's last step after the
|
|
35
|
+
latest compaction, including the steps of the message that recorded it.
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- `applyCompaction` and `latestCompaction` are exported from the package root, so a host can tell
|
|
40
|
+
which stored messages, and which of their images, the model will be sent.
|
|
41
|
+
|
|
42
|
+
### Removed
|
|
43
|
+
|
|
44
|
+
- `fitMessagesToContext`, `ContextBudgetOptions`, `ContextBudgetReport`, `ContextBudgetResult`,
|
|
45
|
+
`TRIM_TRIGGER_RATIO`, `TRIM_TARGET_RATIO`, `SMALL_TOOL_OUTPUT_TOKENS`,
|
|
46
|
+
`PROTECTED_CURRENT_STEPS` and the `OMITTED_HISTORY_TEXT`, `OMITTED_HISTORY_IMAGE_TEXT` and
|
|
47
|
+
`OMITTED_TOOL_OUTPUT_TEXT` notes.
|
|
48
|
+
- `SparkAgentOptions.onContextBudget`.
|
|
49
|
+
|
|
50
|
+
### Migration
|
|
51
|
+
|
|
52
|
+
- Remove any `onContextBudget` handler and any host-side trimming. Pass `history` to have long
|
|
53
|
+
conversations compacted; a run given `messages` is sent exactly what it is given.
|
|
54
|
+
- `contextWindowFor`, `estimateMessagesTokens`, `estimateRequestTokens`, `CHARS_PER_TOKEN` and
|
|
55
|
+
`IMAGE_TOKENS` are unchanged; they now live in `src/context-window.ts`.
|
|
56
|
+
|
|
7
57
|
## 6.5.0 — 2026-10-12
|
|
8
58
|
|
|
9
59
|
The agent proposes a goal during its turn, instead of the host asking a separate model call to
|
package/README.md
CHANGED
|
@@ -75,9 +75,11 @@ The default model-step limit is 100; reaching it returns `status: "incomplete"`,
|
|
|
75
75
|
restart automatically to bypass it. A generation that ends on any other non-`stop` reason fails the
|
|
76
76
|
run with the unified and provider finish reasons in its error and diagnostics.
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
When a run is given its stored `history`, Spark compacts it before any model step once the
|
|
79
|
+
conversation reaches the compaction threshold: earlier turns, and earlier steps of a long turn, are
|
|
80
|
+
replaced by a summary recorded on the run's message as a `data-compaction` part. Nothing else
|
|
81
|
+
shortens a request; one that is still over the model's context window fails with the provider's
|
|
82
|
+
error.
|
|
81
83
|
|
|
82
84
|
### Artifacts
|
|
83
85
|
|
|
@@ -355,7 +357,8 @@ features, patch for backward-compatible fixes.
|
|
|
355
357
|
|
|
356
358
|
- `src/agent.ts` — AI SDK stream and Spark tool orchestration
|
|
357
359
|
- `src/prompt.ts` — shared system prompt
|
|
358
|
-
- `src/
|
|
360
|
+
- `src/compaction.ts` — conversation compaction before each model step
|
|
361
|
+
- `src/context-window.ts` — model context windows and request size estimates
|
|
359
362
|
- `src/ui.ts` — browser-safe protocol types and validation schemas
|
|
360
363
|
- `src/client.ts` — durable `ChatTransport` implementation
|
|
361
364
|
- `src/server/` — service, storage contracts, Express adapter, artifacts, and standalone composition
|
package/dist/agent.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { streamText, type LanguageModel, type ModelMessage } from "ai";
|
|
|
2
2
|
import { type SparkGoalOffer, type SparkPersistence } from "./agent-tools.js";
|
|
3
3
|
import type { SparkArtifactResolver, SparkCapabilities } from "./capabilities.js";
|
|
4
4
|
import type { JavaScriptExecutor } from "./executor.js";
|
|
5
|
-
import { type ContextBudgetReport } from "./context-budget.js";
|
|
6
5
|
import { type SparkCompactionLimits } from "./compaction.js";
|
|
7
6
|
import type { SparkUIMessage, SparkUIMessageChunk } from "./ui.js";
|
|
8
7
|
export type SparkAgentResult = {
|
|
@@ -17,8 +16,8 @@ export type SparkAgentOptions = {
|
|
|
17
16
|
messages?: ModelMessage[];
|
|
18
17
|
/**
|
|
19
18
|
* The stored conversation. It is converted with `convertSparkMessages`, and
|
|
20
|
-
* compacted
|
|
21
|
-
* compaction is written to this run's message as a `data-compaction` part.
|
|
19
|
+
* compacted before any step once it is long enough (./compaction.ts); each
|
|
20
|
+
* new compaction is written to this run's message as a `data-compaction` part.
|
|
22
21
|
*/
|
|
23
22
|
history?: SparkUIMessage[];
|
|
24
23
|
model: LanguageModel;
|
|
@@ -38,12 +37,10 @@ export type SparkAgentOptions = {
|
|
|
38
37
|
/** Gives the model `propose_goal`; absent, it has no way to offer a goal. */
|
|
39
38
|
proposeGoal?: SparkGoalOffer;
|
|
40
39
|
prepareMessages?(messages: ModelMessage[]): Promise<ModelMessage[]>;
|
|
41
|
-
/** The model's context window. Absent: `contextWindowFor(modelId)`. */
|
|
40
|
+
/** The model's context window, which caps when `history` is compacted. Absent: `contextWindowFor(modelId)`. */
|
|
42
41
|
contextWindow?: number;
|
|
43
42
|
/** When `history` is compacted. Absent: the defaults in ./compaction.ts. */
|
|
44
43
|
compaction?: SparkCompactionLimits;
|
|
45
|
-
/** Called when a step's messages had to be trimmed to fit, or still do not fit. */
|
|
46
|
-
onContextBudget?(report: ContextBudgetReport): void;
|
|
47
44
|
};
|
|
48
45
|
export type SparkAgentRun = {
|
|
49
46
|
stream: ReadableStream<SparkUIMessageChunk>;
|
package/dist/agent.js
CHANGED
|
@@ -4,8 +4,8 @@ import { createUIMessageStream, stepCountIs, streamText, } from "ai";
|
|
|
4
4
|
import { createSparkTools } from "./agent-tools.js";
|
|
5
5
|
import { sparkSystemPrompt } from "./prompt.js";
|
|
6
6
|
import { calculateStepCost } from "./pricing.js";
|
|
7
|
-
import { contextWindowFor
|
|
8
|
-
import { compactSparkHistory } from "./compaction.js";
|
|
7
|
+
import { contextWindowFor } from "./context-window.js";
|
|
8
|
+
import { compactSparkHistory, createStepCompaction, latestCompaction } from "./compaction.js";
|
|
9
9
|
import { convertSparkMessages } from "./model-history.js";
|
|
10
10
|
function errorMessage(error) {
|
|
11
11
|
return sparkErrorDiagnostics(error).message;
|
|
@@ -131,11 +131,14 @@ export function runSparkAgent(options) {
|
|
|
131
131
|
});
|
|
132
132
|
const readSystem = () => sparkSystemPrompt(options.capabilities.skills, options.instructions);
|
|
133
133
|
let messages = options.messages ?? [];
|
|
134
|
+
// Compaction before each later step; only for a stored conversation,
|
|
135
|
+
// where the compaction can be recorded.
|
|
136
|
+
let compactStep;
|
|
134
137
|
if (options.history !== undefined) {
|
|
135
138
|
const history = options.history;
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
const skills = options.capabilities.skills;
|
|
140
|
+
let compactions = 0;
|
|
141
|
+
const summaryOptions = {
|
|
139
142
|
model: options.model,
|
|
140
143
|
modelId: options.modelId,
|
|
141
144
|
system: readSystem(),
|
|
@@ -149,12 +152,29 @@ export function runSparkAgent(options) {
|
|
|
149
152
|
type: "data-run-status",
|
|
150
153
|
data: { runId: options.runId, status: "running", label: "Making room so we can keep talking" },
|
|
151
154
|
}),
|
|
155
|
+
};
|
|
156
|
+
const record = (compaction) => writer.write({
|
|
157
|
+
type: "data-compaction",
|
|
158
|
+
id: `compaction-${options.messageId}${compactions++ === 0 ? "" : `-${compactions}`}`,
|
|
159
|
+
data: compaction,
|
|
160
|
+
});
|
|
161
|
+
const compacted = await compactSparkHistory({
|
|
162
|
+
...summaryOptions,
|
|
163
|
+
history,
|
|
164
|
+
convert: (compaction) => convertSparkMessages(history, tools, { compaction, skills }),
|
|
152
165
|
});
|
|
153
166
|
messages = compacted.messages;
|
|
154
|
-
if (compacted.compaction !== undefined)
|
|
155
|
-
|
|
156
|
-
}
|
|
167
|
+
if (compacted.compaction !== undefined)
|
|
168
|
+
record(compacted.compaction);
|
|
157
169
|
controller.signal.throwIfAborted();
|
|
170
|
+
compactStep = createStepCompaction({
|
|
171
|
+
...summaryOptions,
|
|
172
|
+
history,
|
|
173
|
+
previous: compacted.compaction ?? latestCompaction(history)?.compaction,
|
|
174
|
+
skills,
|
|
175
|
+
convert: (stored) => convertSparkMessages(stored, tools, { skills }),
|
|
176
|
+
onCompaction: record,
|
|
177
|
+
});
|
|
158
178
|
}
|
|
159
179
|
const generation = streamText({
|
|
160
180
|
model: options.model,
|
|
@@ -166,25 +186,23 @@ export function runSparkAgent(options) {
|
|
|
166
186
|
stopWhen: stepCountIs(maxSteps),
|
|
167
187
|
maxOutputTokens: options.maxOutputTokens,
|
|
168
188
|
providerOptions: options.providerOptions,
|
|
169
|
-
prepareStep: async ({ messages }) => {
|
|
189
|
+
prepareStep: async ({ messages, steps }) => {
|
|
170
190
|
controller.signal.throwIfAborted();
|
|
171
|
-
|
|
191
|
+
// The first step was compacted above, before the request was built.
|
|
192
|
+
// The AI SDK sends each later step what the one before it was sent,
|
|
193
|
+
// so a compaction here holds for the rest of the turn.
|
|
194
|
+
const last = steps.at(-1)?.usage;
|
|
195
|
+
const current = compactStep === undefined || steps.length === 0
|
|
172
196
|
? messages
|
|
173
|
-
: await
|
|
197
|
+
: await compactStep(messages, { inputTokens: last?.inputTokens, outputTokens: last?.outputTokens });
|
|
198
|
+
controller.signal.throwIfAborted();
|
|
199
|
+
const prepared = options.prepareMessages === undefined
|
|
200
|
+
? current
|
|
201
|
+
: await options.prepareMessages(current);
|
|
174
202
|
controller.signal.throwIfAborted();
|
|
175
|
-
const system = readSystem();
|
|
176
|
-
// After the host's own preparation, so what is measured is what is sent.
|
|
177
|
-
const fitted = fitMessagesToContext(prepared, {
|
|
178
|
-
contextWindow,
|
|
179
|
-
maxOutputTokens: options.maxOutputTokens,
|
|
180
|
-
system,
|
|
181
|
-
tools,
|
|
182
|
-
});
|
|
183
|
-
if (fitted.report && (fitted.messages !== prepared || !fitted.report.fits))
|
|
184
|
-
options.onContextBudget?.(fitted.report);
|
|
185
203
|
return {
|
|
186
|
-
system,
|
|
187
|
-
messages:
|
|
204
|
+
system: readSystem(),
|
|
205
|
+
messages: prepared,
|
|
188
206
|
};
|
|
189
207
|
},
|
|
190
208
|
onError: ({ error }) => {
|
package/dist/compaction.d.ts
CHANGED
|
@@ -3,10 +3,12 @@ import type { SparkSkill } from "./capabilities.js";
|
|
|
3
3
|
import { type SparkCompaction, type SparkUIMessage } from "./ui.js";
|
|
4
4
|
/** Compact once the conversation reaches this many tokens. */
|
|
5
5
|
export declare const COMPACTION_THRESHOLD_TOKENS = 256000;
|
|
6
|
-
/** Recent whole turns kept verbatim, up to this many tokens
|
|
6
|
+
/** Recent whole turns, or recent steps of the current turn, kept verbatim, up to this many tokens. */
|
|
7
7
|
export declare const COMPACTION_KEEP_RECENT_TOKENS = 16000;
|
|
8
8
|
/** The user's newest images from the summarized messages that the summary message carries. */
|
|
9
9
|
export declare const COMPACTION_USER_IMAGES = 4;
|
|
10
|
+
/** A compaction during a turn saves at least this share of the threshold, or is not made. */
|
|
11
|
+
export declare const COMPACTION_MIN_SAVED_RATIO = 0.25;
|
|
10
12
|
/**
|
|
11
13
|
* When a conversation is compacted, for a host that exercises compaction
|
|
12
14
|
* without filling a production-sized conversation, such as local development.
|
|
@@ -19,27 +21,28 @@ export type SparkCompactionLimits = {
|
|
|
19
21
|
};
|
|
20
22
|
export declare const COMPACTION_PROMPT = "Write a checkpoint summary of this conversation so far. It replaces the earlier messages in your context, so the work must be able to continue from it alone. Reply with the summary only, without calling tools. This request is not part of the conversation: leave it out of the summary.\nIf the conversation begins with an earlier summary, carry its content forward and update it.\nUse these sections:\n## Goal\n## User preferences and constraints\n## Decisions and current scene state (objects with their names and IDs)\n## Done\n## Open and next steps\n## Artifacts and images (each artifact ID, and what each image the user attached shows)\n## Errors and approaches to avoid\nKeep exact names, IDs, numbers and the user's own wording for requirements. Stay under 1,500 words.";
|
|
21
23
|
export declare const COMPACTION_NOTE = "[Earlier messages in this conversation were replaced by the summary below. The scene itself is unchanged; inspect it for its exact state.]";
|
|
24
|
+
export declare const COMPACTION_STEPS_NOTE = "[Earlier messages in this conversation, and the earlier steps of your work on the message after this one, were replaced by the summary below. The scene itself is unchanged; inspect it for its exact state.]";
|
|
22
25
|
/** The latest compaction in the conversation whose kept message is still in it. */
|
|
23
26
|
export declare function latestCompaction(history: SparkUIMessage[]): {
|
|
24
27
|
compaction: SparkCompaction;
|
|
25
28
|
carrier: number;
|
|
29
|
+
part: number;
|
|
26
30
|
firstKept: number;
|
|
27
31
|
} | undefined;
|
|
28
32
|
/**
|
|
29
33
|
* The conversation as the model reads it: the messages before the compaction's
|
|
30
34
|
* kept message replaced by its summary, with the newest COMPACTION_USER_IMAGES
|
|
31
|
-
* images the user attached to them
|
|
32
|
-
*
|
|
33
|
-
*
|
|
35
|
+
* images the user attached to them, and -- for the latest compaction, when it was
|
|
36
|
+
* written during its turn -- the recording message's steps before
|
|
37
|
+
* `firstKeptStep` left out with them. `compaction` defaults to the latest one in
|
|
38
|
+
* `history`; `skills` are the host's, to send the current text of the skills it
|
|
39
|
+
* names. Without a compaction, `history` is returned as it is.
|
|
34
40
|
*/
|
|
35
41
|
export declare function applyCompaction(history: SparkUIMessage[], options?: {
|
|
36
42
|
compaction?: SparkCompaction;
|
|
37
43
|
skills?: readonly SparkSkill[];
|
|
38
44
|
}): SparkUIMessage[];
|
|
39
|
-
|
|
40
|
-
history: SparkUIMessage[];
|
|
41
|
-
/** `history` as model messages, with `compaction` applied when given. */
|
|
42
|
-
convert(compaction?: SparkCompaction): Promise<ModelMessage[]>;
|
|
45
|
+
type SummaryOptions = {
|
|
43
46
|
model: LanguageModel;
|
|
44
47
|
modelId: string;
|
|
45
48
|
system: string;
|
|
@@ -53,6 +56,11 @@ export type CompactSparkHistoryOptions = {
|
|
|
53
56
|
onCompacting?(): void;
|
|
54
57
|
limits?: SparkCompactionLimits;
|
|
55
58
|
};
|
|
59
|
+
export type CompactSparkHistoryOptions = SummaryOptions & {
|
|
60
|
+
history: SparkUIMessage[];
|
|
61
|
+
/** `history` as model messages, with `compaction` applied when given. */
|
|
62
|
+
convert(compaction?: SparkCompaction): Promise<ModelMessage[]>;
|
|
63
|
+
};
|
|
56
64
|
/**
|
|
57
65
|
* The turn's model messages, compacted first when the conversation has reached
|
|
58
66
|
* the threshold. `compaction` is the new one, for the host to record; it is
|
|
@@ -62,3 +70,28 @@ export declare function compactSparkHistory(options: CompactSparkHistoryOptions)
|
|
|
62
70
|
messages: ModelMessage[];
|
|
63
71
|
compaction?: SparkCompaction;
|
|
64
72
|
}>;
|
|
73
|
+
export type StepCompactionOptions = SummaryOptions & {
|
|
74
|
+
/** The stored conversation, ending with the current user message. */
|
|
75
|
+
history: SparkUIMessage[];
|
|
76
|
+
/** The compaction the turn started from: the one written before its first step, else `history`'s latest. */
|
|
77
|
+
previous?: SparkCompaction;
|
|
78
|
+
/** The host's skills, for the summary message's skill text. */
|
|
79
|
+
skills?: readonly SparkSkill[];
|
|
80
|
+
/** Stored messages as model messages. */
|
|
81
|
+
convert(messages: SparkUIMessage[]): Promise<ModelMessage[]>;
|
|
82
|
+
/** A new compaction, for the host to record on the turn's message. */
|
|
83
|
+
onCompaction(compaction: SparkCompaction): void;
|
|
84
|
+
};
|
|
85
|
+
/** What the provider reported for the step before. */
|
|
86
|
+
export type StepUsage = {
|
|
87
|
+
inputTokens?: number;
|
|
88
|
+
outputTokens?: number;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Compaction before each later step of a turn. Returns the step's messages,
|
|
92
|
+
* compacted when the conversation has reached the threshold; the AI SDK carries
|
|
93
|
+
* what a step was sent into the next, so a compaction holds for the rest of the
|
|
94
|
+
* turn.
|
|
95
|
+
*/
|
|
96
|
+
export declare function createStepCompaction(options: StepCompactionOptions): (messages: ModelMessage[], last?: StepUsage) => Promise<ModelMessage[]>;
|
|
97
|
+
export {};
|