@economic/agents 2.10.5 → 2.10.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,7 +7,7 @@ React client: [`@economic/agents-react`](../react/README.md).
7
7
  ## The three classes
8
8
 
9
9
  - **`Agent`** — the core. Runs the agent loop and keeps message history. Drive it over a WebSocket or programmatically (schedule, alarm, RPC). Most agents stop here.
10
- - **`ChatAgent`** — `Agent` plus chat features: compaction and message feedback.
10
+ - **`ChatAgent`** — `Agent` plus chat features: model-only compaction that preserves the client transcript, and message feedback.
11
11
  - **`Assistant`** — per-user shell over `ChatAgent`: create/list/delete chats, titles, summaries, retention.
12
12
 
13
13
  ```
@@ -188,7 +188,7 @@ You can also drive turns server-side — call `saveMessages` from a schedule or
188
188
 
189
189
  ## A chat agent
190
190
 
191
- For a chat UI, extend `ChatAgent` instead of `Agent`. It adds compaction and message feedback, and connects to a browser with [`useChat`](../react/README.md#usechat) — no `Assistant` required. The DO name is the chat; address one per user, ticket, or whatever fits.
191
+ For a chat UI, extend `ChatAgent` instead of `Agent`. It adds model-only compaction (the stored/client transcript remains unchanged) and message feedback, and connects to a browser with [`useChat`](../react/README.md#usechat) — no `Assistant` required. The DO name is the chat; address one per user, ticket, or whatever fits.
192
192
 
193
193
  ```typescript
194
194
  import { openai } from "@ai-sdk/openai";
@@ -578,7 +578,7 @@ Imported from `@economic/agents` unless noted.
578
578
  | Export | Description |
579
579
  | ----------- | -------------------------------------------------------- |
580
580
  | `Agent` | Core agent base. Implement `getModel`/`getSystemPrompt`. |
581
- | `ChatAgent` | `Agent` + compaction and message feedback. |
581
+ | `ChatAgent` | `Agent` + model-only compaction and message feedback. |
582
582
  | `Assistant` | Per-user manager of `ChatAgent` chats. |
583
583
 
584
584
  ### Helpers and types
package/dist/index.d.mts CHANGED
@@ -186,7 +186,7 @@ declare abstract class Agent<RequestContext extends Record<string, unknown> = Re
186
186
  * @returns The remote skills to be loaded from SKILLS_BUCKET.
187
187
  */
188
188
  protected getRemoteSkills(): string[];
189
- private _pruneTurnMessages;
189
+ protected pruneTurnMessages(messages: ModelMessage[]): ModelMessage[];
190
190
  private _getTurnSystemPrompt;
191
191
  private _prepareTools;
