* Runs exactly one logical turn end to end: per-turn bookkeeping, `turn.started`, * the prompt + goal reminder, the step loop, and `turn.ended`. Goal-agnostic — * the driver layers goal semantics on top. Never throws; abnormal ends are * mapped to a `cancelled`/`failed` `turn.ended` and ret
(
turnId: number,
input: readonly ContentPart[],
origin: PromptOrigin,
signal: AbortSignal,
standalone: boolean,
)
| 478 | * mapped to a `cancelled`/`failed` `turn.ended` and returned. |
| 479 | */ |
| 480 | private async runOneTurn( |
| 481 | turnId: number, |
| 482 | input: readonly ContentPart[], |
| 483 | origin: PromptOrigin, |
| 484 | signal: AbortSignal, |
| 485 | standalone: boolean, |
| 486 | ): Promise<TurnEndResult> { |
| 487 | this.currentStep = 0; |
| 488 | this.stepToolCallKeys.clear(); |
| 489 | this.toolCallDupType.clear(); |
| 490 | const telemetryMode = this.telemetryMode(); |
| 491 | this.telemetryModeByTurn.set(turnId, telemetryMode); |
| 492 | this.currentStepByTurn.set(turnId, 0); |
| 493 | this.agent.telemetry.track('turn_started', { mode: telemetryMode, ...this.requestProtocolProps() }); |
| 494 | this.agent.fullCompaction.resetForTurn(); |
| 495 | this.agent.usage.beginTurn(); |
| 496 | this.agent.emitEvent({ type: 'turn.started', turnId, origin }); |
| 497 | this.agent.context.appendUserMessage(input, origin); |
| 498 | |
| 499 | const startedAt = Date.now(); |
| 500 | let ended: TurnEndedEvent; |
| 501 | let blockedByUserPromptHook = false; |
| 502 | let completedStopReason: LoopTurnStopReason | undefined; |
| 503 | // Emitted after turn.ended (preserving prior ordering), so the error event |
| 504 | // sits just past the turn.ended boundary that consumers watch for. |
| 505 | let errorEvent: AgentEvent | undefined; |
| 506 | try { |
| 507 | const promptHookEnded = await this.applyUserPromptHook(turnId, input, origin, signal, startedAt); |
| 508 | if (promptHookEnded !== undefined) { |
| 509 | ended = promptHookEnded.event; |
| 510 | blockedByUserPromptHook = promptHookEnded.blocked; |
| 511 | } else { |
| 512 | const stopReason = await this.runStepLoop(turnId, signal); |
| 513 | completedStopReason = stopReason; |
| 514 | const reason: TurnEndReason = |
| 515 | stopReason === 'aborted' ? 'cancelled' : stopReason === 'filtered' ? 'filtered' : 'completed'; |
| 516 | ended = { |
| 517 | type: 'turn.ended', |
| 518 | turnId, |
| 519 | reason, |
| 520 | durationMs: Date.now() - startedAt, |
| 521 | }; |
| 522 | } |
| 523 | } catch (error) { |
| 524 | if (isAbortError(error)) { |
| 525 | ended = { type: 'turn.ended', turnId, reason: 'cancelled', durationMs: Date.now() - startedAt }; |
| 526 | } else { |
| 527 | const summary = summarizeTurnError(error, turnId); |
| 528 | void this.agent.hooks?.fireAndForgetTrigger('StopFailure', { |
| 529 | matcherValue: summary.name, |
| 530 | inputData: { errorType: summary.name, errorMessage: summary.message }, |
| 531 | }); |
| 532 | ended = { type: 'turn.ended', turnId, reason: 'failed', error: summary, durationMs: Date.now() - startedAt }; |
| 533 | errorEvent = { type: 'error', ...summary }; |
| 534 | if (this.shouldTrackApiError(turnId)) { |
| 535 | const classification = classifyApiError(error, summary); |
| 536 | const properties: Record<string, TelemetryPropertyValue> = { |
| 537 | error_type: classification.errorType, |
no test coverage detected