Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
aae3aba
feat(adapters/langgraph): compile ManagerWorkers via subgraph composi…
May 28, 2026
be28d2f
refactor(adapters/langgraph): dedup ManagerWorkers converter helpers
May 30, 2026
959afa2
fix: remove output schema restriction for tools with requires_confirm…
spichen Apr 8, 2026
c1317cd
fix: narrow confirmation+multi-output check to Flow ToolNode
spichen May 13, 2026
37497d5
fix: extend raise_on_denial to RemoteTool/ClientTool and preserve mTL…
spichen May 29, 2026
9b68f82
Add generic LLM config support to TS SDK
spichen May 14, 2026
27fe855
fix(langgraph): thread middleware through ManagerWorkers conversion
May 30, 2026
38b4f07
feat(adapters/langgraph): hide ManagerWorkers delegation protocol fro…
May 30, 2026
2c2a399
fix(adapters/langgraph): stream ManagerWorkers worker events under th…
May 31, 2026
0242c9a
feat(adapters/langgraph): resolve single-string agent output from fin…
Jun 7, 2026
0571414
fix(adapters): preserve value types in RemoteTool JSON bodies
Jun 21, 2026
6a53db9
Merge pull request #12 from spichen/fix/remotetool-json-value-types
spichen Jun 21, 2026
a39e33e
fix(adapters/langgraph): answer every ManagerWorkers delegation in a …
Jun 22, 2026
e0b6d74
Merge pull request #13 from spichen/fix/managerworkers-multi-delegation
spichen Jun 22, 2026
fd1eede
fix(adapters/langgraph): strip delegate tool calls from ManagerWorker…
Jun 22, 2026
03222eb
Merge pull request #14 from spichen/fix/managerworkers-hide-delegatio…
spichen Jun 22, 2026
b6dd939
fix(adapters/langgraph): send sensitive_headers on remote MCP requests
Jun 23, 2026
5a17543
Merge pull request #15 from spichen/fix/mcp-merge-sensitive-headers
spichen Jun 23, 2026
4075db1
fix(tracing): replace get_running_tasks() with get_current_task() in …
spichen Jun 24, 2026
d9ce3d3
Merge pull request #16 from spichen/fix/async-trace-o1-task-check
spichen Jun 24, 2026
3c73175
feat(adapters/langgraph): run a ManagerWorkers as a flow step
Jun 29, 2026
b9c5c48
Merge pull request #17 from spichen/feat/manager-as-flow-step
spichen Jun 29, 2026
728e126
feat(adapters/langgraph): support ManagerWorkers as a Swarm member
Jun 30, 2026
285730b
Merge pull request #18 from spichen/feat/managerworkers-as-swarm-member
spichen Jun 30, 2026
aea75d5
feat(adapters/langgraph): run a Swarm as a flow step
Jun 30, 2026
05a5f83
Merge pull request #20 from spichen/feat/swarm-as-flow-step
spichen Jun 30, 2026
2199de6
fix(adapters/langgraph): tolerate nested schema titles in MCP tool sc…
Jul 3, 2026
08f4ebd
Merge pull request #21 from spichen/fix/mcp-nested-schema-titles
spichen Jul 3, 2026
5bfe0af
Revert "fix(adapters/langgraph): strip delegate tool calls from Manag…
Jul 4, 2026
899fb5a
Revert "feat(adapters/langgraph): hide ManagerWorkers delegation prot…
Jul 4, 2026
d530e27
Merge pull request #22 from spichen/revert/managerworkers-hide-delega…
spichen Jul 4, 2026
4010c41
fix(adapters/langgraph): answer delegation with error ToolMessage on …
Jul 4, 2026
65ed9e8
Merge pull request #23 from spichen/fix/worker-subgraph-error-tool-me…
spichen Jul 4, 2026
c6477b1
fix(adapters/langgraph): forward ManagerWorkers delegation task as Sy…
Jul 5, 2026
3017632
Merge pull request #24 from spichen/fix/worker-delegation-system-message
spichen Jul 5, 2026
8db8e63
fix(adapters/langgraph): mark ManagerWorkers delegation task instead …
Jul 5, 2026
2fd5fcf
Merge pull request #25 from spichen/fix/worker-delegation-humanmessage
spichen Jul 5, 2026
a5c3358
fix(langgraph): propagate contextvars into run_async_in_sync worker t…
Jul 15, 2026
86d16c0
fix(langgraph): clear inherited async-library marker in the worker th…
Jul 15, 2026
788a2fa
Merge pull request #28 from spichen/fix/mcp-tenant-context-propagation
spichen Jul 15, 2026
b5931a1
fix(langgraph): agent node output no longer shadowed by same-named input
spichen Aug 6, 2026
e44ea01
fix(langgraph): declare structured_response on multi-agent parent graphs
Aug 10, 2026
64b7cb5
fix(adapters): map bare object schemas to passthrough dicts
spichen Aug 11, 2026
c5e9c4d
fix(adapters): say what a remote tool returned when it isn't JSON
Aug 11, 2026
2edcad7
style: format with the pinned black
Aug 11, 2026
96ea923
style: add the missing copyright header
Aug 11, 2026
4c4312e
fix(langgraph): propagate interrupts from an AgentNode's inner agent
Aug 13, 2026
c83e113
fix(langgraph): offload sync remote tool coroutines
fashkl Aug 16, 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
26 changes: 23 additions & 3 deletions pyagentspec/src/pyagentspec/adapters/_tools_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
maybe_warn_about_unrestricted_templated_url,
validate_url_against_allow_list,
)
from pyagentspec.adapters._utils import render_nested_object_template, render_template
from pyagentspec.adapters._utils import (
render_nested_json_template,
render_nested_object_template,
render_template,
)
from pyagentspec.retrypolicy import RetryPolicy
from pyagentspec.tools.remotetool import RemoteTool as AgentSpecRemoteTool

