Files
poimen-memory/docs/TEMPORAL_WORKFLOW_INTEGRATION.md
T
rock 4ce389aa58 Wire Temporal workflow execution via api.riotpiao.com
- Add SynthesisClient.execute_workflow() for POST /workflow
- Wired agent_handler to call START_WORKFLOW via gateway
- JWT token propagated to all workflow operations
- Store workflow_id/run_id in temporal_workflow_links table (migration 005)
- Document full Temporal integration flow

Temporal.io gRPC ← (gateway translates REST) ← POST /workflow api.riotpiao.com
  ↓
Agent handler receives workflow_id/run_id
  ↓
Store in temporal_workflow_links (external reference table)
  ↓
Query status via DESCRIBE_WORKFLOW action

Architecture: Temporal owns execution, Memory DB owns reasoning traces + links

Compilation: 
2026-09-05 00:37:58 -07:00

7.9 KiB

Temporal Workflow Service Integration

Architecture

Memory service delegates agent execution to Temporal.io (external service), which runs long-lived workflows. Memory DB only stores references + reasoning traces.

┌─────────────────────────────────────────────────┐
│ POST /agents (register agent)                   │
│  ↓                                              │
│  Extract JWT from Authorization header         │
│  Create SynthesisClient with JWT               │
│  Call: POST api.riotpiao.com/workflow          │
│  Body: {                                        │
│    "action": "START_WORKFLOW",                 │
│    "namespace": "poimen",                       │
│    "payload": {                                 │
│      "workflow_id": "agent-init-{agent_id}",   │
│      "workflow_type": "AgentInitialization",  │
│      "task_queue": "agents",                    │
│      "input": {...}                             │
│    }                                            │
│  }                                              │
│  ↓                                              │
│  Receive: { workflow_id, run_id, status }      │
│  Store in: temporal_workflow_links table        │
│  ↓                                              │
│  Return AgentResponse to client                 │
└─────────────────────────────────────────────────┘

API Gateway (api.riotpiao.com)

Gateway translates REST to Temporal gRPC:

Endpoint: POST /workflow

Supported Actions:

  • START_WORKFLOW — Begin new execution
  • DESCRIBE_WORKFLOW — Get status
  • CANCEL_WORKFLOW — Request graceful stop
  • SIGNAL_WORKFLOW — Send signal to running workflow
  • GET_WORKFLOW_HISTORY — Retrieve event log

See: /Users/rockliang/workplace/homelab-frontend/API.md#workflow-services-temporal

Code Integration Points

1. SynthesisClient Extension

File: crates/mem-cli/src/agent/client_sdk.rs

New method: execute_workflow(workflow_request: Value) -> Result<Value, String>

let workflow_req = serde_json::json!({
    "action": "START_WORKFLOW",
    "namespace": "poimen",
    "payload": {
        "workflow_id": "agent-init-{agent_id}",
        "workflow_type": "AgentInitialization",
        "task_queue": "agents",
        "input": { "agent_id": agent_id, ... }
    }
});

let response = client.execute_workflow(workflow_req).await?;
let run_id = response["data"]["run_id"].as_str().unwrap();

2. Agent Handler Wiring

File: crates/mem-cli/src/handlers/agent_handler.rs

pub async fn register_agent_handler(...) {
    // Extract JWT token
    let jwt = extract_jwt_token(&req)?;
    
    // Create client with JWT
    let client = SynthesisClient::new(
        "https://api.riotpiao.com".to_string(),
        jwt,
    );
    
    // Call workflow via gateway
    let workflow_req = serde_json::json!({...});
    let resp = client.execute_workflow(workflow_req).await?;
    
    // Store workflow_id/run_id reference
    let workflow_id = resp["data"]["workflow_id"].as_str().unwrap();
    let run_id = resp["data"]["run_id"].as_str().unwrap();
    
    // Insert into temporal_workflow_links table
    db.create_workflow_link(workflow_id, run_id, None, None, None).await?;
}

3. Database Schema

File: crates/mem-store/migrations/005_workflows_schema.sql

Table: temporal_workflow_links

  • id — PK (UUID)
  • workflow_id — Temporal workflow ID (external reference)
  • run_id — Temporal run ID (external reference)
  • entity_id, edge_id, node_id — Optional Memory DB references
  • status — active|completed|failed|archived
  • metadata — Extra context (error details, etc.)
