@gaia-ai/conductor 0.5.0 → 0.5.4
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/src/cli/gaia.js +3 -2
- package/dist/src/config.d.ts +9 -7
- package/dist/src/config.js +42 -15
- package/dist/src/core/conductor.js +47 -20
- package/package.json +2 -2
package/dist/src/cli/gaia.js
CHANGED
|
@@ -396,8 +396,9 @@ export function buildProgram(deps) {
|
|
|
396
396
|
program
|
|
397
397
|
.name('gaia')
|
|
398
398
|
.description('GAIA conductor + client CLI')
|
|
399
|
-
.option('--conductor <name>', 'select a
|
|
400
|
-
// GAIA-126: a repo may hold several
|
|
399
|
+
.option('--conductor <name>', 'select a conductor by stem (conductor→conductor.config.js, else <name>.conductor.config.js; default: conductor.config.js, else the sole config; env $GAIA_CONDUCTOR)');
|
|
400
|
+
// GAIA-126/137: a repo may hold several .gaia/ conductor configs — the default
|
|
401
|
+
// conductor.config.js plus <stem>.conductor.config.js variants.
|
|
401
402
|
// resolveConfigPath already honours $GAIA_CONDUCTOR; thread the global
|
|
402
403
|
// --conductor flag into it (flag wins over env) so every command selects the
|
|
403
404
|
// named config without per-command wiring. A per-invocation flag beats any
|
package/dist/src/config.d.ts
CHANGED
|
@@ -5,9 +5,9 @@ import type { AgentCandidate, ConductorFileConfig } from '@gaia-ai/core';
|
|
|
5
5
|
* is closed automatically when the ticket state changes on the next claim - the
|
|
6
6
|
* agent does not release it. Run mechanics live here (not in the repo's
|
|
7
7
|
* WORKFLOW.md). The prompt routes through the gaia skill (`ticket:run`) rather
|
|
8
|
-
* than pointing at WORKFLOW.md directly (GAIA-125): the intake
|
|
8
|
+
* than pointing at WORKFLOW.md directly (GAIA-125): the intake line + state
|
|
9
9
|
* engine are a skill mechanic, so a bare "follow WORKFLOW.md" pointer left the
|
|
10
|
-
*
|
|
10
|
+
* intake line unprinted unless an external skill-forcing hook happened to fire. A
|
|
11
11
|
* conductor config may override via the `prompt` field.
|
|
12
12
|
*
|
|
13
13
|
* The ticket + its comments are NOT embedded (GAIA-112): embedding unbounded
|
|
@@ -47,12 +47,14 @@ export declare function resolveAgents(raw: unknown, configPath: string): Promise
|
|
|
47
47
|
* Resolve the conductor config path from `cwd`.
|
|
48
48
|
*
|
|
49
49
|
* 1. An explicit `--config` / `$GAIA_CONDUCTOR_CONFIG` wins verbatim (no walk).
|
|
50
|
-
* 2. Otherwise walk root-ward to the nearest `.gaia/` dir holding
|
|
50
|
+
* 2. Otherwise walk root-ward to the nearest `.gaia/` dir holding a conductor config
|
|
51
51
|
* — so any subdirectory of a project/worktree resolves the same dir.
|
|
52
|
-
* - A `--conductor <name>` / `$GAIA_CONDUCTOR` selector resolves
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
52
|
+
* - A `--conductor <name>` / `$GAIA_CONDUCTOR` selector resolves the stem's
|
|
53
|
+
* file (`conductor` → `conductor.config.js`, else
|
|
54
|
+
* `<name>.conductor.config.js`; error listing the stems if absent).
|
|
55
|
+
* - No selector: `conductor.config.js` present → it is the default; else
|
|
56
|
+
* exactly one config → use it (back-compat); else → error naming the
|
|
57
|
+
* stems + the selector.
|
|
56
58
|
* 3. No `.gaia/` config anywhere up the tree → an actionable `gaia init` error
|
|
57
59
|
* (never leak a raw "Cannot find module" from a later import()).
|
|
58
60
|
*/
|
package/dist/src/config.js
CHANGED
|
@@ -8,9 +8,9 @@ import { pathToFileURL } from 'node:url';
|
|
|
8
8
|
* is closed automatically when the ticket state changes on the next claim - the
|
|
9
9
|
* agent does not release it. Run mechanics live here (not in the repo's
|
|
10
10
|
* WORKFLOW.md). The prompt routes through the gaia skill (`ticket:run`) rather
|
|
11
|
-
* than pointing at WORKFLOW.md directly (GAIA-125): the intake
|
|
11
|
+
* than pointing at WORKFLOW.md directly (GAIA-125): the intake line + state
|
|
12
12
|
* engine are a skill mechanic, so a bare "follow WORKFLOW.md" pointer left the
|
|
13
|
-
*
|
|
13
|
+
* intake line unprinted unless an external skill-forcing hook happened to fire. A
|
|
14
14
|
* conductor config may override via the `prompt` field.
|
|
15
15
|
*
|
|
16
16
|
* The ticket + its comments are NOT embedded (GAIA-112): embedding unbounded
|
|
@@ -40,7 +40,7 @@ export const DEFAULT_AGENT_PROMPT = `You are a GAIA agent working ticket {identi
|
|
|
40
40
|
`Invoke the gaia skill and run \`ticket:run {identifier} {state}\` — it first ` +
|
|
41
41
|
`reads the ticket + all comments ` +
|
|
42
42
|
`(gaia dropsh read gaia_ticket/gaia_ticket/$GAIA_ID --include comments), ` +
|
|
43
|
-
`
|
|
43
|
+
`prints an intake line, runs the {state} engine, and applies ` +
|
|
44
44
|
`WORKFLOW.md's {state} policy. Do ONLY the {state} work — never start or ` +
|
|
45
45
|
`prepare a later state.`;
|
|
46
46
|
function requirePlugin(value, kind) {
|
|
@@ -179,18 +179,36 @@ function optionalPositiveInteger(value, fallback, key) {
|
|
|
179
179
|
}
|
|
180
180
|
return value;
|
|
181
181
|
}
|
|
182
|
-
/** A repo's config files live in this dir, one
|
|
182
|
+
/** A repo's config files live in this dir, one conductor config per conductor. */
|
|
183
183
|
const GAIA_DIR = '.gaia';
|
|
184
|
-
/**
|
|
184
|
+
/** The default conductor's file name; its stem is `conductor`. */
|
|
185
|
+
const DEFAULT_CONFIG = 'conductor.config.js';
|
|
186
|
+
/** Variant files are `<stem>.conductor.config.js`. */
|
|
187
|
+
const VARIANT_SUFFIX = '.conductor.config.js';
|
|
188
|
+
/**
|
|
189
|
+
* List the conductor-config stems in a `.gaia/` dir. A file is a conductor
|
|
190
|
+
* config iff it is exactly `conductor.config.js` (stem `conductor`, the
|
|
191
|
+
* default) or ends with `.conductor.config.js` (stem = the leading part).
|
|
192
|
+
* Every other file (`vite.config.js`, the near-miss `myconductor.config.js`)
|
|
193
|
+
* is ignored.
|
|
194
|
+
*/
|
|
185
195
|
function configStems(gaiaDir) {
|
|
186
196
|
return readdirSync(gaiaDir)
|
|
187
|
-
.
|
|
188
|
-
|
|
197
|
+
.map((f) => f === DEFAULT_CONFIG
|
|
198
|
+
? 'conductor'
|
|
199
|
+
: f.endsWith(VARIANT_SUFFIX)
|
|
200
|
+
? f.slice(0, -VARIANT_SUFFIX.length)
|
|
201
|
+
: undefined)
|
|
202
|
+
.filter((s) => s !== undefined)
|
|
189
203
|
.sort();
|
|
190
204
|
}
|
|
205
|
+
/** Map a conductor stem back to its file name. */
|
|
206
|
+
function fileForStem(stem) {
|
|
207
|
+
return stem === 'conductor' ? DEFAULT_CONFIG : `${stem}${VARIANT_SUFFIX}`;
|
|
208
|
+
}
|
|
191
209
|
/**
|
|
192
210
|
* Walk from `cwd` root-ward (git/eslint style) to the nearest ancestor whose
|
|
193
|
-
* `.gaia/` dir holds at least one
|
|
211
|
+
* `.gaia/` dir holds at least one conductor config; return that `.gaia/` dir, or
|
|
194
212
|
* `undefined` if none is found up to the filesystem root.
|
|
195
213
|
*/
|
|
196
214
|
function findGaiaDir(cwd) {
|
|
@@ -211,12 +229,14 @@ function findGaiaDir(cwd) {
|
|
|
211
229
|
* Resolve the conductor config path from `cwd`.
|
|
212
230
|
*
|
|
213
231
|
* 1. An explicit `--config` / `$GAIA_CONDUCTOR_CONFIG` wins verbatim (no walk).
|
|
214
|
-
* 2. Otherwise walk root-ward to the nearest `.gaia/` dir holding
|
|
232
|
+
* 2. Otherwise walk root-ward to the nearest `.gaia/` dir holding a conductor config
|
|
215
233
|
* — so any subdirectory of a project/worktree resolves the same dir.
|
|
216
|
-
* - A `--conductor <name>` / `$GAIA_CONDUCTOR` selector resolves
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
234
|
+
* - A `--conductor <name>` / `$GAIA_CONDUCTOR` selector resolves the stem's
|
|
235
|
+
* file (`conductor` → `conductor.config.js`, else
|
|
236
|
+
* `<name>.conductor.config.js`; error listing the stems if absent).
|
|
237
|
+
* - No selector: `conductor.config.js` present → it is the default; else
|
|
238
|
+
* exactly one config → use it (back-compat); else → error naming the
|
|
239
|
+
* stems + the selector.
|
|
220
240
|
* 3. No `.gaia/` config anywhere up the tree → an actionable `gaia init` error
|
|
221
241
|
* (never leak a raw "Cannot find module" from a later import()).
|
|
222
242
|
*/
|
|
@@ -232,14 +252,21 @@ export function resolveConfigPath(override, cwd = process.cwd(), conductorName)
|
|
|
232
252
|
const stems = configStems(gaiaDir);
|
|
233
253
|
const name = conductorName ?? process.env.GAIA_CONDUCTOR;
|
|
234
254
|
if (name !== undefined && name !== '') {
|
|
235
|
-
const candidate = join(gaiaDir,
|
|
255
|
+
const candidate = join(gaiaDir, fileForStem(name));
|
|
236
256
|
if (!existsSync(candidate)) {
|
|
237
257
|
throw new Error(`no conductor '${name}' in ${gaiaDir}; available: ${stems.join(', ')}`);
|
|
238
258
|
}
|
|
239
259
|
return candidate;
|
|
240
260
|
}
|
|
261
|
+
// No selector: the file named conductor.config.js is the default (AC-1, AC-2).
|
|
262
|
+
// Two conductor.config.js files cannot coexist in one dir, so ">1 default"
|
|
263
|
+
// (AC-5) is structurally impossible — no ambiguity branch is needed.
|
|
264
|
+
if (stems.includes('conductor')) {
|
|
265
|
+
return join(gaiaDir, DEFAULT_CONFIG);
|
|
266
|
+
}
|
|
267
|
+
// Back-compat: a lone config resolves even without the default name.
|
|
241
268
|
if (stems.length === 1) {
|
|
242
|
-
return join(gaiaDir,
|
|
269
|
+
return join(gaiaDir, fileForStem(stems[0]));
|
|
243
270
|
}
|
|
244
271
|
throw new Error(`${stems.length} conductors in ${gaiaDir} (${stems.join(', ')}); ` +
|
|
245
272
|
'select one with --conductor <name> or $GAIA_CONDUCTOR');
|
|
@@ -141,16 +141,24 @@ export class Conductor {
|
|
|
141
141
|
await this.dispatch(run);
|
|
142
142
|
}
|
|
143
143
|
catch (err) {
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
// `
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
144
|
+
// GAIA-149: terminalise the run to `failed` with the error captured,
|
|
145
|
+
// instead of leaving it `claimed` to expire after the ~300s lease (the
|
|
146
|
+
// old silent stall). `failed` is a terminal sibling of `expired`
|
|
147
|
+
// (dispatch/setup error vs lease lapse); it does NOT reopen the ticket
|
|
148
|
+
// the way the GAIA-41-era fabricated `done` did, so there is no runaway.
|
|
149
|
+
// Marking it now frees the ticket immediately and records *why* it
|
|
150
|
+
// failed; a persistently-failing setup is then parked by the server-side
|
|
151
|
+
// circuit breaker (TicketClaimService::assertNotCircuitBroken) after the
|
|
152
|
+
// retry cap, not by a hot re-dispatch loop. Best-effort: a markFailed
|
|
153
|
+
// that itself errors must not stop the claim loop, so it is swallowed
|
|
154
|
+
// (the run then falls back to lease-expiry recovery).
|
|
153
155
|
this.logger.error({ run: run.runUuid, err: String(err) }, 'dispatch failed');
|
|
156
|
+
try {
|
|
157
|
+
await this.remote.markFailed(run.runUuid, String(err));
|
|
158
|
+
}
|
|
159
|
+
catch (markErr) {
|
|
160
|
+
this.logger.warn({ run: run.runUuid, err: String(markErr) }, 'markFailed failed');
|
|
161
|
+
}
|
|
154
162
|
}
|
|
155
163
|
count += 1;
|
|
156
164
|
claimed += 1;
|
|
@@ -176,15 +184,16 @@ export class Conductor {
|
|
|
176
184
|
const runs = await this.remote.fetchFinalizableRuns(this.id);
|
|
177
185
|
for (const r of runs) {
|
|
178
186
|
try {
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
187
|
+
// Signal the agent to stop before capturing its log. Best-effort and
|
|
188
|
+
// gated on the persistent capability (only hosted executors have a live
|
|
189
|
+
// agent) — mirrors removeWorktree's gate. For herdr this is a no-op
|
|
190
|
+
// (the tab-close in cleanupRun kills the PTY); a stop failure must
|
|
191
|
+
// never block finalisation.
|
|
183
192
|
if (this.executor.capabilities().persistent) {
|
|
184
193
|
const branch = await this.remote.getRunTicketBranchName(r.runUuid);
|
|
185
194
|
if (branch) {
|
|
186
195
|
try {
|
|
187
|
-
await this.executor.
|
|
196
|
+
await this.executor.stopRun(branch);
|
|
188
197
|
}
|
|
189
198
|
catch (err) {
|
|
190
199
|
this.logger.warn({ run: r.runUuid, err: String(err) }, 'agent stop failed');
|
|
@@ -202,14 +211,16 @@ export class Conductor {
|
|
|
202
211
|
? await resolved.agent.getRunLog(r.worktreePath)
|
|
203
212
|
: '';
|
|
204
213
|
// Footprint (GAIA-132): the agent parses its own transcript for the
|
|
205
|
-
// effort metrics
|
|
206
|
-
//
|
|
207
|
-
//
|
|
214
|
+
// effort metrics AND its wall-clock duration_s (from the transcript's
|
|
215
|
+
// first→last timestamps — GAIA-151). Deriving duration from the log
|
|
216
|
+
// keeps it a single source with one format, avoiding the earlier
|
|
217
|
+
// `started_at` re-read that mis-parsed the JSON:API ISO string to 0.
|
|
218
|
+
// The conductor owns the run, so this write always lands (the agent
|
|
219
|
+
// session could not).
|
|
208
220
|
const parsed = resolved.agent.parseFootprint(log);
|
|
209
|
-
const now = Math.floor(Date.now() / 1000);
|
|
210
221
|
const metrics = {
|
|
211
222
|
tokens: parsed.tokens,
|
|
212
|
-
duration_s:
|
|
223
|
+
duration_s: parsed.duration_s,
|
|
213
224
|
agent_turns: parsed.agent_turns,
|
|
214
225
|
tool_calls: parsed.tool_calls,
|
|
215
226
|
user_prompts: parsed.user_prompts,
|
|
@@ -217,6 +228,21 @@ export class Conductor {
|
|
|
217
228
|
};
|
|
218
229
|
await this.remote.finalizeRun(r.runUuid, log, metrics);
|
|
219
230
|
this.logger.info({ run: r.runUuid, footprint: metrics }, 'run finalised');
|
|
231
|
+
// Tab-close is the LAST finalisation step (GAIA-171): the transcript is
|
|
232
|
+
// now captured, so killing the PTY is safe. Best-effort + gated on the
|
|
233
|
+
// persistent capability (only hosted executors have a tab), self-guarded
|
|
234
|
+
// so a close failure never mislabels the run as "finalise failed".
|
|
235
|
+
if (this.executor.capabilities().persistent) {
|
|
236
|
+
const branch = await this.remote.getRunTicketBranchName(r.runUuid);
|
|
237
|
+
if (branch) {
|
|
238
|
+
try {
|
|
239
|
+
await this.executor.cleanupRun(branch);
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
this.logger.warn({ run: r.runUuid, err: String(err) }, 'run tab cleanup failed');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
220
246
|
}
|
|
221
247
|
catch (err) {
|
|
222
248
|
this.logger.warn({ run: r.runUuid, err: String(err) }, 'run finalise failed');
|
|
@@ -360,7 +386,8 @@ export class Conductor {
|
|
|
360
386
|
await this.executor.runHook('after_create', ws.path, { ticket: t.identifier }, env);
|
|
361
387
|
}
|
|
362
388
|
await this.executor.runHook('before_run', ws.path, { ticket: t.identifier }, env);
|
|
363
|
-
|
|
389
|
+
// Clear any leftover tabs of a reused workspace before starting the run.
|
|
390
|
+
await this.executor.cleanupRun(t.branchName);
|
|
364
391
|
const prompt = ws.instructions
|
|
365
392
|
? renderPrompt(this.config.prompt, {
|
|
366
393
|
identifier: t.identifier,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaia-ai/conductor",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "GAIA conductor engine + CLI: registers, claims tickets via JSON:API, dispatches agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"directory": "conductor/packages/conductor"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@gaia-ai/core": "^0.5.
|
|
28
|
+
"@gaia-ai/core": "^0.5.4",
|
|
29
29
|
"@dropsh/plugin-oauth2": "^0.5.7",
|
|
30
30
|
"@dropsh/plugin-jsonapi-schema": "^0.5.7",
|
|
31
31
|
"commander": "^12.1.0",
|