@dawn-ai/langchain 0.1.7 → 0.2.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Dawn contributors
3
+ Copyright (c) Brian Love
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,12 +1,26 @@
1
- <p>
1
+ <p align="center">
2
2
  <img src="https://raw.githubusercontent.com/cacheplane/dawnai/main/docs/brand/dawn-logo-horizontal-black-on-white.png" alt="Dawn" width="180" />
3
3
  </p>
4
4
 
5
5
  # @dawn-ai/langchain
6
6
 
7
- LangChain backend adapters Dawn uses to materialize `chain` and `agent` routes (tool conversion, streaming, retry).
7
+ LangChain backend adapters Dawn uses to materialize `chain` routes and provider-aware `agent` routes (tool conversion, streaming, retry).
8
8
 
9
- This is an internal Dawn workspace package. For Dawn documentation, see <https://dawn-ai.org>.
9
+ `agent()` materialization resolves a LangChain chat model from the route descriptor. Dawn includes `@langchain/openai` for the default/backcompat path and lazy-loads optional provider packages when an agent selects or infers another provider.
10
+
11
+ Install optional provider integrations in applications as needed:
12
+
13
+ ```bash
14
+ pnpm add @langchain/anthropic # anthropic
15
+ pnpm add @langchain/google-genai # google
16
+ pnpm add @langchain/mistralai # mistral
17
+ pnpm add @langchain/groq # groq
18
+ pnpm add @langchain/ollama # ollama
19
+ pnpm add @langchain/xai # xai
20
+ pnpm add @langchain/openrouter # openrouter
21
+ ```
22
+
23
+ This is an internal Dawn workspace package. For Dawn documentation, see <https://github.com/cacheplane/dawnai/tree/main/apps/web/content/docs>.
10
24
 
11
25
  ## License
12
26
 
@@ -1,6 +1,11 @@
1
- import type { RetryConfig } from "@dawn-ai/sdk";
1
+ import type { PromptFragment, StreamTransformer } from "@dawn-ai/core";
2
+ import type { DawnAgent, RetryConfig } from "@dawn-ai/sdk";
3
+ import { type BaseMessageLike } from "@langchain/core/messages";
4
+ import type { BaseCheckpointSaver } from "@langchain/langgraph-checkpoint";
2
5
  import { type ResolvedStateField } from "./state-adapter.js";
