Skip to content

Commit f22824b

Browse files
committed
Add managed runtime context helper and acceptance test
New managed_runtime_context() helper builds tool metadata and workspace context for managed agent runs ManagedAgentRunner.run() now captures context_keys and tool_count in audit log events and result metadata New acceptance test covers context construction, forwarding, audit persistence, and result trace metadata Export managed_runtime_context from package; update acceptance docs
1 parent bdfba24 commit f22824b

4 files changed

Lines changed: 116 additions & 3 deletions

File tree

docs/acceptance.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ TeaAgent acceptance tests live under `tests/acceptance/` and verify user-facing
1010
- TUI prompt approval workflow: approval prompt, destructive write, final run payload, and audit summary.
1111
- MCP client compatibility flow: bearer auth, session lifecycle, `tools/list`, `tools/call`, and session close.
1212
- A2A federation flow: remote discovery, partial endpoint failure, capability routing, delegation, context forwarding, and agent trace metadata.
13+
- Managed runtime flow: tool metadata context construction, workspace/request context forwarding, persisted managed task audit events, and result trace metadata.
1314

1415
## Next User Stories
1516

1617
- Live provider conformance gated by environment variables.
17-
- Managed runtime flow that forwards task context/tools and records managed runtime audit events.
1818
- Long-running worker flow covering `ultrawork start`, heartbeat/status, logs, and stop.
1919
- Workspace edit flow covering hash reads, patch application, git status, test execution, and final diff summary.

teaagent/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
ModelConformanceResult,
6565
run_model_conformance,
6666
)
67+
from teaagent.managed_runtime import managed_runtime_context
6768
from teaagent.mcp_client import MCPClientError, MCPHTTPClient
6869
from teaagent.mcp_http import build_mcp_http_server, serve_mcp_http
6970
from teaagent.mcp_server import handle_mcp_request, serve_mcp_stdio
@@ -252,6 +253,7 @@
252253
'graph_retrieve',
253254
'handle_mcp_request',
254255
'handle_stateless_tool_request',
256+
'managed_runtime_context',
255257
'parse_model_decision',
256258
'parse_permission_mode',
257259
'preflight',

teaagent/managed_runtime.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,19 @@ class ManagedRunResult:
2121
metadata: dict[str, Any] = field(default_factory=dict)
2222

2323

