@nylorun/agents 0.7.0-beta → 0.9.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,397 @@
1
+ /**
2
+ * An AG-UI endpoint over the Tenant's agents, as web-standard `Request` → `Response`
3
+ * handlers. The host authenticates people (`subject`); the handler maps each AG-UI thread to
4
+ * one session per subject, agent and thread, and streams the session's transcript events as
5
+ * AG-UI events. It never trusts a session id from the client, and it calls the Runtime as the
6
+ * subject (`client.as`), so the Runtime itself keeps one subject out of another's sessions.
7
+ */
8
+ import { createHash, randomUUID } from "node:crypto";
9
+ import { EventType } from "@ag-ui/core";
10
+ import { RunAgentInputSchema } from "@ag-ui/core/schemas";
11
+ import { parseSubjectHeaders, } from "@nylorun/core/contracts";
12
+ import { createClient, } from "../client.js";
13
+ import { IncompatibleRuntimeError, RuntimeError } from "../http.js";
14
+ import { messagesFromEvents } from "./history.js";
15
+ import { SSE_CONTENT_TYPE, SSE_HEARTBEAT, sseFrame } from "./sse.js";
16
+ import { RunTranslator } from "./translate.js";
17
+ const HEARTBEAT_MS = 15_000;
18
+ /** Host features the handler needs: the chat transcript, and acting for each subject. */
19
+ const REQUIRED_FEATURES = ["transcript-events", "subject-headers"];
20
+ const DEFAULT_SCOPES = ["sessions:own"];
21
+ const TERMINAL = new Set([
22
+ "turn.completed",
23
+ "turn.paused",
24
+ "turn.failed",
25
+ "turn.cancelled",
26
+ ]);
27
+ class Problem extends Error {
28
+ status;
29
+ code;
30
+ constructor(status, message, code) {
31
+ super(message);
32
+ this.status = status;
33
+ this.code = code;
34
+ }
35
+ }
36
+ function problem(status, message, code) {
37
+ return Response.json({ error: message, ...(code ? { code } : {}) }, { status });
38
+ }
39
+ function problemOf(error) {
40
+ if (error instanceof Problem)
41
+ return error;
42
+ if (error instanceof RuntimeError) {
43
+ if (error.status === 404)
44
+ return new Problem(404, "Not found");
45
+ // The Runtime refused what the client sent (e.g. a cursor it cannot read).
46
+ if (error.status === 400)
47
+ return new Problem(400, "The request was rejected", "invalid_request");
48
+ if (error.status === 409)
49
+ return new Problem(409, "The agent is busy or waiting for an answer to an open interrupt", "session_busy");
50
+ return new Problem(502, "The agent service failed", "runtime_error");
51
+ }
52
+ if (error instanceof IncompatibleRuntimeError)
53
+ return new Problem(502, error.message, "runtime_incompatible");
54
+ return new Problem(500, "Internal error");
55
+ }
56
+ /** A session per subject, agent and thread; one subject can never name another's session. */
57
+ export function sessionIdFor(subject, agentId, threadId) {
58
+ return createHash("sha256")
59
+ .update(`${subject}\u0000${agentId}\u0000${threadId}`)
60
+ .digest("hex")
61
+ .slice(0, 32);
62
+ }
63
+ /** The trailing user message: the Runtime keeps the transcript, AG-UI resends all of it. */
64
+ function lastUserMessage(messages) {
65
+ const last = messages.at(-1);
66
+ if (!last || last.role !== "user")
67
+ throw new Problem(400, "The run's last message must be a user message");
68
+ const text = typeof last.content === "string"
69
+ ? last.content
70
+ : last.content
71
+ .filter((part) => part.type === "text")
72
+ .map((part) => part.text)
73
+ .join("\n");
74
+ if (!text.trim())
75
+ throw new Problem(400, "The user message has no text");
76
+ return { id: last.id, text };
77
+ }
78
+ function normalizeBase(basePath = "/") {
79
+ const trimmed = basePath.replace(/\/+$/, "");
80
+ return trimmed === "" ? "" : trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
81
+ }
82
+ export function createAgUiHandler(options) {
83
+ const allowed = new Set(options.agents.map((agent) => (typeof agent === "string" ? agent : agent.id)));
84
+ const base = normalizeBase(options.basePath);
85
+ const scopes = options.scopes ?? DEFAULT_SCOPES;
86
+ const checked = parseSubjectHeaders("probe", scopes.join(" "));
87
+ if (!checked.ok)
88
+ throw new TypeError(`createAgUiHandler: ${checked.message}`);
89
+ let ready;
90
+ /** The client, once the Runtime is known to have every required feature. Retries after a failure. */
91
+ function connected() {
92
+ ready ??= (async () => {
93
+ const client = await (options.client ?? createClient());
94
+ const features = await client.hostFeatures();
95
+ const missing = REQUIRED_FEATURES.filter((f) => !features.includes(f));
96
+ if (missing.length > 0)
97
+ throw new Problem(502, `The Runtime does not support ${missing.join(", ")}; upgrade the stack (nylorun up).`, "runtime_feature_missing");
98
+ return client;
99
+ })();
100
+ ready.catch(() => {
101
+ ready = undefined;
102
+ });
103
+ return ready;
104
+ }
105
+ /** The client acting for `subject`: the Runtime limits it to the subject's own sessions. */
106
+ async function clientFor(subject) {
107
+ const client = await connected();
108
+ try {
109
+ return client.as(subject, { scopes });
110
+ }
111
+ catch (error) {
112
+ // The host's `subject` returned something the Runtime cannot name (see `as`).
113
+ throw new Problem(500, error.message, "subject_invalid");
114
+ }
115
+ }
116
+ async function subjectOf(request) {
117
+ const subject = await options.subject(request);
118
+ if (!subject)
119
+ throw new Problem(401, "Sign in required");
120
+ return subject;
121
+ }
122
+ function assertAgent(agentId) {
123
+ if (!allowed.has(agentId))
124
+ throw new Problem(404, "Not found");
125
+ }
126
+ async function sessionFor(request, agentId, threadId) {
127
+ assertAgent(agentId);
128
+ const subject = await subjectOf(request);
129
+ const client = await clientFor(subject);
130
+ return {
131
+ client,
132
+ subject,
133
+ session: client.session(sessionIdFor(subject, agentId, threadId)),
134
+ };
135
+ }
136
+ async function submit(session, input) {
137
+ if (input.resume?.length) {
138
+ let cursor = null;
139
+ // The interaction's kind is on the pending view, not in its id.
140
+ const pending = await session.pending();
141
+ const waits = Array.isArray(pending) ? pending : [];
142
+ for (const entry of input.resume) {
143
+ const wait = waits.find((w) => w?.interaction?.id === entry.interruptId);
144
+ const key = `${input.runId}:${entry.interruptId}`;
145
+ const payload = entry.payload;
146
+ const accepted = (wait?.interaction?.kind ?? "approval") === "approval"
147
+ ? await session.approve(entry.interruptId, entry.status === "resolved" &&
148
+ (payload === true ||
149
+ (typeof payload === "object" && payload?.approved === true)), { idempotencyKey: key })
150
+ : entry.status === "cancelled"
151
+ ? await session.cancel({ idempotencyKey: key, reason: "interrupt cancelled" })
152
+ : await session.respond(entry.interruptId, (entry.payload ?? null), {
153
+ idempotencyKey: key,
154
+ });
155
+ cursor ??= accepted.cursor;
156
+ }
157
+ return cursor;
158
+ }
159
+ const message = lastUserMessage(input.messages);
160
+ // The AG-UI message id is the idempotency key: a retried run replays the same turn,
161
+ // and history gives the message back its client id.
162
+ const accepted = await session.input(message.text, {
163
+ idempotencyKey: message.id,
164
+ });
165
+ return accepted.cursor;
166
+ }
167
+ /** Streams translated events until the run ends. Closing the response stops the read only. */
168
+ function stream(request, translator, source, preface = []) {
169
+ const reading = new AbortController();
170
+ request.signal.addEventListener("abort", () => reading.abort(), { once: true });
171
+ const bytes = new TextEncoder();
172
+ const body = new ReadableStream({
173
+ async start(controller) {
174
+ const write = (text) => {
175
+ try {
176
+ controller.enqueue(bytes.encode(text));
177
+ }
178
+ catch {
179
+ reading.abort();
180
+ }
181
+ };
182
+ const heartbeat = setInterval(() => write(SSE_HEARTBEAT), HEARTBEAT_MS);
183
+ try {
184
+ write(sseFrame(translator.started()));
185
+ for (const event of preface)
186
+ write(sseFrame(event));
187
+ if (source)
188
+ for await (const event of source(reading.signal)) {
189
+ const step = translator.translate(event);
190
+ step.events.forEach((out, index) => write(sseFrame(out, index === step.events.length - 1 ? event.cursor : undefined)));
191
+ if (step.finished || reading.signal.aborted)
192
+ break;
193
+ }
194
+ }
195
+ catch (error) {
196
+ if (!reading.signal.aborted) {
197
+ const failure = problemOf(error);
198
+ write(sseFrame({
199
+ type: EventType.RUN_ERROR,
200
+ message: failure.message,
201
+ ...(failure.code ? { code: failure.code } : {}),
202
+ }));
203
+ }
204
+ }
205
+ finally {
206
+ clearInterval(heartbeat);
207
+ reading.abort();
208
+ try {
209
+ controller.close();
210
+ }
211
+ catch {
212
+ /* the client is gone */
213
+ }
214
+ }
215
+ },
216
+ cancel() {
217
+ reading.abort();
218
+ },
219
+ });
220
+ return new Response(body, {
221
+ headers: {
222
+ "content-type": SSE_CONTENT_TYPE,
223
+ "cache-control": "no-cache, no-transform",
224
+ "x-accel-buffering": "no",
225
+ },
226
+ });
227
+ }
228
+ async function run(request, agentId) {
229
+ try {
230
+ assertAgent(agentId);
231
+ const subject = await subjectOf(request);
232
+ const parsed = RunAgentInputSchema.safeParse(await request.json().catch(() => undefined));
233
+ if (!parsed.success)
234
+ throw new Problem(400, "Invalid RunAgentInput");
235
+ const input = parsed.data;
236
+ // Nylorun tools run in the Runtime or the app's executor; the browser runs none.
237
+ if (input.tools?.length)
238
+ throw new Problem(400, "Frontend tools are not supported");
239
+ const client = await clientFor(subject);
240
+ const extra = (await options.session?.(subject, agentId)) ?? {};
241
+ const translator = new RunTranslator(input.threadId, input.runId);
242
+ let session;
243
+ try {
244
+ // The host's parameters first: they never replace the session's identity.
245
+ session = await client.createSession({
246
+ ...extra,
247
+ id: sessionIdFor(subject, agentId, input.threadId),
248
+ agentId,
249
+ ownerUserId: subject,
250
+ });
251
+ }
252
+ catch (error) {
253
+ if (error instanceof RuntimeError && error.status === 409)
254
+ throw new Problem(409, "The thread's session was opened with other parameters", "session_conflict");
255
+ throw error;
256
+ }
257
+ let cursor;
258
+ try {
259
+ cursor = await submit(session, input);
260
+ }
261
+ catch (error) {
262
+ const failure = problemOf(error);
263
+ // A busy or paused session is a run that could not start, not a transport failure.
264
+ if (failure.status !== 409)
265
+ throw failure;
266
+ return stream(request, translator, undefined, [
267
+ {
268
+ type: EventType.RUN_ERROR,
269
+ message: failure.message,
270
+ code: failure.code,
271
+ },
272
+ ]);
273
+ }
274
+ return stream(request, translator, (signal) => session.observe({ ...(cursor ? { cursor } : {}), signal }));
275
+ }
276
+ catch (error) {
277
+ const failure = problemOf(error);
278
+ return problem(failure.status, failure.message, failure.code);
279
+ }
280
+ }
281
+ async function history(request, agentId, threadId) {
282
+ try {
283
+ const { session } = await sessionFor(request, agentId, threadId);
284
+ const items = [];
285
+ let cursor;
286
+ try {
287
+ for (;;) {
288
+ const page = await session.history({ ...(cursor ? { cursor } : {}) });
289
+ items.push(...page.items);
290
+ if (!page.cursor || page.cursor === cursor || page.items.length === 0)
291
+ break;
292
+ cursor = page.cursor;
293
+ }
294
+ }
295
+ catch (error) {
296
+ // A thread that never ran has no session yet.
297
+ if (error instanceof RuntimeError && error.status === 404)
298
+ return Response.json([]);
299
+ throw error;
300
+ }
301
+ return Response.json(messagesFromEvents(items));
302
+ }
303
+ catch (error) {
304
+ const failure = problemOf(error);
305
+ return problem(failure.status, failure.message, failure.code);
306
+ }
307
+ }
308
+ async function reattach(request, agentId, threadId) {
309
+ try {
310
+ const { session } = await sessionFor(request, agentId, threadId);
311
+ const url = new URL(request.url);
312
+ const cursor = request.headers.get("last-event-id") ?? url.searchParams.get("cursor");
313
+ if (!cursor)
314
+ throw new Problem(400, "Last-Event-ID or ?cursor= is required");
315
+ const view = await session.inspect();
316
+ const runId = url.searchParams.get("runId") ??
317
+ view.activeTurnId ??
318
+ (typeof view.lastTurnId === "string" ? view.lastTurnId : randomUUID());
319
+ const translator = new RunTranslator(threadId, runId, { reattached: true });
320
+ if (view.activeTurnId)
321
+ return stream(request, translator, (signal) => session.observe({ cursor, signal }));
322
+ // No turn is running: send what the client missed, if the run ended after the cursor.
323
+ const missed = [];
324
+ let from = cursor;
325
+ for (;;) {
326
+ const page = await session.history({ cursor: from });
327
+ missed.push(...page.items);
328
+ if (!page.cursor || page.cursor === from || page.items.length === 0)
329
+ break;
330
+ from = page.cursor;
331
+ }
332
+ if (!missed.some((event) => TERMINAL.has(event.type)))
333
+ return new Response(null, { status: 204 });
334
+ return stream(request, translator, () => iterate(missed));
335
+ }
336
+ catch (error) {
337
+ const failure = problemOf(error);
338
+ return problem(failure.status, failure.message, failure.code);
339
+ }
340
+ }
341
+ async function cancel(request, agentId, threadId) {
342
+ try {
343
+ const { session } = await sessionFor(request, agentId, threadId);
344
+ await session.cancel({ idempotencyKey: `cancel:${randomUUID()}` });
345
+ return new Response(null, { status: 204 });
346
+ }
347
+ catch (error) {
348
+ const failure = problemOf(error);
349
+ return problem(failure.status, failure.message, failure.code);
350
+ }
351
+ }
352
+ async function route(request) {
353
+ const path = new URL(request.url).pathname;
354
+ if (path !== base && !path.startsWith(`${base}/`))
355
+ return problem(404, "Not found");
356
+ let segments;
357
+ try {
358
+ segments = path
359
+ .slice(base.length)
360
+ .split("/")
361
+ .slice(1)
362
+ .map((segment) => decodeURIComponent(segment));
363
+ }
364
+ catch {
365
+ return problem(404, "Not found");
366
+ }
367
+ if (segments.some((segment) => segment === ""))
368
+ return problem(404, "Not found");
369
+ const [agentId, threads, threadId, action] = segments;
370
+ const method = request.method.toUpperCase();
371
+ const operation = (allow, handle) => {
372
+ if (!agentId || !allowed.has(agentId))
373
+ return problem(404, "Not found");
374
+ if (method !== allow)
375
+ return new Response(JSON.stringify({ error: "Method not allowed" }), {
376
+ status: 405,
377
+ headers: { allow, "content-type": "application/json" },
378
+ });
379
+ return handle();
380
+ };
381
+ if (segments.length === 1)
382
+ return operation("POST", () => run(request, agentId));
383
+ if (segments.length === 4 && threads === "threads") {
384
+ if (action === "messages")
385
+ return operation("GET", () => history(request, agentId, threadId));
386
+ if (action === "events")
387
+ return operation("GET", () => reattach(request, agentId, threadId));
388
+ if (action === "cancel")
389
+ return operation("POST", () => cancel(request, agentId, threadId));
390
+ }
391
+ return problem(404, "Not found");
392
+ }
393
+ return { fetch: route, run, history, reattach, cancel };
394
+ }
395
+ async function* iterate(items) {
396
+ yield* items;
397
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * An AG-UI transcript rebuilt from a session's log, with the ids a live run gave each
3
+ * message (see `translate.ts`), usable as `HttpAgent`'s `initialMessages`.
4
+ */
5
+ import type { Message } from "@ag-ui/core";
6
+ import { type LiveEvent } from "@nylorun/core/contracts";
7
+ export declare function messagesFromEvents(items: readonly LiveEvent[]): Message[];
@@ -0,0 +1,60 @@
1
+ import { parseTranscriptEvent } from "@nylorun/core/contracts";
2
+ import { outputText, toolResultOf } from "./translate.js";
3
+ export function messagesFromEvents(items) {
4
+ const messages = [];
5
+ const results = new Set();
6
+ let sawAssistant = false;
7
+ for (const raw of items) {
8
+ if (raw.type === "command.message") {
9
+ const p = (raw.payload ?? {});
10
+ messages.push({
11
+ // The handler sends the client's message id as the idempotency key.
12
+ id: p.idempotencyKey ?? raw.eventId,
13
+ role: "user",
14
+ content: typeof p.content === "string" ? p.content : JSON.stringify(p.data),
15
+ });
16
+ sawAssistant = false;
17
+ continue;
18
+ }
19
+ const event = parseTranscriptEvent(raw);
20
+ if (!event)
21
+ continue;
22
+ const result = toolResultOf(event);
23
+ if (result) {
24
+ if (results.has(result.callId))
25
+ continue;
26
+ results.add(result.callId);
27
+ messages.push({
28
+ id: `${result.callId}:result`,
29
+ role: "tool",
30
+ toolCallId: result.callId,
31
+ content: result.content,
32
+ });
33
+ continue;
34
+ }
35
+ if (event.type === "message.assistant" && !event.payload.agent) {
36
+ const p = event.payload;
37
+ sawAssistant = true;
38
+ const message = { id: p.invocationId, role: "assistant" };
39
+ if (p.text)
40
+ message.content = p.text;
41
+ if (p.toolCalls.length)
42
+ message.toolCalls = p.toolCalls.map((call) => ({
43
+ id: call.callId,
44
+ type: "function",
45
+ function: { name: call.name, arguments: JSON.stringify(call.input ?? {}) },
46
+ }));
47
+ messages.push(message);
48
+ }
49
+ else if (event.type === "turn.completed" && !sawAssistant) {
50
+ const output = event.payload.output;
51
+ if (output !== undefined && output !== null && outputText(output))
52
+ messages.push({
53
+ id: raw.eventId,
54
+ role: "assistant",
55
+ content: outputText(output),
56
+ });
57
+ }
58
+ }
59
+ return messages;
60
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * `@nylorun/agents/ag-ui`: serve the Tenant's agents to AG-UI clients from the app's own
3
+ * server. The only entry point that loads `@ag-ui/core`.
4
+ */
5
+ export { createAgUiHandler } from "./handler.js";
6
+ export type { AgUiHandler, AgUiHandlerOptions, AgUiSessionOptions, } from "./handler.js";
7
+ export { toNodeListener } from "./node.js";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * `@nylorun/agents/ag-ui`: serve the Tenant's agents to AG-UI clients from the app's own
3
+ * server. The only entry point that loads `@ag-ui/core`.
4
+ */
5
+ export { createAgUiHandler } from "./handler.js";
6
+ export { toNodeListener } from "./node.js";
@@ -0,0 +1,5 @@
1
+ /** `node:http` and Express adapter for a fetch-style handler. */
2
+ import type { IncomingMessage, ServerResponse } from "node:http";
3
+ export declare function toNodeListener(handler: {
4
+ readonly fetch: (request: Request) => Promise<Response>;
5
+ }): (req: IncomingMessage, res: ServerResponse) => void;
@@ -0,0 +1,51 @@
1
+ import { Readable } from "node:stream";
2
+ export function toNodeListener(handler) {
3
+ return (req, res) => {
4
+ void (async () => {
5
+ // A closed connection aborts the request, which stops the read, never the turn.
6
+ const controller = new AbortController();
7
+ res.on("close", () => controller.abort());
8
+ const host = req.headers.host ?? "localhost";
9
+ const url = new URL(req.url ?? "/", `http://${host}`);
10
+ const headers = new Headers();
11
+ for (const [name, value] of Object.entries(req.headers)) {
12
+ if (value === undefined)
13
+ continue;
14
+ for (const item of Array.isArray(value) ? value : [value])
15
+ headers.append(name, item);
16
+ }
17
+ const hasBody = req.method !== "GET" && req.method !== "HEAD";
18
+ const request = new Request(url, {
19
+ method: req.method,
20
+ headers,
21
+ signal: controller.signal,
22
+ ...(hasBody
23
+ ? {
24
+ body: Readable.toWeb(req),
25
+ duplex: "half",
26
+ }
27
+ : {}),
28
+ });
29
+ let response;
30
+ try {
31
+ response = await handler.fetch(request);
32
+ }
33
+ catch {
34
+ response = new Response(JSON.stringify({ error: "Internal error" }), {
35
+ status: 500,
36
+ headers: { "content-type": "application/json" },
37
+ });
38
+ }
39
+ res.writeHead(response.status, Object.fromEntries(response.headers));
40
+ if (!response.body) {
41
+ res.end();
42
+ return;
43
+ }
44
+ res.flushHeaders();
45
+ const body = Readable.fromWeb(response.body);
46
+ body.on("error", () => res.destroy());
47
+ res.on("close", () => body.destroy());
48
+ body.pipe(res);
49
+ })();
50
+ };
51
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * AG-UI over server-sent events: one `data:` line of JSON per event. The last AG-UI event
3
+ * made from a Runtime event carries that event's cursor as `id:`, so a client that resumes
4
+ * from `Last-Event-ID` never lands inside a group.
5
+ */
6
+ import type { BaseEvent } from "@ag-ui/core";
7
+ export declare const SSE_CONTENT_TYPE = "text/event-stream";
8
+ export declare function sseFrame(event: BaseEvent, id?: string): string;
9
+ export declare const SSE_HEARTBEAT = ": keep-alive\n\n";
@@ -0,0 +1,7 @@
1
+ export const SSE_CONTENT_TYPE = "text/event-stream";
2
+ export function sseFrame(event, id) {
3
+ // Cursors are opaque base64; a line break would end the field early.
4
+ const idLine = id && !/[\r\n]/.test(id) ? `id: ${id}\n` : "";
5
+ return `${idLine}data: ${JSON.stringify(event)}\n\n`;
6
+ }
7
+ export const SSE_HEARTBEAT = ": keep-alive\n\n";
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Runtime transcript events → AG-UI 1.0 events for one run. Ids: an assistant message is its
3
+ * model call's `invocationId`, a tool call is the model's `callId`, a tool result is
4
+ * `<callId>:result`, so live runs and `messagesFromEvents` agree.
5
+ */
6
+ import { type BaseEvent } from "@ag-ui/core";
7
+ import { type LiveEvent, type TranscriptEvent } from "@nylorun/core/contracts";
8
+ /** The text a tool result shows; `undefined` while there is no result yet. */
9
+ export declare function toolResultText(result: unknown): string | undefined;
10
+ export declare function outputText(output: unknown): string;
11
+ /** One Runtime event's result for a run: AG-UI events, and whether the run ended. */
12
+ export interface Step {
13
+ readonly events: BaseEvent[];
14
+ readonly finished: boolean;
15
+ }
16
+ /** A tool result from any of the three places a call can end. */
17
+ export declare function toolResultOf(event: TranscriptEvent): {
18
+ callId: string;
19
+ content: string;
20
+ } | undefined;
21
+ export declare class RunTranslator {
22
+ private readonly threadId;
23
+ private readonly runId;
24
+ /** Tool invocation → the model's call id, for interrupts that name the invocation. */
25
+ private readonly callIds;
26
+ private readonly closed;
27
+ private sawAssistant;
28
+ /**
29
+ * `reattached`: the run continues from a cursor, so earlier assistant messages already
30
+ * reached the client and a workflow's output is not repeated as text.
31
+ */
32
+ constructor(threadId: string, runId: string, options?: {
33
+ reattached?: boolean;
34
+ });
35
+ translate(raw: LiveEvent): Step;
36
+ started(): BaseEvent;
37
+ private finished;
38
+ }