mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-11 14:50:14 +00:00
OpenWorker: initial import
Imported from andrewyng/aisuite@1b4bbf303e (contents of its platform/ directory, hoisted to the repo root). Development history prior to this commit lives in that repository. Co-authored-by: Devika <devikaverma11@gmail.com>
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
"""The `explore` tool — a read-only research subagent with its own context window.
|
||||
|
||||
Broad questions ("where is retry logic handled?") burn the main session's context on
|
||||
dozens of file reads. `explore` spawns a child TurnEngine over the same workspace with
|
||||
read-only tools and a fresh context; only its final report returns to the caller.
|
||||
|
||||
The child runs in plan mode — the PermissionEngine hard-blocks writes/shell no matter
|
||||
what the child decides — with no approver, so it never needs an approval round-trip.
|
||||
That's what lets `explore` carry low-risk metadata, which in turn makes several explores
|
||||
in one assistant turn eligible for the engine's parallel execution. No recursion: the
|
||||
child registry has no `explore` tool.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
import aisuite as ai
|
||||
|
||||
from ..engine import TurnEngine
|
||||
from ..events import EventType
|
||||
from ..permissions import Mode, PermissionEngine
|
||||
from ..tools import ToolRegistry
|
||||
from .files import file_tools
|
||||
from .git import git_tools
|
||||
from .search import search_tools
|
||||
|
||||
EXPLORER_INSTRUCTIONS = """You are a read-only code explorer working inside the user's workspace. \
|
||||
Answer the research task you're given by searching and reading the code (`grep`, `read_file`, \
|
||||
`list_files`, `git_log`, `git_status`, `git_diff`). You cannot write files or run commands.
|
||||
|
||||
Your final message is your report — it goes back to the agent that spawned you, not to the \
|
||||
user. Make it self-contained: answer the task directly, reference code as path:line, quote the \
|
||||
key snippets, and note anything surprising you found along the way. If you couldn't find \
|
||||
something, say what you searched so the caller doesn't repeat the same searches."""
|
||||
|
||||
_CHILD_MAX_ITERATIONS = 10
|
||||
|
||||
|
||||
def build_explorer_engine(
|
||||
*,
|
||||
workspace: str | Path,
|
||||
provider: Any,
|
||||
model: str,
|
||||
model_settings: Optional[dict[str, Any]] = None,
|
||||
max_iterations: int = _CHILD_MAX_ITERATIONS,
|
||||
) -> TurnEngine:
|
||||
"""A child engine with the Code agent's read-only tools and a fresh context."""
|
||||
ws = str(Path(workspace).resolve())
|
||||
registry = ToolRegistry()
|
||||
# Read-only slice of the Code agent's toolset, with the same toolkit replacements
|
||||
# (our grep for search_files, our windowed read_file for read_file/read_file_lines).
|
||||
replaced = {"search_files", "read_file", "read_file_lines"}
|
||||
registry.register_all(
|
||||
[
|
||||
t
|
||||
for t in ai.toolkits.files(root=ws) # no allow_write → list/read only
|
||||
if getattr(t, "__name__", "") not in replaced
|
||||
]
|
||||
)
|
||||
registry.register_all(file_tools(ws))
|
||||
registry.register_all(ai.toolkits.git(root=ws)) # git_status, git_diff
|
||||
registry.register_all(git_tools(ws)) # git_log
|
||||
registry.register_all(search_tools(ws)) # grep
|
||||
permissions = PermissionEngine(workspace_root=Path(ws), mode=Mode.PLAN)
|
||||
return TurnEngine(
|
||||
provider=provider,
|
||||
registry=registry,
|
||||
permissions=permissions,
|
||||
model=model,
|
||||
instructions=EXPLORER_INSTRUCTIONS,
|
||||
max_iterations=max_iterations,
|
||||
model_settings=model_settings,
|
||||
)
|
||||
|
||||
|
||||
def explorer_tools(
|
||||
*,
|
||||
workspace: str | Path,
|
||||
provider: Any,
|
||||
model: str,
|
||||
model_settings: Optional[dict[str, Any]] = None,
|
||||
) -> list:
|
||||
def explore(task: str) -> dict:
|
||||
"""Delegate a broad, read-only research task to a subagent with its own fresh
|
||||
context window. It searches and reads the workspace, then returns only its final
|
||||
report — the intermediate file reads never touch your context. Use it for
|
||||
multi-file questions ("where is X handled?", "how does the Y flow work?"); for a
|
||||
single known file, just read it yourself. Independent explore calls run in
|
||||
parallel when requested together. State the task precisely and say what the
|
||||
report should include.
|
||||
|
||||
Args:
|
||||
task (str): The research question, with any constraints and the expected
|
||||
shape of the report.
|
||||
"""
|
||||
engine = build_explorer_engine(
|
||||
workspace=workspace,
|
||||
provider=provider,
|
||||
model=model,
|
||||
model_settings=model_settings,
|
||||
)
|
||||
|
||||
async def _run() -> tuple[str, str]:
|
||||
report, status = "", "unknown"
|
||||
async for event in engine.run(task):
|
||||
if event.type == EventType.ASSISTANT_MESSAGE and event.data.get("text"):
|
||||
report = event.data["text"]
|
||||
elif event.type == EventType.TURN_END:
|
||||
status = event.data.get("status", "unknown")
|
||||
elif event.type == EventType.ERROR:
|
||||
return report, f"error: {event.data.get('error', '')}"
|
||||
return report, status
|
||||
|
||||
# Tools execute in a worker thread (no running loop), so asyncio.run is safe.
|
||||
report, status = asyncio.run(_run())
|
||||
if not report:
|
||||
return {"error": f"explorer produced no report (status: {status})"}
|
||||
result: dict[str, Any] = {"report": report}
|
||||
if status != "completed":
|
||||
result["note"] = (
|
||||
f"explorer stopped early ({status}); the report may be partial"
|
||||
)
|
||||
return result
|
||||
|
||||
return [
|
||||
ai.tool(
|
||||
explore,
|
||||
metadata=ai.ToolMetadata(
|
||||
category="search",
|
||||
risk_level="low",
|
||||
capabilities=["search"],
|
||||
requires_approval=False,
|
||||
),
|
||||
)
|
||||
]
|
||||
Reference in New Issue
Block a user