Files
HomerandClaude Opus 5.5 f9bad01ed7
ci / check (push) Waiting to run
LLeMbas CLI 1.0.0
The first public release of LLeMbas CLI: a terminal coding agent and project manager for any LLM
API, with permission modes, git snapshots, memory and skills, knowledge bases, MCP, voice, and a
link to a LLeMbas instance whose web UI can work its sessions too. Signed Linux binaries for x64
and arm64.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:59:03 +00:00

150 lines
6.8 KiB
TypeScript

// A message sent while the agent works: it goes in at the next step boundary — after every tool
// result of the step that was running, before the next request — and if the model had already
// finished, it carries on with it in the same task. Esc gives it back; busy_input: queue holds it
// for the task's end.
import { afterEach, expect, test } from "bun:test"
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { createApp } from "../src/app.ts"
import type { AskReply, Event } from "../src/bus/index.ts"
import { paths } from "../src/config/paths.ts"
import type { Message } from "../src/provider/types.ts"
import { setTrust } from "../src/project/root.ts"
import { mergeUsers, STEER_MARK } from "../src/session/engine.ts"
import { delta, fakeProvider, toolCall, usage, type Fake } from "./fake-provider.ts"
let fake: Fake | undefined
afterEach(() => fake?.stop())
function setup(script: Parameters<typeof fakeProvider>[0], config = "") {
fake = fakeProvider(script)
mkdirSync(paths.config, { recursive: true })
writeFileSync(
join(paths.config, "connections.yaml"),
`connections:\n f:\n dialect: openai-chat\n base_url: ${fake.url}\n models:\n main: { context: 32768 }\n a: {}\n`,
{ mode: 0o600 },
)
writeFileSync(join(paths.config, "config.yaml"), `model: f/main\ntitles: prompt\n${config}`)
const cwd = mkdtempSync(join(tmpdir(), "ph-steer-"))
Bun.spawnSync(["git", "init", "-q", "-b", "main"], { cwd })
writeFileSync(join(cwd, "a.txt"), "one\n")
setTrust(cwd, "trusted")
const app = createApp({ cwd, mode: "edit", asker: { ask: async (): Promise<AskReply> => ({ kind: "once" }) } })
const events: Event[] = []
app.bus.on((e) => events.push(e))
return { app, events }
}
/** Send `text` once the first step's reply has started streaming. */
function sendDuringFirstStep(app: ReturnType<typeof setup>["app"], text: string) {
let sent = false
app.bus.on((e) => {
if (!sent && (e.type === "text" || e.type === "tool_call_delta")) {
sent = true
expect(app.turns.steer(text)).toBe(true)
}
})
}
const lastUser = (msgs: { role: string; content: unknown }[]) => JSON.stringify(msgs.filter((m) => m.role === "user").at(-1)?.content)
test("sent during a tool step: after that step's tool results, before the next request", async () => {
const { app, events } = setup([
{ gapMs: 150, chunks: [delta({ content: "Listing." }), toolCall(0, "c1", "list", "{}"), usage(10, 5)] },
{ chunks: [delta({ content: "Using b.txt then." }, "stop")] },
])
sendDuringFirstStep(app, "actually call it b.txt")
expect(await app.turns.prompt("make a file")).toBe("stop")
const second = fake!.requests[1].messages as { role: string; content: unknown }[]
// …, assistant (the call), tool (its result), user (the message): the pairing is intact.
expect(second.slice(-3).map((m) => m.role)).toEqual(["assistant", "tool", "user"])
expect(lastUser(second)).toContain("actually call it b.txt")
expect(lastUser(second)).toContain(STEER_MARK)
expect(events.filter((e) => e.type === "steered")).toHaveLength(1)
// The waiting list was shown, then emptied when it went in.
const inbox = events.filter((e) => e.type === "inbox").map((e) => (e.type === "inbox" ? e.texts.length : -1))
expect(inbox).toEqual([1, 0])
expect(app.turns.takeInbox()).toEqual([])
})
test("sent while the final answer streams: the model carries on with it in the same task", async () => {
const { app } = setup([
{ gapMs: 150, chunks: [delta({ content: "All " }), delta({ content: "done." }, "stop")] },
{ chunks: [delta({ content: "Added the tests too." }, "stop")] },
])
sendDuringFirstStep(app, "and add tests")
expect(await app.turns.prompt("fix it")).toBe("stop")
expect(fake!.requests).toHaveLength(2)
const second = fake!.requests[1].messages as { role: string; content: unknown }[]
expect(second.slice(-2).map((m) => m.role)).toEqual(["assistant", "user"])
expect(lastUser(second)).toContain("and add tests")
expect(app.engine.messages.at(-1)).toMatchObject({ role: "assistant" })
})
test("stopped before it went in: Esc gives it back, nothing reached the model", async () => {
const { app } = setup([{ gapMs: 300, chunks: [delta({ content: "Working" }), delta({ content: " on it" }), delta({ content: "." }, "stop")] }])
let sent = false
app.bus.on((e) => {
if (!sent && e.type === "text") {
sent = true
app.turns.steer("wait, not that")
setTimeout(() => app.cancel(), 50)
}
})
expect(await app.turns.prompt("do the thing")).toBe("cancelled")
expect(fake!.requests).toHaveLength(1)
expect(app.turns.takeInbox()).toEqual(["wait, not that"])
})
test("busy_input: queue — held for the end of the task, never sent mid-turn", async () => {
const { app } = setup(
[
{ gapMs: 150, chunks: [delta({ content: "Listing." }), toolCall(0, "c1", "list", "{}")] },
{ chunks: [delta({ content: "Done." }, "stop")] },
],
"busy_input: queue\n",
)
sendDuringFirstStep(app, "next: the docs")
expect(await app.turns.prompt("first: the code")).toBe("stop")
expect(fake!.requests).toHaveLength(2)
expect(JSON.stringify(fake!.requests[1].messages)).not.toContain("next: the docs")
expect(app.turns.takeInbox()).toEqual(["next: the docs"])
})
test("no task running: steer says no, the caller sends a prompt instead", () => {
const { app } = setup([])
expect(app.turns.steer("hello")).toBe(false)
})
test("neighbouring user messages go to the model as one", () => {
const m: Message[] = [
{ role: "user", parts: [{ type: "text", text: "a" }] },
{ role: "assistant", parts: [{ type: "text", text: "b" }] },
{ role: "user", parts: [{ type: "text", text: "c" }] },
{ role: "user", parts: [{ type: "text", text: "d" }] },
]
const merged = mergeUsers(m)
expect(merged).toHaveLength(3)
expect(merged[2]).toEqual({ role: "user", parts: [{ type: "text", text: "c" }, { type: "text", text: "d" }] })
// The stored conversation is not touched.
expect(m).toHaveLength(4)
})
test("compacted in a later step: the message is kept word for word, like the prompt", async () => {
// A 2000-token window and a step that reports 1900 used: the next step compacts.
fake = undefined
const { app } = setup([
{ gapMs: 150, chunks: [delta({ content: "Listing." }), toolCall(0, "c1", "list", "{}"), usage(1500, 10)] },
{ chunks: [toolCall(0, "c2", "list", "{}"), usage(1900, 10)] },
{ chunks: [delta({ content: "Summary: listed twice." }, "stop")] },
{ chunks: [delta({ content: "Done." }, "stop")] },
])
app.engine.model.spec.context = 2000
sendDuringFirstStep(app, "KEEP-THIS-INSTRUCTION")
expect(await app.turns.prompt("list things")).toBe("stop")
const last = JSON.stringify(fake!.requests.at(-1).messages)
expect(last).toContain("compacted")
expect(last).toContain("KEEP-THIS-INSTRUCTION")
})