[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-81476":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":12,"openIssues":14,"contributorsCount":15,"subscribersCount":15,"size":15,"stars1d":15,"stars7d":15,"stars30d":15,"stars90d":15,"forks30d":15,"starsTrendScore":15,"compositeScore":16,"rankGlobal":10,"rankLanguage":10,"license":17,"archived":18,"fork":18,"defaultBranch":19,"hasWiki":18,"hasPages":18,"topics":20,"createdAt":10,"pushedAt":10,"updatedAt":21,"readmeContent":22,"aiSummary":23,"trendingCount":15,"starSnapshotCount":15,"syncStatus":24,"lastSyncTime":25,"discoverSource":26},81476,"circuit-breaker","MonetiseBG\u002Fcircuit-breaker","MonetiseBG","Runtime economic governance for AI agents. Circuit Breaker adds risk-managed execution, cost ceilings, and kill switches to autonomous AI workflows.","https:\u002F\u002Fcircuitbreaker.dev",null,"TypeScript",30,3,1,0,41.81,"Apache License 2.0",false,"main",[],"2026-06-12 04:01:34","# @monetisebg\u002Fcircuit-breaker\n\n[![CI](https:\u002F\u002Fgithub.com\u002FMonetiseBG\u002Fcircuit-breaker\u002Factions\u002Fworkflows\u002Fci.yml\u002Fbadge.svg)](https:\u002F\u002Fgithub.com\u002FMonetiseBG\u002Fcircuit-breaker\u002Factions\u002Fworkflows\u002Fci.yml)\n[![npm version](https:\u002F\u002Fimg.shields.io\u002Fnpm\u002Fv\u002F@monetisebg\u002Fcircuit-breaker.svg)](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002F@monetisebg\u002Fcircuit-breaker)\n\n> One wrapper between you and runaway execution.\n\nMinimal **circuit breaker** for AI agents. Wrap any supported agent and pick a\nmode — the breaker cuts the run short once provider-reported usage crosses a\nlimit, and (optionally) refuses an oversized prompt before it is even sent.\n\n- Zero-config: defaults work out of the box.\n- Two modes, pick one: **`budget-guard`** (token caps) and **`loop-killer`**\n  (state-repeat detection).\n- Post-hoc enforcement by default: token tripping happens **after** each call\n  or turn boundary, so the call that crosses the limit still counts. Use the\n  optional `estimateInputTokens` preflight (see below) to reject oversized\n  initial inputs before any provider work happens.\n- Visible: emits `CircuitBreakerEvent`s as the run progresses.\n- Typed: throws a `CircuitBreakerError`, or routes through your `onTrip` handler.\n- Optional peer dependencies — only install the framework you actually use.\n- No bundled tokenizer: bring your own (`js-tiktoken`, `tiktoken`, provider SDK).\n\nShipped adapters: **LangChain.js**, **OpenAI Agents SDK**, **Claude Agent\nSDK**, **Vercel AI SDK**, **LangGraph Platform SDK**. The core is\nframework-agnostic; rolling your own adapter is a few lines.\n\n\n[Watch the 1-minute overview](https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=nhRmZBkjeFU) — see how Circuit Breaker stops a runaway agent in real time.\n  \n[![Circuit Breaker — 1-min explainer](https:\u002F\u002Fimg.youtube.com\u002Fvi\u002FnhRmZBkjeFU\u002F1.jpg)](https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=nhRmZBkjeFU)\n\n\n## Install\n\nRequires **Node ≥ 22** (the breaker uses `node:crypto`).\n\n```bash\nnpm install @monetisebg\u002Fcircuit-breaker\n# plus the framework you use (minimum versions enforced via peerDependencies):\nnpm install @langchain\u002Fcore@^1.1.47              # for the LangChain adapter\nnpm install @openai\u002Fagents@^0.11.0               # for the OpenAI Agents adapter\nnpm install @anthropic-ai\u002Fclaude-agent-sdk@^0.2  # for the Claude Agent SDK adapter\nnpm install ai@^5                                 # for the Vercel AI SDK adapter\nnpm install @langchain\u002Flanggraph-sdk@^1           # for the LangGraph Platform SDK adapter\n```\n\n## Quick start (`budget-guard`, the default)\n\n```ts\nimport { withCircuitBreaker } from \"@monetisebg\u002Fcircuit-breaker\u002Fopenai-agents\";\n\nconst safeAgent = withCircuitBreaker(agent); \u002F\u002F defaults: 10k input + 10k output\n\nawait safeAgent.run(\"Analyze this dataset\");\n```\n\n`budget-guard` caps input and output tokens **independently**. Default limits:\n`maxInputToken = 10_000`, `maxOutputToken = 10_000`. Token usage is read from\neach provider response, so the breaker trips on the **next** call\u002Fturn after\neither bucket is exceeded — the call that pushed the bucket over the limit\nstill counts. To reject an oversized first prompt before it is sent, pass an\noptional `estimateInputTokens` preflight (next section).\n\n```ts\nwithCircuitBreaker(agent, {\n  mode: \"budget-guard\",     \u002F\u002F optional — this is the default\n  maxInputToken: 50_000,\n  maxOutputToken: 20_000,\n});\n```\n\n### Preflight — `estimateInputTokens`\n\n```ts\nimport { encoding_for_model } from \"js-tiktoken\";\nconst enc = encoding_for_model(\"gpt-4o\");\n\nwithCircuitBreaker(agent, {\n  maxInputToken: 50_000,\n  \u002F\u002F input is the wrapper's call argument (typed per adapter)\n  estimateInputTokens: (input) =>\n    typeof input === \"string\" ? enc.encode(input).length : undefined,\n});\n```\n\nIf the estimate exceeds `maxInputToken` the wrapper throws\n`CircuitBreakerError` with `reason: \"max_input_tokens\"` **before** the\nunderlying runnable \u002F runner \u002F query is called. Return `undefined` to skip\nthe check for that invocation (e.g. when you can't tokenize the input shape).\nThis is opt-in — without an estimator the wrapper behaves as before. No\ntokenizer is bundled.\n\n## `loop-killer` mode\n\n```ts\nwithCircuitBreaker(agent, {\n  mode: \"loop-killer\",\n  maxRetries: 3,            \u002F\u002F default\n  detectRepeatedState: true,\u002F\u002F default — hashes each step's state\n});\n```\n\nWith `detectRepeatedState: true` (default), the breaker hashes each step's\nstate (the latest message \u002F turn input) and trips when any single state\nrecurs more than `maxRetries` times. Set `detectRepeatedState: false` to fall\nback to a plain iteration cap.\n\n## Visibility — `onEvent`\n\nThe breaker emits events you can log, surface in your UI, or pipe to your\nobservability stack.\n\n```ts\nwithCircuitBreaker(agent, {\n  mode: \"loop-killer\",\n  maxRetries: 2,\n  onEvent(event) {\n    \u002F\u002F event: CircuitBreakerEvent\n    console.log(event);\n  },\n});\n```\n\n`CircuitBreakerEvent` shapes:\n\n| Event                                                 | When                                              | Modes        |\n| ----------------------------------------------------- | ------------------------------------------------- | ------------ |\n| `{ type: \"retry\"; retries: number }`                  | A state recurred (`detectRepeatedState: true`) or each iteration past the first (`detectRepeatedState: false`) | loop-killer  |\n| `{ type: \"stop\"; reason: StopReason; saved: number }` | The breaker tripped                               | both         |\n\n`saved` is signed `limit - usage`: positive means headroom that won't be\nspent, negative means the call that pushed us over the limit still counted.\n\n`StopReason` is one of `\"max_input_tokens\" | \"max_output_tokens\" |\n\"max_retries\" | \"repeated_state\"`.\n\n## Graceful handling — `onTrip`\n\nProvide `onTrip` to suppress the throw and return a fallback value:\n\n```ts\nconst safe = withCircuitBreaker(agent, {\n  maxInputToken: 50_000,\n  maxOutputToken: 20_000,\n  onTrip: (ctx) => ({\n    output: \"Sorry, I had to stop early.\",\n    reason: ctx.reason,\n    metrics: ctx.metrics,\n  }),\n});\n```\n\n`onTrip` receives a `TripContext`:\n\n```ts\ninterface TripContext {\n  reason: StopReason;\n  mode: Mode;                              \u002F\u002F \"budget-guard\" | \"loop-killer\"\n  metrics: { iterations: number; retries: number; tokens: {...} };\n  limits: ResolvedLimits;                  \u002F\u002F the limits actually in force\n  saved: number;\n  message: string;\n}\n```\n\n## LangChain.js\n\n```ts\nimport { ChatOpenAI } from \"@langchain\u002Fopenai\";\nimport { AgentExecutor, createOpenAIFunctionsAgent } from \"langchain\u002Fagents\";\nimport { withCircuitBreaker } from \"@monetisebg\u002Fcircuit-breaker\u002Flangchain\";\n\nconst agent = await createOpenAIFunctionsAgent({ llm, tools, prompt });\nconst executor = new AgentExecutor({ agent, tools });\n\nconst safeExecutor = withCircuitBreaker(executor, {\n  maxInputToken: 50_000,\n  maxOutputToken: 20_000,\n});\n\nawait safeExecutor.invoke({ input: \"...\" });\n```\n\nIterations are counted on `handleLLMStart` \u002F `handleChatModelStart`. Token\nusage is read from `handleLLMEnd` with provider-agnostic extraction\n(OpenAI `tokenUsage`, Anthropic `usage`, newer `usage_metadata`).\n\n## OpenAI Agents SDK\n\n```ts\nimport { Agent } from \"@openai\u002Fagents\";\nimport { withCircuitBreaker } from \"@monetisebg\u002Fcircuit-breaker\u002Fopenai-agents\";\n\nconst agent = new Agent({ name: \"Assistant\", instructions: \"...\", tools });\n\nconst safeAgent = withCircuitBreaker(agent, {\n  mode: \"loop-killer\",\n  maxRetries: 3,\n});\n\nawait safeAgent.run(\"Hello\");\n```\n\nIterations are counted on each `agent_start` event (one per turn); the most\nrecent `turnInput` item is hashed for loop detection. Tokens are read live\nfrom `RunContext.usage` on each turn boundary. When a limit is hit the\nwrapper aborts the in-flight run via `AbortSignal`; any caller-supplied\n`signal` is chained, so external cancellation still works.\n\n> Streaming (`stream: true`) is not yet supported. Open an issue if you need it.\n\n## Claude Agent SDK\n\n```ts\nimport { query } from \"@anthropic-ai\u002Fclaude-agent-sdk\";\nimport { withCircuitBreaker } from \"@monetisebg\u002Fcircuit-breaker\u002Fclaude-agent-sdk\";\n\nconst safeQuery = withCircuitBreaker(query, {\n  maxInputToken: 50_000,\n  maxOutputToken: 20_000,\n});\n\nfor await (const message of safeQuery({ prompt: \"Analyze this repo\" })) {\n  \u002F\u002F messages stream through untouched\n}\n```\n\nThe wrapper takes the SDK's `query` function and returns a drop-in\nreplacement with the same call signature. It's itself an async generator —\n`SDKMessage`s stream through unchanged while the breaker watches them.\n\nIterations are counted on each `assistant` message (one per turn); its\ncontent blocks are hashed for `loop-killer` detection. Tokens are read from\neach assistant message's `usage` (input counts `input_tokens` plus cache\nread\u002Fcreation tokens). When a limit is hit the wrapper aborts the in-flight\nquery via the SDK's `abortController` option; any `abortController` you pass\nin `options` is chained, so external cancellation still works.\n\nWith `onTrip`, the callback's return value is yielded as the generator's\nfinal item instead of throwing.\n\n## Vercel AI SDK\n\nFor the [AI SDK](https:\u002F\u002Fai-sdk.dev)'s `generateText` and its internal\ntool-loop. Wrap the imported `generateText` and call the result exactly as you\nwould call `generateText` itself.\n\n```ts\nimport { generateText, stepCountIs } from \"ai\";\nimport { openai } from \"@ai-sdk\u002Fopenai\";\nimport { withCircuitBreaker } from \"@monetisebg\u002Fcircuit-breaker\u002Fvercel-ai-sdk\";\n\nconst guarded = withCircuitBreaker(generateText, {\n  maxInputToken: 50_000,\n  maxOutputToken: 20_000,\n});\n\nconst result = await guarded({\n  model: openai(\"gpt-4o\"),\n  prompt: \"Analyze this repo\",\n  tools: { \u002F* … *\u002F },\n  stopWhen: stepCountIs(20),\n});\n```\n\nThe wrapper takes `generateText` and returns a function with the same options\nand result type. Iterations are counted on each finished step (one per LLM\ncall) via an injected `onStepFinish`; tokens are read from each step's `usage`\nas per-call deltas. For `loop-killer`, the step's tool calls (or its text, as a\nfallback) are hashed — a stuck agent re-issues the same tool call each step.\n\nOn a trip an internal `AbortSignal` cancels the loop before the next LLM call;\nany `abortSignal` you pass is chained, and a caller-supplied `onStepFinish`\nstill fires for every step. If the trip lands on the final step (nothing left\nto abort), it is surfaced after `generateText` returns. Your `stopWhen`,\n`tools`, `prepareStep`, and other options pass through untouched.\n\nWith `onTrip`, the callback's return value becomes the result instead of\nthrowing. Streaming (`streamText`) is not yet supported — use the core\n`CircuitBreaker` directly if you need it.\n\n## LangGraph Platform SDK\n\nFor graphs deployed to **LangGraph Platform** and driven through the remote\n`@langchain\u002Flanggraph-sdk` client. (For an in-process `@langchain\u002Flanggraph`\ngraph, use the [LangChain adapter](#langchainjs) — a compiled graph is a\n`Runnable` and propagates callbacks.)\n\n```ts\nimport { Client } from \"@langchain\u002Flanggraph-sdk\";\nimport { withCircuitBreaker } from \"@monetisebg\u002Fcircuit-breaker\u002Flanggraph-sdk\";\n\nconst client = new Client({ apiUrl: \"http:\u002F\u002Flocalhost:2024\" });\nconst runs = withCircuitBreaker(client.runs, {\n  maxInputToken: 50_000,\n  maxOutputToken: 20_000,\n});\n\nfor await (const chunk of runs.stream(threadId, \"agent\", {\n  input: { messages: [{ role: \"user\", content: \"Analyze this repo\" }] },\n  streamMode: \"updates\",\n})) {\n  \u002F\u002F chunks stream through untouched\n}\n```\n\nThe wrapper takes `client.runs` and returns an object with the same\n`stream(threadId, assistantId, payload)` signature.\n\nBecause the graph executes server-side, the breaker is driven off the\n`events` stream mode — the only mode that reports both per-LLM-call\nboundaries and token usage. The wrapper **forces `events` into the run's\n`streamMode`**; if you didn't request it, those injected chunks are consumed\ninternally and never yielded, so your stream is unchanged. Iterations are\ncounted on each `on_chat_model_start`; tokens are read from each\n`on_chat_model_end`'s `usage_metadata`. For `loop-killer`, the latest input\nmessage is hashed.\n\nOn a trip the wrapper aborts the local stream **and** calls\n`client.runs.cancel(...)` to stop the run server-side (the run id is taken\nfrom the `metadata` event) — closing the SSE connection alone would leave the\ngraph running. Any `signal` you pass in the payload is chained, so external\naborts still work.\n\nWith `onTrip`, the callback's return value is yielded as the generator's\nfinal item instead of throwing.\n\n## Trip output\n\nWhen a limit is reached the wrapper logs and throws:\n\n```\n[circuit-breaker] Agent stopped: input token budget exceeded (10_120\u002F10_000; iterations: 8).\n```\n\nPass `silent: true` to suppress the log, or `logger: (msg, ctx) => …` to send\nit elsewhere.\n\n## Options reference\n\n| Field                  | Mode         | Type        | Default    | Description                                                                  |\n| ---------------------- | ------------ | ----------- | ---------- | ---------------------------------------------------------------------------- |\n| `mode`                 | both         | `Mode`      | `\"budget-guard\"` | `\"budget-guard\"` or `\"loop-killer\"`.                                  |\n| `maxInputToken`        | budget-guard | `int ≥ 1`   | `10_000`   | Max aggregate input tokens before trip (post-hoc).                            |\n| `maxOutputToken`       | budget-guard | `int ≥ 1`   | `10_000`   | Max aggregate output tokens before trip (post-hoc).                           |\n| `estimateInputTokens`  | budget-guard | `(input) => number \\| undefined` | — | Preflight estimator; trips before the call when the estimate exceeds `maxInputToken`. |\n| `maxRetries`           | loop-killer  | `int ≥ 1`   | `3`        | Max times the same state may recur (or, with detection off, raw iterations). |\n| `detectRepeatedState`  | loop-killer  | `boolean`   | `true`     | Hash each step's state for loop detection.                                   |\n| `onEvent`              | both         | `EventListener` | —      | Receives `CircuitBreakerEvent` updates.                                      |\n| `onTrip`               | wrappers     | `OnTrip\u003CR>` | —          | Suppress the throw and use the callback's return value instead.              |\n\nAll numeric options are validated at construction; passing `0`, a negative,\n`NaN`, `Infinity`, or a non-integer throws a `TypeError`.\n\n## Contributing\n\n#### 🤝 Our Philosophy & How You Can Help\n\nWe built Circuit Breaker to solve the immediate, visceral pain of runaway agent costs and infinite loops. However, we know that every execution environment is unique, and **we do not have all the answers.** \n\nRight now, we are intentionally keeping the API minimal with core modes like `budget-guard` and `loop-killer`. We believe that the best systems are discovered through real user friction, not designed in a vacuum. Because of this, our roadmap is entirely driven by how you use — or fight — this tool in the wild.\n\n**We actively want to hear from you, especially if:**\n* **It *almost* fits:** Our default modes are 80% right for you, but you need one specific tweak or condition to make it perfect.\n* **You are building workarounds:** You find yourself writing custom scripts or wrapping our API to force it to do what you need.\n* **You have diverging use cases:** Your industry requires vastly different behavior (e.g., ultra-strict trading apps vs. loose research agents) and our defaults are breaking.\n\nWhen you stop asking *\"what does this do?\"* and start asking *\"can I change how it works?\"*, that is our signal to unlock more programmable control for the community. \n\nPlease open an issue, share your GitHub gists, or reach out to us directly. Your edge cases are our roadmap! \n\nSee [`AGENTS.md`](.\u002FAGENTS.md) for the project layout, build\u002Ftest commands, and the recipe for adding a new framework adapter.\n\n\n## License\n\nApache-2.0 — © 2026 MonetiseBG\n","Circuit Breaker 是一个为AI代理提供运行时经济治理的工具，通过添加执行边界、成本上限和终止开关来控制自主AI工作流程。其核心功能包括两种模式：“预算守护”（限制输入和输出令牌数）和“循环杀手”（检测状态重复），以防止资源过度消耗或无限循环。项目采用TypeScript编写，具有零配置启动、可见性事件发射以及类型化的错误处理等技术特点。适用于需要对AI代理的执行成本进行精细控制的场景，如基于LangChain.js、OpenAI Agents SDK等框架构建的应用程序。通过简单的集成，开发者可以确保AI任务在预设的成本范围内安全运行，避免不必要的费用支出。",2,"2026-06-11 04:05:13","CREATED_QUERY"]