From cbbe5c8c8caecf23b718935d17b1f72677fd1e55 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Fri, 26 Jun 2026 15:02:31 -0400 Subject: [PATCH 1/2] Add runtime reminder manifests --- README.md | 3 +- cmd/claw-api/agent_context.go | 39 +++-- cmd/claw-api/agent_context_test.go | 8 + cmd/claw/compose_up.go | 30 ++++ cmd/claw/runtime_reminders_test.go | 50 ++++++ cmd/claw/skill_data/SKILL.md | 18 +- cmd/clawdash/agent_context.go | 204 ++++++++++++++--------- cmd/clawdash/handler_test.go | 15 +- cmd/clawdash/templates/agent_detail.html | 21 ++- docs/CLLAMA_SPEC.md | 8 +- internal/cllama/context.go | 30 +++- internal/cllama/context_test.go | 20 +++ internal/pod/parser.go | 102 +++++++++++- internal/pod/parser_context_test.go | 185 ++++++++++++++++++++ internal/pod/types.go | 12 +- site/changelog.md | 2 +- site/guide/cllama.md | 9 +- site/guide/pod-yaml.md | 27 ++- skills/clawdapus/SKILL.md | 18 +- 19 files changed, 685 insertions(+), 116 deletions(-) create mode 100644 cmd/claw/runtime_reminders_test.go diff --git a/README.md b/README.md index e92b7c89..494b9567 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. +- **Runtime reminders:** Operators can compile short per-agent reminder snippets into `runtime-reminders.json`; cllama injects enabled reminders 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 runtime reminders, 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..b9d3fa0c 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"` + RuntimeReminders any `json:"runtime_reminders"` + 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 } + runtimeReminders, err := readJSONArtifact(filepath.Join(agentDir, "runtime-reminders.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), + RuntimeReminders: redactJSONValue(runtimeReminders), + ServiceAuth: redactServiceAuthArtifacts(serviceAuth), }) } diff --git a/cmd/claw-api/agent_context_test.go b/cmd/claw-api/agent_context_test.go index 0353fb0a..9506d842 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, "runtime-reminders.json"), []byte(`{"version":1,"reminders":[{"id":"focus","text":"Stay on the active operating contract.","enabled":true,"placement":"before_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) } + runtimeReminders := resp["runtime_reminders"].(map[string]any) + reminders := runtimeReminders["reminders"].([]any) + if reminders[0].(map[string]any)["id"] != "focus" { + t.Fatalf("runtime reminders were not returned: %+v", runtimeReminders) + } } func TestAgentContractPrefersEffectiveAgentsMD(t *testing.T) { diff --git a/cmd/claw/compose_up.go b/cmd/claw/compose_up.go index 7fd9313f..42baca68 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, + RuntimeReminders: agentRuntimeReminders(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, + RuntimeReminders: agentRuntimeReminders(p, name), ServiceAuth: svcAuth, ChannelAllowlist: conversationWallAllowlists[name], Metadata: injectAgentBudget(cllama.InjectCompiledModelPolicy(map[string]any{ @@ -1628,6 +1630,34 @@ func agentBudgetPolicy(p *pod.Pod, serviceName string) *cllama.BudgetPolicy { } } +func agentRuntimeReminders(p *pod.Pod, serviceName string) []cllama.RuntimeReminderManifestEntry { + if p == nil { + return nil + } + var reminders []pod.RuntimeReminderConfig + if p.Context != nil && p.Context.RuntimeReminders != nil { + reminders = p.Context.RuntimeReminders + } + if svc := p.Services[serviceName]; svc != nil && svc.Claw != nil && svc.Claw.Context != nil && svc.Claw.Context.RuntimeReminders != nil { + reminders = svc.Claw.Context.RuntimeReminders + } + if len(reminders) == 0 { + return nil + } + out := make([]cllama.RuntimeReminderManifestEntry, 0, len(reminders)) + for _, reminder := range reminders { + out = append(out, cllama.RuntimeReminderManifestEntry{ + ID: reminder.ID, + Text: reminder.Text, + Enabled: reminder.Enabled, + Placement: reminder.Placement, + MaxChars: reminder.MaxChars, + Cadence: reminder.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/runtime_reminders_test.go b/cmd/claw/runtime_reminders_test.go new file mode 100644 index 00000000..4c827112 --- /dev/null +++ b/cmd/claw/runtime_reminders_test.go @@ -0,0 +1,50 @@ +package main + +import ( + "testing" + + "github.com/mostlydev/clawdapus/internal/pod" +) + +func TestAgentRuntimeRemindersResolvesPodAndServiceConfig(t *testing.T) { + p := &pod.Pod{ + Context: &pod.ContextConfig{ + RuntimeReminders: []pod.RuntimeReminderConfig{{ + ID: "pod-focus", + Text: "Pod reminder.", + Enabled: true, + Placement: "before_feeds", + MaxChars: 800, + Cadence: "every_turn", + }}, + }, + Services: map[string]*pod.Service{ + "inherited": {Claw: &pod.ClawBlock{}}, + "override": {Claw: &pod.ClawBlock{Context: &pod.ContextConfig{ + RuntimeReminders: []pod.RuntimeReminderConfig{{ + ID: "local-focus", + Text: "Local reminder.", + Enabled: false, + Placement: "before_feeds", + MaxChars: 400, + Cadence: "every_turn", + }}, + }}}, + "suppress": {Claw: &pod.ClawBlock{Context: &pod.ContextConfig{ + RuntimeReminders: []pod.RuntimeReminderConfig{}, + }}}, + }, + } + + inherited := agentRuntimeReminders(p, "inherited") + if len(inherited) != 1 || inherited[0].ID != "pod-focus" || !inherited[0].Enabled { + t.Fatalf("expected inherited pod reminder, got %+v", inherited) + } + override := agentRuntimeReminders(p, "override") + if len(override) != 1 || override[0].ID != "local-focus" || override[0].Enabled || override[0].MaxChars != 400 { + t.Fatalf("expected service reminder override, got %+v", override) + } + if suppressed := agentRuntimeReminders(p, "suppress"); suppressed != nil { + t.Fatalf("expected empty service override to suppress pod reminders, got %+v", suppressed) + } +} diff --git a/cmd/claw/skill_data/SKILL.md b/cmd/claw/skill_data/SKILL.md index aa94202c..8028bde4 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, runtime_reminder, + # 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. +### Runtime Reminders + +Pod or service `x-claw.context.runtime-reminders` compiles short operator-authored snippets into each agent's cllama context directory as `runtime-reminders.json`. cllama injects enabled reminders into late runtime context before 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`), and `max-chars` / `max_chars` (default `800`). A service-level list replaces pod-level reminders; an explicit empty list suppresses inherited reminders. + ### 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) + runtime-reminders.json # runtime reminder 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 | +| `runtime-reminders.json` | Runtime reminder 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, runtime reminders, 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 +### Runtime reminders not visible +- Check `runtime-reminders.json` in `.claw-runtime/context//` +- Check `claw audit --type runtime_reminder` for injected or skipped reminder entries +- Verify `cadence` is `every_turn`, `placement` is `before_feeds`, and the reminder 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..834e559c 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"` + RuntimeReminders any `json:"runtime_reminders"` + 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 + RuntimeReminderJSON string + ServiceAuthJSON string + HasMetadata bool + HasFeeds bool + HasTools bool + HasMemory bool + HasRuntimeReminder bool + HasServiceAuth bool + MetadataRows []keyValueRow + FeedRows []feedManifestRow + ToolRows []toolManifestRow + MemoryRows []keyValueRow + RuntimeReminderRows []runtimeReminderRow + ServiceAuthRows []serviceAuthRow + HasMetadataRows bool + HasFeedRows bool + HasToolRows bool + HasMemoryRows bool + HasRuntimeReminderRows bool + HasServiceAuthRows bool } type keyValueRow struct { @@ -249,6 +254,15 @@ type serviceAuthRow struct { DetailJSON string } +type runtimeReminderRow struct { + ID string + Enabled string + Cadence string + Placement string + MaxChars string + Text string +} + type candidateContextRow struct { Provider string UpstreamModel string @@ -430,49 +444,54 @@ func buildAgentContextDetailPageData(podName, agentID, tab string, contract agen feedRows := feedManifestRows(contract.Feeds) toolRows := toolManifestRows(contract.Tools) memoryRows := topLevelRows(contract.Memory) + runtimeReminderRows := runtimeReminderRows(contract.RuntimeReminders) 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), + RuntimeReminderJSON: prettyJSON(contract.RuntimeReminders), + ServiceAuthJSON: prettyJSON(contract.ServiceAuth), + LiveContextJSON: liveView.RawJSON, + MetadataRows: metadataRows, + FeedRows: feedRows, + ToolRows: toolRows, + MemoryRows: memoryRows, + RuntimeReminderRows: runtimeReminderRows, + ServiceAuthRows: serviceAuthRows, } data.HasMetadata = data.MetadataJSON != "" data.HasFeeds = data.FeedsJSON != "" data.HasTools = data.ToolsJSON != "" data.HasMemory = data.MemoryJSON != "" + data.HasRuntimeReminder = data.RuntimeReminderJSON != "" 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.HasRuntimeReminderRows = len(runtimeReminderRows) > 0 data.HasServiceAuthRows = len(serviceAuthRows) > 0 return data } @@ -561,6 +580,33 @@ func scalarInt(v any) int { } } +func runtimeReminderRows(reminders any) []runtimeReminderRow { + root, ok := reminders.(map[string]any) + if !ok { + return nil + } + rawList, ok := root["reminders"].([]any) + if !ok { + return nil + } + rows := make([]runtimeReminderRow, 0, len(rawList)) + for _, item := range rawList { + entry, ok := item.(map[string]any) + if !ok { + continue + } + rows = append(rows, runtimeReminderRow{ + ID: scalarString(entry["id"]), + Enabled: scalarString(entry["enabled"]), + Cadence: scalarString(entry["cadence"]), + Placement: scalarString(entry["placement"]), + MaxChars: scalarString(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..77740ca6 100644 --- a/cmd/clawdash/handler_test.go +++ b/cmd/clawdash/handler_test.go @@ -403,6 +403,19 @@ func TestAgentContextDetailRendersContractAndLiveSnapshot(t *testing.T) { "type": "openclaw", }, Feeds: map[string]any{"feeds": []any{"alerts"}}, + RuntimeReminders: map[string]any{ + "version": float64(1), + "reminders": []any{ + map[string]any{ + "id": "focus", + "text": "Stay on the active operating contract.", + "enabled": true, + "placement": "before_feeds", + "cadence": "every_turn", + "max_chars": float64(800), + }, + }, + }, }, liveContext: map[string]any{ "agent_id": "bot-0", @@ -458,7 +471,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", "Runtime reminders", "runtime-reminders.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..7b72c3dc 100644 --- a/cmd/clawdash/templates/agent_detail.html +++ b/cmd/clawdash/templates/agent_detail.html @@ -259,6 +259,21 @@

