loopengine 0.1.44 → 0.1.46

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.
@@ -252,6 +252,12 @@ export type AgentModelConfig = {
252
252
  model: string;
253
253
  apiKey?: string;
254
254
  maxTokens?: number;
255
+ /** Omit unless the model actually needs it — see
256
+ * OpenAIModelCallOptions.reasoningEffort's own doc comment
257
+ * (core/model-calls/openai-model-call.ts) for why a reasoning-
258
+ * capable model (gpt-5.x) can require this be `'none'` just to use
259
+ * function tools at all. */
260
+ reasoningEffort?: 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
255
261
  } | {
256
262
  provider: 'deepseek';
257
263
  model: string;
@@ -9,6 +9,16 @@ export interface OpenAIModelCallOptions {
9
9
  * one you actually want. */
10
10
  model: string;
11
11
  maxTokens?: number;
12
+ /** Omitted by default — a reasoning-capable model (the gpt-5.x family)
13
+ * otherwise gets whatever effort level OpenAI defaults it to, which
14
+ * some of these models refuse to combine with function tools on
15
+ * /v1/chat/completions at all ("Function tools with reasoning_effort
16
+ * are not supported ... set reasoning_effort to 'none'" — confirmed
17
+ * live against gpt-5.6-luna). Set this to `'none'` for a model that
18
+ * hits that error; every real loopengine call passes `tools` (even an
19
+ * empty array is still the `tools` field being present), so there's no
20
+ * tools-vs-no-tools branch here to get this wrong on. */
21
+ reasoningEffort?: OpenAI.Chat.Completions.ChatCompletionReasoningEffort;
12
22
  /** Inject a pre-configured client instead of building one from apiKey — e.g. to pass a custom `fetch` in tests. */
13
23
  client?: OpenAI;
14
24
  }
@@ -120,6 +120,7 @@ export function createOpenAIModelCall(options) {
120
120
  function: { name: t.name, description: t.description, parameters: t.input_schema },
121
121
  }))
122
122
  : undefined,
123
+ reasoning_effort: options.reasoningEffort,
123
124
  });
124
125
  return toModelResponse(response.choices[0]);
125
126
  };
