From 5d0b8da0d036064ce4089837aff082c4e8bfc15d Mon Sep 17 00:00:00 2001 From: Steven Crothers Date: Fri, 24 Jul 2026 12:42:45 -0400 Subject: [PATCH 1/7] ci: compare buf breaking against latest release tag, not base branch proto.md's stated policy is that the wire-compat guarantee applies to any v1 (or later-released) package, compared against the last released tag -- not to every commit since repo inception. The workflow instead diffed unconditionally against the PR's base branch tip, which would fail any intentional pre-release break (starting with the upcoming provider->model rename) even though no v* tag has ever shipped this protocol. Skip the check entirely until the first v* tag exists, then compare against it. --- .github/workflows/ci.yml | 34 +++++++++++++++++++++++----------- CONTRIBUTING.md | 2 +- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7c86c5..affed8e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -176,12 +176,15 @@ jobs: # # 1. buf lint style/naming per buf.yaml (STANDARD rule set) # 2. buf format formatting drift in .proto sources - # 3. buf breaking wire-compat vs the PR base branch (FILE rule set). - # Plugins are versioned artifacts resolved at runtime — - # an unnoticed contract break strands every provider - # built against the old wire format. An intentional - # break must be visible: this check forces the PR to - # say so. + # 3. buf breaking wire-compat vs the latest v* release tag (FILE rule + # set), per .claude/rules/proto.md's policy: the + # permanence guarantee applies from the first released + # tag onward, not from repo inception. Skipped entirely + # when no v* tag exists yet. Plugins are versioned + # artifacts resolved at runtime — an unnoticed contract + # break strands every provider built against the old + # wire format. An intentional break must be visible: + # this check forces the PR to say so. # 4. drift check pkg/*/proto/v1 is 100% derived output and must match # what the pinned generators produce from api/ exactly. # Catches both hand-edited .pb.go files and .proto @@ -216,13 +219,22 @@ jobs: - name: Format check run: buf format --diff --exit-code - # The base ref isn't fetched on a shallow PR checkout — fetch it into - # a real remote-tracking ref so buf can diff against it. - - name: Breaking-change check (vs base branch) + # Compares against the latest v* release tag, not the PR base branch — + # .claude/rules/proto.md's wire-compat guarantee is scoped to "any v1 + # (or later-released) package", not to every commit since repo + # inception. A shallow PR checkout doesn't have tags by default, so + # fetch them explicitly. + - name: Breaking-change check (vs latest release tag) if: github.event_name == 'pull_request' run: | - git fetch origin "${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}" - buf breaking --against ".git#ref=refs/remotes/origin/${{ github.base_ref }}" + git fetch --tags origin + latest_tag="$(git tag -l 'v*' --sort=-v:refname | head -n1)" + if [ -z "$latest_tag" ]; then + echo "No v* release tag exists yet — skipping buf breaking (proto.md: the wire-compat guarantee applies from the first released tag onward)." + exit 0 + fi + echo "Comparing against $latest_tag" + buf breaking --against ".git#tag=$latest_tag" # Generator versions are pinned by go.mod `tool` directives, so this # regeneration is byte-for-byte reproducible. Binaries go to bin/ per diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fb7630f..1cee8cd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,7 @@ GOBIN=$PWD/bin go install tool # generators pinned via go.mo PATH=$PWD/bin:$PATH buf generate # regenerate pkg/*/proto/v1 — commit the result ``` -CI additionally runs `buf breaking` against the PR base branch — a wire-contract break fails the build by design; if it's intentional, say so explicitly in the PR. +CI additionally runs `buf breaking` against the latest `v*` release tag (skipped when no tag exists yet) — a wire-contract break fails the build by design; if it's intentional, say so explicitly in the PR. Ground rules the gate can't fully check: From 07e4ff883ec4704cd3ab94bc755eab9415449689 Mon Sep 17 00:00:00 2001 From: Steven Crothers Date: Fri, 24 Jul 2026 13:04:29 -0400 Subject: [PATCH 2/7] Rename model-provider category provider -> model Merge provider.v1 into the existing model.v1 package: ProviderService becomes ModelService, ProviderError/ProviderErrorCategory become ModelError/ModelErrorCategory, StreamEvent.Usage is promoted to a top-level Usage message, and CATEGORY_PROVIDER becomes CATEGORY_MODEL (numeric value unchanged). Rename docs/specifications/provider/ to model/ and fix every inbound cross-reference (specs, mkdocs nav, CLAUDE.md, CONTRIBUTING.md, .claude/rules, first-party catalog, proto doc comments). Update hand-written Go (agentprofile, pluginruntime, pkg/common, statebackend) and regenerate pkg/ stubs; pkg/provider is deleted, pkg/model gains the service stubs. Nothing is released yet (no v* tag), so this is the last free window for wire-breaking renames. --- .claude/rules/determinism.md | 2 +- .claude/rules/grpc.md | 18 +- .claude/rules/proto.md | 8 +- CLAUDE.md | 2 +- CONTRIBUTING.md | 2 +- .../agent/common/v1/common.proto | 4 +- .../agent/config/v1/config.proto | 2 +- .../agent/content/v1/content.proto | 20 +- .../agent/context/v1/context.proto | 2 +- .../agent/kernel/v1/kernel.proto | 4 +- .../agent/memory/v1/memory.proto | 2 +- .../agent/model/v1/model.proto | 615 +++- api/pluggableharness/agent/plan/v1/plan.proto | 2 +- .../agent/provider/v1/provider.proto | 599 ---- .../agent/render/v1/render.proto | 2 +- .../agent/schema/v1/schema.proto | 10 +- .../agent/slashcommand/v1/slashcommand.proto | 2 +- api/pluggableharness/agent/tool/v1/tool.proto | 8 +- docs/first-party/providers/README.md | 2 +- docs/first-party/providers/anthropic.md | 18 +- docs/first-party/providers/google.md | 16 +- docs/first-party/providers/openai.md | 14 +- docs/first-party/providers/xai.md | 12 +- docs/first-party/tools/image-vision.md | 2 +- docs/first-party/tools/mcp-client.md | 2 +- docs/index.md | 2 +- docs/specifications/README.md | 2 +- docs/specifications/agent-loop/README.md | 2 +- .../agent-loop/error-recovery.md | 6 +- docs/specifications/agent-loop/subagents.md | 2 +- .../agent-loop/turn-algorithm.md | 2 +- docs/specifications/architecture.md | 8 +- docs/specifications/configuration/README.md | 2 +- .../configuration/agent-profiles.md | 2 +- .../configuration/settings-and-global.md | 2 +- docs/specifications/context/README.md | 2 +- docs/specifications/context/conformance.md | 2 +- docs/specifications/context/data-types.md | 2 +- docs/specifications/context/examples.md | 2 +- docs/specifications/context/protocol.md | 2 +- docs/specifications/conventions.md | 4 +- docs/specifications/frontend/README.md | 2 +- .../frontend/frontend-protocol.md | 6 +- docs/specifications/frontend/render-tree.md | 2 +- .../frontend/widget-protocol.md | 4 +- docs/specifications/glossary.md | 4 +- docs/specifications/kernel-callbacks.md | 6 +- docs/specifications/memory/README.md | 2 +- docs/specifications/memory/protocol.md | 2 +- .../{provider => model}/README.md | 2 +- .../{provider => model}/conformance.md | 2 +- .../{provider => model}/data-types.md | 4 +- .../{provider => model}/examples.md | 2 +- .../{provider => model}/protocol.md | 0 docs/specifications/state-backend.md | 4 +- docs/specifications/tool/README.md | 6 +- docs/specifications/tool/conformance.md | 4 +- docs/specifications/tool/data-types.md | 2 +- docs/specifications/tool/examples.md | 2 +- docs/specifications/tool/protocol.md | 10 +- internal/agentprofile/doc.go | 2 +- internal/agentprofile/model.go | 12 +- internal/agentprofile/model_test.go | 32 +- internal/agentprofile/tools.go | 2 +- internal/agentprofile/types.go | 2 +- internal/kernelcallback/server_test.go | 2 +- internal/pluginruntime/README.md | 2 +- internal/pluginruntime/adapter.go | 6 +- internal/pluginruntime/adapter_test.go | 12 +- internal/pluginruntime/doc.go | 2 +- internal/pluginruntime/launch.go | 2 +- internal/statebackend/doc.go | 4 +- internal/statebackend/event.go | 4 +- internal/telemetry/span_test.go | 2 +- mkdocs.yml | 12 +- pkg/common/plugin.go | 4 +- pkg/common/plugin_test.go | 2 +- pkg/common/proto/v1/common.pb.go | 14 +- pkg/config/proto/v1/config.pb.go | 2 +- pkg/content/proto/v1/content.pb.go | 20 +- pkg/context/proto/v1/context.pb.go | 2 +- pkg/kernel/proto/v1/kernel.pb.go | 4 +- pkg/memory/proto/v1/memory_grpc.pb.go | 4 +- pkg/model/proto/v1/model.pb.go | 2707 ++++++++++++++++- .../proto/v1/model_grpc.pb.go} | 192 +- pkg/plan/proto/v1/plan.pb.go | 2 +- pkg/provider/proto/v1/provider.pb.go | 2630 ---------------- pkg/render/proto/v1/render.pb.go | 2 +- pkg/schema/proto/v1/schema.pb.go | 10 +- pkg/slashcommand/proto/v1/slashcommand.pb.go | 2 +- pkg/tool/proto/v1/tool.pb.go | 6 +- pkg/tool/proto/v1/tool_grpc.pb.go | 4 +- 92 files changed, 3557 insertions(+), 3628 deletions(-) delete mode 100644 api/pluggableharness/agent/provider/v1/provider.proto rename docs/specifications/{provider => model}/README.md (87%) rename docs/specifications/{provider => model}/conformance.md (96%) rename docs/specifications/{provider => model}/data-types.md (95%) rename docs/specifications/{provider => model}/examples.md (99%) rename docs/specifications/{provider => model}/protocol.md (100%) rename pkg/{provider/proto/v1/provider_grpc.pb.go => model/proto/v1/model_grpc.pb.go} (58%) delete mode 100644 pkg/provider/proto/v1/provider.pb.go diff --git a/.claude/rules/determinism.md b/.claude/rules/determinism.md index 6b51076..62c4dac 100644 --- a/.claude/rules/determinism.md +++ b/.claude/rules/determinism.md @@ -61,7 +61,7 @@ formula in the codebase: ## Cost and budget rollup -Cost (`docs/specifications/provider/protocol.md#cost-computation`) and depth +Cost (`docs/specifications/model/protocol.md#cost-computation`) and depth budget (`docs/specifications/agent-loop/subagents.md#depth-limits`) both roll up a session tree the same way: computed and persisted at usage-event time, not recomputed lazily on read. Code that reports a session's total cost or diff --git a/.claude/rules/grpc.md b/.claude/rules/grpc.md index 4311de0..790c5da 100644 --- a/.claude/rules/grpc.md +++ b/.claude/rules/grpc.md @@ -15,7 +15,7 @@ dictated by the specs and MUST match exactly. | Category | RPC | Shape | Source | |---|---|---|---| -| Model | `StreamCompletion` | server-streaming + cancellation | `docs/specifications/provider/README.md#transport--lifecycle` (explicitly *not* bidi) | +| Model | `StreamCompletion` | server-streaming + cancellation | `docs/specifications/model/README.md#transport--lifecycle` (explicitly *not* bidi) | | Tool | `Invoke` | server-streaming | `docs/specifications/tool/protocol.md` | | Frontend | `Attach` | **bidirectional** streaming | `docs/specifications/frontend/frontend-protocol.md` | | Widget | `Attach` | server-streaming only | `docs/specifications/frontend/widget-protocol.md` | @@ -28,7 +28,7 @@ the spec calls for. - **A backend that has no real streaming to do still implements the streaming RPC shape** and emits exactly one terminal message. Do not add a - parallel non-streaming RPC as a shortcut — `docs/specifications/provider/` + parallel non-streaming RPC as a shortcut — `docs/specifications/model/` and `docs/specifications/tool/` both make the streaming signature MUST regardless of whether the underlying vendor API streams. - **Cancellation is normal control flow, not an error.** When the kernel @@ -49,12 +49,12 @@ Canonical mapping (extend per spec, don't invent parallel categories): | Spec category | `grpc/codes` | |---|---| -| `context_length_exceeded` (`docs/specifications/provider/conformance.md`) | `codes.ResourceExhausted` | -| `rate_limited` (`docs/specifications/provider/conformance.md`) | `codes.ResourceExhausted` (distinguished by structured detail, not code alone) | -| `overloaded` (`docs/specifications/provider/conformance.md`) | `codes.Unavailable` | -| `auth_error` (`docs/specifications/provider/conformance.md`) | `codes.Unauthenticated` | -| `invalid_request` (`docs/specifications/provider/conformance.md`) | `codes.InvalidArgument` | -| `content_filtered` (`docs/specifications/provider/conformance.md`) | `codes.FailedPrecondition` | +| `context_length_exceeded` (`docs/specifications/model/conformance.md`) | `codes.ResourceExhausted` | +| `rate_limited` (`docs/specifications/model/conformance.md`) | `codes.ResourceExhausted` (distinguished by structured detail, not code alone) | +| `overloaded` (`docs/specifications/model/conformance.md`) | `codes.Unavailable` | +| `auth_error` (`docs/specifications/model/conformance.md`) | `codes.Unauthenticated` | +| `invalid_request` (`docs/specifications/model/conformance.md`) | `codes.InvalidArgument` | +| `content_filtered` (`docs/specifications/model/conformance.md`) | `codes.FailedPrecondition` | | `process_crashed` (`docs/specifications/tool/conformance.md`'s `ToolErrorCategory`) | `codes.Unavailable` | | cancellation | `codes.Canceled` — never treated as an application error | | unmapped/unexpected | `codes.Internal`, never `codes.Unknown` | @@ -78,7 +78,7 @@ kernel decide. ## The strong-typing rule and its one carve-out `proto.md` bans `Any`/untyped `bytes`/loose maps as a general rule. The -Emit→Render→Paint payload (`docs/specifications/provider/protocol.md#render`, +Emit→Render→Paint payload (`docs/specifications/model/protocol.md#render`, `docs/specifications/frontend/render-tree.md`) is the one deliberate exception: it is opaque *by design* so a producer's payload format can evolve independently of the kernel. Do not "fix" this by giving it a concrete message type — that diff --git a/.claude/rules/proto.md b/.claude/rules/proto.md index 916adcc..7fafcf4 100644 --- a/.claude/rules/proto.md +++ b/.claude/rules/proto.md @@ -12,7 +12,7 @@ is derived and never hand-edited (see `plugin-runtime.md`). ## Syntax and packaging - `syntax = "proto3";` always. -- Package per category, per version: `package pluggableharness.agent..v1;` — `pluggableharness.agent.provider.v1`, `pluggableharness.agent.tool.v1`, `pluggableharness.agent.memory.v1`, `pluggableharness.agent.context.v1`, `pluggableharness.agent.frontend.v1`, `pluggableharness.agent.widget.v1`, `pluggableharness.agent.kernel.v1` (the kernel-callback service). File path mirrors the package under buf's module root (`buf.yaml`'s `api` module): `api/pluggableharness/agent//v1/.proto`. +- Package per category, per version: `package pluggableharness.agent..v1;` — `pluggableharness.agent.model.v1`, `pluggableharness.agent.tool.v1`, `pluggableharness.agent.memory.v1`, `pluggableharness.agent.context.v1`, `pluggableharness.agent.frontend.v1`, `pluggableharness.agent.widget.v1`, `pluggableharness.agent.kernel.v1` (the kernel-callback service). File path mirrors the package under buf's module root (`buf.yaml`'s `api` module): `api/pluggableharness/agent//v1/.proto`. - `option go_package = "github.com/pluggableharness/agent/pkg//proto/v1;v1";` on every file — explicit, never inferred, **always** the full module-qualified path (`github.com/pluggableharness/agent/...`). This *is* required, not optional: `protoc-gen-go` embeds `go_package`'s path verbatim into every cross-file Go `import` statement it generates, so any package imported by another proto (which is every shared package in this repo) needs the real importable path or the generated code fails to compile. Getting this backwards — omitting the module prefix on the theory that `out: .` already means repo-root — was an actual bug caught during Wave B integration: it compiled fine for a standalone, never-imported file, then broke the moment a second file imported it, producing `import "pkg/config/proto/v1"` instead of `import "github.com/pluggableharness/agent/pkg/config/proto/v1"`. - `buf.gen.yaml`'s Go plugins run with `out: .` **and** `opt: module=github.com/pluggableharness/agent`. This `module` option is what reconciles the full-path `go_package` above with landing output at the intended repo-root-relative `pkg//proto/v1/` instead of a redundant nested `./github.com/pluggableharness/agent/pkg/.../` — it tells `protoc-gen-go` to keep the full path for generated import statements but strip that same prefix when computing where to *write* the file relative to `out`. This is why `api/`'s tree (which mirrors the full dotted package name, `api/pluggableharness/agent/...`) and `pkg/`'s tree (which doesn't) intentionally look different — see `go-layout.md`. Do not drop the `module` opt from `buf.gen.yaml` to "simplify" it — that's the line that makes the split possible. @@ -28,7 +28,7 @@ strongly typed as the Go code that implements it. for. If a field's shape varies by category or plugin, model it as a `oneof` of named messages, not `Any` or a `bytes` blob — with **one explicit, spec-documented exception**: the emit/render payload itself. - `docs/specifications/provider/protocol.md` and `docs/specifications/frontend/render-tree.md` + `docs/specifications/model/protocol.md` and `docs/specifications/frontend/render-tree.md` define the Emit→Render→Paint payload as deliberately opaque (kernel and other plugins don't interpret it) — that field stays `bytes`, and only that field. - No untyped `map` or `map` standing in for a @@ -56,7 +56,7 @@ strongly typed as the Go code that implements it. that implements it. - Every `service` has a comment naming which `docs/specifications/` document it implements, e.g. `// ModelService implements the model provider - protocol described in docs/specifications/provider/protocol.md.` + protocol described in docs/specifications/model/protocol.md.` ## Versioning — no breaking changes @@ -69,6 +69,6 @@ strongly typed as the Go code that implements it. work: an old session must always be decodable by a client built against a newer proto, because the field numbers and types of a released message never change. -- A breaking change is shipped as a new package version (`pluggableharness.agent.provider.v2`), never as an edit to `v1`. It lands at `api/pluggableharness/agent/provider/v2/` and generates into `pkg/provider/proto/v2/`, alongside — never replacing — `v1`. The old `v1` service stays defined and generated as long as any retained session was produced by a `v1` plugin. +- A breaking change is shipped as a new package version (`pluggableharness.agent.model.v2`), never as an edit to `v1`. It lands at `api/pluggableharness/agent/model/v2/` and generates into `pkg/model/proto/v2/`, alongside — never replacing — `v1`. The old `v1` service stays defined and generated as long as any retained session was produced by a `v1` plugin. - Field numbers are never reused, even for removed fields — `reserved N;` and `reserved "field_name";` on removal. diff --git a/CLAUDE.md b/CLAUDE.md index e6e68f3..8190064 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ An AI coding harness built as a Go microkernel: the kernel owns plugin lifecycle | Path | Contents | |---|---| -| `docs/specifications/` | The protocol contracts: one directory per plugin category (`provider/` = model, `tool/`, `context/`, `memory/`, `frontend/` incl. widgets) plus kernel behavior (`agent-loop/`, `configuration/`, `kernel-callbacks.md`, `state-backend.md`) | +| `docs/specifications/` | The protocol contracts: one directory per plugin category (`model/`, `tool/`, `context/`, `memory/`, `frontend/` incl. widgets) plus kernel behavior (`agent-loop/`, `configuration/`, `kernel-callbacks.md`, `state-backend.md`) | | `docs/first-party/` | Separate first-party catalog (tools, model providers). **Not** the tool *protocol* — that's `docs/specifications/tool/` | | `api/` | `.proto` sources, buf module root (`buf.yaml`, `buf.gen.yaml`) | | `internal/` | Kernel-side implementation — config, registry, policy, agentprofile, pluginruntime, kernelcallback, telemetry, log, hclsecret, producer | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1cee8cd..c8acc10 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,7 +51,7 @@ Ground rules the gate can't fully check: `docs/specifications/` has two hard editorial rules, defined in [`docs/specifications/conventions.md`](docs/specifications/conventions.md): -- Cross-references are relative path + heading anchor (`[cost computation](provider/protocol.md#cost-computation)`), never section numbers. Before renaming a heading, grep the tree for its anchor — inbound links break silently. +- Cross-references are relative path + heading anchor (`[cost computation](model/protocol.md#cost-computation)`), never section numbers. Before renaming a heading, grep the tree for its anchor — inbound links break silently. - Fix-forward: the docs describe the system as it is. Corrections are written as current, unqualified truth — no strikethrough, no "previously this said" narrative. All Markdown in this repo is GitHub Flavored Markdown with one unwrapped line per paragraph — no hard-wrapping at a fixed column. diff --git a/api/pluggableharness/agent/common/v1/common.proto b/api/pluggableharness/agent/common/v1/common.proto index a454595..228fcf3 100644 --- a/api/pluggableharness/agent/common/v1/common.proto +++ b/api/pluggableharness/agent/common/v1/common.proto @@ -36,8 +36,8 @@ enum Category { // Zero value. Never valid for a real producer; its presence on the wire // means a caller forgot to set the field. CATEGORY_UNSPECIFIED = 0; - // A model (LLM vendor) provider — specifications/provider.md. - CATEGORY_PROVIDER = 1; + // A model (LLM vendor) provider — specifications/model.md. + CATEGORY_MODEL = 1; // A tool provider — specifications/tool.md. CATEGORY_TOOL = 2; // A context provider — specifications/context.md. diff --git a/api/pluggableharness/agent/config/v1/config.proto b/api/pluggableharness/agent/config/v1/config.proto index 00248ea..0f0808e 100644 --- a/api/pluggableharness/agent/config/v1/config.proto +++ b/api/pluggableharness/agent/config/v1/config.proto @@ -37,7 +37,7 @@ message ConfigAttribute { // Whether agent.hcl MUST set this attribute. The kernel MUST reject a // Configure call with a missing required attribute via a structured - // error, per provider.md §3 / tool.md §3's shared Configure contract. + // error, per model.md §3 / tool.md §3's shared Configure contract. bool required = 3; // MUST be true for any attribute that can hold a secret (API keys, diff --git a/api/pluggableharness/agent/content/v1/content.proto b/api/pluggableharness/agent/content/v1/content.proto index cd0104b..4c4cf50 100644 --- a/api/pluggableharness/agent/content/v1/content.proto +++ b/api/pluggableharness/agent/content/v1/content.proto @@ -1,7 +1,7 @@ syntax = "proto3"; // Package pluggableharness.agent.content.v1 defines the canonical content-block message -// schema described in specifications/provider.md §5 — the state backend's +// schema described in specifications/model.md §5 — the state backend's // source of truth for conversation history (state-backend.md §5's `message` // event kind). Every model-provider adapter translates its own vendor // format to and from exactly this schema; nothing else in the system @@ -30,7 +30,7 @@ enum Role { } // Message is one turn in the canonical conversation history: a role plus -// an ordered list of content blocks. provider.md §5 is the source of these +// an ordered list of content blocks. model.md §5 is the source of these // semantics. message Message { // Which party produced this message. MUST be set. @@ -43,7 +43,7 @@ message Message { // ContentBlock is one block within a Message. Exactly one variant is set. // Which variants a given model MAY produce/accept is gated by that model's -// ModelSpec capability flags (provider.md §2, §5): `text` MUST work both +// ModelSpec capability flags (model.md §2, §5): `text` MUST work both // directions unconditionally; `image` requires supports_vision; `tool_use`/ // `tool_result` require supports_tool_use; `thinking`/`redacted_thinking` // require ThinkingSpec.supported. @@ -59,7 +59,7 @@ message ContentBlock { } // TextBlock is plain conversational text. MUST be supported by every -// model, in both directions (provider.md §5). +// model, in both directions (model.md §5). message TextBlock { // The block's text content. string text = 1; @@ -67,7 +67,7 @@ message TextBlock { // ToolUseBlock represents the model requesting a tool invocation. `id` // correlates this block to the resulting ToolResultBlock, mirroring -// provider.md §4's StreamEvent tool_call_start/tool_call_done id +// model.md §4's StreamEvent tool_call_start/tool_call_done id // correlation once the stream has been assembled into a persisted message. message ToolUseBlock { // Correlation id for the matching ToolResultBlock. @@ -104,9 +104,9 @@ message ToolResultBlock { } // ImageBlock is inline image content. Requires the target model's -// ModelSpec.supports_vision (provider.md §5); the kernel MUST reject an +// ModelSpec.supports_vision (model.md §5); the kernel MUST reject an // ImageBlock sent to a model where that flag is false, with -// invalid_request (provider.md §8). +// invalid_request (model.md §8). message ImageBlock { // Raw image bytes. bytes data = 1; @@ -116,16 +116,16 @@ message ImageBlock { } // ThinkingBlock is the model's extended-reasoning output, when -// ThinkingSpec.supported (provider.md §2). Requires the model's +// ThinkingSpec.supported (model.md §2). Requires the model's // ThinkingSpec.supported to be true. message ThinkingBlock { - // The accumulated reasoning text (provider.md §4's thinking_delta + // The accumulated reasoning text (model.md §4's thinking_delta // StreamEvent variants, assembled into one block once the turn // completes). string text = 1; // An opaque vendor integrity token, when the vendor's thinking blocks - // carry one (provider.md §4's thinking_signature StreamEvent variant). + // carry one (model.md §4's thinking_signature StreamEvent variant). // The kernel MUST store and round-trip this verbatim without // interpreting it — it is meaningful only to the vendor that issued it. bytes signature = 2; diff --git a/api/pluggableharness/agent/context/v1/context.proto b/api/pluggableharness/agent/context/v1/context.proto index 3a7f0b2..c46ff5c 100644 --- a/api/pluggableharness/agent/context/v1/context.proto +++ b/api/pluggableharness/agent/context/v1/context.proto @@ -229,7 +229,7 @@ message ContextContribution { // ContextErrorCategory classifies a context provider's failures. // context.md §10 — smaller than the model-provider taxonomy -// (provider.md §8), but a plugin MUST still classify failures rather than +// (model.md §8), but a plugin MUST still classify failures rather than // collapsing them into one generic error. enum ContextErrorCategory { // Zero value. Never valid for a real error; its presence on the wire diff --git a/api/pluggableharness/agent/kernel/v1/kernel.proto b/api/pluggableharness/agent/kernel/v1/kernel.proto index c43175f..b6e19ea 100644 --- a/api/pluggableharness/agent/kernel/v1/kernel.proto +++ b/api/pluggableharness/agent/kernel/v1/kernel.proto @@ -140,7 +140,7 @@ message CountTokensRequest { repeated pluggableharness.agent.content.v1.ContentBlock content = 1; // The model whose tokenizer should be preferred, if that model - // provider implements the optional CountTokens RPC (provider.md §2.1). + // provider implements the optional CountTokens RPC (model.md §2.1). // MAY be omitted, in which case the kernel's fallback heuristic // (kernel-callbacks.md §3) is used. optional pluggableharness.agent.model.v1.ModelRef model_ref = 2; @@ -171,7 +171,7 @@ enum EventKind { // forgot to set the field. EVENT_KIND_UNSPECIFIED = 0; - // A completed model turn's canonical message (provider.md §5). Usage + // A completed model turn's canonical message (model.md §5). Usage // and cost figures live inside this payload, not as separate fields or // a separate EventKind (state-backend.md §5). EVENT_KIND_MESSAGE = 1; diff --git a/api/pluggableharness/agent/memory/v1/memory.proto b/api/pluggableharness/agent/memory/v1/memory.proto index c3ed6c0..9901274 100644 --- a/api/pluggableharness/agent/memory/v1/memory.proto +++ b/api/pluggableharness/agent/memory/v1/memory.proto @@ -29,7 +29,7 @@ service MemoryService { // Configure decodes this provider's agent.hcl config block, per the // same schema-to-cty bridge contract as the rest of this spec series - // (provider.md §3). Unary. memory.md §5. + // (model.md §3). Unary. memory.md §5. rpc Configure(ConfigureRequest) returns (ConfigureResponse); // Recall is the read side: it fires at context-assemble time, competing diff --git a/api/pluggableharness/agent/model/v1/model.proto b/api/pluggableharness/agent/model/v1/model.proto index 68ed1a7..f423319 100644 --- a/api/pluggableharness/agent/model/v1/model.proto +++ b/api/pluggableharness/agent/model/v1/model.proto @@ -1,28 +1,623 @@ syntax = "proto3"; -// Package pluggableharness.agent.model.v1 defines the two distinct model-identity shapes -// used across the specs. They are deliberately NOT unified into one -// message: ModelTarget is a rich "what am I generating context for" -// descriptor (context.md §4, memory.md §6); ModelRef is a narrow -// "which model's tokenizer" selector (kernel-callbacks.md §2). Merging -// them would force every CountTokens caller to populate fields it doesn't -// have and doesn't need. +// Package pluggableharness.agent.model.v1 defines the model (LLM vendor) provider +// plugin protocol described in specifications/model.md — see +// .claude/rules/proto.md — plus the two distinct model-identity shapes used +// across the other specs. The identity shapes are deliberately NOT unified +// into one message: ModelTarget is a rich "what am I generating context +// for" descriptor (context.md §4, memory.md §6); ModelRef is a narrow +// "which model's tokenizer" selector (kernel-callbacks.md §2). Merging them +// would force every CountTokens caller to populate fields it doesn't have +// and doesn't need. package pluggableharness.agent.model.v1; +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; +import "pluggableharness/agent/config/v1/config.proto"; +import "pluggableharness/agent/content/v1/content.proto"; +import "pluggableharness/agent/render/v1/render.proto"; +import "pluggableharness/agent/schema/v1/schema.proto"; +import "pluggableharness/agent/slashcommand/v1/slashcommand.proto"; + option go_package = "github.com/pluggableharness/agent/pkg/model/proto/v1;modelv1"; +// ModelService is the model provider plugin protocol described in +// specifications/model.md §1: a subprocess + gRPC plugin (via +// hashicorp/go-plugin) fronting one LLM vendor. Every plugin exposes +// GetCapabilities, Configure, and StreamCompletion (all MUST); CountTokens +// SHOULD be implemented; Render MAY be implemented. +service ModelService { + // GetCapabilities returns one ModelSpec per model this plugin can serve, + // per model.md §2. MUST be cheap to call repeatedly (the kernel MAY + // call it before every routing decision) and MUST NOT require a network + // call to the vendor if avoidable. + rpc GetCapabilities(GetCapabilitiesRequest) returns (GetCapabilitiesResponse); + + // Configure delivers the provider's agent.hcl config block, already + // decoded from HCL/cty into a Struct by the kernel's schema-to-cty + // bridge, per model.md §3. MUST reject missing required fields (e.g. + // no API key) with a structured error at Configure time rather than + // deferring the failure to the first StreamCompletion call. A plugin + // MUST NOT echo any received secret value into an Emit'd event, a Render + // output, a log line, or an error message. + rpc Configure(ConfigureRequest) returns (ConfigureResponse); + + // StreamCompletion is server-streaming, never bidirectional — this + // matches how a real vendor API actually works: one request in, one + // chunked/SSE response out. The kernel sends one request (full message + // history + tool declarations + generation params) and receives a + // stream of StreamEvents back. A backend whose vendor API is not natively + // streaming (batch-only) MUST still implement this RPC shape, emitting + // its full response as a single terminal burst of events followed by a + // `stop` event (model.md §4). Cancellation is the kernel closing the + // gRPC stream; the plugin MUST treat this as normal control flow — stop + // generating and release resources — surfacing StopReason + // STOP_REASON_CANCELLED, never treating it as an error condition + // (model.md §1, .claude/rules/grpc.md's cancellation rule). + // + // buf:lint:ignore RPC_RESPONSE_STANDARD_NAME + // Stream element type is the bare "StreamEvent" per model.md §4's + // literal spec, naming the streamed domain concept rather than the RPC. + rpc StreamCompletion(StreamCompletionRequest) returns (stream StreamEvent); + + // CountTokens returns an exact token count for the given text, using the + // vendor's real tokenizer. SHOULD be implemented per model provider + // (model.md §2.1) — upgraded from an initial MAY per operator + // decision. A model provider that implements this gets its counts + // marked exact when the kernel resolves a CountTokens call against it + // (kernel-callbacks.md §2); a provider that doesn't falls back to the + // heuristic in kernel-callbacks.md §3, a genuine last resort. + rpc CountTokens(CountTokensRequest) returns (CountTokensResponse); + + // Render is the model-provider side of the Emit->Render->Paint pipeline + // (see docs/specifications/architecture.md), returning a RenderTree for an opaque + // emitted payload — e.g. to render a `thinking` block collapsed by + // default, or usage/cost info specially. MAY be implemented; model.md + // §7 notes most model-provider payloads (plain text, tool calls) render + // fine under the kernel's generic fallback when this RPC is absent. + rpc Render(RenderRequest) returns (RenderResponse); +} + +// GetCapabilitiesRequest is empty: model.md §2 defines GetCapabilities +// as taking no request parameters. +message GetCapabilitiesRequest {} + +// GetCapabilitiesResponse wraps Capabilities for the RPC signature, per +// this repo's per-RPC envelope convention (.claude/rules/proto.md). +message GetCapabilitiesResponse { + Capabilities capabilities = 1; +} + +// Capabilities is GetCapabilities' response payload: every model this +// plugin can serve, plus provider-wide declarations that apply once, not +// per model. +message Capabilities { + // One ModelSpec per model the plugin can serve. MUST have at least one + // entry. + repeated ModelSpec models = 1; + + // Slash commands this provider contributes, declared once for the + // provider as a whole (not per model), per model.md §2 and + // configuration.md §5 / frontend.md §5. MAY be empty. + repeated pluggableharness.agent.slashcommand.v1.SlashCommandSpec slash_commands = 2; + + // The provider's agent.hcl config schema, returned alongside + // capabilities so the kernel knows what fields Configure expects, per + // configuration.md §4. + pluggableharness.agent.config.v1.ConfigSchema config_schema = 3; +} + +// ConfigureRequest wraps the provider's agent.hcl config block, already +// decoded from HCL/cty into a Struct by the kernel's schema-to-cty bridge, +// per model.md §3. +message ConfigureRequest { + // The decoded config value. Field contents are provider-specific (API + // key, base URL override, org/project IDs, etc.) — model.md §3 + // doesn't mandate a shape beyond what ConfigSchema (Capabilities. + // config_schema) declares. A Struct because the shape is genuinely + // provider-defined, not fixed at the proto level (see + // .claude/rules/proto.md's Struct carve-out). + google.protobuf.Struct config = 1; +} + +// ConfigureResponse is empty on success. A Configure failure (e.g. a +// missing required field) surfaces as a gRPC status carrying a +// ModelError in its structured detail, per .claude/rules/grpc.md's +// error-taxonomy convention — there is no in-band error field on this +// message. +message ConfigureResponse {} + +// ModelSpec describes one model this provider can serve, per +// model.md §2. Every field below is MUST unless its comment says +// otherwise. +message ModelSpec { + // The vendor's exact model identifier, used to select this model in + // StreamCompletionRequest.model_id. + string id = 1; + + // The model's input token budget. + int64 context_window = 2; + + // The model's maximum output tokens per response. + int64 max_output_tokens = 3; + + // Whether this model can accept tool declarations and emit tool_use + // content blocks. + bool supports_tool_use = 4; + + // Whether this model can accept image content blocks. + bool supports_vision = 5; + + // Whether the vendor's own backend streams responses. A UX hint only + // (e.g. "don't render a live-typing cursor" when false) — the + // StreamCompletion RPC shape is always server-streaming regardless of + // this value, per model.md §1/§4. + bool supports_streaming = 6; + + // Whether this model can return multiple tool_use blocks in a single + // turn. SHOULD be set accurately; a false or absent value means the + // kernel MUST serialize tool calls for this model. + optional bool supports_parallel_tool_calls = 7; + + // This model's extended-reasoning capability. MUST be present even when + // unsupported — use { supported: false } rather than omitting the + // message, so a caller never has to distinguish "unset" from "no + // thinking mode". + ThinkingSpec thinking = 8; + + // This model's prompt-caching capability. MUST be present even when + // unsupported — use { supported: false } rather than omitting the + // message. + CachingSpec caching = 9; + + // This model's pricing. MUST be present even for a free model (set + // Pricing.free = true). + Pricing pricing = 10; +} + +// ThinkingMode enumerates the shapes of extended-reasoning control found +// across researched vendors (model.md §2) — a plain supports_thinking +// bool would lose information the kernel needs to build a correct +// request, since some vendors (e.g. Anthropic) expose more than one mode +// across their own model lineup. +enum ThinkingMode { + // Zero value. Never valid when ThinkingSpec.supported is true; its + // presence on the wire means a caller forgot to set the field. + THINKING_MODE_UNSPECIFIED = 0; + // The model has no extended-reasoning capability. Pairs with + // ThinkingSpec.supported == false. + THINKING_MODE_NONE = 1; + // The model always reasons, adaptively, with no caller-selectable + // effort level or budget. + THINKING_MODE_ALWAYS_ON_ADAPTIVE = 2; + // The caller selects one of a fixed set of named effort levels + // (ThinkingSpec.effort_levels). + THINKING_MODE_DISCRETE_EFFORT = 3; + // The caller selects a token budget within ThinkingSpec.budget_range. + THINKING_MODE_CONTINUOUS_BUDGET = 4; +} + +// ThinkingBudgetRange bounds the token budget a caller may request when +// ThinkingMode is THINKING_MODE_CONTINUOUS_BUDGET. +message ThinkingBudgetRange { + // The smallest thinking-token budget this model accepts. + int64 min = 1; + // The largest thinking-token budget this model accepts. + int64 max = 2; +} + +// ThinkingSpec describes one model's extended-reasoning capability, per +// model.md §2. +message ThinkingSpec { + // Whether this model has any extended-reasoning capability at all. + bool supported = 1; + + // Which reasoning-control shape this model uses. MUST be + // THINKING_MODE_NONE when supported == false. + ThinkingMode mode = 2; + + // The selectable effort levels, e.g. ["low","medium","high","xhigh", + // "max"]. MUST be non-empty when mode == THINKING_MODE_DISCRETE_EFFORT; + // meaningless otherwise. + repeated string effort_levels = 3; + + // The selectable token-budget range. MUST be present when mode == + // THINKING_MODE_CONTINUOUS_BUDGET; meaningless otherwise. + optional ThinkingBudgetRange budget_range = 4; + + // Whether reasoning can be turned off once enabled. MUST be set + // accurately — some vendors' reasoning cannot be disabled (e.g. a + // researched Grok model defaults reasoning on with no off switch). + bool can_disable = 5; + + // The effort level (discrete_effort) or budget-token value + // (continuous_budget), as a string, the vendor applies when a request + // omits thinking config entirely. MUST be set when mode != + // THINKING_MODE_NONE — makes the vendor's actual default behavior + // visible/auditable via GetCapabilities rather than hidden in adapter + // code, so a kernel wanting deterministic behavior can always send an + // explicit override. + optional string default = 6; +} + +// CachingMode enumerates the prompt-caching mechanics found across +// researched vendors (model.md §2). +enum CachingMode { + // Zero value. Never valid when CachingSpec.supported is true; its + // presence on the wire means a caller forgot to set the field. + CACHING_MODE_UNSPECIFIED = 0; + // The model has no prompt-caching capability. Pairs with + // CachingSpec.supported == false. + CACHING_MODE_NONE = 1; + // The caller must place cache breakpoints on content blocks explicitly + // (Anthropic/Mistral-style). + CACHING_MODE_EXPLICIT_MARKERS = 2; + // The vendor applies caching transparently above a token threshold, no + // caller action required. + CACHING_MODE_IMPLICIT_AUTOMATIC = 3; +} + +// CachingSpec describes one model's prompt-caching capability, per +// model.md §2. +message CachingSpec { + // Whether this model has any prompt-caching capability at all. + bool supported = 1; + + // Which caching mechanic this model uses. MUST be CACHING_MODE_NONE + // when supported == false. + CachingMode mode = 2; + + // Whether this provider runs its own cache-keepalive loop (e.g. a + // background goroutine re-pinging before a cache TTL expires, so a long + // tool-execution gap doesn't let the cache go cold). MUST be set, + // default false. Cache TTL mechanics are vendor-specific, so per + // operator decision this is a provider-owned behavior the kernel never + // drives — this field only tells the kernel/operator whether a given + // provider implements the optimization (model.md §2). + bool keepalive_supported = 3; +} + +// PricingTier is one time-bounded rate within a model's Pricing, per +// model.md §2. Exactly one tier MUST match at any given timestamp +// (effective_from <= ts < effective_until, an omitted bound unbounded on +// that side); the kernel MUST reject a Pricing value at capability-load +// time if its tiers overlap or leave a gap. +message PricingTier { + // The moment this tier becomes active. Omitted means "since this plugin + // version was published". Refines model.md §2's "ISO 8601 + // date/timestamp" into the native well-known type. + optional google.protobuf.Timestamp effective_from = 1; + + // The moment this tier stops being active. Omitted means "still + // current" — an omitted effective_until marks the currently active + // tier. Refines model.md §2's "ISO 8601" into the native well-known + // type. + optional google.protobuf.Timestamp effective_until = 2; + + // Cost per million input tokens, realtime rate. + double input_per_mtok = 3; + + // Cost per million output tokens, realtime rate. + double output_per_mtok = 4; + + // Cost per million cache-write tokens. MUST be present iff + // CachingSpec.supported. + optional double cache_write_per_mtok = 5; + + // Cost per million cache-read tokens, typically far cheaper than + // input_per_mtok — the entire point of caching. MUST be present iff + // CachingSpec.supported. + optional double cache_read_per_mtok = 6; + + // A vendor's discounted batch/async input rate, where one exists (e.g. + // a researched Gemini batch tier). MAY be present. + optional double batch_input_per_mtok = 7; + + // A vendor's discounted batch/async output rate, paired with + // batch_input_per_mtok. MAY be present. + optional double batch_output_per_mtok = 8; +} + +// Pricing describes one model's cost structure, per model.md §2. MUST +// be present on every ModelSpec, even a free one. +message Pricing { + // The pricing currency. MUST be "USD" for v1; reserved for future + // multi-currency support, not acted on by the kernel yet. + string currency = 1; + + // True for a local/free-to-run model (e.g. an Ollama-served model). + // When true, tiers MAY be omitted entirely. + bool free = 2; + + // This model's rate tiers, ordered or not — resolution is by + // effective_from/effective_until, not array position. MUST have at + // least one entry unless free == true. Exactly one tier MUST match any + // given timestamp; the kernel MUST reject overlapping or gapped tiers + // at capability-load time. + repeated PricingTier tiers = 3; +} + +// StreamCompletionRequest is StreamCompletion's request: the full +// canonical conversation, available tools, and generation params for one +// completion, per model.md §4. +message StreamCompletionRequest { + // The canonical conversation history, in emission order (model.md + // §5). + repeated pluggableharness.agent.content.v1.Message messages = 1; + + // Selects which of this provider's ModelSpec.id to use. + string model_id = 2; + + // The tools available to the model on this turn, described in the + // shared JSON-Schema subset (model.md §6). MAY be empty. + repeated ToolDeclaration tools = 3; + + // Generation-time overrides. Omitted means every param takes its + // model-specific default. + optional GenerationParams params = 4; +} + +// ToolDeclaration is one tool the model may call on this turn, per +// model.md §6. Each model-provider adapter translates this into its +// vendor's own tool-definition wire format. +message ToolDeclaration { + // The tool's name, as the model must reference it in a ToolUseBlock. + string name = 1; + + // Human-readable description shown to the model to help it decide + // whether and how to call this tool. + string description = 2; + + // The tool's input shape, in the restricted JSON-Schema subset shared + // across categories (model.md §6, pluggableharness.agent.schema.v1.Schema). + pluggableharness.agent.schema.v1.Schema input_schema = 3; +} + +// GenerationParams carries per-request overrides of otherwise +// model-default generation behavior. +message GenerationParams { + // Selects one of ThinkingSpec.effort_levels. Meaningful only when the + // target model's ThinkingSpec.mode == THINKING_MODE_DISCRETE_EFFORT. + optional string thinking_effort = 1; + + // Selects a token budget within ThinkingSpec.budget_range. Meaningful + // only when the target model's ThinkingSpec.mode == + // THINKING_MODE_CONTINUOUS_BUDGET. + optional int64 thinking_budget_tokens = 2; + + // Per-request override of ModelSpec.max_output_tokens. Omitted means + // use the model's default. + optional int64 max_output_tokens = 3; +} + +// StreamEvent is one message in the stream StreamCompletion returns, per +// model.md §4. Exactly one variant is set. +message StreamEvent { + oneof event { + // An incremental fragment of assistant text output. + TextDelta text_delta = 1; + // An incremental fragment of the model's reasoning output. + ThinkingDelta thinking_delta = 2; + // The vendor's opaque integrity token for the reasoning just emitted. + ThinkingSignature thinking_signature = 3; + // The model has begun requesting a tool invocation. + ToolCallStart tool_call_start = 4; + // An incremental fragment of a tool call's arguments. + ToolCallDelta tool_call_delta = 5; + // A tool call's arguments are complete. + ToolCallDone tool_call_done = 6; + // Token accounting for this completion. + Usage usage = 7; + // The completion has ended. + Stop stop = 8; + // The completion failed. + Error error = 9; + } + + // TextDelta carries one incremental fragment of assistant text output. + // MUST be supported by every plugin, both directions (model.md §5). + message TextDelta { + // The text fragment. + string text = 1; + } + + // ThinkingDelta carries one incremental fragment of the model's + // reasoning output. Only emitted when the target model's + // ThinkingSpec.supported is true. + message ThinkingDelta { + // The reasoning-text fragment. + string text = 1; + } + + // ThinkingSignature carries the vendor's opaque integrity token for the + // reasoning block just completed. MUST be emitted if the vendor's + // thinking blocks carry an integrity signature (model.md §4/§5); the + // kernel MUST store and round-trip this verbatim, never inspecting or + // reformatting it, into ContentBlock's ThinkingBlock.signature. + message ThinkingSignature { + // The opaque, vendor-specific signature bytes. + bytes signature = 1; + } + + // ToolCallStart announces the model has begun requesting a tool + // invocation. + message ToolCallStart { + // Correlation id for the matching ToolCallDelta/ToolCallDone events + // and the resulting ToolUseBlock.id. + string id = 1; + // The tool's declared name (ToolDeclaration.name). + string name = 2; + } + + // ToolCallDelta carries one incremental fragment of a tool call's + // arguments, accumulated by the kernel across deltas into the final + // parsed JSON. + message ToolCallDelta { + // The id from the matching ToolCallStart. + string id = 1; + // A partial-JSON fragment of the call's arguments. + string arguments_fragment = 2; + } + + // ToolCallDone signals a tool call's arguments are complete and ready + // for the kernel to parse and dispatch. + message ToolCallDone { + // The id from the matching ToolCallStart. + string id = 1; + } + + // Stop signals the completion has ended. + message Stop { + // Why the completion ended. + StopReason reason = 1; + } + + // Error signals the completion failed. + message Error { + // The structured error, classified per model.md §8. + ModelError error = 1; + } +} + +// Usage carries token accounting for one completion, per model.md §4.1. +// The kernel computes and persists cost_usd from these counts plus the +// matching PricingTier — the plugin never computes cost itself. Promoted +// to a top-level message, rather than nested under StreamEvent, because +// it is reused outside the stream (event payloads, frontend usage +// updates — forthcoming). +message Usage { + // Input tokens consumed by this completion. + int64 input_tokens = 1; + // Output tokens produced by this completion. + int64 output_tokens = 2; + // Tokens read from cache, if the model supports caching. Never also + // counted in input_tokens. + optional int64 cache_read_tokens = 3; + // Tokens written to cache, if the model supports caching. Never also + // counted in input_tokens. + optional int64 cache_write_tokens = 4; + // Deliberately no cost field: the kernel computes and persists + // cost_usd from these token counts plus the matching PricingTier, per + // model.md §4.1 — the provider never computes cost itself. +} + +// StopReason classifies why a StreamCompletion ended, per model.md §4. +enum StopReason { + // Zero value. Never valid on a real Stop event; its presence on the + // wire means a caller forgot to set the field. + STOP_REASON_UNSPECIFIED = 0; + // The model completed its turn normally. + STOP_REASON_END_TURN = 1; + // The model stopped to request one or more tool invocations. + STOP_REASON_TOOL_USE = 2; + // The model hit its output token limit before completing its turn. + STOP_REASON_MAX_TOKENS = 3; + // The vendor's content filter stopped generation. + STOP_REASON_CONTENT_FILTERED = 4; + // The kernel cancelled the stream (user interrupt, timeout, turn + // abort). MUST be treated by the plugin as normal control flow, never + // as an error (model.md §1, .claude/rules/grpc.md). + STOP_REASON_CANCELLED = 5; +} + +// CountTokensRequest is CountTokens' request: the raw text to count, per +// model.md §2.1. +message CountTokensRequest { + // The text to count tokens for. + string text = 1; +} + +// CountTokensResponse is CountTokens' response. +message CountTokensResponse { + // The exact token count, per this model's real vendor tokenizer. + int64 count = 1; +} + +// RenderRequest carries the opaque payload to render, per model.md §7. +message RenderRequest { + // The opaque emitted payload to render — the Emit->Render->Paint + // pipeline's deliberate carve-out from the strong-typing rule (see + // .claude/rules/grpc.md), never interpreted by the kernel. + bytes payload = 1; +} + +// RenderResponse wraps the resulting RenderTree, per model.md §7. +message RenderResponse { + // The rendered tree, formally defined in frontend.md §1 and shared + // verbatim across every category's Render RPC (tool.md §7, context.md + // §9, memory.md §10) — one RenderTree type for the whole + // Emit->Render->Paint pipeline, not a per-category variant. + pluggableharness.agent.render.v1.RenderTree tree = 1; +} + +// ModelErrorCategory classifies every StreamCompletion/Configure +// failure, per model.md §8. A plugin MUST classify every failure into +// exactly one of these categories and MUST NOT collapse them into a +// single generic error — the kernel's routing/fallback/retry behavior +// depends on telling these apart. +enum ModelErrorCategory { + // Zero value. Never valid on a real ModelError; its presence on the + // wire means a caller forgot to set the field. + MODEL_ERROR_CATEGORY_UNSPECIFIED = 0; + // The request (or accumulated conversation) exceeds the model's context + // window. The kernel MUST NOT blindly retry as-is. + MODEL_ERROR_CATEGORY_CONTEXT_LENGTH_EXCEEDED = 1; + // A vendor-side rate limit was hit. The kernel retries with backoff, + // honoring retry_after if supplied. + MODEL_ERROR_CATEGORY_RATE_LIMITED = 2; + // Transient vendor unavailability (5xx-equivalent). The kernel retries + // with backoff; a candidate for capability-aware fallback. + MODEL_ERROR_CATEGORY_OVERLOADED = 3; + // Bad, expired, or missing credentials. The kernel MUST NOT retry or + // silently fall back; this surfaces to a human. + MODEL_ERROR_CATEGORY_AUTH_ERROR = 4; + // A malformed request — almost always a kernel/adapter bug. The kernel + // MUST NOT retry as-is. + MODEL_ERROR_CATEGORY_INVALID_REQUEST = 5; + // The vendor refused or filtered the content. Surfaced distinctly from + // a generic failure so policy/UX can handle it differently. + MODEL_ERROR_CATEGORY_CONTENT_FILTERED = 6; + // Anything else. MUST include raw_detail for debugging; treated as + // non-retryable by default. + MODEL_ERROR_CATEGORY_UNKNOWN = 7; +} + +// ModelError is the structured error every failure crossing this +// plugin boundary carries, per model.md §8. +message ModelError { + // This failure's category. MUST be set. + ModelErrorCategory category = 1; + + // Human-readable description of the failure. + string message = 2; + + // Whether the kernel may retry this request as-is. + bool retryable = 3; + + // How long the kernel should wait before retrying, when the vendor + // supplies one (typically alongside MODEL_ERROR_CATEGORY_RATE_LIMITED). + // SHOULD be set when available. Refines model.md §8's + // "retry_after_seconds" into the native well-known type. + optional google.protobuf.Duration retry_after = 4; + + // The raw vendor-provided error code or body, for debugging. SHOULD be + // set. + optional string raw_detail = 5; +} + // ModelTarget describes the model a context or memory contribution is // being assembled for, derived from that model's ModelSpec -// (provider.md §2). Carried on context.md's ContextRequest and memory.md's +// (model.md §2). Carried on context.md's ContextRequest and memory.md's // RecallRequest so a provider can tailor its contribution (and compute // tokens against the right budget) for the model that will actually // consume it. message ModelTarget { - // The target model's ModelSpec.id (provider.md §2) — the vendor's exact + // The target model's ModelSpec.id (model.md §2) — the vendor's exact // model identifier. string id = 1; - // The target model's total input token budget (provider.md §2 + // The target model's total input token budget (model.md §2 // ModelSpec.context_window). int64 context_window = 2; diff --git a/api/pluggableharness/agent/plan/v1/plan.proto b/api/pluggableharness/agent/plan/v1/plan.proto index 729dbc5..00771cd 100644 --- a/api/pluggableharness/agent/plan/v1/plan.proto +++ b/api/pluggableharness/agent/plan/v1/plan.proto @@ -56,7 +56,7 @@ message PlanItem { string tool_name = 4; // The call's parsed arguments — the kernel's canonical ToolCall - // representation (provider.md §6 / pluggableharness.agent.schema.v1's subset governs + // representation (model.md §6 / pluggableharness.agent.schema.v1's subset governs // its shape). A Struct per .claude/rules/proto.md's runtime-JSON // carve-out. google.protobuf.Struct input = 5; diff --git a/api/pluggableharness/agent/provider/v1/provider.proto b/api/pluggableharness/agent/provider/v1/provider.proto deleted file mode 100644 index af5b391..0000000 --- a/api/pluggableharness/agent/provider/v1/provider.proto +++ /dev/null @@ -1,599 +0,0 @@ -syntax = "proto3"; - -// Package pluggableharness.agent.provider.v1 defines the model (LLM vendor) provider -// plugin protocol described in specifications/provider.md — see -// .claude/rules/proto.md. -package pluggableharness.agent.provider.v1; - -import "google/protobuf/duration.proto"; -import "google/protobuf/struct.proto"; -import "google/protobuf/timestamp.proto"; -import "pluggableharness/agent/config/v1/config.proto"; -import "pluggableharness/agent/content/v1/content.proto"; -import "pluggableharness/agent/render/v1/render.proto"; -import "pluggableharness/agent/schema/v1/schema.proto"; -import "pluggableharness/agent/slashcommand/v1/slashcommand.proto"; - -option go_package = "github.com/pluggableharness/agent/pkg/provider/proto/v1;providerv1"; - -// ProviderService is the model provider plugin protocol described in -// specifications/provider.md §1: a subprocess + gRPC plugin (via -// hashicorp/go-plugin) fronting one LLM vendor. Every plugin exposes -// GetCapabilities, Configure, and StreamCompletion (all MUST); CountTokens -// SHOULD be implemented; Render MAY be implemented. -service ProviderService { - // GetCapabilities returns one ModelSpec per model this plugin can serve, - // per provider.md §2. MUST be cheap to call repeatedly (the kernel MAY - // call it before every routing decision) and MUST NOT require a network - // call to the vendor if avoidable. - rpc GetCapabilities(GetCapabilitiesRequest) returns (GetCapabilitiesResponse); - - // Configure delivers the provider's agent.hcl config block, already - // decoded from HCL/cty into a Struct by the kernel's schema-to-cty - // bridge, per provider.md §3. MUST reject missing required fields (e.g. - // no API key) with a structured error at Configure time rather than - // deferring the failure to the first StreamCompletion call. A plugin - // MUST NOT echo any received secret value into an Emit'd event, a Render - // output, a log line, or an error message. - rpc Configure(ConfigureRequest) returns (ConfigureResponse); - - // StreamCompletion is server-streaming, never bidirectional — this - // matches how a real vendor API actually works: one request in, one - // chunked/SSE response out. The kernel sends one request (full message - // history + tool declarations + generation params) and receives a - // stream of StreamEvents back. A backend whose vendor API is not natively - // streaming (batch-only) MUST still implement this RPC shape, emitting - // its full response as a single terminal burst of events followed by a - // `stop` event (provider.md §4). Cancellation is the kernel closing the - // gRPC stream; the plugin MUST treat this as normal control flow — stop - // generating and release resources — surfacing StopReason - // STOP_REASON_CANCELLED, never treating it as an error condition - // (provider.md §1, .claude/rules/grpc.md's cancellation rule). - // - // buf:lint:ignore RPC_RESPONSE_STANDARD_NAME - // Stream element type is the bare "StreamEvent" per provider.md §4's - // literal spec, naming the streamed domain concept rather than the RPC. - rpc StreamCompletion(StreamCompletionRequest) returns (stream StreamEvent); - - // CountTokens returns an exact token count for the given text, using the - // vendor's real tokenizer. SHOULD be implemented per model provider - // (provider.md §2.1) — upgraded from an initial MAY per operator - // decision. A model provider that implements this gets its counts - // marked exact when the kernel resolves a CountTokens call against it - // (kernel-callbacks.md §2); a provider that doesn't falls back to the - // heuristic in kernel-callbacks.md §3, a genuine last resort. - rpc CountTokens(CountTokensRequest) returns (CountTokensResponse); - - // Render is the model-provider side of the Emit->Render->Paint pipeline - // (see docs/specifications/architecture.md), returning a RenderTree for an opaque - // emitted payload — e.g. to render a `thinking` block collapsed by - // default, or usage/cost info specially. MAY be implemented; provider.md - // §7 notes most model-provider payloads (plain text, tool calls) render - // fine under the kernel's generic fallback when this RPC is absent. - rpc Render(RenderRequest) returns (RenderResponse); -} - -// GetCapabilitiesRequest is empty: provider.md §2 defines GetCapabilities -// as taking no request parameters. -message GetCapabilitiesRequest {} - -// GetCapabilitiesResponse wraps Capabilities for the RPC signature, per -// this repo's per-RPC envelope convention (.claude/rules/proto.md). -message GetCapabilitiesResponse { - Capabilities capabilities = 1; -} - -// Capabilities is GetCapabilities' response payload: every model this -// plugin can serve, plus provider-wide declarations that apply once, not -// per model. -message Capabilities { - // One ModelSpec per model the plugin can serve. MUST have at least one - // entry. - repeated ModelSpec models = 1; - - // Slash commands this provider contributes, declared once for the - // provider as a whole (not per model), per provider.md §2 and - // configuration.md §5 / frontend.md §5. MAY be empty. - repeated pluggableharness.agent.slashcommand.v1.SlashCommandSpec slash_commands = 2; - - // The provider's agent.hcl config schema, returned alongside - // capabilities so the kernel knows what fields Configure expects, per - // configuration.md §4. - pluggableharness.agent.config.v1.ConfigSchema config_schema = 3; -} - -// ConfigureRequest wraps the provider's agent.hcl config block, already -// decoded from HCL/cty into a Struct by the kernel's schema-to-cty bridge, -// per provider.md §3. -message ConfigureRequest { - // The decoded config value. Field contents are provider-specific (API - // key, base URL override, org/project IDs, etc.) — provider.md §3 - // doesn't mandate a shape beyond what ConfigSchema (Capabilities. - // config_schema) declares. A Struct because the shape is genuinely - // provider-defined, not fixed at the proto level (see - // .claude/rules/proto.md's Struct carve-out). - google.protobuf.Struct config = 1; -} - -// ConfigureResponse is empty on success. A Configure failure (e.g. a -// missing required field) surfaces as a gRPC status carrying a -// ProviderError in its structured detail, per .claude/rules/grpc.md's -// error-taxonomy convention — there is no in-band error field on this -// message. -message ConfigureResponse {} - -// ModelSpec describes one model this provider can serve, per -// provider.md §2. Every field below is MUST unless its comment says -// otherwise. -message ModelSpec { - // The vendor's exact model identifier, used to select this model in - // StreamCompletionRequest.model_id. - string id = 1; - - // The model's input token budget. - int64 context_window = 2; - - // The model's maximum output tokens per response. - int64 max_output_tokens = 3; - - // Whether this model can accept tool declarations and emit tool_use - // content blocks. - bool supports_tool_use = 4; - - // Whether this model can accept image content blocks. - bool supports_vision = 5; - - // Whether the vendor's own backend streams responses. A UX hint only - // (e.g. "don't render a live-typing cursor" when false) — the - // StreamCompletion RPC shape is always server-streaming regardless of - // this value, per provider.md §1/§4. - bool supports_streaming = 6; - - // Whether this model can return multiple tool_use blocks in a single - // turn. SHOULD be set accurately; a false or absent value means the - // kernel MUST serialize tool calls for this model. - optional bool supports_parallel_tool_calls = 7; - - // This model's extended-reasoning capability. MUST be present even when - // unsupported — use { supported: false } rather than omitting the - // message, so a caller never has to distinguish "unset" from "no - // thinking mode". - ThinkingSpec thinking = 8; - - // This model's prompt-caching capability. MUST be present even when - // unsupported — use { supported: false } rather than omitting the - // message. - CachingSpec caching = 9; - - // This model's pricing. MUST be present even for a free model (set - // Pricing.free = true). - Pricing pricing = 10; -} - -// ThinkingMode enumerates the shapes of extended-reasoning control found -// across researched vendors (provider.md §2) — a plain supports_thinking -// bool would lose information the kernel needs to build a correct -// request, since some vendors (e.g. Anthropic) expose more than one mode -// across their own model lineup. -enum ThinkingMode { - // Zero value. Never valid when ThinkingSpec.supported is true; its - // presence on the wire means a caller forgot to set the field. - THINKING_MODE_UNSPECIFIED = 0; - // The model has no extended-reasoning capability. Pairs with - // ThinkingSpec.supported == false. - THINKING_MODE_NONE = 1; - // The model always reasons, adaptively, with no caller-selectable - // effort level or budget. - THINKING_MODE_ALWAYS_ON_ADAPTIVE = 2; - // The caller selects one of a fixed set of named effort levels - // (ThinkingSpec.effort_levels). - THINKING_MODE_DISCRETE_EFFORT = 3; - // The caller selects a token budget within ThinkingSpec.budget_range. - THINKING_MODE_CONTINUOUS_BUDGET = 4; -} - -// ThinkingBudgetRange bounds the token budget a caller may request when -// ThinkingMode is THINKING_MODE_CONTINUOUS_BUDGET. -message ThinkingBudgetRange { - // The smallest thinking-token budget this model accepts. - int64 min = 1; - // The largest thinking-token budget this model accepts. - int64 max = 2; -} - -// ThinkingSpec describes one model's extended-reasoning capability, per -// provider.md §2. -message ThinkingSpec { - // Whether this model has any extended-reasoning capability at all. - bool supported = 1; - - // Which reasoning-control shape this model uses. MUST be - // THINKING_MODE_NONE when supported == false. - ThinkingMode mode = 2; - - // The selectable effort levels, e.g. ["low","medium","high","xhigh", - // "max"]. MUST be non-empty when mode == THINKING_MODE_DISCRETE_EFFORT; - // meaningless otherwise. - repeated string effort_levels = 3; - - // The selectable token-budget range. MUST be present when mode == - // THINKING_MODE_CONTINUOUS_BUDGET; meaningless otherwise. - optional ThinkingBudgetRange budget_range = 4; - - // Whether reasoning can be turned off once enabled. MUST be set - // accurately — some vendors' reasoning cannot be disabled (e.g. a - // researched Grok model defaults reasoning on with no off switch). - bool can_disable = 5; - - // The effort level (discrete_effort) or budget-token value - // (continuous_budget), as a string, the vendor applies when a request - // omits thinking config entirely. MUST be set when mode != - // THINKING_MODE_NONE — makes the vendor's actual default behavior - // visible/auditable via GetCapabilities rather than hidden in adapter - // code, so a kernel wanting deterministic behavior can always send an - // explicit override. - optional string default = 6; -} - -// CachingMode enumerates the prompt-caching mechanics found across -// researched vendors (provider.md §2). -enum CachingMode { - // Zero value. Never valid when CachingSpec.supported is true; its - // presence on the wire means a caller forgot to set the field. - CACHING_MODE_UNSPECIFIED = 0; - // The model has no prompt-caching capability. Pairs with - // CachingSpec.supported == false. - CACHING_MODE_NONE = 1; - // The caller must place cache breakpoints on content blocks explicitly - // (Anthropic/Mistral-style). - CACHING_MODE_EXPLICIT_MARKERS = 2; - // The vendor applies caching transparently above a token threshold, no - // caller action required. - CACHING_MODE_IMPLICIT_AUTOMATIC = 3; -} - -// CachingSpec describes one model's prompt-caching capability, per -// provider.md §2. -message CachingSpec { - // Whether this model has any prompt-caching capability at all. - bool supported = 1; - - // Which caching mechanic this model uses. MUST be CACHING_MODE_NONE - // when supported == false. - CachingMode mode = 2; - - // Whether this provider runs its own cache-keepalive loop (e.g. a - // background goroutine re-pinging before a cache TTL expires, so a long - // tool-execution gap doesn't let the cache go cold). MUST be set, - // default false. Cache TTL mechanics are vendor-specific, so per - // operator decision this is a provider-owned behavior the kernel never - // drives — this field only tells the kernel/operator whether a given - // provider implements the optimization (provider.md §2). - bool keepalive_supported = 3; -} - -// PricingTier is one time-bounded rate within a model's Pricing, per -// provider.md §2. Exactly one tier MUST match at any given timestamp -// (effective_from <= ts < effective_until, an omitted bound unbounded on -// that side); the kernel MUST reject a Pricing value at capability-load -// time if its tiers overlap or leave a gap. -message PricingTier { - // The moment this tier becomes active. Omitted means "since this plugin - // version was published". Refines provider.md §2's "ISO 8601 - // date/timestamp" into the native well-known type. - optional google.protobuf.Timestamp effective_from = 1; - - // The moment this tier stops being active. Omitted means "still - // current" — an omitted effective_until marks the currently active - // tier. Refines provider.md §2's "ISO 8601" into the native well-known - // type. - optional google.protobuf.Timestamp effective_until = 2; - - // Cost per million input tokens, realtime rate. - double input_per_mtok = 3; - - // Cost per million output tokens, realtime rate. - double output_per_mtok = 4; - - // Cost per million cache-write tokens. MUST be present iff - // CachingSpec.supported. - optional double cache_write_per_mtok = 5; - - // Cost per million cache-read tokens, typically far cheaper than - // input_per_mtok — the entire point of caching. MUST be present iff - // CachingSpec.supported. - optional double cache_read_per_mtok = 6; - - // A vendor's discounted batch/async input rate, where one exists (e.g. - // a researched Gemini batch tier). MAY be present. - optional double batch_input_per_mtok = 7; - - // A vendor's discounted batch/async output rate, paired with - // batch_input_per_mtok. MAY be present. - optional double batch_output_per_mtok = 8; -} - -// Pricing describes one model's cost structure, per provider.md §2. MUST -// be present on every ModelSpec, even a free one. -message Pricing { - // The pricing currency. MUST be "USD" for v1; reserved for future - // multi-currency support, not acted on by the kernel yet. - string currency = 1; - - // True for a local/free-to-run model (e.g. an Ollama-served model). - // When true, tiers MAY be omitted entirely. - bool free = 2; - - // This model's rate tiers, ordered or not — resolution is by - // effective_from/effective_until, not array position. MUST have at - // least one entry unless free == true. Exactly one tier MUST match any - // given timestamp; the kernel MUST reject overlapping or gapped tiers - // at capability-load time. - repeated PricingTier tiers = 3; -} - -// StreamCompletionRequest is StreamCompletion's request: the full -// canonical conversation, available tools, and generation params for one -// completion, per provider.md §4. -message StreamCompletionRequest { - // The canonical conversation history, in emission order (provider.md - // §5). - repeated pluggableharness.agent.content.v1.Message messages = 1; - - // Selects which of this provider's ModelSpec.id to use. - string model_id = 2; - - // The tools available to the model on this turn, described in the - // shared JSON-Schema subset (provider.md §6). MAY be empty. - repeated ToolDeclaration tools = 3; - - // Generation-time overrides. Omitted means every param takes its - // model-specific default. - optional GenerationParams params = 4; -} - -// ToolDeclaration is one tool the model may call on this turn, per -// provider.md §6. Each model-provider adapter translates this into its -// vendor's own tool-definition wire format. -message ToolDeclaration { - // The tool's name, as the model must reference it in a ToolUseBlock. - string name = 1; - - // Human-readable description shown to the model to help it decide - // whether and how to call this tool. - string description = 2; - - // The tool's input shape, in the restricted JSON-Schema subset shared - // across categories (provider.md §6, pluggableharness.agent.schema.v1.Schema). - pluggableharness.agent.schema.v1.Schema input_schema = 3; -} - -// GenerationParams carries per-request overrides of otherwise -// model-default generation behavior. -message GenerationParams { - // Selects one of ThinkingSpec.effort_levels. Meaningful only when the - // target model's ThinkingSpec.mode == THINKING_MODE_DISCRETE_EFFORT. - optional string thinking_effort = 1; - - // Selects a token budget within ThinkingSpec.budget_range. Meaningful - // only when the target model's ThinkingSpec.mode == - // THINKING_MODE_CONTINUOUS_BUDGET. - optional int64 thinking_budget_tokens = 2; - - // Per-request override of ModelSpec.max_output_tokens. Omitted means - // use the model's default. - optional int64 max_output_tokens = 3; -} - -// StreamEvent is one message in the stream StreamCompletion returns, per -// provider.md §4. Exactly one variant is set. -message StreamEvent { - oneof event { - // An incremental fragment of assistant text output. - TextDelta text_delta = 1; - // An incremental fragment of the model's reasoning output. - ThinkingDelta thinking_delta = 2; - // The vendor's opaque integrity token for the reasoning just emitted. - ThinkingSignature thinking_signature = 3; - // The model has begun requesting a tool invocation. - ToolCallStart tool_call_start = 4; - // An incremental fragment of a tool call's arguments. - ToolCallDelta tool_call_delta = 5; - // A tool call's arguments are complete. - ToolCallDone tool_call_done = 6; - // Token accounting for this completion. - Usage usage = 7; - // The completion has ended. - Stop stop = 8; - // The completion failed. - Error error = 9; - } - - // TextDelta carries one incremental fragment of assistant text output. - // MUST be supported by every plugin, both directions (provider.md §5). - message TextDelta { - // The text fragment. - string text = 1; - } - - // ThinkingDelta carries one incremental fragment of the model's - // reasoning output. Only emitted when the target model's - // ThinkingSpec.supported is true. - message ThinkingDelta { - // The reasoning-text fragment. - string text = 1; - } - - // ThinkingSignature carries the vendor's opaque integrity token for the - // reasoning block just completed. MUST be emitted if the vendor's - // thinking blocks carry an integrity signature (provider.md §4/§5); the - // kernel MUST store and round-trip this verbatim, never inspecting or - // reformatting it, into ContentBlock's ThinkingBlock.signature. - message ThinkingSignature { - // The opaque, vendor-specific signature bytes. - bytes signature = 1; - } - - // ToolCallStart announces the model has begun requesting a tool - // invocation. - message ToolCallStart { - // Correlation id for the matching ToolCallDelta/ToolCallDone events - // and the resulting ToolUseBlock.id. - string id = 1; - // The tool's declared name (ToolDeclaration.name). - string name = 2; - } - - // ToolCallDelta carries one incremental fragment of a tool call's - // arguments, accumulated by the kernel across deltas into the final - // parsed JSON. - message ToolCallDelta { - // The id from the matching ToolCallStart. - string id = 1; - // A partial-JSON fragment of the call's arguments. - string arguments_fragment = 2; - } - - // ToolCallDone signals a tool call's arguments are complete and ready - // for the kernel to parse and dispatch. - message ToolCallDone { - // The id from the matching ToolCallStart. - string id = 1; - } - - // Usage carries token accounting for this completion, per provider.md - // §4.1. The kernel computes and persists cost_usd from these counts - // plus the matching PricingTier — the plugin never computes cost - // itself. - message Usage { - // Input tokens consumed by this completion. - int64 input_tokens = 1; - // Output tokens produced by this completion. - int64 output_tokens = 2; - // Tokens read from cache, if the model supports caching. Never also - // counted in input_tokens. - optional int64 cache_read_tokens = 3; - // Tokens written to cache, if the model supports caching. Never also - // counted in input_tokens. - optional int64 cache_write_tokens = 4; - // Deliberately no cost field: the kernel computes and persists - // cost_usd from these token counts plus the matching PricingTier, per - // provider.md §4.1 — the provider never computes cost itself. - } - - // Stop signals the completion has ended. - message Stop { - // Why the completion ended. - StopReason reason = 1; - } - - // Error signals the completion failed. - message Error { - // The structured error, classified per provider.md §8. - ProviderError error = 1; - } -} - -// StopReason classifies why a StreamCompletion ended, per provider.md §4. -enum StopReason { - // Zero value. Never valid on a real Stop event; its presence on the - // wire means a caller forgot to set the field. - STOP_REASON_UNSPECIFIED = 0; - // The model completed its turn normally. - STOP_REASON_END_TURN = 1; - // The model stopped to request one or more tool invocations. - STOP_REASON_TOOL_USE = 2; - // The model hit its output token limit before completing its turn. - STOP_REASON_MAX_TOKENS = 3; - // The vendor's content filter stopped generation. - STOP_REASON_CONTENT_FILTERED = 4; - // The kernel cancelled the stream (user interrupt, timeout, turn - // abort). MUST be treated by the plugin as normal control flow, never - // as an error (provider.md §1, .claude/rules/grpc.md). - STOP_REASON_CANCELLED = 5; -} - -// CountTokensRequest is CountTokens' request: the raw text to count, per -// provider.md §2.1. -message CountTokensRequest { - // The text to count tokens for. - string text = 1; -} - -// CountTokensResponse is CountTokens' response. -message CountTokensResponse { - // The exact token count, per this model's real vendor tokenizer. - int64 count = 1; -} - -// RenderRequest carries the opaque payload to render, per provider.md §7. -message RenderRequest { - // The opaque emitted payload to render — the Emit->Render->Paint - // pipeline's deliberate carve-out from the strong-typing rule (see - // .claude/rules/grpc.md), never interpreted by the kernel. - bytes payload = 1; -} - -// RenderResponse wraps the resulting RenderTree, per provider.md §7. -message RenderResponse { - // The rendered tree, formally defined in frontend.md §1 and shared - // verbatim across every category's Render RPC (tool.md §7, context.md - // §9, memory.md §10) — one RenderTree type for the whole - // Emit->Render->Paint pipeline, not a per-category variant. - pluggableharness.agent.render.v1.RenderTree tree = 1; -} - -// ProviderErrorCategory classifies every StreamCompletion/Configure -// failure, per provider.md §8. A plugin MUST classify every failure into -// exactly one of these categories and MUST NOT collapse them into a -// single generic error — the kernel's routing/fallback/retry behavior -// depends on telling these apart. -enum ProviderErrorCategory { - // Zero value. Never valid on a real ProviderError; its presence on the - // wire means a caller forgot to set the field. - PROVIDER_ERROR_CATEGORY_UNSPECIFIED = 0; - // The request (or accumulated conversation) exceeds the model's context - // window. The kernel MUST NOT blindly retry as-is. - PROVIDER_ERROR_CATEGORY_CONTEXT_LENGTH_EXCEEDED = 1; - // A vendor-side rate limit was hit. The kernel retries with backoff, - // honoring retry_after if supplied. - PROVIDER_ERROR_CATEGORY_RATE_LIMITED = 2; - // Transient vendor unavailability (5xx-equivalent). The kernel retries - // with backoff; a candidate for capability-aware fallback. - PROVIDER_ERROR_CATEGORY_OVERLOADED = 3; - // Bad, expired, or missing credentials. The kernel MUST NOT retry or - // silently fall back; this surfaces to a human. - PROVIDER_ERROR_CATEGORY_AUTH_ERROR = 4; - // A malformed request — almost always a kernel/adapter bug. The kernel - // MUST NOT retry as-is. - PROVIDER_ERROR_CATEGORY_INVALID_REQUEST = 5; - // The vendor refused or filtered the content. Surfaced distinctly from - // a generic failure so policy/UX can handle it differently. - PROVIDER_ERROR_CATEGORY_CONTENT_FILTERED = 6; - // Anything else. MUST include raw_detail for debugging; treated as - // non-retryable by default. - PROVIDER_ERROR_CATEGORY_UNKNOWN = 7; -} - -// ProviderError is the structured error every failure crossing this -// plugin boundary carries, per provider.md §8. -message ProviderError { - // This failure's category. MUST be set. - ProviderErrorCategory category = 1; - - // Human-readable description of the failure. - string message = 2; - - // Whether the kernel may retry this request as-is. - bool retryable = 3; - - // How long the kernel should wait before retrying, when the vendor - // supplies one (typically alongside PROVIDER_ERROR_CATEGORY_RATE_LIMITED). - // SHOULD be set when available. Refines provider.md §8's - // "retry_after_seconds" into the native well-known type. - optional google.protobuf.Duration retry_after = 4; - - // The raw vendor-provided error code or body, for debugging. SHOULD be - // set. - optional string raw_detail = 5; -} diff --git a/api/pluggableharness/agent/render/v1/render.proto b/api/pluggableharness/agent/render/v1/render.proto index c73589c..3a11b4d 100644 --- a/api/pluggableharness/agent/render/v1/render.proto +++ b/api/pluggableharness/agent/render/v1/render.proto @@ -2,7 +2,7 @@ syntax = "proto3"; // Package pluggableharness.agent.render.v1 defines the Emit->Render->Paint intermediate // representation described in specifications/frontend.md §1. Every plugin -// category's optional Render() RPC (provider.md §7, tool.md §7, +// category's optional Render() RPC (model.md §7, tool.md §7, // context.md §9, memory.md §10) returns this; every frontend/widget Paints // it. frontend.md §1 MUST: a frontend must render every RenderNode variant // gracefully with a generic fallback (e.g. an unrecognized-in-practice diff --git a/api/pluggableharness/agent/schema/v1/schema.proto b/api/pluggableharness/agent/schema/v1/schema.proto index bf19b64..8a60f51 100644 --- a/api/pluggableharness/agent/schema/v1/schema.proto +++ b/api/pluggableharness/agent/schema/v1/schema.proto @@ -1,9 +1,9 @@ syntax = "proto3"; // Package pluggableharness.agent.schema.v1 defines the restricted JSON-Schema subset -// described in specifications/provider.md §6, shared by tool input/output +// described in specifications/model.md §6, shared by tool input/output // schemas (specifications/tool.md §2) and model tool-calling declarations -// (specifications/provider.md §6). Deliberately NOT full JSON Schema: no +// (specifications/model.md §6). Deliberately NOT full JSON Schema: no // oneOf/anyOf/allOf, no $ref, no pattern, no format, no non-trivial // additionalProperties. Every adapter across every category MUST support // exactly this subset — see .claude/rules/proto.md. @@ -15,7 +15,7 @@ option go_package = "github.com/pluggableharness/agent/pkg/schema/proto/v1;schem // dedicated ENUM value here: JSON Schema's `enum` keyword is a value // constraint, not a distinct type, and in this subset it applies to a // STRING-typed node via Schema.enum_values (the overwhelmingly common -// case for tool argument enums) — see provider.md §6. +// case for tool argument enums) — see model.md §6. enum SchemaType { // Zero value. Never valid for a real schema node; its presence on the // wire means a caller forgot to set the field. @@ -25,7 +25,7 @@ enum SchemaType { // A JSON string. SCHEMA_TYPE_STRING = 2; // A JSON number (integer or floating point — the subset does not - // distinguish the two, per provider.md §6). + // distinguish the two, per model.md §6). SCHEMA_TYPE_NUMBER = 3; // A JSON boolean. SCHEMA_TYPE_BOOLEAN = 4; @@ -37,7 +37,7 @@ enum SchemaType { // self-recursive: an OBJECT node's properties and an ARRAY node's items // are themselves Schema nodes. This message is used both as a tool's // input_schema/output_schema (tool.md §2) and embedded in a model -// provider's tool-calling declarations (provider.md §6) — the same wire +// provider's tool-calling declarations (model.md §6) — the same wire // type in both places, by design, so a kernel-side validator has exactly // one Schema implementation to maintain. message Schema { diff --git a/api/pluggableharness/agent/slashcommand/v1/slashcommand.proto b/api/pluggableharness/agent/slashcommand/v1/slashcommand.proto index 350bcd5..e47ef94 100644 --- a/api/pluggableharness/agent/slashcommand/v1/slashcommand.proto +++ b/api/pluggableharness/agent/slashcommand/v1/slashcommand.proto @@ -3,7 +3,7 @@ syntax = "proto3"; // Package pluggableharness.agent.slashcommand.v1 defines the slash-command declaration // shape described in specifications/configuration.md §5 (and equivalently // specifications/frontend.md §5). Declarable as an optional repeated field -// in every category's capability response: provider.md §2 Capabilities, +// in every category's capability response: model.md §2 Capabilities, // tool.md §2 GetSchemaResponse, context.md §2 ContextCapabilities, // memory.md §3 MemoryCapabilities. package pluggableharness.agent.slashcommand.v1; diff --git a/api/pluggableharness/agent/tool/v1/tool.proto b/api/pluggableharness/agent/tool/v1/tool.proto index 98e420e..014e30b 100644 --- a/api/pluggableharness/agent/tool/v1/tool.proto +++ b/api/pluggableharness/agent/tool/v1/tool.proto @@ -32,7 +32,7 @@ service ToolService { rpc Configure(ConfigureRequest) returns (ConfigureResponse); // Invoke executes one tool call and streams back its events, per - // tool.md §4. Server-streaming, reusing provider.md §1's shape verbatim — + // tool.md §4. Server-streaming, reusing model.md §1's shape verbatim — // a non-incremental operation MUST still implement this shape, emitting // exactly one terminal `result` or `error`. Cancellation is the kernel // closing the gRPC stream; the plugin MUST treat this as normal control @@ -173,11 +173,11 @@ message ToolSchema { // MUST — shown to the model for tool selection and in plan diffs. string description = 4; - // MUST — the common JSON-Schema subset per provider.md §6, describing the + // MUST — the common JSON-Schema subset per model.md §6, describing the // shape of ToolCall.arguments for this operation. pluggableharness.agent.schema.v1.Schema input_schema = 5; - // MUST — the common JSON-Schema subset per provider.md §6, describing the + // MUST — the common JSON-Schema subset per model.md §6, describing the // shape of ToolResult.payload for this operation. pluggableharness.agent.schema.v1.Schema output_schema = 6; @@ -313,7 +313,7 @@ message ToolResult { } // ToolErrorCategory classifies why an Invoke call failed, per tool.md §8. -// Deliberately distinct from provider.md §8's ProviderErrorCategory — there +// Deliberately distinct from model.md §8's ModelErrorCategory — there // is no RATE_LIMITED or CONTEXT_LENGTH_EXCEEDED here, those are // model-vendor concepts. The two enums MUST NOT be merged even though the // surrounding error-envelope shape (category/message/retryable) is diff --git a/docs/first-party/providers/README.md b/docs/first-party/providers/README.md index 89798a3..e995bcb 100644 --- a/docs/first-party/providers/README.md +++ b/docs/first-party/providers/README.md @@ -3,7 +3,7 @@ This directory distills real, sourced capability data for the four LLM vendors PluggableHarness Agent ships first-party model-provider plugins for: **Anthropic**, **OpenAI**, **Google** (Gemini), and **xAI** (Grok). Three other vendors appear in the underlying research (Mistral, Cohere, Ollama) but are out of scope for first-party support and aren't covered here. > [!IMPORTANT] -> **These are descriptive reference documents, not authoritative PluggableHarness Agent protocol specs.** [`docs/specifications/provider/`](../../specifications/provider/README.md) remains the sole source of truth for the model-provider plugin protocol itself (`GetCapabilities`, `Configure`, `StreamCompletion`, `CountTokens`, the `ModelSpec`/`ThinkingSpec`/`CachingSpec`/`Pricing` data types). Each report's closing "Implications for PluggableHarness Agent" section makes non-authoritative, recommendation-grade observations about building that vendor's specific plugin adapter, cross-referenced into the protocol spec by heading anchor. +> **These are descriptive reference documents, not authoritative PluggableHarness Agent protocol specs.** [`docs/specifications/model/`](../../specifications/model/README.md) remains the sole source of truth for the model-provider plugin protocol itself (`GetCapabilities`, `Configure`, `StreamCompletion`, `CountTokens`, the `ModelSpec`/`ThinkingSpec`/`CachingSpec`/`Pricing` data types). Each report's closing "Implications for PluggableHarness Agent" section makes non-authoritative, recommendation-grade observations about building that vendor's specific plugin adapter, cross-referenced into the protocol spec by heading anchor. Confidence varies sharply by vendor: Anthropic's roster and behavior are well-established throughout; the other three vendors' data is internally consistent but leaves "uncertain" fields genuinely uncertain rather than smoothed over. Within a single vendor, confidence also tracks how flagship a model is — a newest/most-prominent model is typically fully specified, while smaller, newer, or less-visible variants (mini/flash/codex/realtime/build variants) are frequently "uncertain" across most fields. Each report's own "Confirmed vs. uncertain" section is the authoritative account of this per vendor — don't assume parity between a flagship and its siblings just because they share a naming prefix. diff --git a/docs/first-party/providers/anthropic.md b/docs/first-party/providers/anthropic.md index ae924ea..2b6b844 100644 --- a/docs/first-party/providers/anthropic.md +++ b/docs/first-party/providers/anthropic.md @@ -1,12 +1,12 @@ # Anthropic -A first-party model-provider reference for PluggableHarness Agent: Anthropic's model lineup, reasoning/caching behavior, and wire-protocol shape, as they bear on building the `docs/specifications/provider/` plugin adapter for this vendor. Descriptive reference, not a protocol spec — see [`docs/specifications/provider/`](../../specifications/provider/README.md) for PluggableHarness Agent's own design authority. +A first-party model-provider reference for PluggableHarness Agent: Anthropic's model lineup, reasoning/caching behavior, and wire-protocol shape, as they bear on building the `docs/specifications/model/` plugin adapter for this vendor. Descriptive reference, not a protocol spec — see [`docs/specifications/model/`](../../specifications/model/README.md) for PluggableHarness Agent's own design authority. ## 1. Overview Anthropic is the vendor whose Claude models are most directly relevant to a coding-agent harness — this project's own reasoning, and much of the terminology in its specs (`thinking`, `cache_control`, adaptive effort), is informed by Claude's API shape. The model family naming has moved away from purely dated snapshot IDs (`claude-3-5-sonnet-20241022`) toward a mix of plain generational names (`claude-opus-4-8`, `claude-sonnet-5`, `claude-sonnet-4-6`) and one remaining dated ID for the smaller tier (`claude-haiku-4-5-20251001`). A plugin author should not assume every Claude model ID follows the same pattern going forward. -Anthropic's overall API philosophy leans toward giving the model more autonomy over its own reasoning: the newest models default to an adaptive thinking mode the caller doesn't have to configure at all, in contrast to the older discrete `budget_tokens` knob that required the caller to pick a token count up front. Tool-call arguments and cache markers are both first-class, explicit constructs on the wire (`input` as a parsed object, `cache_control` as an explicit per-block annotation) rather than automatic, opaque vendor behavior — this is the vendor most likely to require an adapter to reason about `data-types.md`'s [`CachingSpec`](../../specifications/provider/data-types.md#cachingspec) `explicit_markers` mode correctly. +Anthropic's overall API philosophy leans toward giving the model more autonomy over its own reasoning: the newest models default to an adaptive thinking mode the caller doesn't have to configure at all, in contrast to the older discrete `budget_tokens` knob that required the caller to pick a token count up front. Tool-call arguments and cache markers are both first-class, explicit constructs on the wire (`input` as a parsed object, `cache_control` as an explicit per-block annotation) rather than automatic, opaque vendor behavior — this is the vendor most likely to require an adapter to reason about `data-types.md`'s [`CachingSpec`](../../specifications/model/data-types.md#cachingspec) `explicit_markers` mode correctly. ## 2. Model roster & capabilities @@ -59,16 +59,16 @@ Unlike the other three first-party vendors, there is no mini/flash/codex-style t ## 6. Implications for PluggableHarness Agent -**`ThinkingSpec`** ([`data-types.md#thinkingspec`](../../specifications/provider/data-types.md#thinkingspec)): Anthropic is the vendor that most directly motivated this type's sum-type shape rather than a boolean flag. A conformant adapter cannot declare one `ThinkingSpec` for the whole plugin — it must vary per `ModelSpec`: `mode: always_on_adaptive` for Opus 4.8 and Sonnet 5 (with `can_disable` reflecting that `budget_tokens` now 400s rather than silently degrading), `mode: continuous_budget` for Haiku 4.5 (which never gained adaptive support), and — for Sonnet 4.6, which genuinely supports both mechanisms simultaneously during its transitional window — the adapter author will need to pick one canonical `mode` to declare (adaptive is recommended by Anthropic) while being aware `budget_tokens` still functions underneath as an escape hatch not directly representable in a single `ThinkingSpec` value. The `default` field matters concretely here: because Sonnet 5 runs adaptive thinking even when a request omits `thinking` entirely, a kernel wanting deterministic, budget-bounded behavior must know to always send an explicit override rather than relying on omission meaning "no thinking." +**`ThinkingSpec`** ([`data-types.md#thinkingspec`](../../specifications/model/data-types.md#thinkingspec)): Anthropic is the vendor that most directly motivated this type's sum-type shape rather than a boolean flag. A conformant adapter cannot declare one `ThinkingSpec` for the whole plugin — it must vary per `ModelSpec`: `mode: always_on_adaptive` for Opus 4.8 and Sonnet 5 (with `can_disable` reflecting that `budget_tokens` now 400s rather than silently degrading), `mode: continuous_budget` for Haiku 4.5 (which never gained adaptive support), and — for Sonnet 4.6, which genuinely supports both mechanisms simultaneously during its transitional window — the adapter author will need to pick one canonical `mode` to declare (adaptive is recommended by Anthropic) while being aware `budget_tokens` still functions underneath as an escape hatch not directly representable in a single `ThinkingSpec` value. The `default` field matters concretely here: because Sonnet 5 runs adaptive thinking even when a request omits `thinking` entirely, a kernel wanting deterministic, budget-bounded behavior must know to always send an explicit override rather than relying on omission meaning "no thinking." -**`CachingSpec`** ([`data-types.md#cachingspec`](../../specifications/provider/data-types.md#cachingspec)): every current Anthropic model sets `mode: explicit_markers` — the adapter is responsible for placing `cache_control` breakpoints on content blocks per `data-types.md`'s explicit-markers semantics, not for detecting automatic caching. Given the 5-minute standard TTL, an adapter maintaining a long-running agentic session across tool-execution gaps is a strong candidate for implementing the optional `keepalive_supported` behavior described in `data-types.md`'s cache-keepalive note, since Anthropic's own API gives the adapter no server-side keepalive to rely on. +**`CachingSpec`** ([`data-types.md#cachingspec`](../../specifications/model/data-types.md#cachingspec)): every current Anthropic model sets `mode: explicit_markers` — the adapter is responsible for placing `cache_control` breakpoints on content blocks per `data-types.md`'s explicit-markers semantics, not for detecting automatic caching. Given the 5-minute standard TTL, an adapter maintaining a long-running agentic session across tool-execution gaps is a strong candidate for implementing the optional `keepalive_supported` behavior described in `data-types.md`'s cache-keepalive note, since Anthropic's own API gives the adapter no server-side keepalive to rely on. -**Tool schema and `ToolCall`/`ToolResult`** ([`data-types.md#tool-schema`](../../specifications/provider/data-types.md#tool-schema)): Anthropic is one of the vendors (with Google and Ollama) whose tool-call arguments already arrive as a parsed object, so the Anthropic adapter's translation at the string/object boundary is the simpler direction — it serializes the kernel's parsed-JSON internal representation directly into `input` with no encode/decode step, unlike an OpenAI-shaped adapter. The adapter does need to handle the parallel-tool-call batching rule from §4 above (all `tool_result` blocks for one turn must land in a single `user` message) since this is stricter than what the generic protocol assumes about one-result-per-message. +**Tool schema and `ToolCall`/`ToolResult`** ([`data-types.md#tool-schema`](../../specifications/model/data-types.md#tool-schema)): Anthropic is one of the vendors (with Google and Ollama) whose tool-call arguments already arrive as a parsed object, so the Anthropic adapter's translation at the string/object boundary is the simpler direction — it serializes the kernel's parsed-JSON internal representation directly into `input` with no encode/decode step, unlike an OpenAI-shaped adapter. The adapter does need to handle the parallel-tool-call batching rule from §4 above (all `tool_result` blocks for one turn must land in a single `user` message) since this is stricter than what the generic protocol assumes about one-result-per-message. -**`StreamCompletion` and `thinking`/`redacted_thinking` blocks** ([`protocol.md#streamcompletion`](../../specifications/provider/protocol.md#streamcompletion), [`data-types.md#canonical-message--content-block-schema`](../../specifications/provider/data-types.md#canonical-message--content-block-schema)): Claude's extended-thinking output can carry an opaque signature the spec requires the kernel and state backend to round-trip verbatim (`StreamEvent.ThinkingSignature.signature`). An Anthropic adapter must store and replay this signature unmodified on subsequent turns of the same session — Anthropic's API rejects a request where a prior `thinking` block is missing or altered, so any adapter bug that drops or mangles this field will surface as an outright request failure, not a quality regression. +**`StreamCompletion` and `thinking`/`redacted_thinking` blocks** ([`protocol.md#streamcompletion`](../../specifications/model/protocol.md#streamcompletion), [`data-types.md#canonical-message--content-block-schema`](../../specifications/model/data-types.md#canonical-message--content-block-schema)): Claude's extended-thinking output can carry an opaque signature the spec requires the kernel and state backend to round-trip verbatim (`StreamEvent.ThinkingSignature.signature`). An Anthropic adapter must store and replay this signature unmodified on subsequent turns of the same session — Anthropic's API rejects a request where a prior `thinking` block is missing or altered, so any adapter bug that drops or mangles this field will surface as an outright request failure, not a quality regression. -**`GetCapabilities`** ([`protocol.md#getcapabilities`](../../specifications/provider/protocol.md#getcapabilities)): given the retirement of `claude-3-5-sonnet-20241022`, the Anthropic adapter's built-in model list needs a real deprecation lifecycle — this vendor has already demonstrated it will pull a model ID from service with a hard 404 rather than a soft warning period, so `GetCapabilities` should not ship a plugin version whose roster still lists a model the live API no longer serves. +**`GetCapabilities`** ([`protocol.md#getcapabilities`](../../specifications/model/protocol.md#getcapabilities)): given the retirement of `claude-3-5-sonnet-20241022`, the Anthropic adapter's built-in model list needs a real deprecation lifecycle — this vendor has already demonstrated it will pull a model ID from service with a hard 404 rather than a soft warning period, so `GetCapabilities` should not ship a plugin version whose roster still lists a model the live API no longer serves. -**`CountTokens`** ([`protocol.md#counttokens`](../../specifications/provider/protocol.md#counttokens)): Anthropic exposes a token-counting capability, so this adapter is a good candidate to satisfy the protocol's preference for exact (`exact: true`) counts against the kernel's `CountTokens` primitive rather than falling back to the generic heuristic — a plugin author should implement this against Anthropic's real tokenizer rather than treating it as optional. +**`CountTokens`** ([`protocol.md#counttokens`](../../specifications/model/protocol.md#counttokens)): Anthropic exposes a token-counting capability, so this adapter is a good candidate to satisfy the protocol's preference for exact (`exact: true`) counts against the kernel's `CountTokens` primitive rather than falling back to the generic heuristic — a plugin author should implement this against Anthropic's real tokenizer rather than treating it as optional. -**`Pricing`** ([`data-types.md#pricing`](../../specifications/provider/data-types.md#pricing)): because caching is supported, `cache_write_per_mtok` and `cache_read_per_mtok` are both required fields on every Anthropic `PricingTier`, not optional ones — this report's source data did not include per-model dollar figures, so an adapter author will need to source current Anthropic pricing separately before populating this shape. +**`Pricing`** ([`data-types.md#pricing`](../../specifications/model/data-types.md#pricing)): because caching is supported, `cache_write_per_mtok` and `cache_read_per_mtok` are both required fields on every Anthropic `PricingTier`, not optional ones — this report's source data did not include per-model dollar figures, so an adapter author will need to source current Anthropic pricing separately before populating this shape. diff --git a/docs/first-party/providers/google.md b/docs/first-party/providers/google.md index ff1413d..79723e3 100644 --- a/docs/first-party/providers/google.md +++ b/docs/first-party/providers/google.md @@ -1,6 +1,6 @@ # Google (Gemini) -A first-party model-provider reference for PluggableHarness Agent: Google's model lineup, reasoning/caching behavior, and wire-protocol shape, as they bear on building the `docs/specifications/provider/` plugin adapter for this vendor. Descriptive reference, not a protocol spec — see [`docs/specifications/provider/`](../../specifications/provider/README.md) for PluggableHarness Agent's own design authority. +A first-party model-provider reference for PluggableHarness Agent: Google's model lineup, reasoning/caching behavior, and wire-protocol shape, as they bear on building the `docs/specifications/model/` plugin adapter for this vendor. Descriptive reference, not a protocol spec — see [`docs/specifications/model/`](../../specifications/model/README.md) for PluggableHarness Agent's own design authority. ## 1. Overview @@ -65,16 +65,16 @@ As a general pattern: confidence tracks how flagship a model is. `gemini-3-pro` ## 6. Implications for PluggableHarness Agent -**Per-model `ThinkingSpec.mode`, not a per-vendor constant.** Google's own lineup spans three different values of the [`ThinkingSpec`](../../specifications/provider/data-types.md#thinkingspec) `mode` enum on its own: `discrete_effort` for the 3.x line (with `effort_levels` populated from the LOW/MEDIUM/HIGH or MINIMAL/LOW/MEDIUM/HIGH sets and `default` set to that model's actual default, e.g. `"HIGH"` for `gemini-3-pro`), `continuous_budget` for the 2.5 line (`budget_range` populated from `thinkingBudget`'s token-count bounds), and effectively `none` for `gemini-1.5-pro`. This is exactly the scenario `data-types.md` cites as the reason `ThinkingSpec` lives on each `ModelSpec` rather than being a single vendor-level flag — a Google adapter must build a distinct `ThinkingSpec` per model, and must not assume the `thinking_level` vs `thinkingBudget` parameter name generalizes across the whole roster. +**Per-model `ThinkingSpec.mode`, not a per-vendor constant.** Google's own lineup spans three different values of the [`ThinkingSpec`](../../specifications/model/data-types.md#thinkingspec) `mode` enum on its own: `discrete_effort` for the 3.x line (with `effort_levels` populated from the LOW/MEDIUM/HIGH or MINIMAL/LOW/MEDIUM/HIGH sets and `default` set to that model's actual default, e.g. `"HIGH"` for `gemini-3-pro`), `continuous_budget` for the 2.5 line (`budget_range` populated from `thinkingBudget`'s token-count bounds), and effectively `none` for `gemini-1.5-pro`. This is exactly the scenario `data-types.md` cites as the reason `ThinkingSpec` lives on each `ModelSpec` rather than being a single vendor-level flag — a Google adapter must build a distinct `ThinkingSpec` per model, and must not assume the `thinking_level` vs `thinkingBudget` parameter name generalizes across the whole roster. -**`CachingSpec.mode` cannot represent Google's actual behavior as a single value.** [`CachingSpec`](../../specifications/provider/data-types.md#cachingspec)'s `mode` field is a single enum per model (`none` / `explicit_markers` / `implicit_automatic`) — a sum in the "one active mode" sense, not a set. But Google's 2.5+ models genuinely run both simultaneously: implicit automatic caching is on by default (75% discount, no caller action) *and* explicit manual declaration is available concurrently for a deeper 90% discount. Neither enum value alone captures this. A plugin author building this adapter has to make a real choice here rather than treating it as a formality: the pragmatic default is `mode: implicit_automatic` (it matches what happens when the caller does nothing, which is the common case), with the explicit/manual pathway and its better discount rate exposed separately — e.g. surfaced only through documentation or a provider-specific `Configure` option rather than through `CachingSpec` itself. This is worth flagging back to the protocol's designers as a real gap: the current `CachingSpec` shape has no way to declare "this model supports two caching modes concurrently, with different discount rates," which is precisely the situation Google's own docs describe. +**`CachingSpec.mode` cannot represent Google's actual behavior as a single value.** [`CachingSpec`](../../specifications/model/data-types.md#cachingspec)'s `mode` field is a single enum per model (`none` / `explicit_markers` / `implicit_automatic`) — a sum in the "one active mode" sense, not a set. But Google's 2.5+ models genuinely run both simultaneously: implicit automatic caching is on by default (75% discount, no caller action) *and* explicit manual declaration is available concurrently for a deeper 90% discount. Neither enum value alone captures this. A plugin author building this adapter has to make a real choice here rather than treating it as a formality: the pragmatic default is `mode: implicit_automatic` (it matches what happens when the caller does nothing, which is the common case), with the explicit/manual pathway and its better discount rate exposed separately — e.g. surfaced only through documentation or a provider-specific `Configure` option rather than through `CachingSpec` itself. This is worth flagging back to the protocol's designers as a real gap: the current `CachingSpec` shape has no way to declare "this model supports two caching modes concurrently, with different discount rates," which is precisely the situation Google's own docs describe. -**Tool-call arguments arrive pre-parsed.** Per the [tool schema](../../specifications/provider/data-types.md#tool-schema) section's cross-vendor note, Google — like Anthropic and Ollama, unlike OpenAI and Mistral — delivers `functionCall.args` as an already-parsed object rather than a JSON-encoded string. The adapter's `ToolCall` translation should pass this straight through into the kernel's internal parsed-JSON representation without a parse step, and the reverse `function_response` submission needs no re-encoding to a string either. +**Tool-call arguments arrive pre-parsed.** Per the [tool schema](../../specifications/model/data-types.md#tool-schema) section's cross-vendor note, Google — like Anthropic and Ollama, unlike OpenAI and Mistral — delivers `functionCall.args` as an already-parsed object rather than a JSON-encoded string. The adapter's `ToolCall` translation should pass this straight through into the kernel's internal parsed-JSON representation without a parse step, and the reverse `function_response` submission needs no re-encoding to a string either. -**Context window: record current state, not roadmap state.** `gemini-2.5-pro`'s "1M tokens (2M coming soon)" phrasing from the vendor's own material is a trap for a naive `ModelSpec.context_window` value — that field is a single `int` representing what the model can do today ([`ModelSpec`](../../specifications/provider/data-types.md#modelspec)), so the adapter must publish `1_000_000`, not a value that anticipates the unreleased 2M figure, and should update it only once the larger window actually ships. +**Context window: record current state, not roadmap state.** `gemini-2.5-pro`'s "1M tokens (2M coming soon)" phrasing from the vendor's own material is a trap for a naive `ModelSpec.context_window` value — that field is a single `int` representing what the model can do today ([`ModelSpec`](../../specifications/model/data-types.md#modelspec)), so the adapter must publish `1_000_000`, not a value that anticipates the unreleased 2M figure, and should update it only once the larger window actually ships. -**Vision and streaming gaps need resolving before publishing `ModelSpec`, not defaulting to true.** Because `supports_vision` and `supports_streaming` are both plain, required booleans on `ModelSpec`, and the [canonical message schema](../../specifications/provider/data-types.md#canonical-message--content-block-schema) requires an `image` block be rejected with a clear `invalid_request` error whenever `supports_vision` is `false`, an adapter cannot ship "uncertain" as a value for `gemini-3-flash`, `gemini-2.5-pro`, `gemini-2.5-flash`, or `gemini-1.5-pro` — each needs an actual confirmed answer before its `ModelSpec` is published, since guessing wrong in either direction either silently breaks image-capable requests or incorrectly rejects valid ones. +**Vision and streaming gaps need resolving before publishing `ModelSpec`, not defaulting to true.** Because `supports_vision` and `supports_streaming` are both plain, required booleans on `ModelSpec`, and the [canonical message schema](../../specifications/model/data-types.md#canonical-message--content-block-schema) requires an `image` block be rejected with a clear `invalid_request` error whenever `supports_vision` is `false`, an adapter cannot ship "uncertain" as a value for `gemini-3-flash`, `gemini-2.5-pro`, `gemini-2.5-flash`, or `gemini-1.5-pro` — each needs an actual confirmed answer before its `ModelSpec` is published, since guessing wrong in either direction either silently breaks image-capable requests or incorrectly rejects valid ones. -**Auth details block `Configure` today.** Because the exact API-key wire format isn't confirmed in the available material, whoever implements this plugin's [`Configure`](../../specifications/provider/protocol.md#configure) RPC needs to independently verify the header/parameter shape against Google's actual API reference before writing the implementation — `Configure` MUST reject cleanly on a missing key, which is straightforward, but it also needs to know where the key actually goes on the wire, which the source material doesn't specify. +**Auth details block `Configure` today.** Because the exact API-key wire format isn't confirmed in the available material, whoever implements this plugin's [`Configure`](../../specifications/model/protocol.md#configure) RPC needs to independently verify the header/parameter shape against Google's actual API reference before writing the implementation — `Configure` MUST reject cleanly on a missing key, which is straightforward, but it also needs to know where the key actually goes on the wire, which the source material doesn't specify. -**`CountTokens` and `Render` are unaddressed by the available research.** Nothing in the source material speaks to whether Google exposes a real per-model tokenizer that could back [`CountTokens`](../../specifications/provider/protocol.md#counttokens) with `exact: true` results, so this needs a direct look at Google's API reference rather than an assumption either way; until confirmed, the adapter should let counts fall through to the kernel's fallback heuristic rather than claiming exactness it hasn't verified. `Render` is a MAY and nothing here suggests Google needs anything beyond the kernel's generic fallback. +**`CountTokens` and `Render` are unaddressed by the available research.** Nothing in the source material speaks to whether Google exposes a real per-model tokenizer that could back [`CountTokens`](../../specifications/model/protocol.md#counttokens) with `exact: true` results, so this needs a direct look at Google's API reference rather than an assumption either way; until confirmed, the adapter should let counts fall through to the kernel's fallback heuristic rather than claiming exactness it hasn't verified. `Render` is a MAY and nothing here suggests Google needs anything beyond the kernel's generic fallback. diff --git a/docs/first-party/providers/openai.md b/docs/first-party/providers/openai.md index fc47f5f..5d00ceb 100644 --- a/docs/first-party/providers/openai.md +++ b/docs/first-party/providers/openai.md @@ -1,6 +1,6 @@ # OpenAI -A first-party model-provider reference for PluggableHarness Agent: OpenAI's model lineup, reasoning/caching behavior, and wire-protocol shape, as they bear on building the `docs/specifications/provider/` plugin adapter for this vendor. Descriptive reference, not a protocol spec — see [`docs/specifications/provider/`](../../specifications/provider/README.md) for PluggableHarness Agent's own design authority. +A first-party model-provider reference for PluggableHarness Agent: OpenAI's model lineup, reasoning/caching behavior, and wire-protocol shape, as they bear on building the `docs/specifications/model/` plugin adapter for this vendor. Descriptive reference, not a protocol spec — see [`docs/specifications/model/`](../../specifications/model/README.md) for PluggableHarness Agent's own design authority. ## 1. Overview @@ -53,14 +53,14 @@ Authentication is an HTTP Bearer token: `Authorization: Bearer
-[Model provider](specifications/provider/README.md) +[Model provider](specifications/model/README.md) The LLM vendor plugin protocol — capabilities, streaming completion, token counting, pricing.
diff --git a/docs/specifications/README.md b/docs/specifications/README.md index 946e4c4..a7c0f4a 100644 --- a/docs/specifications/README.md +++ b/docs/specifications/README.md @@ -10,7 +10,7 @@ Start with [`conventions.md`](conventions.md) — it defines the requirement key 2. [`glossary.md`](glossary.md) — terminology. 3. [`architecture.md`](architecture.md) — the system narrative. 4. The six plugin-category protocols (any order — cross-linked as needed): - - [`provider/`](provider/README.md) — model (LLM vendor) provider. + - [`model/`](model/README.md) — model (LLM vendor) provider. - [`tool/`](tool/README.md) — tool provider (resource / data_source / interactive). - [`context/`](context/README.md) — context provider. - [`memory/`](memory/README.md) — memory provider. diff --git a/docs/specifications/agent-loop/README.md b/docs/specifications/agent-loop/README.md index ecb79cd..cb53367 100644 --- a/docs/specifications/agent-loop/README.md +++ b/docs/specifications/agent-loop/README.md @@ -1,6 +1,6 @@ # Agent loop -Covers the kernel's own required turn-by-turn control flow — not a plugin protocol. Nothing in this directory is optional plugin surface; every conforming kernel implementation MUST implement this directory's MUST-level behavior regardless of which providers happen to be loaded. The tone throughout is "here is what the kernel does," not "here is what a plugin author implements" — contrast with [`provider/`](../provider/README.md), [`tool/`](../tool/README.md), [`context/`](../context/README.md), [`memory/`](../memory/README.md), and [`frontend/`](../frontend/README.md), each of which a third-party plugin author actually builds against. +Covers the kernel's own required turn-by-turn control flow — not a plugin protocol. Nothing in this directory is optional plugin surface; every conforming kernel implementation MUST implement this directory's MUST-level behavior regardless of which providers happen to be loaded. The tone throughout is "here is what the kernel does," not "here is what a plugin author implements" — contrast with [`model/`](../model/README.md), [`tool/`](../tool/README.md), [`context/`](../context/README.md), [`memory/`](../memory/README.md), and [`frontend/`](../frontend/README.md), each of which a third-party plugin author actually builds against. This design reflects patterns observed across roughly 16 agentic coding systems (Claude Code, Codex CLI, Gemini CLI, Aider, Cline, Kilo Code, opencode, Continue, Goose, OpenHands, SWE-agent, Zed, Plandex, Open Interpreter, Cursor, Windsurf/Cascade, Amp). Where a strong convergent pattern holds across independent implementations, this directory adopts it as a MUST. Where there's real divergence with no clear winner, a document here makes an explicit judgment call and says so, or carries the question into [`conformance.md`](conformance.md#open-questions). See [`architecture.md`](../architecture.md) for the surrounding system (provider categories, Emit→Render→Paint, state backend, plan/apply terminology) — this directory only covers the kernel loop algorithm in detail. diff --git a/docs/specifications/agent-loop/error-recovery.md b/docs/specifications/agent-loop/error-recovery.md index f5beb0c..f4a5a0a 100644 --- a/docs/specifications/agent-loop/error-recovery.md +++ b/docs/specifications/agent-loop/error-recovery.md @@ -4,12 +4,12 @@ How the kernel reacts to a failure surfacing mid-turn — a model-provider error ## Model-provider errors -Per [`provider/conformance.md#error-taxonomy`](../provider/conformance.md#error-taxonomy), the kernel's reaction to each error category is: +Per [`model/conformance.md#error-taxonomy`](../model/conformance.md#error-taxonomy), the kernel's reaction to each error category is: -- **`rate_limited` / `overloaded`**: the kernel MUST retry with exponential backoff and jitter. Base retry delays vary by more than an order of magnitude across surveyed harnesses, with no convergent "correct" value — the kernel MUST make the base delay and backoff factor configurable, shipping with canonical defaults rather than requiring configuration before first use: `base_delay_ms = 500`, `backoff_factor = 2`, `max_retries = 5` ([`configuration/blocks-reference.md`](../configuration/blocks-reference.md)'s `settings{}` block). These are a reasonable midpoint of the observed range across harnesses, not derived from any single one — operator-overridable, not load-bearing precision. The kernel MUST honor a supplied `retry_after_seconds` when present (both `ProviderError.retry_after_seconds` and, where the transport surfaces raw HTTP headers, `retry-after`/`retry-after-ms` forms). Per-attempt and per-session retry caps MUST be tracked separately — a session-wide retry ceiling prevents an endlessly-retrying single call from silently consuming the entire wall-clock/cost budget ([`turn-algorithm.md#independent-bound-dimensions`](turn-algorithm.md#independent-bound-dimensions)) without ever advancing the turn counter. +- **`rate_limited` / `overloaded`**: the kernel MUST retry with exponential backoff and jitter. Base retry delays vary by more than an order of magnitude across surveyed harnesses, with no convergent "correct" value — the kernel MUST make the base delay and backoff factor configurable, shipping with canonical defaults rather than requiring configuration before first use: `base_delay_ms = 500`, `backoff_factor = 2`, `max_retries = 5` ([`configuration/blocks-reference.md`](../configuration/blocks-reference.md)'s `settings{}` block). These are a reasonable midpoint of the observed range across harnesses, not derived from any single one — operator-overridable, not load-bearing precision. The kernel MUST honor a supplied `retry_after_seconds` when present (both `ModelError.retry_after_seconds` and, where the transport surfaces raw HTTP headers, `retry-after`/`retry-after-ms` forms). Per-attempt and per-session retry caps MUST be tracked separately — a session-wide retry ceiling prevents an endlessly-retrying single call from silently consuming the entire wall-clock/cost budget ([`turn-algorithm.md#independent-bound-dimensions`](turn-algorithm.md#independent-bound-dimensions)) without ever advancing the turn counter. - **`context_length_exceeded`**: the kernel MUST NOT blindly retry the same request. This MUST instead trigger a context-reduction path on the next `context-assemble` pass (compaction/pruning mechanics are context- and memory-provider concerns — see [`context/README.md`](../context/README.md) and [`memory/README.md`](../memory/README.md), not specified here) or, if no context provider can shrink further, fail the turn cleanly with a distinguishable status rather than looping. - **`auth_error` / `invalid_request`**: the kernel MUST NOT retry and MUST NOT silently fall back to another model in a routing chain — both are configuration/programming errors that retrying cannot fix and that fallback would only mask. -- **`content_filtered`**: the kernel MUST surface this distinctly (already required by [`provider/conformance.md#error-taxonomy`](../provider/conformance.md#error-taxonomy)) so a `post-model-response` observer or the frontend can react differently than to a generic failure. +- **`content_filtered`**: the kernel MUST surface this distinctly (already required by [`model/conformance.md#error-taxonomy`](../model/conformance.md#error-taxonomy)) so a `post-model-response` observer or the frontend can react differently than to a generic failure. ## Tool-provider (plugin) crashes diff --git a/docs/specifications/agent-loop/subagents.md b/docs/specifications/agent-loop/subagents.md index 4b787b9..d83f430 100644 --- a/docs/specifications/agent-loop/subagents.md +++ b/docs/specifications/agent-loop/subagents.md @@ -64,7 +64,7 @@ Scoping child tools to the task's privilege level at spawn time is a convergent ## Cancellation propagation -If a session is cancelled (the model-provider stream cancellation described in [`provider/README.md`](../provider/README.md#transport--lifecycle), or an explicit user interrupt), the kernel MUST cascade-cancel every in-flight child `RunSession` reachable from that session, recursively through any grandchildren. This is a live mechanism, not a state-backend lookup: the kernel propagates cancellation through its own in-memory tracking of currently-running child sessions (the same in-memory session state depth-budget threading and cost-rollup use, [`../state-backend.md#live-vs-post-hoc-tree-walking`](../state-backend.md#live-vs-post-hoc-tree-walking)) — the state backend plays no role in deciding *which* sessions to cancel. Cancellation MUST NOT leave orphaned child sessions running after their parent has been torn down. Each cancelled child session MUST reach `session-end` with `status = cancelled`, durably recorded to its own `session_meta` row, and its `final_message` (if any partial content exists) MUST still be recorded to the state backend for audit, even though it never reaches a waiting parent (the parent is itself being cancelled). +If a session is cancelled (the model-provider stream cancellation described in [`model/README.md`](../model/README.md#transport--lifecycle), or an explicit user interrupt), the kernel MUST cascade-cancel every in-flight child `RunSession` reachable from that session, recursively through any grandchildren. This is a live mechanism, not a state-backend lookup: the kernel propagates cancellation through its own in-memory tracking of currently-running child sessions (the same in-memory session state depth-budget threading and cost-rollup use, [`../state-backend.md#live-vs-post-hoc-tree-walking`](../state-backend.md#live-vs-post-hoc-tree-walking)) — the state backend plays no role in deciding *which* sessions to cancel. Cancellation MUST NOT leave orphaned child sessions running after their parent has been torn down. Each cancelled child session MUST reach `session-end` with `status = cancelled`, durably recorded to its own `session_meta` row, and its `final_message` (if any partial content exists) MUST still be recorded to the state backend for audit, even though it never reaches a waiting parent (the parent is itself being cancelled). ## Inter-session communication diff --git a/docs/specifications/agent-loop/turn-algorithm.md b/docs/specifications/agent-loop/turn-algorithm.md index 985ff15..55b2028 100644 --- a/docs/specifications/agent-loop/turn-algorithm.md +++ b/docs/specifications/agent-loop/turn-algorithm.md @@ -68,7 +68,7 @@ Each bound MUST be checked independently — hitting any one of the three MUST t ### Cost accounting -`ModelSpec.pricing` ([`provider/data-types.md#pricing`](../provider/data-types.md#pricing)) is what makes per-turn cost computation possible: the kernel MUST compute and persist `cost_usd` per `usage` event at receipt time, per [`provider/protocol.md#cost-computation`](../provider/protocol.md#cost-computation). `max_cost_usd` tracking is exactly that: the kernel MUST accumulate a running sum of `cost_usd` across every `usage` event in a session and compare it against `max_cost_usd` at step 17. +`ModelSpec.pricing` ([`model/data-types.md#pricing`](../model/data-types.md#pricing)) is what makes per-turn cost computation possible: the kernel MUST compute and persist `cost_usd` per `usage` event at receipt time, per [`model/protocol.md#cost-computation`](../model/protocol.md#cost-computation). `max_cost_usd` tracking is exactly that: the kernel MUST accumulate a running sum of `cost_usd` across every `usage` event in a session and compare it against `max_cost_usd` at step 17. **Cost rolls up the session tree** — the same reasoning [Depth limits](subagents.md#depth-limits) already establishes for `max_depth`. A session's `max_cost_usd` MUST account for its own spend *and every descendant `RunSession`'s spend*, not just its own direct model calls — otherwise a cost bound is trivially defeated by spawning sub-agents to do the expensive work: diff --git a/docs/specifications/architecture.md b/docs/specifications/architecture.md index bd0e813..4911749 100644 --- a/docs/specifications/architecture.md +++ b/docs/specifications/architecture.md @@ -8,7 +8,7 @@ This is a deliberate fusion of two lineages: Terraform's plugin/provider/ schema Six categories share a common shape: `GetSchema`/`GetCapabilities` (declare what you do), `Configure` (accept config decoded from HCL), then category-specific RPCs. -- **Model provider** ([`provider/`](provider/README.md)) — an LLM vendor. `GetCapabilities` returns a quantitative envelope per model (context window, thinking/caching modes, pricing tiers), not just feature flags. `StreamCompletion` is **server-streaming with cancellation**, not bidirectional — this matches how a real LLM vendor API actually works: one request in, one chunked/SSE response out; cancellation is the kernel closing the stream, a standard server-streaming operation. See [`provider/README.md`](provider/README.md#transport--lifecycle). +- **Model provider** ([`model/`](model/README.md)) — an LLM vendor. `GetCapabilities` returns a quantitative envelope per model (context window, thinking/caching modes, pricing tiers), not just feature flags. `StreamCompletion` is **server-streaming with cancellation**, not bidirectional — this matches how a real LLM vendor API actually works: one request in, one chunked/SSE response out; cancellation is the kernel closing the stream, a standard server-streaming operation. See [`model/README.md`](model/README.md#transport--lifecycle). - **Tool provider** ([`tool/`](tool/README.md)) — `GetSchema` returns resources/data-sources/interactive calls, each with a JSON-Schema input/ output and a `kind`. `Invoke` is server-streaming (so e.g. `exec` can stream live stdout instead of blocking). - **Memory provider** ([`memory/`](memory/README.md)) — reads at `context-assemble` (inject relevant recall), writes at `post-response`/ `session-end` (decide what's worth persisting). Backend-agnostic (markdown files, sqlite, vector store, remote service) behind one interface — the same abstraction-over-backend move Terraform makes for state. - **Context provider** ([`context/`](context/README.md)) — hooks `context-assemble`, contributes text/data before each turn. Multiple can load simultaneously (a CLAUDE.md reader, an AGENTS.md reader, etc.) — sidesteps the convention-file format war entirely; it's a plugin choice, not a core opinion. @@ -103,9 +103,9 @@ Ordering within a hook is declaration order in `agent.hcl`, not runtime registra ## Canonical message / tool-schema format -Internal representation is content-block messages (`text`/`tool_use`/ `tool_result`/`image`/`thinking`/`redacted_thinking`) — the widest practical superset today. This is the state backend's source of truth, independent of whether any one vendor's wire format still exists at replay time. Each model-provider adapter owns its own lossy translation to/from vendor wire format. See [`provider/data-types.md`](provider/data-types.md). +Internal representation is content-block messages (`text`/`tool_use`/ `tool_result`/`image`/`thinking`/`redacted_thinking`) — the widest practical superset today. This is the state backend's source of truth, independent of whether any one vendor's wire format still exists at replay time. Each model-provider adapter owns its own lossy translation to/from vendor wire format. See [`model/data-types.md`](model/data-types.md). -Tool schemas: declared once per resource in a common JSON Schema subset all major vendors actually support (object/string/number/boolean/array/enum — skip exotic keywords like `oneOf`/`$ref` chains); each adapter translates to its vendor's tool-definition format. See [`provider/data-types.md#tool-schema`](provider/data-types.md#tool-schema). +Tool schemas: declared once per resource in a common JSON Schema subset all major vendors actually support (object/string/number/boolean/array/enum — skip exotic keywords like `oneOf`/`$ref` chains); each adapter translates to its vendor's tool-definition format. See [`model/data-types.md#tool-schema`](model/data-types.md#tool-schema). ## Context budget @@ -113,7 +113,7 @@ Ceiling is **not** a config value — it's asserted at runtime from the resolved Allocation policy (v1): fixed per-context-provider token caps declared in `agent.hcl`, validated against the dynamically-known ceiling at assembly time. Adaptive priority-based negotiation (providers asked to compress under pressure) is explicitly deferred — no adaptive machinery until there's evidence the fixed-cap approach is insufficient. See [`context/data-types.md`](context/data-types.md#budget-mechanics). -Generation-time parameters are validated the same way: an effort/thinking setting is checked against the resolved model's declared capabilities before it ever reaches the wire, and model routing/fallback chains are capability-aware for the identical reason — a candidate is only eligible for a turn if it actually satisfies that turn's real requirements (context needed, tool-use, vision, thinking), not merely because it's listed first. See [`provider/protocol.md#generation-parameter-validation-and-capability-aware-routing`](provider/protocol.md#generation-parameter-validation-and-capability-aware-routing) and [`configuration/agent-profiles.md#model-routing`](configuration/agent-profiles.md#model-routing). +Generation-time parameters are validated the same way: an effort/thinking setting is checked against the resolved model's declared capabilities before it ever reaches the wire, and model routing/fallback chains are capability-aware for the identical reason — a candidate is only eligible for a turn if it actually satisfies that turn's real requirements (context needed, tool-use, vision, thinking), not merely because it's listed first. See [`model/protocol.md#generation-parameter-validation-and-capability-aware-routing`](model/protocol.md#generation-parameter-validation-and-capability-aware-routing) and [`configuration/agent-profiles.md#model-routing`](configuration/agent-profiles.md#model-routing). ## CLI shape diff --git a/docs/specifications/configuration/README.md b/docs/specifications/configuration/README.md index 3e81c9d..bb90a4d 100644 --- a/docs/specifications/configuration/README.md +++ b/docs/specifications/configuration/README.md @@ -1,6 +1,6 @@ # Configuration — `agent.hcl` -Covers the project-level configuration file (`agent.hcl`), the schema-to-`cty` bridge every provider's `Configure` RPC relies on, the policy DSL, agent profiles, the global user-level config file, and the kernel-written lock file. Unlike [`provider/`](../provider/README.md), [`tool/`](../tool/README.md), and [`context/`](../context/README.md) (plugin protocols) or [`agent-loop/`](../agent-loop/README.md) (kernel turn behavior), this category is a static wiring layer that reconciles concrete needs surfaced elsewhere: `tool/protocol.md`'s `risk` field, `agent-loop/`'s sub-agent profiles and loop bounds, and `context/`'s per-provider token budgets. +Covers the project-level configuration file (`agent.hcl`), the schema-to-`cty` bridge every provider's `Configure` RPC relies on, the policy DSL, agent profiles, the global user-level config file, and the kernel-written lock file. Unlike [`model/`](../model/README.md), [`tool/`](../tool/README.md), and [`context/`](../context/README.md) (plugin protocols) or [`agent-loop/`](../agent-loop/README.md) (kernel turn behavior), this category is a static wiring layer that reconciles concrete needs surfaced elsewhere: `tool/protocol.md`'s `risk` field, `agent-loop/`'s sub-agent profiles and loop bounds, and `context/`'s per-provider token budgets. ## Scope diff --git a/docs/specifications/configuration/agent-profiles.md b/docs/specifications/configuration/agent-profiles.md index 85c2ca8..e48219d 100644 --- a/docs/specifications/configuration/agent-profiles.md +++ b/docs/specifications/configuration/agent-profiles.md @@ -40,7 +40,7 @@ The root/main interactive session is not architecturally distinct from a sub-age ## Model routing -`model` is a structured block, not an inline shorthand string — one `primary` and zero or more `fallback` sub-blocks, each an explicit `{provider, id}` pair resolved against `required_providers`/`provider` blocks (see [`blocks-reference.md`](blocks-reference.md)). Fallback candidates MUST additionally satisfy the model provider's capability-aware routing rule (see [`../provider/README.md`](../provider/README.md)): a candidate is only eligible for a given turn if it satisfies that turn's actual requirements (context length needed, tool-use, vision, thinking), checked mechanically, not just listed in declaration order. +`model` is a structured block, not an inline shorthand string — one `primary` and zero or more `fallback` sub-blocks, each an explicit `{provider, id}` pair resolved against `required_providers`/`provider` blocks (see [`blocks-reference.md`](blocks-reference.md)). Fallback candidates MUST additionally satisfy the model provider's capability-aware routing rule (see [`../model/README.md`](../model/README.md)): a candidate is only eligible for a given turn if it satisfies that turn's actual requirements (context length needed, tool-use, vision, thinking), checked mechanically, not just listed in declaration order. Remember that `primary`/`fallback` are HCL blocks with two attributes each (`provider`, `id`) — they MUST be written multi-line; see [`blocks-reference.md#hcl-single-line-blocks-take-only-one-argument`](blocks-reference.md#hcl-single-line-blocks-take-only-one-argument). diff --git a/docs/specifications/configuration/settings-and-global.md b/docs/specifications/configuration/settings-and-global.md index c9b8947..6f2a508 100644 --- a/docs/specifications/configuration/settings-and-global.md +++ b/docs/specifications/configuration/settings-and-global.md @@ -14,7 +14,7 @@ settings { } ``` -`retry{}` holds the kernel's canonical backoff configuration — the values [`../agent-loop/turn-algorithm.md`](../agent-loop/turn-algorithm.md) uses when retrying a `rate_limited`/`overloaded` model-provider error (see [`../provider/conformance.md`](../provider/conformance.md#error-taxonomy)). It is operator-overridable but ships with sensible defaults so a bare `agent.hcl` works without tuning before first use: `base_delay_ms = 500`, `backoff_factor = 2`, `max_retries = 5`. Like `observability{}`, this sub-block is **all-or-nothing** once declared at all — every one of its three attributes is required; there is no partially-specified `retry{}`. +`retry{}` holds the kernel's canonical backoff configuration — the values [`../agent-loop/turn-algorithm.md`](../agent-loop/turn-algorithm.md) uses when retrying a `rate_limited`/`overloaded` model-provider error (see [`../model/conformance.md`](../model/conformance.md#error-taxonomy)). It is operator-overridable but ships with sensible defaults so a bare `agent.hcl` works without tuning before first use: `base_delay_ms = 500`, `backoff_factor = 2`, `max_retries = 5`. Like `observability{}`, this sub-block is **all-or-nothing** once declared at all — every one of its three attributes is required; there is no partially-specified `retry{}`. ## The `telemetry` switch diff --git a/docs/specifications/context/README.md b/docs/specifications/context/README.md index 46214d4..6e5151e 100644 --- a/docs/specifications/context/README.md +++ b/docs/specifications/context/README.md @@ -8,7 +8,7 @@ This category covers content injected into the prompt *before* a model call, sou - On-demand code-intelligence retrieval (grep, LSP, embeddings) — that's tool-provider territory, see [`tool/README.md`](../tool/README.md). - Cross-session persisted knowledge an agent itself decides to write — that's memory-provider territory, see [`memory/README.md`](../memory/README.md). -- Keeping a vendor prompt-cache prefix warm across a long tool-execution gap — that's a model-provider concern, owned by the adapter that already understands its own vendor's TTL mechanics, not this category or the kernel loop. See [`CachingSpec.keepalive_supported`](../provider/data-types.md#cachingspec). +- Keeping a vendor prompt-cache prefix warm across a long tool-execution gap — that's a model-provider concern, owned by the adapter that already understands its own vendor's TTL mechanics, not this category or the kernel loop. See [`CachingSpec.keepalive_supported`](../model/data-types.md#cachingspec). ## Transport & lifecycle diff --git a/docs/specifications/context/conformance.md b/docs/specifications/context/conformance.md index 0268991..bf78308 100644 --- a/docs/specifications/context/conformance.md +++ b/docs/specifications/context/conformance.md @@ -2,7 +2,7 @@ ## Error taxonomy -Smaller than the model-provider taxonomy ([`provider/conformance.md#error-taxonomy`](../provider/conformance.md#error-taxonomy)), but a plugin MUST still classify every failure into one of the following rather than collapsing them into one generic error: +Smaller than the model-provider taxonomy ([`model/conformance.md#error-taxonomy`](../model/conformance.md#error-taxonomy)), but a plugin MUST still classify every failure into one of the following rather than collapsing them into one generic error: | Category | Meaning | Kernel's expected reaction | |---|---|---| diff --git a/docs/specifications/context/data-types.md b/docs/specifications/context/data-types.md index 3713a89..818d041 100644 --- a/docs/specifications/context/data-types.md +++ b/docs/specifications/context/data-types.md @@ -20,7 +20,7 @@ ContextRequest { } ``` -`model_target` is the same `ModelTarget` shape [`memory/data-types.md`](../memory/data-types.md)'s `RecallRequest` carries — a rich "what am I assembling for" descriptor derived from the resolved model provider's `ModelSpec` ([`provider/data-types.md#modelspec`](../provider/data-types.md#modelspec)), distinct from the narrower `ModelRef` that [`kernel-callbacks.md#counttokens`](../kernel-callbacks.md#counttokens) uses to select a tokenizer. It lets a provider tailor content — and compute tokens against the right budget — for the model that will actually consume it, not just the one configured at session start; a sub-agent routed to a smaller model gets a correspondingly smaller `effective_ceiling` here automatically (see [`architecture.md#context-budget`](../architecture.md#context-budget)). +`model_target` is the same `ModelTarget` shape [`memory/data-types.md`](../memory/data-types.md)'s `RecallRequest` carries — a rich "what am I assembling for" descriptor derived from the resolved model provider's `ModelSpec` ([`model/data-types.md#modelspec`](../model/data-types.md#modelspec)), distinct from the narrower `ModelRef` that [`kernel-callbacks.md#counttokens`](../kernel-callbacks.md#counttokens) uses to select a tokenizer. It lets a provider tailor content — and compute tokens against the right budget — for the model that will actually consume it, not just the one configured at session start; a sub-agent routed to a smaller model gets a correspondingly smaller `effective_ceiling` here automatically (see [`architecture.md#context-budget`](../architecture.md#context-budget)). `conversation_history` arrives populated only when this provider's own `ContextCapabilities.compactor == true` (see [`protocol.md#session-wide-conversation-compaction`](protocol.md#session-wide-conversation-compaction)); for every other provider it's empty, indistinguishable from "not provided." diff --git a/docs/specifications/context/examples.md b/docs/specifications/context/examples.md index 870c459..c006ebd 100644 --- a/docs/specifications/context/examples.md +++ b/docs/specifications/context/examples.md @@ -38,7 +38,7 @@ message ContextSection { } ``` -Note the two `buf:lint:ignore` annotations on `Contribute`: the request and response are the bare `ContextRequest`/`ContextContribution`, not `ContributeRequest`/`ContributeResponse` — an intentional, annotated deviation from buf's default RPC-naming lint, chosen because neither name is reused by another RPC in this file and the spec's own names (`ContextRequest`, `ContextContribution`) carry real documentation value that a generic `ContributeRequest` wrapper wouldn't. This mirrors [`provider/examples.md`](../provider/examples.md#the-wire-protocol)'s identical annotation on `StreamCompletion`. +Note the two `buf:lint:ignore` annotations on `Contribute`: the request and response are the bare `ContextRequest`/`ContextContribution`, not `ContributeRequest`/`ContributeResponse` — an intentional, annotated deviation from buf's default RPC-naming lint, chosen because neither name is reused by another RPC in this file and the spec's own names (`ContextRequest`, `ContextContribution`) carry real documentation value that a generic `ContributeRequest` wrapper wouldn't. This mirrors [`model/examples.md`](../model/examples.md#the-wire-protocol)'s identical annotation on `StreamCompletion`. ## A worked `context-assemble` sequence diff --git a/docs/specifications/context/protocol.md b/docs/specifications/context/protocol.md index 34fa808..fb8de12 100644 --- a/docs/specifications/context/protocol.md +++ b/docs/specifications/context/protocol.md @@ -20,7 +20,7 @@ ContextCapabilities { `compactor` is a normal capability flag, not a special case: a context provider MAY declare itself the one responsible for compacting or summarizing content — including, but not limited to, other providers' already-assembled sections and the session's conversation history — when the context budget is under pressure. Declaring `compactor: true` extends what this provider is allowed to touch on `Contribute` (see [`data-types.md#ordering--chaining`](data-types.md#ordering--chaining)); a provider that doesn't declare it stays confined to its own section(s). -`ContextCapabilities` MAY additionally include `slash_commands: []SlashCommandSpec` and MUST include the provider's `ConfigSchema`, so the kernel knows what fields `Configure` expects before ever calling it — the same shape every provider category's `GetCapabilities` follows, see [`provider/protocol.md#getcapabilities`](../provider/protocol.md#getcapabilities). +`ContextCapabilities` MAY additionally include `slash_commands: []SlashCommandSpec` and MUST include the provider's `ConfigSchema`, so the kernel knows what fields `Configure` expects before ever calling it — the same shape every provider category's `GetCapabilities` follows, see [`model/protocol.md#getcapabilities`](../model/protocol.md#getcapabilities). ## `Configure` diff --git a/docs/specifications/conventions.md b/docs/specifications/conventions.md index 288e24c..ef4e3b1 100644 --- a/docs/specifications/conventions.md +++ b/docs/specifications/conventions.md @@ -13,7 +13,7 @@ How to read and how to write every file under `docs/specifications/`. ## Cross-references — anchors only, never section numbers -**Every cross-reference is a relative file path plus a Markdown heading anchor**, e.g. `[cost computation](provider/protocol.md#cost-computation)`. A heading anchor survives reordering of sections; only a heading *rename* breaks it, which is both rarer and easy to catch by grepping for the anchor text across the tree. +**Every cross-reference is a relative file path plus a Markdown heading anchor**, e.g. `[cost computation](model/protocol.md#cost-computation)`. A heading anchor survives reordering of sections; only a heading *rename* breaks it, which is both rarer and easy to catch by grepping for the anchor text across the tree. When linking to a heading, use GitHub-flavored anchor rules: lowercase, spaces to hyphens, punctuation stripped (`## Cost computation` → `#cost-computation`). @@ -31,7 +31,7 @@ When linking to a heading, use GitHub-flavored anchor rules: lowercase, spaces t ## Directory shape -Each plugin-category directory (`provider/`, `tool/`, `context/`, `memory/`, `frontend/`) follows the same five-file template: +Each plugin-category directory (`model/`, `tool/`, `context/`, `memory/`, `frontend/`) follows the same five-file template: - `README.md` — overview and transport & lifecycle. - `protocol.md` — every RPC in the category, request/response shape, MUST/SHOULD/MAY behavior. diff --git a/docs/specifications/frontend/README.md b/docs/specifications/frontend/README.md index 4b08ce4..86c86e0 100644 --- a/docs/specifications/frontend/README.md +++ b/docs/specifications/frontend/README.md @@ -5,7 +5,7 @@ Covers **two** plugin categories in one directory, both concerned with what the - **Frontend provider** ([`frontend-protocol.md`](frontend-protocol.md)) — owns the terminal (or window, or voice channel): the process the kernel's state-event stream attaches to, responsible for actually painting pixels/text and turning operator input into `ClientEvent`s. - **Widget provider** ([`widget-protocol.md`](widget-protocol.md)) — contributes content *into* whichever frontend is active, without owning it. This is a genuine sixth plugin category, not merely an extension of the other five (see [`architecture.md`](../architecture.md#the-six-provider-categories)) — a git-status panel or a context-budget indicator isn't naturally "a tool" or "a context provider," it just wants to put something on screen. -Both categories share one vocabulary, formalized once and reused verbatim: the [`RenderTree`](render-tree.md#rendertree) intermediate representation (text runs, code blocks, diffs, tables, links, a sub-session node, an interactive `action` node) and the region/placement model ([`render-tree.md`](render-tree.md#placement--regions)). Every other plugin category's optional `Render` RPC — [`provider/protocol.md#render`](../provider/protocol.md#render), [`tool/protocol.md#render`](../tool/protocol.md#render), and the equivalent sections in `context/` and `memory/` — returns exactly this type; this directory is where the type itself is formally, canonically defined. +Both categories share one vocabulary, formalized once and reused verbatim: the [`RenderTree`](render-tree.md#rendertree) intermediate representation (text runs, code blocks, diffs, tables, links, a sub-session node, an interactive `action` node) and the region/placement model ([`render-tree.md`](render-tree.md#placement--regions)). Every other plugin category's optional `Render` RPC — [`model/protocol.md#render`](../model/protocol.md#render), [`tool/protocol.md#render`](../tool/protocol.md#render), and the equivalent sections in `context/` and `memory/` — returns exactly this type; this directory is where the type itself is formally, canonically defined. The wire protocol for both categories, plus the shared `RenderTree` IR and the `SlashCommandSpec` type, is defined as gRPC services with protobuf messages — see [`examples.md`](examples.md) for the schema. `RenderTree` and `SlashCommandSpec` are deliberately factored into their own shared vocabulary rather than nested inside the frontend or widget definitions: both the frontend and widget protocols place content into the same `Region` vocabulary, and every provider category (not just frontend/widget) declares a `SlashCommandSpec` in its capability response — see [`frontend-protocol.md#slash-commands`](frontend-protocol.md#slash-commands). diff --git a/docs/specifications/frontend/frontend-protocol.md b/docs/specifications/frontend/frontend-protocol.md index e76c553..3db8d54 100644 --- a/docs/specifications/frontend/frontend-protocol.md +++ b/docs/specifications/frontend/frontend-protocol.md @@ -16,11 +16,11 @@ service FrontendService { } ``` -`GetCapabilities` returns this frontend's `slash_commands` (see [Slash commands](#slash-commands) below) and `ConfigSchema`; it MUST be cheaply re-queryable and MUST NOT require a network call, the same guarantee [`provider/protocol.md#getcapabilities`](../provider/protocol.md#getcapabilities) requires of a model provider. `Configure` follows the same contract as [`provider/protocol.md#configure`](../provider/protocol.md#configure): a config value decoded from the provider's `agent.hcl` block via the schema-to-cty bridge, rejected with a structured error at configure time rather than deferred to the first `Attach`, and never echoing a received secret back out through any event, render, or log line. +`GetCapabilities` returns this frontend's `slash_commands` (see [Slash commands](#slash-commands) below) and `ConfigSchema`; it MUST be cheaply re-queryable and MUST NOT require a network call, the same guarantee [`model/protocol.md#getcapabilities`](../model/protocol.md#getcapabilities) requires of a model provider. `Configure` follows the same contract as [`model/protocol.md#configure`](../model/protocol.md#configure): a config value decoded from the provider's `agent.hcl` block via the schema-to-cty bridge, rejected with a structured error at configure time rather than deferred to the first `Attach`, and never echoing a received secret back out through any event, render, or log line. ## Fast path vs. full render -Live token-by-token text streaming (a model provider's `text_delta`, per [`provider/protocol.md#streamcompletion`](../provider/protocol.md#streamcompletion)) does **not** round-trip through a producer's `Render` call per token — that would be far too slow. The kernel forwards raw text deltas to the frontend directly as they arrive, as `ServerEvent.stream_delta`; `Render` is invoked once per producer per logically-complete unit (a finished message, a finished tool result), not per token, and its result arrives as `ServerEvent.render` carrying a [`PlacedContent`](render-tree.md#placement--regions). +Live token-by-token text streaming (a model provider's `text_delta`, per [`model/protocol.md#streamcompletion`](../model/protocol.md#streamcompletion)) does **not** round-trip through a producer's `Render` call per token — that would be far too slow. The kernel forwards raw text deltas to the frontend directly as they arrive, as `ServerEvent.stream_delta`; `Render` is invoked once per producer per logically-complete unit (a finished message, a finished tool result), not per token, and its result arrives as `ServerEvent.render` carrying a [`PlacedContent`](render-tree.md#placement--regions). ```protobuf message ServerEvent { @@ -136,7 +136,7 @@ message ClientEvent { ## Slash commands -`SlashCommandSpec` is defined once, canonically, here — every other category's capability response ([`provider/protocol.md#getcapabilities`](../provider/protocol.md#getcapabilities), [`tool/protocol.md#getschema`](../tool/protocol.md#getschema), and the equivalent sections in `context/` and `memory/`) declares an optional `[]SlashCommandSpec` field of this same type and links back here rather than redefining it. The wire type is factored out into its own shared vocabulary for exactly that reason: it's shared by every provider category's `GetCapabilities`/`GetSchema` response, not owned by the frontend category alone. +`SlashCommandSpec` is defined once, canonically, here — every other category's capability response ([`model/protocol.md#getcapabilities`](../model/protocol.md#getcapabilities), [`tool/protocol.md#getschema`](../tool/protocol.md#getschema), and the equivalent sections in `context/` and `memory/`) declares an optional `[]SlashCommandSpec` field of this same type and links back here rather than redefining it. The wire type is factored out into its own shared vocabulary for exactly that reason: it's shared by every provider category's `GetCapabilities`/`GetSchema` response, not owned by the frontend category alone. ```protobuf enum Dispatch { diff --git a/docs/specifications/frontend/render-tree.md b/docs/specifications/frontend/render-tree.md index b5e422a..e0e826c 100644 --- a/docs/specifications/frontend/render-tree.md +++ b/docs/specifications/frontend/render-tree.md @@ -1,6 +1,6 @@ # RenderTree -The display-agnostic intermediate representation every category's optional `Render` RPC returns — [`provider/protocol.md#render`](../provider/protocol.md#render), [`tool/protocol.md#render`](../tool/protocol.md#render), and the equivalent sections in `context/` and `memory/` all return exactly this type. A frontend paints it; nothing upstream of `Render` needs to know how. This document is the canonical, standalone definition; every other category's `Render` section links back here rather than re-describing the shape. +The display-agnostic intermediate representation every category's optional `Render` RPC returns — [`model/protocol.md#render`](../model/protocol.md#render), [`tool/protocol.md#render`](../tool/protocol.md#render), and the equivalent sections in `context/` and `memory/` all return exactly this type. A frontend paints it; nothing upstream of `Render` needs to know how. This document is the canonical, standalone definition; every other category's `Render` section links back here rather than re-describing the shape. The wire type is deliberately factored into its own vocabulary, separate from both the frontend and widget protocols — both the frontend provider protocol ([`frontend-protocol.md`](frontend-protocol.md)) and the widget provider protocol ([`widget-protocol.md`](widget-protocol.md)) place content into the same `Region` enum, and neither category should depend on the other's definitions. diff --git a/docs/specifications/frontend/widget-protocol.md b/docs/specifications/frontend/widget-protocol.md index b91521e..9304d04 100644 --- a/docs/specifications/frontend/widget-protocol.md +++ b/docs/specifications/frontend/widget-protocol.md @@ -25,7 +25,7 @@ message WidgetCapabilities { } ``` -`Configure` follows the same contract as [`provider/protocol.md#configure`](../provider/protocol.md#configure): config decoded from the widget's `agent.hcl` block, rejected with a structured error at configure time rather than deferred, never echoing a received secret back out. +`Configure` follows the same contract as [`model/protocol.md#configure`](../model/protocol.md#configure): config decoded from the widget's `agent.hcl` block, rejected with a structured error at configure time rather than deferred, never echoing a received secret back out. `Attach` opens a server-streaming feed of this widget's rendered updates for one session: @@ -41,7 +41,7 @@ message WidgetUpdate { } ``` -This stream is purely how the widget pushes its rendered updates out; it never receives anything back on this channel — there is no equivalent of the frontend protocol's `ClientEvent`. Cancellation is the kernel closing the gRPC stream; the plugin **MUST** treat this as normal control flow, never as an error, the same cancellation discipline every server-streaming RPC in this protocol series requires ([`provider/README.md#transport--lifecycle`](../provider/README.md#transport--lifecycle)). +This stream is purely how the widget pushes its rendered updates out; it never receives anything back on this channel — there is no equivalent of the frontend protocol's `ClientEvent`. Cancellation is the kernel closing the gRPC stream; the plugin **MUST** treat this as normal control flow, never as an error, the same cancellation discipline every server-streaming RPC in this protocol series requires ([`model/README.md#transport--lifecycle`](../model/README.md#transport--lifecycle)). ## Deriving display state — no new data feed diff --git a/docs/specifications/glossary.md b/docs/specifications/glossary.md index d3bf274..d962daf 100644 --- a/docs/specifications/glossary.md +++ b/docs/specifications/glossary.md @@ -5,7 +5,7 @@ Terminology used throughout `docs/specifications/`. | Term | Meaning | |---|---| | **Provider** | A plugin binary implementing one of the six categories: model, tool, memory, context, frontend, widget. | -| **Category** | One of the six provider kinds above, each with its own protocol (`provider/`, `tool/`, `memory/`, `context/`, `frontend/` — widget is documented alongside frontend). | +| **Category** | One of the six provider kinds above, each with its own protocol (`model/`, `tool/`, `memory/`, `context/`, `frontend/` — widget is documented alongside frontend). | | **Resource** | A tool operation that **mutates** state — gated behind the plan/apply flow. See [`agent-loop/plan-apply-gate.md`](agent-loop/plan-apply-gate.md). | | **Data source** | A tool operation that only **reads** — executes freely (subject to a policy precheck, not a plan/apply gate), feeds the plan. | | **Interactive** | A tool kind for calls that neither read nor write state but require a human response mid-turn (e.g. `ask_user`). See [`tool/protocol.md`](tool/protocol.md#kind-interactive) and [`agent-loop/plan-apply-gate.md`](agent-loop/plan-apply-gate.md). | @@ -26,5 +26,5 @@ Terminology used throughout `docs/specifications/`. | **Depth budget** | The remaining sub-agent nesting allowance threaded from a profile's configured maximum, decremented per `RunSession` hop, distinct at the root (kernel default) vs. a child (inherited). | | **State backend** | The kernel-owned, non-pluggable (in v1) persistence layer — sqlite-per-session — recording every event, cost figure, and plan item. The kernel is its sole writer. See [`state-backend.md`](state-backend.md). | | **Schema-to-cty bridge** | The mechanism translating a provider's declared config schema into an `hcldec` spec so `agent.hcl` provider blocks decode through real HCL2/`cty`, distinct from the JSON-Schema subset tool authors use for LLM function-calling. See [`configuration/blocks-reference.md`](configuration/blocks-reference.md). | -| **Canonical message** | The kernel's internal content-block message representation (`text`, `tool_use`, `tool_result`, `image`, `thinking`, `redacted_thinking`) — the state backend's source of truth, independent of any one vendor's wire format. See [`provider/data-types.md`](provider/data-types.md). | +| **Canonical message** | The kernel's internal content-block message representation (`text`, `tool_use`, `tool_result`, `image`, `thinking`, `redacted_thinking`) — the state backend's source of truth, independent of any one vendor's wire format. See [`model/data-types.md`](model/data-types.md). | | **Lock file** | `.agent/agent.lock.hcl` — pins resolved provider version, source, and checksum per provider, mirroring `.terraform.lock.hcl`. See [`configuration/lock-file.md`](configuration/lock-file.md). | diff --git a/docs/specifications/kernel-callbacks.md b/docs/specifications/kernel-callbacks.md index 5a4cb71..6d96553 100644 --- a/docs/specifications/kernel-callbacks.md +++ b/docs/specifications/kernel-callbacks.md @@ -51,13 +51,13 @@ CountTokensResult { ```go CountTokens(req): if req.model_ref is set and that model provider implements the optional - CountTokens RPC (provider/protocol.md#counttokens): + CountTokens RPC (model/protocol.md#counttokens): return (that provider's count, exact: true) else: return (fallback_heuristic(req.content), exact: false) ``` -A model provider's own `CountTokens` (when implemented) uses its real vendor tokenizer — some vendors expose a dedicated counting endpoint, others require a bundled tokenizer library; this document doesn't mandate which, only the RPC shape. [`provider/protocol.md#counttokens`](provider/protocol.md#counttokens) declares this a SHOULD for model providers: the fallback formula below is deliberately kept simple and single-purpose rather than made smarter, on the reasoning that accuracy should come from providers actually implementing real tokenizers, not from the kernel guessing better. Still not a MUST there, since not every vendor makes exact counting cheap or even possible without a network round-trip — but `exact: false` results should be the exception in practice, not the norm. +A model provider's own `CountTokens` (when implemented) uses its real vendor tokenizer — some vendors expose a dedicated counting endpoint, others require a bundled tokenizer library; this document doesn't mandate which, only the RPC shape. [`model/protocol.md#counttokens`](model/protocol.md#counttokens) declares this a SHOULD for model providers: the fallback formula below is deliberately kept simple and single-purpose rather than made smarter, on the reasoning that accuracy should come from providers actually implementing real tokenizers, not from the kernel guessing better. Still not a MUST there, since not every vendor makes exact counting cheap or even possible without a network round-trip — but `exact: false` results should be the exception in practice, not the norm. ### Why a kernel primitive, not a provider-local heuristic @@ -154,7 +154,7 @@ The six-level vocabulary (`TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL`) is | Bidirectional callback channel, unconditional per plugin | MUST | "The callback channel" | | `RunSession` callable via this channel | MUST | semantics in [`agent-loop/subagents.md`](agent-loop/subagents.md) | | `CountTokens` callable via this channel | MUST | "CountTokens" | -| Model-provider's own `CountTokens` RPC | SHOULD, per model provider | [`provider/protocol.md#counttokens`](provider/protocol.md#counttokens) | +| Model-provider's own `CountTokens` RPC | SHOULD, per model provider | [`model/protocol.md#counttokens`](model/protocol.md#counttokens) | | Exact-vs-fallback resolution algorithm | MUST | "CountTokens" | | Single documented fallback formula, no per-caller variation | MUST | "The fallback heuristic" | | Context/memory providers computing `tokens` via this primitive, not their own heuristic | MUST | "Why a kernel primitive, not a provider-local heuristic" | diff --git a/docs/specifications/memory/README.md b/docs/specifications/memory/README.md index ebe3554..4e32384 100644 --- a/docs/specifications/memory/README.md +++ b/docs/specifications/memory/README.md @@ -1,6 +1,6 @@ # Memory provider protocol -Covers the **memory provider** category — plugins that persist knowledge across sessions and recall it into future ones. Sibling category to [`provider/`](../provider/README.md), [`tool/`](../tool/README.md), [`context/`](../context/README.md), and [`frontend/`](../frontend/README.md). +Covers the **memory provider** category — plugins that persist knowledge across sessions and recall it into future ones. Sibling category to [`model/`](../model/README.md), [`tool/`](../tool/README.md), [`context/`](../context/README.md), and [`frontend/`](../frontend/README.md). A memory provider does two things: it reads relevant recall into context assembly, and it writes new knowledge worth persisting across sessions. This is a **distinct plugin category with its own protocol**, not a reuse of the context provider's `Contribute` RPC — memory-specific data (record type, scope, provenance, ratification status) stays first-class through a dedicated `Recall` RPC, and the kernel adapts results into `ContextSection`s before merging them into the assembled prompt. See [`protocol.md#recall-the-read-side`](protocol.md#recall-the-read-side). diff --git a/docs/specifications/memory/protocol.md b/docs/specifications/memory/protocol.md index dc26c32..18b6112 100644 --- a/docs/specifications/memory/protocol.md +++ b/docs/specifications/memory/protocol.md @@ -24,7 +24,7 @@ Fires at the same `context-assemble` hook point context providers fire at — me ### Relevance ranking -Ranking under `token_budget` pressure carries exactly one protocol-level rule, not a full ranking algorithm: a provider SHOULD weight `project`-type records more heavily toward recency than `user`/`feedback`/`reference` records when deciding what to keep, directly matching [`taxonomy.md#project`](taxonomy.md#project)'s definition of `project` as the type that decays fastest. Beyond that one rule, the ranking mechanism itself — keyword match, an internal embedding index, whatever a provider chooses — is entirely provider-internal, consistent with retrieval and embeddings being out of scope elsewhere ([`provider/conformance.md`](../provider/conformance.md#required-vs-optional-support--summary-matrix)). +Ranking under `token_budget` pressure carries exactly one protocol-level rule, not a full ranking algorithm: a provider SHOULD weight `project`-type records more heavily toward recency than `user`/`feedback`/`reference` records when deciding what to keep, directly matching [`taxonomy.md#project`](taxonomy.md#project)'s definition of `project` as the type that decays fastest. Beyond that one rule, the ranking mechanism itself — keyword match, an internal embedding index, whatever a provider chooses — is entirely provider-internal, consistent with retrieval and embeddings being out of scope elsewhere ([`model/conformance.md`](../model/conformance.md#required-vs-optional-support--summary-matrix)). ### Kernel-side translation into context assembly diff --git a/docs/specifications/provider/README.md b/docs/specifications/model/README.md similarity index 87% rename from docs/specifications/provider/README.md rename to docs/specifications/model/README.md index 654f6d1..4405ea3 100644 --- a/docs/specifications/provider/README.md +++ b/docs/specifications/model/README.md @@ -1,6 +1,6 @@ # Model provider protocol -Covers the **model provider** category — an LLM vendor plugin (Anthropic, OpenAI, Gemini, etc.). Named `provider`, not `model-provider`, because in this system's Terraform-derived vocabulary the LLM vendor plugin is the closest analog to what Terraform itself calls a "provider" — it's the anchor spec category; the other five (`tool/`, `context/`, `memory/`, `frontend/`) follow the shape it establishes. +Covers the **model provider** category — an LLM vendor plugin (Anthropic, OpenAI, Gemini, etc.). In this system's Terraform-derived vocabulary the LLM vendor plugin is the closest analog to what Terraform itself calls a "provider" — it's the anchor spec category; the other five (`tool/`, `context/`, `memory/`, `frontend/`) follow the shape it establishes. Real-world LLM vendors (Anthropic, OpenAI, Google Gemini, Mistral, Cohere, xAI, Ollama, and others) diverge in significant ways — reasoning control, caching mechanics, tool-call wire shape — and this category's data types are shaped to accommodate that heterogeneity rather than assuming one vendor's design is universal. diff --git a/docs/specifications/provider/conformance.md b/docs/specifications/model/conformance.md similarity index 96% rename from docs/specifications/provider/conformance.md rename to docs/specifications/model/conformance.md index 7c38107..11e76df 100644 --- a/docs/specifications/provider/conformance.md +++ b/docs/specifications/model/conformance.md @@ -14,7 +14,7 @@ A plugin MUST classify every failure into one of the following, and MUST NOT col | `content_filtered` | Vendor refused/filtered content | Surface distinctly from a generic failure — policy/UX may want to handle this differently | | `unknown` | Anything else | MUST include the raw vendor error message/code for debugging; treat as non-retryable by default | -`ProviderError` MUST include: `category` (above), `message` (human-readable), `retryable` (bool), and SHOULD include `retry_after_seconds` and the raw vendor-provided error code/body for debugging. +`ModelError` MUST include: `category` (above), `message` (human-readable), `retryable` (bool), and SHOULD include `retry_after_seconds` and the raw vendor-provided error code/body for debugging. On the wire, each category maps to a `grpc/codes.Code`: `context_length_exceeded` → `ResourceExhausted`, `rate_limited` → `ResourceExhausted` with structured detail, `overloaded` → `Unavailable`, `auth_error` → `Unauthenticated`, `invalid_request` → `InvalidArgument`, `content_filtered` → `FailedPrecondition`, cancellation → `Canceled` — never an application error, `unknown`/unmapped → `Internal`, never `Unknown`. diff --git a/docs/specifications/provider/data-types.md b/docs/specifications/model/data-types.md similarity index 95% rename from docs/specifications/provider/data-types.md rename to docs/specifications/model/data-types.md index 9a93092..85878e7 100644 --- a/docs/specifications/provider/data-types.md +++ b/docs/specifications/model/data-types.md @@ -109,7 +109,7 @@ StreamEvent = oneof { tool_call_done { id: string } usage { input_tokens, output_tokens, cache_read_tokens?, cache_write_tokens? } stop { reason: StopReason } - error ProviderError // see conformance.md#error-taxonomy + error ModelError // see conformance.md#error-taxonomy } StopReason = enum { @@ -123,7 +123,7 @@ StopReason = enum { } ``` -A plugin MUST classify every terminal failure via a `stop` event's `content_filtered` reason or an `error` event carrying a `ProviderError` ([`conformance.md#error-taxonomy`](conformance.md#error-taxonomy)) — the in-band `error` variant is how a plugin reports a classified failure *within* an otherwise-open stream, distinct from the stream simply being torn down at the transport level (a gRPC-level status, or the kernel closing the stream on cancellation). A plugin whose backend fails outright before producing any events MAY end the stream with just an `error` event and no preceding `stop`. +A plugin MUST classify every terminal failure via a `stop` event's `content_filtered` reason or an `error` event carrying a `ModelError` ([`conformance.md#error-taxonomy`](conformance.md#error-taxonomy)) — the in-band `error` variant is how a plugin reports a classified failure *within* an otherwise-open stream, distinct from the stream simply being torn down at the transport level (a gRPC-level status, or the kernel closing the stream on cancellation). A plugin whose backend fails outright before producing any events MAY end the stream with just an `error` event and no preceding `stop`. ## Canonical message & content-block schema diff --git a/docs/specifications/provider/examples.md b/docs/specifications/model/examples.md similarity index 99% rename from docs/specifications/provider/examples.md rename to docs/specifications/model/examples.md index 9801dba..5a75c8b 100644 --- a/docs/specifications/provider/examples.md +++ b/docs/specifications/model/examples.md @@ -23,7 +23,7 @@ provider "anthropic" { The wire shape is (trimmed to the service declaration and `ModelSpec`): ```protobuf -service ProviderService { +service ModelService { rpc GetCapabilities(GetCapabilitiesRequest) returns (GetCapabilitiesResponse); rpc Configure(ConfigureRequest) returns (ConfigureResponse); // buf:lint:ignore RPC_RESPONSE_STANDARD_NAME diff --git a/docs/specifications/provider/protocol.md b/docs/specifications/model/protocol.md similarity index 100% rename from docs/specifications/provider/protocol.md rename to docs/specifications/model/protocol.md diff --git a/docs/specifications/state-backend.md b/docs/specifications/state-backend.md index df183b3..f8949a4 100644 --- a/docs/specifications/state-backend.md +++ b/docs/specifications/state-backend.md @@ -84,7 +84,7 @@ CREATE TABLE cost_ledger ( ); ``` -Appended once per completed model turn, populated from the same `cost_usd` computation the model provider protocol already requires the kernel to perform at usage-event time ([`provider/protocol.md#cost-computation`](provider/protocol.md#cost-computation)) — the kernel already has these numbers in hand at write time, so this table costs nothing extra to populate and turns running-total cost tracking (`SUM(cost_usd)`) into a single indexed query instead of a full scan and JSON-parse of every `message`-kind event. +Appended once per completed model turn, populated from the same `cost_usd` computation the model provider protocol already requires the kernel to perform at usage-event time ([`model/protocol.md#cost-computation`](model/protocol.md#cost-computation)) — the kernel already has these numbers in hand at write time, so this table costs nothing extra to populate and turns running-total cost tracking (`SUM(cost_usd)`) into a single indexed query instead of a full scan and JSON-parse of every `message`-kind event. ### plan_items @@ -128,7 +128,7 @@ This is the authoritative, complete enumeration of `events.kind` — the source ```protobuf kind = enum { message // a completed model turn's accumulated canonical - // message (provider/data-types.md#canonical-message); + // message (model/data-types.md#canonical-message); // usage/cost figures are embedded in this payload // and drive cost_ledger above tool_call diff --git a/docs/specifications/tool/README.md b/docs/specifications/tool/README.md index 1b82fd2..5d8086f 100644 --- a/docs/specifications/tool/README.md +++ b/docs/specifications/tool/README.md @@ -1,10 +1,10 @@ # Tool provider protocol -Covers the **tool provider** category — file I/O, shell execution, search, web access, task tracking, sub-agent spawning, and similar operations (Claude Code's "tools," Terraform's closest analog would be a provider's resources and data sources combined). Sibling to [`provider/`](../provider/README.md) (model provider) in this system's Terraform-derived vocabulary; the other plugin categories ([`context/`](../context/README.md), [`memory/`](../memory/README.md), [`frontend/`](../frontend/README.md)) follow the same shape this and `provider/` establish. +Covers the **tool provider** category — file I/O, shell execution, search, web access, task tracking, sub-agent spawning, and similar operations (Claude Code's "tools," Terraform's closest analog would be a provider's resources and data sources combined). Sibling to [`model/`](../model/README.md) (model provider) in this system's Terraform-derived vocabulary; the other plugin categories ([`context/`](../context/README.md), [`memory/`](../memory/README.md), [`frontend/`](../frontend/README.md)) follow the same shape this and `model/` establish. Tool sets converge strongly across agentic coding harnesses on a common core of operations. Where that convergence exists, this category treats it as evidence for what belongs in the reference catalog ([`reference-catalog.md`](reference-catalog.md)). Where genuine divergence exists (edit mechanisms, risk/approval models, concurrency handling), this category calls that out explicitly rather than picking one approach and presenting it as settled. -This category depends directly on [`provider/`](../provider/README.md): the common JSON-Schema subset tool authors write `input_schema`/`output_schema` within is the same one [`provider/data-types.md#tool-schema`](../provider/data-types.md#tool-schema) defines for model tool-calling declarations (one wire type, `pluggableharness.agent.schema.v1.Schema`, shared by both categories), and `Invoke`'s server-streaming-plus-cancellation shape reuses [`provider/README.md`](../provider/README.md#transport--lifecycle)'s `StreamCompletion` pattern verbatim. See [`architecture.md`](../architecture.md) for the surrounding system (plan/apply gate, hook dispatch, Emit→Render→Paint, state backend) — this directory only covers the tool-provider RPC surface, data types, and reference catalog in detail. +This category depends directly on [`model/`](../model/README.md): the common JSON-Schema subset tool authors write `input_schema`/`output_schema` within is the same one [`model/data-types.md#tool-schema`](../model/data-types.md#tool-schema) defines for model tool-calling declarations (one wire type, `pluggableharness.agent.schema.v1.Schema`, shared by both categories), and `Invoke`'s server-streaming-plus-cancellation shape reuses [`model/README.md`](../model/README.md#transport--lifecycle)'s `StreamCompletion` pattern verbatim. See [`architecture.md`](../architecture.md) for the surrounding system (plan/apply gate, hook dispatch, Emit→Render→Paint, state backend) — this directory only covers the tool-provider RPC surface, data types, and reference catalog in detail. ## Transport & lifecycle @@ -12,7 +12,7 @@ Subprocess + gRPC via `hashicorp/go-plugin`, per [`architecture.md`](../architec A tool provider plugin exposes three RPCs: `GetSchema`, `Configure`, `Invoke`. It MAY additionally implement `Render` (see [`protocol.md#render`](protocol.md#render)). -**`Invoke` is server-streaming**, the same shape [`provider/README.md`](../provider/README.md#transport--lifecycle) specifies for `StreamCompletion` and for the identical reason: a tool like `exec` needs to stream live stdout/stderr rather than blocking until completion, and none of the underlying primitives (process exec, HTTP fetch, file I/O) need mid-call client input on the same call. **Cancellation follows the model-provider pattern exactly**: the kernel cancels/closes the gRPC stream; it is not a distinct RPC or a sentinel event the plugin must invent. Plugin authors MUST treat stream cancellation as a normal, expected event — kill the child process, release file handles/sockets, discard buffers — never as an error condition. A tool provider and a model provider are both "long-running, streaming, cancellable" from the kernel's point of view, and giving them different cancellation mechanics would be an unforced inconsistency. +**`Invoke` is server-streaming**, the same shape [`model/README.md`](../model/README.md#transport--lifecycle) specifies for `StreamCompletion` and for the identical reason: a tool like `exec` needs to stream live stdout/stderr rather than blocking until completion, and none of the underlying primitives (process exec, HTTP fetch, file I/O) need mid-call client input on the same call. **Cancellation follows the model-provider pattern exactly**: the kernel cancels/closes the gRPC stream; it is not a distinct RPC or a sentinel event the plugin must invent. Plugin authors MUST treat stream cancellation as a normal, expected event — kill the child process, release file handles/sockets, discard buffers — never as an error condition. A tool provider and a model provider are both "long-running, streaming, cancellable" from the kernel's point of view, and giving them different cancellation mechanics would be an unforced inconsistency. ## Category structure diff --git a/docs/specifications/tool/conformance.md b/docs/specifications/tool/conformance.md index e675ebc..ef3f657 100644 --- a/docs/specifications/tool/conformance.md +++ b/docs/specifications/tool/conformance.md @@ -2,7 +2,7 @@ ## Error taxonomy -Distinct from [`provider/conformance.md#error-taxonomy`](../provider/conformance.md#error-taxonomy)'s `ProviderError` — a tool's failure modes are a different domain (no `rate_limited`/`context_length_exceeded`, which are model-vendor concepts) — but follows the same shape and the same non-negotiable principle: a plugin MUST classify every failure, MUST NOT collapse them into one generic error, for the same reason the model-provider protocol cites (undifferentiated errors are undebuggable after the fact). +Distinct from [`model/conformance.md#error-taxonomy`](../model/conformance.md#error-taxonomy)'s `ModelError` — a tool's failure modes are a different domain (no `rate_limited`/`context_length_exceeded`, which are model-vendor concepts) — but follows the same shape and the same non-negotiable principle: a plugin MUST classify every failure, MUST NOT collapse them into one generic error, for the same reason the model-provider protocol cites (undifferentiated errors are undebuggable after the fact). ```protobuf ToolError { @@ -57,7 +57,7 @@ On the wire, `process_crashed` maps to `codes.Unavailable` — the same code use |---|---|---| | `GetSchema` / `Configure` / `Invoke` RPCs | MUST | the whole protocol surface | | Streaming RPC shape for `Invoke` | MUST | see [`README.md`](README.md#transport--lifecycle) / [`protocol.md#invoke`](protocol.md#invoke) — applies even to non-streaming operations | -| `input_schema`/`output_schema` in the common JSON-Schema subset | MUST | [`provider/data-types.md#tool-schema`](../provider/data-types.md#tool-schema) | +| `input_schema`/`output_schema` in the common JSON-Schema subset | MUST | [`model/data-types.md#tool-schema`](../model/data-types.md#tool-schema) | | `kind` (resource / data_source / interactive) | MUST, per operation | drives the plan/apply gate; [`protocol.md#kind-interactive`](protocol.md#kind-interactive) | | `risk` classification | MUST, per operation | see [`data-types.md#riskclass`](data-types.md#riskclass); `read_only` for `data_source` and `interactive` alike | | `ConcurrencySpec.safe` | MUST, per operation except `interactive` | absent/unset MUST be treated as `false`; MUST NOT be declared for `interactive` | diff --git a/docs/specifications/tool/data-types.md b/docs/specifications/tool/data-types.md index 5046c6e..dd11205 100644 --- a/docs/specifications/tool/data-types.md +++ b/docs/specifications/tool/data-types.md @@ -33,7 +33,7 @@ ToolCall { id string // MUST — kernel-assigned, echoed in every emitted event for correlation tool_name string // MUST — matches a ToolSchema.name from this provider's GetSchema arguments JSON // MUST — already-parsed JSON conforming to input_schema; per - // provider/data-types.md#tool-schema, the kernel's internal ToolCall + // model/data-types.md#tool-schema, the kernel's internal ToolCall // representation always stores parsed arguments regardless of which // model-provider adapter produced them } diff --git a/docs/specifications/tool/examples.md b/docs/specifications/tool/examples.md index cd5bdf7..c2bfb3f 100644 --- a/docs/specifications/tool/examples.md +++ b/docs/specifications/tool/examples.md @@ -15,7 +15,7 @@ provider "filesystem" { } ``` -`allowed_roots` is the kind of ordinary `Configure` field [`protocol.md#configure`](protocol.md#configure) describes as a provider's capability boundary — not a secret, but a jail root the plugin enforces internally. Resolving `env(...)`-style indirection for any actual secret fields (a hosted `web_search` provider's API key, say) follows the same kernel-side HCL/`cty` bridge described in [`provider/examples.md`](../provider/examples.md#a-provider-block-in-agenthcl) — the plugin always receives a resolved literal value. +`allowed_roots` is the kind of ordinary `Configure` field [`protocol.md#configure`](protocol.md#configure) describes as a provider's capability boundary — not a secret, but a jail root the plugin enforces internally. Resolving `env(...)`-style indirection for any actual secret fields (a hosted `web_search` provider's API key, say) follows the same kernel-side HCL/`cty` bridge described in [`model/examples.md`](../model/examples.md#a-provider-block-in-agenthcl) — the plugin always receives a resolved literal value. ## The wire protocol diff --git a/docs/specifications/tool/protocol.md b/docs/specifications/tool/protocol.md index b0adf21..3487d7a 100644 --- a/docs/specifications/tool/protocol.md +++ b/docs/specifications/tool/protocol.md @@ -4,7 +4,7 @@ The three RPCs a tool provider plugin exposes, plus the optional `Render`. See [ ## `GetSchema` -Returns a list of `ToolSchema` values, one per operation the plugin exposes. Like [`provider/protocol.md#getcapabilities`](../provider/protocol.md#getcapabilities), this MUST be re-queryable cheaply and MUST NOT require a network call — a provider wrapping a hosted service (e.g. a web-search API) declares its schema statically; only `Invoke` talks to the network. +Returns a list of `ToolSchema` values, one per operation the plugin exposes. Like [`model/protocol.md#getcapabilities`](../model/protocol.md#getcapabilities), this MUST be re-queryable cheaply and MUST NOT require a network call — a provider wrapping a hosted service (e.g. a web-search API) declares its schema statically; only `Invoke` talks to the network. ```protobuf ToolSchema { @@ -16,7 +16,7 @@ ToolSchema { // and reads nothing external — see below. risk RiskClass // MUST — see data-types.md#riskclass description string // MUST — shown to the model for tool selection and in plan diffs - input_schema JSONSchema // MUST — common subset per provider/data-types.md#tool-schema + input_schema JSONSchema // MUST — common subset per model/data-types.md#tool-schema output_schema JSONSchema // MUST — same subset; describes the `result` payload shape streaming bool // MUST — true if Invoke may emit intermediate events (output_chunk, // progress, partial_result) before the terminal event; false if @@ -39,7 +39,7 @@ The overall `GetSchema` response (the wrapper around this list of `ToolSchema`s) ## `Configure` -Same contract as [`provider/protocol.md#configure`](../provider/protocol.md#configure): config decoded from the provider's `agent.hcl` block via the schema-to-cty bridge; field contents are provider-specific. +Same contract as [`model/protocol.md#configure`](../model/protocol.md#configure): config decoded from the provider's `agent.hcl` block via the schema-to-cty bridge; field contents are provider-specific. - `Configure` MUST reject with a structured error on missing required fields (e.g. an `exec` provider requiring a working-directory jail root) rather than deferring failure to the first `Invoke`. - A plugin MUST NOT echo a received secret (API keys for a hosted `web_search` provider, etc.) into an `Emit`'d event, `Render` output, log line, or error message. @@ -54,10 +54,10 @@ Semantics: - **`output_schema` conformance is enforced strictly, not advisory.** The kernel MUST validate a `result.payload` against the operation's declared `output_schema` before accepting it. A non-conforming payload MUST be rejected and re-surfaced to the plugin boundary as an `unknown`-category `ToolError` (see [`conformance.md#error-taxonomy`](conformance.md#error-taxonomy)) — not silently passed through to history, and not a warning-and-continue. Malformed data flowing into the state backend is a correctness bug, not a UX inconvenience to be lenient about. - Exactly one of `result` or `error` MUST close the stream. `output_chunk`, `progress`, and `partial_result` MAY each appear zero or more times before it; `exit_status` MAY appear at most once, and only for tools whose underlying operation is a child process (the exec/shell family). - `exit_status` is distinct from `result` because the two can genuinely be different moments: an exec tool's child process can exit while the tool itself is still doing post-processing (truncating output, computing a diff) before it can emit a conformant `result`. Providers for non-process-backed tools (file read, grep, web fetch) MUST NOT emit `exit_status`. -- A plugin whose operation is not naturally incremental (e.g. `file_read`) MUST still implement the streaming RPC shape, emitting a single terminal `result` with no lead-up events — the same non-streaming-backend accommodation [`provider/protocol.md#streamcompletion`](../provider/protocol.md#streamcompletion) makes for `StreamCompletion`. `ToolSchema.streaming = false` signals this as a UX hint. +- A plugin whose operation is not naturally incremental (e.g. `file_read`) MUST still implement the streaming RPC shape, emitting a single terminal `result` with no lead-up events — the same non-streaming-backend accommodation [`model/protocol.md#streamcompletion`](../model/protocol.md#streamcompletion) makes for `StreamCompletion`. `ToolSchema.streaming = false` signals this as a UX hint. - On cancellation (see [`README.md`](README.md#transport--lifecycle)), a plugin for a `resource` operation MUST make a best effort to report, via a final `output_chunk`/`partial_result` before the stream closes, what had actually happened before the cancel landed (e.g. "process received SIGTERM, partial output already streamed is valid") — the plan/apply audit log needs an honest record of partial mutation, not silence. A plugin MUST NOT synthesize a `result` claiming full success after a cancelled operation. - `output_chunk` ordering within a single stream MUST be preserved (stdout/stderr interleaving is otherwise ambiguous); the kernel treats `stream` as a hint for display, not a demultiplexing key the plugin can reorder around. ## Render -Same optionality as [`provider/protocol.md#render`](../provider/protocol.md#render) — returning the `RenderTree` formally defined in [`frontend/render-tree.md`](../frontend/render-tree.md) — but tool-result rendering is where custom `Render` matters *more* than it does for model providers (per [`architecture.md`](../architecture.md#emit--render--paint-pipeline), "the tool-result side ... is where custom rendering matters more"). Reference examples: an `edit_file` result rendering as a unified diff rather than raw before/after text; an `exec` result's accumulated `output_chunk`s rendering as a scrollback pane; a `spawn_subagent` result rendering as a collapsible sub-session node ([`architecture.md`](../architecture.md#emit--render--paint-pipeline)'s `RenderTree` already reserves a node type for this). If not implemented, the kernel falls back to its generic default (pretty-printed JSON payload). +Same optionality as [`model/protocol.md#render`](../model/protocol.md#render) — returning the `RenderTree` formally defined in [`frontend/render-tree.md`](../frontend/render-tree.md) — but tool-result rendering is where custom `Render` matters *more* than it does for model providers (per [`architecture.md`](../architecture.md#emit--render--paint-pipeline), "the tool-result side ... is where custom rendering matters more"). Reference examples: an `edit_file` result rendering as a unified diff rather than raw before/after text; an `exec` result's accumulated `output_chunk`s rendering as a scrollback pane; a `spawn_subagent` result rendering as a collapsible sub-session node ([`architecture.md`](../architecture.md#emit--render--paint-pipeline)'s `RenderTree` already reserves a node type for this). If not implemented, the kernel falls back to its generic default (pretty-printed JSON payload). diff --git a/internal/agentprofile/doc.go b/internal/agentprofile/doc.go index 67363b3..177eb14 100644 --- a/internal/agentprofile/doc.go +++ b/internal/agentprofile/doc.go @@ -10,7 +10,7 @@ // spawned from a parent — that make max_depth an inherited, only-ever- // shrinking budget rather than a static per-profile ceiling. // - Capability-aware model fallback (configuration.md §8.2, cross-referencing -// provider.md §9's required-capability matrix): walking a profile's +// model.md §9's required-capability matrix): walking a profile's // model{} block's primary-then-fallback chain and picking the first // candidate whose ModelSpec actually satisfies a turn's requirements. // - Tool-scoping resolution (configuration.md §8.3): expanding a profile's diff --git a/internal/agentprofile/model.go b/internal/agentprofile/model.go index 66ed124..f16187a 100644 --- a/internal/agentprofile/model.go +++ b/internal/agentprofile/model.go @@ -3,7 +3,7 @@ package agentprofile import ( "errors" - providerv1 "github.com/pluggableharness/agent/pkg/provider/proto/v1" + modelv1 "github.com/pluggableharness/agent/pkg/model/proto/v1" ) // ErrNoEligibleModel is returned by SelectModel when no candidate in the @@ -13,7 +13,7 @@ var ErrNoEligibleModel = errors.New("agentprofile: no eligible model in primary+ // TurnRequirements describes what a single turn actually needs from a // model, per configuration.md §8.2's "context length needed, tool-use, -// vision, thinking" axes (cross-referencing provider.md §9's required- +// vision, thinking" axes (cross-referencing model.md §9's required- // capability matrix, which gates exactly these same four capabilities). type TurnRequirements struct { // NeedsToolUse requires the candidate's ModelSpec.SupportsToolUse. @@ -33,7 +33,7 @@ type TurnRequirements struct { // per satisfies (configuration.md §8.2). Declaration order is a preference, // not the sole criterion: a candidate ineligible for this turn's actual // requirements is skipped even if it comes first, and the kernel falls -// through to the next declared candidate — this is provider.md §9's +// through to the next declared candidate — this is model.md §9's // capability-aware routing rule, checked mechanically per turn. // // specs is caller-supplied: this package owns none of the provider @@ -41,7 +41,7 @@ type TurnRequirements struct { // no entry in specs (not found, or its provider/model not loaded this // session) is treated as not eligible and skipped, not an error — only an // empty result across the whole chain is an error. -func SelectModel(block ModelBlock, specs map[ModelRef]*providerv1.ModelSpec, req TurnRequirements) (ModelRef, error) { +func SelectModel(block ModelBlock, specs map[ModelRef]*modelv1.ModelSpec, req TurnRequirements) (ModelRef, error) { candidates := make([]ModelRef, 0, 1+len(block.Fallbacks)) candidates = append(candidates, block.Primary) candidates = append(candidates, block.Fallbacks...) @@ -59,8 +59,8 @@ func SelectModel(block ModelBlock, specs map[ModelRef]*providerv1.ModelSpec, req } // satisfies reports whether spec meets every axis of req -// (configuration.md §8.2, provider.md §9). -func satisfies(spec *providerv1.ModelSpec, req TurnRequirements) bool { +// (configuration.md §8.2, model.md §9). +func satisfies(spec *modelv1.ModelSpec, req TurnRequirements) bool { if req.NeedsToolUse && !spec.GetSupportsToolUse() { return false } diff --git a/internal/agentprofile/model_test.go b/internal/agentprofile/model_test.go index 438ad16..002b62d 100644 --- a/internal/agentprofile/model_test.go +++ b/internal/agentprofile/model_test.go @@ -4,19 +4,19 @@ import ( "errors" "testing" - providerv1 "github.com/pluggableharness/agent/pkg/provider/proto/v1" + modelv1 "github.com/pluggableharness/agent/pkg/model/proto/v1" ) // capableSpec returns a ModelSpec that satisfies every TurnRequirements // axis exercised in this file, for tests to selectively degrade. -func capableSpec() *providerv1.ModelSpec { - return &providerv1.ModelSpec{ +func capableSpec() *modelv1.ModelSpec { + return &modelv1.ModelSpec{ Id: "capable", ContextWindow: 200_000, SupportsToolUse: true, SupportsVision: true, SupportsStreaming: true, - Thinking: &providerv1.ThinkingSpec{Supported: true}, + Thinking: &modelv1.ThinkingSpec{Supported: true}, } } @@ -28,7 +28,7 @@ func TestSelectModel_primaryEligible(t *testing.T) { Primary: primary, Fallbacks: []ModelRef{{Provider: "anthropic", ID: "claude-sonnet-5"}}, } - specs := map[ModelRef]*providerv1.ModelSpec{ + specs := map[ModelRef]*modelv1.ModelSpec{ primary: capableSpec(), {Provider: "anthropic", ID: "claude-sonnet-5"}: capableSpec(), } @@ -46,12 +46,12 @@ func TestSelectModel_fallsThroughOnEachRequirementAxis(t *testing.T) { tests := []struct { name string req TurnRequirements - make func() *providerv1.ModelSpec // primary spec, ineligible on exactly one axis + make func() *modelv1.ModelSpec // primary spec, ineligible on exactly one axis }{ { name: "tool use", req: TurnRequirements{NeedsToolUse: true}, - make: func() *providerv1.ModelSpec { + make: func() *modelv1.ModelSpec { s := capableSpec() s.SupportsToolUse = false return s @@ -60,7 +60,7 @@ func TestSelectModel_fallsThroughOnEachRequirementAxis(t *testing.T) { { name: "vision", req: TurnRequirements{NeedsVision: true}, - make: func() *providerv1.ModelSpec { + make: func() *modelv1.ModelSpec { s := capableSpec() s.SupportsVision = false return s @@ -69,16 +69,16 @@ func TestSelectModel_fallsThroughOnEachRequirementAxis(t *testing.T) { { name: "thinking, spec present but unsupported", req: TurnRequirements{NeedsThinking: true}, - make: func() *providerv1.ModelSpec { + make: func() *modelv1.ModelSpec { s := capableSpec() - s.Thinking = &providerv1.ThinkingSpec{Supported: false} + s.Thinking = &modelv1.ThinkingSpec{Supported: false} return s }, }, { name: "thinking, spec entirely absent", req: TurnRequirements{NeedsThinking: true}, - make: func() *providerv1.ModelSpec { + make: func() *modelv1.ModelSpec { s := capableSpec() s.Thinking = nil return s @@ -87,7 +87,7 @@ func TestSelectModel_fallsThroughOnEachRequirementAxis(t *testing.T) { { name: "context window too small", req: TurnRequirements{MinContextWindow: 150_000}, - make: func() *providerv1.ModelSpec { + make: func() *modelv1.ModelSpec { s := capableSpec() s.ContextWindow = 100_000 // below the 150_000 requirement; fallback's 200_000 (capableSpec default) clears it return s @@ -102,7 +102,7 @@ func TestSelectModel_fallsThroughOnEachRequirementAxis(t *testing.T) { primary := ModelRef{Provider: "anthropic", ID: "primary"} fallback := ModelRef{Provider: "anthropic", ID: "fallback"} block := ModelBlock{Primary: primary, Fallbacks: []ModelRef{fallback}} - specs := map[ModelRef]*providerv1.ModelSpec{ + specs := map[ModelRef]*modelv1.ModelSpec{ primary: tt.make(), fallback: capableSpec(), } @@ -127,7 +127,7 @@ func TestSelectModel_entireChainIneligible(t *testing.T) { incapable := capableSpec() incapable.SupportsToolUse = false - specs := map[ModelRef]*providerv1.ModelSpec{ + specs := map[ModelRef]*modelv1.ModelSpec{ primary: incapable, fallback: incapable, } @@ -146,7 +146,7 @@ func TestSelectModel_missingFromSpecsIsSkippedNotError(t *testing.T) { block := ModelBlock{Primary: primary, Fallbacks: []ModelRef{fallback}} // primary has no entry in specs at all. - specs := map[ModelRef]*providerv1.ModelSpec{ + specs := map[ModelRef]*modelv1.ModelSpec{ fallback: capableSpec(), } @@ -163,7 +163,7 @@ func TestSelectModel_emptySpecsMap(t *testing.T) { t.Parallel() block := ModelBlock{Primary: ModelRef{Provider: "anthropic", ID: "x"}} - _, err := SelectModel(block, map[ModelRef]*providerv1.ModelSpec{}, TurnRequirements{}) + _, err := SelectModel(block, map[ModelRef]*modelv1.ModelSpec{}, TurnRequirements{}) if !errors.Is(err, ErrNoEligibleModel) { t.Fatalf("SelectModel error = %v, want wrapping ErrNoEligibleModel", err) } diff --git a/internal/agentprofile/tools.go b/internal/agentprofile/tools.go index b6715ac..32d5d34 100644 --- a/internal/agentprofile/tools.go +++ b/internal/agentprofile/tools.go @@ -41,7 +41,7 @@ var ErrUnknownTool = errors.New("agentprofile: tool not advertised by provider") // the entry. This is config-validation territory where the information // needed to catch a typo (the provider's real schema) is right there, and // this project's stated general posture is "ambiguity is an error" (see -// provider.md §10's discussion of overlapping pricing tiers) — a +// model.md §10's discussion of overlapping pricing tiers) — a // misspelled tool name silently resolving to "granted nothing" would be a // much harder bug to notice than a load-time error naming the bad entry. A // reviewer preferring silent drop, or a separate "unresolved" return value diff --git a/internal/agentprofile/types.go b/internal/agentprofile/types.go index 628385e..fca6a2d 100644 --- a/internal/agentprofile/types.go +++ b/internal/agentprofile/types.go @@ -9,7 +9,7 @@ type ModelRef struct { // model-family name) that this ref resolves against. Provider string // ID is the vendor's exact model identifier, matching ModelSpec.Id - // (pkg/provider/proto/v1) for the resolved provider. + // (pkg/model/proto/v1) for the resolved provider. ID string } diff --git a/internal/kernelcallback/server_test.go b/internal/kernelcallback/server_test.go index d3e88bf..cc4bcf2 100644 --- a/internal/kernelcallback/server_test.go +++ b/internal/kernelcallback/server_test.go @@ -111,7 +111,7 @@ func TestServer_Log_ignoresContextProducer(t *testing.T) { // by the Server's own baked-in identity — attribution is a property of // this Server instance, never a client- or caller-supplied value. spoofed := &commonv1.ProducerRef{ - Category: commonv1.Category_CATEGORY_PROVIDER, + Category: commonv1.Category_CATEGORY_MODEL, Name: "spoofed", Version: "9.9.9", } diff --git a/internal/pluginruntime/README.md b/internal/pluginruntime/README.md index d00c7d0..600aa75 100644 --- a/internal/pluginruntime/README.md +++ b/internal/pluginruntime/README.md @@ -18,7 +18,7 @@ subprocess implementing one category: 5. Dialing and completing the handshake. 6. The authoritative post-handshake protocol-version gate. 7. Dispensing the category's raw generated service client - (`providerv1.ProviderServiceClient`, `toolv1.ToolServiceClient`, ...). + (`modelv1.ModelServiceClient`, `toolv1.ToolServiceClient`, ...). 8. Returning a `*Plugin` wrapping the dispensed client and the plugin's producer identity. diff --git a/internal/pluginruntime/adapter.go b/internal/pluginruntime/adapter.go index 173ff30..f640238 100644 --- a/internal/pluginruntime/adapter.go +++ b/internal/pluginruntime/adapter.go @@ -15,7 +15,7 @@ import ( frontendv1 "github.com/pluggableharness/agent/pkg/frontend/proto/v1" kernelv1 "github.com/pluggableharness/agent/pkg/kernel/proto/v1" memoryv1 "github.com/pluggableharness/agent/pkg/memory/proto/v1" - providerv1 "github.com/pluggableharness/agent/pkg/provider/proto/v1" + modelv1 "github.com/pluggableharness/agent/pkg/model/proto/v1" toolv1 "github.com/pluggableharness/agent/pkg/tool/proto/v1" widgetv1 "github.com/pluggableharness/agent/pkg/widget/proto/v1" @@ -82,8 +82,8 @@ func (p *categoryPlugin) newCallbackServer(opts []grpc.ServerOption) *grpc.Serve // dialed over conn — the value a Plugin's Dispensed() ultimately returns. func newCategoryClient(category commonv1.Category, conn *grpc.ClientConn) (any, error) { switch category { - case commonv1.Category_CATEGORY_PROVIDER: - return providerv1.NewProviderServiceClient(conn), nil + case commonv1.Category_CATEGORY_MODEL: + return modelv1.NewModelServiceClient(conn), nil case commonv1.Category_CATEGORY_TOOL: return toolv1.NewToolServiceClient(conn), nil case commonv1.Category_CATEGORY_CONTEXT: diff --git a/internal/pluginruntime/adapter_test.go b/internal/pluginruntime/adapter_test.go index fbf2b91..eda78a8 100644 --- a/internal/pluginruntime/adapter_test.go +++ b/internal/pluginruntime/adapter_test.go @@ -15,7 +15,7 @@ import ( contextv1 "github.com/pluggableharness/agent/pkg/context/proto/v1" frontendv1 "github.com/pluggableharness/agent/pkg/frontend/proto/v1" memoryv1 "github.com/pluggableharness/agent/pkg/memory/proto/v1" - providerv1 "github.com/pluggableharness/agent/pkg/provider/proto/v1" + modelv1 "github.com/pluggableharness/agent/pkg/model/proto/v1" toolv1 "github.com/pluggableharness/agent/pkg/tool/proto/v1" widgetv1 "github.com/pluggableharness/agent/pkg/widget/proto/v1" ) @@ -44,7 +44,7 @@ func TestPluginMap(t *testing.T) { cb := &fakeCallbackServer{} for _, category := range []commonv1.Category{ - commonv1.Category_CATEGORY_PROVIDER, + commonv1.Category_CATEGORY_MODEL, commonv1.Category_CATEGORY_TOOL, commonv1.Category_CATEGORY_CONTEXT, commonv1.Category_CATEGORY_MEMORY, @@ -87,7 +87,7 @@ func TestNewCategoryClient(t *testing.T) { category commonv1.Category want any }{ - {commonv1.Category_CATEGORY_PROVIDER, providerv1.ProviderServiceClient(nil)}, + {commonv1.Category_CATEGORY_MODEL, modelv1.ModelServiceClient(nil)}, {commonv1.Category_CATEGORY_TOOL, toolv1.ToolServiceClient(nil)}, {commonv1.Category_CATEGORY_CONTEXT, contextv1.ContextServiceClient(nil)}, {commonv1.Category_CATEGORY_MEMORY, memoryv1.MemoryServiceClient(nil)}, @@ -103,9 +103,9 @@ func TestNewCategoryClient(t *testing.T) { t.Fatalf("newCategoryClient: %v", err) } switch tt.category { - case commonv1.Category_CATEGORY_PROVIDER: - if _, ok := got.(providerv1.ProviderServiceClient); !ok { - t.Fatalf("got %T, want providerv1.ProviderServiceClient", got) + case commonv1.Category_CATEGORY_MODEL: + if _, ok := got.(modelv1.ModelServiceClient); !ok { + t.Fatalf("got %T, want modelv1.ModelServiceClient", got) } case commonv1.Category_CATEGORY_TOOL: if _, ok := got.(toolv1.ToolServiceClient); !ok { diff --git a/internal/pluginruntime/doc.go b/internal/pluginruntime/doc.go index 7994612..ae8b4fa 100644 --- a/internal/pluginruntime/doc.go +++ b/internal/pluginruntime/doc.go @@ -7,7 +7,7 @@ // subprocess spawn under a minimal environment allowlist, handshake, the // authoritative post-handshake protocol-version gate, and dispense — and // returns a *Plugin whose Dispensed() is the raw generated category -// service client (providerv1.ProviderServiceClient, toolv1.ToolServiceClient, +// service client (modelv1.ModelServiceClient, toolv1.ToolServiceClient, // etc.). Every launched plugin is simultaneously wired with a real, // servable KernelCallbackService on a fixed, well-known broker ID // (pkg/common.CallbackBrokerID), so the plugin can call back into the diff --git a/internal/pluginruntime/launch.go b/internal/pluginruntime/launch.go index 30b75c1..7653648 100644 --- a/internal/pluginruntime/launch.go +++ b/internal/pluginruntime/launch.go @@ -121,7 +121,7 @@ type Plugin struct { } // Dispensed returns the raw generated category-service client for this -// plugin (e.g. providerv1.ProviderServiceClient for a provider plugin, +// plugin (e.g. modelv1.ModelServiceClient for a model plugin, // per launch step 8). Callers type-assert to the category's generated // client interface — this package returns it as any because it has no // category-specific knowledge of its own. diff --git a/internal/statebackend/doc.go b/internal/statebackend/doc.go index da1f03d..c50e1f8 100644 --- a/internal/statebackend/doc.go +++ b/internal/statebackend/doc.go @@ -51,11 +51,11 @@ // Unix-like systems only; Windows ACLs are out of scope. // - cost_ledger.cost_usd stores exactly the cost_usd value the caller // computed (the model provider protocol's own cost computation, -// docs/specifications/provider/protocol.md#cost-computation); this +// docs/specifications/model/protocol.md#cost-computation); this // package never recomputes a cost or token figure itself // (.claude/rules/determinism.md). // - events.producer_category and producers.category store the lowercase -// plugin-category vocabulary: provider, tool, context, memory, +// plugin-category vocabulary: model, tool, context, memory, // frontend, widget — the same names docs/specifications/ uses as its // own per-category directory names. // - Store.List and Store.Children read each file's session_meta row diff --git a/internal/statebackend/event.go b/internal/statebackend/event.go index 66bada5..537a2a5 100644 --- a/internal/statebackend/event.go +++ b/internal/statebackend/event.go @@ -125,14 +125,14 @@ func decodeEventKind(text string) (kernelv1.EventKind, error) { // state-backend.md's DDL leaves the column's exact text vocabulary // undocumented (unlike session_meta.status and events.kind, which the spec // enumerates literally) — this uses the same lowercase category names the -// specifications/ tree itself uses as directory names (provider/, tool/, +// specifications/ tree itself uses as directory names (model/, tool/, // context/, memory/, frontend/, widget/), for consistency with every other // lowercase-text enum this package stores. CATEGORY_UNSPECIFIED is // deliberately absent — a producer's category MUST NOT ever be // unspecified (kernel-callbacks.md's server-derived producer identity is // always a real category). var producerCategoryText = map[commonv1.Category]string{ - commonv1.Category_CATEGORY_PROVIDER: "provider", + commonv1.Category_CATEGORY_MODEL: "model", commonv1.Category_CATEGORY_TOOL: "tool", commonv1.Category_CATEGORY_CONTEXT: "context", commonv1.Category_CATEGORY_MEMORY: "memory", diff --git a/internal/telemetry/span_test.go b/internal/telemetry/span_test.go index 2bfc70b..988fb26 100644 --- a/internal/telemetry/span_test.go +++ b/internal/telemetry/span_test.go @@ -137,7 +137,7 @@ func TestStartModelCall_withProducer(t *testing.T) { t.Parallel() p, backend := newTestProvider(t) - producer := &commonv1.ProducerRef{Category: commonv1.Category_CATEGORY_PROVIDER, Name: "anthropic", Version: "1.0.0"} + producer := &commonv1.ProducerRef{Category: commonv1.Category_CATEGORY_MODEL, Name: "anthropic", Version: "1.0.0"} _, span := p.StartModelCall(context.Background(), "claude-sonnet", producer) telemetry.EndSpan(span, nil) diff --git a/mkdocs.yml b/mkdocs.yml index 774bba8..0e0c421 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -136,7 +136,7 @@ plugins: - specifications/*.md - specifications/agent-loop/*.md - specifications/configuration/*.md - - specifications/provider/*.md + - specifications/model/*.md - specifications/tool/*.md - specifications/context/*.md - specifications/memory/*.md @@ -174,11 +174,11 @@ nav: - Examples: specifications/configuration/examples.md - Conformance: specifications/configuration/conformance.md - Model provider protocol: - - specifications/provider/README.md - - Protocol: specifications/provider/protocol.md - - Data types: specifications/provider/data-types.md - - Examples: specifications/provider/examples.md - - Conformance: specifications/provider/conformance.md + - specifications/model/README.md + - Protocol: specifications/model/protocol.md + - Data types: specifications/model/data-types.md + - Examples: specifications/model/examples.md + - Conformance: specifications/model/conformance.md - Tool provider protocol: - specifications/tool/README.md - Protocol: specifications/tool/protocol.md diff --git a/pkg/common/plugin.go b/pkg/common/plugin.go index 412e5b2..bd7b26d 100644 --- a/pkg/common/plugin.go +++ b/pkg/common/plugin.go @@ -58,8 +58,8 @@ const CallbackBrokerID uint32 = 1 // correctly wired kernel or plugin. func PluginKey(c commonv1.Category) string { switch c { - case commonv1.Category_CATEGORY_PROVIDER: - return "provider" + case commonv1.Category_CATEGORY_MODEL: + return "model" case commonv1.Category_CATEGORY_TOOL: return "tool" case commonv1.Category_CATEGORY_CONTEXT: diff --git a/pkg/common/plugin_test.go b/pkg/common/plugin_test.go index ac31433..a79895c 100644 --- a/pkg/common/plugin_test.go +++ b/pkg/common/plugin_test.go @@ -35,7 +35,7 @@ func TestPluginKey(t *testing.T) { want string }{ {"unspecified", commonv1.Category_CATEGORY_UNSPECIFIED, "unspecified"}, - {"provider", commonv1.Category_CATEGORY_PROVIDER, "provider"}, + {"model", commonv1.Category_CATEGORY_MODEL, "model"}, {"tool", commonv1.Category_CATEGORY_TOOL, "tool"}, {"context", commonv1.Category_CATEGORY_CONTEXT, "context"}, {"memory", commonv1.Category_CATEGORY_MEMORY, "memory"}, diff --git a/pkg/common/proto/v1/common.pb.go b/pkg/common/proto/v1/common.pb.go index c8f8cbd..01998be 100644 --- a/pkg/common/proto/v1/common.pb.go +++ b/pkg/common/proto/v1/common.pb.go @@ -39,8 +39,8 @@ const ( // Zero value. Never valid for a real producer; its presence on the wire // means a caller forgot to set the field. Category_CATEGORY_UNSPECIFIED Category = 0 - // A model (LLM vendor) provider — specifications/provider.md. - Category_CATEGORY_PROVIDER Category = 1 + // A model (LLM vendor) provider — specifications/model.md. + Category_CATEGORY_MODEL Category = 1 // A tool provider — specifications/tool.md. Category_CATEGORY_TOOL Category = 2 // A context provider — specifications/context.md. @@ -57,7 +57,7 @@ const ( var ( Category_name = map[int32]string{ 0: "CATEGORY_UNSPECIFIED", - 1: "CATEGORY_PROVIDER", + 1: "CATEGORY_MODEL", 2: "CATEGORY_TOOL", 3: "CATEGORY_CONTEXT", 4: "CATEGORY_MEMORY", @@ -66,7 +66,7 @@ var ( } Category_value = map[string]int32{ "CATEGORY_UNSPECIFIED": 0, - "CATEGORY_PROVIDER": 1, + "CATEGORY_MODEL": 1, "CATEGORY_TOOL": 2, "CATEGORY_CONTEXT": 3, "CATEGORY_MEMORY": 4, @@ -274,10 +274,10 @@ const file_pluggableharness_agent_common_v1_common_proto_rawDesc = "" + "\x10protocol_version\x18\x05 \x01(\rR\x0fprotocolVersion\"i\n" + "\vProviderRef\x12F\n" + "\bcategory\x18\x01 \x01(\x0e2*.pluggableharness.agent.common.v1.CategoryR\bcategory\x12\x12\n" + - "\x04name\x18\x02 \x01(\tR\x04name*\xa5\x01\n" + + "\x04name\x18\x02 \x01(\tR\x04name*\xa2\x01\n" + "\bCategory\x12\x18\n" + - "\x14CATEGORY_UNSPECIFIED\x10\x00\x12\x15\n" + - "\x11CATEGORY_PROVIDER\x10\x01\x12\x11\n" + + "\x14CATEGORY_UNSPECIFIED\x10\x00\x12\x12\n" + + "\x0eCATEGORY_MODEL\x10\x01\x12\x11\n" + "\rCATEGORY_TOOL\x10\x02\x12\x14\n" + "\x10CATEGORY_CONTEXT\x10\x03\x12\x13\n" + "\x0fCATEGORY_MEMORY\x10\x04\x12\x15\n" + diff --git a/pkg/config/proto/v1/config.pb.go b/pkg/config/proto/v1/config.pb.go index a79494c..c949637 100644 --- a/pkg/config/proto/v1/config.pb.go +++ b/pkg/config/proto/v1/config.pb.go @@ -107,7 +107,7 @@ type ConfigAttribute struct { Type AttrType `protobuf:"varint,2,opt,name=type,proto3,enum=pluggableharness.agent.config.v1.AttrType" json:"type,omitempty"` // Whether agent.hcl MUST set this attribute. The kernel MUST reject a // Configure call with a missing required attribute via a structured - // error, per provider.md §3 / tool.md §3's shared Configure contract. + // error, per model.md §3 / tool.md §3's shared Configure contract. Required bool `protobuf:"varint,3,opt,name=required,proto3" json:"required,omitempty"` // MUST be true for any attribute that can hold a secret (API keys, // tokens, credentials). configuration.md §4 restricts a sensitive diff --git a/pkg/content/proto/v1/content.pb.go b/pkg/content/proto/v1/content.pb.go index f3cf931..9102b05 100644 --- a/pkg/content/proto/v1/content.pb.go +++ b/pkg/content/proto/v1/content.pb.go @@ -5,7 +5,7 @@ // source: pluggableharness/agent/content/v1/content.proto // Package pluggableharness.agent.content.v1 defines the canonical content-block message -// schema described in specifications/provider.md §5 — the state backend's +// schema described in specifications/model.md §5 — the state backend's // source of truth for conversation history (state-backend.md §5's `message` // event kind). Every model-provider adapter translates its own vendor // format to and from exactly this schema; nothing else in the system @@ -90,7 +90,7 @@ func (Role) EnumDescriptor() ([]byte, []int) { } // Message is one turn in the canonical conversation history: a role plus -// an ordered list of content blocks. provider.md §5 is the source of these +// an ordered list of content blocks. model.md §5 is the source of these // semantics. type Message struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -149,7 +149,7 @@ func (x *Message) GetContent() []*ContentBlock { // ContentBlock is one block within a Message. Exactly one variant is set. // Which variants a given model MAY produce/accept is gated by that model's -// ModelSpec capability flags (provider.md §2, §5): `text` MUST work both +// ModelSpec capability flags (model.md §2, §5): `text` MUST work both // directions unconditionally; `image` requires supports_vision; `tool_use`/ // `tool_result` require supports_tool_use; `thinking`/`redacted_thinking` // require ThinkingSpec.supported. @@ -300,7 +300,7 @@ func (*ContentBlock_Thinking) isContentBlock_Block() {} func (*ContentBlock_RedactedThinking) isContentBlock_Block() {} // TextBlock is plain conversational text. MUST be supported by every -// model, in both directions (provider.md §5). +// model, in both directions (model.md §5). type TextBlock struct { state protoimpl.MessageState `protogen:"open.v1"` // The block's text content. @@ -348,7 +348,7 @@ func (x *TextBlock) GetText() string { // ToolUseBlock represents the model requesting a tool invocation. `id` // correlates this block to the resulting ToolResultBlock, mirroring -// provider.md §4's StreamEvent tool_call_start/tool_call_done id +// model.md §4's StreamEvent tool_call_start/tool_call_done id // correlation once the stream has been assembled into a persisted message. type ToolUseBlock struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -489,9 +489,9 @@ func (x *ToolResultBlock) GetIsError() bool { } // ImageBlock is inline image content. Requires the target model's -// ModelSpec.supports_vision (provider.md §5); the kernel MUST reject an +// ModelSpec.supports_vision (model.md §5); the kernel MUST reject an // ImageBlock sent to a model where that flag is false, with -// invalid_request (provider.md §8). +// invalid_request (model.md §8). type ImageBlock struct { state protoimpl.MessageState `protogen:"open.v1"` // Raw image bytes. @@ -547,16 +547,16 @@ func (x *ImageBlock) GetMediaType() string { } // ThinkingBlock is the model's extended-reasoning output, when -// ThinkingSpec.supported (provider.md §2). Requires the model's +// ThinkingSpec.supported (model.md §2). Requires the model's // ThinkingSpec.supported to be true. type ThinkingBlock struct { state protoimpl.MessageState `protogen:"open.v1"` - // The accumulated reasoning text (provider.md §4's thinking_delta + // The accumulated reasoning text (model.md §4's thinking_delta // StreamEvent variants, assembled into one block once the turn // completes). Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` // An opaque vendor integrity token, when the vendor's thinking blocks - // carry one (provider.md §4's thinking_signature StreamEvent variant). + // carry one (model.md §4's thinking_signature StreamEvent variant). // The kernel MUST store and round-trip this verbatim without // interpreting it — it is meaningful only to the vendor that issued it. Signature []byte `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"` diff --git a/pkg/context/proto/v1/context.pb.go b/pkg/context/proto/v1/context.pb.go index 585ad5f..6741e8b 100644 --- a/pkg/context/proto/v1/context.pb.go +++ b/pkg/context/proto/v1/context.pb.go @@ -96,7 +96,7 @@ func (Stability) EnumDescriptor() ([]byte, []int) { // ContextErrorCategory classifies a context provider's failures. // context.md §10 — smaller than the model-provider taxonomy -// (provider.md §8), but a plugin MUST still classify failures rather than +// (model.md §8), but a plugin MUST still classify failures rather than // collapsing them into one generic error. type ContextErrorCategory int32 diff --git a/pkg/kernel/proto/v1/kernel.pb.go b/pkg/kernel/proto/v1/kernel.pb.go index c28c5fc..7003a74 100644 --- a/pkg/kernel/proto/v1/kernel.pb.go +++ b/pkg/kernel/proto/v1/kernel.pb.go @@ -48,7 +48,7 @@ const ( // Zero value. Never valid on the wire; its presence means a caller // forgot to set the field. EventKind_EVENT_KIND_UNSPECIFIED EventKind = 0 - // A completed model turn's canonical message (provider.md §5). Usage + // A completed model turn's canonical message (model.md §5). Usage // and cost figures live inside this payload, not as separate fields or // a separate EventKind (state-backend.md §5). EventKind_EVENT_KIND_MESSAGE EventKind = 1 @@ -310,7 +310,7 @@ type CountTokensRequest struct { // content-type constraint context.md and memory.md already impose. Content []*v11.ContentBlock `protobuf:"bytes,1,rep,name=content,proto3" json:"content,omitempty"` // The model whose tokenizer should be preferred, if that model - // provider implements the optional CountTokens RPC (provider.md §2.1). + // provider implements the optional CountTokens RPC (model.md §2.1). // MAY be omitted, in which case the kernel's fallback heuristic // (kernel-callbacks.md §3) is used. ModelRef *v13.ModelRef `protobuf:"bytes,2,opt,name=model_ref,json=modelRef,proto3,oneof" json:"model_ref,omitempty"` diff --git a/pkg/memory/proto/v1/memory_grpc.pb.go b/pkg/memory/proto/v1/memory_grpc.pb.go index cd5d9b2..94a05bf 100644 --- a/pkg/memory/proto/v1/memory_grpc.pb.go +++ b/pkg/memory/proto/v1/memory_grpc.pb.go @@ -51,7 +51,7 @@ type MemoryServiceClient interface { GetCapabilities(ctx context.Context, in *GetCapabilitiesRequest, opts ...grpc.CallOption) (*GetCapabilitiesResponse, error) // Configure decodes this provider's agent.hcl config block, per the // same schema-to-cty bridge contract as the rest of this spec series - // (provider.md §3). Unary. memory.md §5. + // (model.md §3). Unary. memory.md §5. Configure(ctx context.Context, in *ConfigureRequest, opts ...grpc.CallOption) (*ConfigureResponse, error) // Recall is the read side: it fires at context-assemble time, competing // for the same token budget pool as context providers, and returns the @@ -192,7 +192,7 @@ type MemoryServiceServer interface { GetCapabilities(context.Context, *GetCapabilitiesRequest) (*GetCapabilitiesResponse, error) // Configure decodes this provider's agent.hcl config block, per the // same schema-to-cty bridge contract as the rest of this spec series - // (provider.md §3). Unary. memory.md §5. + // (model.md §3). Unary. memory.md §5. Configure(context.Context, *ConfigureRequest) (*ConfigureResponse, error) // Recall is the read side: it fires at context-assemble time, competing // for the same token budget pool as context providers, and returns the diff --git a/pkg/model/proto/v1/model.pb.go b/pkg/model/proto/v1/model.pb.go index 2a0279c..c1830e3 100644 --- a/pkg/model/proto/v1/model.pb.go +++ b/pkg/model/proto/v1/model.pb.go @@ -4,19 +4,29 @@ // protoc (unknown) // source: pluggableharness/agent/model/v1/model.proto -// Package pluggableharness.agent.model.v1 defines the two distinct model-identity shapes -// used across the specs. They are deliberately NOT unified into one -// message: ModelTarget is a rich "what am I generating context for" -// descriptor (context.md §4, memory.md §6); ModelRef is a narrow -// "which model's tokenizer" selector (kernel-callbacks.md §2). Merging -// them would force every CountTokens caller to populate fields it doesn't -// have and doesn't need. +// Package pluggableharness.agent.model.v1 defines the model (LLM vendor) provider +// plugin protocol described in specifications/model.md — see +// .claude/rules/proto.md — plus the two distinct model-identity shapes used +// across the other specs. The identity shapes are deliberately NOT unified +// into one message: ModelTarget is a rich "what am I generating context +// for" descriptor (context.md §4, memory.md §6); ModelRef is a narrow +// "which model's tokenizer" selector (kernel-callbacks.md §2). Merging them +// would force every CountTokens caller to populate fields it doesn't have +// and doesn't need. package modelv1 import ( + v11 "github.com/pluggableharness/agent/pkg/config/proto/v1" + v12 "github.com/pluggableharness/agent/pkg/content/proto/v1" + v14 "github.com/pluggableharness/agent/pkg/render/proto/v1" + v13 "github.com/pluggableharness/agent/pkg/schema/proto/v1" + v1 "github.com/pluggableharness/agent/pkg/slashcommand/proto/v1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" + structpb "google.golang.org/protobuf/types/known/structpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" unsafe "unsafe" @@ -29,44 +39,2042 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +// ThinkingMode enumerates the shapes of extended-reasoning control found +// across researched vendors (model.md §2) — a plain supports_thinking +// bool would lose information the kernel needs to build a correct +// request, since some vendors (e.g. Anthropic) expose more than one mode +// across their own model lineup. +type ThinkingMode int32 + +const ( + // Zero value. Never valid when ThinkingSpec.supported is true; its + // presence on the wire means a caller forgot to set the field. + ThinkingMode_THINKING_MODE_UNSPECIFIED ThinkingMode = 0 + // The model has no extended-reasoning capability. Pairs with + // ThinkingSpec.supported == false. + ThinkingMode_THINKING_MODE_NONE ThinkingMode = 1 + // The model always reasons, adaptively, with no caller-selectable + // effort level or budget. + ThinkingMode_THINKING_MODE_ALWAYS_ON_ADAPTIVE ThinkingMode = 2 + // The caller selects one of a fixed set of named effort levels + // (ThinkingSpec.effort_levels). + ThinkingMode_THINKING_MODE_DISCRETE_EFFORT ThinkingMode = 3 + // The caller selects a token budget within ThinkingSpec.budget_range. + ThinkingMode_THINKING_MODE_CONTINUOUS_BUDGET ThinkingMode = 4 +) + +// Enum value maps for ThinkingMode. +var ( + ThinkingMode_name = map[int32]string{ + 0: "THINKING_MODE_UNSPECIFIED", + 1: "THINKING_MODE_NONE", + 2: "THINKING_MODE_ALWAYS_ON_ADAPTIVE", + 3: "THINKING_MODE_DISCRETE_EFFORT", + 4: "THINKING_MODE_CONTINUOUS_BUDGET", + } + ThinkingMode_value = map[string]int32{ + "THINKING_MODE_UNSPECIFIED": 0, + "THINKING_MODE_NONE": 1, + "THINKING_MODE_ALWAYS_ON_ADAPTIVE": 2, + "THINKING_MODE_DISCRETE_EFFORT": 3, + "THINKING_MODE_CONTINUOUS_BUDGET": 4, + } +) + +func (x ThinkingMode) Enum() *ThinkingMode { + p := new(ThinkingMode) + *p = x + return p +} + +func (x ThinkingMode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ThinkingMode) Descriptor() protoreflect.EnumDescriptor { + return file_pluggableharness_agent_model_v1_model_proto_enumTypes[0].Descriptor() +} + +func (ThinkingMode) Type() protoreflect.EnumType { + return &file_pluggableharness_agent_model_v1_model_proto_enumTypes[0] +} + +func (x ThinkingMode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ThinkingMode.Descriptor instead. +func (ThinkingMode) EnumDescriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{0} +} + +// CachingMode enumerates the prompt-caching mechanics found across +// researched vendors (model.md §2). +type CachingMode int32 + +const ( + // Zero value. Never valid when CachingSpec.supported is true; its + // presence on the wire means a caller forgot to set the field. + CachingMode_CACHING_MODE_UNSPECIFIED CachingMode = 0 + // The model has no prompt-caching capability. Pairs with + // CachingSpec.supported == false. + CachingMode_CACHING_MODE_NONE CachingMode = 1 + // The caller must place cache breakpoints on content blocks explicitly + // (Anthropic/Mistral-style). + CachingMode_CACHING_MODE_EXPLICIT_MARKERS CachingMode = 2 + // The vendor applies caching transparently above a token threshold, no + // caller action required. + CachingMode_CACHING_MODE_IMPLICIT_AUTOMATIC CachingMode = 3 +) + +// Enum value maps for CachingMode. +var ( + CachingMode_name = map[int32]string{ + 0: "CACHING_MODE_UNSPECIFIED", + 1: "CACHING_MODE_NONE", + 2: "CACHING_MODE_EXPLICIT_MARKERS", + 3: "CACHING_MODE_IMPLICIT_AUTOMATIC", + } + CachingMode_value = map[string]int32{ + "CACHING_MODE_UNSPECIFIED": 0, + "CACHING_MODE_NONE": 1, + "CACHING_MODE_EXPLICIT_MARKERS": 2, + "CACHING_MODE_IMPLICIT_AUTOMATIC": 3, + } +) + +func (x CachingMode) Enum() *CachingMode { + p := new(CachingMode) + *p = x + return p +} + +func (x CachingMode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (CachingMode) Descriptor() protoreflect.EnumDescriptor { + return file_pluggableharness_agent_model_v1_model_proto_enumTypes[1].Descriptor() +} + +func (CachingMode) Type() protoreflect.EnumType { + return &file_pluggableharness_agent_model_v1_model_proto_enumTypes[1] +} + +func (x CachingMode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use CachingMode.Descriptor instead. +func (CachingMode) EnumDescriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{1} +} + +// StopReason classifies why a StreamCompletion ended, per model.md §4. +type StopReason int32 + +const ( + // Zero value. Never valid on a real Stop event; its presence on the + // wire means a caller forgot to set the field. + StopReason_STOP_REASON_UNSPECIFIED StopReason = 0 + // The model completed its turn normally. + StopReason_STOP_REASON_END_TURN StopReason = 1 + // The model stopped to request one or more tool invocations. + StopReason_STOP_REASON_TOOL_USE StopReason = 2 + // The model hit its output token limit before completing its turn. + StopReason_STOP_REASON_MAX_TOKENS StopReason = 3 + // The vendor's content filter stopped generation. + StopReason_STOP_REASON_CONTENT_FILTERED StopReason = 4 + // The kernel cancelled the stream (user interrupt, timeout, turn + // abort). MUST be treated by the plugin as normal control flow, never + // as an error (model.md §1, .claude/rules/grpc.md). + StopReason_STOP_REASON_CANCELLED StopReason = 5 +) + +// Enum value maps for StopReason. +var ( + StopReason_name = map[int32]string{ + 0: "STOP_REASON_UNSPECIFIED", + 1: "STOP_REASON_END_TURN", + 2: "STOP_REASON_TOOL_USE", + 3: "STOP_REASON_MAX_TOKENS", + 4: "STOP_REASON_CONTENT_FILTERED", + 5: "STOP_REASON_CANCELLED", + } + StopReason_value = map[string]int32{ + "STOP_REASON_UNSPECIFIED": 0, + "STOP_REASON_END_TURN": 1, + "STOP_REASON_TOOL_USE": 2, + "STOP_REASON_MAX_TOKENS": 3, + "STOP_REASON_CONTENT_FILTERED": 4, + "STOP_REASON_CANCELLED": 5, + } +) + +func (x StopReason) Enum() *StopReason { + p := new(StopReason) + *p = x + return p +} + +func (x StopReason) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (StopReason) Descriptor() protoreflect.EnumDescriptor { + return file_pluggableharness_agent_model_v1_model_proto_enumTypes[2].Descriptor() +} + +func (StopReason) Type() protoreflect.EnumType { + return &file_pluggableharness_agent_model_v1_model_proto_enumTypes[2] +} + +func (x StopReason) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use StopReason.Descriptor instead. +func (StopReason) EnumDescriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{2} +} + +// ModelErrorCategory classifies every StreamCompletion/Configure +// failure, per model.md §8. A plugin MUST classify every failure into +// exactly one of these categories and MUST NOT collapse them into a +// single generic error — the kernel's routing/fallback/retry behavior +// depends on telling these apart. +type ModelErrorCategory int32 + +const ( + // Zero value. Never valid on a real ModelError; its presence on the + // wire means a caller forgot to set the field. + ModelErrorCategory_MODEL_ERROR_CATEGORY_UNSPECIFIED ModelErrorCategory = 0 + // The request (or accumulated conversation) exceeds the model's context + // window. The kernel MUST NOT blindly retry as-is. + ModelErrorCategory_MODEL_ERROR_CATEGORY_CONTEXT_LENGTH_EXCEEDED ModelErrorCategory = 1 + // A vendor-side rate limit was hit. The kernel retries with backoff, + // honoring retry_after if supplied. + ModelErrorCategory_MODEL_ERROR_CATEGORY_RATE_LIMITED ModelErrorCategory = 2 + // Transient vendor unavailability (5xx-equivalent). The kernel retries + // with backoff; a candidate for capability-aware fallback. + ModelErrorCategory_MODEL_ERROR_CATEGORY_OVERLOADED ModelErrorCategory = 3 + // Bad, expired, or missing credentials. The kernel MUST NOT retry or + // silently fall back; this surfaces to a human. + ModelErrorCategory_MODEL_ERROR_CATEGORY_AUTH_ERROR ModelErrorCategory = 4 + // A malformed request — almost always a kernel/adapter bug. The kernel + // MUST NOT retry as-is. + ModelErrorCategory_MODEL_ERROR_CATEGORY_INVALID_REQUEST ModelErrorCategory = 5 + // The vendor refused or filtered the content. Surfaced distinctly from + // a generic failure so policy/UX can handle it differently. + ModelErrorCategory_MODEL_ERROR_CATEGORY_CONTENT_FILTERED ModelErrorCategory = 6 + // Anything else. MUST include raw_detail for debugging; treated as + // non-retryable by default. + ModelErrorCategory_MODEL_ERROR_CATEGORY_UNKNOWN ModelErrorCategory = 7 +) + +// Enum value maps for ModelErrorCategory. +var ( + ModelErrorCategory_name = map[int32]string{ + 0: "MODEL_ERROR_CATEGORY_UNSPECIFIED", + 1: "MODEL_ERROR_CATEGORY_CONTEXT_LENGTH_EXCEEDED", + 2: "MODEL_ERROR_CATEGORY_RATE_LIMITED", + 3: "MODEL_ERROR_CATEGORY_OVERLOADED", + 4: "MODEL_ERROR_CATEGORY_AUTH_ERROR", + 5: "MODEL_ERROR_CATEGORY_INVALID_REQUEST", + 6: "MODEL_ERROR_CATEGORY_CONTENT_FILTERED", + 7: "MODEL_ERROR_CATEGORY_UNKNOWN", + } + ModelErrorCategory_value = map[string]int32{ + "MODEL_ERROR_CATEGORY_UNSPECIFIED": 0, + "MODEL_ERROR_CATEGORY_CONTEXT_LENGTH_EXCEEDED": 1, + "MODEL_ERROR_CATEGORY_RATE_LIMITED": 2, + "MODEL_ERROR_CATEGORY_OVERLOADED": 3, + "MODEL_ERROR_CATEGORY_AUTH_ERROR": 4, + "MODEL_ERROR_CATEGORY_INVALID_REQUEST": 5, + "MODEL_ERROR_CATEGORY_CONTENT_FILTERED": 6, + "MODEL_ERROR_CATEGORY_UNKNOWN": 7, + } +) + +func (x ModelErrorCategory) Enum() *ModelErrorCategory { + p := new(ModelErrorCategory) + *p = x + return p +} + +func (x ModelErrorCategory) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ModelErrorCategory) Descriptor() protoreflect.EnumDescriptor { + return file_pluggableharness_agent_model_v1_model_proto_enumTypes[3].Descriptor() +} + +func (ModelErrorCategory) Type() protoreflect.EnumType { + return &file_pluggableharness_agent_model_v1_model_proto_enumTypes[3] +} + +func (x ModelErrorCategory) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ModelErrorCategory.Descriptor instead. +func (ModelErrorCategory) EnumDescriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{3} +} + +// GetCapabilitiesRequest is empty: model.md §2 defines GetCapabilities +// as taking no request parameters. +type GetCapabilitiesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetCapabilitiesRequest) Reset() { + *x = GetCapabilitiesRequest{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetCapabilitiesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetCapabilitiesRequest) ProtoMessage() {} + +func (x *GetCapabilitiesRequest) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetCapabilitiesRequest.ProtoReflect.Descriptor instead. +func (*GetCapabilitiesRequest) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{0} +} + +// GetCapabilitiesResponse wraps Capabilities for the RPC signature, per +// this repo's per-RPC envelope convention (.claude/rules/proto.md). +type GetCapabilitiesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Capabilities *Capabilities `protobuf:"bytes,1,opt,name=capabilities,proto3" json:"capabilities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetCapabilitiesResponse) Reset() { + *x = GetCapabilitiesResponse{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetCapabilitiesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetCapabilitiesResponse) ProtoMessage() {} + +func (x *GetCapabilitiesResponse) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetCapabilitiesResponse.ProtoReflect.Descriptor instead. +func (*GetCapabilitiesResponse) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{1} +} + +func (x *GetCapabilitiesResponse) GetCapabilities() *Capabilities { + if x != nil { + return x.Capabilities + } + return nil +} + +// Capabilities is GetCapabilities' response payload: every model this +// plugin can serve, plus provider-wide declarations that apply once, not +// per model. +type Capabilities struct { + state protoimpl.MessageState `protogen:"open.v1"` + // One ModelSpec per model the plugin can serve. MUST have at least one + // entry. + Models []*ModelSpec `protobuf:"bytes,1,rep,name=models,proto3" json:"models,omitempty"` + // Slash commands this provider contributes, declared once for the + // provider as a whole (not per model), per model.md §2 and + // configuration.md §5 / frontend.md §5. MAY be empty. + SlashCommands []*v1.SlashCommandSpec `protobuf:"bytes,2,rep,name=slash_commands,json=slashCommands,proto3" json:"slash_commands,omitempty"` + // The provider's agent.hcl config schema, returned alongside + // capabilities so the kernel knows what fields Configure expects, per + // configuration.md §4. + ConfigSchema *v11.ConfigSchema `protobuf:"bytes,3,opt,name=config_schema,json=configSchema,proto3" json:"config_schema,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Capabilities) Reset() { + *x = Capabilities{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Capabilities) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Capabilities) ProtoMessage() {} + +func (x *Capabilities) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Capabilities.ProtoReflect.Descriptor instead. +func (*Capabilities) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{2} +} + +func (x *Capabilities) GetModels() []*ModelSpec { + if x != nil { + return x.Models + } + return nil +} + +func (x *Capabilities) GetSlashCommands() []*v1.SlashCommandSpec { + if x != nil { + return x.SlashCommands + } + return nil +} + +func (x *Capabilities) GetConfigSchema() *v11.ConfigSchema { + if x != nil { + return x.ConfigSchema + } + return nil +} + +// ConfigureRequest wraps the provider's agent.hcl config block, already +// decoded from HCL/cty into a Struct by the kernel's schema-to-cty bridge, +// per model.md §3. +type ConfigureRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The decoded config value. Field contents are provider-specific (API + // key, base URL override, org/project IDs, etc.) — model.md §3 + // doesn't mandate a shape beyond what ConfigSchema (Capabilities. + // config_schema) declares. A Struct because the shape is genuinely + // provider-defined, not fixed at the proto level (see + // .claude/rules/proto.md's Struct carve-out). + Config *structpb.Struct `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConfigureRequest) Reset() { + *x = ConfigureRequest{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConfigureRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConfigureRequest) ProtoMessage() {} + +func (x *ConfigureRequest) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConfigureRequest.ProtoReflect.Descriptor instead. +func (*ConfigureRequest) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{3} +} + +func (x *ConfigureRequest) GetConfig() *structpb.Struct { + if x != nil { + return x.Config + } + return nil +} + +// ConfigureResponse is empty on success. A Configure failure (e.g. a +// missing required field) surfaces as a gRPC status carrying a +// ModelError in its structured detail, per .claude/rules/grpc.md's +// error-taxonomy convention — there is no in-band error field on this +// message. +type ConfigureResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConfigureResponse) Reset() { + *x = ConfigureResponse{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConfigureResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConfigureResponse) ProtoMessage() {} + +func (x *ConfigureResponse) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConfigureResponse.ProtoReflect.Descriptor instead. +func (*ConfigureResponse) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{4} +} + +// ModelSpec describes one model this provider can serve, per +// model.md §2. Every field below is MUST unless its comment says +// otherwise. +type ModelSpec struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The vendor's exact model identifier, used to select this model in + // StreamCompletionRequest.model_id. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // The model's input token budget. + ContextWindow int64 `protobuf:"varint,2,opt,name=context_window,json=contextWindow,proto3" json:"context_window,omitempty"` + // The model's maximum output tokens per response. + MaxOutputTokens int64 `protobuf:"varint,3,opt,name=max_output_tokens,json=maxOutputTokens,proto3" json:"max_output_tokens,omitempty"` + // Whether this model can accept tool declarations and emit tool_use + // content blocks. + SupportsToolUse bool `protobuf:"varint,4,opt,name=supports_tool_use,json=supportsToolUse,proto3" json:"supports_tool_use,omitempty"` + // Whether this model can accept image content blocks. + SupportsVision bool `protobuf:"varint,5,opt,name=supports_vision,json=supportsVision,proto3" json:"supports_vision,omitempty"` + // Whether the vendor's own backend streams responses. A UX hint only + // (e.g. "don't render a live-typing cursor" when false) — the + // StreamCompletion RPC shape is always server-streaming regardless of + // this value, per model.md §1/§4. + SupportsStreaming bool `protobuf:"varint,6,opt,name=supports_streaming,json=supportsStreaming,proto3" json:"supports_streaming,omitempty"` + // Whether this model can return multiple tool_use blocks in a single + // turn. SHOULD be set accurately; a false or absent value means the + // kernel MUST serialize tool calls for this model. + SupportsParallelToolCalls *bool `protobuf:"varint,7,opt,name=supports_parallel_tool_calls,json=supportsParallelToolCalls,proto3,oneof" json:"supports_parallel_tool_calls,omitempty"` + // This model's extended-reasoning capability. MUST be present even when + // unsupported — use { supported: false } rather than omitting the + // message, so a caller never has to distinguish "unset" from "no + // thinking mode". + Thinking *ThinkingSpec `protobuf:"bytes,8,opt,name=thinking,proto3" json:"thinking,omitempty"` + // This model's prompt-caching capability. MUST be present even when + // unsupported — use { supported: false } rather than omitting the + // message. + Caching *CachingSpec `protobuf:"bytes,9,opt,name=caching,proto3" json:"caching,omitempty"` + // This model's pricing. MUST be present even for a free model (set + // Pricing.free = true). + Pricing *Pricing `protobuf:"bytes,10,opt,name=pricing,proto3" json:"pricing,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModelSpec) Reset() { + *x = ModelSpec{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModelSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModelSpec) ProtoMessage() {} + +func (x *ModelSpec) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ModelSpec.ProtoReflect.Descriptor instead. +func (*ModelSpec) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{5} +} + +func (x *ModelSpec) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *ModelSpec) GetContextWindow() int64 { + if x != nil { + return x.ContextWindow + } + return 0 +} + +func (x *ModelSpec) GetMaxOutputTokens() int64 { + if x != nil { + return x.MaxOutputTokens + } + return 0 +} + +func (x *ModelSpec) GetSupportsToolUse() bool { + if x != nil { + return x.SupportsToolUse + } + return false +} + +func (x *ModelSpec) GetSupportsVision() bool { + if x != nil { + return x.SupportsVision + } + return false +} + +func (x *ModelSpec) GetSupportsStreaming() bool { + if x != nil { + return x.SupportsStreaming + } + return false +} + +func (x *ModelSpec) GetSupportsParallelToolCalls() bool { + if x != nil && x.SupportsParallelToolCalls != nil { + return *x.SupportsParallelToolCalls + } + return false +} + +func (x *ModelSpec) GetThinking() *ThinkingSpec { + if x != nil { + return x.Thinking + } + return nil +} + +func (x *ModelSpec) GetCaching() *CachingSpec { + if x != nil { + return x.Caching + } + return nil +} + +func (x *ModelSpec) GetPricing() *Pricing { + if x != nil { + return x.Pricing + } + return nil +} + +// ThinkingBudgetRange bounds the token budget a caller may request when +// ThinkingMode is THINKING_MODE_CONTINUOUS_BUDGET. +type ThinkingBudgetRange struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The smallest thinking-token budget this model accepts. + Min int64 `protobuf:"varint,1,opt,name=min,proto3" json:"min,omitempty"` + // The largest thinking-token budget this model accepts. + Max int64 `protobuf:"varint,2,opt,name=max,proto3" json:"max,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ThinkingBudgetRange) Reset() { + *x = ThinkingBudgetRange{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ThinkingBudgetRange) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ThinkingBudgetRange) ProtoMessage() {} + +func (x *ThinkingBudgetRange) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ThinkingBudgetRange.ProtoReflect.Descriptor instead. +func (*ThinkingBudgetRange) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{6} +} + +func (x *ThinkingBudgetRange) GetMin() int64 { + if x != nil { + return x.Min + } + return 0 +} + +func (x *ThinkingBudgetRange) GetMax() int64 { + if x != nil { + return x.Max + } + return 0 +} + +// ThinkingSpec describes one model's extended-reasoning capability, per +// model.md §2. +type ThinkingSpec struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Whether this model has any extended-reasoning capability at all. + Supported bool `protobuf:"varint,1,opt,name=supported,proto3" json:"supported,omitempty"` + // Which reasoning-control shape this model uses. MUST be + // THINKING_MODE_NONE when supported == false. + Mode ThinkingMode `protobuf:"varint,2,opt,name=mode,proto3,enum=pluggableharness.agent.model.v1.ThinkingMode" json:"mode,omitempty"` + // The selectable effort levels, e.g. ["low","medium","high","xhigh", + // "max"]. MUST be non-empty when mode == THINKING_MODE_DISCRETE_EFFORT; + // meaningless otherwise. + EffortLevels []string `protobuf:"bytes,3,rep,name=effort_levels,json=effortLevels,proto3" json:"effort_levels,omitempty"` + // The selectable token-budget range. MUST be present when mode == + // THINKING_MODE_CONTINUOUS_BUDGET; meaningless otherwise. + BudgetRange *ThinkingBudgetRange `protobuf:"bytes,4,opt,name=budget_range,json=budgetRange,proto3,oneof" json:"budget_range,omitempty"` + // Whether reasoning can be turned off once enabled. MUST be set + // accurately — some vendors' reasoning cannot be disabled (e.g. a + // researched Grok model defaults reasoning on with no off switch). + CanDisable bool `protobuf:"varint,5,opt,name=can_disable,json=canDisable,proto3" json:"can_disable,omitempty"` + // The effort level (discrete_effort) or budget-token value + // (continuous_budget), as a string, the vendor applies when a request + // omits thinking config entirely. MUST be set when mode != + // THINKING_MODE_NONE — makes the vendor's actual default behavior + // visible/auditable via GetCapabilities rather than hidden in adapter + // code, so a kernel wanting deterministic behavior can always send an + // explicit override. + Default *string `protobuf:"bytes,6,opt,name=default,proto3,oneof" json:"default,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ThinkingSpec) Reset() { + *x = ThinkingSpec{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ThinkingSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ThinkingSpec) ProtoMessage() {} + +func (x *ThinkingSpec) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ThinkingSpec.ProtoReflect.Descriptor instead. +func (*ThinkingSpec) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{7} +} + +func (x *ThinkingSpec) GetSupported() bool { + if x != nil { + return x.Supported + } + return false +} + +func (x *ThinkingSpec) GetMode() ThinkingMode { + if x != nil { + return x.Mode + } + return ThinkingMode_THINKING_MODE_UNSPECIFIED +} + +func (x *ThinkingSpec) GetEffortLevels() []string { + if x != nil { + return x.EffortLevels + } + return nil +} + +func (x *ThinkingSpec) GetBudgetRange() *ThinkingBudgetRange { + if x != nil { + return x.BudgetRange + } + return nil +} + +func (x *ThinkingSpec) GetCanDisable() bool { + if x != nil { + return x.CanDisable + } + return false +} + +func (x *ThinkingSpec) GetDefault() string { + if x != nil && x.Default != nil { + return *x.Default + } + return "" +} + +// CachingSpec describes one model's prompt-caching capability, per +// model.md §2. +type CachingSpec struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Whether this model has any prompt-caching capability at all. + Supported bool `protobuf:"varint,1,opt,name=supported,proto3" json:"supported,omitempty"` + // Which caching mechanic this model uses. MUST be CACHING_MODE_NONE + // when supported == false. + Mode CachingMode `protobuf:"varint,2,opt,name=mode,proto3,enum=pluggableharness.agent.model.v1.CachingMode" json:"mode,omitempty"` + // Whether this provider runs its own cache-keepalive loop (e.g. a + // background goroutine re-pinging before a cache TTL expires, so a long + // tool-execution gap doesn't let the cache go cold). MUST be set, + // default false. Cache TTL mechanics are vendor-specific, so per + // operator decision this is a provider-owned behavior the kernel never + // drives — this field only tells the kernel/operator whether a given + // provider implements the optimization (model.md §2). + KeepaliveSupported bool `protobuf:"varint,3,opt,name=keepalive_supported,json=keepaliveSupported,proto3" json:"keepalive_supported,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CachingSpec) Reset() { + *x = CachingSpec{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CachingSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CachingSpec) ProtoMessage() {} + +func (x *CachingSpec) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CachingSpec.ProtoReflect.Descriptor instead. +func (*CachingSpec) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{8} +} + +func (x *CachingSpec) GetSupported() bool { + if x != nil { + return x.Supported + } + return false +} + +func (x *CachingSpec) GetMode() CachingMode { + if x != nil { + return x.Mode + } + return CachingMode_CACHING_MODE_UNSPECIFIED +} + +func (x *CachingSpec) GetKeepaliveSupported() bool { + if x != nil { + return x.KeepaliveSupported + } + return false +} + +// PricingTier is one time-bounded rate within a model's Pricing, per +// model.md §2. Exactly one tier MUST match at any given timestamp +// (effective_from <= ts < effective_until, an omitted bound unbounded on +// that side); the kernel MUST reject a Pricing value at capability-load +// time if its tiers overlap or leave a gap. +type PricingTier struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The moment this tier becomes active. Omitted means "since this plugin + // version was published". Refines model.md §2's "ISO 8601 + // date/timestamp" into the native well-known type. + EffectiveFrom *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=effective_from,json=effectiveFrom,proto3,oneof" json:"effective_from,omitempty"` + // The moment this tier stops being active. Omitted means "still + // current" — an omitted effective_until marks the currently active + // tier. Refines model.md §2's "ISO 8601" into the native well-known + // type. + EffectiveUntil *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=effective_until,json=effectiveUntil,proto3,oneof" json:"effective_until,omitempty"` + // Cost per million input tokens, realtime rate. + InputPerMtok float64 `protobuf:"fixed64,3,opt,name=input_per_mtok,json=inputPerMtok,proto3" json:"input_per_mtok,omitempty"` + // Cost per million output tokens, realtime rate. + OutputPerMtok float64 `protobuf:"fixed64,4,opt,name=output_per_mtok,json=outputPerMtok,proto3" json:"output_per_mtok,omitempty"` + // Cost per million cache-write tokens. MUST be present iff + // CachingSpec.supported. + CacheWritePerMtok *float64 `protobuf:"fixed64,5,opt,name=cache_write_per_mtok,json=cacheWritePerMtok,proto3,oneof" json:"cache_write_per_mtok,omitempty"` + // Cost per million cache-read tokens, typically far cheaper than + // input_per_mtok — the entire point of caching. MUST be present iff + // CachingSpec.supported. + CacheReadPerMtok *float64 `protobuf:"fixed64,6,opt,name=cache_read_per_mtok,json=cacheReadPerMtok,proto3,oneof" json:"cache_read_per_mtok,omitempty"` + // A vendor's discounted batch/async input rate, where one exists (e.g. + // a researched Gemini batch tier). MAY be present. + BatchInputPerMtok *float64 `protobuf:"fixed64,7,opt,name=batch_input_per_mtok,json=batchInputPerMtok,proto3,oneof" json:"batch_input_per_mtok,omitempty"` + // A vendor's discounted batch/async output rate, paired with + // batch_input_per_mtok. MAY be present. + BatchOutputPerMtok *float64 `protobuf:"fixed64,8,opt,name=batch_output_per_mtok,json=batchOutputPerMtok,proto3,oneof" json:"batch_output_per_mtok,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PricingTier) Reset() { + *x = PricingTier{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PricingTier) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PricingTier) ProtoMessage() {} + +func (x *PricingTier) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PricingTier.ProtoReflect.Descriptor instead. +func (*PricingTier) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{9} +} + +func (x *PricingTier) GetEffectiveFrom() *timestamppb.Timestamp { + if x != nil { + return x.EffectiveFrom + } + return nil +} + +func (x *PricingTier) GetEffectiveUntil() *timestamppb.Timestamp { + if x != nil { + return x.EffectiveUntil + } + return nil +} + +func (x *PricingTier) GetInputPerMtok() float64 { + if x != nil { + return x.InputPerMtok + } + return 0 +} + +func (x *PricingTier) GetOutputPerMtok() float64 { + if x != nil { + return x.OutputPerMtok + } + return 0 +} + +func (x *PricingTier) GetCacheWritePerMtok() float64 { + if x != nil && x.CacheWritePerMtok != nil { + return *x.CacheWritePerMtok + } + return 0 +} + +func (x *PricingTier) GetCacheReadPerMtok() float64 { + if x != nil && x.CacheReadPerMtok != nil { + return *x.CacheReadPerMtok + } + return 0 +} + +func (x *PricingTier) GetBatchInputPerMtok() float64 { + if x != nil && x.BatchInputPerMtok != nil { + return *x.BatchInputPerMtok + } + return 0 +} + +func (x *PricingTier) GetBatchOutputPerMtok() float64 { + if x != nil && x.BatchOutputPerMtok != nil { + return *x.BatchOutputPerMtok + } + return 0 +} + +// Pricing describes one model's cost structure, per model.md §2. MUST +// be present on every ModelSpec, even a free one. +type Pricing struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The pricing currency. MUST be "USD" for v1; reserved for future + // multi-currency support, not acted on by the kernel yet. + Currency string `protobuf:"bytes,1,opt,name=currency,proto3" json:"currency,omitempty"` + // True for a local/free-to-run model (e.g. an Ollama-served model). + // When true, tiers MAY be omitted entirely. + Free bool `protobuf:"varint,2,opt,name=free,proto3" json:"free,omitempty"` + // This model's rate tiers, ordered or not — resolution is by + // effective_from/effective_until, not array position. MUST have at + // least one entry unless free == true. Exactly one tier MUST match any + // given timestamp; the kernel MUST reject overlapping or gapped tiers + // at capability-load time. + Tiers []*PricingTier `protobuf:"bytes,3,rep,name=tiers,proto3" json:"tiers,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Pricing) Reset() { + *x = Pricing{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Pricing) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Pricing) ProtoMessage() {} + +func (x *Pricing) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Pricing.ProtoReflect.Descriptor instead. +func (*Pricing) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{10} +} + +func (x *Pricing) GetCurrency() string { + if x != nil { + return x.Currency + } + return "" +} + +func (x *Pricing) GetFree() bool { + if x != nil { + return x.Free + } + return false +} + +func (x *Pricing) GetTiers() []*PricingTier { + if x != nil { + return x.Tiers + } + return nil +} + +// StreamCompletionRequest is StreamCompletion's request: the full +// canonical conversation, available tools, and generation params for one +// completion, per model.md §4. +type StreamCompletionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The canonical conversation history, in emission order (model.md + // §5). + Messages []*v12.Message `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"` + // Selects which of this provider's ModelSpec.id to use. + ModelId string `protobuf:"bytes,2,opt,name=model_id,json=modelId,proto3" json:"model_id,omitempty"` + // The tools available to the model on this turn, described in the + // shared JSON-Schema subset (model.md §6). MAY be empty. + Tools []*ToolDeclaration `protobuf:"bytes,3,rep,name=tools,proto3" json:"tools,omitempty"` + // Generation-time overrides. Omitted means every param takes its + // model-specific default. + Params *GenerationParams `protobuf:"bytes,4,opt,name=params,proto3,oneof" json:"params,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StreamCompletionRequest) Reset() { + *x = StreamCompletionRequest{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StreamCompletionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StreamCompletionRequest) ProtoMessage() {} + +func (x *StreamCompletionRequest) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StreamCompletionRequest.ProtoReflect.Descriptor instead. +func (*StreamCompletionRequest) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{11} +} + +func (x *StreamCompletionRequest) GetMessages() []*v12.Message { + if x != nil { + return x.Messages + } + return nil +} + +func (x *StreamCompletionRequest) GetModelId() string { + if x != nil { + return x.ModelId + } + return "" +} + +func (x *StreamCompletionRequest) GetTools() []*ToolDeclaration { + if x != nil { + return x.Tools + } + return nil +} + +func (x *StreamCompletionRequest) GetParams() *GenerationParams { + if x != nil { + return x.Params + } + return nil +} + +// ToolDeclaration is one tool the model may call on this turn, per +// model.md §6. Each model-provider adapter translates this into its +// vendor's own tool-definition wire format. +type ToolDeclaration struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The tool's name, as the model must reference it in a ToolUseBlock. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Human-readable description shown to the model to help it decide + // whether and how to call this tool. + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + // The tool's input shape, in the restricted JSON-Schema subset shared + // across categories (model.md §6, pluggableharness.agent.schema.v1.Schema). + InputSchema *v13.Schema `protobuf:"bytes,3,opt,name=input_schema,json=inputSchema,proto3" json:"input_schema,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ToolDeclaration) Reset() { + *x = ToolDeclaration{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ToolDeclaration) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ToolDeclaration) ProtoMessage() {} + +func (x *ToolDeclaration) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ToolDeclaration.ProtoReflect.Descriptor instead. +func (*ToolDeclaration) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{12} +} + +func (x *ToolDeclaration) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ToolDeclaration) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *ToolDeclaration) GetInputSchema() *v13.Schema { + if x != nil { + return x.InputSchema + } + return nil +} + +// GenerationParams carries per-request overrides of otherwise +// model-default generation behavior. +type GenerationParams struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Selects one of ThinkingSpec.effort_levels. Meaningful only when the + // target model's ThinkingSpec.mode == THINKING_MODE_DISCRETE_EFFORT. + ThinkingEffort *string `protobuf:"bytes,1,opt,name=thinking_effort,json=thinkingEffort,proto3,oneof" json:"thinking_effort,omitempty"` + // Selects a token budget within ThinkingSpec.budget_range. Meaningful + // only when the target model's ThinkingSpec.mode == + // THINKING_MODE_CONTINUOUS_BUDGET. + ThinkingBudgetTokens *int64 `protobuf:"varint,2,opt,name=thinking_budget_tokens,json=thinkingBudgetTokens,proto3,oneof" json:"thinking_budget_tokens,omitempty"` + // Per-request override of ModelSpec.max_output_tokens. Omitted means + // use the model's default. + MaxOutputTokens *int64 `protobuf:"varint,3,opt,name=max_output_tokens,json=maxOutputTokens,proto3,oneof" json:"max_output_tokens,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GenerationParams) Reset() { + *x = GenerationParams{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GenerationParams) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GenerationParams) ProtoMessage() {} + +func (x *GenerationParams) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GenerationParams.ProtoReflect.Descriptor instead. +func (*GenerationParams) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{13} +} + +func (x *GenerationParams) GetThinkingEffort() string { + if x != nil && x.ThinkingEffort != nil { + return *x.ThinkingEffort + } + return "" +} + +func (x *GenerationParams) GetThinkingBudgetTokens() int64 { + if x != nil && x.ThinkingBudgetTokens != nil { + return *x.ThinkingBudgetTokens + } + return 0 +} + +func (x *GenerationParams) GetMaxOutputTokens() int64 { + if x != nil && x.MaxOutputTokens != nil { + return *x.MaxOutputTokens + } + return 0 +} + +// StreamEvent is one message in the stream StreamCompletion returns, per +// model.md §4. Exactly one variant is set. +type StreamEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Event: + // + // *StreamEvent_TextDelta_ + // *StreamEvent_ThinkingDelta_ + // *StreamEvent_ThinkingSignature_ + // *StreamEvent_ToolCallStart_ + // *StreamEvent_ToolCallDelta_ + // *StreamEvent_ToolCallDone_ + // *StreamEvent_Usage + // *StreamEvent_Stop_ + // *StreamEvent_Error_ + Event isStreamEvent_Event `protobuf_oneof:"event"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StreamEvent) Reset() { + *x = StreamEvent{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StreamEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StreamEvent) ProtoMessage() {} + +func (x *StreamEvent) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StreamEvent.ProtoReflect.Descriptor instead. +func (*StreamEvent) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{14} +} + +func (x *StreamEvent) GetEvent() isStreamEvent_Event { + if x != nil { + return x.Event + } + return nil +} + +func (x *StreamEvent) GetTextDelta() *StreamEvent_TextDelta { + if x != nil { + if x, ok := x.Event.(*StreamEvent_TextDelta_); ok { + return x.TextDelta + } + } + return nil +} + +func (x *StreamEvent) GetThinkingDelta() *StreamEvent_ThinkingDelta { + if x != nil { + if x, ok := x.Event.(*StreamEvent_ThinkingDelta_); ok { + return x.ThinkingDelta + } + } + return nil +} + +func (x *StreamEvent) GetThinkingSignature() *StreamEvent_ThinkingSignature { + if x != nil { + if x, ok := x.Event.(*StreamEvent_ThinkingSignature_); ok { + return x.ThinkingSignature + } + } + return nil +} + +func (x *StreamEvent) GetToolCallStart() *StreamEvent_ToolCallStart { + if x != nil { + if x, ok := x.Event.(*StreamEvent_ToolCallStart_); ok { + return x.ToolCallStart + } + } + return nil +} + +func (x *StreamEvent) GetToolCallDelta() *StreamEvent_ToolCallDelta { + if x != nil { + if x, ok := x.Event.(*StreamEvent_ToolCallDelta_); ok { + return x.ToolCallDelta + } + } + return nil +} + +func (x *StreamEvent) GetToolCallDone() *StreamEvent_ToolCallDone { + if x != nil { + if x, ok := x.Event.(*StreamEvent_ToolCallDone_); ok { + return x.ToolCallDone + } + } + return nil +} + +func (x *StreamEvent) GetUsage() *Usage { + if x != nil { + if x, ok := x.Event.(*StreamEvent_Usage); ok { + return x.Usage + } + } + return nil +} + +func (x *StreamEvent) GetStop() *StreamEvent_Stop { + if x != nil { + if x, ok := x.Event.(*StreamEvent_Stop_); ok { + return x.Stop + } + } + return nil +} + +func (x *StreamEvent) GetError() *StreamEvent_Error { + if x != nil { + if x, ok := x.Event.(*StreamEvent_Error_); ok { + return x.Error + } + } + return nil +} + +type isStreamEvent_Event interface { + isStreamEvent_Event() +} + +type StreamEvent_TextDelta_ struct { + // An incremental fragment of assistant text output. + TextDelta *StreamEvent_TextDelta `protobuf:"bytes,1,opt,name=text_delta,json=textDelta,proto3,oneof"` +} + +type StreamEvent_ThinkingDelta_ struct { + // An incremental fragment of the model's reasoning output. + ThinkingDelta *StreamEvent_ThinkingDelta `protobuf:"bytes,2,opt,name=thinking_delta,json=thinkingDelta,proto3,oneof"` +} + +type StreamEvent_ThinkingSignature_ struct { + // The vendor's opaque integrity token for the reasoning just emitted. + ThinkingSignature *StreamEvent_ThinkingSignature `protobuf:"bytes,3,opt,name=thinking_signature,json=thinkingSignature,proto3,oneof"` +} + +type StreamEvent_ToolCallStart_ struct { + // The model has begun requesting a tool invocation. + ToolCallStart *StreamEvent_ToolCallStart `protobuf:"bytes,4,opt,name=tool_call_start,json=toolCallStart,proto3,oneof"` +} + +type StreamEvent_ToolCallDelta_ struct { + // An incremental fragment of a tool call's arguments. + ToolCallDelta *StreamEvent_ToolCallDelta `protobuf:"bytes,5,opt,name=tool_call_delta,json=toolCallDelta,proto3,oneof"` +} + +type StreamEvent_ToolCallDone_ struct { + // A tool call's arguments are complete. + ToolCallDone *StreamEvent_ToolCallDone `protobuf:"bytes,6,opt,name=tool_call_done,json=toolCallDone,proto3,oneof"` +} + +type StreamEvent_Usage struct { + // Token accounting for this completion. + Usage *Usage `protobuf:"bytes,7,opt,name=usage,proto3,oneof"` +} + +type StreamEvent_Stop_ struct { + // The completion has ended. + Stop *StreamEvent_Stop `protobuf:"bytes,8,opt,name=stop,proto3,oneof"` +} + +type StreamEvent_Error_ struct { + // The completion failed. + Error *StreamEvent_Error `protobuf:"bytes,9,opt,name=error,proto3,oneof"` +} + +func (*StreamEvent_TextDelta_) isStreamEvent_Event() {} + +func (*StreamEvent_ThinkingDelta_) isStreamEvent_Event() {} + +func (*StreamEvent_ThinkingSignature_) isStreamEvent_Event() {} + +func (*StreamEvent_ToolCallStart_) isStreamEvent_Event() {} + +func (*StreamEvent_ToolCallDelta_) isStreamEvent_Event() {} + +func (*StreamEvent_ToolCallDone_) isStreamEvent_Event() {} + +func (*StreamEvent_Usage) isStreamEvent_Event() {} + +func (*StreamEvent_Stop_) isStreamEvent_Event() {} + +func (*StreamEvent_Error_) isStreamEvent_Event() {} + +// Usage carries token accounting for one completion, per model.md §4.1. +// The kernel computes and persists cost_usd from these counts plus the +// matching PricingTier — the plugin never computes cost itself. Promoted +// to a top-level message, rather than nested under StreamEvent, because +// it is reused outside the stream (event payloads, frontend usage +// updates — forthcoming). +type Usage struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Input tokens consumed by this completion. + InputTokens int64 `protobuf:"varint,1,opt,name=input_tokens,json=inputTokens,proto3" json:"input_tokens,omitempty"` + // Output tokens produced by this completion. + OutputTokens int64 `protobuf:"varint,2,opt,name=output_tokens,json=outputTokens,proto3" json:"output_tokens,omitempty"` + // Tokens read from cache, if the model supports caching. Never also + // counted in input_tokens. + CacheReadTokens *int64 `protobuf:"varint,3,opt,name=cache_read_tokens,json=cacheReadTokens,proto3,oneof" json:"cache_read_tokens,omitempty"` + // Tokens written to cache, if the model supports caching. Never also + // counted in input_tokens. + CacheWriteTokens *int64 `protobuf:"varint,4,opt,name=cache_write_tokens,json=cacheWriteTokens,proto3,oneof" json:"cache_write_tokens,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Usage) Reset() { + *x = Usage{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Usage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Usage) ProtoMessage() {} + +func (x *Usage) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Usage.ProtoReflect.Descriptor instead. +func (*Usage) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{15} +} + +func (x *Usage) GetInputTokens() int64 { + if x != nil { + return x.InputTokens + } + return 0 +} + +func (x *Usage) GetOutputTokens() int64 { + if x != nil { + return x.OutputTokens + } + return 0 +} + +func (x *Usage) GetCacheReadTokens() int64 { + if x != nil && x.CacheReadTokens != nil { + return *x.CacheReadTokens + } + return 0 +} + +func (x *Usage) GetCacheWriteTokens() int64 { + if x != nil && x.CacheWriteTokens != nil { + return *x.CacheWriteTokens + } + return 0 +} + +// CountTokensRequest is CountTokens' request: the raw text to count, per +// model.md §2.1. +type CountTokensRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The text to count tokens for. + Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CountTokensRequest) Reset() { + *x = CountTokensRequest{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CountTokensRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CountTokensRequest) ProtoMessage() {} + +func (x *CountTokensRequest) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CountTokensRequest.ProtoReflect.Descriptor instead. +func (*CountTokensRequest) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{16} +} + +func (x *CountTokensRequest) GetText() string { + if x != nil { + return x.Text + } + return "" +} + +// CountTokensResponse is CountTokens' response. +type CountTokensResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The exact token count, per this model's real vendor tokenizer. + Count int64 `protobuf:"varint,1,opt,name=count,proto3" json:"count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CountTokensResponse) Reset() { + *x = CountTokensResponse{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CountTokensResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CountTokensResponse) ProtoMessage() {} + +func (x *CountTokensResponse) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CountTokensResponse.ProtoReflect.Descriptor instead. +func (*CountTokensResponse) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{17} +} + +func (x *CountTokensResponse) GetCount() int64 { + if x != nil { + return x.Count + } + return 0 +} + +// RenderRequest carries the opaque payload to render, per model.md §7. +type RenderRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The opaque emitted payload to render — the Emit->Render->Paint + // pipeline's deliberate carve-out from the strong-typing rule (see + // .claude/rules/grpc.md), never interpreted by the kernel. + Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RenderRequest) Reset() { + *x = RenderRequest{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RenderRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RenderRequest) ProtoMessage() {} + +func (x *RenderRequest) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RenderRequest.ProtoReflect.Descriptor instead. +func (*RenderRequest) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{18} +} + +func (x *RenderRequest) GetPayload() []byte { + if x != nil { + return x.Payload + } + return nil +} + +// RenderResponse wraps the resulting RenderTree, per model.md §7. +type RenderResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The rendered tree, formally defined in frontend.md §1 and shared + // verbatim across every category's Render RPC (tool.md §7, context.md + // §9, memory.md §10) — one RenderTree type for the whole + // Emit->Render->Paint pipeline, not a per-category variant. + Tree *v14.RenderTree `protobuf:"bytes,1,opt,name=tree,proto3" json:"tree,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RenderResponse) Reset() { + *x = RenderResponse{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RenderResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RenderResponse) ProtoMessage() {} + +func (x *RenderResponse) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RenderResponse.ProtoReflect.Descriptor instead. +func (*RenderResponse) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{19} +} + +func (x *RenderResponse) GetTree() *v14.RenderTree { + if x != nil { + return x.Tree + } + return nil +} + +// ModelError is the structured error every failure crossing this +// plugin boundary carries, per model.md §8. +type ModelError struct { + state protoimpl.MessageState `protogen:"open.v1"` + // This failure's category. MUST be set. + Category ModelErrorCategory `protobuf:"varint,1,opt,name=category,proto3,enum=pluggableharness.agent.model.v1.ModelErrorCategory" json:"category,omitempty"` + // Human-readable description of the failure. + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + // Whether the kernel may retry this request as-is. + Retryable bool `protobuf:"varint,3,opt,name=retryable,proto3" json:"retryable,omitempty"` + // How long the kernel should wait before retrying, when the vendor + // supplies one (typically alongside MODEL_ERROR_CATEGORY_RATE_LIMITED). + // SHOULD be set when available. Refines model.md §8's + // "retry_after_seconds" into the native well-known type. + RetryAfter *durationpb.Duration `protobuf:"bytes,4,opt,name=retry_after,json=retryAfter,proto3,oneof" json:"retry_after,omitempty"` + // The raw vendor-provided error code or body, for debugging. SHOULD be + // set. + RawDetail *string `protobuf:"bytes,5,opt,name=raw_detail,json=rawDetail,proto3,oneof" json:"raw_detail,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModelError) Reset() { + *x = ModelError{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModelError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModelError) ProtoMessage() {} + +func (x *ModelError) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ModelError.ProtoReflect.Descriptor instead. +func (*ModelError) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{20} +} + +func (x *ModelError) GetCategory() ModelErrorCategory { + if x != nil { + return x.Category + } + return ModelErrorCategory_MODEL_ERROR_CATEGORY_UNSPECIFIED +} + +func (x *ModelError) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *ModelError) GetRetryable() bool { + if x != nil { + return x.Retryable + } + return false +} + +func (x *ModelError) GetRetryAfter() *durationpb.Duration { + if x != nil { + return x.RetryAfter + } + return nil +} + +func (x *ModelError) GetRawDetail() string { + if x != nil && x.RawDetail != nil { + return *x.RawDetail + } + return "" +} + // ModelTarget describes the model a context or memory contribution is // being assembled for, derived from that model's ModelSpec -// (provider.md §2). Carried on context.md's ContextRequest and memory.md's +// (model.md §2). Carried on context.md's ContextRequest and memory.md's // RecallRequest so a provider can tailor its contribution (and compute // tokens against the right budget) for the model that will actually // consume it. type ModelTarget struct { state protoimpl.MessageState `protogen:"open.v1"` - // The target model's ModelSpec.id (provider.md §2) — the vendor's exact - // model identifier. - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - // The target model's total input token budget (provider.md §2 - // ModelSpec.context_window). - ContextWindow int64 `protobuf:"varint,2,opt,name=context_window,json=contextWindow,proto3" json:"context_window,omitempty"` - // The usable portion of context_window after the kernel reserves space - // for expected output, tool schemas, and other fixed overhead for this - // turn — the number a context/memory provider should actually budget - // its contribution against, rather than the raw context_window. - EffectiveCeiling int64 `protobuf:"varint,3,opt,name=effective_ceiling,json=effectiveCeiling,proto3" json:"effective_ceiling,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The target model's ModelSpec.id (model.md §2) — the vendor's exact + // model identifier. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // The target model's total input token budget (model.md §2 + // ModelSpec.context_window). + ContextWindow int64 `protobuf:"varint,2,opt,name=context_window,json=contextWindow,proto3" json:"context_window,omitempty"` + // The usable portion of context_window after the kernel reserves space + // for expected output, tool schemas, and other fixed overhead for this + // turn — the number a context/memory provider should actually budget + // its contribution against, rather than the raw context_window. + EffectiveCeiling int64 `protobuf:"varint,3,opt,name=effective_ceiling,json=effectiveCeiling,proto3" json:"effective_ceiling,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModelTarget) Reset() { + *x = ModelTarget{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModelTarget) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModelTarget) ProtoMessage() {} + +func (x *ModelTarget) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ModelTarget.ProtoReflect.Descriptor instead. +func (*ModelTarget) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{21} +} + +func (x *ModelTarget) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *ModelTarget) GetContextWindow() int64 { + if x != nil { + return x.ContextWindow + } + return 0 +} + +func (x *ModelTarget) GetEffectiveCeiling() int64 { + if x != nil { + return x.EffectiveCeiling + } + return 0 +} + +// ModelRef narrowly selects a model for token-counting purposes +// (kernel-callbacks.md §2 CountTokensRequest.model_ref). Optional on the +// request it's embedded in — omitted means "use the kernel's fallback +// heuristic," never "count against an unspecified model." +type ModelRef struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The declared name of the model provider plugin (common.v1.ProducerRef + // or ProviderRef's name field), e.g. "anthropic". + Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + // The model's ModelSpec.id within that provider. + Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModelRef) Reset() { + *x = ModelRef{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModelRef) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModelRef) ProtoMessage() {} + +func (x *ModelRef) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ModelRef.ProtoReflect.Descriptor instead. +func (*ModelRef) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{22} +} + +func (x *ModelRef) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + +func (x *ModelRef) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +// TextDelta carries one incremental fragment of assistant text output. +// MUST be supported by every plugin, both directions (model.md §5). +type StreamEvent_TextDelta struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The text fragment. + Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *ModelTarget) Reset() { - *x = ModelTarget{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[0] +func (x *StreamEvent_TextDelta) Reset() { + *x = StreamEvent_TextDelta{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ModelTarget) String() string { +func (x *StreamEvent_TextDelta) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ModelTarget) ProtoMessage() {} +func (*StreamEvent_TextDelta) ProtoMessage() {} -func (x *ModelTarget) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[0] +func (x *StreamEvent_TextDelta) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -77,62 +2085,94 @@ func (x *ModelTarget) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ModelTarget.ProtoReflect.Descriptor instead. -func (*ModelTarget) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{0} +// Deprecated: Use StreamEvent_TextDelta.ProtoReflect.Descriptor instead. +func (*StreamEvent_TextDelta) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{14, 0} } -func (x *ModelTarget) GetId() string { +func (x *StreamEvent_TextDelta) GetText() string { if x != nil { - return x.Id + return x.Text } return "" } -func (x *ModelTarget) GetContextWindow() int64 { +// ThinkingDelta carries one incremental fragment of the model's +// reasoning output. Only emitted when the target model's +// ThinkingSpec.supported is true. +type StreamEvent_ThinkingDelta struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The reasoning-text fragment. + Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StreamEvent_ThinkingDelta) Reset() { + *x = StreamEvent_ThinkingDelta{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StreamEvent_ThinkingDelta) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StreamEvent_ThinkingDelta) ProtoMessage() {} + +func (x *StreamEvent_ThinkingDelta) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[24] if x != nil { - return x.ContextWindow + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return 0 + return mi.MessageOf(x) } -func (x *ModelTarget) GetEffectiveCeiling() int64 { +// Deprecated: Use StreamEvent_ThinkingDelta.ProtoReflect.Descriptor instead. +func (*StreamEvent_ThinkingDelta) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{14, 1} +} + +func (x *StreamEvent_ThinkingDelta) GetText() string { if x != nil { - return x.EffectiveCeiling + return x.Text } - return 0 + return "" } -// ModelRef narrowly selects a model for token-counting purposes -// (kernel-callbacks.md §2 CountTokensRequest.model_ref). Optional on the -// request it's embedded in — omitted means "use the kernel's fallback -// heuristic," never "count against an unspecified model." -type ModelRef struct { +// ThinkingSignature carries the vendor's opaque integrity token for the +// reasoning block just completed. MUST be emitted if the vendor's +// thinking blocks carry an integrity signature (model.md §4/§5); the +// kernel MUST store and round-trip this verbatim, never inspecting or +// reformatting it, into ContentBlock's ThinkingBlock.signature. +type StreamEvent_ThinkingSignature struct { state protoimpl.MessageState `protogen:"open.v1"` - // The declared name of the model provider plugin (common.v1.ProducerRef - // or ProviderRef's name field), e.g. "anthropic". - Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` - // The model's ModelSpec.id within that provider. - Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` + // The opaque, vendor-specific signature bytes. + Signature []byte `protobuf:"bytes,1,opt,name=signature,proto3" json:"signature,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ModelRef) Reset() { - *x = ModelRef{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[1] +func (x *StreamEvent_ThinkingSignature) Reset() { + *x = StreamEvent_ThinkingSignature{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ModelRef) String() string { +func (x *StreamEvent_ThinkingSignature) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ModelRef) ProtoMessage() {} +func (*StreamEvent_ThinkingSignature) ProtoMessage() {} -func (x *ModelRef) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[1] +func (x *StreamEvent_ThinkingSignature) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -143,37 +2183,451 @@ func (x *ModelRef) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ModelRef.ProtoReflect.Descriptor instead. -func (*ModelRef) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{1} +// Deprecated: Use StreamEvent_ThinkingSignature.ProtoReflect.Descriptor instead. +func (*StreamEvent_ThinkingSignature) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{14, 2} } -func (x *ModelRef) GetProvider() string { +func (x *StreamEvent_ThinkingSignature) GetSignature() []byte { if x != nil { - return x.Provider + return x.Signature + } + return nil +} + +// ToolCallStart announces the model has begun requesting a tool +// invocation. +type StreamEvent_ToolCallStart struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Correlation id for the matching ToolCallDelta/ToolCallDone events + // and the resulting ToolUseBlock.id. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // The tool's declared name (ToolDeclaration.name). + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StreamEvent_ToolCallStart) Reset() { + *x = StreamEvent_ToolCallStart{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StreamEvent_ToolCallStart) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StreamEvent_ToolCallStart) ProtoMessage() {} + +func (x *StreamEvent_ToolCallStart) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StreamEvent_ToolCallStart.ProtoReflect.Descriptor instead. +func (*StreamEvent_ToolCallStart) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{14, 3} +} + +func (x *StreamEvent_ToolCallStart) GetId() string { + if x != nil { + return x.Id } return "" } -func (x *ModelRef) GetId() string { +func (x *StreamEvent_ToolCallStart) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +// ToolCallDelta carries one incremental fragment of a tool call's +// arguments, accumulated by the kernel across deltas into the final +// parsed JSON. +type StreamEvent_ToolCallDelta struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The id from the matching ToolCallStart. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // A partial-JSON fragment of the call's arguments. + ArgumentsFragment string `protobuf:"bytes,2,opt,name=arguments_fragment,json=argumentsFragment,proto3" json:"arguments_fragment,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StreamEvent_ToolCallDelta) Reset() { + *x = StreamEvent_ToolCallDelta{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StreamEvent_ToolCallDelta) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StreamEvent_ToolCallDelta) ProtoMessage() {} + +func (x *StreamEvent_ToolCallDelta) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StreamEvent_ToolCallDelta.ProtoReflect.Descriptor instead. +func (*StreamEvent_ToolCallDelta) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{14, 4} +} + +func (x *StreamEvent_ToolCallDelta) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *StreamEvent_ToolCallDelta) GetArgumentsFragment() string { + if x != nil { + return x.ArgumentsFragment + } + return "" +} + +// ToolCallDone signals a tool call's arguments are complete and ready +// for the kernel to parse and dispatch. +type StreamEvent_ToolCallDone struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The id from the matching ToolCallStart. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StreamEvent_ToolCallDone) Reset() { + *x = StreamEvent_ToolCallDone{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StreamEvent_ToolCallDone) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StreamEvent_ToolCallDone) ProtoMessage() {} + +func (x *StreamEvent_ToolCallDone) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StreamEvent_ToolCallDone.ProtoReflect.Descriptor instead. +func (*StreamEvent_ToolCallDone) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{14, 5} +} + +func (x *StreamEvent_ToolCallDone) GetId() string { if x != nil { return x.Id } return "" } +// Stop signals the completion has ended. +type StreamEvent_Stop struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Why the completion ended. + Reason StopReason `protobuf:"varint,1,opt,name=reason,proto3,enum=pluggableharness.agent.model.v1.StopReason" json:"reason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StreamEvent_Stop) Reset() { + *x = StreamEvent_Stop{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StreamEvent_Stop) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StreamEvent_Stop) ProtoMessage() {} + +func (x *StreamEvent_Stop) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StreamEvent_Stop.ProtoReflect.Descriptor instead. +func (*StreamEvent_Stop) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{14, 6} +} + +func (x *StreamEvent_Stop) GetReason() StopReason { + if x != nil { + return x.Reason + } + return StopReason_STOP_REASON_UNSPECIFIED +} + +// Error signals the completion failed. +type StreamEvent_Error struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The structured error, classified per model.md §8. + Error *ModelError `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StreamEvent_Error) Reset() { + *x = StreamEvent_Error{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StreamEvent_Error) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StreamEvent_Error) ProtoMessage() {} + +func (x *StreamEvent_Error) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StreamEvent_Error.ProtoReflect.Descriptor instead. +func (*StreamEvent_Error) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{14, 7} +} + +func (x *StreamEvent_Error) GetError() *ModelError { + if x != nil { + return x.Error + } + return nil +} + var File_pluggableharness_agent_model_v1_model_proto protoreflect.FileDescriptor const file_pluggableharness_agent_model_v1_model_proto_rawDesc = "" + "\n" + - "+pluggableharness/agent/model/v1/model.proto\x12\x1fpluggableharness.agent.model.v1\"q\n" + + "+pluggableharness/agent/model/v1/model.proto\x12\x1fpluggableharness.agent.model.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a-pluggableharness/agent/config/v1/config.proto\x1a/pluggableharness/agent/content/v1/content.proto\x1a-pluggableharness/agent/render/v1/render.proto\x1a-pluggableharness/agent/schema/v1/schema.proto\x1a9pluggableharness/agent/slashcommand/v1/slashcommand.proto\"\x18\n" + + "\x16GetCapabilitiesRequest\"l\n" + + "\x17GetCapabilitiesResponse\x12Q\n" + + "\fcapabilities\x18\x01 \x01(\v2-.pluggableharness.agent.model.v1.CapabilitiesR\fcapabilities\"\x88\x02\n" + + "\fCapabilities\x12B\n" + + "\x06models\x18\x01 \x03(\v2*.pluggableharness.agent.model.v1.ModelSpecR\x06models\x12_\n" + + "\x0eslash_commands\x18\x02 \x03(\v28.pluggableharness.agent.slashcommand.v1.SlashCommandSpecR\rslashCommands\x12S\n" + + "\rconfig_schema\x18\x03 \x01(\v2..pluggableharness.agent.config.v1.ConfigSchemaR\fconfigSchema\"C\n" + + "\x10ConfigureRequest\x12/\n" + + "\x06config\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x06config\"\x13\n" + + "\x11ConfigureResponse\"\xb0\x04\n" + + "\tModelSpec\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12%\n" + + "\x0econtext_window\x18\x02 \x01(\x03R\rcontextWindow\x12*\n" + + "\x11max_output_tokens\x18\x03 \x01(\x03R\x0fmaxOutputTokens\x12*\n" + + "\x11supports_tool_use\x18\x04 \x01(\bR\x0fsupportsToolUse\x12'\n" + + "\x0fsupports_vision\x18\x05 \x01(\bR\x0esupportsVision\x12-\n" + + "\x12supports_streaming\x18\x06 \x01(\bR\x11supportsStreaming\x12D\n" + + "\x1csupports_parallel_tool_calls\x18\a \x01(\bH\x00R\x19supportsParallelToolCalls\x88\x01\x01\x12I\n" + + "\bthinking\x18\b \x01(\v2-.pluggableharness.agent.model.v1.ThinkingSpecR\bthinking\x12F\n" + + "\acaching\x18\t \x01(\v2,.pluggableharness.agent.model.v1.CachingSpecR\acaching\x12B\n" + + "\apricing\x18\n" + + " \x01(\v2(.pluggableharness.agent.model.v1.PricingR\apricingB\x1f\n" + + "\x1d_supports_parallel_tool_calls\"9\n" + + "\x13ThinkingBudgetRange\x12\x10\n" + + "\x03min\x18\x01 \x01(\x03R\x03min\x12\x10\n" + + "\x03max\x18\x02 \x01(\x03R\x03max\"\xcf\x02\n" + + "\fThinkingSpec\x12\x1c\n" + + "\tsupported\x18\x01 \x01(\bR\tsupported\x12A\n" + + "\x04mode\x18\x02 \x01(\x0e2-.pluggableharness.agent.model.v1.ThinkingModeR\x04mode\x12#\n" + + "\reffort_levels\x18\x03 \x03(\tR\feffortLevels\x12\\\n" + + "\fbudget_range\x18\x04 \x01(\v24.pluggableharness.agent.model.v1.ThinkingBudgetRangeH\x00R\vbudgetRange\x88\x01\x01\x12\x1f\n" + + "\vcan_disable\x18\x05 \x01(\bR\n" + + "canDisable\x12\x1d\n" + + "\adefault\x18\x06 \x01(\tH\x01R\adefault\x88\x01\x01B\x0f\n" + + "\r_budget_rangeB\n" + + "\n" + + "\b_default\"\x9e\x01\n" + + "\vCachingSpec\x12\x1c\n" + + "\tsupported\x18\x01 \x01(\bR\tsupported\x12@\n" + + "\x04mode\x18\x02 \x01(\x0e2,.pluggableharness.agent.model.v1.CachingModeR\x04mode\x12/\n" + + "\x13keepalive_supported\x18\x03 \x01(\bR\x12keepaliveSupported\"\xd0\x04\n" + + "\vPricingTier\x12F\n" + + "\x0eeffective_from\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampH\x00R\reffectiveFrom\x88\x01\x01\x12H\n" + + "\x0feffective_until\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampH\x01R\x0eeffectiveUntil\x88\x01\x01\x12$\n" + + "\x0einput_per_mtok\x18\x03 \x01(\x01R\finputPerMtok\x12&\n" + + "\x0foutput_per_mtok\x18\x04 \x01(\x01R\routputPerMtok\x124\n" + + "\x14cache_write_per_mtok\x18\x05 \x01(\x01H\x02R\x11cacheWritePerMtok\x88\x01\x01\x122\n" + + "\x13cache_read_per_mtok\x18\x06 \x01(\x01H\x03R\x10cacheReadPerMtok\x88\x01\x01\x124\n" + + "\x14batch_input_per_mtok\x18\a \x01(\x01H\x04R\x11batchInputPerMtok\x88\x01\x01\x126\n" + + "\x15batch_output_per_mtok\x18\b \x01(\x01H\x05R\x12batchOutputPerMtok\x88\x01\x01B\x11\n" + + "\x0f_effective_fromB\x12\n" + + "\x10_effective_untilB\x17\n" + + "\x15_cache_write_per_mtokB\x16\n" + + "\x14_cache_read_per_mtokB\x17\n" + + "\x15_batch_input_per_mtokB\x18\n" + + "\x16_batch_output_per_mtok\"}\n" + + "\aPricing\x12\x1a\n" + + "\bcurrency\x18\x01 \x01(\tR\bcurrency\x12\x12\n" + + "\x04free\x18\x02 \x01(\bR\x04free\x12B\n" + + "\x05tiers\x18\x03 \x03(\v2,.pluggableharness.agent.model.v1.PricingTierR\x05tiers\"\x9f\x02\n" + + "\x17StreamCompletionRequest\x12F\n" + + "\bmessages\x18\x01 \x03(\v2*.pluggableharness.agent.content.v1.MessageR\bmessages\x12\x19\n" + + "\bmodel_id\x18\x02 \x01(\tR\amodelId\x12F\n" + + "\x05tools\x18\x03 \x03(\v20.pluggableharness.agent.model.v1.ToolDeclarationR\x05tools\x12N\n" + + "\x06params\x18\x04 \x01(\v21.pluggableharness.agent.model.v1.GenerationParamsH\x00R\x06params\x88\x01\x01B\t\n" + + "\a_params\"\x94\x01\n" + + "\x0fToolDeclaration\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + + "\vdescription\x18\x02 \x01(\tR\vdescription\x12K\n" + + "\finput_schema\x18\x03 \x01(\v2(.pluggableharness.agent.schema.v1.SchemaR\vinputSchema\"\xf1\x01\n" + + "\x10GenerationParams\x12,\n" + + "\x0fthinking_effort\x18\x01 \x01(\tH\x00R\x0ethinkingEffort\x88\x01\x01\x129\n" + + "\x16thinking_budget_tokens\x18\x02 \x01(\x03H\x01R\x14thinkingBudgetTokens\x88\x01\x01\x12/\n" + + "\x11max_output_tokens\x18\x03 \x01(\x03H\x02R\x0fmaxOutputTokens\x88\x01\x01B\x12\n" + + "\x10_thinking_effortB\x19\n" + + "\x17_thinking_budget_tokensB\x14\n" + + "\x12_max_output_tokens\"\x80\n" + + "\n" + + "\vStreamEvent\x12W\n" + + "\n" + + "text_delta\x18\x01 \x01(\v26.pluggableharness.agent.model.v1.StreamEvent.TextDeltaH\x00R\ttextDelta\x12c\n" + + "\x0ethinking_delta\x18\x02 \x01(\v2:.pluggableharness.agent.model.v1.StreamEvent.ThinkingDeltaH\x00R\rthinkingDelta\x12o\n" + + "\x12thinking_signature\x18\x03 \x01(\v2>.pluggableharness.agent.model.v1.StreamEvent.ThinkingSignatureH\x00R\x11thinkingSignature\x12d\n" + + "\x0ftool_call_start\x18\x04 \x01(\v2:.pluggableharness.agent.model.v1.StreamEvent.ToolCallStartH\x00R\rtoolCallStart\x12d\n" + + "\x0ftool_call_delta\x18\x05 \x01(\v2:.pluggableharness.agent.model.v1.StreamEvent.ToolCallDeltaH\x00R\rtoolCallDelta\x12a\n" + + "\x0etool_call_done\x18\x06 \x01(\v29.pluggableharness.agent.model.v1.StreamEvent.ToolCallDoneH\x00R\ftoolCallDone\x12>\n" + + "\x05usage\x18\a \x01(\v2&.pluggableharness.agent.model.v1.UsageH\x00R\x05usage\x12G\n" + + "\x04stop\x18\b \x01(\v21.pluggableharness.agent.model.v1.StreamEvent.StopH\x00R\x04stop\x12J\n" + + "\x05error\x18\t \x01(\v22.pluggableharness.agent.model.v1.StreamEvent.ErrorH\x00R\x05error\x1a\x1f\n" + + "\tTextDelta\x12\x12\n" + + "\x04text\x18\x01 \x01(\tR\x04text\x1a#\n" + + "\rThinkingDelta\x12\x12\n" + + "\x04text\x18\x01 \x01(\tR\x04text\x1a1\n" + + "\x11ThinkingSignature\x12\x1c\n" + + "\tsignature\x18\x01 \x01(\fR\tsignature\x1a3\n" + + "\rToolCallStart\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x1aN\n" + + "\rToolCallDelta\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12-\n" + + "\x12arguments_fragment\x18\x02 \x01(\tR\x11argumentsFragment\x1a\x1e\n" + + "\fToolCallDone\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x1aK\n" + + "\x04Stop\x12C\n" + + "\x06reason\x18\x01 \x01(\x0e2+.pluggableharness.agent.model.v1.StopReasonR\x06reason\x1aJ\n" + + "\x05Error\x12A\n" + + "\x05error\x18\x01 \x01(\v2+.pluggableharness.agent.model.v1.ModelErrorR\x05errorB\a\n" + + "\x05event\"\xe0\x01\n" + + "\x05Usage\x12!\n" + + "\finput_tokens\x18\x01 \x01(\x03R\vinputTokens\x12#\n" + + "\routput_tokens\x18\x02 \x01(\x03R\foutputTokens\x12/\n" + + "\x11cache_read_tokens\x18\x03 \x01(\x03H\x00R\x0fcacheReadTokens\x88\x01\x01\x121\n" + + "\x12cache_write_tokens\x18\x04 \x01(\x03H\x01R\x10cacheWriteTokens\x88\x01\x01B\x14\n" + + "\x12_cache_read_tokensB\x15\n" + + "\x13_cache_write_tokens\"(\n" + + "\x12CountTokensRequest\x12\x12\n" + + "\x04text\x18\x01 \x01(\tR\x04text\"+\n" + + "\x13CountTokensResponse\x12\x14\n" + + "\x05count\x18\x01 \x01(\x03R\x05count\")\n" + + "\rRenderRequest\x12\x18\n" + + "\apayload\x18\x01 \x01(\fR\apayload\"R\n" + + "\x0eRenderResponse\x12@\n" + + "\x04tree\x18\x01 \x01(\v2,.pluggableharness.agent.render.v1.RenderTreeR\x04tree\"\x99\x02\n" + + "\n" + + "ModelError\x12O\n" + + "\bcategory\x18\x01 \x01(\x0e23.pluggableharness.agent.model.v1.ModelErrorCategoryR\bcategory\x12\x18\n" + + "\amessage\x18\x02 \x01(\tR\amessage\x12\x1c\n" + + "\tretryable\x18\x03 \x01(\bR\tretryable\x12?\n" + + "\vretry_after\x18\x04 \x01(\v2\x19.google.protobuf.DurationH\x00R\n" + + "retryAfter\x88\x01\x01\x12\"\n" + + "\n" + + "raw_detail\x18\x05 \x01(\tH\x01R\trawDetail\x88\x01\x01B\x0e\n" + + "\f_retry_afterB\r\n" + + "\v_raw_detail\"q\n" + "\vModelTarget\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12%\n" + "\x0econtext_window\x18\x02 \x01(\x03R\rcontextWindow\x12+\n" + "\x11effective_ceiling\x18\x03 \x01(\x03R\x10effectiveCeiling\"6\n" + "\bModelRef\x12\x1a\n" + "\bprovider\x18\x01 \x01(\tR\bprovider\x12\x0e\n" + - "\x02id\x18\x02 \x01(\tR\x02idB>ZZ pluggableharness.agent.model.v1.Capabilities + 9, // 1: pluggableharness.agent.model.v1.Capabilities.models:type_name -> pluggableharness.agent.model.v1.ModelSpec + 35, // 2: pluggableharness.agent.model.v1.Capabilities.slash_commands:type_name -> pluggableharness.agent.slashcommand.v1.SlashCommandSpec + 36, // 3: pluggableharness.agent.model.v1.Capabilities.config_schema:type_name -> pluggableharness.agent.config.v1.ConfigSchema + 37, // 4: pluggableharness.agent.model.v1.ConfigureRequest.config:type_name -> google.protobuf.Struct + 11, // 5: pluggableharness.agent.model.v1.ModelSpec.thinking:type_name -> pluggableharness.agent.model.v1.ThinkingSpec + 12, // 6: pluggableharness.agent.model.v1.ModelSpec.caching:type_name -> pluggableharness.agent.model.v1.CachingSpec + 14, // 7: pluggableharness.agent.model.v1.ModelSpec.pricing:type_name -> pluggableharness.agent.model.v1.Pricing + 0, // 8: pluggableharness.agent.model.v1.ThinkingSpec.mode:type_name -> pluggableharness.agent.model.v1.ThinkingMode + 10, // 9: pluggableharness.agent.model.v1.ThinkingSpec.budget_range:type_name -> pluggableharness.agent.model.v1.ThinkingBudgetRange + 1, // 10: pluggableharness.agent.model.v1.CachingSpec.mode:type_name -> pluggableharness.agent.model.v1.CachingMode + 38, // 11: pluggableharness.agent.model.v1.PricingTier.effective_from:type_name -> google.protobuf.Timestamp + 38, // 12: pluggableharness.agent.model.v1.PricingTier.effective_until:type_name -> google.protobuf.Timestamp + 13, // 13: pluggableharness.agent.model.v1.Pricing.tiers:type_name -> pluggableharness.agent.model.v1.PricingTier + 39, // 14: pluggableharness.agent.model.v1.StreamCompletionRequest.messages:type_name -> pluggableharness.agent.content.v1.Message + 16, // 15: pluggableharness.agent.model.v1.StreamCompletionRequest.tools:type_name -> pluggableharness.agent.model.v1.ToolDeclaration + 17, // 16: pluggableharness.agent.model.v1.StreamCompletionRequest.params:type_name -> pluggableharness.agent.model.v1.GenerationParams + 40, // 17: pluggableharness.agent.model.v1.ToolDeclaration.input_schema:type_name -> pluggableharness.agent.schema.v1.Schema + 27, // 18: pluggableharness.agent.model.v1.StreamEvent.text_delta:type_name -> pluggableharness.agent.model.v1.StreamEvent.TextDelta + 28, // 19: pluggableharness.agent.model.v1.StreamEvent.thinking_delta:type_name -> pluggableharness.agent.model.v1.StreamEvent.ThinkingDelta + 29, // 20: pluggableharness.agent.model.v1.StreamEvent.thinking_signature:type_name -> pluggableharness.agent.model.v1.StreamEvent.ThinkingSignature + 30, // 21: pluggableharness.agent.model.v1.StreamEvent.tool_call_start:type_name -> pluggableharness.agent.model.v1.StreamEvent.ToolCallStart + 31, // 22: pluggableharness.agent.model.v1.StreamEvent.tool_call_delta:type_name -> pluggableharness.agent.model.v1.StreamEvent.ToolCallDelta + 32, // 23: pluggableharness.agent.model.v1.StreamEvent.tool_call_done:type_name -> pluggableharness.agent.model.v1.StreamEvent.ToolCallDone + 19, // 24: pluggableharness.agent.model.v1.StreamEvent.usage:type_name -> pluggableharness.agent.model.v1.Usage + 33, // 25: pluggableharness.agent.model.v1.StreamEvent.stop:type_name -> pluggableharness.agent.model.v1.StreamEvent.Stop + 34, // 26: pluggableharness.agent.model.v1.StreamEvent.error:type_name -> pluggableharness.agent.model.v1.StreamEvent.Error + 41, // 27: pluggableharness.agent.model.v1.RenderResponse.tree:type_name -> pluggableharness.agent.render.v1.RenderTree + 3, // 28: pluggableharness.agent.model.v1.ModelError.category:type_name -> pluggableharness.agent.model.v1.ModelErrorCategory + 42, // 29: pluggableharness.agent.model.v1.ModelError.retry_after:type_name -> google.protobuf.Duration + 2, // 30: pluggableharness.agent.model.v1.StreamEvent.Stop.reason:type_name -> pluggableharness.agent.model.v1.StopReason + 24, // 31: pluggableharness.agent.model.v1.StreamEvent.Error.error:type_name -> pluggableharness.agent.model.v1.ModelError + 4, // 32: pluggableharness.agent.model.v1.ModelService.GetCapabilities:input_type -> pluggableharness.agent.model.v1.GetCapabilitiesRequest + 7, // 33: pluggableharness.agent.model.v1.ModelService.Configure:input_type -> pluggableharness.agent.model.v1.ConfigureRequest + 15, // 34: pluggableharness.agent.model.v1.ModelService.StreamCompletion:input_type -> pluggableharness.agent.model.v1.StreamCompletionRequest + 20, // 35: pluggableharness.agent.model.v1.ModelService.CountTokens:input_type -> pluggableharness.agent.model.v1.CountTokensRequest + 22, // 36: pluggableharness.agent.model.v1.ModelService.Render:input_type -> pluggableharness.agent.model.v1.RenderRequest + 5, // 37: pluggableharness.agent.model.v1.ModelService.GetCapabilities:output_type -> pluggableharness.agent.model.v1.GetCapabilitiesResponse + 8, // 38: pluggableharness.agent.model.v1.ModelService.Configure:output_type -> pluggableharness.agent.model.v1.ConfigureResponse + 18, // 39: pluggableharness.agent.model.v1.ModelService.StreamCompletion:output_type -> pluggableharness.agent.model.v1.StreamEvent + 21, // 40: pluggableharness.agent.model.v1.ModelService.CountTokens:output_type -> pluggableharness.agent.model.v1.CountTokensResponse + 23, // 41: pluggableharness.agent.model.v1.ModelService.Render:output_type -> pluggableharness.agent.model.v1.RenderResponse + 37, // [37:42] is the sub-list for method output_type + 32, // [32:37] is the sub-list for method input_type + 32, // [32:32] is the sub-list for extension type_name + 32, // [32:32] is the sub-list for extension extendee + 0, // [0:32] is the sub-list for field type_name } func init() { file_pluggableharness_agent_model_v1_model_proto_init() } @@ -205,18 +2743,37 @@ func file_pluggableharness_agent_model_v1_model_proto_init() { if File_pluggableharness_agent_model_v1_model_proto != nil { return } + file_pluggableharness_agent_model_v1_model_proto_msgTypes[5].OneofWrappers = []any{} + file_pluggableharness_agent_model_v1_model_proto_msgTypes[7].OneofWrappers = []any{} + file_pluggableharness_agent_model_v1_model_proto_msgTypes[9].OneofWrappers = []any{} + file_pluggableharness_agent_model_v1_model_proto_msgTypes[11].OneofWrappers = []any{} + file_pluggableharness_agent_model_v1_model_proto_msgTypes[13].OneofWrappers = []any{} + file_pluggableharness_agent_model_v1_model_proto_msgTypes[14].OneofWrappers = []any{ + (*StreamEvent_TextDelta_)(nil), + (*StreamEvent_ThinkingDelta_)(nil), + (*StreamEvent_ThinkingSignature_)(nil), + (*StreamEvent_ToolCallStart_)(nil), + (*StreamEvent_ToolCallDelta_)(nil), + (*StreamEvent_ToolCallDone_)(nil), + (*StreamEvent_Usage)(nil), + (*StreamEvent_Stop_)(nil), + (*StreamEvent_Error_)(nil), + } + file_pluggableharness_agent_model_v1_model_proto_msgTypes[15].OneofWrappers = []any{} + file_pluggableharness_agent_model_v1_model_proto_msgTypes[20].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_pluggableharness_agent_model_v1_model_proto_rawDesc), len(file_pluggableharness_agent_model_v1_model_proto_rawDesc)), - NumEnums: 0, - NumMessages: 2, + NumEnums: 4, + NumMessages: 31, NumExtensions: 0, - NumServices: 0, + NumServices: 1, }, GoTypes: file_pluggableharness_agent_model_v1_model_proto_goTypes, DependencyIndexes: file_pluggableharness_agent_model_v1_model_proto_depIdxs, + EnumInfos: file_pluggableharness_agent_model_v1_model_proto_enumTypes, MessageInfos: file_pluggableharness_agent_model_v1_model_proto_msgTypes, }.Build() File_pluggableharness_agent_model_v1_model_proto = out.File diff --git a/pkg/provider/proto/v1/provider_grpc.pb.go b/pkg/model/proto/v1/model_grpc.pb.go similarity index 58% rename from pkg/provider/proto/v1/provider_grpc.pb.go rename to pkg/model/proto/v1/model_grpc.pb.go index 3312f74..4057cd7 100644 --- a/pkg/provider/proto/v1/provider_grpc.pb.go +++ b/pkg/model/proto/v1/model_grpc.pb.go @@ -2,13 +2,19 @@ // versions: // - protoc-gen-go-grpc v1.6.2 // - protoc (unknown) -// source: pluggableharness/agent/provider/v1/provider.proto +// source: pluggableharness/agent/model/v1/model.proto -// Package pluggableharness.agent.provider.v1 defines the model (LLM vendor) provider -// plugin protocol described in specifications/provider.md — see -// .claude/rules/proto.md. +// Package pluggableharness.agent.model.v1 defines the model (LLM vendor) provider +// plugin protocol described in specifications/model.md — see +// .claude/rules/proto.md — plus the two distinct model-identity shapes used +// across the other specs. The identity shapes are deliberately NOT unified +// into one message: ModelTarget is a rich "what am I generating context +// for" descriptor (context.md §4, memory.md §6); ModelRef is a narrow +// "which model's tokenizer" selector (kernel-callbacks.md §2). Merging them +// would force every CountTokens caller to populate fields it doesn't have +// and doesn't need. -package providerv1 +package modelv1 import ( context "context" @@ -23,31 +29,31 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - ProviderService_GetCapabilities_FullMethodName = "/pluggableharness.agent.provider.v1.ProviderService/GetCapabilities" - ProviderService_Configure_FullMethodName = "/pluggableharness.agent.provider.v1.ProviderService/Configure" - ProviderService_StreamCompletion_FullMethodName = "/pluggableharness.agent.provider.v1.ProviderService/StreamCompletion" - ProviderService_CountTokens_FullMethodName = "/pluggableharness.agent.provider.v1.ProviderService/CountTokens" - ProviderService_Render_FullMethodName = "/pluggableharness.agent.provider.v1.ProviderService/Render" + ModelService_GetCapabilities_FullMethodName = "/pluggableharness.agent.model.v1.ModelService/GetCapabilities" + ModelService_Configure_FullMethodName = "/pluggableharness.agent.model.v1.ModelService/Configure" + ModelService_StreamCompletion_FullMethodName = "/pluggableharness.agent.model.v1.ModelService/StreamCompletion" + ModelService_CountTokens_FullMethodName = "/pluggableharness.agent.model.v1.ModelService/CountTokens" + ModelService_Render_FullMethodName = "/pluggableharness.agent.model.v1.ModelService/Render" ) -// ProviderServiceClient is the client API for ProviderService service. +// ModelServiceClient is the client API for ModelService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. // -// ProviderService is the model provider plugin protocol described in -// specifications/provider.md §1: a subprocess + gRPC plugin (via +// ModelService is the model provider plugin protocol described in +// specifications/model.md §1: a subprocess + gRPC plugin (via // hashicorp/go-plugin) fronting one LLM vendor. Every plugin exposes // GetCapabilities, Configure, and StreamCompletion (all MUST); CountTokens // SHOULD be implemented; Render MAY be implemented. -type ProviderServiceClient interface { +type ModelServiceClient interface { // GetCapabilities returns one ModelSpec per model this plugin can serve, - // per provider.md §2. MUST be cheap to call repeatedly (the kernel MAY + // per model.md §2. MUST be cheap to call repeatedly (the kernel MAY // call it before every routing decision) and MUST NOT require a network // call to the vendor if avoidable. GetCapabilities(ctx context.Context, in *GetCapabilitiesRequest, opts ...grpc.CallOption) (*GetCapabilitiesResponse, error) // Configure delivers the provider's agent.hcl config block, already // decoded from HCL/cty into a Struct by the kernel's schema-to-cty - // bridge, per provider.md §3. MUST reject missing required fields (e.g. + // bridge, per model.md §3. MUST reject missing required fields (e.g. // no API key) with a structured error at Configure time rather than // deferring the failure to the first StreamCompletion call. A plugin // MUST NOT echo any received secret value into an Emit'd event, a Render @@ -60,19 +66,19 @@ type ProviderServiceClient interface { // stream of StreamEvents back. A backend whose vendor API is not natively // streaming (batch-only) MUST still implement this RPC shape, emitting // its full response as a single terminal burst of events followed by a - // `stop` event (provider.md §4). Cancellation is the kernel closing the + // `stop` event (model.md §4). Cancellation is the kernel closing the // gRPC stream; the plugin MUST treat this as normal control flow — stop // generating and release resources — surfacing StopReason // STOP_REASON_CANCELLED, never treating it as an error condition - // (provider.md §1, .claude/rules/grpc.md's cancellation rule). + // (model.md §1, .claude/rules/grpc.md's cancellation rule). // // buf:lint:ignore RPC_RESPONSE_STANDARD_NAME - // Stream element type is the bare "StreamEvent" per provider.md §4's + // Stream element type is the bare "StreamEvent" per model.md §4's // literal spec, naming the streamed domain concept rather than the RPC. StreamCompletion(ctx context.Context, in *StreamCompletionRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[StreamEvent], error) // CountTokens returns an exact token count for the given text, using the // vendor's real tokenizer. SHOULD be implemented per model provider - // (provider.md §2.1) — upgraded from an initial MAY per operator + // (model.md §2.1) — upgraded from an initial MAY per operator // decision. A model provider that implements this gets its counts // marked exact when the kernel resolves a CountTokens call against it // (kernel-callbacks.md §2); a provider that doesn't falls back to the @@ -81,43 +87,43 @@ type ProviderServiceClient interface { // Render is the model-provider side of the Emit->Render->Paint pipeline // (see docs/specifications/architecture.md), returning a RenderTree for an opaque // emitted payload — e.g. to render a `thinking` block collapsed by - // default, or usage/cost info specially. MAY be implemented; provider.md + // default, or usage/cost info specially. MAY be implemented; model.md // §7 notes most model-provider payloads (plain text, tool calls) render // fine under the kernel's generic fallback when this RPC is absent. Render(ctx context.Context, in *RenderRequest, opts ...grpc.CallOption) (*RenderResponse, error) } -type providerServiceClient struct { +type modelServiceClient struct { cc grpc.ClientConnInterface } -func NewProviderServiceClient(cc grpc.ClientConnInterface) ProviderServiceClient { - return &providerServiceClient{cc} +func NewModelServiceClient(cc grpc.ClientConnInterface) ModelServiceClient { + return &modelServiceClient{cc} } -func (c *providerServiceClient) GetCapabilities(ctx context.Context, in *GetCapabilitiesRequest, opts ...grpc.CallOption) (*GetCapabilitiesResponse, error) { +func (c *modelServiceClient) GetCapabilities(ctx context.Context, in *GetCapabilitiesRequest, opts ...grpc.CallOption) (*GetCapabilitiesResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(GetCapabilitiesResponse) - err := c.cc.Invoke(ctx, ProviderService_GetCapabilities_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, ModelService_GetCapabilities_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } -func (c *providerServiceClient) Configure(ctx context.Context, in *ConfigureRequest, opts ...grpc.CallOption) (*ConfigureResponse, error) { +func (c *modelServiceClient) Configure(ctx context.Context, in *ConfigureRequest, opts ...grpc.CallOption) (*ConfigureResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ConfigureResponse) - err := c.cc.Invoke(ctx, ProviderService_Configure_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, ModelService_Configure_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } -func (c *providerServiceClient) StreamCompletion(ctx context.Context, in *StreamCompletionRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[StreamEvent], error) { +func (c *modelServiceClient) StreamCompletion(ctx context.Context, in *StreamCompletionRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[StreamEvent], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &ProviderService_ServiceDesc.Streams[0], ProviderService_StreamCompletion_FullMethodName, cOpts...) + stream, err := c.cc.NewStream(ctx, &ModelService_ServiceDesc.Streams[0], ModelService_StreamCompletion_FullMethodName, cOpts...) if err != nil { return nil, err } @@ -132,46 +138,46 @@ func (c *providerServiceClient) StreamCompletion(ctx context.Context, in *Stream } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type ProviderService_StreamCompletionClient = grpc.ServerStreamingClient[StreamEvent] +type ModelService_StreamCompletionClient = grpc.ServerStreamingClient[StreamEvent] -func (c *providerServiceClient) CountTokens(ctx context.Context, in *CountTokensRequest, opts ...grpc.CallOption) (*CountTokensResponse, error) { +func (c *modelServiceClient) CountTokens(ctx context.Context, in *CountTokensRequest, opts ...grpc.CallOption) (*CountTokensResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(CountTokensResponse) - err := c.cc.Invoke(ctx, ProviderService_CountTokens_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, ModelService_CountTokens_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } -func (c *providerServiceClient) Render(ctx context.Context, in *RenderRequest, opts ...grpc.CallOption) (*RenderResponse, error) { +func (c *modelServiceClient) Render(ctx context.Context, in *RenderRequest, opts ...grpc.CallOption) (*RenderResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(RenderResponse) - err := c.cc.Invoke(ctx, ProviderService_Render_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, ModelService_Render_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } -// ProviderServiceServer is the server API for ProviderService service. -// All implementations must embed UnimplementedProviderServiceServer +// ModelServiceServer is the server API for ModelService service. +// All implementations must embed UnimplementedModelServiceServer // for forward compatibility. // -// ProviderService is the model provider plugin protocol described in -// specifications/provider.md §1: a subprocess + gRPC plugin (via +// ModelService is the model provider plugin protocol described in +// specifications/model.md §1: a subprocess + gRPC plugin (via // hashicorp/go-plugin) fronting one LLM vendor. Every plugin exposes // GetCapabilities, Configure, and StreamCompletion (all MUST); CountTokens // SHOULD be implemented; Render MAY be implemented. -type ProviderServiceServer interface { +type ModelServiceServer interface { // GetCapabilities returns one ModelSpec per model this plugin can serve, - // per provider.md §2. MUST be cheap to call repeatedly (the kernel MAY + // per model.md §2. MUST be cheap to call repeatedly (the kernel MAY // call it before every routing decision) and MUST NOT require a network // call to the vendor if avoidable. GetCapabilities(context.Context, *GetCapabilitiesRequest) (*GetCapabilitiesResponse, error) // Configure delivers the provider's agent.hcl config block, already // decoded from HCL/cty into a Struct by the kernel's schema-to-cty - // bridge, per provider.md §3. MUST reject missing required fields (e.g. + // bridge, per model.md §3. MUST reject missing required fields (e.g. // no API key) with a structured error at Configure time rather than // deferring the failure to the first StreamCompletion call. A plugin // MUST NOT echo any received secret value into an Emit'd event, a Render @@ -184,19 +190,19 @@ type ProviderServiceServer interface { // stream of StreamEvents back. A backend whose vendor API is not natively // streaming (batch-only) MUST still implement this RPC shape, emitting // its full response as a single terminal burst of events followed by a - // `stop` event (provider.md §4). Cancellation is the kernel closing the + // `stop` event (model.md §4). Cancellation is the kernel closing the // gRPC stream; the plugin MUST treat this as normal control flow — stop // generating and release resources — surfacing StopReason // STOP_REASON_CANCELLED, never treating it as an error condition - // (provider.md §1, .claude/rules/grpc.md's cancellation rule). + // (model.md §1, .claude/rules/grpc.md's cancellation rule). // // buf:lint:ignore RPC_RESPONSE_STANDARD_NAME - // Stream element type is the bare "StreamEvent" per provider.md §4's + // Stream element type is the bare "StreamEvent" per model.md §4's // literal spec, naming the streamed domain concept rather than the RPC. StreamCompletion(*StreamCompletionRequest, grpc.ServerStreamingServer[StreamEvent]) error // CountTokens returns an exact token count for the given text, using the // vendor's real tokenizer. SHOULD be implemented per model provider - // (provider.md §2.1) — upgraded from an initial MAY per operator + // (model.md §2.1) — upgraded from an initial MAY per operator // decision. A model provider that implements this gets its counts // marked exact when the kernel resolves a CountTokens call against it // (kernel-callbacks.md §2); a provider that doesn't falls back to the @@ -205,169 +211,169 @@ type ProviderServiceServer interface { // Render is the model-provider side of the Emit->Render->Paint pipeline // (see docs/specifications/architecture.md), returning a RenderTree for an opaque // emitted payload — e.g. to render a `thinking` block collapsed by - // default, or usage/cost info specially. MAY be implemented; provider.md + // default, or usage/cost info specially. MAY be implemented; model.md // §7 notes most model-provider payloads (plain text, tool calls) render // fine under the kernel's generic fallback when this RPC is absent. Render(context.Context, *RenderRequest) (*RenderResponse, error) - mustEmbedUnimplementedProviderServiceServer() + mustEmbedUnimplementedModelServiceServer() } -// UnimplementedProviderServiceServer must be embedded to have +// UnimplementedModelServiceServer must be embedded to have // forward compatible implementations. // // NOTE: this should be embedded by value instead of pointer to avoid a nil // pointer dereference when methods are called. -type UnimplementedProviderServiceServer struct{} +type UnimplementedModelServiceServer struct{} -func (UnimplementedProviderServiceServer) GetCapabilities(context.Context, *GetCapabilitiesRequest) (*GetCapabilitiesResponse, error) { +func (UnimplementedModelServiceServer) GetCapabilities(context.Context, *GetCapabilitiesRequest) (*GetCapabilitiesResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetCapabilities not implemented") } -func (UnimplementedProviderServiceServer) Configure(context.Context, *ConfigureRequest) (*ConfigureResponse, error) { +func (UnimplementedModelServiceServer) Configure(context.Context, *ConfigureRequest) (*ConfigureResponse, error) { return nil, status.Error(codes.Unimplemented, "method Configure not implemented") } -func (UnimplementedProviderServiceServer) StreamCompletion(*StreamCompletionRequest, grpc.ServerStreamingServer[StreamEvent]) error { +func (UnimplementedModelServiceServer) StreamCompletion(*StreamCompletionRequest, grpc.ServerStreamingServer[StreamEvent]) error { return status.Error(codes.Unimplemented, "method StreamCompletion not implemented") } -func (UnimplementedProviderServiceServer) CountTokens(context.Context, *CountTokensRequest) (*CountTokensResponse, error) { +func (UnimplementedModelServiceServer) CountTokens(context.Context, *CountTokensRequest) (*CountTokensResponse, error) { return nil, status.Error(codes.Unimplemented, "method CountTokens not implemented") } -func (UnimplementedProviderServiceServer) Render(context.Context, *RenderRequest) (*RenderResponse, error) { +func (UnimplementedModelServiceServer) Render(context.Context, *RenderRequest) (*RenderResponse, error) { return nil, status.Error(codes.Unimplemented, "method Render not implemented") } -func (UnimplementedProviderServiceServer) mustEmbedUnimplementedProviderServiceServer() {} -func (UnimplementedProviderServiceServer) testEmbeddedByValue() {} +func (UnimplementedModelServiceServer) mustEmbedUnimplementedModelServiceServer() {} +func (UnimplementedModelServiceServer) testEmbeddedByValue() {} -// UnsafeProviderServiceServer may be embedded to opt out of forward compatibility for this service. -// Use of this interface is not recommended, as added methods to ProviderServiceServer will +// UnsafeModelServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ModelServiceServer will // result in compilation errors. -type UnsafeProviderServiceServer interface { - mustEmbedUnimplementedProviderServiceServer() +type UnsafeModelServiceServer interface { + mustEmbedUnimplementedModelServiceServer() } -func RegisterProviderServiceServer(s grpc.ServiceRegistrar, srv ProviderServiceServer) { - // If the following call panics, it indicates UnimplementedProviderServiceServer was +func RegisterModelServiceServer(s grpc.ServiceRegistrar, srv ModelServiceServer) { + // If the following call panics, it indicates UnimplementedModelServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { t.testEmbeddedByValue() } - s.RegisterService(&ProviderService_ServiceDesc, srv) + s.RegisterService(&ModelService_ServiceDesc, srv) } -func _ProviderService_GetCapabilities_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { +func _ModelService_GetCapabilities_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetCapabilitiesRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(ProviderServiceServer).GetCapabilities(ctx, in) + return srv.(ModelServiceServer).GetCapabilities(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: ProviderService_GetCapabilities_FullMethodName, + FullMethod: ModelService_GetCapabilities_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ProviderServiceServer).GetCapabilities(ctx, req.(*GetCapabilitiesRequest)) + return srv.(ModelServiceServer).GetCapabilities(ctx, req.(*GetCapabilitiesRequest)) } return interceptor(ctx, in, info, handler) } -func _ProviderService_Configure_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { +func _ModelService_Configure_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ConfigureRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(ProviderServiceServer).Configure(ctx, in) + return srv.(ModelServiceServer).Configure(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: ProviderService_Configure_FullMethodName, + FullMethod: ModelService_Configure_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ProviderServiceServer).Configure(ctx, req.(*ConfigureRequest)) + return srv.(ModelServiceServer).Configure(ctx, req.(*ConfigureRequest)) } return interceptor(ctx, in, info, handler) } -func _ProviderService_StreamCompletion_Handler(srv interface{}, stream grpc.ServerStream) error { +func _ModelService_StreamCompletion_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(StreamCompletionRequest) if err := stream.RecvMsg(m); err != nil { return err } - return srv.(ProviderServiceServer).StreamCompletion(m, &grpc.GenericServerStream[StreamCompletionRequest, StreamEvent]{ServerStream: stream}) + return srv.(ModelServiceServer).StreamCompletion(m, &grpc.GenericServerStream[StreamCompletionRequest, StreamEvent]{ServerStream: stream}) } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type ProviderService_StreamCompletionServer = grpc.ServerStreamingServer[StreamEvent] +type ModelService_StreamCompletionServer = grpc.ServerStreamingServer[StreamEvent] -func _ProviderService_CountTokens_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { +func _ModelService_CountTokens_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CountTokensRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(ProviderServiceServer).CountTokens(ctx, in) + return srv.(ModelServiceServer).CountTokens(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: ProviderService_CountTokens_FullMethodName, + FullMethod: ModelService_CountTokens_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ProviderServiceServer).CountTokens(ctx, req.(*CountTokensRequest)) + return srv.(ModelServiceServer).CountTokens(ctx, req.(*CountTokensRequest)) } return interceptor(ctx, in, info, handler) } -func _ProviderService_Render_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { +func _ModelService_Render_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(RenderRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(ProviderServiceServer).Render(ctx, in) + return srv.(ModelServiceServer).Render(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: ProviderService_Render_FullMethodName, + FullMethod: ModelService_Render_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ProviderServiceServer).Render(ctx, req.(*RenderRequest)) + return srv.(ModelServiceServer).Render(ctx, req.(*RenderRequest)) } return interceptor(ctx, in, info, handler) } -// ProviderService_ServiceDesc is the grpc.ServiceDesc for ProviderService service. +// ModelService_ServiceDesc is the grpc.ServiceDesc for ModelService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) -var ProviderService_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "pluggableharness.agent.provider.v1.ProviderService", - HandlerType: (*ProviderServiceServer)(nil), +var ModelService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "pluggableharness.agent.model.v1.ModelService", + HandlerType: (*ModelServiceServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "GetCapabilities", - Handler: _ProviderService_GetCapabilities_Handler, + Handler: _ModelService_GetCapabilities_Handler, }, { MethodName: "Configure", - Handler: _ProviderService_Configure_Handler, + Handler: _ModelService_Configure_Handler, }, { MethodName: "CountTokens", - Handler: _ProviderService_CountTokens_Handler, + Handler: _ModelService_CountTokens_Handler, }, { MethodName: "Render", - Handler: _ProviderService_Render_Handler, + Handler: _ModelService_Render_Handler, }, }, Streams: []grpc.StreamDesc{ { StreamName: "StreamCompletion", - Handler: _ProviderService_StreamCompletion_Handler, + Handler: _ModelService_StreamCompletion_Handler, ServerStreams: true, }, }, - Metadata: "pluggableharness/agent/provider/v1/provider.proto", + Metadata: "pluggableharness/agent/model/v1/model.proto", } diff --git a/pkg/plan/proto/v1/plan.pb.go b/pkg/plan/proto/v1/plan.pb.go index c481233..a4ffc81 100644 --- a/pkg/plan/proto/v1/plan.pb.go +++ b/pkg/plan/proto/v1/plan.pb.go @@ -117,7 +117,7 @@ type PlanItem struct { // The tool operation being called (tool.md §2 ToolSchema.name). ToolName string `protobuf:"bytes,4,opt,name=tool_name,json=toolName,proto3" json:"tool_name,omitempty"` // The call's parsed arguments — the kernel's canonical ToolCall - // representation (provider.md §6 / pluggableharness.agent.schema.v1's subset governs + // representation (model.md §6 / pluggableharness.agent.schema.v1's subset governs // its shape). A Struct per .claude/rules/proto.md's runtime-JSON // carve-out. Input *structpb.Struct `protobuf:"bytes,5,opt,name=input,proto3" json:"input,omitempty"` diff --git a/pkg/provider/proto/v1/provider.pb.go b/pkg/provider/proto/v1/provider.pb.go deleted file mode 100644 index 9a466dc..0000000 --- a/pkg/provider/proto/v1/provider.pb.go +++ /dev/null @@ -1,2630 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc (unknown) -// source: pluggableharness/agent/provider/v1/provider.proto - -// Package pluggableharness.agent.provider.v1 defines the model (LLM vendor) provider -// plugin protocol described in specifications/provider.md — see -// .claude/rules/proto.md. - -package providerv1 - -import ( - v11 "github.com/pluggableharness/agent/pkg/config/proto/v1" - v12 "github.com/pluggableharness/agent/pkg/content/proto/v1" - v14 "github.com/pluggableharness/agent/pkg/render/proto/v1" - v13 "github.com/pluggableharness/agent/pkg/schema/proto/v1" - v1 "github.com/pluggableharness/agent/pkg/slashcommand/proto/v1" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - durationpb "google.golang.org/protobuf/types/known/durationpb" - structpb "google.golang.org/protobuf/types/known/structpb" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -// ThinkingMode enumerates the shapes of extended-reasoning control found -// across researched vendors (provider.md §2) — a plain supports_thinking -// bool would lose information the kernel needs to build a correct -// request, since some vendors (e.g. Anthropic) expose more than one mode -// across their own model lineup. -type ThinkingMode int32 - -const ( - // Zero value. Never valid when ThinkingSpec.supported is true; its - // presence on the wire means a caller forgot to set the field. - ThinkingMode_THINKING_MODE_UNSPECIFIED ThinkingMode = 0 - // The model has no extended-reasoning capability. Pairs with - // ThinkingSpec.supported == false. - ThinkingMode_THINKING_MODE_NONE ThinkingMode = 1 - // The model always reasons, adaptively, with no caller-selectable - // effort level or budget. - ThinkingMode_THINKING_MODE_ALWAYS_ON_ADAPTIVE ThinkingMode = 2 - // The caller selects one of a fixed set of named effort levels - // (ThinkingSpec.effort_levels). - ThinkingMode_THINKING_MODE_DISCRETE_EFFORT ThinkingMode = 3 - // The caller selects a token budget within ThinkingSpec.budget_range. - ThinkingMode_THINKING_MODE_CONTINUOUS_BUDGET ThinkingMode = 4 -) - -// Enum value maps for ThinkingMode. -var ( - ThinkingMode_name = map[int32]string{ - 0: "THINKING_MODE_UNSPECIFIED", - 1: "THINKING_MODE_NONE", - 2: "THINKING_MODE_ALWAYS_ON_ADAPTIVE", - 3: "THINKING_MODE_DISCRETE_EFFORT", - 4: "THINKING_MODE_CONTINUOUS_BUDGET", - } - ThinkingMode_value = map[string]int32{ - "THINKING_MODE_UNSPECIFIED": 0, - "THINKING_MODE_NONE": 1, - "THINKING_MODE_ALWAYS_ON_ADAPTIVE": 2, - "THINKING_MODE_DISCRETE_EFFORT": 3, - "THINKING_MODE_CONTINUOUS_BUDGET": 4, - } -) - -func (x ThinkingMode) Enum() *ThinkingMode { - p := new(ThinkingMode) - *p = x - return p -} - -func (x ThinkingMode) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (ThinkingMode) Descriptor() protoreflect.EnumDescriptor { - return file_pluggableharness_agent_provider_v1_provider_proto_enumTypes[0].Descriptor() -} - -func (ThinkingMode) Type() protoreflect.EnumType { - return &file_pluggableharness_agent_provider_v1_provider_proto_enumTypes[0] -} - -func (x ThinkingMode) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use ThinkingMode.Descriptor instead. -func (ThinkingMode) EnumDescriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{0} -} - -// CachingMode enumerates the prompt-caching mechanics found across -// researched vendors (provider.md §2). -type CachingMode int32 - -const ( - // Zero value. Never valid when CachingSpec.supported is true; its - // presence on the wire means a caller forgot to set the field. - CachingMode_CACHING_MODE_UNSPECIFIED CachingMode = 0 - // The model has no prompt-caching capability. Pairs with - // CachingSpec.supported == false. - CachingMode_CACHING_MODE_NONE CachingMode = 1 - // The caller must place cache breakpoints on content blocks explicitly - // (Anthropic/Mistral-style). - CachingMode_CACHING_MODE_EXPLICIT_MARKERS CachingMode = 2 - // The vendor applies caching transparently above a token threshold, no - // caller action required. - CachingMode_CACHING_MODE_IMPLICIT_AUTOMATIC CachingMode = 3 -) - -// Enum value maps for CachingMode. -var ( - CachingMode_name = map[int32]string{ - 0: "CACHING_MODE_UNSPECIFIED", - 1: "CACHING_MODE_NONE", - 2: "CACHING_MODE_EXPLICIT_MARKERS", - 3: "CACHING_MODE_IMPLICIT_AUTOMATIC", - } - CachingMode_value = map[string]int32{ - "CACHING_MODE_UNSPECIFIED": 0, - "CACHING_MODE_NONE": 1, - "CACHING_MODE_EXPLICIT_MARKERS": 2, - "CACHING_MODE_IMPLICIT_AUTOMATIC": 3, - } -) - -func (x CachingMode) Enum() *CachingMode { - p := new(CachingMode) - *p = x - return p -} - -func (x CachingMode) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (CachingMode) Descriptor() protoreflect.EnumDescriptor { - return file_pluggableharness_agent_provider_v1_provider_proto_enumTypes[1].Descriptor() -} - -func (CachingMode) Type() protoreflect.EnumType { - return &file_pluggableharness_agent_provider_v1_provider_proto_enumTypes[1] -} - -func (x CachingMode) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use CachingMode.Descriptor instead. -func (CachingMode) EnumDescriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{1} -} - -// StopReason classifies why a StreamCompletion ended, per provider.md §4. -type StopReason int32 - -const ( - // Zero value. Never valid on a real Stop event; its presence on the - // wire means a caller forgot to set the field. - StopReason_STOP_REASON_UNSPECIFIED StopReason = 0 - // The model completed its turn normally. - StopReason_STOP_REASON_END_TURN StopReason = 1 - // The model stopped to request one or more tool invocations. - StopReason_STOP_REASON_TOOL_USE StopReason = 2 - // The model hit its output token limit before completing its turn. - StopReason_STOP_REASON_MAX_TOKENS StopReason = 3 - // The vendor's content filter stopped generation. - StopReason_STOP_REASON_CONTENT_FILTERED StopReason = 4 - // The kernel cancelled the stream (user interrupt, timeout, turn - // abort). MUST be treated by the plugin as normal control flow, never - // as an error (provider.md §1, .claude/rules/grpc.md). - StopReason_STOP_REASON_CANCELLED StopReason = 5 -) - -// Enum value maps for StopReason. -var ( - StopReason_name = map[int32]string{ - 0: "STOP_REASON_UNSPECIFIED", - 1: "STOP_REASON_END_TURN", - 2: "STOP_REASON_TOOL_USE", - 3: "STOP_REASON_MAX_TOKENS", - 4: "STOP_REASON_CONTENT_FILTERED", - 5: "STOP_REASON_CANCELLED", - } - StopReason_value = map[string]int32{ - "STOP_REASON_UNSPECIFIED": 0, - "STOP_REASON_END_TURN": 1, - "STOP_REASON_TOOL_USE": 2, - "STOP_REASON_MAX_TOKENS": 3, - "STOP_REASON_CONTENT_FILTERED": 4, - "STOP_REASON_CANCELLED": 5, - } -) - -func (x StopReason) Enum() *StopReason { - p := new(StopReason) - *p = x - return p -} - -func (x StopReason) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (StopReason) Descriptor() protoreflect.EnumDescriptor { - return file_pluggableharness_agent_provider_v1_provider_proto_enumTypes[2].Descriptor() -} - -func (StopReason) Type() protoreflect.EnumType { - return &file_pluggableharness_agent_provider_v1_provider_proto_enumTypes[2] -} - -func (x StopReason) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use StopReason.Descriptor instead. -func (StopReason) EnumDescriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{2} -} - -// ProviderErrorCategory classifies every StreamCompletion/Configure -// failure, per provider.md §8. A plugin MUST classify every failure into -// exactly one of these categories and MUST NOT collapse them into a -// single generic error — the kernel's routing/fallback/retry behavior -// depends on telling these apart. -type ProviderErrorCategory int32 - -const ( - // Zero value. Never valid on a real ProviderError; its presence on the - // wire means a caller forgot to set the field. - ProviderErrorCategory_PROVIDER_ERROR_CATEGORY_UNSPECIFIED ProviderErrorCategory = 0 - // The request (or accumulated conversation) exceeds the model's context - // window. The kernel MUST NOT blindly retry as-is. - ProviderErrorCategory_PROVIDER_ERROR_CATEGORY_CONTEXT_LENGTH_EXCEEDED ProviderErrorCategory = 1 - // A vendor-side rate limit was hit. The kernel retries with backoff, - // honoring retry_after if supplied. - ProviderErrorCategory_PROVIDER_ERROR_CATEGORY_RATE_LIMITED ProviderErrorCategory = 2 - // Transient vendor unavailability (5xx-equivalent). The kernel retries - // with backoff; a candidate for capability-aware fallback. - ProviderErrorCategory_PROVIDER_ERROR_CATEGORY_OVERLOADED ProviderErrorCategory = 3 - // Bad, expired, or missing credentials. The kernel MUST NOT retry or - // silently fall back; this surfaces to a human. - ProviderErrorCategory_PROVIDER_ERROR_CATEGORY_AUTH_ERROR ProviderErrorCategory = 4 - // A malformed request — almost always a kernel/adapter bug. The kernel - // MUST NOT retry as-is. - ProviderErrorCategory_PROVIDER_ERROR_CATEGORY_INVALID_REQUEST ProviderErrorCategory = 5 - // The vendor refused or filtered the content. Surfaced distinctly from - // a generic failure so policy/UX can handle it differently. - ProviderErrorCategory_PROVIDER_ERROR_CATEGORY_CONTENT_FILTERED ProviderErrorCategory = 6 - // Anything else. MUST include raw_detail for debugging; treated as - // non-retryable by default. - ProviderErrorCategory_PROVIDER_ERROR_CATEGORY_UNKNOWN ProviderErrorCategory = 7 -) - -// Enum value maps for ProviderErrorCategory. -var ( - ProviderErrorCategory_name = map[int32]string{ - 0: "PROVIDER_ERROR_CATEGORY_UNSPECIFIED", - 1: "PROVIDER_ERROR_CATEGORY_CONTEXT_LENGTH_EXCEEDED", - 2: "PROVIDER_ERROR_CATEGORY_RATE_LIMITED", - 3: "PROVIDER_ERROR_CATEGORY_OVERLOADED", - 4: "PROVIDER_ERROR_CATEGORY_AUTH_ERROR", - 5: "PROVIDER_ERROR_CATEGORY_INVALID_REQUEST", - 6: "PROVIDER_ERROR_CATEGORY_CONTENT_FILTERED", - 7: "PROVIDER_ERROR_CATEGORY_UNKNOWN", - } - ProviderErrorCategory_value = map[string]int32{ - "PROVIDER_ERROR_CATEGORY_UNSPECIFIED": 0, - "PROVIDER_ERROR_CATEGORY_CONTEXT_LENGTH_EXCEEDED": 1, - "PROVIDER_ERROR_CATEGORY_RATE_LIMITED": 2, - "PROVIDER_ERROR_CATEGORY_OVERLOADED": 3, - "PROVIDER_ERROR_CATEGORY_AUTH_ERROR": 4, - "PROVIDER_ERROR_CATEGORY_INVALID_REQUEST": 5, - "PROVIDER_ERROR_CATEGORY_CONTENT_FILTERED": 6, - "PROVIDER_ERROR_CATEGORY_UNKNOWN": 7, - } -) - -func (x ProviderErrorCategory) Enum() *ProviderErrorCategory { - p := new(ProviderErrorCategory) - *p = x - return p -} - -func (x ProviderErrorCategory) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (ProviderErrorCategory) Descriptor() protoreflect.EnumDescriptor { - return file_pluggableharness_agent_provider_v1_provider_proto_enumTypes[3].Descriptor() -} - -func (ProviderErrorCategory) Type() protoreflect.EnumType { - return &file_pluggableharness_agent_provider_v1_provider_proto_enumTypes[3] -} - -func (x ProviderErrorCategory) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use ProviderErrorCategory.Descriptor instead. -func (ProviderErrorCategory) EnumDescriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{3} -} - -// GetCapabilitiesRequest is empty: provider.md §2 defines GetCapabilities -// as taking no request parameters. -type GetCapabilitiesRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetCapabilitiesRequest) Reset() { - *x = GetCapabilitiesRequest{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetCapabilitiesRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetCapabilitiesRequest) ProtoMessage() {} - -func (x *GetCapabilitiesRequest) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetCapabilitiesRequest.ProtoReflect.Descriptor instead. -func (*GetCapabilitiesRequest) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{0} -} - -// GetCapabilitiesResponse wraps Capabilities for the RPC signature, per -// this repo's per-RPC envelope convention (.claude/rules/proto.md). -type GetCapabilitiesResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Capabilities *Capabilities `protobuf:"bytes,1,opt,name=capabilities,proto3" json:"capabilities,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetCapabilitiesResponse) Reset() { - *x = GetCapabilitiesResponse{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetCapabilitiesResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetCapabilitiesResponse) ProtoMessage() {} - -func (x *GetCapabilitiesResponse) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[1] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetCapabilitiesResponse.ProtoReflect.Descriptor instead. -func (*GetCapabilitiesResponse) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{1} -} - -func (x *GetCapabilitiesResponse) GetCapabilities() *Capabilities { - if x != nil { - return x.Capabilities - } - return nil -} - -// Capabilities is GetCapabilities' response payload: every model this -// plugin can serve, plus provider-wide declarations that apply once, not -// per model. -type Capabilities struct { - state protoimpl.MessageState `protogen:"open.v1"` - // One ModelSpec per model the plugin can serve. MUST have at least one - // entry. - Models []*ModelSpec `protobuf:"bytes,1,rep,name=models,proto3" json:"models,omitempty"` - // Slash commands this provider contributes, declared once for the - // provider as a whole (not per model), per provider.md §2 and - // configuration.md §5 / frontend.md §5. MAY be empty. - SlashCommands []*v1.SlashCommandSpec `protobuf:"bytes,2,rep,name=slash_commands,json=slashCommands,proto3" json:"slash_commands,omitempty"` - // The provider's agent.hcl config schema, returned alongside - // capabilities so the kernel knows what fields Configure expects, per - // configuration.md §4. - ConfigSchema *v11.ConfigSchema `protobuf:"bytes,3,opt,name=config_schema,json=configSchema,proto3" json:"config_schema,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Capabilities) Reset() { - *x = Capabilities{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Capabilities) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Capabilities) ProtoMessage() {} - -func (x *Capabilities) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[2] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Capabilities.ProtoReflect.Descriptor instead. -func (*Capabilities) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{2} -} - -func (x *Capabilities) GetModels() []*ModelSpec { - if x != nil { - return x.Models - } - return nil -} - -func (x *Capabilities) GetSlashCommands() []*v1.SlashCommandSpec { - if x != nil { - return x.SlashCommands - } - return nil -} - -func (x *Capabilities) GetConfigSchema() *v11.ConfigSchema { - if x != nil { - return x.ConfigSchema - } - return nil -} - -// ConfigureRequest wraps the provider's agent.hcl config block, already -// decoded from HCL/cty into a Struct by the kernel's schema-to-cty bridge, -// per provider.md §3. -type ConfigureRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The decoded config value. Field contents are provider-specific (API - // key, base URL override, org/project IDs, etc.) — provider.md §3 - // doesn't mandate a shape beyond what ConfigSchema (Capabilities. - // config_schema) declares. A Struct because the shape is genuinely - // provider-defined, not fixed at the proto level (see - // .claude/rules/proto.md's Struct carve-out). - Config *structpb.Struct `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ConfigureRequest) Reset() { - *x = ConfigureRequest{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ConfigureRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ConfigureRequest) ProtoMessage() {} - -func (x *ConfigureRequest) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[3] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ConfigureRequest.ProtoReflect.Descriptor instead. -func (*ConfigureRequest) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{3} -} - -func (x *ConfigureRequest) GetConfig() *structpb.Struct { - if x != nil { - return x.Config - } - return nil -} - -// ConfigureResponse is empty on success. A Configure failure (e.g. a -// missing required field) surfaces as a gRPC status carrying a -// ProviderError in its structured detail, per .claude/rules/grpc.md's -// error-taxonomy convention — there is no in-band error field on this -// message. -type ConfigureResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ConfigureResponse) Reset() { - *x = ConfigureResponse{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ConfigureResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ConfigureResponse) ProtoMessage() {} - -func (x *ConfigureResponse) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[4] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ConfigureResponse.ProtoReflect.Descriptor instead. -func (*ConfigureResponse) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{4} -} - -// ModelSpec describes one model this provider can serve, per -// provider.md §2. Every field below is MUST unless its comment says -// otherwise. -type ModelSpec struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The vendor's exact model identifier, used to select this model in - // StreamCompletionRequest.model_id. - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - // The model's input token budget. - ContextWindow int64 `protobuf:"varint,2,opt,name=context_window,json=contextWindow,proto3" json:"context_window,omitempty"` - // The model's maximum output tokens per response. - MaxOutputTokens int64 `protobuf:"varint,3,opt,name=max_output_tokens,json=maxOutputTokens,proto3" json:"max_output_tokens,omitempty"` - // Whether this model can accept tool declarations and emit tool_use - // content blocks. - SupportsToolUse bool `protobuf:"varint,4,opt,name=supports_tool_use,json=supportsToolUse,proto3" json:"supports_tool_use,omitempty"` - // Whether this model can accept image content blocks. - SupportsVision bool `protobuf:"varint,5,opt,name=supports_vision,json=supportsVision,proto3" json:"supports_vision,omitempty"` - // Whether the vendor's own backend streams responses. A UX hint only - // (e.g. "don't render a live-typing cursor" when false) — the - // StreamCompletion RPC shape is always server-streaming regardless of - // this value, per provider.md §1/§4. - SupportsStreaming bool `protobuf:"varint,6,opt,name=supports_streaming,json=supportsStreaming,proto3" json:"supports_streaming,omitempty"` - // Whether this model can return multiple tool_use blocks in a single - // turn. SHOULD be set accurately; a false or absent value means the - // kernel MUST serialize tool calls for this model. - SupportsParallelToolCalls *bool `protobuf:"varint,7,opt,name=supports_parallel_tool_calls,json=supportsParallelToolCalls,proto3,oneof" json:"supports_parallel_tool_calls,omitempty"` - // This model's extended-reasoning capability. MUST be present even when - // unsupported — use { supported: false } rather than omitting the - // message, so a caller never has to distinguish "unset" from "no - // thinking mode". - Thinking *ThinkingSpec `protobuf:"bytes,8,opt,name=thinking,proto3" json:"thinking,omitempty"` - // This model's prompt-caching capability. MUST be present even when - // unsupported — use { supported: false } rather than omitting the - // message. - Caching *CachingSpec `protobuf:"bytes,9,opt,name=caching,proto3" json:"caching,omitempty"` - // This model's pricing. MUST be present even for a free model (set - // Pricing.free = true). - Pricing *Pricing `protobuf:"bytes,10,opt,name=pricing,proto3" json:"pricing,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ModelSpec) Reset() { - *x = ModelSpec{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ModelSpec) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ModelSpec) ProtoMessage() {} - -func (x *ModelSpec) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[5] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ModelSpec.ProtoReflect.Descriptor instead. -func (*ModelSpec) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{5} -} - -func (x *ModelSpec) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *ModelSpec) GetContextWindow() int64 { - if x != nil { - return x.ContextWindow - } - return 0 -} - -func (x *ModelSpec) GetMaxOutputTokens() int64 { - if x != nil { - return x.MaxOutputTokens - } - return 0 -} - -func (x *ModelSpec) GetSupportsToolUse() bool { - if x != nil { - return x.SupportsToolUse - } - return false -} - -func (x *ModelSpec) GetSupportsVision() bool { - if x != nil { - return x.SupportsVision - } - return false -} - -func (x *ModelSpec) GetSupportsStreaming() bool { - if x != nil { - return x.SupportsStreaming - } - return false -} - -func (x *ModelSpec) GetSupportsParallelToolCalls() bool { - if x != nil && x.SupportsParallelToolCalls != nil { - return *x.SupportsParallelToolCalls - } - return false -} - -func (x *ModelSpec) GetThinking() *ThinkingSpec { - if x != nil { - return x.Thinking - } - return nil -} - -func (x *ModelSpec) GetCaching() *CachingSpec { - if x != nil { - return x.Caching - } - return nil -} - -func (x *ModelSpec) GetPricing() *Pricing { - if x != nil { - return x.Pricing - } - return nil -} - -// ThinkingBudgetRange bounds the token budget a caller may request when -// ThinkingMode is THINKING_MODE_CONTINUOUS_BUDGET. -type ThinkingBudgetRange struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The smallest thinking-token budget this model accepts. - Min int64 `protobuf:"varint,1,opt,name=min,proto3" json:"min,omitempty"` - // The largest thinking-token budget this model accepts. - Max int64 `protobuf:"varint,2,opt,name=max,proto3" json:"max,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ThinkingBudgetRange) Reset() { - *x = ThinkingBudgetRange{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ThinkingBudgetRange) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ThinkingBudgetRange) ProtoMessage() {} - -func (x *ThinkingBudgetRange) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[6] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ThinkingBudgetRange.ProtoReflect.Descriptor instead. -func (*ThinkingBudgetRange) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{6} -} - -func (x *ThinkingBudgetRange) GetMin() int64 { - if x != nil { - return x.Min - } - return 0 -} - -func (x *ThinkingBudgetRange) GetMax() int64 { - if x != nil { - return x.Max - } - return 0 -} - -// ThinkingSpec describes one model's extended-reasoning capability, per -// provider.md §2. -type ThinkingSpec struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Whether this model has any extended-reasoning capability at all. - Supported bool `protobuf:"varint,1,opt,name=supported,proto3" json:"supported,omitempty"` - // Which reasoning-control shape this model uses. MUST be - // THINKING_MODE_NONE when supported == false. - Mode ThinkingMode `protobuf:"varint,2,opt,name=mode,proto3,enum=pluggableharness.agent.provider.v1.ThinkingMode" json:"mode,omitempty"` - // The selectable effort levels, e.g. ["low","medium","high","xhigh", - // "max"]. MUST be non-empty when mode == THINKING_MODE_DISCRETE_EFFORT; - // meaningless otherwise. - EffortLevels []string `protobuf:"bytes,3,rep,name=effort_levels,json=effortLevels,proto3" json:"effort_levels,omitempty"` - // The selectable token-budget range. MUST be present when mode == - // THINKING_MODE_CONTINUOUS_BUDGET; meaningless otherwise. - BudgetRange *ThinkingBudgetRange `protobuf:"bytes,4,opt,name=budget_range,json=budgetRange,proto3,oneof" json:"budget_range,omitempty"` - // Whether reasoning can be turned off once enabled. MUST be set - // accurately — some vendors' reasoning cannot be disabled (e.g. a - // researched Grok model defaults reasoning on with no off switch). - CanDisable bool `protobuf:"varint,5,opt,name=can_disable,json=canDisable,proto3" json:"can_disable,omitempty"` - // The effort level (discrete_effort) or budget-token value - // (continuous_budget), as a string, the vendor applies when a request - // omits thinking config entirely. MUST be set when mode != - // THINKING_MODE_NONE — makes the vendor's actual default behavior - // visible/auditable via GetCapabilities rather than hidden in adapter - // code, so a kernel wanting deterministic behavior can always send an - // explicit override. - Default *string `protobuf:"bytes,6,opt,name=default,proto3,oneof" json:"default,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ThinkingSpec) Reset() { - *x = ThinkingSpec{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ThinkingSpec) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ThinkingSpec) ProtoMessage() {} - -func (x *ThinkingSpec) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[7] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ThinkingSpec.ProtoReflect.Descriptor instead. -func (*ThinkingSpec) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{7} -} - -func (x *ThinkingSpec) GetSupported() bool { - if x != nil { - return x.Supported - } - return false -} - -func (x *ThinkingSpec) GetMode() ThinkingMode { - if x != nil { - return x.Mode - } - return ThinkingMode_THINKING_MODE_UNSPECIFIED -} - -func (x *ThinkingSpec) GetEffortLevels() []string { - if x != nil { - return x.EffortLevels - } - return nil -} - -func (x *ThinkingSpec) GetBudgetRange() *ThinkingBudgetRange { - if x != nil { - return x.BudgetRange - } - return nil -} - -func (x *ThinkingSpec) GetCanDisable() bool { - if x != nil { - return x.CanDisable - } - return false -} - -func (x *ThinkingSpec) GetDefault() string { - if x != nil && x.Default != nil { - return *x.Default - } - return "" -} - -// CachingSpec describes one model's prompt-caching capability, per -// provider.md §2. -type CachingSpec struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Whether this model has any prompt-caching capability at all. - Supported bool `protobuf:"varint,1,opt,name=supported,proto3" json:"supported,omitempty"` - // Which caching mechanic this model uses. MUST be CACHING_MODE_NONE - // when supported == false. - Mode CachingMode `protobuf:"varint,2,opt,name=mode,proto3,enum=pluggableharness.agent.provider.v1.CachingMode" json:"mode,omitempty"` - // Whether this provider runs its own cache-keepalive loop (e.g. a - // background goroutine re-pinging before a cache TTL expires, so a long - // tool-execution gap doesn't let the cache go cold). MUST be set, - // default false. Cache TTL mechanics are vendor-specific, so per - // operator decision this is a provider-owned behavior the kernel never - // drives — this field only tells the kernel/operator whether a given - // provider implements the optimization (provider.md §2). - KeepaliveSupported bool `protobuf:"varint,3,opt,name=keepalive_supported,json=keepaliveSupported,proto3" json:"keepalive_supported,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CachingSpec) Reset() { - *x = CachingSpec{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CachingSpec) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CachingSpec) ProtoMessage() {} - -func (x *CachingSpec) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[8] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use CachingSpec.ProtoReflect.Descriptor instead. -func (*CachingSpec) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{8} -} - -func (x *CachingSpec) GetSupported() bool { - if x != nil { - return x.Supported - } - return false -} - -func (x *CachingSpec) GetMode() CachingMode { - if x != nil { - return x.Mode - } - return CachingMode_CACHING_MODE_UNSPECIFIED -} - -func (x *CachingSpec) GetKeepaliveSupported() bool { - if x != nil { - return x.KeepaliveSupported - } - return false -} - -// PricingTier is one time-bounded rate within a model's Pricing, per -// provider.md §2. Exactly one tier MUST match at any given timestamp -// (effective_from <= ts < effective_until, an omitted bound unbounded on -// that side); the kernel MUST reject a Pricing value at capability-load -// time if its tiers overlap or leave a gap. -type PricingTier struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The moment this tier becomes active. Omitted means "since this plugin - // version was published". Refines provider.md §2's "ISO 8601 - // date/timestamp" into the native well-known type. - EffectiveFrom *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=effective_from,json=effectiveFrom,proto3,oneof" json:"effective_from,omitempty"` - // The moment this tier stops being active. Omitted means "still - // current" — an omitted effective_until marks the currently active - // tier. Refines provider.md §2's "ISO 8601" into the native well-known - // type. - EffectiveUntil *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=effective_until,json=effectiveUntil,proto3,oneof" json:"effective_until,omitempty"` - // Cost per million input tokens, realtime rate. - InputPerMtok float64 `protobuf:"fixed64,3,opt,name=input_per_mtok,json=inputPerMtok,proto3" json:"input_per_mtok,omitempty"` - // Cost per million output tokens, realtime rate. - OutputPerMtok float64 `protobuf:"fixed64,4,opt,name=output_per_mtok,json=outputPerMtok,proto3" json:"output_per_mtok,omitempty"` - // Cost per million cache-write tokens. MUST be present iff - // CachingSpec.supported. - CacheWritePerMtok *float64 `protobuf:"fixed64,5,opt,name=cache_write_per_mtok,json=cacheWritePerMtok,proto3,oneof" json:"cache_write_per_mtok,omitempty"` - // Cost per million cache-read tokens, typically far cheaper than - // input_per_mtok — the entire point of caching. MUST be present iff - // CachingSpec.supported. - CacheReadPerMtok *float64 `protobuf:"fixed64,6,opt,name=cache_read_per_mtok,json=cacheReadPerMtok,proto3,oneof" json:"cache_read_per_mtok,omitempty"` - // A vendor's discounted batch/async input rate, where one exists (e.g. - // a researched Gemini batch tier). MAY be present. - BatchInputPerMtok *float64 `protobuf:"fixed64,7,opt,name=batch_input_per_mtok,json=batchInputPerMtok,proto3,oneof" json:"batch_input_per_mtok,omitempty"` - // A vendor's discounted batch/async output rate, paired with - // batch_input_per_mtok. MAY be present. - BatchOutputPerMtok *float64 `protobuf:"fixed64,8,opt,name=batch_output_per_mtok,json=batchOutputPerMtok,proto3,oneof" json:"batch_output_per_mtok,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *PricingTier) Reset() { - *x = PricingTier{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *PricingTier) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PricingTier) ProtoMessage() {} - -func (x *PricingTier) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[9] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use PricingTier.ProtoReflect.Descriptor instead. -func (*PricingTier) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{9} -} - -func (x *PricingTier) GetEffectiveFrom() *timestamppb.Timestamp { - if x != nil { - return x.EffectiveFrom - } - return nil -} - -func (x *PricingTier) GetEffectiveUntil() *timestamppb.Timestamp { - if x != nil { - return x.EffectiveUntil - } - return nil -} - -func (x *PricingTier) GetInputPerMtok() float64 { - if x != nil { - return x.InputPerMtok - } - return 0 -} - -func (x *PricingTier) GetOutputPerMtok() float64 { - if x != nil { - return x.OutputPerMtok - } - return 0 -} - -func (x *PricingTier) GetCacheWritePerMtok() float64 { - if x != nil && x.CacheWritePerMtok != nil { - return *x.CacheWritePerMtok - } - return 0 -} - -func (x *PricingTier) GetCacheReadPerMtok() float64 { - if x != nil && x.CacheReadPerMtok != nil { - return *x.CacheReadPerMtok - } - return 0 -} - -func (x *PricingTier) GetBatchInputPerMtok() float64 { - if x != nil && x.BatchInputPerMtok != nil { - return *x.BatchInputPerMtok - } - return 0 -} - -func (x *PricingTier) GetBatchOutputPerMtok() float64 { - if x != nil && x.BatchOutputPerMtok != nil { - return *x.BatchOutputPerMtok - } - return 0 -} - -// Pricing describes one model's cost structure, per provider.md §2. MUST -// be present on every ModelSpec, even a free one. -type Pricing struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The pricing currency. MUST be "USD" for v1; reserved for future - // multi-currency support, not acted on by the kernel yet. - Currency string `protobuf:"bytes,1,opt,name=currency,proto3" json:"currency,omitempty"` - // True for a local/free-to-run model (e.g. an Ollama-served model). - // When true, tiers MAY be omitted entirely. - Free bool `protobuf:"varint,2,opt,name=free,proto3" json:"free,omitempty"` - // This model's rate tiers, ordered or not — resolution is by - // effective_from/effective_until, not array position. MUST have at - // least one entry unless free == true. Exactly one tier MUST match any - // given timestamp; the kernel MUST reject overlapping or gapped tiers - // at capability-load time. - Tiers []*PricingTier `protobuf:"bytes,3,rep,name=tiers,proto3" json:"tiers,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Pricing) Reset() { - *x = Pricing{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Pricing) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Pricing) ProtoMessage() {} - -func (x *Pricing) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[10] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Pricing.ProtoReflect.Descriptor instead. -func (*Pricing) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{10} -} - -func (x *Pricing) GetCurrency() string { - if x != nil { - return x.Currency - } - return "" -} - -func (x *Pricing) GetFree() bool { - if x != nil { - return x.Free - } - return false -} - -func (x *Pricing) GetTiers() []*PricingTier { - if x != nil { - return x.Tiers - } - return nil -} - -// StreamCompletionRequest is StreamCompletion's request: the full -// canonical conversation, available tools, and generation params for one -// completion, per provider.md §4. -type StreamCompletionRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The canonical conversation history, in emission order (provider.md - // §5). - Messages []*v12.Message `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"` - // Selects which of this provider's ModelSpec.id to use. - ModelId string `protobuf:"bytes,2,opt,name=model_id,json=modelId,proto3" json:"model_id,omitempty"` - // The tools available to the model on this turn, described in the - // shared JSON-Schema subset (provider.md §6). MAY be empty. - Tools []*ToolDeclaration `protobuf:"bytes,3,rep,name=tools,proto3" json:"tools,omitempty"` - // Generation-time overrides. Omitted means every param takes its - // model-specific default. - Params *GenerationParams `protobuf:"bytes,4,opt,name=params,proto3,oneof" json:"params,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *StreamCompletionRequest) Reset() { - *x = StreamCompletionRequest{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *StreamCompletionRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StreamCompletionRequest) ProtoMessage() {} - -func (x *StreamCompletionRequest) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[11] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use StreamCompletionRequest.ProtoReflect.Descriptor instead. -func (*StreamCompletionRequest) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{11} -} - -func (x *StreamCompletionRequest) GetMessages() []*v12.Message { - if x != nil { - return x.Messages - } - return nil -} - -func (x *StreamCompletionRequest) GetModelId() string { - if x != nil { - return x.ModelId - } - return "" -} - -func (x *StreamCompletionRequest) GetTools() []*ToolDeclaration { - if x != nil { - return x.Tools - } - return nil -} - -func (x *StreamCompletionRequest) GetParams() *GenerationParams { - if x != nil { - return x.Params - } - return nil -} - -// ToolDeclaration is one tool the model may call on this turn, per -// provider.md §6. Each model-provider adapter translates this into its -// vendor's own tool-definition wire format. -type ToolDeclaration struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The tool's name, as the model must reference it in a ToolUseBlock. - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Human-readable description shown to the model to help it decide - // whether and how to call this tool. - Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` - // The tool's input shape, in the restricted JSON-Schema subset shared - // across categories (provider.md §6, pluggableharness.agent.schema.v1.Schema). - InputSchema *v13.Schema `protobuf:"bytes,3,opt,name=input_schema,json=inputSchema,proto3" json:"input_schema,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ToolDeclaration) Reset() { - *x = ToolDeclaration{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ToolDeclaration) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ToolDeclaration) ProtoMessage() {} - -func (x *ToolDeclaration) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[12] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ToolDeclaration.ProtoReflect.Descriptor instead. -func (*ToolDeclaration) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{12} -} - -func (x *ToolDeclaration) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *ToolDeclaration) GetDescription() string { - if x != nil { - return x.Description - } - return "" -} - -func (x *ToolDeclaration) GetInputSchema() *v13.Schema { - if x != nil { - return x.InputSchema - } - return nil -} - -// GenerationParams carries per-request overrides of otherwise -// model-default generation behavior. -type GenerationParams struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Selects one of ThinkingSpec.effort_levels. Meaningful only when the - // target model's ThinkingSpec.mode == THINKING_MODE_DISCRETE_EFFORT. - ThinkingEffort *string `protobuf:"bytes,1,opt,name=thinking_effort,json=thinkingEffort,proto3,oneof" json:"thinking_effort,omitempty"` - // Selects a token budget within ThinkingSpec.budget_range. Meaningful - // only when the target model's ThinkingSpec.mode == - // THINKING_MODE_CONTINUOUS_BUDGET. - ThinkingBudgetTokens *int64 `protobuf:"varint,2,opt,name=thinking_budget_tokens,json=thinkingBudgetTokens,proto3,oneof" json:"thinking_budget_tokens,omitempty"` - // Per-request override of ModelSpec.max_output_tokens. Omitted means - // use the model's default. - MaxOutputTokens *int64 `protobuf:"varint,3,opt,name=max_output_tokens,json=maxOutputTokens,proto3,oneof" json:"max_output_tokens,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GenerationParams) Reset() { - *x = GenerationParams{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[13] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GenerationParams) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GenerationParams) ProtoMessage() {} - -func (x *GenerationParams) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[13] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GenerationParams.ProtoReflect.Descriptor instead. -func (*GenerationParams) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{13} -} - -func (x *GenerationParams) GetThinkingEffort() string { - if x != nil && x.ThinkingEffort != nil { - return *x.ThinkingEffort - } - return "" -} - -func (x *GenerationParams) GetThinkingBudgetTokens() int64 { - if x != nil && x.ThinkingBudgetTokens != nil { - return *x.ThinkingBudgetTokens - } - return 0 -} - -func (x *GenerationParams) GetMaxOutputTokens() int64 { - if x != nil && x.MaxOutputTokens != nil { - return *x.MaxOutputTokens - } - return 0 -} - -// StreamEvent is one message in the stream StreamCompletion returns, per -// provider.md §4. Exactly one variant is set. -type StreamEvent struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to Event: - // - // *StreamEvent_TextDelta_ - // *StreamEvent_ThinkingDelta_ - // *StreamEvent_ThinkingSignature_ - // *StreamEvent_ToolCallStart_ - // *StreamEvent_ToolCallDelta_ - // *StreamEvent_ToolCallDone_ - // *StreamEvent_Usage_ - // *StreamEvent_Stop_ - // *StreamEvent_Error_ - Event isStreamEvent_Event `protobuf_oneof:"event"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *StreamEvent) Reset() { - *x = StreamEvent{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *StreamEvent) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StreamEvent) ProtoMessage() {} - -func (x *StreamEvent) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[14] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use StreamEvent.ProtoReflect.Descriptor instead. -func (*StreamEvent) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{14} -} - -func (x *StreamEvent) GetEvent() isStreamEvent_Event { - if x != nil { - return x.Event - } - return nil -} - -func (x *StreamEvent) GetTextDelta() *StreamEvent_TextDelta { - if x != nil { - if x, ok := x.Event.(*StreamEvent_TextDelta_); ok { - return x.TextDelta - } - } - return nil -} - -func (x *StreamEvent) GetThinkingDelta() *StreamEvent_ThinkingDelta { - if x != nil { - if x, ok := x.Event.(*StreamEvent_ThinkingDelta_); ok { - return x.ThinkingDelta - } - } - return nil -} - -func (x *StreamEvent) GetThinkingSignature() *StreamEvent_ThinkingSignature { - if x != nil { - if x, ok := x.Event.(*StreamEvent_ThinkingSignature_); ok { - return x.ThinkingSignature - } - } - return nil -} - -func (x *StreamEvent) GetToolCallStart() *StreamEvent_ToolCallStart { - if x != nil { - if x, ok := x.Event.(*StreamEvent_ToolCallStart_); ok { - return x.ToolCallStart - } - } - return nil -} - -func (x *StreamEvent) GetToolCallDelta() *StreamEvent_ToolCallDelta { - if x != nil { - if x, ok := x.Event.(*StreamEvent_ToolCallDelta_); ok { - return x.ToolCallDelta - } - } - return nil -} - -func (x *StreamEvent) GetToolCallDone() *StreamEvent_ToolCallDone { - if x != nil { - if x, ok := x.Event.(*StreamEvent_ToolCallDone_); ok { - return x.ToolCallDone - } - } - return nil -} - -func (x *StreamEvent) GetUsage() *StreamEvent_Usage { - if x != nil { - if x, ok := x.Event.(*StreamEvent_Usage_); ok { - return x.Usage - } - } - return nil -} - -func (x *StreamEvent) GetStop() *StreamEvent_Stop { - if x != nil { - if x, ok := x.Event.(*StreamEvent_Stop_); ok { - return x.Stop - } - } - return nil -} - -func (x *StreamEvent) GetError() *StreamEvent_Error { - if x != nil { - if x, ok := x.Event.(*StreamEvent_Error_); ok { - return x.Error - } - } - return nil -} - -type isStreamEvent_Event interface { - isStreamEvent_Event() -} - -type StreamEvent_TextDelta_ struct { - // An incremental fragment of assistant text output. - TextDelta *StreamEvent_TextDelta `protobuf:"bytes,1,opt,name=text_delta,json=textDelta,proto3,oneof"` -} - -type StreamEvent_ThinkingDelta_ struct { - // An incremental fragment of the model's reasoning output. - ThinkingDelta *StreamEvent_ThinkingDelta `protobuf:"bytes,2,opt,name=thinking_delta,json=thinkingDelta,proto3,oneof"` -} - -type StreamEvent_ThinkingSignature_ struct { - // The vendor's opaque integrity token for the reasoning just emitted. - ThinkingSignature *StreamEvent_ThinkingSignature `protobuf:"bytes,3,opt,name=thinking_signature,json=thinkingSignature,proto3,oneof"` -} - -type StreamEvent_ToolCallStart_ struct { - // The model has begun requesting a tool invocation. - ToolCallStart *StreamEvent_ToolCallStart `protobuf:"bytes,4,opt,name=tool_call_start,json=toolCallStart,proto3,oneof"` -} - -type StreamEvent_ToolCallDelta_ struct { - // An incremental fragment of a tool call's arguments. - ToolCallDelta *StreamEvent_ToolCallDelta `protobuf:"bytes,5,opt,name=tool_call_delta,json=toolCallDelta,proto3,oneof"` -} - -type StreamEvent_ToolCallDone_ struct { - // A tool call's arguments are complete. - ToolCallDone *StreamEvent_ToolCallDone `protobuf:"bytes,6,opt,name=tool_call_done,json=toolCallDone,proto3,oneof"` -} - -type StreamEvent_Usage_ struct { - // Token accounting for this completion. - Usage *StreamEvent_Usage `protobuf:"bytes,7,opt,name=usage,proto3,oneof"` -} - -type StreamEvent_Stop_ struct { - // The completion has ended. - Stop *StreamEvent_Stop `protobuf:"bytes,8,opt,name=stop,proto3,oneof"` -} - -type StreamEvent_Error_ struct { - // The completion failed. - Error *StreamEvent_Error `protobuf:"bytes,9,opt,name=error,proto3,oneof"` -} - -func (*StreamEvent_TextDelta_) isStreamEvent_Event() {} - -func (*StreamEvent_ThinkingDelta_) isStreamEvent_Event() {} - -func (*StreamEvent_ThinkingSignature_) isStreamEvent_Event() {} - -func (*StreamEvent_ToolCallStart_) isStreamEvent_Event() {} - -func (*StreamEvent_ToolCallDelta_) isStreamEvent_Event() {} - -func (*StreamEvent_ToolCallDone_) isStreamEvent_Event() {} - -func (*StreamEvent_Usage_) isStreamEvent_Event() {} - -func (*StreamEvent_Stop_) isStreamEvent_Event() {} - -func (*StreamEvent_Error_) isStreamEvent_Event() {} - -// CountTokensRequest is CountTokens' request: the raw text to count, per -// provider.md §2.1. -type CountTokensRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The text to count tokens for. - Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CountTokensRequest) Reset() { - *x = CountTokensRequest{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CountTokensRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CountTokensRequest) ProtoMessage() {} - -func (x *CountTokensRequest) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[15] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use CountTokensRequest.ProtoReflect.Descriptor instead. -func (*CountTokensRequest) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{15} -} - -func (x *CountTokensRequest) GetText() string { - if x != nil { - return x.Text - } - return "" -} - -// CountTokensResponse is CountTokens' response. -type CountTokensResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The exact token count, per this model's real vendor tokenizer. - Count int64 `protobuf:"varint,1,opt,name=count,proto3" json:"count,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CountTokensResponse) Reset() { - *x = CountTokensResponse{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[16] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CountTokensResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CountTokensResponse) ProtoMessage() {} - -func (x *CountTokensResponse) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[16] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use CountTokensResponse.ProtoReflect.Descriptor instead. -func (*CountTokensResponse) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{16} -} - -func (x *CountTokensResponse) GetCount() int64 { - if x != nil { - return x.Count - } - return 0 -} - -// RenderRequest carries the opaque payload to render, per provider.md §7. -type RenderRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The opaque emitted payload to render — the Emit->Render->Paint - // pipeline's deliberate carve-out from the strong-typing rule (see - // .claude/rules/grpc.md), never interpreted by the kernel. - Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RenderRequest) Reset() { - *x = RenderRequest{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[17] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *RenderRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RenderRequest) ProtoMessage() {} - -func (x *RenderRequest) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[17] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use RenderRequest.ProtoReflect.Descriptor instead. -func (*RenderRequest) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{17} -} - -func (x *RenderRequest) GetPayload() []byte { - if x != nil { - return x.Payload - } - return nil -} - -// RenderResponse wraps the resulting RenderTree, per provider.md §7. -type RenderResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The rendered tree, formally defined in frontend.md §1 and shared - // verbatim across every category's Render RPC (tool.md §7, context.md - // §9, memory.md §10) — one RenderTree type for the whole - // Emit->Render->Paint pipeline, not a per-category variant. - Tree *v14.RenderTree `protobuf:"bytes,1,opt,name=tree,proto3" json:"tree,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RenderResponse) Reset() { - *x = RenderResponse{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[18] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *RenderResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RenderResponse) ProtoMessage() {} - -func (x *RenderResponse) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[18] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use RenderResponse.ProtoReflect.Descriptor instead. -func (*RenderResponse) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{18} -} - -func (x *RenderResponse) GetTree() *v14.RenderTree { - if x != nil { - return x.Tree - } - return nil -} - -// ProviderError is the structured error every failure crossing this -// plugin boundary carries, per provider.md §8. -type ProviderError struct { - state protoimpl.MessageState `protogen:"open.v1"` - // This failure's category. MUST be set. - Category ProviderErrorCategory `protobuf:"varint,1,opt,name=category,proto3,enum=pluggableharness.agent.provider.v1.ProviderErrorCategory" json:"category,omitempty"` - // Human-readable description of the failure. - Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` - // Whether the kernel may retry this request as-is. - Retryable bool `protobuf:"varint,3,opt,name=retryable,proto3" json:"retryable,omitempty"` - // How long the kernel should wait before retrying, when the vendor - // supplies one (typically alongside PROVIDER_ERROR_CATEGORY_RATE_LIMITED). - // SHOULD be set when available. Refines provider.md §8's - // "retry_after_seconds" into the native well-known type. - RetryAfter *durationpb.Duration `protobuf:"bytes,4,opt,name=retry_after,json=retryAfter,proto3,oneof" json:"retry_after,omitempty"` - // The raw vendor-provided error code or body, for debugging. SHOULD be - // set. - RawDetail *string `protobuf:"bytes,5,opt,name=raw_detail,json=rawDetail,proto3,oneof" json:"raw_detail,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ProviderError) Reset() { - *x = ProviderError{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[19] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ProviderError) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ProviderError) ProtoMessage() {} - -func (x *ProviderError) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[19] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ProviderError.ProtoReflect.Descriptor instead. -func (*ProviderError) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{19} -} - -func (x *ProviderError) GetCategory() ProviderErrorCategory { - if x != nil { - return x.Category - } - return ProviderErrorCategory_PROVIDER_ERROR_CATEGORY_UNSPECIFIED -} - -func (x *ProviderError) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -func (x *ProviderError) GetRetryable() bool { - if x != nil { - return x.Retryable - } - return false -} - -func (x *ProviderError) GetRetryAfter() *durationpb.Duration { - if x != nil { - return x.RetryAfter - } - return nil -} - -func (x *ProviderError) GetRawDetail() string { - if x != nil && x.RawDetail != nil { - return *x.RawDetail - } - return "" -} - -// TextDelta carries one incremental fragment of assistant text output. -// MUST be supported by every plugin, both directions (provider.md §5). -type StreamEvent_TextDelta struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The text fragment. - Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *StreamEvent_TextDelta) Reset() { - *x = StreamEvent_TextDelta{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[20] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *StreamEvent_TextDelta) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StreamEvent_TextDelta) ProtoMessage() {} - -func (x *StreamEvent_TextDelta) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[20] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use StreamEvent_TextDelta.ProtoReflect.Descriptor instead. -func (*StreamEvent_TextDelta) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{14, 0} -} - -func (x *StreamEvent_TextDelta) GetText() string { - if x != nil { - return x.Text - } - return "" -} - -// ThinkingDelta carries one incremental fragment of the model's -// reasoning output. Only emitted when the target model's -// ThinkingSpec.supported is true. -type StreamEvent_ThinkingDelta struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The reasoning-text fragment. - Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *StreamEvent_ThinkingDelta) Reset() { - *x = StreamEvent_ThinkingDelta{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[21] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *StreamEvent_ThinkingDelta) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StreamEvent_ThinkingDelta) ProtoMessage() {} - -func (x *StreamEvent_ThinkingDelta) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[21] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use StreamEvent_ThinkingDelta.ProtoReflect.Descriptor instead. -func (*StreamEvent_ThinkingDelta) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{14, 1} -} - -func (x *StreamEvent_ThinkingDelta) GetText() string { - if x != nil { - return x.Text - } - return "" -} - -// ThinkingSignature carries the vendor's opaque integrity token for the -// reasoning block just completed. MUST be emitted if the vendor's -// thinking blocks carry an integrity signature (provider.md §4/§5); the -// kernel MUST store and round-trip this verbatim, never inspecting or -// reformatting it, into ContentBlock's ThinkingBlock.signature. -type StreamEvent_ThinkingSignature struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The opaque, vendor-specific signature bytes. - Signature []byte `protobuf:"bytes,1,opt,name=signature,proto3" json:"signature,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *StreamEvent_ThinkingSignature) Reset() { - *x = StreamEvent_ThinkingSignature{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[22] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *StreamEvent_ThinkingSignature) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StreamEvent_ThinkingSignature) ProtoMessage() {} - -func (x *StreamEvent_ThinkingSignature) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[22] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use StreamEvent_ThinkingSignature.ProtoReflect.Descriptor instead. -func (*StreamEvent_ThinkingSignature) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{14, 2} -} - -func (x *StreamEvent_ThinkingSignature) GetSignature() []byte { - if x != nil { - return x.Signature - } - return nil -} - -// ToolCallStart announces the model has begun requesting a tool -// invocation. -type StreamEvent_ToolCallStart struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Correlation id for the matching ToolCallDelta/ToolCallDone events - // and the resulting ToolUseBlock.id. - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - // The tool's declared name (ToolDeclaration.name). - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *StreamEvent_ToolCallStart) Reset() { - *x = StreamEvent_ToolCallStart{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[23] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *StreamEvent_ToolCallStart) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StreamEvent_ToolCallStart) ProtoMessage() {} - -func (x *StreamEvent_ToolCallStart) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[23] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use StreamEvent_ToolCallStart.ProtoReflect.Descriptor instead. -func (*StreamEvent_ToolCallStart) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{14, 3} -} - -func (x *StreamEvent_ToolCallStart) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *StreamEvent_ToolCallStart) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -// ToolCallDelta carries one incremental fragment of a tool call's -// arguments, accumulated by the kernel across deltas into the final -// parsed JSON. -type StreamEvent_ToolCallDelta struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The id from the matching ToolCallStart. - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - // A partial-JSON fragment of the call's arguments. - ArgumentsFragment string `protobuf:"bytes,2,opt,name=arguments_fragment,json=argumentsFragment,proto3" json:"arguments_fragment,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *StreamEvent_ToolCallDelta) Reset() { - *x = StreamEvent_ToolCallDelta{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[24] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *StreamEvent_ToolCallDelta) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StreamEvent_ToolCallDelta) ProtoMessage() {} - -func (x *StreamEvent_ToolCallDelta) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[24] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use StreamEvent_ToolCallDelta.ProtoReflect.Descriptor instead. -func (*StreamEvent_ToolCallDelta) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{14, 4} -} - -func (x *StreamEvent_ToolCallDelta) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *StreamEvent_ToolCallDelta) GetArgumentsFragment() string { - if x != nil { - return x.ArgumentsFragment - } - return "" -} - -// ToolCallDone signals a tool call's arguments are complete and ready -// for the kernel to parse and dispatch. -type StreamEvent_ToolCallDone struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The id from the matching ToolCallStart. - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *StreamEvent_ToolCallDone) Reset() { - *x = StreamEvent_ToolCallDone{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[25] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *StreamEvent_ToolCallDone) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StreamEvent_ToolCallDone) ProtoMessage() {} - -func (x *StreamEvent_ToolCallDone) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[25] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use StreamEvent_ToolCallDone.ProtoReflect.Descriptor instead. -func (*StreamEvent_ToolCallDone) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{14, 5} -} - -func (x *StreamEvent_ToolCallDone) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -// Usage carries token accounting for this completion, per provider.md -// §4.1. The kernel computes and persists cost_usd from these counts -// plus the matching PricingTier — the plugin never computes cost -// itself. -type StreamEvent_Usage struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Input tokens consumed by this completion. - InputTokens int64 `protobuf:"varint,1,opt,name=input_tokens,json=inputTokens,proto3" json:"input_tokens,omitempty"` - // Output tokens produced by this completion. - OutputTokens int64 `protobuf:"varint,2,opt,name=output_tokens,json=outputTokens,proto3" json:"output_tokens,omitempty"` - // Tokens read from cache, if the model supports caching. Never also - // counted in input_tokens. - CacheReadTokens *int64 `protobuf:"varint,3,opt,name=cache_read_tokens,json=cacheReadTokens,proto3,oneof" json:"cache_read_tokens,omitempty"` - // Tokens written to cache, if the model supports caching. Never also - // counted in input_tokens. - CacheWriteTokens *int64 `protobuf:"varint,4,opt,name=cache_write_tokens,json=cacheWriteTokens,proto3,oneof" json:"cache_write_tokens,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *StreamEvent_Usage) Reset() { - *x = StreamEvent_Usage{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[26] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *StreamEvent_Usage) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StreamEvent_Usage) ProtoMessage() {} - -func (x *StreamEvent_Usage) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[26] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use StreamEvent_Usage.ProtoReflect.Descriptor instead. -func (*StreamEvent_Usage) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{14, 6} -} - -func (x *StreamEvent_Usage) GetInputTokens() int64 { - if x != nil { - return x.InputTokens - } - return 0 -} - -func (x *StreamEvent_Usage) GetOutputTokens() int64 { - if x != nil { - return x.OutputTokens - } - return 0 -} - -func (x *StreamEvent_Usage) GetCacheReadTokens() int64 { - if x != nil && x.CacheReadTokens != nil { - return *x.CacheReadTokens - } - return 0 -} - -func (x *StreamEvent_Usage) GetCacheWriteTokens() int64 { - if x != nil && x.CacheWriteTokens != nil { - return *x.CacheWriteTokens - } - return 0 -} - -// Stop signals the completion has ended. -type StreamEvent_Stop struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Why the completion ended. - Reason StopReason `protobuf:"varint,1,opt,name=reason,proto3,enum=pluggableharness.agent.provider.v1.StopReason" json:"reason,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *StreamEvent_Stop) Reset() { - *x = StreamEvent_Stop{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[27] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *StreamEvent_Stop) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StreamEvent_Stop) ProtoMessage() {} - -func (x *StreamEvent_Stop) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[27] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use StreamEvent_Stop.ProtoReflect.Descriptor instead. -func (*StreamEvent_Stop) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{14, 7} -} - -func (x *StreamEvent_Stop) GetReason() StopReason { - if x != nil { - return x.Reason - } - return StopReason_STOP_REASON_UNSPECIFIED -} - -// Error signals the completion failed. -type StreamEvent_Error struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The structured error, classified per provider.md §8. - Error *ProviderError `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *StreamEvent_Error) Reset() { - *x = StreamEvent_Error{} - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[28] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *StreamEvent_Error) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StreamEvent_Error) ProtoMessage() {} - -func (x *StreamEvent_Error) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[28] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use StreamEvent_Error.ProtoReflect.Descriptor instead. -func (*StreamEvent_Error) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP(), []int{14, 8} -} - -func (x *StreamEvent_Error) GetError() *ProviderError { - if x != nil { - return x.Error - } - return nil -} - -var File_pluggableharness_agent_provider_v1_provider_proto protoreflect.FileDescriptor - -const file_pluggableharness_agent_provider_v1_provider_proto_rawDesc = "" + - "\n" + - "1pluggableharness/agent/provider/v1/provider.proto\x12\"pluggableharness.agent.provider.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a-pluggableharness/agent/config/v1/config.proto\x1a/pluggableharness/agent/content/v1/content.proto\x1a-pluggableharness/agent/render/v1/render.proto\x1a-pluggableharness/agent/schema/v1/schema.proto\x1a9pluggableharness/agent/slashcommand/v1/slashcommand.proto\"\x18\n" + - "\x16GetCapabilitiesRequest\"o\n" + - "\x17GetCapabilitiesResponse\x12T\n" + - "\fcapabilities\x18\x01 \x01(\v20.pluggableharness.agent.provider.v1.CapabilitiesR\fcapabilities\"\x8b\x02\n" + - "\fCapabilities\x12E\n" + - "\x06models\x18\x01 \x03(\v2-.pluggableharness.agent.provider.v1.ModelSpecR\x06models\x12_\n" + - "\x0eslash_commands\x18\x02 \x03(\v28.pluggableharness.agent.slashcommand.v1.SlashCommandSpecR\rslashCommands\x12S\n" + - "\rconfig_schema\x18\x03 \x01(\v2..pluggableharness.agent.config.v1.ConfigSchemaR\fconfigSchema\"C\n" + - "\x10ConfigureRequest\x12/\n" + - "\x06config\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x06config\"\x13\n" + - "\x11ConfigureResponse\"\xb9\x04\n" + - "\tModelSpec\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id\x12%\n" + - "\x0econtext_window\x18\x02 \x01(\x03R\rcontextWindow\x12*\n" + - "\x11max_output_tokens\x18\x03 \x01(\x03R\x0fmaxOutputTokens\x12*\n" + - "\x11supports_tool_use\x18\x04 \x01(\bR\x0fsupportsToolUse\x12'\n" + - "\x0fsupports_vision\x18\x05 \x01(\bR\x0esupportsVision\x12-\n" + - "\x12supports_streaming\x18\x06 \x01(\bR\x11supportsStreaming\x12D\n" + - "\x1csupports_parallel_tool_calls\x18\a \x01(\bH\x00R\x19supportsParallelToolCalls\x88\x01\x01\x12L\n" + - "\bthinking\x18\b \x01(\v20.pluggableharness.agent.provider.v1.ThinkingSpecR\bthinking\x12I\n" + - "\acaching\x18\t \x01(\v2/.pluggableharness.agent.provider.v1.CachingSpecR\acaching\x12E\n" + - "\apricing\x18\n" + - " \x01(\v2+.pluggableharness.agent.provider.v1.PricingR\apricingB\x1f\n" + - "\x1d_supports_parallel_tool_calls\"9\n" + - "\x13ThinkingBudgetRange\x12\x10\n" + - "\x03min\x18\x01 \x01(\x03R\x03min\x12\x10\n" + - "\x03max\x18\x02 \x01(\x03R\x03max\"\xd5\x02\n" + - "\fThinkingSpec\x12\x1c\n" + - "\tsupported\x18\x01 \x01(\bR\tsupported\x12D\n" + - "\x04mode\x18\x02 \x01(\x0e20.pluggableharness.agent.provider.v1.ThinkingModeR\x04mode\x12#\n" + - "\reffort_levels\x18\x03 \x03(\tR\feffortLevels\x12_\n" + - "\fbudget_range\x18\x04 \x01(\v27.pluggableharness.agent.provider.v1.ThinkingBudgetRangeH\x00R\vbudgetRange\x88\x01\x01\x12\x1f\n" + - "\vcan_disable\x18\x05 \x01(\bR\n" + - "canDisable\x12\x1d\n" + - "\adefault\x18\x06 \x01(\tH\x01R\adefault\x88\x01\x01B\x0f\n" + - "\r_budget_rangeB\n" + - "\n" + - "\b_default\"\xa1\x01\n" + - "\vCachingSpec\x12\x1c\n" + - "\tsupported\x18\x01 \x01(\bR\tsupported\x12C\n" + - "\x04mode\x18\x02 \x01(\x0e2/.pluggableharness.agent.provider.v1.CachingModeR\x04mode\x12/\n" + - "\x13keepalive_supported\x18\x03 \x01(\bR\x12keepaliveSupported\"\xd0\x04\n" + - "\vPricingTier\x12F\n" + - "\x0eeffective_from\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampH\x00R\reffectiveFrom\x88\x01\x01\x12H\n" + - "\x0feffective_until\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampH\x01R\x0eeffectiveUntil\x88\x01\x01\x12$\n" + - "\x0einput_per_mtok\x18\x03 \x01(\x01R\finputPerMtok\x12&\n" + - "\x0foutput_per_mtok\x18\x04 \x01(\x01R\routputPerMtok\x124\n" + - "\x14cache_write_per_mtok\x18\x05 \x01(\x01H\x02R\x11cacheWritePerMtok\x88\x01\x01\x122\n" + - "\x13cache_read_per_mtok\x18\x06 \x01(\x01H\x03R\x10cacheReadPerMtok\x88\x01\x01\x124\n" + - "\x14batch_input_per_mtok\x18\a \x01(\x01H\x04R\x11batchInputPerMtok\x88\x01\x01\x126\n" + - "\x15batch_output_per_mtok\x18\b \x01(\x01H\x05R\x12batchOutputPerMtok\x88\x01\x01B\x11\n" + - "\x0f_effective_fromB\x12\n" + - "\x10_effective_untilB\x17\n" + - "\x15_cache_write_per_mtokB\x16\n" + - "\x14_cache_read_per_mtokB\x17\n" + - "\x15_batch_input_per_mtokB\x18\n" + - "\x16_batch_output_per_mtok\"\x80\x01\n" + - "\aPricing\x12\x1a\n" + - "\bcurrency\x18\x01 \x01(\tR\bcurrency\x12\x12\n" + - "\x04free\x18\x02 \x01(\bR\x04free\x12E\n" + - "\x05tiers\x18\x03 \x03(\v2/.pluggableharness.agent.provider.v1.PricingTierR\x05tiers\"\xa5\x02\n" + - "\x17StreamCompletionRequest\x12F\n" + - "\bmessages\x18\x01 \x03(\v2*.pluggableharness.agent.content.v1.MessageR\bmessages\x12\x19\n" + - "\bmodel_id\x18\x02 \x01(\tR\amodelId\x12I\n" + - "\x05tools\x18\x03 \x03(\v23.pluggableharness.agent.provider.v1.ToolDeclarationR\x05tools\x12Q\n" + - "\x06params\x18\x04 \x01(\v24.pluggableharness.agent.provider.v1.GenerationParamsH\x00R\x06params\x88\x01\x01B\t\n" + - "\a_params\"\x94\x01\n" + - "\x0fToolDeclaration\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + - "\vdescription\x18\x02 \x01(\tR\vdescription\x12K\n" + - "\finput_schema\x18\x03 \x01(\v2(.pluggableharness.agent.schema.v1.SchemaR\vinputSchema\"\xf1\x01\n" + - "\x10GenerationParams\x12,\n" + - "\x0fthinking_effort\x18\x01 \x01(\tH\x00R\x0ethinkingEffort\x88\x01\x01\x129\n" + - "\x16thinking_budget_tokens\x18\x02 \x01(\x03H\x01R\x14thinkingBudgetTokens\x88\x01\x01\x12/\n" + - "\x11max_output_tokens\x18\x03 \x01(\x03H\x02R\x0fmaxOutputTokens\x88\x01\x01B\x12\n" + - "\x10_thinking_effortB\x19\n" + - "\x17_thinking_budget_tokensB\x14\n" + - "\x12_max_output_tokens\"\x93\f\n" + - "\vStreamEvent\x12Z\n" + - "\n" + - "text_delta\x18\x01 \x01(\v29.pluggableharness.agent.provider.v1.StreamEvent.TextDeltaH\x00R\ttextDelta\x12f\n" + - "\x0ethinking_delta\x18\x02 \x01(\v2=.pluggableharness.agent.provider.v1.StreamEvent.ThinkingDeltaH\x00R\rthinkingDelta\x12r\n" + - "\x12thinking_signature\x18\x03 \x01(\v2A.pluggableharness.agent.provider.v1.StreamEvent.ThinkingSignatureH\x00R\x11thinkingSignature\x12g\n" + - "\x0ftool_call_start\x18\x04 \x01(\v2=.pluggableharness.agent.provider.v1.StreamEvent.ToolCallStartH\x00R\rtoolCallStart\x12g\n" + - "\x0ftool_call_delta\x18\x05 \x01(\v2=.pluggableharness.agent.provider.v1.StreamEvent.ToolCallDeltaH\x00R\rtoolCallDelta\x12d\n" + - "\x0etool_call_done\x18\x06 \x01(\v2<.pluggableharness.agent.provider.v1.StreamEvent.ToolCallDoneH\x00R\ftoolCallDone\x12M\n" + - "\x05usage\x18\a \x01(\v25.pluggableharness.agent.provider.v1.StreamEvent.UsageH\x00R\x05usage\x12J\n" + - "\x04stop\x18\b \x01(\v24.pluggableharness.agent.provider.v1.StreamEvent.StopH\x00R\x04stop\x12M\n" + - "\x05error\x18\t \x01(\v25.pluggableharness.agent.provider.v1.StreamEvent.ErrorH\x00R\x05error\x1a\x1f\n" + - "\tTextDelta\x12\x12\n" + - "\x04text\x18\x01 \x01(\tR\x04text\x1a#\n" + - "\rThinkingDelta\x12\x12\n" + - "\x04text\x18\x01 \x01(\tR\x04text\x1a1\n" + - "\x11ThinkingSignature\x12\x1c\n" + - "\tsignature\x18\x01 \x01(\fR\tsignature\x1a3\n" + - "\rToolCallStart\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + - "\x04name\x18\x02 \x01(\tR\x04name\x1aN\n" + - "\rToolCallDelta\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id\x12-\n" + - "\x12arguments_fragment\x18\x02 \x01(\tR\x11argumentsFragment\x1a\x1e\n" + - "\fToolCallDone\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id\x1a\xe0\x01\n" + - "\x05Usage\x12!\n" + - "\finput_tokens\x18\x01 \x01(\x03R\vinputTokens\x12#\n" + - "\routput_tokens\x18\x02 \x01(\x03R\foutputTokens\x12/\n" + - "\x11cache_read_tokens\x18\x03 \x01(\x03H\x00R\x0fcacheReadTokens\x88\x01\x01\x121\n" + - "\x12cache_write_tokens\x18\x04 \x01(\x03H\x01R\x10cacheWriteTokens\x88\x01\x01B\x14\n" + - "\x12_cache_read_tokensB\x15\n" + - "\x13_cache_write_tokens\x1aN\n" + - "\x04Stop\x12F\n" + - "\x06reason\x18\x01 \x01(\x0e2..pluggableharness.agent.provider.v1.StopReasonR\x06reason\x1aP\n" + - "\x05Error\x12G\n" + - "\x05error\x18\x01 \x01(\v21.pluggableharness.agent.provider.v1.ProviderErrorR\x05errorB\a\n" + - "\x05event\"(\n" + - "\x12CountTokensRequest\x12\x12\n" + - "\x04text\x18\x01 \x01(\tR\x04text\"+\n" + - "\x13CountTokensResponse\x12\x14\n" + - "\x05count\x18\x01 \x01(\x03R\x05count\")\n" + - "\rRenderRequest\x12\x18\n" + - "\apayload\x18\x01 \x01(\fR\apayload\"R\n" + - "\x0eRenderResponse\x12@\n" + - "\x04tree\x18\x01 \x01(\v2,.pluggableharness.agent.render.v1.RenderTreeR\x04tree\"\xa2\x02\n" + - "\rProviderError\x12U\n" + - "\bcategory\x18\x01 \x01(\x0e29.pluggableharness.agent.provider.v1.ProviderErrorCategoryR\bcategory\x12\x18\n" + - "\amessage\x18\x02 \x01(\tR\amessage\x12\x1c\n" + - "\tretryable\x18\x03 \x01(\bR\tretryable\x12?\n" + - "\vretry_after\x18\x04 \x01(\v2\x19.google.protobuf.DurationH\x00R\n" + - "retryAfter\x88\x01\x01\x12\"\n" + - "\n" + - "raw_detail\x18\x05 \x01(\tH\x01R\trawDetail\x88\x01\x01B\x0e\n" + - "\f_retry_afterB\r\n" + - "\v_raw_detail*\xb3\x01\n" + - "\fThinkingMode\x12\x1d\n" + - "\x19THINKING_MODE_UNSPECIFIED\x10\x00\x12\x16\n" + - "\x12THINKING_MODE_NONE\x10\x01\x12$\n" + - " THINKING_MODE_ALWAYS_ON_ADAPTIVE\x10\x02\x12!\n" + - "\x1dTHINKING_MODE_DISCRETE_EFFORT\x10\x03\x12#\n" + - "\x1fTHINKING_MODE_CONTINUOUS_BUDGET\x10\x04*\x8a\x01\n" + - "\vCachingMode\x12\x1c\n" + - "\x18CACHING_MODE_UNSPECIFIED\x10\x00\x12\x15\n" + - "\x11CACHING_MODE_NONE\x10\x01\x12!\n" + - "\x1dCACHING_MODE_EXPLICIT_MARKERS\x10\x02\x12#\n" + - "\x1fCACHING_MODE_IMPLICIT_AUTOMATIC\x10\x03*\xb6\x01\n" + - "\n" + - "StopReason\x12\x1b\n" + - "\x17STOP_REASON_UNSPECIFIED\x10\x00\x12\x18\n" + - "\x14STOP_REASON_END_TURN\x10\x01\x12\x18\n" + - "\x14STOP_REASON_TOOL_USE\x10\x02\x12\x1a\n" + - "\x16STOP_REASON_MAX_TOKENS\x10\x03\x12 \n" + - "\x1cSTOP_REASON_CONTENT_FILTERED\x10\x04\x12\x19\n" + - "\x15STOP_REASON_CANCELLED\x10\x05*\xef\x02\n" + - "\x15ProviderErrorCategory\x12'\n" + - "#PROVIDER_ERROR_CATEGORY_UNSPECIFIED\x10\x00\x123\n" + - "/PROVIDER_ERROR_CATEGORY_CONTEXT_LENGTH_EXCEEDED\x10\x01\x12(\n" + - "$PROVIDER_ERROR_CATEGORY_RATE_LIMITED\x10\x02\x12&\n" + - "\"PROVIDER_ERROR_CATEGORY_OVERLOADED\x10\x03\x12&\n" + - "\"PROVIDER_ERROR_CATEGORY_AUTH_ERROR\x10\x04\x12+\n" + - "'PROVIDER_ERROR_CATEGORY_INVALID_REQUEST\x10\x05\x12,\n" + - "(PROVIDER_ERROR_CATEGORY_CONTENT_FILTERED\x10\x06\x12#\n" + - "\x1fPROVIDER_ERROR_CATEGORY_UNKNOWN\x10\a2\x8e\x05\n" + - "\x0fProviderService\x12\x8a\x01\n" + - "\x0fGetCapabilities\x12:.pluggableharness.agent.provider.v1.GetCapabilitiesRequest\x1a;.pluggableharness.agent.provider.v1.GetCapabilitiesResponse\x12x\n" + - "\tConfigure\x124.pluggableharness.agent.provider.v1.ConfigureRequest\x1a5.pluggableharness.agent.provider.v1.ConfigureResponse\x12\x82\x01\n" + - "\x10StreamCompletion\x12;.pluggableharness.agent.provider.v1.StreamCompletionRequest\x1a/.pluggableharness.agent.provider.v1.StreamEvent0\x01\x12~\n" + - "\vCountTokens\x126.pluggableharness.agent.provider.v1.CountTokensRequest\x1a7.pluggableharness.agent.provider.v1.CountTokensResponse\x12o\n" + - "\x06Render\x121.pluggableharness.agent.provider.v1.RenderRequest\x1a2.pluggableharness.agent.provider.v1.RenderResponseBDZBgithub.com/pluggableharness/agent/pkg/provider/proto/v1;providerv1b\x06proto3" - -var ( - file_pluggableharness_agent_provider_v1_provider_proto_rawDescOnce sync.Once - file_pluggableharness_agent_provider_v1_provider_proto_rawDescData []byte -) - -func file_pluggableharness_agent_provider_v1_provider_proto_rawDescGZIP() []byte { - file_pluggableharness_agent_provider_v1_provider_proto_rawDescOnce.Do(func() { - file_pluggableharness_agent_provider_v1_provider_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_pluggableharness_agent_provider_v1_provider_proto_rawDesc), len(file_pluggableharness_agent_provider_v1_provider_proto_rawDesc))) - }) - return file_pluggableharness_agent_provider_v1_provider_proto_rawDescData -} - -var file_pluggableharness_agent_provider_v1_provider_proto_enumTypes = make([]protoimpl.EnumInfo, 4) -var file_pluggableharness_agent_provider_v1_provider_proto_msgTypes = make([]protoimpl.MessageInfo, 29) -var file_pluggableharness_agent_provider_v1_provider_proto_goTypes = []any{ - (ThinkingMode)(0), // 0: pluggableharness.agent.provider.v1.ThinkingMode - (CachingMode)(0), // 1: pluggableharness.agent.provider.v1.CachingMode - (StopReason)(0), // 2: pluggableharness.agent.provider.v1.StopReason - (ProviderErrorCategory)(0), // 3: pluggableharness.agent.provider.v1.ProviderErrorCategory - (*GetCapabilitiesRequest)(nil), // 4: pluggableharness.agent.provider.v1.GetCapabilitiesRequest - (*GetCapabilitiesResponse)(nil), // 5: pluggableharness.agent.provider.v1.GetCapabilitiesResponse - (*Capabilities)(nil), // 6: pluggableharness.agent.provider.v1.Capabilities - (*ConfigureRequest)(nil), // 7: pluggableharness.agent.provider.v1.ConfigureRequest - (*ConfigureResponse)(nil), // 8: pluggableharness.agent.provider.v1.ConfigureResponse - (*ModelSpec)(nil), // 9: pluggableharness.agent.provider.v1.ModelSpec - (*ThinkingBudgetRange)(nil), // 10: pluggableharness.agent.provider.v1.ThinkingBudgetRange - (*ThinkingSpec)(nil), // 11: pluggableharness.agent.provider.v1.ThinkingSpec - (*CachingSpec)(nil), // 12: pluggableharness.agent.provider.v1.CachingSpec - (*PricingTier)(nil), // 13: pluggableharness.agent.provider.v1.PricingTier - (*Pricing)(nil), // 14: pluggableharness.agent.provider.v1.Pricing - (*StreamCompletionRequest)(nil), // 15: pluggableharness.agent.provider.v1.StreamCompletionRequest - (*ToolDeclaration)(nil), // 16: pluggableharness.agent.provider.v1.ToolDeclaration - (*GenerationParams)(nil), // 17: pluggableharness.agent.provider.v1.GenerationParams - (*StreamEvent)(nil), // 18: pluggableharness.agent.provider.v1.StreamEvent - (*CountTokensRequest)(nil), // 19: pluggableharness.agent.provider.v1.CountTokensRequest - (*CountTokensResponse)(nil), // 20: pluggableharness.agent.provider.v1.CountTokensResponse - (*RenderRequest)(nil), // 21: pluggableharness.agent.provider.v1.RenderRequest - (*RenderResponse)(nil), // 22: pluggableharness.agent.provider.v1.RenderResponse - (*ProviderError)(nil), // 23: pluggableharness.agent.provider.v1.ProviderError - (*StreamEvent_TextDelta)(nil), // 24: pluggableharness.agent.provider.v1.StreamEvent.TextDelta - (*StreamEvent_ThinkingDelta)(nil), // 25: pluggableharness.agent.provider.v1.StreamEvent.ThinkingDelta - (*StreamEvent_ThinkingSignature)(nil), // 26: pluggableharness.agent.provider.v1.StreamEvent.ThinkingSignature - (*StreamEvent_ToolCallStart)(nil), // 27: pluggableharness.agent.provider.v1.StreamEvent.ToolCallStart - (*StreamEvent_ToolCallDelta)(nil), // 28: pluggableharness.agent.provider.v1.StreamEvent.ToolCallDelta - (*StreamEvent_ToolCallDone)(nil), // 29: pluggableharness.agent.provider.v1.StreamEvent.ToolCallDone - (*StreamEvent_Usage)(nil), // 30: pluggableharness.agent.provider.v1.StreamEvent.Usage - (*StreamEvent_Stop)(nil), // 31: pluggableharness.agent.provider.v1.StreamEvent.Stop - (*StreamEvent_Error)(nil), // 32: pluggableharness.agent.provider.v1.StreamEvent.Error - (*v1.SlashCommandSpec)(nil), // 33: pluggableharness.agent.slashcommand.v1.SlashCommandSpec - (*v11.ConfigSchema)(nil), // 34: pluggableharness.agent.config.v1.ConfigSchema - (*structpb.Struct)(nil), // 35: google.protobuf.Struct - (*timestamppb.Timestamp)(nil), // 36: google.protobuf.Timestamp - (*v12.Message)(nil), // 37: pluggableharness.agent.content.v1.Message - (*v13.Schema)(nil), // 38: pluggableharness.agent.schema.v1.Schema - (*v14.RenderTree)(nil), // 39: pluggableharness.agent.render.v1.RenderTree - (*durationpb.Duration)(nil), // 40: google.protobuf.Duration -} -var file_pluggableharness_agent_provider_v1_provider_proto_depIdxs = []int32{ - 6, // 0: pluggableharness.agent.provider.v1.GetCapabilitiesResponse.capabilities:type_name -> pluggableharness.agent.provider.v1.Capabilities - 9, // 1: pluggableharness.agent.provider.v1.Capabilities.models:type_name -> pluggableharness.agent.provider.v1.ModelSpec - 33, // 2: pluggableharness.agent.provider.v1.Capabilities.slash_commands:type_name -> pluggableharness.agent.slashcommand.v1.SlashCommandSpec - 34, // 3: pluggableharness.agent.provider.v1.Capabilities.config_schema:type_name -> pluggableharness.agent.config.v1.ConfigSchema - 35, // 4: pluggableharness.agent.provider.v1.ConfigureRequest.config:type_name -> google.protobuf.Struct - 11, // 5: pluggableharness.agent.provider.v1.ModelSpec.thinking:type_name -> pluggableharness.agent.provider.v1.ThinkingSpec - 12, // 6: pluggableharness.agent.provider.v1.ModelSpec.caching:type_name -> pluggableharness.agent.provider.v1.CachingSpec - 14, // 7: pluggableharness.agent.provider.v1.ModelSpec.pricing:type_name -> pluggableharness.agent.provider.v1.Pricing - 0, // 8: pluggableharness.agent.provider.v1.ThinkingSpec.mode:type_name -> pluggableharness.agent.provider.v1.ThinkingMode - 10, // 9: pluggableharness.agent.provider.v1.ThinkingSpec.budget_range:type_name -> pluggableharness.agent.provider.v1.ThinkingBudgetRange - 1, // 10: pluggableharness.agent.provider.v1.CachingSpec.mode:type_name -> pluggableharness.agent.provider.v1.CachingMode - 36, // 11: pluggableharness.agent.provider.v1.PricingTier.effective_from:type_name -> google.protobuf.Timestamp - 36, // 12: pluggableharness.agent.provider.v1.PricingTier.effective_until:type_name -> google.protobuf.Timestamp - 13, // 13: pluggableharness.agent.provider.v1.Pricing.tiers:type_name -> pluggableharness.agent.provider.v1.PricingTier - 37, // 14: pluggableharness.agent.provider.v1.StreamCompletionRequest.messages:type_name -> pluggableharness.agent.content.v1.Message - 16, // 15: pluggableharness.agent.provider.v1.StreamCompletionRequest.tools:type_name -> pluggableharness.agent.provider.v1.ToolDeclaration - 17, // 16: pluggableharness.agent.provider.v1.StreamCompletionRequest.params:type_name -> pluggableharness.agent.provider.v1.GenerationParams - 38, // 17: pluggableharness.agent.provider.v1.ToolDeclaration.input_schema:type_name -> pluggableharness.agent.schema.v1.Schema - 24, // 18: pluggableharness.agent.provider.v1.StreamEvent.text_delta:type_name -> pluggableharness.agent.provider.v1.StreamEvent.TextDelta - 25, // 19: pluggableharness.agent.provider.v1.StreamEvent.thinking_delta:type_name -> pluggableharness.agent.provider.v1.StreamEvent.ThinkingDelta - 26, // 20: pluggableharness.agent.provider.v1.StreamEvent.thinking_signature:type_name -> pluggableharness.agent.provider.v1.StreamEvent.ThinkingSignature - 27, // 21: pluggableharness.agent.provider.v1.StreamEvent.tool_call_start:type_name -> pluggableharness.agent.provider.v1.StreamEvent.ToolCallStart - 28, // 22: pluggableharness.agent.provider.v1.StreamEvent.tool_call_delta:type_name -> pluggableharness.agent.provider.v1.StreamEvent.ToolCallDelta - 29, // 23: pluggableharness.agent.provider.v1.StreamEvent.tool_call_done:type_name -> pluggableharness.agent.provider.v1.StreamEvent.ToolCallDone - 30, // 24: pluggableharness.agent.provider.v1.StreamEvent.usage:type_name -> pluggableharness.agent.provider.v1.StreamEvent.Usage - 31, // 25: pluggableharness.agent.provider.v1.StreamEvent.stop:type_name -> pluggableharness.agent.provider.v1.StreamEvent.Stop - 32, // 26: pluggableharness.agent.provider.v1.StreamEvent.error:type_name -> pluggableharness.agent.provider.v1.StreamEvent.Error - 39, // 27: pluggableharness.agent.provider.v1.RenderResponse.tree:type_name -> pluggableharness.agent.render.v1.RenderTree - 3, // 28: pluggableharness.agent.provider.v1.ProviderError.category:type_name -> pluggableharness.agent.provider.v1.ProviderErrorCategory - 40, // 29: pluggableharness.agent.provider.v1.ProviderError.retry_after:type_name -> google.protobuf.Duration - 2, // 30: pluggableharness.agent.provider.v1.StreamEvent.Stop.reason:type_name -> pluggableharness.agent.provider.v1.StopReason - 23, // 31: pluggableharness.agent.provider.v1.StreamEvent.Error.error:type_name -> pluggableharness.agent.provider.v1.ProviderError - 4, // 32: pluggableharness.agent.provider.v1.ProviderService.GetCapabilities:input_type -> pluggableharness.agent.provider.v1.GetCapabilitiesRequest - 7, // 33: pluggableharness.agent.provider.v1.ProviderService.Configure:input_type -> pluggableharness.agent.provider.v1.ConfigureRequest - 15, // 34: pluggableharness.agent.provider.v1.ProviderService.StreamCompletion:input_type -> pluggableharness.agent.provider.v1.StreamCompletionRequest - 19, // 35: pluggableharness.agent.provider.v1.ProviderService.CountTokens:input_type -> pluggableharness.agent.provider.v1.CountTokensRequest - 21, // 36: pluggableharness.agent.provider.v1.ProviderService.Render:input_type -> pluggableharness.agent.provider.v1.RenderRequest - 5, // 37: pluggableharness.agent.provider.v1.ProviderService.GetCapabilities:output_type -> pluggableharness.agent.provider.v1.GetCapabilitiesResponse - 8, // 38: pluggableharness.agent.provider.v1.ProviderService.Configure:output_type -> pluggableharness.agent.provider.v1.ConfigureResponse - 18, // 39: pluggableharness.agent.provider.v1.ProviderService.StreamCompletion:output_type -> pluggableharness.agent.provider.v1.StreamEvent - 20, // 40: pluggableharness.agent.provider.v1.ProviderService.CountTokens:output_type -> pluggableharness.agent.provider.v1.CountTokensResponse - 22, // 41: pluggableharness.agent.provider.v1.ProviderService.Render:output_type -> pluggableharness.agent.provider.v1.RenderResponse - 37, // [37:42] is the sub-list for method output_type - 32, // [32:37] is the sub-list for method input_type - 32, // [32:32] is the sub-list for extension type_name - 32, // [32:32] is the sub-list for extension extendee - 0, // [0:32] is the sub-list for field type_name -} - -func init() { file_pluggableharness_agent_provider_v1_provider_proto_init() } -func file_pluggableharness_agent_provider_v1_provider_proto_init() { - if File_pluggableharness_agent_provider_v1_provider_proto != nil { - return - } - file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[5].OneofWrappers = []any{} - file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[7].OneofWrappers = []any{} - file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[9].OneofWrappers = []any{} - file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[11].OneofWrappers = []any{} - file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[13].OneofWrappers = []any{} - file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[14].OneofWrappers = []any{ - (*StreamEvent_TextDelta_)(nil), - (*StreamEvent_ThinkingDelta_)(nil), - (*StreamEvent_ThinkingSignature_)(nil), - (*StreamEvent_ToolCallStart_)(nil), - (*StreamEvent_ToolCallDelta_)(nil), - (*StreamEvent_ToolCallDone_)(nil), - (*StreamEvent_Usage_)(nil), - (*StreamEvent_Stop_)(nil), - (*StreamEvent_Error_)(nil), - } - file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[19].OneofWrappers = []any{} - file_pluggableharness_agent_provider_v1_provider_proto_msgTypes[26].OneofWrappers = []any{} - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_pluggableharness_agent_provider_v1_provider_proto_rawDesc), len(file_pluggableharness_agent_provider_v1_provider_proto_rawDesc)), - NumEnums: 4, - NumMessages: 29, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_pluggableharness_agent_provider_v1_provider_proto_goTypes, - DependencyIndexes: file_pluggableharness_agent_provider_v1_provider_proto_depIdxs, - EnumInfos: file_pluggableharness_agent_provider_v1_provider_proto_enumTypes, - MessageInfos: file_pluggableharness_agent_provider_v1_provider_proto_msgTypes, - }.Build() - File_pluggableharness_agent_provider_v1_provider_proto = out.File - file_pluggableharness_agent_provider_v1_provider_proto_goTypes = nil - file_pluggableharness_agent_provider_v1_provider_proto_depIdxs = nil -} diff --git a/pkg/render/proto/v1/render.pb.go b/pkg/render/proto/v1/render.pb.go index b0ca63e..e673a26 100644 --- a/pkg/render/proto/v1/render.pb.go +++ b/pkg/render/proto/v1/render.pb.go @@ -6,7 +6,7 @@ // Package pluggableharness.agent.render.v1 defines the Emit->Render->Paint intermediate // representation described in specifications/frontend.md §1. Every plugin -// category's optional Render() RPC (provider.md §7, tool.md §7, +// category's optional Render() RPC (model.md §7, tool.md §7, // context.md §9, memory.md §10) returns this; every frontend/widget Paints // it. frontend.md §1 MUST: a frontend must render every RenderNode variant // gracefully with a generic fallback (e.g. an unrecognized-in-practice diff --git a/pkg/schema/proto/v1/schema.pb.go b/pkg/schema/proto/v1/schema.pb.go index 0f8617c..93e8b03 100644 --- a/pkg/schema/proto/v1/schema.pb.go +++ b/pkg/schema/proto/v1/schema.pb.go @@ -5,9 +5,9 @@ // source: pluggableharness/agent/schema/v1/schema.proto // Package pluggableharness.agent.schema.v1 defines the restricted JSON-Schema subset -// described in specifications/provider.md §6, shared by tool input/output +// described in specifications/model.md §6, shared by tool input/output // schemas (specifications/tool.md §2) and model tool-calling declarations -// (specifications/provider.md §6). Deliberately NOT full JSON Schema: no +// (specifications/model.md §6). Deliberately NOT full JSON Schema: no // oneOf/anyOf/allOf, no $ref, no pattern, no format, no non-trivial // additionalProperties. Every adapter across every category MUST support // exactly this subset — see .claude/rules/proto.md. @@ -33,7 +33,7 @@ const ( // dedicated ENUM value here: JSON Schema's `enum` keyword is a value // constraint, not a distinct type, and in this subset it applies to a // STRING-typed node via Schema.enum_values (the overwhelmingly common -// case for tool argument enums) — see provider.md §6. +// case for tool argument enums) — see model.md §6. type SchemaType int32 const ( @@ -45,7 +45,7 @@ const ( // A JSON string. SchemaType_SCHEMA_TYPE_STRING SchemaType = 2 // A JSON number (integer or floating point — the subset does not - // distinguish the two, per provider.md §6). + // distinguish the two, per model.md §6). SchemaType_SCHEMA_TYPE_NUMBER SchemaType = 3 // A JSON boolean. SchemaType_SCHEMA_TYPE_BOOLEAN SchemaType = 4 @@ -104,7 +104,7 @@ func (SchemaType) EnumDescriptor() ([]byte, []int) { // self-recursive: an OBJECT node's properties and an ARRAY node's items // are themselves Schema nodes. This message is used both as a tool's // input_schema/output_schema (tool.md §2) and embedded in a model -// provider's tool-calling declarations (provider.md §6) — the same wire +// provider's tool-calling declarations (model.md §6) — the same wire // type in both places, by design, so a kernel-side validator has exactly // one Schema implementation to maintain. type Schema struct { diff --git a/pkg/slashcommand/proto/v1/slashcommand.pb.go b/pkg/slashcommand/proto/v1/slashcommand.pb.go index 5451bf5..341b783 100644 --- a/pkg/slashcommand/proto/v1/slashcommand.pb.go +++ b/pkg/slashcommand/proto/v1/slashcommand.pb.go @@ -7,7 +7,7 @@ // Package pluggableharness.agent.slashcommand.v1 defines the slash-command declaration // shape described in specifications/configuration.md §5 (and equivalently // specifications/frontend.md §5). Declarable as an optional repeated field -// in every category's capability response: provider.md §2 Capabilities, +// in every category's capability response: model.md §2 Capabilities, // tool.md §2 GetSchemaResponse, context.md §2 ContextCapabilities, // memory.md §3 MemoryCapabilities. diff --git a/pkg/tool/proto/v1/tool.pb.go b/pkg/tool/proto/v1/tool.pb.go index dfe589d..74ed8f7 100644 --- a/pkg/tool/proto/v1/tool.pb.go +++ b/pkg/tool/proto/v1/tool.pb.go @@ -228,7 +228,7 @@ func (OutputStream) EnumDescriptor() ([]byte, []int) { } // ToolErrorCategory classifies why an Invoke call failed, per tool.md §8. -// Deliberately distinct from provider.md §8's ProviderErrorCategory — there +// Deliberately distinct from model.md §8's ModelErrorCategory — there // is no RATE_LIMITED or CONTEXT_LENGTH_EXCEEDED here, those are // model-vendor concepts. The two enums MUST NOT be merged even though the // surrounding error-envelope shape (category/message/retryable) is @@ -595,10 +595,10 @@ type ToolSchema struct { Risk RiskClass `protobuf:"varint,3,opt,name=risk,proto3,enum=pluggableharness.agent.tool.v1.RiskClass" json:"risk,omitempty"` // MUST — shown to the model for tool selection and in plan diffs. Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` - // MUST — the common JSON-Schema subset per provider.md §6, describing the + // MUST — the common JSON-Schema subset per model.md §6, describing the // shape of ToolCall.arguments for this operation. InputSchema *v12.Schema `protobuf:"bytes,5,opt,name=input_schema,json=inputSchema,proto3" json:"input_schema,omitempty"` - // MUST — the common JSON-Schema subset per provider.md §6, describing the + // MUST — the common JSON-Schema subset per model.md §6, describing the // shape of ToolResult.payload for this operation. OutputSchema *v12.Schema `protobuf:"bytes,6,opt,name=output_schema,json=outputSchema,proto3" json:"output_schema,omitempty"` // MUST — true if Invoke may emit intermediate ToolEvents (output_chunk, diff --git a/pkg/tool/proto/v1/tool_grpc.pb.go b/pkg/tool/proto/v1/tool_grpc.pb.go index 052883a..e251592 100644 --- a/pkg/tool/proto/v1/tool_grpc.pb.go +++ b/pkg/tool/proto/v1/tool_grpc.pb.go @@ -50,7 +50,7 @@ type ToolServiceClient interface { // an in-band field on ConfigureResponse. Configure(ctx context.Context, in *ConfigureRequest, opts ...grpc.CallOption) (*ConfigureResponse, error) // Invoke executes one tool call and streams back its events, per - // tool.md §4. Server-streaming, reusing provider.md §1's shape verbatim — + // tool.md §4. Server-streaming, reusing model.md §1's shape verbatim — // a non-incremental operation MUST still implement this shape, emitting // exactly one terminal `result` or `error`. Cancellation is the kernel // closing the gRPC stream; the plugin MUST treat this as normal control @@ -139,7 +139,7 @@ type ToolServiceServer interface { // an in-band field on ConfigureResponse. Configure(context.Context, *ConfigureRequest) (*ConfigureResponse, error) // Invoke executes one tool call and streams back its events, per - // tool.md §4. Server-streaming, reusing provider.md §1's shape verbatim — + // tool.md §4. Server-streaming, reusing model.md §1's shape verbatim — // a non-incremental operation MUST still implement this shape, emitting // exactly one terminal `result` or `error`. Cancellation is the kernel // closing the gRPC stream; the plugin MUST treat this as normal control From 82a76a41ad0d1bc94312bf7163ff81587cd592b4 Mon Sep 17 00:00:00 2001 From: Steven Crothers Date: Fri, 24 Jul 2026 13:21:36 -0400 Subject: [PATCH 3/7] Add hook/v1 surface and shared foundation types New hook.v1 package: HookSubscriberService with unary DispatchHook, eight typed hook-point payloads (context-assemble stays on ContextService.Contribute), observe/transform/veto modes, HookDecision, and the HookError taxonomy. Veto-mode subscription is open to any plugin declared in agent.hcl; errors and timeouts fail closed to deny. Shared foundations: ApplyResult/ApplyItem/ApplyOutcome in plan.v1; ContextSection and Stability relocated to content.v1 (shared by the context protocol and the forthcoming model-request context field); CallContext in common.v1; EVENT_KIND_HOOK_ERROR plus RunSessionResult aggregate cost/usage in kernel.v1; ConfigAttribute nested object schema and declared defaults in config.v1. Specs updated fix-forward; statebackend maps the new event kind; stubs regenerated. --- .../agent/common/v1/common.proto | 20 + .../agent/config/v1/config.proto | 31 +- .../agent/content/v1/content.proto | 61 + .../agent/context/v1/context.proto | 64 +- api/pluggableharness/agent/hook/v1/hook.proto | 393 ++++ .../agent/kernel/v1/kernel.proto | 30 + api/pluggableharness/agent/plan/v1/plan.proto | 67 + docs/specifications/agent-loop/README.md | 2 +- docs/specifications/agent-loop/conformance.md | 2 +- .../agent-loop/hook-dispatch.md | 69 +- docs/specifications/agent-loop/subagents.md | 8 + docs/specifications/architecture.md | 6 +- .../configuration/blocks-reference.md | 33 +- .../specifications/configuration/lock-file.md | 4 + docs/specifications/context/data-types.md | 2 + docs/specifications/kernel-callbacks.md | 4 +- docs/specifications/state-backend.md | 11 +- internal/statebackend/event.go | 1 + internal/statebackend/event_test.go | 17 + pkg/common/proto/v1/common.pb.go | 84 +- pkg/config/proto/v1/config.pb.go | 66 +- pkg/content/proto/v1/content.pb.go | 247 ++- pkg/context/proto/v1/context.pb.go | 361 +--- pkg/context/proto/v1/context_grpc.pb.go | 7 + pkg/hook/proto/v1/hook.pb.go | 1737 +++++++++++++++++ pkg/hook/proto/v1/hook_grpc.pb.go | 175 ++ pkg/kernel/proto/v1/kernel.pb.go | 88 +- pkg/plan/proto/v1/plan.pb.go | 321 ++- 28 files changed, 3497 insertions(+), 414 deletions(-) create mode 100644 api/pluggableharness/agent/hook/v1/hook.proto create mode 100644 pkg/hook/proto/v1/hook.pb.go create mode 100644 pkg/hook/proto/v1/hook_grpc.pb.go diff --git a/api/pluggableharness/agent/common/v1/common.proto b/api/pluggableharness/agent/common/v1/common.proto index 228fcf3..979f9b4 100644 --- a/api/pluggableharness/agent/common/v1/common.proto +++ b/api/pluggableharness/agent/common/v1/common.proto @@ -98,3 +98,23 @@ message ProviderRef { // not globally — matches ProducerRef.name's uniqueness scope. string name = 2; } + +// CallContext identifies the session, turn, and working directory a given +// RPC call executes for. Attached to a model provider's +// StreamCompletionRequest and a tool provider's ToolCall in this protocol +// revision (forthcoming in those files) — it's what a plugin passes back +// on its own KernelCallbackService.Emit call (kernel-callbacks.md §Emit) +// for correlation, without having to separately thread session_id/turn_id +// through every call site by hand. +message CallContext { + // The session this call executes for. ULID-formatted, per this file's + // ID conventions above (§"ID and timestamp conventions") — the same + // session_id a plugin passes to Emit. + string session_id = 1; + + // The turn within that session this call executes for. ULID-formatted. + string turn_id = 2; + + // The session's working directory at call time. + string working_directory = 3; +} diff --git a/api/pluggableharness/agent/config/v1/config.proto b/api/pluggableharness/agent/config/v1/config.proto index 0f0808e..a268863 100644 --- a/api/pluggableharness/agent/config/v1/config.proto +++ b/api/pluggableharness/agent/config/v1/config.proto @@ -11,8 +11,10 @@ package pluggableharness.agent.config.v1; option go_package = "github.com/pluggableharness/agent/pkg/config/proto/v1;configv1"; // AttrType is the small subset of HCL/cty attribute types a provider's -// config schema may use in v1. No nested blocks — configuration.md §4 -// deliberately keeps this flat. +// config schema may use in v1. ATTR_TYPE_OBJECT is the one type with +// structure below the scalar/list/map level — ConfigAttribute.object_attributes +// carries its nested schema, recursively, rather than accepting an +// unvalidated dynamic object (configuration.md §The schema-to-cty bridge). enum AttrType { // Zero value. Never valid for a real attribute; its presence on the wire // means a caller forgot to set the field. @@ -51,6 +53,31 @@ message ConfigAttribute { // Human-readable description, shown wherever agent.hcl schema is // surfaced to an operator (docs generation, validation errors). string description = 5; + + // The nested attribute schema for this attribute's object shape. MUST + // be set (non-empty) iff type == ATTR_TYPE_OBJECT; MUST be empty for + // every other type. configuration.md's schema-to-cty bridge decodes + // this the same way it decodes the provider's own top-level + // ConfigSchema.attributes, so an object attribute's fields get the same + // required/sensitive/description treatment as any top-level attribute. + // One level of nesting is sanctioned per direct ConfigAttribute; deeper + // nesting is expressed by an entry in object_attributes itself being + // type == ATTR_TYPE_OBJECT with its own populated object_attributes — + // the schema is recursive, not flat-capped. + repeated ConfigAttribute object_attributes = 6; + + // A JSON-encoded default value applied when this attribute is optional + // (required == false) and agent.hcl omits it. Absent means "no default + // — an omitted optional attribute decodes to that type's cty zero + // value." String-typed (rather than a typed field per AttrType, or a + // google.protobuf.Struct/Value) to stay cty-agnostic at the proto + // level: the kernel's schema-to-cty bridge is the only thing that + // interprets this string, parsing it as JSON and converting the result + // to the cty.Value this attribute's type expects — the wire type + // itself doesn't need to model cty's type system to carry a default + // through it. A default for an ATTR_TYPE_OBJECT attribute is a + // JSON object matching object_attributes' shape. + optional string default_json = 7; } // ConfigSchema is a provider's complete config-schema advertisement, diff --git a/api/pluggableharness/agent/content/v1/content.proto b/api/pluggableharness/agent/content/v1/content.proto index 4c4cf50..f5c0035 100644 --- a/api/pluggableharness/agent/content/v1/content.proto +++ b/api/pluggableharness/agent/content/v1/content.proto @@ -8,6 +8,15 @@ syntax = "proto3"; // invents a competing message representation. Also consumed by // context.md's conversation_history/rewritten_history (§5.1), memory.md's // record content, and kernel-callbacks.md's CountTokensRequest. +// +// This package is also the shared home for ContextSection and Stability +// (below) — the prompt-assembly section chain that context.md's +// ContextService.Contribute produces and consumes, and that the model +// provider's completion request will carry (forthcoming). Both types live +// here, not in context.v1, because a section is fundamentally a content +// shape (a labeled, stability-tagged list of ContentBlocks) that a second +// consumer besides the context protocol needs to reference without +// depending on the whole context.v1 package. package pluggableharness.agent.content.v1; import "google/protobuf/struct.proto"; @@ -139,3 +148,55 @@ message RedactedThinkingBlock { // Opaque, vendor-encrypted bytes. The kernel never inspects this. bytes data = 1; } + +// Stability hints whether a ContextSection's content changes turn to turn, +// used both as a context provider's ContextCapabilities-level declaration +// (pluggableharness.agent.context.v1.ContextCapabilities.stability) and +// per ContextSection below. context.md §7: this is a direct translation of +// the research's strongest cross-cutting finding — harnesses converge on a +// tools -> system -> static-project-context -> conversation-tail prefix +// ordering because it is a constraint, not a preference, for prompt-cache +// reuse. +enum Stability { + // Zero value. Never valid for a real capability or section declaration; + // its presence on the wire means a caller forgot to set the field. + STABILITY_UNSPECIFIED = 0; + // Content that doesn't change turn to turn for the life of the session, + // e.g. a repo's CLAUDE.md. + STABILITY_STATIC = 1; + // Content that's recomputed per turn, e.g. git status or a file tree. + STABILITY_DYNAMIC = 2; +} + +// ContextSection is one provider's contribution to the assembled prompt +// context. context.md §4, §7. +message ContextSection { + // The producing plugin's declared name — a plain string, not a + // common.v1.ProducerRef, used as the identity key for a provider + // re-finding and replacing its own prior section. + string provider = 1; + + // Human-readable label the kernel uses to wrap this section in a clearly + // delimited boundary when concatenating the chain into the final prompt. + // MUST be set. context.md §4, §7. + string label = 2; + + // The section's content, in emission order. MUST be set. Text-only in + // v1 — the kernel MUST reject a non-text block here rather than silently + // dropping it. context.md §4, §7, §11. + repeated ContentBlock content = 3; + + // This section's token count, computed via the kernel's CountTokens + // callback (kernel-callbacks.md §2), never a provider-local heuristic. + // MUST be set. context.md §4. + int64 tokens = 4; + + // Whether this section's content changes turn to turn. context.md §7. + Stability stability = 5; + + // Whether this section was truncated to fit its budget. Per context.md + // §6, setting this true is not itself sufficient to satisfy the budget + // constraint — a section that still exceeds token_budget MUST be + // rejected by the kernel regardless of this flag. + bool truncated = 6; +} diff --git a/api/pluggableharness/agent/context/v1/context.proto b/api/pluggableharness/agent/context/v1/context.proto index c46ff5c..a829f44 100644 --- a/api/pluggableharness/agent/context/v1/context.proto +++ b/api/pluggableharness/agent/context/v1/context.proto @@ -5,6 +5,13 @@ syntax = "proto3"; // context-assemble and contribute content to the prompt before each model // call (e.g. a CLAUDE.md reader, an AGENTS.md reader, a git-status/file-tree // summarizer). See .claude/rules/proto.md. +// +// ContextSection and Stability — the section chain this protocol assembles +// and the turn-to-turn-change hint each section carries — are defined in +// pluggableharness.agent.content.v1, not here: that chain is consumed by +// both this protocol and the model provider's completion request +// (forthcoming), so it's homed alongside content.v1's other shared content +// shapes rather than duplicated or owned by only one consumer. package pluggableharness.agent.context.v1; import "google/protobuf/struct.proto"; @@ -80,24 +87,6 @@ message ConfigureRequest { google.protobuf.Struct config = 1; } -// Stability hints whether a ContextSection's content changes turn to turn, -// used both as ContextCapabilities' provider-wide declaration and per -// ContextSection. context.md §7: this is a direct translation of the -// research's strongest cross-cutting finding — harnesses converge on a -// tools -> system -> static-project-context -> conversation-tail prefix -// ordering because it is a constraint, not a preference, for prompt-cache -// reuse. -enum Stability { - // Zero value. Never valid for a real capability or section declaration; - // its presence on the wire means a caller forgot to set the field. - STABILITY_UNSPECIFIED = 0; - // Content that doesn't change turn to turn for the life of the session, - // e.g. a repo's CLAUDE.md. - STABILITY_STATIC = 1; - // Content that's recomputed per turn, e.g. git status or a file tree. - STABILITY_DYNAMIC = 2; -} - // ContextCapabilities reports a context provider's static properties. // context.md §2. stability and compactor MUST be re-queryable cheaply and // MUST NOT depend on a live read of the content source — a provider reading @@ -110,7 +99,7 @@ message ContextCapabilities { // Whether this provider's contributed content changes turn to turn. MUST // be set. context.md §2, §7. - Stability stability = 2; + pluggableharness.agent.content.v1.Stability stability = 2; // Whether this provider acts as a compactor: MAY rewrite, merge, or drop // other providers' sections in the chain it receives, and MAY receive @@ -167,7 +156,7 @@ message ContextRequest { // The accumulated output of earlier providers in this hook's // declaration-order chain. MUST be set (MAY be empty on the first // provider in the chain). context.md §4, §5. - repeated ContextSection prior_sections = 8; + repeated pluggableharness.agent.content.v1.ContextSection prior_sections = 8; // The session's conversation history. Populated ONLY for a provider whose // ContextCapabilities.compactor == true; a non-compactor provider MUST @@ -177,46 +166,13 @@ message ContextRequest { repeated pluggableharness.agent.content.v1.Message conversation_history = 9; } -// ContextSection is one provider's contribution to the assembled prompt -// context. context.md §4, §7. -message ContextSection { - // The producing plugin's declared name — a plain string, not a - // common.v1.ProducerRef, used as the identity key for a provider - // re-finding and replacing its own prior section. - string provider = 1; - - // Human-readable label the kernel uses to wrap this section in a clearly - // delimited boundary when concatenating the chain into the final prompt. - // MUST be set. context.md §4, §7. - string label = 2; - - // The section's content, in emission order. MUST be set. Text-only in - // v1 — the kernel MUST reject a non-text block here rather than silently - // dropping it. context.md §4, §7, §11. - repeated pluggableharness.agent.content.v1.ContentBlock content = 3; - - // This section's token count, computed via the kernel's CountTokens - // callback (kernel-callbacks.md §2), never a provider-local heuristic. - // MUST be set. context.md §4. - int64 tokens = 4; - - // Whether this section's content changes turn to turn. context.md §7. - Stability stability = 5; - - // Whether this section was truncated to fit its budget. Per context.md - // §6, setting this true is not itself sufficient to satisfy the budget - // constraint — a section that still exceeds token_budget MUST be - // rejected by the kernel regardless of this flag. - bool truncated = 6; -} - // ContextContribution is Contribute's response: the full, possibly-modified // section chain, with this provider's own section appended — never a // delta. context.md §4. message ContextContribution { // The full accumulated chain, in declaration order, including this // provider's own new or updated section(s). - repeated ContextSection sections = 1; + repeated pluggableharness.agent.content.v1.ContextSection sections = 1; // The session's conversation history, rewritten to replace what was sent // in ContextRequest.conversation_history. MAY be included by a compactor diff --git a/api/pluggableharness/agent/hook/v1/hook.proto b/api/pluggableharness/agent/hook/v1/hook.proto new file mode 100644 index 0000000..2566e40 --- /dev/null +++ b/api/pluggableharness/agent/hook/v1/hook.proto @@ -0,0 +1,393 @@ +syntax = "proto3"; + +// Package pluggableharness.agent.hook.v1 defines the hook-dispatch RPC surface +// described in agent-loop/hook-dispatch.md and architecture.md §Hook +// dispatch semantics: the wire contract the kernel uses to invoke any +// plugin (of any of the six categories) that declares a `hook{}` block in +// agent.hcl. This is deliberately one shared service rather than a +// per-category RPC — hashicorp/go-plugin (.claude/rules/plugin-runtime.md) +// muxes multiple gRPC services over one broker connection, so the kernel +// dials HookSubscriberService on the same subprocess that already serves +// that plugin's own category service. A plugin with no `hook{}` blocks in +// agent.hcl simply never has it called. +// +// context-assemble is deliberately absent from this surface's HookPoint +// enum below — it stays on ContextService.Contribute +// (context/protocol.md#contribute-the-context-assemble-rpc; architecture.md +// §Hook dispatch semantics), which already carries the full accumulated +// ContextSection chain and doesn't need a second, competing dispatch path. +// This surface serves the other eight hook points only. +package pluggableharness.agent.hook.v1; + +import "pluggableharness/agent/common/v1/common.proto"; +import "pluggableharness/agent/content/v1/content.proto"; +import "pluggableharness/agent/model/v1/model.proto"; +import "pluggableharness/agent/plan/v1/plan.proto"; +import "pluggableharness/agent/session/v1/session.proto"; +import "pluggableharness/agent/tool/v1/tool.proto"; + +option go_package = "github.com/pluggableharness/agent/pkg/hook/proto/v1;hookv1"; + +// HookSubscriberService is the hook-dispatch protocol described in +// agent-loop/hook-dispatch.md: the kernel invokes a subscribing plugin +// once per hook-point firing it's declared for, in agent.hcl declaration +// order alongside every other subscriber at that point +// (agent-loop/hook-dispatch.md#dispatch-order-and-payload-flow). Every +// plugin category MAY implement this service; whether the kernel ever +// dials it for a given plugin process is entirely a function of that +// plugin's own agent.hcl `hook{}` declarations, not its category. +service HookSubscriberService { + // DispatchHook delivers one hook-point firing to one subscriber. Unary, + // not streaming: one invocation is one request/one response + // (agent-loop/hook-dispatch.md#dispatch-order-and-payload-flow). Per- + // subscriber timeout is a ctx deadline the kernel sets + // (.claude/rules/grpc.md "Context and deadlines"; + // agent-loop/hook-dispatch.md#timeout-behavior), not a wire field on + // this request. Cancellation (the kernel closing the call because the + // turn is being aborted) is normal control flow, per + // .claude/rules/grpc.md — never logged as a failure. + rpc DispatchHook(DispatchHookRequest) returns (DispatchHookResponse); +} + +// HookPoint identifies which of the eight dispatchable points in the agent +// loop a DispatchHookRequest fires for. Not carried directly on +// DispatchHookRequest (the set HookPayload oneof variant already implies +// the point) — this enum exists for HookError, which has no oneof to infer +// a point from. architecture.md §Hook dispatch semantics enumerates these +// nine hook-point names; context-assemble (the ninth) is deliberately +// excluded here — see this file's package comment. +enum HookPoint { + // Zero value. Never valid on the wire; its presence means a caller + // forgot to set the field. + HOOK_POINT_UNSPECIFIED = 0; + // Session creation, before the first turn begins. + HOOK_POINT_SESSION_START = 1; + // Immediately before a model provider's StreamCompletion is called. + HOOK_POINT_PRE_MODEL_CALL = 2; + // Immediately after a model turn's canonical message has been + // assembled from the completion stream. + HOOK_POINT_POST_MODEL_RESPONSE = 3; + // Immediately before a plan item's tool call is applied. + HOOK_POINT_PRE_TOOL_CALL = 4; + // Once a turn's Plan has been fully built, before plan/apply gate + // dispatch. The kernel-privileged policy veto subscriber + // (architecture.md §Policy — first-party, not a plugin category) always + // runs at this point. + HOOK_POINT_PLAN_READY = 5; + // Immediately after a plan item's tool call has produced a terminal + // ToolResult or ToolError. + HOOK_POINT_POST_TOOL_CALL = 6; + // Immediately after a turn's whole Plan has finished applying (every + // item reached a terminal ApplyOutcome). + HOOK_POINT_POST_APPLY = 7; + // Session termination, once the session has reached a terminal + // SessionStatus. + HOOK_POINT_SESSION_END = 8; +} + +// HookMode is the operator-declared subscription mode for one plugin's +// hook{} block, per architecture.md §Hook dispatch semantics and +// agent-loop/hook-dispatch.md#dispatch-order-and-payload-flow. Mode is +// per-subscription (agent.hcl-declared), not per-payload — it is not +// carried in HookPayload itself, only echoed on DispatchHookRequest so the +// subscriber knows which of DispatchHookResponse's three outcome shapes is +// expected of it. +enum HookMode { + // Zero value. Never valid on the wire; its presence means a caller + // forgot to set the field. + HOOK_MODE_UNSPECIFIED = 0; + // Read-only, fire-and-forget. A raised error or malformed response is + // logged and dispatch continues; an observe subscriber can never alter + // the payload or abort the chain + // (agent-loop/hook-dispatch.md#subscriber-error-handling). + HOOK_MODE_OBSERVE = 1; + // Sequential chain member: receives the prior stage's payload, returns + // a modified version of the same variant. An error or malformed + // response aborts the remainder of that hook's chain and surfaces a + // hook_error event (agent-loop/hook-dispatch.md#subscriber-error-handling). + HOOK_MODE_TRANSFORM = 2; + // Returns an explicit allow/deny verdict. An error or timeout is + // treated identically to an explicit deny — fail-closed + // (agent-loop/hook-dispatch.md#timeout-behavior). + HOOK_MODE_VETO = 3; +} + +// HookPayload carries one hook point's data. Exactly one oneof variant is +// set; which variant is set *is* the point being dispatched — the +// parallel HookPoint enum above exists only for contexts (HookError) that +// have no oneof to infer the point from. +message HookPayload { + oneof payload { + // Fires at HOOK_POINT_SESSION_START. + SessionStartPayload session_start = 1; + // Fires at HOOK_POINT_PRE_MODEL_CALL. + PreModelCallPayload pre_model_call = 2; + // Fires at HOOK_POINT_POST_MODEL_RESPONSE. + PostModelResponsePayload post_model_response = 3; + // Fires at HOOK_POINT_PRE_TOOL_CALL. + PreToolCallPayload pre_tool_call = 4; + // Fires at HOOK_POINT_PLAN_READY. + PlanReadyPayload plan_ready = 5; + // Fires at HOOK_POINT_POST_TOOL_CALL. + PostToolCallPayload post_tool_call = 6; + // Fires at HOOK_POINT_POST_APPLY. + PostApplyPayload post_apply = 7; + // Fires at HOOK_POINT_SESSION_END. + SessionEndPayload session_end = 8; + } +} + +// SessionStartPayload fires once, at session creation. No field here is +// transform-mutable per agent-loop/hook-dispatch.md's per-point mutable- +// field table — session identity and startup parameters are fixed by the +// time this hook fires. +message SessionStartPayload { + // The new session's id. MUST be set. Immutable. + string session_id = 1; + // The agent.hcl profile this session was created under. MUST be set. + // Immutable. + string profile = 2; + // The parent session's id, when this is a sub-agent session + // (agent-loop/subagents.md). Absent for a root session. Immutable. + optional string parent_session_id = 3; + // The session's working directory. MUST be set. Immutable. + string working_directory = 4; +} + +// PreModelCallPayload fires immediately before a model provider's +// StreamCompletion is invoked. A transform subscriber MAY rewrite +// `messages` (e.g. redaction, injection of an additional instruction); it +// MUST NOT alter `model` — a hook subscriber does not get to silently +// reroute a turn to a different model than the one the turn algorithm +// already resolved. +message PreModelCallPayload { + // The messages about to be sent to the model. Transform-mutable. + repeated pluggableharness.agent.content.v1.Message messages = 1; + // The model this call targets. Immutable. + pluggableharness.agent.model.v1.ModelRef model = 2; +} + +// PostModelResponsePayload fires immediately after a model turn's +// canonical message has been assembled, before it is persisted +// (EVENT_KIND_MESSAGE). Primarily an observe-mode point (memory providers +// recording the turn, widgets displaying live usage) — no field here is +// documented transform-mutable in agent-loop/hook-dispatch.md's per-point +// table; a transform subscriber at this point MUST return the payload +// unchanged. +message PostModelResponsePayload { + // The assembled assistant message. MUST be set. + pluggableharness.agent.content.v1.Message message = 1; + // Which model-provider build produced `message`, for supersedes/replay + // attribution (architecture.md §Versioning & schema drift — + // "supersedes"). MUST be set. + pluggableharness.agent.common.v1.ProducerRef model = 2; + // Token usage for the completion that produced `message`. MUST be set. + pluggableharness.agent.model.v1.Usage usage = 3; + // The kernel-computed cost, in USD, of the completion that produced + // `message` (model/protocol.md#cost-computation — the provider never + // computes cost itself). MUST be set. + double cost_usd = 4; +} + +// PreToolCallPayload fires immediately before an already-allowed plan +// item's tool call is applied. No field here is documented transform- +// mutable — a hook subscriber observes or vetoes an about-to-execute call, +// it does not get to silently rewrite its arguments; argument mutation is +// the plan/apply gate's own concern (agent-loop.md §5), not a general hook +// capability. +message PreToolCallPayload { + // The call about to be applied. MUST be set. Immutable. + pluggableharness.agent.tool.v1.ToolCall call = 1; + // The originating plan item, carrying the policy decision that allowed + // this call through to apply. MUST be set. Immutable. + pluggableharness.agent.plan.v1.PlanItem plan_item = 2; +} + +// PlanReadyPayload fires once a turn's Plan has been fully built, before +// the plan/apply gate dispatches it. This is the veto-bearing hook point: +// the kernel-privileged policy subscriber (architecture.md §Policy — +// first-party, not a plugin category) always runs here in HOOK_MODE_VETO. +// No field here is transform-mutable — a hook subscriber does not rewrite +// plan items; only the plan/apply gate itself and the policy veto affect +// what applies. +message PlanReadyPayload { + // The fully-built plan awaiting policy evaluation. MUST be set. + // Immutable. + pluggableharness.agent.plan.v1.Plan plan = 1; +} + +// PostToolCallPayload fires immediately after a plan item's tool call +// reaches a terminal outcome. Observe-only in practice — no field here is +// documented transform-mutable. +message PostToolCallPayload { + // The call that completed. MUST be set. Immutable. + pluggableharness.agent.tool.v1.ToolCall call = 1; + oneof outcome { + // The call's successful terminal result. + pluggableharness.agent.tool.v1.ToolResult result = 2; + // The call's failed terminal result. + pluggableharness.agent.tool.v1.ToolError error = 3; + } +} + +// PostApplyPayload fires once a turn's whole Plan has finished applying — +// every item has reached a terminal ApplyOutcome +// (pluggableharness.agent.plan.v1.ApplyResult.ApplyOutcome). Reuses +// plan.v1.ApplyResult rather than defining its own per-item outcome shape, +// so the post-apply hook's subject and the EVENT_KIND_APPLY event (the +// forthcoming event.v1 package) are the exact same message. Observe-only — +// applying has already happened by the time this fires, so there is +// nothing left to transform. +message PostApplyPayload { + // The turn's per-item apply outcomes. MUST be set. + pluggableharness.agent.plan.v1.ApplyResult apply = 1; +} + +// SessionEndPayload fires once, when a session reaches a terminal +// SessionStatus. No field here is transform-mutable — a session's outcome +// is already final by the time this fires. +message SessionEndPayload { + // The session that ended. MUST be set. Immutable. + string session_id = 1; + // The session's terminal status. MUST be set (never + // SESSION_STATUS_RUNNING). Immutable. + pluggableharness.agent.session.v1.SessionStatus status = 2; +} + +// DispatchHookRequest is one hook-point firing delivered to one +// subscriber. +message DispatchHookRequest { + // The payload for this firing. MUST be set; the set oneof variant + // implicitly identifies the hook point. + HookPayload payload = 1; + // The operator-configured mode this subscription runs under + // (agent.hcl's hook{} block). MUST be set — tells the subscriber which + // of DispatchHookResponse's three outcome shapes is expected back. + HookMode mode = 2; + // Disambiguates a plugin declaring more than one hook{} block at the + // same HookPoint. Absent when a plugin has exactly one subscription at + // this point. + optional string subscription_id = 3; +} + +// DispatchHookResponse carries a subscriber's outcome, shaped by the +// HookMode the request declared. The kernel MUST reject (surfacing a +// hook_error, per agent-loop/hook-dispatch.md#subscriber-error-handling) +// a response whose oneof variant doesn't match the request's declared +// mode — HOOK_ERROR_CATEGORY_INVALID_RESPONSE. +message DispatchHookResponse { + oneof outcome { + // The subscriber's acknowledgment, for a HOOK_MODE_OBSERVE request. + ObserveAck observe = 1; + // The subscriber's modified payload, for a HOOK_MODE_TRANSFORM + // request. + TransformResult transform = 2; + // The subscriber's allow/deny verdict, for a HOOK_MODE_VETO request. + VetoResult veto = 3; + } + + // ObserveAck is empty. The kernel discards it (and any payload an + // observe subscriber mistakenly returns) unconditionally — observe mode + // can never alter the payload, per + // agent-loop/hook-dispatch.md#subscriber-error-handling. + message ObserveAck {} + + // TransformResult carries a transform subscriber's modified payload. + message TransformResult { + // MUST be the same oneof variant as the request's payload. MUST only + // mutate the fields this hook point documents as transform-mutable + // (see each *Payload message's own comment) — the kernel MUST reject + // a response that changes an immutable field or the variant itself, + // per agent-loop/hook-dispatch.md#subscriber-error-handling. + HookPayload payload = 1; + } + + // VetoResult carries a veto subscriber's allow/deny verdict. + message VetoResult { + // MUST be set. HOOK_DECISION_UNSPECIFIED is treated as an invalid + // response (HOOK_ERROR_CATEGORY_INVALID_RESPONSE), not as an implicit + // deny — the fail-closed behavior for a genuinely absent/erroring + // response is handled at the gRPC-status level, not by this enum's + // zero value. + HookDecision decision = 1; + } +} + +// HookDecision is a veto subscriber's coarse allow/deny verdict over a +// whole HookPayload. Deliberately distinct from +// pluggableharness.agent.plan.v1.PlanDecision — that enum is per-plan-item +// and carries PENDING/ASK, which are meaningless for a hook-level veto; +// the two enums MUST NOT be merged, per .claude/rules/proto.md's +// no-untyped-overload discipline (see also ToolErrorCategory vs +// ModelErrorCategory for the same non-merge precedent). +enum HookDecision { + // Zero value. Never valid as a deliberately-chosen verdict; its + // presence on the wire means a caller forgot to set the field. + HOOK_DECISION_UNSPECIFIED = 0; + // The dispatch chain proceeds normally. + HOOK_DECISION_ALLOW = 1; + // The kernel MUST NOT let the vetoed action proceed. What "the action" + // means is point-specific: at HOOK_POINT_PLAN_READY it means the whole + // plan (or, for the kernel-privileged policy subscriber, the + // per-item decisions policy itself produces directly — a third-party + // veto subscriber at plan-ready returns only this coarse ALLOW/DENY + // over the whole plan, per architecture.md §Policy — first-party, not a + // plugin category). + HOOK_DECISION_DENY = 2; +} + +// HookErrorCategory classifies why a hook dispatch to one subscriber +// failed. Deliberately its own enum, not reused from tool.v1 or model.v1 — +// per .claude/rules/proto.md, a category proto's error taxonomy is never +// merged with another category's even where the surrounding shape is +// parallel. +enum HookErrorCategory { + // Zero value. Never valid for a real error; its presence on the wire + // means a caller forgot to set the field. + HOOK_ERROR_CATEGORY_UNSPECIFIED = 0; + // A transform subscriber raised an error or returned a malformed + // response. Aborts the remainder of that hook's chain + // (agent-loop/hook-dispatch.md#subscriber-error-handling). + HOOK_ERROR_CATEGORY_TRANSFORM_FAILED = 1; + // A veto subscriber raised an error. Treated identically to an + // explicit deny — fail-closed + // (agent-loop/hook-dispatch.md#subscriber-error-handling). + HOOK_ERROR_CATEGORY_VETO_FAILED = 2; + // The subscriber exceeded its per-subscriber deadline + // (agent-loop/hook-dispatch.md#timeout-behavior). For a veto + // subscriber, resolved identically to HOOK_ERROR_CATEGORY_VETO_FAILED — + // fail-closed deny. + HOOK_ERROR_CATEGORY_TIMEOUT = 3; + // The subscriber's DispatchHookResponse didn't match what its declared + // HookMode requires — wrong oneof variant, or (for transform) a + // payload variant that doesn't match the request's, or a mutation to a + // field this hook point doesn't document as transform-mutable. + HOOK_ERROR_CATEGORY_INVALID_RESPONSE = 4; + // The subscriber's plugin subprocess died mid-dispatch (transport + // error, not a graceful error the plugin chose to return). MUST be + // kernel-synthesized only, mirroring + // tool/conformance.md#error-taxonomy's TOOL_ERROR_CATEGORY_PROCESS_CRASHED + // — a plugin process that crashes obviously cannot emit this itself. + HOOK_ERROR_CATEGORY_PROCESS_CRASHED = 5; + // Anything else. + HOOK_ERROR_CATEGORY_UNKNOWN = 6; +} + +// HookError is the structured detail describing one failed hook dispatch. +// Also the payload shape the kernel-synthesized EVENT_KIND_HOOK_ERROR +// event carries (kernel.v1.EventKind, state-backend.md §The kind enum) — +// the forthcoming event.v1 package's HookErrorEvent wraps this same +// message rather than redefining it. +message HookError { + // Which hook point the failing dispatch was for. MUST be set. + HookPoint point = 1; + // Which plugin build the failing subscriber was. MUST be set. + pluggableharness.agent.common.v1.ProducerRef subscriber = 2; + // The HookMode the failing subscription was declared under. MUST be + // set. + HookMode mode = 3; + // Which category of failure this is. MUST be set. + HookErrorCategory category = 4; + // Human-readable detail, e.g. the raw subscriber error message. + string message = 5; +} diff --git a/api/pluggableharness/agent/kernel/v1/kernel.proto b/api/pluggableharness/agent/kernel/v1/kernel.proto index b6e19ea..45b5265 100644 --- a/api/pluggableharness/agent/kernel/v1/kernel.proto +++ b/api/pluggableharness/agent/kernel/v1/kernel.proto @@ -129,6 +129,28 @@ message RunSessionResult { // SESSION_STATUS_RUNNING — a RunSession call only returns once the // child session has reached a terminal state. pluggableharness.agent.session.v1.SessionStatus status = 3; + + // The child session's aggregate cost, in USD, summed across every turn + // it ran, including any of its own descendant sub-agent sessions. MUST + // be set. Deliberately a flat field, not a + // pluggableharness.agent.model.v1.Usage-shaped reference — this is a + // whole-session rollup (state-backend.md's cost_ledger SUM, per + // .claude/rules/determinism.md's "Cost and budget rollup"), a different + // shape from one completion call's per-call Usage. Lets an orchestrator + // plugin do budget-aware fan-out (spend-check a child's outcome before + // deciding whether to spawn another) without needing to separately sum + // the child's own event history itself. + double total_cost_usd = 4; + + // The child session's aggregate input token count, summed across every + // turn it ran, including descendants. MUST be set. Same flat, + // aggregate-not-per-call rationale as total_cost_usd. + int64 total_input_tokens = 5; + + // The child session's aggregate output token count, summed across + // every turn it ran, including descendants. MUST be set. Same flat, + // aggregate-not-per-call rationale as total_cost_usd. + int64 total_output_tokens = 6; } // CountTokensRequest asks the kernel to count tokens for a block of @@ -200,6 +222,14 @@ enum EventKind { // A memory provider's deletion of an existing record. EVENT_KIND_MEMORY_DELETE = 9; + + // Kernel-synthesized when a transform or veto hook subscriber fails + // (agent-loop/hook-dispatch.md#subscriber-error-handling) — never + // emitted by a plugin's own Emit call, only by the kernel itself + // dispatching a hook. Payload shape is + // pluggableharness.agent.hook.v1.HookError, wrapped by the forthcoming + // event.v1 package's HookErrorEvent (state-backend.md §5). + EVENT_KIND_HOOK_ERROR = 10; } // EmitRequest asks the kernel to persist one event into the calling diff --git a/api/pluggableharness/agent/plan/v1/plan.proto b/api/pluggableharness/agent/plan/v1/plan.proto index 00771cd..ec834b8 100644 --- a/api/pluggableharness/agent/plan/v1/plan.proto +++ b/api/pluggableharness/agent/plan/v1/plan.proto @@ -10,6 +10,7 @@ syntax = "proto3"; package pluggableharness.agent.plan.v1; import "google/protobuf/struct.proto"; +import "pluggableharness/agent/tool/v1/tool.proto"; option go_package = "github.com/pluggableharness/agent/pkg/plan/proto/v1;planv1"; @@ -77,3 +78,69 @@ message Plan { // The plan's items, in the order they were identified during the turn. repeated PlanItem items = 2; } + +// ApplyResult carries a turn's complete set of per-item apply outcomes, +// once every item in its Plan has reached a terminal ApplyOutcome, per +// agent-loop.md §5.2. Homed in plan.v1 (rather than the forthcoming +// event.v1 package that would otherwise seem the more obvious owner) so +// both pluggableharness.agent.hook.v1's PostApplyPayload and event.v1's +// future EVENT_KIND_APPLY payload can reference this exact message +// without either importing the other — plan.v1 has no dependency on +// either, keeping the graph acyclic. +message ApplyResult { + // The turn this apply outcome belongs to. Matches the originating + // Plan.turn_id. + string turn_id = 1; + + // One outcome per applied plan item, in apply order. + repeated ApplyItem items = 2; + + // ApplyItem is one plan item's apply outcome. + message ApplyItem { + // The originating PlanItem.id this outcome is for. MUST be set. + string plan_item_id = 1; + + // The originating PlanItem.tool_call_id this outcome is for. MUST + // be set. + string tool_call_id = 2; + + // How this item's apply attempt concluded. MUST be set (never + // APPLY_OUTCOME_UNSPECIFIED). + ApplyOutcome outcome = 3; + + // Absent for APPLY_OUTCOME_DENIED and APPLY_OUTCOME_SKIPPED — + // neither outcome executes the underlying tool call, so neither has + // a ToolResult/ToolError to carry. + oneof result { + // The call's successful result, when outcome == + // APPLY_OUTCOME_APPLIED. + pluggableharness.agent.tool.v1.ToolResult tool_result = 4; + // The call's failed result, when outcome == APPLY_OUTCOME_FAILED. + pluggableharness.agent.tool.v1.ToolError tool_error = 5; + } + } + + // ApplyOutcome classifies how one plan item's apply attempt concluded. + enum ApplyOutcome { + // Zero value. Never valid for a real outcome; its presence on the + // wire means a caller forgot to set the field. + APPLY_OUTCOME_UNSPECIFIED = 0; + // The item's call executed and succeeded. + APPLY_OUTCOME_APPLIED = 1; + // The item's call executed and failed on its own terms (a + // ToolError). + APPLY_OUTCOME_FAILED = 2; + // The item was not executed: the plan/apply gate synthesized a + // denial instead, per agent-loop.md §5.2's tool_result denial block + // (kernel policy PLAN_DECISION_DENY, or a HOOK_DECISION_DENY veto + // at plan-ready). The model observes this denial in its own + // history via the synthesized ToolResultBlock, not via this + // ApplyItem. + APPLY_OUTCOME_DENIED = 3; + // The item was never reached because an earlier item in the same + // apply pass aborted the whole apply. Not reached in the current + // apply algorithm — reserved for a future partial-apply-then-abort + // mode. + APPLY_OUTCOME_SKIPPED = 4; + } +} diff --git a/docs/specifications/agent-loop/README.md b/docs/specifications/agent-loop/README.md index cb53367..8bac579 100644 --- a/docs/specifications/agent-loop/README.md +++ b/docs/specifications/agent-loop/README.md @@ -13,7 +13,7 @@ This design reflects patterns observed across roughly 16 agentic coding systems ## Reading order - [`turn-algorithm.md`](turn-algorithm.md) — the numbered `RunTurn` algorithm, turn-level tool-call concurrency, loop termination and bounds (independent bound dimensions, cost accounting, limit-reached behavior, done detection, doom-loop detection). -- [`hook-dispatch.md`](hook-dispatch.md) — dispatch order and payload flow, subscriber error handling, timeout behavior, parallelism within one hook point, and open questions around `veto`-mode registration. +- [`hook-dispatch.md`](hook-dispatch.md) — the `pluggableharness.agent.hook.v1` wire contract, dispatch order and payload flow, subscriber error handling, timeout behavior, parallelism within one hook point, and the `veto`-mode subscription trust model. - [`plan-apply-gate.md`](plan-apply-gate.md) — plan construction and policy evaluation, decision semantics, the circuit breaker on repeated denials, and the `data_source`/`interactive` policy precheck. - [`subagents.md`](subagents.md) — `RunSession`'s data types, context isolation, concurrency limits, session-hierarchy bookkeeping, structural depth limits, tool scoping at spawn, cancellation propagation, and the (deliberate) absence of inter-session communication. - [`error-recovery.md`](error-recovery.md) — model-provider error handling and tool-provider (plugin) crash handling mid-turn. diff --git a/docs/specifications/agent-loop/conformance.md b/docs/specifications/agent-loop/conformance.md index 90f6c8f..91f1ff3 100644 --- a/docs/specifications/agent-loop/conformance.md +++ b/docs/specifications/agent-loop/conformance.md @@ -27,6 +27,7 @@ | Per-subscriber timeout enforcement | MUST | [`hook-dispatch.md`](hook-dispatch.md#timeout-behavior) | | Sequential dispatch for `transform`/`veto` within one hook point | MUST | [`hook-dispatch.md`](hook-dispatch.md#parallelism-within-one-hook-point) | | Concurrent dispatch among consecutive `observe` subscribers | MAY | [`hook-dispatch.md`](hook-dispatch.md#parallelism-within-one-hook-point) | +| Third-party `veto`-mode hook subscription, via `agent.hcl` declaration | MAY | [`hook-dispatch.md`](hook-dispatch.md#veto-mode-subscription-trust-model) | | Per-`PlanItem` (not per-plan) policy evaluation | MUST | [`plan-apply-gate.md`](plan-apply-gate.md#plan-construction-and-policy-evaluation) | | Batched UI presentation of multiple `ask` items | MAY | [`plan-apply-gate.md`](plan-apply-gate.md#plan-construction-and-policy-evaluation) | | `deny` synthesizes a `tool_result` denial block back to the model | MUST | [`plan-apply-gate.md`](plan-apply-gate.md#decision-semantics) | @@ -51,7 +52,6 @@ ## Open questions - **Veto-hook timeout fail-closed default** ([`hook-dispatch.md`](hook-dispatch.md#timeout-behavior)). Chosen over a fail-open-with-explicit-instructions alternative because policy sits at the terminal mutation gate. Worth revisiting if fail-closed proves too disruptive to interactive UX in practice — there is no clear consensus among comparable systems here, only one adjacent precedent this design deliberately diverges from. -- **Whether third-party plugins may register `veto`-mode hooks at all**, or whether `veto` is policy-exclusive ([`hook-dispatch.md`](hook-dispatch.md#open-questions)). Carried forward from [`architecture.md`](../architecture.md#policy--first-party-not-a-plugin-category) — this is a plugin trust-model question that cross-harness comparison doesn't settle, and this document's hook-dispatch mechanics apply equally either way once that's decided. - **Tool-provider crash handling** ([`error-recovery.md`](error-recovery.md#tool-provider-plugin-crashes)) is reasoned by analogy to the denial-feedback pattern, not a pattern directly established for crashes specifically. Should be revisited if tool-result formatting and feedback conventions evolve to address crash handling directly. - **Full context-compaction algorithm** (trigger metric, mechanism, tail protection) is deliberately out of scope across this whole directory — "what's worth remembering" and context injection belong to memory/context providers ([`memory/README.md`](../memory/README.md), [`context/README.md`](../context/README.md)), not the kernel loop. This directory's only compaction-adjacent obligation is the reaction to a `context_length_exceeded` error ([`error-recovery.md#model-provider-errors`](error-recovery.md#model-provider-errors)). diff --git a/docs/specifications/agent-loop/hook-dispatch.md b/docs/specifications/agent-loop/hook-dispatch.md index 6a9edce..ef75bf0 100644 --- a/docs/specifications/agent-loop/hook-dispatch.md +++ b/docs/specifications/agent-loop/hook-dispatch.md @@ -2,6 +2,66 @@ [`architecture.md`](../architecture.md#hook-dispatch-semantics) establishes the ordered-chain model (declaration order, three subscriber modes — `observe`/`transform`/`veto`) but leaves mechanics unspecified; this document is that mechanics layer. No surveyed harness exposes a generalized, third-party-pluggable hook-dispatch subsystem in this shape, so the following is this kernel's own design, informed by the closest analogous patterns where one exists. +## Wire contract — `pluggableharness.agent.hook.v1` + +`pluggableharness.agent.hook.v1.HookSubscriberService` (`api/pluggableharness/agent/hook/v1/hook.proto`) is the wire surface every hook subscriber implements, regardless of which of the six plugin categories the subscribing plugin otherwise belongs to. It is one shared service, not a per-category RPC: `hashicorp/go-plugin` natively muxes multiple gRPC services over a single subprocess connection, so the kernel dials `HookSubscriberService` on the same connection it already holds to that plugin's category service. A plugin declaring no `hook{}` block in `agent.hcl` simply never has `DispatchHook` called. + +`DispatchHook` is unary — one hook-point firing, delivered to one subscriber, is one request/one response. There is no separate scheduling RPC or subscription-registration call; `agent.hcl`'s `hook{}` blocks are the sole source of "which plugins subscribe to which points in which mode," resolved at config-load time, and dispatch order within a point is the declaration order described in [Dispatch order and payload flow](#dispatch-order-and-payload-flow) below. + +### Hook points + +`hook.v1.HookPoint` enumerates eight of [`architecture.md`](../architecture.md#hook-dispatch-semantics)'s nine named points — every one except `context-assemble`, which stays on `ContextService.Contribute` ([`../context/protocol.md#contribute-the-context-assemble-rpc`](../context/protocol.md#contribute-the-context-assemble-rpc)) rather than riding this surface. `Contribute` already carries the full accumulated `ContextSection` chain as a first-class typed request/response; routing it through the generic `HookPayload` oneof below would just be a second, redundant path to the same effect with weaker typing. + +| Hook point | `HookPayload` variant | +|---|---| +| `session-start` | `SessionStartPayload` | +| `pre-model-call` | `PreModelCallPayload` | +| `post-model-response` | `PostModelResponsePayload` | +| `pre-tool-call` | `PreToolCallPayload` | +| `plan-ready` | `PlanReadyPayload` | +| `post-tool-call` | `PostToolCallPayload` | +| `post-apply` | `PostApplyPayload` | +| `session-end` | `SessionEndPayload` | + +`HookPayload` is a `oneof`; the set variant *is* the point being dispatched — `DispatchHookRequest` carries no separate `HookPoint` field. `HookPoint` exists on the wire only where there's no oneof to infer a point from: `HookError`, and the future `event.v1.HookErrorEvent` it's embedded in. + +### Dispatch modes → response shapes + +`DispatchHookRequest.mode` (`hook.v1.HookMode`) tells the subscriber which of `DispatchHookResponse`'s three outcome shapes is expected back: + +| Mode | Expected response | Payload semantics | +|---|---|---| +| `HOOK_MODE_OBSERVE` | `ObserveAck` (empty) | Fire-and-forget. The kernel discards any payload the subscriber returns even if one is present — observe mode can never alter the chain. | +| `HOOK_MODE_TRANSFORM` | `TransformResult { payload }` | `payload` MUST be the same `HookPayload` oneof variant as the request. The kernel applies only the fields this point's [mutable-field table](#per-point-transform-mutable-fields) below documents as mutable; every other field is compared against the request and any change is rejected. | +| `HOOK_MODE_VETO` | `VetoResult { decision }` | `decision` MUST be `HOOK_DECISION_ALLOW` or `HOOK_DECISION_DENY`. `HOOK_DECISION_UNSPECIFIED` is an invalid response, not an implicit allow or deny. | + +A response whose oneof variant doesn't match `mode` — an `observe` subscriber returning `VetoResult`, a `transform` subscriber returning `ObserveAck`, and so on — is `HOOK_ERROR_CATEGORY_INVALID_RESPONSE`, handled per [Subscriber error handling](#subscriber-error-handling) below. + +### Per-point transform-mutable fields + +Only `pre-model-call` grants a `transform` subscriber real payload mutation in v1; every other point's `transform` mode is either not meaningfully mutable or not expected to be subscribed in `transform` mode at all (a `transform` subscriber at a non-mutable point MUST return the payload byte-identical to what it received — the kernel rejects any diff as `HOOK_ERROR_CATEGORY_INVALID_RESPONSE`, the [Dispatch modes → response shapes](#dispatch-modes--response-shapes) table's variant-and-field check applies uniformly regardless of which point it's checking). + +| `HookPayload` variant | Transform-mutable fields | Immutable fields | +|---|---|---| +| `SessionStartPayload` | none | `session_id`, `profile`, `parent_session_id`, `working_directory` | +| `PreModelCallPayload` | `messages` | `model` — a hook subscriber does not get to silently reroute a turn to a different model than the one the turn algorithm already resolved | +| `PostModelResponsePayload` | none | `message`, `model`, `usage`, `cost_usd` — the completion has already happened; there is nothing left to transform, only to observe | +| `PreToolCallPayload` | none | `call`, `plan_item` — argument mutation is the plan/apply gate's own concern ([`plan-apply-gate.md`](plan-apply-gate.md)), not a general hook capability | +| `PlanReadyPayload` | none | `plan` — a hook subscriber does not rewrite plan items; only the plan/apply gate itself and the `veto`-mode policy decision affect what applies | +| `PostToolCallPayload` | none | `call`, `result`/`error` | +| `PostApplyPayload` | none | `apply` — applying has already happened by the time this fires | +| `SessionEndPayload` | none | `session_id`, `status` — the outcome is already final | + +`messages` mutation at `pre-model-call` is the one case where `transform` mode does real work: redaction, injecting an additional instruction, or similar content-level rewriting of what's about to be sent to the model. + +### `INVALID_RESPONSE` handling + +`HOOK_ERROR_CATEGORY_INVALID_RESPONSE` covers every shape mismatch this document defines: wrong oneof variant for the declared `mode`, a `transform` response whose `HookPayload` variant doesn't match the request's, a `transform` response that mutates a field the [mutable-field table](#per-point-transform-mutable-fields) doesn't list, and `HOOK_DECISION_UNSPECIFIED` on a `veto` response. It is handled exactly like `HOOK_ERROR_CATEGORY_TRANSFORM_FAILED`/`HOOK_ERROR_CATEGORY_VETO_FAILED` per [Subscriber error handling](#subscriber-error-handling) below, mode-appropriately: an invalid `observe` response is logged and dispatch continues (observe errors are never fatal to the chain); an invalid `transform` response aborts the chain and raises `hook_error`; an invalid `veto` response fails closed to `HOOK_DECISION_DENY`. + +### Per-subscriber timeout + +Per-subscriber timeout is a `ctx` deadline the kernel sets on the `DispatchHook` call itself (`default_hook_timeout_ms`, with a per-subscriber `agent.hcl` override — see [Timeout behavior](#timeout-behavior) below), not a field carried on `DispatchHookRequest`. This matches the "Context and deadlines" convention used identically across every other category's protocol: the deadline is transport-level, not application-level, so a subscriber honoring `ctx` cancellation promptly is what actually bounds the kernel's wall-clock wait. + ## Dispatch order and payload flow For a given hook point, the kernel MUST visit registered subscribers in a single pass, in declaration order (`agent.hcl` order), regardless of subscriber mode. There is no separate scheduling phase per mode — `observe`, `transform`, and `veto` subscribers are interleaved in whatever order they were declared, and each sees the payload as transformed by every subscriber before it in that order: @@ -49,7 +109,14 @@ Because `transform` subscribers depend on the prior subscriber's output and `vet A conforming kernel MUST instrument one hook point's whole dispatch as a single span covering the ordered subscriber chain, with a nested span per subscriber invocation — so concurrent `observe`-mode subscribers appear as sibling children in the resulting trace, matching the parallelism this section permits. +## Veto-mode subscription trust model + +`HOOK_MODE_VETO` is open to **any** plugin declared in `agent.hcl` with a `hook{}` block at a veto-bearing point — it is not policy-exclusive. `agent.hcl` declaration *is* the operator's trust grant: an operator who writes a `hook { point = "plan-ready", mode = "veto" }` block naming a third-party plugin has explicitly opted that plugin into terminal-gate authority over applies, the same way declaring any resource-kind tool provider at all is already an implicit trust decision. There is no separate allowlist or first-party-only restriction layered on top of `agent.hcl` itself. + +This does not diminish policy's own privileged position: the kernel-owned policy engine ([`architecture.md`](../architecture.md#policy--first-party-not-a-plugin-category)) is not a plugin at all and does not go through `HookSubscriberService` — it evaluates `plan-ready` directly and always runs, unconditionally, producing per-item `PlanDecision`s. A third-party `veto`-mode subscriber at `plan-ready` sits alongside policy in the same declaration-order chain and returns only the coarser `hook.v1.HookDecision` (`ALLOW`/`DENY`) over the whole payload — it cannot express `PlanDecision`'s per-item `PENDING`/`ALLOW`/`ASK`/`DENY` granularity, and it cannot override a `DENY` policy has already produced earlier in the chain (per [Dispatch order and payload flow](#dispatch-order-and-payload-flow), an explicit non-`allow` short-circuits the remaining subscribers at that point). + +Third-party `veto` errors and timeouts still fail closed to `HOOK_DECISION_DENY`, identically to policy's own fail-closed behavior — [Timeout behavior](#timeout-behavior) above draws no distinction between a first-party and third-party veto subscriber's failure mode. A malfunctioning or slow third-party veto subscriber can only ever make the kernel more conservative (deny more), never less — it cannot widen what gets auto-applied by failing. + ## Open questions - **Veto-hook timeout fail-closed default.** Chosen over designs that fail open with explicit instructions because policy sits at the terminal mutation gate. Worth revisiting if fail-closed proves too disruptive to interactive UX in practice — there is no clear consensus among comparable systems here, only one adjacent precedent this design deliberately diverges from. -- **Whether third-party plugins may register `veto`-mode hooks at all**, or whether `veto` is policy-exclusive. Carried forward from [`architecture.md`](../architecture.md#policy--first-party-not-a-plugin-category) — this is a plugin trust-model question that cross-harness comparison doesn't settle, and this document's dispatch mechanics apply equally either way once that's decided. diff --git a/docs/specifications/agent-loop/subagents.md b/docs/specifications/agent-loop/subagents.md index d83f430..2c8efae 100644 --- a/docs/specifications/agent-loop/subagents.md +++ b/docs/specifications/agent-loop/subagents.md @@ -24,6 +24,14 @@ RunSessionResult { // crosses the session boundary back to the parent turn status enum { completed, error_max_turns, error_max_budget_usd, error_max_wall_clock, cancelled, failed } + total_cost_usd float64 // MUST — the child session's aggregate cost, + // summed across every turn it ran, including + // descendant sub-agent sessions; an aggregate, + // not one completion's Usage, so an + // orchestrator plugin can do budget-aware + // fan-out on a child's outcome + total_input_tokens int64 // MUST — same aggregate shape as total_cost_usd + total_output_tokens int64 // MUST — same aggregate shape as total_cost_usd } ``` diff --git a/docs/specifications/architecture.md b/docs/specifications/architecture.md index 4911749..571ebfe 100644 --- a/docs/specifications/architecture.md +++ b/docs/specifications/architecture.md @@ -89,7 +89,7 @@ Kept honest to the microkernel: not privileged kernel code. The kernel exposes a ## Policy — first-party, not a plugin category -Ties directly to the plan/apply gate (kernel-owned). Lives in `agent.hcl` as a small rule-matching DSL, deliberately mirroring a shape already proven out in practice (Claude Code's own `settings.json` allow/deny + auto-mode classifier). Mechanically, policy is the kernel-privileged `veto`-mode subscriber at the `plan-ready` hook — always run, always respected. Whether third-party plugins may register `veto`-mode hooks at all remains an open question; see [`agent-loop/hook-dispatch.md`](agent-loop/hook-dispatch.md#open-questions). See [`configuration/policy-dsl.md`](configuration/policy-dsl.md) for the full DSL and evaluation semantics, including conflict-detection. +Ties directly to the plan/apply gate (kernel-owned). Lives in `agent.hcl` as a small rule-matching DSL, deliberately mirroring a shape already proven out in practice (Claude Code's own `settings.json` allow/deny + auto-mode classifier). Mechanically, policy is the kernel-privileged `veto`-mode subscriber at the `plan-ready` hook — always run, always respected, and not itself a plugin call (it does not go through `HookSubscriberService`). Third-party plugins MAY also register `veto`-mode hooks, `agent.hcl` declaration being the operator's trust grant to do so; see [`agent-loop/hook-dispatch.md#veto-mode-subscription-trust-model`](agent-loop/hook-dispatch.md#veto-mode-subscription-trust-model). See [`configuration/policy-dsl.md`](configuration/policy-dsl.md) for the full DSL and evaluation semantics, including conflict-detection. ## Hook dispatch semantics @@ -99,7 +99,9 @@ Hook points (`session-start`, `context-assemble`, `pre-model-call`, `post-model- - `transform` — receives the previous stage's output, returns a modified version; the next subscriber sees the transformed payload (context providers at `context-assemble`). - `veto` — can short-circuit with an explicit decision (policy at `plan-ready`). -Ordering within a hook is declaration order in `agent.hcl`, not runtime registration order — determinism matters especially for `context-assemble`, where order affects what the model attends to. See [`agent-loop/hook-dispatch.md`](agent-loop/hook-dispatch.md). +Ordering within a hook is declaration order in `agent.hcl`, not runtime registration order — determinism matters especially for `context-assemble`, where order affects what the model attends to. + +The wire surface for all eight dispatchable points other than `context-assemble` (which stays on `ContextService.Contribute`, per [`context/protocol.md#contribute-the-context-assemble-rpc`](context/protocol.md#contribute-the-context-assemble-rpc)) is `pluggableharness.agent.hook.v1.HookSubscriberService` — one shared service every plugin category MAY implement, dispatched to over the same `hashicorp/go-plugin` connection as that plugin's own category service. See [`agent-loop/hook-dispatch.md`](agent-loop/hook-dispatch.md) for the full dispatch mechanics and wire contract. ## Canonical message / tool-schema format diff --git a/docs/specifications/configuration/blocks-reference.md b/docs/specifications/configuration/blocks-reference.md index 82db1df..c02762e 100644 --- a/docs/specifications/configuration/blocks-reference.md +++ b/docs/specifications/configuration/blocks-reference.md @@ -78,19 +78,36 @@ ConfigSchema { } ConfigAttribute { - name string - type enum { string, number, bool, list_string, list_number, map_string, object } - // a deliberately small subset of cty's type system — no nested - // block types in v1 - required bool - sensitive bool // MUST — see "Secrets" below - description string + name string + type enum { string, number, bool, list_string, list_number, map_string, object } + // a deliberately small subset of cty's type system + required bool + sensitive bool // MUST — see "Secrets" below + description string + object_attributes []ConfigAttribute // MUST be set (non-empty) iff type == object; + // MUST be empty for every other type — see + // "Nested object attributes" below + default_json string? // MAY — a JSON-encoded default applied when this + // attribute is optional and agent.hcl omits it — + // see "Declared defaults" below } ``` The kernel converts a `ConfigSchema` into an `hcldec` spec, decodes the matching `provider` block body into a `cty.Value` against it — resolving any `env(...)` calls during decoding — and marshals the result to the wire format `Configure` expects (JSON, carried as a `google.protobuf.Struct` on the wire). -An attribute of type `object` accepts any object-shaped value dynamically, rather than being validated against a fixed nested schema — consistent with this document's "no nested block types in v1" rule. +### Nested object attributes + +An attribute of type `object` carries its own nested schema in `object_attributes`, structurally identical to a top-level `ConfigSchema.attributes` list — each nested `ConfigAttribute` gets the same `required`/`sensitive`/`description` treatment the schema-to-cty bridge already applies at the top level, rather than accepting an unvalidated dynamic object. One level of nesting is the common case; deeper nesting is expressed the same way recursively — an entry in `object_attributes` MAY itself be `type == object` with its own populated `object_attributes`, with no depth cap enforced by the wire type itself. A provider author should still keep nesting shallow in practice: this schema exists to be validated and documented, and a deeply nested config block defeats both purposes. + +`sensitive` and the `env(...)` shape-validation rule (see "Secrets" below) apply identically to a nested attribute — a secret buried inside an `object`-typed attribute is validated exactly as if it were a top-level attribute of the same type. + +### Declared defaults + +`default_json` supplies the value the schema-to-cty bridge uses when a `required = false` attribute's corresponding HCL expression is absent from the `provider` block body entirely. Absent `default_json` means "no default" — an omitted optional attribute decodes to that type's cty zero value (empty string, `0`, `false`, an empty list/map), exactly as it did before this field existed. + +`default_json` is a JSON-encoded string rather than a typed field per `AttrType` (or a `google.protobuf.Struct`/`Value`) so the wire type stays cty-agnostic: the kernel's schema-to-cty bridge is the only thing that interprets it, parsing the JSON and converting the result to the `cty.Value` the attribute's declared `type` expects. Encoding rule: the JSON value's shape MUST match `type` under the same mapping the bridge already uses when decoding an actual HCL-supplied value (a JSON string for `string`, a JSON number for `number`, a JSON array of strings for `list_string`, a JSON object for `object` — matching that attribute's own `object_attributes` shape, recursively for nested `object`-typed defaults). A `default_json` that doesn't parse as JSON, or that parses but doesn't match `type`'s expected shape, MUST be rejected as a config-load-time error against the provider's own advertised schema — the same "misconfiguration is a load-time error" posture this document applies everywhere else. + +`default_json` MUST NOT be set on an attribute with `sensitive = true` — a declared default is a literal value baked into the schema advertisement itself, which is exactly the literal-secret-value case "Secrets" below forbids regardless of where the literal appears. ## Secrets: `sensitive` and `env(...)` diff --git a/docs/specifications/configuration/lock-file.md b/docs/specifications/configuration/lock-file.md index 1453bf5..ffcc178 100644 --- a/docs/specifications/configuration/lock-file.md +++ b/docs/specifications/configuration/lock-file.md @@ -43,3 +43,7 @@ Checksum verification computes the installed binary's SHA-256 digest and compare Checksum comparison uses plain equality rather than a timing-safe comparison, since it verifies a published binary's hash against a known-good value, not a secret token — there is no timing side-channel to defend against: an attacker who can observe comparison timing learns nothing they couldn't already get by reading the (public) lock file or the (public) release artifact. This MUST NOT be changed to a constant-time comparison; doing so would defend against a threat that doesn't apply here. Checksum verification logs only the binary path and platform, at `DEBUG` level. + +## `dev_overrides` and identity without a lock entry + +A binary resolved via [`settings-and-global.md#dev_overrides`](settings-and-global.md#dev_overrides) has no `provider "" { ... }` entry in this file at all — `dev_overrides` exists precisely to bypass the registry/lock-file resolution path, so there is no `source`/`version`/`checksums` for the kernel to read identity from the way it would for a normally-resolved plugin. The kernel instead obtains that plugin build's identity directly from the process itself, via that category's own `Describe` RPC — a `Describe(DescribeRequest) -> DescribeResponse { producer: common.v1.ProducerRef }` call every one of the six category protocols gains in this same protocol revision. The plugin reports its own `{name, version, source, category, protocol_version}` at connection time, rather than the kernel inferring it from a lock-file row that in this case doesn't exist. This is the canonical explanation for the general "how does the kernel know what it's actually running" question wherever a `dev_overrides` binary is in play; other specs needing to address plugin identity resolution without a lock entry should point here rather than re-deriving it. diff --git a/docs/specifications/context/data-types.md b/docs/specifications/context/data-types.md index 818d041..fcb037a 100644 --- a/docs/specifications/context/data-types.md +++ b/docs/specifications/context/data-types.md @@ -26,6 +26,8 @@ ContextRequest { ## `ContextSection` +Canonically defined on the wire as `pluggableharness.agent.content.v1.ContextSection` (alongside `Stability` below) rather than in this protocol's own package — it's a shared content shape a second consumer besides context assembly needs to reference; the semantics below remain this document's, unchanged by where the message lives on the wire. + One provider's contribution to the assembled prompt context: ```protobuf diff --git a/docs/specifications/kernel-callbacks.md b/docs/specifications/kernel-callbacks.md index 6d96553..87dd94a 100644 --- a/docs/specifications/kernel-callbacks.md +++ b/docs/specifications/kernel-callbacks.md @@ -2,7 +2,7 @@ This formalizes the **plugin-to-kernel** direction of communication — the reverse of every other protocol in this series, which covers kernel-to-plugin RPCs (`GetCapabilities`, `Configure`, `StreamCompletion`, `Invoke`, `Attach`, and so on). Four primitives live here: -- **`RunSession`** — runs a full agent session on a plugin's behalf. Used for sub-agent spawns today, and reserved for a future non-interactive pipeline mode. Full turn-by-turn semantics live in [`agent-loop/subagents.md`](agent-loop/subagents.md); this document defines only the wire-level calling mechanism. +- **`RunSession`** — runs a full agent session on a plugin's behalf. Used for sub-agent spawns today, and reserved for a future non-interactive pipeline mode. Full turn-by-turn semantics live in [`agent-loop/subagents.md`](agent-loop/subagents.md); this document defines only the wire-level calling mechanism. `RunSessionResult` carries, alongside `final_message` and `status`, three aggregate usage fields — `total_cost_usd`, `total_input_tokens`, `total_output_tokens` — summed across every turn the child session ran, including any of its own descendant sub-agent sessions. These are deliberately flat fields, not a reference to a single completion's `Usage` shape: `RunSessionResult` reports a whole-session rollup (the same `cost_ledger` SUM [`state-backend.md`](state-backend.md) already computes), a different thing from one model call's per-call token counts. The rollup lets a calling plugin do budget-aware fan-out — checking a just-finished child's actual spend before deciding whether to spawn another — without separately re-summing the child's event history itself. - **`CountTokens`** — resolves an exact-if-possible token count for a string, the shared primitive every other category's `tokens` field routes through. - **`Emit`** — how a plugin persists anything into the session's state backend. - **`Log`** — carries a plugin's own log output into the kernel's centralized logging, so it doesn't vanish into an unread subprocess stderr. @@ -108,6 +108,8 @@ EmitResult { `EventKind` is `state-backend.md`'s authoritative enum, restated here only because it's the wire-level type `Emit` actually carries — this document does not own its definition, and `state-backend.md` remains authoritative. Like every enum in this system, `EventKind`'s zero value, `EVENT_KIND_UNSPECIFIED`, is never valid on the wire — a caller that forgets to set `kind` produces a detectable, named "unspecified" error rather than something that silently looks like a real event kind. Usage/cost, `Render` output, and `session_start`/`session_end` deliberately don't get their own `EventKind` at all — see [`state-backend.md#the-kind-enum`](state-backend.md#the-kind-enum) for why. +`Emit` accepts `EVENT_KIND_HOOK_ERROR` like any other kind, with one difference: the kernel is the one calling it, on a failing hook subscriber's behalf, rather than a plugin calling `Emit` for itself — see [`state-backend.md#the-kind-enum`](state-backend.md#the-kind-enum) for why this kind is kernel-synthesized and [`agent-loop/hook-dispatch.md#subscriber-error-handling`](agent-loop/hook-dispatch.md#subscriber-error-handling) for when it fires. + ## Log `Log` carries a plugin's own log output into the kernel's centralized logging, so it doesn't vanish into an unread subprocess stderr — plugin logs and the kernel's own logs end up in one place instead of two. Unlike `Emit`, a `Log` call is not tied to an active session: a plugin MAY call `Log` before any session exists (process startup, or from within `Configure`) or after one has ended (during shutdown), so `session_id` is optional here where it is mandatory for `Emit`. diff --git a/docs/specifications/state-backend.md b/docs/specifications/state-backend.md index f8949a4..70c8975 100644 --- a/docs/specifications/state-backend.md +++ b/docs/specifications/state-backend.md @@ -32,7 +32,7 @@ CREATE TABLE events ( id TEXT NOT NULL UNIQUE, -- stable event identifier, independent of storage timestamp TEXT NOT NULL, -- wall-clock, display only, not ordering-authoritative kind TEXT NOT NULL, -- see "The kind enum" below for the authoritative enum - producer_category TEXT NOT NULL, -- provider | tool | context | memory | frontend | widget + producer_category TEXT NOT NULL, -- model | tool | context | memory | frontend | widget producer_name TEXT NOT NULL, producer_version TEXT NOT NULL, schema_version TEXT NOT NULL, @@ -142,9 +142,18 @@ kind = enum { memory_write memory_update memory_delete + hook_error // kernel-synthesized when a transform or veto hook + // subscriber fails + // (agent-loop/hook-dispatch.md#subscriber-error-handling); + // payload shape is + // pluggableharness.agent.hook.v1.HookError, + // wrapped by the forthcoming event.v1 package's + // HookErrorEvent } ``` +`hook_error` is the one `kind` the kernel writes on a subscriber's behalf rather than in response to that subscriber's own `Emit` call — a hook subscriber that just failed can't be relied on to call `Emit` itself; the kernel detects the failure during dispatch and persists the event directly. `producer_category`/`producer_name`/`producer_version` still identify the failing subscriber (`HookError.subscriber`, a `ProducerRef`), not the kernel itself — see [`kernel-callbacks.md#emit`](kernel-callbacks.md#emit) for how every other `kind` is written by the producing plugin's own callback connection. + Three things deliberately do **not** get their own `kind`: - **Usage/cost** is not a separate kind — it's a structured field inside a `message` event's payload, extracted into `cost_ledger` at write time. Giving it a separate event would duplicate data already present in the message that produced it. diff --git a/internal/statebackend/event.go b/internal/statebackend/event.go index 537a2a5..9346ecc 100644 --- a/internal/statebackend/event.go +++ b/internal/statebackend/event.go @@ -88,6 +88,7 @@ var eventKindText = map[kernelv1.EventKind]string{ kernelv1.EventKind_EVENT_KIND_MEMORY_WRITE: "memory_write", kernelv1.EventKind_EVENT_KIND_MEMORY_UPDATE: "memory_update", kernelv1.EventKind_EVENT_KIND_MEMORY_DELETE: "memory_delete", + kernelv1.EventKind_EVENT_KIND_HOOK_ERROR: "hook_error", } // eventTextKind is eventKindText inverted, built once from eventKindText diff --git a/internal/statebackend/event_test.go b/internal/statebackend/event_test.go index 3a9e4f7..6fca6e6 100644 --- a/internal/statebackend/event_test.go +++ b/internal/statebackend/event_test.go @@ -41,6 +41,23 @@ func TestDecodeEventKind_unrecognized(t *testing.T) { } } +func TestEncodeEventKind_hookError(t *testing.T) { + t.Parallel() + // EVENT_KIND_HOOK_ERROR is kernel-synthesized (never emitted by a + // plugin's own Emit call, per docs/specifications/state-backend.md#the-kind-enum) + // but round-trips through the same eventKindText table as every other + // kind; this pins the exact stored text against the kind enum's + // dedicated table entry, on top of the generic TestEventKind_roundTrip + // coverage above. + got, err := encodeEventKind(kernelv1.EventKind_EVENT_KIND_HOOK_ERROR) + if err != nil { + t.Fatalf("encodeEventKind(EVENT_KIND_HOOK_ERROR): %v", err) + } + if got != "hook_error" { + t.Errorf("encodeEventKind(EVENT_KIND_HOOK_ERROR) = %q, want %q", got, "hook_error") + } +} + func TestProducerCategory_roundTrip(t *testing.T) { t.Parallel() diff --git a/pkg/common/proto/v1/common.pb.go b/pkg/common/proto/v1/common.pb.go index 01998be..c1de80f 100644 --- a/pkg/common/proto/v1/common.pb.go +++ b/pkg/common/proto/v1/common.pb.go @@ -261,6 +261,78 @@ func (x *ProviderRef) GetName() string { return "" } +// CallContext identifies the session, turn, and working directory a given +// RPC call executes for. Attached to a model provider's +// StreamCompletionRequest and a tool provider's ToolCall in this protocol +// revision (forthcoming in those files) — it's what a plugin passes back +// on its own KernelCallbackService.Emit call (kernel-callbacks.md §Emit) +// for correlation, without having to separately thread session_id/turn_id +// through every call site by hand. +type CallContext struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The session this call executes for. ULID-formatted, per this file's + // ID conventions above (§"ID and timestamp conventions") — the same + // session_id a plugin passes to Emit. + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + // The turn within that session this call executes for. ULID-formatted. + TurnId string `protobuf:"bytes,2,opt,name=turn_id,json=turnId,proto3" json:"turn_id,omitempty"` + // The session's working directory at call time. + WorkingDirectory string `protobuf:"bytes,3,opt,name=working_directory,json=workingDirectory,proto3" json:"working_directory,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CallContext) Reset() { + *x = CallContext{} + mi := &file_pluggableharness_agent_common_v1_common_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CallContext) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CallContext) ProtoMessage() {} + +func (x *CallContext) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_common_v1_common_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CallContext.ProtoReflect.Descriptor instead. +func (*CallContext) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_common_v1_common_proto_rawDescGZIP(), []int{2} +} + +func (x *CallContext) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *CallContext) GetTurnId() string { + if x != nil { + return x.TurnId + } + return "" +} + +func (x *CallContext) GetWorkingDirectory() string { + if x != nil { + return x.WorkingDirectory + } + return "" +} + var File_pluggableharness_agent_common_v1_common_proto protoreflect.FileDescriptor const file_pluggableharness_agent_common_v1_common_proto_rawDesc = "" + @@ -274,7 +346,12 @@ const file_pluggableharness_agent_common_v1_common_proto_rawDesc = "" + "\x10protocol_version\x18\x05 \x01(\rR\x0fprotocolVersion\"i\n" + "\vProviderRef\x12F\n" + "\bcategory\x18\x01 \x01(\x0e2*.pluggableharness.agent.common.v1.CategoryR\bcategory\x12\x12\n" + - "\x04name\x18\x02 \x01(\tR\x04name*\xa2\x01\n" + + "\x04name\x18\x02 \x01(\tR\x04name\"r\n" + + "\vCallContext\x12\x1d\n" + + "\n" + + "session_id\x18\x01 \x01(\tR\tsessionId\x12\x17\n" + + "\aturn_id\x18\x02 \x01(\tR\x06turnId\x12+\n" + + "\x11working_directory\x18\x03 \x01(\tR\x10workingDirectory*\xa2\x01\n" + "\bCategory\x12\x18\n" + "\x14CATEGORY_UNSPECIFIED\x10\x00\x12\x12\n" + "\x0eCATEGORY_MODEL\x10\x01\x12\x11\n" + @@ -297,11 +374,12 @@ func file_pluggableharness_agent_common_v1_common_proto_rawDescGZIP() []byte { } var file_pluggableharness_agent_common_v1_common_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_pluggableharness_agent_common_v1_common_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_pluggableharness_agent_common_v1_common_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_pluggableharness_agent_common_v1_common_proto_goTypes = []any{ (Category)(0), // 0: pluggableharness.agent.common.v1.Category (*ProducerRef)(nil), // 1: pluggableharness.agent.common.v1.ProducerRef (*ProviderRef)(nil), // 2: pluggableharness.agent.common.v1.ProviderRef + (*CallContext)(nil), // 3: pluggableharness.agent.common.v1.CallContext } var file_pluggableharness_agent_common_v1_common_proto_depIdxs = []int32{ 0, // 0: pluggableharness.agent.common.v1.ProducerRef.category:type_name -> pluggableharness.agent.common.v1.Category @@ -324,7 +402,7 @@ func file_pluggableharness_agent_common_v1_common_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_pluggableharness_agent_common_v1_common_proto_rawDesc), len(file_pluggableharness_agent_common_v1_common_proto_rawDesc)), NumEnums: 1, - NumMessages: 2, + NumMessages: 3, NumExtensions: 0, NumServices: 0, }, diff --git a/pkg/config/proto/v1/config.pb.go b/pkg/config/proto/v1/config.pb.go index c949637..a7326e2 100644 --- a/pkg/config/proto/v1/config.pb.go +++ b/pkg/config/proto/v1/config.pb.go @@ -29,8 +29,10 @@ const ( ) // AttrType is the small subset of HCL/cty attribute types a provider's -// config schema may use in v1. No nested blocks — configuration.md §4 -// deliberately keeps this flat. +// config schema may use in v1. ATTR_TYPE_OBJECT is the one type with +// structure below the scalar/list/map level — ConfigAttribute.object_attributes +// carries its nested schema, recursively, rather than accepting an +// unvalidated dynamic object (configuration.md §The schema-to-cty bridge). type AttrType int32 const ( @@ -118,7 +120,30 @@ type ConfigAttribute struct { Sensitive bool `protobuf:"varint,4,opt,name=sensitive,proto3" json:"sensitive,omitempty"` // Human-readable description, shown wherever agent.hcl schema is // surfaced to an operator (docs generation, validation errors). - Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"` + Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"` + // The nested attribute schema for this attribute's object shape. MUST + // be set (non-empty) iff type == ATTR_TYPE_OBJECT; MUST be empty for + // every other type. configuration.md's schema-to-cty bridge decodes + // this the same way it decodes the provider's own top-level + // ConfigSchema.attributes, so an object attribute's fields get the same + // required/sensitive/description treatment as any top-level attribute. + // One level of nesting is sanctioned per direct ConfigAttribute; deeper + // nesting is expressed by an entry in object_attributes itself being + // type == ATTR_TYPE_OBJECT with its own populated object_attributes — + // the schema is recursive, not flat-capped. + ObjectAttributes []*ConfigAttribute `protobuf:"bytes,6,rep,name=object_attributes,json=objectAttributes,proto3" json:"object_attributes,omitempty"` + // A JSON-encoded default value applied when this attribute is optional + // (required == false) and agent.hcl omits it. Absent means "no default + // — an omitted optional attribute decodes to that type's cty zero + // value." String-typed (rather than a typed field per AttrType, or a + // google.protobuf.Struct/Value) to stay cty-agnostic at the proto + // level: the kernel's schema-to-cty bridge is the only thing that + // interprets this string, parsing it as JSON and converting the result + // to the cty.Value this attribute's type expects — the wire type + // itself doesn't need to model cty's type system to carry a default + // through it. A default for an ATTR_TYPE_OBJECT attribute is a + // JSON object matching object_attributes' shape. + DefaultJson *string `protobuf:"bytes,7,opt,name=default_json,json=defaultJson,proto3,oneof" json:"default_json,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -188,6 +213,20 @@ func (x *ConfigAttribute) GetDescription() string { return "" } +func (x *ConfigAttribute) GetObjectAttributes() []*ConfigAttribute { + if x != nil { + return x.ObjectAttributes + } + return nil +} + +func (x *ConfigAttribute) GetDefaultJson() string { + if x != nil && x.DefaultJson != nil { + return *x.DefaultJson + } + return "" +} + // ConfigSchema is a provider's complete config-schema advertisement, // returned alongside its capabilities. type ConfigSchema struct { @@ -239,13 +278,16 @@ var File_pluggableharness_agent_config_v1_config_proto protoreflect.FileDescript const file_pluggableharness_agent_config_v1_config_proto_rawDesc = "" + "\n" + - "-pluggableharness/agent/config/v1/config.proto\x12 pluggableharness.agent.config.v1\"\xc1\x01\n" + + "-pluggableharness/agent/config/v1/config.proto\x12 pluggableharness.agent.config.v1\"\xda\x02\n" + "\x0fConfigAttribute\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12>\n" + "\x04type\x18\x02 \x01(\x0e2*.pluggableharness.agent.config.v1.AttrTypeR\x04type\x12\x1a\n" + "\brequired\x18\x03 \x01(\bR\brequired\x12\x1c\n" + "\tsensitive\x18\x04 \x01(\bR\tsensitive\x12 \n" + - "\vdescription\x18\x05 \x01(\tR\vdescription\"a\n" + + "\vdescription\x18\x05 \x01(\tR\vdescription\x12^\n" + + "\x11object_attributes\x18\x06 \x03(\v21.pluggableharness.agent.config.v1.ConfigAttributeR\x10objectAttributes\x12&\n" + + "\fdefault_json\x18\a \x01(\tH\x00R\vdefaultJson\x88\x01\x01B\x0f\n" + + "\r_default_json\"a\n" + "\fConfigSchema\x12Q\n" + "\n" + "attributes\x18\x01 \x03(\v21.pluggableharness.agent.config.v1.ConfigAttributeR\n" + @@ -281,12 +323,13 @@ var file_pluggableharness_agent_config_v1_config_proto_goTypes = []any{ } var file_pluggableharness_agent_config_v1_config_proto_depIdxs = []int32{ 0, // 0: pluggableharness.agent.config.v1.ConfigAttribute.type:type_name -> pluggableharness.agent.config.v1.AttrType - 1, // 1: pluggableharness.agent.config.v1.ConfigSchema.attributes:type_name -> pluggableharness.agent.config.v1.ConfigAttribute - 2, // [2:2] is the sub-list for method output_type - 2, // [2:2] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 1, // 1: pluggableharness.agent.config.v1.ConfigAttribute.object_attributes:type_name -> pluggableharness.agent.config.v1.ConfigAttribute + 1, // 2: pluggableharness.agent.config.v1.ConfigSchema.attributes:type_name -> pluggableharness.agent.config.v1.ConfigAttribute + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name } func init() { file_pluggableharness_agent_config_v1_config_proto_init() } @@ -294,6 +337,7 @@ func file_pluggableharness_agent_config_v1_config_proto_init() { if File_pluggableharness_agent_config_v1_config_proto != nil { return } + file_pluggableharness_agent_config_v1_config_proto_msgTypes[0].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/pkg/content/proto/v1/content.pb.go b/pkg/content/proto/v1/content.pb.go index 9102b05..e60e3cc 100644 --- a/pkg/content/proto/v1/content.pb.go +++ b/pkg/content/proto/v1/content.pb.go @@ -12,6 +12,15 @@ // invents a competing message representation. Also consumed by // context.md's conversation_history/rewritten_history (§5.1), memory.md's // record content, and kernel-callbacks.md's CountTokensRequest. +// +// This package is also the shared home for ContextSection and Stability +// (below) — the prompt-assembly section chain that context.md's +// ContextService.Contribute produces and consumes, and that the model +// provider's completion request will carry (forthcoming). Both types live +// here, not in context.v1, because a section is fundamentally a content +// shape (a labeled, stability-tagged list of ContentBlocks) that a second +// consumer besides the context protocol needs to reference without +// depending on the whole context.v1 package. package contentv1 @@ -89,6 +98,68 @@ func (Role) EnumDescriptor() ([]byte, []int) { return file_pluggableharness_agent_content_v1_content_proto_rawDescGZIP(), []int{0} } +// Stability hints whether a ContextSection's content changes turn to turn, +// used both as a context provider's ContextCapabilities-level declaration +// (pluggableharness.agent.context.v1.ContextCapabilities.stability) and +// per ContextSection below. context.md §7: this is a direct translation of +// the research's strongest cross-cutting finding — harnesses converge on a +// tools -> system -> static-project-context -> conversation-tail prefix +// ordering because it is a constraint, not a preference, for prompt-cache +// reuse. +type Stability int32 + +const ( + // Zero value. Never valid for a real capability or section declaration; + // its presence on the wire means a caller forgot to set the field. + Stability_STABILITY_UNSPECIFIED Stability = 0 + // Content that doesn't change turn to turn for the life of the session, + // e.g. a repo's CLAUDE.md. + Stability_STABILITY_STATIC Stability = 1 + // Content that's recomputed per turn, e.g. git status or a file tree. + Stability_STABILITY_DYNAMIC Stability = 2 +) + +// Enum value maps for Stability. +var ( + Stability_name = map[int32]string{ + 0: "STABILITY_UNSPECIFIED", + 1: "STABILITY_STATIC", + 2: "STABILITY_DYNAMIC", + } + Stability_value = map[string]int32{ + "STABILITY_UNSPECIFIED": 0, + "STABILITY_STATIC": 1, + "STABILITY_DYNAMIC": 2, + } +) + +func (x Stability) Enum() *Stability { + p := new(Stability) + *p = x + return p +} + +func (x Stability) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Stability) Descriptor() protoreflect.EnumDescriptor { + return file_pluggableharness_agent_content_v1_content_proto_enumTypes[1].Descriptor() +} + +func (Stability) Type() protoreflect.EnumType { + return &file_pluggableharness_agent_content_v1_content_proto_enumTypes[1] +} + +func (x Stability) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Stability.Descriptor instead. +func (Stability) EnumDescriptor() ([]byte, []int) { + return file_pluggableharness_agent_content_v1_content_proto_rawDescGZIP(), []int{1} +} + // Message is one turn in the canonical conversation history: a role plus // an ordered list of content blocks. model.md §5 is the source of these // semantics. @@ -657,6 +728,109 @@ func (x *RedactedThinkingBlock) GetData() []byte { return nil } +// ContextSection is one provider's contribution to the assembled prompt +// context. context.md §4, §7. +type ContextSection struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The producing plugin's declared name — a plain string, not a + // common.v1.ProducerRef, used as the identity key for a provider + // re-finding and replacing its own prior section. + Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + // Human-readable label the kernel uses to wrap this section in a clearly + // delimited boundary when concatenating the chain into the final prompt. + // MUST be set. context.md §4, §7. + Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"` + // The section's content, in emission order. MUST be set. Text-only in + // v1 — the kernel MUST reject a non-text block here rather than silently + // dropping it. context.md §4, §7, §11. + Content []*ContentBlock `protobuf:"bytes,3,rep,name=content,proto3" json:"content,omitempty"` + // This section's token count, computed via the kernel's CountTokens + // callback (kernel-callbacks.md §2), never a provider-local heuristic. + // MUST be set. context.md §4. + Tokens int64 `protobuf:"varint,4,opt,name=tokens,proto3" json:"tokens,omitempty"` + // Whether this section's content changes turn to turn. context.md §7. + Stability Stability `protobuf:"varint,5,opt,name=stability,proto3,enum=pluggableharness.agent.content.v1.Stability" json:"stability,omitempty"` + // Whether this section was truncated to fit its budget. Per context.md + // §6, setting this true is not itself sufficient to satisfy the budget + // constraint — a section that still exceeds token_budget MUST be + // rejected by the kernel regardless of this flag. + Truncated bool `protobuf:"varint,6,opt,name=truncated,proto3" json:"truncated,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContextSection) Reset() { + *x = ContextSection{} + mi := &file_pluggableharness_agent_content_v1_content_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContextSection) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContextSection) ProtoMessage() {} + +func (x *ContextSection) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_content_v1_content_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContextSection.ProtoReflect.Descriptor instead. +func (*ContextSection) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_content_v1_content_proto_rawDescGZIP(), []int{8} +} + +func (x *ContextSection) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + +func (x *ContextSection) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + +func (x *ContextSection) GetContent() []*ContentBlock { + if x != nil { + return x.Content + } + return nil +} + +func (x *ContextSection) GetTokens() int64 { + if x != nil { + return x.Tokens + } + return 0 +} + +func (x *ContextSection) GetStability() Stability { + if x != nil { + return x.Stability + } + return Stability_STABILITY_UNSPECIFIED +} + +func (x *ContextSection) GetTruncated() bool { + if x != nil { + return x.Truncated + } + return false +} + var File_pluggableharness_agent_content_v1_content_proto protoreflect.FileDescriptor const file_pluggableharness_agent_content_v1_content_proto_rawDesc = "" + @@ -693,11 +867,22 @@ const file_pluggableharness_agent_content_v1_content_proto_rawDesc = "" + "\x04text\x18\x01 \x01(\tR\x04text\x12\x1c\n" + "\tsignature\x18\x02 \x01(\fR\tsignature\"+\n" + "\x15RedactedThinkingBlock\x12\x12\n" + - "\x04data\x18\x01 \x01(\fR\x04data*?\n" + + "\x04data\x18\x01 \x01(\fR\x04data\"\x8f\x02\n" + + "\x0eContextSection\x12\x1a\n" + + "\bprovider\x18\x01 \x01(\tR\bprovider\x12\x14\n" + + "\x05label\x18\x02 \x01(\tR\x05label\x12I\n" + + "\acontent\x18\x03 \x03(\v2/.pluggableharness.agent.content.v1.ContentBlockR\acontent\x12\x16\n" + + "\x06tokens\x18\x04 \x01(\x03R\x06tokens\x12J\n" + + "\tstability\x18\x05 \x01(\x0e2,.pluggableharness.agent.content.v1.StabilityR\tstability\x12\x1c\n" + + "\ttruncated\x18\x06 \x01(\bR\ttruncated*?\n" + "\x04Role\x12\x14\n" + "\x10ROLE_UNSPECIFIED\x10\x00\x12\r\n" + "\tROLE_USER\x10\x01\x12\x12\n" + - "\x0eROLE_ASSISTANT\x10\x02BBZ@github.com/pluggableharness/agent/pkg/content/proto/v1;contentv1b\x06proto3" + "\x0eROLE_ASSISTANT\x10\x02*S\n" + + "\tStability\x12\x19\n" + + "\x15STABILITY_UNSPECIFIED\x10\x00\x12\x14\n" + + "\x10STABILITY_STATIC\x10\x01\x12\x15\n" + + "\x11STABILITY_DYNAMIC\x10\x02BBZ@github.com/pluggableharness/agent/pkg/content/proto/v1;contentv1b\x06proto3" var ( file_pluggableharness_agent_content_v1_content_proto_rawDescOnce sync.Once @@ -711,36 +896,40 @@ func file_pluggableharness_agent_content_v1_content_proto_rawDescGZIP() []byte { return file_pluggableharness_agent_content_v1_content_proto_rawDescData } -var file_pluggableharness_agent_content_v1_content_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_pluggableharness_agent_content_v1_content_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_pluggableharness_agent_content_v1_content_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_pluggableharness_agent_content_v1_content_proto_msgTypes = make([]protoimpl.MessageInfo, 9) var file_pluggableharness_agent_content_v1_content_proto_goTypes = []any{ (Role)(0), // 0: pluggableharness.agent.content.v1.Role - (*Message)(nil), // 1: pluggableharness.agent.content.v1.Message - (*ContentBlock)(nil), // 2: pluggableharness.agent.content.v1.ContentBlock - (*TextBlock)(nil), // 3: pluggableharness.agent.content.v1.TextBlock - (*ToolUseBlock)(nil), // 4: pluggableharness.agent.content.v1.ToolUseBlock - (*ToolResultBlock)(nil), // 5: pluggableharness.agent.content.v1.ToolResultBlock - (*ImageBlock)(nil), // 6: pluggableharness.agent.content.v1.ImageBlock - (*ThinkingBlock)(nil), // 7: pluggableharness.agent.content.v1.ThinkingBlock - (*RedactedThinkingBlock)(nil), // 8: pluggableharness.agent.content.v1.RedactedThinkingBlock - (*structpb.Struct)(nil), // 9: google.protobuf.Struct + (Stability)(0), // 1: pluggableharness.agent.content.v1.Stability + (*Message)(nil), // 2: pluggableharness.agent.content.v1.Message + (*ContentBlock)(nil), // 3: pluggableharness.agent.content.v1.ContentBlock + (*TextBlock)(nil), // 4: pluggableharness.agent.content.v1.TextBlock + (*ToolUseBlock)(nil), // 5: pluggableharness.agent.content.v1.ToolUseBlock + (*ToolResultBlock)(nil), // 6: pluggableharness.agent.content.v1.ToolResultBlock + (*ImageBlock)(nil), // 7: pluggableharness.agent.content.v1.ImageBlock + (*ThinkingBlock)(nil), // 8: pluggableharness.agent.content.v1.ThinkingBlock + (*RedactedThinkingBlock)(nil), // 9: pluggableharness.agent.content.v1.RedactedThinkingBlock + (*ContextSection)(nil), // 10: pluggableharness.agent.content.v1.ContextSection + (*structpb.Struct)(nil), // 11: google.protobuf.Struct } var file_pluggableharness_agent_content_v1_content_proto_depIdxs = []int32{ 0, // 0: pluggableharness.agent.content.v1.Message.role:type_name -> pluggableharness.agent.content.v1.Role - 2, // 1: pluggableharness.agent.content.v1.Message.content:type_name -> pluggableharness.agent.content.v1.ContentBlock - 3, // 2: pluggableharness.agent.content.v1.ContentBlock.text:type_name -> pluggableharness.agent.content.v1.TextBlock - 4, // 3: pluggableharness.agent.content.v1.ContentBlock.tool_use:type_name -> pluggableharness.agent.content.v1.ToolUseBlock - 5, // 4: pluggableharness.agent.content.v1.ContentBlock.tool_result:type_name -> pluggableharness.agent.content.v1.ToolResultBlock - 6, // 5: pluggableharness.agent.content.v1.ContentBlock.image:type_name -> pluggableharness.agent.content.v1.ImageBlock - 7, // 6: pluggableharness.agent.content.v1.ContentBlock.thinking:type_name -> pluggableharness.agent.content.v1.ThinkingBlock - 8, // 7: pluggableharness.agent.content.v1.ContentBlock.redacted_thinking:type_name -> pluggableharness.agent.content.v1.RedactedThinkingBlock - 9, // 8: pluggableharness.agent.content.v1.ToolUseBlock.arguments:type_name -> google.protobuf.Struct - 2, // 9: pluggableharness.agent.content.v1.ToolResultBlock.content:type_name -> pluggableharness.agent.content.v1.ContentBlock - 10, // [10:10] is the sub-list for method output_type - 10, // [10:10] is the sub-list for method input_type - 10, // [10:10] is the sub-list for extension type_name - 10, // [10:10] is the sub-list for extension extendee - 0, // [0:10] is the sub-list for field type_name + 3, // 1: pluggableharness.agent.content.v1.Message.content:type_name -> pluggableharness.agent.content.v1.ContentBlock + 4, // 2: pluggableharness.agent.content.v1.ContentBlock.text:type_name -> pluggableharness.agent.content.v1.TextBlock + 5, // 3: pluggableharness.agent.content.v1.ContentBlock.tool_use:type_name -> pluggableharness.agent.content.v1.ToolUseBlock + 6, // 4: pluggableharness.agent.content.v1.ContentBlock.tool_result:type_name -> pluggableharness.agent.content.v1.ToolResultBlock + 7, // 5: pluggableharness.agent.content.v1.ContentBlock.image:type_name -> pluggableharness.agent.content.v1.ImageBlock + 8, // 6: pluggableharness.agent.content.v1.ContentBlock.thinking:type_name -> pluggableharness.agent.content.v1.ThinkingBlock + 9, // 7: pluggableharness.agent.content.v1.ContentBlock.redacted_thinking:type_name -> pluggableharness.agent.content.v1.RedactedThinkingBlock + 11, // 8: pluggableharness.agent.content.v1.ToolUseBlock.arguments:type_name -> google.protobuf.Struct + 3, // 9: pluggableharness.agent.content.v1.ToolResultBlock.content:type_name -> pluggableharness.agent.content.v1.ContentBlock + 3, // 10: pluggableharness.agent.content.v1.ContextSection.content:type_name -> pluggableharness.agent.content.v1.ContentBlock + 1, // 11: pluggableharness.agent.content.v1.ContextSection.stability:type_name -> pluggableharness.agent.content.v1.Stability + 12, // [12:12] is the sub-list for method output_type + 12, // [12:12] is the sub-list for method input_type + 12, // [12:12] is the sub-list for extension type_name + 12, // [12:12] is the sub-list for extension extendee + 0, // [0:12] is the sub-list for field type_name } func init() { file_pluggableharness_agent_content_v1_content_proto_init() } @@ -761,8 +950,8 @@ func file_pluggableharness_agent_content_v1_content_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_pluggableharness_agent_content_v1_content_proto_rawDesc), len(file_pluggableharness_agent_content_v1_content_proto_rawDesc)), - NumEnums: 1, - NumMessages: 8, + NumEnums: 2, + NumMessages: 9, NumExtensions: 0, NumServices: 0, }, diff --git a/pkg/context/proto/v1/context.pb.go b/pkg/context/proto/v1/context.pb.go index 6741e8b..51234ff 100644 --- a/pkg/context/proto/v1/context.pb.go +++ b/pkg/context/proto/v1/context.pb.go @@ -9,15 +9,22 @@ // context-assemble and contribute content to the prompt before each model // call (e.g. a CLAUDE.md reader, an AGENTS.md reader, a git-status/file-tree // summarizer). See .claude/rules/proto.md. +// +// ContextSection and Stability — the section chain this protocol assembles +// and the turn-to-turn-change hint each section carries — are defined in +// pluggableharness.agent.content.v1, not here: that chain is consumed by +// both this protocol and the model provider's completion request +// (forthcoming), so it's homed alongside content.v1's other shared content +// shapes rather than duplicated or owned by only one consumer. package contextv1 import ( - v11 "github.com/pluggableharness/agent/pkg/config/proto/v1" - v13 "github.com/pluggableharness/agent/pkg/content/proto/v1" - v12 "github.com/pluggableharness/agent/pkg/model/proto/v1" + v12 "github.com/pluggableharness/agent/pkg/config/proto/v1" + v1 "github.com/pluggableharness/agent/pkg/content/proto/v1" + v13 "github.com/pluggableharness/agent/pkg/model/proto/v1" v14 "github.com/pluggableharness/agent/pkg/render/proto/v1" - v1 "github.com/pluggableharness/agent/pkg/slashcommand/proto/v1" + v11 "github.com/pluggableharness/agent/pkg/slashcommand/proto/v1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" structpb "google.golang.org/protobuf/types/known/structpb" @@ -33,67 +40,6 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// Stability hints whether a ContextSection's content changes turn to turn, -// used both as ContextCapabilities' provider-wide declaration and per -// ContextSection. context.md §7: this is a direct translation of the -// research's strongest cross-cutting finding — harnesses converge on a -// tools -> system -> static-project-context -> conversation-tail prefix -// ordering because it is a constraint, not a preference, for prompt-cache -// reuse. -type Stability int32 - -const ( - // Zero value. Never valid for a real capability or section declaration; - // its presence on the wire means a caller forgot to set the field. - Stability_STABILITY_UNSPECIFIED Stability = 0 - // Content that doesn't change turn to turn for the life of the session, - // e.g. a repo's CLAUDE.md. - Stability_STABILITY_STATIC Stability = 1 - // Content that's recomputed per turn, e.g. git status or a file tree. - Stability_STABILITY_DYNAMIC Stability = 2 -) - -// Enum value maps for Stability. -var ( - Stability_name = map[int32]string{ - 0: "STABILITY_UNSPECIFIED", - 1: "STABILITY_STATIC", - 2: "STABILITY_DYNAMIC", - } - Stability_value = map[string]int32{ - "STABILITY_UNSPECIFIED": 0, - "STABILITY_STATIC": 1, - "STABILITY_DYNAMIC": 2, - } -) - -func (x Stability) Enum() *Stability { - p := new(Stability) - *p = x - return p -} - -func (x Stability) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (Stability) Descriptor() protoreflect.EnumDescriptor { - return file_pluggableharness_agent_context_v1_context_proto_enumTypes[0].Descriptor() -} - -func (Stability) Type() protoreflect.EnumType { - return &file_pluggableharness_agent_context_v1_context_proto_enumTypes[0] -} - -func (x Stability) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use Stability.Descriptor instead. -func (Stability) EnumDescriptor() ([]byte, []int) { - return file_pluggableharness_agent_context_v1_context_proto_rawDescGZIP(), []int{0} -} - // ContextErrorCategory classifies a context provider's failures. // context.md §10 — smaller than the model-provider taxonomy // (model.md §8), but a plugin MUST still classify failures rather than @@ -155,11 +101,11 @@ func (x ContextErrorCategory) String() string { } func (ContextErrorCategory) Descriptor() protoreflect.EnumDescriptor { - return file_pluggableharness_agent_context_v1_context_proto_enumTypes[1].Descriptor() + return file_pluggableharness_agent_context_v1_context_proto_enumTypes[0].Descriptor() } func (ContextErrorCategory) Type() protoreflect.EnumType { - return &file_pluggableharness_agent_context_v1_context_proto_enumTypes[1] + return &file_pluggableharness_agent_context_v1_context_proto_enumTypes[0] } func (x ContextErrorCategory) Number() protoreflect.EnumNumber { @@ -168,7 +114,7 @@ func (x ContextErrorCategory) Number() protoreflect.EnumNumber { // Deprecated: Use ContextErrorCategory.Descriptor instead. func (ContextErrorCategory) EnumDescriptor() ([]byte, []int) { - return file_pluggableharness_agent_context_v1_context_proto_rawDescGZIP(), []int{1} + return file_pluggableharness_agent_context_v1_context_proto_rawDescGZIP(), []int{0} } // GetCapabilitiesRequest carries no fields — GetCapabilities takes no @@ -316,7 +262,7 @@ type ContextCapabilities struct { DefaultTokenBudget int64 `protobuf:"varint,1,opt,name=default_token_budget,json=defaultTokenBudget,proto3" json:"default_token_budget,omitempty"` // Whether this provider's contributed content changes turn to turn. MUST // be set. context.md §2, §7. - Stability Stability `protobuf:"varint,2,opt,name=stability,proto3,enum=pluggableharness.agent.context.v1.Stability" json:"stability,omitempty"` + Stability v1.Stability `protobuf:"varint,2,opt,name=stability,proto3,enum=pluggableharness.agent.content.v1.Stability" json:"stability,omitempty"` // Whether this provider acts as a compactor: MAY rewrite, merge, or drop // other providers' sections in the chain it receives, and MAY receive // conversation_history on ContextRequest and return rewritten_history. @@ -324,10 +270,10 @@ type ContextCapabilities struct { Compactor bool `protobuf:"varint,3,opt,name=compactor,proto3" json:"compactor,omitempty"` // Slash commands this provider contributes. MAY be empty. // context.md §2, configuration.md §5. - SlashCommands []*v1.SlashCommandSpec `protobuf:"bytes,4,rep,name=slash_commands,json=slashCommands,proto3" json:"slash_commands,omitempty"` + SlashCommands []*v11.SlashCommandSpec `protobuf:"bytes,4,rep,name=slash_commands,json=slashCommands,proto3" json:"slash_commands,omitempty"` // This provider's agent.hcl config schema, advertised so the kernel knows // what fields Configure accepts. configuration.md §4. - ConfigSchema *v11.ConfigSchema `protobuf:"bytes,5,opt,name=config_schema,json=configSchema,proto3" json:"config_schema,omitempty"` + ConfigSchema *v12.ConfigSchema `protobuf:"bytes,5,opt,name=config_schema,json=configSchema,proto3" json:"config_schema,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -369,11 +315,11 @@ func (x *ContextCapabilities) GetDefaultTokenBudget() int64 { return 0 } -func (x *ContextCapabilities) GetStability() Stability { +func (x *ContextCapabilities) GetStability() v1.Stability { if x != nil { return x.Stability } - return Stability_STABILITY_UNSPECIFIED + return v1.Stability(0) } func (x *ContextCapabilities) GetCompactor() bool { @@ -383,14 +329,14 @@ func (x *ContextCapabilities) GetCompactor() bool { return false } -func (x *ContextCapabilities) GetSlashCommands() []*v1.SlashCommandSpec { +func (x *ContextCapabilities) GetSlashCommands() []*v11.SlashCommandSpec { if x != nil { return x.SlashCommands } return nil } -func (x *ContextCapabilities) GetConfigSchema() *v11.ConfigSchema { +func (x *ContextCapabilities) GetConfigSchema() *v12.ConfigSchema { if x != nil { return x.ConfigSchema } @@ -455,7 +401,7 @@ type ContextRequest struct { // The model this contribution is being assembled for, so the provider can // tailor content (and compute tokens against the right budget) for the // model that will actually consume it. MUST be set. context.md §4. - ModelTarget *v12.ModelTarget `protobuf:"bytes,5,opt,name=model_target,json=modelTarget,proto3" json:"model_target,omitempty"` + ModelTarget *v13.ModelTarget `protobuf:"bytes,5,opt,name=model_target,json=modelTarget,proto3" json:"model_target,omitempty"` // Paths touched so far this session, enabling JIT-scoped contributions // (e.g. a subdirectory-scoped convention-file reader). MAY be empty, e.g. // at turn 0 / session start. context.md §4, §8. @@ -465,13 +411,13 @@ type ContextRequest struct { // The accumulated output of earlier providers in this hook's // declaration-order chain. MUST be set (MAY be empty on the first // provider in the chain). context.md §4, §5. - PriorSections []*ContextSection `protobuf:"bytes,8,rep,name=prior_sections,json=priorSections,proto3" json:"prior_sections,omitempty"` + PriorSections []*v1.ContextSection `protobuf:"bytes,8,rep,name=prior_sections,json=priorSections,proto3" json:"prior_sections,omitempty"` // The session's conversation history. Populated ONLY for a provider whose // ContextCapabilities.compactor == true; a non-compactor provider MUST // NOT receive this — for those providers this field arrives empty, which // is indistinguishable from (and semantically equivalent to) "not // provided". context.md §5.1. - ConversationHistory []*v13.Message `protobuf:"bytes,9,rep,name=conversation_history,json=conversationHistory,proto3" json:"conversation_history,omitempty"` + ConversationHistory []*v1.Message `protobuf:"bytes,9,rep,name=conversation_history,json=conversationHistory,proto3" json:"conversation_history,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -534,7 +480,7 @@ func (x *ContextRequest) GetTokenBudget() int64 { return 0 } -func (x *ContextRequest) GetModelTarget() *v12.ModelTarget { +func (x *ContextRequest) GetModelTarget() *v13.ModelTarget { if x != nil { return x.ModelTarget } @@ -555,123 +501,20 @@ func (x *ContextRequest) GetWorkingDirectory() string { return "" } -func (x *ContextRequest) GetPriorSections() []*ContextSection { +func (x *ContextRequest) GetPriorSections() []*v1.ContextSection { if x != nil { return x.PriorSections } return nil } -func (x *ContextRequest) GetConversationHistory() []*v13.Message { +func (x *ContextRequest) GetConversationHistory() []*v1.Message { if x != nil { return x.ConversationHistory } return nil } -// ContextSection is one provider's contribution to the assembled prompt -// context. context.md §4, §7. -type ContextSection struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The producing plugin's declared name — a plain string, not a - // common.v1.ProducerRef, used as the identity key for a provider - // re-finding and replacing its own prior section. - Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` - // Human-readable label the kernel uses to wrap this section in a clearly - // delimited boundary when concatenating the chain into the final prompt. - // MUST be set. context.md §4, §7. - Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"` - // The section's content, in emission order. MUST be set. Text-only in - // v1 — the kernel MUST reject a non-text block here rather than silently - // dropping it. context.md §4, §7, §11. - Content []*v13.ContentBlock `protobuf:"bytes,3,rep,name=content,proto3" json:"content,omitempty"` - // This section's token count, computed via the kernel's CountTokens - // callback (kernel-callbacks.md §2), never a provider-local heuristic. - // MUST be set. context.md §4. - Tokens int64 `protobuf:"varint,4,opt,name=tokens,proto3" json:"tokens,omitempty"` - // Whether this section's content changes turn to turn. context.md §7. - Stability Stability `protobuf:"varint,5,opt,name=stability,proto3,enum=pluggableharness.agent.context.v1.Stability" json:"stability,omitempty"` - // Whether this section was truncated to fit its budget. Per context.md - // §6, setting this true is not itself sufficient to satisfy the budget - // constraint — a section that still exceeds token_budget MUST be - // rejected by the kernel regardless of this flag. - Truncated bool `protobuf:"varint,6,opt,name=truncated,proto3" json:"truncated,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ContextSection) Reset() { - *x = ContextSection{} - mi := &file_pluggableharness_agent_context_v1_context_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ContextSection) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ContextSection) ProtoMessage() {} - -func (x *ContextSection) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_context_v1_context_proto_msgTypes[6] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ContextSection.ProtoReflect.Descriptor instead. -func (*ContextSection) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_context_v1_context_proto_rawDescGZIP(), []int{6} -} - -func (x *ContextSection) GetProvider() string { - if x != nil { - return x.Provider - } - return "" -} - -func (x *ContextSection) GetLabel() string { - if x != nil { - return x.Label - } - return "" -} - -func (x *ContextSection) GetContent() []*v13.ContentBlock { - if x != nil { - return x.Content - } - return nil -} - -func (x *ContextSection) GetTokens() int64 { - if x != nil { - return x.Tokens - } - return 0 -} - -func (x *ContextSection) GetStability() Stability { - if x != nil { - return x.Stability - } - return Stability_STABILITY_UNSPECIFIED -} - -func (x *ContextSection) GetTruncated() bool { - if x != nil { - return x.Truncated - } - return false -} - // ContextContribution is Contribute's response: the full, possibly-modified // section chain, with this provider's own section appended — never a // delta. context.md §4. @@ -679,21 +522,21 @@ type ContextContribution struct { state protoimpl.MessageState `protogen:"open.v1"` // The full accumulated chain, in declaration order, including this // provider's own new or updated section(s). - Sections []*ContextSection `protobuf:"bytes,1,rep,name=sections,proto3" json:"sections,omitempty"` + Sections []*v1.ContextSection `protobuf:"bytes,1,rep,name=sections,proto3" json:"sections,omitempty"` // The session's conversation history, rewritten to replace what was sent // in ContextRequest.conversation_history. MAY be included by a compactor // provider (ContextCapabilities.compactor == true) alongside its section // contribution. When present, the kernel MUST replace the turn's // conversation history with this before the next model call. context.md // §5.1. - RewrittenHistory []*v13.Message `protobuf:"bytes,2,rep,name=rewritten_history,json=rewrittenHistory,proto3" json:"rewritten_history,omitempty"` + RewrittenHistory []*v1.Message `protobuf:"bytes,2,rep,name=rewritten_history,json=rewrittenHistory,proto3" json:"rewritten_history,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ContextContribution) Reset() { *x = ContextContribution{} - mi := &file_pluggableharness_agent_context_v1_context_proto_msgTypes[7] + mi := &file_pluggableharness_agent_context_v1_context_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -705,7 +548,7 @@ func (x *ContextContribution) String() string { func (*ContextContribution) ProtoMessage() {} func (x *ContextContribution) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_context_v1_context_proto_msgTypes[7] + mi := &file_pluggableharness_agent_context_v1_context_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -718,17 +561,17 @@ func (x *ContextContribution) ProtoReflect() protoreflect.Message { // Deprecated: Use ContextContribution.ProtoReflect.Descriptor instead. func (*ContextContribution) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_context_v1_context_proto_rawDescGZIP(), []int{7} + return file_pluggableharness_agent_context_v1_context_proto_rawDescGZIP(), []int{6} } -func (x *ContextContribution) GetSections() []*ContextSection { +func (x *ContextContribution) GetSections() []*v1.ContextSection { if x != nil { return x.Sections } return nil } -func (x *ContextContribution) GetRewrittenHistory() []*v13.Message { +func (x *ContextContribution) GetRewrittenHistory() []*v1.Message { if x != nil { return x.RewrittenHistory } @@ -753,7 +596,7 @@ type ContextError struct { func (x *ContextError) Reset() { *x = ContextError{} - mi := &file_pluggableharness_agent_context_v1_context_proto_msgTypes[8] + mi := &file_pluggableharness_agent_context_v1_context_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -765,7 +608,7 @@ func (x *ContextError) String() string { func (*ContextError) ProtoMessage() {} func (x *ContextError) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_context_v1_context_proto_msgTypes[8] + mi := &file_pluggableharness_agent_context_v1_context_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -778,7 +621,7 @@ func (x *ContextError) ProtoReflect() protoreflect.Message { // Deprecated: Use ContextError.ProtoReflect.Descriptor instead. func (*ContextError) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_context_v1_context_proto_rawDescGZIP(), []int{8} + return file_pluggableharness_agent_context_v1_context_proto_rawDescGZIP(), []int{7} } func (x *ContextError) GetCategory() ContextErrorCategory { @@ -816,7 +659,7 @@ type RenderRequest struct { func (x *RenderRequest) Reset() { *x = RenderRequest{} - mi := &file_pluggableharness_agent_context_v1_context_proto_msgTypes[9] + mi := &file_pluggableharness_agent_context_v1_context_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -828,7 +671,7 @@ func (x *RenderRequest) String() string { func (*RenderRequest) ProtoMessage() {} func (x *RenderRequest) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_context_v1_context_proto_msgTypes[9] + mi := &file_pluggableharness_agent_context_v1_context_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -841,7 +684,7 @@ func (x *RenderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RenderRequest.ProtoReflect.Descriptor instead. func (*RenderRequest) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_context_v1_context_proto_rawDescGZIP(), []int{9} + return file_pluggableharness_agent_context_v1_context_proto_rawDescGZIP(), []int{8} } func (x *RenderRequest) GetPayload() []byte { @@ -863,7 +706,7 @@ type RenderResponse struct { func (x *RenderResponse) Reset() { *x = RenderResponse{} - mi := &file_pluggableharness_agent_context_v1_context_proto_msgTypes[10] + mi := &file_pluggableharness_agent_context_v1_context_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -875,7 +718,7 @@ func (x *RenderResponse) String() string { func (*RenderResponse) ProtoMessage() {} func (x *RenderResponse) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_context_v1_context_proto_msgTypes[10] + mi := &file_pluggableharness_agent_context_v1_context_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -888,7 +731,7 @@ func (x *RenderResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RenderResponse.ProtoReflect.Descriptor instead. func (*RenderResponse) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_context_v1_context_proto_rawDescGZIP(), []int{10} + return file_pluggableharness_agent_context_v1_context_proto_rawDescGZIP(), []int{9} } func (x *RenderResponse) GetTree() *v14.RenderTree { @@ -910,7 +753,7 @@ const file_pluggableharness_agent_context_v1_context_proto_rawDesc = "" + "\x06config\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x06config\"\xe7\x02\n" + "\x13ContextCapabilities\x120\n" + "\x14default_token_budget\x18\x01 \x01(\x03R\x12defaultTokenBudget\x12J\n" + - "\tstability\x18\x02 \x01(\x0e2,.pluggableharness.agent.context.v1.StabilityR\tstability\x12\x1c\n" + + "\tstability\x18\x02 \x01(\x0e2,.pluggableharness.agent.content.v1.StabilityR\tstability\x12\x1c\n" + "\tcompactor\x18\x03 \x01(\bR\tcompactor\x12_\n" + "\x0eslash_commands\x18\x04 \x03(\v28.pluggableharness.agent.slashcommand.v1.SlashCommandSpecR\rslashCommands\x12S\n" + "\rconfig_schema\x18\x05 \x01(\v2..pluggableharness.agent.config.v1.ConfigSchemaR\fconfigSchema\"\x13\n" + @@ -925,17 +768,10 @@ const file_pluggableharness_agent_context_v1_context_proto_rawDesc = "" + "\fmodel_target\x18\x05 \x01(\v2,.pluggableharness.agent.model.v1.ModelTargetR\vmodelTarget\x12#\n" + "\rfiles_touched\x18\x06 \x03(\tR\ffilesTouched\x12+\n" + "\x11working_directory\x18\a \x01(\tR\x10workingDirectory\x12X\n" + - "\x0eprior_sections\x18\b \x03(\v21.pluggableharness.agent.context.v1.ContextSectionR\rpriorSections\x12]\n" + - "\x14conversation_history\x18\t \x03(\v2*.pluggableharness.agent.content.v1.MessageR\x13conversationHistory\"\x8f\x02\n" + - "\x0eContextSection\x12\x1a\n" + - "\bprovider\x18\x01 \x01(\tR\bprovider\x12\x14\n" + - "\x05label\x18\x02 \x01(\tR\x05label\x12I\n" + - "\acontent\x18\x03 \x03(\v2/.pluggableharness.agent.content.v1.ContentBlockR\acontent\x12\x16\n" + - "\x06tokens\x18\x04 \x01(\x03R\x06tokens\x12J\n" + - "\tstability\x18\x05 \x01(\x0e2,.pluggableharness.agent.context.v1.StabilityR\tstability\x12\x1c\n" + - "\ttruncated\x18\x06 \x01(\bR\ttruncated\"\xbd\x01\n" + + "\x0eprior_sections\x18\b \x03(\v21.pluggableharness.agent.content.v1.ContextSectionR\rpriorSections\x12]\n" + + "\x14conversation_history\x18\t \x03(\v2*.pluggableharness.agent.content.v1.MessageR\x13conversationHistory\"\xbd\x01\n" + "\x13ContextContribution\x12M\n" + - "\bsections\x18\x01 \x03(\v21.pluggableharness.agent.context.v1.ContextSectionR\bsections\x12W\n" + + "\bsections\x18\x01 \x03(\v21.pluggableharness.agent.content.v1.ContextSectionR\bsections\x12W\n" + "\x11rewritten_history\x18\x02 \x03(\v2*.pluggableharness.agent.content.v1.MessageR\x10rewrittenHistory\"\x9b\x01\n" + "\fContextError\x12S\n" + "\bcategory\x18\x01 \x01(\x0e27.pluggableharness.agent.context.v1.ContextErrorCategoryR\bcategory\x12\x18\n" + @@ -944,11 +780,7 @@ const file_pluggableharness_agent_context_v1_context_proto_rawDesc = "" + "\rRenderRequest\x12\x18\n" + "\apayload\x18\x01 \x01(\fR\apayload\"R\n" + "\x0eRenderResponse\x12@\n" + - "\x04tree\x18\x01 \x01(\v2,.pluggableharness.agent.render.v1.RenderTreeR\x04tree*S\n" + - "\tStability\x12\x19\n" + - "\x15STABILITY_UNSPECIFIED\x10\x00\x12\x14\n" + - "\x10STABILITY_STATIC\x10\x01\x12\x15\n" + - "\x11STABILITY_DYNAMIC\x10\x02*\x95\x02\n" + + "\x04tree\x18\x01 \x01(\v2,.pluggableharness.agent.render.v1.RenderTreeR\x04tree*\x95\x02\n" + "\x14ContextErrorCategory\x12&\n" + "\"CONTEXT_ERROR_CATEGORY_UNSPECIFIED\x10\x00\x12-\n" + ")CONTEXT_ERROR_CATEGORY_SOURCE_UNAVAILABLE\x10\x01\x12*\n" + @@ -975,58 +807,55 @@ func file_pluggableharness_agent_context_v1_context_proto_rawDescGZIP() []byte { return file_pluggableharness_agent_context_v1_context_proto_rawDescData } -var file_pluggableharness_agent_context_v1_context_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_pluggableharness_agent_context_v1_context_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_pluggableharness_agent_context_v1_context_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_pluggableharness_agent_context_v1_context_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_pluggableharness_agent_context_v1_context_proto_goTypes = []any{ - (Stability)(0), // 0: pluggableharness.agent.context.v1.Stability - (ContextErrorCategory)(0), // 1: pluggableharness.agent.context.v1.ContextErrorCategory - (*GetCapabilitiesRequest)(nil), // 2: pluggableharness.agent.context.v1.GetCapabilitiesRequest - (*GetCapabilitiesResponse)(nil), // 3: pluggableharness.agent.context.v1.GetCapabilitiesResponse - (*ConfigureRequest)(nil), // 4: pluggableharness.agent.context.v1.ConfigureRequest - (*ContextCapabilities)(nil), // 5: pluggableharness.agent.context.v1.ContextCapabilities - (*ConfigureResponse)(nil), // 6: pluggableharness.agent.context.v1.ConfigureResponse - (*ContextRequest)(nil), // 7: pluggableharness.agent.context.v1.ContextRequest - (*ContextSection)(nil), // 8: pluggableharness.agent.context.v1.ContextSection - (*ContextContribution)(nil), // 9: pluggableharness.agent.context.v1.ContextContribution - (*ContextError)(nil), // 10: pluggableharness.agent.context.v1.ContextError - (*RenderRequest)(nil), // 11: pluggableharness.agent.context.v1.RenderRequest - (*RenderResponse)(nil), // 12: pluggableharness.agent.context.v1.RenderResponse - (*structpb.Struct)(nil), // 13: google.protobuf.Struct - (*v1.SlashCommandSpec)(nil), // 14: pluggableharness.agent.slashcommand.v1.SlashCommandSpec - (*v11.ConfigSchema)(nil), // 15: pluggableharness.agent.config.v1.ConfigSchema - (*v12.ModelTarget)(nil), // 16: pluggableharness.agent.model.v1.ModelTarget - (*v13.Message)(nil), // 17: pluggableharness.agent.content.v1.Message - (*v13.ContentBlock)(nil), // 18: pluggableharness.agent.content.v1.ContentBlock - (*v14.RenderTree)(nil), // 19: pluggableharness.agent.render.v1.RenderTree + (ContextErrorCategory)(0), // 0: pluggableharness.agent.context.v1.ContextErrorCategory + (*GetCapabilitiesRequest)(nil), // 1: pluggableharness.agent.context.v1.GetCapabilitiesRequest + (*GetCapabilitiesResponse)(nil), // 2: pluggableharness.agent.context.v1.GetCapabilitiesResponse + (*ConfigureRequest)(nil), // 3: pluggableharness.agent.context.v1.ConfigureRequest + (*ContextCapabilities)(nil), // 4: pluggableharness.agent.context.v1.ContextCapabilities + (*ConfigureResponse)(nil), // 5: pluggableharness.agent.context.v1.ConfigureResponse + (*ContextRequest)(nil), // 6: pluggableharness.agent.context.v1.ContextRequest + (*ContextContribution)(nil), // 7: pluggableharness.agent.context.v1.ContextContribution + (*ContextError)(nil), // 8: pluggableharness.agent.context.v1.ContextError + (*RenderRequest)(nil), // 9: pluggableharness.agent.context.v1.RenderRequest + (*RenderResponse)(nil), // 10: pluggableharness.agent.context.v1.RenderResponse + (*structpb.Struct)(nil), // 11: google.protobuf.Struct + (v1.Stability)(0), // 12: pluggableharness.agent.content.v1.Stability + (*v11.SlashCommandSpec)(nil), // 13: pluggableharness.agent.slashcommand.v1.SlashCommandSpec + (*v12.ConfigSchema)(nil), // 14: pluggableharness.agent.config.v1.ConfigSchema + (*v13.ModelTarget)(nil), // 15: pluggableharness.agent.model.v1.ModelTarget + (*v1.ContextSection)(nil), // 16: pluggableharness.agent.content.v1.ContextSection + (*v1.Message)(nil), // 17: pluggableharness.agent.content.v1.Message + (*v14.RenderTree)(nil), // 18: pluggableharness.agent.render.v1.RenderTree } var file_pluggableharness_agent_context_v1_context_proto_depIdxs = []int32{ - 5, // 0: pluggableharness.agent.context.v1.GetCapabilitiesResponse.capabilities:type_name -> pluggableharness.agent.context.v1.ContextCapabilities - 13, // 1: pluggableharness.agent.context.v1.ConfigureRequest.config:type_name -> google.protobuf.Struct - 0, // 2: pluggableharness.agent.context.v1.ContextCapabilities.stability:type_name -> pluggableharness.agent.context.v1.Stability - 14, // 3: pluggableharness.agent.context.v1.ContextCapabilities.slash_commands:type_name -> pluggableharness.agent.slashcommand.v1.SlashCommandSpec - 15, // 4: pluggableharness.agent.context.v1.ContextCapabilities.config_schema:type_name -> pluggableharness.agent.config.v1.ConfigSchema - 16, // 5: pluggableharness.agent.context.v1.ContextRequest.model_target:type_name -> pluggableharness.agent.model.v1.ModelTarget - 8, // 6: pluggableharness.agent.context.v1.ContextRequest.prior_sections:type_name -> pluggableharness.agent.context.v1.ContextSection + 4, // 0: pluggableharness.agent.context.v1.GetCapabilitiesResponse.capabilities:type_name -> pluggableharness.agent.context.v1.ContextCapabilities + 11, // 1: pluggableharness.agent.context.v1.ConfigureRequest.config:type_name -> google.protobuf.Struct + 12, // 2: pluggableharness.agent.context.v1.ContextCapabilities.stability:type_name -> pluggableharness.agent.content.v1.Stability + 13, // 3: pluggableharness.agent.context.v1.ContextCapabilities.slash_commands:type_name -> pluggableharness.agent.slashcommand.v1.SlashCommandSpec + 14, // 4: pluggableharness.agent.context.v1.ContextCapabilities.config_schema:type_name -> pluggableharness.agent.config.v1.ConfigSchema + 15, // 5: pluggableharness.agent.context.v1.ContextRequest.model_target:type_name -> pluggableharness.agent.model.v1.ModelTarget + 16, // 6: pluggableharness.agent.context.v1.ContextRequest.prior_sections:type_name -> pluggableharness.agent.content.v1.ContextSection 17, // 7: pluggableharness.agent.context.v1.ContextRequest.conversation_history:type_name -> pluggableharness.agent.content.v1.Message - 18, // 8: pluggableharness.agent.context.v1.ContextSection.content:type_name -> pluggableharness.agent.content.v1.ContentBlock - 0, // 9: pluggableharness.agent.context.v1.ContextSection.stability:type_name -> pluggableharness.agent.context.v1.Stability - 8, // 10: pluggableharness.agent.context.v1.ContextContribution.sections:type_name -> pluggableharness.agent.context.v1.ContextSection - 17, // 11: pluggableharness.agent.context.v1.ContextContribution.rewritten_history:type_name -> pluggableharness.agent.content.v1.Message - 1, // 12: pluggableharness.agent.context.v1.ContextError.category:type_name -> pluggableharness.agent.context.v1.ContextErrorCategory - 19, // 13: pluggableharness.agent.context.v1.RenderResponse.tree:type_name -> pluggableharness.agent.render.v1.RenderTree - 2, // 14: pluggableharness.agent.context.v1.ContextService.GetCapabilities:input_type -> pluggableharness.agent.context.v1.GetCapabilitiesRequest - 4, // 15: pluggableharness.agent.context.v1.ContextService.Configure:input_type -> pluggableharness.agent.context.v1.ConfigureRequest - 7, // 16: pluggableharness.agent.context.v1.ContextService.Contribute:input_type -> pluggableharness.agent.context.v1.ContextRequest - 11, // 17: pluggableharness.agent.context.v1.ContextService.Render:input_type -> pluggableharness.agent.context.v1.RenderRequest - 3, // 18: pluggableharness.agent.context.v1.ContextService.GetCapabilities:output_type -> pluggableharness.agent.context.v1.GetCapabilitiesResponse - 6, // 19: pluggableharness.agent.context.v1.ContextService.Configure:output_type -> pluggableharness.agent.context.v1.ConfigureResponse - 9, // 20: pluggableharness.agent.context.v1.ContextService.Contribute:output_type -> pluggableharness.agent.context.v1.ContextContribution - 12, // 21: pluggableharness.agent.context.v1.ContextService.Render:output_type -> pluggableharness.agent.context.v1.RenderResponse - 18, // [18:22] is the sub-list for method output_type - 14, // [14:18] is the sub-list for method input_type - 14, // [14:14] is the sub-list for extension type_name - 14, // [14:14] is the sub-list for extension extendee - 0, // [0:14] is the sub-list for field type_name + 16, // 8: pluggableharness.agent.context.v1.ContextContribution.sections:type_name -> pluggableharness.agent.content.v1.ContextSection + 17, // 9: pluggableharness.agent.context.v1.ContextContribution.rewritten_history:type_name -> pluggableharness.agent.content.v1.Message + 0, // 10: pluggableharness.agent.context.v1.ContextError.category:type_name -> pluggableharness.agent.context.v1.ContextErrorCategory + 18, // 11: pluggableharness.agent.context.v1.RenderResponse.tree:type_name -> pluggableharness.agent.render.v1.RenderTree + 1, // 12: pluggableharness.agent.context.v1.ContextService.GetCapabilities:input_type -> pluggableharness.agent.context.v1.GetCapabilitiesRequest + 3, // 13: pluggableharness.agent.context.v1.ContextService.Configure:input_type -> pluggableharness.agent.context.v1.ConfigureRequest + 6, // 14: pluggableharness.agent.context.v1.ContextService.Contribute:input_type -> pluggableharness.agent.context.v1.ContextRequest + 9, // 15: pluggableharness.agent.context.v1.ContextService.Render:input_type -> pluggableharness.agent.context.v1.RenderRequest + 2, // 16: pluggableharness.agent.context.v1.ContextService.GetCapabilities:output_type -> pluggableharness.agent.context.v1.GetCapabilitiesResponse + 5, // 17: pluggableharness.agent.context.v1.ContextService.Configure:output_type -> pluggableharness.agent.context.v1.ConfigureResponse + 7, // 18: pluggableharness.agent.context.v1.ContextService.Contribute:output_type -> pluggableharness.agent.context.v1.ContextContribution + 10, // 19: pluggableharness.agent.context.v1.ContextService.Render:output_type -> pluggableharness.agent.context.v1.RenderResponse + 16, // [16:20] is the sub-list for method output_type + 12, // [12:16] is the sub-list for method input_type + 12, // [12:12] is the sub-list for extension type_name + 12, // [12:12] is the sub-list for extension extendee + 0, // [0:12] is the sub-list for field type_name } func init() { file_pluggableharness_agent_context_v1_context_proto_init() } @@ -1039,8 +868,8 @@ func file_pluggableharness_agent_context_v1_context_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_pluggableharness_agent_context_v1_context_proto_rawDesc), len(file_pluggableharness_agent_context_v1_context_proto_rawDesc)), - NumEnums: 2, - NumMessages: 11, + NumEnums: 1, + NumMessages: 10, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/context/proto/v1/context_grpc.pb.go b/pkg/context/proto/v1/context_grpc.pb.go index c1c1e9b..b2796d5 100644 --- a/pkg/context/proto/v1/context_grpc.pb.go +++ b/pkg/context/proto/v1/context_grpc.pb.go @@ -9,6 +9,13 @@ // context-assemble and contribute content to the prompt before each model // call (e.g. a CLAUDE.md reader, an AGENTS.md reader, a git-status/file-tree // summarizer). See .claude/rules/proto.md. +// +// ContextSection and Stability — the section chain this protocol assembles +// and the turn-to-turn-change hint each section carries — are defined in +// pluggableharness.agent.content.v1, not here: that chain is consumed by +// both this protocol and the model provider's completion request +// (forthcoming), so it's homed alongside content.v1's other shared content +// shapes rather than duplicated or owned by only one consumer. package contextv1 diff --git a/pkg/hook/proto/v1/hook.pb.go b/pkg/hook/proto/v1/hook.pb.go new file mode 100644 index 0000000..590313d --- /dev/null +++ b/pkg/hook/proto/v1/hook.pb.go @@ -0,0 +1,1737 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: pluggableharness/agent/hook/v1/hook.proto + +// Package pluggableharness.agent.hook.v1 defines the hook-dispatch RPC surface +// described in agent-loop/hook-dispatch.md and architecture.md §Hook +// dispatch semantics: the wire contract the kernel uses to invoke any +// plugin (of any of the six categories) that declares a `hook{}` block in +// agent.hcl. This is deliberately one shared service rather than a +// per-category RPC — hashicorp/go-plugin (.claude/rules/plugin-runtime.md) +// muxes multiple gRPC services over one broker connection, so the kernel +// dials HookSubscriberService on the same subprocess that already serves +// that plugin's own category service. A plugin with no `hook{}` blocks in +// agent.hcl simply never has it called. +// +// context-assemble is deliberately absent from this surface's HookPoint +// enum below — it stays on ContextService.Contribute +// (context/protocol.md#contribute-the-context-assemble-rpc; architecture.md +// §Hook dispatch semantics), which already carries the full accumulated +// ContextSection chain and doesn't need a second, competing dispatch path. +// This surface serves the other eight hook points only. + +package hookv1 + +import ( + v12 "github.com/pluggableharness/agent/pkg/common/proto/v1" + v1 "github.com/pluggableharness/agent/pkg/content/proto/v1" + v11 "github.com/pluggableharness/agent/pkg/model/proto/v1" + v14 "github.com/pluggableharness/agent/pkg/plan/proto/v1" + v15 "github.com/pluggableharness/agent/pkg/session/proto/v1" + v13 "github.com/pluggableharness/agent/pkg/tool/proto/v1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// HookPoint identifies which of the eight dispatchable points in the agent +// loop a DispatchHookRequest fires for. Not carried directly on +// DispatchHookRequest (the set HookPayload oneof variant already implies +// the point) — this enum exists for HookError, which has no oneof to infer +// a point from. architecture.md §Hook dispatch semantics enumerates these +// nine hook-point names; context-assemble (the ninth) is deliberately +// excluded here — see this file's package comment. +type HookPoint int32 + +const ( + // Zero value. Never valid on the wire; its presence means a caller + // forgot to set the field. + HookPoint_HOOK_POINT_UNSPECIFIED HookPoint = 0 + // Session creation, before the first turn begins. + HookPoint_HOOK_POINT_SESSION_START HookPoint = 1 + // Immediately before a model provider's StreamCompletion is called. + HookPoint_HOOK_POINT_PRE_MODEL_CALL HookPoint = 2 + // Immediately after a model turn's canonical message has been + // assembled from the completion stream. + HookPoint_HOOK_POINT_POST_MODEL_RESPONSE HookPoint = 3 + // Immediately before a plan item's tool call is applied. + HookPoint_HOOK_POINT_PRE_TOOL_CALL HookPoint = 4 + // Once a turn's Plan has been fully built, before plan/apply gate + // dispatch. The kernel-privileged policy veto subscriber + // (architecture.md §Policy — first-party, not a plugin category) always + // runs at this point. + HookPoint_HOOK_POINT_PLAN_READY HookPoint = 5 + // Immediately after a plan item's tool call has produced a terminal + // ToolResult or ToolError. + HookPoint_HOOK_POINT_POST_TOOL_CALL HookPoint = 6 + // Immediately after a turn's whole Plan has finished applying (every + // item reached a terminal ApplyOutcome). + HookPoint_HOOK_POINT_POST_APPLY HookPoint = 7 + // Session termination, once the session has reached a terminal + // SessionStatus. + HookPoint_HOOK_POINT_SESSION_END HookPoint = 8 +) + +// Enum value maps for HookPoint. +var ( + HookPoint_name = map[int32]string{ + 0: "HOOK_POINT_UNSPECIFIED", + 1: "HOOK_POINT_SESSION_START", + 2: "HOOK_POINT_PRE_MODEL_CALL", + 3: "HOOK_POINT_POST_MODEL_RESPONSE", + 4: "HOOK_POINT_PRE_TOOL_CALL", + 5: "HOOK_POINT_PLAN_READY", + 6: "HOOK_POINT_POST_TOOL_CALL", + 7: "HOOK_POINT_POST_APPLY", + 8: "HOOK_POINT_SESSION_END", + } + HookPoint_value = map[string]int32{ + "HOOK_POINT_UNSPECIFIED": 0, + "HOOK_POINT_SESSION_START": 1, + "HOOK_POINT_PRE_MODEL_CALL": 2, + "HOOK_POINT_POST_MODEL_RESPONSE": 3, + "HOOK_POINT_PRE_TOOL_CALL": 4, + "HOOK_POINT_PLAN_READY": 5, + "HOOK_POINT_POST_TOOL_CALL": 6, + "HOOK_POINT_POST_APPLY": 7, + "HOOK_POINT_SESSION_END": 8, + } +) + +func (x HookPoint) Enum() *HookPoint { + p := new(HookPoint) + *p = x + return p +} + +func (x HookPoint) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (HookPoint) Descriptor() protoreflect.EnumDescriptor { + return file_pluggableharness_agent_hook_v1_hook_proto_enumTypes[0].Descriptor() +} + +func (HookPoint) Type() protoreflect.EnumType { + return &file_pluggableharness_agent_hook_v1_hook_proto_enumTypes[0] +} + +func (x HookPoint) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use HookPoint.Descriptor instead. +func (HookPoint) EnumDescriptor() ([]byte, []int) { + return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{0} +} + +// HookMode is the operator-declared subscription mode for one plugin's +// hook{} block, per architecture.md §Hook dispatch semantics and +// agent-loop/hook-dispatch.md#dispatch-order-and-payload-flow. Mode is +// per-subscription (agent.hcl-declared), not per-payload — it is not +// carried in HookPayload itself, only echoed on DispatchHookRequest so the +// subscriber knows which of DispatchHookResponse's three outcome shapes is +// expected of it. +type HookMode int32 + +const ( + // Zero value. Never valid on the wire; its presence means a caller + // forgot to set the field. + HookMode_HOOK_MODE_UNSPECIFIED HookMode = 0 + // Read-only, fire-and-forget. A raised error or malformed response is + // logged and dispatch continues; an observe subscriber can never alter + // the payload or abort the chain + // (agent-loop/hook-dispatch.md#subscriber-error-handling). + HookMode_HOOK_MODE_OBSERVE HookMode = 1 + // Sequential chain member: receives the prior stage's payload, returns + // a modified version of the same variant. An error or malformed + // response aborts the remainder of that hook's chain and surfaces a + // hook_error event (agent-loop/hook-dispatch.md#subscriber-error-handling). + HookMode_HOOK_MODE_TRANSFORM HookMode = 2 + // Returns an explicit allow/deny verdict. An error or timeout is + // treated identically to an explicit deny — fail-closed + // (agent-loop/hook-dispatch.md#timeout-behavior). + HookMode_HOOK_MODE_VETO HookMode = 3 +) + +// Enum value maps for HookMode. +var ( + HookMode_name = map[int32]string{ + 0: "HOOK_MODE_UNSPECIFIED", + 1: "HOOK_MODE_OBSERVE", + 2: "HOOK_MODE_TRANSFORM", + 3: "HOOK_MODE_VETO", + } + HookMode_value = map[string]int32{ + "HOOK_MODE_UNSPECIFIED": 0, + "HOOK_MODE_OBSERVE": 1, + "HOOK_MODE_TRANSFORM": 2, + "HOOK_MODE_VETO": 3, + } +) + +func (x HookMode) Enum() *HookMode { + p := new(HookMode) + *p = x + return p +} + +func (x HookMode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (HookMode) Descriptor() protoreflect.EnumDescriptor { + return file_pluggableharness_agent_hook_v1_hook_proto_enumTypes[1].Descriptor() +} + +func (HookMode) Type() protoreflect.EnumType { + return &file_pluggableharness_agent_hook_v1_hook_proto_enumTypes[1] +} + +func (x HookMode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use HookMode.Descriptor instead. +func (HookMode) EnumDescriptor() ([]byte, []int) { + return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{1} +} + +// HookDecision is a veto subscriber's coarse allow/deny verdict over a +// whole HookPayload. Deliberately distinct from +// pluggableharness.agent.plan.v1.PlanDecision — that enum is per-plan-item +// and carries PENDING/ASK, which are meaningless for a hook-level veto; +// the two enums MUST NOT be merged, per .claude/rules/proto.md's +// no-untyped-overload discipline (see also ToolErrorCategory vs +// ModelErrorCategory for the same non-merge precedent). +type HookDecision int32 + +const ( + // Zero value. Never valid as a deliberately-chosen verdict; its + // presence on the wire means a caller forgot to set the field. + HookDecision_HOOK_DECISION_UNSPECIFIED HookDecision = 0 + // The dispatch chain proceeds normally. + HookDecision_HOOK_DECISION_ALLOW HookDecision = 1 + // The kernel MUST NOT let the vetoed action proceed. What "the action" + // means is point-specific: at HOOK_POINT_PLAN_READY it means the whole + // plan (or, for the kernel-privileged policy subscriber, the + // per-item decisions policy itself produces directly — a third-party + // veto subscriber at plan-ready returns only this coarse ALLOW/DENY + // over the whole plan, per architecture.md §Policy — first-party, not a + // plugin category). + HookDecision_HOOK_DECISION_DENY HookDecision = 2 +) + +// Enum value maps for HookDecision. +var ( + HookDecision_name = map[int32]string{ + 0: "HOOK_DECISION_UNSPECIFIED", + 1: "HOOK_DECISION_ALLOW", + 2: "HOOK_DECISION_DENY", + } + HookDecision_value = map[string]int32{ + "HOOK_DECISION_UNSPECIFIED": 0, + "HOOK_DECISION_ALLOW": 1, + "HOOK_DECISION_DENY": 2, + } +) + +func (x HookDecision) Enum() *HookDecision { + p := new(HookDecision) + *p = x + return p +} + +func (x HookDecision) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (HookDecision) Descriptor() protoreflect.EnumDescriptor { + return file_pluggableharness_agent_hook_v1_hook_proto_enumTypes[2].Descriptor() +} + +func (HookDecision) Type() protoreflect.EnumType { + return &file_pluggableharness_agent_hook_v1_hook_proto_enumTypes[2] +} + +func (x HookDecision) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use HookDecision.Descriptor instead. +func (HookDecision) EnumDescriptor() ([]byte, []int) { + return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{2} +} + +// HookErrorCategory classifies why a hook dispatch to one subscriber +// failed. Deliberately its own enum, not reused from tool.v1 or model.v1 — +// per .claude/rules/proto.md, a category proto's error taxonomy is never +// merged with another category's even where the surrounding shape is +// parallel. +type HookErrorCategory int32 + +const ( + // Zero value. Never valid for a real error; its presence on the wire + // means a caller forgot to set the field. + HookErrorCategory_HOOK_ERROR_CATEGORY_UNSPECIFIED HookErrorCategory = 0 + // A transform subscriber raised an error or returned a malformed + // response. Aborts the remainder of that hook's chain + // (agent-loop/hook-dispatch.md#subscriber-error-handling). + HookErrorCategory_HOOK_ERROR_CATEGORY_TRANSFORM_FAILED HookErrorCategory = 1 + // A veto subscriber raised an error. Treated identically to an + // explicit deny — fail-closed + // (agent-loop/hook-dispatch.md#subscriber-error-handling). + HookErrorCategory_HOOK_ERROR_CATEGORY_VETO_FAILED HookErrorCategory = 2 + // The subscriber exceeded its per-subscriber deadline + // (agent-loop/hook-dispatch.md#timeout-behavior). For a veto + // subscriber, resolved identically to HOOK_ERROR_CATEGORY_VETO_FAILED — + // fail-closed deny. + HookErrorCategory_HOOK_ERROR_CATEGORY_TIMEOUT HookErrorCategory = 3 + // The subscriber's DispatchHookResponse didn't match what its declared + // HookMode requires — wrong oneof variant, or (for transform) a + // payload variant that doesn't match the request's, or a mutation to a + // field this hook point doesn't document as transform-mutable. + HookErrorCategory_HOOK_ERROR_CATEGORY_INVALID_RESPONSE HookErrorCategory = 4 + // The subscriber's plugin subprocess died mid-dispatch (transport + // error, not a graceful error the plugin chose to return). MUST be + // kernel-synthesized only, mirroring + // tool/conformance.md#error-taxonomy's TOOL_ERROR_CATEGORY_PROCESS_CRASHED + // — a plugin process that crashes obviously cannot emit this itself. + HookErrorCategory_HOOK_ERROR_CATEGORY_PROCESS_CRASHED HookErrorCategory = 5 + // Anything else. + HookErrorCategory_HOOK_ERROR_CATEGORY_UNKNOWN HookErrorCategory = 6 +) + +// Enum value maps for HookErrorCategory. +var ( + HookErrorCategory_name = map[int32]string{ + 0: "HOOK_ERROR_CATEGORY_UNSPECIFIED", + 1: "HOOK_ERROR_CATEGORY_TRANSFORM_FAILED", + 2: "HOOK_ERROR_CATEGORY_VETO_FAILED", + 3: "HOOK_ERROR_CATEGORY_TIMEOUT", + 4: "HOOK_ERROR_CATEGORY_INVALID_RESPONSE", + 5: "HOOK_ERROR_CATEGORY_PROCESS_CRASHED", + 6: "HOOK_ERROR_CATEGORY_UNKNOWN", + } + HookErrorCategory_value = map[string]int32{ + "HOOK_ERROR_CATEGORY_UNSPECIFIED": 0, + "HOOK_ERROR_CATEGORY_TRANSFORM_FAILED": 1, + "HOOK_ERROR_CATEGORY_VETO_FAILED": 2, + "HOOK_ERROR_CATEGORY_TIMEOUT": 3, + "HOOK_ERROR_CATEGORY_INVALID_RESPONSE": 4, + "HOOK_ERROR_CATEGORY_PROCESS_CRASHED": 5, + "HOOK_ERROR_CATEGORY_UNKNOWN": 6, + } +) + +func (x HookErrorCategory) Enum() *HookErrorCategory { + p := new(HookErrorCategory) + *p = x + return p +} + +func (x HookErrorCategory) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (HookErrorCategory) Descriptor() protoreflect.EnumDescriptor { + return file_pluggableharness_agent_hook_v1_hook_proto_enumTypes[3].Descriptor() +} + +func (HookErrorCategory) Type() protoreflect.EnumType { + return &file_pluggableharness_agent_hook_v1_hook_proto_enumTypes[3] +} + +func (x HookErrorCategory) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use HookErrorCategory.Descriptor instead. +func (HookErrorCategory) EnumDescriptor() ([]byte, []int) { + return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{3} +} + +// HookPayload carries one hook point's data. Exactly one oneof variant is +// set; which variant is set *is* the point being dispatched — the +// parallel HookPoint enum above exists only for contexts (HookError) that +// have no oneof to infer the point from. +type HookPayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Payload: + // + // *HookPayload_SessionStart + // *HookPayload_PreModelCall + // *HookPayload_PostModelResponse + // *HookPayload_PreToolCall + // *HookPayload_PlanReady + // *HookPayload_PostToolCall + // *HookPayload_PostApply + // *HookPayload_SessionEnd + Payload isHookPayload_Payload `protobuf_oneof:"payload"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HookPayload) Reset() { + *x = HookPayload{} + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HookPayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HookPayload) ProtoMessage() {} + +func (x *HookPayload) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HookPayload.ProtoReflect.Descriptor instead. +func (*HookPayload) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{0} +} + +func (x *HookPayload) GetPayload() isHookPayload_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *HookPayload) GetSessionStart() *SessionStartPayload { + if x != nil { + if x, ok := x.Payload.(*HookPayload_SessionStart); ok { + return x.SessionStart + } + } + return nil +} + +func (x *HookPayload) GetPreModelCall() *PreModelCallPayload { + if x != nil { + if x, ok := x.Payload.(*HookPayload_PreModelCall); ok { + return x.PreModelCall + } + } + return nil +} + +func (x *HookPayload) GetPostModelResponse() *PostModelResponsePayload { + if x != nil { + if x, ok := x.Payload.(*HookPayload_PostModelResponse); ok { + return x.PostModelResponse + } + } + return nil +} + +func (x *HookPayload) GetPreToolCall() *PreToolCallPayload { + if x != nil { + if x, ok := x.Payload.(*HookPayload_PreToolCall); ok { + return x.PreToolCall + } + } + return nil +} + +func (x *HookPayload) GetPlanReady() *PlanReadyPayload { + if x != nil { + if x, ok := x.Payload.(*HookPayload_PlanReady); ok { + return x.PlanReady + } + } + return nil +} + +func (x *HookPayload) GetPostToolCall() *PostToolCallPayload { + if x != nil { + if x, ok := x.Payload.(*HookPayload_PostToolCall); ok { + return x.PostToolCall + } + } + return nil +} + +func (x *HookPayload) GetPostApply() *PostApplyPayload { + if x != nil { + if x, ok := x.Payload.(*HookPayload_PostApply); ok { + return x.PostApply + } + } + return nil +} + +func (x *HookPayload) GetSessionEnd() *SessionEndPayload { + if x != nil { + if x, ok := x.Payload.(*HookPayload_SessionEnd); ok { + return x.SessionEnd + } + } + return nil +} + +type isHookPayload_Payload interface { + isHookPayload_Payload() +} + +type HookPayload_SessionStart struct { + // Fires at HOOK_POINT_SESSION_START. + SessionStart *SessionStartPayload `protobuf:"bytes,1,opt,name=session_start,json=sessionStart,proto3,oneof"` +} + +type HookPayload_PreModelCall struct { + // Fires at HOOK_POINT_PRE_MODEL_CALL. + PreModelCall *PreModelCallPayload `protobuf:"bytes,2,opt,name=pre_model_call,json=preModelCall,proto3,oneof"` +} + +type HookPayload_PostModelResponse struct { + // Fires at HOOK_POINT_POST_MODEL_RESPONSE. + PostModelResponse *PostModelResponsePayload `protobuf:"bytes,3,opt,name=post_model_response,json=postModelResponse,proto3,oneof"` +} + +type HookPayload_PreToolCall struct { + // Fires at HOOK_POINT_PRE_TOOL_CALL. + PreToolCall *PreToolCallPayload `protobuf:"bytes,4,opt,name=pre_tool_call,json=preToolCall,proto3,oneof"` +} + +type HookPayload_PlanReady struct { + // Fires at HOOK_POINT_PLAN_READY. + PlanReady *PlanReadyPayload `protobuf:"bytes,5,opt,name=plan_ready,json=planReady,proto3,oneof"` +} + +type HookPayload_PostToolCall struct { + // Fires at HOOK_POINT_POST_TOOL_CALL. + PostToolCall *PostToolCallPayload `protobuf:"bytes,6,opt,name=post_tool_call,json=postToolCall,proto3,oneof"` +} + +type HookPayload_PostApply struct { + // Fires at HOOK_POINT_POST_APPLY. + PostApply *PostApplyPayload `protobuf:"bytes,7,opt,name=post_apply,json=postApply,proto3,oneof"` +} + +type HookPayload_SessionEnd struct { + // Fires at HOOK_POINT_SESSION_END. + SessionEnd *SessionEndPayload `protobuf:"bytes,8,opt,name=session_end,json=sessionEnd,proto3,oneof"` +} + +func (*HookPayload_SessionStart) isHookPayload_Payload() {} + +func (*HookPayload_PreModelCall) isHookPayload_Payload() {} + +func (*HookPayload_PostModelResponse) isHookPayload_Payload() {} + +func (*HookPayload_PreToolCall) isHookPayload_Payload() {} + +func (*HookPayload_PlanReady) isHookPayload_Payload() {} + +func (*HookPayload_PostToolCall) isHookPayload_Payload() {} + +func (*HookPayload_PostApply) isHookPayload_Payload() {} + +func (*HookPayload_SessionEnd) isHookPayload_Payload() {} + +// SessionStartPayload fires once, at session creation. No field here is +// transform-mutable per agent-loop/hook-dispatch.md's per-point mutable- +// field table — session identity and startup parameters are fixed by the +// time this hook fires. +type SessionStartPayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The new session's id. MUST be set. Immutable. + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + // The agent.hcl profile this session was created under. MUST be set. + // Immutable. + Profile string `protobuf:"bytes,2,opt,name=profile,proto3" json:"profile,omitempty"` + // The parent session's id, when this is a sub-agent session + // (agent-loop/subagents.md). Absent for a root session. Immutable. + ParentSessionId *string `protobuf:"bytes,3,opt,name=parent_session_id,json=parentSessionId,proto3,oneof" json:"parent_session_id,omitempty"` + // The session's working directory. MUST be set. Immutable. + WorkingDirectory string `protobuf:"bytes,4,opt,name=working_directory,json=workingDirectory,proto3" json:"working_directory,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SessionStartPayload) Reset() { + *x = SessionStartPayload{} + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SessionStartPayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SessionStartPayload) ProtoMessage() {} + +func (x *SessionStartPayload) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SessionStartPayload.ProtoReflect.Descriptor instead. +func (*SessionStartPayload) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{1} +} + +func (x *SessionStartPayload) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *SessionStartPayload) GetProfile() string { + if x != nil { + return x.Profile + } + return "" +} + +func (x *SessionStartPayload) GetParentSessionId() string { + if x != nil && x.ParentSessionId != nil { + return *x.ParentSessionId + } + return "" +} + +func (x *SessionStartPayload) GetWorkingDirectory() string { + if x != nil { + return x.WorkingDirectory + } + return "" +} + +// PreModelCallPayload fires immediately before a model provider's +// StreamCompletion is invoked. A transform subscriber MAY rewrite +// `messages` (e.g. redaction, injection of an additional instruction); it +// MUST NOT alter `model` — a hook subscriber does not get to silently +// reroute a turn to a different model than the one the turn algorithm +// already resolved. +type PreModelCallPayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The messages about to be sent to the model. Transform-mutable. + Messages []*v1.Message `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"` + // The model this call targets. Immutable. + Model *v11.ModelRef `protobuf:"bytes,2,opt,name=model,proto3" json:"model,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PreModelCallPayload) Reset() { + *x = PreModelCallPayload{} + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PreModelCallPayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PreModelCallPayload) ProtoMessage() {} + +func (x *PreModelCallPayload) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PreModelCallPayload.ProtoReflect.Descriptor instead. +func (*PreModelCallPayload) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{2} +} + +func (x *PreModelCallPayload) GetMessages() []*v1.Message { + if x != nil { + return x.Messages + } + return nil +} + +func (x *PreModelCallPayload) GetModel() *v11.ModelRef { + if x != nil { + return x.Model + } + return nil +} + +// PostModelResponsePayload fires immediately after a model turn's +// canonical message has been assembled, before it is persisted +// (EVENT_KIND_MESSAGE). Primarily an observe-mode point (memory providers +// recording the turn, widgets displaying live usage) — no field here is +// documented transform-mutable in agent-loop/hook-dispatch.md's per-point +// table; a transform subscriber at this point MUST return the payload +// unchanged. +type PostModelResponsePayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The assembled assistant message. MUST be set. + Message *v1.Message `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + // Which model-provider build produced `message`, for supersedes/replay + // attribution (architecture.md §Versioning & schema drift — + // "supersedes"). MUST be set. + Model *v12.ProducerRef `protobuf:"bytes,2,opt,name=model,proto3" json:"model,omitempty"` + // Token usage for the completion that produced `message`. MUST be set. + Usage *v11.Usage `protobuf:"bytes,3,opt,name=usage,proto3" json:"usage,omitempty"` + // The kernel-computed cost, in USD, of the completion that produced + // `message` (model/protocol.md#cost-computation — the provider never + // computes cost itself). MUST be set. + CostUsd float64 `protobuf:"fixed64,4,opt,name=cost_usd,json=costUsd,proto3" json:"cost_usd,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PostModelResponsePayload) Reset() { + *x = PostModelResponsePayload{} + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PostModelResponsePayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PostModelResponsePayload) ProtoMessage() {} + +func (x *PostModelResponsePayload) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PostModelResponsePayload.ProtoReflect.Descriptor instead. +func (*PostModelResponsePayload) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{3} +} + +func (x *PostModelResponsePayload) GetMessage() *v1.Message { + if x != nil { + return x.Message + } + return nil +} + +func (x *PostModelResponsePayload) GetModel() *v12.ProducerRef { + if x != nil { + return x.Model + } + return nil +} + +func (x *PostModelResponsePayload) GetUsage() *v11.Usage { + if x != nil { + return x.Usage + } + return nil +} + +func (x *PostModelResponsePayload) GetCostUsd() float64 { + if x != nil { + return x.CostUsd + } + return 0 +} + +// PreToolCallPayload fires immediately before an already-allowed plan +// item's tool call is applied. No field here is documented transform- +// mutable — a hook subscriber observes or vetoes an about-to-execute call, +// it does not get to silently rewrite its arguments; argument mutation is +// the plan/apply gate's own concern (agent-loop.md §5), not a general hook +// capability. +type PreToolCallPayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The call about to be applied. MUST be set. Immutable. + Call *v13.ToolCall `protobuf:"bytes,1,opt,name=call,proto3" json:"call,omitempty"` + // The originating plan item, carrying the policy decision that allowed + // this call through to apply. MUST be set. Immutable. + PlanItem *v14.PlanItem `protobuf:"bytes,2,opt,name=plan_item,json=planItem,proto3" json:"plan_item,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PreToolCallPayload) Reset() { + *x = PreToolCallPayload{} + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PreToolCallPayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PreToolCallPayload) ProtoMessage() {} + +func (x *PreToolCallPayload) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PreToolCallPayload.ProtoReflect.Descriptor instead. +func (*PreToolCallPayload) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{4} +} + +func (x *PreToolCallPayload) GetCall() *v13.ToolCall { + if x != nil { + return x.Call + } + return nil +} + +func (x *PreToolCallPayload) GetPlanItem() *v14.PlanItem { + if x != nil { + return x.PlanItem + } + return nil +} + +// PlanReadyPayload fires once a turn's Plan has been fully built, before +// the plan/apply gate dispatches it. This is the veto-bearing hook point: +// the kernel-privileged policy subscriber (architecture.md §Policy — +// first-party, not a plugin category) always runs here in HOOK_MODE_VETO. +// No field here is transform-mutable — a hook subscriber does not rewrite +// plan items; only the plan/apply gate itself and the policy veto affect +// what applies. +type PlanReadyPayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The fully-built plan awaiting policy evaluation. MUST be set. + // Immutable. + Plan *v14.Plan `protobuf:"bytes,1,opt,name=plan,proto3" json:"plan,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlanReadyPayload) Reset() { + *x = PlanReadyPayload{} + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlanReadyPayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlanReadyPayload) ProtoMessage() {} + +func (x *PlanReadyPayload) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlanReadyPayload.ProtoReflect.Descriptor instead. +func (*PlanReadyPayload) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{5} +} + +func (x *PlanReadyPayload) GetPlan() *v14.Plan { + if x != nil { + return x.Plan + } + return nil +} + +// PostToolCallPayload fires immediately after a plan item's tool call +// reaches a terminal outcome. Observe-only in practice — no field here is +// documented transform-mutable. +type PostToolCallPayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The call that completed. MUST be set. Immutable. + Call *v13.ToolCall `protobuf:"bytes,1,opt,name=call,proto3" json:"call,omitempty"` + // Types that are valid to be assigned to Outcome: + // + // *PostToolCallPayload_Result + // *PostToolCallPayload_Error + Outcome isPostToolCallPayload_Outcome `protobuf_oneof:"outcome"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PostToolCallPayload) Reset() { + *x = PostToolCallPayload{} + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PostToolCallPayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PostToolCallPayload) ProtoMessage() {} + +func (x *PostToolCallPayload) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PostToolCallPayload.ProtoReflect.Descriptor instead. +func (*PostToolCallPayload) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{6} +} + +func (x *PostToolCallPayload) GetCall() *v13.ToolCall { + if x != nil { + return x.Call + } + return nil +} + +func (x *PostToolCallPayload) GetOutcome() isPostToolCallPayload_Outcome { + if x != nil { + return x.Outcome + } + return nil +} + +func (x *PostToolCallPayload) GetResult() *v13.ToolResult { + if x != nil { + if x, ok := x.Outcome.(*PostToolCallPayload_Result); ok { + return x.Result + } + } + return nil +} + +func (x *PostToolCallPayload) GetError() *v13.ToolError { + if x != nil { + if x, ok := x.Outcome.(*PostToolCallPayload_Error); ok { + return x.Error + } + } + return nil +} + +type isPostToolCallPayload_Outcome interface { + isPostToolCallPayload_Outcome() +} + +type PostToolCallPayload_Result struct { + // The call's successful terminal result. + Result *v13.ToolResult `protobuf:"bytes,2,opt,name=result,proto3,oneof"` +} + +type PostToolCallPayload_Error struct { + // The call's failed terminal result. + Error *v13.ToolError `protobuf:"bytes,3,opt,name=error,proto3,oneof"` +} + +func (*PostToolCallPayload_Result) isPostToolCallPayload_Outcome() {} + +func (*PostToolCallPayload_Error) isPostToolCallPayload_Outcome() {} + +// PostApplyPayload fires once a turn's whole Plan has finished applying — +// every item has reached a terminal ApplyOutcome +// (pluggableharness.agent.plan.v1.ApplyResult.ApplyOutcome). Reuses +// plan.v1.ApplyResult rather than defining its own per-item outcome shape, +// so the post-apply hook's subject and the EVENT_KIND_APPLY event (the +// forthcoming event.v1 package) are the exact same message. Observe-only — +// applying has already happened by the time this fires, so there is +// nothing left to transform. +type PostApplyPayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The turn's per-item apply outcomes. MUST be set. + Apply *v14.ApplyResult `protobuf:"bytes,1,opt,name=apply,proto3" json:"apply,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PostApplyPayload) Reset() { + *x = PostApplyPayload{} + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PostApplyPayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PostApplyPayload) ProtoMessage() {} + +func (x *PostApplyPayload) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PostApplyPayload.ProtoReflect.Descriptor instead. +func (*PostApplyPayload) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{7} +} + +func (x *PostApplyPayload) GetApply() *v14.ApplyResult { + if x != nil { + return x.Apply + } + return nil +} + +// SessionEndPayload fires once, when a session reaches a terminal +// SessionStatus. No field here is transform-mutable — a session's outcome +// is already final by the time this fires. +type SessionEndPayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The session that ended. MUST be set. Immutable. + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + // The session's terminal status. MUST be set (never + // SESSION_STATUS_RUNNING). Immutable. + Status v15.SessionStatus `protobuf:"varint,2,opt,name=status,proto3,enum=pluggableharness.agent.session.v1.SessionStatus" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SessionEndPayload) Reset() { + *x = SessionEndPayload{} + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SessionEndPayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SessionEndPayload) ProtoMessage() {} + +func (x *SessionEndPayload) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SessionEndPayload.ProtoReflect.Descriptor instead. +func (*SessionEndPayload) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{8} +} + +func (x *SessionEndPayload) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *SessionEndPayload) GetStatus() v15.SessionStatus { + if x != nil { + return x.Status + } + return v15.SessionStatus(0) +} + +// DispatchHookRequest is one hook-point firing delivered to one +// subscriber. +type DispatchHookRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The payload for this firing. MUST be set; the set oneof variant + // implicitly identifies the hook point. + Payload *HookPayload `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"` + // The operator-configured mode this subscription runs under + // (agent.hcl's hook{} block). MUST be set — tells the subscriber which + // of DispatchHookResponse's three outcome shapes is expected back. + Mode HookMode `protobuf:"varint,2,opt,name=mode,proto3,enum=pluggableharness.agent.hook.v1.HookMode" json:"mode,omitempty"` + // Disambiguates a plugin declaring more than one hook{} block at the + // same HookPoint. Absent when a plugin has exactly one subscription at + // this point. + SubscriptionId *string `protobuf:"bytes,3,opt,name=subscription_id,json=subscriptionId,proto3,oneof" json:"subscription_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DispatchHookRequest) Reset() { + *x = DispatchHookRequest{} + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DispatchHookRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DispatchHookRequest) ProtoMessage() {} + +func (x *DispatchHookRequest) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DispatchHookRequest.ProtoReflect.Descriptor instead. +func (*DispatchHookRequest) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{9} +} + +func (x *DispatchHookRequest) GetPayload() *HookPayload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *DispatchHookRequest) GetMode() HookMode { + if x != nil { + return x.Mode + } + return HookMode_HOOK_MODE_UNSPECIFIED +} + +func (x *DispatchHookRequest) GetSubscriptionId() string { + if x != nil && x.SubscriptionId != nil { + return *x.SubscriptionId + } + return "" +} + +// DispatchHookResponse carries a subscriber's outcome, shaped by the +// HookMode the request declared. The kernel MUST reject (surfacing a +// hook_error, per agent-loop/hook-dispatch.md#subscriber-error-handling) +// a response whose oneof variant doesn't match the request's declared +// mode — HOOK_ERROR_CATEGORY_INVALID_RESPONSE. +type DispatchHookResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Outcome: + // + // *DispatchHookResponse_Observe + // *DispatchHookResponse_Transform + // *DispatchHookResponse_Veto + Outcome isDispatchHookResponse_Outcome `protobuf_oneof:"outcome"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DispatchHookResponse) Reset() { + *x = DispatchHookResponse{} + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DispatchHookResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DispatchHookResponse) ProtoMessage() {} + +func (x *DispatchHookResponse) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DispatchHookResponse.ProtoReflect.Descriptor instead. +func (*DispatchHookResponse) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{10} +} + +func (x *DispatchHookResponse) GetOutcome() isDispatchHookResponse_Outcome { + if x != nil { + return x.Outcome + } + return nil +} + +func (x *DispatchHookResponse) GetObserve() *DispatchHookResponse_ObserveAck { + if x != nil { + if x, ok := x.Outcome.(*DispatchHookResponse_Observe); ok { + return x.Observe + } + } + return nil +} + +func (x *DispatchHookResponse) GetTransform() *DispatchHookResponse_TransformResult { + if x != nil { + if x, ok := x.Outcome.(*DispatchHookResponse_Transform); ok { + return x.Transform + } + } + return nil +} + +func (x *DispatchHookResponse) GetVeto() *DispatchHookResponse_VetoResult { + if x != nil { + if x, ok := x.Outcome.(*DispatchHookResponse_Veto); ok { + return x.Veto + } + } + return nil +} + +type isDispatchHookResponse_Outcome interface { + isDispatchHookResponse_Outcome() +} + +type DispatchHookResponse_Observe struct { + // The subscriber's acknowledgment, for a HOOK_MODE_OBSERVE request. + Observe *DispatchHookResponse_ObserveAck `protobuf:"bytes,1,opt,name=observe,proto3,oneof"` +} + +type DispatchHookResponse_Transform struct { + // The subscriber's modified payload, for a HOOK_MODE_TRANSFORM + // request. + Transform *DispatchHookResponse_TransformResult `protobuf:"bytes,2,opt,name=transform,proto3,oneof"` +} + +type DispatchHookResponse_Veto struct { + // The subscriber's allow/deny verdict, for a HOOK_MODE_VETO request. + Veto *DispatchHookResponse_VetoResult `protobuf:"bytes,3,opt,name=veto,proto3,oneof"` +} + +func (*DispatchHookResponse_Observe) isDispatchHookResponse_Outcome() {} + +func (*DispatchHookResponse_Transform) isDispatchHookResponse_Outcome() {} + +func (*DispatchHookResponse_Veto) isDispatchHookResponse_Outcome() {} + +// HookError is the structured detail describing one failed hook dispatch. +// Also the payload shape the kernel-synthesized EVENT_KIND_HOOK_ERROR +// event carries (kernel.v1.EventKind, state-backend.md §The kind enum) — +// the forthcoming event.v1 package's HookErrorEvent wraps this same +// message rather than redefining it. +type HookError struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Which hook point the failing dispatch was for. MUST be set. + Point HookPoint `protobuf:"varint,1,opt,name=point,proto3,enum=pluggableharness.agent.hook.v1.HookPoint" json:"point,omitempty"` + // Which plugin build the failing subscriber was. MUST be set. + Subscriber *v12.ProducerRef `protobuf:"bytes,2,opt,name=subscriber,proto3" json:"subscriber,omitempty"` + // The HookMode the failing subscription was declared under. MUST be + // set. + Mode HookMode `protobuf:"varint,3,opt,name=mode,proto3,enum=pluggableharness.agent.hook.v1.HookMode" json:"mode,omitempty"` + // Which category of failure this is. MUST be set. + Category HookErrorCategory `protobuf:"varint,4,opt,name=category,proto3,enum=pluggableharness.agent.hook.v1.HookErrorCategory" json:"category,omitempty"` + // Human-readable detail, e.g. the raw subscriber error message. + Message string `protobuf:"bytes,5,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HookError) Reset() { + *x = HookError{} + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HookError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HookError) ProtoMessage() {} + +func (x *HookError) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HookError.ProtoReflect.Descriptor instead. +func (*HookError) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{11} +} + +func (x *HookError) GetPoint() HookPoint { + if x != nil { + return x.Point + } + return HookPoint_HOOK_POINT_UNSPECIFIED +} + +func (x *HookError) GetSubscriber() *v12.ProducerRef { + if x != nil { + return x.Subscriber + } + return nil +} + +func (x *HookError) GetMode() HookMode { + if x != nil { + return x.Mode + } + return HookMode_HOOK_MODE_UNSPECIFIED +} + +func (x *HookError) GetCategory() HookErrorCategory { + if x != nil { + return x.Category + } + return HookErrorCategory_HOOK_ERROR_CATEGORY_UNSPECIFIED +} + +func (x *HookError) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +// ObserveAck is empty. The kernel discards it (and any payload an +// observe subscriber mistakenly returns) unconditionally — observe mode +// can never alter the payload, per +// agent-loop/hook-dispatch.md#subscriber-error-handling. +type DispatchHookResponse_ObserveAck struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DispatchHookResponse_ObserveAck) Reset() { + *x = DispatchHookResponse_ObserveAck{} + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DispatchHookResponse_ObserveAck) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DispatchHookResponse_ObserveAck) ProtoMessage() {} + +func (x *DispatchHookResponse_ObserveAck) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DispatchHookResponse_ObserveAck.ProtoReflect.Descriptor instead. +func (*DispatchHookResponse_ObserveAck) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{10, 0} +} + +// TransformResult carries a transform subscriber's modified payload. +type DispatchHookResponse_TransformResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + // MUST be the same oneof variant as the request's payload. MUST only + // mutate the fields this hook point documents as transform-mutable + // (see each *Payload message's own comment) — the kernel MUST reject + // a response that changes an immutable field or the variant itself, + // per agent-loop/hook-dispatch.md#subscriber-error-handling. + Payload *HookPayload `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DispatchHookResponse_TransformResult) Reset() { + *x = DispatchHookResponse_TransformResult{} + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DispatchHookResponse_TransformResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DispatchHookResponse_TransformResult) ProtoMessage() {} + +func (x *DispatchHookResponse_TransformResult) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DispatchHookResponse_TransformResult.ProtoReflect.Descriptor instead. +func (*DispatchHookResponse_TransformResult) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{10, 1} +} + +func (x *DispatchHookResponse_TransformResult) GetPayload() *HookPayload { + if x != nil { + return x.Payload + } + return nil +} + +// VetoResult carries a veto subscriber's allow/deny verdict. +type DispatchHookResponse_VetoResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + // MUST be set. HOOK_DECISION_UNSPECIFIED is treated as an invalid + // response (HOOK_ERROR_CATEGORY_INVALID_RESPONSE), not as an implicit + // deny — the fail-closed behavior for a genuinely absent/erroring + // response is handled at the gRPC-status level, not by this enum's + // zero value. + Decision HookDecision `protobuf:"varint,1,opt,name=decision,proto3,enum=pluggableharness.agent.hook.v1.HookDecision" json:"decision,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DispatchHookResponse_VetoResult) Reset() { + *x = DispatchHookResponse_VetoResult{} + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DispatchHookResponse_VetoResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DispatchHookResponse_VetoResult) ProtoMessage() {} + +func (x *DispatchHookResponse_VetoResult) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DispatchHookResponse_VetoResult.ProtoReflect.Descriptor instead. +func (*DispatchHookResponse_VetoResult) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{10, 2} +} + +func (x *DispatchHookResponse_VetoResult) GetDecision() HookDecision { + if x != nil { + return x.Decision + } + return HookDecision_HOOK_DECISION_UNSPECIFIED +} + +var File_pluggableharness_agent_hook_v1_hook_proto protoreflect.FileDescriptor + +const file_pluggableharness_agent_hook_v1_hook_proto_rawDesc = "" + + "\n" + + ")pluggableharness/agent/hook/v1/hook.proto\x12\x1epluggableharness.agent.hook.v1\x1a-pluggableharness/agent/common/v1/common.proto\x1a/pluggableharness/agent/content/v1/content.proto\x1a+pluggableharness/agent/model/v1/model.proto\x1a)pluggableharness/agent/plan/v1/plan.proto\x1a/pluggableharness/agent/session/v1/session.proto\x1a)pluggableharness/agent/tool/v1/tool.proto\"\xf0\x05\n" + + "\vHookPayload\x12Z\n" + + "\rsession_start\x18\x01 \x01(\v23.pluggableharness.agent.hook.v1.SessionStartPayloadH\x00R\fsessionStart\x12[\n" + + "\x0epre_model_call\x18\x02 \x01(\v23.pluggableharness.agent.hook.v1.PreModelCallPayloadH\x00R\fpreModelCall\x12j\n" + + "\x13post_model_response\x18\x03 \x01(\v28.pluggableharness.agent.hook.v1.PostModelResponsePayloadH\x00R\x11postModelResponse\x12X\n" + + "\rpre_tool_call\x18\x04 \x01(\v22.pluggableharness.agent.hook.v1.PreToolCallPayloadH\x00R\vpreToolCall\x12Q\n" + + "\n" + + "plan_ready\x18\x05 \x01(\v20.pluggableharness.agent.hook.v1.PlanReadyPayloadH\x00R\tplanReady\x12[\n" + + "\x0epost_tool_call\x18\x06 \x01(\v23.pluggableharness.agent.hook.v1.PostToolCallPayloadH\x00R\fpostToolCall\x12Q\n" + + "\n" + + "post_apply\x18\a \x01(\v20.pluggableharness.agent.hook.v1.PostApplyPayloadH\x00R\tpostApply\x12T\n" + + "\vsession_end\x18\b \x01(\v21.pluggableharness.agent.hook.v1.SessionEndPayloadH\x00R\n" + + "sessionEndB\t\n" + + "\apayload\"\xc2\x01\n" + + "\x13SessionStartPayload\x12\x1d\n" + + "\n" + + "session_id\x18\x01 \x01(\tR\tsessionId\x12\x18\n" + + "\aprofile\x18\x02 \x01(\tR\aprofile\x12/\n" + + "\x11parent_session_id\x18\x03 \x01(\tH\x00R\x0fparentSessionId\x88\x01\x01\x12+\n" + + "\x11working_directory\x18\x04 \x01(\tR\x10workingDirectoryB\x14\n" + + "\x12_parent_session_id\"\x9e\x01\n" + + "\x13PreModelCallPayload\x12F\n" + + "\bmessages\x18\x01 \x03(\v2*.pluggableharness.agent.content.v1.MessageR\bmessages\x12?\n" + + "\x05model\x18\x02 \x01(\v2).pluggableharness.agent.model.v1.ModelRefR\x05model\"\xfe\x01\n" + + "\x18PostModelResponsePayload\x12D\n" + + "\amessage\x18\x01 \x01(\v2*.pluggableharness.agent.content.v1.MessageR\amessage\x12C\n" + + "\x05model\x18\x02 \x01(\v2-.pluggableharness.agent.common.v1.ProducerRefR\x05model\x12<\n" + + "\x05usage\x18\x03 \x01(\v2&.pluggableharness.agent.model.v1.UsageR\x05usage\x12\x19\n" + + "\bcost_usd\x18\x04 \x01(\x01R\acostUsd\"\x99\x01\n" + + "\x12PreToolCallPayload\x12<\n" + + "\x04call\x18\x01 \x01(\v2(.pluggableharness.agent.tool.v1.ToolCallR\x04call\x12E\n" + + "\tplan_item\x18\x02 \x01(\v2(.pluggableharness.agent.plan.v1.PlanItemR\bplanItem\"L\n" + + "\x10PlanReadyPayload\x128\n" + + "\x04plan\x18\x01 \x01(\v2$.pluggableharness.agent.plan.v1.PlanR\x04plan\"\xe7\x01\n" + + "\x13PostToolCallPayload\x12<\n" + + "\x04call\x18\x01 \x01(\v2(.pluggableharness.agent.tool.v1.ToolCallR\x04call\x12D\n" + + "\x06result\x18\x02 \x01(\v2*.pluggableharness.agent.tool.v1.ToolResultH\x00R\x06result\x12A\n" + + "\x05error\x18\x03 \x01(\v2).pluggableharness.agent.tool.v1.ToolErrorH\x00R\x05errorB\t\n" + + "\aoutcome\"U\n" + + "\x10PostApplyPayload\x12A\n" + + "\x05apply\x18\x01 \x01(\v2+.pluggableharness.agent.plan.v1.ApplyResultR\x05apply\"|\n" + + "\x11SessionEndPayload\x12\x1d\n" + + "\n" + + "session_id\x18\x01 \x01(\tR\tsessionId\x12H\n" + + "\x06status\x18\x02 \x01(\x0e20.pluggableharness.agent.session.v1.SessionStatusR\x06status\"\xdc\x01\n" + + "\x13DispatchHookRequest\x12E\n" + + "\apayload\x18\x01 \x01(\v2+.pluggableharness.agent.hook.v1.HookPayloadR\apayload\x12<\n" + + "\x04mode\x18\x02 \x01(\x0e2(.pluggableharness.agent.hook.v1.HookModeR\x04mode\x12,\n" + + "\x0fsubscription_id\x18\x03 \x01(\tH\x00R\x0esubscriptionId\x88\x01\x01B\x12\n" + + "\x10_subscription_id\"\xfb\x03\n" + + "\x14DispatchHookResponse\x12[\n" + + "\aobserve\x18\x01 \x01(\v2?.pluggableharness.agent.hook.v1.DispatchHookResponse.ObserveAckH\x00R\aobserve\x12d\n" + + "\ttransform\x18\x02 \x01(\v2D.pluggableharness.agent.hook.v1.DispatchHookResponse.TransformResultH\x00R\ttransform\x12U\n" + + "\x04veto\x18\x03 \x01(\v2?.pluggableharness.agent.hook.v1.DispatchHookResponse.VetoResultH\x00R\x04veto\x1a\f\n" + + "\n" + + "ObserveAck\x1aX\n" + + "\x0fTransformResult\x12E\n" + + "\apayload\x18\x01 \x01(\v2+.pluggableharness.agent.hook.v1.HookPayloadR\apayload\x1aV\n" + + "\n" + + "VetoResult\x12H\n" + + "\bdecision\x18\x01 \x01(\x0e2,.pluggableharness.agent.hook.v1.HookDecisionR\bdecisionB\t\n" + + "\aoutcome\"\xc2\x02\n" + + "\tHookError\x12?\n" + + "\x05point\x18\x01 \x01(\x0e2).pluggableharness.agent.hook.v1.HookPointR\x05point\x12M\n" + + "\n" + + "subscriber\x18\x02 \x01(\v2-.pluggableharness.agent.common.v1.ProducerRefR\n" + + "subscriber\x12<\n" + + "\x04mode\x18\x03 \x01(\x0e2(.pluggableharness.agent.hook.v1.HookModeR\x04mode\x12M\n" + + "\bcategory\x18\x04 \x01(\x0e21.pluggableharness.agent.hook.v1.HookErrorCategoryR\bcategory\x12\x18\n" + + "\amessage\x18\x05 \x01(\tR\amessage*\x97\x02\n" + + "\tHookPoint\x12\x1a\n" + + "\x16HOOK_POINT_UNSPECIFIED\x10\x00\x12\x1c\n" + + "\x18HOOK_POINT_SESSION_START\x10\x01\x12\x1d\n" + + "\x19HOOK_POINT_PRE_MODEL_CALL\x10\x02\x12\"\n" + + "\x1eHOOK_POINT_POST_MODEL_RESPONSE\x10\x03\x12\x1c\n" + + "\x18HOOK_POINT_PRE_TOOL_CALL\x10\x04\x12\x19\n" + + "\x15HOOK_POINT_PLAN_READY\x10\x05\x12\x1d\n" + + "\x19HOOK_POINT_POST_TOOL_CALL\x10\x06\x12\x19\n" + + "\x15HOOK_POINT_POST_APPLY\x10\a\x12\x1a\n" + + "\x16HOOK_POINT_SESSION_END\x10\b*i\n" + + "\bHookMode\x12\x19\n" + + "\x15HOOK_MODE_UNSPECIFIED\x10\x00\x12\x15\n" + + "\x11HOOK_MODE_OBSERVE\x10\x01\x12\x17\n" + + "\x13HOOK_MODE_TRANSFORM\x10\x02\x12\x12\n" + + "\x0eHOOK_MODE_VETO\x10\x03*^\n" + + "\fHookDecision\x12\x1d\n" + + "\x19HOOK_DECISION_UNSPECIFIED\x10\x00\x12\x17\n" + + "\x13HOOK_DECISION_ALLOW\x10\x01\x12\x16\n" + + "\x12HOOK_DECISION_DENY\x10\x02*\x9c\x02\n" + + "\x11HookErrorCategory\x12#\n" + + "\x1fHOOK_ERROR_CATEGORY_UNSPECIFIED\x10\x00\x12(\n" + + "$HOOK_ERROR_CATEGORY_TRANSFORM_FAILED\x10\x01\x12#\n" + + "\x1fHOOK_ERROR_CATEGORY_VETO_FAILED\x10\x02\x12\x1f\n" + + "\x1bHOOK_ERROR_CATEGORY_TIMEOUT\x10\x03\x12(\n" + + "$HOOK_ERROR_CATEGORY_INVALID_RESPONSE\x10\x04\x12'\n" + + "#HOOK_ERROR_CATEGORY_PROCESS_CRASHED\x10\x05\x12\x1f\n" + + "\x1bHOOK_ERROR_CATEGORY_UNKNOWN\x10\x062\x92\x01\n" + + "\x15HookSubscriberService\x12y\n" + + "\fDispatchHook\x123.pluggableharness.agent.hook.v1.DispatchHookRequest\x1a4.pluggableharness.agent.hook.v1.DispatchHookResponseB pluggableharness.agent.hook.v1.SessionStartPayload + 6, // 1: pluggableharness.agent.hook.v1.HookPayload.pre_model_call:type_name -> pluggableharness.agent.hook.v1.PreModelCallPayload + 7, // 2: pluggableharness.agent.hook.v1.HookPayload.post_model_response:type_name -> pluggableharness.agent.hook.v1.PostModelResponsePayload + 8, // 3: pluggableharness.agent.hook.v1.HookPayload.pre_tool_call:type_name -> pluggableharness.agent.hook.v1.PreToolCallPayload + 9, // 4: pluggableharness.agent.hook.v1.HookPayload.plan_ready:type_name -> pluggableharness.agent.hook.v1.PlanReadyPayload + 10, // 5: pluggableharness.agent.hook.v1.HookPayload.post_tool_call:type_name -> pluggableharness.agent.hook.v1.PostToolCallPayload + 11, // 6: pluggableharness.agent.hook.v1.HookPayload.post_apply:type_name -> pluggableharness.agent.hook.v1.PostApplyPayload + 12, // 7: pluggableharness.agent.hook.v1.HookPayload.session_end:type_name -> pluggableharness.agent.hook.v1.SessionEndPayload + 19, // 8: pluggableharness.agent.hook.v1.PreModelCallPayload.messages:type_name -> pluggableharness.agent.content.v1.Message + 20, // 9: pluggableharness.agent.hook.v1.PreModelCallPayload.model:type_name -> pluggableharness.agent.model.v1.ModelRef + 19, // 10: pluggableharness.agent.hook.v1.PostModelResponsePayload.message:type_name -> pluggableharness.agent.content.v1.Message + 21, // 11: pluggableharness.agent.hook.v1.PostModelResponsePayload.model:type_name -> pluggableharness.agent.common.v1.ProducerRef + 22, // 12: pluggableharness.agent.hook.v1.PostModelResponsePayload.usage:type_name -> pluggableharness.agent.model.v1.Usage + 23, // 13: pluggableharness.agent.hook.v1.PreToolCallPayload.call:type_name -> pluggableharness.agent.tool.v1.ToolCall + 24, // 14: pluggableharness.agent.hook.v1.PreToolCallPayload.plan_item:type_name -> pluggableharness.agent.plan.v1.PlanItem + 25, // 15: pluggableharness.agent.hook.v1.PlanReadyPayload.plan:type_name -> pluggableharness.agent.plan.v1.Plan + 23, // 16: pluggableharness.agent.hook.v1.PostToolCallPayload.call:type_name -> pluggableharness.agent.tool.v1.ToolCall + 26, // 17: pluggableharness.agent.hook.v1.PostToolCallPayload.result:type_name -> pluggableharness.agent.tool.v1.ToolResult + 27, // 18: pluggableharness.agent.hook.v1.PostToolCallPayload.error:type_name -> pluggableharness.agent.tool.v1.ToolError + 28, // 19: pluggableharness.agent.hook.v1.PostApplyPayload.apply:type_name -> pluggableharness.agent.plan.v1.ApplyResult + 29, // 20: pluggableharness.agent.hook.v1.SessionEndPayload.status:type_name -> pluggableharness.agent.session.v1.SessionStatus + 4, // 21: pluggableharness.agent.hook.v1.DispatchHookRequest.payload:type_name -> pluggableharness.agent.hook.v1.HookPayload + 1, // 22: pluggableharness.agent.hook.v1.DispatchHookRequest.mode:type_name -> pluggableharness.agent.hook.v1.HookMode + 16, // 23: pluggableharness.agent.hook.v1.DispatchHookResponse.observe:type_name -> pluggableharness.agent.hook.v1.DispatchHookResponse.ObserveAck + 17, // 24: pluggableharness.agent.hook.v1.DispatchHookResponse.transform:type_name -> pluggableharness.agent.hook.v1.DispatchHookResponse.TransformResult + 18, // 25: pluggableharness.agent.hook.v1.DispatchHookResponse.veto:type_name -> pluggableharness.agent.hook.v1.DispatchHookResponse.VetoResult + 0, // 26: pluggableharness.agent.hook.v1.HookError.point:type_name -> pluggableharness.agent.hook.v1.HookPoint + 21, // 27: pluggableharness.agent.hook.v1.HookError.subscriber:type_name -> pluggableharness.agent.common.v1.ProducerRef + 1, // 28: pluggableharness.agent.hook.v1.HookError.mode:type_name -> pluggableharness.agent.hook.v1.HookMode + 3, // 29: pluggableharness.agent.hook.v1.HookError.category:type_name -> pluggableharness.agent.hook.v1.HookErrorCategory + 4, // 30: pluggableharness.agent.hook.v1.DispatchHookResponse.TransformResult.payload:type_name -> pluggableharness.agent.hook.v1.HookPayload + 2, // 31: pluggableharness.agent.hook.v1.DispatchHookResponse.VetoResult.decision:type_name -> pluggableharness.agent.hook.v1.HookDecision + 13, // 32: pluggableharness.agent.hook.v1.HookSubscriberService.DispatchHook:input_type -> pluggableharness.agent.hook.v1.DispatchHookRequest + 14, // 33: pluggableharness.agent.hook.v1.HookSubscriberService.DispatchHook:output_type -> pluggableharness.agent.hook.v1.DispatchHookResponse + 33, // [33:34] is the sub-list for method output_type + 32, // [32:33] is the sub-list for method input_type + 32, // [32:32] is the sub-list for extension type_name + 32, // [32:32] is the sub-list for extension extendee + 0, // [0:32] is the sub-list for field type_name +} + +func init() { file_pluggableharness_agent_hook_v1_hook_proto_init() } +func file_pluggableharness_agent_hook_v1_hook_proto_init() { + if File_pluggableharness_agent_hook_v1_hook_proto != nil { + return + } + file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[0].OneofWrappers = []any{ + (*HookPayload_SessionStart)(nil), + (*HookPayload_PreModelCall)(nil), + (*HookPayload_PostModelResponse)(nil), + (*HookPayload_PreToolCall)(nil), + (*HookPayload_PlanReady)(nil), + (*HookPayload_PostToolCall)(nil), + (*HookPayload_PostApply)(nil), + (*HookPayload_SessionEnd)(nil), + } + file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[1].OneofWrappers = []any{} + file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[6].OneofWrappers = []any{ + (*PostToolCallPayload_Result)(nil), + (*PostToolCallPayload_Error)(nil), + } + file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[9].OneofWrappers = []any{} + file_pluggableharness_agent_hook_v1_hook_proto_msgTypes[10].OneofWrappers = []any{ + (*DispatchHookResponse_Observe)(nil), + (*DispatchHookResponse_Transform)(nil), + (*DispatchHookResponse_Veto)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_pluggableharness_agent_hook_v1_hook_proto_rawDesc), len(file_pluggableharness_agent_hook_v1_hook_proto_rawDesc)), + NumEnums: 4, + NumMessages: 15, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_pluggableharness_agent_hook_v1_hook_proto_goTypes, + DependencyIndexes: file_pluggableharness_agent_hook_v1_hook_proto_depIdxs, + EnumInfos: file_pluggableharness_agent_hook_v1_hook_proto_enumTypes, + MessageInfos: file_pluggableharness_agent_hook_v1_hook_proto_msgTypes, + }.Build() + File_pluggableharness_agent_hook_v1_hook_proto = out.File + file_pluggableharness_agent_hook_v1_hook_proto_goTypes = nil + file_pluggableharness_agent_hook_v1_hook_proto_depIdxs = nil +} diff --git a/pkg/hook/proto/v1/hook_grpc.pb.go b/pkg/hook/proto/v1/hook_grpc.pb.go new file mode 100644 index 0000000..1f2a1e2 --- /dev/null +++ b/pkg/hook/proto/v1/hook_grpc.pb.go @@ -0,0 +1,175 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.2 +// - protoc (unknown) +// source: pluggableharness/agent/hook/v1/hook.proto + +// Package pluggableharness.agent.hook.v1 defines the hook-dispatch RPC surface +// described in agent-loop/hook-dispatch.md and architecture.md §Hook +// dispatch semantics: the wire contract the kernel uses to invoke any +// plugin (of any of the six categories) that declares a `hook{}` block in +// agent.hcl. This is deliberately one shared service rather than a +// per-category RPC — hashicorp/go-plugin (.claude/rules/plugin-runtime.md) +// muxes multiple gRPC services over one broker connection, so the kernel +// dials HookSubscriberService on the same subprocess that already serves +// that plugin's own category service. A plugin with no `hook{}` blocks in +// agent.hcl simply never has it called. +// +// context-assemble is deliberately absent from this surface's HookPoint +// enum below — it stays on ContextService.Contribute +// (context/protocol.md#contribute-the-context-assemble-rpc; architecture.md +// §Hook dispatch semantics), which already carries the full accumulated +// ContextSection chain and doesn't need a second, competing dispatch path. +// This surface serves the other eight hook points only. + +package hookv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + HookSubscriberService_DispatchHook_FullMethodName = "/pluggableharness.agent.hook.v1.HookSubscriberService/DispatchHook" +) + +// HookSubscriberServiceClient is the client API for HookSubscriberService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// HookSubscriberService is the hook-dispatch protocol described in +// agent-loop/hook-dispatch.md: the kernel invokes a subscribing plugin +// once per hook-point firing it's declared for, in agent.hcl declaration +// order alongside every other subscriber at that point +// (agent-loop/hook-dispatch.md#dispatch-order-and-payload-flow). Every +// plugin category MAY implement this service; whether the kernel ever +// dials it for a given plugin process is entirely a function of that +// plugin's own agent.hcl `hook{}` declarations, not its category. +type HookSubscriberServiceClient interface { + // DispatchHook delivers one hook-point firing to one subscriber. Unary, + // not streaming: one invocation is one request/one response + // (agent-loop/hook-dispatch.md#dispatch-order-and-payload-flow). Per- + // subscriber timeout is a ctx deadline the kernel sets + // (.claude/rules/grpc.md "Context and deadlines"; + // agent-loop/hook-dispatch.md#timeout-behavior), not a wire field on + // this request. Cancellation (the kernel closing the call because the + // turn is being aborted) is normal control flow, per + // .claude/rules/grpc.md — never logged as a failure. + DispatchHook(ctx context.Context, in *DispatchHookRequest, opts ...grpc.CallOption) (*DispatchHookResponse, error) +} + +type hookSubscriberServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewHookSubscriberServiceClient(cc grpc.ClientConnInterface) HookSubscriberServiceClient { + return &hookSubscriberServiceClient{cc} +} + +func (c *hookSubscriberServiceClient) DispatchHook(ctx context.Context, in *DispatchHookRequest, opts ...grpc.CallOption) (*DispatchHookResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DispatchHookResponse) + err := c.cc.Invoke(ctx, HookSubscriberService_DispatchHook_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// HookSubscriberServiceServer is the server API for HookSubscriberService service. +// All implementations must embed UnimplementedHookSubscriberServiceServer +// for forward compatibility. +// +// HookSubscriberService is the hook-dispatch protocol described in +// agent-loop/hook-dispatch.md: the kernel invokes a subscribing plugin +// once per hook-point firing it's declared for, in agent.hcl declaration +// order alongside every other subscriber at that point +// (agent-loop/hook-dispatch.md#dispatch-order-and-payload-flow). Every +// plugin category MAY implement this service; whether the kernel ever +// dials it for a given plugin process is entirely a function of that +// plugin's own agent.hcl `hook{}` declarations, not its category. +type HookSubscriberServiceServer interface { + // DispatchHook delivers one hook-point firing to one subscriber. Unary, + // not streaming: one invocation is one request/one response + // (agent-loop/hook-dispatch.md#dispatch-order-and-payload-flow). Per- + // subscriber timeout is a ctx deadline the kernel sets + // (.claude/rules/grpc.md "Context and deadlines"; + // agent-loop/hook-dispatch.md#timeout-behavior), not a wire field on + // this request. Cancellation (the kernel closing the call because the + // turn is being aborted) is normal control flow, per + // .claude/rules/grpc.md — never logged as a failure. + DispatchHook(context.Context, *DispatchHookRequest) (*DispatchHookResponse, error) + mustEmbedUnimplementedHookSubscriberServiceServer() +} + +// UnimplementedHookSubscriberServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedHookSubscriberServiceServer struct{} + +func (UnimplementedHookSubscriberServiceServer) DispatchHook(context.Context, *DispatchHookRequest) (*DispatchHookResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DispatchHook not implemented") +} +func (UnimplementedHookSubscriberServiceServer) mustEmbedUnimplementedHookSubscriberServiceServer() {} +func (UnimplementedHookSubscriberServiceServer) testEmbeddedByValue() {} + +// UnsafeHookSubscriberServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to HookSubscriberServiceServer will +// result in compilation errors. +type UnsafeHookSubscriberServiceServer interface { + mustEmbedUnimplementedHookSubscriberServiceServer() +} + +func RegisterHookSubscriberServiceServer(s grpc.ServiceRegistrar, srv HookSubscriberServiceServer) { + // If the following call panics, it indicates UnimplementedHookSubscriberServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&HookSubscriberService_ServiceDesc, srv) +} + +func _HookSubscriberService_DispatchHook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DispatchHookRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HookSubscriberServiceServer).DispatchHook(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: HookSubscriberService_DispatchHook_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HookSubscriberServiceServer).DispatchHook(ctx, req.(*DispatchHookRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// HookSubscriberService_ServiceDesc is the grpc.ServiceDesc for HookSubscriberService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var HookSubscriberService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "pluggableharness.agent.hook.v1.HookSubscriberService", + HandlerType: (*HookSubscriberServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "DispatchHook", + Handler: _HookSubscriberService_DispatchHook_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "pluggableharness/agent/hook/v1/hook.proto", +} diff --git a/pkg/kernel/proto/v1/kernel.pb.go b/pkg/kernel/proto/v1/kernel.pb.go index 7003a74..e8585c8 100644 --- a/pkg/kernel/proto/v1/kernel.pb.go +++ b/pkg/kernel/proto/v1/kernel.pb.go @@ -69,21 +69,29 @@ const ( EventKind_EVENT_KIND_MEMORY_UPDATE EventKind = 8 // A memory provider's deletion of an existing record. EventKind_EVENT_KIND_MEMORY_DELETE EventKind = 9 + // Kernel-synthesized when a transform or veto hook subscriber fails + // (agent-loop/hook-dispatch.md#subscriber-error-handling) — never + // emitted by a plugin's own Emit call, only by the kernel itself + // dispatching a hook. Payload shape is + // pluggableharness.agent.hook.v1.HookError, wrapped by the forthcoming + // event.v1 package's HookErrorEvent (state-backend.md §5). + EventKind_EVENT_KIND_HOOK_ERROR EventKind = 10 ) // Enum value maps for EventKind. var ( EventKind_name = map[int32]string{ - 0: "EVENT_KIND_UNSPECIFIED", - 1: "EVENT_KIND_MESSAGE", - 2: "EVENT_KIND_TOOL_CALL", - 3: "EVENT_KIND_TOOL_RESULT", - 4: "EVENT_KIND_PLAN", - 5: "EVENT_KIND_APPLY", - 6: "EVENT_KIND_CONTEXT_CONTRIBUTION", - 7: "EVENT_KIND_MEMORY_WRITE", - 8: "EVENT_KIND_MEMORY_UPDATE", - 9: "EVENT_KIND_MEMORY_DELETE", + 0: "EVENT_KIND_UNSPECIFIED", + 1: "EVENT_KIND_MESSAGE", + 2: "EVENT_KIND_TOOL_CALL", + 3: "EVENT_KIND_TOOL_RESULT", + 4: "EVENT_KIND_PLAN", + 5: "EVENT_KIND_APPLY", + 6: "EVENT_KIND_CONTEXT_CONTRIBUTION", + 7: "EVENT_KIND_MEMORY_WRITE", + 8: "EVENT_KIND_MEMORY_UPDATE", + 9: "EVENT_KIND_MEMORY_DELETE", + 10: "EVENT_KIND_HOOK_ERROR", } EventKind_value = map[string]int32{ "EVENT_KIND_UNSPECIFIED": 0, @@ -96,6 +104,7 @@ var ( "EVENT_KIND_MEMORY_WRITE": 7, "EVENT_KIND_MEMORY_UPDATE": 8, "EVENT_KIND_MEMORY_DELETE": 9, + "EVENT_KIND_HOOK_ERROR": 10, } ) @@ -245,9 +254,28 @@ type RunSessionResult struct { // The child session's terminal lifecycle status. Never // SESSION_STATUS_RUNNING — a RunSession call only returns once the // child session has reached a terminal state. - Status v12.SessionStatus `protobuf:"varint,3,opt,name=status,proto3,enum=pluggableharness.agent.session.v1.SessionStatus" json:"status,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Status v12.SessionStatus `protobuf:"varint,3,opt,name=status,proto3,enum=pluggableharness.agent.session.v1.SessionStatus" json:"status,omitempty"` + // The child session's aggregate cost, in USD, summed across every turn + // it ran, including any of its own descendant sub-agent sessions. MUST + // be set. Deliberately a flat field, not a + // pluggableharness.agent.model.v1.Usage-shaped reference — this is a + // whole-session rollup (state-backend.md's cost_ledger SUM, per + // .claude/rules/determinism.md's "Cost and budget rollup"), a different + // shape from one completion call's per-call Usage. Lets an orchestrator + // plugin do budget-aware fan-out (spend-check a child's outcome before + // deciding whether to spawn another) without needing to separately sum + // the child's own event history itself. + TotalCostUsd float64 `protobuf:"fixed64,4,opt,name=total_cost_usd,json=totalCostUsd,proto3" json:"total_cost_usd,omitempty"` + // The child session's aggregate input token count, summed across every + // turn it ran, including descendants. MUST be set. Same flat, + // aggregate-not-per-call rationale as total_cost_usd. + TotalInputTokens int64 `protobuf:"varint,5,opt,name=total_input_tokens,json=totalInputTokens,proto3" json:"total_input_tokens,omitempty"` + // The child session's aggregate output token count, summed across + // every turn it ran, including descendants. MUST be set. Same flat, + // aggregate-not-per-call rationale as total_cost_usd. + TotalOutputTokens int64 `protobuf:"varint,6,opt,name=total_output_tokens,json=totalOutputTokens,proto3" json:"total_output_tokens,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RunSessionResult) Reset() { @@ -301,6 +329,27 @@ func (x *RunSessionResult) GetStatus() v12.SessionStatus { return v12.SessionStatus(0) } +func (x *RunSessionResult) GetTotalCostUsd() float64 { + if x != nil { + return x.TotalCostUsd + } + return 0 +} + +func (x *RunSessionResult) GetTotalInputTokens() int64 { + if x != nil { + return x.TotalInputTokens + } + return 0 +} + +func (x *RunSessionResult) GetTotalOutputTokens() int64 { + if x != nil { + return x.TotalOutputTokens + } + return 0 +} + // CountTokensRequest asks the kernel to count tokens for a block of // content, optionally against a specific model's tokenizer. See // kernel-callbacks.md §2. @@ -667,12 +716,15 @@ const file_pluggableharness_agent_kernel_v1_kernel_proto_rawDesc = "" + "\x11parent_session_id\x18\x03 \x01(\tR\x0fparentSessionId\x12'\n" + "\x0fremaining_depth\x18\x04 \x01(\x05R\x0eremainingDepth\x129\n" + "\x19remaining_cost_budget_usd\x18\x05 \x01(\x01R\x16remainingCostBudgetUsd\x12X\n" + - "\x10scoped_providers\x18\x06 \x03(\v2-.pluggableharness.agent.common.v1.ProviderRefR\x0fscopedProviders\"\xcc\x01\n" + + "\x10scoped_providers\x18\x06 \x03(\v2-.pluggableharness.agent.common.v1.ProviderRefR\x0fscopedProviders\"\xd0\x02\n" + "\x10RunSessionResult\x12\x1d\n" + "\n" + "session_id\x18\x01 \x01(\tR\tsessionId\x12O\n" + "\rfinal_message\x18\x02 \x01(\v2*.pluggableharness.agent.content.v1.MessageR\ffinalMessage\x12H\n" + - "\x06status\x18\x03 \x01(\x0e20.pluggableharness.agent.session.v1.SessionStatusR\x06status\"\xba\x01\n" + + "\x06status\x18\x03 \x01(\x0e20.pluggableharness.agent.session.v1.SessionStatusR\x06status\x12$\n" + + "\x0etotal_cost_usd\x18\x04 \x01(\x01R\ftotalCostUsd\x12,\n" + + "\x12total_input_tokens\x18\x05 \x01(\x03R\x10totalInputTokens\x12.\n" + + "\x13total_output_tokens\x18\x06 \x01(\x03R\x11totalOutputTokens\"\xba\x01\n" + "\x12CountTokensRequest\x12I\n" + "\acontent\x18\x01 \x03(\v2/.pluggableharness.agent.content.v1.ContentBlockR\acontent\x12K\n" + "\tmodel_ref\x18\x02 \x01(\v2).pluggableharness.agent.model.v1.ModelRefH\x00R\bmodelRef\x88\x01\x01B\f\n" + @@ -697,7 +749,7 @@ const file_pluggableharness_agent_kernel_v1_kernel_proto_rawDesc = "" + "session_id\x18\x01 \x01(\tH\x00R\tsessionId\x88\x01\x01\x12=\n" + "\x05entry\x18\x02 \x01(\v2'.pluggableharness.agent.log.v1.LogEntryR\x05entryB\r\n" + "\v_session_id\"\v\n" + - "\tLogResult*\x9e\x02\n" + + "\tLogResult*\xb9\x02\n" + "\tEventKind\x12\x1a\n" + "\x16EVENT_KIND_UNSPECIFIED\x10\x00\x12\x16\n" + "\x12EVENT_KIND_MESSAGE\x10\x01\x12\x18\n" + @@ -708,7 +760,9 @@ const file_pluggableharness_agent_kernel_v1_kernel_proto_rawDesc = "" + "\x1fEVENT_KIND_CONTEXT_CONTRIBUTION\x10\x06\x12\x1b\n" + "\x17EVENT_KIND_MEMORY_WRITE\x10\a\x12\x1c\n" + "\x18EVENT_KIND_MEMORY_UPDATE\x10\b\x12\x1c\n" + - "\x18EVENT_KIND_MEMORY_DELETE\x10\t2\xcf\x03\n" + + "\x18EVENT_KIND_MEMORY_DELETE\x10\t\x12\x19\n" + + "\x15EVENT_KIND_HOOK_ERROR\x10\n" + + "2\xcf\x03\n" + "\x15KernelCallbackService\x12u\n" + "\n" + "RunSession\x123.pluggableharness.agent.kernel.v1.RunSessionRequest\x1a2.pluggableharness.agent.kernel.v1.RunSessionResult\x12x\n" + diff --git a/pkg/plan/proto/v1/plan.pb.go b/pkg/plan/proto/v1/plan.pb.go index a4ffc81..e3269db 100644 --- a/pkg/plan/proto/v1/plan.pb.go +++ b/pkg/plan/proto/v1/plan.pb.go @@ -15,6 +15,7 @@ package planv1 import ( + v1 "github.com/pluggableharness/agent/pkg/tool/proto/v1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" structpb "google.golang.org/protobuf/types/known/structpb" @@ -103,6 +104,77 @@ func (PlanDecision) EnumDescriptor() ([]byte, []int) { return file_pluggableharness_agent_plan_v1_plan_proto_rawDescGZIP(), []int{0} } +// ApplyOutcome classifies how one plan item's apply attempt concluded. +type ApplyResult_ApplyOutcome int32 + +const ( + // Zero value. Never valid for a real outcome; its presence on the + // wire means a caller forgot to set the field. + ApplyResult_APPLY_OUTCOME_UNSPECIFIED ApplyResult_ApplyOutcome = 0 + // The item's call executed and succeeded. + ApplyResult_APPLY_OUTCOME_APPLIED ApplyResult_ApplyOutcome = 1 + // The item's call executed and failed on its own terms (a + // ToolError). + ApplyResult_APPLY_OUTCOME_FAILED ApplyResult_ApplyOutcome = 2 + // The item was not executed: the plan/apply gate synthesized a + // denial instead, per agent-loop.md §5.2's tool_result denial block + // (kernel policy PLAN_DECISION_DENY, or a HOOK_DECISION_DENY veto + // at plan-ready). The model observes this denial in its own + // history via the synthesized ToolResultBlock, not via this + // ApplyItem. + ApplyResult_APPLY_OUTCOME_DENIED ApplyResult_ApplyOutcome = 3 + // The item was never reached because an earlier item in the same + // apply pass aborted the whole apply. Not reached in the current + // apply algorithm — reserved for a future partial-apply-then-abort + // mode. + ApplyResult_APPLY_OUTCOME_SKIPPED ApplyResult_ApplyOutcome = 4 +) + +// Enum value maps for ApplyResult_ApplyOutcome. +var ( + ApplyResult_ApplyOutcome_name = map[int32]string{ + 0: "APPLY_OUTCOME_UNSPECIFIED", + 1: "APPLY_OUTCOME_APPLIED", + 2: "APPLY_OUTCOME_FAILED", + 3: "APPLY_OUTCOME_DENIED", + 4: "APPLY_OUTCOME_SKIPPED", + } + ApplyResult_ApplyOutcome_value = map[string]int32{ + "APPLY_OUTCOME_UNSPECIFIED": 0, + "APPLY_OUTCOME_APPLIED": 1, + "APPLY_OUTCOME_FAILED": 2, + "APPLY_OUTCOME_DENIED": 3, + "APPLY_OUTCOME_SKIPPED": 4, + } +) + +func (x ApplyResult_ApplyOutcome) Enum() *ApplyResult_ApplyOutcome { + p := new(ApplyResult_ApplyOutcome) + *p = x + return p +} + +func (x ApplyResult_ApplyOutcome) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ApplyResult_ApplyOutcome) Descriptor() protoreflect.EnumDescriptor { + return file_pluggableharness_agent_plan_v1_plan_proto_enumTypes[1].Descriptor() +} + +func (ApplyResult_ApplyOutcome) Type() protoreflect.EnumType { + return &file_pluggableharness_agent_plan_v1_plan_proto_enumTypes[1] +} + +func (x ApplyResult_ApplyOutcome) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ApplyResult_ApplyOutcome.Descriptor instead. +func (ApplyResult_ApplyOutcome) EnumDescriptor() ([]byte, []int) { + return file_pluggableharness_agent_plan_v1_plan_proto_rawDescGZIP(), []int{2, 0} +} + // PlanItem is one resource (or policy-checked data_source/interactive) // call awaiting or having received a plan/apply decision. type PlanItem struct { @@ -264,11 +336,193 @@ func (x *Plan) GetItems() []*PlanItem { return nil } +// ApplyResult carries a turn's complete set of per-item apply outcomes, +// once every item in its Plan has reached a terminal ApplyOutcome, per +// agent-loop.md §5.2. Homed in plan.v1 (rather than the forthcoming +// event.v1 package that would otherwise seem the more obvious owner) so +// both pluggableharness.agent.hook.v1's PostApplyPayload and event.v1's +// future EVENT_KIND_APPLY payload can reference this exact message +// without either importing the other — plan.v1 has no dependency on +// either, keeping the graph acyclic. +type ApplyResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The turn this apply outcome belongs to. Matches the originating + // Plan.turn_id. + TurnId string `protobuf:"bytes,1,opt,name=turn_id,json=turnId,proto3" json:"turn_id,omitempty"` + // One outcome per applied plan item, in apply order. + Items []*ApplyResult_ApplyItem `protobuf:"bytes,2,rep,name=items,proto3" json:"items,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApplyResult) Reset() { + *x = ApplyResult{} + mi := &file_pluggableharness_agent_plan_v1_plan_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApplyResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApplyResult) ProtoMessage() {} + +func (x *ApplyResult) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_plan_v1_plan_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApplyResult.ProtoReflect.Descriptor instead. +func (*ApplyResult) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_plan_v1_plan_proto_rawDescGZIP(), []int{2} +} + +func (x *ApplyResult) GetTurnId() string { + if x != nil { + return x.TurnId + } + return "" +} + +func (x *ApplyResult) GetItems() []*ApplyResult_ApplyItem { + if x != nil { + return x.Items + } + return nil +} + +// ApplyItem is one plan item's apply outcome. +type ApplyResult_ApplyItem struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The originating PlanItem.id this outcome is for. MUST be set. + PlanItemId string `protobuf:"bytes,1,opt,name=plan_item_id,json=planItemId,proto3" json:"plan_item_id,omitempty"` + // The originating PlanItem.tool_call_id this outcome is for. MUST + // be set. + ToolCallId string `protobuf:"bytes,2,opt,name=tool_call_id,json=toolCallId,proto3" json:"tool_call_id,omitempty"` + // How this item's apply attempt concluded. MUST be set (never + // APPLY_OUTCOME_UNSPECIFIED). + Outcome ApplyResult_ApplyOutcome `protobuf:"varint,3,opt,name=outcome,proto3,enum=pluggableharness.agent.plan.v1.ApplyResult_ApplyOutcome" json:"outcome,omitempty"` + // Absent for APPLY_OUTCOME_DENIED and APPLY_OUTCOME_SKIPPED — + // neither outcome executes the underlying tool call, so neither has + // a ToolResult/ToolError to carry. + // + // Types that are valid to be assigned to Result: + // + // *ApplyResult_ApplyItem_ToolResult + // *ApplyResult_ApplyItem_ToolError + Result isApplyResult_ApplyItem_Result `protobuf_oneof:"result"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApplyResult_ApplyItem) Reset() { + *x = ApplyResult_ApplyItem{} + mi := &file_pluggableharness_agent_plan_v1_plan_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApplyResult_ApplyItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApplyResult_ApplyItem) ProtoMessage() {} + +func (x *ApplyResult_ApplyItem) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_plan_v1_plan_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApplyResult_ApplyItem.ProtoReflect.Descriptor instead. +func (*ApplyResult_ApplyItem) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_plan_v1_plan_proto_rawDescGZIP(), []int{2, 0} +} + +func (x *ApplyResult_ApplyItem) GetPlanItemId() string { + if x != nil { + return x.PlanItemId + } + return "" +} + +func (x *ApplyResult_ApplyItem) GetToolCallId() string { + if x != nil { + return x.ToolCallId + } + return "" +} + +func (x *ApplyResult_ApplyItem) GetOutcome() ApplyResult_ApplyOutcome { + if x != nil { + return x.Outcome + } + return ApplyResult_APPLY_OUTCOME_UNSPECIFIED +} + +func (x *ApplyResult_ApplyItem) GetResult() isApplyResult_ApplyItem_Result { + if x != nil { + return x.Result + } + return nil +} + +func (x *ApplyResult_ApplyItem) GetToolResult() *v1.ToolResult { + if x != nil { + if x, ok := x.Result.(*ApplyResult_ApplyItem_ToolResult); ok { + return x.ToolResult + } + } + return nil +} + +func (x *ApplyResult_ApplyItem) GetToolError() *v1.ToolError { + if x != nil { + if x, ok := x.Result.(*ApplyResult_ApplyItem_ToolError); ok { + return x.ToolError + } + } + return nil +} + +type isApplyResult_ApplyItem_Result interface { + isApplyResult_ApplyItem_Result() +} + +type ApplyResult_ApplyItem_ToolResult struct { + // The call's successful result, when outcome == + // APPLY_OUTCOME_APPLIED. + ToolResult *v1.ToolResult `protobuf:"bytes,4,opt,name=tool_result,json=toolResult,proto3,oneof"` +} + +type ApplyResult_ApplyItem_ToolError struct { + // The call's failed result, when outcome == APPLY_OUTCOME_FAILED. + ToolError *v1.ToolError `protobuf:"bytes,5,opt,name=tool_error,json=toolError,proto3,oneof"` +} + +func (*ApplyResult_ApplyItem_ToolResult) isApplyResult_ApplyItem_Result() {} + +func (*ApplyResult_ApplyItem_ToolError) isApplyResult_ApplyItem_Result() {} + var File_pluggableharness_agent_plan_v1_plan_proto protoreflect.FileDescriptor const file_pluggableharness_agent_plan_v1_plan_proto_rawDesc = "" + "\n" + - ")pluggableharness/agent/plan/v1/plan.proto\x12\x1epluggableharness.agent.plan.v1\x1a\x1cgoogle/protobuf/struct.proto\"\x8d\x02\n" + + ")pluggableharness/agent/plan/v1/plan.proto\x12\x1epluggableharness.agent.plan.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a)pluggableharness/agent/tool/v1/tool.proto\"\x8d\x02\n" + "\bPlanItem\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12 \n" + "\ftool_call_id\x18\x02 \x01(\tR\n" + @@ -281,7 +535,27 @@ const file_pluggableharness_agent_plan_v1_plan_proto_rawDesc = "" + "decided_by\x18\a \x01(\tR\tdecidedBy\"_\n" + "\x04Plan\x12\x17\n" + "\aturn_id\x18\x01 \x01(\tR\x06turnId\x12>\n" + - "\x05items\x18\x02 \x03(\v2(.pluggableharness.agent.plan.v1.PlanItemR\x05items*\x90\x01\n" + + "\x05items\x18\x02 \x03(\v2(.pluggableharness.agent.plan.v1.PlanItemR\x05items\"\xd8\x04\n" + + "\vApplyResult\x12\x17\n" + + "\aturn_id\x18\x01 \x01(\tR\x06turnId\x12K\n" + + "\x05items\x18\x02 \x03(\v25.pluggableharness.agent.plan.v1.ApplyResult.ApplyItemR\x05items\x1a\xc8\x02\n" + + "\tApplyItem\x12 \n" + + "\fplan_item_id\x18\x01 \x01(\tR\n" + + "planItemId\x12 \n" + + "\ftool_call_id\x18\x02 \x01(\tR\n" + + "toolCallId\x12R\n" + + "\aoutcome\x18\x03 \x01(\x0e28.pluggableharness.agent.plan.v1.ApplyResult.ApplyOutcomeR\aoutcome\x12M\n" + + "\vtool_result\x18\x04 \x01(\v2*.pluggableharness.agent.tool.v1.ToolResultH\x00R\n" + + "toolResult\x12J\n" + + "\n" + + "tool_error\x18\x05 \x01(\v2).pluggableharness.agent.tool.v1.ToolErrorH\x00R\ttoolErrorB\b\n" + + "\x06result\"\x97\x01\n" + + "\fApplyOutcome\x12\x1d\n" + + "\x19APPLY_OUTCOME_UNSPECIFIED\x10\x00\x12\x19\n" + + "\x15APPLY_OUTCOME_APPLIED\x10\x01\x12\x18\n" + + "\x14APPLY_OUTCOME_FAILED\x10\x02\x12\x18\n" + + "\x14APPLY_OUTCOME_DENIED\x10\x03\x12\x19\n" + + "\x15APPLY_OUTCOME_SKIPPED\x10\x04*\x90\x01\n" + "\fPlanDecision\x12\x1d\n" + "\x19PLAN_DECISION_UNSPECIFIED\x10\x00\x12\x19\n" + "\x15PLAN_DECISION_PENDING\x10\x01\x12\x17\n" + @@ -301,23 +575,32 @@ func file_pluggableharness_agent_plan_v1_plan_proto_rawDescGZIP() []byte { return file_pluggableharness_agent_plan_v1_plan_proto_rawDescData } -var file_pluggableharness_agent_plan_v1_plan_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_pluggableharness_agent_plan_v1_plan_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_pluggableharness_agent_plan_v1_plan_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_pluggableharness_agent_plan_v1_plan_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_pluggableharness_agent_plan_v1_plan_proto_goTypes = []any{ - (PlanDecision)(0), // 0: pluggableharness.agent.plan.v1.PlanDecision - (*PlanItem)(nil), // 1: pluggableharness.agent.plan.v1.PlanItem - (*Plan)(nil), // 2: pluggableharness.agent.plan.v1.Plan - (*structpb.Struct)(nil), // 3: google.protobuf.Struct + (PlanDecision)(0), // 0: pluggableharness.agent.plan.v1.PlanDecision + (ApplyResult_ApplyOutcome)(0), // 1: pluggableharness.agent.plan.v1.ApplyResult.ApplyOutcome + (*PlanItem)(nil), // 2: pluggableharness.agent.plan.v1.PlanItem + (*Plan)(nil), // 3: pluggableharness.agent.plan.v1.Plan + (*ApplyResult)(nil), // 4: pluggableharness.agent.plan.v1.ApplyResult + (*ApplyResult_ApplyItem)(nil), // 5: pluggableharness.agent.plan.v1.ApplyResult.ApplyItem + (*structpb.Struct)(nil), // 6: google.protobuf.Struct + (*v1.ToolResult)(nil), // 7: pluggableharness.agent.tool.v1.ToolResult + (*v1.ToolError)(nil), // 8: pluggableharness.agent.tool.v1.ToolError } var file_pluggableharness_agent_plan_v1_plan_proto_depIdxs = []int32{ - 3, // 0: pluggableharness.agent.plan.v1.PlanItem.input:type_name -> google.protobuf.Struct + 6, // 0: pluggableharness.agent.plan.v1.PlanItem.input:type_name -> google.protobuf.Struct 0, // 1: pluggableharness.agent.plan.v1.PlanItem.decision:type_name -> pluggableharness.agent.plan.v1.PlanDecision - 1, // 2: pluggableharness.agent.plan.v1.Plan.items:type_name -> pluggableharness.agent.plan.v1.PlanItem - 3, // [3:3] is the sub-list for method output_type - 3, // [3:3] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name + 2, // 2: pluggableharness.agent.plan.v1.Plan.items:type_name -> pluggableharness.agent.plan.v1.PlanItem + 5, // 3: pluggableharness.agent.plan.v1.ApplyResult.items:type_name -> pluggableharness.agent.plan.v1.ApplyResult.ApplyItem + 1, // 4: pluggableharness.agent.plan.v1.ApplyResult.ApplyItem.outcome:type_name -> pluggableharness.agent.plan.v1.ApplyResult.ApplyOutcome + 7, // 5: pluggableharness.agent.plan.v1.ApplyResult.ApplyItem.tool_result:type_name -> pluggableharness.agent.tool.v1.ToolResult + 8, // 6: pluggableharness.agent.plan.v1.ApplyResult.ApplyItem.tool_error:type_name -> pluggableharness.agent.tool.v1.ToolError + 7, // [7:7] is the sub-list for method output_type + 7, // [7:7] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name } func init() { file_pluggableharness_agent_plan_v1_plan_proto_init() } @@ -325,13 +608,17 @@ func file_pluggableharness_agent_plan_v1_plan_proto_init() { if File_pluggableharness_agent_plan_v1_plan_proto != nil { return } + file_pluggableharness_agent_plan_v1_plan_proto_msgTypes[3].OneofWrappers = []any{ + (*ApplyResult_ApplyItem_ToolResult)(nil), + (*ApplyResult_ApplyItem_ToolError)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_pluggableharness_agent_plan_v1_plan_proto_rawDesc), len(file_pluggableharness_agent_plan_v1_plan_proto_rawDesc)), - NumEnums: 1, - NumMessages: 2, + NumEnums: 2, + NumMessages: 4, NumExtensions: 0, NumServices: 0, }, From 5fae6a698bcaccfdd58337ec2a3886cea4799127 Mon Sep 17 00:00:00 2001 From: Steven Crothers Date: Fri, 24 Jul 2026 13:28:08 -0400 Subject: [PATCH 4/7] Move HookPoint enum to common.v1 hook.v1 imports model/tool/plan/session for its typed payloads, so those packages could not import HookPoint back for their capability messages' supported_hook_points field without a cycle. common.v1 imports nothing -- the same reasoning that homes Category there. --- .../agent/common/v1/common.proto | 42 +++ api/pluggableharness/agent/hook/v1/hook.proto | 51 +--- .../agent-loop/hook-dispatch.md | 4 +- pkg/common/proto/v1/common.pb.go | 120 +++++++- pkg/hook/proto/v1/hook.pb.go | 273 ++++++------------ pkg/hook/proto/v1/hook_grpc.pb.go | 4 +- 6 files changed, 257 insertions(+), 237 deletions(-) diff --git a/api/pluggableharness/agent/common/v1/common.proto b/api/pluggableharness/agent/common/v1/common.proto index 979f9b4..f664b98 100644 --- a/api/pluggableharness/agent/common/v1/common.proto +++ b/api/pluggableharness/agent/common/v1/common.proto @@ -50,6 +50,48 @@ enum Category { CATEGORY_WIDGET = 6; } +// HookPoint identifies one of the eight dispatchable points in the agent +// loop (agent-loop/hook-dispatch.md; architecture.md §Hook dispatch +// semantics enumerates the nine hook-point names, of which +// context-assemble is deliberately excluded — it stays on +// ContextService.Contribute rather than the hook.v1 dispatch surface). +// Lives here rather than in hook.v1 because hook.v1 imports several +// category packages (model, tool, plan, session) for its typed payloads: +// a category package advertising `supported_hook_points` in its +// capability message could not import hook.v1 back without a cycle, and +// this file is the import-nothing leaf every package may depend on — +// the same reasoning that homes Category here. Used by hook.v1's +// HookError, the event.v1 hook-error payload, and every category's +// capability advertisement. +enum HookPoint { + // Zero value. Never valid on the wire; its presence means a caller + // forgot to set the field. + HOOK_POINT_UNSPECIFIED = 0; + // Session creation, before the first turn begins. + HOOK_POINT_SESSION_START = 1; + // Immediately before a model provider's StreamCompletion is called. + HOOK_POINT_PRE_MODEL_CALL = 2; + // Immediately after a model turn's canonical message has been + // assembled from the completion stream. + HOOK_POINT_POST_MODEL_RESPONSE = 3; + // Immediately before a plan item's tool call is applied. + HOOK_POINT_PRE_TOOL_CALL = 4; + // Once a turn's Plan has been fully built, before plan/apply gate + // dispatch. The kernel-privileged policy veto subscriber + // (architecture.md §Policy — first-party, not a plugin category) always + // runs at this point. + HOOK_POINT_PLAN_READY = 5; + // Immediately after a plan item's tool call has produced a terminal + // ToolResult or ToolError. + HOOK_POINT_POST_TOOL_CALL = 6; + // Immediately after a turn's whole Plan has finished applying (every + // item reached a terminal ApplyOutcome). + HOOK_POINT_POST_APPLY = 7; + // Session termination, once the session has reached a terminal + // SessionStatus. + HOOK_POINT_SESSION_END = 8; +} + // ProducerRef identifies the exact plugin build that produced something: a // specific name, version, and source, pinned to the category it // implements. This is the wire-level counterpart of the "supersedes" diff --git a/api/pluggableharness/agent/hook/v1/hook.proto b/api/pluggableharness/agent/hook/v1/hook.proto index 2566e40..d5f74fa 100644 --- a/api/pluggableharness/agent/hook/v1/hook.proto +++ b/api/pluggableharness/agent/hook/v1/hook.proto @@ -11,8 +11,8 @@ syntax = "proto3"; // that plugin's own category service. A plugin with no `hook{}` blocks in // agent.hcl simply never has it called. // -// context-assemble is deliberately absent from this surface's HookPoint -// enum below — it stays on ContextService.Contribute +// context-assemble is deliberately absent from this surface's hook-point +// vocabulary (common.v1.HookPoint) — it stays on ContextService.Contribute // (context/protocol.md#contribute-the-context-assemble-rpc; architecture.md // §Hook dispatch semantics), which already carries the full accumulated // ContextSection chain and doesn't need a second, competing dispatch path. @@ -49,41 +49,12 @@ service HookSubscriberService { rpc DispatchHook(DispatchHookRequest) returns (DispatchHookResponse); } -// HookPoint identifies which of the eight dispatchable points in the agent -// loop a DispatchHookRequest fires for. Not carried directly on -// DispatchHookRequest (the set HookPayload oneof variant already implies -// the point) — this enum exists for HookError, which has no oneof to infer -// a point from. architecture.md §Hook dispatch semantics enumerates these -// nine hook-point names; context-assemble (the ninth) is deliberately -// excluded here — see this file's package comment. -enum HookPoint { - // Zero value. Never valid on the wire; its presence means a caller - // forgot to set the field. - HOOK_POINT_UNSPECIFIED = 0; - // Session creation, before the first turn begins. - HOOK_POINT_SESSION_START = 1; - // Immediately before a model provider's StreamCompletion is called. - HOOK_POINT_PRE_MODEL_CALL = 2; - // Immediately after a model turn's canonical message has been - // assembled from the completion stream. - HOOK_POINT_POST_MODEL_RESPONSE = 3; - // Immediately before a plan item's tool call is applied. - HOOK_POINT_PRE_TOOL_CALL = 4; - // Once a turn's Plan has been fully built, before plan/apply gate - // dispatch. The kernel-privileged policy veto subscriber - // (architecture.md §Policy — first-party, not a plugin category) always - // runs at this point. - HOOK_POINT_PLAN_READY = 5; - // Immediately after a plan item's tool call has produced a terminal - // ToolResult or ToolError. - HOOK_POINT_POST_TOOL_CALL = 6; - // Immediately after a turn's whole Plan has finished applying (every - // item reached a terminal ApplyOutcome). - HOOK_POINT_POST_APPLY = 7; - // Session termination, once the session has reached a terminal - // SessionStatus. - HOOK_POINT_SESSION_END = 8; -} +// The HookPoint enum itself lives in common.v1 (common/v1/common.proto), +// not here: hook.v1 imports several category packages (model, tool, plan, +// session) for its typed payloads, so a category package advertising +// `supported_hook_points` in its capability message could not import this +// package back without a cycle. common.v1 is the import-nothing leaf both +// sides can safely share. // HookMode is the operator-declared subscription mode for one plugin's // hook{} block, per architecture.md §Hook dispatch semantics and @@ -114,8 +85,8 @@ enum HookMode { // HookPayload carries one hook point's data. Exactly one oneof variant is // set; which variant is set *is* the point being dispatched — the -// parallel HookPoint enum above exists only for contexts (HookError) that -// have no oneof to infer the point from. +// parallel common.v1.HookPoint enum exists only for contexts (HookError) +// that have no oneof to infer the point from. message HookPayload { oneof payload { // Fires at HOOK_POINT_SESSION_START. @@ -380,7 +351,7 @@ enum HookErrorCategory { // message rather than redefining it. message HookError { // Which hook point the failing dispatch was for. MUST be set. - HookPoint point = 1; + pluggableharness.agent.common.v1.HookPoint point = 1; // Which plugin build the failing subscriber was. MUST be set. pluggableharness.agent.common.v1.ProducerRef subscriber = 2; // The HookMode the failing subscription was declared under. MUST be diff --git a/docs/specifications/agent-loop/hook-dispatch.md b/docs/specifications/agent-loop/hook-dispatch.md index ef75bf0..ae87d4e 100644 --- a/docs/specifications/agent-loop/hook-dispatch.md +++ b/docs/specifications/agent-loop/hook-dispatch.md @@ -10,7 +10,7 @@ ### Hook points -`hook.v1.HookPoint` enumerates eight of [`architecture.md`](../architecture.md#hook-dispatch-semantics)'s nine named points — every one except `context-assemble`, which stays on `ContextService.Contribute` ([`../context/protocol.md#contribute-the-context-assemble-rpc`](../context/protocol.md#contribute-the-context-assemble-rpc)) rather than riding this surface. `Contribute` already carries the full accumulated `ContextSection` chain as a first-class typed request/response; routing it through the generic `HookPayload` oneof below would just be a second, redundant path to the same effect with weaker typing. +`common.v1.HookPoint` (homed in `common/v1/common.proto` — the import-nothing leaf — so category capability messages can advertise `supported_hook_points` without importing `hook.v1`, which itself imports several category packages for its typed payloads) enumerates eight of [`architecture.md`](../architecture.md#hook-dispatch-semantics)'s nine named points — every one except `context-assemble`, which stays on `ContextService.Contribute` ([`../context/protocol.md#contribute-the-context-assemble-rpc`](../context/protocol.md#contribute-the-context-assemble-rpc)) rather than riding this surface. `Contribute` already carries the full accumulated `ContextSection` chain as a first-class typed request/response; routing it through the generic `HookPayload` oneof below would just be a second, redundant path to the same effect with weaker typing. | Hook point | `HookPayload` variant | |---|---| @@ -23,7 +23,7 @@ | `post-apply` | `PostApplyPayload` | | `session-end` | `SessionEndPayload` | -`HookPayload` is a `oneof`; the set variant *is* the point being dispatched — `DispatchHookRequest` carries no separate `HookPoint` field. `HookPoint` exists on the wire only where there's no oneof to infer a point from: `HookError`, and the future `event.v1.HookErrorEvent` it's embedded in. +`HookPayload` is a `oneof`; the set variant *is* the point being dispatched — `DispatchHookRequest` carries no separate `HookPoint` field. `common.v1.HookPoint` exists on the wire only where there's no oneof to infer a point from: `HookError`, and the future `event.v1.HookErrorEvent` it's embedded in. ### Dispatch modes → response shapes diff --git a/pkg/common/proto/v1/common.pb.go b/pkg/common/proto/v1/common.pb.go index c1de80f..75c2887 100644 --- a/pkg/common/proto/v1/common.pb.go +++ b/pkg/common/proto/v1/common.pb.go @@ -102,6 +102,103 @@ func (Category) EnumDescriptor() ([]byte, []int) { return file_pluggableharness_agent_common_v1_common_proto_rawDescGZIP(), []int{0} } +// HookPoint identifies one of the eight dispatchable points in the agent +// loop (agent-loop/hook-dispatch.md; architecture.md §Hook dispatch +// semantics enumerates the nine hook-point names, of which +// context-assemble is deliberately excluded — it stays on +// ContextService.Contribute rather than the hook.v1 dispatch surface). +// Lives here rather than in hook.v1 because hook.v1 imports several +// category packages (model, tool, plan, session) for its typed payloads: +// a category package advertising `supported_hook_points` in its +// capability message could not import hook.v1 back without a cycle, and +// this file is the import-nothing leaf every package may depend on — +// the same reasoning that homes Category here. Used by hook.v1's +// HookError, the event.v1 hook-error payload, and every category's +// capability advertisement. +type HookPoint int32 + +const ( + // Zero value. Never valid on the wire; its presence means a caller + // forgot to set the field. + HookPoint_HOOK_POINT_UNSPECIFIED HookPoint = 0 + // Session creation, before the first turn begins. + HookPoint_HOOK_POINT_SESSION_START HookPoint = 1 + // Immediately before a model provider's StreamCompletion is called. + HookPoint_HOOK_POINT_PRE_MODEL_CALL HookPoint = 2 + // Immediately after a model turn's canonical message has been + // assembled from the completion stream. + HookPoint_HOOK_POINT_POST_MODEL_RESPONSE HookPoint = 3 + // Immediately before a plan item's tool call is applied. + HookPoint_HOOK_POINT_PRE_TOOL_CALL HookPoint = 4 + // Once a turn's Plan has been fully built, before plan/apply gate + // dispatch. The kernel-privileged policy veto subscriber + // (architecture.md §Policy — first-party, not a plugin category) always + // runs at this point. + HookPoint_HOOK_POINT_PLAN_READY HookPoint = 5 + // Immediately after a plan item's tool call has produced a terminal + // ToolResult or ToolError. + HookPoint_HOOK_POINT_POST_TOOL_CALL HookPoint = 6 + // Immediately after a turn's whole Plan has finished applying (every + // item reached a terminal ApplyOutcome). + HookPoint_HOOK_POINT_POST_APPLY HookPoint = 7 + // Session termination, once the session has reached a terminal + // SessionStatus. + HookPoint_HOOK_POINT_SESSION_END HookPoint = 8 +) + +// Enum value maps for HookPoint. +var ( + HookPoint_name = map[int32]string{ + 0: "HOOK_POINT_UNSPECIFIED", + 1: "HOOK_POINT_SESSION_START", + 2: "HOOK_POINT_PRE_MODEL_CALL", + 3: "HOOK_POINT_POST_MODEL_RESPONSE", + 4: "HOOK_POINT_PRE_TOOL_CALL", + 5: "HOOK_POINT_PLAN_READY", + 6: "HOOK_POINT_POST_TOOL_CALL", + 7: "HOOK_POINT_POST_APPLY", + 8: "HOOK_POINT_SESSION_END", + } + HookPoint_value = map[string]int32{ + "HOOK_POINT_UNSPECIFIED": 0, + "HOOK_POINT_SESSION_START": 1, + "HOOK_POINT_PRE_MODEL_CALL": 2, + "HOOK_POINT_POST_MODEL_RESPONSE": 3, + "HOOK_POINT_PRE_TOOL_CALL": 4, + "HOOK_POINT_PLAN_READY": 5, + "HOOK_POINT_POST_TOOL_CALL": 6, + "HOOK_POINT_POST_APPLY": 7, + "HOOK_POINT_SESSION_END": 8, + } +) + +func (x HookPoint) Enum() *HookPoint { + p := new(HookPoint) + *p = x + return p +} + +func (x HookPoint) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (HookPoint) Descriptor() protoreflect.EnumDescriptor { + return file_pluggableharness_agent_common_v1_common_proto_enumTypes[1].Descriptor() +} + +func (HookPoint) Type() protoreflect.EnumType { + return &file_pluggableharness_agent_common_v1_common_proto_enumTypes[1] +} + +func (x HookPoint) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use HookPoint.Descriptor instead. +func (HookPoint) EnumDescriptor() ([]byte, []int) { + return file_pluggableharness_agent_common_v1_common_proto_rawDescGZIP(), []int{1} +} + // ProducerRef identifies the exact plugin build that produced something: a // specific name, version, and source, pinned to the category it // implements. This is the wire-level counterpart of the "supersedes" @@ -359,7 +456,17 @@ const file_pluggableharness_agent_common_v1_common_proto_rawDesc = "" + "\x10CATEGORY_CONTEXT\x10\x03\x12\x13\n" + "\x0fCATEGORY_MEMORY\x10\x04\x12\x15\n" + "\x11CATEGORY_FRONTEND\x10\x05\x12\x13\n" + - "\x0fCATEGORY_WIDGET\x10\x06B@Z>github.com/pluggableharness/agent/pkg/common/proto/v1;commonv1b\x06proto3" + "\x0fCATEGORY_WIDGET\x10\x06*\x97\x02\n" + + "\tHookPoint\x12\x1a\n" + + "\x16HOOK_POINT_UNSPECIFIED\x10\x00\x12\x1c\n" + + "\x18HOOK_POINT_SESSION_START\x10\x01\x12\x1d\n" + + "\x19HOOK_POINT_PRE_MODEL_CALL\x10\x02\x12\"\n" + + "\x1eHOOK_POINT_POST_MODEL_RESPONSE\x10\x03\x12\x1c\n" + + "\x18HOOK_POINT_PRE_TOOL_CALL\x10\x04\x12\x19\n" + + "\x15HOOK_POINT_PLAN_READY\x10\x05\x12\x1d\n" + + "\x19HOOK_POINT_POST_TOOL_CALL\x10\x06\x12\x19\n" + + "\x15HOOK_POINT_POST_APPLY\x10\a\x12\x1a\n" + + "\x16HOOK_POINT_SESSION_END\x10\bB@Z>github.com/pluggableharness/agent/pkg/common/proto/v1;commonv1b\x06proto3" var ( file_pluggableharness_agent_common_v1_common_proto_rawDescOnce sync.Once @@ -373,13 +480,14 @@ func file_pluggableharness_agent_common_v1_common_proto_rawDescGZIP() []byte { return file_pluggableharness_agent_common_v1_common_proto_rawDescData } -var file_pluggableharness_agent_common_v1_common_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_pluggableharness_agent_common_v1_common_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_pluggableharness_agent_common_v1_common_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_pluggableharness_agent_common_v1_common_proto_goTypes = []any{ (Category)(0), // 0: pluggableharness.agent.common.v1.Category - (*ProducerRef)(nil), // 1: pluggableharness.agent.common.v1.ProducerRef - (*ProviderRef)(nil), // 2: pluggableharness.agent.common.v1.ProviderRef - (*CallContext)(nil), // 3: pluggableharness.agent.common.v1.CallContext + (HookPoint)(0), // 1: pluggableharness.agent.common.v1.HookPoint + (*ProducerRef)(nil), // 2: pluggableharness.agent.common.v1.ProducerRef + (*ProviderRef)(nil), // 3: pluggableharness.agent.common.v1.ProviderRef + (*CallContext)(nil), // 4: pluggableharness.agent.common.v1.CallContext } var file_pluggableharness_agent_common_v1_common_proto_depIdxs = []int32{ 0, // 0: pluggableharness.agent.common.v1.ProducerRef.category:type_name -> pluggableharness.agent.common.v1.Category @@ -401,7 +509,7 @@ func file_pluggableharness_agent_common_v1_common_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_pluggableharness_agent_common_v1_common_proto_rawDesc), len(file_pluggableharness_agent_common_v1_common_proto_rawDesc)), - NumEnums: 1, + NumEnums: 2, NumMessages: 3, NumExtensions: 0, NumServices: 0, diff --git a/pkg/hook/proto/v1/hook.pb.go b/pkg/hook/proto/v1/hook.pb.go index 590313d..d47a66c 100644 --- a/pkg/hook/proto/v1/hook.pb.go +++ b/pkg/hook/proto/v1/hook.pb.go @@ -15,8 +15,8 @@ // that plugin's own category service. A plugin with no `hook{}` blocks in // agent.hcl simply never has it called. // -// context-assemble is deliberately absent from this surface's HookPoint -// enum below — it stays on ContextService.Contribute +// context-assemble is deliberately absent from this surface's hook-point +// vocabulary (common.v1.HookPoint) — it stays on ContextService.Contribute // (context/protocol.md#contribute-the-context-assemble-rpc; architecture.md // §Hook dispatch semantics), which already carries the full accumulated // ContextSection chain and doesn't need a second, competing dispatch path. @@ -45,97 +45,6 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// HookPoint identifies which of the eight dispatchable points in the agent -// loop a DispatchHookRequest fires for. Not carried directly on -// DispatchHookRequest (the set HookPayload oneof variant already implies -// the point) — this enum exists for HookError, which has no oneof to infer -// a point from. architecture.md §Hook dispatch semantics enumerates these -// nine hook-point names; context-assemble (the ninth) is deliberately -// excluded here — see this file's package comment. -type HookPoint int32 - -const ( - // Zero value. Never valid on the wire; its presence means a caller - // forgot to set the field. - HookPoint_HOOK_POINT_UNSPECIFIED HookPoint = 0 - // Session creation, before the first turn begins. - HookPoint_HOOK_POINT_SESSION_START HookPoint = 1 - // Immediately before a model provider's StreamCompletion is called. - HookPoint_HOOK_POINT_PRE_MODEL_CALL HookPoint = 2 - // Immediately after a model turn's canonical message has been - // assembled from the completion stream. - HookPoint_HOOK_POINT_POST_MODEL_RESPONSE HookPoint = 3 - // Immediately before a plan item's tool call is applied. - HookPoint_HOOK_POINT_PRE_TOOL_CALL HookPoint = 4 - // Once a turn's Plan has been fully built, before plan/apply gate - // dispatch. The kernel-privileged policy veto subscriber - // (architecture.md §Policy — first-party, not a plugin category) always - // runs at this point. - HookPoint_HOOK_POINT_PLAN_READY HookPoint = 5 - // Immediately after a plan item's tool call has produced a terminal - // ToolResult or ToolError. - HookPoint_HOOK_POINT_POST_TOOL_CALL HookPoint = 6 - // Immediately after a turn's whole Plan has finished applying (every - // item reached a terminal ApplyOutcome). - HookPoint_HOOK_POINT_POST_APPLY HookPoint = 7 - // Session termination, once the session has reached a terminal - // SessionStatus. - HookPoint_HOOK_POINT_SESSION_END HookPoint = 8 -) - -// Enum value maps for HookPoint. -var ( - HookPoint_name = map[int32]string{ - 0: "HOOK_POINT_UNSPECIFIED", - 1: "HOOK_POINT_SESSION_START", - 2: "HOOK_POINT_PRE_MODEL_CALL", - 3: "HOOK_POINT_POST_MODEL_RESPONSE", - 4: "HOOK_POINT_PRE_TOOL_CALL", - 5: "HOOK_POINT_PLAN_READY", - 6: "HOOK_POINT_POST_TOOL_CALL", - 7: "HOOK_POINT_POST_APPLY", - 8: "HOOK_POINT_SESSION_END", - } - HookPoint_value = map[string]int32{ - "HOOK_POINT_UNSPECIFIED": 0, - "HOOK_POINT_SESSION_START": 1, - "HOOK_POINT_PRE_MODEL_CALL": 2, - "HOOK_POINT_POST_MODEL_RESPONSE": 3, - "HOOK_POINT_PRE_TOOL_CALL": 4, - "HOOK_POINT_PLAN_READY": 5, - "HOOK_POINT_POST_TOOL_CALL": 6, - "HOOK_POINT_POST_APPLY": 7, - "HOOK_POINT_SESSION_END": 8, - } -) - -func (x HookPoint) Enum() *HookPoint { - p := new(HookPoint) - *p = x - return p -} - -func (x HookPoint) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (HookPoint) Descriptor() protoreflect.EnumDescriptor { - return file_pluggableharness_agent_hook_v1_hook_proto_enumTypes[0].Descriptor() -} - -func (HookPoint) Type() protoreflect.EnumType { - return &file_pluggableharness_agent_hook_v1_hook_proto_enumTypes[0] -} - -func (x HookPoint) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use HookPoint.Descriptor instead. -func (HookPoint) EnumDescriptor() ([]byte, []int) { - return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{0} -} - // HookMode is the operator-declared subscription mode for one plugin's // hook{} block, per architecture.md §Hook dispatch semantics and // agent-loop/hook-dispatch.md#dispatch-order-and-payload-flow. Mode is @@ -192,11 +101,11 @@ func (x HookMode) String() string { } func (HookMode) Descriptor() protoreflect.EnumDescriptor { - return file_pluggableharness_agent_hook_v1_hook_proto_enumTypes[1].Descriptor() + return file_pluggableharness_agent_hook_v1_hook_proto_enumTypes[0].Descriptor() } func (HookMode) Type() protoreflect.EnumType { - return &file_pluggableharness_agent_hook_v1_hook_proto_enumTypes[1] + return &file_pluggableharness_agent_hook_v1_hook_proto_enumTypes[0] } func (x HookMode) Number() protoreflect.EnumNumber { @@ -205,7 +114,7 @@ func (x HookMode) Number() protoreflect.EnumNumber { // Deprecated: Use HookMode.Descriptor instead. func (HookMode) EnumDescriptor() ([]byte, []int) { - return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{1} + return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{0} } // HookDecision is a veto subscriber's coarse allow/deny verdict over a @@ -258,11 +167,11 @@ func (x HookDecision) String() string { } func (HookDecision) Descriptor() protoreflect.EnumDescriptor { - return file_pluggableharness_agent_hook_v1_hook_proto_enumTypes[2].Descriptor() + return file_pluggableharness_agent_hook_v1_hook_proto_enumTypes[1].Descriptor() } func (HookDecision) Type() protoreflect.EnumType { - return &file_pluggableharness_agent_hook_v1_hook_proto_enumTypes[2] + return &file_pluggableharness_agent_hook_v1_hook_proto_enumTypes[1] } func (x HookDecision) Number() protoreflect.EnumNumber { @@ -271,7 +180,7 @@ func (x HookDecision) Number() protoreflect.EnumNumber { // Deprecated: Use HookDecision.Descriptor instead. func (HookDecision) EnumDescriptor() ([]byte, []int) { - return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{2} + return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{1} } // HookErrorCategory classifies why a hook dispatch to one subscriber @@ -346,11 +255,11 @@ func (x HookErrorCategory) String() string { } func (HookErrorCategory) Descriptor() protoreflect.EnumDescriptor { - return file_pluggableharness_agent_hook_v1_hook_proto_enumTypes[3].Descriptor() + return file_pluggableharness_agent_hook_v1_hook_proto_enumTypes[2].Descriptor() } func (HookErrorCategory) Type() protoreflect.EnumType { - return &file_pluggableharness_agent_hook_v1_hook_proto_enumTypes[3] + return &file_pluggableharness_agent_hook_v1_hook_proto_enumTypes[2] } func (x HookErrorCategory) Number() protoreflect.EnumNumber { @@ -359,13 +268,13 @@ func (x HookErrorCategory) Number() protoreflect.EnumNumber { // Deprecated: Use HookErrorCategory.Descriptor instead. func (HookErrorCategory) EnumDescriptor() ([]byte, []int) { - return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{3} + return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{2} } // HookPayload carries one hook point's data. Exactly one oneof variant is // set; which variant is set *is* the point being dispatched — the -// parallel HookPoint enum above exists only for contexts (HookError) that -// have no oneof to infer the point from. +// parallel common.v1.HookPoint enum exists only for contexts (HookError) +// that have no oneof to infer the point from. type HookPayload struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Payload: @@ -1279,7 +1188,7 @@ func (*DispatchHookResponse_Veto) isDispatchHookResponse_Outcome() {} type HookError struct { state protoimpl.MessageState `protogen:"open.v1"` // Which hook point the failing dispatch was for. MUST be set. - Point HookPoint `protobuf:"varint,1,opt,name=point,proto3,enum=pluggableharness.agent.hook.v1.HookPoint" json:"point,omitempty"` + Point v12.HookPoint `protobuf:"varint,1,opt,name=point,proto3,enum=pluggableharness.agent.common.v1.HookPoint" json:"point,omitempty"` // Which plugin build the failing subscriber was. MUST be set. Subscriber *v12.ProducerRef `protobuf:"bytes,2,opt,name=subscriber,proto3" json:"subscriber,omitempty"` // The HookMode the failing subscription was declared under. MUST be @@ -1323,11 +1232,11 @@ func (*HookError) Descriptor() ([]byte, []int) { return file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP(), []int{11} } -func (x *HookError) GetPoint() HookPoint { +func (x *HookError) GetPoint() v12.HookPoint { if x != nil { return x.Point } - return HookPoint_HOOK_POINT_UNSPECIFIED + return v12.HookPoint(0) } func (x *HookError) GetSubscriber() *v12.ProducerRef { @@ -1563,25 +1472,15 @@ const file_pluggableharness_agent_hook_v1_hook_proto_rawDesc = "" + "\n" + "VetoResult\x12H\n" + "\bdecision\x18\x01 \x01(\x0e2,.pluggableharness.agent.hook.v1.HookDecisionR\bdecisionB\t\n" + - "\aoutcome\"\xc2\x02\n" + - "\tHookError\x12?\n" + - "\x05point\x18\x01 \x01(\x0e2).pluggableharness.agent.hook.v1.HookPointR\x05point\x12M\n" + + "\aoutcome\"\xc4\x02\n" + + "\tHookError\x12A\n" + + "\x05point\x18\x01 \x01(\x0e2+.pluggableharness.agent.common.v1.HookPointR\x05point\x12M\n" + "\n" + "subscriber\x18\x02 \x01(\v2-.pluggableharness.agent.common.v1.ProducerRefR\n" + "subscriber\x12<\n" + "\x04mode\x18\x03 \x01(\x0e2(.pluggableharness.agent.hook.v1.HookModeR\x04mode\x12M\n" + "\bcategory\x18\x04 \x01(\x0e21.pluggableharness.agent.hook.v1.HookErrorCategoryR\bcategory\x12\x18\n" + - "\amessage\x18\x05 \x01(\tR\amessage*\x97\x02\n" + - "\tHookPoint\x12\x1a\n" + - "\x16HOOK_POINT_UNSPECIFIED\x10\x00\x12\x1c\n" + - "\x18HOOK_POINT_SESSION_START\x10\x01\x12\x1d\n" + - "\x19HOOK_POINT_PRE_MODEL_CALL\x10\x02\x12\"\n" + - "\x1eHOOK_POINT_POST_MODEL_RESPONSE\x10\x03\x12\x1c\n" + - "\x18HOOK_POINT_PRE_TOOL_CALL\x10\x04\x12\x19\n" + - "\x15HOOK_POINT_PLAN_READY\x10\x05\x12\x1d\n" + - "\x19HOOK_POINT_POST_TOOL_CALL\x10\x06\x12\x19\n" + - "\x15HOOK_POINT_POST_APPLY\x10\a\x12\x1a\n" + - "\x16HOOK_POINT_SESSION_END\x10\b*i\n" + + "\amessage\x18\x05 \x01(\tR\amessage*i\n" + "\bHookMode\x12\x19\n" + "\x15HOOK_MODE_UNSPECIFIED\x10\x00\x12\x15\n" + "\x11HOOK_MODE_OBSERVE\x10\x01\x12\x17\n" + @@ -1614,75 +1513,75 @@ func file_pluggableharness_agent_hook_v1_hook_proto_rawDescGZIP() []byte { return file_pluggableharness_agent_hook_v1_hook_proto_rawDescData } -var file_pluggableharness_agent_hook_v1_hook_proto_enumTypes = make([]protoimpl.EnumInfo, 4) +var file_pluggableharness_agent_hook_v1_hook_proto_enumTypes = make([]protoimpl.EnumInfo, 3) var file_pluggableharness_agent_hook_v1_hook_proto_msgTypes = make([]protoimpl.MessageInfo, 15) var file_pluggableharness_agent_hook_v1_hook_proto_goTypes = []any{ - (HookPoint)(0), // 0: pluggableharness.agent.hook.v1.HookPoint - (HookMode)(0), // 1: pluggableharness.agent.hook.v1.HookMode - (HookDecision)(0), // 2: pluggableharness.agent.hook.v1.HookDecision - (HookErrorCategory)(0), // 3: pluggableharness.agent.hook.v1.HookErrorCategory - (*HookPayload)(nil), // 4: pluggableharness.agent.hook.v1.HookPayload - (*SessionStartPayload)(nil), // 5: pluggableharness.agent.hook.v1.SessionStartPayload - (*PreModelCallPayload)(nil), // 6: pluggableharness.agent.hook.v1.PreModelCallPayload - (*PostModelResponsePayload)(nil), // 7: pluggableharness.agent.hook.v1.PostModelResponsePayload - (*PreToolCallPayload)(nil), // 8: pluggableharness.agent.hook.v1.PreToolCallPayload - (*PlanReadyPayload)(nil), // 9: pluggableharness.agent.hook.v1.PlanReadyPayload - (*PostToolCallPayload)(nil), // 10: pluggableharness.agent.hook.v1.PostToolCallPayload - (*PostApplyPayload)(nil), // 11: pluggableharness.agent.hook.v1.PostApplyPayload - (*SessionEndPayload)(nil), // 12: pluggableharness.agent.hook.v1.SessionEndPayload - (*DispatchHookRequest)(nil), // 13: pluggableharness.agent.hook.v1.DispatchHookRequest - (*DispatchHookResponse)(nil), // 14: pluggableharness.agent.hook.v1.DispatchHookResponse - (*HookError)(nil), // 15: pluggableharness.agent.hook.v1.HookError - (*DispatchHookResponse_ObserveAck)(nil), // 16: pluggableharness.agent.hook.v1.DispatchHookResponse.ObserveAck - (*DispatchHookResponse_TransformResult)(nil), // 17: pluggableharness.agent.hook.v1.DispatchHookResponse.TransformResult - (*DispatchHookResponse_VetoResult)(nil), // 18: pluggableharness.agent.hook.v1.DispatchHookResponse.VetoResult - (*v1.Message)(nil), // 19: pluggableharness.agent.content.v1.Message - (*v11.ModelRef)(nil), // 20: pluggableharness.agent.model.v1.ModelRef - (*v12.ProducerRef)(nil), // 21: pluggableharness.agent.common.v1.ProducerRef - (*v11.Usage)(nil), // 22: pluggableharness.agent.model.v1.Usage - (*v13.ToolCall)(nil), // 23: pluggableharness.agent.tool.v1.ToolCall - (*v14.PlanItem)(nil), // 24: pluggableharness.agent.plan.v1.PlanItem - (*v14.Plan)(nil), // 25: pluggableharness.agent.plan.v1.Plan - (*v13.ToolResult)(nil), // 26: pluggableharness.agent.tool.v1.ToolResult - (*v13.ToolError)(nil), // 27: pluggableharness.agent.tool.v1.ToolError - (*v14.ApplyResult)(nil), // 28: pluggableharness.agent.plan.v1.ApplyResult - (v15.SessionStatus)(0), // 29: pluggableharness.agent.session.v1.SessionStatus + (HookMode)(0), // 0: pluggableharness.agent.hook.v1.HookMode + (HookDecision)(0), // 1: pluggableharness.agent.hook.v1.HookDecision + (HookErrorCategory)(0), // 2: pluggableharness.agent.hook.v1.HookErrorCategory + (*HookPayload)(nil), // 3: pluggableharness.agent.hook.v1.HookPayload + (*SessionStartPayload)(nil), // 4: pluggableharness.agent.hook.v1.SessionStartPayload + (*PreModelCallPayload)(nil), // 5: pluggableharness.agent.hook.v1.PreModelCallPayload + (*PostModelResponsePayload)(nil), // 6: pluggableharness.agent.hook.v1.PostModelResponsePayload + (*PreToolCallPayload)(nil), // 7: pluggableharness.agent.hook.v1.PreToolCallPayload + (*PlanReadyPayload)(nil), // 8: pluggableharness.agent.hook.v1.PlanReadyPayload + (*PostToolCallPayload)(nil), // 9: pluggableharness.agent.hook.v1.PostToolCallPayload + (*PostApplyPayload)(nil), // 10: pluggableharness.agent.hook.v1.PostApplyPayload + (*SessionEndPayload)(nil), // 11: pluggableharness.agent.hook.v1.SessionEndPayload + (*DispatchHookRequest)(nil), // 12: pluggableharness.agent.hook.v1.DispatchHookRequest + (*DispatchHookResponse)(nil), // 13: pluggableharness.agent.hook.v1.DispatchHookResponse + (*HookError)(nil), // 14: pluggableharness.agent.hook.v1.HookError + (*DispatchHookResponse_ObserveAck)(nil), // 15: pluggableharness.agent.hook.v1.DispatchHookResponse.ObserveAck + (*DispatchHookResponse_TransformResult)(nil), // 16: pluggableharness.agent.hook.v1.DispatchHookResponse.TransformResult + (*DispatchHookResponse_VetoResult)(nil), // 17: pluggableharness.agent.hook.v1.DispatchHookResponse.VetoResult + (*v1.Message)(nil), // 18: pluggableharness.agent.content.v1.Message + (*v11.ModelRef)(nil), // 19: pluggableharness.agent.model.v1.ModelRef + (*v12.ProducerRef)(nil), // 20: pluggableharness.agent.common.v1.ProducerRef + (*v11.Usage)(nil), // 21: pluggableharness.agent.model.v1.Usage + (*v13.ToolCall)(nil), // 22: pluggableharness.agent.tool.v1.ToolCall + (*v14.PlanItem)(nil), // 23: pluggableharness.agent.plan.v1.PlanItem + (*v14.Plan)(nil), // 24: pluggableharness.agent.plan.v1.Plan + (*v13.ToolResult)(nil), // 25: pluggableharness.agent.tool.v1.ToolResult + (*v13.ToolError)(nil), // 26: pluggableharness.agent.tool.v1.ToolError + (*v14.ApplyResult)(nil), // 27: pluggableharness.agent.plan.v1.ApplyResult + (v15.SessionStatus)(0), // 28: pluggableharness.agent.session.v1.SessionStatus + (v12.HookPoint)(0), // 29: pluggableharness.agent.common.v1.HookPoint } var file_pluggableharness_agent_hook_v1_hook_proto_depIdxs = []int32{ - 5, // 0: pluggableharness.agent.hook.v1.HookPayload.session_start:type_name -> pluggableharness.agent.hook.v1.SessionStartPayload - 6, // 1: pluggableharness.agent.hook.v1.HookPayload.pre_model_call:type_name -> pluggableharness.agent.hook.v1.PreModelCallPayload - 7, // 2: pluggableharness.agent.hook.v1.HookPayload.post_model_response:type_name -> pluggableharness.agent.hook.v1.PostModelResponsePayload - 8, // 3: pluggableharness.agent.hook.v1.HookPayload.pre_tool_call:type_name -> pluggableharness.agent.hook.v1.PreToolCallPayload - 9, // 4: pluggableharness.agent.hook.v1.HookPayload.plan_ready:type_name -> pluggableharness.agent.hook.v1.PlanReadyPayload - 10, // 5: pluggableharness.agent.hook.v1.HookPayload.post_tool_call:type_name -> pluggableharness.agent.hook.v1.PostToolCallPayload - 11, // 6: pluggableharness.agent.hook.v1.HookPayload.post_apply:type_name -> pluggableharness.agent.hook.v1.PostApplyPayload - 12, // 7: pluggableharness.agent.hook.v1.HookPayload.session_end:type_name -> pluggableharness.agent.hook.v1.SessionEndPayload - 19, // 8: pluggableharness.agent.hook.v1.PreModelCallPayload.messages:type_name -> pluggableharness.agent.content.v1.Message - 20, // 9: pluggableharness.agent.hook.v1.PreModelCallPayload.model:type_name -> pluggableharness.agent.model.v1.ModelRef - 19, // 10: pluggableharness.agent.hook.v1.PostModelResponsePayload.message:type_name -> pluggableharness.agent.content.v1.Message - 21, // 11: pluggableharness.agent.hook.v1.PostModelResponsePayload.model:type_name -> pluggableharness.agent.common.v1.ProducerRef - 22, // 12: pluggableharness.agent.hook.v1.PostModelResponsePayload.usage:type_name -> pluggableharness.agent.model.v1.Usage - 23, // 13: pluggableharness.agent.hook.v1.PreToolCallPayload.call:type_name -> pluggableharness.agent.tool.v1.ToolCall - 24, // 14: pluggableharness.agent.hook.v1.PreToolCallPayload.plan_item:type_name -> pluggableharness.agent.plan.v1.PlanItem - 25, // 15: pluggableharness.agent.hook.v1.PlanReadyPayload.plan:type_name -> pluggableharness.agent.plan.v1.Plan - 23, // 16: pluggableharness.agent.hook.v1.PostToolCallPayload.call:type_name -> pluggableharness.agent.tool.v1.ToolCall - 26, // 17: pluggableharness.agent.hook.v1.PostToolCallPayload.result:type_name -> pluggableharness.agent.tool.v1.ToolResult - 27, // 18: pluggableharness.agent.hook.v1.PostToolCallPayload.error:type_name -> pluggableharness.agent.tool.v1.ToolError - 28, // 19: pluggableharness.agent.hook.v1.PostApplyPayload.apply:type_name -> pluggableharness.agent.plan.v1.ApplyResult - 29, // 20: pluggableharness.agent.hook.v1.SessionEndPayload.status:type_name -> pluggableharness.agent.session.v1.SessionStatus - 4, // 21: pluggableharness.agent.hook.v1.DispatchHookRequest.payload:type_name -> pluggableharness.agent.hook.v1.HookPayload - 1, // 22: pluggableharness.agent.hook.v1.DispatchHookRequest.mode:type_name -> pluggableharness.agent.hook.v1.HookMode - 16, // 23: pluggableharness.agent.hook.v1.DispatchHookResponse.observe:type_name -> pluggableharness.agent.hook.v1.DispatchHookResponse.ObserveAck - 17, // 24: pluggableharness.agent.hook.v1.DispatchHookResponse.transform:type_name -> pluggableharness.agent.hook.v1.DispatchHookResponse.TransformResult - 18, // 25: pluggableharness.agent.hook.v1.DispatchHookResponse.veto:type_name -> pluggableharness.agent.hook.v1.DispatchHookResponse.VetoResult - 0, // 26: pluggableharness.agent.hook.v1.HookError.point:type_name -> pluggableharness.agent.hook.v1.HookPoint - 21, // 27: pluggableharness.agent.hook.v1.HookError.subscriber:type_name -> pluggableharness.agent.common.v1.ProducerRef - 1, // 28: pluggableharness.agent.hook.v1.HookError.mode:type_name -> pluggableharness.agent.hook.v1.HookMode - 3, // 29: pluggableharness.agent.hook.v1.HookError.category:type_name -> pluggableharness.agent.hook.v1.HookErrorCategory - 4, // 30: pluggableharness.agent.hook.v1.DispatchHookResponse.TransformResult.payload:type_name -> pluggableharness.agent.hook.v1.HookPayload - 2, // 31: pluggableharness.agent.hook.v1.DispatchHookResponse.VetoResult.decision:type_name -> pluggableharness.agent.hook.v1.HookDecision - 13, // 32: pluggableharness.agent.hook.v1.HookSubscriberService.DispatchHook:input_type -> pluggableharness.agent.hook.v1.DispatchHookRequest - 14, // 33: pluggableharness.agent.hook.v1.HookSubscriberService.DispatchHook:output_type -> pluggableharness.agent.hook.v1.DispatchHookResponse + 4, // 0: pluggableharness.agent.hook.v1.HookPayload.session_start:type_name -> pluggableharness.agent.hook.v1.SessionStartPayload + 5, // 1: pluggableharness.agent.hook.v1.HookPayload.pre_model_call:type_name -> pluggableharness.agent.hook.v1.PreModelCallPayload + 6, // 2: pluggableharness.agent.hook.v1.HookPayload.post_model_response:type_name -> pluggableharness.agent.hook.v1.PostModelResponsePayload + 7, // 3: pluggableharness.agent.hook.v1.HookPayload.pre_tool_call:type_name -> pluggableharness.agent.hook.v1.PreToolCallPayload + 8, // 4: pluggableharness.agent.hook.v1.HookPayload.plan_ready:type_name -> pluggableharness.agent.hook.v1.PlanReadyPayload + 9, // 5: pluggableharness.agent.hook.v1.HookPayload.post_tool_call:type_name -> pluggableharness.agent.hook.v1.PostToolCallPayload + 10, // 6: pluggableharness.agent.hook.v1.HookPayload.post_apply:type_name -> pluggableharness.agent.hook.v1.PostApplyPayload + 11, // 7: pluggableharness.agent.hook.v1.HookPayload.session_end:type_name -> pluggableharness.agent.hook.v1.SessionEndPayload + 18, // 8: pluggableharness.agent.hook.v1.PreModelCallPayload.messages:type_name -> pluggableharness.agent.content.v1.Message + 19, // 9: pluggableharness.agent.hook.v1.PreModelCallPayload.model:type_name -> pluggableharness.agent.model.v1.ModelRef + 18, // 10: pluggableharness.agent.hook.v1.PostModelResponsePayload.message:type_name -> pluggableharness.agent.content.v1.Message + 20, // 11: pluggableharness.agent.hook.v1.PostModelResponsePayload.model:type_name -> pluggableharness.agent.common.v1.ProducerRef + 21, // 12: pluggableharness.agent.hook.v1.PostModelResponsePayload.usage:type_name -> pluggableharness.agent.model.v1.Usage + 22, // 13: pluggableharness.agent.hook.v1.PreToolCallPayload.call:type_name -> pluggableharness.agent.tool.v1.ToolCall + 23, // 14: pluggableharness.agent.hook.v1.PreToolCallPayload.plan_item:type_name -> pluggableharness.agent.plan.v1.PlanItem + 24, // 15: pluggableharness.agent.hook.v1.PlanReadyPayload.plan:type_name -> pluggableharness.agent.plan.v1.Plan + 22, // 16: pluggableharness.agent.hook.v1.PostToolCallPayload.call:type_name -> pluggableharness.agent.tool.v1.ToolCall + 25, // 17: pluggableharness.agent.hook.v1.PostToolCallPayload.result:type_name -> pluggableharness.agent.tool.v1.ToolResult + 26, // 18: pluggableharness.agent.hook.v1.PostToolCallPayload.error:type_name -> pluggableharness.agent.tool.v1.ToolError + 27, // 19: pluggableharness.agent.hook.v1.PostApplyPayload.apply:type_name -> pluggableharness.agent.plan.v1.ApplyResult + 28, // 20: pluggableharness.agent.hook.v1.SessionEndPayload.status:type_name -> pluggableharness.agent.session.v1.SessionStatus + 3, // 21: pluggableharness.agent.hook.v1.DispatchHookRequest.payload:type_name -> pluggableharness.agent.hook.v1.HookPayload + 0, // 22: pluggableharness.agent.hook.v1.DispatchHookRequest.mode:type_name -> pluggableharness.agent.hook.v1.HookMode + 15, // 23: pluggableharness.agent.hook.v1.DispatchHookResponse.observe:type_name -> pluggableharness.agent.hook.v1.DispatchHookResponse.ObserveAck + 16, // 24: pluggableharness.agent.hook.v1.DispatchHookResponse.transform:type_name -> pluggableharness.agent.hook.v1.DispatchHookResponse.TransformResult + 17, // 25: pluggableharness.agent.hook.v1.DispatchHookResponse.veto:type_name -> pluggableharness.agent.hook.v1.DispatchHookResponse.VetoResult + 29, // 26: pluggableharness.agent.hook.v1.HookError.point:type_name -> pluggableharness.agent.common.v1.HookPoint + 20, // 27: pluggableharness.agent.hook.v1.HookError.subscriber:type_name -> pluggableharness.agent.common.v1.ProducerRef + 0, // 28: pluggableharness.agent.hook.v1.HookError.mode:type_name -> pluggableharness.agent.hook.v1.HookMode + 2, // 29: pluggableharness.agent.hook.v1.HookError.category:type_name -> pluggableharness.agent.hook.v1.HookErrorCategory + 3, // 30: pluggableharness.agent.hook.v1.DispatchHookResponse.TransformResult.payload:type_name -> pluggableharness.agent.hook.v1.HookPayload + 1, // 31: pluggableharness.agent.hook.v1.DispatchHookResponse.VetoResult.decision:type_name -> pluggableharness.agent.hook.v1.HookDecision + 12, // 32: pluggableharness.agent.hook.v1.HookSubscriberService.DispatchHook:input_type -> pluggableharness.agent.hook.v1.DispatchHookRequest + 13, // 33: pluggableharness.agent.hook.v1.HookSubscriberService.DispatchHook:output_type -> pluggableharness.agent.hook.v1.DispatchHookResponse 33, // [33:34] is the sub-list for method output_type 32, // [32:33] is the sub-list for method input_type 32, // [32:32] is the sub-list for extension type_name @@ -1721,7 +1620,7 @@ func file_pluggableharness_agent_hook_v1_hook_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_pluggableharness_agent_hook_v1_hook_proto_rawDesc), len(file_pluggableharness_agent_hook_v1_hook_proto_rawDesc)), - NumEnums: 4, + NumEnums: 3, NumMessages: 15, NumExtensions: 0, NumServices: 1, diff --git a/pkg/hook/proto/v1/hook_grpc.pb.go b/pkg/hook/proto/v1/hook_grpc.pb.go index 1f2a1e2..bf9328d 100644 --- a/pkg/hook/proto/v1/hook_grpc.pb.go +++ b/pkg/hook/proto/v1/hook_grpc.pb.go @@ -15,8 +15,8 @@ // that plugin's own category service. A plugin with no `hook{}` blocks in // agent.hcl simply never has it called. // -// context-assemble is deliberately absent from this surface's HookPoint -// enum below — it stays on ContextService.Contribute +// context-assemble is deliberately absent from this surface's hook-point +// vocabulary (common.v1.HookPoint) — it stays on ContextService.Contribute // (context/protocol.md#contribute-the-context-assemble-rpc; architecture.md // §Hook dispatch semantics), which already carries the full accumulated // ContextSection chain and doesn't need a second, competing dispatch path. From ff8096b3dbb95a5cd92920d6f7b240cd79fd1d83 Mon Sep 17 00:00:00 2001 From: Steven Crothers Date: Fri, 24 Jul 2026 13:42:58 -0400 Subject: [PATCH 5/7] Extend category protocols: model, tool, context, memory, frontend model.v1: StreamCompletionRequest gains assembled_context (the kernel-assembled ContextSection chain), CallContext, and explicit cache breakpoints; GenerationParams (temperature, stop_sequences, tool_choice); input-size-dimensioned PricingTier; Usage.reasoning_tokens; StopReason REFUSAL/STOP_SEQUENCE; CountTokens model_id. content.v1: DocumentBlock, Message.id and model attribution. tool.v1: ToolCall.call_context (session/turn/cwd), Preview RPC feeding the plan gate, ToolSchema default_timeout and idempotent hints. context.v1/memory.v1: turn_id standardization (ULID string), compactor pressure stats, ListRecords/GetRecord, MemoryRecord provenance and relevance_score, INVALID_SCOPE. frontend.v1: connection-scoped multiplexed Attach with full session lifecycle (create/attach/resume/detach/list; deletion deliberately withheld from all plugins), supersedes-replay backfill, aggregate slash command registry, usage and own-status events, PlanDecisionScope, content-block UserMessage. widget.v1: WidgetError. plan.v1: PlanItem kind/risk/description/preview snapshots. session.v1: SessionInfo. Every category gains Describe (ProducerRef identity) and supported_hook_points (common.v1.HookPoint). Specs updated fix-forward; stubs regenerated. --- .../agent/content/v1/content.proto | 51 + .../agent/context/v1/context.proto | 59 +- .../agent/frontend/v1/frontend.proto | 384 ++- .../agent/memory/v1/memory.proto | 154 +- .../agent/model/v1/model.proto | 244 +- api/pluggableharness/agent/plan/v1/plan.proto | 32 + .../agent/render/v1/render.proto | 6 + .../agent/session/v1/session.proto | 40 + api/pluggableharness/agent/tool/v1/tool.proto | 87 + .../agent/widget/v1/widget.proto | 58 + .../agent-loop/plan-apply-gate.md | 26 + docs/specifications/context/README.md | 4 +- docs/specifications/context/conformance.md | 3 + docs/specifications/context/data-types.md | 16 +- docs/specifications/context/examples.md | 9 +- docs/specifications/context/protocol.md | 16 +- docs/specifications/frontend/README.md | 21 +- docs/specifications/frontend/conformance.md | 28 +- docs/specifications/frontend/examples.md | 46 +- .../frontend/frontend-protocol.md | 130 +- docs/specifications/frontend/render-tree.md | 13 +- .../frontend/widget-protocol.md | 32 +- docs/specifications/memory/README.md | 4 +- docs/specifications/memory/conformance.md | 14 +- docs/specifications/memory/data-types.md | 60 +- docs/specifications/memory/examples.md | 30 +- docs/specifications/memory/protocol.md | 24 +- docs/specifications/model/README.md | 6 +- docs/specifications/model/conformance.md | 14 +- docs/specifications/model/data-types.md | 131 +- docs/specifications/model/examples.md | 41 +- docs/specifications/model/protocol.md | 33 +- docs/specifications/state-backend.md | 6 +- docs/specifications/tool/README.md | 4 +- docs/specifications/tool/conformance.md | 14 +- docs/specifications/tool/data-types.md | 19 +- docs/specifications/tool/examples.md | 18 + docs/specifications/tool/protocol.md | 29 +- pkg/content/proto/v1/content.pb.go | 199 +- pkg/context/proto/v1/context.pb.go | 287 ++- pkg/context/proto/v1/context_grpc.pb.go | 54 + pkg/frontend/proto/v1/frontend.pb.go | 2212 ++++++++++++++--- pkg/frontend/proto/v1/frontend_grpc.pb.go | 128 +- pkg/memory/proto/v1/memory.pb.go | 848 +++++-- pkg/memory/proto/v1/memory_grpc.pb.go | 150 ++ pkg/model/proto/v1/model.pb.go | 1204 +++++++-- pkg/model/proto/v1/model_grpc.pb.go | 58 + pkg/plan/proto/v1/plan.pb.go | 94 +- pkg/render/proto/v1/render.pb.go | 20 +- pkg/session/proto/v1/session.pb.go | 159 +- pkg/tool/proto/v1/tool.pb.go | 460 +++- pkg/tool/proto/v1/tool_grpc.pb.go | 96 + pkg/widget/proto/v1/widget.pb.go | 386 ++- pkg/widget/proto/v1/widget_grpc.pb.go | 54 + 54 files changed, 7132 insertions(+), 1183 deletions(-) diff --git a/api/pluggableharness/agent/content/v1/content.proto b/api/pluggableharness/agent/content/v1/content.proto index f5c0035..14f20fd 100644 --- a/api/pluggableharness/agent/content/v1/content.proto +++ b/api/pluggableharness/agent/content/v1/content.proto @@ -48,6 +48,37 @@ message Message { // The message's content, in emission order. A single message MAY carry // multiple blocks (e.g. an assistant turn with both text and a tool_use). repeated ContentBlock content = 2; + + // Kernel-assigned ULID, stable across replay. This is the correlation + // anchor for deltas and forking (e.g. a frontend edit-and-resubmit that + // forks history at this message) — assigned once when the message is + // persisted and never reassigned, even when the same conversation is + // replayed against a newer plugin version. MUST be set by the kernel + // before persisting; a plugin never generates this value itself, per + // .claude/rules/determinism.md's ordering-authority rule for kernel- + // assigned identifiers. + string id = 3; + + // The id of the model that produced this message, when role == + // ROLE_ASSISTANT. A plain string (the target model's ModelSpec.id), not + // a model.v1.ModelRef or model.v1.ModelTarget — content.v1 MUST NOT + // import model.v1, since model.v1 already imports content.v1 (for + // Message/ContentBlock/ContextSection) and a reverse import would be a + // cyclic dependency buf rejects at build time. Omitted for a + // ROLE_USER message, or when the producing model is otherwise unknown. + optional string produced_by_model_id = 4; + + // The declared name of the model provider plugin that produced this + // message (common.v1.ProducerRef.name / ProviderRef.name's scope), when + // role == ROLE_ASSISTANT. Same plain-string rationale as + // produced_by_model_id above. Because the kernel's routing/fallback + // chain (model/protocol.md#generation-parameter-validation-and- + // capability-aware-routing) may serve adjacent turns in the same + // session from different providers or different models, two + // consecutive ROLE_ASSISTANT messages in one conversation MAY carry + // different produced_by_model_id/produced_by_provider values — this is + // expected, not an anomaly, and MUST be preserved verbatim on replay. + optional string produced_by_provider = 5; } // ContentBlock is one block within a Message. Exactly one variant is set. @@ -64,6 +95,7 @@ message ContentBlock { ImageBlock image = 4; ThinkingBlock thinking = 5; RedactedThinkingBlock redacted_thinking = 6; + DocumentBlock document = 7; } } @@ -149,6 +181,25 @@ message RedactedThinkingBlock { bytes data = 1; } +// DocumentBlock is inline non-image document content (e.g. a PDF) — the +// document-attachment analog of ImageBlock. Requires the target model's +// ModelSpec.supports_documents (model/data-types.md#modelspec); the +// kernel MUST reject a DocumentBlock sent to a model where that flag is +// false, with invalid_request, mirroring ImageBlock's supports_vision +// rule (model/data-types.md#canonical-message--content-block-schema). +message DocumentBlock { + // Raw document bytes. + bytes data = 1; + + // The document's MIME type, e.g. "application/pdf". + string media_type = 2; + + // The document's original filename, when known — several vendors + // surface this to the model as a citation/reference label. MAY be + // omitted. + optional string filename = 3; +} + // Stability hints whether a ContextSection's content changes turn to turn, // used both as a context provider's ContextCapabilities-level declaration // (pluggableharness.agent.context.v1.ContextCapabilities.stability) and diff --git a/api/pluggableharness/agent/context/v1/context.proto b/api/pluggableharness/agent/context/v1/context.proto index a829f44..37126fd 100644 --- a/api/pluggableharness/agent/context/v1/context.proto +++ b/api/pluggableharness/agent/context/v1/context.proto @@ -15,6 +15,7 @@ syntax = "proto3"; package pluggableharness.agent.context.v1; import "google/protobuf/struct.proto"; +import "pluggableharness/agent/common/v1/common.proto"; import "pluggableharness/agent/config/v1/config.proto"; import "pluggableharness/agent/content/v1/content.proto"; import "pluggableharness/agent/model/v1/model.proto"; @@ -66,6 +67,16 @@ service ContextService { // be implemented; if not, the kernel falls back to its generic default // rendering. rpc Render(RenderRequest) returns (RenderResponse); + + // Describe reports this plugin build's own identity — {name, version, + // source, category, protocol_version} via ProducerRef — independent of + // any lock-file entry. This is the mechanism a dev_overrides-resolved + // binary (which has no provider {} lock entry to read identity from) + // uses to self-report at connection time; see + // docs/specifications/configuration/lock-file.md's dev_overrides note, + // which is the canonical explanation for this RPC across every plugin + // category that gains it in this protocol revision. + rpc Describe(DescribeRequest) returns (DescribeResponse); } // GetCapabilitiesRequest carries no fields — GetCapabilities takes no @@ -114,6 +125,16 @@ message ContextCapabilities { // This provider's agent.hcl config schema, advertised so the kernel knows // what fields Configure accepts. configuration.md §4. pluggableharness.agent.config.v1.ConfigSchema config_schema = 5; + + // Which hook points (agent-loop/hook-dispatch.md) this provider declares + // HookSubscriberService.DispatchHook subscriptions for, advertised + // up front alongside its other static properties. MAY be empty — a + // context provider with no hook{} blocks in agent.hcl never has + // DispatchHook called on it. The enum itself lives in common.v1, not + // hook.v1 — hook.v1 imports this package's model/tool/plan dependencies, + // so a category capability message importing hook.v1 directly would + // cycle back through it; common.v1 is the shared leaf package instead. + repeated pluggableharness.agent.common.v1.HookPoint supported_hook_points = 6; } // ConfigureResponse is empty on success. A Configure failure (e.g. a @@ -132,8 +153,11 @@ message ContextRequest { // session. Empty for a top-level session. string parent_session_id = 2; - // Which turn of the session this firing is for. - int64 turn_number = 3; + // Which turn of the session this firing is for. A ULID, standardized + // across the whole protocol (matches plan.v1's turn_id field). Same field + // number as the retired int64 turn-number predecessor field; the rename + // is a sanctioned pre-release wire change, not a v2 bump. + string turn_id = 3; // The kernel's computed token allocation for this provider on this call. // MUST be set. A returned section MUST NOT exceed this. context.md §4, @@ -164,6 +188,17 @@ message ContextRequest { // is indistinguishable from (and semantically equivalent to) "not // provided". context.md §5.1. repeated pluggableharness.agent.content.v1.Message conversation_history = 9; + + // The current conversation-history token total, kernel-computed. One of + // the two signals a compactor provider needs to decide WHEN to compact + // without re-counting history itself — see data-types.md's compactor + // workflow discussion in the orbit of §5.1. + int64 history_tokens = 10; + + // The total assembled context size of the previous turn (all providers' + // sections combined), kernel-computed. The second compact-timing signal, + // alongside history_tokens above. + int64 assembled_tokens_last_turn = 11; } // ContextContribution is Contribute's response: the full, possibly-modified @@ -233,6 +268,14 @@ message RenderRequest { // other plugin; only the producing provider's own Render implementation // understands its shape. bytes payload = 1; + + // The schema version this payload was emitted against, so a Render + // implementation can detect drift between the version it was built + // against and the version live in a running session. See + // ../frontend/render-tree.md#schema-versioning for the canonical + // definition of this field's semantics (owned by the frontend/widget + // workstream; this field just carries the same value). + string schema_version = 2; } // RenderResponse wraps the rendered output of the optional Render RPC. @@ -241,3 +284,15 @@ message RenderResponse { // (frontend.md §1). context.md §9. pluggableharness.agent.render.v1.RenderTree tree = 1; } + +// DescribeRequest carries no fields — Describe takes no request-scoped +// parameters. +message DescribeRequest {} + +// DescribeResponse reports this plugin build's own identity. See the +// Describe RPC comment on ContextService above. +message DescribeResponse { + // This plugin build's identity: name, version, source, category, + // protocol_version. + pluggableharness.agent.common.v1.ProducerRef producer = 1; +} diff --git a/api/pluggableharness/agent/frontend/v1/frontend.proto b/api/pluggableharness/agent/frontend/v1/frontend.proto index 7a5bf8d..53619fd 100644 --- a/api/pluggableharness/agent/frontend/v1/frontend.proto +++ b/api/pluggableharness/agent/frontend/v1/frontend.proto @@ -6,7 +6,10 @@ syntax = "proto3"; package pluggableharness.agent.frontend.v1; import "google/protobuf/struct.proto"; +import "pluggableharness/agent/common/v1/common.proto"; import "pluggableharness/agent/config/v1/config.proto"; +import "pluggableharness/agent/content/v1/content.proto"; +import "pluggableharness/agent/model/v1/model.proto"; import "pluggableharness/agent/plan/v1/plan.proto"; import "pluggableharness/agent/render/v1/render.proto"; import "pluggableharness/agent/session/v1/session.proto"; @@ -26,21 +29,30 @@ service FrontendService { // Unary. frontend.md §3.1. rpc Configure(ConfigureRequest) returns (ConfigureResponse); - // Attach opens the session-scoped event channel between the kernel and - // this frontend: ServerEvents flow from kernel to frontend, ClientEvents - // flow from frontend to kernel, both directions live for the duration of - // the stream. Bidirectional streaming — frontend.md §3.1, and (along with - // the kernel callback channel) one of only two genuinely bidirectional - // RPCs in this protocol series (see .claude/rules/grpc.md). + // Attach opens ONE multiplexed, connection-scoped bidirectional event + // channel between the kernel and this frontend connection — not a + // per-session stream. A frontend subscribes individual sessions onto + // this one stream via the session-control ClientEvent variants + // (create_session/attach_session/resume_session/detach_session), and + // unsubscribes the same way; connection-level operations + // (list_sessions, the aggregate slash-command registry) have a natural + // home here precisely because the stream isn't tied to one session. + // ServerEvents flow from kernel to frontend, ClientEvents flow from + // frontend to kernel, both directions live for the duration of the + // stream. Bidirectional streaming — frontend.md §"Transport", and + // (along with the kernel callback channel) one of only two genuinely + // bidirectional RPCs in this protocol series (see .claude/rules/grpc.md). // - // Multiple frontends MAY Attach to one session concurrently - // (frontend.md §3.3): every ServerEvent broadcasts identically to every - // attached frontend, with no partitioning and no "primary" frontend. - // ClientEvents are processed in kernel arrival order; for - // ClientEvent.plan_decision and ClientEvent.interactive_response - // specifically, which name a pending item by id, the kernel applies - // first-response-wins arbitration and MUST reject any later response for - // an already-resolved item with a distinct error back to its sender. + // Multiple frontends MAY subscribe to the same session concurrently on + // their own Attach streams (frontend.md §"Session scope"): every + // ServerEvent for a given session broadcasts identically to every + // frontend subscribed to that session, with no partitioning and no + // "primary" frontend. ClientEvents are processed in kernel arrival + // order; for ClientEvent.plan_decision and + // ClientEvent.interactive_response specifically, which name a pending + // item by id within a session, the kernel applies first-response-wins + // arbitration per session and MUST reject any later response for an + // already-resolved item with a distinct error back to its sender. // // buf:lint:ignore RPC_REQUEST_STANDARD_NAME // buf:lint:ignore RPC_RESPONSE_STANDARD_NAME @@ -50,6 +62,27 @@ service FrontendService { // uniqueness violation); renaming to Attach*Request/Response would only // satisfy a style convention while discarding real spec traceability. rpc Attach(stream ClientEvent) returns (stream ServerEvent); + + // Describe reports this plugin build's own identity — {name, version, + // source, category, protocol_version} — directly from the running + // process, rather than the kernel inferring it from a lock-file row. + // Every one of the six category protocols gains this identical RPC in + // this protocol revision; it exists specifically for a + // `dev_overrides`-resolved binary (configuration/lock-file.md's + // "dev_overrides and identity without a lock entry"), which has no + // provider {} lock-file entry to read identity from at all. + rpc Describe(DescribeRequest) returns (DescribeResponse); +} + +// DescribeRequest carries no fields — Describe takes no parameters. +message DescribeRequest {} + +// DescribeResponse reports this plugin build's own identity, obtained +// directly from the running process rather than a lock-file row — +// configuration/lock-file.md's "dev_overrides and identity without a lock +// entry". +message DescribeResponse { + pluggableharness.agent.common.v1.ProducerRef producer = 1; } // GetCapabilitiesRequest carries no fields; capability discovery is not @@ -69,6 +102,17 @@ message FrontendCapabilities { repeated pluggableharness.agent.slashcommand.v1.SlashCommandSpec slash_commands = 1; // This provider's `agent.hcl` configuration schema (configuration.md §4). pluggableharness.agent.config.v1.ConfigSchema config_schema = 2; + // Regions this frontend proactively declares it can render into. A + // complement to, not a replacement for, the reactive + // FRONTEND_ERROR_CATEGORY_REGION_UNSUPPORTED error a placement the + // frontend can't honor still produces — this lets a producer route + // content preferentially without waiting to find out the hard way. + repeated pluggableharness.agent.render.v1.Region supported_regions = 3; + // Hook points this frontend can subscribe to (agent-loop/hook-dispatch.md), + // so a mis-declared agent.hcl hook{} block naming an unsupported point + // can be rejected at config-load time rather than failing at first + // dispatch. + repeated pluggableharness.agent.common.v1.HookPoint supported_hook_points = 4; } // ConfigureRequest carries this provider's `agent.hcl` configuration as a @@ -85,8 +129,35 @@ message ConfigureRequest { message ConfigureResponse {} // ServerEvent is one message the kernel sends to an attached frontend over -// Attach, described in frontend.md §3.2. Exactly one variant is set. +// the single multiplexed Attach stream, described in frontend.md §3.2. +// Exactly one variant is set. message ServerEvent { + // The session this event is scoped to. Set for every session-scoped + // variant (stream_delta .. session_status_update); empty for the one + // connection-level variant, session_list. + string session_id = 100; + + // Correlates a response to the ClientEvent control message that + // triggered it, echoing that message's own request_id + // (hello/create_session/attach_session/resume_session/detach_session/ + // list_sessions all carry one). Set on session_created, session_attached, + // backfill_complete, session_detached, session_list, and on error when + // it answers one of those control requests. Unset for ordinary live + // session events that were not triggered by a specific client request + // (stream_delta, render, plan_ready, ...). + optional string request_id = 101; + + // Field 13 is deliberately withheld, not merely unused: it would have + // been session_deleted, for a SessionDeleted variant answering a + // DeleteSession control event. Sessions are protected from every + // plugin, frontends included — pruning is a kernel CLI + operator-at- + // keyboard action only (frontend.md §"No session deletion"); no + // frontend-triggered deletion mechanism exists in this protocol. The + // number stays reserved, not reused, so it can never be accidentally + // repurposed for something unrelated if deletion is ever reconsidered. + reserved 13; + reserved "session_deleted"; + oneof event { StreamDelta stream_delta = 1; Render render = 2; @@ -95,6 +166,36 @@ message ServerEvent { InteractiveRequest interactive_request = 5; SessionTreeUpdate session_tree_update = 6; Error error = 7; + // Acknowledges a ClientEvent.CreateSession, carrying the new + // session's info. The kernel auto-attaches the creating stream to + // this session (frontend.md §"Session lifecycle"). + SessionCreated session_created = 8; + // Acknowledges a ClientEvent.AttachSession or ResumeSession, carrying + // the session's current info. Opens the backfill batch: the + // replayed `render` events that follow, bracketed by the eventual + // backfill_complete (frontend.md §"Backfill"). + SessionAttached session_attached = 9; + // The done-marker closing a backfill batch opened by session_attached + // above. Live events with sequence > last_sequence follow this. + // Unicast to the attaching stream only, never broadcast. + BackfillComplete backfill_complete = 10; + // Acknowledges a ClientEvent.DetachSession. + SessionDetached session_detached = 11; + // Answers a ClientEvent.ListSessions. Connection-scoped — session_id + // above is empty for this variant. + SessionList session_list = 12; + // The profile-scoped aggregate slash-command registry across every + // provider loaded for this session, sent on session_attached and + // again whenever the registry changes. + SlashCommandRegistry slash_command_registry = 14; + // Per-turn token/cost accounting and context-budget pressure. + UsageUpdate usage_update = 15; + // This session's OWN lifecycle status. Deliberately distinct from + // session_tree_update above, which reports a CHILD session's status + // — this variant is the attached session's own transition (e.g. + // RUNNING -> COMPLETED, or a bound-exhausted re-open per + // frontend.md §"Resume and re-open semantics"). + SessionStatusUpdate session_status_update = 16; } // StreamDelta is the fast path for incremental text display, skipping a @@ -145,7 +246,9 @@ message ServerEvent { // SessionTreeUpdate reports a change in a nested sub-session's lifecycle // (e.g. a RunSession-spawned child), so a frontend can keep a - // SubSessionNode's displayed status current. + // SubSessionNode's displayed status current. Reports a CHILD session's + // status — for the attached session's OWN status, see + // SessionStatusUpdate below, a deliberately distinct variant. message SessionTreeUpdate { // The parent session's id. string parent_session_id = 1; @@ -160,6 +263,70 @@ message ServerEvent { // The error's category and message. FrontendError error = 1; } + + // SessionCreated acknowledges a successful ClientEvent.CreateSession. + message SessionCreated { + // The newly created session's info. + pluggableharness.agent.session.v1.SessionInfo info = 1; + } + + // SessionAttached acknowledges a successful ClientEvent.AttachSession or + // ResumeSession, and opens that session's backfill batch. + message SessionAttached { + // The attached session's current info. + pluggableharness.agent.session.v1.SessionInfo info = 1; + } + + // BackfillComplete is the done-marker closing a backfill batch, per + // frontend.md §"Backfill". Unicast to the attaching stream only. + message BackfillComplete { + // The persisted sequence number of the last event replayed in this + // batch. Live events with sequence > last_sequence follow. + int64 last_sequence = 1; + } + + // SessionDetached acknowledges a successful ClientEvent.DetachSession. + message SessionDetached {} + + // SessionList answers a ClientEvent.ListSessions. + message SessionList { + // The matching sessions, most-recently-started first. + repeated pluggableharness.agent.session.v1.SessionInfo sessions = 1; + } + + // SlashCommandRegistry is the profile-scoped aggregate of every loaded + // provider's declared slash commands for this session, per + // frontend.md §"Slash commands". + message SlashCommandRegistry { + // Every registered command, name-collision-checked at config-load + // time (frontend.md §"Slash commands"). + repeated pluggableharness.agent.slashcommand.v1.SlashCommandSpec commands = 1; + } + + // UsageUpdate carries one turn's token/cost accounting and the + // session's running totals, for a context-budget indicator or similar. + message UsageUpdate { + // This turn's token accounting. + pluggableharness.agent.model.v1.Usage turn = 1; + // The session's running total spend in USD, mirroring + // session.v1.SessionInfo.cost_usd. + double cumulative_cost_usd = 2; + // The session's running total token count against `effective_ceiling` + // below — the pair a context-budget indicator divides to get + // pressure (e.g. "51,204 / 200,000"). + int64 used_tokens = 3; + // The usable context budget this session's turns are measured + // against (model.v1.ModelTarget.effective_ceiling). + int64 effective_ceiling = 4; + } + + // SessionStatusUpdate reports the attached session's OWN lifecycle + // status transition. Distinct from SessionTreeUpdate above, which + // reports a CHILD session's status. + message SessionStatusUpdate { + // The session's new status. + pluggableharness.agent.session.v1.SessionStatus status = 1; + } } // ClientDecision is the user's resolution of a pending PermissionRequest, @@ -175,9 +342,49 @@ enum ClientDecision { CLIENT_DECISION_DENY = 2; } -// ClientEvent is one message a frontend sends to the kernel over Attach, -// described in frontend.md §3.2. Exactly one variant is set. +// PlanDecisionScope is how durably a ClientEvent.PlanDecision applies, +// beyond just the one PlanItem it names. Orthogonal to ClientDecision: +// decision says allow/deny, scope says how long that verdict is +// remembered. agent-loop/plan-apply-gate.md's "PlanDecisionScope +// semantics" documents evaluation order and the ALWAYS persistence +// obligation. +enum PlanDecisionScope { + // Zero value. Never valid for a real decision; its presence on the wire + // means a caller forgot to set the field. + PLAN_DECISION_SCOPE_UNSPECIFIED = 0; + // Applies to this PlanItem only. The default a frontend SHOULD send + // when the user hasn't explicitly asked for a broader scope. + PLAN_DECISION_SCOPE_ONCE = 1; + // Applies to the rest of this session, for calls matching the same + // provider/tool_name (and, where policy's match schema supports it, + // narrower criteria) — an in-memory, session-lifetime rule, not + // written to agent.hcl or any persisted policy store. + PLAN_DECISION_SCOPE_SESSION = 2; + // The kernel persists this as policy, applying beyond this session to + // future sessions under the same profile. Requires kernel-side policy + // persistence (agent-loop/plan-apply-gate.md#plandecisionscope-semantics) + // — a frontend MUST NOT assume ALWAYS is honored merely because it was + // sent; a kernel that cannot persist policy MUST reject it as a + // distinct error rather than silently downgrading to SESSION or ONCE. + PLAN_DECISION_SCOPE_ALWAYS = 3; +} + +// ClientEvent is one message a frontend sends to the kernel over the +// single multiplexed Attach stream, described in frontend.md §3.2. +// Exactly one variant is set. message ClientEvent { + // The session this event is scoped to. REQUIRED for every + // session-scoped variant (user_message, slash_command, plan_decision, + // interactive_response, action_trigger, interrupt) — the kernel MUST + // reject one of these arriving with an empty session_id as + // FRONTEND_ERROR_CATEGORY_INVALID_CLIENT_EVENT. Empty for the + // connection-level control variants (hello, create_session, + // attach_session, resume_session, detach_session, list_sessions), which + // either don't yet have a bound session or operate across sessions — + // attach_session/resume_session instead name the target session inside + // their own nested message. + string session_id = 100; + oneof event { UserMessage user_message = 1; SlashCommand slash_command = 2; @@ -185,12 +392,41 @@ message ClientEvent { InteractiveResponse interactive_response = 4; ActionTrigger action_trigger = 5; Interrupt interrupt = 6; + // MAY be sent as the first message on a newly opened Attach stream; + // asserts the protocol version only. Not required to bind any + // session — that happens via the variants below. + Hello hello = 7; + // Creates a new session and auto-attaches this stream to it. + CreateSession create_session = 8; + // Subscribes an existing (possibly live) session onto this stream, + // triggering a backfill replay (frontend.md §"Backfill"). + AttachSession attach_session = 9; + // Attaches a historical session for continuation or replay, per + // frontend.md §"Resume and re-open semantics" — a COMPLETED or + // CANCELLED session MAY be re-opened to RUNNING for new turns; a + // bound-exhausted (error_max_*) or FAILED session attaches + // replay-only and rejects any subsequent user_message for it. + ResumeSession resume_session = 10; + // Unsubscribes a session from this stream without affecting the + // session itself. + DetachSession detach_session = 11; + // Requests the connection-scoped session summary list. + ListSessions list_sessions = 12; } // UserMessage is ordinary chat input from the user. message UserMessage { - // The message text. - string text = 1; + // Field 1 was a bare `string text`, superseded by `content` below — + // block-structured input (image paste/attachments) had no entry + // point under a plain string. A frontend sending plain typed text + // sends a single TextBlock; `content` MAY carry more (e.g. a + // TextBlock plus an ImageBlock for a pasted screenshot, gated by the + // target model's supports_vision same as any other ImageBlock). + reserved 1; + reserved "text"; + // The message content, in emission order. MUST contain at least one + // block. + repeated pluggableharness.agent.content.v1.ContentBlock content = 2; } // SlashCommand is a dispatched slash command invocation (frontend.md §5). @@ -213,6 +449,9 @@ message ClientEvent { // input_schema (tool.md §6); an invalid correction is rejected as a // distinct error, not silently coerced. optional google.protobuf.Struct corrected_input = 3; + // How durably this decision applies beyond this one item — see + // PlanDecisionScope. + PlanDecisionScope scope = 4; } // InteractiveResponse resolves a pending ServerEvent.InteractiveRequest. @@ -238,11 +477,94 @@ message ClientEvent { // The arguments to invoke it with, echoed unchanged from the // originating ActionNode.args. google.protobuf.Struct args = 3; + // The declared name of the tool provider plugin `tool_name` belongs + // to, echoed unchanged from the originating ActionNode.provider + // (render.proto) — tool_name is only unique per provider. + string provider = 4; } // Interrupt carries no fields; it signals that the user wants to // interrupt the current turn. message Interrupt {} + + // Hello MAY be sent as the first ClientEvent on a newly opened Attach + // stream, to assert the protocol version. Binding a session happens via + // the session-control variants, not via Hello. + message Hello { + // The protocol version this frontend was built against. + uint32 protocol_version = 1; + } + + // CreateSession creates a new session and auto-attaches this stream to + // it, per frontend.md §"Session lifecycle". Answered by + // ServerEvent.SessionCreated. + message CreateSession { + // Client-generated, echoed back on ServerEvent.request_id. + string request_id = 1; + // The agent.hcl profile to create the session under. Absent means + // the kernel's configured default profile. + optional string profile = 2; + // An initial user message to seed the session with, submitted as the + // first turn once the session is created. Absent creates an empty + // session awaiting the first ordinary user_message. + optional string initial_prompt = 3; + // The session's working directory. Absent means the kernel's own + // working directory at creation time. + optional string working_directory = 4; + } + + // AttachSession subscribes an existing session (live or terminal) onto + // this stream, triggering a backfill replay. Answered by + // ServerEvent.SessionAttached, bracketing a replay batch closed by + // ServerEvent.BackfillComplete. + message AttachSession { + // Client-generated, echoed back on ServerEvent.request_id. + string request_id = 1; + // The session to attach. + string session_id = 2; + } + + // ResumeSession attaches a historical (possibly terminal) session, per + // frontend.md §"Resume and re-open semantics". A COMPLETED or + // CANCELLED session MAY be re-opened to SESSION_STATUS_RUNNING for new + // turns; a bound-exhausted (error_max_*) or FAILED session attaches + // replay-only — the kernel MUST reject a subsequent user_message + // against it with FRONTEND_ERROR_CATEGORY_SESSION_REPLAY_ONLY. Answered + // identically to AttachSession: SessionAttached bracketing a backfill + // batch. + message ResumeSession { + // Client-generated, echoed back on ServerEvent.request_id. + string request_id = 1; + // The session to resume. + string session_id = 2; + } + + // DetachSession unsubscribes a session from this stream without + // affecting the session itself — other streams attached to the same + // session, and the session's own execution, are unaffected. Answered + // by ServerEvent.SessionDetached. + message DetachSession { + // Client-generated, echoed back on ServerEvent.request_id. + string request_id = 1; + // The session to detach. + string session_id = 2; + } + + // ListSessions requests the connection-scoped session summary list. + // Answered by ServerEvent.SessionList. + message ListSessions { + // Client-generated, echoed back on ServerEvent.request_id. + string request_id = 1; + // Restricts the result to sessions in this status. Absent means no + // status filter. + optional pluggableharness.agent.session.v1.SessionStatus status = 2; + // Restricts the result to children of this session. Absent means no + // parent filter. + optional string parent_session_id = 3; + // True: only root sessions (no parent_session_id). False: all + // sessions matching the other filters, at any depth. + bool roots_only = 4; + } } // FrontendErrorCategory classifies a FrontendError, per the error taxonomy @@ -262,6 +584,28 @@ enum FrontendErrorCategory { FRONTEND_ERROR_CATEGORY_REGION_UNSUPPORTED = 3; // An error that does not fit any other category. FRONTEND_ERROR_CATEGORY_UNKNOWN = 4; + // AttachSession, ResumeSession, DetachSession, or ListSessions' + // parent_session_id filter named a session_id the kernel has no record + // of. + FRONTEND_ERROR_CATEGORY_SESSION_NOT_FOUND = 5; + // CreateSession failed — an invalid profile, or an unusable + // working_directory. + FRONTEND_ERROR_CATEGORY_SESSION_CREATE_FAILED = 6; + // A session-mutating control event targeted a session currently + // SESSION_STATUS_RUNNING in a way that conflicts with that (reserved + // for future session-mutating control events; no current variant in + // this protocol revision triggers it, since DetachSession is always + // safe on a running session). + FRONTEND_ERROR_CATEGORY_SESSION_BUSY = 7; + // ResumeSession named a session file with a newer PRAGMA user_version + // than this kernel understands (state-backend.md §"Schema migration"). + FRONTEND_ERROR_CATEGORY_SCHEMA_TOO_NEW = 8; + // A user_message (or any other new-turn-inducing event) targeted a + // session attached replay-only per frontend.md §"Resume and re-open + // semantics" — a bound-exhausted (error_max_*) or FAILED session + // resumed via ResumeSession. Distinct from SESSION_BUSY: the session + // isn't running, it's terminal and specifically barred from new turns. + FRONTEND_ERROR_CATEGORY_SESSION_REPLAY_ONLY = 9; } // FrontendError is the structured error type for this category, per diff --git a/api/pluggableharness/agent/memory/v1/memory.proto b/api/pluggableharness/agent/memory/v1/memory.proto index 9901274..0bf5be2 100644 --- a/api/pluggableharness/agent/memory/v1/memory.proto +++ b/api/pluggableharness/agent/memory/v1/memory.proto @@ -12,6 +12,7 @@ package pluggableharness.agent.memory.v1; import "google/protobuf/struct.proto"; import "google/protobuf/timestamp.proto"; +import "pluggableharness/agent/common/v1/common.proto"; import "pluggableharness/agent/config/v1/config.proto"; import "pluggableharness/agent/content/v1/content.proto"; import "pluggableharness/agent/model/v1/model.proto"; @@ -65,6 +66,30 @@ service MemoryService { // review-inbox view distinct from ordinary recall), in place of the // kernel's generic fallback. MAY be implemented. Unary. memory.md §10. rpc Render(RenderRequest) returns (RenderResponse); + + // ListRecords is the enumeration/audit path: paginated browsing of this + // provider's records, filterable by type/scope/status. Unlike Recall, + // PENDING records ARE listable here without any include_pending-style + // gate — this is the review-inbox path (e.g. a ratification review UI), + // not the budget-constrained per-turn recall path. MUST be implemented; + // cheap for any real backend. memory.md §7. + rpc ListRecords(ListRecordsRequest) returns (ListRecordsResponse); + + // GetRecord fetches exactly one record by id. MUST fail with a + // structured MemoryError (category NOT_FOUND) if `id` doesn't match an + // existing record, rather than returning an empty result. MUST be + // implemented; cheap for any real backend. memory.md §7. + rpc GetRecord(GetRecordRequest) returns (GetRecordResponse); + + // Describe reports this plugin build's own identity — {name, version, + // source, category, protocol_version} via ProducerRef — independent of + // any lock-file entry. This is the mechanism a dev_overrides-resolved + // binary (which has no provider {} lock entry to read identity from) + // uses to self-report at connection time; see + // docs/specifications/configuration/lock-file.md's dev_overrides note, + // which is the canonical explanation for this RPC across every plugin + // category that gains it in this protocol revision. + rpc Describe(DescribeRequest) returns (DescribeResponse); } // GetCapabilitiesRequest carries no fields; GetCapabilities takes no @@ -155,6 +180,18 @@ message MemoryCapabilities { // This provider's agent.hcl config schema, per configuration.md §4. pluggableharness.agent.config.v1.ConfigSchema config_schema = 6; + + // Which hook points (agent-loop/hook-dispatch.md) this provider declares + // HookSubscriberService.DispatchHook subscriptions for. Note: this + // category's implicit post_model_response (observe, every turn) and + // session_end (fires once) write triggers (memory.md §Write triggers) + // now ride the hook.v1 HookSubscriberService.DispatchHook surface — see + // protocol.md's Write triggers section for the payload shapes. The + // HookPoint enum itself lives in common.v1, not hook.v1 — hook.v1 + // imports this package's model/tool/plan dependencies, so a category + // capability message importing hook.v1 directly would cycle back + // through it; common.v1 is the shared leaf package instead. + repeated pluggableharness.agent.common.v1.HookPoint supported_hook_points = 7; } // GetCapabilitiesResponse wraps this provider's capability advertisement. @@ -186,8 +223,12 @@ message RecallRequest { // The requesting session's id. string session_id = 1; - // The current turn number within that session. - int64 turn_number = 2; + // The current turn within that session. A ULID, standardized across the + // whole protocol (matches plan.v1's turn_id field and context.v1's + // ContextRequest.turn_id, same treatment). Same field number as the + // retired int64 turn-number predecessor field; the rename is a + // sanctioned pre-release wire change, not a v2 bump. + string turn_id = 2; // The token budget this Recall call MUST self-truncate its returned // records to. Competes for the same budget pool as context providers, @@ -266,6 +307,40 @@ message MemoryRecord { // When this record was last modified. google.protobuf.Timestamp updated_at = 10; + + // Where this record came from and who wrote it. Kernel-populated at + // Record time, immutable thereafter — never provider-supplied or + // provider-mutable. memory.md's README already frames provenance as + // first-class; this is where the record shape finally carries it. + Provenance provenance = 11; + + // This record's recall-time relevance, in [0, 1]. Set only on + // Recall/ListRecords responses — never persisted, never present on a + // Record/UpdateRecord request or response. Lets the kernel merge + // multiple memory providers' results under one shared budget + // (data-types.md#recallrequest--memoryrecord) using a comparable score + // rather than provider-opaque ordering alone. A provider setting this + // MUST normalize it to [0, 1]; scores from different providers are only + // meaningfully comparable if each normalizes to the same range. + optional double relevance_score = 12; +} + +// Provenance records where a MemoryRecord came from and who wrote it. +// Kernel-populated at Record time; immutable afterward, like the rest of a +// record's write-time metadata. +message Provenance { + // The session id that produced this record. + string source_session_id = 1; + + // The turn id (ULID, see ContextRequest.turn_id) within source_session_id + // that produced this record, when known. Absent for a record written + // outside normal turn flow (e.g. a backfill/import). + optional string source_turn_id = 2; + + // The producing plugin's declared name, or the reference tool path that + // wrote it (e.g. "memory.remember"). Kernel-populated from the calling + // context, not provider-supplied. + string recorded_by = 3; } // RecordRequest creates a new record. memory.md §7. @@ -407,6 +482,10 @@ enum MemoryErrorCategory { MEMORY_ERROR_CATEGORY_SOURCE_UNAVAILABLE = 5; // Any error not covered by a more specific category above. MEMORY_ERROR_CATEGORY_UNKNOWN = 6; + // Record specified a MemoryScope this provider doesn't support (absent + // from GetCapabilities.supported_scopes) — the scope-taxonomy mirror of + // MEMORY_ERROR_CATEGORY_INVALID_TYPE above. + MEMORY_ERROR_CATEGORY_INVALID_SCOPE = 7; } // MemoryError is the structured detail carried on a gRPC error status @@ -428,6 +507,14 @@ message RenderRequest { // Paint carve-out; this is the one field in this file that is // deliberately untyped bytes rather than a concrete message. bytes payload = 1; + + // The schema version this payload was emitted against, so a Render + // implementation can detect drift between the version it was built + // against and the version live in a running session. See + // ../frontend/render-tree.md#schema-versioning for the canonical + // definition of this field's semantics (owned by the frontend/widget + // workstream; this field just carries the same value). + string schema_version = 2; } // RenderResponse wraps this provider's rendered output. memory.md §10. @@ -435,3 +522,66 @@ message RenderResponse { // The rendered tree, per frontend.md §1. pluggableharness.agent.render.v1.RenderTree tree = 1; } + +// ListRecordsRequest is the enumeration/audit query: paginated browsing of +// this provider's records, filterable by type/scope/status. Unlike +// RecallRequest, there is no include_pending gate — this is the +// review-inbox path (e.g. a ratification review UI or generic record +// browsing), where PENDING records ARE listable, not the budget-constrained +// per-turn recall path. memory.md §7. +message ListRecordsRequest { + // Restricts results to these MemoryTypes. MAY be empty, meaning all + // types this provider supports. + repeated MemoryType type_filter = 1; + + // Restricts results to these MemoryScopes. MAY be empty, meaning all + // scopes this provider supports. + repeated MemoryScope scope_filter = 2; + + // Restricts results to this RecordStatus. Unset means both CANONICAL and + // PENDING records are eligible — this is the one path where a PENDING + // record is listable without any include_pending-style gate. + optional RecordStatus status_filter = 3; + + // Maximum number of records to return in one page. + int32 page_size = 4; + + // Opaque continuation token from a prior ListRecordsResponse. + // next_page_token. Empty on the first page. + string page_token = 5; +} + +// ListRecordsResponse carries one page of matching records. +message ListRecordsResponse { + // This page's records. + repeated MemoryRecord records = 1; + + // Opaque continuation token for the next page. Empty when this is the + // last page. + string next_page_token = 2; +} + +// GetRecordRequest identifies exactly one record to fetch by id. +message GetRecordRequest { + // The record's id. MUST match an existing record, or the call fails with + // a structured MemoryError (category NOT_FOUND). + string id = 1; +} + +// GetRecordResponse wraps the fetched record. +message GetRecordResponse { + // The fetched record. + MemoryRecord record = 1; +} + +// DescribeRequest carries no fields — Describe takes no request-scoped +// parameters. +message DescribeRequest {} + +// DescribeResponse reports this plugin build's own identity. See the +// Describe RPC comment on MemoryService above. +message DescribeResponse { + // This plugin build's identity: name, version, source, category, + // protocol_version. + pluggableharness.agent.common.v1.ProducerRef producer = 1; +} diff --git a/api/pluggableharness/agent/model/v1/model.proto b/api/pluggableharness/agent/model/v1/model.proto index f423319..e157465 100644 --- a/api/pluggableharness/agent/model/v1/model.proto +++ b/api/pluggableharness/agent/model/v1/model.proto @@ -14,6 +14,7 @@ package pluggableharness.agent.model.v1; import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; import "google/protobuf/timestamp.proto"; +import "pluggableharness/agent/common/v1/common.proto"; import "pluggableharness/agent/config/v1/config.proto"; import "pluggableharness/agent/content/v1/content.proto"; import "pluggableharness/agent/render/v1/render.proto"; @@ -77,6 +78,18 @@ service ModelService { // §7 notes most model-provider payloads (plain text, tool calls) render // fine under the kernel's generic fallback when this RPC is absent. rpc Render(RenderRequest) returns (RenderResponse); + + // Describe reports this plugin build's own identity — {name, version, + // source, category, protocol_version} — directly from the running + // process, rather than the kernel inferring it from a lock-file row. + // This matters specifically for a dev_overrides binary + // (configuration/settings-and-global.md#dev_overrides), which bypasses + // the registry/lock-file resolution path entirely and so has no + // provider "" { ... } lock entry to read identity from; see + // configuration/lock-file.md's dev_overrides note for the canonical + // explanation, shared verbatim across all six category protocols that + // gain this RPC in this same protocol revision. + rpc Describe(DescribeRequest) returns (DescribeResponse); } // GetCapabilitiesRequest is empty: model.md §2 defines GetCapabilities @@ -106,6 +119,20 @@ message Capabilities { // capabilities so the kernel knows what fields Configure expects, per // configuration.md §4. pluggableharness.agent.config.v1.ConfigSchema config_schema = 3; + + // Which hook points (agent-loop/hook-dispatch.md) this plugin can serve + // via HookSubscriberService.DispatchHook. The kernel MUST reject an + // agent.hcl hook{} block naming a point not present here, at + // config-load time. + // + // Typed as common.v1.HookPoint, not hook.v1.HookPoint: hook.proto + // imports model.proto (for ModelRef/Usage on its PreModelCall/ + // PostModelResponse hook payloads), so model.proto importing hook.proto + // for this field would be a cyclic file dependency — confirmed via + // `buf build`, which rejects it outright ("detected cyclic import"). + // HookPoint itself lives in common.v1 for exactly this reason (see + // common.proto), already imported here for CallContext/Describe. + repeated pluggableharness.agent.common.v1.HookPoint supported_hook_points = 4; } // ConfigureRequest wraps the provider's agent.hcl config block, already @@ -128,6 +155,18 @@ message ConfigureRequest { // message. message ConfigureResponse {} +// DescribeRequest is empty: Describe takes no request parameters, per +// configuration/lock-file.md's dev_overrides note. +message DescribeRequest {} + +// DescribeResponse reports this plugin build's own identity, per +// configuration/lock-file.md's dev_overrides note. +message DescribeResponse { + // This plugin build's identity, as it would otherwise appear in a + // lock-file provider "" { ... } entry. + pluggableharness.agent.common.v1.ProducerRef producer = 1; +} + // ModelSpec describes one model this provider can serve, per // model.md §2. Every field below is MUST unless its comment says // otherwise. @@ -174,6 +213,22 @@ message ModelSpec { // This model's pricing. MUST be present even for a free model (set // Pricing.free = true). Pricing pricing = 10; + + // Which GenerationParams.tool_choice.mode values this model accepts. + // SHOULD declare precisely which subset a vendor supports rather than + // collapsing to a bool, mirroring ThinkingSpec/CachingSpec's sum-type + // rationale above — vendors differ in which of AUTO/ANY/NONE/SPECIFIC + // they expose. Empty means this model does not support constraining + // tool choice at all (only free-form model-decides behavior); the + // kernel MUST NOT send a GenerationParams.tool_choice with a mode + // absent from this list. + repeated ToolChoiceMode supported_tool_choice_modes = 11; + + // Whether this model can accept document content blocks (content.v1 + // DocumentBlock, e.g. inline PDFs). Mirrors supports_vision's rule: + // the kernel MUST reject a DocumentBlock sent to a model where this is + // false, with invalid_request, rather than silently dropping it. + bool supports_documents = 12; } // ThinkingMode enumerates the shapes of extended-reasoning control found @@ -278,11 +333,15 @@ message CachingSpec { bool keepalive_supported = 3; } -// PricingTier is one time-bounded rate within a model's Pricing, per -// model.md §2. Exactly one tier MUST match at any given timestamp -// (effective_from <= ts < effective_until, an omitted bound unbounded on -// that side); the kernel MUST reject a Pricing value at capability-load -// time if its tiers overlap or leave a gap. +// PricingTier is one time-bounded, input-size-bounded rate within a +// model's Pricing, per model.md §2. Exactly one tier MUST match at any +// given (timestamp, input_token_count) pair — timestamp resolved against +// effective_from/effective_until (an omitted bound unbounded on that +// side) AND input_token_count resolved against input_tokens_from/ +// input_tokens_until (likewise unbounded when omitted) simultaneously; +// the kernel MUST reject a Pricing value at capability-load time if its +// tiers overlap or leave a gap across either dimension, exactly as it +// already does for the time dimension alone. message PricingTier { // The moment this tier becomes active. Omitted means "since this plugin // version was published". Refines model.md §2's "ISO 8601 @@ -317,6 +376,20 @@ message PricingTier { // A vendor's discounted batch/async output rate, paired with // batch_input_per_mtok. MAY be present. optional double batch_output_per_mtok = 8; + + // The smallest accumulated-input-token count this tier applies to, + // inclusive. Omitted means unbounded below (matches any input size down + // to zero). Real vendors price by input size as well as by time — e.g. + // a distinct, higher rate once a request's input exceeds 200k tokens — + // and this field lets a tier declare that dimension alongside the + // existing effective_from/effective_until time bounds. Refines + // model/data-types.md#pricing's tier-matching rule into two dimensions. + optional int64 input_tokens_from = 9; + + // The input-token count this tier stops applying to, exclusive. + // Omitted means unbounded above. Half-open with input_tokens_from, + // matching effective_from/effective_until's half-open convention. + optional int64 input_tokens_until = 10; } // Pricing describes one model's cost structure, per model.md §2. MUST @@ -356,6 +429,75 @@ message StreamCompletionRequest { // Generation-time overrides. Omitted means every param takes its // model-specific default. optional GenerationParams params = 4; + + // The kernel-assembled context chain — the accumulated output of every + // context provider's Contribute call plus memory recall + // (context/protocol.md#contribute-the-context-assemble-rpc), in chain + // order (the same order context.md's ContextRequest.prior_sections/ + // Contribute response chain uses: each provider appends after the + // sections it received). This is distinct from `messages` above: it is + // system-level/preamble content, never a conversational turn, which is + // why it's carried as its own field rather than folded into `messages` + // as a synthetic message — content.v1.Role deliberately has no SYSTEM + // value (content.proto's Role comment) precisely because this content + // is never a Message with a role. Each model-provider adapter maps + // this chain to its vendor's own system/preamble mechanism (a top-level + // `system` string, a leading system-role message, etc.) — how that + // mapping happens is adapter-internal and not part of this wire + // contract. + repeated pluggableharness.agent.content.v1.ContextSection assembled_context = 5; + + // Session/turn/working-directory attribution for this call. MUST be set + // by the kernel on every StreamCompletionRequest. This is what the + // plugin passes back on its own KernelCallbackService.Emit and Log + // calls (kernel-callbacks.md#emit) for correlation, without having to + // separately thread session_id/turn_id through adapter-internal call + // sites by hand. + pluggableharness.agent.common.v1.CallContext call_context = 6; + + // Cache breakpoints for this request, wire-level and request-scoped — + // NOT carried on the persisted content.v1.ContentBlock, since a + // breakpoint's placement is a per-request optimization decision, not a + // durable property of the conversation history itself. Meaningful only + // when the target model's CachingSpec.mode == + // CACHING_MODE_EXPLICIT_MARKERS; a model-provider adapter targeting a + // model whose CachingSpec.mode is CACHING_MODE_IMPLICIT_AUTOMATIC or + // CACHING_MODE_NONE MUST ignore this field rather than error on it. + // Placement is a kernel decision, not the plugin's: the kernel knows + // each assembled_context section's Stability (content.proto's Stability + // enum) and each message's position, so it places breakpoints at + // natural stable-prefix boundaries — see + // model/protocol.md#cache-breakpoint-placement-policy. + repeated CacheBreakpoint cache_breakpoints = 7; +} + +// CacheBreakpoint marks one position in a StreamCompletionRequest where +// the kernel wants the adapter to insert a vendor-native cache-control +// marker, per StreamCompletionRequest.cache_breakpoints above. +message CacheBreakpoint { + // The position this breakpoint marks. Exactly one variant is set. + oneof position { + // Immediately after the assembled_context chain — the kernel's most + // common choice, since assembled_context is usually the longest + // stable prefix (context/data-types.md#ordering--chaining orders + // STABILITY_STATIC sections before STABILITY_DYNAMIC ones). + AfterAssembledContext after_assembled_context = 1; + + // Immediately after the tools declaration list. + AfterTools after_tools = 2; + + // Immediately after the message at this zero-based index within + // `messages`. + int64 after_message_index = 3; + } + + // AfterAssembledContext is an empty marker message: its presence as the + // set oneof variant is the entire signal, no further data needed. + message AfterAssembledContext {} + + // AfterTools is an empty marker message: its presence as the set oneof + // variant is the entire signal, no further data needed. + message AfterTools {} } // ToolDeclaration is one tool the model may call on this turn, per @@ -389,6 +531,61 @@ message GenerationParams { // Per-request override of ModelSpec.max_output_tokens. Omitted means // use the model's default. optional int64 max_output_tokens = 3; + + // Sampling temperature. Omitted means use the model's default. Range + // and exact semantics are vendor-specific; the kernel does not clamp + // or reinterpret this value, it is passed through to the adapter as + // given. + optional double temperature = 4; + + // Sequences that, if generated, MUST cause the model to stop before + // producing them. Omitted/empty means no caller-supplied stop + // sequences. When a vendor honors one of these, the plugin MUST report + // it back via StreamEvent.Stop.matched_stop_sequence with StopReason + // STOP_REASON_STOP_SEQUENCE. + repeated string stop_sequences = 5; + + // Constrains whether/how the model must use a tool this turn. Omitted + // means the model decides freely (equivalent to + // TOOL_CHOICE_MODE_AUTO). Meaningful only when the target model's + // ModelSpec.supported_tool_choice_modes is non-empty; the kernel MUST + // NOT send a mode absent from that list, mirroring + // thinking_effort/thinking_budget_tokens' ThinkingSpec-validation rule + // above — an unsupported mode is a kernel-level reject-or-fallback, not + // something forwarded to the vendor. + optional ToolChoice tool_choice = 6; +} + +// ToolChoiceMode enumerates the tool-invocation constraint shapes found +// across researched vendors, per the same "declare precisely, don't +// collapse to a bool" reasoning as ThinkingMode/CachingMode above. +enum ToolChoiceMode { + // Zero value. Never valid on a real ToolChoice; its presence on the + // wire means a caller forgot to set the field. + TOOL_CHOICE_MODE_UNSPECIFIED = 0; + // The model decides freely whether and which tool to call. Equivalent + // to omitting GenerationParams.tool_choice entirely. + TOOL_CHOICE_MODE_AUTO = 1; + // The model MUST call some tool this turn, but may pick which one. + TOOL_CHOICE_MODE_ANY = 2; + // The model MUST NOT call any tool this turn, even if tools were + // declared. + TOOL_CHOICE_MODE_NONE = 3; + // The model MUST call the specific tool named in ToolChoice.tool_name. + TOOL_CHOICE_MODE_SPECIFIC = 4; +} + +// ToolChoice carries one request's tool-invocation constraint, per +// GenerationParams.tool_choice above. +message ToolChoice { + // Which constraint shape applies. MUST be set. + ToolChoiceMode mode = 1; + + // The tool the model MUST call. MUST be set, and MUST name a tool + // present in StreamCompletionRequest.tools, when mode == + // TOOL_CHOICE_MODE_SPECIFIC; meaningless and MUST be omitted for every + // other mode. + optional string tool_name = 2; } // StreamEvent is one message in the stream StreamCompletion returns, per @@ -471,6 +668,11 @@ message StreamEvent { message Stop { // Why the completion ended. StopReason reason = 1; + + // Which GenerationParams.stop_sequences entry was matched. Set iff + // reason == STOP_REASON_STOP_SEQUENCE; MUST be omitted for every + // other reason. + optional string matched_stop_sequence = 2; } // Error signals the completion failed. @@ -497,6 +699,15 @@ message Usage { // Tokens written to cache, if the model supports caching. Never also // counted in input_tokens. optional int64 cache_write_tokens = 4; + // Thinking/reasoning tokens, when the vendor reports them as a + // distinct count (ThinkingSpec.supported models only). Never also + // counted in output_tokens — a vendor that folds reasoning tokens into + // its reported output_tokens has no separate figure to report here, so + // this stays unset in that case rather than being derived/subtracted. + // Billed at the output rate (PricingTier.output_per_mtok) unless a + // future Pricing revision declares a distinct reasoning rate — there is + // none as of this revision. + optional int64 reasoning_tokens = 5; // Deliberately no cost field: the kernel computes and persists // cost_usd from these token counts plus the matching PricingTier, per // model.md §4.1 — the provider never computes cost itself. @@ -519,6 +730,16 @@ enum StopReason { // abort). MUST be treated by the plugin as normal control flow, never // as an error (model.md §1, .claude/rules/grpc.md). STOP_REASON_CANCELLED = 5; + // The model or vendor refused to continue generating — distinct from + // STOP_REASON_CONTENT_FILTERED, which is the vendor's automated content + // filter stopping generation; REFUSAL is the model itself declining + // (e.g. a safety-trained refusal message), a semantically different + // event even though both are policy-driven stops. + STOP_REASON_REFUSAL = 6; + // The model stopped because it generated one of + // GenerationParams.stop_sequences. Stop.matched_stop_sequence carries + // which one. + STOP_REASON_STOP_SEQUENCE = 7; } // CountTokensRequest is CountTokens' request: the raw text to count, per @@ -526,6 +747,11 @@ enum StopReason { message CountTokensRequest { // The text to count tokens for. string text = 1; + + // Selects which of this provider's ModelSpec.id to count against — a + // provider serving several models MAY have distinct tokenizers per + // model. MUST be set. + string model_id = 2; } // CountTokensResponse is CountTokens' response. @@ -540,6 +766,14 @@ message RenderRequest { // pipeline's deliberate carve-out from the strong-typing rule (see // .claude/rules/grpc.md), never interpreted by the kernel. bytes payload = 1; + + // The schema_version `payload` was emitted under, per + // ../frontend/render-tree.md#schema-versioning. MUST be set — lets this + // Render implementation interpret a payload emitted by an older plugin + // version consistently across a replayed session, the same + // "supersedes" reasoning architecture.md applies elsewhere to + // schema-drift-sensitive persisted data. + string schema_version = 2; } // RenderResponse wraps the resulting RenderTree, per model.md §7. diff --git a/api/pluggableharness/agent/plan/v1/plan.proto b/api/pluggableharness/agent/plan/v1/plan.proto index ec834b8..df3200a 100644 --- a/api/pluggableharness/agent/plan/v1/plan.proto +++ b/api/pluggableharness/agent/plan/v1/plan.proto @@ -10,6 +10,7 @@ syntax = "proto3"; package pluggableharness.agent.plan.v1; import "google/protobuf/struct.proto"; +import "pluggableharness/agent/render/v1/render.proto"; import "pluggableharness/agent/tool/v1/tool.proto"; option go_package = "github.com/pluggableharness/agent/pkg/plan/proto/v1;planv1"; @@ -68,6 +69,37 @@ message PlanItem { // The name of the policy rule or subscriber that produced `decision`, // for audit (state-backend.md §4.4 plan_items.decided_by). string decided_by = 7; + + // --- Snapshot fields (agent-loop/plan-apply-gate.md#plan-construction-and-policy-evaluation) --- + // + // The four fields below are captured from the originating tool + // operation's ToolSchema at plan-construction time, not looked up live + // at display or audit time. A later agent.hcl change to the provider's + // schema (a re-classified kind/risk, an edited description) MUST NOT + // retroactively alter a historical plan's audit record — the snapshot + // is what a frontend renders and what state-backend.md's plan_items + // table persists, independent of whatever the schema looks like now. + + // Snapshot of ToolSchema.kind (tool/data-types.md#toolschema) for the + // operation this item calls, at plan-construction time. + pluggableharness.agent.tool.v1.ToolKind kind = 8; + + // Snapshot of ToolSchema.risk, at plan-construction time. + pluggableharness.agent.tool.v1.RiskClass risk = 9; + + // Snapshot of ToolSchema.description, at plan-construction time. + string description = 10; + + // The tool provider's dry-run preview of this call's effect, when the + // provider implements ToolService.Preview (tool/protocol.md#preview). + // The kernel calls Preview at plan-construction time for + // TOOL_KIND_RESOURCE items whose provider implements it; absent when + // the provider has no Preview implementation, in which case a frontend + // falls back to rendering the raw `input` above. Pinned to + // render.v1.RenderTree — the exact type ToolService.Preview's own + // response carries, so this stored snapshot and the RPC's live output + // share one wire shape (agent-loop/plan-apply-gate.md#preview-flow). + optional pluggableharness.agent.render.v1.RenderTree preview = 11; } // Plan collects every policy-evaluated call identified during one turn. diff --git a/api/pluggableharness/agent/render/v1/render.proto b/api/pluggableharness/agent/render/v1/render.proto index 3a11b4d..4c58297 100644 --- a/api/pluggableharness/agent/render/v1/render.proto +++ b/api/pluggableharness/agent/render/v1/render.proto @@ -251,4 +251,10 @@ message ActionNode { // carve-out) — validated against the tool's input_schema on dispatch, // same as any other Invoke call. google.protobuf.Struct args = 4; + // The declared name of the tool provider plugin `tool_name` belongs to. + // tool_name is only unique per provider (matching plan.v1.PlanItem's own + // provider/tool_name pairing), so this disambiguates which provider's + // operation to invoke on activation. Echoed unchanged onto the resulting + // ClientEvent.ActionTrigger.provider (frontend.md §"Client events"). + string provider = 5; } diff --git a/api/pluggableharness/agent/session/v1/session.proto b/api/pluggableharness/agent/session/v1/session.proto index aa7792a..e6810d5 100644 --- a/api/pluggableharness/agent/session/v1/session.proto +++ b/api/pluggableharness/agent/session/v1/session.proto @@ -6,6 +6,8 @@ syntax = "proto3"; // session_tree_update ServerEvent. package pluggableharness.agent.session.v1; +import "google/protobuf/timestamp.proto"; + option go_package = "github.com/pluggableharness/agent/pkg/session/proto/v1;sessionv1"; // SessionStatus is a session's lifecycle state. RUNNING is the only value @@ -35,3 +37,41 @@ enum SessionStatus { // The session ended due to an unrecoverable error. SESSION_STATUS_FAILED = 7; } + +// SessionInfo is a session's shareable summary, mirroring +// state-backend.md's session_meta row plus a cheap cost_ledger SUM. Used +// by frontend.md's SessionCreated/SessionAttached/SessionList ServerEvent +// variants — the frontend protocol's read-only view of session lifecycle +// state, never mutated by a frontend directly. +message SessionInfo { + // The session's id. ULID, matches the session's sqlite filename stem + // (state-backend.md §"File layout"). + string session_id = 1; + + // The parent session's id, when this is a sub-agent session + // (agent-loop/subagents.md). Absent for a root session — mirrors + // session_meta.parent_session_id. + optional string parent_session_id = 2; + + // The agent.hcl profile this session was created under. + string profile = 3; + + // The session's current lifecycle status. + SessionStatus status = 4; + + // The session's depth in its ancestor chain — mirrors + // session_meta.depth (agent-loop/subagents.md#depth-limits). + int32 depth = 5; + + // When the session was created. Mirrors session_meta.started_at. + google.protobuf.Timestamp started_at = 6; + + // When the session reached a terminal status. Absent while RUNNING. + // Mirrors session_meta.ended_at. + optional google.protobuf.Timestamp ended_at = 7; + + // The session's running total spend — SUM(cost_usd) over + // state-backend.md's cost_ledger table for this session. Absent if no + // cost has been incurred yet, rather than a meaningless zero. + optional double cost_usd = 8; +} diff --git a/api/pluggableharness/agent/tool/v1/tool.proto b/api/pluggableharness/agent/tool/v1/tool.proto index 014e30b..936b024 100644 --- a/api/pluggableharness/agent/tool/v1/tool.proto +++ b/api/pluggableharness/agent/tool/v1/tool.proto @@ -6,7 +6,9 @@ syntax = "proto3"; // and similar operations. package pluggableharness.agent.tool.v1; +import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; +import "pluggableharness/agent/common/v1/common.proto"; import "pluggableharness/agent/config/v1/config.proto"; import "pluggableharness/agent/render/v1/render.proto"; import "pluggableharness/agent/schema/v1/schema.proto"; @@ -43,6 +45,20 @@ service ToolService { // per tool.md §7. MAY be implemented; if absent, the kernel falls back to // its generic default (pretty-printed JSON payload). rpc Render(RenderRequest) returns (RenderResponse); + + // Preview returns a dry-run, human-readable description of what Invoke + // would do for the given call, without performing it — per + // protocol.md#preview. MAY be implemented; a kernel MUST tolerate its + // absence and fall back to showing the call's raw arguments in the + // plan/apply gate's permission UI. + rpc Preview(PreviewRequest) returns (PreviewResponse); + + // Describe reports this plugin build's own identity — per + // protocol.md#describe and configuration/lock-file.md's dev_overrides + // note, this is how the kernel learns a dev_overrides-resolved plugin's + // {name, version, source, category, protocol_version} when there is no + // lock-file entry to read it from. + rpc Describe(DescribeRequest) returns (DescribeResponse); } // GetSchemaRequest carries no fields — GetSchema takes no parameters. @@ -62,6 +78,13 @@ message GetSchemaResponse { // This provider's agent.hcl config schema, per configuration.md §4 — // what fields Configure's request may be decoded from. pluggableharness.agent.config.v1.ConfigSchema config_schema = 3; + + // Which of the eight dispatchable hook points (hook/v1.HookPoint) this + // provider subscribes a HookSubscriberService to, per this provider's own + // agent.hcl hook{} blocks. Lets the kernel validate a hook{} declaration + // at config-load time instead of discovering an unsupported subscription + // only when that hook point first fires. + repeated pluggableharness.agent.common.v1.HookPoint supported_hook_points = 4; } // ConfigureRequest wraps this provider's already-decoded agent.hcl config. @@ -188,6 +211,19 @@ message ToolSchema { // MUST, except MUST NOT be meaningfully set for TOOL_KIND_INTERACTIVE. ConcurrencySpec concurrency = 8; + + // SHOULD — the deadline the kernel applies to Invoke for this operation + // absent an agent.hcl override. Absent means the kernel's global default + // applies instead (configuration/settings-and-global.md). + optional google.protobuf.Duration default_timeout = 9; + + // True iff re-running this operation with identical arguments cannot + // produce a different end state than running it once. Gates whether the + // kernel MAY auto-retry a retryable ToolError for a TOOL_KIND_RESOURCE + // operation — see conformance.md#error-taxonomy's retry interaction. + // TOOL_KIND_DATA_SOURCE operations are implicitly safe to retry + // regardless of this field. + bool idempotent = 10; } // InvokeRequest wraps the call to execute. A thin per-RPC envelope around @@ -219,6 +255,15 @@ message ToolCall { // MUST — already-parsed JSON conforming to that ToolSchema's // input_schema. google.protobuf.Struct arguments = 3; + + // MUST be set by the kernel. Carries the session_id/turn_id this call + // executes for — what the plugin echoes back on its own + // KernelCallbackService.Emit/Log calls for attribution — and the + // session's working_directory, which any process-backed operation + // (the reference catalog's exec.bash, read_file, and similar) MUST + // resolve relative-path arguments against. See + // pluggableharness.agent.common.v1.CallContext. + pluggableharness.agent.common.v1.CallContext call_context = 4; } // ToolEvent is one message in the stream Invoke returns, per tool.md §4. @@ -383,6 +428,12 @@ message ToolError { message RenderRequest { // The opaque emitted payload to render. bytes payload = 1; + + // The schema version the payload was emitted under, per + // ../frontend/render-tree.md#schema-versioning. Lets a Render + // implementation decode a payload emitted by an older build of this + // same plugin. + string schema_version = 2; } // RenderResponse wraps the rendered tree. A thin per-RPC envelope around @@ -392,3 +443,39 @@ message RenderResponse { // The rendered tree. pluggableharness.agent.render.v1.RenderTree tree = 1; } + +// PreviewRequest wraps the call to describe, per protocol.md#preview. +message PreviewRequest { + // The call Preview describes a dry run of. Same shape as an Invoke + // request; Preview MUST NOT execute it. + ToolCall call = 1; +} + +// PreviewResponse carries a dry-run, human-readable description of what +// Invoke(call) would do, per protocol.md#preview — e.g. an edit tool +// returns the diff it would apply. Rendered into the plan/apply gate's +// permission UI via PlanItem.preview (pluggableharness.agent.plan.v1, +// a sibling protocol revision) — that field and this response share the +// same pluggableharness.agent.render.v1.RenderTree type by design, so a +// Preview call's output and a plan item's stored preview are +// interchangeable. +message PreviewResponse { + // The dry-run preview, rendered as a RenderTree. Producing this MUST NOT + // mutate anything and MUST be side-effect-free — the same guarantee + // TOOL_KIND_DATA_SOURCE operations make, but unconditionally, regardless + // of the call's actual ToolKind. + pluggableharness.agent.render.v1.RenderTree preview = 1; +} + +// DescribeRequest carries no fields — Describe takes no parameters. +message DescribeRequest {} + +// DescribeResponse reports this plugin build's own identity, per +// configuration/lock-file.md's dev_overrides note: a dev_overrides-resolved +// plugin has no lock-file entry for the kernel to read {name, version, +// source, category, protocol_version} from, so the kernel obtains it +// directly from the running process via this RPC instead. +message DescribeResponse { + // This plugin build's identity. + pluggableharness.agent.common.v1.ProducerRef producer = 1; +} diff --git a/api/pluggableharness/agent/widget/v1/widget.proto b/api/pluggableharness/agent/widget/v1/widget.proto index fb6c819..949948c 100644 --- a/api/pluggableharness/agent/widget/v1/widget.proto +++ b/api/pluggableharness/agent/widget/v1/widget.proto @@ -8,6 +8,7 @@ syntax = "proto3"; package pluggableharness.agent.widget.v1; import "google/protobuf/struct.proto"; +import "pluggableharness/agent/common/v1/common.proto"; import "pluggableharness/agent/config/v1/config.proto"; import "pluggableharness/agent/render/v1/render.proto"; @@ -47,6 +48,27 @@ service WidgetService { // literal spec, naming the domain concept rather than the RPC. Not a // uniqueness violation: WidgetUpdate is used by exactly this one RPC. rpc Attach(AttachRequest) returns (stream WidgetUpdate); + + // Describe reports this plugin build's own identity — {name, version, + // source, category, protocol_version} — directly from the running + // process, rather than the kernel inferring it from a lock-file row. + // Every one of the six category protocols gains this identical RPC in + // this protocol revision; it exists specifically for a + // `dev_overrides`-resolved binary (configuration/lock-file.md's + // "dev_overrides and identity without a lock entry"), which has no + // provider {} lock-file entry to read identity from at all. + rpc Describe(DescribeRequest) returns (DescribeResponse); +} + +// DescribeRequest carries no fields — Describe takes no parameters. +message DescribeRequest {} + +// DescribeResponse reports this plugin build's own identity, obtained +// directly from the running process rather than a lock-file row — +// configuration/lock-file.md's "dev_overrides and identity without a lock +// entry". +message DescribeResponse { + pluggableharness.agent.common.v1.ProducerRef producer = 1; } // GetCapabilitiesRequest carries no fields — GetCapabilities takes no @@ -68,6 +90,12 @@ message WidgetCapabilities { // This provider's agent.hcl config schema, per configuration.md §4 — // what fields Configure's request may be decoded from. pluggableharness.agent.config.v1.ConfigSchema config_schema = 2; + + // Hook points this widget can subscribe to in observe mode + // (agent-loop/hook-dispatch.md), so a mis-declared agent.hcl hook{} + // block naming an unsupported point can be rejected at config-load + // time rather than failing at first dispatch. + repeated pluggableharness.agent.common.v1.HookPoint supported_hook_points = 3; } // ConfigureRequest carries this provider's already-decoded agent.hcl block. @@ -98,3 +126,33 @@ message WidgetUpdate { // True: replace this widget's prior content in `region`. False: append. bool replace = 3; } + +// WidgetErrorCategory classifies a WidgetError, mirroring +// FrontendErrorCategory's shape (frontend.proto) for the widget category — +// resolves frontend/conformance.md's prior open question of whether +// widgets need a structured error type of their own. +enum WidgetErrorCategory { + // Zero value. Never valid for a real error; its presence on the wire + // means a caller forgot to set the field. + WIDGET_ERROR_CATEGORY_UNSPECIFIED = 0; + // A RenderTree or WidgetUpdate could not be displayed. + WIDGET_ERROR_CATEGORY_RENDER_FAILED = 1; + // A WidgetUpdate named a Region this widget's frontend cannot honor. + WIDGET_ERROR_CATEGORY_REGION_UNSUPPORTED = 2; + // An error that does not fit any other category. + WIDGET_ERROR_CATEGORY_UNKNOWN = 3; +} + +// WidgetError is the structured error type for the widget category, +// mirroring FrontendError (frontend.proto). Unlike the frontend category +// (whose Attach errors surface in-band via ServerEvent.Error), widget +// Attach has no return channel other than the stream itself — WidgetError +// is carried in the structured detail of a gRPC status on Configure or +// Attach, per .claude/rules/grpc.md's error-taxonomy discipline, not as an +// in-band stream message. +message WidgetError { + // The error's category. + WidgetErrorCategory category = 1; + // A human-readable message. + string message = 2; +} diff --git a/docs/specifications/agent-loop/plan-apply-gate.md b/docs/specifications/agent-loop/plan-apply-gate.md index 4de04a9..274f553 100644 --- a/docs/specifications/agent-loop/plan-apply-gate.md +++ b/docs/specifications/agent-loop/plan-apply-gate.md @@ -12,6 +12,12 @@ PlanItem { input // parsed JSON args (kernel's canonical ToolCall representation, tool/data-types.md) decision enum { pending, allow, ask, deny } decided_by string // subscriber/policy-rule name that produced the decision + + // Snapshot fields — see "Snapshot rationale" below. + kind tool.v1.ToolKind + risk tool.v1.RiskClass + description string + preview render.v1.RenderTree? // optional; see "Preview flow" below } Plan { @@ -22,6 +28,16 @@ Plan { Policy evaluation happens as the `plan-ready` hook's `veto` chain ([`architecture.md`](../architecture.md#policy--first-party-not-a-plugin-category) — "policy is the kernel-privileged veto-mode subscriber at the plan-ready hook, always run, always respected"). The kernel MUST evaluate policy rules per `PlanItem`, not once for the whole plan — a plan with three resource calls against three different tool providers can and MUST receive three independently evaluated decisions. Presentation MAY batch multiple `ask`-decision items from the same plan into a single combined approval UI interaction (matching the "shown as a diff for approval" framing, and Terraform's own plan-diff precedent), but the decision unit underneath MUST remain per-item so a human can approve some resource calls in a plan and reject others without rejecting the whole turn — this also enables a corrected-input redirect (the model supplies corrected arguments rather than a binary accept/reject) as a frontend feature without a kernel data-model change; see [`frontend/frontend-protocol.md`](../frontend/frontend-protocol.md)'s `plan_decision.corrected_input`. +### Snapshot rationale + +`kind`, `risk`, `description`, and `preview` are captured from the originating tool operation's `ToolSchema` (and, for `preview`, from a live `Preview` call — see below) at **plan-construction time**, not looked up live whenever a plan is later displayed or audited. This matters because `ToolSchema` itself is not immutable across a provider's lifetime: an operator can edit `agent.hcl`, a provider can ship a new version reclassifying an operation's risk, or a description can be reworded — none of which may retroactively alter what a *historical* plan's audit record says happened. `state-backend.md`'s `plan_items` table persists a `plan-ready`-time snapshot precisely so "what risk was this call classified at when it ran" stays answerable after the classification itself has since changed. A frontend rendering a live, in-progress plan and a CLI displaying a plan from six months ago both read the same snapshot fields — neither re-resolves against the provider's current `GetSchema` response. + +### Preview flow + +`preview` is populated by the kernel calling the originating tool provider's `Preview` RPC ([`tool/protocol.md#preview`](../tool/protocol.md#preview)) at plan-construction time, for `TOOL_KIND_RESOURCE` items whose provider implements it — a dry-run description of the call's effect (e.g. a diff for a file write, a request summary for an HTTP call), returned as a `render.v1.RenderTree` and stored on the `PlanItem` verbatim, the same type `Preview`'s own RPC response carries. `data_source` and `interactive` items MUST NOT have `preview` populated — `Preview` is a resource-item concept, mirroring how only resource items reach the plan/apply gate's `allow`/`ask`/`deny` decision at all (see [Data source and interactive calls](#data-source-and-interactive-calls) below). + +A provider that does not implement `Preview` leaves `preview` absent on every `PlanItem` it produces — this is an ordinary, unexceptional absence, not an error condition; a frontend MUST fall back to rendering the raw `input` field (the call's parsed arguments) in that case, exactly as it would for a plan built before `Preview` existed. The kernel MUST NOT block plan construction on a slow or failing `Preview` call beyond its own ordinary per-RPC deadline (`.claude/rules/grpc.md`'s "Context and deadlines") — a `Preview` timeout or error degrades to an absent `preview` for that item, never to an aborted plan. + ## Decision semantics - `allow` — the kernel proceeds to apply this item without further interaction. @@ -49,3 +65,13 @@ What differs between `data_source` and `interactive` is scheduling, not policy: The precheck's evaluation result MUST distinguish an outright `ask`-turned-`deny` downgrade from a plain `deny` decision — a caller needs to be able to log that a winning `ask` decision was flipped to `deny` for a `data_source`/`interactive` call, rather than have that transition happen silently. The `interactive`-kind precheck reuses the `data_source` precheck's defaulting and downgrade rules verbatim rather than the policy DSL gaining a third match kind of its own: [`configuration/policy-dsl.md`](../configuration/policy-dsl.md)'s match schema stays two-valued (`resource`/`data_source`), and an `interactive` call routes through the same non-interactive precheck path a `data_source` call uses. + +## PlanDecisionScope semantics + +[`frontend/frontend-protocol.md`](../frontend/frontend-protocol.md)'s `ClientEvent.PlanDecision` carries a `scope` field (`PlanDecisionScope`: `ONCE`/`SESSION`/`ALWAYS`) alongside `decision` and `corrected_input`. `scope` governs how durably the resolved decision applies beyond the one `PlanItem` it names — it is evaluated by the plan/apply gate at the same point `decision` and `corrected_input` are, immediately on receipt of a `plan_decision` client event, not deferred to any later stage: + +- **`ONCE`** (the default a frontend SHOULD send absent explicit operator intent): applies to the named `PlanItem` only. No durable record beyond the ordinary `plan_items` audit row this decision produces regardless of scope. +- **`SESSION`**: the kernel MUST remember this verdict for the rest of the current session, in memory — not written to `agent.hcl` or any persisted policy store — and apply it automatically to any future plan item in the same session matching the same `(provider, tool_name)` pair, without re-emitting a `permission_request`/blocking on a fresh `plan_decision`. A `SESSION`-scoped `deny` suppresses future `ask`/`allow` items the same way; a `SESSION`-scoped `allow` (with or without `corrected_input`) auto-applies the decision (re-validating `corrected_input` against the current call's own arguments each time, per [`frontend/frontend-protocol.md#plan_decisioncorrected_input`](../frontend/frontend-protocol.md#plan_decisioncorrected_input) — a `SESSION` scope remembers the *verdict*, not a frozen copy of the corrected arguments). This rule lapses at session end; it does not survive a `ResumeSession` re-open ([`frontend/frontend-protocol.md#resume-and-re-open-semantics`](../frontend/frontend-protocol.md#resume-and-re-open-semantics)) into a fresh session of rules. +- **`ALWAYS`**: the kernel MUST persist this verdict as policy — surviving beyond the current session, applying to future sessions under the same profile — via the same policy-rule mechanism [`configuration/policy-dsl.md`](../configuration/policy-dsl.md) already governs for operator-authored rules. This requires kernel-side policy persistence: a kernel build that cannot durably write a new policy rule (e.g. no writable policy store configured) MUST reject an `ALWAYS`-scoped `plan_decision` with a distinct error rather than silently downgrading it to `SESSION` or `ONCE` — a frontend and its operator need to know an "always allow this" request didn't actually stick, not discover it the next time the same prompt reappears. An `ALWAYS`-scoped decision, once persisted, takes effect starting with the *next* plan-ready evaluation it would match — it does not retroactively alter the plan item that triggered it, which has already been decided via the ordinary `decision`/`corrected_input` fields. + +`SESSION` and `ALWAYS` are both strictly broader than what the underlying per-item decision unit ([Plan construction and policy evaluation](#plan-construction-and-policy-evaluation) above) requires — they are a frontend/operator convenience layered on top of, not a replacement for, per-item evaluation: policy still runs and still produces an independent decision for every item in every future plan, it's simply that a `SESSION`/`ALWAYS` rule can now be one of the things that decision is based on. diff --git a/docs/specifications/context/README.md b/docs/specifications/context/README.md index 6e5151e..cef656f 100644 --- a/docs/specifications/context/README.md +++ b/docs/specifications/context/README.md @@ -14,7 +14,7 @@ This category covers content injected into the prompt *before* a model call, sou Subprocess + gRPC via `hashicorp/go-plugin`, per [`architecture.md`](../architecture.md#transport). The standard handshake applies uniformly across all six provider categories and isn't repeated per category. -A context provider plugin exposes three RPCs: `GetCapabilities`, `Configure`, `Contribute`. It MAY additionally implement `Render` (see [`protocol.md#render`](protocol.md#render)). +A context provider plugin exposes four RPCs: `GetCapabilities`, `Configure`, `Contribute`, `Describe`. It MAY additionally implement `Render` (see [`protocol.md#render`](protocol.md#render)). **`Contribute` is unary request/response, not streamed.** Unlike a model provider's `StreamCompletion`, context assembly happens before the model call starts, and convention-file/orientation content is small enough that no researched harness needed token-level streaming for it. See [`protocol.md#contribute-the-context-assemble-rpc`](protocol.md#contribute-the-context-assemble-rpc). @@ -26,7 +26,7 @@ Per [`architecture.md`](../architecture.md#hook-dispatch-semantics), context pro ## Category structure -- [`protocol.md`](protocol.md) — the three/four RPCs: `GetCapabilities`, `Configure`, `Contribute`, `Render`. +- [`protocol.md`](protocol.md) — the four/five RPCs: `GetCapabilities`, `Configure`, `Contribute`, `Describe`, `Render`. - [`data-types.md`](data-types.md) — `ContextRequest`, `ContextSection`, `ContextContribution`, the ordering/chaining and compaction contract, and content-structuring requirements. - [`examples.md`](examples.md) — the real proto wire definitions, a worked two-provider `context-assemble` sequence, and a budget worked example. - [`conformance.md`](conformance.md) — the error taxonomy and the MUST/SHOULD/MAY summary matrix, plus genuinely open questions. diff --git a/docs/specifications/context/conformance.md b/docs/specifications/context/conformance.md index bf78308..12575df 100644 --- a/docs/specifications/context/conformance.md +++ b/docs/specifications/context/conformance.md @@ -21,6 +21,9 @@ On the wire, using the canonical gRPC error-code mapping: `source_unavailable` | Capability | Level | Notes | |---|---|---| | `GetCapabilities` / `Configure` / `Contribute` RPCs | MUST | the whole protocol surface | +| `Describe` RPC | MUST | reports this build's own `common.v1.ProducerRef` identity; see [`configuration/lock-file.md`](../configuration/lock-file.md#dev_overrides-and-identity-without-a-lock-entry) for why this exists | +| `ContextRequest.history_tokens` / `assembled_tokens_last_turn` | MUST (kernel-side) | kernel-computed on every firing, not just compactor-directed ones — [`data-types.md#compactor-timing-signals`](data-types.md#compactor-timing-signals) | +| `supported_hook_points` declaration | MAY | empty unless this provider also declares `hook{}` blocks in `agent.hcl` | | `text` content blocks | MUST | baseline; v1 has no other content type | | Non-text content blocks (`image`, etc.) | MUST NOT (v1) | kernel MUST reject, not silently drop | | `stability` declaration | MUST | [`data-types.md#stability-hint--cache-prefix-ordering`](data-types.md#stability-hint--cache-prefix-ordering) | diff --git a/docs/specifications/context/data-types.md b/docs/specifications/context/data-types.md index fcb037a..92e6bcd 100644 --- a/docs/specifications/context/data-types.md +++ b/docs/specifications/context/data-types.md @@ -6,7 +6,9 @@ ```protobuf ContextRequest { - session_id, parent_session_id, turn_number + session_id, parent_session_id + turn_id string // ULID, standardized across the whole + // protocol (matches plan.v1's turn_id) token_budget int // MUST — kernel-computed allocation // for this call, see #ordering--chaining model_target ModelTarget // MUST — { id, context_window, effective_ceiling }, @@ -17,6 +19,12 @@ ContextRequest { // providers in this hook's // declaration-order chain conversation_history []Message? // populated ONLY for a compactor provider + history_tokens int // kernel-computed current + // conversation-history token total — + // see #compactor-timing-signals + assembled_tokens_last_turn int // kernel-computed total assembled + // context size of the previous turn — + // see #compactor-timing-signals } ``` @@ -66,6 +74,12 @@ ContextContribution { `sections` is the full chain, never a delta (see [`protocol.md#contribute-the-context-assemble-rpc`](protocol.md#contribute-the-context-assemble-rpc)). `rewritten_history`, when present, replaces the turn's conversation history before the next model call — see [`protocol.md#session-wide-conversation-compaction`](protocol.md#session-wide-conversation-compaction). +### Compactor timing signals + +`ContextRequest.history_tokens` (the current conversation-history token total) and `ContextRequest.assembled_tokens_last_turn` (the total assembled context size of the previous turn) are both kernel-computed and carried on every firing, not just ones directed at a compactor provider. They exist to answer one question a [`protocol.md#session-wide-conversation-compaction`](protocol.md#session-wide-conversation-compaction) compactor provider otherwise has no cheap way to answer: **when** to compact, as distinct from **how**. Without them, a compactor wanting to trigger only once history crosses some threshold would have to re-derive a token count itself — either re-running `CountTokens` over `conversation_history` on every firing (wasteful, and only available to a compactor provider in the first place, per the `conversation_history` visibility rule above) or maintaining its own running estimate (drifts from the kernel's authoritative count). Surfacing both figures as plain kernel-computed fields lets any compactor implement a threshold policy (e.g. "compact once `history_tokens` exceeds 50% of the model's `effective_ceiling`", mirroring Gemini CLI's documented compression trigger) without re-counting anything itself — the same "kernel computes, provider decides" division of labor `tokens` already follows elsewhere in this spec (see [`#contextsection`](#contextsection) above). + +`assembled_tokens_last_turn` complements `history_tokens` by reporting the *other* half of what consumed the previous turn's budget — the sum of every provider's contributed `ContextSection.tokens`, not just conversation history — so a compactor can distinguish "history growth is the pressure" from "another provider's contributions grew" without needing visibility into other providers' internals. + ## Ordering & chaining Per [`architecture.md`](../architecture.md#hook-dispatch-semantics)'s hook dispatch semantics, context providers subscribe at `context-assemble` in `transform` mode, running as an **ordered chain in `agent.hcl` declaration order** (not runtime registration order). Concretely: provider *N* receives provider *1..N-1*'s already-merged `prior_sections` and returns the full chain including its own addition. diff --git a/docs/specifications/context/examples.md b/docs/specifications/context/examples.md index c006ebd..6908b06 100644 --- a/docs/specifications/context/examples.md +++ b/docs/specifications/context/examples.md @@ -14,18 +14,21 @@ service ContextService { rpc Contribute(ContextRequest) returns (ContextContribution); rpc Render(RenderRequest) returns (RenderResponse); + rpc Describe(DescribeRequest) returns (DescribeResponse); } message ContextRequest { string session_id = 1; string parent_session_id = 2; - int64 turn_number = 3; + string turn_id = 3; int64 token_budget = 4; pluggableharness.agent.model.v1.ModelTarget model_target = 5; repeated string files_touched = 6; string working_directory = 7; repeated ContextSection prior_sections = 8; repeated pluggableharness.agent.content.v1.Message conversation_history = 9; + int64 history_tokens = 10; + int64 assembled_tokens_last_turn = 11; } message ContextSection { @@ -63,7 +66,7 @@ context "agents-md" { ```text → ContextRequest{ - session_id: "sess_01", turn_number: 1, + session_id: "sess_01", turn_id: "turn_01", token_budget: 2000, model_target: { id: "claude-opus-5", context_window: 200000, effective_ceiling: 176000 }, files_touched: [], @@ -101,7 +104,7 @@ Neither provider mutated the other's section — each returned the full chain wi ```text → ContextRequest{ - ... turn_number: 4, + ... turn_id: "turn_04", files_touched: ["src/auth/validator.py"], prior_sections: [ { provider: "claude-md", ... }, { provider: "agents-md", ... } ], } diff --git a/docs/specifications/context/protocol.md b/docs/specifications/context/protocol.md index fb8de12..70aab57 100644 --- a/docs/specifications/context/protocol.md +++ b/docs/specifications/context/protocol.md @@ -1,6 +1,6 @@ # Context provider — protocol -The three RPCs a context provider plugin MUST expose, plus the one it MAY. See [`README.md`](README.md#transport--lifecycle) for the transport-level framing (unary, not streamed) that applies to `Contribute` specifically. +The three RPCs a context provider plugin MUST expose, plus the two it MAY. See [`README.md`](README.md#transport--lifecycle) for the transport-level framing (unary, not streamed) that applies to `Contribute` specifically. ## `GetCapabilities` @@ -22,6 +22,8 @@ ContextCapabilities { `ContextCapabilities` MAY additionally include `slash_commands: []SlashCommandSpec` and MUST include the provider's `ConfigSchema`, so the kernel knows what fields `Configure` expects before ever calling it — the same shape every provider category's `GetCapabilities` follows, see [`model/protocol.md#getcapabilities`](../model/protocol.md#getcapabilities). +`ContextCapabilities` also MAY declare `supported_hook_points: []common.v1.HookPoint` — which hook points (beyond `context-assemble` itself, which never rides this field) this provider subscribes `HookSubscriberService.DispatchHook` to, per [`agent-loop/hook-dispatch.md`](../agent-loop/hook-dispatch.md). Empty unless this provider's `agent.hcl` also declares `hook{}` blocks. + ## `Configure` Accepts a config object decoded from the provider's `agent.hcl` block via the schema-to-cty bridge (see [`configuration/blocks-reference.md`](../configuration/blocks-reference.md)). Field contents are provider-specific — which file(s)/globs to read, max-hop `@import` depth, whether to strip HTML comments, etc. This protocol doesn't mandate a shape beyond: @@ -37,6 +39,8 @@ Contribute(ContextRequest) -> ContextContribution The kernel invokes `Contribute` at least once per turn, before each model call (see [`README.md#firing-cadence--jit-loading`](README.md#firing-cadence--jit-loading)). `Contribute` MUST return the full accumulated `[]ContextSection` chain — this provider's own section appended to `ContextRequest.prior_sections` — never a delta. This mirrors [`architecture.md`](../architecture.md#hook-dispatch-semantics)'s `transform` hook-mode contract, where "each subscriber... returns a modified version, the next subscriber sees the transformed payload." See [`data-types.md`](data-types.md#contextrequest) for the full request/response schema and [`data-types.md#ordering--chaining`](data-types.md#ordering--chaining) for what a provider MAY and MUST NOT touch in the chain it receives. +`ContextRequest.turn_id` identifies which turn a firing is for as a ULID string, standardized across the whole protocol (`plan.v1`'s `turn_id` is the same shape). `ContextRequest` also carries `history_tokens` and `assembled_tokens_last_turn`, both kernel-computed on every firing — see [`data-types.md#compactor-timing-signals`](data-types.md#compactor-timing-signals) for what a compactor provider does with them. + ### Session-wide conversation compaction A provider declaring `compactor: true` MAY see and rewrite the session's conversation history, not just other providers' sections. `ContextRequest` carries a `conversation_history` field populated **only** for a compactor provider — a non-compactor provider MUST NOT receive it, symmetric with the "own section only unless compactor" rule above. A compactor's `Contribute` response MAY include `rewritten_history` alongside its ordinary section contribution; when present, the kernel MUST replace the turn's conversation history with this value before the next model call (see [`agent-loop/turn-algorithm.md`](../agent-loop/turn-algorithm.md)). This reuses the same `context-assemble` hook payload rather than inventing a second mechanism — the "producer, not a generic byte-slicer, owns the reduction" principle that governs section-content budgeting ([`data-types.md#ordering--chaining`](data-types.md#ordering--chaining)) extends to the conversation itself. @@ -46,3 +50,13 @@ A non-compactor provider whose `Contribute` response mutates a section it doesn' ## `Render` Context providers MAY implement `Render` per the general Emit→Render→Paint pipeline ([`architecture.md`](../architecture.md#emit--render--paint-pipeline)), returning the `RenderTree` formally defined in [`frontend/render-tree.md`](../frontend/render-tree.md) — e.g. to render an injected CLAUDE.md section collapsed by default in a transcript view, distinct from the live conversation. If not implemented, the kernel falls back to its generic default rendering. + +`RenderRequest` carries `schema_version` alongside the opaque `payload` — see [`frontend/render-tree.md#schema-versioning`](../frontend/render-tree.md#schema-versioning) for what the value means and how a `Render` implementation is expected to use it. + +## `Describe` + +```text +Describe(DescribeRequest) -> DescribeResponse +``` + +MUST be implemented. Reports this plugin build's own identity — `{name, version, source, category, protocol_version}` via `common.v1.ProducerRef` — independent of any lock-file entry. This is how the kernel identifies a `dev_overrides`-resolved binary, which has no `provider "" { ... }` entry to read identity from the normal way: see [`configuration/lock-file.md`](../configuration/lock-file.md#dev_overrides-and-identity-without-a-lock-entry) for the canonical explanation, shared verbatim across every plugin category that gains this RPC in this protocol revision. diff --git a/docs/specifications/frontend/README.md b/docs/specifications/frontend/README.md index 86c86e0..a282e2b 100644 --- a/docs/specifications/frontend/README.md +++ b/docs/specifications/frontend/README.md @@ -15,22 +15,23 @@ Subprocess + gRPC via `hashicorp/go-plugin`, per [`architecture.md`](../architec The two categories' primary RPC has **different streaming shapes**, and this is the one thing about this directory most worth getting right, since it's easy to conflate the two: -- A **frontend** provider plugin exposes `GetCapabilities`, `Configure`, `Attach`. `Attach` is **genuinely bidirectional** — the frontend sends `ClientEvent`s (operator input) and receives `ServerEvent`s (kernel state) on the same live stream, because the operator can type a message while prior content is still rendering. This is one of only two truly bidirectional RPCs anywhere in this protocol series, the other being the kernel-callback channel; every other category's primary RPC (`StreamCompletion`, `Invoke`) is server-streaming-plus-cancellation. See [`frontend-protocol.md#transport`](frontend-protocol.md#transport). -- A **widget** provider plugin exposes `GetCapabilities`, `Configure`, `Attach` too — same RPC name, **different shape**: widget `Attach` is **server-streaming only**. A widget is passive/display-only in v1; it never sends anything back over this channel. A widget wanting to trigger an action does so by also being a tool provider with a slash command, not through `Attach`. See [`widget-protocol.md#transport`](widget-protocol.md#transport). +- A **frontend** provider plugin exposes `GetCapabilities`, `Configure`, `Attach`, `Describe`. `Attach` is **genuinely bidirectional and connection-scoped** — one stream per connection, not one per session — the frontend sends `ClientEvent`s (operator input, session lifecycle control) and receives `ServerEvent`s (kernel state, session lifecycle acknowledgment) on the same live stream, because the operator can type a message while prior content is still rendering, and because full session lifecycle needs connection-level operations that have no natural per-session home. This is one of only two truly bidirectional RPCs anywhere in this protocol series, the other being the kernel-callback channel; every other category's primary RPC (`StreamCompletion`, `Invoke`) is server-streaming-plus-cancellation. See [`frontend-protocol.md#transport`](frontend-protocol.md#transport). +- A **widget** provider plugin exposes `GetCapabilities`, `Configure`, `Attach`, `Describe` too — same RPC names, **different `Attach` shape**: widget `Attach` is **server-streaming only**, and remains session-scoped (one call per session, via `AttachRequest.session_id`) rather than connection-multiplexed. A widget is passive/display-only in v1; it never sends anything back over this channel. A widget wanting to trigger an action does so by also being a tool provider with a slash command, not through `Attach`. See [`widget-protocol.md#transport`](widget-protocol.md#transport). Both plugins MAY additionally implement `Render`, per the general Emit→Render→Paint pipeline ([`architecture.md`](../architecture.md#emit--render--paint-pipeline)) — though in practice a frontend/widget is far more often a `Render` *consumer* (painting other categories' trees) than a producer of its own. ## Session scope — multi-attach -**Multiple frontends MAY `Attach` to the same session concurrently** — a TUI and a web tail both watching one live session, for example. This follows naturally from how widgets already work: any number of panels can subscribe to the same hook stream, so frontends support the same multiplicity rather than being constrained to a single attachment. +**Multiple frontends MAY subscribe to the same session concurrently** — a TUI and a web tail both watching one live session, for example — each on its own connection-scoped `Attach` stream (`frontend-protocol.md#session-lifecycle`). This follows naturally from how widgets already work: any number of panels can subscribe to the same hook stream, so frontends support the same multiplicity rather than being constrained to a single attachment. -- **`ServerEvent`s broadcast identically to every attached frontend.** No partitioning, no "primary" frontend — every attached frontend sees the same live stream, in the same order. -- **`ClientEvent`s are processed in kernel arrival order**, with one specific arbitration rule for decisions that can only be honored once. See [`frontend-protocol.md#session-scope`](frontend-protocol.md#session-scope) for the full rule (first-response-wins on `plan_decision`/ `interactive_response`, with a distinct rejection error for a late-arriving second response). +- **`ServerEvent`s for a given session broadcast identically to every frontend subscribed to that session.** No partitioning, no "primary" frontend — every subscribed frontend sees that session's live stream, in the same order. A frontend's own `Attach` stream may carry several sessions at once, each broadcasting independently to whichever frontends are subscribed to it. +- **`ClientEvent`s are processed in kernel arrival order, per session**, with one specific arbitration rule for decisions that can only be honored once. See [`frontend-protocol.md#session-scope`](frontend-protocol.md#session-scope) for the full rule (first-response-wins on `plan_decision`/ `interactive_response`, with a distinct rejection error for a late-arriving second response). +- **Attaching a session that's already in progress backfills its history first** — a bracketed replay batch (`session_attached` → replayed `render`s → `backfill_complete`) unicast to the newly attaching stream only, never re-broadcast to frontends already subscribed. See [`frontend-protocol.md#backfill--the-replay-path-not-a-new-subsystem`](frontend-protocol.md#backfill--the-replay-path-not-a-new-subsystem). ## Category structure -- [`render-tree.md`](render-tree.md) — the `RenderTree` IR itself: every node type, the placement/region vocabulary, and why both categories share one definition. The canonical reference every other category's `Render` points back to. -- [`frontend-protocol.md`](frontend-protocol.md) — the frontend provider protocol: transport, fast-path text deltas vs. full `Render`, session scope/multi-attach, slash commands (`SlashCommandSpec`, defined once, canonically, here), the `plan_decision.corrected_input` redirect, and the error taxonomy for this category. -- [`widget-protocol.md`](widget-protocol.md) — the widget provider protocol: transport (server-streaming, not bidi), deriving display state from `observe`-mode hooks with no new data feed, and interactive widgets via the `action` `RenderNode`. -- [`examples.md`](examples.md) — wire-protocol excerpts for all three schemas, a worked frontend `Attach` sequence (plan-ready → render → approve/reject/edit), and a worked widget example (a status-bar widget deriving state from the same event stream a frontend sees). -- [`conformance.md`](conformance.md) — the error taxonomy, the MUST/SHOULD/ MAY summary matrix for both categories, and any genuinely open questions. +- [`render-tree.md`](render-tree.md) — the `RenderTree` IR itself: every node type, the placement/region vocabulary, schema versioning for opaque `Render` payloads, and why both categories share one definition. The canonical reference every other category's `Render` points back to. +- [`frontend-protocol.md`](frontend-protocol.md) — the frontend provider protocol: transport, the connection-scoped multiplexed `Attach` stream, fast-path text deltas vs. full `Render`, full session lifecycle (create/attach/resume/detach/list, backfill, no deletion), session scope/multi-attach, slash commands (`SlashCommandSpec`, defined once, canonically, here), the `plan_decision.corrected_input`/`scope` redirect, and the error taxonomy for this category. +- [`widget-protocol.md`](widget-protocol.md) — the widget provider protocol: transport (server-streaming, not bidi), deriving display state from `observe`-mode hooks with no new data feed, interactive widgets via the `action` `RenderNode`, and the `WidgetError` taxonomy. +- [`examples.md`](examples.md) — wire-protocol excerpts for all three schemas, a worked frontend `Attach` sequence (attach → backfill → plan-ready → render → approve/reject/edit), and a worked widget example (a status-bar widget deriving state from the same event stream a frontend sees). +- [`conformance.md`](conformance.md) — the error taxonomy for both categories, the MUST/SHOULD/MAY summary matrix, and any genuinely open questions. diff --git a/docs/specifications/frontend/conformance.md b/docs/specifications/frontend/conformance.md index 909361b..3eef6ea 100644 --- a/docs/specifications/frontend/conformance.md +++ b/docs/specifications/frontend/conformance.md @@ -13,7 +13,7 @@ The frontend provider category has a structured error type, `FrontendError`: A `Configure`-time `FrontendError` surfaces as a gRPC status carrying the error in its structured detail, mapped to `codes.InvalidArgument` for a malformed config value and `codes.Internal` for anything unmapped — the same canonical "specific code, never a bare `codes.Unknown`" discipline every category in this protocol series follows. An error encountered mid-`Attach` (a bad render, an invalid client event) surfaces in-band as `ServerEvent.error` instead, since `Attach` is a long-lived stream where tearing down the whole connection over one recoverable error would be far too disruptive — only a genuinely fatal condition (the plugin process itself failing) closes the stream with a gRPC status. -**The widget provider category has no structured error type of its own.** There is no `WidgetError` message — widget failures surface only as ordinary gRPC status codes on `Configure` or `Attach` (mapped per the same canonical table: `codes.InvalidArgument`/`codes.Internal`/`codes.Canceled` as appropriate), with no in-band structured category the way `FrontendError` provides for the frontend category. Whether this is a deliberate scope reduction (a widget is passive/display-only, so there's less to classify) is an open question below. +**The widget provider category has its own structured error type, `WidgetError`** ([`widget-protocol.md#error-taxonomy`](widget-protocol.md#error-taxonomy)), mirroring `FrontendError`'s category/message shape. Unlike the frontend category, widget `Attach` has no in-band return channel — it's server-streaming only — so `WidgetError` is always carried in the structured detail of a gRPC status on `Configure` or `Attach`, mapped per the same canonical table (`codes.InvalidArgument`/`codes.Internal`/`codes.Canceled`), never as an in-band stream message. This resolves what was previously an open question in this document about whether widgets needed a categorized error channel at all — they do, for the same reason a partial-failure condition (e.g. "this update renders for one region but not another") needs a name a widget author can report distinctly from an unstructured `codes.Internal`. ## Required vs. optional support — summary matrix @@ -22,28 +22,38 @@ A `Configure`-time `FrontendError` surfaces as a gRPC status carrying the error | `RenderTree` node types render gracefully, including unknown/unspecialized ones | MUST | [`render-tree.md`](render-tree.md#rendertree) | | Placement is a hint, never a mandate | MUST (frontend may reinterpret) | [`render-tree.md`](render-tree.md#placement--regions) | | `overlay` visually distinct from ambient content | MUST | [`render-tree.md`](render-tree.md#placement--regions) | -| Frontend `Attach` is bidirectional | MUST | [`frontend-protocol.md`](frontend-protocol.md#transport) | +| `Render` payloads carry a `schema_version`, echoed unchanged from emit time | MUST | [`render-tree.md`](render-tree.md#schema-versioning) | +| Frontend `Attach` is bidirectional and connection-scoped, multiplexing every subscribed session | MUST | [`frontend-protocol.md`](frontend-protocol.md#transport) | +| `Describe` reports this plugin build's own identity from the running process | MUST | [`frontend-protocol.md`](frontend-protocol.md#transport), [`widget-protocol.md`](widget-protocol.md#transport) | | Streaming text via fast-path deltas, not per-token `Render` | MUST | [`frontend-protocol.md`](frontend-protocol.md#fast-path-vs-full-render) | +| `UserMessage` carries `repeated ContentBlock`, not a bare string | MUST | [`frontend-protocol.md`](frontend-protocol.md#usermessage-carries-contentblocks) | | `plan_decision.corrected_input` re-validated against `input_schema` | MUST | [`frontend-protocol.md`](frontend-protocol.md#plan_decisioncorrected_input) | +| `plan_decision.scope` (`ONCE`/`SESSION`/`ALWAYS`) governs how durably a decision applies | MUST | [`frontend-protocol.md`](frontend-protocol.md#plan_decisioncorrected_input), [`agent-loop/plan-apply-gate.md`](../agent-loop/plan-apply-gate.md#plandecisionscope-semantics) | | `interactive_request`/`interactive_response` for `kind: interactive` tool calls | MUST | [`frontend-protocol.md`](frontend-protocol.md#fast-path-vs-full-render) | -| Multiple frontends MAY `Attach` concurrently | MAY | [`README.md`](README.md#session-scope--multi-attach) | -| `ServerEvent`s broadcast identically to every attached frontend, when multi-attach occurs | MUST | [`frontend-protocol.md`](frontend-protocol.md#session-scope) | -| First-response-wins on `plan_decision`/`interactive_response`; losers get a distinct error | MUST | [`frontend-protocol.md`](frontend-protocol.md#session-scope) | -| Widget `Attach` is server-streaming only (no bidi channel) | MUST | [`widget-protocol.md`](widget-protocol.md#transport) | +| Session lifecycle (create/attach/resume/detach/list) over `ClientEvent`/`ServerEvent` control variants, correlated by `request_id` | MUST | [`frontend-protocol.md`](frontend-protocol.md#session-lifecycle) | +| Backfill on attach is a bracketed, unicast replay batch, never broadcast | MUST | [`frontend-protocol.md`](frontend-protocol.md#backfill--the-replay-path-not-a-new-subsystem) | +| A COMPLETED/CANCELLED session MAY be re-opened to RUNNING via `ResumeSession`; bound-exhausted/FAILED sessions are replay-only | MUST | [`frontend-protocol.md`](frontend-protocol.md#resume-and-re-open-semantics) | +| No session deletion mechanism exists for any plugin category | MUST (by decision) | [`frontend-protocol.md`](frontend-protocol.md#no-session-deletion) | +| Multiple frontends MAY subscribe to the same session concurrently | MAY | [`README.md`](README.md#session-scope--multi-attach) | +| `ServerEvent`s broadcast identically to every frontend subscribed to a given session | MUST | [`frontend-protocol.md`](frontend-protocol.md#session-scope) | +| First-response-wins on `plan_decision`/`interactive_response`, per session; losers get a distinct error | MUST | [`frontend-protocol.md`](frontend-protocol.md#session-scope) | +| Widget `Attach` is server-streaming only (no bidi channel), and remains session-scoped, not connection-multiplexed | MUST | [`widget-protocol.md`](widget-protocol.md#transport) | | Widget-initiated actions via `ActionNode` + frontend `action_trigger`, not a widget-specific RPC | MUST | [`widget-protocol.md`](widget-protocol.md#interactive-widgets) | | Widgets derive state via `observe`-mode hooks, no separate data feed | MUST | [`widget-protocol.md`](widget-protocol.md#deriving-display-state--no-new-data-feed) | | `slash_commands` declarable by any provider category | MUST | [`frontend-protocol.md`](frontend-protocol.md#slash-commands) | | Slash-command name collision across providers | MUST be config-load-time error | [`frontend-protocol.md`](frontend-protocol.md#slash-commands) | +| Aggregate `SlashCommandRegistry` sent on session attach and on registry change | MUST | [`frontend-protocol.md`](frontend-protocol.md#slash-commands) | | `direct_invoke` dispatch bypasses the model turn | MUST | [`frontend-protocol.md`](frontend-protocol.md#slash-commands) | | `prompt_expansion` dispatch costs a model turn | MUST | [`frontend-protocol.md`](frontend-protocol.md#slash-commands) | | `prompt_expansion` scoping via `agent_profile.slash_commands` | MUST | [`frontend-protocol.md`](frontend-protocol.md#slash-commands), [`configuration/agent-profiles.md`](../configuration/agent-profiles.md) | +| `ActionNode`/`ActionTrigger` carry `provider`, since `tool_name` is only unique per provider | MUST | [`render-tree.md`](render-tree.md#interactive-content-the-action-node) | | `ActionNode` dispatches through the same path as `direct_invoke` | MUST | [`render-tree.md`](render-tree.md#interactive-content-the-action-node) | | Reference TUI's region layout | Not normative (one implementation) | [`examples.md`](examples.md#the-reference-tui) | | Structured `FrontendError` taxonomy | MUST | [Error taxonomy](#error-taxonomy) | -| Structured `WidgetError` taxonomy | Not currently specified — see [Open questions](#open-questions) | [Error taxonomy](#error-taxonomy) | +| Structured `WidgetError` taxonomy | MUST | [Error taxonomy](#error-taxonomy) | ## Open questions -- **Whether the widget provider category needs its own structured error type.** `FrontendError` gives the frontend category a categorized, in-band error channel; the widget protocol as built has no equivalent — widget failures are only ordinary gRPC status codes. Unclear whether that's an intentional consequence of widgets being passive/display-only (less to classify: a widget either renders or its stream errors out) or simply an unspecified gap. Worth resolving before a third-party widget author needs to decide how to report a partial-failure condition (e.g. "this widget can display some but not all of its regions"). -- **Whether `region_unsupported` should distinguish "frontend has no concept of this region at all" from "frontend recognizes the region but is out of space for it."** Both currently collapse to the same category; a widget author tuning `priority` might want to tell them apart. +- **Whether `region_unsupported` should distinguish "frontend has no concept of this region at all" from "frontend recognizes the region but is out of space for it."** Both currently collapse to the same category; a widget author tuning `priority` might want to tell them apart. `supported_regions`/`supported_hook_points` capability advertisement (this revision) narrows this somewhat — a producer can now check `supported_regions` proactively — but doesn't fully resolve the reactive-error case. - **Whether a future non-TUI frontend (web, voice) needs additional `Region` values**, or whether the existing six adequately cover every conforming implementation's actual layout needs — the abstract vocabulary was designed against one reference implementation (the TUI) plus a thought experiment (a hypothetical web/voice frontend), not a second built frontend to validate against. +- **`FRONTEND_ERROR_CATEGORY_SESSION_BUSY` currently has no triggering variant** in this protocol revision (no frontend-triggered session-mutating control event conflicts with a `RUNNING` session — `DetachSession` is always safe). It stays reserved for a future control event that would need it, rather than being removed, since a category enum value once shipped is never renumbered per `.claude/rules/proto.md`. diff --git a/docs/specifications/frontend/examples.md b/docs/specifications/frontend/examples.md index a6f1154..da1f856 100644 --- a/docs/specifications/frontend/examples.md +++ b/docs/specifications/frontend/examples.md @@ -11,6 +11,7 @@ service FrontendService { rpc GetCapabilities(GetCapabilitiesRequest) returns (GetCapabilitiesResponse); rpc Configure(ConfigureRequest) returns (ConfigureResponse); rpc Attach(stream ClientEvent) returns (stream ServerEvent); + rpc Describe(DescribeRequest) returns (DescribeResponse); } ``` @@ -21,6 +22,7 @@ service WidgetService { rpc GetCapabilities(GetCapabilitiesRequest) returns (GetCapabilitiesResponse); rpc Configure(ConfigureRequest) returns (ConfigureResponse); rpc Attach(AttachRequest) returns (stream WidgetUpdate); + rpc Describe(DescribeRequest) returns (DescribeResponse); } ``` @@ -46,14 +48,28 @@ See [`render-tree.md`](render-tree.md) for the full `RenderNode` variant list an ## A worked frontend `Attach` sequence -A TUI frontend attaches to a session already in progress, receives a `plan_ready` event for a proposed file edit, renders it, and the operator approves it with a corrected argument: +A TUI frontend opens its one connection-scoped `Attach` stream, attaches an already-running session (backfilling its history first), receives a `plan_ready` event for a proposed file edit, renders it, and the operator approves it with a corrected argument scoped to this session only: ```text -→ Attach() opens the bidirectional stream. - -← ServerEvent{stream_delta: {target_id: "msg_7", text: "I'll fix the "}} -← ServerEvent{stream_delta: {target_id: "msg_7", text: "off-by-one in main.go."}} -← ServerEvent{ +→ Attach() opens the bidirectional, connection-scoped stream. + +→ ClientEvent{ session_id: "", attach_session: { request_id: "r1", session_id: "sess_01H..." } } + +// Backfill: the kernel replays this session's persisted history, bracketed +// by session_attached and backfill_complete — frontend-protocol.md#backfill--the-replay-path-not-a-new-subsystem. +← ServerEvent{ session_id: "sess_01H...", request_id: "r1", + session_attached: { info: { session_id: "sess_01H...", profile: "default", + status: SESSION_STATUS_RUNNING, depth: 0, ... } } } +← ServerEvent{ session_id: "sess_01H...", render: { content: { ... } } } // replayed prior turns +← ServerEvent{ session_id: "sess_01H...", request_id: "r1", + backfill_complete: { last_sequence: 118 } } +← ServerEvent{ session_id: "sess_01H...", + slash_command_registry: { commands: [ ... ] } } + +// Live events for this session follow, sequence > 118: +← ServerEvent{ session_id: "sess_01H...", stream_delta: {target_id: "msg_7", text: "I'll fix the "}} +← ServerEvent{ session_id: "sess_01H...", stream_delta: {target_id: "msg_7", text: "off-by-one in main.go."}} +← ServerEvent{ session_id: "sess_01H...", plan_ready: { plan: { turn_id: "turn_42", @@ -62,17 +78,19 @@ A TUI frontend attaches to a session already in progress, receives a `plan_ready tool_name: "write_file", input: {"path": "main.go", "content": "...for i := 0; i <= n; i++..."}, decision: PLAN_DECISION_PENDING, + kind: TOOL_KIND_RESOURCE, risk: RISK_CLASS_LOW, + description: "Write file contents, creating or overwriting the target path.", }], }, }, } -← ServerEvent{ +← ServerEvent{ session_id: "sess_01H...", permission_request: { plan_item: { id: "item_1", tool_call_id: "tc_9", provider: "filesystem", tool_name: "write_file", decision: PLAN_DECISION_ASK }, }, } -← ServerEvent{ +← ServerEvent{ session_id: "sess_01H...", render: { content: { region: REGION_OVERLAY, @@ -89,22 +107,26 @@ A TUI frontend attaches to a session already in progress, receives a `plan_ready } // The operator reviews the diff in the overlay and corrects the fix to -// use "<=" bounded on n-1 instead, rather than accepting or rejecting outright: -→ ClientEvent{ +// use "<=" bounded on n-1 instead, rather than accepting or rejecting outright, +// and asks the kernel to remember this correction for the rest of the session: +→ ClientEvent{ session_id: "sess_01H...", plan_decision: { plan_item_id: "item_1", decision: CLIENT_DECISION_ALLOW, corrected_input: {"path": "main.go", "content": "...for i := 0; i < n-1; i++..."}, + scope: PLAN_DECISION_SCOPE_SESSION, }, } // The kernel re-validates corrected_input against write_file's input_schema // (tool/data-types.md#toolschema), accepts it, applies the write, and the // turn continues — the model sees the corrected content's tool_result on -// its next turn, per frontend-protocol.md#plan_decisioncorrected_input. +// its next turn, per frontend-protocol.md#plan_decisioncorrected_input. The +// SESSION scope means a matching future write_file call this session skips +// the ask prompt entirely, per agent-loop/plan-apply-gate.md#plandecisionscope-semantics. ``` -If a second, slower frontend also attached to `turn_42` sends its own `plan_decision` for `item_1` after the one above resolved it, the kernel rejects that second response with a `FrontendError{category: FRONTEND_ERROR_CATEGORY_INVALID_CLIENT_EVENT}` back to that frontend alone — per [`frontend-protocol.md#session-scope`](frontend-protocol.md#session-scope)'s first-response-wins rule. The first frontend's approval stands unaffected. +If a second, slower frontend also subscribed to `sess_01H...` sends its own `plan_decision` for `item_1` after the one above resolved it, the kernel rejects that second response with a `FrontendError{category: FRONTEND_ERROR_CATEGORY_INVALID_CLIENT_EVENT}` back to that frontend alone — per [`frontend-protocol.md#session-scope`](frontend-protocol.md#session-scope)'s first-response-wins rule. The first frontend's approval stands unaffected. ## A worked widget example diff --git a/docs/specifications/frontend/frontend-protocol.md b/docs/specifications/frontend/frontend-protocol.md index 3db8d54..fc9094b 100644 --- a/docs/specifications/frontend/frontend-protocol.md +++ b/docs/specifications/frontend/frontend-protocol.md @@ -1,10 +1,10 @@ # Frontend provider — protocol -The frontend provider protocol: the plugin that owns the terminal (or window, or voice channel), attaches to a session's live event stream, and turns operator input into `ClientEvent`s. See [`README.md#transport--lifecycle`](README.md#transport--lifecycle) for how this category's `Attach` shape differs from the widget provider's. +The frontend provider protocol: the plugin that owns the terminal (or window, or voice channel), sends operator input to the kernel as `ClientEvent`s over one connection-scoped stream, and receives session lifecycle and content updates back as `ServerEvent`s on the same stream. See [`README.md#transport--lifecycle`](README.md#transport--lifecycle) for how this category's `Attach` shape differs from the widget provider's. ## Transport -Subprocess + gRPC via `hashicorp/go-plugin`, per [`architecture.md`](../architecture.md#transport). A frontend provider plugin exposes three RPCs: `GetCapabilities`, `Configure`, `Attach`. +Subprocess + gRPC via `hashicorp/go-plugin`, per [`architecture.md`](../architecture.md#transport). A frontend provider plugin exposes four RPCs: `GetCapabilities`, `Configure`, `Attach`, `Describe`. **`Attach` is a bidirectional stream** — the one genuinely bidi RPC in this directory, and one of only two in the entire protocol series (the other being the kernel-callback channel). Every other category's primary RPC (`StreamCompletion`, `Invoke`) is server-streaming-plus-cancellation; the widget provider's own `Attach` ([`widget-protocol.md#transport`](widget-protocol.md#transport)) is server-streaming only despite sharing the RPC name. A frontend genuinely needs both directions live on one connection, unlike a model or tool provider: the operator can type a message while prior content is still rendering, so `ClientEvent`s and `ServerEvent`s must be able to cross in flight. @@ -13,10 +13,15 @@ service FrontendService { rpc GetCapabilities(GetCapabilitiesRequest) returns (GetCapabilitiesResponse); rpc Configure(ConfigureRequest) returns (ConfigureResponse); rpc Attach(stream ClientEvent) returns (stream ServerEvent); + rpc Describe(DescribeRequest) returns (DescribeResponse); } ``` -`GetCapabilities` returns this frontend's `slash_commands` (see [Slash commands](#slash-commands) below) and `ConfigSchema`; it MUST be cheaply re-queryable and MUST NOT require a network call, the same guarantee [`model/protocol.md#getcapabilities`](../model/protocol.md#getcapabilities) requires of a model provider. `Configure` follows the same contract as [`model/protocol.md#configure`](../model/protocol.md#configure): a config value decoded from the provider's `agent.hcl` block via the schema-to-cty bridge, rejected with a structured error at configure time rather than deferred to the first `Attach`, and never echoing a received secret back out through any event, render, or log line. +**`Attach` is connection-scoped, with per-session subscription — not one stream per session.** A frontend opens exactly one `Attach` stream per connection and keeps it open for the connection's whole lifetime; individual sessions are subscribed onto that single stream via the session-control `ClientEvent` variants ([Session lifecycle](#session-lifecycle) below), and every event on the wire — both directions — carries a top-level `session_id` field that multiplexes which session it belongs to. This is deliberate, not incidental: full session lifecycle needs connection-level operations (listing every session, the aggregate slash-command registry, creating a session before anything is attached to it) that have no natural home under a strictly per-session stream, and `.claude/rules/grpc.md`'s streaming-shape table permits exactly one genuinely bidirectional frontend RPC — multiplexing preserves that invariant rather than adding a second bidi stream or folding session control into the unrelated kernel-callback channel. + +`GetCapabilities` returns this frontend's `slash_commands` (see [Slash commands](#slash-commands) below), `ConfigSchema`, `supported_regions`, and `supported_hook_points`; it MUST be cheaply re-queryable and MUST NOT require a network call, the same guarantee [`model/protocol.md#getcapabilities`](../model/protocol.md#getcapabilities) requires of a model provider. `Configure` follows the same contract as [`model/protocol.md#configure`](../model/protocol.md#configure): a config value decoded from the provider's `agent.hcl` block via the schema-to-cty bridge, rejected with a structured error at configure time rather than deferred to the first `Attach`, and never echoing a received secret back out through any event, render, or log line. + +`Describe` reports this plugin build's own identity — `{name, version, source, category, protocol_version}` — directly from the running process, rather than the kernel inferring it from a lock-file row. Every one of the six category protocols gains this identical RPC in this protocol revision; it exists specifically for a `dev_overrides`-resolved binary, which has no `provider {}` lock-file entry to read identity from at all (`configuration/lock-file.md`'s "`dev_overrides` and identity without a lock entry"). ## Fast path vs. full render @@ -24,6 +29,18 @@ Live token-by-token text streaming (a model provider's `text_delta`, per [`model ```protobuf message ServerEvent { + // Scopes this event to a session. Set for every session-scoped variant; + // empty only for the connection-level session_list variant. + string session_id = 100; + + // Correlates a response to the ClientEvent control message that + // triggered it (echoing that message's own request_id). Set on + // session_created/session_attached/backfill_complete/session_detached/ + // session_list, and on error when it answers a control request; unset + // for ordinary live session events not triggered by a specific client + // request. + optional string request_id = 101; + oneof event { StreamDelta stream_delta = 1; Render render = 2; @@ -32,6 +49,15 @@ message ServerEvent { InteractiveRequest interactive_request = 5; SessionTreeUpdate session_tree_update = 6; Error error = 7; + SessionCreated session_created = 8; + SessionAttached session_attached = 9; + BackfillComplete backfill_complete = 10; + SessionDetached session_detached = 11; + SessionList session_list = 12; + // 13 is reserved, not assigned — see "No session deletion" below. + SlashCommandRegistry slash_command_registry = 14; + UsageUpdate usage_update = 15; + SessionStatusUpdate session_status_update = 16; } message StreamDelta { @@ -69,7 +95,7 @@ message ServerEvent { } ``` -`PermissionRequest` asks the operator to resolve a pending `ask` decision ([`agent-loop/plan-apply-gate.md`](../agent-loop/plan-apply-gate.md)): the kernel blocks that plan item's apply until a matching `ClientEvent.plan_decision` resolves it. `InteractiveRequest` carries a [`kind: interactive`](../tool/protocol.md#kind-interactive) tool call across the frontend boundary — the kernel renders the tool's own prompt content as an ordinary `RenderTree` (in the `overlay` region), and `call_id` correlates the request with the eventual `ClientEvent.interactive_response` the same way `tool_call_id` correlates an ordinary resource call and its result elsewhere in this protocol series. A frontend **MUST** render `InteractiveRequest.prompt` in the `overlay` region ([`render-tree.md#placement--regions`](render-tree.md#placement--regions)), the same visual treatment as an ordinary `ask` prompt. +`PermissionRequest` asks the operator to resolve a pending `ask` decision ([`agent-loop/plan-apply-gate.md`](../agent-loop/plan-apply-gate.md)): the kernel blocks that plan item's apply until a matching `ClientEvent.plan_decision` resolves it. `InteractiveRequest` carries a [`kind: interactive`](../tool/protocol.md#kind-interactive) tool call across the frontend boundary — the kernel renders the tool's own prompt content as an ordinary `RenderTree` (in the `overlay` region), and `call_id` correlates the request with the eventual `ClientEvent.interactive_response` the same way `tool_call_id` correlates an ordinary resource call and its result elsewhere in this protocol series. A frontend **MUST** render `InteractiveRequest.prompt` in the `overlay` region ([`render-tree.md#placement--regions`](render-tree.md#placement--regions)), the same visual treatment as an ordinary `ask` prompt. `SessionTreeUpdate` reports a **child** session's status change (a `RunSession`-spawned sub-agent); see [Session lifecycle](#session-lifecycle) below for `SessionStatusUpdate`, the parallel variant for the *attached* session's own status. ## Client events @@ -80,7 +106,18 @@ enum ClientDecision { CLIENT_DECISION_DENY = 2; } +enum PlanDecisionScope { + PLAN_DECISION_SCOPE_UNSPECIFIED = 0; + PLAN_DECISION_SCOPE_ONCE = 1; + PLAN_DECISION_SCOPE_SESSION = 2; + PLAN_DECISION_SCOPE_ALWAYS = 3; +} + message ClientEvent { + // REQUIRED for user_message..interrupt (session-scoped variants); empty + // for the connection-level control variants (hello..list_sessions). + string session_id = 100; + oneof event { UserMessage user_message = 1; SlashCommand slash_command = 2; @@ -88,10 +125,18 @@ message ClientEvent { InteractiveResponse interactive_response = 4; ActionTrigger action_trigger = 5; Interrupt interrupt = 6; + Hello hello = 7; + CreateSession create_session = 8; + AttachSession attach_session = 9; + ResumeSession resume_session = 10; + DetachSession detach_session = 11; + ListSessions list_sessions = 12; } message UserMessage { - string text = 1; + // repeated ContentBlock, not a bare string — see "UserMessage carries + // ContentBlocks" below. + repeated pluggableharness.agent.content.v1.ContentBlock content = 2; } message SlashCommand { @@ -103,6 +148,7 @@ message ClientEvent { string plan_item_id = 1; ClientDecision decision = 2; optional google.protobuf.Struct corrected_input = 3; + PlanDecisionScope scope = 4; } message InteractiveResponse { @@ -114,25 +160,67 @@ message ClientEvent { string node_id = 1; string tool_name = 2; google.protobuf.Struct args = 3; + string provider = 4; } message Interrupt {} } ``` -`ActionTrigger` is what a frontend dispatches when the operator activates a [`RenderTree`'s `ActionNode`](render-tree.md#interactive-content-the-action-node); `node_id`, `tool_name`, and `args` are echoed unchanged from the originating node. The kernel handles the resulting `action_trigger` identically to a `direct_invoke` slash command (below): the normal `Invoke`/plan-apply pipeline, including policy evaluation, with no model turn. +`ActionTrigger` is what a frontend dispatches when the operator activates a [`RenderTree`'s `ActionNode`](render-tree.md#interactive-content-the-action-node); `node_id`, `tool_name`, `args`, and `provider` are echoed unchanged from the originating node — `provider` disambiguates which provider's operation to invoke, since `tool_name` is only unique per provider. The kernel handles the resulting `action_trigger` identically to a `direct_invoke` slash command (below): the normal `Invoke`/plan-apply pipeline, including policy evaluation, with no model turn. + +### UserMessage carries ContentBlocks + +`ClientEvent.UserMessage` no longer carries a bare `string text`; it carries `repeated content.v1.ContentBlock content`, the same block vocabulary [`model/data-types.md#canonical-message--content-block-schema`](../model/data-types.md#canonical-message--content-block-schema) uses everywhere else in this protocol series. The prior string-only shape had no entry point for a pasted image: `content.v1.ImageBlock` and a model's `supports_vision` capability flag already existed, but a frontend had no way to actually *submit* one as user input. A frontend sending ordinary typed text sends a single `TextBlock`; a frontend supporting paste/attachment sends `content` with more than one block (e.g. a `TextBlock` plus an `ImageBlock`), gated the same way any other `ImageBlock` is — the kernel MUST reject an image block against a model whose `ModelSpec.supports_vision` is false, per [`model/data-types.md`](../model/data-types.md). Field 1 (the old `text` field) is `reserved`, per `.claude/rules/proto.md`'s field-number discipline — never reused. ### `plan_decision.corrected_input` `PlanDecision.corrected_input` is an opencode-style `CorrectedError` redirect: rather than a binary allow/deny, the operator can supply corrected tool arguments and have the kernel treat the item as allowed with those arguments instead of the model's originals. When present, the kernel **MUST** re-validate `corrected_input` against the tool's `input_schema` ([`tool/data-types.md#toolschema`](../tool/data-types.md#toolschema)) before treating the item as allowed — an invalid correction **MUST** be rejected back to the sending frontend as a distinct error, never silently coerced and never silently downgraded to a plain `deny`. This re-validation is mechanically part of the plan/apply gate's decision handling; see [`agent-loop/plan-apply-gate.md`](../agent-loop/plan-apply-gate.md) for where it slots into the turn algorithm — this document defines the field and the frontend-facing contract around it, the gate owns applying it. -## Session scope +`PlanDecision.scope` is orthogonal to `decision`/`corrected_input`: it says how durably this verdict is remembered, not what the verdict is. `PLAN_DECISION_SCOPE_ONCE` (the default a frontend SHOULD send absent explicit operator intent) applies only to the named item; `SESSION` applies to the rest of the current session for matching calls; `ALWAYS` asks the kernel to persist the verdict as policy, outliving the session. See [`agent-loop/plan-apply-gate.md#plandecisionscope-semantics`](../agent-loop/plan-apply-gate.md#plandecisionscope-semantics) for evaluation order and the `ALWAYS` persistence obligation — this document defines the field, the gate owns applying it, exactly as with `corrected_input` above. + +## Session lifecycle + +A frontend's single `Attach` stream subscribes and unsubscribes individual sessions via six control `ClientEvent` variants, correlated to their `ServerEvent` responses by a client-generated `request_id` the frontend chooses and the kernel echoes back unchanged on `ServerEvent.request_id`: + +| `ClientEvent` variant | Answered by | Purpose | +|---|---|---| +| `hello` | — (no response) | MAY be sent first, on stream open; asserts `protocol_version` only. Does not bind any session — attaching happens via the variants below. | +| `create_session` | `session_created` | Creates a new session under a given (or default) profile and working directory, optionally seeded with an `initial_prompt`. Auto-attaches the sending stream to the new session. | +| `attach_session` | `session_attached`, then a backfill batch, then `backfill_complete` | Subscribes an existing session (live or terminal) onto this stream. | +| `resume_session` | Same shape as `attach_session` | Attaches a historical session — see [Resume and re-open semantics](#resume-and-re-open-semantics) below for what "resume" permits beyond a plain attach. | +| `detach_session` | `session_detached` | Unsubscribes a session from this stream. Does not affect the session itself, or any other stream still attached to it. | +| `list_sessions` | `session_list` | The one connection-level (not session-scoped) control event — an optional `status`/`parent_session_id` filter and a `roots_only` flag. | + +`CreateSession`, `AttachSession`, `ResumeSession`, `DetachSession`, and `ListSessions` all carry their own `request_id`; `AttachSession`/`ResumeSession`/`DetachSession` additionally carry the target `session_id` in their own body (the top-level `ClientEvent.session_id` is empty for all six control variants, since none of them yet has — or, for `list_sessions`, ever has — a session to scope to before the response arrives). + +### Backfill = the replay path, not a new subsystem + +`architecture.md`'s "replay is just feed old events through the same Render/Paint path" governs `attach_session`/`resume_session` identically to any other replay: on either, the kernel replays the session's persisted events in `sequence` order, re-rendering each via the "supersedes" model (the historical `ProducerRef`'s own `Render`), and emits them as ordinary `ServerEvent.render` — the identical wire shape a live render uses. `stream_delta` is live-only; replayed text arrives as finished `render`s, never as deltas. + +The batch is bracketed: `SessionAttached` (carrying the session's current `SessionInfo`) opens it, the replayed `render` events follow, and `BackfillComplete { last_sequence }` closes it — the done-marker a frontend uses to know it has caught up, after which live events (`sequence > last_sequence`) continue on the same stream. **Backfill is unicast to the attaching stream only, never broadcast** to other frontends already subscribed to that session — a late joiner catching up must not re-flood everyone who was already there. + +### Session scope + +**Multiple frontends MAY subscribe to the same session concurrently**, each on its own `Attach` stream — see [`README.md#session-scope--multi-attach`](README.md#session-scope--multi-attach) for why this is the current design, not a 1:1 attachment model. The operational rules, all re-scoped **per session** now that one connection's stream carries many sessions at once: + +- **`ServerEvent`s for a given session broadcast identically to every frontend subscribed to that session.** No partitioning, no "primary" frontend — every subscribed frontend observes that session's live stream in the same order. A frontend subscribed to session A does not receive session B's events at all, regardless of how many sessions each of its peers is subscribed to. +- **`ClientEvent`s are processed in kernel arrival order, per session.** `user_message`/`slash_command`/`action_trigger`/`interrupt` have no real conflict — multiple frontends sending these for the same session just interleave as ordinary sequential input, a legitimate pairing/multi-operator scenario, not an error case. +- **`plan_decision`/`interactive_response` name a specific pending item** (`plan_item_id`/`call_id`), implicitly scoped to the session that item belongs to. **First response for a given item wins.** Any subsequent response for an already-resolved item **MUST** be rejected back to the sending frontend with a distinct `invalid_client_event`-category error (see [Error taxonomy](#error-taxonomy)), never silently dropped and never silently re-applied — so that frontend's UI can show "already decided elsewhere" rather than appearing to hang. +- Connection-level control responses (`session_list`) are naturally exempt from all of the above — they answer the one requesting stream directly, correlated by `request_id`, and are never broadcast to any other frontend. + +### Resume and re-open semantics + +`ResumeSession` targets a **historical** session — one that may be RUNNING (an ordinary late-attach, identical to `AttachSession`), or may already be terminal. What a terminal session's resume permits depends on which terminal status it's in: + +- **`SESSION_STATUS_COMPLETED` or `SESSION_STATUS_CANCELLED`** MAY be re-opened to `SESSION_STATUS_RUNNING` for new turns. A subsequent `user_message` against a re-opened session is accepted and starts a fresh turn; the session's bounds (`max_turns`, `max_budget_usd`, `max_wall_clock`) reset fresh from its originating profile — a resumed session does not inherit whatever fraction of its bounds it had already consumed before reaching its prior terminal status. The kernel MUST record this `status` transition (terminal → `RUNNING`) the same way any other status change is recorded in `session_meta` (`state-backend.md`'s "Schema migration & corruption recovery" notwithstanding — this is an ordinary in-place status update, not a schema change). +- **A bound-exhausted status (`SESSION_STATUS_ERROR_MAX_TURNS`, `SESSION_STATUS_ERROR_MAX_BUDGET_USD`, `SESSION_STATUS_ERROR_MAX_WALL_CLOCK`) or `SESSION_STATUS_FAILED`** is **replay-only** in this protocol revision. `ResumeSession` against one of these still succeeds — the kernel attaches the stream and backfills its full history exactly as for any other session — but the kernel **MUST** reject any subsequent `user_message` (or any other new-turn-inducing event) against it with `FRONTEND_ERROR_CATEGORY_SESSION_REPLAY_ONLY`, a category distinct from `SESSION_BUSY` precisely because the session isn't running — it's terminal and specifically barred from new turns, not merely occupied. + +A plain `AttachSession` (as opposed to `ResumeSession`) against a terminal session is a read-only backfill-and-watch — it never implicitly re-opens anything; only `ResumeSession` carries re-open intent, and even then only for the two statuses above. -**Multiple frontends MAY `Attach` to the same session concurrently** — see [`README.md#session-scope--multi-attach`](README.md#session-scope--multi-attach) for why this is the current design, not a 1:1 attachment model. The operational rules: +### No session deletion -- **`ServerEvent`s broadcast identically to every attached frontend.** No partitioning, no "primary" frontend — every attached frontend observes the same live stream in the same order. -- **`ClientEvent`s are processed in kernel arrival order.** `user_message`/`slash_command`/`action_trigger`/`interrupt` have no real conflict — multiple frontends sending these just interleave as ordinary sequential input, a legitimate pairing/multi-operator scenario, not an error case. -- **`plan_decision`/`interactive_response` name a specific pending item** (`plan_item_id`/`call_id`). **First response for a given item wins.** Any subsequent response for an already-resolved item **MUST** be rejected back to the sending frontend with a distinct `invalid_client_event`-category error (see [Error taxonomy](#error-taxonomy)), never silently dropped and never silently re-applied — so that frontend's UI can show "already decided elsewhere" rather than appearing to hang. +**No `DeleteSession` (or any other deletion mechanism) exists anywhere in this protocol, for any plugin category, including frontends.** This is a deliberate omission, not a gap: sessions are protected from every plugin, per `state-backend.md`'s retention posture ("no implicit expiry; pruning is an explicit operator action") — pruning a session file is a kernel CLI command an operator runs at the keyboard, never something a frontend (or any other plugin) can trigger over the wire. `ServerEvent`'s field 13 — where a `session_deleted` variant would otherwise have gone, mirroring `session_created`/`session_attached`/`session_detached`'s numbering — is `reserved`, not assigned, specifically so that number is never silently repurposed for something unrelated if frontend-triggered deletion is ever reconsidered by a future protocol revision. ## Slash commands @@ -154,12 +242,12 @@ message SlashCommandSpec { // MUST name one of this SAME provider's own // tool operations optional string template = 5; // MUST be set iff dispatch == DISPATCH_PROMPT_EXPANSION; - // "{arg}"-style placeholders substituted from - // the operator's typed arguments + // "{arg}"-style placeholders substituted from + // the operator's typed arguments } ``` -A name collision across providers **MUST** be a config-load-time error, per this protocol series' established "ambiguity is an error, not a silent pick" pattern. +A name collision across providers **MUST** be a config-load-time error, per this protocol series' established "ambiguity is an error, not a silent pick" pattern. The kernel aggregates every loaded provider's declared commands into one profile-scoped `SlashCommandRegistry`, sent to an attaching frontend as part of `session_attached` and again whenever the registry changes (a plugin reload, a config change) — a frontend does not need to separately call every category's `GetCapabilities`/`GetSchema` and merge the results itself. - **`DISPATCH_DIRECT_INVOKE`**: the frontend recognizes `/name args`, maps `args` to the named tool's `input_schema`, and dispatches it through the *normal* `Invoke`/plan-apply pipeline ([`agent-loop/plan-apply-gate.md`](../agent-loop/plan-apply-gate.md)) — including policy evaluation — with **no model turn**. This is a real behavior difference from an ordinary tool call: the model never sees or decides on this invocation, only its eventual result (appended to history as an ordinary `tool_result`, so the model has full visibility on the *next* turn even though it didn't initiate this one). - **`DISPATCH_PROMPT_EXPANSION`**: the frontend expands `template` with the typed arguments and submits the result as an ordinary `ClientEvent.user_message` — this costs a model turn like any normal message; the only thing the slash command bought was not having to type the full instruction out. @@ -174,6 +262,11 @@ enum FrontendErrorCategory { FRONTEND_ERROR_CATEGORY_INVALID_CLIENT_EVENT = 2; FRONTEND_ERROR_CATEGORY_REGION_UNSUPPORTED = 3; FRONTEND_ERROR_CATEGORY_UNKNOWN = 4; + FRONTEND_ERROR_CATEGORY_SESSION_NOT_FOUND = 5; + FRONTEND_ERROR_CATEGORY_SESSION_CREATE_FAILED = 6; + FRONTEND_ERROR_CATEGORY_SESSION_BUSY = 7; + FRONTEND_ERROR_CATEGORY_SCHEMA_TOO_NEW = 8; + FRONTEND_ERROR_CATEGORY_SESSION_REPLAY_ONLY = 9; } message FrontendError { @@ -185,8 +278,13 @@ message FrontendError { | Category | Meaning | Requirement | |---|---|---| | `render_failed` | A specific `RenderTree` node couldn't be painted (e.g. a malformed `diff`). | MUST fall back to a generic text rendering of whatever content is recoverable; MUST NOT crash the frontend process over one bad node. | -| `invalid_client_event` | Malformed input on the operator-facing side — including a `plan_decision`/`interactive_response` naming an already-resolved item (see [Session scope](#session-scope)). | Rare in the ordinary case, since the frontend itself constructs `ClientEvent`s; MUST be surfaced distinctly, not collapsed into `unknown`. | +| `invalid_client_event` | Malformed input on the operator-facing side — including a `plan_decision`/`interactive_response` naming an already-resolved item (see [Session scope](#session-scope)), or a session-scoped variant arriving with an empty `session_id`. | Rare in the ordinary case, since the frontend itself constructs `ClientEvent`s; MUST be surfaced distinctly, not collapsed into `unknown`. | | `region_unsupported` | A producer targeted a `Region` this frontend has no fallback behavior for at all. | SHOULD be logged; MUST NOT be treated as fatal. | +| `session_not_found` | `attach_session`, `resume_session`, `detach_session`, or `list_sessions`' `parent_session_id` filter named a `session_id` the kernel has no record of. | MUST be surfaced distinctly, correlated by `request_id`. | +| `session_create_failed` | `create_session` failed — an invalid profile, or an unusable working directory. | MUST be surfaced distinctly, correlated by `request_id`. | +| `session_busy` | Reserved for a future session-mutating control event that conflicts with a `RUNNING` session. No variant in this protocol revision currently triggers it. | — | +| `schema_too_new` | `resume_session` named a session file with a `PRAGMA user_version` newer than this kernel understands (`state-backend.md`'s "Schema migration"). | MUST be surfaced distinctly; the kernel MUST refuse to open the file, per `state-backend.md`. | +| `session_replay_only` | A new-turn-inducing event (`user_message`, ...) targeted a session attached replay-only — see [Resume and re-open semantics](#resume-and-re-open-semantics). | MUST be surfaced distinctly, never silently ignored. | | `unknown` | Anything else. | Structured error taxonomy applies here as everywhere else in this protocol series — see [`conformance.md`](conformance.md#error-taxonomy). | -`ConfigureResponse` errors surface as a gRPC status carrying a `FrontendError` in its structured detail, not as an in-band field on the response message. Errors encountered mid-`Attach` surface as `ServerEvent.error`. +`ConfigureResponse` errors surface as a gRPC status carrying a `FrontendError` in its structured detail, not as an in-band field on the response message. Errors encountered mid-`Attach` surface as `ServerEvent.error`, carrying `request_id` when they answer a specific control event. diff --git a/docs/specifications/frontend/render-tree.md b/docs/specifications/frontend/render-tree.md index e0e826c..b1c4c2f 100644 --- a/docs/specifications/frontend/render-tree.md +++ b/docs/specifications/frontend/render-tree.md @@ -94,10 +94,11 @@ message ActionNode { string label = 2; string tool_name = 3; google.protobuf.Struct args = 4; + string provider = 5; } ``` -A frontend rendering an `ActionNode` **MUST** make it interactive (a clickable button, a keybindable list item, whatever fits its own UI) and, on activation, **MUST** dispatch a `ClientEvent.action_trigger` carrying that node's `tool_name`/`args` unchanged ([`frontend-protocol.md#client-events`](frontend-protocol.md#client-events)). The kernel then handles the resulting `action_trigger` **identically to a `direct_invoke` slash command** ([`frontend-protocol.md#slash-commands`](frontend-protocol.md#slash-commands)): the normal `Invoke`/plan-apply pipeline, including policy evaluation, with no model turn. No action-specific dispatch mechanism exists beyond this — `action` nodes are a second way to *reach* the same dispatch path slash commands already use, via a click instead of typed text. +`provider` is the declared name of the tool provider plugin `tool_name` belongs to — `tool_name` is only unique *per provider*, not globally, matching the same naming precedent already established by [`plan/protocol`](../agent-loop/plan-apply-gate.md)'s `PlanItem.provider`. A frontend rendering an `ActionNode` **MUST** make it interactive (a clickable button, a keybindable list item, whatever fits its own UI) and, on activation, **MUST** dispatch a `ClientEvent.action_trigger` carrying that node's `tool_name`/`args`/`provider` unchanged ([`frontend-protocol.md#client-events`](frontend-protocol.md#client-events)). The kernel then handles the resulting `action_trigger` **identically to a `direct_invoke` slash command** ([`frontend-protocol.md#slash-commands`](frontend-protocol.md#slash-commands)): the normal `Invoke`/plan-apply pipeline, including policy evaluation, with no model turn. No action-specific dispatch mechanism exists beyond this — `action` nodes are a second way to *reach* the same dispatch path slash commands already use, via a click instead of typed text. This generalizes past widgets for free, since any producer's `Render` output can include an `ActionNode`, not only a widget's: a `tool_result` diff could include an action offering "undo this change," a memory record could offer "forget this," and so on. Widgets are simply the category that motivated adding it — see [`widget-protocol.md#interactive-widgets`](widget-protocol.md#interactive-widgets) for the widget-specific angle. @@ -133,3 +134,13 @@ message PlacedContent { - **`overlay`** is for content that should visually interrupt (a modal confirmation, an inline `ask`-decision prompt per [`agent-loop/plan-apply-gate.md`](../agent-loop/plan-apply-gate.md)) — a frontend **MUST** render `overlay` content in a way that's visually distinct from ambient `main_chat`/`sidebar` content, even if the specific implementation (a floating pane, a full-screen takeover) is its own choice. A frontend that lacks a given region (e.g. a plain line-based CLI with no sidebar) MAY silently drop `PlacedContent` targeting it, or fold it into another region as a fallback (e.g. sidebar content appended to `main_chat` instead) — placement is always a hint the frontend is free to reinterpret for its own layout, never a mandate the producer can rely on being honored literally. Multiple producers MAY target the same region with `replace: true`; the frontend orders/allocates space among them by `priority` rather than treating the region as a single-writer slot — coexistence, not exclusivity, is the default, and no config-load-time conflict is raised over two producers sharing a region. + +## Schema versioning + +Every category's own local `RenderRequest` message (`model.proto`, `tool.proto`, `context.proto`, `memory.proto` each declare their own — see each category's `protocol.md#render`) carries a `string schema_version = 2;` field alongside the opaque `payload` this document's package comment and `.claude/rules/grpc.md`'s Emit/Render carve-out already describe. This section is the canonical, single definition of what that string means; every category's `Render` section links back here rather than re-explaining it. + +`schema_version` is round-tripped, not invented at `Render` time. A producer sets it once, at emit time, on [`kernel-callbacks.md#emit`](../kernel-callbacks.md#emit)'s `EmitRequest.schema_version` — "versions the shape of `payload`" — and the kernel persists it verbatim alongside that event's opaque `payload` byte-for-byte, per [`state-backend.md`](../state-backend.md)'s `events.schema_version` column. When the kernel later calls that category's `Render` RPC (live, or as part of a historical replay), it passes the *same* string back on `RenderRequest.schema_version` — the value is never recomputed, guessed, or defaulted by the kernel at render time, only threaded through unchanged from what the producer originally declared. + +This is what makes the "supersedes" replay model ([`architecture.md#versioning--schema-drift--supersedes`](../architecture.md#versioning--schema-drift--supersedes)) actually work across a payload-shape change, not just a plugin-version change: a plugin build MAY change what bytes its opaque `payload` contains between releases (a genuine schema evolution, not merely a bug fix), and `schema_version` is how that same build's `Render` implementation tells old-shaped payloads apart from new-shaped ones without having to sniff the bytes. A plugin's `Render` implementation MUST branch on `schema_version` (not attempt to auto-detect a payload's shape from its bytes) whenever it has ever changed its `payload` shape across a released version, and MUST continue to decode every `schema_version` it has ever emitted for as long as any retained session references it — the same permanence guarantee `.claude/rules/proto.md`'s wire-compatibility rule already places on the proto message shapes themselves, applied one level down to the opaque bytes those messages carry. + +`schema_version` is a plain string, not an integer or a proto package version — a producer is free to use whatever scheme fits its own release cadence (`"1"`, `"2024-03-shape"`, semver, ...); the kernel and this protocol series never parse or compare it, only store and echo it back. A producer that has never changed its payload shape MAY use a single constant value indefinitely; there is no requirement to bump it on every release, only on a release that actually changes what `payload`'s bytes mean. diff --git a/docs/specifications/frontend/widget-protocol.md b/docs/specifications/frontend/widget-protocol.md index 9304d04..87bd300 100644 --- a/docs/specifications/frontend/widget-protocol.md +++ b/docs/specifications/frontend/widget-protocol.md @@ -4,29 +4,35 @@ The widget provider protocol: a plugin that contributes content *into* whichever ## Transport -Subprocess + gRPC via `hashicorp/go-plugin`. A widget provider plugin exposes three RPCs: `GetCapabilities`, `Configure`, `Attach`. +Subprocess + gRPC via `hashicorp/go-plugin`. A widget provider plugin exposes four RPCs: `GetCapabilities`, `Configure`, `Attach`, `Describe`. -**Unlike the frontend provider's bidirectional `Attach` ([`frontend-protocol.md#transport`](frontend-protocol.md#transport)), this `Attach` is server-streaming only.** Widgets are passive/display-only in v1 — a widget wanting to trigger an action (not just display state) does so by *also* being a tool provider with a slash command ([`frontend-protocol.md#slash-commands`](frontend-protocol.md#slash-commands)), not through this channel. Sharing the RPC name `Attach` with the frontend protocol while having a genuinely different streaming shape is a real gotcha worth stating plainly: **frontend `Attach` is bidi, widget `Attach` is not.** +**Unlike the frontend provider's bidirectional, connection-scoped `Attach` ([`frontend-protocol.md#transport`](frontend-protocol.md#transport)), this `Attach` is server-streaming only and stays session-scoped** (one call per session, per `AttachRequest.session_id`, not multiplexed across sessions on one connection). Widgets are passive/display-only in v1 — a widget wanting to trigger an action (not just display state) does so by *also* being a tool provider with a slash command ([`frontend-protocol.md#slash-commands`](frontend-protocol.md#slash-commands)), not through this channel. Sharing the RPC name `Attach` with the frontend protocol while having a genuinely different streaming shape is a real gotcha worth stating plainly: **frontend `Attach` is bidi and connection-multiplexed, widget `Attach` is neither.** ```protobuf service WidgetService { rpc GetCapabilities(GetCapabilitiesRequest) returns (GetCapabilitiesResponse); rpc Configure(ConfigureRequest) returns (ConfigureResponse); rpc Attach(AttachRequest) returns (stream WidgetUpdate); + rpc Describe(DescribeRequest) returns (DescribeResponse); } ``` -`GetCapabilities` **MUST** be cheaply re-queryable and **MUST NOT** require a network call, the same guarantee every other category's capability RPC carries. It returns which regions this widget intends to contribute to and its config schema: +`GetCapabilities` **MUST** be cheaply re-queryable and **MUST NOT** require a network call, the same guarantee every other category's capability RPC carries. It returns which regions this widget intends to contribute to, its config schema, and which hook points it can subscribe to: ```protobuf message WidgetCapabilities { repeated pluggableharness.agent.render.v1.Region regions = 1; // MUST — see render-tree.md#placement--regions pluggableharness.agent.config.v1.ConfigSchema config_schema = 2; + repeated pluggableharness.agent.common.v1.HookPoint supported_hook_points = 3; } ``` +`supported_hook_points` lets the kernel reject an `agent.hcl` `hook{}` block naming a point this widget can't actually serve at config-load time, rather than discovering the mismatch at first dispatch — the same "ambiguity/misconfig is a load-time error" pattern this protocol series applies everywhere else. + `Configure` follows the same contract as [`model/protocol.md#configure`](../model/protocol.md#configure): config decoded from the widget's `agent.hcl` block, rejected with a structured error at configure time rather than deferred, never echoing a received secret back out. +`Describe` reports this plugin build's own identity — `{name, version, source, category, protocol_version}` — directly from the running process, rather than the kernel inferring it from a lock-file row. Every one of the six category protocols gains this identical RPC in this protocol revision; it exists specifically for a `dev_overrides`-resolved binary, which has no `provider {}` lock-file entry to read identity from at all (`configuration/lock-file.md`'s "`dev_overrides` and identity without a lock entry"). + `Attach` opens a server-streaming feed of this widget's rendered updates for one session: ```protobuf @@ -57,3 +63,23 @@ A widget provider gets no special session-state API. It is implicitly available A widget's `WidgetUpdate.content` **MAY** include [`ActionNode`s](render-tree.md#interactive-content-the-action-node) the same way any other `RenderTree` can — no widget-specific protocol addition was needed beyond the general `action` node mechanism ([`render-tree.md#interactive-content-the-action-node`](render-tree.md#interactive-content-the-action-node)). A clickable sidebar item (a widget offering "dismiss," "retry," or "open in editor," for example) is expressed exactly like an action node contributed by any other producer: the frontend renders it interactive, and on activation dispatches `ClientEvent.action_trigger` ([`frontend-protocol.md#client-events`](frontend-protocol.md#client-events)), which the kernel handles identically to a `direct_invoke` slash command — the normal `Invoke`/plan-apply pipeline, including policy evaluation, with no model turn. This was the specific gap that motivated adding `ActionNode` to [`render-tree.md`](render-tree.md) in the first place — widgets needed a way to trigger something, not just display state — but the resulting mechanism generalizes past widgets: any producer's rendered content can offer a one-click follow-up action, not only widget-contributed panels. + +## Error taxonomy + +The widget provider category has a structured error type, `WidgetError`, mirroring `FrontendError`'s shape ([`frontend-protocol.md#error-taxonomy`](frontend-protocol.md#error-taxonomy)) — this resolves [`conformance.md`](conformance.md#error-taxonomy)'s prior open question of whether widgets needed a categorized, in-band error channel of their own: + +```protobuf +enum WidgetErrorCategory { + WIDGET_ERROR_CATEGORY_UNSPECIFIED = 0; + WIDGET_ERROR_CATEGORY_RENDER_FAILED = 1; + WIDGET_ERROR_CATEGORY_REGION_UNSUPPORTED = 2; + WIDGET_ERROR_CATEGORY_UNKNOWN = 3; +} + +message WidgetError { + WidgetErrorCategory category = 1; + string message = 2; +} +``` + +Unlike the frontend category — whose `Attach` errors surface in-band via `ServerEvent.error`, since `Attach` is a long-lived bidirectional stream where tearing down the connection over one recoverable error would be disruptive — widget `Attach` is server-streaming only, with no return channel besides the stream itself. `WidgetError` is therefore carried in the structured detail of a gRPC status on `Configure` or `Attach`, mapped per the canonical `codes` table (`.claude/rules/grpc.md`): `codes.InvalidArgument` for a malformed config value or a render this widget can't produce, `codes.Internal` for anything unmapped, `codes.Canceled` for ordinary stream cancellation (never an error). A widget encountering a partial-failure condition (e.g. "this update renders for `top_bar` but not `sidebar`") reports it the same way: a `WidgetError{category: WIDGET_ERROR_CATEGORY_REGION_UNSUPPORTED}` in a gRPC status detail, since there is no in-band error variant on `WidgetUpdate` itself. diff --git a/docs/specifications/memory/README.md b/docs/specifications/memory/README.md index 4e32384..f65f6a0 100644 --- a/docs/specifications/memory/README.md +++ b/docs/specifications/memory/README.md @@ -12,13 +12,13 @@ The design draws on patterns seen across coding harnesses — automatic session Subprocess + gRPC via `hashicorp/go-plugin`, per [`architecture.md`](../architecture.md#transport) — the standard handshake applies uniformly across all six provider categories and isn't repeated here. -A memory provider plugin exposes six RPCs: `GetCapabilities`, `Configure`, `Recall`, `Record`, `UpdateRecord`, `DeleteRecord`. It MAY additionally implement `ApproveRecord`/`RejectRecord` (the optional ratification pattern, [`protocol.md#ratification-optional`](protocol.md#ratification-optional)) and `Render` ([`protocol.md#render`](protocol.md#render)). All seven/nine RPCs are unary — unlike the model provider's `StreamCompletion` or the tool provider's `Invoke`, nothing in this category streams. +A memory provider plugin exposes nine RPCs: `GetCapabilities`, `Configure`, `Recall`, `Record`, `UpdateRecord`, `DeleteRecord`, `ListRecords`, `GetRecord`, `Describe`. It MAY additionally implement `ApproveRecord`/`RejectRecord` (the optional ratification pattern, [`protocol.md#ratification-optional`](protocol.md#ratification-optional)) and `Render` ([`protocol.md#render`](protocol.md#render)). All eleven RPCs are unary — unlike the model provider's `StreamCompletion` or the tool provider's `Invoke`, nothing in this category streams. **A plugin process MAY implement more than one provider-category protocol.** The reference memory provider ([`examples.md#write-triggers-reference-tools`](examples.md#write-triggers-reference-tools)) implements both this protocol and registers as a tool provider (per [`tool/README.md`](../tool/README.md)) for `memory.remember`/`memory.forget`/ `memory.search`, in the same process, with the tool's `Invoke` calling directly into its own `Record` method — no cross-plugin RPC needed. Nothing in this protocol prohibits that; it is simply the natural shape once a category's read/write RPCs and a tool-shaped trigger for the write side turn out to belong to the same plugin. ## Category structure -- [`protocol.md`](protocol.md) — the RPCs: `GetCapabilities`, `Configure`, `Recall`, `Record`/`UpdateRecord`/`DeleteRecord`, `ApproveRecord`/`RejectRecord`, `Render`. +- [`protocol.md`](protocol.md) — the RPCs: `GetCapabilities`, `Configure`, `Recall`, `Record`/`UpdateRecord`/`DeleteRecord`, `ListRecords`/`GetRecord`, `ApproveRecord`/`RejectRecord`, `Render`, `Describe`. - [`data-types.md`](data-types.md) — `MemoryCapabilities`, the `MemoryScope` and `MemoryType` enums, `RecallRequest`/`MemoryRecord`, the write-side request/result types, and the `MemoryError` taxonomy. - [`taxonomy.md`](taxonomy.md) — the fixed record taxonomy in full: what each `MemoryType` means, how it interacts with `MemoryScope`, and why both are fixed at the protocol level rather than left to each provider. - [`examples.md`](examples.md) — an illustrative wire-format excerpt of the service definition, a worked `Recall`/`Record` sequence, a worked `[[name]]` cross-reference example, and the write-triggers table (autonomous hook-driven vs. explicit model-invoked reference tools). diff --git a/docs/specifications/memory/conformance.md b/docs/specifications/memory/conformance.md index c8ac592..c0518cb 100644 --- a/docs/specifications/memory/conformance.md +++ b/docs/specifications/memory/conformance.md @@ -12,16 +12,23 @@ A plugin MUST classify every failure into one of the following `MemoryErrorCateg | `budget_exceeded` | `Recall`'s candidate records exceed `token_budget` even after the provider's own truncation | Same MUST-self-truncate principle as the context provider's budget handling; surface as a context-assembly failure, not a generic error | | `source_unavailable` | This provider's backend storage was unreachable at call time | Retry candidate — transient by nature (a file lock, a down remote service) | | `unknown` | Anything else | MUST include enough detail for debugging; treat as non-retryable by default | +| `invalid_scope` | `Record` specified a `MemoryScope` this provider doesn't support (absent from `GetCapabilities.supported_scopes`) | MUST NOT retry as-is; the caller (or kernel routing) picked the wrong provider for this scope — the scope-taxonomy mirror of `invalid_type` | `MemoryError` MUST include `category` (above), `message` (human-readable), and `retryable` (bool). -On the wire, each category maps to a gRPC status code: `not_found` → `NotFound`, `invalid_type` → `InvalidArgument`, `ratification_unsupported` → `FailedPrecondition`, `budget_exceeded` → `ResourceExhausted`, `source_unavailable` → `Unavailable`, `unknown` → `Internal`, never `Unknown`. +On the wire, each category maps to a gRPC status code: `not_found` → `NotFound`, `invalid_type` → `InvalidArgument`, `ratification_unsupported` → `FailedPrecondition`, `budget_exceeded` → `ResourceExhausted`, `source_unavailable` → `Unavailable`, `unknown` → `Internal`, `invalid_scope` → `InvalidArgument` (same mapping as `invalid_type`), never `Unknown`. ## Required vs. optional support — summary matrix | Capability | Level | Notes | |---|---|---| -| `GetCapabilities`/`Configure`/`Recall`/`Record`/`UpdateRecord`/`DeleteRecord` RPCs | MUST | the core protocol surface | +| `GetCapabilities`/`Configure`/`Recall`/`Record`/`UpdateRecord`/`DeleteRecord`/`ListRecords`/`GetRecord`/`Describe` RPCs | MUST | the core protocol surface | +| `ListRecords`/`GetRecord` enumeration/audit path, `PENDING` listable without a gate | MUST | [`protocol.md#listrecords--getrecord`](protocol.md#listrecords--getrecord) | +| `GetRecord` fails `not_found` on unknown `id` | MUST | same section | +| `Describe` reports this build's own `common.v1.ProducerRef` identity | MUST | [`protocol.md#describe`](protocol.md#describe), [`configuration/lock-file.md`](../configuration/lock-file.md#dev_overrides-and-identity-without-a-lock-entry) | +| `MemoryRecord.provenance` kernel-populated, immutable | MUST | [`data-types.md#provenance`](data-types.md#provenance) | +| `MemoryRecord.relevance_score` normalized to `[0, 1]`, Recall/ListRecords-only, never persisted | MUST, when set | [`data-types.md#relevance_score`](data-types.md#relevance_score) | +| `RecallRequest.turn_id` as ULID | MUST | [`data-types.md#recallrequest--memoryrecord`](data-types.md#recallrequest--memoryrecord) | | Fixed `MemoryType` taxonomy (user/feedback/project/reference) | MUST | [`taxonomy.md`](taxonomy.md) — protocol-level, not provider-defined | | Fixed `MemoryScope` taxonomy (session/project/global) | MUST | [`data-types.md#memoryscope`](data-types.md#memoryscope) | | Record type and scope immutable after creation | MUST | [`taxonomy.md`](taxonomy.md), [`data-types.md#memoryscope`](data-types.md#memoryscope) | @@ -35,13 +42,14 @@ On the wire, each category maps to a gRPC status code: `not_found` → `NotFound | `memory.remember` fuzzy near-match check before creating a new record | MUST | [`protocol.md#write-triggers`](protocol.md#write-triggers) | | `ApproveRecord`/`RejectRecord` | MAY, standardized shape if implemented | [`protocol.md#ratification-optional`](protocol.md#ratification-optional) | | `status: pending` ever returned | MUST NOT, unless `ratification_supported: true` | same section | -| Autonomous write via `post-response`/`session-end` hooks | SHOULD | [`examples.md#autonomous-hook-driven`](examples.md#autonomous-hook-driven) | +| Autonomous write via `post-model-response`/`session-end` hooks (`hook.v1.DispatchHook`) | SHOULD | [`examples.md#autonomous-hook-driven`](examples.md#autonomous-hook-driven), [`protocol.md#write-triggers`](protocol.md#write-triggers) | | `memory.remember`/`memory.forget`/`memory.search` reference tools | SHOULD (reference implementation) | [`examples.md#explicit-model-invoked`](examples.md#explicit-model-invoked) | | Multi-protocol plugin (memory + tool provider in one process) | MAY | [`README.md`](README.md#transport--lifecycle) | | Structured error taxonomy (above) | MUST | | | `Render` | MAY | generic fallback exists | | `MemoryRecord.tokens` computed via `CountTokens` kernel callback | MUST | [`kernel-callbacks.md#counttokens`](../kernel-callbacks.md#counttokens); never a provider-local heuristic | | `RecallRequest.model_target` set | MUST | [`data-types.md#recallrequest--memoryrecord`](data-types.md#recallrequest--memoryrecord) | +| `supported_hook_points` declaration | MAY | empty unless this provider also declares `hook{}` blocks in `agent.hcl` beyond the implicit `post-model-response`/`session-end` subscriptions | ## Open questions diff --git a/docs/specifications/memory/data-types.md b/docs/specifications/memory/data-types.md index 382b35f..5531fe6 100644 --- a/docs/specifications/memory/data-types.md +++ b/docs/specifications/memory/data-types.md @@ -13,6 +13,9 @@ MemoryCapabilities { ratification_supported bool // MUST, default false — see protocol.md#ratification-optional slash_commands []SlashCommandSpec // MAY — see frontend/README.md#slash-commands config_schema ConfigSchema // MUST — decoded per configuration/blocks-reference.md + supported_hook_points []common.v1.HookPoint // MAY be empty — which hook points this + // provider subscribes DispatchHook to, + // see protocol.md#write-triggers } ``` @@ -41,7 +44,9 @@ Fixed at the protocol level, not provider-defined. Full definitions, rationale, ```protobuf RecallRequest { session_id string - turn_number int + turn_id string // ULID, standardized across the whole + // protocol — same treatment as + // context.v1's ContextRequest.turn_id token_budget int // MUST — resolved the same way a context // provider's cap is resolved; memory recall // competes for the SAME budget pool @@ -75,9 +80,32 @@ MemoryRecord { links []string // MUST — record IDs this record references, kernel-parsed // from "[[name]]" syntax; see protocol.md#structural-name-cross-reference-links created_at, updated_at timestamp + provenance Provenance // kernel-populated at Record time, immutable — see + // #provenance below + relevance_score double? // [0, 1], set ONLY on Recall/ListRecords responses, + // never persisted — see #relevance_score below } ``` +## `Provenance` + +```protobuf +Provenance { + source_session_id string // the session that produced this record + source_turn_id string? // the turn (ULID) within source_session_id that + // produced this record, when known + recorded_by string // producing plugin's declared name, or the + // reference tool path that wrote it (e.g. + // "memory.remember") +} +``` + +Kernel-populated at `Record` time and immutable thereafter — never provider-supplied, never mutated by `UpdateRecord`. The memory category's [`README.md`](README.md) already frames provenance as a first-class concern; `MemoryRecord.provenance` is where the record shape finally carries it, so any consumer (a ratification review UI, an audit trail) has one place to look rather than reconstructing "who wrote this" from write-time logs. + +## `relevance_score` + +`MemoryRecord.relevance_score` is this record's recall-time relevance, in `[0, 1]`. It is set only on `Recall` and `ListRecords` responses — never on a `Record`/`UpdateRecord` request or response, and never persisted alongside the record itself. It exists so the kernel can merge multiple memory providers' results under one shared `token_budget` using a comparable figure, rather than relying on each provider's internal, incomparable ordering. A provider that sets `relevance_score` MUST normalize it to `[0, 1]` — scores from two different providers are only meaningfully comparable if both normalize to the same range; a provider that doesn't compute a meaningful relevance figure SHOULD leave the field unset rather than fabricating a value. + `RecallRequest.token_budget` exceeded by the candidate record set even after this provider's own truncation is a `budget_exceeded` error — see [`#memoryerror`](#memoryerror) below. ## The write side @@ -111,6 +139,33 @@ DeleteResult { deleted bool } `RecordResult` is a reusable domain type shared across `Record`'s, `UpdateRecord`'s, and `ApproveRecord`'s responses (each keeps its own per-RPC response message wrapping the same `RecordResult` shape, not a literally-shared RPC response type). `DeleteResult` is the equivalent reusable shape for `DeleteRecord` and `RejectRecord`. +## `ListRecords` / `GetRecord` + +```protobuf +ListRecordsRequest { + type_filter []MemoryType // MAY be empty = all supported types + scope_filter []MemoryScope // MAY be empty = all scopes this provider supports + status_filter RecordStatus? // unset = both canonical and pending are eligible — + // PENDING records ARE listable here, unlike Recall's + // include_pending gate; see protocol.md#listrecords--getrecord + page_size int + page_token string // opaque continuation token; empty on the first page +} + +ListRecordsResponse { + records []MemoryRecord // this page's records + next_page_token string // empty when this is the last page +} + +GetRecordRequest { id string } + +GetRecordResponse { + record MemoryRecord +} +``` + +`GetRecord` MUST fail with a `MemoryError{category: not_found}` for an unknown `id`, the same convention as `UpdateRecord`/`DeleteRecord`/`ApproveRecord`/`RejectRecord`. + ## `MemoryError` ```protobuf @@ -132,6 +187,9 @@ MemoryErrorCategory = enum { // budget handling source_unavailable // backend storage unreachable at call time unknown + invalid_scope // Record specified a MemoryScope this provider doesn't + // support (absent from GetCapabilities.supported_scopes) — + // the scope-taxonomy mirror of invalid_type above } ``` diff --git a/docs/specifications/memory/examples.md b/docs/specifications/memory/examples.md index 3c03977..8dec8da 100644 --- a/docs/specifications/memory/examples.md +++ b/docs/specifications/memory/examples.md @@ -48,6 +48,20 @@ service MemoryService { // review-inbox view distinct from ordinary recall), in place of the // kernel's generic fallback. MAY be implemented. Unary. rpc Render(RenderRequest) returns (RenderResponse); + + // ListRecords is the enumeration/audit path: paginated browsing, + // filterable by type/scope/status. PENDING records ARE listable here, + // unlike Recall's include_pending gate. MUST be implemented. Unary. + rpc ListRecords(ListRecordsRequest) returns (ListRecordsResponse); + + // GetRecord fetches exactly one record by id. MUST fail with a + // structured MemoryError for an unknown id. MUST be implemented. Unary. + rpc GetRecord(GetRecordRequest) returns (GetRecordResponse); + + // Describe reports this plugin build's own identity via + // common.v1.ProducerRef, independent of any lock-file entry. MUST be + // implemented. Unary. + rpc Describe(DescribeRequest) returns (DescribeResponse); } enum MemoryType { @@ -74,7 +88,7 @@ A session working in a project recalls memory at `context-assemble`, then later ```text → RecallRequest{ - session_id: "sess_042", turn_number: 3, + session_id: "sess_042", turn_id: "turn_03", token_budget: 1500, model_target: { id: "claude-opus-5", context_window: 500000, effective_ceiling: 480000 }, working_directory: "/home/user/code/acme-widgets", @@ -85,10 +99,16 @@ A session working in a project recalls memory at `context-assemble`, then later ← RecallResponse{ records: [ { id: "user-role", type: MEMORY_TYPE_USER, scope: MEMORY_SCOPE_GLOBAL, - title: "Operator role", tokens: 42, status: canonical, links: [] }, + title: "Operator role", tokens: 42, status: canonical, links: [], + relevance_score: 0.91, + provenance: { source_session_id: "sess_001", source_turn_id: "turn_07", + recorded_by: "memory.remember" } }, { id: "deploy-pipeline-migration-in-progress", type: MEMORY_TYPE_PROJECT, scope: MEMORY_SCOPE_PROJECT, title: "Migrating the release pipeline to the new deploy tool", - tokens: 88, status: canonical, links: ["deploy-pipeline-runbook"] }, + tokens: 88, status: canonical, links: ["deploy-pipeline-runbook"], + relevance_score: 0.74, + provenance: { source_session_id: "sess_038", source_turn_id: "turn_12", + recorded_by: "memory.remember" } }, ], } @@ -107,6 +127,8 @@ A session working in a project recalls memory at `context-assemble`, then later The kernel adapts each `RecallResponse` record into a `ContextSection` before merging it into the assembled prompt — see [`protocol.md#kernel-side-translation-into-context-assembly`](protocol.md#kernel-side-translation-into-context-assembly). +Both returned records above carry `relevance_score` (this provider's own normalized `[0, 1]` estimate, letting the kernel compare candidates across multiple memory providers under one shared budget) and `provenance` (kernel-populated at write time, immutable — here showing both records were originally written via `memory.remember` in earlier sessions, not the current one). Neither field appears on the `RecordRequest`/`RecordResponse` pair below: `relevance_score` is Recall/ListRecords-only and never persisted, and `provenance` is entirely kernel-populated rather than something a write-side caller supplies. + ## A worked cross-reference example Two records, one referencing the other via `[[name]]` syntax in its content: @@ -139,7 +161,7 @@ Both an autonomous, hook-driven mechanism and an explicit, model-invoked mechani ### Autonomous, hook-driven -A memory provider is implicitly subscribed to `post-response` (`observe` mode, fires every turn) and `session-end` (fires once). Nothing in the protocol prescribes *when* within that stream a provider decides to call its own internal write logic — a "10+ message session" heuristic is a candidate pattern a reference implementation might use, not a protocol requirement. `session-end` firing unconditionally gives every provider a guaranteed last chance to persist something even if its own turn-by-turn heuristic never triggered mid-session. +A memory provider is implicitly subscribed to `post-model-response` (`observe` mode, fires every turn) and `session-end` (fires once), both delivered over `hook.v1.HookSubscriberService.DispatchHook` — see [`protocol.md#write-triggers`](protocol.md#write-triggers). Nothing in the protocol prescribes *when* within that stream a provider decides to call its own internal write logic — a "10+ message session" heuristic is a candidate pattern a reference implementation might use, not a protocol requirement. `session-end` firing unconditionally gives every provider a guaranteed last chance to persist something even if its own turn-by-turn heuristic never triggered mid-session. ### Explicit, model-invoked diff --git a/docs/specifications/memory/protocol.md b/docs/specifications/memory/protocol.md index 18b6112..734d0e2 100644 --- a/docs/specifications/memory/protocol.md +++ b/docs/specifications/memory/protocol.md @@ -18,6 +18,10 @@ Fires at the same `context-assemble` hook point context providers fire at — me `token_budget` MUST be resolved the same way a context provider's cap is resolved — memory recall competes for the **same** budget pool as context providers, not a separate reserved pool. A `Recall` call whose candidate records still exceed `token_budget` after the provider's own truncation MUST fail with `budget_exceeded` ([`data-types.md#memoryerror`](data-types.md#memoryerror)), the same MUST-self-truncate principle [`context/protocol.md`](../context/protocol.md#contribute-the-context-assemble-rpc) applies to context providers. +`RecallRequest.turn_id` identifies the requesting turn as a ULID string, standardized across the whole protocol — the same treatment as [`context/protocol.md`](../context/protocol.md#contribute-the-context-assemble-rpc)'s `ContextRequest.turn_id` and `plan.v1`'s `turn_id` field. + +A returned `MemoryRecord` MAY carry `relevance_score` (`[0, 1]`, normalized) on `Recall` (and `ListRecords`, below) responses — never persisted, never present on a write-side request or response. It lets the kernel merge multiple memory providers' results under one shared budget using a comparable figure. See [`data-types.md#relevance_score`](data-types.md#relevance_score). + `model_target` MUST be set, mirroring the context provider's `ContextRequest` field of the same name — it lets a provider pass a precise model reference into the `CountTokens` kernel callback ([`kernel-callbacks.md#counttokens`](../kernel-callbacks.md#counttokens)) when computing `MemoryRecord.tokens`. `include_pending` MUST default to `false`: a `pending`-status record ([`protocol.md#ratification-optional`](protocol.md#ratification-optional)) MUST NOT surface through ordinary recall, only through an explicit review path. @@ -60,6 +64,14 @@ Records can reference one another with `[[name]]`-style structural links, not ju - A link target MUST NOT be required to already exist — forward references are natural (a record can reference one about to be created in the same batch of work). The kernel MUST NOT reject a `Record`/`UpdateRecord` call over a dangling link, but SHOULD make dangling links queryable (e.g. for a future "clean up broken links" operation) rather than silently losing track of them. - When rendering memory content (`Render` below, or the generic fallback), the kernel MUST resolve `[[name]]` occurrences into [`frontend/render-tree.md`](../frontend/render-tree.md)'s `link` `RenderNode` type, pointing at the target record. This is generic kernel-level post-processing specific to memory-category content, not something each provider's own `Render` implementation needs to reimplement. +## `ListRecords` / `GetRecord` + +The enumeration/audit path — paginated browsing and single-record fetch, distinct from `Recall`'s budget-constrained, relevance-ranked read path. Request/response shapes are in [`data-types.md#listrecords--getrecord`](data-types.md#listrecords--getrecord). Both MUST be implemented — they're cheap for any real backend, and generic tooling (a ratification review UI, a "browse what this provider knows" command) has no other way to enumerate or spot-check records. + +`ListRecordsRequest`'s `type_filter`/`scope_filter` follow the same "empty means all" convention as `RecallRequest`'s filters. `status_filter` is the one place this differs meaningfully from `Recall`: left unset, both `canonical` and `pending` records are eligible — **`PENDING` records ARE listable here**, with no `include_pending`-style gate. This is deliberate: `ListRecords` is the review-inbox path (an operator or a ratification UI paging through drafts awaiting approval), not the per-turn recall path `include_pending` guards against accidental surfacing of unratified content into a model's context. The two paths have opposite defaults because they serve opposite purposes. + +`GetRecord` MUST fail with a structured `MemoryError` (`not_found`, [`data-types.md#memoryerror`](data-types.md#memoryerror)) for an unknown `id`, the same convention as `UpdateRecord`/`DeleteRecord`/`ApproveRecord`/`RejectRecord`. + ## Ratification (optional) Ratification is a pattern a provider MAY implement, not a protocol requirement — matching a low-friction default of writing rather than asking. Where a provider *does* implement it, the shape MUST be standardized so generic tooling (a frontend's "pending memories" view) doesn't need provider-specific handling: @@ -72,7 +84,7 @@ Ratification is a pattern a provider MAY implement, not a protocol requirement Both an autonomous, hook-driven path and an explicit, model-invoked path exist side by side — see [`examples.md#write-triggers-reference-tools`](examples.md#write-triggers-reference-tools) for the full write-triggers table. In outline: -- A memory provider is implicitly subscribed to `post-response` (`observe` mode, fires every turn) and `session-end` (fires once, unconditionally) — giving every provider a guaranteed last chance to persist something even if its own turn-by-turn heuristic never triggered mid-session. Nothing in this protocol prescribes *when* within that stream a provider decides to call its own internal write logic. +- A memory provider is implicitly subscribed to `post-model-response` (`observe` mode, fires every turn) and `session-end` (fires once, unconditionally) — giving every provider a guaranteed last chance to persist something even if its own turn-by-turn heuristic never triggered mid-session. Both ride the shared `pluggableharness.agent.hook.v1.HookSubscriberService.DispatchHook` wire surface ([`agent-loop/hook-dispatch.md`](../agent-loop/hook-dispatch.md)) — the kernel calls `DispatchHook` with a `HookPayload` carrying the `post_model_response` or `session_end` variant, exactly as it would for any other category's hook subscription; this category has no separate, memory-specific dispatch mechanism. `MemoryCapabilities.supported_hook_points` ([`data-types.md#memorycapabilities`](data-types.md#memorycapabilities)) advertises which of these (and any other hook points) this provider actually subscribes to. Nothing in this protocol prescribes *when* within that stream a provider decides to call its own internal write logic. - Three reference tool operations (`memory.remember`, `memory.forget`, `memory.search`) give the model an explicit path that isn't gated by whatever the automatic recall pass happened to surface that turn. `memory.remember`'s `Invoke` MUST decide `Record` vs. `UpdateRecord` by checking whether the model-supplied (or derived) `id` already exists. Before concluding "no existing record, create new," it MUST also perform a fuzzy near-match check (e.g. string similarity against existing record titles/ids within the same `type`/`scope`). If a close-but-not-exact match is found, `Invoke` MUST NOT silently create a duplicate or silently update the near-match — it returns a tool result (not an error; this isn't a failure) listing the near-match candidate(s) and asking the model to confirm which was intended, either by re-invoking `memory.remember` with the corrected `id` or by explicitly proceeding with a new one. This reuses the ordinary tool-result-feeds-back-into-the-model-turn pattern rather than inventing a new interactive escalation — no `interactive`-kind tool involvement is needed, since the model, not a human, resolves the ambiguity on its next turn. @@ -82,3 +94,13 @@ Content-quality guidance (what's worth remembering, what isn't, promoting verbos ## Render Memory providers MAY implement `Render` per the general Emit→Render→Paint pipeline ([`architecture.md`](../architecture.md#emit--render--paint-pipeline)), returning the `RenderTree` formally defined in [`frontend/render-tree.md`](../frontend/render-tree.md). A reference implementation might render a `pending` record specially (e.g. a review-inbox UI element distinct from ordinary recall) — this is exactly the kind of case where custom rendering matters more than the generic fallback. If not implemented, the kernel falls back to its generic default rendering. + +`RenderRequest` carries `schema_version` alongside the opaque `payload` — see [`frontend/render-tree.md#schema-versioning`](../frontend/render-tree.md#schema-versioning) for what the value means and how a `Render` implementation is expected to use it. + +## `Describe` + +```text +Describe(DescribeRequest) -> DescribeResponse +``` + +MUST be implemented. Reports this plugin build's own identity — `{name, version, source, category, protocol_version}` via `common.v1.ProducerRef` — independent of any lock-file entry. This is how the kernel identifies a `dev_overrides`-resolved binary, which has no `provider "" { ... }` entry to read identity from the normal way: see [`configuration/lock-file.md`](../configuration/lock-file.md#dev_overrides-and-identity-without-a-lock-entry) for the canonical explanation, shared verbatim across every plugin category that gains this RPC in this protocol revision. diff --git a/docs/specifications/model/README.md b/docs/specifications/model/README.md index 4405ea3..4998970 100644 --- a/docs/specifications/model/README.md +++ b/docs/specifications/model/README.md @@ -10,13 +10,13 @@ See [`architecture.md`](../architecture.md) for the surrounding system (transpor Subprocess + gRPC via `hashicorp/go-plugin`, per [`architecture.md`](../architecture.md#transport). Standard handshake (magic cookie, protocol version negotiation) applies uniformly across all six provider categories and isn't repeated per category. -A model provider plugin exposes four RPCs: `GetCapabilities`, `Configure`, `StreamCompletion`, `CountTokens`. It MAY additionally implement `Render` (see [`protocol.md#render`](protocol.md#render)). +A model provider plugin exposes five RPCs: `GetCapabilities`, `Configure`, `StreamCompletion`, `CountTokens`, `Describe`. It MAY additionally implement `Render` (see [`protocol.md#render`](protocol.md#render)). **`StreamCompletion` is server-streaming, not bidirectional.** The kernel sends one request (full message history + tool specs + params) and receives a stream of response chunks back — this matches how vendor completion APIs actually work (one HTTP request, SSE/chunked response; vendors generally don't accept mid-stream client input on the same call). Cancellation (the one thing bidirectional streaming would otherwise be needed for) is handled by the kernel simply cancelling/closing the gRPC stream — a standard, natively-supported operation on a server-streaming call. Plugin authors MUST treat stream cancellation as a normal, expected event (stop generating, release resources), never as an error condition. ## Category structure -- [`protocol.md`](protocol.md) — the four/five RPCs: `GetCapabilities`, `Configure`, `StreamCompletion`, `CountTokens`, `Render`. -- [`data-types.md`](data-types.md) — `ModelSpec`, `Pricing`/`PricingTier`, `ThinkingSpec`, `CachingSpec`, the canonical message/content-block schema, and the shared tool-schema subset. +- [`protocol.md`](protocol.md) — the six RPCs: `GetCapabilities`, `Configure`, `StreamCompletion`, `CountTokens`, `Render`, `Describe`. +- [`data-types.md`](data-types.md) — `ModelSpec`, `Pricing`/`PricingTier`, `ThinkingSpec`, `CachingSpec`, `StreamCompletionRequest`, `GenerationParams`/`ToolChoice`, `CacheBreakpoint`, the canonical message/content-block schema, and the shared tool-schema subset. - [`examples.md`](examples.md) — a worked `agent.hcl` provider block, the wire protocol definitions, a cost-computation walkthrough, and a full `StreamCompletion` event sequence. - [`conformance.md`](conformance.md) — the error taxonomy and the MUST/SHOULD/MAY summary matrix, plus genuinely open questions. diff --git a/docs/specifications/model/conformance.md b/docs/specifications/model/conformance.md index 11e76df..2a2fdd9 100644 --- a/docs/specifications/model/conformance.md +++ b/docs/specifications/model/conformance.md @@ -25,19 +25,25 @@ On the wire, each category maps to a `grpc/codes.Code`: `context_length_exceeded | `text` content, both directions | MUST | baseline | | Streaming RPC shape | MUST | see [`README.md`](README.md#transport--lifecycle) / [`protocol.md`](protocol.md#streamcompletion) — applies even to non-streaming backends | | `GetCapabilities` / `Configure` / `StreamCompletion` RPCs | MUST | the whole protocol surface | +| `Describe` RPC | MUST | [`protocol.md#describe`](protocol.md#describe) — identity for `dev_overrides` binaries with no lock-file entry | | Structured error taxonomy (above) | MUST | | | `tool_use` / `tool_result` | MUST, if any served model has `supports_tool_use = true` | | | `image` (vision) | MUST support where `supports_vision = true`; MUST reject cleanly where `false` | | +| `document` | MUST support where `supports_documents = true`; MUST reject cleanly where `false` | [`data-types.md#canonical-message--content-block-schema`](data-types.md#canonical-message--content-block-schema) — mirrors `image`/`supports_vision`'s rule | | Extended thinking/reasoning | MAY, capability-gated via `ThinkingSpec` | declare `mode` precisely, don't collapse to a bool | | Prompt caching | MAY, capability-gated via `CachingSpec` | declare `mode` (explicit vs. implicit) | +| Cache breakpoints (`StreamCompletionRequest.cache_breakpoints`) | MUST honor where `CachingSpec.mode = CACHING_MODE_EXPLICIT_MARKERS`; MUST ignore otherwise | [`protocol.md#cache-breakpoint-placement-policy`](protocol.md#cache-breakpoint-placement-policy) — placement is a kernel decision, never the plugin's | | Parallel tool calls in one turn | SHOULD declare via `supports_parallel_tool_calls` | kernel serializes calls if absent/false | -| `Render` | MAY | generic fallback exists | -| `CountTokens` | SHOULD | kernel falls back to [`kernel-callbacks.md`](../kernel-callbacks.md#the-fallback-heuristic)'s heuristic when absent, treated as a last resort | +| Tool-choice constraint (`GenerationParams.tool_choice`) | MAY, capability-gated via `ModelSpec.supported_tool_choice_modes` | kernel MUST NOT send a mode absent from the declared list, mirroring `ThinkingSpec` validation | +| `Render` | MAY | generic fallback exists; `RenderRequest.schema_version` MUST be set when implemented | +| `CountTokens` | SHOULD | kernel falls back to [`kernel-callbacks.md`](../kernel-callbacks.md#the-fallback-heuristic)'s heuristic when absent, treated as a last resort; `CountTokensRequest.model_id` MUST be set | | `ThinkingSpec.default` | MUST when `mode != none` | [`data-types.md`](data-types.md#thinkingspec) | | `CachingSpec.keepalive_supported` | MUST (field); actual keepalive loop MAY | [`data-types.md`](data-types.md#cachingspec) | -| `Pricing.tiers`, time-bounded/tiered rates | MUST | [`data-types.md`](data-types.md#pricing) — exactly one tier MUST match any given timestamp | +| `Pricing.tiers`, time-bounded/tiered/input-size-bounded rates | MUST | [`data-types.md`](data-types.md#pricing) — exactly one tier MUST match any given `(timestamp, input_token_count)` pair | | `Pricing` on every `ModelSpec` | MUST | required even for `free: true` models | -| Kernel computes + persists `cost_usd` at usage-event time, not lazily at query time | MUST | [`protocol.md`](protocol.md#cost-computation) | +| Kernel computes + persists `cost_usd` at usage-event time, not lazily at query time | MUST | [`protocol.md`](protocol.md#cost-computation) — includes `reasoning_tokens` billed at the output rate | +| `Usage.reasoning_tokens` | SHOULD, when the vendor reports it distinctly | [`data-types.md#streamevent`](data-types.md#streamevent) — never double-counted in `output_tokens` | +| `supported_hook_points` | MUST (field, MAY be empty) | [`data-types.md#capabilitiessupported_hook_points`](data-types.md#capabilitiessupported_hook_points) — kernel rejects an unsupported `hook{}` block at config-load time | | Realtime/voice (WebSocket-style APIs) | MUST NOT — out of scope for v1 | likely a distinct wire protocol per vendor; treat as a future, separate plugin surface, not a mode of this one | | Embeddings | MUST NOT — out of scope for v1 | a separate future concern (likely relevant to memory providers, not modeled here) | diff --git a/docs/specifications/model/data-types.md b/docs/specifications/model/data-types.md index 85878e7..ec0db07 100644 --- a/docs/specifications/model/data-types.md +++ b/docs/specifications/model/data-types.md @@ -16,6 +16,10 @@ ModelSpec { thinking ThinkingSpec // MUST be present; use { supported: false } if none caching CachingSpec // MUST be present; use { supported: false } if none pricing Pricing // MUST be present — see below + supported_tool_choice_modes []ToolChoiceMode // SHOULD — which GenerationParams.tool_choice.mode + // values this model accepts (see below); empty means + // this model can't constrain tool choice at all + supports_documents bool // MUST — whether this model accepts a DocumentBlock content block } ``` @@ -83,12 +87,17 @@ PricingTier { batch_input_per_mtok float64? // MAY — a vendor's discounted batch/async rate, where // one exists (e.g. a Gemini-style batch tier) batch_output_per_mtok float64? // MAY, paired with batch_input_per_mtok + input_tokens_from int64? // MAY — smallest accumulated-input-token count this + // tier applies to, inclusive; omitted means unbounded + // below + input_tokens_until int64? // MAY — input-token count this tier stops applying to, + // exclusive; omitted means unbounded above } ``` -This shape expands beyond a flat current-rate snapshot to model both time-bounded (promotional/expiring) and tiered (realtime vs. batch) rates — an Anthropic-style intro-pricing window and a Gemini-style batch discount are both realistic, concrete examples of what this needs to represent, not hypothetical. +This shape expands beyond a flat current-rate snapshot to model time-bounded (promotional/expiring), tiered (realtime vs. batch), and input-size-bounded rates — an Anthropic-style intro-pricing window, a Gemini-style batch discount, and a vendor charging a distinct, higher rate once a request's accumulated input exceeds 200k tokens are all realistic, concrete examples of what this needs to represent, not hypothetical. `input_tokens_from`/`input_tokens_until` add that third dimension alongside the two time bounds; both pairs are independently half-open (an omitted bound is unbounded on that side). -**Kernel resolution**: given a timestamp (the moment `usage` was received, [`protocol.md#cost-computation`](protocol.md#cost-computation)), the kernel selects the tier where `effective_from <= timestamp < effective_until` (treating an omitted bound as unbounded on that side); exactly one tier MUST match at any given timestamp — a plugin author publishing overlapping or gapped tiers has published an invalid `Pricing` value, and the kernel MUST reject it at capability-load time, not silently pick one. Whether an overlapping-tier rejection should instead be a softer warning is an open question — see [`conformance.md`](conformance.md#open-questions). +**Kernel resolution**: given a `(timestamp, input_token_count)` pair — the timestamp is the moment `usage` was received ([`protocol.md#cost-computation`](protocol.md#cost-computation)); the input token count is that same `usage` event's `input_tokens` — the kernel selects the tier where `effective_from <= timestamp < effective_until` AND `input_tokens_from <= input_token_count < input_tokens_until` (each bound unbounded on its own side when omitted); exactly one tier MUST match at any given `(timestamp, input_token_count)` pair — a plugin author publishing overlapping or gapped tiers, in either dimension, has published an invalid `Pricing` value, and the kernel MUST reject it at capability-load time, not silently pick one. A plugin declaring only the time dimension (every tier's `input_tokens_from`/`input_tokens_until` both omitted) is a degenerate one-tier-per-input-size case and resolves exactly as it did before this field existed. Whether an overlapping-tier rejection should instead be a softer warning is an open question — see [`conformance.md`](conformance.md#open-questions). `Pricing` MUST be present on every `ModelSpec`, even a free one — it is a required message field on the wire, not optional, for exactly that reason. @@ -107,8 +116,8 @@ StreamEvent = oneof { tool_call_start { id: string, name: string } tool_call_delta { id: string, arguments_fragment: string } // partial-JSON accumulation tool_call_done { id: string } - usage { input_tokens, output_tokens, cache_read_tokens?, cache_write_tokens? } - stop { reason: StopReason } + usage { input_tokens, output_tokens, cache_read_tokens?, cache_write_tokens?, reasoning_tokens? } + stop { reason: StopReason, matched_stop_sequence?: string } error ModelError // see conformance.md#error-taxonomy } @@ -120,22 +129,134 @@ StopReason = enum { cancelled // the kernel cancelled the stream (user interrupt, timeout, turn // abort) — MUST be treated by the plugin as normal control flow, // never as an error + refusal // the model or vendor refused to continue generating — distinct + // from content_filtered, which is the vendor's automated content + // filter stopping generation; refusal is the model itself + // declining (e.g. a safety-trained refusal message) + stop_sequence // the model stopped because it generated one of + // GenerationParams.stop_sequences; `matched_stop_sequence` carries + // which one and MUST be set iff reason == stop_sequence } ``` +`usage.reasoning_tokens` is set only when the vendor reports thinking/reasoning tokens as a distinct count (`ThinkingSpec.supported` models only) and is never also counted in `output_tokens` — a vendor that folds reasoning tokens into its reported `output_tokens` has no separate figure to report, so this stays unset rather than being derived or subtracted. It's billed at `PricingTier.output_per_mtok` unless a future `Pricing` revision declares a distinct reasoning rate; there is none as of this revision. + A plugin MUST classify every terminal failure via a `stop` event's `content_filtered` reason or an `error` event carrying a `ModelError` ([`conformance.md#error-taxonomy`](conformance.md#error-taxonomy)) — the in-band `error` variant is how a plugin reports a classified failure *within* an otherwise-open stream, distinct from the stream simply being torn down at the transport level (a gRPC-level status, or the kernel closing the stream on cancellation). A plugin whose backend fails outright before producing any events MAY end the stream with just an `error` event and no preceding `stop`. ## Canonical message & content-block schema -Per [`architecture.md`](../architecture.md#canonical-message--tool-schema-format), the canonical form is content-block messages: `text`, `tool_use`, `tool_result`, `image`, `thinking`, `redacted_thinking`. This is the state backend's source of truth, independent of any one vendor's wire format surviving. +Per [`architecture.md`](../architecture.md#canonical-message--tool-schema-format), the canonical form is content-block messages: `text`, `tool_use`, `tool_result`, `image`, `thinking`, `redacted_thinking`, `document`. This is the state backend's source of truth, independent of any one vendor's wire format surviving. - `text` — MUST be supported by every plugin, both directions. - `image` — MUST be supported by every plugin for a model where `ModelSpec.supports_vision == true`; MUST be rejected with a clear `invalid_request` error (not silently dropped) if sent to a model where it's `false`. +- `document` — inline non-image document content (e.g. a PDF), carrying `data: bytes`, `media_type: string`, and an optional `filename`. MUST be supported by every plugin for a model where `ModelSpec.supports_documents == true`; MUST be rejected with a clear `invalid_request` error (not silently dropped) if sent to a model where it's `false` — the same rule `image`/`supports_vision` already establishes, applied to a second, independent capability flag. - `tool_use` / `tool_result` — MUST be supported wherever `supports_tool_use == true`. - `thinking` / `redacted_thinking` — only relevant where `ThinkingSpec.supported == true`. **A `thinking` block MAY carry an opaque, vendor-specific integrity token** (e.g. a cryptographic signature) that the plugin must store verbatim and echo back unmodified on the next turn, or the vendor API will reject the request. The kernel and state backend MUST treat this token as an opaque blob — never inspected, re-derived, or reformatted, just round-tripped. On the wire, this is [`StreamEvent`](#streamevent)'s `thinking_signature` variant (`bytes`) — see [`examples.md`](examples.md). Each model-provider adapter owns its own lossy translation between this canonical form and its vendor's wire format (e.g. OpenAI has no `thinking` block equivalent — an adapter targeting OpenAI simply never emits one). +### `Message` identity and model attribution + +Every `Message` carries, beyond `role` and `content`: + +```protobuf +Message { + role Role + content []ContentBlock + id string // MUST — kernel-assigned ULID, stable across replay + produced_by_model_id string? // assistant messages only — the producing model's ModelSpec.id + produced_by_provider string? // assistant messages only — the producing provider's declared name +} +``` + +`id` is the correlation anchor for deltas and forking (e.g. a frontend edit-and-resubmit that forks history at a given message) — the kernel assigns it once, at persist time, and it never changes, even when the same conversation is replayed against a newer plugin version. A plugin never generates this value itself. + +`produced_by_model_id`/`produced_by_provider` record which model and provider actually produced a `ROLE_ASSISTANT` message. Both are plain strings, not a `model.v1.ModelRef` or `model.v1.ModelTarget` — `content.v1` MUST NOT import `model.v1`, because `model.v1` already imports `content.v1` (for `Message`/`ContentBlock`/`ContextSection`) and the reverse import would be a cyclic file dependency `buf` rejects at build time. Both fields are omitted for a `ROLE_USER` message, or when the producing model is otherwise unknown. Because the kernel's routing/fallback chain ([`protocol.md#generation-parameter-validation-and-capability-aware-routing`](protocol.md#generation-parameter-validation-and-capability-aware-routing)) may serve adjacent turns in the same session from different providers or different models, two consecutive `ROLE_ASSISTANT` messages in one conversation MAY carry different `produced_by_model_id`/`produced_by_provider` values — this is expected, not an anomaly, and MUST be preserved verbatim on replay. + +## `StreamCompletionRequest` + +`StreamCompletionRequest` is `StreamCompletion`'s request — the full canonical conversation, available tools, and generation params for one completion: + +```protobuf +StreamCompletionRequest { + messages []Message // MUST — canonical conversation history, in emission order + model_id string // MUST — selects which of this provider's ModelSpec.id to use + tools []ToolDeclaration // MAY be empty + params GenerationParams? // omitted means every param takes its model-specific default + assembled_context []ContextSection // MUST — the kernel-assembled context chain, see below + call_context CallContext // MUST — session/turn/working-directory attribution, see below + cache_breakpoints []CacheBreakpoint // MAY be empty — see below +} +``` + +### `assembled_context` + +`assembled_context` is the kernel-assembled context chain: the accumulated output of every context provider's `Contribute` call plus memory recall ([`context/protocol.md#contribute-the-context-assemble-rpc`](../context/protocol.md#contribute-the-context-assemble-rpc)), carried as `content.v1.ContextSection` — the same type `context/protocol.md`'s `Contribute` RPC produces and consumes — **not** `context.v1.ContextContribution`. `ContextSection` lives in `content.v1` specifically so `model.v1` can reference it without importing `context.v1` (which itself imports `model.v1` for `ModelTarget`, so the reverse import would be a cyclic file dependency). + +Ordering is **chain order**: the same order `context/protocol.md`'s `ContextRequest.prior_sections`/`Contribute` response chain uses, where each provider appends its own section after the sections it received. This is the tools → system → static-project-context → conversation-tail prefix ordering `context/data-types.md#ordering--chaining` establishes for prompt-cache reuse. + +`assembled_context` is distinct from `messages`: it is system-level/preamble content, never a conversational turn — which is exactly why it's a separate field rather than a synthetic message. **`content.v1.Role` deliberately has no `SYSTEM` value** ([`data-types.md`](#canonical-message--content-block-schema)'s `Message`/`Role` definitions; `content.proto`'s `Role` comment): system-level content is always assembled as a `ContextSection` chain, never carried as a message with a role. Each model-provider adapter maps `assembled_context` to its own vendor's system/preamble mechanism — a top-level `system` string, a leading system-role message, or whatever else that vendor's API expects — and that mapping is adapter-internal, not part of this wire contract. + +### `call_context` + +`call_context` is a `common.v1.CallContext { session_id, turn_id, working_directory }`. The kernel MUST set it on every `StreamCompletionRequest`. It's what the plugin passes back on its own `KernelCallbackService.Emit`/`Log` calls ([`kernel-callbacks.md#emit`](../kernel-callbacks.md#emit)) for session/turn attribution, without the adapter having to separately thread `session_id`/`turn_id` through its own call sites by hand. + +### `cache_breakpoints` and cache-breakpoint placement policy + +`cache_breakpoints` is `[]CacheBreakpoint`, wire-level and **request-scoped** — deliberately not carried on the persisted `content.v1.ContentBlock`, because a breakpoint's placement is a per-request optimization decision, not a durable property of the conversation history itself: + +```protobuf +CacheBreakpoint { + position oneof { + after_assembled_context // an empty marker — after the whole assembled_context chain + after_tools // an empty marker — after the tools declaration list + after_message_index int64 // after the message at this zero-based index in `messages` + } +} +``` + +`cache_breakpoints` is meaningful only when the target model's `CachingSpec.mode == CACHING_MODE_EXPLICIT_MARKERS`; a model-provider adapter targeting a model whose `CachingSpec.mode` is `CACHING_MODE_IMPLICIT_AUTOMATIC` or `CACHING_MODE_NONE` MUST ignore this field rather than error on it. The adapter maps each breakpoint to its vendor's own cache-control mechanism (e.g. an Anthropic `cache_control` block on the targeted content). + +**Breakpoint placement is a kernel decision, not the plugin's.** The kernel knows each `assembled_context` section's `Stability` (`content.proto`'s `Stability` enum: `STABILITY_STATIC` vs. `STABILITY_DYNAMIC`) and each message's position in the conversation, so it places breakpoints at natural stable-prefix boundaries — the same tools → system → static-project-context → conversation-tail ordering that governs `assembled_context`'s own chain order. In practice this means: a breakpoint after `after_tools` when the tool declaration list is stable turn to turn, and a breakpoint after `after_assembled_context` when the whole chain's leading sections are `STABILITY_STATIC` — since that's usually the longest stable prefix a vendor's prompt cache can actually reuse. A plugin never invents its own placement; it only translates the breakpoints the kernel already decided into vendor-native markers. + +## `GenerationParams` + +`GenerationParams` carries per-request overrides of otherwise model-default generation behavior: + +```protobuf +GenerationParams { + thinking_effort string? // one of ThinkingSpec.effort_levels; THINKING_MODE_DISCRETE_EFFORT only + thinking_budget_tokens int64? // within ThinkingSpec.budget_range; THINKING_MODE_CONTINUOUS_BUDGET only + max_output_tokens int64? // per-request override of ModelSpec.max_output_tokens + temperature double? // sampling temperature; vendor-specific range/semantics, passed through as-is + stop_sequences []string // sequences that MUST stop generation before they're produced + tool_choice ToolChoice? // constrains whether/how the model must use a tool this turn +} + +ToolChoice { + mode ToolChoiceMode // MUST be set + tool_name string? // MUST be set iff mode == SPECIFIC; MUST be omitted otherwise +} + +ToolChoiceMode = enum { UNSPECIFIED, AUTO, ANY, NONE, SPECIFIC } +``` + +`stop_sequences`: when a vendor honors one of these, the plugin MUST report it back via `StreamEvent.Stop.matched_stop_sequence` with `StopReason.STOP_SEQUENCE` (see [`#streamevent`](#streamevent)). + +`tool_choice`: `AUTO` (the model decides freely — equivalent to omitting `tool_choice` entirely), `ANY` (the model MUST call some tool this turn, but may pick which), `NONE` (the model MUST NOT call any tool this turn, even if tools were declared), `SPECIFIC` (the model MUST call the exact tool named in `tool_name`, which MUST name a tool present in `StreamCompletionRequest.tools`). + +**Validation mirrors the existing thinking-params rule.** Just as `thinking_effort`/`thinking_budget_tokens` MUST be validated against the resolved model's `ThinkingSpec` before dispatch ([`protocol.md#generation-parameter-validation-and-capability-aware-routing`](protocol.md#generation-parameter-validation-and-capability-aware-routing)), `tool_choice.mode` MUST be validated against the resolved model's `ModelSpec.supported_tool_choice_modes`: a mode absent from that list is a kernel-level reject-or-fallback, never something forwarded to the vendor and left to surface as a raw API error. `ModelSpec.supported_tool_choice_modes` declares precisely which subset of `AUTO`/`ANY`/`NONE`/`SPECIFIC` a vendor supports, mirroring `ThinkingSpec`/`CachingSpec`'s "declare precisely, don't collapse to a bool" rationale — real vendors differ in which modes they expose. An empty list means the model can't constrain tool choice at all. + +## `Capabilities.supported_hook_points` + +`Capabilities` (`GetCapabilities`'s response payload) additionally carries `supported_hook_points: []common.v1.HookPoint` — which of the eight dispatchable hook points ([`agent-loop/hook-dispatch.md`](../agent-loop/hook-dispatch.md)) this plugin can serve via `HookSubscriberService.DispatchHook`. The kernel MUST reject an `agent.hcl` `hook{}` block naming a point absent from this list, at config-load time. + +This is typed as `common.v1.HookPoint`, not `hook.v1.HookPoint`: `hook.proto` imports `model.proto` (for `ModelRef`/`Usage` on its pre-model-call/post-model-response hook payloads), so `model.proto` importing `hook.proto` for this field would be a cyclic file dependency `buf build` rejects outright. `HookPoint` itself is declared in `common.v1` for exactly this reason, alongside `CallContext` and `ProducerRef`. + +## `Describe` + +`ModelService` gains a `Describe(DescribeRequest) -> DescribeResponse { producer: common.v1.ProducerRef }` RPC, identical in shape across all six category protocols in this protocol revision. It reports this plugin build's own identity — `{name, version, source, category, protocol_version}` — directly from the running process. This matters specifically for a `dev_overrides` binary ([`configuration/settings-and-global.md#dev_overrides`](../configuration/settings-and-global.md#dev_overrides)), which bypasses the registry/lock-file resolution path entirely and so has no `provider "" { ... }` lock entry for the kernel to read identity from; see [`configuration/lock-file.md`](../configuration/lock-file.md#dev_overrides-and-identity-without-a-lock-entry)'s `dev_overrides` note for the canonical explanation. + ## Tool schema Tool resources (declared by tool providers, not model providers — see [`tool/`](../tool/README.md)) are described once in a common JSON Schema subset, and each model-provider adapter translates that into its vendor's tool-definition wire format. diff --git a/docs/specifications/model/examples.md b/docs/specifications/model/examples.md index 5a75c8b..ed2890d 100644 --- a/docs/specifications/model/examples.md +++ b/docs/specifications/model/examples.md @@ -30,6 +30,7 @@ service ModelService { rpc StreamCompletion(StreamCompletionRequest) returns (stream StreamEvent); rpc CountTokens(CountTokensRequest) returns (CountTokensResponse); rpc Render(RenderRequest) returns (RenderResponse); + rpc Describe(DescribeRequest) returns (DescribeResponse); } message ModelSpec { @@ -43,6 +44,8 @@ message ModelSpec { ThinkingSpec thinking = 8; CachingSpec caching = 9; Pricing pricing = 10; + repeated ToolChoiceMode supported_tool_choice_modes = 11; + bool supports_documents = 12; } ``` @@ -54,9 +57,14 @@ A single turn where the model answers with text, then requests one tool call, ex ```text → StreamCompletionRequest{ - messages: [ {role: user, content: [{text: "What's in main.go?"}]} ], + messages: [ {role: user, content: [{text: "What's in main.go?"}], id: "01J..."} ], model_id: "claude-opus-5", tools: [ {name: "read_file", input_schema: {...}} ], + assembled_context: [ + {provider: "project-context", label: "CLAUDE.md", content: [...], tokens: 812, stability: STABILITY_STATIC}, + ], + call_context: {session_id: "01J...", turn_id: "01J...", working_directory: "/repo"}, + cache_breakpoints: [ {after_assembled_context: {}} ], } ← StreamEvent{text_delta: {text: "Let me check "}} @@ -69,6 +77,35 @@ A single turn where the model answers with text, then requests one tool call, ex ← StreamEvent{stop: {reason: STOP_REASON_TOOL_USE}} ``` +The `assembled_context` entry above is what the adapter maps to Anthropic's `system` parameter (or an equivalent leading system-role message for a vendor without a dedicated system slot) — it never appears in `messages` itself, per `content.v1.Role` having no `SYSTEM` value. The single `cache_breakpoints` entry tells the adapter to place a cache-control marker immediately after that assembled-context content, since it's the turn's `STABILITY_STATIC`, longest-stable-prefix content — the adapter translates this to Anthropic's `cache_control: {type: "ephemeral"}` on the corresponding system block. + +A turn where the model declines to answer, having been constrained to a specific tool it chose not to use: + +```text +→ StreamCompletionRequest{ + messages: [ ... ], + model_id: "claude-opus-5", + tools: [ {name: "delete_repo", input_schema: {...}} ], + params: { tool_choice: {mode: TOOL_CHOICE_MODE_NONE} }, + ... + } + +← StreamEvent{text_delta: {text: "I won't do that — it looks destructive and unconfirmed."}} +← StreamEvent{usage: {input_tokens: 201, output_tokens: 19, reasoning_tokens: 143}} +← StreamEvent{stop: {reason: STOP_REASON_REFUSAL}} +``` + +`reasoning_tokens: 143` here is billed at `pricing.output_per_mtok`, per [`protocol.md#cost-computation`](protocol.md#cost-computation), and is never folded into the reported `output_tokens: 19`. + +A turn stopped by a caller-supplied stop sequence: + +```text +→ StreamCompletionRequest{ ..., params: { stop_sequences: [""] } } + +← StreamEvent{text_delta: {text: "42"}} +← StreamEvent{stop: {reason: STOP_REASON_STOP_SEQUENCE, matched_stop_sequence: ""}} +``` + The kernel accumulates `tool_call_delta` fragments by `id` into the final parsed-JSON arguments before dispatching to the tool provider — per [`data-types.md#tool-schema`](data-types.md#tool-schema), arguments are always stored as already-parsed JSON in the kernel's internal representation, regardless of whether the vendor sent a JSON-encoded string (OpenAI/Mistral) or an already-parsed object (Anthropic/Gemini/Ollama) on its own wire. If the kernel cancels the stream mid-flight (user hit Ctrl-C), the plugin sees `context.Canceled`, stops generating, and the stream simply ends — there is no `StreamEvent{stop: {reason: STOP_REASON_CANCELLED}}` guaranteed from every vendor backend, since the cancellation is a transport-level gRPC operation, not a vendor-API-level one for every vendor. A plugin MAY still emit a `STOP_REASON_CANCELLED` stop event on a best-effort basis where its vendor SDK makes that easy. @@ -92,4 +129,6 @@ cost_usd = 412 * 3.00 / 1e6 + 28 * 15.00 / 1e6 + 0 + 0 The kernel persists `0.001656` into the state backend's `cost_ledger` table ([`state-backend.md#cost_ledger`](../state-backend.md#cost_ledger)) at the moment the `usage` event is received — using whichever `PricingTier` matches that timestamp, per [`data-types.md#pricing`](data-types.md#pricing)'s resolution rule. +Had this `usage` event carried `reasoning_tokens`, the worked sum above would gain one more term, `reasoning_tokens * output_per_mtok / 1e6`, using the same `output_per_mtok` rate — see [`protocol.md#cost-computation`](protocol.md#cost-computation)'s full five-term formula. Had `pricing.tiers` declared an `input_tokens_from`/`input_tokens_until` bound (e.g. a higher rate above 200k input tokens), the kernel would first resolve which tier matches `(usage_timestamp, input_tokens)` before doing this arithmetic — the calculation itself is unchanged once the right tier is selected. + **This dollar figure is distinct from telemetry.** Telemetry instrumentation separately *observes* the same usage numbers for dashboards and tracing, but never recomputes or owns the persisted figure — a second computation path for the same number would be exactly the kind of divergence that undermines replay fidelity. Usage instrumentation takes the already-computed cost figure as input and mirrors it onto metrics and trace spans; it runs only after the kernel's cost-ledger write has computed `cost_usd`, never before, which keeps it a pure observability mirror rather than a second source of truth. diff --git a/docs/specifications/model/protocol.md b/docs/specifications/model/protocol.md index 78872b3..0eff047 100644 --- a/docs/specifications/model/protocol.md +++ b/docs/specifications/model/protocol.md @@ -1,6 +1,6 @@ # Model provider — protocol -The five RPCs a model provider plugin exposes. See [`README.md`](README.md#transport--lifecycle) for the transport-level framing (server-streaming, cancellation) that applies to `StreamCompletion` specifically. +The six RPCs a model provider plugin exposes. See [`README.md`](README.md#transport--lifecycle) for the transport-level framing (server-streaming, cancellation) that applies to `StreamCompletion` specifically. ## `GetCapabilities` @@ -8,12 +8,16 @@ Returns a `Capabilities` value with one `ModelSpec` per model the plugin can ser The response MAY additionally include `slash_commands: []SlashCommandSpec` (declared once for the provider as a whole, not per model) and MUST include the provider's `ConfigSchema`, so the kernel knows what fields `Configure` expects before ever calling it. See [`data-types.md`](data-types.md#modelspec) for the full `ModelSpec` shape. +The response also carries `supported_hook_points: []common.v1.HookPoint` ([`data-types.md#capabilitiessupported_hook_points`](data-types.md#capabilitiessupported_hook_points)) — declared once for the provider as a whole, not per model, mirroring `slash_commands`. The kernel MUST reject an `agent.hcl` `hook{}` block for this plugin naming a point absent from this list, at config-load time, rather than discovering the mismatch only when `HookSubscriberService.DispatchHook` is actually called. + ### `CountTokens` ```text -CountTokens(text: string) -> { count: int } +CountTokens(text: string, model_id: string) -> { count: int } ``` +`model_id` MUST be set on every `CountTokensRequest` — it selects which of this provider's `ModelSpec.id` to count against, since a provider serving several models MAY use a distinct tokenizer per model. + SHOULD be implemented per model, using that vendor's real tokenizer: rather than investing in a smarter kernel-side fallback heuristic, the expectation is that providers actually implement this against real vendor tokenizers wherever the vendor makes it available, and the fallback ([`kernel-callbacks.md#the-fallback-heuristic`](../kernel-callbacks.md#the-fallback-heuristic)) stays a genuine last resort, not a normal operating path. This is the model-provider side of [`kernel-callbacks.md`](../kernel-callbacks.md)'s `CountTokens` primitive — a model provider that implements this gets its counts marked `exact: true` when the kernel resolves a `CountTokens` call against it; a model provider that doesn't falls back to the documented heuristic. Still not a MUST, because not every vendor makes exact counting cheap or even possible without a network round-trip — but a provider author should treat skipping it as the exception, not the default. ## `Configure` @@ -28,14 +32,24 @@ Accepts a config object decoded from the provider's `agent.hcl` block via the sc Request: canonical messages ([`data-types.md#canonical-message--content-block-schema`](data-types.md#canonical-message--content-block-schema)) + tool specs ([`data-types.md#tool-schema`](data-types.md#tool-schema)) + -generation params. Response: a stream of [`StreamEvent`](data-types.md#streamevent)s — see [`examples.md#a-full-streamcompletion-event-sequence`](examples.md#a-full-streamcompletion-event-sequence) for a worked sequence. +generation params + the kernel-assembled context chain + call attribution + cache breakpoints ([`data-types.md#streamcompletionrequest`](data-types.md#streamcompletionrequest)). Response: a stream of [`StreamEvent`](data-types.md#streamevent)s — see [`examples.md#a-full-streamcompletion-event-sequence`](examples.md#a-full-streamcompletion-event-sequence) for a worked sequence. A plugin whose backend does not natively stream (batch-only) MUST still implement this RPC shape, emitting the full response as a single terminal burst of events followed by `stop`. `ModelSpec.supports_streaming = false` is how the plugin signals this to the kernel/frontend as a UX hint (e.g. "don't render a live-typing cursor"); it does not change what RPC gets called. +### Assembled context and call attribution + +`StreamCompletionRequest.assembled_context` carries the kernel-assembled context chain — every context provider's contribution plus memory recall, in chain order — as `content.v1.ContextSection`, distinct from `messages`: it is system-level/preamble content, never a conversational turn. `content.v1.Role` deliberately has no `SYSTEM` value for exactly this reason — system content is always an `assembled_context` section, never a message with a role. Each adapter maps the chain to its own vendor's system/preamble mechanism. `StreamCompletionRequest.call_context` (`common.v1.CallContext`) MUST be set by the kernel on every request and is what the plugin echoes back on `KernelCallbackService.Emit`/`Log` for session/turn attribution. Full detail: [`data-types.md#streamcompletionrequest`](data-types.md#streamcompletionrequest). + +### Cache-breakpoint placement policy + +`StreamCompletionRequest.cache_breakpoints` is meaningful only when the target model's `CachingSpec.mode == CACHING_MODE_EXPLICIT_MARKERS`; an adapter targeting any other mode MUST ignore the field. Placement is a **kernel** decision, not the plugin's — the kernel knows each `assembled_context` section's `Stability` and each message's position, so it places breakpoints at the natural stable-prefix boundaries the tools → system → static-project-context → conversation-tail ordering already establishes (most commonly: right after `assembled_context` when its leading sections are `STABILITY_STATIC`, since that's usually the longest prefix a vendor's prompt cache can actually reuse). The adapter's only job is translating the breakpoints it's given into vendor-native cache-control markers — it never decides placement itself. Full shape: [`data-types.md#cache_breakpoints-and-cache-breakpoint-placement-policy`](data-types.md#cache_breakpoints-and-cache-breakpoint-placement-policy). + ### Generation-parameter validation and capability-aware routing `GenerationParams.thinking_effort`/`thinking_budget_tokens` MUST be validated against the resolved model's declared [`ThinkingSpec`](data-types.md#thinkingspec) before the request is dispatched to the plugin — an effort level outside `ThinkingSpec.effort_levels`, or a budget outside `ThinkingSpec.budget_range`, is a kernel-level reject-or-fallback, not something sent to the vendor and left to surface as a raw API error three layers up the stack. A caller (the turn loop, a sub-agent spawn) that needs a parameter the resolved model doesn't support MUST either drop back to that model's default behavior or fail the selection, never forward an invalid combination. +`GenerationParams.tool_choice.mode` follows the identical rule against `ModelSpec.supported_tool_choice_modes`: a mode the resolved model doesn't declare support for MUST NOT be forwarded to the vendor — reject or fall back to `TOOL_CHOICE_MODE_AUTO` (equivalent to omitting `tool_choice`) at the kernel level, same as an out-of-range thinking param. See [`data-types.md#generationparams`](data-types.md#generationparams). + This is the same reasoning that makes model routing and fallback chains capability-aware: a fallback candidate ([`configuration/agent-profiles.md#model-routing`](../configuration/agent-profiles.md#model-routing)) is only eligible for a given turn if its declared `ModelSpec`/`ThinkingSpec`/`CachingSpec` actually satisfy that turn's real requirements — context window needed, tool-use, vision, thinking — checked mechanically against `GetCapabilities`' declared envelope, not assumed from declaration order alone. A model that's merely *listed* as a fallback but can't actually serve the turn is skipped, the same way an unmet generation parameter is rejected rather than shipped to the wire. ### Cost computation @@ -47,12 +61,23 @@ cost_usd = input_tokens * pricing.input_per_mtok / 1e6 + output_tokens * pricing.output_per_mtok / 1e6 + (cache_write_tokens ?? 0) * pricing.cache_write_per_mtok / 1e6 + (cache_read_tokens ?? 0) * pricing.cache_read_per_mtok / 1e6 + + (reasoning_tokens ?? 0) * pricing.output_per_mtok / 1e6 ``` -These four counters are non-overlapping as vendors report them (a cached-read token is never also counted in `input_tokens`), so this is a plain sum, not a subtraction. +`pricing` here is the `PricingTier` matching both the `usage` event's timestamp AND its `input_tokens` count ([`data-types.md#pricing`](data-types.md#pricing)) — a vendor charging a distinct rate above some input-size threshold means the kernel MUST resolve the tier per-event, not once per `ModelSpec`. `reasoning_tokens` is billed at the output rate — it is never folded into `output_tokens` itself, so it needs its own term in the sum rather than being implicitly included. These five counters are non-overlapping as vendors report them (a cached-read token is never also counted in `input_tokens`, a reasoning token is never also counted in `output_tokens`), so this is a plain sum, not a subtraction. **The kernel MUST compute `cost_usd` immediately upon receiving each `usage` event, using whichever provider plugin version is active at that moment, and MUST persist the computed dollar figure into the state backend event's payload** — not just the raw token counts. This is a replay-fidelity requirement, the same reasoning [`architecture.md`](../architecture.md#versioning--schema-drift--supersedes)'s "supersedes" mechanism already applies elsewhere: vendor pricing changes over time (an "intro pricing through 2026-08-31" window is a realistic example), and a session replayed months later must show what was actually paid at the time, not a figure recomputed against whatever the currently-loaded plugin version happens to declare today. See [`examples.md#cost-computation-worked-example`](examples.md#cost-computation-worked-example) for a worked example illustrating this alongside telemetry, a distinct, side-band concern from the persisted `cost_usd` figure. ## Render Model providers MAY implement `Render` per the general Emit→Render→Paint pipeline ([`architecture.md`](../architecture.md#emit--render--paint-pipeline)), returning the `RenderTree` formally defined in [`frontend/render-tree.md`](../frontend/render-tree.md) — e.g. to render a `thinking` block collapsed by default, or to render usage/cost info specially. If not implemented, the kernel falls back to its generic default rendering. This is a MAY, not a SHOULD — most model-provider payloads (plain text, tool calls) render fine under the generic fallback; the tool-result side (owned by tool providers) is where custom rendering matters more. + +`RenderRequest.schema_version` MUST be set alongside `payload` — the schema version the payload was emitted under, so a `Render` implementation can interpret a payload emitted by an older plugin version consistently when a session is replayed. See [`frontend/render-tree.md#schema-versioning`](../frontend/render-tree.md#schema-versioning) for the versioning scheme itself. + +## `Describe` + +```text +Describe(DescribeRequest{}) -> DescribeResponse{ producer: common.v1.ProducerRef } +``` + +Reports this plugin build's own identity — `{name, version, source, category, protocol_version}` — directly from the running process, the same shape every one of the six category protocols gains in this protocol revision. This exists chiefly for a [`configuration/settings-and-global.md#dev_overrides`](../configuration/settings-and-global.md#dev_overrides) binary, which has no `provider "" { ... }` lock-file entry to read identity from; see [`configuration/lock-file.md`](../configuration/lock-file.md#dev_overrides-and-identity-without-a-lock-entry)'s `dev_overrides` note for the canonical explanation. diff --git a/docs/specifications/state-backend.md b/docs/specifications/state-backend.md index 70c8975..2372b31 100644 --- a/docs/specifications/state-backend.md +++ b/docs/specifications/state-backend.md @@ -66,6 +66,10 @@ CREATE TABLE session_meta ( The one table that isn't append-only — a single row, updated in place as the session progresses. This is the table [Cross-session queries](#cross-session-queries)'s "scan session files directly" approach reads: cheap enough (one row) that opening every session file in the directory to check `parent_session_id`/ `status` is inexpensive even at moderate history size. +`status` is not append-only-monotonic: a `completed` or `cancelled` row MAY transition back to `running` — a re-open, per [`frontend/frontend-protocol.md#resume-and-re-open-semantics`](frontend/frontend-protocol.md#resume-and-re-open-semantics)'s `ResumeSession` — and `ended_at` is cleared back to `NULL` on that transition, the same as for a session's original creation. This is an ordinary in-place `UPDATE` of the existing row, not a new row and not a schema change; `error_max_turns`/`error_max_budget_usd`/`error_max_wall_clock`/`failed` never make this transition — those statuses are terminal and replay-only, never re-opened to `running`. `session.v1.SessionInfo` (the frontend protocol's wire-level read model of this row, see below) carries the same status value either way, so a frontend distinguishes "still on its first run" from "re-opened after completing" only via the sequence of `SessionStatusUpdate` events it has observed, not from `SessionInfo` alone. + +`session.v1.SessionInfo` — the message `frontend/frontend-protocol.md`'s `SessionCreated`/`SessionAttached`/`SessionList` `ServerEvent` variants carry — mirrors this table's columns field-for-field (`session_id`, `parent_session_id`, `profile`, `status`, `depth`, `started_at`, `ended_at`), plus one derived field with no column of its own: `cost_usd`, a cheap `SUM(cost_usd)` over this session's `cost_ledger` rows (below), computed at read time rather than cached in `session_meta` — the same indexed-`SUM` query [`cost_ledger`](#cost_ledger)'s own description already establishes as cheap. + ### cost_ledger Structured spend. @@ -164,7 +168,7 @@ Three things deliberately do **not** get their own `kind`: `session_meta.parent_session_id` exists for **post-hoc** queries — reconstructing a session tree after the fact (a CLI command, an audit), by scanning files (see [Cross-session queries](#cross-session-queries)). It is **not** how live mechanisms like cost-rollup or depth-budget threading ([`agent-loop/subagents.md#depth-limits`](agent-loop/subagents.md#depth-limits)) work — those operate on the kernel's own in-memory session state while `RunSession` calls are actively executing, via the callback data flow already defined in [`kernel-callbacks.md`](kernel-callbacks.md), and never need to open a sqlite file to find an ancestor. The two mechanisms answer different questions (what's happening right now vs. what happened previously) and deliberately don't share a code path. -Replay is the other live/post-hoc distinction worth being explicit about: replaying an old session means feeding its persisted events back through the same Render/Paint path a live session uses, against the state backend instead of the live loop ([`architecture.md#emit--render--paint-pipeline`](architecture.md#emit--render--paint-pipeline)). Telemetry is the one thing that must *not* replay faithfully — trace/span IDs are genuinely non-deterministic and MUST NOT be persisted to `events`, `cost_ledger`, or `plan_items`: this schema deliberately has no `trace_id`/`span_id` column anywhere. Consequently, whatever eventually implements replay MUST select a no-op telemetry driver unconditionally, ignoring whatever driver was configured for the live session. A replayed session re-emitting production telemetry, or attempting to reproduce identical trace/span IDs, would both be wrong in ways this schema's silence on trace/span columns is designed to make structurally impossible. +Replay is the other live/post-hoc distinction worth being explicit about: replaying an old session means feeding its persisted events back through the same Render/Paint path a live session uses, against the state backend instead of the live loop ([`architecture.md#emit--render--paint-pipeline`](architecture.md#emit--render--paint-pipeline)). [`frontend/frontend-protocol.md#backfill--the-replay-path-not-a-new-subsystem`](frontend/frontend-protocol.md#backfill--the-replay-path-not-a-new-subsystem) is exactly this mechanism wearing its frontend-facing name: a newly attaching (or resuming) frontend's backfill batch is this same event-replay-through-Render walk over `events`, in `sequence` order, using each event's own `producer_category`/`producer_name`/`producer_version`/`schema_version` columns to invoke the correct (possibly historical, "supersedes"-resolved) plugin build's `Render` — not a separate, frontend-specific replay implementation. Telemetry is the one thing that must *not* replay faithfully — trace/span IDs are genuinely non-deterministic and MUST NOT be persisted to `events`, `cost_ledger`, or `plan_items`: this schema deliberately has no `trace_id`/`span_id` column anywhere. Consequently, whatever eventually implements replay MUST select a no-op telemetry driver unconditionally, ignoring whatever driver was configured for the live session. A replayed session re-emitting production telemetry, or attempting to reproduce identical trace/span IDs, would both be wrong in ways this schema's silence on trace/span columns is designed to make structurally impossible. ## Cross-session queries diff --git a/docs/specifications/tool/README.md b/docs/specifications/tool/README.md index 5d8086f..d54ec1b 100644 --- a/docs/specifications/tool/README.md +++ b/docs/specifications/tool/README.md @@ -10,13 +10,13 @@ This category depends directly on [`model/`](../model/README.md): the common JSO Subprocess + gRPC via `hashicorp/go-plugin`, per [`architecture.md`](../architecture.md#transport). Standard handshake (magic cookie, protocol version negotiation) applies uniformly across all six provider categories and isn't repeated per category. -A tool provider plugin exposes three RPCs: `GetSchema`, `Configure`, `Invoke`. It MAY additionally implement `Render` (see [`protocol.md#render`](protocol.md#render)). +A tool provider plugin exposes four RPCs: `GetSchema`, `Configure`, `Invoke`, `Describe`. It MAY additionally implement `Render` (see [`protocol.md#render`](protocol.md#render)) and `Preview` (see [`protocol.md#preview`](protocol.md#preview)). **`Invoke` is server-streaming**, the same shape [`model/README.md`](../model/README.md#transport--lifecycle) specifies for `StreamCompletion` and for the identical reason: a tool like `exec` needs to stream live stdout/stderr rather than blocking until completion, and none of the underlying primitives (process exec, HTTP fetch, file I/O) need mid-call client input on the same call. **Cancellation follows the model-provider pattern exactly**: the kernel cancels/closes the gRPC stream; it is not a distinct RPC or a sentinel event the plugin must invent. Plugin authors MUST treat stream cancellation as a normal, expected event — kill the child process, release file handles/sockets, discard buffers — never as an error condition. A tool provider and a model provider are both "long-running, streaming, cancellable" from the kernel's point of view, and giving them different cancellation mechanics would be an unforced inconsistency. ## Category structure -- [`protocol.md`](protocol.md) — the three/four RPCs: `GetSchema` (including the `kind: interactive` sub-classification), `Configure`, `Invoke`, `Render`. +- [`protocol.md`](protocol.md) — the RPCs: `GetSchema` (including the `kind: interactive` sub-classification), `Configure`, `Invoke`, `Describe`, `Render`, `Preview`. - [`data-types.md`](data-types.md) — `ToolSchema`, `RiskClass`, the `ToolCall`/`ToolEvent`/`ToolResult` shapes, and `ConcurrencySpec`. - [`reference-catalog.md`](reference-catalog.md) — the first-party reference tool set this protocol defines, and the genuinely ambiguous classification calls (`bash`, `web_fetch`) worth calling out by name. - [`examples.md`](examples.md) — a worked `agent.hcl` provider block, the real proto wire definitions, and a full `Invoke` event sequence. diff --git a/docs/specifications/tool/conformance.md b/docs/specifications/tool/conformance.md index ef3f657..46f628f 100644 --- a/docs/specifications/tool/conformance.md +++ b/docs/specifications/tool/conformance.md @@ -51,23 +51,35 @@ Kernel's expected reaction per category: On the wire, `process_crashed` maps to `codes.Unavailable` — the same code used for a transient, retriable unavailability elsewhere in the system, since a crashed subprocess is exactly that from the kernel's point of view: the service became unavailable, not that the request itself was invalid. +### The `idempotent` / retry interaction + +`ToolSchema.idempotent` (per [`protocol.md#getschema`](protocol.md#getschema)) is the gate on top of the category-reaction table above for exactly one row: a `retryable` `ToolError` returned for a `TOOL_KIND_RESOURCE` operation. The kernel MAY auto-retry such a failure — without surfacing it to the model as a failed call first — only when that operation's `ToolSchema.idempotent` is `true`; when it's `false` (or unset — proto3's zero value for `bool` is `false`, so an operation MUST explicitly declare `idempotent: true` to opt in, never rely on an implicit default), the kernel MUST treat the failure as terminal for this attempt and surface it, exactly as the category-reaction table already prescribes. `TOOL_KIND_DATA_SOURCE` operations are exempt from this gate entirely — they're implicitly safe to retry regardless of `idempotent`, since by definition they cannot mutate anything. `TOOL_KIND_INTERACTIVE` calls are never auto-retried (per [`protocol.md#kind-interactive`](protocol.md#kind-interactive), a human's answer isn't something a kernel can safely redo unprompted). + +This interacts with, but is distinct from, `concurrency_conflict`'s existing "retry serialized against the same key" reaction: that retry is about serialization ordering, not about whether re-running the operation is safe at all, so it applies independent of `idempotent`. + ## Required vs. optional support — summary matrix | Capability | Level | Notes | |---|---|---| | `GetSchema` / `Configure` / `Invoke` RPCs | MUST | the whole protocol surface | +| `Describe` RPC | MUST | [`protocol.md#describe`](protocol.md#describe); needed for `dev_overrides` plugin identity per [`configuration/lock-file.md`](../configuration/lock-file.md#dev_overrides-and-identity-without-a-lock-entry) | | Streaming RPC shape for `Invoke` | MUST | see [`README.md`](README.md#transport--lifecycle) / [`protocol.md#invoke`](protocol.md#invoke) — applies even to non-streaming operations | +| `ToolCall.call_context` | MUST be set by the kernel, every `Invoke` call | [`protocol.md#invoke`](protocol.md#invoke); `working_directory` is what makes process-backed operations usable at all | | `input_schema`/`output_schema` in the common JSON-Schema subset | MUST | [`model/data-types.md#tool-schema`](../model/data-types.md#tool-schema) | | `kind` (resource / data_source / interactive) | MUST, per operation | drives the plan/apply gate; [`protocol.md#kind-interactive`](protocol.md#kind-interactive) | | `risk` classification | MUST, per operation | see [`data-types.md#riskclass`](data-types.md#riskclass); `read_only` for `data_source` and `interactive` alike | | `ConcurrencySpec.safe` | MUST, per operation except `interactive` | absent/unset MUST be treated as `false`; MUST NOT be declared for `interactive` | | `ConcurrencySpec.key_fields` | MAY, per operation | only meaningful under `safe: true` | +| `default_timeout` | SHOULD, per operation | [`protocol.md#getschema`](protocol.md#getschema); absent means the kernel's global default applies | +| `idempotent` | MUST, per operation | [`protocol.md#getschema`](protocol.md#getschema); gates kernel auto-retry, see above | +| `supported_hook_points` | MAY | [`protocol.md#getschema`](protocol.md#getschema); empty means this provider subscribes no `hook{}` blocks | | `exit_status` event | MUST for process-backed (exec-family) operations; MUST NOT otherwise | | | `output_chunk` / `progress` / `partial_result` events | MAY | only for operations with `streaming: true` | | Structured `ToolError` taxonomy, including `process_crashed` | MUST | | | Strict `output_schema` enforcement | MUST | [`protocol.md#invoke`](protocol.md#invoke) | | Best-effort partial-mutation report on cancellation | MUST, for `resource` operations | see [`protocol.md#invoke`](protocol.md#invoke) | -| `Render` | MAY | generic fallback exists | +| `Render` | MAY | generic fallback exists; `RenderRequest.schema_version` per [`../frontend/render-tree.md#schema-versioning`](../frontend/render-tree.md#schema-versioning) | +| `Preview` | MAY | [`protocol.md#preview`](protocol.md#preview); kernel MUST fall back to raw `arguments` when absent; MUST NOT mutate anything when implemented | ## Open questions diff --git a/docs/specifications/tool/data-types.md b/docs/specifications/tool/data-types.md index dd11205..3b85179 100644 --- a/docs/specifications/tool/data-types.md +++ b/docs/specifications/tool/data-types.md @@ -30,12 +30,19 @@ On the wire, `RiskClass` is declared as an enum with an explicit `RISK_CLASS_UNS ```protobuf ToolCall { - id string // MUST — kernel-assigned, echoed in every emitted event for correlation - tool_name string // MUST — matches a ToolSchema.name from this provider's GetSchema - arguments JSON // MUST — already-parsed JSON conforming to input_schema; per - // model/data-types.md#tool-schema, the kernel's internal ToolCall - // representation always stores parsed arguments regardless of which - // model-provider adapter produced them + id string // MUST — kernel-assigned, echoed in every emitted event for correlation + tool_name string // MUST — matches a ToolSchema.name from this provider's GetSchema + arguments JSON // MUST — already-parsed JSON conforming to input_schema; per + // model/data-types.md#tool-schema, the kernel's internal ToolCall + // representation always stores parsed arguments regardless of which + // model-provider adapter produced them + call_context CallContext // MUST be set by the kernel — pluggableharness.agent.common.v1.CallContext. + // Carries session_id/turn_id, echoed by the plugin on its own + // KernelCallbackService.Emit/Log calls for attribution, and + // working_directory — the cwd a process-backed operation (exec/bash, + // read_file, ...) MUST resolve a relative-path argument against; without + // it those tools have no defined cwd and are unusable. See + // protocol.md#invoke. } ToolEvent = oneof { diff --git a/docs/specifications/tool/examples.md b/docs/specifications/tool/examples.md index c2bfb3f..c540f3b 100644 --- a/docs/specifications/tool/examples.md +++ b/docs/specifications/tool/examples.md @@ -27,12 +27,29 @@ service ToolService { rpc Configure(ConfigureRequest) returns (ConfigureResponse); rpc Invoke(InvokeRequest) returns (stream InvokeResponse); rpc Render(RenderRequest) returns (RenderResponse); + rpc Preview(PreviewRequest) returns (PreviewResponse); + rpc Describe(DescribeRequest) returns (DescribeResponse); } message ToolCall { string id = 1; string tool_name = 2; google.protobuf.Struct arguments = 3; + pluggableharness.agent.common.v1.CallContext call_context = 4; +} + +message PreviewRequest { + ToolCall call = 1; +} + +message PreviewResponse { + pluggableharness.agent.render.v1.RenderTree preview = 1; +} + +message DescribeRequest {} + +message DescribeResponse { + pluggableharness.agent.common.v1.ProducerRef producer = 1; } message ToolEvent { @@ -87,6 +104,7 @@ A `bash` tool call that runs a test suite, streams live output, then reports a n id: "tc_42", tool_name: "bash", arguments: {"command": "go test ./..."}, + call_context: {session_id: "01J...", turn_id: "01J...", working_directory: "/home/steven/code/aiagent"}, } } diff --git a/docs/specifications/tool/protocol.md b/docs/specifications/tool/protocol.md index 3487d7a..a2029bf 100644 --- a/docs/specifications/tool/protocol.md +++ b/docs/specifications/tool/protocol.md @@ -1,6 +1,6 @@ # Tool provider — protocol -The three RPCs a tool provider plugin exposes, plus the optional `Render`. See [`README.md`](README.md#transport--lifecycle) for the transport-level framing (server-streaming, cancellation) that applies to `Invoke` specifically. +The three RPCs a tool provider plugin exposes, plus `Describe` (MUST) and the optional `Render`/`Preview`. See [`README.md`](README.md#transport--lifecycle) for the transport-level framing (server-streaming, cancellation) that applies to `Invoke` specifically. ## `GetSchema` @@ -22,11 +22,19 @@ ToolSchema { // progress, partial_result) before the terminal event; false if // Invoke always emits exactly one terminal event with no lead-up concurrency ConcurrencySpec // MUST, except for kind == interactive — see data-types.md#concurrencyspec + default_timeout Duration? // SHOULD — the deadline the kernel applies to Invoke for this + // operation absent an agent.hcl override; omitted means the + // kernel's own global default applies instead + idempotent bool // MUST — true iff re-running this operation with identical + // arguments cannot produce a different end state than running it + // once; see conformance.md#error-taxonomy for the retry interaction } ``` `kind` and `risk` are deliberately separate axes. `kind` is the binary the plan/apply gate mechanically needs — [`configuration/policy-dsl.md`](../configuration/policy-dsl.md)'s policy examples match on it directly (`match = { kind = "data_source" }`). `risk` exists because `kind = resource` alone is too coarse for policy or UX to treat uniformly — the `bash`/`exec` operation alone spans everything from `ls` to `rm -rf $DIR`. A `resource` MUST declare one of `low`/`moderate`/`high`/`critical`; there is no `resource` with `read_only` risk. `risk` MUST be `read_only` for `kind == data_source` and `kind == interactive` alike — neither mutates nor reads anything external, so neither has a blast radius to classify. See [`data-types.md#riskclass`](data-types.md#riskclass) for the full enum, and [`reference-catalog.md`](reference-catalog.md) for how the reference tool set is classified in practice. +`default_timeout` and `idempotent` are both new, independent capability hints, not part of the `kind`/`risk` classification above. `default_timeout` lets a plugin author declare a sensible per-operation deadline (a `web_search` call and a `read_file` call warrant very different defaults) without every `agent.hcl` author having to override it by hand; the kernel's own configured global default (`configuration/settings-and-global.md`) is the fallback when it's absent. `idempotent` exists purely to gate auto-retry: the kernel MAY only auto-retry a retryable `ToolError` for a `TOOL_KIND_RESOURCE` operation when that operation's `idempotent` is `true` — a `TOOL_KIND_DATA_SOURCE` operation is implicitly safe to retry regardless of this field, since it cannot mutate anything by definition. See [`conformance.md#error-taxonomy`](conformance.md#error-taxonomy) for the full retry interaction. + ### `kind: interactive` A genuine third `kind`, alongside `resource` and `data_source`, for calls that neither mutate state nor perform a pure read — they block the current turn on a human response (per [`frontend/frontend-protocol.md`](../frontend/frontend-protocol.md)'s `interactive_request`/`interactive_response` `ServerEvent`/`ClientEvent` pair) and produce no state mutation of their own — the human's answer becomes the tool's `result`. `ask_user` is the canonical example; see [`reference-catalog.md`](reference-catalog.md) for why it doesn't fit `resource` or `data_source`. @@ -35,7 +43,7 @@ A genuine third `kind`, alongside `resource` and `data_source`, for calls that n - `interactive` calls MUST still pass through a policy precheck before executing — the same non-interactive, `allow`/`deny`-only lane [`configuration/policy-dsl.md`](../configuration/policy-dsl.md) already defines for `data_source` calls, extended to cover this kind too. This exists specifically so an operator can `deny` interactive prompts outright in a non-interactive/headless invocation (a future pipeline mode per [`architecture.md`](../architecture.md#cli-shape)) where there is no human attached to answer one — without policy coverage, an `ask_user`-shaped call in a headless context would simply hang forever with no one able to respond. Note that policy's own `Match.Kind` field stays two-valued (`resource`/`data_source`) in v1 — an interactive call routes through the same non-interactive-style precheck path a `data_source` call uses, rather than policy gaining a third match kind of its own. See [`configuration/policy-dsl.md`](../configuration/policy-dsl.md#match-schema). - `interactive` calls MUST execute **sequentially**, never concurrently with other `interactive` calls in the same turn, regardless of any declared `ConcurrencySpec` — asking a human two things at once in one frontend is inherently confusing. `ConcurrencySpec` MUST NOT be declared for an `interactive` operation; if present, the kernel MUST ignore it and enforce sequential execution unconditionally. -The overall `GetSchema` response (the wrapper around this list of `ToolSchema`s) MAY additionally include `slash_commands: []SlashCommandSpec`, per [`frontend/frontend-protocol.md`](../frontend/frontend-protocol.md) — each entry's `tool_name` MUST reference one of this same provider's own operations declared above. +The overall `GetSchema` response (the wrapper around this list of `ToolSchema`s) MAY additionally include `slash_commands: []SlashCommandSpec`, per [`frontend/frontend-protocol.md`](../frontend/frontend-protocol.md) — each entry's `tool_name` MUST reference one of this same provider's own operations declared above, and `supported_hook_points: []pluggableharness.agent.common.v1.HookPoint`, naming which of the eight dispatchable hook points (`agent-loop/hook-dispatch.md`) this provider's `HookSubscriberService` subscribes to per its own `agent.hcl` `hook{}` blocks. Same capability-advertisement semantics as the other five plugin categories: it lets the kernel validate a `hook{}` declaration against what the plugin actually supports at config-load time, rather than discovering an unsupported subscription only when that hook point first fires. ## `Configure` @@ -51,6 +59,7 @@ Request: a `ToolCall`. Response: a stream of `ToolEvent`s. See [`data-types.md`] Semantics: +- **`ToolCall.call_context` MUST be set by the kernel on every `Invoke` call** — see [`data-types.md#toolcall--toolevent--toolresult`](data-types.md#toolcall--toolevent--toolresult) for the field's shape. Its `working_directory` is what a process-backed operation (the reference catalog's `exec`/`bash`, `read_file`, and similarly-shaped tools) MUST resolve any relative-path argument against; without it, those tools have no defined cwd to operate relative to and are unusable. Its `session_id`/`turn_id` are what the plugin echoes back on its own `KernelCallbackService.Emit`/`Log` calls (`kernel-callbacks.md`) for correlation, sparing every provider from having to thread those IDs through by hand. - **`output_schema` conformance is enforced strictly, not advisory.** The kernel MUST validate a `result.payload` against the operation's declared `output_schema` before accepting it. A non-conforming payload MUST be rejected and re-surfaced to the plugin boundary as an `unknown`-category `ToolError` (see [`conformance.md#error-taxonomy`](conformance.md#error-taxonomy)) — not silently passed through to history, and not a warning-and-continue. Malformed data flowing into the state backend is a correctness bug, not a UX inconvenience to be lenient about. - Exactly one of `result` or `error` MUST close the stream. `output_chunk`, `progress`, and `partial_result` MAY each appear zero or more times before it; `exit_status` MAY appear at most once, and only for tools whose underlying operation is a child process (the exec/shell family). - `exit_status` is distinct from `result` because the two can genuinely be different moments: an exec tool's child process can exit while the tool itself is still doing post-processing (truncating output, computing a diff) before it can emit a conformant `result`. Providers for non-process-backed tools (file read, grep, web fetch) MUST NOT emit `exit_status`. @@ -61,3 +70,19 @@ Semantics: ## Render Same optionality as [`model/protocol.md#render`](../model/protocol.md#render) — returning the `RenderTree` formally defined in [`frontend/render-tree.md`](../frontend/render-tree.md) — but tool-result rendering is where custom `Render` matters *more* than it does for model providers (per [`architecture.md`](../architecture.md#emit--render--paint-pipeline), "the tool-result side ... is where custom rendering matters more"). Reference examples: an `edit_file` result rendering as a unified diff rather than raw before/after text; an `exec` result's accumulated `output_chunk`s rendering as a scrollback pane; a `spawn_subagent` result rendering as a collapsible sub-session node ([`architecture.md`](../architecture.md#emit--render--paint-pipeline)'s `RenderTree` already reserves a node type for this). If not implemented, the kernel falls back to its generic default (pretty-printed JSON payload). + +`RenderRequest.schema_version` names which version of the plugin's own emitted-payload schema `payload` was written under, per [`frontend/render-tree.md#schema-versioning`](../frontend/render-tree.md#schema-versioning) — the canonical definition of the versioning scheme every category's `Render` shares. It lets a long-lived plugin decode a `payload` that an older build of itself emitted, without the kernel needing to know anything about the plugin's internal payload format. + +## Preview + +`Preview` returns a dry-run, human-readable description of what `Invoke(call)` *would* do, without doing it. Request: a `PreviewRequest` wrapping the same `ToolCall` shape `Invoke` takes; MUST NOT actually be executed by the plugin. Response: a `PreviewResponse` wrapping a `RenderTree` (the same [`frontend/render-tree.md`](../frontend/render-tree.md) type `Render` returns), describing the call's effect — e.g. an `edit_file` call previews as the unified diff it would apply, a `bash` call previews as the command line it would run. + +- MAY be implemented. A kernel MUST tolerate its absence (an unimplemented `Preview`, or a provider whose `GetSchema` never advertises support) and fall back to showing the call's raw `arguments` in the plan/apply gate's permission UI. +- MUST NOT mutate anything and MUST be side-effect-free — the same guarantee a `TOOL_KIND_DATA_SOURCE` operation makes, but here it applies unconditionally to `Preview` itself regardless of the underlying call's `ToolKind`. A plugin that cannot produce a preview without performing (part of) the operation MUST NOT implement `Preview` for that operation rather than violate this. +- Exists specifically to feed `pluggableharness.agent.plan.v1.PlanItem.preview` (a sibling protocol revision to this one) — the plan/apply gate renders that field to show a human what a pending `resource` call will actually do before they approve it. `PlanItem.preview` and `PreviewResponse.preview` are pinned to the exact same `pluggableharness.agent.render.v1.RenderTree` type by design, so the kernel can store a `Preview` call's output directly as a plan item's preview without any conversion. + +## Describe + +`Describe` reports this plugin build's own identity: request is empty (`DescribeRequest {}`), response is a `DescribeResponse` wrapping a single `pluggableharness.agent.common.v1.ProducerRef producer`. MUST be implemented — every one of the six category protocols gains this RPC in this same protocol revision. + +This exists for the [`configuration/lock-file.md`](../configuration/lock-file.md#dev_overrides-and-identity-without-a-lock-entry) `dev_overrides` case: a plugin resolved via `dev_overrides` has no `provider "" { ... }` lock-file entry for the kernel to read `{name, version, source, category, protocol_version}` from, because `dev_overrides` exists precisely to bypass registry/lock-file resolution. `Describe` lets the kernel obtain that same identity directly from the running process instead, at connection time. diff --git a/pkg/content/proto/v1/content.pb.go b/pkg/content/proto/v1/content.pb.go index e60e3cc..ee87916 100644 --- a/pkg/content/proto/v1/content.pb.go +++ b/pkg/content/proto/v1/content.pb.go @@ -169,9 +169,37 @@ type Message struct { Role Role `protobuf:"varint,1,opt,name=role,proto3,enum=pluggableharness.agent.content.v1.Role" json:"role,omitempty"` // The message's content, in emission order. A single message MAY carry // multiple blocks (e.g. an assistant turn with both text and a tool_use). - Content []*ContentBlock `protobuf:"bytes,2,rep,name=content,proto3" json:"content,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Content []*ContentBlock `protobuf:"bytes,2,rep,name=content,proto3" json:"content,omitempty"` + // Kernel-assigned ULID, stable across replay. This is the correlation + // anchor for deltas and forking (e.g. a frontend edit-and-resubmit that + // forks history at this message) — assigned once when the message is + // persisted and never reassigned, even when the same conversation is + // replayed against a newer plugin version. MUST be set by the kernel + // before persisting; a plugin never generates this value itself, per + // .claude/rules/determinism.md's ordering-authority rule for kernel- + // assigned identifiers. + Id string `protobuf:"bytes,3,opt,name=id,proto3" json:"id,omitempty"` + // The id of the model that produced this message, when role == + // ROLE_ASSISTANT. A plain string (the target model's ModelSpec.id), not + // a model.v1.ModelRef or model.v1.ModelTarget — content.v1 MUST NOT + // import model.v1, since model.v1 already imports content.v1 (for + // Message/ContentBlock/ContextSection) and a reverse import would be a + // cyclic dependency buf rejects at build time. Omitted for a + // ROLE_USER message, or when the producing model is otherwise unknown. + ProducedByModelId *string `protobuf:"bytes,4,opt,name=produced_by_model_id,json=producedByModelId,proto3,oneof" json:"produced_by_model_id,omitempty"` + // The declared name of the model provider plugin that produced this + // message (common.v1.ProducerRef.name / ProviderRef.name's scope), when + // role == ROLE_ASSISTANT. Same plain-string rationale as + // produced_by_model_id above. Because the kernel's routing/fallback + // chain (model/protocol.md#generation-parameter-validation-and- + // capability-aware-routing) may serve adjacent turns in the same + // session from different providers or different models, two + // consecutive ROLE_ASSISTANT messages in one conversation MAY carry + // different produced_by_model_id/produced_by_provider values — this is + // expected, not an anomaly, and MUST be preserved verbatim on replay. + ProducedByProvider *string `protobuf:"bytes,5,opt,name=produced_by_provider,json=producedByProvider,proto3,oneof" json:"produced_by_provider,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Message) Reset() { @@ -218,6 +246,27 @@ func (x *Message) GetContent() []*ContentBlock { return nil } +func (x *Message) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Message) GetProducedByModelId() string { + if x != nil && x.ProducedByModelId != nil { + return *x.ProducedByModelId + } + return "" +} + +func (x *Message) GetProducedByProvider() string { + if x != nil && x.ProducedByProvider != nil { + return *x.ProducedByProvider + } + return "" +} + // ContentBlock is one block within a Message. Exactly one variant is set. // Which variants a given model MAY produce/accept is gated by that model's // ModelSpec capability flags (model.md §2, §5): `text` MUST work both @@ -234,6 +283,7 @@ type ContentBlock struct { // *ContentBlock_Image // *ContentBlock_Thinking // *ContentBlock_RedactedThinking + // *ContentBlock_Document Block isContentBlock_Block `protobuf_oneof:"block"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -330,6 +380,15 @@ func (x *ContentBlock) GetRedactedThinking() *RedactedThinkingBlock { return nil } +func (x *ContentBlock) GetDocument() *DocumentBlock { + if x != nil { + if x, ok := x.Block.(*ContentBlock_Document); ok { + return x.Document + } + } + return nil +} + type isContentBlock_Block interface { isContentBlock_Block() } @@ -358,6 +417,10 @@ type ContentBlock_RedactedThinking struct { RedactedThinking *RedactedThinkingBlock `protobuf:"bytes,6,opt,name=redacted_thinking,json=redactedThinking,proto3,oneof"` } +type ContentBlock_Document struct { + Document *DocumentBlock `protobuf:"bytes,7,opt,name=document,proto3,oneof"` +} + func (*ContentBlock_Text) isContentBlock_Block() {} func (*ContentBlock_ToolUse) isContentBlock_Block() {} @@ -370,6 +433,8 @@ func (*ContentBlock_Thinking) isContentBlock_Block() {} func (*ContentBlock_RedactedThinking) isContentBlock_Block() {} +func (*ContentBlock_Document) isContentBlock_Block() {} + // TextBlock is plain conversational text. MUST be supported by every // model, in both directions (model.md §5). type TextBlock struct { @@ -728,6 +793,77 @@ func (x *RedactedThinkingBlock) GetData() []byte { return nil } +// DocumentBlock is inline non-image document content (e.g. a PDF) — the +// document-attachment analog of ImageBlock. Requires the target model's +// ModelSpec.supports_documents (model/data-types.md#modelspec); the +// kernel MUST reject a DocumentBlock sent to a model where that flag is +// false, with invalid_request, mirroring ImageBlock's supports_vision +// rule (model/data-types.md#canonical-message--content-block-schema). +type DocumentBlock struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Raw document bytes. + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + // The document's MIME type, e.g. "application/pdf". + MediaType string `protobuf:"bytes,2,opt,name=media_type,json=mediaType,proto3" json:"media_type,omitempty"` + // The document's original filename, when known — several vendors + // surface this to the model as a citation/reference label. MAY be + // omitted. + Filename *string `protobuf:"bytes,3,opt,name=filename,proto3,oneof" json:"filename,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DocumentBlock) Reset() { + *x = DocumentBlock{} + mi := &file_pluggableharness_agent_content_v1_content_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DocumentBlock) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DocumentBlock) ProtoMessage() {} + +func (x *DocumentBlock) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_content_v1_content_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DocumentBlock.ProtoReflect.Descriptor instead. +func (*DocumentBlock) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_content_v1_content_proto_rawDescGZIP(), []int{8} +} + +func (x *DocumentBlock) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +func (x *DocumentBlock) GetMediaType() string { + if x != nil { + return x.MediaType + } + return "" +} + +func (x *DocumentBlock) GetFilename() string { + if x != nil && x.Filename != nil { + return *x.Filename + } + return "" +} + // ContextSection is one provider's contribution to the assembled prompt // context. context.md §4, §7. type ContextSection struct { @@ -761,7 +897,7 @@ type ContextSection struct { func (x *ContextSection) Reset() { *x = ContextSection{} - mi := &file_pluggableharness_agent_content_v1_content_proto_msgTypes[8] + mi := &file_pluggableharness_agent_content_v1_content_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -773,7 +909,7 @@ func (x *ContextSection) String() string { func (*ContextSection) ProtoMessage() {} func (x *ContextSection) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_content_v1_content_proto_msgTypes[8] + mi := &file_pluggableharness_agent_content_v1_content_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -786,7 +922,7 @@ func (x *ContextSection) ProtoReflect() protoreflect.Message { // Deprecated: Use ContextSection.ProtoReflect.Descriptor instead. func (*ContextSection) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_content_v1_content_proto_rawDescGZIP(), []int{8} + return file_pluggableharness_agent_content_v1_content_proto_rawDescGZIP(), []int{9} } func (x *ContextSection) GetProvider() string { @@ -835,10 +971,15 @@ var File_pluggableharness_agent_content_v1_content_proto protoreflect.FileDescri const file_pluggableharness_agent_content_v1_content_proto_rawDesc = "" + "\n" + - "/pluggableharness/agent/content/v1/content.proto\x12!pluggableharness.agent.content.v1\x1a\x1cgoogle/protobuf/struct.proto\"\x91\x01\n" + + "/pluggableharness/agent/content/v1/content.proto\x12!pluggableharness.agent.content.v1\x1a\x1cgoogle/protobuf/struct.proto\"\xc0\x02\n" + "\aMessage\x12;\n" + "\x04role\x18\x01 \x01(\x0e2'.pluggableharness.agent.content.v1.RoleR\x04role\x12I\n" + - "\acontent\x18\x02 \x03(\v2/.pluggableharness.agent.content.v1.ContentBlockR\acontent\"\x80\x04\n" + + "\acontent\x18\x02 \x03(\v2/.pluggableharness.agent.content.v1.ContentBlockR\acontent\x12\x0e\n" + + "\x02id\x18\x03 \x01(\tR\x02id\x124\n" + + "\x14produced_by_model_id\x18\x04 \x01(\tH\x00R\x11producedByModelId\x88\x01\x01\x125\n" + + "\x14produced_by_provider\x18\x05 \x01(\tH\x01R\x12producedByProvider\x88\x01\x01B\x17\n" + + "\x15_produced_by_model_idB\x17\n" + + "\x15_produced_by_provider\"\xd0\x04\n" + "\fContentBlock\x12B\n" + "\x04text\x18\x01 \x01(\v2,.pluggableharness.agent.content.v1.TextBlockH\x00R\x04text\x12L\n" + "\btool_use\x18\x02 \x01(\v2/.pluggableharness.agent.content.v1.ToolUseBlockH\x00R\atoolUse\x12U\n" + @@ -846,7 +987,8 @@ const file_pluggableharness_agent_content_v1_content_proto_rawDesc = "" + "toolResult\x12E\n" + "\x05image\x18\x04 \x01(\v2-.pluggableharness.agent.content.v1.ImageBlockH\x00R\x05image\x12N\n" + "\bthinking\x18\x05 \x01(\v20.pluggableharness.agent.content.v1.ThinkingBlockH\x00R\bthinking\x12g\n" + - "\x11redacted_thinking\x18\x06 \x01(\v28.pluggableharness.agent.content.v1.RedactedThinkingBlockH\x00R\x10redactedThinkingB\a\n" + + "\x11redacted_thinking\x18\x06 \x01(\v28.pluggableharness.agent.content.v1.RedactedThinkingBlockH\x00R\x10redactedThinking\x12N\n" + + "\bdocument\x18\a \x01(\v20.pluggableharness.agent.content.v1.DocumentBlockH\x00R\bdocumentB\a\n" + "\x05block\"\x1f\n" + "\tTextBlock\x12\x12\n" + "\x04text\x18\x01 \x01(\tR\x04text\"i\n" + @@ -867,7 +1009,13 @@ const file_pluggableharness_agent_content_v1_content_proto_rawDesc = "" + "\x04text\x18\x01 \x01(\tR\x04text\x12\x1c\n" + "\tsignature\x18\x02 \x01(\fR\tsignature\"+\n" + "\x15RedactedThinkingBlock\x12\x12\n" + - "\x04data\x18\x01 \x01(\fR\x04data\"\x8f\x02\n" + + "\x04data\x18\x01 \x01(\fR\x04data\"p\n" + + "\rDocumentBlock\x12\x12\n" + + "\x04data\x18\x01 \x01(\fR\x04data\x12\x1d\n" + + "\n" + + "media_type\x18\x02 \x01(\tR\tmediaType\x12\x1f\n" + + "\bfilename\x18\x03 \x01(\tH\x00R\bfilename\x88\x01\x01B\v\n" + + "\t_filename\"\x8f\x02\n" + "\x0eContextSection\x12\x1a\n" + "\bprovider\x18\x01 \x01(\tR\bprovider\x12\x14\n" + "\x05label\x18\x02 \x01(\tR\x05label\x12I\n" + @@ -897,7 +1045,7 @@ func file_pluggableharness_agent_content_v1_content_proto_rawDescGZIP() []byte { } var file_pluggableharness_agent_content_v1_content_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_pluggableharness_agent_content_v1_content_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_pluggableharness_agent_content_v1_content_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_pluggableharness_agent_content_v1_content_proto_goTypes = []any{ (Role)(0), // 0: pluggableharness.agent.content.v1.Role (Stability)(0), // 1: pluggableharness.agent.content.v1.Stability @@ -909,8 +1057,9 @@ var file_pluggableharness_agent_content_v1_content_proto_goTypes = []any{ (*ImageBlock)(nil), // 7: pluggableharness.agent.content.v1.ImageBlock (*ThinkingBlock)(nil), // 8: pluggableharness.agent.content.v1.ThinkingBlock (*RedactedThinkingBlock)(nil), // 9: pluggableharness.agent.content.v1.RedactedThinkingBlock - (*ContextSection)(nil), // 10: pluggableharness.agent.content.v1.ContextSection - (*structpb.Struct)(nil), // 11: google.protobuf.Struct + (*DocumentBlock)(nil), // 10: pluggableharness.agent.content.v1.DocumentBlock + (*ContextSection)(nil), // 11: pluggableharness.agent.content.v1.ContextSection + (*structpb.Struct)(nil), // 12: google.protobuf.Struct } var file_pluggableharness_agent_content_v1_content_proto_depIdxs = []int32{ 0, // 0: pluggableharness.agent.content.v1.Message.role:type_name -> pluggableharness.agent.content.v1.Role @@ -921,15 +1070,16 @@ var file_pluggableharness_agent_content_v1_content_proto_depIdxs = []int32{ 7, // 5: pluggableharness.agent.content.v1.ContentBlock.image:type_name -> pluggableharness.agent.content.v1.ImageBlock 8, // 6: pluggableharness.agent.content.v1.ContentBlock.thinking:type_name -> pluggableharness.agent.content.v1.ThinkingBlock 9, // 7: pluggableharness.agent.content.v1.ContentBlock.redacted_thinking:type_name -> pluggableharness.agent.content.v1.RedactedThinkingBlock - 11, // 8: pluggableharness.agent.content.v1.ToolUseBlock.arguments:type_name -> google.protobuf.Struct - 3, // 9: pluggableharness.agent.content.v1.ToolResultBlock.content:type_name -> pluggableharness.agent.content.v1.ContentBlock - 3, // 10: pluggableharness.agent.content.v1.ContextSection.content:type_name -> pluggableharness.agent.content.v1.ContentBlock - 1, // 11: pluggableharness.agent.content.v1.ContextSection.stability:type_name -> pluggableharness.agent.content.v1.Stability - 12, // [12:12] is the sub-list for method output_type - 12, // [12:12] is the sub-list for method input_type - 12, // [12:12] is the sub-list for extension type_name - 12, // [12:12] is the sub-list for extension extendee - 0, // [0:12] is the sub-list for field type_name + 10, // 8: pluggableharness.agent.content.v1.ContentBlock.document:type_name -> pluggableharness.agent.content.v1.DocumentBlock + 12, // 9: pluggableharness.agent.content.v1.ToolUseBlock.arguments:type_name -> google.protobuf.Struct + 3, // 10: pluggableharness.agent.content.v1.ToolResultBlock.content:type_name -> pluggableharness.agent.content.v1.ContentBlock + 3, // 11: pluggableharness.agent.content.v1.ContextSection.content:type_name -> pluggableharness.agent.content.v1.ContentBlock + 1, // 12: pluggableharness.agent.content.v1.ContextSection.stability:type_name -> pluggableharness.agent.content.v1.Stability + 13, // [13:13] is the sub-list for method output_type + 13, // [13:13] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name } func init() { file_pluggableharness_agent_content_v1_content_proto_init() } @@ -937,6 +1087,7 @@ func file_pluggableharness_agent_content_v1_content_proto_init() { if File_pluggableharness_agent_content_v1_content_proto != nil { return } + file_pluggableharness_agent_content_v1_content_proto_msgTypes[0].OneofWrappers = []any{} file_pluggableharness_agent_content_v1_content_proto_msgTypes[1].OneofWrappers = []any{ (*ContentBlock_Text)(nil), (*ContentBlock_ToolUse)(nil), @@ -944,14 +1095,16 @@ func file_pluggableharness_agent_content_v1_content_proto_init() { (*ContentBlock_Image)(nil), (*ContentBlock_Thinking)(nil), (*ContentBlock_RedactedThinking)(nil), + (*ContentBlock_Document)(nil), } + file_pluggableharness_agent_content_v1_content_proto_msgTypes[8].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_pluggableharness_agent_content_v1_content_proto_rawDesc), len(file_pluggableharness_agent_content_v1_content_proto_rawDesc)), NumEnums: 2, - NumMessages: 9, + NumMessages: 10, NumExtensions: 0, NumServices: 0, }, diff --git a/pkg/context/proto/v1/context.pb.go b/pkg/context/proto/v1/context.pb.go index 51234ff..648ab1e 100644 --- a/pkg/context/proto/v1/context.pb.go +++ b/pkg/context/proto/v1/context.pb.go @@ -20,10 +20,11 @@ package contextv1 import ( + v13 "github.com/pluggableharness/agent/pkg/common/proto/v1" v12 "github.com/pluggableharness/agent/pkg/config/proto/v1" v1 "github.com/pluggableharness/agent/pkg/content/proto/v1" - v13 "github.com/pluggableharness/agent/pkg/model/proto/v1" - v14 "github.com/pluggableharness/agent/pkg/render/proto/v1" + v14 "github.com/pluggableharness/agent/pkg/model/proto/v1" + v15 "github.com/pluggableharness/agent/pkg/render/proto/v1" v11 "github.com/pluggableharness/agent/pkg/slashcommand/proto/v1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" @@ -273,9 +274,18 @@ type ContextCapabilities struct { SlashCommands []*v11.SlashCommandSpec `protobuf:"bytes,4,rep,name=slash_commands,json=slashCommands,proto3" json:"slash_commands,omitempty"` // This provider's agent.hcl config schema, advertised so the kernel knows // what fields Configure accepts. configuration.md §4. - ConfigSchema *v12.ConfigSchema `protobuf:"bytes,5,opt,name=config_schema,json=configSchema,proto3" json:"config_schema,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + ConfigSchema *v12.ConfigSchema `protobuf:"bytes,5,opt,name=config_schema,json=configSchema,proto3" json:"config_schema,omitempty"` + // Which hook points (agent-loop/hook-dispatch.md) this provider declares + // HookSubscriberService.DispatchHook subscriptions for, advertised + // up front alongside its other static properties. MAY be empty — a + // context provider with no hook{} blocks in agent.hcl never has + // DispatchHook called on it. The enum itself lives in common.v1, not + // hook.v1 — hook.v1 imports this package's model/tool/plan dependencies, + // so a category capability message importing hook.v1 directly would + // cycle back through it; common.v1 is the shared leaf package instead. + SupportedHookPoints []v13.HookPoint `protobuf:"varint,6,rep,packed,name=supported_hook_points,json=supportedHookPoints,proto3,enum=pluggableharness.agent.common.v1.HookPoint" json:"supported_hook_points,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ContextCapabilities) Reset() { @@ -343,6 +353,13 @@ func (x *ContextCapabilities) GetConfigSchema() *v12.ConfigSchema { return nil } +func (x *ContextCapabilities) GetSupportedHookPoints() []v13.HookPoint { + if x != nil { + return x.SupportedHookPoints + } + return nil +} + // ConfigureResponse is empty on success. A Configure failure (e.g. a // declared source path/glob resolving to nothing) surfaces as a gRPC status // carrying a ContextError in its structured detail, per @@ -392,8 +409,11 @@ type ContextRequest struct { // The parent session's identifier, when this session is a sub-agent // session. Empty for a top-level session. ParentSessionId string `protobuf:"bytes,2,opt,name=parent_session_id,json=parentSessionId,proto3" json:"parent_session_id,omitempty"` - // Which turn of the session this firing is for. - TurnNumber int64 `protobuf:"varint,3,opt,name=turn_number,json=turnNumber,proto3" json:"turn_number,omitempty"` + // Which turn of the session this firing is for. A ULID, standardized + // across the whole protocol (matches plan.v1's turn_id field). Same field + // number as the retired int64 turn-number predecessor field; the rename + // is a sanctioned pre-release wire change, not a v2 bump. + TurnId string `protobuf:"bytes,3,opt,name=turn_id,json=turnId,proto3" json:"turn_id,omitempty"` // The kernel's computed token allocation for this provider on this call. // MUST be set. A returned section MUST NOT exceed this. context.md §4, // §6. @@ -401,7 +421,7 @@ type ContextRequest struct { // The model this contribution is being assembled for, so the provider can // tailor content (and compute tokens against the right budget) for the // model that will actually consume it. MUST be set. context.md §4. - ModelTarget *v13.ModelTarget `protobuf:"bytes,5,opt,name=model_target,json=modelTarget,proto3" json:"model_target,omitempty"` + ModelTarget *v14.ModelTarget `protobuf:"bytes,5,opt,name=model_target,json=modelTarget,proto3" json:"model_target,omitempty"` // Paths touched so far this session, enabling JIT-scoped contributions // (e.g. a subdirectory-scoped convention-file reader). MAY be empty, e.g. // at turn 0 / session start. context.md §4, §8. @@ -418,8 +438,17 @@ type ContextRequest struct { // is indistinguishable from (and semantically equivalent to) "not // provided". context.md §5.1. ConversationHistory []*v1.Message `protobuf:"bytes,9,rep,name=conversation_history,json=conversationHistory,proto3" json:"conversation_history,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The current conversation-history token total, kernel-computed. One of + // the two signals a compactor provider needs to decide WHEN to compact + // without re-counting history itself — see data-types.md's compactor + // workflow discussion in the orbit of §5.1. + HistoryTokens int64 `protobuf:"varint,10,opt,name=history_tokens,json=historyTokens,proto3" json:"history_tokens,omitempty"` + // The total assembled context size of the previous turn (all providers' + // sections combined), kernel-computed. The second compact-timing signal, + // alongside history_tokens above. + AssembledTokensLastTurn int64 `protobuf:"varint,11,opt,name=assembled_tokens_last_turn,json=assembledTokensLastTurn,proto3" json:"assembled_tokens_last_turn,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ContextRequest) Reset() { @@ -466,11 +495,11 @@ func (x *ContextRequest) GetParentSessionId() string { return "" } -func (x *ContextRequest) GetTurnNumber() int64 { +func (x *ContextRequest) GetTurnId() string { if x != nil { - return x.TurnNumber + return x.TurnId } - return 0 + return "" } func (x *ContextRequest) GetTokenBudget() int64 { @@ -480,7 +509,7 @@ func (x *ContextRequest) GetTokenBudget() int64 { return 0 } -func (x *ContextRequest) GetModelTarget() *v13.ModelTarget { +func (x *ContextRequest) GetModelTarget() *v14.ModelTarget { if x != nil { return x.ModelTarget } @@ -515,6 +544,20 @@ func (x *ContextRequest) GetConversationHistory() []*v1.Message { return nil } +func (x *ContextRequest) GetHistoryTokens() int64 { + if x != nil { + return x.HistoryTokens + } + return 0 +} + +func (x *ContextRequest) GetAssembledTokensLastTurn() int64 { + if x != nil { + return x.AssembledTokensLastTurn + } + return 0 +} + // ContextContribution is Contribute's response: the full, possibly-modified // section chain, with this provider's own section appended — never a // delta. context.md §4. @@ -652,7 +695,14 @@ type RenderRequest struct { // Emit->Render->Paint carve-out. Never interpreted by the kernel or any // other plugin; only the producing provider's own Render implementation // understands its shape. - Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"` + Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"` + // The schema version this payload was emitted against, so a Render + // implementation can detect drift between the version it was built + // against and the version live in a running session. See + // ../frontend/render-tree.md#schema-versioning for the canonical + // definition of this field's semantics (owned by the frontend/widget + // workstream; this field just carries the same value). + SchemaVersion string `protobuf:"bytes,2,opt,name=schema_version,json=schemaVersion,proto3" json:"schema_version,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -694,12 +744,19 @@ func (x *RenderRequest) GetPayload() []byte { return nil } +func (x *RenderRequest) GetSchemaVersion() string { + if x != nil { + return x.SchemaVersion + } + return "" +} + // RenderResponse wraps the rendered output of the optional Render RPC. type RenderResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The rendered tree, per the general Emit->Render->Paint pipeline // (frontend.md §1). context.md §9. - Tree *v14.RenderTree `protobuf:"bytes,1,opt,name=tree,proto3" json:"tree,omitempty"` + Tree *v15.RenderTree `protobuf:"bytes,1,opt,name=tree,proto3" json:"tree,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -734,66 +791,160 @@ func (*RenderResponse) Descriptor() ([]byte, []int) { return file_pluggableharness_agent_context_v1_context_proto_rawDescGZIP(), []int{9} } -func (x *RenderResponse) GetTree() *v14.RenderTree { +func (x *RenderResponse) GetTree() *v15.RenderTree { if x != nil { return x.Tree } return nil } +// DescribeRequest carries no fields — Describe takes no request-scoped +// parameters. +type DescribeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeRequest) Reset() { + *x = DescribeRequest{} + mi := &file_pluggableharness_agent_context_v1_context_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeRequest) ProtoMessage() {} + +func (x *DescribeRequest) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_context_v1_context_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeRequest.ProtoReflect.Descriptor instead. +func (*DescribeRequest) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_context_v1_context_proto_rawDescGZIP(), []int{10} +} + +// DescribeResponse reports this plugin build's own identity. See the +// Describe RPC comment on ContextService above. +type DescribeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // This plugin build's identity: name, version, source, category, + // protocol_version. + Producer *v13.ProducerRef `protobuf:"bytes,1,opt,name=producer,proto3" json:"producer,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeResponse) Reset() { + *x = DescribeResponse{} + mi := &file_pluggableharness_agent_context_v1_context_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeResponse) ProtoMessage() {} + +func (x *DescribeResponse) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_context_v1_context_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeResponse.ProtoReflect.Descriptor instead. +func (*DescribeResponse) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_context_v1_context_proto_rawDescGZIP(), []int{11} +} + +func (x *DescribeResponse) GetProducer() *v13.ProducerRef { + if x != nil { + return x.Producer + } + return nil +} + var File_pluggableharness_agent_context_v1_context_proto protoreflect.FileDescriptor const file_pluggableharness_agent_context_v1_context_proto_rawDesc = "" + "\n" + - "/pluggableharness/agent/context/v1/context.proto\x12!pluggableharness.agent.context.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a-pluggableharness/agent/config/v1/config.proto\x1a/pluggableharness/agent/content/v1/content.proto\x1a+pluggableharness/agent/model/v1/model.proto\x1a-pluggableharness/agent/render/v1/render.proto\x1a9pluggableharness/agent/slashcommand/v1/slashcommand.proto\"\x18\n" + + "/pluggableharness/agent/context/v1/context.proto\x12!pluggableharness.agent.context.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a-pluggableharness/agent/common/v1/common.proto\x1a-pluggableharness/agent/config/v1/config.proto\x1a/pluggableharness/agent/content/v1/content.proto\x1a+pluggableharness/agent/model/v1/model.proto\x1a-pluggableharness/agent/render/v1/render.proto\x1a9pluggableharness/agent/slashcommand/v1/slashcommand.proto\"\x18\n" + "\x16GetCapabilitiesRequest\"u\n" + "\x17GetCapabilitiesResponse\x12Z\n" + "\fcapabilities\x18\x01 \x01(\v26.pluggableharness.agent.context.v1.ContextCapabilitiesR\fcapabilities\"C\n" + "\x10ConfigureRequest\x12/\n" + - "\x06config\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x06config\"\xe7\x02\n" + + "\x06config\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x06config\"\xc8\x03\n" + "\x13ContextCapabilities\x120\n" + "\x14default_token_budget\x18\x01 \x01(\x03R\x12defaultTokenBudget\x12J\n" + "\tstability\x18\x02 \x01(\x0e2,.pluggableharness.agent.content.v1.StabilityR\tstability\x12\x1c\n" + "\tcompactor\x18\x03 \x01(\bR\tcompactor\x12_\n" + "\x0eslash_commands\x18\x04 \x03(\v28.pluggableharness.agent.slashcommand.v1.SlashCommandSpecR\rslashCommands\x12S\n" + - "\rconfig_schema\x18\x05 \x01(\v2..pluggableharness.agent.config.v1.ConfigSchemaR\fconfigSchema\"\x13\n" + - "\x11ConfigureResponse\"\xfb\x03\n" + + "\rconfig_schema\x18\x05 \x01(\v2..pluggableharness.agent.config.v1.ConfigSchemaR\fconfigSchema\x12_\n" + + "\x15supported_hook_points\x18\x06 \x03(\x0e2+.pluggableharness.agent.common.v1.HookPointR\x13supportedHookPoints\"\x13\n" + + "\x11ConfigureResponse\"\xd7\x04\n" + "\x0eContextRequest\x12\x1d\n" + "\n" + "session_id\x18\x01 \x01(\tR\tsessionId\x12*\n" + - "\x11parent_session_id\x18\x02 \x01(\tR\x0fparentSessionId\x12\x1f\n" + - "\vturn_number\x18\x03 \x01(\x03R\n" + - "turnNumber\x12!\n" + + "\x11parent_session_id\x18\x02 \x01(\tR\x0fparentSessionId\x12\x17\n" + + "\aturn_id\x18\x03 \x01(\tR\x06turnId\x12!\n" + "\ftoken_budget\x18\x04 \x01(\x03R\vtokenBudget\x12O\n" + "\fmodel_target\x18\x05 \x01(\v2,.pluggableharness.agent.model.v1.ModelTargetR\vmodelTarget\x12#\n" + "\rfiles_touched\x18\x06 \x03(\tR\ffilesTouched\x12+\n" + "\x11working_directory\x18\a \x01(\tR\x10workingDirectory\x12X\n" + "\x0eprior_sections\x18\b \x03(\v21.pluggableharness.agent.content.v1.ContextSectionR\rpriorSections\x12]\n" + - "\x14conversation_history\x18\t \x03(\v2*.pluggableharness.agent.content.v1.MessageR\x13conversationHistory\"\xbd\x01\n" + + "\x14conversation_history\x18\t \x03(\v2*.pluggableharness.agent.content.v1.MessageR\x13conversationHistory\x12%\n" + + "\x0ehistory_tokens\x18\n" + + " \x01(\x03R\rhistoryTokens\x12;\n" + + "\x1aassembled_tokens_last_turn\x18\v \x01(\x03R\x17assembledTokensLastTurn\"\xbd\x01\n" + "\x13ContextContribution\x12M\n" + "\bsections\x18\x01 \x03(\v21.pluggableharness.agent.content.v1.ContextSectionR\bsections\x12W\n" + "\x11rewritten_history\x18\x02 \x03(\v2*.pluggableharness.agent.content.v1.MessageR\x10rewrittenHistory\"\x9b\x01\n" + "\fContextError\x12S\n" + "\bcategory\x18\x01 \x01(\x0e27.pluggableharness.agent.context.v1.ContextErrorCategoryR\bcategory\x12\x18\n" + "\amessage\x18\x02 \x01(\tR\amessage\x12\x1c\n" + - "\tretryable\x18\x03 \x01(\bR\tretryable\")\n" + + "\tretryable\x18\x03 \x01(\bR\tretryable\"P\n" + "\rRenderRequest\x12\x18\n" + - "\apayload\x18\x01 \x01(\fR\apayload\"R\n" + + "\apayload\x18\x01 \x01(\fR\apayload\x12%\n" + + "\x0eschema_version\x18\x02 \x01(\tR\rschemaVersion\"R\n" + "\x0eRenderResponse\x12@\n" + - "\x04tree\x18\x01 \x01(\v2,.pluggableharness.agent.render.v1.RenderTreeR\x04tree*\x95\x02\n" + + "\x04tree\x18\x01 \x01(\v2,.pluggableharness.agent.render.v1.RenderTreeR\x04tree\"\x11\n" + + "\x0fDescribeRequest\"]\n" + + "\x10DescribeResponse\x12I\n" + + "\bproducer\x18\x01 \x01(\v2-.pluggableharness.agent.common.v1.ProducerRefR\bproducer*\x95\x02\n" + "\x14ContextErrorCategory\x12&\n" + "\"CONTEXT_ERROR_CATEGORY_UNSPECIFIED\x10\x00\x12-\n" + ")CONTEXT_ERROR_CATEGORY_SOURCE_UNAVAILABLE\x10\x01\x12*\n" + "&CONTEXT_ERROR_CATEGORY_BUDGET_EXCEEDED\x10\x02\x12*\n" + "&CONTEXT_ERROR_CATEGORY_SCOPE_VIOLATION\x10\x03\x12*\n" + "&CONTEXT_ERROR_CATEGORY_INVALID_REQUEST\x10\x04\x12\"\n" + - "\x1eCONTEXT_ERROR_CATEGORY_UNKNOWN\x10\x052\xfb\x03\n" + + "\x1eCONTEXT_ERROR_CATEGORY_UNKNOWN\x10\x052\xf0\x04\n" + "\x0eContextService\x12\x88\x01\n" + "\x0fGetCapabilities\x129.pluggableharness.agent.context.v1.GetCapabilitiesRequest\x1a:.pluggableharness.agent.context.v1.GetCapabilitiesResponse\x12v\n" + "\tConfigure\x123.pluggableharness.agent.context.v1.ConfigureRequest\x1a4.pluggableharness.agent.context.v1.ConfigureResponse\x12w\n" + "\n" + "Contribute\x121.pluggableharness.agent.context.v1.ContextRequest\x1a6.pluggableharness.agent.context.v1.ContextContribution\x12m\n" + - "\x06Render\x120.pluggableharness.agent.context.v1.RenderRequest\x1a1.pluggableharness.agent.context.v1.RenderResponseBBZ@github.com/pluggableharness/agent/pkg/context/proto/v1;contextv1b\x06proto3" + "\x06Render\x120.pluggableharness.agent.context.v1.RenderRequest\x1a1.pluggableharness.agent.context.v1.RenderResponse\x12s\n" + + "\bDescribe\x122.pluggableharness.agent.context.v1.DescribeRequest\x1a3.pluggableharness.agent.context.v1.DescribeResponseBBZ@github.com/pluggableharness/agent/pkg/context/proto/v1;contextv1b\x06proto3" var ( file_pluggableharness_agent_context_v1_context_proto_rawDescOnce sync.Once @@ -808,7 +959,7 @@ func file_pluggableharness_agent_context_v1_context_proto_rawDescGZIP() []byte { } var file_pluggableharness_agent_context_v1_context_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_pluggableharness_agent_context_v1_context_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_pluggableharness_agent_context_v1_context_proto_msgTypes = make([]protoimpl.MessageInfo, 12) var file_pluggableharness_agent_context_v1_context_proto_goTypes = []any{ (ContextErrorCategory)(0), // 0: pluggableharness.agent.context.v1.ContextErrorCategory (*GetCapabilitiesRequest)(nil), // 1: pluggableharness.agent.context.v1.GetCapabilitiesRequest @@ -821,41 +972,49 @@ var file_pluggableharness_agent_context_v1_context_proto_goTypes = []any{ (*ContextError)(nil), // 8: pluggableharness.agent.context.v1.ContextError (*RenderRequest)(nil), // 9: pluggableharness.agent.context.v1.RenderRequest (*RenderResponse)(nil), // 10: pluggableharness.agent.context.v1.RenderResponse - (*structpb.Struct)(nil), // 11: google.protobuf.Struct - (v1.Stability)(0), // 12: pluggableharness.agent.content.v1.Stability - (*v11.SlashCommandSpec)(nil), // 13: pluggableharness.agent.slashcommand.v1.SlashCommandSpec - (*v12.ConfigSchema)(nil), // 14: pluggableharness.agent.config.v1.ConfigSchema - (*v13.ModelTarget)(nil), // 15: pluggableharness.agent.model.v1.ModelTarget - (*v1.ContextSection)(nil), // 16: pluggableharness.agent.content.v1.ContextSection - (*v1.Message)(nil), // 17: pluggableharness.agent.content.v1.Message - (*v14.RenderTree)(nil), // 18: pluggableharness.agent.render.v1.RenderTree + (*DescribeRequest)(nil), // 11: pluggableharness.agent.context.v1.DescribeRequest + (*DescribeResponse)(nil), // 12: pluggableharness.agent.context.v1.DescribeResponse + (*structpb.Struct)(nil), // 13: google.protobuf.Struct + (v1.Stability)(0), // 14: pluggableharness.agent.content.v1.Stability + (*v11.SlashCommandSpec)(nil), // 15: pluggableharness.agent.slashcommand.v1.SlashCommandSpec + (*v12.ConfigSchema)(nil), // 16: pluggableharness.agent.config.v1.ConfigSchema + (v13.HookPoint)(0), // 17: pluggableharness.agent.common.v1.HookPoint + (*v14.ModelTarget)(nil), // 18: pluggableharness.agent.model.v1.ModelTarget + (*v1.ContextSection)(nil), // 19: pluggableharness.agent.content.v1.ContextSection + (*v1.Message)(nil), // 20: pluggableharness.agent.content.v1.Message + (*v15.RenderTree)(nil), // 21: pluggableharness.agent.render.v1.RenderTree + (*v13.ProducerRef)(nil), // 22: pluggableharness.agent.common.v1.ProducerRef } var file_pluggableharness_agent_context_v1_context_proto_depIdxs = []int32{ 4, // 0: pluggableharness.agent.context.v1.GetCapabilitiesResponse.capabilities:type_name -> pluggableharness.agent.context.v1.ContextCapabilities - 11, // 1: pluggableharness.agent.context.v1.ConfigureRequest.config:type_name -> google.protobuf.Struct - 12, // 2: pluggableharness.agent.context.v1.ContextCapabilities.stability:type_name -> pluggableharness.agent.content.v1.Stability - 13, // 3: pluggableharness.agent.context.v1.ContextCapabilities.slash_commands:type_name -> pluggableharness.agent.slashcommand.v1.SlashCommandSpec - 14, // 4: pluggableharness.agent.context.v1.ContextCapabilities.config_schema:type_name -> pluggableharness.agent.config.v1.ConfigSchema - 15, // 5: pluggableharness.agent.context.v1.ContextRequest.model_target:type_name -> pluggableharness.agent.model.v1.ModelTarget - 16, // 6: pluggableharness.agent.context.v1.ContextRequest.prior_sections:type_name -> pluggableharness.agent.content.v1.ContextSection - 17, // 7: pluggableharness.agent.context.v1.ContextRequest.conversation_history:type_name -> pluggableharness.agent.content.v1.Message - 16, // 8: pluggableharness.agent.context.v1.ContextContribution.sections:type_name -> pluggableharness.agent.content.v1.ContextSection - 17, // 9: pluggableharness.agent.context.v1.ContextContribution.rewritten_history:type_name -> pluggableharness.agent.content.v1.Message - 0, // 10: pluggableharness.agent.context.v1.ContextError.category:type_name -> pluggableharness.agent.context.v1.ContextErrorCategory - 18, // 11: pluggableharness.agent.context.v1.RenderResponse.tree:type_name -> pluggableharness.agent.render.v1.RenderTree - 1, // 12: pluggableharness.agent.context.v1.ContextService.GetCapabilities:input_type -> pluggableharness.agent.context.v1.GetCapabilitiesRequest - 3, // 13: pluggableharness.agent.context.v1.ContextService.Configure:input_type -> pluggableharness.agent.context.v1.ConfigureRequest - 6, // 14: pluggableharness.agent.context.v1.ContextService.Contribute:input_type -> pluggableharness.agent.context.v1.ContextRequest - 9, // 15: pluggableharness.agent.context.v1.ContextService.Render:input_type -> pluggableharness.agent.context.v1.RenderRequest - 2, // 16: pluggableharness.agent.context.v1.ContextService.GetCapabilities:output_type -> pluggableharness.agent.context.v1.GetCapabilitiesResponse - 5, // 17: pluggableharness.agent.context.v1.ContextService.Configure:output_type -> pluggableharness.agent.context.v1.ConfigureResponse - 7, // 18: pluggableharness.agent.context.v1.ContextService.Contribute:output_type -> pluggableharness.agent.context.v1.ContextContribution - 10, // 19: pluggableharness.agent.context.v1.ContextService.Render:output_type -> pluggableharness.agent.context.v1.RenderResponse - 16, // [16:20] is the sub-list for method output_type - 12, // [12:16] is the sub-list for method input_type - 12, // [12:12] is the sub-list for extension type_name - 12, // [12:12] is the sub-list for extension extendee - 0, // [0:12] is the sub-list for field type_name + 13, // 1: pluggableharness.agent.context.v1.ConfigureRequest.config:type_name -> google.protobuf.Struct + 14, // 2: pluggableharness.agent.context.v1.ContextCapabilities.stability:type_name -> pluggableharness.agent.content.v1.Stability + 15, // 3: pluggableharness.agent.context.v1.ContextCapabilities.slash_commands:type_name -> pluggableharness.agent.slashcommand.v1.SlashCommandSpec + 16, // 4: pluggableharness.agent.context.v1.ContextCapabilities.config_schema:type_name -> pluggableharness.agent.config.v1.ConfigSchema + 17, // 5: pluggableharness.agent.context.v1.ContextCapabilities.supported_hook_points:type_name -> pluggableharness.agent.common.v1.HookPoint + 18, // 6: pluggableharness.agent.context.v1.ContextRequest.model_target:type_name -> pluggableharness.agent.model.v1.ModelTarget + 19, // 7: pluggableharness.agent.context.v1.ContextRequest.prior_sections:type_name -> pluggableharness.agent.content.v1.ContextSection + 20, // 8: pluggableharness.agent.context.v1.ContextRequest.conversation_history:type_name -> pluggableharness.agent.content.v1.Message + 19, // 9: pluggableharness.agent.context.v1.ContextContribution.sections:type_name -> pluggableharness.agent.content.v1.ContextSection + 20, // 10: pluggableharness.agent.context.v1.ContextContribution.rewritten_history:type_name -> pluggableharness.agent.content.v1.Message + 0, // 11: pluggableharness.agent.context.v1.ContextError.category:type_name -> pluggableharness.agent.context.v1.ContextErrorCategory + 21, // 12: pluggableharness.agent.context.v1.RenderResponse.tree:type_name -> pluggableharness.agent.render.v1.RenderTree + 22, // 13: pluggableharness.agent.context.v1.DescribeResponse.producer:type_name -> pluggableharness.agent.common.v1.ProducerRef + 1, // 14: pluggableharness.agent.context.v1.ContextService.GetCapabilities:input_type -> pluggableharness.agent.context.v1.GetCapabilitiesRequest + 3, // 15: pluggableharness.agent.context.v1.ContextService.Configure:input_type -> pluggableharness.agent.context.v1.ConfigureRequest + 6, // 16: pluggableharness.agent.context.v1.ContextService.Contribute:input_type -> pluggableharness.agent.context.v1.ContextRequest + 9, // 17: pluggableharness.agent.context.v1.ContextService.Render:input_type -> pluggableharness.agent.context.v1.RenderRequest + 11, // 18: pluggableharness.agent.context.v1.ContextService.Describe:input_type -> pluggableharness.agent.context.v1.DescribeRequest + 2, // 19: pluggableharness.agent.context.v1.ContextService.GetCapabilities:output_type -> pluggableharness.agent.context.v1.GetCapabilitiesResponse + 5, // 20: pluggableharness.agent.context.v1.ContextService.Configure:output_type -> pluggableharness.agent.context.v1.ConfigureResponse + 7, // 21: pluggableharness.agent.context.v1.ContextService.Contribute:output_type -> pluggableharness.agent.context.v1.ContextContribution + 10, // 22: pluggableharness.agent.context.v1.ContextService.Render:output_type -> pluggableharness.agent.context.v1.RenderResponse + 12, // 23: pluggableharness.agent.context.v1.ContextService.Describe:output_type -> pluggableharness.agent.context.v1.DescribeResponse + 19, // [19:24] is the sub-list for method output_type + 14, // [14:19] is the sub-list for method input_type + 14, // [14:14] is the sub-list for extension type_name + 14, // [14:14] is the sub-list for extension extendee + 0, // [0:14] is the sub-list for field type_name } func init() { file_pluggableharness_agent_context_v1_context_proto_init() } @@ -869,7 +1028,7 @@ func file_pluggableharness_agent_context_v1_context_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_pluggableharness_agent_context_v1_context_proto_rawDesc), len(file_pluggableharness_agent_context_v1_context_proto_rawDesc)), NumEnums: 1, - NumMessages: 10, + NumMessages: 12, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/context/proto/v1/context_grpc.pb.go b/pkg/context/proto/v1/context_grpc.pb.go index b2796d5..e688fbd 100644 --- a/pkg/context/proto/v1/context_grpc.pb.go +++ b/pkg/context/proto/v1/context_grpc.pb.go @@ -36,6 +36,7 @@ const ( ContextService_Configure_FullMethodName = "/pluggableharness.agent.context.v1.ContextService/Configure" ContextService_Contribute_FullMethodName = "/pluggableharness.agent.context.v1.ContextService/Contribute" ContextService_Render_FullMethodName = "/pluggableharness.agent.context.v1.ContextService/Render" + ContextService_Describe_FullMethodName = "/pluggableharness.agent.context.v1.ContextService/Describe" ) // ContextServiceClient is the client API for ContextService service. @@ -82,6 +83,15 @@ type ContextServiceClient interface { // be implemented; if not, the kernel falls back to its generic default // rendering. Render(ctx context.Context, in *RenderRequest, opts ...grpc.CallOption) (*RenderResponse, error) + // Describe reports this plugin build's own identity — {name, version, + // source, category, protocol_version} via ProducerRef — independent of + // any lock-file entry. This is the mechanism a dev_overrides-resolved + // binary (which has no provider {} lock entry to read identity from) + // uses to self-report at connection time; see + // docs/specifications/configuration/lock-file.md's dev_overrides note, + // which is the canonical explanation for this RPC across every plugin + // category that gains it in this protocol revision. + Describe(ctx context.Context, in *DescribeRequest, opts ...grpc.CallOption) (*DescribeResponse, error) } type contextServiceClient struct { @@ -132,6 +142,16 @@ func (c *contextServiceClient) Render(ctx context.Context, in *RenderRequest, op return out, nil } +func (c *contextServiceClient) Describe(ctx context.Context, in *DescribeRequest, opts ...grpc.CallOption) (*DescribeResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DescribeResponse) + err := c.cc.Invoke(ctx, ContextService_Describe_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // ContextServiceServer is the server API for ContextService service. // All implementations must embed UnimplementedContextServiceServer // for forward compatibility. @@ -176,6 +196,15 @@ type ContextServiceServer interface { // be implemented; if not, the kernel falls back to its generic default // rendering. Render(context.Context, *RenderRequest) (*RenderResponse, error) + // Describe reports this plugin build's own identity — {name, version, + // source, category, protocol_version} via ProducerRef — independent of + // any lock-file entry. This is the mechanism a dev_overrides-resolved + // binary (which has no provider {} lock entry to read identity from) + // uses to self-report at connection time; see + // docs/specifications/configuration/lock-file.md's dev_overrides note, + // which is the canonical explanation for this RPC across every plugin + // category that gains it in this protocol revision. + Describe(context.Context, *DescribeRequest) (*DescribeResponse, error) mustEmbedUnimplementedContextServiceServer() } @@ -198,6 +227,9 @@ func (UnimplementedContextServiceServer) Contribute(context.Context, *ContextReq func (UnimplementedContextServiceServer) Render(context.Context, *RenderRequest) (*RenderResponse, error) { return nil, status.Error(codes.Unimplemented, "method Render not implemented") } +func (UnimplementedContextServiceServer) Describe(context.Context, *DescribeRequest) (*DescribeResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Describe not implemented") +} func (UnimplementedContextServiceServer) mustEmbedUnimplementedContextServiceServer() {} func (UnimplementedContextServiceServer) testEmbeddedByValue() {} @@ -291,6 +323,24 @@ func _ContextService_Render_Handler(srv interface{}, ctx context.Context, dec fu return interceptor(ctx, in, info, handler) } +func _ContextService_Describe_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DescribeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ContextServiceServer).Describe(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ContextService_Describe_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ContextServiceServer).Describe(ctx, req.(*DescribeRequest)) + } + return interceptor(ctx, in, info, handler) +} + // ContextService_ServiceDesc is the grpc.ServiceDesc for ContextService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -314,6 +364,10 @@ var ContextService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Render", Handler: _ContextService_Render_Handler, }, + { + MethodName: "Describe", + Handler: _ContextService_Describe_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "pluggableharness/agent/context/v1/context.proto", diff --git a/pkg/frontend/proto/v1/frontend.pb.go b/pkg/frontend/proto/v1/frontend.pb.go index 2ccbd3e..6f3adb4 100644 --- a/pkg/frontend/proto/v1/frontend.pb.go +++ b/pkg/frontend/proto/v1/frontend.pb.go @@ -11,11 +11,14 @@ package frontendv1 import ( - v11 "github.com/pluggableharness/agent/pkg/config/proto/v1" - v13 "github.com/pluggableharness/agent/pkg/plan/proto/v1" - v12 "github.com/pluggableharness/agent/pkg/render/proto/v1" - v14 "github.com/pluggableharness/agent/pkg/session/proto/v1" - v1 "github.com/pluggableharness/agent/pkg/slashcommand/proto/v1" + v1 "github.com/pluggableharness/agent/pkg/common/proto/v1" + v12 "github.com/pluggableharness/agent/pkg/config/proto/v1" + v17 "github.com/pluggableharness/agent/pkg/content/proto/v1" + v16 "github.com/pluggableharness/agent/pkg/model/proto/v1" + v14 "github.com/pluggableharness/agent/pkg/plan/proto/v1" + v13 "github.com/pluggableharness/agent/pkg/render/proto/v1" + v15 "github.com/pluggableharness/agent/pkg/session/proto/v1" + v11 "github.com/pluggableharness/agent/pkg/slashcommand/proto/v1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" structpb "google.golang.org/protobuf/types/known/structpb" @@ -87,6 +90,78 @@ func (ClientDecision) EnumDescriptor() ([]byte, []int) { return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{0} } +// PlanDecisionScope is how durably a ClientEvent.PlanDecision applies, +// beyond just the one PlanItem it names. Orthogonal to ClientDecision: +// decision says allow/deny, scope says how long that verdict is +// remembered. agent-loop/plan-apply-gate.md's "PlanDecisionScope +// semantics" documents evaluation order and the ALWAYS persistence +// obligation. +type PlanDecisionScope int32 + +const ( + // Zero value. Never valid for a real decision; its presence on the wire + // means a caller forgot to set the field. + PlanDecisionScope_PLAN_DECISION_SCOPE_UNSPECIFIED PlanDecisionScope = 0 + // Applies to this PlanItem only. The default a frontend SHOULD send + // when the user hasn't explicitly asked for a broader scope. + PlanDecisionScope_PLAN_DECISION_SCOPE_ONCE PlanDecisionScope = 1 + // Applies to the rest of this session, for calls matching the same + // provider/tool_name (and, where policy's match schema supports it, + // narrower criteria) — an in-memory, session-lifetime rule, not + // written to agent.hcl or any persisted policy store. + PlanDecisionScope_PLAN_DECISION_SCOPE_SESSION PlanDecisionScope = 2 + // The kernel persists this as policy, applying beyond this session to + // future sessions under the same profile. Requires kernel-side policy + // persistence (agent-loop/plan-apply-gate.md#plandecisionscope-semantics) + // — a frontend MUST NOT assume ALWAYS is honored merely because it was + // sent; a kernel that cannot persist policy MUST reject it as a + // distinct error rather than silently downgrading to SESSION or ONCE. + PlanDecisionScope_PLAN_DECISION_SCOPE_ALWAYS PlanDecisionScope = 3 +) + +// Enum value maps for PlanDecisionScope. +var ( + PlanDecisionScope_name = map[int32]string{ + 0: "PLAN_DECISION_SCOPE_UNSPECIFIED", + 1: "PLAN_DECISION_SCOPE_ONCE", + 2: "PLAN_DECISION_SCOPE_SESSION", + 3: "PLAN_DECISION_SCOPE_ALWAYS", + } + PlanDecisionScope_value = map[string]int32{ + "PLAN_DECISION_SCOPE_UNSPECIFIED": 0, + "PLAN_DECISION_SCOPE_ONCE": 1, + "PLAN_DECISION_SCOPE_SESSION": 2, + "PLAN_DECISION_SCOPE_ALWAYS": 3, + } +) + +func (x PlanDecisionScope) Enum() *PlanDecisionScope { + p := new(PlanDecisionScope) + *p = x + return p +} + +func (x PlanDecisionScope) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (PlanDecisionScope) Descriptor() protoreflect.EnumDescriptor { + return file_pluggableharness_agent_frontend_v1_frontend_proto_enumTypes[1].Descriptor() +} + +func (PlanDecisionScope) Type() protoreflect.EnumType { + return &file_pluggableharness_agent_frontend_v1_frontend_proto_enumTypes[1] +} + +func (x PlanDecisionScope) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use PlanDecisionScope.Descriptor instead. +func (PlanDecisionScope) EnumDescriptor() ([]byte, []int) { + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{1} +} + // FrontendErrorCategory classifies a FrontendError, per the error taxonomy // in frontend.md §7. type FrontendErrorCategory int32 @@ -106,6 +181,28 @@ const ( FrontendErrorCategory_FRONTEND_ERROR_CATEGORY_REGION_UNSUPPORTED FrontendErrorCategory = 3 // An error that does not fit any other category. FrontendErrorCategory_FRONTEND_ERROR_CATEGORY_UNKNOWN FrontendErrorCategory = 4 + // AttachSession, ResumeSession, DetachSession, or ListSessions' + // parent_session_id filter named a session_id the kernel has no record + // of. + FrontendErrorCategory_FRONTEND_ERROR_CATEGORY_SESSION_NOT_FOUND FrontendErrorCategory = 5 + // CreateSession failed — an invalid profile, or an unusable + // working_directory. + FrontendErrorCategory_FRONTEND_ERROR_CATEGORY_SESSION_CREATE_FAILED FrontendErrorCategory = 6 + // A session-mutating control event targeted a session currently + // SESSION_STATUS_RUNNING in a way that conflicts with that (reserved + // for future session-mutating control events; no current variant in + // this protocol revision triggers it, since DetachSession is always + // safe on a running session). + FrontendErrorCategory_FRONTEND_ERROR_CATEGORY_SESSION_BUSY FrontendErrorCategory = 7 + // ResumeSession named a session file with a newer PRAGMA user_version + // than this kernel understands (state-backend.md §"Schema migration"). + FrontendErrorCategory_FRONTEND_ERROR_CATEGORY_SCHEMA_TOO_NEW FrontendErrorCategory = 8 + // A user_message (or any other new-turn-inducing event) targeted a + // session attached replay-only per frontend.md §"Resume and re-open + // semantics" — a bound-exhausted (error_max_*) or FAILED session + // resumed via ResumeSession. Distinct from SESSION_BUSY: the session + // isn't running, it's terminal and specifically barred from new turns. + FrontendErrorCategory_FRONTEND_ERROR_CATEGORY_SESSION_REPLAY_ONLY FrontendErrorCategory = 9 ) // Enum value maps for FrontendErrorCategory. @@ -116,13 +213,23 @@ var ( 2: "FRONTEND_ERROR_CATEGORY_INVALID_CLIENT_EVENT", 3: "FRONTEND_ERROR_CATEGORY_REGION_UNSUPPORTED", 4: "FRONTEND_ERROR_CATEGORY_UNKNOWN", + 5: "FRONTEND_ERROR_CATEGORY_SESSION_NOT_FOUND", + 6: "FRONTEND_ERROR_CATEGORY_SESSION_CREATE_FAILED", + 7: "FRONTEND_ERROR_CATEGORY_SESSION_BUSY", + 8: "FRONTEND_ERROR_CATEGORY_SCHEMA_TOO_NEW", + 9: "FRONTEND_ERROR_CATEGORY_SESSION_REPLAY_ONLY", } FrontendErrorCategory_value = map[string]int32{ - "FRONTEND_ERROR_CATEGORY_UNSPECIFIED": 0, - "FRONTEND_ERROR_CATEGORY_RENDER_FAILED": 1, - "FRONTEND_ERROR_CATEGORY_INVALID_CLIENT_EVENT": 2, - "FRONTEND_ERROR_CATEGORY_REGION_UNSUPPORTED": 3, - "FRONTEND_ERROR_CATEGORY_UNKNOWN": 4, + "FRONTEND_ERROR_CATEGORY_UNSPECIFIED": 0, + "FRONTEND_ERROR_CATEGORY_RENDER_FAILED": 1, + "FRONTEND_ERROR_CATEGORY_INVALID_CLIENT_EVENT": 2, + "FRONTEND_ERROR_CATEGORY_REGION_UNSUPPORTED": 3, + "FRONTEND_ERROR_CATEGORY_UNKNOWN": 4, + "FRONTEND_ERROR_CATEGORY_SESSION_NOT_FOUND": 5, + "FRONTEND_ERROR_CATEGORY_SESSION_CREATE_FAILED": 6, + "FRONTEND_ERROR_CATEGORY_SESSION_BUSY": 7, + "FRONTEND_ERROR_CATEGORY_SCHEMA_TOO_NEW": 8, + "FRONTEND_ERROR_CATEGORY_SESSION_REPLAY_ONLY": 9, } ) @@ -137,11 +244,11 @@ func (x FrontendErrorCategory) String() string { } func (FrontendErrorCategory) Descriptor() protoreflect.EnumDescriptor { - return file_pluggableharness_agent_frontend_v1_frontend_proto_enumTypes[1].Descriptor() + return file_pluggableharness_agent_frontend_v1_frontend_proto_enumTypes[2].Descriptor() } func (FrontendErrorCategory) Type() protoreflect.EnumType { - return &file_pluggableharness_agent_frontend_v1_frontend_proto_enumTypes[1] + return &file_pluggableharness_agent_frontend_v1_frontend_proto_enumTypes[2] } func (x FrontendErrorCategory) Number() protoreflect.EnumNumber { @@ -150,9 +257,94 @@ func (x FrontendErrorCategory) Number() protoreflect.EnumNumber { // Deprecated: Use FrontendErrorCategory.Descriptor instead. func (FrontendErrorCategory) EnumDescriptor() ([]byte, []int) { + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{2} +} + +// DescribeRequest carries no fields — Describe takes no parameters. +type DescribeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeRequest) Reset() { + *x = DescribeRequest{} + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeRequest) ProtoMessage() {} + +func (x *DescribeRequest) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeRequest.ProtoReflect.Descriptor instead. +func (*DescribeRequest) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{0} +} + +// DescribeResponse reports this plugin build's own identity, obtained +// directly from the running process rather than a lock-file row — +// configuration/lock-file.md's "dev_overrides and identity without a lock +// entry". +type DescribeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Producer *v1.ProducerRef `protobuf:"bytes,1,opt,name=producer,proto3" json:"producer,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeResponse) Reset() { + *x = DescribeResponse{} + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeResponse) ProtoMessage() {} + +func (x *DescribeResponse) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeResponse.ProtoReflect.Descriptor instead. +func (*DescribeResponse) Descriptor() ([]byte, []int) { return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{1} } +func (x *DescribeResponse) GetProducer() *v1.ProducerRef { + if x != nil { + return x.Producer + } + return nil +} + // GetCapabilitiesRequest carries no fields; capability discovery is not // parameterized. type GetCapabilitiesRequest struct { @@ -163,7 +355,7 @@ type GetCapabilitiesRequest struct { func (x *GetCapabilitiesRequest) Reset() { *x = GetCapabilitiesRequest{} - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[0] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -175,7 +367,7 @@ func (x *GetCapabilitiesRequest) String() string { func (*GetCapabilitiesRequest) ProtoMessage() {} func (x *GetCapabilitiesRequest) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[0] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -188,7 +380,7 @@ func (x *GetCapabilitiesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetCapabilitiesRequest.ProtoReflect.Descriptor instead. func (*GetCapabilitiesRequest) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{0} + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{2} } // GetCapabilitiesResponse wraps FrontendCapabilities for the RPC signature, @@ -202,7 +394,7 @@ type GetCapabilitiesResponse struct { func (x *GetCapabilitiesResponse) Reset() { *x = GetCapabilitiesResponse{} - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[1] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -214,7 +406,7 @@ func (x *GetCapabilitiesResponse) String() string { func (*GetCapabilitiesResponse) ProtoMessage() {} func (x *GetCapabilitiesResponse) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[1] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -227,7 +419,7 @@ func (x *GetCapabilitiesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetCapabilitiesResponse.ProtoReflect.Descriptor instead. func (*GetCapabilitiesResponse) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{1} + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{3} } func (x *GetCapabilitiesResponse) GetCapabilities() *FrontendCapabilities { @@ -242,16 +434,27 @@ func (x *GetCapabilitiesResponse) GetCapabilities() *FrontendCapabilities { type FrontendCapabilities struct { state protoimpl.MessageState `protogen:"open.v1"` // Slash commands this frontend contributes. MAY be empty. - SlashCommands []*v1.SlashCommandSpec `protobuf:"bytes,1,rep,name=slash_commands,json=slashCommands,proto3" json:"slash_commands,omitempty"` + SlashCommands []*v11.SlashCommandSpec `protobuf:"bytes,1,rep,name=slash_commands,json=slashCommands,proto3" json:"slash_commands,omitempty"` // This provider's `agent.hcl` configuration schema (configuration.md §4). - ConfigSchema *v11.ConfigSchema `protobuf:"bytes,2,opt,name=config_schema,json=configSchema,proto3" json:"config_schema,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + ConfigSchema *v12.ConfigSchema `protobuf:"bytes,2,opt,name=config_schema,json=configSchema,proto3" json:"config_schema,omitempty"` + // Regions this frontend proactively declares it can render into. A + // complement to, not a replacement for, the reactive + // FRONTEND_ERROR_CATEGORY_REGION_UNSUPPORTED error a placement the + // frontend can't honor still produces — this lets a producer route + // content preferentially without waiting to find out the hard way. + SupportedRegions []v13.Region `protobuf:"varint,3,rep,packed,name=supported_regions,json=supportedRegions,proto3,enum=pluggableharness.agent.render.v1.Region" json:"supported_regions,omitempty"` + // Hook points this frontend can subscribe to (agent-loop/hook-dispatch.md), + // so a mis-declared agent.hcl hook{} block naming an unsupported point + // can be rejected at config-load time rather than failing at first + // dispatch. + SupportedHookPoints []v1.HookPoint `protobuf:"varint,4,rep,packed,name=supported_hook_points,json=supportedHookPoints,proto3,enum=pluggableharness.agent.common.v1.HookPoint" json:"supported_hook_points,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *FrontendCapabilities) Reset() { *x = FrontendCapabilities{} - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[2] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -263,7 +466,7 @@ func (x *FrontendCapabilities) String() string { func (*FrontendCapabilities) ProtoMessage() {} func (x *FrontendCapabilities) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[2] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -276,23 +479,37 @@ func (x *FrontendCapabilities) ProtoReflect() protoreflect.Message { // Deprecated: Use FrontendCapabilities.ProtoReflect.Descriptor instead. func (*FrontendCapabilities) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{2} + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{4} } -func (x *FrontendCapabilities) GetSlashCommands() []*v1.SlashCommandSpec { +func (x *FrontendCapabilities) GetSlashCommands() []*v11.SlashCommandSpec { if x != nil { return x.SlashCommands } return nil } -func (x *FrontendCapabilities) GetConfigSchema() *v11.ConfigSchema { +func (x *FrontendCapabilities) GetConfigSchema() *v12.ConfigSchema { if x != nil { return x.ConfigSchema } return nil } +func (x *FrontendCapabilities) GetSupportedRegions() []v13.Region { + if x != nil { + return x.SupportedRegions + } + return nil +} + +func (x *FrontendCapabilities) GetSupportedHookPoints() []v1.HookPoint { + if x != nil { + return x.SupportedHookPoints + } + return nil +} + // ConfigureRequest carries this provider's `agent.hcl` configuration as a // dynamic Struct, shaped per the ConfigSchema returned by GetCapabilities // (configuration.md §4). @@ -306,7 +523,7 @@ type ConfigureRequest struct { func (x *ConfigureRequest) Reset() { *x = ConfigureRequest{} - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[3] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -318,7 +535,7 @@ func (x *ConfigureRequest) String() string { func (*ConfigureRequest) ProtoMessage() {} func (x *ConfigureRequest) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[3] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -331,7 +548,7 @@ func (x *ConfigureRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigureRequest.ProtoReflect.Descriptor instead. func (*ConfigureRequest) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{3} + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{5} } func (x *ConfigureRequest) GetConfig() *structpb.Struct { @@ -352,7 +569,7 @@ type ConfigureResponse struct { func (x *ConfigureResponse) Reset() { *x = ConfigureResponse{} - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[4] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -364,7 +581,7 @@ func (x *ConfigureResponse) String() string { func (*ConfigureResponse) ProtoMessage() {} func (x *ConfigureResponse) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[4] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -377,13 +594,27 @@ func (x *ConfigureResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigureResponse.ProtoReflect.Descriptor instead. func (*ConfigureResponse) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{4} + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{6} } // ServerEvent is one message the kernel sends to an attached frontend over -// Attach, described in frontend.md §3.2. Exactly one variant is set. +// the single multiplexed Attach stream, described in frontend.md §3.2. +// Exactly one variant is set. type ServerEvent struct { state protoimpl.MessageState `protogen:"open.v1"` + // The session this event is scoped to. Set for every session-scoped + // variant (stream_delta .. session_status_update); empty for the one + // connection-level variant, session_list. + SessionId string `protobuf:"bytes,100,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + // Correlates a response to the ClientEvent control message that + // triggered it, echoing that message's own request_id + // (hello/create_session/attach_session/resume_session/detach_session/ + // list_sessions all carry one). Set on session_created, session_attached, + // backfill_complete, session_detached, session_list, and on error when + // it answers one of those control requests. Unset for ordinary live + // session events that were not triggered by a specific client request + // (stream_delta, render, plan_ready, ...). + RequestId *string `protobuf:"bytes,101,opt,name=request_id,json=requestId,proto3,oneof" json:"request_id,omitempty"` // Types that are valid to be assigned to Event: // // *ServerEvent_StreamDelta_ @@ -393,6 +624,14 @@ type ServerEvent struct { // *ServerEvent_InteractiveRequest_ // *ServerEvent_SessionTreeUpdate_ // *ServerEvent_Error_ + // *ServerEvent_SessionCreated_ + // *ServerEvent_SessionAttached_ + // *ServerEvent_BackfillComplete_ + // *ServerEvent_SessionDetached_ + // *ServerEvent_SessionList_ + // *ServerEvent_SlashCommandRegistry_ + // *ServerEvent_UsageUpdate_ + // *ServerEvent_SessionStatusUpdate_ Event isServerEvent_Event `protobuf_oneof:"event"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -400,7 +639,7 @@ type ServerEvent struct { func (x *ServerEvent) Reset() { *x = ServerEvent{} - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[5] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -412,7 +651,7 @@ func (x *ServerEvent) String() string { func (*ServerEvent) ProtoMessage() {} func (x *ServerEvent) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[5] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -425,7 +664,21 @@ func (x *ServerEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use ServerEvent.ProtoReflect.Descriptor instead. func (*ServerEvent) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{5} + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{7} +} + +func (x *ServerEvent) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *ServerEvent) GetRequestId() string { + if x != nil && x.RequestId != nil { + return *x.RequestId + } + return "" } func (x *ServerEvent) GetEvent() isServerEvent_Event { @@ -498,6 +751,78 @@ func (x *ServerEvent) GetError() *ServerEvent_Error { return nil } +func (x *ServerEvent) GetSessionCreated() *ServerEvent_SessionCreated { + if x != nil { + if x, ok := x.Event.(*ServerEvent_SessionCreated_); ok { + return x.SessionCreated + } + } + return nil +} + +func (x *ServerEvent) GetSessionAttached() *ServerEvent_SessionAttached { + if x != nil { + if x, ok := x.Event.(*ServerEvent_SessionAttached_); ok { + return x.SessionAttached + } + } + return nil +} + +func (x *ServerEvent) GetBackfillComplete() *ServerEvent_BackfillComplete { + if x != nil { + if x, ok := x.Event.(*ServerEvent_BackfillComplete_); ok { + return x.BackfillComplete + } + } + return nil +} + +func (x *ServerEvent) GetSessionDetached() *ServerEvent_SessionDetached { + if x != nil { + if x, ok := x.Event.(*ServerEvent_SessionDetached_); ok { + return x.SessionDetached + } + } + return nil +} + +func (x *ServerEvent) GetSessionList() *ServerEvent_SessionList { + if x != nil { + if x, ok := x.Event.(*ServerEvent_SessionList_); ok { + return x.SessionList + } + } + return nil +} + +func (x *ServerEvent) GetSlashCommandRegistry() *ServerEvent_SlashCommandRegistry { + if x != nil { + if x, ok := x.Event.(*ServerEvent_SlashCommandRegistry_); ok { + return x.SlashCommandRegistry + } + } + return nil +} + +func (x *ServerEvent) GetUsageUpdate() *ServerEvent_UsageUpdate { + if x != nil { + if x, ok := x.Event.(*ServerEvent_UsageUpdate_); ok { + return x.UsageUpdate + } + } + return nil +} + +func (x *ServerEvent) GetSessionStatusUpdate() *ServerEvent_SessionStatusUpdate { + if x != nil { + if x, ok := x.Event.(*ServerEvent_SessionStatusUpdate_); ok { + return x.SessionStatusUpdate + } + } + return nil +} + type isServerEvent_Event interface { isServerEvent_Event() } @@ -530,6 +855,60 @@ type ServerEvent_Error_ struct { Error *ServerEvent_Error `protobuf:"bytes,7,opt,name=error,proto3,oneof"` } +type ServerEvent_SessionCreated_ struct { + // Acknowledges a ClientEvent.CreateSession, carrying the new + // session's info. The kernel auto-attaches the creating stream to + // this session (frontend.md §"Session lifecycle"). + SessionCreated *ServerEvent_SessionCreated `protobuf:"bytes,8,opt,name=session_created,json=sessionCreated,proto3,oneof"` +} + +type ServerEvent_SessionAttached_ struct { + // Acknowledges a ClientEvent.AttachSession or ResumeSession, carrying + // the session's current info. Opens the backfill batch: the + // replayed `render` events that follow, bracketed by the eventual + // backfill_complete (frontend.md §"Backfill"). + SessionAttached *ServerEvent_SessionAttached `protobuf:"bytes,9,opt,name=session_attached,json=sessionAttached,proto3,oneof"` +} + +type ServerEvent_BackfillComplete_ struct { + // The done-marker closing a backfill batch opened by session_attached + // above. Live events with sequence > last_sequence follow this. + // Unicast to the attaching stream only, never broadcast. + BackfillComplete *ServerEvent_BackfillComplete `protobuf:"bytes,10,opt,name=backfill_complete,json=backfillComplete,proto3,oneof"` +} + +type ServerEvent_SessionDetached_ struct { + // Acknowledges a ClientEvent.DetachSession. + SessionDetached *ServerEvent_SessionDetached `protobuf:"bytes,11,opt,name=session_detached,json=sessionDetached,proto3,oneof"` +} + +type ServerEvent_SessionList_ struct { + // Answers a ClientEvent.ListSessions. Connection-scoped — session_id + // above is empty for this variant. + SessionList *ServerEvent_SessionList `protobuf:"bytes,12,opt,name=session_list,json=sessionList,proto3,oneof"` +} + +type ServerEvent_SlashCommandRegistry_ struct { + // The profile-scoped aggregate slash-command registry across every + // provider loaded for this session, sent on session_attached and + // again whenever the registry changes. + SlashCommandRegistry *ServerEvent_SlashCommandRegistry `protobuf:"bytes,14,opt,name=slash_command_registry,json=slashCommandRegistry,proto3,oneof"` +} + +type ServerEvent_UsageUpdate_ struct { + // Per-turn token/cost accounting and context-budget pressure. + UsageUpdate *ServerEvent_UsageUpdate `protobuf:"bytes,15,opt,name=usage_update,json=usageUpdate,proto3,oneof"` +} + +type ServerEvent_SessionStatusUpdate_ struct { + // This session's OWN lifecycle status. Deliberately distinct from + // session_tree_update above, which reports a CHILD session's status + // — this variant is the attached session's own transition (e.g. + // RUNNING -> COMPLETED, or a bound-exhausted re-open per + // frontend.md §"Resume and re-open semantics"). + SessionStatusUpdate *ServerEvent_SessionStatusUpdate `protobuf:"bytes,16,opt,name=session_status_update,json=sessionStatusUpdate,proto3,oneof"` +} + func (*ServerEvent_StreamDelta_) isServerEvent_Event() {} func (*ServerEvent_Render_) isServerEvent_Event() {} @@ -544,10 +923,38 @@ func (*ServerEvent_SessionTreeUpdate_) isServerEvent_Event() {} func (*ServerEvent_Error_) isServerEvent_Event() {} -// ClientEvent is one message a frontend sends to the kernel over Attach, -// described in frontend.md §3.2. Exactly one variant is set. +func (*ServerEvent_SessionCreated_) isServerEvent_Event() {} + +func (*ServerEvent_SessionAttached_) isServerEvent_Event() {} + +func (*ServerEvent_BackfillComplete_) isServerEvent_Event() {} + +func (*ServerEvent_SessionDetached_) isServerEvent_Event() {} + +func (*ServerEvent_SessionList_) isServerEvent_Event() {} + +func (*ServerEvent_SlashCommandRegistry_) isServerEvent_Event() {} + +func (*ServerEvent_UsageUpdate_) isServerEvent_Event() {} + +func (*ServerEvent_SessionStatusUpdate_) isServerEvent_Event() {} + +// ClientEvent is one message a frontend sends to the kernel over the +// single multiplexed Attach stream, described in frontend.md §3.2. +// Exactly one variant is set. type ClientEvent struct { state protoimpl.MessageState `protogen:"open.v1"` + // The session this event is scoped to. REQUIRED for every + // session-scoped variant (user_message, slash_command, plan_decision, + // interactive_response, action_trigger, interrupt) — the kernel MUST + // reject one of these arriving with an empty session_id as + // FRONTEND_ERROR_CATEGORY_INVALID_CLIENT_EVENT. Empty for the + // connection-level control variants (hello, create_session, + // attach_session, resume_session, detach_session, list_sessions), which + // either don't yet have a bound session or operate across sessions — + // attach_session/resume_session instead name the target session inside + // their own nested message. + SessionId string `protobuf:"bytes,100,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` // Types that are valid to be assigned to Event: // // *ClientEvent_UserMessage_ @@ -556,6 +963,12 @@ type ClientEvent struct { // *ClientEvent_InteractiveResponse_ // *ClientEvent_ActionTrigger_ // *ClientEvent_Interrupt_ + // *ClientEvent_Hello_ + // *ClientEvent_CreateSession_ + // *ClientEvent_AttachSession_ + // *ClientEvent_ResumeSession_ + // *ClientEvent_DetachSession_ + // *ClientEvent_ListSessions_ Event isClientEvent_Event `protobuf_oneof:"event"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -563,7 +976,7 @@ type ClientEvent struct { func (x *ClientEvent) Reset() { *x = ClientEvent{} - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[6] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -575,7 +988,7 @@ func (x *ClientEvent) String() string { func (*ClientEvent) ProtoMessage() {} func (x *ClientEvent) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[6] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -588,7 +1001,14 @@ func (x *ClientEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use ClientEvent.ProtoReflect.Descriptor instead. func (*ClientEvent) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{6} + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{8} +} + +func (x *ClientEvent) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" } func (x *ClientEvent) GetEvent() isClientEvent_Event { @@ -652,23 +1072,77 @@ func (x *ClientEvent) GetInterrupt() *ClientEvent_Interrupt { return nil } -type isClientEvent_Event interface { - isClientEvent_Event() +func (x *ClientEvent) GetHello() *ClientEvent_Hello { + if x != nil { + if x, ok := x.Event.(*ClientEvent_Hello_); ok { + return x.Hello + } + } + return nil } -type ClientEvent_UserMessage_ struct { - UserMessage *ClientEvent_UserMessage `protobuf:"bytes,1,opt,name=user_message,json=userMessage,proto3,oneof"` +func (x *ClientEvent) GetCreateSession() *ClientEvent_CreateSession { + if x != nil { + if x, ok := x.Event.(*ClientEvent_CreateSession_); ok { + return x.CreateSession + } + } + return nil } -type ClientEvent_SlashCommand_ struct { - SlashCommand *ClientEvent_SlashCommand `protobuf:"bytes,2,opt,name=slash_command,json=slashCommand,proto3,oneof"` +func (x *ClientEvent) GetAttachSession() *ClientEvent_AttachSession { + if x != nil { + if x, ok := x.Event.(*ClientEvent_AttachSession_); ok { + return x.AttachSession + } + } + return nil } -type ClientEvent_PlanDecision_ struct { - PlanDecision *ClientEvent_PlanDecision `protobuf:"bytes,3,opt,name=plan_decision,json=planDecision,proto3,oneof"` +func (x *ClientEvent) GetResumeSession() *ClientEvent_ResumeSession { + if x != nil { + if x, ok := x.Event.(*ClientEvent_ResumeSession_); ok { + return x.ResumeSession + } + } + return nil } -type ClientEvent_InteractiveResponse_ struct { +func (x *ClientEvent) GetDetachSession() *ClientEvent_DetachSession { + if x != nil { + if x, ok := x.Event.(*ClientEvent_DetachSession_); ok { + return x.DetachSession + } + } + return nil +} + +func (x *ClientEvent) GetListSessions() *ClientEvent_ListSessions { + if x != nil { + if x, ok := x.Event.(*ClientEvent_ListSessions_); ok { + return x.ListSessions + } + } + return nil +} + +type isClientEvent_Event interface { + isClientEvent_Event() +} + +type ClientEvent_UserMessage_ struct { + UserMessage *ClientEvent_UserMessage `protobuf:"bytes,1,opt,name=user_message,json=userMessage,proto3,oneof"` +} + +type ClientEvent_SlashCommand_ struct { + SlashCommand *ClientEvent_SlashCommand `protobuf:"bytes,2,opt,name=slash_command,json=slashCommand,proto3,oneof"` +} + +type ClientEvent_PlanDecision_ struct { + PlanDecision *ClientEvent_PlanDecision `protobuf:"bytes,3,opt,name=plan_decision,json=planDecision,proto3,oneof"` +} + +type ClientEvent_InteractiveResponse_ struct { InteractiveResponse *ClientEvent_InteractiveResponse `protobuf:"bytes,4,opt,name=interactive_response,json=interactiveResponse,proto3,oneof"` } @@ -680,6 +1154,44 @@ type ClientEvent_Interrupt_ struct { Interrupt *ClientEvent_Interrupt `protobuf:"bytes,6,opt,name=interrupt,proto3,oneof"` } +type ClientEvent_Hello_ struct { + // MAY be sent as the first message on a newly opened Attach stream; + // asserts the protocol version only. Not required to bind any + // session — that happens via the variants below. + Hello *ClientEvent_Hello `protobuf:"bytes,7,opt,name=hello,proto3,oneof"` +} + +type ClientEvent_CreateSession_ struct { + // Creates a new session and auto-attaches this stream to it. + CreateSession *ClientEvent_CreateSession `protobuf:"bytes,8,opt,name=create_session,json=createSession,proto3,oneof"` +} + +type ClientEvent_AttachSession_ struct { + // Subscribes an existing (possibly live) session onto this stream, + // triggering a backfill replay (frontend.md §"Backfill"). + AttachSession *ClientEvent_AttachSession `protobuf:"bytes,9,opt,name=attach_session,json=attachSession,proto3,oneof"` +} + +type ClientEvent_ResumeSession_ struct { + // Attaches a historical session for continuation or replay, per + // frontend.md §"Resume and re-open semantics" — a COMPLETED or + // CANCELLED session MAY be re-opened to RUNNING for new turns; a + // bound-exhausted (error_max_*) or FAILED session attaches + // replay-only and rejects any subsequent user_message for it. + ResumeSession *ClientEvent_ResumeSession `protobuf:"bytes,10,opt,name=resume_session,json=resumeSession,proto3,oneof"` +} + +type ClientEvent_DetachSession_ struct { + // Unsubscribes a session from this stream without affecting the + // session itself. + DetachSession *ClientEvent_DetachSession `protobuf:"bytes,11,opt,name=detach_session,json=detachSession,proto3,oneof"` +} + +type ClientEvent_ListSessions_ struct { + // Requests the connection-scoped session summary list. + ListSessions *ClientEvent_ListSessions `protobuf:"bytes,12,opt,name=list_sessions,json=listSessions,proto3,oneof"` +} + func (*ClientEvent_UserMessage_) isClientEvent_Event() {} func (*ClientEvent_SlashCommand_) isClientEvent_Event() {} @@ -692,6 +1204,18 @@ func (*ClientEvent_ActionTrigger_) isClientEvent_Event() {} func (*ClientEvent_Interrupt_) isClientEvent_Event() {} +func (*ClientEvent_Hello_) isClientEvent_Event() {} + +func (*ClientEvent_CreateSession_) isClientEvent_Event() {} + +func (*ClientEvent_AttachSession_) isClientEvent_Event() {} + +func (*ClientEvent_ResumeSession_) isClientEvent_Event() {} + +func (*ClientEvent_DetachSession_) isClientEvent_Event() {} + +func (*ClientEvent_ListSessions_) isClientEvent_Event() {} + // FrontendError is the structured error type for this category, per // frontend.md §7. Carried in ServerEvent.Error and in the structured detail // of a gRPC status returned from Configure. @@ -707,7 +1231,7 @@ type FrontendError struct { func (x *FrontendError) Reset() { *x = FrontendError{} - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[7] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -719,7 +1243,7 @@ func (x *FrontendError) String() string { func (*FrontendError) ProtoMessage() {} func (x *FrontendError) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[7] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -732,7 +1256,7 @@ func (x *FrontendError) ProtoReflect() protoreflect.Message { // Deprecated: Use FrontendError.ProtoReflect.Descriptor instead. func (*FrontendError) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{7} + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{9} } func (x *FrontendError) GetCategory() FrontendErrorCategory { @@ -765,7 +1289,7 @@ type ServerEvent_StreamDelta struct { func (x *ServerEvent_StreamDelta) Reset() { *x = ServerEvent_StreamDelta{} - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[8] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -777,7 +1301,7 @@ func (x *ServerEvent_StreamDelta) String() string { func (*ServerEvent_StreamDelta) ProtoMessage() {} func (x *ServerEvent_StreamDelta) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[8] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -790,7 +1314,7 @@ func (x *ServerEvent_StreamDelta) ProtoReflect() protoreflect.Message { // Deprecated: Use ServerEvent_StreamDelta.ProtoReflect.Descriptor instead. func (*ServerEvent_StreamDelta) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{5, 0} + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{7, 0} } func (x *ServerEvent_StreamDelta) GetTargetId() string { @@ -811,14 +1335,14 @@ func (x *ServerEvent_StreamDelta) GetText() string { type ServerEvent_Render struct { state protoimpl.MessageState `protogen:"open.v1"` // The content, its target region, and its replace/append behavior. - Content *v12.PlacedContent `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` + Content *v13.PlacedContent `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ServerEvent_Render) Reset() { *x = ServerEvent_Render{} - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[9] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -830,7 +1354,7 @@ func (x *ServerEvent_Render) String() string { func (*ServerEvent_Render) ProtoMessage() {} func (x *ServerEvent_Render) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[9] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -843,10 +1367,10 @@ func (x *ServerEvent_Render) ProtoReflect() protoreflect.Message { // Deprecated: Use ServerEvent_Render.ProtoReflect.Descriptor instead. func (*ServerEvent_Render) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{5, 1} + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{7, 1} } -func (x *ServerEvent_Render) GetContent() *v12.PlacedContent { +func (x *ServerEvent_Render) GetContent() *v13.PlacedContent { if x != nil { return x.Content } @@ -859,14 +1383,14 @@ func (x *ServerEvent_Render) GetContent() *v12.PlacedContent { type ServerEvent_PermissionRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The plan item awaiting a decision. - PlanItem *v13.PlanItem `protobuf:"bytes,1,opt,name=plan_item,json=planItem,proto3" json:"plan_item,omitempty"` + PlanItem *v14.PlanItem `protobuf:"bytes,1,opt,name=plan_item,json=planItem,proto3" json:"plan_item,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ServerEvent_PermissionRequest) Reset() { *x = ServerEvent_PermissionRequest{} - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[10] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -878,7 +1402,7 @@ func (x *ServerEvent_PermissionRequest) String() string { func (*ServerEvent_PermissionRequest) ProtoMessage() {} func (x *ServerEvent_PermissionRequest) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[10] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -891,10 +1415,10 @@ func (x *ServerEvent_PermissionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ServerEvent_PermissionRequest.ProtoReflect.Descriptor instead. func (*ServerEvent_PermissionRequest) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{5, 2} + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{7, 2} } -func (x *ServerEvent_PermissionRequest) GetPlanItem() *v13.PlanItem { +func (x *ServerEvent_PermissionRequest) GetPlanItem() *v14.PlanItem { if x != nil { return x.PlanItem } @@ -906,14 +1430,14 @@ func (x *ServerEvent_PermissionRequest) GetPlanItem() *v13.PlanItem { type ServerEvent_PlanReady struct { state protoimpl.MessageState `protogen:"open.v1"` // The plan to display. - Plan *v13.Plan `protobuf:"bytes,1,opt,name=plan,proto3" json:"plan,omitempty"` + Plan *v14.Plan `protobuf:"bytes,1,opt,name=plan,proto3" json:"plan,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ServerEvent_PlanReady) Reset() { *x = ServerEvent_PlanReady{} - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[11] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -925,7 +1449,7 @@ func (x *ServerEvent_PlanReady) String() string { func (*ServerEvent_PlanReady) ProtoMessage() {} func (x *ServerEvent_PlanReady) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[11] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -938,10 +1462,10 @@ func (x *ServerEvent_PlanReady) ProtoReflect() protoreflect.Message { // Deprecated: Use ServerEvent_PlanReady.ProtoReflect.Descriptor instead. func (*ServerEvent_PlanReady) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{5, 3} + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{7, 3} } -func (x *ServerEvent_PlanReady) GetPlan() *v13.Plan { +func (x *ServerEvent_PlanReady) GetPlan() *v14.Plan { if x != nil { return x.Plan } @@ -960,14 +1484,14 @@ type ServerEvent_InteractiveRequest struct { // The tool being invoked (tool.md §2.1 ToolSchema.name). ToolName string `protobuf:"bytes,2,opt,name=tool_name,json=toolName,proto3" json:"tool_name,omitempty"` // The prompt to render. - Prompt *v12.RenderTree `protobuf:"bytes,3,opt,name=prompt,proto3" json:"prompt,omitempty"` + Prompt *v13.RenderTree `protobuf:"bytes,3,opt,name=prompt,proto3" json:"prompt,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ServerEvent_InteractiveRequest) Reset() { *x = ServerEvent_InteractiveRequest{} - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[12] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -979,7 +1503,7 @@ func (x *ServerEvent_InteractiveRequest) String() string { func (*ServerEvent_InteractiveRequest) ProtoMessage() {} func (x *ServerEvent_InteractiveRequest) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[12] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -992,7 +1516,7 @@ func (x *ServerEvent_InteractiveRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ServerEvent_InteractiveRequest.ProtoReflect.Descriptor instead. func (*ServerEvent_InteractiveRequest) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{5, 4} + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{7, 4} } func (x *ServerEvent_InteractiveRequest) GetCallId() string { @@ -1009,7 +1533,7 @@ func (x *ServerEvent_InteractiveRequest) GetToolName() string { return "" } -func (x *ServerEvent_InteractiveRequest) GetPrompt() *v12.RenderTree { +func (x *ServerEvent_InteractiveRequest) GetPrompt() *v13.RenderTree { if x != nil { return x.Prompt } @@ -1018,7 +1542,9 @@ func (x *ServerEvent_InteractiveRequest) GetPrompt() *v12.RenderTree { // SessionTreeUpdate reports a change in a nested sub-session's lifecycle // (e.g. a RunSession-spawned child), so a frontend can keep a -// SubSessionNode's displayed status current. +// SubSessionNode's displayed status current. Reports a CHILD session's +// status — for the attached session's OWN status, see +// SessionStatusUpdate below, a deliberately distinct variant. type ServerEvent_SessionTreeUpdate struct { state protoimpl.MessageState `protogen:"open.v1"` // The parent session's id. @@ -1026,14 +1552,14 @@ type ServerEvent_SessionTreeUpdate struct { // The child session's id. ChildSessionId string `protobuf:"bytes,2,opt,name=child_session_id,json=childSessionId,proto3" json:"child_session_id,omitempty"` // The child session's current status. - Status v14.SessionStatus `protobuf:"varint,3,opt,name=status,proto3,enum=pluggableharness.agent.session.v1.SessionStatus" json:"status,omitempty"` + Status v15.SessionStatus `protobuf:"varint,3,opt,name=status,proto3,enum=pluggableharness.agent.session.v1.SessionStatus" json:"status,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ServerEvent_SessionTreeUpdate) Reset() { *x = ServerEvent_SessionTreeUpdate{} - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[13] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1045,7 +1571,7 @@ func (x *ServerEvent_SessionTreeUpdate) String() string { func (*ServerEvent_SessionTreeUpdate) ProtoMessage() {} func (x *ServerEvent_SessionTreeUpdate) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[13] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1058,7 +1584,7 @@ func (x *ServerEvent_SessionTreeUpdate) ProtoReflect() protoreflect.Message { // Deprecated: Use ServerEvent_SessionTreeUpdate.ProtoReflect.Descriptor instead. func (*ServerEvent_SessionTreeUpdate) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{5, 5} + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{7, 5} } func (x *ServerEvent_SessionTreeUpdate) GetParentSessionId() string { @@ -1075,11 +1601,11 @@ func (x *ServerEvent_SessionTreeUpdate) GetChildSessionId() string { return "" } -func (x *ServerEvent_SessionTreeUpdate) GetStatus() v14.SessionStatus { +func (x *ServerEvent_SessionTreeUpdate) GetStatus() v15.SessionStatus { if x != nil { return x.Status } - return v14.SessionStatus(0) + return v15.SessionStatus(0) } // Error carries a structured, non-fatal frontend error for display. @@ -1093,7 +1619,7 @@ type ServerEvent_Error struct { func (x *ServerEvent_Error) Reset() { *x = ServerEvent_Error{} - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[14] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1105,7 +1631,7 @@ func (x *ServerEvent_Error) String() string { func (*ServerEvent_Error) ProtoMessage() {} func (x *ServerEvent_Error) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[14] + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1118,7 +1644,7 @@ func (x *ServerEvent_Error) ProtoReflect() protoreflect.Message { // Deprecated: Use ServerEvent_Error.ProtoReflect.Descriptor instead. func (*ServerEvent_Error) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{5, 6} + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{7, 6} } func (x *ServerEvent_Error) GetError() *FrontendError { @@ -1128,30 +1654,30 @@ func (x *ServerEvent_Error) GetError() *FrontendError { return nil } -// UserMessage is ordinary chat input from the user. -type ClientEvent_UserMessage struct { +// SessionCreated acknowledges a successful ClientEvent.CreateSession. +type ServerEvent_SessionCreated struct { state protoimpl.MessageState `protogen:"open.v1"` - // The message text. - Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` + // The newly created session's info. + Info *v15.SessionInfo `protobuf:"bytes,1,opt,name=info,proto3" json:"info,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ClientEvent_UserMessage) Reset() { - *x = ClientEvent_UserMessage{} - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[15] +func (x *ServerEvent_SessionCreated) Reset() { + *x = ServerEvent_SessionCreated{} + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ClientEvent_UserMessage) String() string { +func (x *ServerEvent_SessionCreated) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ClientEvent_UserMessage) ProtoMessage() {} +func (*ServerEvent_SessionCreated) ProtoMessage() {} -func (x *ClientEvent_UserMessage) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[15] +func (x *ServerEvent_SessionCreated) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1162,44 +1688,43 @@ func (x *ClientEvent_UserMessage) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ClientEvent_UserMessage.ProtoReflect.Descriptor instead. -func (*ClientEvent_UserMessage) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{6, 0} +// Deprecated: Use ServerEvent_SessionCreated.ProtoReflect.Descriptor instead. +func (*ServerEvent_SessionCreated) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{7, 7} } -func (x *ClientEvent_UserMessage) GetText() string { +func (x *ServerEvent_SessionCreated) GetInfo() *v15.SessionInfo { if x != nil { - return x.Text + return x.Info } - return "" + return nil } -// SlashCommand is a dispatched slash command invocation (frontend.md §5). -type ClientEvent_SlashCommand struct { +// SessionAttached acknowledges a successful ClientEvent.AttachSession or +// ResumeSession, and opens that session's backfill batch. +type ServerEvent_SessionAttached struct { state protoimpl.MessageState `protogen:"open.v1"` - // The command name, without its leading slash. - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // The raw argument string following the command name. - Args string `protobuf:"bytes,2,opt,name=args,proto3" json:"args,omitempty"` + // The attached session's current info. + Info *v15.SessionInfo `protobuf:"bytes,1,opt,name=info,proto3" json:"info,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ClientEvent_SlashCommand) Reset() { - *x = ClientEvent_SlashCommand{} - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[16] +func (x *ServerEvent_SessionAttached) Reset() { + *x = ServerEvent_SessionAttached{} + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ClientEvent_SlashCommand) String() string { +func (x *ServerEvent_SessionAttached) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ClientEvent_SlashCommand) ProtoMessage() {} +func (*ServerEvent_SessionAttached) ProtoMessage() {} -func (x *ClientEvent_SlashCommand) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[16] +func (x *ServerEvent_SessionAttached) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1210,57 +1735,44 @@ func (x *ClientEvent_SlashCommand) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ClientEvent_SlashCommand.ProtoReflect.Descriptor instead. -func (*ClientEvent_SlashCommand) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{6, 1} -} - -func (x *ClientEvent_SlashCommand) GetName() string { - if x != nil { - return x.Name - } - return "" +// Deprecated: Use ServerEvent_SessionAttached.ProtoReflect.Descriptor instead. +func (*ServerEvent_SessionAttached) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{7, 8} } -func (x *ClientEvent_SlashCommand) GetArgs() string { +func (x *ServerEvent_SessionAttached) GetInfo() *v15.SessionInfo { if x != nil { - return x.Args + return x.Info } - return "" + return nil } -// PlanDecision resolves a pending ServerEvent.PermissionRequest. -type ClientEvent_PlanDecision struct { +// BackfillComplete is the done-marker closing a backfill batch, per +// frontend.md §"Backfill". Unicast to the attaching stream only. +type ServerEvent_BackfillComplete struct { state protoimpl.MessageState `protogen:"open.v1"` - // The plan item being resolved, matching - // ServerEvent.PermissionRequest.plan_item's id. - PlanItemId string `protobuf:"bytes,1,opt,name=plan_item_id,json=planItemId,proto3" json:"plan_item_id,omitempty"` - // The user's decision. - Decision ClientDecision `protobuf:"varint,2,opt,name=decision,proto3,enum=pluggableharness.agent.frontend.v1.ClientDecision" json:"decision,omitempty"` - // When present, a user-edited replacement for the plan item's tool - // input. The kernel MUST re-validate this against the tool's - // input_schema (tool.md §6); an invalid correction is rejected as a - // distinct error, not silently coerced. - CorrectedInput *structpb.Struct `protobuf:"bytes,3,opt,name=corrected_input,json=correctedInput,proto3,oneof" json:"corrected_input,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The persisted sequence number of the last event replayed in this + // batch. Live events with sequence > last_sequence follow. + LastSequence int64 `protobuf:"varint,1,opt,name=last_sequence,json=lastSequence,proto3" json:"last_sequence,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *ClientEvent_PlanDecision) Reset() { - *x = ClientEvent_PlanDecision{} - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[17] +func (x *ServerEvent_BackfillComplete) Reset() { + *x = ServerEvent_BackfillComplete{} + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ClientEvent_PlanDecision) String() string { +func (x *ServerEvent_BackfillComplete) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ClientEvent_PlanDecision) ProtoMessage() {} +func (*ServerEvent_BackfillComplete) ProtoMessage() {} -func (x *ClientEvent_PlanDecision) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[17] +func (x *ServerEvent_BackfillComplete) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1271,59 +1783,40 @@ func (x *ClientEvent_PlanDecision) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ClientEvent_PlanDecision.ProtoReflect.Descriptor instead. -func (*ClientEvent_PlanDecision) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{6, 2} -} - -func (x *ClientEvent_PlanDecision) GetPlanItemId() string { - if x != nil { - return x.PlanItemId - } - return "" -} - -func (x *ClientEvent_PlanDecision) GetDecision() ClientDecision { - if x != nil { - return x.Decision - } - return ClientDecision_CLIENT_DECISION_UNSPECIFIED +// Deprecated: Use ServerEvent_BackfillComplete.ProtoReflect.Descriptor instead. +func (*ServerEvent_BackfillComplete) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{7, 9} } -func (x *ClientEvent_PlanDecision) GetCorrectedInput() *structpb.Struct { +func (x *ServerEvent_BackfillComplete) GetLastSequence() int64 { if x != nil { - return x.CorrectedInput + return x.LastSequence } - return nil + return 0 } -// InteractiveResponse resolves a pending ServerEvent.InteractiveRequest. -type ClientEvent_InteractiveResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Correlates to ServerEvent.InteractiveRequest.call_id. - CallId string `protobuf:"bytes,1,opt,name=call_id,json=callId,proto3" json:"call_id,omitempty"` - // The user's response, becoming the interactive call's - // ToolResult.payload. - Response *structpb.Struct `protobuf:"bytes,2,opt,name=response,proto3" json:"response,omitempty"` +// SessionDetached acknowledges a successful ClientEvent.DetachSession. +type ServerEvent_SessionDetached struct { + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ClientEvent_InteractiveResponse) Reset() { - *x = ClientEvent_InteractiveResponse{} - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[18] +func (x *ServerEvent_SessionDetached) Reset() { + *x = ServerEvent_SessionDetached{} + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ClientEvent_InteractiveResponse) String() string { +func (x *ServerEvent_SessionDetached) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ClientEvent_InteractiveResponse) ProtoMessage() {} +func (*ServerEvent_SessionDetached) ProtoMessage() {} -func (x *ClientEvent_InteractiveResponse) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[18] +func (x *ServerEvent_SessionDetached) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1334,59 +1827,35 @@ func (x *ClientEvent_InteractiveResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ClientEvent_InteractiveResponse.ProtoReflect.Descriptor instead. -func (*ClientEvent_InteractiveResponse) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{6, 3} -} - -func (x *ClientEvent_InteractiveResponse) GetCallId() string { - if x != nil { - return x.CallId - } - return "" -} - -func (x *ClientEvent_InteractiveResponse) GetResponse() *structpb.Struct { - if x != nil { - return x.Response - } - return nil +// Deprecated: Use ServerEvent_SessionDetached.ProtoReflect.Descriptor instead. +func (*ServerEvent_SessionDetached) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{7, 10} } -// ActionTrigger is dispatched when a user activates a RenderNode's -// ActionNode (render.proto's ActionNode, frontend.md §5.1). The kernel -// handles this identically to a direct_invoke slash command: the normal -// Invoke/plan-apply pipeline including policy evaluation, with no model -// turn. -type ClientEvent_ActionTrigger struct { +// SessionList answers a ClientEvent.ListSessions. +type ServerEvent_SessionList struct { state protoimpl.MessageState `protogen:"open.v1"` - // The originating ActionNode's id (render.proto's ActionNode.id). - NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` - // The tool operation to invoke (tool.md §2 ToolSchema.name), echoed - // unchanged from the originating ActionNode.tool_name. - ToolName string `protobuf:"bytes,2,opt,name=tool_name,json=toolName,proto3" json:"tool_name,omitempty"` - // The arguments to invoke it with, echoed unchanged from the - // originating ActionNode.args. - Args *structpb.Struct `protobuf:"bytes,3,opt,name=args,proto3" json:"args,omitempty"` + // The matching sessions, most-recently-started first. + Sessions []*v15.SessionInfo `protobuf:"bytes,1,rep,name=sessions,proto3" json:"sessions,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ClientEvent_ActionTrigger) Reset() { - *x = ClientEvent_ActionTrigger{} - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[19] +func (x *ServerEvent_SessionList) Reset() { + *x = ServerEvent_SessionList{} + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ClientEvent_ActionTrigger) String() string { +func (x *ServerEvent_SessionList) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ClientEvent_ActionTrigger) ProtoMessage() {} +func (*ServerEvent_SessionList) ProtoMessage() {} -func (x *ClientEvent_ActionTrigger) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[19] +func (x *ServerEvent_SessionList) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1397,55 +1866,45 @@ func (x *ClientEvent_ActionTrigger) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ClientEvent_ActionTrigger.ProtoReflect.Descriptor instead. -func (*ClientEvent_ActionTrigger) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{6, 4} -} - -func (x *ClientEvent_ActionTrigger) GetNodeId() string { - if x != nil { - return x.NodeId - } - return "" -} - -func (x *ClientEvent_ActionTrigger) GetToolName() string { - if x != nil { - return x.ToolName - } - return "" +// Deprecated: Use ServerEvent_SessionList.ProtoReflect.Descriptor instead. +func (*ServerEvent_SessionList) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{7, 11} } -func (x *ClientEvent_ActionTrigger) GetArgs() *structpb.Struct { +func (x *ServerEvent_SessionList) GetSessions() []*v15.SessionInfo { if x != nil { - return x.Args + return x.Sessions } return nil } -// Interrupt carries no fields; it signals that the user wants to -// interrupt the current turn. -type ClientEvent_Interrupt struct { - state protoimpl.MessageState `protogen:"open.v1"` +// SlashCommandRegistry is the profile-scoped aggregate of every loaded +// provider's declared slash commands for this session, per +// frontend.md §"Slash commands". +type ServerEvent_SlashCommandRegistry struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Every registered command, name-collision-checked at config-load + // time (frontend.md §"Slash commands"). + Commands []*v11.SlashCommandSpec `protobuf:"bytes,1,rep,name=commands,proto3" json:"commands,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ClientEvent_Interrupt) Reset() { - *x = ClientEvent_Interrupt{} - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[20] +func (x *ServerEvent_SlashCommandRegistry) Reset() { + *x = ServerEvent_SlashCommandRegistry{} + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ClientEvent_Interrupt) String() string { +func (x *ServerEvent_SlashCommandRegistry) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ClientEvent_Interrupt) ProtoMessage() {} +func (*ServerEvent_SlashCommandRegistry) ProtoMessage() {} -func (x *ClientEvent_Interrupt) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[20] +func (x *ServerEvent_SlashCommandRegistry) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1456,26 +1915,905 @@ func (x *ClientEvent_Interrupt) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ClientEvent_Interrupt.ProtoReflect.Descriptor instead. -func (*ClientEvent_Interrupt) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{6, 5} +// Deprecated: Use ServerEvent_SlashCommandRegistry.ProtoReflect.Descriptor instead. +func (*ServerEvent_SlashCommandRegistry) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{7, 12} } -var File_pluggableharness_agent_frontend_v1_frontend_proto protoreflect.FileDescriptor - -const file_pluggableharness_agent_frontend_v1_frontend_proto_rawDesc = "" + - "\n" + - "1pluggableharness/agent/frontend/v1/frontend.proto\x12\"pluggableharness.agent.frontend.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a-pluggableharness/agent/config/v1/config.proto\x1a)pluggableharness/agent/plan/v1/plan.proto\x1a-pluggableharness/agent/render/v1/render.proto\x1a/pluggableharness/agent/session/v1/session.proto\x1a9pluggableharness/agent/slashcommand/v1/slashcommand.proto\"\x18\n" + +func (x *ServerEvent_SlashCommandRegistry) GetCommands() []*v11.SlashCommandSpec { + if x != nil { + return x.Commands + } + return nil +} + +// UsageUpdate carries one turn's token/cost accounting and the +// session's running totals, for a context-budget indicator or similar. +type ServerEvent_UsageUpdate struct { + state protoimpl.MessageState `protogen:"open.v1"` + // This turn's token accounting. + Turn *v16.Usage `protobuf:"bytes,1,opt,name=turn,proto3" json:"turn,omitempty"` + // The session's running total spend in USD, mirroring + // session.v1.SessionInfo.cost_usd. + CumulativeCostUsd float64 `protobuf:"fixed64,2,opt,name=cumulative_cost_usd,json=cumulativeCostUsd,proto3" json:"cumulative_cost_usd,omitempty"` + // The session's running total token count against `effective_ceiling` + // below — the pair a context-budget indicator divides to get + // pressure (e.g. "51,204 / 200,000"). + UsedTokens int64 `protobuf:"varint,3,opt,name=used_tokens,json=usedTokens,proto3" json:"used_tokens,omitempty"` + // The usable context budget this session's turns are measured + // against (model.v1.ModelTarget.effective_ceiling). + EffectiveCeiling int64 `protobuf:"varint,4,opt,name=effective_ceiling,json=effectiveCeiling,proto3" json:"effective_ceiling,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ServerEvent_UsageUpdate) Reset() { + *x = ServerEvent_UsageUpdate{} + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ServerEvent_UsageUpdate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServerEvent_UsageUpdate) ProtoMessage() {} + +func (x *ServerEvent_UsageUpdate) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ServerEvent_UsageUpdate.ProtoReflect.Descriptor instead. +func (*ServerEvent_UsageUpdate) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{7, 13} +} + +func (x *ServerEvent_UsageUpdate) GetTurn() *v16.Usage { + if x != nil { + return x.Turn + } + return nil +} + +func (x *ServerEvent_UsageUpdate) GetCumulativeCostUsd() float64 { + if x != nil { + return x.CumulativeCostUsd + } + return 0 +} + +func (x *ServerEvent_UsageUpdate) GetUsedTokens() int64 { + if x != nil { + return x.UsedTokens + } + return 0 +} + +func (x *ServerEvent_UsageUpdate) GetEffectiveCeiling() int64 { + if x != nil { + return x.EffectiveCeiling + } + return 0 +} + +// SessionStatusUpdate reports the attached session's OWN lifecycle +// status transition. Distinct from SessionTreeUpdate above, which +// reports a CHILD session's status. +type ServerEvent_SessionStatusUpdate struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The session's new status. + Status v15.SessionStatus `protobuf:"varint,1,opt,name=status,proto3,enum=pluggableharness.agent.session.v1.SessionStatus" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ServerEvent_SessionStatusUpdate) Reset() { + *x = ServerEvent_SessionStatusUpdate{} + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ServerEvent_SessionStatusUpdate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServerEvent_SessionStatusUpdate) ProtoMessage() {} + +func (x *ServerEvent_SessionStatusUpdate) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ServerEvent_SessionStatusUpdate.ProtoReflect.Descriptor instead. +func (*ServerEvent_SessionStatusUpdate) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{7, 14} +} + +func (x *ServerEvent_SessionStatusUpdate) GetStatus() v15.SessionStatus { + if x != nil { + return x.Status + } + return v15.SessionStatus(0) +} + +// UserMessage is ordinary chat input from the user. +type ClientEvent_UserMessage struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The message content, in emission order. MUST contain at least one + // block. + Content []*v17.ContentBlock `protobuf:"bytes,2,rep,name=content,proto3" json:"content,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientEvent_UserMessage) Reset() { + *x = ClientEvent_UserMessage{} + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientEvent_UserMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientEvent_UserMessage) ProtoMessage() {} + +func (x *ClientEvent_UserMessage) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientEvent_UserMessage.ProtoReflect.Descriptor instead. +func (*ClientEvent_UserMessage) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{8, 0} +} + +func (x *ClientEvent_UserMessage) GetContent() []*v17.ContentBlock { + if x != nil { + return x.Content + } + return nil +} + +// SlashCommand is a dispatched slash command invocation (frontend.md §5). +type ClientEvent_SlashCommand struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The command name, without its leading slash. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // The raw argument string following the command name. + Args string `protobuf:"bytes,2,opt,name=args,proto3" json:"args,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientEvent_SlashCommand) Reset() { + *x = ClientEvent_SlashCommand{} + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientEvent_SlashCommand) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientEvent_SlashCommand) ProtoMessage() {} + +func (x *ClientEvent_SlashCommand) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientEvent_SlashCommand.ProtoReflect.Descriptor instead. +func (*ClientEvent_SlashCommand) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{8, 1} +} + +func (x *ClientEvent_SlashCommand) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ClientEvent_SlashCommand) GetArgs() string { + if x != nil { + return x.Args + } + return "" +} + +// PlanDecision resolves a pending ServerEvent.PermissionRequest. +type ClientEvent_PlanDecision struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The plan item being resolved, matching + // ServerEvent.PermissionRequest.plan_item's id. + PlanItemId string `protobuf:"bytes,1,opt,name=plan_item_id,json=planItemId,proto3" json:"plan_item_id,omitempty"` + // The user's decision. + Decision ClientDecision `protobuf:"varint,2,opt,name=decision,proto3,enum=pluggableharness.agent.frontend.v1.ClientDecision" json:"decision,omitempty"` + // When present, a user-edited replacement for the plan item's tool + // input. The kernel MUST re-validate this against the tool's + // input_schema (tool.md §6); an invalid correction is rejected as a + // distinct error, not silently coerced. + CorrectedInput *structpb.Struct `protobuf:"bytes,3,opt,name=corrected_input,json=correctedInput,proto3,oneof" json:"corrected_input,omitempty"` + // How durably this decision applies beyond this one item — see + // PlanDecisionScope. + Scope PlanDecisionScope `protobuf:"varint,4,opt,name=scope,proto3,enum=pluggableharness.agent.frontend.v1.PlanDecisionScope" json:"scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientEvent_PlanDecision) Reset() { + *x = ClientEvent_PlanDecision{} + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientEvent_PlanDecision) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientEvent_PlanDecision) ProtoMessage() {} + +func (x *ClientEvent_PlanDecision) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientEvent_PlanDecision.ProtoReflect.Descriptor instead. +func (*ClientEvent_PlanDecision) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{8, 2} +} + +func (x *ClientEvent_PlanDecision) GetPlanItemId() string { + if x != nil { + return x.PlanItemId + } + return "" +} + +func (x *ClientEvent_PlanDecision) GetDecision() ClientDecision { + if x != nil { + return x.Decision + } + return ClientDecision_CLIENT_DECISION_UNSPECIFIED +} + +func (x *ClientEvent_PlanDecision) GetCorrectedInput() *structpb.Struct { + if x != nil { + return x.CorrectedInput + } + return nil +} + +func (x *ClientEvent_PlanDecision) GetScope() PlanDecisionScope { + if x != nil { + return x.Scope + } + return PlanDecisionScope_PLAN_DECISION_SCOPE_UNSPECIFIED +} + +// InteractiveResponse resolves a pending ServerEvent.InteractiveRequest. +type ClientEvent_InteractiveResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Correlates to ServerEvent.InteractiveRequest.call_id. + CallId string `protobuf:"bytes,1,opt,name=call_id,json=callId,proto3" json:"call_id,omitempty"` + // The user's response, becoming the interactive call's + // ToolResult.payload. + Response *structpb.Struct `protobuf:"bytes,2,opt,name=response,proto3" json:"response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientEvent_InteractiveResponse) Reset() { + *x = ClientEvent_InteractiveResponse{} + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientEvent_InteractiveResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientEvent_InteractiveResponse) ProtoMessage() {} + +func (x *ClientEvent_InteractiveResponse) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientEvent_InteractiveResponse.ProtoReflect.Descriptor instead. +func (*ClientEvent_InteractiveResponse) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{8, 3} +} + +func (x *ClientEvent_InteractiveResponse) GetCallId() string { + if x != nil { + return x.CallId + } + return "" +} + +func (x *ClientEvent_InteractiveResponse) GetResponse() *structpb.Struct { + if x != nil { + return x.Response + } + return nil +} + +// ActionTrigger is dispatched when a user activates a RenderNode's +// ActionNode (render.proto's ActionNode, frontend.md §5.1). The kernel +// handles this identically to a direct_invoke slash command: the normal +// Invoke/plan-apply pipeline including policy evaluation, with no model +// turn. +type ClientEvent_ActionTrigger struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The originating ActionNode's id (render.proto's ActionNode.id). + NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` + // The tool operation to invoke (tool.md §2 ToolSchema.name), echoed + // unchanged from the originating ActionNode.tool_name. + ToolName string `protobuf:"bytes,2,opt,name=tool_name,json=toolName,proto3" json:"tool_name,omitempty"` + // The arguments to invoke it with, echoed unchanged from the + // originating ActionNode.args. + Args *structpb.Struct `protobuf:"bytes,3,opt,name=args,proto3" json:"args,omitempty"` + // The declared name of the tool provider plugin `tool_name` belongs + // to, echoed unchanged from the originating ActionNode.provider + // (render.proto) — tool_name is only unique per provider. + Provider string `protobuf:"bytes,4,opt,name=provider,proto3" json:"provider,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientEvent_ActionTrigger) Reset() { + *x = ClientEvent_ActionTrigger{} + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientEvent_ActionTrigger) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientEvent_ActionTrigger) ProtoMessage() {} + +func (x *ClientEvent_ActionTrigger) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientEvent_ActionTrigger.ProtoReflect.Descriptor instead. +func (*ClientEvent_ActionTrigger) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{8, 4} +} + +func (x *ClientEvent_ActionTrigger) GetNodeId() string { + if x != nil { + return x.NodeId + } + return "" +} + +func (x *ClientEvent_ActionTrigger) GetToolName() string { + if x != nil { + return x.ToolName + } + return "" +} + +func (x *ClientEvent_ActionTrigger) GetArgs() *structpb.Struct { + if x != nil { + return x.Args + } + return nil +} + +func (x *ClientEvent_ActionTrigger) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + +// Interrupt carries no fields; it signals that the user wants to +// interrupt the current turn. +type ClientEvent_Interrupt struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientEvent_Interrupt) Reset() { + *x = ClientEvent_Interrupt{} + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientEvent_Interrupt) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientEvent_Interrupt) ProtoMessage() {} + +func (x *ClientEvent_Interrupt) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientEvent_Interrupt.ProtoReflect.Descriptor instead. +func (*ClientEvent_Interrupt) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{8, 5} +} + +// Hello MAY be sent as the first ClientEvent on a newly opened Attach +// stream, to assert the protocol version. Binding a session happens via +// the session-control variants, not via Hello. +type ClientEvent_Hello struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The protocol version this frontend was built against. + ProtocolVersion uint32 `protobuf:"varint,1,opt,name=protocol_version,json=protocolVersion,proto3" json:"protocol_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientEvent_Hello) Reset() { + *x = ClientEvent_Hello{} + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientEvent_Hello) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientEvent_Hello) ProtoMessage() {} + +func (x *ClientEvent_Hello) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientEvent_Hello.ProtoReflect.Descriptor instead. +func (*ClientEvent_Hello) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{8, 6} +} + +func (x *ClientEvent_Hello) GetProtocolVersion() uint32 { + if x != nil { + return x.ProtocolVersion + } + return 0 +} + +// CreateSession creates a new session and auto-attaches this stream to +// it, per frontend.md §"Session lifecycle". Answered by +// ServerEvent.SessionCreated. +type ClientEvent_CreateSession struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Client-generated, echoed back on ServerEvent.request_id. + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + // The agent.hcl profile to create the session under. Absent means + // the kernel's configured default profile. + Profile *string `protobuf:"bytes,2,opt,name=profile,proto3,oneof" json:"profile,omitempty"` + // An initial user message to seed the session with, submitted as the + // first turn once the session is created. Absent creates an empty + // session awaiting the first ordinary user_message. + InitialPrompt *string `protobuf:"bytes,3,opt,name=initial_prompt,json=initialPrompt,proto3,oneof" json:"initial_prompt,omitempty"` + // The session's working directory. Absent means the kernel's own + // working directory at creation time. + WorkingDirectory *string `protobuf:"bytes,4,opt,name=working_directory,json=workingDirectory,proto3,oneof" json:"working_directory,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientEvent_CreateSession) Reset() { + *x = ClientEvent_CreateSession{} + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientEvent_CreateSession) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientEvent_CreateSession) ProtoMessage() {} + +func (x *ClientEvent_CreateSession) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientEvent_CreateSession.ProtoReflect.Descriptor instead. +func (*ClientEvent_CreateSession) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{8, 7} +} + +func (x *ClientEvent_CreateSession) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *ClientEvent_CreateSession) GetProfile() string { + if x != nil && x.Profile != nil { + return *x.Profile + } + return "" +} + +func (x *ClientEvent_CreateSession) GetInitialPrompt() string { + if x != nil && x.InitialPrompt != nil { + return *x.InitialPrompt + } + return "" +} + +func (x *ClientEvent_CreateSession) GetWorkingDirectory() string { + if x != nil && x.WorkingDirectory != nil { + return *x.WorkingDirectory + } + return "" +} + +// AttachSession subscribes an existing session (live or terminal) onto +// this stream, triggering a backfill replay. Answered by +// ServerEvent.SessionAttached, bracketing a replay batch closed by +// ServerEvent.BackfillComplete. +type ClientEvent_AttachSession struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Client-generated, echoed back on ServerEvent.request_id. + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + // The session to attach. + SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientEvent_AttachSession) Reset() { + *x = ClientEvent_AttachSession{} + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientEvent_AttachSession) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientEvent_AttachSession) ProtoMessage() {} + +func (x *ClientEvent_AttachSession) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientEvent_AttachSession.ProtoReflect.Descriptor instead. +func (*ClientEvent_AttachSession) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{8, 8} +} + +func (x *ClientEvent_AttachSession) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *ClientEvent_AttachSession) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +// ResumeSession attaches a historical (possibly terminal) session, per +// frontend.md §"Resume and re-open semantics". A COMPLETED or +// CANCELLED session MAY be re-opened to SESSION_STATUS_RUNNING for new +// turns; a bound-exhausted (error_max_*) or FAILED session attaches +// replay-only — the kernel MUST reject a subsequent user_message +// against it with FRONTEND_ERROR_CATEGORY_SESSION_REPLAY_ONLY. Answered +// identically to AttachSession: SessionAttached bracketing a backfill +// batch. +type ClientEvent_ResumeSession struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Client-generated, echoed back on ServerEvent.request_id. + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + // The session to resume. + SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientEvent_ResumeSession) Reset() { + *x = ClientEvent_ResumeSession{} + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientEvent_ResumeSession) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientEvent_ResumeSession) ProtoMessage() {} + +func (x *ClientEvent_ResumeSession) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientEvent_ResumeSession.ProtoReflect.Descriptor instead. +func (*ClientEvent_ResumeSession) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{8, 9} +} + +func (x *ClientEvent_ResumeSession) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *ClientEvent_ResumeSession) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +// DetachSession unsubscribes a session from this stream without +// affecting the session itself — other streams attached to the same +// session, and the session's own execution, are unaffected. Answered +// by ServerEvent.SessionDetached. +type ClientEvent_DetachSession struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Client-generated, echoed back on ServerEvent.request_id. + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + // The session to detach. + SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientEvent_DetachSession) Reset() { + *x = ClientEvent_DetachSession{} + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientEvent_DetachSession) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientEvent_DetachSession) ProtoMessage() {} + +func (x *ClientEvent_DetachSession) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientEvent_DetachSession.ProtoReflect.Descriptor instead. +func (*ClientEvent_DetachSession) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{8, 10} +} + +func (x *ClientEvent_DetachSession) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *ClientEvent_DetachSession) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +// ListSessions requests the connection-scoped session summary list. +// Answered by ServerEvent.SessionList. +type ClientEvent_ListSessions struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Client-generated, echoed back on ServerEvent.request_id. + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + // Restricts the result to sessions in this status. Absent means no + // status filter. + Status *v15.SessionStatus `protobuf:"varint,2,opt,name=status,proto3,enum=pluggableharness.agent.session.v1.SessionStatus,oneof" json:"status,omitempty"` + // Restricts the result to children of this session. Absent means no + // parent filter. + ParentSessionId *string `protobuf:"bytes,3,opt,name=parent_session_id,json=parentSessionId,proto3,oneof" json:"parent_session_id,omitempty"` + // True: only root sessions (no parent_session_id). False: all + // sessions matching the other filters, at any depth. + RootsOnly bool `protobuf:"varint,4,opt,name=roots_only,json=rootsOnly,proto3" json:"roots_only,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientEvent_ListSessions) Reset() { + *x = ClientEvent_ListSessions{} + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientEvent_ListSessions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientEvent_ListSessions) ProtoMessage() {} + +func (x *ClientEvent_ListSessions) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientEvent_ListSessions.ProtoReflect.Descriptor instead. +func (*ClientEvent_ListSessions) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP(), []int{8, 11} +} + +func (x *ClientEvent_ListSessions) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *ClientEvent_ListSessions) GetStatus() v15.SessionStatus { + if x != nil && x.Status != nil { + return *x.Status + } + return v15.SessionStatus(0) +} + +func (x *ClientEvent_ListSessions) GetParentSessionId() string { + if x != nil && x.ParentSessionId != nil { + return *x.ParentSessionId + } + return "" +} + +func (x *ClientEvent_ListSessions) GetRootsOnly() bool { + if x != nil { + return x.RootsOnly + } + return false +} + +var File_pluggableharness_agent_frontend_v1_frontend_proto protoreflect.FileDescriptor + +const file_pluggableharness_agent_frontend_v1_frontend_proto_rawDesc = "" + + "\n" + + "1pluggableharness/agent/frontend/v1/frontend.proto\x12\"pluggableharness.agent.frontend.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a-pluggableharness/agent/common/v1/common.proto\x1a-pluggableharness/agent/config/v1/config.proto\x1a/pluggableharness/agent/content/v1/content.proto\x1a+pluggableharness/agent/model/v1/model.proto\x1a)pluggableharness/agent/plan/v1/plan.proto\x1a-pluggableharness/agent/render/v1/render.proto\x1a/pluggableharness/agent/session/v1/session.proto\x1a9pluggableharness/agent/slashcommand/v1/slashcommand.proto\"\x11\n" + + "\x0fDescribeRequest\"]\n" + + "\x10DescribeResponse\x12I\n" + + "\bproducer\x18\x01 \x01(\v2-.pluggableharness.agent.common.v1.ProducerRefR\bproducer\"\x18\n" + "\x16GetCapabilitiesRequest\"w\n" + "\x17GetCapabilitiesResponse\x12\\\n" + - "\fcapabilities\x18\x01 \x01(\v28.pluggableharness.agent.frontend.v1.FrontendCapabilitiesR\fcapabilities\"\xcc\x01\n" + + "\fcapabilities\x18\x01 \x01(\v28.pluggableharness.agent.frontend.v1.FrontendCapabilitiesR\fcapabilities\"\x84\x03\n" + "\x14FrontendCapabilities\x12_\n" + "\x0eslash_commands\x18\x01 \x03(\v28.pluggableharness.agent.slashcommand.v1.SlashCommandSpecR\rslashCommands\x12S\n" + - "\rconfig_schema\x18\x02 \x01(\v2..pluggableharness.agent.config.v1.ConfigSchemaR\fconfigSchema\"C\n" + + "\rconfig_schema\x18\x02 \x01(\v2..pluggableharness.agent.config.v1.ConfigSchemaR\fconfigSchema\x12U\n" + + "\x11supported_regions\x18\x03 \x03(\x0e2(.pluggableharness.agent.render.v1.RegionR\x10supportedRegions\x12_\n" + + "\x15supported_hook_points\x18\x04 \x03(\x0e2+.pluggableharness.agent.common.v1.HookPointR\x13supportedHookPoints\"C\n" + "\x10ConfigureRequest\x12/\n" + "\x06config\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x06config\"\x13\n" + - "\x11ConfigureResponse\"\xa8\v\n" + - "\vServerEvent\x12`\n" + + "\x11ConfigureResponse\"\xf3\x18\n" + + "\vServerEvent\x12\x1d\n" + + "\n" + + "session_id\x18d \x01(\tR\tsessionId\x12\"\n" + + "\n" + + "request_id\x18e \x01(\tH\x01R\trequestId\x88\x01\x01\x12`\n" + "\fstream_delta\x18\x01 \x01(\v2;.pluggableharness.agent.frontend.v1.ServerEvent.StreamDeltaH\x00R\vstreamDelta\x12P\n" + "\x06render\x18\x02 \x01(\v26.pluggableharness.agent.frontend.v1.ServerEvent.RenderH\x00R\x06render\x12r\n" + "\x12permission_request\x18\x03 \x01(\v2A.pluggableharness.agent.frontend.v1.ServerEvent.PermissionRequestH\x00R\x11permissionRequest\x12Z\n" + @@ -1483,7 +2821,16 @@ const file_pluggableharness_agent_frontend_v1_frontend_proto_rawDesc = "" + "plan_ready\x18\x04 \x01(\v29.pluggableharness.agent.frontend.v1.ServerEvent.PlanReadyH\x00R\tplanReady\x12u\n" + "\x13interactive_request\x18\x05 \x01(\v2B.pluggableharness.agent.frontend.v1.ServerEvent.InteractiveRequestH\x00R\x12interactiveRequest\x12s\n" + "\x13session_tree_update\x18\x06 \x01(\v2A.pluggableharness.agent.frontend.v1.ServerEvent.SessionTreeUpdateH\x00R\x11sessionTreeUpdate\x12M\n" + - "\x05error\x18\a \x01(\v25.pluggableharness.agent.frontend.v1.ServerEvent.ErrorH\x00R\x05error\x1a>\n" + + "\x05error\x18\a \x01(\v25.pluggableharness.agent.frontend.v1.ServerEvent.ErrorH\x00R\x05error\x12i\n" + + "\x0fsession_created\x18\b \x01(\v2>.pluggableharness.agent.frontend.v1.ServerEvent.SessionCreatedH\x00R\x0esessionCreated\x12l\n" + + "\x10session_attached\x18\t \x01(\v2?.pluggableharness.agent.frontend.v1.ServerEvent.SessionAttachedH\x00R\x0fsessionAttached\x12o\n" + + "\x11backfill_complete\x18\n" + + " \x01(\v2@.pluggableharness.agent.frontend.v1.ServerEvent.BackfillCompleteH\x00R\x10backfillComplete\x12l\n" + + "\x10session_detached\x18\v \x01(\v2?.pluggableharness.agent.frontend.v1.ServerEvent.SessionDetachedH\x00R\x0fsessionDetached\x12`\n" + + "\fsession_list\x18\f \x01(\v2;.pluggableharness.agent.frontend.v1.ServerEvent.SessionListH\x00R\vsessionList\x12|\n" + + "\x16slash_command_registry\x18\x0e \x01(\v2D.pluggableharness.agent.frontend.v1.ServerEvent.SlashCommandRegistryH\x00R\x14slashCommandRegistry\x12`\n" + + "\fusage_update\x18\x0f \x01(\v2;.pluggableharness.agent.frontend.v1.ServerEvent.UsageUpdateH\x00R\vusageUpdate\x12y\n" + + "\x15session_status_update\x18\x10 \x01(\v2C.pluggableharness.agent.frontend.v1.ServerEvent.SessionStatusUpdateH\x00R\x13sessionStatusUpdate\x1a>\n" + "\vStreamDelta\x12\x1b\n" + "\ttarget_id\x18\x01 \x01(\tR\btargetId\x12\x12\n" + "\x04text\x18\x02 \x01(\tR\x04text\x1aS\n" + @@ -1502,34 +2849,101 @@ const file_pluggableharness_agent_frontend_v1_frontend_proto_rawDesc = "" + "\x10child_session_id\x18\x02 \x01(\tR\x0echildSessionId\x12H\n" + "\x06status\x18\x03 \x01(\x0e20.pluggableharness.agent.session.v1.SessionStatusR\x06status\x1aP\n" + "\x05Error\x12G\n" + - "\x05error\x18\x01 \x01(\v21.pluggableharness.agent.frontend.v1.FrontendErrorR\x05errorB\a\n" + - "\x05event\"\x9e\t\n" + - "\vClientEvent\x12`\n" + + "\x05error\x18\x01 \x01(\v21.pluggableharness.agent.frontend.v1.FrontendErrorR\x05error\x1aT\n" + + "\x0eSessionCreated\x12B\n" + + "\x04info\x18\x01 \x01(\v2..pluggableharness.agent.session.v1.SessionInfoR\x04info\x1aU\n" + + "\x0fSessionAttached\x12B\n" + + "\x04info\x18\x01 \x01(\v2..pluggableharness.agent.session.v1.SessionInfoR\x04info\x1a7\n" + + "\x10BackfillComplete\x12#\n" + + "\rlast_sequence\x18\x01 \x01(\x03R\flastSequence\x1a\x11\n" + + "\x0fSessionDetached\x1aY\n" + + "\vSessionList\x12J\n" + + "\bsessions\x18\x01 \x03(\v2..pluggableharness.agent.session.v1.SessionInfoR\bsessions\x1al\n" + + "\x14SlashCommandRegistry\x12T\n" + + "\bcommands\x18\x01 \x03(\v28.pluggableharness.agent.slashcommand.v1.SlashCommandSpecR\bcommands\x1a\xc7\x01\n" + + "\vUsageUpdate\x12:\n" + + "\x04turn\x18\x01 \x01(\v2&.pluggableharness.agent.model.v1.UsageR\x04turn\x12.\n" + + "\x13cumulative_cost_usd\x18\x02 \x01(\x01R\x11cumulativeCostUsd\x12\x1f\n" + + "\vused_tokens\x18\x03 \x01(\x03R\n" + + "usedTokens\x12+\n" + + "\x11effective_ceiling\x18\x04 \x01(\x03R\x10effectiveCeiling\x1a_\n" + + "\x13SessionStatusUpdate\x12H\n" + + "\x06status\x18\x01 \x01(\x0e20.pluggableharness.agent.session.v1.SessionStatusR\x06statusB\a\n" + + "\x05eventB\r\n" + + "\v_request_idJ\x04\b\r\x10\x0eR\x0fsession_deleted\"\xb2\x15\n" + + "\vClientEvent\x12\x1d\n" + + "\n" + + "session_id\x18d \x01(\tR\tsessionId\x12`\n" + "\fuser_message\x18\x01 \x01(\v2;.pluggableharness.agent.frontend.v1.ClientEvent.UserMessageH\x00R\vuserMessage\x12c\n" + "\rslash_command\x18\x02 \x01(\v2<.pluggableharness.agent.frontend.v1.ClientEvent.SlashCommandH\x00R\fslashCommand\x12c\n" + "\rplan_decision\x18\x03 \x01(\v2<.pluggableharness.agent.frontend.v1.ClientEvent.PlanDecisionH\x00R\fplanDecision\x12x\n" + "\x14interactive_response\x18\x04 \x01(\v2C.pluggableharness.agent.frontend.v1.ClientEvent.InteractiveResponseH\x00R\x13interactiveResponse\x12f\n" + "\x0eaction_trigger\x18\x05 \x01(\v2=.pluggableharness.agent.frontend.v1.ClientEvent.ActionTriggerH\x00R\ractionTrigger\x12Y\n" + - "\tinterrupt\x18\x06 \x01(\v29.pluggableharness.agent.frontend.v1.ClientEvent.InterruptH\x00R\tinterrupt\x1a!\n" + - "\vUserMessage\x12\x12\n" + - "\x04text\x18\x01 \x01(\tR\x04text\x1a6\n" + + "\tinterrupt\x18\x06 \x01(\v29.pluggableharness.agent.frontend.v1.ClientEvent.InterruptH\x00R\tinterrupt\x12M\n" + + "\x05hello\x18\a \x01(\v25.pluggableharness.agent.frontend.v1.ClientEvent.HelloH\x00R\x05hello\x12f\n" + + "\x0ecreate_session\x18\b \x01(\v2=.pluggableharness.agent.frontend.v1.ClientEvent.CreateSessionH\x00R\rcreateSession\x12f\n" + + "\x0eattach_session\x18\t \x01(\v2=.pluggableharness.agent.frontend.v1.ClientEvent.AttachSessionH\x00R\rattachSession\x12f\n" + + "\x0eresume_session\x18\n" + + " \x01(\v2=.pluggableharness.agent.frontend.v1.ClientEvent.ResumeSessionH\x00R\rresumeSession\x12f\n" + + "\x0edetach_session\x18\v \x01(\v2=.pluggableharness.agent.frontend.v1.ClientEvent.DetachSessionH\x00R\rdetachSession\x12c\n" + + "\rlist_sessions\x18\f \x01(\v2<.pluggableharness.agent.frontend.v1.ClientEvent.ListSessionsH\x00R\flistSessions\x1ad\n" + + "\vUserMessage\x12I\n" + + "\acontent\x18\x02 \x03(\v2/.pluggableharness.agent.content.v1.ContentBlockR\acontentJ\x04\b\x01\x10\x02R\x04text\x1a6\n" + "\fSlashCommand\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" + - "\x04args\x18\x02 \x01(\tR\x04args\x1a\xdb\x01\n" + + "\x04args\x18\x02 \x01(\tR\x04args\x1a\xa8\x02\n" + "\fPlanDecision\x12 \n" + "\fplan_item_id\x18\x01 \x01(\tR\n" + "planItemId\x12N\n" + "\bdecision\x18\x02 \x01(\x0e22.pluggableharness.agent.frontend.v1.ClientDecisionR\bdecision\x12E\n" + - "\x0fcorrected_input\x18\x03 \x01(\v2\x17.google.protobuf.StructH\x00R\x0ecorrectedInput\x88\x01\x01B\x12\n" + + "\x0fcorrected_input\x18\x03 \x01(\v2\x17.google.protobuf.StructH\x00R\x0ecorrectedInput\x88\x01\x01\x12K\n" + + "\x05scope\x18\x04 \x01(\x0e25.pluggableharness.agent.frontend.v1.PlanDecisionScopeR\x05scopeB\x12\n" + "\x10_corrected_input\x1ac\n" + "\x13InteractiveResponse\x12\x17\n" + "\acall_id\x18\x01 \x01(\tR\x06callId\x123\n" + - "\bresponse\x18\x02 \x01(\v2\x17.google.protobuf.StructR\bresponse\x1ar\n" + + "\bresponse\x18\x02 \x01(\v2\x17.google.protobuf.StructR\bresponse\x1a\x8e\x01\n" + "\rActionTrigger\x12\x17\n" + "\anode_id\x18\x01 \x01(\tR\x06nodeId\x12\x1b\n" + "\ttool_name\x18\x02 \x01(\tR\btoolName\x12+\n" + - "\x04args\x18\x03 \x01(\v2\x17.google.protobuf.StructR\x04args\x1a\v\n" + - "\tInterruptB\a\n" + + "\x04args\x18\x03 \x01(\v2\x17.google.protobuf.StructR\x04args\x12\x1a\n" + + "\bprovider\x18\x04 \x01(\tR\bprovider\x1a\v\n" + + "\tInterrupt\x1a2\n" + + "\x05Hello\x12)\n" + + "\x10protocol_version\x18\x01 \x01(\rR\x0fprotocolVersion\x1a\xe0\x01\n" + + "\rCreateSession\x12\x1d\n" + + "\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x12\x1d\n" + + "\aprofile\x18\x02 \x01(\tH\x00R\aprofile\x88\x01\x01\x12*\n" + + "\x0einitial_prompt\x18\x03 \x01(\tH\x01R\rinitialPrompt\x88\x01\x01\x120\n" + + "\x11working_directory\x18\x04 \x01(\tH\x02R\x10workingDirectory\x88\x01\x01B\n" + + "\n" + + "\b_profileB\x11\n" + + "\x0f_initial_promptB\x14\n" + + "\x12_working_directory\x1aM\n" + + "\rAttachSession\x12\x1d\n" + + "\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x12\x1d\n" + + "\n" + + "session_id\x18\x02 \x01(\tR\tsessionId\x1aM\n" + + "\rResumeSession\x12\x1d\n" + + "\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x12\x1d\n" + + "\n" + + "session_id\x18\x02 \x01(\tR\tsessionId\x1aM\n" + + "\rDetachSession\x12\x1d\n" + + "\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x12\x1d\n" + + "\n" + + "session_id\x18\x02 \x01(\tR\tsessionId\x1a\xed\x01\n" + + "\fListSessions\x12\x1d\n" + + "\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x12M\n" + + "\x06status\x18\x02 \x01(\x0e20.pluggableharness.agent.session.v1.SessionStatusH\x00R\x06status\x88\x01\x01\x12/\n" + + "\x11parent_session_id\x18\x03 \x01(\tH\x01R\x0fparentSessionId\x88\x01\x01\x12\x1d\n" + + "\n" + + "roots_only\x18\x04 \x01(\bR\trootsOnlyB\t\n" + + "\a_statusB\x14\n" + + "\x12_parent_session_idB\a\n" + "\x05event\"\x80\x01\n" + "\rFrontendError\x12U\n" + "\bcategory\x18\x01 \x01(\x0e29.pluggableharness.agent.frontend.v1.FrontendErrorCategoryR\bcategory\x12\x18\n" + @@ -1537,17 +2951,28 @@ const file_pluggableharness_agent_frontend_v1_frontend_proto_rawDesc = "" + "\x0eClientDecision\x12\x1f\n" + "\x1bCLIENT_DECISION_UNSPECIFIED\x10\x00\x12\x19\n" + "\x15CLIENT_DECISION_ALLOW\x10\x01\x12\x18\n" + - "\x14CLIENT_DECISION_DENY\x10\x02*\xf2\x01\n" + + "\x14CLIENT_DECISION_DENY\x10\x02*\x97\x01\n" + + "\x11PlanDecisionScope\x12#\n" + + "\x1fPLAN_DECISION_SCOPE_UNSPECIFIED\x10\x00\x12\x1c\n" + + "\x18PLAN_DECISION_SCOPE_ONCE\x10\x01\x12\x1f\n" + + "\x1bPLAN_DECISION_SCOPE_SESSION\x10\x02\x12\x1e\n" + + "\x1aPLAN_DECISION_SCOPE_ALWAYS\x10\x03*\xdb\x03\n" + "\x15FrontendErrorCategory\x12'\n" + "#FRONTEND_ERROR_CATEGORY_UNSPECIFIED\x10\x00\x12)\n" + "%FRONTEND_ERROR_CATEGORY_RENDER_FAILED\x10\x01\x120\n" + ",FRONTEND_ERROR_CATEGORY_INVALID_CLIENT_EVENT\x10\x02\x12.\n" + "*FRONTEND_ERROR_CATEGORY_REGION_UNSUPPORTED\x10\x03\x12#\n" + - "\x1fFRONTEND_ERROR_CATEGORY_UNKNOWN\x10\x042\x88\x03\n" + + "\x1fFRONTEND_ERROR_CATEGORY_UNKNOWN\x10\x04\x12-\n" + + ")FRONTEND_ERROR_CATEGORY_SESSION_NOT_FOUND\x10\x05\x121\n" + + "-FRONTEND_ERROR_CATEGORY_SESSION_CREATE_FAILED\x10\x06\x12(\n" + + "$FRONTEND_ERROR_CATEGORY_SESSION_BUSY\x10\a\x12*\n" + + "&FRONTEND_ERROR_CATEGORY_SCHEMA_TOO_NEW\x10\b\x12/\n" + + "+FRONTEND_ERROR_CATEGORY_SESSION_REPLAY_ONLY\x10\t2\xff\x03\n" + "\x0fFrontendService\x12\x8a\x01\n" + "\x0fGetCapabilities\x12:.pluggableharness.agent.frontend.v1.GetCapabilitiesRequest\x1a;.pluggableharness.agent.frontend.v1.GetCapabilitiesResponse\x12x\n" + "\tConfigure\x124.pluggableharness.agent.frontend.v1.ConfigureRequest\x1a5.pluggableharness.agent.frontend.v1.ConfigureResponse\x12n\n" + - "\x06Attach\x12/.pluggableharness.agent.frontend.v1.ClientEvent\x1a/.pluggableharness.agent.frontend.v1.ServerEvent(\x010\x01BDZBgithub.com/pluggableharness/agent/pkg/frontend/proto/v1;frontendv1b\x06proto3" + "\x06Attach\x12/.pluggableharness.agent.frontend.v1.ClientEvent\x1a/.pluggableharness.agent.frontend.v1.ServerEvent(\x010\x01\x12u\n" + + "\bDescribe\x123.pluggableharness.agent.frontend.v1.DescribeRequest\x1a4.pluggableharness.agent.frontend.v1.DescribeResponseBDZBgithub.com/pluggableharness/agent/pkg/frontend/proto/v1;frontendv1b\x06proto3" var ( file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescOnce sync.Once @@ -1561,81 +2986,132 @@ func file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescGZIP() []byte return file_pluggableharness_agent_frontend_v1_frontend_proto_rawDescData } -var file_pluggableharness_agent_frontend_v1_frontend_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes = make([]protoimpl.MessageInfo, 21) +var file_pluggableharness_agent_frontend_v1_frontend_proto_enumTypes = make([]protoimpl.EnumInfo, 3) +var file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes = make([]protoimpl.MessageInfo, 37) var file_pluggableharness_agent_frontend_v1_frontend_proto_goTypes = []any{ - (ClientDecision)(0), // 0: pluggableharness.agent.frontend.v1.ClientDecision - (FrontendErrorCategory)(0), // 1: pluggableharness.agent.frontend.v1.FrontendErrorCategory - (*GetCapabilitiesRequest)(nil), // 2: pluggableharness.agent.frontend.v1.GetCapabilitiesRequest - (*GetCapabilitiesResponse)(nil), // 3: pluggableharness.agent.frontend.v1.GetCapabilitiesResponse - (*FrontendCapabilities)(nil), // 4: pluggableharness.agent.frontend.v1.FrontendCapabilities - (*ConfigureRequest)(nil), // 5: pluggableharness.agent.frontend.v1.ConfigureRequest - (*ConfigureResponse)(nil), // 6: pluggableharness.agent.frontend.v1.ConfigureResponse - (*ServerEvent)(nil), // 7: pluggableharness.agent.frontend.v1.ServerEvent - (*ClientEvent)(nil), // 8: pluggableharness.agent.frontend.v1.ClientEvent - (*FrontendError)(nil), // 9: pluggableharness.agent.frontend.v1.FrontendError - (*ServerEvent_StreamDelta)(nil), // 10: pluggableharness.agent.frontend.v1.ServerEvent.StreamDelta - (*ServerEvent_Render)(nil), // 11: pluggableharness.agent.frontend.v1.ServerEvent.Render - (*ServerEvent_PermissionRequest)(nil), // 12: pluggableharness.agent.frontend.v1.ServerEvent.PermissionRequest - (*ServerEvent_PlanReady)(nil), // 13: pluggableharness.agent.frontend.v1.ServerEvent.PlanReady - (*ServerEvent_InteractiveRequest)(nil), // 14: pluggableharness.agent.frontend.v1.ServerEvent.InteractiveRequest - (*ServerEvent_SessionTreeUpdate)(nil), // 15: pluggableharness.agent.frontend.v1.ServerEvent.SessionTreeUpdate - (*ServerEvent_Error)(nil), // 16: pluggableharness.agent.frontend.v1.ServerEvent.Error - (*ClientEvent_UserMessage)(nil), // 17: pluggableharness.agent.frontend.v1.ClientEvent.UserMessage - (*ClientEvent_SlashCommand)(nil), // 18: pluggableharness.agent.frontend.v1.ClientEvent.SlashCommand - (*ClientEvent_PlanDecision)(nil), // 19: pluggableharness.agent.frontend.v1.ClientEvent.PlanDecision - (*ClientEvent_InteractiveResponse)(nil), // 20: pluggableharness.agent.frontend.v1.ClientEvent.InteractiveResponse - (*ClientEvent_ActionTrigger)(nil), // 21: pluggableharness.agent.frontend.v1.ClientEvent.ActionTrigger - (*ClientEvent_Interrupt)(nil), // 22: pluggableharness.agent.frontend.v1.ClientEvent.Interrupt - (*v1.SlashCommandSpec)(nil), // 23: pluggableharness.agent.slashcommand.v1.SlashCommandSpec - (*v11.ConfigSchema)(nil), // 24: pluggableharness.agent.config.v1.ConfigSchema - (*structpb.Struct)(nil), // 25: google.protobuf.Struct - (*v12.PlacedContent)(nil), // 26: pluggableharness.agent.render.v1.PlacedContent - (*v13.PlanItem)(nil), // 27: pluggableharness.agent.plan.v1.PlanItem - (*v13.Plan)(nil), // 28: pluggableharness.agent.plan.v1.Plan - (*v12.RenderTree)(nil), // 29: pluggableharness.agent.render.v1.RenderTree - (v14.SessionStatus)(0), // 30: pluggableharness.agent.session.v1.SessionStatus + (ClientDecision)(0), // 0: pluggableharness.agent.frontend.v1.ClientDecision + (PlanDecisionScope)(0), // 1: pluggableharness.agent.frontend.v1.PlanDecisionScope + (FrontendErrorCategory)(0), // 2: pluggableharness.agent.frontend.v1.FrontendErrorCategory + (*DescribeRequest)(nil), // 3: pluggableharness.agent.frontend.v1.DescribeRequest + (*DescribeResponse)(nil), // 4: pluggableharness.agent.frontend.v1.DescribeResponse + (*GetCapabilitiesRequest)(nil), // 5: pluggableharness.agent.frontend.v1.GetCapabilitiesRequest + (*GetCapabilitiesResponse)(nil), // 6: pluggableharness.agent.frontend.v1.GetCapabilitiesResponse + (*FrontendCapabilities)(nil), // 7: pluggableharness.agent.frontend.v1.FrontendCapabilities + (*ConfigureRequest)(nil), // 8: pluggableharness.agent.frontend.v1.ConfigureRequest + (*ConfigureResponse)(nil), // 9: pluggableharness.agent.frontend.v1.ConfigureResponse + (*ServerEvent)(nil), // 10: pluggableharness.agent.frontend.v1.ServerEvent + (*ClientEvent)(nil), // 11: pluggableharness.agent.frontend.v1.ClientEvent + (*FrontendError)(nil), // 12: pluggableharness.agent.frontend.v1.FrontendError + (*ServerEvent_StreamDelta)(nil), // 13: pluggableharness.agent.frontend.v1.ServerEvent.StreamDelta + (*ServerEvent_Render)(nil), // 14: pluggableharness.agent.frontend.v1.ServerEvent.Render + (*ServerEvent_PermissionRequest)(nil), // 15: pluggableharness.agent.frontend.v1.ServerEvent.PermissionRequest + (*ServerEvent_PlanReady)(nil), // 16: pluggableharness.agent.frontend.v1.ServerEvent.PlanReady + (*ServerEvent_InteractiveRequest)(nil), // 17: pluggableharness.agent.frontend.v1.ServerEvent.InteractiveRequest + (*ServerEvent_SessionTreeUpdate)(nil), // 18: pluggableharness.agent.frontend.v1.ServerEvent.SessionTreeUpdate + (*ServerEvent_Error)(nil), // 19: pluggableharness.agent.frontend.v1.ServerEvent.Error + (*ServerEvent_SessionCreated)(nil), // 20: pluggableharness.agent.frontend.v1.ServerEvent.SessionCreated + (*ServerEvent_SessionAttached)(nil), // 21: pluggableharness.agent.frontend.v1.ServerEvent.SessionAttached + (*ServerEvent_BackfillComplete)(nil), // 22: pluggableharness.agent.frontend.v1.ServerEvent.BackfillComplete + (*ServerEvent_SessionDetached)(nil), // 23: pluggableharness.agent.frontend.v1.ServerEvent.SessionDetached + (*ServerEvent_SessionList)(nil), // 24: pluggableharness.agent.frontend.v1.ServerEvent.SessionList + (*ServerEvent_SlashCommandRegistry)(nil), // 25: pluggableharness.agent.frontend.v1.ServerEvent.SlashCommandRegistry + (*ServerEvent_UsageUpdate)(nil), // 26: pluggableharness.agent.frontend.v1.ServerEvent.UsageUpdate + (*ServerEvent_SessionStatusUpdate)(nil), // 27: pluggableharness.agent.frontend.v1.ServerEvent.SessionStatusUpdate + (*ClientEvent_UserMessage)(nil), // 28: pluggableharness.agent.frontend.v1.ClientEvent.UserMessage + (*ClientEvent_SlashCommand)(nil), // 29: pluggableharness.agent.frontend.v1.ClientEvent.SlashCommand + (*ClientEvent_PlanDecision)(nil), // 30: pluggableharness.agent.frontend.v1.ClientEvent.PlanDecision + (*ClientEvent_InteractiveResponse)(nil), // 31: pluggableharness.agent.frontend.v1.ClientEvent.InteractiveResponse + (*ClientEvent_ActionTrigger)(nil), // 32: pluggableharness.agent.frontend.v1.ClientEvent.ActionTrigger + (*ClientEvent_Interrupt)(nil), // 33: pluggableharness.agent.frontend.v1.ClientEvent.Interrupt + (*ClientEvent_Hello)(nil), // 34: pluggableharness.agent.frontend.v1.ClientEvent.Hello + (*ClientEvent_CreateSession)(nil), // 35: pluggableharness.agent.frontend.v1.ClientEvent.CreateSession + (*ClientEvent_AttachSession)(nil), // 36: pluggableharness.agent.frontend.v1.ClientEvent.AttachSession + (*ClientEvent_ResumeSession)(nil), // 37: pluggableharness.agent.frontend.v1.ClientEvent.ResumeSession + (*ClientEvent_DetachSession)(nil), // 38: pluggableharness.agent.frontend.v1.ClientEvent.DetachSession + (*ClientEvent_ListSessions)(nil), // 39: pluggableharness.agent.frontend.v1.ClientEvent.ListSessions + (*v1.ProducerRef)(nil), // 40: pluggableharness.agent.common.v1.ProducerRef + (*v11.SlashCommandSpec)(nil), // 41: pluggableharness.agent.slashcommand.v1.SlashCommandSpec + (*v12.ConfigSchema)(nil), // 42: pluggableharness.agent.config.v1.ConfigSchema + (v13.Region)(0), // 43: pluggableharness.agent.render.v1.Region + (v1.HookPoint)(0), // 44: pluggableharness.agent.common.v1.HookPoint + (*structpb.Struct)(nil), // 45: google.protobuf.Struct + (*v13.PlacedContent)(nil), // 46: pluggableharness.agent.render.v1.PlacedContent + (*v14.PlanItem)(nil), // 47: pluggableharness.agent.plan.v1.PlanItem + (*v14.Plan)(nil), // 48: pluggableharness.agent.plan.v1.Plan + (*v13.RenderTree)(nil), // 49: pluggableharness.agent.render.v1.RenderTree + (v15.SessionStatus)(0), // 50: pluggableharness.agent.session.v1.SessionStatus + (*v15.SessionInfo)(nil), // 51: pluggableharness.agent.session.v1.SessionInfo + (*v16.Usage)(nil), // 52: pluggableharness.agent.model.v1.Usage + (*v17.ContentBlock)(nil), // 53: pluggableharness.agent.content.v1.ContentBlock } var file_pluggableharness_agent_frontend_v1_frontend_proto_depIdxs = []int32{ - 4, // 0: pluggableharness.agent.frontend.v1.GetCapabilitiesResponse.capabilities:type_name -> pluggableharness.agent.frontend.v1.FrontendCapabilities - 23, // 1: pluggableharness.agent.frontend.v1.FrontendCapabilities.slash_commands:type_name -> pluggableharness.agent.slashcommand.v1.SlashCommandSpec - 24, // 2: pluggableharness.agent.frontend.v1.FrontendCapabilities.config_schema:type_name -> pluggableharness.agent.config.v1.ConfigSchema - 25, // 3: pluggableharness.agent.frontend.v1.ConfigureRequest.config:type_name -> google.protobuf.Struct - 10, // 4: pluggableharness.agent.frontend.v1.ServerEvent.stream_delta:type_name -> pluggableharness.agent.frontend.v1.ServerEvent.StreamDelta - 11, // 5: pluggableharness.agent.frontend.v1.ServerEvent.render:type_name -> pluggableharness.agent.frontend.v1.ServerEvent.Render - 12, // 6: pluggableharness.agent.frontend.v1.ServerEvent.permission_request:type_name -> pluggableharness.agent.frontend.v1.ServerEvent.PermissionRequest - 13, // 7: pluggableharness.agent.frontend.v1.ServerEvent.plan_ready:type_name -> pluggableharness.agent.frontend.v1.ServerEvent.PlanReady - 14, // 8: pluggableharness.agent.frontend.v1.ServerEvent.interactive_request:type_name -> pluggableharness.agent.frontend.v1.ServerEvent.InteractiveRequest - 15, // 9: pluggableharness.agent.frontend.v1.ServerEvent.session_tree_update:type_name -> pluggableharness.agent.frontend.v1.ServerEvent.SessionTreeUpdate - 16, // 10: pluggableharness.agent.frontend.v1.ServerEvent.error:type_name -> pluggableharness.agent.frontend.v1.ServerEvent.Error - 17, // 11: pluggableharness.agent.frontend.v1.ClientEvent.user_message:type_name -> pluggableharness.agent.frontend.v1.ClientEvent.UserMessage - 18, // 12: pluggableharness.agent.frontend.v1.ClientEvent.slash_command:type_name -> pluggableharness.agent.frontend.v1.ClientEvent.SlashCommand - 19, // 13: pluggableharness.agent.frontend.v1.ClientEvent.plan_decision:type_name -> pluggableharness.agent.frontend.v1.ClientEvent.PlanDecision - 20, // 14: pluggableharness.agent.frontend.v1.ClientEvent.interactive_response:type_name -> pluggableharness.agent.frontend.v1.ClientEvent.InteractiveResponse - 21, // 15: pluggableharness.agent.frontend.v1.ClientEvent.action_trigger:type_name -> pluggableharness.agent.frontend.v1.ClientEvent.ActionTrigger - 22, // 16: pluggableharness.agent.frontend.v1.ClientEvent.interrupt:type_name -> pluggableharness.agent.frontend.v1.ClientEvent.Interrupt - 1, // 17: pluggableharness.agent.frontend.v1.FrontendError.category:type_name -> pluggableharness.agent.frontend.v1.FrontendErrorCategory - 26, // 18: pluggableharness.agent.frontend.v1.ServerEvent.Render.content:type_name -> pluggableharness.agent.render.v1.PlacedContent - 27, // 19: pluggableharness.agent.frontend.v1.ServerEvent.PermissionRequest.plan_item:type_name -> pluggableharness.agent.plan.v1.PlanItem - 28, // 20: pluggableharness.agent.frontend.v1.ServerEvent.PlanReady.plan:type_name -> pluggableharness.agent.plan.v1.Plan - 29, // 21: pluggableharness.agent.frontend.v1.ServerEvent.InteractiveRequest.prompt:type_name -> pluggableharness.agent.render.v1.RenderTree - 30, // 22: pluggableharness.agent.frontend.v1.ServerEvent.SessionTreeUpdate.status:type_name -> pluggableharness.agent.session.v1.SessionStatus - 9, // 23: pluggableharness.agent.frontend.v1.ServerEvent.Error.error:type_name -> pluggableharness.agent.frontend.v1.FrontendError - 0, // 24: pluggableharness.agent.frontend.v1.ClientEvent.PlanDecision.decision:type_name -> pluggableharness.agent.frontend.v1.ClientDecision - 25, // 25: pluggableharness.agent.frontend.v1.ClientEvent.PlanDecision.corrected_input:type_name -> google.protobuf.Struct - 25, // 26: pluggableharness.agent.frontend.v1.ClientEvent.InteractiveResponse.response:type_name -> google.protobuf.Struct - 25, // 27: pluggableharness.agent.frontend.v1.ClientEvent.ActionTrigger.args:type_name -> google.protobuf.Struct - 2, // 28: pluggableharness.agent.frontend.v1.FrontendService.GetCapabilities:input_type -> pluggableharness.agent.frontend.v1.GetCapabilitiesRequest - 5, // 29: pluggableharness.agent.frontend.v1.FrontendService.Configure:input_type -> pluggableharness.agent.frontend.v1.ConfigureRequest - 8, // 30: pluggableharness.agent.frontend.v1.FrontendService.Attach:input_type -> pluggableharness.agent.frontend.v1.ClientEvent - 3, // 31: pluggableharness.agent.frontend.v1.FrontendService.GetCapabilities:output_type -> pluggableharness.agent.frontend.v1.GetCapabilitiesResponse - 6, // 32: pluggableharness.agent.frontend.v1.FrontendService.Configure:output_type -> pluggableharness.agent.frontend.v1.ConfigureResponse - 7, // 33: pluggableharness.agent.frontend.v1.FrontendService.Attach:output_type -> pluggableharness.agent.frontend.v1.ServerEvent - 31, // [31:34] is the sub-list for method output_type - 28, // [28:31] is the sub-list for method input_type - 28, // [28:28] is the sub-list for extension type_name - 28, // [28:28] is the sub-list for extension extendee - 0, // [0:28] is the sub-list for field type_name + 40, // 0: pluggableharness.agent.frontend.v1.DescribeResponse.producer:type_name -> pluggableharness.agent.common.v1.ProducerRef + 7, // 1: pluggableharness.agent.frontend.v1.GetCapabilitiesResponse.capabilities:type_name -> pluggableharness.agent.frontend.v1.FrontendCapabilities + 41, // 2: pluggableharness.agent.frontend.v1.FrontendCapabilities.slash_commands:type_name -> pluggableharness.agent.slashcommand.v1.SlashCommandSpec + 42, // 3: pluggableharness.agent.frontend.v1.FrontendCapabilities.config_schema:type_name -> pluggableharness.agent.config.v1.ConfigSchema + 43, // 4: pluggableharness.agent.frontend.v1.FrontendCapabilities.supported_regions:type_name -> pluggableharness.agent.render.v1.Region + 44, // 5: pluggableharness.agent.frontend.v1.FrontendCapabilities.supported_hook_points:type_name -> pluggableharness.agent.common.v1.HookPoint + 45, // 6: pluggableharness.agent.frontend.v1.ConfigureRequest.config:type_name -> google.protobuf.Struct + 13, // 7: pluggableharness.agent.frontend.v1.ServerEvent.stream_delta:type_name -> pluggableharness.agent.frontend.v1.ServerEvent.StreamDelta + 14, // 8: pluggableharness.agent.frontend.v1.ServerEvent.render:type_name -> pluggableharness.agent.frontend.v1.ServerEvent.Render + 15, // 9: pluggableharness.agent.frontend.v1.ServerEvent.permission_request:type_name -> pluggableharness.agent.frontend.v1.ServerEvent.PermissionRequest + 16, // 10: pluggableharness.agent.frontend.v1.ServerEvent.plan_ready:type_name -> pluggableharness.agent.frontend.v1.ServerEvent.PlanReady + 17, // 11: pluggableharness.agent.frontend.v1.ServerEvent.interactive_request:type_name -> pluggableharness.agent.frontend.v1.ServerEvent.InteractiveRequest + 18, // 12: pluggableharness.agent.frontend.v1.ServerEvent.session_tree_update:type_name -> pluggableharness.agent.frontend.v1.ServerEvent.SessionTreeUpdate + 19, // 13: pluggableharness.agent.frontend.v1.ServerEvent.error:type_name -> pluggableharness.agent.frontend.v1.ServerEvent.Error + 20, // 14: pluggableharness.agent.frontend.v1.ServerEvent.session_created:type_name -> pluggableharness.agent.frontend.v1.ServerEvent.SessionCreated + 21, // 15: pluggableharness.agent.frontend.v1.ServerEvent.session_attached:type_name -> pluggableharness.agent.frontend.v1.ServerEvent.SessionAttached + 22, // 16: pluggableharness.agent.frontend.v1.ServerEvent.backfill_complete:type_name -> pluggableharness.agent.frontend.v1.ServerEvent.BackfillComplete + 23, // 17: pluggableharness.agent.frontend.v1.ServerEvent.session_detached:type_name -> pluggableharness.agent.frontend.v1.ServerEvent.SessionDetached + 24, // 18: pluggableharness.agent.frontend.v1.ServerEvent.session_list:type_name -> pluggableharness.agent.frontend.v1.ServerEvent.SessionList + 25, // 19: pluggableharness.agent.frontend.v1.ServerEvent.slash_command_registry:type_name -> pluggableharness.agent.frontend.v1.ServerEvent.SlashCommandRegistry + 26, // 20: pluggableharness.agent.frontend.v1.ServerEvent.usage_update:type_name -> pluggableharness.agent.frontend.v1.ServerEvent.UsageUpdate + 27, // 21: pluggableharness.agent.frontend.v1.ServerEvent.session_status_update:type_name -> pluggableharness.agent.frontend.v1.ServerEvent.SessionStatusUpdate + 28, // 22: pluggableharness.agent.frontend.v1.ClientEvent.user_message:type_name -> pluggableharness.agent.frontend.v1.ClientEvent.UserMessage + 29, // 23: pluggableharness.agent.frontend.v1.ClientEvent.slash_command:type_name -> pluggableharness.agent.frontend.v1.ClientEvent.SlashCommand + 30, // 24: pluggableharness.agent.frontend.v1.ClientEvent.plan_decision:type_name -> pluggableharness.agent.frontend.v1.ClientEvent.PlanDecision + 31, // 25: pluggableharness.agent.frontend.v1.ClientEvent.interactive_response:type_name -> pluggableharness.agent.frontend.v1.ClientEvent.InteractiveResponse + 32, // 26: pluggableharness.agent.frontend.v1.ClientEvent.action_trigger:type_name -> pluggableharness.agent.frontend.v1.ClientEvent.ActionTrigger + 33, // 27: pluggableharness.agent.frontend.v1.ClientEvent.interrupt:type_name -> pluggableharness.agent.frontend.v1.ClientEvent.Interrupt + 34, // 28: pluggableharness.agent.frontend.v1.ClientEvent.hello:type_name -> pluggableharness.agent.frontend.v1.ClientEvent.Hello + 35, // 29: pluggableharness.agent.frontend.v1.ClientEvent.create_session:type_name -> pluggableharness.agent.frontend.v1.ClientEvent.CreateSession + 36, // 30: pluggableharness.agent.frontend.v1.ClientEvent.attach_session:type_name -> pluggableharness.agent.frontend.v1.ClientEvent.AttachSession + 37, // 31: pluggableharness.agent.frontend.v1.ClientEvent.resume_session:type_name -> pluggableharness.agent.frontend.v1.ClientEvent.ResumeSession + 38, // 32: pluggableharness.agent.frontend.v1.ClientEvent.detach_session:type_name -> pluggableharness.agent.frontend.v1.ClientEvent.DetachSession + 39, // 33: pluggableharness.agent.frontend.v1.ClientEvent.list_sessions:type_name -> pluggableharness.agent.frontend.v1.ClientEvent.ListSessions + 2, // 34: pluggableharness.agent.frontend.v1.FrontendError.category:type_name -> pluggableharness.agent.frontend.v1.FrontendErrorCategory + 46, // 35: pluggableharness.agent.frontend.v1.ServerEvent.Render.content:type_name -> pluggableharness.agent.render.v1.PlacedContent + 47, // 36: pluggableharness.agent.frontend.v1.ServerEvent.PermissionRequest.plan_item:type_name -> pluggableharness.agent.plan.v1.PlanItem + 48, // 37: pluggableharness.agent.frontend.v1.ServerEvent.PlanReady.plan:type_name -> pluggableharness.agent.plan.v1.Plan + 49, // 38: pluggableharness.agent.frontend.v1.ServerEvent.InteractiveRequest.prompt:type_name -> pluggableharness.agent.render.v1.RenderTree + 50, // 39: pluggableharness.agent.frontend.v1.ServerEvent.SessionTreeUpdate.status:type_name -> pluggableharness.agent.session.v1.SessionStatus + 12, // 40: pluggableharness.agent.frontend.v1.ServerEvent.Error.error:type_name -> pluggableharness.agent.frontend.v1.FrontendError + 51, // 41: pluggableharness.agent.frontend.v1.ServerEvent.SessionCreated.info:type_name -> pluggableharness.agent.session.v1.SessionInfo + 51, // 42: pluggableharness.agent.frontend.v1.ServerEvent.SessionAttached.info:type_name -> pluggableharness.agent.session.v1.SessionInfo + 51, // 43: pluggableharness.agent.frontend.v1.ServerEvent.SessionList.sessions:type_name -> pluggableharness.agent.session.v1.SessionInfo + 41, // 44: pluggableharness.agent.frontend.v1.ServerEvent.SlashCommandRegistry.commands:type_name -> pluggableharness.agent.slashcommand.v1.SlashCommandSpec + 52, // 45: pluggableharness.agent.frontend.v1.ServerEvent.UsageUpdate.turn:type_name -> pluggableharness.agent.model.v1.Usage + 50, // 46: pluggableharness.agent.frontend.v1.ServerEvent.SessionStatusUpdate.status:type_name -> pluggableharness.agent.session.v1.SessionStatus + 53, // 47: pluggableharness.agent.frontend.v1.ClientEvent.UserMessage.content:type_name -> pluggableharness.agent.content.v1.ContentBlock + 0, // 48: pluggableharness.agent.frontend.v1.ClientEvent.PlanDecision.decision:type_name -> pluggableharness.agent.frontend.v1.ClientDecision + 45, // 49: pluggableharness.agent.frontend.v1.ClientEvent.PlanDecision.corrected_input:type_name -> google.protobuf.Struct + 1, // 50: pluggableharness.agent.frontend.v1.ClientEvent.PlanDecision.scope:type_name -> pluggableharness.agent.frontend.v1.PlanDecisionScope + 45, // 51: pluggableharness.agent.frontend.v1.ClientEvent.InteractiveResponse.response:type_name -> google.protobuf.Struct + 45, // 52: pluggableharness.agent.frontend.v1.ClientEvent.ActionTrigger.args:type_name -> google.protobuf.Struct + 50, // 53: pluggableharness.agent.frontend.v1.ClientEvent.ListSessions.status:type_name -> pluggableharness.agent.session.v1.SessionStatus + 5, // 54: pluggableharness.agent.frontend.v1.FrontendService.GetCapabilities:input_type -> pluggableharness.agent.frontend.v1.GetCapabilitiesRequest + 8, // 55: pluggableharness.agent.frontend.v1.FrontendService.Configure:input_type -> pluggableharness.agent.frontend.v1.ConfigureRequest + 11, // 56: pluggableharness.agent.frontend.v1.FrontendService.Attach:input_type -> pluggableharness.agent.frontend.v1.ClientEvent + 3, // 57: pluggableharness.agent.frontend.v1.FrontendService.Describe:input_type -> pluggableharness.agent.frontend.v1.DescribeRequest + 6, // 58: pluggableharness.agent.frontend.v1.FrontendService.GetCapabilities:output_type -> pluggableharness.agent.frontend.v1.GetCapabilitiesResponse + 9, // 59: pluggableharness.agent.frontend.v1.FrontendService.Configure:output_type -> pluggableharness.agent.frontend.v1.ConfigureResponse + 10, // 60: pluggableharness.agent.frontend.v1.FrontendService.Attach:output_type -> pluggableharness.agent.frontend.v1.ServerEvent + 4, // 61: pluggableharness.agent.frontend.v1.FrontendService.Describe:output_type -> pluggableharness.agent.frontend.v1.DescribeResponse + 58, // [58:62] is the sub-list for method output_type + 54, // [54:58] is the sub-list for method input_type + 54, // [54:54] is the sub-list for extension type_name + 54, // [54:54] is the sub-list for extension extendee + 0, // [0:54] is the sub-list for field type_name } func init() { file_pluggableharness_agent_frontend_v1_frontend_proto_init() } @@ -1643,7 +3119,7 @@ func file_pluggableharness_agent_frontend_v1_frontend_proto_init() { if File_pluggableharness_agent_frontend_v1_frontend_proto != nil { return } - file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[5].OneofWrappers = []any{ + file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[7].OneofWrappers = []any{ (*ServerEvent_StreamDelta_)(nil), (*ServerEvent_Render_)(nil), (*ServerEvent_PermissionRequest_)(nil), @@ -1651,23 +3127,39 @@ func file_pluggableharness_agent_frontend_v1_frontend_proto_init() { (*ServerEvent_InteractiveRequest_)(nil), (*ServerEvent_SessionTreeUpdate_)(nil), (*ServerEvent_Error_)(nil), - } - file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[6].OneofWrappers = []any{ + (*ServerEvent_SessionCreated_)(nil), + (*ServerEvent_SessionAttached_)(nil), + (*ServerEvent_BackfillComplete_)(nil), + (*ServerEvent_SessionDetached_)(nil), + (*ServerEvent_SessionList_)(nil), + (*ServerEvent_SlashCommandRegistry_)(nil), + (*ServerEvent_UsageUpdate_)(nil), + (*ServerEvent_SessionStatusUpdate_)(nil), + } + file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[8].OneofWrappers = []any{ (*ClientEvent_UserMessage_)(nil), (*ClientEvent_SlashCommand_)(nil), (*ClientEvent_PlanDecision_)(nil), (*ClientEvent_InteractiveResponse_)(nil), (*ClientEvent_ActionTrigger_)(nil), (*ClientEvent_Interrupt_)(nil), - } - file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[17].OneofWrappers = []any{} + (*ClientEvent_Hello_)(nil), + (*ClientEvent_CreateSession_)(nil), + (*ClientEvent_AttachSession_)(nil), + (*ClientEvent_ResumeSession_)(nil), + (*ClientEvent_DetachSession_)(nil), + (*ClientEvent_ListSessions_)(nil), + } + file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[27].OneofWrappers = []any{} + file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[32].OneofWrappers = []any{} + file_pluggableharness_agent_frontend_v1_frontend_proto_msgTypes[36].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_pluggableharness_agent_frontend_v1_frontend_proto_rawDesc), len(file_pluggableharness_agent_frontend_v1_frontend_proto_rawDesc)), - NumEnums: 2, - NumMessages: 21, + NumEnums: 3, + NumMessages: 37, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/frontend/proto/v1/frontend_grpc.pb.go b/pkg/frontend/proto/v1/frontend_grpc.pb.go index af01905..bb49856 100644 --- a/pkg/frontend/proto/v1/frontend_grpc.pb.go +++ b/pkg/frontend/proto/v1/frontend_grpc.pb.go @@ -26,6 +26,7 @@ const ( FrontendService_GetCapabilities_FullMethodName = "/pluggableharness.agent.frontend.v1.FrontendService/GetCapabilities" FrontendService_Configure_FullMethodName = "/pluggableharness.agent.frontend.v1.FrontendService/Configure" FrontendService_Attach_FullMethodName = "/pluggableharness.agent.frontend.v1.FrontendService/Attach" + FrontendService_Describe_FullMethodName = "/pluggableharness.agent.frontend.v1.FrontendService/Describe" ) // FrontendServiceClient is the client API for FrontendService service. @@ -42,21 +43,30 @@ type FrontendServiceClient interface { // against the schema returned by GetCapabilities (configuration.md §4). // Unary. frontend.md §3.1. Configure(ctx context.Context, in *ConfigureRequest, opts ...grpc.CallOption) (*ConfigureResponse, error) - // Attach opens the session-scoped event channel between the kernel and - // this frontend: ServerEvents flow from kernel to frontend, ClientEvents - // flow from frontend to kernel, both directions live for the duration of - // the stream. Bidirectional streaming — frontend.md §3.1, and (along with - // the kernel callback channel) one of only two genuinely bidirectional - // RPCs in this protocol series (see .claude/rules/grpc.md). + // Attach opens ONE multiplexed, connection-scoped bidirectional event + // channel between the kernel and this frontend connection — not a + // per-session stream. A frontend subscribes individual sessions onto + // this one stream via the session-control ClientEvent variants + // (create_session/attach_session/resume_session/detach_session), and + // unsubscribes the same way; connection-level operations + // (list_sessions, the aggregate slash-command registry) have a natural + // home here precisely because the stream isn't tied to one session. + // ServerEvents flow from kernel to frontend, ClientEvents flow from + // frontend to kernel, both directions live for the duration of the + // stream. Bidirectional streaming — frontend.md §"Transport", and + // (along with the kernel callback channel) one of only two genuinely + // bidirectional RPCs in this protocol series (see .claude/rules/grpc.md). // - // Multiple frontends MAY Attach to one session concurrently - // (frontend.md §3.3): every ServerEvent broadcasts identically to every - // attached frontend, with no partitioning and no "primary" frontend. - // ClientEvents are processed in kernel arrival order; for - // ClientEvent.plan_decision and ClientEvent.interactive_response - // specifically, which name a pending item by id, the kernel applies - // first-response-wins arbitration and MUST reject any later response for - // an already-resolved item with a distinct error back to its sender. + // Multiple frontends MAY subscribe to the same session concurrently on + // their own Attach streams (frontend.md §"Session scope"): every + // ServerEvent for a given session broadcasts identically to every + // frontend subscribed to that session, with no partitioning and no + // "primary" frontend. ClientEvents are processed in kernel arrival + // order; for ClientEvent.plan_decision and + // ClientEvent.interactive_response specifically, which name a pending + // item by id within a session, the kernel applies first-response-wins + // arbitration per session and MUST reject any later response for an + // already-resolved item with a distinct error back to its sender. // // buf:lint:ignore RPC_REQUEST_STANDARD_NAME // buf:lint:ignore RPC_RESPONSE_STANDARD_NAME @@ -66,6 +76,15 @@ type FrontendServiceClient interface { // uniqueness violation); renaming to Attach*Request/Response would only // satisfy a style convention while discarding real spec traceability. Attach(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ClientEvent, ServerEvent], error) + // Describe reports this plugin build's own identity — {name, version, + // source, category, protocol_version} — directly from the running + // process, rather than the kernel inferring it from a lock-file row. + // Every one of the six category protocols gains this identical RPC in + // this protocol revision; it exists specifically for a + // `dev_overrides`-resolved binary (configuration/lock-file.md's + // "dev_overrides and identity without a lock entry"), which has no + // provider {} lock-file entry to read identity from at all. + Describe(ctx context.Context, in *DescribeRequest, opts ...grpc.CallOption) (*DescribeResponse, error) } type frontendServiceClient struct { @@ -109,6 +128,16 @@ func (c *frontendServiceClient) Attach(ctx context.Context, opts ...grpc.CallOpt // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type FrontendService_AttachClient = grpc.BidiStreamingClient[ClientEvent, ServerEvent] +func (c *frontendServiceClient) Describe(ctx context.Context, in *DescribeRequest, opts ...grpc.CallOption) (*DescribeResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DescribeResponse) + err := c.cc.Invoke(ctx, FrontendService_Describe_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // FrontendServiceServer is the server API for FrontendService service. // All implementations must embed UnimplementedFrontendServiceServer // for forward compatibility. @@ -123,21 +152,30 @@ type FrontendServiceServer interface { // against the schema returned by GetCapabilities (configuration.md §4). // Unary. frontend.md §3.1. Configure(context.Context, *ConfigureRequest) (*ConfigureResponse, error) - // Attach opens the session-scoped event channel between the kernel and - // this frontend: ServerEvents flow from kernel to frontend, ClientEvents - // flow from frontend to kernel, both directions live for the duration of - // the stream. Bidirectional streaming — frontend.md §3.1, and (along with - // the kernel callback channel) one of only two genuinely bidirectional - // RPCs in this protocol series (see .claude/rules/grpc.md). + // Attach opens ONE multiplexed, connection-scoped bidirectional event + // channel between the kernel and this frontend connection — not a + // per-session stream. A frontend subscribes individual sessions onto + // this one stream via the session-control ClientEvent variants + // (create_session/attach_session/resume_session/detach_session), and + // unsubscribes the same way; connection-level operations + // (list_sessions, the aggregate slash-command registry) have a natural + // home here precisely because the stream isn't tied to one session. + // ServerEvents flow from kernel to frontend, ClientEvents flow from + // frontend to kernel, both directions live for the duration of the + // stream. Bidirectional streaming — frontend.md §"Transport", and + // (along with the kernel callback channel) one of only two genuinely + // bidirectional RPCs in this protocol series (see .claude/rules/grpc.md). // - // Multiple frontends MAY Attach to one session concurrently - // (frontend.md §3.3): every ServerEvent broadcasts identically to every - // attached frontend, with no partitioning and no "primary" frontend. - // ClientEvents are processed in kernel arrival order; for - // ClientEvent.plan_decision and ClientEvent.interactive_response - // specifically, which name a pending item by id, the kernel applies - // first-response-wins arbitration and MUST reject any later response for - // an already-resolved item with a distinct error back to its sender. + // Multiple frontends MAY subscribe to the same session concurrently on + // their own Attach streams (frontend.md §"Session scope"): every + // ServerEvent for a given session broadcasts identically to every + // frontend subscribed to that session, with no partitioning and no + // "primary" frontend. ClientEvents are processed in kernel arrival + // order; for ClientEvent.plan_decision and + // ClientEvent.interactive_response specifically, which name a pending + // item by id within a session, the kernel applies first-response-wins + // arbitration per session and MUST reject any later response for an + // already-resolved item with a distinct error back to its sender. // // buf:lint:ignore RPC_REQUEST_STANDARD_NAME // buf:lint:ignore RPC_RESPONSE_STANDARD_NAME @@ -147,6 +185,15 @@ type FrontendServiceServer interface { // uniqueness violation); renaming to Attach*Request/Response would only // satisfy a style convention while discarding real spec traceability. Attach(grpc.BidiStreamingServer[ClientEvent, ServerEvent]) error + // Describe reports this plugin build's own identity — {name, version, + // source, category, protocol_version} — directly from the running + // process, rather than the kernel inferring it from a lock-file row. + // Every one of the six category protocols gains this identical RPC in + // this protocol revision; it exists specifically for a + // `dev_overrides`-resolved binary (configuration/lock-file.md's + // "dev_overrides and identity without a lock entry"), which has no + // provider {} lock-file entry to read identity from at all. + Describe(context.Context, *DescribeRequest) (*DescribeResponse, error) mustEmbedUnimplementedFrontendServiceServer() } @@ -166,6 +213,9 @@ func (UnimplementedFrontendServiceServer) Configure(context.Context, *ConfigureR func (UnimplementedFrontendServiceServer) Attach(grpc.BidiStreamingServer[ClientEvent, ServerEvent]) error { return status.Error(codes.Unimplemented, "method Attach not implemented") } +func (UnimplementedFrontendServiceServer) Describe(context.Context, *DescribeRequest) (*DescribeResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Describe not implemented") +} func (UnimplementedFrontendServiceServer) mustEmbedUnimplementedFrontendServiceServer() {} func (UnimplementedFrontendServiceServer) testEmbeddedByValue() {} @@ -230,6 +280,24 @@ func _FrontendService_Attach_Handler(srv interface{}, stream grpc.ServerStream) // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type FrontendService_AttachServer = grpc.BidiStreamingServer[ClientEvent, ServerEvent] +func _FrontendService_Describe_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DescribeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(FrontendServiceServer).Describe(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: FrontendService_Describe_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(FrontendServiceServer).Describe(ctx, req.(*DescribeRequest)) + } + return interceptor(ctx, in, info, handler) +} + // FrontendService_ServiceDesc is the grpc.ServiceDesc for FrontendService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -245,6 +313,10 @@ var FrontendService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Configure", Handler: _FrontendService_Configure_Handler, }, + { + MethodName: "Describe", + Handler: _FrontendService_Describe_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/pkg/memory/proto/v1/memory.pb.go b/pkg/memory/proto/v1/memory.pb.go index 7a26568..251e5d9 100644 --- a/pkg/memory/proto/v1/memory.pb.go +++ b/pkg/memory/proto/v1/memory.pb.go @@ -16,10 +16,11 @@ package memoryv1 import ( + v12 "github.com/pluggableharness/agent/pkg/common/proto/v1" v11 "github.com/pluggableharness/agent/pkg/config/proto/v1" - v13 "github.com/pluggableharness/agent/pkg/content/proto/v1" - v12 "github.com/pluggableharness/agent/pkg/model/proto/v1" - v14 "github.com/pluggableharness/agent/pkg/render/proto/v1" + v14 "github.com/pluggableharness/agent/pkg/content/proto/v1" + v13 "github.com/pluggableharness/agent/pkg/model/proto/v1" + v15 "github.com/pluggableharness/agent/pkg/render/proto/v1" v1 "github.com/pluggableharness/agent/pkg/slashcommand/proto/v1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" @@ -254,6 +255,10 @@ const ( MemoryErrorCategory_MEMORY_ERROR_CATEGORY_SOURCE_UNAVAILABLE MemoryErrorCategory = 5 // Any error not covered by a more specific category above. MemoryErrorCategory_MEMORY_ERROR_CATEGORY_UNKNOWN MemoryErrorCategory = 6 + // Record specified a MemoryScope this provider doesn't support (absent + // from GetCapabilities.supported_scopes) — the scope-taxonomy mirror of + // MEMORY_ERROR_CATEGORY_INVALID_TYPE above. + MemoryErrorCategory_MEMORY_ERROR_CATEGORY_INVALID_SCOPE MemoryErrorCategory = 7 ) // Enum value maps for MemoryErrorCategory. @@ -266,6 +271,7 @@ var ( 4: "MEMORY_ERROR_CATEGORY_BUDGET_EXCEEDED", 5: "MEMORY_ERROR_CATEGORY_SOURCE_UNAVAILABLE", 6: "MEMORY_ERROR_CATEGORY_UNKNOWN", + 7: "MEMORY_ERROR_CATEGORY_INVALID_SCOPE", } MemoryErrorCategory_value = map[string]int32{ "MEMORY_ERROR_CATEGORY_UNSPECIFIED": 0, @@ -275,6 +281,7 @@ var ( "MEMORY_ERROR_CATEGORY_BUDGET_EXCEEDED": 4, "MEMORY_ERROR_CATEGORY_SOURCE_UNAVAILABLE": 5, "MEMORY_ERROR_CATEGORY_UNKNOWN": 6, + "MEMORY_ERROR_CATEGORY_INVALID_SCOPE": 7, } ) @@ -365,9 +372,20 @@ type MemoryCapabilities struct { // remember/forget/search cases via the ordinary tool-provider path. SlashCommands []*v1.SlashCommandSpec `protobuf:"bytes,5,rep,name=slash_commands,json=slashCommands,proto3" json:"slash_commands,omitempty"` // This provider's agent.hcl config schema, per configuration.md §4. - ConfigSchema *v11.ConfigSchema `protobuf:"bytes,6,opt,name=config_schema,json=configSchema,proto3" json:"config_schema,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + ConfigSchema *v11.ConfigSchema `protobuf:"bytes,6,opt,name=config_schema,json=configSchema,proto3" json:"config_schema,omitempty"` + // Which hook points (agent-loop/hook-dispatch.md) this provider declares + // HookSubscriberService.DispatchHook subscriptions for. Note: this + // category's implicit post_model_response (observe, every turn) and + // session_end (fires once) write triggers (memory.md §Write triggers) + // now ride the hook.v1 HookSubscriberService.DispatchHook surface — see + // protocol.md's Write triggers section for the payload shapes. The + // HookPoint enum itself lives in common.v1, not hook.v1 — hook.v1 + // imports this package's model/tool/plan dependencies, so a category + // capability message importing hook.v1 directly would cycle back + // through it; common.v1 is the shared leaf package instead. + SupportedHookPoints []v12.HookPoint `protobuf:"varint,7,rep,packed,name=supported_hook_points,json=supportedHookPoints,proto3,enum=pluggableharness.agent.common.v1.HookPoint" json:"supported_hook_points,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *MemoryCapabilities) Reset() { @@ -442,6 +460,13 @@ func (x *MemoryCapabilities) GetConfigSchema() *v11.ConfigSchema { return nil } +func (x *MemoryCapabilities) GetSupportedHookPoints() []v12.HookPoint { + if x != nil { + return x.SupportedHookPoints + } + return nil +} + // GetCapabilitiesResponse wraps this provider's capability advertisement. type GetCapabilitiesResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -585,8 +610,12 @@ type RecallRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The requesting session's id. SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` - // The current turn number within that session. - TurnNumber int64 `protobuf:"varint,2,opt,name=turn_number,json=turnNumber,proto3" json:"turn_number,omitempty"` + // The current turn within that session. A ULID, standardized across the + // whole protocol (matches plan.v1's turn_id field and context.v1's + // ContextRequest.turn_id, same treatment). Same field number as the + // retired int64 turn-number predecessor field; the rename is a + // sanctioned pre-release wire change, not a v2 bump. + TurnId string `protobuf:"bytes,2,opt,name=turn_id,json=turnId,proto3" json:"turn_id,omitempty"` // The token budget this Recall call MUST self-truncate its returned // records to. Competes for the same budget pool as context providers, // resolved the same way context.md §6 resolves a context provider's cap. @@ -595,7 +624,7 @@ type RecallRequest struct { // §4's ContextRequest field. Lets a provider pass a precise model // reference into the CountTokens kernel callback (kernel-callbacks.md // §2) — added per kernel-callbacks.md §6's correction. MUST be set. - ModelTarget *v12.ModelTarget `protobuf:"bytes,4,opt,name=model_target,json=modelTarget,proto3" json:"model_target,omitempty"` + ModelTarget *v13.ModelTarget `protobuf:"bytes,4,opt,name=model_target,json=modelTarget,proto3" json:"model_target,omitempty"` // Paths of files touched so far this turn, mirroring context.md §4's // field of the same name. MAY be empty. FilesTouched []string `protobuf:"bytes,5,rep,name=files_touched,json=filesTouched,proto3" json:"files_touched,omitempty"` @@ -652,11 +681,11 @@ func (x *RecallRequest) GetSessionId() string { return "" } -func (x *RecallRequest) GetTurnNumber() int64 { +func (x *RecallRequest) GetTurnId() string { if x != nil { - return x.TurnNumber + return x.TurnId } - return 0 + return "" } func (x *RecallRequest) GetTokenBudget() int64 { @@ -666,7 +695,7 @@ func (x *RecallRequest) GetTokenBudget() int64 { return 0 } -func (x *RecallRequest) GetModelTarget() *v12.ModelTarget { +func (x *RecallRequest) GetModelTarget() *v13.ModelTarget { if x != nil { return x.ModelTarget } @@ -771,7 +800,7 @@ type MemoryRecord struct { Title string `protobuf:"bytes,4,opt,name=title,proto3" json:"title,omitempty"` // The record's content. Text-only in v1, same constraint as context.md // §4. - Content []*v13.ContentBlock `protobuf:"bytes,5,rep,name=content,proto3" json:"content,omitempty"` + Content []*v14.ContentBlock `protobuf:"bytes,5,rep,name=content,proto3" json:"content,omitempty"` // This record's size, computed via the kernel's CountTokens callback // (kernel-callbacks.md §2), never a provider-local heuristic. Tokens int64 `protobuf:"varint,6,opt,name=tokens,proto3" json:"tokens,omitempty"` @@ -784,9 +813,23 @@ type MemoryRecord struct { // When this record was first created. CreatedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` // When this record was last modified. - UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + // Where this record came from and who wrote it. Kernel-populated at + // Record time, immutable thereafter — never provider-supplied or + // provider-mutable. memory.md's README already frames provenance as + // first-class; this is where the record shape finally carries it. + Provenance *Provenance `protobuf:"bytes,11,opt,name=provenance,proto3" json:"provenance,omitempty"` + // This record's recall-time relevance, in [0, 1]. Set only on + // Recall/ListRecords responses — never persisted, never present on a + // Record/UpdateRecord request or response. Lets the kernel merge + // multiple memory providers' results under one shared budget + // (data-types.md#recallrequest--memoryrecord) using a comparable score + // rather than provider-opaque ordering alone. A provider setting this + // MUST normalize it to [0, 1]; scores from different providers are only + // meaningfully comparable if each normalizes to the same range. + RelevanceScore *float64 `protobuf:"fixed64,12,opt,name=relevance_score,json=relevanceScore,proto3,oneof" json:"relevance_score,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *MemoryRecord) Reset() { @@ -847,7 +890,7 @@ func (x *MemoryRecord) GetTitle() string { return "" } -func (x *MemoryRecord) GetContent() []*v13.ContentBlock { +func (x *MemoryRecord) GetContent() []*v14.ContentBlock { if x != nil { return x.Content } @@ -889,6 +932,90 @@ func (x *MemoryRecord) GetUpdatedAt() *timestamppb.Timestamp { return nil } +func (x *MemoryRecord) GetProvenance() *Provenance { + if x != nil { + return x.Provenance + } + return nil +} + +func (x *MemoryRecord) GetRelevanceScore() float64 { + if x != nil && x.RelevanceScore != nil { + return *x.RelevanceScore + } + return 0 +} + +// Provenance records where a MemoryRecord came from and who wrote it. +// Kernel-populated at Record time; immutable afterward, like the rest of a +// record's write-time metadata. +type Provenance struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The session id that produced this record. + SourceSessionId string `protobuf:"bytes,1,opt,name=source_session_id,json=sourceSessionId,proto3" json:"source_session_id,omitempty"` + // The turn id (ULID, see ContextRequest.turn_id) within source_session_id + // that produced this record, when known. Absent for a record written + // outside normal turn flow (e.g. a backfill/import). + SourceTurnId *string `protobuf:"bytes,2,opt,name=source_turn_id,json=sourceTurnId,proto3,oneof" json:"source_turn_id,omitempty"` + // The producing plugin's declared name, or the reference tool path that + // wrote it (e.g. "memory.remember"). Kernel-populated from the calling + // context, not provider-supplied. + RecordedBy string `protobuf:"bytes,3,opt,name=recorded_by,json=recordedBy,proto3" json:"recorded_by,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Provenance) Reset() { + *x = Provenance{} + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Provenance) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Provenance) ProtoMessage() {} + +func (x *Provenance) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Provenance.ProtoReflect.Descriptor instead. +func (*Provenance) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{8} +} + +func (x *Provenance) GetSourceSessionId() string { + if x != nil { + return x.SourceSessionId + } + return "" +} + +func (x *Provenance) GetSourceTurnId() string { + if x != nil && x.SourceTurnId != nil { + return *x.SourceTurnId + } + return "" +} + +func (x *Provenance) GetRecordedBy() string { + if x != nil { + return x.RecordedBy + } + return "" +} + // RecordRequest creates a new record. memory.md §7. type RecordRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -903,14 +1030,14 @@ type RecordRequest struct { // Human-readable title. Title string `protobuf:"bytes,4,opt,name=title,proto3" json:"title,omitempty"` // The record's content. - Content []*v13.ContentBlock `protobuf:"bytes,5,rep,name=content,proto3" json:"content,omitempty"` + Content []*v14.ContentBlock `protobuf:"bytes,5,rep,name=content,proto3" json:"content,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RecordRequest) Reset() { *x = RecordRequest{} - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[8] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -922,7 +1049,7 @@ func (x *RecordRequest) String() string { func (*RecordRequest) ProtoMessage() {} func (x *RecordRequest) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[8] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -935,7 +1062,7 @@ func (x *RecordRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RecordRequest.ProtoReflect.Descriptor instead. func (*RecordRequest) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{8} + return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{9} } func (x *RecordRequest) GetType() MemoryType { @@ -966,7 +1093,7 @@ func (x *RecordRequest) GetTitle() string { return "" } -func (x *RecordRequest) GetContent() []*v13.ContentBlock { +func (x *RecordRequest) GetContent() []*v14.ContentBlock { if x != nil { return x.Content } @@ -989,7 +1116,7 @@ type RecordResult struct { func (x *RecordResult) Reset() { *x = RecordResult{} - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[9] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1001,7 +1128,7 @@ func (x *RecordResult) String() string { func (*RecordResult) ProtoMessage() {} func (x *RecordResult) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[9] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1014,7 +1141,7 @@ func (x *RecordResult) ProtoReflect() protoreflect.Message { // Deprecated: Use RecordResult.ProtoReflect.Descriptor instead. func (*RecordResult) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{9} + return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{10} } func (x *RecordResult) GetId() string { @@ -1042,7 +1169,7 @@ type RecordResponse struct { func (x *RecordResponse) Reset() { *x = RecordResponse{} - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[10] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1054,7 +1181,7 @@ func (x *RecordResponse) String() string { func (*RecordResponse) ProtoMessage() {} func (x *RecordResponse) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[10] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1067,7 +1194,7 @@ func (x *RecordResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RecordResponse.ProtoReflect.Descriptor instead. func (*RecordResponse) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{10} + return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{11} } func (x *RecordResponse) GetResult() *RecordResult { @@ -1088,14 +1215,14 @@ type UpdateRecordRequest struct { Title *string `protobuf:"bytes,2,opt,name=title,proto3,oneof" json:"title,omitempty"` // The record's new content, replacing the existing content wholesale — // not a patch. MUST be set. - Content []*v13.ContentBlock `protobuf:"bytes,3,rep,name=content,proto3" json:"content,omitempty"` + Content []*v14.ContentBlock `protobuf:"bytes,3,rep,name=content,proto3" json:"content,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *UpdateRecordRequest) Reset() { *x = UpdateRecordRequest{} - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[11] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1107,7 +1234,7 @@ func (x *UpdateRecordRequest) String() string { func (*UpdateRecordRequest) ProtoMessage() {} func (x *UpdateRecordRequest) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[11] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1120,7 +1247,7 @@ func (x *UpdateRecordRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateRecordRequest.ProtoReflect.Descriptor instead. func (*UpdateRecordRequest) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{11} + return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{12} } func (x *UpdateRecordRequest) GetId() string { @@ -1137,7 +1264,7 @@ func (x *UpdateRecordRequest) GetTitle() string { return "" } -func (x *UpdateRecordRequest) GetContent() []*v13.ContentBlock { +func (x *UpdateRecordRequest) GetContent() []*v14.ContentBlock { if x != nil { return x.Content } @@ -1158,7 +1285,7 @@ type UpdateRecordResponse struct { func (x *UpdateRecordResponse) Reset() { *x = UpdateRecordResponse{} - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[12] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1170,7 +1297,7 @@ func (x *UpdateRecordResponse) String() string { func (*UpdateRecordResponse) ProtoMessage() {} func (x *UpdateRecordResponse) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[12] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1183,7 +1310,7 @@ func (x *UpdateRecordResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateRecordResponse.ProtoReflect.Descriptor instead. func (*UpdateRecordResponse) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{12} + return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{13} } func (x *UpdateRecordResponse) GetResult() *RecordResult { @@ -1205,7 +1332,7 @@ type DeleteRecordRequest struct { func (x *DeleteRecordRequest) Reset() { *x = DeleteRecordRequest{} - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[13] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1217,7 +1344,7 @@ func (x *DeleteRecordRequest) String() string { func (*DeleteRecordRequest) ProtoMessage() {} func (x *DeleteRecordRequest) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[13] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1230,7 +1357,7 @@ func (x *DeleteRecordRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteRecordRequest.ProtoReflect.Descriptor instead. func (*DeleteRecordRequest) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{13} + return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{14} } func (x *DeleteRecordRequest) GetId() string { @@ -1254,7 +1381,7 @@ type DeleteResult struct { func (x *DeleteResult) Reset() { *x = DeleteResult{} - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[14] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1266,7 +1393,7 @@ func (x *DeleteResult) String() string { func (*DeleteResult) ProtoMessage() {} func (x *DeleteResult) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[14] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1279,7 +1406,7 @@ func (x *DeleteResult) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteResult.ProtoReflect.Descriptor instead. func (*DeleteResult) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{14} + return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{15} } func (x *DeleteResult) GetDeleted() bool { @@ -1300,7 +1427,7 @@ type DeleteRecordResponse struct { func (x *DeleteRecordResponse) Reset() { *x = DeleteRecordResponse{} - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[15] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1312,7 +1439,7 @@ func (x *DeleteRecordResponse) String() string { func (*DeleteRecordResponse) ProtoMessage() {} func (x *DeleteRecordResponse) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[15] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1325,7 +1452,7 @@ func (x *DeleteRecordResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteRecordResponse.ProtoReflect.Descriptor instead. func (*DeleteRecordResponse) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{15} + return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{16} } func (x *DeleteRecordResponse) GetResult() *DeleteResult { @@ -1347,7 +1474,7 @@ type ApproveRecordRequest struct { func (x *ApproveRecordRequest) Reset() { *x = ApproveRecordRequest{} - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[16] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1359,7 +1486,7 @@ func (x *ApproveRecordRequest) String() string { func (*ApproveRecordRequest) ProtoMessage() {} func (x *ApproveRecordRequest) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[16] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1372,7 +1499,7 @@ func (x *ApproveRecordRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveRecordRequest.ProtoReflect.Descriptor instead. func (*ApproveRecordRequest) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{16} + return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{17} } func (x *ApproveRecordRequest) GetId() string { @@ -1396,7 +1523,7 @@ type ApproveRecordResponse struct { func (x *ApproveRecordResponse) Reset() { *x = ApproveRecordResponse{} - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[17] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1408,7 +1535,7 @@ func (x *ApproveRecordResponse) String() string { func (*ApproveRecordResponse) ProtoMessage() {} func (x *ApproveRecordResponse) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[17] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1421,7 +1548,7 @@ func (x *ApproveRecordResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveRecordResponse.ProtoReflect.Descriptor instead. func (*ApproveRecordResponse) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{17} + return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{18} } func (x *ApproveRecordResponse) GetResult() *RecordResult { @@ -1443,7 +1570,7 @@ type RejectRecordRequest struct { func (x *RejectRecordRequest) Reset() { *x = RejectRecordRequest{} - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[18] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1455,7 +1582,7 @@ func (x *RejectRecordRequest) String() string { func (*RejectRecordRequest) ProtoMessage() {} func (x *RejectRecordRequest) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[18] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1468,7 +1595,7 @@ func (x *RejectRecordRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RejectRecordRequest.ProtoReflect.Descriptor instead. func (*RejectRecordRequest) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{18} + return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{19} } func (x *RejectRecordRequest) GetId() string { @@ -1492,7 +1619,7 @@ type RejectRecordResponse struct { func (x *RejectRecordResponse) Reset() { *x = RejectRecordResponse{} - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[19] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1504,7 +1631,7 @@ func (x *RejectRecordResponse) String() string { func (*RejectRecordResponse) ProtoMessage() {} func (x *RejectRecordResponse) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[19] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1517,7 +1644,7 @@ func (x *RejectRecordResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RejectRecordResponse.ProtoReflect.Descriptor instead. func (*RejectRecordResponse) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{19} + return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{20} } func (x *RejectRecordResponse) GetResult() *DeleteResult { @@ -1543,7 +1670,7 @@ type MemoryError struct { func (x *MemoryError) Reset() { *x = MemoryError{} - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[20] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1555,7 +1682,7 @@ func (x *MemoryError) String() string { func (*MemoryError) ProtoMessage() {} func (x *MemoryError) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[20] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1568,7 +1695,7 @@ func (x *MemoryError) ProtoReflect() protoreflect.Message { // Deprecated: Use MemoryError.ProtoReflect.Descriptor instead. func (*MemoryError) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{20} + return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{21} } func (x *MemoryError) GetCategory() MemoryErrorCategory { @@ -1598,14 +1725,21 @@ type RenderRequest struct { // The opaque emitted payload to render — see grpc.md's Emit->Render-> // Paint carve-out; this is the one field in this file that is // deliberately untyped bytes rather than a concrete message. - Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"` + Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"` + // The schema version this payload was emitted against, so a Render + // implementation can detect drift between the version it was built + // against and the version live in a running session. See + // ../frontend/render-tree.md#schema-versioning for the canonical + // definition of this field's semantics (owned by the frontend/widget + // workstream; this field just carries the same value). + SchemaVersion string `protobuf:"bytes,2,opt,name=schema_version,json=schemaVersion,proto3" json:"schema_version,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RenderRequest) Reset() { *x = RenderRequest{} - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[21] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1617,7 +1751,7 @@ func (x *RenderRequest) String() string { func (*RenderRequest) ProtoMessage() {} func (x *RenderRequest) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[21] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1630,7 +1764,7 @@ func (x *RenderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RenderRequest.ProtoReflect.Descriptor instead. func (*RenderRequest) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{21} + return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{22} } func (x *RenderRequest) GetPayload() []byte { @@ -1640,18 +1774,25 @@ func (x *RenderRequest) GetPayload() []byte { return nil } +func (x *RenderRequest) GetSchemaVersion() string { + if x != nil { + return x.SchemaVersion + } + return "" +} + // RenderResponse wraps this provider's rendered output. memory.md §10. type RenderResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The rendered tree, per frontend.md §1. - Tree *v14.RenderTree `protobuf:"bytes,1,opt,name=tree,proto3" json:"tree,omitempty"` + Tree *v15.RenderTree `protobuf:"bytes,1,opt,name=tree,proto3" json:"tree,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RenderResponse) Reset() { *x = RenderResponse{} - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[22] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1663,7 +1804,7 @@ func (x *RenderResponse) String() string { func (*RenderResponse) ProtoMessage() {} func (x *RenderResponse) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[22] + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1676,39 +1817,366 @@ func (x *RenderResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RenderResponse.ProtoReflect.Descriptor instead. func (*RenderResponse) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{22} + return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{23} } -func (x *RenderResponse) GetTree() *v14.RenderTree { +func (x *RenderResponse) GetTree() *v15.RenderTree { if x != nil { return x.Tree } return nil } +// ListRecordsRequest is the enumeration/audit query: paginated browsing of +// this provider's records, filterable by type/scope/status. Unlike +// RecallRequest, there is no include_pending gate — this is the +// review-inbox path (e.g. a ratification review UI or generic record +// browsing), where PENDING records ARE listable, not the budget-constrained +// per-turn recall path. memory.md §7. +type ListRecordsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Restricts results to these MemoryTypes. MAY be empty, meaning all + // types this provider supports. + TypeFilter []MemoryType `protobuf:"varint,1,rep,packed,name=type_filter,json=typeFilter,proto3,enum=pluggableharness.agent.memory.v1.MemoryType" json:"type_filter,omitempty"` + // Restricts results to these MemoryScopes. MAY be empty, meaning all + // scopes this provider supports. + ScopeFilter []MemoryScope `protobuf:"varint,2,rep,packed,name=scope_filter,json=scopeFilter,proto3,enum=pluggableharness.agent.memory.v1.MemoryScope" json:"scope_filter,omitempty"` + // Restricts results to this RecordStatus. Unset means both CANONICAL and + // PENDING records are eligible — this is the one path where a PENDING + // record is listable without any include_pending-style gate. + StatusFilter *RecordStatus `protobuf:"varint,3,opt,name=status_filter,json=statusFilter,proto3,enum=pluggableharness.agent.memory.v1.RecordStatus,oneof" json:"status_filter,omitempty"` + // Maximum number of records to return in one page. + PageSize int32 `protobuf:"varint,4,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Opaque continuation token from a prior ListRecordsResponse. + // next_page_token. Empty on the first page. + PageToken string `protobuf:"bytes,5,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRecordsRequest) Reset() { + *x = ListRecordsRequest{} + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRecordsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRecordsRequest) ProtoMessage() {} + +func (x *ListRecordsRequest) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRecordsRequest.ProtoReflect.Descriptor instead. +func (*ListRecordsRequest) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{24} +} + +func (x *ListRecordsRequest) GetTypeFilter() []MemoryType { + if x != nil { + return x.TypeFilter + } + return nil +} + +func (x *ListRecordsRequest) GetScopeFilter() []MemoryScope { + if x != nil { + return x.ScopeFilter + } + return nil +} + +func (x *ListRecordsRequest) GetStatusFilter() RecordStatus { + if x != nil && x.StatusFilter != nil { + return *x.StatusFilter + } + return RecordStatus_RECORD_STATUS_UNSPECIFIED +} + +func (x *ListRecordsRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListRecordsRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +// ListRecordsResponse carries one page of matching records. +type ListRecordsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // This page's records. + Records []*MemoryRecord `protobuf:"bytes,1,rep,name=records,proto3" json:"records,omitempty"` + // Opaque continuation token for the next page. Empty when this is the + // last page. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRecordsResponse) Reset() { + *x = ListRecordsResponse{} + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRecordsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRecordsResponse) ProtoMessage() {} + +func (x *ListRecordsResponse) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRecordsResponse.ProtoReflect.Descriptor instead. +func (*ListRecordsResponse) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{25} +} + +func (x *ListRecordsResponse) GetRecords() []*MemoryRecord { + if x != nil { + return x.Records + } + return nil +} + +func (x *ListRecordsResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +// GetRecordRequest identifies exactly one record to fetch by id. +type GetRecordRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The record's id. MUST match an existing record, or the call fails with + // a structured MemoryError (category NOT_FOUND). + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetRecordRequest) Reset() { + *x = GetRecordRequest{} + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetRecordRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetRecordRequest) ProtoMessage() {} + +func (x *GetRecordRequest) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetRecordRequest.ProtoReflect.Descriptor instead. +func (*GetRecordRequest) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{26} +} + +func (x *GetRecordRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +// GetRecordResponse wraps the fetched record. +type GetRecordResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The fetched record. + Record *MemoryRecord `protobuf:"bytes,1,opt,name=record,proto3" json:"record,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetRecordResponse) Reset() { + *x = GetRecordResponse{} + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetRecordResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetRecordResponse) ProtoMessage() {} + +func (x *GetRecordResponse) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetRecordResponse.ProtoReflect.Descriptor instead. +func (*GetRecordResponse) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{27} +} + +func (x *GetRecordResponse) GetRecord() *MemoryRecord { + if x != nil { + return x.Record + } + return nil +} + +// DescribeRequest carries no fields — Describe takes no request-scoped +// parameters. +type DescribeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeRequest) Reset() { + *x = DescribeRequest{} + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeRequest) ProtoMessage() {} + +func (x *DescribeRequest) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeRequest.ProtoReflect.Descriptor instead. +func (*DescribeRequest) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{28} +} + +// DescribeResponse reports this plugin build's own identity. See the +// Describe RPC comment on MemoryService above. +type DescribeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // This plugin build's identity: name, version, source, category, + // protocol_version. + Producer *v12.ProducerRef `protobuf:"bytes,1,opt,name=producer,proto3" json:"producer,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeResponse) Reset() { + *x = DescribeResponse{} + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeResponse) ProtoMessage() {} + +func (x *DescribeResponse) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeResponse.ProtoReflect.Descriptor instead. +func (*DescribeResponse) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP(), []int{29} +} + +func (x *DescribeResponse) GetProducer() *v12.ProducerRef { + if x != nil { + return x.Producer + } + return nil +} + var File_pluggableharness_agent_memory_v1_memory_proto protoreflect.FileDescriptor const file_pluggableharness_agent_memory_v1_memory_proto_rawDesc = "" + "\n" + - "-pluggableharness/agent/memory/v1/memory.proto\x12 pluggableharness.agent.memory.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a-pluggableharness/agent/config/v1/config.proto\x1a/pluggableharness/agent/content/v1/content.proto\x1a+pluggableharness/agent/model/v1/model.proto\x1a-pluggableharness/agent/render/v1/render.proto\x1a9pluggableharness/agent/slashcommand/v1/slashcommand.proto\"\x18\n" + - "\x16GetCapabilitiesRequest\"\xe4\x03\n" + + "-pluggableharness/agent/memory/v1/memory.proto\x12 pluggableharness.agent.memory.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a-pluggableharness/agent/common/v1/common.proto\x1a-pluggableharness/agent/config/v1/config.proto\x1a/pluggableharness/agent/content/v1/content.proto\x1a+pluggableharness/agent/model/v1/model.proto\x1a-pluggableharness/agent/render/v1/render.proto\x1a9pluggableharness/agent/slashcommand/v1/slashcommand.proto\"\x18\n" + + "\x16GetCapabilitiesRequest\"\xc5\x04\n" + "\x12MemoryCapabilities\x120\n" + "\x14default_token_budget\x18\x01 \x01(\x03R\x12defaultTokenBudget\x12U\n" + "\x0fsupported_types\x18\x02 \x03(\x0e2,.pluggableharness.agent.memory.v1.MemoryTypeR\x0esupportedTypes\x12X\n" + "\x10supported_scopes\x18\x03 \x03(\x0e2-.pluggableharness.agent.memory.v1.MemoryScopeR\x0fsupportedScopes\x125\n" + "\x16ratification_supported\x18\x04 \x01(\bR\x15ratificationSupported\x12_\n" + "\x0eslash_commands\x18\x05 \x03(\v28.pluggableharness.agent.slashcommand.v1.SlashCommandSpecR\rslashCommands\x12S\n" + - "\rconfig_schema\x18\x06 \x01(\v2..pluggableharness.agent.config.v1.ConfigSchemaR\fconfigSchema\"s\n" + + "\rconfig_schema\x18\x06 \x01(\v2..pluggableharness.agent.config.v1.ConfigSchemaR\fconfigSchema\x12_\n" + + "\x15supported_hook_points\x18\a \x03(\x0e2+.pluggableharness.agent.common.v1.HookPointR\x13supportedHookPoints\"s\n" + "\x17GetCapabilitiesResponse\x12X\n" + "\fcapabilities\x18\x01 \x01(\v24.pluggableharness.agent.memory.v1.MemoryCapabilitiesR\fcapabilities\"C\n" + "\x10ConfigureRequest\x12/\n" + "\x06config\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x06config\"\x13\n" + - "\x11ConfigureResponse\"\xdf\x03\n" + + "\x11ConfigureResponse\"\xd7\x03\n" + "\rRecallRequest\x12\x1d\n" + "\n" + - "session_id\x18\x01 \x01(\tR\tsessionId\x12\x1f\n" + - "\vturn_number\x18\x02 \x01(\x03R\n" + - "turnNumber\x12!\n" + + "session_id\x18\x01 \x01(\tR\tsessionId\x12\x17\n" + + "\aturn_id\x18\x02 \x01(\tR\x06turnId\x12!\n" + "\ftoken_budget\x18\x03 \x01(\x03R\vtokenBudget\x12O\n" + "\fmodel_target\x18\x04 \x01(\v2,.pluggableharness.agent.model.v1.ModelTargetR\vmodelTarget\x12#\n" + "\rfiles_touched\x18\x05 \x03(\tR\ffilesTouched\x12+\n" + @@ -1718,7 +2186,7 @@ const file_pluggableharness_agent_memory_v1_memory_proto_rawDesc = "" + "\fscope_filter\x18\b \x03(\x0e2-.pluggableharness.agent.memory.v1.MemoryScopeR\vscopeFilter\x12'\n" + "\x0finclude_pending\x18\t \x01(\bR\x0eincludePending\"Z\n" + "\x0eRecallResponse\x12H\n" + - "\arecords\x18\x01 \x03(\v2..pluggableharness.agent.memory.v1.MemoryRecordR\arecords\"\xf2\x03\n" + + "\arecords\x18\x01 \x03(\v2..pluggableharness.agent.memory.v1.MemoryRecordR\arecords\"\x82\x05\n" + "\fMemoryRecord\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12@\n" + "\x04type\x18\x02 \x01(\x0e2,.pluggableharness.agent.memory.v1.MemoryTypeR\x04type\x12C\n" + @@ -1732,7 +2200,19 @@ const file_pluggableharness_agent_memory_v1_memory_proto_rawDesc = "" + "created_at\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" + "\n" + "updated_at\x18\n" + - " \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\"\x93\x02\n" + + " \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\x12L\n" + + "\n" + + "provenance\x18\v \x01(\v2,.pluggableharness.agent.memory.v1.ProvenanceR\n" + + "provenance\x12,\n" + + "\x0frelevance_score\x18\f \x01(\x01H\x00R\x0erelevanceScore\x88\x01\x01B\x12\n" + + "\x10_relevance_score\"\x97\x01\n" + + "\n" + + "Provenance\x12*\n" + + "\x11source_session_id\x18\x01 \x01(\tR\x0fsourceSessionId\x12)\n" + + "\x0esource_turn_id\x18\x02 \x01(\tH\x00R\fsourceTurnId\x88\x01\x01\x12\x1f\n" + + "\vrecorded_by\x18\x03 \x01(\tR\n" + + "recordedByB\x11\n" + + "\x0f_source_turn_id\"\x93\x02\n" + "\rRecordRequest\x12@\n" + "\x04type\x18\x01 \x01(\x0e2,.pluggableharness.agent.memory.v1.MemoryTypeR\x04type\x12C\n" + "\x05scope\x18\x02 \x01(\x0e2-.pluggableharness.agent.memory.v1.MemoryScopeR\x05scope\x12\x13\n" + @@ -1769,11 +2249,31 @@ const file_pluggableharness_agent_memory_v1_memory_proto_rawDesc = "" + "\vMemoryError\x12Q\n" + "\bcategory\x18\x01 \x01(\x0e25.pluggableharness.agent.memory.v1.MemoryErrorCategoryR\bcategory\x12\x18\n" + "\amessage\x18\x02 \x01(\tR\amessage\x12\x1c\n" + - "\tretryable\x18\x03 \x01(\bR\tretryable\")\n" + + "\tretryable\x18\x03 \x01(\bR\tretryable\"P\n" + "\rRenderRequest\x12\x18\n" + - "\apayload\x18\x01 \x01(\fR\apayload\"R\n" + + "\apayload\x18\x01 \x01(\fR\apayload\x12%\n" + + "\x0eschema_version\x18\x02 \x01(\tR\rschemaVersion\"R\n" + "\x0eRenderResponse\x12@\n" + - "\x04tree\x18\x01 \x01(\v2,.pluggableharness.agent.render.v1.RenderTreeR\x04tree*\x8d\x01\n" + + "\x04tree\x18\x01 \x01(\v2,.pluggableharness.agent.render.v1.RenderTreeR\x04tree\"\xdd\x02\n" + + "\x12ListRecordsRequest\x12M\n" + + "\vtype_filter\x18\x01 \x03(\x0e2,.pluggableharness.agent.memory.v1.MemoryTypeR\n" + + "typeFilter\x12P\n" + + "\fscope_filter\x18\x02 \x03(\x0e2-.pluggableharness.agent.memory.v1.MemoryScopeR\vscopeFilter\x12X\n" + + "\rstatus_filter\x18\x03 \x01(\x0e2..pluggableharness.agent.memory.v1.RecordStatusH\x00R\fstatusFilter\x88\x01\x01\x12\x1b\n" + + "\tpage_size\x18\x04 \x01(\x05R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x05 \x01(\tR\tpageTokenB\x10\n" + + "\x0e_status_filter\"\x87\x01\n" + + "\x13ListRecordsResponse\x12H\n" + + "\arecords\x18\x01 \x03(\v2..pluggableharness.agent.memory.v1.MemoryRecordR\arecords\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\"\n" + + "\x10GetRecordRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\"[\n" + + "\x11GetRecordResponse\x12F\n" + + "\x06record\x18\x01 \x01(\v2..pluggableharness.agent.memory.v1.MemoryRecordR\x06record\"\x11\n" + + "\x0fDescribeRequest\"]\n" + + "\x10DescribeResponse\x12I\n" + + "\bproducer\x18\x01 \x01(\v2-.pluggableharness.agent.common.v1.ProducerRefR\bproducer*\x8d\x01\n" + "\n" + "MemoryType\x12\x1b\n" + "\x17MEMORY_TYPE_UNSPECIFIED\x10\x00\x12\x14\n" + @@ -1789,7 +2289,7 @@ const file_pluggableharness_agent_memory_v1_memory_proto_rawDesc = "" + "\fRecordStatus\x12\x1d\n" + "\x19RECORD_STATUS_UNSPECIFIED\x10\x00\x12\x1b\n" + "\x17RECORD_STATUS_CANONICAL\x10\x01\x12\x19\n" + - "\x15RECORD_STATUS_PENDING\x10\x02*\xb9\x02\n" + + "\x15RECORD_STATUS_PENDING\x10\x02*\xe2\x02\n" + "\x13MemoryErrorCategory\x12%\n" + "!MEMORY_ERROR_CATEGORY_UNSPECIFIED\x10\x00\x12#\n" + "\x1fMEMORY_ERROR_CATEGORY_NOT_FOUND\x10\x01\x12&\n" + @@ -1797,7 +2297,8 @@ const file_pluggableharness_agent_memory_v1_memory_proto_rawDesc = "" + ".MEMORY_ERROR_CATEGORY_RATIFICATION_UNSUPPORTED\x10\x03\x12)\n" + "%MEMORY_ERROR_CATEGORY_BUDGET_EXCEEDED\x10\x04\x12,\n" + "(MEMORY_ERROR_CATEGORY_SOURCE_UNAVAILABLE\x10\x05\x12!\n" + - "\x1dMEMORY_ERROR_CATEGORY_UNKNOWN\x10\x062\xd5\b\n" + + "\x1dMEMORY_ERROR_CATEGORY_UNKNOWN\x10\x06\x12'\n" + + "#MEMORY_ERROR_CATEGORY_INVALID_SCOPE\x10\a2\xba\v\n" + "\rMemoryService\x12\x86\x01\n" + "\x0fGetCapabilities\x128.pluggableharness.agent.memory.v1.GetCapabilitiesRequest\x1a9.pluggableharness.agent.memory.v1.GetCapabilitiesResponse\x12t\n" + "\tConfigure\x122.pluggableharness.agent.memory.v1.ConfigureRequest\x1a3.pluggableharness.agent.memory.v1.ConfigureResponse\x12k\n" + @@ -1807,7 +2308,10 @@ const file_pluggableharness_agent_memory_v1_memory_proto_rawDesc = "" + "\fDeleteRecord\x125.pluggableharness.agent.memory.v1.DeleteRecordRequest\x1a6.pluggableharness.agent.memory.v1.DeleteRecordResponse\x12\x80\x01\n" + "\rApproveRecord\x126.pluggableharness.agent.memory.v1.ApproveRecordRequest\x1a7.pluggableharness.agent.memory.v1.ApproveRecordResponse\x12}\n" + "\fRejectRecord\x125.pluggableharness.agent.memory.v1.RejectRecordRequest\x1a6.pluggableharness.agent.memory.v1.RejectRecordResponse\x12k\n" + - "\x06Render\x12/.pluggableharness.agent.memory.v1.RenderRequest\x1a0.pluggableharness.agent.memory.v1.RenderResponseB@Z>github.com/pluggableharness/agent/pkg/memory/proto/v1;memoryv1b\x06proto3" + "\x06Render\x12/.pluggableharness.agent.memory.v1.RenderRequest\x1a0.pluggableharness.agent.memory.v1.RenderResponse\x12z\n" + + "\vListRecords\x124.pluggableharness.agent.memory.v1.ListRecordsRequest\x1a5.pluggableharness.agent.memory.v1.ListRecordsResponse\x12t\n" + + "\tGetRecord\x122.pluggableharness.agent.memory.v1.GetRecordRequest\x1a3.pluggableharness.agent.memory.v1.GetRecordResponse\x12q\n" + + "\bDescribe\x121.pluggableharness.agent.memory.v1.DescribeRequest\x1a2.pluggableharness.agent.memory.v1.DescribeResponseB@Z>github.com/pluggableharness/agent/pkg/memory/proto/v1;memoryv1b\x06proto3" var ( file_pluggableharness_agent_memory_v1_memory_proto_rawDescOnce sync.Once @@ -1822,7 +2326,7 @@ func file_pluggableharness_agent_memory_v1_memory_proto_rawDescGZIP() []byte { } var file_pluggableharness_agent_memory_v1_memory_proto_enumTypes = make([]protoimpl.EnumInfo, 4) -var file_pluggableharness_agent_memory_v1_memory_proto_msgTypes = make([]protoimpl.MessageInfo, 23) +var file_pluggableharness_agent_memory_v1_memory_proto_msgTypes = make([]protoimpl.MessageInfo, 30) var file_pluggableharness_agent_memory_v1_memory_proto_goTypes = []any{ (MemoryType)(0), // 0: pluggableharness.agent.memory.v1.MemoryType (MemoryScope)(0), // 1: pluggableharness.agent.memory.v1.MemoryScope @@ -1836,81 +2340,104 @@ var file_pluggableharness_agent_memory_v1_memory_proto_goTypes = []any{ (*RecallRequest)(nil), // 9: pluggableharness.agent.memory.v1.RecallRequest (*RecallResponse)(nil), // 10: pluggableharness.agent.memory.v1.RecallResponse (*MemoryRecord)(nil), // 11: pluggableharness.agent.memory.v1.MemoryRecord - (*RecordRequest)(nil), // 12: pluggableharness.agent.memory.v1.RecordRequest - (*RecordResult)(nil), // 13: pluggableharness.agent.memory.v1.RecordResult - (*RecordResponse)(nil), // 14: pluggableharness.agent.memory.v1.RecordResponse - (*UpdateRecordRequest)(nil), // 15: pluggableharness.agent.memory.v1.UpdateRecordRequest - (*UpdateRecordResponse)(nil), // 16: pluggableharness.agent.memory.v1.UpdateRecordResponse - (*DeleteRecordRequest)(nil), // 17: pluggableharness.agent.memory.v1.DeleteRecordRequest - (*DeleteResult)(nil), // 18: pluggableharness.agent.memory.v1.DeleteResult - (*DeleteRecordResponse)(nil), // 19: pluggableharness.agent.memory.v1.DeleteRecordResponse - (*ApproveRecordRequest)(nil), // 20: pluggableharness.agent.memory.v1.ApproveRecordRequest - (*ApproveRecordResponse)(nil), // 21: pluggableharness.agent.memory.v1.ApproveRecordResponse - (*RejectRecordRequest)(nil), // 22: pluggableharness.agent.memory.v1.RejectRecordRequest - (*RejectRecordResponse)(nil), // 23: pluggableharness.agent.memory.v1.RejectRecordResponse - (*MemoryError)(nil), // 24: pluggableharness.agent.memory.v1.MemoryError - (*RenderRequest)(nil), // 25: pluggableharness.agent.memory.v1.RenderRequest - (*RenderResponse)(nil), // 26: pluggableharness.agent.memory.v1.RenderResponse - (*v1.SlashCommandSpec)(nil), // 27: pluggableharness.agent.slashcommand.v1.SlashCommandSpec - (*v11.ConfigSchema)(nil), // 28: pluggableharness.agent.config.v1.ConfigSchema - (*structpb.Struct)(nil), // 29: google.protobuf.Struct - (*v12.ModelTarget)(nil), // 30: pluggableharness.agent.model.v1.ModelTarget - (*v13.ContentBlock)(nil), // 31: pluggableharness.agent.content.v1.ContentBlock - (*timestamppb.Timestamp)(nil), // 32: google.protobuf.Timestamp - (*v14.RenderTree)(nil), // 33: pluggableharness.agent.render.v1.RenderTree + (*Provenance)(nil), // 12: pluggableharness.agent.memory.v1.Provenance + (*RecordRequest)(nil), // 13: pluggableharness.agent.memory.v1.RecordRequest + (*RecordResult)(nil), // 14: pluggableharness.agent.memory.v1.RecordResult + (*RecordResponse)(nil), // 15: pluggableharness.agent.memory.v1.RecordResponse + (*UpdateRecordRequest)(nil), // 16: pluggableharness.agent.memory.v1.UpdateRecordRequest + (*UpdateRecordResponse)(nil), // 17: pluggableharness.agent.memory.v1.UpdateRecordResponse + (*DeleteRecordRequest)(nil), // 18: pluggableharness.agent.memory.v1.DeleteRecordRequest + (*DeleteResult)(nil), // 19: pluggableharness.agent.memory.v1.DeleteResult + (*DeleteRecordResponse)(nil), // 20: pluggableharness.agent.memory.v1.DeleteRecordResponse + (*ApproveRecordRequest)(nil), // 21: pluggableharness.agent.memory.v1.ApproveRecordRequest + (*ApproveRecordResponse)(nil), // 22: pluggableharness.agent.memory.v1.ApproveRecordResponse + (*RejectRecordRequest)(nil), // 23: pluggableharness.agent.memory.v1.RejectRecordRequest + (*RejectRecordResponse)(nil), // 24: pluggableharness.agent.memory.v1.RejectRecordResponse + (*MemoryError)(nil), // 25: pluggableharness.agent.memory.v1.MemoryError + (*RenderRequest)(nil), // 26: pluggableharness.agent.memory.v1.RenderRequest + (*RenderResponse)(nil), // 27: pluggableharness.agent.memory.v1.RenderResponse + (*ListRecordsRequest)(nil), // 28: pluggableharness.agent.memory.v1.ListRecordsRequest + (*ListRecordsResponse)(nil), // 29: pluggableharness.agent.memory.v1.ListRecordsResponse + (*GetRecordRequest)(nil), // 30: pluggableharness.agent.memory.v1.GetRecordRequest + (*GetRecordResponse)(nil), // 31: pluggableharness.agent.memory.v1.GetRecordResponse + (*DescribeRequest)(nil), // 32: pluggableharness.agent.memory.v1.DescribeRequest + (*DescribeResponse)(nil), // 33: pluggableharness.agent.memory.v1.DescribeResponse + (*v1.SlashCommandSpec)(nil), // 34: pluggableharness.agent.slashcommand.v1.SlashCommandSpec + (*v11.ConfigSchema)(nil), // 35: pluggableharness.agent.config.v1.ConfigSchema + (v12.HookPoint)(0), // 36: pluggableharness.agent.common.v1.HookPoint + (*structpb.Struct)(nil), // 37: google.protobuf.Struct + (*v13.ModelTarget)(nil), // 38: pluggableharness.agent.model.v1.ModelTarget + (*v14.ContentBlock)(nil), // 39: pluggableharness.agent.content.v1.ContentBlock + (*timestamppb.Timestamp)(nil), // 40: google.protobuf.Timestamp + (*v15.RenderTree)(nil), // 41: pluggableharness.agent.render.v1.RenderTree + (*v12.ProducerRef)(nil), // 42: pluggableharness.agent.common.v1.ProducerRef } var file_pluggableharness_agent_memory_v1_memory_proto_depIdxs = []int32{ 0, // 0: pluggableharness.agent.memory.v1.MemoryCapabilities.supported_types:type_name -> pluggableharness.agent.memory.v1.MemoryType 1, // 1: pluggableharness.agent.memory.v1.MemoryCapabilities.supported_scopes:type_name -> pluggableharness.agent.memory.v1.MemoryScope - 27, // 2: pluggableharness.agent.memory.v1.MemoryCapabilities.slash_commands:type_name -> pluggableharness.agent.slashcommand.v1.SlashCommandSpec - 28, // 3: pluggableharness.agent.memory.v1.MemoryCapabilities.config_schema:type_name -> pluggableharness.agent.config.v1.ConfigSchema - 5, // 4: pluggableharness.agent.memory.v1.GetCapabilitiesResponse.capabilities:type_name -> pluggableharness.agent.memory.v1.MemoryCapabilities - 29, // 5: pluggableharness.agent.memory.v1.ConfigureRequest.config:type_name -> google.protobuf.Struct - 30, // 6: pluggableharness.agent.memory.v1.RecallRequest.model_target:type_name -> pluggableharness.agent.model.v1.ModelTarget - 0, // 7: pluggableharness.agent.memory.v1.RecallRequest.type_filter:type_name -> pluggableharness.agent.memory.v1.MemoryType - 1, // 8: pluggableharness.agent.memory.v1.RecallRequest.scope_filter:type_name -> pluggableharness.agent.memory.v1.MemoryScope - 11, // 9: pluggableharness.agent.memory.v1.RecallResponse.records:type_name -> pluggableharness.agent.memory.v1.MemoryRecord - 0, // 10: pluggableharness.agent.memory.v1.MemoryRecord.type:type_name -> pluggableharness.agent.memory.v1.MemoryType - 1, // 11: pluggableharness.agent.memory.v1.MemoryRecord.scope:type_name -> pluggableharness.agent.memory.v1.MemoryScope - 31, // 12: pluggableharness.agent.memory.v1.MemoryRecord.content:type_name -> pluggableharness.agent.content.v1.ContentBlock - 2, // 13: pluggableharness.agent.memory.v1.MemoryRecord.status:type_name -> pluggableharness.agent.memory.v1.RecordStatus - 32, // 14: pluggableharness.agent.memory.v1.MemoryRecord.created_at:type_name -> google.protobuf.Timestamp - 32, // 15: pluggableharness.agent.memory.v1.MemoryRecord.updated_at:type_name -> google.protobuf.Timestamp - 0, // 16: pluggableharness.agent.memory.v1.RecordRequest.type:type_name -> pluggableharness.agent.memory.v1.MemoryType - 1, // 17: pluggableharness.agent.memory.v1.RecordRequest.scope:type_name -> pluggableharness.agent.memory.v1.MemoryScope - 31, // 18: pluggableharness.agent.memory.v1.RecordRequest.content:type_name -> pluggableharness.agent.content.v1.ContentBlock - 2, // 19: pluggableharness.agent.memory.v1.RecordResult.status:type_name -> pluggableharness.agent.memory.v1.RecordStatus - 13, // 20: pluggableharness.agent.memory.v1.RecordResponse.result:type_name -> pluggableharness.agent.memory.v1.RecordResult - 31, // 21: pluggableharness.agent.memory.v1.UpdateRecordRequest.content:type_name -> pluggableharness.agent.content.v1.ContentBlock - 13, // 22: pluggableharness.agent.memory.v1.UpdateRecordResponse.result:type_name -> pluggableharness.agent.memory.v1.RecordResult - 18, // 23: pluggableharness.agent.memory.v1.DeleteRecordResponse.result:type_name -> pluggableharness.agent.memory.v1.DeleteResult - 13, // 24: pluggableharness.agent.memory.v1.ApproveRecordResponse.result:type_name -> pluggableharness.agent.memory.v1.RecordResult - 18, // 25: pluggableharness.agent.memory.v1.RejectRecordResponse.result:type_name -> pluggableharness.agent.memory.v1.DeleteResult - 3, // 26: pluggableharness.agent.memory.v1.MemoryError.category:type_name -> pluggableharness.agent.memory.v1.MemoryErrorCategory - 33, // 27: pluggableharness.agent.memory.v1.RenderResponse.tree:type_name -> pluggableharness.agent.render.v1.RenderTree - 4, // 28: pluggableharness.agent.memory.v1.MemoryService.GetCapabilities:input_type -> pluggableharness.agent.memory.v1.GetCapabilitiesRequest - 7, // 29: pluggableharness.agent.memory.v1.MemoryService.Configure:input_type -> pluggableharness.agent.memory.v1.ConfigureRequest - 9, // 30: pluggableharness.agent.memory.v1.MemoryService.Recall:input_type -> pluggableharness.agent.memory.v1.RecallRequest - 12, // 31: pluggableharness.agent.memory.v1.MemoryService.Record:input_type -> pluggableharness.agent.memory.v1.RecordRequest - 15, // 32: pluggableharness.agent.memory.v1.MemoryService.UpdateRecord:input_type -> pluggableharness.agent.memory.v1.UpdateRecordRequest - 17, // 33: pluggableharness.agent.memory.v1.MemoryService.DeleteRecord:input_type -> pluggableharness.agent.memory.v1.DeleteRecordRequest - 20, // 34: pluggableharness.agent.memory.v1.MemoryService.ApproveRecord:input_type -> pluggableharness.agent.memory.v1.ApproveRecordRequest - 22, // 35: pluggableharness.agent.memory.v1.MemoryService.RejectRecord:input_type -> pluggableharness.agent.memory.v1.RejectRecordRequest - 25, // 36: pluggableharness.agent.memory.v1.MemoryService.Render:input_type -> pluggableharness.agent.memory.v1.RenderRequest - 6, // 37: pluggableharness.agent.memory.v1.MemoryService.GetCapabilities:output_type -> pluggableharness.agent.memory.v1.GetCapabilitiesResponse - 8, // 38: pluggableharness.agent.memory.v1.MemoryService.Configure:output_type -> pluggableharness.agent.memory.v1.ConfigureResponse - 10, // 39: pluggableharness.agent.memory.v1.MemoryService.Recall:output_type -> pluggableharness.agent.memory.v1.RecallResponse - 14, // 40: pluggableharness.agent.memory.v1.MemoryService.Record:output_type -> pluggableharness.agent.memory.v1.RecordResponse - 16, // 41: pluggableharness.agent.memory.v1.MemoryService.UpdateRecord:output_type -> pluggableharness.agent.memory.v1.UpdateRecordResponse - 19, // 42: pluggableharness.agent.memory.v1.MemoryService.DeleteRecord:output_type -> pluggableharness.agent.memory.v1.DeleteRecordResponse - 21, // 43: pluggableharness.agent.memory.v1.MemoryService.ApproveRecord:output_type -> pluggableharness.agent.memory.v1.ApproveRecordResponse - 23, // 44: pluggableharness.agent.memory.v1.MemoryService.RejectRecord:output_type -> pluggableharness.agent.memory.v1.RejectRecordResponse - 26, // 45: pluggableharness.agent.memory.v1.MemoryService.Render:output_type -> pluggableharness.agent.memory.v1.RenderResponse - 37, // [37:46] is the sub-list for method output_type - 28, // [28:37] is the sub-list for method input_type - 28, // [28:28] is the sub-list for extension type_name - 28, // [28:28] is the sub-list for extension extendee - 0, // [0:28] is the sub-list for field type_name + 34, // 2: pluggableharness.agent.memory.v1.MemoryCapabilities.slash_commands:type_name -> pluggableharness.agent.slashcommand.v1.SlashCommandSpec + 35, // 3: pluggableharness.agent.memory.v1.MemoryCapabilities.config_schema:type_name -> pluggableharness.agent.config.v1.ConfigSchema + 36, // 4: pluggableharness.agent.memory.v1.MemoryCapabilities.supported_hook_points:type_name -> pluggableharness.agent.common.v1.HookPoint + 5, // 5: pluggableharness.agent.memory.v1.GetCapabilitiesResponse.capabilities:type_name -> pluggableharness.agent.memory.v1.MemoryCapabilities + 37, // 6: pluggableharness.agent.memory.v1.ConfigureRequest.config:type_name -> google.protobuf.Struct + 38, // 7: pluggableharness.agent.memory.v1.RecallRequest.model_target:type_name -> pluggableharness.agent.model.v1.ModelTarget + 0, // 8: pluggableharness.agent.memory.v1.RecallRequest.type_filter:type_name -> pluggableharness.agent.memory.v1.MemoryType + 1, // 9: pluggableharness.agent.memory.v1.RecallRequest.scope_filter:type_name -> pluggableharness.agent.memory.v1.MemoryScope + 11, // 10: pluggableharness.agent.memory.v1.RecallResponse.records:type_name -> pluggableharness.agent.memory.v1.MemoryRecord + 0, // 11: pluggableharness.agent.memory.v1.MemoryRecord.type:type_name -> pluggableharness.agent.memory.v1.MemoryType + 1, // 12: pluggableharness.agent.memory.v1.MemoryRecord.scope:type_name -> pluggableharness.agent.memory.v1.MemoryScope + 39, // 13: pluggableharness.agent.memory.v1.MemoryRecord.content:type_name -> pluggableharness.agent.content.v1.ContentBlock + 2, // 14: pluggableharness.agent.memory.v1.MemoryRecord.status:type_name -> pluggableharness.agent.memory.v1.RecordStatus + 40, // 15: pluggableharness.agent.memory.v1.MemoryRecord.created_at:type_name -> google.protobuf.Timestamp + 40, // 16: pluggableharness.agent.memory.v1.MemoryRecord.updated_at:type_name -> google.protobuf.Timestamp + 12, // 17: pluggableharness.agent.memory.v1.MemoryRecord.provenance:type_name -> pluggableharness.agent.memory.v1.Provenance + 0, // 18: pluggableharness.agent.memory.v1.RecordRequest.type:type_name -> pluggableharness.agent.memory.v1.MemoryType + 1, // 19: pluggableharness.agent.memory.v1.RecordRequest.scope:type_name -> pluggableharness.agent.memory.v1.MemoryScope + 39, // 20: pluggableharness.agent.memory.v1.RecordRequest.content:type_name -> pluggableharness.agent.content.v1.ContentBlock + 2, // 21: pluggableharness.agent.memory.v1.RecordResult.status:type_name -> pluggableharness.agent.memory.v1.RecordStatus + 14, // 22: pluggableharness.agent.memory.v1.RecordResponse.result:type_name -> pluggableharness.agent.memory.v1.RecordResult + 39, // 23: pluggableharness.agent.memory.v1.UpdateRecordRequest.content:type_name -> pluggableharness.agent.content.v1.ContentBlock + 14, // 24: pluggableharness.agent.memory.v1.UpdateRecordResponse.result:type_name -> pluggableharness.agent.memory.v1.RecordResult + 19, // 25: pluggableharness.agent.memory.v1.DeleteRecordResponse.result:type_name -> pluggableharness.agent.memory.v1.DeleteResult + 14, // 26: pluggableharness.agent.memory.v1.ApproveRecordResponse.result:type_name -> pluggableharness.agent.memory.v1.RecordResult + 19, // 27: pluggableharness.agent.memory.v1.RejectRecordResponse.result:type_name -> pluggableharness.agent.memory.v1.DeleteResult + 3, // 28: pluggableharness.agent.memory.v1.MemoryError.category:type_name -> pluggableharness.agent.memory.v1.MemoryErrorCategory + 41, // 29: pluggableharness.agent.memory.v1.RenderResponse.tree:type_name -> pluggableharness.agent.render.v1.RenderTree + 0, // 30: pluggableharness.agent.memory.v1.ListRecordsRequest.type_filter:type_name -> pluggableharness.agent.memory.v1.MemoryType + 1, // 31: pluggableharness.agent.memory.v1.ListRecordsRequest.scope_filter:type_name -> pluggableharness.agent.memory.v1.MemoryScope + 2, // 32: pluggableharness.agent.memory.v1.ListRecordsRequest.status_filter:type_name -> pluggableharness.agent.memory.v1.RecordStatus + 11, // 33: pluggableharness.agent.memory.v1.ListRecordsResponse.records:type_name -> pluggableharness.agent.memory.v1.MemoryRecord + 11, // 34: pluggableharness.agent.memory.v1.GetRecordResponse.record:type_name -> pluggableharness.agent.memory.v1.MemoryRecord + 42, // 35: pluggableharness.agent.memory.v1.DescribeResponse.producer:type_name -> pluggableharness.agent.common.v1.ProducerRef + 4, // 36: pluggableharness.agent.memory.v1.MemoryService.GetCapabilities:input_type -> pluggableharness.agent.memory.v1.GetCapabilitiesRequest + 7, // 37: pluggableharness.agent.memory.v1.MemoryService.Configure:input_type -> pluggableharness.agent.memory.v1.ConfigureRequest + 9, // 38: pluggableharness.agent.memory.v1.MemoryService.Recall:input_type -> pluggableharness.agent.memory.v1.RecallRequest + 13, // 39: pluggableharness.agent.memory.v1.MemoryService.Record:input_type -> pluggableharness.agent.memory.v1.RecordRequest + 16, // 40: pluggableharness.agent.memory.v1.MemoryService.UpdateRecord:input_type -> pluggableharness.agent.memory.v1.UpdateRecordRequest + 18, // 41: pluggableharness.agent.memory.v1.MemoryService.DeleteRecord:input_type -> pluggableharness.agent.memory.v1.DeleteRecordRequest + 21, // 42: pluggableharness.agent.memory.v1.MemoryService.ApproveRecord:input_type -> pluggableharness.agent.memory.v1.ApproveRecordRequest + 23, // 43: pluggableharness.agent.memory.v1.MemoryService.RejectRecord:input_type -> pluggableharness.agent.memory.v1.RejectRecordRequest + 26, // 44: pluggableharness.agent.memory.v1.MemoryService.Render:input_type -> pluggableharness.agent.memory.v1.RenderRequest + 28, // 45: pluggableharness.agent.memory.v1.MemoryService.ListRecords:input_type -> pluggableharness.agent.memory.v1.ListRecordsRequest + 30, // 46: pluggableharness.agent.memory.v1.MemoryService.GetRecord:input_type -> pluggableharness.agent.memory.v1.GetRecordRequest + 32, // 47: pluggableharness.agent.memory.v1.MemoryService.Describe:input_type -> pluggableharness.agent.memory.v1.DescribeRequest + 6, // 48: pluggableharness.agent.memory.v1.MemoryService.GetCapabilities:output_type -> pluggableharness.agent.memory.v1.GetCapabilitiesResponse + 8, // 49: pluggableharness.agent.memory.v1.MemoryService.Configure:output_type -> pluggableharness.agent.memory.v1.ConfigureResponse + 10, // 50: pluggableharness.agent.memory.v1.MemoryService.Recall:output_type -> pluggableharness.agent.memory.v1.RecallResponse + 15, // 51: pluggableharness.agent.memory.v1.MemoryService.Record:output_type -> pluggableharness.agent.memory.v1.RecordResponse + 17, // 52: pluggableharness.agent.memory.v1.MemoryService.UpdateRecord:output_type -> pluggableharness.agent.memory.v1.UpdateRecordResponse + 20, // 53: pluggableharness.agent.memory.v1.MemoryService.DeleteRecord:output_type -> pluggableharness.agent.memory.v1.DeleteRecordResponse + 22, // 54: pluggableharness.agent.memory.v1.MemoryService.ApproveRecord:output_type -> pluggableharness.agent.memory.v1.ApproveRecordResponse + 24, // 55: pluggableharness.agent.memory.v1.MemoryService.RejectRecord:output_type -> pluggableharness.agent.memory.v1.RejectRecordResponse + 27, // 56: pluggableharness.agent.memory.v1.MemoryService.Render:output_type -> pluggableharness.agent.memory.v1.RenderResponse + 29, // 57: pluggableharness.agent.memory.v1.MemoryService.ListRecords:output_type -> pluggableharness.agent.memory.v1.ListRecordsResponse + 31, // 58: pluggableharness.agent.memory.v1.MemoryService.GetRecord:output_type -> pluggableharness.agent.memory.v1.GetRecordResponse + 33, // 59: pluggableharness.agent.memory.v1.MemoryService.Describe:output_type -> pluggableharness.agent.memory.v1.DescribeResponse + 48, // [48:60] is the sub-list for method output_type + 36, // [36:48] is the sub-list for method input_type + 36, // [36:36] is the sub-list for extension type_name + 36, // [36:36] is the sub-list for extension extendee + 0, // [0:36] is the sub-list for field type_name } func init() { file_pluggableharness_agent_memory_v1_memory_proto_init() } @@ -1918,15 +2445,18 @@ func file_pluggableharness_agent_memory_v1_memory_proto_init() { if File_pluggableharness_agent_memory_v1_memory_proto != nil { return } + file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[7].OneofWrappers = []any{} file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[8].OneofWrappers = []any{} - file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[11].OneofWrappers = []any{} + file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[9].OneofWrappers = []any{} + file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[12].OneofWrappers = []any{} + file_pluggableharness_agent_memory_v1_memory_proto_msgTypes[24].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_pluggableharness_agent_memory_v1_memory_proto_rawDesc), len(file_pluggableharness_agent_memory_v1_memory_proto_rawDesc)), NumEnums: 4, - NumMessages: 23, + NumMessages: 30, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/memory/proto/v1/memory_grpc.pb.go b/pkg/memory/proto/v1/memory_grpc.pb.go index 94a05bf..6816a05 100644 --- a/pkg/memory/proto/v1/memory_grpc.pb.go +++ b/pkg/memory/proto/v1/memory_grpc.pb.go @@ -37,6 +37,9 @@ const ( MemoryService_ApproveRecord_FullMethodName = "/pluggableharness.agent.memory.v1.MemoryService/ApproveRecord" MemoryService_RejectRecord_FullMethodName = "/pluggableharness.agent.memory.v1.MemoryService/RejectRecord" MemoryService_Render_FullMethodName = "/pluggableharness.agent.memory.v1.MemoryService/Render" + MemoryService_ListRecords_FullMethodName = "/pluggableharness.agent.memory.v1.MemoryService/ListRecords" + MemoryService_GetRecord_FullMethodName = "/pluggableharness.agent.memory.v1.MemoryService/GetRecord" + MemoryService_Describe_FullMethodName = "/pluggableharness.agent.memory.v1.MemoryService/Describe" ) // MemoryServiceClient is the client API for MemoryService service. @@ -80,6 +83,27 @@ type MemoryServiceClient interface { // review-inbox view distinct from ordinary recall), in place of the // kernel's generic fallback. MAY be implemented. Unary. memory.md §10. Render(ctx context.Context, in *RenderRequest, opts ...grpc.CallOption) (*RenderResponse, error) + // ListRecords is the enumeration/audit path: paginated browsing of this + // provider's records, filterable by type/scope/status. Unlike Recall, + // PENDING records ARE listable here without any include_pending-style + // gate — this is the review-inbox path (e.g. a ratification review UI), + // not the budget-constrained per-turn recall path. MUST be implemented; + // cheap for any real backend. memory.md §7. + ListRecords(ctx context.Context, in *ListRecordsRequest, opts ...grpc.CallOption) (*ListRecordsResponse, error) + // GetRecord fetches exactly one record by id. MUST fail with a + // structured MemoryError (category NOT_FOUND) if `id` doesn't match an + // existing record, rather than returning an empty result. MUST be + // implemented; cheap for any real backend. memory.md §7. + GetRecord(ctx context.Context, in *GetRecordRequest, opts ...grpc.CallOption) (*GetRecordResponse, error) + // Describe reports this plugin build's own identity — {name, version, + // source, category, protocol_version} via ProducerRef — independent of + // any lock-file entry. This is the mechanism a dev_overrides-resolved + // binary (which has no provider {} lock entry to read identity from) + // uses to self-report at connection time; see + // docs/specifications/configuration/lock-file.md's dev_overrides note, + // which is the canonical explanation for this RPC across every plugin + // category that gains it in this protocol revision. + Describe(ctx context.Context, in *DescribeRequest, opts ...grpc.CallOption) (*DescribeResponse, error) } type memoryServiceClient struct { @@ -180,6 +204,36 @@ func (c *memoryServiceClient) Render(ctx context.Context, in *RenderRequest, opt return out, nil } +func (c *memoryServiceClient) ListRecords(ctx context.Context, in *ListRecordsRequest, opts ...grpc.CallOption) (*ListRecordsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListRecordsResponse) + err := c.cc.Invoke(ctx, MemoryService_ListRecords_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *memoryServiceClient) GetRecord(ctx context.Context, in *GetRecordRequest, opts ...grpc.CallOption) (*GetRecordResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetRecordResponse) + err := c.cc.Invoke(ctx, MemoryService_GetRecord_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *memoryServiceClient) Describe(ctx context.Context, in *DescribeRequest, opts ...grpc.CallOption) (*DescribeResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DescribeResponse) + err := c.cc.Invoke(ctx, MemoryService_Describe_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // MemoryServiceServer is the server API for MemoryService service. // All implementations must embed UnimplementedMemoryServiceServer // for forward compatibility. @@ -221,6 +275,27 @@ type MemoryServiceServer interface { // review-inbox view distinct from ordinary recall), in place of the // kernel's generic fallback. MAY be implemented. Unary. memory.md §10. Render(context.Context, *RenderRequest) (*RenderResponse, error) + // ListRecords is the enumeration/audit path: paginated browsing of this + // provider's records, filterable by type/scope/status. Unlike Recall, + // PENDING records ARE listable here without any include_pending-style + // gate — this is the review-inbox path (e.g. a ratification review UI), + // not the budget-constrained per-turn recall path. MUST be implemented; + // cheap for any real backend. memory.md §7. + ListRecords(context.Context, *ListRecordsRequest) (*ListRecordsResponse, error) + // GetRecord fetches exactly one record by id. MUST fail with a + // structured MemoryError (category NOT_FOUND) if `id` doesn't match an + // existing record, rather than returning an empty result. MUST be + // implemented; cheap for any real backend. memory.md §7. + GetRecord(context.Context, *GetRecordRequest) (*GetRecordResponse, error) + // Describe reports this plugin build's own identity — {name, version, + // source, category, protocol_version} via ProducerRef — independent of + // any lock-file entry. This is the mechanism a dev_overrides-resolved + // binary (which has no provider {} lock entry to read identity from) + // uses to self-report at connection time; see + // docs/specifications/configuration/lock-file.md's dev_overrides note, + // which is the canonical explanation for this RPC across every plugin + // category that gains it in this protocol revision. + Describe(context.Context, *DescribeRequest) (*DescribeResponse, error) mustEmbedUnimplementedMemoryServiceServer() } @@ -258,6 +333,15 @@ func (UnimplementedMemoryServiceServer) RejectRecord(context.Context, *RejectRec func (UnimplementedMemoryServiceServer) Render(context.Context, *RenderRequest) (*RenderResponse, error) { return nil, status.Error(codes.Unimplemented, "method Render not implemented") } +func (UnimplementedMemoryServiceServer) ListRecords(context.Context, *ListRecordsRequest) (*ListRecordsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListRecords not implemented") +} +func (UnimplementedMemoryServiceServer) GetRecord(context.Context, *GetRecordRequest) (*GetRecordResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetRecord not implemented") +} +func (UnimplementedMemoryServiceServer) Describe(context.Context, *DescribeRequest) (*DescribeResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Describe not implemented") +} func (UnimplementedMemoryServiceServer) mustEmbedUnimplementedMemoryServiceServer() {} func (UnimplementedMemoryServiceServer) testEmbeddedByValue() {} @@ -441,6 +525,60 @@ func _MemoryService_Render_Handler(srv interface{}, ctx context.Context, dec fun return interceptor(ctx, in, info, handler) } +func _MemoryService_ListRecords_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListRecordsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MemoryServiceServer).ListRecords(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MemoryService_ListRecords_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MemoryServiceServer).ListRecords(ctx, req.(*ListRecordsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _MemoryService_GetRecord_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetRecordRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MemoryServiceServer).GetRecord(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MemoryService_GetRecord_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MemoryServiceServer).GetRecord(ctx, req.(*GetRecordRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _MemoryService_Describe_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DescribeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MemoryServiceServer).Describe(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MemoryService_Describe_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MemoryServiceServer).Describe(ctx, req.(*DescribeRequest)) + } + return interceptor(ctx, in, info, handler) +} + // MemoryService_ServiceDesc is the grpc.ServiceDesc for MemoryService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -484,6 +622,18 @@ var MemoryService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Render", Handler: _MemoryService_Render_Handler, }, + { + MethodName: "ListRecords", + Handler: _MemoryService_ListRecords_Handler, + }, + { + MethodName: "GetRecord", + Handler: _MemoryService_GetRecord_Handler, + }, + { + MethodName: "Describe", + Handler: _MemoryService_Describe_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "pluggableharness/agent/memory/v1/memory.proto", diff --git a/pkg/model/proto/v1/model.pb.go b/pkg/model/proto/v1/model.pb.go index c1830e3..68c6035 100644 --- a/pkg/model/proto/v1/model.pb.go +++ b/pkg/model/proto/v1/model.pb.go @@ -17,10 +17,11 @@ package modelv1 import ( + v12 "github.com/pluggableharness/agent/pkg/common/proto/v1" v11 "github.com/pluggableharness/agent/pkg/config/proto/v1" - v12 "github.com/pluggableharness/agent/pkg/content/proto/v1" - v14 "github.com/pluggableharness/agent/pkg/render/proto/v1" - v13 "github.com/pluggableharness/agent/pkg/schema/proto/v1" + v13 "github.com/pluggableharness/agent/pkg/content/proto/v1" + v15 "github.com/pluggableharness/agent/pkg/render/proto/v1" + v14 "github.com/pluggableharness/agent/pkg/schema/proto/v1" v1 "github.com/pluggableharness/agent/pkg/slashcommand/proto/v1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" @@ -170,6 +171,72 @@ func (CachingMode) EnumDescriptor() ([]byte, []int) { return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{1} } +// ToolChoiceMode enumerates the tool-invocation constraint shapes found +// across researched vendors, per the same "declare precisely, don't +// collapse to a bool" reasoning as ThinkingMode/CachingMode above. +type ToolChoiceMode int32 + +const ( + // Zero value. Never valid on a real ToolChoice; its presence on the + // wire means a caller forgot to set the field. + ToolChoiceMode_TOOL_CHOICE_MODE_UNSPECIFIED ToolChoiceMode = 0 + // The model decides freely whether and which tool to call. Equivalent + // to omitting GenerationParams.tool_choice entirely. + ToolChoiceMode_TOOL_CHOICE_MODE_AUTO ToolChoiceMode = 1 + // The model MUST call some tool this turn, but may pick which one. + ToolChoiceMode_TOOL_CHOICE_MODE_ANY ToolChoiceMode = 2 + // The model MUST NOT call any tool this turn, even if tools were + // declared. + ToolChoiceMode_TOOL_CHOICE_MODE_NONE ToolChoiceMode = 3 + // The model MUST call the specific tool named in ToolChoice.tool_name. + ToolChoiceMode_TOOL_CHOICE_MODE_SPECIFIC ToolChoiceMode = 4 +) + +// Enum value maps for ToolChoiceMode. +var ( + ToolChoiceMode_name = map[int32]string{ + 0: "TOOL_CHOICE_MODE_UNSPECIFIED", + 1: "TOOL_CHOICE_MODE_AUTO", + 2: "TOOL_CHOICE_MODE_ANY", + 3: "TOOL_CHOICE_MODE_NONE", + 4: "TOOL_CHOICE_MODE_SPECIFIC", + } + ToolChoiceMode_value = map[string]int32{ + "TOOL_CHOICE_MODE_UNSPECIFIED": 0, + "TOOL_CHOICE_MODE_AUTO": 1, + "TOOL_CHOICE_MODE_ANY": 2, + "TOOL_CHOICE_MODE_NONE": 3, + "TOOL_CHOICE_MODE_SPECIFIC": 4, + } +) + +func (x ToolChoiceMode) Enum() *ToolChoiceMode { + p := new(ToolChoiceMode) + *p = x + return p +} + +func (x ToolChoiceMode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ToolChoiceMode) Descriptor() protoreflect.EnumDescriptor { + return file_pluggableharness_agent_model_v1_model_proto_enumTypes[2].Descriptor() +} + +func (ToolChoiceMode) Type() protoreflect.EnumType { + return &file_pluggableharness_agent_model_v1_model_proto_enumTypes[2] +} + +func (x ToolChoiceMode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ToolChoiceMode.Descriptor instead. +func (ToolChoiceMode) EnumDescriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{2} +} + // StopReason classifies why a StreamCompletion ended, per model.md §4. type StopReason int32 @@ -189,6 +256,16 @@ const ( // abort). MUST be treated by the plugin as normal control flow, never // as an error (model.md §1, .claude/rules/grpc.md). StopReason_STOP_REASON_CANCELLED StopReason = 5 + // The model or vendor refused to continue generating — distinct from + // STOP_REASON_CONTENT_FILTERED, which is the vendor's automated content + // filter stopping generation; REFUSAL is the model itself declining + // (e.g. a safety-trained refusal message), a semantically different + // event even though both are policy-driven stops. + StopReason_STOP_REASON_REFUSAL StopReason = 6 + // The model stopped because it generated one of + // GenerationParams.stop_sequences. Stop.matched_stop_sequence carries + // which one. + StopReason_STOP_REASON_STOP_SEQUENCE StopReason = 7 ) // Enum value maps for StopReason. @@ -200,6 +277,8 @@ var ( 3: "STOP_REASON_MAX_TOKENS", 4: "STOP_REASON_CONTENT_FILTERED", 5: "STOP_REASON_CANCELLED", + 6: "STOP_REASON_REFUSAL", + 7: "STOP_REASON_STOP_SEQUENCE", } StopReason_value = map[string]int32{ "STOP_REASON_UNSPECIFIED": 0, @@ -208,6 +287,8 @@ var ( "STOP_REASON_MAX_TOKENS": 3, "STOP_REASON_CONTENT_FILTERED": 4, "STOP_REASON_CANCELLED": 5, + "STOP_REASON_REFUSAL": 6, + "STOP_REASON_STOP_SEQUENCE": 7, } ) @@ -222,11 +303,11 @@ func (x StopReason) String() string { } func (StopReason) Descriptor() protoreflect.EnumDescriptor { - return file_pluggableharness_agent_model_v1_model_proto_enumTypes[2].Descriptor() + return file_pluggableharness_agent_model_v1_model_proto_enumTypes[3].Descriptor() } func (StopReason) Type() protoreflect.EnumType { - return &file_pluggableharness_agent_model_v1_model_proto_enumTypes[2] + return &file_pluggableharness_agent_model_v1_model_proto_enumTypes[3] } func (x StopReason) Number() protoreflect.EnumNumber { @@ -235,7 +316,7 @@ func (x StopReason) Number() protoreflect.EnumNumber { // Deprecated: Use StopReason.Descriptor instead. func (StopReason) EnumDescriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{2} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{3} } // ModelErrorCategory classifies every StreamCompletion/Configure @@ -307,11 +388,11 @@ func (x ModelErrorCategory) String() string { } func (ModelErrorCategory) Descriptor() protoreflect.EnumDescriptor { - return file_pluggableharness_agent_model_v1_model_proto_enumTypes[3].Descriptor() + return file_pluggableharness_agent_model_v1_model_proto_enumTypes[4].Descriptor() } func (ModelErrorCategory) Type() protoreflect.EnumType { - return &file_pluggableharness_agent_model_v1_model_proto_enumTypes[3] + return &file_pluggableharness_agent_model_v1_model_proto_enumTypes[4] } func (x ModelErrorCategory) Number() protoreflect.EnumNumber { @@ -320,7 +401,7 @@ func (x ModelErrorCategory) Number() protoreflect.EnumNumber { // Deprecated: Use ModelErrorCategory.Descriptor instead. func (ModelErrorCategory) EnumDescriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{3} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{4} } // GetCapabilitiesRequest is empty: model.md §2 defines GetCapabilities @@ -422,9 +503,22 @@ type Capabilities struct { // The provider's agent.hcl config schema, returned alongside // capabilities so the kernel knows what fields Configure expects, per // configuration.md §4. - ConfigSchema *v11.ConfigSchema `protobuf:"bytes,3,opt,name=config_schema,json=configSchema,proto3" json:"config_schema,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + ConfigSchema *v11.ConfigSchema `protobuf:"bytes,3,opt,name=config_schema,json=configSchema,proto3" json:"config_schema,omitempty"` + // Which hook points (agent-loop/hook-dispatch.md) this plugin can serve + // via HookSubscriberService.DispatchHook. The kernel MUST reject an + // agent.hcl hook{} block naming a point not present here, at + // config-load time. + // + // Typed as common.v1.HookPoint, not hook.v1.HookPoint: hook.proto + // imports model.proto (for ModelRef/Usage on its PreModelCall/ + // PostModelResponse hook payloads), so model.proto importing hook.proto + // for this field would be a cyclic file dependency — confirmed via + // `buf build`, which rejects it outright ("detected cyclic import"). + // HookPoint itself lives in common.v1 for exactly this reason (see + // common.proto), already imported here for CallContext/Describe. + SupportedHookPoints []v12.HookPoint `protobuf:"varint,4,rep,packed,name=supported_hook_points,json=supportedHookPoints,proto3,enum=pluggableharness.agent.common.v1.HookPoint" json:"supported_hook_points,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Capabilities) Reset() { @@ -478,6 +572,13 @@ func (x *Capabilities) GetConfigSchema() *v11.ConfigSchema { return nil } +func (x *Capabilities) GetSupportedHookPoints() []v12.HookPoint { + if x != nil { + return x.SupportedHookPoints + } + return nil +} + // ConfigureRequest wraps the provider's agent.hcl config block, already // decoded from HCL/cty into a Struct by the kernel's schema-to-cty bridge, // per model.md §3. @@ -572,6 +673,92 @@ func (*ConfigureResponse) Descriptor() ([]byte, []int) { return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{4} } +// DescribeRequest is empty: Describe takes no request parameters, per +// configuration/lock-file.md's dev_overrides note. +type DescribeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeRequest) Reset() { + *x = DescribeRequest{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeRequest) ProtoMessage() {} + +func (x *DescribeRequest) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeRequest.ProtoReflect.Descriptor instead. +func (*DescribeRequest) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{5} +} + +// DescribeResponse reports this plugin build's own identity, per +// configuration/lock-file.md's dev_overrides note. +type DescribeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // This plugin build's identity, as it would otherwise appear in a + // lock-file provider "" { ... } entry. + Producer *v12.ProducerRef `protobuf:"bytes,1,opt,name=producer,proto3" json:"producer,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeResponse) Reset() { + *x = DescribeResponse{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeResponse) ProtoMessage() {} + +func (x *DescribeResponse) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeResponse.ProtoReflect.Descriptor instead. +func (*DescribeResponse) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{6} +} + +func (x *DescribeResponse) GetProducer() *v12.ProducerRef { + if x != nil { + return x.Producer + } + return nil +} + // ModelSpec describes one model this provider can serve, per // model.md §2. Every field below is MUST unless its comment says // otherwise. @@ -609,14 +796,28 @@ type ModelSpec struct { Caching *CachingSpec `protobuf:"bytes,9,opt,name=caching,proto3" json:"caching,omitempty"` // This model's pricing. MUST be present even for a free model (set // Pricing.free = true). - Pricing *Pricing `protobuf:"bytes,10,opt,name=pricing,proto3" json:"pricing,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Pricing *Pricing `protobuf:"bytes,10,opt,name=pricing,proto3" json:"pricing,omitempty"` + // Which GenerationParams.tool_choice.mode values this model accepts. + // SHOULD declare precisely which subset a vendor supports rather than + // collapsing to a bool, mirroring ThinkingSpec/CachingSpec's sum-type + // rationale above — vendors differ in which of AUTO/ANY/NONE/SPECIFIC + // they expose. Empty means this model does not support constraining + // tool choice at all (only free-form model-decides behavior); the + // kernel MUST NOT send a GenerationParams.tool_choice with a mode + // absent from this list. + SupportedToolChoiceModes []ToolChoiceMode `protobuf:"varint,11,rep,packed,name=supported_tool_choice_modes,json=supportedToolChoiceModes,proto3,enum=pluggableharness.agent.model.v1.ToolChoiceMode" json:"supported_tool_choice_modes,omitempty"` + // Whether this model can accept document content blocks (content.v1 + // DocumentBlock, e.g. inline PDFs). Mirrors supports_vision's rule: + // the kernel MUST reject a DocumentBlock sent to a model where this is + // false, with invalid_request, rather than silently dropping it. + SupportsDocuments bool `protobuf:"varint,12,opt,name=supports_documents,json=supportsDocuments,proto3" json:"supports_documents,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ModelSpec) Reset() { *x = ModelSpec{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[5] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -628,7 +829,7 @@ func (x *ModelSpec) String() string { func (*ModelSpec) ProtoMessage() {} func (x *ModelSpec) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[5] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -641,7 +842,7 @@ func (x *ModelSpec) ProtoReflect() protoreflect.Message { // Deprecated: Use ModelSpec.ProtoReflect.Descriptor instead. func (*ModelSpec) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{5} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{7} } func (x *ModelSpec) GetId() string { @@ -714,6 +915,20 @@ func (x *ModelSpec) GetPricing() *Pricing { return nil } +func (x *ModelSpec) GetSupportedToolChoiceModes() []ToolChoiceMode { + if x != nil { + return x.SupportedToolChoiceModes + } + return nil +} + +func (x *ModelSpec) GetSupportsDocuments() bool { + if x != nil { + return x.SupportsDocuments + } + return false +} + // ThinkingBudgetRange bounds the token budget a caller may request when // ThinkingMode is THINKING_MODE_CONTINUOUS_BUDGET. type ThinkingBudgetRange struct { @@ -728,7 +943,7 @@ type ThinkingBudgetRange struct { func (x *ThinkingBudgetRange) Reset() { *x = ThinkingBudgetRange{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[6] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -740,7 +955,7 @@ func (x *ThinkingBudgetRange) String() string { func (*ThinkingBudgetRange) ProtoMessage() {} func (x *ThinkingBudgetRange) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[6] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -753,7 +968,7 @@ func (x *ThinkingBudgetRange) ProtoReflect() protoreflect.Message { // Deprecated: Use ThinkingBudgetRange.ProtoReflect.Descriptor instead. func (*ThinkingBudgetRange) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{6} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{8} } func (x *ThinkingBudgetRange) GetMin() int64 { @@ -804,7 +1019,7 @@ type ThinkingSpec struct { func (x *ThinkingSpec) Reset() { *x = ThinkingSpec{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[7] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -816,7 +1031,7 @@ func (x *ThinkingSpec) String() string { func (*ThinkingSpec) ProtoMessage() {} func (x *ThinkingSpec) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[7] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -829,7 +1044,7 @@ func (x *ThinkingSpec) ProtoReflect() protoreflect.Message { // Deprecated: Use ThinkingSpec.ProtoReflect.Descriptor instead. func (*ThinkingSpec) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{7} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{9} } func (x *ThinkingSpec) GetSupported() bool { @@ -897,7 +1112,7 @@ type CachingSpec struct { func (x *CachingSpec) Reset() { *x = CachingSpec{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[8] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -909,7 +1124,7 @@ func (x *CachingSpec) String() string { func (*CachingSpec) ProtoMessage() {} func (x *CachingSpec) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[8] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -922,7 +1137,7 @@ func (x *CachingSpec) ProtoReflect() protoreflect.Message { // Deprecated: Use CachingSpec.ProtoReflect.Descriptor instead. func (*CachingSpec) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{8} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{10} } func (x *CachingSpec) GetSupported() bool { @@ -946,11 +1161,15 @@ func (x *CachingSpec) GetKeepaliveSupported() bool { return false } -// PricingTier is one time-bounded rate within a model's Pricing, per -// model.md §2. Exactly one tier MUST match at any given timestamp -// (effective_from <= ts < effective_until, an omitted bound unbounded on -// that side); the kernel MUST reject a Pricing value at capability-load -// time if its tiers overlap or leave a gap. +// PricingTier is one time-bounded, input-size-bounded rate within a +// model's Pricing, per model.md §2. Exactly one tier MUST match at any +// given (timestamp, input_token_count) pair — timestamp resolved against +// effective_from/effective_until (an omitted bound unbounded on that +// side) AND input_token_count resolved against input_tokens_from/ +// input_tokens_until (likewise unbounded when omitted) simultaneously; +// the kernel MUST reject a Pricing value at capability-load time if its +// tiers overlap or leave a gap across either dimension, exactly as it +// already does for the time dimension alone. type PricingTier struct { state protoimpl.MessageState `protogen:"open.v1"` // The moment this tier becomes active. Omitted means "since this plugin @@ -979,13 +1198,25 @@ type PricingTier struct { // A vendor's discounted batch/async output rate, paired with // batch_input_per_mtok. MAY be present. BatchOutputPerMtok *float64 `protobuf:"fixed64,8,opt,name=batch_output_per_mtok,json=batchOutputPerMtok,proto3,oneof" json:"batch_output_per_mtok,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The smallest accumulated-input-token count this tier applies to, + // inclusive. Omitted means unbounded below (matches any input size down + // to zero). Real vendors price by input size as well as by time — e.g. + // a distinct, higher rate once a request's input exceeds 200k tokens — + // and this field lets a tier declare that dimension alongside the + // existing effective_from/effective_until time bounds. Refines + // model/data-types.md#pricing's tier-matching rule into two dimensions. + InputTokensFrom *int64 `protobuf:"varint,9,opt,name=input_tokens_from,json=inputTokensFrom,proto3,oneof" json:"input_tokens_from,omitempty"` + // The input-token count this tier stops applying to, exclusive. + // Omitted means unbounded above. Half-open with input_tokens_from, + // matching effective_from/effective_until's half-open convention. + InputTokensUntil *int64 `protobuf:"varint,10,opt,name=input_tokens_until,json=inputTokensUntil,proto3,oneof" json:"input_tokens_until,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PricingTier) Reset() { *x = PricingTier{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[9] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -997,7 +1228,7 @@ func (x *PricingTier) String() string { func (*PricingTier) ProtoMessage() {} func (x *PricingTier) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[9] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1010,7 +1241,7 @@ func (x *PricingTier) ProtoReflect() protoreflect.Message { // Deprecated: Use PricingTier.ProtoReflect.Descriptor instead. func (*PricingTier) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{9} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{11} } func (x *PricingTier) GetEffectiveFrom() *timestamppb.Timestamp { @@ -1069,6 +1300,20 @@ func (x *PricingTier) GetBatchOutputPerMtok() float64 { return 0 } +func (x *PricingTier) GetInputTokensFrom() int64 { + if x != nil && x.InputTokensFrom != nil { + return *x.InputTokensFrom + } + return 0 +} + +func (x *PricingTier) GetInputTokensUntil() int64 { + if x != nil && x.InputTokensUntil != nil { + return *x.InputTokensUntil + } + return 0 +} + // Pricing describes one model's cost structure, per model.md §2. MUST // be present on every ModelSpec, even a free one. type Pricing struct { @@ -1091,7 +1336,7 @@ type Pricing struct { func (x *Pricing) Reset() { *x = Pricing{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[10] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1103,7 +1348,7 @@ func (x *Pricing) String() string { func (*Pricing) ProtoMessage() {} func (x *Pricing) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[10] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1116,7 +1361,7 @@ func (x *Pricing) ProtoReflect() protoreflect.Message { // Deprecated: Use Pricing.ProtoReflect.Descriptor instead. func (*Pricing) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{10} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{12} } func (x *Pricing) GetCurrency() string { @@ -1147,7 +1392,7 @@ type StreamCompletionRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The canonical conversation history, in emission order (model.md // §5). - Messages []*v12.Message `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"` + Messages []*v13.Message `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"` // Selects which of this provider's ModelSpec.id to use. ModelId string `protobuf:"bytes,2,opt,name=model_id,json=modelId,proto3" json:"model_id,omitempty"` // The tools available to the model on this turn, described in the @@ -1155,14 +1400,51 @@ type StreamCompletionRequest struct { Tools []*ToolDeclaration `protobuf:"bytes,3,rep,name=tools,proto3" json:"tools,omitempty"` // Generation-time overrides. Omitted means every param takes its // model-specific default. - Params *GenerationParams `protobuf:"bytes,4,opt,name=params,proto3,oneof" json:"params,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Params *GenerationParams `protobuf:"bytes,4,opt,name=params,proto3,oneof" json:"params,omitempty"` + // The kernel-assembled context chain — the accumulated output of every + // context provider's Contribute call plus memory recall + // (context/protocol.md#contribute-the-context-assemble-rpc), in chain + // order (the same order context.md's ContextRequest.prior_sections/ + // Contribute response chain uses: each provider appends after the + // sections it received). This is distinct from `messages` above: it is + // system-level/preamble content, never a conversational turn, which is + // why it's carried as its own field rather than folded into `messages` + // as a synthetic message — content.v1.Role deliberately has no SYSTEM + // value (content.proto's Role comment) precisely because this content + // is never a Message with a role. Each model-provider adapter maps + // this chain to its vendor's own system/preamble mechanism (a top-level + // `system` string, a leading system-role message, etc.) — how that + // mapping happens is adapter-internal and not part of this wire + // contract. + AssembledContext []*v13.ContextSection `protobuf:"bytes,5,rep,name=assembled_context,json=assembledContext,proto3" json:"assembled_context,omitempty"` + // Session/turn/working-directory attribution for this call. MUST be set + // by the kernel on every StreamCompletionRequest. This is what the + // plugin passes back on its own KernelCallbackService.Emit and Log + // calls (kernel-callbacks.md#emit) for correlation, without having to + // separately thread session_id/turn_id through adapter-internal call + // sites by hand. + CallContext *v12.CallContext `protobuf:"bytes,6,opt,name=call_context,json=callContext,proto3" json:"call_context,omitempty"` + // Cache breakpoints for this request, wire-level and request-scoped — + // NOT carried on the persisted content.v1.ContentBlock, since a + // breakpoint's placement is a per-request optimization decision, not a + // durable property of the conversation history itself. Meaningful only + // when the target model's CachingSpec.mode == + // CACHING_MODE_EXPLICIT_MARKERS; a model-provider adapter targeting a + // model whose CachingSpec.mode is CACHING_MODE_IMPLICIT_AUTOMATIC or + // CACHING_MODE_NONE MUST ignore this field rather than error on it. + // Placement is a kernel decision, not the plugin's: the kernel knows + // each assembled_context section's Stability (content.proto's Stability + // enum) and each message's position, so it places breakpoints at + // natural stable-prefix boundaries — see + // model/protocol.md#cache-breakpoint-placement-policy. + CacheBreakpoints []*CacheBreakpoint `protobuf:"bytes,7,rep,name=cache_breakpoints,json=cacheBreakpoints,proto3" json:"cache_breakpoints,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *StreamCompletionRequest) Reset() { *x = StreamCompletionRequest{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[11] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1174,7 +1456,7 @@ func (x *StreamCompletionRequest) String() string { func (*StreamCompletionRequest) ProtoMessage() {} func (x *StreamCompletionRequest) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[11] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1187,10 +1469,10 @@ func (x *StreamCompletionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StreamCompletionRequest.ProtoReflect.Descriptor instead. func (*StreamCompletionRequest) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{11} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{13} } -func (x *StreamCompletionRequest) GetMessages() []*v12.Message { +func (x *StreamCompletionRequest) GetMessages() []*v13.Message { if x != nil { return x.Messages } @@ -1218,6 +1500,137 @@ func (x *StreamCompletionRequest) GetParams() *GenerationParams { return nil } +func (x *StreamCompletionRequest) GetAssembledContext() []*v13.ContextSection { + if x != nil { + return x.AssembledContext + } + return nil +} + +func (x *StreamCompletionRequest) GetCallContext() *v12.CallContext { + if x != nil { + return x.CallContext + } + return nil +} + +func (x *StreamCompletionRequest) GetCacheBreakpoints() []*CacheBreakpoint { + if x != nil { + return x.CacheBreakpoints + } + return nil +} + +// CacheBreakpoint marks one position in a StreamCompletionRequest where +// the kernel wants the adapter to insert a vendor-native cache-control +// marker, per StreamCompletionRequest.cache_breakpoints above. +type CacheBreakpoint struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The position this breakpoint marks. Exactly one variant is set. + // + // Types that are valid to be assigned to Position: + // + // *CacheBreakpoint_AfterAssembledContext_ + // *CacheBreakpoint_AfterTools_ + // *CacheBreakpoint_AfterMessageIndex + Position isCacheBreakpoint_Position `protobuf_oneof:"position"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CacheBreakpoint) Reset() { + *x = CacheBreakpoint{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CacheBreakpoint) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CacheBreakpoint) ProtoMessage() {} + +func (x *CacheBreakpoint) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CacheBreakpoint.ProtoReflect.Descriptor instead. +func (*CacheBreakpoint) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{14} +} + +func (x *CacheBreakpoint) GetPosition() isCacheBreakpoint_Position { + if x != nil { + return x.Position + } + return nil +} + +func (x *CacheBreakpoint) GetAfterAssembledContext() *CacheBreakpoint_AfterAssembledContext { + if x != nil { + if x, ok := x.Position.(*CacheBreakpoint_AfterAssembledContext_); ok { + return x.AfterAssembledContext + } + } + return nil +} + +func (x *CacheBreakpoint) GetAfterTools() *CacheBreakpoint_AfterTools { + if x != nil { + if x, ok := x.Position.(*CacheBreakpoint_AfterTools_); ok { + return x.AfterTools + } + } + return nil +} + +func (x *CacheBreakpoint) GetAfterMessageIndex() int64 { + if x != nil { + if x, ok := x.Position.(*CacheBreakpoint_AfterMessageIndex); ok { + return x.AfterMessageIndex + } + } + return 0 +} + +type isCacheBreakpoint_Position interface { + isCacheBreakpoint_Position() +} + +type CacheBreakpoint_AfterAssembledContext_ struct { + // Immediately after the assembled_context chain — the kernel's most + // common choice, since assembled_context is usually the longest + // stable prefix (context/data-types.md#ordering--chaining orders + // STABILITY_STATIC sections before STABILITY_DYNAMIC ones). + AfterAssembledContext *CacheBreakpoint_AfterAssembledContext `protobuf:"bytes,1,opt,name=after_assembled_context,json=afterAssembledContext,proto3,oneof"` +} + +type CacheBreakpoint_AfterTools_ struct { + // Immediately after the tools declaration list. + AfterTools *CacheBreakpoint_AfterTools `protobuf:"bytes,2,opt,name=after_tools,json=afterTools,proto3,oneof"` +} + +type CacheBreakpoint_AfterMessageIndex struct { + // Immediately after the message at this zero-based index within + // `messages`. + AfterMessageIndex int64 `protobuf:"varint,3,opt,name=after_message_index,json=afterMessageIndex,proto3,oneof"` +} + +func (*CacheBreakpoint_AfterAssembledContext_) isCacheBreakpoint_Position() {} + +func (*CacheBreakpoint_AfterTools_) isCacheBreakpoint_Position() {} + +func (*CacheBreakpoint_AfterMessageIndex) isCacheBreakpoint_Position() {} + // ToolDeclaration is one tool the model may call on this turn, per // model.md §6. Each model-provider adapter translates this into its // vendor's own tool-definition wire format. @@ -1230,14 +1643,14 @@ type ToolDeclaration struct { Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` // The tool's input shape, in the restricted JSON-Schema subset shared // across categories (model.md §6, pluggableharness.agent.schema.v1.Schema). - InputSchema *v13.Schema `protobuf:"bytes,3,opt,name=input_schema,json=inputSchema,proto3" json:"input_schema,omitempty"` + InputSchema *v14.Schema `protobuf:"bytes,3,opt,name=input_schema,json=inputSchema,proto3" json:"input_schema,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ToolDeclaration) Reset() { *x = ToolDeclaration{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[12] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1249,7 +1662,7 @@ func (x *ToolDeclaration) String() string { func (*ToolDeclaration) ProtoMessage() {} func (x *ToolDeclaration) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[12] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1262,7 +1675,7 @@ func (x *ToolDeclaration) ProtoReflect() protoreflect.Message { // Deprecated: Use ToolDeclaration.ProtoReflect.Descriptor instead. func (*ToolDeclaration) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{12} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{15} } func (x *ToolDeclaration) GetName() string { @@ -1279,7 +1692,7 @@ func (x *ToolDeclaration) GetDescription() string { return "" } -func (x *ToolDeclaration) GetInputSchema() *v13.Schema { +func (x *ToolDeclaration) GetInputSchema() *v14.Schema { if x != nil { return x.InputSchema } @@ -1300,13 +1713,33 @@ type GenerationParams struct { // Per-request override of ModelSpec.max_output_tokens. Omitted means // use the model's default. MaxOutputTokens *int64 `protobuf:"varint,3,opt,name=max_output_tokens,json=maxOutputTokens,proto3,oneof" json:"max_output_tokens,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Sampling temperature. Omitted means use the model's default. Range + // and exact semantics are vendor-specific; the kernel does not clamp + // or reinterpret this value, it is passed through to the adapter as + // given. + Temperature *float64 `protobuf:"fixed64,4,opt,name=temperature,proto3,oneof" json:"temperature,omitempty"` + // Sequences that, if generated, MUST cause the model to stop before + // producing them. Omitted/empty means no caller-supplied stop + // sequences. When a vendor honors one of these, the plugin MUST report + // it back via StreamEvent.Stop.matched_stop_sequence with StopReason + // STOP_REASON_STOP_SEQUENCE. + StopSequences []string `protobuf:"bytes,5,rep,name=stop_sequences,json=stopSequences,proto3" json:"stop_sequences,omitempty"` + // Constrains whether/how the model must use a tool this turn. Omitted + // means the model decides freely (equivalent to + // TOOL_CHOICE_MODE_AUTO). Meaningful only when the target model's + // ModelSpec.supported_tool_choice_modes is non-empty; the kernel MUST + // NOT send a mode absent from that list, mirroring + // thinking_effort/thinking_budget_tokens' ThinkingSpec-validation rule + // above — an unsupported mode is a kernel-level reject-or-fallback, not + // something forwarded to the vendor. + ToolChoice *ToolChoice `protobuf:"bytes,6,opt,name=tool_choice,json=toolChoice,proto3,oneof" json:"tool_choice,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GenerationParams) Reset() { *x = GenerationParams{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[13] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1318,7 +1751,7 @@ func (x *GenerationParams) String() string { func (*GenerationParams) ProtoMessage() {} func (x *GenerationParams) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[13] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1331,7 +1764,7 @@ func (x *GenerationParams) ProtoReflect() protoreflect.Message { // Deprecated: Use GenerationParams.ProtoReflect.Descriptor instead. func (*GenerationParams) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{13} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{16} } func (x *GenerationParams) GetThinkingEffort() string { @@ -1355,6 +1788,86 @@ func (x *GenerationParams) GetMaxOutputTokens() int64 { return 0 } +func (x *GenerationParams) GetTemperature() float64 { + if x != nil && x.Temperature != nil { + return *x.Temperature + } + return 0 +} + +func (x *GenerationParams) GetStopSequences() []string { + if x != nil { + return x.StopSequences + } + return nil +} + +func (x *GenerationParams) GetToolChoice() *ToolChoice { + if x != nil { + return x.ToolChoice + } + return nil +} + +// ToolChoice carries one request's tool-invocation constraint, per +// GenerationParams.tool_choice above. +type ToolChoice struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Which constraint shape applies. MUST be set. + Mode ToolChoiceMode `protobuf:"varint,1,opt,name=mode,proto3,enum=pluggableharness.agent.model.v1.ToolChoiceMode" json:"mode,omitempty"` + // The tool the model MUST call. MUST be set, and MUST name a tool + // present in StreamCompletionRequest.tools, when mode == + // TOOL_CHOICE_MODE_SPECIFIC; meaningless and MUST be omitted for every + // other mode. + ToolName *string `protobuf:"bytes,2,opt,name=tool_name,json=toolName,proto3,oneof" json:"tool_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ToolChoice) Reset() { + *x = ToolChoice{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ToolChoice) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ToolChoice) ProtoMessage() {} + +func (x *ToolChoice) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ToolChoice.ProtoReflect.Descriptor instead. +func (*ToolChoice) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{17} +} + +func (x *ToolChoice) GetMode() ToolChoiceMode { + if x != nil { + return x.Mode + } + return ToolChoiceMode_TOOL_CHOICE_MODE_UNSPECIFIED +} + +func (x *ToolChoice) GetToolName() string { + if x != nil && x.ToolName != nil { + return *x.ToolName + } + return "" +} + // StreamEvent is one message in the stream StreamCompletion returns, per // model.md §4. Exactly one variant is set. type StreamEvent struct { @@ -1377,7 +1890,7 @@ type StreamEvent struct { func (x *StreamEvent) Reset() { *x = StreamEvent{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[14] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1389,7 +1902,7 @@ func (x *StreamEvent) String() string { func (*StreamEvent) ProtoMessage() {} func (x *StreamEvent) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[14] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1402,7 +1915,7 @@ func (x *StreamEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use StreamEvent.ProtoReflect.Descriptor instead. func (*StreamEvent) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{14} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{18} } func (x *StreamEvent) GetEvent() isStreamEvent_Event { @@ -1578,13 +2091,22 @@ type Usage struct { // Tokens written to cache, if the model supports caching. Never also // counted in input_tokens. CacheWriteTokens *int64 `protobuf:"varint,4,opt,name=cache_write_tokens,json=cacheWriteTokens,proto3,oneof" json:"cache_write_tokens,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Thinking/reasoning tokens, when the vendor reports them as a + // distinct count (ThinkingSpec.supported models only). Never also + // counted in output_tokens — a vendor that folds reasoning tokens into + // its reported output_tokens has no separate figure to report here, so + // this stays unset in that case rather than being derived/subtracted. + // Billed at the output rate (PricingTier.output_per_mtok) unless a + // future Pricing revision declares a distinct reasoning rate — there is + // none as of this revision. + ReasoningTokens *int64 `protobuf:"varint,5,opt,name=reasoning_tokens,json=reasoningTokens,proto3,oneof" json:"reasoning_tokens,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Usage) Reset() { *x = Usage{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[15] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1596,7 +2118,7 @@ func (x *Usage) String() string { func (*Usage) ProtoMessage() {} func (x *Usage) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[15] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1609,7 +2131,7 @@ func (x *Usage) ProtoReflect() protoreflect.Message { // Deprecated: Use Usage.ProtoReflect.Descriptor instead. func (*Usage) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{15} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{19} } func (x *Usage) GetInputTokens() int64 { @@ -1640,19 +2162,30 @@ func (x *Usage) GetCacheWriteTokens() int64 { return 0 } +func (x *Usage) GetReasoningTokens() int64 { + if x != nil && x.ReasoningTokens != nil { + return *x.ReasoningTokens + } + return 0 +} + // CountTokensRequest is CountTokens' request: the raw text to count, per // model.md §2.1. type CountTokensRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The text to count tokens for. - Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` + Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` + // Selects which of this provider's ModelSpec.id to count against — a + // provider serving several models MAY have distinct tokenizers per + // model. MUST be set. + ModelId string `protobuf:"bytes,2,opt,name=model_id,json=modelId,proto3" json:"model_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CountTokensRequest) Reset() { *x = CountTokensRequest{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[16] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1664,7 +2197,7 @@ func (x *CountTokensRequest) String() string { func (*CountTokensRequest) ProtoMessage() {} func (x *CountTokensRequest) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[16] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1677,7 +2210,7 @@ func (x *CountTokensRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CountTokensRequest.ProtoReflect.Descriptor instead. func (*CountTokensRequest) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{16} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{20} } func (x *CountTokensRequest) GetText() string { @@ -1687,6 +2220,13 @@ func (x *CountTokensRequest) GetText() string { return "" } +func (x *CountTokensRequest) GetModelId() string { + if x != nil { + return x.ModelId + } + return "" +} + // CountTokensResponse is CountTokens' response. type CountTokensResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1698,7 +2238,7 @@ type CountTokensResponse struct { func (x *CountTokensResponse) Reset() { *x = CountTokensResponse{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[17] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1710,7 +2250,7 @@ func (x *CountTokensResponse) String() string { func (*CountTokensResponse) ProtoMessage() {} func (x *CountTokensResponse) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[17] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1723,7 +2263,7 @@ func (x *CountTokensResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CountTokensResponse.ProtoReflect.Descriptor instead. func (*CountTokensResponse) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{17} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{21} } func (x *CountTokensResponse) GetCount() int64 { @@ -1739,14 +2279,21 @@ type RenderRequest struct { // The opaque emitted payload to render — the Emit->Render->Paint // pipeline's deliberate carve-out from the strong-typing rule (see // .claude/rules/grpc.md), never interpreted by the kernel. - Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"` + Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"` + // The schema_version `payload` was emitted under, per + // ../frontend/render-tree.md#schema-versioning. MUST be set — lets this + // Render implementation interpret a payload emitted by an older plugin + // version consistently across a replayed session, the same + // "supersedes" reasoning architecture.md applies elsewhere to + // schema-drift-sensitive persisted data. + SchemaVersion string `protobuf:"bytes,2,opt,name=schema_version,json=schemaVersion,proto3" json:"schema_version,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RenderRequest) Reset() { *x = RenderRequest{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[18] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1758,7 +2305,7 @@ func (x *RenderRequest) String() string { func (*RenderRequest) ProtoMessage() {} func (x *RenderRequest) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[18] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1771,7 +2318,7 @@ func (x *RenderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RenderRequest.ProtoReflect.Descriptor instead. func (*RenderRequest) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{18} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{22} } func (x *RenderRequest) GetPayload() []byte { @@ -1781,6 +2328,13 @@ func (x *RenderRequest) GetPayload() []byte { return nil } +func (x *RenderRequest) GetSchemaVersion() string { + if x != nil { + return x.SchemaVersion + } + return "" +} + // RenderResponse wraps the resulting RenderTree, per model.md §7. type RenderResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1788,14 +2342,14 @@ type RenderResponse struct { // verbatim across every category's Render RPC (tool.md §7, context.md // §9, memory.md §10) — one RenderTree type for the whole // Emit->Render->Paint pipeline, not a per-category variant. - Tree *v14.RenderTree `protobuf:"bytes,1,opt,name=tree,proto3" json:"tree,omitempty"` + Tree *v15.RenderTree `protobuf:"bytes,1,opt,name=tree,proto3" json:"tree,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RenderResponse) Reset() { *x = RenderResponse{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[19] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1807,7 +2361,7 @@ func (x *RenderResponse) String() string { func (*RenderResponse) ProtoMessage() {} func (x *RenderResponse) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[19] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1820,10 +2374,10 @@ func (x *RenderResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RenderResponse.ProtoReflect.Descriptor instead. func (*RenderResponse) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{19} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{23} } -func (x *RenderResponse) GetTree() *v14.RenderTree { +func (x *RenderResponse) GetTree() *v15.RenderTree { if x != nil { return x.Tree } @@ -1854,7 +2408,7 @@ type ModelError struct { func (x *ModelError) Reset() { *x = ModelError{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[20] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1866,7 +2420,7 @@ func (x *ModelError) String() string { func (*ModelError) ProtoMessage() {} func (x *ModelError) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[20] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1879,7 +2433,7 @@ func (x *ModelError) ProtoReflect() protoreflect.Message { // Deprecated: Use ModelError.ProtoReflect.Descriptor instead. func (*ModelError) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{20} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{24} } func (x *ModelError) GetCategory() ModelErrorCategory { @@ -1942,7 +2496,7 @@ type ModelTarget struct { func (x *ModelTarget) Reset() { *x = ModelTarget{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[21] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1954,7 +2508,7 @@ func (x *ModelTarget) String() string { func (*ModelTarget) ProtoMessage() {} func (x *ModelTarget) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[21] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1967,7 +2521,7 @@ func (x *ModelTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use ModelTarget.ProtoReflect.Descriptor instead. func (*ModelTarget) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{21} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{25} } func (x *ModelTarget) GetId() string { @@ -2008,7 +2562,7 @@ type ModelRef struct { func (x *ModelRef) Reset() { *x = ModelRef{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[22] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2020,7 +2574,7 @@ func (x *ModelRef) String() string { func (*ModelRef) ProtoMessage() {} func (x *ModelRef) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[22] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2033,7 +2587,7 @@ func (x *ModelRef) ProtoReflect() protoreflect.Message { // Deprecated: Use ModelRef.ProtoReflect.Descriptor instead. func (*ModelRef) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{22} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{26} } func (x *ModelRef) GetProvider() string { @@ -2050,6 +2604,82 @@ func (x *ModelRef) GetId() string { return "" } +// AfterAssembledContext is an empty marker message: its presence as the +// set oneof variant is the entire signal, no further data needed. +type CacheBreakpoint_AfterAssembledContext struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CacheBreakpoint_AfterAssembledContext) Reset() { + *x = CacheBreakpoint_AfterAssembledContext{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CacheBreakpoint_AfterAssembledContext) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CacheBreakpoint_AfterAssembledContext) ProtoMessage() {} + +func (x *CacheBreakpoint_AfterAssembledContext) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CacheBreakpoint_AfterAssembledContext.ProtoReflect.Descriptor instead. +func (*CacheBreakpoint_AfterAssembledContext) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{14, 0} +} + +// AfterTools is an empty marker message: its presence as the set oneof +// variant is the entire signal, no further data needed. +type CacheBreakpoint_AfterTools struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CacheBreakpoint_AfterTools) Reset() { + *x = CacheBreakpoint_AfterTools{} + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CacheBreakpoint_AfterTools) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CacheBreakpoint_AfterTools) ProtoMessage() {} + +func (x *CacheBreakpoint_AfterTools) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CacheBreakpoint_AfterTools.ProtoReflect.Descriptor instead. +func (*CacheBreakpoint_AfterTools) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{14, 1} +} + // TextDelta carries one incremental fragment of assistant text output. // MUST be supported by every plugin, both directions (model.md §5). type StreamEvent_TextDelta struct { @@ -2062,7 +2692,7 @@ type StreamEvent_TextDelta struct { func (x *StreamEvent_TextDelta) Reset() { *x = StreamEvent_TextDelta{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[23] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2074,7 +2704,7 @@ func (x *StreamEvent_TextDelta) String() string { func (*StreamEvent_TextDelta) ProtoMessage() {} func (x *StreamEvent_TextDelta) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[23] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2087,7 +2717,7 @@ func (x *StreamEvent_TextDelta) ProtoReflect() protoreflect.Message { // Deprecated: Use StreamEvent_TextDelta.ProtoReflect.Descriptor instead. func (*StreamEvent_TextDelta) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{14, 0} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{18, 0} } func (x *StreamEvent_TextDelta) GetText() string { @@ -2110,7 +2740,7 @@ type StreamEvent_ThinkingDelta struct { func (x *StreamEvent_ThinkingDelta) Reset() { *x = StreamEvent_ThinkingDelta{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[24] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2122,7 +2752,7 @@ func (x *StreamEvent_ThinkingDelta) String() string { func (*StreamEvent_ThinkingDelta) ProtoMessage() {} func (x *StreamEvent_ThinkingDelta) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[24] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2135,7 +2765,7 @@ func (x *StreamEvent_ThinkingDelta) ProtoReflect() protoreflect.Message { // Deprecated: Use StreamEvent_ThinkingDelta.ProtoReflect.Descriptor instead. func (*StreamEvent_ThinkingDelta) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{14, 1} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{18, 1} } func (x *StreamEvent_ThinkingDelta) GetText() string { @@ -2160,7 +2790,7 @@ type StreamEvent_ThinkingSignature struct { func (x *StreamEvent_ThinkingSignature) Reset() { *x = StreamEvent_ThinkingSignature{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[25] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2172,7 +2802,7 @@ func (x *StreamEvent_ThinkingSignature) String() string { func (*StreamEvent_ThinkingSignature) ProtoMessage() {} func (x *StreamEvent_ThinkingSignature) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[25] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2185,7 +2815,7 @@ func (x *StreamEvent_ThinkingSignature) ProtoReflect() protoreflect.Message { // Deprecated: Use StreamEvent_ThinkingSignature.ProtoReflect.Descriptor instead. func (*StreamEvent_ThinkingSignature) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{14, 2} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{18, 2} } func (x *StreamEvent_ThinkingSignature) GetSignature() []byte { @@ -2210,7 +2840,7 @@ type StreamEvent_ToolCallStart struct { func (x *StreamEvent_ToolCallStart) Reset() { *x = StreamEvent_ToolCallStart{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[26] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2222,7 +2852,7 @@ func (x *StreamEvent_ToolCallStart) String() string { func (*StreamEvent_ToolCallStart) ProtoMessage() {} func (x *StreamEvent_ToolCallStart) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[26] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2235,7 +2865,7 @@ func (x *StreamEvent_ToolCallStart) ProtoReflect() protoreflect.Message { // Deprecated: Use StreamEvent_ToolCallStart.ProtoReflect.Descriptor instead. func (*StreamEvent_ToolCallStart) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{14, 3} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{18, 3} } func (x *StreamEvent_ToolCallStart) GetId() string { @@ -2267,7 +2897,7 @@ type StreamEvent_ToolCallDelta struct { func (x *StreamEvent_ToolCallDelta) Reset() { *x = StreamEvent_ToolCallDelta{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[27] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2279,7 +2909,7 @@ func (x *StreamEvent_ToolCallDelta) String() string { func (*StreamEvent_ToolCallDelta) ProtoMessage() {} func (x *StreamEvent_ToolCallDelta) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[27] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2292,7 +2922,7 @@ func (x *StreamEvent_ToolCallDelta) ProtoReflect() protoreflect.Message { // Deprecated: Use StreamEvent_ToolCallDelta.ProtoReflect.Descriptor instead. func (*StreamEvent_ToolCallDelta) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{14, 4} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{18, 4} } func (x *StreamEvent_ToolCallDelta) GetId() string { @@ -2321,7 +2951,7 @@ type StreamEvent_ToolCallDone struct { func (x *StreamEvent_ToolCallDone) Reset() { *x = StreamEvent_ToolCallDone{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[28] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2333,7 +2963,7 @@ func (x *StreamEvent_ToolCallDone) String() string { func (*StreamEvent_ToolCallDone) ProtoMessage() {} func (x *StreamEvent_ToolCallDone) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[28] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2346,7 +2976,7 @@ func (x *StreamEvent_ToolCallDone) ProtoReflect() protoreflect.Message { // Deprecated: Use StreamEvent_ToolCallDone.ProtoReflect.Descriptor instead. func (*StreamEvent_ToolCallDone) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{14, 5} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{18, 5} } func (x *StreamEvent_ToolCallDone) GetId() string { @@ -2360,14 +2990,18 @@ func (x *StreamEvent_ToolCallDone) GetId() string { type StreamEvent_Stop struct { state protoimpl.MessageState `protogen:"open.v1"` // Why the completion ended. - Reason StopReason `protobuf:"varint,1,opt,name=reason,proto3,enum=pluggableharness.agent.model.v1.StopReason" json:"reason,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Reason StopReason `protobuf:"varint,1,opt,name=reason,proto3,enum=pluggableharness.agent.model.v1.StopReason" json:"reason,omitempty"` + // Which GenerationParams.stop_sequences entry was matched. Set iff + // reason == STOP_REASON_STOP_SEQUENCE; MUST be omitted for every + // other reason. + MatchedStopSequence *string `protobuf:"bytes,2,opt,name=matched_stop_sequence,json=matchedStopSequence,proto3,oneof" json:"matched_stop_sequence,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *StreamEvent_Stop) Reset() { *x = StreamEvent_Stop{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[29] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2379,7 +3013,7 @@ func (x *StreamEvent_Stop) String() string { func (*StreamEvent_Stop) ProtoMessage() {} func (x *StreamEvent_Stop) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[29] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2392,7 +3026,7 @@ func (x *StreamEvent_Stop) ProtoReflect() protoreflect.Message { // Deprecated: Use StreamEvent_Stop.ProtoReflect.Descriptor instead. func (*StreamEvent_Stop) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{14, 6} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{18, 6} } func (x *StreamEvent_Stop) GetReason() StopReason { @@ -2402,6 +3036,13 @@ func (x *StreamEvent_Stop) GetReason() StopReason { return StopReason_STOP_REASON_UNSPECIFIED } +func (x *StreamEvent_Stop) GetMatchedStopSequence() string { + if x != nil && x.MatchedStopSequence != nil { + return *x.MatchedStopSequence + } + return "" +} + // Error signals the completion failed. type StreamEvent_Error struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -2413,7 +3054,7 @@ type StreamEvent_Error struct { func (x *StreamEvent_Error) Reset() { *x = StreamEvent_Error{} - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[30] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2425,7 +3066,7 @@ func (x *StreamEvent_Error) String() string { func (*StreamEvent_Error) ProtoMessage() {} func (x *StreamEvent_Error) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[30] + mi := &file_pluggableharness_agent_model_v1_model_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2438,7 +3079,7 @@ func (x *StreamEvent_Error) ProtoReflect() protoreflect.Message { // Deprecated: Use StreamEvent_Error.ProtoReflect.Descriptor instead. func (*StreamEvent_Error) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{14, 7} + return file_pluggableharness_agent_model_v1_model_proto_rawDescGZIP(), []int{18, 7} } func (x *StreamEvent_Error) GetError() *ModelError { @@ -2452,17 +3093,21 @@ var File_pluggableharness_agent_model_v1_model_proto protoreflect.FileDescriptor const file_pluggableharness_agent_model_v1_model_proto_rawDesc = "" + "\n" + - "+pluggableharness/agent/model/v1/model.proto\x12\x1fpluggableharness.agent.model.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a-pluggableharness/agent/config/v1/config.proto\x1a/pluggableharness/agent/content/v1/content.proto\x1a-pluggableharness/agent/render/v1/render.proto\x1a-pluggableharness/agent/schema/v1/schema.proto\x1a9pluggableharness/agent/slashcommand/v1/slashcommand.proto\"\x18\n" + + "+pluggableharness/agent/model/v1/model.proto\x12\x1fpluggableharness.agent.model.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a-pluggableharness/agent/common/v1/common.proto\x1a-pluggableharness/agent/config/v1/config.proto\x1a/pluggableharness/agent/content/v1/content.proto\x1a-pluggableharness/agent/render/v1/render.proto\x1a-pluggableharness/agent/schema/v1/schema.proto\x1a9pluggableharness/agent/slashcommand/v1/slashcommand.proto\"\x18\n" + "\x16GetCapabilitiesRequest\"l\n" + "\x17GetCapabilitiesResponse\x12Q\n" + - "\fcapabilities\x18\x01 \x01(\v2-.pluggableharness.agent.model.v1.CapabilitiesR\fcapabilities\"\x88\x02\n" + + "\fcapabilities\x18\x01 \x01(\v2-.pluggableharness.agent.model.v1.CapabilitiesR\fcapabilities\"\xe9\x02\n" + "\fCapabilities\x12B\n" + "\x06models\x18\x01 \x03(\v2*.pluggableharness.agent.model.v1.ModelSpecR\x06models\x12_\n" + "\x0eslash_commands\x18\x02 \x03(\v28.pluggableharness.agent.slashcommand.v1.SlashCommandSpecR\rslashCommands\x12S\n" + - "\rconfig_schema\x18\x03 \x01(\v2..pluggableharness.agent.config.v1.ConfigSchemaR\fconfigSchema\"C\n" + + "\rconfig_schema\x18\x03 \x01(\v2..pluggableharness.agent.config.v1.ConfigSchemaR\fconfigSchema\x12_\n" + + "\x15supported_hook_points\x18\x04 \x03(\x0e2+.pluggableharness.agent.common.v1.HookPointR\x13supportedHookPoints\"C\n" + "\x10ConfigureRequest\x12/\n" + "\x06config\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x06config\"\x13\n" + - "\x11ConfigureResponse\"\xb0\x04\n" + + "\x11ConfigureResponse\"\x11\n" + + "\x0fDescribeRequest\"]\n" + + "\x10DescribeResponse\x12I\n" + + "\bproducer\x18\x01 \x01(\v2-.pluggableharness.agent.common.v1.ProducerRefR\bproducer\"\xcf\x05\n" + "\tModelSpec\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12%\n" + "\x0econtext_window\x18\x02 \x01(\x03R\rcontextWindow\x12*\n" + @@ -2474,7 +3119,9 @@ const file_pluggableharness_agent_model_v1_model_proto_rawDesc = "" + "\bthinking\x18\b \x01(\v2-.pluggableharness.agent.model.v1.ThinkingSpecR\bthinking\x12F\n" + "\acaching\x18\t \x01(\v2,.pluggableharness.agent.model.v1.CachingSpecR\acaching\x12B\n" + "\apricing\x18\n" + - " \x01(\v2(.pluggableharness.agent.model.v1.PricingR\apricingB\x1f\n" + + " \x01(\v2(.pluggableharness.agent.model.v1.PricingR\apricing\x12n\n" + + "\x1bsupported_tool_choice_modes\x18\v \x03(\x0e2/.pluggableharness.agent.model.v1.ToolChoiceModeR\x18supportedToolChoiceModes\x12-\n" + + "\x12supports_documents\x18\f \x01(\bR\x11supportsDocumentsB\x1f\n" + "\x1d_supports_parallel_tool_calls\"9\n" + "\x13ThinkingBudgetRange\x12\x10\n" + "\x03min\x18\x01 \x01(\x03R\x03min\x12\x10\n" + @@ -2493,7 +3140,7 @@ const file_pluggableharness_agent_model_v1_model_proto_rawDesc = "" + "\vCachingSpec\x12\x1c\n" + "\tsupported\x18\x01 \x01(\bR\tsupported\x12@\n" + "\x04mode\x18\x02 \x01(\x0e2,.pluggableharness.agent.model.v1.CachingModeR\x04mode\x12/\n" + - "\x13keepalive_supported\x18\x03 \x01(\bR\x12keepaliveSupported\"\xd0\x04\n" + + "\x13keepalive_supported\x18\x03 \x01(\bR\x12keepaliveSupported\"\xe1\x05\n" + "\vPricingTier\x12F\n" + "\x0eeffective_from\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampH\x00R\reffectiveFrom\x88\x01\x01\x12H\n" + "\x0feffective_until\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampH\x01R\x0eeffectiveUntil\x88\x01\x01\x12$\n" + @@ -2502,34 +3149,64 @@ const file_pluggableharness_agent_model_v1_model_proto_rawDesc = "" + "\x14cache_write_per_mtok\x18\x05 \x01(\x01H\x02R\x11cacheWritePerMtok\x88\x01\x01\x122\n" + "\x13cache_read_per_mtok\x18\x06 \x01(\x01H\x03R\x10cacheReadPerMtok\x88\x01\x01\x124\n" + "\x14batch_input_per_mtok\x18\a \x01(\x01H\x04R\x11batchInputPerMtok\x88\x01\x01\x126\n" + - "\x15batch_output_per_mtok\x18\b \x01(\x01H\x05R\x12batchOutputPerMtok\x88\x01\x01B\x11\n" + + "\x15batch_output_per_mtok\x18\b \x01(\x01H\x05R\x12batchOutputPerMtok\x88\x01\x01\x12/\n" + + "\x11input_tokens_from\x18\t \x01(\x03H\x06R\x0finputTokensFrom\x88\x01\x01\x121\n" + + "\x12input_tokens_until\x18\n" + + " \x01(\x03H\aR\x10inputTokensUntil\x88\x01\x01B\x11\n" + "\x0f_effective_fromB\x12\n" + "\x10_effective_untilB\x17\n" + "\x15_cache_write_per_mtokB\x16\n" + "\x14_cache_read_per_mtokB\x17\n" + "\x15_batch_input_per_mtokB\x18\n" + - "\x16_batch_output_per_mtok\"}\n" + + "\x16_batch_output_per_mtokB\x14\n" + + "\x12_input_tokens_fromB\x15\n" + + "\x13_input_tokens_until\"}\n" + "\aPricing\x12\x1a\n" + "\bcurrency\x18\x01 \x01(\tR\bcurrency\x12\x12\n" + "\x04free\x18\x02 \x01(\bR\x04free\x12B\n" + - "\x05tiers\x18\x03 \x03(\v2,.pluggableharness.agent.model.v1.PricingTierR\x05tiers\"\x9f\x02\n" + + "\x05tiers\x18\x03 \x03(\v2,.pluggableharness.agent.model.v1.PricingTierR\x05tiers\"\xb0\x04\n" + "\x17StreamCompletionRequest\x12F\n" + "\bmessages\x18\x01 \x03(\v2*.pluggableharness.agent.content.v1.MessageR\bmessages\x12\x19\n" + "\bmodel_id\x18\x02 \x01(\tR\amodelId\x12F\n" + "\x05tools\x18\x03 \x03(\v20.pluggableharness.agent.model.v1.ToolDeclarationR\x05tools\x12N\n" + - "\x06params\x18\x04 \x01(\v21.pluggableharness.agent.model.v1.GenerationParamsH\x00R\x06params\x88\x01\x01B\t\n" + - "\a_params\"\x94\x01\n" + + "\x06params\x18\x04 \x01(\v21.pluggableharness.agent.model.v1.GenerationParamsH\x00R\x06params\x88\x01\x01\x12^\n" + + "\x11assembled_context\x18\x05 \x03(\v21.pluggableharness.agent.content.v1.ContextSectionR\x10assembledContext\x12P\n" + + "\fcall_context\x18\x06 \x01(\v2-.pluggableharness.agent.common.v1.CallContextR\vcallContext\x12]\n" + + "\x11cache_breakpoints\x18\a \x03(\v20.pluggableharness.agent.model.v1.CacheBreakpointR\x10cacheBreakpointsB\t\n" + + "\a_params\"\xd9\x02\n" + + "\x0fCacheBreakpoint\x12\x80\x01\n" + + "\x17after_assembled_context\x18\x01 \x01(\v2F.pluggableharness.agent.model.v1.CacheBreakpoint.AfterAssembledContextH\x00R\x15afterAssembledContext\x12^\n" + + "\vafter_tools\x18\x02 \x01(\v2;.pluggableharness.agent.model.v1.CacheBreakpoint.AfterToolsH\x00R\n" + + "afterTools\x120\n" + + "\x13after_message_index\x18\x03 \x01(\x03H\x00R\x11afterMessageIndex\x1a\x17\n" + + "\x15AfterAssembledContext\x1a\f\n" + + "\n" + + "AfterToolsB\n" + + "\n" + + "\bposition\"\x94\x01\n" + "\x0fToolDeclaration\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + "\vdescription\x18\x02 \x01(\tR\vdescription\x12K\n" + - "\finput_schema\x18\x03 \x01(\v2(.pluggableharness.agent.schema.v1.SchemaR\vinputSchema\"\xf1\x01\n" + + "\finput_schema\x18\x03 \x01(\v2(.pluggableharness.agent.schema.v1.SchemaR\vinputSchema\"\xb2\x03\n" + "\x10GenerationParams\x12,\n" + "\x0fthinking_effort\x18\x01 \x01(\tH\x00R\x0ethinkingEffort\x88\x01\x01\x129\n" + "\x16thinking_budget_tokens\x18\x02 \x01(\x03H\x01R\x14thinkingBudgetTokens\x88\x01\x01\x12/\n" + - "\x11max_output_tokens\x18\x03 \x01(\x03H\x02R\x0fmaxOutputTokens\x88\x01\x01B\x12\n" + + "\x11max_output_tokens\x18\x03 \x01(\x03H\x02R\x0fmaxOutputTokens\x88\x01\x01\x12%\n" + + "\vtemperature\x18\x04 \x01(\x01H\x03R\vtemperature\x88\x01\x01\x12%\n" + + "\x0estop_sequences\x18\x05 \x03(\tR\rstopSequences\x12Q\n" + + "\vtool_choice\x18\x06 \x01(\v2+.pluggableharness.agent.model.v1.ToolChoiceH\x04R\n" + + "toolChoice\x88\x01\x01B\x12\n" + "\x10_thinking_effortB\x19\n" + "\x17_thinking_budget_tokensB\x14\n" + - "\x12_max_output_tokens\"\x80\n" + + "\x12_max_output_tokensB\x0e\n" + + "\f_temperatureB\x0e\n" + + "\f_tool_choice\"\x81\x01\n" + + "\n" + + "ToolChoice\x12C\n" + + "\x04mode\x18\x01 \x01(\x0e2/.pluggableharness.agent.model.v1.ToolChoiceModeR\x04mode\x12 \n" + + "\ttool_name\x18\x02 \x01(\tH\x00R\btoolName\x88\x01\x01B\f\n" + + "\n" + + "_tool_name\"\xd4\n" + "\n" + "\vStreamEvent\x12W\n" + "\n" + @@ -2555,25 +3232,31 @@ const file_pluggableharness_agent_model_v1_model_proto_rawDesc = "" + "\x02id\x18\x01 \x01(\tR\x02id\x12-\n" + "\x12arguments_fragment\x18\x02 \x01(\tR\x11argumentsFragment\x1a\x1e\n" + "\fToolCallDone\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id\x1aK\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x1a\x9e\x01\n" + "\x04Stop\x12C\n" + - "\x06reason\x18\x01 \x01(\x0e2+.pluggableharness.agent.model.v1.StopReasonR\x06reason\x1aJ\n" + + "\x06reason\x18\x01 \x01(\x0e2+.pluggableharness.agent.model.v1.StopReasonR\x06reason\x127\n" + + "\x15matched_stop_sequence\x18\x02 \x01(\tH\x00R\x13matchedStopSequence\x88\x01\x01B\x18\n" + + "\x16_matched_stop_sequence\x1aJ\n" + "\x05Error\x12A\n" + "\x05error\x18\x01 \x01(\v2+.pluggableharness.agent.model.v1.ModelErrorR\x05errorB\a\n" + - "\x05event\"\xe0\x01\n" + + "\x05event\"\xa5\x02\n" + "\x05Usage\x12!\n" + "\finput_tokens\x18\x01 \x01(\x03R\vinputTokens\x12#\n" + "\routput_tokens\x18\x02 \x01(\x03R\foutputTokens\x12/\n" + "\x11cache_read_tokens\x18\x03 \x01(\x03H\x00R\x0fcacheReadTokens\x88\x01\x01\x121\n" + - "\x12cache_write_tokens\x18\x04 \x01(\x03H\x01R\x10cacheWriteTokens\x88\x01\x01B\x14\n" + + "\x12cache_write_tokens\x18\x04 \x01(\x03H\x01R\x10cacheWriteTokens\x88\x01\x01\x12.\n" + + "\x10reasoning_tokens\x18\x05 \x01(\x03H\x02R\x0freasoningTokens\x88\x01\x01B\x14\n" + "\x12_cache_read_tokensB\x15\n" + - "\x13_cache_write_tokens\"(\n" + + "\x13_cache_write_tokensB\x13\n" + + "\x11_reasoning_tokens\"C\n" + "\x12CountTokensRequest\x12\x12\n" + - "\x04text\x18\x01 \x01(\tR\x04text\"+\n" + + "\x04text\x18\x01 \x01(\tR\x04text\x12\x19\n" + + "\bmodel_id\x18\x02 \x01(\tR\amodelId\"+\n" + "\x13CountTokensResponse\x12\x14\n" + - "\x05count\x18\x01 \x01(\x03R\x05count\")\n" + + "\x05count\x18\x01 \x01(\x03R\x05count\"P\n" + "\rRenderRequest\x12\x18\n" + - "\apayload\x18\x01 \x01(\fR\apayload\"R\n" + + "\apayload\x18\x01 \x01(\fR\apayload\x12%\n" + + "\x0eschema_version\x18\x02 \x01(\tR\rschemaVersion\"R\n" + "\x0eRenderResponse\x12@\n" + "\x04tree\x18\x01 \x01(\v2,.pluggableharness.agent.render.v1.RenderTreeR\x04tree\"\x99\x02\n" + "\n" + @@ -2604,7 +3287,13 @@ const file_pluggableharness_agent_model_v1_model_proto_rawDesc = "" + "\x18CACHING_MODE_UNSPECIFIED\x10\x00\x12\x15\n" + "\x11CACHING_MODE_NONE\x10\x01\x12!\n" + "\x1dCACHING_MODE_EXPLICIT_MARKERS\x10\x02\x12#\n" + - "\x1fCACHING_MODE_IMPLICIT_AUTOMATIC\x10\x03*\xb6\x01\n" + + "\x1fCACHING_MODE_IMPLICIT_AUTOMATIC\x10\x03*\xa1\x01\n" + + "\x0eToolChoiceMode\x12 \n" + + "\x1cTOOL_CHOICE_MODE_UNSPECIFIED\x10\x00\x12\x19\n" + + "\x15TOOL_CHOICE_MODE_AUTO\x10\x01\x12\x18\n" + + "\x14TOOL_CHOICE_MODE_ANY\x10\x02\x12\x19\n" + + "\x15TOOL_CHOICE_MODE_NONE\x10\x03\x12\x1d\n" + + "\x19TOOL_CHOICE_MODE_SPECIFIC\x10\x04*\xee\x01\n" + "\n" + "StopReason\x12\x1b\n" + "\x17STOP_REASON_UNSPECIFIED\x10\x00\x12\x18\n" + @@ -2612,7 +3301,9 @@ const file_pluggableharness_agent_model_v1_model_proto_rawDesc = "" + "\x14STOP_REASON_TOOL_USE\x10\x02\x12\x1a\n" + "\x16STOP_REASON_MAX_TOKENS\x10\x03\x12 \n" + "\x1cSTOP_REASON_CONTENT_FILTERED\x10\x04\x12\x19\n" + - "\x15STOP_REASON_CANCELLED\x10\x05*\xd4\x02\n" + + "\x15STOP_REASON_CANCELLED\x10\x05\x12\x17\n" + + "\x13STOP_REASON_REFUSAL\x10\x06\x12\x1d\n" + + "\x19STOP_REASON_STOP_SEQUENCE\x10\a*\xd4\x02\n" + "\x12ModelErrorCategory\x12$\n" + " MODEL_ERROR_CATEGORY_UNSPECIFIED\x10\x00\x120\n" + ",MODEL_ERROR_CATEGORY_CONTEXT_LENGTH_EXCEEDED\x10\x01\x12%\n" + @@ -2621,13 +3312,14 @@ const file_pluggableharness_agent_model_v1_model_proto_rawDesc = "" + "\x1fMODEL_ERROR_CATEGORY_AUTH_ERROR\x10\x04\x12(\n" + "$MODEL_ERROR_CATEGORY_INVALID_REQUEST\x10\x05\x12)\n" + "%MODEL_ERROR_CATEGORY_CONTENT_FILTERED\x10\x06\x12 \n" + - "\x1cMODEL_ERROR_CATEGORY_UNKNOWN\x10\a2\xec\x04\n" + + "\x1cMODEL_ERROR_CATEGORY_UNKNOWN\x10\a2\xdd\x05\n" + "\fModelService\x12\x84\x01\n" + "\x0fGetCapabilities\x127.pluggableharness.agent.model.v1.GetCapabilitiesRequest\x1a8.pluggableharness.agent.model.v1.GetCapabilitiesResponse\x12r\n" + "\tConfigure\x121.pluggableharness.agent.model.v1.ConfigureRequest\x1a2.pluggableharness.agent.model.v1.ConfigureResponse\x12|\n" + "\x10StreamCompletion\x128.pluggableharness.agent.model.v1.StreamCompletionRequest\x1a,.pluggableharness.agent.model.v1.StreamEvent0\x01\x12x\n" + "\vCountTokens\x123.pluggableharness.agent.model.v1.CountTokensRequest\x1a4.pluggableharness.agent.model.v1.CountTokensResponse\x12i\n" + - "\x06Render\x12..pluggableharness.agent.model.v1.RenderRequest\x1a/.pluggableharness.agent.model.v1.RenderResponseB>ZZ pluggableharness.agent.model.v1.Capabilities - 9, // 1: pluggableharness.agent.model.v1.Capabilities.models:type_name -> pluggableharness.agent.model.v1.ModelSpec - 35, // 2: pluggableharness.agent.model.v1.Capabilities.slash_commands:type_name -> pluggableharness.agent.slashcommand.v1.SlashCommandSpec - 36, // 3: pluggableharness.agent.model.v1.Capabilities.config_schema:type_name -> pluggableharness.agent.config.v1.ConfigSchema - 37, // 4: pluggableharness.agent.model.v1.ConfigureRequest.config:type_name -> google.protobuf.Struct - 11, // 5: pluggableharness.agent.model.v1.ModelSpec.thinking:type_name -> pluggableharness.agent.model.v1.ThinkingSpec - 12, // 6: pluggableharness.agent.model.v1.ModelSpec.caching:type_name -> pluggableharness.agent.model.v1.CachingSpec - 14, // 7: pluggableharness.agent.model.v1.ModelSpec.pricing:type_name -> pluggableharness.agent.model.v1.Pricing - 0, // 8: pluggableharness.agent.model.v1.ThinkingSpec.mode:type_name -> pluggableharness.agent.model.v1.ThinkingMode - 10, // 9: pluggableharness.agent.model.v1.ThinkingSpec.budget_range:type_name -> pluggableharness.agent.model.v1.ThinkingBudgetRange - 1, // 10: pluggableharness.agent.model.v1.CachingSpec.mode:type_name -> pluggableharness.agent.model.v1.CachingMode - 38, // 11: pluggableharness.agent.model.v1.PricingTier.effective_from:type_name -> google.protobuf.Timestamp - 38, // 12: pluggableharness.agent.model.v1.PricingTier.effective_until:type_name -> google.protobuf.Timestamp - 13, // 13: pluggableharness.agent.model.v1.Pricing.tiers:type_name -> pluggableharness.agent.model.v1.PricingTier - 39, // 14: pluggableharness.agent.model.v1.StreamCompletionRequest.messages:type_name -> pluggableharness.agent.content.v1.Message - 16, // 15: pluggableharness.agent.model.v1.StreamCompletionRequest.tools:type_name -> pluggableharness.agent.model.v1.ToolDeclaration - 17, // 16: pluggableharness.agent.model.v1.StreamCompletionRequest.params:type_name -> pluggableharness.agent.model.v1.GenerationParams - 40, // 17: pluggableharness.agent.model.v1.ToolDeclaration.input_schema:type_name -> pluggableharness.agent.schema.v1.Schema - 27, // 18: pluggableharness.agent.model.v1.StreamEvent.text_delta:type_name -> pluggableharness.agent.model.v1.StreamEvent.TextDelta - 28, // 19: pluggableharness.agent.model.v1.StreamEvent.thinking_delta:type_name -> pluggableharness.agent.model.v1.StreamEvent.ThinkingDelta - 29, // 20: pluggableharness.agent.model.v1.StreamEvent.thinking_signature:type_name -> pluggableharness.agent.model.v1.StreamEvent.ThinkingSignature - 30, // 21: pluggableharness.agent.model.v1.StreamEvent.tool_call_start:type_name -> pluggableharness.agent.model.v1.StreamEvent.ToolCallStart - 31, // 22: pluggableharness.agent.model.v1.StreamEvent.tool_call_delta:type_name -> pluggableharness.agent.model.v1.StreamEvent.ToolCallDelta - 32, // 23: pluggableharness.agent.model.v1.StreamEvent.tool_call_done:type_name -> pluggableharness.agent.model.v1.StreamEvent.ToolCallDone - 19, // 24: pluggableharness.agent.model.v1.StreamEvent.usage:type_name -> pluggableharness.agent.model.v1.Usage - 33, // 25: pluggableharness.agent.model.v1.StreamEvent.stop:type_name -> pluggableharness.agent.model.v1.StreamEvent.Stop - 34, // 26: pluggableharness.agent.model.v1.StreamEvent.error:type_name -> pluggableharness.agent.model.v1.StreamEvent.Error - 41, // 27: pluggableharness.agent.model.v1.RenderResponse.tree:type_name -> pluggableharness.agent.render.v1.RenderTree - 3, // 28: pluggableharness.agent.model.v1.ModelError.category:type_name -> pluggableharness.agent.model.v1.ModelErrorCategory - 42, // 29: pluggableharness.agent.model.v1.ModelError.retry_after:type_name -> google.protobuf.Duration - 2, // 30: pluggableharness.agent.model.v1.StreamEvent.Stop.reason:type_name -> pluggableharness.agent.model.v1.StopReason - 24, // 31: pluggableharness.agent.model.v1.StreamEvent.Error.error:type_name -> pluggableharness.agent.model.v1.ModelError - 4, // 32: pluggableharness.agent.model.v1.ModelService.GetCapabilities:input_type -> pluggableharness.agent.model.v1.GetCapabilitiesRequest - 7, // 33: pluggableharness.agent.model.v1.ModelService.Configure:input_type -> pluggableharness.agent.model.v1.ConfigureRequest - 15, // 34: pluggableharness.agent.model.v1.ModelService.StreamCompletion:input_type -> pluggableharness.agent.model.v1.StreamCompletionRequest - 20, // 35: pluggableharness.agent.model.v1.ModelService.CountTokens:input_type -> pluggableharness.agent.model.v1.CountTokensRequest - 22, // 36: pluggableharness.agent.model.v1.ModelService.Render:input_type -> pluggableharness.agent.model.v1.RenderRequest - 5, // 37: pluggableharness.agent.model.v1.ModelService.GetCapabilities:output_type -> pluggableharness.agent.model.v1.GetCapabilitiesResponse - 8, // 38: pluggableharness.agent.model.v1.ModelService.Configure:output_type -> pluggableharness.agent.model.v1.ConfigureResponse - 18, // 39: pluggableharness.agent.model.v1.ModelService.StreamCompletion:output_type -> pluggableharness.agent.model.v1.StreamEvent - 21, // 40: pluggableharness.agent.model.v1.ModelService.CountTokens:output_type -> pluggableharness.agent.model.v1.CountTokensResponse - 23, // 41: pluggableharness.agent.model.v1.ModelService.Render:output_type -> pluggableharness.agent.model.v1.RenderResponse - 37, // [37:42] is the sub-list for method output_type - 32, // [32:37] is the sub-list for method input_type - 32, // [32:32] is the sub-list for extension type_name - 32, // [32:32] is the sub-list for extension extendee - 0, // [0:32] is the sub-list for field type_name + 7, // 0: pluggableharness.agent.model.v1.GetCapabilitiesResponse.capabilities:type_name -> pluggableharness.agent.model.v1.Capabilities + 12, // 1: pluggableharness.agent.model.v1.Capabilities.models:type_name -> pluggableharness.agent.model.v1.ModelSpec + 42, // 2: pluggableharness.agent.model.v1.Capabilities.slash_commands:type_name -> pluggableharness.agent.slashcommand.v1.SlashCommandSpec + 43, // 3: pluggableharness.agent.model.v1.Capabilities.config_schema:type_name -> pluggableharness.agent.config.v1.ConfigSchema + 44, // 4: pluggableharness.agent.model.v1.Capabilities.supported_hook_points:type_name -> pluggableharness.agent.common.v1.HookPoint + 45, // 5: pluggableharness.agent.model.v1.ConfigureRequest.config:type_name -> google.protobuf.Struct + 46, // 6: pluggableharness.agent.model.v1.DescribeResponse.producer:type_name -> pluggableharness.agent.common.v1.ProducerRef + 14, // 7: pluggableharness.agent.model.v1.ModelSpec.thinking:type_name -> pluggableharness.agent.model.v1.ThinkingSpec + 15, // 8: pluggableharness.agent.model.v1.ModelSpec.caching:type_name -> pluggableharness.agent.model.v1.CachingSpec + 17, // 9: pluggableharness.agent.model.v1.ModelSpec.pricing:type_name -> pluggableharness.agent.model.v1.Pricing + 2, // 10: pluggableharness.agent.model.v1.ModelSpec.supported_tool_choice_modes:type_name -> pluggableharness.agent.model.v1.ToolChoiceMode + 0, // 11: pluggableharness.agent.model.v1.ThinkingSpec.mode:type_name -> pluggableharness.agent.model.v1.ThinkingMode + 13, // 12: pluggableharness.agent.model.v1.ThinkingSpec.budget_range:type_name -> pluggableharness.agent.model.v1.ThinkingBudgetRange + 1, // 13: pluggableharness.agent.model.v1.CachingSpec.mode:type_name -> pluggableharness.agent.model.v1.CachingMode + 47, // 14: pluggableharness.agent.model.v1.PricingTier.effective_from:type_name -> google.protobuf.Timestamp + 47, // 15: pluggableharness.agent.model.v1.PricingTier.effective_until:type_name -> google.protobuf.Timestamp + 16, // 16: pluggableharness.agent.model.v1.Pricing.tiers:type_name -> pluggableharness.agent.model.v1.PricingTier + 48, // 17: pluggableharness.agent.model.v1.StreamCompletionRequest.messages:type_name -> pluggableharness.agent.content.v1.Message + 20, // 18: pluggableharness.agent.model.v1.StreamCompletionRequest.tools:type_name -> pluggableharness.agent.model.v1.ToolDeclaration + 21, // 19: pluggableharness.agent.model.v1.StreamCompletionRequest.params:type_name -> pluggableharness.agent.model.v1.GenerationParams + 49, // 20: pluggableharness.agent.model.v1.StreamCompletionRequest.assembled_context:type_name -> pluggableharness.agent.content.v1.ContextSection + 50, // 21: pluggableharness.agent.model.v1.StreamCompletionRequest.call_context:type_name -> pluggableharness.agent.common.v1.CallContext + 19, // 22: pluggableharness.agent.model.v1.StreamCompletionRequest.cache_breakpoints:type_name -> pluggableharness.agent.model.v1.CacheBreakpoint + 32, // 23: pluggableharness.agent.model.v1.CacheBreakpoint.after_assembled_context:type_name -> pluggableharness.agent.model.v1.CacheBreakpoint.AfterAssembledContext + 33, // 24: pluggableharness.agent.model.v1.CacheBreakpoint.after_tools:type_name -> pluggableharness.agent.model.v1.CacheBreakpoint.AfterTools + 51, // 25: pluggableharness.agent.model.v1.ToolDeclaration.input_schema:type_name -> pluggableharness.agent.schema.v1.Schema + 22, // 26: pluggableharness.agent.model.v1.GenerationParams.tool_choice:type_name -> pluggableharness.agent.model.v1.ToolChoice + 2, // 27: pluggableharness.agent.model.v1.ToolChoice.mode:type_name -> pluggableharness.agent.model.v1.ToolChoiceMode + 34, // 28: pluggableharness.agent.model.v1.StreamEvent.text_delta:type_name -> pluggableharness.agent.model.v1.StreamEvent.TextDelta + 35, // 29: pluggableharness.agent.model.v1.StreamEvent.thinking_delta:type_name -> pluggableharness.agent.model.v1.StreamEvent.ThinkingDelta + 36, // 30: pluggableharness.agent.model.v1.StreamEvent.thinking_signature:type_name -> pluggableharness.agent.model.v1.StreamEvent.ThinkingSignature + 37, // 31: pluggableharness.agent.model.v1.StreamEvent.tool_call_start:type_name -> pluggableharness.agent.model.v1.StreamEvent.ToolCallStart + 38, // 32: pluggableharness.agent.model.v1.StreamEvent.tool_call_delta:type_name -> pluggableharness.agent.model.v1.StreamEvent.ToolCallDelta + 39, // 33: pluggableharness.agent.model.v1.StreamEvent.tool_call_done:type_name -> pluggableharness.agent.model.v1.StreamEvent.ToolCallDone + 24, // 34: pluggableharness.agent.model.v1.StreamEvent.usage:type_name -> pluggableharness.agent.model.v1.Usage + 40, // 35: pluggableharness.agent.model.v1.StreamEvent.stop:type_name -> pluggableharness.agent.model.v1.StreamEvent.Stop + 41, // 36: pluggableharness.agent.model.v1.StreamEvent.error:type_name -> pluggableharness.agent.model.v1.StreamEvent.Error + 52, // 37: pluggableharness.agent.model.v1.RenderResponse.tree:type_name -> pluggableharness.agent.render.v1.RenderTree + 4, // 38: pluggableharness.agent.model.v1.ModelError.category:type_name -> pluggableharness.agent.model.v1.ModelErrorCategory + 53, // 39: pluggableharness.agent.model.v1.ModelError.retry_after:type_name -> google.protobuf.Duration + 3, // 40: pluggableharness.agent.model.v1.StreamEvent.Stop.reason:type_name -> pluggableharness.agent.model.v1.StopReason + 29, // 41: pluggableharness.agent.model.v1.StreamEvent.Error.error:type_name -> pluggableharness.agent.model.v1.ModelError + 5, // 42: pluggableharness.agent.model.v1.ModelService.GetCapabilities:input_type -> pluggableharness.agent.model.v1.GetCapabilitiesRequest + 8, // 43: pluggableharness.agent.model.v1.ModelService.Configure:input_type -> pluggableharness.agent.model.v1.ConfigureRequest + 18, // 44: pluggableharness.agent.model.v1.ModelService.StreamCompletion:input_type -> pluggableharness.agent.model.v1.StreamCompletionRequest + 25, // 45: pluggableharness.agent.model.v1.ModelService.CountTokens:input_type -> pluggableharness.agent.model.v1.CountTokensRequest + 27, // 46: pluggableharness.agent.model.v1.ModelService.Render:input_type -> pluggableharness.agent.model.v1.RenderRequest + 10, // 47: pluggableharness.agent.model.v1.ModelService.Describe:input_type -> pluggableharness.agent.model.v1.DescribeRequest + 6, // 48: pluggableharness.agent.model.v1.ModelService.GetCapabilities:output_type -> pluggableharness.agent.model.v1.GetCapabilitiesResponse + 9, // 49: pluggableharness.agent.model.v1.ModelService.Configure:output_type -> pluggableharness.agent.model.v1.ConfigureResponse + 23, // 50: pluggableharness.agent.model.v1.ModelService.StreamCompletion:output_type -> pluggableharness.agent.model.v1.StreamEvent + 26, // 51: pluggableharness.agent.model.v1.ModelService.CountTokens:output_type -> pluggableharness.agent.model.v1.CountTokensResponse + 28, // 52: pluggableharness.agent.model.v1.ModelService.Render:output_type -> pluggableharness.agent.model.v1.RenderResponse + 11, // 53: pluggableharness.agent.model.v1.ModelService.Describe:output_type -> pluggableharness.agent.model.v1.DescribeResponse + 48, // [48:54] is the sub-list for method output_type + 42, // [42:48] is the sub-list for method input_type + 42, // [42:42] is the sub-list for extension type_name + 42, // [42:42] is the sub-list for extension extendee + 0, // [0:42] is the sub-list for field type_name } func init() { file_pluggableharness_agent_model_v1_model_proto_init() } @@ -2743,12 +3458,18 @@ func file_pluggableharness_agent_model_v1_model_proto_init() { if File_pluggableharness_agent_model_v1_model_proto != nil { return } - file_pluggableharness_agent_model_v1_model_proto_msgTypes[5].OneofWrappers = []any{} file_pluggableharness_agent_model_v1_model_proto_msgTypes[7].OneofWrappers = []any{} file_pluggableharness_agent_model_v1_model_proto_msgTypes[9].OneofWrappers = []any{} file_pluggableharness_agent_model_v1_model_proto_msgTypes[11].OneofWrappers = []any{} file_pluggableharness_agent_model_v1_model_proto_msgTypes[13].OneofWrappers = []any{} file_pluggableharness_agent_model_v1_model_proto_msgTypes[14].OneofWrappers = []any{ + (*CacheBreakpoint_AfterAssembledContext_)(nil), + (*CacheBreakpoint_AfterTools_)(nil), + (*CacheBreakpoint_AfterMessageIndex)(nil), + } + file_pluggableharness_agent_model_v1_model_proto_msgTypes[16].OneofWrappers = []any{} + file_pluggableharness_agent_model_v1_model_proto_msgTypes[17].OneofWrappers = []any{} + file_pluggableharness_agent_model_v1_model_proto_msgTypes[18].OneofWrappers = []any{ (*StreamEvent_TextDelta_)(nil), (*StreamEvent_ThinkingDelta_)(nil), (*StreamEvent_ThinkingSignature_)(nil), @@ -2759,15 +3480,16 @@ func file_pluggableharness_agent_model_v1_model_proto_init() { (*StreamEvent_Stop_)(nil), (*StreamEvent_Error_)(nil), } - file_pluggableharness_agent_model_v1_model_proto_msgTypes[15].OneofWrappers = []any{} - file_pluggableharness_agent_model_v1_model_proto_msgTypes[20].OneofWrappers = []any{} + file_pluggableharness_agent_model_v1_model_proto_msgTypes[19].OneofWrappers = []any{} + file_pluggableharness_agent_model_v1_model_proto_msgTypes[24].OneofWrappers = []any{} + file_pluggableharness_agent_model_v1_model_proto_msgTypes[35].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_pluggableharness_agent_model_v1_model_proto_rawDesc), len(file_pluggableharness_agent_model_v1_model_proto_rawDesc)), - NumEnums: 4, - NumMessages: 31, + NumEnums: 5, + NumMessages: 37, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/model/proto/v1/model_grpc.pb.go b/pkg/model/proto/v1/model_grpc.pb.go index 4057cd7..738d818 100644 --- a/pkg/model/proto/v1/model_grpc.pb.go +++ b/pkg/model/proto/v1/model_grpc.pb.go @@ -34,6 +34,7 @@ const ( ModelService_StreamCompletion_FullMethodName = "/pluggableharness.agent.model.v1.ModelService/StreamCompletion" ModelService_CountTokens_FullMethodName = "/pluggableharness.agent.model.v1.ModelService/CountTokens" ModelService_Render_FullMethodName = "/pluggableharness.agent.model.v1.ModelService/Render" + ModelService_Describe_FullMethodName = "/pluggableharness.agent.model.v1.ModelService/Describe" ) // ModelServiceClient is the client API for ModelService service. @@ -91,6 +92,17 @@ type ModelServiceClient interface { // §7 notes most model-provider payloads (plain text, tool calls) render // fine under the kernel's generic fallback when this RPC is absent. Render(ctx context.Context, in *RenderRequest, opts ...grpc.CallOption) (*RenderResponse, error) + // Describe reports this plugin build's own identity — {name, version, + // source, category, protocol_version} — directly from the running + // process, rather than the kernel inferring it from a lock-file row. + // This matters specifically for a dev_overrides binary + // (configuration/settings-and-global.md#dev_overrides), which bypasses + // the registry/lock-file resolution path entirely and so has no + // provider "" { ... } lock entry to read identity from; see + // configuration/lock-file.md's dev_overrides note for the canonical + // explanation, shared verbatim across all six category protocols that + // gain this RPC in this same protocol revision. + Describe(ctx context.Context, in *DescribeRequest, opts ...grpc.CallOption) (*DescribeResponse, error) } type modelServiceClient struct { @@ -160,6 +172,16 @@ func (c *modelServiceClient) Render(ctx context.Context, in *RenderRequest, opts return out, nil } +func (c *modelServiceClient) Describe(ctx context.Context, in *DescribeRequest, opts ...grpc.CallOption) (*DescribeResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DescribeResponse) + err := c.cc.Invoke(ctx, ModelService_Describe_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // ModelServiceServer is the server API for ModelService service. // All implementations must embed UnimplementedModelServiceServer // for forward compatibility. @@ -215,6 +237,17 @@ type ModelServiceServer interface { // §7 notes most model-provider payloads (plain text, tool calls) render // fine under the kernel's generic fallback when this RPC is absent. Render(context.Context, *RenderRequest) (*RenderResponse, error) + // Describe reports this plugin build's own identity — {name, version, + // source, category, protocol_version} — directly from the running + // process, rather than the kernel inferring it from a lock-file row. + // This matters specifically for a dev_overrides binary + // (configuration/settings-and-global.md#dev_overrides), which bypasses + // the registry/lock-file resolution path entirely and so has no + // provider "" { ... } lock entry to read identity from; see + // configuration/lock-file.md's dev_overrides note for the canonical + // explanation, shared verbatim across all six category protocols that + // gain this RPC in this same protocol revision. + Describe(context.Context, *DescribeRequest) (*DescribeResponse, error) mustEmbedUnimplementedModelServiceServer() } @@ -240,6 +273,9 @@ func (UnimplementedModelServiceServer) CountTokens(context.Context, *CountTokens func (UnimplementedModelServiceServer) Render(context.Context, *RenderRequest) (*RenderResponse, error) { return nil, status.Error(codes.Unimplemented, "method Render not implemented") } +func (UnimplementedModelServiceServer) Describe(context.Context, *DescribeRequest) (*DescribeResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Describe not implemented") +} func (UnimplementedModelServiceServer) mustEmbedUnimplementedModelServiceServer() {} func (UnimplementedModelServiceServer) testEmbeddedByValue() {} @@ -344,6 +380,24 @@ func _ModelService_Render_Handler(srv interface{}, ctx context.Context, dec func return interceptor(ctx, in, info, handler) } +func _ModelService_Describe_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DescribeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ModelServiceServer).Describe(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ModelService_Describe_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ModelServiceServer).Describe(ctx, req.(*DescribeRequest)) + } + return interceptor(ctx, in, info, handler) +} + // ModelService_ServiceDesc is the grpc.ServiceDesc for ModelService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -367,6 +421,10 @@ var ModelService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Render", Handler: _ModelService_Render_Handler, }, + { + MethodName: "Describe", + Handler: _ModelService_Describe_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/pkg/plan/proto/v1/plan.pb.go b/pkg/plan/proto/v1/plan.pb.go index e3269db..f3a463a 100644 --- a/pkg/plan/proto/v1/plan.pb.go +++ b/pkg/plan/proto/v1/plan.pb.go @@ -15,6 +15,7 @@ package planv1 import ( + v11 "github.com/pluggableharness/agent/pkg/render/proto/v1" v1 "github.com/pluggableharness/agent/pkg/tool/proto/v1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" @@ -197,7 +198,24 @@ type PlanItem struct { Decision PlanDecision `protobuf:"varint,6,opt,name=decision,proto3,enum=pluggableharness.agent.plan.v1.PlanDecision" json:"decision,omitempty"` // The name of the policy rule or subscriber that produced `decision`, // for audit (state-backend.md §4.4 plan_items.decided_by). - DecidedBy string `protobuf:"bytes,7,opt,name=decided_by,json=decidedBy,proto3" json:"decided_by,omitempty"` + DecidedBy string `protobuf:"bytes,7,opt,name=decided_by,json=decidedBy,proto3" json:"decided_by,omitempty"` + // Snapshot of ToolSchema.kind (tool/data-types.md#toolschema) for the + // operation this item calls, at plan-construction time. + Kind v1.ToolKind `protobuf:"varint,8,opt,name=kind,proto3,enum=pluggableharness.agent.tool.v1.ToolKind" json:"kind,omitempty"` + // Snapshot of ToolSchema.risk, at plan-construction time. + Risk v1.RiskClass `protobuf:"varint,9,opt,name=risk,proto3,enum=pluggableharness.agent.tool.v1.RiskClass" json:"risk,omitempty"` + // Snapshot of ToolSchema.description, at plan-construction time. + Description string `protobuf:"bytes,10,opt,name=description,proto3" json:"description,omitempty"` + // The tool provider's dry-run preview of this call's effect, when the + // provider implements ToolService.Preview (tool/protocol.md#preview). + // The kernel calls Preview at plan-construction time for + // TOOL_KIND_RESOURCE items whose provider implements it; absent when + // the provider has no Preview implementation, in which case a frontend + // falls back to rendering the raw `input` above. Pinned to + // render.v1.RenderTree — the exact type ToolService.Preview's own + // response carries, so this stored snapshot and the RPC's live output + // share one wire shape (agent-loop/plan-apply-gate.md#preview-flow). + Preview *v11.RenderTree `protobuf:"bytes,11,opt,name=preview,proto3,oneof" json:"preview,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -281,6 +299,34 @@ func (x *PlanItem) GetDecidedBy() string { return "" } +func (x *PlanItem) GetKind() v1.ToolKind { + if x != nil { + return x.Kind + } + return v1.ToolKind(0) +} + +func (x *PlanItem) GetRisk() v1.RiskClass { + if x != nil { + return x.Risk + } + return v1.RiskClass(0) +} + +func (x *PlanItem) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *PlanItem) GetPreview() *v11.RenderTree { + if x != nil { + return x.Preview + } + return nil +} + // Plan collects every policy-evaluated call identified during one turn. type Plan struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -522,7 +568,7 @@ var File_pluggableharness_agent_plan_v1_plan_proto protoreflect.FileDescriptor const file_pluggableharness_agent_plan_v1_plan_proto_rawDesc = "" + "\n" + - ")pluggableharness/agent/plan/v1/plan.proto\x12\x1epluggableharness.agent.plan.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a)pluggableharness/agent/tool/v1/tool.proto\"\x8d\x02\n" + + ")pluggableharness/agent/plan/v1/plan.proto\x12\x1epluggableharness.agent.plan.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a-pluggableharness/agent/render/v1/render.proto\x1a)pluggableharness/agent/tool/v1/tool.proto\"\x85\x04\n" + "\bPlanItem\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12 \n" + "\ftool_call_id\x18\x02 \x01(\tR\n" + @@ -532,7 +578,14 @@ const file_pluggableharness_agent_plan_v1_plan_proto_rawDesc = "" + "\x05input\x18\x05 \x01(\v2\x17.google.protobuf.StructR\x05input\x12H\n" + "\bdecision\x18\x06 \x01(\x0e2,.pluggableharness.agent.plan.v1.PlanDecisionR\bdecision\x12\x1d\n" + "\n" + - "decided_by\x18\a \x01(\tR\tdecidedBy\"_\n" + + "decided_by\x18\a \x01(\tR\tdecidedBy\x12<\n" + + "\x04kind\x18\b \x01(\x0e2(.pluggableharness.agent.tool.v1.ToolKindR\x04kind\x12=\n" + + "\x04risk\x18\t \x01(\x0e2).pluggableharness.agent.tool.v1.RiskClassR\x04risk\x12 \n" + + "\vdescription\x18\n" + + " \x01(\tR\vdescription\x12K\n" + + "\apreview\x18\v \x01(\v2,.pluggableharness.agent.render.v1.RenderTreeH\x00R\apreview\x88\x01\x01B\n" + + "\n" + + "\b_preview\"_\n" + "\x04Plan\x12\x17\n" + "\aturn_id\x18\x01 \x01(\tR\x06turnId\x12>\n" + "\x05items\x18\x02 \x03(\v2(.pluggableharness.agent.plan.v1.PlanItemR\x05items\"\xd8\x04\n" + @@ -585,22 +638,28 @@ var file_pluggableharness_agent_plan_v1_plan_proto_goTypes = []any{ (*ApplyResult)(nil), // 4: pluggableharness.agent.plan.v1.ApplyResult (*ApplyResult_ApplyItem)(nil), // 5: pluggableharness.agent.plan.v1.ApplyResult.ApplyItem (*structpb.Struct)(nil), // 6: google.protobuf.Struct - (*v1.ToolResult)(nil), // 7: pluggableharness.agent.tool.v1.ToolResult - (*v1.ToolError)(nil), // 8: pluggableharness.agent.tool.v1.ToolError + (v1.ToolKind)(0), // 7: pluggableharness.agent.tool.v1.ToolKind + (v1.RiskClass)(0), // 8: pluggableharness.agent.tool.v1.RiskClass + (*v11.RenderTree)(nil), // 9: pluggableharness.agent.render.v1.RenderTree + (*v1.ToolResult)(nil), // 10: pluggableharness.agent.tool.v1.ToolResult + (*v1.ToolError)(nil), // 11: pluggableharness.agent.tool.v1.ToolError } var file_pluggableharness_agent_plan_v1_plan_proto_depIdxs = []int32{ - 6, // 0: pluggableharness.agent.plan.v1.PlanItem.input:type_name -> google.protobuf.Struct - 0, // 1: pluggableharness.agent.plan.v1.PlanItem.decision:type_name -> pluggableharness.agent.plan.v1.PlanDecision - 2, // 2: pluggableharness.agent.plan.v1.Plan.items:type_name -> pluggableharness.agent.plan.v1.PlanItem - 5, // 3: pluggableharness.agent.plan.v1.ApplyResult.items:type_name -> pluggableharness.agent.plan.v1.ApplyResult.ApplyItem - 1, // 4: pluggableharness.agent.plan.v1.ApplyResult.ApplyItem.outcome:type_name -> pluggableharness.agent.plan.v1.ApplyResult.ApplyOutcome - 7, // 5: pluggableharness.agent.plan.v1.ApplyResult.ApplyItem.tool_result:type_name -> pluggableharness.agent.tool.v1.ToolResult - 8, // 6: pluggableharness.agent.plan.v1.ApplyResult.ApplyItem.tool_error:type_name -> pluggableharness.agent.tool.v1.ToolError - 7, // [7:7] is the sub-list for method output_type - 7, // [7:7] is the sub-list for method input_type - 7, // [7:7] is the sub-list for extension type_name - 7, // [7:7] is the sub-list for extension extendee - 0, // [0:7] is the sub-list for field type_name + 6, // 0: pluggableharness.agent.plan.v1.PlanItem.input:type_name -> google.protobuf.Struct + 0, // 1: pluggableharness.agent.plan.v1.PlanItem.decision:type_name -> pluggableharness.agent.plan.v1.PlanDecision + 7, // 2: pluggableharness.agent.plan.v1.PlanItem.kind:type_name -> pluggableharness.agent.tool.v1.ToolKind + 8, // 3: pluggableharness.agent.plan.v1.PlanItem.risk:type_name -> pluggableharness.agent.tool.v1.RiskClass + 9, // 4: pluggableharness.agent.plan.v1.PlanItem.preview:type_name -> pluggableharness.agent.render.v1.RenderTree + 2, // 5: pluggableharness.agent.plan.v1.Plan.items:type_name -> pluggableharness.agent.plan.v1.PlanItem + 5, // 6: pluggableharness.agent.plan.v1.ApplyResult.items:type_name -> pluggableharness.agent.plan.v1.ApplyResult.ApplyItem + 1, // 7: pluggableharness.agent.plan.v1.ApplyResult.ApplyItem.outcome:type_name -> pluggableharness.agent.plan.v1.ApplyResult.ApplyOutcome + 10, // 8: pluggableharness.agent.plan.v1.ApplyResult.ApplyItem.tool_result:type_name -> pluggableharness.agent.tool.v1.ToolResult + 11, // 9: pluggableharness.agent.plan.v1.ApplyResult.ApplyItem.tool_error:type_name -> pluggableharness.agent.tool.v1.ToolError + 10, // [10:10] is the sub-list for method output_type + 10, // [10:10] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name } func init() { file_pluggableharness_agent_plan_v1_plan_proto_init() } @@ -608,6 +667,7 @@ func file_pluggableharness_agent_plan_v1_plan_proto_init() { if File_pluggableharness_agent_plan_v1_plan_proto != nil { return } + file_pluggableharness_agent_plan_v1_plan_proto_msgTypes[0].OneofWrappers = []any{} file_pluggableharness_agent_plan_v1_plan_proto_msgTypes[3].OneofWrappers = []any{ (*ApplyResult_ApplyItem_ToolResult)(nil), (*ApplyResult_ApplyItem_ToolError)(nil), diff --git a/pkg/render/proto/v1/render.pb.go b/pkg/render/proto/v1/render.pb.go index e673a26..67344ee 100644 --- a/pkg/render/proto/v1/render.pb.go +++ b/pkg/render/proto/v1/render.pb.go @@ -1283,7 +1283,13 @@ type ActionNode struct { // runtime-defined per tool (see .claude/rules/proto.md's Struct // carve-out) — validated against the tool's input_schema on dispatch, // same as any other Invoke call. - Args *structpb.Struct `protobuf:"bytes,4,opt,name=args,proto3" json:"args,omitempty"` + Args *structpb.Struct `protobuf:"bytes,4,opt,name=args,proto3" json:"args,omitempty"` + // The declared name of the tool provider plugin `tool_name` belongs to. + // tool_name is only unique per provider (matching plan.v1.PlanItem's own + // provider/tool_name pairing), so this disambiguates which provider's + // operation to invoke on activation. Echoed unchanged onto the resulting + // ClientEvent.ActionTrigger.provider (frontend.md §"Client events"). + Provider string `protobuf:"bytes,5,opt,name=provider,proto3" json:"provider,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1346,6 +1352,13 @@ func (x *ActionNode) GetArgs() *structpb.Struct { return nil } +func (x *ActionNode) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + var File_pluggableharness_agent_render_v1_render_proto protoreflect.FileDescriptor const file_pluggableharness_agent_render_v1_render_proto_rawDesc = "" + @@ -1415,13 +1428,14 @@ const file_pluggableharness_agent_render_v1_render_proto_rawDesc = "" + "\x0eSubSessionNode\x12\x1d\n" + "\n" + "session_id\x18\x01 \x01(\tR\tsessionId\x12\x18\n" + - "\asummary\x18\x02 \x01(\tR\asummary\"|\n" + + "\asummary\x18\x02 \x01(\tR\asummary\"\x98\x01\n" + "\n" + "ActionNode\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" + "\x05label\x18\x02 \x01(\tR\x05label\x12\x1b\n" + "\ttool_name\x18\x03 \x01(\tR\btoolName\x12+\n" + - "\x04args\x18\x04 \x01(\v2\x17.google.protobuf.StructR\x04args*\xa1\x01\n" + + "\x04args\x18\x04 \x01(\v2\x17.google.protobuf.StructR\x04args\x12\x1a\n" + + "\bprovider\x18\x05 \x01(\tR\bprovider*\xa1\x01\n" + "\x06Region\x12\x16\n" + "\x12REGION_UNSPECIFIED\x10\x00\x12\x14\n" + "\x10REGION_MAIN_CHAT\x10\x01\x12\x12\n" + diff --git a/pkg/session/proto/v1/session.pb.go b/pkg/session/proto/v1/session.pb.go index a6f06a8..1914f77 100644 --- a/pkg/session/proto/v1/session.pb.go +++ b/pkg/session/proto/v1/session.pb.go @@ -14,6 +14,7 @@ package sessionv1 import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" unsafe "unsafe" @@ -107,11 +108,145 @@ func (SessionStatus) EnumDescriptor() ([]byte, []int) { return file_pluggableharness_agent_session_v1_session_proto_rawDescGZIP(), []int{0} } +// SessionInfo is a session's shareable summary, mirroring +// state-backend.md's session_meta row plus a cheap cost_ledger SUM. Used +// by frontend.md's SessionCreated/SessionAttached/SessionList ServerEvent +// variants — the frontend protocol's read-only view of session lifecycle +// state, never mutated by a frontend directly. +type SessionInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The session's id. ULID, matches the session's sqlite filename stem + // (state-backend.md §"File layout"). + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + // The parent session's id, when this is a sub-agent session + // (agent-loop/subagents.md). Absent for a root session — mirrors + // session_meta.parent_session_id. + ParentSessionId *string `protobuf:"bytes,2,opt,name=parent_session_id,json=parentSessionId,proto3,oneof" json:"parent_session_id,omitempty"` + // The agent.hcl profile this session was created under. + Profile string `protobuf:"bytes,3,opt,name=profile,proto3" json:"profile,omitempty"` + // The session's current lifecycle status. + Status SessionStatus `protobuf:"varint,4,opt,name=status,proto3,enum=pluggableharness.agent.session.v1.SessionStatus" json:"status,omitempty"` + // The session's depth in its ancestor chain — mirrors + // session_meta.depth (agent-loop/subagents.md#depth-limits). + Depth int32 `protobuf:"varint,5,opt,name=depth,proto3" json:"depth,omitempty"` + // When the session was created. Mirrors session_meta.started_at. + StartedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=started_at,json=startedAt,proto3" json:"started_at,omitempty"` + // When the session reached a terminal status. Absent while RUNNING. + // Mirrors session_meta.ended_at. + EndedAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=ended_at,json=endedAt,proto3,oneof" json:"ended_at,omitempty"` + // The session's running total spend — SUM(cost_usd) over + // state-backend.md's cost_ledger table for this session. Absent if no + // cost has been incurred yet, rather than a meaningless zero. + CostUsd *float64 `protobuf:"fixed64,8,opt,name=cost_usd,json=costUsd,proto3,oneof" json:"cost_usd,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SessionInfo) Reset() { + *x = SessionInfo{} + mi := &file_pluggableharness_agent_session_v1_session_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SessionInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SessionInfo) ProtoMessage() {} + +func (x *SessionInfo) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_session_v1_session_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SessionInfo.ProtoReflect.Descriptor instead. +func (*SessionInfo) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_session_v1_session_proto_rawDescGZIP(), []int{0} +} + +func (x *SessionInfo) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *SessionInfo) GetParentSessionId() string { + if x != nil && x.ParentSessionId != nil { + return *x.ParentSessionId + } + return "" +} + +func (x *SessionInfo) GetProfile() string { + if x != nil { + return x.Profile + } + return "" +} + +func (x *SessionInfo) GetStatus() SessionStatus { + if x != nil { + return x.Status + } + return SessionStatus_SESSION_STATUS_UNSPECIFIED +} + +func (x *SessionInfo) GetDepth() int32 { + if x != nil { + return x.Depth + } + return 0 +} + +func (x *SessionInfo) GetStartedAt() *timestamppb.Timestamp { + if x != nil { + return x.StartedAt + } + return nil +} + +func (x *SessionInfo) GetEndedAt() *timestamppb.Timestamp { + if x != nil { + return x.EndedAt + } + return nil +} + +func (x *SessionInfo) GetCostUsd() float64 { + if x != nil && x.CostUsd != nil { + return *x.CostUsd + } + return 0 +} + var File_pluggableharness_agent_session_v1_session_proto protoreflect.FileDescriptor const file_pluggableharness_agent_session_v1_session_proto_rawDesc = "" + "\n" + - "/pluggableharness/agent/session/v1/session.proto\x12!pluggableharness.agent.session.v1*\x98\x02\n" + + "/pluggableharness/agent/session/v1/session.proto\x12!pluggableharness.agent.session.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\x9e\x03\n" + + "\vSessionInfo\x12\x1d\n" + + "\n" + + "session_id\x18\x01 \x01(\tR\tsessionId\x12/\n" + + "\x11parent_session_id\x18\x02 \x01(\tH\x00R\x0fparentSessionId\x88\x01\x01\x12\x18\n" + + "\aprofile\x18\x03 \x01(\tR\aprofile\x12H\n" + + "\x06status\x18\x04 \x01(\x0e20.pluggableharness.agent.session.v1.SessionStatusR\x06status\x12\x14\n" + + "\x05depth\x18\x05 \x01(\x05R\x05depth\x129\n" + + "\n" + + "started_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\tstartedAt\x12:\n" + + "\bended_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampH\x01R\aendedAt\x88\x01\x01\x12\x1e\n" + + "\bcost_usd\x18\b \x01(\x01H\x02R\acostUsd\x88\x01\x01B\x14\n" + + "\x12_parent_session_idB\v\n" + + "\t_ended_atB\v\n" + + "\t_cost_usd*\x98\x02\n" + "\rSessionStatus\x12\x1e\n" + "\x1aSESSION_STATUS_UNSPECIFIED\x10\x00\x12\x1a\n" + "\x16SESSION_STATUS_RUNNING\x10\x01\x12\x1c\n" + @@ -135,15 +270,21 @@ func file_pluggableharness_agent_session_v1_session_proto_rawDescGZIP() []byte { } var file_pluggableharness_agent_session_v1_session_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_pluggableharness_agent_session_v1_session_proto_msgTypes = make([]protoimpl.MessageInfo, 1) var file_pluggableharness_agent_session_v1_session_proto_goTypes = []any{ - (SessionStatus)(0), // 0: pluggableharness.agent.session.v1.SessionStatus + (SessionStatus)(0), // 0: pluggableharness.agent.session.v1.SessionStatus + (*SessionInfo)(nil), // 1: pluggableharness.agent.session.v1.SessionInfo + (*timestamppb.Timestamp)(nil), // 2: google.protobuf.Timestamp } var file_pluggableharness_agent_session_v1_session_proto_depIdxs = []int32{ - 0, // [0:0] is the sub-list for method output_type - 0, // [0:0] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name + 0, // 0: pluggableharness.agent.session.v1.SessionInfo.status:type_name -> pluggableharness.agent.session.v1.SessionStatus + 2, // 1: pluggableharness.agent.session.v1.SessionInfo.started_at:type_name -> google.protobuf.Timestamp + 2, // 2: pluggableharness.agent.session.v1.SessionInfo.ended_at:type_name -> google.protobuf.Timestamp + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name } func init() { file_pluggableharness_agent_session_v1_session_proto_init() } @@ -151,19 +292,21 @@ func file_pluggableharness_agent_session_v1_session_proto_init() { if File_pluggableharness_agent_session_v1_session_proto != nil { return } + file_pluggableharness_agent_session_v1_session_proto_msgTypes[0].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_pluggableharness_agent_session_v1_session_proto_rawDesc), len(file_pluggableharness_agent_session_v1_session_proto_rawDesc)), NumEnums: 1, - NumMessages: 0, + NumMessages: 1, NumExtensions: 0, NumServices: 0, }, GoTypes: file_pluggableharness_agent_session_v1_session_proto_goTypes, DependencyIndexes: file_pluggableharness_agent_session_v1_session_proto_depIdxs, EnumInfos: file_pluggableharness_agent_session_v1_session_proto_enumTypes, + MessageInfos: file_pluggableharness_agent_session_v1_session_proto_msgTypes, }.Build() File_pluggableharness_agent_session_v1_session_proto = out.File file_pluggableharness_agent_session_v1_session_proto_goTypes = nil diff --git a/pkg/tool/proto/v1/tool.pb.go b/pkg/tool/proto/v1/tool.pb.go index 74ed8f7..35f1fbe 100644 --- a/pkg/tool/proto/v1/tool.pb.go +++ b/pkg/tool/proto/v1/tool.pb.go @@ -12,12 +12,14 @@ package toolv1 import ( + v12 "github.com/pluggableharness/agent/pkg/common/proto/v1" v11 "github.com/pluggableharness/agent/pkg/config/proto/v1" - v13 "github.com/pluggableharness/agent/pkg/render/proto/v1" - v12 "github.com/pluggableharness/agent/pkg/schema/proto/v1" + v14 "github.com/pluggableharness/agent/pkg/render/proto/v1" + v13 "github.com/pluggableharness/agent/pkg/schema/proto/v1" v1 "github.com/pluggableharness/agent/pkg/slashcommand/proto/v1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" structpb "google.golang.org/protobuf/types/known/structpb" reflect "reflect" sync "sync" @@ -373,9 +375,15 @@ type GetSchemaResponse struct { SlashCommands []*v1.SlashCommandSpec `protobuf:"bytes,2,rep,name=slash_commands,json=slashCommands,proto3" json:"slash_commands,omitempty"` // This provider's agent.hcl config schema, per configuration.md §4 — // what fields Configure's request may be decoded from. - ConfigSchema *v11.ConfigSchema `protobuf:"bytes,3,opt,name=config_schema,json=configSchema,proto3" json:"config_schema,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + ConfigSchema *v11.ConfigSchema `protobuf:"bytes,3,opt,name=config_schema,json=configSchema,proto3" json:"config_schema,omitempty"` + // Which of the eight dispatchable hook points (hook/v1.HookPoint) this + // provider subscribes a HookSubscriberService to, per this provider's own + // agent.hcl hook{} blocks. Lets the kernel validate a hook{} declaration + // at config-load time instead of discovering an unsupported subscription + // only when that hook point first fires. + SupportedHookPoints []v12.HookPoint `protobuf:"varint,4,rep,packed,name=supported_hook_points,json=supportedHookPoints,proto3,enum=pluggableharness.agent.common.v1.HookPoint" json:"supported_hook_points,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetSchemaResponse) Reset() { @@ -429,6 +437,13 @@ func (x *GetSchemaResponse) GetConfigSchema() *v11.ConfigSchema { return nil } +func (x *GetSchemaResponse) GetSupportedHookPoints() []v12.HookPoint { + if x != nil { + return x.SupportedHookPoints + } + return nil +} + // ConfigureRequest wraps this provider's already-decoded agent.hcl config. type ConfigureRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -597,16 +612,27 @@ type ToolSchema struct { Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` // MUST — the common JSON-Schema subset per model.md §6, describing the // shape of ToolCall.arguments for this operation. - InputSchema *v12.Schema `protobuf:"bytes,5,opt,name=input_schema,json=inputSchema,proto3" json:"input_schema,omitempty"` + InputSchema *v13.Schema `protobuf:"bytes,5,opt,name=input_schema,json=inputSchema,proto3" json:"input_schema,omitempty"` // MUST — the common JSON-Schema subset per model.md §6, describing the // shape of ToolResult.payload for this operation. - OutputSchema *v12.Schema `protobuf:"bytes,6,opt,name=output_schema,json=outputSchema,proto3" json:"output_schema,omitempty"` + OutputSchema *v13.Schema `protobuf:"bytes,6,opt,name=output_schema,json=outputSchema,proto3" json:"output_schema,omitempty"` // MUST — true if Invoke may emit intermediate ToolEvents (output_chunk, // progress, partial_result) before the terminal event; false if Invoke // always emits exactly one terminal event with no lead-up. Streaming bool `protobuf:"varint,7,opt,name=streaming,proto3" json:"streaming,omitempty"` // MUST, except MUST NOT be meaningfully set for TOOL_KIND_INTERACTIVE. - Concurrency *ConcurrencySpec `protobuf:"bytes,8,opt,name=concurrency,proto3" json:"concurrency,omitempty"` + Concurrency *ConcurrencySpec `protobuf:"bytes,8,opt,name=concurrency,proto3" json:"concurrency,omitempty"` + // SHOULD — the deadline the kernel applies to Invoke for this operation + // absent an agent.hcl override. Absent means the kernel's global default + // applies instead (configuration/settings-and-global.md). + DefaultTimeout *durationpb.Duration `protobuf:"bytes,9,opt,name=default_timeout,json=defaultTimeout,proto3,oneof" json:"default_timeout,omitempty"` + // True iff re-running this operation with identical arguments cannot + // produce a different end state than running it once. Gates whether the + // kernel MAY auto-retry a retryable ToolError for a TOOL_KIND_RESOURCE + // operation — see conformance.md#error-taxonomy's retry interaction. + // TOOL_KIND_DATA_SOURCE operations are implicitly safe to retry + // regardless of this field. + Idempotent bool `protobuf:"varint,10,opt,name=idempotent,proto3" json:"idempotent,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -669,14 +695,14 @@ func (x *ToolSchema) GetDescription() string { return "" } -func (x *ToolSchema) GetInputSchema() *v12.Schema { +func (x *ToolSchema) GetInputSchema() *v13.Schema { if x != nil { return x.InputSchema } return nil } -func (x *ToolSchema) GetOutputSchema() *v12.Schema { +func (x *ToolSchema) GetOutputSchema() *v13.Schema { if x != nil { return x.OutputSchema } @@ -697,6 +723,20 @@ func (x *ToolSchema) GetConcurrency() *ConcurrencySpec { return nil } +func (x *ToolSchema) GetDefaultTimeout() *durationpb.Duration { + if x != nil { + return x.DefaultTimeout + } + return nil +} + +func (x *ToolSchema) GetIdempotent() bool { + if x != nil { + return x.Idempotent + } + return false +} + // InvokeRequest wraps the call to execute. A thin per-RPC envelope around // ToolCall, which keeps its own rich structure independent of the RPC // signature. @@ -804,7 +844,15 @@ type ToolCall struct { ToolName string `protobuf:"bytes,2,opt,name=tool_name,json=toolName,proto3" json:"tool_name,omitempty"` // MUST — already-parsed JSON conforming to that ToolSchema's // input_schema. - Arguments *structpb.Struct `protobuf:"bytes,3,opt,name=arguments,proto3" json:"arguments,omitempty"` + Arguments *structpb.Struct `protobuf:"bytes,3,opt,name=arguments,proto3" json:"arguments,omitempty"` + // MUST be set by the kernel. Carries the session_id/turn_id this call + // executes for — what the plugin echoes back on its own + // KernelCallbackService.Emit/Log calls for attribution — and the + // session's working_directory, which any process-backed operation + // (the reference catalog's exec.bash, read_file, and similar) MUST + // resolve relative-path arguments against. See + // pluggableharness.agent.common.v1.CallContext. + CallContext *v12.CallContext `protobuf:"bytes,4,opt,name=call_context,json=callContext,proto3" json:"call_context,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -860,6 +908,13 @@ func (x *ToolCall) GetArguments() *structpb.Struct { return nil } +func (x *ToolCall) GetCallContext() *v12.CallContext { + if x != nil { + return x.CallContext + } + return nil +} + // ToolEvent is one message in the stream Invoke returns, per tool.md §4. // Exactly one of `result`/`error` closes the stream; `output_chunk`, // `progress`, and `partial_result` MAY each appear zero or more times @@ -1146,7 +1201,12 @@ func (x *ToolError) GetDetails() *structpb.Struct { type RenderRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The opaque emitted payload to render. - Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"` + Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"` + // The schema version the payload was emitted under, per + // ../frontend/render-tree.md#schema-versioning. Lets a Render + // implementation decode a payload emitted by an older build of this + // same plugin. + SchemaVersion string `protobuf:"bytes,2,opt,name=schema_version,json=schemaVersion,proto3" json:"schema_version,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1188,13 +1248,20 @@ func (x *RenderRequest) GetPayload() []byte { return nil } +func (x *RenderRequest) GetSchemaVersion() string { + if x != nil { + return x.SchemaVersion + } + return "" +} + // RenderResponse wraps the rendered tree. A thin per-RPC envelope around // RenderTree, which keeps its own rich structure independent of the RPC // signature. type RenderResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The rendered tree. - Tree *v13.RenderTree `protobuf:"bytes,1,opt,name=tree,proto3" json:"tree,omitempty"` + Tree *v14.RenderTree `protobuf:"bytes,1,opt,name=tree,proto3" json:"tree,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1229,13 +1296,203 @@ func (*RenderResponse) Descriptor() ([]byte, []int) { return file_pluggableharness_agent_tool_v1_tool_proto_rawDescGZIP(), []int{13} } -func (x *RenderResponse) GetTree() *v13.RenderTree { +func (x *RenderResponse) GetTree() *v14.RenderTree { if x != nil { return x.Tree } return nil } +// PreviewRequest wraps the call to describe, per protocol.md#preview. +type PreviewRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The call Preview describes a dry run of. Same shape as an Invoke + // request; Preview MUST NOT execute it. + Call *ToolCall `protobuf:"bytes,1,opt,name=call,proto3" json:"call,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PreviewRequest) Reset() { + *x = PreviewRequest{} + mi := &file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PreviewRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PreviewRequest) ProtoMessage() {} + +func (x *PreviewRequest) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PreviewRequest.ProtoReflect.Descriptor instead. +func (*PreviewRequest) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_tool_v1_tool_proto_rawDescGZIP(), []int{14} +} + +func (x *PreviewRequest) GetCall() *ToolCall { + if x != nil { + return x.Call + } + return nil +} + +// PreviewResponse carries a dry-run, human-readable description of what +// Invoke(call) would do, per protocol.md#preview — e.g. an edit tool +// returns the diff it would apply. Rendered into the plan/apply gate's +// permission UI via PlanItem.preview (pluggableharness.agent.plan.v1, +// a sibling protocol revision) — that field and this response share the +// same pluggableharness.agent.render.v1.RenderTree type by design, so a +// Preview call's output and a plan item's stored preview are +// interchangeable. +type PreviewResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The dry-run preview, rendered as a RenderTree. Producing this MUST NOT + // mutate anything and MUST be side-effect-free — the same guarantee + // TOOL_KIND_DATA_SOURCE operations make, but unconditionally, regardless + // of the call's actual ToolKind. + Preview *v14.RenderTree `protobuf:"bytes,1,opt,name=preview,proto3" json:"preview,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PreviewResponse) Reset() { + *x = PreviewResponse{} + mi := &file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PreviewResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PreviewResponse) ProtoMessage() {} + +func (x *PreviewResponse) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PreviewResponse.ProtoReflect.Descriptor instead. +func (*PreviewResponse) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_tool_v1_tool_proto_rawDescGZIP(), []int{15} +} + +func (x *PreviewResponse) GetPreview() *v14.RenderTree { + if x != nil { + return x.Preview + } + return nil +} + +// DescribeRequest carries no fields — Describe takes no parameters. +type DescribeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeRequest) Reset() { + *x = DescribeRequest{} + mi := &file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeRequest) ProtoMessage() {} + +func (x *DescribeRequest) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeRequest.ProtoReflect.Descriptor instead. +func (*DescribeRequest) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_tool_v1_tool_proto_rawDescGZIP(), []int{16} +} + +// DescribeResponse reports this plugin build's own identity, per +// configuration/lock-file.md's dev_overrides note: a dev_overrides-resolved +// plugin has no lock-file entry for the kernel to read {name, version, +// source, category, protocol_version} from, so the kernel obtains it +// directly from the running process via this RPC instead. +type DescribeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // This plugin build's identity. + Producer *v12.ProducerRef `protobuf:"bytes,1,opt,name=producer,proto3" json:"producer,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeResponse) Reset() { + *x = DescribeResponse{} + mi := &file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeResponse) ProtoMessage() {} + +func (x *DescribeResponse) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeResponse.ProtoReflect.Descriptor instead. +func (*DescribeResponse) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_tool_v1_tool_proto_rawDescGZIP(), []int{17} +} + +func (x *DescribeResponse) GetProducer() *v12.ProducerRef { + if x != nil { + return x.Producer + } + return nil +} + // OutputChunk carries one slice of raw stdout/stderr-shaped output from a // process-backed operation. type ToolEvent_OutputChunk struct { @@ -1250,7 +1507,7 @@ type ToolEvent_OutputChunk struct { func (x *ToolEvent_OutputChunk) Reset() { *x = ToolEvent_OutputChunk{} - mi := &file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[14] + mi := &file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1262,7 +1519,7 @@ func (x *ToolEvent_OutputChunk) String() string { func (*ToolEvent_OutputChunk) ProtoMessage() {} func (x *ToolEvent_OutputChunk) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[14] + mi := &file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1307,7 +1564,7 @@ type ToolEvent_Progress struct { func (x *ToolEvent_Progress) Reset() { *x = ToolEvent_Progress{} - mi := &file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[15] + mi := &file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1319,7 +1576,7 @@ func (x *ToolEvent_Progress) String() string { func (*ToolEvent_Progress) ProtoMessage() {} func (x *ToolEvent_Progress) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[15] + mi := &file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1361,7 +1618,7 @@ type ToolEvent_PartialResult struct { func (x *ToolEvent_PartialResult) Reset() { *x = ToolEvent_PartialResult{} - mi := &file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[16] + mi := &file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1373,7 +1630,7 @@ func (x *ToolEvent_PartialResult) String() string { func (*ToolEvent_PartialResult) ProtoMessage() {} func (x *ToolEvent_PartialResult) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[16] + mi := &file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1413,7 +1670,7 @@ type ToolEvent_ExitStatus struct { func (x *ToolEvent_ExitStatus) Reset() { *x = ToolEvent_ExitStatus{} - mi := &file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[17] + mi := &file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1425,7 +1682,7 @@ func (x *ToolEvent_ExitStatus) String() string { func (*ToolEvent_ExitStatus) ProtoMessage() {} func (x *ToolEvent_ExitStatus) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[17] + mi := &file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1459,19 +1716,20 @@ var File_pluggableharness_agent_tool_v1_tool_proto protoreflect.FileDescriptor const file_pluggableharness_agent_tool_v1_tool_proto_rawDesc = "" + "\n" + - ")pluggableharness/agent/tool/v1/tool.proto\x12\x1epluggableharness.agent.tool.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a-pluggableharness/agent/config/v1/config.proto\x1a-pluggableharness/agent/render/v1/render.proto\x1a-pluggableharness/agent/schema/v1/schema.proto\x1a9pluggableharness/agent/slashcommand/v1/slashcommand.proto\"\x12\n" + - "\x10GetSchemaRequest\"\x8b\x02\n" + + ")pluggableharness/agent/tool/v1/tool.proto\x12\x1epluggableharness.agent.tool.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a-pluggableharness/agent/common/v1/common.proto\x1a-pluggableharness/agent/config/v1/config.proto\x1a-pluggableharness/agent/render/v1/render.proto\x1a-pluggableharness/agent/schema/v1/schema.proto\x1a9pluggableharness/agent/slashcommand/v1/slashcommand.proto\"\x12\n" + + "\x10GetSchemaRequest\"\xec\x02\n" + "\x11GetSchemaResponse\x12@\n" + "\x05tools\x18\x01 \x03(\v2*.pluggableharness.agent.tool.v1.ToolSchemaR\x05tools\x12_\n" + "\x0eslash_commands\x18\x02 \x03(\v28.pluggableharness.agent.slashcommand.v1.SlashCommandSpecR\rslashCommands\x12S\n" + - "\rconfig_schema\x18\x03 \x01(\v2..pluggableharness.agent.config.v1.ConfigSchemaR\fconfigSchema\"C\n" + + "\rconfig_schema\x18\x03 \x01(\v2..pluggableharness.agent.config.v1.ConfigSchemaR\fconfigSchema\x12_\n" + + "\x15supported_hook_points\x18\x04 \x03(\x0e2+.pluggableharness.agent.common.v1.HookPointR\x13supportedHookPoints\"C\n" + "\x10ConfigureRequest\x12/\n" + "\x06config\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x06config\"\x13\n" + "\x11ConfigureResponse\"D\n" + "\x0fConcurrencySpec\x12\x12\n" + "\x04safe\x18\x01 \x01(\bR\x04safe\x12\x1d\n" + "\n" + - "key_fields\x18\x02 \x03(\tR\tkeyFields\"\xcc\x03\n" + + "key_fields\x18\x02 \x03(\tR\tkeyFields\"\xc9\x04\n" + "\n" + "ToolSchema\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12<\n" + @@ -1481,15 +1739,22 @@ const file_pluggableharness_agent_tool_v1_tool_proto_rawDesc = "" + "\finput_schema\x18\x05 \x01(\v2(.pluggableharness.agent.schema.v1.SchemaR\vinputSchema\x12M\n" + "\routput_schema\x18\x06 \x01(\v2(.pluggableharness.agent.schema.v1.SchemaR\foutputSchema\x12\x1c\n" + "\tstreaming\x18\a \x01(\bR\tstreaming\x12Q\n" + - "\vconcurrency\x18\b \x01(\v2/.pluggableharness.agent.tool.v1.ConcurrencySpecR\vconcurrency\"M\n" + + "\vconcurrency\x18\b \x01(\v2/.pluggableharness.agent.tool.v1.ConcurrencySpecR\vconcurrency\x12G\n" + + "\x0fdefault_timeout\x18\t \x01(\v2\x19.google.protobuf.DurationH\x00R\x0edefaultTimeout\x88\x01\x01\x12\x1e\n" + + "\n" + + "idempotent\x18\n" + + " \x01(\bR\n" + + "idempotentB\x12\n" + + "\x10_default_timeout\"M\n" + "\rInvokeRequest\x12<\n" + "\x04call\x18\x01 \x01(\v2(.pluggableharness.agent.tool.v1.ToolCallR\x04call\"Q\n" + "\x0eInvokeResponse\x12?\n" + - "\x05event\x18\x01 \x01(\v2).pluggableharness.agent.tool.v1.ToolEventR\x05event\"n\n" + + "\x05event\x18\x01 \x01(\v2).pluggableharness.agent.tool.v1.ToolEventR\x05event\"\xc0\x01\n" + "\bToolCall\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x1b\n" + "\ttool_name\x18\x02 \x01(\tR\btoolName\x125\n" + - "\targuments\x18\x03 \x01(\v2\x17.google.protobuf.StructR\targuments\"\xf4\x06\n" + + "\targuments\x18\x03 \x01(\v2\x17.google.protobuf.StructR\targuments\x12P\n" + + "\fcall_context\x18\x04 \x01(\v2-.pluggableharness.agent.common.v1.CallContextR\vcallContext\"\xf4\x06\n" + "\tToolEvent\x12Z\n" + "\foutput_chunk\x18\x01 \x01(\v25.pluggableharness.agent.tool.v1.ToolEvent.OutputChunkH\x00R\voutputChunk\x12P\n" + "\bprogress\x18\x02 \x01(\v22.pluggableharness.agent.tool.v1.ToolEvent.ProgressH\x00R\bprogress\x12`\n" + @@ -1522,11 +1787,19 @@ const file_pluggableharness_agent_tool_v1_tool_proto_rawDesc = "" + "\tretryable\x18\x03 \x01(\bR\tretryable\x126\n" + "\adetails\x18\x04 \x01(\v2\x17.google.protobuf.StructH\x00R\adetails\x88\x01\x01B\n" + "\n" + - "\b_details\")\n" + + "\b_details\"P\n" + "\rRenderRequest\x12\x18\n" + - "\apayload\x18\x01 \x01(\fR\apayload\"R\n" + + "\apayload\x18\x01 \x01(\fR\apayload\x12%\n" + + "\x0eschema_version\x18\x02 \x01(\tR\rschemaVersion\"R\n" + "\x0eRenderResponse\x12@\n" + - "\x04tree\x18\x01 \x01(\v2,.pluggableharness.agent.render.v1.RenderTreeR\x04tree*s\n" + + "\x04tree\x18\x01 \x01(\v2,.pluggableharness.agent.render.v1.RenderTreeR\x04tree\"N\n" + + "\x0ePreviewRequest\x12<\n" + + "\x04call\x18\x01 \x01(\v2(.pluggableharness.agent.tool.v1.ToolCallR\x04call\"Y\n" + + "\x0fPreviewResponse\x12F\n" + + "\apreview\x18\x01 \x01(\v2,.pluggableharness.agent.render.v1.RenderTreeR\apreview\"\x11\n" + + "\x0fDescribeRequest\"]\n" + + "\x10DescribeResponse\x12I\n" + + "\bproducer\x18\x01 \x01(\v2-.pluggableharness.agent.common.v1.ProducerRefR\bproducer*s\n" + "\bToolKind\x12\x19\n" + "\x15TOOL_KIND_UNSPECIFIED\x10\x00\x12\x16\n" + "\x12TOOL_KIND_RESOURCE\x10\x01\x12\x19\n" + @@ -1553,12 +1826,14 @@ const file_pluggableharness_agent_tool_v1_tool_proto_rawDesc = "" + "(TOOL_ERROR_CATEGORY_CONCURRENCY_CONFLICT\x10\x06\x12!\n" + "\x1dTOOL_ERROR_CATEGORY_CANCELLED\x10\a\x12'\n" + "#TOOL_ERROR_CATEGORY_PROCESS_CRASHED\x10\b\x12\x1f\n" + - "\x1bTOOL_ERROR_CATEGORY_UNKNOWN\x10\t2\xc5\x03\n" + + "\x1bTOOL_ERROR_CATEGORY_UNKNOWN\x10\t2\xa0\x05\n" + "\vToolService\x12p\n" + "\tGetSchema\x120.pluggableharness.agent.tool.v1.GetSchemaRequest\x1a1.pluggableharness.agent.tool.v1.GetSchemaResponse\x12p\n" + "\tConfigure\x120.pluggableharness.agent.tool.v1.ConfigureRequest\x1a1.pluggableharness.agent.tool.v1.ConfigureResponse\x12i\n" + "\x06Invoke\x12-.pluggableharness.agent.tool.v1.InvokeRequest\x1a..pluggableharness.agent.tool.v1.InvokeResponse0\x01\x12g\n" + - "\x06Render\x12-.pluggableharness.agent.tool.v1.RenderRequest\x1a..pluggableharness.agent.tool.v1.RenderResponseB pluggableharness.agent.tool.v1.ToolSchema - 22, // 1: pluggableharness.agent.tool.v1.GetSchemaResponse.slash_commands:type_name -> pluggableharness.agent.slashcommand.v1.SlashCommandSpec - 23, // 2: pluggableharness.agent.tool.v1.GetSchemaResponse.config_schema:type_name -> pluggableharness.agent.config.v1.ConfigSchema - 24, // 3: pluggableharness.agent.tool.v1.ConfigureRequest.config:type_name -> google.protobuf.Struct - 0, // 4: pluggableharness.agent.tool.v1.ToolSchema.kind:type_name -> pluggableharness.agent.tool.v1.ToolKind - 1, // 5: pluggableharness.agent.tool.v1.ToolSchema.risk:type_name -> pluggableharness.agent.tool.v1.RiskClass - 25, // 6: pluggableharness.agent.tool.v1.ToolSchema.input_schema:type_name -> pluggableharness.agent.schema.v1.Schema - 25, // 7: pluggableharness.agent.tool.v1.ToolSchema.output_schema:type_name -> pluggableharness.agent.schema.v1.Schema - 8, // 8: pluggableharness.agent.tool.v1.ToolSchema.concurrency:type_name -> pluggableharness.agent.tool.v1.ConcurrencySpec - 12, // 9: pluggableharness.agent.tool.v1.InvokeRequest.call:type_name -> pluggableharness.agent.tool.v1.ToolCall - 13, // 10: pluggableharness.agent.tool.v1.InvokeResponse.event:type_name -> pluggableharness.agent.tool.v1.ToolEvent - 24, // 11: pluggableharness.agent.tool.v1.ToolCall.arguments:type_name -> google.protobuf.Struct - 18, // 12: pluggableharness.agent.tool.v1.ToolEvent.output_chunk:type_name -> pluggableharness.agent.tool.v1.ToolEvent.OutputChunk - 19, // 13: pluggableharness.agent.tool.v1.ToolEvent.progress:type_name -> pluggableharness.agent.tool.v1.ToolEvent.Progress - 20, // 14: pluggableharness.agent.tool.v1.ToolEvent.partial_result:type_name -> pluggableharness.agent.tool.v1.ToolEvent.PartialResult - 21, // 15: pluggableharness.agent.tool.v1.ToolEvent.exit_status:type_name -> pluggableharness.agent.tool.v1.ToolEvent.ExitStatus - 14, // 16: pluggableharness.agent.tool.v1.ToolEvent.result:type_name -> pluggableharness.agent.tool.v1.ToolResult - 15, // 17: pluggableharness.agent.tool.v1.ToolEvent.error:type_name -> pluggableharness.agent.tool.v1.ToolError - 24, // 18: pluggableharness.agent.tool.v1.ToolResult.payload:type_name -> google.protobuf.Struct - 3, // 19: pluggableharness.agent.tool.v1.ToolError.category:type_name -> pluggableharness.agent.tool.v1.ToolErrorCategory - 24, // 20: pluggableharness.agent.tool.v1.ToolError.details:type_name -> google.protobuf.Struct - 26, // 21: pluggableharness.agent.tool.v1.RenderResponse.tree:type_name -> pluggableharness.agent.render.v1.RenderTree - 2, // 22: pluggableharness.agent.tool.v1.ToolEvent.OutputChunk.stream:type_name -> pluggableharness.agent.tool.v1.OutputStream - 24, // 23: pluggableharness.agent.tool.v1.ToolEvent.PartialResult.payload:type_name -> google.protobuf.Struct - 4, // 24: pluggableharness.agent.tool.v1.ToolService.GetSchema:input_type -> pluggableharness.agent.tool.v1.GetSchemaRequest - 6, // 25: pluggableharness.agent.tool.v1.ToolService.Configure:input_type -> pluggableharness.agent.tool.v1.ConfigureRequest - 10, // 26: pluggableharness.agent.tool.v1.ToolService.Invoke:input_type -> pluggableharness.agent.tool.v1.InvokeRequest - 16, // 27: pluggableharness.agent.tool.v1.ToolService.Render:input_type -> pluggableharness.agent.tool.v1.RenderRequest - 5, // 28: pluggableharness.agent.tool.v1.ToolService.GetSchema:output_type -> pluggableharness.agent.tool.v1.GetSchemaResponse - 7, // 29: pluggableharness.agent.tool.v1.ToolService.Configure:output_type -> pluggableharness.agent.tool.v1.ConfigureResponse - 11, // 30: pluggableharness.agent.tool.v1.ToolService.Invoke:output_type -> pluggableharness.agent.tool.v1.InvokeResponse - 17, // 31: pluggableharness.agent.tool.v1.ToolService.Render:output_type -> pluggableharness.agent.tool.v1.RenderResponse - 28, // [28:32] is the sub-list for method output_type - 24, // [24:28] is the sub-list for method input_type - 24, // [24:24] is the sub-list for extension type_name - 24, // [24:24] is the sub-list for extension extendee - 0, // [0:24] is the sub-list for field type_name + 26, // 1: pluggableharness.agent.tool.v1.GetSchemaResponse.slash_commands:type_name -> pluggableharness.agent.slashcommand.v1.SlashCommandSpec + 27, // 2: pluggableharness.agent.tool.v1.GetSchemaResponse.config_schema:type_name -> pluggableharness.agent.config.v1.ConfigSchema + 28, // 3: pluggableharness.agent.tool.v1.GetSchemaResponse.supported_hook_points:type_name -> pluggableharness.agent.common.v1.HookPoint + 29, // 4: pluggableharness.agent.tool.v1.ConfigureRequest.config:type_name -> google.protobuf.Struct + 0, // 5: pluggableharness.agent.tool.v1.ToolSchema.kind:type_name -> pluggableharness.agent.tool.v1.ToolKind + 1, // 6: pluggableharness.agent.tool.v1.ToolSchema.risk:type_name -> pluggableharness.agent.tool.v1.RiskClass + 30, // 7: pluggableharness.agent.tool.v1.ToolSchema.input_schema:type_name -> pluggableharness.agent.schema.v1.Schema + 30, // 8: pluggableharness.agent.tool.v1.ToolSchema.output_schema:type_name -> pluggableharness.agent.schema.v1.Schema + 8, // 9: pluggableharness.agent.tool.v1.ToolSchema.concurrency:type_name -> pluggableharness.agent.tool.v1.ConcurrencySpec + 31, // 10: pluggableharness.agent.tool.v1.ToolSchema.default_timeout:type_name -> google.protobuf.Duration + 12, // 11: pluggableharness.agent.tool.v1.InvokeRequest.call:type_name -> pluggableharness.agent.tool.v1.ToolCall + 13, // 12: pluggableharness.agent.tool.v1.InvokeResponse.event:type_name -> pluggableharness.agent.tool.v1.ToolEvent + 29, // 13: pluggableharness.agent.tool.v1.ToolCall.arguments:type_name -> google.protobuf.Struct + 32, // 14: pluggableharness.agent.tool.v1.ToolCall.call_context:type_name -> pluggableharness.agent.common.v1.CallContext + 22, // 15: pluggableharness.agent.tool.v1.ToolEvent.output_chunk:type_name -> pluggableharness.agent.tool.v1.ToolEvent.OutputChunk + 23, // 16: pluggableharness.agent.tool.v1.ToolEvent.progress:type_name -> pluggableharness.agent.tool.v1.ToolEvent.Progress + 24, // 17: pluggableharness.agent.tool.v1.ToolEvent.partial_result:type_name -> pluggableharness.agent.tool.v1.ToolEvent.PartialResult + 25, // 18: pluggableharness.agent.tool.v1.ToolEvent.exit_status:type_name -> pluggableharness.agent.tool.v1.ToolEvent.ExitStatus + 14, // 19: pluggableharness.agent.tool.v1.ToolEvent.result:type_name -> pluggableharness.agent.tool.v1.ToolResult + 15, // 20: pluggableharness.agent.tool.v1.ToolEvent.error:type_name -> pluggableharness.agent.tool.v1.ToolError + 29, // 21: pluggableharness.agent.tool.v1.ToolResult.payload:type_name -> google.protobuf.Struct + 3, // 22: pluggableharness.agent.tool.v1.ToolError.category:type_name -> pluggableharness.agent.tool.v1.ToolErrorCategory + 29, // 23: pluggableharness.agent.tool.v1.ToolError.details:type_name -> google.protobuf.Struct + 33, // 24: pluggableharness.agent.tool.v1.RenderResponse.tree:type_name -> pluggableharness.agent.render.v1.RenderTree + 12, // 25: pluggableharness.agent.tool.v1.PreviewRequest.call:type_name -> pluggableharness.agent.tool.v1.ToolCall + 33, // 26: pluggableharness.agent.tool.v1.PreviewResponse.preview:type_name -> pluggableharness.agent.render.v1.RenderTree + 34, // 27: pluggableharness.agent.tool.v1.DescribeResponse.producer:type_name -> pluggableharness.agent.common.v1.ProducerRef + 2, // 28: pluggableharness.agent.tool.v1.ToolEvent.OutputChunk.stream:type_name -> pluggableharness.agent.tool.v1.OutputStream + 29, // 29: pluggableharness.agent.tool.v1.ToolEvent.PartialResult.payload:type_name -> google.protobuf.Struct + 4, // 30: pluggableharness.agent.tool.v1.ToolService.GetSchema:input_type -> pluggableharness.agent.tool.v1.GetSchemaRequest + 6, // 31: pluggableharness.agent.tool.v1.ToolService.Configure:input_type -> pluggableharness.agent.tool.v1.ConfigureRequest + 10, // 32: pluggableharness.agent.tool.v1.ToolService.Invoke:input_type -> pluggableharness.agent.tool.v1.InvokeRequest + 16, // 33: pluggableharness.agent.tool.v1.ToolService.Render:input_type -> pluggableharness.agent.tool.v1.RenderRequest + 18, // 34: pluggableharness.agent.tool.v1.ToolService.Preview:input_type -> pluggableharness.agent.tool.v1.PreviewRequest + 20, // 35: pluggableharness.agent.tool.v1.ToolService.Describe:input_type -> pluggableharness.agent.tool.v1.DescribeRequest + 5, // 36: pluggableharness.agent.tool.v1.ToolService.GetSchema:output_type -> pluggableharness.agent.tool.v1.GetSchemaResponse + 7, // 37: pluggableharness.agent.tool.v1.ToolService.Configure:output_type -> pluggableharness.agent.tool.v1.ConfigureResponse + 11, // 38: pluggableharness.agent.tool.v1.ToolService.Invoke:output_type -> pluggableharness.agent.tool.v1.InvokeResponse + 17, // 39: pluggableharness.agent.tool.v1.ToolService.Render:output_type -> pluggableharness.agent.tool.v1.RenderResponse + 19, // 40: pluggableharness.agent.tool.v1.ToolService.Preview:output_type -> pluggableharness.agent.tool.v1.PreviewResponse + 21, // 41: pluggableharness.agent.tool.v1.ToolService.Describe:output_type -> pluggableharness.agent.tool.v1.DescribeResponse + 36, // [36:42] is the sub-list for method output_type + 30, // [30:36] is the sub-list for method input_type + 30, // [30:30] is the sub-list for extension type_name + 30, // [30:30] is the sub-list for extension extendee + 0, // [0:30] is the sub-list for field type_name } func init() { file_pluggableharness_agent_tool_v1_tool_proto_init() } @@ -1648,6 +1941,7 @@ func file_pluggableharness_agent_tool_v1_tool_proto_init() { if File_pluggableharness_agent_tool_v1_tool_proto != nil { return } + file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[5].OneofWrappers = []any{} file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[9].OneofWrappers = []any{ (*ToolEvent_OutputChunk_)(nil), (*ToolEvent_Progress_)(nil), @@ -1657,15 +1951,15 @@ func file_pluggableharness_agent_tool_v1_tool_proto_init() { (*ToolEvent_Error)(nil), } file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[11].OneofWrappers = []any{} - file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[15].OneofWrappers = []any{} - file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[17].OneofWrappers = []any{} + file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[19].OneofWrappers = []any{} + file_pluggableharness_agent_tool_v1_tool_proto_msgTypes[21].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_pluggableharness_agent_tool_v1_tool_proto_rawDesc), len(file_pluggableharness_agent_tool_v1_tool_proto_rawDesc)), NumEnums: 4, - NumMessages: 18, + NumMessages: 22, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/tool/proto/v1/tool_grpc.pb.go b/pkg/tool/proto/v1/tool_grpc.pb.go index e251592..321fe96 100644 --- a/pkg/tool/proto/v1/tool_grpc.pb.go +++ b/pkg/tool/proto/v1/tool_grpc.pb.go @@ -28,6 +28,8 @@ const ( ToolService_Configure_FullMethodName = "/pluggableharness.agent.tool.v1.ToolService/Configure" ToolService_Invoke_FullMethodName = "/pluggableharness.agent.tool.v1.ToolService/Invoke" ToolService_Render_FullMethodName = "/pluggableharness.agent.tool.v1.ToolService/Render" + ToolService_Preview_FullMethodName = "/pluggableharness.agent.tool.v1.ToolService/Preview" + ToolService_Describe_FullMethodName = "/pluggableharness.agent.tool.v1.ToolService/Describe" ) // ToolServiceClient is the client API for ToolService service. @@ -60,6 +62,18 @@ type ToolServiceClient interface { // per tool.md §7. MAY be implemented; if absent, the kernel falls back to // its generic default (pretty-printed JSON payload). Render(ctx context.Context, in *RenderRequest, opts ...grpc.CallOption) (*RenderResponse, error) + // Preview returns a dry-run, human-readable description of what Invoke + // would do for the given call, without performing it — per + // protocol.md#preview. MAY be implemented; a kernel MUST tolerate its + // absence and fall back to showing the call's raw arguments in the + // plan/apply gate's permission UI. + Preview(ctx context.Context, in *PreviewRequest, opts ...grpc.CallOption) (*PreviewResponse, error) + // Describe reports this plugin build's own identity — per + // protocol.md#describe and configuration/lock-file.md's dev_overrides + // note, this is how the kernel learns a dev_overrides-resolved plugin's + // {name, version, source, category, protocol_version} when there is no + // lock-file entry to read it from. + Describe(ctx context.Context, in *DescribeRequest, opts ...grpc.CallOption) (*DescribeResponse, error) } type toolServiceClient struct { @@ -119,6 +133,26 @@ func (c *toolServiceClient) Render(ctx context.Context, in *RenderRequest, opts return out, nil } +func (c *toolServiceClient) Preview(ctx context.Context, in *PreviewRequest, opts ...grpc.CallOption) (*PreviewResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(PreviewResponse) + err := c.cc.Invoke(ctx, ToolService_Preview_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *toolServiceClient) Describe(ctx context.Context, in *DescribeRequest, opts ...grpc.CallOption) (*DescribeResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DescribeResponse) + err := c.cc.Invoke(ctx, ToolService_Describe_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // ToolServiceServer is the server API for ToolService service. // All implementations must embed UnimplementedToolServiceServer // for forward compatibility. @@ -149,6 +183,18 @@ type ToolServiceServer interface { // per tool.md §7. MAY be implemented; if absent, the kernel falls back to // its generic default (pretty-printed JSON payload). Render(context.Context, *RenderRequest) (*RenderResponse, error) + // Preview returns a dry-run, human-readable description of what Invoke + // would do for the given call, without performing it — per + // protocol.md#preview. MAY be implemented; a kernel MUST tolerate its + // absence and fall back to showing the call's raw arguments in the + // plan/apply gate's permission UI. + Preview(context.Context, *PreviewRequest) (*PreviewResponse, error) + // Describe reports this plugin build's own identity — per + // protocol.md#describe and configuration/lock-file.md's dev_overrides + // note, this is how the kernel learns a dev_overrides-resolved plugin's + // {name, version, source, category, protocol_version} when there is no + // lock-file entry to read it from. + Describe(context.Context, *DescribeRequest) (*DescribeResponse, error) mustEmbedUnimplementedToolServiceServer() } @@ -171,6 +217,12 @@ func (UnimplementedToolServiceServer) Invoke(*InvokeRequest, grpc.ServerStreamin func (UnimplementedToolServiceServer) Render(context.Context, *RenderRequest) (*RenderResponse, error) { return nil, status.Error(codes.Unimplemented, "method Render not implemented") } +func (UnimplementedToolServiceServer) Preview(context.Context, *PreviewRequest) (*PreviewResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Preview not implemented") +} +func (UnimplementedToolServiceServer) Describe(context.Context, *DescribeRequest) (*DescribeResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Describe not implemented") +} func (UnimplementedToolServiceServer) mustEmbedUnimplementedToolServiceServer() {} func (UnimplementedToolServiceServer) testEmbeddedByValue() {} @@ -257,6 +309,42 @@ func _ToolService_Render_Handler(srv interface{}, ctx context.Context, dec func( return interceptor(ctx, in, info, handler) } +func _ToolService_Preview_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PreviewRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ToolServiceServer).Preview(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ToolService_Preview_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ToolServiceServer).Preview(ctx, req.(*PreviewRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ToolService_Describe_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DescribeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ToolServiceServer).Describe(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ToolService_Describe_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ToolServiceServer).Describe(ctx, req.(*DescribeRequest)) + } + return interceptor(ctx, in, info, handler) +} + // ToolService_ServiceDesc is the grpc.ServiceDesc for ToolService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -276,6 +364,14 @@ var ToolService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Render", Handler: _ToolService_Render_Handler, }, + { + MethodName: "Preview", + Handler: _ToolService_Preview_Handler, + }, + { + MethodName: "Describe", + Handler: _ToolService_Describe_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/pkg/widget/proto/v1/widget.pb.go b/pkg/widget/proto/v1/widget.pb.go index 18ba1c6..8b4c44c 100644 --- a/pkg/widget/proto/v1/widget.pb.go +++ b/pkg/widget/proto/v1/widget.pb.go @@ -13,8 +13,9 @@ package widgetv1 import ( - v11 "github.com/pluggableharness/agent/pkg/config/proto/v1" - v1 "github.com/pluggableharness/agent/pkg/render/proto/v1" + v1 "github.com/pluggableharness/agent/pkg/common/proto/v1" + v12 "github.com/pluggableharness/agent/pkg/config/proto/v1" + v11 "github.com/pluggableharness/agent/pkg/render/proto/v1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" structpb "google.golang.org/protobuf/types/known/structpb" @@ -30,6 +31,152 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +// WidgetErrorCategory classifies a WidgetError, mirroring +// FrontendErrorCategory's shape (frontend.proto) for the widget category — +// resolves frontend/conformance.md's prior open question of whether +// widgets need a structured error type of their own. +type WidgetErrorCategory int32 + +const ( + // Zero value. Never valid for a real error; its presence on the wire + // means a caller forgot to set the field. + WidgetErrorCategory_WIDGET_ERROR_CATEGORY_UNSPECIFIED WidgetErrorCategory = 0 + // A RenderTree or WidgetUpdate could not be displayed. + WidgetErrorCategory_WIDGET_ERROR_CATEGORY_RENDER_FAILED WidgetErrorCategory = 1 + // A WidgetUpdate named a Region this widget's frontend cannot honor. + WidgetErrorCategory_WIDGET_ERROR_CATEGORY_REGION_UNSUPPORTED WidgetErrorCategory = 2 + // An error that does not fit any other category. + WidgetErrorCategory_WIDGET_ERROR_CATEGORY_UNKNOWN WidgetErrorCategory = 3 +) + +// Enum value maps for WidgetErrorCategory. +var ( + WidgetErrorCategory_name = map[int32]string{ + 0: "WIDGET_ERROR_CATEGORY_UNSPECIFIED", + 1: "WIDGET_ERROR_CATEGORY_RENDER_FAILED", + 2: "WIDGET_ERROR_CATEGORY_REGION_UNSUPPORTED", + 3: "WIDGET_ERROR_CATEGORY_UNKNOWN", + } + WidgetErrorCategory_value = map[string]int32{ + "WIDGET_ERROR_CATEGORY_UNSPECIFIED": 0, + "WIDGET_ERROR_CATEGORY_RENDER_FAILED": 1, + "WIDGET_ERROR_CATEGORY_REGION_UNSUPPORTED": 2, + "WIDGET_ERROR_CATEGORY_UNKNOWN": 3, + } +) + +func (x WidgetErrorCategory) Enum() *WidgetErrorCategory { + p := new(WidgetErrorCategory) + *p = x + return p +} + +func (x WidgetErrorCategory) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (WidgetErrorCategory) Descriptor() protoreflect.EnumDescriptor { + return file_pluggableharness_agent_widget_v1_widget_proto_enumTypes[0].Descriptor() +} + +func (WidgetErrorCategory) Type() protoreflect.EnumType { + return &file_pluggableharness_agent_widget_v1_widget_proto_enumTypes[0] +} + +func (x WidgetErrorCategory) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use WidgetErrorCategory.Descriptor instead. +func (WidgetErrorCategory) EnumDescriptor() ([]byte, []int) { + return file_pluggableharness_agent_widget_v1_widget_proto_rawDescGZIP(), []int{0} +} + +// DescribeRequest carries no fields — Describe takes no parameters. +type DescribeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeRequest) Reset() { + *x = DescribeRequest{} + mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeRequest) ProtoMessage() {} + +func (x *DescribeRequest) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeRequest.ProtoReflect.Descriptor instead. +func (*DescribeRequest) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_widget_v1_widget_proto_rawDescGZIP(), []int{0} +} + +// DescribeResponse reports this plugin build's own identity, obtained +// directly from the running process rather than a lock-file row — +// configuration/lock-file.md's "dev_overrides and identity without a lock +// entry". +type DescribeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Producer *v1.ProducerRef `protobuf:"bytes,1,opt,name=producer,proto3" json:"producer,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeResponse) Reset() { + *x = DescribeResponse{} + mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeResponse) ProtoMessage() {} + +func (x *DescribeResponse) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeResponse.ProtoReflect.Descriptor instead. +func (*DescribeResponse) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_widget_v1_widget_proto_rawDescGZIP(), []int{1} +} + +func (x *DescribeResponse) GetProducer() *v1.ProducerRef { + if x != nil { + return x.Producer + } + return nil +} + // GetCapabilitiesRequest carries no fields — GetCapabilities takes no // parameters. type GetCapabilitiesRequest struct { @@ -40,7 +187,7 @@ type GetCapabilitiesRequest struct { func (x *GetCapabilitiesRequest) Reset() { *x = GetCapabilitiesRequest{} - mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[0] + mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -52,7 +199,7 @@ func (x *GetCapabilitiesRequest) String() string { func (*GetCapabilitiesRequest) ProtoMessage() {} func (x *GetCapabilitiesRequest) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[0] + mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -65,7 +212,7 @@ func (x *GetCapabilitiesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetCapabilitiesRequest.ProtoReflect.Descriptor instead. func (*GetCapabilitiesRequest) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_widget_v1_widget_proto_rawDescGZIP(), []int{0} + return file_pluggableharness_agent_widget_v1_widget_proto_rawDescGZIP(), []int{2} } // GetCapabilitiesResponse wraps WidgetCapabilities for the RPC signature, @@ -79,7 +226,7 @@ type GetCapabilitiesResponse struct { func (x *GetCapabilitiesResponse) Reset() { *x = GetCapabilitiesResponse{} - mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[1] + mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -91,7 +238,7 @@ func (x *GetCapabilitiesResponse) String() string { func (*GetCapabilitiesResponse) ProtoMessage() {} func (x *GetCapabilitiesResponse) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[1] + mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -104,7 +251,7 @@ func (x *GetCapabilitiesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetCapabilitiesResponse.ProtoReflect.Descriptor instead. func (*GetCapabilitiesResponse) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_widget_v1_widget_proto_rawDescGZIP(), []int{1} + return file_pluggableharness_agent_widget_v1_widget_proto_rawDescGZIP(), []int{3} } func (x *GetCapabilitiesResponse) GetCapabilities() *WidgetCapabilities { @@ -119,17 +266,22 @@ func (x *GetCapabilitiesResponse) GetCapabilities() *WidgetCapabilities { type WidgetCapabilities struct { state protoimpl.MessageState `protogen:"open.v1"` // MUST — the regions this widget intends to contribute to. - Regions []v1.Region `protobuf:"varint,1,rep,packed,name=regions,proto3,enum=pluggableharness.agent.render.v1.Region" json:"regions,omitempty"` + Regions []v11.Region `protobuf:"varint,1,rep,packed,name=regions,proto3,enum=pluggableharness.agent.render.v1.Region" json:"regions,omitempty"` // This provider's agent.hcl config schema, per configuration.md §4 — // what fields Configure's request may be decoded from. - ConfigSchema *v11.ConfigSchema `protobuf:"bytes,2,opt,name=config_schema,json=configSchema,proto3" json:"config_schema,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + ConfigSchema *v12.ConfigSchema `protobuf:"bytes,2,opt,name=config_schema,json=configSchema,proto3" json:"config_schema,omitempty"` + // Hook points this widget can subscribe to in observe mode + // (agent-loop/hook-dispatch.md), so a mis-declared agent.hcl hook{} + // block naming an unsupported point can be rejected at config-load + // time rather than failing at first dispatch. + SupportedHookPoints []v1.HookPoint `protobuf:"varint,3,rep,packed,name=supported_hook_points,json=supportedHookPoints,proto3,enum=pluggableharness.agent.common.v1.HookPoint" json:"supported_hook_points,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *WidgetCapabilities) Reset() { *x = WidgetCapabilities{} - mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[2] + mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -141,7 +293,7 @@ func (x *WidgetCapabilities) String() string { func (*WidgetCapabilities) ProtoMessage() {} func (x *WidgetCapabilities) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[2] + mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -154,23 +306,30 @@ func (x *WidgetCapabilities) ProtoReflect() protoreflect.Message { // Deprecated: Use WidgetCapabilities.ProtoReflect.Descriptor instead. func (*WidgetCapabilities) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_widget_v1_widget_proto_rawDescGZIP(), []int{2} + return file_pluggableharness_agent_widget_v1_widget_proto_rawDescGZIP(), []int{4} } -func (x *WidgetCapabilities) GetRegions() []v1.Region { +func (x *WidgetCapabilities) GetRegions() []v11.Region { if x != nil { return x.Regions } return nil } -func (x *WidgetCapabilities) GetConfigSchema() *v11.ConfigSchema { +func (x *WidgetCapabilities) GetConfigSchema() *v12.ConfigSchema { if x != nil { return x.ConfigSchema } return nil } +func (x *WidgetCapabilities) GetSupportedHookPoints() []v1.HookPoint { + if x != nil { + return x.SupportedHookPoints + } + return nil +} + // ConfigureRequest carries this provider's already-decoded agent.hcl block. type ConfigureRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -182,7 +341,7 @@ type ConfigureRequest struct { func (x *ConfigureRequest) Reset() { *x = ConfigureRequest{} - mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[3] + mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -194,7 +353,7 @@ func (x *ConfigureRequest) String() string { func (*ConfigureRequest) ProtoMessage() {} func (x *ConfigureRequest) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[3] + mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -207,7 +366,7 @@ func (x *ConfigureRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigureRequest.ProtoReflect.Descriptor instead. func (*ConfigureRequest) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_widget_v1_widget_proto_rawDescGZIP(), []int{3} + return file_pluggableharness_agent_widget_v1_widget_proto_rawDescGZIP(), []int{5} } func (x *ConfigureRequest) GetConfig() *structpb.Struct { @@ -227,7 +386,7 @@ type ConfigureResponse struct { func (x *ConfigureResponse) Reset() { *x = ConfigureResponse{} - mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[4] + mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -239,7 +398,7 @@ func (x *ConfigureResponse) String() string { func (*ConfigureResponse) ProtoMessage() {} func (x *ConfigureResponse) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[4] + mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -252,7 +411,7 @@ func (x *ConfigureResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigureResponse.ProtoReflect.Descriptor instead. func (*ConfigureResponse) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_widget_v1_widget_proto_rawDescGZIP(), []int{4} + return file_pluggableharness_agent_widget_v1_widget_proto_rawDescGZIP(), []int{6} } // AttachRequest identifies which session's widget instance to attach to. @@ -266,7 +425,7 @@ type AttachRequest struct { func (x *AttachRequest) Reset() { *x = AttachRequest{} - mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[5] + mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -278,7 +437,7 @@ func (x *AttachRequest) String() string { func (*AttachRequest) ProtoMessage() {} func (x *AttachRequest) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[5] + mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -291,7 +450,7 @@ func (x *AttachRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AttachRequest.ProtoReflect.Descriptor instead. func (*AttachRequest) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_widget_v1_widget_proto_rawDescGZIP(), []int{5} + return file_pluggableharness_agent_widget_v1_widget_proto_rawDescGZIP(), []int{7} } func (x *AttachRequest) GetSessionId() string { @@ -306,9 +465,9 @@ func (x *AttachRequest) GetSessionId() string { type WidgetUpdate struct { state protoimpl.MessageState `protogen:"open.v1"` // Which region this update places content into. - Region v1.Region `protobuf:"varint,1,opt,name=region,proto3,enum=pluggableharness.agent.render.v1.Region" json:"region,omitempty"` + Region v11.Region `protobuf:"varint,1,opt,name=region,proto3,enum=pluggableharness.agent.render.v1.Region" json:"region,omitempty"` // The content to place. - Content *v1.RenderTree `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` + Content *v11.RenderTree `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` // True: replace this widget's prior content in `region`. False: append. Replace bool `protobuf:"varint,3,opt,name=replace,proto3" json:"replace,omitempty"` unknownFields protoimpl.UnknownFields @@ -317,7 +476,7 @@ type WidgetUpdate struct { func (x *WidgetUpdate) Reset() { *x = WidgetUpdate{} - mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[6] + mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -329,7 +488,7 @@ func (x *WidgetUpdate) String() string { func (*WidgetUpdate) ProtoMessage() {} func (x *WidgetUpdate) ProtoReflect() protoreflect.Message { - mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[6] + mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -342,17 +501,17 @@ func (x *WidgetUpdate) ProtoReflect() protoreflect.Message { // Deprecated: Use WidgetUpdate.ProtoReflect.Descriptor instead. func (*WidgetUpdate) Descriptor() ([]byte, []int) { - return file_pluggableharness_agent_widget_v1_widget_proto_rawDescGZIP(), []int{6} + return file_pluggableharness_agent_widget_v1_widget_proto_rawDescGZIP(), []int{8} } -func (x *WidgetUpdate) GetRegion() v1.Region { +func (x *WidgetUpdate) GetRegion() v11.Region { if x != nil { return x.Region } - return v1.Region(0) + return v11.Region(0) } -func (x *WidgetUpdate) GetContent() *v1.RenderTree { +func (x *WidgetUpdate) GetContent() *v11.RenderTree { if x != nil { return x.Content } @@ -366,17 +525,82 @@ func (x *WidgetUpdate) GetReplace() bool { return false } +// WidgetError is the structured error type for the widget category, +// mirroring FrontendError (frontend.proto). Unlike the frontend category +// (whose Attach errors surface in-band via ServerEvent.Error), widget +// Attach has no return channel other than the stream itself — WidgetError +// is carried in the structured detail of a gRPC status on Configure or +// Attach, per .claude/rules/grpc.md's error-taxonomy discipline, not as an +// in-band stream message. +type WidgetError struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The error's category. + Category WidgetErrorCategory `protobuf:"varint,1,opt,name=category,proto3,enum=pluggableharness.agent.widget.v1.WidgetErrorCategory" json:"category,omitempty"` + // A human-readable message. + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WidgetError) Reset() { + *x = WidgetError{} + mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WidgetError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WidgetError) ProtoMessage() {} + +func (x *WidgetError) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_widget_v1_widget_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WidgetError.ProtoReflect.Descriptor instead. +func (*WidgetError) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_widget_v1_widget_proto_rawDescGZIP(), []int{9} +} + +func (x *WidgetError) GetCategory() WidgetErrorCategory { + if x != nil { + return x.Category + } + return WidgetErrorCategory_WIDGET_ERROR_CATEGORY_UNSPECIFIED +} + +func (x *WidgetError) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + var File_pluggableharness_agent_widget_v1_widget_proto protoreflect.FileDescriptor const file_pluggableharness_agent_widget_v1_widget_proto_rawDesc = "" + "\n" + - "-pluggableharness/agent/widget/v1/widget.proto\x12 pluggableharness.agent.widget.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a-pluggableharness/agent/config/v1/config.proto\x1a-pluggableharness/agent/render/v1/render.proto\"\x18\n" + + "-pluggableharness/agent/widget/v1/widget.proto\x12 pluggableharness.agent.widget.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a-pluggableharness/agent/common/v1/common.proto\x1a-pluggableharness/agent/config/v1/config.proto\x1a-pluggableharness/agent/render/v1/render.proto\"\x11\n" + + "\x0fDescribeRequest\"]\n" + + "\x10DescribeResponse\x12I\n" + + "\bproducer\x18\x01 \x01(\v2-.pluggableharness.agent.common.v1.ProducerRefR\bproducer\"\x18\n" + "\x16GetCapabilitiesRequest\"s\n" + "\x17GetCapabilitiesResponse\x12X\n" + - "\fcapabilities\x18\x01 \x01(\v24.pluggableharness.agent.widget.v1.WidgetCapabilitiesR\fcapabilities\"\xad\x01\n" + + "\fcapabilities\x18\x01 \x01(\v24.pluggableharness.agent.widget.v1.WidgetCapabilitiesR\fcapabilities\"\x8e\x02\n" + "\x12WidgetCapabilities\x12B\n" + "\aregions\x18\x01 \x03(\x0e2(.pluggableharness.agent.render.v1.RegionR\aregions\x12S\n" + - "\rconfig_schema\x18\x02 \x01(\v2..pluggableharness.agent.config.v1.ConfigSchemaR\fconfigSchema\"C\n" + + "\rconfig_schema\x18\x02 \x01(\v2..pluggableharness.agent.config.v1.ConfigSchemaR\fconfigSchema\x12_\n" + + "\x15supported_hook_points\x18\x03 \x03(\x0e2+.pluggableharness.agent.common.v1.HookPointR\x13supportedHookPoints\"C\n" + "\x10ConfigureRequest\x12/\n" + "\x06config\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x06config\"\x13\n" + "\x11ConfigureResponse\".\n" + @@ -386,11 +610,20 @@ const file_pluggableharness_agent_widget_v1_widget_proto_rawDesc = "" + "\fWidgetUpdate\x12@\n" + "\x06region\x18\x01 \x01(\x0e2(.pluggableharness.agent.render.v1.RegionR\x06region\x12F\n" + "\acontent\x18\x02 \x01(\v2,.pluggableharness.agent.render.v1.RenderTreeR\acontent\x12\x18\n" + - "\areplace\x18\x03 \x01(\bR\areplace2\xfb\x02\n" + + "\areplace\x18\x03 \x01(\bR\areplace\"z\n" + + "\vWidgetError\x12Q\n" + + "\bcategory\x18\x01 \x01(\x0e25.pluggableharness.agent.widget.v1.WidgetErrorCategoryR\bcategory\x12\x18\n" + + "\amessage\x18\x02 \x01(\tR\amessage*\xb6\x01\n" + + "\x13WidgetErrorCategory\x12%\n" + + "!WIDGET_ERROR_CATEGORY_UNSPECIFIED\x10\x00\x12'\n" + + "#WIDGET_ERROR_CATEGORY_RENDER_FAILED\x10\x01\x12,\n" + + "(WIDGET_ERROR_CATEGORY_REGION_UNSUPPORTED\x10\x02\x12!\n" + + "\x1dWIDGET_ERROR_CATEGORY_UNKNOWN\x10\x032\xee\x03\n" + "\rWidgetService\x12\x86\x01\n" + "\x0fGetCapabilities\x128.pluggableharness.agent.widget.v1.GetCapabilitiesRequest\x1a9.pluggableharness.agent.widget.v1.GetCapabilitiesResponse\x12t\n" + "\tConfigure\x122.pluggableharness.agent.widget.v1.ConfigureRequest\x1a3.pluggableharness.agent.widget.v1.ConfigureResponse\x12k\n" + - "\x06Attach\x12/.pluggableharness.agent.widget.v1.AttachRequest\x1a..pluggableharness.agent.widget.v1.WidgetUpdate0\x01B@Z>github.com/pluggableharness/agent/pkg/widget/proto/v1;widgetv1b\x06proto3" + "\x06Attach\x12/.pluggableharness.agent.widget.v1.AttachRequest\x1a..pluggableharness.agent.widget.v1.WidgetUpdate0\x01\x12q\n" + + "\bDescribe\x121.pluggableharness.agent.widget.v1.DescribeRequest\x1a2.pluggableharness.agent.widget.v1.DescribeResponseB@Z>github.com/pluggableharness/agent/pkg/widget/proto/v1;widgetv1b\x06proto3" var ( file_pluggableharness_agent_widget_v1_widget_proto_rawDescOnce sync.Once @@ -404,38 +637,50 @@ func file_pluggableharness_agent_widget_v1_widget_proto_rawDescGZIP() []byte { return file_pluggableharness_agent_widget_v1_widget_proto_rawDescData } -var file_pluggableharness_agent_widget_v1_widget_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_pluggableharness_agent_widget_v1_widget_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_pluggableharness_agent_widget_v1_widget_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_pluggableharness_agent_widget_v1_widget_proto_goTypes = []any{ - (*GetCapabilitiesRequest)(nil), // 0: pluggableharness.agent.widget.v1.GetCapabilitiesRequest - (*GetCapabilitiesResponse)(nil), // 1: pluggableharness.agent.widget.v1.GetCapabilitiesResponse - (*WidgetCapabilities)(nil), // 2: pluggableharness.agent.widget.v1.WidgetCapabilities - (*ConfigureRequest)(nil), // 3: pluggableharness.agent.widget.v1.ConfigureRequest - (*ConfigureResponse)(nil), // 4: pluggableharness.agent.widget.v1.ConfigureResponse - (*AttachRequest)(nil), // 5: pluggableharness.agent.widget.v1.AttachRequest - (*WidgetUpdate)(nil), // 6: pluggableharness.agent.widget.v1.WidgetUpdate - (v1.Region)(0), // 7: pluggableharness.agent.render.v1.Region - (*v11.ConfigSchema)(nil), // 8: pluggableharness.agent.config.v1.ConfigSchema - (*structpb.Struct)(nil), // 9: google.protobuf.Struct - (*v1.RenderTree)(nil), // 10: pluggableharness.agent.render.v1.RenderTree + (WidgetErrorCategory)(0), // 0: pluggableharness.agent.widget.v1.WidgetErrorCategory + (*DescribeRequest)(nil), // 1: pluggableharness.agent.widget.v1.DescribeRequest + (*DescribeResponse)(nil), // 2: pluggableharness.agent.widget.v1.DescribeResponse + (*GetCapabilitiesRequest)(nil), // 3: pluggableharness.agent.widget.v1.GetCapabilitiesRequest + (*GetCapabilitiesResponse)(nil), // 4: pluggableharness.agent.widget.v1.GetCapabilitiesResponse + (*WidgetCapabilities)(nil), // 5: pluggableharness.agent.widget.v1.WidgetCapabilities + (*ConfigureRequest)(nil), // 6: pluggableharness.agent.widget.v1.ConfigureRequest + (*ConfigureResponse)(nil), // 7: pluggableharness.agent.widget.v1.ConfigureResponse + (*AttachRequest)(nil), // 8: pluggableharness.agent.widget.v1.AttachRequest + (*WidgetUpdate)(nil), // 9: pluggableharness.agent.widget.v1.WidgetUpdate + (*WidgetError)(nil), // 10: pluggableharness.agent.widget.v1.WidgetError + (*v1.ProducerRef)(nil), // 11: pluggableharness.agent.common.v1.ProducerRef + (v11.Region)(0), // 12: pluggableharness.agent.render.v1.Region + (*v12.ConfigSchema)(nil), // 13: pluggableharness.agent.config.v1.ConfigSchema + (v1.HookPoint)(0), // 14: pluggableharness.agent.common.v1.HookPoint + (*structpb.Struct)(nil), // 15: google.protobuf.Struct + (*v11.RenderTree)(nil), // 16: pluggableharness.agent.render.v1.RenderTree } var file_pluggableharness_agent_widget_v1_widget_proto_depIdxs = []int32{ - 2, // 0: pluggableharness.agent.widget.v1.GetCapabilitiesResponse.capabilities:type_name -> pluggableharness.agent.widget.v1.WidgetCapabilities - 7, // 1: pluggableharness.agent.widget.v1.WidgetCapabilities.regions:type_name -> pluggableharness.agent.render.v1.Region - 8, // 2: pluggableharness.agent.widget.v1.WidgetCapabilities.config_schema:type_name -> pluggableharness.agent.config.v1.ConfigSchema - 9, // 3: pluggableharness.agent.widget.v1.ConfigureRequest.config:type_name -> google.protobuf.Struct - 7, // 4: pluggableharness.agent.widget.v1.WidgetUpdate.region:type_name -> pluggableharness.agent.render.v1.Region - 10, // 5: pluggableharness.agent.widget.v1.WidgetUpdate.content:type_name -> pluggableharness.agent.render.v1.RenderTree - 0, // 6: pluggableharness.agent.widget.v1.WidgetService.GetCapabilities:input_type -> pluggableharness.agent.widget.v1.GetCapabilitiesRequest - 3, // 7: pluggableharness.agent.widget.v1.WidgetService.Configure:input_type -> pluggableharness.agent.widget.v1.ConfigureRequest - 5, // 8: pluggableharness.agent.widget.v1.WidgetService.Attach:input_type -> pluggableharness.agent.widget.v1.AttachRequest - 1, // 9: pluggableharness.agent.widget.v1.WidgetService.GetCapabilities:output_type -> pluggableharness.agent.widget.v1.GetCapabilitiesResponse - 4, // 10: pluggableharness.agent.widget.v1.WidgetService.Configure:output_type -> pluggableharness.agent.widget.v1.ConfigureResponse - 6, // 11: pluggableharness.agent.widget.v1.WidgetService.Attach:output_type -> pluggableharness.agent.widget.v1.WidgetUpdate - 9, // [9:12] is the sub-list for method output_type - 6, // [6:9] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name + 11, // 0: pluggableharness.agent.widget.v1.DescribeResponse.producer:type_name -> pluggableharness.agent.common.v1.ProducerRef + 5, // 1: pluggableharness.agent.widget.v1.GetCapabilitiesResponse.capabilities:type_name -> pluggableharness.agent.widget.v1.WidgetCapabilities + 12, // 2: pluggableharness.agent.widget.v1.WidgetCapabilities.regions:type_name -> pluggableharness.agent.render.v1.Region + 13, // 3: pluggableharness.agent.widget.v1.WidgetCapabilities.config_schema:type_name -> pluggableharness.agent.config.v1.ConfigSchema + 14, // 4: pluggableharness.agent.widget.v1.WidgetCapabilities.supported_hook_points:type_name -> pluggableharness.agent.common.v1.HookPoint + 15, // 5: pluggableharness.agent.widget.v1.ConfigureRequest.config:type_name -> google.protobuf.Struct + 12, // 6: pluggableharness.agent.widget.v1.WidgetUpdate.region:type_name -> pluggableharness.agent.render.v1.Region + 16, // 7: pluggableharness.agent.widget.v1.WidgetUpdate.content:type_name -> pluggableharness.agent.render.v1.RenderTree + 0, // 8: pluggableharness.agent.widget.v1.WidgetError.category:type_name -> pluggableharness.agent.widget.v1.WidgetErrorCategory + 3, // 9: pluggableharness.agent.widget.v1.WidgetService.GetCapabilities:input_type -> pluggableharness.agent.widget.v1.GetCapabilitiesRequest + 6, // 10: pluggableharness.agent.widget.v1.WidgetService.Configure:input_type -> pluggableharness.agent.widget.v1.ConfigureRequest + 8, // 11: pluggableharness.agent.widget.v1.WidgetService.Attach:input_type -> pluggableharness.agent.widget.v1.AttachRequest + 1, // 12: pluggableharness.agent.widget.v1.WidgetService.Describe:input_type -> pluggableharness.agent.widget.v1.DescribeRequest + 4, // 13: pluggableharness.agent.widget.v1.WidgetService.GetCapabilities:output_type -> pluggableharness.agent.widget.v1.GetCapabilitiesResponse + 7, // 14: pluggableharness.agent.widget.v1.WidgetService.Configure:output_type -> pluggableharness.agent.widget.v1.ConfigureResponse + 9, // 15: pluggableharness.agent.widget.v1.WidgetService.Attach:output_type -> pluggableharness.agent.widget.v1.WidgetUpdate + 2, // 16: pluggableharness.agent.widget.v1.WidgetService.Describe:output_type -> pluggableharness.agent.widget.v1.DescribeResponse + 13, // [13:17] is the sub-list for method output_type + 9, // [9:13] is the sub-list for method input_type + 9, // [9:9] is the sub-list for extension type_name + 9, // [9:9] is the sub-list for extension extendee + 0, // [0:9] is the sub-list for field type_name } func init() { file_pluggableharness_agent_widget_v1_widget_proto_init() } @@ -448,13 +693,14 @@ func file_pluggableharness_agent_widget_v1_widget_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_pluggableharness_agent_widget_v1_widget_proto_rawDesc), len(file_pluggableharness_agent_widget_v1_widget_proto_rawDesc)), - NumEnums: 0, - NumMessages: 7, + NumEnums: 1, + NumMessages: 10, NumExtensions: 0, NumServices: 1, }, GoTypes: file_pluggableharness_agent_widget_v1_widget_proto_goTypes, DependencyIndexes: file_pluggableharness_agent_widget_v1_widget_proto_depIdxs, + EnumInfos: file_pluggableharness_agent_widget_v1_widget_proto_enumTypes, MessageInfos: file_pluggableharness_agent_widget_v1_widget_proto_msgTypes, }.Build() File_pluggableharness_agent_widget_v1_widget_proto = out.File diff --git a/pkg/widget/proto/v1/widget_grpc.pb.go b/pkg/widget/proto/v1/widget_grpc.pb.go index a395159..b8ca628 100644 --- a/pkg/widget/proto/v1/widget_grpc.pb.go +++ b/pkg/widget/proto/v1/widget_grpc.pb.go @@ -28,6 +28,7 @@ const ( WidgetService_GetCapabilities_FullMethodName = "/pluggableharness.agent.widget.v1.WidgetService/GetCapabilities" WidgetService_Configure_FullMethodName = "/pluggableharness.agent.widget.v1.WidgetService/Configure" WidgetService_Attach_FullMethodName = "/pluggableharness.agent.widget.v1.WidgetService/Attach" + WidgetService_Describe_FullMethodName = "/pluggableharness.agent.widget.v1.WidgetService/Describe" ) // WidgetServiceClient is the client API for WidgetService service. @@ -66,6 +67,15 @@ type WidgetServiceClient interface { // literal spec, naming the domain concept rather than the RPC. Not a // uniqueness violation: WidgetUpdate is used by exactly this one RPC. Attach(ctx context.Context, in *AttachRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[WidgetUpdate], error) + // Describe reports this plugin build's own identity — {name, version, + // source, category, protocol_version} — directly from the running + // process, rather than the kernel inferring it from a lock-file row. + // Every one of the six category protocols gains this identical RPC in + // this protocol revision; it exists specifically for a + // `dev_overrides`-resolved binary (configuration/lock-file.md's + // "dev_overrides and identity without a lock entry"), which has no + // provider {} lock-file entry to read identity from at all. + Describe(ctx context.Context, in *DescribeRequest, opts ...grpc.CallOption) (*DescribeResponse, error) } type widgetServiceClient struct { @@ -115,6 +125,16 @@ func (c *widgetServiceClient) Attach(ctx context.Context, in *AttachRequest, opt // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type WidgetService_AttachClient = grpc.ServerStreamingClient[WidgetUpdate] +func (c *widgetServiceClient) Describe(ctx context.Context, in *DescribeRequest, opts ...grpc.CallOption) (*DescribeResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DescribeResponse) + err := c.cc.Invoke(ctx, WidgetService_Describe_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // WidgetServiceServer is the server API for WidgetService service. // All implementations must embed UnimplementedWidgetServiceServer // for forward compatibility. @@ -151,6 +171,15 @@ type WidgetServiceServer interface { // literal spec, naming the domain concept rather than the RPC. Not a // uniqueness violation: WidgetUpdate is used by exactly this one RPC. Attach(*AttachRequest, grpc.ServerStreamingServer[WidgetUpdate]) error + // Describe reports this plugin build's own identity — {name, version, + // source, category, protocol_version} — directly from the running + // process, rather than the kernel inferring it from a lock-file row. + // Every one of the six category protocols gains this identical RPC in + // this protocol revision; it exists specifically for a + // `dev_overrides`-resolved binary (configuration/lock-file.md's + // "dev_overrides and identity without a lock entry"), which has no + // provider {} lock-file entry to read identity from at all. + Describe(context.Context, *DescribeRequest) (*DescribeResponse, error) mustEmbedUnimplementedWidgetServiceServer() } @@ -170,6 +199,9 @@ func (UnimplementedWidgetServiceServer) Configure(context.Context, *ConfigureReq func (UnimplementedWidgetServiceServer) Attach(*AttachRequest, grpc.ServerStreamingServer[WidgetUpdate]) error { return status.Error(codes.Unimplemented, "method Attach not implemented") } +func (UnimplementedWidgetServiceServer) Describe(context.Context, *DescribeRequest) (*DescribeResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Describe not implemented") +} func (UnimplementedWidgetServiceServer) mustEmbedUnimplementedWidgetServiceServer() {} func (UnimplementedWidgetServiceServer) testEmbeddedByValue() {} @@ -238,6 +270,24 @@ func _WidgetService_Attach_Handler(srv interface{}, stream grpc.ServerStream) er // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type WidgetService_AttachServer = grpc.ServerStreamingServer[WidgetUpdate] +func _WidgetService_Describe_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DescribeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WidgetServiceServer).Describe(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: WidgetService_Describe_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WidgetServiceServer).Describe(ctx, req.(*DescribeRequest)) + } + return interceptor(ctx, in, info, handler) +} + // WidgetService_ServiceDesc is the grpc.ServiceDesc for WidgetService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -253,6 +303,10 @@ var WidgetService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Configure", Handler: _WidgetService_Configure_Handler, }, + { + MethodName: "Describe", + Handler: _WidgetService_Describe_Handler, + }, }, Streams: []grpc.StreamDesc{ { From 984610d16c6eca521f85b474314c44d3b61a7345 Mon Sep 17 00:00:00 2001 From: Steven Crothers Date: Fri, 24 Jul 2026 13:47:36 -0400 Subject: [PATCH 6/7] Add event/v1 payload schemas per EventKind One message per kernel.v1.EventKind value: MessageEvent, ToolCallEvent, ToolResultEvent, PlanEvent, ApplyEvent (wrapping plan.v1.ApplyResult, shared with hook.v1's post-apply payload), ContextContributionEvent, MemoryMutationEvent, HookErrorEvent. These messages are schema_version 1 of each kind's payload: normative for the emitting owner, still opaque to the kernel at write time. state-backend.md and kernel-callbacks.md document the mapping; stubs regenerated. Also fix the last stale pkg/provider mention in go-layout.md. --- .claude/rules/go-layout.md | 2 +- .../agent/event/v1/event.proto | 191 +++++ docs/specifications/kernel-callbacks.md | 2 + docs/specifications/state-backend.md | 17 + pkg/event/proto/v1/event.pb.go | 737 ++++++++++++++++++ 5 files changed, 948 insertions(+), 1 deletion(-) create mode 100644 api/pluggableharness/agent/event/v1/event.proto create mode 100644 pkg/event/proto/v1/event.pb.go diff --git a/.claude/rules/go-layout.md b/.claude/rules/go-layout.md index b17161d..bb562f9 100644 --- a/.claude/rules/go-layout.md +++ b/.claude/rules/go-layout.md @@ -19,7 +19,7 @@ api/ .proto sources — buf's module root (see buf.yaml). pluggableharness/agent//v1/*.proto one directory per category per protocol version pkg/ first-class, third-party-consumable Go integration — the only thing a plugin author needs to import. - / pkg/provider/, pkg/tool/, pkg/context/, pkg/memory/, + / pkg/model/, pkg/tool/, pkg/context/, pkg/memory/, pkg/frontend/, pkg/widget/, plus pkg/kernel/ for the kernel-callback service (docs/specifications/kernel-callbacks.md) *.go hand-written ergonomic SDK: the thin, idiomatic Go diff --git a/api/pluggableharness/agent/event/v1/event.proto b/api/pluggableharness/agent/event/v1/event.proto new file mode 100644 index 0000000..b14c54c --- /dev/null +++ b/api/pluggableharness/agent/event/v1/event.proto @@ -0,0 +1,191 @@ +syntax = "proto3"; + +// Package pluggableharness.agent.event.v1 defines the decoded payload shape +// for every pluggableharness.agent.kernel.v1.EventKind — the concrete +// message that a kernel.v1.EmitRequest.payload / the state backend's +// events.payload column (state-backend.md §The kind enum) actually +// contains, once schema_version identifies it. This package defines no +// enum of its own: kernel.v1.EventKind and state-backend.md §The kind enum +// remain the sole, authoritative enumeration of which kinds exist. The +// mapping between a kind and its payload message here is 1:1 and asserted +// only by documentation — a kind → message table lives in +// state-backend.md §The kind enum, mirroring how kernel-callbacks.md +// already restates that enum in prose rather than redefining it. +// +// Each message below IS schema_version "1" of its kind's payload — an +// EmitRequest/events row with schema_version "1" (or "v1") denotes "the +// payload bytes, once unmarshaled, are exactly the event.v1 message named +// for this kind." A future breaking change to any one payload's shape +// ships as a new event.v2 package and schema_version "2", never as an edit +// to the message defined here — proto.md's no-breaking-changes rule for a +// released v1 applies to this package like every other. Because a +// persisted event's schema_version and producer_category/producer_name/ +// producer_version columns together identify exactly which historical +// plugin build to invoke for replay (the "supersedes" model, +// architecture.md §Versioning & schema drift), an old session stays +// decodable by a kernel built against a newer event package without ever +// needing to migrate stored bytes in place. +// +// These messages are normative for the owning spec, but still opaque to +// the kernel — the two are not in tension. For a kernel-produced kind +// (EVENT_KIND_MESSAGE, EVENT_KIND_PLAN, EVENT_KIND_APPLY — the kernel is +// the sole writer of these), the kernel itself populates the message +// defined here and "normative" simply describes what it writes. For a +// plugin-Emit'd kind (EVENT_KIND_TOOL_CALL, EVENT_KIND_TOOL_RESULT, +// EVENT_KIND_CONTEXT_CONTRIBUTION, the three EVENT_KIND_MEMORY_* kinds), +// the owning category spec requires the emitting plugin to shape its +// payload bytes as the matching message here — but the kernel still +// stores EmitRequest.payload as opaque bytes and never parses or +// validates it at Emit time (state-backend.md: "events.payload opaque, +// never inspected by the kernel" is a MUST that this package does not +// relax). "Normative" means the spec that owns a kind dictates the bytes +// a conforming plugin writes; it does not mean the kernel enforces that +// shape on write. Conformance is enforced by the owning category spec and +// by the producing plugin's own (possibly historical, supersedes-resolved) +// Render implementation being able to make sense of what it emitted, not +// by kernel-side schema validation. +// +// Import direction is one-way: event.v1 imports common/content/model/tool/ +// plan/hook to name each payload's fields with real message types (per +// proto.md's ban on Any/untyped bytes/loose maps standing in for a +// structured payload); nothing imports event.v1. That makes this package +// a safe leaf for every payload-referencing category to sit behind without +// risking an import cycle back into any of them. +package pluggableharness.agent.event.v1; + +import "pluggableharness/agent/common/v1/common.proto"; +import "pluggableharness/agent/content/v1/content.proto"; +import "pluggableharness/agent/hook/v1/hook.proto"; +import "pluggableharness/agent/model/v1/model.proto"; +import "pluggableharness/agent/plan/v1/plan.proto"; +import "pluggableharness/agent/tool/v1/tool.proto"; + +option go_package = "github.com/pluggableharness/agent/pkg/event/proto/v1;eventv1"; + +// MessageEvent is the payload for EVENT_KIND_MESSAGE — a completed model +// turn's accumulated canonical message, kernel-produced (state-backend.md +// §The kind enum: usage/cost figures live here, not as a separate kind, +// and drive the cost_ledger table at write time). +message MessageEvent { + // The assembled canonical message. MUST be set. Carries its own + // kernel-assigned id and, when role == ROLE_ASSISTANT, the + // produced_by_model_id/produced_by_provider plain-string attribution + // (content.v1.Message) — those fields identify *which model/provider + // name* produced it, distinct from `model` below. + pluggableharness.agent.content.v1.Message message = 1; + + // The exact plugin build (name, version, source, protocol_version) that + // produced `message`, for supersedes/replay attribution + // (architecture.md §Versioning & schema drift). MUST be set. This is + // deliberately the full ProducerRef, not a repeat of Message's plain- + // string produced_by_model_id/produced_by_provider fields — those two + // exist on Message for lightweight display use without decoding an + // event payload; `model` here is what replay actually dispatches on. + pluggableharness.agent.common.v1.ProducerRef model = 2; + + // Token usage for the completion that produced `message`. MUST be set. + pluggableharness.agent.model.v1.Usage usage = 3; + + // The kernel-computed cost, in USD, of the completion that produced + // `message` (model/protocol.md#cost-computation — the provider never + // computes cost itself). MUST be set. Extracted into the cost_ledger + // table at write time (state-backend.md §cost_ledger). + double cost_usd = 4; +} + +// ToolCallEvent is the payload for EVENT_KIND_TOOL_CALL. +message ToolCallEvent { + // The call being made. MUST be set. + pluggableharness.agent.tool.v1.ToolCall call = 1; +} + +// ToolResultEvent is the payload for EVENT_KIND_TOOL_RESULT. +message ToolResultEvent { + // The originating ToolCall.id this result is for. MUST be set. + string tool_call_id = 1; + + // Exactly one variant MUST be set. + oneof outcome { + // The call's successful result. + pluggableharness.agent.tool.v1.ToolResult result = 2; + // The call's failed result. + pluggableharness.agent.tool.v1.ToolError error = 3; + } +} + +// PlanEvent is the payload for EVENT_KIND_PLAN — a turn's fully-built Plan, +// captured pre plan-ready dispatch, kernel-produced. Drives the plan_items +// table at write time (state-backend.md §plan_items). +message PlanEvent { + // The built plan. MUST be set. + pluggableharness.agent.plan.v1.Plan plan = 1; +} + +// ApplyEvent is the payload for EVENT_KIND_APPLY — a turn's complete set of +// per-item apply outcomes, once every item in its Plan has reached a +// terminal ApplyOutcome (agent-loop.md §5.2), kernel-produced. +message ApplyEvent { + // The turn's per-item apply outcomes. MUST be set. Wraps + // plan.v1.ApplyResult — homed in plan.v1, not redefined here, so this + // event's payload and hook.v1.PostApplyPayload's subject are the exact + // same message rather than two independently-evolving shapes for the + // same data (plan.proto's own ApplyResult comment documents this + // sharing rationale). + pluggableharness.agent.plan.v1.ApplyResult result = 1; +} + +// ContextContributionEvent is the payload for EVENT_KIND_CONTEXT_CONTRIBUTION +// — a context provider's Contribute output (context/protocol.md#contribute- +// the-context-assemble-rpc), or a memory provider's Recall output after +// kernel translation (memory/protocol.md#kernel-side-translation-into- +// context-assembly). Plugin-emitted: the owning category spec requires +// this shape, but the kernel stores it opaquely (see package comment). +message ContextContributionEvent { + // The contributed content blocks, in emission order. Text-only in v1, + // matching context and memory's existing content-type constraint. + repeated pluggableharness.agent.content.v1.ContentBlock content = 1; + + // The token count of `content`, resolved via the kernel's CountTokens + // primitive (kernel-callbacks.md#counttokens), never a provider-local + // heuristic. MUST be set. + int64 tokens = 2; + + // The model target this contribution was budgeted against + // (model.v1.ModelTarget's context_window/effective_ceiling), when the + // contributing provider had one available. Absent when no target was + // known at contribution time. + optional pluggableharness.agent.model.v1.ModelTarget target = 3; +} + +// MemoryMutationEvent is the shared payload for EVENT_KIND_MEMORY_WRITE, +// EVENT_KIND_MEMORY_UPDATE, and EVENT_KIND_MEMORY_DELETE — one message for +// all three kinds because the mutating verb is already carried by the +// envelope's kind; this message only needs to describe the record itself. +// Plugin-emitted: the owning category spec (memory/README.md) requires +// this shape, but the kernel stores it opaquely (see package comment). +message MemoryMutationEvent { + // The mutated record's stable identifier. MUST be set. + string record_id = 1; + + // The record's content blocks, in emission order. MUST be set for + // EVENT_KIND_MEMORY_WRITE and EVENT_KIND_MEMORY_UPDATE; absent for + // EVENT_KIND_MEMORY_DELETE, which has no content left to describe. + repeated pluggableharness.agent.content.v1.ContentBlock content = 2; + + // Open-ended key/value metadata about the mutation (e.g. tags, source), + // a genuine unstructured-by-design case, not a structured-payload dodge + // — proto.md's sanctioned map use. + map attributes = 3; +} + +// HookErrorEvent is the payload for EVENT_KIND_HOOK_ERROR — kernel- +// synthesized on a failing hook transform/veto subscriber's behalf +// (state-backend.md §The kind enum; agent-loop/hook-dispatch.md#subscriber- +// error-handling), rather than emitted by the failing subscriber itself. +// Importing hook.v1 here is correct and acyclic: nothing imports event.v1 +// (see package comment), so event.v1 depending on hook.v1 for this one +// payload introduces no cycle. +message HookErrorEvent { + // The structured detail of the failed hook dispatch. MUST be set. + pluggableharness.agent.hook.v1.HookError error = 1; +} diff --git a/docs/specifications/kernel-callbacks.md b/docs/specifications/kernel-callbacks.md index 87dd94a..4ea3481 100644 --- a/docs/specifications/kernel-callbacks.md +++ b/docs/specifications/kernel-callbacks.md @@ -108,6 +108,8 @@ EmitResult { `EventKind` is `state-backend.md`'s authoritative enum, restated here only because it's the wire-level type `Emit` actually carries — this document does not own its definition, and `state-backend.md` remains authoritative. Like every enum in this system, `EventKind`'s zero value, `EVENT_KIND_UNSPECIFIED`, is never valid on the wire — a caller that forgets to set `kind` produces a detectable, named "unspecified" error rather than something that silently looks like a real event kind. Usage/cost, `Render` output, and `session_start`/`session_end` deliberately don't get their own `EventKind` at all — see [`state-backend.md#the-kind-enum`](state-backend.md#the-kind-enum) for why. +`payload` is always the `pluggableharness.agent.event.v1` message that matches `kind`, marshaled to bytes — [`state-backend.md#the-kind-enum`](state-backend.md#the-kind-enum) carries the authoritative kind → message table. `schema_version` names the `event` package version that message belongs to: `"1"` for `event.v1`, and a future breaking payload change ships as `event.v2` with `schema_version = "2"`, never a silent edit to the `event.v1` shape. This does not change the opacity of `payload` itself — the kernel marshals/unmarshals nothing at `Emit` time for a plugin-supplied payload and never inspects the bytes; `schema_version` only tells a future reader (replay, a newer kernel) which package's generated type to decode with. + `Emit` accepts `EVENT_KIND_HOOK_ERROR` like any other kind, with one difference: the kernel is the one calling it, on a failing hook subscriber's behalf, rather than a plugin calling `Emit` for itself — see [`state-backend.md#the-kind-enum`](state-backend.md#the-kind-enum) for why this kind is kernel-synthesized and [`agent-loop/hook-dispatch.md#subscriber-error-handling`](agent-loop/hook-dispatch.md#subscriber-error-handling) for when it fires. ## Log diff --git a/docs/specifications/state-backend.md b/docs/specifications/state-backend.md index 2372b31..a0f31db 100644 --- a/docs/specifications/state-backend.md +++ b/docs/specifications/state-backend.md @@ -156,6 +156,23 @@ kind = enum { } ``` +Each `kind` above decodes to exactly one concrete message in `pluggableharness.agent.event.v1` (`api/pluggableharness/agent/event/v1/event.proto`) — that package defines no enum of its own; this table, together with `kernel-callbacks.md#emit`'s restatement of the same enum, is the sole source of the kind → message mapping: + +| `kind` | `event.v1` message | +|---|---| +| `message` | `MessageEvent` | +| `tool_call` | `ToolCallEvent` | +| `tool_result` | `ToolResultEvent` | +| `plan` | `PlanEvent` | +| `apply` | `ApplyEvent` | +| `context_contribution` | `ContextContributionEvent` | +| `memory_write` / `memory_update` / `memory_delete` | `MemoryMutationEvent` (one message; the mutating verb is the `kind` itself, not a payload field) | +| `hook_error` | `HookErrorEvent` | + +Each message above IS `events.schema_version = "1"` of its kind's payload: a row with `schema_version = "1"` (or `"v1"`) means "unmarshal `payload` as the `event.v1` message this table names for `kind`." A future breaking change to one payload's shape ships as `event.v2` + `schema_version = "2"`, never as an edit to the `event.v1` message — the same permanence guarantee this document already requires of every other released wire type. + +`event.v1`'s payload messages are normative for the owning spec without being kernel-validated: "normative" means the spec that owns a given `kind` dictates the exact bytes a conforming producer writes, not that the kernel parses or checks those bytes at `Emit` time — `events.payload opaque, never inspected by the kernel` above stays true unchanged. Conformance is enforced by the owning category spec and by the producer's own (possibly historical, "supersedes"-resolved) `Render` being able to make sense of what it emitted, never by kernel-side schema validation. + `hook_error` is the one `kind` the kernel writes on a subscriber's behalf rather than in response to that subscriber's own `Emit` call — a hook subscriber that just failed can't be relied on to call `Emit` itself; the kernel detects the failure during dispatch and persists the event directly. `producer_category`/`producer_name`/`producer_version` still identify the failing subscriber (`HookError.subscriber`, a `ProducerRef`), not the kernel itself — see [`kernel-callbacks.md#emit`](kernel-callbacks.md#emit) for how every other `kind` is written by the producing plugin's own callback connection. Three things deliberately do **not** get their own `kind`: diff --git a/pkg/event/proto/v1/event.pb.go b/pkg/event/proto/v1/event.pb.go new file mode 100644 index 0000000..5963eba --- /dev/null +++ b/pkg/event/proto/v1/event.pb.go @@ -0,0 +1,737 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: pluggableharness/agent/event/v1/event.proto + +// Package pluggableharness.agent.event.v1 defines the decoded payload shape +// for every pluggableharness.agent.kernel.v1.EventKind — the concrete +// message that a kernel.v1.EmitRequest.payload / the state backend's +// events.payload column (state-backend.md §The kind enum) actually +// contains, once schema_version identifies it. This package defines no +// enum of its own: kernel.v1.EventKind and state-backend.md §The kind enum +// remain the sole, authoritative enumeration of which kinds exist. The +// mapping between a kind and its payload message here is 1:1 and asserted +// only by documentation — a kind → message table lives in +// state-backend.md §The kind enum, mirroring how kernel-callbacks.md +// already restates that enum in prose rather than redefining it. +// +// Each message below IS schema_version "1" of its kind's payload — an +// EmitRequest/events row with schema_version "1" (or "v1") denotes "the +// payload bytes, once unmarshaled, are exactly the event.v1 message named +// for this kind." A future breaking change to any one payload's shape +// ships as a new event.v2 package and schema_version "2", never as an edit +// to the message defined here — proto.md's no-breaking-changes rule for a +// released v1 applies to this package like every other. Because a +// persisted event's schema_version and producer_category/producer_name/ +// producer_version columns together identify exactly which historical +// plugin build to invoke for replay (the "supersedes" model, +// architecture.md §Versioning & schema drift), an old session stays +// decodable by a kernel built against a newer event package without ever +// needing to migrate stored bytes in place. +// +// These messages are normative for the owning spec, but still opaque to +// the kernel — the two are not in tension. For a kernel-produced kind +// (EVENT_KIND_MESSAGE, EVENT_KIND_PLAN, EVENT_KIND_APPLY — the kernel is +// the sole writer of these), the kernel itself populates the message +// defined here and "normative" simply describes what it writes. For a +// plugin-Emit'd kind (EVENT_KIND_TOOL_CALL, EVENT_KIND_TOOL_RESULT, +// EVENT_KIND_CONTEXT_CONTRIBUTION, the three EVENT_KIND_MEMORY_* kinds), +// the owning category spec requires the emitting plugin to shape its +// payload bytes as the matching message here — but the kernel still +// stores EmitRequest.payload as opaque bytes and never parses or +// validates it at Emit time (state-backend.md: "events.payload opaque, +// never inspected by the kernel" is a MUST that this package does not +// relax). "Normative" means the spec that owns a kind dictates the bytes +// a conforming plugin writes; it does not mean the kernel enforces that +// shape on write. Conformance is enforced by the owning category spec and +// by the producing plugin's own (possibly historical, supersedes-resolved) +// Render implementation being able to make sense of what it emitted, not +// by kernel-side schema validation. +// +// Import direction is one-way: event.v1 imports common/content/model/tool/ +// plan/hook to name each payload's fields with real message types (per +// proto.md's ban on Any/untyped bytes/loose maps standing in for a +// structured payload); nothing imports event.v1. That makes this package +// a safe leaf for every payload-referencing category to sit behind without +// risking an import cycle back into any of them. + +package eventv1 + +import ( + v11 "github.com/pluggableharness/agent/pkg/common/proto/v1" + v1 "github.com/pluggableharness/agent/pkg/content/proto/v1" + v15 "github.com/pluggableharness/agent/pkg/hook/proto/v1" + v12 "github.com/pluggableharness/agent/pkg/model/proto/v1" + v14 "github.com/pluggableharness/agent/pkg/plan/proto/v1" + v13 "github.com/pluggableharness/agent/pkg/tool/proto/v1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// MessageEvent is the payload for EVENT_KIND_MESSAGE — a completed model +// turn's accumulated canonical message, kernel-produced (state-backend.md +// §The kind enum: usage/cost figures live here, not as a separate kind, +// and drive the cost_ledger table at write time). +type MessageEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The assembled canonical message. MUST be set. Carries its own + // kernel-assigned id and, when role == ROLE_ASSISTANT, the + // produced_by_model_id/produced_by_provider plain-string attribution + // (content.v1.Message) — those fields identify *which model/provider + // name* produced it, distinct from `model` below. + Message *v1.Message `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + // The exact plugin build (name, version, source, protocol_version) that + // produced `message`, for supersedes/replay attribution + // (architecture.md §Versioning & schema drift). MUST be set. This is + // deliberately the full ProducerRef, not a repeat of Message's plain- + // string produced_by_model_id/produced_by_provider fields — those two + // exist on Message for lightweight display use without decoding an + // event payload; `model` here is what replay actually dispatches on. + Model *v11.ProducerRef `protobuf:"bytes,2,opt,name=model,proto3" json:"model,omitempty"` + // Token usage for the completion that produced `message`. MUST be set. + Usage *v12.Usage `protobuf:"bytes,3,opt,name=usage,proto3" json:"usage,omitempty"` + // The kernel-computed cost, in USD, of the completion that produced + // `message` (model/protocol.md#cost-computation — the provider never + // computes cost itself). MUST be set. Extracted into the cost_ledger + // table at write time (state-backend.md §cost_ledger). + CostUsd float64 `protobuf:"fixed64,4,opt,name=cost_usd,json=costUsd,proto3" json:"cost_usd,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageEvent) Reset() { + *x = MessageEvent{} + mi := &file_pluggableharness_agent_event_v1_event_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageEvent) ProtoMessage() {} + +func (x *MessageEvent) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_event_v1_event_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageEvent.ProtoReflect.Descriptor instead. +func (*MessageEvent) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_event_v1_event_proto_rawDescGZIP(), []int{0} +} + +func (x *MessageEvent) GetMessage() *v1.Message { + if x != nil { + return x.Message + } + return nil +} + +func (x *MessageEvent) GetModel() *v11.ProducerRef { + if x != nil { + return x.Model + } + return nil +} + +func (x *MessageEvent) GetUsage() *v12.Usage { + if x != nil { + return x.Usage + } + return nil +} + +func (x *MessageEvent) GetCostUsd() float64 { + if x != nil { + return x.CostUsd + } + return 0 +} + +// ToolCallEvent is the payload for EVENT_KIND_TOOL_CALL. +type ToolCallEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The call being made. MUST be set. + Call *v13.ToolCall `protobuf:"bytes,1,opt,name=call,proto3" json:"call,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ToolCallEvent) Reset() { + *x = ToolCallEvent{} + mi := &file_pluggableharness_agent_event_v1_event_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ToolCallEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ToolCallEvent) ProtoMessage() {} + +func (x *ToolCallEvent) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_event_v1_event_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ToolCallEvent.ProtoReflect.Descriptor instead. +func (*ToolCallEvent) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_event_v1_event_proto_rawDescGZIP(), []int{1} +} + +func (x *ToolCallEvent) GetCall() *v13.ToolCall { + if x != nil { + return x.Call + } + return nil +} + +// ToolResultEvent is the payload for EVENT_KIND_TOOL_RESULT. +type ToolResultEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The originating ToolCall.id this result is for. MUST be set. + ToolCallId string `protobuf:"bytes,1,opt,name=tool_call_id,json=toolCallId,proto3" json:"tool_call_id,omitempty"` + // Exactly one variant MUST be set. + // + // Types that are valid to be assigned to Outcome: + // + // *ToolResultEvent_Result + // *ToolResultEvent_Error + Outcome isToolResultEvent_Outcome `protobuf_oneof:"outcome"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ToolResultEvent) Reset() { + *x = ToolResultEvent{} + mi := &file_pluggableharness_agent_event_v1_event_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ToolResultEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ToolResultEvent) ProtoMessage() {} + +func (x *ToolResultEvent) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_event_v1_event_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ToolResultEvent.ProtoReflect.Descriptor instead. +func (*ToolResultEvent) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_event_v1_event_proto_rawDescGZIP(), []int{2} +} + +func (x *ToolResultEvent) GetToolCallId() string { + if x != nil { + return x.ToolCallId + } + return "" +} + +func (x *ToolResultEvent) GetOutcome() isToolResultEvent_Outcome { + if x != nil { + return x.Outcome + } + return nil +} + +func (x *ToolResultEvent) GetResult() *v13.ToolResult { + if x != nil { + if x, ok := x.Outcome.(*ToolResultEvent_Result); ok { + return x.Result + } + } + return nil +} + +func (x *ToolResultEvent) GetError() *v13.ToolError { + if x != nil { + if x, ok := x.Outcome.(*ToolResultEvent_Error); ok { + return x.Error + } + } + return nil +} + +type isToolResultEvent_Outcome interface { + isToolResultEvent_Outcome() +} + +type ToolResultEvent_Result struct { + // The call's successful result. + Result *v13.ToolResult `protobuf:"bytes,2,opt,name=result,proto3,oneof"` +} + +type ToolResultEvent_Error struct { + // The call's failed result. + Error *v13.ToolError `protobuf:"bytes,3,opt,name=error,proto3,oneof"` +} + +func (*ToolResultEvent_Result) isToolResultEvent_Outcome() {} + +func (*ToolResultEvent_Error) isToolResultEvent_Outcome() {} + +// PlanEvent is the payload for EVENT_KIND_PLAN — a turn's fully-built Plan, +// captured pre plan-ready dispatch, kernel-produced. Drives the plan_items +// table at write time (state-backend.md §plan_items). +type PlanEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The built plan. MUST be set. + Plan *v14.Plan `protobuf:"bytes,1,opt,name=plan,proto3" json:"plan,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlanEvent) Reset() { + *x = PlanEvent{} + mi := &file_pluggableharness_agent_event_v1_event_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlanEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlanEvent) ProtoMessage() {} + +func (x *PlanEvent) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_event_v1_event_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlanEvent.ProtoReflect.Descriptor instead. +func (*PlanEvent) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_event_v1_event_proto_rawDescGZIP(), []int{3} +} + +func (x *PlanEvent) GetPlan() *v14.Plan { + if x != nil { + return x.Plan + } + return nil +} + +// ApplyEvent is the payload for EVENT_KIND_APPLY — a turn's complete set of +// per-item apply outcomes, once every item in its Plan has reached a +// terminal ApplyOutcome (agent-loop.md §5.2), kernel-produced. +type ApplyEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The turn's per-item apply outcomes. MUST be set. Wraps + // plan.v1.ApplyResult — homed in plan.v1, not redefined here, so this + // event's payload and hook.v1.PostApplyPayload's subject are the exact + // same message rather than two independently-evolving shapes for the + // same data (plan.proto's own ApplyResult comment documents this + // sharing rationale). + Result *v14.ApplyResult `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApplyEvent) Reset() { + *x = ApplyEvent{} + mi := &file_pluggableharness_agent_event_v1_event_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApplyEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApplyEvent) ProtoMessage() {} + +func (x *ApplyEvent) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_event_v1_event_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApplyEvent.ProtoReflect.Descriptor instead. +func (*ApplyEvent) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_event_v1_event_proto_rawDescGZIP(), []int{4} +} + +func (x *ApplyEvent) GetResult() *v14.ApplyResult { + if x != nil { + return x.Result + } + return nil +} + +// ContextContributionEvent is the payload for EVENT_KIND_CONTEXT_CONTRIBUTION +// — a context provider's Contribute output (context/protocol.md#contribute- +// the-context-assemble-rpc), or a memory provider's Recall output after +// kernel translation (memory/protocol.md#kernel-side-translation-into- +// context-assembly). Plugin-emitted: the owning category spec requires +// this shape, but the kernel stores it opaquely (see package comment). +type ContextContributionEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The contributed content blocks, in emission order. Text-only in v1, + // matching context and memory's existing content-type constraint. + Content []*v1.ContentBlock `protobuf:"bytes,1,rep,name=content,proto3" json:"content,omitempty"` + // The token count of `content`, resolved via the kernel's CountTokens + // primitive (kernel-callbacks.md#counttokens), never a provider-local + // heuristic. MUST be set. + Tokens int64 `protobuf:"varint,2,opt,name=tokens,proto3" json:"tokens,omitempty"` + // The model target this contribution was budgeted against + // (model.v1.ModelTarget's context_window/effective_ceiling), when the + // contributing provider had one available. Absent when no target was + // known at contribution time. + Target *v12.ModelTarget `protobuf:"bytes,3,opt,name=target,proto3,oneof" json:"target,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContextContributionEvent) Reset() { + *x = ContextContributionEvent{} + mi := &file_pluggableharness_agent_event_v1_event_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContextContributionEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContextContributionEvent) ProtoMessage() {} + +func (x *ContextContributionEvent) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_event_v1_event_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContextContributionEvent.ProtoReflect.Descriptor instead. +func (*ContextContributionEvent) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_event_v1_event_proto_rawDescGZIP(), []int{5} +} + +func (x *ContextContributionEvent) GetContent() []*v1.ContentBlock { + if x != nil { + return x.Content + } + return nil +} + +func (x *ContextContributionEvent) GetTokens() int64 { + if x != nil { + return x.Tokens + } + return 0 +} + +func (x *ContextContributionEvent) GetTarget() *v12.ModelTarget { + if x != nil { + return x.Target + } + return nil +} + +// MemoryMutationEvent is the shared payload for EVENT_KIND_MEMORY_WRITE, +// EVENT_KIND_MEMORY_UPDATE, and EVENT_KIND_MEMORY_DELETE — one message for +// all three kinds because the mutating verb is already carried by the +// envelope's kind; this message only needs to describe the record itself. +// Plugin-emitted: the owning category spec (memory/README.md) requires +// this shape, but the kernel stores it opaquely (see package comment). +type MemoryMutationEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The mutated record's stable identifier. MUST be set. + RecordId string `protobuf:"bytes,1,opt,name=record_id,json=recordId,proto3" json:"record_id,omitempty"` + // The record's content blocks, in emission order. MUST be set for + // EVENT_KIND_MEMORY_WRITE and EVENT_KIND_MEMORY_UPDATE; absent for + // EVENT_KIND_MEMORY_DELETE, which has no content left to describe. + Content []*v1.ContentBlock `protobuf:"bytes,2,rep,name=content,proto3" json:"content,omitempty"` + // Open-ended key/value metadata about the mutation (e.g. tags, source), + // a genuine unstructured-by-design case, not a structured-payload dodge + // — proto.md's sanctioned map use. + Attributes map[string]string `protobuf:"bytes,3,rep,name=attributes,proto3" json:"attributes,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MemoryMutationEvent) Reset() { + *x = MemoryMutationEvent{} + mi := &file_pluggableharness_agent_event_v1_event_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MemoryMutationEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MemoryMutationEvent) ProtoMessage() {} + +func (x *MemoryMutationEvent) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_event_v1_event_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MemoryMutationEvent.ProtoReflect.Descriptor instead. +func (*MemoryMutationEvent) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_event_v1_event_proto_rawDescGZIP(), []int{6} +} + +func (x *MemoryMutationEvent) GetRecordId() string { + if x != nil { + return x.RecordId + } + return "" +} + +func (x *MemoryMutationEvent) GetContent() []*v1.ContentBlock { + if x != nil { + return x.Content + } + return nil +} + +func (x *MemoryMutationEvent) GetAttributes() map[string]string { + if x != nil { + return x.Attributes + } + return nil +} + +// HookErrorEvent is the payload for EVENT_KIND_HOOK_ERROR — kernel- +// synthesized on a failing hook transform/veto subscriber's behalf +// (state-backend.md §The kind enum; agent-loop/hook-dispatch.md#subscriber- +// error-handling), rather than emitted by the failing subscriber itself. +// Importing hook.v1 here is correct and acyclic: nothing imports event.v1 +// (see package comment), so event.v1 depending on hook.v1 for this one +// payload introduces no cycle. +type HookErrorEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The structured detail of the failed hook dispatch. MUST be set. + Error *v15.HookError `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HookErrorEvent) Reset() { + *x = HookErrorEvent{} + mi := &file_pluggableharness_agent_event_v1_event_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HookErrorEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HookErrorEvent) ProtoMessage() {} + +func (x *HookErrorEvent) ProtoReflect() protoreflect.Message { + mi := &file_pluggableharness_agent_event_v1_event_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HookErrorEvent.ProtoReflect.Descriptor instead. +func (*HookErrorEvent) Descriptor() ([]byte, []int) { + return file_pluggableharness_agent_event_v1_event_proto_rawDescGZIP(), []int{7} +} + +func (x *HookErrorEvent) GetError() *v15.HookError { + if x != nil { + return x.Error + } + return nil +} + +var File_pluggableharness_agent_event_v1_event_proto protoreflect.FileDescriptor + +const file_pluggableharness_agent_event_v1_event_proto_rawDesc = "" + + "\n" + + "+pluggableharness/agent/event/v1/event.proto\x12\x1fpluggableharness.agent.event.v1\x1a-pluggableharness/agent/common/v1/common.proto\x1a/pluggableharness/agent/content/v1/content.proto\x1a)pluggableharness/agent/hook/v1/hook.proto\x1a+pluggableharness/agent/model/v1/model.proto\x1a)pluggableharness/agent/plan/v1/plan.proto\x1a)pluggableharness/agent/tool/v1/tool.proto\"\xf2\x01\n" + + "\fMessageEvent\x12D\n" + + "\amessage\x18\x01 \x01(\v2*.pluggableharness.agent.content.v1.MessageR\amessage\x12C\n" + + "\x05model\x18\x02 \x01(\v2-.pluggableharness.agent.common.v1.ProducerRefR\x05model\x12<\n" + + "\x05usage\x18\x03 \x01(\v2&.pluggableharness.agent.model.v1.UsageR\x05usage\x12\x19\n" + + "\bcost_usd\x18\x04 \x01(\x01R\acostUsd\"M\n" + + "\rToolCallEvent\x12<\n" + + "\x04call\x18\x01 \x01(\v2(.pluggableharness.agent.tool.v1.ToolCallR\x04call\"\xc7\x01\n" + + "\x0fToolResultEvent\x12 \n" + + "\ftool_call_id\x18\x01 \x01(\tR\n" + + "toolCallId\x12D\n" + + "\x06result\x18\x02 \x01(\v2*.pluggableharness.agent.tool.v1.ToolResultH\x00R\x06result\x12A\n" + + "\x05error\x18\x03 \x01(\v2).pluggableharness.agent.tool.v1.ToolErrorH\x00R\x05errorB\t\n" + + "\aoutcome\"E\n" + + "\tPlanEvent\x128\n" + + "\x04plan\x18\x01 \x01(\v2$.pluggableharness.agent.plan.v1.PlanR\x04plan\"Q\n" + + "\n" + + "ApplyEvent\x12C\n" + + "\x06result\x18\x01 \x01(\v2+.pluggableharness.agent.plan.v1.ApplyResultR\x06result\"\xd3\x01\n" + + "\x18ContextContributionEvent\x12I\n" + + "\acontent\x18\x01 \x03(\v2/.pluggableharness.agent.content.v1.ContentBlockR\acontent\x12\x16\n" + + "\x06tokens\x18\x02 \x01(\x03R\x06tokens\x12I\n" + + "\x06target\x18\x03 \x01(\v2,.pluggableharness.agent.model.v1.ModelTargetH\x00R\x06target\x88\x01\x01B\t\n" + + "\a_target\"\xa2\x02\n" + + "\x13MemoryMutationEvent\x12\x1b\n" + + "\trecord_id\x18\x01 \x01(\tR\brecordId\x12I\n" + + "\acontent\x18\x02 \x03(\v2/.pluggableharness.agent.content.v1.ContentBlockR\acontent\x12d\n" + + "\n" + + "attributes\x18\x03 \x03(\v2D.pluggableharness.agent.event.v1.MemoryMutationEvent.AttributesEntryR\n" + + "attributes\x1a=\n" + + "\x0fAttributesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"Q\n" + + "\x0eHookErrorEvent\x12?\n" + + "\x05error\x18\x01 \x01(\v2).pluggableharness.agent.hook.v1.HookErrorR\x05errorB>Z pluggableharness.agent.content.v1.Message + 10, // 1: pluggableharness.agent.event.v1.MessageEvent.model:type_name -> pluggableharness.agent.common.v1.ProducerRef + 11, // 2: pluggableharness.agent.event.v1.MessageEvent.usage:type_name -> pluggableharness.agent.model.v1.Usage + 12, // 3: pluggableharness.agent.event.v1.ToolCallEvent.call:type_name -> pluggableharness.agent.tool.v1.ToolCall + 13, // 4: pluggableharness.agent.event.v1.ToolResultEvent.result:type_name -> pluggableharness.agent.tool.v1.ToolResult + 14, // 5: pluggableharness.agent.event.v1.ToolResultEvent.error:type_name -> pluggableharness.agent.tool.v1.ToolError + 15, // 6: pluggableharness.agent.event.v1.PlanEvent.plan:type_name -> pluggableharness.agent.plan.v1.Plan + 16, // 7: pluggableharness.agent.event.v1.ApplyEvent.result:type_name -> pluggableharness.agent.plan.v1.ApplyResult + 17, // 8: pluggableharness.agent.event.v1.ContextContributionEvent.content:type_name -> pluggableharness.agent.content.v1.ContentBlock + 18, // 9: pluggableharness.agent.event.v1.ContextContributionEvent.target:type_name -> pluggableharness.agent.model.v1.ModelTarget + 17, // 10: pluggableharness.agent.event.v1.MemoryMutationEvent.content:type_name -> pluggableharness.agent.content.v1.ContentBlock + 8, // 11: pluggableharness.agent.event.v1.MemoryMutationEvent.attributes:type_name -> pluggableharness.agent.event.v1.MemoryMutationEvent.AttributesEntry + 19, // 12: pluggableharness.agent.event.v1.HookErrorEvent.error:type_name -> pluggableharness.agent.hook.v1.HookError + 13, // [13:13] is the sub-list for method output_type + 13, // [13:13] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name +} + +func init() { file_pluggableharness_agent_event_v1_event_proto_init() } +func file_pluggableharness_agent_event_v1_event_proto_init() { + if File_pluggableharness_agent_event_v1_event_proto != nil { + return + } + file_pluggableharness_agent_event_v1_event_proto_msgTypes[2].OneofWrappers = []any{ + (*ToolResultEvent_Result)(nil), + (*ToolResultEvent_Error)(nil), + } + file_pluggableharness_agent_event_v1_event_proto_msgTypes[5].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_pluggableharness_agent_event_v1_event_proto_rawDesc), len(file_pluggableharness_agent_event_v1_event_proto_rawDesc)), + NumEnums: 0, + NumMessages: 9, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_pluggableharness_agent_event_v1_event_proto_goTypes, + DependencyIndexes: file_pluggableharness_agent_event_v1_event_proto_depIdxs, + MessageInfos: file_pluggableharness_agent_event_v1_event_proto_msgTypes, + }.Build() + File_pluggableharness_agent_event_v1_event_proto = out.File + file_pluggableharness_agent_event_v1_event_proto_goTypes = nil + file_pluggableharness_agent_event_v1_event_proto_depIdxs = nil +} From 72d2aa11d6bf703b8fd0a38f1d6ae4cc85d7d5c6 Mon Sep 17 00:00:00 2001 From: Steven Crothers Date: Fri, 24 Jul 2026 13:48:57 -0400 Subject: [PATCH 7/7] Add glossary entries for event payloads and backfill --- docs/specifications/glossary.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/specifications/glossary.md b/docs/specifications/glossary.md index d962daf..d22319f 100644 --- a/docs/specifications/glossary.md +++ b/docs/specifications/glossary.md @@ -14,6 +14,8 @@ Terminology used throughout `docs/specifications/`. | **Hook** | A named lifecycle point in the agent loop that plugins and first-party policy can subscribe to (`session-start`, `context-assemble`, `pre-model-call`, `post-model-response`, `pre-tool-call`, `post-tool-call`, `plan-ready`, `post-apply`, `session-end`). See [`agent-loop/hook-dispatch.md`](agent-loop/hook-dispatch.md). | | **Hook mode** | How a hook subscriber participates: `observe` (read-only), `transform` (returns a modified payload for the next subscriber), or `veto` (can short-circuit with an explicit decision). | | **Emit** | A plugin sending a raw, opaque-payload event into the kernel, persisted verbatim to the state backend. | +| **Event payload schema** | The `event.v1` message a given event kind's payload marshals to/from — schema_version `"1"` of that kind. Normative for the emitting owner, still opaque to the kernel at write time. See [`state-backend.md`](state-backend.md#the-kind-enum). | +| **Backfill** | The unicast replay a frontend receives on attaching to a session: persisted events re-rendered in sequence order through the supersedes path, bracketed by `SessionAttached` and `BackfillComplete`. See [`frontend/frontend-protocol.md`](frontend/frontend-protocol.md). | | **Render** | A producer plugin turning its own previously-emitted payload into a display-agnostic `RenderTree`, on request from the kernel. | | **Paint** | A frontend plugin turning a `RenderTree` into actual pixels/text/audio. | | **RenderTree** | The display-agnostic intermediate representation every `Render` call returns — formally defined in [`frontend/render-tree.md`](frontend/render-tree.md) and shared verbatim by every category's `Render` RPC. |