Skip to content

Consequential-action gate: external writes need a user request; persona 'proactive' = notice (#685) - #687

Merged
rockfordlhotka merged 1 commit into
mainfrom
issue-685/consequential-action-gate
Oct 10, 2026
Merged

rockfordlhotka merged 1 commit into
mainfrom
issue-685/consequential-action-gate

Conversation

@rockfordlhotka

Copy link
Copy Markdown
Member

Closes #685. Part of #671. Stacked on #684 (#683) → #682 (#665) → #675 (#666). The design was approved by the repo owner.

Problem

A context-only message (a talk abstract and "the talk doesn't exist yet") led RockBot to create calendar events on the user's real marimer-work calendar. The persona directives say to anticipate needs and "do it immediately… do not ask permission", and nothing separated noticing from acting on external systems.

Changes

  • Consequential-action gate (ConsequentialActionGate.cs).
    • What is gated: a side-effecting call (per ToolSideEffects) that targets an external system. That means any MCP write (mcp_invoke_tool judged by its tool_name, or a typed {server}__{tool}), schedule_task/cancel_scheduled_task (otherwise a scheduled task could act later without a request), and anything listed in ExternalTools.
    • Always allowed: agent-local writes (drafts/ files, memory, task list, skills, sandboxed scripts), delegation, and reads.
    • Run origin is explicit. User turns are classified with ClassifyUserRequest; "yes" after a proposal counts as an instruction. Subagents inherit the user's classification, so their own task description can't launder an information-only message. Wisps and workers inherit the enclosing scope. Scheduled, A2A-inbound and Unknown origins are allowed (unchanged behaviour).
    • On refusal the call is not executed. It returns "Not run: … Propose it to the user in one sentence … and wait", is logged, and is counted on rockbot.agent.consequential_action.gated.
    • Check sites: the native function-invoking client, both text-loop sites, and wisp direct steps.
    • Config: AgentHost:ConsequentialActionGate:Enabled (default true).
  • Evaluator backstop. On an information-only turn that needs an instruction, any successful external change (the primary's or relayed from a subagent) makes the reply INCOMPLETE ("unrequested external change"). The re-prompt tells the user plainly what changed so they can keep or undo it.
  • Persona. soul.md and directives.md: "Proactive Behaviors" becomes "Proactive Noticing". Notice and surface freely; acting on your own initiative is limited to the agent's own state. Unrequested external changes get a one-sentence offer. "Response Endings" is scoped to match. subagent-directives.md and worker-directives.md: on a "Not run" refusal, report the proposal; don't retry or reroute.
  • convert_time tool. Converts between IANA zones and shows both UTC offsets. Amsterdam 09:45 on 2026-10-28 → Chicago 03:45; a model had computed 2:45. A line in common-directives.md points to it.

Deploy note

Refresh these PVC copies by hand: soul.md, directives.md, common-directives.md, subagent-directives.md, worker-directives.md.

Known gaps (follow-ups)

Tests

  • ConsequentialActionGateTests (45): classifier, scope, native and text runs, the subagent and nested cases, allowed origins, evaluator backstop.
  • WispConsequentialActionGateTests (3).
  • TimeConversionTests (7).
  • SubagentManagerTests (+2).

Full suite: about 4,515 passed, 0 failed.

🤖 Generated with Claude Code

@rockfordlhotka
rockfordlhotka force-pushed the issue-683/synthesis-evaluator-evidence branch from d00c67d to 3881144 Compare October 10, 2026 20:54
@rockfordlhotka
rockfordlhotka force-pushed the issue-685/consequential-action-gate branch from c8fad44 to daf86d3 Compare October 10, 2026 20:54
@rockfordlhotka
rockfordlhotka force-pushed the issue-683/synthesis-evaluator-evidence branch from 3881144 to 80d9b3f Compare October 10, 2026 21:03
Base automatically changed from issue-683/synthesis-evaluator-evidence to main October 10, 2026 21:08
A context-only user message ("the talk doesn't exist yet") led a subagent and
its wisps to create seven real calendar events. The persona told the agent to
act proactively, and nothing in the framework separated noticing from changing
an external system.

Gate (framework)
- ConsequentialActions: a call is consequential when ToolSideEffects says it
  writes AND it reaches an MCP server (mcp_invoke_tool by tool_name, or a typed
  {server}__{tool} wrapper), or it creates/cancels the agent's scheduled tasks,
  or it is listed in AgentHost:ConsequentialActionGate:ExternalTools. file_*,
  memory, task list, skills, rules, scripts and delegation stay agent-local.
- ActionGateScope(RunOrigin, UserAskedFor), bound per async flow by RunAsync
  (ActionGateContext). UserTurn / SubagentOfUserTurn need an instruction;
  Scheduled, A2A and Unknown origins keep acting. RunAsync(actionGate:) is set
  by UserMessageHandler, UserFeedbackHandler, ScheduledTaskHandler, the A2A
  inbound handlers and the subagent path; nested runs (wisp LLM steps,
  workers) inherit. A user turn uses ClassifyUserRequest with
  followsAgentMessage, so "yes" after a proposal is an instruction.
- SubagentManager captures the spawning scope (SubagentEntry.ActionGate);
  SubagentRunner runs under it and carries RunOrigin/UserAskedFor on
  SubagentResultMessage; the synthesis turn runs under the same scope.
- Checked at every dispatch site: RockBotFunctionInvokingChatClient, both
  text-loop sites, WispExecutor direct steps. A refused call is not run; it
  returns "Not run: <tool> would change <server> but the user did not ask for
  that. Propose it ...", is recorded as failed, logged and counted
  (rockbot.agent.consequential_action.gated).
- AgentHost:ConsequentialActionGate:Enabled (default true) turns it off.

Evaluator backstop
- For an information-only request in a user-turn lineage, successful external
  changes in the loop's or relayed subagents' calls are listed under "External
  changes the user did not ask for" and are grounds for INCOMPLETE; the
  re-prompt says to tell the user plainly what changed so they can keep or
  undo it. The follow-up evaluator no longer suggests unrequested external
  changes.

Persona (seed files; PVC copies need refreshing)
- soul.md, directives.md: proactive = notice and surface; acting on your own
  initiative is limited to memory, working memory and drafts/; external
  changes need a request, offered in one sentence.
- subagent-directives.md, worker-directives.md: on a "Not run" refusal, don't
  retry or reroute; report the proposal.
- common-directives.md: use convert_time for cross-zone times.

Time conversion
- convert_time(datetime, from_timezone, to_timezone) registry tool
  (RockBot.Tools.TimeConversion, AddTimeTools) with the DST rules of the date:
  09:45 Europe/Amsterdam on 2026-10-28 is 03:45 America/Chicago.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rockfordlhotka
rockfordlhotka force-pushed the issue-685/consequential-action-gate branch from daf86d3 to d026734 Compare October 10, 2026 21:08
@rockfordlhotka
rockfordlhotka merged commit 8f4bfc2 into main Oct 10, 2026
2 checks passed
@rockfordlhotka
rockfordlhotka deleted the issue-685/consequential-action-gate branch October 10, 2026 21:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unrequested external side effects: context-only message created 7 real calendar events (persona 'proactive' directives + no action gate)

1 participant