@mondaydotcomorg/atp-client
Version:
Client SDK for Agent Tool Protocol
1,578 lines (1,567 loc) • 50.1 kB
JavaScript
import { ExecutionStatus, CallbackType, ToolOperation } from '@mondaydotcomorg/atp-protocol';
export { ExecutionStatus } from '@mondaydotcomorg/atp-protocol';
import { log } from '@mondaydotcomorg/atp-runtime';
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x)(function(x) {
if (typeof require !== "undefined") return require.apply(this, arguments);
throw Error('Dynamic require of "' + x + '" is not supported');
});
// src/core/session.ts
var ClientSession = class {
static {
__name(this, "ClientSession");
}
baseUrl;
customHeaders;
clientId;
clientToken;
initPromise;
hooks;
constructor(baseUrl, headers, hooks) {
this.baseUrl = baseUrl;
this.customHeaders = headers || {};
this.hooks = hooks;
}
/**
* Initializes the client session with the server.
* This MUST be called before any other operations.
* The server generates and returns a unique client ID and token.
* @param clientInfo - Optional client information
* @param tools - Optional client tool definitions to register with the server
* @param services - Optional client service capabilities (LLM, approval, embedding)
*/
async init(clientInfo, tools, services) {
if (this.initPromise) {
await this.initPromise;
return {
clientId: this.clientId,
token: this.clientToken,
expiresAt: 0,
tokenRotateAt: 0
};
}
this.initPromise = (async () => {
const url = `${this.baseUrl}/api/init`;
const body = JSON.stringify({
clientInfo,
tools: tools || [],
services
});
const headers = await this.prepareHeaders("POST", url, body);
const response = await fetch(url, {
method: "POST",
headers,
body
});
if (!response.ok) {
throw new Error(`Client initialization failed: ${response.status} ${response.statusText}`);
}
const data = await response.json();
this.clientId = data.clientId;
this.clientToken = data.token;
})();
await this.initPromise;
return {
clientId: this.clientId,
token: this.clientToken,
expiresAt: 0,
tokenRotateAt: 0
};
}
/**
* Gets the unique client ID.
*/
getClientId() {
if (!this.clientId) {
throw new Error("Client not initialized. Call init() first.");
}
return this.clientId;
}
/**
* Ensures the client is initialized before making requests.
*/
async ensureInitialized() {
if (!this.clientId) {
throw new Error("Client not initialized. Call init() first.");
}
}
/**
* Creates HTTP headers for requests.
*/
getHeaders() {
const headers = {
"Content-Type": "application/json",
...this.customHeaders
};
if (this.clientId) {
headers["X-Client-ID"] = this.clientId;
}
if (this.clientToken) {
headers["Authorization"] = `Bearer ${this.clientToken}`;
}
return headers;
}
getBaseUrl() {
return this.baseUrl;
}
/**
* Updates the client token from response headers (token refresh).
*/
updateToken(response) {
const newToken = response.headers.get("X-ATP-Token");
if (newToken) {
this.clientToken = newToken;
}
}
/**
* Prepares headers for a request, calling preRequest hook if configured
*/
async prepareHeaders(method, url, body) {
let headers = {
"Content-Type": "application/json",
...this.customHeaders
};
if (this.clientId) {
headers["X-Client-ID"] = this.clientId;
}
if (this.clientToken) {
headers["Authorization"] = `Bearer ${this.clientToken}`;
}
if (this.hooks?.preRequest) {
try {
const result = await this.hooks.preRequest({
url,
method,
currentHeaders: headers,
body
});
if (result.abort) {
throw new Error(result.abortReason || "Request aborted by preRequest hook");
}
if (result.headers) {
headers = result.headers;
}
} catch (error) {
throw error;
}
}
return headers;
}
};
// src/core/in-process-session.ts
var InProcessSession = class {
static {
__name(this, "InProcessSession");
}
server;
clientId;
clientToken;
initialized = false;
initPromise;
constructor(server) {
this.server = server;
}
async init(clientInfo, tools, services) {
if (this.initPromise) {
await this.initPromise;
return {
clientId: this.clientId,
token: this.clientToken,
expiresAt: 0,
tokenRotateAt: 0
};
}
this.initPromise = (async () => {
await this.server.start();
const ctx = this.createContext({
method: "POST",
path: "/api/init",
body: {
clientInfo,
tools: tools || [],
services
}
});
const result = await this.server.handleInit(ctx);
this.clientId = result.clientId;
this.clientToken = result.token;
this.initialized = true;
})();
await this.initPromise;
return {
clientId: this.clientId,
token: this.clientToken,
expiresAt: 0,
tokenRotateAt: 0
};
}
getClientId() {
if (!this.clientId) {
throw new Error("Client not initialized. Call init() first.");
}
return this.clientId;
}
async ensureInitialized() {
if (!this.initialized) {
throw new Error("Client not initialized. Call init() first.");
}
}
getHeaders() {
const headers = {
"content-type": "application/json"
};
if (this.clientId) {
headers["x-client-id"] = this.clientId;
}
if (this.clientToken) {
headers["authorization"] = `Bearer ${this.clientToken}`;
}
return headers;
}
getBaseUrl() {
return "";
}
updateToken(_response) {
}
async prepareHeaders(_method, _url, _body) {
return this.getHeaders();
}
async getDefinitions(options) {
await this.ensureInitialized();
const ctx = this.createContext({
method: "GET",
path: "/api/definitions",
query: options?.apiGroups ? {
apiGroups: options.apiGroups.join(",")
} : {}
});
return await this.server.getDefinitions(ctx);
}
async getRuntimeDefinitions(options) {
await this.ensureInitialized();
const ctx = this.createContext({
method: "GET",
path: "/api/runtime",
query: options?.apis?.length ? {
apis: options.apis.join(",")
} : {}
});
return await this.server.getRuntimeDefinitions(ctx);
}
async getServerInfo() {
await this.ensureInitialized();
return this.server.getInfo();
}
async search(query, options) {
await this.ensureInitialized();
const ctx = this.createContext({
method: "POST",
path: "/api/search",
body: {
query,
...options
}
});
return await this.server.handleSearch(ctx);
}
async explore(path) {
await this.ensureInitialized();
const ctx = this.createContext({
method: "POST",
path: "/api/explore",
body: {
path
}
});
return await this.server.handleExplore(ctx);
}
async execute(code, config) {
await this.ensureInitialized();
const ctx = this.createContext({
method: "POST",
path: "/api/execute",
body: {
code,
config
}
});
return await this.server.handleExecute(ctx);
}
async resume(executionId, callbackResult) {
await this.ensureInitialized();
const ctx = this.createContext({
method: "POST",
path: `/api/resume/${executionId}`,
body: {
result: callbackResult
}
});
return await this.server.handleResume(ctx, executionId);
}
async resumeWithBatchResults(executionId, batchResults) {
await this.ensureInitialized();
const ctx = this.createContext({
method: "POST",
path: `/api/resume/${executionId}`,
body: {
results: batchResults
}
});
return await this.server.handleResume(ctx, executionId);
}
createContext(options) {
const noopLogger = {
debug: /* @__PURE__ */ __name(() => {
}, "debug"),
info: /* @__PURE__ */ __name(() => {
}, "info"),
warn: /* @__PURE__ */ __name(() => {
}, "warn"),
error: /* @__PURE__ */ __name(() => {
}, "error")
};
return {
method: options.method,
path: options.path,
query: options.query || {},
headers: this.getHeaders(),
body: options.body,
clientId: this.clientId,
clientToken: this.clientToken,
logger: noopLogger,
status: 200,
responseBody: null,
throw: /* @__PURE__ */ __name((status, message) => {
const error = new Error(message);
error.status = status;
throw error;
}, "throw"),
assert: /* @__PURE__ */ __name((condition, message) => {
if (!condition) {
throw new Error(message);
}
}, "assert"),
set: /* @__PURE__ */ __name(() => {
}, "set")
};
}
};
// src/core/api-operations.ts
var APIOperations = class {
static {
__name(this, "APIOperations");
}
session;
inProcessSession;
apiDefinitions;
constructor(session, inProcessSession) {
this.session = session;
this.inProcessSession = inProcessSession;
}
/**
* Connects to the server and retrieves API definitions.
*/
async connect(options) {
await this.session.ensureInitialized();
if (this.inProcessSession) {
const data2 = await this.inProcessSession.getDefinitions(options);
this.apiDefinitions = data2.typescript;
return {
serverVersion: data2.version,
capabilities: {},
apiGroups: data2.apiGroups
};
}
const params = new URLSearchParams();
if (options?.apiGroups) {
params.set("apiGroups", options.apiGroups.join(","));
}
const url = `${this.session.getBaseUrl()}/api/definitions?${params}`;
const headers = await this.session.prepareHeaders("GET", url);
const response = await fetch(url, {
headers
});
if (!response.ok) {
throw new Error(`Connection failed: ${response.status} ${response.statusText}`);
}
const data = await response.json();
this.apiDefinitions = data.typescript;
return {
serverVersion: data.version,
capabilities: {},
apiGroups: data.apiGroups
};
}
/**
* Gets the TypeScript type definitions for available APIs.
*/
getTypeDefinitions() {
if (!this.apiDefinitions) {
throw new Error("Not connected. Call connect() first.");
}
return this.apiDefinitions;
}
/**
* Searches for available API functions.
*/
async searchAPI(query, options) {
await this.session.ensureInitialized();
if (this.inProcessSession) {
const data2 = await this.inProcessSession.search(query, options);
return data2.results;
}
const url = `${this.session.getBaseUrl()}/api/search`;
const body = JSON.stringify({
query,
...options
});
const headers = await this.session.prepareHeaders("POST", url, body);
const response = await fetch(url, {
method: "POST",
headers,
body
});
if (!response.ok) {
throw new Error(`Search failed: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return data.results;
}
/**
* Explores the API filesystem at the given path.
*/
async exploreAPI(path) {
await this.session.ensureInitialized();
if (this.inProcessSession) {
return await this.inProcessSession.explore(path);
}
const url = `${this.session.getBaseUrl()}/api/explore`;
const body = JSON.stringify({
path
});
const headers = await this.session.prepareHeaders("POST", url, body);
const response = await fetch(url, {
method: "POST",
headers,
body
});
if (!response.ok) {
throw new Error(`Explore failed: ${response.status} ${response.statusText}`);
}
return await response.json();
}
/**
* Gets information about the server.
*/
async getServerInfo() {
await this.session.ensureInitialized();
if (this.inProcessSession) {
return await this.inProcessSession.getServerInfo();
}
const url = `${this.session.getBaseUrl()}/api/info`;
const headers = await this.session.prepareHeaders("GET", url);
const response = await fetch(url, {
headers
});
if (!response.ok) {
throw new Error(`Failed to get server info: ${response.status}`);
}
return await response.json();
}
/**
* Gets ATP runtime API definitions as TypeScript declarations.
* Returns the full TypeScript definitions for atp.llm.*, atp.cache.*, etc.
* These are the APIs available during code execution.
*
* Behavior:
* - No options: Returns APIs based on client capabilities (default filtering)
* - apis: ['llm', 'cache']: Returns only specified APIs (intersection with client capabilities)
* - apis: []: Returns all APIs regardless of client capabilities
*
* @param options - Optional filtering options
* @param options.apis - Specific APIs to include (e.g., ['llm', 'cache', 'approval'])
*/
async getRuntimeDefinitions(options) {
await this.session.ensureInitialized();
if (this.inProcessSession) {
return await this.inProcessSession.getRuntimeDefinitions(options?.apis ? {
apis: options.apis
} : void 0);
}
const params = new URLSearchParams();
if (options?.apis && options.apis.length > 0) {
params.set("apis", options.apis.join(","));
}
const url = `${this.session.getBaseUrl()}/api/runtime${params.toString() ? `?${params}` : ""}`;
const headers = await this.session.prepareHeaders("GET", url);
const response = await fetch(url, {
headers
});
if (!response.ok) {
throw new Error(`Failed to get runtime definitions: ${response.status}`);
}
return await response.text();
}
};
// src/errors.ts
var ClientCallbackError = class extends Error {
static {
__name(this, "ClientCallbackError");
}
constructor(message) {
super(message);
this.name = "ClientCallbackError";
}
};
// src/core/provenance-registry.ts
var ProvenanceTokenRegistry = class {
static {
__name(this, "ProvenanceTokenRegistry");
}
cache = /* @__PURE__ */ new Map();
maxSize;
ttl;
sequenceCounter = 0;
constructor(maxSize = 1e4, ttlHours = 1) {
this.maxSize = maxSize;
this.ttl = ttlHours * 3600 * 1e3;
}
/**
* Add a token to the registry
*/
add(token) {
this.evictExpired();
if (this.cache.size >= this.maxSize) {
this.evictLRU();
}
this.cache.set(token, {
token,
addedAt: Date.now(),
sequence: this.sequenceCounter++
});
}
/**
* Get recent tokens (non-expired, sorted by age, limited)
* Returns tokens in chronological order (oldest first, most recent last)
*/
getRecentTokens(maxCount = 1e3) {
if (maxCount <= 0) {
return [];
}
this.evictExpired();
const now = Date.now();
const expiredTokens = [];
const entries = Array.from(this.cache.values()).filter((entry) => {
try {
const [body] = entry.token.split(".");
if (!body) {
expiredTokens.push(entry.token);
return false;
}
const payload = JSON.parse(Buffer.from(body, "base64url").toString());
if (!payload.expiresAt || payload.expiresAt <= now) {
expiredTokens.push(entry.token);
return false;
}
return true;
} catch {
expiredTokens.push(entry.token);
return false;
}
}).sort((a, b) => a.sequence - b.sequence).slice(-maxCount);
for (const token of expiredTokens) {
this.cache.delete(token);
}
return entries.map((e) => e.token);
}
/**
* Clear all tokens
*/
clear() {
this.cache.clear();
}
/**
* Get registry size
*/
size() {
return this.cache.size;
}
/**
* Evict expired tokens
*/
evictExpired() {
const now = Date.now();
const toDelete = [];
for (const [token, entry] of this.cache.entries()) {
if (now - entry.addedAt > this.ttl) {
toDelete.push(token);
}
}
for (const token of toDelete) {
this.cache.delete(token);
}
}
/**
* Evict least recently used (oldest) token
*/
evictLRU() {
let oldestToken = null;
let oldestSequence = Infinity;
for (const [token, entry] of this.cache.entries()) {
if (entry.sequence < oldestSequence) {
oldestSequence = entry.sequence;
oldestToken = token;
}
}
if (oldestToken) {
this.cache.delete(oldestToken);
}
}
};
// src/core/execution-operations.ts
var ExecutionOperations = class {
static {
__name(this, "ExecutionOperations");
}
session;
inProcessSession;
serviceProviders;
tokenRegistry;
lastExecutionConfig = null;
constructor(session, serviceProviders, inProcessSession) {
this.session = session;
this.inProcessSession = inProcessSession;
this.serviceProviders = serviceProviders;
this.tokenRegistry = new ProvenanceTokenRegistry();
}
/**
* Executes code on the server with real-time progress updates via SSE.
*/
async executeStream(code, config, onProgress) {
await this.session.ensureInitialized();
const url = `${this.session.getBaseUrl()}/api/execute/stream`;
const body = JSON.stringify({
code,
config
});
const headers = await this.session.prepareHeaders("POST", url, body);
return new Promise((resolve, reject) => {
const fetchImpl = typeof fetch !== "undefined" ? fetch : __require("undici").fetch;
fetchImpl(url, {
method: "POST",
headers,
body
}).then(async (response) => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const reader = response.body?.getReader();
if (!reader) {
throw new Error("Response body is not readable");
}
const decoder = new TextDecoder();
let buffer = "";
let result = null;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, {
stream: true
});
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line && line.startsWith("event:")) {
const event = line.substring(6).trim();
for (let j = i + 1; j < lines.length; j++) {
const dataLine = lines[j];
if (dataLine && dataLine.startsWith("data:")) {
const dataStr = dataLine.substring(5).trim();
if (dataStr) {
try {
const data = JSON.parse(dataStr);
if (event === "progress" && onProgress) {
onProgress(data.message, data.fraction);
} else if (event === "result") {
result = data;
} else if (event === "error") {
reject(new Error(data.message));
return;
}
} catch (e) {
log.error("Failed to parse SSE data", {
dataStr,
error: e
});
}
}
break;
}
}
}
}
}
if (result) {
resolve(result);
} else {
reject(new Error("No result received from server"));
}
}).catch(reject);
});
}
/**
* Executes code on the server in a sandboxed environment.
*/
async execute(code, config) {
await this.session.ensureInitialized();
const hints = this.tokenRegistry.getRecentTokens(1e3);
const detectedClientServices = {
hasLLM: !!this.serviceProviders.getLLM(),
hasApproval: !!this.serviceProviders.getApproval(),
hasEmbedding: !!this.serviceProviders.getEmbedding(),
hasTools: this.serviceProviders.hasTools()
};
const executionConfig = {
...config,
clientServices: {
...detectedClientServices,
...config?.clientServices || {}
},
provenanceHints: hints.length > 0 ? hints : void 0
};
this.lastExecutionConfig = executionConfig;
let result;
if (this.inProcessSession) {
result = await this.inProcessSession.execute(code, executionConfig);
} else {
const url = `${this.session.getBaseUrl()}/api/execute`;
const body = JSON.stringify({
code,
config: executionConfig
});
const headers = await this.session.prepareHeaders("POST", url, body);
const response = await fetch(url, {
method: "POST",
headers,
body
});
this.session.updateToken(response);
if (!response.ok) {
const error = await response.json();
throw new Error(`Execution failed: ${error.error || response.statusText}`);
}
result = await response.json();
}
if (result.provenanceTokens && result.provenanceTokens.length > 0) {
for (const { token } of result.provenanceTokens) {
this.tokenRegistry.add(token);
}
}
if (result.status === ExecutionStatus.PAUSED && result.needsCallbacks) {
return await this.handleBatchCallbacksAndResume(result);
}
if (result.status === ExecutionStatus.PAUSED && result.needsCallback) {
return await this.handlePauseAndResume(result);
}
return result;
}
/**
* Handles batch callbacks by executing them in parallel and resuming.
*/
async handleBatchCallbacksAndResume(pausedResult) {
if (!pausedResult.needsCallbacks || pausedResult.needsCallbacks.length === 0) {
throw new Error("No batch callback requests in paused execution");
}
const missingServiceIds = new Set(pausedResult.needsCallbacks.filter((cb) => !this.serviceProviders.hasServiceForCallback(cb.type)).map((cb) => cb.id));
if (missingServiceIds.size > 0) {
const missingServices = pausedResult.needsCallbacks.filter((cb) => missingServiceIds.has(cb.id));
const explicitlyRequestedMissing = missingServices.filter((cb) => this.wasServiceExplicitlyRequested(cb.type));
const unexpectedMissing = missingServices.filter((cb) => !this.wasServiceExplicitlyRequested(cb.type));
if (explicitlyRequestedMissing.length > 0) {
return pausedResult;
}
const errorMessage = `Missing service providers for callback types: ${unexpectedMissing.map((cb) => cb.type).join(", ")}`;
log.error(`Auto-handling batch paused execution without service providers: ${errorMessage}`, {
executionId: pausedResult.executionId,
missingServices: unexpectedMissing.map((cb) => ({
type: cb.type,
operation: cb.operation,
id: cb.id
}))
});
const existingCallbacks = pausedResult.needsCallbacks.filter((cb) => !missingServiceIds.has(cb.id));
if (existingCallbacks.length > 0) {
try {
const existingResults = await Promise.all(existingCallbacks.map(async (cb) => {
const callbackResult = await this.serviceProviders.handleCallback(cb.type, {
...cb.payload,
operation: cb.operation
});
return {
id: cb.id,
result: callbackResult
};
}));
const allResults = pausedResult.needsCallbacks.map((cb) => {
if (missingServiceIds.has(cb.id)) {
return {
id: cb.id,
result: {
__error: true,
message: `${cb.type} service not provided by client`
}
};
}
return existingResults.find((r) => r.id === cb.id);
});
return await this.resumeWithBatchResults(pausedResult.executionId, allResults);
} catch (error) {
const errorMessage2 = error instanceof Error ? error.message : String(error);
log.error(`Error handling existing services in batch: ${errorMessage2}`, {
executionId: pausedResult.executionId
});
const allErrorResults = pausedResult.needsCallbacks.map((cb) => ({
id: cb.id,
result: {
__error: true,
message: missingServiceIds.has(cb.id) ? `${cb.type} service not provided by client` : errorMessage2
}
}));
return await this.resumeWithBatchResults(pausedResult.executionId, allErrorResults);
}
} else {
const allErrorResults = pausedResult.needsCallbacks.map((cb) => ({
id: cb.id,
result: {
__error: true,
message: `${cb.type} service not provided by client`
}
}));
return await this.resumeWithBatchResults(pausedResult.executionId, allErrorResults);
}
}
try {
const batchResults = await Promise.all(pausedResult.needsCallbacks.map(async (cb) => {
const callbackResult = await this.serviceProviders.handleCallback(cb.type, {
...cb.payload,
operation: cb.operation
});
return {
id: cb.id,
result: callbackResult
};
}));
return await this.resumeWithBatchResults(pausedResult.executionId, batchResults);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`Error handling batch callbacks: ${errorMessage}`, {
executionId: pausedResult.executionId,
callbackCount: pausedResult.needsCallbacks.length
});
const allErrorResults = pausedResult.needsCallbacks.map((cb) => ({
id: cb.id,
result: {
__error: true,
message: errorMessage
}
}));
return await this.resumeWithBatchResults(pausedResult.executionId, allErrorResults);
}
}
/**
* Handles a paused execution by processing the callback and resuming.
*/
async handlePauseAndResume(pausedResult) {
if (!pausedResult.needsCallback) {
throw new Error("No callback request in paused execution");
}
if (!this.serviceProviders.hasServiceForCallback(pausedResult.needsCallback.type)) {
const wasExplicitlyRequested = this.wasServiceExplicitlyRequested(pausedResult.needsCallback.type);
if (wasExplicitlyRequested) {
return pausedResult;
}
const errorMessage = `${pausedResult.needsCallback.type} service not provided by client`;
log.error(`Auto-handling paused execution without service provider: ${errorMessage}`, {
executionId: pausedResult.executionId,
callbackType: pausedResult.needsCallback.type,
operation: pausedResult.needsCallback.operation
});
return await this.resume(pausedResult.executionId, {
__error: true,
message: errorMessage
});
}
try {
const callbackResult = await this.serviceProviders.handleCallback(pausedResult.needsCallback.type, {
...pausedResult.needsCallback.payload,
operation: pausedResult.needsCallback.operation,
executionId: pausedResult.executionId
});
return await this.resume(pausedResult.executionId, callbackResult);
} catch (error) {
if (error instanceof ClientCallbackError) {
throw error;
}
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`Error handling callback: ${errorMessage}`, {
executionId: pausedResult.executionId,
callbackType: pausedResult.needsCallback.type,
operation: pausedResult.needsCallback.operation
});
return await this.resume(pausedResult.executionId, {
__error: true,
message: errorMessage
});
}
}
/**
* Check if a service was explicitly requested in clientServices config
*/
wasServiceExplicitlyRequested(callbackType) {
if (!this.lastExecutionConfig?.clientServices) {
return false;
}
switch (callbackType) {
case CallbackType.LLM:
return this.lastExecutionConfig.clientServices.hasLLM;
case CallbackType.APPROVAL:
return this.lastExecutionConfig.clientServices.hasApproval;
case CallbackType.EMBEDDING:
return this.lastExecutionConfig.clientServices.hasEmbedding;
case CallbackType.TOOL:
return this.lastExecutionConfig.clientServices.hasTools;
default:
return false;
}
}
/**
* Resumes a paused execution with a callback result.
*/
async resume(executionId, callbackResult) {
await this.session.ensureInitialized();
let result;
if (this.inProcessSession) {
result = await this.inProcessSession.resume(executionId, callbackResult);
} else {
const url = `${this.session.getBaseUrl()}/api/resume/${executionId}`;
const body = JSON.stringify({
result: callbackResult
});
const headers = await this.session.prepareHeaders("POST", url, body);
const response = await fetch(url, {
method: "POST",
headers,
body
});
this.session.updateToken(response);
if (!response.ok) {
const error = await response.json();
throw new Error(`Resume failed: ${error.error || response.statusText}`);
}
result = await response.json();
}
if (result.provenanceTokens && result.provenanceTokens.length > 0) {
for (const { token } of result.provenanceTokens) {
this.tokenRegistry.add(token);
}
}
if (result.status === ExecutionStatus.PAUSED && result.needsCallbacks) {
return await this.handleBatchCallbacksAndResume(result);
}
if (result.status === ExecutionStatus.PAUSED && result.needsCallback) {
return await this.handlePauseAndResume(result);
}
return result;
}
/**
* Resumes a paused execution with batch callback results.
*/
async resumeWithBatchResults(executionId, batchResults) {
await this.session.ensureInitialized();
let result;
if (this.inProcessSession) {
result = await this.inProcessSession.resumeWithBatchResults(executionId, batchResults);
} else {
const url = `${this.session.getBaseUrl()}/api/resume/${executionId}`;
const body = JSON.stringify({
results: batchResults
});
const headers = await this.session.prepareHeaders("POST", url, body);
const response = await fetch(url, {
method: "POST",
headers,
body
});
this.session.updateToken(response);
if (!response.ok) {
const error = await response.json();
throw new Error(`Batch resume failed: ${error.error || response.statusText}`);
}
result = await response.json();
}
if (result.provenanceTokens && result.provenanceTokens.length > 0) {
for (const { token } of result.provenanceTokens) {
this.tokenRegistry.add(token);
}
}
if (result.status === ExecutionStatus.PAUSED && result.needsCallbacks) {
return await this.handleBatchCallbacksAndResume(result);
}
if (result.status === ExecutionStatus.PAUSED && result.needsCallback) {
return await this.handlePauseAndResume(result);
}
return result;
}
};
var LLMOperation = {
CALL: "call",
EXTRACT: "extract",
CLASSIFY: "classify"
};
var EmbeddingOperation = {
EMBED: "embed",
SEARCH: "search"
};
var ServiceProviders = class {
static {
__name(this, "ServiceProviders");
}
providers = {};
toolHandlers = /* @__PURE__ */ new Map();
constructor(providers) {
this.providers = providers || {};
if (providers?.tools) {
for (const tool of providers.tools) {
this.toolHandlers.set(tool.name, tool.handler);
}
}
}
provideLLM(handler) {
this.providers.llm = handler;
}
provideApproval(handler) {
this.providers.approval = handler;
}
provideEmbedding(handler) {
this.providers.embedding = handler;
}
provideTools(tools) {
this.providers.tools = tools;
for (const tool of tools) {
this.toolHandlers.set(tool.name, tool.handler);
}
}
getLLM() {
return this.providers.llm;
}
getApproval() {
return this.providers.approval;
}
getEmbedding() {
return this.providers.embedding;
}
getTools() {
return this.providers.tools;
}
/**
* Get tool definitions (without handlers) for sending to server
*/
getToolDefinitions() {
if (!this.providers.tools) {
return [];
}
return this.providers.tools.map((tool) => {
const { handler, ...definition } = tool;
return definition;
});
}
/**
* Check if client has tools
*/
hasTools() {
return !!(this.providers.tools && this.providers.tools.length > 0);
}
/**
* Check if client has any services or tools
*/
hasAnyServices() {
return !!(this.providers.llm || this.providers.approval || this.providers.embedding || this.hasTools());
}
/**
* Check if client has a service for a specific callback type
*/
hasServiceForCallback(callbackType) {
switch (callbackType) {
case CallbackType.LLM:
return !!this.providers.llm;
case CallbackType.APPROVAL:
return !!this.providers.approval;
case CallbackType.EMBEDDING:
return !!this.providers.embedding;
case CallbackType.TOOL:
return this.hasTools();
default:
return false;
}
}
async handleCallback(callbackType, payload) {
if (payload.operation === "batch_parallel" && payload.calls) {
return await Promise.all(payload.calls.map(async (call) => {
return await this.handleCallback(call.type, {
...call.payload,
operation: call.operation
});
}));
}
switch (callbackType) {
case CallbackType.LLM:
if (!this.providers.llm) {
throw new Error("LLM service not provided by client");
}
if (payload.operation === LLMOperation.CALL) {
return await this.providers.llm.call(payload.prompt, payload.options);
} else if (payload.operation === LLMOperation.EXTRACT && this.providers.llm.extract) {
return await this.providers.llm.extract(payload.prompt, payload.schema, payload.options);
} else if (payload.operation === LLMOperation.CLASSIFY && this.providers.llm.classify) {
return await this.providers.llm.classify(payload.text, payload.categories, payload.options);
}
throw new Error(`Unsupported LLM operation: ${payload.operation}`);
case CallbackType.APPROVAL:
if (!this.providers.approval) {
throw new Error("Approval service not provided by client");
}
const contextWithExecutionId = payload.context ? {
...payload.context,
executionId: payload.executionId
} : {
executionId: payload.executionId
};
return await this.providers.approval.request(payload.message, contextWithExecutionId);
case CallbackType.EMBEDDING:
if (!this.providers.embedding) {
throw new Error("Embedding service not provided by client");
}
if (payload.operation === EmbeddingOperation.EMBED) {
return await this.providers.embedding.embed(payload.text);
} else if (payload.operation === EmbeddingOperation.SEARCH) {
const queryEmbedding = await this.providers.embedding.embed(payload.query);
return queryEmbedding;
} else if (payload.operation === "similarity" && this.providers.embedding.similarity) {
return await this.providers.embedding.similarity(payload.text1, payload.text2);
}
throw new Error(`Unsupported embedding operation: ${payload.operation}`);
case CallbackType.TOOL:
if (payload.operation === ToolOperation.CALL) {
const toolName = payload.toolName;
const handler = this.toolHandlers.get(toolName);
if (!handler) {
throw new Error(`Tool '${toolName}' not found in client tools`);
}
const result = await handler(payload.input);
return result;
}
throw new Error(`Unsupported tool operation: ${payload.operation}`);
default:
throw new Error(`Unknown callback type: ${callbackType}`);
}
}
};
// src/tools/types.ts
var ToolNames = {
SEARCH_API: "search_api",
FETCH_ALL_APIS: "fetch_all_apis",
EXECUTE_CODE: "execute_code",
EXPLORE_API: "explore_api"
};
var searchApiInputSchema = z.object({
query: z.string().describe("Search query string")
});
function createSearchApiTool(client) {
return {
name: ToolNames.SEARCH_API,
description: 'Search for APIs by keyword. Provide search term as string like "add", "math", "user", etc.',
inputSchema: zodToJsonSchema(searchApiInputSchema),
zodSchema: searchApiInputSchema,
func: /* @__PURE__ */ __name(async (input) => {
try {
const results = await client.searchAPI(input.query);
return JSON.stringify({
success: true,
results: results.map((r) => ({
apiGroup: r.apiGroup,
functionName: r.functionName,
description: r.description,
signature: r.signature
})),
count: results.length
}, null, 2);
} catch (error) {
return JSON.stringify({
success: false,
error: error.message
}, null, 2);
}
}, "func")
};
}
__name(createSearchApiTool, "createSearchApiTool");
var fetchAllApisInputSchema = z.object({
apiGroups: z.array(z.string()).optional().describe("Optional: Specific API groups to include")
});
function createFetchAllApisTool(client) {
return {
name: ToolNames.FETCH_ALL_APIS,
description: "Get TypeScript definitions of all available APIs. Returns code showing api.add, api.getTodo, etc.",
inputSchema: zodToJsonSchema(fetchAllApisInputSchema),
zodSchema: fetchAllApisInputSchema,
func: /* @__PURE__ */ __name(async (_input) => {
try {
const typescript = client.getTypeDefinitions();
return JSON.stringify({
success: true,
typescript,
message: "Use this TypeScript to understand available api.* functions"
}, null, 2);
} catch (error) {
return JSON.stringify({
success: false,
error: error.message
}, null, 2);
}
}, "func")
};
}
__name(createFetchAllApisTool, "createFetchAllApisTool");
var executeCodeInputSchema = z.object({
code: z.string().describe("The JavaScript/TypeScript code to execute"),
timeout: z.number().optional().describe("Execution timeout in milliseconds (default: 30000)"),
maxMemory: z.number().optional().describe("Maximum memory in bytes (default: 128MB)")
});
function createExecuteCodeTool(client) {
return {
name: ToolNames.EXECUTE_CODE,
description: "Execute JavaScript/TypeScript code to call APIs. IMPORTANT: Code MUST use 'return' statement to see results. Examples: 'return api.groupName.functionName({})' or 'const result = api.group.func({}); return result'. Use bracket notation for dynamic names: api['groupName']['functionName']({}).",
inputSchema: zodToJsonSchema(executeCodeInputSchema),
zodSchema: executeCodeInputSchema,
func: /* @__PURE__ */ __name(async (input) => {
try {
const result = await client.execute(input.code, {
timeout: input.timeout,
maxMemory: input.maxMemory
});
if (result.status === ExecutionStatus.COMPLETED) {
return JSON.stringify({
success: true,
result: result.result,
stats: {
duration: result.stats.duration,
memoryUsed: result.stats.memoryUsed
}
}, null, 2);
} else if (result.status === ExecutionStatus.FAILED) {
return JSON.stringify({
success: false,
error: result.error?.message || "Execution failed",
stack: result.error?.stack,
message: "Code execution failed. Check syntax and fix errors."
}, null, 2);
} else {
return JSON.stringify({
success: false,
error: "Execution timed out",
message: "Code took too long. Simplify or optimize."
}, null, 2);
}
} catch (error) {
return JSON.stringify({
success: false,
error: error.message,
message: "Failed to execute code"
}, null, 2);
}
}, "func")
};
}
__name(createExecuteCodeTool, "createExecuteCodeTool");
var exploreApiInputSchema = z.object({
path: z.string().describe('Path to explore (e.g., "/", "/openapi/github", "/mcp/filesystem/read_file")')
});
function createExploreApiTool(client) {
return {
name: ToolNames.EXPLORE_API,
description: 'Explore APIs using filesystem-like navigation. Navigate through directories to discover available functions. Provide path as string like "/", "/openapi", "/openapi/github", or "/openapi/github/repos/createRepo" to see functions.',
inputSchema: zodToJsonSchema(exploreApiInputSchema),
zodSchema: exploreApiInputSchema,
func: /* @__PURE__ */ __name(async (input) => {
try {
const result = await client.exploreAPI(input.path);
if (result.type === "directory") {
return JSON.stringify({
success: true,
type: "directory",
path: result.path,
items: result.items
}, null, 2);
} else {
return JSON.stringify({
success: true,
type: "function",
name: result.name,
description: result.description,
definition: result.definition,
group: result.group,
path: result.path
}, null, 2);
}
} catch (error) {
return JSON.stringify({
success: false,
error: error.message
}, null, 2);
}
}, "func")
};
}
__name(createExploreApiTool, "createExploreApiTool");
// src/client.ts
var AgentToolProtocolClient = class {
static {
__name(this, "AgentToolProtocolClient");
}
session;
inProcessSession;
apiOps;
execOps;
serviceProviders;
/**
* Creates a new client instance.
*
* @example
* ```typescript
* // HTTP mode
* const client = new AgentToolProtocolClient({
* baseUrl: 'http://localhost:3333',
* headers: { Authorization: 'Bearer token' },
* hooks: {
* preRequest: async (context) => {
* const token = await refreshToken();
* return { headers: { ...context.currentHeaders, Authorization: `Bearer ${token}` } };
* }
* }
* });
*
* // In-process mode (no port binding)
* const server = createServer();
* server.use(myApiGroup);
* const client = new AgentToolProtocolClient({ server });
* ```
*/
constructor(options) {
const { baseUrl, server, headers, serviceProviders, hooks } = options;
if (!baseUrl && !server) {
throw new Error("Either baseUrl or server must be provided");
}
if (baseUrl && server) {
throw new Error("Cannot provide both baseUrl and server");
}
this.serviceProviders = new ServiceProviders(serviceProviders);
if (server) {
this.inProcessSession = new InProcessSession(server);
this.session = this.inProcessSession;
this.apiOps = new APIOperations(this.session, this.inProcessSession);
this.execOps = new ExecutionOperations(this.session, this.serviceProviders, this.inProcessSession);
} else {
this.session = new ClientSession(baseUrl, headers, hooks);
this.apiOps = new APIOperations(this.session);
this.execOps = new ExecutionOperations(this.session, this.serviceProviders);
}
}
/**
* Initializes the client session with the server.
* Automatically registers any client-provided tools and services with the server.
*/
async init(clientInfo) {
const toolDefinitions = this.serviceProviders.getToolDefinitions();
const services = {
hasLLM: !!this.serviceProviders.getLLM(),
hasApproval: !!this.serviceProviders.getApproval(),
hasEmbedding: !!this.serviceProviders.getEmbedding(),
hasTools: this.serviceProviders.hasTools()
};
return await this.session.init(clientInfo, toolDefinitions, services);
}
/**
* Gets the unique client ID.
*/
getClientId() {
return this.session.getClientId();
}
/**
* Provides an LLM implementation for server to use during execution.
*/
provideLLM(handler) {
this.serviceProviders.provideLLM(handler);
}
/**
* Provides an approval handler for server to request human approval.
*/
provideApproval(handler) {
this.serviceProviders.provideApproval(handler);
}
/**
* Provides an embedding model for server to use.
*/
provideEmbedding(handler) {
this.serviceProviders.provideEmbedding(handler);
}
/**
* Provides custom tools that execute on the client side.
* Note: Must be called before init() or re-initialize after calling this.
*/
provideTools(tools) {
this.serviceProviders.provideTools(tools);
}
/**
* Gets all client-provided tools (registered via provideTools).
* Returns the full tool objects including handlers.
*/
getClientTools() {
return this.serviceProviders.getTools() || [];
}
/**
* Gets client-provided tool definitions (without handlers).
* Useful for sending tool metadata to servers.
*/
getClientToolDefinitions() {
return this.serviceProviders.getToolDefinitions();
}
/**
* Gets the ATP tools (execute_code, explore_api, search_api, fetch_all_apis).
* These are ready-to-use tools that can be exposed to MCP or other frameworks.
*
* @example
* ```typescript
* const tools = client.getATPTools();
* for (const tool of tools) {
* mcpServer.tool(tool.name, tool.description, tool.inputSchema, async (args) => {
* const result = await tool.func(args);
* return { content: [{ type: 'text', text: result }] };
* });
* }
* ```
*/
getATPTools() {
return [
createSearchApiTool(this),
createFetchAllApisTool(this),
createExecuteCodeTool(this),
createExploreApiTool(this)
];
}
/**
* Connects to the server and retrieves API definitions.
*/
async connect(options) {
return await this.apiOps.connect(options);
}
/**
* Gets the TypeScript type definitions for available APIs.
*/
getTypeDefinitions() {
return this.apiOps.getTypeDefinitions();
}
/**
* Searches for available API functions.
*/
async searchAPI(query, options) {
return await this.apiOps.searchAPI(query, options);
}
/**
* Explores the API filesystem at the given path.
*/
async exploreAPI(path) {
return await this.apiOps.exploreAPI(path);
}
/**
* Executes code on the server with real-time progress updates via SSE.
*/
async executeStream(code, config, onProgress) {
return await this.execOps.executeStream(code, config, onProgress);
}
/**
* Executes code on the server in a sandboxed environment.
*/
async execute(code, config) {
return await this.execOps.execute(code, config);
}
/**
* Resumes a paused execution with a callback result.
*/
async resume(executionId, callbackResult) {
return await this.execOps.resume(executionId, callbackResult);
}
/**
* Handles a callback request from the server during execution.
*/
async handleCallback(callbackType, payload) {
return await this.serviceProviders.handleCallback(callbackType, payload);
}
/**
* Gets information about the server.
*/
async getServerInfo() {
return await this.apiOps.getServerInfo();
}
/**
* Gets ATP runtime API definitions as TypeScript declarations.
* Returns the full TypeScript definitions for atp.llm.*, atp.cache.*, etc.
* These are the APIs available during code execution.
*
* Behavior:
* - No options: Returns APIs based on client capabilities (default filtering)
* - apis: ['llm', 'cache']: Returns only specified APIs (intersection with client capabilities)
* - apis: []: Returns all APIs regardless of client capabilities
*
* @param options - Optional filtering options
* @param options.apis - Specific APIs to include (e.g., ['llm', 'cache', 'approval'])
*/
async getRuntimeDefinitions(options) {
return await this.apiOps.getRuntimeDefinitions(options);
}
};
var CodeGenerator = class {
static {
__name(this, "CodeGenerator");
}
client;
constructor(client) {
this.client = client;
}
async generateCode(intent, parameters) {
const types = this.client.getTypeDefinitions();
log.debug("Generating code for intent", {
intent,
parameters,
typesLength: types.length
});
return "// Generated code";
}
};
// src/tools.ts
function createToolsFromATPClient(client) {
return [
createSearchApiTool(client),
createFetchAllApisTool(client),
createExecuteCodeTool(client),
createExploreApiTool(client)
];
}
__name(createToolsFromATPClient, "createToolsFromATPClient");
export { AgentToolProtocolClient, ClientCallbackError, CodeGenerator, InProcessSession, ToolNames, createToolsFromATPClient };
//# sourceM