@autonoma-ai/planner 0.1.10 → 0.1.11

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/dist/index.js CHANGED
@@ -3564,6 +3564,9 @@ async function sendRequest(config, payload) {
3564
3564
  }
3565
3565
  return { ok: res.ok, status: res.status, body };
3566
3566
  }
3567
+ async function discover(config) {
3568
+ return sendRequest(config, { action: "discover" });
3569
+ }
3567
3570
  async function up(config, create, testRunId) {
3568
3571
  return sendRequest(config, {
3569
3572
  action: "up",
@@ -3585,6 +3588,130 @@ var init_http_client = __esm({
3585
3588
  }
3586
3589
  });
3587
3590
 
3591
+ // src/agents/04-recipe-builder/discover-schema.ts
3592
+ function parseDiscoverBody(body) {
3593
+ if (!body || typeof body !== "object") return null;
3594
+ const schema = body.schema;
3595
+ if (!schema || typeof schema !== "object") return null;
3596
+ const rawModels = schema.models;
3597
+ if (!Array.isArray(rawModels)) return null;
3598
+ const models = /* @__PURE__ */ new Map();
3599
+ for (const m of rawModels) {
3600
+ if (!m || typeof m !== "object") continue;
3601
+ const mm = m;
3602
+ if (typeof mm.name !== "string") continue;
3603
+ const rawFields = Array.isArray(mm.fields) ? mm.fields : [];
3604
+ const fields = [];
3605
+ for (const f of rawFields) {
3606
+ if (!f || typeof f !== "object") continue;
3607
+ const ff = f;
3608
+ if (typeof ff.name !== "string") continue;
3609
+ fields.push({
3610
+ name: ff.name,
3611
+ type: typeof ff.type === "string" ? ff.type : "string",
3612
+ isRequired: ff.isRequired === true,
3613
+ isId: ff.isId === true,
3614
+ hasDefault: ff.hasDefault === true
3615
+ });
3616
+ }
3617
+ models.set(mm.name, { name: mm.name, tableName: typeof mm.tableName === "string" ? mm.tableName : mm.name, fields });
3618
+ }
3619
+ const scopeField = typeof schema.scopeField === "string" && schema.scopeField.length > 0 ? schema.scopeField : null;
3620
+ return { models, scopeField };
3621
+ }
3622
+ async function fetchDiscoverSchema(config) {
3623
+ try {
3624
+ const res = await discover(config);
3625
+ if (!res.ok) return null;
3626
+ return parseDiscoverBody(res.body);
3627
+ } catch {
3628
+ return null;
3629
+ }
3630
+ }
3631
+ function isMandatory(field) {
3632
+ return field.isRequired && !field.isId && !field.hasDefault;
3633
+ }
3634
+ function renderModelSchema(schema, modelName) {
3635
+ const m = schema.models.get(modelName);
3636
+ if (!m) return null;
3637
+ const lines = m.fields.filter((f) => !f.isId).map((f) => {
3638
+ const req = isMandatory(f) ? "REQUIRED" : f.hasDefault ? "optional (has default)" : "optional";
3639
+ const scopeNote = schema.scopeField && f.name === schema.scopeField ? ' \u2190 scope/tenant field \u2014 the SDK does NOT auto-fill it; set it explicitly (use a { "_ref": "..." } to the scope entity)' : "";
3640
+ return ` - ${f.name}: ${f.type} (${req})${scopeNote}`;
3641
+ });
3642
+ return `Live schema for "${modelName}" (from the SDK /discover endpoint \u2014 this is the SOURCE OF TRUTH; it overrides the entity audit when they disagree):
3643
+ ${lines.join("\n")}
3644
+
3645
+ Every REQUIRED field above must be present on every record. Do not send fields that are not listed.`;
3646
+ }
3647
+ function validateRecipeAgainstSchema(recipe, schema) {
3648
+ const problems = [];
3649
+ const declaredAliases = /* @__PURE__ */ new Set();
3650
+ for (const records of Object.values(recipe)) {
3651
+ for (const rec of records) {
3652
+ if (typeof rec._alias === "string") declaredAliases.add(rec._alias);
3653
+ }
3654
+ }
3655
+ for (const [modelName, records] of Object.entries(recipe)) {
3656
+ const model = schema.models.get(modelName);
3657
+ records.forEach((record, recordIndex) => {
3658
+ if (model) {
3659
+ for (const field of model.fields) {
3660
+ if (!isMandatory(field)) continue;
3661
+ const value = record[field.name];
3662
+ if (value === void 0 || value === null) {
3663
+ problems.push({
3664
+ model: modelName,
3665
+ recordIndex,
3666
+ message: `missing required field "${field.name}" (${field.type})`
3667
+ });
3668
+ }
3669
+ }
3670
+ }
3671
+ const refs = /* @__PURE__ */ new Set();
3672
+ collectRefs2(record, refs);
3673
+ for (const alias of refs) {
3674
+ if (!declaredAliases.has(alias)) {
3675
+ problems.push({
3676
+ model: modelName,
3677
+ recordIndex,
3678
+ message: `_ref points at "${alias}", which no record in this payload declares with an _alias`
3679
+ });
3680
+ }
3681
+ }
3682
+ });
3683
+ }
3684
+ return problems;
3685
+ }
3686
+ function collectRefs2(value, out) {
3687
+ if (Array.isArray(value)) {
3688
+ for (const v of value) collectRefs2(v, out);
3689
+ } else if (value !== null && typeof value === "object") {
3690
+ const obj = value;
3691
+ if (typeof obj._ref === "string") {
3692
+ out.add(obj._ref);
3693
+ return;
3694
+ }
3695
+ for (const v of Object.values(obj)) collectRefs2(v, out);
3696
+ }
3697
+ }
3698
+ function formatValidationProblems(problems) {
3699
+ const byModel = /* @__PURE__ */ new Map();
3700
+ for (const p10 of problems) {
3701
+ const key = `${p10.model}[${p10.recordIndex}]`;
3702
+ if (!byModel.has(key)) byModel.set(key, []);
3703
+ byModel.get(key).push(p10.message);
3704
+ }
3705
+ return [...byModel.entries()].map(([key, msgs]) => `${key}: ${msgs.join("; ")}`).join("\n");
3706
+ }
3707
+ var init_discover_schema = __esm({
3708
+ "src/agents/04-recipe-builder/discover-schema.ts"() {
3709
+ "use strict";
3710
+ init_esm_shims();
3711
+ init_http_client();
3712
+ }
3713
+ });
3714
+
3588
3715
  // src/agents/04-recipe-builder/phases/failure-classifier.ts
3589
3716
  import { Output as Output2, generateText as generateText2 } from "ai";
3590
3717
  import { z as z16 } from "zod";
@@ -3619,6 +3746,8 @@ ${args.validRefAliases?.trim() || "(none \u2014 this is a root entity with no pa
3619
3746
  What the entity audit recorded about how "${args.entityName}" is created:
3620
3747
  ${args.entityAudit?.trim() || "(not available)"}
3621
3748
 
3749
+ ${args.liveSchema?.trim() || "(no live schema available \u2014 the /discover endpoint was unreachable)"}
3750
+
3622
3751
  Test data sent:
3623
3752
  ${JSON.stringify(args.recipe, null, 2)}
3624
3753
 
@@ -3680,7 +3809,7 @@ function summarizeEntityAudit(model) {
3680
3809
  if (model.side_effects?.length) parts.push(`side effects: ${model.side_effects.join(", ")}`);
3681
3810
  return parts.join("; ");
3682
3811
  }
3683
- async function proposeRecipeData(entityName, entityIndex, totalEntities, model, outputDir, _projectRoot, completedEntities) {
3812
+ async function proposeRecipeData(entityName, entityIndex, totalEntities, model, outputDir, _projectRoot, completedEntities, schemaSpec) {
3684
3813
  let result;
3685
3814
  const { logger, onStepFinish } = buildDefaultStepLogger(`propose:${entityName}`, 20);
3686
3815
  const finishTool = tool14({
@@ -3698,10 +3827,14 @@ async function proposeRecipeData(entityName, entityIndex, totalEntities, model,
3698
3827
 
3699
3828
  Read scenarios.md and entity-audit.md from the output directory. Design records that match the scenario data.
3700
3829
 
3830
+ ${schemaSpec ? `${schemaSpec}
3831
+ ` : ""}
3701
3832
  ${completedAliases ? `Already completed entities (use _ref to reference their aliases):
3702
3833
  ${completedAliases}
3703
3834
  ` : "This is a root entity \u2014 no parent references needed."}
3704
3835
 
3836
+ Produce records for "${entityName}" ONLY \u2014 one object per record. Do not include records for other models; the tool assembles parent entities automatically.
3837
+
3705
3838
  Call finish with the JSON array of records.`;
3706
3839
  const readOutputTool = buildReadFileTool(outputDir);
3707
3840
  await runAgent(
@@ -3723,7 +3856,7 @@ Call finish with the JSON array of records.`;
3723
3856
  logger.summary();
3724
3857
  return result ?? [];
3725
3858
  }
3726
- async function reviseRecipeData(entityName, entityIndex, totalEntities, current, feedback, model, outputDir, completedEntities) {
3859
+ async function reviseRecipeData(entityName, entityIndex, totalEntities, current, feedback, model, outputDir, completedEntities, schemaSpec) {
3727
3860
  let revised;
3728
3861
  const finishTool = tool14({
3729
3862
  description: "Submit the fixed recipe data.",
@@ -3747,11 +3880,12 @@ ${completedAliases}
3747
3880
  systemPrompt: `You are fixing recipe data based on user feedback (or a validation failure). Read the error, the current data, and the user's feedback. Read scenarios.md and entity-audit.md if needed. Fix the data and call finish.
3748
3881
 
3749
3882
  Rules:
3883
+ - Return records for "${entityName}" ONLY \u2014 a flat JSON array, one object per record. NEVER group records by model name or nest other models (Client/User/etc.) inside this array. The tool assembles parent entities into the request automatically; your job is only this one entity's rows.
3750
3884
  - _alias fields must be unique identifiers (e.g., "card_1", "transaction_1")
3751
3885
  - _ref fields must reference an alias that ALREADY EXISTS on a parent entity \u2014 see the list of valid targets below. Never invent a _ref to an alias that isn't listed.
3752
3886
  - If the error says "references unknown alias(es): X", a _ref points at "X" but nothing being created declares it. Correct that _ref to one of the valid targets listed below (it's usually a typo, e.g. "users_1" vs "user_1"), or drop the reference if the field is optional. Do NOT leave a _ref pointing at an alias that isn't in the valid targets list.
3753
3887
  - Read scenarios.md to verify you're using correct alias names from parent entities
3754
- - Field names must match the entity's schema from entity-audit.md`,
3888
+ - Field names and required fields must match the live schema below when present (it is the source of truth), otherwise the entity's schema from entity-audit.md`,
3755
3889
  model,
3756
3890
  maxSteps: 15,
3757
3891
  tools: (_heartbeat) => ({
@@ -3769,6 +3903,8 @@ What's wrong / what to change:
3769
3903
  ${feedback}
3770
3904
 
3771
3905
  ${aliasBlock}
3906
+ ${schemaSpec ? `${schemaSpec}
3907
+ ` : ""}
3772
3908
  Read scenarios.md and entity-audit.md to understand the correct aliases and schema. Apply the change and call finish.`,
3773
3909
  () => revised
3774
3910
  );
@@ -3843,7 +3979,7 @@ Read the creation file from the project to understand the existing service/funct
3843
3979
  logger.summary();
3844
3980
  return result ?? "No instructions generated. Check the entity audit for creation_file and creation_function.";
3845
3981
  }
3846
- async function reviewRecipeData(entityName, entityIndex, totalEntities, proposed, model, outputDir, completedEntities) {
3982
+ async function reviewRecipeData(entityName, entityIndex, totalEntities, proposed, model, outputDir, completedEntities, schemaSpec) {
3847
3983
  p5.log.info(
3848
3984
  `Legend for recipe fields:
3849
3985
  _alias \u2014 Internal ID used to reference this record from other entities (e.g., { "_ref": "org_1" })
@@ -3898,7 +4034,8 @@ async function reviewRecipeData(entityName, entityIndex, totalEntities, proposed
3898
4034
  feedback.trim(),
3899
4035
  model,
3900
4036
  outputDir,
3901
- completedEntities
4037
+ completedEntities,
4038
+ schemaSpec
3902
4039
  );
3903
4040
  }
3904
4041
  }
@@ -3923,7 +4060,8 @@ async function promptOnFailure(entityName, errorBody, ctx, phase, httpStatus) {
3923
4060
  error: errorBody,
3924
4061
  recipe: ctx.recipe,
3925
4062
  validRefAliases: ctx.validRefAliases,
3926
- entityAudit: ctx.entityAudit
4063
+ entityAudit: ctx.entityAudit,
4064
+ liveSchema: ctx.liveSchema
3927
4065
  });
3928
4066
  if (ctx.budget.attempts < MAX_AUTOFIX_ATTEMPTS) {
3929
4067
  ctx.budget.attempts++;
@@ -3961,10 +4099,22 @@ async function promptOnFailure(entityName, errorBody, ctx, phase, httpStatus) {
3961
4099
  if (p5.isCancel(fb)) throw new Error("Entity loop cancelled");
3962
4100
  return { feedback: `${fb.trim()}${errorContext}` };
3963
4101
  }
3964
- async function testUpDown(entityName, entityIndex, totalEntities, sdkConfig, recipe, grounding) {
4102
+ async function testUpDown(entityName, entityIndex, totalEntities, sdkConfig, recipe, grounding, discoverSchema) {
3965
4103
  p5.log.info(`Let's verify this factory works. We'll send a test request to create ${entityName}, then check the database.`);
3966
4104
  const failureCtx = { ...grounding, recipe };
3967
4105
  while (true) {
4106
+ if (discoverSchema) {
4107
+ const problems = validateRecipeAgainstSchema(recipe, discoverSchema);
4108
+ if (problems.length > 0) {
4109
+ const errorBody = `Recipe failed local schema validation against /discover (not sent to the server):
4110
+ ${formatValidationProblems(problems)}`;
4111
+ p5.log.error(errorBody);
4112
+ const action = await promptOnFailure(entityName, errorBody, failureCtx, "create");
4113
+ if (action === "skip") return "skip";
4114
+ if (action === "retry") continue;
4115
+ return action;
4116
+ }
4117
+ }
3968
4118
  const testRunId = `test-${Date.now()}`;
3969
4119
  p5.log.step(`[${entityIndex + 1}/${totalEntities}] Sending UP request...`);
3970
4120
  let upResult;
@@ -4023,6 +4173,15 @@ ${formatException(err)}`);
4023
4173
  async function runEntityLoop(state, models, model, projectRoot, outputDir, nonInteractive) {
4024
4174
  const total = state.entityOrder.length;
4025
4175
  const modelMap = new Map(models.map((m) => [m.name, m]));
4176
+ async function loadLiveSchema(name) {
4177
+ if (!state.sdkEndpointUrl || !state.sharedSecret) return { schema: null };
4178
+ const schema = await fetchDiscoverSchema({
4179
+ endpointUrl: state.sdkEndpointUrl,
4180
+ sharedSecret: state.sharedSecret
4181
+ });
4182
+ if (!schema) return { schema: null };
4183
+ return { schema, spec: renderModelSchema(schema, name) ?? void 0 };
4184
+ }
4026
4185
  p5.log.info(
4027
4186
  `We're going to set up your test data factories one entity at a time. Each factory teaches the Autonoma SDK how to create and tear down a specific type of record in YOUR database, using YOUR existing service functions.
4028
4187
 
@@ -4052,6 +4211,7 @@ async function runEntityLoop(state, models, model, projectRoot, outputDir, nonIn
4052
4211
  const depInfo = isRoot ? "This is a root entity \u2014 no dependencies." : `This depends on: ${auditModel.created_by.map((d) => d.owner).join(", ")}`;
4053
4212
  p5.log.step(`[${i + 1}/${total}] ${entityName}`);
4054
4213
  p5.log.info(depInfo);
4214
+ const { spec: recipeSchemaSpec } = await loadLiveSchema(entityName);
4055
4215
  let recipeData = existing?.recipeData;
4056
4216
  if (!recipeData || existing?.status === "pending") {
4057
4217
  recipeData = await proposeRecipeData(
@@ -4061,11 +4221,12 @@ async function runEntityLoop(state, models, model, projectRoot, outputDir, nonIn
4061
4221
  model,
4062
4222
  outputDir,
4063
4223
  projectRoot,
4064
- state.entities
4224
+ state.entities,
4225
+ recipeSchemaSpec
4065
4226
  );
4066
4227
  }
4067
4228
  if (!nonInteractive) {
4068
- recipeData = await reviewRecipeData(entityName, i, total, recipeData, model, outputDir, state.entities);
4229
+ recipeData = await reviewRecipeData(entityName, i, total, recipeData, model, outputDir, state.entities, recipeSchemaSpec);
4069
4230
  }
4070
4231
  state.entities[entityName] = {
4071
4232
  entityName,
@@ -4160,17 +4321,19 @@ Saved to: ${join23(outputDir, "autonoma-config.json")}`,
4160
4321
  endpointUrl: state.sdkEndpointUrl,
4161
4322
  sharedSecret: state.sharedSecret
4162
4323
  };
4324
+ const { schema: discoverSchema, spec: liveSchemaSpec } = await loadLiveSchema(entityName);
4163
4325
  const autofixBudget = { attempts: 0 };
4164
4326
  const grounding = {
4165
4327
  model,
4166
4328
  budget: autofixBudget,
4167
4329
  validRefAliases: summarizeCompletedAliases(state.entities, entityName),
4168
- entityAudit: summarizeEntityAudit(models.find((m) => m.name === entityName))
4330
+ entityAudit: summarizeEntityAudit(models.find((m) => m.name === entityName)),
4331
+ liveSchema: liveSchemaSpec
4169
4332
  };
4170
4333
  let testDone = false;
4171
4334
  while (!testDone) {
4172
4335
  const singleRecipe = buildSingleEntityRecipe(entityName, models, state.entityOrder, state.entities);
4173
- const testResult = await testUpDown(entityName, i, total, sdkConfig, singleRecipe, grounding);
4336
+ const testResult = await testUpDown(entityName, i, total, sdkConfig, singleRecipe, grounding, discoverSchema ?? void 0);
4174
4337
  if (testResult === "success") {
4175
4338
  state.entities[entityName].status = "tested-down";
4176
4339
  p5.log.success(`[${i + 1}/${total}] ${entityName} \u2014 factory verified`);
@@ -4190,7 +4353,8 @@ Saved to: ${join23(outputDir, "autonoma-config.json")}`,
4190
4353
  testResult.feedback,
4191
4354
  model,
4192
4355
  outputDir,
4193
- state.entities
4356
+ state.entities,
4357
+ liveSchemaSpec
4194
4358
  );
4195
4359
  state.entities[entityName].recipeData = revised;
4196
4360
  await saveRecipeState(outputDir, state);
@@ -4219,6 +4383,7 @@ var init_entity_loop = __esm({
4219
4383
  init_notify();
4220
4384
  init_recipe();
4221
4385
  init_http_client();
4386
+ init_discover_schema();
4222
4387
  init_failure_classifier();
4223
4388
  PROPOSAL_PROMPT = `You are a recipe data designer. Given an entity from the entity audit and the scenario data, produce a JSON array of records for this entity.
4224
4389
 
@@ -4255,7 +4420,7 @@ When done, call finish with the instructions text.`;
4255
4420
  import * as p6 from "@clack/prompts";
4256
4421
  import { tool as tool15 } from "ai";
4257
4422
  import { z as z18 } from "zod";
4258
- async function reviseFullRecipe(current, feedback, model, outputDir, entityOrder) {
4423
+ async function reviseFullRecipe(current, feedback, model, outputDir, entityOrder, schemaSpec) {
4259
4424
  let revised;
4260
4425
  const finishTool = tool15({
4261
4426
  description: "Submit the revised full recipe: an object mapping each entity name to its array of records.",
@@ -4281,9 +4446,11 @@ Rules:
4281
4446
  - Apply the user's feedback across whatever entities it touches.
4282
4447
  - Keep _ref values pointing to aliases that actually exist in the recipe. Never invent a _ref to a missing alias.
4283
4448
  - Entities are created in this order (parents first): ${entityOrder.join(" \u2192 ")}. A record may only _ref an alias declared by an entity earlier in that order.
4284
- - Field names/types must match the schema in entity-audit.md.
4449
+ - Field names, types, and required fields must match the live schema below when present (it is the source of truth), otherwise the schema in entity-audit.md.
4285
4450
  - Read scenarios.md and entity-audit.md from the output directory as needed.
4286
-
4451
+ ${schemaSpec ? `
4452
+ ${schemaSpec}
4453
+ ` : ""}
4287
4454
  Return the COMPLETE revised recipe (all entities, not just the changed ones) via finish.`,
4288
4455
  model,
4289
4456
  maxSteps: 20,
@@ -4345,8 +4512,19 @@ async function runFullValidation(state, _models, outputDir, model) {
4345
4512
  endpointUrl: state.sdkEndpointUrl,
4346
4513
  sharedSecret: state.sharedSecret ?? ""
4347
4514
  };
4515
+ const discoverSchema = await fetchDiscoverSchema(sdkConfig);
4516
+ const fullSchemaSpec = discoverSchema ? state.entityOrder.map((name) => renderModelSchema(discoverSchema, name)).filter(Boolean).join("\n\n") || void 0 : void 0;
4348
4517
  let fullRecipe = buildFullRecipe(state.entityOrder, state.entities);
4349
4518
  while (true) {
4519
+ if (discoverSchema) {
4520
+ const problems = validateRecipeAgainstSchema(fullRecipe, discoverSchema);
4521
+ if (problems.length > 0) {
4522
+ p6.log.warn(
4523
+ `Heads up \u2014 the recipe has likely schema problems (from /discover); the full UP may fail:
4524
+ ${formatValidationProblems(problems)}`
4525
+ );
4526
+ }
4527
+ }
4350
4528
  const testRunId = `full-${Date.now()}`;
4351
4529
  p6.log.step(`[Full validation] Creating all ${total} entities...`);
4352
4530
  let upResult;
@@ -4420,7 +4598,7 @@ ${formatException(err)}`);
4420
4598
  return false;
4421
4599
  }
4422
4600
  p6.log.info("Revising the full recipe based on your feedback...");
4423
- const revised = await reviseFullRecipe(fullRecipe, feedback.trim(), model, outputDir, state.entityOrder);
4601
+ const revised = await reviseFullRecipe(fullRecipe, feedback.trim(), model, outputDir, state.entityOrder, fullSchemaSpec);
4424
4602
  if (!revised) {
4425
4603
  p6.log.warn("Couldn't revise automatically. Edit recipe.json manually and re-run with --resume.");
4426
4604
  return false;
@@ -4447,6 +4625,7 @@ var init_full_validation = __esm({
4447
4625
  init_state();
4448
4626
  init_recipe();
4449
4627
  init_http_client();
4628
+ init_discover_schema();
4450
4629
  }
4451
4630
  });
4452
4631