192
192
  /**
package/dist/index.mjs CHANGED
@@ -4,8 +4,8 @@ import { Output, convertToModelMessages, generateText, jsonSchema, pruneMessages
4
4
  import { createRemoteJWKSet, decodeJwt, errors, jwtVerify } from "jose";
5
5
  import { BasicTracerProvider, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
6
6
  import { genericObservability } from "agents/observability";
7
+ import { createCompactFunction, estimateMessageTokens, estimateStringTokens, sanitizeToolPairs, truncateOlderMessages } from "agents/experimental/memory/utils";
7
8
  import { nanoid } from "nanoid";
8
- import { createCompactFunction } from "agents/experimental/memory/utils";
9
9
  //#region src/server/features/auth/index.ts
10
10
  function createJwtAuthConfig(config) {
11
11
  return config;
@@ -1058,7 +1058,7 @@ var Agent = class extends Think {
1058
1058
  return {
1059
1059
  system: this._getTurnSystemPrompt(),
1060
1060
  model: this.getModel(this._turn.context),
1061
- messages: this._pruneTurnMessages(ctx.messages),
1061
+ messages: this.pruneTurnMessages(ctx.messages),
1062
1062
  tools: this._withTurnContextFallback(tools),
1063
1063
  activeTools,
1064
1064
  experimental_transform: buildSourcesTransform(),
@@ -1165,7 +1165,7 @@ var Agent = class extends Think {
1165
1165
  tools: withFallbackToolContext(tools, () => snapshotContext),
1166
1166
  activeTools,
1167
1167
  frameworkToolNames: Object.keys(frameworkTools),
1168
- messages: this._pruneTurnMessages(messages)
1168
+ messages: this.pruneTurnMessages(messages)
1169
1169
  };
1170
1170
  } finally {
1171
1171
  this._turn = previousTurn;
@@ -1183,7 +1183,7 @@ var Agent = class extends Think {
1183
1183
  getRemoteSkills() {
1184
1184
  return [];
1185
1185
  }
1186
- _pruneTurnMessages(messages) {
1186
+ pruneTurnMessages(messages) {
1187
1187
  return pruneMessages({
1188
1188
  messages,
1189
1189
  toolCalls: [{
@@ -1978,15 +1978,130 @@ var Assistant = class extends Agent$1 {
1978
1978
  agentClass: this.constructor.name
1979
1979
  }) });
1980
1980
  };
1981
- //#endregion
1982
- //#region src/server/features/messages.ts
1983
- const COMPACTION_TOKEN_THRESHOLD = 1e5;
1984
1981
  const COMPACTION_MAX_OUTPUT_TOKENS = 2e3;
1985
- const createCompactFn = (model) => createCompactFunction({ summarize: (prompt) => generateText({
1986
- model,
1987
- prompt,
1988
- maxOutputTokens: COMPACTION_MAX_OUTPUT_TOKENS
1989
- }).then((result) => result.text) });
1982
+ const createCompactFn = (model) => createCompactFunction({
1983
+ protectHead: 2,
1984
+ minTailMessages: 1,
1985
+ summarize: (prompt) => generateText({
1986
+ model,
1987
+ prompt,
1988
+ maxOutputTokens: COMPACTION_MAX_OUTPUT_TOKENS
1989
+ }).then((result) => result.text)
1990
+ });
1991
+ const MODEL_COMPACTION_MESSAGE_ID = "compaction_model_context";
1992
+ /**
1993
+ * Model compaction is deliberately stored separately from assistant_messages.
1994
+ * The transcript remains exact for clients; only model input uses the summary.
1995
+ */
1996
+ function ensureModelCompactionTable(sql) {
1997
+ sql`CREATE TABLE IF NOT EXISTS economic_chat_model_compaction (
1998
+ id INTEGER PRIMARY KEY CHECK (id = 1),
1999
+ summary TEXT NOT NULL,
2000
+ from_message_id TEXT NOT NULL,
2001
+ to_message_id TEXT NOT NULL,
2002
+ updated_at INTEGER NOT NULL
2003
+ )`;
2004
+ }
2005
+ function getModelCompaction(sql) {
2006
+ const row = sql`SELECT summary, from_message_id, to_message_id
2007
+ FROM economic_chat_model_compaction
2008
+ WHERE id = 1`[0];
2009
+ return row ? {
2010
+ summary: row.summary,
2011
+ fromMessageId: row.from_message_id,
2012
+ toMessageId: row.to_message_id
2013
+ } : null;
2014
+ }
2015
+ function setModelCompaction(sql, state) {
2016
+ sql`INSERT INTO economic_chat_model_compaction (
2017
+ id, summary, from_message_id, to_message_id, updated_at
2018
+ ) VALUES (
2019
+ 1, ${state.summary}, ${state.fromMessageId}, ${state.toMessageId}, ${Date.now()}
2020
+ ) ON CONFLICT (id) DO UPDATE SET
2021
+ summary = excluded.summary,
2022
+ from_message_id = excluded.from_message_id,
2023
+ to_message_id = excluded.to_message_id,
2024
+ updated_at = excluded.updated_at`;
2025
+ }
2026
+ function clearModelCompaction(sql) {
2027
+ sql`DELETE FROM economic_chat_model_compaction WHERE id = 1`;
2028
+ }
2029
+ /**
2030
+ * Move summaries created by the old Session overlay implementation into the
2031
+ * model-only store, then remove the overlays. Session retained the original
2032
+ * rows, so removing an overlay restores the exact transcript.
2033
+ */
2034
+ function migrateSessionCompaction(sql) {
2035
+ const legacy = sql`SELECT
2036
+ summary, from_message_id, to_message_id
2037
+ FROM assistant_compactions
2038
+ WHERE session_id = ''
2039
+ ORDER BY created_at DESC, rowid DESC
2040
+ LIMIT 1`[0];
2041
+ if (!legacy) return false;
2042
+ if (!getModelCompaction(sql)) setModelCompaction(sql, {
2043
+ summary: legacy.summary,
2044
+ fromMessageId: legacy.from_message_id,
2045
+ toMessageId: legacy.to_message_id
2046
+ });
2047
+ sql`DELETE FROM assistant_compactions WHERE session_id = ''`;
2048
+ return true;
2049
+ }
2050
+ function applyModelCompaction(messages, state) {
2051
+ const fromIndex = messages.findIndex((message) => message.id === state.fromMessageId);
2052
+ const toIndex = messages.findIndex((message) => message.id === state.toMessageId);
2053
+ if (fromIndex === -1 || toIndex < fromIndex) return null;
2054
+ return [
2055
+ ...messages.slice(0, fromIndex),
2056
+ {
2057
+ id: MODEL_COMPACTION_MESSAGE_ID,
2058
+ role: "assistant",
2059
+ parts: [{
2060
+ type: "text",
2061
+ text: state.summary
2062
+ }]
2063
+ },
2064
+ ...messages.slice(toIndex + 1)
2065
+ ];
2066
+ }
2067
+ /** Build the compacted projection used only for the next model call. */
2068
+ async function prepareModelHistory(messages, existingState, compact, systemPrompt = "") {
2069
+ let state = existingState;
2070
+ let projected = state ? applyModelCompaction(messages, state) : messages;
2071
+ let stateChanged = false;
2072
+ if (!projected) {
2073
+ state = null;
2074
+ projected = messages;
2075
+ stateChanged = true;
2076
+ }
2077
+ if (estimateMessageTokens(projected) + estimateStringTokens(systemPrompt) <= 1e5) return {
2078
+ messages: projected,
2079
+ state,
2080
+ stateChanged
2081
+ };
2082
+ const result = await compact(projected);
2083
+ if (!result) return {
2084
+ messages: projected,
2085
+ state,
2086
+ stateChanged
2087
+ };
2088
+ const nextState = {
2089
+ summary: result.summary,
2090
+ fromMessageId: state?.fromMessageId ?? result.fromMessageId,
2091
+ toMessageId: result.toMessageId
2092
+ };
2093
+ const nextProjection = applyModelCompaction(messages, nextState);
2094
+ if (!nextProjection) return {
2095
+ messages: projected,
2096
+ state,
2097
+ stateChanged
2098
+ };
2099
+ return {
2100
+ messages: nextProjection,
2101
+ state: nextState,
2102
+ stateChanged: true
2103
+ };
2104
+ }
1990
2105
  /**
1991
2106
  * Ensures that the feedback table exists.
1992
2107
  * @param sql - The SQL function to use to execute the query.
@@ -2191,14 +2306,38 @@ var ChatAgent = class extends Agent {
2191
2306
  async onStart() {
2192
2307
  await super.onStart();
2193
2308
  ensureFeedbackTableExists(this.sql.bind(this));
2309
+ ensureModelCompactionTable(this.sql.bind(this));
2310
+ if (migrateSessionCompaction(this.sql.bind(this))) await this.syncMessagesFromStorage();
2194
2311
  }
2195
2312
  configureSession(session) {
2196
- return super.configureSession(session).onCompaction(createCompactFn(this.getModel())).compactAfter(COMPACTION_TOKEN_THRESHOLD);
2313
+ return super.configureSession(session);
2197
2314
  }
2198
2315
  async beforeTurn(ctx) {
2199
2316
  const parent = await this.getParentAgent();
2200
2317
  if (parent?.registerChat) await parent.registerChat(this.name);
2201
- return super.beforeTurn(ctx);
2318
+ const config = await super.beforeTurn(ctx) ?? {};
2319
+ try {
2320
+ const prepared = await prepareModelHistory(this.messages, getModelCompaction(this.sql.bind(this)), createCompactFn(config.model ?? ctx.model), config.system ?? ctx.system);
2321
+ if (prepared.stateChanged) if (prepared.state) setModelCompaction(this.sql.bind(this), prepared.state);
2322
+ else clearModelCompaction(this.sql.bind(this));
2323
+ if (prepared.state) {
2324
+ const tools = config.tools ? {
2325
+ ...ctx.tools,
2326
+ ...config.tools
2327
+ } : ctx.tools;
2328
+ const modelMessages = await convertToModelMessages(truncateOlderMessages(sanitizeToolPairs(prepared.messages)), {
2329
+ tools,
2330
+ ignoreIncompleteToolCalls: true
2331
+ });
2332
+ return {
2333
+ ...config,
2334
+ messages: this.pruneTurnMessages(modelMessages)
2335
+ };
2336
+ }
2337
+ } catch (error) {
2338
+ console.error("[ChatAgent] Failed to prepare model-only compaction", error);
2339
+ }
2340
+ return config;
2202
2341
  }
2203
2342
  async onChatResponse(result) {
2204
2343
  recordTurnCompletedAnalytics(this.env.AGENTS_ANALYTICS, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@economic/agents",
3
- "version": "2.10.5",
3
+ "version": "2.10.6",
4
4
  "description": "A starter for creating a TypeScript package.",
5
5
  "license": "MIT",
6
6
  "files": [