(userText: string)
| 348 | } |
| 349 | |
| 350 | async runTurn(userText: string): Promise<void> { |
| 351 | const { onEvent } = this.options.callbacks |
| 352 | this.abortController = new AbortController() |
| 353 | |
| 354 | await this.maybeFireSessionStart() |
| 355 | |
| 356 | // UserPromptSubmit may block the prompt outright or attach extra context. |
| 357 | let promptContext = "" |
| 358 | if (this.hooks.hasHooks("UserPromptSubmit")) { |
| 359 | const result = await this.hooks.run("UserPromptSubmit", { prompt: userText }) |
| 360 | if (result.blocked || result.stopAll) { |
| 361 | const reason = result.blockReason || result.stopReason || "Prompt blocked by a hook." |
| 362 | onEvent({ type: "system", message: reason, isError: true }) |
| 363 | this.abortController = undefined |
| 364 | onEvent({ type: "turn-end" }) |
| 365 | return |
| 366 | } |
| 367 | if (result.additionalContext) promptContext = result.additionalContext |
| 368 | } |
| 369 | |
| 370 | if (!this.title) { |
| 371 | this.title = userText.replace(/\s+/g, " ").trim().slice(0, 80) |
| 372 | } |
| 373 | |
| 374 | let userContent = `<user_query>\n${userText}\n</user_query>` |
| 375 | if (!this.firstMessageSent) { |
| 376 | userContent = `${this.buildEnvironmentDetails()}\n\n${userContent}` |
| 377 | this.firstMessageSent = true |
| 378 | } |
| 379 | // SessionStart context sits above the prompt; UserPromptSubmit context |
| 380 | // is appended after it. Both reach the model but neither is shown as |
| 381 | // user-typed text (they live outside the <user_query> markers). |
| 382 | if (this.pendingStartContext) { |
| 383 | userContent = `${wrapHookContext("SessionStart", this.pendingStartContext)}\n\n${userContent}` |
| 384 | this.pendingStartContext = "" |
| 385 | } |
| 386 | if (promptContext) { |
| 387 | userContent = `${userContent}\n\n${wrapHookContext("UserPromptSubmit", promptContext)}` |
| 388 | } |
| 389 | this.messages.push({ role: "user", content: userContent }) |
| 390 | |
| 391 | try { |
| 392 | this.stopHookActive = false |
| 393 | for (let step = 0; step < MAX_STEPS_PER_TURN; step++) { |
| 394 | const done = await this.runStep() |
| 395 | if (!done) continue |
| 396 | // The model is ready to stop; Stop hooks may force it to continue. |
| 397 | if (await this.shouldContinueAfterStop()) continue |
| 398 | break |
| 399 | } |
| 400 | } catch (error) { |
| 401 | if ((error as Error).name === "AbortError" || this.abortController.signal.aborted) { |
| 402 | onEvent({ type: "error", message: "Interrupted." }) |
| 403 | // Keep the conversation consistent: note the interruption for the model. |
| 404 | this.messages.push({ |
| 405 | role: "user", |
| 406 | content: "System reminder: The user interrupted this response before it finished.", |
| 407 | }) |
no test coverage detected