From d08e1e53e97ae8d32305ca7d0150042d6e1dff3d Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 9 Dec 2025 00:07:24 +0100 Subject: [PATCH] WP4: Add AgentSession prompting methods (prompt, queue, abort, reset) --- packages/coding-agent/docs/refactor.md | 13 ++- .../coding-agent/src/core/agent-session.ts | 110 +++++++++++++++++- 2 files changed, 115 insertions(+), 8 deletions(-) diff --git a/packages/coding-agent/docs/refactor.md b/packages/coding-agent/docs/refactor.md index 3a186e4d..1fb2dc50 100644 --- a/packages/coding-agent/docs/refactor.md +++ b/packages/coding-agent/docs/refactor.md @@ -448,12 +448,13 @@ async reset(): Promise { **Verification:** 1. `npm run check` passes -- [ ] Add `prompt()` method with validation and slash command expansion -- [ ] Add `queueMessage()` method -- [ ] Add `clearQueue()` method -- [ ] Add `abort()` method -- [ ] Add `reset()` method -- [ ] Verify with `npm run check` +- [x] Add `prompt()` method with validation and slash command expansion +- [x] Add `queueMessage()` method +- [x] Add `clearQueue()` method +- [x] Add `abort()` method +- [x] Add `reset()` method +- [x] Add `queuedMessageCount` getter and `getQueuedMessages()` method +- [x] Verify with `npm run check` --- diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index e580b173..81a643f3 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -13,11 +13,13 @@ * Modes use this class and add their own I/O layer on top. */ -import type { Agent, AgentEvent, AgentState, AppMessage, ThinkingLevel } from "@mariozechner/pi-agent-core"; +import type { Agent, AgentEvent, AgentState, AppMessage, Attachment, ThinkingLevel } from "@mariozechner/pi-agent-core"; import type { Model } from "@mariozechner/pi-ai"; +import { getModelsPath } from "../config.js"; +import { getApiKeyForModel } from "../model-config.js"; import type { SessionManager } from "../session-manager.js"; import type { SettingsManager } from "../settings-manager.js"; -import type { FileSlashCommand } from "../slash-commands.js"; +import { expandSlashCommand, type FileSlashCommand } from "../slash-commands.js"; /** Listener function for agent events */ export type AgentEventListener = (event: AgentEvent) => void; @@ -36,6 +38,14 @@ export interface AgentSessionConfig { fileCommands?: FileSlashCommand[]; } +/** Options for AgentSession.prompt() */ +export interface PromptOptions { + /** Whether to expand file-based slash commands (default: true) */ + expandSlashCommands?: boolean; + /** Image/file attachments */ + attachments?: Attachment[]; +} + // ============================================================================ // AgentSession Class // ============================================================================ @@ -52,6 +62,9 @@ export class AgentSession { private _unsubscribeAgent?: () => void; private _eventListeners: AgentEventListener[] = []; + // Message queue state + private _queuedMessages: string[] = []; + constructor(config: AgentSessionConfig) { this.agent = config.agent; this.sessionManager = config.sessionManager; @@ -194,4 +207,97 @@ export class AgentSession { get fileCommands(): ReadonlyArray { return this._fileCommands; } + + // ========================================================================= + // Prompting + // ========================================================================= + + /** + * Send a prompt to the agent. + * - Validates model and API key before sending + * - Expands file-based slash commands by default + * @throws Error if no model selected or no API key available + */ + async prompt(text: string, options?: PromptOptions): Promise { + const expandCommands = options?.expandSlashCommands ?? true; + + // Validate model + if (!this.model) { + throw new Error( + "No model selected.\n\n" + + "Set an API key (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)\n" + + `or create ${getModelsPath()}\n\n` + + "Then use /model to select a model.", + ); + } + + // Validate API key + const apiKey = await getApiKeyForModel(this.model); + if (!apiKey) { + throw new Error( + `No API key found for ${this.model.provider}.\n\n` + + `Set the appropriate environment variable or update ${getModelsPath()}`, + ); + } + + // Expand slash commands if requested + const expandedText = expandCommands ? expandSlashCommand(text, [...this._fileCommands]) : text; + + await this.agent.prompt(expandedText, options?.attachments); + } + + /** + * Queue a message to be sent after the current response completes. + * Use when agent is currently streaming. + */ + async queueMessage(text: string): Promise { + this._queuedMessages.push(text); + await this.agent.queueMessage({ + role: "user", + content: [{ type: "text", text }], + timestamp: Date.now(), + }); + } + + /** + * Clear queued messages and return them. + * Useful for restoring to editor when user aborts. + */ + clearQueue(): string[] { + const queued = [...this._queuedMessages]; + this._queuedMessages = []; + this.agent.clearMessageQueue(); + return queued; + } + + /** Number of messages currently queued */ + get queuedMessageCount(): number { + return this._queuedMessages.length; + } + + /** Get queued messages (read-only) */ + getQueuedMessages(): readonly string[] { + return this._queuedMessages; + } + + /** + * Abort current operation and wait for agent to become idle. + */ + async abort(): Promise { + this.agent.abort(); + await this.agent.waitForIdle(); + } + + /** + * Reset agent and session to start fresh. + * Clears all messages and starts a new session. + */ + async reset(): Promise { + this.unsubscribeAll(); + await this.abort(); + this.agent.reset(); + this.sessionManager.reset(); + this._queuedMessages = []; + // Note: caller should re-subscribe after reset if needed + } }