Imported from treansai/agenomic-typescript (
AGENTS.md). Install upstream withnpx skills add treansai/agenomic-typescript. Copyright stays with the author.
AGENTS.md
Decisions that would otherwise live in code comments. Keep entries short and factual.
Protect
- Request helper:
tools.tsexportsfetchJson(transport only),acceptJson(2xx or typedToolExecutionErrorfrom{ error: { code, message } },invalid_responseon a non JSON 2xx body) andrequestJson.ToolsResource.requestand everyprotect.tsmethod go through them so there is no untyped path; the RMP resources inrmp.tskeep their own local-first helper because their offline mode must not throw. ToolCallResult.resultisT | null: 202 pending and 403 denied envelopes carryresult: null(design section 5).router.callstill returnsTbecause pending and denied always throw;invokecallers must null check.- Two 403 bodies exist on
invoke: an invoke envelope ({ result: null, agenomic: { record_id, ... } }) becomesToolCallDenied; a plain{ error }body (capability lapse, permit invalid, run not active) stays aToolExecutionErrorwith the server code.readEnvelopedecides on the presence ofagenomic.record_id. authorizeLocalaccepts the decision on a 2xx body or on a 403 body that carriesdecisionorprotect(the gateway answers 200 local/gateway, 202 pending, 403 denied). The 403 gate tests for the presence of the key ("decision" in body), not for a string value: a 403 carryingdecision: nullor a number is an admission denial and used to fall through toacceptJsonand surface as a generichttp_error, which hid the denial fromToolCallDeniedand fromrouter.calls. Onlylocal,gatewayandpendingare recognized; every other string, a missing field or a non string isdenied(design section 9, no fallback).- Pending and denied are admission outcomes, not tool outcomes: the router pushes the envelope to
router.callsand throws regardless ofraiseOnError. Synthesized local-path envelopes use real enum values only (source: unrouted,fidelity: contract_only,external_state: none) sohasRealCallsstays false. - The permit is opaque to the SDK (
{ document, signature }records) and forwarded verbatim inreport-local; the SDK never inspects or verifies it (design D6). beforeActionreceives the call identity (ToolActionIntent) and is awaited before any request on both paths, including the re-POST issued byresume; a throw aborts locally and leaves no evidence row. It cannot approve.ToolsResource.clientis publicreadonly(it wasprivate) so the router can pollclient.protect.approvalswithout a second HTTP helper.ToolActionIntent.executionPointisruntime_localwhenever a local function is registered for the tool, even if the gateway later answersgateway: the hook sees the intended execution point before any request, not the final routing.- Synthesized local-path envelopes set
agenomic.decisionto the outcome (protect.outcome, elserequire_approvalfor pending anddenyfor denied) so the field means the same thing as on gateway envelopes; the local authorize verb (pending,denied) lives inLocalAuthorization.decisiononly. resumeruns through the same privatedispatchascallwith aresumeargument (no instance flag) so concurrent calls on the same router never lose their idempotency key.resumelooks the call identity up in a private map keyed byapproval_idfilled whenToolApprovalPendingis thrown; an approval id the router never saw throwsunknown_approvalrather than guessing a body. The re-POST sends the identical body with the originalIdempotency-Key(docs/protect/integration.md section 6: the middleware never records a 202, so the key is free to replay the cached 200 once the call executed). Statusesapprovedandconsumedboth re-issue exactly once, which is the behaviour the Python SDK also implements: a consumed approval already executed, so the replay recovers the stored result, and a 409 on that replay throwsToolExecutionError("conflict", ..., 409)rather than a denial that would suggest nothing ran.pendingpolls untiltimeoutMs(elapsed time is checked before sleeping sotimeoutMs: 0cannot loop);rejected,expiredand unknown strings throwToolCallDeniedwith that status ascode(fail closed).- The consumed recovery contract is verified against a real gateway, not only against an HTTP fixture:
examples/protect-consumed-recovery.tsis the manual harness (closure criterion 3 of the 2026-09-15 Protect campaign), run with the built bundle, andexamples/11_protect_consumed_recovery.pyin agenomic-python is its twin asserting the same contract so a divergence fails an assertion on one side. Observed on a gateway with Postgres and two user owned API keys: on the gateway executed path (invoke, where the SDK sends theIdempotency-Keyderived from run, repetition and logical call id) a resume over a consumed approval answers HTTP 200 and replays the stored result with no second effect; on the runtime local path (local/authorize, which carries no idempotency key) the same resume answers HTTP 409 and throwsToolExecutionError("conflict", ..., 409). Both branches of the contract are real and path dependent, not credential dependent, and both SDKs produce them identically. Keep both; narrowingresumeto one branch would break the other execution point. resumeclaims the approval in aresumingset before its firstawaitand releases it in afinally, then deletes its pending map entry before re-issuing. The deletion alone was not enough: two concurrent resumes of the same approval both read the map entry before either reached the deletion after the polling loop, both re-POSTed, and on the runtime local path, which carries no idempotency key, that executed the local function twice. A second resume while one is in flight now throwsToolExecutionError("resume_in_flight"); a resume that timed out has released its claim, so retrying after a timeout still works. Callingresumetwice in sequence still ends inunknown_approval. The consumed path is reachable only from a second holder of the same identity, which the harness builds by having two routers issue the identicallogicalCallIdand both receive the same 202 and approval id. This is deliberate: a router must not invent an identity it never held.- The pending map stores a deep snapshot of the arguments (
snapshotArgs,structuredClonewith a JSON round trip fallback). The identity kept the caller's object by reference, so mutating the arguments after the 202 maderesumesend a different payload under the logical call id and idempotency key that were approved: the replay would no longer be the approved request. - An empty 2xx body is
invalid_response, not{}.fetchJsonleavesparsedundefined when the text is empty, soacceptJsonrefuses it like any other non JSON 2xx body. Turning it into an empty object let exportedrequestJsonand the less strictly validated tool methods accept a response that carried no JSON at all.benchmarks.tskeeps its own 204 handling and is unaffected. protect.tssubclasses the RMPProtectResourcesoalerts,actionPlan,recommendationsandnotifykeep their local-first behaviour;rmp.tsexposesclientasprotectedfor that reason.index.tsexports the subclass under the single public nameProtectResource.- Path segments in
protect.tsareencodeURIComponent-encoded likermp.ts;policies.*take(policyId, version)and buildpolicy_id@version(crm%402.0.0), the formathandlers_policies.rssplits on the first@. - Wire mirrors in
protect.tsfollowcrates/agenomic-api-types/src/lib.rs(PolicySummary, PolicyDetailResponse, SimulateResponse, PolicyDiffResponse, PolicyBinding, ProtectDecisionDto, ProtectApproval, ProtectRestriction, KillSwitchResponse, OverlayResponse, CatalogResponse, CoverageResponse, ProtectMetricsSummary) andagenomic-policy/src/intent.rs(ProtectDecisionDocumentmirrorsDecision,ProtectDecisionmirrorsInvocationDecision). Uuids and timestamps are strings; enums whose Rust side is a plainString(scope_kind, bindingstatus, restrictionkind) staystringon read and are literal unions on the write side only. Single-record responses are read bare, as the gateway sends them (record(res, "id", ...),"text"for the overlay), and an enveloped body is aninvalid_response: accepting both hid a wire mismatch. The three policy transitions (register, release, deprecate) are the exception, answeringPolicyResponse { policy }, so they go throughpolicyRecord(res)and return a typedPolicyRecord. List responses require the documented array key and raiseinvalid_responsewhen it is absent, so an empty list and a shape mismatch stay distinct.decisions.listis the one paginated read and returns{ decisions, next_cursor? };policies.registeralso accepts the YAML or JSON text, sent as{ document_text };bindings.listcarries thestatusfilter. - OpenAI overlay:
applyOverlayis a pure function returning the same object when the overlay is already present (messages[0]is the system overlay, orinstructionsequals or starts with the overlay text followed by a blank line), so double instrumentation and retries are idempotent and the caller's request object is never mutated. Forresponses.createthe recordedmodel_call.inputbecomes{ instructions, input }wheneverinstructionsis present, otherwiseinput, so the trace reflects the injected request.
Vault
- Agents Vault is an optional commercial module; the contract is
agenomic-cloud/docs/vault/openapi.yaml. The SDK surface is public and inert without the add-on.client.vaultis the admin namespace (API key);client.vault.runtimeand theclient.tools.execute/executionStatusshortcuts use the runtime token. Credentials never cross surfaces: the transport reads the API key only for admin routes and thevrt_token only for/v1/vault/runtime/*, and callerheadersnamedauthorization,proxy-authorizationorx-api-keyare dropped for vault calls. A runtime token that does not start withvrt_is refused when the client is built, so an org API key cannot be wired into an agent runtime by mistake. - Auth header: the OpenAPI declares
x-api-keyfor admin routes; the SDK sendsauthorization: Bearer <apiKey>like every other resource, which the gateway accepts as the same credential (services/api-gateway/src/auth.rs). Runtime routes takeBearer vrt_...as documented. - Read types are snake_case wire mirrors of the OpenAPI schemas (like
protect.ts); inputs are camelCase. Fields the server sends asnullare typed| nullalthough the OpenAPI declares them plain strings (handlers_vault.rsserializesOptionwithout skipping).BindingContentandProviderDescriptorstay in wire shape on input. List filters (environment,provider_id,agent_id,binding_id,state,limit,action_id) come from the handler query structs, not from the OpenAPI, which documents no query parameters. - Error classes extend
ToolExecutionErrorthroughVaultErrorso existing catches keep working. The server's errorcodeis a free string in the OpenAPI; the mapping invault/mapping.tsfollowsVaultError::codeinagenomic-vault/src/error.rspluscapability_*fromagenomic-capabilities. Rate limiting istoo_many_requests(HTTP 429,Retry-After), not documented in the OpenAPI. A lock iscapability_not_entitled,capability_not_in_editionorcapability_disabledand is relayed asVaultNotEntitledError; the SDK never computes entitlement andstatus()returns the server's answer unchanged. - The runtime execute response is not what the OpenAPI says: 403 carries either a
deniedbody (policy) or a plain{ error }(entitlement, permission), and 409 carries either a plain{ error }(vault_*refusals) or arefusedbody.interpretExecutiondecides on the body (statusplus the matching HTTP status andaction_id), and any 2xx that is not a consistentfinished/succeededbody isinvalid_response: fail closed, never a success. outcome_unknownarrives as a 200finishedbody withstate: outcome_unknownand as a 409vault_outcome_unknown; both areVaultOutcomeUnknownErrorand neither is retried. A resend of a deniedactionIdreturns afinished/refusedbody whoseerror_classis the comma-joined reason codes (runtime.rsmark_refused); it maps back toVaultPolicyDeniedError. Statefailed(the destination answered an error) throwsVaultExecutionFailedErrorcarrying the server-filtered body;tools.executereturns only forsucceeded.- Retries: the transport resends the identical serialized body, so
actionIdcannot change between attempts. Reads and executions are idempotent and retry on a network fault, 502, 503, 504 andvault_backend_unavailable/vault_destination_unavailable; 429 retries for every request (the throttle runs before processing); admin writes never retry after a fault. The SDK's own timeout does not retry (the server may still be executing;getExecutionreads it). Execute timeout isdeadlineMs(default 30 s) plus 15 s. - Secrets:
Sensitivekeeps bytes in a module-privateWeakMap, has an own enumerableredactedfield only sostructuredCloneyields the constant mask, and has no accessor; the request serializer substitutes the text through aJSON.stringifyreplacer that reads the holder (toJSONruns first), only for the three secret-write routes (allowSensitive). A plain string is refused (sensitive_required), aSensitiveanywhere else is refused (sensitive_refused, including toolarguments). The transport builds errors from routing facts and never attaches acause; server text is scrubbed against the request's secrets in raw, JSON-escaped, URL, base64, base64url and hex forms, and so is the parsed body of a secret write. Log events carry no body, header or token.RuntimeToken(returned byruntimeIdentities.issue) is masked the same way andtakeOnce()hands the text out once; it is an enrollment credential, not a vault secret value. The transport keeps the client in a#privatefield soclient.vaultstays serializable. secrets.rotatestarts a rotation job (POST /secrets/{id}/rotations,overlap_seconds);secrets.addVersionis the olderPOST /secrets/{id}/versions. The nine paths added later (rotations x5,revocations/{id}/retry|lift,executions/{action_id}/resolve,runtime/grants/{id}/delegations) need a server that implements them; the docs say so.resolvenever re-sends.- Replay is client side because the vault OpenAPI has no replay route:
vault.replay.fixturesmakes a client that answersexecuteandgetExecutionfrom fixtures and nothing else. It holds no runtime token (combining them is refused at construction), has nofetch, and a missing fixture isVaultReplayFixtureMissingError; admin and grant calls throwVaultReplayUnavailableErrorand are never sent live. Each outcome kind synthesizes the wire answer the server gives and goes through the same mapping as a live call; the per-actionIdidempotency of the server (settled answers repeat, a different request on a usedactionIdis a 409) is mirrored. - Not done: receipt signature verification (the contract defines no verification procedure),
lint(this repo has no linter;tsc --strictis the static gate), a CHANGELOG (the repo has none).
Release
- The npm package is
@treansai/agenomic-typescript; the SDK identity string is not.SDK_METADATA.nameinutils.tsand thesdkdefault inbenchmarks.tsstay"agenomic-typescript"because that value is a protocol constant the cloud matches on (agenomic-cloud/crates/agenomic-onboarding/src/enroll.rs,AgenomicTypescript => "agenomic-typescript"), paired withagenomic-python. Scoping the registry name must not move the wire value. buildrunstsupbeforetypecheck, not after.examples/protect-consumed-recovery.tsimports from../dist/index.js(deliberately: it is the harness that runs against the built bundle) andtsconfig.jsonincludesexamples/**/*.ts, sotsc --noEmiton a checkout with nodistfails with 21 unresolved-member errors. The old order madepnpm buildimpossible from a clean clone, which is exactly what CI has.- Publishing goes through
npm publish, notpnpm publish: OIDC trusted publishing landed in pnpm 10.17 andpackageManagerpins 10.0.0. The workflow upgrades the npm CLI only when the runner image ships one below 11.5.1, the version that added trusted publishing. - The release job packs with
npm packand publishes the resulting tarball rather than the working directory, so the artifact attached to the GitHub release is byte-for-byte the one on the registry. That holds from 0.1.2 onward: 0.1.1 is the hand-published bootstrap, so its registry copy carries no provenance and is not the tarball its GitHub release attaches. - The tag trigger is not the authorization.
on: push: tagsmatches a name, not a history, so av*tag on any unmerged branch would otherwise check out that commit and publish it under the package name, with review and branch protection bypassed. The job fetchesmainand refuses unless the checked out commit is reachable from it, which is why checkout runs atfetch-depth: 0. The ancestry test usesgit rev-parse HEADrather thanGITHUB_SHAso it reads the same for a lightweight and an annotated tag. - No npm token exists in the repository.
id-token: writeplus thenpmenvironment is the whole credential; the bootstrap that had to precede it is indocs/release.md. npm publishis handed./artifacts/<name>.tgz, neverartifacts/<name>.tgz. npm-package-arg only treats a spec as a file when it starts with.,/,~/, a drive letter orfile:; a barea/b.tgzfalls through to the hosted-git shorthand parser and npm tries to clonegithub.com/artifacts/<name>.tgz. Verified on npm 10.9.8, 11.19.1 and 12.0.2: the bare form exits 128, the./form resolves the tarball.find ./artifactsis what keeps the prefix.- The publish step skips when the version is already on the registry instead of failing. The tag-vs-package.json guard already makes a mismatched tag impossible, so the only way to reach publish with a published version is replaying the same tag, which is either the bootstrap handover or a re-run of a failed job. Failing there would cost the GitHub release, which is the part still worth producing.
Runtime
tools.tsimportsrandomUUIDfromnode:cryptolikeutils.tsdoes, rather than reaching for thecryptoglobal. The global is only exposed unflagged from Node 19, sologicalCallIdgeneration threwReferenceError: crypto is not definedon Node 18 whileengines.nodepromised>=18. One file was using a different source for the same primitive than the rest of the SDK.- CI gates Node 20, 22 and 24, not the declared
>=18floor. Node 18 has been end of life since April 2025 and, with the import above fixed, it passes every test except one benchmarks HTTP case that lands at 5027ms against vitest's 5000ms default. The floor is left at>=18because the SDK does work there; gating it would mean loosening the timeout for every test to cover a runtime nobody should be on.
Managed prompts
- Scope: references, digests, rendering,
client.prompts(get,resolve,resolveAgent,workspaceId),client.bindings(create,get) and offline bundle reading. Publishing, drafts, aliases, channels, experiments and the LangGraph adapter are Python only, and no SDK exposes a channel promote or rollback: channel moves need a signed-in session and the SDKs hold API keys. - Bundle signatures are not verified. The signature is ed25519 over BLAKE3,
node:cryptohas no BLAKE3 and the package keepszodas its only runtime dependency, soPromptBundle.fromDocumentandreadPromptBundleFilerequireexpectedBundleDigest(theprompt_bundle_digestof the export),expectedWorkspaceIdandexpectedAgentId. A missing pin, including from untyped JavaScript, throwsbundle_untrusted_key; there is no flag to load an unpinned document.signatureVerifiedis alwaysfalse, a signed export is accepted only through the pin, and the governance step is not applied because the pin is the operator's approval. The bundle digest is required rather than the manifest digest because only the artifact set covers child manifests and their prompts. - The load order is the Python one: JSON subset, schema and member types, a signed document without
expires_atrefused, pin present, expiry (onlyYYYY-MM-DDTHH:MM:SSZ), every prompt content digest, the artifact set against the embedded digest then the pin, every manifest digest andexpectedManifestDigest, the exact closure (missing or extra prompts, a child whose release or genome differs from its pin, a child manifest of another agent, an unreachable child, a prompt key that is not its ownprompt_id:version), then scope. The closure walk compares a pin every time it meets one and deduplicates only the traversal: a child or a prompt named by more than one pin (two manifests, two slots, a slot and a fragment) is refused when any of those pins differs from the single record the bundle carries, whichever pin the walk meets first.fromOnlineResponseruns the same checks without a pin and refuses a document carryingsignature. - A verified bundle keeps a deep-frozen JSON copy taken right after the subset check, so mutating the caller's object afterwards cannot change what was verified.
ManagedPromptVersionobjects are deep-frozen too, and their validation result lives in aWeakMap; a version object the SDK did not build (a spread copy, a hand-made literal) is validated again and its digest compared withcontentDigestbefore it renders. - Name-keyed maps never use
inor a write on a plain object: membership isObject.hasOwn, render values live in aMap, thefragmentsmap of a version isObject.create(null), andscrubSecretsJsonbuilds objects withObject.fromEntries. A variable, partial or fragment may be named__proto__,constructorortoString(vectors R063 to R066 and T069).renderText,renderMessagesandcomposealso take aMapof variables for that reason. getdigest-verifies every record of the fetched closure before validating the content, so a tampered fragment reportsprompt_digest_mismatchwith its ref, as the digest verification of RFC 0012 requires for every downloaded entry, instead of the validation itemfragment_digest_mismatch. Content that is otherwise invalid fails closed with the same top-level codes as the Python SDK:prompt_secret_detectedwhen any secret was found,prompt_content_too_largefor the size and count limits,prompt_kind_mismatch,prompt_fragment_cycle,prompt_fragment_depth_exceeded, andprompt_template_invalidotherwise.- Server codes map to the error classes through one explicit table, not by prefix, because no prefix separates them (
execution_binding_conflictis a binding error,prompt_draft_conflictis not). Only the five classes of the minimal surface exist; any other code is a plainApiError. - Registry, verification and render failures of the prompt resources are
ApiError(invalid arguments throwErrororTypeErrorbefore any request):acceptApiJsonmaps the{ error: { code, message, details } }envelope (addingrequest_id,capability,reasonandrequired_plantodetails), a non-JSON error body tohttp_errorand a non-JSON success toinvalid_response, and theToolExecutionErrorthatfetchJsonthrows fortransport_errorandcloud_requiredis re-raised asApiErrorwith the same code and status.tools.tsandprotect.tskeepToolExecutionError. There is no retry and no outage fallback in this SDK; the first failure is raised. A binding answer is checked down to every child pin (a record with a stringprompt_manifest_digest) before the artifacts are compared with it, so a malformed child isinvalid_response, as in Python, and not aTypeError. The artifacts are then compared with the binding in both directions, over the union of the two child id sets: a child the binding pins and the artifacts lack, and a child the artifacts carry and the binding does not pin, both raisemanifest_digest_mismatch(actualorexpectedisnull). A manifest pins a child by release and genome only, so the binding is the document that ties a child manifest digest to the execution, and a child outside it would render prompts no binding vouches for. The registry builds the artifacts from the binding's own children, so an honest answer always has equal sets. - The workspace comes from
GET /v1/whoami, read once per client (aWeakMapkeyed by the client; a failed read is forgotten so the next call asks again). It is never taken from the document being verified, and a version whosecanonical_urinames another workspace raisesworkspace_mismatch. - Aliases are resolved on every call and their target is never cached; verified versions are cached in memory per resource, keyed by workspace and
prompt_id:version, least recently used first out above 1024 entries. bindings.createsends exactlythread_key,scope,selector,runtime_client,includeand, when given,expect, because the registry refuses unknown members, and noIdempotency-Key: the route is create-or-get on the thread key.runtime_client.sdkis theSDK_METADATA.nameprotocol constant andsdk_versionisnull, the package having no version constant.threadKeyandexecutionKeyhashagenomic.thread_key/v1, the workspace and the id with NUL separators, exactly like the Python SDK (the test pins Python's output). An id holding a lone surrogate throwsTypeError, as Python's UTF-8 encoding refuses it, becausenode:cryptowould hash it as U+FFFD and give two different ids the same key, so two threads would share one binding.JsonExchange.headerscarries the response headers for routes that answer anETag; no route of the minimal surface needs it yet.RenderOptions.expectKind,RenderedPrompt.expandedTemplateandRenderedPrompt.renderedDocumentexist for the conformance vectors:kind_mismatchis checked after validation and before variable binding, and the vectors assert the expanded template and the rendered document.scanSecretsbuilds one UTF-16 to code point index per scanned string and reads every finding offset from it. Counting the code points of the prefix for each match made a scan quadratic in the number of findings, so content near the 262144 byte limit filled with short secret-shaped tokens took seconds (tests/prompt-secrets.test.ts).tests/fixtures/spec-vectors/is a byte copy of the SPEC prompt vectors made byscripts/sync-spec-vectors.sh; the harness checksSPEC_VECTORS.lockandMANIFEST.jsonbefore running any vector..gitleaks.tomlkeeps the default rules and allowlists that directory only, because vectors R058, R059, S008, S009 and S014 hold deliberately fake tokens that the secret scanner must detect. Tests build secret-shaped strings at runtime instead of writing them literally.- Registry response goldens are wire shapes, not consistent documents (their manifests and closures disagree), so
tests/prompt-fixtures.tsbuilds bundles, versions and bindings with real digests. examples/prompts-render.tsimports../dist/index.jsand runs withnode --experimental-strip-typesafterpnpm build, like the Protect harness, because Node cannot resolve the extensionless imports ofsrc/.- The code table also maps
bundle_signature_invalidandbundle_ungovernedtoPromptIntegrityErroralthough this SDK never raises them, so a code keeps the class it has in the Python SDK;docs/prompts.mdsays they are never raised here. - A version object the SDK did not build is validated again with its
fragments' kinds taken from their
kind, but only"fragment"is passed on as a prompt kind. A fetched fragment haskind"text"(the registry version answer carries no prompt kind, as in Python), so passing"text"would refuse an unchanged copy withfragment_not_fragment; the pin digest and thefragment_not_textcheck still apply. CHANGELOG.mdstarts with the managed prompts work: the repository had no changelog for 0.1.1 and earlier, and those entries are not reconstructed. It is not shipped:fileslistsdist,README.mdandLICENSE, andnpm packon npm 10.9.8 and 11.21.0 leaves a rootCHANGELOG.mdout, so the packed file set the CI asserts is unchanged.
Knowledge bases
- Scope: the read and publish surface of the Agenomic Cloud knowledge base API plus agent-scoped retrieval (
src/knowledge.ts). Knowledge base creation and settings, collections, document edits, version creation and approval, sources, retrieval events and evaluations are Python only.tests/fixtures/knowledge/holds byte copies of the API contract fixtures; refresh them only when the contract changes. client.knowledgeis aKnowledgeClientbuilt likeclient.prompts: it callsfetchJsonand maps every answer through the promptsacceptApiJson(exported for this, not fromindex.ts), so refusals areApiErrorwith the gateway code and details,transport_errorandcloud_requiredare re-raised asApiError, and invalid arguments throwErrororTypeErrorbefore any request. No typed knowledge error classes exist; the code table is unchanged.- Uploads go through
fetchRaw, which sharesfetchJson's exchange (base URL, bearer, client headers, body parsing) and sends aUint8Array<ArrayBuffer>; the SDK copies the caller's bytes first, so mutating them after the call cannot change what was sent, and the lib DOMBodyInittype refuses aSharedArrayBuffer-backed view.application/octet-streamis the default content type because the gateway treats it as undeclared and infers the format from the path extension. Everyx-agenomic-*header value is percent-encoded UTF-8 as RFC 3986 requires (encodeURIComponentplus!'()*, so the value equals Python'squote(value, safe="")) and the gateway percent-decodes them all; only the document path keeps/unencoded. Tags are encoded one by one and joined with literal commas, so a tag may not hold a comma. - Responses are the contract shapes: lists need their array key (
knowledge_bases,documents,versions,results,matches,retrievals), single records need their wrapper member, and an answer for anotherkb_id, document, section, version, job or agent isinvalid_response. The wire types are hand-mirrored snake_case interfaces; options are camelCase and converted, and request bodies omit absent members because the gateway refuses unknown ones and the filter and execution members are checked against their lists first. - Versions are normalized to numbers (
"v3"and"3"become3) with the server grammar; routes whose path names a version take numbers only. An absent version is left out, so the gateway answers with the published version. publish,rollbackandputAgentKnowledgesendIf-Matchas a quoted integer (publication_generation, the agent configrevision). NoIdempotency-Keyheader is sent; there is no retry in this SDK.KnowledgeBaseHandle.getSectionwith a heading sendsdocumentIdas thedocumentof theget_sectionquery operation in one request (the query route matches the document id first, then path, file name and title) and keeps only matches of that id; no match isknowledge_section_not_foundwith status 0.waitForJobpollsgetJobuntilsucceeded,failedorcancelledand returns the job; pasttimeoutMsit throwsknowledge_job_timeoutwith status 0. Elapsed time is checked before sleeping, sotimeoutMs: 0returns or throws after one read.knowledgeToolreturns a plain{ name, description, parameters, execute }object, with no framework dependency and a JSON schema whose argument names (query,top_k) match the Python tool.executerenders the gateway's delimited context (escaping and the untrusted-data preamble are the server's) and one citation line per result built from ids, path and URI with control characters removed;includeContext: falsereturns citations only. A gateway refusal returnsknowledge search failed: <code>so the model reads it;cloud_requiredand argument errors reject. WithagentId,versionis refused and the execution identity comes from the options merged with the per-callcontext.execution.knowledge.retrievejoinsTrackingEventType(a type union, no runtime allowlist exists in this SDK).
SDK wire conformance (PRA-04, PRA-05, D8, D9)
The gateway DTOs are the source of truth; SDK public signatures stay stable.
/v1/tracesand/v1/traces/batchtake{agent_id: UUID, external_id, payload, events:[{event_type, payload}]}(batch wraps in{items}). SDK envelopes useagent://org/name, so both SDKs resolve the reference to the registered agent UUID (UUID passthrough, explicitagent_ids/agentIdsmap, elseGET /v1/agentsmatching slug or name against the last path segment; cached). Unknown agent raises instead of sending a body the gateway rejects.- Python
upload_bundlesends JSON{agent_id, version, hash:"", metadata, archive_base64}(the server recomputes the Merkle hash and accepts an empty one).versionis a new optional kwarg, default parsed fromname-X.Y.Z.bundle.... - There is no gateway ATEP segment route.
upload_atep_segmentreads the segment and posts each event toPOST /v1/governance/eventswithsource: "atep". get_replay_reportuses/v1/replay-jobs/{id}/report.- Python
TrackingResource.startgainsbaseline,allowed_tools(merged into baseline) andparent_session_id, sent only when set. - TS
TrackingEventTypenow equals the Python vocabulary; the gateway accepts anytypestring. - TS
sendTraceToHttpkeeps posting to the given endpoint URL; the agent lookup derives the base by stripping/v1/traces.
