Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
47ec731
doomloop: implement sliding-window hash detector
scrothers Jul 25, 2026
b08834a
callhash: implement deterministic call and key-field hashing
scrothers Jul 25, 2026
0cb678e
Implement internal/retrypolicy package
scrothers Jul 25, 2026
4db75fe
plugincache: add on-disk plugin binary cache package
scrothers Jul 25, 2026
a5ecd2a
sessionscope: implement refcounted session-grant registry
scrothers Jul 25, 2026
904e5a2
Add internal/xdg pure-domain package
scrothers Jul 25, 2026
aff6e1d
implement schemavalidate package for JSON-Schema validation
scrothers Jul 25, 2026
949ef58
bounds: implement loop-bound tracking with cost rollup seam
scrothers Jul 25, 2026
825fb84
circuitbreaker: implement per-provider denial/crash tripping
scrothers Jul 25, 2026
4ce7d2a
cost: implement pricing-tier resolution and cost formula
scrothers Jul 25, 2026
49dac22
Merge branch 'worktree-agent-a28d7fd3b2cbccb8e' into integration/runt…
scrothers Jul 25, 2026
ef05ecb
Merge branch 'worktree-agent-aab1a204f12f8f5b8' into integration/runt…
scrothers Jul 25, 2026
78368b9
Merge branch 'worktree-agent-ab6ed68fab2509b28' into integration/runt…
scrothers Jul 25, 2026
a327c34
Merge branch 'worktree-agent-a78fab23ccfc7eb47' into integration/runt…
scrothers Jul 25, 2026
f4b324a
Merge branch 'worktree-agent-a14de95ecdc148089' into integration/runt…
scrothers Jul 25, 2026
2819dfa
Merge branch 'worktree-agent-af1c1dd14ebeb4718' into integration/runt…
scrothers Jul 25, 2026
5bd6757
Merge branch 'worktree-agent-a9a99ca8625c0ce17' into integration/runt…
scrothers Jul 25, 2026
55d2c86
Merge branch 'worktree-agent-aaca4a18b66af41b5' into integration/runt…
scrothers Jul 25, 2026
056a87e
Merge branch 'worktree-agent-a33d4ab57da295434' into integration/runt…
scrothers Jul 25, 2026
59bd846
registry: add optional category field to lock file
scrothers Jul 25, 2026
0ca6148
tool: add terminates_turn field to ToolSchema
scrothers Jul 25, 2026
4d7409c
model: add redacted_thinking StreamEvent variant
scrothers Jul 25, 2026
c83825a
config: add event_bus, timeout, and depth settings
scrothers Jul 25, 2026
bd5316f
telemetry: add plan/interactive/callback span helpers
scrothers Jul 25, 2026
34eca25
pluginruntime: share one broker serve per launch
scrothers Jul 25, 2026
6a678ea
pluginruntime: expose hook client over the shared conn
scrothers Jul 25, 2026
475b3dd
statebackend: export kind and event-id helpers
scrothers Jul 25, 2026
40105a5
statebackend: set a sqlite busy timeout
scrothers Jul 25, 2026
af7900a
statebackend: add filtered event reads
scrothers Jul 25, 2026
643a3bc
statebackend: reserve a kernel producer identity
scrothers Jul 25, 2026
f75e127
Merge branch 'worktree-agent-a76b6c0c27874bc5a' into integration/runt…
scrothers Jul 25, 2026
f600404
Merge branch 'worktree-agent-a7fee020619f0a375' into integration/runt…
scrothers Jul 25, 2026
24a6426
Merge branch 'worktree-agent-a80b2228c40222831' into integration/runt…
scrothers Jul 25, 2026
a0534a4
Merge branch 'worktree-agent-a596087662dae1d09' into integration/runt…
scrothers Jul 25, 2026
1c674b7
Merge branch 'worktree-agent-a34320c4f9454d20f' into integration/runt…
scrothers Jul 25, 2026
0eae236
hookpayload: implement point mapping and mutable-field checks
scrothers Jul 25, 2026
4bebda6
streamaccum: implement StreamEvent accumulation into Message
scrothers Jul 25, 2026
1d9c199
providercatalog: define plugin lookup and fake
scrothers Jul 25, 2026
b2b970f
modelrequest: implement param validation and cache placement
scrothers Jul 25, 2026
abd28a6
sessionstate: implement sole-writer session emit path
scrothers Jul 25, 2026
0380734
interactive: define the interactive-call resolution seam
scrothers Jul 25, 2026
694a147
interactive: implement the unattended-refusal driver
scrothers Jul 25, 2026
85cebc6
tokencount: implement exact-vs-fallback token counting
scrothers Jul 25, 2026
726db74
plandecision: define the ask-resolution seam
scrothers Jul 25, 2026
06987e7
plandecision: implement the tracked auto-allow deviation
scrothers Jul 25, 2026
c7b8d5d
plandecision: add a scripted resolver test double
scrothers Jul 25, 2026
7ddb9ae
plandecision: add the resolver driver selector
scrothers Jul 25, 2026
abe553b
Merge branch 'worktree-agent-aaf9682b4cb4c0abd' into integration/runt…
scrothers Jul 25, 2026
6fbc7dd
Merge branch 'worktree-agent-ac34fb3006bbe5253' into integration/runt…
scrothers Jul 25, 2026
26b4842
Merge branch 'worktree-agent-a8c9a9758fd0271f5' into integration/runt…
scrothers Jul 25, 2026
6efd78e
Merge branch 'worktree-agent-a7cca01757098c042' into integration/runt…
scrothers Jul 25, 2026
2f2a2f9
Merge branch 'worktree-agent-a1d07719b922d5be9' into integration/runt…
scrothers Jul 25, 2026
52c6924
Merge branch 'worktree-agent-adc3c3ca7e1aa83e4' into integration/runt…
scrothers Jul 25, 2026
df7bf3f
Merge branch 'worktree-agent-afe24a1b4c14c610b' into integration/runt…
scrothers Jul 25, 2026
56e6244
providerresolve: resolve declared providers to binaries
scrothers Jul 25, 2026
7518944
hookdispatch: add ordered hook dispatcher
scrothers Jul 25, 2026
ce2db47
telemetry: add context-assemble spans and violation metric
scrothers Jul 25, 2026
cf956a1
contextassembly: implement the context-assemble RPC chain
scrothers Jul 25, 2026
afd79fa
modelcall: add StreamCompletion retry loop
scrothers Jul 25, 2026
4660b4e
sessionstate: add read pass-throughs for kernelcallback
scrothers Jul 25, 2026
dedd694
kernelcallback: implement Emit, CountTokens, ReadEvents, GetSession
scrothers Jul 25, 2026
f2d43bc
plangate: implement the plan/apply gate
scrothers Jul 25, 2026
dd0fc08
plangate: note the pinned-kernel-veto attribution gotcha
scrothers Jul 25, 2026
3144c72
hookdispatch: clarify when not to pin a kernel veto
scrothers Jul 25, 2026
27eb82b
plangate: note fail-closed denials are indistinguishable
scrothers Jul 25, 2026
e2433ef
hookdispatch: note hook_error is asymmetric evidence
scrothers Jul 25, 2026
7e5b043
plangate: correct the hook_error evidence claim
scrothers Jul 25, 2026
5d8259e
tooldispatch: add ConcurrencySpec scheduler and Invoke client
scrothers Jul 25, 2026
b55304a
providerresolve: record the resolved platform key
scrothers Jul 25, 2026
03a1fd8
telemetry: add the provider bring-up span
scrothers Jul 25, 2026
4feee48
pluginhost: launch and supervise declared plugins
scrothers Jul 25, 2026
0af7d04
Merge branch 'worktree-agent-af9000c424fee0bb2' into integration/runt…
scrothers Jul 25, 2026
faace23
Merge branch 'worktree-agent-a1a1f1b17022df18d' into integration/runt…
scrothers Jul 25, 2026
74645f4
Merge branch 'worktree-agent-ab9406285c2d28a25' into integration/runt…
scrothers Jul 25, 2026
c71994f
Merge branch 'worktree-agent-a084d6fe4b1a2c68c' into integration/runt…
scrothers Jul 25, 2026
da85b34
Merge branch 'worktree-agent-ab5dcfae3ab11ad2b' into integration/runt…
scrothers Jul 25, 2026
4e061db
Merge branch 'worktree-agent-a32c445391f2c127e' into integration/runt…
scrothers Jul 25, 2026
0d5da78
pluginhost: wire Scopes/Sessions/Tokens into kernelcallback.Config
scrothers Jul 25, 2026
15ed31a
turn: implement the RunTurn driver, steps 1-15
scrothers Jul 25, 2026
975476f
session: implement the session driver and turn loop
scrothers Jul 25, 2026
c14fbfc
telemetry: add providercatalog build span
scrothers Jul 25, 2026
47e2b9d
providercatalog: implement the plugin driver
scrothers Jul 25, 2026
d0db3be
cost: bill a tier-less free model at zero
scrothers Jul 25, 2026
c0f90b6
sessionstate: expose Live's underlying session
scrothers Jul 25, 2026
7bf4a2e
pluginhost: repair the integration harness config
scrothers Jul 25, 2026
e58621e
kernel: add the composition root and cmd/agent
scrothers Jul 25, 2026
c9dda73
anthropic: add the model catalog
scrothers Jul 25, 2026
878e0da
anthropic: add provider config schema and decoding
scrothers Jul 25, 2026
433cf18
model: add Sink.RedactedThinking
scrothers Jul 25, 2026
e8be955
anthropic: add vendor wire types and package docs
scrothers Jul 25, 2026
68092b4
anthropic: add request and schema translation
scrothers Jul 25, 2026
756996f
anthropic: add the SSE reader and event translator
scrothers Jul 25, 2026
f687289
anthropic: add the HTTP client and classifier
scrothers Jul 25, 2026
c0d6e83
anthropic: assemble the Provider implementation
scrothers Jul 25, 2026
e2baa1a
cmd/anthropic: serve the provider as a plugin
scrothers Jul 25, 2026
7d6abd0
anthropic: add integration and e2e test tiers
scrothers Jul 25, 2026
b840602
statebackend: pin proto map ordering in every event payload
scrothers Jul 25, 2026
71b339a
tooldispatch: start the call deadline after the locks, not before
scrothers Jul 25, 2026
9b48106
sessionstate: republish kernel-originated events onto the bus
scrothers Jul 25, 2026
bceb789
docs: drop a link into .claude/ that the site cannot resolve
scrothers Jul 25, 2026
c3396e7
xdg, plugincache: fix POSIX assumptions in tests
scrothers Jul 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
27 changes: 27 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,33 @@ linters:
- unconvert
- unparam
- wastedassign
- depguard
settings:
depguard:
rules:
# internal/anthropic is the reference model-provider plugin. It
# runs out of process and is meant to be indistinguishable from a
# plugin a third party could write against pkg/ alone, so it may
# import only pkg/... and the standard library. Reaching into any
# other internal/ package would make it a privileged in-tree
# shortcut rather than a proof that the published SDK is
# sufficient — and the whole point of building it was to find out
# whether pkg/ really is.
#
# Scoped to non-test files: the integration tier legitimately
# imports internal/pluginruntime to launch the built binary the
# way the kernel does, which is the kernel-launches-plugin
# direction, not a dependency of the plugin itself.
anthropic-plugin-isolation:
list-mode: lax
files:
- "**/internal/anthropic/**"
- "!$test"
allow:
- github.com/pluggableharness/agent/internal/anthropic
deny:
- pkg: github.com/pluggableharness/agent/internal
desc: internal/anthropic is a reference plugin — it may import only pkg/... and the standard library, never another internal/ package (see internal/anthropic/CLAUDE.md)
exclusions:
rules:
- path: _test\.go
Expand Down
36 changes: 30 additions & 6 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,37 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
version: 2

