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.
Files changed (110) hide show
  1. ai_runtime/__init__.py +100 -0
  2. ai_runtime/adapters/__init__.py +0 -0
  3. ai_runtime/adapters/litellm/__init__.py +0 -0
  4. ai_runtime/adapters/litellm/response_adapter.py +58 -0
  5. ai_runtime/agents/__init__.py +12 -0
  6. ai_runtime/agents/agent.py +62 -0
  7. ai_runtime/agents/config_files.py +39 -0
  8. ai_runtime/agents/runner.py +177 -0
  9. ai_runtime/agents/subagent.py +31 -0
  10. ai_runtime/cli.py +81 -0
  11. ai_runtime/commands/__init__.py +5 -0
  12. ai_runtime/commands/registry.py +62 -0
  13. ai_runtime/compat.py +32 -0
  14. ai_runtime/context/__init__.py +15 -0
  15. ai_runtime/context/window.py +110 -0
  16. ai_runtime/conversation/__init__.py +5 -0
  17. ai_runtime/conversation/conversation.py +33 -0
  18. ai_runtime/conversation/enums.py +8 -0
  19. ai_runtime/conversation/message.py +75 -0
  20. ai_runtime/conversation/request.py +38 -0
  21. ai_runtime/conversation/response.py +32 -0
  22. ai_runtime/conversation/usage.py +9 -0
  23. ai_runtime/eventing/__init__.py +1 -0
  24. ai_runtime/eventing/bus.py +34 -0
  25. ai_runtime/execution/__init__.py +8 -0
  26. ai_runtime/execution/background.py +90 -0
  27. ai_runtime/execution/context.py +92 -0
  28. ai_runtime/execution/engine.py +195 -0
  29. ai_runtime/execution/event_processor.py +88 -0
  30. ai_runtime/execution/hooks.py +79 -0
  31. ai_runtime/execution/mode.py +8 -0
  32. ai_runtime/execution/pipeline/__init__.py +13 -0
  33. ai_runtime/execution/pipeline/compaction_stage.py +60 -0
  34. ai_runtime/execution/pipeline/llm_stage.py +51 -0
  35. ai_runtime/execution/pipeline/memory_consolidation_stage.py +76 -0
  36. ai_runtime/execution/pipeline/pipeline.py +39 -0
  37. ai_runtime/execution/pipeline/planner_stage.py +132 -0
  38. ai_runtime/execution/pipeline/request_builder_stage.py +28 -0
  39. ai_runtime/execution/pipeline/stage.py +20 -0
  40. ai_runtime/execution/pipeline/supervisor_stage.py +89 -0
  41. ai_runtime/execution/pipeline/tool_loop_stage.py +242 -0
  42. ai_runtime/execution/plan.py +39 -0
  43. ai_runtime/mcp/__init__.py +13 -0
  44. ai_runtime/mcp/adapter.py +48 -0
  45. ai_runtime/mcp/client.py +110 -0
  46. ai_runtime/memory/__init__.py +12 -0
  47. ai_runtime/memory/conversation_memory.py +42 -0
  48. ai_runtime/memory/semantic.py +55 -0
  49. ai_runtime/memory/store.py +48 -0
  50. ai_runtime/providers/__init__.py +17 -0
  51. ai_runtime/providers/base.py +89 -0
  52. ai_runtime/providers/capabilities.py +22 -0
  53. ai_runtime/providers/config.py +68 -0
  54. ai_runtime/providers/default_registry.py +41 -0
  55. ai_runtime/providers/enums.py +9 -0
  56. ai_runtime/providers/exceptions.py +54 -0
  57. ai_runtime/providers/litellm_exception_mapper.py +49 -0
  58. ai_runtime/providers/litellm_mapper.py +104 -0
  59. ai_runtime/providers/litellm_provider.py +158 -0
  60. ai_runtime/providers/litellm_stream_parser.py +100 -0
  61. ai_runtime/providers/provider.py +35 -0
  62. ai_runtime/providers/provider_info.py +18 -0
  63. ai_runtime/providers/registry.py +78 -0
  64. ai_runtime/providers/sdk_info.py +7 -0
  65. ai_runtime/rag/__init__.py +12 -0
  66. ai_runtime/rag/document.py +13 -0
  67. ai_runtime/rag/retriever.py +41 -0
  68. ai_runtime/rag/vector_store.py +90 -0
  69. ai_runtime/runtime.py +81 -0
  70. ai_runtime/server/__init__.py +13 -0
  71. ai_runtime/server/agent_server.py +111 -0
  72. ai_runtime/server/protocol.py +54 -0
  73. ai_runtime/session/__init__.py +1 -0
  74. ai_runtime/session/session.py +31 -0
  75. ai_runtime/skills/__init__.py +10 -0
  76. ai_runtime/skills/registry.py +48 -0
  77. ai_runtime/skills/skill.py +26 -0
  78. ai_runtime/streaming/__init__.py +25 -0
  79. ai_runtime/streaming/completed.py +19 -0
  80. ai_runtime/streaming/enums.py +13 -0
  81. ai_runtime/streaming/error.py +16 -0
  82. ai_runtime/streaming/event.py +18 -0
  83. ai_runtime/streaming/factory.py +42 -0
  84. ai_runtime/streaming/finish_reason.py +9 -0
  85. ai_runtime/streaming/permission.py +23 -0
  86. ai_runtime/streaming/text.py +18 -0
  87. ai_runtime/streaming/thinking.py +18 -0
  88. ai_runtime/streaming/tool_call.py +21 -0
  89. ai_runtime/streaming/tool_result.py +29 -0
  90. ai_runtime/streaming/usage.py +19 -0
  91. ai_runtime/tools/__init__.py +26 -0
  92. ai_runtime/tools/adapters/function_adapter.py +26 -0
  93. ai_runtime/tools/builtin/__init__.py +31 -0
  94. ai_runtime/tools/builtin/file_tools.py +135 -0
  95. ai_runtime/tools/builtin/shell_tool.py +45 -0
  96. ai_runtime/tools/checkpoints.py +50 -0
  97. ai_runtime/tools/executor.py +47 -0
  98. ai_runtime/tools/guarded_executor.py +63 -0
  99. ai_runtime/tools/permissions.py +77 -0
  100. ai_runtime/tools/registry.py +23 -0
  101. ai_runtime/tools/tool.py +27 -0
  102. ai_runtime/version.py +1 -0
  103. ai_runtime/workspace/__init__.py +5 -0
  104. ai_runtime/workspace/project.py +51 -0
  105. forge_ai_runtime-0.7.0.dist-info/METADATA +351 -0
  106. forge_ai_runtime-0.7.0.dist-info/RECORD +110 -0
  107. forge_ai_runtime-0.7.0.dist-info/WHEEL +5 -0
  108. forge_ai_runtime-0.7.0.dist-info/entry_points.txt +2 -0
  109. forge_ai_runtime-0.7.0.dist-info/licenses/LICENSE +0 -0
  110. 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,5 @@
1
+ """Slash-command / prompt-file registry (à la Copilot's `/` menu)."""
2
+
3
+ from .registry import Command, CommandRegistry, default_commands
4
+
5
+ __all__ = ["Command", "CommandRegistry", "default_commands"]
@@ -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
+ ]