Add Agent.prompt(AppMessage) overload for custom message types

Instead of using continue() which validates roles, prompt() now accepts
an AppMessage directly. This allows hook messages with role: 'hookMessage'
to trigger proper agent loop with message events.

- Add overloads: prompt(AppMessage) and prompt(string, attachments?)
- sendHookMessage uses prompt(appMessage) instead of appendMessage+continue
This commit is contained in:
Mario Zechner 2025-12-27 01:52:52 +01:00
parent 02f2c50155
commit a6322fda59
3 changed files with 33 additions and 25 deletions

View file

@ -170,35 +170,45 @@ export class Agent {
this.messageQueue = []; this.messageQueue = [];
} }
async prompt(input: string, attachments?: Attachment[]) { /** Send a prompt to the agent with an AppMessage. */
async prompt(message: AppMessage): Promise<void>;
/** Send a prompt to the agent with text and optional attachments. */
async prompt(input: string, attachments?: Attachment[]): Promise<void>;
async prompt(input: string | AppMessage, attachments?: Attachment[]) {
const model = this._state.model; const model = this._state.model;
if (!model) { if (!model) {
throw new Error("No model configured"); throw new Error("No model configured");
} }
// Build user message with attachments let userMessage: AppMessage;
const content: Array<TextContent | ImageContent> = [{ type: "text", text: input }];
if (attachments?.length) { if (typeof input === "string") {
for (const a of attachments) { // Build user message from text + attachments
if (a.type === "image") { const content: Array<TextContent | ImageContent> = [{ type: "text", text: input }];
content.push({ type: "image", data: a.content, mimeType: a.mimeType }); if (attachments?.length) {
} else if (a.type === "document" && a.extractedText) { for (const a of attachments) {
content.push({ if (a.type === "image") {
type: "text", content.push({ type: "image", data: a.content, mimeType: a.mimeType });
text: `\n\n[Document: ${a.fileName}]\n${a.extractedText}`, } else if (a.type === "document" && a.extractedText) {
isDocument: true, content.push({
} as TextContent); type: "text",
text: `\n\n[Document: ${a.fileName}]\n${a.extractedText}`,
isDocument: true,
} as TextContent);
}
} }
} }
userMessage = {
role: "user",
content,
attachments: attachments?.length ? attachments : undefined,
timestamp: Date.now(),
};
} else {
// Use provided AppMessage directly
userMessage = input;
} }
const userMessage: AppMessage = {
role: "user",
content,
attachments: attachments?.length ? attachments : undefined,
timestamp: Date.now(),
};
await this._runAgentLoop(userMessage); await this._runAgentLoop(userMessage);
} }

View file

@ -584,10 +584,8 @@ export class AgentSession {
// Queue for processing by agent loop // Queue for processing by agent loop
await this.agent.queueMessage(appMessage); await this.agent.queueMessage(appMessage);
} else if (triggerTurn) { } else if (triggerTurn) {
// Append to agent state and session, then trigger a turn // Send as prompt - agent loop will emit message events
this.agent.appendMessage(appMessage); await this.agent.prompt(appMessage);
// Start a new turn - emit message events for the hook message so TUI can render it
await this.agent.continue(true);
} else { } else {
// Just append to agent state and session, no turn // Just append to agent state and session, no turn
this.agent.appendMessage(appMessage); this.agent.appendMessage(appMessage);

View file

@ -115,7 +115,7 @@ export function messageTransformer(messages: AppMessage[]): Message[] {
}; };
} }
if (isHookAppMessage(m)) { if (isHookAppMessage(m)) {
// Convert hook message to user message // Convert hook message to user message for LLM
return { return {
role: "user", role: "user",
content: m.content, content: m.content,