@mastra/spanner 1.6.2-alpha.0 → 1.6.2-alpha.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/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # @mastra/spanner
2
2
 
3
+ ## 1.6.2-alpha.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Fixed versioned dataset item lookups to return the item visible in the requested dataset snapshot. ([#21979](https://github.com/mastra-ai/mastra/pull/21979))
8
+
9
+ - Added HTTP endpoints for caller-driven experiments: `POST /datasets/:datasetId/experiments` now accepts `start: false` to create an experiment without spawning the runner (with an optional target and run-level `scorerIds`), and new routes `POST /datasets/:datasetId/experiments/:experimentId/items/:itemId/run`, `POST /datasets/:datasetId/experiments/:experimentId/results`, and `POST /datasets/:datasetId/experiments/:experimentId/finalize` let external orchestrators run one item server-side, submit externally computed per-item results (idempotent upsert on retries), and finalize the run with server-computed counts. ([#21888](https://github.com/mastra-ai/mastra/pull/21888))
10
+
11
+ - Added `createDatasetExperiment()`, `runExperimentItem()`, `submitExperimentResult()`, and `finalizeExperiment()` methods so a caller-owned orchestrator (for example Temporal) can drive an experiment loop while Mastra either executes each item server-side or ingests externally computed results. ([#21888](https://github.com/mastra-ai/mastra/pull/21888))
12
+
13
+ - Added `upsertExperimentResult()` to the experiments storage domain plus an `attempt` column on experiment results and a nullable target with an optional `scorerIds` column on experiments, enabling retry-safe result writes for caller-driven experiments (retried submissions with the same `(experimentId, itemId, attempt)` key converge on a single row). `saveScore()` now accepts an optional caller-supplied `id` and upserts on it, so retried experiment submissions replace their previous score rows (latest wins) instead of accumulating duplicates. ([#21888](https://github.com/mastra-ai/mastra/pull/21888))
14
+
15
+ - Added caller-driven experiments so an external orchestrator (for example Temporal workers) can own the experiment loop while Mastra stays the system of record. ([#21888](https://github.com/mastra-ai/mastra/pull/21888))
16
+
17
+ Create an experiment with `dataset.createExperiment()` (idempotent when you pass your own id). With a target, Mastra runs each item for you: call `dataset.runExperimentItem()` per item and Mastra executes the registered agent or workflow, resolves scorers (experiment `scorers`, falling back to item `scorerIds`, then dataset `scorerIds`), and upserts the result. Without a target, run everything yourself and report per-item results with `dataset.submitExperimentResult()` (upsert semantics on `(experimentId, itemId, attempt)` so retried workers converge on a single row). Either way, close the run with `dataset.finalizeExperiment()` and Mastra computes per-item succeeded/failed/skipped counts from the persisted rows. Results go into the same storage as native runs, so Studio views, comparisons, and review summaries work unchanged.
18
+
19
+ ```typescript
20
+ // Caller drives the loop, Mastra runs each item
21
+ const { experimentId } = await dataset.createExperiment({
22
+ id: workflowRunId,
23
+ targetType: 'agent',
24
+ targetId: 'support-agent',
25
+ scorers: ['accuracy'],
26
+ });
27
+
28
+ await dataset.runExperimentItem({ experimentId, itemId });
29
+
30
+ // Or: caller runs everything, Mastra ingests results
31
+ const ingest = await dataset.createExperiment({ id: workflowRunId });
32
+ await dataset.submitExperimentResult({
33
+ experimentId: ingest.experimentId,
34
+ itemId,
35
+ output,
36
+ scores: [{ scorerId: 'accuracy', score: 0.92 }],
37
+ });
38
+
39
+ const experiment = await dataset.finalizeExperiment({ experimentId });
40
+ ```
41
+
42
+ - Updated dependencies [[`9267e9b`](https://github.com/mastra-ai/mastra/commit/9267e9b3d9c2fcf16936050495a787054c2431ab), [`acc3471`](https://github.com/mastra-ai/mastra/commit/acc3471de5f3fde8027ee4e355af292b2bc1bc30), [`b6a771e`](https://github.com/mastra-ai/mastra/commit/b6a771ef23d203ddb348efca8065eff65def8191), [`9267e9b`](https://github.com/mastra-ai/mastra/commit/9267e9b3d9c2fcf16936050495a787054c2431ab), [`26d4016`](https://github.com/mastra-ai/mastra/commit/26d40160ff7f7d8bf95fee2039a52cbc83863533), [`9267e9b`](https://github.com/mastra-ai/mastra/commit/9267e9b3d9c2fcf16936050495a787054c2431ab), [`9267e9b`](https://github.com/mastra-ai/mastra/commit/9267e9b3d9c2fcf16936050495a787054c2431ab), [`57c5103`](https://github.com/mastra-ai/mastra/commit/57c51035a2a36e3df3c4f32f46bb789a66ed5946)]:
43
+ - @mastra/core@1.61.0-alpha.3
44
+
3
45
  ## 1.6.2-alpha.0
4
46
 
5
47
  ### Patch Changes
@@ -3,7 +3,7 @@ name: mastra-spanner
3
3
  description: Documentation for @mastra/spanner. Use when working with @mastra/spanner APIs, configuration, or implementation.
4
4
  metadata:
5
5
  package: "@mastra/spanner"
6
- version: "1.6.2-alpha.0"
6
+ version: "1.6.2-alpha.1"
7
7
  ---
8
8
 
9
9
  ## When to use
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.6.2-alpha.0",
2
+ "version": "1.6.2-alpha.1",
3
3
  "package": "@mastra/spanner",
4
4
  "exports": {},
5
5
  "modules": {}
package/dist/index.cjs CHANGED
@@ -3651,8 +3651,11 @@ var DatasetsSpanner = class DatasetsSpanner extends _mastra_core_storage.Dataset
3651
3651
  if (args.datasetVersion !== void 0) {
3652
3652
  sql = `SELECT * FROM ${tableName}
3653
3653
  WHERE ${quoteIdent("id", "column name")} = @id
3654
- AND ${quoteIdent("datasetVersion", "column name")} = @datasetVersion
3655
- AND ${quoteIdent("isDeleted", "column name")} = FALSE LIMIT 1`;
3654
+ AND ${quoteIdent("datasetVersion", "column name")} <= @datasetVersion
3655
+ AND (${quoteIdent("validTo", "column name")} IS NULL OR ${quoteIdent("validTo", "column name")} > @datasetVersion)
3656
+ AND ${quoteIdent("isDeleted", "column name")} = FALSE
3657
+ ORDER BY ${quoteIdent("datasetVersion", "column name")} DESC
3658
+ LIMIT 1`;
3656
3659
  params.datasetVersion = args.datasetVersion;
3657
3660
  } else sql = `SELECT * FROM ${tableName}
3658
3661
  WHERE ${quoteIdent("id", "column name")} = @id
@@ -3983,8 +3986,9 @@ function rowToExperiment(row) {
3983
3986
  datasetVersion: t.datasetVersion == null ? null : Number(t.datasetVersion),
3984
3987
  organizationId: t.organizationId ?? null,
3985
3988
  projectId: t.projectId ?? null,
3986
- targetType: t.targetType,
3987
- targetId: String(t.targetId),
3989
+ targetType: t.targetType ?? null,
3990
+ targetId: t.targetId == null ? null : String(t.targetId),
3991
+ scorerIds: t.scorerIds ?? null,
3988
3992
  status: t.status,
3989
3993
  totalItems: Number(t.totalItems ?? 0),
3990
3994
  succeededCount: Number(t.succeededCount ?? 0),
@@ -4016,6 +4020,7 @@ function rowToExperimentResult(row) {
4016
4020
  startedAt: toDate(t.startedAt),
4017
4021
  completedAt: toDate(t.completedAt),
4018
4022
  retryCount: Number(t.retryCount ?? 0),
4023
+ attempt: t.attempt == null ? 0 : Number(t.attempt),
4019
4024
  traceId: t.traceId ?? null,
4020
4025
  status: t.status ?? null,
4021
4026
  tags: t.tags ?? null,
@@ -4068,7 +4073,8 @@ var ExperimentsSpanner = class ExperimentsSpanner extends _mastra_core_storage.E
4068
4073
  "experimentSetId",
4069
4074
  "comparisonId",
4070
4075
  "variantId",
4071
- "trialIndex"
4076
+ "trialIndex",
4077
+ "scorerIds"
4072
4078
  ]
4073
4079
  });
4074
4080
  await this.db.alterTable({
@@ -4077,7 +4083,8 @@ var ExperimentsSpanner = class ExperimentsSpanner extends _mastra_core_storage.E
4077
4083
  ifNotExists: [
4078
4084
  "comment",
4079
4085
  "organizationId",
4080
- "projectId"
4086
+ "projectId",
4087
+ "attempt"
4081
4088
  ]
4082
4089
  });
4083
4090
  await this.createDefaultIndexes();
@@ -4106,9 +4113,13 @@ var ExperimentsSpanner = class ExperimentsSpanner extends _mastra_core_storage.E
4106
4113
  columns: ["experimentId", "startedAt"]
4107
4114
  },
4108
4115
  {
4109
- name: "mastra_experiment_results_exp_item_idx",
4116
+ name: "mastra_experiment_results_exp_item_attempt_idx",
4110
4117
  table: _mastra_core_storage.TABLE_EXPERIMENT_RESULTS,
4111
- columns: ["experimentId", "itemId"],
4118
+ columns: [
4119
+ "experimentId",
4120
+ "itemId",
4121
+ "attempt"
4122
+ ],
4112
4123
  unique: true
4113
4124
  },
4114
4125
  {
@@ -4125,6 +4136,7 @@ var ExperimentsSpanner = class ExperimentsSpanner extends _mastra_core_storage.E
4125
4136
  }
4126
4137
  async createDefaultIndexes() {
4127
4138
  if (this.skipDefaultIndexes) return;
4139
+ await this.db.dropIndex("mastra_experiment_results_exp_item_idx").catch(() => {});
4128
4140
  await this.db.createIndexes(this.getDefaultIndexDefinitions());
4129
4141
  }
4130
4142
  async createCustomIndexes() {
@@ -4154,8 +4166,9 @@ var ExperimentsSpanner = class ExperimentsSpanner extends _mastra_core_storage.E
4154
4166
  datasetVersion: input.datasetVersion ?? null,
4155
4167
  organizationId: input.organizationId ?? null,
4156
4168
  projectId: input.projectId ?? null,
4157
- targetType: input.targetType,
4158
- targetId: input.targetId,
4169
+ targetType: input.targetType ?? null,
4170
+ targetId: input.targetId ?? null,
4171
+ scorerIds: input.scorerIds ?? null,
4159
4172
  status: "pending",
4160
4173
  totalItems: input.totalItems,
4161
4174
  succeededCount: 0,
@@ -4186,6 +4199,7 @@ var ExperimentsSpanner = class ExperimentsSpanner extends _mastra_core_storage.E
4186
4199
  projectId: experiment.projectId,
4187
4200
  targetType: experiment.targetType,
4188
4201
  targetId: experiment.targetId,
4202
+ scorerIds: experiment.scorerIds,
4189
4203
  status: experiment.status,
4190
4204
  totalItems: experiment.totalItems,
4191
4205
  succeededCount: 0,
@@ -4465,6 +4479,7 @@ var ExperimentsSpanner = class ExperimentsSpanner extends _mastra_core_storage.E
4465
4479
  startedAt: input.startedAt,
4466
4480
  completedAt: input.completedAt,
4467
4481
  retryCount: input.retryCount,
4482
+ attempt: input.attempt ?? 0,
4468
4483
  traceId: input.traceId ?? null,
4469
4484
  status: input.status ?? null,
4470
4485
  tags: input.tags ?? null,
@@ -4487,6 +4502,7 @@ var ExperimentsSpanner = class ExperimentsSpanner extends _mastra_core_storage.E
4487
4502
  startedAt: result.startedAt,
4488
4503
  completedAt: result.completedAt,
4489
4504
  retryCount: result.retryCount,
4505
+ attempt: result.attempt,
4490
4506
  traceId: result.traceId,
4491
4507
  status: result.status,
4492
4508
  tags: result.tags,
@@ -4507,6 +4523,116 @@ var ExperimentsSpanner = class ExperimentsSpanner extends _mastra_core_storage.E
4507
4523
  }, error);
4508
4524
  }
4509
4525
  }
4526
+ async upsertExperimentResult(input) {
4527
+ try {
4528
+ const attempt = input.attempt ?? 0;
4529
+ const [rows] = await this.database.run({
4530
+ sql: `SELECT ${quoteIdent("id", "column name")} FROM ${quoteIdent(_mastra_core_storage.TABLE_EXPERIMENT_RESULTS, "table name")}
4531
+ WHERE ${quoteIdent("experimentId", "column name")} = @experimentId
4532
+ AND ${quoteIdent("itemId", "column name")} = @itemId
4533
+ AND COALESCE(${quoteIdent("attempt", "column name")}, 0) = @attempt`,
4534
+ params: {
4535
+ experimentId: input.experimentId,
4536
+ itemId: input.itemId,
4537
+ attempt
4538
+ },
4539
+ json: true
4540
+ });
4541
+ let existingId = rows[0]?.id;
4542
+ if (!existingId) try {
4543
+ return await this.addExperimentResult({
4544
+ ...input,
4545
+ attempt
4546
+ });
4547
+ } catch (insertError) {
4548
+ if (!(0, _mastra_core_storage.hasErrorCode)(insertError, /* @__PURE__ */ new Set([6]))) throw insertError;
4549
+ const [winnerRows] = await this.database.run({
4550
+ sql: `SELECT ${quoteIdent("id", "column name")} FROM ${quoteIdent(_mastra_core_storage.TABLE_EXPERIMENT_RESULTS, "table name")}
4551
+ WHERE ${quoteIdent("experimentId", "column name")} = @experimentId
4552
+ AND ${quoteIdent("itemId", "column name")} = @itemId
4553
+ AND COALESCE(${quoteIdent("attempt", "column name")}, 0) = @attempt`,
4554
+ params: {
4555
+ experimentId: input.experimentId,
4556
+ itemId: input.itemId,
4557
+ attempt
4558
+ },
4559
+ json: true
4560
+ });
4561
+ existingId = winnerRows[0]?.id;
4562
+ if (!existingId) throw insertError;
4563
+ }
4564
+ await this.db.runDml({
4565
+ sql: `UPDATE ${quoteIdent(_mastra_core_storage.TABLE_EXPERIMENT_RESULTS, "table name")} SET
4566
+ ${quoteIdent("itemDatasetVersion", "column name")} = @itemDatasetVersion,
4567
+ ${quoteIdent("organizationId", "column name")} = @organizationId,
4568
+ ${quoteIdent("projectId", "column name")} = @projectId,
4569
+ ${quoteIdent("input", "column name")} = @input,
4570
+ ${quoteIdent("output", "column name")} = @output,
4571
+ ${quoteIdent("groundTruth", "column name")} = @groundTruth,
4572
+ ${quoteIdent("error", "column name")} = @error,
4573
+ ${quoteIdent("startedAt", "column name")} = @startedAt,
4574
+ ${quoteIdent("completedAt", "column name")} = @completedAt,
4575
+ ${quoteIdent("retryCount", "column name")} = @retryCount,
4576
+ ${quoteIdent("attempt", "column name")} = @attempt,
4577
+ ${quoteIdent("traceId", "column name")} = @traceId,
4578
+ ${quoteIdent("status", "column name")} = @status,
4579
+ ${quoteIdent("tags", "column name")} = @tags,
4580
+ ${quoteIdent("toolMockReport", "column name")} = @toolMockReport
4581
+ WHERE ${quoteIdent("id", "column name")} = @id`,
4582
+ params: {
4583
+ id: existingId,
4584
+ itemDatasetVersion: input.itemDatasetVersion ?? null,
4585
+ organizationId: input.organizationId ?? null,
4586
+ projectId: input.projectId ?? null,
4587
+ input: JSON.stringify(input.input),
4588
+ output: input.output == null ? null : JSON.stringify(input.output),
4589
+ groundTruth: input.groundTruth == null ? null : JSON.stringify(input.groundTruth),
4590
+ error: input.error == null ? null : JSON.stringify(input.error),
4591
+ startedAt: input.startedAt,
4592
+ completedAt: input.completedAt,
4593
+ retryCount: input.retryCount,
4594
+ attempt,
4595
+ traceId: input.traceId ?? null,
4596
+ status: input.status ?? null,
4597
+ tags: input.tags == null ? null : JSON.stringify(input.tags),
4598
+ toolMockReport: input.toolMockReport == null ? null : JSON.stringify(input.toolMockReport)
4599
+ },
4600
+ types: {
4601
+ itemDatasetVersion: "int64",
4602
+ organizationId: "string",
4603
+ projectId: "string",
4604
+ input: "json",
4605
+ output: "json",
4606
+ groundTruth: "json",
4607
+ error: "json",
4608
+ traceId: "string",
4609
+ status: "string",
4610
+ tags: "json",
4611
+ toolMockReport: "json"
4612
+ }
4613
+ });
4614
+ const updated = await this.getExperimentResultById({ id: existingId });
4615
+ if (!updated) throw new _mastra_core_error.MastraError({
4616
+ id: (0, _mastra_core_storage.createStorageErrorId)("SPANNER", "UPSERT_EXPERIMENT_RESULT", "NOT_FOUND"),
4617
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
4618
+ category: _mastra_core_error.ErrorCategory.USER,
4619
+ text: `Experiment result ${existingId} not found after upsert`,
4620
+ details: { id: existingId }
4621
+ });
4622
+ return updated;
4623
+ } catch (error) {
4624
+ if (error instanceof _mastra_core_error.MastraError) throw error;
4625
+ throw new _mastra_core_error.MastraError({
4626
+ id: (0, _mastra_core_storage.createStorageErrorId)("SPANNER", "UPSERT_EXPERIMENT_RESULT", "FAILED"),
4627
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
4628
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
4629
+ details: {
4630
+ experimentId: input.experimentId,
4631
+ itemId: input.itemId
4632
+ }
4633
+ }, error);
4634
+ }
4635
+ }
4510
4636
  async updateExperimentResult(input) {
4511
4637
  try {
4512
4638
  if (input.status === void 0 && input.tags === void 0 && input.comment === void 0) {
@@ -10800,9 +10926,10 @@ var ScoresSpanner = class ScoresSpanner extends _mastra_core_storage.ScoresStora
10800
10926
  }, error);
10801
10927
  }
10802
10928
  try {
10803
- const scoreId = (0, crypto.randomUUID)();
10929
+ const suppliedId = validatedScore.id;
10930
+ const scoreId = suppliedId ?? (0, crypto.randomUUID)();
10804
10931
  const now = /* @__PURE__ */ new Date();
10805
- const { scorer, preprocessStepResult, analyzeStepResult, metadata, input, output, additionalContext, requestContext, entity, ...rest } = validatedScore;
10932
+ const { id: _suppliedId, scorer, preprocessStepResult, analyzeStepResult, metadata, input, output, additionalContext, requestContext, entity, ...rest } = validatedScore;
10806
10933
  const insertedRecord = {
10807
10934
  id: scoreId,
10808
10935
  ...rest,
@@ -10818,7 +10945,11 @@ var ScoresSpanner = class ScoresSpanner extends _mastra_core_storage.ScoresStora
10818
10945
  createdAt: now,
10819
10946
  updatedAt: now
10820
10947
  };
10821
- await this.db.insert({
10948
+ if (suppliedId) await this.db.upsert({
10949
+ tableName: _mastra_core_storage.TABLE_SCORERS,
10950
+ record: insertedRecord
10951
+ });
10952
+ else await this.db.insert({
10822
10953
  tableName: _mastra_core_storage.TABLE_SCORERS,
10823
10954
  record: insertedRecord
10824
10955
  });