From 3ded54f6423e5eed15d3340aba518759a076f3bd Mon Sep 17 00:00:00 2001 From: tommy0103 Date: Sun, 7 Jun 2026 17:41:24 +0800 Subject: [PATCH] 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. --- SKILL.md | 3 +- references/pitfalls.md | 18 +++++++ references/query-patterns.md | 91 ++++++++++++++++++++++++++++++++---- 3 files changed, 101 insertions(+), 11 deletions(-) diff --git a/SKILL.md b/SKILL.md index 1057165..524a270 100644 --- a/SKILL.md +++ b/SKILL.md @@ -49,7 +49,7 @@ The query file runs inside `(async () => { ... })()`. Use `return` to emit JSON. 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/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. 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`. - Start with cheap locators: `sessions()`, `summaries()`, `search()`, or a small SQL query. - 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. - 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. diff --git a/references/pitfalls.md b/references/pitfalls.md index d28e389..f4ed1db 100644 --- a/references/pitfalls.md +++ b/references/pitfalls.md @@ -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. diff --git a/references/query-patterns.md b/references/query-patterns.md index 9b441ce..c8871bc 100644 --- a/references/query-patterns.md +++ b/references/query-patterns.md @@ -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), }; ```