aoaoe 2.5.0 → 3.0.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/README.md +1 -1
- package/dist/cost-allocation-tags.d.ts +30 -0
- package/dist/cost-allocation-tags.js +47 -0
- package/dist/goal-similarity.d.ts +20 -0
- package/dist/goal-similarity.js +55 -0
- package/dist/index.js +135 -0
- package/dist/input.d.ts +21 -0
- package/dist/input.js +70 -0
- package/dist/predictive-scaling.d.ts +24 -0
- package/dist/predictive-scaling.js +60 -0
- package/dist/session-clone.d.ts +23 -0
- package/dist/session-clone.js +26 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<a href="https://github.com/Talador12/agent-of-agent-of-empires/actions/workflows/ci.yml"><img src="https://github.com/Talador12/agent-of-agent-of-empires/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
5
5
|
<a href="https://www.npmjs.com/package/aoaoe"><img src="https://img.shields.io/npm/v/aoaoe" alt="npm version"></a>
|
|
6
6
|
<a href="https://github.com/Talador12/agent-of-agent-of-empires/releases"><img src="https://img.shields.io/github/v/release/Talador12/agent-of-agent-of-empires" alt="GitHub release"></a>
|
|
7
|
-
<img src="https://img.shields.io/badge/tests-
|
|
7
|
+
<img src="https://img.shields.io/badge/tests-3491-brightgreen" alt="tests">
|
|
8
8
|
<img src="https://img.shields.io/badge/node-%3E%3D20-blue" alt="Node.js >= 20">
|
|
9
9
|
<img src="https://img.shields.io/badge/runtime%20deps-0-brightgreen" alt="zero runtime dependencies">
|
|
10
10
|
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface CostTag {
|
|
2
|
+
key: string;
|
|
3
|
+
value: string;
|
|
4
|
+
}
|
|
5
|
+
export interface TaggedSession {
|
|
6
|
+
sessionTitle: string;
|
|
7
|
+
tags: CostTag[];
|
|
8
|
+
costUsd: number;
|
|
9
|
+
}
|
|
10
|
+
export interface TagReport {
|
|
11
|
+
tagKey: string;
|
|
12
|
+
groups: Array<{
|
|
13
|
+
value: string;
|
|
14
|
+
sessions: number;
|
|
15
|
+
totalCostUsd: number;
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Group sessions by a tag key and sum costs.
|
|
20
|
+
*/
|
|
21
|
+
export declare function groupByTag(sessions: TaggedSession[], tagKey: string): TagReport;
|
|
22
|
+
/**
|
|
23
|
+
* Format tag report for TUI display.
|
|
24
|
+
*/
|
|
25
|
+
export declare function formatTagReport(report: TagReport): string[];
|
|
26
|
+
/**
|
|
27
|
+
* Parse tags from a config string: "team=platform,project=aoaoe"
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseTags(tagStr: string): CostTag[];
|
|
30
|
+
//# sourceMappingURL=cost-allocation-tags.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// cost-allocation-tags.ts — label sessions by team/project for cost attribution.
|
|
2
|
+
// tags are key-value pairs attached to sessions, used to group costs in reports.
|
|
3
|
+
/**
|
|
4
|
+
* Group sessions by a tag key and sum costs.
|
|
5
|
+
*/
|
|
6
|
+
export function groupByTag(sessions, tagKey) {
|
|
7
|
+
const groups = new Map();
|
|
8
|
+
for (const s of sessions) {
|
|
9
|
+
const tag = s.tags.find((t) => t.key === tagKey);
|
|
10
|
+
const value = tag?.value ?? "(untagged)";
|
|
11
|
+
const group = groups.get(value) ?? { sessions: 0, totalCostUsd: 0 };
|
|
12
|
+
group.sessions++;
|
|
13
|
+
group.totalCostUsd += s.costUsd;
|
|
14
|
+
groups.set(value, group);
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
tagKey,
|
|
18
|
+
groups: [...groups.entries()]
|
|
19
|
+
.map(([value, g]) => ({ value, ...g }))
|
|
20
|
+
.sort((a, b) => b.totalCostUsd - a.totalCostUsd),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Format tag report for TUI display.
|
|
25
|
+
*/
|
|
26
|
+
export function formatTagReport(report) {
|
|
27
|
+
if (report.groups.length === 0)
|
|
28
|
+
return [` (no sessions tagged with "${report.tagKey}")`];
|
|
29
|
+
const lines = [];
|
|
30
|
+
lines.push(` Cost by ${report.tagKey}:`);
|
|
31
|
+
for (const g of report.groups) {
|
|
32
|
+
lines.push(` ${g.value.padEnd(20)} ${g.sessions} session${g.sessions !== 1 ? "s" : ""} $${g.totalCostUsd.toFixed(2)}`);
|
|
33
|
+
}
|
|
34
|
+
return lines;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Parse tags from a config string: "team=platform,project=aoaoe"
|
|
38
|
+
*/
|
|
39
|
+
export function parseTags(tagStr) {
|
|
40
|
+
if (!tagStr)
|
|
41
|
+
return [];
|
|
42
|
+
return tagStr.split(",").map((pair) => {
|
|
43
|
+
const [key, ...rest] = pair.split("=");
|
|
44
|
+
return { key: key.trim(), value: rest.join("=").trim() };
|
|
45
|
+
}).filter((t) => t.key && t.value);
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=cost-allocation-tags.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { TaskState } from "./types.js";
|
|
2
|
+
export interface SimilarityPair {
|
|
3
|
+
titleA: string;
|
|
4
|
+
titleB: string;
|
|
5
|
+
similarity: number;
|
|
6
|
+
sharedKeywords: string[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Compute Jaccard similarity between two keyword sets.
|
|
10
|
+
*/
|
|
11
|
+
export declare function jaccardSimilarity(setA: Set<string>, setB: Set<string>): number;
|
|
12
|
+
/**
|
|
13
|
+
* Find pairs of tasks with similar goals.
|
|
14
|
+
*/
|
|
15
|
+
export declare function findSimilarGoals(tasks: readonly TaskState[], threshold?: number): SimilarityPair[];
|
|
16
|
+
/**
|
|
17
|
+
* Format similarity pairs for TUI display.
|
|
18
|
+
*/
|
|
19
|
+
export declare function formatSimilarGoals(pairs: SimilarityPair[]): string[];
|
|
20
|
+
//# sourceMappingURL=goal-similarity.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// goal-similarity.ts — detect sessions with overlapping goals for coordination.
|
|
2
|
+
// uses Jaccard similarity on keyword sets to find related tasks.
|
|
3
|
+
import { extractKeywords } from "./drift-detector.js";
|
|
4
|
+
/**
|
|
5
|
+
* Compute Jaccard similarity between two keyword sets.
|
|
6
|
+
*/
|
|
7
|
+
export function jaccardSimilarity(setA, setB) {
|
|
8
|
+
if (setA.size === 0 && setB.size === 0)
|
|
9
|
+
return 0;
|
|
10
|
+
let intersection = 0;
|
|
11
|
+
for (const w of setA)
|
|
12
|
+
if (setB.has(w))
|
|
13
|
+
intersection++;
|
|
14
|
+
const union = setA.size + setB.size - intersection;
|
|
15
|
+
return union > 0 ? intersection / union : 0;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Find pairs of tasks with similar goals.
|
|
19
|
+
*/
|
|
20
|
+
export function findSimilarGoals(tasks, threshold = 0.3) {
|
|
21
|
+
const keywordSets = tasks.map((t) => ({
|
|
22
|
+
title: t.sessionTitle,
|
|
23
|
+
keywords: new Set(extractKeywords(t.goal)),
|
|
24
|
+
}));
|
|
25
|
+
const pairs = [];
|
|
26
|
+
for (let i = 0; i < keywordSets.length; i++) {
|
|
27
|
+
for (let j = i + 1; j < keywordSets.length; j++) {
|
|
28
|
+
const sim = jaccardSimilarity(keywordSets[i].keywords, keywordSets[j].keywords);
|
|
29
|
+
if (sim >= threshold) {
|
|
30
|
+
const shared = [...keywordSets[i].keywords].filter((w) => keywordSets[j].keywords.has(w));
|
|
31
|
+
pairs.push({
|
|
32
|
+
titleA: keywordSets[i].title,
|
|
33
|
+
titleB: keywordSets[j].title,
|
|
34
|
+
similarity: Math.round(sim * 100) / 100,
|
|
35
|
+
sharedKeywords: shared,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return pairs.sort((a, b) => b.similarity - a.similarity);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Format similarity pairs for TUI display.
|
|
44
|
+
*/
|
|
45
|
+
export function formatSimilarGoals(pairs) {
|
|
46
|
+
if (pairs.length === 0)
|
|
47
|
+
return [" (no similar goals detected)"];
|
|
48
|
+
const lines = [];
|
|
49
|
+
lines.push(` Similar goals (${pairs.length} pair${pairs.length !== 1 ? "s" : ""}):`);
|
|
50
|
+
for (const p of pairs) {
|
|
51
|
+
lines.push(` ${Math.round(p.similarity * 100)}% — "${p.titleA}" ↔ "${p.titleB}" [${p.sharedKeywords.join(", ")}]`);
|
|
52
|
+
}
|
|
53
|
+
return lines;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=goal-similarity.js.map
|
package/dist/index.js
CHANGED
|
@@ -76,6 +76,13 @@ import { defaultAlertRules, evaluateAlertRules, formatAlertRules } from "./alert
|
|
|
76
76
|
import { forecastHealth, formatHealthForecast } from "./health-forecast.js";
|
|
77
77
|
import { tailSession, formatTail, parseTailArgs } from "./session-tail.js";
|
|
78
78
|
import { renderWorkflowDag, renderChainDag } from "./workflow-viz.js";
|
|
79
|
+
import { formatPrometheusMetrics, buildMetricsSnapshot } from "./metrics-export.js";
|
|
80
|
+
import { grepArchives, formatGrepResult } from "./fleet-grep.js";
|
|
81
|
+
import { createExecution, advanceExecution, formatExecution } from "./runbook-executor.js";
|
|
82
|
+
import { cloneSession, formatCloneResult } from "./session-clone.js";
|
|
83
|
+
import { findSimilarGoals, formatSimilarGoals } from "./goal-similarity.js";
|
|
84
|
+
import { groupByTag, formatTagReport, parseTags } from "./cost-allocation-tags.js";
|
|
85
|
+
import { recommendScaling, formatScalingRecommendation } from "./predictive-scaling.js";
|
|
79
86
|
import { buildLifecycleRecords, computeLifecycleStats, formatLifecycleStats } from "./lifecycle-analytics.js";
|
|
80
87
|
import { buildCostAttributions, computeCostReport, formatCostReport } from "./cost-attribution.js";
|
|
81
88
|
import { decomposeGoal, formatDecomposition } from "./goal-decomposer.js";
|
|
@@ -585,6 +592,7 @@ async function main() {
|
|
|
585
592
|
const tokenQuotaManager = new TokenQuotaManager();
|
|
586
593
|
const abReasoningTracker = new ABReasoningTracker(config.reasoner, "claude-code");
|
|
587
594
|
const alertRules = defaultAlertRules();
|
|
595
|
+
let activeRunbookExec = null;
|
|
588
596
|
// checkpoint restore: load previous daemon state if available
|
|
589
597
|
if (shouldRestoreCheckpoint()) {
|
|
590
598
|
const cp = loadCheckpoint();
|
|
@@ -2666,6 +2674,133 @@ async function main() {
|
|
|
2666
2674
|
tui.log("system", "workflow-viz: no active workflow or chain");
|
|
2667
2675
|
}
|
|
2668
2676
|
});
|
|
2677
|
+
// wire /metrics — Prometheus metrics snapshot
|
|
2678
|
+
input.onMetrics(() => {
|
|
2679
|
+
const sessions = tui.getSessions();
|
|
2680
|
+
const tasks = taskManager?.tasks ?? [];
|
|
2681
|
+
const scores = sessions.map((s) => s.status === "working" || s.status === "running" ? 80 : s.status === "error" ? 20 : 50);
|
|
2682
|
+
const health = scores.length > 0 ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) : 100;
|
|
2683
|
+
let cost = 0;
|
|
2684
|
+
for (const s of sessions) {
|
|
2685
|
+
const m = s.costStr?.match(/\$(\d+(?:\.\d+)?)/);
|
|
2686
|
+
if (m)
|
|
2687
|
+
cost += parseFloat(m[1]);
|
|
2688
|
+
}
|
|
2689
|
+
const cacheStats = observationCache.getStats();
|
|
2690
|
+
const reasonerStats = reasonerCostTracker.getSummary();
|
|
2691
|
+
const nudgeReport = nudgeTracker.getReport();
|
|
2692
|
+
const snapshot = buildMetricsSnapshot({
|
|
2693
|
+
fleetHealth: health, totalSessions: sessions.length,
|
|
2694
|
+
activeSessions: sessions.filter((s) => s.status === "working" || s.status === "running").length,
|
|
2695
|
+
errorSessions: sessions.filter((s) => s.status === "error").length,
|
|
2696
|
+
totalTasks: tasks.length, activeTasks: tasks.filter((t) => t.status === "active").length,
|
|
2697
|
+
completedTasks: tasks.filter((t) => t.status === "completed").length,
|
|
2698
|
+
failedTasks: tasks.filter((t) => t.status === "failed").length,
|
|
2699
|
+
totalCostUsd: cost, reasonerCallsTotal: reasonerStats.totalCalls,
|
|
2700
|
+
reasonerCostTotal: reasonerStats.totalCostUsd,
|
|
2701
|
+
cacheHits: cacheStats.totalHits, cacheMisses: cacheStats.totalMisses,
|
|
2702
|
+
nudgesSent: nudgeReport.totalNudges, nudgesEffective: nudgeReport.effectiveNudges,
|
|
2703
|
+
pollIntervalMs: adaptivePollController.intervalMs,
|
|
2704
|
+
uptimeMs: Date.now() - daemonStartedAt,
|
|
2705
|
+
});
|
|
2706
|
+
const text = formatPrometheusMetrics(snapshot);
|
|
2707
|
+
for (const l of text.split("\n").filter(Boolean))
|
|
2708
|
+
tui.log("system", l);
|
|
2709
|
+
});
|
|
2710
|
+
// wire /fleet-grep — search archived outputs
|
|
2711
|
+
input.onFleetGrep((pattern) => {
|
|
2712
|
+
const result = grepArchives(pattern);
|
|
2713
|
+
const lines = formatGrepResult(result);
|
|
2714
|
+
for (const l of lines)
|
|
2715
|
+
tui.log("system", l);
|
|
2716
|
+
});
|
|
2717
|
+
// wire /runbook-exec — execute/advance runbook
|
|
2718
|
+
input.onRunbookExec(() => {
|
|
2719
|
+
if (!activeRunbookExec) {
|
|
2720
|
+
const runbooks = generateRunbooks();
|
|
2721
|
+
if (runbooks.length === 0) {
|
|
2722
|
+
tui.log("system", "runbook-exec: no runbooks to execute (need audit data)");
|
|
2723
|
+
return;
|
|
2724
|
+
}
|
|
2725
|
+
activeRunbookExec = createExecution(runbooks[0]);
|
|
2726
|
+
tui.log("+ action", `runbook-exec: starting "${runbooks[0].title}"`);
|
|
2727
|
+
}
|
|
2728
|
+
const step = advanceExecution(activeRunbookExec);
|
|
2729
|
+
if (step) {
|
|
2730
|
+
tui.log("system", `runbook-exec: executing step — ${step.action}: ${step.detail}`);
|
|
2731
|
+
}
|
|
2732
|
+
else {
|
|
2733
|
+
const lines = formatExecution(activeRunbookExec);
|
|
2734
|
+
for (const l of lines)
|
|
2735
|
+
tui.log("system", l);
|
|
2736
|
+
activeRunbookExec = null;
|
|
2737
|
+
}
|
|
2738
|
+
});
|
|
2739
|
+
// wire /clone — clone a session
|
|
2740
|
+
input.onClone((args) => {
|
|
2741
|
+
const parts = args.split(/\s+/);
|
|
2742
|
+
const [sourceTitle, cloneTitle, ...goalParts] = parts;
|
|
2743
|
+
if (!sourceTitle || !cloneTitle) {
|
|
2744
|
+
tui.log("system", "clone: usage: /clone <source> <new-name> [goal]");
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2747
|
+
const tasks = taskManager?.tasks ?? [];
|
|
2748
|
+
const source = tasks.find((t) => t.sessionTitle.toLowerCase() === sourceTitle.toLowerCase());
|
|
2749
|
+
if (!source) {
|
|
2750
|
+
tui.log("system", `clone: source "${sourceTitle}" not found`);
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2753
|
+
const goalOverride = goalParts.length > 0 ? goalParts.join(" ") : undefined;
|
|
2754
|
+
const def = cloneSession(source, { sourceTitle, cloneTitle, goalOverride });
|
|
2755
|
+
tui.log("+ action", `cloned "${sourceTitle}" → "${cloneTitle}"`);
|
|
2756
|
+
const lines = formatCloneResult({ original: sourceTitle, clone: cloneTitle, goal: def.goal, tool: def.tool ?? "opencode" });
|
|
2757
|
+
for (const l of lines)
|
|
2758
|
+
tui.log("system", l);
|
|
2759
|
+
});
|
|
2760
|
+
// wire /similar-goals — find overlapping goals
|
|
2761
|
+
input.onSimilarGoals(() => {
|
|
2762
|
+
const tasks = taskManager?.tasks ?? [];
|
|
2763
|
+
const pairs = findSimilarGoals(tasks);
|
|
2764
|
+
const lines = formatSimilarGoals(pairs);
|
|
2765
|
+
for (const l of lines)
|
|
2766
|
+
tui.log("system", l);
|
|
2767
|
+
});
|
|
2768
|
+
// wire /cost-tags — group costs by tag
|
|
2769
|
+
input.onCostTags((tagKey) => {
|
|
2770
|
+
const tasks = taskManager?.tasks ?? [];
|
|
2771
|
+
const sessions = tui.getSessions();
|
|
2772
|
+
const tagged = tasks.map((t) => {
|
|
2773
|
+
const s = sessions.find((s) => s.title === t.sessionTitle);
|
|
2774
|
+
let cost = 0;
|
|
2775
|
+
if (s?.costStr) {
|
|
2776
|
+
const m = s.costStr.match(/\$(\d+(?:\.\d+)?)/);
|
|
2777
|
+
if (m)
|
|
2778
|
+
cost = parseFloat(m[1]);
|
|
2779
|
+
}
|
|
2780
|
+
return { sessionTitle: t.sessionTitle, tags: parseTags(t.tags ?? ""), costUsd: cost };
|
|
2781
|
+
});
|
|
2782
|
+
const report = groupByTag(tagged, tagKey);
|
|
2783
|
+
const lines = formatTagReport(report);
|
|
2784
|
+
for (const l of lines)
|
|
2785
|
+
tui.log("system", l);
|
|
2786
|
+
});
|
|
2787
|
+
// wire /scaling — predictive pool scaling
|
|
2788
|
+
input.onScaling(() => {
|
|
2789
|
+
const tasks = taskManager?.tasks ?? [];
|
|
2790
|
+
const poolStatus = sessionPoolManager.getStatus(tasks);
|
|
2791
|
+
const activeSessions = poolStatus.activeCount;
|
|
2792
|
+
const pendingTasks = poolStatus.pendingCount;
|
|
2793
|
+
const utilPct = poolStatus.maxConcurrent > 0 ? Math.round((activeSessions / poolStatus.maxConcurrent) * 100) : 0;
|
|
2794
|
+
const rec = recommendScaling({
|
|
2795
|
+
currentPoolSize: poolStatus.maxConcurrent,
|
|
2796
|
+
activeSessions, pendingTasks,
|
|
2797
|
+
recentUtilizationPct: utilPct, peakUtilizationPct: utilPct,
|
|
2798
|
+
averageTaskDurationMs: 3_600_000,
|
|
2799
|
+
});
|
|
2800
|
+
const lines = formatScalingRecommendation(rec);
|
|
2801
|
+
for (const l of lines)
|
|
2802
|
+
tui.log("system", l);
|
|
2803
|
+
});
|
|
2669
2804
|
input.onCostSummary(() => {
|
|
2670
2805
|
const sessions = tui.getSessions();
|
|
2671
2806
|
const summary = computeCostSummary(sessions, tui.getAllSessionCosts());
|
package/dist/input.d.ts
CHANGED
|
@@ -148,6 +148,13 @@ export type AlertRulesHandler = () => void;
|
|
|
148
148
|
export type SessionTailHandler = (args: string) => void;
|
|
149
149
|
export type HealthForecastHandler = () => void;
|
|
150
150
|
export type WorkflowVizHandler = () => void;
|
|
151
|
+
export type MetricsHandler = () => void;
|
|
152
|
+
export type FleetGrepHandler = (pattern: string) => void;
|
|
153
|
+
export type RunbookExecHandler = () => void;
|
|
154
|
+
export type CloneHandler = (args: string) => void;
|
|
155
|
+
export type SimilarGoalsHandler = () => void;
|
|
156
|
+
export type CostTagsHandler = (tagKey: string) => void;
|
|
157
|
+
export type ScalingHandler = () => void;
|
|
151
158
|
export interface MouseEvent {
|
|
152
159
|
button: number;
|
|
153
160
|
col: number;
|
|
@@ -494,6 +501,20 @@ export declare class InputReader {
|
|
|
494
501
|
onSessionTail(handler: SessionTailHandler): void;
|
|
495
502
|
onHealthForecast(handler: HealthForecastHandler): void;
|
|
496
503
|
onWorkflowViz(handler: WorkflowVizHandler): void;
|
|
504
|
+
private metricsHandler;
|
|
505
|
+
private fleetGrepHandler;
|
|
506
|
+
private runbookExecHandler;
|
|
507
|
+
private cloneHandler;
|
|
508
|
+
private similarGoalsHandler;
|
|
509
|
+
private costTagsHandler;
|
|
510
|
+
private scalingHandler;
|
|
511
|
+
onMetrics(handler: MetricsHandler): void;
|
|
512
|
+
onFleetGrep(handler: FleetGrepHandler): void;
|
|
513
|
+
onRunbookExec(handler: RunbookExecHandler): void;
|
|
514
|
+
onClone(handler: CloneHandler): void;
|
|
515
|
+
onSimilarGoals(handler: SimilarGoalsHandler): void;
|
|
516
|
+
onCostTags(handler: CostTagsHandler): void;
|
|
517
|
+
onScaling(handler: ScalingHandler): void;
|
|
497
518
|
onFleetSearch(handler: FleetSearchHandler): void;
|
|
498
519
|
onNudgeStats(handler: NudgeStatsHandler): void;
|
|
499
520
|
onAllocation(handler: AllocationHandler): void;
|
package/dist/input.js
CHANGED
|
@@ -574,6 +574,20 @@ export class InputReader {
|
|
|
574
574
|
onSessionTail(handler) { this.sessionTailHandler = handler; }
|
|
575
575
|
onHealthForecast(handler) { this.healthForecastHandler = handler; }
|
|
576
576
|
onWorkflowViz(handler) { this.workflowVizHandler = handler; }
|
|
577
|
+
metricsHandler = null;
|
|
578
|
+
fleetGrepHandler = null;
|
|
579
|
+
runbookExecHandler = null;
|
|
580
|
+
cloneHandler = null;
|
|
581
|
+
similarGoalsHandler = null;
|
|
582
|
+
costTagsHandler = null;
|
|
583
|
+
scalingHandler = null;
|
|
584
|
+
onMetrics(handler) { this.metricsHandler = handler; }
|
|
585
|
+
onFleetGrep(handler) { this.fleetGrepHandler = handler; }
|
|
586
|
+
onRunbookExec(handler) { this.runbookExecHandler = handler; }
|
|
587
|
+
onClone(handler) { this.cloneHandler = handler; }
|
|
588
|
+
onSimilarGoals(handler) { this.similarGoalsHandler = handler; }
|
|
589
|
+
onCostTags(handler) { this.costTagsHandler = handler; }
|
|
590
|
+
onScaling(handler) { this.scalingHandler = handler; }
|
|
577
591
|
onFleetSearch(handler) { this.fleetSearchHandler = handler; }
|
|
578
592
|
onNudgeStats(handler) { this.nudgeStatsHandler = handler; }
|
|
579
593
|
onAllocation(handler) { this.allocationHandler = handler; }
|
|
@@ -2524,6 +2538,62 @@ ${BOLD}other:${RESET}
|
|
|
2524
2538
|
else
|
|
2525
2539
|
console.error(`${DIM}workflow-viz not available (no TUI)${RESET}`);
|
|
2526
2540
|
break;
|
|
2541
|
+
case "/metrics":
|
|
2542
|
+
if (this.metricsHandler)
|
|
2543
|
+
this.metricsHandler();
|
|
2544
|
+
else
|
|
2545
|
+
console.error(`${DIM}metrics not available (no TUI)${RESET}`);
|
|
2546
|
+
break;
|
|
2547
|
+
case "/fleet-grep": {
|
|
2548
|
+
const fgArg = line.slice("/fleet-grep".length).trim();
|
|
2549
|
+
if (!fgArg) {
|
|
2550
|
+
console.error(`${DIM}usage: /fleet-grep <pattern>${RESET}`);
|
|
2551
|
+
break;
|
|
2552
|
+
}
|
|
2553
|
+
if (this.fleetGrepHandler)
|
|
2554
|
+
this.fleetGrepHandler(fgArg);
|
|
2555
|
+
else
|
|
2556
|
+
console.error(`${DIM}fleet-grep not available (no TUI)${RESET}`);
|
|
2557
|
+
break;
|
|
2558
|
+
}
|
|
2559
|
+
case "/runbook-exec":
|
|
2560
|
+
if (this.runbookExecHandler)
|
|
2561
|
+
this.runbookExecHandler();
|
|
2562
|
+
else
|
|
2563
|
+
console.error(`${DIM}runbook-exec not available (no TUI)${RESET}`);
|
|
2564
|
+
break;
|
|
2565
|
+
case "/clone": {
|
|
2566
|
+
const clArg = line.slice("/clone".length).trim();
|
|
2567
|
+
if (!clArg) {
|
|
2568
|
+
console.error(`${DIM}usage: /clone <source> <new-name> [goal-override]${RESET}`);
|
|
2569
|
+
break;
|
|
2570
|
+
}
|
|
2571
|
+
if (this.cloneHandler)
|
|
2572
|
+
this.cloneHandler(clArg);
|
|
2573
|
+
else
|
|
2574
|
+
console.error(`${DIM}clone not available (no TUI)${RESET}`);
|
|
2575
|
+
break;
|
|
2576
|
+
}
|
|
2577
|
+
case "/similar-goals":
|
|
2578
|
+
if (this.similarGoalsHandler)
|
|
2579
|
+
this.similarGoalsHandler();
|
|
2580
|
+
else
|
|
2581
|
+
console.error(`${DIM}similar-goals not available (no TUI)${RESET}`);
|
|
2582
|
+
break;
|
|
2583
|
+
case "/cost-tags": {
|
|
2584
|
+
const ctArg = line.slice("/cost-tags".length).trim() || "team";
|
|
2585
|
+
if (this.costTagsHandler)
|
|
2586
|
+
this.costTagsHandler(ctArg);
|
|
2587
|
+
else
|
|
2588
|
+
console.error(`${DIM}cost-tags not available (no TUI)${RESET}`);
|
|
2589
|
+
break;
|
|
2590
|
+
}
|
|
2591
|
+
case "/scaling":
|
|
2592
|
+
if (this.scalingHandler)
|
|
2593
|
+
this.scalingHandler();
|
|
2594
|
+
else
|
|
2595
|
+
console.error(`${DIM}scaling not available (no TUI)${RESET}`);
|
|
2596
|
+
break;
|
|
2527
2597
|
case "/clear":
|
|
2528
2598
|
process.stderr.write("\x1b[2J\x1b[H");
|
|
2529
2599
|
break;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface ScalingRecommendation {
|
|
2
|
+
currentPoolSize: number;
|
|
3
|
+
recommendedPoolSize: number;
|
|
4
|
+
action: "scale-up" | "scale-down" | "maintain";
|
|
5
|
+
reason: string;
|
|
6
|
+
confidence: "low" | "medium" | "high";
|
|
7
|
+
}
|
|
8
|
+
export interface ScalingInput {
|
|
9
|
+
currentPoolSize: number;
|
|
10
|
+
activeSessions: number;
|
|
11
|
+
pendingTasks: number;
|
|
12
|
+
recentUtilizationPct: number;
|
|
13
|
+
peakUtilizationPct: number;
|
|
14
|
+
averageTaskDurationMs: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Compute a scaling recommendation from current fleet state.
|
|
18
|
+
*/
|
|
19
|
+
export declare function recommendScaling(input: ScalingInput): ScalingRecommendation;
|
|
20
|
+
/**
|
|
21
|
+
* Format scaling recommendation for TUI display.
|
|
22
|
+
*/
|
|
23
|
+
export declare function formatScalingRecommendation(rec: ScalingRecommendation): string[];
|
|
24
|
+
//# sourceMappingURL=predictive-scaling.d.ts.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// predictive-scaling.ts — auto-adjust pool size based on workload patterns.
|
|
2
|
+
// analyzes historical utilization and upcoming task queue to recommend
|
|
3
|
+
// scaling the concurrent session pool up or down.
|
|
4
|
+
/**
|
|
5
|
+
* Compute a scaling recommendation from current fleet state.
|
|
6
|
+
*/
|
|
7
|
+
export function recommendScaling(input) {
|
|
8
|
+
const { currentPoolSize, activeSessions, pendingTasks, recentUtilizationPct, peakUtilizationPct } = input;
|
|
9
|
+
// scale up if utilization is consistently high and tasks are queued
|
|
10
|
+
if (recentUtilizationPct > 80 && pendingTasks > 0) {
|
|
11
|
+
const newSize = Math.min(currentPoolSize * 2, currentPoolSize + pendingTasks);
|
|
12
|
+
return {
|
|
13
|
+
currentPoolSize,
|
|
14
|
+
recommendedPoolSize: Math.ceil(newSize),
|
|
15
|
+
action: "scale-up",
|
|
16
|
+
reason: `${recentUtilizationPct}% utilization with ${pendingTasks} pending tasks`,
|
|
17
|
+
confidence: pendingTasks > 3 ? "high" : "medium",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
// scale up if peak utilization hit 100% (pool was fully saturated)
|
|
21
|
+
if (peakUtilizationPct >= 100 && pendingTasks > 0) {
|
|
22
|
+
return {
|
|
23
|
+
currentPoolSize,
|
|
24
|
+
recommendedPoolSize: currentPoolSize + Math.ceil(pendingTasks * 0.5),
|
|
25
|
+
action: "scale-up",
|
|
26
|
+
reason: `pool saturated (peak ${peakUtilizationPct}%) with ${pendingTasks} waiting`,
|
|
27
|
+
confidence: "high",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// scale down if utilization is consistently low
|
|
31
|
+
if (recentUtilizationPct < 30 && activeSessions < currentPoolSize * 0.5 && currentPoolSize > 2) {
|
|
32
|
+
const newSize = Math.max(2, Math.ceil(activeSessions * 1.5));
|
|
33
|
+
return {
|
|
34
|
+
currentPoolSize,
|
|
35
|
+
recommendedPoolSize: newSize,
|
|
36
|
+
action: "scale-down",
|
|
37
|
+
reason: `${recentUtilizationPct}% utilization, only ${activeSessions}/${currentPoolSize} active`,
|
|
38
|
+
confidence: recentUtilizationPct < 15 ? "high" : "medium",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
currentPoolSize,
|
|
43
|
+
recommendedPoolSize: currentPoolSize,
|
|
44
|
+
action: "maintain",
|
|
45
|
+
reason: `utilization ${recentUtilizationPct}% is within normal range`,
|
|
46
|
+
confidence: "high",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Format scaling recommendation for TUI display.
|
|
51
|
+
*/
|
|
52
|
+
export function formatScalingRecommendation(rec) {
|
|
53
|
+
const icon = rec.action === "scale-up" ? "⬆" : rec.action === "scale-down" ? "⬇" : "─";
|
|
54
|
+
const conf = rec.confidence === "high" ? "●" : rec.confidence === "medium" ? "◐" : "○";
|
|
55
|
+
return [
|
|
56
|
+
` ${icon} ${conf} Pool scaling: ${rec.action} (${rec.currentPoolSize} → ${rec.recommendedPoolSize})`,
|
|
57
|
+
` ${rec.reason}`,
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=predictive-scaling.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { TaskState, TaskDefinition } from "./types.js";
|
|
2
|
+
export interface CloneOptions {
|
|
3
|
+
sourceTitle: string;
|
|
4
|
+
cloneTitle: string;
|
|
5
|
+
goalOverride?: string;
|
|
6
|
+
toolOverride?: string;
|
|
7
|
+
modeOverride?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface CloneResult {
|
|
10
|
+
original: string;
|
|
11
|
+
clone: string;
|
|
12
|
+
goal: string;
|
|
13
|
+
tool: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create a clone task definition from an existing task.
|
|
17
|
+
*/
|
|
18
|
+
export declare function cloneSession(source: TaskState, options: CloneOptions): TaskDefinition;
|
|
19
|
+
/**
|
|
20
|
+
* Format clone result for TUI display.
|
|
21
|
+
*/
|
|
22
|
+
export declare function formatCloneResult(result: CloneResult): string[];
|
|
23
|
+
//# sourceMappingURL=session-clone.d.ts.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// session-clone.ts — clone a session to try alternative approaches in parallel.
|
|
2
|
+
// creates a new task definition from an existing session's config + goal,
|
|
3
|
+
// allowing A/B experimentation on the same problem.
|
|
4
|
+
/**
|
|
5
|
+
* Create a clone task definition from an existing task.
|
|
6
|
+
*/
|
|
7
|
+
export function cloneSession(source, options) {
|
|
8
|
+
return {
|
|
9
|
+
repo: source.repo,
|
|
10
|
+
sessionTitle: options.cloneTitle,
|
|
11
|
+
sessionMode: (options.modeOverride ?? source.sessionMode),
|
|
12
|
+
tool: options.toolOverride ?? source.tool,
|
|
13
|
+
goal: options.goalOverride ?? source.goal,
|
|
14
|
+
dependsOn: undefined, // clone runs independently
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Format clone result for TUI display.
|
|
19
|
+
*/
|
|
20
|
+
export function formatCloneResult(result) {
|
|
21
|
+
return [
|
|
22
|
+
` Cloned "${result.original}" → "${result.clone}"`,
|
|
23
|
+
` Tool: ${result.tool} Goal: ${result.goal.length > 60 ? result.goal.slice(0, 57) + "..." : result.goal}`,
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=session-clone.js.map
|