Skip to content

Research Assistant (Temporal Runtime)

This example demonstrates a durable research assistant using the Temporal runtime. It shows:

  • Temporal workflow and activity setup
  • Activity timeout configuration
  • Worker/client architecture
  • Crash-resistant execution
  • Sub-agent orchestration as child workflows

Source Code

The full example is in examples/research-assistant-temporal/.

Prerequisites

  • Node.js 18+
  • Docker (for running Temporal server)
  • OpenAI API key

Project Structure

examples/research-assistant-temporal/
├── src/
│   ├── agent.ts          # Agent definition
│   ├── types.ts          # State and output schemas
│   ├── workflows.ts      # Temporal workflow
│   ├── activities.ts     # Activity definitions
│   ├── worker.ts         # Worker entry point
│   ├── client.ts         # Client entry point
│   ├── tools/
│   │   ├── web-search.ts
│   │   ├── take-notes.ts
│   │   └── summarize.ts
│   └── agents/
│       └── summarizer.ts # Sub-agent
├── docker-compose.yml
└── package.json

Running the Example

1. Start Temporal Server

bash
cd examples/research-assistant-temporal
npm run docker:up

This starts:

  • Temporal server on localhost:7233
  • Temporal UI on http://localhost:8080

2. Start the Worker

bash
# In one terminal
export OPENAI_API_KEY=sk-xxx
npm run worker

3. Run the Client

bash
# In another terminal
npm run client

# Or with a custom topic
npm run client "quantum computing applications"

Key Components

Temporal Workflow

The workflow orchestrates agent execution with durability:

typescript
// src/workflows.ts
import {
  proxyActivities,
  executeChild,
  isCancellation,
  log,
  workflowInfo,
} from '@temporalio/workflow';

import type { AgentWorkflowActivities } from '@helix-agents/runtime-temporal/workflow-exports';
import { runAgentWorkflow } from '@helix-agents/runtime-temporal/workflow-exports';
import type { AgentWorkflowInput, AgentWorkflowResult } from '@helix-agents/runtime-temporal';

// Configure activity timeouts
const activities = proxyActivities<AgentWorkflowActivities>({
  // Maximum time for a single activity execution
  startToCloseTimeout: '5m',

  // Retry configuration
  retry: {
    initialInterval: '1s',
    maximumInterval: '30s',
    backoffCoefficient: 2,
    maximumAttempts: 3,
  },
});

// Main workflow function
export async function agentWorkflow(input: AgentWorkflowInput): Promise<AgentWorkflowResult> {
  const info = workflowInfo();

  return runAgentWorkflow(input, activities, {
    // Start child workflows for sub-agents
    startChildWorkflow: async (childInput: AgentWorkflowInput) => {
      return executeChild(agentWorkflow, {
        workflowId: childInput.runId,
        args: [childInput],
        taskQueue: info.taskQueue,
        parentClosePolicy: 'ABANDON',
      });
    },

    // Check for Temporal cancellation
    isCancellation: (error: unknown) => isCancellation(error),

    // Workflow-safe logging
    log: {
      info: (msg: string, data?: unknown) => log.info(msg, data as object),
      warn: (msg: string, data?: unknown) => log.warn(msg, data as object),
      error: (msg: string, data?: unknown) => log.error(msg, data as object),
    },
  });
}

Key points:

  • proxyActivities creates activity proxies with timeout configuration
  • runAgentWorkflow from the framework handles the agent loop
  • startChildWorkflow enables sub-agent execution as child workflows
  • Activity retries happen automatically on transient failures

Activities

Activities perform all I/O operations:

typescript
// src/activities.ts
import { VercelAIAdapter } from '@helix-agents/llm-vercel';
import { InMemoryStateStore, InMemoryStreamManager } from '@helix-agents/store-memory';
import { GenericAgentActivities, AgentRegistry } from '@helix-agents/runtime-temporal';