# No cmd/ binary exists yet — this is a plugin-host monorepo still in the
# library/proto/spec stage (see go-layout.md). Builds are explicitly
# skipped rather than left to auto-detect a main package that doesn't
# exist. Flip this to false and add a `builds:` block once cmd/<binary>
# exists.
# Two binaries: the kernel and the reference Anthropic model provider.
# Both are stamped through -ldflags rather than reading their identity
# from a file at runtime, matching how internal/pluginhost's integration
# fixture is built (`-X main.fixtureName=...`) — a plugin's Describe RPC
# has to answer from the running process, since a dev_overrides binary has
# no lock-file entry to read identity from
# (configuration/lock-file.md#dev_overrides-and-identity-without-a-lock-entry).
builds:
- skip: true
- id: agent
main: ./cmd/agent
binary: agent
env: [CGO_ENABLED=0]
goos: [linux, darwin, windows]
goarch: [amd64, arm64]
flags: [-trimpath]
ldflags:
- -s -w
- -X main.version={{ .Version }}

- id: anthropic
main: ./cmd/anthropic
binary: agent-provider-anthropic
env: [CGO_ENABLED=0]
goos: [linux, darwin, windows]
goarch: [amd64, arm64]
flags: [-trimpath]
ldflags:
- -s -w
# pluginVersion is what this plugin reports through Describe; the
# source stays the -X-able default in main.go for a checkout build.
- -X main.pluginVersion={{ .Version }}

