# RFD 065: Typed Resource Model for Attachments

- **Status**: Discussion
- **Category**: Design
- **Authors**: Jean Mertz <git@jeanmertz.com>
- **Date**: 2026-03-20
- **Required by**: [RFD 058], [RFD 066], [RFD 067]

## Summary

This RFD replaces the opaque `Attachment` type with a typed `Resource` model
aligned with MCP's resource specification and [RFD 058]'s content block schema.
Resources carry canonical URIs, MIME types, and MCP-standard annotations.
Resource content moves from the system prompt into the `ChatRequest` at the turn
where the resource is attached.
Resources are snapshots — content is captured at attachment time, only updated
when explicitly requested.
A built-in `refresh_resource` tool lets the LLM request fresh content through
the attachment handler when needed.

## Motivation

[RFD 058] introduces typed content blocks for tool responses.
Tools return `resource` blocks with URIs, MIME types, and structured content.
This gives JP the metadata it needs for resource-level features like formatting
control and deduplication ([RFD 067]).

Attachments don't expose this metadata.
The `Attachment` type has a `source: String` (a human-readable label like
`"src/main.rs"`), optional `description`, and `AttachmentContent` (text or
binary).
Attachment handlers receive canonical URLs via `AttachmentConfig`, but the
resolved `Attachment` type has no structured URI field — only a `source:
String` whose content varies by handler.
The file handler sets `source` to the relative path (`"src/main.rs"`), the HTTP
and MCP handlers set it to the full URL, and the command handler sets it to the
command string (`"git diff --cached"`).
Without a guaranteed canonical URI on the resolved type, JP cannot reliably
match an attachment against a tool response that returns the same resource.
This creates three problems:

### No shared identity space

[RFD 067] needs to match resources across tool calls and attachments.
A tool returns `resource { uri: "file:///project/src/main.rs" }`; the file
attachment has `source: "src/main.rs"`.
These refer to the same file, but JP has no reliable way to connect them — the
`source` field is a display label with handler-specific formatting, not a
canonical URI.
Without a shared identity model, deduplication cannot work across the
tool/attachment boundary.

### Wrong insertion point

Attachments are currently resolved at the start of each `jp query` invocation
and prepended to the first user message, regardless of when they were added to
the conversation.
A resource attached at turn 30 appears at turn 0.
This has three consequences:

1. **Cache invalidation.** When content changes, inserting the updated content
   at position 0 invalidates the cache for the entire conversation history.
   If the content is unchanged the cache is preserved (same tokens at the same
   position), but any change — even a single byte — invalidates everything
   after that point.

2. **Semantic mismatch.** The user's message at turn 30 ("look at this file")
   refers to content that appears 30 turns earlier.
   The LLM must connect a recent instruction with distant context.

3. **Inconsistency with tool calls.** Tool results appear at the turn where the
   tool was called.
   Attachments appear at turn 0.
   Two mechanisms for delivering resources to the LLM follow different rules.

### Dynamic resources force unnecessary invalidation

For resources with inherently dynamic content (command output with timestamps,
`git status`, web pages), re-resolution at turn 0 produces different content on
every invocation, guaranteeing cache invalidation every turn — even when the
LLM doesn't need the fresh data.
The current model gives JP no way to distinguish between "content changed
because the user edited the file" and "content changed because the command
output includes a timestamp."

### What happens if we do nothing

Without a shared resource model, [RFD 067] cannot deduplicate across tool calls
and attachments — the primary use case described in its Motivation section.
Tool responses gain typed metadata (via [RFD 058]) while attachments remain
opaque, creating a permanent asymmetry in the resource pipeline.

## Design

### MCP-compatible superset

Any resource or tool-related type JP defines must be constructible from only the
fields that MCP provides.
JP-specific extensions (fields beyond what MCP defines) must have sane defaults
(`Option<T>`, `Vec<T>`, `false`, etc.) so that MCP-sourced data requires no
synthesis or fabrication.
Local tools and attachment handlers may populate the extended fields for richer
functionality.

This ensures MCP tool responses pass through without lossy conversion, while
local tools and handlers can provide additional metadata when it is available.

### Overview