import { ResearchAssistantAgent } from './agent.js';
import { SummarizerAgent } from './agents/summarizer.js';

// Create agent registry
export function createRegistry(): AgentRegistry {
  const registry = new AgentRegistry();
  registry.register(ResearchAssistantAgent);
  registry.register(SummarizerAgent);
  return registry;
}

// Create activities for worker registration
export function createActivities() {
  const registry = createRegistry();
  const stateStore = new InMemoryStateStore();
  const streamManager = new InMemoryStreamManager();
  const llmAdapter = new VercelAIAdapter();

  // Create the activities instance
  const agentActivities = new GenericAgentActivities({
    registry,
    stateStore,
    streamManager,
    llmAdapter,
  });

  // Return bound activity methods
  return {
    initializeAgentState: agentActivities.initializeAgentState.bind(agentActivities),
    executeAgentStep: agentActivities.executeAgentStep.bind(agentActivities),
    executeToolCall: agentActivities.executeToolCall.bind(agentActivities),
    registerSubAgents: agentActivities.registerSubAgents.bind(agentActivities),
    recordSubAgentResult: agentActivities.recordSubAgentResult.bind(agentActivities),
    markAgentFailed: agentActivities.markAgentFailed.bind(agentActivities),
    markAgentAborted: agentActivities.markAgentAborted.bind(agentActivities),
    endAgentStream: agentActivities.endAgentStream.bind(agentActivities),
    failAgentStream: agentActivities.failAgentStream.bind(agentActivities),
    checkExistingState: agentActivities.checkExistingState.bind(agentActivities),
    updateAgentStatus: agentActivities.updateAgentStatus.bind(agentActivities),
  };
}

Key points:

  • GenericAgentActivities provides all the standard agent activities
  • Activities are bound methods for proper this context
  • Registry maps agent names to definitions
  • For production, use RedisStateStore instead of InMemoryStateStore

Worker

The worker bundles workflows and registers activities:

typescript
// src/worker.ts
import { Worker, bundleWorkflowCode, NativeConnection } from '@temporalio/worker';
import { createActivities } from './activities.js';
import path from 'path';

const TEMPORAL_ADDRESS = process.env.TEMPORAL_ADDRESS || 'localhost:7233';
const TASK_QUEUE = process.env.TASK_QUEUE || 'research-assistant';

async function main() {
  // Connect to Temporal server
  const connection = await NativeConnection.connect({
    address: TEMPORAL_ADDRESS,
  });

  // Bundle workflow code for isolation
  const workflowBundle = await bundleWorkflowCode({
    workflowsPath: path.resolve(__dirname, './workflows.ts'),
  });

  // Create activities
  const activities = createActivities();

  // Create and start worker
  const worker = await Worker.create({
    connection,
    namespace: 'default',
    taskQueue: TASK_QUEUE,
    workflowBundle,
    activities,
  });

  // Run the worker (blocks until shutdown)
  await worker.run();
}

main();

Key points:

  • Workflows are bundled for deterministic replay
  • Worker listens on a task queue
  • Multiple workers can share the same task queue for scaling

Client

The client starts workflows and retrieves results:

typescript
// src/client.ts
import { Client, Connection } from '@temporalio/client';
import { v4 as uuidv4 } from 'uuid';
import type { agentWorkflow } from './workflows.js';
import type { AgentWorkflowInput, AgentWorkflowResult } from '@helix-agents/runtime-temporal';

const TEMPORAL_ADDRESS = process.env.TEMPORAL_ADDRESS || 'localhost:7233';
const TASK_QUEUE = process.env.TASK_QUEUE || 'research-assistant';

