{"id":21689,"date":"2026-08-15T16:27:34","date_gmt":"2026-08-15T14:27:34","guid":{"rendered":"https:\/\/www.juust.org\/?p=21689"},"modified":"2026-08-15T22:39:14","modified_gmt":"2026-08-15T20:39:14","slug":"data-based-learning-ts-agent-in-langgraph","status":"publish","type":"post","link":"https:\/\/www.juust.org\/index.php\/data-based-learning-ts-agent-in-langgraph\/2026\/08\/","title":{"rendered":"Heuristic TS Agent in LangGraph"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\">From Static Profiles to Adaptive Policies<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Most multi-<a href=\"https:\/\/www.juust.org\/index.php\/tag\/agent\/\" target=\"_blank\" rel=\"noreferrer noopener\">agent<\/a> workflows use a fixed profile: &#8220;use the balanced settings&#8221; or &#8220;always run in quality mode.&#8221; That works until you need to adapt \u2014 when a run is taking too long, when costs are spiralling, or when quality isn&#8217;t meeting the bar. The next step is a system that watches its own performance and adjusts the next run based on what it learned.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide builds a multi-agent article workflow in TypeScript that tracks every LLM call, every service request, and every cost down to the cent. It stores the results in PostgreSQL, and after each run, it derives an adaptive policy that tunes the next run&#8217;s research depth, scrape limit, and review passes based on measured performance. No machine learning, no bandit algorithms \u2014 just heuristics fed by real data.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">1. What You&#8217;ll Build<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The workflow runs five agents in sequence, the same as the basic LangGraph version:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Researcher<\/strong> \u2014 fetches Google organic SERP results via DataForSEO and scrapes top pages via Jina Reader<\/li>\n\n\n\n<li><strong>SEO strategist<\/strong> \u2014 creates a keyword brief, angle, audience, and outline from live data<\/li>\n\n\n\n<li><strong>Writer<\/strong> \u2014 produces a full article draft<\/li>\n\n\n\n<li><strong>Reviewer<\/strong> \u2014 scores the draft against quality and SEO criteria<\/li>\n\n\n\n<li><strong>Publisher<\/strong> \u2014 converts the final draft into a WordPress REST API payload<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">What&#8217;s new: every call is metered, every cost is estimated, and the entire run is saved to PostgreSQL. After the run, an adaptive policy function analyses the collected metrics and adjusts parameters for the next run \u2014 deeper research if quality was high, shallower scrapes if costs were too steep, or more review passes if the reviewer rejected the draft.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Check the repo on GitHub: <a href=\"https:\/\/github.com\/juustesout\/typescript-learning-ai-agent\" target=\"_blank\" rel=\"noopener\">https:\/\/github.com\/juustesout\/typescript-learning-ai-agent<\/a><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. Project Setup<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>git clone https:\/\/github.com\/juustesout\/typescript-learning-ai-agent tsdatabase\ncd tsdatabase\nnpm install<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Dependencies: <code>pg<\/code> for PostgreSQL, <code>typescript<\/code> and <code>@types\/node<\/code> for the compiler. The project uses no external agent framework \u2014 every model call goes through a 30-line <code>MiniAgent<\/code> class built on top of <code>fetch<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">3. Environment Variables<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>OPENAI_API_KEY=sk-...\nOPENAI_MODEL=gpt-4o-mini\nDATAFORSEO_API_KEY=base64-of-login:password\nDATAFORSEO_BASE_URL=https:\/\/api.dataforseo.com\nWP_BASE_URL=https:\/\/yourblog.com\nWP_USERNAME=admin\nWP_APPLICATION_PASSWORD=xxxx\nJINA_API_KEY=jina_...\nDESKTOP_DB_HOST=host.docker.internal\nDESKTOP_DB_PORT=5433\nDESKTOP_DB_NAME=xxxx\nDESKTOP_DB_USER=xxx\nDESKTOP_DB_PASSWORD=...<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>loadEnv()<\/code> function in <code>src\/database.ts<\/code> and <code>src\/workflow.ts<\/code> reads the <code>.env<\/code> file at module import time \u2014 no dotenv dependency needed.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">4. The Metrics Collector<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The heart of the data layer is the <code>RunMetricsCollector<\/code> class in <code>src\/metrics.ts<\/code>. It records every call the workflow makes \u2014 LLM completions, DataForSEO requests, Jina scrapes, and WordPress API calls \u2014 with timestamps, durations, token counts, and cost estimates:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>collector.recordCall({\n  kind: \"llm\",\n  provider: \"openai\",\n  model: \"gpt-4o-mini\",\n  durationMs: 2487,\n  inputTokens: 2358,\n  outputTokens: 312,\n  estimatedCostUsd: 0.0005,\n  status: \"ok\",\n  metadata: { node: \"research\" },\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The collector also tracks node-level summaries. Each stage of the workflow (research, writer, review, publish) records its total duration, call count, cost, and a quality score:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>collector.recordNode({\n  node: \"research\",\n  durationMs: 5954,\n  callCount: 5,\n  costUsd: 0.0037,\n  qualityScore: 80,\n});<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">5. Cost Estimation<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>estimateModelCostUsd()<\/code> function in <code>src\/metrics.ts<\/code> calculates LLM costs using known pricing per model:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const rateMap = {\n  \"gpt-4o-mini\": { input: 0.00000015, output: 0.0000006 },\n  \"gpt-4o\":      { input: 0.0000025,  output: 0.00001 },\n  \"gpt-4.1-mini\":{ input: 0.0000004,  output: 0.0000016 },\n};<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Service costs are calculated separately. DataForSEO costs are estimated at $0.001 per page, Jina at roughly $50 per billion tokens (effectively negligible for a single run). The <code>calculateServiceCost()<\/code> function keeps these estimates consistent across the codebase.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">6. Agent Profiles<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Four profiles define the operating constraints for a run. Each profile specifies a quality target, a maximum cost budget, a target latency, and a preferred traversal order:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const AGENT_PROFILES = {\n  budget:   { qualityTarget: 68, maxCostUsd: 0.45, targetLatencyMs: 60000 },\n  balanced: { qualityTarget: 82, maxCostUsd: 0.9,  targetLatencyMs: 45000 },\n  quality:  { qualityTarget: 92, maxCostUsd: 1.8,  targetLatencyMs: 60000 },\n  speed:    { qualityTarget: 76, maxCostUsd: 0.6,  targetLatencyMs: 25000 },\n};<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>chooseAgentProfile()<\/code> function selects the best profile given constraints. If you set <code>MAX_COST_USD=0.5<\/code> and <code>MIN_QUALITY_SCORE=75<\/code>, it will automatically pick the <code>balanced<\/code> profile. If you set <code>MAX_LATENCY_MS=25000<\/code>, it picks <code>speed<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">7. The Adaptive Policy<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">After each run, the <code>deriveAdaptivePolicy()<\/code> function in <code>src\/metrics.ts<\/code> analyses the collected metrics and adjusts parameters for the next run. It reads the average quality score, cost, and latency from the current run and derives a new policy:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>function deriveAdaptivePolicy({ profileName, nodeStats }) {\n  const averageQuality = (research.quality + writer.quality + review.quality) \/ 3;\n  const averageLatency = (research.latency + writer.latency + review.latency) \/ 3;\n\n  if (averageQuality &gt;= 82 &amp;&amp; averageLatency &lt;= 6000) {\n    return { mode: \"deep\", researchDepth: 30, scrapeLimit: 8, reviewPasses: 3 };\n  }\n  if (averageLatency &lt;= 3500) {\n    return { mode: \"fast\", researchDepth: 10, scrapeLimit: 3, reviewPasses: 1 };\n  }\n  if (averageCost &lt;= 0.08) {\n    return { mode: \"cheap\", researchDepth: 10, scrapeLimit: 3, reviewPasses: 1 };\n  }\n  return { mode: \"balanced\", researchDepth: 20, scrapeLimit: 5, reviewPasses: 2 };\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The policy is applied to the next run&#8217;s research depth, scrape limit, and maximum review passes. Over time, runs that consistently produce high-quality articles with low latency will naturally drift toward the &#8220;deep&#8221; mode, while runs with tight cost constraints will stay in &#8220;cheap&#8221; mode.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">8. PostgreSQL Schema<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>workflow_runs<\/code> table stores every run with full JSONB data:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>CREATE TABLE workflow_runs (\n  id            SERIAL PRIMARY KEY,\n  topic         TEXT NOT NULL,\n  research      JSONB,\n  draft         JSONB,\n  review        JSONB,\n  publish_payload JSONB,\n  publish_result  JSONB,\n  metrics       JSONB,\n  policy        JSONB,\n  decision_mode TEXT,\n  research_depth INTEGER,\n  scrape_limit  INTEGER,\n  review_passes INTEGER,\n  decision_reason TEXT,\n  created_at    TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>metrics<\/code> column stores the full <code>RunMetricsSummary<\/code> \u2014 every call, every node summary, every cost. The <code>policy<\/code> column stores the adaptive policy that was derived from the run. This means you can query historical runs to see how the policy evolved over time:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SELECT topic, decision_mode, research_depth, scrape_limit, created_at\nFROM workflow_runs\nORDER BY created_at DESC\nLIMIT 10;<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">9. Running the Workflow<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>npm run build &amp;&amp; node dist\/demo.js \"AI sales agents for SMBs\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The demo entrypoint prints the research, draft, review, publish result, and the full metrics summary:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"totalCalls\": 9,\n  \"totalDurationMs\": 24065,\n  \"totalInputTokens\": 7235,\n  \"totalOutputTokens\": 2439,\n  \"totalCostUsd\": 0.0057,\n  \"qualityScore\": 85,\n  \"nodes\": &#91;\n    { \"node\": \"research\", \"durationMs\": 5954, \"callCount\": 5, \"costUsd\": 0.0037, \"qualityScore\": 80 },\n    { \"node\": \"writer\",   \"durationMs\": 9298, \"callCount\": 1, \"costUsd\": 0.0010, \"qualityScore\": 82 },\n    { \"node\": \"review\",   \"durationMs\": 1290, \"callCount\": 1, \"costUsd\": 0.0002, \"qualityScore\": 90 },\n    { \"node\": \"publish\",  \"durationMs\": 6343, \"callCount\": 1, \"costUsd\": 0.0007, \"qualityScore\": 88 }\n  ]\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">10. Architecture Notes<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>No external agent framework<\/strong> \u2014 the <code>MiniAgent<\/code> class calls the OpenAI-compatible API directly via <code>fetch<\/code>. Every model call, token count, and cost is explicitly tracked.<\/li>\n\n\n\n<li><strong>Rule-based adaptation, not ML<\/strong> \u2014 the adaptive policy uses simple thresholds on measured metrics, not a learned model. This keeps the system transparent and predictable while still being data-driven.<\/li>\n\n\n\n<li><strong>Full audit trail<\/strong> \u2014 every run is persisted with its complete call log, cost breakdown, and derived policy. You can trace which decisions led to which outcomes.<\/li>\n\n\n\n<li><strong>Profile auto-selection<\/strong> \u2014 the <code>chooseAgentProfile()<\/code> function picks the best profile from cost, quality, and latency constraints, so you don&#8217;t need to manually switch between profiles.<\/li>\n\n\n\n<li><strong>Retry and fallback<\/strong> \u2014 both the DataForSEO call and Jina scraping wrap their operations in a simple retry loop, returning structured error objects instead of throwing.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">11. Next Steps: From Heuristics to Bandits<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The adaptive policy in this project is a heuristic bridge. It works well for simple cases \u2014 if quality is high, go deeper; if cost is high, stay shallow \u2014 but it can&#8217;t learn complex tradeoffs. The natural evolution is the contextual bandit system implemented in the companion project, which replaces the rule-based policy with a learned routing graph that picks the optimal path at every decision point using historical edge statistics stored in PostgreSQL.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The key insight: the metrics collector, PostgreSQL schema, and cost estimation code in this project are the foundation that the bandit builds on. The bandit reads from the same <code>edge_stats<\/code> table, uses the same cost estimates, and records the same call metrics. The only difference is the decision layer \u2014 rules vs. learned utility.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">12. Get the Code<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The full source code is on GitHub: <a href=\"https:\/\/github.com\/juustesout\/typescript-learning-ai-agent\" target=\"_blank\" rel=\"noopener\">https:\/\/github.com\/juustesout\/typescript-learning-ai-agent<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Clone it, configure your API keys and PostgreSQL connection, and run <code>npm run demo<\/code>. The workflow will research, write, review, publish, and save the full run data to your database. Query the <code>workflow_runs<\/code> table to see how the policy evolves over time.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>A data backed TypeScript Agent in LangGraph (with repo)<\/p>\n","protected":false},"author":5796,"featured_media":21691,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_sitemap_exclude":false,"_sitemap_priority":"","_sitemap_frequency":"","site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"set","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[484,543,479,305],"tags":[483],"class_list":["post-21689","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai","category-agents","category-javascript","category-programming","tag-ai"],"_links":{"self":[{"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/posts\/21689","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/users\/5796"}],"replies":[{"embeddable":true,"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/comments?post=21689"}],"version-history":[{"count":5,"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/posts\/21689\/revisions"}],"predecessor-version":[{"id":21723,"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/posts\/21689\/revisions\/21723"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/media\/21691"}],"wp:attachment":[{"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/media?parent=21689"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/categories?post=21689"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/tags?post=21689"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}