The plain-HTTP surface and executor
The `/health`, `/execute`, and cancel endpoints, the NDJSON event stream, and how the executor and entry point wire them up.
The creator's default surface is a plain HTTP server built on Starlette. The skill calls it A2A; the server's own docstring describes it as a plain-HTTP server that replaces A2A. It streams newline-delimited JSON. AMC does not call this surface: for AMC, use the AMC variant.
Endpoints
| Method and path | Behavior |
|---|---|
GET /health | Calls the health probe and returns its JSON, always with HTTP 200. Readiness is in the body's ready field. A probe that raises returns status: "down". |
POST /execute | Runs one turn and streams events as application/x-ndjson. |
POST /tasks/{task_id}/cancel | Best-effort cancel. Returns {"ok": true, "task_id": ..., "in_flight": ...}. |
The /execute request
{
"context_id": "ctx-1",
"task_id": "task-1",
"message": "Build this week's report",
"files": [{ "name": "brief.pdf", "uri": "https://<your-storage>.example.com/brief.pdf", "mime": "application/pdf" }],
"metadata": { "triggered_by_email": "user@contoso.com" },
"resume_token": null
}context_id and task_id are required; a missing one returns 400, as does a body that is not a JSON object. The other fields are optional.
The NDJSON event stream
| `kind` | Fields | Terminal |
|---|---|---|
status | text | No |
file | name, uri, mime | No |
token_usage | prompt_tokens, completion_tokens, total_tokens | No |
result | text, summary | Yes |
error | message | Yes |
hitl_input_required | wait_info, resume_token | Yes |
paused | reason, resume_token | Yes |
{"kind": "status", "text": "🤖 Build this week's report"}
{"kind": "status", "text": "🛠️ Remote agent executing: Loading skill: report-generator"}
{"kind": "status", "text": "🛠️ Remote agent executing: Fetching sources"}
{"kind": "status", "text": "🛠️ Remote agent executing: Rendering output"}
{"kind": "token_usage", "prompt_tokens": 41200, "completion_tokens": 2300, "total_tokens": 43500}
{"kind": "result", "text": "Report written to /outputs/report.html ...", "summary": "Report written to /outputs/report.html ..."}- The run must yield exactly one terminal event. The server drops anything after it.
- If the run ends without a terminal event, the server appends
{"kind": "error", "message": "agent completed without terminal event"}. - An exception inside the run becomes
errorwith<ExceptionType>: <message>. summaryis the first 200 characters of the final text.- The template never emits
hitl_input_requiredorpaused. They exist in the contract only.
Concurrency and cancellation
A semaphore caps concurrent /execute streams in one process at AGENT_MAX_CONCURRENCY (default 3, with a fallback to 3 if the value is not an integer). Extra requests queue instead of running in parallel without bound.
A cancel is recorded against the task_id and checked before each event is sent. When it is seen, the stream ends with {"kind": "error", "message": "cancelled"}. A cancel that arrives before /execute starts is kept for 300 seconds, so the race is handled and the map stays bounded.
The entry point
__main__.py loads .env, initializes the shared agent at startup, and starts uvicorn on AGENT_HOST (default 0.0.0.0) and AGENT_PORT (default {{PORT}}). If startup init fails, it prints a warning and starts anyway; the first request then tries to create the agent lazily.
async def _health() -> Dict[str, Any]:
use_foundry = os.getenv("CLAUDE_CODE_USE_FOUNDRY") == "1"
has_api_key = bool(os.getenv("ANTHROPIC_API_KEY"))
has_foundry_resource = bool(os.getenv("ANTHROPIC_FOUNDRY_RESOURCE"))
has_foundry = use_foundry and has_foundry_resource and has_api_key
has_anthropic_direct = has_api_key and not use_foundry
outputs_dir = os.getenv("REPORT_OUTPUTS_DIR", "/outputs")
outputs_writable = os.path.isdir(outputs_dir) and os.access(outputs_dir, os.W_OK)
missing: List[str] = []
if not (has_anthropic_direct or has_foundry):
missing.append("ANTHROPIC_API_KEY (Anthropic-direct), or "
"CLAUDE_CODE_USE_FOUNDRY=1 + ANTHROPIC_FOUNDRY_RESOURCE + ANTHROPIC_API_KEY (Foundry)")
if not outputs_writable:
missing.append(f"writable outputs dir at {outputs_dir}")
if not missing:
return {"status": "ok", "ready": True, "detail": "Fully configured"}
return {"status": "not_configured", "ready": False,
"detail": f"Missing: {', '.join(missing)}"}_run_turn maps the core's strings to events: tool lines become deduplicated status events, lines starting with the progress emoji (🤖, 🧠, 🔍, 📝, 📤, 📥) become status, an Error: line becomes a terminal error, and anything else is collected as response text. After the stream it emits token_usage (when the core captured it) and then result with the last response.
The executor
claude_skills_agent_executor.py holds one shared agent instance at class level, guarded by an asyncio.Lock. initialize_at_startup() builds it once. _get_or_create_thread() maps each context_id to a session id. Each /execute call is still an independent single turn: the generated README lists single-turn behavior and ephemeral state as known limitations.
Related
Last updated 2026-10-21