maqcli 0.2.0 → 0.4.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/core/capabilities.d.ts +57 -0
- package/dist/core/capabilities.js +106 -0
- package/dist/core/command-catalog.js +6 -0
- package/dist/core/config-store.d.ts +8 -0
- package/dist/core/config-store.js +4 -0
- package/dist/core/init-wizard.js +7 -5
- package/dist/core/launcher.d.ts +71 -0
- package/dist/core/launcher.js +340 -0
- package/dist/core/onboarding.d.ts +76 -0
- package/dist/core/onboarding.js +88 -0
- package/dist/core/orchestrator.d.ts +85 -0
- package/dist/core/orchestrator.js +220 -0
- package/dist/core/providers-catalog.d.ts +67 -0
- package/dist/core/providers-catalog.js +188 -0
- package/dist/core/session.d.ts +10 -0
- package/dist/core/session.js +47 -23
- package/dist/index.js +62 -1
- package/dist/server/daemon.js +13 -1
- package/dist/server/relay-bridge.js +4 -0
- package/dist/server/webui.d.ts +19 -0
- package/dist/server/webui.js +319 -0
- package/package.json +1 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding knowledge — the JSON "brain dump" the guided launcher hands to
|
|
3
|
+
* Headroom so the master understands, in one place: what MAQ can do (feature
|
|
4
|
+
* catalog), what role each configured provider/model plays, the permission
|
|
5
|
+
* posture, and the available execution strategies.
|
|
6
|
+
*
|
|
7
|
+
* This is NOT injected wholesale into every prompt (that would waste tokens).
|
|
8
|
+
* It is stored at ~/.maqcli/headroom-knowledge.json and surfaced *by query* —
|
|
9
|
+
* the master pulls only the relevant slice for the user's current goal. Keeping
|
|
10
|
+
* it as structured JSON is what makes selective retrieval cheap.
|
|
11
|
+
*/
|
|
12
|
+
import type { TieredModel } from "./capabilities.js";
|
|
13
|
+
export interface ProviderRole {
|
|
14
|
+
provider: string;
|
|
15
|
+
model: string;
|
|
16
|
+
tier: string;
|
|
17
|
+
/** What this model is registered to do in the pipeline. */
|
|
18
|
+
role: "headroom-master" | "worker" | "fan-out" | "reviewer";
|
|
19
|
+
goodFor: string[];
|
|
20
|
+
source: "cli" | "api" | "local";
|
|
21
|
+
}
|
|
22
|
+
export interface HeadroomKnowledge {
|
|
23
|
+
version: number;
|
|
24
|
+
generatedAt: string;
|
|
25
|
+
/** The master's own management/dev skills, surfaced by query not by default. */
|
|
26
|
+
masterSkills: string[];
|
|
27
|
+
/** MAQ feature catalog (name + what it does) for capability-aware planning. */
|
|
28
|
+
features: Array<{
|
|
29
|
+
name: string;
|
|
30
|
+
category: string;
|
|
31
|
+
summary: string;
|
|
32
|
+
}>;
|
|
33
|
+
/** Every registered provider/model and the role it plays. */
|
|
34
|
+
providers: ProviderRole[];
|
|
35
|
+
permissionMode: "full" | "moderate";
|
|
36
|
+
executionModes: Array<{
|
|
37
|
+
id: string;
|
|
38
|
+
when: string;
|
|
39
|
+
}>;
|
|
40
|
+
headroomModel: {
|
|
41
|
+
provider: string;
|
|
42
|
+
model: string;
|
|
43
|
+
auto: boolean;
|
|
44
|
+
} | null;
|
|
45
|
+
}
|
|
46
|
+
/** Management + latest-development skills the master can draw on, by query. */
|
|
47
|
+
export declare const MASTER_SKILLS: string[];
|
|
48
|
+
export declare const EXECUTION_MODES: {
|
|
49
|
+
id: string;
|
|
50
|
+
when: string;
|
|
51
|
+
}[];
|
|
52
|
+
export interface BuildKnowledgeInput {
|
|
53
|
+
providers: ProviderRole[];
|
|
54
|
+
permissionMode: "full" | "moderate";
|
|
55
|
+
headroomModel?: {
|
|
56
|
+
provider: string;
|
|
57
|
+
model: string;
|
|
58
|
+
auto: boolean;
|
|
59
|
+
} | null;
|
|
60
|
+
}
|
|
61
|
+
/** Build the knowledge document from the current configuration. */
|
|
62
|
+
export declare function buildKnowledge(input: BuildKnowledgeInput): HeadroomKnowledge;
|
|
63
|
+
/** Map a discovered tiered model into a provider-role entry. */
|
|
64
|
+
export declare function roleForModel(m: TieredModel, role: ProviderRole["role"], source: ProviderRole["source"], goodFor?: string[]): ProviderRole;
|
|
65
|
+
export declare function knowledgePath(): string;
|
|
66
|
+
export declare function saveKnowledge(k: HeadroomKnowledge): string;
|
|
67
|
+
export declare function loadKnowledge(): HeadroomKnowledge | null;
|
|
68
|
+
/**
|
|
69
|
+
* Selective retrieval: return only the knowledge slices relevant to a query,
|
|
70
|
+
* so the master pays for context proportional to the task, not the whole doc.
|
|
71
|
+
*/
|
|
72
|
+
export declare function queryKnowledge(k: HeadroomKnowledge, query: string): {
|
|
73
|
+
features: HeadroomKnowledge["features"];
|
|
74
|
+
skills: string[];
|
|
75
|
+
providers: ProviderRole[];
|
|
76
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding knowledge — the JSON "brain dump" the guided launcher hands to
|
|
3
|
+
* Headroom so the master understands, in one place: what MAQ can do (feature
|
|
4
|
+
* catalog), what role each configured provider/model plays, the permission
|
|
5
|
+
* posture, and the available execution strategies.
|
|
6
|
+
*
|
|
7
|
+
* This is NOT injected wholesale into every prompt (that would waste tokens).
|
|
8
|
+
* It is stored at ~/.maqcli/headroom-knowledge.json and surfaced *by query* —
|
|
9
|
+
* the master pulls only the relevant slice for the user's current goal. Keeping
|
|
10
|
+
* it as structured JSON is what makes selective retrieval cheap.
|
|
11
|
+
*/
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { configDir } from "./config-store.js";
|
|
15
|
+
import { maqCommands } from "./command-catalog.js";
|
|
16
|
+
/** Management + latest-development skills the master can draw on, by query. */
|
|
17
|
+
export const MASTER_SKILLS = [
|
|
18
|
+
"decompose a goal into independently-verifiable sub-tasks",
|
|
19
|
+
"route each sub-task to the cheapest model that can do it (RouteLLM-style)",
|
|
20
|
+
"compress verbose tool output before it reaches a model (Headroom compress-cache-retrieve)",
|
|
21
|
+
"run read-only Scout recon before proposing changes",
|
|
22
|
+
"gate plans behind a verifier; prefer the smallest change set",
|
|
23
|
+
"verify with the project's own tests, not self-assessment",
|
|
24
|
+
"record a lesson to AGENTS.md when verification fails (self-learning)",
|
|
25
|
+
"fan out independent work in parallel, then join results",
|
|
26
|
+
"keep worker context isolated; the master sees condensed events, not raw transcripts",
|
|
27
|
+
"ask for permission before destructive or out-of-scope actions when in moderate mode",
|
|
28
|
+
];
|
|
29
|
+
export const EXECUTION_MODES = [
|
|
30
|
+
{ id: "parallel", when: "independent sub-tasks; assign per model, run at once, join by goal" },
|
|
31
|
+
{ id: "loop", when: "one hard deliverable; iterate/refine in a loop until it passes" },
|
|
32
|
+
{ id: "safe", when: "split into mini-parts on light models (no interference), merge via a loop, then parallelize" },
|
|
33
|
+
];
|
|
34
|
+
/** Build the knowledge document from the current configuration. */
|
|
35
|
+
export function buildKnowledge(input) {
|
|
36
|
+
return {
|
|
37
|
+
version: 1,
|
|
38
|
+
generatedAt: new Date().toISOString(),
|
|
39
|
+
masterSkills: MASTER_SKILLS,
|
|
40
|
+
features: maqCommands.map((c) => ({ name: c.name, category: c.category, summary: c.summary })),
|
|
41
|
+
providers: input.providers,
|
|
42
|
+
permissionMode: input.permissionMode,
|
|
43
|
+
executionModes: EXECUTION_MODES,
|
|
44
|
+
headroomModel: input.headroomModel ?? null,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Map a discovered tiered model into a provider-role entry. */
|
|
48
|
+
export function roleForModel(m, role, source, goodFor = []) {
|
|
49
|
+
return { provider: m.maqProvider, model: m.id, tier: m.tier, role, goodFor, source };
|
|
50
|
+
}
|
|
51
|
+
export function knowledgePath() {
|
|
52
|
+
return join(configDir(), "headroom-knowledge.json");
|
|
53
|
+
}
|
|
54
|
+
export function saveKnowledge(k) {
|
|
55
|
+
const dir = configDir();
|
|
56
|
+
if (!existsSync(dir))
|
|
57
|
+
mkdirSync(dir, { recursive: true });
|
|
58
|
+
const p = knowledgePath();
|
|
59
|
+
writeFileSync(p, JSON.stringify(k, null, 2), "utf8");
|
|
60
|
+
return p;
|
|
61
|
+
}
|
|
62
|
+
export function loadKnowledge() {
|
|
63
|
+
const p = knowledgePath();
|
|
64
|
+
if (!existsSync(p))
|
|
65
|
+
return null;
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Selective retrieval: return only the knowledge slices relevant to a query,
|
|
75
|
+
* so the master pays for context proportional to the task, not the whole doc.
|
|
76
|
+
*/
|
|
77
|
+
export function queryKnowledge(k, query) {
|
|
78
|
+
const q = query.toLowerCase();
|
|
79
|
+
const words = q.split(/\W+/).filter((w) => w.length >= 3);
|
|
80
|
+
const match = (text) => words.some((w) => text.toLowerCase().includes(w));
|
|
81
|
+
const features = k.features.filter((f) => match(f.name) || match(f.summary));
|
|
82
|
+
const skills = k.masterSkills.filter((s) => match(s));
|
|
83
|
+
return {
|
|
84
|
+
features: features.length ? features : k.features.slice(0, 6),
|
|
85
|
+
skills: skills.length ? skills : k.masterSkills.slice(0, 4),
|
|
86
|
+
providers: k.providers,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestrator — the three god-level execution engines that sit ABOVE the
|
|
3
|
+
* single Scout→Plan→Execute→Verify pipeline. The master (Headroom) never reads
|
|
4
|
+
* the full codebase; it decomposes a goal, dispatches work, and reasons only
|
|
5
|
+
* over the condensed sub-results — then decides the next move until the goal is
|
|
6
|
+
* met.
|
|
7
|
+
*
|
|
8
|
+
* parallel — independent sub-tasks assigned per model, run at once; the
|
|
9
|
+
* master joins the replies, thinks the next batch, repeats until
|
|
10
|
+
* the goal is satisfied (or maxRounds).
|
|
11
|
+
* loop — one hard deliverable; run, and if it doesn't verify, refine the
|
|
12
|
+
* instruction with the failure and retry until it passes (or
|
|
13
|
+
* maxIterations). Best for long single tasks.
|
|
14
|
+
* safe — split into mini-parts on LIGHT models in isolation (no
|
|
15
|
+
* interference), MERGE the parts via a loop, then a final parallel
|
|
16
|
+
* validation pass.
|
|
17
|
+
*
|
|
18
|
+
* Every collaborator (decompose / runTask / evaluate / merge) is injectable, so
|
|
19
|
+
* the engines are fully unit-testable offline with deterministic fakes. The
|
|
20
|
+
* default collaborators are backed by the real provider + runPipeline.
|
|
21
|
+
*/
|
|
22
|
+
import type { MaqEvent } from "./types.js";
|
|
23
|
+
export type ExecutionMode = "parallel" | "loop" | "safe";
|
|
24
|
+
export interface SubTaskResult {
|
|
25
|
+
task: string;
|
|
26
|
+
verified: boolean;
|
|
27
|
+
status: string;
|
|
28
|
+
summary: string;
|
|
29
|
+
tier?: "cheap" | "strong";
|
|
30
|
+
}
|
|
31
|
+
export interface OrchestrationResult {
|
|
32
|
+
goal: string;
|
|
33
|
+
mode: ExecutionMode;
|
|
34
|
+
rounds: number;
|
|
35
|
+
subtasks: SubTaskResult[];
|
|
36
|
+
verified: boolean;
|
|
37
|
+
summary: string;
|
|
38
|
+
}
|
|
39
|
+
export interface EvaluateResult {
|
|
40
|
+
done: boolean;
|
|
41
|
+
reason: string;
|
|
42
|
+
followups: string[];
|
|
43
|
+
}
|
|
44
|
+
export interface RunTaskCtx {
|
|
45
|
+
tier?: "cheap" | "strong";
|
|
46
|
+
onEvent?: (e: MaqEvent) => void;
|
|
47
|
+
signal?: AbortSignal;
|
|
48
|
+
}
|
|
49
|
+
/** Injectable collaborators; defaults are provider/pipeline-backed. */
|
|
50
|
+
export interface OrchestratorDeps {
|
|
51
|
+
decompose: (goal: string, ctx: {
|
|
52
|
+
round: number;
|
|
53
|
+
prior: SubTaskResult[];
|
|
54
|
+
}) => Promise<string[]>;
|
|
55
|
+
runTask: (task: string, ctx: RunTaskCtx) => Promise<SubTaskResult>;
|
|
56
|
+
evaluate: (goal: string, results: SubTaskResult[]) => Promise<EvaluateResult>;
|
|
57
|
+
merge: (goal: string, results: SubTaskResult[], ctx: RunTaskCtx) => Promise<SubTaskResult>;
|
|
58
|
+
}
|
|
59
|
+
export interface OrchestrationOptions {
|
|
60
|
+
cwd?: string;
|
|
61
|
+
target?: string;
|
|
62
|
+
provider?: string;
|
|
63
|
+
model?: string;
|
|
64
|
+
dryRun?: boolean;
|
|
65
|
+
maxConcurrency?: number;
|
|
66
|
+
/** parallel/safe: how many decompose→run→evaluate rounds before stopping. */
|
|
67
|
+
maxRounds?: number;
|
|
68
|
+
/** loop: how many refine→retry iterations before stopping. */
|
|
69
|
+
maxIterations?: number;
|
|
70
|
+
onEvent?: (e: MaqEvent) => void;
|
|
71
|
+
signal?: AbortSignal;
|
|
72
|
+
checkpoint?: () => Promise<void>;
|
|
73
|
+
/** Override any collaborator (tests inject deterministic ones). */
|
|
74
|
+
deps?: Partial<OrchestratorDeps>;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Deterministic, offline goal splitter used as a fallback when a real model
|
|
78
|
+
* can't (or shouldn't) be asked to decompose. Splits on natural connectors.
|
|
79
|
+
*/
|
|
80
|
+
export declare function heuristicSplit(goal: string): string[];
|
|
81
|
+
/** Refine a failed task with the failure context for the next loop iteration. */
|
|
82
|
+
export declare function refineTask(goal: string, last: SubTaskResult): string;
|
|
83
|
+
/** Bounded-concurrency map (a tiny worker pool). Preserves input order. */
|
|
84
|
+
export declare function pool<T, R>(items: T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
|
85
|
+
export declare function runOrchestration(goal: string, mode: ExecutionMode, opts?: OrchestrationOptions): Promise<OrchestrationResult>;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestrator — the three god-level execution engines that sit ABOVE the
|
|
3
|
+
* single Scout→Plan→Execute→Verify pipeline. The master (Headroom) never reads
|
|
4
|
+
* the full codebase; it decomposes a goal, dispatches work, and reasons only
|
|
5
|
+
* over the condensed sub-results — then decides the next move until the goal is
|
|
6
|
+
* met.
|
|
7
|
+
*
|
|
8
|
+
* parallel — independent sub-tasks assigned per model, run at once; the
|
|
9
|
+
* master joins the replies, thinks the next batch, repeats until
|
|
10
|
+
* the goal is satisfied (or maxRounds).
|
|
11
|
+
* loop — one hard deliverable; run, and if it doesn't verify, refine the
|
|
12
|
+
* instruction with the failure and retry until it passes (or
|
|
13
|
+
* maxIterations). Best for long single tasks.
|
|
14
|
+
* safe — split into mini-parts on LIGHT models in isolation (no
|
|
15
|
+
* interference), MERGE the parts via a loop, then a final parallel
|
|
16
|
+
* validation pass.
|
|
17
|
+
*
|
|
18
|
+
* Every collaborator (decompose / runTask / evaluate / merge) is injectable, so
|
|
19
|
+
* the engines are fully unit-testable offline with deterministic fakes. The
|
|
20
|
+
* default collaborators are backed by the real provider + runPipeline.
|
|
21
|
+
*/
|
|
22
|
+
import { makeEvent } from "./types.js";
|
|
23
|
+
import { runPipeline } from "./pipeline.js";
|
|
24
|
+
import { loadConfig } from "./config-store.js";
|
|
25
|
+
import { getProvider } from "./model.js";
|
|
26
|
+
/* --------------------------- pure helpers ------------------------------ */
|
|
27
|
+
/**
|
|
28
|
+
* Deterministic, offline goal splitter used as a fallback when a real model
|
|
29
|
+
* can't (or shouldn't) be asked to decompose. Splits on natural connectors.
|
|
30
|
+
*/
|
|
31
|
+
export function heuristicSplit(goal) {
|
|
32
|
+
const parts = goal
|
|
33
|
+
.split(/\s*(?:,?\s+and\s+then\s+|;|\n|\bthen\b|,\s+and\s+|\band also\b)\s*/i)
|
|
34
|
+
.map((p) => p.trim())
|
|
35
|
+
.filter((p) => p.length > 2);
|
|
36
|
+
const uniq = [...new Set(parts)];
|
|
37
|
+
return uniq.length >= 2 ? uniq : [goal.trim()];
|
|
38
|
+
}
|
|
39
|
+
/** Refine a failed task with the failure context for the next loop iteration. */
|
|
40
|
+
export function refineTask(goal, last) {
|
|
41
|
+
return `${goal}\n\nThe previous attempt did not pass verification (status=${last.status}: ${last.summary}). Diagnose the root cause and fix it; do not repeat the same approach.`;
|
|
42
|
+
}
|
|
43
|
+
/** Bounded-concurrency map (a tiny worker pool). Preserves input order. */
|
|
44
|
+
export async function pool(items, limit, fn) {
|
|
45
|
+
const results = new Array(items.length);
|
|
46
|
+
let next = 0;
|
|
47
|
+
const n = Math.max(1, Math.min(limit, items.length || 1));
|
|
48
|
+
async function worker() {
|
|
49
|
+
for (;;) {
|
|
50
|
+
const i = next++;
|
|
51
|
+
if (i >= items.length)
|
|
52
|
+
return;
|
|
53
|
+
results[i] = await fn(items[i], i);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
await Promise.all(Array.from({ length: n }, () => worker()));
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
59
|
+
/* ------------------------- default collaborators ----------------------- */
|
|
60
|
+
function makeDefaultDeps(opts) {
|
|
61
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
62
|
+
const cfg = loadConfig();
|
|
63
|
+
const providerName = opts.provider ?? cfg.provider;
|
|
64
|
+
const runTask = async (task, ctx) => {
|
|
65
|
+
const model = opts.model ?? (ctx.tier === "strong" ? cfg.strongModel : cfg.cheapModel);
|
|
66
|
+
const res = await runPipeline(task, {
|
|
67
|
+
cwd,
|
|
68
|
+
target: opts.target,
|
|
69
|
+
provider: providerName,
|
|
70
|
+
model,
|
|
71
|
+
dryRun: opts.dryRun,
|
|
72
|
+
onEvent: ctx.onEvent,
|
|
73
|
+
signal: ctx.signal,
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
task,
|
|
77
|
+
verified: res.verify.verified,
|
|
78
|
+
status: res.execute.status,
|
|
79
|
+
summary: res.plan?.winner.summary ?? res.verify.details ?? task,
|
|
80
|
+
tier: ctx.tier,
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
const decompose = async (goal, ctx) => {
|
|
84
|
+
// Follow-up rounds re-run only the unfinished work.
|
|
85
|
+
if (ctx.round > 1 && ctx.prior.some((p) => !p.verified)) {
|
|
86
|
+
return ctx.prior.filter((p) => !p.verified).map((p) => refineTask(goal, p));
|
|
87
|
+
}
|
|
88
|
+
// Ask the model for a JSON array of subtasks; fall back to the splitter.
|
|
89
|
+
try {
|
|
90
|
+
const provider = getProvider(providerName);
|
|
91
|
+
const resp = await provider.complete({
|
|
92
|
+
model: opts.model ?? cfg.cheapModel,
|
|
93
|
+
messages: [
|
|
94
|
+
{ role: "system", content: "Decompose the user's goal into 2-5 independent sub-tasks. Reply with ONLY a JSON array of short strings, no prose." },
|
|
95
|
+
{ role: "user", content: goal },
|
|
96
|
+
],
|
|
97
|
+
maxTokens: 300,
|
|
98
|
+
});
|
|
99
|
+
const arr = JSON.parse(resp.text.trim().replace(/^```(json)?/i, "").replace(/```$/, "").trim());
|
|
100
|
+
if (Array.isArray(arr) && arr.length >= 2)
|
|
101
|
+
return arr.map(String).slice(0, 6);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
/* fall through */
|
|
105
|
+
}
|
|
106
|
+
return heuristicSplit(goal);
|
|
107
|
+
};
|
|
108
|
+
const evaluate = async (goal, results) => {
|
|
109
|
+
const failed = results.filter((r) => !r.verified);
|
|
110
|
+
return {
|
|
111
|
+
done: failed.length === 0,
|
|
112
|
+
reason: failed.length ? `${failed.length} of ${results.length} sub-task(s) unverified` : "all sub-tasks verified against the goal",
|
|
113
|
+
followups: failed.map((r) => refineTask(goal, r)),
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
const merge = async (goal, results, ctx) => {
|
|
117
|
+
const task = `Integrate the following completed sub-results into one coherent solution for the goal "${goal}". Resolve overlaps and ensure the whole passes verification.\n` +
|
|
118
|
+
results.map((r) => `- ${r.task} → ${r.status} (${r.summary})`).join("\n");
|
|
119
|
+
return runTask(task, { ...ctx, tier: "strong" });
|
|
120
|
+
};
|
|
121
|
+
return { decompose, runTask, evaluate, merge };
|
|
122
|
+
}
|
|
123
|
+
/* ------------------------------- engines ------------------------------- */
|
|
124
|
+
export async function runOrchestration(goal, mode, opts = {}) {
|
|
125
|
+
const deps = { ...makeDefaultDeps(opts), ...opts.deps };
|
|
126
|
+
const emit = (e) => opts.onEvent?.(e);
|
|
127
|
+
emit(makeEvent("task.started", { goal, mode, engine: "orchestrator" }));
|
|
128
|
+
let result;
|
|
129
|
+
try {
|
|
130
|
+
if (mode === "loop")
|
|
131
|
+
result = await engineLoop(goal, deps, opts, emit);
|
|
132
|
+
else if (mode === "safe")
|
|
133
|
+
result = await engineSafe(goal, deps, opts, emit);
|
|
134
|
+
else
|
|
135
|
+
result = await engineParallel(goal, deps, opts, emit);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
emit(makeEvent("task.error", { message: err instanceof Error ? err.message : String(err), mode }));
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
emit(makeEvent("task.done", { verified: result.verified, mode, rounds: result.rounds, subtasks: result.subtasks.length, summary: result.summary }));
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
async function engineParallel(goal, deps, opts, emit) {
|
|
145
|
+
const maxRounds = Math.max(1, opts.maxRounds ?? 3);
|
|
146
|
+
const concurrency = opts.maxConcurrency ?? 4;
|
|
147
|
+
const all = []; // full history (what actually ran)
|
|
148
|
+
const completed = []; // verified results carried forward
|
|
149
|
+
let outstanding = []; // last round's failures (fed to the next decompose)
|
|
150
|
+
let round = 0;
|
|
151
|
+
let evaluated = { done: false, reason: "", followups: [] };
|
|
152
|
+
while (round < maxRounds) {
|
|
153
|
+
await opts.checkpoint?.();
|
|
154
|
+
round++;
|
|
155
|
+
const phase = `parallel-round-${round}`;
|
|
156
|
+
emit(makeEvent("phase.started", { phase, mode: "parallel" }));
|
|
157
|
+
const tasks = await deps.decompose(goal, { round, prior: outstanding });
|
|
158
|
+
emit(makeEvent("agent.event", { note: "decomposed", round, subtasks: tasks }));
|
|
159
|
+
const results = await pool(tasks, concurrency, (task) => deps.runTask(task, { tier: "cheap", onEvent: emit, signal: opts.signal }));
|
|
160
|
+
all.push(...results);
|
|
161
|
+
// A follow-up round addresses the previous round's failures, so only THIS
|
|
162
|
+
// round's failures count as outstanding — old ones are superseded.
|
|
163
|
+
completed.push(...results.filter((r) => r.verified));
|
|
164
|
+
outstanding = results.filter((r) => !r.verified);
|
|
165
|
+
evaluated = await deps.evaluate(goal, [...completed, ...outstanding]);
|
|
166
|
+
emit(makeEvent("phase.done", { phase, done: evaluated.done, reason: evaluated.reason, verifiedThisRound: results.filter((r) => r.verified).length }));
|
|
167
|
+
if (evaluated.done || outstanding.length === 0)
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
return { goal, mode: "parallel", rounds: round, subtasks: all, verified: evaluated.done, summary: evaluated.reason };
|
|
171
|
+
}
|
|
172
|
+
async function engineLoop(goal, deps, opts, emit) {
|
|
173
|
+
const maxIterations = Math.max(1, opts.maxIterations ?? 4);
|
|
174
|
+
const attempts = [];
|
|
175
|
+
let task = goal;
|
|
176
|
+
let iter = 0;
|
|
177
|
+
let last = null;
|
|
178
|
+
while (iter < maxIterations) {
|
|
179
|
+
await opts.checkpoint?.();
|
|
180
|
+
iter++;
|
|
181
|
+
const phase = `loop-iteration-${iter}`;
|
|
182
|
+
emit(makeEvent("phase.started", { phase, mode: "loop" }));
|
|
183
|
+
last = await deps.runTask(task, { tier: "strong", onEvent: emit, signal: opts.signal });
|
|
184
|
+
attempts.push(last);
|
|
185
|
+
emit(makeEvent("phase.done", { phase, verified: last.verified, status: last.status }));
|
|
186
|
+
if (last.verified)
|
|
187
|
+
break;
|
|
188
|
+
task = refineTask(goal, last);
|
|
189
|
+
emit(makeEvent("agent.event", { note: "refining after failed verification", iteration: iter }));
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
goal,
|
|
193
|
+
mode: "loop",
|
|
194
|
+
rounds: iter,
|
|
195
|
+
subtasks: attempts,
|
|
196
|
+
verified: last?.verified ?? false,
|
|
197
|
+
summary: last?.verified ? `verified after ${iter} iteration(s)` : `not verified after ${iter} iteration(s)`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
async function engineSafe(goal, deps, opts, emit) {
|
|
201
|
+
const concurrency = opts.maxConcurrency ?? 4;
|
|
202
|
+
// 1. Split into mini-parts and run them ISOLATED on light models.
|
|
203
|
+
await opts.checkpoint?.();
|
|
204
|
+
emit(makeEvent("phase.started", { phase: "safe-split", mode: "safe" }));
|
|
205
|
+
const parts = await deps.decompose(goal, { round: 1, prior: [] });
|
|
206
|
+
emit(makeEvent("agent.event", { note: "split into isolated mini-parts", parts }));
|
|
207
|
+
const partResults = await pool(parts, concurrency, (task) => deps.runTask(task, { tier: "cheap", onEvent: emit, signal: opts.signal }));
|
|
208
|
+
emit(makeEvent("phase.done", { phase: "safe-split", parts: partResults.length, verified: partResults.filter((r) => r.verified).length }));
|
|
209
|
+
// 2. MERGE the parts via a (single) integration step on a strong model.
|
|
210
|
+
await opts.checkpoint?.();
|
|
211
|
+
emit(makeEvent("phase.started", { phase: "safe-merge", mode: "safe" }));
|
|
212
|
+
const merged = await deps.merge(goal, partResults, { onEvent: emit, signal: opts.signal });
|
|
213
|
+
emit(makeEvent("phase.done", { phase: "safe-merge", verified: merged.verified, status: merged.status }));
|
|
214
|
+
// 3. Final validation pass over everything.
|
|
215
|
+
await opts.checkpoint?.();
|
|
216
|
+
const all = [...partResults, merged];
|
|
217
|
+
const evaluated = await deps.evaluate(goal, all);
|
|
218
|
+
emit(makeEvent("phase.done", { phase: "safe-validate", done: evaluated.done, reason: evaluated.reason }));
|
|
219
|
+
return { goal, mode: "safe", rounds: 1, subtasks: all, verified: evaluated.done && merged.verified, summary: evaluated.reason };
|
|
220
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider catalog — the 2026 "menu" the guided launcher shows for AI mode
|
|
3
|
+
* option (3): every kind of AI API provider, its setup format, docs link, and
|
|
4
|
+
* a known model list. Listing is FREE — it consumes zero tokens and makes zero
|
|
5
|
+
* network calls. A provider only becomes "active" the moment a task actually
|
|
6
|
+
* uses it.
|
|
7
|
+
*
|
|
8
|
+
* This is deliberately static + dependency-free: it is the vocabulary the
|
|
9
|
+
* launcher and Headroom knowledge doc are built from. Model lists are the
|
|
10
|
+
* publicly-known families as of 2026-07; the live `/models`-style discovery for
|
|
11
|
+
* a specific key is a separate, opt-in step (see capabilities.ts).
|
|
12
|
+
*/
|
|
13
|
+
import type { CapabilityTier } from "./capabilities.js";
|
|
14
|
+
/** How a provider's HTTP API is shaped (drives which ModelProvider we use). */
|
|
15
|
+
export type ApiFormat = "openai" | "anthropic" | "gemini" | "ollama";
|
|
16
|
+
export interface CatalogModel {
|
|
17
|
+
id: string;
|
|
18
|
+
/** Rough capability class used for the efficient-"mid" auto pick. */
|
|
19
|
+
tier: CapabilityTier;
|
|
20
|
+
/** Multimodal / vision capable. */
|
|
21
|
+
vision?: boolean;
|
|
22
|
+
/** Very large context (>= 200k tokens). */
|
|
23
|
+
longContext?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface CatalogProvider {
|
|
26
|
+
/** Stable id used in config (`provider`). */
|
|
27
|
+
id: string;
|
|
28
|
+
label: string;
|
|
29
|
+
/** OpenAI-compatible? anthropic-native? etc. */
|
|
30
|
+
format: ApiFormat;
|
|
31
|
+
/** The MAQ provider name getProvider() understands. */
|
|
32
|
+
maqProvider: string;
|
|
33
|
+
/** Env var that holds the API key (empty for keyless/local). */
|
|
34
|
+
envVar: string;
|
|
35
|
+
/** Default REST base URL. */
|
|
36
|
+
baseUrl: string;
|
|
37
|
+
/** Where the user gets a key / reads setup docs. */
|
|
38
|
+
docsUrl: string;
|
|
39
|
+
/** True for local/self-hosted providers (no key, no cost). */
|
|
40
|
+
local?: boolean;
|
|
41
|
+
/** Known model families (list-only; not fetched). */
|
|
42
|
+
models: CatalogModel[];
|
|
43
|
+
/** One-line setup hint shown in the launcher. */
|
|
44
|
+
setup: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The registry. Ordered roughly by how commonly it is the user's daily driver.
|
|
48
|
+
* Model ids are the well-known 2026 families; keep them representative rather
|
|
49
|
+
* than exhaustive — the point is a browsable, $0 menu.
|
|
50
|
+
*/
|
|
51
|
+
export declare const PROVIDER_CATALOG: CatalogProvider[];
|
|
52
|
+
export declare function getCatalogProvider(id: string): CatalogProvider | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Detect which catalog providers are usable RIGHT NOW from the environment
|
|
55
|
+
* only — no network, no tokens. Local providers (Ollama) are reported as
|
|
56
|
+
* "actionable" (installed/reachable is confirmed later, on first use).
|
|
57
|
+
*/
|
|
58
|
+
export declare function detectAvailableProviders(env?: NodeJS.ProcessEnv): Array<{
|
|
59
|
+
provider: CatalogProvider;
|
|
60
|
+
active: boolean;
|
|
61
|
+
reason: string;
|
|
62
|
+
}>;
|
|
63
|
+
/** Flat list of every catalog model tagged with its provider (for tiering). */
|
|
64
|
+
export declare function allCatalogModels(): Array<CatalogModel & {
|
|
65
|
+
provider: string;
|
|
66
|
+
maqProvider: string;
|
|
67
|
+
}>;
|