# LangGraphAgent **Repository Path**: Pooyan/lang-graph-agent ## Basic Information - **Project Name**: LangGraphAgent - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-06-18 - **Last Updated**: 2026-10-17 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # LangGraphAgent LangGraph + TypeScript agent starter. ## Scripts - `pnpm dev` runs the local TypeScript entrypoint with `tsx`. - `pnpm check` type-checks the project. - `pnpm test` runs Vitest tests. - `pnpm build` emits JavaScript and declarations to `dist`. - `pnpm start` runs the built entrypoint. - `pnpm langgraph:dev` starts the LangGraph.js local development server. - `pnpm validate:agent` runs a live ReAct loop validation. - `pnpm validate:model` runs a minimal live model API validation. - `pnpm lint` / `pnpm lint:fix` runs ESLint. - `pnpm format:check` / `pnpm format` runs Prettier. - `pnpm verify` runs format check, lint, type check and tests in one go. There is no hosted CI: the repository lives on Gitee, which does not run GitHub Actions. Instead, a local `pre-push` hook runs `pnpm verify` and blocks the push if any gate fails. Bypass it once with `git push --no-verify`. The hook lives at `.git/hooks/pre-push`, which git does not track, so a fresh clone will not have it. Recreate it with: ```bash cat > .git/hooks/pre-push <<'HOOK' #!/bin/sh echo "pre-push: running pnpm verify (format, lint, type check, tests)" if ! pnpm verify; then echo "pre-push: verification failed - push aborted." >&2 exit 1 fi HOOK chmod +x .git/hooks/pre-push ``` ## Local Setup ```bash pnpm install cp .env.example .env pnpm test pnpm dev ``` Set `DEEPSEEK_API_KEY` in `.env` before invoking the default agent graph, `TAVILY_API_KEY` if you want the `web_search` tool to work, and `DATA_ROOT` if you want the daily report tools to work. You can change `DEEPSEEK_MODEL` there as needed. ## Model Provider The starter uses a small DeepSeek HTTP client in `src/agent/model.ts`: ```dotenv DEEPSEEK_API_KEY=your-deepseek-api-key DEEPSEEK_MODEL=deepseek-flash DEEPSEEK_TIMEOUT_MS=120000 DEEPSEEK_MAX_RETRIES=2 ``` `deepseek-flash` is the canonical id returned by `GET /models`. The older `deepseek-v4-flash` still works but is only an alias that the API silently rewrites, so it could disappear without notice. Run the model smoke test and the full ReAct loop validation: ```bash pnpm validate:model pnpm validate:agent ``` `pnpm validate:agent` is backed by reusable smoke-test helpers in `src/agent/smoke.ts`. A failure carries a `code` that says which layer broke: `tool_not_called`, `tool_execution_failed`, `model_api_failed`, `recursion_limit_exceeded`, or `graph_failed`. Model failures are identified by `ModelRequestError` rather than by matching message text, so a runaway tool loop is no longer misreported as a provider outage. The starter graph is exported from `src/agent/graph.ts` and configured in `langgraph.json` as `agent`. Both outbound HTTP calls go through `src/agent/http.ts`. The DeepSeek request aborts after `DEEPSEEK_TIMEOUT_MS` (default 120000) and `web_search` after `searchTimeoutMs` (default 15000, passed to `createAgentTools`); the timeout covers the response body read too, so a server that returns headers and then stalls still fails fast. Both retry with exponential backoff on 408/429/5xx and connection-level failures — DeepSeek up to `DEEPSEEK_MAX_RETRIES` (default 2) and `web_search` up to `searchMaxRetries` (default 2). Timeouts are deliberately not retried so a stalled upstream cannot multiply the wait. When the model returns malformed JSON for a tool call, the call still runs with empty arguments so the tool's own validation can tell the model to retry. The raw payload and parse error are recorded in the response's `response_metadata.toolCallParseFailures`, because the downstream validation error no longer mentions the real cause. ## Agent Loop The graph uses a ReAct-style loop: ```text START -> agent -> tools -> agent -> END ``` The `agent` node calls the DeepSeek chat model with tools bound. If the model returns tool calls, the graph routes to the `tools` node, executes the requested tools, and returns to `agent` for the final response. Default tools: - `get_current_time` returns the current time as an ISO-8601 string. - `calculator` performs add, subtract, multiply, and divide operations. - `web_search` queries the Tavily Search API and returns titles, snippets, and URLs. It requires `TAVILY_API_KEY` and raises a clear error at call time when the key is missing, so the rest of the graph still works without it. - `list_verticals` lists the existing vertical folders under `DATA_ROOT`. - `save_daily_report` writes one day's report, creating the vertical folder when needed. - `read_reports` reads stored reports back as structured data for trend math. - `save_overview` rewrites the root-level `整体情况.md`. The four report tools require `DATA_ROOT` and raise a clear error at call time when it is unset. ## Records on Disk Durable facts live on the filesystem rather than in the message history: ```text / 整体情况.md # current-state summary, regenerated in full 运动健身/ 2026-10-13-日报.md 2026-10-14-日报.md 工作/ 2026-10-14-日报.md ``` Each daily report is YAML frontmatter plus the four-section Markdown body: ```markdown --- date: 2026-10-14 metrics: 体重_kg: 71.8 体脂率_pct: 20.9 vertical: 运动健身 --- ## 一、真实客观数据展示 ... ``` Frontmatter is generated by `save_daily_report` from the structured `metrics` argument, never written by the model as free text, so it always parses. `read_reports` reads it back without involving the model, which is what makes cross-month trend math precise instead of a matter of the model re-reading its own prose. Verticals are created on demand from names the model supplies. Because those names are interpolated into filesystem paths, `src/agent/records.ts` validates them against a whitelist (`sanitizeVertical`) that rejects separators, dots, newlines and over-long values, and every composed path is re-checked to be inside `DATA_ROOT` (`insideRoot`). Dates are validated as real `YYYY-MM-DD` values. `整体情况.md` is regenerated in full from `read_reports` data on every save. It is deliberately not edited incrementally — mutating the previous version would let small reading errors compound day over day. ## System Prompt `src/agent/prompt.ts` holds the system prompt. The agent acts as a health and work data management expert. For each daily entry it resolves the date, lists existing verticals so it reuses one instead of inventing a near-duplicate, reads prior metrics, computes deltas, saves the report, and regenerates the overview. The report itself is a four-section Markdown document — raw data, trend comparison, status assessment, and follow-up recommendations. The prompt also enforces data discipline (never invent values the user did not supply, mark missing fields as unreported, keep derived values such as BMI out of `metrics`), routes date and arithmetic lookups through `get_current_time` and `calculator` instead of guessing, tells the model that history is trimmed so it must not assert past numbers from memory, and declines to give medical diagnoses. The `agent` node prepends it on every model call instead of writing it into state, so checkpointed threads never accumulate a duplicate system message per turn. ## Persistence The graph is compiled with a checkpointer, so every `invoke` must carry a `thread_id`: ```ts await graph.invoke( { messages: [new HumanMessage("hello")] }, { configurable: { thread_id: "my-thread" } } ); ``` Invocations that share a `thread_id` accumulate message history; different threads stay isolated. `createAgentGraph()` defaults to `MemorySaver`, which keeps checkpoints in process memory only. `pnpm dev` instead builds the graph with `createSqliteCheckpointer()` from `src/agent/persistence.ts`, so conversation state survives across separate CLI runs: ```dotenv AGENT_THREAD_ID=default AGENT_SQLITE_PATH=data/agent.sqlite AGENT_HISTORY_MAX_TOKENS=8000 AGENT_RECURSION_LIMIT=40 ``` `data/` is gitignored. Set `AGENT_SQLITE_PATH=:memory:` to keep the SQLite saver but skip writing to disk. The checkpointer is deliberately created by the entrypoint rather than by `createAgentGraph`, so merely importing the graph never creates a database file. ### Run limits `agentRunConfig(threadId, recursionLimit)` builds the config every entrypoint passes to `invoke`. It sets `recursionLimit` explicitly to 40 instead of relying on LangGraph's default of 25: producing one report takes several `calculator` and `read_reports` round trips, and each round costs two graph steps, so 25 leaves little headroom. Exceeding the limit raises `GraphRecursionError`, which `pnpm validate:agent` reports as `recursion_limit_exceeded` rather than blaming the model provider. ### History trimming Without a bound, every turn resends the entire history and cost grows linearly forever. The `agent` node therefore trims what it sends using `trimMessages` with `strategy: "last"` and `startOn: "human"`, budgeted by `AGENT_HISTORY_MAX_TOKENS` (default 8000). Two details matter: - `startOn: "human"` guarantees the window never opens on a `ToolMessage` whose originating `tool_calls` were trimmed away — DeepSeek rejects an orphaned `tool_call_id`. - The system prompt sits **outside** the budget, so lowering the budget can never silently truncate the behaviour contract. Trimming only affects what the model sees. The checkpoint still stores the full history, and the durable record is the report files under `DATA_ROOT`, so a trimmed window loses nothing that matters. `pnpm dev` writes the report to stdout and its run metadata (thread, database path, message count, token usage) to stderr, so the Markdown can be piped directly to a file. ## Dependency Pinning `@langchain/langgraph-checkpoint` and `@langchain/langgraph-checkpoint-sqlite` are pinned to exact versions (`1.2.0` / `1.1.0`) rather than caret ranges. `@langchain/langgraph` resolves checkpoint `1.2.0`, and letting the saver float to a version whose peer range differs makes pnpm install **two copies** of the package — which breaks `BaseCheckpointSaver` assignability and can defeat `instanceof` checks at runtime. `@langchain/langgraph-api` (pulled in by the dev-only `langgraphjs` CLI) declares a narrower `~1.0.0` peer range. That mismatch is allowed explicitly through `pnpm.peerDependencyRules.allowedVersions` in `package.json`. `pnpm langgraph:dev` therefore deserves a manual smoke test after any dependency bump.