chaos-agents 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shreyas Kapale
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # chaos-agents
2
+
3
+ **Chaos Monkey, but for AI agents.**
4
+
5
+ `chaos-agents` injects controlled faults — latency, errors, corrupted /
6
+ truncated / empty outputs — into your agent and tool functions, so you can find
7
+ out how your system behaves *before* the real world does it for you.
8
+
9
+ > ⚠️ Early alpha (v0.1.0). The API is small on purpose and will grow.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install chaos-agents
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```python
20
+ from chaos_agents import ChaosMonkey
21
+
22
+ # Roll the dice on every call.
23
+ monkey = ChaosMonkey(
24
+ latency=0.3, # 30% chance of an artificial delay
25
+ errors=0.1, # 10% chance of raising ChaosError
26
+ corruption=0.1, # 10% chance of scrambling the string output
27
+ truncation=0.05, # 5% chance of truncating the output
28
+ seed=42, # reproducible chaos
29
+ )
30
+
31
+ @monkey.wrap
32
+ def my_agent(prompt: str) -> str:
33
+ return call_llm(prompt)
34
+
35
+ my_agent("summarize this document")
36
+ print(monkey.report()) # {'latency': 1, 'errors': 0, ...}
37
+ ```
38
+
39
+ You can also wrap inline:
40
+
41
+ ```python
42
+ result = monkey.run(call_llm, prompt)
43
+ ```
44
+
45
+ Disable chaos in production without touching call sites:
46
+
47
+ ```python
48
+ monkey = ChaosMonkey(errors=0.1, enabled=False)
49
+ ```
50
+
51
+ ## Built-in faults
52
+
53
+ | Fault | What it simulates |
54
+ | ------------ | -------------------------------------------------- |
55
+ | `latency` | slow / hanging agents and tools |
56
+ | `errors` | crashes and tool failures (raises `ChaosError`) |
57
+ | `corruption` | hallucinated / garbled responses |
58
+ | `truncation` | token-limit cutoffs and interrupted streams |
59
+ | `empty` | silent / dropped responses |
60
+
61
+ ## License
62
+
63
+ MIT
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "chaos-agents",
3
+ "version": "0.1.0",
4
+ "description": "Chaos engineering for AI agents — Chaos Monkey, but for your agents and tools.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "types": "src/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.d.ts",
11
+ "import": "./src/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "src",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "test": "node test/smoke.test.js"
21
+ },
22
+ "keywords": [
23
+ "chaos-engineering",
24
+ "ai-agents",
25
+ "llm",
26
+ "testing",
27
+ "resilience",
28
+ "fault-injection",
29
+ "chaos-monkey"
30
+ ],
31
+ "author": "Shreyas Kapale <shreyas@lyzr.ai>",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/lyzr-ai/chaos-agents.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/lyzr-ai/chaos-agents/issues"
39
+ },
40
+ "homepage": "https://github.com/lyzr-ai/chaos-agents#readme",
41
+ "engines": {
42
+ "node": ">=16"
43
+ }
44
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,58 @@
1
+ export declare const VERSION: string;
2
+
3
+ export declare class ChaosError extends Error {
4
+ constructor(message?: string);
5
+ }
6
+
7
+ export interface ChaosMonkeyOptions {
8
+ /** Probability [0,1] of injecting an artificial delay. */
9
+ latency?: number;
10
+ /** Probability [0,1] of throwing ChaosError instead of running. */
11
+ errors?: number;
12
+ /** Probability [0,1] of scrambling a string return value. */
13
+ corruption?: number;
14
+ /** Probability [0,1] of truncating a string return value. */
15
+ truncation?: number;
16
+ /** Probability [0,1] of blanking the return value. */
17
+ empty?: number;
18
+ /** Seed for reproducible chaos. */
19
+ seed?: number;
20
+ /** Master switch; when false, calls pass through untouched. Default true. */
21
+ enabled?: boolean;
22
+ /** Delay bounds in milliseconds for the latency fault. Default [100, 2000]. */
23
+ latencyRange?: [number, number];
24
+ }
25
+
26
+ export type FaultName =
27
+ | "latency"
28
+ | "errors"
29
+ | "corruption"
30
+ | "truncation"
31
+ | "empty";
32
+
33
+ export declare class ChaosMonkey {
34
+ probabilities: Record<FaultName, number>;
35
+ enabled: boolean;
36
+ latencyRange: [number, number];
37
+ log: FaultName[];
38
+
39
+ constructor(opts?: ChaosMonkeyOptions);
40
+
41
+ /** Invoke `fn` once, applying any faults that trigger this call. */
42
+ run<T>(fn: (...args: any[]) => T | Promise<T>, ...args: any[]): Promise<T>;
43
+
44
+ /** Wrap a function so every call may have faults injected. */
45
+ wrap<F extends (...args: any[]) => any>(
46
+ fn: F
47
+ ): (...args: Parameters<F>) => Promise<Awaited<ReturnType<F>>>;
48
+
49
+ /** Count how many times each fault has fired so far. */
50
+ report(): Record<FaultName, number>;
51
+ }
52
+
53
+ declare const _default: {
54
+ ChaosMonkey: typeof ChaosMonkey;
55
+ ChaosError: typeof ChaosError;
56
+ VERSION: string;
57
+ };
58
+ export default _default;
package/src/index.js ADDED
@@ -0,0 +1,148 @@
1
+ /**
2
+ * chaos-agents: Chaos engineering for AI agents.
3
+ *
4
+ * Chaos Monkey, but for AI agents. Inject controlled faults — latency,
5
+ * errors, corrupted/truncated/empty outputs — into your agent or tool
6
+ * functions to test how resilient they really are.
7
+ *
8
+ * @example
9
+ * import { ChaosMonkey } from "chaos-agents";
10
+ *
11
+ * const monkey = new ChaosMonkey({ latency: 0.3, errors: 0.1, seed: 42 });
12
+ * const agent = monkey.wrap(async (prompt) => callLLM(prompt));
13
+ * await agent("hello"); // may be delayed, may throw, may return garbage
14
+ */
15
+
16
+ export const VERSION = "0.1.0";
17
+
18
+ /** Error thrown by the `errors` fault to simulate an agent/tool failure. */
19
+ export class ChaosError extends Error {
20
+ constructor(message = "chaos-agents: injected failure") {
21
+ super(message);
22
+ this.name = "ChaosError";
23
+ }
24
+ }
25
+
26
+ /** Small seedable PRNG (mulberry32) so chaos can be made reproducible. */
27
+ function makeRng(seed) {
28
+ if (seed === undefined || seed === null) {
29
+ return Math.random;
30
+ }
31
+ let a = seed >>> 0;
32
+ return function () {
33
+ a |= 0;
34
+ a = (a + 0x6d2b79f5) | 0;
35
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
36
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
37
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
38
+ };
39
+ }
40
+
41
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
42
+
43
+ /** Output-mangling faults, applied to the resolved return value. */
44
+ const OUTPUT_FAULTS = {
45
+ corruption(rng, value) {
46
+ if (typeof value !== "string" || value.length === 0) return value;
47
+ const chars = value.split("");
48
+ const swaps = Math.max(1, Math.floor(chars.length / 10));
49
+ for (let n = 0; n < swaps; n++) {
50
+ const i = Math.floor(rng() * chars.length);
51
+ const j = Math.floor(rng() * chars.length);
52
+ [chars[i], chars[j]] = [chars[j], chars[i]];
53
+ }
54
+ return chars.join("");
55
+ },
56
+ truncation(rng, value) {
57
+ if (typeof value !== "string" || value.length === 0) return value;
58
+ return value.slice(0, Math.floor(rng() * (value.length + 1)));
59
+ },
60
+ empty(rng, value) {
61
+ if (typeof value === "string") return "";
62
+ if (Array.isArray(value)) return [];
63
+ if (value && typeof value === "object") return {};
64
+ return null;
65
+ },
66
+ };
67
+
68
+ /**
69
+ * Wrap agent/tool callables and randomly inject faults. Each fault has an
70
+ * independent probability in [0, 1]; on every call the monkey rolls per fault
71
+ * and applies the ones that trigger. The wrapped function is always async.
72
+ */
73
+ export class ChaosMonkey {
74
+ /**
75
+ * @param {object} [opts]
76
+ * @param {number} [opts.latency=0] probability of an artificial delay
77
+ * @param {number} [opts.errors=0] probability of throwing ChaosError
78
+ * @param {number} [opts.corruption=0] probability of scrambling a string result
79
+ * @param {number} [opts.truncation=0] probability of truncating a string result
80
+ * @param {number} [opts.empty=0] probability of blanking the result
81
+ * @param {number} [opts.seed] seed for reproducible chaos
82
+ * @param {boolean}[opts.enabled=true] master switch
83
+ * @param {[number,number]} [opts.latencyRange=[100,2000]] delay bounds in ms
84
+ */
85
+ constructor(opts = {}) {
86
+ this.probabilities = {
87
+ latency: opts.latency ?? 0,
88
+ errors: opts.errors ?? 0,
89
+ corruption: opts.corruption ?? 0,
90
+ truncation: opts.truncation ?? 0,
91
+ empty: opts.empty ?? 0,
92
+ };
93
+ this.enabled = opts.enabled ?? true;
94
+ this.latencyRange = opts.latencyRange ?? [100, 2000];
95
+ this._rng = makeRng(opts.seed);
96
+ this.log = [];
97
+ }
98
+
99
+ _fires(name) {
100
+ return this._rng() < (this.probabilities[name] ?? 0);
101
+ }
102
+
103
+ /** Invoke `fn` once, applying any faults that trigger this call. */
104
+ async run(fn, ...args) {
105
+ if (!this.enabled) return fn(...args);
106
+
107
+ if (this._fires("latency")) {
108
+ this.log.push("latency");
109
+ const [lo, hi] = this.latencyRange;
110
+ await sleep(lo + this._rng() * (hi - lo));
111
+ }
112
+
113
+ if (this._fires("errors")) {
114
+ this.log.push("errors");
115
+ throw new ChaosError();
116
+ }
117
+
118
+ const result = await fn(...args);
119
+
120
+ for (const name of ["corruption", "truncation", "empty"]) {
121
+ if (this._fires(name)) {
122
+ this.log.push(name);
123
+ return OUTPUT_FAULTS[name](this._rng, result);
124
+ }
125
+ }
126
+ return result;
127
+ }
128
+
129
+ /** Decorator/wrapper form of {@link run}. Returns an async function. */
130
+ wrap(fn) {
131
+ const self = this;
132
+ const wrapped = function (...args) {
133
+ return self.run(fn, ...args);
134
+ };
135
+ wrapped.__chaos__ = this;
136
+ return wrapped;
137
+ }
138
+
139
+ /** Count how many times each fault has fired so far. */
140
+ report() {
141
+ const counts = {};
142
+ for (const name of Object.keys(this.probabilities)) counts[name] = 0;
143
+ for (const name of this.log) counts[name] += 1;
144
+ return counts;
145
+ }
146
+ }
147
+
148
+ export default { ChaosMonkey, ChaosError, VERSION };