@@ -8,6 +8,8 @@ export interface AgentEditableFields {
8
8
  model?: {
9
9
  provider: Provider;
10
10
  model?: string;
11
+ maxTokens?: number;
12
+ reasoningEffort?: string;
11
13
  };
12
14
  maxTurns?: number;
13
15
  contextBudgetTokens?: number;
@@ -18,6 +20,8 @@ export interface AgentEditResult {
18
20
  model?: {
19
21
  provider: Provider;
20
22
  model: string;
23
+ maxTokens?: number;
24
+ reasoningEffort?: string;
21
25
  };
22
26
  maxTurns?: number;
23
27
  contextBudgetTokens?: number;
@@ -162,12 +162,34 @@ export function editAgentFile(agentName, fields) {
162
162
  if (provider !== 'anthropic' && !modelName) {
163
163
  throw new AgentModelError(`A model name is required for provider '${provider}' — only anthropic has a default (claude-sonnet-5).`);
164
164
  }
165
+ const maxTokens = fields.model.maxTokens;
166
+ if (maxTokens !== undefined && (!Number.isInteger(maxTokens) || maxTokens < 1)) {
167
+ throw new AgentEditNotSupportedError('model.maxTokens must be a positive integer.');
168
+ }
169
+ const reasoningEffort = fields.model.reasoningEffort;
170
+ // Only AgentModelConfig's 'openai' variant carries this (see its own
171
+ // doc comment) — refusing rather than silently dropping it for
172
+ // another provider, same "don't guess" posture the model-name check
173
+ // right above already follows.
174
+ if (reasoningEffort !== undefined && provider !== 'openai') {
175
+ throw new AgentModelError(`model.reasoningEffort only applies to provider 'openai' — '${provider}' doesn't support it.`);
176
+ }
177
+ // Every field the form actually round-trips (not just provider/model)
178
+ // — omitting maxTokens/reasoningEffort here, the way this used to
179
+ // unconditionally build `{ provider, model }` alone, would silently
180
+ // wipe out a value that was already set the moment an operator
181
+ // changed anything else about the model through this same form.
182
+ const objectFields = [`provider: '${provider}'`, `model: ${tsStringLiteral(modelName)}`];
183
+ if (maxTokens !== undefined)
184
+ objectFields.push(`maxTokens: ${maxTokens}`);
185
+ if (reasoningEffort !== undefined)
186
+ objectFields.push(`reasoningEffort: ${tsStringLiteral(reasoningEffort)}`);
165
187
  edits.push({
166
188
  start: prop.initializer.getStart(sourceFile),
167
189
  end: prop.initializer.getEnd(),
168
- text: `{ provider: '${provider}', model: ${tsStringLiteral(modelName)} }`,
190
+ text: `{ ${objectFields.join(', ')} }`,
169
191
  });
170
- result.model = { provider, model: modelName };
192
+ result.model = { provider, model: modelName, maxTokens, reasoningEffort };
171
193
  // The property's own value is now correct, but a stale
172
194
  // "// reads ANTHROPIC_API_KEY" sitting right after it (from before a
173
195
  // provider change) would be actively misleading, not just cosmetic —
@@ -658,6 +658,7 @@ export const agentsConfigPageHtml = `<!doctype html>
658
658
  '<dt>Provider</dt><dd>' + escapeHtml(cfg.model.provider) + '</dd>' +
659
659
  '<dt>Model</dt><dd>' + escapeHtml(cfg.model.model || '(provider default)') + '</dd>' +
660
660
  '<dt>Max tokens</dt><dd>' + escapeHtml(cfg.model.maxTokens != null ? cfg.model.maxTokens : '(default)') + '</dd>' +
661
+ '<dt>Reasoning effort</dt><dd>' + escapeHtml(cfg.model.reasoningEffort || (cfg.model.provider === 'openai' ? '(provider default)' : 'n/a')) + '</dd>' +
661
662
  '</dl>' +
662
663
  '<form class="add-source" id="modelForm" style="display:none">' +
663
664
  '<label>Provider' +
@@ -670,6 +671,16 @@ export const agentsConfigPageHtml = `<!doctype html>
670
671
  '<label>Model name <span class="hint">(required for openai/deepseek; defaults to claude-sonnet-5 for anthropic)</span>' +
671
672
  '<input type="text" name="modelName" value="' + escapeHtml(cfg.model.model || '') + '">' +
672
673
  '</label>' +
674
+ '<label>Max tokens <span class="hint">(blank for provider default)</span>' +
675
+ '<input type="number" name="maxTokens" min="1" step="1" value="' + (cfg.model.maxTokens != null ? escapeHtml(cfg.model.maxTokens) : '') + '">' +
676
+ '</label>' +
677
+ '<label>Reasoning effort <span class="hint">(openai only — some reasoning models reject function tools unless this is \\\'none\\\'; leave unset otherwise)</span>' +
678
+ '<select name="reasoningEffort">' +
679
+ ['', 'none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'].map(function (v) {
680
+ return '<option value="' + v + '"' + (v === (cfg.model.reasoningEffort || '') ? ' selected' : '') + '>' + (v || '(unset)') + '</option>';
681
+ }).join('') +
682
+ '</select>' +
683
+ '</label>' +
673
684
  '<button type="submit">Save</button>' +
674
685
  '<button type="button" id="cancelModelBtn">Cancel</button>' +
675
686
  '<div class="error" id="modelError"></div>' +
@@ -793,6 +804,10 @@ export const agentsConfigPageHtml = `<!doctype html>
793
804
  var body = { model: { provider: data.get('provider') } };
794
805
  var modelName = data.get('modelName');
795
806
  if (modelName && modelName.trim()) body.model.model = modelName;
807
+ var maxTokens = data.get('maxTokens');
808
+ if (maxTokens && String(maxTokens).trim()) body.model.maxTokens = parseInt(maxTokens, 10);
809
+ var reasoningEffort = data.get('reasoningEffort');
810
+ if (reasoningEffort) body.model.reasoningEffort = reasoningEffort;
796
811
  fetch('/agents/' + encodeURIComponent(name), {
797
812
  method: 'PUT',
798
813
  headers: { 'content-type': 'application/json' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopengine",
3
- "version": "0.1.44",
3
+ "version": "0.1.46",
4
4
  "type": "module",
5
5
  "description": "Runtime for tool-using AI agents with a transparent ReAct loop.",
6
6
  "license": "MIT",