The `Attachment` type is replaced by a `Resource` type aligned with MCP's
resource specification: canonical URI, MIME type, annotations, and typed
content.
Resources are stored as a field on `ChatRequest`, placing them at the turn where
they were attached.
The `conversation.attachments` config field continues to declare what should be
attached.
A built-in `refresh_resource` tool lets the LLM request fresh content for any
attached resource through its original handler.

### The `Resource` type

Handlers return `Vec<Resource>` instead of `Vec<Attachment>`:

```rust
/// A resolved resource with identity metadata.
///
/// Returned by attachment handlers after resolving a URL. Carries both
/// the content (for delivery to the LLM) and metadata (for display and
/// resource management).
pub struct Resource {
    /// Canonical URI identifying this resource.
    ///
    /// The handler produces this from the attachment URL. File handlers
    /// resolve relative paths against the workspace root to produce
    /// absolute `file:///...` URIs. HTTP handlers normalize the URL.
    /// The canonical URI is the identity key for deduplication.
    pub uri: String,

    /// The resource content.
    pub content: ResourceContent,

    /// MIME type of the content (e.g., "text/rust", "image/png").
    pub mime_type: Option<String>,

    /// Optional MCP annotations (audience, priority, lastModified).
    pub annotations: Option<Annotations>,

    // JP extensions (all defaultable, absent for MCP-sourced resources)

    /// Short name of the resource.
    ///
    /// Populated by attachment handlers for display in `jp attachment ls`
    /// and as the document title in providers that support it. Examples:
    /// - File handler: relative path from workspace root (`"src/main.rs"`)
    /// - HTTP handler: the URL (`"https://example.com/doc"`)
    /// - Bear handler: the note title
    /// - Command handler: the command string (`"git diff --cached"`)
    ///
    /// `None` when the resource originates from an MCP tool response,
    /// since MCP's `EmbeddedResource` (the content delivery type) does
    /// not carry a `name` field.
    pub name: Option<String>,

    /// Optional human-readable title for display purposes.
    ///
    /// When absent, providers fall back to `name`, then to `uri`.
    pub title: Option<String>,

    /// Optional description of the resource.
    pub description: Option<String>,

    /// Optional pre-formatted content for LLM delivery.
    ///
    /// When present, JP uses this for the LLM instead of formatting
    /// from `content` + `mime_type`. The raw `content` is still used
    /// for checksums and identity matching. See [RFD 058] for details.
    ///
    /// Attachment handlers and local tools may set this for custom
    /// presentation. When absent, JP formats the resource from
    /// `content` + `mime_type`. MCP tool responses never populate
    /// this field (MCP does not define it).
    pub formatted: Option<String>,
}

