Skip to content

refactor(5/12): change handler contract to event-based emission#323

Open
cameroncooke wants to merge 2 commits intorefactor/build-test-utility-extractionfrom
refactor/runtime-handler-contract
Open

refactor(5/12): change handler contract to event-based emission#323
cameroncooke wants to merge 2 commits intorefactor/build-test-utility-extractionfrom
refactor/runtime-handler-contract

Conversation

@cameroncooke
Copy link
Copy Markdown
Collaborator

Summary

This is PR 5 of 12 in a stacked PR series that decouples the rendering pipeline from MCP transport. Depends on PR 4 (utility extraction). This is the most architecturally significant PR in the stack -- it changes the fundamental contract between tool handlers and the runtime.

The contract change

Before: Tool handlers returned a ToolResponse object containing pre-rendered MCP content:
```typescript
handler: (params) => Promise
```

After: Tool handlers receive a context object and emit events through it, returning nothing:
```typescript
handler: (params, ctx: ToolHandlerContext) => Promise
```

The ToolHandlerContext provides:

  • emit(event: PipelineEvent): Push a structured event into the render session
  • attach(image: ImageAttachment): Attach binary content (screenshots)

This decouples tools from rendering entirely. A tool says "the build succeeded" via ctx.emit(statusLine('success', 'Build succeeded')) without knowing whether the output will be rendered as colored terminal text, JSON, or MCP protocol content.

Tool invoker refactor

The invoker (tool-invoker.ts) is significantly simplified:

  • Deleted postProcessToolResponse, emitNextStepsEvent, renderNextStepsIntoContent, finalizeResult
  • The invoker creates a ToolHandlerContext, passes it to the handler, then emits next-steps into the same render session after the handler completes
  • No more PendingBuildResult pattern -- the handler just awaits the pipeline and the render session accumulates events progressively

Typed tool factory

typed-tool-factory.ts updated to produce handlers matching the new signature. Session-aware wrappers now thread the context through.

Core manifest and schema

  • import-resource-module.ts added for resource manifest loading (split from tool module loading)
  • Schema and load-manifest updated for the simplified handler contract
  • plugin-types.ts updated to reflect new handler signatures

Deleted

  • typed-tool-factory-consolidation.test.ts (tested removed consolidation logic)
  • load-manifest.test.ts (moved to schema test coverage)

Stack navigation

  • PR 1-4/12: Foundation and utility extraction
  • PR 5/12 (this PR): Runtime handler contract and tool invoker
  • PR 6-9/12: Tool migrations (mechanical -- update handlers to use ctx.emit)
  • PR 10-12/12: Boundaries, config, tests

Note for reviewers

PRs 6-9 (tool migrations) depend on this contract change. Each tool handler is updated from return toolResponse([...]) to ctx.emit(...) calls. Those are mechanical transformations but they cannot compile without this PR landing first. If reviewing the stack incrementally, this PR is the critical design decision point.

Test plan

  • npx vitest run passes -- invoker tests updated for new contract
  • Handler context correctly propagates emit/attach to render session
  • Next-steps are emitted into the render session after handler completion
  • Typed tool factory produces handlers with correct signatures

