@womp/spark-agent 6.3.0 → 6.4.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 +34 -0
- package/README.md +2 -2
- package/dist/agent.d.ts +12 -2
- package/dist/agent.js +32 -1
- package/dist/compaction.d.ts +64 -0
- package/dist/compaction.js +296 -0
- package/dist/context-budget.d.ts +2 -0
- package/dist/context-budget.js +14 -6
- package/dist/index.d.ts +1 -0
- package/dist/model-history.d.ts +13 -3
- package/dist/model-history.js +10 -3
- package/dist/server/executor.d.ts +2 -0
- package/dist/server/executor.js +2 -10
- package/dist/server/goal-evaluator.d.ts +7 -0
- package/dist/server/goal-evaluator.js +22 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/ui.d.ts +28 -0
- package/dist/ui.js +9 -0
- package/dist/user-images.d.ts +10 -0
- package/dist/user-images.js +20 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,40 @@ 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
|
+
## 6.4.0 — 2026-10-12
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Conversation compaction. Once a conversation reaches 256k tokens (by the provider's count for its
|
|
12
|
+
last step, or an estimate), the run's own model summarizes everything before the most recent
|
|
13
|
+
16k tokens of whole turns, before the turn's first step. Later requests send the summary, the
|
|
14
|
+
current text of the skills read in the summarized messages, and the newest four images the user
|
|
15
|
+
attached to them (`COMPACTION_USER_IMAGES`), in their place. The summary is
|
|
16
|
+
recorded as a `data-compaction` part (`SparkCompaction`) on the turn's assistant message, so
|
|
17
|
+
hosts store it with the message; the chat still has every message. A summary that fails leaves
|
|
18
|
+
the turn uncompacted, and context trimming still applies.
|
|
19
|
+
- `SparkAgentOptions.history`: the stored conversation, converted and compacted by the run. Pass it
|
|
20
|
+
instead of `messages` to get compaction; `createSparkRunExecutor` now does.
|
|
21
|
+
- `convertSparkMessages` takes `{ compaction, skills }` and applies the latest compaction in the
|
|
22
|
+
messages it is given.
|
|
23
|
+
- `SparkAgentOptions.compaction` and `SparkRunExecutorOptions.compaction` (`SparkCompactionLimits`)
|
|
24
|
+
lower the threshold and the recent tail kept, for a host that exercises compaction without a
|
|
25
|
+
production-sized conversation, such as local development. Absent, the defaults apply.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- Goal evaluation attaches the newest `GOAL_REFERENCE_IMAGES` images the user attached to the
|
|
30
|
+
conversation, however far back, as the reference, ahead of the inspection screenshots. It reads
|
|
31
|
+
them only from `data:` URLs, so a host that stores them elsewhere passes `messages` with those
|
|
32
|
+
images inline; others are listed as unavailable.
|
|
33
|
+
- When a conversation is over its context window, trimming omits the agent's older screenshots
|
|
34
|
+
before the older images the user attached.
|
|
35
|
+
|
|
36
|
+
### Migration
|
|
37
|
+
|
|
38
|
+
- Hosts that validate stored messages with `sparkDataPartSchemas` must take this version before any
|
|
39
|
+
host writing compactions reads the same conversations, or the new part fails validation.
|
|
40
|
+
|
|
7
41
|
## 6.3.0 — 2026-10-09
|
|
8
42
|
|
|
9
43
|
### Added
|
package/README.md
CHANGED
|
@@ -35,8 +35,8 @@ Kakapo, Express, or React at runtime.
|
|
|
35
35
|
|
|
36
36
|
## Running in a host application
|
|
37
37
|
|
|
38
|
-
`runSparkAgent` accepts a configured model,
|
|
39
|
-
and an artifact resolver. It exposes exactly `read_skill` and `execute_javascript` to the model.
|
|
38
|
+
`runSparkAgent` accepts a configured model, the stored conversation (`history`, compacted once it
|
|
39
|
+
is long) or model messages, an executor, a capability composition, and an artifact resolver. It exposes exactly `read_skill` and `execute_javascript` to the model.
|
|
40
40
|
Skills carry instructions, declarations, and JavaScript examples. Kakapo skill declarations are
|
|
41
41
|
generated from its typed API at build time and packaged as JavaScript strings.
|
|
42
42
|
|
package/dist/agent.d.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { type SparkPersistence } from "./agent-tools.js";
|
|
|
3
3
|
import type { SparkArtifactResolver, SparkCapabilities } from "./capabilities.js";
|
|
4
4
|
import type { JavaScriptExecutor } from "./executor.js";
|
|
5
5
|
import { type ContextBudgetReport } from "./context-budget.js";
|
|
6
|
-
import type
|
|
6
|
+
import { type SparkCompactionLimits } from "./compaction.js";
|
|
7
|
+
import type { SparkUIMessage, SparkUIMessageChunk } from "./ui.js";
|
|
7
8
|
export type SparkAgentResult = {
|
|
8
9
|
text: string;
|
|
9
10
|
steps: number;
|
|
@@ -12,7 +13,14 @@ export type SparkAgentResult = {
|
|
|
12
13
|
finishReason: string;
|
|
13
14
|
};
|
|
14
15
|
export type SparkAgentOptions = {
|
|
15
|
-
messages
|
|
16
|
+
/** The conversation as model messages, sent as given. Pass `history` instead to have long conversations compacted. */
|
|
17
|
+
messages?: ModelMessage[];
|
|
18
|
+
/**
|
|
19
|
+
* The stored conversation. It is converted with `convertSparkMessages`, and
|
|
20
|
+
* compacted first once it is long enough (./compaction.ts); the new
|
|
21
|
+
* compaction is written to this run's message as a `data-compaction` part.
|
|
22
|
+
*/
|
|
23
|
+
history?: SparkUIMessage[];
|
|
16
24
|
model: LanguageModel;
|
|
17
25
|
modelId: string;
|
|
18
26
|
providerOptions?: Parameters<typeof streamText>[0]["providerOptions"];
|
|
@@ -30,6 +38,8 @@ export type SparkAgentOptions = {
|
|
|
30
38
|
prepareMessages?(messages: ModelMessage[]): Promise<ModelMessage[]>;
|
|
31
39
|
/** The model's context window. Absent: `contextWindowFor(modelId)`. */
|
|
32
40
|
contextWindow?: number;
|
|
41
|
+
/** When `history` is compacted. Absent: the defaults in ./compaction.ts. */
|
|
42
|
+
compaction?: SparkCompactionLimits;
|
|
33
43
|
/** Called when a step's messages had to be trimmed to fit, or still do not fit. */
|
|
34
44
|
onContextBudget?(report: ContextBudgetReport): void;
|
|
35
45
|
};
|
package/dist/agent.js
CHANGED
|
@@ -5,6 +5,8 @@ import { createSparkTools } from "./agent-tools.js";
|
|
|
5
5
|
import { sparkSystemPrompt } from "./prompt.js";
|
|
6
6
|
import { calculateStepCost } from "./pricing.js";
|
|
7
7
|
import { contextWindowFor, fitMessagesToContext } from "./context-budget.js";
|
|
8
|
+
import { compactSparkHistory } from "./compaction.js";
|
|
9
|
+
import { convertSparkMessages } from "./model-history.js";
|
|
8
10
|
function errorMessage(error) {
|
|
9
11
|
return sparkErrorDiagnostics(error).message;
|
|
10
12
|
}
|
|
@@ -37,6 +39,9 @@ export function runSparkAgent(options) {
|
|
|
37
39
|
if (Number.isInteger(maxSteps) === false || maxSteps < 1) {
|
|
38
40
|
throw new Error("maxSteps must be a positive integer.");
|
|
39
41
|
}
|
|
42
|
+
if ((options.messages === undefined) === (options.history === undefined)) {
|
|
43
|
+
throw new Error("Pass either messages or history.");
|
|
44
|
+
}
|
|
40
45
|
const startedAt = Date.now();
|
|
41
46
|
const controller = new AbortController();
|
|
42
47
|
const onAbort = () => controller.abort(options.signal?.reason);
|
|
@@ -125,10 +130,36 @@ export function runSparkAgent(options) {
|
|
|
125
130
|
write: (chunk) => writer.write(chunk),
|
|
126
131
|
});
|
|
127
132
|
const readSystem = () => sparkSystemPrompt(options.capabilities.skills, options.instructions);
|
|
133
|
+
let messages = options.messages ?? [];
|
|
134
|
+
if (options.history !== undefined) {
|
|
135
|
+
const history = options.history;
|
|
136
|
+
const compacted = await compactSparkHistory({
|
|
137
|
+
history,
|
|
138
|
+
convert: (compaction) => convertSparkMessages(history, tools, { compaction, skills: options.capabilities.skills }),
|
|
139
|
+
model: options.model,
|
|
140
|
+
modelId: options.modelId,
|
|
141
|
+
system: readSystem(),
|
|
142
|
+
tools,
|
|
143
|
+
providerOptions: options.providerOptions,
|
|
144
|
+
maxOutputTokens: options.maxOutputTokens,
|
|
145
|
+
contextWindow,
|
|
146
|
+
signal: controller.signal,
|
|
147
|
+
limits: options.compaction,
|
|
148
|
+
onCompacting: () => writer.write({
|
|
149
|
+
type: "data-run-status",
|
|
150
|
+
data: { runId: options.runId, status: "running", label: "Making room so we can keep talking" },
|
|
151
|
+
}),
|
|
152
|
+
});
|
|
153
|
+
messages = compacted.messages;
|
|
154
|
+
if (compacted.compaction !== undefined) {
|
|
155
|
+
writer.write({ type: "data-compaction", id: `compaction-${options.messageId}`, data: compacted.compaction });
|
|
156
|
+
}
|
|
157
|
+
controller.signal.throwIfAborted();
|
|
158
|
+
}
|
|
128
159
|
const generation = streamText({
|
|
129
160
|
model: options.model,
|
|
130
161
|
system: readSystem(),
|
|
131
|
-
messages
|
|
162
|
+
messages,
|
|
132
163
|
tools,
|
|
133
164
|
toolChoice: "auto",
|
|
134
165
|
abortSignal: controller.signal,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { generateText, type LanguageModel, type ModelMessage, type ToolSet } from "ai";
|
|
2
|
+
import type { SparkSkill } from "./capabilities.js";
|
|
3
|
+
import { type SparkCompaction, type SparkUIMessage } from "./ui.js";
|
|
4
|
+
/** Compact once the conversation reaches this many tokens. */
|
|
5
|
+
export declare const COMPACTION_THRESHOLD_TOKENS = 256000;
|
|
6
|
+
/** Recent whole turns kept verbatim, up to this many tokens, besides the current turn. */
|
|
7
|
+
export declare const COMPACTION_KEEP_RECENT_TOKENS = 16000;
|
|
8
|
+
/** The user's newest images from the summarized messages that the summary message carries. */
|
|
9
|
+
export declare const COMPACTION_USER_IMAGES = 4;
|
|
10
|
+
/**
|
|
11
|
+
* When a conversation is compacted, for a host that exercises compaction
|
|
12
|
+
* without filling a production-sized conversation, such as local development.
|
|
13
|
+
*/
|
|
14
|
+
export type SparkCompactionLimits = {
|
|
15
|
+
/** Default COMPACTION_THRESHOLD_TOKENS. */
|
|
16
|
+
thresholdTokens?: number;
|
|
17
|
+
/** Default COMPACTION_KEEP_RECENT_TOKENS. */
|
|
18
|
+
keepRecentTokens?: number;
|
|
19
|
+
};
|
|
20
|
+
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
|
+
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.]";
|
|
22
|
+
/** The latest compaction in the conversation whose kept message is still in it. */
|
|
23
|
+
export declare function latestCompaction(history: SparkUIMessage[]): {
|
|
24
|
+
compaction: SparkCompaction;
|
|
25
|
+
carrier: number;
|
|
26
|
+
firstKept: number;
|
|
27
|
+
} | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* The conversation as the model reads it: the messages before the compaction's
|
|
30
|
+
* kept message replaced by its summary, with the newest COMPACTION_USER_IMAGES
|
|
31
|
+
* images the user attached to them. `compaction` defaults to the latest one in `history`; `skills` are the
|
|
32
|
+
* host's, to send the current text of the skills it names. Without a
|
|
33
|
+
* compaction, `history` is returned as it is.
|
|
34
|
+
*/
|
|
35
|
+
export declare function applyCompaction(history: SparkUIMessage[], options?: {
|
|
36
|
+
compaction?: SparkCompaction;
|
|
37
|
+
skills?: readonly SparkSkill[];
|
|
38
|
+
}): SparkUIMessage[];
|
|
39
|
+
export type CompactSparkHistoryOptions = {
|
|
40
|
+
history: SparkUIMessage[];
|
|
41
|
+
/** `history` as model messages, with `compaction` applied when given. */
|
|
42
|
+
convert(compaction?: SparkCompaction): Promise<ModelMessage[]>;
|
|
43
|
+
model: LanguageModel;
|
|
44
|
+
modelId: string;
|
|
45
|
+
system: string;
|
|
46
|
+
/** The run's tools; the summary request sends their definitions without `execute`. */
|
|
47
|
+
tools: ToolSet;
|
|
48
|
+
providerOptions?: Parameters<typeof generateText>[0]["providerOptions"];
|
|
49
|
+
maxOutputTokens?: number;
|
|
50
|
+
contextWindow?: number;
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
/** Called just before the summary request, which can take a while. */
|
|
53
|
+
onCompacting?(): void;
|
|
54
|
+
limits?: SparkCompactionLimits;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* The turn's model messages, compacted first when the conversation has reached
|
|
58
|
+
* the threshold. `compaction` is the new one, for the host to record; it is
|
|
59
|
+
* absent when nothing was compacted, including when the summary failed.
|
|
60
|
+
*/
|
|
61
|
+
export declare function compactSparkHistory(options: CompactSparkHistoryOptions): Promise<{
|
|
62
|
+
messages: ModelMessage[];
|
|
63
|
+
compaction?: SparkCompaction;
|
|
64
|
+
}>;
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// Replace a long conversation's earlier messages with a summary.
|
|
2
|
+
//
|
|
3
|
+
// WHEN. Once per turn, before its first step: when the conversation reaches
|
|
4
|
+
// COMPACTION_THRESHOLD_TOKENS, everything before the most recent
|
|
5
|
+
// COMPACTION_KEEP_RECENT_TOKENS of whole turns is summarized by the run's own
|
|
6
|
+
// model. Every later request sends the summary in their place. Below the
|
|
7
|
+
// threshold nothing happens, and a summary that fails leaves the turn as it was:
|
|
8
|
+
// ./context-budget.ts still keeps every request inside the model's window.
|
|
9
|
+
//
|
|
10
|
+
// WHERE IT IS KEPT. The summary is a `data-compaction` part on the assistant
|
|
11
|
+
// message of the turn that wrote it (SparkCompaction, ./ui.ts), so every host
|
|
12
|
+
// stores and reloads it with the message, and the chat still shows every
|
|
13
|
+
// message it replaces. `applyCompaction`, called by `convertSparkMessages`,
|
|
14
|
+
// sends the latest one in place of the messages before `firstKeptMessageId`.
|
|
15
|
+
// A later compaction is written from the conversation as the model then sees
|
|
16
|
+
// it -- the earlier summary first -- so the summary is updated, not restarted.
|
|
17
|
+
//
|
|
18
|
+
// THE CACHE. The summary request is the previous request's prefix -- the same
|
|
19
|
+
// system prompt, tool definitions and messages -- plus one instruction, so the
|
|
20
|
+
// provider's prompt cache serves almost all of it. Tool definitions are sent
|
|
21
|
+
// without `execute`: a call the model makes anyway is never run, and fails the
|
|
22
|
+
// summary instead.
|
|
23
|
+
//
|
|
24
|
+
// SKILLS. A skill read in the summarized messages would otherwise be lost with
|
|
25
|
+
// them. The compaction names them, and its summary message carries their
|
|
26
|
+
// current text, ahead of the summary, so the model does not have to read them again.
|
|
27
|
+
//
|
|
28
|
+
// IMAGES. The images the user attached to the summarized messages are what the
|
|
29
|
+
// work is often built from, so the summary message carries the newest
|
|
30
|
+
// COMPACTION_USER_IMAGES of them too, as the same file parts; the summary
|
|
31
|
+
// describes the rest. They are still on the stored messages: nothing records them.
|
|
32
|
+
import { generateText } from "ai";
|
|
33
|
+
import { CHARS_PER_TOKEN, IMAGE_TOKENS, estimateRequestTokens, fitMessagesToContext, } from "./context-budget.js";
|
|
34
|
+
import { sparkDataPartSchemas } from "./ui.js";
|
|
35
|
+
import { userImages } from "./user-images.js";
|
|
36
|
+
/** Compact once the conversation reaches this many tokens. */
|
|
37
|
+
export const COMPACTION_THRESHOLD_TOKENS = 256_000;
|
|
38
|
+
/** Recent whole turns kept verbatim, up to this many tokens, besides the current turn. */
|
|
39
|
+
export const COMPACTION_KEEP_RECENT_TOKENS = 16_000;
|
|
40
|
+
/** The user's newest images from the summarized messages that the summary message carries. */
|
|
41
|
+
export const COMPACTION_USER_IMAGES = 4;
|
|
42
|
+
export 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.
|
|
43
|
+
If the conversation begins with an earlier summary, carry its content forward and update it.
|
|
44
|
+
Use these sections:
|
|
45
|
+
## Goal
|
|
46
|
+
## User preferences and constraints
|
|
47
|
+
## Decisions and current scene state (objects with their names and IDs)
|
|
48
|
+
## Done
|
|
49
|
+
## Open and next steps
|
|
50
|
+
## Artifacts and images (each artifact ID, and what each image the user attached shows)
|
|
51
|
+
## Errors and approaches to avoid
|
|
52
|
+
Keep exact names, IDs, numbers and the user's own wording for requirements. Stay under 1,500 words.`;
|
|
53
|
+
export 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.]";
|
|
54
|
+
/** The latest compaction in the conversation whose kept message is still in it. */
|
|
55
|
+
export function latestCompaction(history) {
|
|
56
|
+
for (let carrier = history.length - 1; carrier >= 0; carrier -= 1) {
|
|
57
|
+
const message = history[carrier];
|
|
58
|
+
if (message?.role !== "assistant")
|
|
59
|
+
continue;
|
|
60
|
+
for (let i = message.parts.length - 1; i >= 0; i -= 1) {
|
|
61
|
+
const part = message.parts[i];
|
|
62
|
+
if (part?.type !== "data-compaction")
|
|
63
|
+
continue;
|
|
64
|
+
const parsed = sparkDataPartSchemas.compaction.safeParse(part.data);
|
|
65
|
+
if (!parsed.success)
|
|
66
|
+
continue;
|
|
67
|
+
const firstKept = history.findIndex(({ id }) => id === parsed.data.firstKeptMessageId);
|
|
68
|
+
if (firstKept >= 0)
|
|
69
|
+
return { compaction: parsed.data, carrier, firstKept };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The conversation as the model reads it: the messages before the compaction's
|
|
76
|
+
* kept message replaced by its summary, with the newest COMPACTION_USER_IMAGES
|
|
77
|
+
* images the user attached to them. `compaction` defaults to the latest one in `history`; `skills` are the
|
|
78
|
+
* host's, to send the current text of the skills it names. Without a
|
|
79
|
+
* compaction, `history` is returned as it is.
|
|
80
|
+
*/
|
|
81
|
+
export function applyCompaction(history, options = {}) {
|
|
82
|
+
const compaction = options.compaction ?? latestCompaction(history)?.compaction;
|
|
83
|
+
if (compaction === undefined)
|
|
84
|
+
return history;
|
|
85
|
+
const firstKept = history.findIndex(({ id }) => id === compaction.firstKeptMessageId);
|
|
86
|
+
if (firstKept < 0)
|
|
87
|
+
return history;
|
|
88
|
+
// The skills go before the summary, in a part of their own: they change less
|
|
89
|
+
// often, so the next compaction leaves them in the cached prefix.
|
|
90
|
+
const skills = skillsText(compaction.skills, options.skills);
|
|
91
|
+
const parts = [{ type: "text", text: COMPACTION_NOTE }];
|
|
92
|
+
if (skills !== undefined)
|
|
93
|
+
parts.push({ type: "text", text: skills });
|
|
94
|
+
const shown = new Set(userImages(history.slice(firstKept)).map(({ url }) => url));
|
|
95
|
+
const images = userImages(history.slice(0, firstKept))
|
|
96
|
+
.filter(({ url }) => !shown.has(url))
|
|
97
|
+
.slice(-COMPACTION_USER_IMAGES);
|
|
98
|
+
if (images.length > 0) {
|
|
99
|
+
parts.push({ type: "text", text: "The newest images the user attached to the messages the summary replaces, in the order sent:" }, ...images);
|
|
100
|
+
}
|
|
101
|
+
parts.push({ type: "text", text: `<conversation-summary>\n${compaction.summary}\n</conversation-summary>` });
|
|
102
|
+
return [{ id: `compaction-${compaction.firstKeptMessageId}`, role: "user", parts }, ...history.slice(firstKept)];
|
|
103
|
+
}
|
|
104
|
+
function skillsText(names, available) {
|
|
105
|
+
if (names.length === 0)
|
|
106
|
+
return undefined;
|
|
107
|
+
if (available === undefined) {
|
|
108
|
+
return `Skills read earlier in this conversation (read them again before using their APIs): ${names.join(", ")}`;
|
|
109
|
+
}
|
|
110
|
+
const skills = names.flatMap((name) => available.filter((skill) => skill.name === name));
|
|
111
|
+
if (skills.length === 0)
|
|
112
|
+
return undefined;
|
|
113
|
+
return [
|
|
114
|
+
"Skills already read in this conversation, as read_skill returns them now; they need no reading again:",
|
|
115
|
+
...skills.map((skill) => `<skill name="${skill.name}" version="${skill.version}">\n${skill.instructions}\n\n${skill.declarations}${skill.examples.length > 0 ? `\n\nExamples:\n${skill.examples.join("\n\n")}` : ""}\n</skill>`),
|
|
116
|
+
].join("\n\n");
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The turn's model messages, compacted first when the conversation has reached
|
|
120
|
+
* the threshold. `compaction` is the new one, for the host to record; it is
|
|
121
|
+
* absent when nothing was compacted, including when the summary failed.
|
|
122
|
+
*/
|
|
123
|
+
export async function compactSparkHistory(options) {
|
|
124
|
+
const { history } = options;
|
|
125
|
+
const messages = await options.convert();
|
|
126
|
+
const threshold = Math.min(options.limits?.thresholdTokens ?? COMPACTION_THRESHOLD_TOKENS, options.contextWindow === undefined ? Infinity : options.contextWindow - (options.maxOutputTokens ?? 0));
|
|
127
|
+
const previous = latestCompaction(history);
|
|
128
|
+
const size = conversationTokens(history, previous?.carrier ?? -1) ?? estimate(messages, options);
|
|
129
|
+
if (size < threshold)
|
|
130
|
+
return { messages };
|
|
131
|
+
const firstKept = cutIndex(history, previous?.firstKept ?? 0, options.limits?.keepRecentTokens ?? COMPACTION_KEEP_RECENT_TOKENS);
|
|
132
|
+
if (firstKept === undefined)
|
|
133
|
+
return { messages };
|
|
134
|
+
// The request ends before the current user message: the previous request's
|
|
135
|
+
// prefix, which the provider has cached.
|
|
136
|
+
const current = lastIndex(messages, ({ role }) => role === "user");
|
|
137
|
+
if (current <= 0)
|
|
138
|
+
return { messages };
|
|
139
|
+
options.onCompacting?.();
|
|
140
|
+
let summary;
|
|
141
|
+
try {
|
|
142
|
+
summary = await summarize(messages.slice(0, current), options);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (options.signal?.aborted)
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
options.signal?.throwIfAborted();
|
|
149
|
+
if (summary === undefined)
|
|
150
|
+
return { messages };
|
|
151
|
+
const kept = readSkills(history.slice(firstKept));
|
|
152
|
+
const skills = [
|
|
153
|
+
...new Set([...(previous?.compaction.skills ?? []), ...readSkills(history.slice(previous?.firstKept ?? 0, firstKept))]),
|
|
154
|
+
].filter((name) => !kept.has(name));
|
|
155
|
+
const compaction = {
|
|
156
|
+
firstKeptMessageId: history[firstKept].id,
|
|
157
|
+
summary,
|
|
158
|
+
skills,
|
|
159
|
+
modelId: options.modelId,
|
|
160
|
+
tokensBefore: estimate(messages, options),
|
|
161
|
+
tokensAfter: 0,
|
|
162
|
+
createdAt: new Date().toISOString(),
|
|
163
|
+
};
|
|
164
|
+
const compacted = await options.convert(compaction);
|
|
165
|
+
compaction.tokensAfter = estimate(compacted, options);
|
|
166
|
+
return { messages: compacted, compaction };
|
|
167
|
+
}
|
|
168
|
+
async function summarize(messages, options) {
|
|
169
|
+
const definitions = Object.fromEntries(Object.entries(options.tools).map(([name, tool]) => {
|
|
170
|
+
const { execute: _execute, onInputStart: _start, onInputDelta: _delta, onInputAvailable: _available, ...definition } = tool;
|
|
171
|
+
return [name, definition];
|
|
172
|
+
}));
|
|
173
|
+
const fitted = fitMessagesToContext(messages, {
|
|
174
|
+
contextWindow: options.contextWindow,
|
|
175
|
+
maxOutputTokens: options.maxOutputTokens,
|
|
176
|
+
system: options.system,
|
|
177
|
+
tools: definitions,
|
|
178
|
+
}).messages;
|
|
179
|
+
const result = await generateText({
|
|
180
|
+
model: options.model,
|
|
181
|
+
system: options.system,
|
|
182
|
+
tools: definitions,
|
|
183
|
+
toolChoice: "auto",
|
|
184
|
+
messages: [...fitted, { role: "user", content: [{ type: "text", text: COMPACTION_PROMPT }] }],
|
|
185
|
+
providerOptions: options.providerOptions,
|
|
186
|
+
maxOutputTokens: options.maxOutputTokens,
|
|
187
|
+
abortSignal: options.signal,
|
|
188
|
+
});
|
|
189
|
+
const text = result.text.trim();
|
|
190
|
+
return result.finishReason === "stop" && result.toolCalls.length === 0 && text.length > 0 ? text : undefined;
|
|
191
|
+
}
|
|
192
|
+
function estimate(messages, options) {
|
|
193
|
+
return estimateRequestTokens(messages, { system: options.system, tools: options.tools });
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The conversation's size from the provider's count for its last step, plus an
|
|
197
|
+
* estimate of what came after it. Undefined when there is no count to go by:
|
|
198
|
+
* the request is then estimated whole, which errs high.
|
|
199
|
+
*/
|
|
200
|
+
function conversationTokens(history, carrier) {
|
|
201
|
+
const step = lastStepTokens(history, carrier);
|
|
202
|
+
if (step === undefined)
|
|
203
|
+
return undefined;
|
|
204
|
+
return step.tokens + sum(history.slice(step.index + 1).map(uiMessageTokens));
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Input and output tokens of the conversation's last recorded step, when that
|
|
208
|
+
* step was sent after the latest compaction -- otherwise it measured messages
|
|
209
|
+
* the model no longer reads.
|
|
210
|
+
*/
|
|
211
|
+
function lastStepTokens(history, carrier) {
|
|
212
|
+
for (let index = history.length - 1; index > carrier; index -= 1) {
|
|
213
|
+
const message = history[index];
|
|
214
|
+
if (message?.role !== "assistant")
|
|
215
|
+
continue;
|
|
216
|
+
for (let i = message.parts.length - 1; i >= 0; i -= 1) {
|
|
217
|
+
const part = message.parts[i];
|
|
218
|
+
if (part?.type !== "data-step-metrics")
|
|
219
|
+
continue;
|
|
220
|
+
const usage = part.data.usage;
|
|
221
|
+
if (typeof usage?.inputTokens !== "number")
|
|
222
|
+
continue;
|
|
223
|
+
return { index, tokens: usage.inputTokens + (typeof usage.outputTokens === "number" ? usage.outputTokens : 0) };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The first message to keep: the earliest user message after `floor` such that
|
|
230
|
+
* it and everything up to the current user message fit `keepRecent` tokens.
|
|
231
|
+
* The current user message when no earlier turn fits. Undefined when nothing
|
|
232
|
+
* after `floor` would be summarized.
|
|
233
|
+
*/
|
|
234
|
+
function cutIndex(history, floor, keepRecent) {
|
|
235
|
+
const current = lastIndex(history, ({ role }) => role === "user");
|
|
236
|
+
if (current <= floor)
|
|
237
|
+
return undefined;
|
|
238
|
+
let cut = current;
|
|
239
|
+
let tokens = 0;
|
|
240
|
+
for (let i = current - 1; i > floor; i -= 1) {
|
|
241
|
+
tokens += uiMessageTokens(history[i]);
|
|
242
|
+
if (tokens > keepRecent)
|
|
243
|
+
break;
|
|
244
|
+
if (history[i].role === "user")
|
|
245
|
+
cut = i;
|
|
246
|
+
}
|
|
247
|
+
return cut;
|
|
248
|
+
}
|
|
249
|
+
/** Skills read successfully in these messages, in the order first read. */
|
|
250
|
+
function readSkills(messages) {
|
|
251
|
+
const names = new Set();
|
|
252
|
+
for (const message of messages) {
|
|
253
|
+
if (message.role !== "assistant")
|
|
254
|
+
continue;
|
|
255
|
+
for (const part of message.parts) {
|
|
256
|
+
const isReadSkill = part.type === "tool-read_skill" || (part.type === "dynamic-tool" && part.toolName === "read_skill");
|
|
257
|
+
if (!isReadSkill || part.state !== "output-available")
|
|
258
|
+
continue;
|
|
259
|
+
const skills = part.output?.skills;
|
|
260
|
+
if (!Array.isArray(skills))
|
|
261
|
+
continue;
|
|
262
|
+
for (const skill of skills) {
|
|
263
|
+
if (skill?.ok === true && typeof skill.name === "string")
|
|
264
|
+
names.add(skill.name);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return names;
|
|
269
|
+
}
|
|
270
|
+
/** A stored message's size, roughly: its JSON, with images at their model cost and data URLs left out. */
|
|
271
|
+
function uiMessageTokens(message) {
|
|
272
|
+
let images = 0;
|
|
273
|
+
const json = JSON.stringify(message.parts, (key, value) => {
|
|
274
|
+
if (typeof value === "string" && key === "url" && value.startsWith("data:"))
|
|
275
|
+
return "";
|
|
276
|
+
if (typeof value === "object" && value !== null) {
|
|
277
|
+
const item = value;
|
|
278
|
+
if (item.type === "file" && typeof item.mediaType === "string" && item.mediaType.startsWith("image/"))
|
|
279
|
+
images += 1;
|
|
280
|
+
if (item.kind === "image" && item.inspect !== false)
|
|
281
|
+
images += 1;
|
|
282
|
+
}
|
|
283
|
+
return value;
|
|
284
|
+
});
|
|
285
|
+
return Math.ceil((json?.length ?? 0) / CHARS_PER_TOKEN) + images * IMAGE_TOKENS;
|
|
286
|
+
}
|
|
287
|
+
function sum(values) {
|
|
288
|
+
return values.reduce((total, value) => total + value, 0);
|
|
289
|
+
}
|
|
290
|
+
function lastIndex(items, test) {
|
|
291
|
+
for (let i = items.length - 1; i >= 0; i -= 1) {
|
|
292
|
+
if (test(items[i]))
|
|
293
|
+
return i;
|
|
294
|
+
}
|
|
295
|
+
return -1;
|
|
296
|
+
}
|
package/dist/context-budget.d.ts
CHANGED
|
@@ -61,3 +61,5 @@ export type ContextBudgetResult = {
|
|
|
61
61
|
export declare function fitMessagesToContext(messages: ModelMessage[], options: ContextBudgetOptions): ContextBudgetResult;
|
|
62
62
|
/** The estimate for a list of messages, as `fitMessagesToContext` counts them. */
|
|
63
63
|
export declare function estimateMessagesTokens(messages: ModelMessage[]): number;
|
|
64
|
+
/** The estimate for a whole request: its messages, system prompt and tool definitions. */
|
|
65
|
+
export declare function estimateRequestTokens(messages: ModelMessage[], options: Pick<ContextBudgetOptions, "system" | "tools">): number;
|
package/dist/context-budget.js
CHANGED
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
// WHAT, in this order, until the estimate is back under the target:
|
|
15
15
|
//
|
|
16
16
|
// 1. Images before the current turn, oldest first -- the most tokens for the
|
|
17
|
-
// least text, and the ones a later step least needs to see again.
|
|
17
|
+
// least text, and the ones a later step least needs to see again. The
|
|
18
|
+
// agent's own screenshots go before the images the user attached, which
|
|
19
|
+
// are what the work is often built from.
|
|
18
20
|
// 2. Large tool results before the current turn, oldest first. The call and a
|
|
19
21
|
// result part stay, with a note in place of the output, so no provider is
|
|
20
22
|
// handed a call without its result.
|
|
@@ -155,12 +157,12 @@ export function fitMessagesToContext(messages, options) {
|
|
|
155
157
|
* replacement and roughly what that saves -- used only to stop partway through a
|
|
156
158
|
* message, which is then measured again.
|
|
157
159
|
*/
|
|
158
|
-
const rewriteEarlier = (rewrite) => {
|
|
160
|
+
const rewriteEarlier = (rewrite, only = () => true) => {
|
|
159
161
|
for (let i = 0; i < protectedFrom && total > target; i += 1) {
|
|
160
162
|
if (i === current)
|
|
161
163
|
continue;
|
|
162
164
|
const message = out[i];
|
|
163
|
-
if (message === undefined || !Array.isArray(message.content))
|
|
165
|
+
if (message === undefined || !Array.isArray(message.content) || !only(message))
|
|
164
166
|
continue;
|
|
165
167
|
let projected = total;
|
|
166
168
|
let changed = false;
|
|
@@ -183,8 +185,8 @@ export function fitMessagesToContext(messages, options) {
|
|
|
183
185
|
out[i] = next;
|
|
184
186
|
}
|
|
185
187
|
};
|
|
186
|
-
// 1. Earlier images.
|
|
187
|
-
|
|
188
|
+
// 1. Earlier images: the agent's own first, then those the user attached.
|
|
189
|
+
const omitImages = (part, projected) => {
|
|
188
190
|
if (isImage(part)) {
|
|
189
191
|
report.omittedImages += 1;
|
|
190
192
|
return { part: omittedImage(), saved: IMAGE_TOKENS };
|
|
@@ -201,7 +203,9 @@ export function fitMessagesToContext(messages, options) {
|
|
|
201
203
|
return omittedImage();
|
|
202
204
|
});
|
|
203
205
|
return saved === 0 ? { part, saved } : { part: { ...part, output: { ...part.output, value } }, saved };
|
|
204
|
-
}
|
|
206
|
+
};
|
|
207
|
+
rewriteEarlier(omitImages, ({ role }) => role !== "user");
|
|
208
|
+
rewriteEarlier(omitImages, ({ role }) => role === "user");
|
|
205
209
|
// 2. Earlier large tool results.
|
|
206
210
|
rewriteEarlier((part) => {
|
|
207
211
|
if (!isToolResult(part))
|
|
@@ -249,6 +253,10 @@ export function fitMessagesToContext(messages, options) {
|
|
|
249
253
|
export function estimateMessagesTokens(messages) {
|
|
250
254
|
return sum(messages.map(messageTokens));
|
|
251
255
|
}
|
|
256
|
+
/** The estimate for a whole request: its messages, system prompt and tool definitions. */
|
|
257
|
+
export function estimateRequestTokens(messages, options) {
|
|
258
|
+
return estimateMessagesTokens(messages) + textTokens(options.system ?? "") + toolsTokens(options.tools);
|
|
259
|
+
}
|
|
252
260
|
// ── estimation ──────────────────────────────────────────────────────────────
|
|
253
261
|
const estimates = new WeakMap();
|
|
254
262
|
function messageTokens(message) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { runSparkAgent, type SparkAgentOptions, type SparkAgentResult, type SparkAgentRun, } from "./agent.js";
|
|
2
2
|
export { createSparkTools, type SparkPersistence, type SparkToolCall } from "./agent-tools.js";
|
|
3
3
|
export { convertSparkMessages } from "./model-history.js";
|
|
4
|
+
export type { SparkCompactionLimits } from "./compaction.js";
|
|
4
5
|
export * from "./context-budget.js";
|
|
5
6
|
export * from "./capabilities.js";
|
|
6
7
|
export { createSdkSceneEditor, createSdkSceneHost, type SdkSceneEditor, type SdkSceneEditorOptions, type SdkSceneEditIdentity, type SdkSceneServices, } from "./sdk-scene-host.js";
|
package/dist/model-history.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { type ModelMessage } from "ai";
|
|
2
2
|
import type { createSparkTools } from "./agent-tools.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import type { SparkSkill } from "./capabilities.js";
|
|
4
|
+
import { type SparkCompaction, type SparkUIMessage } from "./ui.js";
|
|
5
|
+
/**
|
|
6
|
+
* The stored conversation as model messages. UI artifact previews already
|
|
7
|
+
* represented by a tool result are not model output. The messages before the
|
|
8
|
+
* latest compaction's kept message -- or `options.compaction`'s -- are sent as
|
|
9
|
+
* its summary, with the current text of the skills it names from
|
|
10
|
+
* `options.skills` and the images the user attached to them (./compaction.ts).
|
|
11
|
+
*/
|
|
12
|
+
export declare function convertSparkMessages(messages: SparkUIMessage[], tools: ReturnType<typeof createSparkTools>, options?: {
|
|
13
|
+
compaction?: SparkCompaction;
|
|
14
|
+
skills?: readonly SparkSkill[];
|
|
15
|
+
}): Promise<ModelMessage[]>;
|
package/dist/model-history.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { asSchema, convertToModelMessages } from "ai";
|
|
2
|
+
import { applyCompaction } from "./compaction.js";
|
|
2
3
|
import { sparkDataPartSchemas } from "./ui.js";
|
|
3
4
|
function record(value) {
|
|
4
5
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
@@ -32,9 +33,15 @@ function goalConditionsText({ conditions }) {
|
|
|
32
33
|
+ (reviewed ? `\nPreferences the user will review themselves:${reviewed}` : "")
|
|
33
34
|
+ "]";
|
|
34
35
|
}
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
/**
|
|
37
|
+
* The stored conversation as model messages. UI artifact previews already
|
|
38
|
+
* represented by a tool result are not model output. The messages before the
|
|
39
|
+
* latest compaction's kept message -- or `options.compaction`'s -- are sent as
|
|
40
|
+
* its summary, with the current text of the skills it names from
|
|
41
|
+
* `options.skills` and the images the user attached to them (./compaction.ts).
|
|
42
|
+
*/
|
|
43
|
+
export async function convertSparkMessages(messages, tools, options = {}) {
|
|
44
|
+
const history = await Promise.all(applyCompaction(messages, options).map(async (message) => {
|
|
38
45
|
if (message.role !== "assistant")
|
|
39
46
|
return message;
|
|
40
47
|
const previews = new Set();
|
|
@@ -8,6 +8,8 @@ export type ResolvedSparkModel = {
|
|
|
8
8
|
maxOutputTokens?: number;
|
|
9
9
|
};
|
|
10
10
|
export type SparkRunExecutorOptions = {
|
|
11
|
+
/** When a run's conversation is compacted; see `SparkAgentOptions.compaction`. */
|
|
12
|
+
compaction?: SparkAgentOptions["compaction"];
|
|
11
13
|
resolveModel(settings: SparkRunSettings): ResolvedSparkModel | Promise<ResolvedSparkModel>;
|
|
12
14
|
createHost(context: SparkRunExecutionContext): Promise<Pick<SparkAgentOptions, "capabilities" | "executor" | "artifacts" | "persistence"> & {
|
|
13
15
|
dispose(): void;
|
package/dist/server/executor.js
CHANGED
|
@@ -1,25 +1,17 @@
|
|
|
1
|
-
import { convertSparkMessages } from "../model-history.js";
|
|
2
1
|
import { runSparkAgent } from "../agent.js";
|
|
3
|
-
import { createSparkTools } from "../agent-tools.js";
|
|
4
2
|
export function createSparkRunExecutor(options) {
|
|
5
3
|
return async (context) => {
|
|
6
4
|
const resolved = await options.resolveModel(context.settings);
|
|
7
5
|
const host = await options.createHost(context);
|
|
8
6
|
try {
|
|
9
|
-
const tools = createSparkTools({
|
|
10
|
-
...host,
|
|
11
|
-
runId: context.runId,
|
|
12
|
-
controller: new AbortController(),
|
|
13
|
-
write: () => undefined,
|
|
14
|
-
});
|
|
15
|
-
const messages = await convertSparkMessages(context.uiMessages, tools);
|
|
16
7
|
const run = runSparkAgent({
|
|
17
8
|
...host,
|
|
18
|
-
|
|
9
|
+
history: context.uiMessages,
|
|
19
10
|
model: resolved.model,
|
|
20
11
|
modelId: resolved.modelId,
|
|
21
12
|
providerOptions: resolved.providerOptions,
|
|
22
13
|
maxOutputTokens: resolved.maxOutputTokens,
|
|
14
|
+
compaction: options.compaction,
|
|
23
15
|
runId: context.runId,
|
|
24
16
|
messageId: context.assistantMessageId,
|
|
25
17
|
signal: context.signal,
|
|
@@ -2,6 +2,8 @@ import { z } from "zod";
|
|
|
2
2
|
import type { SparkGoal } from "../goals.js";
|
|
3
3
|
import type { SparkUIMessage } from "../ui.js";
|
|
4
4
|
import type { SparkRunExecutorOptions } from "./executor.js";
|
|
5
|
+
/** How many of the user's newest images an evaluation attaches as the reference. */
|
|
6
|
+
export declare const GOAL_REFERENCE_IMAGES = 4;
|
|
5
7
|
export declare const goalVerdictSchema: z.ZodObject<{
|
|
6
8
|
conditions: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
7
9
|
index: z.ZodNumber;
|
|
@@ -19,6 +21,11 @@ export type SparkGoalEvaluation = z.infer<typeof goalVerdictSchema>;
|
|
|
19
21
|
export type SparkGoalEvaluator = (input: {
|
|
20
22
|
ownerId?: string;
|
|
21
23
|
goal: SparkGoal;
|
|
24
|
+
/**
|
|
25
|
+
* The conversation. The images the user attached to it are the evaluation's
|
|
26
|
+
* reference, read only from `data:` URLs: a host that stores them elsewhere
|
|
27
|
+
* passes the messages with those images inline.
|
|
28
|
+
*/
|
|
22
29
|
messages: SparkUIMessage[];
|
|
23
30
|
context?: Record<string, unknown>;
|
|
24
31
|
signal: AbortSignal;
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { generateText, Output } from "ai";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { userImages } from "../user-images.js";
|
|
3
4
|
import { WOMPIFY_PRESETS, wompifyContextSchema } from "../wompify.js";
|
|
5
|
+
/** How many of the user's newest images an evaluation attaches as the reference. */
|
|
6
|
+
export const GOAL_REFERENCE_IMAGES = 4;
|
|
7
|
+
/** A `data:` URL's bytes; hosts hand the evaluator the user's images inline. */
|
|
8
|
+
function dataUrlBytes(url) {
|
|
9
|
+
const match = /^data:[^,]*;base64,(.*)$/s.exec(url);
|
|
10
|
+
return match ? new Uint8Array(Buffer.from(match[1], "base64")) : undefined;
|
|
11
|
+
}
|
|
4
12
|
const verdictShape = {
|
|
5
13
|
verdict: z.enum(["met", "continue", "blocked"]),
|
|
6
14
|
reason: z.string().min(1).max(2_000),
|
|
@@ -36,6 +44,20 @@ export function createSparkGoalEvaluator(options) {
|
|
|
36
44
|
const content = [];
|
|
37
45
|
const unavailable = [];
|
|
38
46
|
let bytes = 0;
|
|
47
|
+
// The images the user attached are the reference the reconstruction views
|
|
48
|
+
// are compared with, so they go first, ahead of the byte cap. A Wompify
|
|
49
|
+
// goal's reference is a scene object instead.
|
|
50
|
+
const references = wompify.success ? [] : userImages(messages).slice(-GOAL_REFERENCE_IMAGES);
|
|
51
|
+
for (const [index, image] of references.entries()) {
|
|
52
|
+
const name = image.filename ?? `reference ${index + 1}`;
|
|
53
|
+
const data = dataUrlBytes(image.url);
|
|
54
|
+
if (!data || bytes + data.byteLength > 8_000_000) {
|
|
55
|
+
unavailable.push(name);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
bytes += data.byteLength;
|
|
59
|
+
content.push({ type: "text", text: `Reference image the user attached: ${name}` }, { type: "file", data, mediaType: image.mediaType });
|
|
60
|
+
}
|
|
39
61
|
for (const id of selected) {
|
|
40
62
|
signal.throwIfAborted();
|
|
41
63
|
const image = ownerId ? await options.resolveImage?.(ownerId, id) : undefined;
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { MemorySparkArtifactStore } from "./artifacts.js";
|
|
2
2
|
export { WompifyCreditCapReachedError } from "../wompify.js";
|
|
3
|
-
export { createSparkGoalEvaluator, type SparkGoalEvaluator, type SparkGoalEvaluation } from "./goal-evaluator.js";
|
|
3
|
+
export { createSparkGoalEvaluator, GOAL_REFERENCE_IMAGES, type SparkGoalEvaluator, type SparkGoalEvaluation } from "./goal-evaluator.js";
|
|
4
4
|
export { createSparkGoalProposer, type SparkGoalProposer } from "./goal-proposer.js";
|
|
5
5
|
export { sparkGoalCommandSchema, sparkGoalProposalSchema, type SparkGoal, type SparkGoalCommand, type SparkGoalCondition, type SparkGoalProposal, type SparkGoalTry } from "../goals.js";
|
|
6
6
|
export type { SparkArtifactStore, StoredSparkArtifact } from "./artifacts.js";
|
package/dist/server/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { MemorySparkArtifactStore } from "./artifacts.js";
|
|
2
2
|
export { WompifyCreditCapReachedError } from "../wompify.js";
|
|
3
|
-
export { createSparkGoalEvaluator } from "./goal-evaluator.js";
|
|
3
|
+
export { createSparkGoalEvaluator, GOAL_REFERENCE_IMAGES } from "./goal-evaluator.js";
|
|
4
4
|
export { createSparkGoalProposer } from "./goal-proposer.js";
|
|
5
5
|
export { sparkGoalCommandSchema, sparkGoalProposalSchema } from "../goals.js";
|
|
6
6
|
export { createSparkRunExecutor, } from "./executor.js";
|
package/dist/ui.d.ts
CHANGED
|
@@ -91,12 +91,31 @@ export type SparkDataParts = {
|
|
|
91
91
|
receipt: ExecutionReceipt;
|
|
92
92
|
};
|
|
93
93
|
selection: SparkSelectionData;
|
|
94
|
+
compaction: SparkCompaction;
|
|
94
95
|
goal: SparkGoalProposal;
|
|
95
96
|
};
|
|
96
97
|
/** Editor node IDs selected when a user message was sent; never rendered as chat text. */
|
|
97
98
|
export type SparkSelectionData = {
|
|
98
99
|
nodeIds: string[];
|
|
99
100
|
};
|
|
101
|
+
/**
|
|
102
|
+
* A summary of a long conversation's earlier messages, recorded on the assistant
|
|
103
|
+
* message of the turn that wrote it. The model is sent the summary in place of
|
|
104
|
+
* every message before `firstKeptMessageId`; the chat still shows all of them.
|
|
105
|
+
*/
|
|
106
|
+
export type SparkCompaction = {
|
|
107
|
+
/** The user message the model reads verbatim from; everything before it is summarized. */
|
|
108
|
+
firstKeptMessageId: string;
|
|
109
|
+
summary: string;
|
|
110
|
+
/** Skills read in the summarized messages; their current text is sent after the summary. */
|
|
111
|
+
skills: string[];
|
|
112
|
+
/** The model that wrote the summary. */
|
|
113
|
+
modelId: string;
|
|
114
|
+
/** The conversation's size before and after, in tokens. */
|
|
115
|
+
tokensBefore: number;
|
|
116
|
+
tokensAfter: number;
|
|
117
|
+
createdAt: string;
|
|
118
|
+
};
|
|
100
119
|
export type ExecuteJavaScriptInput = {
|
|
101
120
|
purpose: string;
|
|
102
121
|
code: string;
|
|
@@ -335,6 +354,15 @@ export declare const sparkDataPartSchemas: {
|
|
|
335
354
|
readonly selection: z.ZodObject<{
|
|
336
355
|
nodeIds: z.ZodArray<z.ZodString>;
|
|
337
356
|
}, z.core.$strip>;
|
|
357
|
+
readonly compaction: z.ZodObject<{
|
|
358
|
+
firstKeptMessageId: z.ZodString;
|
|
359
|
+
summary: z.ZodString;
|
|
360
|
+
skills: z.ZodArray<z.ZodString>;
|
|
361
|
+
modelId: z.ZodString;
|
|
362
|
+
tokensBefore: z.ZodNumber;
|
|
363
|
+
tokensAfter: z.ZodNumber;
|
|
364
|
+
createdAt: z.ZodString;
|
|
365
|
+
}, z.core.$strip>;
|
|
338
366
|
/**
|
|
339
367
|
* The conditions the user confirmed for the goal this message starts. The model
|
|
340
368
|
* reads it as text (model-history.ts).
|
package/dist/ui.js
CHANGED
|
@@ -86,6 +86,15 @@ export const sparkDataPartSchemas = {
|
|
|
86
86
|
*/
|
|
87
87
|
// Editor ids are the engine's decimal integers; 8192 covers a select-all at the engine's budgets.
|
|
88
88
|
selection: z.object({ nodeIds: z.array(z.string().min(1).max(64)).min(1).max(8192) }),
|
|
89
|
+
compaction: z.object({
|
|
90
|
+
firstKeptMessageId: z.string().min(1),
|
|
91
|
+
summary: z.string().min(1),
|
|
92
|
+
skills: z.array(z.string().min(1)),
|
|
93
|
+
modelId: z.string(),
|
|
94
|
+
tokensBefore: z.number().int().nonnegative(),
|
|
95
|
+
tokensAfter: z.number().int().nonnegative(),
|
|
96
|
+
createdAt: z.string().datetime(),
|
|
97
|
+
}),
|
|
89
98
|
/**
|
|
90
99
|
* The conditions the user confirmed for the goal this message starts. The model
|
|
91
100
|
* reads it as text (model-history.ts).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SparkUIMessage } from "./ui.js";
|
|
2
|
+
export type SparkFilePart = Extract<SparkUIMessage["parts"][number], {
|
|
3
|
+
type: "file";
|
|
4
|
+
}>;
|
|
5
|
+
/**
|
|
6
|
+
* The images the user attached to `messages`, each once, in the order sent:
|
|
7
|
+
* what the work is often built from, so the model calls that do or judge it
|
|
8
|
+
* keep them longest.
|
|
9
|
+
*/
|
|
10
|
+
export declare function userImages(messages: readonly SparkUIMessage[]): SparkFilePart[];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The images the user attached to `messages`, each once, in the order sent:
|
|
3
|
+
* what the work is often built from, so the model calls that do or judge it
|
|
4
|
+
* keep them longest.
|
|
5
|
+
*/
|
|
6
|
+
export function userImages(messages) {
|
|
7
|
+
const seen = new Set();
|
|
8
|
+
const images = [];
|
|
9
|
+
for (const message of messages) {
|
|
10
|
+
if (message.role !== "user")
|
|
11
|
+
continue;
|
|
12
|
+
for (const part of message.parts) {
|
|
13
|
+
if (part.type !== "file" || !part.mediaType.startsWith("image/") || seen.has(part.url))
|
|
14
|
+
continue;
|
|
15
|
+
seen.add(part.url);
|
|
16
|
+
images.push(part);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return images;
|
|
20
|
+
}
|