docs: add one-shot synthesis pattern and turn-cost pitfall

Teach the agent to keep intermediate retrieval inside the query script
  and return compact evidence in a single turn, instead of spending
  multiple conversation rounds showing raw results. Add the pattern to
  query-patterns.md and the rationale to pitfalls.md.
This commit is contained in:
tommy0103
2026-06-07 17:41:24 +08:00
parent 297ef01eaf
commit 3ded54f642
3 changed files with 101 additions and 11 deletions
+2 -1
View File
@@ -49,7 +49,7 @@ The query file runs inside `(async () => { ... })()`. Use `return` to emit JSON.
Use progressive disclosure, but do not guess. Use progressive disclosure, but do not guess.
- Read `references/schema.md` before raw `sql()` unless the needed table/column relationship is already explicit here. - Read `references/schema.md` before raw `sql()` unless the needed table/column relationship is already explicit here.
- Read `references/query-patterns.md` for workflow trees, failed tool counts or failure groups, broad development-history synthesis, file history synthesis, summary neighbors, subagent recall, raw windows, and empty-result handling. - Read `references/query-patterns.md` for one-shot synthesis retrieval, workflow trees, failed tool counts or failure groups, broad development-history synthesis, file history synthesis, summary neighbors, subagent recall, raw windows, and empty-result handling.
- Read `references/pitfalls.md` when a scoped result is empty or tiny, when a query may over-fetch, when a term is hyphenated, when project scope is ambiguous, or when helper row fields are unclear. - Read `references/pitfalls.md` when a scoped result is empty or tiny, when a query may over-fetch, when a term is hyphenated, when project scope is ambiguous, or when helper row fields are unclear.
If a helper row shape is unclear, first run a tiny scoped query and return If a helper row shape is unclear, first run a tiny scoped query and return
@@ -137,6 +137,7 @@ Keep queries scoped, bounded, and structural.
- Treat project scope as three distinct semantics: exact `sessions.project` slug, exact `sessions.project_path`, or fuzzy `LIKE` search. Use `sql()` for exact slug/path membership; helper `project` means fuzzy `LIKE`. - Treat project scope as three distinct semantics: exact `sessions.project` slug, exact `sessions.project_path`, or fuzzy `LIKE` search. Use `sql()` for exact slug/path membership; helper `project` means fuzzy `LIKE`.
- Start with cheap locators: `sessions()`, `summaries()`, `search()`, or a small SQL query. - Start with cheap locators: `sessions()`, `summaries()`, `search()`, or a small SQL query.
- Expand incrementally with `context()`, `trace()`, neighbor SQL, or `raw()` windows. - Expand incrementally with `context()`, `trace()`, neighbor SQL, or `raw()` windows.
- For conclusion, broad history, failure investigation, or file evolution questions, prefer one bounded query script that locates, expands, dedupes, groups, and returns compact evidence rows. Do not spend multiple conversation turns showing intermediate query results.
- Return compact evidence with stable IDs (`session_id`, `uuid`, `tool_call_id`, `run_id`, `agent_id`) and short snippets. - Return compact evidence with stable IDs (`session_id`, `uuid`, `tool_call_id`, `run_id`, `agent_id`) and short snippets.
- Avoid `thread()` unless the user explicitly asks for a full transcript or all smaller probes are insufficient. - Avoid `thread()` unless the user explicitly asks for a full transcript or all smaller probes are insufficient.
- Keep runtime JSON small. Do not return all sessions, all summaries, all tool calls, complete workflow trees, full raw messages, or whole tool results. - Keep runtime JSON small. Do not return all sessions, all summaries, all tool calls, complete workflow trees, full raw messages, or whole tool results.
+18
View File
@@ -107,6 +107,24 @@ sql(`
`, '/absolute/path/to/file') `, '/absolute/path/to/file')
``` ```
## Conversation Turns Are Expensive
SQLite queries are cheap; repeated conversation turns are not. Every turn can
write intermediate query output into conversation context and make later turns
read it again.
For conclusion, broad history, failure investigation, or file evolution tasks,
prefer one bounded query script that does the mechanical retrieval work inside
the script:
1. locate candidates with `search()`, `summaries()`, or SQL;
2. expand only selected hits with `context()`, `trace()`, or neighbor SQL;
3. dedupe and group by `session_id`, facet, file, or tool;
4. return compact evidence rows plus counts/limits.
Do not show every intermediate result to the conversation. Return the final
compact evidence view, then use the model for the conclusion.
## Compact Vs Raw ## Compact Vs Raw
Default to compact evidence. Raw/full access is a conscious escalation. Default to compact evidence. Raw/full access is a conscious escalation.
+81 -10
View File
@@ -26,6 +26,71 @@ return hits.slice(0, 5).map(h => {
}); });
``` ```
## One-Shot Retrieval For Synthesis
Use this for conclusion, broad history, failure investigation, or file evolution
questions. The goal is to reduce conversation turns: keep intermediate search
results inside the query script, then return only a compact task-local evidence
view. This does not create stored semantic entities; the agent still reads the
evidence and forms the conclusion.
```js
const project = '%quiet-zero%';
const topic = 'obelisk retrieval semantics';
const ftsTopic = topic.replace(/[-_]/g, ' ');
const facets = [
'summary conclusion',
'runtime query script',
'failure problem',
'file change',
];
const candidates = [];
for (const facet of facets) {
for (const h of search(`${ftsTopic} ${facet}`, { project, limit: 4 })) {
candidates.push({
kind: 'message',
facet,
session_id: h.session.id,
session_title: h.session.title,
uuid: h.message.uuid,
timestamp: h.message.timestamp,
snippet: h.message.text?.slice(0, 220),
});
}
}
for (const s of summaries({ project, limit: 8 })) {
if (/obelisk|retrieval|context|summary/i.test(`${s.content || ''} ${s.session_title || ''}`)) {
candidates.push({
kind: 'summary',
facet: 'summary',
summary_id: s.id,
session_id: s.session_id,
session_title: s.session_title,
timestamp: s.timestamp,
snippet: s.content?.slice(0, 240),
});
}
}
const seen = new Set();
const evidence = [];
for (const row of candidates.sort((a, b) => String(b.timestamp).localeCompare(String(a.timestamp)))) {
const key = row.uuid || row.summary_id || `${row.session_id}:${row.timestamp}:${row.facet}`;
if (seen.has(key)) continue;
seen.add(key);
evidence.push(row);
if (evidence.length >= 16) break;
}
return {
query_plan: { project, topic, facets, per_facet_limit: 4, max_evidence: 16 },
evidence,
omitted: Math.max(0, candidates.length - evidence.length),
};
```
## Facet Sweep For Broad History ## Facet Sweep For Broad History
Use this only for broad synthesis questions such as "how did X evolve", "what Use this only for broad synthesis questions such as "how did X evolve", "what
@@ -115,18 +180,23 @@ return { summary: s, before, after };
## File History Synthesis ## File History Synthesis
`fileHistory()` contains reads as well as writes. For "why/how did this file `fileHistory()` contains reads as well as writes and old-to-new rows. For
change", scan a bounded `Edit`/`Write` set first, then return only compact "why/how did this file change", scan a bounded `Edit`/`Write` set first, then
evidence. Do not choose the answer from only the first few rows if the question return only compact evidence. Do not return 20 long snippets; keep runtime JSON
asks for evolution. small enough that the final answer, not the query output, carries the prose.
```js ```js
const rows = fileHistory('/absolute/path/to/file', { limit: 100 }); const rows = fileHistory('/absolute/path/to/file', { limit: 80 });
const writes = rows.filter(r => ['Edit', 'Write'].includes(r.toolCall?.name)); const writes = rows.filter(r => ['Edit', 'Write'].includes(r.toolCall?.name));
const reads = rows.filter(r => r.toolCall?.name === 'Read'); const reads = rows.filter(r => r.toolCall?.name === 'Read');
const targetTerms = ['summaries', 'failures', 'raw'];
const bySession = new Map(); const bySession = new Map();
for (const r of writes) { for (const r of writes) {
let input = {};
try { input = JSON.parse(r.toolCall.input_json || '{}'); } catch {}
const delta = String(input.new_string || input.content || input.old_string || '');
const snippet = delta.slice(0, 220);
const sid = r.session.id; const sid = r.session.id;
const group = bySession.get(sid) || { const group = bySession.get(sid) || {
session_id: sid, session_id: sid,
@@ -135,28 +205,29 @@ for (const r of writes) {
write_edit_count: 0, write_edit_count: 0,
first_timestamp: r.timestamp, first_timestamp: r.timestamp,
last_timestamp: r.timestamp, last_timestamp: r.timestamp,
themes: [],
evidence: [], evidence: [],
}; };
group.write_edit_count++; group.write_edit_count++;
group.first_timestamp = group.first_timestamp < r.timestamp ? group.first_timestamp : r.timestamp; group.first_timestamp = group.first_timestamp < r.timestamp ? group.first_timestamp : r.timestamp;
group.last_timestamp = group.last_timestamp > r.timestamp ? group.last_timestamp : r.timestamp; group.last_timestamp = group.last_timestamp > r.timestamp ? group.last_timestamp : r.timestamp;
const snippet = String(r.toolCall.input_json || '').slice(0, 360); if (group.evidence.length < 2) {
if (group.themes.length < 8) group.themes.push(snippet);
if (group.evidence.length < 3) {
group.evidence.push({ group.evidence.push({
tool: r.toolCall.name, tool: r.toolCall.name,
tool_id: r.toolCall.id, tool_id: r.toolCall.id,
timestamp: r.timestamp, timestamp: r.timestamp,
mentions: targetTerms.filter(k => delta.toLowerCase().includes(k)),
snippet, snippet,
}); });
} }
bySession.set(sid, group); bySession.set(sid, group);
} }
const sessions = [...bySession.values()].slice(0, 6);
const returnedEvidence = sessions.reduce((n, s) => n + s.evidence.length, 0);
return { return {
counts: { reads: reads.length, writes_edits: writes.length }, counts: { reads: reads.length, writes_edits: writes.length },
sessions: [...bySession.values()].slice(0, 10), sessions,
omitted_write_edit_rows: Math.max(0, writes.length - returnedEvidence),
}; };
``` ```