From 82a2aff973b1e4ee3d9d36bd89f447e69929ed5f Mon Sep 17 00:00:00 2001 From: epanonymous Date: Tue, 16 Jun 2026 09:31:03 +0000 Subject: [PATCH 1/2] feat(resources): add generic resources namespace --- src/agent.ts | 2 ++ src/deva-client.ts | 3 ++ src/errors.ts | 10 +++++-- src/resources/index.ts | 1 + src/resources/resources.ts | 53 +++++++++++++++++++++++++++++++++ src/types.ts | 61 ++++++++++++++++++++++++++++++++++++-- 6 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 src/resources/resources.ts diff --git a/src/agent.ts b/src/agent.ts index bbcde79..4dadedc 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -22,6 +22,7 @@ export class DevaAgent { public readonly models: DevaClient["models"]; public readonly discover: DevaClient["discover"]; public readonly profile: DevaClient["profile"]; + public readonly resources: DevaClient["resources"]; public readonly wallet: DevaClient["wallet"]; public readonly embeddings: DevaClient["embeddings"]; public readonly transcription: DevaClient["transcription"]; @@ -46,6 +47,7 @@ export class DevaAgent { this.models = this.client.models; this.discover = this.client.discover; this.profile = this.client.profile; + this.resources = this.client.resources; this.wallet = this.client.wallet; this.embeddings = this.client.embeddings; this.transcription = this.client.transcription; diff --git a/src/deva-client.ts b/src/deva-client.ts index 0397097..b4238d1 100644 --- a/src/deva-client.ts +++ b/src/deva-client.ts @@ -9,6 +9,7 @@ import { KvResource } from "./resources/kv.js"; import { MessagingResource } from "./resources/messaging.js"; import { ModelsResource } from "./resources/models.js"; import { ProfileResource } from "./resources/profile.js"; +import { ResourcesResource } from "./resources/resources.js"; import { SearchResource } from "./resources/search.js"; import { SocialResource } from "./resources/social.js"; import { TranscriptionResource } from "./resources/transcription.js"; @@ -31,6 +32,7 @@ export class DevaClient { public readonly models: ModelsResource; public readonly discover: DiscoverResource; public readonly profile: ProfileResource; + public readonly resources: ResourcesResource; public readonly wallet: WalletResource; public readonly embeddings: EmbeddingsResource; public readonly transcription: TranscriptionResource; @@ -50,6 +52,7 @@ export class DevaClient { this.models = new ModelsResource(this.http); this.discover = new DiscoverResource(this.http); this.profile = new ProfileResource(this.http); + this.resources = new ResourcesResource(this.http); this.wallet = new WalletResource(this.http); this.embeddings = new EmbeddingsResource(this.http); this.transcription = new TranscriptionResource(this.http); diff --git a/src/errors.ts b/src/errors.ts index 8e01b5a..9f7f19a 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -101,8 +101,14 @@ export function normalizeErrorEnvelope(status: number, body: unknown): Normalize if (body && typeof body === "object") { const root = body as Record; - const err = (root.error ?? root) as Record; - code = typeof err.code === "string" ? err.code : undefined; + const detail = root.detail; + const err = + root.error && typeof root.error === "object" + ? (root.error as Record) + : detail && typeof detail === "object" + ? (detail as Record) + : root; + code = typeof err.code === "string" ? err.code : typeof err.error === "string" ? err.error : undefined; message = typeof err.message === "string" ? err.message : undefined; type = typeof err.type === "string" ? err.type : undefined; details = err.details; diff --git a/src/resources/index.ts b/src/resources/index.ts index f64d8b8..41a4013 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -7,6 +7,7 @@ export * from "./kv.js"; export * from "./messaging.js"; export * from "./models.js"; export * from "./profile.js"; +export * from "./resources.js"; export * from "./search.js"; export * from "./social.js"; export * from "./transcription.js"; diff --git a/src/resources/resources.ts b/src/resources/resources.ts new file mode 100644 index 0000000..8e722d1 --- /dev/null +++ b/src/resources/resources.ts @@ -0,0 +1,53 @@ +import { DevaHttpClient } from "../client.js"; +import type { + ResourceCatalogResponse, + ResourceEstimateResponse, + ResourceInfo, + ResourceRunResponse +} from "../types.js"; + +export type ResourceRunParams = Record; +export type ResourceEstimateParams = Record; + +/** Generic discover, inspect, estimate, and run APIs for agent resources. */ +export class ResourcesResource { + constructor(private readonly client: DevaHttpClient) {} + + /** Lists the authenticated resource catalog via `GET /v1/agents/resources/catalog`. */ + async list(): Promise { + const response = await this.client.request({ + method: "GET", + path: "/v1/agents/resources/catalog" + }); + return response.resources; + } + + /** Inspects a single resource via `GET /v1/agents/resources/catalog/{resource_id}`. */ + inspect(resourceId: string): Promise { + return this.client.request({ + method: "GET", + path: `/v1/agents/resources/catalog/${encodeURIComponent(resourceId)}` + }); + } + + /** Estimates the karma cost for a planned resource call via `POST /v1/agents/resources/estimate`. */ + estimate(resourceId: string, params: ResourceEstimateParams = {}): Promise { + return this.client.request({ + method: "POST", + path: "/v1/agents/resources/estimate", + body: { + resource_id: resourceId, + params + } + }); + } + + /** Runs a generic-supported resource via `POST /v1/agents/resources/{resource_id}/run`. */ + run>(resourceId: string, params: ResourceRunParams): Promise> { + return this.client.request>({ + method: "POST", + path: `/v1/agents/resources/${encodeURIComponent(resourceId)}/run`, + body: params + }); + } +} diff --git a/src/types.ts b/src/types.ts index a2ef8ae..4971c6f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,14 +1,69 @@ export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; -/** Resource availability status returned by the catalog endpoint. */ -export type ResourceStatus = "AVAILABLE" | "UNAVAILABLE" | "DEGRADED"; - export type JsonPrimitive = string | number | boolean | null; export type JsonValue = JsonPrimitive | JsonObject | JsonValue[]; export interface JsonObject { [key: string]: JsonValue; } +/** Resource category returned by the agent resources catalog. */ +export type ResourceCategory = "ai" | "infrastructure" | "communication" | "utility" | "storage"; + +/** Resource availability status returned by the agent resources catalog. */ +export type ResourceStatus = "available" | "degraded" | "unavailable" | "coming_soon" | "maintenance"; + +export type JsonSchema = JsonObject; + +export interface ResourcePricing { + unit: string; + karma_cost: number; + details?: string | null; + [key: string]: unknown; +} + +export interface ResourceInfo { + id: string; + name: string; + category: ResourceCategory; + description: string; + endpoint: string; + provider?: string | null; + method: string; + pricing: ResourcePricing; + status: ResourceStatus; + auth_required: boolean; + rate_limit?: string | null; + input_schema: JsonSchema; + output_description: string; + supports_generic_run: boolean; + run_endpoint?: string | null; + [key: string]: unknown; +} + +export interface ResourceCatalogResponse { + success?: boolean; + resources: ResourceInfo[]; + total?: number; + [key: string]: unknown; +} + +export interface ResourceEstimateResponse { + success?: boolean; + resource_id: string; + estimated_karma_cost: number; + unit: string; + breakdown?: JsonObject | null; + [key: string]: unknown; +} + +export interface ResourceRunResponse> { + success?: boolean; + resource_id: string; + karma_charged: number; + result: T; + [key: string]: unknown; +} + export interface DevaClientOptions { apiKey?: string; apiBase?: string; From faee781255b853374dca6a2f48bdeac67a293058 Mon Sep 17 00:00:00 2001 From: epanonymous Date: Tue, 16 Jun 2026 09:31:14 +0000 Subject: [PATCH 2/2] test(resources): cover discover inspect run rail --- README.md | 17 +++++ test/resources.test.ts | 146 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 test/resources.test.ts diff --git a/README.md b/README.md index c2447d4..66a7f98 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,23 @@ const x = await agent.search.x({ query: "deva ai", max_results: 10 }); const tweets = await agent.search.xUserTweets({ username: "deva_ai", limit: 5 }); ``` +### Resources (`/v1/agents/resources/catalog`, `/run`, `/estimate`) + +Discover the authenticated catalog, inspect a resource's JSON Schema, estimate cost, then run supported resources through the generic rail. + +```ts +const catalog = await agent.resources.list(); +const webSearch = await agent.resources.inspect("web_search"); + +if (webSearch.supports_generic_run) { + const params = { query: "latest Deva agent resources", count: 5 }; + const estimate = await agent.resources.estimate(webSearch.id, params); + const run = await agent.resources.run(webSearch.id, params); + + console.log(estimate.estimated_karma_cost, run.karma_charged, run.result); +} +``` + ### Files (`/v1/agents/files/*`) ```ts diff --git a/test/resources.test.ts b/test/resources.test.ts new file mode 100644 index 0000000..c42b502 --- /dev/null +++ b/test/resources.test.ts @@ -0,0 +1,146 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { DevaClient, InvalidRequestError } from "../dist/esm/index.js"; + +interface CapturedRequest { + url: string; + method?: string; + body?: unknown; + authorization?: string | null; +} + +function captureFetch(responseBody: unknown, status = 200): { fetch: typeof fetch; requests: CapturedRequest[] } { + const requests: CapturedRequest[] = []; + const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => { + const rawBody = typeof init?.body === "string" ? JSON.parse(init.body) : undefined; + requests.push({ + url: String(input), + method: init?.method, + body: rawBody, + authorization: new Headers(init?.headers).get("authorization") + }); + + return new Response(JSON.stringify(responseBody), { + status, + headers: { "content-type": "application/json" } + }); + }) as unknown as typeof fetch; + + return { fetch: fetchImpl, requests }; +} + +const WEB_SEARCH_RESOURCE = { + id: "web_search", + name: "Web Search", + category: "utility", + description: "Search the web", + endpoint: "/v1/agents/resources/search", + provider: "openrouter", + method: "POST", + pricing: { unit: "per search", karma_cost: 8 }, + status: "available", + auth_required: true, + rate_limit: null, + input_schema: { + type: "object", + properties: { + query: { type: "string" }, + count: { type: "integer", minimum: 1, maximum: 20 } + }, + required: ["query"] + }, + output_description: "Search results, result count, normalized query, and karma_cost.", + supports_generic_run: true, + run_endpoint: "/v1/agents/resources/web_search/run" +}; + +test("resources.list GETs the catalog and returns ResourceInfo[]", async () => { + const r = captureFetch({ success: true, resources: [WEB_SEARCH_RESOURCE], total: 1 }); + const client = new DevaClient({ apiBase: "http://localhost:8000", apiKey: "deva_test", fetch: r.fetch }); + + const resources = await client.resources.list(); + + assert.deepEqual(resources, [WEB_SEARCH_RESOURCE]); + assert.equal(r.requests[0].method, "GET"); + assert.equal(r.requests[0].authorization, "Bearer deva_test"); + assert.ok(r.requests[0].url.endsWith("/v1/agents/resources/catalog"), r.requests[0].url); +}); + +test("resources.inspect GETs /catalog/{resource_id}", async () => { + const r = captureFetch(WEB_SEARCH_RESOURCE); + const client = new DevaClient({ apiBase: "http://localhost:8000", apiKey: "deva_test", fetch: r.fetch }); + + const resource = await client.resources.inspect("web_search"); + + assert.equal(resource.id, "web_search"); + assert.equal(resource.supports_generic_run, true); + assert.deepEqual(resource.input_schema.properties?.query, WEB_SEARCH_RESOURCE.input_schema.properties.query); + assert.equal(r.requests[0].method, "GET"); + assert.ok(r.requests[0].url.endsWith("/v1/agents/resources/catalog/web_search"), r.requests[0].url); +}); + +test("resources.estimate POSTs the estimate envelope", async () => { + const r = captureFetch({ + success: true, + resource_id: "web_search", + estimated_karma_cost: 8, + unit: "per search", + breakdown: { count: 5 } + }); + const client = new DevaClient({ apiBase: "http://localhost:8000", apiKey: "deva_test", fetch: r.fetch }); + + const estimate = await client.resources.estimate("web_search", { query: "deva", count: 5 }); + + assert.equal(estimate.estimated_karma_cost, 8); + assert.equal(r.requests[0].method, "POST"); + assert.deepEqual(r.requests[0].body, { + resource_id: "web_search", + params: { query: "deva", count: 5 } + }); + assert.ok(r.requests[0].url.endsWith("/v1/agents/resources/estimate"), r.requests[0].url); +}); + +test("resources.run POSTs params to /{resource_id}/run", async () => { + const r = captureFetch({ + success: true, + resource_id: "web_search", + karma_charged: 8, + result: { + results: [{ title: "Deva", url: "https://deva.me" }], + karma_cost: 8 + } + }); + const client = new DevaClient({ apiBase: "http://localhost:8000", apiKey: "deva_test", fetch: r.fetch }); + + const run = await client.resources.run("web_search", { query: "deva", count: 1 }); + + assert.equal(run.resource_id, "web_search"); + assert.equal(run.karma_charged, 8); + assert.equal(run.result.karma_cost, 8); + assert.equal(r.requests[0].method, "POST"); + assert.deepEqual(r.requests[0].body, { query: "deva", count: 1 }); + assert.ok(r.requests[0].url.endsWith("/v1/agents/resources/web_search/run"), r.requests[0].url); +}); + +test("resources.run against an unsupported id throws InvalidRequestError with backend code", async () => { + const r = captureFetch( + { + detail: { + error: "RESOURCE_RUN_UNSUPPORTED", + message: "Generic run is not supported for this resource yet." + } + }, + 400 + ); + const client = new DevaClient({ apiBase: "http://localhost:8000", apiKey: "deva_test", fetch: r.fetch }); + + await assert.rejects( + () => client.resources.run("email", { to: ["user@example.com"], subject: "Hi", body_text: "Hello" }), + (error: unknown) => + error instanceof InvalidRequestError && + error.status === 400 && + error.code === "RESOURCE_RUN_UNSUPPORTED" && + error.message === "Generic run is not supported for this resource yet." + ); + assert.ok(r.requests[0].url.endsWith("/v1/agents/resources/email/run"), r.requests[0].url); +});