2026-09-05 00:37:58 -07:00
# 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>`
```rust
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`
```rust
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.)
```sql
INSERT INTO temporal_workflow_links
( workflow_id , run_id , status , created_at )
VALUES
( 'agent-init-agent1' , 'abc123' , 'active' , NOW ());
```
2026-09-05 00:48:12 -07:00
## Workflow & Activity Pattern
**Key Insight** : Memory service orchestrates via REST, Temporal activities do the actual LLM work + persistence.
```
Memory Handler Temporal Workflow LLM API
|
v
Extract JWT token
|
v
SynthesisClient.execute_workflow(
{"action": "START_WORKFLOW", ...}
) ─────────────────────────────→ ReasoningWorkflow
|
v
InferenceActivity
|
v (LLM call)
api.riotpiao.com/v1/chat/completions
|
v (with JWT)
LLM Response
|
v (persist)
memory_entity + memory_edge
|
v (activity completes)
Workflow completes
|
Poll DESCRIBE_WORKFLOW ←─────────── Return result
|
v
Memory handler receives result
|
v
Return to client
```
2026-09-05 00:37:58 -07:00
## Workflow Lifecycle
### 1. Register Agent
```
POST /agents
2026-09-05 00:48:12 -07:00
→ Extract JWT from Authorization header
→ SynthesisClient.execute_workflow({"action": "START_WORKFLOW", ...})
└─ Gateway: POST /workflow + JWT header
└─ Temporal: Start AgentInitialization workflow
└─ Activity: Persist to temporal_workflow_links table
2026-09-05 00:37:58 -07:00
→ Store workflow_id, run_id
2026-09-05 00:48:12 -07:00
→ Return AgentResponse to client
2026-09-05 00:37:58 -07:00
```
2026-09-05 00:48:12 -07:00
### 2. Reasoning Workflow
```
POST /memory/synthesis
→ JWT required (Bearer token)
→ SynthesisClient.execute_workflow(ReasoningWorkflow)
└─ Gateway: POST /workflow + JWT
└─ Temporal: Start ReasoningWorkflow
└─ Activity: InferenceActivity
├─ Call: POST api.riotpiao.com/v1/chat/completions (with JWT)
├─ Parse LLM response
└─ Persist result to memory_entity/memory_edge (temporal_workflow_links tracks link)
→ Poll DESCRIBE_WORKFLOW (retry up to 30× , 100ms interval)
→ Wait for status COMPLETED or FAILED
→ Return ReasoningResult to client
```
### 3. Durability & Checkpointing
```
Activity Crash during LLM call
→ Temporal replays workflow from last checkpoint
→ Activity re-executes (idempotent via idempotency_key)
→ Result persisted to DB (already done from retry)
→ No data loss
```
### 4. Monitor Execution
2026-09-05 00:37:58 -07:00
```
GET /agents/{id}
2026-09-05 00:48:12 -07:00
→ Extract JWT
→ SynthesisClient.execute_workflow({"action": "DESCRIBE_WORKFLOW", ...})
→ Return execution status (RUNNING|COMPLETED|FAILED)
2026-09-05 00:37:58 -07:00
```
2026-09-05 00:48:12 -07:00
### 5. Cleanup
2026-09-05 00:37:58 -07:00
```
DELETE /agents/{id}
2026-09-05 00:48:12 -07:00
→ JWT required
2026-09-05 00:37:58 -07:00
→ 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:
```rust
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:
` ``rust
let client = SynthesisClient::new(url, jwt_token);
` ``
4. Client includes token in all requests:
` ``rust
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):
` ``yaml
# 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:
` ``rust
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
` ``json
{
"success": false,
"error": "WORKFLOW_NOT_FOUND",
"message": "workflow_id 'agent-init-xyz' not found in namespace 'poimen'"
}
` ``
### Temporal Unreachable
` ``json
{
"success": false,
"error": "TEMPORAL_UNAVAILABLE",
"message": "Temporal server connection not available"
}
` ``
### Invalid JWT
` ``json
{
"success": false,
"error": "UNAUTHORIZED",
"message": "Invalid or expired JWT token"
}
` ``
## Testing
### Mock Workflow Response
` ``rust
#[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).