Skip to content

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.

ts
import { input, context, state, memory } from "@thenajs/core";
DecoratorWhereWhat it delivers
@input()a tool's executethe arguments, validated by the schema
@context()a tool's executethe run's Context
@state()an agent constructor, a tool's executethe @Workflow({ state }) instance
@memory(Store?)an agent constructora VectorMemory

In tools

ts
@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:

ts
async execute({ path }: { path: string }) { … }

In agents

ts
@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

ts
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:

ts
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

ts
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:

ts
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

SituationWhat happens
nobody asks for stateworks 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 parametererror 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.