Skip to content

feat: Kiln assistant — auto mode, background eval jobs, and sub-agents (runtime, tabs UI, steer/stop) - #1561

Draft
leonardmq wants to merge 172 commits into
mainfrom
leonard/assistant-subagents
Draft

feat: Kiln assistant — auto mode, background eval jobs, and sub-agents (runtime, tabs UI, steer/stop)#1561
leonardmq wants to merge 172 commits into
mainfrom
leonard/assistant-subagents

Conversation

@leonardmq

@leonardmq leonardmq commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Consolidated PR — this PR was previously stacked on #1517 (leonard/kil-686-eval-job). The stack has been collapsed: this PR now targets main and carries all of #1517's work; #1517 is closed in favor of this one and its full description is preserved below under Carried from #1517. kiln_server counterpart: Kiln-AI/kiln_server#255.

Desktop + web UI for assistant sub-agents:

  • Local sub-agent runtime: registry, runner, orchestration tools, and the /api/conversations sub-agent API surface.
  • Sub-agent UI: tab strip with overflow/status dots, live child transcripts with full main-transcript parity, steer (message a running child) and stop, consent flow, and report chips/panels.
  • Reports delivered exclusively via the injection channel (<subagent_report> messages); kickoff message echoed onto the child stream at run start.
  • Lifecycle fixes: children lookup for auto-mode parents (leaf trace id aliasing), working indicator for a child awaiting its first response, errorText surfaced in chat stream errors.
  • Regenerated OpenAPI web types; desktop version bumped to 1.0.4 (the release that ships sub-agents).

Follow-up work on this stack lands in PR #1558 (into this branch).

Note: the judge_feedback_batch subsystem this stack previously carried has been removed entirely — it was an MVP prompt-optimizer attempt we decided to drop.

Latest: mid-turn message injection, event-loop stalls, forensic debug logging

  • Main-agent queued messages now inject mid-turn. Sends on a live interactive run POST straight to the server inbox (the transport sub-agents/cross-tab already used); the engine folds them into the next upstream round instead of holding them until the whole turn settles. The client-side queue remains only for non-injectable moments (approval box open, consent resolving, no session id, failed POST) and flushes into the live run as soon as it becomes injectable. Server complement: idle sends drain any stranded inbox (error-settle leftovers) into the fresh turn, in send order.
  • Remaining event-loop stalls fixed (the /api/conversations?parent= hang/pile-ups): agent_overview and get_tags (plus a third endpoint since removed with the judge-feedback-batch subsystem) still ran full dataset disk scans on the loop; now offloaded via asyncio.to_thread (same treatment as a02cfeb).
  • Forensic debug logging (KILN_CHAT_DEBUG_LOG, defaulted on in the dev server): JSONL timeline of run/round/tool-batch/inbox events (ids + timings, never content). Every upstream request now carries X-Kiln-Conversation-Id, joining the backend's debug log (kiln_server PR #255) per conversation; the chat footer shows a copyable conversation id while the flag is on.
  • Sub-agent tab strip resilience (from the first debug-log post-mortem, cv_dvs5hq23cn23): the children-list fetch is now bounded (AbortSignal.timeout(10s)) so a request stuck behind the browser's per-origin connection cap can no longer permanently blind the reconcile loop; same-parent blips keep the current tabs (no flicker) while failed cross-parent switches still clear; a 15s watchdog reconnects a firehose EventSource wedged before headers; failed reconciles are logged instead of swallowed. Forensics: sub-agent settles emit run_settled to the debug log, and the empty-model-response error carries finish_reason.

Carried from #1517: assistant auto-mode + the eval/judge job backend (context gauge, chat resilience, queued messages)

