@cognipeer/agent-sdk 0.5.2 → 0.5.4

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
@@ -136,7 +136,16 @@ const agent = createSmartAgent({
136
136
  memory: { provider: "inMemory", scope: "session", writePolicy: "auto_important" },
137
137
  summarization: { summaryTriggerTokens: 8000, summaryMode: "incremental" },
138
138
  context: { policy: "hybrid", lastTurnsToKeep: 8 },
139
- toolResponses: { maxToolResponseChars: 4000, maxToolResponseTokens: 1200 },
139
+ toolResponses: {
140
+ smallResponseChars: 1200,
141
+ smallResponsePolicy: "keep_full",
142
+ defaultPolicy: "keep_structured",
143
+ largeResponsePolicy: "summarize_archive",
144
+ fallbackPolicy: "keep_full",
145
+ keepRecentFullCount: 2,
146
+ maxToolResponseChars: 4000,
147
+ maxToolResponseTokens: 1200,
148
+ },
140
149
  limits: { maxToolCalls: 5, maxContextTokens: 12000 },
141
150
  tracing: { enabled: true },
142
151
  });
@@ -149,6 +158,16 @@ const result = await agent.invoke({
149
158
  console.log(result.content);
150
159
  ```
151
160
 
161
+ Tool-response retention is size-aware and composable:
162
+
163
+ - `smallResponseChars` + `smallResponsePolicy` define how very small payloads are kept.
164
+ - `defaultPolicy` handles normal informative outputs.
165
+ - `largeResponsePolicy` handles verbose outputs that cross the max char/token thresholds.
166
+ - `fallbackPolicy` applies to critical tools and now defaults to `keep_full` so safety-critical payloads are not collapsed accidentally.
167
+ - `keepRecentFullCount` pins the last N successful tool responses at full content during context assembly and excludes them from summarization passes.
168
+
169
+ Resolution order is: per-tool override, then critical fallback, then large/small classification, then the default policy. Redundant payloads like `ok`, `true`, and `null` are dropped automatically.
170
+
152
171
  The smart wrapper now supports runtime presets (`fast`, `balanced`, `deep`, `research`), custom profiles layered on top of a base preset, structured summarization, hybrid context compaction, configurable tool-response retention, in-memory fact storage, delegation limits, and an eval harness via `runSmartAgentEvalHarness(...)`.
153
172
 
154
173
  You can also define a custom profile by extending a built-in preset and overriding only the knobs you need:
@@ -201,6 +220,7 @@ console.log(res.content);
201
220
  - **Native provider layer** – call OpenAI, Anthropic, Azure, Bedrock, Vertex, and any OpenAI-compatible API directly. No LangChain required. Unified `ChatCompletionRequest` / `ChatCompletionResponse` schema with per-provider wire format conversion.
202
221
  - **Full token tracking** – every response surfaces `inputTokens`, `outputTokens`, `cachedInputTokens`, `cachedWriteTokens`, and `reasoningTokens` for all six providers.
203
222
  - **Summarization pipeline** – automatic chunking keeps tool call history within `contextTokenLimit` / `summaryTokenLimit`, archiving originals so `get_tool_response` can fetch them later.
223
+ - **Retention controls** – tool outputs can be kept full, reduced to structured previews, archived, or dropped based on size tiers, critical-tool fallback, per-tool overrides, and recent-response pinning.
204
224
  - **Planning discipline** – when planning is enabled the system prompt distinguishes full plan writes from incremental plan updates and emits `plan` events as todos change.
205
225
  - **Structured output** – supply `outputSchema` and the framework adds a hidden `response` finalize tool; parsed JSON is returned as `result.output`.
206
226
  - **Multi-agent orchestration** – reuse agents via `agent.asTool({ toolName })` or perform handoffs that swap runtimes mid-execution.
package/dist/index.cjs CHANGED
@@ -2138,13 +2138,16 @@ var BASE_DEFAULTS = {
2138
2138
  toolResponses: {
2139
2139
  maxToolResponseChars: 3600,
2140
2140
  maxToolResponseTokens: 1200,
2141
+ smallResponseChars: 0,
2142
+ smallResponsePolicy: "keep_full",
2141
2143
  defaultPolicy: "keep_structured",
2142
2144
  largeResponsePolicy: "summarize_archive",
2143
2145
  toolResponseRetentionByTool: {},
2144
2146
  criticalTools: [...DEFAULT_CRITICAL_TOOLS],
2145
2147
  schemaValidation: "strict",
2146
2148
  retryOnSchemaError: true,
2147
- fallbackPolicy: "keep_structured"
2149
+ fallbackPolicy: "keep_full",
2150
+ keepRecentFullCount: 0
2148
2151
  }
2149
2152
  };
2150
2153
  function buildProfile(overrides) {
@@ -2427,11 +2430,15 @@ function classifyToolResponse(toolName, output, config) {
2427
2430
  const tokenCount = countApproxTokens(serialized);
2428
2431
  const criticalTools = new Set(config.toolResponses.criticalTools);
2429
2432
  if (criticalTools.has(toolName)) return "critical";
2433
+ if (serialized === "ok" || serialized === "true" || serialized === "null") {
2434
+ return "redundant";
2435
+ }
2430
2436
  if (tokenCount >= config.toolResponses.maxToolResponseTokens || serialized.length >= config.toolResponses.maxToolResponseChars) {
2431
2437
  return "verbose";
2432
2438
  }
2433
- if (serialized === "ok" || serialized === "true" || serialized === "null") {
2434
- return "redundant";
2439
+ const smallChars = config.toolResponses.smallResponseChars;
2440
+ if (smallChars > 0 && serialized.length <= smallChars) {
2441
+ return "small";
2435
2442
  }
2436
2443
  return "informative";
2437
2444
  }
@@ -2441,12 +2448,14 @@ function resolveToolResponsePolicy(toolName, output, config) {
2441
2448
  const classification = classifyToolResponse(toolName, output, config);
2442
2449
  const byTool = config.toolResponses.toolResponseRetentionByTool[toolName];
2443
2450
  let retentionPolicy = byTool || config.toolResponses.defaultPolicy;
2444
- if (classification === "critical") {
2445
- retentionPolicy = byTool || config.toolResponses.fallbackPolicy;
2451
+ if (byTool) ; else if (classification === "critical") {
2452
+ retentionPolicy = config.toolResponses.fallbackPolicy;
2446
2453
  } else if (classification === "verbose") {
2447
- retentionPolicy = byTool || config.toolResponses.largeResponsePolicy;
2454
+ retentionPolicy = config.toolResponses.largeResponsePolicy;
2455
+ } else if (classification === "small") {
2456
+ retentionPolicy = config.toolResponses.smallResponsePolicy;
2448
2457
  } else if (classification === "redundant") {
2449
- retentionPolicy = byTool || "drop";
2458
+ retentionPolicy = "drop";
2450
2459
  }
2451
2460
  const summary = summarizeObject(output);
2452
2461
  if (retentionPolicy === "keep_full") {
@@ -2792,10 +2801,10 @@ function createToolsNode(initialTools, opts) {
2792
2801
  let toolMessageContent = responsePolicy.content;
2793
2802
  if (responsePolicy.retentionPolicy === "summarize_archive") {
2794
2803
  toolMessageContent = `ARCHIVED_TOOL_RESPONSE [executionId=${executionId}]: ${responsePolicy.summary}
2795
- Use get_tool_response with executionId "${executionId}" to retrieve the full output.`;
2804
+ Only use get_tool_response with executionId "${executionId}" if this summary is missing a specific field, id, snippet, or exact payload you still need.`;
2796
2805
  } else if (responsePolicy.retentionPolicy === "drop") {
2797
2806
  toolMessageContent = `DROPPED_TOOL_RESPONSE [executionId=${executionId}]: ${responsePolicy.summary}
2798
- Use get_tool_response with executionId "${executionId}" to retrieve the full output.`;
2807
+ Only use get_tool_response with executionId "${executionId}" if this summary is missing a specific field, id, snippet, or exact payload you still need.`;
2799
2808
  }
2800
2809
  appended.push({ role: "tool", content: toolMessageContent, tool_call_id: tc.id || `${tc.name}_${appended.length}`, name: tc.name });
2801
2810
  onEvent?.({ type: "tool_call", phase: "success", name: toolName, id: tc.id, args, result: output, durationMs });
@@ -3981,6 +3990,40 @@ function createAgentNode(opts) {
3981
3990
  }
3982
3991
 
3983
3992
  // src/smart/contextPolicy.ts
3993
+ function collectRecentFullToolCallIds(state, keepRecentFullCount) {
3994
+ if (!keepRecentFullCount || keepRecentFullCount <= 0) return /* @__PURE__ */ new Set();
3995
+ const history = state.toolHistory || [];
3996
+ const ids = [];
3997
+ for (let index = history.length - 1; index >= 0 && ids.length < keepRecentFullCount; index -= 1) {
3998
+ const entry = history[index];
3999
+ if (!entry || !entry.tool_call_id) continue;
4000
+ if (entry.status && entry.status !== "success") continue;
4001
+ ids.push(entry.tool_call_id);
4002
+ }
4003
+ return new Set(ids);
4004
+ }
4005
+ function applyRecentFullOverride(messages, state, config) {
4006
+ const keepN = config.toolResponses.keepRecentFullCount;
4007
+ if (!keepN || keepN <= 0) return messages;
4008
+ const recentIds = collectRecentFullToolCallIds(state, keepN);
4009
+ if (recentIds.size === 0) return messages;
4010
+ const history = state.toolHistory || [];
4011
+ const byCallId = /* @__PURE__ */ new Map();
4012
+ for (const entry of history) {
4013
+ if (entry?.tool_call_id) byCallId.set(entry.tool_call_id, entry);
4014
+ }
4015
+ return messages.map((message) => {
4016
+ if (message.role !== "tool" || !message.tool_call_id) return message;
4017
+ if (!recentIds.has(message.tool_call_id)) return message;
4018
+ const entry = byCallId.get(message.tool_call_id);
4019
+ if (!entry) return message;
4020
+ const raw = entry.rawOutput ?? entry.output;
4021
+ if (raw === void 0) return message;
4022
+ const fullContent = typeof raw === "string" ? raw : safeStringify(raw);
4023
+ if (!fullContent || fullContent === message.content) return message;
4024
+ return { ...message, content: fullContent };
4025
+ });
4026
+ }
3984
4027
  function collectRecentTurns(messages, lastTurnsToKeep) {
3985
4028
  if (lastTurnsToKeep <= 0) return [];
3986
4029
  const systemPrefix = messages.filter((message, index) => index === 0 && message.role === "system");
@@ -4081,7 +4124,7 @@ function clampToBudget(messages, maxContextTokens) {
4081
4124
  function buildModelMessages(state, config) {
4082
4125
  const rawMessages = state.messages || [];
4083
4126
  if (config.context.policy === "raw") {
4084
- return clampToBudget(rawMessages, config.limits.maxContextTokens);
4127
+ return clampToBudget(applyRecentFullOverride(rawMessages, state, config), config.limits.maxContextTokens);
4085
4128
  }
4086
4129
  const recentMessages = collectRecentTurns(rawMessages, config.context.lastTurnsToKeep);
4087
4130
  const latestSummary = state.summaryRecords?.[state.summaryRecords.length - 1];
@@ -4101,7 +4144,7 @@ function buildModelMessages(state, config) {
4101
4144
  ...syntheticContextMessages,
4102
4145
  ...config.context.policy === "summary_only" ? body.filter((message) => message.role === "user" || message.role === "assistant").slice(-2) : body
4103
4146
  ];
4104
- return clampToBudget(assembled, config.limits.maxContextTokens);
4147
+ return clampToBudget(applyRecentFullOverride(assembled, state, config), config.limits.maxContextTokens);
4105
4148
  }
4106
4149
 
4107
4150
  // src/nodes/contextSummarize.ts
@@ -4257,11 +4300,11 @@ function buildSummarizedToolMessage(message, state) {
4257
4300
  toolCallId ? `toolCallId=${toolCallId}` : null,
4258
4301
  executionId ? `executionId=${executionId}` : null
4259
4302
  ].filter(Boolean).join("; ");
4260
- let retrievalHint = "Use get_tool_response if exact details are still needed.";
4303
+ let retrievalHint = "Only use get_tool_response if this summary is missing a specific field, id, snippet, or exact payload you still need.";
4261
4304
  if (executionId && toolCallId) {
4262
- retrievalHint = `Use get_tool_response with executionId "${executionId}" or tool call id "${toolCallId}" if exact details are still needed.`;
4305
+ retrievalHint = `Only use get_tool_response with executionId "${executionId}" or tool call id "${toolCallId}" if this summary is missing a specific field, id, snippet, or exact payload you still need.`;
4263
4306
  } else if (executionId || toolCallId) {
4264
- retrievalHint = `Use get_tool_response with executionId "${executionId || toolCallId}" if exact details are still needed.`;
4307
+ retrievalHint = `Only use get_tool_response with executionId "${executionId || toolCallId}" if this summary is missing a specific field, id, snippet, or exact payload you still need.`;
4265
4308
  }
4266
4309
  return [
4267
4310
  `SUMMARIZED_TOOL_RESPONSE [${references}]`,
@@ -4327,11 +4370,13 @@ function createContextSummarizeNode(opts) {
4327
4370
  const onEvent = state.ctx?.__onEvent;
4328
4371
  const traceSession = state.ctx?.__traceSession;
4329
4372
  const toolResponsesConfigEarly = resolved.toolResponses;
4373
+ const recentFullToolCallIdsEarly = collectRecentFullToolCallIds(state, toolResponsesConfigEarly.keepRecentFullCount);
4330
4374
  const compressableMessages = messages.filter(
4331
4375
  (m) => {
4332
4376
  if (m.role !== "tool") return false;
4333
4377
  if (isSummarizedToolPlaceholder(m.content)) return false;
4334
4378
  if (isSyntheticSummaryMessage(m)) return false;
4379
+ if (m.tool_call_id && recentFullToolCallIdsEarly.has(m.tool_call_id)) return false;
4335
4380
  const histEntry = findToolHistoryEntry(state, m.tool_call_id, m.name);
4336
4381
  if (histEntry?.retentionPolicy === "keep_full") return false;
4337
4382
  if (!histEntry?.retentionPolicy) {
@@ -4518,6 +4563,7 @@ ${canonicalFacts.length > 0 ? canonicalFacts.map((fact) => `- ${fact.key}: ${fac
4518
4563
  }
4519
4564
  }
4520
4565
  const toolResponsesConfig = resolved.toolResponses;
4566
+ const recentFullToolCallIds = collectRecentFullToolCallIds(state, toolResponsesConfig.keepRecentFullCount);
4521
4567
  const newMessages = messages.map((m) => {
4522
4568
  if (m.role === "tool") {
4523
4569
  if (isSyntheticSummaryMessage(m)) {
@@ -4529,6 +4575,7 @@ ${canonicalFacts.length > 0 ? canonicalFacts.map((fact) => `- ${fact.key}: ${fac
4529
4575
  }
4530
4576
  if (isSummarizedToolPlaceholder(m.content)) return m;
4531
4577
  if (toolCallId && protectedToolCallIds.has(toolCallId)) return m;
4578
+ if (toolCallId && recentFullToolCallIds.has(toolCallId)) return m;
4532
4579
  const toolName = m.name || "";
4533
4580
  const historyEntry = findToolHistoryEntry(state, toolCallId, toolName);
4534
4581
  const entryRetention = historyEntry?.retentionPolicy;
@@ -4867,8 +4914,9 @@ function createContextTools(stateRef, opts) {
4867
4914
  }
4868
4915
  const getTool = createTool({
4869
4916
  name: "get_tool_response",
4870
- description: "RETRIEVE the full output of a tool execution whose response was archived, dropped, or later compacted in conversation history. When a tool response appears as 'ARCHIVED_TOOL_RESPONSE [executionId=xxx]', 'DROPPED_TOOL_RESPONSE [executionId=xxx]', or 'SUMMARIZED_TOOL_RESPONSE [...]', pass the referenced executionId to this tool to get the complete data. You can also pass the original tool_call_id from the tool call.",
4917
+ description: "RETRIEVE the full output of a tool execution whose response was archived, dropped, or later compacted in conversation history. Use this only when the preview or summary is missing a specific field, identifier, snippet, or exact payload you still need to finish the task. Do not call it reflexively for every archived response. When a tool response appears as 'ARCHIVED_TOOL_RESPONSE [executionId=xxx]', 'DROPPED_TOOL_RESPONSE [executionId=xxx]', or 'SUMMARIZED_TOOL_RESPONSE [...]', pass the referenced executionId to this tool to get the complete data. You can also pass the original tool_call_id from the tool call.",
4871
4918
  schema: zod.z.object({ executionId: zod.z.string().describe("Tool execution id or original tool_call_id") }),
4919
+ maxExecutionsPerRun: 8,
4872
4920
  func: async ({ executionId }) => {
4873
4921
  const matchesExecution = (t) => t?.executionId === executionId || t?.tool_call_id === executionId;
4874
4922
  let execution = stateRef.toolHistory?.find((t) => matchesExecution(t));
@@ -7124,23 +7172,79 @@ function convertContent(content) {
7124
7172
  function isZodSchema(obj) {
7125
7173
  return obj != null && typeof obj === "object" && typeof obj.safeParse === "function" && "_def" in obj;
7126
7174
  }
7175
+ function hasStrictUnsafeShape(node, inProperty = false) {
7176
+ if (!node || typeof node !== "object") return false;
7177
+ if (Array.isArray(node)) {
7178
+ return node.some((item) => hasStrictUnsafeShape(item, inProperty));
7179
+ }
7180
+ if (typeof node.$ref === "string") {
7181
+ return true;
7182
+ }
7183
+ const hasType = typeof node.type === "string" || Array.isArray(node.type) && node.type.length > 0;
7184
+ const hasProperties = !!node.properties && typeof node.properties === "object" && !Array.isArray(node.properties);
7185
+ const hasItems = node.items !== void 0;
7186
+ const hasComposite = ["anyOf", "oneOf", "allOf"].some((key) => Array.isArray(node[key]) && node[key].length > 0);
7187
+ if (inProperty && !hasType && !hasProperties && !hasItems) {
7188
+ return true;
7189
+ }
7190
+ if (inProperty && hasComposite && !hasType) {
7191
+ return true;
7192
+ }
7193
+ if (hasProperties) {
7194
+ for (const value of Object.values(node.properties)) {
7195
+ if (hasStrictUnsafeShape(value, true)) {
7196
+ return true;
7197
+ }
7198
+ }
7199
+ }
7200
+ if (node.items && hasStrictUnsafeShape(node.items, false)) {
7201
+ return true;
7202
+ }
7203
+ for (const key of ["anyOf", "oneOf", "allOf"]) {
7204
+ if (Array.isArray(node[key]) && hasStrictUnsafeShape(node[key], false)) {
7205
+ return true;
7206
+ }
7207
+ }
7208
+ for (const key of ["definitions", "$defs"]) {
7209
+ const defs = node[key];
7210
+ if (defs && typeof defs === "object") {
7211
+ for (const value of Object.values(defs)) {
7212
+ if (hasStrictUnsafeShape(value, false)) {
7213
+ return true;
7214
+ }
7215
+ }
7216
+ }
7217
+ }
7218
+ return false;
7219
+ }
7220
+ function canUseStrictToolSchema(schema) {
7221
+ if (!schema || typeof schema !== "object") {
7222
+ return true;
7223
+ }
7224
+ if (isZodSchema(schema)) {
7225
+ return true;
7226
+ }
7227
+ return !hasStrictUnsafeShape(schema);
7228
+ }
7127
7229
  function convertToJsonSchema(schema, strict = false) {
7128
7230
  if (!schema || typeof schema !== "object") {
7129
7231
  return { type: "object", properties: {}, required: [], additionalProperties: false };
7130
7232
  }
7131
7233
  if (isZodSchema(schema)) {
7132
- const converted = zodToJsonSchema.zodToJsonSchema(schema, {
7234
+ const converted2 = zodToJsonSchema.zodToJsonSchema(schema, {
7133
7235
  $refStrategy: "none",
7134
7236
  ...strict ? { openaiStrictMode: true } : {}
7135
7237
  });
7136
- delete converted["$schema"];
7137
- if (!converted.type) converted.type = "object";
7138
- return converted;
7238
+ delete converted2["$schema"];
7239
+ if (!converted2.type) converted2.type = "object";
7240
+ return converted2;
7139
7241
  }
7140
- if (!schema.type && schema.properties) {
7141
- return { type: "object", ...schema };
7242
+ const converted = { ...schema };
7243
+ delete converted["$schema"];
7244
+ if (!converted.type && converted.properties) {
7245
+ return { type: "object", ...converted };
7142
7246
  }
7143
- return schema;
7247
+ return converted;
7144
7248
  }
7145
7249
  function normalizeStrictSchema(schema) {
7146
7250
  if (!schema || typeof schema !== "object") return schema;
@@ -7189,20 +7293,25 @@ function toToolDefinition(tool, strict) {
7189
7293
  let name;
7190
7294
  let description;
7191
7295
  let parameters;
7296
+ let rawSchema;
7297
+ let useStrict = strict ?? false;
7192
7298
  if (tool.type === "function" && tool.function) {
7193
7299
  name = tool.function.name;
7194
7300
  description = tool.function.description ?? "";
7195
- parameters = tool.function.parameters ?? { type: "object", properties: {}, additionalProperties: false };
7301
+ rawSchema = tool.function.parameters ?? { type: "object", properties: {}, additionalProperties: false };
7196
7302
  } else {
7197
7303
  name = tool.name ?? "unknown";
7198
7304
  description = tool.description ?? "";
7199
- const rawSchema = tool.schema ?? tool.parameters;
7200
- parameters = convertToJsonSchema(rawSchema, strict);
7305
+ rawSchema = tool.schema ?? tool.parameters;
7306
+ }
7307
+ if (useStrict && !canUseStrictToolSchema(rawSchema)) {
7308
+ useStrict = false;
7201
7309
  }
7202
- if (strict) {
7310
+ parameters = convertToJsonSchema(rawSchema, useStrict);
7311
+ if (useStrict) {
7203
7312
  parameters = normalizeStrictSchema(parameters);
7204
7313
  }
7205
- return { name, description, parameters, ...strict ? { strict: true } : {} };
7314
+ return { name, description, parameters, ...useStrict ? { strict: true } : {} };
7206
7315
  }
7207
7316
  function toBaseChatMessage(response) {
7208
7317
  const msg = {