mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-28 23:01:34 +00:00
* feat(config): add agent config descriptor protocol and advertise via runner capability Introduce shared agent configuration descriptors covering model, effort, permission, and secondary settings per agent flavor, plus the canonical HAPI YOLO -> native permission mode mapping. Runners advertise the builtin descriptors through the runner-state capability so hubs and web can render configuration without hardcoded flavor branches. Migrate the OpenCode create-session model picker from a bespoke radio list to the shared SelectControl combobox. * feat(web): render create-session permission from agent config descriptor Replace the flavor-branched Grok/Codex-family/YOLO permission block with a descriptor-driven PermissionField. Pi now reports permission as managed instead of silently ignoring the YOLO toggle, and YOLO-only flavors show the native permission mode the preference maps to. Removes the superseded GrokPermissionModeSelector and CodexFamilyPermissionModeSelector components. * ci: retry flaky claudeRemote 5s-timeout failure * fix(web): persist explicit OpenCode Default selection instead of restoring a concrete model The parent initialization effect treated every null selected model as 'uninitialized' and auto-picked a concrete advertised model, clobbering the user's explicit Default choice (and a restored Default preference). null now means explicit Default and is preserved; only undefined (no choice made yet) triggers probe-based initialization. Add parent-level regression tests for Default persistence and remembered-model restore. * fix(web): accept undefined selected model in OpencodeModelSelector props * feat(web+cli+hub): unify create-session model/effort fields and add Pi model/effort support Pi's agent config descriptor now advertises model (machine) and effort (static thinking levels) for create AND session availability: - cli: ListPiModelsForMachine RPC runs 'pi --list-models' (cached, inflight deduped) and parses the provider/model table; startup model match accepts provider-qualified ids - hub: GET /api/machines/:id/pi-models route + rpcGateway/syncEngine passthrough - web: NewSession renders Pi models grouped by provider through the generic ModelSelector and a new descriptor-driven EffortField (replaces the per-flavor LaunchEffortSelector/ReasoningEffortSelector pair); launch payload forwards Pi model + thinking-level effort (runner already supported --model/ --effort for pi) * fix(web): render Pi provider groups in ModelSelector and scope Grok availability warning - ModelSelector now renders grouped options as <optgroup> (Pi models are provider-grouped; identical modelIds from different providers stay distinct) - PermissionField only receives autoPermissionModeSupported for Grok — a cached Grok probe result no longer leaks the Grok warning onto other agents Addresses HAPI Bot Minor findings on #1469. * fix(web): drop Object.groupBy from ModelSelector; revalidate restored Pi models against the catalog - ModelSelector buckets options with a reduce instead of Object.groupBy (Safari < 17.4 has no polyfill — New Session would throw on those clients) - Pi restored model/effort are cleared when the value is absent from the live machine catalog, and Create waits for the catalog while a non-default Pi choice is being validated (mirrors Codex/Grok/Copilot handling) Addresses HAPI Bot findings on #1469. * fix(pi+web): serialize startup model before thinking level; hide Pi launch controls during history import - PiSession gains startupModelSettled; the startup set_thinking_level waits for the requested model's set_model attempt to settle first, so a level the default model rejects is not lost before the requested model is confirmed (set_model and set_thinking_level were already serialized by the runtime mutation lock; this pins the model-first ordering) - Create Session hides Pi model/effort controls while a Pi history import is selected — the import reopens the native session as-is and would silently ignore launch-only model/effort values Addresses HAPI Bot findings on #1469. * fix(pi): settle startup-model gate when model discovery fails or returns no models A failed or empty get_available_models response would leave the startupModelSettled gate unresolved, stranding a requested startup effort indefinitely. Resolve the gate on the error path and the empty-models path; adds regression tests for both.
485 lines
20 KiB
TypeScript
485 lines
20 KiB
TypeScript
import type { ApiClient, ApiSessionClient } from '@/lib';
|
|
import type { Metadata } from '@/api/types';
|
|
import type { PiCommandSummary, PiThinkingLevel } from './types';
|
|
import type { PiModelSummary } from '@hapi/protocol/apiTypes';
|
|
import type { PiRpcResolver } from './loop';
|
|
|
|
/**
|
|
* The parts of `get_state` which must move atomically with a native session
|
|
* identity transition. `undefined` means the Pi version did not report that
|
|
* field, so an already-confirmed value must remain intact.
|
|
*/
|
|
export type PiNativeRuntimeState = {
|
|
model?: string | null;
|
|
provider?: string | null;
|
|
thinkingLevel?: PiThinkingLevel | null;
|
|
steeringMode?: 'all' | 'one-at-a-time';
|
|
isStreaming?: boolean;
|
|
};
|
|
|
|
/**
|
|
* Pi session state and hub communication wrapper.
|
|
*
|
|
* Unlike other agents that extend AgentSessionBase (which requires MessageQueue2),
|
|
* Pi sends messages directly via PiTransport RPC — no queue needed.
|
|
* This class manages Pi-specific runtime state and hub keepAlive.
|
|
*/
|
|
export class PiSession {
|
|
readonly api: ApiClient;
|
|
readonly client: ApiSessionClient;
|
|
readonly path: string;
|
|
readonly logPath: string;
|
|
readonly startedBy: 'runner' | 'terminal';
|
|
// Mutable mode — updated by setMode() when the hub switches control
|
|
// (local ↔ remote). keepAlive reads this so the reported mode does not
|
|
// revert to the constructor-time startingMode every 2s tick.
|
|
mode: 'local' | 'remote';
|
|
|
|
// Config state — synced to hub via keepAlive.
|
|
// `undefined` means "not yet known" and is OMITTED from keepAlive so the hub
|
|
// does not clear a persisted value; `null` is an explicit clear. A value is
|
|
// only assigned once Pi confirms it (get_state / successful set_model /
|
|
// successful set_thinking_level).
|
|
currentModel: string | null | undefined;
|
|
currentThinkingLevel: PiThinkingLevel | null | undefined;
|
|
// Pi's set_model requires provider + modelId; learned from get_state
|
|
currentProvider: string | null = null;
|
|
// Startup model from opts.model — prevents get_state from overwriting it
|
|
// with Pi's default. Applied once when get_available_models returns.
|
|
readonly initialModel: string | null;
|
|
/**
|
|
* Settles once the startup model attempt has finished (applied, rejected,
|
|
* or absent). Startup effort waits on it so set_thinking_level never races
|
|
* ahead of set_model — a level the default model rejects would otherwise
|
|
* be lost before the requested model is confirmed.
|
|
*/
|
|
readonly startupModelSettled: Promise<void>;
|
|
resolveStartupModelSettled: (() => void) | null = null;
|
|
// A runner/native resume must prove that Pi loaded this exact session with
|
|
// a non-empty get_state sessionId. Missing or contradictory IDs fail closed.
|
|
expectedNativeSessionId: string | null;
|
|
currentNativeSessionFile: string | null = null;
|
|
|
|
// Streaming state. A generation identifies one concrete Pi turn, rather
|
|
// than merely the transient boolean reported by get_state/lifecycle events.
|
|
// Native steers capture it at arrival and must not cross into a later turn.
|
|
private _piIsStreaming = false;
|
|
private streamingGeneration = 0;
|
|
private promptInFlight = false;
|
|
currentSteeringMode: 'all' | 'one-at-a-time' = 'all';
|
|
|
|
// Cached data from Pi
|
|
cachedPiModels: PiModelSummary[] = [];
|
|
cachedPiCommands: PiCommandSummary[] = [];
|
|
|
|
// RPC resolver — initialized by wireTransportEvents, session-scoped
|
|
rpcResolver: PiRpcResolver | null = null;
|
|
|
|
// Startup ready gate (issue #1143). Pi's socket goes `active` (spawn success)
|
|
// before `pi --mode rpc` returns its initial `get_state`, so a prompt sent
|
|
// in that window reaches Pi before its session is initialized and wedges
|
|
// (agent_start, then silence). Outbound sends that assume a live Pi session
|
|
// are queued via runWhenReady() and drained FIFO once markReady() fires (on
|
|
// the first get_state response).
|
|
private piReady = false;
|
|
private readyCancelled = false;
|
|
private nativeReadyAnnounced = false;
|
|
private nativeReadyPreparation: (() => Promise<void>) | null = null;
|
|
private nativeReadyPreparationStarted = false;
|
|
private historyTransaction: symbol | null = null;
|
|
// All commands that mutate the native Pi runtime (history clone/fork/switch,
|
|
// model/thinking changes, and abort) share one FIFO lock. A history action
|
|
// closes its prompt gate before waiting on this tail, which prevents a later
|
|
// runtime mutation from slipping between its final source sync and fork.
|
|
private runtimeMutationTail: Promise<void> = Promise.resolve();
|
|
// Buffered sends carry their localId so a cancel-queued-message that arrives
|
|
// while a prompt is still held (before drain) can drop it instead of firing
|
|
// a cancelled prompt on markReady (issue #1143 review — MAJOR).
|
|
private readyQueue: Array<{ localId?: string; fn: () => void }> = [];
|
|
private historyDeferredQueue: Array<{ localId?: string; fn: () => void }> = [];
|
|
|
|
private keepAliveInterval: NodeJS.Timeout | null = null;
|
|
|
|
constructor(opts: {
|
|
api: ApiClient;
|
|
client: ApiSessionClient;
|
|
path: string;
|
|
logPath: string;
|
|
startedBy: 'runner' | 'terminal';
|
|
startingMode: 'local' | 'remote';
|
|
model?: string | null;
|
|
expectedNativeSessionId?: string;
|
|
}) {
|
|
this.api = opts.api;
|
|
this.client = opts.client;
|
|
this.path = opts.path;
|
|
this.logPath = opts.logPath;
|
|
this.startedBy = opts.startedBy;
|
|
this.mode = opts.startingMode;
|
|
// currentModel/currentThinkingLevel start undefined ("not yet known")
|
|
// and are set only from Pi's confirmed state (get_state) or a successful
|
|
// set_model/set_thinking_level. Seeding from opts.model/opts.effort here
|
|
// would leak unconfirmed values via the first keepAlive; they are captured
|
|
// as initialModel/startupThinkingLevel and applied once Pi accepts them.
|
|
// undefined is distinct from null (explicit clear): keepAlive omits
|
|
// undefined fields so the hub does not wipe a persisted model/effort on
|
|
// resume before Pi reports its real state.
|
|
this.currentModel = undefined;
|
|
this.initialModel = opts.model?.trim() || null;
|
|
this.startupModelSettled = new Promise<void>((resolve) => {
|
|
this.resolveStartupModelSettled = resolve;
|
|
});
|
|
// No requested startup model → nothing to serialize; release waiters
|
|
// immediately so the effort path keeps its current timing.
|
|
if (!this.initialModel) {
|
|
this.resolveStartupModelSettled?.();
|
|
}
|
|
this.expectedNativeSessionId = opts.expectedNativeSessionId?.trim() || null;
|
|
this.currentThinkingLevel = undefined;
|
|
}
|
|
|
|
/** True once Pi RPC startup has completed and buffered sends have drained. */
|
|
get isReady(): boolean {
|
|
return this.piReady;
|
|
}
|
|
|
|
/** True only after Pi itself has completed a successful get_state. */
|
|
get isNativeReady(): boolean {
|
|
return this.nativeReadyAnnounced;
|
|
}
|
|
|
|
/**
|
|
* Establish a stable native baseline before buffered prompts are released.
|
|
* Pi's history cursor must be read before the first web prompt is allowed
|
|
* to append, otherwise an old duplicate user message could be paired with
|
|
* a new HAPI localId.
|
|
*/
|
|
setNativeReadyPreparation(fn: () => Promise<void>): void {
|
|
this.nativeReadyPreparation = fn;
|
|
}
|
|
|
|
/** Prevent any late startup/history completion from draining outbound work. */
|
|
cancelReadyGate(): void {
|
|
this.readyCancelled = true;
|
|
this.readyQueue = [];
|
|
this.historyDeferredQueue = [];
|
|
}
|
|
|
|
get isHistoryTransactionActive(): boolean {
|
|
return this.historyTransaction !== null;
|
|
}
|
|
|
|
/** True from prompt dispatch until rejection, abort, or true settlement. */
|
|
get hasPromptInFlight(): boolean {
|
|
return this.promptInFlight;
|
|
}
|
|
|
|
get piIsStreaming(): boolean {
|
|
return this._piIsStreaming;
|
|
}
|
|
|
|
set piIsStreaming(value: boolean) {
|
|
// Keep direct state reconciliation (including test adapters) on the
|
|
// same transition invariant as updateThinkingState().
|
|
if (value && !this._piIsStreaming) this.streamingGeneration += 1;
|
|
this._piIsStreaming = value;
|
|
}
|
|
|
|
/** Exact identity of the active Pi stream; absent while Pi is idle. */
|
|
get currentStreamingGeneration(): number | null {
|
|
return this._piIsStreaming ? this.streamingGeneration : null;
|
|
}
|
|
|
|
setPromptInFlight(value: boolean): void {
|
|
this.promptInFlight = value;
|
|
}
|
|
|
|
beginHistoryTransaction(): (options?: { drain?: boolean }) => void {
|
|
if (this.historyTransaction) throw new Error('Conversation history action already in progress');
|
|
const token = Symbol('pi-history-transaction');
|
|
this.historyTransaction = token;
|
|
return (options = {}) => {
|
|
if (this.historyTransaction !== token) return;
|
|
this.historyTransaction = null;
|
|
const deferred = this.historyDeferredQueue;
|
|
this.historyDeferredQueue = [];
|
|
if (options.drain === false) return;
|
|
for (const { fn } of deferred) fn();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Acquire exclusive ownership of the native Pi runtime. Callers which need
|
|
* a transaction spanning multiple RPCs (conversation history) can retain
|
|
* the release callback; single RPC handlers should use runRuntimeMutation.
|
|
*/
|
|
async acquireRuntimeMutation(): Promise<() => void> {
|
|
const previous = this.runtimeMutationTail;
|
|
let releaseCurrent!: () => void;
|
|
this.runtimeMutationTail = new Promise<void>((resolve) => {
|
|
releaseCurrent = resolve;
|
|
});
|
|
await previous;
|
|
|
|
let released = false;
|
|
return () => {
|
|
if (released) return;
|
|
released = true;
|
|
releaseCurrent();
|
|
};
|
|
}
|
|
|
|
/** Serialize one Pi runtime mutation behind any active history operation. */
|
|
async runRuntimeMutation<T>(
|
|
operation: () => Promise<T>,
|
|
options: { poisonOnError?: (error: unknown) => boolean } = {},
|
|
): Promise<T> {
|
|
const release = await this.acquireRuntimeMutation();
|
|
let releaseLease = true;
|
|
try {
|
|
return await operation();
|
|
} catch (error) {
|
|
if (options.poisonOnError?.(error)) releaseLease = false;
|
|
throw error;
|
|
} finally {
|
|
if (releaseLease) release();
|
|
}
|
|
}
|
|
|
|
assertNoHistoryTransaction(operation: string): void {
|
|
if (this.historyTransaction) {
|
|
throw new Error(`Cannot ${operation} while a conversation history action is in progress`);
|
|
}
|
|
}
|
|
|
|
/** Queue ordinary outbound work until native source identity is restored. */
|
|
runWhenHistoryIdle(fn: () => void, localId?: string): void {
|
|
if (!this.historyTransaction) {
|
|
fn();
|
|
return;
|
|
}
|
|
this.historyDeferredQueue.push({ fn, localId });
|
|
}
|
|
|
|
matchesExpectedNativeSessionId(actualSessionId: string | undefined): boolean {
|
|
if (!this.expectedNativeSessionId) return true;
|
|
return Boolean(actualSessionId) && actualSessionId === this.expectedNativeSessionId;
|
|
}
|
|
|
|
/**
|
|
* Commit an in-process native session transition (Pi rewind creates a new
|
|
* branched session file). Future get_state validation must target the new
|
|
* id before its metadata is exposed to the hub.
|
|
*/
|
|
commitNativeSessionIdentity(
|
|
identity: { sessionId: string; sessionFile: string },
|
|
metadataUpdater?: (metadata: Metadata) => Metadata,
|
|
): void {
|
|
this.commitNativeSessionState(identity, {}, metadataUpdater);
|
|
}
|
|
|
|
/**
|
|
* Commit a confirmed get_state snapshot after a native session transition.
|
|
* This intentionally updates runtime values before metadata is made visible
|
|
* so the next keepAlive cannot report the old branch configuration.
|
|
*/
|
|
commitNativeSessionState(
|
|
identity: { sessionId: string; sessionFile: string },
|
|
runtime: PiNativeRuntimeState,
|
|
metadataUpdater?: (metadata: Metadata) => Metadata,
|
|
): void {
|
|
const identityChanged = this.expectedNativeSessionId !== identity.sessionId
|
|
|| this.currentNativeSessionFile !== identity.sessionFile;
|
|
// A rewind can replace the native branch while both snapshots report
|
|
// streaming=true. Invalidate any steer captured for the old branch
|
|
// even though the boolean never passed through an observable idle edge.
|
|
if (identityChanged && this._piIsStreaming && runtime.isStreaming !== false) {
|
|
this.streamingGeneration += 1;
|
|
}
|
|
this.expectedNativeSessionId = identity.sessionId;
|
|
this.currentNativeSessionFile = identity.sessionFile;
|
|
this.applyNativeRuntimeState(runtime);
|
|
this.updateMetadata((metadata) => {
|
|
let next = metadataUpdater?.({
|
|
...metadata,
|
|
piSessionId: identity.sessionId,
|
|
}) ?? {
|
|
...metadata,
|
|
piSessionId: identity.sessionId,
|
|
};
|
|
// A get_state model/provider pair is the authoritative provider-
|
|
// qualified selection for the newly active native branch. Preserve
|
|
// existing metadata only when an older Pi omitted both fields.
|
|
if (runtime.model !== undefined || runtime.provider !== undefined) {
|
|
next = { ...next };
|
|
delete next.piSelectedModel;
|
|
if (this.currentModel && this.currentProvider) {
|
|
next.piSelectedModel = {
|
|
provider: this.currentProvider,
|
|
modelId: this.currentModel,
|
|
};
|
|
}
|
|
}
|
|
return next;
|
|
});
|
|
}
|
|
|
|
/** Apply a confirmed state snapshot without changing native identity metadata. */
|
|
applyNativeRuntimeState(runtime: PiNativeRuntimeState): void {
|
|
if (runtime.model !== undefined) {
|
|
if (runtime.provider === undefined && runtime.model !== this.currentModel) {
|
|
if (runtime.model === null) {
|
|
this.currentProvider = null;
|
|
} else {
|
|
const matchingProviders = new Set(
|
|
this.cachedPiModels
|
|
.filter((model) => model.modelId === runtime.model)
|
|
.map((model) => model.provider),
|
|
);
|
|
// Never combine a newly reported model id with the previous
|
|
// branch's provider. Infer only from an unambiguous catalog.
|
|
this.currentProvider = matchingProviders.size === 1
|
|
? matchingProviders.values().next().value ?? null
|
|
: null;
|
|
}
|
|
}
|
|
this.currentModel = runtime.model;
|
|
}
|
|
if (runtime.provider !== undefined) this.currentProvider = runtime.provider;
|
|
if (runtime.thinkingLevel !== undefined) this.currentThinkingLevel = runtime.thinkingLevel;
|
|
if (runtime.steeringMode !== undefined) this.currentSteeringMode = runtime.steeringMode;
|
|
if (runtime.isStreaming !== undefined) this.piIsStreaming = runtime.isStreaming;
|
|
this.pushKeepAlive();
|
|
}
|
|
|
|
/** Wait for queued metadata writes before exposing a native history result. */
|
|
async flushMetadata(timeoutMs: number = 5_000): Promise<boolean> {
|
|
const flush = (this.client as Partial<ApiSessionClient>).flushMetadata
|
|
return flush ? await flush.call(this.client, timeoutMs) : true
|
|
}
|
|
|
|
/**
|
|
* Run `fn` now if Pi startup is ready, else buffer it FIFO until markReady().
|
|
* Used to gate outbound prompt/steer sends so they never reach Pi before its
|
|
* session is initialized (issue #1143). Pass the message `localId` so a
|
|
* cancel-queued-message can drop it while still buffered.
|
|
*/
|
|
runWhenReady(fn: () => void, localId?: string): void {
|
|
if (this.readyCancelled) return;
|
|
if (this.piReady) {
|
|
fn();
|
|
return;
|
|
}
|
|
this.readyQueue.push({ localId, fn });
|
|
}
|
|
|
|
/**
|
|
* Drop a still-buffered send by localId (cancel-queued-message contract).
|
|
* Returns true if it was buffered and removed (so the hub un-queues the row),
|
|
* false if it was already drained/sent to Pi or never buffered (best-effort,
|
|
* mirrors the other agents' queue.cancelByLocalId semantics).
|
|
*/
|
|
cancelBufferedMessage(localId: string): boolean {
|
|
const idx = this.readyQueue.findIndex((item) => item.localId === localId);
|
|
if (idx !== -1) {
|
|
this.readyQueue.splice(idx, 1);
|
|
return true;
|
|
}
|
|
const deferredIdx = this.historyDeferredQueue.findIndex((item) => item.localId === localId);
|
|
if (deferredIdx === -1) return false;
|
|
this.historyDeferredQueue.splice(deferredIdx, 1);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Release buffered sends. This intentionally does not notify the hub: the
|
|
* startup fallback may use it to avoid losing prompts, but is not proof that
|
|
* Pi loaded a requested native session.
|
|
*/
|
|
markReady(): boolean {
|
|
if (this.piReady || this.readyCancelled) return false;
|
|
this.piReady = true;
|
|
const queued = this.readyQueue;
|
|
this.readyQueue = [];
|
|
for (const { fn } of queued) fn();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* The first successful Pi get_state is the authoritative native-ready
|
|
* point. Unlike markReady(), this tells the hub that a native resume can be
|
|
* considered successful.
|
|
*/
|
|
markNativeReady(): void {
|
|
if (this.readyCancelled || this.nativeReadyAnnounced || this.nativeReadyPreparationStarted) return;
|
|
// The hub uses session-ready as the validated native identity fence.
|
|
// It must precede piSessionId metadata publication, while prompt drain
|
|
// may wait for the append-log baseline below.
|
|
this.nativeReadyAnnounced = true;
|
|
this.client.emitSessionReady();
|
|
if (this.nativeReadyPreparation) {
|
|
this.nativeReadyPreparationStarted = true;
|
|
void this.nativeReadyPreparation()
|
|
// The history feature is optional. A failed baseline must not
|
|
// wedge a normal Pi session; it simply remains unpublished.
|
|
.catch(() => {})
|
|
.finally(() => this.finishNativeReady());
|
|
return;
|
|
}
|
|
this.finishNativeReady();
|
|
}
|
|
|
|
private finishNativeReady(): void {
|
|
this.markReady();
|
|
}
|
|
|
|
startKeepAlive(): void {
|
|
this.pushKeepAlive();
|
|
this.keepAliveInterval = setInterval(() => this.pushKeepAlive(), 2000);
|
|
}
|
|
|
|
stopKeepAlive(): void {
|
|
if (this.keepAliveInterval) {
|
|
clearInterval(this.keepAliveInterval);
|
|
this.keepAliveInterval = null;
|
|
}
|
|
}
|
|
|
|
private getKeepAliveRuntime(): Parameters<ApiSessionClient['keepAlive']>[2] {
|
|
const runtime: NonNullable<Parameters<ApiSessionClient['keepAlive']>[2]> = {};
|
|
if (this.currentModel !== undefined) runtime.model = this.currentModel;
|
|
if (this.currentThinkingLevel !== undefined) runtime.effort = this.currentThinkingLevel;
|
|
return Object.keys(runtime).length > 0 ? runtime : undefined;
|
|
}
|
|
|
|
pushKeepAlive(): void {
|
|
this.client.keepAlive(this.piIsStreaming, this.mode, this.getKeepAliveRuntime());
|
|
}
|
|
|
|
updateThinkingState(thinking: boolean): void {
|
|
this.piIsStreaming = thinking;
|
|
this.client.keepAlive(thinking, this.mode, this.getKeepAliveRuntime());
|
|
}
|
|
|
|
setMode(mode: 'local' | 'remote'): void {
|
|
this.mode = mode;
|
|
this.pushKeepAlive();
|
|
}
|
|
|
|
updateMetadata(updater: (meta: Metadata) => Metadata): void {
|
|
this.client.updateMetadata(updater);
|
|
}
|
|
|
|
sendAgentMessage(message: unknown): void {
|
|
this.client.sendAgentMessage(message);
|
|
}
|
|
|
|
emitMessagesConsumed(localIds: string[], options?: { clearQueuedThinkingGrace?: boolean }): void {
|
|
this.client.emitMessagesConsumed(localIds, options);
|
|
}
|
|
|
|
sendSessionEvent(event: Parameters<ApiSessionClient['sendSessionEvent']>[0]): void {
|
|
this.client.sendSessionEvent(event);
|
|
}
|
|
}
|