@gaia-ai/conductor 0.5.5 → 0.6.1
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/src/cli/config-schema.d.ts +89 -0
- package/dist/src/cli/config-schema.js +146 -0
- package/dist/src/cli/init.d.ts +31 -33
- package/dist/src/cli/init.js +106 -84
- package/dist/src/cli/migrate-addon-names.d.ts +72 -0
- package/dist/src/cli/migrate-addon-names.js +318 -0
- package/dist/src/cli/upgrade.d.ts +53 -0
- package/dist/src/cli/upgrade.js +222 -0
- package/dist/src/cli/version-check.js +5 -1
- package/dist/src/commands/conductor.d.ts +57 -0
- package/dist/src/{cli/gaia.js → commands/conductor.js} +107 -215
- package/dist/src/config.d.ts +65 -24
- package/dist/src/config.js +405 -154
- package/dist/src/contract.d.ts +8 -0
- package/dist/src/contract.js +16 -0
- package/dist/src/core/conductor.d.ts +16 -1
- package/dist/src/core/conductor.js +28 -9
- package/dist/src/index.d.ts +7 -5
- package/dist/src/index.js +26 -3
- package/dist/src/plugins/agent.d.ts +61 -0
- package/dist/src/plugins/agent.js +11 -0
- package/dist/src/plugins/executor.d.ts +104 -0
- package/dist/src/plugins/executor.js +1 -0
- package/dist/src/plugins/plugins.d.ts +60 -0
- package/dist/src/plugins/plugins.js +42 -0
- package/dist/src/plugins/preset.d.ts +48 -0
- package/dist/src/plugins/preset.js +23 -0
- package/dist/src/plugins/remote.d.ts +203 -0
- package/dist/src/plugins/remote.js +1 -0
- package/dist/src/plugins/workspace.d.ts +35 -0
- package/dist/src/plugins/workspace.js +1 -0
- package/dist/src/preset.d.ts +2 -0
- package/dist/src/preset.js +8 -0
- package/dist/src/types.d.ts +65 -0
- package/dist/src/types.js +1 -0
- package/package.json +8 -5
- package/dist/src/cli/gaia.d.ts +0 -23
- package/dist/src/cli/local-registry.d.ts +0 -14
- package/dist/src/cli/local-registry.js +0 -56
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { conductorId } from '@gaia-ai/core';
|
|
3
|
+
import { selectAgent } from '../plugins/plugins.js';
|
|
2
4
|
function sleep(ms, signal) {
|
|
3
5
|
return new Promise((resolve) => {
|
|
4
6
|
if (signal?.aborted) {
|
|
@@ -370,14 +372,11 @@ export class Conductor {
|
|
|
370
372
|
};
|
|
371
373
|
const env = resolveRunEnv(t.effectiveEnvVars, core, this.logger);
|
|
372
374
|
const ws = await this.workspace.ensure(t.identifier, t.branchName, baseRef);
|
|
373
|
-
// A claimable state has no
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
// engine defaults, and the gap is observable in the conductor log rather
|
|
377
|
-
// than wedging the run. `ws.instructions` IS the loaded WORKFLOW.md (null
|
|
378
|
-
// when absent), so no extra stat.
|
|
375
|
+
// A claimable state has no project policy without a WORKFLOW.md. Warn and
|
|
376
|
+
// proceed so the missing project instructions are observable without
|
|
377
|
+
// wedging the run. `ws.instructions` is the loaded file (null when absent).
|
|
379
378
|
if (!ws.instructions) {
|
|
380
|
-
this.logger.warn({ ticket: t.identifier, workspace: ws.path }, 'WORKFLOW.md missing —
|
|
379
|
+
this.logger.warn({ ticket: t.identifier, workspace: ws.path }, 'WORKFLOW.md missing — no project instructions dispatched');
|
|
381
380
|
}
|
|
382
381
|
// Lifecycle hooks are executor-owned + best-effort (GAIA-84): runHook never
|
|
383
382
|
// throws, so neither call can abort dispatch or wedge a run in `claimed`.
|
|
@@ -397,7 +396,7 @@ export class Conductor {
|
|
|
397
396
|
const prompt = ws.instructions
|
|
398
397
|
? renderPrompt(this.config.prompt, {
|
|
399
398
|
identifier: t.identifier,
|
|
400
|
-
state: t.state || '
|
|
399
|
+
state: t.state || 'qualification',
|
|
401
400
|
runUuid: run.runUuid,
|
|
402
401
|
})
|
|
403
402
|
: '';
|
|
@@ -436,8 +435,28 @@ export class Conductor {
|
|
|
436
435
|
async serve(signal) {
|
|
437
436
|
await this.pollLoop(signal);
|
|
438
437
|
}
|
|
438
|
+
/**
|
|
439
|
+
* True once this conductor's own checkout has been removed from disk (its
|
|
440
|
+
* worktree was reaped while the process kept running). Such a conductor is a
|
|
441
|
+
* pure liability: it still heartbeats and still claims runs, but every
|
|
442
|
+
* dispatch fails — no git command and no relative path can resolve from a
|
|
443
|
+
* deleted directory — so each claim burns one attempt and three of them park
|
|
444
|
+
* the ticket behind the circuit breaker. Observed as 30 orphans claiming and
|
|
445
|
+
* failing tickets they could never dispatch.
|
|
446
|
+
*/
|
|
447
|
+
checkoutGone() {
|
|
448
|
+
return !existsSync(this.checkoutRoot);
|
|
449
|
+
}
|
|
439
450
|
async pollLoop(signal) {
|
|
440
451
|
while (!signal?.aborted) {
|
|
452
|
+
if (this.checkoutGone()) {
|
|
453
|
+
// Stop claiming and let the loop end — the process exits and the cron
|
|
454
|
+
// reaper flips status to offline on lease expiry, same as any crash.
|
|
455
|
+
// Deliberately NOT an offline write from here: the loop never owns that
|
|
456
|
+
// transition (see the note at the end of this method).
|
|
457
|
+
this.logger.error({ conductorId: this.id, workspace: this.checkoutRoot }, 'checkout is gone — stopping conductor instead of claiming runs it cannot dispatch');
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
441
460
|
try {
|
|
442
461
|
await this.tick();
|
|
443
462
|
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export type * from '@gaia-ai/core';
|
|
2
|
+
export type { ConductorRegistryEntry } from '@gaia-ai/core';
|
|
2
3
|
export { conductorId } from '@gaia-ai/core';
|
|
3
|
-
export {
|
|
4
|
-
export type
|
|
5
|
-
export {
|
|
6
|
-
export type
|
|
7
|
-
export { DEFAULT_AGENT_PROMPT, loadConductorConfig } from './config.js';
|
|
4
|
+
export { renderGaiaConfig } from './cli/init.js';
|
|
5
|
+
export { type AddonRenameResult, type ConfigSurface, migrateAddonNames, migrateAddonNamesInFile, RENAMED_ADDONS, runAddonRenameMigration, } from './cli/migrate-addon-names.js';
|
|
6
|
+
export { hasProjectConnection, type ProjectConnectionChoice, runConnectionUpgrade, runUpgrade, type UpgradeReport, } from './cli/upgrade.js';
|
|
7
|
+
export { default as conductorCommandPlugin, type GaiaCliDeps, runConductorCli, } from './commands/conductor.js';
|
|
8
|
+
export { composeConductorConfig, DEFAULT_AGENT_PROMPT, loadConductorConfig, stripLegacyConnectionFromConfigSource, } from './config.js';
|
|
9
|
+
export * from './contract.js';
|
|
8
10
|
export { Conductor } from './core/conductor.js';
|
package/dist/src/index.js
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
export { conductorId } from '@gaia-ai/core';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export {
|
|
2
|
+
// GAIA-201: the connection-config template, reused by `gaia upgrade` to seed a
|
|
3
|
+
// `gaia.config.js`.
|
|
4
|
+
export { renderGaiaConfig } from './cli/init.js';
|
|
5
|
+
// GAIA-224 (Finding 8b): the addon package-name rename pass `gaia upgrade` runs
|
|
6
|
+
// as its last step (`@gaia-ai/plugin-*` + the deleted `@gaia-ai/core/builtins`
|
|
7
|
+
// and `@gaia-ai/core/plugins` → `@gaia-ai/addon-*`).
|
|
8
|
+
export { migrateAddonNames, migrateAddonNamesInFile, RENAMED_ADDONS, runAddonRenameMigration, } from './cli/migrate-addon-names.js';
|
|
9
|
+
// GAIA-216: the seed/migration routine, hoisted from the host so `conductor init`
|
|
10
|
+
// (intra-package) and `gaia upgrade` (host→engine) share one implementation.
|
|
11
|
+
export { hasProjectConnection, runConnectionUpgrade, runUpgrade, } from './cli/upgrade.js';
|
|
12
|
+
// GAIA-201: the conductor is now a COMMAND PLUGIN mounted by the `@gaia-ai/gaia`
|
|
13
|
+
// host, not the CLI entrypoint. `main`/`runGaiaCli` are gone; the default export
|
|
14
|
+
// is the `GaiaCommandPlugin`, exposed here as `./commands/conductor` too.
|
|
15
|
+
export { default as conductorCommandPlugin, runConductorCli, } from './commands/conductor.js';
|
|
16
|
+
// GAIA-218: the balanced strip is exported for the host/tests + reuse.
|
|
17
|
+
export { composeConductorConfig, DEFAULT_AGENT_PROMPT, loadConductorConfig, stripLegacyConnectionFromConfigSource, } from './config.js';
|
|
18
|
+
// GAIA-224 (Finding 6): this package now OWNS the conductor-surface contract —
|
|
19
|
+
// the interfaces, the config types, the preset view and the slot selectors — and
|
|
20
|
+
// re-exports the whole of it here. The runtime-light subpath
|
|
21
|
+
// `@gaia-ai/conductor/contract` is the one an ADDON should import (this main
|
|
22
|
+
// entry pulls the engine + the command plugin). The built-in IMPLEMENTATIONS it
|
|
23
|
+
// used to re-export from the deleted `@gaia-ai/core/plugins` barrel now live in
|
|
24
|
+
// their own addons (`@gaia-ai/addon-remote-drupal`,
|
|
25
|
+
// `@gaia-ai/addon-workspace-git`, `@gaia-ai/addon-fake`) and are deliberately NOT
|
|
26
|
+
// re-exported: the engine must not edge an addon (acyclic layer rule).
|
|
27
|
+
export * from './contract.js';
|
|
5
28
|
export { Conductor } from './core/conductor.js';
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-agent effort footprint parsed from that agent's run transcript
|
|
3
|
+
* (GAIA-132). The metrics are transcript-derived and therefore
|
|
4
|
+
* **agent-specific** (each agent's transcript has its own format), so parsing
|
|
5
|
+
* lives behind the agent abstraction (see {@link GaiaAgent.parseFootprint}) —
|
|
6
|
+
* the conductor stays agent-agnostic and writes the footprint verbatim.
|
|
7
|
+
* `duration_s` is likewise derived from the transcript (its first→last entry
|
|
8
|
+
* timestamps), NOT recomputed by the conductor from a re-read `started_at`
|
|
9
|
+
* (GAIA-151): the log is the single source of the run's wall-clock length, so
|
|
10
|
+
* there is no timestamp round-trip through JSON:API to mis-parse.
|
|
11
|
+
*/
|
|
12
|
+
export interface AgentFootprint {
|
|
13
|
+
/** Total tokens across every usage bucket of every assistant turn. */
|
|
14
|
+
tokens: number;
|
|
15
|
+
/**
|
|
16
|
+
* Wall-clock run length in seconds, derived from the transcript's first→last
|
|
17
|
+
* entry timestamps (GAIA-151). 0 when the transcript has fewer than two
|
|
18
|
+
* timestamped entries (empty/absent log).
|
|
19
|
+
*/
|
|
20
|
+
duration_s: number;
|
|
21
|
+
/** Number of assistant turns in the transcript. */
|
|
22
|
+
agent_turns: number;
|
|
23
|
+
/** Number of tool-use calls across all assistant turns. */
|
|
24
|
+
tool_calls: number;
|
|
25
|
+
/** Number of user-submitted prompts. */
|
|
26
|
+
user_prompts: number;
|
|
27
|
+
/** Total words across those user prompts. */
|
|
28
|
+
user_prompt_words: number;
|
|
29
|
+
/** Model of the last assistant turn, when present (informational). */
|
|
30
|
+
model?: string;
|
|
31
|
+
}
|
|
32
|
+
/** An all-zero footprint — the honest result for an empty/absent transcript. */
|
|
33
|
+
export declare function emptyAgentFootprint(): AgentFootprint;
|
|
34
|
+
/**
|
|
35
|
+
* A GAIA agent: the program the conductor runs to work a ticket (e.g. claude).
|
|
36
|
+
* It knows how it is launched (CLI + model + flags) and where its per-run
|
|
37
|
+
* transcript/log lives — so the conductor stays agent-agnostic and the CLI can
|
|
38
|
+
* attach the run log on release without agent-specific knowledge.
|
|
39
|
+
*/
|
|
40
|
+
export interface GaiaAgent {
|
|
41
|
+
/** Stable id, e.g. 'claude'. */
|
|
42
|
+
readonly id: string;
|
|
43
|
+
/**
|
|
44
|
+
* Build the full agent CLI invocation for a GAIA prompt. With an empty
|
|
45
|
+
* prompt, returns the bare launch command (no prompt argument).
|
|
46
|
+
*/
|
|
47
|
+
launchCommand(prompt: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* Locate and read this agent's run transcript for a run that executed in
|
|
50
|
+
* `worktreePath`. MUST return '' (never throw) when nothing is found.
|
|
51
|
+
*/
|
|
52
|
+
getRunLog(worktreePath: string): Promise<string>;
|
|
53
|
+
/**
|
|
54
|
+
* Parse this agent's run transcript (as returned by {@link getRunLog}) into
|
|
55
|
+
* a footprint of effort metrics (GAIA-132). The transcript format is
|
|
56
|
+
* agent-specific, so each agent owns its own parser. MUST be tolerant of
|
|
57
|
+
* blank/partial/absent input (never throw) — an empty log yields
|
|
58
|
+
* {@link emptyAgentFootprint}.
|
|
59
|
+
*/
|
|
60
|
+
parseFootprint(log: string): AgentFootprint;
|
|
61
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export interface SpawnedSession {
|
|
2
|
+
sessionRef: string;
|
|
3
|
+
}
|
|
4
|
+
export interface ExecutorCapabilities {
|
|
5
|
+
persistent: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface SpawnRunInput {
|
|
8
|
+
ticket: {
|
|
9
|
+
uuid: string;
|
|
10
|
+
identifier: string;
|
|
11
|
+
title: string;
|
|
12
|
+
branchName: string;
|
|
13
|
+
state: string;
|
|
14
|
+
url?: string;
|
|
15
|
+
};
|
|
16
|
+
run: {
|
|
17
|
+
uuid: string;
|
|
18
|
+
id: number;
|
|
19
|
+
handler: string;
|
|
20
|
+
};
|
|
21
|
+
workspacePath: string;
|
|
22
|
+
instructions: {
|
|
23
|
+
path: string;
|
|
24
|
+
sha256: string;
|
|
25
|
+
text: string;
|
|
26
|
+
} | null;
|
|
27
|
+
command?: string;
|
|
28
|
+
env?: Record<string, string>;
|
|
29
|
+
}
|
|
30
|
+
/** The four lifecycle-hook slots, keyed exactly like `config.hooks`. */
|
|
31
|
+
export type HookName = 'after_create' | 'before_run' | 'after_run' | 'after_done';
|
|
32
|
+
/** Context for a hook invocation — carries the ticket for observable logging. */
|
|
33
|
+
export interface HookContext {
|
|
34
|
+
/** The ticket identifier/uuid the hook is running for (log context). */
|
|
35
|
+
ticket: string;
|
|
36
|
+
}
|
|
37
|
+
export interface GaiaExecutor {
|
|
38
|
+
id: string;
|
|
39
|
+
capabilities(): ExecutorCapabilities;
|
|
40
|
+
/**
|
|
41
|
+
* Run the lifecycle hook `name` (command from `config.hooks[name]`) in `cwd`,
|
|
42
|
+
* best-effort. This is the SINGLE catch point for all lifecycle hooks:
|
|
43
|
+
*
|
|
44
|
+
* - MUST NEVER throw. No configured command → silent no-op. A failing command
|
|
45
|
+
* → `logger.error({hook,worktree,ticket,err}, 'lifecycle hook failed')` then
|
|
46
|
+
* return. So a hook failure never aborts dispatch or wedges a run in
|
|
47
|
+
* `claimed`.
|
|
48
|
+
* - The underlying shell command still fails honestly (a non-zero exit
|
|
49
|
+
* rejects); only the executor catches + logs + continues.
|
|
50
|
+
*
|
|
51
|
+
* `env`, when given, is the run's resolved environment (per-ticket env_vars
|
|
52
|
+
* merged with the core GAIA_* vars, GAIA-99), merged over the hook process's
|
|
53
|
+
* inherited env. Values may be sensitive — implementations must never log
|
|
54
|
+
* them (log key names only).
|
|
55
|
+
*/
|
|
56
|
+
runHook(name: HookName, cwd: string, ctx: HookContext, env?: Record<string, string>): Promise<void>;
|
|
57
|
+
startRun(input: SpawnRunInput): Promise<SpawnedSession>;
|
|
58
|
+
/**
|
|
59
|
+
* Signal the run's agent to stop. Called by the conductor's run-finalise pass
|
|
60
|
+
* BEFORE it captures the agent transcript (GAIA-132), so a stop must not
|
|
61
|
+
* destroy the log. For herdr this is a NO-OP: at finalise time the agent is
|
|
62
|
+
* idle and its jsonl transcript is already fully written to disk, and the
|
|
63
|
+
* subsequent {@link cleanupRun} tab-close kills the PTY (the agent dies with
|
|
64
|
+
* it). The seam exists for executors whose agent outlives its UI surface.
|
|
65
|
+
* Best-effort; gated by the conductor on `capabilities().persistent`.
|
|
66
|
+
*/
|
|
67
|
+
stopRun(branch: string): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Tear down ONE run's hosted UI surface — for herdr, close only the tab whose
|
|
70
|
+
* label carries the `#<runId>` token (`startRun` labels every tab
|
|
71
|
+
* `<identifier> · <state> #<run.id>`), via `herdr tab close <tab_id>` (killing
|
|
72
|
+
* that PTY). Run-scoped by design (GAIA-183): a sibling run's tab in the same
|
|
73
|
+
* branch workspace — a still-open prior-state run or a concurrent re-claim —
|
|
74
|
+
* and any non-run tab are left untouched. Called as the LAST finalisation step,
|
|
75
|
+
* strictly after the transcript is captured, and again at dispatch to clear a
|
|
76
|
+
* reused workspace's leftover tab for the run being (re-)started. No matching
|
|
77
|
+
* tab → a quiet no-op. Does NOT touch the branch worktree (that is the
|
|
78
|
+
* reap/{@link removeWorktree} lifecycle). Best-effort: a failing tab-close is
|
|
79
|
+
* swallowed and never aborts finalisation or dispatch. A no-op for
|
|
80
|
+
* non-persistent executors.
|
|
81
|
+
*/
|
|
82
|
+
cleanupRun(branch: string, runId: number): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Tear down the branch's entire worktree (git worktree + hosted workspace),
|
|
85
|
+
* reclaiming its disk. Called by the conductor's ticket-cleanup pass once a
|
|
86
|
+
* ticket is done. A no-op for non-persistent executors (no hosted workspace);
|
|
87
|
+
* the conductor gates the call on `capabilities().persistent`.
|
|
88
|
+
*
|
|
89
|
+
* `worktreePath` is the stable, identifier-derived worktree path (from the
|
|
90
|
+
* ticket's latest run). Prefer it over `branch` to resolve the worktree: the
|
|
91
|
+
* checked-out branch is mutable (the coding agent may rename/switch it), the
|
|
92
|
+
* path is not.
|
|
93
|
+
*
|
|
94
|
+
* Returns whether the worktree was actually present on THIS host and torn
|
|
95
|
+
* down (or reclaimed on disk) — i.e. whether the teardown happened locally.
|
|
96
|
+
* `false` means nothing matched here: the worktree is either already gone or
|
|
97
|
+
* lives on another conductor's host. The reaper uses this to avoid marking a
|
|
98
|
+
* ticket `cleaned_up` for a worktree it did not actually tear down (a
|
|
99
|
+
* cross-host false-teardown), leaving it for the host that physically holds
|
|
100
|
+
* it. A genuine failure still throws (surfaced as a teardown miss); a `false`
|
|
101
|
+
* return is a clean "not here", not an error.
|
|
102
|
+
*/
|
|
103
|
+
removeWorktree(branch: string, worktreePath?: string): Promise<boolean>;
|
|
104
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ConductorLogger } from '@gaia-ai/core';
|
|
2
|
+
import type { DropSHPlugin } from 'dropsh/plugin';
|
|
3
|
+
import type { ConductorFileConfig } from '../types.js';
|
|
4
|
+
import type { GaiaAgent } from './agent.js';
|
|
5
|
+
import type { GaiaExecutor } from './executor.js';
|
|
6
|
+
import type { GaiaRemote, Ticket } from './remote.js';
|
|
7
|
+
import type { GaiaWorkspace } from './workspace.js';
|
|
8
|
+
/** Runtime dependencies handed to a plugin factory at selection time. */
|
|
9
|
+
export interface ExecutorDeps {
|
|
10
|
+
/** The in-scope conductor logger — the executor logs hook failures through it. */
|
|
11
|
+
logger: ConductorLogger;
|
|
12
|
+
}
|
|
13
|
+
export interface RemotePlugin extends DropSHPlugin {
|
|
14
|
+
readonly kind: 'remote';
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly requiredModules: string[];
|
|
17
|
+
createRemote(config: ConductorFileConfig): Promise<GaiaRemote>;
|
|
18
|
+
}
|
|
19
|
+
export interface ExecutorPlugin extends DropSHPlugin {
|
|
20
|
+
readonly kind: 'executor';
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly requiredModules: string[];
|
|
23
|
+
createExecutor(config: ConductorFileConfig, deps: ExecutorDeps): Promise<GaiaExecutor>;
|
|
24
|
+
}
|
|
25
|
+
export interface WorkspacePlugin extends DropSHPlugin {
|
|
26
|
+
readonly kind: 'workspace';
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly requiredModules: string[];
|
|
29
|
+
createWorkspace(config: ConductorFileConfig): Promise<GaiaWorkspace>;
|
|
30
|
+
}
|
|
31
|
+
export interface AgentPlugin extends DropSHPlugin {
|
|
32
|
+
readonly kind: 'agent';
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly requiredModules: string[];
|
|
35
|
+
createAgent(config: ConductorFileConfig): Promise<GaiaAgent>;
|
|
36
|
+
}
|
|
37
|
+
/** Resolves the remote from its named config slot. */
|
|
38
|
+
export declare function selectRemote(config: ConductorFileConfig): Promise<GaiaRemote>;
|
|
39
|
+
/** Resolves the executor from its named config slot, injecting the logger. */
|
|
40
|
+
export declare function selectExecutor(config: ConductorFileConfig, logger: ConductorLogger): Promise<GaiaExecutor>;
|
|
41
|
+
/** Resolves the workspace from its named config slot. */
|
|
42
|
+
export declare function selectWorkspace(config: ConductorFileConfig): Promise<GaiaWorkspace>;
|
|
43
|
+
/** A config-side agent choice: an agent plugin + an optional static priority over the ticket. */
|
|
44
|
+
export interface AgentCandidate {
|
|
45
|
+
agent: AgentPlugin;
|
|
46
|
+
priority?: (ticket: Ticket) => number;
|
|
47
|
+
}
|
|
48
|
+
/** A constructed candidate: the agent id, its GaiaAgent, and the config-side priority. */
|
|
49
|
+
export interface ResolvedAgent {
|
|
50
|
+
id: string;
|
|
51
|
+
agent: GaiaAgent;
|
|
52
|
+
priority?: (ticket: Ticket) => number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Pick the agent for a ticket: highest `priority(ticket)` wins; a candidate with
|
|
56
|
+
* no `priority` scores lowest; ties resolve by config array order (stable sort).
|
|
57
|
+
*/
|
|
58
|
+
export declare function selectAgent(candidates: ResolvedAgent[], ticket: Ticket): ResolvedAgent;
|
|
59
|
+
/** Constructs every agent candidate from the resolved config slot (single or array). */
|
|
60
|
+
export declare function selectAgents(config: ConductorFileConfig): Promise<ResolvedAgent[]>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Resolves the remote from its named config slot. */
|
|
2
|
+
export async function selectRemote(config) {
|
|
3
|
+
return config.remote.createRemote(config);
|
|
4
|
+
}
|
|
5
|
+
/** Resolves the executor from its named config slot, injecting the logger. */
|
|
6
|
+
export async function selectExecutor(config, logger) {
|
|
7
|
+
return config.executor.createExecutor(config, { logger });
|
|
8
|
+
}
|
|
9
|
+
/** Resolves the workspace from its named config slot. */
|
|
10
|
+
export async function selectWorkspace(config) {
|
|
11
|
+
return config.workspace.createWorkspace(config);
|
|
12
|
+
}
|
|
13
|
+
/** A missing `priority` scores strictly below any real number (AC-4). */
|
|
14
|
+
const DEFAULT_PRIORITY = Number.NEGATIVE_INFINITY;
|
|
15
|
+
/**
|
|
16
|
+
* Pick the agent for a ticket: highest `priority(ticket)` wins; a candidate with
|
|
17
|
+
* no `priority` scores lowest; ties resolve by config array order (stable sort).
|
|
18
|
+
*/
|
|
19
|
+
export function selectAgent(candidates, ticket) {
|
|
20
|
+
const best = candidates
|
|
21
|
+
.map((c, index) => ({
|
|
22
|
+
c,
|
|
23
|
+
index,
|
|
24
|
+
score: c.priority ? c.priority(ticket) : DEFAULT_PRIORITY,
|
|
25
|
+
}))
|
|
26
|
+
.sort((a, b) => b.score - a.score || a.index - b.index)[0];
|
|
27
|
+
if (!best) {
|
|
28
|
+
throw new Error('selectAgent: no agent candidates configured');
|
|
29
|
+
}
|
|
30
|
+
return best.c;
|
|
31
|
+
}
|
|
32
|
+
/** Constructs every agent candidate from the resolved config slot (single or array). */
|
|
33
|
+
export async function selectAgents(config) {
|
|
34
|
+
const candidates = Array.isArray(config.agent)
|
|
35
|
+
? config.agent
|
|
36
|
+
: [config.agent];
|
|
37
|
+
return Promise.all(candidates.map(async (c) => ({
|
|
38
|
+
id: c.agent.id,
|
|
39
|
+
agent: await c.agent.createAgent(config),
|
|
40
|
+
...(c.priority ? { priority: c.priority } : {}),
|
|
41
|
+
})));
|
|
42
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { AddonEntry, DiscoveredContributions, GaiaPreset } from '@gaia-ai/core';
|
|
2
|
+
import type { AgentCandidate, ExecutorPlugin, RemotePlugin, WorkspacePlugin } from './plugins.js';
|
|
3
|
+
import type { Ticket } from './remote.js';
|
|
4
|
+
/**
|
|
5
|
+
* The conductor surface's concrete contributions — the strongly-typed view of
|
|
6
|
+
* the four conductor accumulators `discoverAddons(entries, 'conductor', …)`
|
|
7
|
+
* fills. Core returns them as opaque `unknown[]`; the engine narrows once, at
|
|
8
|
+
* the discovery call site (see {@link narrowConductorContributions}).
|
|
9
|
+
*/
|
|
10
|
+
export interface ConductorContributions {
|
|
11
|
+
remotes: RemotePlugin[];
|
|
12
|
+
executors: ExecutorPlugin[];
|
|
13
|
+
workspaces: WorkspacePlugin[];
|
|
14
|
+
agents: AgentCandidate[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* One `addons[]` entry on the conductor surface: core's surface-agnostic
|
|
18
|
+
* `AddonEntry` with the agent `priority` narrowed to the real `Ticket`.
|
|
19
|
+
*/
|
|
20
|
+
export type ConductorAddonEntry = string | (Exclude<AddonEntry, string> & {
|
|
21
|
+
priority?: (ticket: Ticket) => number;
|
|
22
|
+
});
|
|
23
|
+
/**
|
|
24
|
+
* A preset as a CONDUCTOR-surface addon writes it: core's `GaiaPreset` with the
|
|
25
|
+
* four conductor accumulators concretely typed.
|
|
26
|
+
*
|
|
27
|
+
* It literally `extends GaiaPreset`, so the compiler — not a comment — enforces
|
|
28
|
+
* that this view stays a faithful narrowing of the kernel contract. That only
|
|
29
|
+
* type-checks because core's `OpaqueAccumulator` takes `never[]` on the input
|
|
30
|
+
* side: `never[]` is assignable to `RemotePlugin[]`, so the concrete member below
|
|
31
|
+
* satisfies the opaque one contravariantly. If core ever widened that parameter
|
|
32
|
+
* to `unknown[]`, this `extends` would fail loudly here instead of the two views
|
|
33
|
+
* silently drifting apart.
|
|
34
|
+
*/
|
|
35
|
+
export interface Preset<O = unknown> extends GaiaPreset<O> {
|
|
36
|
+
remotes?: (acc: RemotePlugin[], opts: O) => RemotePlugin[] | Promise<RemotePlugin[]>;
|
|
37
|
+
executors?: (acc: ExecutorPlugin[], opts: O) => ExecutorPlugin[] | Promise<ExecutorPlugin[]>;
|
|
38
|
+
workspaces?: (acc: WorkspacePlugin[], opts: O) => WorkspacePlugin[] | Promise<WorkspacePlugin[]>;
|
|
39
|
+
agents?: (acc: AgentCandidate[], opts: O) => AgentCandidate[] | Promise<AgentCandidate[]>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The single, reviewed narrowing at the engine's discovery seam: reinterpret
|
|
43
|
+
* core's opaque conductor accumulators as this surface's concrete contribution
|
|
44
|
+
* lists. Safe because every conductor-surface preset is typed against
|
|
45
|
+
* {@link Preset}, and `resolveConductorSlots` still runs the runtime `.kind`
|
|
46
|
+
* guard on whatever actually landed in a singleton slot.
|
|
47
|
+
*/
|
|
48
|
+
export declare function narrowConductorContributions(discovered: Pick<DiscoveredContributions, 'remotes' | 'executors' | 'workspaces' | 'agents'>): ConductorContributions;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// GAIA-224 (Finding 6, decision 7): the CONDUCTOR-SURFACE view of the preset
|
|
2
|
+
// contract. `@gaia-ai/core` owns the surface-AGNOSTIC machinery — it knows the
|
|
3
|
+
// accumulator NAMES (`SURFACE_KEYS`) and threads them (`discoverAddons`), but its
|
|
4
|
+
// per-surface accumulators are OPAQUE (`OpaqueAccumulator`), because the element
|
|
5
|
+
// types of the conductor surface (`RemotePlugin`, `ExecutorPlugin`,
|
|
6
|
+
// `WorkspacePlugin`, `AgentCandidate`) live HERE, in the package that owns the
|
|
7
|
+
// surface. Without that split, `core/src/plugins/preset.ts` would have to import
|
|
8
|
+
// the conductor contract and core would edge `@gaia-ai/conductor` — breaking the
|
|
9
|
+
// "core has zero @gaia-ai/* edges" rule.
|
|
10
|
+
//
|
|
11
|
+
// A conductor-surface addon therefore types its `./preset` accumulators against
|
|
12
|
+
// THIS `Preset` (e.g. `export const agents: Preset['agents'] = …`) instead of
|
|
13
|
+
// core's `GaiaPreset`, and stays structurally assignable to it.
|
|
14
|
+
/**
|
|
15
|
+
* The single, reviewed narrowing at the engine's discovery seam: reinterpret
|
|
16
|
+
* core's opaque conductor accumulators as this surface's concrete contribution
|
|
17
|
+
* lists. Safe because every conductor-surface preset is typed against
|
|
18
|
+
* {@link Preset}, and `resolveConductorSlots` still runs the runtime `.kind`
|
|
19
|
+
* guard on whatever actually landed in a singleton slot.
|
|
20
|
+
*/
|
|
21
|
+
export function narrowConductorContributions(discovered) {
|
|
22
|
+
return discovered;
|
|
23
|
+
}
|