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

View file

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

View file

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

View file

@ -490,30 +490,36 @@ export class AgentSession {
// Expand file-based slash commands if requested // Expand file-based slash commands if requested
const expandedText = expandCommands ? expandSlashCommand(text, [...this._fileCommands]) : text; 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 // Emit before_agent_start hook event
if (this._hookRunner) { if (this._hookRunner) {
const result = await this._hookRunner.emitBeforeAgentStart(expandedText, options?.images); const result = await this._hookRunner.emitBeforeAgentStart(expandedText, options?.images);
if (result?.message) { if (result?.message) {
// Append hook message to agent state and session messages.push({
const hookMessage: HookMessage = {
role: "hookMessage", role: "hookMessage",
customType: result.message.customType, customType: result.message.customType,
content: result.message.content, content: result.message.content,
display: result.message.display, display: result.message.display,
details: result.message.details, details: result.message.details,
timestamp: Date.now(), 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(); await this.waitForRetry();
} }