From a2a3a2d336d46a13663667ffa8d56bbfc85b3c6b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 22:10:34 +0000 Subject: [PATCH] cerebellum: make the LLM seam async; add OpenHermesClient skeleton A real model call is network/process IO, so the LLM seam is now a behaviour with a response callback (ResponseFn), not a synchronous function. StubLLM answers immediately; OpenHermesClient + OpenHermesConfig (OpenAI-compatible url/model) are the real plug, swappable for the stub. CerebellumHarness chains its passes through the async callback. Compiles on ponyc 0.64; demo green. Next: wire the actual chat/completions call behind OpenHermesClient. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_015hmgREHNsxYCuim33yUF2c --- src/ichor/cerebellum.pony | 77 ++++++++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 22 deletions(-) diff --git a/src/ichor/cerebellum.pony b/src/ichor/cerebellum.pony index 7f54520..860110a 100644 --- a/src/ichor/cerebellum.pony +++ b/src/ichor/cerebellum.pony @@ -4,19 +4,51 @@ // the synthesis back through the bus (Brain-bound, so it crosses D1). This is // the C1 integration spine ("plumbing + sequencing, not an organ"). // -// Plug-and-play: the model is the `LLM` seam below. StubLLM stands in now; drop -// a real OpenHermes client (HTTP / subprocess) in its place and nothing else -// on the bus changes. +// Plug-and-play: the model is the `LLM` seam. A real model call is network / +// process IO, so the seam is ASYNCHRONOUS -- `infer` is a behaviour that hands +// its result to a callback. StubLLM answers immediately; OpenHermesClient is the +// real plug. Swap which one you register; nothing else on the bus changes. -// --- the model seam ------------------------------------------------------- -interface val LLM - """One inference call. Implement this to plug a model into the cerebellum.""" - fun infer(prompt: String): String +// --- the model seam (async) ---------------------------------------------- +type ResponseFn is {(String)} val + """Callback the model invokes with its output.""" -class val StubLLM is LLM - """Stand-in until a real OpenHermes client is wired.""" - fun infer(prompt: String): String => - "[stub-openhermes] " + prompt +interface tag LLM + be infer(prompt: String, respond: ResponseFn) + +// --- stub ----------------------------------------------------------------- +actor StubLLM is LLM + new create() => None + + be infer(prompt: String, respond: ResponseFn) => + respond("[stub-openhermes] " + prompt) + +// --- the OpenHermes plug -------------------------------------------------- +class val OpenHermesConfig + """Where to reach an OpenAI-compatible OpenHermes server.""" + let url: String // base, e.g. http://localhost:8080/v1 + let model: String + + new val create( + url': String = "http://localhost:8080/v1", + model': String = "openhermes") + => + url = url' + model = model' + +actor OpenHermesClient is LLM + let _cfg: OpenHermesConfig + + new create(cfg: OpenHermesConfig) => + _cfg = cfg + + be infer(prompt: String, respond: ResponseFn) => + // TODO(loop next): POST an OpenAI-compatible chat/completions request to + // `_cfg.url`/chat/completions via the process seam (curl) or a Pony TCP + // client, parse choices[0].message.content, then call respond() with it. + // Until that IO is wired and testable in this env, surface intent rather + // than perform an untested network call. + respond("[openhermes " + _cfg.model + " @ " + _cfg.url + "] " + prompt) // --- the harness ---------------------------------------------------------- actor CerebellumHarness is OrganReceiver @@ -29,18 +61,19 @@ actor CerebellumHarness is OrganReceiver _out = out' _bus = bus _llm = llm - // C1 L3: pass count is the responsiveness knob (energy-gated later), the - // 4+4 ordering itself is static. Clamp to >=1 so a turn always runs once. + // C1 L3: pass count is the responsiveness knob (energy-gated later); the + // 4+4 ordering is static. Clamp to >=1 so a turn always runs once. _passes = if passes' < 1 then 1 else passes' end be receive(envl: Envelope) => - // A turn arrives off the bus. Sequence the passes through the model, then - // emit the synthesis back onto the bus toward the Brain. - var ctx: String = envl.payload - var pass: USize = 1 - while pass <= _passes do - ctx = _llm.infer(ctx) - pass = pass + 1 + // A turn arrives off the bus: sequence the passes, then emit. + _pass(envl.payload, 1) + + be _pass(ctx: String, n: USize) => + if n > _passes then + _out.print("[cerebellum] " + _passes.string() + " passes -> " + ctx) + _bus.route(Envelope(Cerebellum, Brain, OrganSecretion, ctx)) + else + let self: CerebellumHarness tag = this + _llm.infer(ctx, {(out: String)(self, n) => self._pass(out, n + 1) } val) end - _out.print("[cerebellum] " + _passes.string() + " passes -> " + ctx) - _bus.route(Envelope(Cerebellum, Brain, OrganSecretion, ctx))