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.
135 lines
5.3 KiB
Plaintext
135 lines
5.3 KiB
Plaintext
@startuml
|
||
!theme plain
|
||
title Turnstone — Tool Execution Pipeline (Three Phases)
|
||
|
||
start
|
||
|
||
partition "Phase 1: Prepare" #E8F5E9 {
|
||
:Receive tool_calls list from LLM response;
|
||
|
||
while (more tool_calls?) is (yes)
|
||
:Extract call_id, func_name, raw_args;
|
||
|
||
if (json.loads(raw_args) succeeds?) then (yes)
|
||
:parsed_args = JSON dict;
|
||
else (no)
|
||
:Fallback 1: regex extraction;
|
||
if (regex found keys?) then (yes)
|
||
:parsed_args = extracted dict;
|
||
else (no)
|
||
:Fallback 2: bare string →\nPRIMARY_KEY_MAP[func_name];
|
||
endif
|
||
endif
|
||
|
||
:Dispatch to _prepare_{func_name}();
|
||
|
||
note right
|
||
**Dispatch table (16 built-in + tool_search):**
|
||
┌───────────────┬──────────────────┐
|
||
│ Tool │ Needs Approval? │
|
||
├───────────────┼──────────────────┤
|
||
│ bash │ ✓ Yes │
|
||
│ read_file │ ✗ Auto-approve │
|
||
│ write_file │ ✓ Yes │
|
||
│ edit_file │ ✓ Yes │
|
||
│ search │ ✗ Auto-approve │
|
||
│ diff_file │ ✗ Auto-approve │
|
||
│ web_fetch │ ✗ Auto-approve │
|
||
│ web_search │ ✗ Auto-approve │
|
||
│ tool_search │ ✗ Auto-approve │
|
||
│ task_agent │ ✓ Yes │
|
||
│ memory │ ✗ Auto-approve │
|
||
│ recall │ ✗ Auto-approve │
|
||
│ notify │ ✗ Auto-approve │
|
||
│ watch │ ✓ create only │
|
||
│ skill │ ✓ load only │
|
||
│ read_resource │ ✓ Yes │
|
||
│ use_prompt │ ✓ Yes │
|
||
├───────────────┼──────────────────┤
|
||
│ mcp__* │ ✓ Yes (external) │
|
||
└───────────────┴──────────────────┘
|
||
end note
|
||
|
||
:Build item dict:
|
||
{call_id, func_name, header,
|
||
preview, needs_approval,
|
||
approval_label, execute: Callable};
|
||
endwhile (no)
|
||
}
|
||
|
||
partition "Phase 2: Approve" #FFF3E0 {
|
||
if (any items need approval?) then (yes)
|
||
:_emit_state("attention");
|
||
:ui.approve_tools(items);
|
||
|
||
note right
|
||
**auto_approve check is handled
|
||
internally by ui.approve_tools()**
|
||
|
||
**TerminalUI**: Print headers/previews,
|
||
prompt [y/n/a, optional message]
|
||
If user chose "always":
|
||
Add pending tool names to auto_approve_tools
|
||
(auto-approve these tool types going forward)
|
||
**WebUI**: Enqueue approve_request,
|
||
block on _approval_event.wait()
|
||
**NullUI**: Return (True, None)
|
||
end note
|
||
|
||
if (user approved?) then (yes)
|
||
:_emit_state("running");
|
||
else (denied)
|
||
:Mark all pending items as denied;
|
||
:denial_msg = "Denied by user";
|
||
:_emit_state("running");
|
||
endif
|
||
else (all auto-approved)
|
||
:ui enqueues tool_info event\n(no blocking);
|
||
endif
|
||
}
|
||
|
||
partition "Phase 3: Execute" #E3F2FD {
|
||
:_check_cancelled();
|
||
note right: Cancellation checkpoint:\nraises GenerationCancelled if\ncancel event is set
|
||
if (single tool call?) then (yes)
|
||
:Execute sequentially:\nrun_one(items[0]);
|
||
else (multiple)
|
||
:Execute in parallel:\nThreadPoolExecutor(max_workers=4)\npool.map(run_one, items);
|
||
endif
|
||
|
||
note right
|
||
**run_one(item):**
|
||
if item.error → return error string
|
||
if item.denied → return denial message
|
||
else → item["execute"](item)
|
||
├─ _exec_bash: subprocess.run(["bash", script.sh])
|
||
├─ _exec_read_file: open().readlines() or _exec_read_image (base64)
|
||
├─ _exec_write_file: makedirs + write
|
||
├─ _exec_edit_file: find_occurrences + replace
|
||
├─ _exec_search: grep subprocess
|
||
├─ _exec_web_fetch: httpx.get + LLM summary
|
||
├─ _exec_web_search: SearxNG JSON GET (fallback for local models)
|
||
├─ _exec_tool_search: BM25 search + expand_visible()
|
||
├─ _exec_task: _run_agent(TASK_AGENT_TOOLS)
|
||
├─ _exec_notify: HTTP POST to channel gateway
|
||
├─ _exec_memory: structured memory save/search/delete/list
|
||
├─ _exec_recall: conversation history FTS5 search
|
||
├─ _exec_read_resource: MCPClientManager.read_resource_sync()
|
||
├─ _exec_use_prompt: MCPClientManager.get_prompt_sync()
|
||
└─ _exec_mcp_tool: MCPClientManager.call_tool_sync()
|
||
end note
|
||
|
||
:Collect results: [(call_id, output), ...];
|
||
|
||
:_truncate_output() on each result\n(max context_window × chars_per_token × 0.5 chars\ndefault: ~context_window × 2 chars);
|
||
|
||
:bash: ui.on_tool_output_chunk(call_id, line) per stdout line;
|
||
:ui.on_tool_result(call_id, name, output, is_error) for each;
|
||
}
|
||
|
||
:Return (results, user_feedback);
|
||
|
||
stop
|
||
|
||
@enduml
|