/// Resource content, either UTF-8 text or binary data.
///
/// Matches MCP's resource content model.
pub enum ResourceContent {
    /// UTF-8 text content (source code, markdown, etc.)
    Text(String),
    /// Binary content (images, PDFs, etc.)
    Blob(Vec<u8>),
}
```

`Resource` replaces `Attachment` as the type that handlers return and that the
resolution pipeline consumes.
The `name` field replaces `Attachment.source`.
The `uri` field exposes the canonical identifier that was previously available
at the config layer but discarded during resolution.

The fields are grouped into two tiers following the MCP-compatible superset
principle:

- **MCP core:** `uri`, `content`, `mime_type`, `annotations` — these fields are
  sufficient to construct a `Resource` from any MCP tool response or resource
  read.
- **JP extensions:** `name`, `title`, `description`, `formatted` — all
  `Option<T>`, defaulting to `None` for MCP-sourced resources.
  Attachment handlers and local tools populate these when richer metadata is
  available.

This means `From<rmcp::ResourceContents>` and `From<rmcp::EmbeddedResource>`
conversions require no fabrication — all JP extension fields default to `None`.

### Shared across all resource paths

`Resource` is the single canonical type for resource content across JP.
`ChatRequest.resources` carries `Vec<Resource>` for attachments, and [RFD 058]'s
`ContentBlock::Resource` wraps `Resource` for tool responses.
The same type flows through both paths.

This is possible because the JP extension fields (`name`, `title`,
`description`, `formatted`) are all `Option<T>`.
Attachment handlers populate `name` and possibly `title`/`description`.
Local tools may set `formatted`.
MCP tool responses leave all four as `None`.
A single type serves all sources without requiring separate stripped-down
variants for different contexts.

[RFD 067]'s content tagging works identically regardless of source — same URI
space, same checksum computation (always against the raw `content`, never
against `formatted`), same matching algorithm.

When serialized to JSON, the MCP-core fields follow MCP's resource content
structure:

```json
{
  "uri": "file:///project/src/main.rs",
  "mimeType": "text/x-rust",
  "text": "fn main() {}",
  "annotations": {
    "lastModified": "2026-03-20T10:00:00Z"
  }
}
```

JP extension fields are omitted when `None` (via
`#[serde(skip_serializing_if)]`).
Providers that need display metadata (e.g., Anthropic's `Document.title`) read
`name` or `title` directly from the `Resource`.

### Resources on `ChatRequest`

`ChatRequest` gains a `resources` field:

```rust
pub struct ChatRequest {
    /// The user's query or message content.
    pub content: String,

    /// Optional JSON schema constraining the assistant's response format.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub schema: Option<Map<String, Value>>,

    /// Resources attached to this message.
    ///
    /// Each resource is a snapshot of its content at the time this
    /// ChatRequest was created. Resources are never re-resolved or
    /// updated retroactively.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub resources: Vec<Resource>,
}
```

Resources appear inline in the user message at the turn where they were
attached, the same pattern as `schema`.
Old conversations without a `resources` field deserialize with an empty vec.

Attachment handlers populate the JP extension fields (`name`, `title`,
`description`) and may optionally set `formatted` for custom presentation.
In all cases, `content` carries the raw, unformatted data — checksums and
identity matching ([RFD 067]) always operate on `content`, never on `formatted`.
MCP-sourced resources (e.g., from `refresh_resource` via an MCP handler) carry
only the MCP-core fields.

### Resources are snapshots

A resource attached at turn N captures the content as it exists at turn N. The
content is frozen in the `ChatRequest` event and never changes.
This is the same model as tool call results — `fs_read_file` at turn 3 shows
the file as it was at turn 3, regardless of subsequent edits.

This means:

- **History is immutable.** No retroactive content changes, no cache
  invalidation from re-resolution.
- **Semantic correctness.** The user's message at turn N refers to the content
  as it was at turn N. If the user says "review this file" at turn 5, the
  resource content matches what the user saw at turn 5.
- **Consistency with tools.** Attachments and tool results follow the same
  snapshot pattern.
- **Dynamic resources handled correctly.** A `cmd://git status` snapshot at turn
  3 captures the status at turn 3.
  It doesn't re-run on every subsequent turn, which would produce different
  output and bust the cache.

### Resolution logic

At each `jp query` invocation, JP determines which resources to resolve:

**New conversation (`jp query --new`):** All attachments declared in
`conversation.attachments` are resolved and included in turn 0's `ChatRequest`.

**Explicit `--attach` on a follow-up:** The resource is resolved fresh and
included in the current turn's `ChatRequest`.
The `ConfigDelta` is emitted as before (so `jp attachment ls` works, forks carry
the declaration forward).
If the URI already appears in the conversation history, the fresh content is
still included — the user explicitly asked to attach it, so they expect the
latest content.

**Follow-up with no new attachments:** Config-declared attachments are already
in the conversation history from their initial resolution.
Nothing to resolve.

This means:

- **New conversation:** All config-declared attachments resolved at turn 0.
- **`--attach bar.rs` at turn 5:** bar.rs resolved fresh, included at turn 5.
  ConfigDelta emitted.
- **`--attach foo.rs` at turn 10 (foo.rs was attached at turn 0):** foo.rs
  resolved fresh at turn 10.
  The LLM sees both the turn-0 snapshot and the turn-10 snapshot.
  If the content hasn't changed, [RFD 067] dedup handles it.
  If it has changed, the LLM has both versions.
- **Turn 6 with no `--attach`:** Config still declares bar.rs, but it's already
  in the stream at turn 5.
  Skip.
- **`jp conversation fork`:** New conversation inherits config (including
  attachment declarations).
  Resolution finds no matching URIs in the forked stream and resolves them fresh
  for the fork's first turn.

### Config and `--attach`

`conversation.attachments` remains the declaration mechanism.
`--attach` on a follow-up query continues to write a `ConfigDelta` that adds the
attachment declaration to the config.
This keeps the declaration persistent:

- `jp attachment ls` reads from the accumulated config.
  Works as before.
- `jp attachment rm` removes from the config.
  Content already in the stream is unaffected (history is immutable).
- `jp conversation fork` carries attachment declarations forward via the config.

The change is in *when and where* content is resolved — at the turn where the
resource is first attached (or explicitly re-attached), not at turn 0 on every
invocation.

### Refreshing resources

Resources are snapshots, but the LLM sometimes needs current content.
The refresh path must go through the attachment handler because the handler is
the only component that knows how to produce content for its scheme.
Tools like `fs_read_file` are configured tools that may not be enabled, and many
attachment schemes (Bear notes, command output, MCP resources) have no
corresponding tool.

JP ships a built-in `refresh_resource` tool:

```
refresh_resource(uri: "file:///project/src/main.rs")
```

When called:

1. JP parses the URI scheme.
2. Finds the registered handler for that scheme.
3. Calls the handler's resolve method for that specific URI.
4. Returns the content as a `resource` content block (per [RFD 058]).

The result is a normal tool call response in the conversation stream.
[RFD 067] dedup applies: if the content hasn't changed since the original
attachment (or a previous refresh), the response is replaced with a reference.
If the content has changed, the full content is delivered.

#### Superseded resource handling

When `refresh_resource` delivers new content for a URI that already exists in
the stream, the earlier snapshot becomes stale.
The stale content remains in the stored conversation (history is immutable), but
JP can choose to omit it when building the LLM request — similar to how [RFD
036] compaction drops old tool responses.

Whether to omit a superseded resource involves trade-offs between token savings
and cache invalidation.
Several heuristics are relevant:

- **Token size threshold.** Small resources (\< N tokens) may cost less to keep
  than the cache invalidation caused by removing them.
- **Cache position.** Resources within the provider's cache lookback window
  (e.g., Anthropic's ~30 turns) are expensive to remove.
  Resources outside the window can be removed without cache cost.
- **Content delta.** Nearly identical old/new content saves few tokens on
  removal.
  Completely different content benefits from removal to avoid confusing the LLM
  with contradictory versions.
- **Turn distance.** Recent resources are likely in the LLM's active attention;
  distant resources may not be.

The specific policy (defaults, configuration, and which heuristics to implement)
is deferred to [RFD 067] and [RFD 036], which own the deduplication and
compaction logic respectively.

#### Handler trait change

The handler trait needs to resolve a single resource by URI.
For the current trait, this means adding a method:

```rust
async fn resolve_one(
    &self,
    uri: &Url,
    cwd: &Utf8Path,
    mcp: Client,
) -> Result<Resource>;
```

For the [RFD 015] trait (which already takes `&[Url]`), passing a single-element
slice to `resolve()` is sufficient.
The handler returns one `Resource` for the one URI.

> [!TIP]
> [RFD 015] currently defines the handler return type as `Vec<Attachment>`.
> It will land first without the `Resource` change.
> Once this RFD is accepted, the handler return type should be updated to
> `Vec<Resource>` as a follow-up.

#### When the LLM refreshes

The LLM decides when to refresh.
JP does not automatically re-resolve attachments or rewrite history.
The LLM calls `refresh_resource` when it has reason to believe the content is
stale — for example, after modifying a file, or when the user mentions
something has changed.

Future extensions can inform the LLM about stale resources without automatic
refresh:

- **[RFD 011] notifications.** JP could check declared attachments at the start
  of each turn (by re-resolving and comparing checksums) and emit a system
  notification if content has changed: "The attached resource src/main.rs has
  changed since turn 5."
  The LLM decides whether to call `refresh_resource`.
- **Config-level refresh policy.** An optional `refresh` field on attachment
  declarations (`manual`, `notify`) could control whether JP checks for changes.
  Default is `manual` (no checking).
  Not designed in this RFD.

### Provider migration

Each provider currently handles attachments in its `build_request` method,
receiving them as a separate `ThreadParts::attachments` vec and prepending them
to the first user message.
With resources on `ChatRequest`, providers process them inline as part of the
event stream.

**Anthropic:** The `convert_event` function currently converts a `ChatRequest`
into a single `MessageContent::Text` block.
With resources, it produces a `MessageContent::Text` for the user's query
followed by one `MessageContent::Document` per resource block.
Text resources become `DocumentSource::Text`; binary resources become
`DocumentSource::Base64`.
The `Resource.name` (preserved in event metadata) maps to the document `title`.
Cache breakpoint logic applies to the document blocks at their actual position
rather than always at message 0.

**Google/Gemini:** Text resources are serialized to XML (same as today's
`text_attachments_to_xml`, but inline at the correct turn).
Binary resources become `ContentPart::InlineData`.

**OpenAI:** Text resources become `ContentItem::Text` (XML-wrapped).
Binary resources map to `ContentItem::Image` or `ContentItem::File` depending on
MIME type.

**OpenRouter:** Same as OpenAI, adapted for the OpenRouter content block types.

The `Thread` and `ThreadParts` types lose the `attachments` field.
The `text_attachments_to_xml` helper may remain as a utility for providers that
render text resources as XML, but it operates on `&[Resource]` from the
`ChatRequest` rather than on a separate `Vec<Attachment>`.

### Handler return type

Handlers return `Vec<Resource>` instead of `Vec<Attachment>`.
The handler is responsible for producing the canonical URI because only the
handler knows the scheme-specific canonicalization rules:

- **File handler:** Resolves relative paths against the workspace root to
  produce `file:///absolute/path`.
  Normalizes `.` and `..` segments.
  Infers MIME type from file extension.
- **HTTP handler:** Normalizes the URL (lowercase scheme and host, resolve
  path).
  Uses `Content-Type` header for MIME type.
- **MCP handler:** Uses the MCP resource URI as-is (already canonical per the
  MCP spec).
  Uses the resource's declared MIME type.
- **Command handler:** Uses the original `cmd://...` URL as the canonical URI.

The `name` field replaces what is currently `Attachment.source`.
Each handler populates it with the most human-readable identifier: relative path
for files, URL for HTTP, note title for Bear, command string for cmd.
Since `name` is `Option<String>`, handlers are expected to always set it, but
the type does not enforce this — MCP tool responses legitimately omit it.
The `annotations` field carries MCP-standard metadata; handlers that support it
(MCP handler, HTTP handler via `Last-Modified`) populate it.

## Drawbacks

**Breaking change to the handler trait.** All handlers must be updated to return
`Resource` instead of `Attachment`.
Since handlers are internal (and [RFD 015] is already redesigning the trait),
this is acceptable.
[RFD 015] will land first with `Vec<Attachment>`; the migration to
`Vec<Resource>` is a follow-up after this RFD is accepted.

**Breaking change to provider implementations.** Every provider's
`build_request` method changes — attachments no longer arrive as a separate vec
but as `Resource` fields on `ChatRequest` events.
This is a significant refactor touching all providers.

**Snapshot semantics may surprise users.** Users accustomed to attachments
always showing the latest content will find that attached files are frozen at
attachment time.
The `refresh_resource` tool and explicit `--attach` mitigate this, but the
behavioral change needs documentation.

**Re-attaching an existing URI creates duplicate content.** When a user
`--attach`es a resource that's already in the stream, the conversation contains
two snapshots.
If the content hasn't changed, this wastes tokens (mitigated by [RFD 067]
dedup).
If the content has changed, both versions remain — useful for the LLM to see
the evolution, but potentially confusing.

**Canonical URI correctness is critical.** If a handler produces an incorrect
canonical URI, deduplication fails silently (either missing valid matches or
producing false positives).
Handlers must be tested carefully for URI canonicalization edge cases (symlinks,
case sensitivity, trailing slashes).

## Alternatives

### Keep attachments at turn 0, add metadata

Leave the insertion point unchanged (always first user message) but add URI and
MIME type to the `Attachment` type.
This enables deduplication without the provider refactor.

**Rejected because:** It perpetuates the cache invalidation problem on content
changes.
Adding a resource at turn 30 still modifies turn 0, invalidating everything
after that point.
The insertion-point fix is the primary motivator for this RFD, not just the
metadata.

### Separate `AttachmentResolved` event type

Add a new `EventKind::AttachmentResolved` variant to record when attachment
content is delivered, instead of embedding resources in `ChatRequest`.

**Rejected because:** `EventKind` follows a request/response pattern.
`AttachmentResolved` has no request counterpart and would be a metadata event
that doesn't fit the model.
It would also need special handling in `is_provider_visible()` and in every
provider's event conversion logic.
Embedding resources in `ChatRequest` is simpler and aligns with how providers
already process user messages.

### Auto-refresh changed resources

Automatically re-resolve attachments at each turn and re-insert content when it
changes.
The fresh content would appear in the current `ChatRequest` as an updated
resource block.

**Rejected because:** It forces cache invalidation for dynamic resources on
every turn, even when the LLM doesn't need fresh data.
Resources like command output with timestamps would bust the cache
unconditionally.
The snapshot model with LLM-driven refresh via `refresh_resource` gives the LLM
control over when to pay the refresh cost.

### Use a JP-specific resource type instead of MCP alignment

Define a custom resource model optimized for JP's needs without regard for MCP
compatibility.

**Rejected because:** MCP has already standardized the right primitives for
resource identity (URIs, MIME types, annotations).
Adopting MCP's model means MCP tool responses and attachment resources share the
same type, enabling [RFD 067] dedup across both without translation.
A custom model would require a mapping layer and risk subtle mismatches.

## Non-Goals

- **Automatic resource refresh.** JP does not re-resolve attached resources or
  check for staleness.
  The LLM requests fresh content when it needs it via `refresh_resource`.
  Automatic staleness detection via [RFD 011] notifications is a future
  extension.

- **Resource deduplication logic.** This RFD establishes the shared identity
  model (canonical URIs, MIME types, `Resource`) that makes deduplication
  possible.
  The deduplication algorithm itself is defined in [RFD 067].

- **Handler trait redesign.** [RFD 015] redesigns the handler trait.
  This RFD changes the return type from `Attachment` to `Resource` and adds
  single-resource resolution, but does not redesign the trait structure itself.

- **Attachment config syntax changes.** The `conversation.attachments` config
  field and `--attach` CLI flag continue to work as before.
  The change is in how declarations are resolved, not in how they are declared.

- **Superseded resource removal policy.** This RFD documents the heuristics for
  when to omit superseded resources from the LLM request.
  The specific defaults and configuration are implementation details deferred to
  [RFD 067] and [RFD 036].

## Risks and Open Questions

### URI canonicalization edge cases

File URIs need careful handling: symlinks, case-insensitive filesystems, Unicode
normalization, Windows path separators.
The file handler needs thorough testing for these cases.
A malformed canonical URI silently breaks deduplication.

### Inline content bloats the conversation file

Resource content is stored inline in `events.json` as part of the `ChatRequest`
or `ToolCallResponse` event.
A `--attach ./src` on a modest project can resolve to hundreds of files, and
binary resources inflate further with base64 encoding.
This causes disk bloat, increased parse time on every `jp query` invocation, and
search pollution in developer tools.

This is a blocking concern.
A future RFD can design a system that externalizes all content payloads from
`events.json`.
The `Resource` and `ContentBlock` types in the domain model are not affected —
externalization is a serialization concern handled by the persistence layer.
This RFD proceeds with inline content as the initial storage format.

### Interaction with conversation compaction

[RFD 036] describes conversation compaction.
When compaction operates on turns that contain attached resources, it should
preserve the `Resource` metadata (URI, MIME type) even if the content is
dropped, so that deduplication can still identify the resource.
The specifics depend on the compaction design.

### Migration of existing conversations

Existing conversations have no `resources` field on `ChatRequest`.
The `#[serde(default)]` annotation handles deserialization.
However, existing conversations also have attachments that were resolved via the
old model (prepended to turn 0).
After this change, those attachments won't be in any `ChatRequest.resources`.
If the config still declares them, the resolution logic will resolve them as new
resources on the next turn — even though the content was previously delivered
(invisibly, at turn 0).
This is a one-time duplication that's acceptable for the migration period.

### `refresh_resource` tool visibility

The `refresh_resource` tool should be available whenever the conversation has
attached resources.
If no resources are attached, registering the tool wastes a tool definition in
the context window.
JP could conditionally register it based on whether `ChatRequest.resources` is
non-empty anywhere in the stream.

## Implementation Plan

### Phase 1: `Resource` type and `ResourceContent`

Define `Resource` (with canonical URI, MIME type, content, annotations, plus JP
extensions `name`, `title`, `description`, `formatted`) in a shared location.
Align `ResourceContent` with [RFD 058]'s `ResourceContent` type.
Define the `Annotations` type matching MCP's specification (audience, priority,
lastModified).
Implement `From<rmcp::ResourceContents>` and `From<rmcp::EmbeddedResource>`
conversions.

