Skip to content

Core · Service · Active scaffold

platform-agent-service

Core service for asynchronous agent execution requests, persisting execution state, orchestrating MCP tool calls around AI generations and delivering callbacks to source products.

  • TypeScript
  • NestJS 11
  • Prisma
  • PostgreSQL
  • BullMQ
  • Redis
  • AJV
  • @platform/contracts-agent

Spec sheet

Boundary

Core / Agent execution

Runtime

NestJS 11 HTTP service plus BullMQ processor

Default port

3600

Proxy host

http://agent.cs.lvh.me:8080

Persistence

Dedicated agent-postgres via DATABASE_URL

Queue

BullMQ on Redis

Responsibilities

  • Accept product-owned agent execution requests through an internal service-to-service API.
  • Persist lifecycle state from QUEUED/RUNNING to terminal execution statuses.
  • Dispatch model work through platform-mcp-service, optionally execute allowlisted MCP tools, and validate output against the requested schema.
  • Send completion or failure callbacks to the originating product backend.

Interfaces and contract surface

  • GET /health
  • POST /internal/agent/executions
  • GET /internal/agent/executions/:executionId

Consumers

Dependencies and external touchpoints

Notes

  • Tool-backed executions require an explicit toolPolicy allowlist from the consuming product.
  • Terminal statuses include completed, skipped by policy/duplicate/model, failed and callback failed.

Source references

  • platform-agent-service/package.json
  • platform-agent-service/prisma/schema.prisma
  • platform-agent-service/src/modules/agent-executions

Smoke test esecuzione

bash
export AGENT_URL=http://127.0.0.1:3600
export INTERNAL_TOKEN=platform-local-stack-internal-token

curl -sS -X POST "$AGENT_URL/internal/agent/executions" \
  -H "Content-Type: application/json" \
  -H "x-platform-internal-token: $INTERNAL_TOKEN" \
  -d '{
    "tenantId": "demo",
    "sourceService": "manual",
    "sourceRef": "demo-run",
    "taskKey": "wiki-smoke",
    "provider": "template",
    "instructions": "Rispondi con JSON valido.",
    "input": {"message":"ping"},
    "outputSchema": {"type":"object","properties":{"message":{"type":"string"}},"required":["message"]},
    "dispatch": false,
    "initialStatus": "SKIPPED_POLICY",
    "error": "manual smoke without dispatch"
  }'

Operativita Railway

Il deploy Railway di agent deve eseguire npm run prisma:migrate:deploy prima di npm run start:prod, perche il runtime legge campi persistenti come leaseExpiresAt gia durante l'avvio e fallisce se il database non e allineato allo schema Prisma.

Endpoint reference

Header comuni per la surface interna:

  • x-platform-internal-token: $INTERNAL_TOKEN
  • Content-Type: application/json sulle POST
