LANGGRAPH · August 26, 2026 · 4 min read
What injectAgent() Actually Returns
The signals, the async methods, and the runtime-neutral Agent contract underneath — what you get from one call.
You call injectAgent() once, you get one object back — this post is about what is actually in it.
The API page answers "what's the signature." That is the right question when you are mid-keystroke. This post answers the other one: what is each piece of the return value for, and why is it shaped the way it is?
What are the signals?
Six core signals, and together they are the reactive picture most apps need.
One at a time:
messages— the conversation as aMessage[], updated as tokens stream in. This is what you@forover.status— where the agent is in its run lifecycle, as a single value you can switch on.isLoading—truewhile a run is in flight. The signal behind every "Thinking…" indicator.error— the last run's failure, orundefined. Render it; do nottry/catchyour template.toolCalls— the tool calls the model has made, so you can show work-in-progress instead of dead air.state— the graph's custom state, typed to yourTwhen you use a typed agent ref.
There is a little more on the contract: optional interrupt and subagents signals when the adapter supports those capabilities, and an events$ observable for everything that does not fit a signal.
That is the surface you bind templates to.
No subscriptions, no async pipe bookkeeping, no manual change detection — a streaming token lands in messages, and Angular's reactivity does the rest.
What are the methods?
Four, and they are the imperative half — the things user actions call.
In the order you will reach for them:
submit(input, opts?)— send a user message (or a resume payload, or a state patch) and start a run. Returns a promise that settles when the run does.stop()— cancel the in-flight run.retry()— re-run the last submission after a failure. It is deliberately safe to wire to a button: it is a no-op if a run is already in flight or there is nothing to retry.regenerate(assistantMessageIndex)— discard the assistant message at that index and everything after it, then re-submit the user message that preceded it. This is how "regenerate response" works without you managing message surgery yourself.
Signals tell the template what is true; these methods are how the user changes it.
Why is the return type two types?
Because most of what you get back is not LangGraph-shaped — and that is on purpose.
injectAgent() from @threadplane/langgraph returns a LangGraphAgent<T>, which extends the runtime-neutral Agent contract (by way of AgentWithHistory, which adds a history signal — a sub-contract, so do not count on history surviving a runtime swap; the AG-UI agent implements plain Agent).
Everything above — the six signals, the four methods — lives on that neutral contract.
The neutral slice is what <chat> and the other primitives consume.
They do not know they are talking to LangGraph.
The layer on top is where the runtime shows through.
LangGraphAgent adds the runtime-specific members — raw langGraph*-prefixed signals that expose the underlying BaseMessage and thread-state shapes, plus things like value, branch/setBranch, switchThread, and lifecycle.
They are additive. You reach for them when you need LangGraph itself; you ignore them when you do not.
Here is the payoff: the AG-UI adapter's injectAgent() returns the same neutral slice.
Swap the adapter, and every component bound to messages, isLoading, and submit keeps working.
For me, that is the strongest reason to keep your components on the neutral surface and treat the langGraph* members as an escape hatch — choosing an adapter walks through the tradeoff.
What does this look like in a component?
The whole thing fits in one small component:
import { Component } from '@angular/core';
import { injectAgent } from '@threadplane/langgraph';
@Component({
selector: 'app-support',
template: `
@for (message of chat.messages(); track message.id) {
<p>{{ message.content }}</p>
}
@if (chat.isLoading()) {
<p>Thinking…</p>
}
`,
})
export class SupportComponent {
readonly chat = injectAgent();
}Sending is the same object: a submit button calls chat.submit({ message: text }), and the response streams into messages on its own.
This is not the full setup — injectAgent() needs provideAgent() configured first, and the quickstart covers that.
Conclusion
One call returns the whole agent surface: reactive signals for the template, imperative methods for user actions, and a contract that is not LangGraph-shaped underneath. That last part is the one I think matters most — bind to the neutral slice and the runtime becomes a swappable detail.
The API reference has the full signatures, choosing an adapter covers when the neutral contract earns its keep, and if you have not built the streaming surface yet, start with Build a Streaming Chat UI in Angular with LangGraph.