Injection
Parameter decorators that say what each argument should receive. Because each one declares itself, order does not matter — and two parameters of the same type stop being ambiguous.
import { input, context, state, memory } from "@thenajs/core";| Decorator | Where | What it delivers |
|---|---|---|
@input() | a tool's execute | the arguments, validated by the schema |
@context() | a tool's execute | the run's Context |
@state() | an agent constructor, a tool's execute | the @Workflow({ state }) instance |
@memory(Store?) | an agent constructor | a VectorMemory |
In tools
@Tool({ name: "read", description: "…", schema: z.object({ path: z.string() }) })
export class ReadTool {
async execute(
@input() { path }: { path: string },
@context() ctx: Context,
@state() s: ReviewState,
) {
s.filesRead.push(path);
return readFile(path, "utf8", { signal: ctx.signal });
}
}With no decorators, execute receives just the arguments — the common case:
async execute({ path }: { path: string }) { … }In agents
@Agent({ provider: MyProvider, prompt: "./a.agent.md" })
export class MyAgent {
constructor(
@state() private readonly s: ReviewState,
@memory(QdrantOpenAI) private readonly vectors: VectorMemory,
) {}
}@memory(Store) identifies the memory by the store's class, removing the dependency on the order of ThenaConfig.stores. With no argument it delivers the first registered one.
Without the decorator the positional contract applies — which is why reordering that array silently changes behaviour.
@context() does not work in a constructor
The agent is instantiated before the run begins, so the context does not exist yet. The runtime fails with that explanation instead of injecting undefined. Use it in a tool's execute, or take ctx as a hook parameter.
@tools() — the agent's other tools
async execute(@input() args: Args, @tools() siblings: ToolType[]) {
const target = siblings.find((t) => t.name === "read_file");
return target?.execute({ path: "src/main.ts" });
}Gives a tool the other tools registered on the same agent, already wrapped in the middleware chain. That wrapping is the point: a sibling called this way still opens its own report node and passes through the agent hooks, the app.use({ tool }) middlewares, budget accounting and the tool error policy. A list handed in from outside would lose all five.
Validate the arguments yourself before dispatching — the wrapper runs the chain, not the schema:
const args = target.schema.parse(raw);
await target.execute(args);execute only
The tools are wrapped per step invocation, so during the constructor the list does not exist yet. Using @tools() there fails with that explanation.
ParallelTool is built on this.
context() as a function
provider: () => new OpenAIProvider({ apiKey: keyFor(context().data) });Same object, different timing: inside a step you get the step's context, with state and turn; outside one you get the run's, and touching state throws with an explanation.
Tool constructors
Tools are instantiated by the framework, so their constructors take dependencies too — most usefully WorkflowRuntime:
export class ResearchTool {
constructor(private readonly runtime: WorkflowRuntime) {}
}Why not by type
reflect-metadata reads parameter types and would make these decorators unnecessary. It is not used because esbuild — which tsx uses in dev — does not emit design:paramtypes: type-based injection would compile and then break silently under npm start.
Decorator calls are emitted on both paths.
This is also why experimentalDecorators is required rather than the Stage 3 proposal, which has no parameter decorators at all.
If you do not declare state
| Situation | What happens |
|---|---|
| nobody asks for state | works normally |
an agent asks with @state() | error naming the class and the parameter |
a tool asks with @state() | error naming the method and the parameter |
an until declares a 2nd parameter | error saying to add state |
The last is detected before running, from the arity of until. Without that check the state would arrive undefined and surface as a TypeError on the first field read, never saying what was missing.
A one-parameter until (untilAnswered, or (ctx) => …) never triggers it.
Errors
[thena] @state() in ReviewerAgent (parameter 0): no state declared.
Add `state: MyClass` to @Workflow.
[thena] @memory(QdrantOpenAI) in MyAgent: that store is not registered in
ThenaConfig.stores.Related
- Dependency injection — the concept
- Context
- Vector store
