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
+18
View File
@@ -107,6 +107,24 @@ sql(`
`, '/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
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
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
`fileHistory()` contains reads as well as writes. For "why/how did this file
change", scan a bounded `Edit`/`Write` set first, then return only compact
evidence. Do not choose the answer from only the first few rows if the question
asks for evolution.
`fileHistory()` contains reads as well as writes and old-to-new rows. For
"why/how did this file change", scan a bounded `Edit`/`Write` set first, then
return only compact evidence. Do not return 20 long snippets; keep runtime JSON
small enough that the final answer, not the query output, carries the prose.
```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 reads = rows.filter(r => r.toolCall?.name === 'Read');
const targetTerms = ['summaries', 'failures', 'raw'];
const bySession = new Map();
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 group = bySession.get(sid) || {
session_id: sid,
@@ -135,28 +205,29 @@ for (const r of writes) {
write_edit_count: 0,
first_timestamp: r.timestamp,
last_timestamp: r.timestamp,
themes: [],
evidence: [],
};
group.write_edit_count++;
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;
const snippet = String(r.toolCall.input_json || '').slice(0, 360);
if (group.themes.length < 8) group.themes.push(snippet);
if (group.evidence.length < 3) {
if (group.evidence.length < 2) {
group.evidence.push({
tool: r.toolCall.name,
tool_id: r.toolCall.id,
timestamp: r.timestamp,
mentions: targetTerms.filter(k => delta.toLowerCase().includes(k)),
snippet,
});
}
bySession.set(sid, group);
}
const sessions = [...bySession.values()].slice(0, 6);
const returnedEvidence = sessions.reduce((n, s) => n + s.evidence.length, 0);
return {
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),
};
```