Expand All @@ -38,7 +42,10 @@ def _create_remote_tool_func(remote_tool: AgentSpecRemoteTool) -> Callable[...,
)

def _remote_tool(**kwargs: Any) -> Any:
remote_tool_data = render_nested_object_template(remote_tool.data, kwargs)
# The body is JSON: preserve the type of whole-placeholder values so
# structured tool arguments (arrays/objects/numbers/None) survive instead
# of being stringified to a Python repr. URL/headers/query stay strings.
remote_tool_data = render_nested_json_template(remote_tool.data, kwargs)
remote_tool_headers = {
render_template(k, kwargs): render_nested_object_template(v, kwargs)
for k, v in remote_tool.headers.items()
Expand Down Expand Up @@ -87,7 +94,20 @@ def _remote_tool(**kwargs: Any) -> Any:
response = _request_with_retry(remote_tool.retry_policy, request_kwargs)
if remote_tool.retry_policy is not None and not response.is_success:
response.raise_for_status()
return response.json()
try:
return response.json()
except ValueError as exc:
# A body that isn't JSON is almost always an error page the status
# would have explained — but a tool with no retry policy doesn't
# check the status (a non-2xx JSON error body is handed to the agent
# to read), so the decode failure was the only thing surfaced:
# "Expecting value: line 1 column 1 (char 0)", with no clue that the
# backend answered 401 or served HTML.
raise ValueError(
f"{remote_tool.name} returned {response.status_code} "
f"{response.headers.get('content-type', 'no content-type')}, "
f"which is not JSON: {response.text[:200]!r}"
) from exc

return _remote_tool

Expand Down
60 changes: 59 additions & 1 deletion pyagentspec/src/pyagentspec/adapters/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,57 @@ def render_template(template: Any, inputs: Dict[str, Any]) -> str:
return _render_template_placeholders(template, inputs)


def _to_jsonable(value: Any) -> Any:
"""Normalize a value into JSON-compatible primitives/containers.

Pydantic models (e.g. the per-input models built from a tool's input schema)
are dumped to plain dicts so they serialize correctly into a JSON request
body; nested structures are converted recursively.
"""
if isinstance(value, BaseModel):
return value.model_dump(mode="json")
if isinstance(value, dict):
return {k: _to_jsonable(v) for k, v in value.items()}
if isinstance(value, (list, tuple, set)):
return [_to_jsonable(item) for item in value]
return value


def render_nested_json_template(object: Any, inputs: Dict[str, Any]) -> Any:
"""Render a template destined for a JSON request body.

Behaves like :func:`render_nested_object_template`, except a string *value*
that is exactly one placeholder (e.g. ``"{{members}}"``) is replaced by the
raw input value with its type preserved (list/dict/number/bool/None) — so
structured tool arguments survive as JSON instead of being stringified into a
Python ``repr`` (``"[membersItem(...)]"``) or ``"None"``. Strings with
surrounding text keep ordinary interpolation, and dict keys are always
rendered as strings.
"""
if isinstance(object, str):
return _render_json_leaf(object, inputs)
elif isinstance(object, bytes):
return render_nested_json_template(object.decode("utf-8", errors="replace"), inputs)
elif isinstance(object, dict):
return {
render_template(k, inputs): render_nested_json_template(v, inputs)
for k, v in object.items()
}
elif isinstance(object, list) or isinstance(object, set) or isinstance(object, tuple):
return object.__class__([render_nested_json_template(item, inputs) for item in object])
else:
return object


def _render_json_leaf(template: str, inputs: Dict[str, Any]) -> Any:
"""Whole-placeholder string -> raw (JSON-able) value; otherwise interpolate."""
stripped = template.strip()
matches = list(re.finditer(TEMPLATE_PLACEHOLDER_REGEXP, stripped))
if len(matches) == 1 and matches[0].group(0) == stripped and matches[0].group(1) in inputs:
return _to_jsonable(inputs[matches[0].group(1)])
return _render_template_placeholders(template, inputs)


def _render_template_placeholders(template: str, inputs: Dict[str, Any]) -> str:
"""Render placeholders found in the original template using the list of inputs."""
rendered_parts: List[str] = []
Expand Down Expand Up @@ -137,6 +188,14 @@ def _build_type_from_schema(
return List[item_type] # type: ignore
# objects
if t == "object" or ("properties" in schema or "required" in schema):
props = schema.get("properties", {}) or {}
# An object schema with no declared properties accepts any object
# (JSON Schema semantics). Building an empty create_model() here would
# silently strip every key on validation (pydantic defaults to
# extra="ignore"), so the tool receives {} instead of the LLM's
# arguments. Map it to a passthrough dict instead.
if not props and schema.get("additionalProperties") is not False:
return Dict[str, Any]
# Create or reuse a Pydantic model for this object schema
model_name = schema.get("title") or name
unique_name = model_name
Expand All @@ -145,7 +204,6 @@ def _build_type_from_schema(
suffix += 1
unique_name = f"{model_name}_{suffix}"

props = schema.get("properties", {}) or {}
required = set(schema.get("required", []))

fields: Dict[str, Tuple[Any, Any]] = {}
Expand Down
Loading