EndpointScopoRequestEsempioRisposta rappresentativaErrori e consumer
GET /healthVerifica liveness del servizio agent.Nessun body.curl -sS "$AGENT_URL/health"{"status":"ok","service":"platform-agent-service"}5xx se runtime non disponibile. Consumer: status dashboard e smoke locali.
POST /internal/agent/executionsCrea una richiesta asincrona di esecuzione agent.tenantId, sourceService, sourceRef, taskKey, instructions, input, outputSchema; opzionali provider, model, toolPolicy, metadata, callback, dispatch, initialStatus, error.curl -sS -X POST "$AGENT_URL/internal/agent/executions" -H "Content-Type: application/json" -H "x-platform-internal-token: $INTERNAL_TOKEN" -d '{"tenantId":"demo","sourceService":"cs-chat","sourceRef":"msg_123","taskKey":"reply","provider":"template","instructions":"Restituisci JSON valido.","input":{"message":"ping"},"outputSchema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]},"toolPolicy":{"mode":"none"},"callback":{"url":"http://127.0.0.1:3004/api/internal/agent/events"}}'{"executionId":"agent_exec_123","status":"QUEUED"}400 DTO/schema non valido, 401/403 token, 409 duplicato se la combinazione sorgente/task e gia terminale. Consumer: BFF e worker prodotto.
GET /internal/agent/executions/:executionIdLegge stato, output, usage e trace tool di una esecuzione.Path executionId.curl -sS "$AGENT_URL/internal/agent/executions/agent_exec_123" -H "x-platform-internal-token: $INTERNAL_TOKEN"{"executionId":"agent_exec_123","tenantId":"demo","sourceService":"cs-chat","sourceRef":"msg_123","taskKey":"reply","status":"COMPLETED","output":{"message":"pong"},"usage":{"inputTokens":12,"outputTokens":4,"totalTokens":16,"providerKey":"template","toolCalls":0},"toolTrace":[],"error":null,"createdAt":"2026-05-03T10:00:00.000Z","completedAt":"2026-05-03T10:00:02.000Z"}404 esecuzione inesistente, 401/403 token. Consumer: BFF prodotto, operations e callback recovery.
GET /internal/agent/executions/:executionId/stepsLegge gli step persistenti del loop agentico.Path executionId.curl -sS "$AGENT_URL/internal/agent/executions/agent_exec_123/steps" -H "x-platform-internal-token: $INTERNAL_TOKEN"{"executionId":"agent_exec_123","items":[{"sequence":1,"type":"MODEL_ACTION","status":"SUCCEEDED"},{"sequence":2,"type":"TOOL_CALL","status":"SUCCEEDED","toolName":"platform_connectors_sources"}]}404 esecuzione inesistente, 401/403 token. Consumer: operations e debug runtime.
POST /internal/agent/executions/:executionId/resumeRe-accoda una execution recuperabile.Path executionId, nessun body.curl -sS -X POST "$AGENT_URL/internal/agent/executions/agent_exec_123/resume" -H "x-platform-internal-token: $INTERNAL_TOKEN"{"executionId":"agent_exec_123","status":"RUNNING"}404 esecuzione inesistente, 409 se stato terminale o lease ancora attivo, 401/403 token. Consumer: operations e recovery manuale.
PUT /internal/agent/profiles/:keyCrea o versiona idempotentemente un profilo agente CORE riusabile.Path key; body instructions, outputSchema, opzionali provider, model, toolPolicy, metadata.curl -sS -X PUT "$AGENT_URL/internal/agent/profiles/cs-chat.reply-aggregator" -H "Content-Type: application/json" -H "x-platform-internal-token: $INTERNAL_TOKEN" -d '{"provider":"template","instructions":"Rispondi JSON.","outputSchema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]},"toolPolicy":{"mode":"none"},"metadata":{"productCode":"cs-chat"}}'{"key":"cs-chat.reply-aggregator","version":1,"status":"ACTIVE","contentHash":"..."}400 key/body non validi, 401/403 token. Consumer: BFF prodotto e smoke operativi.
GET /internal/agent/profilesLista versioni profilo con filtri opzionali.Query opzionali key, status.curl -sS "$AGENT_URL/internal/agent/profiles?status=ACTIVE" -H "x-platform-internal-token: $INTERNAL_TOKEN"{"items":[{"key":"cs-chat.reply-aggregator","version":1,"status":"ACTIVE"}]}401/403 token. Consumer: operations/debug.
GET /internal/agent/profiles/:key/latestRisolve l'ultima versione ACTIVE di un profilo.Path key.curl -sS "$AGENT_URL/internal/agent/profiles/cs-chat.reply-aggregator/latest" -H "x-platform-internal-token: $INTERNAL_TOKEN"{"key":"cs-chat.reply-aggregator","version":1,"status":"ACTIVE"}404 se nessuna versione active, 401/403 token. Consumer: operations/debug.
PATCH /internal/agent/profiles/:key/versions/:versionAggiorna solo stato o metadata di una versione profilo.Path key, version; body opzionale status, metadata.curl -sS -X PATCH "$AGENT_URL/internal/agent/profiles/cs-chat.reply-aggregator/versions/1" -H "Content-Type: application/json" -H "x-platform-internal-token: $INTERNAL_TOKEN" -d '{"status":"ACTIVE"}'{"key":"cs-chat.reply-aggregator","version":1,"status":"ACTIVE"}400 body non valido, 404 versione inesistente, 401/403 token. Consumer: operations.
POST /internal/agent/runsCrea una run multi-agent parent con strategia parallel o sequential.tenantId, sourceService, sourceRef, taskKey, strategy, input, outputSchema, agents; parallel richiede aggregator; opzionali metadata, callback, dispatch.curl -sS -X POST "$AGENT_URL/internal/agent/runs" -H "Content-Type: application/json" -H "x-platform-internal-token: $INTERNAL_TOKEN" -d '{"tenantId":"demo","sourceService":"manual","sourceRef":"run_123","taskKey":"multi","strategy":"parallel","input":{"question":"ping"},"outputSchema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]},"agents":[{"key":"a","instructions":"Rispondi JSON.","outputSchema":{"type":"object"}},{"key":"b","instructions":"Rispondi JSON.","outputSchema":{"type":"object"}}],"aggregator":{"key":"final","instructions":"Sintetizza JSON finale.","outputSchema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}'{"runId":"agent_run_123","status":"QUEUED"}400 DTO/schema non valido, 401/403 token. Consumer: BFF e worker prodotto con task multi-agent.
GET /internal/agent/runs/:runIdLegge stato parent, output, usage aggregata e summary dei node.Path runId.curl -sS "$AGENT_URL/internal/agent/runs/agent_run_123" -H "x-platform-internal-token: $INTERNAL_TOKEN"{"runId":"agent_run_123","status":"COMPLETED","strategy":"parallel","output":{"message":"pong"},"nodes":[{"key":"final","role":"AGGREGATOR","status":"COMPLETED"}]}404 run inesistente, 401/403 token. Consumer: operations e recovery callback.
GET /internal/agent/runs/:runId/nodesLegge i node della run multi-agent con execution figlie collegate.Path runId.curl -sS "$AGENT_URL/internal/agent/runs/agent_run_123/nodes" -H "x-platform-internal-token: $INTERNAL_TOKEN"{"runId":"agent_run_123","items":[{"key":"a","role":"SPECIALIST","status":"COMPLETED","executionId":"agent_exec_a"}]}404 run inesistente, 401/403 token. Consumer: operations e debug runtime.
POST /internal/agent/runs/:runId/resumeRe-accoda una run recuperabile.Path runId, nessun body.curl -sS -X POST "$AGENT_URL/internal/agent/runs/agent_run_123/resume" -H "x-platform-internal-token: $INTERNAL_TOKEN"{"runId":"agent_run_123","status":"RUNNING"}404 run inesistente, 409 se stato terminale o lease ancora attivo, 401/403 token. Consumer: operations e recovery manuale.

