semora 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. semora-0.1.0/.gitignore +28 -0
  2. semora-0.1.0/PKG-INFO +88 -0
  3. semora-0.1.0/README.md +49 -0
  4. semora-0.1.0/pyproject.toml +55 -0
  5. semora-0.1.0/src/semora/__init__.py +60 -0
  6. semora-0.1.0/src/semora/background.py +156 -0
  7. semora-0.1.0/src/semora/builtins/__init__.py +248 -0
  8. semora-0.1.0/src/semora/builtins/_exec.py +302 -0
  9. semora-0.1.0/src/semora/builtins/_files.py +256 -0
  10. semora-0.1.0/src/semora/builtins/_search.py +383 -0
  11. semora-0.1.0/src/semora/builtins/_types.py +118 -0
  12. semora-0.1.0/src/semora/builtins/_web.py +208 -0
  13. semora-0.1.0/src/semora/contracts/__init__.py +79 -0
  14. semora-0.1.0/src/semora/contracts/agent.py +40 -0
  15. semora-0.1.0/src/semora/contracts/events.py +244 -0
  16. semora-0.1.0/src/semora/contracts/types.py +197 -0
  17. semora-0.1.0/src/semora/controls.py +356 -0
  18. semora-0.1.0/src/semora/dispatch.py +379 -0
  19. semora-0.1.0/src/semora/driver.py +32 -0
  20. semora-0.1.0/src/semora/engines/__init__.py +5 -0
  21. semora-0.1.0/src/semora/engines/plain/__init__.py +5 -0
  22. semora-0.1.0/src/semora/engines/plain/loop.py +914 -0
  23. semora-0.1.0/src/semora/goal.py +88 -0
  24. semora-0.1.0/src/semora/history.py +296 -0
  25. semora-0.1.0/src/semora/ids.py +11 -0
  26. semora-0.1.0/src/semora/orchestration.py +162 -0
  27. semora-0.1.0/src/semora/orchestrator.py +1340 -0
  28. semora-0.1.0/src/semora/plan_mode.py +133 -0
  29. semora-0.1.0/src/semora/prompts.py +98 -0
  30. semora-0.1.0/src/semora/providers.py +211 -0
  31. semora-0.1.0/src/semora/py.typed +0 -0
  32. semora-0.1.0/src/semora/runtime.py +1146 -0
  33. semora-0.1.0/src/semora/sandbox_remote.py +499 -0
  34. semora-0.1.0/src/semora/skills.py +424 -0
  35. semora-0.1.0/src/semora/subagents.py +784 -0
  36. semora-0.1.0/src/semora/tool_search.py +225 -0
  37. semora-0.1.0/src/semora/tools.py +690 -0
  38. semora-0.1.0/src/semora/transcript.py +255 -0
  39. semora-0.1.0/src/semora/workspace.py +981 -0
