Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,5 @@ go.work.sum
# on disk and the compiler still reads it. `find . -name '* [0-9]*'` finds
# them when a build fails for no apparent reason.
* [0-9]*

/dist/
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ Windows PowerShell:
irm https://raw.githubusercontent.com/mindsdb/yolocoder/main/install.ps1 | iex
```

## Muse + Jev review build

See [the build and integration notes](docs/jev-muse.md) for the optimized review configuration and the distinction from the frozen benchmark.

## Develop

Requires Go 1.24 or newer.
Expand Down
32 changes: 32 additions & 0 deletions docs/jev-muse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Muse + Jev review build

This ports the Round 032 winning approach to current upstream main. It is a review build, **not the binary measured in the frozen benchmark**. The exact benchmark source remains on `review/frozen-r032`; the companion evaluation PR contains a portable source-verified reproduction workflow.

## Build and try it

Requires Go 1.24+, Node 22 for web projects, and a MindsHub account serving both `muse-spark-1-3` and `jev-1.13.0`.

```sh
./scripts/build-jev-muse.sh
YOLOCODER_NO_AUTOUPDATE=1 ./dist/yolocoder-jev-muse config
YOLOCODER_NO_AUTOUPDATE=1 ./dist/yolocoder-jev-muse model
```

Configure MindsHub and select **muse-spark-1-3** as the coding model. The build selects **jev-1.13.0** for routing and **muse-spark-1-3** for the bounded small-edit writer. The primary model remains the user's saved configuration; the model picker does not change the compiled small-edit writer. Keep auto-update disabled when trying the review build so it cannot replace itself with a published main binary.

From an empty app directory or an existing YoloCoder web project, launch the absolute path to the built binary with `--web`. The normal CLI works too. No API key is stored in this repository.

## What changed

- Jev classifies bounded UI changes and selects initial context. Confident small edits use a short Muse conversation with exact literal replacements. Normal tasks can receive a bounded snapshot of application source. Errors or uncertainty leave the coding model able to obtain its own context.
- Completion requires an explicit `task_complete` boolean on normal edits, successful tool results, a complete response envelope, and the project's detected check. A closing sentence alone cannot finish a normal task. Prompts reconcile the whole request and use observed data schemas.
- Patch rejections include bounded path and remaining-edit feedback; elapsed-time feedback and one bounded diagnostic check support required continuations. Narrow transport/policy recovery has finite budgets and does not replay accepted edits.
- Literal replacements validate uniqueness and overlap before applying the batch. Compact-patch errors explain the anchor needed to repair an insertion.

## Integration with newer main

Main added a separate `/preselect` implementation, architecture notes, read-by-pattern, search result pre-reading, and rejected-patch records after the benchmark winner was frozen. These features are retained. In a Jev review build the winner's router takes precedence over `/preselect`, avoiding two selectors for one request. A normal build still uses `/preselect` when enabled.

The winner's stricter `task_complete` tool contract supersedes main's `turn_is_complete`/closing-note completion for normal tasks. Small UI edits retain the winner's closing-note contract. Main's search tools remain available on both paths. Plain builds omit the new router and small-edit writer unless the linker variables are set; normal completion/recovery changes still apply.

The integrated branch needs a fresh paired paid benchmark before its speed/delivery numbers can be claimed. Offline Go tests validate integration behavior, not model performance. The frozen winner scored **53/60 versus vanilla 45/60** on five unseen apps, with mean agent time **47.37s versus 70.58s**. Failures, including upstream errors, were retained. This tested the complete bundle, not Jev in isolation.
379 changes: 257 additions & 122 deletions internal/agent/agent.go

Large diffs are not rendered by default.

20 changes: 16 additions & 4 deletions internal/agent/chat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,13 @@ func TestRunnerOverChatCompletions(t *testing.T) {
if err := json.NewDecoder(request.Body).Decode(&body); err != nil {
t.Fatal(err)
}
// Normal request timing follows the intact conversation in either
// dialect; inspect the original tool result immediately before it.
notes := timingNotes(t, body.Messages)
if len(notes) != 1 || body.Messages[len(body.Messages)-1].Content != notes[0] {
t.Fatal("missing or accumulated normal timing metadata")
}
body.Messages = body.Messages[:len(body.Messages)-1]
requests++
writer.Header().Set("Content-Type", "application/json")
reply := func(message chatMessage) {
Expand All @@ -214,16 +221,21 @@ func TestRunnerOverChatCompletions(t *testing.T) {
t.Fatalf("tool result did not carry the file: %q", content)
}
patch := "@index.html\n-<title>Old</title>\n+<title>New</title>\n"
arguments, _ := json.Marshal(map[string]string{"patch": patch})
arguments, _ := json.Marshal(map[string]any{"patch": patch, "response_comment_for_user": "", "task_complete": false})
reply(chatMessage{Role: "assistant", ToolCalls: []chatToolCall{{
ID: "call_2", Type: "function",
Function: chatCallFunction{Name: "apply_diff", Arguments: string(arguments)},
}}})
case 3:
// And the edit's own result comes back the same way.
// Preserve the edit's tool result before the diagnostic user message.
last := body.Messages[len(body.Messages)-1]
if last.Role != "tool" || last.ToolCallID != "call_2" {
t.Fatalf("last message = %+v, want the edit's result", last)
content, _ := last.Content.(string)
if last.Role != "user" || !strings.Contains(content, "Diagnostic checkpoint skipped") {
t.Fatalf("last message = %+v, want the honest skipped diagnostic", last)
}
tool := body.Messages[len(body.Messages)-2]
if tool.Role != "tool" || tool.ToolCallID != "call_2" {
t.Fatalf("preceding message = %+v, want the edit's result", tool)
}
reply(chatMessage{Role: "assistant", Content: "Retitle"})
default:
Expand Down
80 changes: 60 additions & 20 deletions internal/agent/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ func (client *Client) Dialect() string {
}

func (client *Client) create(ctx context.Context, request responseRequest) (responseEnvelope, error) {
return client.createWithRetry(ctx, request, nil)
}

// retry is a turn-owned, one-use permission, shared by compatibility fallbacks.
// Coding sessions supply it; routing and other callers stay unchanged.
func (client *Client) createWithRetry(ctx context.Context, request responseRequest, retry func(int) bool) (responseEnvelope, error) {
request.Model = client.model
body := any(request)
if client.chat {
Expand All @@ -176,23 +182,10 @@ func (client *Client) create(ctx context.Context, request responseRequest) (resp
// Authorization header), so it is safe to trace.
label := requestLabel(request)
debug.Log("REQUEST "+label, string(payload))
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, client.endpoint, bytes.NewReader(payload))
if err != nil {
return responseEnvelope{}, err
}
httpRequest.Header.Set("Authorization", "Bearer "+client.apiKey)
httpRequest.Header.Set("Content-Type", "application/json")
httpRequest.Header.Set("Accept", "application/json")
response, err := client.http.Do(httpRequest)
if err != nil {
return responseEnvelope{}, fmt.Errorf("call LLM: %w", err)
}
defer response.Body.Close()
replyBody, err := io.ReadAll(io.LimitReader(response.Body, 8<<20))
response, replyBody, err := client.post(ctx, payload, label, retry)
if err != nil {
return responseEnvelope{}, err
}
debug.Log(fmt.Sprintf("RESPONSE %s (%s)", label, response.Status), string(replyBody))

// Check the status before parsing. Decoding first turned a 404 with
// an empty body into "decode LLM response: unexpected end of JSON
Expand All @@ -204,7 +197,7 @@ func (client *Client) create(ctx context.Context, request responseRequest) (resp
debug.Log("DIALECT", "the Responses API returned 404; switching to /v1/chat/completions")
client.chat = true
client.endpoint = chatEndpoint(client.baseURL)
return client.create(ctx, request)
return client.createWithRetry(ctx, request, retry)
}

// Some providers constrain generation to a tool call whenever tools
Expand All @@ -216,7 +209,7 @@ func (client *Client) create(ctx context.Context, request responseRequest) (resp
debug.Log("TOOLS", "the provider demanded a tool call the model would not make; asking again without tools")
request.Tools = nil
request.ToolChoice = ""
return client.create(ctx, request)
return client.createWithRetry(ctx, request, retry)
}

// Some providers won't take tool definitions and a JSON schema in the
Expand All @@ -225,7 +218,7 @@ func (client *Client) create(ctx context.Context, request responseRequest) (resp
if client.chat && !client.dropSchema && rejectsSchemaWithTools(replyBody) {
debug.Log("SCHEMA", "the provider rejects response_format alongside tools; describing the shape in the instructions instead")
client.dropSchema = true
return client.create(ctx, request)
return client.createWithRetry(ctx, request, retry)
}

envelope, parseErr := client.decode(replyBody)
Expand All @@ -238,6 +231,54 @@ func (client *Client) create(ctx context.Context, request responseRequest) (resp
return envelope, nil
}

// post reuses the exact encoded request and context after an eligible error.
// Transport/read errors and successful responses never grant a replay: delivery
// is ambiguous there, or the response may already contain usable tool output.
func (client *Client) post(ctx context.Context, payload []byte, label string, retry func(int) bool) (*http.Response, []byte, error) {
const responseLimit = 8 << 20
endpoint := client.endpoint
for {
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(payload))
if err != nil {
return nil, nil, err
}
httpRequest.Header.Set("Authorization", "Bearer "+client.apiKey)
httpRequest.Header.Set("Content-Type", "application/json")
httpRequest.Header.Set("Accept", "application/json")
response, err := client.http.Do(httpRequest)
if err != nil {
return nil, nil, fmt.Errorf("call LLM: %w", err)
}
replyBody, err := io.ReadAll(io.LimitReader(response.Body, responseLimit))
response.Body.Close()
if err != nil {
return nil, nil, err
}
debug.Log(fmt.Sprintf("RESPONSE %s (%s)", label, response.Status), string(replyBody))
// Reaching the read cap may hide an incomplete body, so do not replay it.
eligible := response.StatusCode == http.StatusBadGateway || response.StatusCode == 524 ||
(response.StatusCode == http.StatusServiceUnavailable && client.model == "muse-spark-1-3" && musePolicyUnavailable(replyBody))
if eligible && len(replyBody) < responseLimit && ctx.Err() == nil && retry != nil && retry(response.StatusCode) {
if err := ctx.Err(); err != nil {
return nil, nil, fmt.Errorf("call LLM: %w", err)
}
continue
}
return response, replyBody, nil
}
}

func musePolicyUnavailable(body []byte) bool {
const message = "The model policy is temporarily unavailable. Please retry shortly."
if strings.TrimSpace(string(body)) == message {
return true
}
var reply struct {
Error *apiError `json:"error"`
}
return json.Unmarshal(body, &reply) == nil && reply.Error != nil && reply.Error.Message == message
}

// decode reads a reply in whichever dialect this client speaks, returning
// it in the Responses shape the rest of the agent works with.
func (client *Client) decode(body []byte) (responseEnvelope, error) {
Expand Down Expand Up @@ -316,9 +357,8 @@ func (response responseEnvelope) calls() []responseItem {
}

// cutOff reports whether the model ran out of output room before it
// finished, rather than deciding anything. The reply then carries no
// usable text — an empty message, or none at all — and the turn should
// ask again, shorter, instead of failing over it.
// finished, rather than deciding anything. An incomplete envelope may
// still contain text or tool calls; callers decide how to continue.
func (response responseEnvelope) cutOff() bool {
if response.IncompleteDetails == nil {
return false
Expand Down
20 changes: 9 additions & 11 deletions internal/agent/complete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func applyWith(t *testing.T, session *changeSession, arguments map[string]any) s
if err != nil {
t.Fatal(err)
}
output, _ := session.applyDiff(responseItem{Name: "apply_diff", Arguments: string(encoded)})
output, _, _ := session.applyDiff(responseItem{Name: "apply_diff", Arguments: string(encoded)})
return output
}

Expand All @@ -34,34 +34,32 @@ func TestAFinishedEditEndsTheTurn(t *testing.T) {
session := sessionOn(t, "a.css", ".a {\n color: red;\n}\n")
applyWith(t, session, map[string]any{
"patch": "@a.css\n- color: red;\n+ color: blue;\n",
"turn_is_complete": true,
"task_complete": true,
"response_comment_for_user": "Made it blue.",
})
if session.closing != "Made it blue." {
t.Fatalf("closing = %q, so the turn would spend a round saying done", session.closing)
}
}

// A model that fills the reply but leaves the flag false is still
// finished — that was the only signal there was, and honouring it costs
// nothing.
func TestAReplyWithoutTheFlagStillEndsTheTurn(t *testing.T) {
// A closing note alone must not end an unfinished normal task.
func TestAReplyWithoutTheFlagDoesNotCompleteTheTask(t *testing.T) {
session := sessionOn(t, "a.css", ".a {\n color: red;\n}\n")
applyWith(t, session, map[string]any{
"patch": "@a.css\n- color: red;\n+ color: blue;\n",
"turn_is_complete": false,
"task_complete": false,
"response_comment_for_user": "Made it blue.",
})
if session.closing != "Made it blue." {
t.Fatalf("closing = %q", session.closing)
if session.complete {
t.Fatal("closing note completed an unfinished task")
}
}

func TestAnUnfinishedEditDoesNotEndTheTurn(t *testing.T) {
session := sessionOn(t, "a.css", ".a {\n color: red;\n}\n")
applyWith(t, session, map[string]any{
"patch": "@a.css\n- color: red;\n+ color: blue;\n",
"turn_is_complete": false,
"task_complete": false,
"response_comment_for_user": "",
})
if session.closing != "" {
Expand All @@ -74,7 +72,7 @@ func TestAFailedEditCannotEndTheTurn(t *testing.T) {
session := sessionOn(t, "a.css", ".a {\n color: red;\n}\n")
applyWith(t, session, map[string]any{
"patch": "@a.css\n- color: nowhere;\n+ color: blue;\n",
"turn_is_complete": true,
"task_complete": true,
"response_comment_for_user": "Made it blue.",
})
if session.closing != "" {
Expand Down
Loading
Loading