Skip to content

Implement new SDK APIs - #10

Merged
bettercallsaulj merged 29 commits into
mainfrom
iml_new_apis
Jul 31, 2026
Merged

Implement new SDK APIs#10
bettercallsaulj merged 29 commits into
mainfrom
iml_new_apis

Conversation

@bettercallsaulj

@bettercallsaulj bettercallsaulj commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add Python bindings for the new GopherAgent.create_with_* factory APIs:
    create_with_url, create_with_server_id, create_with_server_name,
    create_with_gateway_id, create_with_gateway_name, and
    create_with_api_key.
  • Add contract coverage for the optional native FFI symbols, including a clear
    runtime error when the bundled libgopher-orch predates the routing factory
    symbols.
  • Add runnable API examples and keep their PyPI/venv setup centralized in
    examples/api/_run_common.sh.
  • Harden release note/version helpers so stacked tags are resolved
    deterministically and tag-name/version mismatches are reported.

Native submodule bump

The third_party/gopher-orch pin moves from ff84c19 to bf4b46a. The Python
factory bindings depend on native C FFI symbols introduced inside that range,
most directly:

  • 928cdce8 declares the five new ReActAgent factory methods.
  • a3834e6e implements the server/gateway id/name routing factories.
  • 5b85c2e7 implements createByUrl.
  • 63d5ea0d exposes the five new agent factories through the C FFI.

The range also includes later gateway/runtime-header work, but the commits above
are the ABI reason this PR needs the submodule bump.

Validation

  • 151 passed, 116 skipped, 3 failed
  • The three failures reproduce on origin/main and require ./build.sh, so
    they are not regressions from this branch.

