Skip to content

jcode: a coding harness of jlib's own, driven by jserve -- and the client half of the protocol it does not have #242

Description

@xoloki

jserve made jlib an inference server a coding harness can drive (#218,
#220): aider works against it end to end, in its whole edit format. The
harness on the other side of that wire is somebody else's program.

jcode is jlib's own: an app in jlib/apps that reads a project, asks a model
for an edit, applies it, runs the build, and goes round again -- talking to
jserve over HTTP, not linking ai::engine.

Why over the wire rather than in-process

jchat and jalpaca already link the engine directly. A third in-process
driver would prove nothing new and would pay a model load per run.

What already exists for it

  • ai::openai -- the wire format as JSON and nothing else, tested with no
    socket and no GGUF (ai_openai_test).
  • net::http client, util::http framing (with the suspending twins), and a
    server that does keep-alive, chunked output and SSE.
  • sys::run(argv, out, err) -- a subprocess with no shell in between,
    which is what a harness that runs a build needs.
  • sys::Directory, util::json, ai::chat, and jchat as a precedent for
    the loop shape.

What it needs that does not exist

Roughly in dependency order.

  1. The client half of ai::openai. Today the namespace parses a
    request and serialises a response, because it was written for the
    server. jcode needs the mirror image: build a request body, and read back
    either a whole chat.completion or a stream of chat.completion.chunks.
    Same file, same test, still no socket -- and then the two halves can be
    round-tripped against each other, which neither can do alone.

  2. A client that streams, and that reuses a connection. net::http::request()
    reads the whole body and returns, with a 1 MiB cap and no connection pool --
    its header says so, deliberately. An SSE reply is a body you must read as
    it arrives
    , and a harness makes many calls. aider opens a connection per LLM call, even though it asks for keep-alive #221 is the same measurement
    from the server's side: the handshake per call is real, and it is the client
    that decides whether to pay it.

  3. An edit format, and applying it. Start where the coverage already is:
    whole -- a filename and a fenced block -- because it needs no tool calling
    and is the format aider was verified against on this server. A
    search/replace format is the next step and is where the interesting failures
    live: an anchor that does not match is precisely the harness lie the
    server arc kept catching, and here the lie would be "applied" when nothing
    changed.

  4. Containment. A harness writes files and runs programs. Both want a root
    they cannot escape, resolved rather than inspected -- the files()
    lesson from net: serve a directory, and refuse everything outside it by resolving the path rather than inspecting it #225 -- plus a confirm gate that can be turned off on purpose
    rather than by default, and a dry run that prints the edit instead.

  5. Context selection. Which files go into the prompt, and what happens when
    they do not fit. The context length is in the GGUF and jserve knows it;
    the harness either asks or is told, and guessing is how a reply silently
    loses its head.

Tool calling: the honest part

A "full" coding harness in 2026 means tool calls, and jlib cannot make one
today at three separate levels:

  • openai::request deliberately does not model tools/tool_calls; it
    records that it was asked (wants_tools) so a server can refuse loudly
    rather than answer prose.
  • ai::chat does not render a tool list into the model's own template.
  • nothing constrains decoding, so a schema is a request rather than a
    guarantee.

Each of those can be wrong on its own, so each is its own piece of work. The
order that makes sense is the one aider proved: a useful edit loop runs
without tool calling
, so build that first and file tool calling as its own
arc afterwards -- with the measurement of what it buys taken against a jcode
that already works.

What would test it

  • Most of it needs no model. Point jcode at tests/httpserver.hh
    replaying a recorded completion and assert the edit lands on disk in a temp
    tree. That is the same in-process-server shape the rest of the tree uses, and
    it is the only way to test the replies nobody would send on request -- a
    fenced block naming a path outside the root, a stream that stops mid-chunk.
  • A *_live_test driving jcode against a real jserve, which is the
    mirror of what http::server: nothing automated drives jserve, because it needs a model file #236 asks for on the server side, and wants a GGUF for the
    same reason.
  • The break-the-guard rule applies double here. A coding harness's entire job
    is to report that it changed something; every assertion about a file being
    written has to be broken on purpose once and watched to fail.

What it is deliberately not

Not an agent with a shell. Not something to point at a repository it may
commit to unasked. One project root, every write announced, and the build
command it is allowed to run named rather than inferred.

This paragraph is here for the same reason the one at the top of
http_server.hh is: the way a narrow thing becomes a broad one is by nobody
writing down that it was meant to be narrow.

Sub-issues

Each numbered item above becomes its own issue when work starts on it; this one
is the shape and the order.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions