UNPKG

eve

Version:

Filesystem-first framework for durable backend AI agents that run anywhere.

1,313 lines (1,294 loc) 43.4 kB
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 };