diff --git a/services/studio/src/nmp/studio/coding_agent_artifacts.py b/services/studio/src/nmp/studio/coding_agent_artifacts.py
index 83d407360d..1a64852223 100644
--- a/services/studio/src/nmp/studio/coding_agent_artifacts.py
+++ b/services/studio/src/nmp/studio/coding_agent_artifacts.py
@@ -165,16 +165,23 @@ def _decode_answer_pair_value(value: str) -> str:
return decoded if isinstance(decoded, str) else value
+def answer_selection_pairs(text: str) -> list[tuple[str, str]]:
+ """Return the question and answer pairs persisted by AskUserQuestion."""
+ pairs: list[tuple[str, str]] = []
+ for match in _ANSWER_PAIR_RE.finditer(text):
+ question = _decode_answer_pair_value(match.group(1)).strip()
+ answer = _decode_answer_pair_value(match.group(2)).strip()
+ if question and answer:
+ pairs.append((question, answer))
+ return pairs
+
+
def record_answer_selections(
artifacts: ChatArtifactsResponse,
text: str,
question_labels: dict[str, str] | None = None,
) -> None:
- for match in _ANSWER_PAIR_RE.finditer(text):
- question = _decode_answer_pair_value(match.group(1)).strip()
- answer = _decode_answer_pair_value(match.group(2)).strip()
- if not question or not answer:
- continue
+ for question, answer in answer_selection_pairs(text):
label = question_labels.get(question) if question_labels else None
_set_selection_artifact(artifacts, label or _selection_label(question), answer)
diff --git a/services/studio/src/nmp/studio/coding_agent_mcp_tools.py b/services/studio/src/nmp/studio/coding_agent_mcp_tools.py
index d6d41be457..634f79795f 100644
--- a/services/studio/src/nmp/studio/coding_agent_mcp_tools.py
+++ b/services/studio/src/nmp/studio/coding_agent_mcp_tools.py
@@ -226,12 +226,14 @@
),
(
"For clarification, multiple-choice, yes/no, or freeform questions that do NOT map to one of "
- "the select_* tools, use Claude Code's AskUserQuestion tool rather than writing a "
- "questionnaire in markdown."
+ "the select_* tools, use Claude Code's AskUserQuestion tool rather than a questionnaire "
+ "in markdown."
),
(
- "Only fall back to plain chat questions when no suitable UI tool is available, the user "
- "already provided the value, or the UI tool returns skipped or error."
+ "Only fall back to plain chat questions when no suitable UI tool exists, the user already "
+ "provided the value, or the user explicitly skips the UI tool. A timeout, disconnect, or "
+ "other UI-tool error is not permission to continue or repeat the question in plain text; "
+ "leave the input unresolved and tell the user the interactive request must be retried."
),
(
"Set UI tool titles, descriptions, display labels, and output_key values to match the "
diff --git a/services/studio/src/nmp/studio/coding_agents.py b/services/studio/src/nmp/studio/coding_agents.py
index b9693902cd..bf58cdc1bd 100644
--- a/services/studio/src/nmp/studio/coding_agents.py
+++ b/services/studio/src/nmp/studio/coding_agents.py
@@ -9,7 +9,7 @@
import os
import shutil
import uuid
-from collections.abc import AsyncIterator, Mapping
+from collections.abc import AsyncIterator, Awaitable, Mapping
from dataclasses import dataclass
from dataclasses import field as dataclass_field
from pathlib import Path
@@ -21,6 +21,7 @@
from nmp.studio import studio_links
from nmp.studio.coding_agent_artifacts import (
ChatArtifactsResponse,
+ answer_selection_pairs,
record_answer_selections,
record_coding_agent_model,
record_spec_text_artifacts,
@@ -54,13 +55,18 @@
MCP_ROUTE_NAME = "studio_coding_agent_mcp"
PUBLIC_MCP_ROUTE_NAME = "studio_coding_agent_public_mcp"
+PUBLIC_MCP_UNSUPPORTED_METHOD_ROUTE_NAME = "studio_coding_agent_public_mcp_unsupported_method"
PUBLIC_MCP_PATH = "/studio/api/coding-agents/mcp/{session_id}"
+CLAUDE_MCP_TOOL_TIMEOUT_MS = 2_147_483_647
+MCP_KEEPALIVE_INTERVAL_SECONDS = 15
CLAUDE_PROJECTS_DIR = Path.home() / ".claude" / "projects"
SERVER_CWD = Path(os.getcwd()).resolve()
STUDIO_CONTEXT_START = ""
STUDIO_CONTEXT_END = ""
STUDIO_CONTEXT_USER_REQUEST_PREFIX = "User request:"
+STUDIO_MESSAGE_SUMMARY_START = "<<>>"
+STUDIO_MESSAGE_SUMMARY_END = "<<>>"
class NewSessionResponse(BaseModel):
@@ -133,6 +139,14 @@ class HistorySummary:
chat_artifacts: ChatArtifactsResponse = dataclass_field(default_factory=ChatArtifactsResponse)
+@dataclass(frozen=True)
+class HistoryToolUse:
+ """Tool metadata needed to restore user interactions during history replay."""
+
+ name: str
+ input: dict[str, Any]
+
+
def _mcp_tools_for_destinations(
destinations: Mapping[str, studio_links.StudioLinkDestination],
) -> list[dict[str, Any]]:
@@ -148,6 +162,13 @@ def mount_public_mcp_route(app: FastAPI) -> None:
name=PUBLIC_MCP_ROUTE_NAME,
include_in_schema=False,
)
+ app.add_api_route(
+ PUBLIC_MCP_PATH,
+ mcp_unsupported_method,
+ methods=["GET", "DELETE"],
+ name=PUBLIC_MCP_UNSUPPORTED_METHOD_ROUTE_NAME,
+ include_in_schema=False,
+ )
def _validate_session_id(session_id: str) -> str:
@@ -290,10 +311,31 @@ def _build_studio_system_prompt(
"Whenever you need a fileset, fileset reference, dataset, or input/source data file (including an anonymizer or evaluation input, or a CSV/Parquet file), you MUST call mcp__nemo_studio__select_dataset_file instead of asking for a fileset reference or '/#' path in plain text; for an evaluation config file, you MUST call mcp__nemo_studio__select_eval_config.",
"Treat 'which agent', 'pick an agent', 'choose a model', 'which fileset', and 'what is your fileset reference' as mandatory tool-use requests for the matching select_* tool, exactly like Studio link requests are mandatory studio_link requests.",
"Set the picker title and description to match the current workflow, for example title='Select agent to audit'.",
- "Only skip a picker when the user already gave the value, the value is already unambiguous from the conversation, or a previous picker call returned skipped or error.",
+ "Only skip a picker when the user already gave the value, the value is already unambiguous from the conversation, or the user explicitly skipped a previous picker.",
+ "A timeout, disconnect, or other interactive-tool error is not permission to continue or repeat the question in plain text. Leave the input unresolved and tell the user the interactive request must be retried.",
+ "A message that needs user input is not complete until you call the matching Studio input tool. Never end a message with only a plain-text question when an interactive tool applies.",
+ "In particular, if you need an agent, model, dataset file, or evaluation config, call the matching select_* tool before completing the message; mentioning the needed selection in prose is not a substitute for the tool call.",
"For finite choices that have no dedicated Studio picker (for example deployments, jobs, or next actions) and for yes/no or multiple-choice clarifications, use Claude Code's AskUserQuestion tool so Studio can render clickable options instead of asking the user to type.",
"For AskUserQuestion, provide input shaped as {'questions': [{'header': '', 'question': '', 'options': [{'label': '