archives:
- formats: [tar.gz]
Expand Down
17 changes: 17 additions & 0 deletions api/pluggableharness/model/v1/events.proto
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ message StreamEvent {
Stop stop = 8;
// The completion failed.
Error error = 9;
// A complete, vendor-encrypted reasoning block the kernel cannot
// interpret.
RedactedThinking redacted_thinking = 10;
}

// TextDelta carries one incremental fragment of assistant text output.
Expand Down Expand Up @@ -101,6 +104,20 @@ message StreamEvent {
// The structured error, classified per model.md §8.
ModelError error = 1;
}

// RedactedThinking carries one complete, vendor-encrypted reasoning
// block — not an incremental fragment, unlike ThinkingDelta: the
// vendor emits the block whole because its contents are deliberately
// opaque, so there is nothing to accumulate. MUST be emitted whenever
// the vendor produces reasoning content it requires be echoed back
// verbatim on a later turn (model.md §4/§5); the kernel MUST store and
// round-trip it without inspecting it, into ContentBlock's
// RedactedThinkingBlock.data. Only emitted when the target model's
// ThinkingSpec.supported is true.
message RedactedThinking {
// The opaque, vendor-encrypted bytes. The kernel never inspects this.
bytes data = 1;
}
}

// StopReason classifies why a StreamCompletion ended, per model.md §4.
Expand Down
9 changes: 9 additions & 0 deletions api/pluggableharness/tool/v1/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@ message ToolSchema {
// TOOL_KIND_DATA_SOURCE operations are implicitly safe to retry
// regardless of this field.
bool idempotent = 10;

// Declares that the model calling this operation MUST be treated as an
// immediate, successful DoneCheck once this call's post-tool-call hook
// has fired, independent of whether other tool_use blocks were present
// in the same message — see
// agent-loop/turn-algorithm.md#done-detection. MAY, per operation; MAY
// be true only on a TOOL_KIND_RESOURCE operation. Absent or false means
// this operation does not terminate the turn.
bool terminates_turn = 11;
}

