diff --git a/internal/loki/parity_turn_test.go b/internal/loki/parity_turn_test.go new file mode 100644 index 0000000..1312487 --- /dev/null +++ b/internal/loki/parity_turn_test.go @@ -0,0 +1,282 @@ +package loki + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "regexp" + "strings" + "sync" + "testing" + "time" +) + +// parity_turn_test.go — journaux de référence d'un TOUR COMPLET, tel que le +// voit l'interface (lot 6 de la migration Rust). +// +// Chaque scénario passe par la vraie route /api/chat/send du Go, contre un +// faux moteur : le journal d'affichage du tour (deltas, une fois le tour fini +// et coalescé), l'historique rangé et les corps envoyés au moteur sont rangés +// dans rust/loki-rs/tests/parity_turn/.json, avec ce que la façade +// demandera au Go pendant le même tour (réponses de /internal/v1/turn/start +// et /turn/view). Le test Rust (tests/lot6.rs, parite_du_tour_complet) rejoue +// le tour par chat::generate et doit retrouver ces journaux. +// +// LOKI_PARITY_UPDATE=1 go test ./internal/loki -run TestPariteTourComplet + +const parityTurnDir = "../../rust/loki-rs/tests/parity_turn" + +type parityTurnScenario struct { + Name string `json:"name"` + Cfg map[string]string `json:"cfg"` + // Agent : mode agent allumé (réglage de la machine, clé d'état « agent »). + Agent bool `json:"agent,omitempty"` + // Code : discussion en mode Code, avec ces critères d'acceptation. + Code bool `json:"code,omitempty"` + Criteria []Criterion `json:"criteria,omitempty"` + Send map[string]any `json:"send"` + Replies []parityReply `json:"replies"` + ToolResults map[string]string `json:"tool_results"` +} + +type parityTurnExpect struct { + Requests []any `json:"requests"` + Log []map[string]any `json:"log"` + Messages []Message `json:"messages"` + Status int `json:"status"` +} + +type parityTurnFixture struct { + parityTurnScenario + // Réponses du Go à la façade pendant ce tour. + Start any `json:"start"` + View any `json:"view"` + Verifier any `json:"verifier,omitempty"` + Expect parityTurnExpect `json:"expect"` +} + +func parityTurnScenarios() []parityTurnScenario { + send := func(msg string) map[string]any { return map[string]any{"message": msg} } + base := func(name string, s map[string]any, replies ...parityReply) parityTurnScenario { + s["cid"] = "c-" + name // un envoi par scénario : jamais pris pour un réessai + return parityTurnScenario{ + Name: name, Cfg: map[string]string{"MEM_MODE": "off"}, Send: s, Replies: replies, + ToolResults: map[string]string{"bash": "a.txt\nb.txt", "read": "1: ligne"}, + } + } + agent := func(sc parityTurnScenario) parityTurnScenario { + sc.Agent = true + return sc + } + code := func(sc parityTurnScenario, crit ...Criterion) parityTurnScenario { + sc.Agent, sc.Code, sc.Criteria = true, true, crit + return sc + } + return []parityTurnScenario{ + base("reponse_simple", send("Bonjour"), + pOK(pContent("Bon"), pContent("jour !"), pFinish("stop"))), + base("raisonnement", send("Combien ?"), + pOK(pReasoning("Je compte."), pContent("Quatre."), pFinish("stop"))), + agent(base("outil_puis_reponse", send("Liste les fichiers"), + pOK(pToolCall(0, "c1", "bash", `{"command":"ls"}`), pFinish("tool_calls")), + pOK(pContent("Deux fichiers."), pFinish("stop")))), + agent(base("pense_sans_agir", send("Cherche"), + pOK(pReasoning("Il faudrait lister."), pFinish("stop")), + pOK(pContent("Voilà."), pFinish("stop")))), + code(base("code_contrat_deja_tenu", send("Corrige le test"), + pOK(pContent("Corrigé."), pFinish("stop"))), + Criterion{ID: 1, Text: "les tests passent", Status: "passed"}), + code(base("code_contrat_abandonne", send("Construis le jeu"), + pOK(pContent("r0"), pFinish("stop")), pOK(pContent("r1"), pFinish("stop")), + pOK(pContent("r2"), pFinish("stop")), pOK(pContent("v1"), pFinish("stop")), + pOK(pContent("r3"), pFinish("stop")), pOK(pContent("v2"), pFinish("stop")), + pOK(pContent("r4"), pFinish("stop")), pOK(pContent("v3"), pFinish("stop"))), + Criterion{ID: 1, Text: "le build passe", Status: "pending"}), + base("erreur_moteur", send("Bonjour"), + parityReply{Status: 400, Lines: []string{`{"error":{"code":400,"message":"requête invalide","type":"invalid_request_error"}}`}}), + } +} + +// parityTurnLog : le journal du tour, mesures et horaires masqués. +func parityTurnLog(evs []LogEvent) []map[string]any { + out := make([]map[string]any, 0, len(evs)) + for _, e := range evs { + d := map[string]any{} + for k, v := range e.Delta { + switch k { + case "stats": + v = "…" + case "ts0": + continue // horodatage du premier fragment coalescé + } + d[k] = v + } + out = append(out, d) + } + return out +} + +func runParityTurn(t *testing.T, sc parityTurnScenario) parityTurnFixture { + t.Helper() + testHome(t) + t.Setenv(internalTokenEnv, jetonTest) + var ( + mu sync.Mutex + requests []any + next int + ) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/health" { + io.WriteString(w, `{"status":"ok"}`) + return + } + if !strings.HasSuffix(r.URL.Path, "/chat/completions") { + http.NotFound(w, r) + return + } + raw, _ := io.ReadAll(r.Body) + var body any + _ = json.Unmarshal(raw, &body) + mu.Lock() + requests = append(requests, body) + i := next + next++ + mu.Unlock() + if i >= len(sc.Replies) { + t.Errorf("%s : requête %d non scriptée", sc.Name, i+1) + http.Error(w, "non scripté", http.StatusTeapot) + return + } + rep := sc.Replies[i] + if rep.Status == 200 { + w.Header().Set("Content-Type", "text/event-stream") + } + w.WriteHeader(rep.Status) + var b strings.Builder + for _, l := range rep.Lines { + b.WriteString(l + "\n\n") + } + io.WriteString(w, b.String()) + })) + t.Cleanup(srv.Close) + u, _ := url.Parse(srv.URL) + cfg := map[string]string{"PORT": u.Port()} + for k, v := range sc.Cfg { + cfg[k] = v + } + if err := WriteConfig(cfg); err != nil { + t.Fatal(err) + } + if err := setAgentEnabled(sc.Agent); err != nil { + t.Fatal(err) + } + execToolHook = func(_ context.Context, name string, _ map[string]any, _ string, _ Caps) toolExecResult { + if r, ok := sc.ToolResults[name]; ok { + return toolExecResult{Result: r} + } + return toolExecResult{Result: "[erreur] outil inconnu : " + name} + } + t.Cleanup(func() { execToolHook = nil }) + + conv.Reset() + if sc.Code { + id := convEnsureActive() + if err := setConvMode(id, "code"); err != nil { + t.Fatal(err) + } + critSave(id, sc.Criteria) + } + body, _ := json.Marshal(sc.Send) + // Ce que la façade obtiendra du Go pour ce même envoi. + start := turnJSON(t, "/internal/v1/turn/start", string(body), 200) + caps, _ := json.Marshal(map[string]any{"caps": start["turn_caps"]}) + view := turnJSON(t, "/internal/v1/turn/view", string(caps), 200) + var verifier any + if sc.Code { + verifier = turnJSON(t, "/internal/v1/turn/verifier", string(caps), 200) + } + + w := httptest.NewRecorder() + handleChatSend(w, httptest.NewRequest("POST", "/api/chat/send", bytes.NewReader(body))) + deadline := time.Now().Add(10 * time.Second) + for { + conv.mu.Lock() + done := !conv.Generating + conv.mu.Unlock() + if done || time.Now().After(deadline) { + break + } + time.Sleep(10 * time.Millisecond) + } + persistQ.flush() + conv.mu.Lock() + exp := parityTurnExpect{ + Requests: requests, + Log: parityTurnLog(conv.Log), + Messages: append([]Message{}, conv.Messages...), + Status: w.Code, + } + conv.mu.Unlock() + if exp.Requests == nil { + exp.Requests = []any{} + } + return parityTurnFixture{parityTurnScenario: sc, Start: start, View: view, Verifier: verifier, Expect: exp} +} + +// parityMasks : ce que le prompt de la machine porte de propre à CE lancement +// (dossiers temporaires, discussion, date, hôte). La façade reçoit ce prompt +// tel quel par /turn/view : masqué ici, il l'est des deux côtés. +var parityMasks = []struct { + re *regexp.Regexp + with string +}{ + {regexp.MustCompile(`(/private)?` + regexp.QuoteMeta(filepath.Clean(os.TempDir())) + `/Test[^/"\\]*/[0-9]+`), "{{TMP}}"}, + {regexp.MustCompile(`\bc[0-9]{15,}\b`), "{{CONV}}"}, + {regexp.MustCompile(`Date: [0-9]{4}-[0-9]{2}-[0-9]{2}`), "Date: {{DATE}}"}, + {regexp.MustCompile(`Machine: host=[^,]*, [a-z0-9]+/[a-z0-9]+(, user=[^,.]*)?`), "Machine: host={{HOST}}"}, +} + +func parityMask(b []byte) []byte { + for _, m := range parityMasks { + b = m.re.ReplaceAll(b, []byte(m.with)) + } + return b +} + +func TestPariteTourComplet(t *testing.T) { + update := os.Getenv("LOKI_PARITY_UPDATE") == "1" + for _, sc := range parityTurnScenarios() { + t.Run(sc.Name, func(t *testing.T) { + got, err := parityJSON(runParityTurn(t, sc)) + if err != nil { + t.Fatal(err) + } + got = parityMask(got) + path := filepath.Join(parityTurnDir, sc.Name+".json") + if update { + if err := os.MkdirAll(parityTurnDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, got, 0o644); err != nil { + t.Fatal(err) + } + return + } + want, err := os.ReadFile(path) + if err != nil { + t.Fatalf("journal de référence absent (%v) : LOKI_PARITY_UPDATE=1 pour le créer", err) + } + if !bytes.Equal(got, want) { + t.Errorf("le tour Go ne produit plus le journal rangé dans %s.\n"+ + "Si c'est voulu : LOKI_PARITY_UPDATE=1 go test ./internal/loki -run TestPariteTourComplet,\n"+ + "puis cargo test --test lot6 parite_du_tour_complet dit si le port Rust suit.\n--- obtenu ---\n%s", path, got) + } + }) + } +} diff --git a/rust/loki-rs/tests/common/fakego.rs b/rust/loki-rs/tests/common/fakego.rs index 0694f45..19b92dd 100644 --- a/rust/loki-rs/tests/common/fakego.rs +++ b/rust/loki-rs/tests/common/fakego.rs @@ -56,6 +56,12 @@ pub struct FakeGo { pub tool_log: Mutex>, /// Le Go garde la conversation (raison), sinon la façade la tient. pub go_keeps_chat: Mutex>, + /// Réponses imposées de /turn/start et /turn/view (journaux de référence + /// du Go), et résultat par outil. + pub turn_start: Mutex>, + pub turn_view: Mutex>, + pub turn_verifier: Mutex>, + pub tool_results: Mutex>, } impl FakeGo { @@ -219,6 +225,9 @@ async fn handle(st: Arc, req: Request) -> Response if msg.trim().is_empty() { return resp(400, r#"{"error":"message vide"}"#); } + if let Some(v) = st.turn_start.lock().unwrap().clone() { + return resp(200, v.to_string()); + } let code = *st.code.lock().unwrap(); let caps = serde_json::json!({"Agent": true, "Mem": "off", "Code": code, "Role": ""}); let out = serde_json::json!({ @@ -229,6 +238,9 @@ async fn handle(st: Arc, req: Request) -> Response } "/internal/v1/turn/view" => { st.turn_log.lock().unwrap().push("view".into()); + if let Some(v) = st.turn_view.lock().unwrap().clone() { + return resp(200, v.to_string()); + } let out = serde_json::json!({ "head": [{"role": "system", "content": "Tu es Loki."}], "prefix": "PRÉAMBULE", @@ -258,6 +270,9 @@ async fn handle(st: Arc, req: Request) -> Response let body = req.into_body().collect().await.unwrap().to_bytes(); let v: serde_json::Value = serde_json::from_slice(&body).unwrap_or_default(); st.turn_log.lock().unwrap().push("verifier".into()); + if let Some(out) = st.turn_verifier.lock().unwrap().clone() { + return resp(200, out.to_string()); + } let mut caps = v["caps"].clone(); caps["Role"] = "verifier".into(); let out = serde_json::json!({ @@ -317,11 +332,9 @@ async fn handle(st: Arc, req: Request) -> Response *val = serde_json::to_vec(&list).unwrap(); } } - let result = st - .tool_result - .lock() - .unwrap() - .clone() + let by_name = st.tool_results.lock().unwrap().get(name).cloned(); + let result = by_name + .or_else(|| st.tool_result.lock().unwrap().clone()) .unwrap_or_else(|| format!("exec:{}", v["name"].as_str().unwrap())); let out = serde_json::json!({ "label": v["args"]["command"], diff --git a/rust/loki-rs/tests/lot6.rs b/rust/loki-rs/tests/lot6.rs index 9532635..c63b16b 100644 --- a/rust/loki-rs/tests/lot6.rs +++ b/rust/loki-rs/tests/lot6.rs @@ -554,6 +554,17 @@ async fn fake_engine_gated( g.acquire().await.unwrap().forget(); } let out = replies.lock().unwrap().pop_front().unwrap_or_default(); + // « STATUS 400\n » : refus du moteur. + if let Some(rest) = out.strip_prefix("STATUS ") + && let Some((code, body)) = rest.split_once('\n') + { + let mut r = hyper::Response::new(Full::new(bytes::Bytes::from( + body.to_string(), + ))); + *r.status_mut() = + hyper::StatusCode::from_u16(code.parse().unwrap()).unwrap(); + return Ok(r); + } ok(out) } }, @@ -1443,3 +1454,117 @@ async fn deverrouillage_recharge_la_discussion_chargee_vide() { // Déjà rempli : jamais rechargé par-dessus. assert_eq!(s.reload_if_empty().await, Ok(false)); } + +/// Parité d'un tour COMPLET avec le Go (`tests/parity_turn/*.json`, produits +/// par `TestPariteTourComplet` en passant par la vraie route /api/chat/send) : +/// le Go de test sert les mêmes réponses de /turn/start et /turn/view que le +/// vrai, le moteur les mêmes flux, les outils les mêmes résultats. Doivent se +/// retrouver : le journal d'affichage du tour (mesures masquées), l'historique +/// rangé, et les corps envoyés au moteur. +#[tokio::test] +async fn parite_du_tour_complet() { + let dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/parity_turn"); + let mut paths: Vec<_> = std::fs::read_dir(&dir) + .unwrap() + .map(|e| e.unwrap().path()) + .filter(|p| p.extension().is_some_and(|x| x == "json")) + .collect(); + paths.sort(); + assert!(!paths.is_empty()); + let mut failures = Vec::new(); + for p in &paths { + let fx: serde_json::Value = serde_json::from_slice(&std::fs::read(p).unwrap()).unwrap(); + let name = fx["name"].as_str().unwrap().to_string(); + let replies: Vec = fx["replies"] + .as_array() + .unwrap() + .iter() + .map(|r| { + let body: String = r["lines"] + .as_array() + .unwrap() + .iter() + .map(|l| format!("{}\n\n", l.as_str().unwrap())) + .collect(); + match r["status"].as_u64().unwrap() { + 200 => body, + s => format!("STATUS {s}\n{body}"), + } + }) + .collect(); + let (url, seen) = fake_engine(replies).await; + let (svc, go, _stop, _t) = service(Some(&url)).await; + *go.turn_start.lock().unwrap() = Some(fx["start"].clone()); + *go.turn_view.lock().unwrap() = Some(fx["view"].clone()); + *go.tool_results.lock().unwrap() = + serde_json::from_value(fx["tool_results"].clone()).unwrap(); + if !fx["verifier"].is_null() { + *go.turn_verifier.lock().unwrap() = Some(fx["verifier"].clone()); + } + if let Some(c) = fx.get("criteria") { + let id = svc.sessions.conv().id(); + go.set("chat", &format!("crit:{id}"), &c.to_string()); + } + let sent = svc.send(fx["send"].clone()).await; + let status = if sent.is_ok() { 200 } else { 0 }; + until_done(&svc, 1).await; + until(|| go.turn_log.lock().unwrap().contains(&"done".to_string())).await; + + let exp = &fx["expect"]; + let mut diffs = Vec::new(); + if status != exp["status"].as_u64().unwrap() { + diffs.push(format!("statut {status}, attendu {}", exp["status"])); + } + let requests: Vec = seen + .lock() + .unwrap() + .iter() + .map(|(_, b)| b.clone()) + .collect(); + if serde_json::Value::Array(requests.clone()) != exp["requests"] { + diffs.push(format!( + "requêtes :\n obtenu {}\n attendu {}", + serde_json::Value::Array(requests), + exp["requests"] + )); + } + let log: Vec = svc.sessions.conv().with_state(|st| { + st.log + .iter() + .map(|e| { + let mut d = e.delta.clone(); + d.remove("ts0"); + if d.contains_key("stats") { + d.insert("stats".into(), serde_json::json!("…")); + } + serde_json::Value::Object(d) + }) + .collect() + }); + if serde_json::Value::Array(log.clone()) != exp["log"] { + diffs.push(format!( + "journal :\n obtenu {}\n attendu {}", + serde_json::Value::Array(log), + exp["log"] + )); + } + let msgs = svc.sessions.conv().with_state(|st| st.messages.clone()); + let msgs = serde_json::to_value(&msgs).unwrap(); + if msgs != exp["messages"] { + diffs.push(format!( + "historique :\n obtenu {msgs}\n attendu {}", + exp["messages"] + )); + } + if !diffs.is_empty() { + failures.push(format!("{name} :\n{}", diffs.join("\n"))); + } + } + assert!( + failures.is_empty(), + "{} tour(s) sur {} divergent du Go :\n\n{}", + failures.len(), + paths.len(), + failures.join("\n\n") + ); +} diff --git a/rust/loki-rs/tests/parity_turn/code_contrat_abandonne.json b/rust/loki-rs/tests/parity_turn/code_contrat_abandonne.json new file mode 100644 index 0000000..c83a08c --- /dev/null +++ b/rust/loki-rs/tests/parity_turn/code_contrat_abandonne.json @@ -0,0 +1,4498 @@ +{ + "name": "code_contrat_abandonne", + "cfg": { + "MEM_MODE": "off" + }, + "agent": true, + "code": true, + "criteria": [ + { + "id": 1, + "text": "le build passe", + "status": "pending" + } + ], + "send": { + "cid": "c-code_contrat_abandonne", + "message": "Construis le jeu" + }, + "replies": [ + { + "status": 200, + "lines": [ + "data: {\"choices\":[{\"delta\":{\"content\":\"r0\"}}]}", + "data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"completion_tokens\":5,\"prompt_tokens\":100}}", + "data: [DONE]" + ] + }, + { + "status": 200, + "lines": [ + "data: {\"choices\":[{\"delta\":{\"content\":\"r1\"}}]}", + "data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"completion_tokens\":5,\"prompt_tokens\":100}}", + "data: [DONE]" + ] + }, + { + "status": 200, + "lines": [ + "data: {\"choices\":[{\"delta\":{\"content\":\"r2\"}}]}", + "data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"completion_tokens\":5,\"prompt_tokens\":100}}", + "data: [DONE]" + ] + }, + { + "status": 200, + "lines": [ + "data: {\"choices\":[{\"delta\":{\"content\":\"v1\"}}]}", + "data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"completion_tokens\":5,\"prompt_tokens\":100}}", + "data: [DONE]" + ] + }, + { + "status": 200, + "lines": [ + "data: {\"choices\":[{\"delta\":{\"content\":\"r3\"}}]}", + "data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"completion_tokens\":5,\"prompt_tokens\":100}}", + "data: [DONE]" + ] + }, + { + "status": 200, + "lines": [ + "data: {\"choices\":[{\"delta\":{\"content\":\"v2\"}}]}", + "data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"completion_tokens\":5,\"prompt_tokens\":100}}", + "data: [DONE]" + ] + }, + { + "status": 200, + "lines": [ + "data: {\"choices\":[{\"delta\":{\"content\":\"r4\"}}]}", + "data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"completion_tokens\":5,\"prompt_tokens\":100}}", + "data: [DONE]" + ] + }, + { + "status": 200, + "lines": [ + "data: {\"choices\":[{\"delta\":{\"content\":\"v3\"}}]}", + "data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"completion_tokens\":5,\"prompt_tokens\":100}}", + "data: [DONE]" + ] + } + ], + "tool_results": { + "bash": "a.txt\nb.txt", + "read": "1: ligne" + }, + "start": { + "caps": { + "Agent": true, + "Code": true, + "ComputerUse": false, + "Internet": false, + "Mem": "off", + "Role": "" + }, + "code_hint": false, + "content": "Construis le jeu", + "files": [], + "model": "", + "turn_caps": { + "Agent": true, + "Code": true, + "ComputerUse": false, + "Internet": false, + "Mem": "off", + "Role": "builder" + } + }, + "view": { + "head": [], + "prefix": "You are Loki, an expert assistant operating directly on this machine with real tools.\n\nYou are in CODE MODE: an autonomous coding agent working in this discussion's folder.\n\nMethod — in this order:\n1. If the task has no acceptance criteria yet, set them FIRST with the criteria tool (action=add): 2-6 short, testable statements of what DONE means. For a trivial task (one obvious change), skip criteria and just do it.\n2. Explore before you change: read the relevant files (read, grep, glob, git_status). Never edit a file you have not read.\n3. Implement with edit (small patches) or write (new files). Match the style of the surrounding code.\n4. Prove it: run the build/tests with bash. A change that was never run is not done. For a web page or UI, start the dev server with bash_bg then use web_screenshot on its localhost URL — a headless Chromium is ALREADY installed, so NEVER install puppeteer, playwright or a browser.\n5. Report briefly what changed and how you verified it.\n\nRules:\n- PLAN ONCE. If the task needs a plan, write it to PLAN.md in ONE short write, then follow it. Between tool calls, think one or two sentences at most — NEVER restate or re-derive the plan: it is in PLAN.md and in your criteria, read them instead.\n- Write each file COMPLETE in a single write call. Many small writes waste turns.\n- When a criterion is done AND checked, mark it completed (criteria set). Verification starts once none is left pending; only it may mark passed.\n- If a command fails, read the error and fix the cause; do not retry the same command unchanged.\n- Ask the user (ask tool) only for decisions that are genuinely theirs; decide the rest yourself.\n- Making the change is your job; shipping it is not. Without an explicit request, never commit, push, reset or rebase, never deploy, restart a service or replace a running binary. Read-only inspection (git_status, git_diff) is fine and encouraged.\n\nThe shell is bash: use its syntax.\nFor anything about the system or files, use bash instead of guessing. Act immediately — call the right tool, then answer. Never end your turn after only thinking. Be concise.\nTo give the user a file, link it in Markdown with its path relative to your working directory — [le rapport](rapport.pdf) — which downloads it. A raw server path is useless: they read you in a browser.\nYou can schedule work for yourself with task_create — only for what must recur or happen later, never a one-off you can do now. A script from your scripts folder runs as a task with no model at all.\n\nDate: {{DATE}}\n\nMachine: host={{HOST}}, cwd={{TMP}}/workspace/discussions/{{CONV}}. This is your working folder: relative paths in write/edit/bash resolve here, and it is the DEFAULT place for scratch work — notes, outputs, a clone, a test. But it is DISPOSABLE: deleting the discussion wipes it. Any script you want to KEEP (or schedule), write it into your scripts folder {{TMP}}/scripts instead — a separate folder a workspace wipe won't touch; you write and run scripts there normally. Do NOT install or write files into system directories such as /usr/local/bin, /usr, /bin or /etc: those need root and are not yours. Only use an absolute path outside this folder (except your scripts folder) when the user explicitly named that location.", + "tools": [ + { + "function": { + "description": "Run a shell command (bash syntax) and return stdout, stderr and exit code: inspect the system, read files and logs, run scripts. To CREATE or REWRITE a file use write instead — never echo/cat/python -c. Avoid destructive commands unless asked.", + "name": "bash", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + }, + "timeout": { + "description": "Timeout s (default 30, max 300)", + "type": "integer" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Create or replace a file with the exact content given (parent dirs created, content verbatim, no escaping). ALWAYS use this for a script or any text file — NEVER build one through the shell with echo, cat, python -c or Set-Content: quoting breaks.", + "name": "write", + "parameters": { + "properties": { + "content": { + "description": "Full content", + "type": "string" + }, + "file": { + "description": "Path", + "type": "string" + } + }, + "required": [ + "file", + "content" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Patch a file by exact replacement: old → new. old must appear EXACTLY once (add context to make it unique). Prefer this over rewriting a whole file.", + "name": "edit", + "parameters": { + "properties": { + "file": { + "description": "Path", + "type": "string" + }, + "new": { + "description": "Replacement", + "type": "string" + }, + "old": { + "description": "Exact text to replace (unique)", + "type": "string" + } + }, + "required": [ + "file", + "old", + "new" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Bring back verbatim an earlier block archived when this conversation was compacted, from the id shown as recall:rN in the summary.", + "name": "recall", + "parameters": { + "properties": { + "id": { + "description": "Block id, e.g. \"r7\".", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Find an archived block and its recall id by keywords, when it is not listed in the current summary. Then call recall(id).", + "name": "recall_search", + "parameters": { + "properties": { + "limit": { + "description": "Default 5, max 20", + "type": "integer" + }, + "query": { + "description": "Keywords describing the block.", + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List your scheduled tasks with their state, next run, and last result. Pass an id to get that task's full last report.", + "name": "task_list", + "parameters": { + "properties": { + "id": { + "description": "Show only this task, with its full last report", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Schedule work to run later, once or repeating. Give 'prompt' for an AI task, OR 'script' (a saved script name) for a script task that runs with no model and no tokens.", + "name": "task_create", + "parameters": { + "properties": { + "at": { + "description": "One-time task at this local time: \"HH:MM\" (next occurrence) or \"YYYY-MM-DD HH:MM\"", + "type": "string" + }, + "enabled": { + "description": "Start active (default true)", + "type": "boolean" + }, + "in_minutes": { + "description": "One-time task, N minutes from now", + "type": "integer" + }, + "name": { + "type": "string" + }, + "prompt": { + "description": "AI task: self-contained instruction to run each time (omit if using script)", + "type": "string" + }, + "schedule": { + "description": "Repeating: \"@every 2h\" (interval), \"@every 1d@09:00\" (daily time), or a 5-field cron \"min hour dom mon dow\". Omit for a one-time task (use in_minutes or at)", + "type": "string" + }, + "script": { + "description": "Script task: name of a script file you put in the scripts folder, run each time with no AI involved", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Change one or more fields of a scheduled task by id.", + "name": "task_update", + "parameters": { + "properties": { + "enabled": { + "description": "false pauses, true resumes", + "type": "boolean" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "prompt": { + "type": "string" + }, + "schedule": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Delete a scheduled task by id.", + "name": "task_delete", + "parameters": { + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Read a file with 1-indexed line numbers (offset/limit for long files). ALWAYS use this instead of cat/type: output is bounded and reading a file is REQUIRED before you may edit or overwrite it.", + "name": "read", + "parameters": { + "properties": { + "file": { + "description": "Path (relative to the working folder)", + "type": "string" + }, + "limit": { + "description": "Lines (default 400, max 1000)", + "type": "integer" + }, + "offset": { + "description": "Start line (default 1)", + "type": "integer" + } + }, + "required": [ + "file" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Search file contents with a Go regular expression. Returns path:line: text matches (bounded). path may be a file or a directory (default: whole working folder).", + "name": "grep", + "parameters": { + "properties": { + "glob": { + "description": "Only files matching this glob (e.g. *.go)", + "type": "string" + }, + "path": { + "description": "File or directory to search (default .)", + "type": "string" + }, + "pattern": { + "description": "Go regexp", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List files matching a glob pattern (** supported, e.g. **/*.go), most recently modified first.", + "name": "glob", + "parameters": { + "properties": { + "pattern": { + "description": "Glob, e.g. src/**/*.ts", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Ask the user a clarifying question with 2-4 short options, then END your turn. Their answer arrives as the next user message. Use when a decision is genuinely theirs (scope, destructive change, ambiguous requirement) — not for things you can check yourself.", + "name": "ask", + "parameters": { + "properties": { + "options": { + "description": "2-4 short answer options", + "items": { + "type": "string" + }, + "type": "array" + }, + "question": { + "description": "The question", + "type": "string" + } + }, + "required": [ + "question" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Start a shell command in the BACKGROUND (dev server, watcher, long build) and return immediately with a job id. Output goes to a log file; check it with bash_tail. Use this instead of appending '&' in bash.", + "name": "bash_bg", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Read the last lines of a background job's output and its status (running/exited). kill:true stops the job.", + "name": "bash_tail", + "parameters": { + "properties": { + "id": { + "description": "Job id from bash_bg", + "type": "string" + }, + "kill": { + "description": "Stop the job", + "type": "boolean" + }, + "lines": { + "description": "Lines to show (default 40, max 200)", + "type": "integer" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "git status of the working folder (branch + changed files, porcelain format).", + "name": "git_status", + "parameters": { + "properties": { + "dir": { + "description": "Repository subfolder (default: auto-detected)", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "git diff of the working folder (unstaged + staged). Optional file to diff a single path.", + "name": "git_diff", + "parameters": { + "properties": { + "dir": { + "description": "Repository subfolder (default: auto-detected)", + "type": "string" + }, + "file": { + "description": "Limit the diff to this path", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Clone a git repository (https:// or git@) INTO the working folder of this discussion.", + "name": "git_clone", + "parameters": { + "properties": { + "dir": { + "description": "Target folder name (default: repo name)", + "type": "string" + }, + "url": { + "description": "Repository URL", + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Manage the acceptance criteria of the current task (the contract that defines DONE). action=add posits new criteria (texts[]), action=set updates one (id + status, optional note), action=list shows them, action=clear removes them all. Set criteria BEFORE building. Builder: mark each criterion completed once you have done AND checked it — verification only starts when nothing is left pending. Only the verification pass may mark passed or failed.", + "name": "criteria", + "parameters": { + "properties": { + "action": { + "enum": [ + "add", + "set", + "list", + "clear" + ], + "type": "string" + }, + "id": { + "description": "set: criterion id", + "type": "integer" + }, + "note": { + "description": "set: why it failed / how it was verified", + "type": "string" + }, + "status": { + "enum": [ + "pending", + "completed", + "passed", + "failed" + ], + "type": "string" + }, + "texts": { + "description": "add: one entry per criterion, short and testable", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "action" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Délègue une recherche ou une relecture à un rôle qui travaille dans son propre contexte et ne rend que sa réponse. Lecture seule.", + "name": "subagent", + "parameters": { + "properties": { + "role": { + "description": "explorer (trouver/cartographier), code-reviewer (relire un changement), planner (découper)", + "enum": [ + "explorer", + "code-reviewer", + "planner" + ], + "type": "string" + }, + "task": { + "description": "La question, précise et autonome : le rôle ne voit pas la discussion.", + "type": "string" + } + }, + "required": [ + "role", + "task" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Photographie une page web (JS exécuté) pour la MONTRER. La réponse donne la ligne markdown à recopier. Tu ne vois pas l'image.", + "name": "web_screenshot", + "parameters": { + "properties": { + "full_page": { + "description": "Page entière (lourd). Défaut false.", + "type": "boolean" + }, + "url": { + "description": "URL complète", + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" + } + }, + "type": "function" + } + ] + }, + "verifier": { + "caps": { + "Agent": true, + "Code": true, + "ComputerUse": false, + "Internet": false, + "Mem": "off", + "Role": "verifier" + }, + "system": "You are the VERIFICATION pass of code mode. You independently check the acceptance criteria against the actual code — you did not write it, trust nothing.\n\nFor each criterion still pending or failed:\n1. Understand what it actually requires.\n2. Check the real changes: git_diff, read the touched files, run the relevant build/test command with bash when applicable.\n3. Record the verdict with the criteria tool: action=set, status=passed (satisfied) or failed (with a note saying exactly what is missing — actionable, one line).\n\nRules:\n- Trivial or non-code criteria that are plainly satisfied: pass them without exploring.\n- Only fail a criterion that genuinely misses its requirement; a different-but-valid implementation passes.\n- Do NOT fix anything yourself. You only verify and report.\n- Be fast: check the diff first, read only what the criteria require.\n\nMachine: host={{HOST}}, cwd={{TMP}}/workspace/discussions/{{CONV}}. This is your working folder: relative paths in write/edit/bash resolve here, and it is the DEFAULT place for scratch work — notes, outputs, a clone, a test. But it is DISPOSABLE: deleting the discussion wipes it. Any script you want to KEEP (or schedule), write it into your scripts folder {{TMP}}/scripts instead — a separate folder a workspace wipe won't touch; you write and run scripts there normally. Do NOT install or write files into system directories such as /usr/local/bin, /usr, /bin or /etc: those need root and are not yours. Only use an absolute path outside this folder (except your scripts folder) when the user explicitly named that location.", + "tools": [ + { + "function": { + "description": "Run a shell command (bash syntax) and return stdout, stderr and exit code: inspect the system, read files and logs, run scripts. To CREATE or REWRITE a file use write instead — never echo/cat/python -c. Avoid destructive commands unless asked.", + "name": "bash", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + }, + "timeout": { + "description": "Timeout s (default 30, max 300)", + "type": "integer" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Read a file with 1-indexed line numbers (offset/limit for long files). ALWAYS use this instead of cat/type: output is bounded and reading a file is REQUIRED before you may edit or overwrite it.", + "name": "read", + "parameters": { + "properties": { + "file": { + "description": "Path (relative to the working folder)", + "type": "string" + }, + "limit": { + "description": "Lines (default 400, max 1000)", + "type": "integer" + }, + "offset": { + "description": "Start line (default 1)", + "type": "integer" + } + }, + "required": [ + "file" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Search file contents with a Go regular expression. Returns path:line: text matches (bounded). path may be a file or a directory (default: whole working folder).", + "name": "grep", + "parameters": { + "properties": { + "glob": { + "description": "Only files matching this glob (e.g. *.go)", + "type": "string" + }, + "path": { + "description": "File or directory to search (default .)", + "type": "string" + }, + "pattern": { + "description": "Go regexp", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List files matching a glob pattern (** supported, e.g. **/*.go), most recently modified first.", + "name": "glob", + "parameters": { + "properties": { + "pattern": { + "description": "Glob, e.g. src/**/*.ts", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "git status of the working folder (branch + changed files, porcelain format).", + "name": "git_status", + "parameters": { + "properties": { + "dir": { + "description": "Repository subfolder (default: auto-detected)", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "git diff of the working folder (unstaged + staged). Optional file to diff a single path.", + "name": "git_diff", + "parameters": { + "properties": { + "dir": { + "description": "Repository subfolder (default: auto-detected)", + "type": "string" + }, + "file": { + "description": "Limit the diff to this path", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Manage the acceptance criteria of the current task (the contract that defines DONE). action=add posits new criteria (texts[]), action=set updates one (id + status, optional note), action=list shows them, action=clear removes them all. Set criteria BEFORE building. Builder: mark each criterion completed once you have done AND checked it — verification only starts when nothing is left pending. Only the verification pass may mark passed or failed.", + "name": "criteria", + "parameters": { + "properties": { + "action": { + "enum": [ + "add", + "set", + "list", + "clear" + ], + "type": "string" + }, + "id": { + "description": "set: criterion id", + "type": "integer" + }, + "note": { + "description": "set: why it failed / how it was verified", + "type": "string" + }, + "status": { + "enum": [ + "pending", + "completed", + "passed", + "failed" + ], + "type": "string" + }, + "texts": { + "description": "add: one entry per criterion, short and testable", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "action" + ], + "type": "object" + } + }, + "type": "function" + } + ] + }, + "expect": { + "requests": [ + { + "messages": [ + { + "content": "You are Loki, an expert assistant operating directly on this machine with real tools.\n\nYou are in CODE MODE: an autonomous coding agent working in this discussion's folder.\n\nMethod — in this order:\n1. If the task has no acceptance criteria yet, set them FIRST with the criteria tool (action=add): 2-6 short, testable statements of what DONE means. For a trivial task (one obvious change), skip criteria and just do it.\n2. Explore before you change: read the relevant files (read, grep, glob, git_status). Never edit a file you have not read.\n3. Implement with edit (small patches) or write (new files). Match the style of the surrounding code.\n4. Prove it: run the build/tests with bash. A change that was never run is not done. For a web page or UI, start the dev server with bash_bg then use web_screenshot on its localhost URL — a headless Chromium is ALREADY installed, so NEVER install puppeteer, playwright or a browser.\n5. Report briefly what changed and how you verified it.\n\nRules:\n- PLAN ONCE. If the task needs a plan, write it to PLAN.md in ONE short write, then follow it. Between tool calls, think one or two sentences at most — NEVER restate or re-derive the plan: it is in PLAN.md and in your criteria, read them instead.\n- Write each file COMPLETE in a single write call. Many small writes waste turns.\n- When a criterion is done AND checked, mark it completed (criteria set). Verification starts once none is left pending; only it may mark passed.\n- If a command fails, read the error and fix the cause; do not retry the same command unchanged.\n- Ask the user (ask tool) only for decisions that are genuinely theirs; decide the rest yourself.\n- Making the change is your job; shipping it is not. Without an explicit request, never commit, push, reset or rebase, never deploy, restart a service or replace a running binary. Read-only inspection (git_status, git_diff) is fine and encouraged.\n\nThe shell is bash: use its syntax.\nFor anything about the system or files, use bash instead of guessing. Act immediately — call the right tool, then answer. Never end your turn after only thinking. Be concise.\nTo give the user a file, link it in Markdown with its path relative to your working directory — [le rapport](rapport.pdf) — which downloads it. A raw server path is useless: they read you in a browser.\nYou can schedule work for yourself with task_create — only for what must recur or happen later, never a one-off you can do now. A script from your scripts folder runs as a task with no model at all.\n\nDate: {{DATE}}\n\nMachine: host={{HOST}}, cwd={{TMP}}/workspace/discussions/{{CONV}}. This is your working folder: relative paths in write/edit/bash resolve here, and it is the DEFAULT place for scratch work — notes, outputs, a clone, a test. But it is DISPOSABLE: deleting the discussion wipes it. Any script you want to KEEP (or schedule), write it into your scripts folder {{TMP}}/scripts instead — a separate folder a workspace wipe won't touch; you write and run scripts there normally. Do NOT install or write files into system directories such as /usr/local/bin, /usr, /bin or /etc: those need root and are not yours. Only use an absolute path outside this folder (except your scripts folder) when the user explicitly named that location.", + "role": "system" + }, + { + "content": "Construis le jeu", + "role": "user" + } + ], + "model": "loki", + "parallel_tool_calls": false, + "stream": true, + "stream_options": { + "include_usage": true + }, + "temperature": 0.7, + "tools": [ + { + "function": { + "description": "Run a shell command (bash syntax) and return stdout, stderr and exit code: inspect the system, read files and logs, run scripts. To CREATE or REWRITE a file use write instead — never echo/cat/python -c. Avoid destructive commands unless asked.", + "name": "bash", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + }, + "timeout": { + "description": "Timeout s (default 30, max 300)", + "type": "integer" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Create or replace a file with the exact content given (parent dirs created, content verbatim, no escaping). ALWAYS use this for a script or any text file — NEVER build one through the shell with echo, cat, python -c or Set-Content: quoting breaks.", + "name": "write", + "parameters": { + "properties": { + "content": { + "description": "Full content", + "type": "string" + }, + "file": { + "description": "Path", + "type": "string" + } + }, + "required": [ + "file", + "content" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Patch a file by exact replacement: old → new. old must appear EXACTLY once (add context to make it unique). Prefer this over rewriting a whole file.", + "name": "edit", + "parameters": { + "properties": { + "file": { + "description": "Path", + "type": "string" + }, + "new": { + "description": "Replacement", + "type": "string" + }, + "old": { + "description": "Exact text to replace (unique)", + "type": "string" + } + }, + "required": [ + "file", + "old", + "new" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Bring back verbatim an earlier block archived when this conversation was compacted, from the id shown as recall:rN in the summary.", + "name": "recall", + "parameters": { + "properties": { + "id": { + "description": "Block id, e.g. \"r7\".", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Find an archived block and its recall id by keywords, when it is not listed in the current summary. Then call recall(id).", + "name": "recall_search", + "parameters": { + "properties": { + "limit": { + "description": "Default 5, max 20", + "type": "integer" + }, + "query": { + "description": "Keywords describing the block.", + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List your scheduled tasks with their state, next run, and last result. Pass an id to get that task's full last report.", + "name": "task_list", + "parameters": { + "properties": { + "id": { + "description": "Show only this task, with its full last report", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Schedule work to run later, once or repeating. Give 'prompt' for an AI task, OR 'script' (a saved script name) for a script task that runs with no model and no tokens.", + "name": "task_create", + "parameters": { + "properties": { + "at": { + "description": "One-time task at this local time: \"HH:MM\" (next occurrence) or \"YYYY-MM-DD HH:MM\"", + "type": "string" + }, + "enabled": { + "description": "Start active (default true)", + "type": "boolean" + }, + "in_minutes": { + "description": "One-time task, N minutes from now", + "type": "integer" + }, + "name": { + "type": "string" + }, + "prompt": { + "description": "AI task: self-contained instruction to run each time (omit if using script)", + "type": "string" + }, + "schedule": { + "description": "Repeating: \"@every 2h\" (interval), \"@every 1d@09:00\" (daily time), or a 5-field cron \"min hour dom mon dow\". Omit for a one-time task (use in_minutes or at)", + "type": "string" + }, + "script": { + "description": "Script task: name of a script file you put in the scripts folder, run each time with no AI involved", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Change one or more fields of a scheduled task by id.", + "name": "task_update", + "parameters": { + "properties": { + "enabled": { + "description": "false pauses, true resumes", + "type": "boolean" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "prompt": { + "type": "string" + }, + "schedule": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Delete a scheduled task by id.", + "name": "task_delete", + "parameters": { + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Read a file with 1-indexed line numbers (offset/limit for long files). ALWAYS use this instead of cat/type: output is bounded and reading a file is REQUIRED before you may edit or overwrite it.", + "name": "read", + "parameters": { + "properties": { + "file": { + "description": "Path (relative to the working folder)", + "type": "string" + }, + "limit": { + "description": "Lines (default 400, max 1000)", + "type": "integer" + }, + "offset": { + "description": "Start line (default 1)", + "type": "integer" + } + }, + "required": [ + "file" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Search file contents with a Go regular expression. Returns path:line: text matches (bounded). path may be a file or a directory (default: whole working folder).", + "name": "grep", + "parameters": { + "properties": { + "glob": { + "description": "Only files matching this glob (e.g. *.go)", + "type": "string" + }, + "path": { + "description": "File or directory to search (default .)", + "type": "string" + }, + "pattern": { + "description": "Go regexp", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List files matching a glob pattern (** supported, e.g. **/*.go), most recently modified first.", + "name": "glob", + "parameters": { + "properties": { + "pattern": { + "description": "Glob, e.g. src/**/*.ts", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Ask the user a clarifying question with 2-4 short options, then END your turn. Their answer arrives as the next user message. Use when a decision is genuinely theirs (scope, destructive change, ambiguous requirement) — not for things you can check yourself.", + "name": "ask", + "parameters": { + "properties": { + "options": { + "description": "2-4 short answer options", + "items": { + "type": "string" + }, + "type": "array" + }, + "question": { + "description": "The question", + "type": "string" + } + }, + "required": [ + "question" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Start a shell command in the BACKGROUND (dev server, watcher, long build) and return immediately with a job id. Output goes to a log file; check it with bash_tail. Use this instead of appending '&' in bash.", + "name": "bash_bg", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Read the last lines of a background job's output and its status (running/exited). kill:true stops the job.", + "name": "bash_tail", + "parameters": { + "properties": { + "id": { + "description": "Job id from bash_bg", + "type": "string" + }, + "kill": { + "description": "Stop the job", + "type": "boolean" + }, + "lines": { + "description": "Lines to show (default 40, max 200)", + "type": "integer" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "git status of the working folder (branch + changed files, porcelain format).", + "name": "git_status", + "parameters": { + "properties": { + "dir": { + "description": "Repository subfolder (default: auto-detected)", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "git diff of the working folder (unstaged + staged). Optional file to diff a single path.", + "name": "git_diff", + "parameters": { + "properties": { + "dir": { + "description": "Repository subfolder (default: auto-detected)", + "type": "string" + }, + "file": { + "description": "Limit the diff to this path", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Clone a git repository (https:// or git@) INTO the working folder of this discussion.", + "name": "git_clone", + "parameters": { + "properties": { + "dir": { + "description": "Target folder name (default: repo name)", + "type": "string" + }, + "url": { + "description": "Repository URL", + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Manage the acceptance criteria of the current task (the contract that defines DONE). action=add posits new criteria (texts[]), action=set updates one (id + status, optional note), action=list shows them, action=clear removes them all. Set criteria BEFORE building. Builder: mark each criterion completed once you have done AND checked it — verification only starts when nothing is left pending. Only the verification pass may mark passed or failed.", + "name": "criteria", + "parameters": { + "properties": { + "action": { + "enum": [ + "add", + "set", + "list", + "clear" + ], + "type": "string" + }, + "id": { + "description": "set: criterion id", + "type": "integer" + }, + "note": { + "description": "set: why it failed / how it was verified", + "type": "string" + }, + "status": { + "enum": [ + "pending", + "completed", + "passed", + "failed" + ], + "type": "string" + }, + "texts": { + "description": "add: one entry per criterion, short and testable", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "action" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Délègue une recherche ou une relecture à un rôle qui travaille dans son propre contexte et ne rend que sa réponse. Lecture seule.", + "name": "subagent", + "parameters": { + "properties": { + "role": { + "description": "explorer (trouver/cartographier), code-reviewer (relire un changement), planner (découper)", + "enum": [ + "explorer", + "code-reviewer", + "planner" + ], + "type": "string" + }, + "task": { + "description": "La question, précise et autonome : le rôle ne voit pas la discussion.", + "type": "string" + } + }, + "required": [ + "role", + "task" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Photographie une page web (JS exécuté) pour la MONTRER. La réponse donne la ligne markdown à recopier. Tu ne vois pas l'image.", + "name": "web_screenshot", + "parameters": { + "properties": { + "full_page": { + "description": "Page entière (lourd). Défaut false.", + "type": "boolean" + }, + "url": { + "description": "URL complète", + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" + } + }, + "type": "function" + } + ] + }, + { + "messages": [ + { + "content": "You are Loki, an expert assistant operating directly on this machine with real tools.\n\nYou are in CODE MODE: an autonomous coding agent working in this discussion's folder.\n\nMethod — in this order:\n1. If the task has no acceptance criteria yet, set them FIRST with the criteria tool (action=add): 2-6 short, testable statements of what DONE means. For a trivial task (one obvious change), skip criteria and just do it.\n2. Explore before you change: read the relevant files (read, grep, glob, git_status). Never edit a file you have not read.\n3. Implement with edit (small patches) or write (new files). Match the style of the surrounding code.\n4. Prove it: run the build/tests with bash. A change that was never run is not done. For a web page or UI, start the dev server with bash_bg then use web_screenshot on its localhost URL — a headless Chromium is ALREADY installed, so NEVER install puppeteer, playwright or a browser.\n5. Report briefly what changed and how you verified it.\n\nRules:\n- PLAN ONCE. If the task needs a plan, write it to PLAN.md in ONE short write, then follow it. Between tool calls, think one or two sentences at most — NEVER restate or re-derive the plan: it is in PLAN.md and in your criteria, read them instead.\n- Write each file COMPLETE in a single write call. Many small writes waste turns.\n- When a criterion is done AND checked, mark it completed (criteria set). Verification starts once none is left pending; only it may mark passed.\n- If a command fails, read the error and fix the cause; do not retry the same command unchanged.\n- Ask the user (ask tool) only for decisions that are genuinely theirs; decide the rest yourself.\n- Making the change is your job; shipping it is not. Without an explicit request, never commit, push, reset or rebase, never deploy, restart a service or replace a running binary. Read-only inspection (git_status, git_diff) is fine and encouraged.\n\nThe shell is bash: use its syntax.\nFor anything about the system or files, use bash instead of guessing. Act immediately — call the right tool, then answer. Never end your turn after only thinking. Be concise.\nTo give the user a file, link it in Markdown with its path relative to your working directory — [le rapport](rapport.pdf) — which downloads it. A raw server path is useless: they read you in a browser.\nYou can schedule work for yourself with task_create — only for what must recur or happen later, never a one-off you can do now. A script from your scripts folder runs as a task with no model at all.\n\nDate: {{DATE}}\n\nMachine: host={{HOST}}, cwd={{TMP}}/workspace/discussions/{{CONV}}. This is your working folder: relative paths in write/edit/bash resolve here, and it is the DEFAULT place for scratch work — notes, outputs, a clone, a test. But it is DISPOSABLE: deleting the discussion wipes it. Any script you want to KEEP (or schedule), write it into your scripts folder {{TMP}}/scripts instead — a separate folder a workspace wipe won't touch; you write and run scripts there normally. Do NOT install or write files into system directories such as /usr/local/bin, /usr, /bin or /etc: those need root and are not yours. Only use an absolute path outside this folder (except your scripts folder) when the user explicitly named that location.", + "role": "system" + }, + { + "content": "Construis le jeu", + "role": "user" + }, + { + "content": "r0", + "role": "assistant" + }, + { + "content": "Not done yet — these acceptance criteria are still open:\n[ ] #1 le build passe\n\nContinue working on them. Once one is done and checked, mark it completed (criteria action=set, status=completed). If you need the user's decision, use ask.", + "role": "user" + } + ], + "model": "loki", + "parallel_tool_calls": false, + "stream": true, + "stream_options": { + "include_usage": true + }, + "temperature": 0.7, + "tools": [ + { + "function": { + "description": "Run a shell command (bash syntax) and return stdout, stderr and exit code: inspect the system, read files and logs, run scripts. To CREATE or REWRITE a file use write instead — never echo/cat/python -c. Avoid destructive commands unless asked.", + "name": "bash", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + }, + "timeout": { + "description": "Timeout s (default 30, max 300)", + "type": "integer" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Create or replace a file with the exact content given (parent dirs created, content verbatim, no escaping). ALWAYS use this for a script or any text file — NEVER build one through the shell with echo, cat, python -c or Set-Content: quoting breaks.", + "name": "write", + "parameters": { + "properties": { + "content": { + "description": "Full content", + "type": "string" + }, + "file": { + "description": "Path", + "type": "string" + } + }, + "required": [ + "file", + "content" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Patch a file by exact replacement: old → new. old must appear EXACTLY once (add context to make it unique). Prefer this over rewriting a whole file.", + "name": "edit", + "parameters": { + "properties": { + "file": { + "description": "Path", + "type": "string" + }, + "new": { + "description": "Replacement", + "type": "string" + }, + "old": { + "description": "Exact text to replace (unique)", + "type": "string" + } + }, + "required": [ + "file", + "old", + "new" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Bring back verbatim an earlier block archived when this conversation was compacted, from the id shown as recall:rN in the summary.", + "name": "recall", + "parameters": { + "properties": { + "id": { + "description": "Block id, e.g. \"r7\".", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Find an archived block and its recall id by keywords, when it is not listed in the current summary. Then call recall(id).", + "name": "recall_search", + "parameters": { + "properties": { + "limit": { + "description": "Default 5, max 20", + "type": "integer" + }, + "query": { + "description": "Keywords describing the block.", + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List your scheduled tasks with their state, next run, and last result. Pass an id to get that task's full last report.", + "name": "task_list", + "parameters": { + "properties": { + "id": { + "description": "Show only this task, with its full last report", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Schedule work to run later, once or repeating. Give 'prompt' for an AI task, OR 'script' (a saved script name) for a script task that runs with no model and no tokens.", + "name": "task_create", + "parameters": { + "properties": { + "at": { + "description": "One-time task at this local time: \"HH:MM\" (next occurrence) or \"YYYY-MM-DD HH:MM\"", + "type": "string" + }, + "enabled": { + "description": "Start active (default true)", + "type": "boolean" + }, + "in_minutes": { + "description": "One-time task, N minutes from now", + "type": "integer" + }, + "name": { + "type": "string" + }, + "prompt": { + "description": "AI task: self-contained instruction to run each time (omit if using script)", + "type": "string" + }, + "schedule": { + "description": "Repeating: \"@every 2h\" (interval), \"@every 1d@09:00\" (daily time), or a 5-field cron \"min hour dom mon dow\". Omit for a one-time task (use in_minutes or at)", + "type": "string" + }, + "script": { + "description": "Script task: name of a script file you put in the scripts folder, run each time with no AI involved", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Change one or more fields of a scheduled task by id.", + "name": "task_update", + "parameters": { + "properties": { + "enabled": { + "description": "false pauses, true resumes", + "type": "boolean" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "prompt": { + "type": "string" + }, + "schedule": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Delete a scheduled task by id.", + "name": "task_delete", + "parameters": { + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Read a file with 1-indexed line numbers (offset/limit for long files). ALWAYS use this instead of cat/type: output is bounded and reading a file is REQUIRED before you may edit or overwrite it.", + "name": "read", + "parameters": { + "properties": { + "file": { + "description": "Path (relative to the working folder)", + "type": "string" + }, + "limit": { + "description": "Lines (default 400, max 1000)", + "type": "integer" + }, + "offset": { + "description": "Start line (default 1)", + "type": "integer" + } + }, + "required": [ + "file" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Search file contents with a Go regular expression. Returns path:line: text matches (bounded). path may be a file or a directory (default: whole working folder).", + "name": "grep", + "parameters": { + "properties": { + "glob": { + "description": "Only files matching this glob (e.g. *.go)", + "type": "string" + }, + "path": { + "description": "File or directory to search (default .)", + "type": "string" + }, + "pattern": { + "description": "Go regexp", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List files matching a glob pattern (** supported, e.g. **/*.go), most recently modified first.", + "name": "glob", + "parameters": { + "properties": { + "pattern": { + "description": "Glob, e.g. src/**/*.ts", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Ask the user a clarifying question with 2-4 short options, then END your turn. Their answer arrives as the next user message. Use when a decision is genuinely theirs (scope, destructive change, ambiguous requirement) — not for things you can check yourself.", + "name": "ask", + "parameters": { + "properties": { + "options": { + "description": "2-4 short answer options", + "items": { + "type": "string" + }, + "type": "array" + }, + "question": { + "description": "The question", + "type": "string" + } + }, + "required": [ + "question" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Start a shell command in the BACKGROUND (dev server, watcher, long build) and return immediately with a job id. Output goes to a log file; check it with bash_tail. Use this instead of appending '&' in bash.", + "name": "bash_bg", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Read the last lines of a background job's output and its status (running/exited). kill:true stops the job.", + "name": "bash_tail", + "parameters": { + "properties": { + "id": { + "description": "Job id from bash_bg", + "type": "string" + }, + "kill": { + "description": "Stop the job", + "type": "boolean" + }, + "lines": { + "description": "Lines to show (default 40, max 200)", + "type": "integer" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "git status of the working folder (branch + changed files, porcelain format).", + "name": "git_status", + "parameters": { + "properties": { + "dir": { + "description": "Repository subfolder (default: auto-detected)", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "git diff of the working folder (unstaged + staged). Optional file to diff a single path.", + "name": "git_diff", + "parameters": { + "properties": { + "dir": { + "description": "Repository subfolder (default: auto-detected)", + "type": "string" + }, + "file": { + "description": "Limit the diff to this path", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Clone a git repository (https:// or git@) INTO the working folder of this discussion.", + "name": "git_clone", + "parameters": { + "properties": { + "dir": { + "description": "Target folder name (default: repo name)", + "type": "string" + }, + "url": { + "description": "Repository URL", + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Manage the acceptance criteria of the current task (the contract that defines DONE). action=add posits new criteria (texts[]), action=set updates one (id + status, optional note), action=list shows them, action=clear removes them all. Set criteria BEFORE building. Builder: mark each criterion completed once you have done AND checked it — verification only starts when nothing is left pending. Only the verification pass may mark passed or failed.", + "name": "criteria", + "parameters": { + "properties": { + "action": { + "enum": [ + "add", + "set", + "list", + "clear" + ], + "type": "string" + }, + "id": { + "description": "set: criterion id", + "type": "integer" + }, + "note": { + "description": "set: why it failed / how it was verified", + "type": "string" + }, + "status": { + "enum": [ + "pending", + "completed", + "passed", + "failed" + ], + "type": "string" + }, + "texts": { + "description": "add: one entry per criterion, short and testable", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "action" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Délègue une recherche ou une relecture à un rôle qui travaille dans son propre contexte et ne rend que sa réponse. Lecture seule.", + "name": "subagent", + "parameters": { + "properties": { + "role": { + "description": "explorer (trouver/cartographier), code-reviewer (relire un changement), planner (découper)", + "enum": [ + "explorer", + "code-reviewer", + "planner" + ], + "type": "string" + }, + "task": { + "description": "La question, précise et autonome : le rôle ne voit pas la discussion.", + "type": "string" + } + }, + "required": [ + "role", + "task" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Photographie une page web (JS exécuté) pour la MONTRER. La réponse donne la ligne markdown à recopier. Tu ne vois pas l'image.", + "name": "web_screenshot", + "parameters": { + "properties": { + "full_page": { + "description": "Page entière (lourd). Défaut false.", + "type": "boolean" + }, + "url": { + "description": "URL complète", + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" + } + }, + "type": "function" + } + ] + }, + { + "messages": [ + { + "content": "You are Loki, an expert assistant operating directly on this machine with real tools.\n\nYou are in CODE MODE: an autonomous coding agent working in this discussion's folder.\n\nMethod — in this order:\n1. If the task has no acceptance criteria yet, set them FIRST with the criteria tool (action=add): 2-6 short, testable statements of what DONE means. For a trivial task (one obvious change), skip criteria and just do it.\n2. Explore before you change: read the relevant files (read, grep, glob, git_status). Never edit a file you have not read.\n3. Implement with edit (small patches) or write (new files). Match the style of the surrounding code.\n4. Prove it: run the build/tests with bash. A change that was never run is not done. For a web page or UI, start the dev server with bash_bg then use web_screenshot on its localhost URL — a headless Chromium is ALREADY installed, so NEVER install puppeteer, playwright or a browser.\n5. Report briefly what changed and how you verified it.\n\nRules:\n- PLAN ONCE. If the task needs a plan, write it to PLAN.md in ONE short write, then follow it. Between tool calls, think one or two sentences at most — NEVER restate or re-derive the plan: it is in PLAN.md and in your criteria, read them instead.\n- Write each file COMPLETE in a single write call. Many small writes waste turns.\n- When a criterion is done AND checked, mark it completed (criteria set). Verification starts once none is left pending; only it may mark passed.\n- If a command fails, read the error and fix the cause; do not retry the same command unchanged.\n- Ask the user (ask tool) only for decisions that are genuinely theirs; decide the rest yourself.\n- Making the change is your job; shipping it is not. Without an explicit request, never commit, push, reset or rebase, never deploy, restart a service or replace a running binary. Read-only inspection (git_status, git_diff) is fine and encouraged.\n\nThe shell is bash: use its syntax.\nFor anything about the system or files, use bash instead of guessing. Act immediately — call the right tool, then answer. Never end your turn after only thinking. Be concise.\nTo give the user a file, link it in Markdown with its path relative to your working directory — [le rapport](rapport.pdf) — which downloads it. A raw server path is useless: they read you in a browser.\nYou can schedule work for yourself with task_create — only for what must recur or happen later, never a one-off you can do now. A script from your scripts folder runs as a task with no model at all.\n\nDate: {{DATE}}\n\nMachine: host={{HOST}}, cwd={{TMP}}/workspace/discussions/{{CONV}}. This is your working folder: relative paths in write/edit/bash resolve here, and it is the DEFAULT place for scratch work — notes, outputs, a clone, a test. But it is DISPOSABLE: deleting the discussion wipes it. Any script you want to KEEP (or schedule), write it into your scripts folder {{TMP}}/scripts instead — a separate folder a workspace wipe won't touch; you write and run scripts there normally. Do NOT install or write files into system directories such as /usr/local/bin, /usr, /bin or /etc: those need root and are not yours. Only use an absolute path outside this folder (except your scripts folder) when the user explicitly named that location.", + "role": "system" + }, + { + "content": "Construis le jeu", + "role": "user" + }, + { + "content": "r0", + "role": "assistant" + }, + { + "content": "Not done yet — these acceptance criteria are still open:\n[ ] #1 le build passe\n\nContinue working on them. Once one is done and checked, mark it completed (criteria action=set, status=completed). If you need the user's decision, use ask.", + "role": "user" + }, + { + "content": "r1", + "role": "assistant" + }, + { + "content": "Not done yet — these acceptance criteria are still open:\n[ ] #1 le build passe\n\nContinue working on them. Once one is done and checked, mark it completed (criteria action=set, status=completed). If you need the user's decision, use ask.", + "role": "user" + } + ], + "model": "loki", + "parallel_tool_calls": false, + "stream": true, + "stream_options": { + "include_usage": true + }, + "temperature": 0.7, + "tools": [ + { + "function": { + "description": "Run a shell command (bash syntax) and return stdout, stderr and exit code: inspect the system, read files and logs, run scripts. To CREATE or REWRITE a file use write instead — never echo/cat/python -c. Avoid destructive commands unless asked.", + "name": "bash", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + }, + "timeout": { + "description": "Timeout s (default 30, max 300)", + "type": "integer" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Create or replace a file with the exact content given (parent dirs created, content verbatim, no escaping). ALWAYS use this for a script or any text file — NEVER build one through the shell with echo, cat, python -c or Set-Content: quoting breaks.", + "name": "write", + "parameters": { + "properties": { + "content": { + "description": "Full content", + "type": "string" + }, + "file": { + "description": "Path", + "type": "string" + } + }, + "required": [ + "file", + "content" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Patch a file by exact replacement: old → new. old must appear EXACTLY once (add context to make it unique). Prefer this over rewriting a whole file.", + "name": "edit", + "parameters": { + "properties": { + "file": { + "description": "Path", + "type": "string" + }, + "new": { + "description": "Replacement", + "type": "string" + }, + "old": { + "description": "Exact text to replace (unique)", + "type": "string" + } + }, + "required": [ + "file", + "old", + "new" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Bring back verbatim an earlier block archived when this conversation was compacted, from the id shown as recall:rN in the summary.", + "name": "recall", + "parameters": { + "properties": { + "id": { + "description": "Block id, e.g. \"r7\".", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Find an archived block and its recall id by keywords, when it is not listed in the current summary. Then call recall(id).", + "name": "recall_search", + "parameters": { + "properties": { + "limit": { + "description": "Default 5, max 20", + "type": "integer" + }, + "query": { + "description": "Keywords describing the block.", + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List your scheduled tasks with their state, next run, and last result. Pass an id to get that task's full last report.", + "name": "task_list", + "parameters": { + "properties": { + "id": { + "description": "Show only this task, with its full last report", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Schedule work to run later, once or repeating. Give 'prompt' for an AI task, OR 'script' (a saved script name) for a script task that runs with no model and no tokens.", + "name": "task_create", + "parameters": { + "properties": { + "at": { + "description": "One-time task at this local time: \"HH:MM\" (next occurrence) or \"YYYY-MM-DD HH:MM\"", + "type": "string" + }, + "enabled": { + "description": "Start active (default true)", + "type": "boolean" + }, + "in_minutes": { + "description": "One-time task, N minutes from now", + "type": "integer" + }, + "name": { + "type": "string" + }, + "prompt": { + "description": "AI task: self-contained instruction to run each time (omit if using script)", + "type": "string" + }, + "schedule": { + "description": "Repeating: \"@every 2h\" (interval), \"@every 1d@09:00\" (daily time), or a 5-field cron \"min hour dom mon dow\". Omit for a one-time task (use in_minutes or at)", + "type": "string" + }, + "script": { + "description": "Script task: name of a script file you put in the scripts folder, run each time with no AI involved", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Change one or more fields of a scheduled task by id.", + "name": "task_update", + "parameters": { + "properties": { + "enabled": { + "description": "false pauses, true resumes", + "type": "boolean" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "prompt": { + "type": "string" + }, + "schedule": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Delete a scheduled task by id.", + "name": "task_delete", + "parameters": { + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Read a file with 1-indexed line numbers (offset/limit for long files). ALWAYS use this instead of cat/type: output is bounded and reading a file is REQUIRED before you may edit or overwrite it.", + "name": "read", + "parameters": { + "properties": { + "file": { + "description": "Path (relative to the working folder)", + "type": "string" + }, + "limit": { + "description": "Lines (default 400, max 1000)", + "type": "integer" + }, + "offset": { + "description": "Start line (default 1)", + "type": "integer" + } + }, + "required": [ + "file" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Search file contents with a Go regular expression. Returns path:line: text matches (bounded). path may be a file or a directory (default: whole working folder).", + "name": "grep", + "parameters": { + "properties": { + "glob": { + "description": "Only files matching this glob (e.g. *.go)", + "type": "string" + }, + "path": { + "description": "File or directory to search (default .)", + "type": "string" + }, + "pattern": { + "description": "Go regexp", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List files matching a glob pattern (** supported, e.g. **/*.go), most recently modified first.", + "name": "glob", + "parameters": { + "properties": { + "pattern": { + "description": "Glob, e.g. src/**/*.ts", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Ask the user a clarifying question with 2-4 short options, then END your turn. Their answer arrives as the next user message. Use when a decision is genuinely theirs (scope, destructive change, ambiguous requirement) — not for things you can check yourself.", + "name": "ask", + "parameters": { + "properties": { + "options": { + "description": "2-4 short answer options", + "items": { + "type": "string" + }, + "type": "array" + }, + "question": { + "description": "The question", + "type": "string" + } + }, + "required": [ + "question" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Start a shell command in the BACKGROUND (dev server, watcher, long build) and return immediately with a job id. Output goes to a log file; check it with bash_tail. Use this instead of appending '&' in bash.", + "name": "bash_bg", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Read the last lines of a background job's output and its status (running/exited). kill:true stops the job.", + "name": "bash_tail", + "parameters": { + "properties": { + "id": { + "description": "Job id from bash_bg", + "type": "string" + }, + "kill": { + "description": "Stop the job", + "type": "boolean" + }, + "lines": { + "description": "Lines to show (default 40, max 200)", + "type": "integer" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "git status of the working folder (branch + changed files, porcelain format).", + "name": "git_status", + "parameters": { + "properties": { + "dir": { + "description": "Repository subfolder (default: auto-detected)", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "git diff of the working folder (unstaged + staged). Optional file to diff a single path.", + "name": "git_diff", + "parameters": { + "properties": { + "dir": { + "description": "Repository subfolder (default: auto-detected)", + "type": "string" + }, + "file": { + "description": "Limit the diff to this path", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Clone a git repository (https:// or git@) INTO the working folder of this discussion.", + "name": "git_clone", + "parameters": { + "properties": { + "dir": { + "description": "Target folder name (default: repo name)", + "type": "string" + }, + "url": { + "description": "Repository URL", + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Manage the acceptance criteria of the current task (the contract that defines DONE). action=add posits new criteria (texts[]), action=set updates one (id + status, optional note), action=list shows them, action=clear removes them all. Set criteria BEFORE building. Builder: mark each criterion completed once you have done AND checked it — verification only starts when nothing is left pending. Only the verification pass may mark passed or failed.", + "name": "criteria", + "parameters": { + "properties": { + "action": { + "enum": [ + "add", + "set", + "list", + "clear" + ], + "type": "string" + }, + "id": { + "description": "set: criterion id", + "type": "integer" + }, + "note": { + "description": "set: why it failed / how it was verified", + "type": "string" + }, + "status": { + "enum": [ + "pending", + "completed", + "passed", + "failed" + ], + "type": "string" + }, + "texts": { + "description": "add: one entry per criterion, short and testable", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "action" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Délègue une recherche ou une relecture à un rôle qui travaille dans son propre contexte et ne rend que sa réponse. Lecture seule.", + "name": "subagent", + "parameters": { + "properties": { + "role": { + "description": "explorer (trouver/cartographier), code-reviewer (relire un changement), planner (découper)", + "enum": [ + "explorer", + "code-reviewer", + "planner" + ], + "type": "string" + }, + "task": { + "description": "La question, précise et autonome : le rôle ne voit pas la discussion.", + "type": "string" + } + }, + "required": [ + "role", + "task" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Photographie une page web (JS exécuté) pour la MONTRER. La réponse donne la ligne markdown à recopier. Tu ne vois pas l'image.", + "name": "web_screenshot", + "parameters": { + "properties": { + "full_page": { + "description": "Page entière (lourd). Défaut false.", + "type": "boolean" + }, + "url": { + "description": "URL complète", + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" + } + }, + "type": "function" + } + ] + }, + { + "messages": [ + { + "content": "You are the VERIFICATION pass of code mode. You independently check the acceptance criteria against the actual code — you did not write it, trust nothing.\n\nFor each criterion still pending or failed:\n1. Understand what it actually requires.\n2. Check the real changes: git_diff, read the touched files, run the relevant build/test command with bash when applicable.\n3. Record the verdict with the criteria tool: action=set, status=passed (satisfied) or failed (with a note saying exactly what is missing — actionable, one line).\n\nRules:\n- Trivial or non-code criteria that are plainly satisfied: pass them without exploring.\n- Only fail a criterion that genuinely misses its requirement; a different-but-valid implementation passes.\n- Do NOT fix anything yourself. You only verify and report.\n- Be fast: check the diff first, read only what the criteria require.\n\nMachine: host={{HOST}}, cwd={{TMP}}/workspace/discussions/{{CONV}}. This is your working folder: relative paths in write/edit/bash resolve here, and it is the DEFAULT place for scratch work — notes, outputs, a clone, a test. But it is DISPOSABLE: deleting the discussion wipes it. Any script you want to KEEP (or schedule), write it into your scripts folder {{TMP}}/scripts instead — a separate folder a workspace wipe won't touch; you write and run scripts there normally. Do NOT install or write files into system directories such as /usr/local/bin, /usr, /bin or /etc: those need root and are not yours. Only use an absolute path outside this folder (except your scripts folder) when the user explicitly named that location.", + "role": "system" + }, + { + "content": "Task:\nNot done yet — these acceptance criteria are still open:\n[ ] #1 le build passe\n\nContinue working on them. Once one is done and checked, mark it completed (criteria action=set, status=completed). If you need the user's decision, use ask.\n\nAcceptance criteria:\n[ ] #1 le build passe\n\nVerify each non-passed criterion now (git_diff first), and record every verdict with the criteria tool.", + "role": "user" + } + ], + "model": "loki", + "parallel_tool_calls": false, + "stream": true, + "stream_options": { + "include_usage": true + }, + "temperature": 0.7, + "tools": [ + { + "function": { + "description": "Run a shell command (bash syntax) and return stdout, stderr and exit code: inspect the system, read files and logs, run scripts. To CREATE or REWRITE a file use write instead — never echo/cat/python -c. Avoid destructive commands unless asked.", + "name": "bash", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + }, + "timeout": { + "description": "Timeout s (default 30, max 300)", + "type": "integer" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Read a file with 1-indexed line numbers (offset/limit for long files). ALWAYS use this instead of cat/type: output is bounded and reading a file is REQUIRED before you may edit or overwrite it.", + "name": "read", + "parameters": { + "properties": { + "file": { + "description": "Path (relative to the working folder)", + "type": "string" + }, + "limit": { + "description": "Lines (default 400, max 1000)", + "type": "integer" + }, + "offset": { + "description": "Start line (default 1)", + "type": "integer" + } + }, + "required": [ + "file" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Search file contents with a Go regular expression. Returns path:line: text matches (bounded). path may be a file or a directory (default: whole working folder).", + "name": "grep", + "parameters": { + "properties": { + "glob": { + "description": "Only files matching this glob (e.g. *.go)", + "type": "string" + }, + "path": { + "description": "File or directory to search (default .)", + "type": "string" + }, + "pattern": { + "description": "Go regexp", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List files matching a glob pattern (** supported, e.g. **/*.go), most recently modified first.", + "name": "glob", + "parameters": { + "properties": { + "pattern": { + "description": "Glob, e.g. src/**/*.ts", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "git status of the working folder (branch + changed files, porcelain format).", + "name": "git_status", + "parameters": { + "properties": { + "dir": { + "description": "Repository subfolder (default: auto-detected)", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "git diff of the working folder (unstaged + staged). Optional file to diff a single path.", + "name": "git_diff", + "parameters": { + "properties": { + "dir": { + "description": "Repository subfolder (default: auto-detected)", + "type": "string" + }, + "file": { + "description": "Limit the diff to this path", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Manage the acceptance criteria of the current task (the contract that defines DONE). action=add posits new criteria (texts[]), action=set updates one (id + status, optional note), action=list shows them, action=clear removes them all. Set criteria BEFORE building. Builder: mark each criterion completed once you have done AND checked it — verification only starts when nothing is left pending. Only the verification pass may mark passed or failed.", + "name": "criteria", + "parameters": { + "properties": { + "action": { + "enum": [ + "add", + "set", + "list", + "clear" + ], + "type": "string" + }, + "id": { + "description": "set: criterion id", + "type": "integer" + }, + "note": { + "description": "set: why it failed / how it was verified", + "type": "string" + }, + "status": { + "enum": [ + "pending", + "completed", + "passed", + "failed" + ], + "type": "string" + }, + "texts": { + "description": "add: one entry per criterion, short and testable", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "action" + ], + "type": "object" + } + }, + "type": "function" + } + ] + }, + { + "messages": [ + { + "content": "You are Loki, an expert assistant operating directly on this machine with real tools.\n\nYou are in CODE MODE: an autonomous coding agent working in this discussion's folder.\n\nMethod — in this order:\n1. If the task has no acceptance criteria yet, set them FIRST with the criteria tool (action=add): 2-6 short, testable statements of what DONE means. For a trivial task (one obvious change), skip criteria and just do it.\n2. Explore before you change: read the relevant files (read, grep, glob, git_status). Never edit a file you have not read.\n3. Implement with edit (small patches) or write (new files). Match the style of the surrounding code.\n4. Prove it: run the build/tests with bash. A change that was never run is not done. For a web page or UI, start the dev server with bash_bg then use web_screenshot on its localhost URL — a headless Chromium is ALREADY installed, so NEVER install puppeteer, playwright or a browser.\n5. Report briefly what changed and how you verified it.\n\nRules:\n- PLAN ONCE. If the task needs a plan, write it to PLAN.md in ONE short write, then follow it. Between tool calls, think one or two sentences at most — NEVER restate or re-derive the plan: it is in PLAN.md and in your criteria, read them instead.\n- Write each file COMPLETE in a single write call. Many small writes waste turns.\n- When a criterion is done AND checked, mark it completed (criteria set). Verification starts once none is left pending; only it may mark passed.\n- If a command fails, read the error and fix the cause; do not retry the same command unchanged.\n- Ask the user (ask tool) only for decisions that are genuinely theirs; decide the rest yourself.\n- Making the change is your job; shipping it is not. Without an explicit request, never commit, push, reset or rebase, never deploy, restart a service or replace a running binary. Read-only inspection (git_status, git_diff) is fine and encouraged.\n\nThe shell is bash: use its syntax.\nFor anything about the system or files, use bash instead of guessing. Act immediately — call the right tool, then answer. Never end your turn after only thinking. Be concise.\nTo give the user a file, link it in Markdown with its path relative to your working directory — [le rapport](rapport.pdf) — which downloads it. A raw server path is useless: they read you in a browser.\nYou can schedule work for yourself with task_create — only for what must recur or happen later, never a one-off you can do now. A script from your scripts folder runs as a task with no model at all.\n\nDate: {{DATE}}\n\nMachine: host={{HOST}}, cwd={{TMP}}/workspace/discussions/{{CONV}}. This is your working folder: relative paths in write/edit/bash resolve here, and it is the DEFAULT place for scratch work — notes, outputs, a clone, a test. But it is DISPOSABLE: deleting the discussion wipes it. Any script you want to KEEP (or schedule), write it into your scripts folder {{TMP}}/scripts instead — a separate folder a workspace wipe won't touch; you write and run scripts there normally. Do NOT install or write files into system directories such as /usr/local/bin, /usr, /bin or /etc: those need root and are not yours. Only use an absolute path outside this folder (except your scripts folder) when the user explicitly named that location.", + "role": "system" + }, + { + "content": "Construis le jeu", + "role": "user" + }, + { + "content": "r0", + "role": "assistant" + }, + { + "content": "Not done yet — these acceptance criteria are still open:\n[ ] #1 le build passe\n\nContinue working on them. Once one is done and checked, mark it completed (criteria action=set, status=completed). If you need the user's decision, use ask.", + "role": "user" + }, + { + "content": "r1", + "role": "assistant" + }, + { + "content": "Not done yet — these acceptance criteria are still open:\n[ ] #1 le build passe\n\nContinue working on them. Once one is done and checked, mark it completed (criteria action=set, status=completed). If you need the user's decision, use ask.", + "role": "user" + }, + { + "content": "r2", + "role": "assistant" + }, + { + "content": "Verification failed on these criteria:\n[ ] #1 le build passe\n\nFix the code so they pass. Address the notes precisely; do not touch what already passed. Mark each fixed criterion completed again.", + "role": "user" + } + ], + "model": "loki", + "parallel_tool_calls": false, + "stream": true, + "stream_options": { + "include_usage": true + }, + "temperature": 0.7, + "tools": [ + { + "function": { + "description": "Run a shell command (bash syntax) and return stdout, stderr and exit code: inspect the system, read files and logs, run scripts. To CREATE or REWRITE a file use write instead — never echo/cat/python -c. Avoid destructive commands unless asked.", + "name": "bash", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + }, + "timeout": { + "description": "Timeout s (default 30, max 300)", + "type": "integer" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Create or replace a file with the exact content given (parent dirs created, content verbatim, no escaping). ALWAYS use this for a script or any text file — NEVER build one through the shell with echo, cat, python -c or Set-Content: quoting breaks.", + "name": "write", + "parameters": { + "properties": { + "content": { + "description": "Full content", + "type": "string" + }, + "file": { + "description": "Path", + "type": "string" + } + }, + "required": [ + "file", + "content" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Patch a file by exact replacement: old → new. old must appear EXACTLY once (add context to make it unique). Prefer this over rewriting a whole file.", + "name": "edit", + "parameters": { + "properties": { + "file": { + "description": "Path", + "type": "string" + }, + "new": { + "description": "Replacement", + "type": "string" + }, + "old": { + "description": "Exact text to replace (unique)", + "type": "string" + } + }, + "required": [ + "file", + "old", + "new" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Bring back verbatim an earlier block archived when this conversation was compacted, from the id shown as recall:rN in the summary.", + "name": "recall", + "parameters": { + "properties": { + "id": { + "description": "Block id, e.g. \"r7\".", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Find an archived block and its recall id by keywords, when it is not listed in the current summary. Then call recall(id).", + "name": "recall_search", + "parameters": { + "properties": { + "limit": { + "description": "Default 5, max 20", + "type": "integer" + }, + "query": { + "description": "Keywords describing the block.", + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List your scheduled tasks with their state, next run, and last result. Pass an id to get that task's full last report.", + "name": "task_list", + "parameters": { + "properties": { + "id": { + "description": "Show only this task, with its full last report", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Schedule work to run later, once or repeating. Give 'prompt' for an AI task, OR 'script' (a saved script name) for a script task that runs with no model and no tokens.", + "name": "task_create", + "parameters": { + "properties": { + "at": { + "description": "One-time task at this local time: \"HH:MM\" (next occurrence) or \"YYYY-MM-DD HH:MM\"", + "type": "string" + }, + "enabled": { + "description": "Start active (default true)", + "type": "boolean" + }, + "in_minutes": { + "description": "One-time task, N minutes from now", + "type": "integer" + }, + "name": { + "type": "string" + }, + "prompt": { + "description": "AI task: self-contained instruction to run each time (omit if using script)", + "type": "string" + }, + "schedule": { + "description": "Repeating: \"@every 2h\" (interval), \"@every 1d@09:00\" (daily time), or a 5-field cron \"min hour dom mon dow\". Omit for a one-time task (use in_minutes or at)", + "type": "string" + }, + "script": { + "description": "Script task: name of a script file you put in the scripts folder, run each time with no AI involved", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Change one or more fields of a scheduled task by id.", + "name": "task_update", + "parameters": { + "properties": { + "enabled": { + "description": "false pauses, true resumes", + "type": "boolean" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "prompt": { + "type": "string" + }, + "schedule": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Delete a scheduled task by id.", + "name": "task_delete", + "parameters": { + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Read a file with 1-indexed line numbers (offset/limit for long files). ALWAYS use this instead of cat/type: output is bounded and reading a file is REQUIRED before you may edit or overwrite it.", + "name": "read", + "parameters": { + "properties": { + "file": { + "description": "Path (relative to the working folder)", + "type": "string" + }, + "limit": { + "description": "Lines (default 400, max 1000)", + "type": "integer" + }, + "offset": { + "description": "Start line (default 1)", + "type": "integer" + } + }, + "required": [ + "file" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Search file contents with a Go regular expression. Returns path:line: text matches (bounded). path may be a file or a directory (default: whole working folder).", + "name": "grep", + "parameters": { + "properties": { + "glob": { + "description": "Only files matching this glob (e.g. *.go)", + "type": "string" + }, + "path": { + "description": "File or directory to search (default .)", + "type": "string" + }, + "pattern": { + "description": "Go regexp", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List files matching a glob pattern (** supported, e.g. **/*.go), most recently modified first.", + "name": "glob", + "parameters": { + "properties": { + "pattern": { + "description": "Glob, e.g. src/**/*.ts", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Ask the user a clarifying question with 2-4 short options, then END your turn. Their answer arrives as the next user message. Use when a decision is genuinely theirs (scope, destructive change, ambiguous requirement) — not for things you can check yourself.", + "name": "ask", + "parameters": { + "properties": { + "options": { + "description": "2-4 short answer options", + "items": { + "type": "string" + }, + "type": "array" + }, + "question": { + "description": "The question", + "type": "string" + } + }, + "required": [ + "question" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Start a shell command in the BACKGROUND (dev server, watcher, long build) and return immediately with a job id. Output goes to a log file; check it with bash_tail. Use this instead of appending '&' in bash.", + "name": "bash_bg", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Read the last lines of a background job's output and its status (running/exited). kill:true stops the job.", + "name": "bash_tail", + "parameters": { + "properties": { + "id": { + "description": "Job id from bash_bg", + "type": "string" + }, + "kill": { + "description": "Stop the job", + "type": "boolean" + }, + "lines": { + "description": "Lines to show (default 40, max 200)", + "type": "integer" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "git status of the working folder (branch + changed files, porcelain format).", + "name": "git_status", + "parameters": { + "properties": { + "dir": { + "description": "Repository subfolder (default: auto-detected)", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "git diff of the working folder (unstaged + staged). Optional file to diff a single path.", + "name": "git_diff", + "parameters": { + "properties": { + "dir": { + "description": "Repository subfolder (default: auto-detected)", + "type": "string" + }, + "file": { + "description": "Limit the diff to this path", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Clone a git repository (https:// or git@) INTO the working folder of this discussion.", + "name": "git_clone", + "parameters": { + "properties": { + "dir": { + "description": "Target folder name (default: repo name)", + "type": "string" + }, + "url": { + "description": "Repository URL", + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Manage the acceptance criteria of the current task (the contract that defines DONE). action=add posits new criteria (texts[]), action=set updates one (id + status, optional note), action=list shows them, action=clear removes them all. Set criteria BEFORE building. Builder: mark each criterion completed once you have done AND checked it — verification only starts when nothing is left pending. Only the verification pass may mark passed or failed.", + "name": "criteria", + "parameters": { + "properties": { + "action": { + "enum": [ + "add", + "set", + "list", + "clear" + ], + "type": "string" + }, + "id": { + "description": "set: criterion id", + "type": "integer" + }, + "note": { + "description": "set: why it failed / how it was verified", + "type": "string" + }, + "status": { + "enum": [ + "pending", + "completed", + "passed", + "failed" + ], + "type": "string" + }, + "texts": { + "description": "add: one entry per criterion, short and testable", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "action" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Délègue une recherche ou une relecture à un rôle qui travaille dans son propre contexte et ne rend que sa réponse. Lecture seule.", + "name": "subagent", + "parameters": { + "properties": { + "role": { + "description": "explorer (trouver/cartographier), code-reviewer (relire un changement), planner (découper)", + "enum": [ + "explorer", + "code-reviewer", + "planner" + ], + "type": "string" + }, + "task": { + "description": "La question, précise et autonome : le rôle ne voit pas la discussion.", + "type": "string" + } + }, + "required": [ + "role", + "task" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Photographie une page web (JS exécuté) pour la MONTRER. La réponse donne la ligne markdown à recopier. Tu ne vois pas l'image.", + "name": "web_screenshot", + "parameters": { + "properties": { + "full_page": { + "description": "Page entière (lourd). Défaut false.", + "type": "boolean" + }, + "url": { + "description": "URL complète", + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" + } + }, + "type": "function" + } + ] + }, + { + "messages": [ + { + "content": "You are the VERIFICATION pass of code mode. You independently check the acceptance criteria against the actual code — you did not write it, trust nothing.\n\nFor each criterion still pending or failed:\n1. Understand what it actually requires.\n2. Check the real changes: git_diff, read the touched files, run the relevant build/test command with bash when applicable.\n3. Record the verdict with the criteria tool: action=set, status=passed (satisfied) or failed (with a note saying exactly what is missing — actionable, one line).\n\nRules:\n- Trivial or non-code criteria that are plainly satisfied: pass them without exploring.\n- Only fail a criterion that genuinely misses its requirement; a different-but-valid implementation passes.\n- Do NOT fix anything yourself. You only verify and report.\n- Be fast: check the diff first, read only what the criteria require.\n\nMachine: host={{HOST}}, cwd={{TMP}}/workspace/discussions/{{CONV}}. This is your working folder: relative paths in write/edit/bash resolve here, and it is the DEFAULT place for scratch work — notes, outputs, a clone, a test. But it is DISPOSABLE: deleting the discussion wipes it. Any script you want to KEEP (or schedule), write it into your scripts folder {{TMP}}/scripts instead — a separate folder a workspace wipe won't touch; you write and run scripts there normally. Do NOT install or write files into system directories such as /usr/local/bin, /usr, /bin or /etc: those need root and are not yours. Only use an absolute path outside this folder (except your scripts folder) when the user explicitly named that location.", + "role": "system" + }, + { + "content": "Task:\nVerification failed on these criteria:\n[ ] #1 le build passe\n\nFix the code so they pass. Address the notes precisely; do not touch what already passed. Mark each fixed criterion completed again.\n\nAcceptance criteria:\n[ ] #1 le build passe\n\nVerify each non-passed criterion now (git_diff first), and record every verdict with the criteria tool.", + "role": "user" + } + ], + "model": "loki", + "parallel_tool_calls": false, + "stream": true, + "stream_options": { + "include_usage": true + }, + "temperature": 0.7, + "tools": [ + { + "function": { + "description": "Run a shell command (bash syntax) and return stdout, stderr and exit code: inspect the system, read files and logs, run scripts. To CREATE or REWRITE a file use write instead — never echo/cat/python -c. Avoid destructive commands unless asked.", + "name": "bash", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + }, + "timeout": { + "description": "Timeout s (default 30, max 300)", + "type": "integer" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Read a file with 1-indexed line numbers (offset/limit for long files). ALWAYS use this instead of cat/type: output is bounded and reading a file is REQUIRED before you may edit or overwrite it.", + "name": "read", + "parameters": { + "properties": { + "file": { + "description": "Path (relative to the working folder)", + "type": "string" + }, + "limit": { + "description": "Lines (default 400, max 1000)", + "type": "integer" + }, + "offset": { + "description": "Start line (default 1)", + "type": "integer" + } + }, + "required": [ + "file" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Search file contents with a Go regular expression. Returns path:line: text matches (bounded). path may be a file or a directory (default: whole working folder).", + "name": "grep", + "parameters": { + "properties": { + "glob": { + "description": "Only files matching this glob (e.g. *.go)", + "type": "string" + }, + "path": { + "description": "File or directory to search (default .)", + "type": "string" + }, + "pattern": { + "description": "Go regexp", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List files matching a glob pattern (** supported, e.g. **/*.go), most recently modified first.", + "name": "glob", + "parameters": { + "properties": { + "pattern": { + "description": "Glob, e.g. src/**/*.ts", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "git status of the working folder (branch + changed files, porcelain format).", + "name": "git_status", + "parameters": { + "properties": { + "dir": { + "description": "Repository subfolder (default: auto-detected)", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "git diff of the working folder (unstaged + staged). Optional file to diff a single path.", + "name": "git_diff", + "parameters": { + "properties": { + "dir": { + "description": "Repository subfolder (default: auto-detected)", + "type": "string" + }, + "file": { + "description": "Limit the diff to this path", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Manage the acceptance criteria of the current task (the contract that defines DONE). action=add posits new criteria (texts[]), action=set updates one (id + status, optional note), action=list shows them, action=clear removes them all. Set criteria BEFORE building. Builder: mark each criterion completed once you have done AND checked it — verification only starts when nothing is left pending. Only the verification pass may mark passed or failed.", + "name": "criteria", + "parameters": { + "properties": { + "action": { + "enum": [ + "add", + "set", + "list", + "clear" + ], + "type": "string" + }, + "id": { + "description": "set: criterion id", + "type": "integer" + }, + "note": { + "description": "set: why it failed / how it was verified", + "type": "string" + }, + "status": { + "enum": [ + "pending", + "completed", + "passed", + "failed" + ], + "type": "string" + }, + "texts": { + "description": "add: one entry per criterion, short and testable", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "action" + ], + "type": "object" + } + }, + "type": "function" + } + ] + }, + { + "messages": [ + { + "content": "You are Loki, an expert assistant operating directly on this machine with real tools.\n\nYou are in CODE MODE: an autonomous coding agent working in this discussion's folder.\n\nMethod — in this order:\n1. If the task has no acceptance criteria yet, set them FIRST with the criteria tool (action=add): 2-6 short, testable statements of what DONE means. For a trivial task (one obvious change), skip criteria and just do it.\n2. Explore before you change: read the relevant files (read, grep, glob, git_status). Never edit a file you have not read.\n3. Implement with edit (small patches) or write (new files). Match the style of the surrounding code.\n4. Prove it: run the build/tests with bash. A change that was never run is not done. For a web page or UI, start the dev server with bash_bg then use web_screenshot on its localhost URL — a headless Chromium is ALREADY installed, so NEVER install puppeteer, playwright or a browser.\n5. Report briefly what changed and how you verified it.\n\nRules:\n- PLAN ONCE. If the task needs a plan, write it to PLAN.md in ONE short write, then follow it. Between tool calls, think one or two sentences at most — NEVER restate or re-derive the plan: it is in PLAN.md and in your criteria, read them instead.\n- Write each file COMPLETE in a single write call. Many small writes waste turns.\n- When a criterion is done AND checked, mark it completed (criteria set). Verification starts once none is left pending; only it may mark passed.\n- If a command fails, read the error and fix the cause; do not retry the same command unchanged.\n- Ask the user (ask tool) only for decisions that are genuinely theirs; decide the rest yourself.\n- Making the change is your job; shipping it is not. Without an explicit request, never commit, push, reset or rebase, never deploy, restart a service or replace a running binary. Read-only inspection (git_status, git_diff) is fine and encouraged.\n\nThe shell is bash: use its syntax.\nFor anything about the system or files, use bash instead of guessing. Act immediately — call the right tool, then answer. Never end your turn after only thinking. Be concise.\nTo give the user a file, link it in Markdown with its path relative to your working directory — [le rapport](rapport.pdf) — which downloads it. A raw server path is useless: they read you in a browser.\nYou can schedule work for yourself with task_create — only for what must recur or happen later, never a one-off you can do now. A script from your scripts folder runs as a task with no model at all.\n\nDate: {{DATE}}\n\nMachine: host={{HOST}}, cwd={{TMP}}/workspace/discussions/{{CONV}}. This is your working folder: relative paths in write/edit/bash resolve here, and it is the DEFAULT place for scratch work — notes, outputs, a clone, a test. But it is DISPOSABLE: deleting the discussion wipes it. Any script you want to KEEP (or schedule), write it into your scripts folder {{TMP}}/scripts instead — a separate folder a workspace wipe won't touch; you write and run scripts there normally. Do NOT install or write files into system directories such as /usr/local/bin, /usr, /bin or /etc: those need root and are not yours. Only use an absolute path outside this folder (except your scripts folder) when the user explicitly named that location.", + "role": "system" + }, + { + "content": "Construis le jeu", + "role": "user" + }, + { + "content": "r0", + "role": "assistant" + }, + { + "content": "Not done yet — these acceptance criteria are still open:\n[ ] #1 le build passe\n\nContinue working on them. Once one is done and checked, mark it completed (criteria action=set, status=completed). If you need the user's decision, use ask.", + "role": "user" + }, + { + "content": "r1", + "role": "assistant" + }, + { + "content": "Not done yet — these acceptance criteria are still open:\n[ ] #1 le build passe\n\nContinue working on them. Once one is done and checked, mark it completed (criteria action=set, status=completed). If you need the user's decision, use ask.", + "role": "user" + }, + { + "content": "r2", + "role": "assistant" + }, + { + "content": "Verification failed on these criteria:\n[ ] #1 le build passe\n\nFix the code so they pass. Address the notes precisely; do not touch what already passed. Mark each fixed criterion completed again.", + "role": "user" + }, + { + "content": "r3", + "role": "assistant" + }, + { + "content": "Verification failed on these criteria:\n[ ] #1 le build passe\n\nFix the code so they pass. Address the notes precisely; do not touch what already passed. Mark each fixed criterion completed again.", + "role": "user" + } + ], + "model": "loki", + "parallel_tool_calls": false, + "stream": true, + "stream_options": { + "include_usage": true + }, + "temperature": 0.7, + "tools": [ + { + "function": { + "description": "Run a shell command (bash syntax) and return stdout, stderr and exit code: inspect the system, read files and logs, run scripts. To CREATE or REWRITE a file use write instead — never echo/cat/python -c. Avoid destructive commands unless asked.", + "name": "bash", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + }, + "timeout": { + "description": "Timeout s (default 30, max 300)", + "type": "integer" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Create or replace a file with the exact content given (parent dirs created, content verbatim, no escaping). ALWAYS use this for a script or any text file — NEVER build one through the shell with echo, cat, python -c or Set-Content: quoting breaks.", + "name": "write", + "parameters": { + "properties": { + "content": { + "description": "Full content", + "type": "string" + }, + "file": { + "description": "Path", + "type": "string" + } + }, + "required": [ + "file", + "content" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Patch a file by exact replacement: old → new. old must appear EXACTLY once (add context to make it unique). Prefer this over rewriting a whole file.", + "name": "edit", + "parameters": { + "properties": { + "file": { + "description": "Path", + "type": "string" + }, + "new": { + "description": "Replacement", + "type": "string" + }, + "old": { + "description": "Exact text to replace (unique)", + "type": "string" + } + }, + "required": [ + "file", + "old", + "new" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Bring back verbatim an earlier block archived when this conversation was compacted, from the id shown as recall:rN in the summary.", + "name": "recall", + "parameters": { + "properties": { + "id": { + "description": "Block id, e.g. \"r7\".", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Find an archived block and its recall id by keywords, when it is not listed in the current summary. Then call recall(id).", + "name": "recall_search", + "parameters": { + "properties": { + "limit": { + "description": "Default 5, max 20", + "type": "integer" + }, + "query": { + "description": "Keywords describing the block.", + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List your scheduled tasks with their state, next run, and last result. Pass an id to get that task's full last report.", + "name": "task_list", + "parameters": { + "properties": { + "id": { + "description": "Show only this task, with its full last report", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Schedule work to run later, once or repeating. Give 'prompt' for an AI task, OR 'script' (a saved script name) for a script task that runs with no model and no tokens.", + "name": "task_create", + "parameters": { + "properties": { + "at": { + "description": "One-time task at this local time: \"HH:MM\" (next occurrence) or \"YYYY-MM-DD HH:MM\"", + "type": "string" + }, + "enabled": { + "description": "Start active (default true)", + "type": "boolean" + }, + "in_minutes": { + "description": "One-time task, N minutes from now", + "type": "integer" + }, + "name": { + "type": "string" + }, + "prompt": { + "description": "AI task: self-contained instruction to run each time (omit if using script)", + "type": "string" + }, + "schedule": { + "description": "Repeating: \"@every 2h\" (interval), \"@every 1d@09:00\" (daily time), or a 5-field cron \"min hour dom mon dow\". Omit for a one-time task (use in_minutes or at)", + "type": "string" + }, + "script": { + "description": "Script task: name of a script file you put in the scripts folder, run each time with no AI involved", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Change one or more fields of a scheduled task by id.", + "name": "task_update", + "parameters": { + "properties": { + "enabled": { + "description": "false pauses, true resumes", + "type": "boolean" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "prompt": { + "type": "string" + }, + "schedule": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Delete a scheduled task by id.", + "name": "task_delete", + "parameters": { + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Read a file with 1-indexed line numbers (offset/limit for long files). ALWAYS use this instead of cat/type: output is bounded and reading a file is REQUIRED before you may edit or overwrite it.", + "name": "read", + "parameters": { + "properties": { + "file": { + "description": "Path (relative to the working folder)", + "type": "string" + }, + "limit": { + "description": "Lines (default 400, max 1000)", + "type": "integer" + }, + "offset": { + "description": "Start line (default 1)", + "type": "integer" + } + }, + "required": [ + "file" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Search file contents with a Go regular expression. Returns path:line: text matches (bounded). path may be a file or a directory (default: whole working folder).", + "name": "grep", + "parameters": { + "properties": { + "glob": { + "description": "Only files matching this glob (e.g. *.go)", + "type": "string" + }, + "path": { + "description": "File or directory to search (default .)", + "type": "string" + }, + "pattern": { + "description": "Go regexp", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List files matching a glob pattern (** supported, e.g. **/*.go), most recently modified first.", + "name": "glob", + "parameters": { + "properties": { + "pattern": { + "description": "Glob, e.g. src/**/*.ts", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Ask the user a clarifying question with 2-4 short options, then END your turn. Their answer arrives as the next user message. Use when a decision is genuinely theirs (scope, destructive change, ambiguous requirement) — not for things you can check yourself.", + "name": "ask", + "parameters": { + "properties": { + "options": { + "description": "2-4 short answer options", + "items": { + "type": "string" + }, + "type": "array" + }, + "question": { + "description": "The question", + "type": "string" + } + }, + "required": [ + "question" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Start a shell command in the BACKGROUND (dev server, watcher, long build) and return immediately with a job id. Output goes to a log file; check it with bash_tail. Use this instead of appending '&' in bash.", + "name": "bash_bg", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Read the last lines of a background job's output and its status (running/exited). kill:true stops the job.", + "name": "bash_tail", + "parameters": { + "properties": { + "id": { + "description": "Job id from bash_bg", + "type": "string" + }, + "kill": { + "description": "Stop the job", + "type": "boolean" + }, + "lines": { + "description": "Lines to show (default 40, max 200)", + "type": "integer" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "git status of the working folder (branch + changed files, porcelain format).", + "name": "git_status", + "parameters": { + "properties": { + "dir": { + "description": "Repository subfolder (default: auto-detected)", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "git diff of the working folder (unstaged + staged). Optional file to diff a single path.", + "name": "git_diff", + "parameters": { + "properties": { + "dir": { + "description": "Repository subfolder (default: auto-detected)", + "type": "string" + }, + "file": { + "description": "Limit the diff to this path", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Clone a git repository (https:// or git@) INTO the working folder of this discussion.", + "name": "git_clone", + "parameters": { + "properties": { + "dir": { + "description": "Target folder name (default: repo name)", + "type": "string" + }, + "url": { + "description": "Repository URL", + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Manage the acceptance criteria of the current task (the contract that defines DONE). action=add posits new criteria (texts[]), action=set updates one (id + status, optional note), action=list shows them, action=clear removes them all. Set criteria BEFORE building. Builder: mark each criterion completed once you have done AND checked it — verification only starts when nothing is left pending. Only the verification pass may mark passed or failed.", + "name": "criteria", + "parameters": { + "properties": { + "action": { + "enum": [ + "add", + "set", + "list", + "clear" + ], + "type": "string" + }, + "id": { + "description": "set: criterion id", + "type": "integer" + }, + "note": { + "description": "set: why it failed / how it was verified", + "type": "string" + }, + "status": { + "enum": [ + "pending", + "completed", + "passed", + "failed" + ], + "type": "string" + }, + "texts": { + "description": "add: one entry per criterion, short and testable", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "action" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Délègue une recherche ou une relecture à un rôle qui travaille dans son propre contexte et ne rend que sa réponse. Lecture seule.", + "name": "subagent", + "parameters": { + "properties": { + "role": { + "description": "explorer (trouver/cartographier), code-reviewer (relire un changement), planner (découper)", + "enum": [ + "explorer", + "code-reviewer", + "planner" + ], + "type": "string" + }, + "task": { + "description": "La question, précise et autonome : le rôle ne voit pas la discussion.", + "type": "string" + } + }, + "required": [ + "role", + "task" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Photographie une page web (JS exécuté) pour la MONTRER. La réponse donne la ligne markdown à recopier. Tu ne vois pas l'image.", + "name": "web_screenshot", + "parameters": { + "properties": { + "full_page": { + "description": "Page entière (lourd). Défaut false.", + "type": "boolean" + }, + "url": { + "description": "URL complète", + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" + } + }, + "type": "function" + } + ] + }, + { + "messages": [ + { + "content": "You are the VERIFICATION pass of code mode. You independently check the acceptance criteria against the actual code — you did not write it, trust nothing.\n\nFor each criterion still pending or failed:\n1. Understand what it actually requires.\n2. Check the real changes: git_diff, read the touched files, run the relevant build/test command with bash when applicable.\n3. Record the verdict with the criteria tool: action=set, status=passed (satisfied) or failed (with a note saying exactly what is missing — actionable, one line).\n\nRules:\n- Trivial or non-code criteria that are plainly satisfied: pass them without exploring.\n- Only fail a criterion that genuinely misses its requirement; a different-but-valid implementation passes.\n- Do NOT fix anything yourself. You only verify and report.\n- Be fast: check the diff first, read only what the criteria require.\n\nMachine: host={{HOST}}, cwd={{TMP}}/workspace/discussions/{{CONV}}. This is your working folder: relative paths in write/edit/bash resolve here, and it is the DEFAULT place for scratch work — notes, outputs, a clone, a test. But it is DISPOSABLE: deleting the discussion wipes it. Any script you want to KEEP (or schedule), write it into your scripts folder {{TMP}}/scripts instead — a separate folder a workspace wipe won't touch; you write and run scripts there normally. Do NOT install or write files into system directories such as /usr/local/bin, /usr, /bin or /etc: those need root and are not yours. Only use an absolute path outside this folder (except your scripts folder) when the user explicitly named that location.", + "role": "system" + }, + { + "content": "Task:\nVerification failed on these criteria:\n[ ] #1 le build passe\n\nFix the code so they pass. Address the notes precisely; do not touch what already passed. Mark each fixed criterion completed again.\n\nAcceptance criteria:\n[ ] #1 le build passe\n\nVerify each non-passed criterion now (git_diff first), and record every verdict with the criteria tool.", + "role": "user" + } + ], + "model": "loki", + "parallel_tool_calls": false, + "stream": true, + "stream_options": { + "include_usage": true + }, + "temperature": 0.7, + "tools": [ + { + "function": { + "description": "Run a shell command (bash syntax) and return stdout, stderr and exit code: inspect the system, read files and logs, run scripts. To CREATE or REWRITE a file use write instead — never echo/cat/python -c. Avoid destructive commands unless asked.", + "name": "bash", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + }, + "timeout": { + "description": "Timeout s (default 30, max 300)", + "type": "integer" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Read a file with 1-indexed line numbers (offset/limit for long files). ALWAYS use this instead of cat/type: output is bounded and reading a file is REQUIRED before you may edit or overwrite it.", + "name": "read", + "parameters": { + "properties": { + "file": { + "description": "Path (relative to the working folder)", + "type": "string" + }, + "limit": { + "description": "Lines (default 400, max 1000)", + "type": "integer" + }, + "offset": { + "description": "Start line (default 1)", + "type": "integer" + } + }, + "required": [ + "file" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Search file contents with a Go regular expression. Returns path:line: text matches (bounded). path may be a file or a directory (default: whole working folder).", + "name": "grep", + "parameters": { + "properties": { + "glob": { + "description": "Only files matching this glob (e.g. *.go)", + "type": "string" + }, + "path": { + "description": "File or directory to search (default .)", + "type": "string" + }, + "pattern": { + "description": "Go regexp", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List files matching a glob pattern (** supported, e.g. **/*.go), most recently modified first.", + "name": "glob", + "parameters": { + "properties": { + "pattern": { + "description": "Glob, e.g. src/**/*.ts", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "git status of the working folder (branch + changed files, porcelain format).", + "name": "git_status", + "parameters": { + "properties": { + "dir": { + "description": "Repository subfolder (default: auto-detected)", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "git diff of the working folder (unstaged + staged). Optional file to diff a single path.", + "name": "git_diff", + "parameters": { + "properties": { + "dir": { + "description": "Repository subfolder (default: auto-detected)", + "type": "string" + }, + "file": { + "description": "Limit the diff to this path", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Manage the acceptance criteria of the current task (the contract that defines DONE). action=add posits new criteria (texts[]), action=set updates one (id + status, optional note), action=list shows them, action=clear removes them all. Set criteria BEFORE building. Builder: mark each criterion completed once you have done AND checked it — verification only starts when nothing is left pending. Only the verification pass may mark passed or failed.", + "name": "criteria", + "parameters": { + "properties": { + "action": { + "enum": [ + "add", + "set", + "list", + "clear" + ], + "type": "string" + }, + "id": { + "description": "set: criterion id", + "type": "integer" + }, + "note": { + "description": "set: why it failed / how it was verified", + "type": "string" + }, + "status": { + "enum": [ + "pending", + "completed", + "passed", + "failed" + ], + "type": "string" + }, + "texts": { + "description": "add: one entry per criterion, short and testable", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "action" + ], + "type": "object" + } + }, + "type": "function" + } + ] + } + ], + "log": [ + { + "user": "Construis le jeu" + }, + { + "content": "r0", + "seq0": 2, + "toks": 1 + }, + { + "stats": "…" + }, + { + "stats": "…" + }, + { + "stats": "…" + }, + { + "role": "builder" + }, + { + "content": "r1", + "seq0": 7, + "toks": 1 + }, + { + "stats": "…" + }, + { + "stats": "…" + }, + { + "stats": "…" + }, + { + "role": "" + }, + { + "role": "builder" + }, + { + "content": "r2", + "seq0": 13, + "toks": 1 + }, + { + "stats": "…" + }, + { + "stats": "…" + }, + { + "stats": "…" + }, + { + "role": "" + }, + { + "role": "verifier" + }, + { + "content": "v1", + "seq0": 19, + "toks": 1 + }, + { + "ctx_isolated": true, + "stats": "…" + }, + { + "ctx_isolated": true, + "stats": "…" + }, + { + "ctx_isolated": true, + "stats": "…" + }, + { + "role": "" + }, + { + "role": "builder" + }, + { + "content": "r3", + "seq0": 25, + "toks": 1 + }, + { + "stats": "…" + }, + { + "stats": "…" + }, + { + "stats": "…" + }, + { + "role": "" + }, + { + "role": "verifier" + }, + { + "content": "v2", + "seq0": 31, + "toks": 1 + }, + { + "ctx_isolated": true, + "stats": "…" + }, + { + "ctx_isolated": true, + "stats": "…" + }, + { + "ctx_isolated": true, + "stats": "…" + }, + { + "role": "" + }, + { + "role": "builder" + }, + { + "content": "r4", + "seq0": 37, + "toks": 1 + }, + { + "stats": "…" + }, + { + "stats": "…" + }, + { + "stats": "…" + }, + { + "role": "" + }, + { + "role": "verifier" + }, + { + "content": "v3", + "seq0": 43, + "toks": 1 + }, + { + "ctx_isolated": true, + "stats": "…" + }, + { + "ctx_isolated": true, + "stats": "…" + }, + { + "ctx_isolated": true, + "stats": "…" + }, + { + "role": "" + }, + { + "verify_done": "gave_up" + }, + { + "content": "\n\n_(vérification : 1 critère(s) non satisfaits après 2 corrections — voir le panneau des critères)_", + "seq0": 49, + "toks": 1 + }, + { + "turn_done": true + } + ], + "messages": [ + { + "role": "user", + "content": "Construis le jeu" + }, + { + "role": "assistant", + "content": "r0" + }, + { + "role": "user", + "content": "Not done yet — these acceptance criteria are still open:\n[ ] #1 le build passe\n\nContinue working on them. Once one is done and checked, mark it completed (criteria action=set, status=completed). If you need the user's decision, use ask." + }, + { + "role": "assistant", + "content": "r1" + }, + { + "role": "user", + "content": "Not done yet — these acceptance criteria are still open:\n[ ] #1 le build passe\n\nContinue working on them. Once one is done and checked, mark it completed (criteria action=set, status=completed). If you need the user's decision, use ask." + }, + { + "role": "assistant", + "content": "r2" + }, + { + "role": "user", + "content": "Verification failed on these criteria:\n[ ] #1 le build passe\n\nFix the code so they pass. Address the notes precisely; do not touch what already passed. Mark each fixed criterion completed again." + }, + { + "role": "assistant", + "content": "r3" + }, + { + "role": "user", + "content": "Verification failed on these criteria:\n[ ] #1 le build passe\n\nFix the code so they pass. Address the notes precisely; do not touch what already passed. Mark each fixed criterion completed again." + }, + { + "role": "assistant", + "content": "r4" + } + ], + "status": 200 + } +} diff --git a/rust/loki-rs/tests/parity_turn/code_contrat_deja_tenu.json b/rust/loki-rs/tests/parity_turn/code_contrat_deja_tenu.json new file mode 100644 index 0000000..13f8087 --- /dev/null +++ b/rust/loki-rs/tests/parity_turn/code_contrat_deja_tenu.json @@ -0,0 +1,1370 @@ +{ + "name": "code_contrat_deja_tenu", + "cfg": { + "MEM_MODE": "off" + }, + "agent": true, + "code": true, + "criteria": [ + { + "id": 1, + "text": "les tests passent", + "status": "passed" + } + ], + "send": { + "cid": "c-code_contrat_deja_tenu", + "message": "Corrige le test" + }, + "replies": [ + { + "status": 200, + "lines": [ + "data: {\"choices\":[{\"delta\":{\"content\":\"Corrigé.\"}}]}", + "data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"completion_tokens\":5,\"prompt_tokens\":100}}", + "data: [DONE]" + ] + } + ], + "tool_results": { + "bash": "a.txt\nb.txt", + "read": "1: ligne" + }, + "start": { + "caps": { + "Agent": true, + "Code": true, + "ComputerUse": false, + "Internet": false, + "Mem": "off", + "Role": "" + }, + "code_hint": false, + "content": "Corrige le test", + "files": [], + "model": "", + "turn_caps": { + "Agent": true, + "Code": true, + "ComputerUse": false, + "Internet": false, + "Mem": "off", + "Role": "builder" + } + }, + "view": { + "head": [], + "prefix": "You are Loki, an expert assistant operating directly on this machine with real tools.\n\nYou are in CODE MODE: an autonomous coding agent working in this discussion's folder.\n\nMethod — in this order:\n1. If the task has no acceptance criteria yet, set them FIRST with the criteria tool (action=add): 2-6 short, testable statements of what DONE means. For a trivial task (one obvious change), skip criteria and just do it.\n2. Explore before you change: read the relevant files (read, grep, glob, git_status). Never edit a file you have not read.\n3. Implement with edit (small patches) or write (new files). Match the style of the surrounding code.\n4. Prove it: run the build/tests with bash. A change that was never run is not done. For a web page or UI, start the dev server with bash_bg then use web_screenshot on its localhost URL — a headless Chromium is ALREADY installed, so NEVER install puppeteer, playwright or a browser.\n5. Report briefly what changed and how you verified it.\n\nRules:\n- PLAN ONCE. If the task needs a plan, write it to PLAN.md in ONE short write, then follow it. Between tool calls, think one or two sentences at most — NEVER restate or re-derive the plan: it is in PLAN.md and in your criteria, read them instead.\n- Write each file COMPLETE in a single write call. Many small writes waste turns.\n- When a criterion is done AND checked, mark it completed (criteria set). Verification starts once none is left pending; only it may mark passed.\n- If a command fails, read the error and fix the cause; do not retry the same command unchanged.\n- Ask the user (ask tool) only for decisions that are genuinely theirs; decide the rest yourself.\n- Making the change is your job; shipping it is not. Without an explicit request, never commit, push, reset or rebase, never deploy, restart a service or replace a running binary. Read-only inspection (git_status, git_diff) is fine and encouraged.\n\nThe shell is bash: use its syntax.\nFor anything about the system or files, use bash instead of guessing. Act immediately — call the right tool, then answer. Never end your turn after only thinking. Be concise.\nTo give the user a file, link it in Markdown with its path relative to your working directory — [le rapport](rapport.pdf) — which downloads it. A raw server path is useless: they read you in a browser.\nYou can schedule work for yourself with task_create — only for what must recur or happen later, never a one-off you can do now. A script from your scripts folder runs as a task with no model at all.\n\nDate: {{DATE}}\n\nMachine: host={{HOST}}, cwd={{TMP}}/workspace/discussions/{{CONV}}. This is your working folder: relative paths in write/edit/bash resolve here, and it is the DEFAULT place for scratch work — notes, outputs, a clone, a test. But it is DISPOSABLE: deleting the discussion wipes it. Any script you want to KEEP (or schedule), write it into your scripts folder {{TMP}}/scripts instead — a separate folder a workspace wipe won't touch; you write and run scripts there normally. Do NOT install or write files into system directories such as /usr/local/bin, /usr, /bin or /etc: those need root and are not yours. Only use an absolute path outside this folder (except your scripts folder) when the user explicitly named that location.", + "tools": [ + { + "function": { + "description": "Run a shell command (bash syntax) and return stdout, stderr and exit code: inspect the system, read files and logs, run scripts. To CREATE or REWRITE a file use write instead — never echo/cat/python -c. Avoid destructive commands unless asked.", + "name": "bash", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + }, + "timeout": { + "description": "Timeout s (default 30, max 300)", + "type": "integer" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Create or replace a file with the exact content given (parent dirs created, content verbatim, no escaping). ALWAYS use this for a script or any text file — NEVER build one through the shell with echo, cat, python -c or Set-Content: quoting breaks.", + "name": "write", + "parameters": { + "properties": { + "content": { + "description": "Full content", + "type": "string" + }, + "file": { + "description": "Path", + "type": "string" + } + }, + "required": [ + "file", + "content" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Patch a file by exact replacement: old → new. old must appear EXACTLY once (add context to make it unique). Prefer this over rewriting a whole file.", + "name": "edit", + "parameters": { + "properties": { + "file": { + "description": "Path", + "type": "string" + }, + "new": { + "description": "Replacement", + "type": "string" + }, + "old": { + "description": "Exact text to replace (unique)", + "type": "string" + } + }, + "required": [ + "file", + "old", + "new" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Bring back verbatim an earlier block archived when this conversation was compacted, from the id shown as recall:rN in the summary.", + "name": "recall", + "parameters": { + "properties": { + "id": { + "description": "Block id, e.g. \"r7\".", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Find an archived block and its recall id by keywords, when it is not listed in the current summary. Then call recall(id).", + "name": "recall_search", + "parameters": { + "properties": { + "limit": { + "description": "Default 5, max 20", + "type": "integer" + }, + "query": { + "description": "Keywords describing the block.", + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List your scheduled tasks with their state, next run, and last result. Pass an id to get that task's full last report.", + "name": "task_list", + "parameters": { + "properties": { + "id": { + "description": "Show only this task, with its full last report", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Schedule work to run later, once or repeating. Give 'prompt' for an AI task, OR 'script' (a saved script name) for a script task that runs with no model and no tokens.", + "name": "task_create", + "parameters": { + "properties": { + "at": { + "description": "One-time task at this local time: \"HH:MM\" (next occurrence) or \"YYYY-MM-DD HH:MM\"", + "type": "string" + }, + "enabled": { + "description": "Start active (default true)", + "type": "boolean" + }, + "in_minutes": { + "description": "One-time task, N minutes from now", + "type": "integer" + }, + "name": { + "type": "string" + }, + "prompt": { + "description": "AI task: self-contained instruction to run each time (omit if using script)", + "type": "string" + }, + "schedule": { + "description": "Repeating: \"@every 2h\" (interval), \"@every 1d@09:00\" (daily time), or a 5-field cron \"min hour dom mon dow\". Omit for a one-time task (use in_minutes or at)", + "type": "string" + }, + "script": { + "description": "Script task: name of a script file you put in the scripts folder, run each time with no AI involved", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Change one or more fields of a scheduled task by id.", + "name": "task_update", + "parameters": { + "properties": { + "enabled": { + "description": "false pauses, true resumes", + "type": "boolean" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "prompt": { + "type": "string" + }, + "schedule": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Delete a scheduled task by id.", + "name": "task_delete", + "parameters": { + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Read a file with 1-indexed line numbers (offset/limit for long files). ALWAYS use this instead of cat/type: output is bounded and reading a file is REQUIRED before you may edit or overwrite it.", + "name": "read", + "parameters": { + "properties": { + "file": { + "description": "Path (relative to the working folder)", + "type": "string" + }, + "limit": { + "description": "Lines (default 400, max 1000)", + "type": "integer" + }, + "offset": { + "description": "Start line (default 1)", + "type": "integer" + } + }, + "required": [ + "file" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Search file contents with a Go regular expression. Returns path:line: text matches (bounded). path may be a file or a directory (default: whole working folder).", + "name": "grep", + "parameters": { + "properties": { + "glob": { + "description": "Only files matching this glob (e.g. *.go)", + "type": "string" + }, + "path": { + "description": "File or directory to search (default .)", + "type": "string" + }, + "pattern": { + "description": "Go regexp", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List files matching a glob pattern (** supported, e.g. **/*.go), most recently modified first.", + "name": "glob", + "parameters": { + "properties": { + "pattern": { + "description": "Glob, e.g. src/**/*.ts", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Ask the user a clarifying question with 2-4 short options, then END your turn. Their answer arrives as the next user message. Use when a decision is genuinely theirs (scope, destructive change, ambiguous requirement) — not for things you can check yourself.", + "name": "ask", + "parameters": { + "properties": { + "options": { + "description": "2-4 short answer options", + "items": { + "type": "string" + }, + "type": "array" + }, + "question": { + "description": "The question", + "type": "string" + } + }, + "required": [ + "question" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Start a shell command in the BACKGROUND (dev server, watcher, long build) and return immediately with a job id. Output goes to a log file; check it with bash_tail. Use this instead of appending '&' in bash.", + "name": "bash_bg", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Read the last lines of a background job's output and its status (running/exited). kill:true stops the job.", + "name": "bash_tail", + "parameters": { + "properties": { + "id": { + "description": "Job id from bash_bg", + "type": "string" + }, + "kill": { + "description": "Stop the job", + "type": "boolean" + }, + "lines": { + "description": "Lines to show (default 40, max 200)", + "type": "integer" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "git status of the working folder (branch + changed files, porcelain format).", + "name": "git_status", + "parameters": { + "properties": { + "dir": { + "description": "Repository subfolder (default: auto-detected)", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "git diff of the working folder (unstaged + staged). Optional file to diff a single path.", + "name": "git_diff", + "parameters": { + "properties": { + "dir": { + "description": "Repository subfolder (default: auto-detected)", + "type": "string" + }, + "file": { + "description": "Limit the diff to this path", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Clone a git repository (https:// or git@) INTO the working folder of this discussion.", + "name": "git_clone", + "parameters": { + "properties": { + "dir": { + "description": "Target folder name (default: repo name)", + "type": "string" + }, + "url": { + "description": "Repository URL", + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Manage the acceptance criteria of the current task (the contract that defines DONE). action=add posits new criteria (texts[]), action=set updates one (id + status, optional note), action=list shows them, action=clear removes them all. Set criteria BEFORE building. Builder: mark each criterion completed once you have done AND checked it — verification only starts when nothing is left pending. Only the verification pass may mark passed or failed.", + "name": "criteria", + "parameters": { + "properties": { + "action": { + "enum": [ + "add", + "set", + "list", + "clear" + ], + "type": "string" + }, + "id": { + "description": "set: criterion id", + "type": "integer" + }, + "note": { + "description": "set: why it failed / how it was verified", + "type": "string" + }, + "status": { + "enum": [ + "pending", + "completed", + "passed", + "failed" + ], + "type": "string" + }, + "texts": { + "description": "add: one entry per criterion, short and testable", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "action" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Délègue une recherche ou une relecture à un rôle qui travaille dans son propre contexte et ne rend que sa réponse. Lecture seule.", + "name": "subagent", + "parameters": { + "properties": { + "role": { + "description": "explorer (trouver/cartographier), code-reviewer (relire un changement), planner (découper)", + "enum": [ + "explorer", + "code-reviewer", + "planner" + ], + "type": "string" + }, + "task": { + "description": "La question, précise et autonome : le rôle ne voit pas la discussion.", + "type": "string" + } + }, + "required": [ + "role", + "task" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Photographie une page web (JS exécuté) pour la MONTRER. La réponse donne la ligne markdown à recopier. Tu ne vois pas l'image.", + "name": "web_screenshot", + "parameters": { + "properties": { + "full_page": { + "description": "Page entière (lourd). Défaut false.", + "type": "boolean" + }, + "url": { + "description": "URL complète", + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" + } + }, + "type": "function" + } + ] + }, + "verifier": { + "caps": { + "Agent": true, + "Code": true, + "ComputerUse": false, + "Internet": false, + "Mem": "off", + "Role": "verifier" + }, + "system": "You are the VERIFICATION pass of code mode. You independently check the acceptance criteria against the actual code — you did not write it, trust nothing.\n\nFor each criterion still pending or failed:\n1. Understand what it actually requires.\n2. Check the real changes: git_diff, read the touched files, run the relevant build/test command with bash when applicable.\n3. Record the verdict with the criteria tool: action=set, status=passed (satisfied) or failed (with a note saying exactly what is missing — actionable, one line).\n\nRules:\n- Trivial or non-code criteria that are plainly satisfied: pass them without exploring.\n- Only fail a criterion that genuinely misses its requirement; a different-but-valid implementation passes.\n- Do NOT fix anything yourself. You only verify and report.\n- Be fast: check the diff first, read only what the criteria require.\n\nMachine: host={{HOST}}, cwd={{TMP}}/workspace/discussions/{{CONV}}. This is your working folder: relative paths in write/edit/bash resolve here, and it is the DEFAULT place for scratch work — notes, outputs, a clone, a test. But it is DISPOSABLE: deleting the discussion wipes it. Any script you want to KEEP (or schedule), write it into your scripts folder {{TMP}}/scripts instead — a separate folder a workspace wipe won't touch; you write and run scripts there normally. Do NOT install or write files into system directories such as /usr/local/bin, /usr, /bin or /etc: those need root and are not yours. Only use an absolute path outside this folder (except your scripts folder) when the user explicitly named that location.", + "tools": [ + { + "function": { + "description": "Run a shell command (bash syntax) and return stdout, stderr and exit code: inspect the system, read files and logs, run scripts. To CREATE or REWRITE a file use write instead — never echo/cat/python -c. Avoid destructive commands unless asked.", + "name": "bash", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + }, + "timeout": { + "description": "Timeout s (default 30, max 300)", + "type": "integer" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Read a file with 1-indexed line numbers (offset/limit for long files). ALWAYS use this instead of cat/type: output is bounded and reading a file is REQUIRED before you may edit or overwrite it.", + "name": "read", + "parameters": { + "properties": { + "file": { + "description": "Path (relative to the working folder)", + "type": "string" + }, + "limit": { + "description": "Lines (default 400, max 1000)", + "type": "integer" + }, + "offset": { + "description": "Start line (default 1)", + "type": "integer" + } + }, + "required": [ + "file" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Search file contents with a Go regular expression. Returns path:line: text matches (bounded). path may be a file or a directory (default: whole working folder).", + "name": "grep", + "parameters": { + "properties": { + "glob": { + "description": "Only files matching this glob (e.g. *.go)", + "type": "string" + }, + "path": { + "description": "File or directory to search (default .)", + "type": "string" + }, + "pattern": { + "description": "Go regexp", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List files matching a glob pattern (** supported, e.g. **/*.go), most recently modified first.", + "name": "glob", + "parameters": { + "properties": { + "pattern": { + "description": "Glob, e.g. src/**/*.ts", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "git status of the working folder (branch + changed files, porcelain format).", + "name": "git_status", + "parameters": { + "properties": { + "dir": { + "description": "Repository subfolder (default: auto-detected)", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "git diff of the working folder (unstaged + staged). Optional file to diff a single path.", + "name": "git_diff", + "parameters": { + "properties": { + "dir": { + "description": "Repository subfolder (default: auto-detected)", + "type": "string" + }, + "file": { + "description": "Limit the diff to this path", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Manage the acceptance criteria of the current task (the contract that defines DONE). action=add posits new criteria (texts[]), action=set updates one (id + status, optional note), action=list shows them, action=clear removes them all. Set criteria BEFORE building. Builder: mark each criterion completed once you have done AND checked it — verification only starts when nothing is left pending. Only the verification pass may mark passed or failed.", + "name": "criteria", + "parameters": { + "properties": { + "action": { + "enum": [ + "add", + "set", + "list", + "clear" + ], + "type": "string" + }, + "id": { + "description": "set: criterion id", + "type": "integer" + }, + "note": { + "description": "set: why it failed / how it was verified", + "type": "string" + }, + "status": { + "enum": [ + "pending", + "completed", + "passed", + "failed" + ], + "type": "string" + }, + "texts": { + "description": "add: one entry per criterion, short and testable", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "action" + ], + "type": "object" + } + }, + "type": "function" + } + ] + }, + "expect": { + "requests": [ + { + "messages": [ + { + "content": "You are Loki, an expert assistant operating directly on this machine with real tools.\n\nYou are in CODE MODE: an autonomous coding agent working in this discussion's folder.\n\nMethod — in this order:\n1. If the task has no acceptance criteria yet, set them FIRST with the criteria tool (action=add): 2-6 short, testable statements of what DONE means. For a trivial task (one obvious change), skip criteria and just do it.\n2. Explore before you change: read the relevant files (read, grep, glob, git_status). Never edit a file you have not read.\n3. Implement with edit (small patches) or write (new files). Match the style of the surrounding code.\n4. Prove it: run the build/tests with bash. A change that was never run is not done. For a web page or UI, start the dev server with bash_bg then use web_screenshot on its localhost URL — a headless Chromium is ALREADY installed, so NEVER install puppeteer, playwright or a browser.\n5. Report briefly what changed and how you verified it.\n\nRules:\n- PLAN ONCE. If the task needs a plan, write it to PLAN.md in ONE short write, then follow it. Between tool calls, think one or two sentences at most — NEVER restate or re-derive the plan: it is in PLAN.md and in your criteria, read them instead.\n- Write each file COMPLETE in a single write call. Many small writes waste turns.\n- When a criterion is done AND checked, mark it completed (criteria set). Verification starts once none is left pending; only it may mark passed.\n- If a command fails, read the error and fix the cause; do not retry the same command unchanged.\n- Ask the user (ask tool) only for decisions that are genuinely theirs; decide the rest yourself.\n- Making the change is your job; shipping it is not. Without an explicit request, never commit, push, reset or rebase, never deploy, restart a service or replace a running binary. Read-only inspection (git_status, git_diff) is fine and encouraged.\n\nThe shell is bash: use its syntax.\nFor anything about the system or files, use bash instead of guessing. Act immediately — call the right tool, then answer. Never end your turn after only thinking. Be concise.\nTo give the user a file, link it in Markdown with its path relative to your working directory — [le rapport](rapport.pdf) — which downloads it. A raw server path is useless: they read you in a browser.\nYou can schedule work for yourself with task_create — only for what must recur or happen later, never a one-off you can do now. A script from your scripts folder runs as a task with no model at all.\n\nDate: {{DATE}}\n\nMachine: host={{HOST}}, cwd={{TMP}}/workspace/discussions/{{CONV}}. This is your working folder: relative paths in write/edit/bash resolve here, and it is the DEFAULT place for scratch work — notes, outputs, a clone, a test. But it is DISPOSABLE: deleting the discussion wipes it. Any script you want to KEEP (or schedule), write it into your scripts folder {{TMP}}/scripts instead — a separate folder a workspace wipe won't touch; you write and run scripts there normally. Do NOT install or write files into system directories such as /usr/local/bin, /usr, /bin or /etc: those need root and are not yours. Only use an absolute path outside this folder (except your scripts folder) when the user explicitly named that location.", + "role": "system" + }, + { + "content": "Corrige le test", + "role": "user" + } + ], + "model": "loki", + "parallel_tool_calls": false, + "stream": true, + "stream_options": { + "include_usage": true + }, + "temperature": 0.7, + "tools": [ + { + "function": { + "description": "Run a shell command (bash syntax) and return stdout, stderr and exit code: inspect the system, read files and logs, run scripts. To CREATE or REWRITE a file use write instead — never echo/cat/python -c. Avoid destructive commands unless asked.", + "name": "bash", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + }, + "timeout": { + "description": "Timeout s (default 30, max 300)", + "type": "integer" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Create or replace a file with the exact content given (parent dirs created, content verbatim, no escaping). ALWAYS use this for a script or any text file — NEVER build one through the shell with echo, cat, python -c or Set-Content: quoting breaks.", + "name": "write", + "parameters": { + "properties": { + "content": { + "description": "Full content", + "type": "string" + }, + "file": { + "description": "Path", + "type": "string" + } + }, + "required": [ + "file", + "content" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Patch a file by exact replacement: old → new. old must appear EXACTLY once (add context to make it unique). Prefer this over rewriting a whole file.", + "name": "edit", + "parameters": { + "properties": { + "file": { + "description": "Path", + "type": "string" + }, + "new": { + "description": "Replacement", + "type": "string" + }, + "old": { + "description": "Exact text to replace (unique)", + "type": "string" + } + }, + "required": [ + "file", + "old", + "new" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Bring back verbatim an earlier block archived when this conversation was compacted, from the id shown as recall:rN in the summary.", + "name": "recall", + "parameters": { + "properties": { + "id": { + "description": "Block id, e.g. \"r7\".", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Find an archived block and its recall id by keywords, when it is not listed in the current summary. Then call recall(id).", + "name": "recall_search", + "parameters": { + "properties": { + "limit": { + "description": "Default 5, max 20", + "type": "integer" + }, + "query": { + "description": "Keywords describing the block.", + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List your scheduled tasks with their state, next run, and last result. Pass an id to get that task's full last report.", + "name": "task_list", + "parameters": { + "properties": { + "id": { + "description": "Show only this task, with its full last report", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Schedule work to run later, once or repeating. Give 'prompt' for an AI task, OR 'script' (a saved script name) for a script task that runs with no model and no tokens.", + "name": "task_create", + "parameters": { + "properties": { + "at": { + "description": "One-time task at this local time: \"HH:MM\" (next occurrence) or \"YYYY-MM-DD HH:MM\"", + "type": "string" + }, + "enabled": { + "description": "Start active (default true)", + "type": "boolean" + }, + "in_minutes": { + "description": "One-time task, N minutes from now", + "type": "integer" + }, + "name": { + "type": "string" + }, + "prompt": { + "description": "AI task: self-contained instruction to run each time (omit if using script)", + "type": "string" + }, + "schedule": { + "description": "Repeating: \"@every 2h\" (interval), \"@every 1d@09:00\" (daily time), or a 5-field cron \"min hour dom mon dow\". Omit for a one-time task (use in_minutes or at)", + "type": "string" + }, + "script": { + "description": "Script task: name of a script file you put in the scripts folder, run each time with no AI involved", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Change one or more fields of a scheduled task by id.", + "name": "task_update", + "parameters": { + "properties": { + "enabled": { + "description": "false pauses, true resumes", + "type": "boolean" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "prompt": { + "type": "string" + }, + "schedule": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Delete a scheduled task by id.", + "name": "task_delete", + "parameters": { + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Read a file with 1-indexed line numbers (offset/limit for long files). ALWAYS use this instead of cat/type: output is bounded and reading a file is REQUIRED before you may edit or overwrite it.", + "name": "read", + "parameters": { + "properties": { + "file": { + "description": "Path (relative to the working folder)", + "type": "string" + }, + "limit": { + "description": "Lines (default 400, max 1000)", + "type": "integer" + }, + "offset": { + "description": "Start line (default 1)", + "type": "integer" + } + }, + "required": [ + "file" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Search file contents with a Go regular expression. Returns path:line: text matches (bounded). path may be a file or a directory (default: whole working folder).", + "name": "grep", + "parameters": { + "properties": { + "glob": { + "description": "Only files matching this glob (e.g. *.go)", + "type": "string" + }, + "path": { + "description": "File or directory to search (default .)", + "type": "string" + }, + "pattern": { + "description": "Go regexp", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List files matching a glob pattern (** supported, e.g. **/*.go), most recently modified first.", + "name": "glob", + "parameters": { + "properties": { + "pattern": { + "description": "Glob, e.g. src/**/*.ts", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Ask the user a clarifying question with 2-4 short options, then END your turn. Their answer arrives as the next user message. Use when a decision is genuinely theirs (scope, destructive change, ambiguous requirement) — not for things you can check yourself.", + "name": "ask", + "parameters": { + "properties": { + "options": { + "description": "2-4 short answer options", + "items": { + "type": "string" + }, + "type": "array" + }, + "question": { + "description": "The question", + "type": "string" + } + }, + "required": [ + "question" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Start a shell command in the BACKGROUND (dev server, watcher, long build) and return immediately with a job id. Output goes to a log file; check it with bash_tail. Use this instead of appending '&' in bash.", + "name": "bash_bg", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Read the last lines of a background job's output and its status (running/exited). kill:true stops the job.", + "name": "bash_tail", + "parameters": { + "properties": { + "id": { + "description": "Job id from bash_bg", + "type": "string" + }, + "kill": { + "description": "Stop the job", + "type": "boolean" + }, + "lines": { + "description": "Lines to show (default 40, max 200)", + "type": "integer" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "git status of the working folder (branch + changed files, porcelain format).", + "name": "git_status", + "parameters": { + "properties": { + "dir": { + "description": "Repository subfolder (default: auto-detected)", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "git diff of the working folder (unstaged + staged). Optional file to diff a single path.", + "name": "git_diff", + "parameters": { + "properties": { + "dir": { + "description": "Repository subfolder (default: auto-detected)", + "type": "string" + }, + "file": { + "description": "Limit the diff to this path", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Clone a git repository (https:// or git@) INTO the working folder of this discussion.", + "name": "git_clone", + "parameters": { + "properties": { + "dir": { + "description": "Target folder name (default: repo name)", + "type": "string" + }, + "url": { + "description": "Repository URL", + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Manage the acceptance criteria of the current task (the contract that defines DONE). action=add posits new criteria (texts[]), action=set updates one (id + status, optional note), action=list shows them, action=clear removes them all. Set criteria BEFORE building. Builder: mark each criterion completed once you have done AND checked it — verification only starts when nothing is left pending. Only the verification pass may mark passed or failed.", + "name": "criteria", + "parameters": { + "properties": { + "action": { + "enum": [ + "add", + "set", + "list", + "clear" + ], + "type": "string" + }, + "id": { + "description": "set: criterion id", + "type": "integer" + }, + "note": { + "description": "set: why it failed / how it was verified", + "type": "string" + }, + "status": { + "enum": [ + "pending", + "completed", + "passed", + "failed" + ], + "type": "string" + }, + "texts": { + "description": "add: one entry per criterion, short and testable", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "action" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Délègue une recherche ou une relecture à un rôle qui travaille dans son propre contexte et ne rend que sa réponse. Lecture seule.", + "name": "subagent", + "parameters": { + "properties": { + "role": { + "description": "explorer (trouver/cartographier), code-reviewer (relire un changement), planner (découper)", + "enum": [ + "explorer", + "code-reviewer", + "planner" + ], + "type": "string" + }, + "task": { + "description": "La question, précise et autonome : le rôle ne voit pas la discussion.", + "type": "string" + } + }, + "required": [ + "role", + "task" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Photographie une page web (JS exécuté) pour la MONTRER. La réponse donne la ligne markdown à recopier. Tu ne vois pas l'image.", + "name": "web_screenshot", + "parameters": { + "properties": { + "full_page": { + "description": "Page entière (lourd). Défaut false.", + "type": "boolean" + }, + "url": { + "description": "URL complète", + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" + } + }, + "type": "function" + } + ] + } + ], + "log": [ + { + "user": "Corrige le test" + }, + { + "content": "Corrigé.", + "seq0": 2, + "toks": 1 + }, + { + "stats": "…" + }, + { + "stats": "…" + }, + { + "stats": "…" + }, + { + "turn_done": true + } + ], + "messages": [ + { + "role": "user", + "content": "Corrige le test" + }, + { + "role": "assistant", + "content": "Corrigé." + } + ], + "status": 200 + } +} diff --git a/rust/loki-rs/tests/parity_turn/erreur_moteur.json b/rust/loki-rs/tests/parity_turn/erreur_moteur.json new file mode 100644 index 0000000..daf1f36 --- /dev/null +++ b/rust/loki-rs/tests/parity_turn/erreur_moteur.json @@ -0,0 +1,88 @@ +{ + "name": "erreur_moteur", + "cfg": { + "MEM_MODE": "off" + }, + "send": { + "cid": "c-erreur_moteur", + "message": "Bonjour" + }, + "replies": [ + { + "status": 400, + "lines": [ + "{\"error\":{\"code\":400,\"message\":\"requête invalide\",\"type\":\"invalid_request_error\"}}" + ] + } + ], + "tool_results": { + "bash": "a.txt\nb.txt", + "read": "1: ligne" + }, + "start": { + "caps": { + "Agent": false, + "Code": false, + "ComputerUse": false, + "Internet": false, + "Mem": "off", + "Role": "" + }, + "code_hint": false, + "content": "Bonjour", + "files": [], + "model": "", + "turn_caps": { + "Agent": false, + "Code": false, + "ComputerUse": false, + "Internet": false, + "Mem": "off", + "Role": "" + } + }, + "view": { + "head": [], + "prefix": "", + "tools": [] + }, + "expect": { + "requests": [ + { + "messages": [ + { + "content": "Bonjour", + "role": "user" + } + ], + "model": "loki", + "stream": true, + "stream_options": { + "include_usage": true + }, + "temperature": 0.7 + } + ], + "log": [ + { + "user": "Bonjour" + }, + { + "error": "llama-server a renvoyé 400 : {\"error\":{\"code\":400,\"message\":\"requête invalide\",\"type\":\"invalid_request_error\"}}" + }, + { + "ctx_used": 5 + }, + { + "turn_done": true + } + ], + "messages": [ + { + "role": "user", + "content": "Bonjour" + } + ], + "status": 200 + } +} diff --git a/rust/loki-rs/tests/parity_turn/outil_puis_reponse.json b/rust/loki-rs/tests/parity_turn/outil_puis_reponse.json new file mode 100644 index 0000000..ca0a5f8 --- /dev/null +++ b/rust/loki-rs/tests/parity_turn/outil_puis_reponse.json @@ -0,0 +1,870 @@ +{ + "name": "outil_puis_reponse", + "cfg": { + "MEM_MODE": "off" + }, + "agent": true, + "send": { + "cid": "c-outil_puis_reponse", + "message": "Liste les fichiers" + }, + "replies": [ + { + "status": 200, + "lines": [ + "data: {\"choices\":[{\"delta\":{\"tool_calls\":[{\"function\":{\"arguments\":\"{\\\"command\\\":\\\"ls\\\"}\",\"name\":\"bash\"},\"id\":\"c1\",\"index\":0,\"type\":\"function\"}]}}]}", + "data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"tool_calls\"}],\"usage\":{\"completion_tokens\":5,\"prompt_tokens\":100}}", + "data: [DONE]" + ] + }, + { + "status": 200, + "lines": [ + "data: {\"choices\":[{\"delta\":{\"content\":\"Deux fichiers.\"}}]}", + "data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"completion_tokens\":5,\"prompt_tokens\":100}}", + "data: [DONE]" + ] + } + ], + "tool_results": { + "bash": "a.txt\nb.txt", + "read": "1: ligne" + }, + "start": { + "caps": { + "Agent": true, + "Code": false, + "ComputerUse": false, + "Internet": false, + "Mem": "off", + "Role": "" + }, + "code_hint": false, + "content": "Liste les fichiers", + "files": [], + "model": "", + "turn_caps": { + "Agent": true, + "Code": false, + "ComputerUse": false, + "Internet": false, + "Mem": "off", + "Role": "" + } + }, + "view": { + "head": [], + "prefix": "You are Loki, an expert assistant operating directly on this machine with real tools.\n\nThe shell is bash: use its syntax.\nFor anything about the system or files, use bash instead of guessing. Act immediately — call the right tool, then answer. Never end your turn after only thinking. Be concise.\nTo give the user a file, link it in Markdown with its path relative to your working directory — [le rapport](rapport.pdf) — which downloads it. A raw server path is useless: they read you in a browser.\nYou can schedule work for yourself with task_create — only for what must recur or happen later, never a one-off you can do now. A script from your scripts folder runs as a task with no model at all.\n\nDate: {{DATE}}\n\nMachine: host={{HOST}}, cwd={{TMP}}/workspace/discussions/{{CONV}}. This is your working folder: relative paths in write/edit/bash resolve here, and it is the DEFAULT place for scratch work — notes, outputs, a clone, a test. But it is DISPOSABLE: deleting the discussion wipes it. Any script you want to KEEP (or schedule), write it into your scripts folder {{TMP}}/scripts instead — a separate folder a workspace wipe won't touch; you write and run scripts there normally. Do NOT install or write files into system directories such as /usr/local/bin, /usr, /bin or /etc: those need root and are not yours. Only use an absolute path outside this folder (except your scripts folder) when the user explicitly named that location.", + "tools": [ + { + "function": { + "description": "Run a shell command (bash syntax) and return stdout, stderr and exit code: inspect the system, read files and logs, run scripts. To CREATE or REWRITE a file use write instead — never echo/cat/python -c. Avoid destructive commands unless asked.", + "name": "bash", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + }, + "timeout": { + "description": "Timeout s (default 30, max 300)", + "type": "integer" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Create or replace a file with the exact content given (parent dirs created, content verbatim, no escaping). ALWAYS use this for a script or any text file — NEVER build one through the shell with echo, cat, python -c or Set-Content: quoting breaks.", + "name": "write", + "parameters": { + "properties": { + "content": { + "description": "Full content", + "type": "string" + }, + "file": { + "description": "Path", + "type": "string" + } + }, + "required": [ + "file", + "content" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Patch a file by exact replacement: old → new. old must appear EXACTLY once (add context to make it unique). Prefer this over rewriting a whole file.", + "name": "edit", + "parameters": { + "properties": { + "file": { + "description": "Path", + "type": "string" + }, + "new": { + "description": "Replacement", + "type": "string" + }, + "old": { + "description": "Exact text to replace (unique)", + "type": "string" + } + }, + "required": [ + "file", + "old", + "new" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Bring back verbatim an earlier block archived when this conversation was compacted, from the id shown as recall:rN in the summary.", + "name": "recall", + "parameters": { + "properties": { + "id": { + "description": "Block id, e.g. \"r7\".", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Find an archived block and its recall id by keywords, when it is not listed in the current summary. Then call recall(id).", + "name": "recall_search", + "parameters": { + "properties": { + "limit": { + "description": "Default 5, max 20", + "type": "integer" + }, + "query": { + "description": "Keywords describing the block.", + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List your scheduled tasks with their state, next run, and last result. Pass an id to get that task's full last report.", + "name": "task_list", + "parameters": { + "properties": { + "id": { + "description": "Show only this task, with its full last report", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Schedule work to run later, once or repeating. Give 'prompt' for an AI task, OR 'script' (a saved script name) for a script task that runs with no model and no tokens.", + "name": "task_create", + "parameters": { + "properties": { + "at": { + "description": "One-time task at this local time: \"HH:MM\" (next occurrence) or \"YYYY-MM-DD HH:MM\"", + "type": "string" + }, + "enabled": { + "description": "Start active (default true)", + "type": "boolean" + }, + "in_minutes": { + "description": "One-time task, N minutes from now", + "type": "integer" + }, + "name": { + "type": "string" + }, + "prompt": { + "description": "AI task: self-contained instruction to run each time (omit if using script)", + "type": "string" + }, + "schedule": { + "description": "Repeating: \"@every 2h\" (interval), \"@every 1d@09:00\" (daily time), or a 5-field cron \"min hour dom mon dow\". Omit for a one-time task (use in_minutes or at)", + "type": "string" + }, + "script": { + "description": "Script task: name of a script file you put in the scripts folder, run each time with no AI involved", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Change one or more fields of a scheduled task by id.", + "name": "task_update", + "parameters": { + "properties": { + "enabled": { + "description": "false pauses, true resumes", + "type": "boolean" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "prompt": { + "type": "string" + }, + "schedule": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Delete a scheduled task by id.", + "name": "task_delete", + "parameters": { + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + } + ] + }, + "expect": { + "requests": [ + { + "messages": [ + { + "content": "You are Loki, an expert assistant operating directly on this machine with real tools.\n\nThe shell is bash: use its syntax.\nFor anything about the system or files, use bash instead of guessing. Act immediately — call the right tool, then answer. Never end your turn after only thinking. Be concise.\nTo give the user a file, link it in Markdown with its path relative to your working directory — [le rapport](rapport.pdf) — which downloads it. A raw server path is useless: they read you in a browser.\nYou can schedule work for yourself with task_create — only for what must recur or happen later, never a one-off you can do now. A script from your scripts folder runs as a task with no model at all.\n\nDate: {{DATE}}\n\nMachine: host={{HOST}}, cwd={{TMP}}/workspace/discussions/{{CONV}}. This is your working folder: relative paths in write/edit/bash resolve here, and it is the DEFAULT place for scratch work — notes, outputs, a clone, a test. But it is DISPOSABLE: deleting the discussion wipes it. Any script you want to KEEP (or schedule), write it into your scripts folder {{TMP}}/scripts instead — a separate folder a workspace wipe won't touch; you write and run scripts there normally. Do NOT install or write files into system directories such as /usr/local/bin, /usr, /bin or /etc: those need root and are not yours. Only use an absolute path outside this folder (except your scripts folder) when the user explicitly named that location.", + "role": "system" + }, + { + "content": "Liste les fichiers", + "role": "user" + } + ], + "model": "loki", + "parallel_tool_calls": false, + "stream": true, + "stream_options": { + "include_usage": true + }, + "temperature": 0.7, + "tools": [ + { + "function": { + "description": "Run a shell command (bash syntax) and return stdout, stderr and exit code: inspect the system, read files and logs, run scripts. To CREATE or REWRITE a file use write instead — never echo/cat/python -c. Avoid destructive commands unless asked.", + "name": "bash", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + }, + "timeout": { + "description": "Timeout s (default 30, max 300)", + "type": "integer" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Create or replace a file with the exact content given (parent dirs created, content verbatim, no escaping). ALWAYS use this for a script or any text file — NEVER build one through the shell with echo, cat, python -c or Set-Content: quoting breaks.", + "name": "write", + "parameters": { + "properties": { + "content": { + "description": "Full content", + "type": "string" + }, + "file": { + "description": "Path", + "type": "string" + } + }, + "required": [ + "file", + "content" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Patch a file by exact replacement: old → new. old must appear EXACTLY once (add context to make it unique). Prefer this over rewriting a whole file.", + "name": "edit", + "parameters": { + "properties": { + "file": { + "description": "Path", + "type": "string" + }, + "new": { + "description": "Replacement", + "type": "string" + }, + "old": { + "description": "Exact text to replace (unique)", + "type": "string" + } + }, + "required": [ + "file", + "old", + "new" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Bring back verbatim an earlier block archived when this conversation was compacted, from the id shown as recall:rN in the summary.", + "name": "recall", + "parameters": { + "properties": { + "id": { + "description": "Block id, e.g. \"r7\".", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Find an archived block and its recall id by keywords, when it is not listed in the current summary. Then call recall(id).", + "name": "recall_search", + "parameters": { + "properties": { + "limit": { + "description": "Default 5, max 20", + "type": "integer" + }, + "query": { + "description": "Keywords describing the block.", + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List your scheduled tasks with their state, next run, and last result. Pass an id to get that task's full last report.", + "name": "task_list", + "parameters": { + "properties": { + "id": { + "description": "Show only this task, with its full last report", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Schedule work to run later, once or repeating. Give 'prompt' for an AI task, OR 'script' (a saved script name) for a script task that runs with no model and no tokens.", + "name": "task_create", + "parameters": { + "properties": { + "at": { + "description": "One-time task at this local time: \"HH:MM\" (next occurrence) or \"YYYY-MM-DD HH:MM\"", + "type": "string" + }, + "enabled": { + "description": "Start active (default true)", + "type": "boolean" + }, + "in_minutes": { + "description": "One-time task, N minutes from now", + "type": "integer" + }, + "name": { + "type": "string" + }, + "prompt": { + "description": "AI task: self-contained instruction to run each time (omit if using script)", + "type": "string" + }, + "schedule": { + "description": "Repeating: \"@every 2h\" (interval), \"@every 1d@09:00\" (daily time), or a 5-field cron \"min hour dom mon dow\". Omit for a one-time task (use in_minutes or at)", + "type": "string" + }, + "script": { + "description": "Script task: name of a script file you put in the scripts folder, run each time with no AI involved", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Change one or more fields of a scheduled task by id.", + "name": "task_update", + "parameters": { + "properties": { + "enabled": { + "description": "false pauses, true resumes", + "type": "boolean" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "prompt": { + "type": "string" + }, + "schedule": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Delete a scheduled task by id.", + "name": "task_delete", + "parameters": { + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + } + ] + }, + { + "messages": [ + { + "content": "You are Loki, an expert assistant operating directly on this machine with real tools.\n\nThe shell is bash: use its syntax.\nFor anything about the system or files, use bash instead of guessing. Act immediately — call the right tool, then answer. Never end your turn after only thinking. Be concise.\nTo give the user a file, link it in Markdown with its path relative to your working directory — [le rapport](rapport.pdf) — which downloads it. A raw server path is useless: they read you in a browser.\nYou can schedule work for yourself with task_create — only for what must recur or happen later, never a one-off you can do now. A script from your scripts folder runs as a task with no model at all.\n\nDate: {{DATE}}\n\nMachine: host={{HOST}}, cwd={{TMP}}/workspace/discussions/{{CONV}}. This is your working folder: relative paths in write/edit/bash resolve here, and it is the DEFAULT place for scratch work — notes, outputs, a clone, a test. But it is DISPOSABLE: deleting the discussion wipes it. Any script you want to KEEP (or schedule), write it into your scripts folder {{TMP}}/scripts instead — a separate folder a workspace wipe won't touch; you write and run scripts there normally. Do NOT install or write files into system directories such as /usr/local/bin, /usr, /bin or /etc: those need root and are not yours. Only use an absolute path outside this folder (except your scripts folder) when the user explicitly named that location.", + "role": "system" + }, + { + "content": "Liste les fichiers", + "role": "user" + }, + { + "role": "assistant", + "tool_calls": [ + { + "function": { + "arguments": "{\"command\":\"ls\"}", + "name": "bash" + }, + "id": "c1", + "type": "function" + } + ] + }, + { + "content": "a.txt\nb.txt", + "role": "tool", + "tool_call_id": "c1" + } + ], + "model": "loki", + "parallel_tool_calls": false, + "stream": true, + "stream_options": { + "include_usage": true + }, + "temperature": 0.7, + "tools": [ + { + "function": { + "description": "Run a shell command (bash syntax) and return stdout, stderr and exit code: inspect the system, read files and logs, run scripts. To CREATE or REWRITE a file use write instead — never echo/cat/python -c. Avoid destructive commands unless asked.", + "name": "bash", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + }, + "timeout": { + "description": "Timeout s (default 30, max 300)", + "type": "integer" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Create or replace a file with the exact content given (parent dirs created, content verbatim, no escaping). ALWAYS use this for a script or any text file — NEVER build one through the shell with echo, cat, python -c or Set-Content: quoting breaks.", + "name": "write", + "parameters": { + "properties": { + "content": { + "description": "Full content", + "type": "string" + }, + "file": { + "description": "Path", + "type": "string" + } + }, + "required": [ + "file", + "content" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Patch a file by exact replacement: old → new. old must appear EXACTLY once (add context to make it unique). Prefer this over rewriting a whole file.", + "name": "edit", + "parameters": { + "properties": { + "file": { + "description": "Path", + "type": "string" + }, + "new": { + "description": "Replacement", + "type": "string" + }, + "old": { + "description": "Exact text to replace (unique)", + "type": "string" + } + }, + "required": [ + "file", + "old", + "new" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Bring back verbatim an earlier block archived when this conversation was compacted, from the id shown as recall:rN in the summary.", + "name": "recall", + "parameters": { + "properties": { + "id": { + "description": "Block id, e.g. \"r7\".", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Find an archived block and its recall id by keywords, when it is not listed in the current summary. Then call recall(id).", + "name": "recall_search", + "parameters": { + "properties": { + "limit": { + "description": "Default 5, max 20", + "type": "integer" + }, + "query": { + "description": "Keywords describing the block.", + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List your scheduled tasks with their state, next run, and last result. Pass an id to get that task's full last report.", + "name": "task_list", + "parameters": { + "properties": { + "id": { + "description": "Show only this task, with its full last report", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Schedule work to run later, once or repeating. Give 'prompt' for an AI task, OR 'script' (a saved script name) for a script task that runs with no model and no tokens.", + "name": "task_create", + "parameters": { + "properties": { + "at": { + "description": "One-time task at this local time: \"HH:MM\" (next occurrence) or \"YYYY-MM-DD HH:MM\"", + "type": "string" + }, + "enabled": { + "description": "Start active (default true)", + "type": "boolean" + }, + "in_minutes": { + "description": "One-time task, N minutes from now", + "type": "integer" + }, + "name": { + "type": "string" + }, + "prompt": { + "description": "AI task: self-contained instruction to run each time (omit if using script)", + "type": "string" + }, + "schedule": { + "description": "Repeating: \"@every 2h\" (interval), \"@every 1d@09:00\" (daily time), or a 5-field cron \"min hour dom mon dow\". Omit for a one-time task (use in_minutes or at)", + "type": "string" + }, + "script": { + "description": "Script task: name of a script file you put in the scripts folder, run each time with no AI involved", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Change one or more fields of a scheduled task by id.", + "name": "task_update", + "parameters": { + "properties": { + "enabled": { + "description": "false pauses, true resumes", + "type": "boolean" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "prompt": { + "type": "string" + }, + "schedule": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Delete a scheduled task by id.", + "name": "task_delete", + "parameters": { + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + } + ] + } + ], + "log": [ + { + "user": "Liste les fichiers" + }, + { + "stats": "…" + }, + { + "stats": "…" + }, + { + "stats": "…" + }, + { + "tool_used": { + "done": true, + "label": "ls", + "name": "bash", + "result": "a.txt\nb.txt", + "result_chars": 11, + "typing": false + } + }, + { + "content": "Deux fichiers.", + "seq0": 8, + "toks": 1 + }, + { + "stats": "…" + }, + { + "stats": "…" + }, + { + "stats": "…" + }, + { + "turn_done": true + } + ], + "messages": [ + { + "role": "user", + "content": "Liste les fichiers" + }, + { + "role": "assistant", + "tool_calls": [ + { + "id": "c1", + "type": "function", + "function": { + "name": "bash", + "arguments": "{\"command\":\"ls\"}" + } + } + ] + }, + { + "role": "tool", + "content": "a.txt\nb.txt", + "tool_call_id": "c1" + }, + { + "role": "assistant", + "content": "Deux fichiers." + } + ], + "status": 200 + } +} diff --git a/rust/loki-rs/tests/parity_turn/pense_sans_agir.json b/rust/loki-rs/tests/parity_turn/pense_sans_agir.json new file mode 100644 index 0000000..1fe5772 --- /dev/null +++ b/rust/loki-rs/tests/parity_turn/pense_sans_agir.json @@ -0,0 +1,836 @@ +{ + "name": "pense_sans_agir", + "cfg": { + "MEM_MODE": "off" + }, + "agent": true, + "send": { + "cid": "c-pense_sans_agir", + "message": "Cherche" + }, + "replies": [ + { + "status": 200, + "lines": [ + "data: {\"choices\":[{\"delta\":{\"reasoning_content\":\"Il faudrait lister.\"}}]}", + "data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"completion_tokens\":5,\"prompt_tokens\":100}}", + "data: [DONE]" + ] + }, + { + "status": 200, + "lines": [ + "data: {\"choices\":[{\"delta\":{\"content\":\"Voilà.\"}}]}", + "data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"completion_tokens\":5,\"prompt_tokens\":100}}", + "data: [DONE]" + ] + } + ], + "tool_results": { + "bash": "a.txt\nb.txt", + "read": "1: ligne" + }, + "start": { + "caps": { + "Agent": true, + "Code": false, + "ComputerUse": false, + "Internet": false, + "Mem": "off", + "Role": "" + }, + "code_hint": false, + "content": "Cherche", + "files": [], + "model": "", + "turn_caps": { + "Agent": true, + "Code": false, + "ComputerUse": false, + "Internet": false, + "Mem": "off", + "Role": "" + } + }, + "view": { + "head": [], + "prefix": "You are Loki, an expert assistant operating directly on this machine with real tools.\n\nThe shell is bash: use its syntax.\nFor anything about the system or files, use bash instead of guessing. Act immediately — call the right tool, then answer. Never end your turn after only thinking. Be concise.\nTo give the user a file, link it in Markdown with its path relative to your working directory — [le rapport](rapport.pdf) — which downloads it. A raw server path is useless: they read you in a browser.\nYou can schedule work for yourself with task_create — only for what must recur or happen later, never a one-off you can do now. A script from your scripts folder runs as a task with no model at all.\n\nDate: {{DATE}}\n\nMachine: host={{HOST}}, cwd={{TMP}}/workspace/discussions/{{CONV}}. This is your working folder: relative paths in write/edit/bash resolve here, and it is the DEFAULT place for scratch work — notes, outputs, a clone, a test. But it is DISPOSABLE: deleting the discussion wipes it. Any script you want to KEEP (or schedule), write it into your scripts folder {{TMP}}/scripts instead — a separate folder a workspace wipe won't touch; you write and run scripts there normally. Do NOT install or write files into system directories such as /usr/local/bin, /usr, /bin or /etc: those need root and are not yours. Only use an absolute path outside this folder (except your scripts folder) when the user explicitly named that location.", + "tools": [ + { + "function": { + "description": "Run a shell command (bash syntax) and return stdout, stderr and exit code: inspect the system, read files and logs, run scripts. To CREATE or REWRITE a file use write instead — never echo/cat/python -c. Avoid destructive commands unless asked.", + "name": "bash", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + }, + "timeout": { + "description": "Timeout s (default 30, max 300)", + "type": "integer" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Create or replace a file with the exact content given (parent dirs created, content verbatim, no escaping). ALWAYS use this for a script or any text file — NEVER build one through the shell with echo, cat, python -c or Set-Content: quoting breaks.", + "name": "write", + "parameters": { + "properties": { + "content": { + "description": "Full content", + "type": "string" + }, + "file": { + "description": "Path", + "type": "string" + } + }, + "required": [ + "file", + "content" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Patch a file by exact replacement: old → new. old must appear EXACTLY once (add context to make it unique). Prefer this over rewriting a whole file.", + "name": "edit", + "parameters": { + "properties": { + "file": { + "description": "Path", + "type": "string" + }, + "new": { + "description": "Replacement", + "type": "string" + }, + "old": { + "description": "Exact text to replace (unique)", + "type": "string" + } + }, + "required": [ + "file", + "old", + "new" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Bring back verbatim an earlier block archived when this conversation was compacted, from the id shown as recall:rN in the summary.", + "name": "recall", + "parameters": { + "properties": { + "id": { + "description": "Block id, e.g. \"r7\".", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Find an archived block and its recall id by keywords, when it is not listed in the current summary. Then call recall(id).", + "name": "recall_search", + "parameters": { + "properties": { + "limit": { + "description": "Default 5, max 20", + "type": "integer" + }, + "query": { + "description": "Keywords describing the block.", + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List your scheduled tasks with their state, next run, and last result. Pass an id to get that task's full last report.", + "name": "task_list", + "parameters": { + "properties": { + "id": { + "description": "Show only this task, with its full last report", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Schedule work to run later, once or repeating. Give 'prompt' for an AI task, OR 'script' (a saved script name) for a script task that runs with no model and no tokens.", + "name": "task_create", + "parameters": { + "properties": { + "at": { + "description": "One-time task at this local time: \"HH:MM\" (next occurrence) or \"YYYY-MM-DD HH:MM\"", + "type": "string" + }, + "enabled": { + "description": "Start active (default true)", + "type": "boolean" + }, + "in_minutes": { + "description": "One-time task, N minutes from now", + "type": "integer" + }, + "name": { + "type": "string" + }, + "prompt": { + "description": "AI task: self-contained instruction to run each time (omit if using script)", + "type": "string" + }, + "schedule": { + "description": "Repeating: \"@every 2h\" (interval), \"@every 1d@09:00\" (daily time), or a 5-field cron \"min hour dom mon dow\". Omit for a one-time task (use in_minutes or at)", + "type": "string" + }, + "script": { + "description": "Script task: name of a script file you put in the scripts folder, run each time with no AI involved", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Change one or more fields of a scheduled task by id.", + "name": "task_update", + "parameters": { + "properties": { + "enabled": { + "description": "false pauses, true resumes", + "type": "boolean" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "prompt": { + "type": "string" + }, + "schedule": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Delete a scheduled task by id.", + "name": "task_delete", + "parameters": { + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + } + ] + }, + "expect": { + "requests": [ + { + "messages": [ + { + "content": "You are Loki, an expert assistant operating directly on this machine with real tools.\n\nThe shell is bash: use its syntax.\nFor anything about the system or files, use bash instead of guessing. Act immediately — call the right tool, then answer. Never end your turn after only thinking. Be concise.\nTo give the user a file, link it in Markdown with its path relative to your working directory — [le rapport](rapport.pdf) — which downloads it. A raw server path is useless: they read you in a browser.\nYou can schedule work for yourself with task_create — only for what must recur or happen later, never a one-off you can do now. A script from your scripts folder runs as a task with no model at all.\n\nDate: {{DATE}}\n\nMachine: host={{HOST}}, cwd={{TMP}}/workspace/discussions/{{CONV}}. This is your working folder: relative paths in write/edit/bash resolve here, and it is the DEFAULT place for scratch work — notes, outputs, a clone, a test. But it is DISPOSABLE: deleting the discussion wipes it. Any script you want to KEEP (or schedule), write it into your scripts folder {{TMP}}/scripts instead — a separate folder a workspace wipe won't touch; you write and run scripts there normally. Do NOT install or write files into system directories such as /usr/local/bin, /usr, /bin or /etc: those need root and are not yours. Only use an absolute path outside this folder (except your scripts folder) when the user explicitly named that location.", + "role": "system" + }, + { + "content": "Cherche", + "role": "user" + } + ], + "model": "loki", + "parallel_tool_calls": false, + "stream": true, + "stream_options": { + "include_usage": true + }, + "temperature": 0.7, + "tools": [ + { + "function": { + "description": "Run a shell command (bash syntax) and return stdout, stderr and exit code: inspect the system, read files and logs, run scripts. To CREATE or REWRITE a file use write instead — never echo/cat/python -c. Avoid destructive commands unless asked.", + "name": "bash", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + }, + "timeout": { + "description": "Timeout s (default 30, max 300)", + "type": "integer" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Create or replace a file with the exact content given (parent dirs created, content verbatim, no escaping). ALWAYS use this for a script or any text file — NEVER build one through the shell with echo, cat, python -c or Set-Content: quoting breaks.", + "name": "write", + "parameters": { + "properties": { + "content": { + "description": "Full content", + "type": "string" + }, + "file": { + "description": "Path", + "type": "string" + } + }, + "required": [ + "file", + "content" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Patch a file by exact replacement: old → new. old must appear EXACTLY once (add context to make it unique). Prefer this over rewriting a whole file.", + "name": "edit", + "parameters": { + "properties": { + "file": { + "description": "Path", + "type": "string" + }, + "new": { + "description": "Replacement", + "type": "string" + }, + "old": { + "description": "Exact text to replace (unique)", + "type": "string" + } + }, + "required": [ + "file", + "old", + "new" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Bring back verbatim an earlier block archived when this conversation was compacted, from the id shown as recall:rN in the summary.", + "name": "recall", + "parameters": { + "properties": { + "id": { + "description": "Block id, e.g. \"r7\".", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Find an archived block and its recall id by keywords, when it is not listed in the current summary. Then call recall(id).", + "name": "recall_search", + "parameters": { + "properties": { + "limit": { + "description": "Default 5, max 20", + "type": "integer" + }, + "query": { + "description": "Keywords describing the block.", + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List your scheduled tasks with their state, next run, and last result. Pass an id to get that task's full last report.", + "name": "task_list", + "parameters": { + "properties": { + "id": { + "description": "Show only this task, with its full last report", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Schedule work to run later, once or repeating. Give 'prompt' for an AI task, OR 'script' (a saved script name) for a script task that runs with no model and no tokens.", + "name": "task_create", + "parameters": { + "properties": { + "at": { + "description": "One-time task at this local time: \"HH:MM\" (next occurrence) or \"YYYY-MM-DD HH:MM\"", + "type": "string" + }, + "enabled": { + "description": "Start active (default true)", + "type": "boolean" + }, + "in_minutes": { + "description": "One-time task, N minutes from now", + "type": "integer" + }, + "name": { + "type": "string" + }, + "prompt": { + "description": "AI task: self-contained instruction to run each time (omit if using script)", + "type": "string" + }, + "schedule": { + "description": "Repeating: \"@every 2h\" (interval), \"@every 1d@09:00\" (daily time), or a 5-field cron \"min hour dom mon dow\". Omit for a one-time task (use in_minutes or at)", + "type": "string" + }, + "script": { + "description": "Script task: name of a script file you put in the scripts folder, run each time with no AI involved", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Change one or more fields of a scheduled task by id.", + "name": "task_update", + "parameters": { + "properties": { + "enabled": { + "description": "false pauses, true resumes", + "type": "boolean" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "prompt": { + "type": "string" + }, + "schedule": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Delete a scheduled task by id.", + "name": "task_delete", + "parameters": { + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + } + ] + }, + { + "messages": [ + { + "content": "You are Loki, an expert assistant operating directly on this machine with real tools.\n\nThe shell is bash: use its syntax.\nFor anything about the system or files, use bash instead of guessing. Act immediately — call the right tool, then answer. Never end your turn after only thinking. Be concise.\nTo give the user a file, link it in Markdown with its path relative to your working directory — [le rapport](rapport.pdf) — which downloads it. A raw server path is useless: they read you in a browser.\nYou can schedule work for yourself with task_create — only for what must recur or happen later, never a one-off you can do now. A script from your scripts folder runs as a task with no model at all.\n\nDate: {{DATE}}\n\nMachine: host={{HOST}}, cwd={{TMP}}/workspace/discussions/{{CONV}}. This is your working folder: relative paths in write/edit/bash resolve here, and it is the DEFAULT place for scratch work — notes, outputs, a clone, a test. But it is DISPOSABLE: deleting the discussion wipes it. Any script you want to KEEP (or schedule), write it into your scripts folder {{TMP}}/scripts instead — a separate folder a workspace wipe won't touch; you write and run scripts there normally. Do NOT install or write files into system directories such as /usr/local/bin, /usr, /bin or /etc: those need root and are not yours. Only use an absolute path outside this folder (except your scripts folder) when the user explicitly named that location.", + "role": "system" + }, + { + "content": "Cherche", + "role": "user" + }, + { + "content": "You reasoned but did not call a tool or answer. Act NOW: call the appropriate tool directly (e.g. mem_search/mem_read/bash), or give your final answer if you already have the info. Don't explain, act.", + "role": "user" + } + ], + "model": "loki", + "parallel_tool_calls": false, + "stream": true, + "stream_options": { + "include_usage": true + }, + "temperature": 0.7, + "tools": [ + { + "function": { + "description": "Run a shell command (bash syntax) and return stdout, stderr and exit code: inspect the system, read files and logs, run scripts. To CREATE or REWRITE a file use write instead — never echo/cat/python -c. Avoid destructive commands unless asked.", + "name": "bash", + "parameters": { + "properties": { + "command": { + "description": "The command", + "type": "string" + }, + "timeout": { + "description": "Timeout s (default 30, max 300)", + "type": "integer" + } + }, + "required": [ + "command" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Create or replace a file with the exact content given (parent dirs created, content verbatim, no escaping). ALWAYS use this for a script or any text file — NEVER build one through the shell with echo, cat, python -c or Set-Content: quoting breaks.", + "name": "write", + "parameters": { + "properties": { + "content": { + "description": "Full content", + "type": "string" + }, + "file": { + "description": "Path", + "type": "string" + } + }, + "required": [ + "file", + "content" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Patch a file by exact replacement: old → new. old must appear EXACTLY once (add context to make it unique). Prefer this over rewriting a whole file.", + "name": "edit", + "parameters": { + "properties": { + "file": { + "description": "Path", + "type": "string" + }, + "new": { + "description": "Replacement", + "type": "string" + }, + "old": { + "description": "Exact text to replace (unique)", + "type": "string" + } + }, + "required": [ + "file", + "old", + "new" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Bring back verbatim an earlier block archived when this conversation was compacted, from the id shown as recall:rN in the summary.", + "name": "recall", + "parameters": { + "properties": { + "id": { + "description": "Block id, e.g. \"r7\".", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Find an archived block and its recall id by keywords, when it is not listed in the current summary. Then call recall(id).", + "name": "recall_search", + "parameters": { + "properties": { + "limit": { + "description": "Default 5, max 20", + "type": "integer" + }, + "query": { + "description": "Keywords describing the block.", + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "List your scheduled tasks with their state, next run, and last result. Pass an id to get that task's full last report.", + "name": "task_list", + "parameters": { + "properties": { + "id": { + "description": "Show only this task, with its full last report", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Schedule work to run later, once or repeating. Give 'prompt' for an AI task, OR 'script' (a saved script name) for a script task that runs with no model and no tokens.", + "name": "task_create", + "parameters": { + "properties": { + "at": { + "description": "One-time task at this local time: \"HH:MM\" (next occurrence) or \"YYYY-MM-DD HH:MM\"", + "type": "string" + }, + "enabled": { + "description": "Start active (default true)", + "type": "boolean" + }, + "in_minutes": { + "description": "One-time task, N minutes from now", + "type": "integer" + }, + "name": { + "type": "string" + }, + "prompt": { + "description": "AI task: self-contained instruction to run each time (omit if using script)", + "type": "string" + }, + "schedule": { + "description": "Repeating: \"@every 2h\" (interval), \"@every 1d@09:00\" (daily time), or a 5-field cron \"min hour dom mon dow\". Omit for a one-time task (use in_minutes or at)", + "type": "string" + }, + "script": { + "description": "Script task: name of a script file you put in the scripts folder, run each time with no AI involved", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Change one or more fields of a scheduled task by id.", + "name": "task_update", + "parameters": { + "properties": { + "enabled": { + "description": "false pauses, true resumes", + "type": "boolean" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "prompt": { + "type": "string" + }, + "schedule": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + }, + { + "function": { + "description": "Delete a scheduled task by id.", + "name": "task_delete", + "parameters": { + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "function" + } + ] + } + ], + "log": [ + { + "user": "Cherche" + }, + { + "reasoning_content": "Il faudrait lister.", + "seq0": 2, + "toks": 1 + }, + { + "stats": "…" + }, + { + "stats": "…" + }, + { + "stats": "…" + }, + { + "drop_reasoning": true + }, + { + "content": "Voilà.", + "seq0": 7, + "toks": 1 + }, + { + "stats": "…" + }, + { + "stats": "…" + }, + { + "stats": "…" + }, + { + "turn_done": true + } + ], + "messages": [ + { + "role": "user", + "content": "Cherche" + }, + { + "role": "assistant", + "content": "Voilà." + } + ], + "status": 200 + } +} diff --git a/rust/loki-rs/tests/parity_turn/raisonnement.json b/rust/loki-rs/tests/parity_turn/raisonnement.json new file mode 100644 index 0000000..bc9e3e8 --- /dev/null +++ b/rust/loki-rs/tests/parity_turn/raisonnement.json @@ -0,0 +1,108 @@ +{ + "name": "raisonnement", + "cfg": { + "MEM_MODE": "off" + }, + "send": { + "cid": "c-raisonnement", + "message": "Combien ?" + }, + "replies": [ + { + "status": 200, + "lines": [ + "data: {\"choices\":[{\"delta\":{\"reasoning_content\":\"Je compte.\"}}]}", + "data: {\"choices\":[{\"delta\":{\"content\":\"Quatre.\"}}]}", + "data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"completion_tokens\":5,\"prompt_tokens\":100}}", + "data: [DONE]" + ] + } + ], + "tool_results": { + "bash": "a.txt\nb.txt", + "read": "1: ligne" + }, + "start": { + "caps": { + "Agent": false, + "Code": false, + "ComputerUse": false, + "Internet": false, + "Mem": "off", + "Role": "" + }, + "code_hint": false, + "content": "Combien ?", + "files": [], + "model": "", + "turn_caps": { + "Agent": false, + "Code": false, + "ComputerUse": false, + "Internet": false, + "Mem": "off", + "Role": "" + } + }, + "view": { + "head": [], + "prefix": "", + "tools": [] + }, + "expect": { + "requests": [ + { + "messages": [ + { + "content": "Combien ?", + "role": "user" + } + ], + "model": "loki", + "stream": true, + "stream_options": { + "include_usage": true + }, + "temperature": 0.7 + } + ], + "log": [ + { + "user": "Combien ?" + }, + { + "reasoning_content": "Je compte.", + "seq0": 2, + "toks": 1 + }, + { + "content": "Quatre.", + "seq0": 3, + "toks": 1 + }, + { + "stats": "…" + }, + { + "stats": "…" + }, + { + "stats": "…" + }, + { + "turn_done": true + } + ], + "messages": [ + { + "role": "user", + "content": "Combien ?" + }, + { + "role": "assistant", + "content": "Quatre." + } + ], + "status": 200 + } +} diff --git a/rust/loki-rs/tests/parity_turn/reponse_simple.json b/rust/loki-rs/tests/parity_turn/reponse_simple.json new file mode 100644 index 0000000..f5631ea --- /dev/null +++ b/rust/loki-rs/tests/parity_turn/reponse_simple.json @@ -0,0 +1,103 @@ +{ + "name": "reponse_simple", + "cfg": { + "MEM_MODE": "off" + }, + "send": { + "cid": "c-reponse_simple", + "message": "Bonjour" + }, + "replies": [ + { + "status": 200, + "lines": [ + "data: {\"choices\":[{\"delta\":{\"content\":\"Bon\"}}]}", + "data: {\"choices\":[{\"delta\":{\"content\":\"jour !\"}}]}", + "data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"completion_tokens\":5,\"prompt_tokens\":100}}", + "data: [DONE]" + ] + } + ], + "tool_results": { + "bash": "a.txt\nb.txt", + "read": "1: ligne" + }, + "start": { + "caps": { + "Agent": false, + "Code": false, + "ComputerUse": false, + "Internet": false, + "Mem": "off", + "Role": "" + }, + "code_hint": false, + "content": "Bonjour", + "files": [], + "model": "", + "turn_caps": { + "Agent": false, + "Code": false, + "ComputerUse": false, + "Internet": false, + "Mem": "off", + "Role": "" + } + }, + "view": { + "head": [], + "prefix": "", + "tools": [] + }, + "expect": { + "requests": [ + { + "messages": [ + { + "content": "Bonjour", + "role": "user" + } + ], + "model": "loki", + "stream": true, + "stream_options": { + "include_usage": true + }, + "temperature": 0.7 + } + ], + "log": [ + { + "user": "Bonjour" + }, + { + "content": "Bonjour !", + "seq0": 2, + "toks": 2 + }, + { + "stats": "…" + }, + { + "stats": "…" + }, + { + "stats": "…" + }, + { + "turn_done": true + } + ], + "messages": [ + { + "role": "user", + "content": "Bonjour" + }, + { + "role": "assistant", + "content": "Bonjour !" + } + ], + "status": 200 + } +}