forge-ai-runtime 0.7.0__py3-none-any.whl
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.
- ai_runtime/__init__.py +100 -0
- ai_runtime/adapters/__init__.py +0 -0
- ai_runtime/adapters/litellm/__init__.py +0 -0
- ai_runtime/adapters/litellm/response_adapter.py +58 -0
- ai_runtime/agents/__init__.py +12 -0
- ai_runtime/agents/agent.py +62 -0
- ai_runtime/agents/config_files.py +39 -0
- ai_runtime/agents/runner.py +177 -0
- ai_runtime/agents/subagent.py +31 -0
- ai_runtime/cli.py +81 -0
- ai_runtime/commands/__init__.py +5 -0
- ai_runtime/commands/registry.py +62 -0
- ai_runtime/compat.py +32 -0
- ai_runtime/context/__init__.py +15 -0
- ai_runtime/context/window.py +110 -0
- ai_runtime/conversation/__init__.py +5 -0
- ai_runtime/conversation/conversation.py +33 -0
- ai_runtime/conversation/enums.py +8 -0
- ai_runtime/conversation/message.py +75 -0
- ai_runtime/conversation/request.py +38 -0
- ai_runtime/conversation/response.py +32 -0
- ai_runtime/conversation/usage.py +9 -0
- ai_runtime/eventing/__init__.py +1 -0
- ai_runtime/eventing/bus.py +34 -0
- ai_runtime/execution/__init__.py +8 -0
- ai_runtime/execution/background.py +90 -0
- ai_runtime/execution/context.py +92 -0
- ai_runtime/execution/engine.py +195 -0
- ai_runtime/execution/event_processor.py +88 -0
- ai_runtime/execution/hooks.py +79 -0
- ai_runtime/execution/mode.py +8 -0
- ai_runtime/execution/pipeline/__init__.py +13 -0
- ai_runtime/execution/pipeline/compaction_stage.py +60 -0
- ai_runtime/execution/pipeline/llm_stage.py +51 -0
- ai_runtime/execution/pipeline/memory_consolidation_stage.py +76 -0
- ai_runtime/execution/pipeline/pipeline.py +39 -0
- ai_runtime/execution/pipeline/planner_stage.py +132 -0
- ai_runtime/execution/pipeline/request_builder_stage.py +28 -0
- ai_runtime/execution/pipeline/stage.py +20 -0
- ai_runtime/execution/pipeline/supervisor_stage.py +89 -0
- ai_runtime/execution/pipeline/tool_loop_stage.py +242 -0
- ai_runtime/execution/plan.py +39 -0
- ai_runtime/mcp/__init__.py +13 -0
- ai_runtime/mcp/adapter.py +48 -0
- ai_runtime/mcp/client.py +110 -0
- ai_runtime/memory/__init__.py +12 -0
- ai_runtime/memory/conversation_memory.py +42 -0
- ai_runtime/memory/semantic.py +55 -0
- ai_runtime/memory/store.py +48 -0
- ai_runtime/providers/__init__.py +17 -0
- ai_runtime/providers/base.py +89 -0
- ai_runtime/providers/capabilities.py +22 -0
- ai_runtime/providers/config.py +68 -0
- ai_runtime/providers/default_registry.py +41 -0
- ai_runtime/providers/enums.py +9 -0
- ai_runtime/providers/exceptions.py +54 -0
- ai_runtime/providers/litellm_exception_mapper.py +49 -0
- ai_runtime/providers/litellm_mapper.py +104 -0
- ai_runtime/providers/litellm_provider.py +158 -0
- ai_runtime/providers/litellm_stream_parser.py +100 -0
- ai_runtime/providers/provider.py +35 -0
- ai_runtime/providers/provider_info.py +18 -0
- ai_runtime/providers/registry.py +78 -0
- ai_runtime/providers/sdk_info.py +7 -0
- ai_runtime/rag/__init__.py +12 -0
- ai_runtime/rag/document.py +13 -0
- ai_runtime/rag/retriever.py +41 -0
- ai_runtime/rag/vector_store.py +90 -0
- ai_runtime/runtime.py +81 -0
- ai_runtime/server/__init__.py +13 -0
- ai_runtime/server/agent_server.py +111 -0
- ai_runtime/server/protocol.py +54 -0
- ai_runtime/session/__init__.py +1 -0
- ai_runtime/session/session.py +31 -0
- ai_runtime/skills/__init__.py +10 -0
- ai_runtime/skills/registry.py +48 -0
- ai_runtime/skills/skill.py +26 -0
- ai_runtime/streaming/__init__.py +25 -0
- ai_runtime/streaming/completed.py +19 -0
- ai_runtime/streaming/enums.py +13 -0
- ai_runtime/streaming/error.py +16 -0
- ai_runtime/streaming/event.py +18 -0
- ai_runtime/streaming/factory.py +42 -0
- ai_runtime/streaming/finish_reason.py +9 -0
- ai_runtime/streaming/permission.py +23 -0
- ai_runtime/streaming/text.py +18 -0
- ai_runtime/streaming/thinking.py +18 -0
- ai_runtime/streaming/tool_call.py +21 -0
- ai_runtime/streaming/tool_result.py +29 -0
- ai_runtime/streaming/usage.py +19 -0
- ai_runtime/tools/__init__.py +26 -0
- ai_runtime/tools/adapters/function_adapter.py +26 -0
- ai_runtime/tools/builtin/__init__.py +31 -0
- ai_runtime/tools/builtin/file_tools.py +135 -0
- ai_runtime/tools/builtin/shell_tool.py +45 -0
- ai_runtime/tools/checkpoints.py +50 -0
- ai_runtime/tools/executor.py +47 -0
- ai_runtime/tools/guarded_executor.py +63 -0
- ai_runtime/tools/permissions.py +77 -0
- ai_runtime/tools/registry.py +23 -0
- ai_runtime/tools/tool.py +27 -0
- ai_runtime/version.py +1 -0
- ai_runtime/workspace/__init__.py +5 -0
- ai_runtime/workspace/project.py +51 -0
- forge_ai_runtime-0.7.0.dist-info/METADATA +351 -0
- forge_ai_runtime-0.7.0.dist-info/RECORD +110 -0
- forge_ai_runtime-0.7.0.dist-info/WHEEL +5 -0
- forge_ai_runtime-0.7.0.dist-info/entry_points.txt +2 -0
- forge_ai_runtime-0.7.0.dist-info/licenses/LICENSE +0 -0
- forge_ai_runtime-0.7.0.dist-info/top_level.txt +1 -0
ai_runtime/__init__.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from .runtime import AgentRuntime
|
|
2
|
+
from .version import __version__
|
|
3
|
+
|
|
4
|
+
from .context import ContextWindow
|
|
5
|
+
from .memory import (
|
|
6
|
+
MemoryStore,
|
|
7
|
+
InMemoryStore,
|
|
8
|
+
ConversationMemory,
|
|
9
|
+
SemanticMemory,
|
|
10
|
+
)
|
|
11
|
+
from .rag import Document, VectorStore, InMemoryVectorStore, Retriever
|
|
12
|
+
from .skills import Skill, SkillRegistry, ComposedSkills
|
|
13
|
+
from .agents import Agent, AgentRunner
|
|
14
|
+
from .tools import Tool, ToolResult, ToolRegistry, ToolExecutor, FunctionTool
|
|
15
|
+
from .execution.plan import Plan
|
|
16
|
+
from .execution.hooks import HookRegistry, HookEvent, HookContext, HookResult
|
|
17
|
+
from .mcp import MCPClient, StdioTransport, MCPTool, register_mcp_tools
|
|
18
|
+
from .execution.background import BackgroundTaskRegistry, BackgroundTask
|
|
19
|
+
from .agents.subagent import SubAgentSpec, SubAgentResult
|
|
20
|
+
from .tools.permissions import (
|
|
21
|
+
PermissionPolicy,
|
|
22
|
+
PermissionRule,
|
|
23
|
+
PermissionDecision,
|
|
24
|
+
PermissionError,
|
|
25
|
+
)
|
|
26
|
+
from .tools.guarded_executor import GuardedToolExecutor
|
|
27
|
+
from .tools.builtin import (
|
|
28
|
+
ReadFileTool,
|
|
29
|
+
WriteFileTool,
|
|
30
|
+
EditFileTool,
|
|
31
|
+
GlobTool,
|
|
32
|
+
GrepTool,
|
|
33
|
+
BashTool,
|
|
34
|
+
register_builtin_tools,
|
|
35
|
+
)
|
|
36
|
+
from .tools.checkpoints import CheckpointManager, Checkpoint
|
|
37
|
+
from .agents.config_files import load_project_instructions, discover_instructions
|
|
38
|
+
from .server import AgentServer, AgentRequest, AgentResponse
|
|
39
|
+
from .workspace import Project
|
|
40
|
+
from .commands import CommandRegistry, Command, default_commands
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"AgentRuntime",
|
|
44
|
+
"__version__",
|
|
45
|
+
"ContextWindow",
|
|
46
|
+
"MemoryStore",
|
|
47
|
+
"InMemoryStore",
|
|
48
|
+
"ConversationMemory",
|
|
49
|
+
"SemanticMemory",
|
|
50
|
+
"Document",
|
|
51
|
+
"VectorStore",
|
|
52
|
+
"InMemoryVectorStore",
|
|
53
|
+
"Retriever",
|
|
54
|
+
"Skill",
|
|
55
|
+
"SkillRegistry",
|
|
56
|
+
"ComposedSkills",
|
|
57
|
+
"Agent",
|
|
58
|
+
"AgentRunner",
|
|
59
|
+
"SubAgentSpec",
|
|
60
|
+
"SubAgentResult",
|
|
61
|
+
"Tool",
|
|
62
|
+
"ToolResult",
|
|
63
|
+
"ToolRegistry",
|
|
64
|
+
"ToolExecutor",
|
|
65
|
+
"FunctionTool",
|
|
66
|
+
"ReadFileTool",
|
|
67
|
+
"WriteFileTool",
|
|
68
|
+
"EditFileTool",
|
|
69
|
+
"GlobTool",
|
|
70
|
+
"GrepTool",
|
|
71
|
+
"BashTool",
|
|
72
|
+
"register_builtin_tools",
|
|
73
|
+
"CheckpointManager",
|
|
74
|
+
"Checkpoint",
|
|
75
|
+
"PermissionPolicy",
|
|
76
|
+
"PermissionRule",
|
|
77
|
+
"PermissionDecision",
|
|
78
|
+
"PermissionError",
|
|
79
|
+
"GuardedToolExecutor",
|
|
80
|
+
"load_project_instructions",
|
|
81
|
+
"discover_instructions",
|
|
82
|
+
"Plan",
|
|
83
|
+
"HookRegistry",
|
|
84
|
+
"HookEvent",
|
|
85
|
+
"HookContext",
|
|
86
|
+
"HookResult",
|
|
87
|
+
"MCPClient",
|
|
88
|
+
"StdioTransport",
|
|
89
|
+
"MCPTool",
|
|
90
|
+
"register_mcp_tools",
|
|
91
|
+
"BackgroundTaskRegistry",
|
|
92
|
+
"BackgroundTask",
|
|
93
|
+
"AgentServer",
|
|
94
|
+
"AgentRequest",
|
|
95
|
+
"AgentResponse",
|
|
96
|
+
"Project",
|
|
97
|
+
"CommandRegistry",
|
|
98
|
+
"Command",
|
|
99
|
+
"default_commands",
|
|
100
|
+
]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from ai_runtime.conversation import (
|
|
4
|
+
ChatResponse,
|
|
5
|
+
ChatMessage,
|
|
6
|
+
ToolCall,
|
|
7
|
+
Usage,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LiteLLMResponseAdapter:
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def from_response(response: Any) -> ChatResponse:
|
|
15
|
+
choice = response.choices[0]
|
|
16
|
+
message = choice.message
|
|
17
|
+
|
|
18
|
+
tool_calls = LiteLLMResponseAdapter.to_tool_calls(
|
|
19
|
+
getattr(message, "tool_calls", None)
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
return ChatResponse.assistant(
|
|
23
|
+
getattr(message, "content", None) or "",
|
|
24
|
+
usage=LiteLLMResponseAdapter.to_usage(
|
|
25
|
+
getattr(response, "usage", None)
|
|
26
|
+
),
|
|
27
|
+
finish_reason=getattr(choice, "finish_reason", None),
|
|
28
|
+
tool_calls=tool_calls or None,
|
|
29
|
+
raw=response,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def to_tool_calls(raw_calls: Any) -> list[ToolCall]:
|
|
34
|
+
if not raw_calls:
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
calls: list[ToolCall] = []
|
|
38
|
+
for rc in raw_calls:
|
|
39
|
+
function = getattr(rc, "function", None)
|
|
40
|
+
calls.append(
|
|
41
|
+
ToolCall(
|
|
42
|
+
id=getattr(rc, "id", ""),
|
|
43
|
+
name=getattr(function, "name", ""),
|
|
44
|
+
arguments=getattr(function, "arguments", ""),
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
return calls
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def to_usage(usage: Any) -> Usage:
|
|
51
|
+
if usage is None:
|
|
52
|
+
return Usage()
|
|
53
|
+
|
|
54
|
+
return Usage(
|
|
55
|
+
prompt_tokens=getattr(usage, "prompt_tokens", 0) or 0,
|
|
56
|
+
completion_tokens=getattr(usage, "completion_tokens", 0) or 0,
|
|
57
|
+
total_tokens=getattr(usage, "total_tokens", 0) or 0,
|
|
58
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Agent subsystem: declarative agents and orchestration runner."""
|
|
2
|
+
|
|
3
|
+
from .agent import Agent
|
|
4
|
+
from .runner import AgentRunner
|
|
5
|
+
from .subagent import SubAgentSpec, SubAgentResult
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Agent",
|
|
9
|
+
"AgentRunner",
|
|
10
|
+
"SubAgentSpec",
|
|
11
|
+
"SubAgentResult",
|
|
12
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ai_runtime.conversation import ChatMessage
|
|
6
|
+
from ai_runtime.memory import MemoryStore, InMemoryStore, ConversationMemory
|
|
7
|
+
from ai_runtime.skills import SkillRegistry, ComposedSkills
|
|
8
|
+
from ai_runtime.tools import ToolRegistry, ToolExecutor
|
|
9
|
+
from .subagent import SubAgentSpec
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Agent:
|
|
13
|
+
"""A configured agent: provider + system prompt + tools + memory + skills.
|
|
14
|
+
|
|
15
|
+
An `Agent` is a declarative specification. Execution is performed by
|
|
16
|
+
`AgentRunner`, which wires the agent's pieces into the runtime's
|
|
17
|
+
execution pipeline (including the tool-call loop).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
name: str,
|
|
23
|
+
provider,
|
|
24
|
+
system_prompt: str | None = None,
|
|
25
|
+
tool_registry: ToolRegistry | None = None,
|
|
26
|
+
memory_store: MemoryStore | None = None,
|
|
27
|
+
skill_registry: SkillRegistry | None = None,
|
|
28
|
+
skills: list[str] | None = None,
|
|
29
|
+
memory: ConversationMemory | None = None,
|
|
30
|
+
sub_agents: list[SubAgentSpec] | None = None,
|
|
31
|
+
):
|
|
32
|
+
self.name = name
|
|
33
|
+
self.provider = provider
|
|
34
|
+
self.system_prompt = system_prompt
|
|
35
|
+
self.tool_registry = tool_registry or ToolRegistry()
|
|
36
|
+
self.tool_executor = ToolExecutor(self.tool_registry)
|
|
37
|
+
self.memory_store = memory_store or InMemoryStore()
|
|
38
|
+
self.memory = memory or ConversationMemory(self.memory_store, name)
|
|
39
|
+
self.skill_registry = skill_registry
|
|
40
|
+
self.skills: ComposedSkills | None = None
|
|
41
|
+
if skill_registry is not None and skills:
|
|
42
|
+
self.skills = skill_registry.compose(skills)
|
|
43
|
+
self.sub_agents = sub_agents or []
|
|
44
|
+
|
|
45
|
+
def effective_system_prompt(self) -> str | None:
|
|
46
|
+
"""Combine the agent prompt with any composed skill prompts."""
|
|
47
|
+
parts: list[str] = []
|
|
48
|
+
if self.system_prompt:
|
|
49
|
+
parts.append(self.system_prompt)
|
|
50
|
+
if self.skills is not None and self.skills.system_prompt:
|
|
51
|
+
parts.append(self.skills.system_prompt)
|
|
52
|
+
return "\n\n".join(parts) if parts else None
|
|
53
|
+
|
|
54
|
+
def effective_tool_names(self) -> list[str]:
|
|
55
|
+
if self.skills is None:
|
|
56
|
+
return list(self.tool_registry._tools.keys())
|
|
57
|
+
return self.skills.tool_names or list(
|
|
58
|
+
self.tool_registry._tools.keys()
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
async def ensure_memory_loaded(self) -> None:
|
|
62
|
+
await self.memory.load()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Candidate instruction-file locations, checked in priority order.
|
|
8
|
+
INSTRUCTION_CANDIDATES = [
|
|
9
|
+
".github/copilot-instructions.md",
|
|
10
|
+
"AGENTS.md",
|
|
11
|
+
"CLAUDE.md",
|
|
12
|
+
".cursor/rules",
|
|
13
|
+
"ai-runtime.md",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def discover_instructions(project_root: str) -> list[str]:
|
|
18
|
+
"""Return the contents of discovered agent instruction files.
|
|
19
|
+
|
|
20
|
+
Mirrors the instruction-file discovery of Copilot (`copilot-instructions.md`),
|
|
21
|
+
Codex/Claude (`AGENTS.md` / `CLAUDE.md`), and Cursor (`.cursor/rules`).
|
|
22
|
+
Returns a list of markdown text blocks to prepend to the system prompt.
|
|
23
|
+
"""
|
|
24
|
+
root = Path(project_root)
|
|
25
|
+
blocks: list[str] = []
|
|
26
|
+
for rel in INSTRUCTION_CANDIDATES:
|
|
27
|
+
path = root / rel
|
|
28
|
+
if path.is_file():
|
|
29
|
+
blocks.append(path.read_text(encoding="utf-8", errors="replace"))
|
|
30
|
+
elif path.is_dir():
|
|
31
|
+
for f in sorted(path.glob("*.mdc")) + sorted(path.glob("*.md")):
|
|
32
|
+
blocks.append(f.read_text(encoding="utf-8", errors="replace"))
|
|
33
|
+
return blocks
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_project_instructions(project_root: str) -> str | None:
|
|
37
|
+
"""Concatenate discovered instruction files into a single system block."""
|
|
38
|
+
blocks = discover_instructions(project_root)
|
|
39
|
+
return "\n\n---\n\n".join(blocks) if blocks else None
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from ai_runtime.conversation import ChatMessage, ChatRequest
|
|
6
|
+
from ai_runtime.execution.plan import Plan
|
|
7
|
+
from ai_runtime.streaming import StreamEvent
|
|
8
|
+
from collections.abc import AsyncIterator
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from ai_runtime.execution import ExecutionEngine
|
|
12
|
+
|
|
13
|
+
from ai_runtime.execution.context import ExecutionContext
|
|
14
|
+
|
|
15
|
+
from .agent import Agent
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AgentRunner:
|
|
19
|
+
"""Orchestrates agent execution over the runtime pipeline.
|
|
20
|
+
|
|
21
|
+
For each `run`, the runner:
|
|
22
|
+
1. Loads persisted conversation memory.
|
|
23
|
+
2. Prepends the agent's (skill-composed) system prompt.
|
|
24
|
+
3. Executes via `ExecutionEngine` (which includes the tool-call loop).
|
|
25
|
+
4. Persists the updated conversation back to memory.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, agent: Agent, engine: ExecutionEngine | None = None):
|
|
29
|
+
if engine is None:
|
|
30
|
+
from ai_runtime.execution import ExecutionEngine
|
|
31
|
+
|
|
32
|
+
engine = ExecutionEngine()
|
|
33
|
+
self.agent = agent
|
|
34
|
+
self.engine = engine
|
|
35
|
+
self.last_plan: Plan | None = None
|
|
36
|
+
self.last_plan_text: str = ""
|
|
37
|
+
|
|
38
|
+
async def run(
|
|
39
|
+
self,
|
|
40
|
+
message: str | ChatMessage | ChatRequest,
|
|
41
|
+
system_prompt: str | None = None,
|
|
42
|
+
):
|
|
43
|
+
await self.agent.ensure_memory_loaded()
|
|
44
|
+
|
|
45
|
+
context = ExecutionContext(
|
|
46
|
+
provider=self.agent.provider,
|
|
47
|
+
tool_executor=self.agent.tool_executor,
|
|
48
|
+
)
|
|
49
|
+
context.agent = self.agent
|
|
50
|
+
context._engine = self.engine
|
|
51
|
+
|
|
52
|
+
# Seed conversation with system prompt + persisted history.
|
|
53
|
+
prompt = system_prompt or self.agent.effective_system_prompt()
|
|
54
|
+
if prompt:
|
|
55
|
+
context.conversation.add(ChatMessage.system(prompt))
|
|
56
|
+
for msg in self.agent.memory.conversation.messages:
|
|
57
|
+
context.conversation.add(msg)
|
|
58
|
+
|
|
59
|
+
if isinstance(message, str):
|
|
60
|
+
user_msg = ChatMessage.user(message)
|
|
61
|
+
elif isinstance(message, ChatMessage):
|
|
62
|
+
user_msg = message
|
|
63
|
+
else:
|
|
64
|
+
user_msg = ChatMessage.user(message.messages[-1].content)
|
|
65
|
+
|
|
66
|
+
response = await self.engine.chat(context, user_msg)
|
|
67
|
+
|
|
68
|
+
# Persist the new turns (exclude the prepended system prompt).
|
|
69
|
+
if prompt:
|
|
70
|
+
persisted = context.conversation.copy()
|
|
71
|
+
persisted.messages = persisted.messages[1:]
|
|
72
|
+
self.agent.memory._conversation = persisted
|
|
73
|
+
else:
|
|
74
|
+
self.agent.memory._conversation = context.conversation.copy()
|
|
75
|
+
await self.agent.memory.save()
|
|
76
|
+
|
|
77
|
+
return response
|
|
78
|
+
|
|
79
|
+
async def stream(
|
|
80
|
+
self,
|
|
81
|
+
message: str | ChatMessage | ChatRequest,
|
|
82
|
+
system_prompt: str | None = None,
|
|
83
|
+
):
|
|
84
|
+
await self.agent.ensure_memory_loaded()
|
|
85
|
+
|
|
86
|
+
context = ExecutionContext(
|
|
87
|
+
provider=self.agent.provider,
|
|
88
|
+
tool_executor=self.agent.tool_executor,
|
|
89
|
+
)
|
|
90
|
+
context.agent = self.agent
|
|
91
|
+
context._engine = self.engine
|
|
92
|
+
|
|
93
|
+
prompt = system_prompt or self.agent.effective_system_prompt()
|
|
94
|
+
if prompt:
|
|
95
|
+
context.conversation.add(ChatMessage.system(prompt))
|
|
96
|
+
for msg in self.agent.memory.conversation.messages:
|
|
97
|
+
context.conversation.add(msg)
|
|
98
|
+
|
|
99
|
+
if isinstance(message, str):
|
|
100
|
+
user_msg = ChatMessage.user(message)
|
|
101
|
+
elif isinstance(message, ChatMessage):
|
|
102
|
+
user_msg = message
|
|
103
|
+
else:
|
|
104
|
+
user_msg = ChatMessage.user(message.messages[-1].content)
|
|
105
|
+
|
|
106
|
+
async for event in self.engine.stream(context, user_msg):
|
|
107
|
+
yield event
|
|
108
|
+
|
|
109
|
+
await self.agent.memory.save()
|
|
110
|
+
|
|
111
|
+
async def plan(
|
|
112
|
+
self,
|
|
113
|
+
message: str | ChatMessage | ChatRequest,
|
|
114
|
+
system_prompt: str | None = None,
|
|
115
|
+
) -> Plan:
|
|
116
|
+
"""Produce a reviewable plan for the agent without executing tools."""
|
|
117
|
+
await self.agent.ensure_memory_loaded()
|
|
118
|
+
|
|
119
|
+
context = ExecutionContext(
|
|
120
|
+
provider=self.agent.provider,
|
|
121
|
+
tool_executor=self.agent.tool_executor,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
prompt = system_prompt or self.agent.effective_system_prompt()
|
|
125
|
+
if prompt:
|
|
126
|
+
context.conversation.add(ChatMessage.system(prompt))
|
|
127
|
+
for msg in self.agent.memory.conversation.messages:
|
|
128
|
+
context.conversation.add(msg)
|
|
129
|
+
|
|
130
|
+
if isinstance(message, str):
|
|
131
|
+
user_msg = ChatMessage.user(message)
|
|
132
|
+
elif isinstance(message, ChatMessage):
|
|
133
|
+
user_msg = message
|
|
134
|
+
else:
|
|
135
|
+
user_msg = ChatMessage.user(message.messages[-1].content)
|
|
136
|
+
|
|
137
|
+
return await self.engine.plan(context, user_msg)
|
|
138
|
+
|
|
139
|
+
async def stream_plan(
|
|
140
|
+
self,
|
|
141
|
+
message: str | ChatMessage | ChatRequest,
|
|
142
|
+
system_prompt: str | None = None,
|
|
143
|
+
) -> AsyncIterator[StreamEvent]:
|
|
144
|
+
"""Stream a plan (live thinking + placeholder, then the parsed plan).
|
|
145
|
+
|
|
146
|
+
The planner parses the model's JSON into a `Plan` during streaming, so
|
|
147
|
+
no second LLM call is needed — the parsed plan is available on
|
|
148
|
+
`self.last_plan` afterwards.
|
|
149
|
+
"""
|
|
150
|
+
await self.agent.ensure_memory_loaded()
|
|
151
|
+
|
|
152
|
+
context = ExecutionContext(
|
|
153
|
+
provider=self.agent.provider,
|
|
154
|
+
tool_executor=self.agent.tool_executor,
|
|
155
|
+
)
|
|
156
|
+
context.agent = self.agent
|
|
157
|
+
context._engine = self.engine
|
|
158
|
+
|
|
159
|
+
prompt = system_prompt or self.agent.effective_system_prompt()
|
|
160
|
+
if prompt:
|
|
161
|
+
context.conversation.add(ChatMessage.system(prompt))
|
|
162
|
+
for msg in self.agent.memory.conversation.messages:
|
|
163
|
+
context.conversation.add(msg)
|
|
164
|
+
|
|
165
|
+
if isinstance(message, str):
|
|
166
|
+
user_msg = ChatMessage.user(message)
|
|
167
|
+
elif isinstance(message, ChatMessage):
|
|
168
|
+
user_msg = message
|
|
169
|
+
else:
|
|
170
|
+
user_msg = ChatMessage.user(message.messages[-1].content)
|
|
171
|
+
|
|
172
|
+
async for event in self.engine.stream_plan(context, user_msg):
|
|
173
|
+
yield event
|
|
174
|
+
# Capture the parsed plan and the raw streamed text produced during
|
|
175
|
+
# streaming, so the transport can echo back exactly what was shown.
|
|
176
|
+
self.last_plan = context.plan
|
|
177
|
+
self.last_plan_text = context.assistant_text
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from .agent import Agent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class SubAgentSpec:
|
|
12
|
+
"""Declarative spec for a child agent spawned by a supervisor.
|
|
13
|
+
|
|
14
|
+
Mirrors the sub-agent model of agentic coding tools: a child has its
|
|
15
|
+
own tools, model, and isolation scope, and runs in a separate context
|
|
16
|
+
so its transcript does not pollute the parent's context window.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
name: str
|
|
20
|
+
agent: Agent
|
|
21
|
+
task_template: str = "{task}" # receives `task` kwarg
|
|
22
|
+
max_depth: int = 1 # how many levels of nesting allowed
|
|
23
|
+
isolated: bool = True # run in a fresh conversation context
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class SubAgentResult:
|
|
28
|
+
name: str
|
|
29
|
+
output: str
|
|
30
|
+
success: bool = True
|
|
31
|
+
error: str | None = None
|
ai_runtime/cli.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from ai_runtime.agents import Agent
|
|
9
|
+
from ai_runtime.providers.enums import ProviderType
|
|
10
|
+
from ai_runtime.providers import ProviderConfig
|
|
11
|
+
from ai_runtime.server.agent_server import AgentServer
|
|
12
|
+
from ai_runtime.server.protocol import AgentRequest
|
|
13
|
+
from ai_runtime.tools.builtin import register_builtin_tools
|
|
14
|
+
from ai_runtime.tools import ToolRegistry, GuardedToolExecutor, PermissionPolicy
|
|
15
|
+
from ai_runtime.agents.config_files import load_project_instructions
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _build_agent(args) -> Agent:
|
|
19
|
+
config = ProviderConfig(
|
|
20
|
+
provider=ProviderType(args.provider),
|
|
21
|
+
model=args.model,
|
|
22
|
+
api_key=os.getenv(args.api_key_env),
|
|
23
|
+
reasoning_effort=args.reasoning_effort,
|
|
24
|
+
)
|
|
25
|
+
registry = ToolRegistry()
|
|
26
|
+
register_builtin_tools(registry, base_dir=args.cwd)
|
|
27
|
+
executor = GuardedToolExecutor(
|
|
28
|
+
registry.executor if hasattr(registry, "executor") else __import__(
|
|
29
|
+
"ai_runtime.tools.executor", fromlist=["ToolExecutor"]
|
|
30
|
+
).ToolExecutor(registry),
|
|
31
|
+
PermissionPolicy.permissive() if args.yolo else PermissionPolicy.default(),
|
|
32
|
+
)
|
|
33
|
+
return Agent(
|
|
34
|
+
name="cli-agent",
|
|
35
|
+
provider=config,
|
|
36
|
+
system_prompt=load_project_instructions(args.cwd),
|
|
37
|
+
tool_registry=registry,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def _run(args) -> None:
|
|
42
|
+
agent = _build_agent(args)
|
|
43
|
+
server = AgentServer(agent)
|
|
44
|
+
req = AgentRequest(session_id="cli", message=args.prompt, mode=args.mode)
|
|
45
|
+
if args.mode == "stream":
|
|
46
|
+
async for line in server.stream(req):
|
|
47
|
+
sys.stdout.write(line)
|
|
48
|
+
else:
|
|
49
|
+
resp = await server.handle(req)
|
|
50
|
+
print(resp.content)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main() -> None:
|
|
54
|
+
parser = argparse.ArgumentParser(prog="ai-runtime", description="ai_runtime agent CLI")
|
|
55
|
+
parser.add_argument("prompt", nargs="?", help="Prompt to send to the agent")
|
|
56
|
+
parser.add_argument("--provider", default="openai", help="Provider type")
|
|
57
|
+
parser.add_argument("--model", required=True, help="Model id")
|
|
58
|
+
parser.add_argument("--api-key-env", default="OPENAI_API_KEY", help="Env var with API key")
|
|
59
|
+
parser.add_argument("--mode", default="chat", choices=["chat", "stream", "plan"])
|
|
60
|
+
parser.add_argument("--cwd", default=os.getcwd(), help="Project root / sandbox dir")
|
|
61
|
+
parser.add_argument("--reasoning-effort", default=None, help="low|medium|high")
|
|
62
|
+
parser.add_argument("--yolo", action="store_true", help="Allow all tools without prompting")
|
|
63
|
+
parser.add_argument("--serve", action="store_true", help="Start stdio server")
|
|
64
|
+
parser.add_argument("--http", action="store_true", help="Start HTTP server")
|
|
65
|
+
parser.add_argument("--port", type=int, default=8787)
|
|
66
|
+
args = parser.parse_args()
|
|
67
|
+
|
|
68
|
+
if args.serve:
|
|
69
|
+
agent = _build_agent(args)
|
|
70
|
+
asyncio.run(AgentServer(agent).serve_stdio())
|
|
71
|
+
elif args.http:
|
|
72
|
+
agent = _build_agent(args)
|
|
73
|
+
asyncio.run(AgentServer(agent).serve_http(port=args.port))
|
|
74
|
+
elif args.prompt:
|
|
75
|
+
asyncio.run(_run(args))
|
|
76
|
+
else:
|
|
77
|
+
parser.print_help()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
main()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Callable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Command:
|
|
9
|
+
"""A slash command / prompt file (à la Copilot's `/` menu).
|
|
10
|
+
|
|
11
|
+
A command is a named, reusable prompt (or hook) the user can invoke.
|
|
12
|
+
`render` produces the prompt text sent to the model; `run` optionally
|
|
13
|
+
executes side effects (e.g. `/compact`, `/context`).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
name: str
|
|
17
|
+
description: str
|
|
18
|
+
prompt_template: str | None = None
|
|
19
|
+
run: Callable[[Any], Any] | None = None
|
|
20
|
+
|
|
21
|
+
def render(self, **kwargs: Any) -> str:
|
|
22
|
+
if self.prompt_template:
|
|
23
|
+
try:
|
|
24
|
+
return self.prompt_template.format(**kwargs)
|
|
25
|
+
except (KeyError, IndexError):
|
|
26
|
+
return self.prompt_template
|
|
27
|
+
return self.description
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CommandRegistry:
|
|
31
|
+
"""Registry of slash commands / prompt files."""
|
|
32
|
+
|
|
33
|
+
def __init__(self):
|
|
34
|
+
self._commands: dict[str, Command] = {}
|
|
35
|
+
|
|
36
|
+
def register(self, command: Command) -> None:
|
|
37
|
+
self._commands[command.name] = command
|
|
38
|
+
|
|
39
|
+
def get(self, name: str) -> Command | None:
|
|
40
|
+
return self._commands.get(name)
|
|
41
|
+
|
|
42
|
+
def list(self) -> list[Command]:
|
|
43
|
+
return list(self._commands.values())
|
|
44
|
+
|
|
45
|
+
def render(self, name: str, **kwargs: Any) -> str | None:
|
|
46
|
+
cmd = self._commands.get(name)
|
|
47
|
+
return cmd.render(**kwargs) if cmd else None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Built-in commands mirroring Copilot/Claude/Cursor slash commands.
|
|
51
|
+
def default_commands() -> CommandRegistry:
|
|
52
|
+
reg = CommandRegistry()
|
|
53
|
+
reg.register(
|
|
54
|
+
Command("compact", "Summarize the conversation to free context", "Please summarize the conversation so far concisely.")
|
|
55
|
+
)
|
|
56
|
+
reg.register(
|
|
57
|
+
Command("context", "Show a token/context breakdown", "List the current context window usage.")
|
|
58
|
+
)
|
|
59
|
+
reg.register(
|
|
60
|
+
Command("clear", "Clear the conversation history", "Clear all messages from the current session.")
|
|
61
|
+
)
|
|
62
|
+
return reg
|
ai_runtime/compat.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Backwards-compatibility shims.
|
|
2
|
+
|
|
3
|
+
This module preserves names that were renamed or moved in previous releases
|
|
4
|
+
so that existing imports keep working. Deprecated aliases emit a warning and
|
|
5
|
+
will be removed in a future major version.
|
|
6
|
+
|
|
7
|
+
Renames:
|
|
8
|
+
- ``ChatSession`` -> :class:`ai_runtime.session.Session`
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import warnings
|
|
13
|
+
|
|
14
|
+
from .session import Session
|
|
15
|
+
|
|
16
|
+
_DEPRECATION_MSG = (
|
|
17
|
+
"{old} is deprecated and will be removed in a future major version. "
|
|
18
|
+
"Use {new} instead."
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def __getattr__(name: str):
|
|
23
|
+
"""Lazy deprecation shim for renamed symbols."""
|
|
24
|
+
if name == "ChatSession":
|
|
25
|
+
warnings.warn(
|
|
26
|
+
_DEPRECATION_MSG.format(old="ChatSession", new="Session"),
|
|
27
|
+
DeprecationWarning,
|
|
28
|
+
stacklevel=2,
|
|
29
|
+
)
|
|
30
|
+
return Session
|
|
31
|
+
|
|
32
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Context management: token budgeting, truncation, and summarization."""
|
|
2
|
+
|
|
3
|
+
from .window import (
|
|
4
|
+
ContextWindow,
|
|
5
|
+
DropOldestStrategy,
|
|
6
|
+
TruncationStrategy,
|
|
7
|
+
estimate_tokens,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ContextWindow",
|
|
12
|
+
"TruncationStrategy",
|
|
13
|
+
"DropOldestStrategy",
|
|
14
|
+
"estimate_tokens",
|
|
15
|
+
]
|