mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
v0.41.33.0 feat(search): intent-aware adaptive return-sizing + agent-facing query param (#1640)
* feat(search): intent-aware adaptive return-sizing (default-off) New opt-in retrieval feature: trim the ranked result set to an intent-driven cap (entity -> tight, else -> recall-preserving) instead of always returning top-K. Pure module src/core/search/return-policy.ts (resolve + apply + config-read + at-least-minKeep failsafe); wired into hybridSearch after rerank, before slice, offset===0 only; decision stamped into HybridSearchMeta.adaptive_return; cache skipped when on (KNOBS_HASH fold is a follow-up). SearchOpts.adaptiveReturn per-call override. Default OFF — existing search behavior byte-identical. Mechanism is an intent cap, not a score-cliff detector (PrecisionMemBench data showed the cliff carries no signal). 19 unit tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v0.41.30.0) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: document adaptive return-sizing module in CLAUDE.md (v0.41.30.0) Add a Key Files entry for src/core/search/return-policy.ts (intent-aware adaptive return-sizing, default OFF) covering its exports, the four search.adaptive_return* config knobs, the hybrid.ts wiring (post-rerank, pre-slice, offset===0 only), and the cache-skip behavior. Regenerate llms-full.txt to match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(query): agent-facing adaptive_return param on the query op Expose adaptive_return (boolean) on the query MCP/CLI op so the AGENT — not the human config knob — decides per query whether to return a tight, intent-sized set. The param description teaches WHEN (single-answer questions on; breadth off; limit:1 for a hard single-answer cap), matching the salience/recency 'YOU (the agent) decide' pattern. Threaded into hybridSearchCached. End users never touch config; their agent serves them per query. Pinned by test/search/query-op-adaptive-return.test.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: re-version to v0.41.33.0 + document agent surface Re-version 0.41.30.0 -> 0.41.33.0 (VERSION/package.json/CHANGELOG/TODOS/CLAUDE.md). CHANGELOG + CLAUDE.md now document the agent-facing query-op adaptive_return param + when-to-use guidance. llms regenerated (build-llms test green). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(core): harden fence-strip + config parsing against non-string input stripTakesFence/stripFactsFence crashed with "undefined is not an object" when a read op returned a page with no compiled_truth (e.g. metadata-only rows). The get_page untrusted-reader path calls both on page.compiled_truth, which can be undefined. Guard both to no-op when body is not a string. loadSearchModeConfig.safeGet trusted engine.getConfig to honor its string|null contract; a non-string value (array/boolean) reached loadOverridesFromConfig and crashed on ce.toLowerCase(). Treat any non-string config value as "not set" so it falls through to the mode-bundle default, matching missing-key behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f79c1306a2
commit
730aed77f2
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Pins the agent-facing surface for adaptive return-sizing on the `query` op.
|
||||
*
|
||||
* The feature is useless to an agent unless the MCP/op tool schema exposes the
|
||||
* param AND its description teaches WHEN to reach for it. This test guards both
|
||||
* so a future refactor can't silently drop the agent instruction.
|
||||
*/
|
||||
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { operationsByName } from '../../src/core/operations.ts';
|
||||
|
||||
describe('query op — adaptive_return agent surface', () => {
|
||||
const query = operationsByName['query'];
|
||||
|
||||
test('query op exists', () => {
|
||||
expect(query).toBeDefined();
|
||||
});
|
||||
|
||||
test('adaptive_return is a boolean param on query', () => {
|
||||
const param = query.params?.adaptive_return as { type?: string; description?: string } | undefined;
|
||||
expect(param).toBeDefined();
|
||||
expect(param?.type).toBe('boolean');
|
||||
});
|
||||
|
||||
test('description teaches the agent WHEN to use it (single-answer vs breadth)', () => {
|
||||
const desc = (query.params?.adaptive_return as { description?: string })?.description ?? '';
|
||||
// Must instruct the agent on both directions of the decision.
|
||||
expect(desc.toLowerCase()).toContain('true when');
|
||||
expect(desc.toLowerCase()).toContain('breadth');
|
||||
// Must reassure the safety contract so the agent isn't afraid to use it.
|
||||
expect(desc.toLowerCase()).toContain('never returns empty');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Unit tests for intent-aware adaptive return-sizing (v0.42).
|
||||
* Pure logic — no engine, no LLM. Pins the failsafe (never-empty),
|
||||
* intent→cap mapping, config resolution precedence, and the off-by-default
|
||||
* contract.
|
||||
*/
|
||||
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import {
|
||||
DEFAULT_ADAPTIVE_RETURN,
|
||||
adaptiveReturnFromConfig,
|
||||
resolveAdaptiveReturn,
|
||||
adaptiveReturnEnabled,
|
||||
applyAdaptiveReturn,
|
||||
type AdaptiveReturnConfig,
|
||||
} from '../../src/core/search/return-policy.ts';
|
||||
|
||||
const items = (n: number): string[] => Array.from({ length: n }, (_, i) => `r${i}`);
|
||||
|
||||
describe('resolveAdaptiveReturn precedence', () => {
|
||||
test('default is OFF (no behavior change)', () => {
|
||||
expect(resolveAdaptiveReturn(undefined).enabled).toBe(false);
|
||||
expect(DEFAULT_ADAPTIVE_RETURN.enabled).toBe(false);
|
||||
});
|
||||
test('per-call true enables with default caps', () => {
|
||||
const c = resolveAdaptiveReturn(true);
|
||||
expect(c.enabled).toBe(true);
|
||||
expect(c.entityMax).toBe(DEFAULT_ADAPTIVE_RETURN.entityMax);
|
||||
});
|
||||
test('per-call false wins over config-enabled', () => {
|
||||
const c = resolveAdaptiveReturn(false, { enabled: true });
|
||||
expect(c.enabled).toBe(false);
|
||||
});
|
||||
test('per-call object overrides caps but inherits enabled from config when omitted', () => {
|
||||
const c = resolveAdaptiveReturn({ entityMax: 1 }, { enabled: true, otherMax: 9 });
|
||||
expect(c.enabled).toBe(true); // from config
|
||||
expect(c.entityMax).toBe(1); // per-call override
|
||||
expect(c.otherMax).toBe(9); // from config
|
||||
});
|
||||
test('config plane enables', () => {
|
||||
const c = resolveAdaptiveReturn(undefined, { enabled: true });
|
||||
expect(c.enabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('adaptiveReturnFromConfig', () => {
|
||||
test('reads nested search.* keys with clamping', () => {
|
||||
const c = adaptiveReturnFromConfig({
|
||||
search: {
|
||||
adaptive_return: true,
|
||||
adaptive_return_entity_max: 3,
|
||||
adaptive_return_other_max: 8,
|
||||
adaptive_return_min_keep: 2,
|
||||
},
|
||||
});
|
||||
expect(c).toEqual({ enabled: true, entityMax: 3, otherMax: 8, minKeep: 2 });
|
||||
});
|
||||
test('rejects sub-1 / non-numeric caps (falls back to defaults)', () => {
|
||||
const c = adaptiveReturnFromConfig({
|
||||
search: { adaptive_return_entity_max: 0, adaptive_return_other_max: 'x' },
|
||||
});
|
||||
expect(c.entityMax).toBe(DEFAULT_ADAPTIVE_RETURN.entityMax);
|
||||
expect(c.otherMax).toBe(DEFAULT_ADAPTIVE_RETURN.otherMax);
|
||||
});
|
||||
test('empty / null config → empty partial', () => {
|
||||
expect(adaptiveReturnFromConfig(null)).toEqual({});
|
||||
expect(adaptiveReturnFromConfig({})).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('adaptiveReturnEnabled', () => {
|
||||
test('true when per-call true', () => {
|
||||
expect(adaptiveReturnEnabled(true, null)).toBe(true);
|
||||
});
|
||||
test('true when config enables', () => {
|
||||
expect(adaptiveReturnEnabled(undefined, { search: { adaptive_return: true } })).toBe(true);
|
||||
});
|
||||
test('false by default', () => {
|
||||
expect(adaptiveReturnEnabled(undefined, null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
const ON = (over: Partial<AdaptiveReturnConfig> = {}): AdaptiveReturnConfig => ({
|
||||
...DEFAULT_ADAPTIVE_RETURN,
|
||||
enabled: true,
|
||||
...over,
|
||||
});
|
||||
|
||||
describe('applyAdaptiveReturn', () => {
|
||||
test('disabled → passthrough, decision.applied=false', () => {
|
||||
const { kept, decision } = applyAdaptiveReturn(items(20), 'entity', DEFAULT_ADAPTIVE_RETURN);
|
||||
expect(kept.length).toBe(20);
|
||||
expect(decision.applied).toBe(false);
|
||||
});
|
||||
test('entity intent uses entityMax', () => {
|
||||
const { kept, decision } = applyAdaptiveReturn(items(20), 'entity', ON({ entityMax: 2 }));
|
||||
expect(kept.length).toBe(2);
|
||||
expect(decision.cap).toBe(2);
|
||||
expect(decision.applied).toBe(true);
|
||||
});
|
||||
test('non-entity intent uses otherMax', () => {
|
||||
for (const intent of ['temporal', 'event', 'general'] as const) {
|
||||
const { kept } = applyAdaptiveReturn(items(20), intent, ON({ otherMax: 5 }));
|
||||
expect(kept.length).toBe(5);
|
||||
}
|
||||
});
|
||||
test('FAILSAFE: never empty when candidates exist (cap floored to minKeep)', () => {
|
||||
// minKeep defaults to 1; even a degenerate cap can't zero the result.
|
||||
const { kept } = applyAdaptiveReturn(items(10), 'entity', ON({ entityMax: 1, minKeep: 1 }));
|
||||
expect(kept.length).toBe(1);
|
||||
});
|
||||
test('empty input stays empty (no fabrication)', () => {
|
||||
const { kept, decision } = applyAdaptiveReturn([], 'entity', ON());
|
||||
expect(kept.length).toBe(0);
|
||||
expect(decision.applied).toBe(false);
|
||||
});
|
||||
test('cap larger than result set keeps all', () => {
|
||||
const { kept } = applyAdaptiveReturn(items(2), 'general', ON({ otherMax: 6 }));
|
||||
expect(kept.length).toBe(2);
|
||||
});
|
||||
test('preserves order (returns the ranked prefix)', () => {
|
||||
const { kept } = applyAdaptiveReturn(items(5), 'entity', ON({ entityMax: 3 }));
|
||||
expect(kept).toEqual(['r0', 'r1', 'r2']);
|
||||
});
|
||||
test('minKeep > cap still respected (recall floor wins)', () => {
|
||||
const { kept } = applyAdaptiveReturn(items(10), 'entity', ON({ entityMax: 1, minKeep: 3 }));
|
||||
expect(kept.length).toBe(3);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user