@cognipeer/agent-sdk 0.1.2 → 0.2.1

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
@@ -1,6 +1,6 @@
1
1
  # @cognipeer/agent-sdk
2
2
 
3
- [![npm](https://img.shields.io/npm/v/@cognipeer/agent-sdk?color=success)](https://npmjs.com/package/@cognipeer/agent-sdk) [Docs Website](https://cognipeer.github.io/agent-sdk/)
3
+ [![npm](https://img.shields.io/npm/v/@cognipeer/agent-sdk?color=success)](https://npmjs.com/package/@cognipeer/agent-sdk) [Docs Website](https://cognipeer.github.io/agent-sdk/)
4
4
 
5
5
  Lightweight, message-first agent runtime that keeps tool calls transparent, automatically summarizes long histories, and ships with planning, multi-agent handoffs, and structured tracing.
6
6
 
@@ -158,12 +158,27 @@ Examples live under `examples/` with per-folder READMEs. Build the package first
158
158
  | `mcp-tavily/` | MCP remote tool discovery. |
159
159
  | `vision/` | Text + image input using LangChain’s OpenAI bindings. |
160
160
 
161
- Run directly with `tsx`, for example:
161
+ To run examples:
162
162
 
163
- ```sh
164
- # from repo root
165
- npm run build
166
- OPENAI_API_KEY=... npx tsx examples/tools/tools.ts
163
+ ```bash
164
+ # Install root dependencies
165
+ npm install
166
+
167
+ # Install example dependencies
168
+ cd examples
169
+ npm install
170
+
171
+ # Run an example from the examples directory
172
+ npm run example:basic
173
+ npm run example:tools
174
+ npm run example:multi-agent
175
+ ```
176
+
177
+ Or run directly with tsx:
178
+
179
+ ```bash
180
+ # From examples directory
181
+ OPENAI_API_KEY=... npx tsx basic/basic.ts
167
182
  ```
168
183
 
169
184
  ## Architecture snapshot
package/dist/index.cjs CHANGED
@@ -1105,9 +1105,58 @@ function createAgentCoreNode(opts) {
1105
1105
  const shouldLogPrompt = !!traceSession && traceSession.resolvedConfig.logData;
1106
1106
  const promptPayload = shouldLogPrompt ? sanitizeTracePayload(state.messages) : void 0;
1107
1107
  const promptBytes = promptPayload !== void 0 ? estimatePayloadBytes(promptPayload) : void 0;
1108
+ const extractToolUses = (m) => {
1109
+ const tcs = m?.tool_calls || m?.additional_kwargs?.tool_calls;
1110
+ if (!Array.isArray(tcs)) return [];
1111
+ return tcs.map((tc) => ({ id: tc?.id, name: tc?.function?.name || tc?.name })).filter((x) => typeof x.id === "string" && x.id.length > 0);
1112
+ };
1113
+ const normalizeBedrockToolPairing = (input) => {
1114
+ const msgs = Array.isArray(input) ? [...input] : [];
1115
+ for (let i = 0; i < msgs.length; i++) {
1116
+ const m = msgs[i];
1117
+ if (m?.role !== "assistant") continue;
1118
+ const toolUses = extractToolUses(m);
1119
+ if (toolUses.length === 0) continue;
1120
+ const next = msgs[i + 1];
1121
+ const firstId = toolUses[0]?.id;
1122
+ const nextIsToolForFirst = !!(next && next.role === "tool" && next.tool_call_id === firstId);
1123
+ if (nextIsToolForFirst) continue;
1124
+ const placeholders = toolUses.map((tu) => ({
1125
+ role: "tool",
1126
+ name: tu.name || "unknown_tool",
1127
+ tool_call_id: tu.id,
1128
+ content: "SUMMARIZED/DEFERRED: tool result missing in transcript; inserted placeholder for Bedrock tool_result adjacency."
1129
+ }));
1130
+ msgs.splice(i + 1, 0, ...placeholders);
1131
+ i += placeholders.length;
1132
+ }
1133
+ return msgs;
1134
+ };
1135
+ const debugToolPairing = (messagesToSend) => {
1136
+ try {
1137
+ const msgs = Array.isArray(messagesToSend) ? messagesToSend : [];
1138
+ const toolUseIds = [];
1139
+ const missingResults = [];
1140
+ for (let i = 0; i < msgs.length; i++) {
1141
+ const m = msgs[i];
1142
+ if (m?.role === "assistant") {
1143
+ for (const tu of extractToolUses(m)) {
1144
+ toolUseIds.push(tu.id);
1145
+ const next = msgs[i + 1];
1146
+ if (!(next && next.role === "tool" && (next.tool_call_id === tu.id || next.tool_call_id === m?.tool_call_id))) {
1147
+ missingResults.push({ id: tu.id, atIndex: i, tool: tu.name });
1148
+ }
1149
+ }
1150
+ }
1151
+ }
1152
+ } catch (e) {
1153
+ }
1154
+ };
1108
1155
  let response;
1109
1156
  try {
1110
- response = await modelWithTools.invoke([...state.messages]);
1157
+ const normalizedMessages = normalizeBedrockToolPairing([...state.messages]);
1158
+ debugToolPairing(normalizedMessages);
1159
+ response = await modelWithTools.invoke(normalizedMessages);
1111
1160
  } catch (err) {
1112
1161
  const durationMs2 = Date.now() - start;
1113
1162
  recordTraceEvent(traceSession, {
@@ -1694,6 +1743,12 @@ function resolveToolApprovalState(state, resolution) {
1694
1743
  };
1695
1744
  }
1696
1745
 
1746
+ // src/utils/utilTokens.ts
1747
+ function countApproxTokens(text) {
1748
+ if (!text) return 0;
1749
+ return Math.ceil(text.length / 4);
1750
+ }
1751
+
1697
1752
  // src/agent.ts
1698
1753
  function createAgent(opts) {
1699
1754
  const resolver = createResolverNode();
@@ -1758,8 +1813,8 @@ function createAgent(opts) {
1758
1813
  delete nextCtx.__resumeStage;
1759
1814
  state = { ...state, ctx: Object.keys(nextCtx).length > 0 ? nextCtx : void 0 };
1760
1815
  }
1761
- const maxToolCalls = opts.limits?.maxToolCalls ?? 10;
1762
- const iterationLimit = Math.max(maxToolCalls * 3 + 10, 40);
1816
+ const maxToolCalls = opts.limits?.maxToolCalls === void 0 ? 50 : opts.limits?.maxToolCalls;
1817
+ const iterationLimit = maxToolCalls === Infinity ? 100 : Math.max(maxToolCalls * 3 + 10, 40);
1763
1818
  let iterations = 0;
1764
1819
  const onStateChange = config?.onStateChange;
1765
1820
  const checkpointReason = config?.checkpointReason;
@@ -1826,6 +1881,22 @@ function createAgent(opts) {
1826
1881
  }
1827
1882
  }
1828
1883
  }
1884
+ let maxTok;
1885
+ if (opts.summarization && typeof opts.summarization === "object" && opts.summarization.maxTokens) {
1886
+ maxTok = opts.summarization.maxTokens;
1887
+ }
1888
+ if (maxTok === void 0) {
1889
+ maxTok = 5e4;
1890
+ }
1891
+ if (maxTok) {
1892
+ const allText = (state.messages || []).map((m) => typeof m.content === "string" ? m.content : Array.isArray(m.content) ? m.content.map((c) => typeof c === "string" ? c : c?.text ?? c?.content ?? "").join("") : "").join("\n");
1893
+ const tokenCount = countApproxTokens(allText);
1894
+ if (tokenCount > maxTok) {
1895
+ const ctx = { ...state.ctx || {}, __needsSummarization: true };
1896
+ state = { ...state, ctx };
1897
+ break;
1898
+ }
1899
+ }
1829
1900
  state = { ...state, ...await agentCore(state) };
1830
1901
  if (checkpointIfRequested("after_agent")) break;
1831
1902
  const postGuardrails = getGuardrailConfig(state);
@@ -1899,6 +1970,35 @@ function createAgent(opts) {
1899
1970
  if (checkpointIfRequested("after_tools")) break;
1900
1971
  if (state.ctx?.__finalizedDueToStructuredOutput) break;
1901
1972
  }
1973
+ if (opts.outputSchema && !state.ctx?.__finalizedDueToStructuredOutput) {
1974
+ const last = state.messages[state.messages.length - 1];
1975
+ const lastHasToolCalls = Array.isArray(last?.tool_calls) && last.tool_calls.length > 0;
1976
+ if (!lastHasToolCalls) {
1977
+ const forceMsg = {
1978
+ role: "system",
1979
+ content: [
1980
+ "A structured output schema is active.",
1981
+ "You MUST now call tool `response` with the final JSON object that matches the schema.",
1982
+ "Do not write the JSON in the assistant message.",
1983
+ "Call `response` exactly once, then stop."
1984
+ ].join("\n")
1985
+ };
1986
+ const alreadyForced = Boolean(state.ctx?.__structuredOutputForceFinalize);
1987
+ if (!alreadyForced) {
1988
+ const ctx = { ...state.ctx || {}, __structuredOutputForceFinalize: true };
1989
+ state = { ...state, messages: [...state.messages, forceMsg], ctx };
1990
+ try {
1991
+ state = { ...state, ...await agentCore(state) };
1992
+ const lastAfter = state.messages[state.messages.length - 1];
1993
+ const toolCallsAfter = Array.isArray(lastAfter?.tool_calls) ? lastAfter.tool_calls : [];
1994
+ if (toolCallsAfter.length > 0) {
1995
+ state = { ...state, ...await toolsNode(state) };
1996
+ }
1997
+ } catch {
1998
+ }
1999
+ }
2000
+ }
2001
+ }
1902
2002
  if (!pausedStage && typeof onStateChange === "function" && onStateChange(state)) {
1903
2003
  checkpointIfRequested("after_loop");
1904
2004
  }
@@ -2133,234 +2233,100 @@ function createAgentNode(opts) {
2133
2233
  };
2134
2234
  }
2135
2235
 
2136
- // src/utils/utilTokens.ts
2137
- function countApproxTokens(text) {
2138
- if (!text) return 0;
2139
- return Math.ceil(text.length / 4);
2140
- }
2141
-
2142
2236
  // src/nodes/contextSummarize.ts
2143
2237
  var systemMessage = (content) => ({ role: "system", content });
2144
2238
  var humanMessage = (content) => ({ role: "user", content });
2145
- var toolMessage = (name, content, tool_call_id) => ({ role: "tool", name, content, tool_call_id });
2239
+ function getSummarizationConfig(opts) {
2240
+ if (typeof opts.summarization === "object") {
2241
+ return {
2242
+ enabled: opts.summarization.enable !== false,
2243
+ maxTokens: opts.summarization.maxTokens
2244
+ };
2245
+ }
2246
+ return {
2247
+ enabled: opts.summarization !== false,
2248
+ maxTokens: 5e4
2249
+ };
2250
+ }
2146
2251
  function createContextSummarizeNode(opts) {
2147
2252
  return async (state) => {
2148
- const messages = state.messages || [];
2149
- const summaryTokenLimit = opts.limits?.summaryTokenLimit ?? 1e5;
2253
+ const config = getSummarizationConfig(opts);
2254
+ if (!config.enabled) return {};
2150
2255
  const model = opts.model;
2151
- const archived = state.toolHistoryArchived ? [...state.toolHistoryArchived] : [];
2152
- const live = state.toolHistory ? [...state.toolHistory] : [];
2153
- const byCallId = /* @__PURE__ */ new Map();
2154
- for (const item of archived) {
2155
- if (item.tool_call_id) byCallId.set(item.tool_call_id, item);
2156
- }
2157
- for (const item of live) {
2158
- if (item.tool_call_id) byCallId.set(item.tool_call_id, item);
2159
- }
2160
- for (const item of live) {
2161
- if (!item.executionId) item.executionId = nanoid.nanoid();
2256
+ const messages = state.messages || [];
2257
+ const compressableMessages = messages.filter((m) => m.role === "tool" && m.content !== "SUMMARIZED");
2258
+ if (compressableMessages.length === 0) {
2259
+ return {};
2162
2260
  }
2163
- const getText = (m) => {
2164
- if (!m) return "";
2165
- const c = m.content;
2166
- if (typeof c === "string") return c;
2167
- if (Array.isArray(c)) return c.map((p) => typeof p === "string" ? p : p?.text ?? p?.content ?? "").join("");
2168
- return String(c ?? "");
2169
- };
2170
- const sysText = "You are a concise summarization assistant for a tool-using agent. Summarize prior tool executions into a compact brief that preserves key facts, decisions, and outputs. Reference executionId values where helpful. Avoid repeating raw data; prefer synthesis.";
2171
- const sys = systemMessage(sysText);
2172
- let summaryText = "";
2173
- try {
2174
- const limit = Math.max(1e3, Number(summaryTokenLimit) || 1e5);
2175
- const safetyBuffer = Math.floor(Math.min(4e3, limit * 0.05));
2176
- const perCallBudget = Math.max(500, limit - safetyBuffer);
2177
- const safeToolPair = opts?.safeToolPair !== false;
2178
- const chunks = [];
2179
- let current = [];
2180
- let currentTokens = countApproxTokens(sysText);
2181
- let pendingToolCalls = /* @__PURE__ */ new Set();
2182
- const extractToolCallIds = (msg) => {
2183
- const tc = msg?.additional_kwargs?.tool_calls;
2184
- if (!Array.isArray(tc) || tc.length === 0) return [];
2185
- return tc.map((c) => c?.id).filter((id) => typeof id === "string");
2186
- };
2187
- for (let i = 0; i < messages.length; i++) {
2188
- const m = messages[i];
2189
- const text = getText(m);
2190
- const tks = countApproxTokens(text);
2191
- const isAIWithToolCalls = safeToolPair && m?.role === "assistant" && extractToolCallIds(m).length > 0;
2192
- const isToolMsg = safeToolPair && m?.role === "tool";
2193
- const toolCallId = isToolMsg ? m.tool_call_id : void 0;
2194
- const wouldExceed = currentTokens + tks > perCallBudget && current.length > 0;
2195
- if (wouldExceed) {
2196
- let forceAddToCurrent = false;
2197
- if (safeToolPair) {
2198
- if (isToolMsg && toolCallId && pendingToolCalls.has(toolCallId)) {
2199
- forceAddToCurrent = true;
2200
- } else if (isAIWithToolCalls) {
2201
- } else if (pendingToolCalls.size > 0) {
2202
- }
2203
- }
2204
- if (!forceAddToCurrent) {
2205
- if (current.length > 0) {
2206
- chunks.push(current);
2207
- }
2208
- current = [];
2209
- currentTokens = countApproxTokens(sysText);
2210
- pendingToolCalls = /* @__PURE__ */ new Set();
2211
- }
2212
- }
2213
- current.push(m);
2214
- currentTokens += tks;
2215
- if (isAIWithToolCalls) {
2216
- for (const id of extractToolCallIds(m)) pendingToolCalls.add(id);
2217
- }
2218
- if (isToolMsg && toolCallId && pendingToolCalls.has(toolCallId)) {
2219
- pendingToolCalls.delete(toolCallId);
2220
- }
2221
- if (currentTokens >= perCallBudget && pendingToolCalls.size === 0) {
2222
- chunks.push(current);
2223
- current = [];
2224
- currentTokens = countApproxTokens(sysText);
2225
- pendingToolCalls = /* @__PURE__ */ new Set();
2226
- }
2227
- }
2228
- if (current.length > 0) {
2229
- chunks.push(current);
2230
- }
2231
- const partials = [];
2232
- for (const chunk of chunks) {
2233
- let resp = null;
2234
- try {
2235
- resp = await (model?.invoke ? model.invoke([sys, ...chunk]) : null);
2236
- const c = resp?.content;
2237
- const text = typeof c === "string" ? c : Array.isArray(c) ? c.map((p) => typeof p === "string" ? p : p?.text ?? p?.content ?? "").join("") : "";
2238
- partials.push((text || "").trim());
2239
- } catch (e) {
2240
- console.error("Summarization chunk failed:", e);
2241
- partials.push("[summary unavailable for one chunk]");
2242
- }
2261
+ const conversationText = messages.map((m) => {
2262
+ let content = "";
2263
+ if (typeof m.content === "string") content = m.content;
2264
+ else if (Array.isArray(m.content)) {
2265
+ content = m.content.map((c) => c.text || JSON.stringify(c)).join(" ");
2243
2266
  }
2244
- const mergeSysText = "You merge partial summaries into a single cohesive, non-redundant summary that preserves key facts and decisions. Be concise.";
2245
- const mergeSys = systemMessage(mergeSysText);
2246
- const mergeIteratively = async (texts) => {
2247
- if (texts.length === 0) return "";
2248
- if (texts.length === 1) return texts[0];
2249
- const groups = [];
2250
- let group = [];
2251
- let toks = countApproxTokens(mergeSysText);
2252
- for (const t of texts) {
2253
- const tt = `- ${t}`;
2254
- const tk = countApproxTokens(tt);
2255
- if (toks + tk > perCallBudget && group.length > 0) {
2256
- groups.push(group);
2257
- group = [];
2258
- toks = countApproxTokens(mergeSysText);
2259
- }
2260
- group.push(tt);
2261
- toks += tk;
2262
- }
2263
- if (group.length > 0) groups.push(group);
2264
- const next = [];
2265
- for (const g of groups) {
2266
- const mergePrompt = humanMessage(`Merge the following partial summaries into a single concise brief. Avoid duplication, keep key facts and decisions.
2267
+ let prefix = `${m.role.toUpperCase()}`;
2268
+ if (m.name) prefix += ` (${m.name})`;
2269
+ return `${prefix}: ${content}`;
2270
+ }).join("\n\n");
2271
+ const summaryPrompt = [
2272
+ systemMessage("You are a helpful assistant that summarizes conversation history efficiently."),
2273
+ humanMessage(
2274
+ `Please summarize the following conversation history.
2275
+ Focus on:
2276
+ - User goals and intent.
2277
+ - Key decisions made and actions taken.
2278
+ - Important tool outputs and data retrieved.
2279
+ - Current state of the task.
2267
2280
 
2268
- ${g.join("\n")}
2281
+ Conversation:
2282
+ ${conversationText}
2269
2283
 
2270
- Merged summary:`);
2271
- try {
2272
- const r = await (model?.invoke ? model.invoke([mergeSys, mergePrompt]) : null);
2273
- const cc = r?.content;
2274
- const txt = typeof cc === "string" ? cc : Array.isArray(cc) ? cc.map((p) => typeof p === "string" ? p : p?.text ?? p?.content ?? "").join("") : "";
2275
- next.push((txt || "").trim());
2276
- } catch (e) {
2277
- console.error("Merge summarization failed:", e);
2278
- next.push(g.join(" "));
2279
- }
2280
- }
2281
- return mergeIteratively(next);
2282
- };
2283
- summaryText = await mergeIteratively(partials);
2284
- if (!summaryText) summaryText = "Context summarized.";
2285
- } catch (e) {
2286
- console.error("Error during context summarization:", e);
2287
- summaryText = "Context summarized.";
2284
+ Summary:`
2285
+ )
2286
+ ];
2287
+ let summaryText = "Summary unavailable.";
2288
+ try {
2289
+ const response = await model.invoke(summaryPrompt);
2290
+ summaryText = typeof response.content === "string" ? response.content : Array.isArray(response.content) ? response.content.map((c) => c.text || "").join("") : JSON.stringify(response.content);
2291
+ } catch (err) {
2292
+ console.error("[ContextSummarize] Failed to generate summary:", err);
2293
+ return {};
2288
2294
  }
2289
- const rewritten = [];
2290
- const alreadySummarized = (content) => {
2291
- const text = typeof content === "string" ? content : Array.isArray(content) ? content.map((p) => typeof p === "string" ? p : p?.text ?? p?.content ?? "").join("") : String(content ?? "");
2292
- return /\bSUMMARIZED\b/.test(text);
2293
- };
2294
- for (const m of messages) {
2295
- if (m.getType() === "tool") {
2296
- const toolMessageObj = m;
2297
- if (alreadySummarized(toolMessageObj.content) || toolMessageObj.name === "context_summarize" || toolMessageObj.name === "manage_todo_list") {
2298
- rewritten.push(m);
2299
- continue;
2300
- }
2301
- const callId = toolMessageObj.tool_call_id;
2302
- const safeId = callId || `summarized_${nanoid.nanoid(6)}`;
2303
- let hist = callId ? byCallId.get(callId) : void 0;
2304
- if (!hist) {
2305
- hist = {
2306
- executionId: nanoid.nanoid(),
2307
- toolName: toolMessageObj.name || "unknown_tool",
2308
- args: null,
2309
- output: typeof toolMessageObj.content === "string" ? toolMessageObj.content : Array.isArray(toolMessageObj.content) ? toolMessageObj.content.map((p) => typeof p === "string" ? p : p?.text ?? p?.content ?? "").join("") : String(toolMessageObj.content ?? ""),
2310
- rawOutput: void 0,
2311
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2312
- summarized: true,
2313
- messageId: void 0,
2314
- tool_call_id: callId,
2315
- fromCache: false
2316
- };
2317
- if (callId) byCallId.set(callId, hist);
2318
- archived.push({ ...hist });
2319
- }
2320
- if (!hist.executionId) hist.executionId = nanoid.nanoid();
2321
- hist.summarized = true;
2322
- rewritten.push({
2323
- role: "tool",
2324
- content: `SUMMARIZED executionId:'${hist.executionId}'`,
2325
- tool_call_id: safeId,
2326
- name: toolMessage.name
2327
- });
2328
- continue;
2295
+ const newMessages = messages.map((m) => {
2296
+ if (m.role === "tool") {
2297
+ if (m.content === "SUMMARIZED") return m;
2298
+ return {
2299
+ ...m,
2300
+ content: "SUMMARIZED"
2301
+ };
2329
2302
  }
2330
- rewritten.push(m);
2331
- }
2332
- const summarizeCallId = `summarize_${nanoid.nanoid(6)}`;
2333
- const summarizeArgs = { reason: "Exceeded token limit: summarize prior tool outputs for context." };
2334
- const syntheticAssistant = {
2303
+ return m;
2304
+ });
2305
+ const summaryToolCallId = `call_summary_${Date.now()}`;
2306
+ const assistantSummaryCall = {
2335
2307
  role: "assistant",
2336
- content: "",
2337
- additional_kwargs: {
2338
- tool_calls: [
2339
- { id: summarizeCallId, type: "function", function: { name: "context_summarize", arguments: JSON.stringify(summarizeArgs) } }
2340
- ]
2341
- }
2308
+ // "assistant"
2309
+ content: "Context limit reached. Summarizing conversation history to reduce token usage.",
2310
+ tool_calls: [
2311
+ {
2312
+ id: summaryToolCallId,
2313
+ type: "function",
2314
+ function: {
2315
+ name: "summarize_context",
2316
+ arguments: "{}"
2317
+ }
2318
+ }
2319
+ ]
2342
2320
  };
2343
- const syntheticToolResp = {
2321
+ const toolSummaryResponse = {
2344
2322
  role: "tool",
2345
- content: `${summaryText}`,
2346
- tool_call_id: summarizeCallId,
2347
- name: "context_summarize"
2323
+ tool_call_id: summaryToolCallId,
2324
+ name: "summarize_context",
2325
+ // Matches the tool name in the call
2326
+ content: summaryText
2348
2327
  };
2349
- for (const item of live) item.summarized = true;
2350
- const merged = [...archived, ...live];
2351
- const dedupMap = /* @__PURE__ */ new Map();
2352
- for (const it of merged) {
2353
- const key = it.tool_call_id || `${it.toolName}:${it.timestamp}`;
2354
- dedupMap.set(key, { ...it });
2355
- }
2356
- const newArchived = Array.from(dedupMap.values());
2357
- const onEvent = state.ctx?.__onEvent;
2358
- onEvent?.({ type: "summarization", summary: summaryText, archivedCount: newArchived.length });
2359
2328
  return {
2360
- messages: [...rewritten, syntheticAssistant, syntheticToolResp],
2361
- toolHistoryArchived: newArchived,
2362
- toolHistory: [],
2363
- ctx: { ...state.ctx || {}, __contextSummarized: true }
2329
+ messages: [...newMessages, assistantSummaryCall, toolSummaryResponse]
2364
2330
  };
2365
2331
  };
2366
2332
  }
@@ -2454,13 +2420,22 @@ function createContextTools(stateRef, opts) {
2454
2420
 
2455
2421
  // src/graph/decisions.ts
2456
2422
  function needsSummarization(state, opts, summarizationEnabled) {
2457
- const maxTok = opts.limits?.maxToken;
2458
- if (!maxTok || !summarizationEnabled) return false;
2423
+ if (!summarizationEnabled) {
2424
+ return false;
2425
+ }
2426
+ let maxTok;
2427
+ if (typeof opts.summarization === "object" && typeof opts.summarization.maxTokens === "number") {
2428
+ maxTok = opts.summarization.maxTokens;
2429
+ }
2430
+ if (maxTok === void 0) {
2431
+ maxTok = 5e4;
2432
+ }
2459
2433
  try {
2460
2434
  const allText = (state.messages || []).map((m) => typeof m.content === "string" ? m.content : Array.isArray(m.content) ? m.content.map((c) => typeof c === "string" ? c : c?.text ?? c?.content ?? "").join("") : "").join("\n");
2461
2435
  const tokenCount = countApproxTokens(allText);
2462
- return tokenCount > maxTok;
2463
- } catch {
2436
+ const needsSum = tokenCount > maxTok;
2437
+ return needsSum;
2438
+ } catch (e) {
2464
2439
  return false;
2465
2440
  }
2466
2441
  }
@@ -2485,7 +2460,12 @@ function createSmartAgent(opts) {
2485
2460
  const contextTools = createContextTools(stateRef, { planningEnabled, outputSchema: void 0 });
2486
2461
  const mergedTools = [...opts.tools ?? [], ...contextTools];
2487
2462
  const base = createAgent({ ...opts, tools: mergedTools });
2488
- const summarizationEnabled = opts.summarization !== false;
2463
+ let summarizationEnabled = false;
2464
+ if (typeof opts.summarization === "object") {
2465
+ summarizationEnabled = opts.summarization.enable !== false;
2466
+ } else {
2467
+ summarizationEnabled = opts.summarization !== false;
2468
+ }
2489
2469
  const summarizer = summarizationEnabled ? createContextSummarizeNode(opts) : void 0;
2490
2470
  const decideBefore = resolverDecisionFactory(opts, summarizationEnabled);
2491
2471
  const decideAfter = toolsDecisionFactory(opts, summarizationEnabled);
@@ -2516,19 +2496,33 @@ function createSmartAgent(opts) {
2516
2496
  const next = summarizationEnabled ? decideBefore(state) : "agent";
2517
2497
  if (next === "contextSummarize" && summarizer) {
2518
2498
  const delta = await summarizer(state);
2519
- state = { ...state, ...delta };
2520
- continue;
2499
+ if (!delta || Object.keys(delta).length === 0) ; else {
2500
+ state = { ...state, ...delta };
2501
+ continue;
2502
+ }
2521
2503
  }
2522
2504
  const res = await base.invoke(state, config);
2523
2505
  lastResult = res;
2524
2506
  state = res.state || { ...state, messages: res.messages };
2507
+ if (state.ctx?.__needsSummarization && summarizer) {
2508
+ const ctx = { ...state.ctx || {} };
2509
+ delete ctx.__needsSummarization;
2510
+ state = { ...state, ctx };
2511
+ const delta = await summarizer(state);
2512
+ if (delta && Object.keys(delta).length > 0) {
2513
+ state = { ...state, ...delta };
2514
+ }
2515
+ continue;
2516
+ }
2525
2517
  if (state.ctx?.__finalizedDueToStructuredOutput) break;
2526
2518
  if (summarizationEnabled) {
2527
2519
  const after = decideAfter(state);
2528
2520
  if (after === "contextSummarize" && summarizer) {
2529
2521
  const delta = await summarizer(state);
2530
- state = { ...state, ...delta };
2531
- continue;
2522
+ if (delta && Object.keys(delta).length > 0) {
2523
+ state = { ...state, ...delta };
2524
+ continue;
2525
+ }
2532
2526
  }
2533
2527
  }
2534
2528
  break;