Contratto request/response

toolPolicy.mode: "mcp" abilita solo i tool esplicitamente elencati in allowedTools; platform_ai_generate_text resta escluso dai tool invocabili dal modello per evitare loop ricorsivi. maxSteps ha default 4 ed e limitato a 1..8. I limiti runtime opzionali sono maxToolCalls, toolTimeoutMs, maxRepeatedToolCalls e maxTotalTokens. Se dispatch e false, la richiesta puo essere salvata in stato iniziale senza processarla, utile per smoke documentali e test di idempotenza.

Esempio con policy MCP:

json
{
  "tenantId": "demo",
  "sourceService": "cs-chat",
  "sourceRef": "conversation_123",
  "taskKey": "answer-with-tools",
  "provider": "template",
  "instructions": "Usa solo tool autorizzati e restituisci JSON.",
  "input": {
    "question": "Quali servizi Core sono healthy?"
  },
  "outputSchema": {
    "type": "object",
    "properties": {
      "answer": { "type": "string" }
    },
    "required": ["answer"]
  },
  "toolPolicy": {
    "mode": "mcp",
    "allowedTools": ["platform_core_services_health"],
    "maxSteps": 2,
    "maxToolCalls": 2,
    "toolTimeoutMs": 120000,
    "maxRepeatedToolCalls": 1,
    "maxTotalTokens": 20000
  },
  "callback": {
    "url": "http://127.0.0.1:3004/api/internal/agent/events"
  }
}

La callback riceve executionId, tenantId, sourceService, sourceRef, taskKey, status, output, usage, toolTrace, error e metadata.

Run multi-agent

Le run multi-agent sono parent asincroni sopra execution esistenti. In v1 le strategie sono deterministiche:

  • parallel: avvia gli specialisti in parallelo; quando tutti completano, avvia un aggregator che produce l'output finale della run.
  • sequential: avvia un agente alla volta; ogni agente riceve gli output precedenti e l'ultimo output viene validato contro outputSchema parent.

Ogni node crea una normale AgentExecution figlia senza callback HTTP interna. I node possono essere inline o basati su profileKey: in questo secondo caso il servizio risolve l'ultima versione ACTIVE del profilo, copia i campi risolti sul node e salva profileKey, profileVersion e profileSnapshot per audit e replay deterministico. Le override ammesse sono provider, model, toolPolicy e input; instructions e outputSchema restano quelli del profilo.

Quando la child execution termina, il servizio aggiorna il node collegato e riaccoda la parent run. La callback della run viene inviata solo al termine del parent e include summary dei node senza gli output completi.

Durabilita del loop MCP

Ogni execution MCP registra step persistenti MODEL_ACTION, TOOL_CALL, FINAL_OUTPUT ed ERROR. toolTrace resta nel dettaglio execution e nella callback come campo derivato dagli step TOOL_CALL, cosi i consumer esistenti non devono cambiare contratto.

Il worker acquisisce un lease prima di processare una execution e aggiorna heartbeat prima di ogni chiamata modello o tool. All'avvio il servizio re-accoda le execution RUNNING con lease scaduto. POST /resume puo re-accodare solo execution QUEUED o RUNNING con lease scaduto; stati terminali come COMPLETED, FAILED, CALLBACK_FAILED e gli SKIPPED_* rispondono 409.

La ripresa e conservativa: se esiste una MODEL_ACTION riuscita senza tool successivo, il runtime riprende da quell'azione; se l'ultimo step riuscito e un TOOL_CALL, ricostruisce le observation e chiede il passo successivo al modello. Se un TOOL_CALL era STARTED senza output, la execution fallisce con interrupted tool result unknown: il runtime non riesegue tool potenzialmente mutanti quando il risultato precedente e incerto. Tool mutanti richiedono un progetto successivo con approval, idempotency key e audit esplicito.

Workspace reference: /Users/jeanpaul/projects/cs-repository