loopengine 0.1.99 → 0.2.1

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.
@@ -278,11 +278,9 @@ export type AgentModelConfig = {
278
278
  model: string;
279
279
  apiKey?: string;
280
280
  maxTokens?: number;
281
- /** Omit unless the model actually needs it — see
282
- * OpenAIModelCallOptions.reasoningEffort's own doc comment
283
- * (core/model-calls/openai-model-call.ts) for why a reasoning-
284
- * capable model (gpt-5.x) can require this be `'none'` just to use
285
- * function tools at all. */
281
+ /** How much the model reasons before answering; omitted, the
282
+ * model's own default. See OpenAIModelCallOptions.reasoningEffort
283
+ * (core/model-calls/openai-model-call.ts). */
286
284
  reasoningEffort?: 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
287
285
  } | {
288
286
  provider: 'deepseek';
@@ -29,7 +29,12 @@ function toContentBlockParam(block) {
29
29
  function toMessageParam(message) {
30
30
  return {
31
31
  role: message.role === 'assistant' ? 'assistant' : 'user',
32
- content: typeof message.content === 'string' ? message.content : message.content.map(toContentBlockParam),
32
+ // Blocks only another provider can use (another model's reasoning:
33
+ // 'thinking', 'openai_reasoning') are left out; as text they'd be
34
+ // empty text blocks, which the API rejects.
35
+ content: typeof message.content === 'string'
36
+ ? message.content
37
+ : message.content.filter((block) => ['text', 'tool_use', 'tool_result'].includes(block.type)).map(toContentBlockParam),
33
38
  };
34
39
  }
35
40
  export function createAnthropicModelCall(options = {}) {
@@ -9,16 +9,10 @@ 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
+ /** How much the model reasons before answering ('none' to 'max');
13
+ * omitted, the model's own default. Works with tools, since this calls
14
+ * /v1/responses. */
15
+ reasoningEffort?: OpenAI.ReasoningEffort;
22
16
  /** Inject a pre-configured client instead of building one from apiKey — e.g. to pass a custom `fetch` in tests. */
23
17
  client?: OpenAI;
24
18
  }
@@ -30,5 +24,15 @@ export declare function parseToolArguments(raw: string): Record<string, unknown>
30
24
  export declare function toMessageParams(message: Message): ChatMessage[];
31
25
  export declare function toStopReason(finishReason: string): string;
32
26
  export declare function toModelResponse(choice: OpenAI.Chat.Completions.ChatCompletion.Choice): ModelResponse;
27
+ type ResponseInputItem = OpenAI.Responses.ResponseInputItem;
28
+ /** One loopengine Message as Responses input items: an assistant turn's
29
+ * reasoning, text and tool calls in the order the model produced them,
30
+ * and a user turn's tool results (or text). Another provider's 'thinking'
31
+ * blocks are dropped: OpenAI can't use them. */
32
+ export declare function toResponsesInput(message: Message): ResponseInputItem[];
33
+ /** A Response as a ModelResponse: reasoning items as 'openai_reasoning'
34
+ * blocks, message text, and function calls as tool_use blocks (their
35
+ * call_id is the id a tool result answers). */
36
+ export declare function fromResponse(response: OpenAI.Responses.Response): ModelResponse;
33
37
  export declare function createOpenAIModelCall(options: OpenAIModelCallOptions): ModelCall;
34
38
  export {};
@@ -3,6 +3,20 @@
3
3
  // the canned fake in any agent and nothing in run-agent.ts, the adapters,
4
4
  // or any AgentConfig changes.
5
5
  //
6
+ // createOpenAIModelCall calls OpenAI's Responses API (/v1/responses):
7
+ // OpenAI's reasoning models refuse function tools together with any
8
+ // reasoning effort on /v1/chat/completions (every gpt-5.4 to gpt-6.1 model
9
+ // checked live, Nov 2026), so an agent with tools could only run them with
10
+ // reasoning off there. The Chat Completions translation below
11
+ // (toMessageParams/toStopReason/toModelResponse) stays for the
12
+ // OpenAI-compatible providers that reuse it.
13
+ //
14
+ // Responses is called statelessly (store: false): loopengine keeps the
15
+ // conversation itself, so each reasoning item comes back encrypted, rides
16
+ // in the conversation as an 'openai_reasoning' block, and is sent back
17
+ // with the tool results it led to, which the API needs to carry the
18
+ // model's reasoning across tool calls.
19
+ //
6
20
  // Structural mismatch worth knowing: loopengine's Message bundles every
7
21
  // tool_result from one turn into a single user-role message's content
8
22
  // array (mirroring Anthropic's own shape, which this framework's Message
@@ -152,23 +166,88 @@ export function toModelResponse(choice) {
152
166
  }
153
167
  return { stop_reason: toStopReason(choice.finish_reason), content };
154
168
  }
169
+ /** The reasoning item an 'openai_reasoning' block carries, or undefined if
170
+ * its data isn't one (it is sent back only as OpenAI returned it). */
171
+ function reasoningItem(block) {
172
+ try {
173
+ const item = JSON.parse(block.data ?? '');
174
+ return item && item.type === 'reasoning' ? item : undefined;
175
+ }
176
+ catch {
177
+ return undefined;
178
+ }
179
+ }
180
+ /** One loopengine Message as Responses input items: an assistant turn's
181
+ * reasoning, text and tool calls in the order the model produced them,
182
+ * and a user turn's tool results (or text). Another provider's 'thinking'
183
+ * blocks are dropped: OpenAI can't use them. */
184
+ export function toResponsesInput(message) {
185
+ const role = message.role === 'assistant' ? 'assistant' : 'user';
186
+ if (typeof message.content === 'string')
187
+ return [{ role, content: message.content }];
188
+ const items = [];
189
+ for (const block of message.content) {
190
+ if (block.type === 'openai_reasoning') {
191
+ const item = reasoningItem(block);
192
+ if (item)
193
+ items.push(item);
194
+ }
195
+ else if (block.type === 'tool_use') {
196
+ items.push({ type: 'function_call', call_id: block.id, name: block.name, arguments: JSON.stringify(block.input ?? {}) });
197
+ }
198
+ else if (block.type === 'tool_result') {
199
+ items.push({ type: 'function_call_output', call_id: block.tool_use_id, output: block.content ?? '' });
200
+ }
201
+ else if (block.type === 'text' && block.text) {
202
+ items.push({ role, content: block.text });
203
+ }
204
+ }
205
+ return items;
206
+ }
207
+ /** A Response as a ModelResponse: reasoning items as 'openai_reasoning'
208
+ * blocks, message text, and function calls as tool_use blocks (their
209
+ * call_id is the id a tool result answers). */
210
+ export function fromResponse(response) {
211
+ const content = [];
212
+ let calledTools = false;
213
+ for (const item of response.output) {
214
+ if (item.type === 'reasoning') {
215
+ content.push({ type: 'openai_reasoning', data: JSON.stringify(item) });
216
+ }
217
+ else if (item.type === 'message') {
218
+ const text = item.content.map((part) => (part.type === 'output_text' ? part.text : part.refusal)).join('');
219
+ if (text)
220
+ content.push({ type: 'text', text });
221
+ }
222
+ else if (item.type === 'function_call') {
223
+ calledTools = true;
224
+ content.push({ type: 'tool_use', id: item.call_id, name: item.name, input: parseToolArguments(item.arguments) });
225
+ }
226
+ }
227
+ const stop_reason = calledTools
228
+ ? 'tool_use'
229
+ : response.status === 'incomplete' && response.incomplete_details?.reason === 'max_output_tokens'
230
+ ? 'max_tokens'
231
+ : 'end_turn';
232
+ const usage = response.usage ? { input_tokens: response.usage.input_tokens, output_tokens: response.usage.output_tokens } : undefined;
233
+ return { stop_reason, content, ...(usage ? { usage } : {}) };
234
+ }
155
235
  export function createOpenAIModelCall(options) {
156
236
  const client = options.client ?? new OpenAI({ apiKey: options.apiKey });
157
237
  const maxTokens = options.maxTokens ?? 4096;
158
238
  return async (messages, system, tools) => {
159
- const response = await client.chat.completions.create({
239
+ const response = await client.responses.create({
160
240
  model: options.model,
161
- max_completion_tokens: maxTokens,
162
- messages: [{ role: 'system', content: system }, ...messages.flatMap(toMessageParams)],
241
+ instructions: system,
242
+ input: messages.flatMap(toResponsesInput),
243
+ max_output_tokens: maxTokens,
163
244
  tools: tools.length
164
- ? tools.map((t) => ({
165
- type: 'function',
166
- function: { name: t.name, description: t.description, parameters: t.input_schema },
167
- }))
245
+ ? tools.map((t) => ({ type: 'function', name: t.name, description: t.description, parameters: t.input_schema, strict: false }))
168
246
  : undefined,
169
- reasoning_effort: options.reasoningEffort,
247
+ ...(options.reasoningEffort ? { reasoning: { effort: options.reasoningEffort } } : {}),
248
+ store: false,
249
+ include: ['reasoning.encrypted_content'],
170
250
  });
171
- const usage = response.usage ? { input_tokens: response.usage.prompt_tokens, output_tokens: response.usage.completion_tokens } : undefined;
172
- return { ...toModelResponse(response.choices[0]), usage };
251
+ return fromResponse(response);
173
252
  };
174
253
  }
@@ -33,6 +33,10 @@ export interface ModelContentBlock {
33
33
  content?: string;
34
34
  /** tool_result blocks: true if the tool call failed or was denied. */
35
35
  is_error?: boolean;
36
+ /** Provider data carried opaquely and sent back to the same provider,
37
+ * e.g. an 'openai_reasoning' block's encrypted reasoning item (see
38
+ * model-calls/openai-model-call.ts). Never shown to a model as text. */
39
+ data?: string;
36
40
  /** tool_result blocks: the actauth decision's own reason this call was
37
41
  * allowed (e.g. "matched rule '...'"). Extra metadata riding alongside
38
42
  * `content`, not folded into it — every model-calls/* adapter
package/dist/index.d.ts CHANGED
@@ -81,7 +81,7 @@ export { templatesPageHtml } from './web/templates-page.js';
81
81
  export { TemplateError, createFromTemplate, createFromTemplateObject, describeTemplateSchedule, listTemplates, previewTemplate, templateChecklist, type CreatedTemplateAgent, type LegacyTemplateCreateDeps, type TemplateChecklistItem, type TemplateCreateDeps, type TemplatePreview, type TemplateSummary, } from './web/templates-admin.js';
82
82
  export { STARTER_TEMPLATES, starterTemplate, templateScheduleStart, type StarterTemplate, type TemplateRule, type TemplateSchedule, type TemplateSetup, type TemplateSkill, } from './core/system-agents/templates.js';
83
83
  export { workflowPageHtml } from './web/workflow-page.js';
84
- export { WorkflowError, checkWorkflowForAgent, createWorkflow, forgetWorkflow, getPlanJob, listAgentWorkflows, planWorkflow, resolveConnections, runWorkflowNow, startPlanJob, type AgentWorkflowView, type CreatedWorkflow, type PlanJob, type PlannedWorkflow, type WorkflowAgentInfo, type WorkflowCheck, type WorkflowConflict, type WorkflowCreateDeps, } from './web/workflow-admin.js';
84
+ export { WorkflowError, checkWorkflowForAgent, createWorkflow, forgetWorkflow, getPlanJob, listAgentWorkflows, planWorkflow, resolveConnections, runWorkflowNow, startPlanJob, type AgentWorkflowView, type CreatedWorkflow, type PlanJob, type PlannedWorkflow, type WorkflowAgentInfo, type WorkflowCheck, type WorkflowConflict, type WorkflowCreateDeps, type WorkflowMcpDeps, type WorkflowMcpCreateDeps, resolveMcpServers, prepareMcpAbilities, } from './web/workflow-admin.js';
85
85
  export { WORKFLOW_PLANNER_PROMPT, planWorkflowWithModel, validateWorkflowPlan, workflowTemplate, type WorkflowPlan, type WorkflowPlannerTools, type WorkflowStep } from './core/system-agents/workflow-planner.js';
86
86
  export { readWorkflows, removeWorkflow, saveWorkflow, type WorkflowRecord } from './core/workflows.js';
87
87
  export { globalConfigPageHtml } from './web/global-config-page.js';
@@ -92,8 +92,9 @@ export { listDeclaredEnvVars, setEnvVar, unsetEnvVar, setAgentEnvVar, unsetAgent
92
92
  export type { DeclaredEnvVar } from './web/env-admin.js';
93
93
  export { readActauthConfig, addActauthRule, updateActauthRule, removeActauthRule, setDefaultDecision, ActauthRuleExistsError, ActauthRuleNotFoundError, } from './web/actauth-admin.js';
94
94
  export type { ActauthRuleInput, ActauthConfigView } from './web/actauth-admin.js';
95
- export { installAbility, upgradeAbility, removeAbility, listInstalledAbilities, backfillDependencies, AbilityManifestError, AbilityVersionError, AbilityCollisionError, AbilityAlreadyInstalledError, AbilityNotInstalledError, } from './bin/ability-manager.js';
96
- export type { InstalledAbilityRecord, AbilityEnvDecl, UpgradeFileResult } from './bin/ability-manager.js';
95
+ export { installAbility, upgradeAbility, removeAbility, listInstalledAbilities, backfillDependencies, configureAgentMcpTools, AbilityManifestError, AbilityVersionError, AbilityCollisionError, AbilityAlreadyInstalledError, AbilityNotInstalledError, } from './bin/ability-manager.js';
96
+ export type { InstalledAbilityRecord, AbilityEnvDecl, UpgradeFileResult, McpToolChoice } from './bin/ability-manager.js';
97
+ export { APP_CATALOG, AppToolsError, appCatalog, appServer, appTools, catalogServer, connectedApps, isAppAbility, loadAppIcons, refreshAppTools, setAppDefault, type AgentAppTools, type AppCatalogEntry, type AppTool, type AppView, type ConnectedApp, } from './core/mcp-apps.js';
97
98
  export { searchPublicAbilities, fetchLatestAbilityVersion } from './web/abilities-admin.js';
98
99
  export type { PublicAbilitySearchResult } from './web/abilities-admin.js';
99
100
  export { AbilitySourceError, abilityOrigin, readAbilitySources, saveAbilitySource, removeAbilitySource, listSourceAbilities, sourceTokenVar, type AbilitySource, type SourceAbility, type AbilityOrigin } from './core/ability-sources.js';
package/dist/index.js CHANGED
@@ -74,7 +74,7 @@ export { templatesPageHtml } from './web/templates-page.js';
74
74
  export { TemplateError, createFromTemplate, createFromTemplateObject, describeTemplateSchedule, listTemplates, previewTemplate, templateChecklist, } from './web/templates-admin.js';
75
75
  export { STARTER_TEMPLATES, starterTemplate, templateScheduleStart, } from './core/system-agents/templates.js';
76
76
  export { workflowPageHtml } from './web/workflow-page.js';
77
- export { WorkflowError, checkWorkflowForAgent, createWorkflow, forgetWorkflow, getPlanJob, listAgentWorkflows, planWorkflow, resolveConnections, runWorkflowNow, startPlanJob, } from './web/workflow-admin.js';
77
+ export { WorkflowError, checkWorkflowForAgent, createWorkflow, forgetWorkflow, getPlanJob, listAgentWorkflows, planWorkflow, resolveConnections, runWorkflowNow, startPlanJob, resolveMcpServers, prepareMcpAbilities, } from './web/workflow-admin.js';
78
78
  export { WORKFLOW_PLANNER_PROMPT, planWorkflowWithModel, validateWorkflowPlan, workflowTemplate } from './core/system-agents/workflow-planner.js';
79
79
  export { readWorkflows, removeWorkflow, saveWorkflow } from './core/workflows.js';
80
80
  export { globalConfigPageHtml } from './web/global-config-page.js';
@@ -82,7 +82,8 @@ export { playgroundHtml } from './web/playground.js';
82
82
  export { readSkill, writeSkill, deleteSkill, SkillInvalidIdError, SkillNotFoundError } from './web/skills-admin.js';
83
83
  export { listDeclaredEnvVars, setEnvVar, unsetEnvVar, setAgentEnvVar, unsetAgentEnvVar, agentScopedEnvVarName, EnvVarNameError } from './web/env-admin.js';
84
84
  export { readActauthConfig, addActauthRule, updateActauthRule, removeActauthRule, setDefaultDecision, ActauthRuleExistsError, ActauthRuleNotFoundError, } from './web/actauth-admin.js';
85
- export { installAbility, upgradeAbility, removeAbility, listInstalledAbilities, backfillDependencies, AbilityManifestError, AbilityVersionError, AbilityCollisionError, AbilityAlreadyInstalledError, AbilityNotInstalledError, } from './bin/ability-manager.js';
85
+ export { installAbility, upgradeAbility, removeAbility, listInstalledAbilities, backfillDependencies, configureAgentMcpTools, AbilityManifestError, AbilityVersionError, AbilityCollisionError, AbilityAlreadyInstalledError, AbilityNotInstalledError, } from './bin/ability-manager.js';
86
+ export { APP_CATALOG, AppToolsError, appCatalog, appServer, appTools, catalogServer, connectedApps, isAppAbility, loadAppIcons, refreshAppTools, setAppDefault, } from './core/mcp-apps.js';
86
87
  export { searchPublicAbilities, fetchLatestAbilityVersion } from './web/abilities-admin.js';
87
88
  // Private ability sources, the workspace's own abilities and each
88
89
  // ability's details: the console's Abilities page and agent Abilities tab.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopengine",
3
- "version": "0.1.99",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "Runtime for tool-using AI agents with a transparent ReAct loop.",
6
6
  "license": "MIT",