RahulHere added 19 commits June 17, 2026 22:25
Switch the gopher-orch submodule from main to br_release to match the
JS SDK's release pipeline, and advance the recorded pointer from v0.1.13
to the br_release tip (Release 0.1.23, 05667fdf).
Wire up the five gopher_orch_agent_create_by_* C entry points that
landed in gopher-orch 0.1.23 (PR #116) into the Python SDK's ctypes
binding layer, matching the JS koffi side commit ce787c99.

Added in gopher_mcp_python/ffi/library.py:

- argtypes / restype declarations for:
    gopher_orch_agent_create_by_server_id   (4 char*)
    gopher_orch_agent_create_by_server_name (4 char*)
    gopher_orch_agent_create_by_gateway_id  (4 char*)
    gopher_orch_agent_create_by_gateway_name (4 char*)
    gopher_orch_agent_create_by_url         (3 char*)
  Wrapped in try / except AttributeError so the SDK keeps loading
  against older libgopher-orch builds (< 0.1.23) that lack these
  symbols; the matching set_log_level binding above uses the same
  pattern for forward-compatibility.

- Five Python wrapper methods on GopherOrchLibrary:
    agent_create_by_server_id   (provider, model, api_key, server_id)
    agent_create_by_server_name (provider, model, api_key, server_name)
    agent_create_by_gateway_id  (provider, model, api_key, gateway_id)
    agent_create_by_gateway_name(provider, model, api_key, gateway_name)
    agent_create_by_url         (provider, model, url)
  Each uses getattr(self._lib, symbol, None) so a missing C symbol
  yields a None handle instead of an AttributeError; the higher-level
  factory layer (A2) will translate the None into AgentError using
  the same last_error / clear_error pump the existing create() path
  uses.

Verified with the existing tests/test_ffi.py suite (11 passed) and a
local smoke test against the vendored 0.1.1 dylib confirming the
new wrappers return None for missing symbols rather than crashing
at import.
Surface the five routing factories landed in gopher-orch 0.1.23 on
the public GopherAgent class so Python SDK consumers can scope an
agent to a single MCP server, MCP gateway, or a known MCP URL
without hand-rolling a JSON server config.

Added in gopher_mcp_python/agent.py:

- Five @staticmethod factories mirroring create_with_api_key /
  create_with_server_config:
    create_with_server_id   (provider, model, api_key, server_id)
    create_with_server_name (provider, model, api_key, server_name)
    create_with_gateway_id  (provider, model, api_key, gateway_id)
    create_with_gateway_name(provider, model, api_key, gateway_name)
    create_with_url         (provider, model, url)
  Each delegates to the matching FFI wrapper from the A1 binding
  layer via a small lambda passed to the new helper below.

- _create_from_ffi(create_handle) helper that centralises the null
  handle pump shared by the five new factories: lazy library init,
  AgentError translation, and last_error / clear_error draining --
  same pattern as the existing create() inline pump, extracted so
  the five new factories each stay around ten lines instead of
  duplicating the diagnostic plumbing.

- Callable added to the typing imports for the helper signature.

Note: the existing GopherAgent.create(config) path is unchanged --
its inline pump still flows through GopherAgentConfig, which only
models the api_key / server_config XOR. The new factories take a
fourth input (server_id / gateway_name / url) that does not fit
that XOR and so bypass the builder; the next commit documents this
decision on GopherAgentConfig itself.

Verified with the full pytest suite (198 passed, 1 skipped) and a
local smoke test confirming that calling create_with_url against
the vendored 0.1.1 dylib (which lacks the new C symbols) raises
AgentError via the null-handle pump rather than crashing.
Add a class-level docstring note to GopherAgentConfig explaining
that the five new GopherAgent.create_with_* routing factories
deliberately bypass the builder.

Context:

GopherAgentConfig has always modelled an api_key XOR server_config
shape that maps onto the two original C entry points
gopher_orch_agent_create_by_api_key and
gopher_orch_agent_create_by_json. The five routing factories that
landed in gopher-orch 0.1.23 -- create_with_server_id /
create_with_server_name / create_with_gateway_id /
create_with_gateway_name / create_with_url -- each take a fourth
input (server / gateway identifier, or URL) that does not fit the
XOR, so the previous commit added them as static methods on
GopherAgent that dispatch into GopherOrchLibrary via the new
_create_from_ffi helper rather than threading an extra optional
field through GopherAgentConfig.

This commit only updates the docstring -- runtime behaviour is
unchanged. The note exists so a future reader looking at
GopherAgentConfig is not surprised that four of the factory
methods on GopherAgent never construct one.

Mirrors the equivalent docstring landed on the JS-side
GopherAgentConfig (gopher-mcp-js src/config.ts), keeping the two
SDKs aligned on this architectural decision.

Verified with the full pytest suite (198 passed, 1 skipped).
Verify the package-level export surface still covers the new
GopherAgent.create_with_* routing factories, and close the only
parity gap discovered against the JS SDK index.ts.

Findings:

- GopherAgent is already in gopher_mcp_python.__all__, and the
  five new @staticmethod factories ride along on it -- no new
  top-level export needed for the A2 surface.

- The native FFI subpackage exports both GopherOrchLibrary and
  GopherOrchHandle (gopher_mcp_python/ffi/__init__.py), but only
  GopherOrchLibrary was being re-exported at the package root.
  The JS SDK index.ts exposes both, with GopherOrchHandle reserved
  for advanced FFI consumers that need to hand-craft the opaque
  pointer type.

Change in gopher_mcp_python/__init__.py:

- Import GopherOrchHandle next to GopherOrchLibrary
- Add "GopherOrchHandle" to __all__ under the "# FFI" section

No runtime behaviour change for existing consumers; this only
widens the public re-export surface so advanced users no longer
need to reach into gopher_mcp_python.ffi for the handle type.

Verified by importing the new symbol at the top level and walking
the five new GopherAgent factories from the package root.
Add tests/test_agent_create_by.py covering the failure-path contract
for the five GopherAgent.create_with_* factories that the previous
commits added. Mirrors the JS-side suite at
gopher-mcp-js/tests/agent-create-by.test.ts and the C++ failure-path
cases in gopher-orch/tests/gopher/orch/agent_create_by_test.cc.

Cases:

- Empty api_key (one test per routing variant):
    create_with_server_id   raises AgentError
    create_with_server_name raises AgentError
    create_with_gateway_id  raises AgentError
    create_with_gateway_name raises AgentError
  Locks down the nullptr-on-failure contract that fetch_mcp_servers
  surfaces through the wrapper layer.

- Empty url:
    create_with_url with url="" raises AgentError.
  Mirrors the CreateByUrlRejectsEmptyUrl C++ case -- validation
  fires before any FFI work happens.

- Unknown provider:
    create_with_url(BAD_PROVIDER, MODEL, URL) raises AgentError.
  create_with_url synthesises a local http_sse config and reaches
  create_by_json, which rejects an unknown provider name; the
  factory must surface that as AgentError.

- AgentError carries a non-empty message:
  A regression sentinel for the last_error / clear_error pump --
  if a future change to _create_from_ffi swallows the C-side
  diagnostic, this case catches it immediately.

Skip pattern:

  Module-level pytestmark uses GopherOrchLibrary.is_available() to
  skip the whole module when the native library is not loadable,
  matching the existing tests/test_ffi.py decorator approach. CI
  runs build.sh first so the suite executes; the contract tests
  also pass against the vendored 0.1.1 dylib because the FFI
  wrappers' getattr(_lib, symbol, None) pump returns None for
  missing symbols and the helper translates None into AgentError.

Happy-path coverage for the four routing variants needs a stubbed
HTTP listener capturing the /v1/mcp-servers query string with the
new camelCase keys (serverId / serverName / gatewayId /
gatewayName); that infrastructure is tracked separately, matching
the same TODO comment on the C++ side.

Verified locally: 7 tests pass, runs in ~0.05s.
Land examples/api/create_by_api_key.py, the smallest of the seven
create_by_* examples and the first sanity check that the toolchain
(pip install -e ., the right native lib, env vars) is wired
correctly on the Python side.

Direct port of gopher-mcp-js/examples/api/create_by_api_key.ts,
which itself ports gopher-orch/examples/sdk/api/create_by_api_key.cc.

Behaviour:

- Uses a Gopher API key to fetch the caller's full MCP server
  inventory via GET /v1/mcp-servers and constructs an agent with
  every server the api key owns -- no routing parameter.

- Provider defaults to AnthropicProvider; model is taken from the
  LLM_MODEL env var. Refuses to call the FFI when either the API
  key or the model is still the placeholder value, printing a
  clear error to stderr and exiting 1. This matches the env-var-
  required path the JS side picked, so the example never surfaces
  a stale or fictional model identifier even if the toolchain
  would otherwise auto-pick one.

- Accepts queries as positional argv with a single canned fallback
  ("What time is it in Tokyo?") matching the JS sibling.

- try / finally with agent.dispose() so the native handle is
  always released, even if a query fails partway through.

This is the first file in examples/api/, a new self-contained
subdirectory following the same convention as the existing
examples/auth/ and examples/pip/ subdirs. The README, run scripts,
and remaining six variants follow in subsequent commits.

Verified locally: placeholder refusal path exits 1 with a clear
diagnostic; with GOPHER_API_KEY / LLM_MODEL / ANTHROPIC_API_KEY
set, the example completes end-to-end against a real provider.
Land examples/api/create_by_json.py, second of the seven create_by_*
examples. Direct port of
gopher-mcp-js/examples/api/create_by_json.ts, which itself ports
gopher-orch/examples/sdk/api/create_by_json.cc.

Behaviour:

- Builds a GopherAgent from an inline server JSON document, skipping
  the remote /v1/mcp-servers fetch that create_with_api_key performs.

- The inline SERVER_CONFIG follows the
  { succeeded, code, message, data: { servers: [...] } } envelope
  that ConfigLoader on the C++ side accepts. Default servers entry
  is a single http_sse stub pointing at 127.0.0.1:3001/rpc; edit the
  literal to point at a real MCP server before running.

- Built with json.dumps(...) rather than a string literal so the
  inline payload stays valid JSON under any future edit and matches
  the JS sibling's JSON.stringify({...}) shape.

- Provider defaults to AnthropicProvider; model is taken from the
  LLM_MODEL env var. Refuses to call the FFI when LLM_MODEL is still
  the placeholder, printing a clear error to stderr and exiting 1.
  Matches the env-var-required path the JS side picked so the
  example never surfaces a stale or fictional model identifier.

- Accepts queries as positional argv with the canned "What time is
  it in Tokyo?" fallback. try / finally with agent.dispose() so the
  native handle is always released.

Lands without depending on any of the five new routing factories,
so this and create_by_api_key.py are the two variants that work
today against either the vendored 0.1.1 dylib or a fresh
gopher-orch 0.1.23 build.

Verified locally: placeholder refusal path exits 1 with a clear
diagnostic; syntax check clean; no forbidden tokens.
Land examples/api/create_by_server_id.py, third of the seven
create_by_* examples and the first of the four routing variants
added in gopher-orch 0.1.23. Direct port of
gopher-mcp-js/examples/api/create_by_server_id.ts, which itself
ports gopher-orch/examples/sdk/api/create_by_server_id.cc.

Behaviour:

- Scopes the agent to a single MCP server in the caller's workspace
  by id. Internally hits the same GET /v1/mcp-servers endpoint as
  create_with_api_key under the Bearer api key, but adds the
  "?serverId={id}" routing query so the response carries only the
  matching server entry. Use this when the api key owns several
  MCP servers but the agent should bind to exactly one.

- Provider defaults to AnthropicProvider; the model comes from the
  LLM_MODEL env var. Refuses to call the FFI when any of the three
  required env vars (LLM_MODEL, GOPHER_API_KEY, GOPHER_MCP_SERVER_ID)
  is still the placeholder, printing a clear error to stderr and
  exiting 1.

- Reads the routing parameter from GOPHER_MCP_SERVER_ID,
  matching the JS sibling's env-var name so a downstream user can
  share env across the JS and Python toolchains. Same pattern for
  the next three routing variants (server_name, gateway_id,
  gateway_name).

- Accepts queries as positional argv with the canned "What time is
  it in Tokyo?" fallback. try / finally with agent.dispose() so the
  native handle is always released.

Requires the create_with_server_id factory landed earlier in this
PR series; against the vendored 0.1.1 dylib the example exits with
an AgentError from the null-handle pump because the C symbol is
missing. Against a fresh gopher-orch 0.1.23 build the example
completes end-to-end.

Verified locally: placeholder refusal path exits 1 with a clear
diagnostic; syntax check clean; no forbidden tokens.
Land examples/api/create_by_server_name.py, fourth of the seven
create_by_* examples and the second routing variant added in
gopher-orch 0.1.23. Direct port of
gopher-mcp-js/examples/api/create_by_server_name.ts, which itself
ports gopher-orch/examples/sdk/api/create_by_server_name.cc.

Behaviour:

- Scopes the agent to a single MCP server in the caller's workspace
  by human-readable name. Internally hits the same GET
  /v1/mcp-servers endpoint as create_with_api_key under the Bearer
  api key, but adds the "?serverName={name}" routing query so the
  response carries only the matching server entry. Use this when
  the api key owns several MCP servers and the agent should bind to
  exactly one identified by name rather than id.

- Provider defaults to AnthropicProvider; the model comes from the
  LLM_MODEL env var. Refuses to call the FFI when any of the three
  required env vars (LLM_MODEL, GOPHER_API_KEY,
  GOPHER_MCP_SERVER_NAME) is still the placeholder, printing a
  clear error to stderr and exiting 1.

- Reads the routing parameter from GOPHER_MCP_SERVER_NAME,
  matching the JS sibling env-var name. The sibling create_by_
  server_id.py example uses GOPHER_MCP_SERVER_ID; the two coexist
  so a downstream user can flip between routing variants by
  swapping the example file without changing the env block.

- Accepts queries as positional argv with the canned "What time is
  it in Tokyo?" fallback. try / finally with agent.dispose() so the
  native handle is always released.

Requires the create_with_server_name factory landed earlier in this
PR series.

Verified locally: placeholder refusal path exits 1 with a clear
diagnostic; syntax check clean; no forbidden tokens.
Land examples/api/create_by_gateway_id.py, fifth of the seven
create_by_* examples and the third routing variant added in
gopher-orch 0.1.23. Direct port of
gopher-mcp-js/examples/api/create_by_gateway_id.ts, which itself
ports gopher-orch/examples/sdk/api/create_by_gateway_id.cc.

Behaviour:

- Scopes the agent to a single MCP gateway in the caller's
  workspace. Internally hits the same GET /v1/mcp-servers endpoint
  as create_with_api_key under the Bearer api key, but adds the
  "?gatewayId={id}" routing query so the response carries the
  backing MCP servers for that gateway. Use this when the api key
  owns several gateways and the agent should bind to exactly one.

- Gateway routing is the natural choice when the workspace fronts
  several MCP servers behind a single mcp_gateway process; binding
  by gateway id reaches every backing server in one shot rather
  than picking one by server id / name. The variant landed at the
  same time as the per-server routing factories in gopher-orch PR
  #116 so the examples come in matched pairs.

- Provider defaults to AnthropicProvider; model from LLM_MODEL.
  Refuses to call the FFI when LLM_MODEL, GOPHER_API_KEY, or
  GOPHER_MCP_GATEWAY_ID is still the placeholder, printing a clear
  error to stderr and exiting 1. Matches the env-var-required path
  the JS side picked so no specific model identifier is baked
  into source.

- Accepts queries as positional argv with the canned "What time is
  it in Tokyo?" fallback. try / finally with agent.dispose() so the
  native handle is always released.

Requires the create_with_gateway_id factory landed earlier in this
PR series.

Verified locally: placeholder refusal path exits 1; full pytest
suite still passes (205 tests, including the seven contract tests
from earlier in the series).
Land examples/api/create_by_gateway_name.py, sixth of the seven
create_by_* examples and the fourth (last) routing variant added
in gopher-orch 0.1.23. Direct port of
gopher-mcp-js/examples/api/create_by_gateway_name.ts, which itself
ports gopher-orch/examples/sdk/api/create_by_gateway_name.cc.

Behaviour:

- Scopes the agent to a single MCP gateway in the caller's
  workspace by human-readable name. Internally hits the same GET
  /v1/mcp-servers endpoint as create_with_api_key under the Bearer
  api key, but adds the "?gatewayName={name}" routing query so the
  response carries the backing MCP servers for that gateway. Use
  this when the api key owns several gateways and the agent should
  bind to exactly one identified by name rather than id.

- Matches the by-name / by-id pair pattern established earlier for
  server routing: create_by_server_name + create_by_server_id,
  create_by_gateway_name + create_by_gateway_id. The next commit
  (B7) lands the seventh example, create_by_url.py, which is the
  outlier in the set because it skips the remote fetch entirely.

- Provider defaults to AnthropicProvider; model from LLM_MODEL.
  Refuses to call the FFI when LLM_MODEL, GOPHER_API_KEY, or
  GOPHER_MCP_GATEWAY_NAME is still the placeholder, printing a
  clear error to stderr and exiting 1.

- Accepts queries as positional argv with the canned "What time is
  it in Tokyo?" fallback. try / finally with agent.dispose() so the
  native handle is always released.

Requires the create_with_gateway_name factory landed earlier in
this PR series.

Verified locally: placeholder refusal path exits 1 with a clear
diagnostic; syntax check clean; no forbidden tokens.
Land examples/api/create_by_url.py, the seventh and final
create_by_* example. Direct port of
gopher-mcp-js/examples/api/create_by_url.ts, which itself ports
gopher-orch/examples/sdk/api/create_by_url.cc.

Behaviour:

- Builds a GopherAgent from a single MCP server URL, skipping the
  remote /v1/mcp-servers fetch that create_with_api_key performs
  and the inline JSON shape that create_with_server_config
  requires. Internally the factory synthesises an http_sse server
  entry around the URL and delegates to create_by_json. Use this
  for local development or one-off endpoints where the operator
  already knows the URL.

- The outlier of the seven examples: needs no Gopher API key
  because there is no remote config fetch. Only LLM_MODEL and
  GOPHER_MCP_URL are required.

- Provider defaults to AnthropicProvider; model from LLM_MODEL.
  Refuses to call the FFI when LLM_MODEL or GOPHER_MCP_URL is
  still the placeholder, printing a clear error to stderr and
  exiting 1.

- Accepts queries as positional argv with the canned "What time
  is it in Tokyo?" fallback. try / finally with agent.dispose()
  so the native handle is always released.

Closes the matched set: this is the seventh and last variant
needed before the C1 run-script wrappers and C2 README can cover
the full set in a single file-to-factory mapping.

Verified locally: placeholder refusal path exits 1 with a clear
diagnostic; syntax check clean; no forbidden tokens.
…#9)

Land the seven shell wrappers that pair with the .py examples
added earlier in this PR series. Single commit per the JS-side
precedent at gopher-mcp-js commit 53e3f44a.

Files:

    examples/api/create_by_api_key_run.sh
    examples/api/create_by_json_run.sh
    examples/api/create_by_server_id_run.sh
    examples/api/create_by_server_name_run.sh
    examples/api/create_by_gateway_id_run.sh
    examples/api/create_by_gateway_name_run.sh
    examples/api/create_by_url_run.sh

Each wrapper:

- Resolves PROJECT_DIR with two dirname() walks because the
  scripts sit at examples/api/ rather than the existing
  examples/ root.

- Bails out with a pointer at ./build.sh when
  $PROJECT_DIR/native/lib is missing -- this is where the
  build.sh CMake output lands and where the loader resolves
  libgopher-orch.dylib / libgopher-orch.so in dev mode.

- Pre-flight checks that gopher_mcp_python is importable via
    PYTHONPATH="$PROJECT_DIR" python3 -c "import gopher_mcp_python"
  and prints a clear "pip install -e ." hint on failure. The
  .ts side did not need this because tsx + relative import work
  out of the box; Python requires the package to be on sys.path
  or installed.

- Warns (but does not fail) when the per-variant routing env
  vars, GOPHER_API_KEY, LLM_MODEL, or ANTHROPIC_API_KEY are
  unset. The .py example itself enforces the hard failure on
  placeholder values; the wrapper just surfaces the warnings
  early so a user does not get to the Python-side error after a
  longer execution path.

- Exports DYLD_LIBRARY_PATH (macOS) and LD_LIBRARY_PATH (Linux)
  pointing at "$PROJECT_DIR/native/lib", plus PYTHONPATH so the
  example runs even when the package has not been pip installed.

- Runs python3 examples/api/<file>.py "$@" so positional argv
  passes through as queries, matching the .py behaviour.

Differences from the existing examples/client_example_json_run.sh:
  - No lsof / kill-port helper: the new wrappers do not bring up
    any local server, they just call the SDK factory. Matches the
    JS-side wrappers at commit 6769a068 which dropped the helper
    for the same reason.
  - Two dirname() walks instead of one because the new wrappers
    are at examples/api/ rather than examples/.

Verified locally: chmod +x set on all seven; bash -n syntax check
clean on each; smoke run of create_by_server_id_run.sh without env
exits 1 with the expected env-var warnings followed by the
Python-side placeholder rejection; no forbidden tokens.
Land examples/api/README.md, the documentation index for the seven
.py / *_run.sh example pairs added earlier in this PR series.
Direct port of gopher-mcp-js/examples/api/README.md
(commit 26b4662d on the JS side) with Python idiom translations.

Sections:

- File-to-factory mapping: three-column table that ties each
  Python example back to both the C++ canonical reference and
  the TypeScript sibling, plus the GopherAgent factory each
  exercises. Lists all seven create_by_* variants so a reader
  can navigate from any one of the three SDKs to the other two.

- Quick start: build.sh first to drop the native dylib into
  native/lib/, then pip install -e . for editable-mode imports,
  then invoke a wrapper. Mirrors the JS-side three-step intro
  with the pip step substituted for the npm install one.

- Environment-variable matrix: required vs optional env vars
  per example. Includes the four routing variants (server_id,
  server_name, gateway_id, gateway_name), plus a note that
  LLM_MODEL has no default and each example refuses to start
  until it is set -- the same env-var-required path the JS
  side picked.

- Picking the right factory: ties each factory to its routing
  query string and whether it triggers a /v1/mcp-servers
  fetch. Mirrors the gopher-orch/docs/Agent.md "Simple creation
  factories" section so the Python docs stay aligned with the
  upstream C++ docs.

- SDK-resolution note: the .py files import `from
  gopher_mcp_python import GopherAgent`, which resolves against
  whatever pip install -e . pointed at. Downstream consumers
  who pip install gopher-mcp-python from PyPI and copy an
  example do not need to edit the import path. Calls out the
  intentional difference from the JS sibling, which uses a
  relative `'../../src'` import that has to be rewritten on
  copy-out.

- Native-library resolution note: documents the DYLD_LIBRARY_PATH
  / LD_LIBRARY_PATH story for the wrappers, the pre-flight
  `python3 -c "import gopher_mcp_python"` check (which the
  TypeScript wrappers don't need because tsx handles the
  relative import for them), and the GOPHER_MCP_PYTHON_LIBRARY_PATH
  env var as an explicit override checked first by
  gopher_mcp_python/ffi/library.py.

- Cross-reference block: pointers at the C++ examples and docs,
  the TypeScript siblings, the FFI binding layer
  (gopher_mcp_python/ffi/library.py), the high-level wrappers
  (gopher_mcp_python/agent.py), and the contract tests
  (tests/test_agent_create_by.py).

Closes the examples/api/ landing series for the seven
create_by_* variants on the Python side. C3 (pyproject.toml /
setup.py adjustments) was tagged OPTIONAL in the design note
and skipped because no new dependency was needed -- the
examples use only stdlib (os, sys, json, traceback) plus the
SDK itself.

Verified locally: no forbidden tokens; all seven .py and *_run.sh
files referenced in the file-to-factory mapping table exist.
Replace the "warn / interactive prompt if [Unreleased] is empty"
check with an auto-population step that builds release notes from
the Python repo git log since the previous tag and from the
gopher-orch GitHub release notes for the new version. Mirrors the
same improvement landed on the JS-side dump-version.sh so the two
SDKs share a release workflow.

Verified by dry-running Step 4 against the current repo state
(v0.1.21 -> v0.1.23): 15 Python commits in range, gopher-orch
notes fetched and embedded, original CHANGELOG.md restored
afterwards. No interactive input required.
Move all seven examples/api/ wrappers from the local-source +
local-build resolution model to the PyPI-based model already used
by examples/pip/. Each wrapper now bootstraps its own venv,
installs gopher-mcp-python plus the matching platform native
package from PyPI, and runs the .py against the just-installed
package -- not against the in-tree source.

.gitignore
- Add examples/api/test-project-*/ so the per-variant venv
  work dirs the wrappers create do not pollute the working
  tree. The existing test-project-api/ rule covered only the
  examples/pip/ workdir and not the new examples/api/ ones.
Summary:\n- update third_party/gopher-orch submodule branch from br_release to main\n- advance third_party/gopher-orch to latest origin/main commit bf4b46ad

@dIvYaNshhh dIvYaNshhh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went through this one fairly carefully. The SDK core is the strongest part — _create_from_ffi reuses the last_error/clear_error contract from create() exactly, and the config.py docstring explaining why the routing factories bypass the builder XOR answers the obvious reviewer question before it gets asked. Nice.

Main things I'd want addressed before merge:

  1. Missing-symbol handling — a null return from the FFI layer is indistinguishable from a genuine native NULL, so users on an older libgopher-orch get a generic (or stale) error instead of "upgrade". Inline below.
  2. The new tests pass vacuously against exactly that older library, so they won't catch it either.
  3. The >= 0.1.23 claim in the README is wrong — 0.1.23 and 0.1.30 are already on PyPI without these factories. Following the documented SDK_VERSION=0.1.23 recipe gets you a build that doesn't have the feature.
  4. dump-version.sh has a few issues I've noted inline; the short version is that in the repo's current state it produces only the "Pin gopher-orch" line and drops the git-log section entirely.

Two structural notes, not blocking but worth thinking about now rather than later:

  • The seven *_run.sh wrappers are 95–108 lines each and differ only in banner text, work dir, and which env vars they warn about — detect_platform() is copy-pasted verbatim seven times. Same story on the Python side with env_or, the label block, and the run/dispose loop. One parametrised runner would cut ~700 lines and mean the next fix only has to land once.
  • The third_party/gopher-orch bump (ff84c19bf4b46a) is where the actual C symbols live, so it's the load-bearing change here, but there's nothing in the PR description about it. Could you add a line on what's in that range?

Also: there's unrelated formatting churn in five files (blank line removed after imports) plus setup.py (blank line added after the docstring) — two opposite directions. Easier to review if that's split out.

Comment thread gopher_mcp_python/ffi/library.py Outdated
Comment thread gopher_mcp_python/ffi/library.py
Comment thread tests/test_agent_create_by.py
Comment thread examples/api/README.md Outdated
Comment thread dump-version.sh Outdated
Comment thread dump-version.sh Outdated
Comment thread dump-version.sh
RahulHere added 7 commits July 30, 2026 15:30
Summary:
- configure each optional routing factory symbol with its own AttributeError guard
- avoid leaving present ctypes functions with the default c_int restype when an earlier optional symbol is missing
- add a fake-CDLL regression test for partially available routing symbols

Verification:
- git diff --check
- python3 -m pytest tests/test_ffi.py -q
Summary:
- raise a distinct upgrade-focused error when optional routing factory symbols are absent
- avoid confusing missing native symbols with native NULL handle returns or stale last-error state
- add direct FFI and public GopherAgent regression coverage for the missing-symbol path

Verification:
- git diff --check
- python3 -m pytest tests/test_ffi.py -q
Summary:
- skip routing factory contract tests unless all native routing factory symbols are present
- prevent older libgopher-orch builds from satisfying the tests through missing-symbol AgentError paths
- avoid a common local port in the unknown-provider URL case and correct the empty-url validation comment

Verification:
- git diff --check
- python3 -m pytest tests/test_agent_create_by.py tests/test_ffi.py -q
Summary:
- remove hard-coded 0.1.23 routing factory availability claims from examples and diagnostics
- describe routing factory support as requiring a published release with the native symbols
- keep tests aligned with release-neutral missing-symbol wording

Verification:
- git diff --check
- python3 -m pytest tests/test_ffi.py tests/test_agent_create_by.py -q
Summary:
- select the previous Python release tag with git describe from HEAD ancestry
- avoid using the globally highest version tag when it is off-branch or points at HEAD
- warn when the SDK commit range is empty before omitting the SDK changes section

Verification:
- bash -n dump-version.sh
- git diff --check
- git describe --tags --abbrev=0 --match 'v*' HEAD
Summary:
- read the previous package version from the prior tag's pyproject.toml
- derive the previous gopher-orch base version from X.Y.Z or X.Y.Z.E package versions
- warn when the tagged package version cannot be read or parsed

Verification:
- bash -n dump-version.sh
- git diff --check
Summary:
- fail release generation when CHANGELOG.md lacks an Unreleased section
- preserve manual changelog spacing while populating generated notes
- sanitize imported gopher-orch release notes by rewriting PR refs and user mentions
- clean up temporary changelog files with a trap

Verification:
- bash -n dump-version.sh
- git diff --check

@dIvYaNshhh dIvYaNshhh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at b476f194. All seven of my comments have a corresponding commit, and I checked the fixes rather than going off the messages — five are solid, two are partial. Resolving the five.

Confirmed fixed:

  • Per-symbol FFI binding — the routing_factories table with per-symbol try/getattr fully closes the pointer-truncation hazard, and test_should_bind_present_optional_routing_symbols_independently tests exactly the right thing: simulate server_id missing, assert the later four still get c_void_p. Nice test.
  • Missing symbol vs native NULLRuntimeError(_missing_routing_factory_message()) surfacing as an AgentError that tells the user to upgrade is precisely what was needed, and it's covered at both the FFI and public-factory level.
  • Contract tests — this is the one I most wanted to verify, so I ran it on a machine with no ./build.sh output:
    tests/test_agent_create_by.py sssssss     [100%]
    
    All seven skip. Before 18bb5f36 they'd have all passed green while exercising nothing. Also good catches on the stale "before any FFI work happens" comment and moving the test URL off 8080.
  • Version claims — gone from the README and five wrappers.
  • Changelog splicing — got all four sub-points (the [Unreleased] guard, blank-line preservation, the trap covering both temp files, and the #NNN/@user rewriting).

(The one test failure I saw, test_ffi.py::test_library_should_be_available, is pre-existing and just wants ./build.sh — not from this PR.)

Still needs a look — two comments inline, both on dump-version.sh. Short version: git describe is the right call but four tags sit on HEAD so it picks v0.1.16 arbitrarily and the range is still empty; and every one of those tags records pyproject.version = 0.1.2.1, so the new derivation confidently emits "Bump gopher-orch from v0.1.2 to v0.1.30". The second one is the one I'd actually fix, since it went from failing silently to emitting a wrong number.

Two things unchanged from my first pass, neither introduced here:

  1. The ~700 lines duplicated across the seven wrappers (comment inline). This round had to touch 5 of 7 files; #13 touches 2 of 7 for a different fix and leaves 5 broken. Worth collapsing before it bites a third time.
  2. The PR description is still empty, and the ff84c19 → bf4b46a submodule bump — where the actual C symbols these factories bind to live — has no note anywhere. Since that's the load-bearing part of the change and it's invisible in the diff, a couple of lines on what moved would help whoever reviews this next.

Also worth remembering _create_from_ffi re-raises without from e, though it matches the existing create() so it's fine to leave for a follow-up.

Good turnaround on this — the FFI and test work I'd call done.

Comment thread dump-version.sh Outdated
Comment thread dump-version.sh
Comment thread examples/api/create_by_api_key_run.sh Outdated
RahulHere added 3 commits July 31, 2026 14:12
Summary:
- resolve previous Python tag from highest version tag on HEAD first
- fall back to git describe when HEAD has no release tag
- add regression coverage for deterministic release tag selection
Summary:
- compare previous release tag names against recorded package versions
- warn when tag and pyproject versions disagree
- clear mismatched previous orch versions to avoid incorrect bump notes
- add regression coverage for tag/version mismatch handling
Summary:
- Add shared examples/api/_run_common.sh for platform detection, venv setup, SDK/native package install, and example execution.
- Refactor all seven API example runner scripts to source the common helper and keep only variant-specific banners and environment warnings.
- Remove stale hard-coded SDK_VERSION example text from the API key and JSON runner headers.

Verification:
- bash -n examples/api/_run_common.sh examples/api/*_run.sh
- git diff --check
- python3 -m pytest -q

@dIvYaNshhh dIvYaNshhh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All three follow-ups from my last pass are fixed. Resolving the remaining threads — nothing outstanding from me on this PR.

  • cb238eb5git tag --points-at HEAD --list 'v*' --sort=-v:refname | head -1 with the git describe fallback. Deterministic now: picks v0.1.30 off the four stacked tags instead of whichever one git happened to walk first.
  • e2023107 — the tag-name vs recorded-version mismatch warning. This is the one that mattered; the script can still derive a wrong number from bad tag data, but it now says so out loud instead of quietly emitting "Bump from v0.1.2".
  • c4feb45bexamples/api/_run_common.sh at +107, and all seven wrappers down from ~95–108 lines each to ~9–11. Net −632 lines. That's the structural item closed, and it's already paying off downstream: the Debian pip fix in #13 now lands in one place instead of needing seven edits.

Suite at the tip of the stack: 151 passed, 116 skipped, 3 failed — and all three failures reproduce identically on origin/main (they want ./build.sh), so nothing here regressed.

Still worth a couple of lines in the PR description before merge, mainly to say what moved in the ff84c19 → bf4b46a submodule bump, since that's where the C symbols these factories bind to actually live and it's invisible in the diff. Not blocking.

Nice work on this one.

@bettercallsaulj

Copy link
Copy Markdown
Collaborator Author

Thanks for the pass. I addressed the remaining non-blocking note by updating the PR description with the third_party/gopher-orch submodule context.

It now calls out the ff84c19 -> bf4b46a bump and names the native commits that matter for this PR's Python bindings:

  • 928cdce8 declares the five new ReActAgent factory methods.
  • a3834e6e implements the server/gateway id/name routing factories.
  • 5b85c2e7 implements createByUrl.
  • 63d5ea0d exposes the new agent factories through the C FFI.

The three follow-ups you listed are already in the stack, and I don't see anything else needed from this thread before merge.

@bettercallsaulj
bettercallsaulj merged commit 133787e into main Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants