- 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: ✅
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 executionDESCRIBE_WORKFLOW— Get statusCANCEL_WORKFLOW— Request graceful stopSIGNAL_WORKFLOW— Send signal to running workflowGET_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 referencesstatus— active|completed|failed|archivedmetadata— 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:
-
Client provides JWT in Authorization header
-
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 } }) } -
Token passed to SynthesisClient:
let client = SynthesisClient::new(url, jwt_token); -
Client includes token in all requests:
client .post(url) .header("Authorization", format!("Bearer {}", self.jwt_token)) .send() .await -
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
-
Agent DB Persistence
- Save agents to PostgreSQL
agentstable - Instead of in-memory DefaultAgent struct
- Save agents to PostgreSQL
-
Workflow Link Storage
- Actually call
temporal_workflow_linksinsert - Currently stub in agent_handler.rs
- Actually call
-
Status Queries
- Implement GET /agents/{id}
- Call DESCRIBE_WORKFLOW to poll Temporal
-
Webhook Callbacks
- Temporal sends workflow events to webhook_url
- Memory service processes events (entity updates, etc.)
-
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).