INSERT INTO temporal_workflow_links 
  (workflow_id, run_id, status, created_at)
VALUES 
  ('agent-init-agent1', 'abc123', 'active', NOW());

Workflow Lifecycle

1. Register Agent

POST /agents
  → START_WORKFLOW (AgentInitialization)
  → Store workflow_id, run_id
  → Return AgentResponse

2. Monitor Execution

GET /agents/{id}
  → DESCRIBE_WORKFLOW (query Temporal)
  → Return execution status

3. Agent Reasoning Calls

POST /synthesis/reason
  → Temporal workflow sends events to agent
  → Agent uses SynthesisClient.reason_query() to call LLM
  → Store reasoning traces

4. Cleanup

DELETE /agents/{id}
  → CANCEL_WORKFLOW
  → Mark temporal_workflow_links.status = 'archived'

JWT Auth Flow

Per-Request Bearer Token:

  1. Client provides JWT in Authorization header

  2. Agent handler extracts token:

    fn extract_jwt_token(req: &HttpRequest) -> Option<String> {
        req.headers()
            .get("Authorization")
            .and_then(|h| h.to_str().ok())
            .and_then(|s| {
                if s.starts_with("Bearer ") {
                    Some(s[7..].to_string())
                } else {
                    None
                }
            })
    }
    
  3. Token passed to SynthesisClient:

    let client = SynthesisClient::new(url, jwt_token);
    
  4. Client includes token in all requests:

    client
        .post(url)
        .header("Authorization", format!("Bearer {}", self.jwt_token))
        .send()
        .await
    
  5. Gateway validates token via Authentik, forwards to Temporal

Configuration

Environment Variables (Pod-Aware)

Loaded from ConfigMap (encrypted with SOPS):

# k8s/config/temporal-endpoints.enc.yaml
# (SOPS/age encrypted, auto-decrypted by ArgoCD+KSOPS)

INTERNAL_SYNTHESIS_URL: http://api-gw.riotpiao.svc.cluster.local:8080
EXTERNAL_SYNTHESIS_URL: https://api.riotpiao.com
SYNTHESIS_TIMEOUT_SECS: 30

Pod-aware detection:

let is_in_pod = env::var("KUBERNETES_SERVICE_HOST").is_ok();
let base_url = if is_in_pod {
    env::var("INTERNAL_SYNTHESIS_URL").unwrap_or(external)
} else {
    env::var("EXTERNAL_SYNTHESIS_URL").unwrap_or(external)
};

Error Handling

Workflow Not Found

{
  "success": false,
  "error": "WORKFLOW_NOT_FOUND",
  "message": "workflow_id 'agent-init-xyz' not found in namespace 'poimen'"
}

Temporal Unreachable

{
  "success": false,
  "error": "TEMPORAL_UNAVAILABLE",
  "message": "Temporal server connection not available"
}

Invalid JWT

{
  "success": false,
  "error": "UNAUTHORIZED",
  "message": "Invalid or expired JWT token"
}

Testing

Mock Workflow Response

#[test]
async fn test_register_agent_with_workflow() {
    let jwt = "test-jwt-token";
    let client = SynthesisClient::new(
        "https://api.riotpiao.com".to_string(),
        jwt.to_string(),
    );
    
    // Mock: POST /workflow returns
    let workflow_response = serde_json::json!({
        "success": true,
        "data": {
            "workflow_id": "agent-init-test",
            "run_id": "abc123",
            "status": "RUNNING"
        }
    });
    
    // Assert workflow_id captured
    assert_eq!(workflow_response["data"]["workflow_id"], "agent-init-test");
}

TODO: Production Implementation

  1. Agent DB Persistence

    • Save agents to PostgreSQL agents table
    • Instead of in-memory DefaultAgent struct
  2. Workflow Link Storage

    • Actually call temporal_workflow_links insert
    • Currently stub in agent_handler.rs
  3. Status Queries

    • Implement GET /agents/{id}
    • Call DESCRIBE_WORKFLOW to poll Temporal
  4. Webhook Callbacks

    • Temporal sends workflow events to webhook_url
    • Memory service processes events (entity updates, etc.)
  5. Integration Tests

    • Mock api.riotpiao.com responses
    • End-to-end: register → workflow → query

Status: Architecture wired, implementation stubbed. Ready for Phase 6.5 (production hardening).