// ToolCall is one request to execute an operation, per tool.md §4.
Expand Down
117 changes: 117 additions & 0 deletions cmd/agent/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Command agent is the PluggableHarness kernel binary.
//
// This build runs exactly one non-interactive session: it loads agent.hcl,
// launches every resolved provider plugin, runs -prompt to completion,
// prints the session's final message to stdout, and exits. The interactive
// command docs/specifications/architecture.md#cli-shape describes arrives
// with the frontend plugin category; there is no REPL here yet.
//
// Everything below is wiring, per .claude/rules/go-layout.md: flags in,
// one internal/kernel.Run call, an exit code out.
package main

import (
"context"
"errors"
"flag"
"fmt"
"os"
"os/signal"
"runtime/debug"
"syscall"

"github.com/pluggableharness/agent/internal/kernel"
)

// Exit codes. 130 for a SIGINT follows the shell convention (128 + SIGINT),
// so a piped invocation can tell an operator's Ctrl-C apart from a real
// failure; 2 for a usage error matches flag's own convention.
const (
exitOK = 0
exitFailure = 1
exitUsage = 2
exitCanceled = 130
)

// version is overridden at release time via -ldflags; a `go build` from a
// checkout reports whatever the module's own build info knows.
var version = ""

func main() { os.Exit(run()) }

