@bluexlab/maos-ts
Advanced tools
Comparing version 0.0.3 to 0.0.4
@@ -0,3 +1,3 @@ | ||
import { Level, Logger } from 'pino'; | ||
import { ResultAsync } from 'neverthrow'; | ||
import { Level } from 'pino'; | ||
@@ -11,2 +11,6 @@ interface MaosConfig { | ||
} | ||
interface MaosClientConfig { | ||
apiKey: string; | ||
coreUrl: string; | ||
} | ||
interface MaosResponse { | ||
@@ -23,3 +27,10 @@ [key: string]: object; | ||
} | ||
type HandlerInputType = Record<string, unknown>; | ||
interface HandlerContext { | ||
logger: Logger; | ||
meta: InvocationMetaType; | ||
} | ||
interface HandlerInputType { | ||
context: HandlerContext; | ||
payload: Record<string, unknown>; | ||
} | ||
type HandlerResultType = ResultAsync<Record<string, unknown>, Error>; | ||
@@ -39,17 +50,33 @@ type HandlerType = (input: HandlerInputType) => HandlerResultType; | ||
private abortController; | ||
private abortSignal; | ||
constructor(config?: MaosConfig); | ||
getConfig(): ResultAsync<MaosResponse, Error>; | ||
getLogger(): Logger; | ||
on(methodName: string, handler: HandlerType): Maos; | ||
shutdown(): void; | ||
run(): Promise<void>; | ||
private fetchWithAuth; | ||
private postWithAuth; | ||
getConfig(): ResultAsync<MaosResponse, Error>; | ||
private getNextInvocation; | ||
private respondToInvocation; | ||
on(methodName: string, handler: HandlerType): Maos; | ||
private getHandler; | ||
private handleInvocation; | ||
private worker; | ||
shutdown(): void; | ||
run(): Promise<void>; | ||
} | ||
export { type HandlerInputType, type HandlerResultType, type HandlerType, type Invocation, type InvocationMetaType, Maos, type MaosConfig, type MaosResponse }; | ||
declare class MaosClient { | ||
private apiKey; | ||
private coreUrl; | ||
constructor(config: MaosClientConfig); | ||
execute(agent: string, kind: string, input: Record<string, unknown>, wait?: number): ResultAsync<MaosResponse, Error>; | ||
executeAsync(agent: string, kind: string, input: Record<string, unknown>): ResultAsync<MaosAsyncResponder, Error>; | ||
private fetchWithAuth; | ||
private getInvocationResult; | ||
} | ||
declare class MaosAsyncResponder { | ||
private readonly fetcher; | ||
private readonly id; | ||
constructor(fetcher: (endpoint: string) => ResultAsync<MaosResponse, unknown>, id: string); | ||
getResult(wait?: number): ResultAsync<MaosResponse, Error>; | ||
} | ||
export { type HandlerContext, type HandlerInputType, type HandlerResultType, type HandlerType, type Invocation, type InvocationMetaType, Maos, MaosClient, type MaosClientConfig, type MaosConfig, type MaosResponse }; |
@@ -38,10 +38,7 @@ // src/maos.ts | ||
} | ||
}), (err2) => err2); | ||
}), (err3) => err3); | ||
} | ||
function joinUrl(base, ...parts) { | ||
function joinUrl(base, part) { | ||
const url = new URL(base); | ||
parts.forEach((part) => { | ||
url.pathname = new URL(part, url).pathname; | ||
}); | ||
return url.toString(); | ||
return new URL(part, url).toString(); | ||
} | ||
@@ -59,2 +56,8 @@ function jsonValueConverter(key, value) { | ||
}; | ||
var AbortedError = class extends Error { | ||
constructor() { | ||
super("Aborted"); | ||
this.name = "Aborted"; | ||
} | ||
}; | ||
var Maos = class { | ||
@@ -73,4 +76,3 @@ // configs | ||
logger; | ||
abortController; | ||
abortSignal; | ||
abortController = null; | ||
constructor(config = {}) { | ||
@@ -89,6 +91,27 @@ this.apiKey = config.apiKey || process.env.APIKEY || ""; | ||
this.handlers = /* @__PURE__ */ new Map(); | ||
this.logger = pino({ level: this.logLevel }); | ||
this.abortController = new AbortController(); | ||
this.abortSignal = this.abortController.signal; | ||
this.logger = createLogger(this.logLevel); | ||
} | ||
getConfig() { | ||
return this.fetchWithAuth("/v1/config").mapErr((err3) => err3 instanceof Error ? err3 : new Error(String(err3))); | ||
} | ||
getLogger() { | ||
return this.logger; | ||
} | ||
// Register a handler for a method name | ||
on(methodName, handler) { | ||
if (this.handlers.has(methodName)) { | ||
throw new Error(`Handler already registered with method name: ${methodName}`); | ||
} | ||
this.handlers.set(methodName, handler); | ||
return this; | ||
} | ||
shutdown() { | ||
this.abortController?.abort(); | ||
this.shouldRun = false; | ||
} | ||
async run() { | ||
process.on("SIGTERM", () => this.shutdown()); | ||
process.on("SIGINT", () => this.shutdown()); | ||
await Promise.all(Array.from({ length: this.concurrentInvocationNum }, () => this.worker())); | ||
} | ||
fetchWithAuth(endpoint, options = {}, allowAbort = false) { | ||
@@ -101,6 +124,7 @@ const url = joinUrl(this.coreUrl, endpoint); | ||
}); | ||
const fetchPromise = fetch(url, { ...options, headers, signal: allowAbort ? this.abortSignal : void 0 }); | ||
const abortSignal = this.abortController?.signal; | ||
const fetchPromise = fetch(url, { ...options, headers, signal: allowAbort ? abortSignal : void 0 }); | ||
if (!fetchPromise) | ||
return errAsync(new NotFoundError()); | ||
return ResultAsync2.fromPromise(fetchPromise, (err2) => err2).andThen((res) => { | ||
return ResultAsync2.fromPromise(fetchPromise, (err3) => err3).andThen((res) => { | ||
if (!res) | ||
@@ -114,4 +138,4 @@ return err(new Error("No response")); | ||
return ok(res); | ||
}).andThen((res) => ResultAsync2.fromPromise(res.text(), (err2) => err2).map((body) => ({ res, body }))).andThen(({ res, body }) => { | ||
this.logger.info(`Response status: ${res.status}, ${res.statusText} body: ${body}`); | ||
}).andThen((res) => ResultAsync2.fromPromise(res.text(), (err3) => err3).map((body) => ({ res, body }))).andThen(({ res, body }) => { | ||
this.logger.debug(`Response status: ${res.status}, ${res.statusText} body: ${body}`); | ||
if (!body) | ||
@@ -128,28 +152,20 @@ return ok({}); | ||
} | ||
getConfig() { | ||
return this.fetchWithAuth("/v1/config").mapErr((err2) => err2 instanceof Error ? err2 : new Error(String(err2))); | ||
} | ||
getNextInvocation() { | ||
return this.fetchWithAuth("/v1/invocations/next", void 0, true).mapErr((err2) => { | ||
if (err2 instanceof NotFoundError) | ||
return err2; | ||
return new Error(`Failed to get next invocation: ${formatMaosError(err2)}`); | ||
return this.fetchWithAuth("/v1/invocations/next", void 0, true).mapErr((err3) => { | ||
if (err3 instanceof NotFoundError) | ||
return err3; | ||
if (err3 instanceof Error && err3.name === "AbortError") { | ||
return new AbortedError(); | ||
} | ||
return new Error(`Failed to get next invocation: ${formatMaosError(err3)}`); | ||
}); | ||
} | ||
respondToInvocation(invokeId, result) { | ||
return result.andThen((res) => this.postWithAuth(`/v1/invocations/${invokeId}/response`, { result: res })).orElse((err2) => this.postWithAuth(`/v1/invocations/${invokeId}/error`, { | ||
errors: { message: formatMaosError(err2) } | ||
})).mapErr((err2) => { | ||
this.logger.error(`Failed to respond to invocation with id ${invokeId}: ${formatMaosError(err2)}`); | ||
return err2; | ||
return result.andThen((res) => this.postWithAuth(`/v1/invocations/${invokeId}/response`, { result: res })).orElse((err3) => this.postWithAuth(`/v1/invocations/${invokeId}/error`, { | ||
errors: { message: formatMaosError(err3) } | ||
})).mapErr((err3) => { | ||
this.logger.error(`Failed to respond to invocation with id ${invokeId}: ${formatMaosError(err3)}`); | ||
return err3; | ||
}); | ||
} | ||
// Register a handler for a method name | ||
on(methodName, handler) { | ||
if (this.handlers.has(methodName)) { | ||
throw new Error(`Handler already registered with method name: ${methodName}`); | ||
} | ||
this.handlers.set(methodName, handler); | ||
return this; | ||
} | ||
getHandler(methodName) { | ||
@@ -172,12 +188,14 @@ const handler = this.handlers.get(methodName); | ||
const kind = meta.kind; | ||
return this.getHandler(kind).map((handler) => ({ payload, handler })); | ||
return this.getHandler(kind).map((handler) => ({ meta, payload, handler })); | ||
}).andThen( | ||
// trigger the handler within a throwable context | ||
// even though the handler is expected not to throw, we can still catch any errors thrown | ||
Result.fromThrowable(({ payload, handler }) => handler(payload)) | ||
Result.fromThrowable( | ||
({ meta, payload, handler }) => handler({ context: { logger: this.logger, meta }, payload }) | ||
) | ||
).asyncAndThen( | ||
(result) => result.andThen((res) => this.respondToInvocation(String(id), okAsync(res))) | ||
).mapErr(async (err2) => { | ||
this.respondToInvocation(String(id), errAsync(err2 instanceof Error ? err2 : new Error(String(err2)))); | ||
return err2; | ||
).mapErr(async (err3) => { | ||
this.respondToInvocation(String(id), errAsync(err3 instanceof Error ? err3 : new Error(String(err3)))); | ||
return err3; | ||
}); | ||
@@ -189,16 +207,21 @@ }); | ||
while (this.shouldRun) { | ||
await this.handleInvocation().mapErr((err2) => { | ||
if (!(err2 instanceof NotFoundError)) { | ||
this.logger.error(`Error during worker run: ${formatMaosError(err2)}`); | ||
this.abortController = new AbortController(); | ||
await this.handleInvocation().mapErr((err3) => { | ||
if (err3 instanceof AbortedError) { | ||
this.logger.info("Worker run aborted"); | ||
this.shouldRun = false; | ||
} else if (!(err3 instanceof NotFoundError)) { | ||
this.logger.error(`Error during worker run: ${formatMaosError(err3)}`); | ||
} | ||
}); | ||
if (!this.shouldRun) | ||
break; | ||
const timeSinceLastRun = Date.now() - lastRun; | ||
if (timeSinceLastRun < this.minInvocationInterval) { | ||
await sleep(this.invocationPollInterval, this.abortSignal).mapErr((err2) => { | ||
if (err2 instanceof Error && err2.message === "Aborted") { | ||
await sleep(this.invocationPollInterval, this.abortController.signal).mapErr((err3) => { | ||
if (err3 instanceof Error && err3.message === "Aborted") { | ||
this.logger.info("Worker sleep aborted"); | ||
this.shouldRun = false; | ||
} else { | ||
this.logger.error(`Error during worker sleep: ${formatMaosError(err2)}`); | ||
this.logger.error(`Error during worker sleep: ${formatMaosError(err3)}`); | ||
} | ||
@@ -208,16 +231,95 @@ }); | ||
lastRun = Date.now(); | ||
this.abortController = null; | ||
} | ||
} | ||
shutdown() { | ||
this.abortController.abort(); | ||
this.shouldRun = false; | ||
}; | ||
function createLogger(logLevel) { | ||
return pino({ | ||
level: logLevel, | ||
timestamp: () => `,"ts":${Date.now()}`, | ||
// Use Unix timestamp in milliseconds | ||
formatters: { | ||
level: (label) => { | ||
return { level: label.toUpperCase() }; | ||
} | ||
}, | ||
// Customize the output | ||
base: void 0, | ||
// Remove the default 'pid' and 'hostname' fields | ||
messageKey: "msg" | ||
// Set the key for the 'message' field in the output | ||
}); | ||
} | ||
// src/maos-client.ts | ||
import { ResultAsync as ResultAsync3, err as err2, ok as ok2 } from "neverthrow"; | ||
var MaosClient = class { | ||
apiKey; | ||
coreUrl; | ||
constructor(config) { | ||
this.apiKey = config.apiKey; | ||
this.coreUrl = config.coreUrl; | ||
} | ||
async run() { | ||
process.on("SIGTERM", () => this.shutdown()); | ||
process.on("SIGINT", () => this.shutdown()); | ||
await Promise.all(Array.from({ length: this.concurrentInvocationNum }, () => this.worker())); | ||
execute(agent, kind, input, wait) { | ||
const path = `/v1/invocations/sync${wait ? `?wait=${wait}` : ""}`; | ||
return this.fetchWithAuth( | ||
path, | ||
{ method: "POST", body: JSON.stringify({ agent, meta: { kind }, payload: input }) } | ||
).mapErr((err3) => err3 instanceof Error ? err3 : new Error(String(err3))); | ||
} | ||
executeAsync(agent, kind, input) { | ||
return this.fetchWithAuth("/v1/invocations/async", { | ||
method: "POST", | ||
body: JSON.stringify({ agent, meta: { kind }, payload: input }) | ||
}).andThen((res) => { | ||
if (!res || !res.id || typeof res.id !== "string") { | ||
return err2(new Error("Missing invocation id from MAOS")); | ||
} | ||
return ok2(res.id); | ||
}).map((id) => new MaosAsyncResponder((url) => this.fetchWithAuth(url), id)).mapErr((err3) => err3 instanceof Error ? err3 : new Error(String(err3))); | ||
} | ||
fetchWithAuth(endpoint, options = {}) { | ||
const url = joinUrl(this.coreUrl, endpoint); | ||
const headers = new Headers({ | ||
"Authorization": `Bearer ${this.apiKey}`, | ||
"Content-Type": "application/json", | ||
...options.headers | ||
}); | ||
return ResultAsync3.fromPromise(fetch(url, { ...options, headers }), (err3) => err3).andThen((res) => { | ||
if (!res.ok) { | ||
return err2(new Error(`HTTP error! status: ${res.status}, statusText: ${res.statusText}`)); | ||
} | ||
return ok2(res); | ||
}).andThen((res) => ResultAsync3.fromPromise(res.text(), (err3) => err3)).andThen((body) => { | ||
if (!body) | ||
return ok2({}); | ||
return ok2(JSON.parse(body)); | ||
}); | ||
} | ||
getInvocationResult(id) { | ||
return this.fetchWithAuth(`/v1/invocations/${id}`).andThen((res) => { | ||
if (!res || !res.status || typeof res.status !== "string") { | ||
return err2(new Error("Missing status from MAOS")); | ||
} | ||
if (res.status === "completed" || res.status === "discarded") { | ||
return ok2(String(res.result)); | ||
} | ||
return this.getInvocationResult(id); | ||
}); | ||
} | ||
}; | ||
var MaosAsyncResponder = class { | ||
fetcher; | ||
id; | ||
constructor(fetcher, id) { | ||
this.fetcher = fetcher; | ||
this.id = id; | ||
} | ||
getResult(wait) { | ||
return this.fetcher(`/v1/invocations/${this.id}${wait ? `?wait=${wait}` : ""}`).mapErr((err3) => err3 instanceof Error ? err3 : new Error(String(err3))); | ||
} | ||
}; | ||
export { | ||
Maos | ||
Maos, | ||
MaosClient | ||
}; |
{ | ||
"name": "@bluexlab/maos-ts", | ||
"type": "module", | ||
"version": "0.0.3", | ||
"version": "0.0.4", | ||
"packageManager": "pnpm@9.5.0", | ||
@@ -6,0 +6,0 @@ "description": "TypeScript binding of MAOS", |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
31545
683