openbox-langgraph-sdk-python 0.2.0__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,6 +17,8 @@ Example:
17
17
  ... )
18
18
  """
19
19
 
20
+ __version__ = "1.0.0"
21
+
20
22
  from openbox_langgraph.client import GovernanceClient, build_auth_headers
21
23
  from openbox_langgraph.config import (
22
24
  GovernanceConfig,
@@ -115,6 +117,7 @@ __all__ = [
115
117
  "WorkflowEventType",
116
118
  "WorkflowSpanBuffer",
117
119
  "WorkflowSpanProcessor",
120
+ "__version__",
118
121
  "build_agent_identity_canonical_request",
119
122
  "build_auth_headers",
120
123
  "create_agent_identity_headers",
@@ -0,0 +1,126 @@
1
+ """Dual-write ActivityContext registration + per-turn cleanup for the opt-in
2
+ base-SDK core runtime.
3
+
4
+ `langgraph_handler.py` already registers trace/activity correlation with the
5
+ legacy `WorkflowSpanProcessor` at every tool/LLM start and clears it at
6
+ completion — that dual-write's counterpart lands here so the (opt-in, later
7
+ phase) core hook runtime can resolve the SAME activity via
8
+ `TraceContextRegistry` without the legacy processor's behavior changing by one
9
+ line. This module is INERT unless a handler has an actual core runtime — see
10
+ `should_dual_write`.
11
+
12
+ Trace-only, never a ContextVar bind: LangGraph tool/LLM execution runs inside
13
+ a spawned `asyncio.Task` (or, for sync tools, a `run_in_executor` worker
14
+ thread) that already snapshot its ContextVar chain before this module's
15
+ registration call can run — a bind here can never reach that code. See
16
+ `core_runtime.py`'s module docstring and `tests/test_contextvars_propagation.py`
17
+ for the empirical proof.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from typing import Any
23
+
24
+ from openbox_core.contracts.context import ActivityContext
25
+ from openbox_core.runtime import OpenBoxRuntime
26
+
27
+ from openbox_langgraph.config import GovernanceConfig
28
+ from openbox_langgraph.core_runtime import get_trace_registry
29
+
30
+ __all__ = [
31
+ "build_activity_context",
32
+ "register_activity",
33
+ "should_dual_write",
34
+ "unregister_activity",
35
+ ]
36
+
37
+
38
+ def should_dual_write(core_runtime: OpenBoxRuntime | None) -> bool:
39
+ """True only when the handler owns a real core runtime.
40
+
41
+ Handlers built with an injected `client` (e.g. a test double, or a
42
+ subclass overriding `evaluate_event`) have `core_runtime is None` by
43
+ construction (`langgraph_handler.py.__init__`) — this dual-write is
44
+ legacy-only for them, exactly like the gate-routing it mirrors.
45
+ """
46
+ return core_runtime is not None
47
+
48
+
49
+ def build_activity_context(
50
+ *,
51
+ config: GovernanceConfig,
52
+ workflow_id: str,
53
+ run_id: str,
54
+ activity_id: str,
55
+ activity_type: str,
56
+ activity_input: Any = None,
57
+ langgraph_node: str | None = None,
58
+ langgraph_step: int | None = None,
59
+ tool_type: str | None = None,
60
+ tool_name: str | None = None,
61
+ tool_call_id: str | None = None,
62
+ subagent_name: str | None = None,
63
+ parent_ids: list[str] | None = None,
64
+ ) -> ActivityContext:
65
+ """Map one LangGraph activity boundary onto the base SDK's `ActivityContext`.
66
+
67
+ `workflow_id`/`run_id` are the PER-TURN ids `langgraph_handler.py` mints in
68
+ `ainvoke`/`astream_governed`/`astream`/`astream_events` (NOT LangGraph's own
69
+ per-node `run_id`, which becomes `activity_id` instead) — matching the
70
+ legacy governance event's own workflow_id/run_id fields exactly, so a hook
71
+ resolving this context reports the same turn a dashboard operator sees
72
+ from the legacy path.
73
+ """
74
+ metadata: dict[str, Any] = {}
75
+ if langgraph_node is not None:
76
+ metadata["node"] = langgraph_node
77
+ if langgraph_step is not None:
78
+ metadata["step"] = langgraph_step
79
+ if tool_type is not None:
80
+ metadata["tool_type"] = tool_type
81
+ if tool_name is not None:
82
+ metadata["tool_name"] = tool_name
83
+ if tool_call_id is not None:
84
+ metadata["tool_call_id"] = tool_call_id
85
+ if subagent_name is not None:
86
+ metadata["subagent_name"] = subagent_name
87
+ if parent_ids:
88
+ metadata["parent_ids"] = list(parent_ids)
89
+
90
+ return ActivityContext(
91
+ workflow_id=workflow_id,
92
+ run_id=run_id,
93
+ workflow_type=config.agent_name or "LangGraphRun",
94
+ task_queue=config.task_queue or "langgraph",
95
+ activity_id=activity_id,
96
+ activity_type=activity_type,
97
+ activity_input=activity_input,
98
+ agent_name=config.agent_name,
99
+ session_id=config.session_id,
100
+ multi_agent_session_id=config.multi_agent_session_id,
101
+ metadata=metadata,
102
+ )
103
+
104
+
105
+ def register_activity(
106
+ core_runtime: OpenBoxRuntime | None,
107
+ trace_id: int,
108
+ ctx: ActivityContext,
109
+ ) -> None:
110
+ """Trace-only dual-write into the runtime's private `TraceContextRegistry`.
111
+
112
+ No-op when `core_runtime` is `None` (injected-client handlers) — see
113
+ `should_dual_write`. Skips a zero/falsy `trace_id` the same way the legacy
114
+ `if trace_id:` guard at the call site already does, so a degraded OTel
115
+ span (no active provider) never registers a bogus all-zero correlation.
116
+ """
117
+ if core_runtime is None or not trace_id:
118
+ return
119
+ get_trace_registry(core_runtime).register(trace_id, ctx)
120
+
121
+
122
+ def unregister_activity(core_runtime: OpenBoxRuntime | None, trace_id: int | None) -> None:
123
+ """Counterpart cleanup for `register_activity` — activity completion."""
124
+ if core_runtime is None or not trace_id:
125
+ return
126
+ get_trace_registry(core_runtime).unregister(trace_id)
@@ -5,11 +5,17 @@ from __future__ import annotations
5
5
  import json
