@gaia-ai/conductor 0.0.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/LICENSE +21 -0
- package/README.md +5 -0
- package/dist/src/cli/gaia.d.ts +21 -0
- package/dist/src/cli/gaia.js +690 -0
- package/dist/src/cli/init.d.ts +82 -0
- package/dist/src/cli/init.js +232 -0
- package/dist/src/cli/local-registry.d.ts +14 -0
- package/dist/src/cli/local-registry.js +56 -0
- package/dist/src/config.d.ts +51 -0
- package/dist/src/config.js +277 -0
- package/dist/src/core/conductor.d.ts +69 -0
- package/dist/src/core/conductor.js +389 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.js +5 -0
- package/package.json +34 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { conductorId, } from '@gaia-ai/core';
|
|
2
|
+
function sleep(ms, signal) {
|
|
3
|
+
return new Promise((resolve) => {
|
|
4
|
+
if (signal?.aborted) {
|
|
5
|
+
resolve();
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
const timer = setTimeout(() => {
|
|
9
|
+
signal?.removeEventListener('abort', onAbort);
|
|
10
|
+
resolve();
|
|
11
|
+
}, ms);
|
|
12
|
+
const onAbort = () => {
|
|
13
|
+
clearTimeout(timer);
|
|
14
|
+
resolve();
|
|
15
|
+
};
|
|
16
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A run-env key is reserved when it names a conductor-provided core variable:
|
|
21
|
+
* any `GAIA_*`, `WORKSPACE_ROOT`, or `TICKET_URL`. User `env_vars` may not
|
|
22
|
+
* clobber these — the core vars always win (GAIA-99).
|
|
23
|
+
*/
|
|
24
|
+
function isReservedEnvKey(key) {
|
|
25
|
+
return (key.startsWith('GAIA_') || key === 'WORKSPACE_ROOT' || key === 'TICKET_URL');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse the server-canonicalised effective env_vars (`KEY=value` lines, one per
|
|
29
|
+
* line) into a map. The server already stripped comments/blank/malformed lines,
|
|
30
|
+
* so this is deliberately minimal: split on the first `=`, keep a valid key.
|
|
31
|
+
* Defensive against stray blank lines only — never throws.
|
|
32
|
+
*/
|
|
33
|
+
function parseEnvLines(text) {
|
|
34
|
+
const out = {};
|
|
35
|
+
for (const raw of text.split('\n')) {
|
|
36
|
+
const line = raw.trim();
|
|
37
|
+
if (line === '') {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const eq = line.indexOf('=');
|
|
41
|
+
if (eq <= 0) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const key = line.slice(0, eq);
|
|
45
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
out[key] = line.slice(eq + 1);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the environment a run executes in (GAIA-99): parse the ticket's
|
|
54
|
+
* effective env_vars, drop any reserved key (loud warn — key NAME only, never
|
|
55
|
+
* the value), then overlay the conductor's core vars so they always win.
|
|
56
|
+
*
|
|
57
|
+
* Values are potentially secret (e.g. a DB DSN): this function logs key names
|
|
58
|
+
* only, never a value. The returned map is injected into BOTH the agent run env
|
|
59
|
+
* and every workspace hook.
|
|
60
|
+
*/
|
|
61
|
+
export function resolveRunEnv(effectiveEnvVars, core, logger) {
|
|
62
|
+
const user = effectiveEnvVars ? parseEnvLines(effectiveEnvVars) : {};
|
|
63
|
+
const clobbered = [];
|
|
64
|
+
const safe = {};
|
|
65
|
+
for (const [key, value] of Object.entries(user)) {
|
|
66
|
+
if (isReservedEnvKey(key)) {
|
|
67
|
+
clobbered.push(key);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
safe[key] = value;
|
|
71
|
+
}
|
|
72
|
+
if (clobbered.length > 0) {
|
|
73
|
+
logger.warn({ keys: clobbered }, 'ignored reserved keys in ticket env_vars (core vars win)');
|
|
74
|
+
}
|
|
75
|
+
// Core vars overlay last — they can never be overridden.
|
|
76
|
+
return { ...safe, ...core };
|
|
77
|
+
}
|
|
78
|
+
function renderPrompt(template, values) {
|
|
79
|
+
return template
|
|
80
|
+
.replaceAll('{identifier}', values.identifier)
|
|
81
|
+
.replaceAll('{state}', values.state)
|
|
82
|
+
.replaceAll('{runUuid}', values.runUuid);
|
|
83
|
+
}
|
|
84
|
+
export class Conductor {
|
|
85
|
+
config;
|
|
86
|
+
remote;
|
|
87
|
+
executor;
|
|
88
|
+
workspace;
|
|
89
|
+
agent;
|
|
90
|
+
logger;
|
|
91
|
+
checkoutRoot;
|
|
92
|
+
uuid = null;
|
|
93
|
+
constructor(config, remote, executor, workspace, agent, logger, checkoutRoot = process.cwd()) {
|
|
94
|
+
this.config = config;
|
|
95
|
+
this.remote = remote;
|
|
96
|
+
this.executor = executor;
|
|
97
|
+
this.workspace = workspace;
|
|
98
|
+
this.agent = agent;
|
|
99
|
+
this.logger = logger;
|
|
100
|
+
this.checkoutRoot = checkoutRoot;
|
|
101
|
+
}
|
|
102
|
+
get id() {
|
|
103
|
+
return this.config.machine_id ?? conductorId(this.checkoutRoot);
|
|
104
|
+
}
|
|
105
|
+
/** This conductor's registration payload, built from config. */
|
|
106
|
+
registration() {
|
|
107
|
+
return {
|
|
108
|
+
id: this.id,
|
|
109
|
+
project: this.config.project,
|
|
110
|
+
states: this.config.states,
|
|
111
|
+
workspace: this.checkoutRoot,
|
|
112
|
+
label: this.config.label,
|
|
113
|
+
max_parallel: this.config.max_parallel,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async start() {
|
|
117
|
+
this.uuid = await this.remote.registerConductor(this.registration());
|
|
118
|
+
this.logger.info({ conductorId: this.id, uuid: this.uuid, project: this.config.project }, 'conductor started');
|
|
119
|
+
}
|
|
120
|
+
async tick() {
|
|
121
|
+
if (!this.uuid)
|
|
122
|
+
throw new Error('not started');
|
|
123
|
+
let count = await this.remote.activeRunCount(this.id);
|
|
124
|
+
// A heartbeat is the server-side self-heal: it upserts by machine_id (so it
|
|
125
|
+
// never 404s and recreates a vanished registration) and refreshes the lease
|
|
126
|
+
// — no client-side re-register, no separate status read.
|
|
127
|
+
await this.remote.heartbeat(this.registration(), count, this.config.lease_seconds);
|
|
128
|
+
this.logger.info({ conductorId: this.id, active: count }, 'conductor tick');
|
|
129
|
+
await this.finalizeDoneRuns();
|
|
130
|
+
await this.reap();
|
|
131
|
+
let claimed = 0;
|
|
132
|
+
while (count < this.config.max_parallel) {
|
|
133
|
+
const run = await this.remote.claimNext({
|
|
134
|
+
leaseSeconds: this.config.lease_seconds,
|
|
135
|
+
conductorId: this.id,
|
|
136
|
+
});
|
|
137
|
+
if (!run)
|
|
138
|
+
break;
|
|
139
|
+
this.logger.info({ conductorId: this.id, run: run.runUuid, ticket: run.ticketUuid }, 'claimed run');
|
|
140
|
+
try {
|
|
141
|
+
await this.dispatch(run);
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
// A failed dispatch must NOT terminalise the run. The run lifecycle is
|
|
145
|
+
// server-owned; a client-side releaseRun('done') here fabricated a
|
|
146
|
+
// terminal state on failure, which reopened the ticket for claiming and
|
|
147
|
+
// re-dispatched it every tick — the GAIA-41 runaway. Leaving the run
|
|
148
|
+
// `claimed` lets the server's same-state claim conflict
|
|
149
|
+
// (TicketClaimService::retireSupersededRuns) block a re-claim of the
|
|
150
|
+
// same ticket, so we just log and move on. A run stuck `claimed` on a
|
|
151
|
+
// persistently-failing dispatch is freed by the lease-expiry reaper
|
|
152
|
+
// (separate follow-up), not here.
|
|
153
|
+
this.logger.error({ run: run.runUuid, err: String(err) }, 'dispatch failed');
|
|
154
|
+
}
|
|
155
|
+
count += 1;
|
|
156
|
+
claimed += 1;
|
|
157
|
+
}
|
|
158
|
+
if (claimed === 0) {
|
|
159
|
+
this.logger.info({
|
|
160
|
+
conductorId: this.id,
|
|
161
|
+
active: count,
|
|
162
|
+
capacity: this.config.max_parallel,
|
|
163
|
+
}, count >= this.config.max_parallel
|
|
164
|
+
? 'at capacity, nothing claimed'
|
|
165
|
+
: 'idle, nothing to claim');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* One-shot finalisation: for each of this conductor's runs that is
|
|
170
|
+
* state=done but not yet closed, read the agent transcript from its worktree
|
|
171
|
+
* and write it as the final log, then mark the run closed. Keyed off the
|
|
172
|
+
* durable `state=done AND closed=false` query — no in-memory state, so a
|
|
173
|
+
* restarted conductor still finalises any unclosed done run on its next tick.
|
|
174
|
+
*/
|
|
175
|
+
async finalizeDoneRuns() {
|
|
176
|
+
const runs = await this.remote.fetchFinalizableRuns(this.id);
|
|
177
|
+
for (const r of runs) {
|
|
178
|
+
try {
|
|
179
|
+
// Ask the agent to exit gracefully (/exit) before capturing its log.
|
|
180
|
+
// Best-effort and gated on the persistent capability (only hosted
|
|
181
|
+
// executors have a live agent pane) — mirrors removeWorktree's gate.
|
|
182
|
+
// A stop failure must never block finalisation.
|
|
183
|
+
if (this.executor.capabilities().persistent) {
|
|
184
|
+
const branch = await this.remote.getRunTicketBranchName(r.runUuid);
|
|
185
|
+
if (branch) {
|
|
186
|
+
try {
|
|
187
|
+
await this.executor.stopAgent(branch);
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
this.logger.warn({ run: r.runUuid, err: String(err) }, 'agent stop failed');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const log = r.worktreePath
|
|
195
|
+
? await this.agent.getRunLog(r.worktreePath)
|
|
196
|
+
: '';
|
|
197
|
+
// Footprint (GAIA-132): the agent parses its own transcript for the
|
|
198
|
+
// effort metrics (transcript format is agent-specific); the conductor
|
|
199
|
+
// adds duration_s from started_at → now. The conductor owns the run, so
|
|
200
|
+
// this write always lands (the agent session could not).
|
|
201
|
+
const parsed = this.agent.parseFootprint(log);
|
|
202
|
+
const now = Math.floor(Date.now() / 1000);
|
|
203
|
+
const metrics = {
|
|
204
|
+
tokens: parsed.tokens,
|
|
205
|
+
duration_s: r.startedAt !== undefined ? Math.max(0, now - r.startedAt) : 0,
|
|
206
|
+
agent_turns: parsed.agent_turns,
|
|
207
|
+
tool_calls: parsed.tool_calls,
|
|
208
|
+
user_prompts: parsed.user_prompts,
|
|
209
|
+
user_prompt_words: parsed.user_prompt_words,
|
|
210
|
+
};
|
|
211
|
+
await this.remote.finalizeRun(r.runUuid, log, metrics);
|
|
212
|
+
this.logger.info({ run: r.runUuid, footprint: metrics }, 'run finalised');
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
this.logger.warn({ run: r.runUuid, err: String(err) }, 'run finalise failed');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Reconcile finished tickets against their herdr worktrees and tear down any
|
|
221
|
+
* whose workspace still lingers (GAIA-89). Driven by the durable `cleaned_up`
|
|
222
|
+
* flag via {@link GaiaRemote.fetchUncleanedTickets}: every ticket that is
|
|
223
|
+
* finished (state=done OR closed) but not yet cleaned. The SAME reconciliation
|
|
224
|
+
* runs on every tick AND standalone as `gaia conductor reap`, and the query is
|
|
225
|
+
* scoped to THIS conductor (GAIA-121) — so it catches the orphans a
|
|
226
|
+
* live-tick-only path structurally cannot (the conductor was down when the
|
|
227
|
+
* ticket hit `done`, or crashed mid-cleanup) for its OWN tickets. A foreign
|
|
228
|
+
* (non-gaia) worktree has no gaia ticket, so it is never on the work list; a
|
|
229
|
+
* ticket assigned to another conductor is filtered out at the query — both are
|
|
230
|
+
* left untouched with no host enumeration.
|
|
231
|
+
*
|
|
232
|
+
* Per ticket, close is DECOUPLED from teardown (no withholding): a done ticket
|
|
233
|
+
* is closed at once as a lifecycle step, independent of whether its worktree
|
|
234
|
+
* teardown then succeeds. Because the list is conductor-scoped, every ticket's
|
|
235
|
+
* worktree belongs on THIS host: `removeWorktree` returning `false` means the
|
|
236
|
+
* directory was already deleted out of band (nothing left to remove), which is
|
|
237
|
+
* itself a completed teardown → flag `cleaned_up`. There is no cross-host defer
|
|
238
|
+
* (GAIA-121) — the manufactured ambiguity that required it is gone once the
|
|
239
|
+
* query no longer loads foreign tickets. A teardown that THROWS logs loudly and
|
|
240
|
+
* leaves `cleaned_up=0`, so the next reconciliation retries; one miss never
|
|
241
|
+
* orphans the workspace (RC1). Re-running on an already-cleaned ticket is a
|
|
242
|
+
* no-op — it is off the list (idempotent). Each ticket is isolated in a
|
|
243
|
+
* try/catch so one failure never aborts the rest.
|
|
244
|
+
*
|
|
245
|
+
* The `after_done` hook runs through the executor (GAIA-84), which owns the
|
|
246
|
+
* best-effort policy — it logs+swallows a hook failure and never throws, so
|
|
247
|
+
* the core needs no per-call-site try/catch around it.
|
|
248
|
+
*/
|
|
249
|
+
async reap() {
|
|
250
|
+
const tickets = await this.remote.fetchUncleanedTickets(this.id);
|
|
251
|
+
for (const t of tickets) {
|
|
252
|
+
try {
|
|
253
|
+
// Lifecycle: close a done+unclosed ticket at once, decoupled from the
|
|
254
|
+
// infra teardown below — a done ticket whose conductor was down was
|
|
255
|
+
// never closed, and withholding the close on a teardown hiccup would
|
|
256
|
+
// make a finished ticket look unfinished (the pre-GAIA-89 defect).
|
|
257
|
+
if (t.state === 'done' && !t.closed) {
|
|
258
|
+
await this.remote.closeTicket(t.ticketUuid);
|
|
259
|
+
}
|
|
260
|
+
// Infra teardown. The `after_done` hook runs in the worktree regardless
|
|
261
|
+
// of executor kind (a non-persistent gitWorkspace still has an on-disk
|
|
262
|
+
// worktree); only the workspace removal is gated on the persistent
|
|
263
|
+
// capability (mirrors the executor-agnostic release path). Pass the
|
|
264
|
+
// stable worktreePath so the executor resolves the worktree by path,
|
|
265
|
+
// not by the mutable checked-out branch (the coding agent may rename
|
|
266
|
+
// it). Teardown is idempotent: a worktree herdr no longer tracks is a
|
|
267
|
+
// no-op / on-disk reclaim, not a failure.
|
|
268
|
+
if (t.worktreePath) {
|
|
269
|
+
await this.executor.runHook('after_done', t.worktreePath, {
|
|
270
|
+
ticket: t.ticketUuid,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
if (this.executor.capabilities().persistent) {
|
|
274
|
+
try {
|
|
275
|
+
// The work list is scoped to this conductor (GAIA-121), so its
|
|
276
|
+
// worktree belongs on THIS host. Whether removeWorktree tears one
|
|
277
|
+
// down or finds it already gone (`false`), teardown is complete —
|
|
278
|
+
// the boolean is irrelevant, only a THROW (a real failure) matters.
|
|
279
|
+
await this.executor.removeWorktree(t.branchName, t.worktreePath || undefined);
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
this.logger.warn({
|
|
283
|
+
ticket: t.ticketUuid,
|
|
284
|
+
branch: t.branchName,
|
|
285
|
+
worktreePath: t.worktreePath,
|
|
286
|
+
err: String(err),
|
|
287
|
+
}, 'worktree teardown failed');
|
|
288
|
+
continue; // leave cleaned_up=0 → the next reconciliation retries.
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Teardown verified locally (or nothing hosted to tear down): flag it
|
|
292
|
+
// cleaned so it drops off the work list.
|
|
293
|
+
await this.remote.markTicketCleanedUp(t.ticketUuid);
|
|
294
|
+
this.logger.info({ ticket: t.ticketUuid }, 'ticket cleaned up');
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
this.logger.warn({ ticket: t.ticketUuid, err: String(err) }, 'ticket cleanup failed');
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async dispatch(run) {
|
|
302
|
+
const t = await this.remote.getTicket(run.ticketUuid);
|
|
303
|
+
const baseRef = t.baseBranch ? `origin/${t.baseBranch}` : undefined;
|
|
304
|
+
// Resolve the run env once (per-ticket env_vars + core GAIA_* vars) and
|
|
305
|
+
// inject it into BOTH the executor-owned lifecycle hooks AND the agent run
|
|
306
|
+
// env (GAIA-99). Reserved core keys always win; values may be sensitive so
|
|
307
|
+
// only key names are ever logged.
|
|
308
|
+
const core = {
|
|
309
|
+
GAIA_URL: this.config.site.base_url,
|
|
310
|
+
GAIA_ID: run.ticketUuid,
|
|
311
|
+
GAIA_RUN_UUID: run.runUuid,
|
|
312
|
+
WORKSPACE_ROOT: this.checkoutRoot,
|
|
313
|
+
...(t.issueUrl ? { TICKET_URL: t.issueUrl } : {}),
|
|
314
|
+
};
|
|
315
|
+
const env = resolveRunEnv(t.effectiveEnvVars, core, this.logger);
|
|
316
|
+
const ws = await this.workspace.ensure(t.identifier, t.branchName, baseRef);
|
|
317
|
+
// A claimable state has no deterministic policy without a WORKFLOW.md
|
|
318
|
+
// (SKILL.md dispatch reads its `## State: <state>` sections). The human
|
|
319
|
+
// steered this to WARN-and-proceed (not a hard fail): the agent runs on
|
|
320
|
+
// engine defaults, and the gap is observable in the conductor log rather
|
|
321
|
+
// than wedging the run. `ws.instructions` IS the loaded WORKFLOW.md (null
|
|
322
|
+
// when absent), so no extra stat.
|
|
323
|
+
if (!ws.instructions) {
|
|
324
|
+
this.logger.warn({ ticket: t.identifier, workspace: ws.path }, 'WORKFLOW.md missing — dispatching on engine defaults');
|
|
325
|
+
}
|
|
326
|
+
// Lifecycle hooks are executor-owned + best-effort (GAIA-84): runHook never
|
|
327
|
+
// throws, so neither call can abort dispatch or wedge a run in `claimed`.
|
|
328
|
+
// after_create only on a freshly-created worktree; before_run always. Both
|
|
329
|
+
// get the resolved run env so a setup/pre hook sees the same vars (e.g. a DB
|
|
330
|
+
// DSN) the agent does (GAIA-99).
|
|
331
|
+
if (ws.created) {
|
|
332
|
+
await this.executor.runHook('after_create', ws.path, { ticket: t.identifier }, env);
|
|
333
|
+
}
|
|
334
|
+
await this.executor.runHook('before_run', ws.path, { ticket: t.identifier }, env);
|
|
335
|
+
await this.executor.retire(t.branchName);
|
|
336
|
+
const prompt = ws.instructions
|
|
337
|
+
? renderPrompt(this.config.prompt, {
|
|
338
|
+
identifier: t.identifier,
|
|
339
|
+
state: t.state || 'triage',
|
|
340
|
+
runUuid: run.runUuid,
|
|
341
|
+
})
|
|
342
|
+
: '';
|
|
343
|
+
await this.executor.startRun({
|
|
344
|
+
ticket: {
|
|
345
|
+
uuid: t.uuid,
|
|
346
|
+
identifier: t.identifier,
|
|
347
|
+
title: t.title,
|
|
348
|
+
branchName: t.branchName,
|
|
349
|
+
state: t.state,
|
|
350
|
+
...(t.issueUrl ? { url: t.issueUrl } : {}),
|
|
351
|
+
},
|
|
352
|
+
run: { uuid: run.runUuid, id: run.runId, handler: run.handler },
|
|
353
|
+
workspacePath: ws.path,
|
|
354
|
+
instructions: ws.instructions,
|
|
355
|
+
command: this.agent.launchCommand(prompt),
|
|
356
|
+
env,
|
|
357
|
+
});
|
|
358
|
+
this.logger.info({
|
|
359
|
+
ticket: t.identifier,
|
|
360
|
+
run: run.runUuid,
|
|
361
|
+
state: t.state,
|
|
362
|
+
workspace: ws.path,
|
|
363
|
+
// Key names only — env values may be sensitive (GAIA-99).
|
|
364
|
+
envKeys: Object.keys(env),
|
|
365
|
+
}, 'dispatched run');
|
|
366
|
+
// The conductor — not the agent — knows the per-run git worktree path, so
|
|
367
|
+
// it persists `worktree_path` to gaia_run here alongside the running state.
|
|
368
|
+
await this.remote.markRunning(run.runUuid, { worktree_path: ws.path });
|
|
369
|
+
}
|
|
370
|
+
async serve(signal) {
|
|
371
|
+
await this.pollLoop(signal);
|
|
372
|
+
}
|
|
373
|
+
async pollLoop(signal) {
|
|
374
|
+
while (!signal?.aborted) {
|
|
375
|
+
try {
|
|
376
|
+
await this.tick();
|
|
377
|
+
}
|
|
378
|
+
catch (err) {
|
|
379
|
+
this.logger.error({ conductorId: this.id, err: String(err) }, 'tick failed');
|
|
380
|
+
}
|
|
381
|
+
await sleep(this.config.poll_interval_ms, signal);
|
|
382
|
+
}
|
|
383
|
+
// NB: the poll loop never writes status=offline itself. A graceful stop
|
|
384
|
+
// (`gaia conductor stop`) writes offline directly server-side; a crash or
|
|
385
|
+
// hard-kill leaves a stale lease that the Drupal cron reaper flips to
|
|
386
|
+
// offline (gaia_core cron → ConductorReaper). Liveness is thus always
|
|
387
|
+
// derived from status + last_seen/lease_expires_at, never from the loop.
|
|
388
|
+
}
|
|
389
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type * from '@gaia-ai/core';
|
|
2
|
+
export { conductorId } from '@gaia-ai/core';
|
|
3
|
+
export { DrupalGaiaRemote, drupalRemote, FakeGaiaRemote, FakeWorkspace, fakeRemote, fakeWorkspace, GitWorkspace, gitWorkspace, selectExecutor, selectRemote, selectWorkspace, } from '@gaia-ai/core/plugins';
|
|
4
|
+
export type { GaiaCliDeps } from './cli/gaia.js';
|
|
5
|
+
export { main, runGaiaCli } from './cli/gaia.js';
|
|
6
|
+
export type { ConductorRegistryEntry } from './cli/local-registry.js';
|
|
7
|
+
export { DEFAULT_AGENT_PROMPT, loadConductorConfig } from './config.js';
|
|
8
|
+
export { Conductor } from './core/conductor.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { conductorId } from '@gaia-ai/core';
|
|
2
|
+
export { DrupalGaiaRemote, drupalRemote, FakeGaiaRemote, FakeWorkspace, fakeRemote, fakeWorkspace, GitWorkspace, gitWorkspace, selectExecutor, selectRemote, selectWorkspace, } from '@gaia-ai/core/plugins';
|
|
3
|
+
export { main, runGaiaCli } from './cli/gaia.js';
|
|
4
|
+
export { DEFAULT_AGENT_PROMPT, loadConductorConfig } from './config.js';
|
|
5
|
+
export { Conductor } from './core/conductor.js';
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gaia-ai/conductor",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "GAIA conductor engine + CLI: registers, claims tickets via JSON:API, dispatches agents.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20.19"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./dist/src/index.js",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist/src",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://git.key-tec.de/keytec/gaia.git",
|
|
25
|
+
"directory": "conductor/packages/conductor"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@gaia-ai/core": "^0.0.1",
|
|
29
|
+
"@dropsh/plugin-oauth2": "^0.5.7",
|
|
30
|
+
"@dropsh/plugin-jsonapi-schema": "^0.5.7",
|
|
31
|
+
"commander": "^12.1.0",
|
|
32
|
+
"dropsh": "^0.5.7"
|
|
33
|
+
}
|
|
34
|
+
}
|