Add turbo, fix formatting

This commit is contained in:
Tat Dat Duong
2025-02-05 01:57:45 +01:00
parent f580253ff6
commit cfeff355e0
75 changed files with 746 additions and 554 deletions
+2 -1
View File
@@ -5,4 +5,5 @@ dist
.venv
__pycache__
.langgraph_api
.uv
.uv
.turbo
+36
View File
@@ -0,0 +1,36 @@
# Contributing to Agent Protocol
👋 Hi there! Thank you for being interested in contributing to LangGraph.
As an open source project in a rapidly developing field, we are extremely open
to contributions, whether it be in the form of a new feature, improved infra, or better documentation.
To contribute to this project, please follow a ["fork and pull request"](https://docs.github.com/en/get-started/quickstart/contributing-to-projects) workflow. Please do not try to push directly to this repo unless you are a maintainer.
## 🗺️ Contributing Guidelines
Our [issues](https://github.com/langchain-ai/langgraphjs/issues) page contains
with bugs, improvements, and feature requests.
If you start working on an issue, please comment and a maintainer can assign it to you.
If you are adding an issue, please try to keep it focused on a single modular bug/improvement/feature.
If the two issues are related, or blocking, please link them rather than keep them as one single one.
We will try to keep these issues as up to date as possible, though
with the rapid rate of development in this field some may get out of date.
If you notice this happening, please just let us know.
## 🚀 Setup
To get started, you will need to install the dependencies for the project. To do so, run:
```bash
pnpm install
```
To build the project, run:
```bash
pnpm build
```
+21
View File
@@ -0,0 +1,21 @@
MIT License Copyright (c) 2025 LangChain
Permission is hereby granted, free
of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice
(including the next paragraph) shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+1 -1
View File
@@ -1,3 +1,3 @@
# LangGraph.js API and CLI
This repository contains the source code for the in-memory implementation of the LangGraph.js API and the LangGraph.js CLI tools.
This repository contains the source code for the in-memory implementation of the LangGraph.js API and the LangGraph.js CLI tools.
+1
View File
@@ -0,0 +1 @@
{}
+21
View File
@@ -0,0 +1,21 @@
MIT License Copyright (c) 2025 LangChain
Permission is hereby granted, free
of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice
(including the next paragraph) shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+5 -2
View File
@@ -11,10 +11,13 @@
"dist"
],
"scripts": {
"clean": "npx -y bun scripts/clean.mjs",
"cli": "tsx src/cli.mts",
"cli:watch": "tsx watch src/cli.mts",
"build": "rm -rf dist && tsc --outDir dist",
"prepack": "pnpm run build"
"build": "npx -y bun scripts/build.mjs",
"prepack": "pnpm run build",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"dependencies": {
"@clack/prompts": "^0.9.1",
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bun
function $(strings, ...rest) {
console.log("$", ...strings.raw);
return Bun.$(strings, ...rest);
}
await $`rm -rf dist`;
await $`pnpm tsc --outDir dist`;
-8
View File
@@ -1,8 +0,0 @@
Set-PSDebug -Trace 1
$ErrorActionPreference = "Stop"
Remove-Item -Path "dist" -Recurse -Force -ErrorAction SilentlyContinue
tsc --outDir dist
Move-Item -Path "dist/src/*" -Destination "dist"
Remove-Item -Path "dist/src","dist/tests" -Recurse -Force -ErrorAction SilentlyContinue
-9
View File
@@ -1,9 +0,0 @@
#!/bin/bash
set -e # Exit on error
set -x # Print commands before execution
rm -rf dist
tsc --outDir dist
mv dist/src/* dist
rm -rf dist/src dist/tests
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bun
function $(strings, ...rest) {
console.log("$", ...strings.raw);
return Bun.$(strings, ...rest);
}
await $`rm -rf dist`;
-16
View File
@@ -1,16 +0,0 @@
#!/usr/bin/env node
import { spawn } from "node:child_process";
import * as process from "node:process";
const file = process.argv.at(-1);
const proc =
process.platform === "win32"
? spawn(
"powershell.exe",
["-ExecutionPolicy", "Bypass", "-File", `scripts/${file}.ps1`],
{ stdio: "inherit" }
)
: spawn("bash", ["-c", `scripts/${file}.sh`], { stdio: "inherit" });
proc.on("exit", (code) => process.exit(code ?? 0));
+9 -9
View File
@@ -56,7 +56,7 @@ const TEMPLATE_ID_TO_CONFIG = Object.entries(TEMPLATES).reduce(
});
return acc;
},
{} as Record<string, [string, string, string]>
{} as Record<string, [string, string, string]>,
);
const TEMPLATE_IDS = Object.keys(TEMPLATE_ID_TO_CONFIG);
@@ -84,7 +84,7 @@ async function downloadAndExtract(url: string, targetPath: string) {
// Move files from the extracted directory to target path
const extractedDir = (await fs.readdir(targetPath)).find((f) =>
f.endsWith("-main")
f.endsWith("-main"),
);
if (extractedDir) {
const fullExtractedPath = path.join(targetPath, extractedDir);
@@ -93,16 +93,16 @@ async function downloadAndExtract(url: string, targetPath: string) {
files.map((file) =>
fs.rename(
path.join(fullExtractedPath, file),
path.join(targetPath, file)
)
)
path.join(targetPath, file),
),
),
);
await fs.rmdir(fullExtractedPath);
}
} catch (error: unknown) {
if (error instanceof Error) {
throw new Error(
`Failed to download and extract template: ${error.message}`
`Failed to download and extract template: ${error.message}`,
);
}
throw new Error("Failed to download and extract template");
@@ -181,14 +181,14 @@ async function createNew(projectPath?: string, templateId?: string) {
- cd ${path.relative(process.cwd(), absolutePath)}
- yarn install
- npx @langchain/langgraph-cli@latest dev
`
`,
)
: null;
outro(
[`Project created successfully at ${color.green(absolutePath)}`, guide]
.filter(Boolean)
.join("\n\n")
.join("\n\n"),
);
}
@@ -205,7 +205,7 @@ const program = new Command()
if (!config) {
console.error(`Invalid template ID "${options.template}"`);
console.error(
`Available options:\n${TEMPLATE_IDS.map((id) => `- ${id}`).join("\n")}`
`Available options:\n${TEMPLATE_IDS.map((id) => `- ${id}`).join("\n")}`,
);
process.exit(1);
}
@@ -35,7 +35,7 @@ let analyticsPromise = Promise.resolve();
export function withAnalytics<TCommand extends Command<any, any, any>>(
fn?: (command: TCommand) => Record<string, boolean>,
options?: { name?: string }
options?: { name?: string },
) {
if (process.env.LANGGRAPH_CLI_NO_ANALYTICS === "1") {
return () => void 0;
@@ -50,7 +50,7 @@ export function withAnalytics<TCommand extends Command<any, any, any>>(
cli_version: version,
cli_command: options?.name ?? actionCommand.name(),
params: fn?.(actionCommand) ?? {},
}).catch(() => {})
}).catch(() => {}),
);
};
}
+1 -1
View File
@@ -4,7 +4,7 @@ import * as url from "node:url";
async function getVersion() {
try {
const packageJson = url.fileURLToPath(
new URL("../../package.json", import.meta.url)
new URL("../../package.json", import.meta.url),
);
const { version } = JSON.parse(await fs.readFile(packageJson, "utf-8"));
return version;
+1
View File
@@ -0,0 +1 @@
{}
+1 -1
View File
@@ -1,3 +1,3 @@
# LangGraph.js API
In-memory implementation of the LangGraph.js API.
In-memory implementation of the LangGraph.js API.
+4 -1
View File
@@ -11,13 +11,16 @@
"dist/"
],
"scripts": {
"clean": "npx -y bun scripts/clean.mjs",
"build": "npx -y bun scripts/build.mjs",
"dev": "tsx ./tests/utils.server.mts",
"prepack": "pnpm run build",
"typecheck": "tsc --noEmit",
"test": "vitest",
"test:parser": "vitest run ./tests/parser.test.mts --testTimeout 15000",
"test:api": "npx -y bun scripts/test.mjs"
"test:api": "npx -y bun scripts/test.mjs",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"dependencies": {
"@babel/code-frame": "^7.26.2",
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bun
function $(strings, ...rest) {
console.log("$", ...strings.raw);
return Bun.$(strings, ...rest);
}
await $`rm -rf dist`;
+1 -1
View File
@@ -12,6 +12,6 @@ await Promise.race([
(async () => {
await $`bun x wait-port -t 12000 localhost:2024`;
await $`pnpm vitest run --exclude ./tests/parser.test.mts"`;
process.exit(0)
process.exit(0);
})(),
]);
+12 -12
View File
@@ -27,7 +27,7 @@ api.post(
});
return c.json(assistant);
}
},
);
api.post(
@@ -48,7 +48,7 @@ api.post(
}
return c.json(result);
}
},
);
api.get("/assistants/:assistant_id", async (c) => {
@@ -72,7 +72,7 @@ api.patch(
const payload = c.req.valid("json");
return c.json(await Assistants.patch(assistantId, payload));
}
},
);
const RunnableConfigSchema = z.object({
@@ -86,7 +86,7 @@ const RunnableConfigSchema = z.object({
});
const getRunnableConfig = (
userConfig: z.infer<typeof RunnableConfigSchema> | null | undefined
userConfig: z.infer<typeof RunnableConfigSchema> | null | undefined,
) => {
if (!userConfig) return {};
return {
@@ -111,9 +111,9 @@ api.get(
const graph = getGraph(assistant.graph_id);
return c.json(
graph.getGraph({ ...getRunnableConfig(assistant.config), xray }).toJSON()
graph.getGraph({ ...getRunnableConfig(assistant.config), xray }).toJSON(),
);
}
},
);
api.get("/assistants/:assistant_id/schemas", async (c) => {
@@ -140,7 +140,7 @@ api.get(
"/assistants/:assistant_id/subgraphs/:namespace?",
zValidator(
"param",
z.object({ assistant_id: z.string(), namespace: z.string().optional() })
z.object({ assistant_id: z.string(), namespace: z.string().optional() }),
),
zValidator("query", z.object({ recurse: schemas.coercedBoolean.optional() })),
async (c) => {
@@ -173,7 +173,7 @@ api.get(
}
return c.json(Object.fromEntries(result));
}
},
);
api.post(
@@ -184,7 +184,7 @@ api.post(
const assistantId = getAssistantId(c.req.param("assistant_id"));
const { version } = c.req.valid("json");
return c.json(await Assistants.setLatest(assistantId, version));
}
},
);
api.post(
@@ -195,7 +195,7 @@ api.post(
limit: z.number().min(1).max(1000).optional().default(10),
offset: z.number().min(0).optional().default(0),
metadata: z.record(z.unknown()).optional(),
})
}),
),
async (c) => {
// Get Assistant Versions
@@ -206,9 +206,9 @@ api.post(
limit,
offset,
metadata,
})
}),
);
}
},
);
export default api;
+28 -28
View File
@@ -19,7 +19,7 @@ const api = new Hono();
const createValidRun = async (
threadId: string | undefined,
payload: z.infer<typeof schemas.RunCreate>
payload: z.infer<typeof schemas.RunCreate>,
): Promise<Run> => {
const { assistant_id: assistantId, ...run } = payload;
const runId = uuid4();
@@ -78,7 +78,7 @@ const createValidRun = async (
preventInsertInInflight,
afterSeconds: payload.after_seconds,
ifNotExists: payload.if_not_exists,
}
},
);
if (first?.run_id === runId) {
@@ -91,7 +91,7 @@ const createValidRun = async (
await Runs.cancel(
threadId,
inflight.map((run) => run.run_id),
{ action: multitaskStrategy }
{ action: multitaskStrategy },
);
} catch (error) {
logger.warn(
@@ -100,7 +100,7 @@ const createValidRun = async (
error,
run_ids: inflight.map((run) => run.run_id),
thread_id: threadId,
}
},
);
}
}
@@ -129,7 +129,7 @@ api.post(
async () => {
// Search Crons
throw new HTTPException(500, { message: "Not implemented" });
}
},
);
api.delete(
@@ -138,7 +138,7 @@ api.delete(
async () => {
// Delete Cron
throw new HTTPException(500, { message: "Not implemented" });
}
},
);
api.post(
@@ -148,7 +148,7 @@ api.post(
async () => {
// Create Thread Cron
throw new HTTPException(500, { message: "Not implemented" });
}
},
);
api.post("/runs/stream", zValidator("json", schemas.RunCreate), async (c) => {
@@ -166,7 +166,7 @@ api.post("/runs/stream", zValidator("json", schemas.RunCreate), async (c) => {
for await (const { event, data } of Runs.Stream.join(
run.run_id,
undefined,
{ cancelOnDisconnect, ignore404: true }
{ cancelOnDisconnect, ignore404: true },
)) {
await stream.writeSSE({ data: serialiseAsDict(data), event });
}
@@ -197,10 +197,10 @@ api.post(
// Batch Runs
const payload = c.req.valid("json");
const runs = await Promise.all(
payload.map((run) => createValidRun(undefined, run))
payload.map((run) => createValidRun(undefined, run)),
);
return jsonExtra(c, runs);
}
},
);
api.get(
@@ -213,7 +213,7 @@ api.get(
offset: z.coerce.number().nullish(),
status: z.string().nullish(),
metadata: z.record(z.string(), z.unknown()).nullish(),
})
}),
),
async (c) => {
// List runs
@@ -231,7 +231,7 @@ api.get(
]);
return jsonExtra(c, runs);
}
},
);
api.post(
@@ -245,7 +245,7 @@ api.post(
const run = await createValidRun(thread_id, payload);
return jsonExtra(c, run);
}
},
);
api.post(
@@ -268,7 +268,7 @@ api.post(
for await (const { event, data } of Runs.Stream.join(
run.run_id,
thread_id,
{ cancelOnDisconnect }
{ cancelOnDisconnect },
)) {
await stream.writeSSE({ data: serialiseAsDict(data), event });
}
@@ -276,7 +276,7 @@ api.post(
logError(error, { prefix: "Error streaming run" });
}
});
}
},
);
api.post(
@@ -290,14 +290,14 @@ api.post(
const run = await createValidRun(thread_id, payload);
return waitKeepAlive(c, Runs.join(run.run_id, thread_id));
}
},
);
api.get(
"/threads/:thread_id/runs/:run_id",
zValidator(
"param",
z.object({ thread_id: z.string().uuid(), run_id: z.string().uuid() })
z.object({ thread_id: z.string().uuid(), run_id: z.string().uuid() }),
),
async (c) => {
const { thread_id, run_id } = c.req.valid("param");
@@ -307,45 +307,45 @@ api.get(
]);
return jsonExtra(c, run);
}
},
);
api.delete(
"/threads/:thread_id/runs/:run_id",
zValidator(
"param",
z.object({ thread_id: z.string().uuid(), run_id: z.string().uuid() })
z.object({ thread_id: z.string().uuid(), run_id: z.string().uuid() }),
),
async (c) => {
// Delete Run
const { thread_id, run_id } = c.req.valid("param");
await Runs.delete(run_id, thread_id);
return c.body(null, 204);
}
},
);
api.get(
"/threads/:thread_id/runs/:run_id/join",
zValidator(
"param",
z.object({ thread_id: z.string().uuid(), run_id: z.string().uuid() })
z.object({ thread_id: z.string().uuid(), run_id: z.string().uuid() }),
),
async (c) => {
// Join Run Http
const { thread_id, run_id } = c.req.valid("param");
return jsonExtra(c, await Runs.join(run_id, thread_id));
}
},
);
api.get(
"/threads/:thread_id/runs/:run_id/stream",
zValidator(
"param",
z.object({ thread_id: z.string().uuid(), run_id: z.string().uuid() })
z.object({ thread_id: z.string().uuid(), run_id: z.string().uuid() }),
),
zValidator(
"query",
z.object({ cancel_on_disconnect: schemas.coercedBoolean.optional() })
z.object({ cancel_on_disconnect: schemas.coercedBoolean.optional() }),
),
async (c) => {
// Stream Run Http
@@ -362,21 +362,21 @@ api.get(
await stream.writeSSE({ data: serialiseAsDict(data), event });
}
});
}
},
);
api.post(
"/threads/:thread_id/runs/:run_id/cancel",
zValidator(
"param",
z.object({ thread_id: z.string().uuid(), run_id: z.string().uuid() })
z.object({ thread_id: z.string().uuid(), run_id: z.string().uuid() }),
),
zValidator(
"query",
z.object({
wait: z.coerce.boolean().optional().default(false),
action: z.enum(["interrupt", "rollback"]).optional().default("interrupt"),
})
}),
),
async (c) => {
// Cancel Run Http
@@ -386,7 +386,7 @@ api.post(
await Runs.cancel(thread_id, [run_id], { action });
if (wait) await Runs.join(run_id, thread_id);
return c.body(null, wait ? 204 : 202);
}
},
);
export default api;
+4 -4
View File
@@ -54,7 +54,7 @@ api.post(
maxDepth: payload.max_depth,
}),
});
}
},
);
api.post(
@@ -73,7 +73,7 @@ api.post(
});
return c.json({ items: items.map(mapItemsToApi) });
}
},
);
api.put("/store/items", zValidator("json", schemas.StorePutItem), async (c) => {
@@ -93,7 +93,7 @@ api.delete(
if (payload.namespace) validateNamespace(payload.namespace);
await storageStore.delete(payload.namespace ?? [], payload.key);
return c.body(null, 204);
}
},
);
api.get(
@@ -105,7 +105,7 @@ api.get(
const key = payload.key;
const namespace = payload.namespace;
return c.json(mapItemsToApi(await storageStore.get(namespace, key)));
}
},
);
export default api;
+29 -26
View File
@@ -46,7 +46,7 @@ api.post(
}
return jsonExtra(c, result);
}
},
);
api.get(
@@ -54,7 +54,7 @@ api.get(
zValidator("param", z.object({ thread_id: z.string().uuid() })),
zValidator(
"query",
z.object({ subgraphs: schemas.coercedBoolean.optional() })
z.object({ subgraphs: schemas.coercedBoolean.optional() }),
),
async (c) => {
// Get Latest Thread State
@@ -62,11 +62,11 @@ api.get(
const { subgraphs } = c.req.valid("query");
const state = stateSnapshotToThreadState(
await Threads.State.get({ configurable: { thread_id } }, { subgraphs })
await Threads.State.get({ configurable: { thread_id } }, { subgraphs }),
);
return jsonExtra(c, state);
}
},
);
api.post(
@@ -84,7 +84,7 @@ api.post(
as_node: z.string().optional(),
checkpoint_id: z.string().optional(),
checkpoint: schemas.CheckpointSchema.nullish(),
})
}),
),
async (c) => {
// Update Thread State
@@ -104,22 +104,25 @@ api.post(
const inserted = await Threads.State.post(
config,
payload.values,
payload.as_node
payload.as_node,
);
return jsonExtra(c, inserted);
}
},
);
api.get(
"/threads/:thread_id/state/:checkpoint_id",
zValidator(
"param",
z.object({ thread_id: z.string().uuid(), checkpoint_id: z.string().uuid() })
z.object({
thread_id: z.string().uuid(),
checkpoint_id: z.string().uuid(),
}),
),
zValidator(
"query",
z.object({ subgraphs: schemas.coercedBoolean.optional() })
z.object({ subgraphs: schemas.coercedBoolean.optional() }),
),
async (c) => {
// Get Thread State At Checkpoint
@@ -128,12 +131,12 @@ api.get(
const state = stateSnapshotToThreadState(
await Threads.State.get(
{ configurable: { thread_id, checkpoint_id } },
{ subgraphs }
)
{ subgraphs },
),
);
return jsonExtra(c, state);
}
},
);
api.post(
@@ -144,7 +147,7 @@ api.post(
z.object({
subgraphs: schemas.coercedBoolean.optional(),
checkpoint: schemas.CheckpointSchema.nullish(),
})
}),
),
async (c) => {
// Get Thread State At Checkpoint Post
@@ -154,12 +157,12 @@ api.post(
const state = stateSnapshotToThreadState(
await Threads.State.get(
{ configurable: { thread_id, ...checkpoint } },
{ subgraphs }
)
{ subgraphs },
),
);
return jsonExtra(c, state);
}
},
);
api.get(
@@ -174,7 +177,7 @@ api.get(
.default("10")
.transform((value) => parseInt(value, 10)),
before: z.string().optional(),
})
}),
),
async (c) => {
// Get Thread History
@@ -183,10 +186,10 @@ api.get(
const states = await Threads.State.list(
{ configurable: { thread_id, checkpoint_ns: "" } },
{ limit, before }
{ limit, before },
);
return jsonExtra(c, states.map(stateSnapshotToThreadState));
}
},
);
api.post(
@@ -205,7 +208,7 @@ api.post(
checkpoint_map: z.record(z.string(), z.unknown()).optional(),
})
.optional(),
})
}),
),
async (c) => {
// Get Thread History Post
@@ -214,11 +217,11 @@ api.post(
const states = await Threads.State.list(
{ configurable: { thread_id, checkpoint_ns: "", ...checkpoint } },
{ limit, before, metadata }
{ limit, before, metadata },
);
return jsonExtra(c, states.map(stateSnapshotToThreadState));
}
},
);
api.get(
@@ -228,7 +231,7 @@ api.get(
// Get Thread
const { thread_id } = c.req.valid("param");
return jsonExtra(c, await Threads.get(thread_id));
}
},
);
api.delete(
@@ -239,7 +242,7 @@ api.delete(
const { thread_id } = c.req.valid("param");
await Threads.delete(thread_id);
return new Response(null, { status: 204 });
}
},
);
api.patch(
@@ -251,7 +254,7 @@ api.patch(
const { thread_id } = c.req.valid("param");
const { metadata } = c.req.valid("json");
return jsonExtra(c, await Threads.patch(thread_id, { metadata }));
}
},
);
api.post(
@@ -261,7 +264,7 @@ api.post(
// Copy Thread
const { thread_id } = c.req.valid("param");
return jsonExtra(c, await Threads.copy(thread_id));
}
},
);
export default api;
+3 -3
View File
@@ -15,7 +15,7 @@ export async function spawnServer(
options: {
pid: number;
projectCwd: string;
}
},
) {
const localUrl = `http://${args.host}:${args.port}`;
const studioUrl = `${context.hostUrl}/studio?baseUrl=${localUrl}`;
@@ -39,7 +39,7 @@ For production use, please use LangGraph Cloud.
process.execPath,
[
fileURLToPath(
new URL("../../cli.mjs", import.meta.resolve("tsx/esm/api"))
new URL("../../cli.mjs", import.meta.resolve("tsx/esm/api")),
),
"watch",
"--clear-screen=false",
@@ -53,6 +53,6 @@ For production use, please use LangGraph Cloud.
cwd: options.projectCwd,
}),
],
{ stdio: ["inherit", "inherit", "inherit", "ipc"], env: context.env }
{ stdio: ["inherit", "inherit", "inherit", "ipc"], env: context.env },
);
}
@@ -1,17 +1,17 @@
// MIT License
//
//
// Copyright (c) Hiroki Osame <hiroki.osame@gmail.com>
//
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -19,7 +19,7 @@
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
//
// https://github.com/privatenumber/tsx/tree/28a3e7d2b8fd72b683aab8a98dd1fcee4624e4cb
import net from "node:net";
import { getPipePath } from "./utils/get-pipe-path.mjs";
@@ -1,17 +1,17 @@
// MIT License
//
//
// Copyright (c) Hiroki Osame <hiroki.osame@gmail.com>
//
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -19,7 +19,7 @@
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
//
// https://github.com/privatenumber/tsx/tree/28a3e7d2b8fd72b683aab8a98dd1fcee4624e4cb
import path from "node:path";
import os from "node:os";
+4 -4
View File
@@ -23,7 +23,7 @@ export const GRAPH_SPEC: Record<string, GraphSpec> = {};
export const GRAPH_SCHEMA: Record<string, Record<string, GraphSchema>> = {};
export const NAMESPACE_GRAPH = uuid.parse(
"6ba7b821-9dad-11d1-80b4-00c04fd430c8"
"6ba7b821-9dad-11d1-80b4-00c04fd430c8",
);
const ConfigSchema = z.record(z.unknown());
@@ -35,7 +35,7 @@ export const getAssistantId = (graphId: string) => {
export async function registerFromEnv(
specs: Record<string, string>,
options: { cwd: string }
options: { cwd: string },
) {
const envConfig = process.env.LANGGRAPH_CONFIG
? ConfigSchema.parse(JSON.parse(process.env.LANGGRAPH_CONFIG))
@@ -65,7 +65,7 @@ export async function registerFromEnv(
});
return resolved;
})
}),
);
}
@@ -74,7 +74,7 @@ export function getGraph(
options?: {
checkpointer?: BaseCheckpointSaver | null;
store?: BaseStore;
}
},
) {
if (!GRAPHS[graphId])
throw new HTTPException(404, { message: `Graph "${graphId}" not found` });
+5 -5
View File
@@ -8,7 +8,7 @@ import type { JSONSchema7 } from "json-schema";
export const GRAPHS: Record<string, CompiledGraph<string>> = {};
export const NAMESPACE_GRAPH = uuid.parse(
"6ba7b821-9dad-11d1-80b4-00c04fd430c8"
"6ba7b821-9dad-11d1-80b4-00c04fd430c8",
);
export interface GraphSchema {
@@ -25,7 +25,7 @@ export interface GraphSpec {
export async function resolveGraph(
spec: string,
options: { cwd: string; onlyFilePresence?: false }
options: { cwd: string; onlyFilePresence?: false },
): Promise<{
sourceFile: string;
exportSymbol: string;
@@ -34,12 +34,12 @@ export async function resolveGraph(
export async function resolveGraph(
spec: string,
options: { cwd: string; onlyFilePresence: true }
options: { cwd: string; onlyFilePresence: true },
): Promise<{ sourceFile: string; exportSymbol: string; resolved: undefined }>;
export async function resolveGraph(
spec: string,
options: { cwd: string; onlyFilePresence?: boolean }
options: { cwd: string; onlyFilePresence?: boolean },
) {
const [userFile, exportSymbol] = spec.split(":", 2);
const sourceFile = path.resolve(options.cwd, userFile);
@@ -84,7 +84,7 @@ export async function runGraphSchemaWorker(spec: GraphSpec) {
return await new Promise<Record<string, GraphSchema>>((resolve, reject) => {
const worker = new Worker(
fileURLToPath(new URL("./parser/parser.worker.mjs", import.meta.url))
fileURLToPath(new URL("./parser/parser.worker.mjs", import.meta.url)),
);
// Set a timeout to reject if the worker takes too long
+19 -19
View File
@@ -28,7 +28,7 @@ export class SubgraphExtractor {
program: ts.Program,
sourceFile: ts.SourceFile,
inferFile: ts.SourceFile,
options?: { strict?: boolean }
options?: { strict?: boolean },
) {
this.program = program;
this.sourceFile = sourceFile;
@@ -65,7 +65,7 @@ export class SubgraphExtractor {
private find = (
root: ts.Node,
predicate: (node: ts.Node) => boolean
predicate: (node: ts.Node) => boolean,
): ts.Node | undefined => {
let result: ts.Node | undefined = undefined;
@@ -84,7 +84,7 @@ export class SubgraphExtractor {
protected findSubgraphs = (
node: ts.Node,
namespace: string[] = []
namespace: string[] = [],
): {
node: string;
namespace: string[];
@@ -96,7 +96,7 @@ export class SubgraphExtractor {
namespace: string[];
subgraph: { name: string; node: ts.Node };
}[],
node: ts.Node
node: ts.Node,
) => {
if (ts.isCallExpression(node)) {
const firstChild = node.getChildAt(0);
@@ -128,7 +128,7 @@ export class SubgraphExtractor {
variables = this.reduceChildren(
callArg,
this.findSubgraphIdentifiers,
[]
[],
);
} else if (ts.isIdentifier(callArg)) {
variables = this.findSubgraphIdentifiers([], callArg);
@@ -166,13 +166,13 @@ export class SubgraphExtractor {
type InternalFlowNode = ts.Node & { flowNode?: { node: ts.Node } };
const candidate = this.find(
node,
(node: any) => node && "flowNode" in node && node.flowNode
(node: any) => node && "flowNode" in node && node.flowNode,
) as InternalFlowNode | undefined;
if (
candidate?.flowNode &&
this.isGraphOrPregelType(
this.checker.getTypeAtLocation(candidate.flowNode.node)
this.checker.getTypeAtLocation(candidate.flowNode.node),
)
) {
subgraphs = this.findSubgraphs(candidate.flowNode.node, namespace);
@@ -184,7 +184,7 @@ export class SubgraphExtractor {
return [
...subgraphs,
...subgraphs.map(({ subgraph, node }) =>
this.findSubgraphs(subgraph.node, [...namespace, node])
this.findSubgraphs(subgraph.node, [...namespace, node]),
),
].flat();
}
@@ -199,7 +199,7 @@ export class SubgraphExtractor {
const targetExport = exports.find((item) => item.name === name);
if (!targetExport) throw new Error(`Failed to find export "${name}"`);
const varDecls = (targetExport.declarations ?? []).filter(
ts.isVariableDeclaration
ts.isVariableDeclaration,
);
return varDecls.flatMap((varDecl) => {
@@ -209,7 +209,7 @@ export class SubgraphExtractor {
};
public getAugmentedSourceFile = (
name: string
name: string,
): {
files: [filePath: string, contents: string][];
exports: { typeName: string; valueName: string; graphName: string }[];
@@ -238,7 +238,7 @@ export class SubgraphExtractor {
this.getText(this.sourceFile),
...typeExports.map(
({ typeName, valueName }) =>
`export type ${typeName} = typeof ${valueName}`
`export type ${typeName} = typeof ${valueName}`,
),
].join("\n\n");
@@ -246,7 +246,7 @@ export class SubgraphExtractor {
const inferContents = [
...typeExports.map(
({ typeName }) =>
`import type { ${typeName}} from "./__langgraph__source.mts"`
`import type { ${typeName}} from "./__langgraph__source.mts"`,
),
this.inferFile.getText(this.inferFile),
@@ -277,7 +277,7 @@ export class SubgraphExtractor {
protected findSubgraphIdentifiers = (
acc: { node: ts.Node; name: string }[],
node: ts.Node
node: ts.Node,
) => {
if (ts.isIdentifier(node)) {
const smb = this.checker.getSymbolAtLocation(node);
@@ -322,7 +322,7 @@ export class SubgraphExtractor {
protected reduceChildren<Acc>(
node: ts.Node,
fn: (acc: Acc, node: ts.Node) => Acc,
initial: Acc
initial: Acc,
): Acc {
let acc = initial;
function it(node: ts.Node) {
@@ -343,7 +343,7 @@ export class SubgraphExtractor {
files?: [fileName: string, contents: string][];
},
name: string,
options?: { strict?: boolean }
options?: { strict?: boolean },
) {
const dirname =
typeof target === "string" ? path.dirname(target) : __dirname;
@@ -365,7 +365,7 @@ export class SubgraphExtractor {
const inferTemplatePath = path.resolve(
__dirname,
"./schema/types.template.mts"
"./schema/types.template.mts",
);
if (typeof target !== "string") {
@@ -417,7 +417,7 @@ export class SubgraphExtractor {
research,
research.getSourceFile(targetPath)!,
research.getSourceFile(inferTemplatePath)!,
options
options,
);
const { files, exports } = extractor.getAugmentedSourceFile(name);
@@ -438,7 +438,7 @@ export class SubgraphExtractor {
} catch (e) {
console.warn(
`Failed to obtain symbol "${symbol}":`,
(e as Error)?.message
(e as Error)?.message,
);
}
return undefined;
@@ -453,7 +453,7 @@ export class SubgraphExtractor {
state: trySymbol(schemaGenerator, `${typeName}__update`),
config: trySymbol(schemaGenerator, `${typeName}__config`),
},
])
]),
);
}
}
@@ -6,7 +6,7 @@ parentPort?.on("message", async (payload) => {
const result = SubgraphExtractor.extractSchemas(
payload.sourceFile,
payload.exportSymbol,
{ strict: false }
{ strict: false },
);
parentPort?.postMessage(result);
});
@@ -240,7 +240,7 @@ function resolveRequiredFile(
symbol: ts.Symbol,
key: string,
fileName: string,
objectName: string
objectName: string,
): any {
const sourceFile = getSourceFile(symbol);
const requiredFilePath = /^[.\/]+/.test(fileName)
@@ -404,7 +404,7 @@ function getCanonicalDeclaration(sym: ts.Symbol): ts.Declaration {
const declarationCount = sym.declarations?.length ?? 0;
throw new Error(
`Symbol "${sym.name}" has no valueDeclaration and ${declarationCount} declarations.`
`Symbol "${sym.name}" has no valueDeclaration and ${declarationCount} declarations.`,
);
}
@@ -418,7 +418,7 @@ function getSourceFile(sym: ts.Symbol): ts.SourceFile {
while (currentDecl.kind !== ts.SyntaxKind.SourceFile) {
if (currentDecl.parent === undefined) {
throw new Error(
`Unable to locate source file for declaration "${sym.name}".`
`Unable to locate source file for declaration "${sym.name}".`,
);
}
currentDecl = currentDecl.parent;
@@ -561,7 +561,7 @@ class JsonSchemaGenerator {
userSymbols: { [name: string]: ts.Symbol },
inheritingTypes: { [baseName: string]: string[] },
tc: ts.TypeChecker,
private args = getDefaultArgs()
private args = getDefaultArgs(),
) {
this.symbols = symbols;
this.allSymbols = allSymbols;
@@ -570,7 +570,7 @@ class JsonSchemaGenerator {
this.tc = tc;
this.userValidationKeywords = args.validationKeywords.reduce(
(acc, word) => ({ ...acc, [word]: true }),
{}
{},
);
this.constAsEnum = args.constAsEnum;
}
@@ -606,7 +606,7 @@ class JsonSchemaGenerator {
private parseCommentsIntoDefinition(
symbol: ts.Symbol,
definition: Definition,
otherAnnotations: Record<string, true>
otherAnnotations: Record<string, true>,
): void {
if (!symbol) {
return;
@@ -621,7 +621,7 @@ class JsonSchemaGenerator {
.map((comment) => {
const newlineNormalizedComment = comment.text.replace(
/\r\n/g,
"\n"
"\n",
);
// If a comment contains a "{@link XYZ}" inline tag that could not be
@@ -657,7 +657,7 @@ class JsonSchemaGenerator {
}
} else if (name === "TJS" && text.startsWith("-")) {
let match: string[] | RegExpExecArray | null = new RegExp(
REGEX_TJS_JSDOC
REGEX_TJS_JSDOC,
).exec(originalText);
if (match) {
name = match[1];
@@ -673,7 +673,7 @@ class JsonSchemaGenerator {
// to process the "." and beyond from the value
if (subDefinitions[name]) {
const match: string[] | RegExpExecArray | null = new RegExp(
REGEX_GROUP_JSDOC
REGEX_GROUP_JSDOC,
).exec(text);
if (match) {
const k = match[1];
@@ -716,7 +716,7 @@ class JsonSchemaGenerator {
reffedType: ts.Symbol,
definition: Definition,
defaultNumberType = this.args.defaultNumberType,
ignoreUndefined = false
ignoreUndefined = false,
): Definition {
const tupleType = resolveTupleType(propertyType);
@@ -725,7 +725,7 @@ class JsonSchemaGenerator {
const elemTypes: ts.NodeArray<ts.TypeNode> = (propertyType as any)
.typeArguments;
const fixedTypes = elemTypes.map((elType) =>
this.getTypeDefinition(elType as any)
this.getTypeDefinition(elType as any),
);
definition.type = "array";
if (fixedTypes.length > 0) {
@@ -743,12 +743,12 @@ class JsonSchemaGenerator {
const propertyTypeString = this.tc.typeToString(
propertyType,
undefined,
ts.TypeFormatFlags.UseFullyQualifiedType
ts.TypeFormatFlags.UseFullyQualifiedType,
);
const flags = propertyType.flags;
const arrayType = this.tc.getIndexTypeOfType(
propertyType,
ts.IndexKind.Number
ts.IndexKind.Number,
);
if (flags & ts.TypeFlags.String) {
@@ -826,7 +826,7 @@ class JsonSchemaGenerator {
};
if (
!!Array.from((propertyType as any).members as any[])?.find(
(member: [string]) => member[0] !== "__index"
(member: [string]) => member[0] !== "__index",
)
) {
this.getClassDefinition(propertyType, definition);
@@ -879,7 +879,7 @@ class JsonSchemaGenerator {
} else {
// Report that type could not be processed
const error = new TypeError(
"Unsupported type: " + propertyTypeString
"Unsupported type: " + propertyTypeString,
);
(error as any).type = propertyType;
throw error;
@@ -908,7 +908,7 @@ class JsonSchemaGenerator {
private getDefinitionForProperty(
prop: ts.Symbol,
node: ts.Node
node: ts.Node,
): Definition | null {
if (prop.flags & ts.SymbolFlags.Method) {
return null;
@@ -923,7 +923,7 @@ class JsonSchemaGenerator {
undefined,
undefined,
prop,
reffedType
reffedType,
);
if (this.args.titles) {
@@ -967,12 +967,12 @@ class JsonSchemaGenerator {
definition.default = val;
} else if (val) {
console.warn(
"unknown initializer for property " + propertyName + ": " + val
"unknown initializer for property " + propertyName + ": " + val,
);
}
} catch (e) {
console.warn(
"exception evaluating initializer for property " + propertyName
"exception evaluating initializer for property " + propertyName,
);
}
}
@@ -983,13 +983,13 @@ class JsonSchemaGenerator {
private getEnumDefinition(
clazzType: ts.Type,
definition: Definition
definition: Definition,
): Definition {
const node = clazzType.getSymbol()!.getDeclarations()![0];
const fullName = this.tc.typeToString(
clazzType,
undefined,
ts.TypeFormatFlags.UseFullyQualifiedType
ts.TypeFormatFlags.UseFullyQualifiedType,
);
const members: ts.NodeArray<ts.EnumMember> =
node.kind === ts.SyntaxKind.EnumDeclaration
@@ -1034,7 +1034,7 @@ class JsonSchemaGenerator {
"initializer is expression for enum: " +
fullName +
"." +
caseLabel
caseLabel,
);
}
} else if (
@@ -1068,7 +1068,7 @@ class JsonSchemaGenerator {
private getUnionDefinition(
unionType: ts.UnionType,
unionModifier: keyof Definition,
definition: Definition
definition: Definition,
): Definition {
const enumValues: PrimitiveType[] = [];
const simpleTypes: JSONSchema7TypeName[] = [];
@@ -1100,7 +1100,7 @@ class JsonSchemaGenerator {
symbol,
undefined,
undefined,
true
true,
);
if (def.type === ("undefined" as any)) {
continue;
@@ -1184,7 +1184,7 @@ class JsonSchemaGenerator {
private getIntersectionDefinition(
intersectionType: ts.IntersectionType,
definition: Definition
definition: Definition,
): Definition {
const simpleTypes: JSONSchema7TypeName[] = [];
const schemas: Definition[] = [];
@@ -1230,7 +1230,7 @@ class JsonSchemaGenerator {
private getClassDefinition(
clazzType: ts.Type,
definition: Definition
definition: Definition,
): Definition {
const node = clazzType.getSymbol()!.getDeclarations()![0];
@@ -1277,7 +1277,7 @@ class JsonSchemaGenerator {
const fullName = this.tc.typeToString(
clazzType,
undefined,
ts.TypeFormatFlags.UseFullyQualifiedType
ts.TypeFormatFlags.UseFullyQualifiedType,
);
const modifierFlags = ts.getCombinedModifierFlags(node);
@@ -1297,7 +1297,7 @@ class JsonSchemaGenerator {
clazz.members == null
? []
: clazz.members.filter(
(x) => x.kind === ts.SyntaxKind.IndexSignature
(x) => x.kind === ts.SyntaxKind.IndexSignature,
);
if (indexSignatures.length === 1) {
// for case "array-types"
@@ -1305,21 +1305,21 @@ class JsonSchemaGenerator {
indexSignatures[0] as ts.IndexSignatureDeclaration;
if (indexSignature.parameters.length !== 1) {
throw new Error(
"Not supported: IndexSignatureDeclaration parameters.length != 1"
"Not supported: IndexSignatureDeclaration parameters.length != 1",
);
}
const indexSymbol: ts.Symbol = (indexSignature.parameters[0] as any)
.symbol;
const indexType = this.tc.getTypeOfSymbolAtLocation(
indexSymbol,
node
node,
);
const isIndexedObject =
indexType.flags === ts.TypeFlags.String ||
indexType.flags === ts.TypeFlags.Number;
if (indexType.flags !== ts.TypeFlags.Number && !isIndexedObject) {
throw new Error(
"Not supported: IndexSignatureDeclaration with index symbol other than a number or a string"
"Not supported: IndexSignatureDeclaration with index symbol other than a number or a string",
);
}
@@ -1327,7 +1327,7 @@ class JsonSchemaGenerator {
let def: Definition | undefined;
if (typ.flags & ts.TypeFlags.IndexedAccess) {
const targetName = ts.escapeLeadingUnderscores(
(clazzType as any).mapper?.target?.value
(clazzType as any).mapper?.target?.value,
);
const indexedAccessType = typ as ts.IndexedAccessType;
const symbols: Map<ts.__String, ts.Symbol> = (
@@ -1339,7 +1339,7 @@ class JsonSchemaGenerator {
const targetNode = targetSymbol.getDeclarations()![0];
const targetDef = this.getDefinitionForProperty(
targetSymbol,
targetNode
targetNode,
);
if (targetDef) {
def = targetDef;
@@ -1372,7 +1372,7 @@ class JsonSchemaGenerator {
}
return all;
},
{}
{},
);
if (definition.type === undefined) {
@@ -1403,7 +1403,7 @@ class JsonSchemaGenerator {
order.push(prop.getName());
return order;
},
[]
[],
);
definition.propertyOrder = propertyOrder;
@@ -1427,7 +1427,7 @@ class JsonSchemaGenerator {
}
return required;
},
[]
[],
);
if (requiredProps.length > 0) {
@@ -1454,9 +1454,9 @@ class JsonSchemaGenerator {
typ,
undefined,
ts.TypeFormatFlags.NoTruncation |
ts.TypeFormatFlags.UseFullyQualifiedType
ts.TypeFormatFlags.UseFullyQualifiedType,
)
.replace(REGEX_FILE_NAME_OR_SPACE, "")
.replace(REGEX_FILE_NAME_OR_SPACE, ""),
);
}
@@ -1489,7 +1489,7 @@ class JsonSchemaGenerator {
reffedType?: ts.Symbol,
pairedSymbol?: ts.Symbol,
forceNotRef: boolean = false,
ignoreUndefined = false
ignoreUndefined = false,
): Definition {
const definition: Definition = {}; // real definition
@@ -1574,7 +1574,7 @@ class JsonSchemaGenerator {
.getFullyQualifiedName(
reffedType!.getFlags() & ts.SymbolFlags.Alias
? this.tc.getAliasedSymbol(reffedType!)
: reffedType!
: reffedType!,
)
.replace(REGEX_FILE_NAME_OR_SPACE, "");
if (this.args.uniqueNames && reffedType) {
@@ -1582,7 +1582,7 @@ class JsonSchemaGenerator {
const relativePath = path.relative(process.cwd(), sourceFile.fileName);
fullTypeName = `${typeName}.${generateHashOfNode(
getCanonicalDeclaration(reffedType!),
relativePath
relativePath,
)}`;
} else {
fullTypeName = this.makeTypeNameUnique(typ, typeName);
@@ -1595,7 +1595,7 @@ class JsonSchemaGenerator {
const relativePath = path.relative(process.cwd(), sourceFile.fileName);
fullTypeName = `${this.getTypeName(typ)}.${generateHashOfNode(
getCanonicalDeclaration(sym),
relativePath
relativePath,
)}`;
} else if (
reffedType &&
@@ -1631,20 +1631,20 @@ class JsonSchemaGenerator {
this.parseCommentsIntoDefinition(
typ.aliasSymbol!,
definition,
otherAnnotations
otherAnnotations,
);
if (prop) {
this.parseCommentsIntoDefinition(
prop,
returnedDefinition,
otherAnnotations
otherAnnotations,
);
}
if (pairedSymbol && symbol && this.isFromDefaultLib(symbol)) {
this.parseCommentsIntoDefinition(
pairedSymbol,
definition,
otherAnnotations
otherAnnotations,
);
}
@@ -1669,7 +1669,7 @@ class JsonSchemaGenerator {
true,
undefined,
symbol,
symbol
symbol,
);
} else {
reffedDefinition = definition;
@@ -1693,7 +1693,7 @@ class JsonSchemaGenerator {
this.getUnionDefinition(
typ as ts.UnionType,
unionModifier,
definition
definition,
);
} else if (typ.flags & ts.TypeFlags.Intersection) {
if (this.args.noExtraProps) {
@@ -1711,7 +1711,7 @@ class JsonSchemaGenerator {
undefined,
undefined,
undefined,
true
true,
);
definition.type = other.type; // should always be object
definition.properties = {
@@ -1722,19 +1722,19 @@ class JsonSchemaGenerator {
if (Object.keys(other.default || {}).length > 0) {
definition.default = extend(
definition.default || {},
other.default
other.default,
);
}
if (other.required) {
definition.required = unique(
(definition.required || []).concat(other.required)
(definition.required || []).concat(other.required),
).sort();
}
}
} else {
this.getIntersectionDefinition(
typ as ts.IntersectionType,
definition
definition,
);
}
} else if (isRawType) {
@@ -1746,7 +1746,7 @@ class JsonSchemaGenerator {
reffedType!,
definition,
undefined,
ignoreUndefined
ignoreUndefined,
);
} else if (
node &&
@@ -1806,7 +1806,7 @@ class JsonSchemaGenerator {
public getSchemaForSymbol(
symbolName: string,
includeReffedDefinitions: boolean = true,
includeAllOverrides: boolean = false
includeAllOverrides: boolean = false,
): Definition {
const overrideDefinition = this.schemaOverrides.get(symbolName);
if (!this.allSymbols[symbolName] && !overrideDefinition) {
@@ -1827,7 +1827,7 @@ class JsonSchemaGenerator {
undefined,
undefined,
undefined,
this.userSymbols[symbolName] || undefined
this.userSymbols[symbolName] || undefined,
);
}
@@ -1849,7 +1849,7 @@ class JsonSchemaGenerator {
public getSchemaForSymbols(
symbolNames: string[],
includeReffedDefinitions: boolean = true,
includeAllOverrides: boolean = false
includeAllOverrides: boolean = false,
): Definition {
const root: {
$id?: string;
@@ -1875,7 +1875,7 @@ class JsonSchemaGenerator {
undefined,
undefined,
undefined,
this.userSymbols[symbolName]
this.userSymbols[symbolName],
);
}
if (
@@ -1902,7 +1902,7 @@ class JsonSchemaGenerator {
public getMainFileSymbols(
program: ts.Program,
onlyIncludeFiles?: string[]
onlyIncludeFiles?: string[],
): string[] {
function includeFile(file: ts.SourceFile): boolean {
if (onlyIncludeFiles === undefined) {
@@ -1940,7 +1940,7 @@ function generateHashOfNode(node: ts.Node, relativePath: string): string {
export function buildGenerator(
program: ts.Program,
args: PartialArgs = {}
args: PartialArgs = {},
): JsonSchemaGenerator | null {
// Use defaults unless otherwise specified
const settings = getDefaultArgs();
@@ -1990,7 +1990,7 @@ export function buildGenerator(
var baseName = tc.typeToString(
baseType,
undefined,
ts.TypeFormatFlags.UseFullyQualifiedType
ts.TypeFormatFlags.UseFullyQualifiedType,
);
if (!inheritingTypes[baseName]) {
inheritingTypes[baseName] = [];
@@ -2011,14 +2011,14 @@ export function buildGenerator(
userSymbols,
inheritingTypes,
typeChecker,
settings
settings,
);
}
export async function extractGraphSchema(
id: string,
userPath: string,
exportName: string
exportName: string,
) {
const filePath = path.resolve(process.cwd(), userPath);
const parentPath = path.dirname(filePath);
@@ -2104,7 +2104,7 @@ export async function extractGraphSchema(
export type __input = Inspect<FilterAny<__builder["input"]>>;
export type __output = Inspect<FilterAny<__builder["output"]>>;
export type __config = Inspect<FilterAny<__builder["config"]>>;
`
`,
);
const program = ts.createProgram([typePath], {
noEmit: true,
@@ -2120,7 +2120,7 @@ export async function extractGraphSchema(
} catch (e) {
console.error(
`Failed to obtain symbol "${symbol}":`,
(e as Error)?.message
(e as Error)?.message,
);
}
return undefined;
+5 -5
View File
@@ -50,7 +50,7 @@ export const logger = createLogger({
return JSON.stringify({ timestamp, level, event, ...rest });
}),
])
]),
),
transports: [new transports.Console()],
});
@@ -61,7 +61,7 @@ const formatStack = (stack: string | undefined | null) => {
const [firstFile] = stacktraceParser(stack).filter(
(item) =>
!item.file?.split(path.sep).includes("node_modules") &&
!item.file?.startsWith("node:")
!item.file?.startsWith("node:"),
);
if (firstFile?.file && firstFile?.lineNumber) {
@@ -74,7 +74,7 @@ const formatStack = (stack: string | undefined | null) => {
const spliceIndex = messageLines.findIndex((i) => i.includes(filePath));
const padding = " ".repeat(
Math.max(0, messageLines[spliceIndex].indexOf("at"))
Math.max(0, messageLines[spliceIndex].indexOf("at")),
);
const highlightCode = process.stdout.isTTY;
@@ -82,7 +82,7 @@ const formatStack = (stack: string | undefined | null) => {
let codeFrame = codeFrameColumns(
readFileSync(filePath, "utf-8"),
{ start: { line, column } },
{ highlightCode }
{ highlightCode },
);
codeFrame = codeFrame
@@ -113,7 +113,7 @@ export const logError = (
options?: {
context?: Record<string, unknown>;
prefix?: string;
}
},
) => {
let message;
let context = options?.context;
+2 -2
View File
@@ -171,7 +171,7 @@ export const RunCreate = z
z.union([
z.string(),
z.object({ node: z.string(), input: z.unknown().optional() }),
])
]),
),
])
.optional(),
@@ -210,7 +210,7 @@ export const RunCreate = z
"updates",
"events",
"debug",
])
]),
),
z.enum([
"values",
+4 -4
View File
@@ -52,7 +52,7 @@ app.post(
assistants: z.boolean().optional(),
checkpointer: z.boolean().optional(),
store: z.boolean().optional(),
})
}),
),
(c) => {
const { runs, threads, assistants, checkpointer, store } =
@@ -60,7 +60,7 @@ app.post(
truncate({ runs, threads, assistants, checkpointer, store });
return c.json({ ok: true });
}
},
);
export const StartServerSchema = z.object({
@@ -96,8 +96,8 @@ export async function startServer(options: z.infer<typeof StartServerSchema>) {
{ fetch: app.fetch, port: options.port, hostname: options.host },
(c) => {
resolve({ host: `${c.address}:${c.port}`, cleanup });
}
},
);
}
},
);
}
+2 -2
View File
@@ -7,13 +7,13 @@ import { runnableConfigToCheckpoint } from "./utils/runnableConfig.mjs";
import { serializeError } from "./utils/serde.mjs";
const isStateSnapshot = (
state: StateSnapshot | LangGraphRunnableConfig
state: StateSnapshot | LangGraphRunnableConfig,
): state is StateSnapshot => {
return "values" in state && "next" in state;
};
export const stateSnapshotToThreadState = (
state: StateSnapshot
state: StateSnapshot,
): ThreadState => {
return {
values: state.values,
@@ -77,18 +77,18 @@ class InMemorySaver extends MemorySaver {
async put(
config: RunnableConfig,
checkpoint: Checkpoint,
metadata: CheckpointMetadata
metadata: CheckpointMetadata,
): Promise<RunnableConfig> {
return await conn.with(() =>
super.put(config, checkpoint, {
...Object.fromEntries(
Object.entries(config.configurable ?? {}).filter(
([key]) => !key.startsWith("__") && !EXCLUDED_KEYS.includes(key)
)
([key]) => !key.startsWith("__") && !EXCLUDED_KEYS.includes(key),
),
),
...config.metadata,
...metadata,
})
}),
);
}
+38 -38
View File
@@ -135,7 +135,7 @@ export const conn = new FileSystemPersistence<Store>(
assistants: {},
assistant_versions: [],
retry_counter: {},
})
}),
);
class TimeoutError extends Error {}
@@ -255,8 +255,8 @@ export const truncate = (flags: {
Object.entries(STORE.assistants).filter(
([key, assistant]) =>
assistant.metadata?.created_by === "system" &&
uuid5(assistant.graph_id, NAMESPACE_GRAPH) === key
)
uuid5(assistant.graph_id, NAMESPACE_GRAPH) === key,
),
);
}
@@ -271,7 +271,7 @@ const isObject = (value: unknown): value is Record<string, unknown> => {
const isJsonbContained = (
superset: Record<string, unknown> | undefined,
subset: Record<string, unknown> | undefined
subset: Record<string, unknown> | undefined,
): boolean => {
if (superset == null || subset == null) return true;
for (const [key, value] of Object.entries(subset)) {
@@ -321,7 +321,7 @@ export class Assistants {
for (const assistant of filtered.slice(
options.offset,
options.offset + options.limit
options.offset + options.limit,
)) {
yield { ...assistant, name: assistant.name ?? assistant.graph_id };
}
@@ -345,7 +345,7 @@ export class Assistants {
metadata?: Metadata;
if_exists: OnConflictBehavior;
name?: string;
}
},
): Promise<Assistant> {
return conn.with((STORE) => {
if (STORE.assistants[assistantId] != null) {
@@ -389,7 +389,7 @@ export class Assistants {
graph_id?: string;
metadata?: Metadata;
name?: string;
}
},
): Promise<Assistant> {
return conn.with((STORE) => {
const assistant = STORE.assistants[assistantId];
@@ -428,7 +428,7 @@ export class Assistants {
Math.max(
...STORE.assistant_versions
.filter((v) => v["assistant_id"] === assistantId)
.map((v) => v["version"])
.map((v) => v["version"]),
) + 1;
assistant.version = newVersion;
@@ -458,7 +458,7 @@ export class Assistants {
// Cascade delete for assistant versions and crons
STORE.assistant_versions = STORE.assistant_versions.filter(
(v) => v["assistant_id"] !== assistantId
(v) => v["assistant_id"] !== assistantId,
);
for (const run of Object.values(STORE.runs)) {
@@ -473,7 +473,7 @@ export class Assistants {
static async setLatest(
assistantId: string,
version: number
version: number,
): Promise<Assistant> {
return conn.with((STORE) => {
const assistant = STORE.assistants[assistantId];
@@ -481,7 +481,7 @@ export class Assistants {
throw new HTTPException(404, { message: "Assistant not found" });
const assistantVersion = STORE.assistant_versions.find(
(v) => v["assistant_id"] === assistantId && v["version"] === version
(v) => v["assistant_id"] === assistantId && v["version"] === version,
);
if (!assistantVersion)
@@ -509,7 +509,7 @@ export class Assistants {
limit: number;
offset: number;
metadata?: Metadata;
}
},
) {
return conn.with((STORE) => {
const versions = STORE.assistant_versions
@@ -620,7 +620,7 @@ export class Threads {
for (const thread of filtered.slice(
options.offset,
options.offset + options.limit
options.offset + options.limit,
)) {
yield thread;
}
@@ -644,7 +644,7 @@ export class Threads {
options?: {
metadata?: Metadata;
if_exists: OnConflictBehavior;
}
},
): Promise<Thread> {
return conn.with((STORE) => {
const now = new Date();
@@ -674,7 +674,7 @@ export class Threads {
threadId: string,
options?: {
metadata?: Metadata;
}
},
): Promise<Thread> {
return conn.with((STORE) => {
const thread = STORE.threads[threadId];
@@ -699,7 +699,7 @@ export class Threads {
options: {
checkpoint?: CheckpointPayload;
exception?: Error;
}
},
) {
return conn.with((STORE) => {
const thread = STORE.threads[threadId];
@@ -712,7 +712,7 @@ export class Threads {
}
const hasPendingRuns = Object.values(STORE.runs).some(
(run) => run["thread_id"] === threadId && run["status"] === "pending"
(run) => run["thread_id"] === threadId && run["status"] === "pending",
);
let status: ThreadStatus = "idle";
@@ -737,7 +737,7 @@ export class Threads {
if (task.interrupts) acc[task.id] = task.interrupts;
return acc;
},
{}
{},
)
: undefined;
});
@@ -790,7 +790,7 @@ export class Threads {
config: RunnableConfig,
options: {
subgraphs?: boolean;
}
},
): Promise<LangGraphStateSnapshot> {
const subgraphs = options.subgraphs ?? false;
const threadId = config.configurable?.thread_id;
@@ -831,7 +831,7 @@ export class Threads {
| Record<string, unknown>
| null
| undefined,
asNode?: string | undefined
asNode?: string | undefined,
) {
const threadId = config.configurable?.thread_id;
const thread = threadId ? await Threads.get(threadId) : undefined;
@@ -880,7 +880,7 @@ export class Threads {
limit?: number;
before?: string | RunnableConfig;
metadata?: Metadata;
}
},
) {
const threadId = config.configurable?.thread_id;
if (!threadId) return [];
@@ -932,7 +932,7 @@ export class Runs {
if (!thread) {
await console.warn(
`Unexpected missing thread in Runs.next: ${threadId}`
`Unexpected missing thread in Runs.next: ${threadId}`,
);
continue;
}
@@ -965,7 +965,7 @@ export class Runs {
multitaskStrategy?: MultitaskStrategy;
ifNotExists?: IfNotExists;
afterSeconds?: number;
}
},
): Promise<Run[]> {
return conn.with(async (STORE) => {
const assistant = STORE.assistants[assistantId];
@@ -985,7 +985,7 @@ export class Runs {
const config: RunnableConfig = kwargs.config ?? {};
const existingThread = Object.values(STORE.threads).find(
(thread) => thread.thread_id === threadId
(thread) => thread.thread_id === threadId,
);
const now = new Date();
@@ -1000,7 +1000,7 @@ export class Runs {
configurable: Object.assign(
{},
assistant.config?.configurable,
config?.configurable
config?.configurable,
),
}),
created_at: now,
@@ -1025,9 +1025,9 @@ export class Runs {
{},
assistant.config?.configurable,
existingThread?.config?.configurable,
config?.configurable
config?.configurable,
),
}
},
);
existingThread.updated_at = now;
@@ -1039,7 +1039,7 @@ export class Runs {
// if multitask_mode = reject, check for inflight runs
// and if there are any, return them to reject putting a new run
const inflightRuns = Object.values(STORE.runs).filter(
(run) => run.thread_id === threadId && run.status === "pending"
(run) => run.thread_id === threadId && run.status === "pending",
);
if (options?.preventInsertInInflight) {
@@ -1062,14 +1062,14 @@ export class Runs {
existingThread?.config?.configurable?.user_id ??
assistant.config?.configurable?.user_id ??
options?.userId,
}
},
);
const mergedMetadata = Object.assign(
{},
assistant.metadata,
existingThread?.metadata,
metadata
metadata,
);
const newRun: Run = {
@@ -1084,7 +1084,7 @@ export class Runs {
assistant.config,
config,
{ configurable },
{ metadata: mergedMetadata }
{ metadata: mergedMetadata },
),
}),
multitask_strategy: multitaskStrategy,
@@ -1099,7 +1099,7 @@ export class Runs {
static async get(
runId: string,
threadId: string | undefined
threadId: string | undefined,
): Promise<Run | null> {
return conn.with(async (STORE) => {
const run = STORE.runs[runId];
@@ -1115,7 +1115,7 @@ export class Runs {
static async delete(
runId: string,
threadId: string | undefined
threadId: string | undefined,
): Promise<string | null> {
return conn.with(async (STORE) => {
const run = STORE.runs[runId];
@@ -1167,7 +1167,7 @@ export class Runs {
runIds: string[],
options: {
action?: "interrupt" | "rollback";
}
},
) {
return conn.with(async (STORE) => {
const action = options.action ?? "interrupt";
@@ -1194,7 +1194,7 @@ export class Runs {
{
run_id: runId,
thread_id: threadId,
}
},
);
promises.push(Runs.delete(runId, threadId));
@@ -1228,7 +1228,7 @@ export class Runs {
offset?: number | null;
status?: string | null;
metadata?: Metadata | null;
}
},
) {
return conn.with(async (STORE) => {
const runs = Object.values(STORE.runs).filter((run) => {
@@ -1263,7 +1263,7 @@ export class Runs {
options?: {
ignore404?: boolean;
cancelOnDisconnect?: AbortSignal;
}
},
): AsyncGenerator<{ event: string; data: unknown }> {
// TODO: what if we're joining an already completed run? Should we check before?
const signal = options?.cancelOnDisconnect;
@@ -1276,7 +1276,7 @@ export class Runs {
if (message.data === "done") break;
} else {
const streamTopic = message.topic.substring(
`run:${runId}:stream:`.length
`run:${runId}:stream:`.length,
);
yield { event: streamTopic, data: message.data };
+2 -2
View File
@@ -11,7 +11,7 @@ superjson.registerCustom<Uint8Array, string>(
serialize: (v) => Buffer.from(v).toString("base64"),
deserialize: (v) => new Uint8Array(Buffer.from(v, "base64")),
},
"Uint8Array"
"Uint8Array",
);
export function serialize(data: unknown) {
@@ -81,7 +81,7 @@ export class FileSystemPersistence<Schema> {
}
async *withGenerator<T extends AsyncGenerator<any>>(
fn: ((data: Schema) => T) | T
fn: ((data: Schema) => T) | T,
) {
if (this.filepath == null || this.data == null) {
throw new Error(`${this.name} not initialized`);
+1 -1
View File
@@ -30,7 +30,7 @@ class InMemoryStore extends BaseMemoryStore {
}
async batch<Op extends readonly Operation[]>(
operations: Op
operations: Op,
): Promise<OperationResults<Op>> {
return await conn.with(() => super.batch(operations));
}
+10 -10
View File
@@ -112,7 +112,7 @@ function preprocessDebugCheckpointTask(task: DebugTask): StreamTaskResult {
}
const isConfigurablePresent = (
config: unknown
config: unknown,
): config is {
[key: string]: unknown;
callbacks?: unknown;
@@ -130,8 +130,8 @@ const deleteInternalConfigurableFields = (config: unknown) => {
...config,
configurable: Object.fromEntries(
Object.entries(config.configurable).filter(
([key]) => !key.startsWith("__")
)
([key]) => !key.startsWith("__"),
),
),
};
@@ -168,7 +168,7 @@ export async function* streamState(
onCheckpoint?: (checkpoint: StreamCheckpoint) => void;
onTaskResult?: (taskResult: StreamTaskResult) => void;
signal?: AbortSignal;
}
},
): AsyncGenerator<{ event: string; data: unknown }> {
const kwargs = run.kwargs;
const graphId = kwargs.config?.configurable?.graph_id;
@@ -185,8 +185,8 @@ export async function* streamState(
const libStreamMode: Set<LangGraphStreamMode> = new Set(
userStreamMode.filter(
(mode) => mode !== "events" && mode !== "messages-tuple"
) ?? []
(mode) => mode !== "events" && mode !== "messages-tuple",
) ?? [],
);
if (userStreamMode.includes("messages-tuple")) {
@@ -232,7 +232,7 @@ export async function* streamState(
runId: run.run_id,
streamMode: [...libStreamMode],
signal: options?.signal,
}
},
);
const messages: Record<string, BaseMessageChunk> = {};
@@ -344,11 +344,11 @@ export async function* streamState(
kwargs.feedback_keys.map(async (feedback) => {
const { url } = await client.createPresignedFeedbackToken(
run.run_id,
feedback
feedback,
);
return [feedback, url];
})
)
}),
),
);
yield { event: "feedback", data };
+1 -1
View File
@@ -7,7 +7,7 @@ export const combineAbortSignals = (
const abortController = new AbortController();
signals.forEach((signal) =>
signal.addEventListener("abort", () => abortController.abort())
signal.addEventListener("abort", () => abortController.abort()),
);
return abortController.signal;
};
@@ -11,7 +11,7 @@ const ConfigSchema = z.object({
});
export const runnableConfigToCheckpoint = (
config: RunnableConfig | null | undefined
config: RunnableConfig | null | undefined,
): Checkpoint | null => {
if (!config || !config.configurable || !config.configurable.thread_id) {
return null;
@@ -38,7 +38,7 @@ const TaskConfigSchema = z.object({
});
export const taskRunnableConfigToCheckpoint = (
config: RunnableConfig | null | undefined
config: RunnableConfig | null | undefined,
): Partial<Checkpoint> | null => {
if (!config || !config.configurable || !config.configurable.thread_id) {
return null;
+1 -3
View File
@@ -16,7 +16,7 @@ export const serialiseAsDict = (obj: unknown) => {
return value;
},
2
2,
);
};
@@ -26,5 +26,3 @@ export const serializeError = (error: unknown) => {
}
return { error: "Error", message: JSON.stringify(error) };
};
+99 -99
View File
@@ -41,7 +41,7 @@ const truncate = async (
store?: boolean;
checkpoint?: boolean;
}
| "all"
| "all",
) => {
const flags =
options === "all"
@@ -81,7 +81,7 @@ describe("assistants", () => {
await client.assistants.delete(res.assistant_id);
await expect(() => client.assistants.get(res.assistant_id)).rejects.toThrow(
"HTTP 404: Assistant not found"
"HTTP 404: Assistant not found",
);
});
@@ -159,7 +159,7 @@ describe("assistants", () => {
await client.assistants.delete(res.assistant_id);
await expect(() => client.assistants.get(res.assistant_id)).rejects.toThrow(
"HTTP 404: Assistant not found"
"HTTP 404: Assistant not found",
);
});
@@ -184,7 +184,7 @@ describe("assistants", () => {
});
expect(search.length).toBeGreaterThanOrEqual(1);
expect(search.every((i) => i.assistant_id !== create.assistant_id)).toBe(
true
true,
);
});
@@ -193,7 +193,7 @@ describe("assistants", () => {
// (1) initial version
expect(
await client.assistants.getVersions(assistant.assistant_id)
await client.assistants.getVersions(assistant.assistant_id),
).toMatchObject([{ version: 1 }]);
// (2) update and create a new version
@@ -201,7 +201,7 @@ describe("assistants", () => {
config: { configurable: { foo: "bar" } },
});
expect(
await client.assistants.getVersions(assistant.assistant_id)
await client.assistants.getVersions(assistant.assistant_id),
).toMatchObject([
{ version: 2, config: { configurable: { foo: "bar" } } },
{ version: 1 },
@@ -211,14 +211,14 @@ describe("assistants", () => {
expect(
await client.assistants.getVersions(assistant.assistant_id, {
limit: 1,
})
}),
).toMatchObject([{ version: 2 }]);
// descending order
expect(
await client.assistants.getVersions(assistant.assistant_id, {
offset: 1,
})
}),
).toMatchObject([{ version: 1 }]);
// (3) create a version with metadata
@@ -229,14 +229,14 @@ describe("assistants", () => {
expect(
await client.assistants.getVersions(assistant.assistant_id, {
metadata: { foo: "baz" },
})
}),
).toMatchObject([{ version: 3 }]);
// (4) noop update
await client.assistants.update(assistant.assistant_id, {});
expect(
await client.assistants.getVersions(assistant.assistant_id)
await client.assistants.getVersions(assistant.assistant_id),
).toMatchObject([
{ version: 4 },
{ version: 3 },
@@ -246,7 +246,7 @@ describe("assistants", () => {
await client.assistants.delete(assistant.assistant_id);
expect(
await client.assistants.getVersions(assistant.assistant_id)
await client.assistants.getVersions(assistant.assistant_id),
).toMatchObject([]);
});
@@ -260,12 +260,12 @@ describe("assistants", () => {
const updatedAgain = await client.assistants.update(
created.assistant_id,
{}
{},
);
expect(updatedAgain.version).toBe(3);
await expect(
client.assistants.setLatest(created.assistant_id, 4)
client.assistants.setLatest(created.assistant_id, 4),
).rejects.toThrow();
});
@@ -388,7 +388,7 @@ describe("threads copy", () => {
const copiedThread = await client.threads.copy(thread.thread_id);
const copiedThreadState = await client.threads.getState(
copiedThread.thread_id
copiedThread.thread_id,
);
// check copied thread state matches expected output
@@ -414,7 +414,7 @@ describe("threads copy", () => {
// For in-memory connections, check the thread history
const originalHistory = await client.threads.getHistory(thread.thread_id);
const copiedHistory = await client.threads.getHistory(
copiedThread.thread_id
copiedThread.thread_id,
);
expect(originalHistory.length).toBe(copiedHistory.length);
@@ -443,7 +443,7 @@ describe("threads copy", () => {
} else {
const sql = postgres(
process.env.POSTGRES_URI ??
"postgres://postgres:postgres@127.0.0.1:5433/postgres?sslmode=disable"
"postgres://postgres:postgres@127.0.0.1:5433/postgres?sslmode=disable",
);
// check checkpoints in DB
@@ -504,11 +504,11 @@ describe("threads copy", () => {
// test that copied thread has original as well as new values
const copiedThreadState = await client.threads.getState<AgentState>(
copiedThread.thread_id
copiedThread.thread_id,
);
const copiedThreadStateMessages = copiedThreadState.values.messages.map(
(m) => m.content
(m) => m.content,
);
expect(copiedThreadStateMessages).toEqual([
// original messages
@@ -525,7 +525,7 @@ describe("threads copy", () => {
// test that the new run on the copied thread doesn't affect the original one
const currentOriginalThreadState = await client.threads.getState(
thread.thread_id
thread.thread_id,
);
expect(currentOriginalThreadState).toEqual(originalThreadState);
});
@@ -544,7 +544,7 @@ describe("threads copy", () => {
});
const history = await client.threads.getHistory<AgentState>(
thread.thread_id
thread.thread_id,
);
expect(history.length).toBe(5);
expect(history[0].values.messages.length).toBe(4);
@@ -560,11 +560,11 @@ describe("threads copy", () => {
});
const fullHistory = await client.threads.getHistory<AgentState>(
thread.thread_id
thread.thread_id,
);
const filteredHistory = await client.threads.getHistory<AgentState>(
thread.thread_id,
{ metadata: runMetadata }
{ metadata: runMetadata },
);
expect(fullHistory.length).toBe(10);
@@ -595,13 +595,13 @@ describe("threads copy", () => {
});
const copiedThreadState = await client.threads.getState<AgentState>(
copyThread.thread_id
copyThread.thread_id,
);
expect(copiedThreadState.values.messages[0].content).toBe("bar");
// test that updating the copied thread doesn't affect the original one
const currentOriginalThreadState = await client.threads.getState(
thread.thread_id
thread.thread_id,
);
expect(currentOriginalThreadState).toEqual(originalState);
});
@@ -625,7 +625,7 @@ describe("runs", () => {
input: { messages: [{ type: "human", content: "bar" }] },
config: globalConfig,
afterSeconds: 10,
}
},
);
let runs = await client.runs.list(thread.thread_id);
@@ -649,7 +649,7 @@ describe("runs", () => {
const stream = client.runs.stream(
thread.thread_id,
assistant.assistant_id,
{ input, streamMode: "values", config: globalConfig }
{ input, streamMode: "values", config: globalConfig },
);
let runId: string | null = null;
@@ -666,7 +666,7 @@ describe("runs", () => {
if (chunk.event === "values") {
const messageIds = chunk.data.messages.map(
(message: { id: string }) => message.id
(message: { id: string }) => message.id,
);
expect(messageIds.slice(0, -1)).toEqual(previousMessageIds);
previousMessageIds = messageIds;
@@ -686,7 +686,7 @@ describe("runs", () => {
} else {
const sql = postgres(
process.env.POSTGRES_URI ??
"postgres://postgres:postgres@127.0.0.1:5433/postgres?sslmode=disable"
"postgres://postgres:postgres@127.0.0.1:5433/postgres?sslmode=disable",
);
let cur = await sql`SELECT * FROM checkpoints WHERE run_id is null`;
@@ -709,7 +709,7 @@ describe("runs", () => {
client.runs.wait(thread.thread_id, assistant.assistant_id, {
input,
config: { ...globalConfig, recursion_limit: 1 },
})
}),
).rejects.toThrowError(/GraphRecursionError/);
const threadUpdated = await client.threads.get(thread.thread_id);
expect(threadUpdated.status).toBe("error");
@@ -724,7 +724,7 @@ describe("runs", () => {
const values = await client.runs.wait(
thread.thread_id,
assistant.assistant_id,
{ input, config: globalConfig }
{ input, config: globalConfig },
);
expect(Array.isArray((values as any).messages)).toBe(true);
@@ -741,7 +741,7 @@ describe("runs", () => {
const stream = client.runs.stream(
thread.thread_id,
assistant.assistant_id,
{ input, streamMode: "updates", config: globalConfig }
{ input, streamMode: "updates", config: globalConfig },
);
let runId: string | null = null;
@@ -781,18 +781,18 @@ describe("runs", () => {
const stream = client.runs.stream(
thread.thread_id,
assistant.assistant_id,
{ input, streamMode: "events", config: globalConfig }
{ input, streamMode: "events", config: globalConfig },
);
const events = await gatherIterator(stream);
expect(new Set(events.map((i) => i.event))).toEqual(
new Set(["metadata", "events"])
new Set(["metadata", "events"]),
);
expect(
new Set(
events.filter((i) => i.event === "events").map((i) => i.data.event)
)
events.filter((i) => i.event === "events").map((i) => i.data.event),
),
).toEqual(
new Set([
"on_chain_start",
@@ -800,7 +800,7 @@ describe("runs", () => {
"on_chat_model_end",
"on_chat_model_start",
"on_chat_model_stream",
])
]),
);
});
@@ -813,7 +813,7 @@ describe("runs", () => {
const stream = client.runs.stream(
thread.thread_id,
assistant.assistant_id,
{ input, streamMode: "messages", config: globalConfig }
{ input, streamMode: "messages", config: globalConfig },
);
let runId: string | null = null;
@@ -854,7 +854,7 @@ describe("runs", () => {
"messages/metadata",
"messages/partial",
"messages/complete",
])
]),
);
expect(runId).not.toBeNull();
@@ -871,7 +871,7 @@ describe("runs", () => {
const stream = await client.runs.stream(
thread.thread_id,
assistant.assistant_id,
{ input, streamMode: "messages-tuple", config: globalConfig }
{ input, streamMode: "messages-tuple", config: globalConfig },
);
const chunks = await gatherIterator(stream);
@@ -905,7 +905,7 @@ describe("runs", () => {
const stream = await client.runs.stream(
thread.thread_id,
assistant.assistant_id,
{ input, streamMode: ["messages", "values"], config: globalConfig }
{ input, streamMode: ["messages", "values"], config: globalConfig },
);
const chunks = await gatherIterator(stream);
@@ -914,7 +914,7 @@ describe("runs", () => {
const messages: BaseMessage[] = findLast(
chunks,
(i) => i.event === "values"
(i) => i.event === "values",
)?.data.messages;
expect(messages.length).toBe(4);
@@ -931,7 +931,7 @@ describe("runs", () => {
"messages/partial",
"messages/complete",
"values",
])
]),
);
const run = await client.runs.get(thread.thread_id, runId);
@@ -956,7 +956,7 @@ describe("runs", () => {
input,
interruptBefore: ["tool"],
config: globalConfig,
})
}),
);
expect(chunks.filter((i) => i.event === "error").length).toBe(0);
@@ -976,7 +976,7 @@ describe("runs", () => {
client.runs.stream(thread.thread_id, assistant.assistant_id, {
input: null,
config: globalConfig,
})
}),
);
expect(chunks.filter((i) => i.event === "error").length).toBe(0);
@@ -988,7 +988,7 @@ describe("runs", () => {
const threadAfterContinue = await client.threads.get(thread.thread_id);
expect(threadAfterContinue.status).toBe("idle");
}
},
);
it.concurrent("human in the loop - modification", async () => {
@@ -1006,7 +1006,7 @@ describe("runs", () => {
input,
interruptBefore: ["tool"],
config: globalConfig,
})
}),
);
expect(chunks.filter((i) => i.event === "error").length).toBe(0);
@@ -1014,7 +1014,7 @@ describe("runs", () => {
// edit the last message
const lastMessage = findLast(
chunks,
(i) => i.event === "values"
(i) => i.event === "values",
)?.data.messages.at(-1);
lastMessage.content = "modified";
@@ -1031,7 +1031,7 @@ describe("runs", () => {
expect(modifiedThread.metadata?.modified).toBe(true);
const stateAfterModify = await client.threads.getState<AgentState>(
thread.thread_id
thread.thread_id,
);
expect(stateAfterModify.values.messages.at(-1)?.content).toBe("modified");
expect(stateAfterModify.next).toEqual(["tool"]);
@@ -1044,7 +1044,7 @@ describe("runs", () => {
client.runs.stream(thread.thread_id, assistant.assistant_id, {
input: null,
config: globalConfig,
})
}),
);
const threadAfterContinue = await client.threads.get(thread.thread_id);
@@ -1059,7 +1059,7 @@ describe("runs", () => {
// get the history
const history = await client.threads.getHistory<AgentState>(
thread.thread_id
thread.thread_id,
);
expect(history.length).toBe(6);
expect(history[0].next.length).toBe(0);
@@ -1091,13 +1091,13 @@ describe("runs", () => {
};
await expect(
client.runs.wait(thread.thread_id, "non-existent", { input })
client.runs.wait(thread.thread_id, "non-existent", { input }),
).rejects.toThrow(/No assistant found for/);
await expect(
gatherIterator(
client.runs.stream(thread.thread_id, "non-existent", { input })
)
client.runs.stream(thread.thread_id, "non-existent", { input }),
),
).rejects.toThrow(/No assistant found for/);
});
});
@@ -1120,7 +1120,7 @@ describe("shared state", () => {
const res1 = (await client.runs.wait(
thread.thread_id,
assistant.assistant_id,
{ input, config }
{ input, config },
)) as Awaited<Record<string, any>>;
expect(res1.sharedStateValue).toBe(null);
@@ -1128,7 +1128,7 @@ describe("shared state", () => {
const res2 = (await client.runs.wait(
thread.thread_id,
assistant.assistant_id,
{ input, config }
{ input, config },
)) as Awaited<Record<string, any>>;
expect(res2.sharedStateValue).toBe(config.configurable.user_id);
});
@@ -1146,7 +1146,7 @@ describe("shared state", () => {
const res1 = (await client.runs.wait(
thread.thread_id,
assistant.assistant_id,
{ input, config: config1 }
{ input, config: config1 },
)) as Awaited<Record<string, any>>;
// Run with the same thread id but a new config
@@ -1154,7 +1154,7 @@ describe("shared state", () => {
const res2 = (await client.runs.wait(
thread.thread_id,
assistant.assistant_id,
{ input, config: config2 }
{ input, config: config2 },
)) as Awaited<Record<string, any>>;
expect(res1.sharedStateValue).toBe(config1.configurable.user_id);
@@ -1180,12 +1180,12 @@ describe("shared state", () => {
const res1 = (await client.runs.wait(
thread.thread_id,
assistant.assistant_id,
{ input, config }
{ input, config },
)) as Awaited<Record<string, any>>;
expect(res1.sharedStateFromStoreConfig).toBeDefined();
expect(res1.sharedStateFromStoreConfig.id).toBeDefined();
expect(res1.sharedStateFromStoreConfig.id).toBe(
config.configurable.user_id
config.configurable.user_id,
);
});
@@ -1210,12 +1210,12 @@ describe("shared state", () => {
const res1 = (await client.runs.wait(
thread.thread_id,
assistant.assistant_id,
{ input, config }
{ input, config },
)) as Awaited<Record<string, any>>;
expect(res1.sharedStateFromStoreConfig).toBeDefined();
expect(res1.sharedStateFromStoreConfig.id).toBeDefined();
expect(res1.sharedStateFromStoreConfig.id).toBe(
config.configurable.user_id
config.configurable.user_id,
);
// Fetch data from store client
@@ -1305,10 +1305,10 @@ describe("StoreClient", () => {
expect(searchResAfterPut.items[0].createdAt).toBeDefined();
expect(searchResAfterPut.items[0].updatedAt).toBeDefined();
expect(
new Date(searchResAfterPut.items[0].createdAt).getTime()
new Date(searchResAfterPut.items[0].createdAt).getTime(),
).toBeLessThanOrEqual(Date.now());
expect(
new Date(searchResAfterPut.items[0].updatedAt).getTime()
new Date(searchResAfterPut.items[0].updatedAt).getTime(),
).toBeLessThanOrEqual(Date.now());
const updatedValue = { foo: "baz" };
@@ -1325,7 +1325,7 @@ describe("StoreClient", () => {
expect(searchResAfterUpdate.items[0].value).toEqual(updatedValue);
expect(
new Date(searchResAfterUpdate.items[0].updatedAt).getTime()
new Date(searchResAfterUpdate.items[0].updatedAt).getTime(),
).toBeGreaterThan(new Date(searchResAfterPut.items[0].updatedAt).getTime());
const listResAfterPut = await client.store.listNamespaces();
@@ -1349,12 +1349,12 @@ describe("subgraphs", () => {
const assistant = await client.assistants.create({ graphId: "nested" });
expect(
Object.keys(await client.assistants.getSubgraphs(assistant.assistant_id))
Object.keys(await client.assistants.getSubgraphs(assistant.assistant_id)),
).toEqual(["gp_two"]);
const subgraphs = await client.assistants.getSubgraphs(
assistant.assistant_id,
{ recurse: true }
{ recurse: true },
);
expect(Object.keys(subgraphs)).toEqual(["gp_two", "gp_two|p_two"]);
@@ -1399,7 +1399,7 @@ describe("subgraphs", () => {
messages: [{ role: "human", content: "SF", id: "initial-message" }],
},
interruptBefore: ["tool"],
})
}),
);
for (const chunk of chunks) {
@@ -1468,7 +1468,7 @@ describe("subgraphs", () => {
const stateRecursive = await client.threads.getState(
thread.thread_id,
undefined,
{ subgraphs: true }
{ subgraphs: true },
);
expect(stateRecursive.next).toEqual(["weather_graph"]);
@@ -1527,7 +1527,7 @@ describe("subgraphs", () => {
input: null,
streamMode: ["values", "updates"],
streamSubgraphs: true,
})
}),
);
expect(chunks.filter((i) => i.event === "error")).toEqual([]);
@@ -1681,7 +1681,7 @@ describe("subgraphs", () => {
// run until the interrupt (same as before)
let chunks = await gatherIterator(
client.runs.stream(thread.thread_id, assistant.assistant_id, { input })
client.runs.stream(thread.thread_id, assistant.assistant_id, { input }),
);
expect(chunks.filter((i) => i.event === "error")).toEqual([]);
@@ -1713,7 +1713,7 @@ describe("subgraphs", () => {
// get inner state after update
const innerState = await client.threads.getState<{ city: string }>(
thread.thread_id,
state.tasks[0].checkpoint ?? undefined
state.tasks[0].checkpoint ?? undefined,
);
expect(innerState.values.city).toBe("LA");
@@ -1735,7 +1735,7 @@ describe("subgraphs", () => {
chunks = await gatherIterator(
client.runs.stream(thread.thread_id, assistant.assistant_id, {
input: null,
})
}),
);
expect(chunks.filter((i) => i.event === "error")).toEqual([]);
@@ -1820,7 +1820,7 @@ describe("subgraphs", () => {
const stream = await gatherIterator(
client.runs.stream(thread.thread_id, assistant.assistant_id, {
command: { resume: "i want to resume" },
})
}),
);
expect(stream.at(-1)?.event).toBe("values");
@@ -1837,7 +1837,7 @@ describe("errors", () => {
client.runs.stream(thread.thread_id, assistant.assistant_id, {
input: { messages: [] },
streamMode: ["debug", "events"],
})
}),
);
expect(stream.at(-1)).toMatchObject({
@@ -1856,7 +1856,7 @@ describe("errors", () => {
const run = await client.runs.create(
thread.thread_id,
assistant.assistant_id,
{ input: { messages: [] } }
{ input: { messages: [] } },
);
await client.runs.join(thread.thread_id, run.run_id);
@@ -1871,11 +1871,11 @@ describe("errors", () => {
const run = await client.runs.create(
thread.thread_id,
assistant.assistant_id,
{ input: { messages: [] } }
{ input: { messages: [] } },
);
const stream = await gatherIterator(
client.runs.joinStream(thread.thread_id, run.run_id)
client.runs.joinStream(thread.thread_id, run.run_id),
);
expect(stream.at(-1)).toMatchObject({
@@ -1905,7 +1905,7 @@ describe("long running tasks", () => {
{
input: { messages: [], delay },
config: globalConfig,
}
},
);
await client.runs.join(thread.thread_id, run.run_id);
@@ -1921,7 +1921,7 @@ describe("long running tasks", () => {
expect(runResult.values.messages).toMatchObject([
{ content: `finished after ${delay}ms` },
]);
}
},
);
});
@@ -1947,7 +1947,7 @@ describe("command update state", () => {
client.runs.stream(thread.thread_id, assistant.assistant_id, {
command: { update: { keyOne: "value3", keyTwo: "value4" } },
config: globalConfig,
})
}),
);
expect(stream.filter((chunk) => chunk.event === "error")).toEqual([]);
@@ -1982,7 +1982,7 @@ describe("command update state", () => {
],
},
config: globalConfig,
})
}),
);
expect(stream.filter((chunk) => chunk.event === "error")).toEqual([]);
@@ -2005,7 +2005,7 @@ it("stream debug checkpoint", async () => {
{
input,
streamMode: "debug",
}
},
);
const stream = [];
@@ -2024,13 +2024,13 @@ it("stream debug checkpoint", async () => {
step: i.metadata?.step,
checkpoint: i.checkpoint,
parent_checkpoint: i.parent_checkpoint,
}))
})),
).toEqual(
history.map((i) => ({
step: i.metadata?.step,
checkpoint: i.checkpoint,
parent_checkpoint: i.parent_checkpoint,
}))
})),
);
});
@@ -2047,7 +2047,7 @@ it("continue after interrupt must have checkpoint present", async () => {
input,
streamMode: "debug",
interruptBefore: ["router_node"],
})
}),
);
const initialStream = stream
@@ -2062,7 +2062,7 @@ it("continue after interrupt must have checkpoint present", async () => {
client.runs.stream(thread.thread_id, assistant.assistant_id, {
streamMode: "debug",
checkpoint,
})
}),
);
const continueHistory = (
@@ -2078,13 +2078,13 @@ it("continue after interrupt must have checkpoint present", async () => {
step: i.metadata?.step,
checkpoint: i.checkpoint,
parent_checkpoint: i.parent_checkpoint,
}))
})),
).toEqual(
continueHistory.map((i) => ({
step: i.metadata?.step,
checkpoint: i.checkpoint,
parent_checkpoint: i.parent_checkpoint,
}))
})),
);
});
@@ -2093,7 +2093,7 @@ describe("multitasking", () => {
const pollRun = async (
threadId: string,
runId: string,
maxIter: number = 600
maxIter: number = 600,
) => {
let lastStatus:
| Awaited<ReturnType<typeof client.runs.get>>["status"]
@@ -2128,7 +2128,7 @@ describe("multitasking", () => {
const run = await client.runs.create(
thread.thread_id,
assistant.assistant_id,
{ input, config: globalConfig }
{ input, config: globalConfig },
);
// Attempt another run that should be rejected
@@ -2137,7 +2137,7 @@ describe("multitasking", () => {
input,
multitaskStrategy: "reject",
config: globalConfig,
})
}),
).rejects.toThrow();
const runStatus = await pollRun(thread.thread_id, run.run_id);
@@ -2156,7 +2156,7 @@ describe("multitasking", () => {
const run1 = await client.runs.create(
thread.thread_id,
assistant.assistant_id,
{ input: input1, config: globalConfig }
{ input: input1, config: globalConfig },
);
// Start second run that should interrupt first
@@ -2171,7 +2171,7 @@ describe("multitasking", () => {
input: input2,
multitaskStrategy: "interrupt",
config: globalConfig,
}
},
);
const run1Status = await pollRun(thread.thread_id, run1.run_id);
@@ -2204,7 +2204,7 @@ describe("multitasking", () => {
const run1 = await client.runs.create(
thread.thread_id,
assistant.assistant_id,
{ input: input1, config: globalConfig }
{ input: input1, config: globalConfig },
);
// Start second run that should rollback first
@@ -2214,12 +2214,12 @@ describe("multitasking", () => {
const run2 = await client.runs.create(
thread.thread_id,
assistant.assistant_id,
{ input: input2, multitaskStrategy: "rollback", config: globalConfig }
{ input: input2, multitaskStrategy: "rollback", config: globalConfig },
);
// First run should be deleted
await expect(() =>
pollRun(thread.thread_id, run1.run_id)
pollRun(thread.thread_id, run1.run_id),
).rejects.toThrow();
const run2Status = await pollRun(thread.thread_id, run2.run_id);
@@ -2242,7 +2242,7 @@ describe("multitasking", () => {
const run1 = await client.runs.create(
thread.thread_id,
assistant.assistant_id,
{ input: input1, config: globalConfig }
{ input: input1, config: globalConfig },
);
// Start second run that should be enqueued
@@ -2257,7 +2257,7 @@ describe("multitasking", () => {
input: input2,
multitaskStrategy: "enqueue",
config: globalConfig,
}
},
);
const run1Status = await pollRun(thread.thread_id, run1.run_id);
@@ -2283,7 +2283,7 @@ describe("RemoteGraph", () => {
});
const stream = await graph.stream(
{ messages: [{ type: "human", content: "foo", id: "initial-message" }] },
{ streamMode: "values", ...globalConfig }
{ streamMode: "values", ...globalConfig },
);
const chunks = await gatherIterator(stream);
+6 -6
View File
@@ -37,7 +37,7 @@ class StableFakeListChatModel extends FakeListChatModel {
async *_streamResponseChunks(
_messages: BaseMessage[],
options: this["ParsedCallOptions"],
runManager?: CallbackManagerForLLMRun
runManager?: CallbackManagerForLLMRun,
): AsyncGenerator<ChatGenerationChunk> {
const response = this._currentResponse();
this._incrementResponse();
@@ -68,7 +68,7 @@ class StableFakeListChatModel extends FakeListChatModel {
undefined,
undefined,
undefined,
{ chunk }
{ chunk },
);
}
}
@@ -88,7 +88,7 @@ const getModel = (threadId: string) => {
const agentNode = async (
state: typeof GraphAnnotationInput.State,
config: LangGraphRunnableConfig
config: LangGraphRunnableConfig,
) => {
if (state.interrupt) interrupt("i want to interrupt");
@@ -120,7 +120,7 @@ const agentNode = async (
const toolNode = async (
state: typeof GraphAnnotationInput.State,
config: LangGraphRunnableConfig
config: LangGraphRunnableConfig,
) => {
const store = config.store;
let sharedStateFromStoreConfig: Record<string, any> | null = null;
@@ -144,7 +144,7 @@ const toolNode = async (
const checkSharedStateNode = async (
_: typeof GraphAnnotationInput.State,
config: LangGraphRunnableConfig
config: LangGraphRunnableConfig,
): Promise<Partial<typeof GraphAnnotationInput.State>> => {
const store = config.store;
const namespace = ["inputtedState", "data"];
@@ -174,7 +174,7 @@ const workflow = new StateGraph(
input: GraphAnnotationInput,
output: GraphAnnotationOutput,
},
Annotation.Root({ model_name: Annotation<string> })
Annotation.Root({ model_name: Annotation<string> }),
)
.addNode("agent", agentNode)
.addNode("tool", toolNode)
+1 -1
View File
@@ -12,7 +12,7 @@ const StateSchema = Annotation.Root({
});
const longRunning = async (
state: typeof StateSchema.State
state: typeof StateSchema.State,
): Promise<typeof StateSchema.Update> => {
await new Promise((resolve) => setTimeout(resolve, state.delay));
return { messages: [`finished after ${state.delay}ms`] };
+3 -3
View File
@@ -6,7 +6,7 @@ const child = new StateGraph(
reducer: (a, b) => a.concat(b),
}),
child: Annotation<"child_one" | "child_two">,
})
}),
)
.addNode("c_one", () => ({ messages: ["Entered c_one node"] }))
.addNode("c_two", () => ({ messages: ["Entered c_two node"] }))
@@ -20,7 +20,7 @@ const parent = new StateGraph(
reducer: (a, b) => a.concat(b),
}),
parent: Annotation<"parent_one" | "parent_two">,
})
}),
)
.addNode("p_one", () => ({ messages: ["Entered p_one node"] }))
.addNode("p_two", child.compile())
@@ -33,7 +33,7 @@ const grandParent = new StateGraph(
messages: Annotation<string[]>({
reducer: (a, b) => a.concat(b),
}),
})
}),
)
.addNode("gp_one", () => ({ messages: ["Entered gp_one node"] }))
.addNode("gp_two", parent.compile())
+1 -1
View File
@@ -4,4 +4,4 @@
"@langchain/core": "^0.3.22",
"@langchain/langgraph": "^0.2.31"
}
}
}
+1 -1
View File
@@ -49,7 +49,7 @@ const router = new StateGraph(routerState)
if (route === "weather") return "weather_graph";
return "normal_llm_node";
},
["weather_graph", "normal_llm_node"]
["weather_graph", "normal_llm_node"],
)
.addEdge("weather_graph", END)
.addEdge("normal_llm_node", END);
+15 -15
View File
@@ -56,7 +56,7 @@ describe.concurrent("graph factories", () => {
])("%s", ([prop]) => {
const schemas = SubgraphExtractor.extractSchemas(
{ contents: `${common}\n\nexport const graph = ${prop};` },
"graph"
"graph",
);
expect(schemas.graph.input).toMatchObject(MessagesSchema);
@@ -107,7 +107,7 @@ describe.concurrent("subgraphs", () => {
.compile();
`,
},
"graph"
"graph",
);
expect(schemas["graph|child"].input).toMatchObject({
type: "object",
@@ -226,7 +226,7 @@ describe.concurrent("subgraphs", () => {
.compile();
`,
},
"parent"
"parent",
);
expect(Object.keys(schemas)).toEqual(
@@ -234,7 +234,7 @@ describe.concurrent("subgraphs", () => {
"parent",
"parent|parent_two",
"parent|parent_two|child_two",
])
]),
);
expect(schemas.parent.state).toMatchObject({
@@ -352,10 +352,10 @@ describe.concurrent("subgraphs", () => {
`,
},
"parent",
{ strict: true }
{ strict: true },
);
}).toThrowError(
`Multiple unique subgraph invocations found for "parent|parent_one"`
`Multiple unique subgraph invocations found for "parent|parent_one"`,
);
});
@@ -414,11 +414,11 @@ describe.concurrent("subgraphs", () => {
],
],
},
"graph"
"graph",
);
expect(Object.keys(schemas)).toEqual(
expect.arrayContaining(["graph", "graph|child"])
expect.arrayContaining(["graph", "graph|child"]),
);
expect(schemas["graph|child"].input).toMatchObject({
@@ -547,11 +547,11 @@ describe.concurrent("subgraphs", () => {
],
],
},
"graph"
"graph",
);
expect(Object.keys(schemas)).toEqual(
expect.arrayContaining(["graph", "graph|child"])
expect.arrayContaining(["graph", "graph|child"]),
);
expect(schemas["graph|child"].input).toMatchObject({
@@ -665,7 +665,7 @@ describe.concurrent("subgraphs", () => {
export const graph = parent.compile()
`,
},
"graph"
"graph",
);
expect(schemas["graph|child"].input).toMatchObject({
type: "object",
@@ -803,11 +803,11 @@ test.concurrent("weather", () => {
export const graph = router.compile();
`,
},
"graph"
"graph",
);
expect(Object.keys(schemas)).toEqual(
expect.arrayContaining(["graph", "graph|weather_graph"])
expect.arrayContaining(["graph", "graph|weather_graph"]),
);
});
@@ -861,10 +861,10 @@ test.concurrent("nested", () => {
export const graph = grandParent.compile();
`,
},
"graph"
"graph",
);
expect(Object.keys(schemas)).toEqual(
expect.arrayContaining(["graph", "graph|gp_two", "graph|gp_two|p_two"])
expect.arrayContaining(["graph", "graph|gp_two", "graph|gp_two|p_two"]),
);
});
+2 -2
View File
@@ -5,7 +5,7 @@ import { readFile } from "node:fs/promises";
import { dirname } from "node:path";
const configPath = fileURLToPath(
new URL("./graphs/langgraph.json", import.meta.url)
new URL("./graphs/langgraph.json", import.meta.url),
);
const config = JSON.parse(await readFile(configPath, "utf-8"));
@@ -20,5 +20,5 @@ await spawnServer(
env: config.env,
hostUrl: "https://smith.langchain.com",
},
{ pid: process.pid, projectCwd: dirname(configPath) }
{ pid: process.pid, projectCwd: dirname(configPath) },
);
+1
View File
@@ -0,0 +1 @@
{}
+6 -6
View File
@@ -51,18 +51,18 @@ The CLI uses a `langgraph.json` configuration file with these key settings:
```json5
{
// Required: Graph definitions
"graphs": {
"graph": "./src/graph.ts:graph"
graphs: {
graph: "./src/graph.ts:graph",
},
// Optional: Node version (20 only at the moment)
"node_version": "20",
node_version: "20",
// Optional: Environment variables
"env": ".env",
env: ".env",
// Optional: Additional Dockerfile commands
"dockerfile_lines": []
dockerfile_lines: [],
}
```
+4 -1
View File
@@ -15,12 +15,15 @@
"dist/"
],
"scripts": {
"clean": "npx -y bun scripts/clean.mjs",
"build": "npx -y bun scripts/build.mjs",
"prepack": "pnpm run build",
"typecheck": "tsc --noEmit",
"cli": "tsx src/cli/cli.mts",
"cli:watch": "tsx watch src/cli/cli.mts",
"test": "vitest"
"test": "vitest",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"dependencies": {
"@commander-js/extra-typings": "^13.0.0",
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bun
function $(strings, ...rest) {
console.log("$", ...strings.raw);
return Bun.$(strings, ...rest);
}
await $`rm -rf dist`;
+3 -3
View File
@@ -26,7 +26,7 @@ builder
.option("-c, --config <path>", "Path to configuration file", process.cwd())
.option(
"--no-pull",
"Running the server with locally-built images. By default LangGraph will pull the latest images from the registry"
"Running the server with locally-built images. By default LangGraph will pull the latest images from the registry",
)
.argument("[args...]")
.passThroughOptions()
@@ -35,7 +35,7 @@ builder
withAnalytics((command) => ({
config: command.opts().config !== process.cwd(),
pull: command.opts().pull,
}))
})),
)
.action(async (pass, params) => {
const configPath = await getProjectPath(params.config);
@@ -63,6 +63,6 @@ builder
exec = $({ ...opts, input });
await stream(
exec`docker build -f - -t ${params.tag} ${projectDir} ${pass}`
exec`docker build -f - -t ${params.tag} ${projectDir} ${pass}`,
);
});
+10 -10
View File
@@ -17,7 +17,7 @@ import { withAnalytics } from "./utils/analytics.mjs";
builder
.command("dev")
.description(
"Run LangGraph API server in development mode with hot reloading."
"Run LangGraph API server in development mode with hot reloading.",
)
.option("-p, --port <number>", "port to run the server on", "2024")
.option("-h, --host <string>", "host to bind to", "localhost")
@@ -33,7 +33,7 @@ builder
port: command.opts().port !== "2024",
host: command.opts().host !== "localhost",
n_jobs_per_worker: command.opts().nJobsPerWorker !== "10",
}))
})),
)
.action(async (options, { args }) => {
try {
@@ -65,11 +65,11 @@ builder
if (!gitignoreContent.includes(".langgraph_api")) {
logger.info(
"Updating .gitignore to prevent `.langgraph_api` from being committed."
"Updating .gitignore to prevent `.langgraph_api` from being committed.",
);
await fs.appendFile(
gitignorePath,
"\n# LangGraph API\n.langgraph_api\n"
"\n# LangGraph API\n.langgraph_api\n",
);
}
@@ -96,15 +96,15 @@ builder
const oldWatch = Object.entries(watcher.getWatched()).flatMap(
([dir, files]) =>
files.map((file) => path.resolve(projectCwd, dir, file))
files.map((file) => path.resolve(projectCwd, dir, file)),
);
const addedTarget = newWatch.filter(
(target) => !oldWatch.includes(target)
(target) => !oldWatch.includes(target),
);
const removedTarget = oldWatch.filter(
(target) => !newWatch.includes(target)
(target) => !newWatch.includes(target),
);
watcher.unwatch(removedTarget).add(addedTarget);
@@ -130,21 +130,21 @@ builder
if ("python_version" in config) {
logger.warn(
"Launching Python server from @langchain/langgraph-cli is experimental. Please use the `langgraph-cli` package from PyPi instead."
"Launching Python server from @langchain/langgraph-cli is experimental. Please use the `langgraph-cli` package from PyPi instead.",
);
const { spawnPythonServer } = await import("./dev.python.mjs");
child = await spawnPythonServer(
{ ...options, rest: args },
{ configPath, config, env, hostUrl },
{ pid, projectCwd }
{ pid, projectCwd },
);
} else {
const { spawnServer } = await import("@langchain/langgraph-api");
child = await spawnServer(
options,
{ config, env, hostUrl },
{ pid, projectCwd }
{ pid, projectCwd },
);
}
};
+4 -4
View File
@@ -79,7 +79,7 @@ function getDownloadUrl(info: UvBinaryInfo): string {
async function downloadAndExtract(
url: string,
destPath: string,
info: UvBinaryInfo
info: UvBinaryInfo,
): Promise<string> {
const response = await fetch(url);
if (!response.ok)
@@ -100,7 +100,7 @@ async function downloadAndExtract(
await tarExtract({ file: tempFilePath, cwd: tempDirPath });
sourceBinaryPath = path.resolve(
sourceBinaryPath,
path.basename(tempFilePath).slice(0, ".tar.gz".length * -1)
path.basename(tempFilePath).slice(0, ".tar.gz".length * -1),
);
}
sourceBinaryPath = path.resolve(sourceBinaryPath, info.binaryName);
@@ -151,7 +151,7 @@ export async function spawnPythonServer(
options: {
pid: number;
projectCwd: string;
}
},
) {
const deps = await assembleLocalDeps(context.configPath, context.config);
const requirements = deps.rebuildFiles.filter((i) => i.endsWith(".txt"));
@@ -180,6 +180,6 @@ export async function spawnPythonServer(
stdio: ["inherit", "inherit", "inherit"],
env: context.env,
cwd: options.projectCwd,
}
},
);
}
+7 -7
View File
@@ -25,12 +25,12 @@ const fileExists = async (path: string) => {
builder
.command("dockerfile")
.description(
"Generate a Dockerfile for the LangGraph API server, with Docker Compose options."
"Generate a Dockerfile for the LangGraph API server, with Docker Compose options.",
)
.argument("<save-path>", "Path to save the Dockerfile")
.option(
"--add-docker-compose",
"Add additional files for running the LangGraph API server with docker-compose. These files include a docker-compose.yml, .env file, and a .dockerignore file."
"Add additional files for running the LangGraph API server with docker-compose. These files include a docker-compose.yml, .env file, and a .dockerignore file.",
)
.option("-c, --config <path>", "Path to configuration file", process.cwd())
.hook(
@@ -38,7 +38,7 @@ builder
withAnalytics((command) => ({
config: command.opts().config !== process.cwd(),
add_docker_compose: !!command.opts().addDockerCompose,
}))
})),
)
.action(async (savePath, options) => {
const configPath = await getProjectPath(options.config);
@@ -67,7 +67,7 @@ builder
const composePath = path.resolve(
path.dirname(targetPath),
"docker-compose.yml"
"docker-compose.yml",
);
await fs.writeFile(composePath, compose);
@@ -75,7 +75,7 @@ builder
const dockerignorePath = path.resolve(
path.dirname(targetPath),
".dockerignore"
".dockerignore",
);
if (!fileExists(dockerignorePath)) {
@@ -124,7 +124,7 @@ builder
*.test.js
*.spec.js
tests
`
`,
);
logger.info(`✅ Created: ${path.basename(dockerignorePath)}`);
}
@@ -139,7 +139,7 @@ builder
# Or if you have a LangGraph Cloud license key, then uncomment the following line:
# LANGGRAPH_CLOUD_LICENSE_KEY=your-license-key
# Add any other environment variables go below...
`
`,
);
logger.info(`✅ Created: ${path.basename(envPath)}`);
}
+11 -11
View File
@@ -30,7 +30,7 @@ const waitForHealthcheck = async (port: number) => {
while (Date.now() - now < 10_000) {
const ok = await fetch(`http://localhost:${port}/ok`).then(
(res) => res.ok,
() => false
() => false,
);
await new Promise((resolve) => setTimeout(resolve, 100));
@@ -46,23 +46,23 @@ builder
.option("-c, --config <path>", "Path to configuration file", process.cwd())
.option(
"-d, --docker-compose <path>",
"Advanced: Path to docker-compose.yml file with additional services to launch"
"Advanced: Path to docker-compose.yml file with additional services to launch",
)
.option("-p, --port <port>", "Port to run the server on", "8123")
.option("--recreate", "Force recreate containers and volumes", false)
.option(
"--no-pull",
"Running the server with locally-built images. By default LangGraph will pull the latest images from the registry"
"Running the server with locally-built images. By default LangGraph will pull the latest images from the registry",
)
.option("--watch", "Restart on file changes", false)
.option(
"--wait",
"Wait for services to start before returning. Implies --detach",
false
false,
)
.option(
"--postgres-uri <uri>",
"Postgres URI to use for the database. Defaults to launching a local database"
"Postgres URI to use for the database. Defaults to launching a local database",
)
.hook(
"preAction",
@@ -75,7 +75,7 @@ builder
pull: command.opts().pull,
watch: command.opts().watch,
wait: command.opts().wait,
}))
})),
)
.action(async (params) => {
logger.info("Starting LangGraph API server...");
@@ -83,7 +83,7 @@ builder
dedent`
For local dev, requires env var LANGSMITH_API_KEY with access to LangGraph Cloud closed beta.
For production use, requires a license key in env var LANGGRAPH_CLOUD_LICENSE_KEY.
`
`,
);
const configPath = await getProjectPath(params.config);
@@ -118,13 +118,13 @@ builder
// remove dangling images
logger.info(`Pruning dangling images...`);
await stream(
exec`docker image prune -f --filter ${`label=com.docker.compose.project=${name}`}`
exec`docker image prune -f --filter ${`label=com.docker.compose.project=${name}`}`,
);
// remove stale containers
logger.info(`Pruning stale containers...`);
await stream(
exec`docker container prune -f --filter ${`label=com.docker.compose.project=${name}`}`
exec`docker container prune -f --filter ${`label=com.docker.compose.project=${name}`}`,
);
const input = createCompose(capabilities, {
@@ -148,7 +148,7 @@ builder
args.push("--watch");
} else {
logger.warn(
"Watch mode is not available. Please upgrade your Docker Engine."
"Watch mode is not available. Please upgrade your Docker Engine.",
);
}
} else if (params.wait) {
@@ -178,7 +178,7 @@ builder
- LangGraph Studio: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:${params.port}
`);
},
() => void 0
() => void 0,
);
await up.catch(() => void 0);
@@ -34,7 +34,7 @@ async function logData(data: LogData): Promise<void> {
let analyticsPromise = Promise.resolve();
export function withAnalytics<TCommand extends Command<any, any, any>>(
fn?: (command: TCommand) => Record<string, boolean>
fn?: (command: TCommand) => Record<string, boolean>,
) {
if (process.env.LANGGRAPH_CLI_NO_ANALYTICS === "1") {
return () => void 0;
@@ -49,7 +49,7 @@ export function withAnalytics<TCommand extends Command<any, any, any>>(
cli_version: version,
cli_command: actionCommand.name(),
params: fn?.(actionCommand) ?? {},
}).catch(() => {})
}).catch(() => {}),
);
};
}
@@ -53,7 +53,7 @@ export const createIpcServer = async () => {
bufferData((message: Buffer) => {
const data = JSON.parse(message.toString());
server.emit("data", data);
})
}),
);
});
@@ -1,17 +1,17 @@
// MIT License
//
//
// Copyright (c) Hiroki Osame <hiroki.osame@gmail.com>
//
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -19,7 +19,7 @@
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
//
// https://github.com/privatenumber/tsx/tree/28a3e7d2b8fd72b683aab8a98dd1fcee4624e4cb
import path from "node:path";
import os from "node:os";
+1 -1
View File
@@ -4,7 +4,7 @@ import * as url from "node:url";
async function getVersion() {
try {
const packageJson = url.fileURLToPath(
new URL("../../../package.json", import.meta.url)
new URL("../../../package.json", import.meta.url),
);
const { version } = JSON.parse(await fs.readFile(packageJson, "utf-8"));
return version + "+js";
+10 -6
View File
@@ -59,7 +59,7 @@ function parseVersion(input: string): Version {
const patchStr = parts[2] ?? "0";
const major = Number.parseInt(
majorStr.startsWith("v") ? majorStr.slice(1) : majorStr
majorStr.startsWith("v") ? majorStr.slice(1) : majorStr,
);
const minor = Number.parseInt(minorStr);
const patch = Number.parseInt(patchStr.split("-").at(0) ?? "0");
@@ -96,7 +96,7 @@ export async function getDockerCapabilities(): Promise<DockerCapabilities> {
z.object({
Name: z.string(),
Version: z.string().optional(),
})
}),
),
}),
})
@@ -108,11 +108,11 @@ export async function getDockerCapabilities(): Promise<DockerCapabilities> {
const composePlugin = info.data.ClientInfo.Plugins.find(
(i): i is { Name: string; Version: string } =>
i.Name === "compose" && i.Version != null
i.Name === "compose" && i.Version != null,
);
const buildxPlugin = info.data.ClientInfo.Plugins.find(
(i): i is { Name: string; Version: string } =>
i.Name === "buildx" && i.Version != null
i.Name === "buildx" && i.Version != null,
);
let composeRes: Pick<DockerCapabilities, "versionCompose" | "composeType">;
@@ -124,7 +124,7 @@ export async function getDockerCapabilities(): Promise<DockerCapabilities> {
} else {
try {
const standalone = await $(
await getExecaOptions()
await getExecaOptions(),
)`docker-compose --version --short`;
composeRes = {
composeType: "standalone",
@@ -159,7 +159,11 @@ function isPlainObject(value: unknown): value is Record<string, any> {
export function createCompose(
capabilities: DockerCapabilities,
options: { port?: number; postgresUri?: string; apiDef?: Record<string, any> }
options: {
port?: number;
postgresUri?: string;
apiDef?: Record<string, any>;
},
) {
let includeDb = false;
let postgresUri = options.postgresUri;
+16 -16
View File
@@ -25,7 +25,7 @@ async function exists(path: string) {
export async function assembleLocalDeps(
configPath: string,
config: Config
config: Config,
): Promise<LocalDeps> {
const reserved = new Set([
"src",
@@ -46,7 +46,7 @@ export async function assembleLocalDeps(
function checkReserved(name: string, ref: string) {
if (reserved.has(name)) {
throw new Error(
`Package name '${name}' used in local dep '${ref}' is reserved. Rename the directory.`
`Package name '${name}' used in local dep '${ref}' is reserved. Rename the directory.`,
);
}
reserved.add(name);
@@ -71,7 +71,7 @@ export async function assembleLocalDeps(
throw new Error(`Local dependency must be a directory: ${resolved}`);
} else if (!resolved.startsWith(path.dirname(configPath))) {
throw new Error(
`Local dependency must be a subdirectory of the config file: ${resolved}`
`Local dependency must be a subdirectory of the config file: ${resolved}`,
);
}
@@ -98,7 +98,7 @@ export async function assembleLocalDeps(
// flat layout
if (path.basename(resolved).includes("-")) {
throw new Error(
`Package name '${path.basename(resolved)}' contains a hyphen. Rename the directory to use it as flat-layout package.`
`Package name '${path.basename(resolved)}' contains a hyphen. Rename the directory to use it as flat-layout package.`,
);
}
@@ -167,13 +167,13 @@ export async function assembleLocalDeps(
async function updateGraphPaths(
configPath: string,
config: Config,
localDeps: LocalDeps
localDeps: LocalDeps,
) {
for (const [graphId, importStr] of Object.entries(config.graphs)) {
let [moduleStr, attrStr] = importStr.split(":", 2);
if (!moduleStr || !attrStr) {
throw new Error(
`Import string "${importStr}" must be in format "<module>:<attribute>".`
`Import string "${importStr}" must be in format "<module>:<attribute>".`,
);
}
@@ -193,7 +193,7 @@ async function updateGraphPaths(
}
for (const [fauxPkg, [_, destPath]] of Object.entries(
localDeps.fauxPkgs
localDeps.fauxPkgs,
)) {
if (resolved.startsWith(fauxPkg)) {
moduleStr = `${destPath}/${path.relative(fauxPkg, resolved)}`;
@@ -201,7 +201,7 @@ async function updateGraphPaths(
}
throw new Error(
`Module '${importStr}' not found in 'dependencies' list. Add its containing package to 'dependencies' list.`
`Module '${importStr}' not found in 'dependencies' list. Add its containing package to 'dependencies' list.`,
);
}
}
@@ -232,7 +232,7 @@ export async function configToDocker(
watch: boolean;
dockerCommand?: string;
onWorkingDir?: (workingDir: string | undefined) => void;
}
},
) {
// figure out the package manager used here
const testFile = async (file: string) =>
@@ -264,16 +264,16 @@ export async function configToDocker(
: undefined;
const pipReqs = localDeps.pipReqs.map(
([reqpath, destpath]) => `ADD ${reqpath} ${destpath}`
([reqpath, destpath]) => `ADD ${reqpath} ${destpath}`,
);
if (pipReqs.length) {
pipReqs.push(
`RUN ${pipInstall} ${localDeps.pipReqs.map(([, r]) => `-r ${r}`).join(" ")}`
`RUN ${pipInstall} ${localDeps.pipReqs.map(([, r]) => `-r ${r}`).join(" ")}`,
);
}
const localPkg = Object.entries(localDeps.realPkgs).map(
([fullpath, relpath]) => `ADD ${relpath} /deps/${path.basename(fullpath)}`
([fullpath, relpath]) => `ADD ${relpath} /deps/${path.basename(fullpath)}`,
);
const fauxPkgs = Object.entries(localDeps.fauxPkgs).flatMap(
@@ -289,7 +289,7 @@ export async function configToDocker(
echo "${options?.dockerCommand === "build" ? "$line" : "$$line"}" >> /deps/__outer_${path.basename(fullpath)}/pyproject.toml; \
done
`,
]
],
);
if (
@@ -344,7 +344,7 @@ export async function configToDocker(
if (options?.watch && (localDeps.workingDir || localDeps.reloadDir)) {
// TODO: hacky, should add as entrypoint to the langgraph-api base image
lines.push(
`CMD exec uvicorn langgraph_api.server:app --log-config /api/logging.json --no-access-log --host 0.0.0.0 --port 8000 --reload --reload-dir ${localDeps.workingDir || localDeps.reloadDir}`
`CMD exec uvicorn langgraph_api.server:app --log-config /api/logging.json --no-access-log --host 0.0.0.0 --port 8000 --reload --reload-dir ${localDeps.workingDir || localDeps.reloadDir}`,
);
}
@@ -354,7 +354,7 @@ export async function configToDocker(
export async function configToWatch(
configPath: string,
config: Config,
localDeps: LocalDeps
localDeps: LocalDeps,
) {
const projectDir = path.dirname(configPath);
const watch: Array<{
@@ -405,7 +405,7 @@ export async function configToCompose(
options?: {
watch: boolean;
extendEnv?: Record<string, string>;
}
},
): Promise<{
apiDef: Record<string, unknown>;
rewrite: { source: string; target: string } | undefined;
+4 -4
View File
@@ -53,8 +53,8 @@ async function getLoginPath() {
const [fromShell, fromBackup] = await Promise.allSettled(
[extractPathFromShell(), guessUserPath()].map((promise) =>
promise.then(verifyDockerPath)
)
promise.then(verifyDockerPath),
),
);
if (fromShell.status === "fulfilled") {
@@ -65,7 +65,7 @@ async function getLoginPath() {
console.error(
"Failed to get PATH from shell or backup",
fromShell.reason,
fromBackup.reason
fromBackup.reason,
);
throw fromShell.reason || fromBackup.reason;
}
@@ -76,7 +76,7 @@ async function getLoginPath() {
type CommonOptions = Exclude<Parameters<ExecaScriptMethod>[1], undefined>;
export async function getExecaOptions<T extends CommonOptions = {}>(
options?: T
options?: T,
) {
const env = await getLoginPath();
return { ...options, env } as T & { env: typeof env };
+5 -5
View File
@@ -7,7 +7,7 @@ const BaseConfigSchema = z.object({
graphs: z.record(
z.string().refine((i) => i.includes(":"), {
message: "Import string must be in format '<file>:<export>'",
})
}),
),
_INTERNAL_docker_tag: z.string().optional(),
env: z
@@ -45,16 +45,16 @@ const PythonConfigSchema = BaseConfigSchema.merge(
dependencies: z
.array(z.string())
.nonempty("You need to specify at least one dependency"),
})
}),
).merge(
z.object({
python_version: PythonVersionSchema.default(DEFAULT_PYTHON_VERSION),
node_version: NodeVersionSchema.optional(),
})
}),
);
const NodeConfigSchema = BaseConfigSchema.merge(
z.object({ node_version: NodeVersionSchema.default(DEFAULT_NODE_VERSION) })
z.object({ node_version: NodeVersionSchema.default(DEFAULT_NODE_VERSION) }),
);
const ConfigSchema = z.union([NodeConfigSchema, PythonConfigSchema]);
@@ -66,7 +66,7 @@ export const getConfig = (config: z.input<typeof ConfigSchema> | string) => {
const { graphs } = BaseConfigSchema.parse(input);
const isPython = Object.values(graphs).map((i) =>
PYTHON_EXTENSIONS.includes(extname(i.split(":")[0]))
PYTHON_EXTENSIONS.includes(extname(i.split(":")[0])),
);
const somePython = isPython.some((i) => i);
const someNode = !isPython.every((i) => i);
+5 -5
View File
@@ -48,7 +48,7 @@ export const logger = createLogger({
return JSON.stringify({ timestamp, level, event, ...rest });
}),
])
]),
),
transports: [new transports.Console()],
});
@@ -59,7 +59,7 @@ const formatStack = (stack: string | undefined | null) => {
const [firstFile] = stacktraceParser(stack).filter(
(item) =>
!item.file?.split(path.sep).includes("node_modules") &&
!item.file?.startsWith("node:")
!item.file?.startsWith("node:"),
);
if (firstFile?.file && firstFile?.lineNumber) {
@@ -72,7 +72,7 @@ const formatStack = (stack: string | undefined | null) => {
const spliceIndex = messageLines.findIndex((i) => i.includes(filePath));
const padding = " ".repeat(
Math.max(0, messageLines[spliceIndex].indexOf("at"))
Math.max(0, messageLines[spliceIndex].indexOf("at")),
);
const highlightCode = process.stdout.isTTY;
@@ -80,7 +80,7 @@ const formatStack = (stack: string | undefined | null) => {
let codeFrame = codeFrameColumns(
readFileSync(filePath, "utf-8"),
{ start: { line, column } },
{ highlightCode }
{ highlightCode },
);
codeFrame = codeFrame
@@ -111,7 +111,7 @@ export const logError = (
options?: {
context?: Record<string, unknown>;
prefix?: string;
}
},
) => {
let message;
let context = options?.context;
+33 -33
View File
@@ -35,13 +35,13 @@ describe("config to watch", () => {
const localDeps = await assembleLocalDeps(
path.resolve(__dirname, "./unit_tests/langgraph.json"),
config
config,
);
const watch = await configToWatch(
path.resolve(__dirname, "./unit_tests/langgraph.json"),
config,
localDeps
localDeps,
);
expect(watch).toEqual([
@@ -65,13 +65,13 @@ describe("config to watch", () => {
const localDeps = await assembleLocalDeps(
path.resolve(__dirname, "./unit_tests/langgraph.json"),
config
config,
);
const watch = await configToWatch(
path.resolve(__dirname, "./unit_tests/langgraph.json"),
config,
localDeps
localDeps,
);
expect(watch).toEqual([
@@ -113,7 +113,7 @@ describe("config to docker", () => {
const actual = await configToDocker(
PATH_TO_CONFIG,
config,
await assembleLocalDeps(PATH_TO_CONFIG, config)
await assembleLocalDeps(PATH_TO_CONFIG, config),
);
expect(actual).toEqual(dedenter`
@@ -144,7 +144,7 @@ describe("config to docker", () => {
const actual = await configToDocker(
PATH_TO_CONFIG,
config,
await assembleLocalDeps(PATH_TO_CONFIG, config)
await assembleLocalDeps(PATH_TO_CONFIG, config),
);
expect(actual).toEqual(dedenter`
@@ -178,7 +178,7 @@ describe("config to docker", () => {
await configToDocker(
PATH_TO_CONFIG,
config,
await assembleLocalDeps(PATH_TO_CONFIG, config)
await assembleLocalDeps(PATH_TO_CONFIG, config),
);
}).rejects.toThrowError(/Could not find local dependency/);
@@ -194,7 +194,7 @@ describe("config to docker", () => {
await configToDocker(
PATH_TO_CONFIG,
config,
await assembleLocalDeps(PATH_TO_CONFIG, config)
await assembleLocalDeps(PATH_TO_CONFIG, config),
);
}).rejects.toThrowError(/Could not find local module/);
});
@@ -210,7 +210,7 @@ describe("config to docker", () => {
const actual = await configToDocker(
PATH_TO_CONFIG,
config,
await assembleLocalDeps(PATH_TO_CONFIG, config)
await assembleLocalDeps(PATH_TO_CONFIG, config),
);
expect(actual).toEqual(dedenter`
@@ -239,7 +239,7 @@ describe("config to docker", () => {
version = "0.1"
dependencies = ["langchain"]
`,
{ encoding: "utf-8" }
{ encoding: "utf-8" },
);
const graphs = { agent: "./graphs/agent.py:graph" };
@@ -252,7 +252,7 @@ describe("config to docker", () => {
const actual = await configToDocker(
PATH_TO_CONFIG,
config,
await assembleLocalDeps(PATH_TO_CONFIG, config)
await assembleLocalDeps(PATH_TO_CONFIG, config),
);
await fs.rm(pyproject);
@@ -279,7 +279,7 @@ describe("config to docker", () => {
const actual = await configToDocker(
PATH_TO_CONFIG,
config,
await assembleLocalDeps(PATH_TO_CONFIG, config)
await assembleLocalDeps(PATH_TO_CONFIG, config),
);
expect(actual).toEqual(dedenter`
@@ -314,7 +314,7 @@ describe("config to docker", () => {
const actual = await configToDocker(
PATH_TO_CONFIG,
config,
await assembleLocalDeps(PATH_TO_CONFIG, config)
await assembleLocalDeps(PATH_TO_CONFIG, config),
);
// TODO: add support for any packager
@@ -484,7 +484,7 @@ describe("config to compose", () => {
dependencies: ["."],
graphs: graph,
},
{ watch: true }
{ watch: true },
);
expect(yaml.stringify(actual, { blockQuote: "literal" })).toEqual(expected);
@@ -497,7 +497,7 @@ describe("config to compose", () => {
it("env", async () => {
const PATH_TO_CONFIG = path.resolve(
__dirname,
"./env_tests/langgraph.json"
"./env_tests/langgraph.json",
);
const graph = { agent: "./agent.py:graph" };
@@ -545,7 +545,7 @@ describe("config to compose", () => {
{
watch: true,
extendEnv: { ANTHROPIC_API_KEY: "key", OPENAI_API_KEY: "key" },
}
},
);
expect(yaml.stringify(actual, { blockQuote: "literal" })).toEqual(expected);
@@ -566,10 +566,10 @@ describe("config to compose", () => {
env: ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"],
dockerfile_lines: [],
},
{ watch: true }
)
{ watch: true },
),
).rejects.toThrowError(
/Missing environment variables: OPENAI_API_KEY, ANTHROPIC_API_KEY/
/Missing environment variables: OPENAI_API_KEY, ANTHROPIC_API_KEY/,
);
});
@@ -659,7 +659,7 @@ describe("config to compose", () => {
env: ".env",
dockerfile_lines: [],
},
{ watch: true }
{ watch: true },
);
expect(yaml.stringify(actual, { blockQuote: "literal" })).toEqual(expected);
@@ -672,11 +672,11 @@ describe("config to compose", () => {
describe("packaging", () => {
async function loadConfig(
rel: string
rel: string,
): Promise<[path: string, config: Config]> {
const res = path.resolve(__dirname, rel);
const config = getConfig(
JSON.parse(await fs.readFile(res, { encoding: "utf-8" }))
JSON.parse(await fs.readFile(res, { encoding: "utf-8" })),
);
return [res, config];
}
@@ -684,7 +684,7 @@ describe("packaging", () => {
it("faux", async () => {
const { apiDef: actual } = await configToCompose(
...(await loadConfig("./packaging_tests/faux/langgraph.json")),
{ watch: true }
{ watch: true },
);
const expected =
@@ -728,7 +728,7 @@ describe("packaging", () => {
it("js", async () => {
const { apiDef: actual, rewrite } = await configToCompose(
...(await loadConfig("./packaging_tests/js/langgraph.json")),
{ watch: true }
{ watch: true },
);
const expected =
@@ -787,7 +787,7 @@ it("node config and python config", () => {
dockerfile_lines: [],
dependencies: ["."],
graphs: { agent: "./route.ts:agent" },
})
}),
).toEqual({
node_version: "20",
dockerfile_lines: [],
@@ -804,7 +804,7 @@ it("node config and python config", () => {
pip_config_file: undefined,
dependencies: ["."],
graphs: { agent: "./agent.py:graph" },
})
}),
).toEqual({
python_version: "3.11",
pip_config_file: undefined,
@@ -819,7 +819,7 @@ it("node config and python config", () => {
getConfig({
dependencies: ["."],
graphs: { agent: "./agent.py:graph" },
})
}),
).toEqual({
python_version: "3.11",
pip_config_file: undefined,
@@ -842,7 +842,7 @@ it("node config and python config", () => {
getConfig({
dependencies: ["."],
graphs: { js: "./agent.js:graph", py: "./agent.py:graph" },
})
}),
).toEqual({
python_version: "3.12",
node_version: "20",
@@ -860,7 +860,7 @@ it("node config and python config", () => {
python_version: "3.11",
graphs: { agent: "./agent.py:graph", js: "./agent.js:graph" },
dependencies: ["."],
})
}),
)
.toThrow("Only Python 3.12 is supported with Node.js");
@@ -870,7 +870,7 @@ it("node config and python config", () => {
getConfig({
graphs: { agent: "agent.py" },
dependencies: ["."],
})
}),
)
.toThrow(`Import string must be in format '<file>:<export>'`);
@@ -881,7 +881,7 @@ it("node config and python config", () => {
graphs: { agent: "./agent.py:graph" },
// @ts-expect-error
dependencies: [], // Empty array
})
}),
)
.toThrow("You need to specify at least one dependency");
@@ -893,7 +893,7 @@ it("node config and python config", () => {
python_version: "3.10", // Unsupported version
graphs: { agent: "./agent.py:graph" },
dependencies: ["."],
})
}),
)
.toThrow();
@@ -904,7 +904,7 @@ it("node config and python config", () => {
// @ts-expect-error
node_version: "18", // Unsupported version
graphs: { agent: "./agent.js:graph" },
})
}),
)
.toThrow();
});
+2 -2
View File
@@ -48,8 +48,8 @@ it("compose with custom db and healthcheck", () => {
expect(
createCompose(
{ ...DEFAULT_DOCKER_CAPABILITIES, healthcheckStartInterval: true },
{ port, postgresUri }
)
{ port, postgresUri },
),
).toEqual(dedent`
services:
langgraph-redis:
+11 -2
View File
@@ -1,6 +1,15 @@
{
"name": "langgraphjs-api",
"private": true,
"scripts": {},
"keywords": []
"packageManager": "pnpm@9.1.1",
"scripts": {
"clean": "turbo run clean",
"build": "turbo run build",
"format": "turbo run format",
"format:check": "turbo run format:check"
},
"keywords": [],
"devDependencies": {
"turbo": "^2.4.0"
}
}
+66 -1
View File
@@ -6,7 +6,11 @@ settings:
importers:
.: {}
.:
devDependencies:
turbo:
specifier: ^2.4.0
version: 2.4.0
libs/create-langgraph:
dependencies:
@@ -1307,6 +1311,40 @@ packages:
engines: {node: '>=18.0.0'}
hasBin: true
turbo-darwin-64@2.4.0:
resolution: {integrity: sha512-kVMScnPUa3R4n7woNmkR15kOY0aUwCLJcUyH5UC59ggKqr5HIHwweKYK8N1pwBQso0LQF4I9i93hIzfJguCcwQ==}
cpu: [x64]
os: [darwin]
turbo-darwin-arm64@2.4.0:
resolution: {integrity: sha512-8JObIpfun1guA7UlFR5jC/SOVm49lRscxMxfg5jZ5ABft79rhFC+ygN9AwAhGKv6W2DUhIh2xENkSgu4EDmUyg==}
cpu: [arm64]
os: [darwin]
turbo-linux-64@2.4.0:
resolution: {integrity: sha512-xWDGGcRlBuGV7HXWAVuTY6vsQi4aZxGMAnuiuNDg8Ij1aHGohOM0RUsWMXjxz4vuJmjk9+/D6NQqHH3AJEXezg==}
cpu: [x64]
os: [linux]
turbo-linux-arm64@2.4.0:
resolution: {integrity: sha512-c3En99xMguc/Pdtk/rZP53LnDdw0W6lgUc04he8r8F+UHYSNvgzHh0WGXXmCC6lGbBH72kPhhGx4bAwyvi7dug==}
cpu: [arm64]
os: [linux]
turbo-windows-64@2.4.0:
resolution: {integrity: sha512-/gOORuOlyA8JDPzyA16CD3wvyRcuBFePa1URAnFUof9hXQmKxK0VvSDO79cYZFsJSchCKNJpckUS0gYxGsWwoA==}
cpu: [x64]
os: [win32]
turbo-windows-arm64@2.4.0:
resolution: {integrity: sha512-/DJIdTFijEMM5LSiEpSfarDOMOlYqJV+EzmppqWtHqDsOLF4hbbIBH9sJR6OOp5dURAu5eURBYdmvBRz9Lo6TA==}
cpu: [arm64]
os: [win32]
turbo@2.4.0:
resolution: {integrity: sha512-ah/yQp2oMif1X0u7fBJ4MLMygnkbKnW5O8SG6pJvloPCpHfFoZctkSVQiJ3VnvNTq71V2JJIdwmOeu1i34OQyg==}
hasBin: true
type-fest@0.7.1:
resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==}
engines: {node: '>=8'}
@@ -2385,6 +2423,33 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
turbo-darwin-64@2.4.0:
optional: true
turbo-darwin-arm64@2.4.0:
optional: true
turbo-linux-64@2.4.0:
optional: true
turbo-linux-arm64@2.4.0:
optional: true
turbo-windows-64@2.4.0:
optional: true
turbo-windows-arm64@2.4.0:
optional: true
turbo@2.4.0:
optionalDependencies:
turbo-darwin-64: 2.4.0
turbo-darwin-arm64: 2.4.0
turbo-linux-64: 2.4.0
turbo-linux-arm64: 2.4.0
turbo-windows-64: 2.4.0
turbo-windows-arm64: 2.4.0
type-fest@0.7.1: {}
typescript@5.5.4: {}
+26
View File
@@ -0,0 +1,26 @@
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"format": {
"dependsOn": ["^format"]
},
"format:check": {
"dependsOn": ["^format:check"]
},
"test": {
"cache": false,
"dependsOn": ["^build", "build"]
},
"clean": {
"dependsOn": ["^clean"]
},
"precommit": {},
"start": {
"cache": false
}
}
}