Production-style TypeScript agent runtime demonstrating skill selection, tool routing, planning/execution/reflection, approvals, resilience, persistence abstraction, and observability.
This project is a practical baseline for building an LLM agent runtime that can run in three modes:
- deterministic fake gateway for tests and local demos
- real OpenAI gateway for API-backed behavior via the official OpenAI Node SDK
- real Gemini gateway for API-backed behavior via the official Google GenAI SDK
Core flow:
SkillSelectorchooses one skill from user input.- Skill instructions are loaded from
SKILL.md. MultiSkillRouterlimits visible tools to those allowed by the chosen skill.Plannercreates structured steps (parallel-tools,single-tool,reasoning,finalize) validated by zod.StepExecutorexecutes each step with policy and resilience controls.- If a tool requires confirmation, runtime suspends with
waiting_for_approval. resume()continues afterapprove,reject, oredit-and-approve.Reflectorchecks completion andFinalizerreturns structured output.AgentTracercaptures structured run events.
Layered structure:
src/agent/*Shared runtime contracts, execution flow, persistence hooks, and final-result assembly helpers.src/flow/*Shared flow core: block pool, block matching, document model, runtime execution, graph conversion, serialization, and live design monitoring.src/flow/design/*Flow-design extension layer: intent analysis, task-graph reasoning, draft composition, sample execution, reflection, DTOs, provider boundary, deterministic mocks, and a manifest-backed resource surface.src/flow/node-config/design/*Node-configuration design core: block-family strategies, validation, DTOs, and a manifest-backed defaults/knowledge surface plus metadata-backed strategy guidance.src/prompt-lab/*Interactive prompt-improvement workflow: real LLM-backed execution, artifact capture, self-review, user feedback collection, and final Codex prompt synthesis.src/tools/*Shared tool infrastructure only: registry, repository, type contracts, and resource-backed pack loading.src/flow/agent/*,src/flow/node-config/agent/*Skill-oriented flow wrappers plus the flow-owned runtime tool implementations.src/agent/sample-tools.tsSample/demo tool pack implementation kept outsidetools/coreso shared tool infrastructure stays small.src/llm/fake-*.tsDeterministic fake planning, reflection, and final formatting helpers used by tests and demos.
Architecture sketch:
flowchart TD
A["Agent Runtime"] --> B["Skill Selection"]
B --> C["Tool Routing"]
C --> D["Planner Gateway"]
D --> E["flow design extension"]
D --> F["node-config design extension"]
E --> G["Tool wrappers"]
F --> G
G --> H["flow core / graph execution"]
H --> I["Reflection"]
I --> J["Final result formatters"]
J --> K["FinalResult: designDetails + payload"]
D -. fake mode .-> L["fake-plan-builders / fake-reflectors"]
Design boundary summary:
designDetailsinFinalResultis the shared DTO-oriented summary for UI and persistence.payloadinFinalResultis the skill-specific structured result.FakeLlmGatewayis intentionally thin and delegates planning, reflection, and formatting to helper modules.- Example/demo strings are kept in deterministic mock modules where practical so core orchestration code stays focused on design flow rather than fixtures.
Use the root barrel for most application code:
import {
AgentRuntime,
FakeLlmGateway,
FlowDesignProduct,
buildDefaultToolRegistry,
InMemoryRunStateStore,
createRuntime,
} from './src';Use layer-specific barrels when you want tighter boundaries:
import { buildFlowDesignerPayload } from './src/agent';
import { buildFlowDesignerPlan } from './src/llm';
import { designFlowDraft } from './src/flow/design';
import { NodeConfigDesignService } from './src/flow/node-config/design';
import { UnifiedRunEventBus } from './src/observability';
import { PromptLabProduct } from './src/prompt-lab';That split mirrors the current architecture:
- root barrel: convenient app-facing API
- layer barrels: clearer internal boundaries and lower accidental coupling
src/flow/*: shared flow surface that now re-exports its nested design, agent, and node-config extensions
Product-facing usage:
import { FlowDesignProduct } from './src';
const product = new FlowDesignProduct();
const preflight = await product.preflight('이메일을 확인해서 답장 해줘');
const design = await product.design('키워드를 줄테니 블로그 타이틀 여러개 만들기');Use FlowDesignProduct when the application wants explicit product operations such as:
- preflight validation
- full flow design
- node-configuration design
Use AgentRuntime directly when the application wants lower-level skill/runtime control.
Runtime factory usage:
import { createRuntime } from './src';
const runtime = await createRuntime();.
├─ package.json
├─ tsconfig.json
├─ vitest.config.ts
├─ .env.example
├─ README.md
├─ src/
│ ├─ index.ts
│ ├─ demo.ts
│ ├─ agent/
│ ├─ flow/
│ │ ├─ design/
│ │ ├─ agent/
│ │ └─ node-config/
│ │ ├─ design/
│ │ └─ agent/
│ ├─ llm/
│ ├─ prompt-lab/
│ ├─ tools/
│ ├─ policy/
│ ├─ resilience/
│ ├─ state/
│ └─ observability/
├─ data/
│ ├─ flow/
│ ├─ products/
│ ├─ runtime/
│ ├─ skills/
│ └─ tools/
└─ tests/
nvm use
npm installnpm testnpm run demonpm run labprompt-lab starts an interactive CLI that:
- chooses an execution mode: normal run or advisor-evaluation-only
- chooses provider plus
mainandlitemodels - uses cursor-driven selection menus for language, provider, skill, and recommended model choices
- chooses which flow skill to validate (
flow-preflight-validator,flow-designer, ornode-config-designer) in normal run mode - follows Korean by default, with English selectable
- runs the real agent flow and records artifacts under
output/labs/ - can evaluate advisor quality against resource-backed scenario suites in advisor-evaluation mode and summarize whether the current lite model is suitable
- shows a one-line live execution status while the agent is running
- prints a final flow summary when a design snapshot is available
- generates a self-review
- accepts operator feedback
- synthesizes a final Codex prompt for the next iteration
Useful CLI shortcuts:
# rerun the most recent session config and requirement
npm run lab -- --last
# rerun the most recent session without interactive prompts
npm run lab -- --last --auto
# rerun the last session but override only the requirement
npm run lab -- --last --auto --requirement "새 요구사항"
# fully non-interactive execution
npm run lab -- --auto --mode run --provider openai --skill flow-designer --main-model gpt-5-mini --lite-model gpt-4.1-mini --language ko --requirement "그래프 json을 md로 설명해줘"--auto only skips repetitive operator input. It still evaluates prompt-lab auto policy rules and can stop early when the run enters a configured stop condition.
Typical prompt-lab artifacts:
timeline.ndjson: merged runtime timelinedesign-events.ndjson: live flow-design eventsdiagnostics.ndjson: diagnostic logger streamdesigned-flow.md: final captured flow snapshot rendered as a readable node/edge summarydesigned-flow.yml: finalFlowDocumentartifact with blocks, nodes, node config, ports, edges, and related flow metadatadesigned-flow.reagraph.html: Reagraph-based interactive graph view for the final captured flow snapshotresult.json: normalized product resultexecution-timing.json: end-to-end run timing summary plus advisor timing contribution for normal run modeadvisor-evaluation.json: structured advisor quality report with suite/scenario pass and fallback rates, generated in advisor-evaluation modeadvisor-evaluation.md: readable advisor quality summary for the current lite model, generated in advisor-evaluation modeself-review.json: structured self-reviewcodex-prompt.md: final synthesized Codex promptprompt-lab-last-run.json: cached last-run config/requirement used by--last
Prompt-lab resources live in data/products/prompt-lab/:
PROMPT_LAB_MANIFEST.yml: CLI copy, defaults, self-review prompt, and Codex prompt synthesis promptPROMPT_LAB_AUTO_POLICY.yml: auto-run stop/warn policy used by--auto
The current prompt-lab status, known gaps, and next-step notes are tracked in docs/PROMPT_LAB_CONTEXT.md.
All npm scripts are wrapped through scripts/with-project-node.sh, which sources nvm and uses the version from .nvmrc.
The project targets Node.js 22.15.1 or newer and is intended to remain compatible with later major versions.
Demo shows:
- normal completed run
- run suspended for approval
- resumed run after approval
Default is fake gateway.
To use OpenAI:
- copy
.env.exampleto.env - set
OPENAI_API_KEY - optionally set
OPENAI_MODEL - set
USE_REAL_OPENAI=true - optionally set
OPENAI_STRUCTURED_PROXY_URLto route structured parsing through an external HTTP proxy
The CLI/product entrypoints load .env from the project root automatically when present.
Runtime selects gateway in src/index.ts.
The OpenAI gateway is implemented against the SDK responses.parse structured-output flow and expects openai@^6.27.0.
When OPENAI_STRUCTURED_PROXY_URL is set, the gateway serializes the active schema and delegates the structured parse call over HTTP.
To use Gemini:
- copy
.env.exampleto.env - set
GEMINI_API_KEYorGOOGLE_API_KEY - optionally set
GEMINI_MODEL - set
LLM_PROVIDER=geminiorUSE_REAL_GEMINI=true
The Gemini gateway is implemented against @google/genai@^1.28.0 and uses responseMimeType=application/json with responseJsonSchema.
The Gemini SDK requires Node.js 20 or newer for real API execution, and this project standardizes on Node.js 22+.
OPENAI_API_KEY: API key for real gatewayOPENAI_MODEL: model name (default:gpt-4.1-mini)OPENAI_LITE_MODEL: lite model name for lower-cost structured tasks and prompt synthesisOPENAI_STRUCTURED_PROXY_URL: optional HTTP endpoint for proxied structured parsingGEMINI_API_KEY: API key for Gemini APIGOOGLE_API_KEY: alternative Gemini API key env varGEMINI_MODEL: model name (default:gemini-2.0-flash)GEMINI_LITE_MODEL: lite model name for lower-cost structured tasks and prompt synthesisLLM_PROVIDER:fake,openai, orgeminiUSE_REAL_GEMINI:trueorfalseUSE_REAL_OPENAI:trueorfalseCODEX_RESOURCE_ROOT: optional shared resource root; defaults to./dataCODEX_RESOURCE_PROFILE: optional resource profile suffix; if set tostaging, the loader will prefer files such asFLOW_DESIGN_MANIFEST.staging.ymlwhen they exist inside the resource rootCODEX_DEBUG_LOGS=1: enables additional diagnostic logs for resource loading, task-type/task-graph selection, and key node-config decisions. Warnings and errors are still logged without this flag.
Resource loading notes:
- YAML-backed manifests are loaded through a shared async resource layer.
- Today the default source is the local filesystem.
- The resource boundary is intentionally abstracted so the same modules can later be backed by a remote config service, database, or managed manifest store without rewriting the design cores.
- The runtime now expects one shared resource root rather than per-file override paths.
- Each major resource consumer reads through an id-based resource registry and manifest surface:
flow/design:getFlowDesignManifest()flow/node-config/design:getNodeConfigDesignManifest()llm/runtime:getLlmRuntimeManifest()
tools: shared tool infrastructure that registers resource-backed tool packs throughbuildDefaultToolRegistry()
Expected resource root structure:
<CODEX_RESOURCE_ROOT>/
├─ flow/
│ ├─ BLOCK_POOL.yml
│ └─ RESOURCE.md
├─ products/
│ └─ prompt-lab/
│ ├─ PROMPT_LAB_MANIFEST.yml
│ └─ RESOURCE.md
├─ runtime/
│ └─ LLM_RUNTIME_MANIFEST.yml
├─ skills/
│ ├─ RESOURCE.md
│ ├─ flow-designer/
│ │ ├─ SKILL.md
│ │ ├─ FLOW_DESIGN_MANIFEST.yml
│ │ ├─ TOOLS.yml
│ │ └─ RESOURCE.md
│ ├─ flow-preflight-validator/
│ │ ├─ SKILL.md
│ │ ├─ TOOLS.yml
│ │ └─ RESOURCE.md
│ └─ node-config-designer/
│ ├─ SKILL.md
│ ├─ NODE_CONFIG_MANIFEST.yml
│ ├─ TOOLS.yml
│ └─ RESOURCE.md
└─ tools/
└─ sample-tools/
└─ TOOLS.yml
Agent-owned tool sets now live alongside each agent's skill/manifest resources. Sample/demo packs that are reused across tests or local flows, such as sample-tools, remain in the top-level tools/ area until a real shared/common resource surface is introduced.
Each resource-owning folder may also include a small RESOURCE.md file that explains:
- which files in that folder are owned by the skill or shared pack
- which tuning changes belong there
- which changes should stay in shared runtime/common areas
Shared flow resources now live under data/flow/. See data/flow/RESOURCE.md for the block-pool ownership and matching-policy editing guidance.
Skill-owned extension resources live under data/skills/. See data/skills/RESOURCE.md for the shared-vs-skill-owned split.
The code hierarchy mirrors that split:
src/flow/*: shared flow coresrc/flow/design/*: flow-design extensionsrc/flow/node-config/design/*: node-config extensionsrc/flow/agent/*andsrc/flow/node-config/agent/*: skill-oriented wrappers and flow-owned tool implementations on top of those coressrc/tools/core/*: reusable tool infrastructure onlysrc/agent/sample-tools.ts: sample/demo tool pack implementation
Each tool set manifest also carries a version field so pack-level migration can be introduced later without changing the runtime loading contract.
Tool sets also carry:
owner: the skill or area that owns the packscope: one ofagent-owned,sample-only, orshared
Profile-specific variants follow the same layout by inserting the profile name before the extension. Examples:
skills/flow-designer/FLOW_DESIGN_MANIFEST.staging.ymlskills/flow-designer/TOOLS.staging.ymlskills/flow-preflight-validator/TOOLS.staging.ymlskills/node-config-designer/NODE_CONFIG_MANIFEST.production.ymlskills/node-config-designer/TOOLS.production.ymlruntime/LLM_RUNTIME_MANIFEST.dev.ymltools/sample-tools/TOOLS.dev.yml
- Add Postgres implementation of
RunStateStore - Expose runtime through an HTTP API
- Attach a web UI for approvals and trace inspection
- Add distributed tracing / metrics sink
- Expand skill packs and external tool adapters
See docs/ROADMAP.md for the current TODOs grouped by layer.