OpenAPI spec (lib/api-spec/openapi.yaml)
- Added POST /jobs, GET /jobs/{id}, GET /demo endpoints
- Added schemas: CreateJobRequest, CreateJobResponse, JobStatusResponse,
InvoiceInfo, JobState, DemoResponse, ErrorResponse
- Ran codegen: generated CreateJobBody, GetJobResponse, RunDemoQueryParams etc.
Jobs router (artifacts/api-server/src/routes/jobs.ts)
- POST /jobs: validates body, creates LNbits eval invoice, inserts job +
invoice in a DB transaction, returns { jobId, evalInvoice }
- GET /jobs/🆔 fetches job, calls advanceJob() helper, returns state-
appropriate payload (eval/work invoice, reason, result, errorMessage)
- advanceJob() state machine:
- awaiting_eval_payment: checks LNbits, atomically marks paid + advances
state via optimistic WHERE state='awaiting_eval_payment'; runs
AgentService.evaluateRequest, branches to awaiting_work_payment or rejected
- awaiting_work_payment: same pattern for work invoice, runs
AgentService.executeWork, advances to complete
- Any agent/LNbits error transitions job to failed
Demo router (artifacts/api-server/src/routes/demo.ts)
- GET /demo?request=...: in-memory rate limiter (5 req/hour per IP)
- Explicit guard for missing request param (coerce.string() workaround)
- Calls AgentService.executeWork directly, returns { result }
Dev router (artifacts/api-server/src/routes/dev.ts)
- POST /dev/stub/pay/:paymentHash: marks stub invoice paid in-memory
- Only mounted when NODE_ENV !== 'production'
Route index updated to mount all three routers
replit.md: documented full curl flow with all 6 steps, demo endpoint,
and dev stub-pay trigger
End-to-end verified with curl:
- Full flow: create → eval pay → evaluating → work pay → executing → complete
- Error cases: 400 on missing body/param, 404 on unknown job
363 lines
9.4 KiB
TypeScript
363 lines
9.4 KiB
TypeScript
/**
|
|
* Generated by orval v8.5.3 🍺
|
|
* Do not edit manually.
|
|
* Api
|
|
* API specification
|
|
* OpenAPI spec version: 0.1.0
|
|
*/
|
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
import type {
|
|
MutationFunction,
|
|
QueryFunction,
|
|
QueryKey,
|
|
UseMutationOptions,
|
|
UseMutationResult,
|
|
UseQueryOptions,
|
|
UseQueryResult,
|
|
} from "@tanstack/react-query";
|
|
|
|
import type {
|
|
CreateJobRequest,
|
|
CreateJobResponse,
|
|
DemoResponse,
|
|
ErrorResponse,
|
|
HealthStatus,
|
|
JobStatusResponse,
|
|
RunDemoParams,
|
|
} from "./api.schemas";
|
|
|
|
import { customFetch } from "../custom-fetch";
|
|
import type { ErrorType, BodyType } from "../custom-fetch";
|
|
|
|
type AwaitedInput<T> = PromiseLike<T> | T;
|
|
|
|
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
|
|
|
|
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
|
|
|
|
/**
|
|
* Returns server health status
|
|
* @summary Health check
|
|
*/
|
|
export const getHealthCheckUrl = () => {
|
|
return `/api/healthz`;
|
|
};
|
|
|
|
export const healthCheck = async (
|
|
options?: RequestInit,
|
|
): Promise<HealthStatus> => {
|
|
return customFetch<HealthStatus>(getHealthCheckUrl(), {
|
|
...options,
|
|
method: "GET",
|
|
});
|
|
};
|
|
|
|
export const getHealthCheckQueryKey = () => {
|
|
return [`/api/healthz`] as const;
|
|
};
|
|
|
|
export const getHealthCheckQueryOptions = <
|
|
TData = Awaited<ReturnType<typeof healthCheck>>,
|
|
TError = ErrorType<unknown>,
|
|
>(options?: {
|
|
query?: UseQueryOptions<
|
|
Awaited<ReturnType<typeof healthCheck>>,
|
|
TError,
|
|
TData
|
|
>;
|
|
request?: SecondParameter<typeof customFetch>;
|
|
}) => {
|
|
const { query: queryOptions, request: requestOptions } = options ?? {};
|
|
|
|
const queryKey = queryOptions?.queryKey ?? getHealthCheckQueryKey();
|
|
|
|
const queryFn: QueryFunction<Awaited<ReturnType<typeof healthCheck>>> = ({
|
|
signal,
|
|
}) => healthCheck({ signal, ...requestOptions });
|
|
|
|
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
|
Awaited<ReturnType<typeof healthCheck>>,
|
|
TError,
|
|
TData
|
|
> & { queryKey: QueryKey };
|
|
};
|
|
|
|
export type HealthCheckQueryResult = NonNullable<
|
|
Awaited<ReturnType<typeof healthCheck>>
|
|
>;
|
|
export type HealthCheckQueryError = ErrorType<unknown>;
|
|
|
|
/**
|
|
* @summary Health check
|
|
*/
|
|
|
|
export function useHealthCheck<
|
|
TData = Awaited<ReturnType<typeof healthCheck>>,
|
|
TError = ErrorType<unknown>,
|
|
>(options?: {
|
|
query?: UseQueryOptions<
|
|
Awaited<ReturnType<typeof healthCheck>>,
|
|
TError,
|
|
TData
|
|
>;
|
|
request?: SecondParameter<typeof customFetch>;
|
|
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
|
const queryOptions = getHealthCheckQueryOptions(options);
|
|
|
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
|
queryKey: QueryKey;
|
|
};
|
|
|
|
return { ...query, queryKey: queryOptions.queryKey };
|
|
}
|
|
|
|
/**
|
|
* Accepts a request, creates a job row, and issues an eval fee Lightning invoice.
|
|
* @summary Create a new agent job
|
|
*/
|
|
export const getCreateJobUrl = () => {
|
|
return `/api/jobs`;
|
|
};
|
|
|
|
export const createJob = async (
|
|
createJobRequest: CreateJobRequest,
|
|
options?: RequestInit,
|
|
): Promise<CreateJobResponse> => {
|
|
return customFetch<CreateJobResponse>(getCreateJobUrl(), {
|
|
...options,
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", ...options?.headers },
|
|
body: JSON.stringify(createJobRequest),
|
|
});
|
|
};
|
|
|
|
export const getCreateJobMutationOptions = <
|
|
TError = ErrorType<ErrorResponse>,
|
|
TContext = unknown,
|
|
>(options?: {
|
|
mutation?: UseMutationOptions<
|
|
Awaited<ReturnType<typeof createJob>>,
|
|
TError,
|
|
{ data: BodyType<CreateJobRequest> },
|
|
TContext
|
|
>;
|
|
request?: SecondParameter<typeof customFetch>;
|
|
}): UseMutationOptions<
|
|
Awaited<ReturnType<typeof createJob>>,
|
|
TError,
|
|
{ data: BodyType<CreateJobRequest> },
|
|
TContext
|
|
> => {
|
|
const mutationKey = ["createJob"];
|
|
const { mutation: mutationOptions, request: requestOptions } = options
|
|
? options.mutation &&
|
|
"mutationKey" in options.mutation &&
|
|
options.mutation.mutationKey
|
|
? options
|
|
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
|
: { mutation: { mutationKey }, request: undefined };
|
|
|
|
const mutationFn: MutationFunction<
|
|
Awaited<ReturnType<typeof createJob>>,
|
|
{ data: BodyType<CreateJobRequest> }
|
|
> = (props) => {
|
|
const { data } = props ?? {};
|
|
|
|
return createJob(data, requestOptions);
|
|
};
|
|
|
|
return { mutationFn, ...mutationOptions };
|
|
};
|
|
|
|
export type CreateJobMutationResult = NonNullable<
|
|
Awaited<ReturnType<typeof createJob>>
|
|
>;
|
|
export type CreateJobMutationBody = BodyType<CreateJobRequest>;
|
|
export type CreateJobMutationError = ErrorType<ErrorResponse>;
|
|
|
|
/**
|
|
* @summary Create a new agent job
|
|
*/
|
|
export const useCreateJob = <
|
|
TError = ErrorType<ErrorResponse>,
|
|
TContext = unknown,
|
|
>(options?: {
|
|
mutation?: UseMutationOptions<
|
|
Awaited<ReturnType<typeof createJob>>,
|
|
TError,
|
|
{ data: BodyType<CreateJobRequest> },
|
|
TContext
|
|
>;
|
|
request?: SecondParameter<typeof customFetch>;
|
|
}): UseMutationResult<
|
|
Awaited<ReturnType<typeof createJob>>,
|
|
TError,
|
|
{ data: BodyType<CreateJobRequest> },
|
|
TContext
|
|
> => {
|
|
return useMutation(getCreateJobMutationOptions(options));
|
|
};
|
|
|
|
/**
|
|
* Returns current job state. Automatically advances the state machine when a pending invoice is found to be paid.
|
|
* @summary Get job status
|
|
*/
|
|
export const getGetJobUrl = (id: string) => {
|
|
return `/api/jobs/${id}`;
|
|
};
|
|
|
|
export const getJob = async (
|
|
id: string,
|
|
options?: RequestInit,
|
|
): Promise<JobStatusResponse> => {
|
|
return customFetch<JobStatusResponse>(getGetJobUrl(id), {
|
|
...options,
|
|
method: "GET",
|
|
});
|
|
};
|
|
|
|
export const getGetJobQueryKey = (id: string) => {
|
|
return [`/api/jobs/${id}`] as const;
|
|
};
|
|
|
|
export const getGetJobQueryOptions = <
|
|
TData = Awaited<ReturnType<typeof getJob>>,
|
|
TError = ErrorType<ErrorResponse>,
|
|
>(
|
|
id: string,
|
|
options?: {
|
|
query?: UseQueryOptions<Awaited<ReturnType<typeof getJob>>, TError, TData>;
|
|
request?: SecondParameter<typeof customFetch>;
|
|
},
|
|
) => {
|
|
const { query: queryOptions, request: requestOptions } = options ?? {};
|
|
|
|
const queryKey = queryOptions?.queryKey ?? getGetJobQueryKey(id);
|
|
|
|
const queryFn: QueryFunction<Awaited<ReturnType<typeof getJob>>> = ({
|
|
signal,
|
|
}) => getJob(id, { signal, ...requestOptions });
|
|
|
|
return {
|
|
queryKey,
|
|
queryFn,
|
|
enabled: !!id,
|
|
...queryOptions,
|
|
} as UseQueryOptions<Awaited<ReturnType<typeof getJob>>, TError, TData> & {
|
|
queryKey: QueryKey;
|
|
};
|
|
};
|
|
|
|
export type GetJobQueryResult = NonNullable<Awaited<ReturnType<typeof getJob>>>;
|
|
export type GetJobQueryError = ErrorType<ErrorResponse>;
|
|
|
|
/**
|
|
* @summary Get job status
|
|
*/
|
|
|
|
export function useGetJob<
|
|
TData = Awaited<ReturnType<typeof getJob>>,
|
|
TError = ErrorType<ErrorResponse>,
|
|
>(
|
|
id: string,
|
|
options?: {
|
|
query?: UseQueryOptions<Awaited<ReturnType<typeof getJob>>, TError, TData>;
|
|
request?: SecondParameter<typeof customFetch>;
|
|
},
|
|
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
|
const queryOptions = getGetJobQueryOptions(id, options);
|
|
|
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
|
queryKey: QueryKey;
|
|
};
|
|
|
|
return { ...query, queryKey: queryOptions.queryKey };
|
|
}
|
|
|
|
/**
|
|
* Runs the agent without payment. Limited to 5 requests per IP per hour.
|
|
* @summary Free demo (rate-limited)
|
|
*/
|
|
export const getRunDemoUrl = (params: RunDemoParams) => {
|
|
const normalizedParams = new URLSearchParams();
|
|
|
|
Object.entries(params || {}).forEach(([key, value]) => {
|
|
if (value !== undefined) {
|
|
normalizedParams.append(key, value === null ? "null" : value.toString());
|
|
}
|
|
});
|
|
|
|
const stringifiedParams = normalizedParams.toString();
|
|
|
|
return stringifiedParams.length > 0
|
|
? `/api/demo?${stringifiedParams}`
|
|
: `/api/demo`;
|
|
};
|
|
|
|
export const runDemo = async (
|
|
params: RunDemoParams,
|
|
options?: RequestInit,
|
|
): Promise<DemoResponse> => {
|
|
return customFetch<DemoResponse>(getRunDemoUrl(params), {
|
|
...options,
|
|
method: "GET",
|
|
});
|
|
};
|
|
|
|
export const getRunDemoQueryKey = (params?: RunDemoParams) => {
|
|
return [`/api/demo`, ...(params ? [params] : [])] as const;
|
|
};
|
|
|
|
export const getRunDemoQueryOptions = <
|
|
TData = Awaited<ReturnType<typeof runDemo>>,
|
|
TError = ErrorType<ErrorResponse>,
|
|
>(
|
|
params: RunDemoParams,
|
|
options?: {
|
|
query?: UseQueryOptions<Awaited<ReturnType<typeof runDemo>>, TError, TData>;
|
|
request?: SecondParameter<typeof customFetch>;
|
|
},
|
|
) => {
|
|
const { query: queryOptions, request: requestOptions } = options ?? {};
|
|
|
|
const queryKey = queryOptions?.queryKey ?? getRunDemoQueryKey(params);
|
|
|
|
const queryFn: QueryFunction<Awaited<ReturnType<typeof runDemo>>> = ({
|
|
signal,
|
|
}) => runDemo(params, { signal, ...requestOptions });
|
|
|
|
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
|
Awaited<ReturnType<typeof runDemo>>,
|
|
TError,
|
|
TData
|
|
> & { queryKey: QueryKey };
|
|
};
|
|
|
|
export type RunDemoQueryResult = NonNullable<
|
|
Awaited<ReturnType<typeof runDemo>>
|
|
>;
|
|
export type RunDemoQueryError = ErrorType<ErrorResponse>;
|
|
|
|
/**
|
|
* @summary Free demo (rate-limited)
|
|
*/
|
|
|
|
export function useRunDemo<
|
|
TData = Awaited<ReturnType<typeof runDemo>>,
|
|
TError = ErrorType<ErrorResponse>,
|
|
>(
|
|
params: RunDemoParams,
|
|
options?: {
|
|
query?: UseQueryOptions<Awaited<ReturnType<typeof runDemo>>, TError, TData>;
|
|
request?: SecondParameter<typeof customFetch>;
|
|
},
|
|
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
|
const queryOptions = getRunDemoQueryOptions(params, options);
|
|
|
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
|
queryKey: QueryKey;
|
|
};
|
|
|
|
return { ...query, queryKey: queryOptions.queryKey };
|
|
}
|