6
6
  import os
7
7
  from dataclasses import dataclass
8
- from datetime import UTC
9
- from typing import Any
8
+ from typing import TYPE_CHECKING, Any
10
9
 
11
10
  import httpx
12
-
11
+ from openbox_core.client import check_expiration
12
+ from openbox_core.contracts.results import EvaluationResult
13
+ from openbox_core.contracts.results import Verdict as _CoreVerdict
14
+ from openbox_core.errors import ContractError as _CoreContractError
15
+ from openbox_core.errors import GovernanceAPIError as _CoreGovernanceAPIError
16
+ from openbox_core.errors import OpenBoxNetworkError as _CoreOpenBoxNetworkError
17
+
18
+ from openbox_langgraph.core_events import to_envelope
13
19
  from openbox_langgraph.errors import OpenBoxConfigError, OpenBoxNetworkError
14
20
  from openbox_langgraph.identity import (
15
21
  AgentIdentityConfig,
@@ -25,7 +31,107 @@ from openbox_langgraph.types import (
25
31
  to_server_event_type,
26
32
  )
27
33
 
28
- _SDK_VERSION = "0.2.0"
34
+ if TYPE_CHECKING:
35
+ from openbox_core.gate import GovernanceGate
36
+
37
+ _SDK_PACKAGE_VERSION = "1.0.0"
38
+ _SDK_IDENTIFIER = f"openbox-langgraph-python-v{_SDK_PACKAGE_VERSION}"
39
+
40
+
41
+ def _network_fallback_result(on_api_error: str, msg: str) -> GovernanceVerdictResponse | None:
42
+ """Apply the `on_api_error` policy to a NETWORK/transport failure.
43
+
44
+ This is the ONLY place a `GovernanceClient` verdict call may return
45
+ `None` — it corresponds exactly to a client-synthesized fallback
46
+ (`EvaluationResult.fallback_allow`): `verdict=ALLOW`, `fallback_used=True`,
47
+ and an EMPTY `raw` dict (nothing was ever parsed from a real body).
48
+
49
+ A response BODY that happens to be `{"verdict": "block",
50
+ "fallback_used": true}` never reaches this function — it is parsed by
51
+ `_verdict_from_response_data` instead, whose `raw` is always the
52
+ non-empty parsed dict, so the None-collapse below can never fire for it
53
+ and the BLOCK is enforced normally.
54
+
55
+ `msg` is the caller's fully-formatted message (kept caller-side so the
56
+ two distinct failure messages — "Governance API error: HTTP {status}"
57
+ for a bad response, "Governance API unreachable: {e}" for a raised
58
+ exception — stay exactly as they were before this translation layer).
59
+ """
60
+ if on_api_error == "fail_closed":
61
+ raise OpenBoxNetworkError(msg)
62
+ result = EvaluationResult.fallback_allow(msg)
63
+ return _collapse_client_synthesized_fallback(result)
64
+
65
+
66
+ def _verdict_from_response_data(data: dict[str, Any]) -> GovernanceVerdictResponse:
67
+ """Parse a real HTTP response body into a `GovernanceVerdictResponse`.
68
+
69
+ Routed through the base SDK's `EvaluationResult.from_dict` so `raw` is
70
+ always the full parsed body — the non-empty `raw` is exactly what keeps
71
+ `_collapse_client_synthesized_fallback` from ever mistaking a real
72
+ (even oddly-shaped) Core response for a client-side fallback.
73
+ """
74
+ return GovernanceVerdictResponse.from_result(EvaluationResult.from_dict(data))
75
+
76
+
77
+ def _collapse_client_synthesized_fallback(
78
+ result: EvaluationResult,
79
+ ) -> GovernanceVerdictResponse | None:
80
+ """Return `None` ONLY for a client-synthesized fail-open fallback.
81
+
82
+ The three-part discriminator matches `EvaluationResult.fallback_allow`
83
+ exactly and nothing else: `fallback_used=True` AND `verdict is ALLOW` AND
84
+ `raw` is empty (no real body was ever parsed). A response body that
85
+ happens to carry `fallback_used: true` alongside a blocking verdict, or
86
+ alongside ALLOW but WITH a real (non-empty) body, is a real Core response
87
+ and must be returned as a `GovernanceVerdictResponse` so callers enforce
88
+ it — never silently collapsed to `None`/implicit-ALLOW.
89
+ """
90
+ if result.fallback_used and result.verdict is _CoreVerdict.ALLOW and not result.raw:
91
+ return None
92
+ return GovernanceVerdictResponse.from_result(result)
93
+
94
+
95
+ async def _gate_evaluate(
96
+ gate: GovernanceGate, event: LangChainGovernanceEvent, on_api_error: str
97
+ ) -> GovernanceVerdictResponse | None:
98
+ """Evaluate one lifecycle event through the base SDK's strict gate.
99
+
100
+ The single translation seam between `gate.aevaluate`'s base-SDK contract
101
+ (`EvaluationResult`, `openbox_core` exceptions) and this SDK's own
102
+ (`GovernanceVerdictResponse | None`, `openbox_langgraph.errors`) — every
103
+ gate-routed call site in `evaluate_event` goes through this function so
104
+ the translation is defined exactly once. Outcome policy (mirrors the legacy
105
+ httpx path so the wired transport is behaviourally interchangeable):
106
+
107
+ - `EvaluationResult.fallback_allow()` (client-synthesized fail-open on a
108
+ NETWORK error): collapsed to `None` via the same discriminator used for
109
+ the legacy path, so the pre-screen-`None` -> callback re-evaluation ->
110
+ PII-redaction flow keeps firing regardless of which transport produced it.
111
+ - `ContractError` (a malformed envelope — a bug in THIS SDK's own
112
+ event->envelope mapping, raised pre-network by the strict gate, never
113
+ from a Core response): ALWAYS a fail-open telemetry-drop (`None`),
114
+ independent of `on_api_error`. Enforcing fail_closed here would let an
115
+ SDK-side mapping defect block a user's graph for a reason their OWN policy
116
+ never produced — strictly worse than dropping one governance event.
117
+ - `GovernanceAPIError` / `OpenBoxNetworkError` (network-shaped failure) ->
118
+ this SDK's `OpenBoxNetworkError`, same public exception the legacy path
119
+ raises under fail_closed.
120
+ - Any OTHER exception (e.g. a malformed Core 200 body the base parser
121
+ cannot decode) is a transport-shaped fault, NOT a governance verdict:
122
+ routed through `_network_fallback_result` so fail_open returns `None`
123
+ (never crash the graph on a Core hiccup) and fail_closed raises
124
+ `OpenBoxNetworkError` — matching the legacy httpx catch-all exactly.
125
+ """
126
+ try:
127
+ result = await gate.aevaluate(to_envelope(event))
128
+ except _CoreContractError:
129
+ return None
130
+ except (_CoreGovernanceAPIError, _CoreOpenBoxNetworkError) as e:
131
+ raise OpenBoxNetworkError(str(e)) from e
132
+ except Exception as e:
133
+ return _network_fallback_result(on_api_error, f"Governance gate error: {e}")
134
+ return _collapse_client_synthesized_fallback(result)
29
135
 
30
136
 
31
137
  def build_auth_headers(
@@ -38,13 +144,13 @@ def build_auth_headers(
38
144
  ) -> dict[str, str]:
39
145
  """Build standard auth headers for governance API calls.
40
146
 
41
- Single source of truth — used by GovernanceClient and hook_governance.
147
+ Single source of truth for the SDK's outbound governance requests.
42
148
  """
43
149
  headers = {
44
150
  "Authorization": f"Bearer {api_key}",
45
151
  "Content-Type": "application/json",
46
- "User-Agent": f"OpenBox-LangGraph-SDK/{_SDK_VERSION}",
47
- "X-OpenBox-SDK-Version": _SDK_VERSION,
152
+ "User-Agent": f"OpenBox-LangGraph-SDK/{_SDK_IDENTIFIER}",
153
+ "X-OpenBox-SDK-Version": _SDK_IDENTIFIER,
48
154
  }
49
155
  if agent_identity:
50
156
  if method is None or pathname is None:
@@ -87,6 +193,7 @@ class GovernanceClient:
87
193
  on_api_error: str = "fail_open",
88
194
  agent_did: str | None = None,
89
195
  agent_private_key: str | None = None,
196
+ gate: GovernanceGate | None = None,
90
197
  ) -> None:
91
198
  self._api_url = api_url.rstrip("/")
92
199
  self._api_key = api_key
@@ -98,9 +205,19 @@ class GovernanceClient:
98
205
  did=agent_did,
99
206
  private_key=agent_private_key,
100
207
  )