24+
def managed_runtime_context(
25+
registry: Any,
26+
*,
27+
workspace_root: Optional[str] = None,
28+
extra: Optional[dict[str, Any]] = None,
29+
) -> dict[str, Any]:
30+
context = dict(extra or {})
31+
context['tools'] = registry.mcp_metadata()
32+
if workspace_root is not None:
33+
context['workspace_root'] = workspace_root
34+
return context
35+
36+
2437
class ManagedAgentRunner:
2538
def __init__(
2639
self, adapter: ManagedRuntimeAdapter, *, runtime_name: str = ''
@@ -37,10 +50,18 @@ def run(
3750
run_id: str = '',
3851
) -> ManagedRunResult:
3952
ctx = context or {}
53+
context_keys = sorted(ctx.keys())
54+
tools = ctx.get('tools', [])
55+
tool_count = len(tools) if isinstance(tools, list) else 0
4056
_log = None if audit_logger is _AUDIT_UNSET else audit_logger
4157
if _log is not None:
4258
_log.record(
43-
'managed_task_started', run_id, runtime=self._runtime_name, task=task
59+
'managed_task_started',
60+
run_id,
61+
runtime=self._runtime_name,
62+
task=task,
63+
context_keys=context_keys,
64+
tool_count=tool_count,
4465
)
4566
try:
4667
output = self._adapter.run_task(task, context=ctx)
@@ -51,6 +72,8 @@ def run(
5172
run_id,
5273
runtime=self._runtime_name,
5374
error=str(exc),
75+
context_keys=context_keys,
76+
tool_count=tool_count,
5477
)
5578
raise
5679
if _log is not None:
@@ -59,8 +82,18 @@ def run(
5982
run_id,
6083
runtime=self._runtime_name,
6184
output_length=len(output),
85+
context_keys=context_keys,
86+
tool_count=tool_count,
6287
)
63-
return ManagedRunResult(output=output, runtime=self._runtime_name)
88+
return ManagedRunResult(
89+
output=output,
90+
runtime=self._runtime_name,
91+
metadata={
92+
'run_id': run_id,
93+
'context_keys': context_keys,
94+
'tool_count': tool_count,
95+
},
96+
)
6497

6598
def healthy(self) -> bool:
6699
return self._adapter.health_check()
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from __future__ import annotations
2+
3+
import json
4+
import tempfile
5+
import unittest
6+
from pathlib import Path
7+
8+
from teaagent.audit import AuditLogger
9+
from teaagent.managed_runtime import ManagedAgentRunner, managed_runtime_context
10+
from teaagent.tools import ToolAnnotations, ToolRegistry
11+
12+
13+
class _CapturingManagedRuntime:
14+
def __init__(self) -> None:
15+
self.received_task = ''
16+
self.received_context: dict = {}
17+
18+
def run_task(self, task: str, *, context: dict) -> str:
19+
self.received_task = task
20+
self.received_context = context
21+
return f'managed:{len(context["tools"])}'
22+
23+
def health_check(self) -> bool:
24+
return True
25+
26+
27+
class ManagedRuntimeFlowAcceptanceTests(unittest.TestCase):
28+
def test_managed_runtime_receives_tool_context_and_persists_audit(self) -> None:
29+
with tempfile.TemporaryDirectory() as tmp:
30+
audit_path = Path(tmp) / 'managed-run.jsonl'
31+
audit = AuditLogger(audit_path)
32+
registry = ToolRegistry()
33+
registry.register(
34+
name='workspace_read_file',
35+
description='Read a workspace file',
36+
input_schema={'type': 'object'},
37+
output_schema={'type': 'object'},
38+
annotations=ToolAnnotations(read_only=True),
39+
handler=lambda _args: {'content': 'ok'},
40+
)
41+
runtime = _CapturingManagedRuntime()
42+
runner = ManagedAgentRunner(runtime, runtime_name='acceptance-runtime')
43+
44+
context = managed_runtime_context(
45+
registry, workspace_root=tmp, extra={'request_id': 'req-1'}
46+
)
47+
result = runner.run(
48+
'summarize workspace',
49+
context=context,
50+
audit_logger=audit,
51+
run_id='managed-1',
52+
)
53+
events = [
54+
json.loads(line)
55+
for line in audit_path.read_text(encoding='utf-8').splitlines()
56+
]
57+
58+
self.assertEqual(result.output, 'managed:1')
59+
self.assertEqual(result.runtime, 'acceptance-runtime')
60+
self.assertEqual(result.metadata['run_id'], 'managed-1')
61+
self.assertEqual(result.metadata['tool_count'], 1)
62+
self.assertIn('tools', result.metadata['context_keys'])
63+
self.assertEqual(runtime.received_task, 'summarize workspace')
64+
self.assertEqual(runtime.received_context['request_id'], 'req-1')
65+
self.assertEqual(runtime.received_context['workspace_root'], tmp)
66+
self.assertEqual(
67+
runtime.received_context['tools'][0]['name'], 'workspace_read_file'
68+
)
69+
self.assertEqual(
70+
[event['event_type'] for event in events],
71+
['managed_task_started', 'managed_task_completed'],
72+
)
73+
self.assertEqual(events[0]['payload']['tool_count'], 1)
74+
self.assertEqual(events[1]['payload']['output_length'], len('managed:1'))
75+
76+
77+
if __name__ == '__main__':
78+
unittest.main()

0 commit comments

Comments
 (0)