// run parses flags, runs one session, and maps the outcome to an exit
// code. It exists separately from main so every path returns rather than
// calling os.Exit from inside a nested scope, which would skip deferred
// cleanup.
func run() int {
fs := flag.NewFlagSet("agent", flag.ContinueOnError)
fs.SetOutput(os.Stderr)

var (
configPath = fs.String("config", kernel.DefaultConfigFile, "path to the agent.hcl config file")
profile = fs.String("profile", "", `agent_profile block to run under (default "default")`)
prompt = fs.String("prompt", "", "the prompt to run (required: this build has no interactive mode)")
logLevel = fs.String("log-level", "", "override settings.log_level (trace|debug|info|warn|error)")
showVersion = fs.Bool("version", false, "print the version and exit")
)

if err := fs.Parse(os.Args[1:]); err != nil {
// flag already wrote the message and the usage text.
if errors.Is(err, flag.ErrHelp) {
return exitOK
}
return exitUsage
}
if *showVersion {
_, _ = fmt.Fprintln(os.Stdout, buildVersion())
return exitOK
}
if *prompt == "" {
_, _ = fmt.Fprintln(os.Stderr, "agent: -prompt is required")
fs.Usage()
return exitUsage
}

// The one cancellation root: everything below derives from it, so a
// signal reaches the model stream, the tool calls, and the plugin
// subprocesses through the same context internal/kernel already
// threads everywhere.
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

err := kernel.Run(ctx, kernel.Options{
ConfigPath: *configPath,
Profile: *profile,
Prompt: *prompt,
LogLevel: *logLevel,
Stdout: os.Stdout,
Stderr: os.Stderr,
})
switch {
case err == nil:
return exitOK
case errors.Is(err, context.Canceled):
// A real SIGINT is a normal exit, not a failure to report: the
// kernel already persisted the session as cancelled and logged
// why.
return exitCanceled
default:
// The one sanctioned non-slog write in the tree: a config-load
// or path-resolution failure happens before logging is wired at
// all, so slog.Default() would still be stdlib's.
_, _ = fmt.Fprintln(os.Stderr, "agent:", err)
return exitFailure
}
}

