@anvia/studio 1.0.9 → 1.0.12

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/README.md CHANGED
@@ -120,16 +120,22 @@ new Studio([agent], {
120
120
  }).start();
121
121
  ```
122
122
 
123
- The playground message composer shows the allowed models for the selected agent. API callers can
124
- also select a model per run:
123
+ The playground message composer shows the allowed models for the selected agent. It also renders
124
+ generic selectors for controls advertised by each completion model and restores explicit choices
125
+ from session metadata. “Default” omits the control, preserving Agent and provider defaults. API
126
+ callers can select a model and controls per run:
125
127
 
126
128
  ```json
127
129
  {
130
+ "type": "messages",
128
131
  "messages": [{ "role": "user", "content": "Summarize this ticket" }],
129
132
  "model": {
130
133
  "providerId": "anthropic",
131
134
  "modelId": "claude-sonnet-4-20250514"
132
135
  },
136
+ "controls": {
137
+ "reasoningEffort": "high"
138
+ },
133
139
  "stream": true
134
140
  }
135
141
  ```
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import { Hono } from 'hono';
2
2
  import { AgentObserver, AgentRunStartArgs, AgentRunObserver } from '@anvia/core/observability';
3
3
  import { AgentOutcome, AgentInteractionOutcome, AgentStreamEvent, Agent } from '@anvia/core/agent';
4
4
  import { AgentInteractionResponse } from '@anvia/core/agent/interactions';
5
- import { JsonObject, Message, JsonValue, CompletionModelCapabilities, ToolResultContentPart, Usage, CompletionModel, StreamingCompletionModel } from '@anvia/core/completion';
5
+ import { JsonObject, Message, JsonValue, CompletionModelCapabilities, CompletionModelControls, ToolResultContentPart, Usage, CompletionModel, StreamingCompletionModel } from '@anvia/core/completion';
6
6
  import { MemoryStore, MemoryAppendOptions, MemoryErrorOptions, MemoryScope } from '@anvia/core/memory';
7
7
  import { ModelList } from '@anvia/core/model-listing';
8
8
  import { Pipeline, PipelineGraph } from '@anvia/core/pipeline';
@@ -68,6 +68,7 @@ type StudioModelSummary = StudioModelDefinition & {
68
68
  ref: string;
69
69
  providerId: string;
70
70
  providerName?: string;
71
+ controls?: CompletionModelControls;
71
72
  };
72
73
  type StudioModelProviderConfig = {
73
74
  id: string;
@@ -1038,6 +1039,7 @@ type AgentRunRequest = (AgentRunRequestBase & {
1038
1039
  maxTurns?: number;
1039
1040
  toolConcurrency?: number;
1040
1041
  model?: StudioModelRef;
1042
+ controls?: Readonly<Record<string, string>>;
1041
1043
  interactionId?: never;
1042
1044
  response?: never;
1043
1045
  }) | (AgentRunRequestBase & {
@@ -1049,6 +1051,7 @@ type AgentRunRequest = (AgentRunRequestBase & {
1049
1051
  maxTurns?: never;
1050
1052
  toolConcurrency?: never;
1051
1053
  model?: never;
1054
+ controls?: never;
1052
1055
  });
1053
1056
  type AgentRunResponse = Exclude<AgentOutcome, AgentInteractionOutcome> | Omit<AgentInteractionOutcome, "continuation" | "messages">;
1054
1057
  type AgentRunStreamEvent = AgentStreamEvent | StudioSessionLogEvent | StudioPipelineLogEvent | StudioPipelineFinalEvent;
package/dist/index.js CHANGED
@@ -490,6 +490,8 @@ function generationMetadata(args, endArgs) {
490
490
  historyCount: request.chatHistory.length,
491
491
  temperature: request.temperature,
492
492
  maxTokens: request.maxTokens,
493
+ controls: request.controls,
494
+ reasoningEffort: request.controls?.reasoningEffort,
493
495
  toolChoice: request.toolChoice,
494
496
  providerOptionKeys: isRecord(request.providerOptions) ? Object.keys(request.providerOptions).sort() : void 0,
495
497
  hasOutputSchema: request.outputSchema !== void 0,
@@ -531,6 +533,8 @@ function completionRequestSummary(request) {
531
533
  toolNames: request.tools.map((tool) => tool.name),
532
534
  temperature: request.temperature,
533
535
  maxTokens: request.maxTokens,
536
+ controls: request.controls,
537
+ reasoningEffort: request.controls?.reasoningEffort,
534
538
  toolChoice: request.toolChoice,
535
539
  providerOptionKeys: isRecord(request.providerOptions) ? Object.keys(request.providerOptions).sort() : void 0,
536
540
  hasOutputSchema: request.outputSchema !== void 0
@@ -1026,7 +1030,12 @@ async function parseJsonBody(c, validate) {
1026
1030
  }
1027
1031
 
1028
1032
  // src/runtime/models.ts
1033
+ import {
1034
+ assertCompletionControlsSupported,
1035
+ CompletionCapabilityError
1036
+ } from "@anvia/core/completion";
1029
1037
  var STUDIO_MODEL_METADATA_KEY = "studioModel";
1038
+ var STUDIO_CONTROLS_METADATA_KEY = "studioControls";
1030
1039
  function createStudioModelRegistry(config) {
1031
1040
  if (config === void 0) {
1032
1041
  return void 0;
@@ -1073,7 +1082,7 @@ function studioModelsConfig(registry, agents) {
1073
1082
  }
1074
1083
  const providers = [...registry.providers.values()].map((provider) => {
1075
1084
  const models = [...provider.staticModels.values()].map(
1076
- (model) => modelSummary(provider, model.id, model)
1085
+ (model) => staticModelSummary(provider, model.id, model)
1077
1086
  );
1078
1087
  const config2 = {
1079
1088
  id: provider.id,
@@ -1097,17 +1106,18 @@ function studioModelsConfig(registry, agents) {
1097
1106
  }
1098
1107
  function registerModelRoutes(app, props) {
1099
1108
  app.get("/models", async (c) => {
1100
- if (props.registry === void 0) {
1109
+ const registry = props.registry;
1110
+ if (registry === void 0) {
1101
1111
  return errorResponse(c, 404, "not_found", "Model registry not configured");
1102
1112
  }
1103
1113
  const providers = await Promise.all(
1104
- [...props.registry.providers.values()].map((provider) => providerCatalog(provider))
1114
+ [...registry.providers.values()].map((provider) => providerCatalog(registry, provider))
1105
1115
  );
1106
1116
  const response = {
1107
1117
  providers
1108
1118
  };
1109
- if (props.registry.defaultModelRef !== void 0) {
1110
- response.defaultModelRef = props.registry.defaultModelRef;
1119
+ if (registry.defaultModelRef !== void 0) {
1120
+ response.defaultModelRef = registry.defaultModelRef;
1111
1121
  }
1112
1122
  return c.json(response);
1113
1123
  });
@@ -1119,7 +1129,7 @@ function registerModelRoutes(app, props) {
1119
1129
  if (provider === void 0) {
1120
1130
  return errorResponse(c, 404, "not_found", "Model provider not found");
1121
1131
  }
1122
- return c.json(await providerCatalog(provider));
1132
+ return c.json(await providerCatalog(props.registry, provider));
1123
1133
  });
1124
1134
  app.get("/agents/:agentId/models", async (c) => {
1125
1135
  const agentId = c.req.param("agentId");
@@ -1155,6 +1165,14 @@ function resolveStudioModel(registry, input) {
1155
1165
  model = provider.createCompletionModel({ modelId });
1156
1166
  registry.modelCache.set(selectedRef, model);
1157
1167
  }
1168
+ try {
1169
+ assertCompletionControlsSupported(model, input.request.controls);
1170
+ } catch (error) {
1171
+ if (error instanceof CompletionCapabilityError) {
1172
+ throw new ModelSelectionError(error.message);
1173
+ }
1174
+ throw error;
1175
+ }
1158
1176
  const metadata = provider.staticModels.get(modelId);
1159
1177
  return {
1160
1178
  ref: selectedRef,
@@ -1166,6 +1184,16 @@ function sessionModelRef(metadata) {
1166
1184
  const value = metadata?.[STUDIO_MODEL_METADATA_KEY];
1167
1185
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
1168
1186
  }
1187
+ function sessionControlValues(metadata) {
1188
+ const value = metadata?.[STUDIO_CONTROLS_METADATA_KEY];
1189
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
1190
+ const controls = Object.fromEntries(
1191
+ Object.entries(value).filter(
1192
+ (entry) => typeof entry[1] === "string"
1193
+ )
1194
+ );
1195
+ return Object.freeze(controls);
1196
+ }
1169
1197
  function normalizeOptionalModelRef(ref) {
1170
1198
  return ref === void 0 ? void 0 : normalizeModelRef(ref);
1171
1199
  }
@@ -1190,7 +1218,9 @@ var ModelSelectionError = class extends Error {
1190
1218
  };
1191
1219
  async function agentModelsCatalog(registry, agent) {
1192
1220
  const policy = registry.agentPolicies.get(agent.id);
1193
- const catalogs = await Promise.all([...registry.providers.values()].map(providerCatalog));
1221
+ const catalogs = await Promise.all(
1222
+ [...registry.providers.values()].map((provider) => providerCatalog(registry, provider))
1223
+ );
1194
1224
  const warnings = catalogs.flatMap(
1195
1225
  (catalog) => catalog.warning === void 0 ? [] : [{ providerId: catalog.id, warning: catalog.warning }]
1196
1226
  );
@@ -1205,7 +1235,7 @@ async function agentModelsCatalog(registry, agent) {
1205
1235
  const { providerId, modelId } = parseModelRef(ref);
1206
1236
  const provider = registry.providers.get(providerId);
1207
1237
  return provider === void 0 ? [] : [
1208
- modelSummary(provider, modelId, {
1238
+ modelSummary(registry, provider, modelId, {
1209
1239
  id: modelId
1210
1240
  })
1211
1241
  ];
@@ -1216,16 +1246,33 @@ async function agentModelsCatalog(registry, agent) {
1216
1246
  const defaultModelRef = policy?.defaultModelRef ?? registry.defaultModelRef;
1217
1247
  const summary = {
1218
1248
  agentId: agent.id,
1219
- models: [...models, ...exactPolicyModels]
1249
+ models: [...models, ...exactPolicyModels].map(
1250
+ (model) => withAgentControlDefaults(model, agent.agent.controls)
1251
+ )
1220
1252
  };
1221
1253
  if (defaultModelRef !== void 0) summary.defaultModelRef = defaultModelRef;
1222
1254
  if (warnings.length > 0) summary.warnings = warnings;
1223
1255
  return summary;
1224
1256
  }
1225
- async function providerCatalog(provider) {
1257
+ function withAgentControlDefaults(model, defaults) {
1258
+ if (model.controls === void 0 || defaults === void 0) return model;
1259
+ let changed = false;
1260
+ const controls = Object.fromEntries(
1261
+ Object.entries(model.controls).map(([id, control]) => {
1262
+ const defaultValue = defaults[id];
1263
+ if (typeof defaultValue !== "string" || !control.options.includes(defaultValue) || control.defaultValue === defaultValue) {
1264
+ return [id, control];
1265
+ }
1266
+ changed = true;
1267
+ return [id, { ...control, defaultValue }];
1268
+ })
1269
+ );
1270
+ return changed ? { ...model, controls } : model;
1271
+ }
1272
+ async function providerCatalog(registry, provider) {
1226
1273
  const models = /* @__PURE__ */ new Map();
1227
1274
  for (const model of provider.staticModels.values()) {
1228
- models.set(model.id, modelSummary(provider, model.id, model));
1275
+ models.set(model.id, modelSummary(registry, provider, model.id, model));
1229
1276
  }
1230
1277
  let warning;
1231
1278
  if (provider.listModels !== void 0) {
@@ -1244,7 +1291,7 @@ async function providerCatalog(provider) {
1244
1291
  if (model.contextLength !== void 0) metadata.contextLength = model.contextLength;
1245
1292
  Object.assign(metadata, staticModel?.metadata);
1246
1293
  definition.metadata = metadata;
1247
- models.set(model.id, modelSummary(provider, model.id, definition));
1294
+ models.set(model.id, modelSummary(registry, provider, model.id, definition));
1248
1295
  }
1249
1296
  } catch (error) {
1250
1297
  const serialized = serializeError(error);
@@ -1261,7 +1308,13 @@ async function providerCatalog(provider) {
1261
1308
  if (warning !== void 0) catalog.warning = warning;
1262
1309
  return catalog;
1263
1310
  }
1264
- function modelSummary(provider, modelId, model) {
1311
+ function modelSummary(registry, provider, modelId, model) {
1312
+ const summary = staticModelSummary(provider, modelId, model);
1313
+ const controls = completionModelFor(registry, provider, modelId).controls;
1314
+ if (controls !== void 0) summary.controls = controls;
1315
+ return summary;
1316
+ }
1317
+ function staticModelSummary(provider, modelId, model) {
1265
1318
  const summary = {
1266
1319
  ...model,
1267
1320
  id: modelId,
@@ -1271,6 +1324,15 @@ function modelSummary(provider, modelId, model) {
1271
1324
  if (provider.name !== void 0) summary.providerName = provider.name;
1272
1325
  return summary;
1273
1326
  }
1327
+ function completionModelFor(registry, provider, modelId) {
1328
+ const ref = `${provider.id}:${modelId}`;
1329
+ let model = registry.modelCache.get(ref);
1330
+ if (model === void 0) {
1331
+ model = provider.createCompletionModel({ modelId });
1332
+ registry.modelCache.set(ref, model);
1333
+ }
1334
+ return model;
1335
+ }
1274
1336
  function ensureModelAllowed(registry, agentId, ref) {
1275
1337
  const { providerId } = parseModelRef(ref);
1276
1338
  if (!registry.providers.has(providerId)) {
@@ -2469,13 +2531,13 @@ function parseRunRequestBody(c, body) {
2469
2531
  if (typeof body.interactionId !== "string" || body.interactionId.trim().length === 0) {
2470
2532
  return { error: errorResponse(c, 400, "bad_request", "interactionId must be a string") };
2471
2533
  }
2472
- if (body.messages !== void 0 || body.sessionId !== void 0 || body.model !== void 0) {
2534
+ if (body.messages !== void 0 || body.sessionId !== void 0 || body.model !== void 0 || body.controls !== void 0) {
2473
2535
  return {
2474
2536
  error: errorResponse(
2475
2537
  c,
2476
2538
  400,
2477
2539
  "bad_request",
2478
- "Interaction responses cannot include messages, sessionId, or model"
2540
+ "Interaction responses cannot include messages, sessionId, model, or controls"
2479
2541
  )
2480
2542
  };
2481
2543
  }
@@ -2578,6 +2640,14 @@ function parseRunRequestOptions(c, body, request) {
2578
2640
  };
2579
2641
  }
2580
2642
  }
2643
+ if (request.type === "messages" && "controls" in body) {
2644
+ if (!isObject(body.controls) || Object.values(body.controls).some((value) => typeof value !== "string")) {
2645
+ return {
2646
+ error: errorResponse(c, 400, "bad_request", "controls must be an object of string values")
2647
+ };
2648
+ }
2649
+ request.controls = body.controls;
2650
+ }
2581
2651
  if ("metadata" in body) {
2582
2652
  if (!isJsonObject(body.metadata)) {
2583
2653
  return { error: errorResponse(c, 400, "bad_request", "metadata must be an object") };
@@ -3195,29 +3265,35 @@ async function prepareAgentRun(c, props, abortSignal) {
3195
3265
  if (session instanceof Response) {
3196
3266
  return session;
3197
3267
  }
3198
- const selectedModel = selectRunModel(c, props, agent, body, session);
3268
+ const persistedControls = sessionControlValues(session?.metadata);
3269
+ const runBody = body.controls === void 0 && persistedControls !== void 0 ? { ...body, controls: persistedControls } : body;
3270
+ const selectedModel = selectRunModel(c, props, agent, runBody, session);
3199
3271
  if (selectedModel instanceof Response) {
3200
3272
  return selectedModel;
3201
3273
  }
3202
- const runAgent = selectedModel.model === void 0 ? agent.agent : cloneAgent(agent.agent, { model: selectedModel.model });
3274
+ const runAgent = selectedModel.model === void 0 ? agent.agent : cloneAgent(agent.agent, {
3275
+ model: selectedModel.model,
3276
+ controls: compatibleAgentControls(agent.agent.controls, selectedModel.model)
3277
+ });
3203
3278
  const runId = globalThis.crypto.randomUUID();
3204
3279
  const runStartedAt = Date.now();
3205
3280
  await recordRunReceived({
3206
3281
  agentId,
3207
- body,
3282
+ body: runBody,
3208
3283
  runId,
3209
3284
  selectedModel,
3210
3285
  session,
3211
3286
  store: props.stores.sessions
3212
3287
  });
3213
3288
  await recordSelectedModelWarnings({
3289
+ body: runBody,
3214
3290
  runId,
3215
3291
  selectedModel,
3216
3292
  session,
3217
3293
  store: props.stores.sessions
3218
3294
  });
3219
- const memoryMetadata = runMemoryMetadata(agentId, body, selectedModel, runId);
3220
- const promptMessage = body.messages.at(-1);
3295
+ const memoryMetadata = runMemoryMetadata(agentId, runBody, selectedModel, runId);
3296
+ const promptMessage = runBody.messages.at(-1);
3221
3297
  const sessionStore = props.stores.sessions;
3222
3298
  const shouldPersistSessionMessages = session !== void 0 && sessionStore !== void 0 && !usesStoreAsAgentMemory(runAgent, sessionStore);
3223
3299
  if (shouldPersistSessionMessages) {
@@ -3230,7 +3306,7 @@ async function prepareAgentRun(c, props, abortSignal) {
3230
3306
  }
3231
3307
  const execution = createRunExecution({
3232
3308
  agentId,
3233
- body,
3309
+ body: runBody,
3234
3310
  memoryMetadata,
3235
3311
  promptMessage,
3236
3312
  runAgent,
@@ -3239,12 +3315,12 @@ async function prepareAgentRun(c, props, abortSignal) {
3239
3315
  return {
3240
3316
  agentId,
3241
3317
  agent,
3242
- body,
3318
+ body: runBody,
3243
3319
  memoryMetadata,
3244
3320
  execution,
3245
3321
  failureMessages: void 0,
3246
3322
  memoryCompactionLogged: false,
3247
- options: createRunOptions(body, agentId, session, abortSignal),
3323
+ options: createRunOptions(runBody, agentId, session, abortSignal),
3248
3324
  runAgent,
3249
3325
  runId,
3250
3326
  runStartedAt,
@@ -3255,6 +3331,15 @@ async function prepareAgentRun(c, props, abortSignal) {
3255
3331
  shouldPersistSessionMessages
3256
3332
  };
3257
3333
  }
3334
+ function compatibleAgentControls(controls, model) {
3335
+ const modelControls = model.controls;
3336
+ const compatible = Object.fromEntries(
3337
+ Object.entries(controls ?? {}).filter(
3338
+ (entry) => typeof entry[1] === "string" && modelControls !== void 0 && Object.hasOwn(modelControls, entry[0]) && modelControls[entry[0]]?.options.includes(entry[1]) === true
3339
+ )
3340
+ );
3341
+ return Object.keys(compatible).length === 0 ? void 0 : compatible;
3342
+ }
3258
3343
  async function resolveRunSession(c, body, agentId, stores) {
3259
3344
  if (body.sessionId !== void 0 && stores.sessions === void 0) {
3260
3345
  return unsupportedCapability(c, "sessions");
@@ -3298,12 +3383,15 @@ async function recordRunReceived(props) {
3298
3383
  if (props.body.toolConcurrency !== void 0) {
3299
3384
  input.toolConcurrency = props.body.toolConcurrency;
3300
3385
  }
3301
- if (props.body.metadata !== void 0 || props.selectedModel.ref !== void 0) {
3386
+ if (props.body.metadata !== void 0 || props.selectedModel.ref !== void 0 || props.body.controls !== void 0) {
3302
3387
  const metadata = {};
3303
3388
  Object.assign(metadata, props.body.metadata);
3304
3389
  if (props.selectedModel.ref !== void 0) {
3305
3390
  metadata[STUDIO_MODEL_METADATA_KEY] = props.selectedModel.ref;
3306
3391
  }
3392
+ if (props.body.controls !== void 0) {
3393
+ metadata[STUDIO_CONTROLS_METADATA_KEY] = props.body.controls;
3394
+ }
3307
3395
  input.metadata = metadata;
3308
3396
  }
3309
3397
  await appendSessionLog(props.store, runReceivedLog(input));
@@ -3323,10 +3411,13 @@ async function recordSelectedModelWarnings(props) {
3323
3411
  metadata: warning
3324
3412
  });
3325
3413
  }
3326
- if (sessionModelRef(props.session.metadata) !== props.selectedModel.ref) {
3414
+ if (sessionModelRef(props.session.metadata) !== props.selectedModel.ref || props.body.controls !== void 0) {
3327
3415
  const metadata = {};
3328
3416
  Object.assign(metadata, props.session.metadata);
3329
3417
  metadata[STUDIO_MODEL_METADATA_KEY] = props.selectedModel.ref;
3418
+ if (props.body.controls !== void 0) {
3419
+ metadata[STUDIO_CONTROLS_METADATA_KEY] = props.body.controls;
3420
+ }
3330
3421
  await props.store?.updateSessionMetadata?.(props.session.id, metadata);
3331
3422
  }
3332
3423
  }
@@ -3334,6 +3425,7 @@ function runMemoryMetadata(agentId, body, selectedModel, runId) {
3334
3425
  const metadata = { agentId };
3335
3426
  Object.assign(metadata, body.metadata);
3336
3427
  if (selectedModel.ref !== void 0) metadata[STUDIO_MODEL_METADATA_KEY] = selectedModel.ref;
3428
+ if (body.controls !== void 0) metadata[STUDIO_CONTROLS_METADATA_KEY] = body.controls;
3337
3429
  metadata.studioRunId = runId;
3338
3430
  return metadata;
3339
3431
  }
@@ -3365,6 +3457,7 @@ function createRunOptions(body, agentId, session, abortSignal) {
3365
3457
  if (body.type === "messages" && body.toolConcurrency !== void 0) {
3366
3458
  options.toolConcurrency = body.toolConcurrency;
3367
3459
  }
3460
+ if (body.type === "messages" && body.controls !== void 0) options.controls = body.controls;
3368
3461
  if (body.trace !== void 0) {
3369
3462
  options.trace = traceForRun(body.trace, agentId, session);
3370
3463
  } else if (session !== void 0) {