@dawn-ai/langchain 0.1.8 → 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.
Files changed (51) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +16 -2
  3. package/dist/agent-adapter.d.ts +60 -4
  4. package/dist/agent-adapter.d.ts.map +1 -1
  5. package/dist/agent-adapter.js +396 -77
  6. package/dist/chat-model-factory.d.ts +11 -0
  7. package/dist/chat-model-factory.d.ts.map +1 -0
  8. package/dist/chat-model-factory.js +55 -0
  9. package/dist/index.d.ts +19 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +11 -1
  12. package/dist/model-provider-resolver.d.ts +8 -0
  13. package/dist/model-provider-resolver.d.ts.map +1 -0
  14. package/dist/model-provider-resolver.js +40 -0
  15. package/dist/offload/offload-store.d.ts +30 -0
  16. package/dist/offload/offload-store.d.ts.map +1 -0
  17. package/dist/offload/offload-store.js +105 -0
  18. package/dist/offload/offload-tool-output.d.ts +10 -0
  19. package/dist/offload/offload-tool-output.d.ts.map +1 -0
  20. package/dist/offload/offload-tool-output.js +18 -0
  21. package/dist/offload/stub.d.ts +8 -0
  22. package/dist/offload/stub.d.ts.map +1 -0
  23. package/dist/offload/stub.js +14 -0
  24. package/dist/subagent-dispatcher.d.ts +33 -0
  25. package/dist/subagent-dispatcher.d.ts.map +1 -0
  26. package/dist/subagent-dispatcher.js +182 -0
  27. package/dist/subagent-tool-bridge.d.ts +27 -0
  28. package/dist/subagent-tool-bridge.d.ts.map +1 -0
  29. package/dist/subagent-tool-bridge.js +32 -0
  30. package/dist/summarization/hook.d.ts +31 -0
  31. package/dist/summarization/hook.d.ts.map +1 -0
  32. package/dist/summarization/hook.js +44 -0
  33. package/dist/summarization/index.d.ts +5 -0
  34. package/dist/summarization/index.d.ts.map +1 -0
  35. package/dist/summarization/index.js +4 -0
  36. package/dist/summarization/split.d.ts +12 -0
  37. package/dist/summarization/split.d.ts.map +1 -0
  38. package/dist/summarization/split.js +27 -0
  39. package/dist/summarization/summarize.d.ts +10 -0
  40. package/dist/summarization/summarize.d.ts.map +1 -0
  41. package/dist/summarization/summarize.js +30 -0
  42. package/dist/summarization/token-counter.d.ts +10 -0
  43. package/dist/summarization/token-counter.d.ts.map +1 -0
  44. package/dist/summarization/token-counter.js +29 -0
  45. package/dist/tool-converter.d.ts +5 -1
  46. package/dist/tool-converter.d.ts.map +1 -1
  47. package/dist/tool-converter.js +74 -21
  48. package/dist/unwrap-tool-result.d.ts +35 -0
  49. package/dist/unwrap-tool-result.d.ts.map +1 -0
  50. package/dist/unwrap-tool-result.js +42 -0
  51. package/package.json +55 -10
@@ -1,7 +1,13 @@
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";
10
+ import { buildSummarizationHook } from "./summarization/index.js";
5
11
  import { convertToolToLangChain } from "./tool-converter.js";
