Support multiple messages in agent.prompt() and agentLoop

- agentLoop now accepts AgentMessage[] instead of single message
- agent.prompt() accepts AgentMessage | AgentMessage[]
- Emits message_start/end for each message in the array
- AgentSession.prompt() builds array with hook message + user message
- TUI now receives events for before_agent_start injected messages
This commit is contained in:
Mario Zechner 2025-12-28 17:03:38 +01:00
parent 575c875475
commit 41af99cccf
4 changed files with 48 additions and 36 deletions

View file

@ -26,7 +26,7 @@ import type {
* The prompt is added to the context and events are emitted for it.
*/
export function agentLoop(
prompt: AgentMessage,
prompts: AgentMessage[],
context: AgentContext,
config: AgentLoopConfig,
signal?: AbortSignal,
@ -35,16 +35,18 @@ export function agentLoop(
const stream = createAgentStream();
(async () => {
const newMessages: AgentMessage[] = [prompt];
const newMessages: AgentMessage[] = [...prompts];
const currentContext: AgentContext = {
...context,
messages: [...context.messages, prompt],
messages: [...context.messages, ...prompts],
};
stream.push({ type: "agent_start" });
stream.push({ type: "turn_start" });
stream.push({ type: "message_start", message: prompt });
stream.push({ type: "message_end", message: prompt });
for (const prompt of prompts) {
stream.push({ type: "message_start", message: prompt });
stream.push({ type: "message_end", message: prompt });
}
await runLoop(currentContext, newMessages, config, signal, stream, streamFn);
})();

View file

@ -168,29 +168,33 @@ export class Agent {
}
/** Send a prompt with an AgentMessage */
async prompt(message: AgentMessage): Promise<void>;
async prompt(message: AgentMessage | AgentMessage[]): Promise<void>;
async prompt(input: string, images?: ImageContent[]): Promise<void>;
async prompt(input: string | AgentMessage, images?: ImageContent[]) {
async prompt(input: string | AgentMessage | AgentMessage[], images?: ImageContent[]) {
const model = this._state.model;
if (!model) throw new Error("No model configured");
let userMessage: AgentMessage;
let msgs: AgentMessage[];
if (typeof input === "string") {
if (Array.isArray(input)) {
msgs = input;
} else if (typeof input === "string") {
const content: Array<TextContent | ImageContent> = [{ type: "text", text: input }];
if (images && images.length > 0) {
content.push(...images);
}
userMessage = {
role: "user",
content,
timestamp: Date.now(),
};
msgs = [
{
role: "user",
content,
timestamp: Date.now(),
},
];
} else {
userMessage = input;
msgs = [input];
}
await this._runLoop(userMessage);
await this._runLoop(msgs);
}
/** Continue from current context (for retry after overflow) */
@ -208,10 +212,10 @@ export class Agent {
/**
* Run the agent loop.
* If userMessage is provided, starts a new conversation turn.
* If messages are provided, starts a new conversation turn with those messages.
* Otherwise, continues from existing context.
*/
private async _runLoop(userMessage?: AgentMessage) {
private async _runLoop(messages?: AgentMessage[]) {
const model = this._state.model;
if (!model) throw new Error("No model configured");
@ -262,8 +266,8 @@ export class Agent {
let partial: AgentMessage | null = null;
try {
const stream = userMessage
? agentLoop(userMessage, context, config, this.abortController.signal, this.streamFn)
const stream = messages
? agentLoop(messages, context, config, this.abortController.signal, this.streamFn)
: agentLoopContinue(context, config, this.abortController.signal, this.streamFn);
for await (const event of stream) {

View file

@ -105,7 +105,7 @@ describe("agentLoop with AgentMessage", () => {
};
const events: AgentEvent[] = [];
const stream = agentLoop(userPrompt, context, config, undefined, streamFn);
const stream = agentLoop([userPrompt], context, config, undefined, streamFn);
for await (const event of stream) {
events.push(event);
@ -172,7 +172,7 @@ describe("agentLoop with AgentMessage", () => {
};
const events: AgentEvent[] = [];
const stream = agentLoop(userPrompt, context, config, undefined, streamFn);
const stream = agentLoop([userPrompt], context, config, undefined, streamFn);
for await (const event of stream) {
events.push(event);
@ -224,7 +224,7 @@ describe("agentLoop with AgentMessage", () => {
return stream;
};
const stream = agentLoop(userPrompt, context, config, undefined, streamFn);
const stream = agentLoop([userPrompt], context, config, undefined, streamFn);
for await (const _ of stream) {
// consume
@ -288,7 +288,7 @@ describe("agentLoop with AgentMessage", () => {
};
const events: AgentEvent[] = [];
const stream = agentLoop(userPrompt, context, config, undefined, streamFn);
const stream = agentLoop([userPrompt], context, config, undefined, streamFn);
for await (const event of stream) {
events.push(event);
@ -351,7 +351,7 @@ describe("agentLoop with AgentMessage", () => {
};
const events: AgentEvent[] = [];
const stream = agentLoop(userPrompt, context, config, undefined, (_model, ctx, _options) => {
const stream = agentLoop([userPrompt], context, config, undefined, (_model, ctx, _options) => {
// Check if interrupt message is in context on second call
if (callIndex === 1) {
sawInterruptInContext = ctx.messages.some(

View file

@ -490,30 +490,36 @@ export class AgentSession {
// Expand file-based slash commands if requested
const expandedText = expandCommands ? expandSlashCommand(text, [...this._fileCommands]) : text;
// Build messages array (hook message if any, then user message)
const messages: AgentMessage[] = [];
// Add user message
const userContent: (TextContent | ImageContent)[] = [{ type: "text", text: expandedText }];
if (options?.images) {
userContent.push(...options.images);
}
messages.push({
role: "user",
content: userContent,
timestamp: Date.now(),
});
// Emit before_agent_start hook event
if (this._hookRunner) {
const result = await this._hookRunner.emitBeforeAgentStart(expandedText, options?.images);
if (result?.message) {
// Append hook message to agent state and session
const hookMessage: HookMessage = {
messages.push({
role: "hookMessage",
customType: result.message.customType,
content: result.message.content,
display: result.message.display,
details: result.message.details,
timestamp: Date.now(),
};
this.agent.appendMessage(hookMessage);
this.sessionManager.appendCustomMessageEntry(
result.message.customType,
result.message.content,
result.message.display,
result.message.details,
);
});
}
}
await this.agent.prompt(expandedText, options?.images);
await this.agent.prompt(messages);
await this.waitForRetry();
}