Loop-Level Protocols
RFCs: 203 (LoopWorkingMemory), 220 (StrangeLoop), 604 (LoopPlanner), 617 (OperationSecurity) Locations:
packages/soothe/src/soothe/protocols/loop_working_memory.pypackages/soothe/src/soothe/protocols/loop_planner.py(covered in planner.md)packages/soothe/src/soothe/protocols/operation_security.pyStatus: Implemented
What Loop-Level Protocols Are
These protocols serve StrangeLoop’s Plan → Execute cycle specifically. They are not general-purpose infrastructure — they exist because the agentic loop has unique needs: a bounded scratchpad for planning prompts, and operation-level security checks during execution.
Two protocols live here:
LoopWorkingMemoryProtocol— a bounded scratchpad that records Act-step outcomes and renders them into Plan-phase prompts.OperationSecurityProtocol— operation-level security evaluation for individual tool calls and spawns.
(LoopPlannerProtocol is documented in planner.md since it shares the planner domain.)
LoopWorkingMemoryProtocol
Role
During a StrangeLoop, each Execute phase produces step results. The Plan phase (next iteration) needs to see what happened — but not the full raw history, which would blow the LLM context window. LoopWorkingMemoryProtocol is the bounded intermediary: it records step outcomes, then renders a truncated view for the planner.
Key Operations
clear()— reset for a new goal. Called at loop start.record_step_result(step_id, description, output, error, success, workspace, thread_id)— record one Act-step outcome. Outputs are truncated to bounded previews (e.g., 200 chars) to control size.render_for_reason(max_chars?)— return formatted text for Plan-phase prompt injection. Whenmax_charsis set, the rendered text is hard-truncated.
Design Principle: Bounded Memory
Working memory is deliberately bounded — the default implementation caps at ~20 entries with truncated previews. The philosophy: the planner needs recent outcomes and patterns, not exhaustive history. When memory exceeds the bound, oldest entries are evicted (FIFO).
Execute phase: record_step_result(...) → memory grows (bounded)
Plan phase: render_for_reason(max_chars=2000) → bounded slice injected into prompt
This bounded approach contrasts with ContextEngine (soothe.foundation.context), which provides unbounded accumulation with relevance-based projection. Working memory is the pragmatic, always-available scratchpad; ContextEngine is the richer, persistent ledger. ContextProtocol was planned (RFC-302 draft) but never implemented — context is managed by ContextEngine directly, not a protocol.
Workspace Spill
When bounded memory exceeds limits, implementations may spill overflow to a workspace file (e.g., .soothe/working_memory_spill.json) before evicting from the in-memory list. This is optional — the protocol doesn’t mandate it — but it preserves detail that bounded eviction would lose. Recent entries stay in memory; older ones spill to disk.
Implementation
The default implementation (DefaultWorkingMemory) is an in-memory bounded list with truncated output previews, error tracking, and optional workspace spill. Configurable via agent.loop.working_memory settings (max_entries, max_chars_per_entry, spill_to_workspace).
OperationSecurityProtocol
Role
OperationSecurityProtocol provides operation-level security checks — evaluating whether a specific operation (tool call, shell command, file write) should proceed. It’s a normalized, operation-focused layer distinct from the broader PolicyProtocol.
Key Operation
evaluate(request, context) → OperationSecurityDecision— evaluate whether an operation should be allowed, returning a verdict (allow/deny/need_approval) with a reason.
Operation Kinds
The protocol normalizes operations into typed kinds:
filesystem_read,filesystem_write— file operationsshell_execute,python_execute— code executionprocess_control— process managementgeneric— fallback
This typing lets security rules target specific operation categories (e.g., audit all shell_execute calls) without parsing tool arguments generically.
Request and Context
OperationSecurityRequest— carriesaction_type,tool_name,tool_args,operation_kind,target_path, andcommand(for shell operations). This is richer thanPolicyProtocol’sActionRequestbecause it includes operation-specific extraction (path, command).OperationSecurityContext— thread ID, workspace path, and security config. Workspace is used for stream-scoped filesystem policy.OperationSecurityDecision— verdict, reason, optionalrule_idfor audit traceability.
Relationship to PolicyProtocol
OperationSecurityProtocol and PolicyProtocol are complementary, not redundant:
PolicyProtocol(see policy.md) operates on structuredPermissiongrants (category/action/scope) and computes narrowed permission sets for subagent delegation. It’s the policy decision layer.OperationSecurityProtocolis the operation evaluation layer — it normalizes concrete operations (a shell command, a file path) into security requests and applies rules, potentially delegating toPolicyProtocolfor permission matching.
In practice, operation security can wrap policy: convert an OperationSecurityRequest into an ActionRequest, call PolicyProtocol.check(), then augment the result with operation-specific concerns (audit logging, path validation).
Integration with StrangeLoop
These protocols plug into the Plan → Execute cycle at specific points:
StrangeLoop iteration:
Plan phase:
→ LoopWorkingMemory.render_for_reason() injected into planner prompt
→ LoopPlanner.plan() produces PlanResult
Execute phase:
→ OperationSecurity.evaluate() checks each tool call before execution
→ LoopWorkingMemory.record_step_result() captures outcome
→ next iteration
Working memory flows into the Plan phase (rendering); operation security gates the Execute phase (checking). Both are per-loop, in-memory, and reset between goals.
Gotchas
- Working memory is per-loop, not per-thread —
clear()is called at the start of each goal/loop. Cross-thread continuity requiresMemoryProtocol, not working memory. - Truncation is lossy —
record_step_resulttruncates outputs to bounded previews. If you need full output, capture it separately; working memory is for the planner’s summary view. - Operation security may run synchronously — unlike persistence protocols,
evaluate()is not async in the source. Don’t block on external calls inside it. operation_kindaffects audit — security config can specify which operation kinds require audit logging (audit_operationslist). Generic kind misses targeted audit rules.
Configuration
agent:
loop:
working_memory:
max_entries: 20
max_chars_per_entry: 200
spill_to_workspace: false
security:
audit_operations: [shell_execute, subagent_spawn]
audit_log_path: ~/.soothe/logs/operations.log
Related Documentation
- Planner Protocol —
LoopPlannerProtocolconsumes working memory - Policy Protocol —
PolicyProtocolbacks operation security decisions - Execution Protocols —
LoopRunnerProtocolorchestrates the loop - ContextProtocol — not implemented; context managed by ContextEngine