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
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:
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.
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.
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
startedat 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. SDK package publication is a separate gate. The normative public stream contract defines session cancellation and frame behavior in full.