aoaoe 1.3.0 → 2.5.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/dist/ab-reasoning.d.ts +42 -0
- package/dist/ab-reasoning.js +91 -0
- package/dist/alert-composer.d.ts +32 -0
- package/dist/alert-composer.js +51 -0
- package/dist/alert-rule-dsl.d.ts +32 -0
- package/dist/alert-rule-dsl.js +87 -0
- package/dist/alert-rules.d.ts +42 -0
- package/dist/alert-rules.js +94 -0
- package/dist/fleet-federation.d.ts +34 -0
- package/dist/fleet-federation.js +55 -0
- package/dist/fleet-grep.d.ts +22 -0
- package/dist/fleet-grep.js +70 -0
- package/dist/health-forecast.d.ts +23 -0
- package/dist/health-forecast.js +101 -0
- package/dist/index.js +271 -1
- package/dist/input.d.ts +42 -0
- package/dist/input.js +130 -0
- package/dist/metrics-export.d.ts +29 -0
- package/dist/metrics-export.js +58 -0
- package/dist/multi-reasoner.d.ts +36 -0
- package/dist/multi-reasoner.js +87 -0
- package/dist/output-archival.d.ts +23 -0
- package/dist/output-archival.js +72 -0
- package/dist/runbook-executor.d.ts +37 -0
- package/dist/runbook-executor.js +78 -0
- package/dist/runbook-generator.d.ts +21 -0
- package/dist/runbook-generator.js +104 -0
- package/dist/session-checkpoint.d.ts +55 -0
- package/dist/session-checkpoint.js +69 -0
- package/dist/session-tail.d.ts +24 -0
- package/dist/session-tail.js +52 -0
- package/dist/token-quota.d.ts +45 -0
- package/dist/token-quota.js +76 -0
- package/dist/workflow-chain.d.ts +33 -0
- package/dist/workflow-chain.js +69 -0
- package/dist/workflow-cost-forecast.d.ts +22 -0
- package/dist/workflow-cost-forecast.js +55 -0
- package/dist/workflow-templates.d.ts +25 -0
- package/dist/workflow-templates.js +92 -0
- package/dist/workflow-viz.d.ts +15 -0
- package/dist/workflow-viz.js +74 -0
- package/package.json +1 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ReasonerResult, Action } from "./types.js";
|
|
2
|
+
export interface ABTrialResult {
|
|
3
|
+
timestamp: number;
|
|
4
|
+
backendA: string;
|
|
5
|
+
backendB: string;
|
|
6
|
+
actionsA: Action[];
|
|
7
|
+
actionsB: Action[];
|
|
8
|
+
confidenceA?: string;
|
|
9
|
+
confidenceB?: string;
|
|
10
|
+
winner: "a" | "b" | "tie";
|
|
11
|
+
reason: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ABStats {
|
|
14
|
+
totalTrials: number;
|
|
15
|
+
winsA: number;
|
|
16
|
+
winsB: number;
|
|
17
|
+
ties: number;
|
|
18
|
+
backendA: string;
|
|
19
|
+
backendB: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Compare two reasoner results and determine which is better.
|
|
23
|
+
* Heuristic: more specific actions > wait, higher confidence > lower,
|
|
24
|
+
* fewer redundant actions = better.
|
|
25
|
+
*/
|
|
26
|
+
export declare function compareResults(resultA: ReasonerResult, resultB: ReasonerResult, backendA: string, backendB: string, now?: number): ABTrialResult;
|
|
27
|
+
/**
|
|
28
|
+
* Track A/B trial results over time.
|
|
29
|
+
*/
|
|
30
|
+
export declare class ABReasoningTracker {
|
|
31
|
+
private trials;
|
|
32
|
+
private backendA;
|
|
33
|
+
private backendB;
|
|
34
|
+
constructor(backendA: string, backendB: string);
|
|
35
|
+
/** Record a trial result. */
|
|
36
|
+
recordTrial(result: ABTrialResult): void;
|
|
37
|
+
/** Get aggregate stats. */
|
|
38
|
+
getStats(): ABStats;
|
|
39
|
+
/** Format stats for TUI display. */
|
|
40
|
+
formatStats(): string[];
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=ab-reasoning.d.ts.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// ab-reasoning.ts — run two reasoner backends on the same observation,
|
|
2
|
+
// compare their outputs, and track which performs better over time.
|
|
3
|
+
/**
|
|
4
|
+
* Compare two reasoner results and determine which is better.
|
|
5
|
+
* Heuristic: more specific actions > wait, higher confidence > lower,
|
|
6
|
+
* fewer redundant actions = better.
|
|
7
|
+
*/
|
|
8
|
+
export function compareResults(resultA, resultB, backendA, backendB, now = Date.now()) {
|
|
9
|
+
let scoreA = 0;
|
|
10
|
+
let scoreB = 0;
|
|
11
|
+
// prefer non-wait actions over wait
|
|
12
|
+
const nonWaitA = resultA.actions.filter((a) => a.action !== "wait").length;
|
|
13
|
+
const nonWaitB = resultB.actions.filter((a) => a.action !== "wait").length;
|
|
14
|
+
if (nonWaitA > nonWaitB)
|
|
15
|
+
scoreA += 2;
|
|
16
|
+
else if (nonWaitB > nonWaitA)
|
|
17
|
+
scoreB += 2;
|
|
18
|
+
// prefer higher confidence
|
|
19
|
+
const confOrder = { high: 3, medium: 2, low: 1 };
|
|
20
|
+
const confA = confOrder[resultA.confidence ?? "medium"] ?? 2;
|
|
21
|
+
const confB = confOrder[resultB.confidence ?? "medium"] ?? 2;
|
|
22
|
+
if (confA > confB)
|
|
23
|
+
scoreA += 1;
|
|
24
|
+
else if (confB > confA)
|
|
25
|
+
scoreB += 1;
|
|
26
|
+
// prefer fewer total actions (more focused)
|
|
27
|
+
if (resultA.actions.length > 0 && resultB.actions.length > 0) {
|
|
28
|
+
if (resultA.actions.length < resultB.actions.length)
|
|
29
|
+
scoreA += 1;
|
|
30
|
+
else if (resultB.actions.length < resultA.actions.length)
|
|
31
|
+
scoreB += 1;
|
|
32
|
+
}
|
|
33
|
+
const winner = scoreA > scoreB ? "a" : scoreB > scoreA ? "b" : "tie";
|
|
34
|
+
const reason = `A(${nonWaitA} actions, ${resultA.confidence ?? "?"}) vs B(${nonWaitB} actions, ${resultB.confidence ?? "?"})`;
|
|
35
|
+
return {
|
|
36
|
+
timestamp: now,
|
|
37
|
+
backendA,
|
|
38
|
+
backendB,
|
|
39
|
+
actionsA: resultA.actions,
|
|
40
|
+
actionsB: resultB.actions,
|
|
41
|
+
confidenceA: resultA.confidence,
|
|
42
|
+
confidenceB: resultB.confidence,
|
|
43
|
+
winner,
|
|
44
|
+
reason,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Track A/B trial results over time.
|
|
49
|
+
*/
|
|
50
|
+
export class ABReasoningTracker {
|
|
51
|
+
trials = [];
|
|
52
|
+
backendA;
|
|
53
|
+
backendB;
|
|
54
|
+
constructor(backendA, backendB) {
|
|
55
|
+
this.backendA = backendA;
|
|
56
|
+
this.backendB = backendB;
|
|
57
|
+
}
|
|
58
|
+
/** Record a trial result. */
|
|
59
|
+
recordTrial(result) {
|
|
60
|
+
this.trials.push(result);
|
|
61
|
+
}
|
|
62
|
+
/** Get aggregate stats. */
|
|
63
|
+
getStats() {
|
|
64
|
+
return {
|
|
65
|
+
totalTrials: this.trials.length,
|
|
66
|
+
winsA: this.trials.filter((t) => t.winner === "a").length,
|
|
67
|
+
winsB: this.trials.filter((t) => t.winner === "b").length,
|
|
68
|
+
ties: this.trials.filter((t) => t.winner === "tie").length,
|
|
69
|
+
backendA: this.backendA,
|
|
70
|
+
backendB: this.backendB,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** Format stats for TUI display. */
|
|
74
|
+
formatStats() {
|
|
75
|
+
const s = this.getStats();
|
|
76
|
+
if (s.totalTrials === 0)
|
|
77
|
+
return [" (no A/B trials recorded yet)"];
|
|
78
|
+
const pctA = s.totalTrials > 0 ? Math.round((s.winsA / s.totalTrials) * 100) : 0;
|
|
79
|
+
const pctB = s.totalTrials > 0 ? Math.round((s.winsB / s.totalTrials) * 100) : 0;
|
|
80
|
+
return [
|
|
81
|
+
` A/B Reasoning: ${s.totalTrials} trials`,
|
|
82
|
+
` ${s.backendA}: ${s.winsA} wins (${pctA}%)`,
|
|
83
|
+
` ${s.backendB}: ${s.winsB} wins (${pctB}%)`,
|
|
84
|
+
` Ties: ${s.ties}`,
|
|
85
|
+
s.winsA > s.winsB ? ` → ${s.backendA} is performing better` :
|
|
86
|
+
s.winsB > s.winsA ? ` → ${s.backendB} is performing better` :
|
|
87
|
+
` → Both backends performing equally`,
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=ab-reasoning.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AlertContext } from "./alert-rules.js";
|
|
2
|
+
export type ComposedCondition = (ctx: AlertContext) => boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Compose multiple condition strings with AND logic.
|
|
5
|
+
* All conditions must be true for the composed condition to fire.
|
|
6
|
+
*/
|
|
7
|
+
export declare function composeAnd(expressions: string[]): ComposedCondition | null;
|
|
8
|
+
/**
|
|
9
|
+
* Compose multiple condition strings with OR logic.
|
|
10
|
+
* Any condition being true fires the composed condition.
|
|
11
|
+
*/
|
|
12
|
+
export declare function composeOr(expressions: string[]): ComposedCondition | null;
|
|
13
|
+
/**
|
|
14
|
+
* Parse a composed condition from config.
|
|
15
|
+
* Format: { "and": ["fleetHealth < 40", "errorSessions > 2"] }
|
|
16
|
+
* or { "or": ["fleetHealth < 20", "stuckSessions >= 3"] }
|
|
17
|
+
* or plain string "fleetHealth < 50"
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseComposedCondition(spec: string | {
|
|
20
|
+
and: string[];
|
|
21
|
+
} | {
|
|
22
|
+
or: string[];
|
|
23
|
+
}): ComposedCondition | null;
|
|
24
|
+
/**
|
|
25
|
+
* Format a composed condition for display.
|
|
26
|
+
*/
|
|
27
|
+
export declare function formatComposedCondition(spec: string | {
|
|
28
|
+
and: string[];
|
|
29
|
+
} | {
|
|
30
|
+
or: string[];
|
|
31
|
+
}): string;
|
|
32
|
+
//# sourceMappingURL=alert-composer.d.ts.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// alert-composer.ts — AND/OR composition of alert conditions.
|
|
2
|
+
// extends alert-rule-dsl.ts with logical operators for complex rules.
|
|
3
|
+
import { parseCondition } from "./alert-rule-dsl.js";
|
|
4
|
+
/**
|
|
5
|
+
* Compose multiple condition strings with AND logic.
|
|
6
|
+
* All conditions must be true for the composed condition to fire.
|
|
7
|
+
*/
|
|
8
|
+
export function composeAnd(expressions) {
|
|
9
|
+
const fns = expressions.map(parseCondition);
|
|
10
|
+
if (fns.some((f) => f === null))
|
|
11
|
+
return null;
|
|
12
|
+
return (ctx) => fns.every((f) => f(ctx));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Compose multiple condition strings with OR logic.
|
|
16
|
+
* Any condition being true fires the composed condition.
|
|
17
|
+
*/
|
|
18
|
+
export function composeOr(expressions) {
|
|
19
|
+
const fns = expressions.map(parseCondition);
|
|
20
|
+
if (fns.some((f) => f === null))
|
|
21
|
+
return null;
|
|
22
|
+
return (ctx) => fns.some((f) => f(ctx));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Parse a composed condition from config.
|
|
26
|
+
* Format: { "and": ["fleetHealth < 40", "errorSessions > 2"] }
|
|
27
|
+
* or { "or": ["fleetHealth < 20", "stuckSessions >= 3"] }
|
|
28
|
+
* or plain string "fleetHealth < 50"
|
|
29
|
+
*/
|
|
30
|
+
export function parseComposedCondition(spec) {
|
|
31
|
+
if (typeof spec === "string")
|
|
32
|
+
return parseCondition(spec);
|
|
33
|
+
if ("and" in spec)
|
|
34
|
+
return composeAnd(spec.and);
|
|
35
|
+
if ("or" in spec)
|
|
36
|
+
return composeOr(spec.or);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Format a composed condition for display.
|
|
41
|
+
*/
|
|
42
|
+
export function formatComposedCondition(spec) {
|
|
43
|
+
if (typeof spec === "string")
|
|
44
|
+
return spec;
|
|
45
|
+
if ("and" in spec)
|
|
46
|
+
return spec.and.join(" AND ");
|
|
47
|
+
if ("or" in spec)
|
|
48
|
+
return spec.or.join(" OR ");
|
|
49
|
+
return "unknown";
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=alert-composer.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AlertContext, AlertRule, AlertSeverity } from "./alert-rules.js";
|
|
2
|
+
export interface AlertRuleConfig {
|
|
3
|
+
name: string;
|
|
4
|
+
severity: AlertSeverity;
|
|
5
|
+
condition: string;
|
|
6
|
+
cooldownMin: number;
|
|
7
|
+
description?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Parse a DSL condition string into a function.
|
|
11
|
+
* Format: "<field> <op> <value>" — e.g., "fleetHealth < 40"
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseCondition(expr: string): ((ctx: AlertContext) => boolean) | null;
|
|
14
|
+
/**
|
|
15
|
+
* Convert user-defined rule configs into AlertRule objects.
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseAlertRuleConfigs(configs: AlertRuleConfig[]): {
|
|
18
|
+
rules: AlertRule[];
|
|
19
|
+
errors: string[];
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Validate a DSL condition string without executing it.
|
|
23
|
+
*/
|
|
24
|
+
export declare function validateCondition(expr: string): {
|
|
25
|
+
valid: boolean;
|
|
26
|
+
error?: string;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Format available DSL fields for help display.
|
|
30
|
+
*/
|
|
31
|
+
export declare function formatDslHelp(): string[];
|
|
32
|
+
//# sourceMappingURL=alert-rule-dsl.d.ts.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// alert-rule-dsl.ts — user-defined alert rules via config file.
|
|
2
|
+
// parses a simple DSL for defining custom alert conditions:
|
|
3
|
+
// { "name": "my-rule", "severity": "warning", "condition": "fleetHealth < 40", "cooldownMin": 10 }
|
|
4
|
+
// supported operators and fields
|
|
5
|
+
const VALID_FIELDS = new Set(["fleetHealth", "activeSessions", "errorSessions", "totalCostUsd", "hourlyCostRate", "stuckSessions"]);
|
|
6
|
+
const VALID_OPS = new Set(["<", ">", "<=", ">=", "==", "!="]);
|
|
7
|
+
/**
|
|
8
|
+
* Parse a DSL condition string into a function.
|
|
9
|
+
* Format: "<field> <op> <value>" — e.g., "fleetHealth < 40"
|
|
10
|
+
*/
|
|
11
|
+
export function parseCondition(expr) {
|
|
12
|
+
const match = expr.trim().match(/^(\w+)\s*(<=?|>=?|[!=]=)\s*(\d+(?:\.\d+)?)$/);
|
|
13
|
+
if (!match)
|
|
14
|
+
return null;
|
|
15
|
+
const [, field, op, valueStr] = match;
|
|
16
|
+
if (!VALID_FIELDS.has(field))
|
|
17
|
+
return null;
|
|
18
|
+
if (!VALID_OPS.has(op))
|
|
19
|
+
return null;
|
|
20
|
+
const value = parseFloat(valueStr);
|
|
21
|
+
if (!isFinite(value))
|
|
22
|
+
return null;
|
|
23
|
+
return (ctx) => {
|
|
24
|
+
const actual = ctx[field];
|
|
25
|
+
if (typeof actual !== "number")
|
|
26
|
+
return false;
|
|
27
|
+
switch (op) {
|
|
28
|
+
case "<": return actual < value;
|
|
29
|
+
case ">": return actual > value;
|
|
30
|
+
case "<=": return actual <= value;
|
|
31
|
+
case ">=": return actual >= value;
|
|
32
|
+
case "==": return actual === value;
|
|
33
|
+
case "!=": return actual !== value;
|
|
34
|
+
default: return false;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Convert user-defined rule configs into AlertRule objects.
|
|
40
|
+
*/
|
|
41
|
+
export function parseAlertRuleConfigs(configs) {
|
|
42
|
+
const rules = [];
|
|
43
|
+
const errors = [];
|
|
44
|
+
for (const cfg of configs) {
|
|
45
|
+
const condition = parseCondition(cfg.condition);
|
|
46
|
+
if (!condition) {
|
|
47
|
+
errors.push(`invalid condition in rule "${cfg.name}": "${cfg.condition}"`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
rules.push({
|
|
51
|
+
name: cfg.name,
|
|
52
|
+
description: cfg.description ?? cfg.condition,
|
|
53
|
+
severity: cfg.severity,
|
|
54
|
+
condition,
|
|
55
|
+
cooldownMs: cfg.cooldownMin * 60_000,
|
|
56
|
+
lastFiredAt: 0,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return { rules, errors };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Validate a DSL condition string without executing it.
|
|
63
|
+
*/
|
|
64
|
+
export function validateCondition(expr) {
|
|
65
|
+
const match = expr.trim().match(/^(\w+)\s*(<=?|>=?|[!=]=)\s*(\d+(?:\.\d+)?)$/);
|
|
66
|
+
if (!match)
|
|
67
|
+
return { valid: false, error: `invalid format: expected "<field> <op> <value>", got "${expr}"` };
|
|
68
|
+
const [, field, op] = match;
|
|
69
|
+
if (!VALID_FIELDS.has(field))
|
|
70
|
+
return { valid: false, error: `unknown field "${field}". valid: ${[...VALID_FIELDS].join(", ")}` };
|
|
71
|
+
if (!VALID_OPS.has(op))
|
|
72
|
+
return { valid: false, error: `unknown operator "${op}". valid: ${[...VALID_OPS].join(", ")}` };
|
|
73
|
+
return { valid: true };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Format available DSL fields for help display.
|
|
77
|
+
*/
|
|
78
|
+
export function formatDslHelp() {
|
|
79
|
+
return [
|
|
80
|
+
" Alert rule DSL:",
|
|
81
|
+
" Format: <field> <op> <value>",
|
|
82
|
+
` Fields: ${[...VALID_FIELDS].join(", ")}`,
|
|
83
|
+
` Operators: ${[...VALID_OPS].join(", ")}`,
|
|
84
|
+
' Example: { "name": "low-health", "severity": "warning", "condition": "fleetHealth < 50", "cooldownMin": 10 }',
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=alert-rule-dsl.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type AlertSeverity = "info" | "warning" | "critical";
|
|
2
|
+
export type AlertCondition = (ctx: AlertContext) => boolean;
|
|
3
|
+
export interface AlertContext {
|
|
4
|
+
fleetHealth: number;
|
|
5
|
+
activeSessions: number;
|
|
6
|
+
errorSessions: number;
|
|
7
|
+
totalCostUsd: number;
|
|
8
|
+
hourlyCostRate: number;
|
|
9
|
+
stuckSessions: number;
|
|
10
|
+
idleMinutes: Map<string, number>;
|
|
11
|
+
}
|
|
12
|
+
export interface AlertRule {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
severity: AlertSeverity;
|
|
16
|
+
condition: AlertCondition;
|
|
17
|
+
cooldownMs: number;
|
|
18
|
+
lastFiredAt: number;
|
|
19
|
+
}
|
|
20
|
+
export interface FiredAlert {
|
|
21
|
+
ruleName: string;
|
|
22
|
+
severity: AlertSeverity;
|
|
23
|
+
message: string;
|
|
24
|
+
timestamp: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Built-in alert rules.
|
|
28
|
+
*/
|
|
29
|
+
export declare function defaultAlertRules(): AlertRule[];
|
|
30
|
+
/**
|
|
31
|
+
* Evaluate all alert rules against current fleet state.
|
|
32
|
+
*/
|
|
33
|
+
export declare function evaluateAlertRules(rules: AlertRule[], ctx: AlertContext, now?: number): FiredAlert[];
|
|
34
|
+
/**
|
|
35
|
+
* Format fired alerts for TUI display.
|
|
36
|
+
*/
|
|
37
|
+
export declare function formatFiredAlerts(alerts: FiredAlert[]): string[];
|
|
38
|
+
/**
|
|
39
|
+
* Format all rules and their status for TUI display.
|
|
40
|
+
*/
|
|
41
|
+
export declare function formatAlertRules(rules: AlertRule[], now?: number): string[];
|
|
42
|
+
//# sourceMappingURL=alert-rules.d.ts.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// alert-rules.ts — custom fleet health alerting rules beyond SLA threshold.
|
|
2
|
+
// define conditions that trigger alerts when met, with configurable
|
|
3
|
+
// severity, cooldown, and notification routing.
|
|
4
|
+
/**
|
|
5
|
+
* Built-in alert rules.
|
|
6
|
+
*/
|
|
7
|
+
export function defaultAlertRules() {
|
|
8
|
+
return [
|
|
9
|
+
{
|
|
10
|
+
name: "fleet-health-critical",
|
|
11
|
+
description: "Fleet health dropped below 30",
|
|
12
|
+
severity: "critical",
|
|
13
|
+
condition: (ctx) => ctx.fleetHealth < 30,
|
|
14
|
+
cooldownMs: 10 * 60_000,
|
|
15
|
+
lastFiredAt: 0,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: "high-error-rate",
|
|
19
|
+
description: "More than 50% of sessions in error state",
|
|
20
|
+
severity: "critical",
|
|
21
|
+
condition: (ctx) => ctx.activeSessions > 0 && (ctx.errorSessions / ctx.activeSessions) > 0.5,
|
|
22
|
+
cooldownMs: 5 * 60_000,
|
|
23
|
+
lastFiredAt: 0,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "cost-spike",
|
|
27
|
+
description: "Hourly cost rate exceeds $5",
|
|
28
|
+
severity: "warning",
|
|
29
|
+
condition: (ctx) => ctx.hourlyCostRate > 5,
|
|
30
|
+
cooldownMs: 15 * 60_000,
|
|
31
|
+
lastFiredAt: 0,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "all-stuck",
|
|
35
|
+
description: "All active sessions are stuck",
|
|
36
|
+
severity: "critical",
|
|
37
|
+
condition: (ctx) => ctx.activeSessions > 0 && ctx.stuckSessions === ctx.activeSessions,
|
|
38
|
+
cooldownMs: 10 * 60_000,
|
|
39
|
+
lastFiredAt: 0,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "no-active-sessions",
|
|
43
|
+
description: "No active sessions running",
|
|
44
|
+
severity: "info",
|
|
45
|
+
condition: (ctx) => ctx.activeSessions === 0,
|
|
46
|
+
cooldownMs: 30 * 60_000,
|
|
47
|
+
lastFiredAt: 0,
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Evaluate all alert rules against current fleet state.
|
|
53
|
+
*/
|
|
54
|
+
export function evaluateAlertRules(rules, ctx, now = Date.now()) {
|
|
55
|
+
const fired = [];
|
|
56
|
+
for (const rule of rules) {
|
|
57
|
+
if (now - rule.lastFiredAt < rule.cooldownMs)
|
|
58
|
+
continue;
|
|
59
|
+
if (rule.condition(ctx)) {
|
|
60
|
+
rule.lastFiredAt = now;
|
|
61
|
+
fired.push({
|
|
62
|
+
ruleName: rule.name,
|
|
63
|
+
severity: rule.severity,
|
|
64
|
+
message: rule.description,
|
|
65
|
+
timestamp: now,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return fired;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Format fired alerts for TUI display.
|
|
73
|
+
*/
|
|
74
|
+
export function formatFiredAlerts(alerts) {
|
|
75
|
+
if (alerts.length === 0)
|
|
76
|
+
return [" ✅ no alerts fired"];
|
|
77
|
+
const icons = { info: "ℹ", warning: "⚠", critical: "🚨" };
|
|
78
|
+
return alerts.map((a) => ` ${icons[a.severity]} [${a.severity}] ${a.ruleName}: ${a.message}`);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Format all rules and their status for TUI display.
|
|
82
|
+
*/
|
|
83
|
+
export function formatAlertRules(rules, now = Date.now()) {
|
|
84
|
+
const lines = [];
|
|
85
|
+
lines.push(` Alert rules (${rules.length}):`);
|
|
86
|
+
for (const r of rules) {
|
|
87
|
+
const cooldownRemaining = Math.max(0, r.cooldownMs - (now - r.lastFiredAt));
|
|
88
|
+
const cooldownStr = cooldownRemaining > 0 ? ` (cooldown: ${Math.round(cooldownRemaining / 60_000)}m)` : "";
|
|
89
|
+
const icon = r.severity === "critical" ? "🚨" : r.severity === "warning" ? "⚠" : "ℹ";
|
|
90
|
+
lines.push(` ${icon} ${r.name}: ${r.description}${cooldownStr}`);
|
|
91
|
+
}
|
|
92
|
+
return lines;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=alert-rules.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface FederationPeer {
|
|
2
|
+
name: string;
|
|
3
|
+
url: string;
|
|
4
|
+
lastSeenAt?: number;
|
|
5
|
+
status: "online" | "offline" | "unknown";
|
|
6
|
+
}
|
|
7
|
+
export interface FederatedFleetState {
|
|
8
|
+
peer: string;
|
|
9
|
+
sessions: number;
|
|
10
|
+
activeTasks: number;
|
|
11
|
+
fleetHealth: number;
|
|
12
|
+
totalCostUsd: number;
|
|
13
|
+
lastUpdatedAt: number;
|
|
14
|
+
}
|
|
15
|
+
export interface FederationOverview {
|
|
16
|
+
peers: FederatedFleetState[];
|
|
17
|
+
totalSessions: number;
|
|
18
|
+
totalActiveTasks: number;
|
|
19
|
+
averageHealth: number;
|
|
20
|
+
totalCostUsd: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Fetch fleet state from a peer daemon's health endpoint.
|
|
24
|
+
*/
|
|
25
|
+
export declare function fetchPeerState(peer: FederationPeer, timeoutMs?: number): Promise<FederatedFleetState | null>;
|
|
26
|
+
/**
|
|
27
|
+
* Aggregate fleet state from all peers into an overview.
|
|
28
|
+
*/
|
|
29
|
+
export declare function aggregateFederation(states: FederatedFleetState[]): FederationOverview;
|
|
30
|
+
/**
|
|
31
|
+
* Format federation overview for TUI display.
|
|
32
|
+
*/
|
|
33
|
+
export declare function formatFederationOverview(overview: FederationOverview): string[];
|
|
34
|
+
//# sourceMappingURL=fleet-federation.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// fleet-federation.ts — coordinate across multiple aoaoe daemons via HTTP.
|
|
2
|
+
// each daemon exposes a lightweight status endpoint; the federation client
|
|
3
|
+
// aggregates fleet state across hosts for unified monitoring.
|
|
4
|
+
/**
|
|
5
|
+
* Fetch fleet state from a peer daemon's health endpoint.
|
|
6
|
+
*/
|
|
7
|
+
export async function fetchPeerState(peer, timeoutMs = 5000) {
|
|
8
|
+
try {
|
|
9
|
+
const controller = new AbortController();
|
|
10
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
11
|
+
const response = await fetch(`${peer.url}/health`, { signal: controller.signal });
|
|
12
|
+
clearTimeout(timer);
|
|
13
|
+
if (!response.ok)
|
|
14
|
+
return null;
|
|
15
|
+
const data = await response.json();
|
|
16
|
+
return {
|
|
17
|
+
peer: peer.name,
|
|
18
|
+
sessions: data.sessions ?? 0,
|
|
19
|
+
activeTasks: data.activeTasks ?? 0,
|
|
20
|
+
fleetHealth: data.fleetHealth ?? 0,
|
|
21
|
+
totalCostUsd: data.totalCostUsd ?? 0,
|
|
22
|
+
lastUpdatedAt: Date.now(),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Aggregate fleet state from all peers into an overview.
|
|
31
|
+
*/
|
|
32
|
+
export function aggregateFederation(states) {
|
|
33
|
+
const totalSessions = states.reduce((s, p) => s + p.sessions, 0);
|
|
34
|
+
const totalActiveTasks = states.reduce((s, p) => s + p.activeTasks, 0);
|
|
35
|
+
const totalCost = states.reduce((s, p) => s + p.totalCostUsd, 0);
|
|
36
|
+
const avgHealth = states.length > 0
|
|
37
|
+
? Math.round(states.reduce((s, p) => s + p.fleetHealth, 0) / states.length)
|
|
38
|
+
: 0;
|
|
39
|
+
return { peers: states, totalSessions, totalActiveTasks, averageHealth: avgHealth, totalCostUsd: totalCost };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Format federation overview for TUI display.
|
|
43
|
+
*/
|
|
44
|
+
export function formatFederationOverview(overview) {
|
|
45
|
+
if (overview.peers.length === 0)
|
|
46
|
+
return [" (no federation peers configured)"];
|
|
47
|
+
const lines = [];
|
|
48
|
+
lines.push(` Federation: ${overview.peers.length} peers, ${overview.totalSessions} sessions, health ${overview.averageHealth}/100, $${overview.totalCostUsd.toFixed(2)} total`);
|
|
49
|
+
for (const p of overview.peers) {
|
|
50
|
+
const age = Math.round((Date.now() - p.lastUpdatedAt) / 60_000);
|
|
51
|
+
lines.push(` ${p.peer}: ${p.sessions} sessions, ${p.activeTasks} active, health ${p.fleetHealth}/100, $${p.totalCostUsd.toFixed(2)} (${age}m ago)`);
|
|
52
|
+
}
|
|
53
|
+
return lines;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=fleet-federation.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface GrepHit {
|
|
2
|
+
archive: string;
|
|
3
|
+
lineNumber: number;
|
|
4
|
+
line: string;
|
|
5
|
+
matchStart: number;
|
|
6
|
+
matchEnd: number;
|
|
7
|
+
}
|
|
8
|
+
export interface GrepResult {
|
|
9
|
+
pattern: string;
|
|
10
|
+
totalHits: number;
|
|
11
|
+
filesSearched: number;
|
|
12
|
+
hits: GrepHit[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Search across all archived outputs for a regex pattern.
|
|
16
|
+
*/
|
|
17
|
+
export declare function grepArchives(pattern: string, maxResults?: number, maxFiles?: number): GrepResult;
|
|
18
|
+
/**
|
|
19
|
+
* Format grep results for TUI display.
|
|
20
|
+
*/
|
|
21
|
+
export declare function formatGrepResult(result: GrepResult): string[];
|
|
22
|
+
//# sourceMappingURL=fleet-grep.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// fleet-grep.ts — regex search across archived session outputs.
|
|
2
|
+
// searches gzipped archive files for matches, returning results with context.
|
|
3
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { gunzipSync } from "node:zlib";
|
|
7
|
+
const ARCHIVE_DIR = join(homedir(), ".aoaoe", "output-archive");
|
|
8
|
+
/**
|
|
9
|
+
* Search across all archived outputs for a regex pattern.
|
|
10
|
+
*/
|
|
11
|
+
export function grepArchives(pattern, maxResults = 50, maxFiles = 20) {
|
|
12
|
+
if (!existsSync(ARCHIVE_DIR))
|
|
13
|
+
return { pattern, totalHits: 0, filesSearched: 0, hits: [] };
|
|
14
|
+
let regex;
|
|
15
|
+
try {
|
|
16
|
+
regex = new RegExp(pattern, "gi");
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return { pattern, totalHits: 0, filesSearched: 0, hits: [] };
|
|
20
|
+
}
|
|
21
|
+
const files = readdirSync(ARCHIVE_DIR)
|
|
22
|
+
.filter((f) => f.endsWith(".txt.gz"))
|
|
23
|
+
.sort()
|
|
24
|
+
.reverse()
|
|
25
|
+
.slice(0, maxFiles);
|
|
26
|
+
const hits = [];
|
|
27
|
+
let totalHits = 0;
|
|
28
|
+
for (const file of files) {
|
|
29
|
+
try {
|
|
30
|
+
const compressed = readFileSync(join(ARCHIVE_DIR, file));
|
|
31
|
+
const content = gunzipSync(compressed).toString("utf-8");
|
|
32
|
+
const lines = content.split("\n");
|
|
33
|
+
for (let i = 0; i < lines.length; i++) {
|
|
34
|
+
regex.lastIndex = 0;
|
|
35
|
+
const match = regex.exec(lines[i]);
|
|
36
|
+
if (match) {
|
|
37
|
+
totalHits++;
|
|
38
|
+
if (hits.length < maxResults) {
|
|
39
|
+
hits.push({
|
|
40
|
+
archive: file,
|
|
41
|
+
lineNumber: i + 1,
|
|
42
|
+
line: lines[i].slice(0, 200),
|
|
43
|
+
matchStart: match.index,
|
|
44
|
+
matchEnd: match.index + match[0].length,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch { /* skip unreadable archives */ }
|
|
51
|
+
}
|
|
52
|
+
return { pattern, totalHits, filesSearched: files.length, hits };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Format grep results for TUI display.
|
|
56
|
+
*/
|
|
57
|
+
export function formatGrepResult(result) {
|
|
58
|
+
if (result.totalHits === 0)
|
|
59
|
+
return [` fleet-grep: no matches for "${result.pattern}" in ${result.filesSearched} archives`];
|
|
60
|
+
const lines = [];
|
|
61
|
+
lines.push(` fleet-grep: ${result.totalHits} match${result.totalHits !== 1 ? "es" : ""} for "${result.pattern}" in ${result.filesSearched} archives:`);
|
|
62
|
+
for (const h of result.hits.slice(0, 15)) {
|
|
63
|
+
const preview = h.line.length > 80 ? h.line.slice(0, 77) + "..." : h.line;
|
|
64
|
+
lines.push(` [${h.archive.slice(0, 30)}:${h.lineNumber}] ${preview}`);
|
|
65
|
+
}
|
|
66
|
+
if (result.hits.length > 15)
|
|
67
|
+
lines.push(` ... and ${result.hits.length - 15} more`);
|
|
68
|
+
return lines;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=fleet-grep.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface HealthForecast {
|
|
2
|
+
currentHealth: number;
|
|
3
|
+
trendPerHour: number;
|
|
4
|
+
projectedHealth1h: number;
|
|
5
|
+
projectedHealth4h: number;
|
|
6
|
+
projectedHealth24h: number;
|
|
7
|
+
slaBreachInMs: number;
|
|
8
|
+
slaBreachLabel: string;
|
|
9
|
+
trend: "improving" | "stable" | "declining";
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Forecast fleet health from a time series of samples.
|
|
13
|
+
* Each sample is { timestamp, health }.
|
|
14
|
+
*/
|
|
15
|
+
export declare function forecastHealth(samples: Array<{
|
|
16
|
+
timestamp: number;
|
|
17
|
+
health: number;
|
|
18
|
+
}>, slaThreshold?: number, now?: number): HealthForecast | null;
|
|
19
|
+
/**
|
|
20
|
+
* Format health forecast for TUI display.
|
|
21
|
+
*/
|
|
22
|
+
export declare function formatHealthForecast(forecast: HealthForecast): string[];
|
|
23
|
+
//# sourceMappingURL=health-forecast.d.ts.map
|