diff --git a/README.md b/README.md index e92b7c89..d781c46f 100644 --- a/README.md +++ b/README.md @@ -378,6 +378,7 @@ When a reasoning model tries to govern itself, the guardrails are part of the sa - **Audit logging:** Structured JSON on stdout — timestamp, agent, model, latency, tokens, cost, intervention reason. - **Managed tool mediation:** Services declare callable tools via `claw.describe` (MCP-shaped schemas). `claw up` compiles per-agent `tools.json`. cllama injects tools into LLM requests, intercepts `tool_call` responses, executes them against the service, and loops until terminal text — transparent to the runner. HTTP services, Streamable HTTP MCP sidecars, and stdio MCP servers wrapped by `claw-mcp-stdio` are supported. - **Ambient memory plane:** Services declare `recall`, `retain`, and `forget` endpoints via `claw.describe`. `claw up` compiles per-agent `memory.json`. cllama calls `/recall` before each inference turn and `/retain` after each successful response (async, non-blocking). Memory intelligence stays in swappable external services — the proxy owns orchestration only. +- **Context blocks:** Operators can compile short per-agent context snippets into `context-blocks.json`; cllama injects enabled blocks into late runtime context so durable operating focus stays visible without expanding the primary contract. - **Operator dashboard:** Real-time web UI at host port 8181 by default (container `:8081`) — agent activity, provider status, cost breakdown. The reference implementation is [`cllama`](https://github.com/mostlydev/cllama) — a zero-dependency Go binary that implements the transport layer (identity, routing, cost tracking, budget enforcement). Future proxy types (`cllama-policy`) will add bidirectional interception: evaluating outbound prompts and amending inbound responses against the agent's behavioral contract. @@ -554,7 +555,7 @@ inline the service's advertised tools and the mount path: Clawdapus is designed for autonomous fleet governance. The operator writes the `Clawfile` and sets the budgets, but day-to-day oversight can be delegated to a **Master Claw** — an AI governor. **The Governance Proxy is its Sensory Organ:** -The `cllama` proxy is the programmatic choke point. It sits on the network, holds provider credentials, applies compiled model/tool/context/budget policy, rejects over-cap turns, and emits structured telemetry (cost, interventions, tool rounds). It doesn't "think" about management; it is a passive sensor and firewall. +The `cllama` proxy is the programmatic choke point. It sits on the network, holds provider credentials, applies compiled model/tool/context/budget policy, rejects over-cap turns, injects compiled context blocks, and emits structured telemetry (cost, interventions, tool rounds). It doesn't "think" about management; it is a passive sensor and firewall. **The Master Claw is the Brain:** The Master Claw is an actual LLM-powered agent running in the pod, reading proxy telemetry and acting on it. `x-claw.master` wires this today: it auto-injects a `claw-api` service and hands the governor a scoped bearer token and `CLAW_API_URL`, so it can read fleet telemetry and act through an authenticated, scope-checked API. The executive policy it runs — shifting enforced budget caps, quarantining a high-cost or off-policy agent, promoting a recipe — is operator-defined (recipe promotion is still on the roadmap). diff --git a/cmd/claw-api/agent_context.go b/cmd/claw-api/agent_context.go index 2100890e..032845f6 100644 --- a/cmd/claw-api/agent_context.go +++ b/cmd/claw-api/agent_context.go @@ -26,14 +26,15 @@ type agentIndexEntry struct { } type agentContractResponse struct { - ClawID string `json:"claw_id"` - AgentsMD string `json:"agents_md"` - ClawdapusMD string `json:"clawdapus_md"` - Metadata any `json:"metadata"` - Feeds any `json:"feeds"` - Tools any `json:"tools"` - Memory any `json:"memory"` - ServiceAuth map[string]any `json:"service_auth,omitempty"` + ClawID string `json:"claw_id"` + AgentsMD string `json:"agents_md"` + ClawdapusMD string `json:"clawdapus_md"` + Metadata any `json:"metadata"` + Feeds any `json:"feeds"` + Tools any `json:"tools"` + Memory any `json:"memory"` + ContextBlocks any `json:"context_blocks"` + ServiceAuth map[string]any `json:"service_auth,omitempty"` } type agentContextPath struct { @@ -134,6 +135,11 @@ func (h *apiHandler) handleAgentContract(w http.ResponseWriter, agentID string) writeJSONError(w, http.StatusInternalServerError, err.Error()) return } + contextBlocks, err := readJSONArtifact(filepath.Join(agentDir, "context-blocks.json"), true) + if err != nil { + writeJSONError(w, http.StatusInternalServerError, err.Error()) + return + } serviceAuth, err := readServiceAuthArtifacts(filepath.Join(agentDir, "service-auth")) if err != nil { writeJSONError(w, http.StatusInternalServerError, err.Error()) @@ -141,14 +147,15 @@ func (h *apiHandler) handleAgentContract(w http.ResponseWriter, agentID string) } writeJSON(w, http.StatusOK, agentContractResponse{ - ClawID: agentID, - AgentsMD: string(agentsMD), - ClawdapusMD: string(clawdapusMD), - Metadata: redactJSONValue(metadata), - Feeds: redactJSONValue(feeds), - Tools: redactJSONValue(tools), - Memory: redactJSONValue(memory), - ServiceAuth: redactServiceAuthArtifacts(serviceAuth), + ClawID: agentID, + AgentsMD: string(agentsMD), + ClawdapusMD: string(clawdapusMD), + Metadata: redactJSONValue(metadata), + Feeds: redactJSONValue(feeds), + Tools: redactJSONValue(tools), + Memory: redactJSONValue(memory), + ContextBlocks: redactJSONValue(contextBlocks), + ServiceAuth: redactServiceAuthArtifacts(serviceAuth), }) } diff --git a/cmd/claw-api/agent_context_test.go b/cmd/claw-api/agent_context_test.go index 0353fb0a..ea5a7c3f 100644 --- a/cmd/claw-api/agent_context_test.go +++ b/cmd/claw-api/agent_context_test.go @@ -95,6 +95,9 @@ func TestAgentContractRedactsContextCredentials(t *testing.T) { if err := os.WriteFile(filepath.Join(agentDir, "memory.json"), []byte(`{"service":"mem","auth":{"type":"bearer","token":"memory-token"}}`), 0o644); err != nil { t.Fatal(err) } + if err := os.WriteFile(filepath.Join(agentDir, "context-blocks.json"), []byte(`{"version":1,"blocks":[{"id":"focus","kind":"runtime_motivation","text":"Stay on the active operating contract.","enabled":true,"placement":"after_feeds","cadence":"every_turn","max_chars":800}]}`), 0o644); err != nil { + t.Fatal(err) + } authDir := filepath.Join(agentDir, "service-auth") if err := os.MkdirAll(authDir, 0o700); err != nil { t.Fatal(err) @@ -147,6 +150,11 @@ func TestAgentContractRedactsContextCredentials(t *testing.T) { if clawAPIAuth["token"] != "[REDACTED]" { t.Fatalf("service-auth token was not redacted: %+v", clawAPIAuth) } + contextBlocks := resp["context_blocks"].(map[string]any) + blocks := contextBlocks["blocks"].([]any) + if blocks[0].(map[string]any)["id"] != "focus" || blocks[0].(map[string]any)["kind"] != "runtime_motivation" { + t.Fatalf("context blocks were not returned: %+v", contextBlocks) + } } func TestAgentContractPrefersEffectiveAgentsMD(t *testing.T) { diff --git a/cmd/claw/compose_up.go b/cmd/claw/compose_up.go index 7fd9313f..37d547e6 100644 --- a/cmd/claw/compose_up.go +++ b/cmd/claw/compose_up.go @@ -600,6 +600,7 @@ func runComposeUp(podFile string) (err error) { Tools: tools, ToolPolicy: agentToolPolicy(p, name), Memory: memory, + ContextBlocks: agentContextBlocks(p, name), ServiceAuth: ordinalAuth, ChannelAllowlist: conversationWallAllowlists[ordinalName], Metadata: injectAgentBudget(cllama.InjectCompiledModelPolicy(map[string]any{ @@ -647,6 +648,7 @@ func runComposeUp(podFile string) (err error) { Tools: tools, ToolPolicy: agentToolPolicy(p, name), Memory: memory, + ContextBlocks: agentContextBlocks(p, name), ServiceAuth: svcAuth, ChannelAllowlist: conversationWallAllowlists[name], Metadata: injectAgentBudget(cllama.InjectCompiledModelPolicy(map[string]any{ @@ -1628,6 +1630,35 @@ func agentBudgetPolicy(p *pod.Pod, serviceName string) *cllama.BudgetPolicy { } } +func agentContextBlocks(p *pod.Pod, serviceName string) []cllama.ContextBlockManifestEntry { + if p == nil { + return nil + } + var blocks []pod.ContextBlockConfig + if p.Context != nil && p.Context.Blocks != nil { + blocks = p.Context.Blocks + } + if svc := p.Services[serviceName]; svc != nil && svc.Claw != nil && svc.Claw.Context != nil && svc.Claw.Context.Blocks != nil { + blocks = svc.Claw.Context.Blocks + } + if len(blocks) == 0 { + return nil + } + out := make([]cllama.ContextBlockManifestEntry, 0, len(blocks)) + for _, block := range blocks { + out = append(out, cllama.ContextBlockManifestEntry{ + ID: block.ID, + Kind: block.Kind, + Text: block.Text, + Enabled: block.Enabled, + Placement: block.Placement, + MaxChars: block.MaxChars, + Cadence: block.Cadence, + }) + } + return out +} + func injectAgentBudget(meta map[string]any, budget *cllama.BudgetPolicy) map[string]any { if budget != nil { meta["budget"] = budget diff --git a/cmd/claw/context_blocks_test.go b/cmd/claw/context_blocks_test.go new file mode 100644 index 00000000..17c921db --- /dev/null +++ b/cmd/claw/context_blocks_test.go @@ -0,0 +1,52 @@ +package main + +import ( + "testing" + + "github.com/mostlydev/clawdapus/internal/pod" +) + +func TestAgentContextBlocksResolvesPodAndServiceConfig(t *testing.T) { + p := &pod.Pod{ + Context: &pod.ContextConfig{ + Blocks: []pod.ContextBlockConfig{{ + ID: "pod-focus", + Kind: "runtime_motivation", + Text: "Pod block.", + Enabled: true, + Placement: "after_feeds", + MaxChars: 800, + Cadence: "every_turn", + }}, + }, + Services: map[string]*pod.Service{ + "inherited": {Claw: &pod.ClawBlock{}}, + "override": {Claw: &pod.ClawBlock{Context: &pod.ContextConfig{ + Blocks: []pod.ContextBlockConfig{{ + ID: "local-focus", + Kind: "feed_frame", + Text: "Local block.", + Enabled: false, + Placement: "before_feeds", + MaxChars: 400, + Cadence: "every_turn", + }}, + }}}, + "suppress": {Claw: &pod.ClawBlock{Context: &pod.ContextConfig{ + Blocks: []pod.ContextBlockConfig{}, + }}}, + }, + } + + inherited := agentContextBlocks(p, "inherited") + if len(inherited) != 1 || inherited[0].ID != "pod-focus" || inherited[0].Kind != "runtime_motivation" || inherited[0].Placement != "after_feeds" || !inherited[0].Enabled { + t.Fatalf("expected inherited pod context block, got %+v", inherited) + } + override := agentContextBlocks(p, "override") + if len(override) != 1 || override[0].ID != "local-focus" || override[0].Kind != "feed_frame" || override[0].Enabled || override[0].MaxChars != 400 { + t.Fatalf("expected service context block override, got %+v", override) + } + if suppressed := agentContextBlocks(p, "suppress"); suppressed != nil { + t.Fatalf("expected empty service override to suppress pod context blocks, got %+v", suppressed) + } +} diff --git a/cmd/claw/skill_data/SKILL.md b/cmd/claw/skill_data/SKILL.md index aa94202c..1123e140 100644 --- a/cmd/claw/skill_data/SKILL.md +++ b/cmd/claw/skill_data/SKILL.md @@ -42,7 +42,8 @@ claw agent add [name] # add agent service to existing pod claw audit [--since ] [--claw ] [--type ] [--json] # summarize cllama telemetry from container logs # types: request, response, error, intervention, - # feed_fetch, provider_pool, tool_call + # feed_fetch, feed_injection, context_block, + # memory_op, provider_pool, tool_call claw api schedule # inspect/control scheduled invocations via claw-api # list | get | pause | resume | skip-next | # clear-skip-next | fire @@ -305,6 +306,12 @@ When a service subscribes to a memory service via `x-claw.memory`, cllama perfor `claw up` compiles `memory.json` into each subscribing agent's cllama context directory with endpoint URLs, auth tokens, and timeout configuration. +### Context Blocks + +Pod or service `x-claw.context.blocks` compiles short operator-authored snippets into each agent's cllama context directory as `context-blocks.json`. cllama injects enabled blocks into late runtime context before or after feeds on every turn, so durable operating focus stays visible without expanding the primary contract. + +Supported fields: `id` (required), `text` (required), `enabled` (default `true`), `cadence` (`every_turn`), `placement` (`before_feeds` or `after_feeds`, default `after_feeds`), and `max-chars` / `max_chars` (default `800`). A service-level list replaces pod-level blocks; an explicit empty list suppresses inherited blocks. + ### Managed Tool Mediation (v0.5.0) When a service subscribes to tools via `x-claw.tools`, cllama performs bounded tool execution within the inference turn: @@ -374,6 +381,7 @@ The proxy sits between agents and LLM providers. Agents get bearer tokens, proxy CLAWDAPUS.md # infrastructure map tools.json # managed tool manifest (when tools subscribed) memory.json # memory service config (when memory subscribed) + context-blocks.json # context block manifest (when configured) ``` ### Provider support @@ -425,6 +433,7 @@ When the aggregate cap drops a feed the model sees an explicit `--- FEED: | `jobs.json` | Cron schedule for INVOKE tasks | Runner state directory | | `tools.json` | Managed tool manifest per agent | cllama context directory | | `memory.json` | Memory service config per agent | cllama context directory | +| `context-blocks.json` | Runtime context block manifest per agent | cllama context directory | ## Drivers @@ -493,7 +502,7 @@ When a pod declares a `clawdash` surface, `claw up` publishes the operational da - **Fleet / Topology** — running services, wiring, driver types. - **Agents** — per-agent *contract* as compiled at `claw up` time (AGENTS.md, CLAWDAPUS.md, feed subscriptions, managed tools, memory wiring, metadata). -- **Agents → Live Context** — the system message, tools array, injected feeds, memory recall, time context, and interventions that were assembled for the most recent inference turn. Sourced from the cllama snapshot store (`/internal/context//snapshot`, proxied through `claw-api`). Credentials and token fields are redacted. +- **Agents → Live Context** — the system message, tools array, context blocks, injected feeds, memory recall, time context, and interventions that were assembled for the most recent inference turn. Sourced from the cllama snapshot store (`/internal/context//snapshot`, proxied through `claw-api`). Credentials and token fields are redacted. - **Schedule** — `INVOKE` and `x-claw.invoke` cron entries, with `claw api schedule ...` controls. All views are read-only and scoped through `claw-api` principals. Use this before log-diving — "what did the model actually see last turn" has a direct answer here. @@ -521,6 +530,11 @@ All views are read-only and scoped through `claw-api` principals. Use this befor - `claw memory forget --entry-id ` writes tombstones; subsequent backfills skip those entries - Declaring `memory:` without `cllama:` is a hard error +### Context blocks not visible +- Check `context-blocks.json` in `.claw-runtime/context//` +- Check `claw audit --type context_block` for injected or skipped context block entries +- Verify `cadence` is `every_turn`, `placement` is `before_feeds` or `after_feeds`, and the block text is within `max-chars` + ## Working Examples | Example | Path | What it demonstrates | diff --git a/cmd/clawdash/agent_context.go b/cmd/clawdash/agent_context.go index 1b283e10..53ecab6e 100644 --- a/cmd/clawdash/agent_context.go +++ b/cmd/clawdash/agent_context.go @@ -34,14 +34,15 @@ type agentContextIndexEntry struct { } type agentContractView struct { - ClawID string `json:"claw_id"` - AgentsMD string `json:"agents_md"` - ClawdapusMD string `json:"clawdapus_md"` - Metadata any `json:"metadata"` - Feeds any `json:"feeds"` - Tools any `json:"tools"` - Memory any `json:"memory"` - ServiceAuth map[string]any `json:"service_auth,omitempty"` + ClawID string `json:"claw_id"` + AgentsMD string `json:"agents_md"` + ClawdapusMD string `json:"clawdapus_md"` + Metadata any `json:"metadata"` + Feeds any `json:"feeds"` + Tools any `json:"tools"` + Memory any `json:"memory"` + ContextBlocks any `json:"context_blocks"` + ServiceAuth map[string]any `json:"service_auth,omitempty"` } type agentContextHTTPClient struct { @@ -174,47 +175,51 @@ type agentsPageData struct { } type agentContextDetailPageData struct { - PodName string - ActiveTab string - HasSchedule bool - HasAgentContext bool - ContextTab string - IsContractTab bool - IsLiveTab bool - ContractPath string - LivePath string - ClawID string - Service string - ClawType string - Contract agentContractView - HasContract bool - LiveContext agentLiveContextView - LiveContextJSON string - HasLiveContext bool - ContractError string - LiveError string - HasContractErr bool - HasLiveError bool - MetadataJSON string - FeedsJSON string - ToolsJSON string - MemoryJSON string - ServiceAuthJSON string - HasMetadata bool - HasFeeds bool - HasTools bool - HasMemory bool - HasServiceAuth bool - MetadataRows []keyValueRow - FeedRows []feedManifestRow - ToolRows []toolManifestRow - MemoryRows []keyValueRow - ServiceAuthRows []serviceAuthRow - HasMetadataRows bool - HasFeedRows bool - HasToolRows bool - HasMemoryRows bool - HasServiceAuthRows bool + PodName string + ActiveTab string + HasSchedule bool + HasAgentContext bool + ContextTab string + IsContractTab bool + IsLiveTab bool + ContractPath string + LivePath string + ClawID string + Service string + ClawType string + Contract agentContractView + HasContract bool + LiveContext agentLiveContextView + LiveContextJSON string + HasLiveContext bool + ContractError string + LiveError string + HasContractErr bool + HasLiveError bool + MetadataJSON string + FeedsJSON string + ToolsJSON string + MemoryJSON string + ContextBlockJSON string + ServiceAuthJSON string + HasMetadata bool + HasFeeds bool + HasTools bool + HasMemory bool + HasContextBlock bool + HasServiceAuth bool + MetadataRows []keyValueRow + FeedRows []feedManifestRow + ToolRows []toolManifestRow + MemoryRows []keyValueRow + ContextBlockRows []contextBlockRow + ServiceAuthRows []serviceAuthRow + HasMetadataRows bool + HasFeedRows bool + HasToolRows bool + HasMemoryRows bool + HasContextBlockRows bool + HasServiceAuthRows bool } type keyValueRow struct { @@ -249,6 +254,16 @@ type serviceAuthRow struct { DetailJSON string } +type contextBlockRow struct { + ID string + Kind string + Enabled string + Cadence string + Placement string + MaxChars string + Text string +} + type candidateContextRow struct { Provider string UpstreamModel string @@ -430,49 +445,54 @@ func buildAgentContextDetailPageData(podName, agentID, tab string, contract agen feedRows := feedManifestRows(contract.Feeds) toolRows := toolManifestRows(contract.Tools) memoryRows := topLevelRows(contract.Memory) + contextBlockRows := contextBlockRows(contract.ContextBlocks) serviceAuthRows := serviceAuthRows(contract.ServiceAuth) data := agentContextDetailPageData{ - PodName: podName, - ActiveTab: "agents", - HasSchedule: hasSchedule, - HasAgentContext: hasAgentContext, - ContextTab: tab, - IsContractTab: tab == "contract", - IsLiveTab: tab == "live", - ContractPath: "/agents/" + url.PathEscape(clawID) + "?tab=contract", - LivePath: "/agents/" + url.PathEscape(clawID) + "?tab=live", - ClawID: clawID, - Service: metadataString(contract.Metadata, "service"), - ClawType: metadataString(contract.Metadata, "type"), - Contract: contract, - HasContract: contractErr == nil, - LiveContext: liveView, - ContractError: errString(contractErr), - LiveError: errString(liveErr), - HasContractErr: contractErr != nil, - HasLiveError: tab == "live" && liveErr != nil, - MetadataJSON: prettyJSON(contract.Metadata), - FeedsJSON: prettyJSON(contract.Feeds), - ToolsJSON: prettyJSON(contract.Tools), - MemoryJSON: prettyJSON(contract.Memory), - ServiceAuthJSON: prettyJSON(contract.ServiceAuth), - LiveContextJSON: liveView.RawJSON, - MetadataRows: metadataRows, - FeedRows: feedRows, - ToolRows: toolRows, - MemoryRows: memoryRows, - ServiceAuthRows: serviceAuthRows, + PodName: podName, + ActiveTab: "agents", + HasSchedule: hasSchedule, + HasAgentContext: hasAgentContext, + ContextTab: tab, + IsContractTab: tab == "contract", + IsLiveTab: tab == "live", + ContractPath: "/agents/" + url.PathEscape(clawID) + "?tab=contract", + LivePath: "/agents/" + url.PathEscape(clawID) + "?tab=live", + ClawID: clawID, + Service: metadataString(contract.Metadata, "service"), + ClawType: metadataString(contract.Metadata, "type"), + Contract: contract, + HasContract: contractErr == nil, + LiveContext: liveView, + ContractError: errString(contractErr), + LiveError: errString(liveErr), + HasContractErr: contractErr != nil, + HasLiveError: tab == "live" && liveErr != nil, + MetadataJSON: prettyJSON(contract.Metadata), + FeedsJSON: prettyJSON(contract.Feeds), + ToolsJSON: prettyJSON(contract.Tools), + MemoryJSON: prettyJSON(contract.Memory), + ContextBlockJSON: prettyJSON(contract.ContextBlocks), + ServiceAuthJSON: prettyJSON(contract.ServiceAuth), + LiveContextJSON: liveView.RawJSON, + MetadataRows: metadataRows, + FeedRows: feedRows, + ToolRows: toolRows, + MemoryRows: memoryRows, + ContextBlockRows: contextBlockRows, + ServiceAuthRows: serviceAuthRows, } data.HasMetadata = data.MetadataJSON != "" data.HasFeeds = data.FeedsJSON != "" data.HasTools = data.ToolsJSON != "" data.HasMemory = data.MemoryJSON != "" + data.HasContextBlock = data.ContextBlockJSON != "" data.HasServiceAuth = data.ServiceAuthJSON != "" data.HasLiveContext = data.LiveContextJSON != "" data.HasMetadataRows = len(metadataRows) > 0 data.HasFeedRows = len(feedRows) > 0 data.HasToolRows = len(toolRows) > 0 data.HasMemoryRows = len(memoryRows) > 0 + data.HasContextBlockRows = len(contextBlockRows) > 0 data.HasServiceAuthRows = len(serviceAuthRows) > 0 return data } @@ -561,6 +581,34 @@ func scalarInt(v any) int { } } +func contextBlockRows(blocks any) []contextBlockRow { + root, ok := blocks.(map[string]any) + if !ok { + return nil + } + rawList, ok := root["blocks"].([]any) + if !ok { + return nil + } + rows := make([]contextBlockRow, 0, len(rawList)) + for _, item := range rawList { + entry, ok := item.(map[string]any) + if !ok { + continue + } + rows = append(rows, contextBlockRow{ + ID: scalarString(entry["id"]), + Kind: scalarString(entry["kind"]), + Enabled: displayValue(entry["enabled"]), + Cadence: scalarString(entry["cadence"]), + Placement: scalarString(entry["placement"]), + MaxChars: displayValue(entry["max_chars"]), + Text: scalarString(entry["text"]), + }) + } + return rows +} + func displayValue(v any) string { if s := scalarString(v); s != "" { return s diff --git a/cmd/clawdash/handler_test.go b/cmd/clawdash/handler_test.go index 812fbfb3..bd808bb1 100644 --- a/cmd/clawdash/handler_test.go +++ b/cmd/clawdash/handler_test.go @@ -403,6 +403,20 @@ func TestAgentContextDetailRendersContractAndLiveSnapshot(t *testing.T) { "type": "openclaw", }, Feeds: map[string]any{"feeds": []any{"alerts"}}, + ContextBlocks: map[string]any{ + "version": float64(1), + "blocks": []any{ + map[string]any{ + "id": "focus", + "kind": "runtime_motivation", + "text": "Stay on the active operating contract.", + "enabled": true, + "placement": "after_feeds", + "cadence": "every_turn", + "max_chars": float64(800), + }, + }, + }, }, liveContext: map[string]any{ "agent_id": "bot-0", @@ -458,7 +472,7 @@ func TestAgentContextDetailRendersContractAndLiveSnapshot(t *testing.T) { t.Fatalf("expected 200, got %d body=%s", w.Code, w.Body.String()) } body := w.Body.String() - for _, want := range []string{"# Contract", "# Infrastructure", "alerts", "openclaw", "Compiled manifests", "Runtime inputs"} { + for _, want := range []string{"# Contract", "# Infrastructure", "alerts", "openclaw", "Context blocks", "context-blocks.json", "Compiled manifests", "Runtime inputs"} { if !strings.Contains(body, want) { t.Fatalf("expected %q in body:\n%s", want, body) } diff --git a/cmd/clawdash/templates/agent_detail.html b/cmd/clawdash/templates/agent_detail.html index 02323a85..dd94ff3a 100644 --- a/cmd/clawdash/templates/agent_detail.html +++ b/cmd/clawdash/templates/agent_detail.html @@ -259,6 +259,21 @@

Runtime inputs

{{end}} + {{if .HasContextBlockRows}} +
+
Context blocks
+
+ + + + {{range .ContextBlockRows}} + + {{end}} + +
IDKindEnabledCadencePlacementMax charsText
{{.ID}}{{.Kind}}{{.Enabled}}{{.Cadence}}{{.Placement}}{{.MaxChars}}{{.Text}}
+
+
+ {{end}} {{if .HasServiceAuthRows}}
Service credentials
@@ -274,10 +289,10 @@

Runtime inputs

{{end}} - {{if and (not .HasMetadata) (not .HasFeeds) (not .HasTools) (not .HasMemory) (not .HasServiceAuth)}} + {{if and (not .HasMetadata) (not .HasFeeds) (not .HasTools) (not .HasMemory) (not .HasContextBlock) (not .HasServiceAuth)}}
No generated JSON artifacts were returned for this agent.
{{end}} - {{if or .HasMetadata .HasFeeds .HasTools .HasMemory .HasServiceAuth}} + {{if or .HasMetadata .HasFeeds .HasTools .HasMemory .HasContextBlock .HasServiceAuth}}
Raw runtime JSON {{if .HasMetadata}}
metadata.json
@@ -288,6 +303,8 @@ 

Runtime inputs

{{.ToolsJSON}}
{{end}} {{if .HasMemory}}
memory.json
 {{.MemoryJSON}}
{{end}} + {{if .HasContextBlock}}
context-blocks.json
+{{.ContextBlockJSON}}
{{end}} {{if .HasServiceAuth}}
service-auth
 {{.ServiceAuthJSON}}
{{end}}
diff --git a/docs/CLLAMA_SPEC.md b/docs/CLLAMA_SPEC.md index 3b285acc..6ab22eb9 100644 --- a/docs/CLLAMA_SPEC.md +++ b/docs/CLLAMA_SPEC.md @@ -52,7 +52,8 @@ Clawdapus bind-mounts a shared directory into the proxy (at `CLAW_CONTEXT_ROOT`) ├── crypto-crusher-0/ │ ├── AGENTS.md # Compiled contract (includes, enforce, guide) │ ├── CLAWDAPUS.md # Infrastructure map -│ └── metadata.json # Identity, handles, and active policy modules +│ ├── metadata.json # Identity, handles, and active policy modules +│ └── context-blocks.json # Optional operator-authored context blocks ├── crypto-crusher-1/ │ └── ... ``` @@ -73,7 +74,7 @@ The proxy SHOULD execute the following pipeline: 3. **Model Validation:** Ensure the requested `model` is within the `CLAW_ALLOWED_MODELS` list (parsed from `metadata.json`). ### B. Outbound Interception (Context, Routing & Policy Slots) -1. **Context Aggregation:** The proxy loads the agent-specific compiled context from `CLAW_CONTEXT_ROOT` and MAY inject infrastructure-owned runtime context such as feeds, memory recall, time context, and channel deltas. +1. **Context Aggregation:** The proxy loads the agent-specific compiled context from `CLAW_CONTEXT_ROOT` and MAY inject infrastructure-owned runtime context such as context blocks, feeds, memory recall, time context, and channel deltas. 2. **Tool Scoping:** If the agent's request contains `tools`, the proxy evaluates the request against the compiled tool manifest for that agent. The reference implementation only exposes tools declared for that agent; policy-plane implementations MAY further filter or deny tools. 3. **Prompt Decoration (Pre-Prompting):** Policy-plane implementations MAY modify the outbound `messages` array, injecting specific rules, priorities, or warnings based on the compiled context. The passthrough reference does not perform policy prompt decoration. 4. **Policy Blocking:** If the outbound prompt violates a loaded policy module, a policy-plane implementation MAY short-circuit the request and return an error or a mock response. The passthrough reference does not perform policy blocking. @@ -97,13 +98,14 @@ The `cllama` proxy MUST emit structured JSON logs to `stdout`. Clawdapus collect Logs must contain the following fields: - `ts`: ISO-8601 UTC timestamp. - `claw_id`: The calling agent. -- `type`: one of `request`, `response`, `error`, `intervention`, `feed_fetch`, `feed_injection`, `memory_op`, `channel_context_op`, or `provider_pool`. +- `type`: one of `request`, `response`, `error`, `intervention`, `feed_fetch`, `feed_injection`, `context_block`, `memory_op`, `channel_context_op`, or `provider_pool`. - `intervention`: If the proxy modified routing, mediation, or other request handling, it describes why. In the reference logger this field is present on every event and is `null` when no intervention occurred. Event-specific fields may also be present: - `status_code`, `latency_ms`, `tokens_in`, `tokens_out`, `cost_usd` for request/response/error events - `feed_name`, `feed_url` for feed fetch events - `feed_name`, `source`, `feed_status`, and byte-budget fields for feed injection events +- `context_block_id`, `context_block_kind`, `context_block_status`, `context_block_cadence`, `context_block_placement`, and `context_block_reason` for context block events - `kind`, `channels`, `retained`, `returned`, `omitted`, byte counts, and source/status fields for channel context operations - `provider`, `key_id`, `action`, `reason`, `cooldown_until` for provider-pool events - `memory_service`, `memory_op`, `memory_status`, `memory_blocks`, `memory_bytes`, `memory_removed` for memory telemetry events diff --git a/internal/cllama/context.go b/internal/cllama/context.go index c60882ca..4c65f583 100644 --- a/internal/cllama/context.go +++ b/internal/cllama/context.go @@ -17,6 +17,7 @@ type AgentContextInput struct { Tools []ToolManifestEntry ToolPolicy *ToolPolicy // nil means DefaultToolPolicy Memory *MemoryManifestEntry + ContextBlocks []ContextBlockManifestEntry ServiceAuth []ServiceAuthEntry ChannelAllowlist []string } @@ -100,6 +101,21 @@ type MemoryOp struct { TimeoutMS int `json:"timeout_ms,omitempty"` } +type ContextBlockManifest struct { + Version int `json:"version"` + Blocks []ContextBlockManifestEntry `json:"blocks"` +} + +type ContextBlockManifestEntry struct { + ID string `json:"id"` + Kind string `json:"kind,omitempty"` + Text string `json:"text"` + Enabled bool `json:"enabled"` + Placement string `json:"placement"` + MaxChars int `json:"max_chars"` + Cadence string `json:"cadence"` +} + var DefaultToolPolicy = ToolPolicy{ MaxRounds: 8, TimeoutPerToolMS: 30000, @@ -124,7 +140,7 @@ func EffectiveToolPolicy(maxRounds, timeoutPerToolMS, totalTimeoutMS *int) ToolP // GenerateContextDir writes per-agent context files under: // -// /context//{AGENTS.md,AGENTS.effective.md,CLAWDAPUS.md,metadata.json,feeds.json,tools.json,memory.json,service-auth/...} +// /context//{AGENTS.md,AGENTS.effective.md,CLAWDAPUS.md,metadata.json,feeds.json,tools.json,memory.json,context-blocks.json,service-auth/...} func GenerateContextDir(runtimeDir string, agents []AgentContextInput) error { for _, agent := range agents { if agent.AgentID == "" { @@ -196,6 +212,19 @@ func GenerateContextDir(runtimeDir string, agents []AgentContextInput) error { } } + if len(agent.ContextBlocks) > 0 { + blocksJSON, err := json.MarshalIndent(ContextBlockManifest{ + Version: 1, + Blocks: append([]ContextBlockManifestEntry(nil), agent.ContextBlocks...), + }, "", " ") + if err != nil { + return fmt.Errorf("marshal context blocks for %q: %w", agent.AgentID, err) + } + if err := os.WriteFile(filepath.Join(agentDir, "context-blocks.json"), append(blocksJSON, '\n'), 0644); err != nil { + return fmt.Errorf("write context-blocks.json for %q: %w", agent.AgentID, err) + } + } + if len(agent.ChannelAllowlist) > 0 { allowlistJSON, err := json.MarshalIndent(ChannelAllowlistManifest{ Version: 1, diff --git a/internal/cllama/context_test.go b/internal/cllama/context_test.go index b55b6bf1..f056956b 100644 --- a/internal/cllama/context_test.go +++ b/internal/cllama/context_test.go @@ -129,6 +129,15 @@ func TestGenerateContextDirWritesOptionalFeedsAndServiceAuth(t *testing.T) { }, Auth: &AuthEntry{Type: "bearer", Token: "memory-token"}, }, + ContextBlocks: []ContextBlockManifestEntry{{ + ID: "focus", + Kind: "runtime_motivation", + Text: "Keep the operating contract visible.", + Enabled: true, + Placement: "after_feeds", + MaxChars: 800, + Cadence: "every_turn", + }}, ServiceAuth: []ServiceAuthEntry{{ Service: "claw-api", AuthType: "bearer", @@ -186,6 +195,18 @@ func TestGenerateContextDirWritesOptionalFeedsAndServiceAuth(t *testing.T) { t.Fatalf("unexpected memory manifest payload: %v", memory) } + blocksRaw, err := os.ReadFile(filepath.Join(dir, "context", "octopus", "context-blocks.json")) + if err != nil { + t.Fatal(err) + } + var blocks ContextBlockManifest + if err := json.Unmarshal(blocksRaw, &blocks); err != nil { + t.Fatal(err) + } + if blocks.Version != 1 || len(blocks.Blocks) != 1 || blocks.Blocks[0].ID != "focus" || blocks.Blocks[0].Kind != "runtime_motivation" || blocks.Blocks[0].Placement != "after_feeds" || !blocks.Blocks[0].Enabled { + t.Fatalf("unexpected context blocks manifest: %+v", blocks) + } + authRaw, err := os.ReadFile(filepath.Join(dir, "context", "octopus", "service-auth", "claw-api.json")) if err != nil { t.Fatal(err) diff --git a/internal/pod/parser.go b/internal/pod/parser.go index d6edc08b..dd6bffde 100644 --- a/internal/pod/parser.go +++ b/internal/pod/parser.go @@ -8,6 +8,7 @@ import ( "strconv" "strings" "time" + "unicode/utf8" "gopkg.in/yaml.v3" @@ -86,6 +87,7 @@ type rawClawBlock struct { type rawContextConfig struct { Channel *rawChannelContextConfig `yaml:"channel"` + Blocks []rawContextBlockConfig `yaml:"blocks"` } type rawChannelContextConfig struct { @@ -96,6 +98,17 @@ type rawChannelContextConfig struct { Buffer int `yaml:"buffer"` } +type rawContextBlockConfig struct { + ID string `yaml:"id"` + Kind string `yaml:"kind"` + Text string `yaml:"text"` + Enabled *bool `yaml:"enabled"` + Placement string `yaml:"placement"` + MaxCharsHyphen int `yaml:"max-chars"` + MaxCharsUnderscore int `yaml:"max_chars"` + Cadence string `yaml:"cadence"` +} + type rawMCPStdioBlock struct { Command string `yaml:"command"` Args []string `yaml:"args"` @@ -813,10 +826,14 @@ func parseContextConfig(raw *rawContextConfig) (*ContextConfig, error) { if err != nil { return nil, fmt.Errorf("channel: %w", err) } - if channel == nil { + blocks, err := parseContextBlockConfigs(raw.Blocks) + if err != nil { + return nil, fmt.Errorf("blocks: %w", err) + } + if channel == nil && blocks == nil { return &ContextConfig{}, nil } - return &ContextConfig{Channel: channel}, nil + return &ContextConfig{Channel: channel, Blocks: blocks}, nil } func parseChannelContextConfig(raw *rawChannelContextConfig) (*ChannelContextConfig, error) { @@ -863,6 +880,91 @@ func selectChannelContextMaxChars(hyphen, underscore int) (int, error) { return underscore, nil } +func parseContextBlockConfigs(raw []rawContextBlockConfig) ([]ContextBlockConfig, error) { + if raw == nil { + return nil, nil + } + out := make([]ContextBlockConfig, 0, len(raw)) + seen := make(map[string]struct{}, len(raw)) + for i, entry := range raw { + block, err := parseContextBlockConfig(entry) + if err != nil { + return nil, fmt.Errorf("entry %d: %w", i, err) + } + if _, ok := seen[block.ID]; ok { + return nil, fmt.Errorf("entry %d: duplicate id %q", i, block.ID) + } + seen[block.ID] = struct{}{} + out = append(out, block) + } + return out, nil +} + +func parseContextBlockConfig(raw rawContextBlockConfig) (ContextBlockConfig, error) { + id := strings.TrimSpace(raw.ID) + if id == "" { + return ContextBlockConfig{}, fmt.Errorf("id must not be empty") + } + kind := strings.TrimSpace(raw.Kind) + if kind == "" { + kind = "context_block" + } + text := strings.TrimSpace(raw.Text) + if text == "" { + return ContextBlockConfig{}, fmt.Errorf("text must not be empty") + } + placement := strings.TrimSpace(raw.Placement) + if placement == "" { + placement = "after_feeds" + } + if placement != "before_feeds" && placement != "after_feeds" { + return ContextBlockConfig{}, fmt.Errorf("placement must be before_feeds or after_feeds") + } + cadence := strings.TrimSpace(raw.Cadence) + if cadence == "" { + cadence = "every_turn" + } + if cadence != "every_turn" { + return ContextBlockConfig{}, fmt.Errorf("cadence must be every_turn") + } + maxChars, err := selectContextBlockMaxChars(raw.MaxCharsHyphen, raw.MaxCharsUnderscore) + if err != nil { + return ContextBlockConfig{}, err + } + if maxChars == 0 { + maxChars = 800 + } + if maxChars < 0 { + return ContextBlockConfig{}, fmt.Errorf("max-chars must be >= 0") + } + if utf8.RuneCountInString(text) > maxChars { + return ContextBlockConfig{}, fmt.Errorf("text length must be <= max-chars") + } + enabled := true + if raw.Enabled != nil { + enabled = *raw.Enabled + } + return ContextBlockConfig{ + ID: id, + Kind: kind, + Text: text, + Enabled: enabled, + Placement: placement, + MaxChars: maxChars, + Cadence: cadence, + }, nil +} + +func selectContextBlockMaxChars(hyphen, underscore int) (int, error) { + if hyphen != 0 && underscore != 0 && hyphen != underscore { + return 0, fmt.Errorf("max-chars and max_chars cannot both be set to different values") + } + if hyphen != 0 { + return hyphen, nil + } + return underscore, nil +} + func parseMCPStdio(serviceName string, raw *rawMCPStdioBlock, agent string, cllama []string, count int) (*MCPStdioBlock, error) { if raw == nil { return nil, nil diff --git a/internal/pod/parser_context_test.go b/internal/pod/parser_context_test.go index 0cfd35fe..0a10b6ad 100644 --- a/internal/pod/parser_context_test.go +++ b/internal/pod/parser_context_test.go @@ -59,6 +59,69 @@ services: } } +func TestParsePodContextBlocks(t *testing.T) { + p, err := Parse(strings.NewReader(` +x-claw: + context: + blocks: + - id: operating-focus + text: Keep the operating contract visible. +services: + inherited: + image: example/agent:latest + x-claw: + agent: ./AGENTS.md + override: + image: example/agent:latest + x-claw: + agent: ./AGENTS.md + context: + blocks: + - id: local-focus + kind: feed_frame + text: Use the local reminder. + enabled: false + cadence: every_turn + placement: before_feeds + max_chars: 80 + suppress: + image: example/agent:latest + x-claw: + agent: ./AGENTS.md + context: + blocks: [] +`)) + if err != nil { + t.Fatalf("Parse: %v", err) + } + if p.Context == nil || len(p.Context.Blocks) != 1 { + t.Fatalf("expected pod context block config, got %+v", p.Context) + } + podBlock := p.Context.Blocks[0] + if podBlock.ID != "operating-focus" || podBlock.Kind != "context_block" || podBlock.MaxChars != 800 || !podBlock.Enabled || podBlock.Cadence != "every_turn" || podBlock.Placement != "after_feeds" { + t.Fatalf("unexpected pod context block defaults: %+v", podBlock) + } + + inherited := p.Services["inherited"] + if inherited == nil || inherited.Claw == nil || inherited.Claw.Context != nil { + t.Fatalf("expected no service context override for inherited service, got %+v", inherited) + } + + override := p.Services["override"] + if override == nil || override.Claw == nil || override.Claw.Context == nil || len(override.Claw.Context.Blocks) != 1 { + t.Fatalf("expected service context block override, got %+v", override) + } + local := override.Claw.Context.Blocks[0] + if local.ID != "local-focus" || local.Kind != "feed_frame" || local.Enabled || local.MaxChars != 80 || local.Placement != "before_feeds" { + t.Fatalf("unexpected service context block: %+v", local) + } + + suppress := p.Services["suppress"] + if suppress == nil || suppress.Claw == nil || suppress.Claw.Context == nil || suppress.Claw.Context.Blocks == nil || len(suppress.Claw.Context.Blocks) != 0 { + t.Fatalf("expected explicit empty context block override, got %+v", suppress) + } +} + func TestParsePodContextChannelRejectsInvalidValues(t *testing.T) { cases := []struct { name string @@ -126,6 +189,129 @@ services: } } +func TestParsePodContextBlocksRejectInvalidValues(t *testing.T) { + cases := []struct { + name string + yaml string + want string + }{ + { + name: "missing id", + yaml: ` +x-claw: + context: + blocks: + - text: Missing id. +services: + agent: + image: example/agent:latest + x-claw: + agent: ./AGENTS.md +`, + want: "id", + }, + { + name: "duplicate id", + yaml: ` +x-claw: + context: + blocks: + - id: focus + text: One. + - id: focus + text: Two. +services: + agent: + image: example/agent:latest + x-claw: + agent: ./AGENTS.md +`, + want: "duplicate", + }, + { + name: "unsupported cadence", + yaml: ` +x-claw: + context: + blocks: + - id: focus + text: One. + cadence: min_interval +services: + agent: + image: example/agent:latest + x-claw: + agent: ./AGENTS.md +`, + want: "cadence", + }, + { + name: "unsupported placement", + yaml: ` +x-claw: + context: + blocks: + - id: focus + text: One. + placement: middle +services: + agent: + image: example/agent:latest + x-claw: + agent: ./AGENTS.md +`, + want: "placement", + }, + { + name: "oversized text", + yaml: ` +x-claw: + context: + blocks: + - id: focus + text: Too long. + max_chars: 3 +services: + agent: + image: example/agent:latest + x-claw: + agent: ./AGENTS.md +`, + want: "max-chars", + }, + { + name: "conflicting max chars aliases", + yaml: ` +x-claw: + context: + blocks: + - id: focus + text: One. + max-chars: 10 + max_chars: 20 +services: + agent: + image: example/agent:latest + x-claw: + agent: ./AGENTS.md +`, + want: "max-chars", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := Parse(strings.NewReader(tc.yaml)) + if err == nil { + t.Fatal("expected parse error") + } + if !strings.Contains(err.Error(), tc.want) { + t.Fatalf("expected error containing %q, got %v", tc.want, err) + } + }) + } +} + func TestParsePodContextChannelAwarenessIsNotPublicConfig(t *testing.T) { p, err := Parse(strings.NewReader(` x-claw: diff --git a/internal/pod/types.go b/internal/pod/types.go index a8d18845..d5b5d17d 100644 --- a/internal/pod/types.go +++ b/internal/pod/types.go @@ -132,6 +132,7 @@ type ChannelMemoryConfig struct { type ContextConfig struct { Channel *ChannelContextConfig + Blocks []ContextBlockConfig } type ChannelContextConfig struct { @@ -141,6 +142,16 @@ type ChannelContextConfig struct { Buffer int } +type ContextBlockConfig struct { + ID string + Kind string + Text string + Enabled bool + Placement string + MaxChars int + Cadence string +} + type IncludeEntry struct { ID string File string diff --git a/site/changelog.md b/site/changelog.md index 69eccad2..efed508b 100644 --- a/site/changelog.md +++ b/site/changelog.md @@ -29,7 +29,7 @@ outline: deep ## Unreleased - +- **Context block manifests** -- `x-claw.context.blocks` compiles short operator-authored context blocks into per-agent `context-blocks.json` files, with pod-level inheritance, service-level replacement, and explicit empty-list suppression. Blocks carry a `kind` tag, support `before_feeds` or `after_feeds` placement, and are exposed by claw-api and Clawdash alongside other runtime JSON so operators can verify the prompt-shaping input before relying on paired cllama runtime injection. Closes [#324](https://github.com/mostlydev/clawdapus/issues/324). ## v0.24.0 {#v0-24-0} diff --git a/site/guide/cllama.md b/site/guide/cllama.md index 8da5c08c..a0a92ece 100644 --- a/site/guide/cllama.md +++ b/site/guide/cllama.md @@ -156,6 +156,7 @@ During `claw up`, Clawdapus generates context files under the runtime directory: │ ├── feeds.json │ ├── tools.json │ ├── memory.json +│ ├── context-blocks.json │ └── channels-allowlist.json ├── crypto-crusher-1/ │ ├── AGENTS.md @@ -178,6 +179,7 @@ During `claw up`, Clawdapus generates context files under the runtime directory: | `feeds.json` | Resolved context feed subscriptions and fetch metadata. | | `tools.json` | Compiled managed tool schemas, execution metadata, auth, and mediation budgets. | | `memory.json` | Memory service recall/retain/forget endpoints and auth. | +| `context-blocks.json` | Optional operator-authored context blocks that the proxy can inject into late runtime context. | | `channels-allowlist.json` | Channel IDs the agent is authorized to read for channel context and retrieval. | ### Container-Side Mount @@ -192,7 +194,7 @@ The mount path must include the `context/` directory segment. The proxy expects ### Context Mount Contents -The reference loader reads the compiled contract (`AGENTS.md`), infrastructure map (`CLAWDAPUS.md`), identity metadata, service auth, tool manifest, memory manifest, model policy, budget policy, and channel allowlist. There is still no generic policy-decoration config or response-amendment hook in the context mount. +The reference loader reads the compiled contract (`AGENTS.md`), infrastructure map (`CLAWDAPUS.md`), identity metadata, service auth, tool manifest, memory manifest, context block manifest, model policy, budget policy, and channel allowlist. There is still no generic policy-decoration config or response-amendment hook in the context mount. ### Internal Context Snapshots @@ -201,7 +203,7 @@ For operator visibility, cllama stores the most recent provider-visible context - `GET /internal/context` - `GET /internal/context//snapshot` -Clawdash reads these through claw-api so operators can inspect the effective system contract, late runtime context, feed blocks or skip notices, memory recall, tool schemas, model route, and redacted metadata for the last turn. Snapshots are diagnostic state only; they are not a control plane and do not mutate agent context. +Clawdash reads these through claw-api so operators can inspect the effective system contract, late runtime context, context blocks, feed blocks or skip notices, memory recall, tool schemas, model route, and redacted metadata for the last turn. Snapshots are diagnostic state only; they are not a control plane and do not mutate agent context. ### Scaled Services @@ -415,7 +417,7 @@ Every request through the proxy produces a structured JSON log entry on stdout. | `static_system_hash` | sha256 of the stable system contract (`messages[0]` for OpenAI / top-level `system` for Anthropic). Should be byte-stable across turns when nothing about the agent's contract changed. | | `first_system_hash` | sha256 of the first system message in the assembled payload. v1 mirrors `static_system_hash`; reserved for future Anthropic `cache_control` differentiation. | | `first_non_system_hash` | sha256 of the first non-system message. Stable on multi-turn runners; expected to drift on single-turn Discord runners and surfaces that drift via this field. | -| `dynamic_context_hash` | sha256 of the late runtime-context block (memory + feeds + time + channel deltas). Changes per turn when new context arrives. | +| `dynamic_context_hash` | sha256 of the late runtime-context block (context blocks + memory + feeds + time + channel deltas). Changes per turn when new context arrives. | | `tools_hash` | sha256 of the canonicalized `tools[]` payload. | | `cached_tokens` | Provider-reported `usage.prompt_tokens_details.cached_tokens` when present. | | `cache_write_tokens` | Provider-reported `usage.prompt_tokens_details.cache_write_tokens` when present. | @@ -425,6 +427,7 @@ Event-specific fields may also be present depending on `type`: - `static_system_hash`, `first_system_hash`, `first_non_system_hash`, `dynamic_context_hash`, `tools_hash` — request events (prompt assembly fingerprint) - `feed_name`, `feed_url`, `fetched_at`, `cached` — feed fetch events - `feed_name`, `source`, `feed_status` (`included` / `empty` / `skipped_total_cap`), `feed_truncated`, `feed_source_bytes`, `feed_source_exact`, `feed_content_bytes`, `feed_block_bytes`, `feed_total_before`, `feed_total_after`, `feed_max_response_bytes`, `feed_max_total_bytes` — `feed_injection` events (one per manifest entry, recording whether the feed actually reached the provider-visible context after the per-feed and aggregate byte caps) +- `context_block_id`, `context_block_kind`, `context_block_status`, `context_block_cadence`, `context_block_placement`, `context_block_reason` — `context_block` events (one per manifest entry that was injected or skipped) - `provider`, `key_id`, `action`, `reason`, `cooldown_until` — provider pool events - `memory_service`, `memory_op`, `memory_status`, `memory_blocks`, `memory_bytes`, `memory_removed` — memory telemetry events diff --git a/site/guide/pod-yaml.md b/site/guide/pod-yaml.md index cb8209d0..b3b30a56 100644 --- a/site/guide/pod-yaml.md +++ b/site/guide/pod-yaml.md @@ -58,7 +58,7 @@ The top-level `x-claw` block declares shared configuration that all services inh | `memory-defaults` | Memory service subscription inherited by all services | | `skills-defaults` | Operator skill files inherited by all services | | `handles-defaults` | Shared chat topology (guild IDs, channel IDs) inherited by all services | -| `context` | Tunes auto-injected runtime context feeds (currently the `channel-context` tail served by `claw-wall`) | +| `context` | Tunes auto-injected runtime context, including channel-context tails and context blocks | | `channel-memory` | Optional durable channel-memory sidecar integration for channel retrieval | | `principals` | Explicit `claw-api` principals, verbs, scopes, and injection targets | | `alert-webhooks` / `alert-mentions` | Pod-scoped fleet alert delivery settings | @@ -119,6 +119,32 @@ services: All values must be positive, and `total-timeout-ms` must be at least `timeout-per-tool-ms`. The merged policy is compiled into each agent's `tools.json` in the cllama context directory. +### Context Blocks + +Context blocks are short operator-authored snippets that stay visible in late runtime context without bloating the primary contract. Use them for durable operating focus that should appear every turn, such as a compact policy reminder, a feed-reading frame, or current campaign objective. + +Declare blocks at pod level under `x-claw.context.blocks`, or override them per service under `services..x-claw.context.blocks`. A service-level list replaces the pod-level list; an explicit empty list suppresses inherited blocks for that service. + +```yaml +x-claw: + context: + blocks: + - id: operating-focus + kind: runtime_motivation + text: Keep the active operating contract visible; act on current evidence. + cadence: every_turn + placement: after_feeds + max-chars: 800 + +services: + quiet-bot: + x-claw: + context: + blocks: [] # suppress pod-level blocks +``` + +`id` and `text` are required. `kind` defaults to `context_block`, `enabled` defaults to `true`, `cadence` currently supports `every_turn`, `placement` supports `before_feeds` and `after_feeds` (default), and `max-chars` defaults to `800`. `claw up` validates the manifest and writes `context-blocks.json` to each subscribing agent's cllama context directory. + ### Budget And Request-Rate Caps Services with cllama can declare a pre-dispatch budget policy. `limit-usd` caps known reported spend in a sliding session-history window; `max-requests` caps successful 2xx turns in that same window. When a cap is already reached, cllama rejects the next OpenAI-compatible or Anthropic-format request with HTTP 429 and logs `budget_exceeded` or `rate_limited`. diff --git a/skills/clawdapus/SKILL.md b/skills/clawdapus/SKILL.md index aa94202c..1123e140 100644 --- a/skills/clawdapus/SKILL.md +++ b/skills/clawdapus/SKILL.md @@ -42,7 +42,8 @@ claw agent add [name] # add agent service to existing pod claw audit [--since ] [--claw ] [--type ] [--json] # summarize cllama telemetry from container logs # types: request, response, error, intervention, - # feed_fetch, provider_pool, tool_call + # feed_fetch, feed_injection, context_block, + # memory_op, provider_pool, tool_call claw api schedule # inspect/control scheduled invocations via claw-api # list | get | pause | resume | skip-next | # clear-skip-next | fire @@ -305,6 +306,12 @@ When a service subscribes to a memory service via `x-claw.memory`, cllama perfor `claw up` compiles `memory.json` into each subscribing agent's cllama context directory with endpoint URLs, auth tokens, and timeout configuration. +### Context Blocks + +Pod or service `x-claw.context.blocks` compiles short operator-authored snippets into each agent's cllama context directory as `context-blocks.json`. cllama injects enabled blocks into late runtime context before or after feeds on every turn, so durable operating focus stays visible without expanding the primary contract. + +Supported fields: `id` (required), `text` (required), `enabled` (default `true`), `cadence` (`every_turn`), `placement` (`before_feeds` or `after_feeds`, default `after_feeds`), and `max-chars` / `max_chars` (default `800`). A service-level list replaces pod-level blocks; an explicit empty list suppresses inherited blocks. + ### Managed Tool Mediation (v0.5.0) When a service subscribes to tools via `x-claw.tools`, cllama performs bounded tool execution within the inference turn: @@ -374,6 +381,7 @@ The proxy sits between agents and LLM providers. Agents get bearer tokens, proxy CLAWDAPUS.md # infrastructure map tools.json # managed tool manifest (when tools subscribed) memory.json # memory service config (when memory subscribed) + context-blocks.json # context block manifest (when configured) ``` ### Provider support @@ -425,6 +433,7 @@ When the aggregate cap drops a feed the model sees an explicit `--- FEED: | `jobs.json` | Cron schedule for INVOKE tasks | Runner state directory | | `tools.json` | Managed tool manifest per agent | cllama context directory | | `memory.json` | Memory service config per agent | cllama context directory | +| `context-blocks.json` | Runtime context block manifest per agent | cllama context directory | ## Drivers @@ -493,7 +502,7 @@ When a pod declares a `clawdash` surface, `claw up` publishes the operational da - **Fleet / Topology** — running services, wiring, driver types. - **Agents** — per-agent *contract* as compiled at `claw up` time (AGENTS.md, CLAWDAPUS.md, feed subscriptions, managed tools, memory wiring, metadata). -- **Agents → Live Context** — the system message, tools array, injected feeds, memory recall, time context, and interventions that were assembled for the most recent inference turn. Sourced from the cllama snapshot store (`/internal/context//snapshot`, proxied through `claw-api`). Credentials and token fields are redacted. +- **Agents → Live Context** — the system message, tools array, context blocks, injected feeds, memory recall, time context, and interventions that were assembled for the most recent inference turn. Sourced from the cllama snapshot store (`/internal/context//snapshot`, proxied through `claw-api`). Credentials and token fields are redacted. - **Schedule** — `INVOKE` and `x-claw.invoke` cron entries, with `claw api schedule ...` controls. All views are read-only and scoped through `claw-api` principals. Use this before log-diving — "what did the model actually see last turn" has a direct answer here. @@ -521,6 +530,11 @@ All views are read-only and scoped through `claw-api` principals. Use this befor - `claw memory forget --entry-id ` writes tombstones; subsequent backfills skip those entries - Declaring `memory:` without `cllama:` is a hard error +### Context blocks not visible +- Check `context-blocks.json` in `.claw-runtime/context//` +- Check `claw audit --type context_block` for injected or skipped context block entries +- Verify `cadence` is `every_turn`, `placement` is `before_feeds` or `after_feeds`, and the block text is within `max-chars` + ## Working Examples | Example | Path | What it demonstrates |