feat(proto): add RunAgent, CancelAnswerJob, Agent* messages

Extend AnswerJobState with RUNNING/CANCELLED/EXPIRED (values 5-7, backward
compatible). Add AgentRequest/AgentResponse/AgentToolLoopOptions/
AgentResponseContract/AgentUsage/AgentOutputFormat and Cancel* messages.
Existing RPCs unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kaien 2026-08-05 23:07:59 +03:00
parent 9c923913f6
commit 00e8f9b190
3 changed files with 990 additions and 112 deletions

915
ai.pb.go

File diff suppressed because it is too large Load Diff

101
ai.proto
View File

@ -46,12 +46,18 @@ message GetAnswerJobRequest {
}
// AnswerJobState — состояние отложенной генерации ответа.
// Значения 0-4 исторические (GenerateAnswer deferred). Значения 5-7 добавлены для
// durable RunAgent jobs; старые клиенты их не создают и потому не увидят. RUNNING
// для старого клиента семантически эквивалентен PENDING (job ещё не терминальна).
enum AnswerJobState {
ANSWER_JOB_STATE_UNSPECIFIED = 0;
ANSWER_JOB_STATE_PENDING = 1; // ещё выполняется (evolver работает / модель дописывает)
ANSWER_JOB_STATE_PENDING = 1; // ещё не взята в работу / выполняется (evolver работает, модель дописывает)
ANSWER_JOB_STATE_DONE = 2; // готово, content заполнен
ANSWER_JOB_STATE_FAILED = 3; // ошибка, error заполнен
ANSWER_JOB_STATE_NOT_FOUND = 4; // неизвестный/протухший job_id (напр. после рестарта core)
ANSWER_JOB_STATE_RUNNING = 5; // взята воркером (lease активен), tool loop исполняется
ANSWER_JOB_STATE_CANCELLED = 6; // отменена через CancelAnswerJob
ANSWER_JOB_STATE_EXPIRED = 7; // истёк expiration до достижения терминального состояния
}
// AnswerJobResponse — результат опроса отложенной генерации.
@ -117,8 +123,101 @@ message HealthCheckResponse {
string version = 2;
}
// ---------------------------------------------------------------------------
// RunAgent: надёжный агентный вызов (server-side tool filtering, deferred jobs).
// В отличие от GenerateAnswer, НЕ подмешивает Telegram/Steam-персонализацию и не
// требует telegram_user_id/steam_user_id. Вызывающая сторона полностью владеет
// массивом messages (включая system). Требует service-to-service авторизации
// (Bearer service-token в gRPC metadata) — agent_id/tool_profile сами по себе не
// являются авторизацией.
// ---------------------------------------------------------------------------
// AgentToolLoopOptions задаёт лимиты tool loop. Значения клиента могут только
// сузить серверные потолки (max_tool_iterations/max_total_tool_calls ≤ server max).
message AgentToolLoopOptions {
bool tools_enabled = 1; // false — модель отвечает без tools
uint32 max_tool_iterations = 2; // потолок round-trip'ов модель↔tools (0 = серверный default)
uint32 max_total_tool_calls = 3; // потолок РЕАЛЬНЫХ tool call'ов, не итераций (0 = серверный default)
uint32 tool_timeout_seconds = 4; // таймаут одного tool call (0 = серверный default)
uint32 total_timeout_seconds = 5; // wall-clock таймаут всего вызова (0 = серверный default), независим от budget
}
// AgentOutputFormat — требуемый формат финального ответа.
enum AgentOutputFormat {
AGENT_OUTPUT_FORMAT_UNSPECIFIED = 0; // трактуется как TEXT
AGENT_OUTPUT_FORMAT_TEXT = 1; // произвольный текст
AGENT_OUTPUT_FORMAT_JSON = 2; // должен быть валидный JSON (без schema-проверки)
AGENT_OUTPUT_FORMAT_JSON_SCHEMA = 3; // валидный JSON, проходящий json_schema
}
// AgentResponseContract описывает требования к структурированному ответу.
message AgentResponseContract {
AgentOutputFormat format = 1;
string json_schema = 2; // JSON Schema (draft-07), обязателен при format=JSON_SCHEMA
bool repair_invalid_output = 3; // при провале валидации — один repair-запрос модели (без новых tools)
}
// AgentRequest — запрос агентного вызова.
message AgentRequest {
repeated StatelessMessage messages = 1; // полный контекст, включая system
string model_name = 2; // модель (по умолчанию deepseek-v4-flash)
bool enable_reasoning = 3; // reasoning не попадает в обычный content
string agent_id = 4; // идентификатор логического агента (для логов/метрик, НЕ авторизация)
string tool_profile = 5; // серверный профиль tools (НЕ авторизация; проверяется против credential)
repeated string requested_tools = 6; // сужение профиля: effective = profile.allowed ∩ requested; пусто = все allowed
AgentToolLoopOptions tool_loop = 7;
AgentResponseContract response_contract = 8;
string request_id = 9; // сквозной идентификатор для трассировки
string idempotency_key = 10; // повторный RunAgent с тем же ключом не создаёт дубликат job
bool supports_deferred = 11; // клиент умеет status=PENDING + GetAnswerJob для тяжёлых операций
map<string, string> metadata = 12; // произвольные метки (не логируются как секреты)
}
// AgentUsage — счётчики использования за вызов.
message AgentUsage {
uint32 tool_iterations = 1; // сколько round-trip'ов модель↔tools произошло
uint32 total_tool_calls = 2; // сколько РЕАЛЬНЫХ tool call'ов исполнено (3 calls в одной итерации = 3)
repeated string tools_used = 3;
uint64 input_tokens = 4;
uint64 output_tokens = 5;
}
// AgentResponse — результат агентного вызова.
message AgentResponse {
AnswerStatus status = 1; // COMPLETE — content финальный; PENDING — тянуть через GetAnswerJob(job_id)
string content = 2;
string error = 3;
double duration_ms = 4;
string job_id = 5; // при status=PENDING
AgentUsage usage = 6;
}
// CancelAnswerJobRequest — запрос отмены отложенной job (идемпотентный).
message CancelAnswerJobRequest {
string job_id = 1;
string reason = 2; // причина отмены (сохраняется), опционально
}
// CancelAnswerJobResponse — результат отмены.
message CancelAnswerJobResponse {
AnswerJobState state = 1; // состояние job после запроса (CANCELLED, либо терминальное, если уже завершилась)
bool cancelled = 2; // true — отмена применена этим вызовом; false — job уже была терминальна/отменена
string error = 3;
}
// AIService - основной сервис для работы с AI
service AIService {
// RunAgent — надёжный агентный вызов с серверной фильтрацией tools и deferred
// jobs. Требует service-to-service авторизации.
rpc RunAgent(AgentRequest) returns (AgentResponse);
// CancelAnswerJob отменяет отложенную job (идемпотентно). Требует авторизации.
rpc CancelAnswerJob(CancelAnswerJobRequest) returns (CancelAnswerJobResponse);
// GenerateAnswer генерирует ответ на запрос
rpc GenerateAnswer(AIRequest) returns (AIResponse);

View File

@ -2,7 +2,7 @@
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc v7.34.0
// source: proto/l-ai-core-proto/ai.proto
// source: ai.proto
package proto
@ -19,6 +19,8 @@ import (
const _ = grpc.SupportPackageIsVersion9
const (
AIService_RunAgent_FullMethodName = "/ai.AIService/RunAgent"
AIService_CancelAnswerJob_FullMethodName = "/ai.AIService/CancelAnswerJob"
AIService_GenerateAnswer_FullMethodName = "/ai.AIService/GenerateAnswer"
AIService_GenerateStateless_FullMethodName = "/ai.AIService/GenerateStateless"
AIService_HealthCheck_FullMethodName = "/ai.AIService/HealthCheck"
@ -33,6 +35,11 @@ const (
//
// AIService - основной сервис для работы с AI
type AIServiceClient interface {
// RunAgent — надёжный агентный вызов с серверной фильтрацией tools и deferred
// jobs. Требует service-to-service авторизации.
RunAgent(ctx context.Context, in *AgentRequest, opts ...grpc.CallOption) (*AgentResponse, error)
// CancelAnswerJob отменяет отложенную job (идемпотентно). Требует авторизации.
CancelAnswerJob(ctx context.Context, in *CancelAnswerJobRequest, opts ...grpc.CallOption) (*CancelAnswerJobResponse, error)
// GenerateAnswer генерирует ответ на запрос
GenerateAnswer(ctx context.Context, in *AIRequest, opts ...grpc.CallOption) (*AIResponse, error)
// GenerateStateless генерирует ответ из массива messages (system+history+user)
@ -56,6 +63,26 @@ func NewAIServiceClient(cc grpc.ClientConnInterface) AIServiceClient {
return &aIServiceClient{cc}
}
func (c *aIServiceClient) RunAgent(ctx context.Context, in *AgentRequest, opts ...grpc.CallOption) (*AgentResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AgentResponse)
err := c.cc.Invoke(ctx, AIService_RunAgent_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *aIServiceClient) CancelAnswerJob(ctx context.Context, in *CancelAnswerJobRequest, opts ...grpc.CallOption) (*CancelAnswerJobResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CancelAnswerJobResponse)
err := c.cc.Invoke(ctx, AIService_CancelAnswerJob_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *aIServiceClient) GenerateAnswer(ctx context.Context, in *AIRequest, opts ...grpc.CallOption) (*AIResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AIResponse)
@ -122,6 +149,11 @@ func (c *aIServiceClient) GetAnswerJob(ctx context.Context, in *GetAnswerJobRequ
//
// AIService - основной сервис для работы с AI
type AIServiceServer interface {
// RunAgent — надёжный агентный вызов с серверной фильтрацией tools и deferred
// jobs. Требует service-to-service авторизации.
RunAgent(context.Context, *AgentRequest) (*AgentResponse, error)
// CancelAnswerJob отменяет отложенную job (идемпотентно). Требует авторизации.
CancelAnswerJob(context.Context, *CancelAnswerJobRequest) (*CancelAnswerJobResponse, error)
// GenerateAnswer генерирует ответ на запрос
GenerateAnswer(context.Context, *AIRequest) (*AIResponse, error)
// GenerateStateless генерирует ответ из массива messages (system+history+user)
@ -145,6 +177,12 @@ type AIServiceServer interface {
// pointer dereference when methods are called.
type UnimplementedAIServiceServer struct{}
func (UnimplementedAIServiceServer) RunAgent(context.Context, *AgentRequest) (*AgentResponse, error) {
return nil, status.Error(codes.Unimplemented, "method RunAgent not implemented")
}
func (UnimplementedAIServiceServer) CancelAnswerJob(context.Context, *CancelAnswerJobRequest) (*CancelAnswerJobResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CancelAnswerJob not implemented")
}
func (UnimplementedAIServiceServer) GenerateAnswer(context.Context, *AIRequest) (*AIResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GenerateAnswer not implemented")
}
@ -184,6 +222,42 @@ func RegisterAIServiceServer(s grpc.ServiceRegistrar, srv AIServiceServer) {
s.RegisterService(&AIService_ServiceDesc, srv)
}
func _AIService_RunAgent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AgentRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AIServiceServer).RunAgent(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AIService_RunAgent_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AIServiceServer).RunAgent(ctx, req.(*AgentRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AIService_CancelAnswerJob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CancelAnswerJobRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AIServiceServer).CancelAnswerJob(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AIService_CancelAnswerJob_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AIServiceServer).CancelAnswerJob(ctx, req.(*CancelAnswerJobRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AIService_GenerateAnswer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AIRequest)
if err := dec(in); err != nil {
@ -299,6 +373,14 @@ var AIService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "ai.AIService",
HandlerType: (*AIServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "RunAgent",
Handler: _AIService_RunAgent_Handler,
},
{
MethodName: "CancelAnswerJob",
Handler: _AIService_CancelAnswerJob_Handler,
},
{
MethodName: "GenerateAnswer",
Handler: _AIService_GenerateAnswer_Handler,
@ -325,5 +407,5 @@ var AIService_ServiceDesc = grpc.ServiceDesc{
},
},
Streams: []grpc.StreamDesc{},
Metadata: "proto/l-ai-core-proto/ai.proto",
Metadata: "ai.proto",
}