feat(agent-sdk): let agent_knowledge_recall request source chunks (#2995)
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
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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" }));
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user