Harness spec 3.1.0: the prefix stays still, in the CLI
ci / check (push) Canceled after 0s

The system prompt is made once per session (start, compaction, model
switch), kept in session_meta and resent byte for byte; it holds no
date. What changes mid-session -- the date, a mode the person chose, a
plan or memory or instruction file they edited, a personality, MCP
instructions that arrived later -- is a <session-note> on the next user
message, stored with it. The tool list is fixed for the session:
plan_submit is always offered and answers unavailable outside plan
mode, the wrap-up keeps the tools and runs no call, a withdrawn tool
stays listed and answers its refusal; the first request waits up to
mcp_wait_ms for MCP servers. Compaction asks for its summary as the
next turn of the same conversation. Cached input tokens are parsed
(incl. llama.cpp timings), stored per request and shown in /context.

Spec: context.md §7/§10, loop.json context.note (+ plan_ended,
plan_own, personal, empty, mcp_wait_ms), modes.json plan.refused,
compact.md without the transcript heading. Conformance area prefix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-11-05 13:22:02 +00:00
1 parent ecae6247fd
commit 8e06590d57
36 files changed
+1533 -169

No files matched your search

+19 -3
View File
@@ -207,6 +207,10 @@ export class OpenAIChatClient implements Client {
const addText = (kind: "text" | "reasoning", text: string) => out.add(kind, text)
let finish = "stop"
let usage: StreamEvent | undefined
// llama.cpp's own count, in its last chunk: `timings` with `cache_n` (read from its cache) and
// `prompt_n` (read now). The cached part of a request when `usage` does not say it.
let timings: { cache: number; read: number; out?: number } | undefined
let progressCache: number | undefined
for await (const { data } of sseJson(stream)) {
const chunk = data as any
@@ -217,6 +221,9 @@ export class OpenAIChatClient implements Client {
// string code; statusOf() still reads it as the server error it is.
throw new ProviderError(`${m.ref}: ${String(msg).trim()}`, statusOf(err), undefined, !out.output)
}
const tm = chunk?.timings
if (tm && typeof tm === "object" && Number.isFinite(tm.cache_n))
timings = { cache: Number(tm.cache_n), read: Number(tm.prompt_n) || 0, ...(Number.isFinite(tm.predicted_n) ? { out: Number(tm.predicted_n) } : {}) }
if (chunk?.usage && typeof chunk.usage === "object") {
const u = chunk.usage
usage = {
@@ -232,8 +239,10 @@ export class OpenAIChatClient implements Client {
// llama.cpp, while it reads the prompt: { total, cache, processed, time_ms }. Anything not
// shaped like that is ignored, never an error.
const pp = chunk?.prompt_progress
if (pp && typeof pp === "object" && Number.isFinite(pp.total) && pp.total > 0 && !out.output)
yield { type: "progress", total: Number(pp.total), cache: Number(pp.cache) || 0, processed: Number(pp.processed) || 0, ms: Number(pp.time_ms) || 0 }
if (pp && typeof pp === "object" && Number.isFinite(pp.total) && pp.total > 0 && !out.output) {
progressCache = Number(pp.cache) || 0
yield { type: "progress", total: Number(pp.total), cache: progressCache, processed: Number(pp.processed) || 0, ms: Number(pp.time_ms) || 0 }
}
const choice = chunk?.choices?.[0]
if (!choice) continue
const delta = choice.delta ?? choice.message ?? {}
@@ -272,9 +281,16 @@ export class OpenAIChatClient implements Client {
if (calls.length && finish !== "length") finish = "tool_calls"
const message = out.message()
const parts = message.parts
// The cached input tokens (harness context.md §10 rule 6): the usage's own field
// (`prompt_tokens_details.cached_tokens`, or `cache_read_input_tokens` where an Anthropic-shaped
// count comes through), else llama.cpp's timings, else its last progress report.
const cached = timings?.cache ?? progressCache
if (usage?.type === "usage" && usage.usage.cached === undefined && cached !== undefined) usage.usage.cached = cached
if (!usage && timings?.out !== undefined)
usage = { type: "usage", usage: { input: timings.cache + timings.read, output: timings.out, cached: timings.cache } }
if (!usage) {
const out = parts.reduce((n, p) => n + estimateTokens(p.type === "tool_call" ? p.name + p.args : p.text), 0)
usage = { type: "usage", usage: { input: estimateValue(body.messages), output: out, estimated: true } }
usage = { type: "usage", usage: { input: estimateValue(body.messages), output: out, estimated: true, ...(cached !== undefined ? { cached } : {}) } }
}
yield usage
yield { type: "finish", reason: finish, message }