mirror of
https://github.com/HKUDS/OpenSpace.git
synced 2026-08-28 05:15:00 +00:00
1553 lines
58 KiB
Python
1553 lines
58 KiB
Python
"""Model-visible attachment messages.
|
|
|
|
OpenSpace stores attachment payloads in ``_meta.attachment`` and renders them
|
|
as provider-neutral ``role=user`` system-reminder text. This module is the
|
|
single attachment envelope/formatter used by compact, memory, skills, and the
|
|
agent loop.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Any, Iterable, Mapping, Sequence
|
|
from uuid import uuid4
|
|
|
|
from openspace.grounding.core.types import BackendType, ToolStatus
|
|
from openspace.services.runtime_support.plan_mode import (
|
|
PLAN_MODE_ATTACHMENT_TURNS_BETWEEN_ATTACHMENTS,
|
|
PLAN_MODE_FULL_REMINDER_EVERY_N_ATTACHMENTS,
|
|
)
|
|
TODO_REMINDER_TURNS_SINCE_WRITE = 10
|
|
TODO_REMINDER_TURNS_BETWEEN_REMINDERS = 10
|
|
|
|
POST_COMPACT_MAX_FILES_TO_RESTORE = 5
|
|
POST_COMPACT_TOKEN_BUDGET = 50_000
|
|
POST_COMPACT_MAX_TOKENS_PER_FILE = 5_000
|
|
POST_COMPACT_MAX_TOKENS_PER_SKILL = 5_000
|
|
POST_COMPACT_SKILLS_TOKEN_BUDGET = 25_000
|
|
|
|
CLAUDE_IN_CHROME_MCP_SERVER_NAME = "claude-in-chrome"
|
|
CHROME_TOOL_SEARCH_INSTRUCTIONS = """**IMPORTANT: Before using any chrome browser tools, you MUST first load them using ToolSearch.**
|
|
|
|
Chrome browser tools are MCP tools that require loading before use. Before calling any mcp__claude-in-chrome__* tool:
|
|
1. Use ToolSearch with `select:mcp__claude-in-chrome__<tool_name>` to load the specific tool
|
|
2. Then call the tool
|
|
|
|
For example, to get tab context:
|
|
1. First: ToolSearch with query "select:mcp__claude-in-chrome__tabs_context_mcp"
|
|
2. Then: Call mcp__claude-in-chrome__tabs_context_mcp"""
|
|
|
|
|
|
def create_attachment_message(attachment: Mapping[str, Any]) -> dict[str, Any]:
|
|
"""Create a provider-neutral OpenSpace attachment message."""
|
|
|
|
safe_attachment = _json_safe_attachment(dict(attachment))
|
|
return {
|
|
"role": "user",
|
|
"content": format_attachment_for_model(safe_attachment),
|
|
"_meta": {
|
|
"type": "attachment",
|
|
"attachment_type": safe_attachment.get("type"),
|
|
"attachment": safe_attachment,
|
|
"uuid": str(uuid4()),
|
|
"timestamp": time.time(),
|
|
},
|
|
}
|
|
|
|
|
|
def format_attachment_for_model(attachment: Mapping[str, Any]) -> str:
|
|
attachment_type = attachment.get("type")
|
|
|
|
if attachment_type == "file":
|
|
filename = str(attachment.get("filename") or "")
|
|
content = _content_to_text(attachment.get("content"))
|
|
note = ""
|
|
if attachment.get("truncated"):
|
|
note = (
|
|
f"\n\nNote: The file {filename} was too large and has been "
|
|
"truncated. Use the read tool to read more of the file if needed."
|
|
)
|
|
return (
|
|
"<system-reminder>\n"
|
|
f"Contents of {filename}:\n\n{content}{note}"
|
|
"\n</system-reminder>"
|
|
)
|
|
|
|
if attachment_type == "compact_file_reference":
|
|
filename = str(attachment.get("filename") or "")
|
|
return (
|
|
"<system-reminder>\n"
|
|
f"Note: {filename} was read before the last conversation was "
|
|
"summarized, but the contents are too large to include. Use the "
|
|
"read tool if you need to access it."
|
|
"\n</system-reminder>"
|
|
)
|
|
|
|
if attachment_type == "nested_memory":
|
|
content = attachment.get("content")
|
|
if isinstance(content, Mapping):
|
|
path = str(content.get("path") or attachment.get("path") or "")
|
|
body = str(content.get("content") or "")
|
|
else:
|
|
path = str(attachment.get("path") or "")
|
|
body = str(content or "")
|
|
return (
|
|
"<system-reminder>\n"
|
|
f"Contents of {path}:\n\n{body}"
|
|
"\n</system-reminder>"
|
|
)
|
|
|
|
if attachment_type == "relevant_memories":
|
|
chunks: list[str] = []
|
|
for memory in attachment.get("memories") or []:
|
|
if not isinstance(memory, Mapping):
|
|
continue
|
|
header = str(memory.get("header") or memory.get("path") or "memory")
|
|
chunks.append(f"{header}\n{memory.get('content') or ''}")
|
|
return (
|
|
"<system-reminder>\n"
|
|
+ ("Relevant memories:\n\n" + "\n\n---\n\n".join(chunks) if chunks else "No relevant memories.")
|
|
+ "\n</system-reminder>"
|
|
)
|
|
|
|
if attachment_type == "deferred_tools_delta":
|
|
added = [str(line) for line in (attachment.get("addedLines") or [])]
|
|
removed = [str(name) for name in (attachment.get("removedNames") or [])]
|
|
parts: list[str] = []
|
|
if added:
|
|
parts.append(
|
|
"The following deferred tools are now available via `tool_search`:\n"
|
|
+ "\n".join(added)
|
|
)
|
|
if removed:
|
|
parts.append(
|
|
"The following deferred tools are no longer available:\n"
|
|
+ "\n".join(removed)
|
|
)
|
|
if not parts:
|
|
parts.append("Deferred tool availability did not change.")
|
|
return "<system-reminder>\n" + "\n\n".join(parts) + "\n</system-reminder>"
|
|
|
|
if attachment_type == "agent_listing_delta":
|
|
added = [str(line) for line in (attachment.get("addedLines") or [])]
|
|
removed = [str(name) for name in (attachment.get("removedTypes") or [])]
|
|
parts: list[str] = []
|
|
if added:
|
|
heading = (
|
|
"Available agent types for the Agent tool:"
|
|
if attachment.get("isInitial")
|
|
else "New agent types are now available for the Agent tool:"
|
|
)
|
|
parts.append(heading + "\n" + "\n".join(added))
|
|
if removed:
|
|
parts.append(
|
|
"The following agent types are no longer available:\n"
|
|
+ "\n".join(f"- {name}" for name in removed)
|
|
)
|
|
if attachment.get("isInitial") and attachment.get("showConcurrencyNote"):
|
|
parts.append(
|
|
"Launch multiple agents concurrently whenever possible, to "
|
|
"maximize performance; to do that, use a single message with "
|
|
"multiple tool uses."
|
|
)
|
|
return "<system-reminder>\n" + "\n\n".join(parts) + "\n</system-reminder>"
|
|
|
|
if attachment_type == "mcp_instructions_delta":
|
|
added = [str(block) for block in (attachment.get("addedBlocks") or [])]
|
|
removed = [str(name) for name in (attachment.get("removedNames") or [])]
|
|
parts: list[str] = []
|
|
if added:
|
|
parts.append(
|
|
"# MCP Server Instructions\n\n"
|
|
"The following MCP servers have provided instructions for how "
|
|
"to use their tools and resources:\n\n"
|
|
+ "\n\n".join(added)
|
|
)
|
|
if removed:
|
|
parts.append(
|
|
"The following MCP server instructions no longer apply:\n"
|
|
+ "\n".join(removed)
|
|
)
|
|
return "<system-reminder>\n" + "\n\n".join(parts) + "\n</system-reminder>"
|
|
|
|
if attachment_type == "todo_reminder":
|
|
todo_items = [
|
|
f"{index + 1}. [{todo.get('status')}] {todo.get('content')}"
|
|
for index, todo in enumerate(attachment.get("content") or [])
|
|
if isinstance(todo, Mapping)
|
|
]
|
|
message = (
|
|
"The TodoWrite tool hasn't been used recently. If you're working "
|
|
"on tasks that would benefit from tracking progress, consider "
|
|
"using the TodoWrite tool to track progress. Also consider "
|
|
"cleaning up the todo list if has become stale and no longer "
|
|
"matches what you are working on. Only use it if it's relevant "
|
|
"to the current work. This is just a gentle reminder - ignore if "
|
|
"not applicable. Make sure that you NEVER mention this reminder "
|
|
"to the user\n"
|
|
)
|
|
if todo_items:
|
|
message += "\n\nHere are the existing contents of your todo list:\n\n["
|
|
message += "\n".join(todo_items) + "]"
|
|
return "<system-reminder>\n" + message + "\n</system-reminder>"
|
|
|
|
if attachment_type == "task_reminder":
|
|
task_items = [
|
|
f"#{task.get('id')}. [{task.get('status')}] {task.get('subject')}"
|
|
for task in (attachment.get("content") or [])
|
|
if isinstance(task, Mapping)
|
|
]
|
|
message = (
|
|
"The task tools haven't been used recently. If you're working on "
|
|
"tasks that would benefit from tracking progress, consider using "
|
|
"TaskCreate to add new tasks and TaskUpdate to update task status. "
|
|
"Only use these if relevant to the current work. This is just a "
|
|
"gentle reminder - ignore if not applicable. Make sure that you "
|
|
"NEVER mention this reminder to the user\n"
|
|
)
|
|
if task_items:
|
|
message += "\n\nHere are the existing tasks:\n\n" + "\n".join(task_items)
|
|
return "<system-reminder>\n" + message + "\n</system-reminder>"
|
|
|
|
if attachment_type == "diagnostics":
|
|
lines = ["New diagnostics were reported after recent file changes:"]
|
|
for file in attachment.get("files") or []:
|
|
if isinstance(file, Mapping):
|
|
uri = str(file.get("uri") or "")
|
|
diagnostics = file.get("diagnostics") or []
|
|
else:
|
|
uri = str(getattr(file, "uri", "") or "")
|
|
diagnostics = getattr(file, "diagnostics", []) or []
|
|
if not diagnostics:
|
|
continue
|
|
lines.append(f"\n{uri}:")
|
|
for diagnostic in diagnostics:
|
|
if isinstance(diagnostic, Mapping):
|
|
severity = str(diagnostic.get("severity") or "Error")
|
|
message = str(diagnostic.get("message") or "")
|
|
range_value = diagnostic.get("range") or {}
|
|
source = diagnostic.get("source")
|
|
code = diagnostic.get("code")
|
|
else:
|
|
severity = str(getattr(diagnostic, "severity", "Error"))
|
|
message = str(getattr(diagnostic, "message", ""))
|
|
range_value = getattr(diagnostic, "range", {}) or {}
|
|
source = getattr(diagnostic, "source", None)
|
|
code = getattr(diagnostic, "code", None)
|
|
start = range_value.get("start", {}) if isinstance(range_value, Mapping) else {}
|
|
location = f"{int(start.get('line', 0)) + 1}:{int(start.get('character', 0)) + 1}"
|
|
suffix = (f" [{code}]" if code else "") + (f" ({source})" if source else "")
|
|
lines.append(f"- {severity} at {location}: {message}{suffix}")
|
|
return (
|
|
"<system-reminder>\n"
|
|
+ "\n".join(lines)
|
|
+ "\nUse the diagnostics to guide fixes when relevant. Do not mention this reminder directly."
|
|
+ "\n</system-reminder>"
|
|
)
|
|
|
|
if attachment_type == "skill_listing":
|
|
content = str(attachment.get("content") or "").strip()
|
|
if not content:
|
|
return "<system-reminder>No new skills are available.</system-reminder>"
|
|
heading = "Available skills:" if attachment.get("isInitial") else "Additional skills are now available:"
|
|
return (
|
|
"<system-reminder>\n"
|
|
f"{heading}\n{content}\n\n"
|
|
"If one of these skills matches the task, call the Skill tool "
|
|
"with that skill name before applying it."
|
|
"\n</system-reminder>"
|
|
)
|
|
|
|
if attachment_type == "skill_discovery":
|
|
skills = [s for s in (attachment.get("skills") or []) if isinstance(s, Mapping)]
|
|
if not skills:
|
|
return "<system-reminder>No relevant skills were discovered.</system-reminder>"
|
|
lines = ["Skills relevant to your task:"]
|
|
for skill in skills:
|
|
name = str(skill.get("name") or "")
|
|
desc = str(skill.get("description") or "")
|
|
lines.append(f"- {name}: {desc}" if desc else f"- {name}")
|
|
lines.append("Use the Skill tool to load full instructions before applying a skill.")
|
|
return "<system-reminder>\n" + "\n".join(lines) + "\n</system-reminder>"
|
|
|
|
if attachment_type == "dynamic_skill":
|
|
skill_names = [str(name) for name in (attachment.get("skillNames") or [])]
|
|
skill_dir = str(attachment.get("displayPath") or attachment.get("skillDir") or "")
|
|
lines = [
|
|
f"Skills were found near the files you touched: {skill_dir}",
|
|
*[f"- {name}" for name in skill_names],
|
|
"Use the Skill tool to load one if it is relevant.",
|
|
]
|
|
return "<system-reminder>\n" + "\n".join(lines) + "\n</system-reminder>"
|
|
|
|
if attachment_type == "skill_state":
|
|
return (
|
|
"<system-reminder>"
|
|
"Skill protocol state restored after compaction."
|
|
"</system-reminder>"
|
|
)
|
|
|
|
if attachment_type == "invoked_skills":
|
|
skills = [s for s in (attachment.get("skills") or []) if isinstance(s, Mapping)]
|
|
if not skills:
|
|
return ""
|
|
skills_content = "\n\n---\n\n".join(
|
|
f"### Skill: {skill.get('name')}\nPath: {skill.get('path')}\n\n{skill.get('content') or ''}"
|
|
for skill in skills
|
|
)
|
|
return (
|
|
"<system-reminder>\n"
|
|
"The following skills were invoked in this session. Continue to "
|
|
f"follow these guidelines:\n\n{skills_content}"
|
|
"\n</system-reminder>"
|
|
)
|
|
|
|
if attachment_type == "invoked_skill_content":
|
|
name = str(attachment.get("name") or "")
|
|
content = str(attachment.get("content") or "")
|
|
return (
|
|
"<system-reminder>\n"
|
|
f"Skill `{name}` has been loaded. Follow these instructions for "
|
|
f"the current task when relevant.\n\n{content}"
|
|
"\n</system-reminder>"
|
|
)
|
|
|
|
if attachment_type == "plan_file_reference":
|
|
return (
|
|
"<system-reminder>\n"
|
|
f"A plan file exists from plan mode at: {attachment.get('planFilePath')}\n\n"
|
|
f"Plan contents:\n\n{attachment.get('planContent')}\n\n"
|
|
"If this plan is relevant to the current work and not already "
|
|
"complete, continue working on it."
|
|
"\n</system-reminder>"
|
|
)
|
|
|
|
if attachment_type == "plan_mode":
|
|
mode = "subagent" if attachment.get("isSubAgent") else "main agent"
|
|
plan_path = attachment.get("planFilePath")
|
|
if attachment.get("reminderType") == "compact":
|
|
body = (
|
|
f"You are still in plan mode for the {mode}. Continue to avoid "
|
|
"making changes until plan mode has been exited."
|
|
)
|
|
else:
|
|
body = (
|
|
f"You are in plan mode for the {mode}. Your task is to explore, "
|
|
"ask clarifying questions if needed, and write an implementation "
|
|
"plan before making changes.\n\n"
|
|
"Rules:\n"
|
|
"- Do NOT make code or file changes except editing the plan file.\n"
|
|
"- Use read-only tools to inspect the codebase.\n"
|
|
"- Write the plan to the plan file.\n"
|
|
"- When ready for approval, use ExitPlanMode.\n"
|
|
)
|
|
if plan_path:
|
|
body += f"\nPlan file: {plan_path}"
|
|
return (
|
|
"<system-reminder>\n"
|
|
+ body
|
|
+ "\n</system-reminder>"
|
|
)
|
|
|
|
if attachment_type == "plan_mode_reentry":
|
|
return (
|
|
"<system-reminder>\n"
|
|
"You have re-entered plan mode. Continue exploring and updating the "
|
|
"plan file, but do not make implementation changes until the plan is approved."
|
|
"\n</system-reminder>"
|
|
)
|
|
|
|
if attachment_type == "plan_mode_exit":
|
|
return (
|
|
"<system-reminder>\n"
|
|
"Plan mode has been exited. You may now implement the approved plan."
|
|
"\n</system-reminder>"
|
|
)
|
|
|
|
if attachment_type == "verify_plan_reminder":
|
|
return (
|
|
"<system-reminder>\n"
|
|
"Before continuing implementation, verify that your next steps still "
|
|
"match the approved plan. If the plan is stale or wrong, re-enter "
|
|
"plan mode instead of silently diverging."
|
|
"\n</system-reminder>"
|
|
)
|
|
|
|
if attachment_type == "task_status":
|
|
status = str(attachment.get("status") or "")
|
|
display_status = "stopped" if status == "killed" else status
|
|
description = str(attachment.get("description") or "")
|
|
task_id = str(attachment.get("taskId") or "")
|
|
if status == "killed":
|
|
text = f'Task "{description}" ({task_id}) was stopped by the user.'
|
|
elif status == "running":
|
|
parts = [f'Background agent "{description}" ({task_id}) is still running.']
|
|
if attachment.get("deltaSummary"):
|
|
parts.append(f"Progress: {attachment.get('deltaSummary')}")
|
|
output_path = attachment.get("outputFilePath")
|
|
if output_path:
|
|
parts.append(
|
|
"Do NOT spawn a duplicate. You will be notified when it "
|
|
f"completes. You can read partial output at {output_path}."
|
|
)
|
|
else:
|
|
parts.append("Do NOT spawn a duplicate. You will be notified when it completes.")
|
|
text = " ".join(parts)
|
|
else:
|
|
parts = [
|
|
f"Task {task_id}",
|
|
f"(type: {attachment.get('taskType')})",
|
|
f"(status: {display_status})",
|
|
f"(description: {description})",
|
|
]
|
|
if attachment.get("deltaSummary"):
|
|
parts.append(f"Delta: {attachment.get('deltaSummary')}")
|
|
if attachment.get("outputFilePath"):
|
|
parts.append(f"Read the output file to retrieve the result: {attachment.get('outputFilePath')}")
|
|
text = " ".join(parts)
|
|
return "<system-reminder>\n" + text + "\n</system-reminder>"
|
|
|
|
return f"<system-reminder>{json.dumps(dict(attachment), ensure_ascii=False, default=str)}</system-reminder>"
|
|
|
|
|
|
def get_deferred_tools_delta_attachment(
|
|
tools: Sequence[Any],
|
|
model: str | None,
|
|
existing_messages: Sequence[Mapping[str, Any]] | None = None,
|
|
*,
|
|
scan_context: Mapping[str, Any] | None = None,
|
|
) -> list[dict[str, Any]]:
|
|
"""Provider-neutral OpenSpace ``getDeferredToolsDeltaAttachment`` equivalent."""
|
|
|
|
del model # OS intentionally does not gate this on Anthropic tool_reference.
|
|
pool_names = {
|
|
str(getattr(tool, "name", ""))
|
|
for tool in tools
|
|
if getattr(tool, "name", None)
|
|
}
|
|
current = {
|
|
str(getattr(tool, "name", ""))
|
|
for tool in tools
|
|
if getattr(tool, "is_deferred", False) and getattr(tool, "name", None)
|
|
}
|
|
|
|
announced = _scan_announced_names(
|
|
existing_messages or [],
|
|
"deferred_tools_delta",
|
|
added_key="addedNames",
|
|
removed_key="removedNames",
|
|
)
|
|
added = sorted(current - announced)
|
|
removed = sorted(
|
|
name for name in announced
|
|
if name not in current and name not in pool_names
|
|
)
|
|
if not added and not removed:
|
|
return []
|
|
|
|
attachment: dict[str, Any] = {
|
|
"type": "deferred_tools_delta",
|
|
"addedNames": added,
|
|
"addedLines": added,
|
|
"removedNames": removed,
|
|
}
|
|
if scan_context:
|
|
attachment["scanContext"] = dict(scan_context)
|
|
return [attachment]
|
|
|
|
|
|
def get_agent_listing_delta_attachment(
|
|
context: Any,
|
|
messages: Sequence[Mapping[str, Any]] | None,
|
|
) -> list[dict[str, Any]]:
|
|
from openspace.agents.agent_tool_utils import get_agent_listing_delta_attachment as impl
|
|
|
|
return impl(context, messages)
|
|
|
|
|
|
def get_mcp_instructions_delta_attachment(
|
|
mcp_clients: Iterable[Any] | None,
|
|
tools: Sequence[Any],
|
|
model: str | None,
|
|
messages: Sequence[Mapping[str, Any]] | None,
|
|
) -> list[dict[str, Any]]:
|
|
"""OpenSpace ``getMcpInstructionsDeltaAttachment`` for OS MCP runtime state."""
|
|
|
|
del model # OS uses provider-neutral text attachments.
|
|
sources = _collect_mcp_instruction_sources(mcp_clients, tools)
|
|
connected_names = {source["name"] for source in sources}
|
|
blocks_by_name = {
|
|
source["name"]: f"## {source['name']}\n{source['instructions']}"
|
|
for source in sources
|
|
if source.get("instructions")
|
|
}
|
|
|
|
if (
|
|
CLAUDE_IN_CHROME_MCP_SERVER_NAME in connected_names
|
|
and _is_tool_search_tool_available(tools)
|
|
):
|
|
existing = blocks_by_name.get(CLAUDE_IN_CHROME_MCP_SERVER_NAME)
|
|
chrome_block = (
|
|
f"## {CLAUDE_IN_CHROME_MCP_SERVER_NAME}\n"
|
|
f"{CHROME_TOOL_SEARCH_INSTRUCTIONS}"
|
|
)
|
|
blocks_by_name[CLAUDE_IN_CHROME_MCP_SERVER_NAME] = (
|
|
f"{existing}\n\n{CHROME_TOOL_SEARCH_INSTRUCTIONS}"
|
|
if existing
|
|
else chrome_block
|
|
)
|
|
|
|
announced = _scan_announced_names(
|
|
messages or [],
|
|
"mcp_instructions_delta",
|
|
added_key="addedNames",
|
|
removed_key="removedNames",
|
|
)
|
|
added = sorted(
|
|
({"name": name, "block": block} for name, block in blocks_by_name.items() if name not in announced),
|
|
key=lambda item: item["name"],
|
|
)
|
|
removed = sorted(name for name in announced if name not in connected_names)
|
|
if not added and not removed:
|
|
return []
|
|
return [
|
|
{
|
|
"type": "mcp_instructions_delta",
|
|
"addedNames": [item["name"] for item in added],
|
|
"addedBlocks": [item["block"] for item in added],
|
|
"removedNames": removed,
|
|
}
|
|
]
|
|
|
|
|
|
def get_todo_reminder_turn_counts(
|
|
messages: Sequence[Mapping[str, Any]],
|
|
) -> dict[str, int]:
|
|
last_todo_seen = False
|
|
last_reminder_seen = False
|
|
turns_since_write = 0
|
|
turns_since_reminder = 0
|
|
|
|
for message in reversed(messages):
|
|
if message.get("role") == "assistant":
|
|
if _is_thinking_only_message(message):
|
|
continue
|
|
if not last_todo_seen and _assistant_used_tool(message, {"TodoWrite", "todo_write"}):
|
|
last_todo_seen = True
|
|
if not last_todo_seen:
|
|
turns_since_write += 1
|
|
if not last_reminder_seen:
|
|
turns_since_reminder += 1
|
|
elif not last_reminder_seen and _attachment_type(message) == "todo_reminder":
|
|
last_reminder_seen = True
|
|
|
|
if last_todo_seen and last_reminder_seen:
|
|
break
|
|
|
|
return {
|
|
"turnsSinceLastTodoWrite": turns_since_write,
|
|
"turnsSinceLastReminder": turns_since_reminder,
|
|
}
|
|
|
|
|
|
def get_todo_reminder_attachments(
|
|
messages: Sequence[Mapping[str, Any]] | None,
|
|
context: Any,
|
|
) -> list[dict[str, Any]]:
|
|
try:
|
|
from openspace.services.runtime_support.settings import get_setting
|
|
|
|
cwd = getattr(context, "cwd", None)
|
|
if not bool(get_setting("todoFeatureEnabled", True, cwd=cwd)):
|
|
return []
|
|
if not bool(get_setting("attachments.todoReminderEnabled", True, cwd=cwd)):
|
|
return []
|
|
except Exception:
|
|
pass
|
|
|
|
tools = list(getattr(context, "tools", []) or [])
|
|
if not any(_tool_matches_name(tool, "todo_write") or _tool_matches_name(tool, "TodoWrite") for tool in tools):
|
|
return []
|
|
if any(
|
|
_tool_matches_name(tool, "brief")
|
|
or _tool_matches_name(tool, "Brief")
|
|
or _tool_matches_name(tool, "SendUserMessage")
|
|
for tool in tools
|
|
):
|
|
return []
|
|
if not messages:
|
|
return []
|
|
|
|
counts = get_todo_reminder_turn_counts(messages)
|
|
if (
|
|
counts["turnsSinceLastTodoWrite"] < TODO_REMINDER_TURNS_SINCE_WRITE
|
|
or counts["turnsSinceLastReminder"] < TODO_REMINDER_TURNS_BETWEEN_REMINDERS
|
|
):
|
|
return []
|
|
|
|
try:
|
|
from openspace.tools.todo_tool import get_todo_key
|
|
|
|
todo_key = get_todo_key(context)
|
|
except Exception:
|
|
todo_key = str(getattr(context, "agent_id", None) or getattr(context, "session_id", None) or "primary")
|
|
state = getattr(context, "todo_state", {}) or {}
|
|
todos = list(state.get(todo_key, []) or [])
|
|
return [
|
|
{
|
|
"type": "todo_reminder",
|
|
"content": todos,
|
|
"itemCount": len(todos),
|
|
}
|
|
]
|
|
|
|
|
|
def get_task_reminder_attachments(
|
|
messages: Sequence[Mapping[str, Any]] | None,
|
|
context: Any,
|
|
) -> list[dict[str, Any]]:
|
|
# OpenSpace enables this only for its write-capable task-list tools. OpenSpace's
|
|
# current TaskGet/List tools are the async-agent registry, not todo tools.
|
|
del messages, context
|
|
return []
|
|
|
|
|
|
async def get_diagnostic_attachments(context: Any) -> list[dict[str, Any]]:
|
|
tools = list(getattr(context, "tools", []) or [])
|
|
if not any(_tool_matches_name(tool, "bash") or _tool_matches_name(tool, "Bash") for tool in tools):
|
|
return []
|
|
|
|
files = []
|
|
tracker = getattr(context, "diagnostic_tracker", None)
|
|
if tracker is not None:
|
|
try:
|
|
files.extend(await tracker.get_new_diagnostics())
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
from openspace.services.lsp.diagnostic_registry import check_for_lsp_diagnostics, clear_all_lsp_diagnostics
|
|
|
|
for diagnostic_set in check_for_lsp_diagnostics():
|
|
files.extend(diagnostic_set.get("files", [])) # type: ignore[arg-type]
|
|
if files:
|
|
clear_all_lsp_diagnostics()
|
|
except Exception:
|
|
pass
|
|
|
|
if not files:
|
|
return []
|
|
return [{"type": "diagnostics", "files": files, "isNew": True}]
|
|
|
|
|
|
def get_turn_attachment_messages(
|
|
context: Any,
|
|
messages: Sequence[Mapping[str, Any]],
|
|
*,
|
|
model: str | None = None,
|
|
) -> list[dict[str, Any]]:
|
|
attachments: list[dict[str, Any]] = []
|
|
attachments.extend(
|
|
get_deferred_tools_delta_attachment(
|
|
getattr(context, "all_tools", []) or getattr(context, "tools", []) or [],
|
|
model or getattr(context, "model", None),
|
|
messages,
|
|
scan_context={"callSite": "attachments_main"},
|
|
)
|
|
)
|
|
attachments.extend(get_agent_listing_delta_attachment(context, messages))
|
|
attachments.extend(
|
|
get_mcp_instructions_delta_attachment(
|
|
getattr(context, "mcp_clients", None),
|
|
getattr(context, "all_tools", []) or getattr(context, "tools", []) or [],
|
|
model or getattr(context, "model", None),
|
|
messages,
|
|
)
|
|
)
|
|
attachments.extend(get_todo_reminder_attachments(messages, context))
|
|
attachments.extend(get_plan_mode_attachments(context, messages))
|
|
plan_exit = get_plan_mode_exit_attachment(context)
|
|
if plan_exit is not None:
|
|
attachments.append(plan_exit)
|
|
verify_plan = get_verify_plan_reminder_attachment(context, messages)
|
|
if verify_plan is not None:
|
|
attachments.append(verify_plan)
|
|
return [create_attachment_message(attachment) for attachment in attachments]
|
|
|
|
|
|
async def get_turn_attachment_messages_async(
|
|
context: Any,
|
|
messages: Sequence[Mapping[str, Any]],
|
|
*,
|
|
model: str | None = None,
|
|
) -> list[dict[str, Any]]:
|
|
base = get_turn_attachment_messages(context, messages, model=model)
|
|
diagnostics = await get_diagnostic_attachments(context)
|
|
if not diagnostics:
|
|
return base
|
|
return base + [create_attachment_message(attachment) for attachment in diagnostics]
|
|
|
|
|
|
async def create_post_compact_attachments(
|
|
context: Any,
|
|
*,
|
|
effective_model: str | None,
|
|
messages_to_keep: Sequence[Mapping[str, Any]] | None,
|
|
full_compact: bool,
|
|
pre_compact_read_file_state: Mapping[str, Any] | None = None,
|
|
pre_compact_nested_memory_source_paths: Iterable[str] | None = None,
|
|
) -> list[dict[str, Any]]:
|
|
"""Rebuild OpenSpace post-compact attachments in the same broad order."""
|
|
|
|
read_state = getattr(context, "read_file_state", None)
|
|
snapshot = dict(pre_compact_read_file_state or (read_state if isinstance(read_state, dict) else {}))
|
|
source_paths = {
|
|
str(path)
|
|
for path in (pre_compact_nested_memory_source_paths or getattr(context, "nested_memory_source_paths", set()) or set())
|
|
if str(path)
|
|
}
|
|
source_paths.update(str(path) for path in snapshot.keys() if str(path))
|
|
|
|
if isinstance(read_state, dict):
|
|
read_state.clear()
|
|
loaded = getattr(context, "loaded_nested_memory_paths", None)
|
|
if isinstance(loaded, set):
|
|
loaded.clear()
|
|
triggers = getattr(context, "nested_memory_triggers", None)
|
|
if isinstance(triggers, set):
|
|
triggers.clear()
|
|
|
|
preserved = list(messages_to_keep or [])
|
|
attachments: list[dict[str, Any]] = []
|
|
attachments.extend(
|
|
await create_post_compact_file_attachments(
|
|
snapshot,
|
|
context,
|
|
POST_COMPACT_MAX_FILES_TO_RESTORE,
|
|
preserved_messages=preserved,
|
|
)
|
|
)
|
|
attachments.extend(await create_async_agent_attachments_if_needed(context))
|
|
|
|
plan_attachment = create_plan_attachment_if_needed(getattr(context, "agent_id", None), context=context)
|
|
if plan_attachment is not None:
|
|
attachments.append(plan_attachment)
|
|
|
|
plan_mode_attachment = create_plan_mode_attachment_if_needed(context)
|
|
if plan_mode_attachment is not None:
|
|
attachments.append(plan_mode_attachment)
|
|
|
|
skill_state = create_skill_state_attachment_if_needed(context)
|
|
if skill_state is not None:
|
|
attachments.append(skill_state)
|
|
invoked_skills = create_invoked_skills_attachment_if_needed(context)
|
|
if invoked_skills is not None:
|
|
attachments.append(invoked_skills)
|
|
attachments.extend(create_dynamic_skill_attachments_if_needed(context, delta_history=[] if full_compact else preserved))
|
|
|
|
delta_history = [] if full_compact else preserved
|
|
for attachment in get_deferred_tools_delta_attachment(
|
|
getattr(context, "all_tools", []) or getattr(context, "tools", []) or [],
|
|
effective_model,
|
|
delta_history,
|
|
scan_context={"callSite": "compact_full" if full_compact else "compact_partial"},
|
|
):
|
|
attachments.append(create_attachment_message(attachment))
|
|
for attachment in get_agent_listing_delta_attachment(context, delta_history):
|
|
attachments.append(create_attachment_message(attachment))
|
|
for attachment in get_mcp_instructions_delta_attachment(
|
|
getattr(context, "mcp_clients", None),
|
|
getattr(context, "all_tools", []) or getattr(context, "tools", []) or [],
|
|
effective_model,
|
|
delta_history,
|
|
):
|
|
attachments.append(create_attachment_message(attachment))
|
|
|
|
for attachment in get_post_compact_nested_memory_attachments(context, source_paths):
|
|
attachments.append(create_attachment_message(attachment))
|
|
if isinstance(triggers, set):
|
|
triggers.clear()
|
|
return attachments
|
|
|
|
|
|
async def create_post_compact_file_attachments(
|
|
read_file_state: Mapping[str, Any],
|
|
context: Any,
|
|
max_files: int = POST_COMPACT_MAX_FILES_TO_RESTORE,
|
|
*,
|
|
preserved_messages: Sequence[Mapping[str, Any]] | None = None,
|
|
) -> list[dict[str, Any]]:
|
|
preserved_read_paths = _collect_read_tool_file_paths(preserved_messages or [])
|
|
recent_files = sorted(
|
|
(
|
|
(str(path), _read_state_timestamp(entry))
|
|
for path, entry in read_file_state.items()
|
|
if str(path)
|
|
and not _should_exclude_from_post_compact_restore(str(path), context)
|
|
and str(Path(str(path)).expanduser().resolve()) not in preserved_read_paths
|
|
),
|
|
key=lambda item: item[1],
|
|
reverse=True,
|
|
)[:max_files]
|
|
|
|
used_tokens = 0
|
|
messages: list[dict[str, Any]] = []
|
|
for filename, _timestamp in recent_files:
|
|
attachment = await generate_file_attachment(filename, context, mode="compact")
|
|
if attachment is None:
|
|
continue
|
|
message = create_attachment_message(attachment)
|
|
tokens = _rough_token_count(json.dumps(message, ensure_ascii=False, default=str))
|
|
if used_tokens + tokens > POST_COMPACT_TOKEN_BUDGET:
|
|
continue
|
|
used_tokens += tokens
|
|
messages.append(message)
|
|
return messages
|
|
|
|
|
|
async def generate_file_attachment(
|
|
filename: str,
|
|
context: Any,
|
|
*,
|
|
mode: str = "compact",
|
|
) -> dict[str, Any] | None:
|
|
path = Path(filename).expanduser()
|
|
try:
|
|
resolved = path.resolve()
|
|
except OSError:
|
|
return None
|
|
if not resolved.exists() or not resolved.is_file():
|
|
return None
|
|
if _is_read_denied(str(resolved), context):
|
|
return None
|
|
|
|
from openspace.grounding.backends.shell.file_tools import ReadFileTool
|
|
from openspace.tool_runtime.pipeline.execution import (
|
|
run_tool_use,
|
|
tool_call_result_to_tool_result,
|
|
)
|
|
|
|
reader = ReadFileTool()
|
|
reader.set_context(context)
|
|
tool_use_id = f"compact_read_{abs(hash(str(resolved))) & 0xffffffff:x}"
|
|
pipeline_result = await run_tool_use(
|
|
{
|
|
"id": tool_use_id,
|
|
"type": "function",
|
|
"function": {
|
|
"name": reader.name,
|
|
"arguments": {
|
|
"file_path": str(resolved),
|
|
"offset": 1,
|
|
"limit": None,
|
|
},
|
|
},
|
|
},
|
|
{reader.name: reader},
|
|
context,
|
|
assistant_message={
|
|
"role": "assistant",
|
|
"content": "",
|
|
"tool_calls": [
|
|
{
|
|
"id": tool_use_id,
|
|
"type": "function",
|
|
"function": {
|
|
"name": reader.name,
|
|
"arguments": {
|
|
"file_path": str(resolved),
|
|
"offset": 1,
|
|
"limit": None,
|
|
},
|
|
},
|
|
}
|
|
],
|
|
"_meta": {
|
|
"type": "synthetic_tool_call",
|
|
"source": "post_compact_file_attachment",
|
|
},
|
|
},
|
|
)
|
|
result = tool_call_result_to_tool_result(pipeline_result)
|
|
if result.status != ToolStatus.SUCCESS:
|
|
content = str(result.content or result.error or "")
|
|
if mode == "compact" and ("exceeds" in content or "too large" in content.lower()):
|
|
return {
|
|
"type": "compact_file_reference",
|
|
"filename": str(resolved),
|
|
"displayPath": _display_path(resolved, context),
|
|
}
|
|
return None
|
|
|
|
content = result.content
|
|
tokens = _rough_token_count(_content_to_text(content))
|
|
if mode == "compact" and tokens > POST_COMPACT_MAX_TOKENS_PER_FILE:
|
|
_drop_read_file_state(context, str(resolved))
|
|
return {
|
|
"type": "compact_file_reference",
|
|
"filename": str(resolved),
|
|
"displayPath": _display_path(resolved, context),
|
|
}
|
|
|
|
return {
|
|
"type": "file",
|
|
"filename": str(resolved),
|
|
"content": content,
|
|
"metadata": dict(result.metadata or {}),
|
|
"displayPath": _display_path(resolved, context),
|
|
}
|
|
|
|
|
|
def create_plan_attachment_if_needed(agent_id: str | None = None, *, context: Any | None = None) -> dict[str, Any] | None:
|
|
del agent_id
|
|
plan_path = getattr(context, "plan_file_path", None) if context is not None else None
|
|
if not plan_path:
|
|
return None
|
|
try:
|
|
content = Path(str(plan_path)).expanduser().read_text(encoding="utf-8")
|
|
except OSError:
|
|
return None
|
|
if not content.strip():
|
|
return None
|
|
return create_attachment_message(
|
|
{
|
|
"type": "plan_file_reference",
|
|
"planFilePath": str(Path(str(plan_path)).expanduser()),
|
|
"planContent": content,
|
|
}
|
|
)
|
|
|
|
|
|
def create_plan_mode_attachment_if_needed(context: Any) -> dict[str, Any] | None:
|
|
if str(getattr(context, "permission_mode", "") or "").lower() != "plan":
|
|
return None
|
|
return create_attachment_message(
|
|
{
|
|
"type": "plan_mode",
|
|
"reminderType": "full",
|
|
"isSubAgent": bool(getattr(context, "agent_id", None) and getattr(context, "agent_id", None) != "primary"),
|
|
"planFilePath": getattr(context, "plan_file_path", None),
|
|
"planExists": bool(getattr(context, "plan_file_path", None)),
|
|
}
|
|
)
|
|
|
|
|
|
def get_plan_mode_attachments(
|
|
context: Any,
|
|
messages: Sequence[Mapping[str, Any]],
|
|
) -> list[dict[str, Any]]:
|
|
if str(getattr(context, "permission_mode", "") or "").lower() != "plan":
|
|
return []
|
|
existing_count = sum(
|
|
1
|
|
for message in messages
|
|
if (
|
|
isinstance(message, Mapping)
|
|
and isinstance(message.get("_meta"), Mapping)
|
|
and message["_meta"].get("attachment_type")
|
|
in {"plan_mode", "plan_mode_reentry"}
|
|
)
|
|
)
|
|
turns_since = _turns_since_attachment(messages, {"plan_mode", "plan_mode_reentry"})
|
|
if existing_count and turns_since < PLAN_MODE_ATTACHMENT_TURNS_BETWEEN_ATTACHMENTS:
|
|
return []
|
|
reminder_type = (
|
|
"full"
|
|
if existing_count % PLAN_MODE_FULL_REMINDER_EVERY_N_ATTACHMENTS == 0
|
|
else "compact"
|
|
)
|
|
return [
|
|
{
|
|
"type": "plan_mode",
|
|
"reminderType": reminder_type,
|
|
"isSubAgent": bool(
|
|
getattr(context, "agent_id", None)
|
|
and getattr(context, "agent_id", None) != "primary"
|
|
),
|
|
"planFilePath": getattr(context, "plan_file_path", None),
|
|
"planExists": bool(getattr(context, "plan_file_path", None)),
|
|
}
|
|
]
|
|
|
|
|
|
def get_plan_mode_exit_attachment(context: Any) -> dict[str, Any] | None:
|
|
if not bool(getattr(context, "plan_mode_exit_pending", False)):
|
|
return None
|
|
setattr(context, "plan_mode_exit_pending", False)
|
|
return {"type": "plan_mode_exit"}
|
|
|
|
|
|
def get_verify_plan_reminder_attachment(
|
|
context: Any,
|
|
messages: Sequence[Mapping[str, Any]],
|
|
) -> dict[str, Any] | None:
|
|
if str(getattr(context, "permission_mode", "") or "").lower() == "plan":
|
|
return None
|
|
if not bool(getattr(context, "plan_mode_exited_in_session", False)):
|
|
return None
|
|
if _turns_since_attachment(messages, {"plan_mode_exit", "verify_plan_reminder"}) < 10:
|
|
return None
|
|
return {"type": "verify_plan_reminder"}
|
|
|
|
|
|
def _turns_since_attachment(
|
|
messages: Sequence[Mapping[str, Any]],
|
|
attachment_types: set[str],
|
|
) -> int:
|
|
turns = 0
|
|
for message in reversed(messages):
|
|
if (
|
|
isinstance(message, Mapping)
|
|
and isinstance(message.get("_meta"), Mapping)
|
|
and message["_meta"].get("attachment_type") in attachment_types
|
|
):
|
|
return turns
|
|
if isinstance(message, Mapping) and message.get("role") == "assistant":
|
|
turns += 1
|
|
return 10**9
|
|
|
|
|
|
def create_invoked_skills_attachment_if_needed(context: Any) -> dict[str, Any] | None:
|
|
records_by_agent = getattr(context, "invoked_skills_by_agent", None)
|
|
if not isinstance(records_by_agent, Mapping):
|
|
return None
|
|
|
|
current_agent_id = str(getattr(context, "agent_id", None) or "primary")
|
|
records = list(records_by_agent.get(current_agent_id, ()) or ())
|
|
if not records:
|
|
return None
|
|
|
|
max_chars_per_skill = POST_COMPACT_MAX_TOKENS_PER_SKILL * 4
|
|
total_budget_chars = POST_COMPACT_SKILLS_TOKEN_BUDGET * 4
|
|
used_chars = 0
|
|
skills: list[dict[str, Any]] = []
|
|
records.sort(key=lambda record: getattr(record, "invoked_at", 0.0), reverse=True)
|
|
for record in records:
|
|
content = str(getattr(record, "content", "") or "")
|
|
if not content:
|
|
continue
|
|
truncated = False
|
|
if len(content) > max_chars_per_skill:
|
|
content = (
|
|
content[:max_chars_per_skill]
|
|
+ "\n\n[Skill content truncated during compaction.]"
|
|
)
|
|
truncated = True
|
|
if used_chars + len(content) > total_budget_chars:
|
|
continue
|
|
used_chars += len(content)
|
|
item: dict[str, Any] = {
|
|
"skill_id": str(getattr(record, "skill_id", "") or ""),
|
|
"name": str(getattr(record, "name", "") or ""),
|
|
"path": str(getattr(record, "path", "") or ""),
|
|
"content": content,
|
|
"agent_id": current_agent_id,
|
|
"allowed_tools": [
|
|
str(tool)
|
|
for tool in (getattr(record, "allowed_tools", None) or [])
|
|
if str(tool).strip()
|
|
],
|
|
"model": getattr(record, "model", None),
|
|
"effort": getattr(record, "effort", None),
|
|
"execution_context": str(getattr(record, "execution_context", None) or "inline"),
|
|
}
|
|
if truncated:
|
|
item["truncated"] = "true"
|
|
skills.append(item)
|
|
|
|
if not skills:
|
|
return None
|
|
return create_attachment_message({"type": "invoked_skills", "skills": skills})
|
|
|
|
|
|
def create_skill_state_attachment_if_needed(context: Any) -> dict[str, Any] | None:
|
|
sent_by_agent = {
|
|
str(agent): sorted(str(name) for name in names if str(name).strip())
|
|
for agent, names in (getattr(context, "sent_skill_names_by_agent", {}) or {}).items()
|
|
if names
|
|
}
|
|
discovered = sorted(
|
|
str(name)
|
|
for name in (getattr(context, "discovered_skill_names", set()) or set())
|
|
if str(name).strip()
|
|
)
|
|
sent_dynamic = sorted(
|
|
str(key)
|
|
for key in (getattr(context, "sent_dynamic_skill_keys", set()) or set())
|
|
if str(key).strip()
|
|
)
|
|
path_activated = sorted(
|
|
str(name)
|
|
for name in (getattr(context, "path_activated_skill_names", set()) or set())
|
|
if str(name).strip()
|
|
)
|
|
if not any([sent_by_agent, discovered, sent_dynamic, path_activated]):
|
|
return None
|
|
return create_attachment_message(
|
|
{
|
|
"type": "skill_state",
|
|
"sentSkillNamesByAgent": sent_by_agent,
|
|
"discoveredSkillNames": discovered,
|
|
"sentDynamicSkillKeys": sent_dynamic,
|
|
"pathActivatedSkillNames": path_activated,
|
|
}
|
|
)
|
|
|
|
|
|
def create_dynamic_skill_attachments_if_needed(
|
|
context: Any,
|
|
delta_history: Sequence[Mapping[str, Any]] | None = None,
|
|
) -> list[dict[str, Any]]:
|
|
"""Re-emit model-visible dynamic skill notices after compaction."""
|
|
|
|
already_sent = _scan_dynamic_skill_keys(delta_history or [])
|
|
messages: list[dict[str, Any]] = []
|
|
for key in sorted(str(k) for k in (getattr(context, "sent_dynamic_skill_keys", set()) or set()) if str(k).strip()):
|
|
if key in already_sent:
|
|
continue
|
|
skill_dir, names = _parse_dynamic_skill_key(key)
|
|
if not skill_dir or not names:
|
|
continue
|
|
messages.append(
|
|
create_attachment_message(
|
|
{
|
|
"type": "dynamic_skill",
|
|
"skillDir": skill_dir,
|
|
"displayPath": skill_dir,
|
|
"skillNames": names,
|
|
}
|
|
)
|
|
)
|
|
return messages
|
|
|
|
|
|
async def create_async_agent_attachments_if_needed(context: Any) -> list[dict[str, Any]]:
|
|
task_manager = getattr(context, "task_manager", None)
|
|
if task_manager is None or not hasattr(task_manager, "list_all"):
|
|
return []
|
|
try:
|
|
tasks = list(task_manager.list_all())
|
|
except Exception:
|
|
return []
|
|
|
|
messages: list[dict[str, Any]] = []
|
|
current_agent_id = str(getattr(context, "agent_id", "") or "")
|
|
for task in tasks:
|
|
if getattr(task, "retrieved", False):
|
|
continue
|
|
if str(getattr(task, "status", "")) == "pending":
|
|
continue
|
|
if str(getattr(task, "agent_id", "") or getattr(task, "id", "")) == current_agent_id:
|
|
continue
|
|
task_type = str(getattr(getattr(task, "type", None), "value", getattr(task, "type", "")))
|
|
if task_type not in {"local_agent", "in_process_teammate", "coordinator_worker"}:
|
|
continue
|
|
progress = getattr(task, "progress", None)
|
|
status = str(getattr(getattr(task, "status", None), "value", getattr(task, "status", "")))
|
|
delta = getattr(progress, "summary", None) if status == "running" else (getattr(task, "error", None) or _extract_task_result_text(getattr(task, "result", None)))
|
|
messages.append(
|
|
create_attachment_message(
|
|
{
|
|
"type": "task_status",
|
|
"taskId": str(getattr(task, "id", "")),
|
|
"taskType": task_type,
|
|
"description": str(getattr(task, "description", "") or ""),
|
|
"status": status,
|
|
"deltaSummary": delta,
|
|
"outputFilePath": str(getattr(task, "output_file", "") or ""),
|
|
}
|
|
)
|
|
)
|
|
return messages
|
|
|
|
|
|
def get_post_compact_nested_memory_attachments(
|
|
context: Any,
|
|
source_paths: Iterable[str],
|
|
) -> list[dict[str, Any]]:
|
|
from openspace.services.memory.openspace_md import (
|
|
get_nested_memory_attachments_for_file,
|
|
is_memory_file_path,
|
|
)
|
|
|
|
attachments: list[dict[str, Any]] = []
|
|
loaded = getattr(context, "loaded_nested_memory_paths", None)
|
|
if isinstance(loaded, set):
|
|
loaded.clear()
|
|
for path in sorted({str(path) for path in source_paths if str(path)}):
|
|
if is_memory_file_path(path):
|
|
continue
|
|
attachments.extend(
|
|
get_nested_memory_attachments_for_file(
|
|
path,
|
|
context,
|
|
ignore_read_file_state=True,
|
|
)
|
|
)
|
|
return attachments
|
|
|
|
|
|
def _scan_announced_names(
|
|
messages: Sequence[Mapping[str, Any]],
|
|
attachment_type: str,
|
|
*,
|
|
added_key: str,
|
|
removed_key: str,
|
|
) -> set[str]:
|
|
announced: set[str] = set()
|
|
for message in messages:
|
|
attachment = _attachment_payload(message)
|
|
if not isinstance(attachment, Mapping) or attachment.get("type") != attachment_type:
|
|
continue
|
|
added = attachment.get(added_key)
|
|
if isinstance(added, Sequence) and not isinstance(added, (str, bytes, bytearray)):
|
|
announced.update(str(name) for name in added if name)
|
|
removed = attachment.get(removed_key)
|
|
if isinstance(removed, Sequence) and not isinstance(removed, (str, bytes, bytearray)):
|
|
announced.difference_update(str(name) for name in removed if name)
|
|
return announced
|
|
|
|
|
|
def _attachment_payload(message: Mapping[str, Any]) -> Mapping[str, Any] | None:
|
|
meta = message.get("_meta")
|
|
if isinstance(meta, Mapping):
|
|
attachment = meta.get("attachment")
|
|
if isinstance(attachment, Mapping):
|
|
return attachment
|
|
attachment = message.get("attachment")
|
|
if isinstance(attachment, Mapping):
|
|
return attachment
|
|
return None
|
|
|
|
|
|
def _attachment_type(message: Mapping[str, Any]) -> str | None:
|
|
attachment = _attachment_payload(message)
|
|
return str(attachment.get("type")) if isinstance(attachment, Mapping) else None
|
|
|
|
|
|
def _collect_mcp_instruction_sources(
|
|
mcp_clients: Iterable[Any] | None,
|
|
tools: Sequence[Any],
|
|
) -> list[dict[str, str]]:
|
|
sources: dict[str, str] = {}
|
|
for client in mcp_clients or ():
|
|
name = _extract_mcp_name(client)
|
|
instructions = _extract_mcp_instructions(client)
|
|
if name:
|
|
sources[name] = instructions
|
|
|
|
for tool in tools:
|
|
runtime = getattr(tool, "runtime_info", None)
|
|
if runtime is None or getattr(runtime, "backend", None) != BackendType.MCP:
|
|
continue
|
|
name = str(getattr(runtime, "server_name", "") or "")
|
|
if not name:
|
|
continue
|
|
instructions = _extract_mcp_instructions_from_runtime(runtime)
|
|
if instructions:
|
|
sources.setdefault(name, instructions)
|
|
else:
|
|
sources.setdefault(name, "")
|
|
|
|
return [{"name": name, "instructions": instructions} for name, instructions in sorted(sources.items())]
|
|
|
|
|
|
def _extract_mcp_name(value: Any) -> str:
|
|
if isinstance(value, Mapping):
|
|
raw = value.get("name") or value.get("serverName") or value.get("server_name") or value.get("session_name") or value.get("sessionId") or value.get("session_id") or ""
|
|
else:
|
|
raw = (
|
|
getattr(value, "name", None)
|
|
or getattr(value, "server_name", None)
|
|
or getattr(value, "serverName", None)
|
|
or getattr(value, "session_name", None)
|
|
or getattr(value, "session_id", None)
|
|
or ""
|
|
)
|
|
name = str(raw)
|
|
if name.startswith("mcp-") and len(name) > 4:
|
|
return name[4:]
|
|
return name
|
|
|
|
|
|
def _extract_mcp_instructions(value: Any) -> str:
|
|
if isinstance(value, Mapping):
|
|
raw = value.get("instructions")
|
|
return str(raw).strip() if raw else ""
|
|
raw = getattr(value, "instructions", None)
|
|
if raw:
|
|
return str(raw).strip()
|
|
return _extract_instructions_from_session_info(getattr(value, "session_info", None))
|
|
|
|
|
|
def _extract_mcp_instructions_from_runtime(runtime: Any) -> str:
|
|
client = getattr(runtime, "grounding_client", None)
|
|
session_name = getattr(runtime, "session_name", None)
|
|
if client is None or not session_name:
|
|
return ""
|
|
try:
|
|
session = getattr(client, "_sessions", {}).get(session_name)
|
|
except Exception:
|
|
session = None
|
|
if session is None:
|
|
return ""
|
|
return _extract_instructions_from_session_info(getattr(session, "session_info", None))
|
|
|
|
|
|
def _extract_instructions_from_session_info(session_info: Any) -> str:
|
|
if isinstance(session_info, Mapping):
|
|
raw = session_info.get("instructions")
|
|
return str(raw).strip() if raw else ""
|
|
raw = getattr(session_info, "instructions", None)
|
|
return str(raw).strip() if raw else ""
|
|
|
|
|
|
def _is_tool_search_tool_available(tools: Sequence[Any]) -> bool:
|
|
return any(_tool_matches_name(tool, "tool_search") or _tool_matches_name(tool, "ToolSearch") for tool in tools)
|
|
|
|
|
|
def _tool_matches_name(tool: Any, name: str) -> bool:
|
|
if str(getattr(tool, "name", "") or "") == name:
|
|
return True
|
|
return name in {str(alias) for alias in (getattr(tool, "aliases", None) or [])}
|
|
|
|
|
|
def _assistant_used_tool(message: Mapping[str, Any], names: set[str]) -> bool:
|
|
for call in message.get("tool_calls") or []:
|
|
if not isinstance(call, Mapping):
|
|
continue
|
|
fn = call.get("function")
|
|
name = fn.get("name") if isinstance(fn, Mapping) else call.get("name")
|
|
if str(name) in names:
|
|
return True
|
|
content = message.get("content")
|
|
if isinstance(content, Sequence) and not isinstance(content, (str, bytes, bytearray)):
|
|
for block in content:
|
|
if isinstance(block, Mapping) and block.get("type") == "tool_use" and str(block.get("name")) in names:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _is_thinking_only_message(message: Mapping[str, Any]) -> bool:
|
|
content = message.get("content")
|
|
if not isinstance(content, Sequence) or isinstance(content, (str, bytes, bytearray)):
|
|
return False
|
|
blocks = [block for block in content if isinstance(block, Mapping)]
|
|
return bool(blocks) and all(str(block.get("type")) == "thinking" for block in blocks)
|
|
|
|
|
|
def _collect_read_tool_file_paths(messages: Sequence[Mapping[str, Any]]) -> set[str]:
|
|
stub_ids: set[str] = set()
|
|
for message in messages:
|
|
if message.get("role") != "tool":
|
|
continue
|
|
content = _content_to_text(message.get("content"))
|
|
if content.startswith("File unchanged since last read"):
|
|
tool_call_id = message.get("tool_call_id")
|
|
if tool_call_id:
|
|
stub_ids.add(str(tool_call_id))
|
|
|
|
paths: set[str] = set()
|
|
for message in messages:
|
|
if message.get("role") != "assistant":
|
|
continue
|
|
for tool_use_id, tool_name, tool_input in _iter_assistant_tool_uses(message):
|
|
if tool_use_id in stub_ids or tool_name not in {"read", "Read", "FileRead"}:
|
|
continue
|
|
raw_path = tool_input.get("file_path") or tool_input.get("path")
|
|
if not raw_path:
|
|
continue
|
|
try:
|
|
paths.add(str(Path(str(raw_path)).expanduser().resolve()))
|
|
except OSError:
|
|
paths.add(str(raw_path))
|
|
return paths
|
|
|
|
|
|
def _iter_assistant_tool_uses(message: Mapping[str, Any]) -> Iterable[tuple[str, str, Mapping[str, Any]]]:
|
|
for call in message.get("tool_calls") or []:
|
|
if not isinstance(call, Mapping):
|
|
continue
|
|
tool_use_id = str(call.get("id") or "")
|
|
fn = call.get("function")
|
|
if isinstance(fn, Mapping):
|
|
name = str(fn.get("name") or "")
|
|
raw_args = fn.get("arguments")
|
|
if isinstance(raw_args, str):
|
|
try:
|
|
args = json.loads(raw_args)
|
|
except json.JSONDecodeError:
|
|
args = {}
|
|
elif isinstance(raw_args, Mapping):
|
|
args = raw_args
|
|
else:
|
|
args = {}
|
|
yield tool_use_id, name, args
|
|
content = message.get("content")
|
|
if isinstance(content, Sequence) and not isinstance(content, (str, bytes, bytearray)):
|
|
for block in content:
|
|
if not isinstance(block, Mapping) or block.get("type") != "tool_use":
|
|
continue
|
|
raw_input = block.get("input")
|
|
yield str(block.get("id") or ""), str(block.get("name") or ""), raw_input if isinstance(raw_input, Mapping) else {}
|
|
|
|
|
|
def _should_exclude_from_post_compact_restore(filename: str, context: Any) -> bool:
|
|
try:
|
|
from openspace.services.memory.openspace_md import is_memory_file_path
|
|
|
|
if is_memory_file_path(filename):
|
|
return True
|
|
except Exception:
|
|
pass
|
|
plan_path = getattr(context, "plan_file_path", None)
|
|
if plan_path:
|
|
try:
|
|
return Path(filename).expanduser().resolve() == Path(str(plan_path)).expanduser().resolve()
|
|
except OSError:
|
|
return False
|
|
return False
|
|
|
|
|
|
def _read_state_timestamp(entry: Any) -> float:
|
|
if isinstance(entry, Mapping):
|
|
raw = entry.get("timestamp", 0)
|
|
else:
|
|
raw = getattr(entry, "timestamp", 0)
|
|
try:
|
|
return float(raw)
|
|
except (TypeError, ValueError):
|
|
return 0.0
|
|
|
|
|
|
def _is_read_denied(filename: str, context: Any) -> bool:
|
|
permission_context = getattr(context, "permission_context", None)
|
|
if permission_context is None:
|
|
return True
|
|
try:
|
|
from openspace.grounding.core.permissions import check_read_permission_for_tool
|
|
|
|
result = check_read_permission_for_tool("read", filename, permission_context)
|
|
return str(getattr(result, "behavior", "")) == "deny"
|
|
except Exception:
|
|
return True
|
|
|
|
|
|
def _drop_read_file_state(context: Any, filename: str) -> None:
|
|
read_state = getattr(context, "read_file_state", None)
|
|
if not isinstance(read_state, dict):
|
|
return
|
|
read_state.pop(filename, None)
|
|
try:
|
|
read_state.pop(str(Path(filename).expanduser().resolve()), None)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def _display_path(path: Path, context: Any) -> str:
|
|
try:
|
|
cwd = Path(str(getattr(context, "cwd", os.getcwd()))).expanduser().resolve()
|
|
return str(path.relative_to(cwd))
|
|
except Exception:
|
|
return str(path)
|
|
|
|
|
|
def _rough_token_count(text: str) -> int:
|
|
return max(1, len(text) // 4)
|
|
|
|
|
|
def _content_to_text(content: Any) -> str:
|
|
if isinstance(content, str):
|
|
return content
|
|
if isinstance(content, Sequence) and not isinstance(content, (str, bytes, bytearray)):
|
|
parts: list[str] = []
|
|
for block in content:
|
|
if isinstance(block, Mapping):
|
|
if block.get("type") == "text":
|
|
parts.append(str(block.get("text") or ""))
|
|
elif block.get("type") in {"image", "image_url"}:
|
|
parts.append("[image]")
|
|
elif block.get("type") == "document":
|
|
parts.append("[document]")
|
|
else:
|
|
parts.append(json.dumps(dict(block), ensure_ascii=False, default=str))
|
|
else:
|
|
parts.append(str(block))
|
|
return "\n".join(part for part in parts if part)
|
|
return json.dumps(content, ensure_ascii=False, default=str)
|
|
|
|
|
|
def _json_safe_attachment(attachment: Mapping[str, Any]) -> dict[str, Any]:
|
|
def convert(value: Any) -> Any:
|
|
if isinstance(value, Mapping):
|
|
return {str(k): convert(v) for k, v in value.items()}
|
|
if isinstance(value, (list, tuple, set)):
|
|
return [convert(v) for v in value]
|
|
if isinstance(value, Path):
|
|
return str(value)
|
|
return value
|
|
|
|
return convert(dict(attachment))
|
|
|
|
|
|
def _scan_dynamic_skill_keys(messages: Sequence[Mapping[str, Any]]) -> set[str]:
|
|
keys: set[str] = set()
|
|
for message in messages:
|
|
attachment = _attachment_payload(message)
|
|
if not isinstance(attachment, Mapping):
|
|
continue
|
|
if attachment.get("type") != "dynamic_skill":
|
|
continue
|
|
skill_dir = str(attachment.get("skillDir") or attachment.get("displayPath") or "")
|
|
names = sorted(str(name) for name in (attachment.get("skillNames") or []) if str(name).strip())
|
|
if skill_dir and names:
|
|
keys.add(f"{skill_dir}:{','.join(names)}")
|
|
return keys
|
|
|
|
|
|
def _parse_dynamic_skill_key(key: str) -> tuple[str, list[str]]:
|
|
if ":" not in key:
|
|
return "", []
|
|
skill_dir, raw_names = key.rsplit(":", 1)
|
|
names = sorted(name.strip() for name in raw_names.split(",") if name.strip())
|
|
return skill_dir, names
|
|
|
|
|
|
def _extract_task_result_text(result: Any) -> str | None:
|
|
if isinstance(result, Mapping):
|
|
for key in ("result", "content", "summary", "output"):
|
|
value = result.get(key)
|
|
if value:
|
|
return _content_to_text(value)
|
|
return None
|
|
|
|
|
|
__all__ = [
|
|
"create_attachment_message",
|
|
"format_attachment_for_model",
|
|
"get_deferred_tools_delta_attachment",
|
|
"get_agent_listing_delta_attachment",
|
|
"get_mcp_instructions_delta_attachment",
|
|
"get_todo_reminder_turn_counts",
|
|
"get_todo_reminder_attachments",
|
|
"get_diagnostic_attachments",
|
|
"get_task_reminder_attachments",
|
|
"get_turn_attachment_messages",
|
|
"get_turn_attachment_messages_async",
|
|
"create_post_compact_attachments",
|
|
"create_post_compact_file_attachments",
|
|
"generate_file_attachment",
|
|
"create_plan_attachment_if_needed",
|
|
"create_plan_mode_attachment_if_needed",
|
|
"get_plan_mode_attachments",
|
|
"get_plan_mode_exit_attachment",
|
|
"get_verify_plan_reminder_attachment",
|
|
"create_invoked_skills_attachment_if_needed",
|
|
"create_skill_state_attachment_if_needed",
|
|
"create_dynamic_skill_attachments_if_needed",
|
|
"create_async_agent_attachments_if_needed",
|
|
"get_post_compact_nested_memory_attachments",
|
|
]
|