208
+ # Optional base-SDK gate. When wired (by the handler, from a core
209
+ # runtime built off the SAME api_url/api_key/timeout/on_api_error),
210
+ # `evaluate_event`'s ASYNC path routes lifecycle events through it
211
+ # instead of this client's own httpx transport — see `evaluate_event`.
212
+ # `None` (the default) preserves the exact legacy transport/serialization
213
+ # for every existing caller that constructs a bare `GovernanceClient()`.
214
+ # `evaluate_event_sync` (sync middleware hooks) is unaffected either way.
215
+ self._gate = gate
101
216
  # Deduplication: prevent sending the same (activity_id, event_type) twice
102
217
  # within the same workflow run. Keyed by (workflow_id, run_id) so it resets
103
- # automatically on each new ainvoke() call.
218
+ # automatically on each new ainvoke() call. Shared by every evaluate_event
219
+ # call site regardless of which transport (gate or legacy httpx) is active
220
+ # for a given call — dedup is a client-level concern, not a transport one.
104
221
  self._dedup_run: tuple[str, str] | None = None
105
222
  self._dedup_sent: set[tuple[str, str]] = set()
106
223
 
@@ -186,13 +303,23 @@ class GovernanceClient:
186
303
  """Send a governance event to OpenBox Core and return the verdict.
187
304
 
188
305
  Returns `None` on network failure when `on_api_error` is `fail_open`.
189
- Silently drops duplicate (activity_id, event_type) pairs within the same run.
306
+ Silently drops duplicate (activity_id, event_type) pairs within the same run
307
+ — this de-dup pre-check runs BEFORE either transport below, so it applies
308
+ identically whether a `gate` is wired or not.
309
+
310
+ When a `gate` was supplied at construction (see `__init__`), the event is
311
+ routed through the base SDK's `EventEnvelope` + `GovernanceGate.aevaluate`
312
+ instead of this client's own httpx transport — see `_gate_evaluate`.
313
+ Overriding `evaluate_event` in a subclass (e.g. the golden-fixture
314
+ harness's `RecordingGovernanceClient`) still fully intercepts either way,
315
+ since the branch lives inside THIS method, never at a call site.
190
316
 
191
317
  Args:
192
318
  event: The governance event payload to evaluate.
193
319
 
194
320
  Raises:
195
- OpenBoxNetworkError: On network failure when `on_api_error` is `fail_closed`.
321
+ OpenBoxNetworkError: On network failure when `on_api_error` is `fail_closed`
322
+ (from either transport).
196
323
  """
