"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. `;