@cameroncooke cameroncooke force-pushed the refactor/runtime-handler-contract branch from 089f6a3 to fa1ece2 Compare April 8, 2026 21:29
@cameroncooke cameroncooke force-pushed the refactor/build-test-utility-extraction branch from 344aeba to f7616d2 Compare April 8, 2026 21:29
@cameroncooke cameroncooke force-pushed the refactor/runtime-handler-contract branch from fa1ece2 to e6e44bb Compare April 9, 2026 07:49
@cameroncooke cameroncooke force-pushed the refactor/build-test-utility-extraction branch from f7616d2 to 7b1a15a Compare April 9, 2026 07:49
@cameroncooke cameroncooke force-pushed the refactor/runtime-handler-contract branch from e6e44bb to 8208efa Compare April 9, 2026 07:59
@cameroncooke cameroncooke force-pushed the refactor/build-test-utility-extraction branch from 7b1a15a to 379222d Compare April 9, 2026 07:59
@cameroncooke cameroncooke force-pushed the refactor/build-test-utility-extraction branch from 379222d to 7ffc681 Compare April 9, 2026 08:45
@cameroncooke cameroncooke force-pushed the refactor/runtime-handler-contract branch from 8208efa to 3d0b705 Compare April 9, 2026 08:45
@cameroncooke cameroncooke force-pushed the refactor/build-test-utility-extraction branch from 9d23583 to 2496ad9 Compare April 9, 2026 10:39
@cameroncooke cameroncooke force-pushed the refactor/build-test-utility-extraction branch from 2496ad9 to 7ac6621 Compare April 9, 2026 10:56
@cameroncooke cameroncooke force-pushed the refactor/runtime-handler-contract branch from 84180f1 to 1a1c722 Compare April 9, 2026 10:56
@cameroncooke cameroncooke force-pushed the refactor/build-test-utility-extraction branch from 7ac6621 to b6ea0c1 Compare April 9, 2026 11:22
@cameroncooke cameroncooke force-pushed the refactor/runtime-handler-contract branch 2 times, most recently from 9396aff to fef58c3 Compare April 9, 2026 11:31
@cameroncooke cameroncooke force-pushed the refactor/build-test-utility-extraction branch 2 times, most recently from 30231f9 to a36e4d5 Compare April 9, 2026 11:48
@cameroncooke cameroncooke force-pushed the refactor/runtime-handler-contract branch from fef58c3 to 03324df Compare April 9, 2026 11:48
@cameroncooke cameroncooke force-pushed the refactor/build-test-utility-extraction branch from a36e4d5 to c0d77e5 Compare April 9, 2026 12:03
@cameroncooke cameroncooke force-pushed the refactor/runtime-handler-contract branch from 03324df to 589573e Compare April 9, 2026 12:03
@cameroncooke cameroncooke force-pushed the refactor/build-test-utility-extraction branch from c0d77e5 to 7315fff Compare April 9, 2026 14:43
@cameroncooke cameroncooke force-pushed the refactor/runtime-handler-contract branch from 589573e to 886a46f Compare April 9, 2026 14:43
@cameroncooke cameroncooke force-pushed the refactor/runtime-handler-contract branch from 886a46f to a42228b Compare April 9, 2026 15:15
@cameroncooke cameroncooke force-pushed the refactor/build-test-utility-extraction branch from 7315fff to 80b323f Compare April 9, 2026 15:15
@cameroncooke cameroncooke force-pushed the refactor/runtime-handler-contract branch from a42228b to 696acb9 Compare April 9, 2026 15:40
@cameroncooke cameroncooke force-pushed the refactor/build-test-utility-extraction branch from 80b323f to 3126be4 Compare April 9, 2026 15:40
@cameroncooke cameroncooke force-pushed the refactor/build-test-utility-extraction branch from 3126be4 to 09a2d7d Compare April 9, 2026 20:50
@cameroncooke cameroncooke force-pushed the refactor/build-test-utility-extraction branch from 09a2d7d to bee0b39 Compare April 9, 2026 20:50
@cameroncooke cameroncooke force-pushed the refactor/runtime-handler-contract branch from 696acb9 to 0384076 Compare April 9, 2026 20:50
Four tool files still accessed the removed errorResponse property
on ValidationResult. Updated to construct error responses from
the new errorMessage field.

Restore responses/index.ts barrel with shim functions for
createErrorResponse/createTextResponse (removed in handler-contract
refactor but still consumed by ~35 files migrated in PRs 6-9).
Also restore processToolResponse in next-steps-renderer.ts.

Note: --no-verify required because docs:check boots the full CLI
which hits forward-references to later PRs in the Graphite stack.
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c7b7e13. Configure here.

const hasTextContent = response.content.some((item) => item.type === 'text');
if (!hasTextContent && nextStepsSection) {
processedContent.push({ type: 'text', text: nextStepsSection.trim() });
}
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.

Next steps silently dropped for mixed content types

Medium Severity

processToolResponse appends the next-steps section only when the last content item is text. The fallback (if (!hasTextContent)) checks whether any item is text. When the last item is non-text (e.g., an image) but earlier items are text, hasTextContent is true so the fallback is skipped, and the next-steps section is silently dropped from the output.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c7b7e13. Configure here.

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.

1 participant