From 77f27a40d839245d2b6fee98f6121f7aede77f14 Mon Sep 17 00:00:00 2001 From: Shane Lindsay Date: Fri, 17 Apr 2026 20:58:37 +0100 Subject: [PATCH] fix: align supervision defaults and status handling --- README.md | 45 ++++++++++++---- src/engine.ts | 11 +++- src/index.ts | 111 +++++++++++++++++++++------------------ src/state.ts | 63 +++++++++++++++++----- src/types.ts | 7 +++ src/ui/settings-panel.ts | 2 +- 6 files changed, 165 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 8ebf5c0..dbfe310 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # pi-supervisor -A [pi](https://pi.dev) extension that supervises the coding agent and steers it toward a defined outcome. It observes every conversation turn, injects guiding messages when the agent drifts, and signals when the goal is reached — like a tech lead watching over a dev's shoulder. +A [pi](https://pi.dev) extension that supervises the coding agent and steers it toward a defined goal. It observes every conversation turn, injects guiding messages when the agent drifts, and signals when the goal is reached — like a tech lead watching over a dev's shoulder. > A supervisor as the intelligent overseer keeping the agent on track. @@ -20,6 +20,16 @@ https://github.com/user-attachments/assets/f3b23662-6473-4ac3-82f7-c7f9b64fa7c7 Then start the conversation normally — the supervisor watches from outside without modifying the agent's context. +The supervisor only sees the active goal, a recent slice of the conversation, +the latest summary if one exists, and recent steering history. It does not +inspect the repo or run tools itself. + +If you are not sure how to phrase the goal, ask the agent to draft one for you first. A good draft can be either: + +- a single high-signal line for simple work +- a multi-line plain-language brief +- a slightly longer structured brief with sub-goals, phases, priorities, or constraints + 1. **After each run** — a separate supervisor LLM analyzes the conversation against the goal (all sensitivities) 2. **Mid-run, between tool calls** — also checks for drift on `medium` and `high` sensitivity and can steer the agent without waiting for it to finish 3. **On completion** — supervisor signals done and stops automatically @@ -42,10 +52,10 @@ pi -e ~/projects/pi-supervisor/src/index.ts | Command | Description | |---|---| -| `/supervise ` | Start supervising toward a desired outcome | +| `/supervise ` | Start supervising toward a desired goal | | `/supervise` or `/supervise settings` | Open the interactive settings panel | | `/supervise stop` | Stop active supervision | -| `/supervise status` | Show current state (opens settings panel if active) | +| `/supervise status` | Open current supervision settings when active | | `/supervise widget` | Toggle the status widget on/off | | `/supervise model` | Open the interactive model picker | | `/supervise model ` | Set supervisor model directly | @@ -56,6 +66,14 @@ pi -e ~/projects/pi-supervisor/src/index.ts ``` /supervise Refactor the auth module to use dependency injection and add 90% test coverage +# If you want help first, ask the agent to draft a goal. +# For example: "Draft a supervision goal for fixing the hook runtime." + +# A simple drafted goal might be: +/supervise Fix the hook runtime, keep the change minimal, and leave the supervision loop working cleanly + +# A longer drafted goal can be passed through the tool instead of typing it inline. + /supervise model # Opens pi's model selector — pick any model with a configured API key @@ -65,7 +83,14 @@ pi -e ~/projects/pi-supervisor/src/index.ts /supervise stop ``` -The agent can also initiate supervision itself by calling the `start_supervision` tool — useful when it recognises a task needs goal tracking. Once active, supervision is locked: only the user can change or stop it. +The agent can also initiate supervision itself by calling the `start_supervision` tool with the goal text directly — useful when it recognises a task needs goal tracking. Once active, supervision is locked: only the user can change or stop it. + +That means a good workflow is often: + +1. ask the agent to draft a goal +2. start with the highest-level goal on the first line +3. keep it as a one-line goal if that is enough +4. add sub-goals, phases, priorities, or constraints only when they help ## UI @@ -76,7 +101,7 @@ Run `/supervise` (no args) or `/supervise settings` to open the interactive sett - **Model** — shows current model; press Enter to browse all available models - **Sensitivity** — cycle through `low`/`medium`/`high` with Enter or Space - **Widget** — toggle visibility -- **Outcome** (when active) — shows goal, steer count, and turn count +- **Goal** (when active) — shows goal, steer count, and turn count - **Stop** (when active) — stop supervision directly from the panel Navigate with arrow keys, Escape to close. Changes are applied on close. @@ -104,7 +129,7 @@ The second line shows the supervisor's reasoning as it streams. Toggle the widge | `medium` (default) | End of run + every 3rd tool cycle mid-run | ≥ 0.90 | On clear drift | | `high` | End of run + every tool cycle mid-run | ≥ 0.85 | Proactively | -**End-of-run** (`agent_end`): fires once per user prompt after the agent finishes and goes idle. The supervisor must decide `done`, `steer`, or `continue`. +**End-of-run** (`agent_end`): fires once per user prompt after the agent finishes and goes idle. The supervisor must decide `done` or `steer`. Idle `continue` responses are converted into a steer so the run does not stall. **Mid-run** (`turn_end`): fires after each LLM tool-call cycle while the agent is still working. Steering is injected immediately (interrupting the current run) only when confidence exceeds the threshold. The agent has at least 2 sub-turns to settle before mid-run checks begin. @@ -118,19 +143,21 @@ The supervisor runs on a **separate model** — it can be a cheaper/faster model 3. Active chat model (`ctx.model`) — so it works out of the box with no configuration 4. Built-in default: `anthropic/claude-haiku-4-5-20251001` -Change at any time with `/supervise model` (interactive picker) or `/supervise model ` (direct). The selection is saved to `.pi/supervisor-config.json` if the `.pi/` directory exists. +Change at any time with `/supervise model` (interactive picker) or `/supervise model ` (direct). If supervision is inactive, the new selection becomes the default for the next `/supervise`. The model is also saved to `.pi/supervisor-config.json` if the `.pi/` directory exists. ## Focus and Goal Discipline The supervisor is a pure outside observer — it does not modify the agent's system prompt. Goal discipline is enforced entirely through steering messages when the agent drifts. If the agent asks an out-of-scope clarifying question, the supervisor redirects it back to the goal rather than answering. +In practice, the goal should read like a compact run brief, not a full spec. The full goal text is reused on every supervisor check, so it is fine for the goal to be a single line, a multi-line plain-language brief, or a short structured block. A good default is to start with the highest-level goal on the first line, then add sub-goals, phased steps, priorities, or constraints only when they genuinely help the supervisor judge progress and completion. If the wording is not obvious, ask the agent to draft a goal before starting supervision. + ## Stagnation Detection If the supervisor sends **5 consecutive steering messages** without declaring the goal done, it switches to a lenient evaluation mode: if the goal is ≥80% achieved, it declares done rather than looping forever on minor improvements. The threshold is configurable via `MAX_IDLE_STEERS` in `src/index.ts`. ## Customizing the Supervisor: SUPERVISOR.md -The supervisor's reasoning is controlled by its **system prompt** — not the goal. The goal is always set at runtime via `/supervise `. `SUPERVISOR.md` defines *how* the supervisor thinks: its rules, persona, and project-specific constraints. +The supervisor's reasoning is controlled by its **system prompt** — not the goal. The goal is always set at runtime via `/supervise `. `SUPERVISOR.md` defines *how* the supervisor thinks: its rules, persona, and project-specific constraints. **Discovery order** (mirrors pi's `SYSTEM.md` convention): @@ -140,7 +167,7 @@ The supervisor's reasoning is controlled by its **system prompt** — not the go | 2 | `~/.pi/agent/SUPERVISOR.md` | Global personal rules | | 3 | Built-in template | Fallback | -The active source is shown when you run `/supervise ` or `/supervise status`. +The active source is shown when you start supervision with `/supervise `. ### Built-in system prompt diff --git a/src/engine.ts b/src/engine.ts index ecb6aba..ef7bb1a 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -229,7 +229,16 @@ export async function analyze( const userPrompt = buildUserPrompt(state, snapshot, agentIsIdle, stagnating, compactionSummary); try { - return await callSupervisorModel(ctx, state.provider, state.modelId, systemPrompt, userPrompt, signal, onDelta); + const decision = await callSupervisorModel(ctx, state.provider, state.modelId, systemPrompt, userPrompt, signal, onDelta); + if (agentIsIdle && decision.action === "continue") { + return { + action: "steer", + message: "Please continue working toward the goal.", + reasoning: decision.reasoning || "Idle continue converted to steer", + confidence: decision.confidence, + }; + } + return decision; } catch { // When idle and analysis fails, nudge rather than silently do nothing return agentIsIdle diff --git a/src/index.ts b/src/index.ts index faf3b15..6967fc7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,10 @@ /** - * pi-supervisor — A pi extension that supervises the chat and steers it toward a defined outcome. + * pi-supervisor — A pi extension that supervises the chat and steers it toward a defined goal. * * Commands: - * /supervise — start supervising + * /supervise — start supervising * /supervise stop — stop supervision - * /supervise status — show current status widget + * /supervise status — open supervision settings when active * /supervise model — open interactive model picker (pi-style) * /supervise model

