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 +21 -0
- package/README.md +63 -0
- package/package.json +44 -0
- package/src/index.d.ts +58 -0
- package/src/index.js +148 -0
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 };
|