From 9f15d11ed967feaafc157e0c4289895069d990c1 Mon Sep 17 00:00:00 2001 From: Raphael Simon Date: Sat, 14 Mar 2026 19:19:46 -0700 Subject: [PATCH] runtime: stop implicit SessionID search attribute injection Keep sessionful runs strict in the runtime while making Temporal search attributes caller-owned again. This avoids turning SessionID into an undeclared namespace schema requirement and keeps one-shot runs explicitly sessionless. --- docs/runtime.md | 10 +++++++--- runtime/agent/runtime/runtime.go | 4 ---- runtime/agent/runtime/runtime_test.go | 23 +++++++++++++++++++++-- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/docs/runtime.md b/docs/runtime.md index 0528e0e9..16054672 100644 --- a/docs/runtime.md +++ b/docs/runtime.md @@ -1400,7 +1400,7 @@ client.Run(ctx, "session-1", msgs, runtime.WithMetadata(map[string]any{"request_id": "abc"}), runtime.WithTaskQueue("custom-queue"), runtime.WithMemo(map[string]any{"workflow_name": "Chat"}), - runtime.WithSearchAttributes(map[string]any{"SessionID": "s1"}), + runtime.WithSearchAttributes(map[string]any{"tenant": "acme"}), runtime.WithTiming(runtime.Timing{ Budget: 2 * time.Minute, Plan: 30 * time.Second, @@ -1409,6 +1409,10 @@ client.Run(ctx, "session-1", msgs, ) ``` +Search attributes are passed through to the workflow engine as caller-owned +index metadata. The runtime does not mirror `SessionID` into engine search +attributes automatically. + `Timing.Plan` and `Timing.Tools` are semantic attempt budgets. They bound how long a healthy planner or tool attempt may run once execution starts. Queue-wait timeouts and heartbeat-based liveness detection are engine-specific concerns and @@ -1860,8 +1864,8 @@ var ErrRateLimited = errors.New("model: rate limited") 3. **Choose one streaming path.** Either use the decorated model client OR `planner.ConsumeStream`, never both. -4. **Set SessionID.** Every run requires a session ID for proper grouping and - memory association. +4. **Set SessionID for sessionful runs.** `Run` and `Start` require a session ID + for grouping and memory association. `OneShotRun` is explicitly sessionless. 5. **Trust the contracts.** Don't add defensive checks for values guaranteed by Goa validation or construction. Let violations fail fast. diff --git a/runtime/agent/runtime/runtime.go b/runtime/agent/runtime/runtime.go index 0e0d44d1..2d34f99b 100644 --- a/runtime/agent/runtime/runtime.go +++ b/runtime/agent/runtime/runtime.go @@ -1392,13 +1392,9 @@ func (r *Runtime) startRunOn(ctx context.Context, input *RunInput, workflowName, } } if requireSession { - if req.SearchAttributes == nil { - req.SearchAttributes = make(map[string]any, 1) - } if v, ok := req.SearchAttributes["SessionID"]; ok && v != input.SessionID { return nil, fmt.Errorf("workflow search attribute SessionID=%v does not match session id %q", v, input.SessionID) } - req.SearchAttributes["SessionID"] = input.SessionID now := time.Now().UTC() if err := r.SessionStore.UpsertRun(ctx, session.RunMeta{ AgentID: string(input.AgentID), diff --git a/runtime/agent/runtime/runtime_test.go b/runtime/agent/runtime/runtime_test.go index b61b14fd..f8a51d9e 100644 --- a/runtime/agent/runtime/runtime_test.go +++ b/runtime/agent/runtime/runtime_test.go @@ -103,6 +103,26 @@ func TestStartRunRequiresSessionID(t *testing.T) { require.NoError(t, err) } +func TestStartRunDoesNotInjectSessionSearchAttribute(t *testing.T) { + eng := &stubEngine{} + rt := &Runtime{ + Engine: eng, + logger: telemetry.NoopLogger{}, + metrics: telemetry.NoopMetrics{}, + tracer: telemetry.NoopTracer{}, + SessionStore: sessioninmem.New(), + agents: map[agent.Ident]AgentRegistration{ + "service.agent": {ID: "service.agent", Workflow: engine.WorkflowDefinition{Name: "service.workflow", TaskQueue: "q"}}, + }, + } + client := rt.MustClient(agent.Ident("service.agent")) + _, err := rt.CreateSession(context.Background(), "sess-1") + require.NoError(t, err) + _, err = client.Start(context.Background(), "sess-1", nil) + require.NoError(t, err) + require.Nil(t, eng.last.SearchAttributes) +} + func TestFinishWithoutToolCalls_UsesPlannerFinalToolResult(t *testing.T) { rt := &Runtime{ logger: telemetry.NoopLogger{}, @@ -524,8 +544,7 @@ func TestStartRunForwardsWorkflowOptions(t *testing.T) { require.Equal(t, in.RunID, eng.last.ID) require.Equal(t, in.WorkflowOptions.Memo, eng.last.Memo) require.Equal(t, map[string]any{ - "SessionID": in.SessionID, - "sa": "x", + "sa": "x", }, eng.last.SearchAttributes) require.Equal(t, 5, eng.last.RetryPolicy.MaxAttempts) require.Equal(t, 5*time.Second, eng.last.RetryPolicy.InitialInterval)