feat(agent): add task-oriented run logs and execution details

This commit is contained in:
RockChinQ
2026-09-18 00:59:17 +08:00
parent b264d46d77
commit a6ac25cf84
20 changed files with 1080 additions and 11 deletions
@@ -0,0 +1,58 @@
"""Agent monitoring snapshots retain useful input without duplicating binary payloads."""
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
from langbot.pkg.agent.runner.run_journal import AgentRunJournal
@pytest.mark.asyncio
@pytest.mark.parametrize('event_type', ['message.received', 'group.member_joined'])
async def test_agent_run_snapshot_records_identity_and_redacts_inline_attachments(event_type):
journal = AgentRunJournal(SimpleNamespace())
ledger = SimpleNamespace(create_run=AsyncMock(return_value={}))
journal._run_ledger_store = ledger
raw_input = {
'text': 'Describe the attached image',
'contents': [{'type': 'image_base64', 'image_base64': 'large-image-data'}],
'attachments': [{'name': 'notes.txt', 'content': 'large-file-data'}],
}
event = SimpleNamespace(
event_id='event-1',
conversation_id='conversation-1',
thread_id=None,
workspace_id='workspace-1',
bot_id='bot-1',
event_type=event_type,
data={'user_id': 'user-1', 'group_id': 'group-1'},
source='platform',
delivery=SimpleNamespace(model_dump=lambda **kwargs: {'target_id': 'group-1'}),
)
binding = SimpleNamespace(
binding_id='agent_agent-1_runner-1',
agent_id='agent-1',
processor_id='agent-1',
processor_type='agent',
)
await journal.create_run(
event=event,
binding=binding,
descriptor=SimpleNamespace(id='runner-1'),
context={'run_id': 'run-1', 'input': raw_input},
authorization={},
)
saved = ledger.create_run.call_args.kwargs
assert saved['agent_id'] == 'agent-1'
assert saved['metadata']['input']['text'] == raw_input['text']
assert saved['metadata']['input']['contents'][0]['image_base64'] is None
assert saved['metadata']['input']['attachments'][0]['content'] is None
assert saved['metadata']['delivery'] == {'target_id': 'group-1'}
assert raw_input['contents'][0]['image_base64'] == 'large-image-data'
assert raw_input['attachments'][0]['content'] == 'large-file-data'
if event_type != 'message.received':
assert saved['metadata']['input_event'] == event.data
else:
assert 'input_event' not in saved['metadata']
@@ -472,3 +472,30 @@ async def test_run_lifecycle_retains_milliseconds(store, monkeypatch):
await store.finalize_run(run_id='run-ms', status='completed')
saved = await store.get_run('run-ms')
assert saved['finished_at_ms'] - saved['started_at_ms'] == 275
@pytest.mark.asyncio
async def test_agent_run_pages_include_old_bindings_without_leaking_other_agents(store):
for run_id, agent_id, binding, workspace in [
('old', None, 'agent_one_plugin:a/old/default', 'w'),
('debug', None, 'debug:one:plugin:a/new/default', 'w'),
('new', 'one', 'new-binding', 'w'),
('other', 'two', 'agent_two_plugin:a/old/default', 'w'),
('conflicting-id', 'two', 'agent_one_plugin:a/old/default', 'w'),
('other-workspace', 'one', 'agent_one_plugin:a/old/default', 'other'),
('plugin-processor', None, 'event_processor:one', 'w'),
]:
await store.create_run(
run_id=run_id,
event_id=None,
agent_id=agent_id,
binding_id=binding,
workspace_id=workspace,
runner_id='runner',
)
page, cursor, more, total = await store.list_runs(agent_id='one', workspace_id='w', limit=2)
assert [run['run_id'] for run in page] == ['new', 'debug']
assert more and total == 3
page, cursor, more, total = await store.list_runs(agent_id='one', workspace_id='w', before_id=cursor, limit=2)
assert [run['run_id'] for run in page] == ['old']
assert not more and total == 3
@@ -905,3 +905,44 @@ async def test_processor_trace_rejects_other_workspace_or_instance(monkeypatch,
with pytest.raises(ValueError, match='Processor run not found'):
await service.get_processor_run_events(WORKSPACE_UUID, 'one', 'run')
store.page_run_events.assert_not_called()
async def test_agent_runs_use_workspace_and_stable_agent_identity(monkeypatch):
service = AgentService(_make_app())
service.get_agent = AsyncMock(return_value={'kind': 'agent'})
service.ap.persistence_mgr.get_db_engine = Mock()
store = SimpleNamespace(list_runs=AsyncMock(return_value=([], None, False, 0)))
monkeypatch.setattr('langbot.pkg.agent.runner.run_ledger_store.RunLedgerStore', Mock(return_value=store))
assert (await service.get_processor_runs(WORKSPACE_UUID, 'one', before_id=12))['total'] == 0
store.list_runs.assert_awaited_once_with(workspace_id=WORKSPACE_UUID, agent_id='one', before_id=12)
@pytest.mark.parametrize(
'binding,agent_id,workspace,allowed',
[
('agent_one_plugin:a/old/default', None, WORKSPACE_UUID, True),
('debug:one:plugin:a/new/default', None, WORKSPACE_UUID, True),
('any-new-binding', 'one', WORKSPACE_UUID, True),
('agent_two_plugin:a/old/default', None, WORKSPACE_UUID, False),
('debug:two:plugin:a/new/default', None, WORKSPACE_UUID, False),
('agent_one_plugin:a/old/default', 'two', WORKSPACE_UUID, False),
('agent_one_plugin:a/old/default', None, 'other-workspace', False),
('event_processor:one', None, WORKSPACE_UUID, False),
],
)
async def test_agent_trace_authorizes_history_across_runner_changes(monkeypatch, binding, agent_id, workspace, allowed):
service = AgentService(_make_app())
service.get_agent = AsyncMock(return_value={'kind': 'agent'})
service.ap.persistence_mgr.get_db_engine = Mock()
store = SimpleNamespace(
get_run=AsyncMock(return_value={'workspace_id': workspace, 'binding_id': binding, 'agent_id': agent_id}),
page_run_events=AsyncMock(return_value=([], None, None, False)),
)
monkeypatch.setattr('langbot.pkg.agent.runner.run_ledger_store.RunLedgerStore', Mock(return_value=store))
if allowed:
await service.get_processor_run_events(WORKSPACE_UUID, 'one', 'run', after_sequence=100)
store.page_run_events.assert_awaited_once_with(run_id='run', after_sequence=100, limit=100)
else:
with pytest.raises(ValueError, match='Processor run not found'):
await service.get_processor_run_events(WORKSPACE_UUID, 'one', 'run')
store.page_run_events.assert_not_called()