@@ -0,0 +1,28 @@
1
+ .venv/
2
+ .pytest_cache/
3
+ .mypy_cache/
4
+ .ruff_cache/
5
+ __pycache__/
6
+ *.py[cod]
7
+ *.egg-info/
8
+ build/
9
+ dist/
10
+ .coverage
11
+ htmlcov/
12
+ .env
13
+ .env.*
14
+ !.env.example
15
+
16
+
17
+ # Local tool/editor state — machine-specific, never pushed.
18
+ .claude/
19
+ .codecanvas/
20
+ .vscode/
21
+
22
+
23
+ # Superpowers design/spec scratch — working notes, not project documentation.
24
+ docs/superpowers/
25
+
26
+ # 로컬 자격증명 — 절대 커밋 금지.
27
+ a.txt
28
+ *.token
semora-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.5
2
+ Name: semora
3
+ Version: 0.1.0
4
+ Summary: Durable multi-agent runtime for Python.
5
+ Project-URL: Homepage, https://github.com/donggyun112/semora
6
+ Project-URL: Source, https://github.com/donggyun112/semora
7
+ Project-URL: Changelog, https://github.com/donggyun112/semora/blob/main/CHANGELOG.md
8
+ Author: donggyun112
9
+ License-Expression: MIT
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.12
16
+ Requires-Dist: langchain-core<2,>=1
17
+ Requires-Dist: loguru<1,>=0.7
18
+ Requires-Dist: semora-llm==0.1.0
19
+ Requires-Dist: semora-store==0.1.0
20
+ Provides-Extra: anthropic
21
+ Requires-Dist: langchain-anthropic<2,>=1; extra == 'anthropic'
22
+ Provides-Extra: fork
23
+ Requires-Dist: semora-fork==0.1.0; extra == 'fork'
24
+ Provides-Extra: google
25
+ Requires-Dist: langchain-google-genai<5,>=4; extra == 'google'
26
+ Provides-Extra: openai
27
+ Requires-Dist: langchain-openai<2,>=1; extra == 'openai'
28
+ Provides-Extra: openrouter
29
+ Requires-Dist: langchain-openai<2,>=1; extra == 'openrouter'
30
+ Provides-Extra: permissions
31
+ Requires-Dist: semora-permissions==0.1.0; extra == 'permissions'
32
+ Provides-Extra: postgres
33
+ Requires-Dist: semora-store-pg==0.1.0; extra == 'postgres'
34
+ Provides-Extra: ui
35
+ Requires-Dist: semora-ui==0.1.0; extra == 'ui'
36
+ Provides-Extra: xai
37
+ Requires-Dist: langchain-xai<2,>=1; extra == 'xai'
38
+ Description-Content-Type: text/markdown
39
+
40
+ # semora
41
+
42
+ Durable agent runtime for Python. This is the core: contracts, control points, tool
43
+ execution, the orchestrator, the plain `async while` planner, and `AgentRuntime`.
44
+
45
+ By default `AgentRuntime` drives the planner directly — no orchestrator, no ledger:
46
+
47
+ ```python
48
+ from semora import Agent, AgentRuntime, ChatModel
49
+
50
+ model = ChatModel(model="gpt-4.1")
51
+ agent = Agent("reviewer", "Reviews repositories", model, tools, system_prompt)
52
+ outcome = await AgentRuntime().run("attempt-42", agent, "inspect this repository")
53
+ ```
54
+
55
+ This path cannot suspend or recover a crashed round. Attach a ledger when those
56
+ guarantees are required:
57
+
58
+ ```python
59
+ from semora import AgentRuntime, MemorySteps
60
+
61
+ runtime = AgentRuntime(store=MemorySteps())
62
+ outcome = await runtime.run("attempt-42", agent, "inspect this repository")
63
+ ```
64
+
65
+ Policy lives on `Controls` / `ControlPlane` (`on_inputs`, `before_model`, `pre_tool_use`,
66
+ `after_tool_call`, `before_finish`, `on_resume`, `on_suspend`). `before_finish` can refuse
67
+ an ending and send the loop around again.
68
+
69
+ `semora.builtins.builtin_tools()` supplies `read`, `write`, `edit`, `grep`, `glob`, `Bash`,
70
+ and `web_fetch`. `web_search` is not included. `Bash` stays disabled until
71
+ `ExecToolOptions.allow_list` is set. File and process effects use the `WorkspaceProvider`
72
+ injected by `AgentRuntime` (`semora.workspace`).
73
+
74
+ Install extras beside it:
75
+
76
+ | | |
77
+ |---|---|
78
+ | `semora` | the runtime, with in-memory `MemorySteps` |
79
+ | `semora[openai]` | `langchain-openai` |
80
+ | `semora[anthropic]` | `langchain-anthropic` |
81
+ | `semora[google]` | `langchain-google-genai` |
82
+ | `semora[xai]` | `langchain-xai` |
83
+ | `semora[openrouter]` | OpenAI adapter for OpenRouter |
84
+ | `semora[postgres]` | Postgres ledger (`semora_store_pg`) |
85
+ | `semora[permissions]` | permission rule table (`semora_permissions`) |
86
+ | `semora[ui]` | local console (`semora_ui`) |
87
+
88
+ See the [repository README](../../README.md) for examples.
semora-0.1.0/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # semora
2
+
3
+ Durable agent runtime for Python. This is the core: contracts, control points, tool
4
+ execution, the orchestrator, the plain `async while` planner, and `AgentRuntime`.
5
+
6
+ By default `AgentRuntime` drives the planner directly — no orchestrator, no ledger:
7
+
8
+ ```python
9
+ from semora import Agent, AgentRuntime, ChatModel
10
+
11
+ model = ChatModel(model="gpt-4.1")
12
+ agent = Agent("reviewer", "Reviews repositories", model, tools, system_prompt)
13
+ outcome = await AgentRuntime().run("attempt-42", agent, "inspect this repository")
14
+ ```
15
+
16
+ This path cannot suspend or recover a crashed round. Attach a ledger when those
17
+ guarantees are required:
18
+
19
+ ```python
20
+ from semora import AgentRuntime, MemorySteps
21
+
22
+ runtime = AgentRuntime(store=MemorySteps())
23
+ outcome = await runtime.run("attempt-42", agent, "inspect this repository")
24
+ ```
25
+
26
+ Policy lives on `Controls` / `ControlPlane` (`on_inputs`, `before_model`, `pre_tool_use`,
27
+ `after_tool_call`, `before_finish`, `on_resume`, `on_suspend`). `before_finish` can refuse
28
+ an ending and send the loop around again.
29
+
30
+ `semora.builtins.builtin_tools()` supplies `read`, `write`, `edit`, `grep`, `glob`, `Bash`,
31
+ and `web_fetch`. `web_search` is not included. `Bash` stays disabled until
32
+ `ExecToolOptions.allow_list` is set. File and process effects use the `WorkspaceProvider`
33
+ injected by `AgentRuntime` (`semora.workspace`).
34
+
35
+ Install extras beside it:
36
+
37
+ | | |
38
+ |---|---|
39
+ | `semora` | the runtime, with in-memory `MemorySteps` |
40
+ | `semora[openai]` | `langchain-openai` |
41
+ | `semora[anthropic]` | `langchain-anthropic` |
42
+ | `semora[google]` | `langchain-google-genai` |
43
+ | `semora[xai]` | `langchain-xai` |
44
+ | `semora[openrouter]` | OpenAI adapter for OpenRouter |
45
+ | `semora[postgres]` | Postgres ledger (`semora_store_pg`) |
46
+ | `semora[permissions]` | permission rule table (`semora_permissions`) |
47
+ | `semora[ui]` | local console (`semora_ui`) |
48
+
49
+ See the [repository README](../../README.md) for examples.
@@ -0,0 +1,55 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "semora"
7
+ version = "0.1.0"
8
+ description = "Durable multi-agent runtime for Python."
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = "MIT"
12
+ authors = [{ name = "donggyun112" }]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Typing :: Typed",
19
+ ]
20
+ urls = { Homepage = "https://github.com/donggyun112/semora", Source = "https://github.com/donggyun112/semora", Changelog = "https://github.com/donggyun112/semora/blob/main/CHANGELOG.md" }
21
+ dependencies = [
22
+ "langchain-core>=1,<2",
23
+ "loguru>=0.7,<1",
24
+ "semora-llm==0.1.0",
25
+ "semora-store==0.1.0",
26
+ ]
27
+ # The core: contracts, controls, tool execution, the orchestrator, the engine, and the facade over
28
+ # them. Those are layers of one thing rather than separate products — they share a dependency
29
+ # footprint and nobody installs them apart — so they are subpackages here.
30
+ # `tests/test_packaging.py` keeps them layered, which is the job separate manifests used to do.
31
+
32
+ [project.optional-dependencies]
33
+ postgres = ["semora-store-pg==0.1.0"]
34
+ permissions = ["semora-permissions==0.1.0"]
35
+ ui = ["semora-ui==0.1.0"]
36
+ fork = ["semora-fork==0.1.0"]
37
+ # Chat models stay LangChain's. These extras only install the provider adapter;
38
+ # `from langchain_openai import ChatOpenAI` is still how a caller constructs one.
39
+ openai = ["langchain-openai>=1,<2"]
40
+ anthropic = ["langchain-anthropic>=1,<2"]
41
+ google = ["langchain-google-genai>=4,<5"]
42
+ xai = ["langchain-xai>=1,<2"]
43
+ # OpenRouter speaks the OpenAI wire. Same adapter, different base URL.
44
+ openrouter = ["langchain-openai>=1,<2"]
45
+
46
+ [tool.hatch.build.targets.wheel]
47
+ packages = ["src/semora"]
48
+
49
+ [tool.uv.sources]
50
+ semora-fork = { workspace = true }
51
+ semora-llm = { workspace = true }
52
+ semora-permissions = { workspace = true }
53
+ semora-store = { workspace = true }
54
+ semora-store-pg = { workspace = true }
55
+ semora-ui = { workspace = true }
@@ -0,0 +1,60 @@
1
+ """Semora's public Python package."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ from semora_llm import ChatModel
6
+ from semora_store import ExecutionContext, MemorySteps
7
+
8
+ from .contracts import Agent, AgentDefinition, PendingInput, ToolCall, Tools
9
+ from .controls import (
10
+ Continue,
11
+ ControlPlane,
12
+ Controls,
13
+ Ctx,
14
+ Deny,
15
+ FinishPolicy,
16
+ Halt,
17
+ Ingress,
18
+ Permissions,
19
+ Proceed,
20
+ ResumeInput,
21
+ Suspend,
22
+ gate,
23
+ )
24
+ from .ids import new_run_id
25
+ from .runtime import AgentRuntime, run
26
+ from .workspace import HostWorkspaceProvider
27
+
28
+ try:
29
+ __version__ = version("semora")
30
+ except PackageNotFoundError: # pragma: no cover - source tree without installation
31
+ __version__ = "0.0.0"
32
+
33
+ __all__ = [
34
+ "Agent",
35
+ "AgentDefinition",
36
+ "AgentRuntime",
37
+ "ChatModel",
38
+ "Continue",
39
+ "ControlPlane",
40
+ "Controls",
41
+ "Ctx",
42
+ "Deny",
43
+ "ExecutionContext",
44
+ "FinishPolicy",
45
+ "Halt",
46
+ "HostWorkspaceProvider",
47
+ "Ingress",
48
+ "MemorySteps",
49
+ "PendingInput",
50
+ "Permissions",
51
+ "Proceed",
52
+ "ResumeInput",
53
+ "Suspend",
54
+ "ToolCall",
55
+ "Tools",
56
+ "__version__",
57
+ "gate",
58
+ "new_run_id",
59
+ "run",
60
+ ]
@@ -0,0 +1,156 @@
1
+ """Process-local registry and result types for agent-launched background work."""
2
+
3
+ import asyncio
4
+ import time
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass, field
7
+ from typing import Any, Literal
8
+
9
+ __all__ = [
10
+ "BackgroundResult",
11
+ "BackgroundTask",
12
+ "BackgroundTasks",
13
+ "TaskStatus",
14
+ ]
15
+
16
+ TaskStatus = Literal["running", "done", "error", "cancelled"]
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class BackgroundResult:
21
+ """Result delivered when managed background work settles."""
22
+
23
+ task_id: str
24
+ kind: str
25
+ label: str
26
+ content: str
27
+ is_error: bool = False
28
+
29
+ def as_message(self) -> str:
30
+ """Render the result as a model-visible background notification."""
31
+ state = "failed" if self.is_error else "completed"
32
+ header = f'[background {self.kind} "{self.label}" {state}] (task {self.task_id})'
33
+ return f"{header}\n{self.content}"
34
+
35
+
36
+ @dataclass(slots=True)
37
+ class BackgroundTask:
38
+ """Registered background task and its observable lifecycle state."""
39
+
40
+ task_id: str
41
+ kind: str
42
+ label: str
43
+ started_at: float
44
+ task: asyncio.Task[Any]
45
+ status: TaskStatus = "running"
46
+ settled_at: float | None = None
47
+ read_output: Callable[[], str] | None = None
48
+
49
+ def snapshot(self) -> dict[str, Any]:
50
+ """Return a serializable task status snapshot."""
51
+ return {
52
+ "task_id": self.task_id,
53
+ "kind": self.kind,
54
+ "label": self.label,
55
+ "status": self.status,
56
+ "started_at": self.started_at,
57
+ **({"settled_at": self.settled_at} if self.settled_at is not None else {}),
58
+ }
59
+
60
+
61
+ @dataclass
62
+ class BackgroundTasks:
63
+ """Manage process-local background tasks launched by one parent run."""
64
+
65
+ max_settled_retained: int = 50
66
+
67
+ _tasks: dict[str, BackgroundTask] = field(default_factory=dict, repr=False)
68
+ _listeners: list[Callable[[str, TaskStatus], None]] = field(default_factory=list, repr=False)
69
+
70
+ def register(
71
+ self,
72
+ task_id: str,
73
+ kind: str,
74
+ label: str,
75
+ task: asyncio.Task[Any],
76
+ *,
77
+ read_output: Callable[[], str] | None = None,
78
+ ) -> BackgroundTask:
79
+ """Register a launched task without notifying settlement listeners."""
80
+ entry = BackgroundTask(
81
+ task_id=task_id,
82
+ kind=kind,
83
+ label=label,
84
+ started_at=time.time(),
85
+ task=task,
86
+ read_output=read_output,
87
+ )
88
+ self._tasks[task_id] = entry
89
+ return entry
90
+
91
+ def settle(self, task_id: str, status: TaskStatus) -> None:
92
+ """Record the first terminal status for a task."""
93
+ entry = self._tasks.get(task_id)
94
+ if entry is None or entry.status != "running":
95
+ return
96
+ entry.status = status
97
+ entry.settled_at = time.time()
98
+ self._prune()
99
+ self._notify(task_id, status)
100
+
101
+ def cancel(self, task_id: str) -> bool:
102
+ """Cancel a running task and report whether cancellation was applied."""
103
+ entry = self._tasks.get(task_id)
104
+ if entry is None or entry.status != "running":
105
+ return False
106
+ entry.status = "cancelled"
107
+ entry.settled_at = time.time()
108
+ entry.task.cancel()
109
+ self._prune()
110
+ self._notify(task_id, "cancelled")
111
+ return True
112
+
113
+ def cancel_all(self) -> int:
114
+ """Cancel all running tasks and return the number cancelled."""
115
+ return sum(self.cancel(task_id) for task_id in list(self._tasks))
116
+
117
+ def get(self, task_id: str) -> BackgroundTask | None:
118
+ """Return a task record by identifier."""
119
+ return self._tasks.get(task_id)
120
+
121
+ def status(self, task_id: str) -> TaskStatus | None:
122
+ """Return a task status by identifier."""
123
+ entry = self._tasks.get(task_id)
124
+ return entry.status if entry is not None else None
125
+
126
+ def list(self) -> list[dict[str, Any]]:
127
+ """Return task snapshots in launch order."""
128
+ return [
129
+ entry.snapshot()
130
+ for entry in sorted(self._tasks.values(), key=lambda item: item.started_at)
131
+ ]
132
+
133
+ def subscribe(self, listener: Callable[[str, TaskStatus], None]) -> Callable[[], None]:
134
+ """Subscribe to task settlements and return an unsubscribe callback."""
135
+ self._listeners.append(listener)
136
+
137
+ def unsubscribe() -> None:
138
+ if listener in self._listeners:
139
+ self._listeners.remove(listener)
140
+
141
+ return unsubscribe
142
+
143
+ def _notify(self, task_id: str, status: TaskStatus) -> None:
144
+ for listener in list(self._listeners):
145
+ try:
146
+ listener(task_id, status)
147
+ except Exception: # one bad listener must not break settling
148
+ continue
149
+
150
+ def _prune(self) -> None:
151
+ settled = sorted(
152
+ (item for item in self._tasks.values() if item.status != "running"),
153
+ key=lambda item: item.settled_at or 0.0,
154
+ )
155
+ for entry in settled[: max(0, len(settled) - self.max_settled_retained)]:
156
+ del self._tasks[entry.task_id]
@@ -0,0 +1,248 @@
1
+ """TS-compatible core built-in tool bundle.
2
+
3
+ The bundle intentionally contains file/process tools plus ``web_fetch``. It does not include
4
+ ``web_search``; applications can add their own search provider when they need one.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from functools import partial
10
+ from typing import Any
11
+
12
+ from ..workspace import ToolContext
13
+ from ._exec import exec_is_read_only, exec_tool
14
+ from ._files import edit_tool, read_tool, write_tool
15
+ from ._search import glob_tool, grep_tool
16
+ from ._types import (
17
+ BuiltinToolState,
18
+ ExecToolOptions,
19
+ Handler,
20
+ WebFetchResponse,
21
+ WebFetchSummarizer,
22
+ WebFetchToolOptions,
23
+ WebFetchTransport,
24
+ error_result,
25
+ )
26
+ from ._web import UrllibWebFetchTransport, web_fetch_tool
27
+
28
+ __all__ = [
29
+ "BuiltinTools",
30
+ "ExecToolOptions",
31
+ "UrllibWebFetchTransport",
32
+ "WebFetchResponse",
33
+ "WebFetchSummarizer",
34
+ "WebFetchToolOptions",
35
+ "WebFetchTransport",
36
+ "builtin_tools",
37
+ ]
38
+
39
+
40
+ class BuiltinTools:
41
+ """Context-aware implementation of the TS sandbox bundle plus ``web_fetch``."""
42
+
43
+ def __init__(
44
+ self,
45
+ *,
46
+ context: ToolContext | None = None,
47
+ exec_options: ExecToolOptions | None = None,
48
+ web_fetch_options: WebFetchToolOptions | None = None,
49
+ _state: BuiltinToolState | None = None,
50
+ ) -> None:
51
+ """Configure tool policy independently from a turn's workspace session."""
52
+ self._context = context or ToolContext(workdir=".")
53
+ self._exec_options = exec_options or ExecToolOptions()
54
+ self._web_fetch_options = web_fetch_options or WebFetchToolOptions()
55
+ self._state = _state or BuiltinToolState()
56
+ self._handlers: dict[str, Handler] = {
57
+ "read": read_tool,
58
+ "write": write_tool,
59
+ "edit": edit_tool,
60
+ "glob": glob_tool,
61
+ "grep": grep_tool,
62
+ "Bash": partial(exec_tool, options=self._exec_options),
63
+ "web_fetch": partial(web_fetch_tool, options=self._web_fetch_options),
64
+ }
65
+ self._definitions = _definitions(self._exec_options.allow_shell)
66
+
67
+ async def execute(self, name: str, call_id: str, arguments: Any) -> dict[str, Any]:
68
+ """Execute a named built-in against the currently bound context."""
69
+ handler = self._handlers.get(name)
70
+ if handler is None:
71
+ return error_result(f"Unknown tool: {name}")
72
+ return dict(await handler(call_id, arguments, self._context, self._state))
73
+
74
+ def get(self, name: str) -> dict[str, Any] | None:
75
+ """Return one model-visible tool definition."""
76
+ definition = self._definitions.get(name)
77
+ return dict(definition) if definition is not None else None
78
+
79
+ def list(self) -> list[dict[str, Any]]:
80
+ """Return the stable core set, deliberately excluding ``web_search``."""
81
+ return [dict(definition) for definition in self._definitions.values()]
82
+
83
+ def get_context(self) -> ToolContext:
84
+ """Return the immutable execution context currently bound to the tools."""
85
+ return self._context
86
+
87
+ def with_context(self, context: ToolContext) -> BuiltinTools:
88
+ """Bind a runtime workspace while retaining locks and fetch cache across turns."""
89
+ return BuiltinTools(
90
+ context=context,
91
+ exec_options=self._exec_options,
92
+ web_fetch_options=self._web_fetch_options,
93
+ _state=self._state,
94
+ )
95
+
96
+
97
+ def builtin_tools(
98
+ *,
99
+ context: ToolContext | None = None,
100
+ exec_options: ExecToolOptions | None = None,
101
+ web_fetch_options: WebFetchToolOptions | None = None,
102
+ ) -> BuiltinTools:
103
+ """Create the standard bundle, optionally bound to a caller-managed workspace context."""
104
+ return BuiltinTools(
105
+ context=context,
106
+ exec_options=exec_options,
107
+ web_fetch_options=web_fetch_options,
108
+ )
109
+
110
+
111
+ def _definitions(allow_shell: bool) -> dict[str, dict[str, Any]]:
112
+ return {
113
+ "read": {
114
+ "name": "read",
115
+ "description": (
116
+ "Read a file or directory from the workspace. Text is line-numbered and paged; "
117
+ "images return inline and Jupyter notebooks return readable cell text."
118
+ ),
119
+ "parameters": {
120
+ "type": "object",
121
+ "properties": {
122
+ "path": {"type": "string"},
123
+ "offset": {"type": "number"},
124
+ "limit": {"type": "number"},
125
+ "pages": {"type": "string"},
126
+ },
127
+ "required": ["path"],
128
+ },
129
+ "is_read_only": True,
130
+ "is_concurrency_safe": True,
131
+ },
132
+ "write": {
133
+ "name": "write",
134
+ "description": "Create or replace a UTF-8 file inside the workspace.",
135
+ "parameters": {
136
+ "type": "object",
137
+ "properties": {"path": {"type": "string"}, "content": {"type": "string"}},
138
+ "required": ["path", "content"],
139
+ },
140
+ "is_read_only": False,
141
+ "is_concurrency_safe": False,
142
+ },
143
+ "edit": {
144
+ "name": "edit",
145
+ "description": "Replace an exact string in a UTF-8 workspace file.",
146
+ "parameters": {
147
+ "type": "object",
148
+ "properties": {
149
+ "path": {"type": "string"},
150
+ "old_string": {"type": "string"},
151
+ "new_string": {"type": "string"},
152
+ "replace_all": {"type": "boolean"},
153
+ },
154
+ "required": ["path", "old_string", "new_string"],
155
+ },
156
+ "is_read_only": False,
157
+ "is_concurrency_safe": False,
158
+ },
159
+ "grep": {
160
+ "name": "grep",
161
+ "description": (
162
+ "Search workspace file contents with ripgrep, falling back to system grep. "
163
+ "Supports content, files_with_matches, and count output modes."
164
+ ),
165
+ "parameters": {
166
+ "type": "object",
167
+ "properties": {
168
+ "pattern": {"type": "string"},
169
+ "path": {"type": "string"},
170
+ "glob": {"type": "string"},
171
+ "type": {"type": "string"},
172
+ "output_mode": {
173
+ "type": "string",
174
+ "enum": ["content", "files_with_matches", "count"],
175
+ },
176
+ "-A": {"type": "number"},
177
+ "-B": {"type": "number"},
178
+ "-C": {"type": "number"},
179
+ "context": {"type": "number"},
180
+ "-n": {"type": "boolean"},
181
+ "-i": {"type": "boolean"},
182
+ "head_limit": {"type": "number"},
183
+ "offset": {"type": "number"},
184
+ "multiline": {"type": "boolean"},
185
+ },
186
+ "required": ["pattern"],
187
+ },
188
+ "is_read_only": True,
189
+ "is_concurrency_safe": True,
190
+ },
191
+ "glob": {
192
+ "name": "glob",
193
+ "description": "Find workspace files by glob pattern with ripgrep.",
194
+ "parameters": {
195
+ "type": "object",
196
+ "properties": {
197
+ "pattern": {"type": "string"},
198
+ "path": {"type": "string"},
199
+ "head_limit": {"type": "number"},
200
+ "offset": {"type": "number"},
201
+ },
202
+ "required": ["pattern"],
203
+ },
204
+ "is_read_only": True,
205
+ "is_concurrency_safe": True,
206
+ },
207
+ "Bash": {
208
+ "name": "Bash",
209
+ "description": (
210
+ "Execute an allow-listed command in the workspace. "
211
+ + (
212
+ "argv is preferred; shell strings are enabled for pipes and globs."
213
+ if allow_shell
214
+ else "Shell-string mode is disabled; use argv."
215
+ )
216
+ ),
217
+ "parameters": {
218
+ "type": "object",
219
+ "properties": {
220
+ "argv": {"type": "array", "items": {"type": "string"}},
221
+ "command": {"type": "string"},
222
+ "timeoutMs": {"type": "number"},
223
+ "cwd": {"type": "string"},
224
+ "run_in_background": {"type": "boolean"},
225
+ },
226
+ },
227
+ "is_read_only": exec_is_read_only,
228
+ "is_concurrency_safe": exec_is_read_only,
229
+ },
230
+ "web_fetch": {
231
+ "name": "web_fetch",
232
+ "description": (
233
+ "Fetch an HTTPS URL and return readable content. HTTP is upgraded to HTTPS; "
234
+ "results are cached by URL and prompt."
235
+ ),
236
+ "parameters": {
237
+ "type": "object",
238
+ "properties": {
239
+ "url": {"type": "string"},
240
+ "prompt": {"type": "string"},
241
+ "max_chars": {"type": "number"},
242
+ },
243
+ "required": ["url"],
244
+ },
245
+ "is_read_only": True,
246
+ "is_concurrency_safe": True,
247
+ },
248
+ }