6
12
  function assertAgentLike(entry) {
7
13
  if (typeof entry !== "object" ||
@@ -11,31 +17,202 @@ function assertAgentLike(entry) {
11
17
  throw new Error("Agent entry must expose invoke(input) — expected a LangChain agent");
12
18
  }
13
19
  }
14
- const materializedAgents = new WeakMap();
15
- async function materializeAgent(descriptor, tools, stateFields, middlewareContext) {
16
- const cached = materializedAgents.get(descriptor);
17
- if (cached) {
18
- return cached;
20
+ // Cache keyed on descriptor only. Assumption: a given descriptor is always
21
+ // invoked with the same capability contributions (prompt fragments come from
22
+ // the route directory, which is stable per descriptor). If that assumption
23
+ // changes, the cache key must include a hash of the fragments/transformers.
24
+ let materializedAgents = new WeakMap();
25
+ /**
26
+ * Test-only escape hatch: reset the materialized-agents cache so the next
27
+ * harness run creates a fresh LLM instance (e.g. pointing at a new aimock
28
+ * port). Exported (and re-exported via `@dawn-ai/cli/runtime`) so the
29
+ * `@dawn-ai/testing` harness can clear the cache on teardown. Not for
30
+ * production use; the `__`/`ForTests` name marks it internal-by-convention.
31
+ */
32
+ export function __resetMaterializedAgentsForTests() {
33
+ materializedAgents = new WeakMap();
34
+ }
35
+ export function composePromptMessages(systemPrompt, promptFragments, state) {
36
+ const rendered = promptFragments
37
+ .filter((f) => f.placement === "after_user_prompt")
38
+ .map((f) => f.render(state))
39
+ .filter((s) => s.length > 0);
40
+ const composed = [systemPrompt, ...rendered].join("\n\n");
41
+ const messages = Array.isArray(state.messages) ? state.messages : [];
42
+ return [{ role: "system", content: composed }, ...messages];
43
+ }
44
+ async function materializeAgent(descriptor, tools, checkpointer, opts = {}) {
45
+ if (!opts.bypassCache) {
46
+ const cached = materializedAgents.get(descriptor);
47
+ if (cached) {
48
+ return cached;
49
+ }
19
50
  }
20
51
  const { createReactAgent } = await import("@langchain/langgraph/prebuilt");
21
- const { ChatOpenAI } = await import("@langchain/openai");
22
- const langchainTools = tools.map((tool) => convertToolToLangChain(tool, middlewareContext));
23
- const llm = new ChatOpenAI({
52
+ const langchainTools = tools.map((tool) => convertToolToLangChain(tool, opts.middlewareContext, opts.offload));
53
+ const provider = resolveProvider({
54
+ model: descriptor.model,
55
+ ...(descriptor.provider !== undefined ? { provider: descriptor.provider } : {}),
56
+ });
57
+ const llm = await createChatModel({
24
58
  model: descriptor.model,
59
+ provider,
60
+ ...(descriptor.reasoning ? { reasoning: descriptor.reasoning } : {}),
25
61
  });
62
+ const fragments = opts.promptFragments ?? [];
26
63
  const agentOptions = {
27
64
  llm,
28
65
  tools: langchainTools,
29
- prompt: descriptor.systemPrompt,
66
+ // Function-form prompt re-renders fragments on every model turn so they
67
+ // can reflect live state (e.g., the current todos list).
68
+ prompt: fragments.length > 0
69
+ ? (state) => composePromptMessages(descriptor.systemPrompt, fragments, state)
70
+ : descriptor.systemPrompt,
71
+ // Required so `interrupt()` can park graph state and `Command({resume})`
72
+ // can replay it. Paired with `config.configurable.thread_id`.
73
+ checkpointer,
30
74
  };
31
- if (stateFields && stateFields.length > 0) {
32
- agentOptions.stateSchema = materializeStateSchema(stateFields);
75
+ const runningSummaryField = {
76
+ name: "runningSummary",
77
+ reducer: "replace",
78
+ default: undefined,
79
+ };
80
+ const effectiveStateFields = opts.summarization
81
+ ? [...(opts.stateFields ?? []).filter((f) => f.name !== "runningSummary"), runningSummaryField]
82
+ : (opts.stateFields ?? []);
83
+ if (effectiveStateFields.length > 0) {
84
+ agentOptions.stateSchema = materializeStateSchema(effectiveStateFields);
85
+ }
86
+ if (opts.summarization) {
87
+ agentOptions.preModelHook = buildSummarizationHook(opts.summarization);
33
88
  }
34
89
  // biome-ignore lint/suspicious/noExplicitAny: dynamically-built options don't satisfy strict StateDefinition type
35
90
  const compiled = createReactAgent(agentOptions);
36
- materializedAgents.set(descriptor, compiled);
91
+ if (!opts.bypassCache) {
92
+ materializedAgents.set(descriptor, compiled);
93
+ }
37
94
  return compiled;
38
95
  }
96
+ export async function materializeAgentGraph(options) {
97
+ return materializeAgent(options.descriptor, options.tools ?? [], options.checkpointer, {
98
+ ...(options.stateFields ? { stateFields: options.stateFields } : {}),
99
+ ...(options.promptFragments ? { promptFragments: options.promptFragments } : {}),
100
+ ...(options.summarization ? { summarization: options.summarization } : {}),
101
+ });
102
+ }
103
+ /**
104
+ * LangGraph 1.x's `interrupt()` throws a `GraphInterrupt` from inside the tool
105
+ * node. Under `streamEvents` v2 this surfaces as an `on_tool_error` whose
106
+ * `event.data.error` is the `GraphInterrupt` instance — its `.name` is
107
+ * `"GraphInterrupt"` and its `.interrupts` array carries the `{ id, value }`
108
+ * entries we need. The top-level `on_chain_end` for `LangGraph` does NOT
109
+ * include `__interrupt__` in this code path (that key appears only on the
110
+ * `invoke`/`stream` return value), so detection must happen at the tool error.
111
+ *
112
+ * We still keep the `__interrupt__` extractor for `on_chain_end` as a
113
+ * defensive fallback in case a future LangGraph version surfaces interrupts
114
+ * via the chain output too.
115
+ */
116
+ const INTERRUPT_KEY = "__interrupt__";
117
+ function extractInterrupts(output) {
118
+ if (!output || typeof output !== "object")
119
+ return undefined;
120
+ const maybe = output[INTERRUPT_KEY];
121
+ if (!Array.isArray(maybe))
122
+ return undefined;
123
+ return maybe;
124
+ }
125
+ /**
126
+ * Detects a thrown `GraphInterrupt` surfaced via `on_tool_error`.
127
+ *
128
+ * LangGraph's `interrupt()` throws a `GraphInterrupt` whose `.message` is
129
+ * `JSON.stringify(interrupts)` and whose `.interrupts` array carries the
130
+ * `{ id, value }` entries. By the time the error reaches `streamEvents`'
131
+ * `data.error` it has already been stringified — typically into
132
+ * `<JSON interrupts>\n\nGraphInterrupt: <JSON interrupts>\n at ...stack`.
133
+ *
134
+ * We handle three shapes defensively:
135
+ * - object with `.name === "GraphInterrupt"` and `.interrupts` array
136
+ * (in case a future LangGraph version surfaces the live error)
137
+ * - object/Error whose stringified message starts with a JSON array
138
+ * - bare string with the `GraphInterrupt:` marker
139
+ */
140
+ function extractInterruptsFromError(error) {
141
+ if (!error)
142
+ return undefined;
143
+ if (typeof error === "object") {
144
+ const e = error;
145
+ if (e.name === "GraphInterrupt" && Array.isArray(e.interrupts) && e.interrupts.length > 0) {
146
+ return e.interrupts;
147
+ }
148
+ if (typeof e.message === "string") {
149
+ const parsed = parseInterruptStringMessage(e.message);
150
+ if (parsed)
151
+ return parsed;
152
+ }
153
+ }
154
+ if (typeof error === "string") {
155
+ const parsed = parseInterruptStringMessage(error);
156
+ if (parsed)
157
+ return parsed;
158
+ }
159
+ return undefined;
160
+ }
161
+ /**
162
+ * Parses the stringified form of a GraphInterrupt's message. The string
163
+ * begins with `JSON.stringify(interrupts, null, 2)` and is followed by
164
+ * `\n\nGraphInterrupt: ...\n at ...` stack metadata. We slice the leading
165
+ * JSON array up to the first `]` followed by a newline + non-JSON sentinel
166
+ * and parse it.
167
+ */
168
+ function parseInterruptStringMessage(text) {
169
+ const trimmed = text.trimStart();
170
+ if (!trimmed.startsWith("["))
171
+ return undefined;
172
+ // Find the matching closing bracket by bracket counting at depth 0 — robust
173
+ // against nested arrays in the interrupt payloads.
174
+ let depth = 0;
175
+ let inString = false;
176
+ let escaped = false;
177
+ let end = -1;
178
+ for (let i = 0; i < trimmed.length; i++) {
179
+ const ch = trimmed[i];
180
+ if (escaped) {
181
+ escaped = false;
182
+ continue;
183
+ }
184
+ if (inString) {
185
+ if (ch === "\\")
186
+ escaped = true;
187
+ else if (ch === '"')
188
+ inString = false;
189
+ continue;
190
+ }
191
+ if (ch === '"')
192
+ inString = true;
193
+ else if (ch === "[")
194
+ depth++;
195
+ else if (ch === "]") {
196
+ depth--;
197
+ if (depth === 0) {
198
+ end = i;
199
+ break;
200
+ }
201
+ }
202
+ }
203
+ if (end === -1)
204
+ return undefined;
205
+ const json = trimmed.slice(0, end + 1);
206
+ try {
207
+ const parsed = JSON.parse(json);
208
+ if (!Array.isArray(parsed) || parsed.length === 0)
209
+ return undefined;
210
+ return parsed;
211
+ }
212
+ catch {
213
+ return undefined;
214
+ }
215
+ }
39
216
  export async function executeAgent(options) {
40
217
  let result;
41
218
  for await (const chunk of streamAgent(options)) {
@@ -46,22 +223,73 @@ export async function executeAgent(options) {
46
223
  return result;
47
224
  }
48
225
  export async function* streamAgent(options) {
226
+ if (!options.checkpointer) {
227
+ 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.");
228
+ }
229
+ // If the caller is passing a Command directly (resume path), forward it
230
+ // verbatim without the usual input preparation and message extraction.
231
+ const isCommandInput = options.input instanceof Command;
49
232
  const { agentInput, config } = prepareAgentCall(options);
50
- const messages = extractMessages(agentInput);
233
+ const messages = isCommandInput ? [] : extractMessages(agentInput);
234
+ // Per-call subagent event queue. The bridge's writer pushes here; the
235
+ // streaming generator drains the queue alongside normal stream chunks. This
236
+ // avoids the module-level mutable-writer anti-pattern: each call has its
237
+ // own queue scoped to the surrounding generator frame.
238
+ const subagentEvents = [];
239
+ const queueWriter = (event) => {
240
+ subagentEvents.push({
241
+ type: event.event,
242
+ data: event.data,
243
+ });
244
+ };
245
+ // Per-call counter shared with the dispatcher. While a child is active, the
246
+ // parent's on_chat_model_stream events are suppressed (LangChain v2
247
+ // streamEvents propagates child events to the parent listener via
248
+ // async-local-storage tracing, so without this gate every child token
249
+ // appears twice on the parent stream: once as a raw token chunk and once
250
+ // wrapped in a subagent.message envelope).
251
+ const streamContext = createSubagentStreamContext();
252
+ const resolver = options.subagentResolver;
253
+ const hasTaskTool = options.tools.some((t) => t.name === "task");
254
+ const effectiveTools = resolver && hasTaskTool
255
+ ? options.tools.map((t) => t.name === "task"
256
+ ? {
257
+ ...t,
258
+ run: bridgeSubagentTool({
259
+ subagentResolver: resolver,
260
+ writer: queueWriter,
261
+ parentConfig: config,
262
+ streamContext,
263
+ }).run,
264
+ }
265
+ : t)
266
+ : options.tools;
51
267
  // DawnAgent descriptor path — materialize on first use
52
268
  if (isDawnAgent(options.entry)) {
53
- const materializedAgent = await materializeAgent(options.entry, options.tools, options.stateFields, options.middlewareContext);
269
+ // Bypass the per-descriptor cache when a resolver is wired: the bridged
270
+ // tool closes over the per-call queue + parent config, so caching would
271
+ // bind those to a single call.
272
+ const materializedAgent = await materializeAgent(options.entry, effectiveTools, options.checkpointer, {
273
+ ...(options.stateFields ? { stateFields: options.stateFields } : {}),
274
+ ...(options.middlewareContext ? { middlewareContext: options.middlewareContext } : {}),
275
+ ...(options.promptFragments ? { promptFragments: options.promptFragments } : {}),
276
+ ...(resolver && hasTaskTool ? { bypassCache: true } : {}),
277
+ ...(options.offload ? { offload: options.offload } : {}),
278
+ ...(options.summarization ? { summarization: options.summarization } : {}),
279
+ });
54
280
  const retryConfig = options.entry.retry;
55
- yield* streamFromRunnable(materializedAgent, { messages }, config, retryConfig);
281
+ const runnableInput = isCommandInput ? options.input : { messages };
282
+ yield* streamFromRunnable(materializedAgent, runnableInput, config, retryConfig, options.streamTransformers, subagentEvents, streamContext);
56
283
  return;
57
284
  }
58
285
  // Legacy path — raw Runnable with .invoke()
59
286
  assertAgentLike(options.entry);
60
- const langchainTools = options.tools.map((tool) => convertToolToLangChain(tool, options.middlewareContext));
287
+ const langchainTools = effectiveTools.map((tool) => convertToolToLangChain(tool, options.middlewareContext, options.offload));
61
288
  if (langchainTools.length > 0) {
62
289
  config.tools = langchainTools;
63
290
  }
64
- yield* streamFromRunnable(options.entry, { messages }, config, options.retry);
291
+ const runnableInput = isCommandInput ? options.input : { messages };
292
+ yield* streamFromRunnable(options.entry, runnableInput, config, options.retry, options.streamTransformers, subagentEvents, streamContext);
65
293
  }
66
294
  function prepareAgentCall(options) {
67
295
  const inputRecord = (options.input ?? {});
@@ -78,12 +306,27 @@ function prepareAgentCall(options) {
78
306
  const config = {
79
307
  signal: options.signal,
80
308
  };
81
- if (Object.keys(params).length > 0) {
82
- config.configurable = params;
309
+ const configurable = { ...params };
310
+ if (options.threadId !== undefined && options.threadId.length > 0) {
311
+ configurable.thread_id = options.threadId;
312
+ }
313
+ if (Object.keys(configurable).length > 0) {
314
+ config.configurable = configurable;
83
315
  }
84
316
  return { agentInput, config };
85
317
  }
86
- async function* streamFromRunnable(runnable, input, config, retryConfig) {
318
+ async function* streamFromRunnable(runnable, input, config, retryConfig, streamTransformers, subagentEvents, streamContext) {
319
+ // Drains any pending subagent events queued by the bridge. Called before
320
+ // each normal yield to keep ordering predictable on the single event loop.
321
+ function* drainSubagentEvents() {
322
+ if (!subagentEvents)
323
+ return;
324
+ while (subagentEvents.length > 0) {
325
+ const next = subagentEvents.shift();
326
+ if (next)
327
+ yield next;
328
+ }
329
+ }
87
330
  const streamable = runnable;
88
331
  if (typeof streamable.streamEvents !== "function") {
89
332
  // Fallback: invoke with retry and emit a single done event
@@ -97,72 +340,148 @@ async function* streamFromRunnable(runnable, input, config, retryConfig) {
97
340
  yield { type: "done", data: result };
98
341
  return;
99
342
  }
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) {
343
+ // Capture into a typed const so TS narrowing survives across the nested
344
+ // async-generator closure below. Bind to `streamable` — LangGraph's
345
+ // Pregel.streamEvents reads `this.config?.recursionLimit`, so calling it
346
+ // unbound throws "Cannot read properties of undefined (reading 'config')".
347
+ const streamEventsFn = streamable.streamEvents.bind(streamable);
348
+ // Process a single streamEvents iterator: yield AgentStreamChunks and
349
+ // return whatever __interrupt__ entries appeared in the graph's final
350
+ // on_chain_end output.
351
+ async function* processEventStream(invocationInput, invocationConfig, allowRetryOnError) {
352
+ let finalOutput;
353
+ let capturedInterrupts = [];
354
+ let hasYielded = false;
355
+ const maxStreamAttempts = allowRetryOnError ? (retryConfig?.maxAttempts ?? 3) : 1;
356
+ for (let attempt = 0; attempt < maxStreamAttempts; attempt++) {
357
+ hasYielded = false;
358
+ finalOutput = undefined;
359
+ capturedInterrupts = [];
360
+ try {
361
+ for await (const event of streamEventsFn(invocationInput, {
362
+ ...invocationConfig,
363
+ version: "v2",
364
+ })) {
365
+ yield* drainSubagentEvents();
366
+ switch (event.event) {
367
+ case "on_chat_model_stream": {
368
+ if (streamContext && streamContext.activeChildRuns > 0)
369
+ break;
370
+ const content = event.data.chunk?.content;
371
+ if (content && typeof content === "string" && content.length > 0) {
372
+ hasYielded = true;
373
+ yield { type: "token", data: content };
374
+ }
375
+ break;
376
+ }
377
+ case "on_tool_start": {
118
378
  hasYielded = true;
119
- yield { type: "token", data: content };
379
+ yield {
380
+ type: "tool_call",
381
+ data: {
382
+ name: event.name,
383
+ input: event.data.input ?? event.data.chunk ?? event.data.output,
384
+ },
385
+ };
386
+ break;
120
387
  }
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;
388
+ case "on_tool_end": {
389
+ hasYielded = true;
390
+ yield {
391
+ type: "tool_result",
392
+ data: { name: event.name, output: event.data.output },
393
+ };
394
+ for (const transformer of streamTransformers ?? []) {
395
+ if (transformer.observes !== "tool_result")
396
+ continue;
397
+ for await (const out of transformer.transform({
398
+ toolName: event.name,
399
+ toolOutput: event.data.output,
400
+ })) {
401
+ yield {
402
+ type: out.event,
403
+ data: out.data,
404
+ };
405
+ }
406
+ }
407
+ break;
408
+ }
409
+ case "on_tool_error": {
410
+ // LangGraph's interrupt() throws a GraphInterrupt from inside
411
+ // the tool node. The error bubbles through streamEvents as
412
+ // on_tool_error with the GraphInterrupt instance on data.error.
413
+ // LangGraph itself catches it to park the checkpointer state,
414
+ // so the outer iterator continues normally afterwards.
415
+ const interrupts = extractInterruptsFromError(event.data.error);
416
+ if (interrupts && interrupts.length > 0) {
417
+ capturedInterrupts = interrupts;
418
+ for (const entry of interrupts) {
419
+ hasYielded = true;
420
+ if (process.env.DAWN_DEBUG_INTERRUPTS === "1") {
421
+ if (!entry.value ||
422
+ typeof entry.value.interruptId !== "string") {
423
+ console.warn("[dawn] interrupt entry.value missing interruptId — capability bug:", JSON.stringify(entry).slice(0, 300));
424
+ }
425
+ }
426
+ yield {
427
+ type: "interrupt",
428
+ // The capability's interrupt() payload is wrapped in
429
+ // entry.value by LangGraph — surface it verbatim so the
430
+ // SSE consumer sees the original {interruptId, kind, ...}
431
+ // envelope the workspace capability emitted.
432
+ data: entry.value,
433
+ };
434
+ }
435
+ }
436
+ break;
437
+ }
438
+ case "on_chain_end": {
439
+ if (event.name === "LangGraph") {
440
+ finalOutput = event.data.output;
441
+ const interrupts = extractInterrupts(event.data.output);
442
+ if (interrupts && interrupts.length > 0) {
443
+ capturedInterrupts = interrupts;
444
+ for (const entry of interrupts) {
445
+ hasYielded = true;
446
+ yield {
447
+ type: "interrupt",
448
+ // The capability's interrupt() payload is wrapped in
449
+ // entry.value by LangGraph — surface it verbatim so the
450
+ // SSE consumer sees the original {interruptId, kind, ...}
451
+ // envelope the workspace capability emitted.
452
+ data: entry.value,
453
+ };
454
+ }
455
+ }
456
+ }
457
+ break;
145
458
  }
146
- break;
147
459
  }
148
460
  }
461
+ // Stream completed successfully
462
+ return { finalOutput, interrupts: capturedInterrupts };
149
463
  }
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;
464
+ catch (error) {
465
+ const err = error instanceof Error ? error : new Error(String(error));
466
+ if (hasYielded || !isRetryableError(error) || attempt === maxStreamAttempts - 1) {
467
+ throw err;
468
+ }
469
+ const delay = Math.min(1000 * 2 ** attempt + Math.random() * 500, 10_000);
470
+ await new Promise((resolve) => setTimeout(resolve, delay));
159
471
  }
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
472
  }
473
+ // Unreachable: the loop either returns or throws.
474
+ return { finalOutput, interrupts: capturedInterrupts };
164
475
  }
165
- yield { type: "done", data: finalOutput };
476
+ // Invoke the stream. After yielding any interrupt envelopes, return cleanly.
477
+ // Resume is state-based: the caller posts to /threads/:id/resume with the
478
+ // decision, which opens a new SSE stream with Command({resume: decision}) as
479
+ // input. The adapter does NOT park here waiting for an in-process promise.
480
+ const pass = yield* processEventStream(input, config, /* allowRetryOnError */ true);
481
+ // Final drain in case the last tool call was the bridged task tool —
482
+ // its events would otherwise be stranded after the stream ends.
483
+ yield* drainSubagentEvents();
484
+ yield { type: "done", data: pass.finalOutput };
166
485
  }
167
486
  function isInputMessageArray(value) {
168
487
  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,CAoCnB"}
@@ -0,0 +1,55 @@
1
+ const providerSpecs = {
2
+ openai: { packageName: "@langchain/openai", exportName: "ChatOpenAI" },
3
+ anthropic: { packageName: "@langchain/anthropic", exportName: "ChatAnthropic" },
4
+ // Official LangChain JS docs and current npm availability support this stable package/class.
5
+ google: { packageName: "@langchain/google-genai", exportName: "ChatGoogleGenerativeAI" },
6
+ mistral: { packageName: "@langchain/mistralai", exportName: "ChatMistralAI" },
7
+ groq: { packageName: "@langchain/groq", exportName: "ChatGroq" },
8
+ ollama: { packageName: "@langchain/ollama", exportName: "ChatOllama" },
9
+ xai: { packageName: "@langchain/xai", exportName: "ChatXAI" },
10
+ openrouter: { packageName: "@langchain/openrouter", exportName: "ChatOpenRouter" },
11
+ };
12
+ export function missingProviderPackageMessage(provider, packageName) {
13
+ return `Provider "${provider}" requires ${packageName}. Install it with: pnpm add ${packageName}`;
14
+ }
15
+ export async function createChatModel(options) {
16
+ const spec = providerSpecs[options.provider];
17
+ const importer = options.importer ??
18
+ ((specifier) => import(specifier));
19
+ let moduleExports;
20
+ try {
21
+ moduleExports = await importer(spec.packageName);
22
+ }
23
+ catch (error) {
24
+ if (isMissingModuleError(error, spec.packageName)) {
25
+ throw new Error(missingProviderPackageMessage(options.provider, spec.packageName));
26
+ }
27
+ throw error;
28
+ }
29
+ const Constructor = moduleExports[spec.exportName];
30
+ if (typeof Constructor !== "function") {
31
+ throw new Error(`Provider "${options.provider}" package ${spec.packageName} does not export ${spec.exportName}.`);
32
+ }
33
+ const constructorOptions = { model: options.model };
34
+ if (options.provider === "openai" && options.reasoning?.effort) {
35
+ constructorOptions.reasoningEffort = options.reasoning.effort;
36
+ }
37
+ if (options.provider === "openai") {
38
+ const baseURL = process.env.OPENAI_BASE_URL;
39
+ if (baseURL) {
40
+ constructorOptions.configuration = { baseURL };
41
+ }
42
+ }
43
+ return new Constructor(constructorOptions);
44
+ }
45
+ function isMissingModuleError(error, expectedPackageName) {
46
+ return (error instanceof Error &&
47
+ ("code" in error ? error.code === "ERR_MODULE_NOT_FOUND" : true) &&
48
+ referencesPackageSpecifier(error.message, expectedPackageName) &&
49
+ /Cannot find (package|module)|ERR_MODULE_NOT_FOUND/i.test(error.message));
50
+ }
51
+ function referencesPackageSpecifier(message, packageName) {
52
+ return (message.includes(`'${packageName}'`) ||
53
+ message.includes(`"${packageName}"`) ||
54
+ message.includes(`\`${packageName}\``));
55
+ }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,26 @@
1
- export type { AgentStreamChunk } from "./agent-adapter.js";
2
- export { executeAgent, streamAgent } from "./agent-adapter.js";
1
+ export type { BuiltInModelProviderId, ModelProviderId } from "@dawn-ai/sdk";
2
+ export { Command } from "@langchain/langgraph";
3
+ export type { AgentStreamChunk, DawnToolDefinition, SubagentResolver, } from "./agent-adapter.js";
4
+ export { __resetMaterializedAgentsForTests, executeAgent, materializeAgentGraph, streamAgent, } from "./agent-adapter.js";
3
5
  export { chainAdapter } from "./chain-adapter.js";
6
+ export { createChatModel } from "./chat-model-factory.js";
7
+ export { inferProvider, resolveProvider } from "./model-provider-resolver.js";
8
+ export type { OffloadStoreOptions } from "./offload/offload-store.js";
9
+ export { buildOffloadFileName, OffloadStore } from "./offload/offload-store.js";
10
+ export type { OffloadToolOutputCtx } from "./offload/offload-tool-output.js";
11
+ export { offloadToolOutput } from "./offload/offload-tool-output.js";
12
+ export { buildStub } from "./offload/stub.js";
4
13
  export type { RetryOptions } from "./retry.js";
5
14
  export { isRetryableError, withRetry } from "./retry.js";
6
15
  export { materializeStateSchema } from "./state-adapter.js";
16
+ export type { SubagentEvent, SubagentStreamContext, } from "./subagent-dispatcher.js";
17
+ export { createSubagentStreamContext, dispatchSubagent, MAX_SUBAGENT_DEPTH, } from "./subagent-dispatcher.js";
18
+ export type { SubagentResolverResult } from "./subagent-tool-bridge.js";
19
+ export { bridgeSubagentTool } from "./subagent-tool-bridge.js";
20
+ export * from "./summarization/index.js";
21
+ export type { OffloadFn } from "./tool-converter.js";
7
22
  export { convertToolToLangChain } from "./tool-converter.js";
8
23
  export { executeWithToolLoop } from "./tool-loop.js";
24
+ export type { UnwrappedToolResult } from "./unwrap-tool-result.js";
25
+ export { unwrapToolResult } from "./unwrap-tool-result.js";
9
26
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAC1D,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAA;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAC3E,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAC9C,YAAY,EACV,gBAAgB,EAChB,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,iCAAiC,EACjC,YAAY,EACZ,qBAAqB,EACrB,WAAW,GACZ,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AACzD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAA;AAC7E,YAAY,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AACrE,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAC/E,YAAY,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAA;AAC5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAA;AACpE,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAC7C,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,YAAY,EACV,aAAa,EACb,qBAAqB,GACtB,MAAM,0BAA0B,CAAA;AACjC,OAAO,EACL,2BAA2B,EAC3B,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,0BAA0B,CAAA;AACjC,YAAY,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAA;AACvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAC9D,cAAc,0BAA0B,CAAA;AACxC,YAAY,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAA;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAA;AACpD,YAAY,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAA;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA"}