Skip to content

Hooks

Overview

Turn hooks are the primary extension mechanism for customizing the workstream turn lifecycle. They run at specific points during each conversation turn and allow host applications to inject context, route agents, add prompt sections, and react to completed turns. All hooks are optional — the SDK provides sensible defaults when a hook is not provided.

Turn hooks are optional callbacks provided through LotaRuntimeConfig.turnHooks. They let the consumer application customize context assembly, agent resolution, instruction injection, and post-turn side effects.

buildContext

Called before agent execution to assemble the full context payload for the turn.

ts
turnHooks: {
  buildContext: async (params) => {
    return {
      systemOrganizationDetails: '...',
      retrievedKnowledgeSection: '...',
      preSeededMemoriesSection: '...',
      // ... additional context fields
    }
  }
}

When It Runs

After the user message is persisted and before the agent is invoked. The returned context is merged into the agent's prompt assembly.

Use Cases

  • Injecting organization profile data
  • Adding startup foundation summaries
  • Including document knowledge retrieval results
  • Providing integration-specific context (GitHub repos, Linear projects)

afterTurn

Called after the agent turn completes (after message persistence, before run cleanup).

ts
turnHooks: {
  afterTurn: async (params) => {
    // Record analytics
    // Trigger integrations
    // Update external systems
  }
}

When It Runs

After the agent's response has been fully streamed and persisted. The turn's messages, metadata, and state delta are available.

Use Cases

  • Recording turn analytics (token usage, cost, duration)
  • Triggering external integrations
  • Updating organization state based on turn results
  • Enqueuing custom background jobs

resolveAgent

Resolves agent configuration and routing for a given turn. Determines which agent handles the turn, what model it uses, and what reasoning profile to apply.

ts
turnHooks: {
  resolveAgent: async (params) => {
    return {
      agentId: 'chief',
      model: bifrostChatModel('openrouter/google/gemini-3.1-pro-preview'),
      providerOptions: {
        openai: { forceReasoning: true, reasoningEffort: 'high', reasoningSummary: 'detailed' },
      },
      tools: { /* agent tool set */ },
      extraInstructions: '<additional-context>...</additional-context>',
      stopConditions: [stepCountIs(15)],
    }
  }
}

When It Runs

After context is built and before the agent is created. The returned configuration drives agent instantiation.

Parameters Available

The hook receives information about the workstream, user, organization, and the current message to make routing decisions:

  • Workstream mode (direct/group), core type, agent ID
  • User message content and mentions
  • Organization onboarding status
  • Conversation history context

Use Cases

  • Routing to different agents based on message content or @mentions
  • Selecting model tiers based on conversation complexity
  • Applying different reasoning profiles for different workstream types
  • Overriding tool sets for specific agent/context combinations

buildExtraInstructionSections

Returns additional XML instruction sections that are injected into the agent's system prompt.

ts
turnHooks: {
  buildExtraInstructionSections: async (params) => {
    const sections: string[] = []

    sections.push([
      '<organization-profile>',
      `Company: ${org.name}`,
      `Stage: ${org.stage}`,
      '</organization-profile>',
    ].join('\n'))

    sections.push([
      '<recent-context>',
      recentActivitySummary,
      '</recent-context>',
    ].join('\n'))

    return sections
  }
}

When It Runs

During prompt assembly, after core instructions and before conversation history. The returned sections are concatenated and inserted into the system prompt.

Output Format

Each returned string should be a self-contained XML section. The SDK does not add any wrapping -- the sections are inserted as-is:

xml
<organization-profile>
Company: Acme Corp
Stage: Series A
Product: Developer tools for API testing
</organization-profile>

Use Cases

  • Injecting startup foundation context
  • Adding organization catalog data
  • Including indexed repository summaries
  • Providing integration status context (connected services, available tools)

Hook Execution Order

During a workstream turn, hooks execute in this order:

  1. buildContext -- assemble context payload
  2. buildExtraInstructionSections -- generate prompt sections
  3. resolveAgent -- determine agent and model
  4. (agent executes)
  5. afterTurn -- post-turn processing

If any hook throws, the turn fails and the error is propagated to the client. The afterTurn hook runs in a fire-and-forget pattern for non-critical side effects; critical post-turn work (memory extraction, compaction) is handled by the SDK's internal queues.

Complete Example

The following example demonstrates all four hooks working together in a real host application. This shows how a product layer wires into the SDK turn lifecycle to inject organization context, route agents, customize prompts, and track analytics.

ts
import { createLotaRuntime } from '@lota-sdk/core'
import { bifrostChatModel } from '@lota-sdk/core/ai'

const runtime = await createLotaRuntime({
  // ... database, redis, s3, agents config ...

  turnHooks: {
    // 1. Assemble context from your product's data layer
    buildContext: async ({ orgId, userId, workstreamId }) => {
      const org = await getOrganization(orgId)
      const knowledge = await getKnowledgeForWorkstream(workstreamId)

      return {
        systemOrganizationDetails: `Company: ${org.name}, Stage: ${org.stage}, Industry: ${org.industry}`,
        retrievedKnowledgeSection: knowledge,
      }
    },

    // 2. Inject product-specific prompt sections
    buildExtraInstructionSections: async ({ orgId }) => {
      const org = await getOrganization(orgId)

      return [
        `<organization-profile>\n${org.foundation}\n</organization-profile>`,
        `<connected-integrations>\n${org.integrations.join(', ')}\n</connected-integrations>`,
      ]
    },

    // 3. Route to the right agent with the right model and tools
    resolveAgent: async ({ workstream, lastUserMessage }) => {
      // Direct workstreams use their assigned agent; group workstreams route through chief
      const agentId = workstream.mode === 'direct'
        ? workstream.agentId
        : 'chief'

      return {
        agentId,
        model: bifrostChatModel('openrouter/anthropic/claude-sonnet-4'),
        tools: await buildToolsForAgent(agentId, workstream),
      }
    },

    // 4. React to completed turns
    afterTurn: async ({ orgId, workstreamId, messages, tokenUsage }) => {
      // Track usage for billing and analytics
      await analytics.trackTurn({ orgId, workstreamId, tokenUsage })

      // Notify external systems
      await integrations.notifyTurnCompleted({ orgId, workstreamId })
    },
  },
})

await runtime.connect()

This pattern keeps product-specific logic (organization data, billing, integrations) in the host application while the SDK handles the core turn lifecycle, message persistence, memory extraction, and context compaction.