/** * Report what an agent is doing on the gRPC stream, opening it when needed. * * The TypeScript mirror of `synap_integrations_common/stream_events.py` and * `history.py`. Every integration hooks a different framework, and they all have * the same job once a hook fires: turn it into a Synap stream event. A copy of * that per integration is a copy per integration of getting the role wrong, * forgetting an id, or letting an exception escape into somebody's agent loop. * * **Three rules, and they are the whole design.** * * *The stream is the default path.* The first report on an SDK with no stream * opens one (`sdk.instance.listen()`), so a caller who never heard of * `listen()` still gets turns, tool calls and reasoning to the server, and * anticipation bundles back. `SYNAP_AUTO_LISTEN=false` or * `setAutoListen(sdk, false)` turns that off. * * *REST when the stream cannot carry it.* A stream that will not open is not * used. Turns then go over REST (`recordTurn`); events that only exist on the * stream are dropped. A failed open is retried once after a few seconds (a * cold first connection), then no more than once a minute. * * *Never throws.* A hook runs inside the framework's own loop. A telemetry * call that throws there takes down the agent run, and no context is worth * that. Every function swallows everything. * * The second half of this file (`fetchCompactedHistory`, `spliceHistory`) puts * Synap's compaction where the framework's own history would go. * * --- * * ⚠ **This file is vendored, not imported.** The three TypeScript integrations * (`claude-agent-ts`, `mastra`, `eve`) deliberately carry no runtime * dependencies: they duck-type the SDK so a user brings their own version of * it. A shared package would mean a new public npm name that has to be * published before any of the three can be. So this file is byte-identical in * all three, and `stream-events-is-vendored.test.ts` fails the build if the * copies drift. Change one, copy it to the other two. * * ⚠ **Why a helper at all, when `vercel-adk` hand-rolls it.** Hand-rolling is * what left `strands-agents` calling `send_message({event_type: 'tool_call'})`, * which has no field for a `tool_call_id`, so its tool results had nothing to * pair with. The typed methods set the id and set `role` and `event_type` * together, which is the pair that was silently wrong in our own published * example. */ /** * The slice of the SDK this file touches. Duck-typed for the same reason the * rest of these packages are: a user passes the real `@maximem/synap-js-sdk`, * a test passes a stub, and neither is imported here. * * Everything is optional. An older SDK with no `instance` namespace must make * `streamIsActive` answer no, not throw. */ export interface SynapStreamSdkLike { instance?: { /** A getter on the real SDK (`NAMESPACE.is_listening`), like Python's. */ is_listening?: boolean; /** Not on the published SDK's `instance`; kept for stubs that model it as a method. */ isListening?: () => boolean; listen?: (options?: Record) => Promise; send_message?: (options: Record) => Promise; record_tool_call?: (options: Record) => Promise; record_tool_result?: (options: Record) => Promise; record_thinking?: (options: Record) => Promise; end_session?: (conversationId: string) => Promise; }; conversation?: { record_message?: (options: Record) => Promise; context?: { get_context_for_prompt?: (options: Record) => Promise; }; }; } /** Ids every report can carry. Absent ones are dropped, never sent as ''. */ export interface StreamScope { conversationId?: string; userId?: string; customerId?: string; } /** * Turn opening the stream on first report on or off for one SDK. * * Overrides `SYNAP_AUTO_LISTEN` for that SDK. A stream the caller opened * themselves is used either way. */ export declare function setAutoListen(sdk: unknown, enabled: boolean): void; /** * Whether there is a stream to report on. Does not open one. * * ⚠ The real SDK exposes `instance.is_listening` as a GETTER, the same name as * Python's. The method `isListening()` exists only on its private `_internal` * object, so reading the method here answered "not listening" on every real * SDK and left the stream permanently unused. Both are read: the getter first, * then the method for a stub that models it that way. Reading a method without * calling it gives a function object, which is truthy, so a method is always * called. */ export declare function streamIsActive(sdk: unknown): boolean; /** * Open the stream if there is none. Resolves to whether there is one to use. * * Never throws. Never waits more than ten seconds. After a failed open, the * first retry comes after three seconds (a cold first connection), later ones * no more than once a minute; until then every turn goes over REST. Concurrent * calls share one open, even across copies of this file. */ export declare function ensureStream(sdk: unknown): Promise; /** * Report a user or assistant turn. Resolves to whether it was sent. * * `assistant_message` is the event the anticipation agent acts on: it is the * moment a turn ends and the next one can be predicted. An integration that * reports everything else and not this one gets no anticipation at all. */ export declare function reportTurn(sdk: unknown, args: StreamScope & { role: string; content: string; metadata?: Record; }): Promise; /** * Write a user or assistant turn: on the stream (opened if need be), over REST * when the stream cannot carry it. Resolves to whether it was written. * * A turn is the conversation itself, not telemetry about it: without it the * server has nothing to compact and the SDK nothing to search. Stream first, * REST second, never both: the server persists a streamed turn from its event, * so a REST write on top would store and extract it twice. Never throws. */ export declare function recordTurn(sdk: unknown, args: StreamScope & { role: string; content: string; metadata?: Record; }): Promise; /** Report a tool the agent is invoking. */ export declare function reportToolCall(sdk: unknown, args: StreamScope & { toolName: string; /** Only a plain object travels. See `asArgsObject`. */ toolArgs?: unknown; toolCallId?: string; }): Promise; /** * Report what a tool returned. * * ⚠ A tool result is usually the caller's customer data. It is an anticipation * hint and never becomes a long-term memory, but it does leave their process. * An integration passes through what the framework gives it and does not go * looking for more. */ export declare function reportToolResult(sdk: unknown, args: StreamScope & { result: unknown; toolName?: string; toolCallId?: string; }): Promise; /** * Report one reasoning step, when the framework exposes one. * * Most providers return a summary rather than raw reasoning, and some return * nothing. An integration reports what it is given; a turn with no reasoning is * an ordinary turn, not a gap to fill. */ export declare function reportReasoning(sdk: unknown, args: StreamScope & { content: string; stepIndex?: number; thoughtType?: string; }): Promise; /** * Close the session for a conversation the integration knows has ended. * * Never opens a stream: there is no session to end on a new one. */ export declare function endSession(sdk: unknown, conversationId: string): Promise; /** The conversation before the current turn, as Synap holds it. */ export interface CompactedHistory { /** * The compaction block and any earlier turns history search found, wrapped * in `` tags. */ preamble: string; /** The turns after the compaction, oldest first. */ turns: ReadonlyArray; compactionId?: string | null; } /** * Whether a message is the history `spliceHistory` put there, not a turn. * * An integration that writes messages back to Synap skips these, or the * compaction would be recorded as something somebody said. */ export declare function isSynapHistory(text: unknown): boolean; /** * The compacted conversation, or `null` when there is nothing to replace. * * `null` when the conversation has no compaction yet (the framework's list is * still short and is the better record), when the SDK predates compaction * blocks, and on any error. Never throws. */ export declare function fetchCompactedHistory(sdk: unknown, conversationId: string): Promise; /** * `fetchCompactedHistory`'s parsing of a `get_context_for_prompt` response, for * a caller that has to make the call itself (to choose a style, or to let an * error propagate). `null` when the response carries no compaction block. */ export declare function compactedHistoryFrom(response: unknown): CompactedHistory | null; /** * Replace every turn before the current one with Synap's history. * * `roleOf` returns `"system"`, `"user"`, `"assistant"` or anything else (a tool * message, say). `makeMessage(role, text)` builds a message of the framework's * type; it is called with `"system"` once, for the preamble, and with `"user"` * or `"assistant"` for turns. * * The result, in order: * * 1. the framework's own system messages from before the current turn; * 2. the preamble, as one system message; * 3. Synap's turns after the compaction; * 4. any earlier turns the framework has and Synap does not yet (a write still * in flight), rebuilt as plain text; * 5. the current turn untouched: the last user message and everything after it, * so a tool call and its result stay paired. * * Tool messages from earlier turns are dropped with the turns they belonged * to: the compaction covers them, and history search finds their results. */ export declare function spliceHistory(messages: readonly M[], history: CompactedHistory, fns: { roleOf: (message: M) => string; textOf: (message: M) => string; makeMessage: (role: string, text: string) => M; }): M[]; //# sourceMappingURL=stream-events.d.ts.map