# Sibling Scopes
URL: /tap/docs/store/sibling-scopes

Scopes that reference each other at the same level.

> For AI agents: a documentation index is available at [llms.txt](/llms.txt). Use `.md` for canonical markdown pages; `.mdx` is kept as a backwards-compatible alias on supported URL paths.

Child scopes point down — from parent to child. But sometimes scopes at the same level need to interact. A `tools` scope might need to register definitions into a `modelContext` scope. A `composer` scope might need to read from a `thread` scope.

These are **sibling scopes** — scopes that sit at the same level in the provider tree and reference each other.

## The problem

When you pass multiple scopes to `useAui`, they're all created as siblings:

```
const aui = useAui({
  thread: ThreadResource(),
  tools: ToolsResource(),
  modelContext: ModelContextResource(),
});
```

But what if `ToolsResource` needs to call methods on the `modelContext` scope? At definition time, the `tools` resource doesn't have access to its siblings.

## useAssistantClientRef

`useAssistantClientRef()` gives a resource access to the store being built. It returns a ref whose `.current` property points to the `AssistantClient` once all scopes are mounted:

```
import { resource } from "@assistant-ui/tap";
import { useAssistantClientRef } from "@assistant-ui/store";
import type { ClientOutput } from "@assistant-ui/store";
import { useEffect } from "react";

const useToolsResource = (): ClientOutput<"tools"> => {
  const clientRef = useAssistantClientRef();

  useEffect(() => {
    // access a sibling scope
    const unsub = clientRef.current!.modelContext().register({
      getModelContext: () => ({ tools: myTools }),
    });
    return () => unsub();
  }, [clientRef]);

  return {
    getState: () => ({ ... }),
  };
};

const ToolsResource = resource(useToolsResource);
```

Use `useAssistantClientRef` in effects, not during the resource body. The ref is populated after all sibling scopes are mounted, so it's only safe to access in `useEffect` or in methods called later.

## attachTransformScopes

`useAssistantClientRef` lets a scope talk to siblings at runtime. But what if the sibling scope doesn't exist yet? If a user only provides `thread: ThreadResource()`, there's no `modelContext` to talk to.

`attachTransformScopes` solves this by letting a resource declare: "when I'm mounted, make sure these sibling scopes also exist."

```
import { attachTransformScopes } from "@assistant-ui/store";

attachTransformScopes(ToolsResource, (scopes, parent) => {
  // ensure modelContext exists as a sibling
  if (!scopes.modelContext && parent.modelContext.source === null) {
    scopes.modelContext = ModelContextResource();
  }
});
```

The transform function receives:

- `scopes` — the current scopes config being built (mutate it directly)
- `parent` — the parent `AssistantClient` (from the `AuiProvider` above)

It mutates the `scopes` object directly, adding scopes that should be created alongside.

### Checking parent.source

Before adding a scope, check `parent.scopeName.source`:

- `null` — the scope isn't provided by any ancestor. Safe to add it here.
- `"root"` or a scope name — an ancestor already provides this scope. Don't duplicate it.

```
attachTransformScopes(MyResource, (scopes, parent) => {
  // only add if not already in scopes AND not provided by a parent
  if (!scopes.tools && parent.tools.source === null) {
    scopes.tools = ToolsResource();
  }
});
```

### Adding derived scopes

Transforms can also add `Derived` scopes:

```
attachTransformScopes(ThreadResource, (scopes) => {
  scopes.composer ??= Derived({
    source: "thread",
    query: {},
    get: (aui) => aui.thread().composer(),
  });
});
```

This ensures that when a user provides a `thread` scope, the `composer` scope is automatically wired up as a child of that thread — without the user having to declare it.

### Iterative application

Transforms are applied iteratively. When a transform adds a new root scope, Store checks if that new scope also has a transform attached — and runs it too. This continues until no new scopes are added.

For example:

1. User provides `thread: ThreadResource()`
2. `ThreadResource`'s transform adds `tools: ToolsResource()` and `modelContext: ModelContextResource()`
3. `ToolsResource`'s transform runs — sees `modelContext` already exists, does nothing
4. No more new scopes → done

> [!warn]
>
> Each resource can only have one transform attached. Calling `attachTransformScopes` twice on the same resource throws an error.

## When to use which

| Pattern                 | Use when                                                                        |
| ----------------------- | ------------------------------------------------------------------------------- |
| `useAssistantClientRef` | A scope needs to call methods on a sibling at runtime (effects, event handlers) |
| `attachTransformScopes` | A scope needs to guarantee a sibling exists before mounting                     |

In practice, they're often used together: `attachTransformScopes` ensures the sibling scope is created, then `useAssistantClientRef` accesses it at runtime.