loopengine 0.2.1 → 0.2.2
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/core/agent-config.d.ts +4 -0
- package/dist/core/model-calls/anthropic-model-call.d.ts +7 -0
- package/dist/core/model-calls/anthropic-model-call.js +34 -5
- package/dist/core/model-calls/openai-model-call.js +2 -1
- package/dist/web/agent-file-admin.d.ts +2 -0
- package/dist/web/agent-file-admin.js +8 -6
- package/dist/web/agents-config-page.js +17 -2
- package/package.json +1 -1
|
@@ -273,6 +273,10 @@ export type AgentModelConfig = {
|
|
|
273
273
|
model?: string;
|
|
274
274
|
apiKey?: string;
|
|
275
275
|
maxTokens?: number;
|
|
276
|
+
/** How much Claude thinks before answering; omitted, the model's own
|
|
277
|
+
* default. See AnthropicModelCallOptions.reasoningEffort
|
|
278
|
+
* (core/model-calls/anthropic-model-call.ts). */
|
|
279
|
+
reasoningEffort?: 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
276
280
|
} | {
|
|
277
281
|
provider: 'openai';
|
|
278
282
|
model: string;
|
|
@@ -4,7 +4,14 @@ export interface AnthropicModelCallOptions {
|
|
|
4
4
|
/** Defaults to the ANTHROPIC_API_KEY env var, same as the SDK itself. */
|
|
5
5
|
apiKey?: string;
|
|
6
6
|
model?: string;
|
|
7
|
+
/** Defaults to 4096, or 16000 with a reasoning effort set: thinking
|
|
8
|
+
* counts toward it. */
|
|
7
9
|
maxTokens?: number;
|
|
10
|
+
/** How much Claude thinks before answering. 'none' turns thinking off;
|
|
11
|
+
* 'minimal' and 'low' to 'max' turn on adaptive thinking at that effort
|
|
12
|
+
* (Claude has no 'minimal', so it means 'low'). Omitted, the model's
|
|
13
|
+
* own default. */
|
|
14
|
+
reasoningEffort?: 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
8
15
|
/** Inject a pre-configured client instead of building one from apiKey — e.g. to pass a custom `fetch` in tests. */
|
|
9
16
|
client?: Anthropic;
|
|
10
17
|
}
|
|
@@ -13,7 +13,24 @@
|
|
|
13
13
|
// Anthropic's own API expects a tool_use to be answered by a matching
|
|
14
14
|
// tool_result. This file's only job is translating between that shape and
|
|
15
15
|
// the SDK's own param/response types — see message-to-param below.
|
|
16
|
+
//
|
|
17
|
+
// With a reasoning effort set, Claude thinks before answering (adaptive
|
|
18
|
+
// thinking at that effort), and its thinking blocks come back signed. The
|
|
19
|
+
// API needs them sent back unchanged with the tool results they led to,
|
|
20
|
+
// so each rides in the conversation as an 'anthropic_thinking' block (the
|
|
21
|
+
// whole block as JSON in `data`), sent back only to Anthropic.
|
|
16
22
|
import Anthropic from '@anthropic-ai/sdk';
|
|
23
|
+
/** The thinking (or redacted thinking) block an 'anthropic_thinking'
|
|
24
|
+
* block carries, or undefined if its data isn't one. */
|
|
25
|
+
function thinkingBlockParam(block) {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(block.data ?? '');
|
|
28
|
+
return parsed && (parsed.type === 'thinking' || parsed.type === 'redacted_thinking') ? parsed : undefined;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
17
34
|
function toContentBlockParam(block) {
|
|
18
35
|
if (block.type === 'tool_use') {
|
|
19
36
|
return { type: 'tool_use', id: block.id, name: block.name, input: block.input ?? {} };
|
|
@@ -29,18 +46,27 @@ function toContentBlockParam(block) {
|
|
|
29
46
|
function toMessageParam(message) {
|
|
30
47
|
return {
|
|
31
48
|
role: message.role === 'assistant' ? 'assistant' : 'user',
|
|
32
|
-
//
|
|
33
|
-
// 'thinking', 'openai_reasoning')
|
|
34
|
-
// empty text blocks, which the API
|
|
49
|
+
// Claude's own signed thinking goes back as it came. Blocks only
|
|
50
|
+
// another provider can use (DeepSeek's 'thinking', 'openai_reasoning')
|
|
51
|
+
// are left out; as text they'd be empty text blocks, which the API
|
|
52
|
+
// rejects.
|
|
35
53
|
content: typeof message.content === 'string'
|
|
36
54
|
? message.content
|
|
37
|
-
: message.content.
|
|
55
|
+
: message.content.flatMap((block) => {
|
|
56
|
+
if (block.type === 'anthropic_thinking') {
|
|
57
|
+
const thinking = thinkingBlockParam(block);
|
|
58
|
+
return thinking ? [thinking] : [];
|
|
59
|
+
}
|
|
60
|
+
return ['text', 'tool_use', 'tool_result'].includes(block.type) ? [toContentBlockParam(block)] : [];
|
|
61
|
+
}),
|
|
38
62
|
};
|
|
39
63
|
}
|
|
40
64
|
export function createAnthropicModelCall(options = {}) {
|
|
41
65
|
const client = options.client ?? new Anthropic({ apiKey: options.apiKey });
|
|
42
66
|
const model = options.model ?? 'claude-sonnet-5';
|
|
43
|
-
const maxTokens = options.maxTokens ?? 4096;
|
|
67
|
+
const maxTokens = options.maxTokens ?? (options.reasoningEffort ? 16000 : 4096);
|
|
68
|
+
const effort = options.reasoningEffort === 'minimal' ? 'low' : options.reasoningEffort;
|
|
69
|
+
const reasoning = effort === undefined ? {} : effort === 'none' ? { thinking: { type: 'disabled' } } : { thinking: { type: 'adaptive' }, output_config: { effort } };
|
|
44
70
|
return async (messages, system, tools) => {
|
|
45
71
|
const response = await client.messages.create({
|
|
46
72
|
model,
|
|
@@ -54,10 +80,13 @@ export function createAnthropicModelCall(options = {}) {
|
|
|
54
80
|
input_schema: t.input_schema,
|
|
55
81
|
}))
|
|
56
82
|
: undefined,
|
|
83
|
+
...reasoning,
|
|
57
84
|
});
|
|
58
85
|
const content = response.content.map((block) => {
|
|
59
86
|
if (block.type === 'text')
|
|
60
87
|
return { type: 'text', text: block.text };
|
|
88
|
+
if (block.type === 'thinking' || block.type === 'redacted_thinking')
|
|
89
|
+
return { type: 'anthropic_thinking', data: JSON.stringify(block) };
|
|
61
90
|
if (block.type === 'tool_use') {
|
|
62
91
|
return { type: 'tool_use', id: block.id, name: block.name, input: block.input };
|
|
63
92
|
}
|
|
@@ -234,7 +234,8 @@ export function fromResponse(response) {
|
|
|
234
234
|
}
|
|
235
235
|
export function createOpenAIModelCall(options) {
|
|
236
236
|
const client = options.client ?? new OpenAI({ apiKey: options.apiKey });
|
|
237
|
-
|
|
237
|
+
// Reasoning counts toward max_output_tokens.
|
|
238
|
+
const maxTokens = options.maxTokens ?? (options.reasoningEffort && options.reasoningEffort !== 'none' ? 16000 : 4096);
|
|
238
239
|
return async (messages, system, tools) => {
|
|
239
240
|
const response = await client.responses.create({
|
|
240
241
|
model: options.model,
|
|
@@ -3,6 +3,8 @@ export declare class AgentEditNotSupportedError extends Error {
|
|
|
3
3
|
export declare class AgentFileNotFoundError extends Error {
|
|
4
4
|
}
|
|
5
5
|
type Provider = 'anthropic' | 'openai' | 'deepseek' | 'kimi' | 'glm' | 'gemini' | 'xai' | 'meta';
|
|
6
|
+
/** The providers whose model calls take a reasoning effort. */
|
|
7
|
+
export declare const REASONING_PROVIDERS: Provider[];
|
|
6
8
|
export interface AgentEditableFields {
|
|
7
9
|
systemPrompt?: string;
|
|
8
10
|
model?: {
|
|
@@ -48,6 +48,8 @@ export class AgentEditNotSupportedError extends Error {
|
|
|
48
48
|
}
|
|
49
49
|
export class AgentFileNotFoundError extends Error {
|
|
50
50
|
}
|
|
51
|
+
/** The providers whose model calls take a reasoning effort. */
|
|
52
|
+
export const REASONING_PROVIDERS = ['openai', 'anthropic'];
|
|
51
53
|
const MODEL_ENV_VAR = {
|
|
52
54
|
anthropic: 'ANTHROPIC_API_KEY',
|
|
53
55
|
openai: 'OPENAI_API_KEY',
|
|
@@ -178,12 +180,12 @@ export function editAgentFile(agentName, fields) {
|
|
|
178
180
|
throw new AgentEditNotSupportedError('model.maxTokens must be a positive integer.');
|
|
179
181
|
}
|
|
180
182
|
const reasoningEffort = fields.model.reasoningEffort;
|
|
181
|
-
// Only AgentModelConfig's 'openai'
|
|
182
|
-
// doc
|
|
183
|
-
// another provider, same "don't guess" posture the model-name
|
|
184
|
-
// right above already follows.
|
|
185
|
-
if (reasoningEffort !== undefined && provider
|
|
186
|
-
throw new AgentModelError(`model.reasoningEffort only applies to
|
|
183
|
+
// Only AgentModelConfig's 'openai' and 'anthropic' variants carry this
|
|
184
|
+
// (see their doc comments) — refusing rather than silently dropping it
|
|
185
|
+
// for another provider, same "don't guess" posture the model-name
|
|
186
|
+
// check right above already follows.
|
|
187
|
+
if (reasoningEffort !== undefined && !REASONING_PROVIDERS.includes(provider)) {
|
|
188
|
+
throw new AgentModelError(`model.reasoningEffort only applies to providers ${REASONING_PROVIDERS.join(' and ')} — '${provider}' doesn't support it.`);
|
|
187
189
|
}
|
|
188
190
|
// Every field the form actually round-trips (not just provider/model)
|
|
189
191
|
// — omitting maxTokens/reasoningEffort here, the way this used to
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
import { devUiCss } from './dev-ui-styles.js';
|
|
29
29
|
import { MESSAGING_ICONS } from './messaging-icons.js';
|
|
30
30
|
import { consoleShellScript, consoleSidebarHtml } from './console-shell.js';
|
|
31
|
+
import { REASONING_PROVIDERS } from './agent-file-admin.js';
|
|
31
32
|
export const agentsConfigPageHtml = `<!doctype html>
|
|
32
33
|
<html lang="en">
|
|
33
34
|
<head>
|
|
@@ -358,6 +359,8 @@ ${consoleSidebarHtml}
|
|
|
358
359
|
${consoleShellScript}
|
|
359
360
|
<script>
|
|
360
361
|
(function () {
|
|
362
|
+
// The providers whose models take a reasoning effort (web/agent-file-admin.ts).
|
|
363
|
+
var REASONING_PROVIDERS = ${JSON.stringify(REASONING_PROVIDERS)};
|
|
361
364
|
var empty = document.getElementById('empty');
|
|
362
365
|
var detail = document.getElementById('detail');
|
|
363
366
|
var currentName = null;
|
|
@@ -792,7 +795,7 @@ ${consoleShellScript}
|
|
|
792
795
|
'<dt>Provider</dt><dd>' + escapeHtml(cfg.model.provider) + '</dd>' +
|
|
793
796
|
'<dt>Model</dt><dd>' + escapeHtml(cfg.model.model || '(provider default)') + '</dd>' +
|
|
794
797
|
'<dt>Max tokens</dt><dd>' + escapeHtml(cfg.model.maxTokens != null ? cfg.model.maxTokens : '(default)') + '</dd>' +
|
|
795
|
-
'<dt>Reasoning effort</dt><dd>' + escapeHtml(cfg.model.reasoningEffort || (cfg.model.provider
|
|
798
|
+
'<dt>Reasoning effort</dt><dd>' + escapeHtml(cfg.model.reasoningEffort || (REASONING_PROVIDERS.indexOf(cfg.model.provider) !== -1 ? '(model default)' : 'n/a')) + '</dd>' +
|
|
796
799
|
'</dl>' +
|
|
797
800
|
'<form class="add-source" id="modelForm" style="display:none">' +
|
|
798
801
|
'<label>Provider' +
|
|
@@ -808,7 +811,7 @@ ${consoleShellScript}
|
|
|
808
811
|
'<label>Max tokens <span class="hint">(blank for provider default)</span>' +
|
|
809
812
|
'<input type="number" name="maxTokens" min="1" step="1" value="' + (cfg.model.maxTokens != null ? escapeHtml(cfg.model.maxTokens) : '') + '">' +
|
|
810
813
|
'</label>' +
|
|
811
|
-
'<label>Reasoning effort <span class="hint">(
|
|
814
|
+
'<label id="reasoningEffortField">Reasoning effort <span class="hint">(how much the model thinks before answering: none turns it off, unset uses the model\\\'s default)</span>' +
|
|
812
815
|
'<select name="reasoningEffort">' +
|
|
813
816
|
['', 'none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'].map(function (v) {
|
|
814
817
|
return '<option value="' + v + '"' + (v === (cfg.model.reasoningEffort || '') ? ' selected' : '') + '>' + (v || '(unset)') + '</option>';
|
|
@@ -918,7 +921,19 @@ ${consoleShellScript}
|
|
|
918
921
|
var modelForm = overviewPanel.querySelector('#modelForm');
|
|
919
922
|
var modelError = overviewPanel.querySelector('#modelError');
|
|
920
923
|
|
|
924
|
+
// Only OpenAI and Anthropic models take a reasoning effort: the
|
|
925
|
+
// field shows for those, and is cleared for any other provider so
|
|
926
|
+
// it's never saved with one.
|
|
927
|
+
var providerSelect = modelForm.querySelector('select[name="provider"]');
|
|
928
|
+
var reasoningField = modelForm.querySelector('#reasoningEffortField');
|
|
929
|
+
function syncReasoningField() {
|
|
930
|
+
var supported = REASONING_PROVIDERS.indexOf(providerSelect.value) !== -1;
|
|
931
|
+
reasoningField.style.display = supported ? '' : 'none';
|
|
932
|
+
if (!supported) reasoningField.querySelector('select').value = '';
|
|
933
|
+
}
|
|
934
|
+
providerSelect.addEventListener('change', syncReasoningField);
|
|
921
935
|
editModelBtn.addEventListener('click', function () {
|
|
936
|
+
syncReasoningField();
|
|
922
937
|
modelDisplay.style.display = 'none';
|
|
923
938
|
editModelBtn.style.display = 'none';
|
|
924
939
|
modelForm.style.display = '';
|