Files
kontrans.pagedev.pl/node_modules/@lowire/loop/lib/loop.js
2026-03-03 23:49:13 +01:00

184 lines
9.5 KiB
JavaScript

"use strict";
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Loop = void 0;
const registry_1 = require("./providers/registry");
const cache_1 = require("./cache");
const summary_1 = require("./summary");
class Loop {
_provider;
_loopOptions;
_cacheOutput = {};
constructor(options) {
this._provider = (0, registry_1.getProvider)(options.api);
this._loopOptions = options;
}
async run(task, runOptions = {}) {
const options = { ...this._loopOptions, ...runOptions };
const allTools = [...(options.tools || []).map(wrapToolWithIsDone)];
const conversation = {
systemPrompt,
messages: [
{ role: 'user', content: task },
],
tools: allTools,
};
const debug = options.debug;
const budget = {
tokens: options.maxTokens,
toolCalls: options.maxToolCalls,
toolCallRetries: options.maxToolCallRetries,
};
const totalUsage = { input: 0, output: 0 };
debug?.('lowire:loop')(`Starting ${this._provider.name} loop\n${task}`);
const maxTurns = options.maxTurns || 100;
for (let turns = 0; turns < maxTurns; ++turns) {
if (options.maxTokens && budget.tokens !== undefined && budget.tokens <= 0)
return { status: 'error', error: `Budget tokens ${options.maxTokens} exhausted`, usage: totalUsage, turns };
debug?.('lowire:loop')(`Turn ${turns + 1} of (max ${maxTurns})`);
const caches = options.cache ? {
input: options.cache,
output: this._cacheOutput,
} : undefined;
const summarizedConversation = options.summarize ? this._summarizeConversation(task, conversation, options) : conversation;
await options.onBeforeTurn?.({ conversation: summarizedConversation, totalUsage, budgetTokens: budget.tokens });
if (options.signal?.aborted)
return { status: 'break', usage: totalUsage, turns };
debug?.('lowire:loop')(`Request`, JSON.stringify({ ...summarizedConversation, tools: `${summarizedConversation.tools.length} tools` }, null, 2));
const tokenEstimate = Math.floor(JSON.stringify(summarizedConversation).length / 4);
if (budget.tokens !== undefined && tokenEstimate >= budget.tokens)
return { status: 'error', error: `Input token estimate ${tokenEstimate} exceeds budget ${budget.tokens}`, usage: totalUsage, turns };
const { result: assistantMessage, usage } = await (0, cache_1.cachedComplete)(this._provider, summarizedConversation, caches, {
...options,
maxTokens: budget.tokens !== undefined ? budget.tokens - tokenEstimate : undefined,
signal: options.signal,
});
if (assistantMessage.stopReason.code === 'error')
return { status: 'error', error: assistantMessage.stopReason.message, usage: totalUsage, turns };
if (assistantMessage.stopReason.code === 'max_tokens')
return { status: 'error', error: `Max tokens exhausted`, usage: totalUsage, turns };
const intent = assistantMessage.content.filter(part => part.type === 'text').map(part => part.text).join('\n');
totalUsage.input += usage.input;
totalUsage.output += usage.output;
if (budget.tokens !== undefined)
budget.tokens -= usage.input + usage.output;
debug?.('lowire:loop')('Usage', `input: ${usage.input}, output: ${usage.output}`);
debug?.('lowire:loop')('Assistant', intent, JSON.stringify(assistantMessage.content, null, 2));
await options.onAfterTurn?.({ assistantMessage, totalUsage, budgetTokens: budget.tokens });
if (options.signal?.aborted)
return { status: 'break', usage: totalUsage, turns };
conversation.messages.push(assistantMessage);
const toolCalls = assistantMessage.content.filter(part => part.type === 'tool_call');
if (toolCalls.length === 0) {
assistantMessage.toolError = 'Error: tool call is expected in every assistant message. Call the "report_result" tool when the task is complete.';
continue;
}
for (const toolCall of toolCalls) {
if (budget.toolCalls !== undefined && --budget.toolCalls < 0)
return { status: 'error', error: `Failed to perform step, max tool calls (${options.maxToolCalls}) reached`, usage: totalUsage, turns };
const { name, arguments: args } = toolCall;
debug?.('lowire:loop')('Call tool', name, JSON.stringify(args, null, 2));
const status = await options.onBeforeToolCall?.({ assistantMessage, toolCall });
if (options.signal?.aborted)
return { status: 'break', usage: totalUsage, turns };
if (status === 'disallow') {
toolCall.result = {
content: [{ type: 'text', text: 'Tool call is disallowed.' }],
isError: true,
};
continue;
}
try {
const result = await options.callTool({
name,
arguments: {
...args,
_meta: {
'dev.lowire/intent': intent,
'dev.lowire/history': true,
'dev.lowire/state': true,
}
}
});
const text = result.content.filter(part => part.type === 'text').map(part => part.text).join('\n');
debug?.('lowire:loop')('Tool result', text, JSON.stringify(result, null, 2));
const status = await options.onAfterToolCall?.({ assistantMessage, toolCall, result });
if (options.signal?.aborted)
return { status: 'break', usage: totalUsage, turns };
if (status === 'disallow') {
toolCall.result = {
content: [{ type: 'text', text: 'Tool result is disallowed to be reported.' }],
isError: true,
};
continue;
}
toolCall.result = result;
if (args._is_done && !result.isError)
return { result, status: 'ok', usage: totalUsage, turns };
}
catch (error) {
const errorMessage = `Error while executing tool "${name}": ${error instanceof Error ? error.message : String(error)}\n\nPlease try to recover and complete the task.`;
await options.onToolCallError?.({ assistantMessage, toolCall, error });
if (options.signal?.aborted)
return { status: 'break', usage: totalUsage, turns };
toolCall.result = {
content: [{ type: 'text', text: errorMessage }],
isError: true,
};
}
}
const hasErrors = toolCalls.some(toolCall => toolCall.result?.isError);
if (!hasErrors)
budget.toolCallRetries = options.maxToolCallRetries;
if (hasErrors && budget.toolCallRetries !== undefined && --budget.toolCallRetries < 0)
return { status: 'error', error: `Failed to perform action after ${options.maxToolCallRetries} tool call retries`, usage: totalUsage, turns };
}
return { status: 'error', error: `Failed to perform step, max attempts reached`, usage: totalUsage, turns: maxTurns };
}
_summarizeConversation(task, conversation, options) {
const { summary, lastMessage } = (0, summary_1.summarizeConversation)(task, conversation, options);
return {
...conversation,
messages: [
{ role: 'user', content: summary },
...lastMessage ? [lastMessage] : [],
],
};
}
cache() {
return this._cacheOutput;
}
}
exports.Loop = Loop;
function wrapToolWithIsDone(tool) {
const inputSchema = { ...tool.inputSchema };
inputSchema.properties = {
...inputSchema.properties,
_is_done: { type: 'boolean', description: 'Whether the task is complete. If false, agentic loop will continue to perform the task.' },
};
inputSchema.required = [...(inputSchema.required || []), '_is_done'];
return {
...tool,
inputSchema,
};
}
const systemPrompt = `
- You are an autonomous agent designed to complete tasks by interacting with tools.
- Perform the user task.
- If you see text surrounded by %, it is a secret and you should preserve it as such. It will be replaced with the actual value before the tool call.
`;