Skip to content

Exec Protocol

The exec protocol defines how PromptKit communicates with external tool, eval, and hook subprocesses. Two modes are supported:

  • One-shot (exec): Process spawned per invocation, JSON over stdin/stdout
  • Long-running (server): Process spawned once, JSON-RPC 2.0 over stdin/stdout
runtime → stdin: JSON request (single object)
runtime ← stdout: JSON response (single object)
runtime ← stderr: diagnostic logging (forwarded to runtime logger)
runtime ← exit: 0 = success, non-zero = error

The runtime spawns a new process for each invocation. The request is written to stdin as a single JSON object, and the response is read from stdout as a single JSON object.

runtime → stdin: {"jsonrpc":"2.0","id":N,"method":"execute","params":{...}}
runtime ← stdout: {"jsonrpc":"2.0","id":N,"result":{...}}

Line-delimited (one JSON object per line). The process stays alive across invocations.

FieldTypeDescription
jsonrpcstringAlways "2.0"
idintegerRequest ID (monotonically increasing)
methodstringAlways "execute"
paramsobjectRequest parameters
params.argsobjectTool arguments from the LLM
FieldTypeDescription
jsonrpcstringAlways "2.0"
idintegerMust match request ID
resultanySuccess result (mutually exclusive with error)
errorobjectError (mutually exclusive with result)
error.codeintegerError code
error.messagestringError message
{"args": {"city": "NYC", "units": "metric"}}

The args object contains the tool arguments as provided by the LLM, matching the tool’s input schema.

Success:

{"result": {"temp": 22, "condition": "cloudy"}}

Error:

{"error": "city not found"}

Pending (human-in-the-loop):

{"pending": {"reason": "requires_approval", "message": "Refund of $500 requires manager approval"}}

A pending response pauses the pipeline until the operation is resolved externally.

{
"type": "sentiment_check",
"params": {"language": "en"},
"content": "I'd be happy to help you with that!",
"context": {
"messages": [],
"turn_index": 1,
"tool_calls": [],
"variables": {},
"metadata": {}
}
}
FieldTypeDescription
typestringEval type name
paramsobjectEval-specific parameters from config
contentstringCurrent assistant output to evaluate
contextobjectConversation context
context.messagesarrayConversation history
context.turn_indexintegerCurrent turn index
context.tool_callsarrayTool calls made during this turn
context.variablesobjectTemplate variables in scope
context.metadataobjectArbitrary metadata from the pipeline
{
"score": 0.85,
"detail": "Sentiment polarity: 0.85",
"data": {"polarity": 0.85, "subjectivity": 0.6}
}
FieldTypeDescription
scorefloatNumeric score (0.0—1.0)
detailstringHuman-readable explanation
dataobjectArbitrary structured data
  • Assertions: AssertionEvalHandler checks min/max score thresholds to determine pass/fail.
  • Guardrails: GuardrailHookAdapter calls IsPassed() (score < 1.0 = fail) to enforce or monitor.
  • Standalone evals: Score is recorded as a metric.

Hooks receive a JSON object describing the hook type, phase, and event-specific payload.

before_call phase:

{
"hook": "provider",
"phase": "before_call",
"request": {
"provider_id": "anthropic-main",
"model": "claude-sonnet-4-20250514",
"messages": [],
"system_prompt": "You are a helpful assistant.",
"round": 1
}
}

after_call phase (includes a response field):

{
"hook": "provider",
"phase": "after_call",
"request": {
"provider_id": "anthropic-main",
"model": "claude-sonnet-4-20250514",
"messages": [],
"system_prompt": "You are a helpful assistant.",
"round": 1
},
"response": {
"provider_id": "anthropic-main",
"model": "claude-sonnet-4-20250514",
"message": {},
"latency_ms": 450
}
}

before_execution phase:

{
"hook": "tool",
"phase": "before_execution",
"request": {
"name": "db_query",
"args": {"query": "SELECT ..."},
"call_id": "call_abc123"
}
}

after_execution phase (includes a response field):

{
"hook": "tool",
"phase": "after_execution",
"request": {
"name": "db_query",
"args": {"query": "SELECT ..."},
"call_id": "call_abc123"
},
"response": {
"name": "db_query",
"call_id": "call_abc123",
"content": "{\"rows\": []}",
"latency_ms": 120
}
}
{
"hook": "session",
"phase": "session_start",
"event": {
"session_id": "sess_123",
"conversation_id": "conv_456",
"messages": [],
"turn_index": 0
}
}

Filter mode (provider and tool hooks):

{"allow": true}
{"allow": false, "reason": "PII detected in input"}
{"allow": false, "enforced": true, "reason": "PII redacted", "metadata": {"field": "ssn"}}
FieldTypeDescription
allowboolWhether to allow the operation
reasonstringDenial reason (when allow is false)
enforcedboolWhether the hook already applied enforcement
metadataobjectArbitrary metadata passed to the pipeline

Observe mode (session hooks):

{"ack": true}

Session hooks are observe-only and do not influence the pipeline.

  • One-shot: Non-zero exit code indicates an error. Stderr is captured for diagnostics.
  • Server: JSON-RPC error response. The process is restarted on unexpected termination.
  • Filter hooks: Process failure results in deny (fail-closed safety).
  • Observe hooks: Process failure is swallowed. The pipeline always continues.
  • Exec Tools (how-to)
  • Exec Hooks (how-to)
  • RuntimeConfig (reference)