@mastra/factory 0.1.0 → 0.2.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/factory.js +142 -35
  3. package/dist/factory.js.map +1 -1
  4. package/dist/index.js +142 -35
  5. package/dist/index.js.map +1 -1
  6. package/dist/integrations/github/integration.js +1 -1
  7. package/dist/integrations/github/integration.js.map +1 -1
  8. package/dist/integrations/github/routes.js +1 -1
  9. package/dist/integrations/github/routes.js.map +1 -1
  10. package/dist/integrations/platform/api-client.js +1 -1
  11. package/dist/integrations/platform/api-client.js.map +1 -1
  12. package/dist/integrations/platform/github/event-worker.js.map +1 -1
  13. package/dist/integrations/platform/github/integration.js +2 -2
  14. package/dist/integrations/platform/github/integration.js.map +1 -1
  15. package/dist/integrations/platform/linear/integration.js +1 -1
  16. package/dist/integrations/platform/linear/integration.js.map +1 -1
  17. package/dist/routes/config.d.ts +4 -0
  18. package/dist/routes/config.d.ts.map +1 -1
  19. package/dist/routes/config.js +89 -26
  20. package/dist/routes/config.js.map +1 -1
  21. package/dist/routes/surface.d.ts.map +1 -1
  22. package/dist/routes/surface.js +136 -31
  23. package/dist/routes/surface.js.map +1 -1
  24. package/dist/routes/work-items.d.ts.map +1 -1
  25. package/dist/routes/work-items.js +2 -1
  26. package/dist/routes/work-items.js.map +1 -1
  27. package/dist/rules/defaults.d.ts.map +1 -1
  28. package/dist/rules/defaults.js +3 -1
  29. package/dist/rules/defaults.js.map +1 -1
  30. package/dist/rules/github-service.d.ts.map +1 -1
  31. package/dist/rules/github-service.js +10 -2
  32. package/dist/rules/github-service.js.map +1 -1
  33. package/dist/rules/index.js +3 -1
  34. package/dist/rules/index.js.map +1 -1
  35. package/dist/rules/start-coordinator.d.ts +3 -1
  36. package/dist/rules/start-coordinator.d.ts.map +1 -1
  37. package/dist/rules/start-coordinator.js +33 -1
  38. package/dist/rules/start-coordinator.js.map +1 -1
  39. package/dist/server-error.js +1 -1
  40. package/dist/server-error.js.map +1 -1
  41. package/package.json +6 -6
@@ -24900,6 +24900,7 @@ var FactoryGithubEventService = class {
24900
24900
  repositoryId,
24901
24901
  issueNumber,
24902
24902
  pullRequestNumber,
24903
+ string(object(pullRequest?.head)?.ref),
24903
24904
  provenance
24904
24905
  );
24905
24906
  const actor = await githubActor(this.options.github, {
@@ -24994,7 +24995,7 @@ var FactoryGithubEventService = class {
24994
24995
  });
24995
24996
  return { status: committed.status };
24996
24997
  }
24997
- async #relatedItem(orgId, projectId, repositoryId, issueNumber, pullRequestNumber, provenance) {
24998
+ async #relatedItem(orgId, projectId, repositoryId, issueNumber, pullRequestNumber, pullRequestHeadBranch, provenance) {
24998
24999
  const items = await this.options.storage.list({ orgId, factoryProjectId: projectId });
24999
25000
  if (provenance) return items.find((item) => item.id === provenance.workItemId);
25000
25001
  if (issueNumber) {
@@ -25003,7 +25004,14 @@ var FactoryGithubEventService = class {
25003
25004
  if (pullRequestNumber) {
25004
25005
  return items.find((item) => item.externalSource?.externalId === canonicalSourceKey("pull-request", pullRequestNumber)) ?? items.find(
25005
25006
  (item) => item.externalSource?.externalId === legacySourceKey(repositoryId, "pull-request", pullRequestNumber)
25006
- );
25007
+ ) ?? // Provenance fallback: a PR pushed from a work item's session branch
25008
+ // belongs to that item even when no gh-pr-create provenance was
25009
+ // recorded (session predating state seeding, or the PR was opened
25010
+ // outside the tracked tool call). Session branches are per-item
25011
+ // (`factory/issue-N`), so a head-branch match is unambiguous.
25012
+ (pullRequestHeadBranch ? items.find(
25013
+ (item) => item.externalSource?.type !== "pull-request" && Object.values(item.sessions).some((session) => session.branch === pullRequestHeadBranch)
25014
+ ) : void 0);
25007
25015
  }
25008
25016
  return void 0;
25009
25017
  }