197
324
  server_event_type = to_server_event_type(event.event_type)
198
325
  if event.activity_id and self._is_duplicate(
@@ -205,18 +332,22 @@ class GovernanceClient:
205
332
  )
206
333
  return None
207
334
 
208
- payload = event.to_dict()
209
- payload["event_type"] = server_event_type
210
- payload["task_queue"] = event.task_queue or "langgraph"
211
- payload["source"] = "workflow-telemetry"
212
-
213
335
  if os.environ.get("OPENBOX_DEBUG") == "1":
214
336
  import json
215
337
 
216
338
  print(
217
- f"[OpenBox Debug] governance request: {json.dumps(payload, indent=2, default=str)}"
339
+ f"[OpenBox Debug] governance request: "
340
+ f"{json.dumps(event.to_dict(), indent=2, default=str)}"
218
341
  )
219
342
 
343
+ if self._gate is not None:
344
+ return await _gate_evaluate(self._gate, event, self._on_api_error)
345
+
346
+ payload = event.to_dict()
347
+ payload["event_type"] = server_event_type
348
+ payload["task_queue"] = event.task_queue or "langgraph"
349
+ payload["source"] = "workflow-telemetry"
350
+
220
351
  try:
221
352
  client = self._get_client()
222
353
  body = _json_body(payload)