3
- interface DawnToolDefinition {
6
+ import { type SubagentResolverResult } from "./subagent-tool-bridge.js";
7
+ export type SubagentResolver = (leafName: string) => SubagentResolverResult | undefined;
8
+ export interface DawnToolDefinition {
4
9
  readonly description?: string;
5
10
  readonly name: string;
6
11
  readonly run: (input: unknown, context: {
@@ -9,12 +14,33 @@ interface DawnToolDefinition {
9
14
  }) => Promise<unknown> | unknown;
10
15
  readonly schema?: unknown;
11
16
  }
17
+ export declare function composePromptMessages(systemPrompt: string, promptFragments: readonly PromptFragment[], state: Record<string, unknown>): BaseMessageLike[];
18
+ export declare function materializeAgentGraph(options: {
19
+ readonly checkpointer: BaseCheckpointSaver;
20
+ readonly descriptor: DawnAgent;
21
+ readonly tools?: readonly DawnToolDefinition[];
22
+ readonly stateFields?: readonly ResolvedStateField[];
23
+ readonly promptFragments?: readonly PromptFragment[];
24
+ }): Promise<unknown>;
12
25
  export interface AgentStreamChunk {
13
- readonly type: "token" | "tool_call" | "tool_result" | "done";
26
+ readonly type: "token" | "tool_call" | "tool_result" | "interrupt" | "done" | (string & {});
14
27
  readonly data: unknown;
15
28
  }
16
29
  export interface AgentOptions {
30
+ /**
31
+ * Checkpointer used by LangGraph to park interrupted graph state and replay
32
+ * from it on resume. Required — the CLI runtime supplies a SQLite-backed
33
+ * instance by default. If you call agent-adapter directly (e.g. in tests),
34
+ * pass `new MemorySaver()` from `@langchain/langgraph`.
35
+ */
36
+ readonly checkpointer: BaseCheckpointSaver;
17
37
  readonly entry: unknown;
38
+ /**
39
+ * The agent input. For a normal invocation, this is a record like
40
+ * `{messages: [...]}`. For a resume invocation (after a parked interrupt),
41
+ * pass a `Command({resume: decision})` instance directly — the adapter will
42
+ * forward it verbatim to `streamEvents` instead of wrapping it in messages.
43
+ */
18
44
  readonly input: unknown;
19
45
  readonly middlewareContext?: Readonly<Record<string, unknown>>;
20
46
  readonly retry?: RetryConfig;
@@ -22,8 +48,25 @@ export interface AgentOptions {
22
48
  readonly signal: AbortSignal;
23
49
  readonly stateFields?: readonly ResolvedStateField[];
24
50
  readonly tools: readonly DawnToolDefinition[];
51
+ readonly promptFragments?: readonly PromptFragment[];
52
+ readonly streamTransformers?: readonly StreamTransformer[];
53
+ /**
54
+ * Resolves a subagent leaf name to a child graph + routeId. When set, the
55
+ * `task` tool contributed by the subagents capability marker is intercepted
56
+ * inside `streamFromRunnable` and replaced with a bridge that dispatches the
57
+ * call via `dispatchSubagent`. Emitted `subagent.*` events are queued and
58
+ * drained alongside normal stream chunks (no module-level mutable state).
59
+ */
60
+ readonly subagentResolver?: SubagentResolver;
61
+ /**
62
+ * Stable per-conversation identifier used as LangGraph's `thread_id`. When
63
+ * set, the agent-adapter wires it into `config.configurable.thread_id` so
64
+ * the checkpointer can park interrupted state. Required for resume to work
65
+ * — without a thread_id, an interrupt ends the stream with no way to
66
+ * replay.
67
+ */
68
+ readonly threadId?: string;
25
69
  }
26
70
  export declare function executeAgent(options: AgentOptions): Promise<unknown>;
27
71
  export declare function streamAgent(options: AgentOptions): AsyncGenerator<AgentStreamChunk>;
28
- export {};
29
72
  //# sourceMappingURL=agent-adapter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent-adapter.d.ts","sourceRoot":"","sources":["../src/agent-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAa,WAAW,EAAE,MAAM,cAAc,CAAA;AAI1D,OAAO,EAA0B,KAAK,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAGpF,UAAU,kBAAkB;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,EAAE,CACZ,KAAK,EAAE,OAAO,EACd,OAAO,EAAE;QACP,QAAQ,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;QACvD,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;KAC7B,KACE,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAA;IAC/B,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAC1B;AAwDD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,WAAW,GAAG,aAAa,GAAG,MAAM,CAAA;IAC7D,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,QAAQ,CAAC,iBAAiB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IAC9D,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAA;IAC5B,QAAQ,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAA;IAC3C,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;IAC5B,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAA;IACpD,QAAQ,CAAC,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAA;CAC9C;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,CAQ1E;AAED,wBAAuB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,cAAc,CAAC,gBAAgB,CAAC,CA4B1F"}
1
+ {"version":3,"file":"agent-adapter.d.ts","sourceRoot":"","sources":["../src/agent-adapter.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,12 @@
1
1
  import { isDawnAgent } from "@dawn-ai/sdk";
2
2
  import { HumanMessage } from "@langchain/core/messages";
3
+ import { Command } from "@langchain/langgraph";
4
+ import { createChatModel } from "./chat-model-factory.js";
5
+ import { resolveProvider } from "./model-provider-resolver.js";
3
6
  import { isRetryableError, withRetry } from "./retry.js";
4
7
  import { materializeStateSchema } from "./state-adapter.js";
8
+ import { createSubagentStreamContext, } from "./subagent-dispatcher.js";
9
+ import { bridgeSubagentTool } from "./subagent-tool-bridge.js";
5
10
  import { convertToolToLangChain } from "./tool-converter.js";
6
11
  function assertAgentLike(entry) {
7
12
  if (typeof entry !== "object" ||
@@ -11,31 +16,177 @@ function assertAgentLike(entry) {
11
16
  throw new Error("Agent entry must expose invoke(input) — expected a LangChain agent");
12
17
  }
13
18
  }
19
+ // Cache keyed on descriptor only. Assumption: a given descriptor is always
20
+ // invoked with the same capability contributions (prompt fragments come from
21
+ // the route directory, which is stable per descriptor). If that assumption
22
+ // changes, the cache key must include a hash of the fragments/transformers.
14
23
  const materializedAgents = new WeakMap();
15
- async function materializeAgent(descriptor, tools, stateFields, middlewareContext) {
16
- const cached = materializedAgents.get(descriptor);
17
- if (cached) {
18
- return cached;
24
+ export function composePromptMessages(systemPrompt, promptFragments, state) {
25
+ const rendered = promptFragments
26
+ .filter((f) => f.placement === "after_user_prompt")
27
+ .map((f) => f.render(state))
28
+ .filter((s) => s.length > 0);
29
+ const composed = [systemPrompt, ...rendered].join("\n\n");
30
+ const messages = Array.isArray(state.messages) ? state.messages : [];
31
+ return [{ role: "system", content: composed }, ...messages];
32
+ }
33
+ async function materializeAgent(descriptor, tools, checkpointer, stateFields, middlewareContext, promptFragments, options) {
34
+ if (!options?.bypassCache) {
35
+ const cached = materializedAgents.get(descriptor);
36
+ if (cached) {
37
+ return cached;
38
+ }
19
39
  }
20
40
  const { createReactAgent } = await import("@langchain/langgraph/prebuilt");
21
- const { ChatOpenAI } = await import("@langchain/openai");
22
41
  const langchainTools = tools.map((tool) => convertToolToLangChain(tool, middlewareContext));
23
- const llm = new ChatOpenAI({
42
+ const provider = resolveProvider({
24
43
  model: descriptor.model,
44
+ ...(descriptor.provider !== undefined ? { provider: descriptor.provider } : {}),
25
45
  });
46
+ const llm = await createChatModel({
47
+ model: descriptor.model,
48
+ provider,
49
+ ...(descriptor.reasoning ? { reasoning: descriptor.reasoning } : {}),
50
+ });
51
+ const fragments = promptFragments ?? [];
26
52
  const agentOptions = {
27
53
  llm,
28
54
  tools: langchainTools,
29
- prompt: descriptor.systemPrompt,
55
+ // Function-form prompt re-renders fragments on every model turn so they
56
+ // can reflect live state (e.g., the current todos list).
57
+ prompt: fragments.length > 0
58
+ ? (state) => composePromptMessages(descriptor.systemPrompt, fragments, state)
59
+ : descriptor.systemPrompt,
60
+ // Required so `interrupt()` can park graph state and `Command({resume})`
61
+ // can replay it. Paired with `config.configurable.thread_id`.
62
+ checkpointer,
30
63
  };
31
64
  if (stateFields && stateFields.length > 0) {
32
65
  agentOptions.stateSchema = materializeStateSchema(stateFields);
33
66
  }
34
67
  // biome-ignore lint/suspicious/noExplicitAny: dynamically-built options don't satisfy strict StateDefinition type
35
68
  const compiled = createReactAgent(agentOptions);
36
- materializedAgents.set(descriptor, compiled);
69
+ if (!options?.bypassCache) {
70
+ materializedAgents.set(descriptor, compiled);
71
+ }
37
72
  return compiled;
38
73
  }
74
+ export async function materializeAgentGraph(options) {
75
+ return materializeAgent(options.descriptor, options.tools ?? [], options.checkpointer, options.stateFields, undefined, options.promptFragments);
76
+ }
77
+ /**
78
+ * LangGraph 1.x's `interrupt()` throws a `GraphInterrupt` from inside the tool
79
+ * node. Under `streamEvents` v2 this surfaces as an `on_tool_error` whose
80
+ * `event.data.error` is the `GraphInterrupt` instance — its `.name` is
81
+ * `"GraphInterrupt"` and its `.interrupts` array carries the `{ id, value }`
82
+ * entries we need. The top-level `on_chain_end` for `LangGraph` does NOT
83
+ * include `__interrupt__` in this code path (that key appears only on the
84
+ * `invoke`/`stream` return value), so detection must happen at the tool error.
85
+ *
86
+ * We still keep the `__interrupt__` extractor for `on_chain_end` as a
87
+ * defensive fallback in case a future LangGraph version surfaces interrupts
88
+ * via the chain output too.
89
+ */
90
+ const INTERRUPT_KEY = "__interrupt__";
91
+ function extractInterrupts(output) {
92
+ if (!output || typeof output !== "object")
93
+ return undefined;
94
+ const maybe = output[INTERRUPT_KEY];
95
+ if (!Array.isArray(maybe))
96
+ return undefined;
97
+ return maybe;
98
+ }
99
+ /**
100
+ * Detects a thrown `GraphInterrupt` surfaced via `on_tool_error`.
101
+ *
102
+ * LangGraph's `interrupt()` throws a `GraphInterrupt` whose `.message` is
103
+ * `JSON.stringify(interrupts)` and whose `.interrupts` array carries the
104
+ * `{ id, value }` entries. By the time the error reaches `streamEvents`'
105
+ * `data.error` it has already been stringified — typically into
106
+ * `<JSON interrupts>\n\nGraphInterrupt: <JSON interrupts>\n at ...stack`.
107
+ *
108
+ * We handle three shapes defensively:
109
+ * - object with `.name === "GraphInterrupt"` and `.interrupts` array
110
+ * (in case a future LangGraph version surfaces the live error)
111
+ * - object/Error whose stringified message starts with a JSON array
112
+ * - bare string with the `GraphInterrupt:` marker
113
+ */
114
+ function extractInterruptsFromError(error) {
115
+ if (!error)
116
+ return undefined;
117
+ if (typeof error === "object") {
118
+ const e = error;
119
+ if (e.name === "GraphInterrupt" && Array.isArray(e.interrupts) && e.interrupts.length > 0) {
120
+ return e.interrupts;
121
+ }
122
+ if (typeof e.message === "string") {
123
+ const parsed = parseInterruptStringMessage(e.message);
124
+ if (parsed)
125
+ return parsed;
126
+ }
127
+ }
128
+ if (typeof error === "string") {
129
+ const parsed = parseInterruptStringMessage(error);
130
+ if (parsed)
131
+ return parsed;
132
+ }
133
+ return undefined;
134
+ }
135
+ /**
136
+ * Parses the stringified form of a GraphInterrupt's message. The string
137
+ * begins with `JSON.stringify(interrupts, null, 2)` and is followed by
138
+ * `\n\nGraphInterrupt: ...\n at ...` stack metadata. We slice the leading
139
+ * JSON array up to the first `]` followed by a newline + non-JSON sentinel
140
+ * and parse it.
141
+ */
142
+ function parseInterruptStringMessage(text) {
143
+ const trimmed = text.trimStart();
144
+ if (!trimmed.startsWith("["))
145
+ return undefined;
146
+ // Find the matching closing bracket by bracket counting at depth 0 — robust
147
+ // against nested arrays in the interrupt payloads.
148
+ let depth = 0;
149
+ let inString = false;
150
+ let escaped = false;
151
+ let end = -1;
152
+ for (let i = 0; i < trimmed.length; i++) {
153
+ const ch = trimmed[i];
154
+ if (escaped) {
155
+ escaped = false;
156
+ continue;
157
+ }
158
+ if (inString) {
159
+ if (ch === "\\")
160
+ escaped = true;
161
+ else if (ch === '"')
162
+ inString = false;
163
+ continue;
164
+ }
165
+ if (ch === '"')
166
+ inString = true;
167
+ else if (ch === "[")
168
+ depth++;
169
+ else if (ch === "]") {
170
+ depth--;
171
+ if (depth === 0) {
172
+ end = i;
173
+ break;
174
+ }
175
+ }
176
+ }
177
+ if (end === -1)
178
+ return undefined;
179
+ const json = trimmed.slice(0, end + 1);
180
+ try {
181
+ const parsed = JSON.parse(json);
182
+ if (!Array.isArray(parsed) || parsed.length === 0)
183
+ return undefined;
184
+ return parsed;
185
+ }
186
+ catch {
187
+ return undefined;
188
+ }
189
+ }
39
190
  export async function executeAgent(options) {
40
191
  let result;
41
192
  for await (const chunk of streamAgent(options)) {
@@ -46,22 +197,66 @@ export async function executeAgent(options) {
46
197
  return result;
47
198
  }
48
199
  export async function* streamAgent(options) {
200
+ if (!options.checkpointer) {
201
+ throw new Error("[dawn] agent-adapter requires a checkpointer in AgentOptions. The CLI runtime instantiates sqliteCheckpointer by default; if you're calling agent-adapter directly, pass one explicitly.");
202
+ }
203
+ // If the caller is passing a Command directly (resume path), forward it
204
+ // verbatim without the usual input preparation and message extraction.
205
+ const isCommandInput = options.input instanceof Command;
49
206
  const { agentInput, config } = prepareAgentCall(options);
50
- const messages = extractMessages(agentInput);
207
+ const messages = isCommandInput ? [] : extractMessages(agentInput);
208
+ // Per-call subagent event queue. The bridge's writer pushes here; the
209
+ // streaming generator drains the queue alongside normal stream chunks. This
210
+ // avoids the module-level mutable-writer anti-pattern: each call has its
211
+ // own queue scoped to the surrounding generator frame.
212
+ const subagentEvents = [];
213
+ const queueWriter = (event) => {
214
+ subagentEvents.push({
215
+ type: event.event,
216
+ data: event.data,
217
+ });
218
+ };
219
+ // Per-call counter shared with the dispatcher. While a child is active, the
220
+ // parent's on_chat_model_stream events are suppressed (LangChain v2
221
+ // streamEvents propagates child events to the parent listener via
222
+ // async-local-storage tracing, so without this gate every child token
223
+ // appears twice on the parent stream: once as a raw token chunk and once
224
+ // wrapped in a subagent.message envelope).
225
+ const streamContext = createSubagentStreamContext();
226
+ const resolver = options.subagentResolver;
227
+ const hasTaskTool = options.tools.some((t) => t.name === "task");
228
+ const effectiveTools = resolver && hasTaskTool
229
+ ? options.tools.map((t) => t.name === "task"
230
+ ? {
231
+ ...t,
232
+ run: bridgeSubagentTool({
233
+ subagentResolver: resolver,
234
+ writer: queueWriter,
235
+ parentConfig: config,
236
+ streamContext,
237
+ }).run,
238
+ }
239
+ : t)
240
+ : options.tools;
51
241
  // DawnAgent descriptor path — materialize on first use
52
242
  if (isDawnAgent(options.entry)) {
53
- const materializedAgent = await materializeAgent(options.entry, options.tools, options.stateFields, options.middlewareContext);
243
+ // Bypass the per-descriptor cache when a resolver is wired: the bridged
244
+ // tool closes over the per-call queue + parent config, so caching would
245
+ // bind those to a single call.
246
+ const materializedAgent = await materializeAgent(options.entry, effectiveTools, options.checkpointer, options.stateFields, options.middlewareContext, options.promptFragments, resolver && hasTaskTool ? { bypassCache: true } : undefined);
54
247
  const retryConfig = options.entry.retry;
55
- yield* streamFromRunnable(materializedAgent, { messages }, config, retryConfig);
248
+ const runnableInput = isCommandInput ? options.input : { messages };
249
+ yield* streamFromRunnable(materializedAgent, runnableInput, config, retryConfig, options.streamTransformers, subagentEvents, streamContext);
56
250
  return;
57
251
  }
58
252
  // Legacy path — raw Runnable with .invoke()
59
253
  assertAgentLike(options.entry);
60
- const langchainTools = options.tools.map((tool) => convertToolToLangChain(tool, options.middlewareContext));
254
+ const langchainTools = effectiveTools.map((tool) => convertToolToLangChain(tool, options.middlewareContext));
61
255
  if (langchainTools.length > 0) {
62
256
  config.tools = langchainTools;
63
257
  }
64
- yield* streamFromRunnable(options.entry, { messages }, config, options.retry);
258
+ const runnableInput = isCommandInput ? options.input : { messages };
259
+ yield* streamFromRunnable(options.entry, runnableInput, config, options.retry, options.streamTransformers, subagentEvents, streamContext);
65
260
  }
66
261
  function prepareAgentCall(options) {
67
262
  const inputRecord = (options.input ?? {});
@@ -78,12 +273,27 @@ function prepareAgentCall(options) {
78
273
  const config = {
79
274
  signal: options.signal,
80
275
  };
81
- if (Object.keys(params).length > 0) {
82
- config.configurable = params;
276
+ const configurable = { ...params };
277
+ if (options.threadId !== undefined && options.threadId.length > 0) {
278
+ configurable.thread_id = options.threadId;
279
+ }
280
+ if (Object.keys(configurable).length > 0) {
281
+ config.configurable = configurable;
83
282
  }
84
283
  return { agentInput, config };
85
284
  }
86
- async function* streamFromRunnable(runnable, input, config, retryConfig) {
285
+ async function* streamFromRunnable(runnable, input, config, retryConfig, streamTransformers, subagentEvents, streamContext) {
286
+ // Drains any pending subagent events queued by the bridge. Called before
287
+ // each normal yield to keep ordering predictable on the single event loop.
288
+ function* drainSubagentEvents() {
289
+ if (!subagentEvents)
290
+ return;
291
+ while (subagentEvents.length > 0) {
292
+ const next = subagentEvents.shift();
293
+ if (next)
294
+ yield next;
295
+ }
296
+ }
87
297
  const streamable = runnable;
88
298
  if (typeof streamable.streamEvents !== "function") {
89
299
  // Fallback: invoke with retry and emit a single done event
@@ -97,72 +307,148 @@ async function* streamFromRunnable(runnable, input, config, retryConfig) {
97
307
  yield { type: "done", data: result };
98
308
  return;
99
309
  }
100
- let finalOutput;
101
- let hasYielded = false;
102
- let lastStreamError;
103
- // Retry the entire stream if it fails before producing any output
104
- const maxStreamAttempts = retryConfig?.maxAttempts ?? 3;
105
- for (let attempt = 0; attempt < maxStreamAttempts; attempt++) {
106
- hasYielded = false;
107
- lastStreamError = undefined;
108
- finalOutput = undefined;
109
- try {
110
- for await (const event of streamable.streamEvents(input, {
111
- ...config,
112
- version: "v2",
113
- })) {
114
- switch (event.event) {
115
- case "on_chat_model_stream": {
116
- const content = event.data.chunk?.content;
117
- if (content && typeof content === "string" && content.length > 0) {
310
+ // Capture into a typed const so TS narrowing survives across the nested
311
+ // async-generator closure below. Bind to `streamable` — LangGraph's
312
+ // Pregel.streamEvents reads `this.config?.recursionLimit`, so calling it
313
+ // unbound throws "Cannot read properties of undefined (reading 'config')".
314
+ const streamEventsFn = streamable.streamEvents.bind(streamable);
315
+ // Process a single streamEvents iterator: yield AgentStreamChunks and
316
+ // return whatever __interrupt__ entries appeared in the graph's final
317
+ // on_chain_end output.
318
+ async function* processEventStream(invocationInput, invocationConfig, allowRetryOnError) {
319
+ let finalOutput;
320
+ let capturedInterrupts = [];
321
+ let hasYielded = false;
322
+ const maxStreamAttempts = allowRetryOnError ? (retryConfig?.maxAttempts ?? 3) : 1;
323
+ for (let attempt = 0; attempt < maxStreamAttempts; attempt++) {
324
+ hasYielded = false;
325
+ finalOutput = undefined;
326
+ capturedInterrupts = [];
327
+ try {
328
+ for await (const event of streamEventsFn(invocationInput, {
329
+ ...invocationConfig,
330
+ version: "v2",
331
+ })) {
332
+ yield* drainSubagentEvents();
333
+ switch (event.event) {
334
+ case "on_chat_model_stream": {
335
+ if (streamContext && streamContext.activeChildRuns > 0)
336
+ break;
337
+ const content = event.data.chunk?.content;
338
+ if (content && typeof content === "string" && content.length > 0) {
339
+ hasYielded = true;
340
+ yield { type: "token", data: content };
341
+ }
342
+ break;
343
+ }
344
+ case "on_tool_start": {
118
345
  hasYielded = true;
119
- yield { type: "token", data: content };
346
+ yield {
347
+ type: "tool_call",
348
+ data: {
349
+ name: event.name,
350
+ input: event.data.chunk ?? event.data.output,
351
+ },
352
+ };
353
+ break;
120
354
  }
121
- break;
122
- }
123
- case "on_tool_start": {
124
- hasYielded = true;
125
- yield {
126
- type: "tool_call",
127
- data: {
128
- name: event.name,
129
- input: event.data.chunk ?? event.data.output,
130
- },
131
- };
132
- break;
133
- }
134
- case "on_tool_end": {
135
- hasYielded = true;
136
- yield {
137
- type: "tool_result",
138
- data: { name: event.name, output: event.data.output },
139
- };
140
- break;
141
- }
142
- case "on_chain_end": {
143
- if (event.name === "LangGraph") {
144
- finalOutput = event.data.output;
355
+ case "on_tool_end": {
356
+ hasYielded = true;
357
+ yield {
358
+ type: "tool_result",
359
+ data: { name: event.name, output: event.data.output },
360
+ };
361
+ for (const transformer of streamTransformers ?? []) {
362
+ if (transformer.observes !== "tool_result")
363
+ continue;
364
+ for await (const out of transformer.transform({
365
+ toolName: event.name,
366
+ toolOutput: event.data.output,
367
+ })) {
368
+ yield {
369
+ type: out.event,
370
+ data: out.data,
371
+ };
372
+ }
373
+ }
374
+ break;
375
+ }
376
+ case "on_tool_error": {
377
+ // LangGraph's interrupt() throws a GraphInterrupt from inside
378
+ // the tool node. The error bubbles through streamEvents as
379
+ // on_tool_error with the GraphInterrupt instance on data.error.
380
+ // LangGraph itself catches it to park the checkpointer state,
381
+ // so the outer iterator continues normally afterwards.
382
+ const interrupts = extractInterruptsFromError(event.data.error);
383
+ if (interrupts && interrupts.length > 0) {
384
+ capturedInterrupts = interrupts;
385
+ for (const entry of interrupts) {
386
+ hasYielded = true;
387
+ if (process.env.DAWN_DEBUG_INTERRUPTS === "1") {
388
+ if (!entry.value ||
389
+ typeof entry.value.interruptId !== "string") {
390
+ console.warn("[dawn] interrupt entry.value missing interruptId — capability bug:", JSON.stringify(entry).slice(0, 300));
391
+ }
392
+ }
393
+ yield {
394
+ type: "interrupt",
395
+ // The capability's interrupt() payload is wrapped in
396
+ // entry.value by LangGraph — surface it verbatim so the
397
+ // SSE consumer sees the original {interruptId, kind, ...}
398
+ // envelope the workspace capability emitted.
399
+ data: entry.value,
400
+ };
401
+ }
402
+ }
403
+ break;
404
+ }
405
+ case "on_chain_end": {
406
+ if (event.name === "LangGraph") {
407
+ finalOutput = event.data.output;
408
+ const interrupts = extractInterrupts(event.data.output);
409
+ if (interrupts && interrupts.length > 0) {
410
+ capturedInterrupts = interrupts;
411
+ for (const entry of interrupts) {
412
+ hasYielded = true;
413
+ yield {
414
+ type: "interrupt",
415
+ // The capability's interrupt() payload is wrapped in
416
+ // entry.value by LangGraph — surface it verbatim so the
417
+ // SSE consumer sees the original {interruptId, kind, ...}
418
+ // envelope the workspace capability emitted.
419
+ data: entry.value,
420
+ };
421
+ }
422
+ }
423
+ }
424
+ break;
145
425
  }
146
- break;
147
426
  }
148
427
  }
428
+ // Stream completed successfully
429
+ return { finalOutput, interrupts: capturedInterrupts };
149
430
  }
150
- // Stream completed successfully
151
- break;
152
- }
153
- catch (error) {
154
- lastStreamError = error instanceof Error ? error : new Error(String(error));
155
- // If we already yielded chunks, we can't retry (client has partial data)
156
- // Or if the error isn't retryable, rethrow immediately
157
- if (hasYielded || !isRetryableError(error) || attempt === maxStreamAttempts - 1) {
158
- throw lastStreamError;
431
+ catch (error) {
432
+ const err = error instanceof Error ? error : new Error(String(error));
433
+ if (hasYielded || !isRetryableError(error) || attempt === maxStreamAttempts - 1) {
434
+ throw err;
435
+ }
436
+ const delay = Math.min(1000 * 2 ** attempt + Math.random() * 500, 10_000);
437
+ await new Promise((resolve) => setTimeout(resolve, delay));
159
438
  }
160
- // Backoff before retry
161
- const delay = Math.min(1000 * 2 ** attempt + Math.random() * 500, 10_000);
162
- await new Promise((resolve) => setTimeout(resolve, delay));
163
439
  }
440
+ // Unreachable: the loop either returns or throws.
441
+ return { finalOutput, interrupts: capturedInterrupts };
164
442
  }
165
- yield { type: "done", data: finalOutput };
443
+ // Invoke the stream. After yielding any interrupt envelopes, return cleanly.
444
+ // Resume is state-based: the caller posts to /threads/:id/resume with the
445
+ // decision, which opens a new SSE stream with Command({resume: decision}) as
446
+ // input. The adapter does NOT park here waiting for an in-process promise.
447
+ const pass = yield* processEventStream(input, config, /* allowRetryOnError */ true);
448
+ // Final drain in case the last tool call was the bridged task tool —
449
+ // its events would otherwise be stranded after the stream ends.
450
+ yield* drainSubagentEvents();
451
+ yield { type: "done", data: pass.finalOutput };
166
452
  }
167
453
  function isInputMessageArray(value) {
168
454
  return (Array.isArray(value) &&
@@ -0,0 +1,11 @@
1
+ import type { BuiltInModelProviderId, ReasoningConfig } from "@dawn-ai/sdk";
2
+ type Importer = (specifier: string) => Promise<Record<string, unknown>>;
3
+ export declare function missingProviderPackageMessage(provider: BuiltInModelProviderId, packageName: string): string;
4
+ export declare function createChatModel(options: {
5
+ readonly model: string;
6
+ readonly provider: BuiltInModelProviderId;
7
+ readonly reasoning?: ReasoningConfig;
8
+ readonly importer?: Importer;
9
+ }): Promise<unknown>;
10
+ export {};
11
+ //# sourceMappingURL=chat-model-factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-model-factory.d.ts","sourceRoot":"","sources":["../src/chat-model-factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAE3E,KAAK,QAAQ,GAAG,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;AAoBvE,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,sBAAsB,EAChC,WAAW,EAAE,MAAM,GAClB,MAAM,CAER;AAED,wBAAsB,eAAe,CAAC,OAAO,EAAE;IAC7C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,QAAQ,EAAE,sBAAsB,CAAA;IACzC,QAAQ,CAAC,SAAS,CAAC,EAAE,eAAe,CAAA;IACpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAA;CAC7B,GAAG,OAAO,CAAC,OAAO,CAAC,CA6BnB"}