mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
110d44b07e
`man` and `math` duplicated capabilities already reachable through `bash`; `plan_agent` is better expressed as a `task_agent` running a planning skill, and carried a large amount of special-case machinery (plan-review gate, refinement loop, per-kind model routing). Removing all three shrinks the tool surface and cuts per-call token cost. Also removed, as dead-once-the-tools-are-gone: - the `math` sandbox executor (`turnstone.core.sandbox`) and its `[sandbox]` extra; the eval analyst now runs bash-only - the read-only `AGENT_TOOLS` sub-agent tool set and the `agent` tool-metadata key (`task_agent`/`TASK_AGENT_TOOLS` retained) - the plan-review protocol end to end: the `on_plan_review` UI hook, `resolve_plan`, `POST /v1/api/plan` + `POST /v1/api/route/plan`, the `plan_review`/`plan_resolved` SSE events, and their Python SDK / TypeScript SDK / OpenAPI / frontend / Discord+Slack bindings - the `model.plan_alias` / `model.plan_effort` settings and the registry `plan_model` / `plan_effort` routing fields TOOLS 31->28, TASK_AGENT_TOOLS 13->11; COORDINATOR_TOOLS unchanged. BREAKING CHANGE: removes the `man`, `math`, `plan_agent` tools, the plan-review SSE/HTTP/SDK surface, and the plan_* model-routing settings from the experimental 1.6 line.
184 lines
5.6 KiB
Plaintext
184 lines
5.6 KiB
Plaintext
@startuml
|
||
!theme plain
|
||
title Turnstone — Conversation Turn Lifecycle
|
||
|
||
skinparam sequenceArrowThickness 1.5
|
||
skinparam sequenceLifeLineBackgroundColor #F5F5F5
|
||
|
||
participant "User /\nHTTP Client" as User
|
||
participant "ChatSession" as CS
|
||
participant "SessionUI" as UI
|
||
participant "LLMProvider\n(OpenAI / Anthropic)" as LLM
|
||
participant "Tool Executor\n(ThreadPool)" as TP
|
||
database "SQLite" as DB
|
||
|
||
== User Input ==
|
||
|
||
User -> CS : send(user_input)
|
||
activate CS
|
||
|
||
CS -> CS : messages.append({role: "user", content: input})
|
||
CS -> DB : save_message(ws_id, "user", input)
|
||
|
||
== LLM Call Loop ==
|
||
|
||
group loop [while tool_calls present]
|
||
|
||
CS -> UI : on_turn_start()
|
||
note right of UI
|
||
SessionUIBase resets the per-turn inflight
|
||
buffers (_ws_inflight_content / reasoning /
|
||
seq) that fuel the SSE in_progress_snapshot
|
||
event for mid-stream refresh resume.
|
||
end note
|
||
|
||
CS -> UI : on_state_change("thinking")
|
||
CS -> UI : on_thinking_start()
|
||
|
||
CS -> LLM : provider.create_streaming(\n client, model, messages, tools, ...)\n (normalized StreamChunk iterator)
|
||
activate LLM
|
||
|
||
note right of CS
|
||
Retry up to 3× on transient errors:
|
||
RateLimitError, APITimeoutError,
|
||
APIConnectionError, InternalServerError,
|
||
ServiceUnavailableError, APIError
|
||
Backoff: 1s, 2s, 4s
|
||
end note
|
||
|
||
== Streaming Response ==
|
||
|
||
loop for each chunk in stream
|
||
LLM --> CS : delta
|
||
note right of CS
|
||
on_thinking_stop() called on first
|
||
delta token via _stop_spinner_once()
|
||
end note
|
||
alt reasoning_content present
|
||
CS -> UI : on_reasoning_token(text)
|
||
else content present
|
||
CS -> UI : on_content_token(text)
|
||
else tool_call delta
|
||
CS -> CS : accumulate in tool_calls_acc
|
||
else info_delta present
|
||
CS -> UI : on_info(text)\n(e.g. server-side web search status)
|
||
end
|
||
end
|
||
|
||
note right of CS
|
||
**Cancellation checkpoint:**
|
||
_check_cancelled() runs per chunk.
|
||
If cancel_event is set, raises
|
||
GenerationCancelled — preserves
|
||
partial content, emits idle state.
|
||
end note
|
||
|
||
LLM --> CS : stream complete (usage stats)
|
||
deactivate LLM
|
||
|
||
CS -> UI : on_thinking_stop() (no-op guard: already called by _stop_spinner_once)
|
||
CS -> UI : on_stream_end()
|
||
|
||
CS -> CS : _update_token_table()\ncalibrate chars_per_token ratio
|
||
CS -> CS : messages.append(assistant_msg)
|
||
CS -> UI : on_turn_committed()
|
||
note right of UI
|
||
Drops the per-turn inflight buffers — the
|
||
assistant message is now in the history
|
||
list, so the in_progress_snapshot must
|
||
not re-render it during the next tool-
|
||
execution window or the next streaming turn.
|
||
end note
|
||
CS -> DB : save_message(ws_id, "assistant", content)
|
||
CS -> DB : save_message(ws_id, "tool_call", ...) ×N
|
||
|
||
== Tool Dispatch (if tool_calls) ==
|
||
|
||
alt no tool_calls
|
||
CS -> UI : on_status(usage, context_window, effort)
|
||
|
||
opt prompt_tokens > context_window × auto_compact_pct
|
||
CS -> CS : _compact_messages(auto=True)
|
||
CS -> LLM : Non-streaming summarization call
|
||
CS -> CS : Replace messages with [summary]
|
||
end
|
||
|
||
opt first exchange & no title
|
||
CS -> CS : Background thread: _generate_title()
|
||
end
|
||
|
||
CS -> UI : on_state_change("idle")
|
||
CS --> User : return
|
||
|
||
else has tool_calls
|
||
CS -> UI : on_state_change("running")
|
||
|
||
== Phase 1: Prepare ==
|
||
CS -> CS : [_prepare_tool(tc) for tc in tool_calls]\nParse JSON args, validate,\nbuild preview + header
|
||
|
||
== Phase 2: Approve ==
|
||
CS -> UI : on_state_change("attention")
|
||
CS -> UI : approve_tools(items)
|
||
activate UI
|
||
note right of UI
|
||
TerminalUI: input() prompt
|
||
WebUI: _approval_event.wait()
|
||
NullUI: returns (True, None)
|
||
end note
|
||
UI --> CS : (approved: bool, feedback: str?)
|
||
deactivate UI
|
||
CS -> UI : on_state_change("running")
|
||
|
||
== Phase 3: Execute ==
|
||
CS -> TP : ThreadPoolExecutor(max_workers=4)\nrun_one(item) for each tool
|
||
activate TP
|
||
|
||
note right of TP
|
||
Parallel execution:
|
||
bash → Popen + line-by-line streaming
|
||
read_file → open().read() or base64 image
|
||
search → grep subprocess
|
||
edit_file → string replace
|
||
task → _run_agent() sub-loop
|
||
web_fetch → httpx + LLM summarize
|
||
web_search → provider-native or SearxNG fallback
|
||
memory/recall → SQLite
|
||
end note
|
||
|
||
note right of TP
|
||
bash: on_tool_output_chunk(call_id, line)
|
||
called per stdout line,
|
||
then on_tool_result(call_id, name, output, is_error).
|
||
is_error=True when execution failed.
|
||
call_id routes chunks/results to correct
|
||
tool div during parallel execution.
|
||
Other tools: on_tool_result() only.
|
||
end note
|
||
|
||
TP --> CS : [(call_id, output), ...]
|
||
deactivate TP
|
||
|
||
loop for each result
|
||
CS -> CS : messages.append({role: "tool", ...})
|
||
CS -> DB : save_message(ws_id, "tool_result", ...)
|
||
end
|
||
|
||
opt user_feedback from approval
|
||
CS -> CS : messages.append({role: "user", content: feedback})
|
||
end
|
||
|
||
note right of CS : Loop back for next LLM call
|
||
|
||
else GenerationCancelled
|
||
CS -> CS : Preserve partial content\nor roll back incomplete tools
|
||
CS -> UI : on_info("[Generation cancelled]")
|
||
CS -> UI : on_state_change("idle")
|
||
CS --> User : return (no re-raise)
|
||
end
|
||
|
||
end
|
||
|
||
deactivate CS
|
||
|
||
@enduml
|