@@ -231,21 +362,19 @@ class GovernanceClient:
231
362
  )
232
363
 
233
364
  if not response.is_success:
234
- if self._on_api_error == "fail_closed":
235
- msg = f"Governance API error: HTTP {response.status_code}"
236
- raise OpenBoxNetworkError(msg)
237
- return None
365
+ return _network_fallback_result(
366
+ self._on_api_error, f"Governance API error: HTTP {response.status_code}"
367
+ )
238
368
 
239
369
  data = response.json()
240
- return GovernanceVerdictResponse.from_dict(data)
370
+ return _verdict_from_response_data(data)
241
371
 
242
372
  except OpenBoxNetworkError:
243
373
  raise
244
374
  except Exception as e:
245
- if self._on_api_error == "fail_closed":
246
- msg = f"Governance API unreachable: {e}"
247
- raise OpenBoxNetworkError(msg) from e
248
- return None
375
+ return _network_fallback_result(
376
+ self._on_api_error, f"Governance API unreachable: {e}"
377
+ )
249
378
 
250
379
  def evaluate_event_sync(
251
380
  self, event: LangChainGovernanceEvent
@@ -288,21 +417,19 @@ class GovernanceClient:
288
417
  )
289
418
 
290
419
  if not response.is_success:
291
- if self._on_api_error == "fail_closed":
292
- msg = f"Governance API error: HTTP {response.status_code}"
293
- raise OpenBoxNetworkError(msg)
294
- return None
420
+ return _network_fallback_result(
421
+ self._on_api_error, f"Governance API error: HTTP {response.status_code}"
422
+ )
295
423
 
296
424
  data = response.json()
297
- return GovernanceVerdictResponse.from_dict(data)
425
+ return _verdict_from_response_data(data)
298
426
 
299
427
  except OpenBoxNetworkError:
300
428
  raise
301
429
  except Exception as e:
302
- if self._on_api_error == "fail_closed":
303
- msg = f"Governance API unreachable: {e}"
304
- raise OpenBoxNetworkError(msg) from e
305
- return None
430
+ return _network_fallback_result(
431
+ self._on_api_error, f"Governance API unreachable: {e}"
432
+ )
306
433
 
307
434
  async def poll_approval(self, params: ApprovalPollParams) -> ApprovalResponse | None:
308
435
  """Poll for HITL approval status.
@@ -335,19 +462,15 @@ class GovernanceClient:
335
462
  return None
336
463
 
337
464
  data = response.json()
338
- parsed = parse_approval_response(data)
339
-
340
- # SDK-side expiration check
341
- if parsed.approval_expiration_time and not parsed.expired:
342
- from datetime import datetime
343
-
344
- expiry = datetime.fromisoformat(
345
- parsed.approval_expiration_time.replace("Z", "+00:00")
346
- )
347
- if expiry < datetime.now(tz=UTC):
348
- parsed.expired = True
349
-
350
- return parsed
465
+ # SDK-side expiration check — run on the raw dict BEFORE parsing
466
+ # (matches openbox_core.client.check_expiration's own call order:
467
+ # check_expiration(data) then ApprovalResult.from_dict(data)).
468
+ # Handles ISO 'Z', ISO offset, and space-separated DB timestamp
469
+ # formats; a malformed timestamp is logged and left un-flagged
470
+ # rather than raised, so one bad timestamp string degrades to
471
+ # "expiration not confirmed" instead of aborting the whole poll.
472
+ check_expiration(data)
473
+ return parse_approval_response(data)
351
474
 
352
475
  except Exception:
353
476
  return None
@@ -80,9 +80,37 @@ class GovernanceConfig:
80
80
  skip_tool_types: set[str] = field(default_factory=set)
81
81
  hitl: HITLConfig = field(default_factory=HITLConfig)
82
82
  session_id: str | None = None
83
+ multi_agent_session_id: str | None = None
84
+ """Optional multi-agent session correlation id, distinct from `session_id`.
85
+
86
+ Threaded onto the base SDK's `ActivityContext.multi_agent_session_id` at
87
+ activity-boundary dual-write registration — never mixed into `session_id`,
88
+ which stays the single-agent chat-session identifier the legacy governance
89
+ events already carry."""
83
90
  agent_name: str | None = None
84
91
  task_queue: str = "langgraph"
85
92
  use_native_interrupt: bool = False
93
+ use_core_instrumentation: bool = True
94
+ """Route hook governance through the shared ``openbox_core`` base
95
+ instrumentation — the only hook runtime. Default ``True``. Setting it
96
+ ``False`` fails fast (``OpenBoxConfigError``): legacy in-repo hook
97
+ governance has been removed, so there is nothing to fall back to."""
98
+ strict_activity_context: bool = False
99
+ """Fail loudly when a tool cannot be bound to a proven ActivityContext.
100
+
101
+ A tool run is bindable only inside a governed turn — the handler supplies
102
+ the turn's ``workflow_id``/``run_id`` down each tool's ``RunnableConfig``.
103
+ When that turn identity is absent (e.g. a tool driven outside the handler),
104
+ the ToolNode binder cannot prove which activity a hook span belongs to.
105
+
106
+ Default ``False``: log a warning once per such invocation and execute the
107
+ tool UNBOUND (its hook spans stay intentionally unattached — never guessed).
108
+ ``True`` (a test/debug aid): raise ``OpenBoxConfigError`` BEFORE the tool
109
+ body runs, so the tool never executes unbound. Note the raise is thrown from
110
+ inside the ``ToolNode`` call, so a ``ToolNode`` with the default
111
+ ``handle_tool_errors=True`` converts it into an error ``ToolMessage``
112
+ (loud and visible, tool still not run) rather than propagating; build the
113
+ ``ToolNode`` with ``handle_tool_errors=False`` to make it hard-fail the run."""
86
114
  root_node_names: set[str] = field(default_factory=set)
87
115
  tool_type_map: dict[str, str] = field(default_factory=dict)
88
116
  """Optional mapping of tool name → tool_type for execution tree classification.
@@ -142,9 +170,12 @@ def merge_config(partial: dict[str, Any] | None = None) -> GovernanceConfig:
142
170
  skip_tool_types=_to_set(partial.get("skip_tool_types")),
143
171
  hitl=hitl,
144
172
  session_id=partial.get("session_id"),
173
+ multi_agent_session_id=partial.get("multi_agent_session_id"),
145
174
  agent_name=partial.get("agent_name"),
146
175
  task_queue=partial.get("task_queue", "langgraph"),
147
176
  use_native_interrupt=partial.get("use_native_interrupt", False),
177
+ use_core_instrumentation=partial.get("use_core_instrumentation", True),
178
+ strict_activity_context=partial.get("strict_activity_context", False),
148
179
  root_node_names=_to_set(partial.get("root_node_names")),
149
180
  tool_type_map=tool_type_map,
150
181
  )