@sns-myagent/cli 0.2.0 → 0.3.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/CHANGELOG.md +12 -1
- package/README.md +7 -8
- package/package.json +3 -3
- package/scripts/apply-pi-natives-patch.js +82 -56
- package/src/adapters/telegram/bot.ts +41 -1
- package/src/adapters/telegram/bridge.ts +186 -0
- package/src/adapters/telegram/handler.ts +138 -13
- package/src/adapters/telegram/index.ts +12 -2
- package/src/agents/__tests__/config.test.ts +79 -0
- package/src/agents/__tests__/resilience.test.ts +119 -0
- package/src/agents/__tests__/strategies.test.ts +83 -0
- package/src/agents/config.ts +367 -0
- package/src/agents/ensemble.ts +224 -0
- package/src/agents/resilience.ts +332 -0
- package/src/agents/strategies/best-of-n.ts +108 -0
- package/src/agents/strategies/consensus.ts +105 -0
- package/src/agents/strategies/critic.ts +131 -0
- package/src/agents/strategies/types.ts +68 -0
- package/src/async/__tests__/task-runner.test.ts +162 -0
- package/src/async/__tests__/task-store.test.ts +146 -0
- package/src/async/index.ts +28 -1
- package/src/async/notifier.ts +133 -0
- package/src/async/task-runner.ts +175 -0
- package/src/async/task-store.ts +170 -0
- package/src/async/types.ts +70 -0
- package/src/cli/entry.ts +3 -1
- package/src/cli/index.ts +69 -54
- package/src/config/index.ts +1 -1
- package/src/debug/index.ts +1 -1
- package/src/modes/components/welcome.ts +13 -6
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/modes/setup-wizard/scenes/splash.ts +1 -1
- package/src/session/agent-session.ts +1 -1
- package/src/slash-commands/builtin-registry.ts +63 -0
- package/src/slash-commands/helpers/task.ts +181 -0
- package/src/tbm/__tests__/tbm.test.ts +660 -0
- package/src/tbm/comm-modes.ts +165 -0
- package/src/tbm/config.ts +136 -0
- package/src/tbm/context-delta.ts +146 -0
- package/src/tbm/context-pyramid.ts +202 -0
- package/src/tbm/dashboard.ts +131 -0
- package/src/tbm/index.ts +247 -0
- package/src/tbm/lazy-skills.ts +182 -0
- package/src/tbm/response-cache.ts +220 -0
- package/src/tbm/tombstone.ts +189 -0
- package/src/tbm/tool-compress.ts +230 -0
- package/src/tools/ask.ts +1 -1
- package/src/tui/chat-blocks.ts +205 -0
- package/src/tui/chat-ui.ts +270 -0
- package/src/tui/code-cell.ts +90 -1
- package/src/tui/command-palette.ts +189 -0
- package/src/tui/index.ts +4 -0
- package/src/tui/splash.ts +130 -0
- package/src/ui/banner.ts +69 -29
- package/src/ui/colors.ts +24 -0
- package/src/ui/error-display.ts +130 -0
- package/src/ui/gradient.ts +104 -0
- package/src/ui/index.ts +15 -0
- package/src/ui/memory-toast.ts +102 -0
- package/src/ui/status-bar.ts +36 -30
- package/bin/snscoder.js +0 -98
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agents.yaml config system — SNS-MyAgent Phase 5.2
|
|
3
|
+
*
|
|
4
|
+
* Parses custom agent roles from ~/.sns-myagent/agents.yaml (user-level)
|
|
5
|
+
* or ./.sns-myagent/agents.yaml (project-level).
|
|
6
|
+
*
|
|
7
|
+
* Falls back to bundled Pi agents when no custom config exists.
|
|
8
|
+
* Hot-reloads on file change via fs.watch.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import * as os from "node:os";
|
|
14
|
+
import { EventEmitter } from "node:events";
|
|
15
|
+
|
|
16
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export interface AgentRoleConfig {
|
|
19
|
+
/** Model to use (e.g. "claude-sonnet", "gpt-4o", "perplexity") */
|
|
20
|
+
model: string;
|
|
21
|
+
/** Provider (e.g. "anthropic", "openai", "custom") */
|
|
22
|
+
provider?: string;
|
|
23
|
+
/** Tools this agent can use */
|
|
24
|
+
tools?: string[];
|
|
25
|
+
/** Custom system prompt override */
|
|
26
|
+
system_prompt?: string;
|
|
27
|
+
/** Thinking level: low | medium | high | max */
|
|
28
|
+
thinking_level?: string;
|
|
29
|
+
/** Max tokens for output */
|
|
30
|
+
max_tokens?: number;
|
|
31
|
+
/** Temperature override */
|
|
32
|
+
temperature?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface EnsembleConfig {
|
|
36
|
+
/** Strategy: consensus | critic | best_of_n */
|
|
37
|
+
strategy: "consensus" | "critic" | "best_of_n";
|
|
38
|
+
/** For consensus/best_of_n: list of agent roles to spawn */
|
|
39
|
+
agents?: string[];
|
|
40
|
+
/** For critic: generator agent */
|
|
41
|
+
generator?: string;
|
|
42
|
+
/** For critic: critic agent */
|
|
43
|
+
critic?: string;
|
|
44
|
+
/** Max iteration rounds (critic strategy) */
|
|
45
|
+
max_rounds?: number;
|
|
46
|
+
/** Consensus threshold 0-1 (consensus strategy) */
|
|
47
|
+
threshold?: number;
|
|
48
|
+
/** Number of candidates (best_of_n strategy) */
|
|
49
|
+
n?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AgentsConfig {
|
|
53
|
+
/** Named agent roles */
|
|
54
|
+
agents: Record<string, AgentRoleConfig>;
|
|
55
|
+
/** Named ensemble strategies */
|
|
56
|
+
ensembles?: Record<string, EnsembleConfig>;
|
|
57
|
+
/** Default agent for unspecified tasks */
|
|
58
|
+
default_agent?: string;
|
|
59
|
+
/** Global concurrency limit */
|
|
60
|
+
max_concurrency?: number;
|
|
61
|
+
/** Global timeout per task (ms) */
|
|
62
|
+
task_timeout_ms?: number;
|
|
63
|
+
/** Retry attempts on failure */
|
|
64
|
+
retry_attempts?: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── Config Paths ────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
const USER_CONFIG_DIR = path.join(os.homedir(), ".sns-myagent");
|
|
70
|
+
const USER_CONFIG_PATH = path.join(USER_CONFIG_DIR, "agents.yaml");
|
|
71
|
+
const PROJECT_CONFIG_FILENAME = ".sns-myagent/agents.yaml";
|
|
72
|
+
|
|
73
|
+
function findProjectConfig(): string | null {
|
|
74
|
+
let dir = process.cwd();
|
|
75
|
+
const root = path.parse(dir).root;
|
|
76
|
+
while (dir !== root) {
|
|
77
|
+
const candidate = path.join(dir, PROJECT_CONFIG_FILENAME);
|
|
78
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
79
|
+
dir = path.dirname(dir);
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── YAML Parser (lightweight, no deps) ─────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Minimal YAML parser for agents.yaml structure.
|
|
88
|
+
* Handles: top-level keys, nested objects, arrays, strings, numbers, booleans.
|
|
89
|
+
* NOT a full YAML parser — sufficient for our flat config schema.
|
|
90
|
+
*/
|
|
91
|
+
function parseYamlSimple(text: string): Record<string, unknown> {
|
|
92
|
+
const result: Record<string, unknown> = {};
|
|
93
|
+
const lines = text.split("\n");
|
|
94
|
+
let currentKey = "";
|
|
95
|
+
let currentSubObj: Record<string, unknown> | null = null;
|
|
96
|
+
let currentArray: string[] | null = null;
|
|
97
|
+
let inMultiline = false;
|
|
98
|
+
let multilineKey = "";
|
|
99
|
+
let multilineIndent = 0;
|
|
100
|
+
let multilineLines: string[] = [];
|
|
101
|
+
|
|
102
|
+
for (const rawLine of lines) {
|
|
103
|
+
const line = rawLine.replace(/\r$/, "");
|
|
104
|
+
// Skip empty lines and comments
|
|
105
|
+
if (/^\s*$/.test(line) || /^\s*#/.test(line)) continue;
|
|
106
|
+
|
|
107
|
+
const indent = line.search(/\S/);
|
|
108
|
+
const trimmed = line.trim();
|
|
109
|
+
|
|
110
|
+
// Handle multiline strings (|)
|
|
111
|
+
if (inMultiline) {
|
|
112
|
+
if (indent > multilineIndent) {
|
|
113
|
+
multilineLines.push(line.slice(multilineIndent + 2));
|
|
114
|
+
continue;
|
|
115
|
+
} else {
|
|
116
|
+
// End multiline
|
|
117
|
+
if (currentSubObj) {
|
|
118
|
+
currentSubObj[multilineKey] = multilineLines.join("\n");
|
|
119
|
+
} else {
|
|
120
|
+
result[multilineKey] = multilineLines.join("\n");
|
|
121
|
+
}
|
|
122
|
+
inMultiline = false;
|
|
123
|
+
multilineLines = [];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Top-level key
|
|
128
|
+
if (indent === 0 && trimmed.includes(":")) {
|
|
129
|
+
const colonIdx = trimmed.indexOf(":");
|
|
130
|
+
currentKey = trimmed.slice(0, colonIdx).trim();
|
|
131
|
+
const rest = trimmed.slice(colonIdx + 1).trim();
|
|
132
|
+
if (rest === "|" || rest === ">") {
|
|
133
|
+
inMultiline = true;
|
|
134
|
+
multilineKey = currentKey;
|
|
135
|
+
multilineIndent = 0;
|
|
136
|
+
multilineLines = [];
|
|
137
|
+
} else if (rest === "" || rest === "{}") {
|
|
138
|
+
result[currentKey] = {};
|
|
139
|
+
} else {
|
|
140
|
+
result[currentKey] = parseValue(rest);
|
|
141
|
+
}
|
|
142
|
+
currentSubObj = null;
|
|
143
|
+
currentArray = null;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Nested key (indent > 0)
|
|
148
|
+
if (indent > 0 && trimmed.includes(":") && !trimmed.startsWith("-")) {
|
|
149
|
+
const colonIdx = trimmed.indexOf(":");
|
|
150
|
+
const subKey = trimmed.slice(0, colonIdx).trim();
|
|
151
|
+
const rest = trimmed.slice(colonIdx + 1).trim();
|
|
152
|
+
|
|
153
|
+
// Ensure parent exists as object
|
|
154
|
+
if (!result[currentKey] || typeof result[currentKey] !== "object") {
|
|
155
|
+
result[currentKey] = {};
|
|
156
|
+
}
|
|
157
|
+
const parent = result[currentKey] as Record<string, unknown>;
|
|
158
|
+
|
|
159
|
+
if (rest === "|" || rest === ">") {
|
|
160
|
+
inMultiline = true;
|
|
161
|
+
multilineKey = subKey;
|
|
162
|
+
multilineIndent = indent;
|
|
163
|
+
multilineLines = [];
|
|
164
|
+
} else if (rest === "") {
|
|
165
|
+
// Could be a sub-sub-object; create placeholder
|
|
166
|
+
if (!parent[subKey]) parent[subKey] = {};
|
|
167
|
+
} else {
|
|
168
|
+
parent[subKey] = parseValue(rest);
|
|
169
|
+
}
|
|
170
|
+
currentSubObj = parent;
|
|
171
|
+
currentArray = null;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Array item
|
|
176
|
+
if (trimmed.startsWith("- ")) {
|
|
177
|
+
const value = trimmed.slice(2).trim();
|
|
178
|
+
if (!result[currentKey]) {
|
|
179
|
+
result[currentKey] = [];
|
|
180
|
+
}
|
|
181
|
+
const arr = result[currentKey] as string[];
|
|
182
|
+
arr.push(parseValue(value) as string);
|
|
183
|
+
currentArray = arr;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Sub-level array item (indented)
|
|
188
|
+
if (indent > 0 && trimmed.startsWith("- ")) {
|
|
189
|
+
const value = trimmed.slice(2).trim();
|
|
190
|
+
if (currentSubObj) {
|
|
191
|
+
// Find the last key in currentSubObj that's an array
|
|
192
|
+
const keys = Object.keys(currentSubObj);
|
|
193
|
+
const lastKey = keys[keys.length - 1];
|
|
194
|
+
if (lastKey && Array.isArray(currentSubObj[lastKey])) {
|
|
195
|
+
(currentSubObj[lastKey] as string[]).push(parseValue(value) as string);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Close any open multiline
|
|
203
|
+
if (inMultiline && multilineLines.length > 0) {
|
|
204
|
+
if (currentSubObj) {
|
|
205
|
+
currentSubObj[multilineKey] = multilineLines.join("\n");
|
|
206
|
+
} else {
|
|
207
|
+
result[multilineKey] = multilineLines.join("\n");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function parseValue(raw: string): unknown {
|
|
215
|
+
if (raw === "true") return true;
|
|
216
|
+
if (raw === "false") return false;
|
|
217
|
+
if (raw === "null" || raw === "~") return null;
|
|
218
|
+
if (/^\d+$/.test(raw)) return Number.parseInt(raw, 10);
|
|
219
|
+
if (/^\d+\.\d+$/.test(raw)) return Number.parseFloat(raw);
|
|
220
|
+
// Strip quotes
|
|
221
|
+
if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
|
|
222
|
+
return raw.slice(1, -1);
|
|
223
|
+
}
|
|
224
|
+
return raw;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ─── Config Loader ──────────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
const DEFAULT_CONFIG: AgentsConfig = {
|
|
230
|
+
agents: {},
|
|
231
|
+
ensembles: {},
|
|
232
|
+
default_agent: "task",
|
|
233
|
+
max_concurrency: 4,
|
|
234
|
+
task_timeout_ms: 120_000,
|
|
235
|
+
retry_attempts: 3,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
function parseConfig(raw: Record<string, unknown>): AgentsConfig {
|
|
239
|
+
const config: AgentsConfig = { ...DEFAULT_CONFIG };
|
|
240
|
+
|
|
241
|
+
if (raw.agents && typeof raw.agents === "object") {
|
|
242
|
+
config.agents = raw.agents as Record<string, AgentRoleConfig>;
|
|
243
|
+
}
|
|
244
|
+
if (raw.ensembles && typeof raw.ensembles === "object") {
|
|
245
|
+
config.ensembles = raw.ensembles as Record<string, EnsembleConfig>;
|
|
246
|
+
}
|
|
247
|
+
if (typeof raw.default_agent === "string") {
|
|
248
|
+
config.default_agent = raw.default_agent;
|
|
249
|
+
}
|
|
250
|
+
if (typeof raw.max_concurrency === "number") {
|
|
251
|
+
config.max_concurrency = raw.max_concurrency;
|
|
252
|
+
}
|
|
253
|
+
if (typeof raw.task_timeout_ms === "number") {
|
|
254
|
+
config.task_timeout_ms = raw.task_timeout_ms;
|
|
255
|
+
}
|
|
256
|
+
if (typeof raw.retry_attempts === "number") {
|
|
257
|
+
config.retry_attempts = raw.retry_attempts;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return config;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ─── Singleton + Hot Reload ─────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
class AgentsConfigManager extends EventEmitter {
|
|
266
|
+
#config: AgentsConfig = DEFAULT_CONFIG;
|
|
267
|
+
#watcher: fs.FSWatcher | null = null;
|
|
268
|
+
#configPath: string | null = null;
|
|
269
|
+
|
|
270
|
+
get config(): AgentsConfig {
|
|
271
|
+
return this.#config;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
get configPath(): string | null {
|
|
275
|
+
return this.#configPath;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Load config from project-level, then user-level, then defaults.
|
|
280
|
+
* Project overrides user which overrides defaults.
|
|
281
|
+
*/
|
|
282
|
+
load(): AgentsConfig {
|
|
283
|
+
const projectPath = findProjectConfig();
|
|
284
|
+
const userPath = fs.existsSync(USER_CONFIG_PATH) ? USER_CONFIG_PATH : null;
|
|
285
|
+
const loadPath = projectPath ?? userPath;
|
|
286
|
+
|
|
287
|
+
if (!loadPath) {
|
|
288
|
+
this.#config = DEFAULT_CONFIG;
|
|
289
|
+
this.#configPath = null;
|
|
290
|
+
return this.#config;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const text = fs.readFileSync(loadPath, "utf-8");
|
|
295
|
+
const raw = parseYamlSimple(text);
|
|
296
|
+
this.#config = parseConfig(raw);
|
|
297
|
+
this.#configPath = loadPath;
|
|
298
|
+
} catch {
|
|
299
|
+
this.#config = DEFAULT_CONFIG;
|
|
300
|
+
this.#configPath = null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return this.#config;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Start watching the config file for changes.
|
|
308
|
+
* Emits 'reload' event when config changes.
|
|
309
|
+
*/
|
|
310
|
+
watch(): void {
|
|
311
|
+
if (this.#watcher) return;
|
|
312
|
+
if (!this.#configPath) return;
|
|
313
|
+
|
|
314
|
+
this.#watcher = fs.watch(this.#configPath, { persistent: false }, () => {
|
|
315
|
+
this.load();
|
|
316
|
+
this.emit("reload", this.#config);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
stopWatching(): void {
|
|
321
|
+
if (this.#watcher) {
|
|
322
|
+
this.#watcher.close();
|
|
323
|
+
this.#watcher = null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Resolve a role to its config. Falls back to default_agent.
|
|
329
|
+
*/
|
|
330
|
+
resolveRole(roleName: string): AgentRoleConfig | null {
|
|
331
|
+
return this.#config.agents[roleName] ?? null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Get ensemble config by name.
|
|
336
|
+
*/
|
|
337
|
+
getEnsemble(name: string): EnsembleConfig | null {
|
|
338
|
+
return this.#config.ensembles?.[name] ?? null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* List all configured agent role names.
|
|
343
|
+
*/
|
|
344
|
+
listRoles(): string[] {
|
|
345
|
+
return Object.keys(this.#config.agents);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* List all configured ensemble names.
|
|
350
|
+
*/
|
|
351
|
+
listEnsembles(): string[] {
|
|
352
|
+
return Object.keys(this.#config.ensembles ?? {});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Singleton
|
|
357
|
+
let _instance: AgentsConfigManager | null = null;
|
|
358
|
+
|
|
359
|
+
export function getAgentsConfig(): AgentsConfigManager {
|
|
360
|
+
if (!_instance) {
|
|
361
|
+
_instance = new AgentsConfigManager();
|
|
362
|
+
_instance.load();
|
|
363
|
+
}
|
|
364
|
+
return _instance;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export { AgentsConfigManager, DEFAULT_CONFIG, parseYamlSimple };
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensemble Orchestrator — SNS-MyAgent Phase 5.3e
|
|
3
|
+
*
|
|
4
|
+
* Main entry point for multi-agent ensemble execution.
|
|
5
|
+
* Loads agents.yaml config, resolves strategy, executes with resilience.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getAgentsConfig } from "./config.js";
|
|
9
|
+
import * as resilience from "./resilience.js";
|
|
10
|
+
import type { AgentRoleConfig } from "./config.js";
|
|
11
|
+
import type { AgentResponse, EnsembleResult, EnsembleStrategy } from "./strategies/types.js";
|
|
12
|
+
import { ConsensusStrategy } from "./strategies/consensus.js";
|
|
13
|
+
import { CriticStrategy } from "./strategies/critic.js";
|
|
14
|
+
import { BestOfNStrategy } from "./strategies/best-of-n.js";
|
|
15
|
+
|
|
16
|
+
export interface EnsembleOptions {
|
|
17
|
+
/** Ensemble name from agents.yaml, or strategy name inline */
|
|
18
|
+
ensemble?: string;
|
|
19
|
+
/** Inline strategy override: "consensus" | "critic" | "best_of_n" */
|
|
20
|
+
strategy?: "consensus" | "critic" | "best_of_n";
|
|
21
|
+
/** Agent roles to use (overrides ensemble config) */
|
|
22
|
+
agents?: string[];
|
|
23
|
+
/** Strategy-specific options */
|
|
24
|
+
strategyOptions?: Record<string, unknown>;
|
|
25
|
+
/** Override max concurrency */
|
|
26
|
+
maxConcurrency?: number;
|
|
27
|
+
/** Override task timeout (ms) */
|
|
28
|
+
taskTimeoutMs?: number;
|
|
29
|
+
/** Enable resilience (retry/timeout/circuit-breaker) */
|
|
30
|
+
resilient?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface EnsembleExecutionResult extends EnsembleResult {
|
|
34
|
+
configUsed: {
|
|
35
|
+
ensemble?: string;
|
|
36
|
+
strategy: string;
|
|
37
|
+
agents: string[];
|
|
38
|
+
};
|
|
39
|
+
costBreakdown: Array<{ role: string; model: string; inputTokens: number; outputTokens: number; costUsd?: number }>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Execute an ensemble task.
|
|
44
|
+
*
|
|
45
|
+
* @param prompt - User prompt/task description
|
|
46
|
+
* @param executeAgent - Function to spawn an agent and get response
|
|
47
|
+
* @param options - Ensemble configuration
|
|
48
|
+
* @returns Ensemble result with cost breakdown
|
|
49
|
+
*/
|
|
50
|
+
export async function executeEnsemble(
|
|
51
|
+
prompt: string,
|
|
52
|
+
executeAgent: (role: string, prompt: string) => Promise<AgentResponse>,
|
|
53
|
+
options: EnsembleOptions = {},
|
|
54
|
+
): Promise<EnsembleExecutionResult> {
|
|
55
|
+
const config = getAgentsConfig().config;
|
|
56
|
+
|
|
57
|
+
// Resolve ensemble/strategy/agents
|
|
58
|
+
const { strategyName, agentRoles, strategyOpts } = resolveEnsembleConfig(config, options);
|
|
59
|
+
|
|
60
|
+
// Get strategy instance
|
|
61
|
+
const strategy = createStrategy(strategyName, strategyOpts);
|
|
62
|
+
|
|
63
|
+
// Execute with resilience if enabled
|
|
64
|
+
const executeWithResilience = options.resilient !== false
|
|
65
|
+
? createResilientExecutor(config, options)
|
|
66
|
+
: executeAgent;
|
|
67
|
+
|
|
68
|
+
const result = await strategy.execute(prompt, agentRoles, executeWithResilience);
|
|
69
|
+
|
|
70
|
+
// Build cost breakdown
|
|
71
|
+
const costBreakdown = buildCostBreakdown(result, agentRoles, config);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
...result,
|
|
75
|
+
configUsed: {
|
|
76
|
+
ensemble: options.ensemble,
|
|
77
|
+
strategy: strategyName,
|
|
78
|
+
agents: agentRoles,
|
|
79
|
+
},
|
|
80
|
+
costBreakdown,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveEnsembleConfig(
|
|
85
|
+
config: ReturnType<typeof getAgentsConfig>["config"],
|
|
86
|
+
options: EnsembleOptions,
|
|
87
|
+
): { strategyName: string; agentRoles: string[]; strategyOpts: Record<string, unknown> } {
|
|
88
|
+
// 1. Explicit ensemble name from agents.yaml
|
|
89
|
+
if (options.ensemble) {
|
|
90
|
+
const ensemble = config.ensembles?.[options.ensemble];
|
|
91
|
+
if (!ensemble) throw new Error(`Ensemble "${options.ensemble}" not found in agents.yaml`);
|
|
92
|
+
return {
|
|
93
|
+
strategyName: ensemble.strategy,
|
|
94
|
+
agentRoles: options.agents ?? ensemble.agents ?? [],
|
|
95
|
+
strategyOpts: { ...ensemble, ...options.strategyOptions },
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 2. Inline strategy
|
|
100
|
+
if (options.strategy) {
|
|
101
|
+
return {
|
|
102
|
+
strategyName: options.strategy,
|
|
103
|
+
agentRoles: options.agents ?? (config.default_agent ? [config.default_agent] : []),
|
|
104
|
+
strategyOpts: options.strategyOptions ?? {},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 3. Default: single agent (no ensemble)
|
|
109
|
+
const defaultAgent = config.default_agent ?? "task";
|
|
110
|
+
return {
|
|
111
|
+
strategyName: "single",
|
|
112
|
+
agentRoles: options.agents ?? [defaultAgent],
|
|
113
|
+
strategyOpts: {},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function createStrategy(name: string, options: Record<string, unknown>): EnsembleStrategy {
|
|
118
|
+
switch (name) {
|
|
119
|
+
case "consensus":
|
|
120
|
+
return new ConsensusStrategy(options);
|
|
121
|
+
case "critic":
|
|
122
|
+
return new CriticStrategy(options);
|
|
123
|
+
case "best_of_n":
|
|
124
|
+
return new BestOfNStrategy(options);
|
|
125
|
+
case "single":
|
|
126
|
+
// Single agent fallback
|
|
127
|
+
return {
|
|
128
|
+
name: "single",
|
|
129
|
+
async execute(prompt, agents, executeAgent) {
|
|
130
|
+
const response = await executeAgent(agents[0], prompt);
|
|
131
|
+
return {
|
|
132
|
+
final: response.content,
|
|
133
|
+
responses: [response],
|
|
134
|
+
strategy: "single",
|
|
135
|
+
rounds: 1,
|
|
136
|
+
totalTimeMs: response.timeMs,
|
|
137
|
+
totalTokens: response.tokens ?? { input: 0, output: 0 },
|
|
138
|
+
winner: agents[0],
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
default:
|
|
143
|
+
throw new Error(`Unknown ensemble strategy: ${name}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function createResilientExecutor(
|
|
148
|
+
config: ReturnType<typeof getAgentsConfig>["config"],
|
|
149
|
+
options: EnsembleOptions,
|
|
150
|
+
) {
|
|
151
|
+
return async (role: string, prompt: string): Promise<AgentResponse> => {
|
|
152
|
+
const agentConfig = config.agents[role];
|
|
153
|
+
const modelKey = agentConfig?.model ?? "default";
|
|
154
|
+
|
|
155
|
+
const breaker = resilience.getCircuitBreaker(modelKey);
|
|
156
|
+
|
|
157
|
+
return resilience.withRetry(
|
|
158
|
+
async () => {
|
|
159
|
+
return resilience.withTimeout(
|
|
160
|
+
async () => {
|
|
161
|
+
// This will be replaced by actual agent execution via the wrapper
|
|
162
|
+
throw new Error("executeAgent must be provided by caller");
|
|
163
|
+
},
|
|
164
|
+
options.taskTimeoutMs ?? config.task_timeout_ms ?? 120_000,
|
|
165
|
+
`ensemble:${role}`,
|
|
166
|
+
);
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
maxAttempts: config.retry_attempts ?? 3,
|
|
170
|
+
baseDelayMs: 1000,
|
|
171
|
+
},
|
|
172
|
+
);
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function buildCostBreakdown(
|
|
177
|
+
result: EnsembleResult,
|
|
178
|
+
agentRoles: string[],
|
|
179
|
+
config: ReturnType<typeof getAgentsConfig>["config"],
|
|
180
|
+
): EnsembleExecutionResult["costBreakdown"] {
|
|
181
|
+
const breakdown: EnsembleExecutionResult["costBreakdown"] = [];
|
|
182
|
+
|
|
183
|
+
for (const response of result.responses) {
|
|
184
|
+
const role = response.role;
|
|
185
|
+
const agentConfig = config.agents[role] as AgentRoleConfig | undefined;
|
|
186
|
+
|
|
187
|
+
breakdown.push({
|
|
188
|
+
role,
|
|
189
|
+
model: agentConfig?.model ?? "unknown",
|
|
190
|
+
inputTokens: response.tokens?.input ?? 0,
|
|
191
|
+
outputTokens: response.tokens?.output ?? 0,
|
|
192
|
+
// costUsd: calculateCost(agentConfig?.model, response.tokens), // TODO: add pricing
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return breakdown;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Execute a simple single-agent task with resilience.
|
|
201
|
+
* Convenience wrapper for non-ensemble usage.
|
|
202
|
+
*/
|
|
203
|
+
export async function executeAgentTask(
|
|
204
|
+
role: string,
|
|
205
|
+
prompt: string,
|
|
206
|
+
executeAgent: (role: string, prompt: string) => Promise<AgentResponse>,
|
|
207
|
+
options: { timeoutMs?: number; retries?: number } = {},
|
|
208
|
+
): Promise<AgentResponse> {
|
|
209
|
+
const config = getAgentsConfig().config;
|
|
210
|
+
const timeoutMs = options.timeoutMs ?? config.task_timeout_ms ?? 120_000;
|
|
211
|
+
const retries = options.retries ?? config.retry_attempts ?? 3;
|
|
212
|
+
|
|
213
|
+
return resilience.withRetry(
|
|
214
|
+
async () => resilience.withTimeout(
|
|
215
|
+
() => executeAgent(role, prompt),
|
|
216
|
+
timeoutMs,
|
|
217
|
+
`agent:${role}`,
|
|
218
|
+
),
|
|
219
|
+
{ maxAttempts: retries },
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export { getAgentsConfig } from "./config.js";
|
|
224
|
+
export { ConsensusStrategy, CriticStrategy, BestOfNStrategy } from "./strategies/index.js";
|