Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 41 additions & 3 deletions apps/website/content/docs/langgraph/api/api-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,12 @@
"type": "(id: string) => void",
"description": "Optional callback invoked when a new thread is created",
"optional": true
},
{
"name": "clientOptions",
"type": "LangGraphClientOptions",
"description": "Optional SDK client tuning (e.g. `maxRetries`)",
"optional": true
}
],
"examples": [
Expand Down Expand Up @@ -886,6 +892,12 @@
"description": "Agent or graph identifier on the LangGraph platform.",
"optional": true
},
{
"name": "clientOptions",
"type": "LangGraphClientOptions",
"description": "Tuning options for the default transport's LangGraph SDK client (e.g. retry budget).",
"optional": true
},
{
"name": "filterSubagentMessages",
"type": "boolean",
Expand Down Expand Up @@ -1002,7 +1014,7 @@
{
"name": "AgentOptions",
"kind": "interface",
"description": "Options for creating a LangGraph-backed agent via agent.",
"description": "",
"properties": [
{
"name": "apiUrl",
Expand All @@ -1016,6 +1028,12 @@
"description": "Agent or graph identifier on the LangGraph platform.",
"optional": false
},
{
"name": "clientOptions",
"type": "LangGraphClientOptions",
"description": "Tuning options for the default transport's LangGraph SDK client (e.g. retry budget).",
"optional": true
},
{
"name": "filterSubagentMessages",
"type": "boolean",
Expand Down Expand Up @@ -1626,6 +1644,20 @@
],
"examples": []
},
{
"name": "LangGraphClientOptions",
"kind": "interface",
"description": "Tuning options for the underlying LangGraph SDK `Client` constructed by the\ndefault FetchStreamTransport. Ignored when a custom `transport` is\nsupplied (the transport owns its own client).",
"properties": [
{
"name": "maxRetries",
"type": "number",
"description": "How many times a failed request — including the initial stream connect —\nis retried with exponential backoff before the error surfaces. Maps to the\nSDK's `callerOptions.maxRetries`. Omitted → the SDK default (currently 4).\n\nSet `0` to fail fast: useful for e2e tests that force a connection failure\nand assert the error surfaces promptly, rather than after the full\nmulti-second backoff window.",
"optional": true
}
],
"examples": []
},
{
"name": "LangGraphSubmitOptions",
"kind": "interface",
Expand Down Expand Up @@ -2319,14 +2351,20 @@
{
"name": "createLangGraphClient",
"kind": "function",
"description": "Construct a LangGraph SDK Client that accepts both absolute URLs\n(`http://localhost:2024`) and relative `/api`-style paths that get\nproxied by middleware in production. The SDK itself rejects\nrelative URLs, so this helper rewrites them against\n`window.location.origin` when running in the browser.\n\nSingle source of truth for the absolute-URL rewrite — the streaming\ntransport (`fetch-stream.transport.ts`) and the threads adapter\n(`LangGraphThreadsAdapter`) both go through here.",
"signature": "createLangGraphClient(apiUrl: string): Client<>",
"description": "Construct a LangGraph SDK Client that accepts both absolute URLs\n(`http://localhost:2024`) and relative `/api`-style paths that get\nproxied by middleware in production. The SDK itself rejects\nrelative URLs, so this helper rewrites them against\n`window.location.origin` when running in the browser.\n\nSingle source of truth for the absolute-URL rewrite — the streaming\ntransport (`fetch-stream.transport.ts`) and the threads adapter\n(`LangGraphThreadsAdapter`) both go through here.\n\n`clientOptions.maxRetries` maps to the SDK's `callerOptions.maxRetries`,\nwhich governs how many times a failed request (including the initial\nstream connect) is retried with exponential backoff before the error\nsurfaces. Omitted → the SDK default (currently 4). Apps under test set\n`0` so a forced connection failure surfaces immediately instead of after\nthe full backoff window.",
"signature": "createLangGraphClient(apiUrl: string, clientOptions: LangGraphClientOptions): Client<>",
"params": [
{
"name": "apiUrl",
"type": "string",
"description": "",
"optional": false
},
{
"name": "clientOptions",
"type": "LangGraphClientOptions",
"description": "",
"optional": true
}
],
"returns": {
Expand Down
8 changes: 8 additions & 0 deletions examples/chat/angular/e2e/error-handling.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ import { messageInput, openDemo, sendButton, waitForFinalAssistant } from './tes
test('error handling: failed stream surfaces an alert and the next send recovers', async ({
page,
}) => {
// The LangGraph SDK retries a failed stream connect with exponential backoff
// (~15s) before surfacing the error — fine for production resilience, too slow
// to assert here. Opt this app instance into fail-fast (0 connect retries) so
// the alert appears promptly. Set before bootstrap via addInitScript.
await page.addInitScript(() => {
localStorage.setItem('THREADPLANE_E2E_MAX_RETRIES', '0');
});

await openDemo(page, '/embed');

await page.route('**/runs/stream', async (route) => {
Expand Down
13 changes: 11 additions & 2 deletions examples/chat/angular/src/app/shell/demo-shell.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
import { PalettePersistence } from './palette-persistence.service';
import { ProjectsService } from './projects.service';
import { DEMO_AGENT } from './shell-tokens';
import { e2eClientOptions } from './e2e-overrides';
import { createCanonicalDemoRuntimeTelemetrySink } from './runtime-telemetry';
import { environment } from '../../environments/environment';

Expand Down Expand Up @@ -85,9 +86,17 @@ function parseUrl(url: string): { mode: DemoMode; threadId: string | null } {
// model selection — all per-instance. The telemetry sink delegates to the
// shell-built sink (populated in the constructor) since the real sink needs
// the injected telemetry service + live model() read.
provideAgent({
// Factory form: the config is resolved lazily at injection time (when the
// AGENT singleton is first constructed), not at module-load. This matters
// for `clientOptions` below — the e2e flag in localStorage is only reliably
// readable once the app is running, after bootstrap.
provideAgent(() => ({
apiUrl: environment.langGraphApiUrl,
assistantId: environment.assistantId,
// Production keeps the SDK's default connect-retry budget. e2e specs that
// force a connection failure set localStorage['THREADPLANE_E2E_MAX_RETRIES']
// so the error surfaces immediately instead of after the backoff window.
...(e2eClientOptions() ? { clientOptions: e2eClientOptions() } : {}),
threadId: threadIdState,
onThreadId: (id: string) => {
// The signal→URL effect picks this up and stamps the new id
Expand All @@ -100,7 +109,7 @@ function parseUrl(url: string): { mode: DemoMode; threadId: string | null } {
// resulting tools:<id>-namespaced stream events.
subagentToolNames: ['research'],
telemetry: (event) => telemetrySink?.(event),
}),
})),
{ provide: DEMO_AGENT, useFactory: () => inject(DemoShell).agent },
],
})
Expand Down
29 changes: 29 additions & 0 deletions examples/chat/angular/src/app/shell/e2e-overrides.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
import { e2eClientOptions } from './e2e-overrides';

const KEY = 'THREADPLANE_E2E_MAX_RETRIES';

describe('e2eClientOptions', () => {
afterEach(() => localStorage.removeItem(KEY));

it('returns undefined when the flag is absent (production default preserved)', () => {
expect(e2eClientOptions()).toBeUndefined();
});

it('maps "0" to { maxRetries: 0 } (fail-fast under test)', () => {
localStorage.setItem(KEY, '0');
expect(e2eClientOptions()).toEqual({ maxRetries: 0 });
});

it('maps a positive integer string to that retry budget', () => {
localStorage.setItem(KEY, '3');
expect(e2eClientOptions()).toEqual({ maxRetries: 3 });
});

it('ignores non-integer / negative / garbage values', () => {
for (const bad of ['', 'abc', '-1', '1.5', 'NaN']) {
localStorage.setItem(KEY, bad);
expect(e2eClientOptions()).toBeUndefined();
}
});
});
32 changes: 32 additions & 0 deletions examples/chat/angular/src/app/shell/e2e-overrides.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
import type { LangGraphClientOptions } from '@threadplane/langgraph';

/**
* Test-only escape hatch for the LangGraph SDK client tuning.
*
* Production keeps the SDK's resilient default (connect failures retry with
* exponential backoff before the error surfaces — good for transient blips,
* but a deliberate ~15s delay on a hard failure). e2e specs that force a
* connection failure can't wait that long, so they set
* `localStorage['THREADPLANE_E2E_MAX_RETRIES'] = '0'` (via
* `page.addInitScript`, before the app bootstraps) to make the error surface
* immediately.
*
* Returns `undefined` when the flag is absent — i.e. always in real use — so
* the SDK default is preserved. Never reads anything in production.
*/
const E2E_MAX_RETRIES_KEY = 'THREADPLANE_E2E_MAX_RETRIES';

export function e2eClientOptions(): LangGraphClientOptions | undefined {
if (typeof localStorage === 'undefined') return undefined;
let raw: string | null = null;
try {
raw = localStorage.getItem(E2E_MAX_RETRIES_KEY);
} catch {
return undefined; // storage blocked (e.g. sandboxed iframe) — ignore
}
if (raw === null || raw.trim() === '') return undefined;
const maxRetries = Number(raw);
if (!Number.isInteger(maxRetries) || maxRetries < 0) return undefined;
return { maxRetries };
}
4 changes: 4 additions & 0 deletions libs/langgraph/src/lib/agent.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { agent } from './agent.fn';
import type {
AgentTransport,
LangGraphAgent,
LangGraphClientOptions,
} from './agent.types';

/**
Expand Down Expand Up @@ -38,6 +39,8 @@ export interface AgentConfig<
toMessage?: (msg: unknown) => BaseMessage;
/** Custom transport. Defaults to {@link FetchStreamTransport}. */
transport?: AgentTransport;
/** Tuning options for the default transport's LangGraph SDK client (e.g. retry budget). */
clientOptions?: LangGraphClientOptions;
/** Optional app-owned telemetry sink. No telemetry is emitted unless this is provided. */
telemetry?: AgentRuntimeTelemetrySink | false;
/** When true, subagent messages are filtered from the main messages signal. */
Expand Down Expand Up @@ -117,6 +120,7 @@ export function provideAgent<T = Record<string, unknown>>(
...(config.throttle !== undefined ? { throttle: config.throttle } : {}),
...(config.toMessage !== undefined ? { toMessage: config.toMessage } : {}),
...(config.transport !== undefined ? { transport: config.transport } : {}),
...(config.clientOptions !== undefined ? { clientOptions: config.clientOptions } : {}),
...(config.telemetry !== undefined ? { telemetry: config.telemetry } : {}),
...(config.filterSubagentMessages !== undefined ? { filterSubagentMessages: config.filterSubagentMessages } : {}),
...(config.subagentToolNames !== undefined ? { subagentToolNames: config.subagentToolNames } : {}),
Expand Down
20 changes: 20 additions & 0 deletions libs/langgraph/src/lib/agent.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,24 @@ export interface AgentTransport {
// The second generic is retained for source compatibility with existing typed
// AgentOptions references even though the options shape no longer depends on it.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
/**
* Tuning options for the underlying LangGraph SDK `Client` constructed by the
* default {@link FetchStreamTransport}. Ignored when a custom `transport` is
* supplied (the transport owns its own client).
*/
export interface LangGraphClientOptions {
/**
* How many times a failed request — including the initial stream connect —
* is retried with exponential backoff before the error surfaces. Maps to the
* SDK's `callerOptions.maxRetries`. Omitted → the SDK default (currently 4).
*
* Set `0` to fail fast: useful for e2e tests that force a connection failure
* and assert the error surfaces promptly, rather than after the full
* multi-second backoff window.
*/
maxRetries?: number;
}

export interface AgentOptions<T, _ResolvedBag extends BagTemplate> {
/** Base URL of the LangGraph Platform API. Defaults to `provideAgent({ apiUrl })` when omitted. */
apiUrl?: string;
Expand All @@ -257,6 +275,8 @@ export interface AgentOptions<T, _ResolvedBag extends BagTemplate> {
toMessage?: (msg: unknown) => BaseMessage;
/** Custom transport. Defaults to FetchStreamTransport. */
transport?: AgentTransport;
/** Tuning options for the default transport's LangGraph SDK client (e.g. retry budget). */
clientOptions?: LangGraphClientOptions;
/** Optional app-owned telemetry sink. No telemetry is emitted unless this is provided. */
telemetry?: AgentRuntimeTelemetrySink | false;
/** When true, subagent messages are filtered from the main messages signal. */
Expand Down
20 changes: 18 additions & 2 deletions libs/langgraph/src/lib/client/create-langgraph-client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: MIT
import { Client } from '@langchain/langgraph-sdk';
import type { LangGraphClientOptions } from '../agent.types';

/**
* Construct a LangGraph SDK Client that accepts both absolute URLs
Expand All @@ -12,14 +13,29 @@ import { Client } from '@langchain/langgraph-sdk';
* transport (`fetch-stream.transport.ts`) and the threads adapter
* (`LangGraphThreadsAdapter`) both go through here.
*
* `clientOptions.maxRetries` maps to the SDK's `callerOptions.maxRetries`,
* which governs how many times a failed request (including the initial
* stream connect) is retried with exponential backoff before the error
* surfaces. Omitted → the SDK default (currently 4). Apps under test set
* `0` so a forced connection failure surfaces immediately instead of after
* the full backoff window.
*
* @example
* ```ts
* const client = createLangGraphClient(environment.langGraphApiUrl);
* const threads = await client.threads.search({ limit: 50 });
* ```
*/
export function createLangGraphClient(apiUrl: string): Client {
return new Client({ apiUrl: toAbsoluteApiUrl(apiUrl) });
export function createLangGraphClient(
apiUrl: string,
clientOptions?: LangGraphClientOptions,
): Client {
return new Client({
apiUrl: toAbsoluteApiUrl(apiUrl),
...(clientOptions?.maxRetries !== undefined
? { callerOptions: { maxRetries: clientOptions.maxRetries } }
: {}),
});
}

/** Exported separately so non-Client callers (e.g. raw fetch) can
Expand Down
2 changes: 1 addition & 1 deletion libs/langgraph/src/lib/internals/stream-manager.bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export function createStreamManagerBridge<T, ResolvedBag extends BagTemplate = B
userOnThreadId?.(id);
};
const transport: AgentTransport =
options.transport ?? new FetchStreamTransport(options.apiUrl, wrappedOnThreadId);
options.transport ?? new FetchStreamTransport(options.apiUrl, wrappedOnThreadId, options.clientOptions);

let currentThreadId: string | null = null;
let lastPayload: unknown = null;
Expand Down
7 changes: 4 additions & 3 deletions libs/langgraph/src/lib/transport/fetch-stream.transport.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
import type { Client, StreamMode, ThreadState } from '@langchain/langgraph-sdk';
import type { AgentQueueEntry, AgentTransport, LangGraphSubmitOptions, StreamEvent } from '../agent.types';
import type { AgentQueueEntry, AgentTransport, LangGraphClientOptions, LangGraphSubmitOptions, StreamEvent } from '../agent.types';
import { createLangGraphClient } from '../client/create-langgraph-client';

/**
Expand All @@ -24,12 +24,13 @@ export class FetchStreamTransport implements AgentTransport {
/**
* @param apiUrl - Base URL of the LangGraph Platform API
* @param onThreadId - Optional callback invoked when a new thread is created
* @param clientOptions - Optional SDK client tuning (e.g. `maxRetries`)
*/
constructor(apiUrl: string, onThreadId?: (id: string) => void) {
constructor(apiUrl: string, onThreadId?: (id: string) => void, clientOptions?: LangGraphClientOptions) {
// createLangGraphClient handles the absolute-URL normalization
// required by the SDK when `apiUrl` is a relative `/api`-style
// path proxied by middleware in production.
this.client = createLangGraphClient(apiUrl);
this.client = createLangGraphClient(apiUrl, clientOptions);
this.onThreadId = onThreadId;
}

Expand Down
1 change: 1 addition & 0 deletions libs/langgraph/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export { AgentLifecycleRegistry } from './lib/agent-lifecycle-registry';
// Public types
export type {
AgentOptions,
LangGraphClientOptions,
AgentBranchTree,
AgentBranchTreeFork,
AgentBranchTreeNode,
Expand Down
Loading