@womp/spark-agent 6.4.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 CHANGED
@@ -4,6 +4,103 @@ 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
+
57
+ ## 6.5.0 — 2026-10-12
58
+
59
+ The agent proposes a goal during its turn, instead of the host asking a separate model call to
60
+ read every message before it is sent. Ordinary messages start at once, the proposal shares the
61
+ run's prompt cache, and when to propose is decided in one place: the tool's description.
62
+
63
+ ### Added
64
+
65
+ - The `propose_goal({ title, conditions })` tool, declared only when the host can ask the user.
66
+ The run waits in `requires_action` for the answer, so nothing is built before it. The tool
67
+ result tells the agent to build toward the goal, to carry on as a single try, or to stop and
68
+ wait.
69
+ - The `data-goal-proposal` part (`sparkGoalProposalPartSchema`) stores the proposal on the
70
+ assistant message that made it: `title`, `conditions`, the `toolCallId`, and the `outcome` once
71
+ there is one. Hosts render it as the confirmation while its run waits and as the goal once it
72
+ started; the model does not read it.
73
+ - `SparkAgentServiceOptions.confirmGoal` shows a proposal and resolves a `SparkGoalDecision`
74
+ (`sparkGoalDecisionSchema`): a goal with its caps, a single try, or unanswered. A confirmed goal
75
+ starts with the run that proposed it as its first try, with the run's request as its objective.
76
+ - `SparkAgentServiceOptions.onGoalStart`, called when a confirmed goal is stored and before the
77
+ agent continues. A goal it rejects does not start.
78
+ - `SparkRunExecutionContext.proposeGoal` and `SparkAgentOptions.proposeGoal` (`SparkGoalOffer`)
79
+ carry the offer to the agent. A custom `executeRun` that drops it leaves the tool undeclared.
80
+
81
+ ### Removed
82
+
83
+ - `createSparkGoalProposer`, `SparkGoalProposer`, `SparkAgentService.proposeGoal`,
84
+ `SparkAgentServiceOptions.proposeGoal`, `POST /conversations/:id/goal/proposal` and
85
+ `SparkChatTransport.proposeGoal`.
86
+ - `title` and `conditions` on the goal `set` command, and the `data-goal` part with its
87
+ `sparkDataPartSchemas.goal` schema: conditions now come only from a proposal the user confirmed.
88
+ A conversation that holds a `data-goal` part, written by 6.3.0 or 6.4.0, no longer validates.
89
+ - `SparkChatTransport.sendMessages` no longer forwards `body.goal`.
90
+ - `MAX_GOAL_LENGTH` from `@womp/spark-agent/ui`.
91
+
92
+ ### Migration
93
+
94
+ - Replace the pre-send proposal call and the `set` command that followed it with `confirmGoal`:
95
+ show the proposal when asked, resolve the user's answer, and send every message as an ordinary
96
+ one. Render the `data-goal-proposal` part as the confirmation while its run waits.
97
+ - A host that billed a goal from the `set` command starts counting in `onGoalStart`; the first
98
+ try's run began before the goal existed.
99
+ - A host that stores tool calls by name must accept `propose_goal`, and must offer the tool again
100
+ when it replays that history, so the earlier call reads as it did live.
101
+ - A host that validates stored messages against `sparkDataPartSchemas` must be on this version
102
+ before it reads a conversation that holds a `data-goal-proposal` part.
103
+
7
104
  ## 6.4.0 — 2026-10-12
8
105
 
9
106
  ### Added
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
- Before each model step, Spark trims older images, large tool results, and whole earlier turns to
79
- keep the request inside the model's context window. The latest user message and the most recent
80
- steps are never trimmed.
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
 
@@ -138,7 +140,6 @@ Closing or reloading the page only detaches the SSE subscriber. It does not canc
138
140
  POST /agent/conversations/:conversationId/messages
139
141
  PATCH /agent/conversations/:conversationId/settings
140
142
  POST /agent/conversations/:conversationId/goal
141
- POST /agent/conversations/:conversationId/goal/proposal
142
143
  GET /agent/runs/:runId/stream?after=0
143
144
  POST /agent/runs/:runId/cancel
