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>
240 lines
12 KiB
TypeScript
240 lines
12 KiB
TypeScript
// What the CLI does with each conformance case (harness/conformance/*.json). The cases are the
|
|
// harness spec's: LLeMbas runs the same files through its own implementation. The test compares
|
|
// this with each case's `expect`; `bun run harness record` fills in an `expect` a new case lacks.
|
|
import { alwaysPatterns, toRules, evaluate, DEFAULT_RULES, type PermissionRequest } from "../src/permission/evaluate.ts"
|
|
import { BUILTIN_HARDLINE, hardlineCommand, plainCommands } from "../src/permission/hardline.ts"
|
|
import { splitCommand, words } from "../src/permission/bash.ts"
|
|
import { prefix } from "../src/permission/arity.ts"
|
|
import { split } from "../src/library/chunks.ts"
|
|
import { ThinkSplitter } from "../src/provider/think.ts"
|
|
import { advertisedEfforts, effortRefused } from "../src/provider/effort.ts"
|
|
import { replace } from "../src/tool/replace.ts"
|
|
import { applyUnified, parseUnified } from "../src/tool/unidiff.ts"
|
|
import { derive, joinBom, parse } from "../src/tool/patch.ts"
|
|
import type { Mode, PermissionConfig } from "../src/config/schema.ts"
|
|
import { apply, fromTodos, normalise, type Plan } from "../src/plan/plan.ts"
|
|
import { ToolCallAccumulator } from "../src/provider/openai-chat.ts"
|
|
import { parseInfo, titleFor } from "../src/prototype/fence.ts"
|
|
import { resolveBase } from "../src/library/store.ts"
|
|
import { NarrowError, narrowParameters, type Narrow } from "../src/harness.ts"
|
|
import { fillPrompt } from "../src/prompt/fill.ts"
|
|
import { instructionChain } from "../src/prompt/assemble.ts"
|
|
import { CONTEXT, hunks, lineCount, memoryResults, presentIds, ResultContext, sha256 } from "../src/session/results.ts"
|
|
import { readBody } from "../src/tool/read.ts"
|
|
import { applyOps, type Op, type Target } from "../src/memory/store.ts"
|
|
import { jobNote, type NoteCase } from "../src/jobs/note.ts"
|
|
import { compactAsk, noteFor, offeredFor, toldAt, unavailable, type Now, type Requirement, type Told } from "../src/session/prefix.ts"
|
|
import { promptText } from "../src/prompt/assemble.ts"
|
|
import { TOOL_SPECS } from "../src/harness.ts"
|
|
|
|
type Case = Record<string, unknown>
|
|
|
|
/** Whether a path is at `ro` or above under `grants` (fs_access's rule: the most specific grant on
|
|
* the path or above it wins; none where nothing is granted). No grants at all: everything readable. */
|
|
function readableUnder(grants: Record<string, string> | undefined, path: string): boolean {
|
|
if (!grants) return true
|
|
let best: string | undefined
|
|
for (const g of Object.keys(grants)) {
|
|
const covers = g === "/" || path === g || path.startsWith(g + "/")
|
|
if (covers && (best === undefined || g.length > best.length)) best = g
|
|
}
|
|
return best !== undefined && grants[best] !== "none"
|
|
}
|
|
|
|
export const AREAS: Record<string, (c: Case) => unknown> = {
|
|
hardline: (c) => hardlineCommand(c.line as string, BUILTIN_HARDLINE)?.id ?? null,
|
|
split: (c) => splitCommand(c.line as string),
|
|
plain: (c) => plainCommands(c.line as string),
|
|
arity: (c) => prefix(words(c.command as string)).join(" "),
|
|
always: (c) => {
|
|
const line = c.line as string
|
|
const split = splitCommand(line)
|
|
// What an approval card offers to remember: nothing when the split cannot be trusted.
|
|
if (split.unsafe.length) return []
|
|
return alwaysPatterns({ permission: "bash", class: "execute", patterns: [line], command: line }, split.commands.length ? split.commands : [line])
|
|
},
|
|
permission: (c) => {
|
|
const line = c.line as string
|
|
const req: PermissionRequest = { permission: "bash", class: "execute", patterns: [line], command: line, paths: ["/project"] }
|
|
const rules = [...toRules(DEFAULT_RULES, "default"), ...toRules((c.rules ?? {}) as PermissionConfig, "global")]
|
|
return evaluate(req, { mode: c.mode as Mode, rules, hardline: BUILTIN_HARDLINE, root: "/project", planDir: "/project/.agent/plans", projectDir: "/project/.agent" }).action
|
|
},
|
|
chunk: (c) => split(c.text as string, c.size as number, c.overlap as number),
|
|
think: (c) => {
|
|
const s = new ThinkSplitter()
|
|
const pieces = [...(c.chunks as string[]).flatMap((x) => s.feed(x)), ...s.flush()]
|
|
const out = { reasoning: "", text: "" }
|
|
for (const p of pieces) out[p.kind] += p.text
|
|
return out
|
|
},
|
|
effort: (c) => ({ refused: effortRefused(c.message as string), advertised: advertisedEfforts(c.message as string) }),
|
|
edit: (c) => {
|
|
try {
|
|
return { content: replace(c.content as string, c.old as string, c.new as string, c.all === true) }
|
|
} catch (e) {
|
|
return { error: (e as Error).message }
|
|
}
|
|
},
|
|
unidiff: (c) => {
|
|
try {
|
|
const files = parseUnified(c.patch as string)
|
|
return { content: applyUnified(c.content as string, files.flatMap((f) => f.hunks)) }
|
|
} catch (e) {
|
|
return { error: (e as Error).message }
|
|
}
|
|
},
|
|
plan: (c) => {
|
|
if (c.op === "normalise") return normalise(c.plan)
|
|
if (c.op === "update") return apply(c.plan as Plan, c.changes)
|
|
if (c.op === "todos") return fromTodos(c.todos)
|
|
throw new Error(`plan: unknown op ${String(c.op)}`)
|
|
},
|
|
prototype: (c) => {
|
|
const info = parseInfo(c.info as string)
|
|
return info ? { kind: info.kind, title: titleFor(c.code as string, info.title) } : null
|
|
},
|
|
fill: (c) => {
|
|
try {
|
|
return { text: fillPrompt(c.text as string, c.vars as Record<string, string>) }
|
|
} catch (e) {
|
|
// "prompt: no value for {{a}}, {{c}}": the names, sorted.
|
|
return { missing: [...(e as Error).message.matchAll(/\{\{(\w+)\}\}/g)].map((m) => m[1]!).sort() }
|
|
}
|
|
},
|
|
narrow: (c) => {
|
|
try {
|
|
return { parameters: narrowParameters(c.parameters as Record<string, unknown>, c.narrow as Narrow) }
|
|
} catch (e) {
|
|
if (e instanceof NarrowError) return { error: e.code }
|
|
throw e
|
|
}
|
|
},
|
|
memory: (c) => {
|
|
const r = applyOps(c.target as Target, c.entries as string[], c.ops as Op[], c.limit as number)
|
|
return { ok: r.ok, entries: r.ok ? r.after : c.entries, message: r.message, usage: r.usage, shown: Boolean(r.entries) }
|
|
},
|
|
jobs: (c) => jobNote(c as unknown as NoteCase),
|
|
// The instruction chain (filesystem.md): the CLI's own walk, with the case's files and grants as
|
|
// what it may read.
|
|
instructions: (c) => {
|
|
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. `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 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 === "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
|
|
if (st.op === "read") {
|
|
const { body, from, to, of } = readBody(st.content, st.offset, st.limit)
|
|
text = rc.insert({ kind: "read", path: st.path, key: st.path, sha: sha256(st.content), from, to, of, body }, st.id)
|
|
} else if (st.op === "edit" || st.op === "write") {
|
|
const sha = sha256(st.after)
|
|
text = rc.insert({ kind: "edit", files: [st.before === undefined ? { path: st.path, sha, created: lineCount(st.after) } : { path: st.path, sha, hunks: hunks(st.before, st.after) }] }, st.id)
|
|
} 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
|
|
})
|
|
},
|
|
// The prefix (context.md §10): one session's requests, step by step — which prompt each sends (a
|
|
// new number each time one is made), which tools it offers, and the note on a user message.
|
|
prefix: (c) => {
|
|
const start = c.start as Partial<Now> & { could?: Requirement[]; met?: Requirement[] }
|
|
const could = new Set<Requirement>(start.could ?? [])
|
|
let met = new Set<Requirement>(start.met ?? could)
|
|
const now: Now = { date: start.date!, mode: start.mode ?? "manual", plan: start.plan ?? "", memory: start.memory ?? "", personality: start.personality ?? "", files: start.files ?? {}, mcp: start.mcp ?? {} }
|
|
const modeText = (m: Mode) => fillPrompt(promptText(`modes/${m}.md`), { plan_dir: (c.plan_dir as string | undefined) ?? ".agent/plans" })
|
|
const requires = (t: string) => TOOL_SPECS[t]?.requires as Requirement[] | undefined
|
|
let made: { system: number; tools: string[]; told: Told } | undefined
|
|
let prompts = 0
|
|
let carried: string | undefined
|
|
const make = () => {
|
|
if (made) return made
|
|
made = { system: ++prompts, tools: (c.tools as string[]).filter((t) => offeredFor(requires(t), could)), told: { ...toldAt(now), ...(carried ? { date: carried } : {}) } }
|
|
carried = undefined
|
|
return made
|
|
}
|
|
const fields = ["date", "mode", "plan", "memory", "personality", "files", "mcp"] as const
|
|
return (c.steps as Record<string, any>[]).map((st) => {
|
|
if (st.op === "set") {
|
|
for (const f of fields) if (f in st) (now as unknown as Record<string, unknown>)[f] = structuredClone(st[f])
|
|
if (st.met) met = new Set(st.met)
|
|
// The model's own tool changed it: its result told the model.
|
|
if (st.by === "model" && made) for (const f of fields) if (f in st && f !== "date") (made.told as unknown as Record<string, unknown>)[f] = structuredClone(st[f])
|
|
return null
|
|
}
|
|
if (st.op === "call") {
|
|
const here = new Set(met)
|
|
if (now.mode === "plan") here.add("plan")
|
|
else here.delete("plan")
|
|
return unavailable(st.tool, requires(st.tool), here) ?? null
|
|
}
|
|
if (st.op === "model") {
|
|
carried = made?.told.date
|
|
made = undefined
|
|
return null
|
|
}
|
|
const k = make()
|
|
if (st.op === "user") {
|
|
const { note, told } = noteFor(k.told, now, modeText)
|
|
if (note) k.told = told
|
|
return { system: k.system, tools: k.tools, note: note || null }
|
|
}
|
|
if (st.op === "tool" || st.op === "wrapup") return { system: k.system, tools: k.tools, note: null }
|
|
if (st.op === "compact") {
|
|
made = undefined
|
|
return { system: k.system, tools: k.tools, ask: compactAsk() }
|
|
}
|
|
throw new Error(`prefix: unknown op ${String(st.op)}`)
|
|
})
|
|
},
|
|
library_scope: (c) => resolveBase(c.bases as { id: string; name: string }[], c.base as string | null),
|
|
toolcall_index: (c) => {
|
|
// The deltas as the OpenAI stream carries them: `function.name` and `function.arguments`.
|
|
const acc = new ToolCallAccumulator()
|
|
acc.feed((c.deltas as Record<string, unknown>[]).map(({ index, id, name, arguments: args }) => ({ index, id, function: { name, arguments: args } })))
|
|
return acc.result().map((p) => ({ id: p.id, name: p.name, arguments: p.args }))
|
|
},
|
|
envelope: (c) => {
|
|
// planPatch's logic without the filesystem: every file followed through the patch in memory.
|
|
const files = new Map<string, string | null>(Object.entries(c.files as Record<string, string>))
|
|
try {
|
|
for (const h of parse(c.patch as string)) {
|
|
const text = files.get(h.path) ?? null
|
|
if (h.type === "add") {
|
|
if (text !== null) throw new Error(`${h.path} already exists; use Update File to change it.`)
|
|
files.set(h.path, h.contents.endsWith("\n") ? h.contents : h.contents + "\n")
|
|
} else if (h.type === "delete") {
|
|
if (text === null) throw new Error(`${h.path} does not exist.`)
|
|
files.set(h.path, null)
|
|
} else {
|
|
if (text === null) throw new Error(`${h.path} does not exist.`)
|
|
const next = derive(h.path, h.chunks, text)
|
|
if (h.movePath) files.set(h.path, null)
|
|
files.set(h.movePath ?? h.path, joinBom(next.content, next.bom))
|
|
}
|
|
}
|
|
return { files: Object.fromEntries([...files].sort(([a], [b]) => a.localeCompare(b))) }
|
|
} catch (e) {
|
|
return { error: (e as Error).message }
|
|
}
|
|
},
|
|
}
|