eve
Version:
Filesystem-first framework for durable backend AI agents that run anywhere.
1,313 lines (1,294 loc) • 43.4 kB
JavaScript
import { computed, onScopeDispose, shallowRef } from "vue";
//#region src/protocol/routes.ts
const EVE_ROUTE_PREFIX = "/eve/v1";
const EVE_HEALTH_ROUTE_PATH = `${EVE_ROUTE_PREFIX}/health`;
const EVE_INFO_ROUTE_PATH = `${EVE_ROUTE_PREFIX}/info`;
const EVE_CREATE_SESSION_ROUTE_PATH = `${EVE_ROUTE_PREFIX}/session`;
const EVE_CONTINUE_SESSION_ROUTE_PATTERN = `${EVE_ROUTE_PREFIX}/session/:sessionId`;
const EVE_MESSAGE_STREAM_ROUTE_PATTERN = `${EVE_ROUTE_PREFIX}/session/:sessionId/stream`;
const EVE_DEV_DISPATCH_SCHEDULE_ROUTE_PATTERN = `${EVE_ROUTE_PREFIX}/dev/schedules/:scheduleId`;
const EVE_DEV_RUNTIME_ARTIFACTS_ROUTE_PATH = `${EVE_ROUTE_PREFIX}/dev/runtime-artifacts`;
const EVE_DEV_RUNTIME_ARTIFACTS_REBUILD_ROUTE_PATH = `${EVE_DEV_RUNTIME_ARTIFACTS_ROUTE_PATH}/rebuild`;
const EVE_CONNECTION_CALLBACK_ROUTE_PATTERN = `${EVE_ROUTE_PREFIX}/connections/:name/callback/:token`;
const EVE_CALLBACK_ROUTE_PATTERN = `${EVE_ROUTE_PREFIX}/callback/:token`;
function createEveMessageStreamRoutePath(sessionId) {
return `${EVE_ROUTE_PREFIX}/session/${encodeURIComponent(sessionId)}/stream`;
}
function createEveContinueSessionRoutePath(sessionId) {
return `${EVE_ROUTE_PREFIX}/session/${encodeURIComponent(sessionId)}`;
}
//#endregion
//#region src/shared/guards.ts
function isObject(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
//#endregion
//#region src/client/client-error.ts
var ClientError = class extends Error {
status;
body;
constructor(status, body) {
let message = body || `Server returned ${status}.`;
try {
const parsed = JSON.parse(body);
if (isObject(parsed) && typeof parsed.error === "string") message = parsed.error;
} catch {}
super(message);
this.name = "ClientError";
this.status = status;
this.body = body;
}
};
//#endregion
//#region src/protocol/message.ts
const EVE_SESSION_ID_HEADER = "x-eve-session-id";
const textEncoder = new TextEncoder();
function isCurrentTurnBoundaryEvent(event) {
return event.type === "session.completed" || event.type === "session.failed" || event.type === "session.waiting";
}
//#endregion
//#region src/shared/json.ts
const INVALID_JSON_VALUE_CANDIDATE = Symbol("invalid-json-value-candidate");
const JSON_VALUE_ERROR_MESSAGE = "Expected a JSON-serializable value.";
const JSON_OBJECT_ERROR_MESSAGE = "Expected a JSON-serializable object.";
function parseJsonValue(value) {
const normalized = normalizeJsonValueCandidate(value);
if (normalized === INVALID_JSON_VALUE_CANDIDATE) throw new TypeError(JSON_VALUE_ERROR_MESSAGE);
return normalized;
}
function parseJsonObject(value) {
const normalized = parseJsonValue(value);
if (!isJsonObjectValue(normalized)) throw new TypeError(JSON_OBJECT_ERROR_MESSAGE);
return normalized;
}
function normalizeJsonValueCandidate(value, seen = /* @__PURE__ */ new WeakSet()) {
if (value === null || typeof value === "boolean" || typeof value === "string") return value;
if (typeof value === "number") return Number.isFinite(value) ? value : INVALID_JSON_VALUE_CANDIDATE;
if (Array.isArray(value)) {
const normalizedItems = [];
for (const item of value) {
const normalizedItem = normalizeJsonValueCandidate(item, seen);
if (normalizedItem === INVALID_JSON_VALUE_CANDIDATE) return INVALID_JSON_VALUE_CANDIDATE;
normalizedItems.push(normalizedItem);
}
return normalizedItems;
}
if (typeof value !== "object" || value === void 0) return INVALID_JSON_VALUE_CANDIDATE;
if (!isPlainObject(value)) return INVALID_JSON_VALUE_CANDIDATE;
if (seen.has(value)) return INVALID_JSON_VALUE_CANDIDATE;
seen.add(value);
const normalized = {};
for (const [key, entry] of Object.entries(value)) {
if (entry === void 0) continue;
const normalizedEntry = normalizeJsonValueCandidate(entry, seen);
if (normalizedEntry === INVALID_JSON_VALUE_CANDIDATE) return INVALID_JSON_VALUE_CANDIDATE;
normalized[key] = normalizedEntry;
}
seen.delete(value);
return normalized;
}
function isJsonObjectValue(value) {
return value !== null && !Array.isArray(value) && typeof value === "object";
}
function isPlainObject(value) {
const prototype = Object.getPrototypeOf(value);
return prototype === null || prototype === Object.prototype;
}
//#endregion
//#region src/shared/json-schema.ts
const STANDARD_JSON_SCHEMA_TARGET = "draft-07";
function normalizeJsonSchemaDefinition(value, direction = "input") {
if (isStandardSchema(value)) return parseJsonObject(value["~standard"].jsonSchema[direction]({ target: STANDARD_JSON_SCHEMA_TARGET }));
return parseJsonObject(value);
}
function isStandardSchema(value) {
return value !== null && typeof value === "object" && "~standard" in value;
}
//#endregion
//#region src/client/output-schema.ts
function normalizeOutputSchemaForRequest(schema) {
return schema === void 0 ? void 0 : normalizeJsonSchemaDefinition(schema, "output");
}
function extractCompletedResult(events) {
let result;
for (const event of events) if (isResultCompletedEvent(event)) result = event.data.result;
return result;
}
function isResultCompletedEvent(event) {
return event.type === "result.completed";
}
//#endregion
//#region src/client/session-utils.ts
function createInitialSessionState() {
return { streamIndex: 0 };
}
function advanceSession(input) {
const boundaryEvent = findBoundaryEvent(input.events);
const streamIndex = input.session.streamIndex + input.events.length;
if (boundaryEvent?.type === "session.waiting" || input.preserveCompletedSessions === true && boundaryEvent?.type === "session.completed") return {
continuationToken: input.continuationToken ?? input.session.continuationToken,
sessionId: input.sessionId,
streamIndex
};
return createInitialSessionState();
}
function extractCompletedMessage(events) {
let lastMessage;
for (const event of events) if (isFinalMessageCompleted(event)) lastMessage = event.data.message ?? void 0;
return lastMessage;
}
function deriveResultStatus(events) {
const boundary = findBoundaryEvent(events);
if (boundary?.type === "session.waiting") return "waiting";
if (boundary?.type === "session.failed") return "failed";
return "completed";
}
function extractInputRequests(events) {
const requests = [];
for (const event of events) if (event.type === "input.requested") requests.push(...event.data.requests);
return requests;
}
function findBoundaryEvent(events) {
for (let i = events.length - 1; i >= 0; i--) {
const event = events[i];
if (event !== void 0 && isCurrentTurnBoundaryEvent(event)) return event;
}
}
function isFinalMessageCompleted(event) {
return event.type === "message.completed" && event.data.finishReason !== "tool-calls";
}
//#endregion
//#region src/client/message-response.ts
var MessageResponse = class {
continuationToken;
sessionId;
#consumed = false;
#createStream;
constructor(input) {
this.continuationToken = input.continuationToken;
this.sessionId = input.sessionId;
this.#createStream = input.createStream;
}
async result() {
const events = [];
for await (const event of this) events.push(event);
return {
data: extractCompletedResult(events),
events,
inputRequests: extractInputRequests(events),
message: extractCompletedMessage(events),
sessionId: this.sessionId,
status: deriveResultStatus(events)
};
}
[Symbol.asyncIterator]() {
if (this.#consumed) throw new Error("MessageResponse has already been consumed.");
this.#consumed = true;
return this.#createStream();
}
};
//#endregion
//#region src/client/ndjson.ts
function isStreamDisconnectError(error) {
if (error instanceof DOMException) return error.name === "AbortError";
if (!(error instanceof Error)) return false;
const errorCode = "code" in error && typeof error.code === "string" ? error.code : void 0;
return error.name === "AbortError" || error.message === "terminated" || errorCode === "UND_ERR_SOCKET" || /abort|cancel|disconnect|premature close|socket|terminated/i.test(error.message);
}
async function* readNdjsonStream(body) {
const reader = body.getReader();
const decoder = new TextDecoder();
let buffer = "";
try {
while (true) {
const result = await reader.read();
if (result.done) {
buffer += decoder.decode();
break;
}
if (result.value) buffer += decoder.decode(result.value, { stream: true });
let newlineIndex = buffer.indexOf("\n");
while (newlineIndex !== -1) {
const line = buffer.slice(0, newlineIndex).trim();
buffer = buffer.slice(newlineIndex + 1);
if (line.length > 0) yield JSON.parse(line);
newlineIndex = buffer.indexOf("\n");
}
}
const trailing = buffer.trim();
if (trailing.length > 0) yield JSON.parse(trailing);
} finally {
reader.releaseLock();
}
}
//#endregion
//#region src/client/url.ts
function createClientUrl(host, routePath, searchParams) {
const normalizedRoute = routePath.startsWith("/") ? routePath : `/${routePath}`;
const search = formatSearch(searchParams);
if (isAbsoluteUrl(host)) {
const url = new URL(host);
url.pathname = `${trimTrailingSlash(url.pathname)}${normalizedRoute}`;
url.search = search;
url.hash = "";
return url.toString();
}
return `${trimTrailingSlash(host)}${normalizedRoute}${search}`;
}
function isAbsoluteUrl(value) {
return /^[a-z][a-z\d+\-.]*:/i.test(value);
}
function trimTrailingSlash(value) {
if (value === "/") return "";
return value.endsWith("/") ? value.slice(0, -1) : value;
}
function formatSearch(searchParams) {
if (!searchParams || Object.keys(searchParams).length === 0) return "";
return `?${new URLSearchParams(searchParams).toString()}`;
}
//#endregion
//#region src/client/open-stream.ts
const STREAM_OPEN_RETRY_ATTEMPTS = 12;
const STREAM_OPEN_RETRY_DELAY_MS = 250;
const STREAM_OPEN_RETRYABLE_STATUS = new Set([
404,
409,
425,
500,
502,
503,
504
]);
async function* openStreamIterable(input) {
let startIndex = input.startIndex;
let remainingReconnectAttempts = input.maxReconnectAttempts;
while (true) {
const body = await openStreamBody({
...input,
startIndex
});
let disconnected = false;
try {
for await (const event of readNdjsonStream(body)) {
startIndex += 1;
yield event;
}
} catch (error) {
if (!isStreamDisconnectError(error)) throw error;
disconnected = true;
}
if (!disconnected || input.signal?.aborted || remainingReconnectAttempts <= 0) return;
remainingReconnectAttempts -= 1;
}
}
async function openStreamBody(input) {
let lastStatus;
let lastBody;
for (let attempt = 0; attempt < STREAM_OPEN_RETRY_ATTEMPTS; attempt += 1) {
const url = createClientUrl(input.host, createEveMessageStreamRoutePath(input.sessionId), input.startIndex > 0 ? { startIndex: String(input.startIndex) } : void 0);
const headers = await input.resolveHeaders();
const response = await fetch(url, {
headers,
redirect: input.redirect,
signal: input.signal ?? null
});
if (response.ok) {
if (!response.body) throw new ClientError(response.status, "Response body is null.");
return response.body;
}
lastStatus = response.status;
lastBody = await response.text();
if (!STREAM_OPEN_RETRYABLE_STATUS.has(response.status)) throw new ClientError(response.status, lastBody);
if (attempt < STREAM_OPEN_RETRY_ATTEMPTS - 1) await sleep$1(STREAM_OPEN_RETRY_DELAY_MS);
}
throw new ClientError(lastStatus ?? 0, lastBody ?? "Failed to open message stream.");
}
async function sleep$1(ms) {
await new Promise((resolve) => setTimeout(resolve, ms));
}
//#endregion
//#region src/client/session.ts
const DELIVER_RETRY_ATTEMPTS = 10;
const DELIVER_RETRY_DELAY_MS = 200;
var ClientSession = class {
#context;
#state;
constructor(context, state) {
this.#context = context;
this.#state = state;
}
get state() {
return this.#state;
}
async send(input) {
const payload = normalizeSendTurnInput(input);
const state = this.#state;
const { continuationToken, sessionId } = await this.#postTurn(payload, state);
return new MessageResponse({
continuationToken,
createStream: () => this.#createEventStream(sessionId, continuationToken, state, payload),
sessionId
});
}
stream(options) {
const sessionId = this.#state.sessionId;
if (!sessionId) throw new Error("Session has no session ID. Send a message first.");
return this.#streamAndAdvance(sessionId, options);
}
async #postTurn(input, session) {
const routePath = session.sessionId ? createEveContinueSessionRoutePath(session.sessionId) : EVE_CREATE_SESSION_ROUTE_PATH;
const url = createClientUrl(this.#context.host, routePath);
const headers = await this.#context.resolveHeaders(input.headers);
headers.set("content-type", "application/json");
const body = createHandleMessageBody({
input,
outputSchema: normalizeOutputSchemaForRequest(input.outputSchema),
session
});
if (body === null) throw new Error("Session.send requires a non-empty message, inputResponses, or both.");
const response = await postTurnWithRetry({
body: JSON.stringify(body),
headers,
mustDeliver: (input.inputResponses?.length ?? 0) > 0,
redirect: this.#context.redirect,
signal: input.signal,
url
});
const payload = await response.json();
const sessionId = (typeof payload.sessionId === "string" ? payload.sessionId : void 0) ?? response.headers.get("x-eve-session-id")?.trim() ?? session.sessionId;
if (!sessionId) throw new Error("Message route did not return a session id.");
return {
continuationToken: typeof payload.continuationToken === "string" ? payload.continuationToken : void 0,
sessionId
};
}
async *#createEventStream(sessionId, continuationToken, initialState, input) {
const events = [];
try {
let currentStreamIndex = initialState.sessionId === sessionId ? initialState.streamIndex : 0;
let remainingReconnectAttempts = this.#context.maxReconnectAttempts;
while (true) {
const body = await this.#openStreamBody(sessionId, currentStreamIndex, input.signal, input.headers);
let foundBoundary = false;
try {
for await (const event of readNdjsonStream(body)) {
events.push(event);
currentStreamIndex += 1;
yield event;
if (isCurrentTurnBoundaryEvent(event)) {
foundBoundary = true;
break;
}
}
} catch (error) {
if (!isStreamDisconnectError(error)) throw error;
}
if (foundBoundary) break;
if (input.signal?.aborted) break;
if (remainingReconnectAttempts <= 0) break;
remainingReconnectAttempts -= 1;
}
} finally {
this.#state = advanceSession({
continuationToken,
events,
preserveCompletedSessions: this.#context.preserveCompletedSessions,
sessionId,
session: initialState
});
}
}
async #openStreamBody(sessionId, startIndex, signal, headers) {
return await openStreamBody({
host: this.#context.host,
resolveHeaders: () => this.#context.resolveHeaders(headers),
redirect: this.#context.redirect,
sessionId,
signal,
startIndex
});
}
async *#streamAndAdvance(sessionId, options) {
const initialState = this.#state;
const streamIndex = options?.startIndex ?? initialState.streamIndex;
const events = [];
try {
for await (const event of openStreamIterable({
host: this.#context.host,
maxReconnectAttempts: this.#context.maxReconnectAttempts,
resolveHeaders: () => this.#context.resolveHeaders(),
redirect: this.#context.redirect,
sessionId,
signal: options?.signal,
startIndex: streamIndex
})) {
events.push(event);
yield event;
}
} finally {
this.#state = advanceSession({
continuationToken: initialState.continuationToken,
events,
preserveCompletedSessions: this.#context.preserveCompletedSessions,
session: {
...initialState,
sessionId,
streamIndex
},
sessionId
});
}
}
};
async function postTurnWithRetry(input) {
const attempts = input.mustDeliver ? DELIVER_RETRY_ATTEMPTS : 1;
let lastStatus;
let lastBody;
for (let attempt = 0; attempt < attempts; attempt += 1) {
const response = await fetch(input.url, {
body: input.body,
headers: input.headers,
method: "POST",
redirect: input.redirect,
signal: input.signal ?? null
});
if (response.ok) return response;
lastStatus = response.status;
lastBody = await response.text();
if (!isRetryableDeliveryFailure(response.status, lastBody)) throw new ClientError(response.status, lastBody);
if (attempt < attempts - 1) await sleep(DELIVER_RETRY_DELAY_MS);
}
throw new ClientError(lastStatus ?? 0, lastBody ?? "Failed to deliver session turn.");
}
function isRetryableDeliveryFailure(status, body) {
return status === 500 && /target session was not found/i.test(body);
}
async function sleep(ms) {
await new Promise((resolve) => setTimeout(resolve, ms));
}
function normalizeSendTurnInput(input) {
return typeof input === "string" ? { message: input } : input;
}
function createHandleMessageBody(input) {
const body = {};
if (input.input.message !== void 0) body.message = input.input.message;
if (input.input.inputResponses !== void 0 && input.input.inputResponses.length > 0) body.inputResponses = input.input.inputResponses;
if (input.input.clientContext !== void 0) body.clientContext = input.input.clientContext;
if (input.outputSchema !== void 0) body.outputSchema = input.outputSchema;
if (input.session.continuationToken !== void 0) body.continuationToken = input.session.continuationToken;
if (Object.keys(body).length === 0) return null;
if (input.session.continuationToken === void 0 && body.message === void 0) return null;
if (input.session.continuationToken !== void 0 && body.message === void 0 && body.inputResponses === void 0) return null;
return body;
}
//#endregion
//#region src/client/client.ts
var Client = class {
#auth;
#headers;
#host;
#maxReconnectAttempts;
#preserveCompletedSessions;
#redirect;
constructor(options) {
this.#host = options.host;
this.#auth = options.auth;
this.#headers = options.headers;
this.#maxReconnectAttempts = options.maxReconnectAttempts ?? 3;
this.#preserveCompletedSessions = options.preserveCompletedSessions ?? false;
this.#redirect = options.redirect;
}
async health() {
const url = createClientUrl(this.#host, EVE_HEALTH_ROUTE_PATH);
const headers = await this.#resolveHeaders();
const response = await fetch(url, withRedirectPolicy({ headers }, this.#redirect));
if (!response.ok) {
const body = await response.text();
throw new ClientError(response.status, body);
}
return await response.json();
}
async info() {
const url = createClientUrl(this.#host, EVE_INFO_ROUTE_PATH);
const headers = await this.#resolveHeaders();
const response = await fetch(url, withRedirectPolicy({ headers }, this.#redirect));
if (!response.ok) {
const body = await response.text();
throw new ClientError(response.status, body);
}
return await response.json();
}
async fetch(path, init = {}) {
const url = createClientUrl(this.#host, path);
const headers = await this.#resolveHeaders(headersInitToRecord(init.headers));
return await fetch(url, withRedirectPolicy({
...init,
headers
}, this.#redirect));
}
session(state) {
let resolved;
if (typeof state === "string") resolved = {
continuationToken: state,
streamIndex: 0
};
else if (state) resolved = state;
else resolved = createInitialSessionState();
return new ClientSession({
host: this.#host,
maxReconnectAttempts: this.#maxReconnectAttempts,
preserveCompletedSessions: this.#preserveCompletedSessions,
redirect: this.#redirect,
resolveHeaders: (perRequest) => this.#resolveHeaders(perRequest)
}, resolved);
}
async #resolveHeaders(perRequest) {
const headers = new Headers();
const baseHeaders = await resolveHeadersValue(this.#headers);
for (const [key, value] of Object.entries(baseHeaders)) headers.set(key, value);
if (perRequest) for (const [key, value] of Object.entries(perRequest)) headers.set(key, value);
const authorization = await this.#resolveAuthorizationHeader();
if (authorization) headers.set("authorization", authorization);
return headers;
}
async #resolveAuthorizationHeader() {
const auth = this.#auth;
if (!auth) return void 0;
if ("bearer" in auth) {
const token = (await resolveTokenValue(auth.bearer)).trim();
if (token.length === 0) return void 0;
return `Bearer ${token}`;
}
if ("basic" in auth) {
const password = await resolveTokenValue(auth.basic.password);
return `Basic ${encodeBasicCredentials(auth.basic.username, password)}`;
}
}
};
async function resolveTokenValue(value) {
return typeof value === "function" ? value() : value;
}
async function resolveHeadersValue(value) {
if (value === void 0) return {};
return typeof value === "function" ? await value() : value;
}
function headersInitToRecord(headers) {
if (headers === void 0) return {};
return Object.fromEntries(new Headers(headers).entries());
}
function withRedirectPolicy(init, redirect) {
return redirect === void 0 ? init : {
...init,
redirect
};
}
function encodeBasicCredentials(username, password) {
const bytes = new TextEncoder().encode(`${username}:${password}`);
const binaryString = Array.from(bytes, (byte) => String.fromCodePoint(byte)).join("");
return btoa(binaryString);
}
//#endregion
//#region src/shared/errors.ts
function toErrorMessage(error) {
if (error instanceof Error) return error.message;
if (typeof error === "string") return error;
if (error === null || error === void 0) return String(error);
if (isObject(error)) {
if (typeof error.message === "string" && error.message.length > 0) return error.message;
return safeJsonStringify(error);
}
return String(error);
}
function toError(raw) {
if (raw instanceof Error) return raw;
const error = new Error(toErrorMessage(raw));
if (!isObject(raw)) return error;
if (typeof raw.name === "string" && raw.name.length > 0) error.name = raw.name;
if (typeof raw.stack === "string" && raw.stack.length > 0) error.stack = raw.stack;
if ("cause" in raw && raw.cause !== void 0 && raw.cause !== raw) error.cause = raw.cause;
return error;
}
function safeJsonStringify(value) {
try {
return JSON.stringify(value) ?? String(value);
} catch {
return String(value);
}
}
//#endregion
//#region src/client/eve-agent-store.ts
var EveAgentStore = class {
#createSession;
#optimistic;
#reducer;
#subscribers = /* @__PURE__ */ new Set();
#abortController;
#callbacks = {};
#data;
#error;
#events;
#operationId = 0;
#pendingMessageSubmission;
#projectionEvents;
#session;
#snapshot;
#status = "ready";
constructor(init) {
this.#createSession = init.session ? void 0 : () => new Client({
auth: init.auth,
headers: init.headers,
host: init.host ?? "",
maxReconnectAttempts: init.maxReconnectAttempts
}).session(init.initialSession);
this.#events = [...init.initialEvents ?? []];
this.#projectionEvents = [...this.#events];
this.#optimistic = init.optimistic ?? true;
this.#reducer = init.reducer;
this.#session = init.session ?? this.#createOwnedSession();
this.#data = this.#reduceProjectionEvents(this.#projectionEvents);
this.#snapshot = this.#createSnapshot();
}
get snapshot() {
return this.#snapshot;
}
setCallbacks(callbacks) {
this.#callbacks = callbacks;
}
subscribe(callback) {
this.#subscribers.add(callback);
return () => {
this.#subscribers.delete(callback);
};
}
async send(input) {
if (this.#status === "streaming" || this.#status === "submitted") throw new Error("eve session is already processing a turn.");
const operationId = this.#startOperation();
const abortController = new AbortController();
this.#abortController = abortController;
this.#error = void 0;
this.#status = "submitted";
this.#publish();
try {
const preparedInput = await this.#callbacks.prepareSend?.(input) ?? input;
if (!this.#isCurrentOperation(operationId)) return;
this.#projectOptimisticMessage(preparedInput);
this.#projectInputResponses(preparedInput);
this.#publish();
const response = await this.#session.send({
...preparedInput,
signal: createAbortSignal(preparedInput.signal, abortController.signal)
});
let sawEvent = false;
for await (const event of response) {
if (!this.#isCurrentOperation(operationId)) return;
if (!sawEvent) {
sawEvent = true;
this.#status = "streaming";
}
this.#events = [...this.#events, event];
this.#applyServerEvent(event);
this.#callbacks.onEvent?.(event);
this.#applyTerminalStreamFailure(event);
this.#publish();
}
if (!this.#isCurrentOperation(operationId)) return;
this.#status = this.#error === void 0 ? "ready" : "error";
} catch (error) {
if (!this.#isCurrentOperation(operationId)) return;
if (isAbortError(error)) {
this.#status = "ready";
this.#failPendingMessageSubmission(toError(error));
} else {
this.#error = toError(error);
this.#status = "error";
this.#failPendingMessageSubmission(this.#error);
this.#callbacks.onError?.(this.#error);
}
} finally {
if (this.#isCurrentOperation(operationId)) {
this.#abortController = void 0;
this.#callbacks.onSessionChange?.(this.#session.state);
this.#publish();
this.#callbacks.onFinish?.(this.#snapshot);
}
}
}
stop() {
this.#abortController?.abort();
}
reset() {
this.#invalidateOperation();
this.stop();
this.#abortController = void 0;
this.#session = this.#createSession?.() ?? this.#session;
this.#events = [];
this.#pendingMessageSubmission = void 0;
this.#projectionEvents = [];
this.#data = this.#reducer.initial();
this.#error = void 0;
this.#status = "ready";
this.#callbacks.onSessionChange?.(this.#session.state);
this.#publish();
}
#createOwnedSession() {
if (!this.#createSession) throw new Error("Cannot create an owned eve session from an external session.");
return this.#createSession();
}
#startOperation() {
this.#operationId += 1;
return this.#operationId;
}
#invalidateOperation() {
this.#operationId += 1;
}
#isCurrentOperation(operationId) {
return this.#operationId === operationId;
}
#projectOptimisticMessage(input) {
if (!this.#optimistic || input.message === void 0) return;
const id = createSubmissionId();
const pending = {
createdAt: Date.now(),
id,
message: summarizeUserContent(input.message)
};
this.#pendingMessageSubmission = pending;
this.#appendProjectionEvent({
data: {
createdAt: pending.createdAt,
message: pending.message,
submissionId: pending.id
},
type: "client.message.submitted"
});
}
#projectInputResponses(input) {
if (input.inputResponses === void 0 || input.inputResponses.length === 0) return;
this.#appendProjectionEvent({
data: {
createdAt: Date.now(),
responses: input.inputResponses
},
type: "client.input.responded"
});
}
#applyServerEvent(event) {
if (event.type === "message.received" && this.#pendingMessageSubmission !== void 0) {
const submissionId = this.#pendingMessageSubmission.id;
this.#pendingMessageSubmission = void 0;
this.#replaceProjectionEvent((candidate) => candidate.type === "client.message.submitted" && candidate.data.submissionId === submissionId, event);
return;
}
this.#appendProjectionEvent(event);
}
#applyTerminalStreamFailure(event) {
const error = toTerminalStreamFailureError(event);
if (error === void 0) return;
this.#status = "error";
this.#failPendingMessageSubmission(error);
if (this.#error === void 0) {
this.#error = error;
this.#callbacks.onError?.(error);
}
}
#failPendingMessageSubmission(error) {
const pending = this.#pendingMessageSubmission;
if (pending === void 0) return;
this.#pendingMessageSubmission = void 0;
this.#replaceProjectionEvent((event) => event.type === "client.message.submitted" && event.data.submissionId === pending.id, {
data: {
createdAt: pending.createdAt,
error: { message: error.message },
message: pending.message,
submissionId: pending.id
},
type: "client.message.failed"
});
}
#appendProjectionEvent(event) {
this.#projectionEvents = [...this.#projectionEvents, event];
this.#data = this.#reducer.reduce(this.#data, event);
}
#replaceProjectionEvent(predicate, replacement) {
let replaced = false;
this.#projectionEvents = this.#projectionEvents.map((event) => {
if (!replaced && predicate(event)) {
replaced = true;
return replacement;
}
return event;
});
if (!replaced) this.#projectionEvents = [...this.#projectionEvents, replacement];
this.#data = this.#reduceProjectionEvents(this.#projectionEvents);
}
#reduceProjectionEvents(events) {
let data = this.#reducer.initial();
for (const event of events) data = this.#reducer.reduce(data, event);
return data;
}
#createSnapshot() {
return {
data: this.#data,
error: this.#error,
events: this.#events,
session: this.#session.state,
status: this.#status
};
}
#publish() {
this.#snapshot = this.#createSnapshot();
for (const subscriber of this.#subscribers) subscriber();
}
};
let submissionSequence = 0;
function createSubmissionId() {
const randomUUID = globalThis.crypto?.randomUUID;
if (randomUUID !== void 0) return randomUUID.call(globalThis.crypto);
submissionSequence += 1;
return `submission_${submissionSequence.toString()}`;
}
function createAbortSignal(first, second) {
return first ? AbortSignal.any([first, second]) : second;
}
function summarizeUserContent(message) {
if (typeof message === "string") return message;
const parts = [];
for (const part of message) {
if (part.type === "text") {
parts.push(part.text);
continue;
}
if (part.type === "file") parts.push(part.filename ? `[file: ${part.filename}]` : "[file]");
}
return parts.join("\n");
}
function isAbortError(error) {
return error instanceof Error && error.name === "AbortError";
}
function toTerminalStreamFailureError(event) {
if (event.type !== "session.failed") return;
const error = new Error(event.data.message);
error.name = event.data.code;
return error;
}
//#endregion
//#region src/client/message-reducer.ts
function defaultMessageReducer() {
return {
initial() {
return { messages: [] };
},
reduce(data, event) {
return reduceMessageData(data, event);
}
};
}
function reduceMessageData(data, event) {
switch (event.type) {
case "client.message.submitted": return upsertMessage(data, {
id: optimisticUserMessageId(event.data.submissionId),
metadata: {
optimistic: true,
status: "submitted"
},
parts: [{
type: "text",
text: event.data.message
}],
role: "user"
});
case "client.message.failed": return upsertMessage(data, {
id: optimisticUserMessageId(event.data.submissionId),
metadata: {
optimistic: true,
status: "failed"
},
parts: [{
type: "text",
text: event.data.message
}],
role: "user"
});
case "client.input.responded": {
let next = data;
for (const response of event.data.responses) next = respondToInputRequest(next, response);
return next;
}
case "message.received": return upsertMessage(data, {
id: `${event.data.turnId}:user`,
metadata: {
status: "complete",
turnId: event.data.turnId
},
parts: [{
type: "text",
text: event.data.message,
state: "done"
}],
role: "user"
});
case "step.started": return updateAssistantMessage(data, event.data.turnId, (message) => ensureStepStartPart(message, event.data.stepIndex));
case "reasoning.appended": return updateAssistantMessage(data, event.data.turnId, (message) => upsertPart(ensureStepStartPart(message, event.data.stepIndex), {
state: "streaming",
stepIndex: event.data.stepIndex,
text: event.data.reasoningSoFar,
type: "reasoning"
}));
case "reasoning.completed": return updateAssistantMessage(data, event.data.turnId, (message) => upsertPart(ensureStepStartPart(message, event.data.stepIndex), {
state: "done",
stepIndex: event.data.stepIndex,
text: event.data.reasoning,
type: "reasoning"
}));
case "actions.requested": {
let next = data;
for (const action of event.data.actions) {
const descriptor = normalizeActionRequest(action);
next = updateAssistantMessage(next, event.data.turnId, (message) => upsertPart(ensureStepStartPart(message, event.data.stepIndex), {
input: "input" in action ? action.input : void 0,
state: "input-available",
stepIndex: event.data.stepIndex,
toolCallId: action.callId,
toolMetadata: createToolMetadata(descriptor),
toolName: descriptor.toolName,
type: "dynamic-tool"
}));
}
return next;
}
case "input.requested": {
let next = data;
for (const request of event.data.requests) {
const descriptor = normalizeActionRequest(request.action);
next = updateAssistantMessage(next, event.data.turnId, (message) => upsertPart(ensureStepStartPart(message, event.data.stepIndex), {
approval: { id: request.requestId },
input: request.action.input,
state: "approval-requested",
stepIndex: event.data.stepIndex,
toolCallId: request.action.callId,
toolMetadata: createToolMetadata(descriptor, { inputRequest: toMessageInputRequest(request) }),
toolName: descriptor.toolName,
type: "dynamic-tool"
}));
}
return next;
}
case "action.result": {
const descriptor = normalizeActionResult(event.data.result);
const existing = findToolPart(data, event.data.result.callId);
const denied = event.data.error?.code === "TOOL_EXECUTION_DENIED";
const failed = event.data.status === "failed" && !denied;
const approvalId = existing?.approval?.id ?? event.data.result.callId;
const toolMetadata = mergeToolMetadata(existing?.toolMetadata, createToolMetadata(descriptor));
const resultPartBase = {
input: existing?.input,
stepIndex: event.data.stepIndex,
toolCallId: event.data.result.callId,
toolMetadata,
toolName: existing?.toolName ?? descriptor.toolName,
type: "dynamic-tool"
};
let nextPart;
if (denied) nextPart = {
...resultPartBase,
approval: {
approved: false,
id: approvalId,
reason: event.data.error?.message
},
state: "output-denied"
};
else if (failed) nextPart = {
...resultPartBase,
approval: approvedApproval(existing),
errorText: event.data.error?.message ?? stringifyUnknown(event.data.result.output),
state: "output-error"
};
else nextPart = {
...resultPartBase,
approval: approvedApproval(existing),
output: event.data.result.output,
state: "output-available"
};
if (existing !== void 0) return updateToolPart(data, event.data.result.callId, nextPart);
return updateAssistantMessage(data, event.data.turnId, (message) => upsertPart(ensureStepStartPart(message, event.data.stepIndex), nextPart));
}
case "message.appended": return updateAssistantMessage(data, event.data.turnId, (message) => upsertPart(ensureStepStartPart(message, event.data.stepIndex), {
state: "streaming",
stepIndex: event.data.stepIndex,
text: event.data.messageSoFar,
type: "text"
}));
case "message.completed": return updateAssistantMessage(data, event.data.turnId, (message) => {
if (event.data.message === null) return completeExistingTextPart(message);
return upsertPart(ensureStepStartPart(message, event.data.stepIndex), {
state: "done",
stepIndex: event.data.stepIndex,
text: event.data.message,
type: "text"
});
});
case "result.completed": return updateAssistantMetadata(data, event.data.turnId, { result: event.data.result });
case "turn.completed": return updateAssistantMetadata(data, event.data.turnId, { status: "complete" });
case "turn.failed":
case "session.failed": return data;
default: return data;
}
}
function respondToInputRequest(data, response) {
const existing = findToolPartByApprovalId(data, response.requestId);
if (!existing) return data;
const approval = { id: response.requestId };
if (response.text !== void 0) approval.reason = response.text;
return updateToolPart(data, existing.toolCallId, {
approval,
input: existing.input,
state: "approval-responded",
stepIndex: existing.stepIndex,
toolCallId: existing.toolCallId,
toolMetadata: mergeToolMetadata(existing.toolMetadata, { eve: {
inputResponse: response,
kind: existing.toolMetadata?.eve?.kind ?? "unknown",
name: existing.toolMetadata?.eve?.name ?? existing.toolName
} }),
toolName: existing.toolName,
type: "dynamic-tool"
});
}
function updateAssistantMessage(data, turnId, update) {
return upsertMessage(data, update(data.messages.find((message) => message.role === "assistant" && message.metadata?.turnId === turnId) ?? createAssistantMessage(turnId)));
}
function updateAssistantMetadata(data, turnId, metadata) {
return updateAssistantMessage(data, turnId, (message) => ({
...message,
metadata: {
...message.metadata,
...metadata
}
}));
}
function createAssistantMessage(turnId) {
return {
id: `${turnId}:assistant`,
metadata: {
status: "streaming",
turnId
},
parts: [],
role: "assistant"
};
}
function ensureStepStartPart(message, stepIndex) {
const stepStartCount = message.parts.filter((part) => part.type === "step-start").length;
if (stepStartCount > stepIndex) return message;
const missingCount = stepIndex - stepStartCount + 1;
return {
...message,
parts: [...message.parts, ...Array.from({ length: missingCount }, () => ({ type: "step-start" }))]
};
}
function upsertPart(message, next) {
const index = message.parts.findIndex((part) => partKey(part) === partKey(next));
const parts = index === -1 ? [...message.parts, next] : [
...message.parts.slice(0, index),
next,
...message.parts.slice(index + 1)
];
return {
...message,
metadata: {
...message.metadata,
status: next.type === "text" && next.state === "done" ? "complete" : "streaming"
},
parts
};
}
function completeExistingTextPart(message) {
const index = findLastIndex(message.parts, (part) => part.type === "text");
if (index === -1) return message;
const existing = message.parts[index];
if (existing?.type !== "text") return message;
return {
...message,
metadata: {
...message.metadata,
status: "complete"
},
parts: [
...message.parts.slice(0, index),
{
...existing,
state: "done"
},
...message.parts.slice(index + 1)
]
};
}
function updateToolPart(data, toolCallId, next) {
const message = data.messages.find((candidate) => candidate.role === "assistant" && candidate.parts.some((part) => part.type === "dynamic-tool" && part.toolCallId === toolCallId));
if (!message) return data;
return upsertMessage(data, upsertPart(message, next));
}
function findToolPart(data, toolCallId) {
for (const message of data.messages) for (const part of message.parts) if (part.type === "dynamic-tool" && part.toolCallId === toolCallId) return part;
}
function findToolPartByApprovalId(data, approvalId) {
for (const message of data.messages) for (const part of message.parts) if (part.type === "dynamic-tool" && part.approval?.id === approvalId) return part;
}
function partKey(part) {
switch (part.type) {
case "text": return `text:${part.stepIndex ?? 0}`;
case "reasoning": return `reasoning:${part.stepIndex ?? 0}`;
case "step-start": return "step-start";
case "dynamic-tool": return `dynamic-tool:${part.toolCallId}`;
}
}
function upsertMessage(data, next) {
const index = data.messages.findIndex((message) => message.id === next.id);
if (index === -1) return { messages: [...data.messages, next] };
return { messages: [
...data.messages.slice(0, index),
next,
...data.messages.slice(index + 1)
] };
}
function toMessageInputRequest(request) {
return {
allowFreeform: request.allowFreeform,
display: request.display,
options: request.options,
prompt: request.prompt,
requestId: request.requestId
};
}
function createToolMetadata(descriptor, extra) {
return { eve: {
inputRequest: extra?.inputRequest,
kind: descriptor.kind,
name: descriptor.name
} };
}
function mergeToolMetadata(current, next) {
const kind = next.eve?.kind ?? current?.eve?.kind ?? "unknown";
const name = next.eve?.name ?? current?.eve?.name ?? "unknown";
return { eve: {
...current?.eve,
...next.eve,
inputRequest: next.eve?.inputRequest ?? current?.eve?.inputRequest,
inputResponse: next.eve?.inputResponse ?? current?.eve?.inputResponse,
kind,
name
} };
}
function approvedApproval(part) {
if (!part?.approval?.id) return;
return {
approved: true,
id: part.approval.id,
isAutomatic: part.approval.isAutomatic,
reason: part.approval.reason
};
}
function normalizeActionRequest(action) {
switch (action.kind) {
case "load-skill": return {
kind: "load-skill",
name: "load_skill",
toolName: "eve:load-skill"
};
case "tool-call": return {
kind: "tool-call",
name: action.toolName,
toolName: action.toolName
};
case "subagent-call": return {
kind: "subagent-call",
name: action.subagentName,
toolName: `eve:subagent:${action.subagentName}`
};
case "remote-agent-call": return {
kind: "subagent-call",
name: action.remoteAgentName,
toolName: `eve:subagent:${action.remoteAgentName}`
};
}
}
function normalizeActionResult(result) {
switch (result.kind) {
case "load-skill-result": return {
kind: "load-skill",
name: result.name ?? "load_skill",
toolName: "eve:load-skill"
};
case "tool-result": return {
kind: "tool-call",
name: result.toolName,
toolName: result.toolName
};
case "subagent-result": return {
kind: "subagent-call",
name: result.subagentName,
toolName: `eve:subagent:${result.subagentName}`
};
}
}
function optimisticUserMessageId(submissionId) {
return `optimistic:${submissionId}:user`;
}
function stringifyUnknown(value) {
if (typeof value === "string") return value;
try {
return JSON.stringify(value);
} catch {
return "Action failed.";
}
}
function findLastIndex(items, predicate) {
for (let index = items.length - 1; index >= 0; index -= 1) if (predicate(items[index])) return index;
return -1;
}
//#endregion
//#region src/vue/use-eve-agent.ts
function useEveAgent(options = {}) {
const reducer = options.reducer ?? defaultMessageReducer();
const store = new EveAgentStore({
auth: options.auth,
headers: options.headers,
host: options.host,
initialEvents: options.initialEvents,
initialSession: options.initialSession,
maxReconnectAttempts: options.maxReconnectAttempts,
optimistic: options.optimistic,
reducer,
session: options.session
});
store.setCallbacks({
onError: options.onError,
onEvent: options.onEvent,
onFinish: options.onFinish,
onSessionChange: options.onSessionChange,
prepareSend: options.prepareSend
});
const snapshot = shallowRef(store.snapshot);
if ("window" in globalThis) {
const unsubscribe = store.subscribe(() => {
snapshot.value = store.snapshot;
});
onScopeDispose(() => {
unsubscribe();
store.stop();
});
}
return {
data: computed(() => snapshot.value.data),
error: computed(() => snapshot.value.error),
events: computed(() => snapshot.value.events),
reset: () => store.reset(),
send: (input) => store.send(input),
session: computed(() => snapshot.value.session),
status: computed(() => snapshot.value.status),
stop: () => store.stop()
};
}
//#endregion
export { defaultMessageReducer as n, useEveAgent as t };