@@ -25158,16 +25166,28 @@ async function configureThread(session, request) {
25158
25166
  await Promise.all(Object.entries(settings).map(([key, value]) => session.thread.setSetting({ key, value })));
25159
25167
  return threadId;
25160
25168
  }
25169
+ async function applyMemorySettings(session, record) {
25170
+ if (record?.observerModelId) await session.om.observer.switchModel({ modelId: record.observerModelId });
25171
+ if (record?.reflectorModelId) await session.om.reflector.switchModel({ modelId: record.reflectorModelId });
25172
+ const state = {
25173
+ ...record?.observationThreshold != null ? { observationThreshold: record.observationThreshold } : {},
25174
+ ...record?.reflectionThreshold != null ? { reflectionThreshold: record.reflectionThreshold } : {},
25175
+ ...record?.observeAttachments != null ? { observeAttachments: record.observeAttachments } : {}
25176
+ };
25177
+ if (Object.keys(state).length > 0) await session.state.set(state);
25178
+ }
25161
25179
  var FactoryStartCoordinator = class {
25162
25180
  #controller;
25163
25181
  #storage;
25164
25182
  #transitionService;
25165
25183
  #sourceControl;
25166
- constructor(controller, storage, transitionService, sourceControl) {
25184
+ #memorySettings;
25185
+ constructor(controller, storage, transitionService, sourceControl, memorySettings) {
25167
25186
  this.#controller = controller;
25168
25187
  this.#storage = storage;
25169
25188
  this.#transitionService = transitionService;
25170
25189
  this.#sourceControl = sourceControl;
25190
+ this.#memorySettings = memorySettings;
25171
25191
  }
25172
25192
  async prepare(request) {
25173
25193
  const storage = this.#storage;
@@ -25190,6 +25210,26 @@ var FactoryStartCoordinator = class {
25190
25210
  tags: sessionState
25191
25211
  });
25192
25212
  await session.state.set(sessionState);
25213
+ if (this.#memorySettings) {
25214
+ try {
25215
+ const record = await this.#memorySettings.get({ orgId: request.orgId, userId: request.userId });
25216
+ await applyMemorySettings(session, record);
25217
+ } catch (error) {
25218
+ console.warn("[Factory Start] Failed to apply observational-memory settings", {
25219
+ error: error instanceof Error ? error.message : String(error)
25220
+ });
25221
+ }
25222
+ }
25223
+ if (request.defaultModelId) {
25224
+ try {
25225
+ await session.model.switch({ modelId: request.defaultModelId });
25226
+ } catch (error) {
25227
+ console.warn("[Factory Start] Failed to apply factory default model", {
25228
+ modelId: request.defaultModelId,
25229
+ error: error instanceof Error ? error.message : String(error)
25230
+ });
25231
+ }
25232
+ }
25193
25233
  const threadId = await configureThread(session, request);
25194
25234
  const kickoffMessage = await resolveKickoffMessage(session, request.invocation);
25195
25235
  const prepared = await storage.prepareRunStart({
@@ -25745,6 +25785,16 @@ async function applyPackToSession({
25745
25785
  }
25746
25786
  var DEFAULT_OBSERVATION_THRESHOLD = 3e4;
25747
25787
  var DEFAULT_REFLECTION_THRESHOLD = 4e4;
25788
+ function providerOMModelId(providerId, factoryModelId) {
25789
+ switch (providerId) {
25790
+ case "anthropic":
25791
+ return "anthropic/claude-haiku-4-5";
25792
+ case "openai":
25793
+ return "openai/gpt-5.4-mini";
25794
+ default:
25795
+ return factoryModelId || DEFAULT_OM_MODEL_ID;
25796
+ }
25797
+ }
25748
25798
  function readOMConfig(session) {
25749
25799
  const state = session.state.get() ?? {};
25750
25800
  const observeAttachments = state.observeAttachments;
@@ -25756,6 +25806,15 @@ function readOMConfig(session) {
25756
25806
  observeAttachments: observeAttachments === true || observeAttachments === false ? observeAttachments : "auto"
25757
25807
  };
25758
25808
  }
25809
+ function readStoredOMConfig(record) {
25810
+ return {
25811
+ observerModelId: record?.observerModelId ?? DEFAULT_OM_MODEL_ID,
25812
+ reflectorModelId: record?.reflectorModelId ?? DEFAULT_OM_MODEL_ID,
25813
+ observationThreshold: record?.observationThreshold ?? DEFAULT_OBSERVATION_THRESHOLD,
25814
+ reflectionThreshold: record?.reflectionThreshold ?? DEFAULT_REFLECTION_THRESHOLD,
25815
+ observeAttachments: record?.observeAttachments ?? "auto"
25816
+ };
25817
+ }
25759
25818
  async function resolveMemorySettingsContext({
25760
25819
  c,
25761
25820
  auth,
@@ -26165,19 +26224,60 @@ var ConfigRoutes = class extends Route {
26165
26224
  }
26166
26225
  }
26167
26226
  }),
26227
+ registerApiRoute("/web/config/om/provider-defaults", {
26228
+ method: "POST",
26229
+ requiresAuth: false,
26230
+ handler: async (c) => {
26231
+ let body;
26232
+ try {
26233
+ body = await c.req.json();
26234
+ } catch {
26235
+ return c.json({ error: "Invalid JSON body" }, 400);
26236
+ }
26237
+ const providerId = typeof body.providerId === "string" ? body.providerId.trim() : "";
26238
+ const factoryModelId = typeof body.factoryModelId === "string" ? body.factoryModelId.trim() : "";
26239
+ if (!providerId) return c.json({ error: "Missing required field: providerId" }, 400);
26240
+ const context = await resolveMemorySettingsContext({
26241
+ c: loose(c),
26242
+ auth,
26243
+ memorySettings: options.memorySettings
26244
+ });
26245
+ if ("response" in context) return context.response;
26246
+ try {
26247
+ const tenantCredentials = await listTenantCredentialsForRequest({
26248
+ c: loose(c),
26249
+ auth,
26250
+ credentials: options.modelCredentials
26251
+ });
26252
+ const access = await buildProviderAccess({
26253
+ controller,
26254
+ authStorage: tenantCredentials ? void 0 : authStorage,
26255
+ tenantCredentials
26256
+ });
26257
+ if (!access[providerId]) return c.json({ error: `Provider "${providerId}" is not configured` }, 400);
26258
+ const modelId = providerOMModelId(providerId, factoryModelId);
26259
+ const record = await context.storage.patch({
26260
+ orgId: context.orgId,
26261
+ userId: context.userId,
26262
+ patch: {},
26263
+ fillIfUnset: { observerModelId: modelId, reflectorModelId: modelId }
26264
+ });
26265
+ return c.json({ ok: true, config: readStoredOMConfig(record) });
26266
+ } catch (error) {
26267
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
26268
+ }
26269
+ }
26270
+ }),
26168
26271
  // ── Observational memory ──────────────────────────────────────────────────
26169
- // Mirrors the TUI's /om command. All five knobs are session-scoped (resolved
26170
- // from the session, persisted to its state + thread setting) and durably
26171
- // stored in the per-(org, user) `memory-settings` app table never
26172
- // settings.json. GET hydrates the session from the stored row first so the
26173
- // DB, not the SDK's boot-time seed, is the source of truth.
26272
+ // Mirrors the TUI's /om command. All five knobs are durably stored in the
26273
+ // per-(org, user) `memory-settings` app table never settings.json. When a
26274
+ // session is supplied, changes are also applied to its state and thread.
26174
26275
  registerApiRoute("/web/config/om", {
26175
26276
  method: "GET",
26176
26277
  requiresAuth: false,
26177
26278
  handler: async (c) => {
26178
26279
  const resourceId = c.req.query("resourceId");
26179
26280
  const scope = c.req.query("scope") || void 0;
26180
- if (!resourceId) return c.json({ error: "Missing required query param: resourceId" }, 400);
26181
26281
  const context = await resolveMemorySettingsContext({
26182
26282
  c: loose(c),
26183
26283
  auth,
@@ -26185,9 +26285,10 @@ var ConfigRoutes = class extends Route {
26185
26285
  });
26186
26286
  if ("response" in context) return context.response;
26187
26287
  try {
26288
+ const record = await context.storage.get({ orgId: context.orgId, userId: context.userId });
26289
+ if (!resourceId) return c.json({ config: readStoredOMConfig(record) });
26188
26290
  const session = await controller.getSessionByResource?.(resourceId, scope);
26189
26291
  if (!session) return c.json({ error: `No session for resourceId "${resourceId}"` }, 404);
26190
- const record = await context.storage.get({ orgId: context.orgId, userId: context.userId });
26191
26292
  await hydrateSessionMemorySettings(session, record);
26192
26293
  return c.json({ config: readOMConfig(session) });
26193
26294
  } catch (error) {
@@ -26212,7 +26313,6 @@ var ConfigRoutes = class extends Route {
26212
26313
  const resourceId = typeof body.resourceId === "string" ? body.resourceId : "";
26213
26314
  const scope = typeof body.scope === "string" && body.scope ? body.scope : void 0;
26214
26315
  const modelId = typeof body.modelId === "string" ? body.modelId.trim() : "";
26215
- if (!resourceId) return c.json({ error: "Missing required field: resourceId" }, 400);
26216
26316
  if (!modelId) return c.json({ error: "Missing required field: modelId" }, 400);
26217
26317
  const context = await resolveMemorySettingsContext({
26218
26318
  c: loose(c),
@@ -26221,18 +26321,19 @@ var ConfigRoutes = class extends Route {
26221
26321
  });
26222
26322
  if ("response" in context) return context.response;
26223
26323
  try {
26224
- const session = await controller.getSessionByResource?.(resourceId, scope);
26225
- if (!session) return c.json({ error: `No session for resourceId "${resourceId}"` }, 404);
26226
- const otherRole = role === "observer" ? session.om.reflector : session.om.observer;
26227
- const otherRoleCurrentModelId = otherRole.modelId() ?? null;
26228
- await session.om[role].switchModel({ modelId });
26324
+ const session = resourceId ? await controller.getSessionByResource?.(resourceId, scope) : void 0;
26325
+ if (resourceId && !session) return c.json({ error: `No session for resourceId "${resourceId}"` }, 404);
26326
+ const otherRole = session ? role === "observer" ? session.om.reflector : session.om.observer : void 0;
26327
+ const otherRoleCurrentModelId = otherRole?.modelId() ?? null;
26328
+ await session?.om[role].switchModel({ modelId });
26229
26329
  const otherKey = role === "observer" ? "reflectorModelId" : "observerModelId";
26230
26330
  await persistMemorySettings(
26231
26331
  context,
26232
26332
  { [role === "observer" ? "observerModelId" : "reflectorModelId"]: modelId },
26233
26333
  otherRoleCurrentModelId ? { [otherKey]: otherRoleCurrentModelId } : void 0
26234
26334
  );
26235
- return c.json({ ok: true, config: readOMConfig(session) });
26335
+ const config = session ? readOMConfig(session) : readStoredOMConfig(await context.storage.get({ orgId: context.orgId, userId: context.userId }));
26336
+ return c.json({ ok: true, config });
26236
26337
  } catch (error) {
26237
26338
  return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
26238
26339
  }
@@ -26250,7 +26351,6 @@ var ConfigRoutes = class extends Route {
26250
26351
  }
26251
26352
  const resourceId = typeof body.resourceId === "string" ? body.resourceId : "";
26252
26353
  const scope = typeof body.scope === "string" && body.scope ? body.scope : void 0;
26253
- if (!resourceId) return c.json({ error: "Missing required field: resourceId" }, 400);
26254
26354
  const observation = typeof body.observationThreshold === "number" && body.observationThreshold > 0 ? Math.round(body.observationThreshold) : void 0;
26255
26355
  const reflection = typeof body.reflectionThreshold === "number" && body.reflectionThreshold > 0 ? Math.round(body.reflectionThreshold) : void 0;
26256
26356
  if (observation === void 0 && reflection === void 0) {
@@ -26263,13 +26363,13 @@ var ConfigRoutes = class extends Route {
26263
26363
  });
26264
26364
  if ("response" in context) return context.response;
26265
26365
  try {
26266
- const session = await controller.getSessionByResource?.(resourceId, scope);
26267
- if (!session) return c.json({ error: `No session for resourceId "${resourceId}"` }, 404);
26268
- if (observation !== void 0) {
26366
+ const session = resourceId ? await controller.getSessionByResource?.(resourceId, scope) : void 0;
26367
+ if (resourceId && !session) return c.json({ error: `No session for resourceId "${resourceId}"` }, 404);
26368
+ if (observation !== void 0 && session) {
26269
26369
  await session.state.set({ observationThreshold: observation });
26270
26370
  await session.thread.setSetting({ key: "observationThreshold", value: observation });
26271
26371
  }
26272
- if (reflection !== void 0) {
26372
+ if (reflection !== void 0 && session) {
26273
26373
  await session.state.set({ reflectionThreshold: reflection });
26274
26374
  await session.thread.setSetting({ key: "reflectionThreshold", value: reflection });
26275
26375
  }
@@ -26277,7 +26377,8 @@ var ConfigRoutes = class extends Route {
26277
26377
  ...observation !== void 0 ? { observationThreshold: observation } : {},
26278
26378
  ...reflection !== void 0 ? { reflectionThreshold: reflection } : {}
26279
26379
  });
26280
- return c.json({ ok: true, config: readOMConfig(session) });
26380
+ const config = session ? readOMConfig(session) : readStoredOMConfig(await context.storage.get({ orgId: context.orgId, userId: context.userId }));
26381
+ return c.json({ ok: true, config });
26281
26382
  } catch (error) {
26282
26383
  return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
26283
26384
  }
@@ -26295,7 +26396,6 @@ var ConfigRoutes = class extends Route {
26295
26396
  }
26296
26397
  const resourceId = typeof body.resourceId === "string" ? body.resourceId : "";
26297
26398
  const scope = typeof body.scope === "string" && body.scope ? body.scope : void 0;
26298
- if (!resourceId) return c.json({ error: "Missing required field: resourceId" }, 400);
26299
26399
  const raw = body.value;
26300
26400
  const value = raw === "auto" || raw === true || raw === false ? raw : "auto";
26301
26401
  if (raw !== "auto" && raw !== true && raw !== false) {
@@ -26308,12 +26408,15 @@ var ConfigRoutes = class extends Route {
26308
26408
  });
26309
26409
  if ("response" in context) return context.response;
26310
26410
  try {
26311
- const session = await controller.getSessionByResource?.(resourceId, scope);
26312
- if (!session) return c.json({ error: `No session for resourceId "${resourceId}"` }, 404);
26313
- await session.state.set({ observeAttachments: value });
26314
- await session.thread.setSetting({ key: "observeAttachments", value });
26411
+ const session = resourceId ? await controller.getSessionByResource?.(resourceId, scope) : void 0;
26412
+ if (resourceId && !session) return c.json({ error: `No session for resourceId "${resourceId}"` }, 404);
26413
+ if (session) {
26414
+ await session.state.set({ observeAttachments: value });
26415
+ await session.thread.setSetting({ key: "observeAttachments", value });
26416
+ }
26315
26417
  await persistMemorySettings(context, { observeAttachments: value });
26316
- return c.json({ ok: true, config: readOMConfig(session) });
26418
+ const config = session ? readOMConfig(session) : readStoredOMConfig(await context.storage.get({ orgId: context.orgId, userId: context.userId }));
26419
+ return c.json({ ok: true, config });
26317
26420
  } catch (error) {
26318
26421
  return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
26319
26422
  }
@@ -28037,7 +28140,7 @@ var WorkItemRoutes = class extends Route {
28037
28140
  if (!project) {
28038
28141
  return { response: c.json({ error: "Project not found" }, 404) };
28039
28142
  }
28040
- return { ...tenant, factoryProjectId: projectId };
28143
+ return { ...tenant, factoryProjectId: projectId, defaultModelId: project.defaultModelId };
28041
28144
  }
28042
28145
  /**
28043
28146
  * Emit the audit events a successful work-item PATCH implies: always
@@ -28322,6 +28425,7 @@ var WorkItemRoutes = class extends Route {
28322
28425
  const input = parseStartBody(await readJson(loose5(c)), resolved, resolved.factoryProjectId);
28323
28426
  if (!input) return c.json({ error: "invalid_factory_start" }, 400);
28324
28427
  input.requestContext = loose5(c).get("requestContext");
28428
+ input.defaultModelId = resolved.defaultModelId ?? void 0;
28325
28429
  if (!input.workItem.id && ((input.workItem.input.stages ?? ["intake"]).length !== 1 || (input.workItem.input.stages ?? ["intake"])[0] !== "intake")) {
28326
28430
  return c.json(
28327
28431
  { error: "governed_transition_required", message: "Create the work item in Intake before starting it." },
@@ -28616,7 +28720,8 @@ function assembleFactoryApiRoutes(deps) {
28616
28720
  deps.controller,
28617
28721
  deps.domains.workItems,
28618
28722
  transitionService,
28619
- githubIntegration?.sourceControlStorage
28723
+ githubIntegration?.sourceControlStorage,
28724
+ deps.domains.memorySettings
28620
28725
  ) : void 0;
28621
28726
  if (transitionService && startCoordinator) {
28622
28727
  deps.onFactoryRuntime?.({