Agent Lifecycle Signals
The @threadplane/langgraph library exposes per-agent lifecycle signals on every LangGraphAgent returned by injectAgent(). They are timestamps and classifications derived from the stream you already have — handy for debugging, custom dashboards, or telemetry integrations.
Interface
import { InjectionToken, Signal } from '@angular/core';
import type { AgentErrorKind } from '@threadplane/chat';
export interface AgentLifecycle {
/** Epoch ms of the first stream chunk arrival. Resets on switchThread(). */
readonly streamStartedAt: Signal<number | null>;
/** Epoch ms + failure class of the most recent stream error. Resets on switchThread(). */
readonly streamErrorAt: Signal<{ at: number; kind: AgentErrorKind | string } | null>;
/** Epoch ms of the first interrupt$ non-null in this stream. Resets on switchThread(). */
readonly interruptReceivedAt: Signal<number | null>;
/** Epoch ms of the most recent submit({ resume }) call. Resets on switchThread(). */
readonly interruptResolvedAt: Signal<number | null>;
/** Epoch ms when the agent's "create new thread" branch fired. Resets on switchThread(). */
readonly threadCreatedAt: Signal<number | null>;
/** Epoch ms when an existing thread was restored from server (proves persistence). Resets on switchThread(). */
readonly threadPersistedAt: Signal<number | null>;
/** Epoch ms of the first tool call append. Resets on switchThread(). */
readonly toolCallStartedAt: Signal<number | null>;
/** Epoch ms of the first tool call result transition. Resets on switchThread(). */
readonly toolCallCompletedAt: Signal<number | null>;
}
export const AGENT_LIFECYCLE = new InjectionToken<AgentLifecycle>('AGENT_LIFECYCLE');Derivation
Six of the eight signals derive directly from existing stream subjects on the agent (values$, messages$, error$, interrupt$, toolCalls$, history$):
| Signal | Source |
|---|---|
streamStartedAt | first non-empty values$ or messages$ emission |
streamErrorAt | error$ emission, classified by AgentErrorKind |
interruptReceivedAt | first non-null interrupt$ value |
threadPersistedAt | first non-empty history$ emission |
toolCallStartedAt | first tool-call append in toolCalls$ |
toolCallCompletedAt | first tool-call result transition in toolCalls$ |
Two signals require explicit hook points that the agent already invokes:
| Signal | Hook |
|---|---|
interruptResolvedAt | submit({ resume }) |
threadCreatedAt | the first submit() on an agent that has no thread yet |
streamErrorAt().kind is the AgentErrorKind of the failure — connection, auth, server, interrupted, or aborted — the same value agent.error()?.kind carries. The runtime normalizes every failure through toAgentError() before it reaches the lifecycle, so the discriminator is actionable. A value the runtime could not normalize falls back to the error's constructor name, which is why the type is AgentErrorKind | string.
Subscribing
import { Component, effect } from '@angular/core';
import { injectAgent } from '@threadplane/langgraph';
@Component({ /* ... */ })
export class MyComponent {
private chat = injectAgent();
constructor() {
effect(() => {
const err = this.chat.lifecycle.streamErrorAt();
if (err) {
console.log('Stream error at', err.at, 'kind:', err.kind);
}
});
}
}For application-wide instrumentation, inject AgentLifecycleRegistry and read back the lifecycles of every live agent. It is providedIn: 'root', so nothing has to be provided and every agent registers into the same instance no matter which injector built it — a route-scoped or component-scoped provideAgent() is still visible from the root:
import { inject } from '@angular/core';
import { AgentLifecycleRegistry } from '@threadplane/langgraph';
const registry = inject(AgentLifecycleRegistry);
const lifecycles = registry.lifecycles();Registration is scoped to the agent's lifetime: an agent unregisters when the injector that created it is destroyed, so lifecycles() never accumulates lifecycles for agents that are gone.
provideAgent() also provides the AGENT_LIFECYCLE token, so a service that needs only the timings can inject the lifecycle without reaching for the agent:
import { inject } from '@angular/core';
import { AGENT_LIFECYCLE } from '@threadplane/langgraph';
const lifecycle = inject(AGENT_LIFECYCLE); // === injectAgent().lifecycleAGENT_LIFECYCLE follows the same last-one-wins rule as the ref-less injectAgent(): when several provideAgent(ref, …) calls share an injector level, the token resolves the lifecycle of the last ref provided. Read injectAgent(ref).lifecycle when an injector provides more than one agent. See provideAgent().
Reset semantics
All eight signals reset on switchThread(). This keeps lifecycle observations scoped to the current thread.