Harness spec: stubs per request, the compaction table from the reads it covers

context.md and loop.json `context`, spec 3.0.0-draft:

- A stub names only a result in the request being built. A chat on the
  server sends no earlier reply's tool results, so a read in the next reply
  is answered in full there; the CLI, which resends every result, keeps
  stubbing across steps. Said for reads and for repeated listings alike.
- The compaction table is drawn from the reads the summary covers: the
  full reads since the previous compaction in turns still in the history,
  whether or not the request still carries them. A turn taken back takes
  its reads with it. The CLI's dedupe table is exactly that set; a chat
  finds it in what it stored.
- Ids are the result's number in the session, in hex, at least 4 digits
  (#0001); the examples and the prompt block now use such ids.
- A bracket names the path as the call gave it; what identifies a file
  is its absolute path.

Conformance `context`: a `reply` step (a request carrying none of the
results so far) and a case for both rules. The CLI's runner keeps the
history apart from the request; compactionTable takes the reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-10-28 01:20:28 +00:00
1 parent eacf84fd67
commit 4ca6af12b1
6 files changed
+150 -40

No files matched your search

+14 -5
View File
@@ -117,15 +117,23 @@ export const AREAS: Record<string, (c: Case) => unknown> = {
const files = new Set(c.files as string[])
return instructionChain(c.boundary as string, c.start as string, (p) => files.has(p) && readableUnder(c.grants as Record<string, string> | undefined, p))
},
// Context hygiene: one session's insertions, step by step. The conversation is the results
// inserted since the last compaction, which is what a stub may point at.
// Context hygiene: one session's insertions, step by step. `messages` is the request being built
// (what a stub may point at); `history` the results since the last compaction, which its table
// is drawn from. `reply` starts a request that carries none of them, as a chat's next reply does.
context: (c) => {
const messages: { role: string; content: string }[] = []
const rc = new ResultContext(memoryResults(), () => presentIds(messages), { ...CONTEXT.cap, ...((c.limits ?? {}) as object) })
const history = new Set<string>()
const store = memoryResults()
const rc = new ResultContext(store, () => presentIds(messages), { ...CONTEXT.cap, ...((c.limits ?? {}) as object) })
return (c.steps as Record<string, any>[]).map((st) => {
if (st.op === "compact") {
const table = rc.compactionTable()
if (st.op === "reply") {
messages.length = 0
return ""
}
if (st.op === "compact") {
const table = rc.compactionTable(store.all().filter((r) => history.has(r.id)))
messages.length = 0
history.clear()
return table
}
let text: string
@@ -138,6 +146,7 @@ export const AREAS: Record<string, (c: Case) => unknown> = {
} else if (st.op === "recall") text = rc.insert({ kind: "recall", target: st.target, offset: st.offset, limit: st.limit }, st.id)
else text = rc.insert({ kind: "output", tool: st.tool, args: st.args, text: st.text }, st.id)
messages.push({ role: "tool", content: text })
history.add(st.id)
return text
})
},