Runtime inputs

{{end}} + {{if .HasRuntimeReminderRows}} +
+
Runtime reminders
+
+ + + + {{range .RuntimeReminderRows}} + + {{end}} + +
IDEnabledCadencePlacementMax charsText
{{.ID}}{{.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 .HasRuntimeReminder) (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 .HasRuntimeReminder .HasServiceAuth}}
Raw runtime JSON {{if .HasMetadata}}
metadata.json
@@ -288,6 +303,8 @@ 

Runtime inputs

{{.ToolsJSON}}
{{end}} {{if .HasMemory}}
memory.json
 {{.MemoryJSON}}
{{end}} + {{if .HasRuntimeReminder}}
runtime-reminders.json
+{{.RuntimeReminderJSON}}
{{end}} {{if .HasServiceAuth}}
service-auth
 {{.ServiceAuthJSON}}
{{end}}
diff --git a/docs/CLLAMA_SPEC.md b/docs/CLLAMA_SPEC.md index 3b285acc..d7ae2e58 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 +│ └── runtime-reminders.json # Optional operator-authored reminder snippets ├── 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 runtime reminders, 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`, `runtime_reminder`, `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 +- `runtime_reminder_id`, `runtime_reminder_status`, `runtime_reminder_cadence`, `runtime_reminder_placement`, and `runtime_reminder_reason` for reminder injection 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..9e19455c 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 + RuntimeReminders []RuntimeReminderManifestEntry ServiceAuth []ServiceAuthEntry ChannelAllowlist []string } @@ -100,6 +101,20 @@ type MemoryOp struct { TimeoutMS int `json:"timeout_ms,omitempty"` } +type RuntimeReminderManifest struct { + Version int `json:"version"` + Reminders []RuntimeReminderManifestEntry `json:"reminders"` +} + +type RuntimeReminderManifestEntry struct { + ID string `json:"id"` + 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 +139,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,runtime-reminders.json,service-auth/...} func GenerateContextDir(runtimeDir string, agents []AgentContextInput) error { for _, agent := range agents { if agent.AgentID == "" { @@ -196,6 +211,19 @@ func GenerateContextDir(runtimeDir string, agents []AgentContextInput) error { } } + if len(agent.RuntimeReminders) > 0 { + remindersJSON, err := json.MarshalIndent(RuntimeReminderManifest{ + Version: 1, + Reminders: append([]RuntimeReminderManifestEntry(nil), agent.RuntimeReminders...), + }, "", " ") + if err != nil { + return fmt.Errorf("marshal runtime reminders for %q: %w", agent.AgentID, err) + } + if err := os.WriteFile(filepath.Join(agentDir, "runtime-reminders.json"), append(remindersJSON, '\n'), 0644); err != nil { + return fmt.Errorf("write runtime-reminders.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..2e628d78 100644 --- a/internal/cllama/context_test.go +++ b/internal/cllama/context_test.go @@ -129,6 +129,14 @@ func TestGenerateContextDirWritesOptionalFeedsAndServiceAuth(t *testing.T) { }, Auth: &AuthEntry{Type: "bearer", Token: "memory-token"}, }, + RuntimeReminders: []RuntimeReminderManifestEntry{{ + ID: "focus", + Text: "Keep the operating contract visible.", + Enabled: true, + Placement: "before_feeds", + MaxChars: 800, + Cadence: "every_turn", + }}, ServiceAuth: []ServiceAuthEntry{{ Service: "claw-api", AuthType: "bearer", @@ -186,6 +194,18 @@ func TestGenerateContextDirWritesOptionalFeedsAndServiceAuth(t *testing.T) { t.Fatalf("unexpected memory manifest payload: %v", memory) } + remindersRaw, err := os.ReadFile(filepath.Join(dir, "context", "octopus", "runtime-reminders.json")) + if err != nil { + t.Fatal(err) + } + var reminders RuntimeReminderManifest + if err := json.Unmarshal(remindersRaw, &reminders); err != nil { + t.Fatal(err) + } + if reminders.Version != 1 || len(reminders.Reminders) != 1 || reminders.Reminders[0].ID != "focus" || !reminders.Reminders[0].Enabled { + t.Fatalf("unexpected runtime reminders manifest: %+v", reminders) + } + 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..248acdab 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" @@ -85,7 +86,8 @@ type rawClawBlock struct { } type rawContextConfig struct { - Channel *rawChannelContextConfig `yaml:"channel"` + Channel *rawChannelContextConfig `yaml:"channel"` + RuntimeReminders []rawRuntimeReminderConfig `yaml:"runtime-reminders"` } type rawChannelContextConfig struct { @@ -96,6 +98,16 @@ type rawChannelContextConfig struct { Buffer int `yaml:"buffer"` } +type rawRuntimeReminderConfig struct { + ID string `yaml:"id"` + 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 +825,14 @@ func parseContextConfig(raw *rawContextConfig) (*ContextConfig, error) { if err != nil { return nil, fmt.Errorf("channel: %w", err) } - if channel == nil { + runtimeReminders, err := parseRuntimeReminderConfigs(raw.RuntimeReminders) + if err != nil { + return nil, fmt.Errorf("runtime-reminders: %w", err) + } + if channel == nil && runtimeReminders == nil { return &ContextConfig{}, nil } - return &ContextConfig{Channel: channel}, nil + return &ContextConfig{Channel: channel, RuntimeReminders: runtimeReminders}, nil } func parseChannelContextConfig(raw *rawChannelContextConfig) (*ChannelContextConfig, error) { @@ -863,6 +879,86 @@ func selectChannelContextMaxChars(hyphen, underscore int) (int, error) { return underscore, nil } +func parseRuntimeReminderConfigs(raw []rawRuntimeReminderConfig) ([]RuntimeReminderConfig, error) { + if raw == nil { + return nil, nil + } + out := make([]RuntimeReminderConfig, 0, len(raw)) + seen := make(map[string]struct{}, len(raw)) + for i, entry := range raw { + reminder, err := parseRuntimeReminderConfig(entry) + if err != nil { + return nil, fmt.Errorf("entry %d: %w", i, err) + } + if _, ok := seen[reminder.ID]; ok { + return nil, fmt.Errorf("entry %d: duplicate id %q", i, reminder.ID) + } + seen[reminder.ID] = struct{}{} + out = append(out, reminder) + } + return out, nil +} + +func parseRuntimeReminderConfig(raw rawRuntimeReminderConfig) (RuntimeReminderConfig, error) { + id := strings.TrimSpace(raw.ID) + if id == "" { + return RuntimeReminderConfig{}, fmt.Errorf("id must not be empty") + } + text := strings.TrimSpace(raw.Text) + if text == "" { + return RuntimeReminderConfig{}, fmt.Errorf("text must not be empty") + } + placement := strings.TrimSpace(raw.Placement) + if placement == "" { + placement = "before_feeds" + } + if placement != "before_feeds" { + return RuntimeReminderConfig{}, fmt.Errorf("placement must be before_feeds") + } + cadence := strings.TrimSpace(raw.Cadence) + if cadence == "" { + cadence = "every_turn" + } + if cadence != "every_turn" { + return RuntimeReminderConfig{}, fmt.Errorf("cadence must be every_turn") + } + maxChars, err := selectRuntimeReminderMaxChars(raw.MaxCharsHyphen, raw.MaxCharsUnderscore) + if err != nil { + return RuntimeReminderConfig{}, err + } + if maxChars == 0 { + maxChars = 800 + } + if maxChars < 0 { + return RuntimeReminderConfig{}, fmt.Errorf("max-chars must be >= 0") + } + if utf8.RuneCountInString(text) > maxChars { + return RuntimeReminderConfig{}, fmt.Errorf("text length must be <= max-chars") + } + enabled := true + if raw.Enabled != nil { + enabled = *raw.Enabled + } + return RuntimeReminderConfig{ + ID: id, + Text: text, + Enabled: enabled, + Placement: placement, + MaxChars: maxChars, + Cadence: cadence, + }, nil +} + +func selectRuntimeReminderMaxChars(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..d360aacc 100644 --- a/internal/pod/parser_context_test.go +++ b/internal/pod/parser_context_test.go @@ -59,6 +59,68 @@ services: } } +func TestParsePodContextRuntimeReminders(t *testing.T) { + p, err := Parse(strings.NewReader(` +x-claw: + context: + runtime-reminders: + - 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: + runtime-reminders: + - id: local-focus + 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: + runtime-reminders: [] +`)) + if err != nil { + t.Fatalf("Parse: %v", err) + } + if p.Context == nil || len(p.Context.RuntimeReminders) != 1 { + t.Fatalf("expected pod runtime reminder config, got %+v", p.Context) + } + podReminder := p.Context.RuntimeReminders[0] + if podReminder.ID != "operating-focus" || podReminder.MaxChars != 800 || !podReminder.Enabled || podReminder.Cadence != "every_turn" || podReminder.Placement != "before_feeds" { + t.Fatalf("unexpected pod reminder defaults: %+v", podReminder) + } + + 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.RuntimeReminders) != 1 { + t.Fatalf("expected service runtime reminder override, got %+v", override) + } + local := override.Claw.Context.RuntimeReminders[0] + if local.ID != "local-focus" || local.Enabled || local.MaxChars != 80 { + t.Fatalf("unexpected service reminder: %+v", local) + } + + suppress := p.Services["suppress"] + if suppress == nil || suppress.Claw == nil || suppress.Claw.Context == nil || suppress.Claw.Context.RuntimeReminders == nil || len(suppress.Claw.Context.RuntimeReminders) != 0 { + t.Fatalf("expected explicit empty runtime reminder override, got %+v", suppress) + } +} + func TestParsePodContextChannelRejectsInvalidValues(t *testing.T) { cases := []struct { name string @@ -126,6 +188,129 @@ services: } } +func TestParsePodContextRuntimeRemindersRejectInvalidValues(t *testing.T) { + cases := []struct { + name string + yaml string + want string + }{ + { + name: "missing id", + yaml: ` +x-claw: + context: + runtime-reminders: + - text: Missing id. +services: + agent: + image: example/agent:latest + x-claw: + agent: ./AGENTS.md +`, + want: "id", + }, + { + name: "duplicate id", + yaml: ` +x-claw: + context: + runtime-reminders: + - 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: + runtime-reminders: + - 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: + runtime-reminders: + - id: focus + text: One. + placement: after_feeds +services: + agent: + image: example/agent:latest + x-claw: + agent: ./AGENTS.md +`, + want: "placement", + }, + { + name: "oversized text", + yaml: ` +x-claw: + context: + runtime-reminders: + - 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: + runtime-reminders: + - 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..9d630d2e 100644 --- a/internal/pod/types.go +++ b/internal/pod/types.go @@ -131,7 +131,8 @@ type ChannelMemoryConfig struct { } type ContextConfig struct { - Channel *ChannelContextConfig + Channel *ChannelContextConfig + RuntimeReminders []RuntimeReminderConfig } type ChannelContextConfig struct { @@ -141,6 +142,15 @@ type ChannelContextConfig struct { Buffer int } +type RuntimeReminderConfig struct { + ID 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..ed567064 100644 --- a/site/changelog.md +++ b/site/changelog.md @@ -29,7 +29,7 @@ outline: deep ## Unreleased - +- **Runtime reminder manifests** -- `x-claw.context.runtime-reminders` compiles short operator-authored reminders into per-agent `runtime-reminders.json` files, with pod-level inheritance, service-level replacement, and explicit empty-list suppression. claw-api and Clawdash expose the generated manifest 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..19eb14a9 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 +│ ├── runtime-reminders.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. | +| `runtime-reminders.json` | Optional operator-authored reminder snippets 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, runtime reminder 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, runtime reminders, 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 (runtime reminders + 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) +- `runtime_reminder_id`, `runtime_reminder_status`, `runtime_reminder_cadence`, `runtime_reminder_placement`, `runtime_reminder_reason` — `runtime_reminder` 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..58d5ff6c 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 runtime reminders | | `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,31 @@ 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. +### Runtime Reminders + +Runtime reminders 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 or current campaign objective. + +Declare reminders at pod level under `x-claw.context.runtime-reminders`, or override them per service under `services..x-claw.context.runtime-reminders`. A service-level list replaces the pod-level list; an explicit empty list suppresses inherited reminders for that service. + +```yaml +x-claw: + context: + runtime-reminders: + - id: operating-focus + text: Keep the active operating contract visible; act on current evidence. + cadence: every_turn + placement: before_feeds + max-chars: 800 + +services: + quiet-bot: + x-claw: + context: + runtime-reminders: [] # suppress pod-level reminders +``` + +`id` and `text` are required. `enabled` defaults to `true`, `cadence` currently supports `every_turn`, `placement` currently supports `before_feeds`, and `max-chars` defaults to `800`. `claw up` validates the manifest and writes `runtime-reminders.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..8028bde4 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, runtime_reminder, + # 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. +### Runtime Reminders + +Pod or service `x-claw.context.runtime-reminders` compiles short operator-authored snippets into each agent's cllama context directory as `runtime-reminders.json`. cllama injects enabled reminders into late runtime context before 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`), and `max-chars` / `max_chars` (default `800`). A service-level list replaces pod-level reminders; an explicit empty list suppresses inherited reminders. + ### 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) + runtime-reminders.json # runtime reminder 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 | +| `runtime-reminders.json` | Runtime reminder 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, runtime reminders, 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 +### Runtime reminders not visible +- Check `runtime-reminders.json` in `.claw-runtime/context//` +- Check `claw audit --type runtime_reminder` for injected or skipped reminder entries +- Verify `cadence` is `every_turn`, `placement` is `before_feeds`, and the reminder text is within `max-chars` + ## Working Examples | Example | Path | What it demonstrates | From 0ccbef30e806e938282767493201bfc985ec56cf Mon Sep 17 00:00:00 2001 From: Wojtek Date: Fri, 26 Jun 2026 19:02:13 -0400 Subject: [PATCH 2/2] Generalize runtime reminders as context blocks --- README.md | 4 +- cmd/claw-api/agent_context.go | 38 ++--- cmd/claw-api/agent_context_test.go | 10 +- cmd/claw/compose_up.go | 37 ++--- cmd/claw/context_blocks_test.go | 52 ++++++ cmd/claw/runtime_reminders_test.go | 50 ------ cmd/claw/skill_data/SKILL.md | 22 +-- cmd/clawdash/agent_context.go | 196 ++++++++++++----------- cmd/clawdash/handler_test.go | 9 +- cmd/clawdash/templates/agent_detail.html | 18 +-- docs/CLLAMA_SPEC.md | 8 +- internal/cllama/context.go | 27 ++-- internal/cllama/context_test.go | 15 +- internal/pod/parser.go | 60 +++---- internal/pod/parser_context_test.go | 49 +++--- internal/pod/types.go | 7 +- site/changelog.md | 2 +- site/guide/cllama.md | 12 +- site/guide/pod-yaml.md | 17 +- skills/clawdapus/SKILL.md | 22 +-- 20 files changed, 336 insertions(+), 319 deletions(-) create mode 100644 cmd/claw/context_blocks_test.go delete mode 100644 cmd/claw/runtime_reminders_test.go diff --git a/README.md b/README.md index 494b9567..d781c46f 100644 --- a/README.md +++ b/README.md @@ -378,7 +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. -- **Runtime reminders:** Operators can compile short per-agent reminder snippets into `runtime-reminders.json`; cllama injects enabled reminders into late runtime context so durable operating focus stays visible without expanding the primary contract. +- **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. @@ -555,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, injects compiled runtime reminders, 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 b9d3fa0c..032845f6 100644 --- a/cmd/claw-api/agent_context.go +++ b/cmd/claw-api/agent_context.go @@ -26,15 +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"` - RuntimeReminders any `json:"runtime_reminders"` - 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 { @@ -135,7 +135,7 @@ func (h *apiHandler) handleAgentContract(w http.ResponseWriter, agentID string) writeJSONError(w, http.StatusInternalServerError, err.Error()) return } - runtimeReminders, err := readJSONArtifact(filepath.Join(agentDir, "runtime-reminders.json"), true) + contextBlocks, err := readJSONArtifact(filepath.Join(agentDir, "context-blocks.json"), true) if err != nil { writeJSONError(w, http.StatusInternalServerError, err.Error()) return @@ -147,15 +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), - RuntimeReminders: redactJSONValue(runtimeReminders), - 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 9506d842..ea5a7c3f 100644 --- a/cmd/claw-api/agent_context_test.go +++ b/cmd/claw-api/agent_context_test.go @@ -95,7 +95,7 @@ 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, "runtime-reminders.json"), []byte(`{"version":1,"reminders":[{"id":"focus","text":"Stay on the active operating contract.","enabled":true,"placement":"before_feeds","cadence":"every_turn","max_chars":800}]}`), 0o644); err != nil { + 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") @@ -150,10 +150,10 @@ func TestAgentContractRedactsContextCredentials(t *testing.T) { if clawAPIAuth["token"] != "[REDACTED]" { t.Fatalf("service-auth token was not redacted: %+v", clawAPIAuth) } - runtimeReminders := resp["runtime_reminders"].(map[string]any) - reminders := runtimeReminders["reminders"].([]any) - if reminders[0].(map[string]any)["id"] != "focus" { - t.Fatalf("runtime reminders were not returned: %+v", runtimeReminders) + 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) } } diff --git a/cmd/claw/compose_up.go b/cmd/claw/compose_up.go index 42baca68..37d547e6 100644 --- a/cmd/claw/compose_up.go +++ b/cmd/claw/compose_up.go @@ -600,7 +600,7 @@ func runComposeUp(podFile string) (err error) { Tools: tools, ToolPolicy: agentToolPolicy(p, name), Memory: memory, - RuntimeReminders: agentRuntimeReminders(p, name), + ContextBlocks: agentContextBlocks(p, name), ServiceAuth: ordinalAuth, ChannelAllowlist: conversationWallAllowlists[ordinalName], Metadata: injectAgentBudget(cllama.InjectCompiledModelPolicy(map[string]any{ @@ -648,7 +648,7 @@ func runComposeUp(podFile string) (err error) { Tools: tools, ToolPolicy: agentToolPolicy(p, name), Memory: memory, - RuntimeReminders: agentRuntimeReminders(p, name), + ContextBlocks: agentContextBlocks(p, name), ServiceAuth: svcAuth, ChannelAllowlist: conversationWallAllowlists[name], Metadata: injectAgentBudget(cllama.InjectCompiledModelPolicy(map[string]any{ @@ -1630,29 +1630,30 @@ func agentBudgetPolicy(p *pod.Pod, serviceName string) *cllama.BudgetPolicy { } } -func agentRuntimeReminders(p *pod.Pod, serviceName string) []cllama.RuntimeReminderManifestEntry { +func agentContextBlocks(p *pod.Pod, serviceName string) []cllama.ContextBlockManifestEntry { if p == nil { return nil } - var reminders []pod.RuntimeReminderConfig - if p.Context != nil && p.Context.RuntimeReminders != nil { - reminders = p.Context.RuntimeReminders + 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.RuntimeReminders != nil { - reminders = svc.Claw.Context.RuntimeReminders + 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(reminders) == 0 { + if len(blocks) == 0 { return nil } - out := make([]cllama.RuntimeReminderManifestEntry, 0, len(reminders)) - for _, reminder := range reminders { - out = append(out, cllama.RuntimeReminderManifestEntry{ - ID: reminder.ID, - Text: reminder.Text, - Enabled: reminder.Enabled, - Placement: reminder.Placement, - MaxChars: reminder.MaxChars, - Cadence: reminder.Cadence, + 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 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/runtime_reminders_test.go b/cmd/claw/runtime_reminders_test.go deleted file mode 100644 index 4c827112..00000000 --- a/cmd/claw/runtime_reminders_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package main - -import ( - "testing" - - "github.com/mostlydev/clawdapus/internal/pod" -) - -func TestAgentRuntimeRemindersResolvesPodAndServiceConfig(t *testing.T) { - p := &pod.Pod{ - Context: &pod.ContextConfig{ - RuntimeReminders: []pod.RuntimeReminderConfig{{ - ID: "pod-focus", - Text: "Pod reminder.", - Enabled: true, - Placement: "before_feeds", - MaxChars: 800, - Cadence: "every_turn", - }}, - }, - Services: map[string]*pod.Service{ - "inherited": {Claw: &pod.ClawBlock{}}, - "override": {Claw: &pod.ClawBlock{Context: &pod.ContextConfig{ - RuntimeReminders: []pod.RuntimeReminderConfig{{ - ID: "local-focus", - Text: "Local reminder.", - Enabled: false, - Placement: "before_feeds", - MaxChars: 400, - Cadence: "every_turn", - }}, - }}}, - "suppress": {Claw: &pod.ClawBlock{Context: &pod.ContextConfig{ - RuntimeReminders: []pod.RuntimeReminderConfig{}, - }}}, - }, - } - - inherited := agentRuntimeReminders(p, "inherited") - if len(inherited) != 1 || inherited[0].ID != "pod-focus" || !inherited[0].Enabled { - t.Fatalf("expected inherited pod reminder, got %+v", inherited) - } - override := agentRuntimeReminders(p, "override") - if len(override) != 1 || override[0].ID != "local-focus" || override[0].Enabled || override[0].MaxChars != 400 { - t.Fatalf("expected service reminder override, got %+v", override) - } - if suppressed := agentRuntimeReminders(p, "suppress"); suppressed != nil { - t.Fatalf("expected empty service override to suppress pod reminders, got %+v", suppressed) - } -} diff --git a/cmd/claw/skill_data/SKILL.md b/cmd/claw/skill_data/SKILL.md index 8028bde4..1123e140 100644 --- a/cmd/claw/skill_data/SKILL.md +++ b/cmd/claw/skill_data/SKILL.md @@ -42,7 +42,7 @@ 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, feed_injection, runtime_reminder, + # 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 | @@ -306,11 +306,11 @@ 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. -### Runtime Reminders +### Context Blocks -Pod or service `x-claw.context.runtime-reminders` compiles short operator-authored snippets into each agent's cllama context directory as `runtime-reminders.json`. cllama injects enabled reminders into late runtime context before feeds on every turn, so durable operating focus stays visible without expanding the primary contract. +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`), and `max-chars` / `max_chars` (default `800`). A service-level list replaces pod-level reminders; an explicit empty list suppresses inherited reminders. +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) @@ -381,7 +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) - runtime-reminders.json # runtime reminder manifest (when configured) + context-blocks.json # context block manifest (when configured) ``` ### Provider support @@ -433,7 +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 | -| `runtime-reminders.json` | Runtime reminder manifest per agent | cllama context directory | +| `context-blocks.json` | Runtime context block manifest per agent | cllama context directory | ## Drivers @@ -502,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, runtime reminders, 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. @@ -530,10 +530,10 @@ 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 -### Runtime reminders not visible -- Check `runtime-reminders.json` in `.claw-runtime/context//` -- Check `claw audit --type runtime_reminder` for injected or skipped reminder entries -- Verify `cadence` is `every_turn`, `placement` is `before_feeds`, and the reminder text is within `max-chars` +### 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 diff --git a/cmd/clawdash/agent_context.go b/cmd/clawdash/agent_context.go index 834e559c..53ecab6e 100644 --- a/cmd/clawdash/agent_context.go +++ b/cmd/clawdash/agent_context.go @@ -34,15 +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"` - RuntimeReminders any `json:"runtime_reminders"` - 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 { @@ -175,51 +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 - RuntimeReminderJSON string - ServiceAuthJSON string - HasMetadata bool - HasFeeds bool - HasTools bool - HasMemory bool - HasRuntimeReminder bool - HasServiceAuth bool - MetadataRows []keyValueRow - FeedRows []feedManifestRow - ToolRows []toolManifestRow - MemoryRows []keyValueRow - RuntimeReminderRows []runtimeReminderRow - ServiceAuthRows []serviceAuthRow - HasMetadataRows bool - HasFeedRows bool - HasToolRows bool - HasMemoryRows bool - HasRuntimeReminderRows 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 { @@ -254,8 +254,9 @@ type serviceAuthRow struct { DetailJSON string } -type runtimeReminderRow struct { +type contextBlockRow struct { ID string + Kind string Enabled string Cadence string Placement string @@ -444,54 +445,54 @@ func buildAgentContextDetailPageData(podName, agentID, tab string, contract agen feedRows := feedManifestRows(contract.Feeds) toolRows := toolManifestRows(contract.Tools) memoryRows := topLevelRows(contract.Memory) - runtimeReminderRows := runtimeReminderRows(contract.RuntimeReminders) + 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), - RuntimeReminderJSON: prettyJSON(contract.RuntimeReminders), - ServiceAuthJSON: prettyJSON(contract.ServiceAuth), - LiveContextJSON: liveView.RawJSON, - MetadataRows: metadataRows, - FeedRows: feedRows, - ToolRows: toolRows, - MemoryRows: memoryRows, - RuntimeReminderRows: runtimeReminderRows, - 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.HasRuntimeReminder = data.RuntimeReminderJSON != "" + 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.HasRuntimeReminderRows = len(runtimeReminderRows) > 0 + data.HasContextBlockRows = len(contextBlockRows) > 0 data.HasServiceAuthRows = len(serviceAuthRows) > 0 return data } @@ -580,27 +581,28 @@ func scalarInt(v any) int { } } -func runtimeReminderRows(reminders any) []runtimeReminderRow { - root, ok := reminders.(map[string]any) +func contextBlockRows(blocks any) []contextBlockRow { + root, ok := blocks.(map[string]any) if !ok { return nil } - rawList, ok := root["reminders"].([]any) + rawList, ok := root["blocks"].([]any) if !ok { return nil } - rows := make([]runtimeReminderRow, 0, len(rawList)) + rows := make([]contextBlockRow, 0, len(rawList)) for _, item := range rawList { entry, ok := item.(map[string]any) if !ok { continue } - rows = append(rows, runtimeReminderRow{ + rows = append(rows, contextBlockRow{ ID: scalarString(entry["id"]), - Enabled: scalarString(entry["enabled"]), + Kind: scalarString(entry["kind"]), + Enabled: displayValue(entry["enabled"]), Cadence: scalarString(entry["cadence"]), Placement: scalarString(entry["placement"]), - MaxChars: scalarString(entry["max_chars"]), + MaxChars: displayValue(entry["max_chars"]), Text: scalarString(entry["text"]), }) } diff --git a/cmd/clawdash/handler_test.go b/cmd/clawdash/handler_test.go index 77740ca6..bd808bb1 100644 --- a/cmd/clawdash/handler_test.go +++ b/cmd/clawdash/handler_test.go @@ -403,14 +403,15 @@ func TestAgentContextDetailRendersContractAndLiveSnapshot(t *testing.T) { "type": "openclaw", }, Feeds: map[string]any{"feeds": []any{"alerts"}}, - RuntimeReminders: map[string]any{ + ContextBlocks: map[string]any{ "version": float64(1), - "reminders": []any{ + "blocks": []any{ map[string]any{ "id": "focus", + "kind": "runtime_motivation", "text": "Stay on the active operating contract.", "enabled": true, - "placement": "before_feeds", + "placement": "after_feeds", "cadence": "every_turn", "max_chars": float64(800), }, @@ -471,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", "Runtime reminders", "runtime-reminders.json", "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 7b72c3dc..dd94ff3a 100644 --- a/cmd/clawdash/templates/agent_detail.html +++ b/cmd/clawdash/templates/agent_detail.html @@ -259,15 +259,15 @@