No behavioral changes yet.
Can be merged independently.

### Phase 2: Handler return type migration

Update the handler trait to return `Vec<Resource>` instead of `Vec<Attachment>`.
Update all built-in handlers to produce canonical URIs and MIME types.

If [RFD 015] lands first, this phase is smaller — the trait is already being
rewritten.
If not, this is an additive change to the existing trait.

Depends on Phase 1.
Can be merged independently.

### Phase 3: `ChatRequest.resources` and resolution logic

Add `resources: Vec<Resource>` to `ChatRequest`.
Implement the resolution logic: resolve config-declared attachments on new
conversations, resolve `--attach` resources fresh at the current turn.

Update `build_thread` / `register_attachment` to use the new resolution path
instead of building a separate `Vec<Attachment>`.

Depends on Phase 2.

### Phase 4: Provider migration

Update all providers to read resources from `ChatRequest` events instead of
`ThreadParts::attachments`.
Remove the `attachments` field from `Thread` and `ThreadParts`.
Update or remove `text_attachments_to_xml` as needed.

This is the largest phase.
Each provider can be migrated independently.

Depends on Phase 3.

### Phase 5: `refresh_resource` built-in tool

Implement the `refresh_resource` tool: URI parsing, handler dispatch,
single-resource resolution, resource block response.
Conditionally register the tool when the conversation has attached resources.

