@nestjs-adk/core 0.0.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,28 +1,320 @@
1
1
  # @nestjs-adk/core
2
2
 
3
- **NestJS-native** AI agent framework: decorators (`@Agent`, `@Tool`, `@Skill`, `@WorkflowAgent`), DI and abstract contracts over pluggable engines. The agent instance is the execution handle — inject the class and call `ask()` / `stream()` / `approve()` / `reject()`.
3
+ **Build AI agents the NestJS way.**
4
+
5
+ NestJS developers already know how to build good software: classes, modules, providers and dependency injection. nestjs-adk brings AI agents into that same world. An agent is a class with a decorator. A tool is a provider. Everything is injected, validated and tested like the rest of your app.
6
+
7
+ This package is the framework itself. It gives you the decorators, the module, the run loop and all the contracts. To actually talk to an LLM you also install an engine, and the first supported engine is the Google ADK:
8
+
9
+ ```bash
10
+ npm i @nestjs-adk/core @nestjs-adk/google
11
+ ```
12
+
13
+ ## Your first agent
14
+
15
+ Start by registering the module once, in your root module. This is where you choose the engine and the default model:
16
+
17
+ ```ts
18
+ import { AdkModule } from "@nestjs-adk/core";
19
+ import { GoogleAdkEngine } from "@nestjs-adk/google";
20
+
21
+ @Module({
22
+ imports: [
23
+ AdkModule.forRoot({
24
+ engine: GoogleAdkEngine,
25
+ defaultModel: "gemini-2.5-flash",
26
+ }),
27
+ ],
28
+ providers: [SupportAgent, LookupOrderTool, OrdersService, ChatService],
29
+ })
30
+ export class AppModule {}
31
+ ```
32
+
33
+ Now create the agent. It is a class that extends `AdkAgent` and is described by the `@Agent` decorator:
4
34
 
5
35
  ```ts
36
+ import { Agent, AdkAgent } from "@nestjs-adk/core";
37
+
6
38
  @Agent({
7
39
  name: "support_agent",
8
40
  description: "Customer support.",
9
- model: "gemini-2.5-flash",
10
41
  prompt: "You are the store's support agent.",
11
42
  tools: [LookupOrderTool],
12
43
  })
13
44
  export class SupportAgent extends AdkAgent {}
45
+ ```
46
+
47
+ Notice that the agent went into `providers` like any other class. There is no special registration step. If you forget to register a tool, a prompt class or a sub-agent, the app fails at startup with an error that points at the missing class, so configuration mistakes never reach runtime.
48
+
49
+ To use the agent, inject it. The instance itself is the handle:
14
50
 
15
- // anywhere in your app — plain Nest DI
16
- constructor(private readonly support: SupportAgent) {}
17
- const { text } = await this.support.ask({ sessionId, message: "where is my order?" });
51
+ ```ts
52
+ @Injectable()
53
+ export class ChatService {
54
+ constructor(private readonly support: SupportAgent) {}
55
+
56
+ async answer(sessionId: string, message: string) {
57
+ const { text } = await this.support.ask({ sessionId, message });
58
+ return text;
59
+ }
60
+ }
18
61
  ```
19
62
 
20
- Includes: fail-fast boot validation, prompts (literal, `.md` files or `AdkPrompt` builder classes), model specs (`OpenAiLike`, `ModelRouter` with failover), sessions with pluggable `SessionStore`, automatic artifact offload, native compaction, HITL approvals, structured output validation, leveled run logs with token usage, and an `Embedder` contract.
63
+ `ask()` runs the agent and returns the final result. `stream()` gives you the events one by one while the agent works. Later in this document you will also meet `approve()` and `reject()`, used for human approval.
21
64
 
