Skip to content

refactor(agent): tighten message, permission, and stop boundaries#1973

Merged
zerob13 merged 3 commits into
devfrom
refactor/light-arch
Jul 15, 2026
Merged

refactor(agent): tighten message, permission, and stop boundaries#1973
zerob13 merged 3 commits into
devfrom
refactor/light-arch

Conversation

@zerob13

@zerob13 zerob13 commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • canonicalize send-message input once at the session application boundary
  • assign explicit owners for permission-mode defaults and persisted decoding
  • preserve provider stop reasons through AI SDK, ACP, accumulator, and settlement paths
  • validate pending input payloads at the storage boundary and degrade corrupt payloads to raw user text
  • retain completed architecture contracts while removing execution-only SDD artifacts and redundant tests

Why

Repeated normalization and lossy stop-reason mapping made downstream modules distrust typed callers and allowed invalid or bounded outcomes to be hidden. This change narrows internal contracts and keeps compatibility handling at owned boundaries. Pending payload corruption is logged but fails open so conversation continuity is not blocked.

Validation

  • pnpm run format
  • pnpm run i18n
  • pnpm run lint
  • pnpm run typecheck
  • pnpm run test:memory (46 files passed, 745 tests passed)
  • pnpm run test:main -- --run (365 files passed, 4189 tests passed)

Summary by CodeRabbit

  • New Features
    • Added structured message input handling for sending, steering, and queueing (text/files/skills/inline items), with schema-based validation and graceful decoding of legacy or malformed saved queued payloads.
  • Bug Fixes
    • Improved provider/ACP terminal outcome handling: missing terminal events now error; token/turn-limit outcomes (including max_turn_requests) are preserved; tool-use cancellations/refusals produce correct error+stop sequences without “successful completion” fallbacks.
    • Permission modes now retain the explicitly provided mode and persist as a required value, with safer handling of unknown legacy values.
  • Documentation
    • Documented message/application, permission-policy ownership, and provider stop-reason mapping contracts; removed an outdated terminal-outcome inconsistency spec.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds architecture contracts and aligns runtime behavior around three boundaries: canonical SendMessageInput propagation, explicit permission-mode defaulting and persistence ownership, and lossless provider terminal stop-reason translation through ACP, AI SDK, accumulation, and settlement paths.

Changes

Canonical message boundary

