External API reference

August 21, 2026 · View on GitHub

Notes on the third-party API shapes this code is written against, gathered from the LangChain and AWS docs (pinned 2026-07-01). These are the details that are easy to get wrong or that changed recently, kept here so the code and the docs don't drift. Sources are at the end of each section.


1. deepagents (LangChain Deep Agents)

  • Install deepagents (stable 0.7.x — the repo pins >=0.7.0,<0.8), Python ≥ 3.11. Extras: deepagents[aws] for Bedrock, deepagents[quickjs] for dynamic subagents.
  • 0.7.0 breaking changes this repo codes against:
    • TodoListMiddleware is NO LONGER a default — without it there is no write_todos tool, todos state, or planning prompt. The harvest attaches it explicitly to the MAIN agent (from langchain.agents.middleware import TodoListMiddleware); sub-agents stay lean (their prompts don't plan with todos).
    • Default prompts are lean: the authored base prompt is empty and the tool-usage prose constants (TASK_SYSTEM_PROMPT, FILESYSTEM_SYSTEM_PROMPT, …) are gone. Fine here — every role's prompt is fully authored in prompts.py.
    • A recursive delete filesystem tool is exposed to the model whenever the backend supports it (FilesystemBackend does). The OKF guard refuses it unconditionally — nothing in a bundle is agent-deletable.
    • write_file now OVERWRITES an existing file instead of returning a file-exists error (no create-only mode). The guard engine already validates overwrites against the existing doc (augmentation rules), so this is covered.
    • BackendProtocol.read() returns a ReadResult (error / file_data{"content"} / line-range fields), not the rendered string — the benchmark solver's custom read_file renders it via format_content_with_line_numbers (see solver._read_text).
    • FilesystemBackend/LocalShellBackend default virtual_mode=True now (we always passed it explicitly). read_file output uses dynamic-width line numbers + two-space separator (no fixed cat -n gutter); agent-facing empty ls/glob render as No files found.
  • from deepagents import create_deep_agent, CompiledSubAgent
  • Signature (everything after tools is keyword-only):
    create_deep_agent(
        model: str | BaseChatModel | None = None,
        tools=None,
        *,
        system_prompt: str | SystemMessage | None = None,   # not "instructions"
        middleware: Sequence[AgentMiddleware] = (),
        subagents=None,
        backend: BackendProtocol | None = None,
        **model_kwargs,   # e.g. model_provider="bedrock_converse", temperature=...
    )
    
    Returns a compiled LangGraph graph; call .invoke({"messages": [...]}), .ainvoke(...), or .stream(...).
  • Bedrock model. Build a ChatBedrockConverse and pass it as create_deep_agent(model=chat_model, ...). It comes from the standalone langchain-aws package (pip install langchain-aws), not the langchain[aws] extra. Building the model explicitly lets adaptive-thinking config ride on it via additional_model_request_fields (see §7 of OKF_DESIGN.md).
  • Backends (from deepagents.backends import ...): StateBackend (default, ephemeral in graph state), FilesystemBackend(root_dir=<abs>, virtual_mode=True) (real files — virtual_mode=True is required, the default False gives no path confinement), and CompositeBackend(default=..., routes={"/prefix/": ...}). The recommended setup is CompositeBackend(default=StateBackend(), routes={"/workspace/": FilesystemBackend(root_dir=..., virtual_mode=True)}), so the agent's internal files (/large_tool_results/, /conversation_history/) stay ephemeral and only /workspace/ hits disk.
  • Built-in tools: ls, read_file, write_file, edit_file, delete, glob, grep, task — plus write_todos when TodoListMiddleware is attached (no longer a default in 0.7). write_file(file_path, content) creates OR fully replaces; edit_file(file_path, old_string, new_string) is an exact-string replace (Claude Code semantics); delete(file_path) is recursive (refused by the OKF guard in this repo); read_file supports pagination.
  • Subagent dict (SubAgent): name (required), description (required), system_prompt (required — never inherited), tools (optional — when set it replaces the inherited tools), model (optional — inherits the parent), middleware (optional — not inherited; appended to the subagent's default stack). Exposed to the parent through the task tool.
  • Dynamic fan-out: add CodeInterpreterMiddleware from langchain_quickjs (needs deepagents[quickjs]); with subagents configured, agent code can call a task() global to fan out. Beta.
  • Middleware: from langchain.agents.middleware import AgentMiddleware.
    def wrap_tool_call(self, request, handler):
        name = request.tool_call["name"]      # dict indexing, not request.name
        args = request.tool_call["args"]
        # short-circuit (the tool never runs): return a ToolMessage/Command without handler
        return ToolMessage(content="refused", tool_call_id=request.tool_call["id"])
        # or override args then run: return handler(request.override(...))
        # or run normally: return handler(request)
    
    ToolMessage is from langchain.messages, Command from langgraph.types. Custom middleware= is appended to the main stack. The guard has to be attached to each subagent's middleware list too, since subagent middleware doesn't inherit.

2. Bedrock AgentCore Runtime

  • SDK: bedrock-agentcore. Container images must be ARM64.
  • HTTP agent (harvest):
    from bedrock_agentcore.runtime import BedrockAgentCoreApp
    app = BedrockAgentCoreApp()
    @app.entrypoint
    def invoke(payload, context=None): ...   # payload = deserialized body
    @app.ping
    def ping(): return "HealthyBusy" if busy else "Healthy"
    app.run()   # serves /invocations + /ping on 0.0.0.0:8080
    
    Long work goes on a background thread reporting HealthyBusy, so the session isn't idled out (8h cap). Don't advance time_of_last_update on every ping.
  • MCP server (consumption): serve on 0.0.0.0:8000/mcp, stateless streamable-HTTP.
    mcp = FastMCP(host="0.0.0.0", stateless_http=True)
    mcp.run(transport="streamable-http")   # port 8000, path /mcp
    
    It has to accept the platform-injected Mcp-Session-Id header.
  • Deploy (control plane): boto3.client("bedrock-agentcore-control") .create_agent_runtime(agentRuntimeName, agentRuntimeArtifact={"containerConfiguration": {"containerUri": "<ecr>:<tag>"}}, networkConfiguration={"networkMode": "VPC", "networkModeConfig": {"subnets": [...], "securityGroups": [...]}}, roleArn, protocolConfiguration={"serverProtocol": "HTTP"|"MCP"}, authorizerConfiguration, filesystemConfigurations=[...]).
  • Invoke (data plane): boto3.client("bedrock-agentcore") .invoke_agent_runtime(agentRuntimeArn=..., payload=<bytes>, runtimeSessionId=..., qualifier="DEFAULT"). runtimeSessionId is the dataset id, giving session affinity.
  • Filesystem config: {"s3FilesAccessPoint": {"accessPointArn": "arn:aws:s3files:<region>:<acct>:file-system/<fs>/access-point/<ap>", "mountPath": "/mnt/data"}}. Runtime-scoped, shared across all sessions, VPC required, mountPath under /mnt. The exec role needs s3files:ClientMount/ClientWrite/GetAccessPoint.
  • JWT auth: authorizerConfiguration={"customJWTAuthorizer": {"discoveryUrl": "<issuer>/.well-known/openid-configuration", "allowedClients": [<client_id>]}}. The discoveryUrl has to end with /.well-known/openid-configuration.
  • Terraform (hashicorp/aws ~> 6.0): aws_bedrockagentcore_agent_runtime with agent_runtime_artifact { container_configuration { container_uri } }, network_configuration { network_mode network_mode_config { subnets security_groups } }, protocol_configuration { server_protocol = "HTTP"|"MCP" } (a block, not a string), authorizer_configuration { custom_jwt_authorizer { discovery_url allowed_clients } }, filesystem_configuration { s3_files_access_point { access_point_arn mount_path } }, and lifecycle_configuration { idle_runtime_session_timeout max_lifetime }. The nested configs are HCL blocks; environment_variables and tags are maps.

A Gateway is an MCP front end for tools AgentCore hosts. Web Search is reachable only this way — there is no direct search API — so the chat agent gets it by talking MCP to a gateway whose single target is the built-in web-search connector.

Not to be confused with Bedrock's own "Web Search" tool (Aug 2026): a server-side built-in tool over the same Amazon index, offered only on the bedrock-mantle Responses API (GPT-5.x — not Converse/Anthropic, not bedrock-runtime), with Search + Fetch (cached page content) ops, url_citation annotations, its own IAM namespace (bedrock-websearch:InvokeSearch/InvokeFetch), and an external_web_access parameter — default true to match OpenAI, but live-web retrieval is additionally gated by bedrock-websearch:ExternalWebAccess, which AmazonBedrockFullAccess deliberately omits because external fetch is a data-exfiltration channel for an agent holding internal data (encode data in a URL, fetch it). The chat agent deliberately does NOT use it: the gateway connector serves every chat model (Anthropic included), is search-only, and stays in-AWS; the chat role holds no bedrock-websearch:* actions, and our Mantle requests never send the built-in tool (our LangChain web_search is a type: "function" tool — the shared name does not trigger the type: "web_search" built-in).

  • Gateway (Terraform, native): aws_bedrockagentcore_gateway with name role_arn protocol_type = "MCP" and authorizer_type = "AWS_IAM" (valid: CUSTOM_JWT, AWS_IAM; authorizer_configuration { custom_jwt_authorizer { discovery_url … } } is required only for CUSTOM_JWT). Exports gateway_id, gateway_arn, and gateway_url — the …/mcp endpoint.
  • Connector target — NOT natively supported. aws_bedrockagentcore_gateway_target's target_configuration.mcp accepts lambda / api_gateway / mcp_server / open_api_schema / smithy_model but no connector block (verified against provider 6.54/6.56). AWS::BedrockAgentCore::GatewayTarget in the CloudFormation registry does support Mcp.Connector and is FULLY_MUTABLE, so we declare the target with aws_cloudcontrolapi_resource (same provider, still declarative):
    desired_state = jsonencode({
      Name = "<target>", GatewayIdentifier = "<gateway_id>"
      TargetConfiguration = { Mcp = { Connector = {
        Source         = { ConnectorId = "web-search" }
        Configurations = [{ Name = "WebSearch", ParameterValues = {} }]
      } } }
      CredentialProviderConfigurations = [{ CredentialProviderType = "GATEWAY_IAM_ROLE" }]
    })
    
    Connector targets need no iamCredentialProvider (the connector's service is known to the gateway); MCP-server and OpenAPI targets do.
  • Connector versioning — the CFN registry can't pin. The control-plane API accepts source: {connectorId, version}, and version: "1.2.0" is what turns on the request-level filters (below) plus the target-level domain include list — a gateway snapshots the tool schema at target creation, and an unpinned target keeps the pre-1.2.0 schema and silently ignores a filters argument (verified live, Aug 2026). But AWS::BedrockAgentCore::GatewayTarget's ConnectorSource carries only ConnectorId (additionalProperties: false in the live registry schema, all three connector regions), so neither Cloud Control nor awscc can express the pin. The shipped CLI/SDK models lag too: as of aws-cli 2.35.8 / botocore 1.43.44 their ConnectorSource also has only connectorId, so update-gateway-target with source.version fails CLIENT-SIDE (ParamValidation) while the service accepts it (raw call → 202, schema gains filters — verified live). Our bridge: a terraform_data step sends UpdateGatewayTarget raw — PUT /gateways/{id}/targets/{targetId}/ on bedrock-agentcore-control.<region>, SigV4 service name bedrock-agentcore, signed by the stdlib-python script infra/compute/files/web_search_pin.py fed by aws configure export-credentials (curl's --aws-sigv4 is not portable: macOS's SecureTransport curl silently downgrades it to Basic auth). The step is the single writer for parameterValues (both domain lists) — see infra/compute/web_search.tf. Fold it back into the CLI, then the declarative resource, as the models and the CFN registry catch up.
  • Operator domain filtering (parameterValues.domainFilter): exclude (any connector version) and include (1.2.0+), ≤100 bare domains each, subdomains match, enforced server-side and invisible to the model. Request-level lists compose with them — target-level constraints can never be relaxed per call.
  • IAM, two roles. The gateway service role (trust bedrock-agentcore.amazonaws.com) needs bedrock-agentcore:InvokeGateway on …:gateway/* and bedrock-agentcore:InvokeWebSearch on the service-owned ARN arn:aws:bedrock-agentcore:<region>:aws:tool/web-search.v1 (account field is literally aws; region is the gateway's). The caller (our chat runtime) needs only bedrock-agentcore:InvokeGateway on the gateway ARN.
  • Invoke (MCP over HTTPS, SigV4 service bedrock-agentcore): POST JSON-RPC to gateway_url. initialize (protocolVersion "2025-06-18") returns an Mcp-Session-Id header to echo on subsequent calls; then tools/call {name, arguments}. Send Accept: application/json, text/event-stream — the reply may be either plain JSON or SSE data: frames.
  • Tool naming: ${target_name}___${tool_name} (three underscores), e.g. okf-web-search___WebSearch.
  • WebSearch I/O: input is {query (≤200 chars, required), maxResults (1–25, default 10), filters?}. filters (connector 1.2.0+ only — absent from the schema, and silently ignored on the wire, on older targets) is {domainFilter: {include?, exclude?}, publishedDateFilter: {from?, to?}} — ≤100 bare domains per list (root domain matches subdomains), inclusive ISO-8601 UTC date bounds matching each page's publication date. The result is an MCP tool result whose text block is JSON: {id, results:[{text, url, title, publishedDate}]} (url/title/ publishedDate all optional; publishedDate format is unpinned — ISO dates but also prose like "05:00PM, Sunday, October 06 2024, PDT" observed live). Our wrapper surfaces the filters as published_after/published_before/ include_domains/exclude_domains, gated on OKF_WEB_SEARCH_FILTERS_ENABLED (services/chat/src/chat/web_search.py).
  • Region + acceptable use: connector available in us-east-1, eu-west-1, and ap-northeast-1. Queries are served inside AWS (not handed to a third-party engine), but AWS's terms require retaining the source citations/links in anything shown to end users, and forbid bulk extraction or building a competing index.

3. S3 Vectors — boto3.client("s3vectors")

  • create_vector_bucket(vectorBucketName=...).
  • create_index(vectorBucketName, indexName, dataType="float32", dimension=512, distanceMetric="cosine", metadataConfiguration={"nonFilterableMetadataKeys": ["title","description","s3_key"]}). The dimension, distanceMetric, dataType, and metadataConfiguration are immutable — changing one means replacing the index and re-embedding.
  • put_vectors(vectorBucketName, indexName, vectors=[{"key": <path>, "data": {"float32": [floats, len == dim]}, "metadata": {...}}]), up to 500 per call. Note data is a tagged union {"float32": [...]}, not a flat vector key. An existing key is fully overwritten.
  • query_vectors(vectorBucketName, indexName, topK, queryVector={"float32":[...]}, filter={...}, returnMetadata=True, returnDistance=True) returns {"vectors": [{"key","distance","metadata"}], "distanceMetric", "nextToken"}. Page ≤ 100.
  • delete_vectors(vectorBucketName, indexName, keys=[...]), up to 500.
  • Filter operators: $eq $ne $gt $gte $lt $lte $in $nin $exists $and $or (no prefix, substring, or regex). Range operators are number-only. $eq against a list value matches any element.
  • The 403 trap: query_vectors with a filter or returnMetadata=True also needs s3vectors:GetVectors, not just s3vectors:QueryVectors.
  • Filterable metadata is ≤ 2 KB per vector (≤ 40 KB total); non-filterable keys are ≤ 10 per index.
  • Terraform (hashicorp/aws ~> 6.0): aws_s3vectors_vector_bucket and aws_s3vectors_index (args data_type, dimension, distance_metric, and a metadata_configuration { non_filterable_metadata_keys = [...] } block — all force replacement, matching the API's immutability).

4. Titan V2 / Glue / Athena / EventBridge

  • Titan V2: boto3.client("bedrock-runtime").invoke_model(modelId= "amazon.titan-embed-text-v2:0", body=json.dumps({"inputText": t[:50000], "dimensions": 512, "normalize": True})). dimensions is only 1024, 512, or 256. Read the result at json.loads(resp["body"].read())["embedding"]. Throttling raises ThrottlingException; retry with backoff.
  • Glue (boto3.client("glue")): get_databases / get_tables page at 100, get_partitions at 1000; also get_table_versions. Table.StorageDescriptor.Columns[] is {Name, Type (Hive string), Comment}; Table.PartitionKeys[] has the same shape. Detect change via Table.UpdateTime and VersionId (monotonic), never LastAccessTime.
  • Athena (boto3.client("athena")): start_query_execution(QueryString, QueryExecutionContext={"Database": ...}, ResultConfiguration={"OutputLocation": "s3://..."} | WorkGroup=...), then poll get_query_execution(...)["QueryExecution"]["Status"]["State"] until a terminal state (SUCCEEDED, FAILED, CANCELLED — two Ls), then get_query_results(...). Row 0 is the header; cells are at Rows[].Data[].VarCharValue; paginate with NextToken.
  • Glue change event: {"source": ["aws.glue"], "detail-type": ["Glue Data Catalog Table State Change"]}; detail {databaseName, tableName, typeOfChange, changedPartitions}.
  • S3 events: enable with aws_s3_bucket_notification { eventbridge = true } (all events flow to the default bus; filter in the rule). Event {"source": ["aws.s3"], "detail-type": ["Object Created","Object Deleted"]}; detail {bucket.name, object.key, object.size, object.sequencer}. Order and dedup on object.sequencer — a hex string, comparable lexicographically per key.

5. Terraform (hashicorp/aws ~> 6.0, native throughout)

  • aws_cognito_user_pool exposes endpoint = cognito-idp.<region>.amazonaws.com/<poolId> (no scheme). Issuer is https://${endpoint}; discovery is ${issuer}/.well-known/openid-configuration.
  • aws_cognito_user_pool_client: allowed_oauth_flows_user_pool_client=true, allowed_oauth_flows=["code"], allowed_oauth_scopes, callback_urls, logout_urls, supported_identity_providers=["COGNITO"]. A SPA client has no secret.
  • API Gateway v2: aws_apigatewayv2_api (protocol_type="HTTP") + aws_apigatewayv2_authorizer (authorizer_type="JWT", identity_sources=["$request.header.Authorization"], jwt_configuration { audience=[client_id], issuer="https://${endpoint}" }) + aws_apigatewayv2_integration (AWS_PROXY, integration_uri=<lambda invoke_arn>, payload_format_version="2.0") + aws_apigatewayv2_route (route_key, authorization_type="JWT", authorizer_id) + aws_apigatewayv2_stage (name="$default", auto_deploy=true). aws_lambda_permission uses source_arn = "${aws_apigatewayv2_api.x.execution_arn}/*/*".
  • aws_lambda_function (package_type Image or Zip), aws_iam_role + aws_iam_role_policy (managed_policy_arns / inline_policy are deprecated), aws_lambda_event_source_mapping (SQS: event_source_arn, batch_size, function_response_types=["ReportBatchItemFailures"], no starting_position).
  • aws_dynamodb_table (PAY_PER_REQUEST; declare attribute blocks only for key attributes, or you get a perpetual diff).
  • aws_s3_bucket_notification { eventbridge = true } (atomic — one per bucket) + aws_cloudwatch_event_rule (event_pattern via jsonencode) + aws_cloudwatch_event_target + aws_sqs_queue + aws_sqs_queue_policy (Principal events.amazonaws.com, must include Version="2012-10-17").
  • aws_cloudfront_origin_access_control (signing_protocol="sigv4") + aws_cloudfront_distribution (OAC origin). The SPA fallback is a custom_error_response for both 403 and 404 → 200 /index.html, since OAC on S3 returns 403 for a missing object.
  • EventBridge S3 key filter: an array of content filters under one field is OR, not AND — object.key = [{prefix},{suffix}] matches prefix OR suffix. For prefix-AND-suffix, use a single wildcard: object = { key = [{ wildcard = "okf/*.md" }] }.
  • S3 Files (native): aws_s3files_file_system (bucket = bundle-bucket ARN, role_arn, accept_bucket_warning, optional prefix; the role is assumed by elasticfilesystem.amazonaws.com) + aws_s3files_mount_target (file_system_id, subnet_id, security_groups) + aws_s3files_access_point (file_system_id, root_directory { path = "/okf" }, posix_user { uid gid }; exports arn). Mount that arn in the runtime's filesystem_configuration.
  • Backend: terraform { backend "s3" { bucket, key, region, use_lockfile = true } } (DynamoDB locking is deprecated). Cross-stack reads use data "terraform_remote_state" (root-level outputs only).
  • Everything, including S3 Vectors, AgentCore, and S3 Files, is native in hashicorp/aws ~> 6.0 (v6.45+); the awscc provider isn't needed.
  • The registry docs render client-side (empty to WebFetch), so read them from raw.githubusercontent.com/hashicorp/terraform-provider-aws/main/website/docs/r/<name>.html.markdown. Newer services like s3files may not have published docs yet — read the resource's Go schema under internal/service/<svc>/.

6. React + Cognito OIDC (react-oidc-context on oidc-client-ts)

  • AuthProvider config: authority="https://cognito-idp.<region>.amazonaws.com/ <poolId>", client_id, redirect_uri, response_type="code" (PKCE S256), scope="openid email profile", onSigninCallback (strip ?code&state, or silent renew breaks), userStore: new WebStorageStateStore({ store: window.localStorage }) (to survive SPA navigations).
  • useAuth() returns {isLoading, isAuthenticated, user, error, signinRedirect, signoutRedirect, removeUser}. Tokens live at auth.user?.id_token, .access_token, .profile.
  • Send the ID token to an API Gateway JWT authorizer with audience=<client_id> — the ID token's aud equals the client id, while the access token has no aud, only client_id and scope.
  • Cognito's /logout is on the hosted-UI domain (https://<domain>.auth.<region>.amazoncognito.com/logout?client_id=..&logout_uri=..), not the issuer host.
  • Vite MPA: build.rollupOptions.input = { name: resolve(...html), ... }.