22
- Pair it with an engine [`@nestjs-adk/google`](https://www.npmjs.com/package/@nestjs-adk/google) and, for tests, [`@nestjs-adk/testing`](https://www.npmjs.com/package/@nestjs-adk/testing).
65
+ That is the whole mental model: configure the module once, register classes as providers, inject the agent and call it.
23
66
 
24
- ```bash
25
- npm i @nestjs-adk/core @nestjs-adk/google
67
+ ## Tools
68
+
69
+ A tool is something the model can decide to call. In nestjs-adk a shared tool is a class that extends `AdkTool`. The Zod schema plays two roles at the same time: it tells the model what arguments exist, and it types the input for you:
70
+
71
+ ```ts
72
+ import { Tool, AdkTool, type ToolContext } from "@nestjs-adk/core";
73
+ import { z } from "zod";
74
+
75
+ const schema = z.object({ city: z.string().describe("City name") });
76
+
77
+ @Tool({ name: "get_weather", description: "Current weather.", schema })
78
+ export class GetWeatherTool extends AdkTool<typeof schema> {
79
+ constructor(private readonly weather: WeatherService) {
80
+ super();
81
+ }
82
+
83
+ execute(input: z.infer<typeof schema>, ctx: ToolContext) {
84
+ return this.weather.fetch(input.city);
85
+ }
86
+ }
87
+ ```
88
+
89
+ The tool is a normal provider, so it can inject services, repositories or anything else. Whatever `execute` returns goes back to the model, as long as it is serializable.
90
+
91
+ The `execute` method receives two very different things. The `input` comes from the model: it decided the values based on the schema. The `ctx` comes from your application: it carries `userId`, custom `attributes` and the session `state` that you passed to `ask()`. This separation matters for security. Sensitive data like a tenant id should never be part of the schema, because the model could invent it. Pass it through `ctx` instead, where the model cannot touch it.
92
+
93
+ When a tool belongs to a single agent, you can skip the class and declare it as a method on the agent itself:
94
+
95
+ ```ts
96
+ @Agent({ name: "support_agent", description: "Customer support.", prompt: "..." })
97
+ export class SupportAgent extends AdkAgent {
98
+ constructor(private readonly orders: OrdersService) {
99
+ super();
100
+ }
101
+
102
+ @Tool({ description: "Looks up an order by id.", schema: orderSchema })
103
+ lookupOrder(input: z.infer<typeof orderSchema>) {
104
+ return this.orders.find(input.orderId);
105
+ }
106
+ }
107
+ ```
108
+
109
+ ## Skills
110
+
111
+ Skills are blocks of domain knowledge written as text. They exist so your prompt does not grow into one giant string. A skill can be a class that extends `AdkSkill` with the `@Skill` decorator, or a method on the agent.
112
+
113
+ Each skill has a mode. With `mode: "always"` the content is included in the instruction on every run. The default mode is on demand: the agent only sees a catalog with the skill names and descriptions, plus a `load_skill` tool it can call when it decides it needs the full content. This keeps the context small while still making the knowledge available.
114
+
115
+ ## Prompts
116
+
117
+ There are two ways to give an agent its instruction, and they are separate fields so the intent is always clear.
118
+
119
+ The first way is directly on the decorator. Use `prompt` for literal text and `promptFile` for a markdown file:
120
+
121
+ ```ts
122
+ @Agent({ name: "support", prompt: "You are the store's support agent." })
123
+
124
+ @Agent({ name: "support", promptFile: "agents/support/main.prompt.md" })
125
+
126
+ @Agent({ name: "support", promptFile: "./prompts/main.prompt.md" })
127
+ ```
128
+
129
+ A plain `promptFile` path is resolved from the prompts directory that you configure in `forRoot({ prompts: { dir } })`. A path that starts with `./` is resolved relative to the agent's own file. Files are read once and cached in memory.
130
+
131
+ The second way is a builder class, for prompts that need logic or data. Extend `AdkPrompt`, register it as a provider and point the `prompt` field at the class:
132
+
133
+ ```ts
134
+ @Injectable()
135
+ class SupportPrompt extends AdkPrompt {
136
+ constructor(private readonly config: SupportConfig) {
137
+ super();
138
+ }
139
+
140
+ build(ctx: PromptContext) {
141
+ return this.fromFile("agents/support/main.prompt.md", {
142
+ tone: this.config.tone,
143
+ plan: ctx.state.get("plan"),
144
+ });
145
+ }
146
+ }
147
+
148
+ @Agent({ name: "support", prompt: SupportPrompt })
149
+ ```
150
+
151
+ The builder has full dependency injection and receives the run context, so it can read the state and the attributes you passed to `ask()`. The `fromFile` helper reads a cached template and fills `{{var}}` placeholders.
152
+
153
+ Setting both `prompt` and `promptFile` on the same agent is an error at startup. One agent, one source of instruction.
154
+
155
+ The final instruction is always composed in the same order: the prompt, then the `always` skills, then the on demand catalog. Because the order is stable, the prefix of your requests stays identical between runs, which lets the provider cache it.
156
+
157
+ ## Models
158
+
159
+ The `model` field on `@Agent` and the `defaultModel` on `forRoot` accept a string or a model spec class. Specs are small objects that only carry configuration. The engine turns them into real clients:
160
+
161
+ ```ts
162
+ model: "gemini-2.5-flash"
163
+
164
+ model: new Gemini("gemini-2.5-flash", { labels, cache: { content }, config })
165
+
166
+ model: new OpenAiLike("gpt-4o-mini", { baseUrl, apiKeyEnv })
167
+
168
+ model: new ModelRouter({
169
+ targets: {
170
+ primary: new Gemini("gemini-2.5-flash"),
171
+ fallback: new OpenAiLike("gpt-4o-mini", { baseUrl: "https://openrouter.ai/api/v1" }),
172
+ },
173
+ })
174
+ ```
175
+
176
+ `OpenAiLike` covers every provider that speaks the OpenAI API, which includes OpenAI itself, OpenRouter, Ollama and many others. `Gemini` lives in `@nestjs-adk/google` and adds Vertex AI options, billing labels and explicit content caching.
177
+
178
+ `ModelRouter` gives you failover in one line. When the current target fails before the first chunk of the response, for example with a 429, the router moves to the next target in the declared order. Every switch is reported as a `model_rerouted` event, so failovers are never silent. If you use a router as your `defaultModel`, you get global failover for the whole app.
179
+
180
+ ## Sessions
181
+
182
+ Pass a `sessionId` to `ask()` and the conversation becomes persistent. The agent remembers previous turns because the framework stores every event and replays the history into the model's context on each run. Without a `sessionId` the session is ephemeral and nothing is kept.
183
+
184
+ Storage goes through the `SessionStore` contract. The default implementation keeps everything in memory, which is perfect for development. For production you implement the contract with your own database and pass the class to `forRoot({ session })`. The store is the single source of truth: engines read from it and write to it, and never keep a private copy of the history.
185
+
186
+ ## Validating the session state
187
+
188
+ The session state is a shared bag. Your code writes to it, tools write to it, and it can arrive from outside through a stored session. By default nothing checks those values, so a malformed value can travel all the way into your database layer. If you want a guarantee, declare a schema on the agent:
189
+
190
+ ```ts
191
+ const reportState = z.object({ tenantId: z.string().min(1), count: z.number() });
192
+
193
+ @Agent({ name: "reporter", description: "Builds reports.", state: reportState, ... })
194
+ class ReporterAgent extends AdkAgent {}
195
+ ```
196
+
197
+ From that moment the framework validates the declared keys at every border. When a run starts, the state coming from `ask()` and from the stored session is checked before any call to the model, so an invalid value fails fast with an `AgentStateInvalidError` and costs zero tokens. When a tool writes with `ctx.state.set`, the write is checked at that moment. Keys that the schema does not declare keep flowing freely, which matters for pipelines where one agent writes its output for the next one.
198
+
199
+ Inside a tool you can also demand a value instead of hoping it is there:
200
+
201
+ ```ts
202
+ execute(input, ctx: ToolContext<z.infer<typeof reportState>>) {
203
+ const tenantId = ctx.state.require("tenantId"); // typed as string, throws AgentStateMissingError if absent
204
+ }
205
+ ```
206
+
207
+ The generic on `ToolContext` is an annotation you choose, because the same tool can serve many agents. Typing it gives you autocomplete and typed reads. If a tool serves two agents, annotate it with the union of the two state types and TypeScript will only let you touch the keys both agents share.
208
+
209
+ One note about errors: `AgentStateInvalidError` exposes the raw Zod issues in `error.issues`. Decide what your application logs, because in Zod v4 the issues can include the rejected value.
210
+
211
+ ## Capping the loop
212
+
213
+ A lost model can call tools forever, and a broken tool can fail forever while the model retries. Both burn tokens. The framework ships two optional caps:
214
+
215
+ ```ts
216
+ @Agent({ name: "reporter", maxIterations: 16, maxConsecutiveToolFailures: 2, ... })
217
+ ```
218
+
219
+ `maxIterations` limits the model and tool round trips in a single run. Passing the cap aborts the run with an `AgentMaxIterationsError` that carries the aggregated token usage and the last requested tool, so you know what the loop cost before it died. `maxConsecutiveToolFailures` is a circuit breaker per tool: when the same tool fails that many times in a row the run aborts with a `ToolRepeatedFailureError`, without waiting for the bigger cap. A success resets the count.
220
+
221
+ Both are off unless you set them. You can define module wide defaults with `forRoot({ defaults: { maxIterations: 16 } })`, override them per agent in the decorator, and override both per call in `ask()`. The call wins over the agent, and the agent wins over the module. Because the per call override wins, build your `RunInput` in your own code and never from a raw external payload.
222
+
223
+ These caps protect runs that go through `ask()`, `stream()` and the runner. The `adk web` playground resolves agents through a different path and does not count iterations, which is fine because it is a development tool.
224
+
225
+ ## Keeping the context small
226
+
227
+ Long conversations and big tool results eat your context window. The framework handles both cases for you.
228
+
229
+ When a tool returns a very large result, above 20 thousand characters, the framework stores the full content as an artifact and gives the model a short summary plus a `read_artifact` tool. The model can read the full content when it really needs it. You can turn this off for a specific tool with `offload: false`.
230
+
231
+ For long histories there is compaction. Configure a policy and old turns get summarized by an LLM when the history passes a token threshold:
232
+
233
+ ```ts
234
+ context: contextPolicy({
235
+ compaction: { maxTokens: 50_000, keepRecent: 5 },
236
+ })
26
237
  ```
27
238
 
28
- Full documentation: [github.com/gabrieljsilva/nestjs-adk](https://github.com/gabrieljsilva/nestjs-adk)
239
+ You can set the policy globally in `forRoot` and override it per agent.
240
+
241
+ ## Human approval
242
+
243
+ Some actions should not run without a person saying yes. Mark the tool:
244
+
245
+ ```ts
246
+ @Tool({ name: "refund", description: "Refunds an order.", schema, requiresApproval: true })
247
+ ```
248
+
249
+ `requiresApproval` also accepts a function of the input and the context, so you can require approval only above a value, for example.
250
+
251
+ When the model calls a tool that requires approval, the tool does not execute. The run pauses and returns `status: "pending_approval"` with the pending call id. Your application shows this to a human, and then:
252
+
253
+ ```ts
254
+ await agent.approve({ sessionId, callId }); // executes the tool and resumes the run
255
+ await agent.reject({ sessionId, callId, reason }); // skips the tool and tells the model why
256
+ ```
257
+
258
+ Both calls return a normal run result, so the conversation continues naturally after the decision.
259
+
260
+ ## Structured output
261
+
262
+ When you need data instead of prose, declare an output schema:
263
+
264
+ ```ts
265
+ @Agent({ name: "reporter", description: "Builds reports.", output: reportSchema, outputKey: "report" })
266
+ class ReporterAgent extends AdkAgent<typeof reportSchema> {}
267
+
268
+ const run = await reporter.ask({ message });
269
+ run.output; // typed and validated
270
+ ```
271
+
272
+ The result is parsed and validated with the schema. If the model produces something that does not match, you get an `OutputValidationError` instead of silently wrong data. The optional `outputKey` also writes the validated output into the session state, which is useful to pass data between agents in a pipeline.
273
+
274
+ ## Sub-agents and workflows
275
+
276
+ An agent can delegate to other agents. With `subAgents: [OtherAgent]` the model itself decides when to transfer the conversation. When you want deterministic control instead, declare a workflow:
277
+
278
+ ```ts
279
+ @WorkflowAgent({ name: "etl", mode: "sequential", agents: [ExtractAgent, SummarizeAgent] })
280
+ class EtlWorkflow extends AdkWorkflow {}
281
+ ```
282
+
283
+ Modes are `sequential`, `parallel` and `loop`. A workflow is also an agent: you inject the class and call `ask()` or `stream()` on the instance, exactly like before.
284
+
285
+ ## Streaming, events and errors
286
+
287
+ `stream()` yields a normalized event loop: `run_start`, `tool_call`, `tool_result`, `llm_response`, `model_rerouted`, `approval_required` and `final`. Every event carries a `raw` field with the original payload from the provider, so no information is lost. `ask()` consumes the same loop and aggregates it into a `RunResult` with `text`, `usage`, `events`, `status` and, when declared, `output`.
288
+
289
+ Errors are not events. They throw as typed classes that extend `AdkError` and carry a `code`. Configuration problems throw at startup and point at the class that caused them. Runtime problems throw classes like `AiEmptyResponseError`, `OutputValidationError`, `ToolExecutionError`, `ModelsExhaustedError`, `AgentStateInvalidError` and `AgentMaxIterationsError`, so you can catch exactly what you care about.
290
+
291
+ ## Logs
292
+
293
+ Turn on run logs in the module:
294
+
295
+ ```ts
296
+ AdkModule.forRoot({ engine: GoogleAdkEngine, logging: "debug" })
297
+ ```
298
+
299
+ Logs go through the normal Nest `Logger` with the context `Adk:<agent_name>`. Levels are cumulative. `"info"` (or `true`) logs the start and the end of each run with duration and token usage. `"debug"` adds tool calls and tool results. `"verbose"` adds intermediate model responses and stops truncating payloads. Reroutes and approval pauses are always logged as warnings.
300
+
301
+ ```
302
+ run start session=smoke-1 user=u1 message=What's the status of my order 123?
303
+ tool call lookup_order args={"orderId":"123"}
304
+ tool result lookup_order result={"id":"123","status":"shipped"}
305
+ run done in 1389ms text=Your order 123 has shipped. | tokens in=772 out=41 total=813
306
+ ```
307
+
308
+ Token usage is also available programmatically on every result as `run.usage`.
309
+
310
+ ## Embeddings
311
+
312
+ The core ships an `Embedder` contract with no default implementation, so you bring the provider you prefer. Configure it once with `forRoot({ embedder })` and inject `Embedder` wherever you need vectors, for semantic search or deduplication. The `Similarity` provider offers cosine similarity, and the testing package uses the same embedder for semantic assertions.
313
+
314
+ ## Testing
315
+
316
+ Testing deserves its own package. [`@nestjs-adk/testing`](https://www.npmjs.com/package/@nestjs-adk/testing) gives you a scripted fake LLM, stackable mocks over the real agent instance, Vitest matchers and an LLM-as-judge helper. Your test setup stays plain `@nestjs/testing`.
317
+
318
+ ## Learn more
319
+
320
+ The full project, with a working playground app and real AI smoke tests, lives at [github.com/gabrieljsilva/nestjs-adk](https://github.com/gabrieljsilva/nestjs-adk).
package/dist/index.cjs CHANGED
@@ -159,6 +159,42 @@ class OutputValidationError extends AdkError {
159
159
  this.code = "OUTPUT_VALIDATION_FAILED";
160
160
  }
161
161
  }
162
+ class AgentStateInvalidError extends AdkError {
163
+ constructor(agent, issues, key) {
164
+ super(key
165
+ ? `Agent "${agent}" received a value for state key "${key}" that does not match its declared state schema.`
166
+ : `Agent "${agent}" received state that does not match its declared state schema.`);
167
+ this.issues = issues;
168
+ this.key = key;
169
+ this.code = "AGENT_STATE_INVALID";
170
+ }
171
+ }
172
+ class AgentStateMissingError extends AdkError {
173
+ constructor(agent, key) {
174
+ super(`Agent "${agent}" requires state key "${key}" but it is absent.`);
175
+ this.key = key;
176
+ this.code = "AGENT_STATE_MISSING";
177
+ }
178
+ }
179
+ class AgentMaxIterationsError extends AdkError {
180
+ constructor(agent, limit, usage, lastTool) {
181
+ super(`Agent "${agent}" exceeded maxIterations (${limit}) — the run was aborted.${lastTool ? ` Last requested tool: "${lastTool}".` : ""}`);
182
+ this.limit = limit;
183
+ this.usage = usage;
184
+ this.lastTool = lastTool;
185
+ this.code = "AGENT_MAX_ITERATIONS";
186
+ }
187
+ }
188
+ class ToolRepeatedFailureError extends AdkError {
189
+ constructor(agent, tool, failures, cause) {
190
+ super(`Tool "${tool}" failed ${failures} consecutive times on agent "${agent}" — the run was aborted.`, {
191
+ cause,
192
+ });
193
+ this.tool = tool;
194
+ this.failures = failures;
195
+ this.code = "TOOL_REPEATED_FAILURE";
196
+ }
197
+ }
162
198
  class McpConnectionError extends AdkError {
163
199
  constructor(server, cause) {
164
200
  super(`MCP server "${server}" connection failed.`, { cause });
@@ -827,6 +863,13 @@ class RunLogger {
827
863
  this.logger.verbose(json(event));
828
864
  }
829
865
  }
866
+ abort(error, usage) {
867
+ this.logger.warn(`run aborted after ${Date.now() - this.startedAt}ms: ${error.message}${usageSuffix(usage)}`);
868
+ }
869
+ toolFailure(tool, count, limit) {
870
+ if (this.enabled("debug"))
871
+ this.logger.debug(`tool "${tool}" failed (${count}/${limit ?? "∞"} consecutive)`);
872
+ }
830
873
  enabled(level) {
831
874
  return LEVEL_WEIGHT[level] <= LEVEL_WEIGHT[this.level];
832
875
  }
@@ -852,9 +895,17 @@ function json(value) {
852
895
  }
853
896
 
854
897
  class DeltaStateBag {
855
- constructor(initial) {
898
+ constructor(initial, guard = {}) {
856
899
  this.initial = initial;
900
+ this.guard = guard;
857
901
  this.changes = new Map();
902
+ const schema = guard.schema;
903
+ if (!schema)
904
+ return;
905
+ for (const key of Object.keys(schema.shape)) {
906
+ if (initial[key] !== undefined)
907
+ this.validate(key, initial[key]);
908
+ }
858
909
  }
859
910
  get(key) {
860
911
  if (this.changes.has(key))
@@ -862,11 +913,28 @@ class DeltaStateBag {
862
913
  return this.initial[key];
863
914
  }
864
915
  set(key, value) {
916
+ this.validate(key, value);
865
917
  this.changes.set(key, value);
866
918
  }
919
+ require(key) {
920
+ const value = this.get(key);
921
+ if (value === undefined)
922
+ throw new AgentStateMissingError(this.guard.agent ?? "unknown", key);
923
+ return value;
924
+ }
867
925
  delta() {
868
926
  return Object.fromEntries(this.changes);
869
927
  }
928
+ validate(key, value) {
929
+ const shape = this.guard.schema?.shape;
930
+ if (!shape || !Object.hasOwn(shape, key))
931
+ return;
932
+ const field = shape[key];
933
+ const result = field.safeParse(value);
934
+ if (!result.success) {
935
+ throw new AgentStateInvalidError(this.guard.agent ?? "unknown", result.error?.issues, key);
936
+ }
937
+ }
870
938
  }
871
939
 
872
940
  const EMPTY_USAGE = { promptTokens: 0, outputTokens: 0, totalTokens: 0 };
@@ -885,23 +953,54 @@ exports.AgentRunner = class AgentRunner {
885
953
  const log = RunLogger.create(this.options.logging, definition.name);
886
954
  log?.start(input);
887
955
  const session = await this.openSession(input);
888
- const state = new DeltaStateBag({ ...(session?.state ?? {}), ...(input.state ?? {}) });
889
- const runtime = { scope: input.sessionId ?? node_crypto.randomUUID(), pendings: [] };
890
- const ctx = this.createToolContext(definition, input, state);
956
+ const state = this.stateBagFor(definition, { ...(session?.state ?? {}), ...(input.state ?? {}) });
957
+ const controller = new AbortController();
958
+ const signal = input.signal ? AbortSignal.any([input.signal, controller.signal]) : controller.signal;
959
+ const runtime = this.createRuntime(definition, input, controller, log);
960
+ const ctx = this.createToolContext(definition, input, state, signal);
891
961
  const resolved = await this.resolveAgent(definition, ctx, runtime);
892
- const engineInput = { ...input, history: session?.events };
962
+ const engineInput = { ...input, signal, history: session?.events };
893
963
  if (session)
894
964
  await this.persist(session.id, "user", "message", { text: input.message });
895
- for await (const event of this.engine.run(resolved, engineInput)) {
896
- log?.event(event);
897
- if (session)
898
- await this.persistAgentEvent(session.id, event);
899
- if (event.type === "final" && definition.output && definition.outputKey) {
900
- const parsed = definition.output.safeParse(tryParseJson(event.text));
901
- if (parsed.success)
902
- state.set(definition.outputKey, parsed.data);
965
+ let iterations = 0;
966
+ let usage = EMPTY_USAGE;
967
+ let lastEventType;
968
+ try {
969
+ for await (const event of this.engine.run(resolved, engineInput)) {
970
+ if (runtime.fatal)
971
+ break;
972
+ if (event.type === "llm_response" && event.usage)
973
+ usage = addUsage(usage, event.usage);
974
+ if (event.type === "tool_call" && lastEventType !== "tool_call") {
975
+ iterations += 1;
976
+ const limit = runtime.limits.maxIterations;
977
+ if (limit !== undefined && iterations > limit) {
978
+ runtime.abort(new AgentMaxIterationsError(definition.name, limit, usage, event.tool));
979
+ break;
980
+ }
981
+ }
982
+ lastEventType = event.type;
983
+ log?.event(event);
984
+ if (session)
985
+ await this.persistAgentEvent(session.id, event);
986
+ if (event.type === "final" && definition.output && definition.outputKey) {
987
+ const parsed = definition.output.safeParse(tryParseJson(event.text));
988
+ if (parsed.success)
989
+ state.set(definition.outputKey, parsed.data);
990
+ }
991
+ yield event;
992
+ }
993
+ }
994
+ catch (error) {
995
+ if (runtime.fatal) {
996
+ log?.abort(runtime.fatal, usage);
997
+ throw runtime.fatal;
903
998
  }
904
- yield event;
999
+ throw error;
1000
+ }
1001
+ if (runtime.fatal) {
1002
+ log?.abort(runtime.fatal, usage);
1003
+ throw runtime.fatal;
905
1004
  }
906
1005
  for (const pending of runtime.pendings) {
907
1006
  const approval = {
@@ -960,7 +1059,7 @@ exports.AgentRunner = class AgentRunner {
960
1059
  const binding = definition.tools.find((candidate) => candidate.options.name === entry.tool);
961
1060
  if (!binding)
962
1061
  throw new ApprovalNotFoundError(params.callId, params.sessionId);
963
- const state = new DeltaStateBag(session.state);
1062
+ const state = this.stateBagFor(definition, session.state);
964
1063
  const ctx = this.createToolContext(definition, { message: "", sessionId: session.id, userId: session.userId }, state);
965
1064
  const result = await this.executeBinding(definition, binding, entry.args, ctx);
966
1065
  await this.persist(session.id, "tool", "tool_result", { callId: entry.callId, tool: entry.tool, result });
@@ -978,11 +1077,9 @@ exports.AgentRunner = class AgentRunner {
978
1077
  }
979
1078
  resolve(agentType, input = { message: "" }) {
980
1079
  const definition = this.definitionOf(agentType);
981
- const state = new DeltaStateBag({ ...(input.state ?? {}) });
982
- return this.resolveAgent(definition, this.createToolContext(definition, input, state), {
983
- scope: input.sessionId ?? node_crypto.randomUUID(),
984
- pendings: [],
985
- });
1080
+ const state = this.stateBagFor(definition, { ...(input.state ?? {}) });
1081
+ const runtime = this.createRuntime(definition, input);
1082
+ return this.resolveAgent(definition, this.createToolContext(definition, input, state), runtime);
986
1083
  }
987
1084
  definitionOf(agentType) {
988
1085
  return typeof agentType === "string" ? this.registry.get(agentType) : this.registry.getByType(agentType);
@@ -1005,17 +1102,42 @@ exports.AgentRunner = class AgentRunner {
1005
1102
  throw new ApprovalNotFoundError(callId, sessionId);
1006
1103
  return { session, entry, rest: pendings.filter((candidate) => candidate.callId !== callId) };
1007
1104
  }
1008
- createToolContext(definition, input, state) {
1105
+ createToolContext(definition, input, state, signal) {
1009
1106
  return {
1010
1107
  agentName: definition.name,
1011
1108
  sessionId: input.sessionId,
1012
1109
  userId: input.userId,
1013
1110
  state,
1014
1111
  attributes: input.attributes ?? {},
1015
- signal: input.signal ?? new AbortController().signal,
1112
+ signal: signal ?? input.signal ?? new AbortController().signal,
1016
1113
  actions: { endRun: () => undefined },
1017
1114
  };
1018
1115
  }
1116
+ stateBagFor(definition, initial) {
1117
+ return new DeltaStateBag(initial, { schema: definition.options?.state, agent: definition.name });
1118
+ }
1119
+ createRuntime(definition, input, controller, log) {
1120
+ const runtime = {
1121
+ scope: input.sessionId ?? node_crypto.randomUUID(),
1122
+ pendings: [],
1123
+ failures: new Map(),
1124
+ limits: this.limitsFor(definition, input),
1125
+ log,
1126
+ abort: (error) => {
1127
+ runtime.fatal ??= error;
1128
+ controller?.abort();
1129
+ },
1130
+ };
1131
+ return runtime;
1132
+ }
1133
+ limitsFor(definition, input) {
1134
+ return {
1135
+ maxIterations: input.maxIterations ?? definition.options?.maxIterations ?? this.options.defaults?.maxIterations,
1136
+ maxConsecutiveToolFailures: input.maxConsecutiveToolFailures ??
1137
+ definition.options?.maxConsecutiveToolFailures ??
1138
+ this.options.defaults?.maxConsecutiveToolFailures,
1139
+ };
1140
+ }
1019
1141
  policyOf(definition) {
1020
1142
  return definition.options?.context ?? this.options.context;
1021
1143
  }
@@ -1044,7 +1166,7 @@ exports.AgentRunner = class AgentRunner {
1044
1166
  note: `Action "${pending.tool}" requires user approval and was NOT executed. Let the user know it is awaiting approval.`,
1045
1167
  };
1046
1168
  }
1047
- const raw = await this.executeBinding(definition, binding, input, ctx);
1169
+ const raw = await this.executeGuarded(definition, binding, input, ctx, runtime);
1048
1170
  if (!offloadEnabled || binding.options.offload === false)
1049
1171
  return raw;
1050
1172
  return this.maybeOffload(raw, binding.options.name ?? "", threshold, runtime.scope);
@@ -1102,6 +1224,26 @@ exports.AgentRunner = class AgentRunner {
1102
1224
  const self = binding.kind === "class" ? binding.instance : definition.instance;
1103
1225
  return requirement.call(self, input, ctx);
1104
1226
  }
1227
+ async executeGuarded(definition, binding, input, ctx, runtime) {
1228
+ const tool = binding.options.name ?? "";
1229
+ try {
1230
+ const raw = await this.executeBinding(definition, binding, input, ctx);
1231
+ runtime.failures.delete(tool);
1232
+ return raw;
1233
+ }
1234
+ catch (error) {
1235
+ const count = (runtime.failures.get(tool) ?? 0) + 1;
1236
+ runtime.failures.set(tool, count);
1237
+ const limit = runtime.limits.maxConsecutiveToolFailures;
1238
+ runtime.log?.toolFailure(tool, count, limit);
1239
+ if (limit !== undefined && count >= limit) {
1240
+ const fatal = new ToolRepeatedFailureError(definition.name, tool, count, error);
1241
+ runtime.abort(fatal);
1242
+ throw fatal;
1243
+ }
1244
+ throw error;
1245
+ }
1246
+ }
1105
1247
  async executeBinding(definition, binding, input, ctx) {
1106
1248
  try {
1107
1249
  if (binding.kind === "class")
@@ -1197,6 +1339,13 @@ exports.AgentRunner = __decorate([
1197
1339
  SessionStore,
1198
1340
  ArtifactStore, Object, ToolsetResolver])
1199
1341
  ], exports.AgentRunner);
1342
+ function addUsage(total, delta) {
1343
+ return {
1344
+ promptTokens: total.promptTokens + delta.promptTokens,
1345
+ outputTokens: total.outputTokens + delta.outputTokens,
1346
+ totalTokens: total.totalTokens + delta.totalTokens,
1347
+ };
1348
+ }
1200
1349
  function tryParseJson(text) {
1201
1350
  try {
1202
1351
  return JSON.parse(text);
@@ -1404,8 +1553,11 @@ exports.AdkTool = AdkTool;
1404
1553
  exports.AdkWorkflow = AdkWorkflow;
1405
1554
  exports.Agent = Agent;
1406
1555
  exports.AgentDefinition = AgentDefinition;
1556
+ exports.AgentMaxIterationsError = AgentMaxIterationsError;
1407
1557
  exports.AgentNotFoundError = AgentNotFoundError;
1408
1558
  exports.AgentRef = AgentRef;
1559
+ exports.AgentStateInvalidError = AgentStateInvalidError;
1560
+ exports.AgentStateMissingError = AgentStateMissingError;
1409
1561
  exports.AiEmptyResponseError = AiEmptyResponseError;
1410
1562
  exports.ApprovalNotFoundError = ApprovalNotFoundError;
1411
1563
  exports.ArtifactStore = ArtifactStore;
@@ -1433,6 +1585,7 @@ exports.Skill = Skill;
1433
1585
  exports.SkillNotFoundError = SkillNotFoundError;
1434
1586
  exports.Tool = Tool;
1435
1587
  exports.ToolExecutionError = ToolExecutionError;
1588
+ exports.ToolRepeatedFailureError = ToolRepeatedFailureError;
1436
1589
  exports.ToolsetResolver = ToolsetResolver;
1437
1590
  exports.UnregisteredPromptError = UnregisteredPromptError;
1438
1591
  exports.UnregisteredSkillError = UnregisteredSkillError;
package/dist/index.d.ts CHANGED
@@ -27,6 +27,7 @@ export { AgentRunner } from "./lib/runner/agent-runner";
27
27
  export { RunLogger } from "./lib/runner/run-logger";
28
28
  export type { LoggingOption } from "./lib/runner/run-logger";
29
29
  export { DeltaStateBag } from "./lib/runner/state-bag";
30
+ export type { StateGuard } from "./lib/runner/state-bag";
30
31
  export { buildInstruction, skillContent } from "./lib/runner/instruction-builder";
31
32
  export { Gemini, ModelRouter, OpenAiLike, isModelSpec } from "./lib/models/model-specs";
32
33
  export { DEFAULT_OFFLOAD_THRESHOLD, contextPolicy } from "./lib/models/context-policy";
@@ -40,7 +41,7 @@ export type { SkillBinding, ToolBinding } from "./lib/registry/agent-definition"
40
41
  export { AgentRef } from "./lib/registry/agent-ref";
41
42
  export { AgentRegistry } from "./lib/registry/agent-registry";
42
43
  export { AdkBootError, AdkError, DuplicateAgentNameError, ConflictingPromptError, InvalidWorkflowError, MissingModelError, ReservedMethodError, UnregisteredPromptError, UnregisteredSkillError, UnregisteredSubAgentError, UnregisteredToolError, UnresolvedToolsetError, } from "./lib/errors";
43
- export { AgentNotFoundError, AiEmptyResponseError, ApprovalNotFoundError, EmbedderNotConfiguredError, McpConnectionError, ModelsExhaustedError, OutputValidationError, SessionNotFoundError, SkillNotFoundError, ToolExecutionError, } from "./lib/errors/runtime.errors";
44
+ export { AgentMaxIterationsError, AgentNotFoundError, AgentStateInvalidError, AgentStateMissingError, AiEmptyResponseError, ApprovalNotFoundError, EmbedderNotConfiguredError, McpConnectionError, ModelsExhaustedError, OutputValidationError, SessionNotFoundError, SkillNotFoundError, ToolExecutionError, ToolRepeatedFailureError, } from "./lib/errors/runtime.errors";
44
45
  export type { AgentEvent, ArtifactPart, ArtifactRef, PendingApproval, RawRef, RunInput, RunResult, Session, SessionEvent, SessionInit, TokenUsage, } from "./lib/types/events";
45
46
  export type { AgentOptions, AnyZodObject, ModelInput, SkillMode, SkillOptions, ToolOptions, WorkflowMode, WorkflowOptions, } from "./lib/types/options";
46
47
  export type { StateBag, ToolContext } from "./lib/types/tool-context";
package/dist/index.mjs CHANGED
@@ -157,6 +157,42 @@ class OutputValidationError extends AdkError {
157
157
  this.code = "OUTPUT_VALIDATION_FAILED";
158
158
  }
159
159
  }
160
+ class AgentStateInvalidError extends AdkError {
161
+ constructor(agent, issues, key) {
162
+ super(key
163
+ ? `Agent "${agent}" received a value for state key "${key}" that does not match its declared state schema.`
164
+ : `Agent "${agent}" received state that does not match its declared state schema.`);
165
+ this.issues = issues;
166
+ this.key = key;
167
+ this.code = "AGENT_STATE_INVALID";
168
+ }
169
+ }
170
+ class AgentStateMissingError extends AdkError {
171
+ constructor(agent, key) {
172
+ super(`Agent "${agent}" requires state key "${key}" but it is absent.`);
173
+ this.key = key;
174
+ this.code = "AGENT_STATE_MISSING";
175
+ }
176
+ }
177
+ class AgentMaxIterationsError extends AdkError {
178
+ constructor(agent, limit, usage, lastTool) {
179
+ super(`Agent "${agent}" exceeded maxIterations (${limit}) — the run was aborted.${lastTool ? ` Last requested tool: "${lastTool}".` : ""}`);
180
+ this.limit = limit;
181
+ this.usage = usage;
182
+ this.lastTool = lastTool;
183
+ this.code = "AGENT_MAX_ITERATIONS";
184
+ }
185
+ }
186
+ class ToolRepeatedFailureError extends AdkError {
187
+ constructor(agent, tool, failures, cause) {
188
+ super(`Tool "${tool}" failed ${failures} consecutive times on agent "${agent}" — the run was aborted.`, {
189
+ cause,
190
+ });
191
+ this.tool = tool;
192
+ this.failures = failures;
193
+ this.code = "TOOL_REPEATED_FAILURE";
194
+ }
195
+ }
160
196
  class McpConnectionError extends AdkError {
161
197
  constructor(server, cause) {
162
198
  super(`MCP server "${server}" connection failed.`, { cause });
@@ -825,6 +861,13 @@ class RunLogger {
825
861
  this.logger.verbose(json(event));
826
862
  }
827
863
  }
864
+ abort(error, usage) {
865
+ this.logger.warn(`run aborted after ${Date.now() - this.startedAt}ms: ${error.message}${usageSuffix(usage)}`);
866
+ }
867
+ toolFailure(tool, count, limit) {
868
+ if (this.enabled("debug"))
869
+ this.logger.debug(`tool "${tool}" failed (${count}/${limit ?? "∞"} consecutive)`);
870
+ }
828
871
  enabled(level) {
829
872
  return LEVEL_WEIGHT[level] <= LEVEL_WEIGHT[this.level];
830
873
  }
@@ -850,9 +893,17 @@ function json(value) {
850
893
  }
851
894
 
852
895
  class DeltaStateBag {
853
- constructor(initial) {
896
+ constructor(initial, guard = {}) {
854
897
  this.initial = initial;
898
+ this.guard = guard;
855
899
  this.changes = new Map();
900
+ const schema = guard.schema;
901
+ if (!schema)
902
+ return;
903
+ for (const key of Object.keys(schema.shape)) {
904
+ if (initial[key] !== undefined)
905
+ this.validate(key, initial[key]);
906
+ }
856
907
  }
857
908
  get(key) {
858
909
  if (this.changes.has(key))
@@ -860,11 +911,28 @@ class DeltaStateBag {
860
911
  return this.initial[key];
861
912
  }
862
913
  set(key, value) {
914
+ this.validate(key, value);
863
915
  this.changes.set(key, value);
864
916
  }
917
+ require(key) {
918
+ const value = this.get(key);
919
+ if (value === undefined)
920
+ throw new AgentStateMissingError(this.guard.agent ?? "unknown", key);
921
+ return value;
922
+ }
865
923
  delta() {
866
924
  return Object.fromEntries(this.changes);
867
925
  }
926
+ validate(key, value) {
927
+ const shape = this.guard.schema?.shape;
928
+ if (!shape || !Object.hasOwn(shape, key))
929
+ return;
930
+ const field = shape[key];
931
+ const result = field.safeParse(value);
932
+ if (!result.success) {
933
+ throw new AgentStateInvalidError(this.guard.agent ?? "unknown", result.error?.issues, key);
934
+ }
935
+ }
868
936
  }
869
937
 
870
938
  const EMPTY_USAGE = { promptTokens: 0, outputTokens: 0, totalTokens: 0 };
@@ -883,23 +951,54 @@ let AgentRunner = class AgentRunner {
883
951
  const log = RunLogger.create(this.options.logging, definition.name);
884
952
  log?.start(input);
885
953
  const session = await this.openSession(input);
886
- const state = new DeltaStateBag({ ...(session?.state ?? {}), ...(input.state ?? {}) });
887
- const runtime = { scope: input.sessionId ?? randomUUID(), pendings: [] };
888
- const ctx = this.createToolContext(definition, input, state);
954
+ const state = this.stateBagFor(definition, { ...(session?.state ?? {}), ...(input.state ?? {}) });
955
+ const controller = new AbortController();
956
+ const signal = input.signal ? AbortSignal.any([input.signal, controller.signal]) : controller.signal;
957
+ const runtime = this.createRuntime(definition, input, controller, log);
958
+ const ctx = this.createToolContext(definition, input, state, signal);
889
959
  const resolved = await this.resolveAgent(definition, ctx, runtime);
890
- const engineInput = { ...input, history: session?.events };
960
+ const engineInput = { ...input, signal, history: session?.events };
891
961
  if (session)
892
962
  await this.persist(session.id, "user", "message", { text: input.message });
893
- for await (const event of this.engine.run(resolved, engineInput)) {
894
- log?.event(event);
895
- if (session)
896
- await this.persistAgentEvent(session.id, event);
897
- if (event.type === "final" && definition.output && definition.outputKey) {
898
- const parsed = definition.output.safeParse(tryParseJson(event.text));
899
- if (parsed.success)
900
- state.set(definition.outputKey, parsed.data);
963
+ let iterations = 0;
964
+ let usage = EMPTY_USAGE;
965
+ let lastEventType;
966
+ try {
967
+ for await (const event of this.engine.run(resolved, engineInput)) {
968
+ if (runtime.fatal)
969
+ break;
970
+ if (event.type === "llm_response" && event.usage)
971
+ usage = addUsage(usage, event.usage);
972
+ if (event.type === "tool_call" && lastEventType !== "tool_call") {
973
+ iterations += 1;
974
+ const limit = runtime.limits.maxIterations;
975
+ if (limit !== undefined && iterations > limit) {
976
+ runtime.abort(new AgentMaxIterationsError(definition.name, limit, usage, event.tool));
977
+ break;
978
+ }
979
+ }
980
+ lastEventType = event.type;
981
+ log?.event(event);
982
+ if (session)
983
+ await this.persistAgentEvent(session.id, event);
984
+ if (event.type === "final" && definition.output && definition.outputKey) {
985
+ const parsed = definition.output.safeParse(tryParseJson(event.text));
986
+ if (parsed.success)
987
+ state.set(definition.outputKey, parsed.data);
988
+ }
989
+ yield event;
990
+ }
991
+ }
992
+ catch (error) {
993
+ if (runtime.fatal) {
994
+ log?.abort(runtime.fatal, usage);
995
+ throw runtime.fatal;
901
996
  }
902
- yield event;
997
+ throw error;
998
+ }
999
+ if (runtime.fatal) {
1000
+ log?.abort(runtime.fatal, usage);
1001
+ throw runtime.fatal;
903
1002
  }
904
1003
  for (const pending of runtime.pendings) {
905
1004
  const approval = {
@@ -958,7 +1057,7 @@ let AgentRunner = class AgentRunner {
958
1057
  const binding = definition.tools.find((candidate) => candidate.options.name === entry.tool);
959
1058
  if (!binding)
960
1059
  throw new ApprovalNotFoundError(params.callId, params.sessionId);
961
- const state = new DeltaStateBag(session.state);
1060
+ const state = this.stateBagFor(definition, session.state);
962
1061
  const ctx = this.createToolContext(definition, { message: "", sessionId: session.id, userId: session.userId }, state);
963
1062
  const result = await this.executeBinding(definition, binding, entry.args, ctx);
964
1063
  await this.persist(session.id, "tool", "tool_result", { callId: entry.callId, tool: entry.tool, result });
@@ -976,11 +1075,9 @@ let AgentRunner = class AgentRunner {
976
1075
  }
977
1076
  resolve(agentType, input = { message: "" }) {
978
1077
  const definition = this.definitionOf(agentType);
979
- const state = new DeltaStateBag({ ...(input.state ?? {}) });
980
- return this.resolveAgent(definition, this.createToolContext(definition, input, state), {
981
- scope: input.sessionId ?? randomUUID(),
982
- pendings: [],
983
- });
1078
+ const state = this.stateBagFor(definition, { ...(input.state ?? {}) });
1079
+ const runtime = this.createRuntime(definition, input);
1080
+ return this.resolveAgent(definition, this.createToolContext(definition, input, state), runtime);
984
1081
  }
985
1082
  definitionOf(agentType) {
986
1083
  return typeof agentType === "string" ? this.registry.get(agentType) : this.registry.getByType(agentType);
@@ -1003,17 +1100,42 @@ let AgentRunner = class AgentRunner {
1003
1100
  throw new ApprovalNotFoundError(callId, sessionId);
1004
1101
  return { session, entry, rest: pendings.filter((candidate) => candidate.callId !== callId) };
1005
1102
  }
1006
- createToolContext(definition, input, state) {
1103
+ createToolContext(definition, input, state, signal) {
1007
1104
  return {
1008
1105
  agentName: definition.name,
1009
1106
  sessionId: input.sessionId,
1010
1107
  userId: input.userId,
1011
1108
  state,
1012
1109
  attributes: input.attributes ?? {},
1013
- signal: input.signal ?? new AbortController().signal,
1110
+ signal: signal ?? input.signal ?? new AbortController().signal,
1014
1111
  actions: { endRun: () => undefined },
1015
1112
  };
1016
1113
  }
1114
+ stateBagFor(definition, initial) {
1115
+ return new DeltaStateBag(initial, { schema: definition.options?.state, agent: definition.name });
1116
+ }
1117
+ createRuntime(definition, input, controller, log) {
1118
+ const runtime = {
1119
+ scope: input.sessionId ?? randomUUID(),
1120
+ pendings: [],
1121
+ failures: new Map(),
1122
+ limits: this.limitsFor(definition, input),
1123
+ log,
1124
+ abort: (error) => {
1125
+ runtime.fatal ??= error;
1126
+ controller?.abort();
1127
+ },
1128
+ };
1129
+ return runtime;
1130
+ }
1131
+ limitsFor(definition, input) {
1132
+ return {
1133
+ maxIterations: input.maxIterations ?? definition.options?.maxIterations ?? this.options.defaults?.maxIterations,
1134
+ maxConsecutiveToolFailures: input.maxConsecutiveToolFailures ??
1135
+ definition.options?.maxConsecutiveToolFailures ??
1136
+ this.options.defaults?.maxConsecutiveToolFailures,
1137
+ };
1138
+ }
1017
1139
  policyOf(definition) {
1018
1140
  return definition.options?.context ?? this.options.context;
1019
1141
  }
@@ -1042,7 +1164,7 @@ let AgentRunner = class AgentRunner {
1042
1164
  note: `Action "${pending.tool}" requires user approval and was NOT executed. Let the user know it is awaiting approval.`,
1043
1165
  };
1044
1166
  }
1045
- const raw = await this.executeBinding(definition, binding, input, ctx);
1167
+ const raw = await this.executeGuarded(definition, binding, input, ctx, runtime);
1046
1168
  if (!offloadEnabled || binding.options.offload === false)
1047
1169
  return raw;
1048
1170
  return this.maybeOffload(raw, binding.options.name ?? "", threshold, runtime.scope);
@@ -1100,6 +1222,26 @@ let AgentRunner = class AgentRunner {
1100
1222
  const self = binding.kind === "class" ? binding.instance : definition.instance;
1101
1223
  return requirement.call(self, input, ctx);
1102
1224
  }
1225
+ async executeGuarded(definition, binding, input, ctx, runtime) {
1226
+ const tool = binding.options.name ?? "";
1227
+ try {
1228
+ const raw = await this.executeBinding(definition, binding, input, ctx);
1229
+ runtime.failures.delete(tool);
1230
+ return raw;
1231
+ }
1232
+ catch (error) {
1233
+ const count = (runtime.failures.get(tool) ?? 0) + 1;
1234
+ runtime.failures.set(tool, count);
1235
+ const limit = runtime.limits.maxConsecutiveToolFailures;
1236
+ runtime.log?.toolFailure(tool, count, limit);
1237
+ if (limit !== undefined && count >= limit) {
1238
+ const fatal = new ToolRepeatedFailureError(definition.name, tool, count, error);
1239
+ runtime.abort(fatal);
1240
+ throw fatal;
1241
+ }
1242
+ throw error;
1243
+ }
1244
+ }
1103
1245
  async executeBinding(definition, binding, input, ctx) {
1104
1246
  try {
1105
1247
  if (binding.kind === "class")
@@ -1195,6 +1337,13 @@ AgentRunner = __decorate([
1195
1337
  SessionStore,
1196
1338
  ArtifactStore, Object, ToolsetResolver])
1197
1339
  ], AgentRunner);
1340
+ function addUsage(total, delta) {
1341
+ return {
1342
+ promptTokens: total.promptTokens + delta.promptTokens,
1343
+ outputTokens: total.outputTokens + delta.outputTokens,
1344
+ totalTokens: total.totalTokens + delta.totalTokens,
1345
+ };
1346
+ }
1198
1347
  function tryParseJson(text) {
1199
1348
  try {
1200
1349
  return JSON.parse(text);
@@ -1391,4 +1540,4 @@ function isScriptedModel(model) {
1391
1540
  return typeof model === "object" && model !== null && "__adkScriptedModel" in model;
1392
1541
  }
1393
1542
 
1394
- export { ADK_OPTIONS, AdkAgent, AdkBootError, AdkEngine, AdkError, AdkModule, AdkPrompt, AdkSkill, AdkTool, AdkWorkflow, Agent, AgentDefinition, AgentNotFoundError, AgentRef, AgentRegistry, AgentRunner, AgentSessions, AiEmptyResponseError, ApprovalNotFoundError, ArtifactStore, ConflictingPromptError, DEFAULT_OFFLOAD_THRESHOLD, DeltaStateBag, DuplicateAgentNameError, Embedder, EmbedderNotConfiguredError, Gemini, InMemoryArtifactStore, InMemorySessionStore, InvalidWorkflowError, McpConnectionError, MemoryStore, MissingModelError, ModelRouter, ModelsExhaustedError, OpenAiLike, OutputValidationError, PromptFiles, ReservedMethodError, RunLogger, ScriptedEngine, ScriptedModel, SessionNotFoundError, SessionStore, Similarity, Skill, SkillNotFoundError, Tool, ToolExecutionError, ToolsetResolver, UnregisteredPromptError, UnregisteredSkillError, UnregisteredSubAgentError, UnregisteredToolError, UnresolvedToolsetError, WorkflowAgent, buildInstruction, callTool, contextPolicy, fail, isModelSpec, isScriptedModel, isToolsetRef, skillContent, text, toolset };
1543
+ export { ADK_OPTIONS, AdkAgent, AdkBootError, AdkEngine, AdkError, AdkModule, AdkPrompt, AdkSkill, AdkTool, AdkWorkflow, Agent, AgentDefinition, AgentMaxIterationsError, AgentNotFoundError, AgentRef, AgentRegistry, AgentRunner, AgentSessions, AgentStateInvalidError, AgentStateMissingError, AiEmptyResponseError, ApprovalNotFoundError, ArtifactStore, ConflictingPromptError, DEFAULT_OFFLOAD_THRESHOLD, DeltaStateBag, DuplicateAgentNameError, Embedder, EmbedderNotConfiguredError, Gemini, InMemoryArtifactStore, InMemorySessionStore, InvalidWorkflowError, McpConnectionError, MemoryStore, MissingModelError, ModelRouter, ModelsExhaustedError, OpenAiLike, OutputValidationError, PromptFiles, ReservedMethodError, RunLogger, ScriptedEngine, ScriptedModel, SessionNotFoundError, SessionStore, Similarity, Skill, SkillNotFoundError, Tool, ToolExecutionError, ToolRepeatedFailureError, ToolsetResolver, UnregisteredPromptError, UnregisteredSkillError, UnregisteredSubAgentError, UnregisteredToolError, UnresolvedToolsetError, WorkflowAgent, buildInstruction, callTool, contextPolicy, fail, isModelSpec, isScriptedModel, isToolsetRef, skillContent, text, toolset };
@@ -1,3 +1,3 @@
1
1
  export { AdkError } from "./adk.error";
2
2
  export { AdkBootError, DuplicateAgentNameError, ConflictingPromptError, InvalidWorkflowError, MissingModelError, ReservedMethodError, UnregisteredPromptError, UnregisteredSkillError, UnregisteredSubAgentError, UnregisteredToolError, UnresolvedToolsetError, } from "./boot.errors";
3
- export { AgentNotFoundError, ApprovalNotFoundError, EmbedderNotConfiguredError, AiEmptyResponseError, McpConnectionError, ModelsExhaustedError, OutputValidationError, SessionNotFoundError, SkillNotFoundError, ToolExecutionError, } from "./runtime.errors";
3
+ export { AgentMaxIterationsError, AgentNotFoundError, AgentStateInvalidError, AgentStateMissingError, ApprovalNotFoundError, EmbedderNotConfiguredError, AiEmptyResponseError, McpConnectionError, ModelsExhaustedError, OutputValidationError, SessionNotFoundError, SkillNotFoundError, ToolExecutionError, ToolRepeatedFailureError, } from "./runtime.errors";
@@ -1,3 +1,4 @@
1
+ import type { TokenUsage } from "../types/events";
1
2
  import { AdkError } from "./adk.error";
2
3
  export declare class AiEmptyResponseError extends AdkError {
3
4
  readonly code = "AI_EMPTY_RESPONSE";
@@ -41,6 +42,30 @@ export declare class OutputValidationError extends AdkError {
41
42
  readonly code = "OUTPUT_VALIDATION_FAILED";
42
43
  constructor(agent: string, rawOutput: unknown, issues: unknown);
43
44
  }
45
+ export declare class AgentStateInvalidError extends AdkError {
46
+ readonly issues: unknown;
47
+ readonly key?: string | undefined;
48
+ readonly code = "AGENT_STATE_INVALID";
49
+ constructor(agent: string, issues: unknown, key?: string | undefined);
50
+ }
51
+ export declare class AgentStateMissingError extends AdkError {
52
+ readonly key: string;
53
+ readonly code = "AGENT_STATE_MISSING";
54
+ constructor(agent: string, key: string);
55
+ }
56
+ export declare class AgentMaxIterationsError extends AdkError {
57
+ readonly limit: number;
58
+ readonly usage: TokenUsage;
59
+ readonly lastTool?: string | undefined;
60
+ readonly code = "AGENT_MAX_ITERATIONS";
61
+ constructor(agent: string, limit: number, usage: TokenUsage, lastTool?: string | undefined);
62
+ }
63
+ export declare class ToolRepeatedFailureError extends AdkError {
64
+ readonly tool: string;
65
+ readonly failures: number;
66
+ readonly code = "TOOL_REPEATED_FAILURE";
67
+ constructor(agent: string, tool: string, failures: number, cause: unknown);
68
+ }
44
69
  export declare class McpConnectionError extends AdkError {
45
70
  readonly code = "MCP_CONNECTION_FAILED";
46
71
  constructor(server: string, cause: unknown);
@@ -16,6 +16,10 @@ export interface AdkModuleOptions {
16
16
  };
17
17
  logging?: import("../runner/run-logger").LoggingOption;
18
18
  context?: import("../models/context-policy").ContextPolicy;
19
+ defaults?: {
20
+ maxIterations?: number;
21
+ maxConsecutiveToolFailures?: number;
22
+ };
19
23
  }
20
24
  export interface AdkModuleAsyncOptions extends Pick<ModuleMetadata, "imports"> {
21
25
  engine: Type<AdkEngine>;
@@ -32,9 +32,13 @@ export declare class AgentRunner {
32
32
  private openSession;
33
33
  private takePending;
34
34
  private createToolContext;
35
+ private stateBagFor;
36
+ private createRuntime;
37
+ private limitsFor;
35
38
  private policyOf;
36
39
  private resolveAgent;
37
40
  private needsApproval;
41
+ private executeGuarded;
38
42
  private executeBinding;
39
43
  private maybeOffload;
40
44
  private createReadArtifactTool;
@@ -1,4 +1,4 @@
1
- import type { AgentEvent, RunInput } from "../types/events";
1
+ import type { AgentEvent, RunInput, TokenUsage } from "../types/events";
2
2
  export type LoggingOption = boolean | "info" | "debug" | "verbose" | undefined;
3
3
  export declare class RunLogger {
4
4
  private readonly level;
@@ -8,6 +8,8 @@ export declare class RunLogger {
8
8
  private constructor();
9
9
  start(input: RunInput): void;
10
10
  event(event: AgentEvent): void;
11
+ abort(error: Error, usage: TokenUsage): void;
12
+ toolFailure(tool: string, count: number, limit: number | undefined): void;
11
13
  private enabled;
12
14
  private preview;
13
15
  }
@@ -1,9 +1,17 @@
1
+ import type { AnyZodObject } from "../types/options";
1
2
  import type { StateBag } from "../types/tool-context";
3
+ export interface StateGuard {
4
+ schema?: AnyZodObject;
5
+ agent?: string;
6
+ }
2
7
  export declare class DeltaStateBag implements StateBag {
3
8
  private readonly initial;
9
+ private readonly guard;
4
10
  private readonly changes;
5
- constructor(initial: Record<string, unknown>);
11
+ constructor(initial: Record<string, unknown>, guard?: StateGuard);
6
12
  get<T = unknown>(key: string): T | undefined;
7
13
  set(key: string, value: unknown): void;
14
+ require<T = unknown>(key: string): T;
8
15
  delta(): Record<string, unknown>;
16
+ private validate;
9
17
  }
@@ -66,6 +66,8 @@ export interface RunInput {
66
66
  attributes?: Record<string, unknown>;
67
67
  labels?: Record<string, string>;
68
68
  signal?: AbortSignal;
69
+ maxIterations?: number;
70
+ maxConsecutiveToolFailures?: number;
69
71
  history?: SessionEvent[];
70
72
  }
71
73
  export interface PendingApproval {
@@ -20,6 +20,9 @@ export interface AgentOptions {
20
20
  output?: ZodObject<ZodRawShape>;
21
21
  outputKey?: string;
22
22
  context?: import("../models/context-policy").ContextPolicy;
23
+ state?: AnyZodObject;
24
+ maxIterations?: number;
25
+ maxConsecutiveToolFailures?: number;
23
26
  }
24
27
  export interface ToolOptions<S extends AnyZodObject = AnyZodObject> {
25
28
  name?: string;
@@ -1,15 +1,18 @@
1
- export interface ToolContext {
1
+ export interface ToolContext<TState extends Record<string, unknown> = Record<string, unknown>> {
2
2
  agentName: string;
3
3
  sessionId?: string;
4
4
  userId?: string;
5
- state: StateBag;
5
+ state: StateBag<TState>;
6
6
  attributes: Record<string, unknown>;
7
7
  signal: AbortSignal;
8
8
  actions: {
9
9
  endRun(): void;
10
10
  };
11
11
  }
12
- export interface StateBag {
12
+ export interface StateBag<TState extends Record<string, unknown> = Record<string, unknown>> {
13
+ get<K extends keyof TState & string, V extends TState[K]>(key: K): V | undefined;
13
14
  get<T = unknown>(key: string): T | undefined;
14
- set(key: string, value: unknown): void;
15
+ set<K extends keyof TState & string>(key: K, value: TState[K]): void;
16
+ require<K extends keyof TState & string, V extends TState[K]>(key: K): V;
17
+ require<T = unknown>(key: string): T;
15
18
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestjs-adk/core",
3
- "version": "0.0.2",
3
+ "version": "1.0.0",
4
4
  "description": "NestJS-native agent framework: decorators, DI and abstract contracts over pluggable AI agent engines (Google ADK first).",
5
5
  "license": "MIT",
6
6
  "sideEffects": false,
@@ -31,5 +31,14 @@
31
31
  },
32
32
  "publishConfig": {
33
33
  "access": "public"
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/gabrieljsilva/nestjs-adk.git",
38
+ "directory": "packages/core"
39
+ },
40
+ "homepage": "https://github.com/gabrieljsilva/nestjs-adk#readme",
41
+ "bugs": {
42
+ "url": "https://github.com/gabrieljsilva/nestjs-adk/issues"
34
43
  }
35
44
  }