144
145
  ```
@@ -147,14 +148,17 @@ Conversation settings contain `modelId` and `reasoningEffort`; save them with
147
148
  `transport.saveConversationSettings(id, settings)`. The goal endpoint accepts `set`, `pause`,
148
149
  `resume`, `clear`, and `update_budget` commands.
149
150
 
150
- A goal starts from an ordinary message. `transport.proposeGoal(id, message, settings)` returns a
151
- title and the success conditions read from the message, each marked `measurable` or left to the
152
- user's review; it stores and starts nothing. After the user confirms them with a tries cap
153
- (`maxTurns`) and a credit cap, send the message with `body.goal` set to a `set` command carrying
154
- `title`, `conditions`, `maxTurns` and `creditBudget`. The service attaches the confirmed conditions
155
- to that message as a `data-goal` part, evaluates each measurable condition after every try, and
156
- records the result on `goal.conditions`. Send the message without `body.goal` to try once. Hosts
157
- enable proposals with `SparkAgentServiceOptions.proposeGoal`, such as `createSparkGoalProposer`.
151
+ A goal starts from an ordinary message, when the agent proposes one. A host that sets
152
+ `SparkAgentServiceOptions.confirmGoal` (with `evaluateGoal`) gives the agent the `propose_goal`
153
+ tool: it calls it before building when the request sets constraints a result can be measured
154
+ against, with a title and the conditions, each marked `measurable` or left to the user's review.
155
+ The run then waits in `requires_action` while `confirmGoal` shows the proposal, which the
156
+ assistant message stores as a `data-goal-proposal` part, and resolves the user's answer: `{ kind: "goal", maxTurns,
157
+ creditBudget }`, `{ kind: "once" }`, or `{ kind: "unanswered" }` when the host stops waiting. A
158
+ confirmed goal starts with that run as its first try; the service evaluates each measurable
159
+ condition after every try and records the result on `goal.conditions`. Any other answer leaves an
160
+ ordinary turn. The part then carries the `outcome`: `started`, `declined`, `unanswered`, or `active`
161
+ when a goal was already running. `onGoalStart` tells a host that counts a goal's spending when one starts.
158
162
 
159
163
  The SSE response contains ordered persistence envelopes around standard AI SDK chunks:
160
164
 
@@ -353,7 +357,8 @@ features, patch for backward-compatible fixes.
353
357
 
354
358
  - `src/agent.ts` — AI SDK stream and Spark tool orchestration
355
359
  - `src/prompt.ts` — shared system prompt
356
- - `src/context-budget.ts` — per-step context-window trimming
360
+ - `src/compaction.ts` — conversation compaction before each model step
361
+ - `src/context-window.ts` — model context windows and request size estimates
357
362
  - `src/ui.ts` — browser-safe protocol types and validation schemas
358
363
  - `src/client.ts` — durable `ChatTransport` implementation
359
364
  - `src/server/` — service, storage contracts, Express adapter, artifacts, and standalone composition
@@ -1,12 +1,21 @@
1
1
  import { type Tool } from "ai";
2
2
  import type { ExecutionReceipt, SparkArtifactResolver, SparkCapabilities } from "./capabilities.js";
3
3
  import type { JavaScriptExecutor } from "./executor.js";
4
+ import { type SparkGoalOutcome, type SparkGoalProposal } from "./goals.js";
4
5
  import type { SparkUIMessage, SparkUIMessageChunk, SparkUITools } from "./ui.js";
5
6
  export type SparkToolCall = {
6
7
  id: string;
7
- name: "read_skill" | "execute_javascript";
8
+ name: "read_skill" | "execute_javascript" | "propose_goal";
8
9
  input: unknown;
9
10
  };
11
+ /**
12
+ * Shows the user a goal the model proposed and waits for their answer, starting
13
+ * the goal for this run when they confirm it.
14
+ */
15
+ export type SparkGoalOffer = (proposal: SparkGoalProposal, call: {
16
+ toolCallId: string;
17
+ signal: AbortSignal;
18
+ }) => Promise<SparkGoalOutcome>;
10
19
  export interface SparkPersistence {
11
20
  beforeToolCall(call: SparkToolCall): Promise<void>;
12
21
  recordReceipt(callId: string, receipt: ExecutionReceipt): Promise<void>;
@@ -22,9 +31,16 @@ export type SparkToolOptions = {
22
31
  controller: AbortController;
23
32
  persistence?: SparkPersistence;
24
33
  executionTimeoutMs?: number;
34
+ /** Present when the host can ask the user: the model then has `propose_goal`. */
35
+ proposeGoal?: SparkGoalOffer;
25
36
  write(chunk: SparkUIMessageChunk): void;
26
37
  };
27
- export type SparkTools = {
28
- [Name in keyof SparkUITools]: Tool<SparkUITools[Name]["input"], SparkUITools[Name]["output"]>;
38
+ type SparkTool<Name extends keyof SparkUITools> = Tool<SparkUITools[Name]["input"], SparkUITools[Name]["output"]>;
39
+ type SparkCoreTools = {
40
+ [Name in "read_skill" | "execute_javascript"]: SparkTool<Name>;
29
41
  };
42
+ export type SparkTools = SparkCoreTools | (SparkCoreTools & {
43
+ propose_goal: SparkTool<"propose_goal">;
44
+ });
30
45
  export declare function createSparkTools(options: SparkToolOptions): SparkTools;
46
+ export {};
@@ -3,6 +3,7 @@ import { z } from "zod";
3
3
  import { boundedResultText, canonicalJsonValue } from "./bounded-output.js";
4
4
  import { completedInputString } from "./tool-input-label.js";
5
5
  import { UserWait } from "./execution-lifetime.js";
6
+ import { sparkGoalProposalSchema } from "./goals.js";
6
7
  function errorMessage(error) {
7
8
  return error instanceof Error ? error.message : String(error);
8
9
  }
@@ -21,6 +22,13 @@ const artifactRefSchema = z.object({
21
22
  mediaType: z.string().min(1),
22
23
  name: z.string().optional(),
23
24
  });
25
+ /** What the model is told a proposal came to, and so what to do next. */
26
+ const GOAL_OUTCOMES = {
27
+ started: "The user confirmed the goal. Build it now, and verify every measurable condition with tool results before you finish. An independent check follows each try and sends back what is still unmet.",
28
+ declined: "The user chose a single try. Carry out the request as usual and do not propose a goal for it again.",
29
+ unanswered: "The user has not answered. Build nothing: say that you are waiting for their answer, and stop.",
30
+ active: "A goal is already running in this conversation. Keep working toward it.",
31
+ };
24
32
  export function createSparkTools(options) {
25
33
  const { controller, persistence } = options;
26
34
  const executionTimeoutMs = options.executionTimeoutMs ?? 60_000;
@@ -76,7 +84,37 @@ export function createSparkTools(options) {
76
84
  onInputAvailable: ({ toolCallId }) => { inputs.delete(toolCallId); },
77
85
  };
78
86
  };
87
+ const { proposeGoal } = options;
79
88
  return {
89
+ ...(proposeGoal === undefined ? {} : {
90
+ propose_goal: tool({
91
+ description: "Offer to run the user's request as a goal: work that repeats until an independent check verifies its conditions. Call it once, before building or generating anything, when the request sets a constraint the finished result passes or fails by measurement: a size, a count, a limit, a fit to something real, a clearance, an angle, a weight, a price, or a physical requirement such as being one piece, resting on the ground or parts not intersecting. Do not call it for a question, a small direct edit, a request that only describes what to make, or while a goal is running. It shows the user the conditions and waits for their answer.",
92
+ inputSchema: sparkGoalProposalSchema.extend({
93
+ title: sparkGoalProposalSchema.shape.title.describe("Two to four words in sentence case naming what is being made."),
94
+ conditions: sparkGoalProposalSchema.shape.conditions.describe("Only constraints the user stated or directly implied, never your own, each marked measurable. The description of what to make is not a list of constraints: leave out its parts and features, and any instruction about how to work, what to check or what to render. State each as a fact about the finished result in a few words of the user's language, with its number and unit: \"Height is 120 mm\", not \"Make it 120 mm tall\". Split compound constraints. If the user asked for a look, style, feel or quality that takes judgement, add it as one condition that is not measurable, left to their review."),
95
+ }),
96
+ execute: (input, { toolCallId }) => execute({ id: toolCallId, name: "propose_goal", input }, async () => {
97
+ const status = (status, label) => options.write({ type: "data-run-status", data: { runId: options.runId, status, label } });
98
+ // The message keeps the proposal, and then what became of it, under one id.
99
+ const store = (outcome) => options.write({
100
+ type: "data-goal-proposal", id: `goal-proposal-${toolCallId}`,
101
+ data: { ...input, toolCallId, ...(outcome ? { outcome } : {}) },
102
+ });
103
+ store();
104
+ status("requires_action", "Waiting for your answer");
105
+ try {
106
+ const goal = await proposeGoal(input, { toolCallId, signal: controller.signal });
107
+ store(goal);
108
+ return { goal };
109
+ }
110
+ finally {
111
+ if (!controller.signal.aborted)
112
+ status("running", "Continuing after user input");
113
+ }
114
+ }),
115
+ toModelOutput: ({ output }) => ({ type: "text", value: GOAL_OUTCOMES[output.goal] }),
116
+ }),
117
+ }),
80
118
  read_skill: tool({
81
119
  ...inputLabels("name"),
82
120
  description: "Read the skills listed in names. Returns instructions, TypeScript API declarations, and JavaScript examples from the available directory.",
package/dist/agent.d.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import { streamText, type LanguageModel, type ModelMessage } from "ai";
2
- import { type SparkPersistence } from "./agent-tools.js";
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 first once it is long enough (./compaction.ts); the new
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;
@@ -35,13 +34,13 @@ export type SparkAgentOptions = {
35
34
  maxOutputTokens?: number;
36
35
  executionTimeoutMs?: number;
37
36
  persistence?: SparkPersistence;
37
+ /** Gives the model `propose_goal`; absent, it has no way to offer a goal. */
38
+ proposeGoal?: SparkGoalOffer;
38
39
  prepareMessages?(messages: ModelMessage[]): Promise<ModelMessage[]>;
39
- /** The model's context window. Absent: `contextWindowFor(modelId)`. */
40
+ /** The model's context window, which caps when `history` is compacted. Absent: `contextWindowFor(modelId)`. */
40
41
  contextWindow?: number;
41
42
  /** When `history` is compacted. Absent: the defaults in ./compaction.ts. */
42
43
  compaction?: SparkCompactionLimits;
43
- /** Called when a step's messages had to be trimmed to fit, or still do not fit. */
44
- onContextBudget?(report: ContextBudgetReport): void;
45
44
  };
46
45
  export type SparkAgentRun = {
47
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, fitMessagesToContext } from "./context-budget.js";
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 compacted = await compactSparkHistory({
137
- history,
138
- convert: (compaction) => convertSparkMessages(history, tools, { compaction, skills: options.capabilities.skills }),
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
- writer.write({ type: "data-compaction", id: `compaction-${options.messageId}`, data: compacted.compaction });
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
- const prepared = options.prepareMessages === undefined
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 options.prepareMessages(messages);
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: fitted.messages,
204
+ system: readSystem(),
205
+ messages: prepared,
188
206
  };
189
207
  },
190
208
  onError: ({ error }) => {
package/dist/client.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { ChatRequestOptions, ChatTransport, UIMessageChunk } from "ai";
2
2
  import type { SparkRunSettings } from "./server/types.js";
3
- import type { SparkGoal, SparkGoalCommand, SparkGoalProposal } from "./goals.js";
4
- export type { SparkGoal, SparkGoalCommand, SparkGoalCondition, SparkGoalProposal, SparkGoalTry } from "./goals.js";
3
+ import type { SparkGoal, SparkGoalCommand } from "./goals.js";
4
+ export type { SparkGoal, SparkGoalCommand, SparkGoalCondition, SparkGoalDecision, SparkGoalProposal, SparkGoalTry } from "./goals.js";
5
5
  import type { SparkConversation, SparkConversationSummary, SparkRun } from "./server/types.js";
6
6
  import type { SparkUIMessage } from "./ui.js";
7
7
  export type { SparkConversation, SparkConversationSummary, SparkRun, SparkRunSettings, } from "./server/types.js";
@@ -44,8 +44,6 @@ export declare class SparkChatTransport implements ChatTransport<SparkUIMessage>
44
44
  goal?: SparkGoal;
45
45
  run?: SparkRun;
46
46
  }>;
47
- /** The success conditions read from a message, to confirm before sending it as a goal. */
48
- proposeGoal(id: string, message: SparkUIMessage, settings: SparkRunSettings, signal?: AbortSignal): Promise<SparkGoalProposal>;
49
47
  getGoal(id: string): Promise<SparkGoal | undefined>;
50
48
  getRun(id: string): Promise<SparkRun>;
51
49
  saveConversationSettings(id: string, settings: SparkRunSettings): Promise<void>;
package/dist/client.js CHANGED
@@ -60,8 +60,6 @@ export class SparkChatTransport {
60
60
  message,
61
61
  messageId: options.messageId,
62
62
  settings,
63
- // A confirmed goal: the message starts it, and the run is its first try.
64
- ...(requestBody?.goal === undefined ? {} : { goal: requestBody.goal }),
65
63
  }),
66
64
  signal: options.abortSignal,
67
65
  }, options.headers);
@@ -129,15 +127,6 @@ export class SparkChatTransport {
129
127
  throw await responseError(response);
130
128
  return response.json();
131
129
  }
132
- /** The success conditions read from a message, to confirm before sending it as a goal. */
133
- async proposeGoal(id, message, settings, signal) {
134
- const response = await this.request(`/conversations/${encodeURIComponent(id)}/goal/proposal`, {
135
- method: "POST", body: JSON.stringify({ message, settings }), signal,
136
- });
137
- if (!response.ok)
138
- throw await responseError(response);
139
- return (await response.json()).proposal;
140
- }
141
130
  async getGoal(id) {
142
131
  const response = await this.request(`/conversations/${encodeURIComponent(id)}/goal`, { method: "GET" });
143
132
  if (!response.ok)
@@ -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, besides the current turn. */
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. `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.
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
- export type CompactSparkHistoryOptions = {
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 {};