@raindrop-ai/ai-sdk 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -190,6 +190,120 @@ Sub-agents (a `generateText`/`streamText` invoked from inside a tool's
190
190
  `execute`) nest automatically: the inner generation's spans are parented under
191
191
  the tool span so parent and child spans stay correctly nested.
192
192
 
193
+ ## Detached sub-agents
194
+
195
+ A **detached** sub-agent is different: it runs in another process (a queue
196
+ worker, a container, another machine), does not block its caller, and reports as
197
+ its own Raindrop event with its own lifecycle. Neither the trace nor the event id
198
+ survives that boundary, so the link is carried explicitly — the caller stamps it
199
+ on the span it dispatched from, the child stamps the reverse reference on every
200
+ span it emits, and a carrier travels with the job.
201
+
202
+ **Caller** — allocate the child's event id and get the headers to send:
203
+
204
+ ```ts
205
+ const rd = raindrop({ context: { userId: "user_123" } });
206
+ registerTelemetry(rd);
207
+
208
+ const launch_subagent = tool({
209
+ description: "Hand a task to a background sub-agent",
210
+ inputSchema: z.object({ task: z.string() }),
211
+ execute: async ({ task }) => {
212
+ const dispatch = rd.subagent({ name: "researcher", input: { task } });
213
+ await queue.send({ task, headers: dispatch.headers });
214
+ // Return a handle, not an answer — the answer is the child's to report.
215
+ return { jobId: dispatch.childEventId, status: "accepted" };
216
+ },
217
+ });
218
+ ```
219
+
220
+ The dispatch span is **your tool's span**, so it carries your tool's name — the
221
+ example above emits `launch_subagent` because that is what the tool is called
222
+ here, not because the SDK imposes it. `launch_subagent` is only the fallback
223
+ name, used when `subagent()` has to synthesize a dispatch span because no tool
224
+ call was open; pass `toolName` to choose your own. The sub-agent's own name
225
+ travels on `raindrop.handoff.name` either way, which is where readers group by
226
+ it.
227
+
228
+ `subagent()` allocates `childEventId` **before** dispatching, so the caller's
229
+ hand-off resolves while the job is still queued. It also opts the turn in to
230
+ producing an event: a turn whose entire content is launches finishes with
231
+ `finishReason: "tool-calls"` and no assistant text, and such turns are dropped as
232
+ intermediate steps by default. If you launch from outside a turn this SDK
233
+ instruments (the v4-v6 `wrap()` path), pass the opt-in yourself with
234
+ `eventMetadata({ toolEvents: "allow" })`.
235
+
236
+ The caller never writes the child's status. It returned a job handle and moved
237
+ on; status is derived from the child's own event.
238
+
239
+ **Child** — resume from the carrier and report under the id the caller allocated:
240
+
241
+ ```ts
242
+ const run = rd.subagents.resume(dispatch.headers);
243
+
244
+ try {
245
+ await generateText({
246
+ model: anthropic("claude-sonnet-4-5"),
247
+ prompt: job.task,
248
+ experimental_telemetry: { isEnabled: true, metadata: eventMetadata(run.metadata) },
249
+ });
250
+ } catch (err) {
251
+ // Only needed for a failure OUTSIDE a generation — one that throws already
252
+ // reports itself.
253
+ await run.fail(err);
254
+ }
255
+ ```
256
+
257
+ `run.metadata` carries the child's event id, identity, and the reverse reference,
258
+ which the integration stamps on every span the run emits. A cancellation is the
259
+ one outcome telemetry cannot show on its own (spans close, nothing errors, there
260
+ is just no answer), so state it with `run.cancel(reason)`; a generation cancelled
261
+ via its `AbortSignal` is marked automatically.
262
+
263
+ A child that ends without reporting anything produces no event at all, which
264
+ leaves its caller unable to tell a failed job from one that never started. The
265
+ SDK therefore records an abort reason as the run's output whenever a detached
266
+ child errors, aborts, or is cancelled.
267
+
268
+ Output alone is not enough for a failure, though. Status is derived from the
269
+ child's telemetry — a run with final output and no errored span reads as
270
+ `finished` — so `run.fail()` also marks the child's own spans as errored, and
271
+ records one when the job died before it reached the model and left nothing to
272
+ mark. `run.cancel()` deliberately does not: cancelled and failed are different
273
+ states, and error spans are what separate them.
274
+
275
+ `resume(headers)` returns a usable run even when there is no carrier — a child
276
+ invoked directly rather than dispatched is a legitimate state, not an error — so
277
+ call sites stay unconditional.
278
+
279
+ The carrier wins over anything local. `resume(headers, { convoId })` treats its
280
+ options as fallbacks for the no-carrier case, not overrides, because every field
281
+ the carrier supplies names something the caller already recorded: a child that
282
+ keeps its own convo id lands in a different conversation from the supervisor that
283
+ launched it, and one that keeps its own event id leaves the caller's reference
284
+ pointing at nothing.
285
+
286
+ > **Trust boundary.** A carrier names the event a child will be attributed to, so
287
+ > accepting one from an untrusted caller lets that caller write into another
288
+ > tenant's trace. Only read carriers that came from inside your own trust
289
+ > boundary.
290
+
291
+ `fromHeaders` also accepts LangSmith's `langsmith-trace` + `langsmith-metadata`
292
+ headers, so a service already propagating those links with no call-site change.
293
+
294
+ **Send every header `dispatch.headers` gives you, not just `traceparent`.** There
295
+ are three: the standard `traceparent` and `baggage` pair, which other readers
296
+ understand, and `x-raindrop-handoff`, which carries the same fields under a name
297
+ no propagator owns. The standard pair is not ours to rely on — an instrumented
298
+ HTTP client rewrites `traceparent` from its own client span, and OTel's baggage
299
+ propagator *replaces* `baggage` with the caller's own entries, so a caller
300
+ propagating something ordinary like a tenant id would otherwise strip every
301
+ hand-off identity and leave the child unlinked. The Raindrop header is read last
302
+ and wins; the standard pair remains the fallback for a carrier written by a
303
+ different sender. A carrier header that arrives **repeated with differing values**
304
+ is refused entirely, leaving an honestly unlinked child rather than a guess about
305
+ which value gets to name the event this process is attributed to.
306
+
193
307
  ### AI SDK v7+ native telemetry (advanced)
194
308
 
195
309
  For finer control you can build the integration from a `createRaindropAISDK`