Архитектура¶
У glia одно правило: никакого скрытого потока управления. Хотите понять, что
делает агент, — читаете agent.py. Хотите понять, какое состояние он хранит, —
читаете trajectory.py. Это вся система.
Составные части¶
┌─────────────────────────────────────────────┐
│ Agent │
│ цикл: вызвать модель → запустить инструменты │
└───────┬───────────────┬──────────────┬───────┘
│ │ │
┌───────▼──────┐ ┌──────▼──────┐ ┌─────▼───────┐
│ LLM │ │ ToolRegistry│ │ Trajectory │
│ (протокол) │ │ (@tool fns)│ │ состояние + │
└───────┬──────┘ └─────────────┘ │ события │
│ └─────────────┘
┌─────────┴─────────┐
┌────▼─────┐ ┌─────▼──────┐
│ ClaudeLLM│ │ EchoLLM │
│(Anthropic) │ (офлайн/CI)│
└──────────┘ └────────────┘
| Модуль | Ответственность |
|---|---|
types.py |
Блоки контента, Message, Usage. Неизменяемые, сериализуемые в JSON. |
llm.py |
Граница провайдера: LLMRequest, LLMResponse, протокол LLM. |
tools.py |
Декоратор @tool (схема из аннотаций типов), ToolRegistry. |
trajectory.py |
Состояние запуска и типы Event. Сам «стеклянный ящик». |
agent.py |
Цикл. run() и run_events(). |
memory.py |
Инженерия контекста: компакторы. |
guardrails.py |
Валидаторы входа/выхода. |
structured.py |
Структурированный вывод через принудительный вызов инструмента. |
checkpoint.py |
Сохранение/загрузка траектории; hook-чек-пойнтер. |
evals.py |
Механизм «эвалы как тесты». |
approval.py |
Human-in-the-loop подтверждение инструментов. |
providers/ |
EchoLLM (офлайн) и ClaudeLLM (Anthropic). |
Цикл, точно¶
Agent.run_events() — единственный источник поведения. Каждая итерация:
- (опционально) компакция — если
Compactorсчитает траекторию слишком большой, сначала сжать её, породив событиеCompacted. - собрать
LLMRequestиз текущей траектории и схем инструментов. - вызвать провайдера (
ModelCall→ModelResponse). Еслиstream=Trueи провайдер это поддерживает, сначала переизлучаются дельты текста как событияModelDelta. Сообщение ассистента и егоUsageдобавляются в траекторию. - если модель запросила инструменты: объявить все вызовы (
ToolCalled), при наличии политики прогнать каждый через шлюз подтверждения (ApprovalRequested→ApprovalResolved), выполнить одобренные — по умолчанию параллельно (asyncio.gather) — испуститьToolReturnedв порядке вызовов, добавить все результаты одним ходом пользователя, повторить. - иначе: прогнать выходные guardrails и завершить (
RunFinished).
Почему так¶
- Блоки — закрытое объединение, а не иерархия классов. Вы делаете
isinstance/match по четырём типам; нет подклассов, которые нужно «открывать», и нет системы плагинов. - Результаты инструментов — ход
user. Это соглашение Anthropic, и оно держит нашу модель сообщений и формат провода согласованными без «долга» на перевод. - События — записи, а не команды. Hooks наблюдают; они никогда не меняют цикл. Это делает поведение читаемым, а hooks — безопасными.
- Граница провайдера намеренно крошечная. Адаптация к новой модели или изменению API затрагивает один файл.
EchoLLM— полноправный гражданин. Детерминированное офлайн-тестирование всего цикла — цель проектирования, а не «довесок».
Как расширять¶
- Новый провайдер: реализуйте
async def generate(request) -> LLMResponse. ~40 строк. Простейший пример —providers/echo.py. - Новая стратегия контекста: реализуйте протокол
Compactor(should_compact+compact). См.memory.py. - Новый guardrail: функция
(text) -> None, которая бросаетGuardrailTripped. Это весь интерфейс. - Наблюдаемость: добавьте hook. Через него проходит каждое событие — экспортируйте в логи, трассировщик или UI.