Files
qwen-code/packages/core/src/subagents/subagent.ts
2025-10-31 18:09:08 +08:00

896 lines
29 KiB
TypeScript

/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/
import { reportError } from '../utils/errorReporting.js';
import type { Config } from '../config/config.js';
import { type ToolCallRequestInfo } from '../core/turn.js';
import {
CoreToolScheduler,
type ToolCall,
type WaitingToolCall,
} from '../core/coreToolScheduler.js';
import type {
ToolConfirmationOutcome,
ToolCallConfirmationDetails,
} from '../tools/tools.js';
import { getInitialChatHistory } from '../utils/environmentContext.js';
import type {
Content,
Part,
FunctionCall,
GenerateContentConfig,
FunctionDeclaration,
GenerateContentResponseUsageMetadata,
} from '@google/genai';
import { GeminiChat } from '../core/geminiChat.js';
import type {
PromptConfig,
ModelConfig,
RunConfig,
ToolConfig,
} from './types.js';
import { SubagentTerminateMode } from './types.js';
import type {
SubAgentFinishEvent,
SubAgentRoundEvent,
SubAgentStartEvent,
SubAgentToolCallEvent,
SubAgentToolResultEvent,
SubAgentStreamTextEvent,
SubAgentErrorEvent,
} from './subagent-events.js';
import {
type SubAgentEventEmitter,
SubAgentEventType,
} from './subagent-events.js';
import {
SubagentStatistics,
type SubagentStatsSummary,
} from './subagent-statistics.js';
import type { SubagentHooks } from './subagent-hooks.js';
import { logSubagentExecution } from '../telemetry/loggers.js';
import { SubagentExecutionEvent } from '../telemetry/types.js';
import { TaskTool } from '../tools/task.js';
import { DEFAULT_QWEN_MODEL } from '../config/models.js';
/**
* @fileoverview Defines the configuration interfaces for a subagent.
*
* These interfaces specify the structure for defining the subagent's prompt,
* the model parameters, and the execution settings.
*/
interface ExecutionStats {
startTimeMs: number;
totalDurationMs: number;
rounds: number;
totalToolCalls: number;
successfulToolCalls: number;
failedToolCalls: number;
inputTokens?: number;
outputTokens?: number;
totalTokens?: number;
estimatedCost?: number;
}
/**
* Manages the runtime context state for the subagent.
* This class provides a mechanism to store and retrieve key-value pairs
* that represent the dynamic state and variables accessible to the subagent
* during its execution.
*/
export class ContextState {
private state: Record<string, unknown> = {};
/**
* Retrieves a value from the context state.
*
* @param key - The key of the value to retrieve.
* @returns The value associated with the key, or undefined if the key is not found.
*/
get(key: string): unknown {
return this.state[key];
}
/**
* Sets a value in the context state.
*
* @param key - The key to set the value under.
* @param value - The value to set.
*/
set(key: string, value: unknown): void {
this.state[key] = value;
}
/**
* Retrieves all keys in the context state.
*
* @returns An array of all keys in the context state.
*/
get_keys(): string[] {
return Object.keys(this.state);
}
}
/**
* Replaces `${...}` placeholders in a template string with values from a context.
*
* This function identifies all placeholders in the format `${key}`, validates that
* each key exists in the provided `ContextState`, and then performs the substitution.
*
* @param template The template string containing placeholders.
* @param context The `ContextState` object providing placeholder values.
* @returns The populated string with all placeholders replaced.
* @throws {Error} if any placeholder key is not found in the context.
*/
function templateString(template: string, context: ContextState): string {
const placeholderRegex = /\$\{(\w+)\}/g;
// First, find all unique keys required by the template.
const requiredKeys = new Set(
Array.from(template.matchAll(placeholderRegex), (match) => match[1]),
);
// Check if all required keys exist in the context.
const contextKeys = new Set(context.get_keys());
const missingKeys = Array.from(requiredKeys).filter(
(key) => !contextKeys.has(key),
);
if (missingKeys.length > 0) {
throw new Error(
`Missing context values for the following keys: ${missingKeys.join(
', ',
)}`,
);
}
// Perform the replacement using a replacer function.
return template.replace(placeholderRegex, (_match, key) =>
String(context.get(key)),
);
}
/**
* Represents the scope and execution environment for a subagent.
* This class orchestrates the subagent's lifecycle, managing its chat interactions,
* runtime context, and the collection of its outputs.
*/
export class SubAgentScope {
executionStats: ExecutionStats = {
startTimeMs: 0,
totalDurationMs: 0,
rounds: 0,
totalToolCalls: 0,
successfulToolCalls: 0,
failedToolCalls: 0,
inputTokens: 0,
outputTokens: 0,
totalTokens: 0,
estimatedCost: 0,
};
private toolUsage = new Map<
string,
{
count: number;
success: number;
failure: number;
lastError?: string;
totalDurationMs?: number;
averageDurationMs?: number;
}
>();
private eventEmitter?: SubAgentEventEmitter;
private finalText: string = '';
private terminateMode: SubagentTerminateMode = SubagentTerminateMode.ERROR;
private readonly stats = new SubagentStatistics();
private hooks?: SubagentHooks;
private readonly subagentId: string;
/**
* Constructs a new SubAgentScope instance.
* @param name - The name for the subagent, used for logging and identification.
* @param runtimeContext - The shared runtime configuration and services.
* @param promptConfig - Configuration for the subagent's prompt and behavior.
* @param modelConfig - Configuration for the generative model parameters.
* @param runConfig - Configuration for the subagent's execution environment.
* @param toolConfig - Optional configuration for tools available to the subagent.
*/
private constructor(
readonly name: string,
readonly runtimeContext: Config,
private readonly promptConfig: PromptConfig,
private readonly modelConfig: ModelConfig,
private readonly runConfig: RunConfig,
private readonly toolConfig?: ToolConfig,
eventEmitter?: SubAgentEventEmitter,
hooks?: SubagentHooks,
) {
const randomPart = Math.random().toString(36).slice(2, 8);
this.subagentId = `${this.name}-${randomPart}`;
this.eventEmitter = eventEmitter;
this.hooks = hooks;
}
/**
* Creates and validates a new SubAgentScope instance.
* This factory method ensures that all tools provided in the prompt configuration
* are valid for non-interactive use before creating the subagent instance.
* @param {string} name - The name of the subagent.
* @param {Config} runtimeContext - The shared runtime configuration and services.
* @param {PromptConfig} promptConfig - Configuration for the subagent's prompt and behavior.
* @param {ModelConfig} modelConfig - Configuration for the generative model parameters.
* @param {RunConfig} runConfig - Configuration for the subagent's execution environment.
* @param {ToolConfig} [toolConfig] - Optional configuration for tools.
* @returns {Promise<SubAgentScope>} A promise that resolves to a valid SubAgentScope instance.
* @throws {Error} If any tool requires user confirmation.
*/
static async create(
name: string,
runtimeContext: Config,
promptConfig: PromptConfig,
modelConfig: ModelConfig,
runConfig: RunConfig,
toolConfig?: ToolConfig,
eventEmitter?: SubAgentEventEmitter,
hooks?: SubagentHooks,
): Promise<SubAgentScope> {
return new SubAgentScope(
name,
runtimeContext,
promptConfig,
modelConfig,
runConfig,
toolConfig,
eventEmitter,
hooks,
);
}
/**
* Runs the subagent in a non-interactive mode.
* This method orchestrates the subagent's execution loop, including prompt templating,
* tool execution, and termination conditions.
* @param {ContextState} context - The current context state containing variables for prompt templating.
* @returns {Promise<void>} A promise that resolves when the subagent has completed its execution.
*/
async runNonInteractive(
context: ContextState,
externalSignal?: AbortSignal,
): Promise<void> {
const chat = await this.createChatObject(context);
if (!chat) {
this.terminateMode = SubagentTerminateMode.ERROR;
return;
}
const abortController = new AbortController();
const onAbort = () => abortController.abort();
if (externalSignal) {
if (externalSignal.aborted) {
abortController.abort();
this.terminateMode = SubagentTerminateMode.CANCELLED;
return;
}
externalSignal.addEventListener('abort', onAbort, { once: true });
}
const toolRegistry = this.runtimeContext.getToolRegistry();
// Prepare the list of tools available to the subagent.
// If no explicit toolConfig or it contains "*" or is empty, inherit all tools.
const toolsList: FunctionDeclaration[] = [];
if (this.toolConfig) {
const asStrings = this.toolConfig.tools.filter(
(t): t is string => typeof t === 'string',
);
const hasWildcard = asStrings.includes('*');
const onlyInlineDecls = this.toolConfig.tools.filter(
(t): t is FunctionDeclaration => typeof t !== 'string',
);
if (hasWildcard || asStrings.length === 0) {
toolsList.push(
...toolRegistry
.getFunctionDeclarations()
.filter((t) => t.name !== TaskTool.Name),
);
} else {
toolsList.push(
...toolRegistry.getFunctionDeclarationsFiltered(asStrings),
);
}
toolsList.push(...onlyInlineDecls);
} else {
// Inherit all available tools by default when not specified.
toolsList.push(
...toolRegistry
.getFunctionDeclarations()
.filter((t) => t.name !== TaskTool.Name),
);
}
const initialTaskText = String(
(context.get('task_prompt') as string) ?? 'Get Started!',
);
let currentMessages: Content[] = [
{ role: 'user', parts: [{ text: initialTaskText }] },
];
const startTime = Date.now();
this.executionStats.startTimeMs = startTime;
this.stats.start(startTime);
let turnCounter = 0;
try {
// Emit start event
this.eventEmitter?.emit(SubAgentEventType.START, {
subagentId: this.subagentId,
name: this.name,
model:
this.modelConfig.model ||
this.runtimeContext.getModel() ||
DEFAULT_QWEN_MODEL,
tools: (this.toolConfig?.tools || ['*']).map((t) =>
typeof t === 'string' ? t : t.name,
),
timestamp: Date.now(),
} as SubAgentStartEvent);
// Log telemetry for subagent start
const startEvent = new SubagentExecutionEvent(this.name, 'started');
logSubagentExecution(this.runtimeContext, startEvent);
while (true) {
// Check termination conditions.
if (
this.runConfig.max_turns &&
turnCounter >= this.runConfig.max_turns
) {
this.terminateMode = SubagentTerminateMode.MAX_TURNS;
break;
}
let durationMin = (Date.now() - startTime) / (1000 * 60);
if (
this.runConfig.max_time_minutes &&
durationMin >= this.runConfig.max_time_minutes
) {
this.terminateMode = SubagentTerminateMode.TIMEOUT;
break;
}
const promptId = `${this.runtimeContext.getSessionId()}#${this.subagentId}#${turnCounter++}`;
const messageParams = {
message: currentMessages[0]?.parts || [],
config: {
abortSignal: abortController.signal,
tools: [{ functionDeclarations: toolsList }],
},
};
const responseStream = await chat.sendMessageStream(
this.modelConfig.model ||
this.runtimeContext.getModel() ||
DEFAULT_QWEN_MODEL,
messageParams,
promptId,
);
this.eventEmitter?.emit(SubAgentEventType.ROUND_START, {
subagentId: this.subagentId,
round: turnCounter,
promptId,
timestamp: Date.now(),
} as SubAgentRoundEvent);
const functionCalls: FunctionCall[] = [];
let roundText = '';
let lastUsage: GenerateContentResponseUsageMetadata | undefined =
undefined;
let currentResponseId: string | undefined = undefined;
for await (const streamEvent of responseStream) {
if (abortController.signal.aborted) {
this.terminateMode = SubagentTerminateMode.CANCELLED;
return;
}
// Handle retry events
if (streamEvent.type === 'retry') {
continue;
}
// Handle chunk events
if (streamEvent.type === 'chunk') {
const resp = streamEvent.value;
// Track the response ID for tool call correlation
if (resp.responseId) {
currentResponseId = resp.responseId;
}
if (resp.functionCalls) functionCalls.push(...resp.functionCalls);
const content = resp.candidates?.[0]?.content;
const parts = content?.parts || [];
for (const p of parts) {
const txt = (p as Part & { text?: string }).text;
if (txt) roundText += txt;
if (txt)
this.eventEmitter?.emit(SubAgentEventType.STREAM_TEXT, {
subagentId: this.subagentId,
round: turnCounter,
text: txt,
timestamp: Date.now(),
} as SubAgentStreamTextEvent);
}
if (resp.usageMetadata) lastUsage = resp.usageMetadata;
}
}
this.executionStats.rounds = turnCounter;
this.stats.setRounds(turnCounter);
durationMin = (Date.now() - startTime) / (1000 * 60);
if (
this.runConfig.max_time_minutes &&
durationMin >= this.runConfig.max_time_minutes
) {
this.terminateMode = SubagentTerminateMode.TIMEOUT;
break;
}
// Update token usage if available
if (lastUsage) {
const inTok = Number(lastUsage.promptTokenCount || 0);
const outTok = Number(lastUsage.candidatesTokenCount || 0);
if (isFinite(inTok) || isFinite(outTok)) {
this.stats.recordTokens(
isFinite(inTok) ? inTok : 0,
isFinite(outTok) ? outTok : 0,
);
// mirror legacy fields for compatibility
this.executionStats.inputTokens =
(this.executionStats.inputTokens || 0) +
(isFinite(inTok) ? inTok : 0);
this.executionStats.outputTokens =
(this.executionStats.outputTokens || 0) +
(isFinite(outTok) ? outTok : 0);
this.executionStats.totalTokens =
(this.executionStats.inputTokens || 0) +
(this.executionStats.outputTokens || 0);
this.executionStats.estimatedCost =
(this.executionStats.inputTokens || 0) * 3e-5 +
(this.executionStats.outputTokens || 0) * 6e-5;
}
}
if (functionCalls.length > 0) {
currentMessages = await this.processFunctionCalls(
functionCalls,
abortController,
promptId,
turnCounter,
currentResponseId,
);
} else {
// No tool calls — treat this as the model's final answer.
if (roundText && roundText.trim().length > 0) {
this.finalText = roundText.trim();
this.terminateMode = SubagentTerminateMode.GOAL;
break;
}
// Otherwise, nudge the model to finalize a result.
currentMessages = [
{
role: 'user',
parts: [
{
text: 'Please provide the final result now and stop calling tools.',
},
],
},
];
}
this.eventEmitter?.emit(SubAgentEventType.ROUND_END, {
subagentId: this.subagentId,
round: turnCounter,
promptId,
timestamp: Date.now(),
} as SubAgentRoundEvent);
}
} catch (error) {
console.error('Error during subagent execution:', error);
this.terminateMode = SubagentTerminateMode.ERROR;
this.eventEmitter?.emit(SubAgentEventType.ERROR, {
subagentId: this.subagentId,
error: error instanceof Error ? error.message : String(error),
timestamp: Date.now(),
} as SubAgentErrorEvent);
throw error;
} finally {
if (externalSignal) externalSignal.removeEventListener('abort', onAbort);
this.executionStats.totalDurationMs = Date.now() - startTime;
const summary = this.stats.getSummary(Date.now());
this.eventEmitter?.emit(SubAgentEventType.FINISH, {
subagentId: this.subagentId,
terminateReason: this.terminateMode,
timestamp: Date.now(),
rounds: summary.rounds,
totalDurationMs: summary.totalDurationMs,
totalToolCalls: summary.totalToolCalls,
successfulToolCalls: summary.successfulToolCalls,
failedToolCalls: summary.failedToolCalls,
inputTokens: summary.inputTokens,
outputTokens: summary.outputTokens,
totalTokens: summary.totalTokens,
} as SubAgentFinishEvent);
const completionEvent = new SubagentExecutionEvent(
this.name,
this.terminateMode === SubagentTerminateMode.GOAL
? 'completed'
: 'failed',
{
terminate_reason: this.terminateMode,
result: this.finalText,
execution_summary: this.stats.formatCompact(
'Subagent execution completed',
),
},
);
logSubagentExecution(this.runtimeContext, completionEvent);
await this.hooks?.onStop?.({
subagentId: this.subagentId,
name: this.name,
terminateReason: this.terminateMode,
summary: summary as unknown as Record<string, unknown>,
timestamp: Date.now(),
});
}
}
/**
* Processes a list of function calls, executing each one and collecting their responses.
* This method iterates through the provided function calls, executes them using the
* `executeToolCall` function (or handles `self.emitvalue` internally), and aggregates
* their results. It also manages error reporting for failed tool executions.
* @param {FunctionCall[]} functionCalls - An array of `FunctionCall` objects to process.
* @param {ToolRegistry} toolRegistry - The tool registry to look up and execute tools.
* @param {AbortController} abortController - An `AbortController` to signal cancellation of tool executions.
* @param {string} responseId - Optional API response ID for correlation with tool calls.
* @returns {Promise<Content[]>} A promise that resolves to an array of `Content` parts representing the tool responses,
* which are then used to update the chat history.
*/
private async processFunctionCalls(
functionCalls: FunctionCall[],
abortController: AbortController,
promptId: string,
currentRound: number,
responseId?: string,
): Promise<Content[]> {
const toolResponseParts: Part[] = [];
// Build scheduler
const responded = new Set<string>();
let resolveBatch: (() => void) | null = null;
const scheduler = new CoreToolScheduler({
outputUpdateHandler: undefined,
onAllToolCallsComplete: async (completedCalls) => {
for (const call of completedCalls) {
const toolName = call.request.name;
const duration = call.durationMs ?? 0;
const success = call.status === 'success';
const errorMessage =
call.status === 'error' || call.status === 'cancelled'
? call.response.error?.message
: undefined;
// Update aggregate stats
this.executionStats.totalToolCalls += 1;
if (success) {
this.executionStats.successfulToolCalls += 1;
} else {
this.executionStats.failedToolCalls += 1;
}
// Per-tool usage
const tu = this.toolUsage.get(toolName) || {
count: 0,
success: 0,
failure: 0,
totalDurationMs: 0,
averageDurationMs: 0,
};
tu.count += 1;
if (success) {
tu.success += 1;
} else {
tu.failure += 1;
tu.lastError = errorMessage || 'Unknown error';
}
tu.totalDurationMs = (tu.totalDurationMs || 0) + duration;
tu.averageDurationMs =
tu.count > 0 ? tu.totalDurationMs / tu.count : 0;
this.toolUsage.set(toolName, tu);
// Emit tool result event
this.eventEmitter?.emit(SubAgentEventType.TOOL_RESULT, {
subagentId: this.subagentId,
round: currentRound,
callId: call.request.callId,
name: toolName,
success,
error: errorMessage,
resultDisplay: call.response.resultDisplay
? typeof call.response.resultDisplay === 'string'
? call.response.resultDisplay
: JSON.stringify(call.response.resultDisplay)
: undefined,
durationMs: duration,
timestamp: Date.now(),
} as SubAgentToolResultEvent);
// Update statistics service
this.stats.recordToolCall(
toolName,
success,
duration,
this.toolUsage.get(toolName)?.lastError,
);
// post-tool hook
await this.hooks?.postToolUse?.({
subagentId: this.subagentId,
name: this.name,
toolName,
args: call.request.args,
success,
durationMs: duration,
errorMessage,
timestamp: Date.now(),
});
// Append response parts
const respParts = call.response.responseParts;
if (respParts) {
const parts = Array.isArray(respParts) ? respParts : [respParts];
for (const part of parts) {
if (typeof part === 'string') {
toolResponseParts.push({ text: part });
} else if (part) {
toolResponseParts.push(part);
}
}
}
}
// Signal that this batch is complete (all tools terminal)
resolveBatch?.();
},
onToolCallsUpdate: (calls: ToolCall[]) => {
for (const call of calls) {
if (call.status !== 'awaiting_approval') continue;
const waiting = call as WaitingToolCall;
// Emit approval request event for UI visibility
try {
const { confirmationDetails } = waiting;
const { onConfirm: _onConfirm, ...rest } = confirmationDetails;
this.eventEmitter?.emit(SubAgentEventType.TOOL_WAITING_APPROVAL, {
subagentId: this.subagentId,
round: currentRound,
callId: waiting.request.callId,
name: waiting.request.name,
description: this.getToolDescription(
waiting.request.name,
waiting.request.args,
),
confirmationDetails: rest,
respond: async (
outcome: ToolConfirmationOutcome,
payload?: Parameters<
ToolCallConfirmationDetails['onConfirm']
>[1],
) => {
if (responded.has(waiting.request.callId)) return;
responded.add(waiting.request.callId);
await waiting.confirmationDetails.onConfirm(outcome, payload);
},
timestamp: Date.now(),
});
} catch {
// ignore UI event emission failures
}
// UI now renders inline confirmation via task tool live output.
}
},
getPreferredEditor: () => undefined,
config: this.runtimeContext,
onEditorClose: () => {},
});
// Prepare requests and emit TOOL_CALL events
const requests: ToolCallRequestInfo[] = functionCalls.map((fc) => {
const toolName = String(fc.name || 'unknown');
const callId = fc.id ?? `${fc.name}-${Date.now()}`;
const args = (fc.args ?? {}) as Record<string, unknown>;
const request: ToolCallRequestInfo = {
callId,
name: toolName,
args,
isClientInitiated: true,
prompt_id: promptId,
response_id: responseId,
};
const description = this.getToolDescription(toolName, args);
this.eventEmitter?.emit(SubAgentEventType.TOOL_CALL, {
subagentId: this.subagentId,
round: currentRound,
callId,
name: toolName,
args,
description,
timestamp: Date.now(),
} as SubAgentToolCallEvent);
// pre-tool hook
void this.hooks?.preToolUse?.({
subagentId: this.subagentId,
name: this.name,
toolName,
args,
timestamp: Date.now(),
});
return request;
});
if (requests.length > 0) {
// Create a per-batch completion promise, resolve when onAllToolCallsComplete fires
const batchDone = new Promise<void>((resolve) => {
resolveBatch = () => {
resolve();
resolveBatch = null;
};
});
await scheduler.schedule(requests, abortController.signal);
await batchDone; // Wait for approvals + execution to finish
}
// If all tool calls failed, inform the model so it can re-evaluate.
if (functionCalls.length > 0 && toolResponseParts.length === 0) {
toolResponseParts.push({
text: 'All tool calls failed. Please analyze the errors and try an alternative approach.',
});
}
return [{ role: 'user', parts: toolResponseParts }];
}
getEventEmitter() {
return this.eventEmitter;
}
getStatistics() {
const total = this.executionStats.totalToolCalls;
const successRate =
total > 0 ? (this.executionStats.successfulToolCalls / total) * 100 : 0;
return {
...this.executionStats,
successRate,
toolUsage: Array.from(this.toolUsage.entries()).map(([name, v]) => ({
name,
...v,
})),
};
}
getExecutionSummary(): SubagentStatsSummary {
return this.stats.getSummary();
}
getFinalText(): string {
return this.finalText;
}
getTerminateMode(): SubagentTerminateMode {
return this.terminateMode;
}
private async createChatObject(context: ContextState) {
if (!this.promptConfig.systemPrompt && !this.promptConfig.initialMessages) {
throw new Error(
'PromptConfig must have either `systemPrompt` or `initialMessages` defined.',
);
}
if (this.promptConfig.systemPrompt && this.promptConfig.initialMessages) {
throw new Error(
'PromptConfig cannot have both `systemPrompt` and `initialMessages` defined.',
);
}
const envHistory = await getInitialChatHistory(this.runtimeContext);
const start_history = [
...envHistory,
...(this.promptConfig.initialMessages ?? []),
];
const systemInstruction = this.promptConfig.systemPrompt
? this.buildChatSystemPrompt(context)
: undefined;
try {
const generationConfig: GenerateContentConfig & {
systemInstruction?: string | Content;
} = {
temperature: this.modelConfig.temp,
topP: this.modelConfig.top_p,
};
if (systemInstruction) {
generationConfig.systemInstruction = systemInstruction;
}
return new GeminiChat(
this.runtimeContext,
generationConfig,
start_history,
);
} catch (error) {
await reportError(
error,
'Error initializing chat session.',
start_history,
'startChat',
);
// The calling function will handle the undefined return.
return undefined;
}
}
/**
* Safely retrieves the description of a tool by attempting to build it.
* Returns an empty string if any error occurs during the process.
*
* @param toolName The name of the tool to get description for.
* @param args The arguments that would be passed to the tool.
* @returns The tool description or empty string if error occurs.
*/
private getToolDescription(
toolName: string,
args: Record<string, unknown>,
): string {
try {
const toolRegistry = this.runtimeContext.getToolRegistry();
const tool = toolRegistry.getTool(toolName);
if (!tool) {
return '';
}
const toolInstance = tool.build(args);
return toolInstance.getDescription() || '';
} catch {
// Safely ignore all runtime errors and return empty string
return '';
}
}
private buildChatSystemPrompt(context: ContextState): string {
if (!this.promptConfig.systemPrompt) {
// This should ideally be caught in createChatObject, but serves as a safeguard.
return '';
}
let finalPrompt = templateString(this.promptConfig.systemPrompt, context);
// Add general non-interactive instructions.
finalPrompt += `
Important Rules:
- You operate in non-interactive mode: do not ask the user questions; proceed with available context.
- Use tools only when necessary to obtain facts or make changes.
- When the task is complete, return the final result as a normal model response (not a tool call) and stop.`;
return finalPrompt;
}
}