Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0999527
fix(sdk): resiliency/reliability/observability hardening across middl…
draxios Jun 23, 2026
b3fcbd1
fix(cli): resiliency/reliability/observability hardening (TUI, auth, …
draxios Jun 23, 2026
0c9901b
fix(daemon): harden the unattended runner (triggers, token, scheduler…
draxios Jun 23, 2026
bfc03f2
fix(sdk): activate no-op safety/feature middleware + harden serve
draxios Jun 23, 2026
11e589d
fix(cli): SSRF guards on web/agent fetch, sign TraceFile header, fix …
draxios Jun 23, 2026
63c04b5
fix(daemon): scheduler dispatch for event triggers + safe-by-default …
draxios Jun 23, 2026
2b28420
fix(sdk): data-integrity + concurrency hardening (lost edits, atomic …
draxios Jun 23, 2026
5b0a0dc
fix(cli): concurrency + lifecycle hardening (orchestrator, auto-commi…
draxios Jun 23, 2026
3e0ef8b
feat(sdk): deepagents 0.6.12 backend type foundation + dep floors
draxios Jul 11, 2026
836cdea
feat(sdk): complete deepagents backend rewrite (FileData v2, delete, …
draxios Jul 11, 2026
7687282
feat(sdk): middleware interop surface + fix two permission-bypass vulns
draxios Jul 11, 2026
674906a
feat(sdk): deepagents drop-in core API (SystemPromptConfig, exports, …
draxios Jul 12, 2026
a270707
chore(sdk): remove dead, drifted base_prompt.md
draxios Jul 12, 2026
38e7930
feat(sdk): ship built-in model profiles + bedrock caching + video read
draxios Jul 12, 2026
f5f8473
docs: record deepagents parity waves 1-3 as shipped
draxios Jul 12, 2026
70ffb43
feat(sdk): add GoalToolsMiddleware for persistent agent-visible goals
draxios Jul 12, 2026
70e4e50
fix(cli): escape untrusted markup on trust surfaces + headless HITL/c…
draxios Jul 12, 2026
6c4df82
feat(cli): managed ripgrep auto-install + ${VAR} MCP header interpola…
draxios Jul 12, 2026
9feaeb3
feat(cli): native reasoning-effort, ctrl+x external editor, /goal and…
draxios Jul 12, 2026
e602887
feat(cli): env-var registry + config manifest with real config command
draxios Jul 12, 2026
d5a9df0
fix(sdk): checkpointing works on fresh repos and never touches the us…
draxios Jul 12, 2026
c30d5ed
feat(cli): spec-compliant MCP OAuth (mcp SDK OAuthClientProvider)
draxios Jul 12, 2026
6000c2f
feat(sdk): add pluggable symlink-trust checker hook to SkillsMiddleware
draxios Jul 12, 2026
8271f4b
feat(cli): theme system, skill trust store, and UX polish
draxios Jul 12, 2026
13239ff
Merge remote-tracking branch 'origin/main' into chore/resiliency-hard…
draxios Jul 12, 2026
600e164
style(sdk): wrap two over-length lines flagged by ruff (checkpointing…
draxios Jul 12, 2026
0ae7be5
test(cli): allow real sockets in MCP OAuth loopback tests under CI so…
draxios Jul 12, 2026
055bd81
fix(sdk): don't follow a symlinked leaf on filesystem write/delete
draxios Jul 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
272 changes: 272 additions & 0 deletions PARITY.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions libs/bog-agents/bog_agents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
# deepagents compatibility surface (see bog_agents.deepagents)
"create_deep_agent": ("bog_agents.deepagents", "create_deep_agent"),
"create_sub_agent": ("bog_agents.middleware.subagents", "create_sub_agent"),
"SUBAGENT_RESPONSE_FORMAT_CONFIG_KEY": ("bog_agents.middleware.subagents", "SUBAGENT_RESPONSE_FORMAT_CONFIG_KEY"),
"SystemPromptConfig": ("bog_agents.graph", "SystemPromptConfig"),
"FsToolName": ("bog_agents.middleware.filesystem", "FsToolName"),
"FilesystemPermission": ("bog_agents.middleware.permissions", "FilesystemPermission"),
"FilesystemPermissionsMiddleware": ("bog_agents.middleware.permissions", "FilesystemPermissionsMiddleware"),
"RubricMiddleware": ("bog_agents.middleware.rubric", "RubricMiddleware"),
Expand Down
5 changes: 5 additions & 0 deletions libs/bog-agents/bog_agents/_api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Internal helpers for `bog_agents`.

Modules under this package are private. Their API is allowed to change between
minor releases without deprecation.
"""
131 changes: 131 additions & 0 deletions libs/bog-agents/bog_agents/_api/deprecation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""Adapter for `langchain_core`'s private deprecation helpers.

Centralizes the import surface so an upstream rename or move is a one-file
change.

Re-exports:
- `deprecated`: decorator for callables, classes, and properties.
- `warn_deprecated`: helper for parameter/value-level deprecations where the
callable itself isn't being deprecated. Wraps the upstream helper to
accept a `stacklevel` argument (the upstream version hardcodes
`stacklevel=4`, which mis-attributes warnings emitted directly from a
deprecated method body).
- `suppress_langchain_deprecation_warning`: context manager that silences
emissions from this module's helpers (use sparingly — it is type-wide).
- `LangChainDeprecationWarning`: warning class emitted by the helpers above
(subclass of `DeprecationWarning`).
"""

from __future__ import annotations

import warnings

from langchain_core._api.deprecation import (
LangChainDeprecationWarning,
deprecated,
suppress_langchain_deprecation_warning,
warn_deprecated as _lc_warn_deprecated,
)

__all__ = [
"LangChainDeprecationWarning",
"deprecated",
"reset_deprecation_dedupe",
"suppress_langchain_deprecation_warning",
"warn_deprecated",
]


def warn_deprecated(
since: str,
*,
message: str = "",
name: str = "",
alternative: str = "",
alternative_import: str = "",
pending: bool = False,
obj_type: str = "",
addendum: str = "",
removal: str = "",
package: str = "",
stacklevel: int = 2,
) -> None:
"""Emit a deprecation warning with caller-controlled stack attribution.

`langchain_core.warn_deprecated` formats a standard message but hardcodes
`stacklevel=4` in its internal `warnings.warn` call. That value targets a
decorator-wrapped frame layout; when called directly from a deprecated
method's body the warning is attributed one frame too high (above the
user's call site). This wrapper captures the formatted upstream warning
and re-emits it with an explicit `stacklevel`, so the warning points at
the user's call site.

Args:
since: Release at which this API became deprecated.
message: Override the default deprecation message. See upstream
`langchain_core.warn_deprecated` for supported format specifiers.
name: Name of the deprecated object.
alternative: Alternative API the user may use instead.
alternative_import: Alternative import path the user may use instead.
pending: If `True`, uses a `PendingDeprecationWarning` instead of a
`DeprecationWarning`. Cannot be combined with `removal`.
obj_type: Object type label (e.g., `"function"`, `"class"`).
addendum: Additional text appended to the final message.
removal: Expected removal version. Cannot be combined with `pending`.
package: Package name attribution for the deprecation message.
stacklevel: Frames above this call to attribute the warning to,
using the same convention as `warnings.warn` (`1` = this call,
`2` = the caller of the method body that invoked us, etc.).
"""
with warnings.catch_warnings(record=True) as captured:
warnings.simplefilter("always")
_lc_warn_deprecated(
since,
message=message,
name=name,
alternative=alternative,
alternative_import=alternative_import,
pending=pending,
obj_type=obj_type,
addendum=addendum,
removal=removal,
package=package,
)
if not captured:
return
record = captured[0]
warnings.warn(record.message, category=record.category, stacklevel=stacklevel + 1)


def reset_deprecation_dedupe(*targets: object) -> None:
"""Reset the `@deprecated` decorator's dedupe flag for testing.

The langchain_core `@deprecated` decorator emits each warning at most once
per process via a closure-bound `warned` flag. Tests that assert per-call
emission must reset that flag between cases — otherwise the assertions
become reorder-sensitive (notably under `pytest -n auto`).

Accepts decorated functions, methods, and `property` objects (in which
case the `fget` closure is reset). Targets without the expected `warned`
freevar are silently skipped, so passing non-decorated callables is safe.

Args:
*targets: Decorated callables (or properties wrapping them) to reset.
"""
for target in targets:
fn = target.fget if isinstance(target, property) else target
code = getattr(fn, "__code__", None)
closure = getattr(fn, "__closure__", None)
if code is None or closure is None:
continue
try:
index = code.co_freevars.index("warned")
except ValueError:
continue
cell = closure[index]
try:
current = cell.cell_contents
except ValueError: # empty cell
continue
if isinstance(current, bool):
cell.cell_contents = False
177 changes: 168 additions & 9 deletions libs/bog-agents/bog_agents/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import logging
import os
from collections.abc import Mapping
from typing import Any

from langchain.chat_models import init_chat_model
Expand All @@ -12,6 +13,32 @@
logger = logging.getLogger(__name__)


_PROVIDER_ALIASES: dict[str, str] = {
"azure_openai": "azure",
"mistralai": "mistral",
}
"""Known provider aliases between LangChain specs and LangSmith params.

LangChain `provider:model` specs and the `ls_provider` reported by
`_get_ls_params` use different provider names for some integrations.
Canonicalize only known aliases before comparing providers.
"""

_BEDROCK_PROVIDERS: frozenset[str] = frozenset({"amazon_bedrock", "anthropic_bedrock", "aws", "bedrock", "bedrock_converse"})
"""Normalized provider names that identify AWS Bedrock chat models."""

_BEDROCK_MODEL_CLASSES: frozenset[str] = frozenset({"ChatAnthropicBedrock", "ChatBedrock", "ChatBedrockConverse", "ChatBedrockNovaSonic"})
"""`langchain-aws` chat model class names that identify AWS Bedrock models."""

_BEDROCK_REGIONAL_PREFIXES: tuple[str, ...] = ("apac.", "amer.", "au.", "eu.", "global.", "jp.", "sa.", "us.", "us-gov.")
"""Regional inference profile prefixes stripped from Bedrock model identifiers.

Distinct from `_BEDROCK_PROFILE_PREFIXES` (used by the bare-id auto-resolver):
this wider set is used only to peel a regional prefix off a Bedrock model id
before checking for a cache-capable Nova identifier.
"""


# Long-output, long-thinking model turns can legitimately run for an hour
# or more, especially for /review-style tasks chained with long tool calls
# (builds, test suites, fan-out research) inside a single turn. The default
Expand Down Expand Up @@ -205,8 +232,6 @@ def _model_init_kwargs(model: str, timeout_secs: float | None) -> dict[str, Any]
Dict of kwargs to splat into `init_chat_model`.
"""
kwargs: dict[str, Any] = {}
if model.startswith("openai:"):
kwargs["use_responses_api"] = True
if timeout_secs is None:
return kwargs
if model.startswith(("bedrock:", "bedrock_converse:")):
Expand All @@ -218,6 +243,29 @@ def _model_init_kwargs(model: str, timeout_secs: float | None) -> dict[str, Any]
return kwargs


def _apply_openai_responses_default(model: str, kwargs: dict[str, Any]) -> None:
"""Default OpenAI specs to the Responses API unless a profile decided.

Historically `_model_init_kwargs` hardcoded `use_responses_api=True` for
every `openai:` spec, which landed in the caller-kwargs layer that wins
over `apply_provider_profile` — making a `ProviderProfile` override (e.g.
`ProviderProfile(init_kwargs={"use_responses_api": False})`) permanently
dead. The default now sits **beneath** the profile: it is only applied when
neither a registered profile nor the caller has already set the key, so a
profile can turn the Responses API off (or on). Behavior for OpenAI users
is unchanged when no profile is registered — the SDK still defaults them to
the Responses API.

Mutates `kwargs` in place.

Args:
model: Model spec like `provider:identifier`.
kwargs: Merged `init_chat_model` kwargs (post profile application).
"""
if model.startswith("openai:") and "use_responses_api" not in kwargs:
kwargs["use_responses_api"] = True


def resolve_model(model: str | BaseChatModel) -> BaseChatModel:
"""Resolve a model string to a `BaseChatModel`.

Expand Down Expand Up @@ -248,6 +296,7 @@ def resolve_model(model: str | BaseChatModel) -> BaseChatModel:
from bog_agents.profiles.provider.provider_profiles import apply_provider_profile

kwargs = apply_provider_profile(model, kwargs)
_apply_openai_responses_default(model, kwargs)
try:
return init_chat_model(model, **kwargs)
except TypeError as exc:
Expand Down Expand Up @@ -299,21 +348,114 @@ def get_model_provider(model: BaseChatModel) -> str | None:
"""
try:
params = model._get_ls_params()
except Exception: # noqa: BLE001 - best-effort; never fail model resolution over this
except (AttributeError, TypeError, NotImplementedError) as exc:
# A missing or raising `_get_ls_params` causes profile resolution to
# silently miss for this model. Log at INFO (not DEBUG) so custom
# integrations can debug "my profile isn't applying" without turning on
# DEBUG. Narrowed from a bare `except`: only the shapes a partial or
# unimplemented `_get_ls_params` actually raises are swallowed; any
# other error surfaces instead of being silently mapped to `None`.
logger.info(
"Could not extract provider from %s.%s via _get_ls_params: %s",
type(model).__module__,
type(model).__name__,
exc,
)
return None
if not isinstance(params, Mapping):
# A custom integration may return `None` (or another non-mapping)
# instead of raising. Treat that as "provider unavailable" rather than
# letting the subsequent `.get` raise `AttributeError`.
logger.info(
"Could not extract provider from %s.%s: _get_ls_params returned %s, not a mapping",
type(model).__module__,
type(model).__name__,
type(params).__name__,
)
return None
provider = params.get("ls_provider")
if isinstance(provider, str) and provider:
return provider
return None


def is_bedrock_model(model: str | BaseChatModel) -> bool:
"""Check whether a model targets AWS Bedrock.

For string specs, the provider half (before the first colon) is normalized
and checked against the known Bedrock provider names, and bare Nova
identifiers (which may omit a provider prefix) are recognised directly. For
instances, the provider reported by `get_model_provider` is checked first,
then the concrete `langchain-aws` chat model class name as a fallback.

Args:
model: Model spec in `provider:model` format, or a chat model instance.

Returns:
`True` if the model targets AWS Bedrock, otherwise `False`.
"""
if isinstance(model, str):
if _is_bedrock_nova_model_id(model):
return True
provider, separator, _ = model.partition(":")
return bool(separator) and _normalize_provider(provider) in _BEDROCK_PROVIDERS

provider = get_model_provider(model)
if provider is not None and _normalize_provider(provider) in _BEDROCK_PROVIDERS:
return True
return type(model).__name__ in _BEDROCK_MODEL_CLASSES


def _is_bedrock_nova_model_id(model: str) -> bool:
"""Check for a cache-capable Bedrock Nova model identifier.

Peels a single regional inference-profile prefix (e.g. `us.`) off the id,
then tests for the `amazon.nova-` family.

Args:
model: Model spec or bare model identifier.

Returns:
`True` when the identifier names a Bedrock Nova model.
"""
identifier = model
for prefix in _BEDROCK_REGIONAL_PREFIXES:
if identifier.startswith(prefix):
identifier = identifier.removeprefix(prefix)
break
return identifier.startswith("amazon.nova-")


def _normalize_provider(provider: str) -> str:
"""Canonicalize a provider name so equal providers compare equal.

Specs use the `provider:model` spelling (lowercase, underscore-separated,
e.g. `azure_openai`), while the `ls_provider` reported by `_get_ls_params`
may differ in case, use hyphens (`openai-codex`), or use an entirely
different name (`mistralai` vs `mistral`). Folding both sides through this
function before comparison keeps those spellings from reading as a mismatch.

Args:
provider: Raw provider name from either a spec or `ls_provider`.

Returns:
The canonical provider name.
"""
normalized = provider.lower().replace("-", "_")
return _PROVIDER_ALIASES.get(normalized, normalized)


def model_matches_spec(model: BaseChatModel, spec: str) -> bool:
"""Check whether a model instance already matches a string model spec.

Matching is performed in two ways: first by exact string equality between
`spec` and the model identifier, then by comparing only the model-name
portion of a `provider:model` spec against the identifier. For example,
`"openai:gpt-5"` matches a model with identifier `"gpt-5"`.
Bare specs (no colon) match by model identifier alone. Provider-prefixed
specs must match on **both** the model identifier and the provider: a spec
like `"openai:gpt-5"` no longer matches an Anthropic model that merely
happens to expose a `"gpt-5"` identifier. When the current model's provider
cannot be inspected (`_get_ls_params` unavailable), the check falls back to
identifier-only matching for backwards compatibility with custom models.
Provider comparison is normalized (see `_normalize_provider`), so case,
hyphen/underscore spelling, and known aliases do not read as a mismatch.

Assumes the `provider:model` convention (single colon separator).

Expand All @@ -330,8 +472,25 @@ def model_matches_spec(model: BaseChatModel, spec: str) -> bool:
if spec == current:
return True

_, separator, model_name = spec.partition(":")
return bool(separator) and model_name == current
provider, separator, model_name = spec.partition(":")
if not separator or model_name != current:
return False

current_provider = get_model_provider(model)
if current_provider is None:
# Provider could not be inspected, so the spec's provider cannot be
# confirmed. Fall back to the identifier-only match. Logged at DEBUG so
# a consumer skipping a model swap on the strength of this match (e.g.
# the runtime model override) is traceable when it surprises.
logger.debug(
"Matched spec %r on identifier alone; provider for %s.%s is uninspectable, so the spec's %r provider was not verified",
spec,
type(model).__module__,
type(model).__name__,
provider,
)
return True
return _normalize_provider(provider) == _normalize_provider(current_provider)


def _string_value(config: dict[str, Any], key: str) -> str | None:
Expand Down
Loading
Loading