Add `resolve_one` to the handler trait (or use single-element `resolve` if [RFD
015] has landed).

Depends on Phase 2 and Phase 3.

### Phase 6: Remove `Attachment` type

Once all consumers have migrated to `Resource`, remove the `Attachment` type,
`AttachmentContent`, and related code from `jp_attachment`.
Rename the crate's public API to use `Resource` terminology.

Depends on all previous phases.

## References

- [MCP Resources Specification (2025-11-25)][MCP Resources] — defines resource
  URIs, content types, annotations, and URI schemes.
  The `Resource` type in this RFD follows MCP's resource content format with JP
  extensions.
- [MCP Tools][MCP Tools] — defines typed content blocks for tool responses.
- [MCP Tasks][MCP Tasks] — defines the task lifecycle model relevant to
  stateful tool support.
- [RFD 058: Typed Content Blocks for Tool Responses][RFD 058] — introduces the
  `ContentBlock` and `ResourceContent` types for tool responses.
  This RFD aligns attachments with the same model via the shared `Resource`
  type.
- [RFD 067: Resource Deduplication for Token Efficiency][RFD 067] — defines
  deduplication logic that depends on the shared identity model (canonical URIs,
  checksums) established by this RFD and RFD 058.
- [RFD 015: Simplified Attachment Handler Trait][RFD 015] — redesigns the
  handler trait to stateless `validate`/`resolve`.
  This RFD changes the return type; RFD 015 changes the trait structure.
  Both can proceed independently.
- [RFD 011: System Notification Queue][RFD 011] — future path for notifying the
  LLM about stale resources without automatic refresh.
- [RFD 036: Conversation Compaction][RFD 036] — relevant for how resource
  metadata interacts with compacted conversations, and for superseded resource
  removal heuristics.

[MCP Resources]: https://modelcontextprotocol.io/specification/2025-11-25/server/resources.md
[MCP Tasks]: https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks.md
[MCP Tools]: https://modelcontextprotocol.io/specification/2025-11-25/server/tools.md
[RFD 011]: 011-system-notification-queue.md
[RFD 015]: 015-simplified-attachment-handler-trait.md
[RFD 036]: 036-conversation-compaction.md
[RFD 058]: 058-typed-content-blocks-for-tool-responses.md
[RFD 066]: 066-content-addressable-blob-store.md
[RFD 067]: 067-resource-deduplication-for-token-efficiency.md