// buildVersion reports the release version when one was stamped in, and
// falls back to the module's own recorded build info otherwise.
func buildVersion() string {
if version != "" {
return version
}
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "" {
return info.Main.Version
}
return "(devel)"
}
55 changes: 55 additions & 0 deletions cmd/anthropic/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Command anthropic is the Anthropic model-provider plugin.
//
// It is a hashicorp/go-plugin subprocess: the kernel launches it, speaks
// pluggableharness.model.v1.ModelService to it over gRPC, and kills it at
// session end. It is never run directly by a human — started from a
// shell it simply prints go-plugin's handshake line and waits.
//
// Everything here is wiring, per .claude/rules/go-layout.md: build the
// provider, hand it to pkg/model's service adapter, serve. All real logic
// lives in internal/anthropic.
package main

import (
"github.com/pluggableharness/agent/internal/anthropic"
commonv1 "github.com/pluggableharness/agent/pkg/common/proto/v1"
"github.com/pluggableharness/agent/pkg/model"
"github.com/pluggableharness/agent/pkg/plugin"
)

// Identity this build reports through the Describe RPC.
//
// These are variables rather than constants so a release build can stamp
// them with -ldflags (see .goreleaser.yaml), matching how
// internal/pluginhost's integration fixture is built. Describe has to
// answer from the running process because a dev_overrides binary has no
// agent.lock.hcl entry for the kernel to read identity from
// (configuration/lock-file.md#dev_overrides-and-identity-without-a-lock-entry).
var (
pluginName = "anthropic"
pluginVersion = "0.0.0"
pluginSource = "github.com/pluggableharness/agent-provider-anthropic"
)

func main() {
identity := plugin.Identity{
Name: pluginName,
Version: pluginVersion,
Source: pluginSource,
}

// The callback handle is constructed here and handed to both
// plugin.Serve and the model service, but is deliberately never
// dialed from main: pkg/plugin's "callback-timing trap" means
// Callback.Client may only be called from inside an RPC handler,
// after go-plugin has begun serving the broker.
callback := plugin.NewCallback()
provider := anthropic.New()

plugin.Serve(plugin.Config{
Identity: identity,
Category: commonv1.Category_CATEGORY_MODEL,
Callback: callback,
Services: []plugin.Service{model.NewService(provider, identity, callback)},
})
}
2 changes: 1 addition & 1 deletion docs/specifications/agent-loop/turn-algorithm.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Injecting a structured "final answer" turn — disabling tools and forcing text-

The kernel MUST support implicit done detection: a model response containing no `tool_use` blocks ends the turn loop successfully. This is the dominant pattern across surveyed harnesses and requires no cooperation from tool providers, which matters for a microkernel where tool providers are third-party and heterogeneous.

