@mindstudio-ai/agent 0.1.99 → 0.1.100

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
@@ -3525,6 +3525,178 @@ var stepMetadata = {
3525
3525
  }
3526
3526
  };
3527
3527
 
3528
+ // src/task/schema.ts
3529
+ function describe(value) {
3530
+ if (value === void 0) return "undefined";
3531
+ const json = JSON.stringify(value);
3532
+ if (json === void 0) return String(value);
3533
+ return json.length > 60 ? `${json.slice(0, 57)}...` : json;
3534
+ }
3535
+ function matchesTypeName(value, name) {
3536
+ switch (name) {
3537
+ case "null":
3538
+ return value === null;
3539
+ case "string":
3540
+ return typeof value === "string";
3541
+ case "number":
3542
+ return typeof value === "number";
3543
+ case "integer":
3544
+ return typeof value === "number" && Number.isInteger(value);
3545
+ case "boolean":
3546
+ return typeof value === "boolean";
3547
+ case "array":
3548
+ return Array.isArray(value);
3549
+ case "object":
3550
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3551
+ }
3552
+ }
3553
+ function validateAgainstSchema(value, schema) {
3554
+ const errors = [];
3555
+ walk(value, schema, "$", errors);
3556
+ return errors;
3557
+ }
3558
+ function walk(value, schema, path, errors) {
3559
+ if (schema.const !== void 0) {
3560
+ if (value !== schema.const) {
3561
+ errors.push({
3562
+ path,
3563
+ message: `expected the constant ${describe(schema.const)}, got ${describe(value)}`
3564
+ });
3565
+ }
3566
+ return;
3567
+ }
3568
+ if (schema.enum !== void 0) {
3569
+ if (!schema.enum.includes(value)) {
3570
+ errors.push({
3571
+ path,
3572
+ message: `expected one of ${schema.enum.map(describe).join(" | ")}, got ${describe(value)}`
3573
+ });
3574
+ }
3575
+ return;
3576
+ }
3577
+ if (schema.type !== void 0) {
3578
+ const names = Array.isArray(schema.type) ? schema.type : [schema.type];
3579
+ if (!names.some((name) => matchesTypeName(value, name))) {
3580
+ errors.push({
3581
+ path,
3582
+ message: `expected type ${names.join(" | ")}, got ${describe(value)}`
3583
+ });
3584
+ return;
3585
+ }
3586
+ }
3587
+ if (matchesTypeName(value, "object")) {
3588
+ const record = value;
3589
+ if (schema.required) {
3590
+ for (const key of schema.required) {
3591
+ if (!(key in record) || record[key] === void 0) {
3592
+ errors.push({
3593
+ path,
3594
+ message: `missing required property "${key}"`
3595
+ });
3596
+ }
3597
+ }
3598
+ }
3599
+ if (schema.properties) {
3600
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
3601
+ if (key in record && record[key] !== void 0) {
3602
+ walk(record[key], propSchema, `${path}.${key}`, errors);
3603
+ }
3604
+ }
3605
+ if (schema.additionalProperties === false) {
3606
+ for (const key of Object.keys(record)) {
3607
+ if (!(key in schema.properties)) {
3608
+ errors.push({
3609
+ path,
3610
+ message: `unexpected property "${key}" (additionalProperties is false)`
3611
+ });
3612
+ }
3613
+ }
3614
+ }
3615
+ }
3616
+ }
3617
+ if (Array.isArray(value) && schema.items) {
3618
+ value.forEach((item, index) => {
3619
+ walk(item, schema.items, `${path}[${index}]`, errors);
3620
+ });
3621
+ }
3622
+ }
3623
+ function formatValidationErrors(errors) {
3624
+ return errors.map((e) => `- at ${e.path}: ${e.message}`).join("\n");
3625
+ }
3626
+ var SUPPORTED_KEYWORDS = /* @__PURE__ */ new Set([
3627
+ "type",
3628
+ "properties",
3629
+ "required",
3630
+ "additionalProperties",
3631
+ "items",
3632
+ "enum",
3633
+ "const",
3634
+ // Harmless annotations — ignored by the validator, allowed to pass through.
3635
+ "description",
3636
+ "title",
3637
+ "examples",
3638
+ "default"
3639
+ ]);
3640
+ function assertSupportedSchema(schema) {
3641
+ assertSupportedNode(schema, "$");
3642
+ }
3643
+ function assertSupportedNode(schema, path) {
3644
+ for (const key of Object.keys(schema)) {
3645
+ if (SUPPORTED_KEYWORDS.has(key)) continue;
3646
+ const hint = key === "nullable" ? ' Use a type array instead: type: ["string", "null"].' : key === "oneOf" || key === "anyOf" || key === "allOf" ? " Model alternatives with an enum, a type array, or a discriminating property instead." : "";
3647
+ throw new MindStudioError(
3648
+ `[task] Unsupported JSON Schema keyword "${key}" at ${path} in outputSchema. Supported: type, properties, required, additionalProperties, items, enum, const.${hint}`,
3649
+ "task_output_schema_unsupported",
3650
+ 400
3651
+ );
3652
+ }
3653
+ if (schema.properties) {
3654
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
3655
+ assertSupportedNode(propSchema, `${path}.${key}`);
3656
+ }
3657
+ }
3658
+ if (schema.items) {
3659
+ assertSupportedNode(schema.items, `${path}[]`);
3660
+ }
3661
+ }
3662
+ function buildExampleFromSchema(schema) {
3663
+ if (schema.const !== void 0) return schema.const;
3664
+ if (schema.enum !== void 0 && schema.enum.length > 0)
3665
+ return schema.enum[0];
3666
+ const names = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
3667
+ const name = names.find((n) => n !== "null") ?? names[0];
3668
+ switch (name) {
3669
+ case "object": {
3670
+ const example = {};
3671
+ for (const [key, propSchema] of Object.entries(
3672
+ schema.properties ?? {}
3673
+ )) {
3674
+ example[key] = buildExampleFromSchema(propSchema);
3675
+ }
3676
+ return example;
3677
+ }
3678
+ case "array":
3679
+ return schema.items ? [buildExampleFromSchema(schema.items)] : [];
3680
+ case "string":
3681
+ return "...";
3682
+ case "number":
3683
+ case "integer":
3684
+ return 0;
3685
+ case "boolean":
3686
+ return true;
3687
+ case "null":
3688
+ return null;
3689
+ default:
3690
+ return null;
3691
+ }
3692
+ }
3693
+ function stripCodeFences(text) {
3694
+ const match = /^```[a-zA-Z]*[ \t]*\r?\n([\s\S]*?)\r?\n?```$/.exec(
3695
+ text.trim()
3696
+ );
3697
+ return match ? match[1] : text;
3698
+ }
3699
+
3528
3700
  // src/task/index.ts
