@gaia-ai/core 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.
@@ -7,7 +7,13 @@ export class ExecError extends Error {
7
7
  exitCode;
8
8
  stderr;
9
9
  constructor(file, args, exitCode, stderr, options) {
10
- super(`exec failed: ${file}`, options);
10
+ // Include exitCode + stderr in the message so a stringified ExecError
11
+ // (e.g. gaia_run.error_log = String(err)) carries the ACTUAL cause, not a
12
+ // bare "exec failed: herdr". Args are omitted on purpose — they can embed
13
+ // secrets (herdr's `KEY='value' <agent>` pane-run string, GAIA-99); stderr
14
+ // is the tool's own diagnostic output and is trimmed.
15
+ const detail = stderr.trim();
16
+ super(`exec failed: ${file} (exit ${exitCode ?? 'null'})${detail ? `: ${detail}` : ''}`, options);
11
17
  this.file = file;
12
18
  this.args = args;
13
19
  this.exitCode = exitCode;
@@ -3,12 +3,21 @@
3
3
  * (GAIA-132). The metrics are transcript-derived and therefore
4
4
  * **agent-specific** (each agent's transcript has its own format), so parsing
5
5
  * lives behind the agent abstraction (see {@link GaiaAgent.parseFootprint}) —
6
- * the conductor stays agent-agnostic and only adds `duration_s` (wall-clock,
7
- * agent-independent) before writing the run's footprint.
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.
8
11
  */
9
12
  export interface AgentFootprint {
10
13
  /** Total tokens across every usage bucket of every assistant turn. */
11
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;
12
21
  /** Number of assistant turns in the transcript. */
13
22
  agent_turns: number;
14
23
  /** Number of tool-use calls across all assistant turns. */
@@ -2,6 +2,7 @@
2
2
  export function emptyAgentFootprint() {
3
3
  return {
4
4
  tokens: 0,
5
+ duration_s: 0,
5
6
  agent_turns: 0,
6
7
  tool_calls: 0,
7
8
  user_prompts: 0,
@@ -55,19 +55,26 @@ export interface GaiaExecutor {
55
55
  */
56
56
  runHook(name: HookName, cwd: string, ctx: HookContext, env?: Record<string, string>): Promise<void>;
57
57
  startRun(input: SpawnRunInput): Promise<SpawnedSession>;
58
- /** Send C-c to every open (non-`(done)`) tab in the branch workspace and
59
- * append ` (done)` to its label. Best-effort, non-destructive: tabs stay open
60
- * for the human to close. */
61
- retire(branch: string): Promise<void>;
62
58
  /**
63
- * Ask the agent in each open (non-`(done)`) tab of the branch workspace to
64
- * exit gracefully by typing `/exit` + Enter into its pane a clean shutdown
65
- * vs {@link retire}'s C-c SIGINT. Called by the conductor's run-finalise pass
66
- * once a run is done. Best-effort and non-destructive (tabs stay open); a
67
- * no-op for non-persistent executors (no hosted agent pane), so the conductor
68
- * gates the call on `capabilities().persistent`.
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`.
69
66
  */
70
- stopAgent(branch: string): Promise<void>;
67
+ stopRun(branch: string): Promise<void>;
68
+ /**
69
+ * Tear down the run's hosted UI surface — for herdr, close every tab of the
70
+ * branch workspace via `herdr tab close <tab_id>` (killing the PTY). Called
71
+ * as the LAST finalisation step, strictly after the transcript is captured,
72
+ * and again at dispatch to clear a reused workspace's leftover tabs. Does NOT
73
+ * touch the branch worktree (that is the reap/{@link removeWorktree}
74
+ * lifecycle). Best-effort: one failing tab-close must not abort the others,
75
+ * finalisation, or dispatch. A no-op for non-persistent executors.
76
+ */
77
+ cleanupRun(branch: string): Promise<void>;
71
78
  /**
72
79
  * Tear down the branch's entire worktree (git worktree + hosted workspace),
73
80
  * reclaiming its disk. Called by the conductor's ticket-cleanup pass once a
@@ -17,6 +17,7 @@ export declare class DrupalGaiaRemote implements GaiaRemote {
17
17
  setConductorStatus(id: string, status: string): Promise<void>;
18
18
  listConductors(owner?: 'me'): Promise<ConductorStatus[]>;
19
19
  markRunning(uuid: string, attrs?: RunWriteAttributes): Promise<void>;
20
+ markFailed(uuid: string, errorLog: string): Promise<void>;
20
21
  fetchFinalizableRuns(id: string): Promise<FinalizableRun[]>;
21
22
  finalizeRun(uuid: string, log: string, metrics?: RunMetrics): Promise<void>;
22
23
  fetchUncleanedTickets(id: string): Promise<UncleanTicket[]>;
@@ -220,24 +220,34 @@ export class DrupalGaiaRemote {
220
220
  },
221
221
  });
222
222
  }
223
+ async markFailed(uuid, errorLog) {
224
+ const t = Math.floor(Date.now() / 1000);
225
+ await this.api.update('gaia_run', uuid, {
226
+ attributes: {
227
+ state: 'failed',
228
+ error_log: errorLog,
229
+ closed: true,
230
+ closed_date: t,
231
+ },
232
+ });
233
+ }
223
234
  async fetchFinalizableRuns(id) {
224
235
  const rows = await this.api
225
236
  .collection('gaia_run')
226
237
  .where('conductor_id.machine_id', '=', id)
227
238
  .where('state', '=', 'done')
228
239
  .where('closed', '=', '0')
229
- .fields(['worktree_path', 'started_at', 'agent'])
240
+ .fields(['worktree_path', 'agent'])
230
241
  .page(100)
231
242
  .list();
232
- return rows.map((r) => {
233
- const startedAt = r.attr('started_at');
234
- return {
235
- runUuid: r.id,
236
- worktreePath: r.attr('worktree_path') ?? '',
237
- ...(typeof startedAt === 'number' ? { startedAt } : {}),
238
- agent: r.attr('agent') ?? '',
239
- };
240
- });
243
+ // No `started_at` read: the run's duration_s is derived from the agent
244
+ // transcript at finalize (GAIA-151), so the conductor no longer round-trips
245
+ // the timestamp back through JSON:API.
246
+ return rows.map((r) => ({
247
+ runUuid: r.id,
248
+ worktreePath: r.attr('worktree_path') ?? '',
249
+ agent: r.attr('agent') ?? '',
250
+ }));
241
251
  }
242
252
  async finalizeRun(uuid, log, metrics) {
243
253
  const t = Math.floor(Date.now() / 1000);
@@ -11,8 +11,6 @@ export interface FakeSeedRun {
11
11
  handler?: string;
12
12
  worktreePath?: string;
13
13
  closed?: boolean;
14
- /** Unix timestamp (seconds) the run started — drives footprint duration_s. */
15
- startedAt?: number;
16
14
  /** Id of the agent that ran (GAIA-144) — routes footprint parsing at finalize. */
17
15
  agent?: string;
18
16
  }
@@ -61,9 +59,14 @@ export interface FinalizeRunCall {
61
59
  log: string;
62
60
  metrics?: RunMetrics;
63
61
  }
62
+ export interface MarkFailedCall {
63
+ runUuid: string;
64
+ errorLog: string;
65
+ }
64
66
  export declare class FakeGaiaRemote implements GaiaRemote {
65
67
  readonly calls: {
66
68
  markRunning: MarkRunningCall[];
69
+ markFailed: MarkFailedCall[];
67
70
  finalizeRun: FinalizeRunCall[];
68
71
  closeTicket: string[];
69
72
  markCleanedUp: string[];
@@ -93,6 +96,7 @@ export declare class FakeGaiaRemote implements GaiaRemote {
93
96
  getRunTicketIdentifier(runUuid: string): Promise<string>;
94
97
  getRunTicketBranchName(runUuid: string): Promise<string>;
95
98
  markRunning(runUuid: string, attrs?: RunWriteAttributes): Promise<void>;
99
+ markFailed(runUuid: string, errorLog: string): Promise<void>;
96
100
  fetchFinalizableRuns(_conductorId: string): Promise<FinalizableRun[]>;
97
101
  finalizeRun(runUuid: string, log: string, metrics?: RunMetrics): Promise<void>;
98
102
  fetchUncleanedTickets(conductorId: string): Promise<UncleanTicket[]>;
@@ -2,6 +2,7 @@ const ACTIVE = ['claimed', 'running'];
2
2
  export class FakeGaiaRemote {
3
3
  calls = {
4
4
  markRunning: [],
5
+ markFailed: [],
5
6
  finalizeRun: [],
6
7
  closeTicket: [],
7
8
  markCleanedUp: [],
@@ -33,7 +34,6 @@ export class FakeGaiaRemote {
33
34
  ...(r.worktreePath !== undefined
34
35
  ? { worktreePath: r.worktreePath }
35
36
  : {}),
36
- ...(r.startedAt !== undefined ? { startedAt: r.startedAt } : {}),
37
37
  ...(r.agent !== undefined ? { agent: r.agent } : {}),
38
38
  ...(r.closed !== undefined ? { closed: r.closed } : {}),
39
39
  });
@@ -155,13 +155,20 @@ export class FakeGaiaRemote {
155
155
  }
156
156
  }
157
157
  }
158
+ async markFailed(runUuid, errorLog) {
159
+ this.calls.markFailed.push({ runUuid, errorLog });
160
+ const run = this.runs.get(runUuid);
161
+ if (run) {
162
+ run.state = 'failed';
163
+ run.closed = true;
164
+ }
165
+ }
158
166
  async fetchFinalizableRuns(_conductorId) {
159
167
  return [...this.runs.values()]
160
168
  .filter((r) => r.state === 'done' && !r.closed)
161
169
  .map((r) => ({
162
170
  runUuid: r.runUuid,
163
171
  worktreePath: r.worktreePath ?? '',
164
- ...(r.startedAt !== undefined ? { startedAt: r.startedAt } : {}),
165
172
  agent: r.agent ?? '',
166
173
  }));
167
174
  }
@@ -64,19 +64,14 @@ export interface FinalizableRun {
64
64
  runUuid: string;
65
65
  /** Absolute path of the per-run git worktree, or '' when unset. */
66
66
  worktreePath: string;
67
- /**
68
- * Unix timestamp (seconds) the run started, or undefined when unset. The
69
- * conductor derives the footprint `duration_s` as `now − startedAt` at close
70
- * (GAIA-132).
71
- */
72
- startedAt?: number;
73
67
  /** Id of the agent that ran (GAIA-144); routes footprint parsing at finalize. '' / undefined when unset. */
74
68
  agent?: string;
75
69
  }
76
70
  /**
77
71
  * Per-run effort footprint the conductor writes onto `gaia_run` at close
78
- * (GAIA-132). Six integer metrics: the five parsed from the agent transcript
79
- * plus `duration_s` computed from `started_at close`.
72
+ * (GAIA-132). Six integer metrics, all parsed from the agent transcript
73
+ * including `duration_s`, the run's wall-clock length derived from the
74
+ * transcript's first→last timestamps (GAIA-151), not a re-read `started_at`.
80
75
  */
81
76
  export interface RunMetrics {
82
77
  tokens: number;
@@ -147,6 +142,16 @@ export interface GaiaRemote {
147
142
  */
148
143
  getRunTicketBranchName(runUuid: string): Promise<string>;
149
144
  markRunning(runUuid: string, attrs?: RunWriteAttributes): Promise<void>;
145
+ /**
146
+ * Terminalise a run to the `failed` state on a dispatch/setup error (GAIA-149).
147
+ * PATCHes state=failed, records the failure detail in `error_log`, and closes
148
+ * the run (closed + closed_date). `failed` is a terminal sibling of `expired`
149
+ * (dispatch error vs lease lapse), so the run stops counting toward capacity
150
+ * and its ticket is no longer blocked by the single-active-run gate — instead
151
+ * of the old "leave it `claimed` to expire after ~300s" silent stall. The
152
+ * failure is visible immediately and its cause is recorded.
153
+ */
154
+ markFailed(runUuid: string, errorLog: string): Promise<void>;
150
155
  /**
151
156
  * Runs this conductor owns that are state=done but not yet closed — the
152
157
  * conductor's one-shot finalisation work list.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaia-ai/core",
3
- "version": "0.5.0",
3
+ "version": "0.5.4",
4
4
  "description": "GAIA global contract: plugin API, built-in remotes/workspaces/auth, shared primitives.",
5
5
  "type": "module",
6
6
  "license": "MIT",