Runtime inputs

{{end}} - {{if .HasRuntimeReminderRows}} + {{if .HasContextBlockRows}}
-
Runtime reminders
+
Context blocks
- + - {{range .RuntimeReminderRows}} - + {{range .ContextBlockRows}} + {{end}}
IDEnabledCadencePlacementMax charsText
IDKindEnabledCadencePlacementMax charsText
{{.ID}}{{.Enabled}}{{.Cadence}}{{.Placement}}{{.MaxChars}}{{.Text}}
{{.ID}}{{.Kind}}{{.Enabled}}{{.Cadence}}{{.Placement}}{{.MaxChars}}{{.Text}}
@@ -289,10 +289,10 @@

Runtime inputs

{{end}} - {{if and (not .HasMetadata) (not .HasFeeds) (not .HasTools) (not .HasMemory) (not .HasRuntimeReminder) (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 .HasRuntimeReminder .HasServiceAuth}} + {{if or .HasMetadata .HasFeeds .HasTools .HasMemory .HasContextBlock .HasServiceAuth}}
Raw runtime JSON {{if .HasMetadata}}
metadata.json
@@ -303,8 +303,8 @@ 

Runtime inputs

{{.ToolsJSON}}
{{end}} {{if .HasMemory}}
memory.json
 {{.MemoryJSON}}
{{end}} - {{if .HasRuntimeReminder}}
runtime-reminders.json
-{{.RuntimeReminderJSON}}
{{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 d7ae2e58..6ab22eb9 100644 --- a/docs/CLLAMA_SPEC.md +++ b/docs/CLLAMA_SPEC.md @@ -53,7 +53,7 @@ Clawdapus bind-mounts a shared directory into the proxy (at `CLAW_CONTEXT_ROOT`) │ ├── AGENTS.md # Compiled contract (includes, enforce, guide) │ ├── CLAWDAPUS.md # Infrastructure map │ ├── metadata.json # Identity, handles, and active policy modules -│ └── runtime-reminders.json # Optional operator-authored reminder snippets +│ └── context-blocks.json # Optional operator-authored context blocks ├── crypto-crusher-1/ │ └── ... ``` @@ -74,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 runtime reminders, 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. @@ -98,14 +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`, `runtime_reminder`, `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 -- `runtime_reminder_id`, `runtime_reminder_status`, `runtime_reminder_cadence`, `runtime_reminder_placement`, and `runtime_reminder_reason` for reminder 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 9e19455c..4c65f583 100644 --- a/internal/cllama/context.go +++ b/internal/cllama/context.go @@ -17,7 +17,7 @@ type AgentContextInput struct { Tools []ToolManifestEntry ToolPolicy *ToolPolicy // nil means DefaultToolPolicy Memory *MemoryManifestEntry - RuntimeReminders []RuntimeReminderManifestEntry + ContextBlocks []ContextBlockManifestEntry ServiceAuth []ServiceAuthEntry ChannelAllowlist []string } @@ -101,13 +101,14 @@ type MemoryOp struct { TimeoutMS int `json:"timeout_ms,omitempty"` } -type RuntimeReminderManifest struct { - Version int `json:"version"` - Reminders []RuntimeReminderManifestEntry `json:"reminders"` +type ContextBlockManifest struct { + Version int `json:"version"` + Blocks []ContextBlockManifestEntry `json:"blocks"` } -type RuntimeReminderManifestEntry struct { +type ContextBlockManifestEntry struct { ID string `json:"id"` + Kind string `json:"kind,omitempty"` Text string `json:"text"` Enabled bool `json:"enabled"` Placement string `json:"placement"` @@ -139,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,runtime-reminders.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 == "" { @@ -211,16 +212,16 @@ func GenerateContextDir(runtimeDir string, agents []AgentContextInput) error { } } - if len(agent.RuntimeReminders) > 0 { - remindersJSON, err := json.MarshalIndent(RuntimeReminderManifest{ - Version: 1, - Reminders: append([]RuntimeReminderManifestEntry(nil), agent.RuntimeReminders...), + 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 runtime reminders for %q: %w", agent.AgentID, err) + return fmt.Errorf("marshal context blocks for %q: %w", agent.AgentID, err) } - if err := os.WriteFile(filepath.Join(agentDir, "runtime-reminders.json"), append(remindersJSON, '\n'), 0644); err != nil { - return fmt.Errorf("write runtime-reminders.json 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) } } diff --git a/internal/cllama/context_test.go b/internal/cllama/context_test.go index 2e628d78..f056956b 100644 --- a/internal/cllama/context_test.go +++ b/internal/cllama/context_test.go @@ -129,11 +129,12 @@ func TestGenerateContextDirWritesOptionalFeedsAndServiceAuth(t *testing.T) { }, Auth: &AuthEntry{Type: "bearer", Token: "memory-token"}, }, - RuntimeReminders: []RuntimeReminderManifestEntry{{ + ContextBlocks: []ContextBlockManifestEntry{{ ID: "focus", + Kind: "runtime_motivation", Text: "Keep the operating contract visible.", Enabled: true, - Placement: "before_feeds", + Placement: "after_feeds", MaxChars: 800, Cadence: "every_turn", }}, @@ -194,16 +195,16 @@ func TestGenerateContextDirWritesOptionalFeedsAndServiceAuth(t *testing.T) { t.Fatalf("unexpected memory manifest payload: %v", memory) } - remindersRaw, err := os.ReadFile(filepath.Join(dir, "context", "octopus", "runtime-reminders.json")) + blocksRaw, err := os.ReadFile(filepath.Join(dir, "context", "octopus", "context-blocks.json")) if err != nil { t.Fatal(err) } - var reminders RuntimeReminderManifest - if err := json.Unmarshal(remindersRaw, &reminders); err != nil { + var blocks ContextBlockManifest + if err := json.Unmarshal(blocksRaw, &blocks); err != nil { t.Fatal(err) } - if reminders.Version != 1 || len(reminders.Reminders) != 1 || reminders.Reminders[0].ID != "focus" || !reminders.Reminders[0].Enabled { - t.Fatalf("unexpected runtime reminders manifest: %+v", reminders) + 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")) diff --git a/internal/pod/parser.go b/internal/pod/parser.go index 248acdab..dd6bffde 100644 --- a/internal/pod/parser.go +++ b/internal/pod/parser.go @@ -86,8 +86,8 @@ type rawClawBlock struct { } type rawContextConfig struct { - Channel *rawChannelContextConfig `yaml:"channel"` - RuntimeReminders []rawRuntimeReminderConfig `yaml:"runtime-reminders"` + Channel *rawChannelContextConfig `yaml:"channel"` + Blocks []rawContextBlockConfig `yaml:"blocks"` } type rawChannelContextConfig struct { @@ -98,8 +98,9 @@ type rawChannelContextConfig struct { Buffer int `yaml:"buffer"` } -type rawRuntimeReminderConfig struct { +type rawContextBlockConfig struct { ID string `yaml:"id"` + Kind string `yaml:"kind"` Text string `yaml:"text"` Enabled *bool `yaml:"enabled"` Placement string `yaml:"placement"` @@ -825,14 +826,14 @@ func parseContextConfig(raw *rawContextConfig) (*ContextConfig, error) { if err != nil { return nil, fmt.Errorf("channel: %w", err) } - runtimeReminders, err := parseRuntimeReminderConfigs(raw.RuntimeReminders) + blocks, err := parseContextBlockConfigs(raw.Blocks) if err != nil { - return nil, fmt.Errorf("runtime-reminders: %w", err) + return nil, fmt.Errorf("blocks: %w", err) } - if channel == nil && runtimeReminders == nil { + if channel == nil && blocks == nil { return &ContextConfig{}, nil } - return &ContextConfig{Channel: channel, RuntimeReminders: runtimeReminders}, nil + return &ContextConfig{Channel: channel, Blocks: blocks}, nil } func parseChannelContextConfig(raw *rawChannelContextConfig) (*ChannelContextConfig, error) { @@ -879,68 +880,73 @@ func selectChannelContextMaxChars(hyphen, underscore int) (int, error) { return underscore, nil } -func parseRuntimeReminderConfigs(raw []rawRuntimeReminderConfig) ([]RuntimeReminderConfig, error) { +func parseContextBlockConfigs(raw []rawContextBlockConfig) ([]ContextBlockConfig, error) { if raw == nil { return nil, nil } - out := make([]RuntimeReminderConfig, 0, len(raw)) + out := make([]ContextBlockConfig, 0, len(raw)) seen := make(map[string]struct{}, len(raw)) for i, entry := range raw { - reminder, err := parseRuntimeReminderConfig(entry) + block, err := parseContextBlockConfig(entry) if err != nil { return nil, fmt.Errorf("entry %d: %w", i, err) } - if _, ok := seen[reminder.ID]; ok { - return nil, fmt.Errorf("entry %d: duplicate id %q", i, reminder.ID) + if _, ok := seen[block.ID]; ok { + return nil, fmt.Errorf("entry %d: duplicate id %q", i, block.ID) } - seen[reminder.ID] = struct{}{} - out = append(out, reminder) + seen[block.ID] = struct{}{} + out = append(out, block) } return out, nil } -func parseRuntimeReminderConfig(raw rawRuntimeReminderConfig) (RuntimeReminderConfig, error) { +func parseContextBlockConfig(raw rawContextBlockConfig) (ContextBlockConfig, error) { id := strings.TrimSpace(raw.ID) if id == "" { - return RuntimeReminderConfig{}, fmt.Errorf("id must not be empty") + 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 RuntimeReminderConfig{}, fmt.Errorf("text must not be empty") + return ContextBlockConfig{}, fmt.Errorf("text must not be empty") } placement := strings.TrimSpace(raw.Placement) if placement == "" { - placement = "before_feeds" + placement = "after_feeds" } - if placement != "before_feeds" { - return RuntimeReminderConfig{}, fmt.Errorf("placement must be before_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 RuntimeReminderConfig{}, fmt.Errorf("cadence must be every_turn") + return ContextBlockConfig{}, fmt.Errorf("cadence must be every_turn") } - maxChars, err := selectRuntimeReminderMaxChars(raw.MaxCharsHyphen, raw.MaxCharsUnderscore) + maxChars, err := selectContextBlockMaxChars(raw.MaxCharsHyphen, raw.MaxCharsUnderscore) if err != nil { - return RuntimeReminderConfig{}, err + return ContextBlockConfig{}, err } if maxChars == 0 { maxChars = 800 } if maxChars < 0 { - return RuntimeReminderConfig{}, fmt.Errorf("max-chars must be >= 0") + return ContextBlockConfig{}, fmt.Errorf("max-chars must be >= 0") } if utf8.RuneCountInString(text) > maxChars { - return RuntimeReminderConfig{}, fmt.Errorf("text length must be <= max-chars") + return ContextBlockConfig{}, fmt.Errorf("text length must be <= max-chars") } enabled := true if raw.Enabled != nil { enabled = *raw.Enabled } - return RuntimeReminderConfig{ + return ContextBlockConfig{ ID: id, + Kind: kind, Text: text, Enabled: enabled, Placement: placement, @@ -949,7 +955,7 @@ func parseRuntimeReminderConfig(raw rawRuntimeReminderConfig) (RuntimeReminderCo }, nil } -func selectRuntimeReminderMaxChars(hyphen, underscore int) (int, error) { +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") } diff --git a/internal/pod/parser_context_test.go b/internal/pod/parser_context_test.go index d360aacc..0a10b6ad 100644 --- a/internal/pod/parser_context_test.go +++ b/internal/pod/parser_context_test.go @@ -59,11 +59,11 @@ services: } } -func TestParsePodContextRuntimeReminders(t *testing.T) { +func TestParsePodContextBlocks(t *testing.T) { p, err := Parse(strings.NewReader(` x-claw: context: - runtime-reminders: + blocks: - id: operating-focus text: Keep the operating contract visible. services: @@ -76,8 +76,9 @@ services: x-claw: agent: ./AGENTS.md context: - runtime-reminders: + blocks: - id: local-focus + kind: feed_frame text: Use the local reminder. enabled: false cadence: every_turn @@ -88,17 +89,17 @@ services: x-claw: agent: ./AGENTS.md context: - runtime-reminders: [] + blocks: [] `)) if err != nil { t.Fatalf("Parse: %v", err) } - if p.Context == nil || len(p.Context.RuntimeReminders) != 1 { - t.Fatalf("expected pod runtime reminder config, got %+v", p.Context) + if p.Context == nil || len(p.Context.Blocks) != 1 { + t.Fatalf("expected pod context block config, got %+v", p.Context) } - podReminder := p.Context.RuntimeReminders[0] - if podReminder.ID != "operating-focus" || podReminder.MaxChars != 800 || !podReminder.Enabled || podReminder.Cadence != "every_turn" || podReminder.Placement != "before_feeds" { - t.Fatalf("unexpected pod reminder defaults: %+v", podReminder) + 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"] @@ -107,17 +108,17 @@ services: } override := p.Services["override"] - if override == nil || override.Claw == nil || override.Claw.Context == nil || len(override.Claw.Context.RuntimeReminders) != 1 { - t.Fatalf("expected service runtime reminder override, got %+v", 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.RuntimeReminders[0] - if local.ID != "local-focus" || local.Enabled || local.MaxChars != 80 { - t.Fatalf("unexpected service reminder: %+v", local) + 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.RuntimeReminders == nil || len(suppress.Claw.Context.RuntimeReminders) != 0 { - t.Fatalf("expected explicit empty runtime reminder override, got %+v", 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) } } @@ -188,7 +189,7 @@ services: } } -func TestParsePodContextRuntimeRemindersRejectInvalidValues(t *testing.T) { +func TestParsePodContextBlocksRejectInvalidValues(t *testing.T) { cases := []struct { name string yaml string @@ -199,7 +200,7 @@ func TestParsePodContextRuntimeRemindersRejectInvalidValues(t *testing.T) { yaml: ` x-claw: context: - runtime-reminders: + blocks: - text: Missing id. services: agent: @@ -214,7 +215,7 @@ services: yaml: ` x-claw: context: - runtime-reminders: + blocks: - id: focus text: One. - id: focus @@ -232,7 +233,7 @@ services: yaml: ` x-claw: context: - runtime-reminders: + blocks: - id: focus text: One. cadence: min_interval @@ -249,10 +250,10 @@ services: yaml: ` x-claw: context: - runtime-reminders: + blocks: - id: focus text: One. - placement: after_feeds + placement: middle services: agent: image: example/agent:latest @@ -266,7 +267,7 @@ services: yaml: ` x-claw: context: - runtime-reminders: + blocks: - id: focus text: Too long. max_chars: 3 @@ -283,7 +284,7 @@ services: yaml: ` x-claw: context: - runtime-reminders: + blocks: - id: focus text: One. max-chars: 10 diff --git a/internal/pod/types.go b/internal/pod/types.go index 9d630d2e..d5b5d17d 100644 --- a/internal/pod/types.go +++ b/internal/pod/types.go @@ -131,8 +131,8 @@ type ChannelMemoryConfig struct { } type ContextConfig struct { - Channel *ChannelContextConfig - RuntimeReminders []RuntimeReminderConfig + Channel *ChannelContextConfig + Blocks []ContextBlockConfig } type ChannelContextConfig struct { @@ -142,8 +142,9 @@ type ChannelContextConfig struct { Buffer int } -type RuntimeReminderConfig struct { +type ContextBlockConfig struct { ID string + Kind string Text string Enabled bool Placement string diff --git a/site/changelog.md b/site/changelog.md index ed567064..efed508b 100644 --- a/site/changelog.md +++ b/site/changelog.md @@ -29,7 +29,7 @@ outline: deep ## Unreleased -- **Runtime reminder manifests** -- `x-claw.context.runtime-reminders` compiles short operator-authored reminders into per-agent `runtime-reminders.json` files, with pod-level inheritance, service-level replacement, and explicit empty-list suppression. claw-api and Clawdash expose the generated manifest 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). +- **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 19eb14a9..a0a92ece 100644 --- a/site/guide/cllama.md +++ b/site/guide/cllama.md @@ -156,7 +156,7 @@ During `claw up`, Clawdapus generates context files under the runtime directory: │ ├── feeds.json │ ├── tools.json │ ├── memory.json -│ ├── runtime-reminders.json +│ ├── context-blocks.json │ └── channels-allowlist.json ├── crypto-crusher-1/ │ ├── AGENTS.md @@ -179,7 +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. | -| `runtime-reminders.json` | Optional operator-authored reminder snippets that the proxy can inject into late runtime context. | +| `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 @@ -194,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, runtime reminder 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 @@ -203,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, runtime reminders, 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 @@ -417,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 (runtime reminders + 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. | @@ -427,7 +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) -- `runtime_reminder_id`, `runtime_reminder_status`, `runtime_reminder_cadence`, `runtime_reminder_placement`, `runtime_reminder_reason` — `runtime_reminder` events (one per manifest entry that was injected or skipped) +- `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 58d5ff6c..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, including channel-context tails and runtime reminders | +| `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,30 +119,31 @@ 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. -### Runtime Reminders +### Context Blocks -Runtime reminders 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 or current campaign objective. +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 reminders at pod level under `x-claw.context.runtime-reminders`, or override them per service under `services..x-claw.context.runtime-reminders`. A service-level list replaces the pod-level list; an explicit empty list suppresses inherited reminders for that service. +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: - runtime-reminders: + blocks: - id: operating-focus + kind: runtime_motivation text: Keep the active operating contract visible; act on current evidence. cadence: every_turn - placement: before_feeds + placement: after_feeds max-chars: 800 services: quiet-bot: x-claw: context: - runtime-reminders: [] # suppress pod-level reminders + blocks: [] # suppress pod-level blocks ``` -`id` and `text` are required. `enabled` defaults to `true`, `cadence` currently supports `every_turn`, `placement` currently supports `before_feeds`, and `max-chars` defaults to `800`. `claw up` validates the manifest and writes `runtime-reminders.json` to each subscribing agent's cllama context directory. +`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 diff --git a/skills/clawdapus/SKILL.md b/skills/clawdapus/SKILL.md index 8028bde4..1123e140 100644 --- a/skills/clawdapus/SKILL.md +++ b/skills/clawdapus/SKILL.md @@ -42,7 +42,7 @@ 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, feed_injection, runtime_reminder, + # 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 | @@ -306,11 +306,11 @@ 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. -### Runtime Reminders +### Context Blocks -Pod or service `x-claw.context.runtime-reminders` compiles short operator-authored snippets into each agent's cllama context directory as `runtime-reminders.json`. cllama injects enabled reminders into late runtime context before feeds on every turn, so durable operating focus stays visible without expanding the primary contract. +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`), and `max-chars` / `max_chars` (default `800`). A service-level list replaces pod-level reminders; an explicit empty list suppresses inherited reminders. +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) @@ -381,7 +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) - runtime-reminders.json # runtime reminder manifest (when configured) + context-blocks.json # context block manifest (when configured) ``` ### Provider support @@ -433,7 +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 | -| `runtime-reminders.json` | Runtime reminder manifest per agent | cllama context directory | +| `context-blocks.json` | Runtime context block manifest per agent | cllama context directory | ## Drivers @@ -502,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, runtime reminders, 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. @@ -530,10 +530,10 @@ 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 -### Runtime reminders not visible -- Check `runtime-reminders.json` in `.claw-runtime/context//` -- Check `claw audit --type runtime_reminder` for injected or skipped reminder entries -- Verify `cadence` is `every_turn`, `placement` is `before_feeds`, and the reminder text is within `max-chars` +### 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