async function main() {
  const topic = process.argv[2] || 'the benefits of TypeScript';

  // Connect to Temporal
  const connection = await Connection.connect({
    address: TEMPORAL_ADDRESS,
  });

  const client = new Client({ connection });

  // Generate unique run ID
  const runId = `research-${Date.now()}-${uuidv4().slice(0, 8)}`;

  // Prepare workflow input
  const input: AgentWorkflowInput = {
    agentType: 'research-assistant',
    runId,
    streamId: runId,
    message: topic,
  };

  // Start the workflow
  const handle = await client.workflow.start<typeof agentWorkflow>('agentWorkflow', {
    workflowId: runId,
    taskQueue: TASK_QUEUE,
    args: [input],
  });

  console.log(`Workflow started: ${runId}`);
  console.log(`View in UI: http://localhost:8080/namespaces/default/workflows/${runId}`);

  // Wait for result
  const result: AgentWorkflowResult = await handle.result();

  if (result.status === 'completed' && result.output) {
    console.log('Research Output:', result.output);
  } else if (result.status === 'failed') {
    console.error('Research failed:', result.error);
  }

  await connection.close();
}

main();

Activity Timeouts

Configure timeouts based on your use case:

typescript
const activities = proxyActivities<AgentWorkflowActivities>({
  // Time for activity to complete (includes queue time)
  scheduleToCloseTimeout: '10m',

  // Time from start to completion
  startToCloseTimeout: '5m',

  // Heartbeat timeout (for long activities)
  heartbeatTimeout: '30s',

  // Retry policy
  retry: {
    initialInterval: '1s',
    maximumInterval: '30s',
    backoffCoefficient: 2,
    maximumAttempts: 3,
    // Don't retry non-retryable errors
    nonRetryableErrorTypes: ['ValidationError'],
  },
});

Timeout recommendations:

  • startToCloseTimeout: 2-5 minutes for LLM calls (they can be slow)
  • heartbeatTimeout: 30 seconds for activities that report progress
  • maximumAttempts: 3-5 for transient failures

Sub-Agents as Child Workflows

Sub-agents run as independent child workflows:

typescript
// In workflows.ts
startChildWorkflow: async (childInput: AgentWorkflowInput) => {
  return executeChild(agentWorkflow, {
    // Child workflow ID (must be unique)
    workflowId: childInput.runId,

    // Pass the sub-agent input
    args: [childInput],

    // Use same task queue as parent
    taskQueue: info.taskQueue,

    // Don't cancel child when parent completes
    parentClosePolicy: 'ABANDON',
  });
},

Parent close policies:

  • ABANDON: Child continues running (recommended for sub-agents)
  • TERMINATE: Child is terminated
  • REQUEST_CANCEL: Child receives cancellation signal

Production Considerations

State Storage

For production with multiple workers, use Redis:

typescript
import { RedisStateStore } from '@helix-agents/store-redis';

const stateStore = new RedisStateStore({
  host: process.env.REDIS_HOST,
  port: parseInt(process.env.REDIS_PORT || '6379'),
});

Scaling Workers

Run multiple worker instances:

bash
# Start multiple workers on same task queue
npm run worker &
npm run worker &
npm run worker &

Temporal automatically distributes work across workers.

Temporal Cloud

For production, use Temporal Cloud:

typescript
const connection = await Connection.connect({
  address: 'your-namespace.tmprl.cloud:7233',
  tls: {
    clientCertPair: {
      crt: fs.readFileSync('./client.pem'),
      key: fs.readFileSync('./client.key'),
    },
  },
});

const client = new Client({
  connection,
  namespace: 'your-namespace',
});

Monitoring

Temporal provides visibility into workflow execution:

  • Web UI: View workflow history, status, and stack traces
  • Queries: Inspect running workflow state
  • Search: Find workflows by custom attributes

Workflow Determinism

Workflows must be deterministic for replay. Avoid:

  • Date.now() - use import { sleep } from '@temporalio/workflow'
  • Math.random() - use workflow-safe alternatives
  • Network calls - use activities instead
  • Non-deterministic libraries - check Temporal compatibility

The runAgentWorkflow function handles this for you, but be careful with custom workflow code.

Next Steps

Released under the MIT License.