Surface Abstraction Layer
Relevant source files
The following files were used as context for generating this wiki page:
The Surface Abstraction Layer provides a unified interface, defined by the AgentSurface interface, which allows the pi-digby agent to communicate with users across different messaging platforms such as Slack and Linear. This layer abstracts the platform-specific complexities—such as message threading, ephemeral updates, and media handling—allowing the core agent logic to remain platform-agnostic.
The layer ensures that every agent run follows a consistent lifecycle, beginning with an indication of activity and ending with a terminal operation (resolving, rejecting, or suppressing the output). It manages the serialization of updates and the presentation of run statistics, such as step counts and execution costs.
Sources: src/surface/types.ts:1-35, src/surface/slack.ts:33-42, src/main.ts:109-158
Core Interface: AgentSurface
The AgentSurface interface defines the contract for any platform implementation. It categorizes operations into streaming progress, detail reporting, and terminal lifecycle events.
| Method | Description |
|---|---|
emitThinking() | Signals the start of an agent run (e.g., displaying a "Thinking" placeholder). |
emitProgress(text) | Streams tool/step progress, typically appended to the current output. |
emitResponse(text) | Updates or sets the final response text, replacing prior progress if applicable. |
emitDetail(text) | Posts supplementary info, such as reasoning or debug data, often in threads. |
emitReaction(emoji, id) | Adds a reaction to a specific message. |
emitFile(path, title) | Shares a file or attachment through the surface. |
resolve() | Terminal operation marking the run as successful. |
reject(error) | Terminal operation marking the run as failed with an error message. |
suppress() | Terminal operation that deletes or hides all output (used for [SILENT] replies). |
flush() | Returns a promise that resolves when all pending platform operations are finished. |
Sources: src/surface/types.ts:2-34
Architecture and Data Flow
The Surface Abstraction Layer acts as the bridge between the AgentSession (the core logic) and the external messaging client. When an agent run is triggered, a specific implementation (e.g., SlackSurface) is instantiated and passed to the runner.
Component Relationship
The following diagram illustrates how the Surface relates to the Agent and the platform clients:
Execution Flow
- Initialization:
main.tscreates aSlackSurfaceorLinearSurfacefor a new event. - Streaming: During
runner.run(), thecreateEventHandlertranslatesAgentSessionEventtypes into surface calls. - Serialization: In
SlackSurface, updates are enqueued in anupdateChainpromise to prevent race conditions during message edits. - Finalization: Once the agent completes,
main.tscallsresolve()(orreject()) and thenflush()to ensure all platform APIs have received the final state.
Sources: src/main.ts:121-158, src/surface/slack.ts:98-102, src/agent/events.ts:96-180
Platform Implementations
SlackSurface
The Slack implementation focuses on an "edit-in-place" model to keep channel noise low. It maintains a single primary message that is updated as progress occurs.
- Update Chain: Uses
enqueueUpdateto serialize calls to the Slack API, ensuring thatupdateMessagecalls happen in the correct order. - Footer Logic: Automatically computes a footer containing the step count and cost. During the run, it shows "streaming"; after
resolve(), it shows the actual dollar cost. - Truncation: Enforces a
MAX_MESSAGE_LENGTH(35,000 characters) to comply with Slack limits. - Markdown Conversion: Includes
mdToMrkdwnto convert standard markdown (**bold**) to Slack's specific formatting (*bold*).
Sources: src/surface/slack.ts:9-30, src/surface/slack.ts:65-92, src/surface/slack.ts:200-210
LinearSurface
The Linear implementation differs because Linear's activity log is append-only.
- Thoughts vs. Responses: Progress updates (
emitProgress) are posted as ephemeral "thoughts" in the issue activity. - Buffering: The final response is buffered and only posted to the issue once
resolve()is called. - No Deletion: Unlike Slack,
suppress()cannot delete messages already sent to the timeline; it primarily prevents the final buffered response from being posted.
Sources: src/surface/linear.ts:11-66
Event Integration
The createEventHandler in src/agent/events.ts bridges the core agent events to the surface.
Sources: src/agent/events.ts:101-155
Lifecycle and Safety
The layer implements a strict lifecycle to ensure the UI does not hang in a "thinking" state.
- Terminal Ops:
resolve(),reject(), andsuppress()are idempotent. Once one is called, the surface is "resolved," and subsequent terminal calls are ignored. - Dispose: The
dispose()method acts as a safety net. If a run ends unexpectedly without a terminal call,dispose()will triggerreject("Run ended unexpectedly").
Sources: src/surface/slack.ts:175-225, test/run-context.test.ts:213-228
Summary
The Surface Abstraction Layer is a critical infrastructure component in pi-digby that decouples the agent's reasoning from the delivery mechanism. By providing a unified AgentSurface API, the system supports rich interaction features like live-streaming progress, cost-tracking footers, and threaded debugging while maintaining compatibility with both Slack's mutable message model and Linear's append-only activity feed.