@evo-hq/evo-agent 0.2.2 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evo-hq/evo-agent",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Lightweight reporting SDK for evo experiments. Zero dependencies.",
5
5
  "type": "module",
6
6
  "publishConfig": {
package/src/backend.js CHANGED
@@ -1,5 +1,11 @@
1
- import { writeFileSync, mkdirSync } from "node:fs";
2
- import { join } from "node:path";
1
+ import {
2
+ writeFileSync,
3
+ mkdirSync,
4
+ openSync,
5
+ closeSync,
6
+ renameSync,
7
+ } from "node:fs";
8
+ import { dirname, join } from "node:path";
3
9
 
4
10
  export class LocalBackend {
5
11
  setup({ tracesDir, experimentId } = {}) {
@@ -17,7 +23,29 @@ export class LocalBackend {
17
23
  }
18
24
 
19
25
  emitResult(result) {
20
- process.stdout.write(JSON.stringify(result, null, 2) + "\n");
26
+ const payload = JSON.stringify(result, null, 2);
27
+ const resultPath = process.env.EVO_RESULT_PATH;
28
+ if (!resultPath) {
29
+ process.stdout.write(payload + "\n");
30
+ return;
31
+ }
32
+ mkdirSync(dirname(resultPath), { recursive: true });
33
+ // Claim destination + tmp+rename: duplicate writers fail-fast on the
34
+ // 'wx' (O_EXCL) claim; a crash mid-publish leaves an empty file at
35
+ // resultPath (caught by load_result) instead of a partial write.
36
+ try {
37
+ closeSync(openSync(resultPath, "wx"));
38
+ } catch (e) {
39
+ if (e.code === "EEXIST") {
40
+ throw new Error(
41
+ `${resultPath} already exists; only one Run.finish() / writeResult() per attempt`
42
+ );
43
+ }
44
+ throw e;
45
+ }
46
+ const tmp = resultPath + ".tmp";
47
+ writeFileSync(tmp, payload, "utf-8");
48
+ renameSync(tmp, resultPath);
21
49
  }
22
50
 
23
51
  emitGateSummary({ passed, lines }) {
package/src/run.js CHANGED
@@ -30,6 +30,7 @@ export class Run {
30
30
  experimentId: this._experimentId,
31
31
  });
32
32
  this._tasks = {};
33
+ this._taskMeta = {};
33
34
  this._taskStarted = {};
34
35
  this._logs = {};
35
36
  this._startedAt = utcNow();
@@ -57,15 +58,21 @@ export class Run {
57
58
  startedAt,
58
59
  endedAt,
59
60
  artifacts,
61
+ direction,
60
62
  ...extra
61
63
  } = opts;
62
64
 
65
+ if (direction !== undefined && direction !== "max" && direction !== "min") {
66
+ throw new Error(`direction must be 'max' or 'min', got ${JSON.stringify(direction)}`);
67
+ }
68
+
63
69
  const trace = {
64
70
  experiment_id: this._experimentId,
65
71
  task_id: taskId,
66
72
  status: status ?? (score >= passThreshold ? "passed" : "failed"),
67
73
  score,
68
74
  };
75
+ if (direction !== undefined) trace.direction = direction;
69
76
  if (summary !== undefined) trace.summary = summary;
70
77
  if (failureReason !== undefined) trace.failure_reason = failureReason;
71
78
  if (cost !== undefined) trace.cost = cost;
@@ -75,6 +82,7 @@ export class Run {
75
82
  Object.assign(trace, extra);
76
83
 
77
84
  this._tasks[taskId] = score;
85
+ if (direction !== undefined) this._taskMeta[taskId] = { direction };
78
86
  if (this._logs[taskId]?.length) trace.log = [...this._logs[taskId]];
79
87
 
80
88
  this._backend.writeTrace(trace);
@@ -97,6 +105,11 @@ export class Run {
97
105
  started_at: this._startedAt,
98
106
  ended_at: utcNow(),
99
107
  };
108
+ if (Object.keys(this._taskMeta).length > 0) {
109
+ result.tasks_meta = Object.fromEntries(
110
+ Object.entries(this._taskMeta).map(([k, v]) => [k, { ...v }])
111
+ );
112
+ }
100
113
  this._backend.emitResult(result);
101
114
  return result;
102
115
  }