Layer / File(s) Summary
Contracts and application boundary
docs/architecture/send-message-canonical-boundary/*, src/main/presenter/sessionApplication/*, src/shared/contracts/routes/*
Structured message inputs are normalized at the session application boundary and validated through SendMessageInputSchema.
Runtime propagation and pending storage
src/main/agent/..., src/main/presenter/agentRuntimePresenter/{turnCoordinator,contextBuilder,acpCompatibilityDependencies}.ts
Downstream send, steer, queue, context, and ACP paths consume SendMessageInput directly; persisted payloads are decoded with validation and fallback handling.

Permission-mode ownership

Layer / File(s) Summary
Assignment and persistence boundaries
docs/architecture/permission-mode-policy-owner/*, src/main/presenter/sessionApplication/*, src/main/presenter/sqlitePresenter/*, src/main/presenter/agentRuntimePresenter/*
Assignment owns omitted-mode defaults, persistence decodes invalid values to default, and runtime/settings paths propagate exact modes without shared normalization.
Deferred execution
src/main/presenter/agentRuntimePresenter/deferredToolExecutor.ts
Deferred tools obtain provider, agent, and permission data from current session state before execution.

Provider terminal contract

Layer / File(s) Summary
Provider translation and settlement
src/shared/types/core/llm-events.ts, src/main/agent/acp/runtime/*, src/main/presenter/llmProviderPresenter/*, src/main/presenter/agentRuntimePresenter/{accumulator,process,types}.ts
Adds ProviderRoundStopReason, emits explicit ACP and AI SDK terminal events, initializes and resets stream stop state, and rejects incomplete or unsupported terminal outcomes.
Validation
test/main/**
Tests cover structured payload propagation, persistence decoding, permission-mode propagation, ACP settlement, AI SDK finish/abort handling, and terminal decisions.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SessionTurnCoordinator
  participant TurnCoordinator
  participant AgentRuntime
  participant PendingInputStore

  SessionTurnCoordinator->>SessionTurnCoordinator: normalize input once
  SessionTurnCoordinator->>TurnCoordinator: SendMessageInput
  TurnCoordinator->>AgentRuntime: send or queue SendMessageInput
  AgentRuntime->>PendingInputStore: persist structured payload
  PendingInputStore-->>AgentRuntime: validated decoded payload
Loading
sequenceDiagram
  participant Provider
  participant TerminalMapper
  participant Accumulator
  participant CompatibilityAdapter

  Provider->>TerminalMapper: provider stop reason
  TerminalMapper->>Accumulator: terminal events
  Accumulator->>Accumulator: assign stream stop reason
  Accumulator->>CompatibilityAdapter: settled stream state
  CompatibilityAdapter-->>Provider: preserved terminal outcome
Loading

Possibly related PRs

Suggested reviewers: zhangmo8

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title concisely reflects the PR’s main refactor themes: message input canonicalization, permission-mode ownership, and stop-reason boundary handling.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/light-arch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@zerob13 zerob13 marked this pull request as ready for review July 14, 2026 16:49

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/presenter/agentRuntimePresenter/contextBuilder.ts (1)

1088-1100: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the legacy string overload here. buildContextWithMetadata already takes SendMessageInput, but buildContext still accepts string | SendMessageInput and normalizes strings inline. Narrow this to SendMessageInput and update the remaining raw-string test calls in test/main/presenter/agentRuntimePresenter/contextBuilder.test.ts and test/main/presenter/agentRuntimePresenter/tapeService.test.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/presenter/agentRuntimePresenter/contextBuilder.ts` around lines 1088
- 1100, Remove the string overload from buildContext by changing its
newUserContent parameter to SendMessageInput and passing it directly to
buildContextWithMetadata without inline normalization. Update remaining
buildContext callers in contextBuilder.test.ts and tapeService.test.ts to
provide SendMessageInput objects instead of raw strings.
🧹 Nitpick comments (2)
src/main/presenter/agentRuntimePresenter/process.ts (1)

562-594: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consolidate the duplicated max_tool_calls checks.

The outcome.type === 'max_tool_calls' condition is evaluated twice here, separated by the abort check. Consolidating these blocks into a single branch improves readability and keeps the logic tightly coupled.

If the intention is to skip marking tool calls as aborted when a user explicitly cancels the generation, placing the consolidated block immediately after the abort check handles this cleanly.

♻️ Proposed refactor
-  if (outcome.type === 'max_tool_calls') {
-    logger.info(
-      `[ProcessStream] max tool calls reached (${outcome.attemptedToolCount} > ${outcome.limit}), stopping`
-    )
-    state.planTerminalReason = 'max_steps'
-    markUnexecutedToolCallsForLimit(state)
-  }
-
   if (io.abortSignal.aborted) {
     stampRunOutcome(state, 'aborted', 'user_stop')
     finalizeUserCanceledErrorIfNeeded(state, io)
     return {
       status: 'aborted',
       stopReason: 'user_stop',
       errorMessage: USER_CANCELED_GENERATION_ERROR,
       usage: buildUsageSnapshot(state)
     }
   }
 
   if (outcome.type === 'max_tool_calls') {
+    logger.info(
+      `[ProcessStream] max tool calls reached (${outcome.attemptedToolCount} > ${outcome.limit}), stopping`
+    )
+    state.planTerminalReason = 'max_steps'
+    markUnexecutedToolCallsForLimit(state)
     stampRunOutcome(state, 'completed', 'max_tool_calls')
     outputSink.complete({
       runId: run.runId,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/presenter/agentRuntimePresenter/process.ts` around lines 562 - 594,
Consolidate the two outcome.type === 'max_tool_calls' blocks in the process
stream flow into one branch positioned after the io.abortSignal.aborted
handling. Preserve the existing max-tool-call logging, planTerminalReason
assignment, markUnexecutedToolCallsForLimit call, completion signaling, and
return values, while ensuring user cancellation is handled first.
src/main/presenter/agentRuntimePresenter/index.ts (1)

873-876: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract repeated inline normalization to a helper.

The fallback logic coercing string | SendMessageInput to { text, files: [] } is duplicated across four methods. Consider extracting this into a private helper to keep the code DRY.

  • src/main/presenter/agentRuntimePresenter/index.ts#L873-L876: Extract to a private method toSendMessageInput(content: string | SendMessageInput): SendMessageInput.
  • src/main/presenter/agentRuntimePresenter/index.ts#L907-L908: Call the helper here.
  • src/main/presenter/agentRuntimePresenter/index.ts#L967-L968: Call the helper here.
  • src/main/presenter/agentRuntimePresenter/index.ts#L1045-L1046: Call the helper here.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/presenter/agentRuntimePresenter/index.ts` around lines 873 - 876,
Extract the duplicated string-or-input normalization into a private
toSendMessageInput(content: string | SendMessageInput): SendMessageInput helper
in src/main/presenter/agentRuntimePresenter/index.ts, preserving the existing
text and empty-files fallback. Replace the inline normalization at lines
873-876, 907-908, 967-968, and 1045-1046 with calls to this helper; all four
sites require the same change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/agent/acp/runtime/acpContentMapper.ts`:
- Around line 11-28: Add a default branch to the stop-reason switch in the ACP
content-mapping function so unexpected protocol values always return a valid
event array. Preserve existing mappings and use the established error-and-stop
event pattern for the fallback, preventing callers from receiving undefined.

In `@test/main/presenter/usageStatsService.test.ts`:
- Line 439: Update the SessionRow type and the deepchatSessionsTable.create mock
to include a permissionMode field and accept the fourth argument, respectively.
Store the provided permissionMode when creating session rows so the mock matches
the table API and schema used by the test setup.

---

Outside diff comments:
In `@src/main/presenter/agentRuntimePresenter/contextBuilder.ts`:
- Around line 1088-1100: Remove the string overload from buildContext by
changing its newUserContent parameter to SendMessageInput and passing it
directly to buildContextWithMetadata without inline normalization. Update
remaining buildContext callers in contextBuilder.test.ts and tapeService.test.ts
to provide SendMessageInput objects instead of raw strings.

---

Nitpick comments:
In `@src/main/presenter/agentRuntimePresenter/index.ts`:
- Around line 873-876: Extract the duplicated string-or-input normalization into
a private toSendMessageInput(content: string | SendMessageInput):
SendMessageInput helper in src/main/presenter/agentRuntimePresenter/index.ts,
preserving the existing text and empty-files fallback. Replace the inline
normalization at lines 873-876, 907-908, 967-968, and 1045-1046 with calls to
this helper; all four sites require the same change.

In `@src/main/presenter/agentRuntimePresenter/process.ts`:
- Around line 562-594: Consolidate the two outcome.type === 'max_tool_calls'
blocks in the process stream flow into one branch positioned after the
io.abortSignal.aborted handling. Preserve the existing max-tool-call logging,
planTerminalReason assignment, markUnexecutedToolCallsForLimit call, completion
signaling, and return values, while ensuring user cancellation is handled first.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 801bd9ea-f56e-4796-803e-db57b95655d8

📥 Commits

Reviewing files that changed from the base of the PR and between a461415 and 2452846.

📒 Files selected for processing (61)
  • docs/architecture/agent-system.md
  • docs/architecture/permission-mode-policy-owner/spec.md
  • docs/architecture/provider-round-stop-contract/spec.md
  • docs/architecture/send-message-canonical-boundary/spec.md
  • docs/issues/agent-terminal-outcome-consistency/spec.md
  • src/main/agent/acp/instance/acpAgentInstance.ts
  • src/main/agent/acp/instance/acpAgentRuntime.ts
  • src/main/agent/acp/instance/ports.ts
  • src/main/agent/acp/runtime/acpContentMapper.ts
  • src/main/agent/acp/runtime/index.ts
  • src/main/agent/deepchat/deepChatAgentRepository.ts
  • src/main/agent/deepchat/pending/pendingInputCoordinator.ts
  • src/main/agent/deepchat/pending/pendingInputStore.ts
  • src/main/agent/manager/deepChatAgentBackend.ts
  • src/main/agent/manager/sessionHandles.ts
  • src/main/agent/shared/agentSessionHandle.ts
  • src/main/presenter/agentRuntimePresenter/accumulator.ts
  • src/main/presenter/agentRuntimePresenter/acpCompatibilityAdapters.ts
  • src/main/presenter/agentRuntimePresenter/acpCompatibilityDependencies.ts
  • src/main/presenter/agentRuntimePresenter/compactionService.ts
  • src/main/presenter/agentRuntimePresenter/contextBuilder.ts
  • src/main/presenter/agentRuntimePresenter/deferredToolExecutor.ts
  • src/main/presenter/agentRuntimePresenter/generationSettings.ts
  • src/main/presenter/agentRuntimePresenter/index.ts
  • src/main/presenter/agentRuntimePresenter/process.ts
  • src/main/presenter/agentRuntimePresenter/sessionSettingsCoordinator.ts
  • src/main/presenter/agentRuntimePresenter/sessionStore.ts
  • src/main/presenter/agentRuntimePresenter/tapeViewPolicy.ts
  • src/main/presenter/agentRuntimePresenter/turnCoordinator.ts
  • src/main/presenter/agentRuntimePresenter/types.ts
  • src/main/presenter/llmProviderPresenter/aiSdk/streamAdapter.ts
  • src/main/presenter/llmProviderPresenter/providers/acpProvider.ts
  • src/main/presenter/sessionApplication/agentAssignmentPolicy.ts
  • src/main/presenter/sessionApplication/turnCoordinator.ts
  • src/main/presenter/sqlitePresenter/tables/deepchatSessions.ts
  • src/shared/contracts/routes/sessions.routes.ts
  • src/shared/types/agent-interface.d.ts
  • src/shared/types/core/llm-events.ts
  • test/main/agent/acp/instance/acpAgentRuntime.test.ts
  • test/main/agent/acp/runtime/acpContentMapper.test.ts
  • test/main/agent/deepchat/pending/pendingInputStore.test.ts
  • test/main/agent/manager/deepChatAgentBackend.test.ts
  • test/main/agent/manager/directAcpAgentBackend.test.ts
  • test/main/presenter/agentRepository.test.ts
  • test/main/presenter/agentRuntimePresenter/accumulator.test.ts
  • test/main/presenter/agentRuntimePresenter/acpCompatibilityAdapters.test.ts
  • test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts
  • test/main/presenter/agentRuntimePresenter/compactionService.test.ts
  • test/main/presenter/agentRuntimePresenter/contextBuilder.test.ts
  • test/main/presenter/agentRuntimePresenter/process.test.ts
  • test/main/presenter/agentRuntimePresenter/tapeService.test.ts
  • test/main/presenter/agentRuntimePresenter/tapeViewAssembler.test.ts
  • test/main/presenter/agentRuntimePresenter/tapeViewPolicy.test.ts
  • test/main/presenter/llmProviderPresenter/aiSdkStreamAdapter.test.ts
  • test/main/presenter/sessionApplication/agentAssignmentPolicy.test.ts
  • test/main/presenter/sessionApplication/runtimeIntegration.test.ts
  • test/main/presenter/sessionApplication/turnCoordinator.test.ts
  • test/main/presenter/sqlitePresenter.test.ts
  • test/main/presenter/sqlitePresenter/deepchatSessionsTable.test.ts
  • test/main/presenter/usageStatsService.test.ts
  • test/main/routes/contracts.test.ts
💤 Files with no reviewable changes (1)
  • docs/issues/agent-terminal-outcome-consistency/spec.md

Comment thread src/main/agent/acp/runtime/acpContentMapper.ts
Comment thread test/main/presenter/usageStatsService.test.ts
@zerob13 zerob13 changed the title refactor(agent): simplify core boundaries refactor(agent): tighten message, permission, and stop boundaries Jul 15, 2026

@zhangmo8 zhangmo8 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed current head 7d66ae8.

No blocking findings. The renderer is unchanged, and activeSkills/inlineItems remain preserved through send, steer, initial-turn, queue, persistence, and ACP paths, so normal skill chips in the composer are unaffected. Pending-input validation only changes behavior for malformed or corrupt payloads.

Validation performed:

  • 222 focused main-process tests passed
  • 26 ChatInputBox and MessageItemUser renderer tests passed
  • typecheck:node and typecheck:web passed
  • PR build and memory checks are green

@zerob13 zerob13 merged commit e042478 into dev Jul 15, 2026
4 checks passed
@zhangmo8 zhangmo8 deleted the refactor/light-arch branch July 15, 2026 04:54
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.

2 participants