An explicit terminal-tool pattern is more reliable in harnesses that use it, because it lets a tool provider carry a structured completion report. The kernel MUST support this as an opt-in: any tool provider MAY declare a resource with a `terminates_turn: bool` schema annotation; if the model calls such a tool, the kernel treats it as `DoneCheck` success immediately after that call's `post-tool-call` hook, independent of whether other `tool_use` blocks were present in the same message. Implicit no-tool-calls remains the MUST-support baseline; explicit terminal tools are an additive MAY, resolving the tension between LLM providers that don't reliably call a terminal tool and tool providers that want a structured completion signal by keeping implicit detection as the non-negotiable floor and layering explicit termination on top where a tool provider opts in.
An explicit terminal-tool pattern is more reliable in harnesses that use it, because it lets a tool provider carry a structured completion report. The kernel MUST support this as an opt-in: any tool provider MAY declare a resource with a `terminates_turn: bool` schema annotation (the `ToolSchema.terminates_turn` field, [`tool/data-types.md#terminates_turn`](../tool/data-types.md#terminates_turn)); if the model calls such a tool, the kernel treats it as `DoneCheck` success immediately after that call's `post-tool-call` hook, independent of whether other `tool_use` blocks were present in the same message. Implicit no-tool-calls remains the MUST-support baseline; explicit terminal tools are an additive MAY, resolving the tension between LLM providers that don't reliably call a terminal tool and tool providers that want a structured completion signal by keeping implicit detection as the non-negotiable floor and layering explicit termination on top where a tool provider opts in.

### Doom-loop detection

Expand Down
2 changes: 1 addition & 1 deletion docs/specifications/event-bus.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ No other wildcard form exists in v1 — no mid-string wildcard, no multi-segment
## Delivery semantics

- **Best-effort, not guaranteed.** A `Publish` call returns as soon as the kernel has fanned the event out to every currently-subscribed stream's queue; it does not wait for any subscriber to actually receive or process the event, and a subscriber that connects after a `Publish` call already returned never sees that event. There is no backlog and no replay — this is `internal/eventbus`'s own ephemeral contract, inherited unchanged.
- **Per-subscriber ordering only.** A single `Subscribe` stream sees events matching its filters in the order they were published; there is no ordering guarantee across two different `Subscribe` streams, and no ordering guarantee relative to `ReadEvents` or anything hook-dispatch related. Nothing here carries a `sequence` number — [`determinism.md`](../.claude/rules/determinism.md)'s ordering-authority rule governs persisted, replay-critical ordering, and this bus persists nothing and participates in no replay, so it is deliberately outside that rule's scope, exactly as `internal/eventbus`'s own design notes already state.
- **Per-subscriber ordering only.** A single `Subscribe` stream sees events matching its filters in the order they were published; there is no ordering guarantee across two different `Subscribe` streams, and no ordering guarantee relative to `ReadEvents` or anything hook-dispatch related. Nothing here carries a `sequence` number — `.claude/rules/determinism.md`'s ordering-authority rule governs persisted, replay-critical ordering, and this bus persists nothing and participates in no replay, so it is deliberately outside that rule's scope, exactly as `internal/eventbus`'s own design notes already state.
- **Observe-only, never a veto.** Unlike a hook subscriber in `veto` mode, a bus subscriber cannot block, modify, or reject the event it received — the bus has no response channel for that at all. A slow or broken subscriber can only ever affect itself (see "Backpressure" below), never the publisher or any other subscriber.

## Backpressure
Expand Down
1 change: 1 addition & 0 deletions docs/specifications/model/conformance.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ On the wire, each category maps to a `grpc/codes.Code`: `context_length_exceeded
| `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 |
| `StreamEvent.redacted_thinking` | MUST, for a vendor that emits vendor-encrypted reasoning blocks | [`data-types.md#streamevent`](data-types.md#streamevent) — a whole block, never fragmented; stored and echoed back verbatim or the vendor rejects the whole conversation on a later turn |
| 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 |
Expand Down
4 changes: 4 additions & 0 deletions docs/specifications/model/data-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ StreamEvent = oneof {
// MUST be emitted if the vendor's
// thinking blocks carry an integrity
// signature
redacted_thinking { data: bytes } // a complete vendor-encrypted
// reasoning block, not a fragment;
// only when ThinkingSpec.supported
tool_call_start { id: string, name: string }
tool_call_delta { id: string, arguments_fragment: string } // partial-JSON accumulation
tool_call_done { id: string }
Expand Down Expand Up @@ -152,6 +155,7 @@ Per [`architecture.md`](../architecture.md#canonical-message--tool-schema-format
- `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).
- A `redacted_thinking` block is the whole-block analogue of that rule: reasoning content the vendor encrypts outright, opaque even as text, that the plugin MUST still store verbatim and echo back unmodified on a later turn or the vendor rejects the entire conversation — not just the affected block. On the wire it arrives as [`StreamEvent`](#streamevent)'s `redacted_thinking` variant carrying `data: bytes`, and unlike `thinking_delta` it is never fragmented across events: there is nothing for the kernel to accumulate, so the vendor emits the block whole and the plugin forwards it whole into `ContentBlock`'s `redacted_thinking`. A plugin serving a vendor that produces such blocks MUST emit this variant rather than dropping the content or flattening it into a `thinking` block.

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).

Expand Down
Loading
Loading