agent-trajectories 0.5.2 → 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.
@@ -1,3 +1,9 @@
1
+ // src/sdk/client.ts
2
+ import { spawn } from "child_process";
3
+ import { existsSync as existsSync2, readFileSync } from "fs";
4
+ import { createRequire } from "module";
5
+ import { dirname, resolve as resolvePath } from "path";
6
+
1
7
  // src/core/id.ts
2
8
  import { webcrypto } from "crypto";
3
9
  var ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -18,7 +24,7 @@ function generateChapterId() {
18
24
  return `chap_${generateRandomId()}`;
19
25
  }
20
26
  function isValidTrajectoryId(id) {
21
- return /^traj_[a-z0-9]{12}$/.test(id);
27
+ return /^traj_[a-z0-9_]+$/.test(id);
22
28
  }
23
29
  function isValidChapterId(id) {
24
30
  return /^chap_[a-z0-9]{12}$/.test(id);
@@ -50,18 +56,20 @@ var TrajectoryStatusSchema = z.enum([
50
56
  "completed",
51
57
  "abandoned"
52
58
  ]);
53
- var TrajectoryEventTypeSchema = z.enum([
54
- "prompt",
55
- "thinking",
56
- "tool_call",
57
- "tool_result",
58
- "message_sent",
59
- "message_received",
60
- "decision",
61
- "finding",
62
- "reflection",
63
- "note",
64
- "error"
59
+ var TrajectoryEventTypeSchema = z.union([
60
+ z.literal("prompt"),
61
+ z.literal("thinking"),
62
+ z.literal("tool_call"),
63
+ z.literal("tool_result"),
64
+ z.literal("message_sent"),
65
+ z.literal("message_received"),
66
+ z.literal("decision"),
67
+ z.literal("finding"),
68
+ z.literal("reflection"),
69
+ z.literal("note"),
70
+ z.literal("error"),
71
+ z.string()
72
+ // Allow event types emitted by other tools (e.g. agent-relay's completion-evidence / completion-marker). Downstream code filters to known types.
65
73
  ]);
66
74
  var EventSignificanceSchema = z.enum([
67
75
  "low",
@@ -91,7 +99,7 @@ var DecisionSchema = z.object({
91
99
  });
92
100
  var AgentParticipationSchema = z.object({
93
101
  name: z.string().min(1, "Agent name is required"),
94
- role: z.enum(["lead", "contributor", "reviewer"]),
102
+ role: z.string().min(1, "Agent role is required"),
95
103
  joinedAt: z.string().datetime(),
96
104
  leftAt: z.string().datetime().optional()
97
105
  });
@@ -151,7 +159,7 @@ var TrajectoryTraceRefSchema = z.object({
151
159
  traceId: z.string().optional()
152
160
  });
153
161
  var TrajectorySchema = z.object({
154
- id: z.string().regex(/^traj_[a-z0-9]+$/, "Invalid trajectory ID format"),
162
+ id: z.string().regex(/^traj_[a-z0-9_]+$/, "Invalid trajectory ID format"),
155
163
  version: z.literal(1),
156
164
  task: TaskReferenceSchema,
157
165
  status: TrajectoryStatusSchema,
@@ -160,10 +168,11 @@ var TrajectorySchema = z.object({
160
168
  agents: z.array(AgentParticipationSchema),
161
169
  chapters: z.array(ChapterSchema),
162
170
  retrospective: RetrospectiveSchema.optional(),
163
- commits: z.array(z.string()),
164
- filesChanged: z.array(z.string()),
165
- projectId: z.string(),
166
- tags: z.array(z.string()),
171
+ commits: z.array(z.string()).default([]),
172
+ filesChanged: z.array(z.string()).default([]),
173
+ projectId: z.string().optional(),
174
+ workflowId: z.string().optional(),
175
+ tags: z.array(z.string()).default([]),
167
176
  _trace: TrajectoryTraceRefSchema.optional()
168
177
  });
169
178
  var CreateTrajectoryInputSchema = z.object({
@@ -696,11 +705,129 @@ var FileStorage = class {
696
705
  trajectories: {}
697
706
  });
698
707
  }
708
+ await this.reconcileIndex();
709
+ }
710
+ /**
711
+ * Scan active/ and completed/ recursively and add any trajectory files
712
+ * missing from the index. Existing entries are preserved — reconcile
713
+ * only adds, never removes.
714
+ *
715
+ * Handles three on-disk layouts in completed/:
716
+ * - flat: completed/{id}.json (legacy workforce data)
717
+ * - monthly: completed/YYYY-MM/{id}.json (current save() writes)
718
+ * - nested: completed/.../{id}.json (defensive — any depth)
719
+ *
720
+ * Returns a ReconcileSummary so tests and CLI wrappers can observe
721
+ * outcomes without parsing logs. Only writes the index if anything was
722
+ * added.
723
+ */
724
+ async reconcileIndex() {
725
+ const summary = {
726
+ scanned: 0,
727
+ added: 0,
728
+ alreadyIndexed: 0,
729
+ skippedMalformedJson: 0,
730
+ skippedSchemaViolation: 0,
731
+ skippedIoError: 0
732
+ };
733
+ const index = await this.loadIndex();
734
+ const before = Object.keys(index.trajectories).length;
735
+ const discovered = [];
736
+ try {
737
+ const activeFiles = await readdir(this.activeDir);
738
+ for (const file of activeFiles) {
739
+ if (!file.endsWith(".json")) continue;
740
+ discovered.push(join(this.activeDir, file));
741
+ }
742
+ } catch (error) {
743
+ if (error.code !== "ENOENT") throw error;
744
+ }
745
+ await this.walkJsonFilesInto(this.completedDir, discovered);
746
+ for (const filePath of discovered) {
747
+ summary.scanned += 1;
748
+ const result = await this.readTrajectoryFile(filePath);
749
+ if (!result.ok) {
750
+ if (result.reason === "malformed_json") {
751
+ summary.skippedMalformedJson += 1;
752
+ } else if (result.reason === "schema_violation") {
753
+ summary.skippedSchemaViolation += 1;
754
+ } else {
755
+ summary.skippedIoError += 1;
756
+ }
757
+ continue;
758
+ }
759
+ const trajectory2 = result.trajectory;
760
+ if (index.trajectories[trajectory2.id]) {
761
+ summary.alreadyIndexed += 1;
762
+ continue;
763
+ }
764
+ index.trajectories[trajectory2.id] = {
765
+ title: trajectory2.task.title,
766
+ status: trajectory2.status,
767
+ startedAt: trajectory2.startedAt,
768
+ completedAt: trajectory2.completedAt,
769
+ path: filePath
770
+ };
771
+ summary.added += 1;
772
+ }
773
+ if (Object.keys(index.trajectories).length !== before) {
774
+ await this.saveIndex(index);
775
+ }
776
+ const hadSkips = summary.skippedMalformedJson + summary.skippedSchemaViolation + summary.skippedIoError > 0;
777
+ if (summary.added > 0 || hadSkips) {
778
+ const parts = [`reconciled ${summary.added}/${summary.scanned}`];
779
+ if (summary.skippedMalformedJson > 0) {
780
+ parts.push(`malformed: ${summary.skippedMalformedJson}`);
781
+ }
782
+ if (summary.skippedSchemaViolation > 0) {
783
+ parts.push(`invalid: ${summary.skippedSchemaViolation}`);
784
+ }
785
+ if (summary.skippedIoError > 0) {
786
+ parts.push(`io: ${summary.skippedIoError}`);
787
+ }
788
+ console.warn(`[trajectories] ${parts.join(", ")}`);
789
+ }
790
+ return summary;
699
791
  }
700
792
  /**
701
- * Save a trajectory
793
+ * Recursively collect all .json file paths under `dir` into `out`.
794
+ * Silently treats a missing directory as empty.
702
795
  */
703
- async save(trajectory2) {
796
+ async walkJsonFilesInto(dir, out) {
797
+ let entries;
798
+ try {
799
+ entries = await readdir(dir, { withFileTypes: true });
800
+ } catch (error) {
801
+ if (error.code === "ENOENT") return;
802
+ throw error;
803
+ }
804
+ for (const entry of entries) {
805
+ const entryPath = join(dir, entry.name);
806
+ if (entry.isDirectory()) {
807
+ await this.walkJsonFilesInto(entryPath, out);
808
+ } else if (entry.isFile() && entry.name.endsWith(".json")) {
809
+ out.push(entryPath);
810
+ }
811
+ }
812
+ }
813
+ /**
814
+ * Save a trajectory.
815
+ *
816
+ * Validates the input against the trajectory schema before touching
817
+ * disk. Closes the historical read/write asymmetry where save() would
818
+ * happily write data that the reader then rejected, producing files
819
+ * that could never be loaded back.
820
+ */
821
+ async save(input) {
822
+ const validation = validateTrajectory(input);
823
+ if (!validation.success) {
824
+ const issues = validation.errors?.issues.map((issue) => {
825
+ const path = issue.path.length > 0 ? issue.path.join(".") : "root";
826
+ return `${path}: ${issue.message}`;
827
+ }).join("; ") ?? "unknown validation error";
828
+ throw new Error(`Cannot save invalid trajectory: ${issues}`);
829
+ }
830
+ const trajectory2 = validation.data;
704
831
  const isCompleted = trajectory2.status === "completed" || trajectory2.status === "abandoned";
705
832
  let filePath;
706
833
  if (isCompleted) {
@@ -730,19 +857,23 @@ var FileStorage = class {
730
857
  async get(id) {
731
858
  const activePath = join(this.activeDir, `${id}.json`);
732
859
  if (existsSync(activePath)) {
733
- return this.readTrajectoryFile(activePath);
860
+ return this.readTrajectoryOrNull(activePath);
734
861
  }
735
862
  const index = await this.loadIndex();
736
863
  const entry = index.trajectories[id];
737
864
  if (entry?.path && existsSync(entry.path)) {
738
- return this.readTrajectoryFile(entry.path);
865
+ return this.readTrajectoryOrNull(entry.path);
739
866
  }
740
867
  try {
868
+ const flatPath = join(this.completedDir, `${id}.json`);
869
+ if (existsSync(flatPath)) {
870
+ return this.readTrajectoryOrNull(flatPath);
871
+ }
741
872
  const months = await readdir(this.completedDir);
742
873
  for (const month of months) {
743
874
  const filePath = join(this.completedDir, month, `${id}.json`);
744
875
  if (existsSync(filePath)) {
745
- return this.readTrajectoryFile(filePath);
876
+ return this.readTrajectoryOrNull(filePath);
746
877
  }
747
878
  }
748
879
  } catch (error) {
@@ -765,7 +896,7 @@ var FileStorage = class {
765
896
  let mostRecent = null;
766
897
  let mostRecentTime = 0;
767
898
  for (const file of jsonFiles) {
768
- const trajectory2 = await this.readTrajectoryFile(
899
+ const trajectory2 = await this.readTrajectoryOrNull(
769
900
  join(this.activeDir, file)
770
901
  );
771
902
  if (trajectory2) {
@@ -893,20 +1024,44 @@ var FileStorage = class {
893
1024
  async close() {
894
1025
  }
895
1026
  // Private helpers
1027
+ /**
1028
+ * Read a trajectory file and return a tagged result so callers can
1029
+ * distinguish missing files, malformed JSON, and schema violations.
1030
+ *
1031
+ * Does NOT log. Callers choose whether to warn, swallow, or throw.
1032
+ */
896
1033
  async readTrajectoryFile(path) {
1034
+ let content;
897
1035
  try {
898
- const content = await readFile(path, "utf-8");
899
- const data = JSON.parse(content);
900
- const validation = validateTrajectory(data);
901
- if (validation.success) {
902
- return validation.data;
903
- }
904
- console.error(`Invalid trajectory at ${path}:`, validation.errors);
905
- return null;
1036
+ content = await readFile(path, "utf-8");
906
1037
  } catch (error) {
907
- console.error(`Failed to read trajectory at ${path}:`, error);
908
- return null;
1038
+ return { ok: false, reason: "io_error", path, error };
1039
+ }
1040
+ let data;
1041
+ try {
1042
+ data = JSON.parse(content);
1043
+ } catch (error) {
1044
+ return { ok: false, reason: "malformed_json", path, error };
909
1045
  }
1046
+ const validation = validateTrajectory(data);
1047
+ if (validation.success) {
1048
+ return { ok: true, trajectory: validation.data };
1049
+ }
1050
+ return {
1051
+ ok: false,
1052
+ reason: "schema_violation",
1053
+ path,
1054
+ error: validation.errors
1055
+ };
1056
+ }
1057
+ /**
1058
+ * Convenience wrapper for callers that only care whether they got a
1059
+ * trajectory. Returns null for any failure and writes nothing to the
1060
+ * console — so nothing leaks into test output or the CLI spinner.
1061
+ */
1062
+ async readTrajectoryOrNull(path) {
1063
+ const result = await this.readTrajectoryFile(path);
1064
+ return result.ok ? result.trajectory : null;
910
1065
  }
911
1066
  async loadIndex() {
912
1067
  try {
@@ -944,6 +1099,124 @@ var FileStorage = class {
944
1099
  };
945
1100
 
946
1101
  // src/sdk/client.ts
1102
+ var require2 = createRequire(import.meta.url);
1103
+ function normalizeOptionalString(value) {
1104
+ const normalized = value?.trim();
1105
+ return normalized ? normalized : void 0;
1106
+ }
1107
+ function normalizeAutoCompactOptions(autoCompact) {
1108
+ if (!autoCompact) {
1109
+ return false;
1110
+ }
1111
+ if (autoCompact === true) {
1112
+ return { mechanical: false, markdown: true };
1113
+ }
1114
+ return {
1115
+ mechanical: autoCompact.mechanical ?? false,
1116
+ markdown: autoCompact.markdown ?? true
1117
+ };
1118
+ }
1119
+ function resolveStartWorkflowId(options) {
1120
+ if (options !== void 0 && Object.prototype.hasOwnProperty.call(options, "workflowId")) {
1121
+ return normalizeOptionalString(options.workflowId);
1122
+ }
1123
+ return normalizeOptionalString(process.env.TRAJECTORIES_WORKFLOW_ID);
1124
+ }
1125
+ function resolveTrajectoryCliInvocation() {
1126
+ const explicitCli = normalizeOptionalString(process.env.TRAJECTORIES_CLI);
1127
+ if (explicitCli) {
1128
+ if (/\.(?:cjs|mjs|js)$/i.test(explicitCli)) {
1129
+ return { command: process.execPath, args: [explicitCli] };
1130
+ }
1131
+ return { command: explicitCli, args: [] };
1132
+ }
1133
+ try {
1134
+ const packageJsonPath = require2.resolve("agent-trajectories/package.json");
1135
+ const pkg = JSON.parse(
1136
+ readFileSync(packageJsonPath, "utf-8")
1137
+ );
1138
+ const binEntry = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.trail ?? (pkg.name ? pkg.bin?.[pkg.name] : void 0);
1139
+ if (binEntry) {
1140
+ const cliPath = resolvePath(dirname(packageJsonPath), binEntry);
1141
+ if (existsSync2(cliPath)) {
1142
+ return { command: process.execPath, args: [cliPath] };
1143
+ }
1144
+ }
1145
+ } catch {
1146
+ }
1147
+ return { command: "trail", args: [] };
1148
+ }
1149
+ function parseCompactWorkflowOutput(stdout) {
1150
+ const compactedPath = stdout.match(
1151
+ /^\s*Compacted trajectory saved to:\s*(.+)$/m
1152
+ )?.[1];
1153
+ const markdownPath = stdout.match(
1154
+ /^\s*Markdown summary saved to:\s*(.+)$/m
1155
+ )?.[1];
1156
+ if (!compactedPath) {
1157
+ throw new Error("compactWorkflow failed: unable to parse compacted path");
1158
+ }
1159
+ return {
1160
+ compactedPath: compactedPath.trim(),
1161
+ ...markdownPath ? { markdownPath: markdownPath.trim() } : {}
1162
+ };
1163
+ }
1164
+ async function compactWorkflow(workflowId, options) {
1165
+ const normalizedWorkflowId = normalizeOptionalString(workflowId);
1166
+ if (!normalizedWorkflowId) {
1167
+ throw new Error("compactWorkflow failed: workflowId is required");
1168
+ }
1169
+ const cli = resolveTrajectoryCliInvocation();
1170
+ const args = [
1171
+ ...cli.args,
1172
+ "compact",
1173
+ "--workflow",
1174
+ normalizedWorkflowId,
1175
+ "--all"
1176
+ ];
1177
+ if (options?.markdown) {
1178
+ args.push("--markdown");
1179
+ }
1180
+ if (options?.mechanical) {
1181
+ args.push("--mechanical");
1182
+ }
1183
+ return new Promise((resolve, reject) => {
1184
+ const child = spawn(cli.command, args, {
1185
+ cwd: options?.cwd,
1186
+ stdio: ["ignore", "pipe", "pipe"]
1187
+ });
1188
+ const stdoutChunks = [];
1189
+ let stderr = "";
1190
+ child.stdout.on("data", (chunk) => {
1191
+ stdoutChunks.push(Buffer.from(chunk));
1192
+ });
1193
+ child.stderr.on("data", (chunk) => {
1194
+ stderr += chunk.toString();
1195
+ process.stderr.write(chunk);
1196
+ });
1197
+ child.on("error", (error) => {
1198
+ reject(new Error(`compactWorkflow failed: ${error.message}`));
1199
+ });
1200
+ child.on("close", (code) => {
1201
+ if (code !== 0) {
1202
+ reject(
1203
+ new Error(
1204
+ `compactWorkflow failed: ${stderr.trim() || `exit code ${code}`}`
1205
+ )
1206
+ );
1207
+ return;
1208
+ }
1209
+ try {
1210
+ const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
1211
+ resolve(parseCompactWorkflowOutput(stdout));
1212
+ } catch (error) {
1213
+ reject(
1214
+ error instanceof Error ? error : new Error("compactWorkflow failed: unable to parse CLI output")
1215
+ );
1216
+ }
1217
+ });
1218
+ });
1219
+ }
947
1220
  var TrajectorySession = class {
948
1221
  trajectory;
949
1222
  client;
@@ -965,6 +1238,23 @@ var TrajectorySession = class {
965
1238
  get id() {
966
1239
  return this.trajectory.id;
967
1240
  }
1241
+ async autoCompactIfConfigured() {
1242
+ const autoCompact = this.client.getAutoCompactOptions();
1243
+ if (!autoCompact || !this.trajectory.workflowId) {
1244
+ return;
1245
+ }
1246
+ try {
1247
+ await compactWorkflow(this.trajectory.workflowId, {
1248
+ ...autoCompact,
1249
+ cwd: this.client.getAutoCompactCwd()
1250
+ });
1251
+ } catch (error) {
1252
+ const message = error instanceof Error ? error.message : String(error);
1253
+ console.error(
1254
+ `Warning: autoCompact failed for workflow ${this.trajectory.workflowId}: ${message}`
1255
+ );
1256
+ }
1257
+ }
968
1258
  /**
969
1259
  * Start a new chapter
970
1260
  * @param title - Chapter title
@@ -1074,6 +1364,7 @@ var TrajectorySession = class {
1074
1364
  async complete(input) {
1075
1365
  this.trajectory = completeTrajectory(this.trajectory, input);
1076
1366
  await this.client.save(this.trajectory);
1367
+ await this.autoCompactIfConfigured();
1077
1368
  return this.trajectory;
1078
1369
  }
1079
1370
  /**
@@ -1139,11 +1430,21 @@ var TrajectoryClient = class {
1139
1430
  defaultAgent;
1140
1431
  projectId;
1141
1432
  autoSave;
1433
+ autoCompactCwd;
1434
+ autoCompact;
1142
1435
  constructor(options = {}) {
1143
1436
  this.storage = options.storage ?? new FileStorage(options.dataDir);
1144
1437
  this.defaultAgent = options.defaultAgent ?? process.env.TRAJECTORIES_AGENT;
1145
1438
  this.projectId = options.projectId ?? process.env.TRAJECTORIES_PROJECT;
1146
1439
  this.autoSave = options.autoSave ?? true;
1440
+ this.autoCompact = normalizeAutoCompactOptions(options.autoCompact);
1441
+ this.autoCompactCwd = options.storage ? void 0 : options.dataDir;
1442
+ }
1443
+ getAutoCompactOptions() {
1444
+ return this.autoCompact;
1445
+ }
1446
+ getAutoCompactCwd() {
1447
+ return this.autoCompactCwd;
1147
1448
  }
1148
1449
  /**
1149
1450
  * Initialize the client (creates storage directories, etc.)
@@ -1183,13 +1484,16 @@ var TrajectoryClient = class {
1183
1484
  "Complete or abandon the active trajectory first"
1184
1485
  );
1185
1486
  }
1487
+ const workflowId = resolveStartWorkflowId(options);
1488
+ const { workflowId: _workflowId, ...createOptions } = options ?? {};
1186
1489
  const trajectory2 = createTrajectory({
1187
1490
  title,
1188
1491
  projectId: this.projectId,
1189
- ...options
1492
+ ...createOptions
1190
1493
  });
1191
- await this.storage.save(trajectory2);
1192
- return new TrajectorySession(trajectory2, this, this.autoSave);
1494
+ const stampedTrajectory = workflowId ? { ...trajectory2, workflowId } : trajectory2;
1495
+ await this.storage.save(stampedTrajectory);
1496
+ return new TrajectorySession(stampedTrajectory, this, this.autoSave);
1193
1497
  }
1194
1498
  /**
1195
1499
  * Resume the currently active trajectory
@@ -1573,7 +1877,7 @@ function trajectory(title) {
1573
1877
 
1574
1878
  // src/core/trailers.ts
1575
1879
  import { execSync as execSync2 } from "child_process";
1576
- import { readFileSync } from "fs";
1880
+ import { readFileSync as readFileSync2 } from "fs";
1577
1881
 
1578
1882
  // src/core/trace.ts
1579
1883
  import { execSync } from "child_process";
@@ -1615,7 +1919,7 @@ function parseTrajectoryFromMessage(commitMessage) {
1615
1919
  const lines = commitMessage.split("\n");
1616
1920
  for (const line of lines) {
1617
1921
  const match = line.match(
1618
- new RegExp(`^${TRAJECTORY_TRAILER_KEY}:\\s*(traj_[a-z0-9]+)$`)
1922
+ new RegExp(`^${TRAJECTORY_TRAILER_KEY}:\\s*(traj_[a-z0-9_]+)$`)
1619
1923
  );
1620
1924
  if (match) {
1621
1925
  return match[1];
@@ -1717,6 +2021,7 @@ export {
1717
2021
  exportToPRSummary,
1718
2022
  exportToTimeline,
1719
2023
  FileStorage,
2024
+ compactWorkflow,
1720
2025
  TrajectorySession,
1721
2026
  TrajectoryClient,
1722
2027
  TrajectoryBuilder,
@@ -1728,4 +2033,4 @@ export {
1728
2033
  getCommitsBetween,
1729
2034
  getFilesChangedBetween
1730
2035
  };
1731
- //# sourceMappingURL=chunk-4MACDCF4.js.map
2036
+ //# sourceMappingURL=chunk-W222QB6V.js.map