"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var index_exports = {};
__export(index_exports, {
Agent: () => Agent,
AgentSession: () => AgentSession,
RecursiveAgentError: () => RecursiveAgentError,
RecursiveClient: () => RecursiveClient,
Run: () => Run,
mapFinishReasonToSubtype: () => mapFinishReasonToSubtype
});
module.exports = __toCommonJS(index_exports);
var RecursiveAgentError = class extends Error {
constructor(message, options) {
super(message);
this.name = "RecursiveAgentError";
this.isRetryable = options?.isRetryable ?? false;
Object.setPrototypeOf(this, new.target.prototype);
}
};
async function* parseSse(lines) {
let eventType = "message";
const dataParts = [];
for await (const line of lines) {
if (line === "") {
if (dataParts.length > 0) {
const payload = dataParts.join("\n");
let parsed;
try {
parsed = JSON.parse(payload);
} catch {
parsed = { raw: payload };
}
yield { type: eventType, data: parsed };
}
eventType = "message";
dataParts.length = 0;
continue;
}
if (line.startsWith("event:")) {
eventType = line.slice(6).trim();
} else if (line.startsWith("data:")) {
dataParts.push(line.slice(5).trim());
}
}
}
async function* streamToLines(body) {
const reader = body.getReader();
const decoder = new TextDecoder();
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const parts = buffer.split("\n");
buffer = parts.pop() ?? "";
for (const part of parts) {
yield part;
}
}
if (buffer) yield buffer;
} finally {
reader.releaseLock();
}
}
var HttpClient = class {
constructor({ baseUrl, apiKey }) {
this.baseUrl = baseUrl.replace(/\/$/, "");
this.headers = {
"Content-Type": "application/json"
};
const key = apiKey ?? process.env["RECURSIVE_API_KEY"];
if (key) {
this.headers["x-api-key"] = key;
}
}
async get(path) {
const resp = await this._fetch("GET", path);
return resp.json();
}
async post(path, body) {
const resp = await this._fetch("POST", path, body);
return resp.json();
}
async patch(path, body) {
const resp = await this._fetch("PATCH", path, body);
return resp.json();
}
async delete(path) {
await this._fetch("DELETE", path);
}
async *streamEvents(path) {
let resp;
try {
resp = await fetch(`${this.baseUrl}${path}`, {
method: "GET",
headers: { ...this.headers, Accept: "text/event-stream" }
});
} catch (err) {
throw new RecursiveAgentError(
`Cannot reach Recursive server at ${this.baseUrl}: ${err}`,
{ isRetryable: true }
);
}
if (!resp.ok) {
const text = await resp.text();
throw new RecursiveAgentError(
`HTTP ${resp.status}: ${text}`,
{ isRetryable: resp.status >= 500 }
);
}
if (!resp.body) {
throw new RecursiveAgentError("Response body is null");
}
const body = resp.body;
try {
yield* parseSse(streamToLines(body));
} finally {
try {
await body.cancel();
} catch {
}
}
}
async _fetch(method, path, body) {
try {
const resp = await fetch(`${this.baseUrl}${path}`, {
method,
headers: this.headers,
body: body !== void 0 ? JSON.stringify(body) : void 0
});
if (!resp.ok) {
const text = await resp.text();
throw new RecursiveAgentError(
`HTTP ${resp.status}: ${text}`,
{ isRetryable: resp.status >= 500 }
);
}
return resp;
} catch (err) {
if (err instanceof RecursiveAgentError) throw err;
throw new RecursiveAgentError(
`Cannot reach Recursive server at ${this.baseUrl}: ${err}`,
{ isRetryable: true }
);
}
}
};
function mapFinishReasonToSubtype(finishReason, status) {
if (status === "cancelled") return "cancelled";
if (!finishReason) return status === "finished" ? "success" : "error_during_execution";
if (finishReason.includes("BudgetExceeded") || finishReason.includes("TranscriptLimit")) {
return "error_max_turns";
}
if (finishReason.includes("Cancelled")) return "cancelled";
if (finishReason.includes("NoMoreToolCalls") || finishReason.includes("PlanPending")) {
return "success";
}
return status === "finished" ? "success" : "error_during_execution";
}
var Run = class {
constructor(sessionId, http) {
this._result = null;
this._sendPromise = null;
this.id = sessionId;
this._http = http;
}
_fail(err) {
if (this._result === null) {
const msg = err instanceof Error ? err.message : `failed to send message: ${err}`;
this._result = {
id: this.id,
status: "error",
subtype: "error_during_execution",
error: msg,
ok: false
};
}
}
async *stream() {
let finishReason;
let usageData;
let runStatus = "finished";
const resultParts = [];
let numTurns = 0;
const startMs = Date.now();
for await (const event of this._http.streamEvents(
`/sessions/${this.id}/events`
)) {
const evType = event.type;
const data = event.data;
if (evType === "message" || evType === "") {
const msg = parseMessage(data, this.id);
if (msg) {
if (msg.type === "assistant") {
numTurns++;
for (const block of msg.content) {
if (block.type === "text") resultParts.push(block.text);
}
}
yield msg;
}
} else if (evType === "partial_message") {
yield {
type: "system",
subtype: "partial_message",
data
};
} else if (evType === "tool_progress") {
const tp = {
type: "tool_progress",
toolUseId: String(data["tool_use_id"] ?? ""),
toolName: String(data["tool_name"] ?? ""),
elapsedMs: Number(data["elapsed_ms"] ?? 0),
sessionId: this.id
};
yield tp;
} else if (evType === "done") {
finishReason = data["finish_reason"];
usageData = data["usage"];
runStatus = data["status"] ?? "finished";
break;
} else if (evType === "error") {
runStatus = "error";
this._result = {
id: this.id,
status: "error",
subtype: "error_during_execution",
error: String(data["message"] ?? data),
ok: false,
numTurns,
durationMs: Date.now() - startMs
};
return;
}
}
const usage = usageData ? parseUsage(usageData) : void 0;
this._result = {
id: this.id,
status: runStatus,
subtype: mapFinishReasonToSubtype(finishReason, runStatus),
finishReason,
usage,
ok: runStatus === "finished",
result: resultParts.length > 0 ? resultParts.join("") : void 0,
numTurns,
durationMs: Date.now() - startMs
};
}
messages() {
return this.stream();
}
async *iterText() {
for await (const msg of this.stream()) {
if (msg.type === "assistant") {
for (const block of msg.content) {
if (block.type === "text") yield block.text;
}
}
}
}
async text() {
const chunks = [];
for await (const chunk of this.iterText()) {
chunks.push(chunk);
}
return chunks.join("");
}
async wait() {
if (this._result === null) {
for await (const _ of this.stream()) {
}
}
if (this._sendPromise) {
await this._sendPromise;
}
return this._result;
}
async cancel() {
try {
await this._http.post(`/sessions/${this.id}/interrupt`, {});
} catch {
}
}
supports(operation) {
return ["stream", "messages", "iterText", "text", "wait", "cancel"].includes(
operation
);
}
};
function parseMessage(data, sessionId) {
const role = data["role"];
const contentRaw = data["content"];
if (role === "assistant") {
const content = [];
if (typeof contentRaw === "string") {
content.push({ type: "text", text: contentRaw });
} else if (Array.isArray(contentRaw)) {
for (const item of contentRaw) {
const t = item["type"];
if (t === "text") {
content.push({ type: "text", text: String(item["text"] ?? "") });
} else if (t === "tool_use") {
content.push({
type: "tool_use",
id: String(item["id"] ?? ""),
name: String(item["name"] ?? ""),
input: item["input"] ?? {}
});
}
}
}
return { type: "assistant", content, sessionId };
}
if (role === "user") {
return {
type: "user",
content: typeof contentRaw === "string" ? contentRaw : String(contentRaw ?? ""),
sessionId
};
}
if (role === "system" || data["type"] === "system") {
return {
type: "system",
subtype: String(data["subtype"] ?? ""),
data
};
}
return null;
}
function parseUsage(data) {
return {
inputTokens: Number(data["input_tokens"] ?? 0),
outputTokens: Number(data["output_tokens"] ?? 0),
cacheCreationTokens: data["cache_creation_tokens"] != null ? Number(data["cache_creation_tokens"]) : void 0,
cacheReadTokens: data["cache_read_tokens"] != null ? Number(data["cache_read_tokens"]) : void 0,
reasoningTokens: data["reasoning_tokens"] != null ? Number(data["reasoning_tokens"]) : void 0
};
}
var AgentSession = class {
constructor(sessionId, http, options) {
this._closed = false;
this.sessionId = sessionId;
this._http = http;
this._ownsSession = options.ownsSession;
}
async send(message) {
if (this._closed) {
throw new RecursiveAgentError("Agent session is already closed.");
}
const run = new Run(this.sessionId, this._http);
const sendPromise = this._http.post(`/sessions/${this.sessionId}/messages`, { content: message }).catch((err) => {
run._fail(err);
});
run._sendPromise = sendPromise;
return run;
}
async close() {
if (!this._closed) {
this._closed = true;
if (this._ownsSession) {
try {
await this._http.delete(`/sessions/${this.sessionId}`);
} catch {
}
}
}
}
async [Symbol.asyncDispose]() {
return this.close();
}
};
var Agent = class {
static async create(options = {}) {
const http = makeClient(options);
const body = {};
if (options.systemPrompt) body["system_prompt"] = options.systemPrompt;
const data = await http.post("/sessions", body);
return new AgentSession(data.id, http, { ownsSession: true });
}
static async resume(sessionId, options = {}) {
const http = makeClient(options);
await http.get(`/sessions/${sessionId}`);
return new AgentSession(sessionId, http, { ownsSession: false });
}
static async prompt(message, options = {}) {
const http = makeClient(options);
const body = { goal: message };
if (options.systemPrompt) body["system_prompt"] = options.systemPrompt;
if (options.maxSteps != null) body["max_steps"] = options.maxSteps;
const data = await http.post("/run", body);
const usageRaw = data["usage"];
const status = data["status"] ?? "finished";
const finishReason = data["finish_reason"];
return {
id: String(data["session_id"] ?? ""),
status,
subtype: mapFinishReasonToSubtype(finishReason, status),
finishReason,
error: data["error"],
usage: usageRaw ? {
inputTokens: Number(usageRaw["input_tokens"] ?? 0),
outputTokens: Number(usageRaw["output_tokens"] ?? 0)
} : void 0,
ok: status === "finished"
};
}
static async listSessions(pagination = {}, options = {}) {
const http = makeClient(options);
const params = new URLSearchParams();
if (pagination.limit != null) params.set("limit", String(pagination.limit));
if (pagination.offset != null) params.set("offset", String(pagination.offset));
const url = params.size > 0 ? `/sessions?${params}` : "/sessions";
const data = await http.get(url);
return data.map((s) => ({
id: String(s["id"]),
createdAt: String(s["created_at"] ?? ""),
messageCount: Number(s["message_count"] ?? 0),
lastPrompt: s["last_prompt"],
firstPrompt: s["first_prompt"],
goal: s["goal"],
title: s["title"]
}));
}
static async renameSession(sessionId, title, options = {}) {
const http = makeClient(options);
await http.patch(`/sessions/${sessionId}`, { title });
}
static async getSessionMessages(sessionId, options = {}) {
const http = makeClient(options);
const data = await http.get(`/sessions/${sessionId}`);
return data["messages"] ?? [];
}
static async deleteSession(sessionId, options = {}) {
const http = makeClient(options);
await http.delete(`/sessions/${sessionId}`);
}
};
function makeClient(options) {
const baseUrl = options.baseUrl ?? process.env["RECURSIVE_BASE_URL"] ?? "http://127.0.0.1:3000";
return new HttpClient({ baseUrl, apiKey: options.apiKey });
}
var RecursiveClient = class {
constructor(options = {}) {
const baseUrl = options.baseUrl ?? process.env["RECURSIVE_BASE_URL"] ?? "http://127.0.0.1:3000";
this.baseUrl = baseUrl.replace(/\/$/, "");
this._http = new HttpClient({ baseUrl, apiKey: options.apiKey });
}
async health() {
const url = `${this.baseUrl}/health`;
const headers = {};
const apiKey = process.env["RECURSIVE_API_KEY"];
if (apiKey) headers["x-api-key"] = apiKey;
const resp = await fetch(url, { method: "GET", headers });
return resp.text();
}
async listTools() {
const data = await this._http.get("/tools");
return data.map((t) => ({
name: String(t["name"] ?? ""),
description: String(t["description"] ?? ""),
parameters: t["parameters"] ?? {}
}));
}
async listSessions() {
const data = await this._http.get("/sessions");
return data.map((s) => ({
id: String(s["id"]),
createdAt: String(s["created_at"] ?? ""),
messageCount: Number(s["message_count"] ?? 0),
lastPrompt: s["last_prompt"],
firstPrompt: s["first_prompt"],
goal: s["goal"]
}));
}
async getSession(sessionId) {
const data = await this._http.get(
`/sessions/${sessionId}`
);
const detail = {
id: String(data["id"] ?? sessionId),
createdAt: String(data["created_at"] ?? ""),
messages: data["messages"] ?? [],
status: String(data["status"] ?? "idle")
};
if (data["pending_plan"] != null) {
detail.pendingPlan = String(data["pending_plan"]);
}
if (data["todos"] != null) {
detail.todos = data["todos"];
}
const goal = data["goal"];
if (goal && typeof goal === "object") {
detail.goal = parseGoalState(goal);
}
return detail;
}
async deleteSession(sessionId) {
await this._http.delete(`/sessions/${sessionId}`);
}
async getSessionMessages(sessionId) {
const detail = await this.getSession(sessionId);
return detail.messages;
}
async approvePlan(sessionId, options = {}) {
const body = {};
if (options.edits != null) body["edits"] = options.edits;
const data = await this._http.post(
`/sessions/${sessionId}/plan/confirm`,
body
);
return {
status: data["status"] ?? "approved",
sessionId: String(data["session_id"] ?? sessionId)
};
}
async rejectPlan(sessionId, options = {}) {
const data = await this._http.post(
`/sessions/${sessionId}/plan/reject`,
{ reason: options.reason ?? "" }
);
return {
status: data["status"] ?? "rejected",
sessionId: String(data["session_id"] ?? sessionId)
};
}
async setGoal(sessionId, condition, options = {}) {
const data = await this._http.post(
`/sessions/${sessionId}/goal`,
{ condition, max_turns: options.maxTurns ?? 20 }
);
return {
status: String(data["status"] ?? "pursuing"),
sessionId: String(data["session_id"] ?? sessionId)
};
}
async clearGoal(sessionId) {
const url = `${this.baseUrl}/sessions/${sessionId}/goal`;
const headers = {
"Content-Type": "application/json"
};
const apiKey = process.env["RECURSIVE_API_KEY"];
if (apiKey) headers["x-api-key"] = apiKey;
const resp = await fetch(url, { method: "DELETE", headers });
if (!resp.ok) {
const text = await resp.text();
throw new Error(`HTTP ${resp.status}: ${text}`);
}
const data = await resp.json();
return {
status: String(data["status"] ?? "cleared"),
sessionId: String(data["session_id"] ?? sessionId)
};
}
async getGoal(sessionId) {
const detail = await this.getSession(sessionId);
return detail.goal ?? null;
}
async listSlashCommands() {
const data = await this._http.get("/slash-commands");
return data.map((c) => ({
name: String(c["name"] ?? ""),
description: String(c["description"] ?? ""),
source: String(c["source"] ?? ""),
aliases: c["aliases"] ?? [],
argumentHint: String(c["argument_hint"] ?? "")
}));
}
};
function parseGoalState(raw) {
return {
condition: String(raw["condition"] ?? ""),
status: String(raw["status"] ?? ""),
turns: Number(raw["turns"] ?? 0),
maxTurns: Number(raw["max_turns"] ?? 0),
lastReason: raw["last_reason"]
};
}
0 && (module.exports = {
Agent,
AgentSession,
RecursiveAgentError,
RecursiveClient,
Run,
mapFinishReasonToSubtype
});