@womp/spark-agent 5.8.0 → 5.9.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/.env.example ADDED
@@ -0,0 +1,6 @@
1
+ OPENAI_API_KEY=
2
+ ANTHROPIC_API_KEY=
3
+ GEMINI_API_KEY=
4
+ KAKAPO_URL=ws://127.0.0.1:5502
5
+ PORT=8787
6
+ SPARK_STORAGE_PATH=.spark/spark.sqlite
@@ -12,6 +12,15 @@ export declare const TRIM_TRIGGER_RATIO = 0.9;
12
12
  export declare const TRIM_TARGET_RATIO = 0.75;
13
13
  /** A tool result at or below this is left whole: not worth the detail it loses. */
14
14
  export declare const SMALL_TOOL_OUTPUT_TOKENS = 500;
15
+ /**
16
+ * Steps of the CURRENT turn kept whole, however long that turn runs.
17
+ *
18
+ * The model is mid-task and reasons from what it just did, so the recent steps
19
+ * have to survive; the ones before them are history like any other and are worth
20
+ * more as window than as detail. Five is small enough that a long run always
21
+ * recovers room and large enough to hold a read-edit-verify cycle.
22
+ */
23
+ export declare const PROTECTED_CURRENT_STEPS = 5;
15
24
  export declare const OMITTED_HISTORY_TEXT = "[Earlier messages in this conversation were left out of this request to fit the model's context window.]";
16
25
  export declare const OMITTED_HISTORY_IMAGE_TEXT = "[An earlier image was left out of this request to fit the model's context window.]";
17
26
  export declare const OMITTED_TOOL_OUTPUT_TEXT = "[This earlier tool result was left out of this request to fit the model's context window.]";
@@ -22,9 +22,22 @@
22
22
  // still paired with its result. A note in their place tells the model that
23
23
  // earlier messages were left out.
24
24
  //
25
- // The current turn -- the latest user message and everything after it -- is never
26
- // changed. When it alone is over the window the request goes out as it is and the
27
- // provider's own error says so; nothing here can make that turn smaller.
25
+ // THE CURRENT TURN is the latest user message and everything after it. The
26
+ // message itself -- the question and any image the user just attached -- is never
27
+ // changed, and neither are the last PROTECTED_CURRENT_STEPS steps after it: the
28
+ // model has to be able to see what it just did.
29
+ //
30
+ // Its EARLIER steps are trimmed like any other history, by stages 1 and 2 above.
31
+ // They were exempt until 2026-10-01, on the assumption that a turn is small. An
32
+ // agentic run's turn is not: every step it takes lands after that user message,
33
+ // and `maxSteps` is 100. dev0 thread 142417 reached the window that way -- ten
34
+ // minutes of work, earlier history already trimmed to nothing, and what was left
35
+ // over the limit was the run's own tool results, which nothing was allowed to
36
+ // touch. Stage 3 still stops at the current turn, because dropping whole messages
37
+ // inside it cannot keep calls paired with their results.
38
+ //
39
+ // When the protected part alone is over the window the request still goes out as
40
+ // it is, and the provider's own error says so.
28
41
  //
29
42
  // ESTIMATES, NOT COUNTS. Counting needs the provider's tokenizer or a request.
30
43
  // The estimate is deliberately high (CHARS_PER_TOKEN is under the ~3.5-4 of prose)
@@ -50,6 +63,15 @@ export const TRIM_TRIGGER_RATIO = 0.9;
50
63
  export const TRIM_TARGET_RATIO = 0.75;
51
64
  /** A tool result at or below this is left whole: not worth the detail it loses. */
52
65
  export const SMALL_TOOL_OUTPUT_TOKENS = 500;
66
+ /**
67
+ * Steps of the CURRENT turn kept whole, however long that turn runs.
68
+ *
69
+ * The model is mid-task and reasons from what it just did, so the recent steps
70
+ * have to survive; the ones before them are history like any other and are worth
71
+ * more as window than as detail. Five is small enough that a long run always
72
+ * recovers room and large enough to hold a read-edit-verify cycle.
73
+ */
74
+ export const PROTECTED_CURRENT_STEPS = 5;
53
75
  export const OMITTED_HISTORY_TEXT = "[Earlier messages in this conversation were left out of this request to fit the model's context window.]";
54
76
  export const OMITTED_HISTORY_IMAGE_TEXT = "[An earlier image was left out of this request to fit the model's context window.]";
55
77
  export const OMITTED_TOOL_OUTPUT_TEXT = "[This earlier tool result was left out of this request to fit the model's context window.]";
@@ -76,6 +98,25 @@ export function contextWindowFor(modelId) {
76
98
  return 1_048_576;
77
99
  return undefined;
78
100
  }
101
+ /**
102
+ * Where the current turn's protected tail begins: the Nth-from-last step.
103
+ *
104
+ * Counted by ASSISTANT messages, because that is what starts a step -- the tool
105
+ * messages that answer it follow and are protected with it, so no call is ever
106
+ * separated from its result. A turn with fewer steps than that protects all of
107
+ * them, which is what this did for every turn before it could trim one.
108
+ */
109
+ function protectedTailStart(messages, current) {
110
+ let steps = 0;
111
+ for (let i = messages.length - 1; i > current; i -= 1) {
112
+ if (messages[i]?.role !== "assistant")
113
+ continue;
114
+ steps += 1;
115
+ if (steps >= PROTECTED_CURRENT_STEPS)
116
+ return i;
117
+ }
118
+ return current + 1;
119
+ }
79
120
  export function fitMessagesToContext(messages, options) {
80
121
  if (options.contextWindow === undefined)
81
122
  return { messages };
@@ -96,10 +137,15 @@ export function fitMessagesToContext(messages, options) {
96
137
  };
97
138
  if (originalTokens <= budgetTokens * TRIM_TRIGGER_RATIO)
98
139
  return { messages, report };
99
- // The current turn starts at the latest user message and is never changed.
140
+ // The current turn starts at the latest user message. With no user message at
141
+ // all there is no turn to reason about and nothing is touched.
100
142
  const current = lastIndex(messages, ({ role }) => role === "user");
101
- if (current <= 0)
143
+ if (current < 0)
102
144
  return { messages, report };
145
+ // Stages 1 and 2 rewrite everything before this: all earlier turns, and the
146
+ // current turn's own earlier steps. `current` itself is skipped -- the user's
147
+ // question and the image they just attached are not history.
148
+ const protectedFrom = protectedTailStart(messages, current);
103
149
  const target = budgetTokens * TRIM_TARGET_RATIO;
104
150
  const out = messages.slice();
105
151
  let total = originalTokens;
@@ -110,7 +156,9 @@ export function fitMessagesToContext(messages, options) {
110
156
  * message, which is then measured again.
111
157
  */
112
158
  const rewriteEarlier = (rewrite) => {
113
- for (let i = 0; i < current && total > target; i += 1) {
159
+ for (let i = 0; i < protectedFrom && total > target; i += 1) {
160
+ if (i === current)
161
+ continue;
114
162
  const message = out[i];
115
163
  if (message === undefined || !Array.isArray(message.content))
116
164
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@womp/spark-agent",
3
- "version": "5.8.0",
3
+ "version": "5.9.0",
4
4
  "description": "Portable skills-based AI agent with local and durable chat transports for Womp",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",