Skip to content
Draft
Show file tree
Hide file tree
Changes from 122 commits
Commits
Show all changes
172 commits
Select commit Hold shift + click to select a range
4bb73d6
feat: run evals as background jobs via /api/jobs/evals/run
leonardmq Jun 25, 2026
73350dd
feat(assistant): auto mode, rebased onto the eval-job branch
leonardmq Jun 25, 2026
20b27a5
fix: address PR review comments on auto mode rescope
leonardmq Jun 25, 2026
a503a19
fix: guard resyncOnLoad against mid-flight conversation switch
leonardmq Jun 25, 2026
8d80450
feat(jobs): multi-job wait endpoint + log eval per-item errors
leonardmq Jun 25, 2026
13ce998
Phase 3: studio_server proxy contract for context_usage
leonardmq Jun 25, 2026
c76fb59
web_ui: add context-usage gauge (Phase 4)
leonardmq Jun 25, 2026
3ffb64d
feat(chat): show "summarizing…" compaction indicator during pre-infer…
leonardmq Jun 25, 2026
c1a8641
fix(web_ui): position gauge tooltip after it's visible (PR feedback)
leonardmq Jun 25, 2026
5472f82
fix(web_ui): make context gauge tooltip keyboard-accessible (PR feedb…
leonardmq Jun 25, 2026
af673f7
refactor: auto mode manual vs server congruence, ctx gauge color, com…
leonardmq Jun 29, 2026
9bea3c0
refactor: better handling of auto tool congruence
leonardmq Jun 29, 2026
0b0740e
Merge pull request #1519 from Kiln-AI/leonard/kil-727-assistant-compa…
leonardmq Jun 29, 2026
3c56d5f
Merge branch 'leonard/kil-686-feat-background-job-system' of github.c…
leonardmq Jun 29, 2026
00accb9
Remove single-job wait endpoint in favor of bulk wait
leonardmq Jun 29, 2026
7a22914
Drop generic create-job endpoint and make wait a POST
leonardmq Jun 29, 2026
cf40cb2
Merge pull request #1527 from Kiln-AI/leonard/only-bulk-wait-job
leonardmq Jun 29, 2026
cb6c34b
refactor: pass auto mode info to backend
leonardmq Jun 29, 2026
5ed3df4
feat(auto): frame mid-burst user messages as side notes
leonardmq Jun 29, 2026
c32a799
feat(auto): retry transient upstream errors with backoff
leonardmq Jun 29, 2026
fd22c16
feat(auto): render "retrying N/M…" in the transcript
leonardmq Jun 29, 2026
4d7457a
feat(jobs): publish worker properties, render eval summary in jobs table
leonardmq Jun 29, 2026
a974ac7
refactor(jobs): narrow eval props once, drop redundant optional chaining
leonardmq Jun 29, 2026
ff7d201
fix(jobs): render judge name, title the job id, cover prompt-resoluti…
leonardmq Jun 29, 2026
69d576a
fix(auto): honor Stop during retry + cover the no-duplicate guard
leonardmq Jun 29, 2026
633a039
fix(jobs): guard the full _describe() serialization path
leonardmq Jun 29, 2026
113199a
test(jobs): always release gated worker in properties test
leonardmq Jun 29, 2026
87d55d5
Merge pull request #1530 from Kiln-AI/leonard/kil-686-job-properties
leonardmq Jun 29, 2026
f0b347e
fix(auto): address PR review feedback (trace id, None-safe content, t…
leonardmq Jun 29, 2026
4aa9593
Merge branch 'leonard/kil-686-eval-job' of github.com:Kiln-AI/Kiln in…
leonardmq Jun 29, 2026
eb7ec9a
fix(jobs): surface original eval error instead of generic wrapper
leonardmq Jun 30, 2026
a529c78
fix(jobs): harden eval error-detail extraction against buggy __str__
leonardmq Jun 30, 2026
6839f5f
test(jobs): cover RetryableError path and assert error-log extras
leonardmq Jun 30, 2026
ab8aefb
Merge pull request #1531 from Kiln-AI/leonard/kil-686-eval-job-expose…
leonardmq Jun 30, 2026
102780f
feat(chat): retry transient upstream errors in BOTH auto and interactive
leonardmq Jun 30, 2026
8ac6556
fix(chat): polish retry indicator + fix compacting on follow-up turns
leonardmq Jun 30, 2026
509ee13
feat(chat): slower retry backoff schedule (~5 min total)
leonardmq Jun 30, 2026
917818c
fix(chat): honor Stop during a retry backoff before re-POSTing
leonardmq Jun 30, 2026
36bc500
feat(chat): replace auto-mode "waiting for you" with a hard Stop button
leonardmq Jun 30, 2026
2812f50
Merge pull request #1532 from Kiln-AI/leonard/kil-692-chat-retry-both…
leonardmq Jun 30, 2026
e6be854
Merge branch 'leonard/kil-686-eval-job' of github.com:Kiln-AI/Kiln in…
leonardmq Jun 30, 2026
5360fac
Merge pull request #1529 from Kiln-AI/leonard/kil-692-auto-mode-resil…
leonardmq Jun 30, 2026
a3df450
feat(chat): confirm Stop with a dialog noting background jobs keep ru…
leonardmq Jun 30, 2026
01cb65c
style(chat): align Stop dialog cancel button label with codebase conv…
leonardmq Jun 30, 2026
99f18da
style(chat): simplify Stop dialog body to a single paragraph
leonardmq Jun 30, 2026
3a94e92
Merge pull request #1533 from Kiln-AI/leonard/kil-686-auto-mode-stop-…
leonardmq Jun 30, 2026
9853857
feat(chat): queue messages sent mid-turn with send-now / edit / cancel
leonardmq Jun 30, 2026
1378aeb
Merge branch 'leonard/kil-686-eval-job' of github.com:Kiln-AI/Kiln in…
leonardmq Jun 30, 2026
c8aba36
fix(chat): inject queued messages immediately in auto mode; reword ba…
leonardmq Jun 30, 2026
62f4a44
fix(chat): hold auto-mode queued message until the next round boundary
leonardmq Jun 30, 2026
eefddbe
fix(chat): fix duplicate render + lost message on refresh for injecte…
leonardmq Jun 30, 2026
21e1745
fix(chat): strip <system-reminder> side-note framing from hydrated tr…
leonardmq Jun 30, 2026
c7430f2
fix(chat): preserve queued text on rejected dispatch + guard send-now…
leonardmq Jun 30, 2026
ea6a043
style(chat): cleaner queued-message banner scroll (header row + edge …
leonardmq Jun 30, 2026
8a4d564
fix(chat): dedupe injected-message echo on re-attach (stop refresh du…
leonardmq Jun 30, 2026
e71503d
fix(chat): render in-flight round into a fresh turn on re-attach (sto…
leonardmq Jun 30, 2026
a0bb85f
Merge pull request #1534 from Kiln-AI/leonard/kil-queued-chat-message
leonardmq Jul 1, 2026
1d89072
Merge branch 'leonard/kil-686-feat-background-job-system' of github.c…
leonardmq Jul 1, 2026
c649b79
docs(jobs): add Field descriptions to eval job models
leonardmq Jul 1, 2026
4b5a5ad
Merge judge_feedback_batch into kil-686 (jobs + judge in one branch) …
tawnymanticore Jul 2, 2026
6f57d99
Merge branch 'main' of github.com:Kiln-AI/Kiln into leonard/kil-686-e…
leonardmq Jul 2, 2026
91eeae0
Merge branch 'leonard/kil-686-eval-job' of github.com:Kiln-AI/Kiln in…
leonardmq Jul 2, 2026
38c4083
Fix dead transient-error retries and give background jobs a patient r…
leonardmq Jul 3, 2026
fe2f15d
Share a recursive KilnRunError unwrap between retry classification an…
leonardmq Jul 3, 2026
51cbfb8
Merge pull request #1547 from Kiln-AI/leonard/kil-686-job-transient-r…
leonardmq Jul 3, 2026
93cbec2
Add assistant sub-agents implementation plan spec
leonardmq Jul 8, 2026
d38a9e7
Add local sub-agent runtime: registry, runner, orchestration tools, API
leonardmq Jul 8, 2026
6a44597
Regenerate web UI OpenAPI types for sub-agent endpoints
leonardmq Jul 8, 2026
6e77f1f
Add sub-agent UI: tabs, live transcripts, steer/stop, consent, report…
leonardmq Jul 8, 2026
7dada75
Bump desktop version to 1.0.4 — the release that ships sub-agents
leonardmq Jul 8, 2026
e016e82
Surface errorText in chat stream errors instead of a generic fallback
leonardmq Jul 8, 2026
e3e1cbe
Fix children lookup for auto-mode parents: alias leaf trace ids to th…
leonardmq Jul 8, 2026
325b6aa
Render sub-agent tabs with full main-transcript parity
leonardmq Jul 8, 2026
f8a0b39
Sub-agent UX round: live report panels, tab lifecycle, overflow, stat…
leonardmq Jul 8, 2026
47e3c68
Echo the kickoff message onto the sub-agent stream at run start
leonardmq Jul 8, 2026
aafa6fa
Deliver sub-agent reports exclusively via the injection channel
leonardmq Jul 8, 2026
ab73457
Show working indicator for a running sub-agent awaiting its first res…
leonardmq Jul 8, 2026
a84cbbc
Add unified conversation runtime foundation (chat/runtime/) + golden-…
leonardmq Jul 8, 2026
45f0ac2
Port sub-agents onto the unified conversation runtime (phase 2)
leonardmq Jul 9, 2026
8c9521c
Port auto mode onto the unified conversation runtime (phase 3)
leonardmq Jul 9, 2026
9c7db0d
Port interactive chat onto the unified conversation runtime (phase 4)
leonardmq Jul 9, 2026
4e6423a
Key the desktop↔browser chat surface on session ids (phase 5)
leonardmq Jul 9, 2026
948939f
Adopt upstream session-id continuation; drop the desktop root→leaf sc…
leonardmq Jul 9, 2026
1589787
Fix two queued-message races in the unified interactive path
leonardmq Jul 9, 2026
1c2ff2b
Wake idle interactive parents when a sub-agent reports back
leonardmq Jul 9, 2026
4df65f2
Fix intermittent missing sub-agent tab for a running child
leonardmq Jul 9, 2026
c021747
Fix CI: check in agent-policy annotations for /api/conversations; def…
leonardmq Jul 10, 2026
0acde43
Address review: broaden SSE parse guard, chain-scoped index eviction,…
leonardmq Jul 10, 2026
45c880a
Ensure background-sync test stops the poll task on failure (try/finally)
leonardmq Jul 10, 2026
820b9ad
Merge pull request #1558 from Kiln-AI/leonard/assistant-unified-runtime
leonardmq Jul 10, 2026
a02cfeb
Assistant fixes: kill-the-tree stop, per-agent gauge/scroll/queue UX,…
leonardmq Jul 10, 2026
a7160b4
Assistant: mid-turn message injection, remaining event-loop stalls, f…
leonardmq Jul 10, 2026
ddf2f2d
spec(artifact_provenance): add project overview
claude Jul 10, 2026
984ab72
spec(artifact_provenance): add functional spec (draft)
claude Jul 10, 2026
9ae0322
spec(artifact_provenance): mark functional spec complete
claude Jul 10, 2026
f99e08e
spec(artifact_provenance): add architecture (draft) + refine validators
claude Jul 10, 2026
9609647
spec(artifact_provenance): finalize architecture + add implementation…
claude Jul 10, 2026
883e965
spec(artifact_provenance): mark implementation plan complete
claude Jul 10, 2026
df87955
feat(datamodel): add KilnArtifactProvenance submodel and helper (prov…
claude Jul 10, 2026
68a3f3b
Merge remote-tracking branch 'origin/main' into leonard/kil-686-eval-job
leonardmq Jul 10, 2026
f470235
Merge branch 'leonard/kil-686-eval-job' into leonard/assistant-subagents
leonardmq Jul 10, 2026
362da63
feat(provenance): Tier-1 backend wiring for artifact provenance (phas…
claude Jul 10, 2026
3f79e96
Add agent_memory spec project (planning docs)
claude Jul 10, 2026
25dffd7
feat(provenance): Tier-1 clone/create forms stamp provenance lineage …
claude Jul 10, 2026
921cecb
Sub-agent tab strip resilience + forensics from first debug-log post-…
leonardmq Jul 10, 2026
4976dcd
Implement agent memory Phase 1 (Memory datamodel + core store)
claude Jul 10, 2026
3c7f726
feat(provenance): Tier-2 backend wiring for artifact provenance (phas…
claude Jul 10, 2026
391224c
Implement agent memory Phase 3 (harness tool adapters)
claude Jul 10, 2026
f0dd3fa
feat(provenance): Tier-2 clone wiring + full OpenAPI client regen (ph…
claude Jul 10, 2026
590fab0
fix(provenance): stamp raw sibling id when cloning a prompt
claude Jul 10, 2026
05b4448
fix(provenance): address PR review feedback
claude Jul 11, 2026
83ffcb0
Add Phase 4 (memory REST API); supersede Phase 2 (experiments MCP)
claude Jul 13, 2026
54124d4
Implement Phase 4: memory REST API (libs/server)
claude Jul 13, 2026
d242e9e
Remove 'stale'-tag guidance from memory tool/API descriptions
claude Jul 14, 2026
6fdd34d
Add merge-blocker TODOs to gate judge feedback batch from main
claude Jul 16, 2026
5d0d23f
Merge pull request #1589 from Kiln-AI/claude/judge-feedback-todo-bloc…
scosman Jul 16, 2026
776df4b
specs: assistant_autonomy_lifecycle project (overview, functional spe…
leonardmq Jul 22, 2026
4d31e7f
Phase 1 (assistant_autonomy_lifecycle): auto mode only turns off by u…
leonardmq Jul 22, 2026
a9c20c0
Phase 2 (assistant_autonomy_lifecycle): spawn requires auto mode, one…
leonardmq Jul 22, 2026
c638734
Phase 3 (assistant_autonomy_lifecycle): consent spawn variant, observ…
leonardmq Jul 22, 2026
5cd00c8
Address PR review feedback: busy guard before auto flip, spec doc cor…
leonardmq Jul 22, 2026
7ff0838
Fix sub-agent tabs sometimes never appearing: event-carried lineage, …
leonardmq Jul 22, 2026
455a786
Merge pull request #1615 from Kiln-AI/leonard/subagent-tab-event-attr…
leonardmq Jul 22, 2026
6efc11a
Merge pull request #1614 from Kiln-AI/leonard/assistant-autonomy-life…
leonardmq Jul 22, 2026
57330b7
Gate assistant dev affordances behind PUBLIC_ENABLE_DEV_TOOLS
leonardmq Jul 22, 2026
f3d4488
Gate sub-agent history rows server-side (desktop dev mode + upstream …
leonardmq Jul 22, 2026
ee76cbd
Revert "Gate sub-agent history rows server-side (desktop dev mode + u…
leonardmq Jul 22, 2026
6693982
Merge pull request #1616 from Kiln-AI/leonard/devtools-gate-assistant…
leonardmq Jul 22, 2026
80db3a8
Raise memory caps to 280/4000 and harden write-side validation
claude Jul 30, 2026
d281278
Merge pull request #1633 from Kiln-AI/scosman/memory-caps
scosman Jul 30, 2026
1201708
Flag the missing eval-job split parameter as a ship blocker
claude Aug 12, 2026
149a1c1
Merge remote-tracking branch 'origin/main' into tmp/kil686-main
leonardmq Aug 21, 2026
721c494
Make the eval job split-aware, now that eval_splits has landed
leonardmq Aug 21, 2026
b45016d
Fix CI: types, annotations, and the lint the merge un-hid
leonardmq Aug 21, 2026
4e6a1c4
Merge pull request #1717 from Kiln-AI/claude/kil-686-sync-main
leonardmq Aug 21, 2026
1d3b774
Merge leonard/kil-686-eval-job into the sub-agents branch
leonardmq Aug 21, 2026
fca02ee
Clear the two Debug Detector hits that are genuinely leftovers
leonardmq Aug 24, 2026
eba5aa2
Merge remote-tracking branch 'origin/leonard/kil-686-eval-job' into t…
leonardmq Aug 24, 2026
f0ee6e2
Add full skill bundle support: atomic create with files, provenance, …
claude Aug 26, 2026
a80fdc4
Remove provenance from skill bundles — landing separately in PR #1565
claude Aug 26, 2026
d44ae49
Code review round 1: harden skill bundle validation and resource endp…
claude Aug 26, 2026
5b1a5ab
Code review round 2: stream clone copies, honest error codes, dedupe …
claude Aug 26, 2026
93c4e15
Code review round 3: clone edge cases, staging durability, resource-r…
claude Aug 26, 2026
a0bde31
Code review round 4: Windows fsync guard, normalization collisions, f…
claude Aug 26, 2026
63d332e
Code review round 5: cross-dict conflicts, empty dirs on clone, base6…
claude Aug 26, 2026
d6c4a96
Code review round 6: complete surrogate hardening, clone case-variant…
claude Aug 26, 2026
0bc4554
Code review round 7: clone case-fold exclusion, aggregate cap, thread…
claude Aug 26, 2026
ffcd5ef
Code review round 8: reject unencodable paths, durable commit rename
claude Aug 26, 2026
fe9a384
Code review round 9: skip undecodable filenames, scrub HTTPException …
claude Aug 26, 2026
44456f8
Fix Debug Detector CI failure and sanitize nested HTTPException details
claude Aug 26, 2026
974d71a
Address review feedback: allow duplicate skill names, startup sweep, …
claude Aug 26, 2026
da0400f
Never stage or sweep through a symlinked .skill_staging
claude Aug 26, 2026
be11b6b
Reject duplicate skill names within one run config
claude Aug 26, 2026
8cb3467
Show save errors from the run page's Save current options action
claude Aug 27, 2026
0d6d7a8
Remove run-config duplicate-skill-name validation and save-error hand…
claude Aug 27, 2026
a92d139
Reject Windows-absolute bundle paths and a symlinked skills directory
claude Aug 27, 2026
62a50c0
Reject symlinked resource roots; skip backslash filename test on Windows
claude Aug 27, 2026
1329863
Remove the judge feedback batch subsystem
claude Aug 28, 2026
e7cc958
Merge branch 'leonard/kil-686-eval-job' into leonard/assistant-subagents
claude Aug 28, 2026
39d216c
Fix jobs-wait hang, runtime races, and review hygiene items from code…
claude Aug 28, 2026
324d478
Fix Debug Detector CI: drop stray print and TODO wording
claude Aug 30, 2026
d7a2e95
Address CodeRabbit review threads (escaping, races, tests, spec hygiene)
claude Aug 30, 2026
e12b27e
Merge main into agent memory branch
claude Aug 31, 2026
a00cbe0
Address review feedback on the memory store and tools
claude Aug 31, 2026
a4bffa0
Merge branch 'main' into artifact provenance branch
claude Aug 31, 2026
0fa169e
Address two remaining bot review nitpicks
claude Aug 31, 2026
ee282f6
Merge pull request #1565 from Kiln-AI/claude/new-project-spec-0zdve1
leonardmq Aug 31, 2026
b28eb96
Merge leonard/assistant-subagents into the agent memory branch
claude Aug 31, 2026
1a68f9e
Merge pull request #1572 from Kiln-AI/claude/kiln-new-project-mtncvl
leonardmq Aug 31, 2026
e190c2f
Merge leonard/assistant-subagents into the skill bundle branch
claude Aug 31, 2026
4047c86
Merge pull request #1732 from Kiln-AI/claude/kiln-integration-tests-a…
leonardmq Sep 1, 2026
95abb08
Merge remote-tracking branch 'origin/main' into pr-1561-work
claude Sep 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ reviews/
# ActRight discover local state
.act_right

.playwright-mcp/
6 changes: 5 additions & 1 deletion app/desktop/desktop_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@
from app.desktop.log_config import log_config
from app.desktop.studio_server.agent_api import connect_agent_api
from app.desktop.studio_server.batch_plan_api import connect_batch_plan_api
from app.desktop.studio_server.chat import connect_chat_api
from app.desktop.studio_server.chat import (
connect_chat_api,
connect_conversations_api,
)
from app.desktop.studio_server.code_tool_api import connect_code_tool_api
from app.desktop.studio_server.copilot_api import connect_copilot_api
from app.desktop.studio_server.data_gen_api import connect_data_gen_api
Expand Down Expand Up @@ -158,6 +161,7 @@ def make_app(tk_root: tk.Tk | None = None):
connect_agent_api(app)
connect_dev_tools(app)
connect_chat_api(app)
connect_conversations_api(app)
connect_jobs_api(app)
# Important: webhost must be last, it handles all other URLs
connect_webhost(app)
Expand Down
4 changes: 4 additions & 0 deletions app/desktop/dev_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ def set_dev_env_vars() -> None:
os.environ.setdefault("KILN_SKIP_REMOTE_MODEL_LIST", "true")
os.environ["DEBUG_EVENT_LOOP"] = "true"
os.environ["KILN_DEV_MODE"] = "true"
# Assistant forensic debug logging on by default in dev (unless explicitly set, e.g.
# KILN_CHAT_DEBUG_LOG=0) so the JSONL timeline already exists when an assistant issue
# needs investigating. See chat/debug_log.py.
os.environ.setdefault("KILN_CHAT_DEBUG_LOG", "1")
50 changes: 41 additions & 9 deletions app/desktop/git_sync/test_background_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,26 @@ def second_clone(git_repos, tmp_path: Path):
return second_path


async def wait_for(predicate, timeout: float = 10.0, interval: float = 0.05) -> None:
"""Poll until the (sync or async) predicate is truthy, or fail loudly.

Fixed sleeps flake on loaded CI runners — a real ``git fetch`` on an
executor thread can take longer than any constant chosen here. Polling
with a generous deadline keeps the tests fast locally and deterministic
under load.
"""
deadline = asyncio.get_event_loop().time() + timeout
while True:
result = predicate()
if asyncio.iscoroutine(result):
result = await result
if result:
return
if asyncio.get_event_loop().time() > deadline:
pytest.fail(f"Condition not met within {timeout}s")
await asyncio.sleep(interval)


@pytest.mark.asyncio
async def test_poll_loop_fetches_and_fast_forwards(manager, git_repos, second_clone):
local_path, _ = git_repos
Expand All @@ -40,7 +60,11 @@ async def test_poll_loop_fetches_and_fast_forwards(manager, git_repos, second_cl
bg = BackgroundSync(manager, poll_interval=0.05, idle_pause_after=60.0)
await bg.start()
try:
await asyncio.sleep(0.3)

async def head_advanced() -> bool:
return await manager.get_head() != old_head

await wait_for(head_advanced)
finally:
await bg.stop()

Expand Down Expand Up @@ -72,18 +96,26 @@ async def test_idle_pause_and_resume(manager, git_repos, second_clone):
bg = BackgroundSync(manager, poll_interval=0.05, idle_pause_after=0.1)
await bg.start()

await asyncio.sleep(0.3)
try:
# Let the loop notice it has been idle for > idle_pause_after and
# park. (Parking has no observable flag; two 0.05s ticks past the
# 0.1s idle threshold suffice, so 0.5s carries a wide margin.)
await asyncio.sleep(0.5)

commit_in_repo(second_clone, "after_idle.txt", "data", "after idle")
push_from(second_clone)

commit_in_repo(second_clone, "after_idle.txt", "data", "after idle")
push_from(second_clone)
old_head = await manager.get_head()
assert not (local_path / "after_idle.txt").exists()

old_head = await manager.get_head()
assert not (local_path / "after_idle.txt").exists()
bg.notify_request()

bg.notify_request()
await asyncio.sleep(0.3)
async def head_advanced() -> bool:
return await manager.get_head() != old_head

await bg.stop()
await wait_for(head_advanced)
finally:
await bg.stop()

new_head = await manager.get_head()
assert new_head != old_head
Expand Down
84 changes: 47 additions & 37 deletions app/desktop/studio_server/agent_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -551,40 +551,50 @@ async def agent_overview(
Path(description="The unique identifier of the task within the project."),
],
) -> AgentOverview:
task = task_from_id(project_id, task_id)
project = task.parent_project()
if project is None:
raise HTTPException(status_code=404, detail="Project not found")

task_run_configs = get_all_run_configs(project_id, task_id)

instruction_text = truncate_to_words_with_agent_sentinel(task.instruction, 70)

return AgentOverview(
project=AgentOverviewProject(
id=project.id,
name=project.name,
description=project.description,
),
task=AgentOverviewTask(
id=task.id,
name=task.name,
description=task.description,
instruction=instruction_text or "",
input_json_schema=task.input_json_schema,
output_json_schema=task.output_json_schema,
default_run_config_id=task.default_run_config_id,
),
dataset=_dataset_stats(task),
docs=_docs_stats(project),
search_tools=_search_tools_block(project),
prompts=_prompts_block(task, project, task_run_configs),
specs=_specs_block(task),
evals=_evals_block(task),
tool_servers=_tool_servers_block(project),
run_configs=_run_configs_block(task, task_run_configs),
fine_tunes=_fine_tunes_block(task),
prompt_optimization_jobs=_prompt_optimization_jobs_block(task),
skills=_skills_block(project),
connected_providers=await _connected_providers_block(),
)
connected_providers = await _connected_providers_block()

# The block builders below scan child directories on disk (runs, specs,
# evals, fine-tunes, ...) — far too slow for the event loop on large
# datasets, so the whole assembly runs in a worker thread.
def build_overview() -> AgentOverview:
task = task_from_id(project_id, task_id)
project = task.parent_project()
if project is None:
raise HTTPException(status_code=404, detail="Project not found")

task_run_configs = get_all_run_configs(project_id, task_id)

instruction_text = truncate_to_words_with_agent_sentinel(
task.instruction, 70
)

return AgentOverview(
project=AgentOverviewProject(
id=project.id,
name=project.name,
description=project.description,
),
task=AgentOverviewTask(
id=task.id,
name=task.name,
description=task.description,
instruction=instruction_text or "",
input_json_schema=task.input_json_schema,
output_json_schema=task.output_json_schema,
default_run_config_id=task.default_run_config_id,
),
dataset=_dataset_stats(task),
docs=_docs_stats(project),
search_tools=_search_tools_block(project),
prompts=_prompts_block(task, project, task_run_configs),
specs=_specs_block(task),
evals=_evals_block(task),
tool_servers=_tool_servers_block(project),
run_configs=_run_configs_block(task, task_run_configs),
fine_tunes=_fine_tunes_block(task),
prompt_optimization_jobs=_prompt_optimization_jobs_block(task),
skills=_skills_block(project),
connected_providers=connected_providers,
)

return await asyncio.to_thread(build_overview)
7 changes: 3 additions & 4 deletions app/desktop/studio_server/chat/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from app.desktop.studio_server.chat.routes import ExecuteToolsRequest, connect_chat_api
from app.desktop.studio_server.chat.routes import connect_chat_api
from app.desktop.studio_server.chat.runtime.api import connect_conversations_api
from app.desktop.studio_server.chat.sse_parser import EventParser
from app.desktop.studio_server.chat.stream_session import (
ChatStreamSession,
RoundState,
ToolCallInfo,
execute_tool,
Expand All @@ -14,13 +14,12 @@
)

__all__ = [
"ChatStreamSession",
"EventParser",
"ExecuteToolsRequest",
"KilnToolInputMetadata",
"RoundState",
"ToolCallInfo",
"connect_chat_api",
"connect_conversations_api",
"execute_tool",
"execute_tool_batch",
"tool_input_executor_is_server",
Expand Down
13 changes: 12 additions & 1 deletion app/desktop/studio_server/chat/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,22 @@
from pydantic import TypeAdapter

CHAT_TIMEOUT = httpx.Timeout(timeout=300.0, connect=30.0)
MAX_TOOL_ROUNDS = 100
MAX_TOOL_ROUNDS = 1000

SSE_TYPE_TOOL_CALLS_PENDING = "tool-calls-pending"
SSE_TYPE_TOOL_EXEC_START = "kiln-tool-execution-start"
SSE_TYPE_TOOL_EXEC_END = "kiln-tool-execution-end"
# Emitted by the OLD interactive loop when the model calls enable_auto_mode
# (survives until phase 4 ports the interactive path; the runtime interceptor
# emits the same event via stream_session's formatter). The rest of the old
# auto lifecycle vocabulary (auto-mode-on/off/idle/state) died in phase 3,
# replaced by the unified conversation-state event (runtime/sse.py).
SSE_TYPE_AUTO_MODE_CONSENT_REQUIRED = "auto-mode-consent-required"
# Emitted between retry attempts after a transient upstream failure (by BOTH the
# interactive chat stream and the runtime engine — they share the retry helper),
# so the UI can show "retrying N/M…" instead of a hard error. Carries
# {attempt, max_attempts, status_code?, run_id?}.
SSE_TYPE_CHAT_RETRY = "kiln-chat-retry"

DENIED_TOOL_OUTPUT = json.dumps(
{"error": "The user did not accept the toolcall"}, ensure_ascii=False
Expand Down
71 changes: 71 additions & 0 deletions app/desktop/studio_server/chat/debug_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Forensic debug logging for the assistant runtime (desktop side).

Enabled with the ``KILN_CHAT_DEBUG_LOG`` environment variable: ``1``/``true``
logs to ``<settings>/logs/kiln_chat_debug.jsonl``; any other non-empty value
is used as the target file path. Each event is one JSON line carrying ids and
timings — never message content — keyed by the conversation ``session_id``
(``cv_…``). The desktop sends that same id upstream on every request as
``X-Kiln-Conversation-Id``, and kiln_server's ``chat_debug_log_enabled`` flag
logs it on its side, so the two timelines can be joined per conversation when
investigating a stuck or misbehaving assistant conversation.

This is a local debugging tool: writes are synchronous appends under a
process-wide lock and the file grows unbounded while the flag is set.
"""

import json
import os
import threading
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from kiln_ai.utils.config import Config

ENV_VAR = "KILN_CHAT_DEBUG_LOG"
_FALSY = frozenset(["", "0", "false", "no", "off"])
_TRUTHY_DEFAULT_PATH = frozenset(["1", "true", "yes", "on"])

_write_lock = threading.Lock()
_process_started = time.monotonic()


def chat_debug_enabled() -> bool:
return os.getenv(ENV_VAR, "").strip().lower() not in _FALSY


def _target_path() -> Path:
value = os.getenv(ENV_VAR, "").strip()
if value.lower() in _TRUTHY_DEFAULT_PATH:
return Path(Config.settings_dir()) / "logs" / "kiln_chat_debug.jsonl"
return Path(value)


def chat_debug_log(
event: str, conversation_id: str | None = None, **fields: Any
) -> None:
"""Append one JSONL debug event; a no-op unless the env flag is set.

``elapsed_ms`` is process-relative (a shared monotonic clock) so gaps
between events are readable without timestamp arithmetic.
"""
if not chat_debug_enabled():
return
record: dict[str, Any] = {
"ts": datetime.now(timezone.utc).isoformat(timespec="milliseconds"),
"elapsed_ms": round((time.monotonic() - _process_started) * 1000, 1),
"event": event,
"conversation_id": conversation_id,
**fields,
}
line = json.dumps(record, default=str, ensure_ascii=False)
# Debug-only tool: a failed write must never affect the runtime.
try:
path = _target_path()
with _write_lock:
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "a", encoding="utf-8") as f:
f.write(line + "\n")
except OSError:
pass
72 changes: 9 additions & 63 deletions app/desktop/studio_server/chat/helpers.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import json
from unittest.mock import AsyncMock, MagicMock
"""Tiny shared test fixtures for the chat package.

Phase 4 note: this module used to also carry the httpx mock builders
(``make_httpx_mock`` / ``make_n_round_mock_client`` and the PATCH_* targets)
that drove the deleted ``POST /api/chat`` route tests; the surviving suites
script upstream rounds through ``chat/test_fakes.py`` instead, so only the
SSE payload builder remains.
"""

PATCH_ASYNC_CLIENT = "app.desktop.studio_server.chat.stream_session.httpx.AsyncClient"
PATCH_EXECUTE_TOOL = "app.desktop.studio_server.chat.stream_session.execute_tool"
import json


def sse_text_delta(delta: str, text_id: str = "text-test") -> bytes:
Expand All @@ -12,62 +17,3 @@ def sse_text_delta(delta: str, text_id: str = "text-test") -> bytes:
"delta": delta,
}
return f"data: {json.dumps(payload, ensure_ascii=False)}\n\n".encode()


def make_httpx_mock(status_code: int = 200, chunks: list[bytes] | None = None):
if chunks is None:
chunks = [sse_text_delta("hello")]

async def mock_aiter_bytes():
for chunk in chunks:
yield chunk

mock_upstream = MagicMock()
mock_upstream.status_code = status_code
mock_upstream.aiter_bytes.return_value = mock_aiter_bytes()
mock_upstream.aread = AsyncMock(
return_value=b'{"message":"upstream error"}' if status_code != 200 else b""
)
mock_upstream.__aenter__ = AsyncMock(return_value=mock_upstream)
mock_upstream.__aexit__ = AsyncMock(return_value=None)

mock_client = MagicMock()
mock_client.stream.return_value = mock_upstream
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=None)

mock_async_client_class = MagicMock(return_value=mock_client)
return mock_async_client_class, mock_client, mock_upstream


def make_stream_mock(chunks: list[bytes]):
"""Create a mock upstream response that yields the given SSE chunks."""

async def mock_aiter_bytes():
for chunk in chunks:
yield chunk

mock_upstream = MagicMock()
mock_upstream.status_code = 200
mock_upstream.aiter_bytes.return_value = mock_aiter_bytes()
mock_upstream.__aenter__ = AsyncMock(return_value=mock_upstream)
mock_upstream.__aexit__ = AsyncMock(return_value=None)
return mock_upstream


def make_n_round_mock_client(*chunk_rounds: list[bytes]):
"""Create a mock httpx client that serves multiple streaming rounds in sequence."""
mocks = [make_stream_mock(chunks) for chunks in chunk_rounds]
call_count = 0

def side_effect(*args, **kwargs):
nonlocal call_count
idx = min(call_count, len(mocks) - 1)
call_count += 1
return mocks[idx]

mock_client = MagicMock()
mock_client.stream.side_effect = side_effect
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=None)
return mock_client, lambda: call_count
Loading
Loading