From 33489e26e0d1092647ca2f864e765f6452770506 Mon Sep 17 00:00:00 2001 From: ChethanUK Date: Mon, 27 Jul 2026 22:57:06 +0200 Subject: [PATCH] feat(agent-sdk): let agent_knowledge_recall request source chunks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The recall API already supports `include: {chunks}`, and the TypeScript client already exposes it as `includeChunks`/`maxChunkTokens`, but the agent tool forwarded only `{maxTokens, types}` — so an agent had no way to reach the raw source text a fact was extracted from, which is exactly what "what did we actually say" questions need. Add optional `include_chunks` / `max_chunk_tokens` parameters and pass them through. Both are additive and off by default, so recall responses are unchanged unless an agent asks for chunks. Fixes #2949 --- hindsight-docs/docs-integrations/openclaw.md | 2 +- .../hindsight-agent-sdk/src/index.ts | 21 +++++++ .../hindsight-agent-sdk/tests/index.test.ts | 57 +++++++++++++++++++ .../references/sdks/integrations/openclaw.md | 2 +- 4 files changed, 80 insertions(+), 2 deletions(-) diff --git a/hindsight-docs/docs-integrations/openclaw.md b/hindsight-docs/docs-integrations/openclaw.md index 1db3323af1..99135baebe 100644 --- a/hindsight-docs/docs-integrations/openclaw.md +++ b/hindsight-docs/docs-integrations/openclaw.md @@ -153,7 +153,7 @@ Optional settings in `~/.openclaw/openclaw.json`: - `enableKnowledgeTools` - Register `agent_knowledge_*` tools for explicit agent-driven lookup, reflection, ingest, and knowledge-page management (default: `false`). - `debug` - Enable debug logging (default: `false`). -When using `agent_knowledge_recall` manually, pass `max_tokens` to control how much memory text the recall response may contain. The tool has no `max_results` parameter — to cap the number of automatically injected memories, use `recallTopK` on auto-recall instead. +When using `agent_knowledge_recall` manually, pass `max_tokens` to control how much memory text the recall response may contain. The tool has no `max_results` parameter — to cap the number of automatically injected memories, use `recallTopK` on auto-recall instead. When the answer needs verbatim wording or an exact number rather than an extracted fact, pass `include_chunks: true` to also get the raw source text those memories came from, and `max_chunk_tokens` to bound it (default `8192`). When using `agent_knowledge_reflect`, keep the default conservative settings unless you intentionally need a deeper synthesis: `budget` defaults to `low`, `max_tokens` defaults to `1024`, and `fact_types` defaults to `world`, `experience`, and `observation`. Reflect calls can be more expensive than recall because they retrieve memories and then call the configured Reflect LLM to generate an answer. For production banks, set a finite bank-level `reflect_source_facts_max_tokens` value (for example `4096` or `8192`) instead of leaving it unlimited, so ad-hoc reflection cannot pull an unbounded amount of source facts into the LLM context. diff --git a/hindsight-tools/hindsight-agent-sdk/src/index.ts b/hindsight-tools/hindsight-agent-sdk/src/index.ts index 42f6098855..fee62d4adc 100644 --- a/hindsight-tools/hindsight-agent-sdk/src/index.ts +++ b/hindsight-tools/hindsight-agent-sdk/src/index.ts @@ -99,6 +99,14 @@ function parseRecallMaxTokens(input: unknown): number { return n; } +/** Undefined on absent/invalid input so the client's own chunk budget default applies. */ +function parseChunkMaxTokens(input: unknown): number | undefined { + if (input === undefined || input === null) return undefined; + const n = Math.floor(Number(input)); + if (!Number.isFinite(n) || n < 1) return undefined; + return n; +} + // ── Factory ──────────────────────────────────────────── /** @@ -261,6 +269,17 @@ export function createKnowledgeTools(opts: CreateKnowledgeToolsOptions): Knowled items: { type: "string", enum: ["world", "experience", "observation"] }, description: "Alias for fact_types.", }, + include_chunks: { + type: "boolean", + description: + "Also return the raw source text the memories were extracted from (default false). " + + "Use only when the answer needs verbatim wording, an exact quote, or an exact number " + + "that an extracted fact may have rounded or paraphrased — it costs significantly more tokens.", + }, + max_chunk_tokens: { + type: "number", + description: "Token budget for the returned source chunks (default 8192).", + }, }, required: ["query"], }, @@ -273,6 +292,8 @@ export function createKnowledgeTools(opts: CreateKnowledgeToolsOptions): Knowled const result = await client.recall(bankId, params.query as string, { maxTokens, types, + includeChunks: params.include_chunks === true, + maxChunkTokens: parseChunkMaxTokens(params.max_chunk_tokens), }); return ok(result); }, diff --git a/hindsight-tools/hindsight-agent-sdk/tests/index.test.ts b/hindsight-tools/hindsight-agent-sdk/tests/index.test.ts index d2950c7adf..0315777744 100644 --- a/hindsight-tools/hindsight-agent-sdk/tests/index.test.ts +++ b/hindsight-tools/hindsight-agent-sdk/tests/index.test.ts @@ -206,6 +206,63 @@ describe("createKnowledgeTools", () => { expect(body.max_tokens).toBe(512); }); + it("recall omits source chunks unless include_chunks is exactly true", async () => { + const tool = tools.find((t) => t.name === "agent_knowledge_recall")!; + + // Omitted, false, and a truthy-string "false" must all leave chunks off. + for (const params of [ + { query: "no chunks" }, + { query: "no chunks", include_chunks: false }, + { query: "no chunks", include_chunks: "false" }, + // A chunk budget alone must not switch chunks on. + { query: "no chunks", max_chunk_tokens: 2048 }, + ]) { + mockFetch.mockReset(); + mockFetch.mockReturnValueOnce(mockResponse({ results: [{ text: "found" }] })); + await tool.execute(params); + const body = await getBody(mockFetch.mock.calls[0]); + expect(body.include?.chunks, JSON.stringify(params)).toBeUndefined(); + } + }); + + it("recall include_chunks requests source chunks and returns them to the agent", async () => { + mockFetch.mockReturnValueOnce( + mockResponse({ + results: [{ text: "found" }], + chunks: { c1: { chunk_text: "the fee is 4.375%", truncated: false } }, + }) + ); + + const tool = tools.find((t) => t.name === "agent_knowledge_recall")!; + const result = await tool.execute({ query: "exact wording", include_chunks: true }); + + const body = await getBody(mockFetch.mock.calls[0]); + expect(body.include.chunks).toEqual({ max_tokens: 8192 }); + // The chunks must survive into the tool output, not just the request. + expect(JSON.parse(result.content[0].text).chunks.c1.chunk_text).toBe("the fee is 4.375%"); + }); + + it("recall falls back to the client's chunk budget on unusable max_chunk_tokens", async () => { + const tool = tools.find((t) => t.name === "agent_knowledge_recall")!; + + for (const [maxChunkTokens, expected] of [ + [2048, 2048], + [0, 8192], + [-5, 8192], + ["not-a-number", 8192], + ] as const) { + mockFetch.mockReset(); + mockFetch.mockReturnValueOnce(mockResponse({ results: [{ text: "found" }] })); + await tool.execute({ + query: "exact wording", + include_chunks: true, + max_chunk_tokens: maxChunkTokens, + }); + const body = await getBody(mockFetch.mock.calls[0]); + expect(body.include.chunks.max_tokens, String(maxChunkTokens)).toBe(expected); + } + }); + it("reflect sends POST with conservative defaults", async () => { mockFetch.mockReturnValueOnce(mockResponse({ text: "answer" })); diff --git a/skills/hindsight-docs/references/sdks/integrations/openclaw.md b/skills/hindsight-docs/references/sdks/integrations/openclaw.md index 63cbd4777b..7a18f8d120 100644 --- a/skills/hindsight-docs/references/sdks/integrations/openclaw.md +++ b/skills/hindsight-docs/references/sdks/integrations/openclaw.md @@ -148,7 +148,7 @@ Optional settings in `~/.openclaw/openclaw.json`: - `enableKnowledgeTools` - Register `agent_knowledge_*` tools for explicit agent-driven lookup, reflection, ingest, and knowledge-page management (default: `false`). - `debug` - Enable debug logging (default: `false`). -When using `agent_knowledge_recall` manually, pass `max_tokens` to control how much memory text the recall response may contain. The tool has no `max_results` parameter — to cap the number of automatically injected memories, use `recallTopK` on auto-recall instead. +When using `agent_knowledge_recall` manually, pass `max_tokens` to control how much memory text the recall response may contain. The tool has no `max_results` parameter — to cap the number of automatically injected memories, use `recallTopK` on auto-recall instead. When the answer needs verbatim wording or an exact number rather than an extracted fact, pass `include_chunks: true` to also get the raw source text those memories came from, and `max_chunk_tokens` to bound it (default `8192`). When using `agent_knowledge_reflect`, keep the default conservative settings unless you intentionally need a deeper synthesis: `budget` defaults to `low`, `max_tokens` defaults to `1024`, and `fact_types` defaults to `world`, `experience`, and `observation`. Reflect calls can be more expensive than recall because they retrieve memories and then call the configured Reflect LLM to generate an answer. For production banks, set a finite bank-level `reflect_source_facts_max_tokens` value (for example `4096` or `8192`) instead of leaving it unlimited, so ad-hoc reflection cannot pull an unbounded amount of source facts into the LLM context.