> Canonical guide: https://developers.foxlight.ai/build/calling-and-streaming/
> Contract snapshots: Skulk 2.0.0 (b0af39c79b6b7102b2478062904f1d7cc8619975); SDK 0.4.0 (31bb090b8f64689f87514e48385e22b6ad94a6c2). Check the installed runtime when versions differ.

# Calling and _streaming_

In-process extensions and protocol-4 managed providers support unary, server streaming, client streaming and bidirectional calls. Public callers use `ExtensionContext`; the managed SDK serves those calls from an isolated child.

## Negotiate a unary call

```python
from skulk.extensions import descriptor_revision

descriptors = await context.describe_node(node_id)
echo = next(item for item in descriptors if item.qualified_id == "echo@1.0.0")
result = await context.call_capability(
    node_id, echo.id, echo.version, descriptor_revision(echo),
    {"text": "Hello from the fabric."},
)
if result.ok:
    print(result.result)
else:
    print(result.error.code, result.error.message)
```

Choose a provider and descriptor before this snippet. Discovery may return no match; handle that as unavailable in your application. A valid REST call envelope returns HTTP 200 with a typed success or failure; malformed envelopes return 422. Inspect `error.code` instead of parsing prose.

## Open a stream

Use the exact descriptor revision and a payload matching its `input_schema`:

```python
session = await context.stream_capability(
    node_id, descriptor.id, descriptor.version,
    descriptor_revision(descriptor), payload,
)
if not session.open_result.ok:
    print(session.open_result.error.code)
else:
    async for frame in session.frames:
        if frame.kind == "chunk":
            consume(frame.payload, frame.media)
        elif frame.is_terminal:
            print(frame.kind)
```

`consume` belongs to your application. Handle a failed or cancelled terminal as failure; the arrival of some chunks is not a successful completion.

:::tip The HTTP route admits the call
`POST /v1/capabilities/stream` returns a typed admission result. Its HTTP response does **not** carry the generated media. Output uses the node-addressed provider data plane. An ordinary browser should use an applicable REST/media or WebSocket adapter, or an application service built on the extension caller contract.
:::

## Send input without closing output

Client-streaming and bidirectional sessions provide `session.input`. Server-streaming sessions have no input sink.

```python
if session.input is not None:
    await session.input.send_chunk(metadata, media=attachment)
    await session.input.complete()
```

Send only schema-valid metadata and supported media attachments. `complete()` half-closes caller input; continue consuming provider output until its terminal. For bidirectional work, run input production and output consumption concurrently with bounded queues. Do not collect every input frame before reading output.

## Lifecycle and failure rules

- One deadline covers admission, work and cleanup. A new frame does not reset it.
- Skulk owns `started` at sequence zero. Providers emit ordered output beginning at one, one terminal, then return.
- Cancellation, disconnect, bad framing, invalid schemas and pressure affect that call. Do not replay effects automatically after losing a response.
- Raw inline media is bounded to 1 MiB per frame and stays out of State and event logs.
- Managed children share one active invocation slot across all modes; cleanup completes before successful terminal delivery.

For managed child implementation, upgrade order and local protocol bounds, read [managed streaming](https://developers.foxlight.ai/build/sdk/streams/). SDK package publication is a separate gate. The [normative public stream contract](https://docs.foxlight.ai/skulk/next/extensions/#streaming-a-capability) defines session cancellation and frame behavior in full.
