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
Draft
feat: Kiln assistant — auto mode, background eval jobs, and sub-agents (runtime, tabs UI, steer/stop)#1561leonardmq wants to merge 172 commits into
leonardmq wants to merge 172 commits into
Conversation
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
…pacting indicator
…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
…to leonard/kil-692-auto-mode-resilience
…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
marked this pull request as draft
August 28, 2026 17:10
… 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Desktop + web UI for assistant sub-agents:
/api/conversationssub-agent API surface.<subagent_report>messages); kickoff message echoed onto the child stream at run start.Follow-up work on this stack lands in PR #1558 (into this branch).
Latest: mid-turn message injection, event-loop stalls, forensic debug logging
/api/conversations?parent=hang/pile-ups):agent_overviewandget_tags(plus a third endpoint since removed with the judge-feedback-batch subsystem) still ran full dataset disk scans on the loop; now offloaded viaasyncio.to_thread(same treatment as a02cfeb).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 carriesX-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.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 firehoseEventSourcewedged before headers; failed reconciles are logged instead of swallowed. Forensics: sub-agent settles emitrun_settledto the debug log, and the empty-model-response error carriesfinish_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_serverserves 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).
Main features
EvalJobWorker+ a typed, non-streamingPOST /api/jobs/evals/runkickoff for agents, plus a bulkPOST /api/jobs/waitto await many jobs at once. Agents use these instead of the SSE eval endpoints.chat/auto/app-server subsystem and matching web UI.Agent permissions
Agent-allowed job endpoints:
POST /api/jobs/evals/run(allow + approval — evals cost AI credits) andPOST /api/jobs/wait(allow). The UI-only/api/chat/auto/*set and the SSErun_comparison/run_calibrationeval 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 existingEvalRunnerso 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.run_comparison,run_calibration) flipped to agent-forbidden.JobDerivedState.erroris nowint | None; a worker that can't derive its error count from entities returnsNone, and_apply_derivedkeeps the live reported count instead of clobbering it to0.2. Assistant auto mode (rescoped, #1518)
chat/auto/subsystem: auto-run registry, runner, event bus, SSE, and API (/api/chat/auto/*: enable, decline, resolve, sessions, per-run message/stop/events).enable_auto_mode/disable_auto_modebuilt-in tools + tool-registry wiring.auto_run_store, consent dialog, chat-history grouping, reworkedchat.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).jobs/events.py(KeepalivePing/iter_with_keepalive, generic over aTypeVar).Literal["user"]role, optimistic-flag reset on inject failure, consent-event trace fallback, keyboard-focus reveal,consentPendingheld throughrequestEnable, and a guard soresyncOnLoadcan't overwrite a newly-selected session on a mid-flight conversation switch.3. Multi-job wait + eval per-item error logging
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;EvalJobWorkerforwards each failed dataset item's exception toctx.report_error(withdataset_id+run_config_id). Previously only the error count was reported, soGET /api/jobs/{id}/errorsshowed "no errors recorded" even when every item failed.4. Context-usage gauge + compaction indicator — KIL-727 (#1519)
ChatSessionSnapshotgains acontext_usagefield (context_tokens,context_limit,context_percent,compacted) so a session reopened from history renders the gauge. Relies on Pydantic's defaultextra="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_usage_gauge.svelte) — a small grey bar in the input footer (opposite Auto mode), percentage above and a tooltip showing approximateused / totaltokens; hidden before the first turn. Token counting is intentionally approximate (overestimate-biased; copy uses "≈"). Tooltip fixed to measure after visible and made keyboard-accessible.finished(a fast started→finished pair would collapse the window); cleared by the first real assistant content, on error, and on reset.streaming_chat.ts/chat_session_store.ts/auto_run_store.tsso the gauge updates during auto-mode bursts too.5. Auto/manual congruence + gauge polish
chat/auto/runner.py).6. Jobs-API consolidation — bulk wait only (#1527)
GET /{id}/wait(bulk wait covers it with one id) and the genericPOST /api/jobs/{type}create endpoint (production only runs evals, via the typed route — the generic path had no real caller).GET /api/jobs/waitconverted toPOST /api/jobs/waitwith aWaitForJobsRequestbody (ids as JSON, not repeated query params); with the generic POST gone, the route-ordering workaround is dropped./jobstest page becomes a real jobs panel;NoopJobWorkerstays as a registry-level test fixture but is no longer registered in production.7. Auto-mode resilience — KIL-692 (#1529)
<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.auto-mode-retrySSE event drives a "retrying N/M…" transcript affordance.USER_STOPPED(publishes auto-mode-off) instead of leaving the flag on.8. Worker properties + eval summary in the jobs table (#1530)
compute_statepure-read pattern; stored dict-on-the-wire likeprogress_detail, computed behind a guard so a failingdescribe()never breaks job creation).describe()→ type-check →model_dump()path wrapped so a bad payload can never breakcreate(); 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)
KilnRunError.originalsoGET /api/jobs/{id}/errorsshows the real failure instead of the generic "An unexpected error occurred." wrapper text.__str__on the original exception (falls back to the class name), and covered the commonRetryableError(str(original))path.10. Chat retry in both modes + hard Stop (#1532, #1533)
iter_round_with_retries(stream_session.py) owns the retry loop and is used by both the auto runner and interactiveChatStreamSession.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_RETRIESderives from its length.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_stop_dialog.svelte(notes that already-kicked-off background jobs keep running).11. Mid-turn message queueing (#1534)
<system-reminder>side-note framing from hydrated transcripts.12. Eval job model docs
Fielddescriptions to the eval job models.13. Judge feedback batch subsystem (#1536) — since removed
judge_feedback_batchcapability (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 validatedconcurrencyparam (feat(judge-feedback-batch): expose concurrency in job params #1544) remains.14. Structured add-example dialog (#1540, via main)
$lib/components/add_example_dialog.sveltewith configurableinclude_input/include_outputand 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 + exportedGuideSampletype.15. Remove the judge feedback batch subsystem
judge_feedback_batchsubsystem 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