3529
3701
  function resolveStepType(name) {
3530
3702
  const meta = stepMetadata[name];
@@ -3550,7 +3722,11 @@ function buildTaskRequestBody(options) {
3550
3722
  prompt: options.prompt,
3551
3723
  input: options.input,
3552
3724
  tools: mapTools(options.tools),
3553
- structuredOutputExample: typeof options.structuredOutputExample === "string" ? options.structuredOutputExample : JSON.stringify(options.structuredOutputExample),
3725
+ // The legacy whole-task route requires structuredOutputExample and
3726
+ // composes its own prompt server-side, so in schema mode we synthesize a
3727
+ // skeleton example from the schema. Validation still happens client-side
3728
+ // after the result comes back (see _runTaskInner).
3729
+ structuredOutputExample: options.outputSchema ? JSON.stringify(buildExampleFromSchema(options.outputSchema)) : typeof options.structuredOutputExample === "string" ? options.structuredOutputExample : JSON.stringify(options.structuredOutputExample),
3554
3730
  model: options.model,
3555
3731
  ...options.maxTurns != null && { maxTurns: options.maxTurns },
3556
3732
  ...options.appId != null && { appId: options.appId },
@@ -3751,6 +3927,7 @@ async function runTaskStream(httpConfig, body, onEvent) {
3751
3927
  // src/task/local.ts
3752
3928
  var DEFAULT_MAX_TURNS = 20;
3753
3929
  var MAX_TURNS_LIMIT = 100;
3930
+ var MAX_SCHEMA_REPAIR_ATTEMPTS = 3;
3754
3931
  var MAX_TOOL_OUTPUT_CHARS = 5e4;
3755
3932
  var MAX_TURN_ATTEMPTS = 5;
3756
3933
  var INITIAL_BACKOFF_MS = 1e3;
@@ -3946,12 +4123,23 @@ async function runTurnWithRetry(httpConfig, body) {
3946
4123
  async function runTaskLocal(deps, options) {
3947
4124
  const { httpConfig } = deps;
3948
4125
  const onEvent = options.onEvent;
3949
- const structuredOutputExample = typeof options.structuredOutputExample === "string" ? options.structuredOutputExample : JSON.stringify(options.structuredOutputExample);
3950
- const system = `${options.prompt}
4126
+ const outputSchema = options.outputSchema;
4127
+ if (outputSchema) assertSupportedSchema(outputSchema);
4128
+ let system;
4129
+ if (outputSchema) {
4130
+ system = `${options.prompt}
4131
+
4132
+ When you have completed the task, respond with your final output as a single JSON object that conforms to this JSON Schema. Respond with the JSON object itself \u2014 NOT the schema, no prose, no code fences:
4133
+ ${JSON.stringify(outputSchema)}
4134
+ <!-- cache_breakpoint -->`;
4135
+ } else {
4136
+ const structuredOutputExample = typeof options.structuredOutputExample === "string" ? options.structuredOutputExample : JSON.stringify(options.structuredOutputExample);
4137
+ system = `${options.prompt}
3951
4138
 
3952
4139
  When you have completed the task, respond with your final output as JSON matching this example:
3953
4140
  ${structuredOutputExample}
3954
4141
  <!-- cache_breakpoint -->`;
4142
+ }
3955
4143
  const wireTools = mapTools(options.tools);
3956
4144
  const toolKinds = /* @__PURE__ */ new Map();
3957
4145
  const toolDefaults = /* @__PURE__ */ new Map();
@@ -3972,6 +4160,7 @@ ${structuredOutputExample}
3972
4160
  { role: "user", content: JSON.stringify(options.input) }
3973
4161
  ];
3974
4162
  let loopCount = 0;
4163
+ let schemaRepairCount = 0;
3975
4164
  const toolCallLog = [];
3976
4165
  const totalUsage = {
3977
4166
  inputTokens: 0,
@@ -4003,11 +4192,38 @@ ${structuredOutputExample}
4003
4192
  logTaskResult(result);
4004
4193
  return result;
4005
4194
  };
4195
+ const schemaMismatch = (outputRaw, errors) => {
4196
+ onEvent?.({
4197
+ type: "error",
4198
+ error: "Output did not conform to outputSchema.",
4199
+ errors
4200
+ });
4201
+ return new MindStudioError(
4202
+ "[task] Output did not conform to outputSchema after all repair attempts.",
4203
+ "task_output_schema_mismatch",
4204
+ 422,
4205
+ {
4206
+ outputRaw,
4207
+ errors,
4208
+ turns: loopCount,
4209
+ usage: totalUsage,
4210
+ toolCalls: toolCallLog
4211
+ }
4212
+ );
4213
+ };
4006
4214
  const turnFailure = (err) => {
4007
4215
  if (err instanceof TurnError && err.phase === "model") {
4008
4216
  if (isDevMode()) {
4009
4217
  console.error(`[task] Model call failed: ${err.message}`);
4010
4218
  }
4219
+ if (outputSchema) {
4220
+ throw new MindStudioError(
4221
+ `[task] ${err.message}`,
4222
+ "task_execution_error",
4223
+ 500,
4224
+ { turns: loopCount, usage: totalUsage, toolCalls: toolCallLog }
4225
+ );
4226
+ }
4011
4227
  return finish(buildResult(null, "", false, loopCount));
4012
4228
  }
4013
4229
  if (err instanceof TurnError) {
@@ -4066,11 +4282,11 @@ ${structuredOutputExample}
4066
4282
  return { toolCall, ...result };
4067
4283
  })
4068
4284
  );
4069
- for (const { toolCall, output: output2, billingCost, isError } of results) {
4285
+ for (const { toolCall, output: output3, billingCost, isError } of results) {
4070
4286
  if (billingCost) {
4071
4287
  totalUsage.totalBillingCost += billingCost;
4072
4288
  }
4073
- const truncated = truncateToolOutput(output2);
4289
+ const truncated = truncateToolOutput(output3);
4074
4290
  messages.push({
4075
4291
  role: "user",
4076
4292
  content: JSON.stringify(truncated),
@@ -4089,17 +4305,43 @@ ${structuredOutputExample}
4089
4305
  continue;
4090
4306
  }
4091
4307
  messages.push({ role: "assistant", content: turn.text });
4308
+ let output2;
4309
+ let parseOk = true;
4092
4310
  try {
4093
- const output2 = JSON.parse(turn.text);
4094
- return finish(buildResult(output2, turn.text, true, loopCount));
4311
+ output2 = JSON.parse(outputSchema ? stripCodeFences(turn.text) : turn.text);
4095
4312
  } catch {
4096
- if (loopCount < maxTurns) {
4313
+ parseOk = false;
4314
+ }
4315
+ if (parseOk) {
4316
+ if (!outputSchema) {
4317
+ return finish(buildResult(output2, turn.text, true, loopCount));
4318
+ }
4319
+ const errors = validateAgainstSchema(output2, outputSchema);
4320
+ if (errors.length === 0) {
4321
+ return finish(buildResult(output2, turn.text, true, loopCount));
4322
+ }
4323
+ if (loopCount < maxTurns && schemaRepairCount < MAX_SCHEMA_REPAIR_ATTEMPTS) {
4324
+ schemaRepairCount++;
4097
4325
  messages.push({
4098
4326
  role: "user",
4099
- content: "Your response was not valid JSON. Please respond with ONLY the JSON output, no other text."
4327
+ content: `Your JSON output did not conform to the required schema. Fix these problems and respond again with ONLY the corrected JSON:
4328
+ ${formatValidationErrors(errors)}`
4100
4329
  });
4101
4330
  continue;
4102
4331
  }
4332
+ throw schemaMismatch(turn.text, errors);
4333
+ }
4334
+ if (loopCount < maxTurns) {
4335
+ messages.push({
4336
+ role: "user",
4337
+ content: "Your response was not valid JSON. Please respond with ONLY the JSON output, no other text."
4338
+ });
4339
+ continue;
4340
+ }
4341
+ if (outputSchema) {
4342
+ throw schemaMismatch(turn.text, [
4343
+ { path: "$", message: "output was not valid JSON" }
4344
+ ]);
4103
4345
  }
4104
4346
  return finish(buildResult(turn.text, turn.text, false, loopCount));
4105
4347
  }
@@ -4129,11 +4371,22 @@ ${structuredOutputExample}
4129
4371
  let parsedSuccessfully = true;
4130
4372
  let output;
4131
4373
  try {
4132
- output = JSON.parse(finalText);
4374
+ output = JSON.parse(outputSchema ? stripCodeFences(finalText) : finalText);
4133
4375
  } catch {
4134
4376
  output = finalText;
4135
4377
  parsedSuccessfully = false;
4136
4378
  }
4379
+ if (outputSchema) {
4380
+ if (!parsedSuccessfully) {
4381
+ throw schemaMismatch(finalText, [
4382
+ { path: "$", message: "output was not valid JSON" }
4383
+ ]);
4384
+ }
4385
+ const errors = validateAgainstSchema(output, outputSchema);
4386
+ if (errors.length > 0) {
4387
+ throw schemaMismatch(finalText, errors);
4388
+ }
4389
+ }
4137
4390
  return finish(
4138
4391
  buildResult(output, finalText, parsedSuccessfully, loopCount + 1)
4139
4392
  );
@@ -5150,29 +5403,6 @@ var MindStudioAgent = class {
5150
5403
  };
5151
5404
  }
5152
5405
  }
5153
- /**
5154
- * Run a task agent — a multi-step tool-use loop that composes SDK actions
5155
- * to produce structured output. The model receives the prompt and tools,
5156
- * calls actions as needed, and returns JSON matching the output example.
5157
- *
5158
- * Tools can be SDK actions, your own app's methods, or both. App methods run
5159
- * as the user who invoked the method that started the task, with their roles.
5160
- *
5161
- * ```ts
5162
- * const result = await agent.runTask<{ name: string; url: string }>({
5163
- * prompt: 'Find canonical info for this restaurant, then save it.',
5164
- * input: { restaurantName: 'Tartine Bakery SF' },
5165
- * tools: [
5166
- * 'searchGoogle',
5167
- * 'fetchUrl',
5168
- * { appMethod: 'saveRestaurant', description: 'Persist the researched restaurant.' },
5169
- * ],
5170
- * structuredOutputExample: JSON.stringify({ name: 'Tartine Bakery', url: 'https://tartinebakery.com' }),
5171
- * model: 'claude-4-6-sonnet',
5172
- * });
5173
- * console.log(result.output.name); // 'Tartine Bakery'
5174
- * ```
5175
- */
5176
5406
  async runTask(options) {
5177
5407
  const taskPromise = this._runTaskInner(options);
5178
5408
  const hook = globalThis.__msWaitUntil;
@@ -5296,10 +5526,25 @@ var MindStudioAgent = class {
5296
5526
  } catch (err) {
5297
5527
  if (err instanceof MindStudioError && err.code === TURN_UNAVAILABLE_CODE) {
5298
5528
  const body = buildTaskRequestBody(options);
5299
- if (options.onEvent) {
5300
- return runTaskStream(httpConfig, body, options.onEvent);
5529
+ const result = options.onEvent ? await runTaskStream(httpConfig, body, options.onEvent) : await runTaskPoll(httpConfig, body);
5530
+ if (options.outputSchema) {
5531
+ const errors = result.parsedSuccessfully ? validateAgainstSchema(result.output, options.outputSchema) : [{ path: "$", message: "output was not valid JSON" }];
5532
+ if (errors.length > 0) {
5533
+ throw new MindStudioError(
5534
+ "[task] Output did not conform to outputSchema (legacy task route, no repair turns).",
5535
+ "task_output_schema_mismatch",
5536
+ 422,
5537
+ {
5538
+ outputRaw: result.outputRaw,
5539
+ errors,
5540
+ turns: result.turns,
5541
+ usage: result.usage,
5542
+ toolCalls: result.toolCalls
5543
+ }
5544
+ );
5545
+ }
5301
5546
  }
5302
- return runTaskPoll(httpConfig, body);
5547
+ return result;
5303
5548
  }
5304
5549
  throw err;
5305
5550
  }