Agent Runtime — Hooks API
Table of Contents
upsquad/runtime/hook/v0alpha1/hook_service.proto
hook_service.proto — HookService gRPC bridge between the Python agent-worker and the Go-side hook registry (LLD 15 primitives).
Founder decision Q5 (#380): the package is tagged v0alpha to mark the
shape as unstable while Wave 4A burns in. The wire-format package
name is v0alpha1 because buf's STANDARD lint rules require a
canonical vN[alpha|beta]M suffix — that is the buf-compliant
spelling of the intent behind Q5. Once the four lifecycle points
stabilise (post-LLD 17 audit enrichment, post-LLD 18 client
dashboards), a migration to upsquad.runtime.hook.v1 lands in its
own proto, with v0alpha1 kept for one release for rollback.
Why this service exists
The Go core owns the Wave 4A hook dispatcher (LLD 15 §3.3 — one Registry per binary). Blocking on_session_start and pre_llm_call hooks run before any Python-side work begins, so they are invoked inside Go. But the LangGraph worker (services/agent-worker) also needs to fire two async points — post_tool_call and on_error — at the moment they occur inside Python code. Rather than maintain a second hook registry in Python, the worker reaches across the process boundary via this RPC and lets the Go registry dispatch.
Latency invariant (§4 of task prompt): FireHook P95 < 30 ms including proto marshal + network hop + Go-side InvokeBlocking. BudgetPreLLMCallAggregate (LLD 15 threshold.go) is 30 ms for the whole point, so the bridge has to keep its own overhead below ~5 ms on localhost for the budget to leave any room for real hook work. FireHookAsync pays only the enqueue cost — the Go-side async worker pool drains the task after the RPC returns.
FireHookAsyncRequest
FireHookAsyncRequest is the request type for the fire-and-forget FireHookAsync RPC.
| Field | Type | Label | Description |
|---|---|---|---|
| common | HookRequestCommon | common carries the shared identity + payload fields. |
FireHookAsyncResponse
FireHookAsyncResponse is returned as soon as the async task is on the Go-side queue. Always empty — the caller does not observe the hook result. Present only because gRPC requires a typed response.
FireHookRequest
FireHookRequest is the request type for the blocking FireHook RPC.
| Field | Type | Label | Description |
|---|---|---|---|
| common | HookRequestCommon | common carries the shared identity + payload fields. |
FireHookResponse
FireHookResponse is the response type for the blocking FireHook RPC.
| Field | Type | Label | Description |
|---|---|---|---|
| output | google.protobuf.Struct | output is the merged hook output. Shape mirrors the LLD 15 typed Out structs (PreLLMCallOut / OnSessionStartOut). Empty when no hooks are registered for the point. |
HookRequestCommon
HookRequestCommon carries the identity fields shared by both RPC request types. Kept as a nested message so the linter's RPC_REQUEST_STANDARD_NAME rule is satisfied by distinct FireHookRequest / FireHookAsyncRequest wrappers.
| Field | Type | Label | Description |
|---|---|---|---|
| hook_name | string | hook_name is the canonical point name — one of "on_session_start", "pre_llm_call", "post_tool_call", or "on_error". An unknown value returns InvalidArgument. | |
| session_id | string | session_id is the UUID of the agent session firing the hook. Used by the Go-side disable store (LLD 15 §3.2 founder Q3) to scope per-session circuit-breaker state. REQUIRED. | |
| org_id | string | org_id is the tenant UUID. Propagated to audit hooks in LLD 17. REQUIRED. | |
| agent_id | string | agent_id is the agent UUID. Set when the hook has an associated agent (all four points do). REQUIRED. | |
| payload | google.protobuf.Struct | payload carries the typed per-point input as a JSON-like Struct. The server-side translator expects field names matching the Go struct (e.g. PreLLMCallIn has Model, Messages, Tools, LoopCount, UserID). Unknown fields are ignored — the contract is "new fields are additive" so Python and Go can roll out independently. |
HookService
HookService is the Go-side gRPC entry point invoked by the Python agent-worker to run Wave 4A hooks. Two RPCs, one per flavour:
- FireHook — blocking; returns the merged hook output or an error. Used for pre_llm_call and on_session_start when the caller is Python.
- FireHookAsync — fire-and-forget; enqueues and returns immediately. Used for post_tool_call and on_error.
Wiring (LLD 16 §Wiring and shelfware gate): this service is registered on the agent-orchestrator gRPC server (main.go → runtimeserver.Config.HookRegistry) and consumed by the worker-side client in agent_worker/hooks.py.
| Method Name | Request Type | Response Type | Description |
|---|---|---|---|
| FireHook | FireHookRequest | FireHookResponse | FireHook invokes a blocking hook (on_session_start / pre_llm_call). Returns FireHookResponse on success, or a gRPC status with code=FailedPrecondition when the hook itself returned an error. Transient errors (Unavailable/DeadlineExceeded) are safe to retry; FailedPrecondition is not. |
| FireHookAsync | FireHookAsyncRequest | FireHookAsyncResponse | FireHookAsync enqueues a fire-and-forget hook (post_tool_call / on_error). The server returns as soon as the task is on the registry's async queue. Drops on a full queue are recorded on the Go side and surface only via metrics — the client never sees an error for a drop. |
Scalar Value Types
| .proto Type | Notes | C++ | Java | Python | Go | C# | PHP | Ruby |
|---|---|---|---|---|---|---|---|---|
| double | double | double | float | float64 | double | float | Float | |
| float | float | float | float | float32 | float | float | Float | |
| int32 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) |
| int64 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 | long | int/long | int64 | long | integer/string | Bignum |
| uint32 | Uses variable-length encoding. | uint32 | int | int/long | uint32 | uint | integer | Bignum or Fixnum (as required) |
| uint64 | Uses variable-length encoding. | uint64 | long | int/long | uint64 | ulong | integer/string | Bignum or Fixnum (as required) |
| sint32 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) |
| sint64 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. | int64 | long | int/long | int64 | long | integer/string | Bignum |
| fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 2^28. | uint32 | int | int | uint32 | uint | integer | Bignum or Fixnum (as required) |
| fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 2^56. | uint64 | long | int/long | uint64 | ulong | integer/string | Bignum |
| sfixed32 | Always four bytes. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) |
| sfixed64 | Always eight bytes. | int64 | long | int/long | int64 | long | integer/string | Bignum |
| bool | bool | boolean | boolean | bool | bool | boolean | TrueClass/FalseClass | |
| string | A string must always contain UTF-8 encoded or 7-bit ASCII text. | string | String | str/unicode | string | string | string | String (UTF-8) |
| bytes | May contain any arbitrary sequence of bytes. | string | ByteString | str | []byte | ByteString | string | String (ASCII-8BIT) |