This is the app branch behind KIL-686 (background jobs) that has grown to carry the whole assistant-driven eval/optimize workflow. It began as "run an eval as a background job" and consolidated several stacked PRs — auto mode (#1518), the context gauge / compaction indicator (#1519), the jobs-API cleanups (#1527), auto-mode resilience (#1529), worker properties (#1530), chat retry + Stop (#1532/#1533), and queued messages (#1534) — into a single branch so one desktop_server serves everything the merged kiln-chat skill needs.

No existing eval UI is changed; the eval job work is purely additive (new background-job paths for agents).

Note: the judge_feedback_batch subsystem (#1536) previously carried by this branch has been removed entirely — it was an MVP prompt-optimizer attempt we decided to drop.

Main features

  • Eval as a background jobEvalJobWorker + a typed, non-streaming POST /api/jobs/evals/run kickoff for agents, plus a bulk POST /api/jobs/wait to await many jobs at once. Agents use these instead of the SSE eval endpoints.
  • Assistant auto mode — the assistant can run steps autonomously (reflective optimization) with explicit consent and a hard Stop, backed by a self-contained chat/auto/ app-server subsystem and matching web UI.
  • Context-usage gauge + "Summarizing…" indicator (KIL-727) — a glanceable context-window gauge in the assistant footer and a compaction indicator during the server's pre-inference summarization window.
  • Chat resilience — transient-error retry with ramping backoff in both interactive and auto modes, plus a real Stop control (confirmed by a dialog).
  • Mid-turn message queueing — messages typed during a turn are queued (not dropped or fired immediately) with Send-now / Edit / Discard controls, and flushed at the next round boundary.
  • Jobs-table run summaries — workers publish static descriptive properties; the jobs table renders an eval run-config-style summary.
  • Structured add-example dialog (Support structured data entry in the add example dialog #1540, via main) — the add/edit example dialog is now a reusable component with schema-aware, field-by-field entry.

Agent permissions

Agent-allowed job endpoints: POST /api/jobs/evals/run (allow + approval — evals cost AI credits) and POST /api/jobs/wait (allow). The UI-only /api/chat/auto/* set and the SSE run_comparison / run_calibration eval endpoints are agent-forbidden (deny). Regenerated agent-check annotations and the OpenAPI TS schema throughout.

Testing

New worker, endpoint, registry, auto-mode, retry, queued-message, context-gauge, and compaction-indicator tests across Python and web. uv run ./checks.sh --agent-mode → green (Python lint/format/type/tests, web lint/format/check/test/build, OpenAPI schema in sync).

Changelog

Linear with the commit history — each entry is one holistic increment (roughly one merged sub-PR / ticket). Append new entries at the bottom.

1. Run evals as background jobs

  • EvalJobWorker (jobs/workers/eval.py) wraps the existing EvalRunner so an eval runs in the background. Idempotent, supports_pause=True (EvalRunner excludes already-run (eval_config, run_config, dataset) triples), progress against full eval-set size.
  • POST /api/jobs/evals/run — typed, non-streaming kickoff (body is the 5 eval ids; returns {job_id, status}). Allow + requires-approval. Two-segment path so it can't be shadowed by the generic job route.
  • SSE eval-run endpoints (run_comparison, run_calibration) flipped to agent-forbidden.
  • Reconcile fix: JobDerivedState.error is now int | None; a worker that can't derive its error count from entities returns None, and _apply_derived keeps the live reported count instead of clobbering it to 0.

2. Assistant auto mode (rescoped, #1518)

  • Self-contained chat/auto/ subsystem: auto-run registry, runner, event bus, SSE, and API (/api/chat/auto/*: enable, decline, resolve, sessions, per-run message/stop/events).
  • Core: enable_auto_mode / disable_auto_mode built-in tools + tool-registry wiring.
  • Web UI: auto_run_store, consent dialog, chat-history grouping, reworked chat.svelte (in-transcript live working/idle, inject-on-send, reconnect handling), preserving the base's UI refactors (scrollbar-to-side, DaisyUI send/stop, centering wrapper).
  • Salvaged from the dropped eval/finetune/RAG-via-job refactor stack (feat: run eval, finetune via background jobs #1436/feat: run RAG via background jobs #1450); the one real coupling (a generic SSE keepalive helper) was ported into jobs/events.py (KeepalivePing / iter_with_keepalive, generic over a TypeVar).
  • Review fixes: Literal["user"] role, optimistic-flag reset on inject failure, consent-event trace fallback, keyboard-focus reveal, consentPending held through requestEnable, and a guard so resyncOnLoad can't overwrite a newly-selected session on a mid-flight conversation switch.

3. Multi-job wait + eval per-item error logging

  • Multi-job wait backed by JobRegistry.wait_many() — blocks until all given jobs are terminal and returns their records. Lets a caller await several eval jobs in one call.
  • EvalRunner.run() now accepts observers; EvalJobWorker forwards each failed dataset item's exception to ctx.report_error (with dataset_id + run_config_id). Previously only the error count was reported, so GET /api/jobs/{id}/errors showed "no errors recorded" even when every item failed.

4. Context-usage gauge + compaction indicator — KIL-727 (#1519)

  • studio_server proxyChatSessionSnapshot gains a context_usage field (context_tokens, context_limit, context_percent, compacted) so a session reopened from history renders the gauge. Relies on Pydantic's default extra="ignore" (made explicit) to drop unmodeled upstream keys at the client boundary (extra="forbid" deliberately avoided). SSE stays a raw passthrough; passthrough tests added.
  • Context gauge (context_usage_gauge.svelte) — a small grey bar in the input footer (opposite Auto mode), percentage above and a tooltip showing approximate used / total tokens; hidden before the first turn. Token counting is intentionally approximate (overestimate-biased; copy uses "≈"). Tooltip fixed to measure after visible and made keyboard-accessible.
  • "Summarizing…" indicator — on the backend's compaction signal, the Thinking activity indicator shows "Summarizing earlier messages to free up context…" during the pre-inference window. Not cleared on finished (a fast started→finished pair would collapse the window); cleared by the first real assistant content, on error, and on reset.
  • Wired through streaming_chat.ts / chat_session_store.ts / auto_run_store.ts so the gauge updates during auto-mode bursts too.

5. Auto/manual congruence + gauge polish

  • The auto runner and the manual chat path are kept congruent so auto-mode bursts behave identically to a user driving each step; tool-availability reconciled between manual and server-driven runs, and tool-congruence handling hardened (chat/auto/runner.py).

6. Jobs-API consolidation — bulk wait only (#1527)

  • Dropped the single-job GET /{id}/wait (bulk wait covers it with one id) and the generic POST /api/jobs/{type} create endpoint (production only runs evals, via the typed route — the generic path had no real caller).
  • GET /api/jobs/wait converted to POST /api/jobs/wait with a WaitForJobsRequest body (ids as JSON, not repeated query params); with the generic POST gone, the route-ordering workaround is dropped.
  • The temporary /jobs test page becomes a real jobs panel; NoopJobWorker stays as a registry-level test fixture but is no longer registered in production.

7. Auto-mode resilience — KIL-692 (#1529)

  • Side-note framing — mid-burst user messages are wrapped in a <system-reminder> telling the model to weave its reply into ongoing work and keep going, so a quick aside no longer settles the burst IDLE.
  • Transient-error retry — bounded full-jitter exponential backoff on retryable upstream failures (429/5xx). Only failures that streamed no content are retried (no duplicate output). New auto-mode-retry SSE event drives a "retrying N/M…" transcript affordance.
  • Stop pressed while retrying settles USER_STOPPED (publishes auto-mode-off) instead of leaving the flag on.

8. Worker properties + eval summary in the jobs table (#1530)

  • Generic mechanism for workers to publish static, descriptive properties derived once from params at create time (mirrors the compute_state pure-read pattern; stored dict-on-the-wire like progress_detail, computed behind a guard so a failing describe() never breaks job creation).
  • The eval worker publishes eval name, run config (name, model, resolved prompt name, tool/skill counts), and judge (name, algorithm, model), resolving prompt ids to names the way the frontend does. The jobs-table Details cell renders this as a run-config-style summary.
  • Hardening: full describe() → type-check → model_dump() path wrapped so a bad payload can never break create(); undeclared-model case made explicit; test coverage for the contract guard and the previously-dead prompt-resolution branches.

9. Surface the original eval error (#1531)

  • The eval error-log observer unwraps KilnRunError.original so GET /api/jobs/{id}/errors shows the real failure instead of the generic "An unexpected error occurred." wrapper text.
  • Hardened against a buggy __str__ on the original exception (falls back to the class name), and covered the common RetryableError(str(original)) path.

10. Chat retry in both modes + hard Stop (#1532, #1533)

  • Shared iter_round_with_retries (stream_session.py) owns the retry loop and is used by both the auto runner and interactive ChatStreamSession.stream() — previously only auto mode retried. Backoff is an explicit ramp capped at 60s (1, 2, 5, 10, 20, 30, 60, 60, 60, 60, ±15% jitter, ~5 min total); MAX_CHAT_RETRIES derives from its length.
  • SSE event generalized auto-mode-retry → kiln-chat-retry; the frontend renders "Temporary issue — retrying N/M…" from either source. Stop is honored during the backoff (no re-POST).
  • Auto mode's "waiting for you" affordance replaced with a real Stop button, confirmed by auto_mode_stop_dialog.svelte (notes that already-kicked-off background jobs keep running).

11. Mid-turn message queueing (#1534)

  • A message typed while a turn is in flight is held in a client-side queue instead of being dropped (interactive) or fired immediately (auto). The composer surfaces the queued message with Send now, Discard, and Edit; a second send coalesces.
  • Auto-sends when the turn yields (auto: injected at the next round boundary) and appears in the trace at the point it was actually sent; the textarea is no longer disabled during a turn.
  • Re-attach/refresh correctness: dedupe injected-message echo, render an in-flight round into a fresh turn, preserve queued text on rejected dispatch, and strip <system-reminder> side-note framing from hydrated transcripts.

12. Eval job model docs

  • Added Field descriptions to the eval job models.

13. Judge feedback batch subsystem (#1536) — since removed

  • This branch previously merged the judge_feedback_batch capability (datamodel + runner + synchronous REST API + job-backed worker) as an MVP prompt-optimizer gate. See entry 15 — it has been removed from the branch entirely. The eval job's validated concurrency param (feat(judge-feedback-batch): expose concurrency in job params #1544) remains.

14. Structured add-example dialog (#1540, via main)

  • The data-guide add/edit example dialog is promoted to a reusable $lib/components/add_example_dialog.svelte with configurable include_input / include_output and schema-aware, field-by-field entry (RunInputFormElement) for structured tasks; plaintext tasks keep the textarea. The route-local copy is deleted and callers consume the shared component + exported GuideSample type.

15. Remove the judge feedback batch subsystem

  • Deleted the whole judge_feedback_batch subsystem added in entry 13: core datamodel + runner, the synchronous REST API, the job-backed worker, their tests, the jobs-table UI branch, the OpenAPI tag, and the (never-shipped) agent-check annotations. Regenerated the OpenAPI TS schema. It was an MVP prompt-optimizer attempt, dropped in favor of not shipping it at all.

🤖 Generated with Claude Code

leonardmq and others added 30 commits June 25, 2026 12:50
Add an EvalJobWorker that wraps the existing EvalRunner so an eval can run
in the background through the job system. Expose a typed, non-streaming
kickoff endpoint POST /api/jobs/evals/run for agents (allow + requires
approval); poll GET /api/jobs/{id} for progress/result instead of SSE. The
endpoint uses a two-segment path so it can never be shadowed by the generic
POST /api/jobs/{type} route.

Flip the UI's SSE eval-run endpoints (run_comparison, run_calibration) to
agent-forbidden, since agents should use the background job API instead.

Also fix a reconcile bug surfaced by review: a worker that can't derive its
error count from entities (failed eval items leave no EvalRun) now returns
JobDerivedState.error=None, and _apply_derived keeps the live reported error
count instead of clobbering it to 0 on reconcile (mirrors total/message).

Regenerated agent-check annotations and the OpenAPI TS schema.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QNY986xT2VCm7xngUg4tF
Salvages the assistant auto-mode prototype (PR #1451) onto
leonard/kil-686-eval-job, dropping the eval/finetune/RAG-via-job refactor
(PRs #1436/#1450) that the original branch was stacked on.

The auto-mode feature is self-contained: a new chat/auto/ app-server
subsystem (auto-run registry/runner/events/SSE + API), enable/disable
auto-mode built-in tools in libs/core, and the assistant web UI
(auto_run_store, consent dialog, chat history, chat.svelte). Its only
coupling to the dropped stack was a generic SSE keepalive helper, which
the dropped stack had extracted into jobs/events.py; that helper
(KeepalivePing / KEEPALIVE_PING / iter_with_keepalive) is ported here so
chat/auto reuses it unchanged.

Conflict resolutions vs the newer base:
- chat.svelte: took auto-mode's version (the built+tested feature) and
  re-applied the base's three UI refactors landed since the fork —
  scrollbar-to-the-side, DaisyUI btn-circle send/stop, and the centering
  wrapper div (content centered while the scrollbar stays at the edge).
- .env.example: kept PUBLIC_ENABLE_JOBS; dropped PUBLIC_SHOW_TOOL_CALL_DETAILS
  (auto-mode removed that debug feature).

Regenerated agent-check annotations and the OpenAPI TS schema.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QNY986xT2VCm7xngUg4tF
Low-risk quick-wins from automated review:
- jobs/events.py: make iter_with_keepalive generic (TypeVar) so the bytes
  SSE consumer in chat/auto is typed correctly, not just the JobEvent case.
- chat/auto/models.py: pin InboundMessage.role to Literal["user"] so the
  /message endpoint can't be handed a system/assistant role.
- auto_run_store.ts: clear the optimistic working flag when an inject send
  fails (no burst started, so nothing else would clear it).
- chat_session_store.ts: in handleAutoModeConsent, fall back to the last
  assistant message's trace before continuationTraceId so live-chat consent
  events with a null payload trace aren't dropped.
- chat.svelte: hold consentPending through requestEnable() so a slow enable
  can't re-enable the button and double-dispatch.
- chat_history_row.svelte: reveal the delete action on keyboard focus
  (group-focus-within), not just hover.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QNY986xT2VCm7xngUg4tF
If the user switches conversations while resyncOnLoad's resolve() or
snapshot GET is in flight, the resolved stale run could be hydrated into
and attached onto the newly-selected session. Re-check the active trace
after each await and bail with a plain return (never detach()/loadSession,
since the shared auto_run_store may already be owned by the new session).

Addresses the resync race flagged in PR review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QNY986xT2VCm7xngUg4tF
Two fixes to the background-job/eval flow:

- Eval error log: EvalRunner.run() now accepts observers, and EvalJobWorker
  passes one that forwards each failed dataset item's exception to
  ctx.report_error. Previously only the error COUNT (Progress.errors) was
  reported, so GET /api/jobs/{id}/errors showed "no errors recorded" even
  when every item failed.

- Multi-job wait: add GET /api/jobs/wait?ids=a&ids=b&timeout=, backed by
  JobRegistry.wait_many(), to block until ALL given jobs are terminal and
  return their records. Lets a caller that kicked off several eval jobs (one
  per run config) wait for them in one call. Declared before /api/jobs/{id}
  so "wait" doesn't resolve to {id}. Pure observer, like /{id}/wait.

Regenerated the OpenAPI TS schema and agent-check annotations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QNY986xT2VCm7xngUg4tF
Surface the kiln_server context-usage gauge contract through the studio_server
proxy to the web UI (architecture §7):

- Add a ContextUsage Pydantic model and a context_usage field to
  ChatSessionSnapshot in the GET /api/chat/sessions/{id} proxy, so a session
  reopened from history renders the gauge. All ContextUsage fields are optional
  so an older/partial upstream never 500s the proxy.
- Make ChatSessionSnapshot's extra="ignore" explicit (Pydantic v2 default):
  unknown upstream keys (notably the server-only compacted_trace) are silently
  dropped at the client boundary (functional_spec §7.3 containment). Documented
  that extra="forbid" must NOT be used — it would raise/500 on a leaked key.
- SSE proxy needs no change: EventParser forwards complete lines verbatim, so
  context_usage on the kiln_chat_trace event passes through untouched. Added a
  passthrough test asserting this.
- Tests: round-trip context_usage; drop compacted_trace at the route and via a
  direct model-config invariant; tolerate a missing context_usage.
- Regenerated api_schema.d.ts so the web UI types carry context_usage
  (Phase 4 depends on it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CNJdjd98QPux2QhReKZY9z
Surface the kiln_server context_usage on the /assistant chat so users get a
glanceable, approximate signal of how full the conversation's context window is.

- streaming_chat.ts: parse context_usage off the kiln_chat_trace snapshot event
  (normalizeContextUsage tolerates partial/missing upstream fields) and fire a
  new onContextUsage callback alongside onChatTrace.
- chat_session_store.ts: contextUsage in PersistedChatSession (persisted to
  sessionStorage), setContextUsage setter wired into the interactive stream, the
  auto-run sink, and the resume/handoff path; set on history/resync load; cleared
  on reset.
- session_messages.ts: hydrateSessionFromSnapshot returns contextUsage from the
  session GET response (threaded through the history apply event into loadSession).
- auto_run_store.ts: AutoRunChatSink gains onContextUsage so the gauge updates
  during auto-mode bursts too.
- context_usage_gauge.svelte: compact grey two-div bar (bg-base-content/10 track,
  bg-base-content/30 fill, no color ramp) with the percent stacked above and a
  tooltip carrying approximate {used}/{total} token counts; hidden when usage is
  null. Mounted in the chat input footer row, right-aligned opposite Auto mode.

Tests: streaming_chat (parse + onContextUsage), chat_session_store (set/persist/
load/reset), session_messages (hydrate), auto_run_store sink, and a gauge
component test (markup, grey classes, fill width, percent-above, tooltip tokens,
hidden-when-null).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CNJdjd98QPux2QhReKZY9z
…ence (Phase 5)

Surface the server's pre-inference compaction window in the assistant UI by
handling the new kiln_compaction_status SSE event (architecture.md §8.5,
functional_spec.md §9.1).

- streaming_chat.ts: parse kiln_compaction_status; add onCompactionStatus
  through StreamEventProcessor + the interactive/resume option surfaces. Set
  compacting on "started"; deliberately do NOT clear on "finished" (a fast or
  buffered started→finished pair would collapse the window) — the indicator is
  cleared by the first REAL assistant content (text/tool/exec-start/snapshot)
  and on error.
- chat_session_store.ts: runtime-only compacting flag (not persisted) wired
  through the interactive, resume, and auto-run sink paths; cleared on
  start/finish/error/reset/loadSession/new-turn/idle/off.
- auto_run_store.ts: onCompactionStatus on the sink + processor.
- chat.svelte / chat_status_steps.svelte: render the SAME Thinking activity
  indicator markup with the summarizing label. Because compaction happens before
  any assistant message exists, render it as a standalone activity row keyed
  only on compacting (a per-message mount has no DOM anchor in that window), and
  suppress the empty-message Thinking cursor while compacting.
- studio_server: test that kiln_compaction_status passes through the raw SSE
  proxy untouched (no model change).
- Tests: processor set/clear behavior, store flag plumbing + non-persistence,
  the indicator copy, and an integration test rendering chat.svelte that the
  summarizing row is visible with compacting=true and NO assistant message.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CNJdjd98QPux2QhReKZY9z
Addresses Gemini review feedback on the context-usage gauge tooltip:

- showTooltip awaits tick() before computePosition, so Floating UI
  measures the tooltip's real size instead of 0×0 (it's display:none
  until isVisible flips), fixing wrong initial placement.
- Clears any prior autoUpdate registration before re-registering and
  bails if the tooltip was hidden during the await, avoiding a
  listener leak / re-init-on-hidden race.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CNJdjd98QPux2QhReKZY9z
…ack)

Addresses CodeRabbit feedback: the token-count tooltip was reachable
only via mouse. Make the meter trigger focusable (tabindex=0) and show/
hide the tooltip on focus/blur as well as mouseenter/mouseleave, so
keyboard users get the same info. Also clears the a11y mouse-events
warning on this element.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CNJdjd98QPux2QhReKZY9z
…ction

KIL-727: assistant context-usage gauge + compaction indicator (app side)
…om:Kiln-AI/Kiln into leonard/kil-686-eval-job
Drop GET /api/jobs/{id}/wait; the bulk GET /api/jobs/wait?ids=...
endpoint covers the single-job case with one id. Update stale doc
references and regenerate the OpenAPI schema.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHXGXnj1GKbFretPWeQWgP
Remove POST /api/jobs/{type} (and its wait=true/timeout inline-wait path):
production only runs evals, which use the typed POST /api/jobs/evals/run,
so the generic create endpoint had no real caller (only the temporary
test page + noop test worker). The NoopJobWorker stays as a registry-level
test fixture but is no longer registered in production.

Convert GET /api/jobs/wait to POST with a WaitForJobsRequest body, so the
job ids travel as JSON instead of repeated query params. With the generic
POST gone there's no route collision, so the ordering workaround is dropped.

Rewire test job creation to registry.create(), turn the temporary /jobs
test page into a real jobs panel (the jobs dialog links to it), and
regenerate the OpenAPI schema and agent-check annotations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHXGXnj1GKbFretPWeQWgP
Remove single-job wait endpoint in favor of bulk wait
A user message that arrives while an auto-mode burst is in flight was
appended as a raw user turn. The model would reply to it in plain text,
and that text-only turn settles the burst IDLE ("asked_user") — so a
quick aside from the user halted the autonomous run.

Wrap drained mid-burst messages in a system-reminder that tells the model
to weave its reply into ongoing work and keep going in the same turn,
stopping only if explicitly asked or the task is complete. Applies to both
injection sites (append-after-tools and drain-before-idle); the seed
message and idle-wake messages are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01765EZhz9fQ1mgDvsqFKeQr
Auto mode runs unattended, so a single transient upstream failure (rate
limit, 5xx, connection blip) parked the whole burst IDLE until a human
returned. Retry such failures with bounded full-jitter exponential
backoff (max 10 attempts) before giving up.

- iter_upstream_round gains a defer_terminal_error mode that hands the
  error payload + status to the caller instead of emitting it, so the
  runner can decide to retry before anything reaches the client.
- RoundState carries the deferred payload, status code, and a retryable
  flag (429/500/502/503/504; 4xx excluded). Only failures that streamed
  no content are retried, so a retry can never duplicate output.
- New auto-mode-retry SSE event ({attempt, max_attempts, status_code?})
  so the UI can show "retrying N/10…".
- Pre-response connection errors are caught in the runner and retried too.

The interactive path is unchanged (defer_terminal_error defaults False).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01765EZhz9fQ1mgDvsqFKeQr
Consume the new auto-mode-retry SSE event in auto_run_store: expose a
`retry` store ({attempt, max}) set on each retry event and cleared by the
next event of any other kind (recovered round, or settled idle/off). The
burst keeps reading as working during retries.

chat.svelte shows a transient "Connection issue — retrying N/M…" affordance
in the transcript (mirrors the reconnecting affordance), so an unattended
run reads as still-working rather than stalled or errored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01765EZhz9fQ1mgDvsqFKeQr
Add a generic mechanism for job workers to publish static, descriptive
properties about their work, derived once from params at create time
(mirrors the compute_state pure-read pattern; stored dict-on-the-wire like
progress_detail). The registry computes them in create() behind a guard so a
failing describe() never breaks job creation.

The eval worker is the first implementation: it publishes the eval name, run
config (name, model, resolved prompt name, tool/skill counts), and judge
(name, algorithm, model) — resolving prompt ids to names the same way the
frontend does (generator label, custom id::, reused frozen task_run_config::,
fine-tune, local frozen).

The jobs table Details cell renders this as a run-config-style summary:
"Eval: <name>" header, flat labeled property lines, then a muted date and id.
Model names are formatted with the shared model-name helper; long values
truncate with tooltips.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDgjq9EjvM7aYwa2Ccv9L1
Hoist eval_job_properties(job) to a {#each}-level {@const} and gate the cell
on {#if p}, so p is type-narrowed to non-null inside the block. Removes the
?./??/&& noise on guaranteed-present fields (addresses PR review).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDgjq9EjvM7aYwa2Ccv9L1
…on branches

Address deep-review findings:
- Render judge_name in the Judge line ("Judge: <name> (<algorithm>)") instead of
  the algorithm alone — the field was published, typed, and tested but unused.
- Add title={job.id} so the truncated id can be read in full (it's the value
  users copy for support/debugging).
- Backend tests: cover the previously-dead prompt-resolution branches —
  task_run_config:: reused-frozen (no own prompt), fine_tune_prompt::, the
  raw-id fallback, and the MCP/non-agent run-config path.
- Registry tests: cover the describe() contract guard — typed-properties happy
  path, wrong-type drop, and the raise-is-swallowed path — at the generic
  (non-eval) worker level.
- Frontend test: assert the judge name+algorithm line, the model line, and the
  non-zero "N available" tool/skill branch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDgjq9EjvM7aYwa2Ccv9L1
Address deep code review findings:

- Stop pressed while retrying a persistent transient failure now settles
  USER_STOPPED (so the supervisor publishes auto-mode-off) instead of
  IDLE("error") with the flag left on — restoring the graceful-stop
  contract. (Phase 1 moderate)
- Add the missing test for the central anti-duplication guard: a transport
  failure after content was streamed must NOT be retried (re-POST would
  duplicate output). New FakeUpstreamResponse.raise_transport_error_after_chunks
  drives a mid-stream httpx.ReadError. (Phase 1/4 moderate)
- Add a stop-during-retry test; strengthen the retry-then-succeeds test to
  assert attempt progression (1, 2) via structured parsing and that backoff
  sleep actually ran. (Phase 4 mild)
- UI: "Connection issue" copy mislabeled 429/5xx retries → "Temporary issue",
  and guard the degraded 0/0 render. (Phase 3 mild)
- Make _side_note_message a module-level function for consistent call style;
  tighten the retry docstring and note the accepted non-idempotent re-POST.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01765EZhz9fQ1mgDvsqFKeQr
Wrap describe(), the contract type-check, and model_dump() in a single
try/except so a bad payload or a serialization failure can never break
create() — the guard's whole purpose is to keep describe failures non-fatal.
Also make the missing-properties_model case explicit (log + drop) instead of
relying on model_dump throwing. Adds a registry test for the undeclared-model
drop path. (addresses CodeRabbit)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDgjq9EjvM7aYwa2Ccv9L1
Wrap the assertion in try/finally so DescribeWorker.gate is released even on
failure, and await terminal status so the spawned job can't leak into
teardown. (addresses CodeRabbit)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDgjq9EjvM7aYwa2Ccv9L1
feat(jobs): publish worker properties, render eval summary in jobs table
…est)

- Use round_state.trace_id_for_error for the transport-error give-up payload
  so a trace id that streamed in before the error is reflected. (gemini)
- _side_note_message: `base.get("content") or ""` so an explicit None content
  can't serialize to the literal "None". (gemini)
- test_stop_requested_during_retry: flip stop_requested from inside the patched
  backoff sleep so the test exercises the in-retry transition (one retry
  emitted) rather than the pre-run stop path. (coderabbit)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01765EZhz9fQ1mgDvsqFKeQr
claude added 13 commits August 26, 2026 08:38
…pool handlers

- Clone excludes skill.kiln/SKILL.md case- and normalization-folded, so
  a variant like 'Skill.MD' on a case-insensitive filesystem can't
  overwrite the regenerated identity files in staging
- Aggregate request content capped (3M chars across files) at parse
  time, bounding decode/staging work for a max-count bundle
- Skill install/clone/resource handlers are plain def (FastAPI
  threadpool) so their file I/O and fsyncs don't block the event loop;
  a process-wide write lock keeps the install-once name check race-free

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hr8tm9wQRGYZyTPpcy1kr
- Bundle paths containing lone surrogates (or otherwise not encodable to
  the filesystem) are rejected at validation as a 422, with a belt catch
  converting any staging UnicodeEncodeError to a validation error
- Size/UTF-8 error messages use repr so echoed paths can't break the
  JSON error response; API 422 details are surrogate-safe joined
- The skills/ parent directory entry is fsynced (best-effort) after the
  commit rename, so a power loss right after a reported success can't
  roll the installed skill back

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hr8tm9wQRGYZyTPpcy1kr
…details centrally

- Resource listing skips filenames that aren't UTF-8 (surrogateescape
  names from hand-added files) instead of failing the whole listing
- Lone-surrogate scrubbing moved into the shared HTTPException handler
  (module-level safe_str), protecting every endpoint's detail and
  removing the per-endpoint helper
- Drop redundant binascii.Error catch (it subclasses ValueError)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hr8tm9wQRGYZyTPpcy1kr
- Replace print-statement placeholder strings in test data with plain
  comments; the Debug Detector workflow greps for bare print( in any
  .py file, string literals included
- HTTPException detail sanitization now recurses into dicts/lists so
  nested request-derived strings (e.g. bulk-upload failed filenames)
  can't break the error response encoding either

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hr8tm9wQRGYZyTPpcy1kr
…async handlers

- Remove the global skill-name uniqueness check: coexisting versions of
  a skill share a name and differ by id (run configs reference skills by
  id). Install-once now means immutability only, not name exclusivity
- Resource directory routing is an explicit mapping that raises on an
  unknown prefix — no silent fallback to assets/
- New sweep_stale_skill_staging(), called on desktop app startup, so
  crash-orphaned staging drafts never accumulate
- Add .skill_staging/ to the git-sync .gitignore (transient, uuid-named
  per install — never worth syncing)
- Route handlers are async again per repo convention; the write lock is
  gone with the name-uniqueness check that motivated it
- Test asserts sanitized surrogate absent from error responses

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hr8tm9wQRGYZyTPpcy1kr
is_dir() follows symlinks, so a symlinked staging root (e.g. synced in)
could redirect the startup sweep's deletions to an arbitrary directory,
and installs would stage through it. Both paths now reject/skip
symlinked staging roots, with tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hr8tm9wQRGYZyTPpcy1kr
Found during end-to-end stress testing: with duplicate skill names now
allowed project-wide (coexisting versions), nothing stopped attaching
two same-named skills to one run config — and SkillTool loads skills by
name, so one would silently shadow the other at runtime. Run config
creation now returns a 422 naming the colliding skills.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hr8tm9wQRGYZyTPpcy1kr
handle_save in saved_run_configs_dropdown swallowed rejections (try/
finally with no catch), so a failed save — e.g. the 422 for duplicate
skill names in one run config — spun and then silently did nothing. The
component already declared save_config_error and rendered it below the
dropdown; the handler now sets it, and clears it on the next attempt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hr8tm9wQRGYZyTPpcy1kr
…ling

These land separately as a focused change targeting main; existing
behavior (the adapter's runtime duplicate-name check) is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hr8tm9wQRGYZyTPpcy1kr
- Path validators reject drive-letter forms (C:/x, C:x) and leading
  backslashes platform-independently: a leading-slash check alone
  misses them, and on Windows joining such a path onto the staging dir
  would override the base entirely. Applies to inline files and copy
  paths alike, so validation behaves the same on every OS
- The commit rename never follows a symlinked skills/ directory:
  mkdir(exist_ok=True) accepts an existing symlink and os.rename would
  write the bundle outside the project folder — now a clear 422, with a
  regression test proving nothing escapes through the link

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hr8tm9wQRGYZyTPpcy1kr
A references/ or assets/ dir replaced with a symlink would become the
containment base after resolve(), exposing whatever it points at through
resource reads and listings. Reject it on read and skip it when listing.

Also skip test_clone_allows_backslash_in_filename on Windows, where
backslash is a path separator and the filename can't exist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hr8tm9wQRGYZyTPpcy1kr
Drop the judge_feedback_batch MVP prompt-optimizer entirely: core
datamodel + runner, the synchronous REST API, the job-backed worker,
their tests, the jobs-table UI branch, the OpenAPI tag, and the
never-shipped agent-check annotations. Regenerated the OpenAPI TS
schema.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y9LpFRd9NFwY1em32ZRMn7
# Conflicts:
#	app/desktop/studio_server/judge_feedback_batch_api.py
@leonardmq
leonardmq marked this pull request as draft August 28, 2026 17:10
@leonardmq leonardmq changed the title Assistant sub-agents: local runtime, tabs UI, steer/stop, report injection feat: Kiln assistant — auto mode, background eval jobs, and sub-agents (runtime, tabs UI, steer/stop) Aug 28, 2026
@leonardmq
leonardmq changed the base branch from leonard/kil-686-eval-job to main August 28, 2026 17:33
claude and others added 13 commits August 28, 2026 18:38
… review

Jobs API:
- POST /api/jobs/wait can no longer hang a request coroutine forever: the
  timeout is now always bounded (default 600s, max 3600s; 504 means "still
  running", re-issue to keep waiting). A plain handler is not cancelled on
  client disconnect, and a PAUSED job is not terminal, so an unbounded wait
  on one leaked the coroutine for the life of the process and hung the
  calling agent. Docs now spell out the PAUSED behavior.
- wait_many holds the JobRecord references it validated up front instead of
  re-reading the registry map after the await, so deleting an
  already-terminal job mid-wait can no longer 500 the wait with a KeyError.

Conversation runtime:
- enable_auto pre-marks the record RUNNING while the consent batch executes:
  a message POSTed in that await window now queues to the inbox instead of
  starting a burst that made start_run 409 and stranded the executed spawn's
  results (leaving the gating spawn_subagent call forever unanswered in the
  persisted trace).
- Deciding a live parked approval batch now publishes a RUNNING
  conversation-state event on wake, so other tabs clear their approval box
  (the frontend's box-clearing relies on onWorkingChange(true)).
- rehydrate_pending_approvals re-runs its entry guards after the trace fetch
  await, so two tabs refreshing concurrently converge on one batch instead
  of minting two (the loser's batch id 404ing on decide).
- The upstream session-snapshot fetch quotes the session key into the URL
  (a browser-supplied key containing '/', '..', '?' or '#' could otherwise
  reach a different upstream path with the user's bearer token).

Hygiene:
- disable_auto_mode tool docs and standalone run() now describe/mirror the
  actual contract: the app server refuses the call (auto mode turns off only
  via the user's Stop button) — they previously promised {"status":
  "disabled"} and an executed disable.
- Removed dead code: 7 agent-check annotation files for the deleted
  /api/chat/auto/* routes, the stale route-collision comment on
  /api/jobs/evals/run, and the permanently-skipped old-loop golden test.
- Regenerated the OpenAPI TS schema for the jobs-wait timeout change.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LkBuCiAoZbG36BX6bmc5ej
The Debug Detector workflow flags any print( call and any TODO/FIXME outside
markdown. Two leftovers in this branch tripped it: the golden-fixture
regeneration script's print (now sys.stdout.write — same CLI output) and a
comment referring to "the phase-3 TODO" (now "follow-up").

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LkBuCiAoZbG36BX6bmc5ej
- format_subagent_report now escapes agent_type like title (a quote or
  markup in a dynamic attribute could corrupt the frame the client parses).
- chat.svelte guards child steer sends with a pending flag: Enter can no
  longer double-send while a child POST is in flight, and text typed during
  the await can't be wiped by the first request's success.
- beginInteractiveTurn re-checks the store generation after ensure()
  resolves (same pattern as resyncOnLoad), so a reset()/loadSession() racing
  the ensure can't stamp the abandoned conversation's session id back.
- streaming_chat.test.ts precedence test uses distinct ids for the two
  spellings so the assertion can actually fail; the consent-dialog spawn
  callout test is split so the manual (no payload) variant is really covered.
- Removed developer-local absolute paths from the committed specs.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LkBuCiAoZbG36BX6bmc5ej
Resolve conflicts with the code tools work that landed on main:
- Project: keep both the code_tools and memories parent_of entries/accessors
- task_run: keep both the usage re-exports and the shared validate_tags import
- tool_id/tool_registry: keep both the code tool and memory tool ID branches,
  and drop the memory branch's project derivation now that
  tool_from_id_and_project receives the project directly

Regenerate the OpenAPI TypeScript schema for the merged API surface.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9DQBRdbvamDeEpfWuZ78A
- list_memories: reject negative limit/offset instead of falling into negative
  index slicing, which returned a surprising page and a wrong remaining count.
  Reachable from the agent tool, whose schema has no minimum (the REST API
  already constrains both).
- save_memory tool: missing overview/scope now returns a tool error rather than
  raising an uncaught KeyError, matching update/delete in the same file.
- Memory.scope: declare max_length so the cap the validator already enforces
  shows up in the generated schema, like overview and content.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9DQBRdbvamDeEpfWuZ78A
Resolves conflicts in eval_api.py and test_eval_api.py where main's
code-trust gate for code evals and this branch's provenance validation
landed in the same create_eval_config path. Both are kept: provenance is
validated first, then the code-trust gate runs before save.

In test_eval_api.py, main relocated and expanded the eval_api import block,
so this branch's UpdateRunConfigRequest import folds into main's block. The
test-body conflicts were both-sided independent additions; all tests from
both branches are retained.
- provenance_api: use builtin type[] instead of typing.Type (PEP 585),
  dropping the now-unused import.
- provenance: compare derived_from_ids duplicates on the stripped id, so a
  whitespace-padded repeat is caught by the duplicate check rather than
  slipping through to the sibling-existence lookup. Drops the redundant
  str() so the type checker sees only str in the seen set. Test added.
Artifact provenance: shared lineage submodel across compiled/tunable artifacts
Two conflicts from the artifact-provenance work on the new base:
- datamodel/__init__.py: keep both KilnArtifactProvenance and Memory in __all__
- datamodel/extraction.py: keep both the KilnArtifactProvenance import and the
  shared validate_tags import

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9DQBRdbvamDeEpfWuZ78A
Phase 1: Add Memory datamodel and store-agnostic core API
Resolves three conflicts, all in the skill create/clone path where this
branch's bundle work meets the artifact provenance that landed on the base
via #1565:

- skill_api.py: the create endpoint keeps atomic bundle installation and
  also accepts provenance. Lineage is validated before anything is staged,
  so a bad parent id can't leave a half-written bundle behind. The clone
  endpoint gains the same optional provenance field.
- skill_bundle.py: create_skill_with_files and clone_skill take an optional
  provenance and stamp it on the skill they commit. A clone never inherits
  its source's provenance — a clone's provenance describes the clone.
- skill_form.svelte: clone keeps using the dedicated clone endpoint (so
  references/ and assets/ are copied) while stamping the lineage the base
  branch added; a fresh create still posts origin "human" with no parent.

Regenerated api_schema.d.ts; agent policy annotations verified current.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011aBTP89YWb8uWy3x5N42r1
…ccess-rj28jo

Skill bundles: atomic create with resource files, full-directory clone, resource APIs
# Conflicts:
#	libs/core/kiln_ai/tools/skill_tool.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants