@raindrop-ai/deep-agents 0.0.1

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 ADDED
@@ -0,0 +1,123 @@
1
+ # @raindrop-ai/deep-agents
2
+
3
+ Raindrop integration for [LangChain Deep Agents](https://docs.langchain.com/oss/javascript/deepagents/overview). Automatically captures LLM calls, tool usage (planning, filesystem, shell, subagent delegation), chains, and agent actions via LangChain's callback system.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @raindrop-ai/deep-agents @langchain/core deepagents
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { createDeepAgent } from "deepagents";
15
+ import { ChatAnthropic } from "@langchain/anthropic";
16
+ import { createRaindropDeepAgents } from "@raindrop-ai/deep-agents";
17
+
18
+ const raindrop = createRaindropDeepAgents({
19
+ writeKey: "your-write-key",
20
+ userId: "user-123",
21
+ convoId: "session-abc",
22
+ });
23
+
24
+ const agent = createDeepAgent({
25
+ model: new ChatAnthropic({ model: "claude-sonnet-4-20250514" }),
26
+ tools: [/* filesystem, shell, etc. */],
27
+ });
28
+
29
+ const result = await agent.invoke(
30
+ { messages: [{ role: "user", content: "Create a hello world file" }] },
31
+ { callbacks: [raindrop.handler] },
32
+ );
33
+
34
+ await raindrop.shutdown();
35
+ ```
36
+
37
+ ## What gets captured
38
+
39
+ - **LLM calls**: model name, input, output, token usage (`prompt_tokens`, `completion_tokens`)
40
+ - **Extended token categories**: `ai.usage.cached_tokens` (OpenAI prompt cache hits, Anthropic `cache_read`), `ai.usage.thoughts_tokens` (o1/o3 reasoning tokens) — when reported by the provider
41
+ - **Finish reason**: captured as `ai.finish_reason` in event properties (`"stop"`, `"length"`, `"tool_calls"`, `"content_filter"`, etc.)
42
+ - **Tool calls**: tool name (`write_todos`, `read_file`, `write_file`, `edit_file`, `ls`, `glob`, `grep`, `execute`, `task`), input, output
43
+ - **Chains**: execution spans with parent-child nesting
44
+ - **Agent actions**: tool selection and finish events
45
+ - **Multi-modal messages**: typed-parts content (`[{type: "text", text: "..."}, ...]`) is extracted cleanly — text-type parts are concatenated; non-text parts (images, audio) are skipped
46
+ - **Errors**: captured with `error.type` + `error.message` properties and `[Error] ...` output on the event, plus OTLP error status on the span
47
+
48
+ ## Options
49
+
50
+ | Option | Type | Default | Description |
51
+ |--------|------|---------|-------------|
52
+ | `writeKey` | `string` | - | Raindrop API write key (omit to disable telemetry) |
53
+ | `endpoint` | `string` | `https://api.raindrop.ai/v1/` | API endpoint |
54
+ | `userId` | `string` | - | Associate all events with a user |
55
+ | `convoId` | `string` | - | Group events into a conversation |
56
+ | `debug` | `boolean` | `false` | Enable verbose logging |
57
+ | `traceChains` | `boolean` | `true` | Create spans for chain execution |
58
+
59
+ ## LangGraph Support
60
+
61
+ Deep Agents is built on LangGraph. The handler automatically:
62
+ - Filters LangGraph-internal chain events (`__start__`, `__end__`, `ChannelWrite:*`, `ChannelRead:*`, `Branch:*`) — the colon is part of LangGraph's naming convention, user-defined chains whose names happen to start with "Branch" (e.g. `BranchRouter`) are NOT filtered
63
+ - Keeps the root `LangGraph` chain so the OTel association context is in place before any inner span fires (filtering it orphans all downstream spans)
64
+ - Deduplicates LLM callbacks that LangGraph fires multiple times with the same `runId`
65
+ - Links parent-child spans for chain→LLM→tool hierarchies
66
+
67
+ ## API Surface
68
+
69
+ The `createRaindropDeepAgents()` factory returns:
70
+
71
+ - `handler` — LangChain `BaseCallbackHandler` to pass via `config.callbacks`
72
+ - `lastEventId` — `event_id` of the most recently finalized root event (or `undefined` if none has landed). Use this to attach a follow-up `signals.track()` to the agent invocation you just ran without polling the dashboard. Example:
73
+ ```typescript
74
+ await agent.invoke({ messages: [...] }, { callbacks: [raindrop.handler] });
75
+ if (raindrop.lastEventId) {
76
+ await raindrop.signals.track({
77
+ eventId: raindrop.lastEventId,
78
+ name: "thumbs_up",
79
+ type: "feedback",
80
+ sentiment: "POSITIVE",
81
+ });
82
+ }
83
+ ```
84
+ - `events.patch(eventId, patch)` — update event properties
85
+ - `events.finish(eventId, patch)` — finalize an event
86
+ - `events.addAttachments(eventId, attachments)` — attach files/text to an event
87
+ - `events.setProperties(eventId, properties)` — set custom properties
88
+ - `users.identify(users)` — identify users with traits
89
+ - `signals.track(signal)` — track user signals/feedback
90
+ - `flush()` — flush pending telemetry
91
+ - `shutdown()` — flush and close connections
92
+
93
+ ## Known Limitations
94
+
95
+ - **Beta status**: API surface may change in future releases.
96
+ - **Deep Agents version**: Requires `deepagents >= 0.5.0`.
97
+ - **Streaming**: Token-by-token streaming events are not captured individually; only the final aggregated response is tracked.
98
+ - **Subagent isolation**: When using the `task` tool for subagent delegation, each subagent's callbacks fire independently.
99
+ - **Concurrent invocations on a shared handler**: A single handler instance keeps one in-flight span map at a time. Running multiple `agent.invoke(...)` calls **concurrently with the same handler** (e.g. `Promise.all([agent.invoke(...), agent.invoke(...)])` with the same `raindrop.handler`) can scramble the linkage between events and traces. Instantiate one `createRaindropDeepAgents()` per concurrent request; sequential invocations on a shared handler are fully supported.
100
+ - **Long chain inputs/outputs are truncated**: Chain-level `input` and `output` captured on the root event are truncated to ~8 KB to stay within the SDK's payload-size limit. Per-LLM child events still carry full prompts.
101
+
102
+ ## Testing
103
+
104
+ ```bash
105
+ # Unit tests (no external services; use MSW to intercept HTTP)
106
+ pnpm test -- tests/handler.test.ts
107
+ ```
108
+
109
+ **E2E tests** run REAL Deep Agents workflows via `createDeepAgent()` against a REAL LLM (OpenAI gpt-4o-mini) and verify events / tool calls / traces against the production dashboard TRPC API. They skip automatically when any of `RAINDROP_WRITE_KEY`, `OPENAI_API_KEY`, or `RAINDROP_DASHBOARD_TOKEN` is missing.
110
+
111
+ ```bash
112
+ # E2E — requires all three keys
113
+ RAINDROP_WRITE_KEY=xxx \
114
+ OPENAI_API_KEY=sk-... \
115
+ RAINDROP_DASHBOARD_TOKEN=eyJ... \
116
+ pnpm test -- tests/e2e.test.ts
117
+ ```
118
+
119
+ The dashboard token comes from `app.raindrop.ai` → DevTools → Network → any `backend.raindrop.ai` request → `Authorization: Bearer ...` (expires every ~30 min, so grab a fresh one immediately before running).
120
+
121
+ ## Python
122
+
123
+ A Python version of this integration is available as `raindrop-deep-agents` on PyPI. See [the Python package](https://github.com/invisible-tools/dawn/tree/main/packages/deep-agents-python) for details.
@@ -0,0 +1,365 @@
1
+ import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
2
+ import { Serialized } from '@langchain/core/load/serializable';
3
+ import { LLMResult } from '@langchain/core/outputs';
4
+ import { BaseMessage } from '@langchain/core/messages';
5
+ import { AgentAction, AgentFinish } from '@langchain/core/agents';
6
+ import { ChainValues } from '@langchain/core/utils/types';
7
+
8
+ type OtlpAnyValue = {
9
+ stringValue?: string;
10
+ intValue?: string;
11
+ doubleValue?: number;
12
+ boolValue?: boolean;
13
+ arrayValue?: {
14
+ values: OtlpAnyValue[];
15
+ };
16
+ };
17
+ type OtlpKeyValue = {
18
+ key: string;
19
+ value: OtlpAnyValue;
20
+ };
21
+ declare const SpanStatusCode: {
22
+ readonly UNSET: 0;
23
+ readonly OK: 1;
24
+ readonly ERROR: 2;
25
+ };
26
+ type OtlpSpanStatus = {
27
+ code: (typeof SpanStatusCode)[keyof typeof SpanStatusCode] | number;
28
+ message?: string;
29
+ };
30
+ type OtlpSpan = {
31
+ traceId: string;
32
+ spanId: string;
33
+ parentSpanId?: string;
34
+ name: string;
35
+ startTimeUnixNano: string;
36
+ endTimeUnixNano: string;
37
+ attributes?: OtlpKeyValue[];
38
+ status?: OtlpSpanStatus;
39
+ };
40
+ type SpanIds = {
41
+ traceIdB64: string;
42
+ spanIdB64: string;
43
+ parentSpanIdB64?: string;
44
+ };
45
+
46
+ type Attachment = {
47
+ type: string;
48
+ role: string;
49
+ name?: string;
50
+ value: string;
51
+ };
52
+ type IdentifyInput = {
53
+ userId: string;
54
+ traits?: Record<string, unknown>;
55
+ };
56
+ type Patch = {
57
+ eventName?: string;
58
+ userId?: string;
59
+ convoId?: string;
60
+ input?: string;
61
+ output?: string;
62
+ model?: string;
63
+ properties?: Record<string, unknown>;
64
+ attachments?: Attachment[];
65
+ isPending?: boolean;
66
+ timestamp?: string;
67
+ };
68
+ type SignalInput = {
69
+ eventId: string;
70
+ name: string;
71
+ type?: "default" | "feedback" | "edit" | "standard" | "agent" | "agent_internal";
72
+ sentiment?: "POSITIVE" | "NEGATIVE";
73
+ timestamp?: string;
74
+ properties?: Record<string, unknown>;
75
+ attachmentId?: string;
76
+ comment?: string;
77
+ after?: string;
78
+ };
79
+ type EventShipperOptions = {
80
+ writeKey?: string;
81
+ endpoint?: string;
82
+ enabled?: boolean;
83
+ debug: boolean;
84
+ partialFlushMs?: number;
85
+ sdkName?: string;
86
+ libraryName?: string;
87
+ libraryVersion?: string;
88
+ defaultEventName?: string;
89
+ };
90
+ declare class EventShipper {
91
+ private baseUrl;
92
+ private writeKey?;
93
+ private enabled;
94
+ private debug;
95
+ private partialFlushMs;
96
+ private sdkName;
97
+ private prefix;
98
+ private defaultEventName;
99
+ private context;
100
+ private buffers;
101
+ private sticky;
102
+ private timers;
103
+ private inFlight;
104
+ constructor(opts: EventShipperOptions);
105
+ isDebugEnabled(): boolean;
106
+ private authHeaders;
107
+ patch(eventId: string, patch: Patch): Promise<void>;
108
+ finish(eventId: string, patch: {
109
+ output?: string;
110
+ model?: string;
111
+ properties?: Record<string, unknown>;
112
+ userId?: string;
113
+ }): Promise<void>;
114
+ flush(): Promise<void>;
115
+ shutdown(): Promise<void>;
116
+ trackSignal(signal: SignalInput): Promise<void>;
117
+ identify(users: IdentifyInput | IdentifyInput[]): Promise<void>;
118
+ private flushOne;
119
+ }
120
+
121
+ type InternalSpan = {
122
+ ids: SpanIds;
123
+ name: string;
124
+ startTimeUnixNano: string;
125
+ endTimeUnixNano?: string;
126
+ attributes: Array<OtlpKeyValue | undefined>;
127
+ };
128
+ type TraceShipperOptions = {
129
+ writeKey?: string;
130
+ endpoint?: string;
131
+ enabled?: boolean;
132
+ debug: boolean;
133
+ debugSpans?: boolean;
134
+ flushIntervalMs?: number;
135
+ maxBatchSize?: number;
136
+ maxQueueSize?: number;
137
+ sdkName?: string;
138
+ serviceName?: string;
139
+ serviceVersion?: string;
140
+ };
141
+ declare class TraceShipper {
142
+ private baseUrl;
143
+ private writeKey?;
144
+ private enabled;
145
+ private debug;
146
+ private debugSpans;
147
+ private sdkName;
148
+ private prefix;
149
+ private serviceName;
150
+ private serviceVersion;
151
+ private flushIntervalMs;
152
+ private maxBatchSize;
153
+ private maxQueueSize;
154
+ private queue;
155
+ private timer;
156
+ private inFlight;
157
+ /** URL of the local debugger (from RAINDROP_LOCAL_DEBUGGER env var). */
158
+ private localDebuggerUrl;
159
+ constructor(opts: TraceShipperOptions);
160
+ isDebugEnabled(): boolean;
161
+ private authHeaders;
162
+ startSpan(args: {
163
+ name: string;
164
+ parent?: {
165
+ traceIdB64: string;
166
+ spanIdB64: string;
167
+ };
168
+ eventId: string;
169
+ operationId?: string;
170
+ attributes?: Array<OtlpKeyValue | undefined>;
171
+ startTimeUnixNano?: string;
172
+ }): InternalSpan;
173
+ endSpan(span: InternalSpan, extra?: {
174
+ attributes?: InternalSpan["attributes"];
175
+ error?: unknown;
176
+ status?: OtlpSpanStatus;
177
+ endTimeUnixNano?: string;
178
+ }): void;
179
+ createSpan(args: {
180
+ name: string;
181
+ parent?: {
182
+ traceIdB64: string;
183
+ spanIdB64: string;
184
+ };
185
+ eventId: string;
186
+ startTimeUnixNano: string;
187
+ endTimeUnixNano: string;
188
+ attributes?: Array<OtlpKeyValue | undefined>;
189
+ status?: OtlpSpanStatus;
190
+ }): void;
191
+ enqueue(span: OtlpSpan): void;
192
+ flush(): Promise<void>;
193
+ shutdown(): Promise<void>;
194
+ }
195
+
196
+ type ParentSpanContext = {
197
+ traceIdB64: string;
198
+ spanIdB64: string;
199
+ eventId: string;
200
+ };
201
+ interface ContextSpan {
202
+ readonly traceIdB64: string;
203
+ readonly spanIdB64: string;
204
+ readonly eventId: string;
205
+ log?(data: Record<string, unknown>): void;
206
+ }
207
+ interface AsyncLocalStorageLike<T> {
208
+ getStore(): T | undefined;
209
+ run<R>(store: T, callback: () => R): R;
210
+ enterWith?(store: T): void;
211
+ }
212
+ declare abstract class ContextManager {
213
+ abstract getParentSpanIds(): ParentSpanContext | undefined;
214
+ abstract runInContext<R>(span: ContextSpan, callback: () => R): R;
215
+ abstract getCurrentSpan(): ContextSpan | undefined;
216
+ abstract isReady(): boolean;
217
+ }
218
+ declare global {
219
+ var RAINDROP_CONTEXT_MANAGER: (new () => ContextManager) | undefined;
220
+ var RAINDROP_ASYNC_LOCAL_STORAGE: (new <T>() => AsyncLocalStorageLike<T>) | undefined;
221
+ }
222
+
223
+ interface RaindropDeepAgentsHandlerOptions {
224
+ eventShipper: EventShipper;
225
+ traceShipper: TraceShipper;
226
+ userId?: string;
227
+ convoId?: string;
228
+ traceChains?: boolean;
229
+ }
230
+ declare class RaindropDeepAgentsHandler extends BaseCallbackHandler {
231
+ name: string;
232
+ private eventShipper;
233
+ private traceShipper;
234
+ private userId?;
235
+ private convoId?;
236
+ private traceChains;
237
+ private spans;
238
+ private rootRunIds;
239
+ private eventIds;
240
+ private forwardedSpans;
241
+ /**
242
+ * event_id of the most recently finalized ROOT event. Exposed via
243
+ * ``RaindropDeepAgentsClient.lastEventId`` so callers can attach a
244
+ * follow-up signal to the agent invocation they just ran without
245
+ * polling the dashboard. Persists across runs; not reset on cleanup.
246
+ */
247
+ _lastRootEventId: string | undefined;
248
+ constructor(opts: RaindropDeepAgentsHandlerOptions);
249
+ private getEventId;
250
+ private getParent;
251
+ private cleanup;
252
+ private finalizeEventIfRoot;
253
+ handleLLMStart(llm: Serialized, prompts: string[], runId: string, parentRunId?: string, _extraParams?: Record<string, unknown>, tags?: string[], metadata?: Record<string, unknown>, runName?: string): Promise<void>;
254
+ handleChatModelStart(llm: Serialized, messages: BaseMessage[][], runId: string, parentRunId?: string, _extraParams?: Record<string, unknown>, tags?: string[], metadata?: Record<string, unknown>, runName?: string): Promise<void>;
255
+ handleLLMEnd(output: LLMResult, runId: string, _parentRunId?: string): Promise<void>;
256
+ handleLLMError(err: unknown, runId: string): Promise<void>;
257
+ handleChainStart(chain: Serialized, _inputs: ChainValues, runId: string, parentRunId?: string, _tags?: string[], _metadata?: Record<string, unknown>, _runType?: string, runName?: string): Promise<void>;
258
+ handleChainEnd(_outputs: ChainValues, runId: string): Promise<void>;
259
+ handleChainError(err: unknown, runId: string): Promise<void>;
260
+ handleToolStart(tool: Serialized, input: string, runId: string, parentRunId?: string, _tags?: string[], _metadata?: Record<string, unknown>, runName?: string): Promise<void>;
261
+ handleToolEnd(output: unknown, runId: string): Promise<void>;
262
+ handleToolError(err: unknown, runId: string): Promise<void>;
263
+ handleAgentAction(action: AgentAction, runId: string, parentRunId?: string): Promise<void>;
264
+ handleAgentEnd(_action: AgentFinish, runId: string): Promise<void>;
265
+ }
266
+
267
+ interface DeepAgentsOptions {
268
+ /**
269
+ * API write key. If omitted, telemetry shipping is disabled but the handler
270
+ * is still available for a consistent integration surface.
271
+ */
272
+ writeKey?: string;
273
+ endpoint?: string;
274
+ debug?: boolean;
275
+ userId?: string;
276
+ convoId?: string;
277
+ traceChains?: boolean;
278
+ }
279
+ type RaindropDeepAgentsClient = {
280
+ /**
281
+ * LangChain callback handler to pass to Deep Agents via `config.callbacks`.
282
+ *
283
+ * @example
284
+ * ```typescript
285
+ * const raindrop = createRaindropDeepAgents({ writeKey: "..." });
286
+ * const agent = createDeepAgent({ tools: [...] });
287
+ * const result = await agent.invoke(
288
+ * { messages: [{ role: "user", content: "Hello" }] },
289
+ * { callbacks: [raindrop.handler] },
290
+ * );
291
+ * await raindrop.shutdown();
292
+ * ```
293
+ */
294
+ handler: RaindropDeepAgentsHandler;
295
+ /**
296
+ * `event_id` of the most recently finalized ROOT event, or `undefined`
297
+ * if no root event has landed yet. Use this to attach a feedback
298
+ * `signals.track()` to the agent invocation you just ran, without
299
+ * having to poll the dashboard for the real id.
300
+ *
301
+ * @example
302
+ * ```typescript
303
+ * const raindrop = createRaindropDeepAgents({ writeKey: "..." });
304
+ * await agent.invoke({ messages: [...] }, { callbacks: [raindrop.handler] });
305
+ * if (raindrop.lastEventId) {
306
+ * await raindrop.signals.track({
307
+ * eventId: raindrop.lastEventId,
308
+ * name: "thumbs_up",
309
+ * type: "feedback",
310
+ * sentiment: "POSITIVE",
311
+ * });
312
+ * }
313
+ * ```
314
+ */
315
+ readonly lastEventId: string | undefined;
316
+ events: {
317
+ patch(eventId: string, patch: Patch): Promise<void>;
318
+ finish(eventId: string, patch: {
319
+ output?: string;
320
+ model?: string;
321
+ properties?: Record<string, unknown>;
322
+ }): Promise<void>;
323
+ addAttachments(eventId: string, attachments: Attachment[]): Promise<void>;
324
+ setProperties(eventId: string, properties: Record<string, unknown>): Promise<void>;
325
+ };
326
+ users: {
327
+ identify(users: IdentifyInput | IdentifyInput[]): Promise<void>;
328
+ };
329
+ signals: {
330
+ track(signal: SignalInput): Promise<void>;
331
+ };
332
+ flush(): Promise<void>;
333
+ shutdown(): Promise<void>;
334
+ };
335
+ /**
336
+ * Create a Raindrop client for instrumenting LangChain Deep Agents.
337
+ *
338
+ * Deep Agents is built on LangChain and LangGraph. This integration provides
339
+ * a callback handler that captures LLM calls, tool usage (write_todos,
340
+ * read_file, write_file, edit_file, task, execute, etc.), chains, and agent
341
+ * actions — shipping them to Raindrop for observability.
342
+ *
343
+ * @example
344
+ * ```typescript
345
+ * import { createDeepAgent } from "deepagents";
346
+ * import { createRaindropDeepAgents } from "@raindrop-ai/deep-agents";
347
+ *
348
+ * const raindrop = createRaindropDeepAgents({
349
+ * writeKey: process.env.RAINDROP_API_KEY,
350
+ * userId: "user-123",
351
+ * convoId: "convo-456",
352
+ * });
353
+ *
354
+ * const agent = createDeepAgent({ tools: [myTool] });
355
+ * const result = await agent.invoke(
356
+ * { messages: [{ role: "user", content: "Research LangGraph" }] },
357
+ * { callbacks: [raindrop.handler] },
358
+ * );
359
+ *
360
+ * await raindrop.shutdown();
361
+ * ```
362
+ */
363
+ declare function createRaindropDeepAgents(opts: DeepAgentsOptions): RaindropDeepAgentsClient;
364
+
365
+ export { type DeepAgentsOptions, type RaindropDeepAgentsClient, RaindropDeepAgentsHandler, createRaindropDeepAgents };