friday-framework-runtime 0.1.0a0__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.
- friday_framework_runtime-0.1.0a0.dist-info/METADATA +45 -0
- friday_framework_runtime-0.1.0a0.dist-info/RECORD +8 -0
- friday_framework_runtime-0.1.0a0.dist-info/WHEEL +4 -0
- friday_runtime/__init__.py +15 -0
- friday_runtime/config.py +1681 -0
- friday_runtime/di.py +342 -0
- friday_runtime/kernel.py +673 -0
- friday_runtime/tool_loading.py +177 -0
friday_runtime/kernel.py
ADDED
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Runtime Kernel — the headless operating system for Friday.
|
|
3
|
+
|
|
4
|
+
RuntimeKernel implements SystemRuntime and manages the full lifecycle
|
|
5
|
+
of framework services. It can be used without the CLI presentation
|
|
6
|
+
layer for headless execution, testing, and embedding.
|
|
7
|
+
|
|
8
|
+
Agent subsystem properties (agent_loader, agent_registry, orchestrator)
|
|
9
|
+
are lazy-initialized on first access, preserving the existing error
|
|
10
|
+
semantics from FridayContainer.
|
|
11
|
+
|
|
12
|
+
Ref: Phase 1 Build Plan, Step 4.3
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import uuid
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, cast
|
|
20
|
+
|
|
21
|
+
from friday_agent import (
|
|
22
|
+
AgentLoader,
|
|
23
|
+
AgentOrchestrator,
|
|
24
|
+
AgentProfile,
|
|
25
|
+
AgentRegistry,
|
|
26
|
+
AutonomousAgent,
|
|
27
|
+
CopilotAgent,
|
|
28
|
+
DefaultToolRegistry,
|
|
29
|
+
InteractiveAgent,
|
|
30
|
+
)
|
|
31
|
+
from friday_core.interfaces import ToolRegistry
|
|
32
|
+
from friday_core.interfaces.runtime import SystemRuntime
|
|
33
|
+
from friday_core.profiles import MemoryProfile
|
|
34
|
+
from friday_core.security import SanitizationConfig
|
|
35
|
+
|
|
36
|
+
from friday_runtime.config import RuntimeConfig
|
|
37
|
+
from friday_runtime.di import DIContainer
|
|
38
|
+
from friday_runtime.tool_loading import (
|
|
39
|
+
collect_profile_tool_references,
|
|
40
|
+
validate_agent_tool_bindings,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class RuntimeKernel(SystemRuntime):
|
|
47
|
+
"""
|
|
48
|
+
Headless runtime kernel for the Friday Agent Framework.
|
|
49
|
+
|
|
50
|
+
Manages service lifecycle (LLM, memory, tools, telemetry) and
|
|
51
|
+
provides agent spawning and workflow execution. Does not depend
|
|
52
|
+
on any CLI or presentation layer.
|
|
53
|
+
|
|
54
|
+
Usage:
|
|
55
|
+
kernel = RuntimeKernel(
|
|
56
|
+
config=RuntimeConfig.from_env(),
|
|
57
|
+
memory_profile=MemoryProfile.FULL,
|
|
58
|
+
)
|
|
59
|
+
await kernel.start()
|
|
60
|
+
|
|
61
|
+
agent = kernel.spawn_agent("researcher")
|
|
62
|
+
result = await agent.run("Find data about Python parsers")
|
|
63
|
+
|
|
64
|
+
await kernel.stop()
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
config: RuntimeConfig | None = None,
|
|
70
|
+
memory_profile: MemoryProfile = MemoryProfile.FULL,
|
|
71
|
+
sanitization_config: SanitizationConfig | None = None,
|
|
72
|
+
) -> None:
|
|
73
|
+
"""
|
|
74
|
+
Initialize the runtime kernel.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
config: Runtime configuration (defaults to env-based).
|
|
78
|
+
memory_profile: Memory storage profile.
|
|
79
|
+
sanitization_config: Optional output sanitization config,
|
|
80
|
+
forwarded to CopilotAgent on creation.
|
|
81
|
+
"""
|
|
82
|
+
self._config = config or RuntimeConfig.from_env()
|
|
83
|
+
self._memory_profile = memory_profile
|
|
84
|
+
self._sanitization_config = sanitization_config
|
|
85
|
+
|
|
86
|
+
self._di = DIContainer(self._config, self._memory_profile)
|
|
87
|
+
self._initialized = False
|
|
88
|
+
|
|
89
|
+
# Lazy agent subsystem
|
|
90
|
+
self._agent_loader: AgentLoader | None = None
|
|
91
|
+
self._agent_registry: AgentRegistry | None = None
|
|
92
|
+
self._orchestrator: AgentOrchestrator | None = None
|
|
93
|
+
self._agent_factories: dict[str, Callable[[AgentProfile], Any]] = {
|
|
94
|
+
"interactive": self._create_interactive_agent,
|
|
95
|
+
"autonomous": self._create_autonomous_agent,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# --- SystemRuntime Implementation ---
|
|
99
|
+
|
|
100
|
+
async def start(self) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Initialize all eager services (LLM, memory, tools, telemetry).
|
|
103
|
+
|
|
104
|
+
Must be called before any other operations.
|
|
105
|
+
"""
|
|
106
|
+
if self._initialized:
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
logger.info("Starting RuntimeKernel...")
|
|
110
|
+
await self._di.boot()
|
|
111
|
+
try:
|
|
112
|
+
self._agent_registry = self._build_agent_registry()
|
|
113
|
+
except Exception:
|
|
114
|
+
await self._di.shutdown()
|
|
115
|
+
raise
|
|
116
|
+
self._initialized = True
|
|
117
|
+
logger.info("RuntimeKernel started successfully")
|
|
118
|
+
|
|
119
|
+
async def stop(self) -> None:
|
|
120
|
+
"""
|
|
121
|
+
Gracefully shut down all services.
|
|
122
|
+
|
|
123
|
+
Flushes logs, persists memory state, and releases resources.
|
|
124
|
+
Safe to call multiple times.
|
|
125
|
+
"""
|
|
126
|
+
logger.info("Stopping RuntimeKernel...")
|
|
127
|
+
await self._di.shutdown()
|
|
128
|
+
self._initialized = False
|
|
129
|
+
logger.info("RuntimeKernel stopped")
|
|
130
|
+
|
|
131
|
+
async def create_session(self, user_id: str = "default") -> str:
|
|
132
|
+
"""
|
|
133
|
+
Create a new session identifier.
|
|
134
|
+
|
|
135
|
+
Phase 1 stub: returns a UUID string without persistent
|
|
136
|
+
session tracking.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
user_id: User identifier for the session.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
A unique session ID string.
|
|
143
|
+
"""
|
|
144
|
+
session_id = str(uuid.uuid4())
|
|
145
|
+
logger.debug(f"Created session {session_id} for user {user_id}")
|
|
146
|
+
return session_id
|
|
147
|
+
|
|
148
|
+
def spawn_agent(self, name: str) -> Any:
|
|
149
|
+
"""
|
|
150
|
+
Create an agent instance by name from the registry.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
name: Registered agent name.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Configured agent instance.
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
KeyError: If no agent profile exists with the given name.
|
|
160
|
+
RuntimeError: If kernel not started or no factory configured.
|
|
161
|
+
"""
|
|
162
|
+
self._ensure_initialized()
|
|
163
|
+
return self.agent_registry.create_agent(name)
|
|
164
|
+
|
|
165
|
+
async def execute_workflow(
|
|
166
|
+
self,
|
|
167
|
+
workflow_name: str,
|
|
168
|
+
initial_inputs: dict[str, Any] | None = None,
|
|
169
|
+
) -> Any:
|
|
170
|
+
"""
|
|
171
|
+
Load and execute a named workflow definition.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
workflow_name: Name of the workflow to execute.
|
|
175
|
+
initial_inputs: Optional initial variable bindings.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
WorkflowExecution with final state.
|
|
179
|
+
"""
|
|
180
|
+
self._ensure_initialized()
|
|
181
|
+
from friday_agent.workflows import WorkflowEngine, WorkflowLoader
|
|
182
|
+
from friday_memory import ScratchPadStore
|
|
183
|
+
|
|
184
|
+
workflow_session_id = await self.create_session(user_id="workflow")
|
|
185
|
+
await self.orchestrator.set_session(workflow_session_id, user_id="workflow")
|
|
186
|
+
|
|
187
|
+
loader = WorkflowLoader(self._resolve_workflows_dir())
|
|
188
|
+
scratchpad = ScratchPadStore()
|
|
189
|
+
engine = WorkflowEngine(
|
|
190
|
+
orchestrator=self.orchestrator,
|
|
191
|
+
scratchpad=scratchpad,
|
|
192
|
+
telemetry=self.telemetry,
|
|
193
|
+
agent_info_resolver=self.get_agent_info,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if self._config.workflow_mode == "legacy":
|
|
197
|
+
bundle = loader.load_legacy_as_canonical(workflow_name)
|
|
198
|
+
self._emit_workflow_routing_event(
|
|
199
|
+
workflow_name=workflow_name,
|
|
200
|
+
requested_mode="legacy",
|
|
201
|
+
resolved_mode="legacy_adapter",
|
|
202
|
+
)
|
|
203
|
+
return await engine.execute_canonical(
|
|
204
|
+
bundle.workflow,
|
|
205
|
+
bundle.pipelines,
|
|
206
|
+
initial_inputs,
|
|
207
|
+
role_bindings=self._config.agents_role_bindings,
|
|
208
|
+
execution_id=workflow_session_id,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
if self._config.workflow_mode == "canonical":
|
|
212
|
+
bundle = loader.load_canonical(workflow_name)
|
|
213
|
+
self._emit_workflow_routing_event(
|
|
214
|
+
workflow_name=workflow_name,
|
|
215
|
+
requested_mode="canonical",
|
|
216
|
+
resolved_mode="canonical",
|
|
217
|
+
)
|
|
218
|
+
return await engine.execute_canonical(
|
|
219
|
+
bundle.workflow,
|
|
220
|
+
bundle.pipelines,
|
|
221
|
+
initial_inputs,
|
|
222
|
+
role_bindings=self._config.agents_role_bindings,
|
|
223
|
+
execution_id=workflow_session_id,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# auto mode: attempt canonical first, then fall back to legacy path.
|
|
227
|
+
try:
|
|
228
|
+
bundle = loader.load_canonical(workflow_name)
|
|
229
|
+
except Exception:
|
|
230
|
+
bundle = loader.load_legacy_as_canonical(workflow_name)
|
|
231
|
+
logger.warning(
|
|
232
|
+
"Canonical workflow load failed for '%s'; using legacy adapter path",
|
|
233
|
+
workflow_name,
|
|
234
|
+
)
|
|
235
|
+
self._emit_workflow_routing_event(
|
|
236
|
+
workflow_name=workflow_name,
|
|
237
|
+
requested_mode="auto",
|
|
238
|
+
resolved_mode="legacy_adapter",
|
|
239
|
+
)
|
|
240
|
+
return await engine.execute_canonical(
|
|
241
|
+
bundle.workflow,
|
|
242
|
+
bundle.pipelines,
|
|
243
|
+
initial_inputs,
|
|
244
|
+
role_bindings=self._config.agents_role_bindings,
|
|
245
|
+
execution_id=workflow_session_id,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
self._emit_workflow_routing_event(
|
|
249
|
+
workflow_name=workflow_name,
|
|
250
|
+
requested_mode="auto",
|
|
251
|
+
resolved_mode="canonical",
|
|
252
|
+
)
|
|
253
|
+
return await engine.execute_canonical(
|
|
254
|
+
bundle.workflow,
|
|
255
|
+
bundle.pipelines,
|
|
256
|
+
initial_inputs,
|
|
257
|
+
role_bindings=self._config.agents_role_bindings,
|
|
258
|
+
execution_id=workflow_session_id,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# --- Eager Service Accessors ---
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def config(self) -> RuntimeConfig:
|
|
265
|
+
"""Get runtime configuration."""
|
|
266
|
+
return self._config
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def memory_profile(self) -> MemoryProfile:
|
|
270
|
+
"""Get current memory profile."""
|
|
271
|
+
return self._memory_profile
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def telemetry(self) -> Any:
|
|
275
|
+
"""Get telemetry interface (may be None if disabled)."""
|
|
276
|
+
return self._di.telemetry
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def llm(self) -> Any:
|
|
280
|
+
"""Get LLM service."""
|
|
281
|
+
self._ensure_initialized()
|
|
282
|
+
return self._di.llm
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def embeddings(self) -> Any:
|
|
286
|
+
"""Get embedding service."""
|
|
287
|
+
self._ensure_initialized()
|
|
288
|
+
return self._di.embeddings
|
|
289
|
+
|
|
290
|
+
@property
|
|
291
|
+
def memory(self) -> Any:
|
|
292
|
+
"""Get memory controller."""
|
|
293
|
+
self._ensure_initialized()
|
|
294
|
+
return self._di.memory
|
|
295
|
+
|
|
296
|
+
@property
|
|
297
|
+
def context_engine(self) -> Any:
|
|
298
|
+
"""Get context assembler."""
|
|
299
|
+
self._ensure_initialized()
|
|
300
|
+
return self._di.context_engine
|
|
301
|
+
|
|
302
|
+
@property
|
|
303
|
+
def multi_agent_memory(self) -> Any:
|
|
304
|
+
"""Get multi-agent memory facade when multi-agent mode is enabled."""
|
|
305
|
+
self._ensure_initialized()
|
|
306
|
+
return self._di.multi_agent_memory
|
|
307
|
+
|
|
308
|
+
@property
|
|
309
|
+
def tool_registry(self) -> DefaultToolRegistry:
|
|
310
|
+
"""Get tool registry."""
|
|
311
|
+
self._ensure_initialized()
|
|
312
|
+
assert self._di.tool_registry is not None
|
|
313
|
+
return self._di.tool_registry
|
|
314
|
+
|
|
315
|
+
# --- Lazy Agent Subsystem ---
|
|
316
|
+
|
|
317
|
+
@property
|
|
318
|
+
def agent_loader(self) -> AgentLoader:
|
|
319
|
+
"""
|
|
320
|
+
Get or create the agent loader (lazy).
|
|
321
|
+
|
|
322
|
+
Created on first access. Raises if agents_dir is missing.
|
|
323
|
+
"""
|
|
324
|
+
if self._agent_loader is None:
|
|
325
|
+
agents_dir = (
|
|
326
|
+
Path(self._config.agents_dir)
|
|
327
|
+
if self._config.agents_dir
|
|
328
|
+
else Path("agents")
|
|
329
|
+
)
|
|
330
|
+
self._agent_loader = AgentLoader(agents_dir)
|
|
331
|
+
return self._agent_loader
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def agent_registry(self) -> AgentRegistry:
|
|
335
|
+
"""
|
|
336
|
+
Get or create the agent registry (lazy).
|
|
337
|
+
|
|
338
|
+
Auto-configured with:
|
|
339
|
+
- AgentLoader for YAML loading
|
|
340
|
+
- Factory for agent instantiation
|
|
341
|
+
- All agents from the agents directory (if it exists)
|
|
342
|
+
"""
|
|
343
|
+
if self._agent_registry is None:
|
|
344
|
+
self._agent_registry = self._build_agent_registry()
|
|
345
|
+
|
|
346
|
+
return self._agent_registry
|
|
347
|
+
|
|
348
|
+
@property
|
|
349
|
+
def orchestrator(self) -> AgentOrchestrator:
|
|
350
|
+
"""
|
|
351
|
+
Get or create the agent orchestrator (lazy).
|
|
352
|
+
|
|
353
|
+
Configured with the agent registry and first available agent
|
|
354
|
+
as the default.
|
|
355
|
+
"""
|
|
356
|
+
if self._orchestrator is None:
|
|
357
|
+
default_agent = None
|
|
358
|
+
available_agents = self.agent_registry.list_agents()
|
|
359
|
+
if available_agents:
|
|
360
|
+
default_agent = available_agents[0]
|
|
361
|
+
logger.debug(f"Using '{default_agent}' as default orchestrator agent")
|
|
362
|
+
|
|
363
|
+
self._orchestrator = AgentOrchestrator(
|
|
364
|
+
registry=self.agent_registry,
|
|
365
|
+
default_agent=default_agent,
|
|
366
|
+
)
|
|
367
|
+
logger.info("Agent orchestrator initialized")
|
|
368
|
+
|
|
369
|
+
return self._orchestrator
|
|
370
|
+
|
|
371
|
+
# --- Agent Factory ---
|
|
372
|
+
|
|
373
|
+
def register_agent_factory(
|
|
374
|
+
self,
|
|
375
|
+
agent_type: str,
|
|
376
|
+
factory: Callable[[AgentProfile], Any],
|
|
377
|
+
) -> None:
|
|
378
|
+
"""Register or override a runtime factory for a profile `agent_type`."""
|
|
379
|
+
self._agent_factories[agent_type] = factory
|
|
380
|
+
|
|
381
|
+
def get_copilot_agent(
|
|
382
|
+
self,
|
|
383
|
+
profile: AgentProfile | None = None,
|
|
384
|
+
) -> CopilotAgent:
|
|
385
|
+
"""
|
|
386
|
+
Create a CopilotAgent with all dependencies wired.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
profile: Agent profile (uses default if not provided).
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
Configured CopilotAgent instance.
|
|
393
|
+
"""
|
|
394
|
+
self._ensure_initialized()
|
|
395
|
+
|
|
396
|
+
if profile is None:
|
|
397
|
+
profile = self._default_profile()
|
|
398
|
+
|
|
399
|
+
return CopilotAgent(
|
|
400
|
+
profile=profile,
|
|
401
|
+
memory=self.memory,
|
|
402
|
+
context_engine=self.context_engine,
|
|
403
|
+
llm=self._get_llm_for_profile(profile),
|
|
404
|
+
tool_registry=self._tool_registry_interface(),
|
|
405
|
+
telemetry=self.telemetry,
|
|
406
|
+
sanitization_config=self._sanitization_config,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
def get_interactive_agent(
|
|
410
|
+
self,
|
|
411
|
+
profile: AgentProfile | None = None,
|
|
412
|
+
) -> InteractiveAgent:
|
|
413
|
+
"""
|
|
414
|
+
Create an InteractiveAgent with all dependencies wired.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
profile: Agent profile (uses default if not provided).
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
Configured InteractiveAgent instance.
|
|
421
|
+
"""
|
|
422
|
+
self._ensure_initialized()
|
|
423
|
+
|
|
424
|
+
if profile is None:
|
|
425
|
+
profile = self._default_profile()
|
|
426
|
+
|
|
427
|
+
return InteractiveAgent(
|
|
428
|
+
profile=profile,
|
|
429
|
+
memory=self.memory,
|
|
430
|
+
context_engine=self.context_engine,
|
|
431
|
+
llm=self._get_llm_for_profile(profile),
|
|
432
|
+
tool_registry=self._tool_registry_interface(),
|
|
433
|
+
telemetry=self.telemetry,
|
|
434
|
+
sanitization_config=self._sanitization_config,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
def get_team_agent(self, name: str) -> Any:
|
|
438
|
+
"""
|
|
439
|
+
Get or create a named agent from the registry.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
name: Agent name from registry.
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
Configured agent instance.
|
|
446
|
+
"""
|
|
447
|
+
self._ensure_initialized()
|
|
448
|
+
return self.agent_registry.create_agent(name)
|
|
449
|
+
|
|
450
|
+
def list_available_agents(self) -> list[str]:
|
|
451
|
+
"""
|
|
452
|
+
List available agent names from the registry.
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
Sorted list of agent names.
|
|
456
|
+
"""
|
|
457
|
+
return cast(list[str], self.agent_registry.list_agents())
|
|
458
|
+
|
|
459
|
+
def get_agent_info(self, name: str) -> dict | None:
|
|
460
|
+
"""
|
|
461
|
+
Get information about an agent.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
name: Agent name.
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
Dict with agent info or None if not found.
|
|
468
|
+
"""
|
|
469
|
+
profile = self.agent_registry.get(name)
|
|
470
|
+
if profile is None:
|
|
471
|
+
return None
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
"name": profile.name,
|
|
475
|
+
"role": profile.role,
|
|
476
|
+
"goal": profile.goal,
|
|
477
|
+
"llm_service": self._resolve_llm_service_name(profile),
|
|
478
|
+
"agent_type": profile.agent_type,
|
|
479
|
+
"capabilities": profile.capabilities,
|
|
480
|
+
"tools": profile.tools,
|
|
481
|
+
"tool_autonomy": profile.tool_autonomy,
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
# --- Private Helpers ---
|
|
485
|
+
|
|
486
|
+
def _ensure_initialized(self) -> None:
|
|
487
|
+
"""Raise if kernel not started."""
|
|
488
|
+
if not self._initialized:
|
|
489
|
+
raise RuntimeError("RuntimeKernel not started. Call start() first.")
|
|
490
|
+
|
|
491
|
+
def _build_agent_registry(self) -> AgentRegistry:
|
|
492
|
+
"""Create and validate the registry against runtime LLM bindings."""
|
|
493
|
+
registry = AgentRegistry(self.agent_loader)
|
|
494
|
+
registry.set_factory(self._create_agent)
|
|
495
|
+
|
|
496
|
+
if self.agent_loader.agents_dir.exists():
|
|
497
|
+
registry.load_all_from_directory()
|
|
498
|
+
self._validate_agent_profiles(registry.list_profiles())
|
|
499
|
+
|
|
500
|
+
return registry
|
|
501
|
+
|
|
502
|
+
def _resolve_workflows_dir(self) -> Path:
|
|
503
|
+
"""Resolve workflow bundle root for runtime workflow execution."""
|
|
504
|
+
configured = self._config.workflows_dir
|
|
505
|
+
if configured:
|
|
506
|
+
raw = Path(configured).expanduser()
|
|
507
|
+
if raw.is_absolute():
|
|
508
|
+
return raw.resolve()
|
|
509
|
+
install_root = Path(self._config.install_root or ".").expanduser().resolve()
|
|
510
|
+
return (install_root / raw).resolve()
|
|
511
|
+
|
|
512
|
+
agents_dir = Path(self._config.agents_dir or "agents")
|
|
513
|
+
return (agents_dir / "workflows").resolve()
|
|
514
|
+
|
|
515
|
+
def _validate_agent_profiles(self, profiles: list[AgentProfile]) -> None:
|
|
516
|
+
"""Fail startup deterministically on invalid agent/runtime bindings."""
|
|
517
|
+
registry = self._di.tool_registry
|
|
518
|
+
if registry is None:
|
|
519
|
+
raise RuntimeError("Runtime tool registry not initialized during startup")
|
|
520
|
+
|
|
521
|
+
available_tool_names = set(cast(list[str], registry.list_names()))
|
|
522
|
+
for profile in profiles:
|
|
523
|
+
resolved_service = self._resolve_llm_service_name(profile)
|
|
524
|
+
self._resolve_agent_factory(profile.agent_type)
|
|
525
|
+
validate_agent_tool_bindings(
|
|
526
|
+
agent_name=profile.name,
|
|
527
|
+
declared_tools=collect_profile_tool_references(profile),
|
|
528
|
+
available_tool_names=available_tool_names,
|
|
529
|
+
)
|
|
530
|
+
logger.debug(
|
|
531
|
+
"Validated agent '%s' LLM binding to service '%s' (agent_type=%s)",
|
|
532
|
+
profile.name,
|
|
533
|
+
resolved_service,
|
|
534
|
+
profile.agent_type,
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
def _resolve_llm_service_name(self, profile: AgentProfile) -> str:
|
|
538
|
+
"""Resolve the effective runtime LLM service for a profile."""
|
|
539
|
+
if profile.llm_service is not None:
|
|
540
|
+
if profile.llm_service not in self._config.llm_pool.services:
|
|
541
|
+
raise ValueError(
|
|
542
|
+
f"Agent '{profile.name}' references unknown llm_service "
|
|
543
|
+
f"'{profile.llm_service}'"
|
|
544
|
+
)
|
|
545
|
+
return cast(str, profile.llm_service)
|
|
546
|
+
|
|
547
|
+
if self._config.strict_agent_llm_binding:
|
|
548
|
+
raise ValueError(
|
|
549
|
+
f"Agent '{profile.name}' must declare llm_service when "
|
|
550
|
+
"strict_agent_llm_binding is enabled"
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
return cast(str, self._config.llm_pool.default_service)
|
|
554
|
+
|
|
555
|
+
def _get_llm_for_profile(self, profile: AgentProfile) -> Any:
|
|
556
|
+
"""Return the configured runtime LLM client for the agent profile."""
|
|
557
|
+
service_name = self._resolve_llm_service_name(profile)
|
|
558
|
+
try:
|
|
559
|
+
return self._di.llm_pool[service_name]
|
|
560
|
+
except KeyError as exc:
|
|
561
|
+
raise RuntimeError(
|
|
562
|
+
f"Runtime LLM pool missing configured service '{service_name}'"
|
|
563
|
+
) from exc
|
|
564
|
+
|
|
565
|
+
def _get_llm_service_config_for_profile(self, profile: AgentProfile) -> Any:
|
|
566
|
+
"""Return the resolved runtime LLM service config for the profile."""
|
|
567
|
+
return self._config.get_llm_service_config(self._resolve_llm_service_name(profile))
|
|
568
|
+
|
|
569
|
+
def _resolve_agent_factory(self, agent_type: str) -> Callable[[AgentProfile], Any]:
|
|
570
|
+
"""Resolve the runtime factory for the profile `agent_type`."""
|
|
571
|
+
try:
|
|
572
|
+
return self._agent_factories[agent_type]
|
|
573
|
+
except KeyError as exc:
|
|
574
|
+
raise ValueError(
|
|
575
|
+
f"Unregistered agent_type '{agent_type}' for runtime factory mapping"
|
|
576
|
+
) from exc
|
|
577
|
+
|
|
578
|
+
def _create_agent(self, profile: AgentProfile) -> Any:
|
|
579
|
+
"""
|
|
580
|
+
Factory function for creating agents from profiles.
|
|
581
|
+
|
|
582
|
+
Used by AgentRegistry when create_agent() is called.
|
|
583
|
+
|
|
584
|
+
Args:
|
|
585
|
+
profile: Agent profile to instantiate.
|
|
586
|
+
|
|
587
|
+
Returns:
|
|
588
|
+
Configured agent instance.
|
|
589
|
+
"""
|
|
590
|
+
return self._resolve_agent_factory(profile.agent_type)(profile)
|
|
591
|
+
|
|
592
|
+
def _tool_registry_interface(self) -> ToolRegistry:
|
|
593
|
+
"""Return the registry narrowed to the protocol expected by agents."""
|
|
594
|
+
return cast(ToolRegistry, self.tool_registry)
|
|
595
|
+
|
|
596
|
+
def _create_interactive_agent(self, profile: AgentProfile) -> InteractiveAgent:
|
|
597
|
+
"""Create the default interactive runtime agent."""
|
|
598
|
+
llm_service_config = self._get_llm_service_config_for_profile(profile)
|
|
599
|
+
return InteractiveAgent(
|
|
600
|
+
profile=profile,
|
|
601
|
+
memory=self.memory,
|
|
602
|
+
context_engine=self.context_engine,
|
|
603
|
+
llm=self._get_llm_for_profile(profile),
|
|
604
|
+
tool_registry=self._tool_registry_interface(),
|
|
605
|
+
telemetry=self.telemetry,
|
|
606
|
+
sanitization_config=self._sanitization_config,
|
|
607
|
+
runtime_base_system_prompt=self._config.prompting.base_system_prompt,
|
|
608
|
+
resolved_llm_service_name=self._resolve_llm_service_name(profile),
|
|
609
|
+
resolved_provider_name=llm_service_config.provider,
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
def _create_autonomous_agent(self, profile: AgentProfile) -> AutonomousAgent:
|
|
613
|
+
"""Create the autonomous runtime agent."""
|
|
614
|
+
llm_service_config = self._get_llm_service_config_for_profile(profile)
|
|
615
|
+
return AutonomousAgent(
|
|
616
|
+
profile=profile,
|
|
617
|
+
memory=self.memory,
|
|
618
|
+
context_engine=self.context_engine,
|
|
619
|
+
llm=self._get_llm_for_profile(profile),
|
|
620
|
+
tool_registry=self._tool_registry_interface(),
|
|
621
|
+
telemetry=self.telemetry,
|
|
622
|
+
sanitization_config=self._sanitization_config,
|
|
623
|
+
runtime_base_system_prompt=self._config.prompting.base_system_prompt,
|
|
624
|
+
resolved_llm_service_name=self._resolve_llm_service_name(profile),
|
|
625
|
+
resolved_provider_name=llm_service_config.provider,
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
def _default_profile(self) -> AgentProfile:
|
|
629
|
+
"""Create default agent profile."""
|
|
630
|
+
return AgentProfile(
|
|
631
|
+
name="Friday",
|
|
632
|
+
role="Personal Assistant",
|
|
633
|
+
goal="Help users accomplish their tasks efficiently and accurately",
|
|
634
|
+
system_prompt_template=(
|
|
635
|
+
"You are {name}, a {role}.\n\n"
|
|
636
|
+
"Your goal: {goal}\n\n"
|
|
637
|
+
"You have access to a persistent memory system. "
|
|
638
|
+
"Use the available tools to:\n"
|
|
639
|
+
"- Remember important information with store_fact\n"
|
|
640
|
+
"- Recall relevant context with recall_memory\n"
|
|
641
|
+
"- Save working notes with scratchpad_write\n\n"
|
|
642
|
+
"Be helpful, accurate, and concise in your responses."
|
|
643
|
+
),
|
|
644
|
+
tools=[
|
|
645
|
+
"recall_memory",
|
|
646
|
+
"store_fact",
|
|
647
|
+
"scratchpad_write",
|
|
648
|
+
"scratchpad_read",
|
|
649
|
+
"scratchpad_list",
|
|
650
|
+
],
|
|
651
|
+
temperature=0.7,
|
|
652
|
+
llm_service=self._config.llm_pool.default_service,
|
|
653
|
+
agent_type="interactive",
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
def _emit_workflow_routing_event(
|
|
657
|
+
self,
|
|
658
|
+
*,
|
|
659
|
+
workflow_name: str,
|
|
660
|
+
requested_mode: str,
|
|
661
|
+
resolved_mode: str,
|
|
662
|
+
) -> None:
|
|
663
|
+
"""Emit auditable workflow routing information when telemetry is enabled."""
|
|
664
|
+
if self.telemetry is None:
|
|
665
|
+
return
|
|
666
|
+
self.telemetry.capture_event(
|
|
667
|
+
"workflow_mode_selected",
|
|
668
|
+
{
|
|
669
|
+
"workflow_name": workflow_name,
|
|
670
|
+
"requested_mode": requested_mode,
|
|
671
|
+
"resolved_mode": resolved_mode,
|
|
672
|
+
},
|
|
673
|
+
)
|