Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog/unreleased/15528-sdk-ts-parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
type: fix
scope: sdk
issue: 15528
pr: 0
---
The TypeScript SDK now matches the routes it calls: `sessions.list` sends `scope`/`team_id`, `knowledge.search` is a POST with a JSON body, `analytics.usage`/`performance` take no arguments, and several responses are modeled as the flat documents the backend actually returns rather than invented envelopes. Breaking: `agents.getConfig` now requires an `agentId` (the route is per-agent, not global), and `agents.updateConfig` is removed — no backend route ever answered it. `agents.setModel`/`setEnabled` are added to reach the Python SDK's parity. It also gained the `/api` prefix every request needs, which it was missing entirely (#16495) — the same defect #15053 already found once in the Python package. `repo_tests/sdk_ts_request_contract_test.py` pins both packages' requests against the same backend-derived oracle `sdk_request_url_test.py` uses for Python.
32 changes: 19 additions & 13 deletions docs/sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,25 +168,31 @@ listed has no SDK method — use raw HTTP.
| sessions | `create` / `update` / `delete` | Yes | Yes |
| agents | `health` | Yes | Yes |
| agents | `get_config` | Yes | Yes (`getConfig`) |
| agents | `set_model` / `set_enabled` | Yes | No |
| agents | `send_command` | Yes (route defect, see below) | Yes (`sendCommand`) |
| agents | `set_model` / `set_enabled` | Yes | Yes (`setModel` / `setEnabled`) |
| agents | `send_command` | Yes | Yes (`sendCommand`) |
| knowledge | `stats` / `add_text` / `search` | Yes | Yes |
| knowledge | `get_entries` | Yes | Yes (`getEntries`) |
| analytics | `usage` / `performance` | Yes | Yes |
| chat send / stream | — | No | No |
| collections, workflows, models, file upload, voice, multimodal | — | No | No |

Two known defects, both filed rather than papered over:

* `agents.send_command` (both packages) targets `POST /api/agent/execute_command`,
which declares a `dict` body parameter alongside a `Form` field. FastAPI
therefore publishes it as form-encoded while requiring a field that can only
arrive as JSON, so **no client can call it** — every candidate body answers
422. The SDK half waits on the route being fixed.
* The TypeScript package has not had the request-contract corrections the Python
package received (`sessions.list` still sends `limit`/`offset`,
`analytics.usage` still sends `period`, several flat responses are still
modelled as envelopes). Prefer the Python SDK until that lands.
The TypeScript package's request-contract corrections landed in #15528: it
now matches the routes exactly (`sessions.list` sends `scope`/`team_id`,
`knowledge.search` is a POST with a JSON body, `analytics.usage`/
`performance` take no arguments, several responses are flat documents
rather than envelopes) and the `/api` root every request needs, which it
was missing entirely (#16495) -- the same defect #15053 already found once
in the Python package. `repo_tests/sdk_ts_request_contract_test.py` pins
both packages' requests against the same backend-derived oracle
`sdk_request_url_test.py` uses for Python.

One known defect remains, filed rather than papered over: both packages'
`send_command` used to target `POST /api/agent/execute_command` while it
declared a `dict` body parameter alongside a `Form` field, so FastAPI
published it as form-encoded while requiring a field that could only
arrive as JSON -- every candidate body answered 422. Fixed on the route
side (#15527); both SDKs already sent the right shape and needed no
further change.

---

Expand Down
21 changes: 20 additions & 1 deletion libs/autobot-sdk-ts/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@
const DEFAULT_BASE_URL = "http://localhost:8000";
const TOKEN_ENV = "AUTOBOT_API_TOKEN";

// Every core and optional router is mounted by the backend's application
// factory at `/api{registry_prefix}` -- a single loop, no exceptions (#15053,
// #16495 -- the same bug found once already in the Python SDK, and found here
// undiscovered because nothing in this package's default test run makes an
// HTTP call). The only mounts that are NOT under `/api` are the OpenAI/
// Anthropic compatibility routers at `/v1` and the JWKS document at
// `/.well-known`, and this SDK exposes neither. So the prefix is a property
// of the whole surface and belongs here, applied once, rather than repeated
// on every resource path.
const API_PREFIX = "/api";

/** Place a resource path under the backend's API root, once, idempotently.
* `/chat/sessions` -> `/api/chat/sessions`. A path that already carries the
* prefix is not double-prefixed. */
function apiPath(path: string): string {
const normalised = "/" + path.replace(/^\/+/, "");
return normalised === API_PREFIX || normalised.startsWith(`${API_PREFIX}/`) ? normalised : `${API_PREFIX}${normalised}`;
}

export interface ClientOptions {
baseUrl?: string;
token?: string;
Expand Down Expand Up @@ -36,7 +55,7 @@ export class AutoBotHttpClient {
}

private buildUrl(path: string, params?: Record<string, string | number | undefined>): string {
const url = new URL(`${this.baseUrl}${path}`);
const url = new URL(`${this.baseUrl}${apiPath(path)}`);
if (params) {
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null) {
Expand Down
7 changes: 7 additions & 0 deletions libs/autobot-sdk-ts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,25 @@ export { AutoBotHttpClient } from "./client.js";
export type { ClientOptions } from "./client.js";
export type {
AgentConfig,
AgentConfigHealthCheck,
AgentConfigOptions,
AgentHealth,
AnalyticsPerformance,
AnalyticsUsage,
ChatMessage,
DataResponse,
KnowledgeAddResult,
KnowledgeEntries,
KnowledgeEntry,
KnowledgeSearchResult,
KnowledgeStats,
Session,
SessionCreate,
SessionDelete,
SessionDeleteFileHandling,
SessionDeleteKbCleanup,
SessionDeleteTerminalCleanup,
SessionDeleteTranscriptCleanup,
SessionList,
SessionMessages,
SessionUpdate,
Expand Down
35 changes: 29 additions & 6 deletions libs/autobot-sdk-ts/src/resources/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,45 @@
// SPDX-License-Identifier: Apache-2.0
// AutoBot - AI-Powered Automation Platform
// Author: mrveiss
/**
* Agent resource operations.
*
* The agent surface spans two mounts (#15528, mirroring the Python SDK's
* already-fixed shape): the agent router is registered under `/agent` and
* per-agent configuration under `/agent_config`, so `health()` needed a
* `/agent` segment it was missing, and `getConfig`/`setModel`/`setEnabled`
* are all per-agent (`/agent_config/agents/{agentId}/...`), not the
* bare `/agent/config` this used to call with no `agentId` at all.
*
* `updateConfig` is gone rather than fixed: there is no PUT route for
* arbitrary agent config, on either mount. `setModel`/`setEnabled` are the
* two ways an agent's config can actually be changed.
*/
import type { AutoBotHttpClient } from "../client.js";
import type { AgentConfig, AgentHealth, DataResponse } from "../types.js";
import type { AgentConfig, AgentHealth } from "../types.js";

export class AgentsResource {
constructor(private readonly client: AutoBotHttpClient) {}

health(): Promise<AgentHealth> {
return this.client.get("/health/detailed");
return this.client.get("/agent/health/detailed");
}

getConfig(): Promise<DataResponse<AgentConfig>> {
return this.client.get("/agent/config");
/** Configuration of one agent. The response is a flat document, not a
* `DataResponse` envelope. */
getConfig(agentId: string): Promise<AgentConfig> {
return this.client.get(`/agent_config/agents/${agentId}`);
}

updateConfig(fields: Partial<AgentConfig>): Promise<DataResponse<AgentConfig>> {
return this.client.put("/agent/config", fields);
setModel(agentId: string, model: string, provider?: string): Promise<Record<string, unknown>> {
const body: Record<string, unknown> = { agent_id: agentId, model };
if (provider) body["provider"] = provider;
return this.client.post(`/agent_config/agents/${agentId}/model`, body);
}

setEnabled(agentId: string, enabled: boolean): Promise<Record<string, unknown>> {
const action = enabled ? "enable" : "disable";
return this.client.post(`/agent_config/agents/${agentId}/${action}`);
}

// #15527: the route reads `command` only. `sessionId` named nothing it has,
Expand Down
27 changes: 22 additions & 5 deletions libs/autobot-sdk-ts/src/resources/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,34 @@
// SPDX-License-Identifier: Apache-2.0
// AutoBot - AI-Powered Automation Platform
// Author: mrveiss
/**
* Analytics resource operations.
*
* `/analytics/usage` and `/analytics/performance` are not routes: the
* analytics router serves `usage/statistics` and `performance/metrics`
* under its `/analytics` mount (#15528, mirroring the Python SDK's
* already-fixed shape).
*
* Neither route wraps its body in a `DataResponse` envelope, and neither
* declares a `period` query parameter -- both methods used to send one on
* every call; FastAPI ignored it, so the window a caller asked for was
* never the window they got. The argument is gone rather than renamed:
* there is no parameter on either route to map it to.
*/
import type { AutoBotHttpClient } from "../client.js";
import type { AnalyticsPerformance, AnalyticsUsage, DataResponse } from "../types.js";
import type { AnalyticsPerformance, AnalyticsUsage } from "../types.js";

export class AnalyticsResource {
constructor(private readonly client: AutoBotHttpClient) {}

usage(period = "day"): Promise<DataResponse<AnalyticsUsage>> {
return this.client.get("/analytics/usage", { period });
/** Usage statistics over the collector's own window. The route takes no
* arguments, so neither does this. */
usage(): Promise<AnalyticsUsage> {
return this.client.get("/analytics/usage/statistics");
}

performance(period = "day"): Promise<DataResponse<AnalyticsPerformance>> {
return this.client.get("/analytics/performance", { period });
/** Performance metrics over the collector's own window. */
performance(): Promise<AnalyticsPerformance> {
return this.client.get("/analytics/performance/metrics");
}
}
53 changes: 40 additions & 13 deletions libs/autobot-sdk-ts/src/resources/knowledge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,57 @@
// SPDX-License-Identifier: Apache-2.0
// AutoBot - AI-Powered Automation Platform
// Author: mrveiss
/**
* Knowledge resource operations.
*
* `/knowledge_base/search` is served by POST only; this used to ask for it
* with GET, which answers 405 even once the path is right. The verb is part
* of the route, so it's corrected here (#15528, mirroring the Python SDK's
* already-fixed shape).
*
* None of these four routes returns a `DataResponse` envelope -- each
* returns its document flat.
*/
import type { AutoBotHttpClient } from "../client.js";
import type {
DataResponse,
KnowledgeAddResult,
KnowledgeSearchResult,
KnowledgeStats,
} from "../types.js";
import type { KnowledgeAddResult, KnowledgeEntries, KnowledgeSearchResult, KnowledgeStats } from "../types.js";

const DEFAULT_SEARCH_LIMIT = 10;
const DEFAULT_PAGE_SIZE = 50;

export class KnowledgeResource {
constructor(private readonly client: AutoBotHttpClient) {}

stats(): Promise<DataResponse<KnowledgeStats>> {
stats(): Promise<KnowledgeStats> {
return this.client.get("/knowledge_base/stats");
}

addText(text: string, category?: string, source?: string): Promise<DataResponse<KnowledgeAddResult>> {
return this.client.post("/knowledge_base/add_text", { text, category, source });
addText(text: string, category?: string, source?: string): Promise<KnowledgeAddResult> {
const body: Record<string, unknown> = { text };
if (category) body["category"] = category;
if (source) body["source"] = source;
return this.client.post("/knowledge_base/add_text", body);
}

search(query: string, limit = 10, category?: string): Promise<DataResponse<KnowledgeSearchResult>> {
return this.client.get("/knowledge_base/search", { query, limit, category });
/**
* Search the knowledge base.
*
* The route takes its arguments in a JSON body, and names the result cap
* `limit` (the route also accepts the legacy wire name `top_k`, but this
* SDK only ever sent `limit`). It has no category filter -- use
* {@link getEntries}, whose route does.
*/
search(query: string, limit = DEFAULT_SEARCH_LIMIT): Promise<KnowledgeSearchResult> {
return this.client.post("/knowledge_base/search", { query, limit });
}

getEntries(limit = 50, offset = 0, category?: string): Promise<DataResponse<KnowledgeSearchResult>> {
return this.client.get("/knowledge_base/entries", { limit, offset, category });
/**
* One page of stored entries, newest first.
*
* The route is **cursor**-paginated, not offset-paginated (#15528). Pass
* the previous response's `next_cursor` to advance; omitting it starts at
* the beginning.
*/
getEntries(limit = DEFAULT_PAGE_SIZE, cursor?: string, category?: string): Promise<KnowledgeEntries> {
return this.client.get("/knowledge_base/entries", { limit, cursor, category });
}
}
33 changes: 29 additions & 4 deletions libs/autobot-sdk-ts/src/resources/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@
// SPDX-License-Identifier: Apache-2.0
// AutoBot - AI-Powered Automation Platform
// Author: mrveiss
/**
* Session resource operations.
*
* `list()` used to send `limit`/`offset`. `GET /chat/sessions` accepts
* neither -- it declares `scope` and `team_id` and returns the caller's
* whole list -- so FastAPI dropped both and the caller's paging silently
* did not apply (#15528, mirroring #15119's already-fixed Python shape).
* The two parameters the route does take are offered instead. `get()`
* gains the route's real `page`/`per_page`, which the SDK could not reach
* at all.
*/
import type { AutoBotHttpClient } from "../client.js";
import type {
DataResponse,
Expand All @@ -15,12 +26,26 @@ import type {
export class SessionsResource {
constructor(private readonly client: AutoBotHttpClient) {}

list(limit = 50, offset = 0): Promise<DataResponse<SessionList>> {
return this.client.get("/chat/sessions", { limit, offset });
/**
* Chat sessions visible to the caller.
*
* `scope` is `"user"` (the route's default), `"org"`, `"team"` or
* `"shared"`; `teamId` is required when `scope="team"`. The route is not
* paginated, which is why there is no `limit`/`offset` here (#15528).
*/
list(scope?: string, teamId?: string): Promise<DataResponse<SessionList>> {
return this.client.get("/chat/sessions", { scope, team_id: teamId });
}

get(sessionId: string): Promise<DataResponse<SessionMessages>> {
return this.client.get(`/chat/sessions/${sessionId}`);
/**
* One session's messages, page by page.
*
* `page`/`perPage` are the route's own parameter names; omitting either
* leaves the route's default in force rather than restating it here, so
* the two cannot drift apart.
*/
get(sessionId: string, page?: number, perPage?: number): Promise<DataResponse<SessionMessages>> {
return this.client.get(`/chat/sessions/${sessionId}`, { page, per_page: perPage });
}

create(title?: string, metadata?: Record<string, unknown>): Promise<DataResponse<SessionCreate>> {
Expand Down
Loading
Loading