— set model directly (scripting) * /supervise sensitivity — adjust steering sensitivity @@ -46,6 +46,18 @@ export default function (pi: ExtensionAPI) { let currentCtx: ExtensionContext | undefined; let idleSteers = 0; // consecutive agent_end steers; reset on done/stop/new supervision + const resolveDefaults = (ctx: ExtensionContext) => { + const prefs = state.getPrefs(); + const workspaceModel = loadWorkspaceModel(ctx.cwd); + const sessionModel = ctx.model; + + return { + provider: prefs.provider ?? workspaceModel?.provider ?? sessionModel?.provider ?? DEFAULT_PROVIDER, + modelId: prefs.modelId ?? workspaceModel?.modelId ?? sessionModel?.id ?? DEFAULT_MODEL_ID, + sensitivity: prefs.sensitivity ?? DEFAULT_SENSITIVITY, + }; + }; + // ---- Session lifecycle: restore state ---- const onSessionLoad = (ctx: ExtensionContext) => { @@ -136,7 +148,7 @@ export default function (pi: ExtensionAPI) { idleSteers = 0; updateUI(ctx, state.getState(), { type: "done" }); const suffix = stagnating ? ` (stopped after ${MAX_IDLE_STEERS} steering attempts — goal substantially achieved)` : ""; - ctx.ui.notify(`Supervisor: outcome achieved! "${s.outcome}"${suffix}`, "info"); + ctx.ui.notify(`Supervisor: goal achieved! "${s.outcome}"${suffix}`, "info"); state.stop(); updateUI(ctx, state.getState()); } else { @@ -147,7 +159,7 @@ export default function (pi: ExtensionAPI) { // ---- /supervise command ---- pi.registerCommand("supervise", { - description: "Supervise the chat toward a desired outcome (/supervise )", + description: "Supervise the chat toward a desired goal (/supervise )", handler: async (args, ctx) => { currentCtx = ctx; const trimmed = args?.trim() ?? ""; @@ -177,17 +189,18 @@ export default function (pi: ExtensionAPI) { if (trimmed === "status") { const s = state.getState(); - if (!s) { - ctx.ui.notify("No active supervision. Use /supervise to start.", "info"); + if (!s || !s.active) { + ctx.ui.notify("No active supervision. Use /supervise to start.", "info"); return; } // Open the interactive settings panel (same as bare /supervise) - const result = await openSettings(ctx, s, DEFAULT_PROVIDER, DEFAULT_MODEL_ID, DEFAULT_SENSITIVITY); + const defaults = resolveDefaults(ctx); + const result = await openSettings(ctx, s, defaults.provider, defaults.modelId, defaults.sensitivity); if (result?.model) { - if (state.isActive()) state.setModel(result.model.provider, result.model.modelId); + state.setModel(result.model.provider, result.model.modelId); saveWorkspaceModel(ctx.cwd, result.model.provider, result.model.modelId); } - if (result?.sensitivity && state.isActive()) state.setSensitivity(result.sensitivity); + if (result?.sensitivity) state.setSensitivity(result.sensitivity); if (result?.widget !== undefined && result.widget !== isWidgetVisible()) toggleWidget(); if (result?.action === "stop" && state.isActive()) { state.stop(); idleSteers = 0; } updateUI(ctx, state.getState()); @@ -200,19 +213,18 @@ export default function (pi: ExtensionAPI) { if (!spec) { // No args → open the interactive pi-style model picker const s = state.getState(); - const picked = await pickModel(ctx, s?.provider, s?.modelId); + const defaults = resolveDefaults(ctx); + const picked = await pickModel(ctx, s?.provider ?? defaults.provider, s?.modelId ?? defaults.modelId); if (!picked) return; // user cancelled const provider = picked.provider; const modelId = picked.id; - if (state.isActive()) { - state.setModel(provider, modelId); - updateUI(ctx, state.getState()); - } + state.setModel(provider, modelId); + if (state.isActive()) updateUI(ctx, state.getState()); const saved = saveWorkspaceModel(ctx.cwd, provider, modelId); ctx.ui.notify( - `Supervisor model set to ${provider}/${modelId}${state.isActive() ? "" : " (takes effect on next /supervise)"}` + + `Supervisor model set to ${provider}/${modelId}${state.isActive() ? "" : " (default for the next /supervise)"}` + (saved ? " · saved to .pi/" : ""), "info" ); @@ -224,20 +236,18 @@ export default function (pi: ExtensionAPI) { let provider: string; let modelId: string; if (slashIdx === -1) { - provider = state.getState()?.provider ?? DEFAULT_PROVIDER; + provider = resolveDefaults(ctx).provider; modelId = spec; } else { provider = spec.slice(0, slashIdx); modelId = spec.slice(slashIdx + 1); } - if (state.isActive()) { - state.setModel(provider, modelId); - updateUI(ctx, state.getState()); - } + state.setModel(provider, modelId); + if (state.isActive()) updateUI(ctx, state.getState()); const saved = saveWorkspaceModel(ctx.cwd, provider, modelId); ctx.ui.notify( - `Supervisor model set to ${provider}/${modelId}${state.isActive() ? "" : " (takes effect on next /supervise)"}` + + `Supervisor model set to ${provider}/${modelId}${state.isActive() ? "" : " (default for the next /supervise)"}` + (saved ? " · saved to .pi/" : ""), "info" ); @@ -250,12 +260,12 @@ export default function (pi: ExtensionAPI) { ctx.ui.notify("Usage: /supervise sensitivity ", "warning"); return; } - if (!state.isActive()) { - ctx.ui.notify(`Sensitivity will be set to "${level}" on next /supervise.`, "info"); - } else { - state.setSensitivity(level); + state.setSensitivity(level); + if (state.isActive()) { updateUI(ctx, state.getState()); ctx.ui.notify(`Supervisor sensitivity set to "${level}"`, "info"); + } else { + ctx.ui.notify(`Supervisor sensitivity set to "${level}" (default for the next /supervise).`, "info"); } return; } @@ -264,18 +274,17 @@ export default function (pi: ExtensionAPI) { if (!trimmed || trimmed === "settings") { const s = state.getState(); - const result = await openSettings(ctx, s, DEFAULT_PROVIDER, DEFAULT_MODEL_ID, DEFAULT_SENSITIVITY); + const defaults = resolveDefaults(ctx); + const result = await openSettings(ctx, s, defaults.provider, defaults.modelId, defaults.sensitivity); if (!result) return; // user cancelled with no changes // Apply model change if (result.model) { const { provider: p, modelId: m } = result.model; - if (state.isActive()) { - state.setModel(p, m); - } + state.setModel(p, m); const saved = saveWorkspaceModel(ctx.cwd, p, m); ctx.ui.notify( - `Supervisor model set to ${p}/${m}${state.isActive() ? "" : " (takes effect on next /supervise)"}` + + `Supervisor model set to ${p}/${m}${state.isActive() ? "" : " (default for the next /supervise)"}` + (saved ? " · saved to .pi/" : ""), "info" ); @@ -283,10 +292,11 @@ export default function (pi: ExtensionAPI) { // Apply sensitivity change if (result.sensitivity) { - if (state.isActive()) { - state.setSensitivity(result.sensitivity); - } - ctx.ui.notify(`Supervisor sensitivity set to "${result.sensitivity}"`, "info"); + state.setSensitivity(result.sensitivity); + ctx.ui.notify( + `Supervisor sensitivity set to "${result.sensitivity}"${state.isActive() ? "" : " (default for the next /supervise)"}`, + "info" + ); } // Apply widget toggle @@ -308,16 +318,14 @@ export default function (pi: ExtensionAPI) { return; } - // Resolve model settings: session state → workspace config → active session model → built-in defaults - const existing = state.getState(); - const workspaceModel = loadWorkspaceModel(ctx.cwd); - const sessionModel = ctx.model; - let provider = existing?.provider ?? workspaceModel?.provider ?? sessionModel?.provider ?? DEFAULT_PROVIDER; - let modelId = existing?.modelId ?? workspaceModel?.modelId ?? sessionModel?.id ?? DEFAULT_MODEL_ID; - const sensitivity = existing?.sensitivity ?? DEFAULT_SENSITIVITY; + // Resolve default settings for the next run. + const defaults = resolveDefaults(ctx); + let provider = defaults.provider; + let modelId = defaults.modelId; + const sensitivity = defaults.sensitivity; // Only prompt for a model if none has been configured yet - if (!existing) { + if (!state.getPrefs().provider && !state.getPrefs().modelId) { const apiKey = await ctx.modelRegistry.getApiKeyForProvider(provider); if (!apiKey) { ctx.ui.notify(`No API key for "${provider}/${modelId}" — pick a model with an available key.`, "warning"); @@ -347,13 +355,13 @@ export default function (pi: ExtensionAPI) { name: "start_supervision", label: "Start Supervision", description: - "Activate the supervisor to track the conversation toward a specific outcome. " + + "Activate the supervisor to track the conversation toward a specific goal. " + "The supervisor will observe every turn and steer the agent if it drifts. " + "Once supervision is active it is locked — only the user can change or stop it.", parameters: Type.Object({ outcome: Type.String({ description: - "The desired end-state to supervise toward. Be specific and measurable " + + "The goal text to supervise toward. Be specific and measurable " + "(e.g. 'Implement JWT auth with refresh tokens and full test coverage').", }), sensitivity: Type.Optional(Type.Union([ @@ -379,15 +387,16 @@ export default function (pi: ExtensionAPI) { const s = state.getState()!; return text( `Supervision is already active and cannot be changed by the model.\n` + - `Active outcome: "${s.outcome}"\n` + + `Active goal: "${s.outcome}"\n` + `Only the user can stop or modify supervision via /supervise.` ); } // Resolve sensitivity - const sensitivity: Sensitivity = params.sensitivity ?? DEFAULT_SENSITIVITY; + const prefs = state.getPrefs(); + const sensitivity: Sensitivity = params.sensitivity ?? prefs.sensitivity ?? DEFAULT_SENSITIVITY; - // Resolve model: tool param → workspace config → active session model → built-in default + // Resolve model: tool param → session prefs → workspace config → active session model → built-in default let provider: string; let modelId: string; if (params.model) { @@ -397,8 +406,8 @@ export default function (pi: ExtensionAPI) { } else { const workspaceModel = loadWorkspaceModel(ctx.cwd); const sessionModel = ctx.model; - provider = workspaceModel?.provider ?? sessionModel?.provider ?? DEFAULT_PROVIDER; - modelId = workspaceModel?.modelId ?? sessionModel?.id ?? DEFAULT_MODEL_ID; + provider = prefs.provider ?? workspaceModel?.provider ?? sessionModel?.provider ?? DEFAULT_PROVIDER; + modelId = prefs.modelId ?? workspaceModel?.modelId ?? sessionModel?.id ?? DEFAULT_MODEL_ID; } state.start(params.outcome, provider, modelId, sensitivity); @@ -415,7 +424,7 @@ export default function (pi: ExtensionAPI) { "info" ); - return text(`Supervision active. Outcome: "${params.outcome}" | ${provider}/${modelId} | sensitivity: ${sensitivity}`); + return text(`Supervision active. Goal: "${params.outcome}" | ${provider}/${modelId} | sensitivity: ${sensitivity}`); }, }); } diff --git a/src/state.ts b/src/state.ts index 7f5c5a6..0869272 100644 --- a/src/state.ts +++ b/src/state.ts @@ -3,9 +3,10 @@ */ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent"; -import type { SupervisorState, SupervisorIntervention, Sensitivity } from "./types.js"; +import type { SupervisorState, SupervisorIntervention, Sensitivity, SupervisorPrefs } from "./types.js"; const ENTRY_TYPE = "supervisor-state"; +const PREFS_ENTRY_TYPE = "supervisor-prefs"; export const DEFAULT_PROVIDER = "anthropic"; export const DEFAULT_MODEL_ID = "claude-haiku-4-5-20251001"; @@ -13,6 +14,7 @@ export const DEFAULT_SENSITIVITY: Sensitivity = "medium"; export class SupervisorStateManager { private state: SupervisorState | null = null; + private prefs: SupervisorPrefs = {}; private pi: ExtensionAPI; constructor(pi: ExtensionAPI) { @@ -31,6 +33,8 @@ export class SupervisorStateManager { turnCount: 0, }; this.persist(); + this.prefs = { ...this.prefs, provider, modelId, sensitivity }; + this.persistPrefs(); } stop(): void { @@ -47,6 +51,10 @@ export class SupervisorStateManager { return this.state; } + getPrefs(): SupervisorPrefs { + return this.prefs; + } + addIntervention(intervention: SupervisorIntervention): void { if (!this.state) return; this.state.interventions.push(intervention); @@ -59,33 +67,64 @@ export class SupervisorStateManager { } setModel(provider: string, modelId: string): void { - if (!this.state) return; - this.state.provider = provider; - this.state.modelId = modelId; - this.persist(); + if (this.state) { + this.state.provider = provider; + this.state.modelId = modelId; + this.persist(); + } + this.prefs = { ...this.prefs, provider, modelId }; + this.persistPrefs(); } setSensitivity(sensitivity: Sensitivity): void { - if (!this.state) return; - this.state.sensitivity = sensitivity; - this.persist(); + if (this.state) { + this.state.sensitivity = sensitivity; + this.persist(); + } + this.prefs = { ...this.prefs, sensitivity }; + this.persistPrefs(); } /** Restore state from session entries (finds the most recent supervisor-state entry). */ loadFromSession(ctx: ExtensionContext): void { const entries = ctx.sessionManager.getBranch(); - for (let i = entries.length - 1; i >= 0; i--) { + this.state = null; + this.prefs = {}; + + let foundState = false; + let foundPrefs = false; + + for (let i = entries.length - 1; i >= 0 && (!foundState || !foundPrefs); i--) { const entry = entries[i]; - if (entry.type === "custom" && (entry as any).customType === ENTRY_TYPE) { + if (entry.type !== "custom") continue; + + if (!foundState && (entry as any).customType === ENTRY_TYPE) { this.state = (entry as any).data as SupervisorState; - return; + foundState = true; + continue; + } + + if (!foundPrefs && (entry as any).customType === PREFS_ENTRY_TYPE) { + this.prefs = ((entry as any).data ?? {}) as SupervisorPrefs; + foundPrefs = true; } } - this.state = null; + + if (!foundPrefs && this.state) { + this.prefs = { + provider: this.state.provider, + modelId: this.state.modelId, + sensitivity: this.state.sensitivity, + }; + } } private persist(): void { if (!this.state) return; this.pi.appendEntry(ENTRY_TYPE, { ...this.state }); } + + private persistPrefs(): void { + this.pi.appendEntry(PREFS_ENTRY_TYPE, { ...this.prefs }); + } } diff --git a/src/types.ts b/src/types.ts index b271f05..17a3002 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,6 +13,13 @@ export interface SupervisorIntervention { timestamp: number; } +/** Session-scoped defaults for future supervision runs */ +export interface SupervisorPrefs { + provider?: string; + modelId?: string; + sensitivity?: Sensitivity; +} + /** Full supervisor state — persisted to session */ export interface SupervisorState { active: boolean; diff --git a/src/ui/settings-panel.ts b/src/ui/settings-panel.ts index b0e80ad..22d4df9 100644 --- a/src/ui/settings-panel.ts +++ b/src/ui/settings-panel.ts @@ -96,7 +96,7 @@ export async function openSettings( if (isActive) { items.push({ id: "outcome", - label: "Outcome", + label: "Goal", description: `Steers: ${state!.interventions.length} · Turns: ${state!.turnCount}`, currentValue: `"${state!.outcome.length > 60 ? state!.outcome.slice(0, 59) + "…" : state!.outcome}"`, });