> ## Documentation Index
> Fetch the complete documentation index at: https://raindrop.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Tracing

> Record spans, tool calls, model calls and sub-agents inside an interaction, in every Raindrop SDK. Pick a language in any code sample and the whole page follows.

Tracing records what happens inside an interaction (multi-step pipelines, model calls, tool calls and sub-agents) so you can see the full execution flow, debug prompt chains and understand the intermediate steps behind a response. Pick your language in any code sample; every sample and language note on this page switches with it.

In Python, turn tracing on first with `tracing_enabled=True` (see [Configuration](https://raindrop.ai/docs/sdk/overview.md#configuration)); the other SDKs record spans by default.

This page builds on [interactions](https://raindrop.ai/docs/sdk/overview.md#track-an-interaction) from the [SDKs](https://raindrop.ai/docs/sdk/overview.md) page, which also covers recording a tool call after it happened ([Tool spans](https://raindrop.ai/docs/sdk/overview.md#tool-spans)).

## Spans

Wrap a unit of work, such as retrieval, a pipeline step or an agent run, in a span. Spans you start from an interaction inherit its user, conversation and event, so Raindrop shows them on that event, and spans can nest to form the run's tree.

```typescript TypeScript icon="https://mintcdn.com/dawn-a6c57108/72ImDkk3v1Xlc68q/images/icons/typescript.svg?fit=max&auto=format&n=72ImDkk3v1Xlc68q&q=85&s=d8ece0bb7b221c02bae30c5db9c578b3" theme={null}
const turn = raindrop.begin({
  eventId: randomUUID(),
  userId: "user-123",
  event: "agent_run",
  input: "Summarize the release notes",
});

const docs = await turn.withSpan({ name: "retrieve_docs" }, async () => {
  return ["notes-1.md", "notes-2.md"]; // LLM calls in here are traced too
});
```

```python Python icon="python" theme={null}
turn = raindrop.begin(
    user_id="user-123",
    event="agent_run",
    input="Summarize the release notes",
)

span = turn.start_span(kind="task", name="retrieve_docs")
span.record_input({"query": "release notes"})
span.record_output(["notes-1.md", "notes-2.md"])
span.end()
```

```go Go icon="golang" theme={null}
turn := client.Begin(ctx, raindrop.BeginOptions{
	UserID: "user-123",
	Event:  "agent_run",
	Input:  "Summarize the release notes",
})

_ = turn.WithSpan(raindrop.SpanOptions{Name: "retrieve_docs"}, func(ctx context.Context, span *raindrop.Span) error {
	// LLM calls or other work here
	return nil
})
```

```rust Rust icon="rust" theme={null}
use raindrop::SpanOptions;

let turn = client
    .begin(BeginOptions {
        user_id: "user-123".into(),
        event: "agent_run".into(),
        input: "Summarize the release notes".into(),
        ..Default::default()
    })
    .await;

let span = turn.start_span(SpanOptions {
    name: "retrieve_docs".into(),
    ..Default::default()
});
// ... LLM calls or other work ...
span.end();
```

```java Java icon="java" theme={null}
Interaction turn = raindrop.begin(BeginOptions.builder()
        .userId("user-123")
        .event("agent_run")
        .input("Summarize the release notes")
        .build());

Span retrieve = turn.startTask("retrieve_docs");
try {
    retrieve.setOutput(List.of("notes-1.md", "notes-2.md"));
} finally {
    retrieve.end();
}
```

```swift Swift icon="swift" theme={null}
let turn = raindrop.begin(BeginOptions(
  userId: "user-123",
  event: "agent_run",
  input: "Summarize the release notes"
))

let docs = await turn.withSpan("retrieve_docs") { _ in
  ["notes-1.md", "notes-2.md"]  // spans started in here nest under retrieve_docs
}
```

> [tab] TypeScript
>
> Use `withSpan` to trace tasks or operations. Any LLM calls within the span are automatically captured:
>
> ```typescript theme={null}
> // Basic span
> const result = await interaction.withSpan(
>   { name: "generate_response" },
>   async () => {
>     return "Generated response";
>   }
> );
>
> // Span with metadata
> const result = await interaction.withSpan(
>   {
>     name: "embedding_generation",
>     properties: { model: "text-embedding-3-large" },
>     inputParameters: ["What is the weather today?"],
>   },
>   async () => {
>     return [0.1, 0.2, 0.3, 0.4];
>   }
> );
> ```
>
> | Parameter | Type | Description |
> | - | - | - |
> | `name` | `string` | Name for identification in traces |
> | `properties` | `Record<string, string>` | Additional metadata |
> | `inputParameters` | `unknown[]` | Input parameters for the task |

> [tab] Python
>
> Python records spans only when tracing is on: initialize with `raindrop.init(os.getenv("RAINDROP_WRITE_KEY"), tracing_enabled=True)` (or `Raindrop(..., tracing_enabled=True)`). Without it, the event is sent but its spans aren't.
>
> When the span lifecycle doesn't fit within a `with` block (e.g., the span starts in one function and ends in another), use `interaction.start_span()` to create a `ManualSpan` with an explicit `.end()` call.
>
> ```python theme={null}
> span = interaction.start_span(kind="tool", name="my_tool")
> ```
>
> **Parameters:**
>
> * `kind` (Literal\["task", "tool"]): The type of span.
> * `name` (str): Name of the span.
> * `version` (Optional\[int]): Version number for the span.
>
> **`ManualSpan` methods:**
>
> * `record_input(data)`: Record input data for the span.
> * `record_output(data)`: Record output data for the span.
> * `set_properties(props)`: Set custom properties on the span.
> * `end(error=None)`: End the span. Pass an exception to mark it as failed.
>
> **`ManualSpan` properties:**
>
> * `event_id`: The interaction's event\_id.
>
> The span automatically inherits association properties (`event_id`, `user_id`, `event`, `convo_id`) from the interaction.
>
> **Example:**
>
> ```python theme={null}
> import os
> import raindrop.analytics as raindrop
>
> raindrop.init(os.getenv("RAINDROP_WRITE_KEY"), tracing_enabled=True)
>
> @raindrop.interaction("my_workflow")
> def main():
>     interaction = raindrop.begin(user_id="user123", event="process_data", input="...")
>
>     # Start a span
>     span = interaction.start_span(kind="tool", name="external_api")
>     span.record_input({"query": "example"})
>
>     try:
>         result = call_external_api()
>         span.record_output(result)
>         span.end()
>     except Exception as e:
>         span.end(error=e)
>
>     interaction.finish(output="Done")
>     raindrop.shutdown()
> ```
>
> For block-scoped spans, use the `task_span` context manager; see [Tool calls](#tool-calls).

> [tab] Go
>
> Use `WithSpan` to trace tasks or operations:
>
> ```go theme={null}
> // Basic span
> err := interaction.WithSpan(
> 	raindrop.SpanOptions{Name: "generate_response"},
> 	func(ctx context.Context, span *raindrop.Span) error {
> 		// LLM calls or other work here
> 		return nil
> 	},
> )
>
> // Span with metadata
> err := interaction.WithSpan(
> 	raindrop.SpanOptions{
> 		Name:       "embedding_generation",
> 		Properties: map[string]any{"model": "text-embedding-3-large"},
> 	},
> 	func(ctx context.Context, span *raindrop.Span) error {
> 		if span != nil {
> 			span.SetAttributes(raindrop.StringAttr("ai.model.id", "text-embedding-3-large"))
> 		}
> 		return nil
> 	},
> )
> ```
>
> | Parameter | Type | Description |
> | - | - | - |
> | `Name` | `string` | Name for identification in traces |
> | `Properties` | `map[string]any` | Additional metadata |
> | `Attributes` | `[]Attribute` | OTLP attributes |
>
> **Standalone Tracer**
>
> Use `Tracer()` for batch jobs or non-conversation work where you still want spans and tool traces:
>
> ```go theme={null}
> tracer := client.Tracer(map[string]any{"job_id": "batch-123"})
>
> _ = tracer.WithSpan(raindrop.SpanOptions{
> 	Name:       "offline_enrichment",
> 	Properties: map[string]any{"step": "embed"},
> }, func(ctx context.Context, span *raindrop.Span) error {
> 	if span != nil {
> 		span.SetAttributes(raindrop.StringAttr("job.kind", "offline"))
> 	}
> 	return nil
> })
>
> tracer.TrackTool(raindrop.TrackToolOptions{
> 	Name:       "vector_lookup",
> 	Input:      map[string]any{"query": "mission coffee"},
> 	Output:     map[string]any{"winner": "Ritual Coffee Roasters"},
> 	Properties: map[string]any{"step": "retrieve"},
> })
> ```
>
> **Span Attributes**
>
> The SDK provides helpers for creating OTLP-compatible attributes:
>
> ```go theme={null}
> span.SetAttributes(
> 	raindrop.StringAttr("ai.model.id", "gpt-4o"),
> 	raindrop.IntAttr("ai.usage.prompt_tokens", 150),
> 	raindrop.FloatAttr("ai.latency_seconds", 1.23),
> 	raindrop.BoolAttr("ai.stream", true),
> 	raindrop.StringSliceAttr("ai.tools", []string{"search", "calculator"}),
> )
> ```

> [tab] Rust
>
> Use manual spans for workflow, task, retrieval, or any other work that is not specifically an LLM generation or tool call. Build span trees by passing a parent into `SpanOptions::parent`:
>
> ```rust theme={null}
> use raindrop::{Attribute, SpanOptions};
>
> let parent = client.start_span(SpanOptions {
>     name: "agent.run".into(),
>     event_id: "evt_123".into(),
>     operation_id: "ai.workflow".into(),
>     ..Default::default()
> });
>
> let retrieval = client.start_span(SpanOptions {
>     name: "rag.retrieve".into(),
>     event_id: "evt_123".into(),
>     operation_id: "ai.task".into(),
>     parent: Some(parent.clone()),
>     attributes: vec![Attribute::string("retrieval.index", "docs")],
>     ..Default::default()
> });
> retrieval.set_attributes([
>     Attribute::int("retrieval.results", 4),
> ]);
> retrieval.end();
>
> parent.end();
> ```
>
> Spans started from an `Interaction` automatically inherit its `user_id`, `convo_id`, and `event` as `traceloop.association.properties.*` attributes, so the dashboard groups them under the same user, conversation, and event:
>
> ```rust theme={null}
> let interaction = client
>     .begin(BeginOptions {
>         user_id: "user-123".into(),
>         convo_id: "conv-456".into(),
>         event: "agent_run".into(),
>         ..Default::default()
>     })
>     .await;
>
> let span = interaction.start_span(SpanOptions {
>     name: "rag.retrieve".into(),
>     ..Default::default()
> });
> // ... do work ...
> span.end();
> ```
>
> > [!NOTE]
> >
> > Plain `client.start_span(...)` calls only need `name` + `event_id`. The SDK automatically emits
> > `traceloop.association.properties.event_id` so the span survives the backend's ingestion filter.
> > For non-event-bound spans, set `operation_id` (e.g. `"ai.workflow"`) or pass `properties` so the
> > span has at least one of `ai.operationId`, `traceloop.span.kind`, `traceloop.workflow.name`,
> > `traceloop.association.properties.*`, or `gen_ai.*` — otherwise it will be dropped server-side.
>
> **Closure-style helpers**
>
> If you prefer scoped instrumentation, `with_span` runs a closure inside a span and automatically marks the span as failed on `Err`:
>
> ```rust theme={null}
> interaction
>     .with_span::<_, _, _, std::io::Error>(
>         SpanOptions { name: "summarize".into(), ..Default::default() },
>         |span| async move {
>             span.set_attributes([Attribute::string("phase", "draft")]);
>             Ok::<_, std::io::Error>(())
>         },
>     )
>     .await?;
> ```
>
> **Standalone Tracer**
>
> Use `Client::tracer()` for batch jobs or non-conversation work where you still want spans, LLM spans, and tool traces:
>
> ```rust theme={null}
> let tracer = client.tracer(BTreeMap::from([("job_id".into(), json!("batch-123"))]));
>
> let span = tracer.start_span(SpanOptions { name: "embed".into(), ..Default::default() });
> span.end();
>
> let llm = tracer.start_llm_span(
>     "batch.summarize",
>     LlmOptions {
>         model: "gpt-4o-mini".into(),
>         input: Some("Summarize the batch.".into()),
>         ..Default::default()
>     },
> );
> llm.set_output("Batch summary complete.");
> llm.end();
>
> tracer.track_tool(TrackToolOptions {
>     name: "vector_lookup".into(),
>     input: Some(json!({ "query": "mission coffee" })),
>     output: Some(json!({ "winner": "Ritual Coffee Roasters" })),
>     properties: BTreeMap::from([("step".into(), json!("retrieve"))]),
>     ..Default::default()
> });
> ```
>
> **Span Attributes**
>
> The SDK provides typed helpers for OTLP-compatible attributes:
>
> ```rust theme={null}
> span.set_attributes([
>     Attribute::string("ai.model.id", "gpt-4o"),
>     Attribute::int("ai.usage.prompt_tokens", 150),
>     Attribute::float("ai.latency_seconds", 1.23),
>     Attribute::bool("ai.stream", true),
>     Attribute::string_array("ai.tools", vec!["search".into(), "calculator".into()]),
> ]);
> ```

> [tab] Java
>
> Spans are nestable: a span started from another span is parented to it, so subagent and tool
> trajectories form the correct tree in the trace view. Use `startSpan(SpanOptions)` for a generic
> span (for example a subagent with a custom `operationId`), and `startTool` / `startTask` for the
> common cases:
>
> ```java theme={null}
> Span root = it.startSpan(SpanOptions.builder("agent.root")
>         .kind("workflow").operationId("ai.workflow").build());
>
> Span researcher = root.startSpan(SpanOptions.builder("subagent.researcher")
>         .kind("task").operationId("ai.subagent")
>         .attribute("subagent.name", "researcher").build());
>
> Span lookup = researcher.startTool("customer_profile_lookup");
> lookup.setInput(Map.of("customer_id", "cust_123"));
> lookup.setOutput(Map.of("plan", "enterprise"));
> lookup.end();
>
> researcher.end();
> root.end();
> ```
>
> You can also start a free-standing tool span that is not tied to an interaction with
> `raindrop.startToolSpan("name")`.

> [tab] Swift
>
> Spans are exported to Raindrop as OpenTelemetry (OTLP JSON). Start them from an interaction so they're linked to its event, user and conversation:
>
> ```swift theme={null}
> let run = interaction.startSpan("agent.run", operationId: "ai.workflow")
>
> let llm = interaction.startLlmSpan(
>   "anthropic.messages",
>   provider: "anthropic",
>   model: "claude-sonnet-4-5",
>   messages: [.system("You are concise."), .user("What's the weather in Paris?")],
>   tools: [ToolDeclaration(name: "get_weather", inputSchema: ["type": "object"])],
>   parent: .span(run)
> )
> llm.setOutput("I'll check the weather.")
> llm.setUsage(TokenUsage(inputTokens: 47, outputTokens: 11))
> llm.end()
>
> let forecast = try await interaction.withTool("get_weather", input: ["city": "Paris"], parent: .span(run)) {
>   try await weather(for: "Paris")
> }
>
> run.end()
> await interaction.finish(output: "It's sunny in Paris.")
> ```
>
> * `startToolSpan(_:input:)` gives you a `ToolSpan` to end yourself. `trackTool(_:input:output:error:duration:startTime:endTime:parent:)` records a call that already happened: give a start time plus a `duration` or an `endTime` (the end wins when both are given).
> * `withTool` records the result, or the thrown error, and rethrows.
> * Token usage is sent as raw provider counts (`gen_ai.usage.*`); Raindrop computes cost.
> * `RaindropClient.startSpan`, `startLlmSpan` and `startToolSpan` create spans outside an interaction. Pass `eventId:` to link them to an event.
> * `span.ref` is a `SpanRef` (hex trace and span ids). Save it to parent spans recorded later, even in another process: `parent: .ref(savedRef)`. This works for `trackTool` too.
> * `RaindropConfiguration.tracingEnabled = false` turns all spans off.
>
> **Nesting spans automatically**
>
> A span started without `parent:` nests under the **current span** if that span belongs to the same event. `withSpan`, `withTool` and `inlineSubagent` make their span current while their closure runs, so you don't have to pass parents around:
>
> ```swift theme={null}
> let answer = try await interaction.withSpan("agent.run", operationId: "ai.workflow") { _ in
>   let llm = interaction.startLlmSpan("anthropic.messages", model: "claude-sonnet-4-5", input: question)  // child of agent.run
>   llm.setOutput(draft)
>   llm.end()
>   return try await interaction.withTool("get_weather", input: ["city": "Paris"]) {  // child of agent.run
>     try await weather(for: "Paris")
>   }
> }
> ```
>
> * The current span is task-local: `Task {}` and `async let` inherit it, `Task.detached` doesn't. `Span.current` reads it, and `span.withCurrent { }` sets it for any span.
> * Spans for a different event never nest under it, so a detached subagent (its own event) stays separate even when started inside the scope.
> * An explicit `parent:` always wins. `parent: .root` starts a new trace.

## Tool calls

Trace a tool call as it runs: its input, output, duration and any error are recorded on a tool span. To record a call after it happened, use `trackTool` (see [Tool spans](https://raindrop.ai/docs/sdk/overview.md#tool-spans)).

```typescript TypeScript icon="https://mintcdn.com/dawn-a6c57108/72ImDkk3v1Xlc68q/images/icons/typescript.svg?fit=max&auto=format&n=72ImDkk3v1Xlc68q&q=85&s=d8ece0bb7b221c02bae30c5db9c578b3" theme={null}
const results = await turn.withTool(
  { name: "web_search", inputParameters: { query: "release notes" } },
  async () => ["https://example.com/notes"],
);
```

```python Python icon="python" theme={null}
tool = turn.start_span(kind="tool", name="web_search")
tool.record_input({"query": "release notes"})
try:
    tool.record_output(["https://example.com/notes"])
    tool.end()
except Exception as e:
    tool.end(error=e)
```

```go Go icon="golang" theme={null}
results, err := raindrop.WithTool(turn, "web_search", raindrop.ToolOptions{
	Input: map[string]any{"query": "release notes"},
}, func() ([]string, error) {
	return []string{"https://example.com/notes"}, nil
})
```

```rust Rust icon="rust" theme={null}
use raindrop::ToolOptions;

let results = raindrop::with_tool::<_, _, std::io::Error>(
    &turn,
    "web_search",
    ToolOptions {
        input: Some(json!({ "query": "release notes" })),
        ..Default::default()
    },
    || Ok(json!(["https://example.com/notes"])),
)?;
```

```java Java icon="java" theme={null}
Span search = turn.startTool("web_search");
search.setInput(Map.of("query", "release notes"));
try {
    search.setOutput(List.of("https://example.com/notes"));
} catch (RuntimeException e) {
    search.setError(e);
} finally {
    search.end();
}
```

```swift Swift icon="swift" theme={null}
let results = await turn.withTool("web_search", input: ["query": "release notes"]) {
  ["https://example.com/notes"]
}
```

> [tab] TypeScript
>
> Use `withTool` to trace agent actions—memory operations, web searches, API calls, and more:
>
> ```typescript theme={null}
> // Basic tool call
> const result = await interaction.withTool(
>   { name: "search_tool" },
>   async () => {
>     return "Search results";
>   }
> );
>
> // Tool with metadata
> const result = await interaction.withTool(
>   {
>     name: "calculator",
>     properties: { operation: "multiply" },
>     inputParameters: { a: 5, b: 10 },
>   },
>   async () => {
>     return "Result: 50";
>   }
> );
> ```
>
> | Parameter | Type | Description |
> | - | - | - |
> | `name` | `string` | Name for identification in traces |
> | `version` | `number` | Version number of the tool |
> | `properties` | `Record<string, string>` | Additional metadata |
> | `inputParameters` | `Record<string, any>` | Input parameters for the tool |
> | `traceContent` | `boolean` | Whether to trace content |
> | `suppressTracing` | `boolean` | Suppress tracing for this invocation |

> [tab] Python
>
> Python records spans only when tracing is on: initialize with `raindrop.init(os.getenv("RAINDROP_WRITE_KEY"), tracing_enabled=True)` (or `Raindrop(..., tracing_enabled=True)`). Without it, the event is sent but its spans aren't.
>
> When decorators aren't feasible (e.g., dynamic tool selection, complex control flow), you can use context managers for fine-grained tracing control.
>
> **Requirements:**
>
> 1. Initialize with `tracing_enabled=True`
> 2. Decorate your entry-point function with `@raindrop.interaction` to establish a tracing context
> 3. Use `with raindrop.tool_span(...)` or `with raindrop.task_span(...)` for traced blocks
>
> Context managers support both synchronous and asynchronous code. Spans automatically inherit the current trace context and no-op when tracing is disabled.
>
> **Available methods on the span object:**
>
> * `record_input(data)`: Record input data for the span
> * `record_output(data)`: Record output data for the span
> * `set_properties(props)`: Set custom properties on the span
>
> **Example:**
>
> ```python theme={null}
> import asyncio
> import os
> import raindrop.analytics as raindrop
> import time
>
> raindrop.init(os.getenv("RAINDROP_WRITE_KEY"), tracing_enabled=True)
>
> @raindrop.task("main_task")
> async def main():
>     interaction = raindrop.begin(
>         user_id="user789",
>         event="multi_tool_query",
>         input="Find and summarize docs for the search API"
>     )
>
>     # Synchronous tool call: web_search
>     with raindrop.tool_span("web_search", version=1) as tool:
>         tool.record_input({"query": "search API documentation"})
>         time.sleep(0.1)  # Simulate API call
>         search_results = ["https://docs.example.com/api", "https://blog.example.com/tutorial"]
>         tool.set_properties({"results_count": len(search_results)})
>         tool.record_output({"urls": search_results})
>
>     # Async tool call: document_reranker
>     async with raindrop.tool_span("document_reranker") as tool:
>         tool.record_input({
>             "documents": search_results, 
>             "query": "search API usage"
>         })
>         await asyncio.sleep(0.05)  # Simulate async reranking
>         best_doc = search_results[0]
>         tool.record_output({"top_document": best_doc})
>
>     # Nested task span for summarization
>     with raindrop.task_span("summarization") as task:
>         task.record_input({"document": best_doc})
>         summary = "The search API allows filtering by..."
>         task.record_output({"summary": summary})
>
>     interaction.finish(output=summary)
>     raindrop.shutdown()
>
> if __name__ == "__main__":
>     asyncio.run(main())
> ```

> [tab] Go
>
> `WithTool` traces an agent tool call and captures its input and output automatically.
>
> `WithTool` is a generic function—the return type matches your callback's return type.

> [tab] Rust
>
> `with_tool` and `with_tool_async` run a closure inside a tool span and JSON-serialize its result onto `traceloop.entity.output`. To start and end a tool span yourself, use `interaction.start_tool_span(...)`; see the Rust tab of [Tool spans](https://raindrop.ai/docs/sdk/overview.md#tool-spans).

> [tab] Java
>
> Spans created from an interaction inherit its `userId`, `convoId`, and `event`, so the dashboard
> groups them under the same user, conversation, and event. Use `startTool` for tool calls and
> `startTask` for other units of work.
>
> `setInput`, `setOutput`, `setError` (which accepts a `Throwable` or a `String`), and `setAttribute`
> all return the span for chaining. Always `end()` a span — a `try/finally` guarantees it even when
> the work throws.

> [tab] Swift
>
> `withTool` records the result, or the thrown error, and rethrows. `startToolSpan(_:input:)` gives you a `ToolSpan` to end yourself.

## Model calls

The TypeScript and Python SDKs instrument common LLM client libraries, so model calls made inside an interaction appear as spans without extra code. The other SDKs record the model calls you describe. Either way, the list of tools a model was given is recorded as `ai.prompt.tools`; see [Tool catalog capture](https://raindrop.ai/docs/sdk/tool-catalog.md).

> [tab] TypeScript
>
> Wrap your code with `withSpan` or `withTool` on an interaction, and LLM calls inside are automatically captured:
>
> ```typescript theme={null}
> import { Raindrop } from "raindrop-ai";
>
> const raindrop = new Raindrop({ writeKey: RAINDROP_API_KEY });
>
> const interaction = raindrop.begin({ ... });
> await interaction.withSpan({ name: "my_task" }, async () => {
>   // LLM calls here are automatically traced
> });
> ```
>
> **Next.js users:** Add `raindrop-ai` to [`serverExternalPackages`](https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages) in your config:
>
> ```typescript theme={null}
> // next.config.js
> /** @type {import('next').NextConfig} */
> const nextConfig = {
>   serverExternalPackages: ['raindrop-ai'],
> };
>
> module.exports = nextConfig;
> ```
>
> **Module Instrumentation**
>
> In some environments, automatic instrumentation may not work due to module loading order or bundler behavior. Use `instrumentModules` to explicitly specify which modules to instrument:
>
> > [!WARNING]
> >
> > **Anthropic users:** You must use a module namespace import (`import * as ...`), not the default export.
>
> ```typescript theme={null}
> import OpenAI from "openai";
> import Anthropic from "@anthropic-ai/sdk";
> import * as AnthropicModule from "@anthropic-ai/sdk";  // Required for instrumentation
> import { Raindrop } from "raindrop-ai";
>
> const raindrop = new Raindrop({
>   writeKey: RAINDROP_API_KEY,
>   instrumentModules: {
>     openAI: OpenAI,
>     anthropic: AnthropicModule,  // Pass the module namespace, not the default export
>   },
> });
> ```
>
> Supported modules: `openAI`, `anthropic`, `cohere`, `bedrock`, `google_vertexai`, `google_aiplatform`, `pinecone`, `together`, `langchain`, `llamaIndex`, `chromadb`, `qdrant`, `mcp`.
>
> For Bedrock, use a namespace import — same pattern as Anthropic above. Cross-region inference profile model IDs (e.g. `us.anthropic.claude-*`) are supported.
>
> ```typescript theme={null}
> import * as BedrockModule from "@aws-sdk/client-bedrock-runtime";
>
> const raindrop = new Raindrop({
>   writeKey: RAINDROP_API_KEY,
>   instrumentModules: { bedrock: BedrockModule },
> });
> ```
>
> **OpenTelemetry Integration**
>
> If you already have an OpenTelemetry setup (Sentry, Datadog, Honeycomb, etc.), integrate Raindrop alongside it using `useExternalOtel`:
>
> ```typescript theme={null}
> import { NodeSDK } from "@opentelemetry/sdk-node";
> import * as AnthropicModule from "@anthropic-ai/sdk";
> import Anthropic from "@anthropic-ai/sdk";
> import { Raindrop } from "raindrop-ai";
>
> // 1. Create Raindrop with useExternalOtel
> const raindrop = new Raindrop({
>   writeKey: RAINDROP_API_KEY,
>   useExternalOtel: true,
>   instrumentModules: { anthropic: AnthropicModule },
> });
>
> // 2. Add Raindrop's processor and instrumentations to your NodeSDK
> const sdk = new NodeSDK({
>   spanProcessors: [
>     raindrop.createSpanProcessor(),  // Sends traces to Raindrop
>     datadogProcessor,                  // Your existing processor
>   ],
>   instrumentations: raindrop.getInstrumentations(),
> });
> sdk.start();
>
> // If using Sentry, skip NodeSDK and pass the processor directly:
> // Sentry.init({
> //   dsn: process.env.SENTRY_DSN,
> //   openTelemetrySpanProcessors: [raindrop.createSpanProcessor()],
> // });
>
> // 3. Create AI clients AFTER SDK starts
> const anthropic = new Anthropic({ apiKey: "..." });
>
> // 4. Use Raindrop normally
> const interaction = raindrop.begin({
>   eventId: "my-event",
>   event: "chat_request",
>   userId: "user_123",
>   input: "Hello!",
> });
>
> await interaction.withSpan({ name: "generate_response" }, async () => {
>   const response = await anthropic.messages.create({
>     model: "claude-3-haiku-20240307",
>     max_tokens: 100,
>     messages: [{ role: "user", content: "Hello!" }],
>   });
>   return response;
> });
>
> await interaction.finish({ output: "Response from Claude" });
> ```
>
> | Method | Description |
> | - | - |
> | `createSpanProcessor()` | Returns a span processor that sends traces to Raindrop |
> | `getInstrumentations()` | Returns OpenTelemetry instrumentations for AI libraries |
>
> > [!NOTE]
> >
> > Without `instrumentModules`, `getInstrumentations()` returns instrumentations for all supported AI libraries. Specify `instrumentModules` to instrument only specific libraries.

> [tab] Python
>
> **Auto-instrumentation**
>
> When `tracing_enabled=True`, the SDK automatically instruments detected LLM
> client libraries (OpenAI, Anthropic, Bedrock, etc.) so their calls appear as
> spans in your traces.
>
> > [!WARNING]
> >
> > Auto-instrumentation produces **spans**, not events. `tracing_enabled=True` alone shows
> > nothing on the Events page. Wrap each request in [`begin()` / `finish()`](https://raindrop.ai/docs/sdk/overview#track-an-interaction)
> > so its LLM calls attach to an event as spans. [`track_ai()`](https://raindrop.ai/docs/sdk/overview#track-a-single-event) also
> > creates an event, but earlier spans don't link to it. Signals, issues, and users are all
> > built on events.
>
> You can use the `Instruments` enum to control which libraries are instrumented:
>
> ```python theme={null}
> from raindrop.analytics import Instruments
>
> # Only auto-instrument Anthropic
> raindrop.init(
>     os.getenv("RAINDROP_WRITE_KEY"),
>     tracing_enabled=True,
>     instruments={Instruments.ANTHROPIC},
> )
>
> # Auto-instrument everything except OpenAI
> raindrop.init(
>     os.getenv("RAINDROP_WRITE_KEY"),
>     tracing_enabled=True,
>     block_instruments={Instruments.OPENAI},
> )
>
> # Disable all auto-instrumentation (manual tracing still works)
> raindrop.init(
>     os.getenv("RAINDROP_WRITE_KEY"),
>     tracing_enabled=True,
>     auto_instrument=False,
> )
> ```
>
> > [!TIP]
> >
> > When auto-instrumentation is enabled, the SDK automatically suppresses noisy
> > warnings from instrumentors for providers you don't use (e.g. "Error initializing
> > MistralAI instrumentor") and from OTel attribute type validation (e.g. provider
> > SDKs using sentinel types like `Omit`). Enable `set_debug_logs(True)` to see
> > these messages for troubleshooting.
>
> **Tracing with decorators**
>
> 1. Enable tracing by passing `tracing_enabled=True` to `raindrop.init(...)`.
> 2. Decorate your entry-point function with `@raindrop.interaction`.
> 3. Decorate tool functions with `@raindrop.tool`.
>
> The example below traces OpenAI tool calls. It enables tracing, decorates a tool, starts an interaction with `begin(...)`, and finishes it later via `resume_interaction()`.
>
> ```python theme={null}
> import json
> import os
> from openai import OpenAI
> import raindrop.analytics as raindrop
>
> @raindrop.tool("get_current_weather")
> def get_current_weather(location: str, unit: str = "celsius"):
>     """Mock weather tool."""
>     return {"location": location, "temperature": 22, "unit": unit}
>
> def send_to_user(text: str) -> None:
>     # Resume the current interaction from the tracing context and finish elsewhere
>     raindrop.resume_interaction().finish(output=text)
>     print(f"Sending to user: {text}")
>
> @raindrop.interaction("weather_interaction")
> def main() -> None:
>     client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
>
>     # Create an interaction for observability (begin → finish)
>     interaction = raindrop.begin(
>         user_id="user-001",
>         event="weather_query",
>         input="What's the weather in Boston, MA today?",
>         convo_id="convo-weather-001",
>     )
>
>     messages = [
>         {"role": "system", "content": "You are helpful. Use tools when needed."},
>         {"role": "user", "content": "What's the weather in Boston, MA today?"},
>     ]
>
>     # Let the model request tool invocations if needed
>     first = client.chat.completions.create(
>         model="gpt-4o-mini",
>         messages=messages,
>         tools=[{
>             "type": "function",
>             "function": {
>                 "name": "get_current_weather",
>                 "parameters": {
>                     "type": "object",
>                     "properties": {
>                         "location": {"type": "string"},
>                         "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
>                     },
>                     "required": ["location"],
>                 },
>             },
>         }],
>         tool_choice="auto",
>         temperature=0.2,
>     )
>
>     choice = first.choices[0]
>     tool_calls = getattr(choice.message, "tool_calls", None)
>
>     if tool_calls:
>         messages.append({
>             "role": "assistant",
>             "content": getattr(choice.message, "content", None),
>             "tool_calls": [{
>                 "id": tc.id,
>                 "type": "function",
>                 "function": {"name": tc.function.name, "arguments": tc.function.arguments},
>             } for tc in tool_calls],
>         })
>
>         for tc in tool_calls:
>             args = json.loads(tc.function.arguments or "{}")
>             result = (
>                 get_current_weather(**args)
>                 if tc.function.name == "get_current_weather"
>                 else {"error": "unknown tool"}
>             )
>             messages.append({
>                 "role": "tool",
>                 "tool_call_id": tc.id,
>                 "name": tc.function.name,
>                 "content": json.dumps(result),
>             })
>
>         # Final model response after tools
>         second = client.chat.completions.create(
>             model="gpt-4o-mini", messages=messages, temperature=0.2
>         )
>         final_text = second.choices[0].message.content or ""
>     else:
>         final_text = choice.message.content or ""
>
>     print("Assistant:\n", final_text)
>     send_to_user(final_text)
>     raindrop.flush()
>     raindrop.shutdown()
>
> if __name__ == "__main__":
>     raindrop.init(os.getenv("RAINDROP_WRITE_KEY"), tracing_enabled=True)
>     main()
> ```
>
> **Using the `@raindrop.interaction()` decorator**
>
> You can wrap your flow with the `@raindrop.interaction("name")` decorator to ensure a tracing context exists, which allows `resume_interaction()` to find the current Interaction without passing an `event_id`:
>
> ```python theme={null}
> import os
> import raindrop.analytics as raindrop
>
> raindrop.init(os.getenv("RAINDROP_WRITE_KEY"), tracing_enabled=True)
>
> def send_to_user():
>     interaction = raindrop.resume_interaction()
>     interaction.finish(output="It's sunny!")
>
> @raindrop.interaction("weather_flow")
> def run_weather_flow():
>     interaction = raindrop.begin(user_id="user-001", event="weather_query", input="What's the weather?")
>     send_to_user()
>
>
> run_weather_flow()
> ```
>
> > [!NOTE]
> >
> > **Resume caveats:**
> >
> > * `resume_interaction()` resolves the current Interaction by reading the active tracing context (current span). It will only find an existing Interaction when called under the same traced execution that called `begin(...)` (for example, inside a function decorated with `@raindrop.task`, `@raindrop.tool`, or `@raindrop.interaction`).
> > * If your code runs outside that tracing context (separate thread/process, lost OpenTelemetry context, background job, etc.), pass the event ID explicitly: `resume_interaction(event_id="...")`.
> > * If neither a matching trace context nor an `event_id` is available, a new `Interaction` instance will be created.

> [tab] Go
>
> The Go SDK records the spans you create; it doesn't instrument LLM client libraries. Record a model call with `WithSpan` and the `ai.*` attributes from [Spans](#spans) (for example `ai.model.id` and `ai.usage.prompt_tokens`).

> [tab] Rust
>
> **No automatic LLM-client instrumentation.** Unlike the Python and TypeScript SDKs, the Rust SDK does not auto-hook into LLM frameworks. Create spans manually via `start_span`, `start_llm_span`, `start_tool_span`, `with_span`, `with_tool`, or `track_tool`.
>
> **LLM Spans**
>
> Use `start_llm_span` for model calls. `LlmSpan` emits the attributes that Raindrop's backend and frontend understand for prompt messages, response text, model/provider, and token usage.
>
> ```rust theme={null}
> use raindrop::{LlmMessage, LlmOptions, SpanOptions};
>
> let agent = interaction.start_span(SpanOptions {
>     name: "agent.run".into(),
>     operation_id: "ai.workflow".into(),
>     ..Default::default()
> });
>
> let llm = interaction.start_llm_span(
>     "openai.responses",
>     LlmOptions {
>         parent: Some(agent.clone()),
>         provider: "openai".into(),
>         model: "gpt-4o".into(),
>         messages: vec![
>             LlmMessage::system("You are concise."),
>             LlmMessage::user("What is the weather in San Francisco?"),
>         ],
>         ..Default::default()
>     },
> );
>
> let output = call_llm().await;
>
> llm.set_output(output);
> llm.set_token_usage("gpt-4o", /* input */ 47, /* output */ 11);
> llm.end();
> agent.end();
> ```
>
> `messages` is the chat-shaped prompt representation. Use it when your provider call takes a role/content array instead of a single text prompt:
>
> ```rust theme={null}
> let llm = interaction.start_llm_span(
>     "openai.chat",
>     LlmOptions {
>         model: "gpt-4o".into(),
>         provider: "openai".into(),
>         messages: vec![
>             LlmMessage::system("You answer in one sentence."),
>             LlmMessage::user("Summarize this support ticket."),
>             LlmMessage::assistant("The user is asking about billing."),
>             LlmMessage::user("Add the next action."),
>         ],
>         ..Default::default()
>     },
> );
>
> llm.set_output("Ask the user for the invoice number.");
> llm.end();
> ```
>
> `LlmMessage::system`, `LlmMessage::user`, and `LlmMessage::assistant` are convenience constructors; use `LlmMessage::new(role, content)` for provider-specific roles. If both `input` and `messages` are set in `LlmOptions`, `messages` wins. The backend uses the last user message as the span's `input_payload`, while the frontend renderer can show the full message array.
>
> If you only know the messages after creating the span, use `set_messages`. It replaces any previous `input` or prompt-message attributes:
>
> ```rust theme={null}
> let llm = interaction.start_llm_span(
>     "anthropic.messages",
>     LlmOptions {
>         provider: "anthropic".into(),
>         model: "claude-sonnet-4-5".into(),
>         ..Default::default()
>     },
> );
>
> llm.set_messages([
>     LlmMessage::system("You answer using the provided tool result."),
>     LlmMessage::user("What is the weather in San Francisco?"),
>     LlmMessage::assistant("I will call get_weather."),
>     LlmMessage::user("The tool returned 67°F and windy."),
> ]);
> llm.set_output("It is 67°F and windy in San Francisco.");
> llm.end();
> ```
>
> You can also seed the LLM span from `LlmOptions`:
>
> ```rust theme={null}
> let llm = interaction.start_llm_span(
>     "anthropic.messages",
>     LlmOptions {
>         provider: "anthropic".into(),
>         model: "claude-sonnet-4-5".into(),
>         input: Some("Summarize this ticket.".into()),
>         output: Some("The user needs help with billing.".into()),
>         input_tokens: 120,
>         output_tokens: 24,
>         ..Default::default()
>     },
> );
> llm.end();
> ```
>
> `LlmSpan` exposes `set_model`, `set_provider`, `set_input`, `set_messages`, `set_output`, `set_io`, `set_token_usage`, `set_error`, `end`, and `end_at`.

> [tab] Java
>
> The SDK is manual-only: it does not monkey-patch or auto-instrument your LLM client libraries, so it cannot interfere with your application's runtime. Record a model call as a span with `startSpan(SpanOptions)`; see [Spans](#spans).

> [tab] Swift
>
> No automatic instrumentation of LLM clients: record model calls with `startLlmSpan` (shown in the Swift tab of [Spans](#spans)). It takes the provider, model, chat `messages` and the `tools` the model was given; set the result with `setOutput` and token usage with `setUsage`. Token usage is sent as raw provider counts (`gen_ai.usage.*`); Raindrop computes cost.

## Sub-agents

A detached sub-agent is work launched from one event that reports as its own event, linked back to the launcher, possibly in another process or service. The launcher records the hand-off: this adds a dispatch span to its event and returns the child's event id and carrier headers. Send all of the headers with the job, not only `traceparent`. The worker opens the child's event from them, and every span it records links back to the launcher.

The carrier is the same in every SDK, so a hand-off can cross languages. A missing or unreadable carrier never fails the work: the child reports as an unlinked event. Only accept carriers from inside your own trust boundary, since a carrier decides which event the child's data is attributed to.

```typescript TypeScript icon="https://mintcdn.com/dawn-a6c57108/72ImDkk3v1Xlc68q/images/icons/typescript.svg?fit=max&auto=format&n=72ImDkk3v1Xlc68q&q=85&s=d8ece0bb7b221c02bae30c5db9c578b3" theme={null}
// Launcher: record the hand-off and send all of its headers with the job.
const dispatch = turn.subagent({ name: "researcher", input: { task: "find sources" } });
const jobHeaders: Record<string, string> = { ...dispatch.headers };
await turn.finish({ output: `Dispatched ${dispatch.name}` });

// Worker (any process): open the child's own event from those headers.
const run = raindrop.resumeSubagent(jobHeaders);
await run.finish({ output: "Found 3 sources" });
```

```python Python icon="python" theme={null}
# Launcher: record the hand-off and send all of its headers with the job.
dispatch = turn.subagent(name="researcher", input={"task": "find sources"})
job_headers = dict(dispatch.headers)
turn.finish(output=f"Dispatched {dispatch.name}")

# Worker (any process): open the child's own event from those headers.
from raindrop import Raindrop

rd = Raindrop(api_key=os.getenv("RAINDROP_WRITE_KEY"), tracing_enabled=True)
with rd.resume_subagent(headers=job_headers) as run:
    run.finish(output="Found 3 sources")
```

```go Go icon="golang" theme={null}
// Launcher: record the hand-off and send all of its headers with the job.
dispatch := turn.Subagent(raindrop.SubagentOptions{Name: "researcher", Input: "find sources"})
jobHeaders := dispatch.Headers
_ = turn.Finish(raindrop.FinishOptions{Output: "Dispatched researcher"})

// Worker (any process): open the child's own event from those headers.
run := client.ResumeSubagent(ctx, jobHeaders, raindrop.ResumeSubagentOptions{UserID: "user-123"})
_ = run.Finish(raindrop.FinishOptions{Output: "Found 3 sources"})
```

```rust Rust icon="rust" theme={null}
use raindrop::{ResumeSubagentOptions, SubagentOptions};

// Launcher: record the hand-off and send all of its headers with the job.
let dispatch = turn.subagent(SubagentOptions {
    name: "researcher".into(),
    input: Some(json!({ "task": "find sources" })),
    ..Default::default()
});
let job_headers = dispatch.headers.clone();
turn.finish(FinishOptions {
    output: "Dispatched researcher".into(),
    ..Default::default()
})
.await?;

// Worker (any process): open the child's own event from those headers.
let run = client
    .resume_subagent(
        &job_headers,
        ResumeSubagentOptions {
            user_id: "user-123".into(),
            ..Default::default()
        },
    )
    .await;
run.finish(FinishOptions {
    output: "Found 3 sources".into(),
    ..Default::default()
})
.await?;
```

```java Java icon="java" theme={null}
// Launcher: record the hand-off and send all of its headers with the job.
SubagentDispatch dispatch = turn.dispatchSubagent("researcher", null, "find sources");
Map<String, String> jobHeaders = dispatch.headers();
turn.setOutput("Dispatched researcher").finish();

// Worker (any process): open the child's own event from those headers.
Interaction run = raindrop.resumeSubagent(jobHeaders, "user-123");
run.setOutput("Found 3 sources").finish();
```

```swift Swift icon="swift" theme={null}
// Launcher: record the hand-off and send all of its headers with the job.
let handoff = turn.subagent(name: "researcher", input: ["task": "find sources"])
let jobHeaders = handoff.headers
await turn.finish(output: "Dispatched researcher")

// Worker (any process): open the child's own event from those headers.
let run = raindrop.beginSubagent(headers: jobHeaders, userId: "user-123")
await run.finish(output: "Found 3 sources")
```

> [tab] TypeScript
>
> A detached sub-agent runs asynchronously in another process, worker, or machine.
> Unlike `resumeInteraction()`, which joins the parent's event, it reports as its
> **own event**. The parent mints the child event and sends a hand-off carrier;
> the worker resumes that event and reports its result.
>
> > [!NOTE]
> >
> > **Beta.** The detached sub-agent API surface may change in a minor release.
>
> **1. Dispatch from the parent**
>
> Each process creates its own `Raindrop` instance. The parent allocates the
> child event before sending the job:
>
> ```typescript theme={null}
> // Parent process
> import { Raindrop } from "raindrop-ai";
>
> const raindrop = new Raindrop({
>   writeKey: process.env.RAINDROP_API_KEY,
> });
>
> const interaction = raindrop.begin({
>   event: "parent_turn",
>   userId: "user_123",
>   convoId: "conversation_456",
>   input: "Find the relevant documents",
> });
>
> const dispatch = interaction.subagent({
>   name: "researcher",
>   input: { task: "Find the relevant documents" },
> });
> ```
>
> **2. Send the hand-off**
>
> Send the job with all of `dispatch.headers`:
>
> ```typescript theme={null}
> await queue.send({
>   task: "Find the relevant documents",
>   headers: dispatch.headers,
> });
>
> await interaction.finish({
>   output: `Dispatched ${dispatch.name} as ${dispatch.childEventId}`,
> });
> ```
>
> `childEventId` is allocated before dispatch, so the link is resolvable while the
> job is queued. The dispatch span is named `launch_subagent` by default; use
> `toolName` to choose another name.
>
> `interaction.subagent()` returns:
>
> * `childEventId` — the child event id, allocated before dispatch
>
> * `name` — the sub-agent name
>
> * `parentEventId` — the parent event id
>
> * `dispatchSpanId` — the dispatch span id, when tracing is enabled
>
> * `headers` — the native carrier headers
>
> * `langsmithHeaders` — the carrier in LangSmith's header shape
>
> * `carrier` — the parsed carrier for non-HTTP transports
>
> > [!NOTE]
> >
> > Send **all** returned headers, not only `traceparent`. The carrier includes
> > `traceparent`, `baggage` with `raindrop-*` fields, and `x-raindrop-handoff`.
> > The hand-off header wins if standard propagation headers are rewritten or
> > replaced.
>
> With `disabled: true`, `childEventId` is still allocated, but `carrier` is
> `null` and both header objects are empty. There is no dispatch span or carrier
> link.
>
> **3. Resume in the worker**
>
> The carrier travels with the job. It is JSON-serialisable, so a queue, file, or
> another process can carry it. The worker's `request.headers` below means the
> received header bag:
>
> ```typescript theme={null}
> // Worker process
> import { Raindrop } from "raindrop-ai";
>
> const raindrop = new Raindrop({
>   writeKey: process.env.RAINDROP_API_KEY,
> });
>
> const run = raindrop.resumeSubagent(request.headers);
>
> try {
>   const answer = await run.interaction.withTool(
>     { name: "search_corpus" },
>     () => doTheWork()
>   );
>   await run.finish({ output: answer });
> } catch (error) {
>   await run.fail(error);
> }
>
> await raindrop.close();
> ```
>
> For a non-HTTP transport, the first argument accepts the received `TraceCarrier`
> too:
>
> ```typescript theme={null}
> const run = raindrop.resumeSubagent(carrier);
> // Equivalent: raindrop.resumeSubagent(undefined, { parent: carrier });
> ```
>
> A short-lived worker must `await raindrop.close()` before exit to flush buffered
> events.
>
> **4. Report the outcome**
>
> `withSubagentRun()` wraps the same worker flow:
>
> ```typescript theme={null}
> await raindrop.withSubagentRun(request.headers, async (run) => {
>   await run.finish({ output: await doTheWork() });
> });
> ```
>
> It resolves with the callback's return value. If the callback returns without
> reporting, the SDK aborts the run. If it throws, the SDK reports the failure and
> rethrows the error.
>
> `run.interaction` is the child's ordinary interaction object. It supports
> `withSpan`, [`withTool`](#tool-calls), `trackTool`, and the other interaction
> APIs. Use the object form: `withTool({ name: "search_corpus" }, fn)`.
>
> While the run is open, its reverse reference is bound to the execution context.
> Every span the child emits—including auto-instrumented spans—is linked back to the
> dispatch. `cancel()` emits no span, so a cancelled child has no reverse reference on
> a child span.
>
> **Reported outcomes**
>
> * `finish({ output })` reports a successful result; the caller supplies the output.
> * `fail(reason)` marks telemetry as errored and synthesizes output from the reason.
> * `cancel(reason)` writes a terminal event with
>   `raindrop.handoff.terminal=cancelled` and synthesized output, but emits no span or
>   errored span.
>
> Status comes from the child's telemetry, so a failed run needs an errored span.
> Only a child that reports **no outcome at all** produces no event, leaving its
> launcher `queued`.
>
> **Carrier rules**
>
> The carrier is authoritative. `SubagentResumeOptions` values (`name`, `eventId`,
> `userId`, `convoId`, `event`, `input`, `model`, or `properties`) are fallbacks only
> when no carrier is available.
>
> A missing carrier is valid: the child still reports its own event, with
> `run.linked === false` and `run.parent === null`.
>
> When a carrier resolves, `run.linked === true` and `run.parent` describes the
> launching context, including the parent event id and dispatch span id. The
> child's `run.eventId` is the `childEventId` allocated by the parent.
>
> > [!WARNING]
> >
> > Only accept carriers inside your own trust boundary; an untrusted carrier can
> > attribute a child to another tenant's event. For non-HTTP transports, pass the
> > carrier as the first argument or use
> > `resumeSubagent(undefined, { parent: carrier })`. Passing `{ parent: carrier }`
> > as the first argument does not link the run.
>
> **Dispatch options**
>
> | Option | Type | Description |
> | - | - | - |
> | `name` | `string` | Name of the sub-agent |
> | `input` | `unknown` | Launch arguments recorded on the dispatch span |
> | `childEventId` | `string` | Optional caller-supplied child event id |
> | `toolName` | `string` | Dispatch span name; defaults to `launch_subagent` |
> | `properties` | `RaindropProperties` | Extra dispatch span properties |

> [tab] Python
>
> Python records spans only when tracing is on: initialize with `raindrop.init(os.getenv("RAINDROP_WRITE_KEY"), tracing_enabled=True)` (or `Raindrop(..., tracing_enabled=True)`). Without it, the event is sent but its spans aren't.
>
> A detached sub-agent runs asynchronously in another process, worker, or machine.
> Unlike `resume_interaction()`, which joins the parent's event, it reports as its
> **own event**. The parent mints the child event and sends a hand-off carrier; the
> worker resumes that event and reports its result.
>
> > [!NOTE]
> >
> > **Beta.** The detached sub-agent API surface may change in a minor release.
>
> Python does not have a separate `withSubagentRun()` helper. The idiomatic form
> is the `SubagentRun` context manager shown below.
>
> **1. Dispatch from the parent**
>
> Each process creates its own `Raindrop` instance. The parent allocates the
> child event before sending the job:
>
> ```python theme={null}
> # Parent process
> import os
> import requests
> from raindrop import Raindrop
>
> rd = Raindrop(
>     api_key=os.environ["RAINDROP_WRITE_KEY"],
>     tracing_enabled=True,
> )
>
> interaction = rd.begin(
>     user_id="user_123",
>     event="parent_turn",
>     convo_id="conversation_456",
>     input="Find the relevant documents",
> )
>
> dispatch = interaction.subagent(
>     name="researcher",
>     input={"task": "Find the relevant documents"},
> )
> ```
>
> **2. Send the hand-off**
>
> `subagent()` records telemetry and returns a dispatch object; it does not
> transport the job. Send the job and the carrier separately:
>
> ```python theme={null}
> job = {
>     "task": "Find the relevant documents",
> }
>
> requests.post(worker_url, json=job, headers=dispatch.headers)
>
> interaction.finish(
>     output=f"Dispatched {dispatch.name} as {dispatch.child_event_id}",
> )
> rd.flush()
> rd.shutdown()
> ```
>
> Send **all** of `dispatch.headers`, not only `traceparent`. The carrier
> includes `traceparent`, `baggage`, and `x-raindrop-handoff`; cherry-picking
> only `traceparent` silently unlinks the child. `dispatch.langsmith_headers`
> is also available when the worker uses LangSmith-style instrumentation.
>
> The child event ID is allocated before dispatch, so the link is resolvable
> while the job is queued. The dispatch span is named `launch_subagent` by
> default; pass `tool_name` to choose another name.
>
> **3. Resume in the worker**
>
> The carrier travels with the job. Use the context manager as the worker's
> entry point:
>
> ```python theme={null}
> # Worker process
> import os
> from raindrop import Raindrop
>
> rd = Raindrop(
>     api_key=os.environ["RAINDROP_WRITE_KEY"],
>     tracing_enabled=True,
> )
>
> try:
>     with rd.resume_subagent(headers=request.headers) as run:
>         answer = do_the_work()
>         run.finish(output=answer)
> finally:
>     rd.flush()
>     rd.shutdown()
> ```
>
> `run.interaction` is the child's ordinary interaction object. Every span
> emitted while the run is open carries the reverse reference to the parent
> dispatch. If the worker exits the block without reporting an outcome, the
> context manager closes the run with an abort outcome instead of leaving the
> parent stuck in `queued`. If the body raises, it reports an abort and
> re-raises the exception. A cancellation exception is reported as cancelled
> rather than failed.
>
> **4. Report the outcome**
>
> Use the method that matches the worker's result:
>
> ```python theme={null}
> run.finish(output=answer)                 # successful result
> run.fail(error)                           # errored child span and abort output
> run.cancel("user aborted the request")   # cancelled terminal event
> ```
>
> `finish()` reports the child's output. `fail()` marks the child's telemetry
> as errored and synthesizes output from the reason. `cancel()` writes
> `raindrop.handoff.terminal="cancelled"` on the child event without marking
> the run as failed. A run that reports no outcome is what the context manager
> rescues on exit.
>
> > [!WARNING]
> >
> > Only accept carriers inside your own trust boundary. A carrier from an
> > untrusted caller can attribute the child to another tenant's event.
>
> The carrier is the same across Raindrop SDKs, so a hand-off can cross languages:
> a Python parent can dispatch to a TypeScript worker, and the reverse also works.
> The worker resumes the parent-minted child event either way.

> [tab] Go
>
> A detached sub-agent runs outside the current process and reports as its own event, linked back to the event that launched it. Record the launch with `Subagent()`, pass the returned headers to the child, and open the child's event with `ResumeSubagent()`:
>
> ```go theme={null}
> // Launcher
> dispatch := interaction.Subagent(raindrop.SubagentOptions{
> 	Name:  "researcher",
> 	Input: "Find recent papers on retrieval",
> })
> // Send dispatch.Headers with the job (e.g. as HTTP headers or queue metadata).
> // dispatch.ChildEventID is the child's event ID.
>
> // Child (any process, any Raindrop SDK on the launcher side)
> run := client.ResumeSubagent(ctx, headers, raindrop.ResumeSubagentOptions{
> 	UserID: "user-123", // used only if the headers don't name a user
> })
> _ = run.Finish(raindrop.FinishOptions{Output: "Found 3 papers"})
> ```
>
> The headers are `x-raindrop-handoff`, `baggage` and, when available, `traceparent`. Every span the child records while its run is open links back to the launcher. If the headers are missing or can't be read, the child still reports, as an unlinked event.

> [tab] Rust
>
> A detached sub-agent is work launched from one event that reports as its own event, possibly in another process or service. The launcher records the hand-off with `interaction.subagent()`, which adds a dispatch tool span to its event and returns the child's event ID plus carrier headers to pass along with the job:
>
> ```rust theme={null}
> use raindrop::SubagentOptions;
>
> let dispatch = interaction.subagent(SubagentOptions {
>     name: "researcher".into(),
>     input: Some(json!({ "task": "find three sources" })),
>     ..Default::default()
> });
> // Send dispatch.headers (and optionally dispatch.child_event_id) with the job.
> ```
>
> | Field | Type | Description |
> | - | - | - |
> | `name` | `String` | Sub-agent name, shown on the launcher's event (default `subagent`) |
> | `child_event_id` | `String` | The child's event ID; generated when empty (a job ID works too) |
> | `input` | `Option<Value>` | What the sub-agent was asked to do, recorded on the dispatch span |
> | `tool_name` | `String` | Name of the dispatch span (default `dispatch_subagent`) |
>
> The child opens its own event from those headers with `client.resume_subagent()`. The headers can come from any Raindrop SDK. Every span the returned interaction records links back to the launcher, and a missing or unreadable carrier never fails the work: the child just runs as an unlinked event.
>
> ```rust theme={null}
> use raindrop::{FinishOptions, ResumeSubagentOptions};
>
> let child = client
>     .resume_subagent(&headers, ResumeSubagentOptions {
>         user_id: "user-123".into(),
>         input: "find three sources".into(),
>         ..Default::default()
>     })
>     .await;
> // ... do the work, record spans on `child` ...
> child.finish(FinishOptions { output: summary, ..Default::default() }).await?;
> ```
>
> `ResumeSubagentOptions` takes `user_id`, `event` (defaults to the sub-agent's name), `input` and `convo_id`. The user and conversation IDs carried in the headers win; these fill in what the headers leave out.

> [tab] Java
>
> A sub-agent that runs as its own job reports as its own event, linked to the interaction that
> launched it. `dispatchSubagent(name, childEventId, input)` records a `launch_subagent` dispatch span
> on the launcher's event and returns the child's event ID (generated when you pass `null`) and the
> carrier headers to send with the job. The worker opens the child's own event from those headers
> with `resumeSubagent`; every span it records links back to the launcher:
>
> ```java theme={null}
> SubagentDispatch dispatch = it.dispatchSubagent("researcher", null, "find sources");
> queue.submit(job, dispatch.headers());
>
> // In the worker:
> Interaction run = raindrop.resumeSubagent(requestHeaders, "user-123"); // user if the headers carry none
> run.setOutput("found 3 sources").finish();
> ```
>
> The headers can come from any Raindrop SDK. A missing or unreadable carrier never fails the work:
> the run reports as an unlinked event.

> [tab] Swift
>
> A subagent can be recorded in two ways. Use **inline** when the parent waits for the subagent as part of the same turn, and **detached** when it runs on its own.
>
> | | Inline | Detached |
> | - | - | - |
> | Events | Part of the parent's event | Its own event, linked to the parent |
> | Spans | Nested under one subagent span in the parent's trace | Its own trace |
> | In Raindrop | A subagent within the turn | A subagent pill that opens the child event |
> | API | `inlineSubagent(_:input:) { }` | `subagent(name:input:)` + `beginSubagent(_:)` |
>
> **Inline subagents**
>
> ```swift theme={null}
> let summary = try await turn.inlineSubagent("order-worker", input: .string(question)) {
>   // Model calls, tools and spans started here nest under the subagent automatically.
>   try await worker.run(question, interaction: turn)
> }
> ```
>
> `inlineSubagent` records a tool span tagged `raindrop.agent.role=subagent` and `raindrop.subagent.name`, makes it current while the closure runs, records the result (or the thrown error) and rethrows.
>
> **Detached subagents (experimental)**
>
> A detached subagent is work that reports as its **own** event, linked back to the turn that launched it. It can run in a `Task.detached`, on another thread, or in another process. The link is just ids, carried on span attributes (`raindrop.handoff.*`).
>
> ```swift theme={null}
> // Parent turn: record the launch and get the ids the child needs.
> let handoff = turn.subagent(name: "researcher", input: ["topic": "swift"])
>
> Task.detached {
>   let run = raindrop.beginSubagent(handoff, input: "Research Swift concurrency")
>   do {
>     let notes = try await research()
>     await run.finish(output: notes)
>   } catch {
>     await run.fail(error)   // a CancellationError is recorded as a cancellation
>   }
> }
>
> await turn.finish(output: "I've asked the researcher to look into it.")
> ```
>
> * `subagent(name:)` records a `launch_subagent` tool span on the parent with the child's event id. Later spans in that turn are opted in so a turn whose only content is launches still becomes an event.
> * `beginSubagent(_:)` opens the child's interaction under `handoff.childEventId`. Every span started from it carries `raindrop.handoff.parentEventId`, `parentSpanId`, `name` and `raindrop.agent.role=subagent`. The child's user and conversation default to the parent's.
> * End the child with `finish(output:)`, `fail(_:)` (records an errored `ai.subagent.failed` span if none of its spans errored, and outputs `Aborted: …`) or `cancel(reason:)` (sets `raindrop.handoff.terminal=cancelled` on the child event).
> * `SubagentHandoff` is plain `Codable` data. To run the child in another process, encode it and send it however you like. You can also build one from ids you stored yourself: `SubagentHandoff(parentEventId:parentSpanId:childEventId:name:)`.
> * To hand off to another service over HTTP, send `handoff.headers` (`x-raindrop-handoff`, `baggage` and, when the ids are W3C-shaped, `traceparent`) with the request. The child opens its event with `raindrop.beginSubagent(headers:userId:)`; `userId` is used when the headers carry none. Headers written by any Raindrop SDK work, and a child with no readable carrier still reports, as an unlinked event.
> * Only accept a handoff from code you trust: it decides which event the child's data is attributed to.
>
> Detached subagents are experimental.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.