@adonis-agora/agent 0.25.2 → 0.25.4

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.
Files changed (181) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/assets/spa/assets/index-C1YrXiT7.js +9 -0
  3. package/dist/assets/spa/assets/index-DVnnBvfn.css +1 -0
  4. package/dist/assets/spa/index.html +2 -2
  5. package/dist/providers/agent_provider.d.ts.map +1 -1
  6. package/dist/providers/agent_provider.js +1 -1
  7. package/dist/providers/agent_provider.js.map +1 -1
  8. package/dist/providers/dashboard_provider.d.ts.map +1 -1
  9. package/dist/providers/dashboard_provider.js.map +1 -1
  10. package/dist/providers/mcp_provider.d.ts.map +1 -1
  11. package/dist/providers/mcp_provider.js.map +1 -1
  12. package/dist/src/actor-resolver.d.ts.map +1 -1
  13. package/dist/src/actor-resolver.js.map +1 -1
  14. package/dist/src/agent-deps-factory.d.ts.map +1 -1
  15. package/dist/src/agent-deps-factory.js.map +1 -1
  16. package/dist/src/agent-loop.d.ts.map +1 -1
  17. package/dist/src/agent-loop.js.map +1 -1
  18. package/dist/src/agent-registry.d.ts.map +1 -1
  19. package/dist/src/agent-service.d.ts.map +1 -1
  20. package/dist/src/agent-service.js.map +1 -1
  21. package/dist/src/ai-sdk/ai-sdk-model.d.ts.map +1 -1
  22. package/dist/src/ai-sdk/ai-sdk-model.js.map +1 -1
  23. package/dist/src/ai-sdk/attachment-fetch-downloader.js.map +1 -1
  24. package/dist/src/ai-sdk/index.d.ts +1 -1
  25. package/dist/src/ai-sdk/index.d.ts.map +1 -1
  26. package/dist/src/ai-sdk/index.js.map +1 -1
  27. package/dist/src/ai-tool-ref.d.ts.map +1 -1
  28. package/dist/src/authorizer.d.ts.map +1 -1
  29. package/dist/src/authorizer.js.map +1 -1
  30. package/dist/src/authz/authz-actor-resolver.d.ts.map +1 -1
  31. package/dist/src/authz/authz-tool-authorizer.d.ts.map +1 -1
  32. package/dist/src/authz/index.d.ts +3 -3
  33. package/dist/src/authz/index.d.ts.map +1 -1
  34. package/dist/src/authz/index.js +1 -1
  35. package/dist/src/authz/index.js.map +1 -1
  36. package/dist/src/base-tool.d.ts.map +1 -1
  37. package/dist/src/base-tool.js.map +1 -1
  38. package/dist/src/client/chat-client.d.ts.map +1 -1
  39. package/dist/src/client/chat-client.js.map +1 -1
  40. package/dist/src/client/index.d.ts +2 -2
  41. package/dist/src/client/index.d.ts.map +1 -1
  42. package/dist/src/client/index.js +1 -1
  43. package/dist/src/client/index.js.map +1 -1
  44. package/dist/src/dashboard/index.d.ts +1 -1
  45. package/dist/src/dashboard/index.d.ts.map +1 -1
  46. package/dist/src/dashboard/index.js.map +1 -1
  47. package/dist/src/data/index.d.ts +5 -5
  48. package/dist/src/data/index.d.ts.map +1 -1
  49. package/dist/src/data/index.js +3 -3
  50. package/dist/src/data/index.js.map +1 -1
  51. package/dist/src/data/sql-validator.d.ts.map +1 -1
  52. package/dist/src/data/sql-validator.js.map +1 -1
  53. package/dist/src/data/table-access.d.ts.map +1 -1
  54. package/dist/src/data/tenant-scope.d.ts.map +1 -1
  55. package/dist/src/data/tenant-scope.js.map +1 -1
  56. package/dist/src/define_config.d.ts +3 -3
  57. package/dist/src/define_config.d.ts.map +1 -1
  58. package/dist/src/define_config.js +1 -1
  59. package/dist/src/define_config.js.map +1 -1
  60. package/dist/src/durable/agent-run-workflow.d.ts.map +1 -1
  61. package/dist/src/durable/agent-run-workflow.js.map +1 -1
  62. package/dist/src/durable/durable-agent-runner.d.ts.map +1 -1
  63. package/dist/src/durable/durable-agent-runner.js.map +1 -1
  64. package/dist/src/durable/index.d.ts +2 -2
  65. package/dist/src/durable/index.d.ts.map +1 -1
  66. package/dist/src/durable/index.js +2 -2
  67. package/dist/src/durable/index.js.map +1 -1
  68. package/dist/src/hooks/tools.d.ts.map +1 -1
  69. package/dist/src/hooks/tools.js.map +1 -1
  70. package/dist/src/in-process-sink.d.ts.map +1 -1
  71. package/dist/src/index.d.ts +57 -57
  72. package/dist/src/index.d.ts.map +1 -1
  73. package/dist/src/index.js +46 -46
  74. package/dist/src/index.js.map +1 -1
  75. package/dist/src/mcp/auth.js.map +1 -1
  76. package/dist/src/mcp/define_config.d.ts +3 -3
  77. package/dist/src/mcp/define_config.d.ts.map +1 -1
  78. package/dist/src/mcp/define_config.js +1 -1
  79. package/dist/src/mcp/define_config.js.map +1 -1
  80. package/dist/src/mcp/index.d.ts +4 -4
  81. package/dist/src/mcp/index.d.ts.map +1 -1
  82. package/dist/src/mcp/index.js +2 -2
  83. package/dist/src/mcp/index.js.map +1 -1
  84. package/dist/src/mcp/server.js +1 -1
  85. package/dist/src/mcp/server.js.map +1 -1
  86. package/dist/src/rag/chunk.js.map +1 -1
  87. package/dist/src/rag/embedding-retriever.d.ts.map +1 -1
  88. package/dist/src/rag/embedding-retriever.js.map +1 -1
  89. package/dist/src/rag/hybrid-retriever.d.ts.map +1 -1
  90. package/dist/src/rag/hybrid-retriever.js.map +1 -1
  91. package/dist/src/rag/index.d.ts +8 -8
  92. package/dist/src/rag/index.d.ts.map +1 -1
  93. package/dist/src/rag/index.js +6 -6
  94. package/dist/src/rag/index.js.map +1 -1
  95. package/dist/src/rag/ingest.js.map +1 -1
  96. package/dist/src/rag/keyword-retriever.d.ts.map +1 -1
  97. package/dist/src/rag/keyword-retriever.js.map +1 -1
  98. package/dist/src/rag/memory-vector-store.d.ts.map +1 -1
  99. package/dist/src/rag/memory-vector-store.js.map +1 -1
  100. package/dist/src/rag/pg-vector-store.d.ts.map +1 -1
  101. package/dist/src/rag/pg-vector-store.js.map +1 -1
  102. package/dist/src/rag/qdrant-store.d.ts.map +1 -1
  103. package/dist/src/rag/qdrant-store.js +3 -3
  104. package/dist/src/rag/qdrant-store.js.map +1 -1
  105. package/dist/src/rag/reranking-retriever.d.ts.map +1 -1
  106. package/dist/src/rag/reranking-retriever.js.map +1 -1
  107. package/dist/src/rag/vector-store.d.ts.map +1 -1
  108. package/dist/src/rag/vector-store.js.map +1 -1
  109. package/dist/src/rag-media/index.d.ts +4 -4
  110. package/dist/src/rag-media/index.d.ts.map +1 -1
  111. package/dist/src/rag-media/index.js +4 -4
  112. package/dist/src/rag-media/index.js.map +1 -1
  113. package/dist/src/rag-media/media-rag-ingestion.d.ts.map +1 -1
  114. package/dist/src/rag-media/media-rag-ingestion.js +2 -2
  115. package/dist/src/rag-media/media-rag-ingestion.js.map +1 -1
  116. package/dist/src/rag-media/text-extractor.d.ts.map +1 -1
  117. package/dist/src/rag-media/text-extractor.js.map +1 -1
  118. package/dist/src/react/use-agent-chat.js.map +1 -1
  119. package/dist/src/redis-token-stream-sink.d.ts.map +1 -1
  120. package/dist/src/redis-token-stream-sink.js.map +1 -1
  121. package/dist/src/runners/inline-agent-runner.d.ts.map +1 -1
  122. package/dist/src/runners/inline-agent-runner.js.map +1 -1
  123. package/dist/src/stores/factory.d.ts.map +1 -1
  124. package/dist/src/stores/factory.js.map +1 -1
  125. package/dist/src/stores/ledger-quota.d.ts.map +1 -1
  126. package/dist/src/stores/ledger-quota.js.map +1 -1
  127. package/dist/src/stores/lucid-governance-queries.d.ts.map +1 -1
  128. package/dist/src/stores/lucid-governance-queries.js.map +1 -1
  129. package/dist/src/stores/lucid-pricing.d.ts.map +1 -1
  130. package/dist/src/stores/lucid-pricing.js.map +1 -1
  131. package/dist/src/stores/lucid-schema.d.ts +6 -6
  132. package/dist/src/stores/lucid-schema.d.ts.map +1 -1
  133. package/dist/src/stores/lucid.d.ts.map +1 -1
  134. package/dist/src/stores/lucid.js.map +1 -1
  135. package/dist/src/telescope/dashboard.js.map +1 -1
  136. package/dist/src/telescope/data-providers.d.ts.map +1 -1
  137. package/dist/src/telescope/extension.js +1 -1
  138. package/dist/src/telescope/extension.js.map +1 -1
  139. package/dist/src/telescope/index.d.ts +3 -3
  140. package/dist/src/telescope/index.d.ts.map +1 -1
  141. package/dist/src/telescope/index.js +2 -2
  142. package/dist/src/telescope/index.js.map +1 -1
  143. package/dist/src/telescope/rag-data-providers.js +1 -1
  144. package/dist/src/telescope/rag-data-providers.js.map +1 -1
  145. package/dist/src/testing/fake-embedding-provider.d.ts +1 -1
  146. package/dist/src/testing/fake-embedding-provider.d.ts.map +1 -1
  147. package/dist/src/testing/fake-embedding-provider.js.map +1 -1
  148. package/dist/src/testing/fake-media.d.ts +2 -2
  149. package/dist/src/testing/fake-media.d.ts.map +1 -1
  150. package/dist/src/testing/fake-media.js +2 -2
  151. package/dist/src/testing/fake-media.js.map +1 -1
  152. package/dist/src/testing/fake-model-provider.d.ts.map +1 -1
  153. package/dist/src/testing/fake-model-provider.js.map +1 -1
  154. package/dist/src/testing/in-memory-actor-directory.d.ts.map +1 -1
  155. package/dist/src/testing/in-memory-actor-directory.js.map +1 -1
  156. package/dist/src/testing/in-memory-attachment-staging.d.ts.map +1 -1
  157. package/dist/src/testing/in-memory-governance-queries.d.ts.map +1 -1
  158. package/dist/src/testing/in-memory-governance-queries.js.map +1 -1
  159. package/dist/src/testing/in-memory-pricing.d.ts.map +1 -1
  160. package/dist/src/testing/in-memory-quota.d.ts.map +1 -1
  161. package/dist/src/testing/in-memory-quota.js.map +1 -1
  162. package/dist/src/testing/in-memory-sink.d.ts.map +1 -1
  163. package/dist/src/testing/in-memory-store.d.ts.map +1 -1
  164. package/dist/src/testing/in-memory-store.js +0 -1
  165. package/dist/src/testing/in-memory-store.js.map +1 -1
  166. package/dist/src/testing/index.d.ts +8 -8
  167. package/dist/src/testing/index.d.ts.map +1 -1
  168. package/dist/src/testing/index.js +8 -8
  169. package/dist/src/testing/index.js.map +1 -1
  170. package/dist/src/tool-discovery.js.map +1 -1
  171. package/dist/src/tool-registry.d.ts.map +1 -1
  172. package/dist/src/tool-registry.js.map +1 -1
  173. package/dist/stubs/main.js.map +1 -1
  174. package/package.json +19 -15
  175. package/skills/agent-governance/SKILL.md +233 -0
  176. package/skills/agent-personas-agents/SKILL.md +220 -0
  177. package/skills/agent-setup/SKILL.md +273 -0
  178. package/skills/agent-testing/SKILL.md +236 -0
  179. package/skills/agent-tools/SKILL.md +270 -0
  180. package/dist/assets/spa/assets/index-DshcMnMD.css +0 -1
  181. package/dist/assets/spa/assets/index-HVy8whBx.js +0 -9
@@ -0,0 +1,236 @@
1
+ ---
2
+ name: agent-testing
3
+ description: >-
4
+ Test @adonis-agora/agent offline: FakeModelProvider with a pure FakeScript
5
+ (args, turnIndex) and echoScript, the in-memory SPI doubles from
6
+ @adonis-agora/agent/testing (InMemoryAgentStore, InMemoryTokenStreamSink.subscribe,
7
+ InMemoryQuotaStore, InMemoryPricingStore, InMemoryGovernanceQueries,
8
+ InMemoryActorDirectory, InMemoryAttachmentStagingStore, FakeEmbeddingProvider,
9
+ FakeReranker, inMemoryRetriever), driving runAgentLoop directly with the
10
+ step/awaitApproval/openSink hooks seam, or AgentDepsFactory + InlineAgentRunner, and
11
+ asserting on typed StreamFrames. Use for "test the agent without an API key",
12
+ "script a tool-call turn", "assert streamed text", "HITL approve in tests".
13
+ metadata:
14
+ type: core
15
+ library: "@adonis-agora/agent"
16
+ library_version: "0.25.2"
17
+ framework: adonisjs
18
+ sources:
19
+ - "DavideCarvalho/adonis-agent:packages/adonis/docs/testing.mdx"
20
+ - "DavideCarvalho/adonis-agent:packages/adonis/src/testing/index.ts"
21
+ - "DavideCarvalho/adonis-agent:packages/adonis/src/testing/fake-model-provider.ts"
22
+ - "DavideCarvalho/adonis-agent:packages/adonis/src/agent-loop.ts"
23
+ ---
24
+
25
+ # Testing @adonis-agora/agent offline
26
+
27
+ Everything the agent depends on is a seam, so a test can run a full turn — model call,
28
+ tool execution, governance, cost accounting — with **no API key and no database**. The
29
+ in-memory doubles implement the SAME SPIs as the production adapters, so a green test
30
+ exercises the real loop code paths.
31
+
32
+ ## Setup
33
+
34
+ A minimal turn through the full loop, straight from the testing kit:
35
+
36
+ ```ts
37
+ import {
38
+ runAgentLoop,
39
+ ToolRegistry,
40
+ DefaultRolesPolicy,
41
+ } from '@adonis-agora/agent'
42
+ import {
43
+ FakeModelProvider,
44
+ echoScript,
45
+ InMemoryAgentStore,
46
+ InMemoryTokenStreamSink,
47
+ } from '@adonis-agora/agent/testing'
48
+
49
+ const sink = new InMemoryTokenStreamSink()
50
+ const { text } = await runAgentLoop(
51
+ {
52
+ model: new FakeModelProvider(echoScript('hi there')),
53
+ store: new InMemoryAgentStore(),
54
+ registry: new ToolRegistry(),
55
+ rolesPolicy: new DefaultRolesPolicy(),
56
+ day: '2026-07-21',
57
+ systemPrompt: 'You are a helpful assistant.',
58
+ },
59
+ { threadId: 'thread-1', actor: { id: 'user-1', roles: ['USER'] }, userText: 'hi' },
60
+ {
61
+ runId: crypto.randomUUID(),
62
+ openSink: () => sink.open('run-1'),
63
+ awaitApproval: async () => 'approve',
64
+ step: (_name, fn) => fn(),
65
+ },
66
+ )
67
+ ```
68
+
69
+ `hooks.step(name, fn)` is the checkpoint seam: inline it just calls `fn`; the durable
70
+ runner memoizes each named step. `hooks.awaitApproval` is where a HITL decision comes
71
+ from — return `{ approved: true }` (or a bare `'approve'`) to auto-approve in tests.
72
+
73
+ Source: `packages/adonis/README.md` ("Framework-agnostic core"),
74
+ `packages/adonis/src/agent-loop.ts` (`AgentLoopHooks`).
75
+
76
+ ## Core patterns
77
+
78
+ ### Pattern 1 — script a tool-call round-trip
79
+
80
+ `FakeModelProvider` takes a script — a pure function of `(args, turnIndex)`. `turnIndex`
81
+ counts assistant turns already in history, so there is no internal counter to reset:
82
+
83
+ ```ts
84
+ import { FakeModelProvider } from '@adonis-agora/agent/testing'
85
+
86
+ const model = new FakeModelProvider((args, turnIndex) => {
87
+ if (turnIndex === 0) {
88
+ // first turn: ask to call a tool
89
+ return { text: 'Let me check that.', toolCall: { name: 'getWeather', input: { city: 'Lisbon' } } }
90
+ }
91
+ // second turn: finish with the tool result in hand
92
+ return { text: 'It is 21°C in Lisbon.' }
93
+ })
94
+ ```
95
+
96
+ A scripted turn may also report `costUsd`, exactly as a gateway provider would, to
97
+ exercise the cost fold.
98
+
99
+ Source: `packages/adonis/docs/testing.mdx` ("The fake model"),
100
+ `packages/adonis/src/testing/fake-model-provider.ts`.
101
+
102
+ ### Pattern 2 — run through the runner like production does
103
+
104
+ `AgentDepsFactory` + `InlineAgentRunner` is the same composition the provider builds at
105
+ boot, so the test covers run lifecycle persistence, not just the loop:
106
+
107
+ ```ts
108
+ import {
109
+ AgentDepsFactory,
110
+ AgentRegistry,
111
+ ToolRegistry,
112
+ DefaultToolAuthorizer,
113
+ InlineAgentRunner,
114
+ } from '@adonis-agora/agent'
115
+ import {
116
+ FakeModelProvider,
117
+ echoScript,
118
+ InMemoryAgentStore,
119
+ InMemoryTokenStreamSink,
120
+ } from '@adonis-agora/agent/testing'
121
+
122
+ const store = new InMemoryAgentStore()
123
+ const sink = new InMemoryTokenStreamSink()
124
+ const registry = new ToolRegistry()
125
+ const agents = new AgentRegistry()
126
+ agents.register({ name: 'default' })
127
+
128
+ const factory = new AgentDepsFactory({
129
+ model: new FakeModelProvider(echoScript('hello')),
130
+ store,
131
+ sink,
132
+ rolesPolicy: new DefaultToolAuthorizer(['ADMIN']),
133
+ registry,
134
+ agents,
135
+ })
136
+
137
+ const runner = new InlineAgentRunner(factory, store)
138
+ const { runId } = await runner.start({
139
+ threadId: (await store.createThread({ actor: { id: 'u1', roles: ['ADMIN'] }, persona: 'default' })).id,
140
+ actor: { id: 'u1', roles: ['ADMIN'] },
141
+ userText: 'hi',
142
+ })
143
+
144
+ let text = ''
145
+ for await (const frame of sink.subscribe(runId)) {
146
+ if (frame.t === 'text') text += frame.v
147
+ }
148
+ ```
149
+
150
+ Source: `packages/adonis/docs/testing.mdx` ("A minimal turn in a test").
151
+
152
+ ### Pattern 3 — assert on typed `StreamFrame`s, not bytes
153
+
154
+ `sink.subscribe(runId)` yields discriminated frames — `{ t: 'text', v }` and
155
+ `{ t: 'component', name, data }` — so one loop can assert prose and Generative-UI
156
+ emissions separately. The SSE wire encoding happens later, in the provider's route
157
+ handler; a test never sees it:
158
+
159
+ ```ts
160
+ const components: { name: string; data: unknown }[] = []
161
+ for await (const frame of sink.subscribe(runId)) {
162
+ if (frame.t === 'text') text += frame.v
163
+ if (frame.t === 'component') components.push({ name: frame.name, data: frame.data })
164
+ }
165
+ ```
166
+
167
+ Source: `packages/adonis/docs/testing.mdx` ("Because the fakes are behavioral twins"
168
+ note), `packages/adonis/src/in-process-sink.ts`.
169
+
170
+ ## Common mistakes
171
+
172
+ ### MEDIUM — keeping mutable state in a FakeModelProvider script
173
+
174
+ ```ts
175
+ // Wrong — a closure counter: the script is no longer a pure function of its inputs.
176
+ let calls = 0
177
+ const model = new FakeModelProvider(() => (calls++ === 0 ? { text: 'call tool' } : { text: 'done' }))
178
+ ```
179
+
180
+ ```ts
181
+ // Correct — derive the turn from turnIndex (assistant turns already in history).
182
+ const model = new FakeModelProvider((_args, turnIndex) =>
183
+ turnIndex === 0 ? { text: 'call tool' } : { text: 'done' },
184
+ )
185
+ ```
186
+
187
+ Mechanism: `turnIndex` is computed from the message history each call
188
+ (`messages.filter(m => m.role === 'assistant').length`), so history-derived scripts stay
189
+ deterministic and replay-safe; a closure counter diverges the moment a run is retried or
190
+ replayed.
191
+ Source: `packages/adonis/src/testing/fake-model-provider.ts` (`turnIndex` derivation).
192
+
193
+ ### MEDIUM — registering a tool the test actor's roles cannot reach, then asserting it ran
194
+
195
+ ```ts
196
+ // Wrong — DefaultRolesPolicy(['ADMIN']) + actor roles: ['USER'] → the tool is never OFFERED.
197
+ registry.register({ name: 'get_weather', kind: 'read', description: '', inputSchema: zodSchema }, handler)
198
+ const { text } = await runAgentLoop({ ..., rolesPolicy: new DefaultRolesPolicy() },
199
+ { actor: { id: 'u1', roles: ['USER'] }, ... }, hooks)
200
+ ```
201
+
202
+ ```ts
203
+ // Correct — align the actor's roles with the tool spec (or the policy's defaults).
204
+ await runAgentLoop({ ..., rolesPolicy: new DefaultRolesPolicy() },
205
+ { actor: { id: 'u1', roles: ['ADMIN'] }, ... }, hooks)
206
+ ```
207
+
208
+ Mechanism: the offered-tools filter drops role-forbidden tools BEFORE the model turn, so
209
+ the fake model's `toolCall` for a hidden tool fails with `ToolForbiddenError` at invoke —
210
+ the loop never auto-executes what was never offered.
211
+ Source: `packages/adonis/src/tool-registry.ts` (`definitionsFor`, `invoke`),
212
+ `packages/adonis/docs/governance/authorization.mdx` ("The double check").
213
+
214
+ ### LOW — asserting on the in-memory sink's frames as if they were SSE text
215
+
216
+ ```ts
217
+ // Wrong — frames are typed objects, not an SSE byte stream.
218
+ const raw = await res.text() // 'data: {"delta":"..."}\n\n'
219
+ expect(raw).toContain('delta')
220
+ ```
221
+
222
+ ```ts
223
+ // Correct — narrow on t and assert the payload.
224
+ for await (const frame of sink.subscribe(runId)) {
225
+ if (frame.t === 'text') expect(frame.v).toBeDefined()
226
+ }
227
+ ```
228
+
229
+ Mechanism: `InMemoryTokenStreamSink.subscribe` yields `StreamFrame` objects; the SSE
230
+ envelope (`event: meta` / `data: {"delta":...}` / `event: done`) is produced by the
231
+ provider's route handler via `frameToSse`, a layer tests never cross.
232
+ Source: `packages/adonis/docs/testing.mdx` (StreamFrame note),
233
+ `packages/adonis/src/sse.ts` (`frameToSse`).
234
+
235
+ See also: `agent-tools/SKILL.md` — registering real tools into the test registry;
236
+ `agent-setup/SKILL.md` — why `day` is a required, runner-stamped loop dep.
@@ -0,0 +1,270 @@
1
+ ---
2
+ name: agent-tools
3
+ description: >-
4
+ Author and register governed tools for @adonis-agora/agent: @AiTool decorated classes,
5
+ BaseTool/ReadTool/ActionTool static-tool subclasses, defineTool functional tools,
6
+ discovery from app/agent_tools via the .adonisjs/agent/tools.js barrel (toolsHook with
7
+ source/importAlias/output) vs the runtime scan fallback, Standard Schema inputs
8
+ (Zod/Valibot/ArkType), AiToolCtx actor scoping, constructor DI (@inject, lazy + cached),
9
+ roles/ability fail-closed gating under defaultRoles, and the dataTool governed read-only
10
+ SQL satellite. Use for "write a tool", "my tool is never called", "tool forbidden",
11
+ "agent_tools barrel not generating", or "let the agent query the database".
12
+ metadata:
13
+ type: core
14
+ library: "@adonis-agora/agent"
15
+ library_version: "0.25.2"
16
+ framework: adonisjs
17
+ sources:
18
+ - "DavideCarvalho/adonis-agent:packages/adonis/docs/authoring/tools.mdx"
19
+ - "DavideCarvalho/adonis-agent:packages/adonis/docs/authoring/data-satellite.mdx"
20
+ - "DavideCarvalho/adonis-agent:packages/adonis/src/ai-tool-ref.ts"
21
+ - "DavideCarvalho/adonis-agent:packages/adonis/src/tool-discovery.ts"
22
+ ---
23
+
24
+ # Writing governed tools for @adonis-agora/agent
25
+
26
+ A tool is a named capability with a typed input schema; the model decides *when* to call
27
+ it, the loop decides *whether it may* (role gate) and *how* (auto-execute vs human
28
+ approval). `read` auto-executes, `action` pauses for HITL approval. There are two ways to
29
+ author one — a class or a function — and both register the same way.
30
+
31
+ ## Setup
32
+
33
+ Create `app/agent_tools/get_weather.ts`. The Assembler init hook registered by
34
+ `node ace configure @adonis-agora/agent` generates the `.adonisjs/agent/tools.js` barrel
35
+ at build/dev time, and the provider registers every export at boot:
36
+
37
+ ```ts
38
+ // app/agent_tools/get_weather.ts
39
+ import { z } from 'zod'
40
+ import { ReadTool } from '@adonis-agora/agent'
41
+ import type { AiToolCtx } from '@adonis-agora/agent'
42
+
43
+ const input = z.object({ city: z.string() })
44
+ type Input = z.infer<typeof input>
45
+
46
+ export default class GetWeather extends ReadTool<Input, { tempC: number }> {
47
+ // No `kind` — the base pins 'read'. Type-checks bare, no satisfies needed.
48
+ static tool = {
49
+ name: 'get_weather',
50
+ description: 'Current weather for a city.',
51
+ input,
52
+ roles: ['MEMBER'], // omit → config.defaultRoles (['ADMIN'] by default)
53
+ }
54
+
55
+ async execute({ city }: Input, _ctx: AiToolCtx) {
56
+ return { tempC: 21 }
57
+ }
58
+ }
59
+ ```
60
+
61
+ Prefer a function? `defineTool(options, execute)` returns a branded `{ spec, handler }`
62
+ discovery picks up identically — or pass it straight to `defineConfig({ tools: [...] })`.
63
+
64
+ ## Core patterns
65
+
66
+ ### Pattern 1 — mutating tools as `ActionTool` (HITL approval)
67
+
68
+ `kind: 'action'` records the call `pending_approval` and suspends the run until a client
69
+ posts approve/reject to `/agent/tool-call/approve|reject`. Dependencies come from the IoC
70
+ container — declare them in the constructor with `@inject()`:
71
+
72
+ ```ts
73
+ // app/agent_tools/purge_cache.ts
74
+ import { inject } from '@adonisjs/core'
75
+ import { z } from 'zod'
76
+ import { ActionTool } from '@adonis-agora/agent'
77
+ import type { AiToolCtx } from '@adonis-agora/agent'
78
+
79
+ const input = z.object({ key: z.string() })
80
+ type Input = z.infer<typeof input>
81
+
82
+ @inject()
83
+ export default class PurgeCache extends ActionTool<Input, { purged: string }> {
84
+ constructor(private readonly cache: CacheService) {
85
+ super()
86
+ }
87
+
88
+ static tool = {
89
+ name: 'purge_cache',
90
+ description: 'Purge one cache key.',
91
+ input,
92
+ ability: 'cache.purge', // consumed by an ability-aware RolesPolicy (authz adapter)
93
+ }
94
+
95
+ async execute({ key }: Input, _ctx: AiToolCtx) {
96
+ await this.cache.forget(key)
97
+ return { purged: key }
98
+ }
99
+ }
100
+ ```
101
+
102
+ Class tools resolve lazily through the container on first invocation, then cached — so a
103
+ constructor needing a peer service fails nothing at boot.
104
+
105
+ Source: `packages/adonis/docs/authoring/tools.mdx` ("Constructor DI").
106
+
107
+ ### Pattern 2 — let the agent query SQL with `dataTool` (fail-closed)
108
+
109
+ `dataTool` validates a single SELECT, enforces a REQUIRED role→group→table allow-list
110
+ (there is no allow-all), rewrites in an optional tenant scope, injects LIMIT, and
111
+ truncates oversized results before they hit the model's context:
112
+
113
+ ```ts
114
+ // config/agent.ts
115
+ import { defineConfig, dataTool } from '@adonis-agora/agent'
116
+ import db from '@adonisjs/lucid/services/db'
117
+
118
+ export default defineConfig({
119
+ model: () => aiSdkModel(openai('gpt-4o-mini')),
120
+ tools: [
121
+ dataTool({
122
+ db: db.connection('readonly'), // point at a read-only replica
123
+ tableAccess: { // REQUIRED — fail-closed, no implicit allow-all
124
+ roleGroups: { MEMBER: ['sales'], ADMIN: ['sales', 'ops'] },
125
+ tablesByGroup: { sales: ['orders', 'products'], ops: ['audit_log'] },
126
+ },
127
+ tenant: { tenantColumn: 'tenant_id', scopedTables: ['orders'] },
128
+ maxRows: 100, // LIMIT injected when the query has none
129
+ }),
130
+ ],
131
+ })
132
+ ```
133
+
134
+ The model sees a tool named `executeSql` taking `{ sql: string }`. Two governance layers
135
+ stack: `roles`/`ability` gate whether the tool may be called at all; `tableAccess` +
136
+ `tenant` gate what a permitted call may read.
137
+
138
+ Source: `packages/adonis/docs/authoring/data-satellite.mdx`.
139
+
140
+ ### Pattern 3 — move the scan directory with `toolsHook`
141
+
142
+ To keep tools in a shared package or differently-named directory, call the hook factory
143
+ yourself in `adonisrc.ts`:
144
+
145
+ ```ts
146
+ // adonisrc.ts
147
+ import { defineConfig } from '@adonisjs/core'
148
+ import { toolsHook } from '@adonis-agora/agent/hooks/tools'
149
+
150
+ export default defineConfig({
151
+ hooks: {
152
+ init: [
153
+ async () => ({
154
+ default: toolsHook({ source: 'app/ai/tools', importAlias: '#ai_tools' }),
155
+ }),
156
+ ],
157
+ },
158
+ })
159
+ ```
160
+
161
+ Change `source` and `importAlias` together; leave `output` alone (default
162
+ `.adonisjs/agent/tools.ts`) — moving the output makes the provider silently fall back to
163
+ the runtime scan you were replacing.
164
+
165
+ Source: `packages/adonis/docs/authoring/tools.mdx` ("Configuring the generator"),
166
+ `packages/adonis/src/hooks/tools.ts` (`ToolsHookOptions`).
167
+
168
+ ## Common mistakes
169
+
170
+ ### CRITICAL — shipping tools without `roles` and calling them unreachable
171
+
172
+ ```ts
173
+ // Wrong — no roles → inherits defaultRoles ['ADMIN']; MEMBER actors never see this tool.
174
+ static tool = { name: 'get_order', description: '...', input }
175
+ ```
176
+
177
+ ```ts
178
+ // Correct — declare who may invoke.
179
+ static tool = { name: 'get_order', description: '...', input, roles: ['MEMBER'] }
180
+ ```
181
+
182
+ Mechanism: `definitionsFor` filters the offered tool list through the role gate BEFORE
183
+ each model turn, so a tool the actor cannot invoke is never described to the model — it
184
+ cannot call what it was never shown. This is why "the model didn't call my tool" is
185
+ usually an authorization default, not a prompt problem.
186
+ Source: `packages/adonis/docs/authoring/tools.mdx` (fail-closed callout),
187
+ `packages/adonis/src/tool-registry.ts` (`DefaultRolesPolicy.can`).
188
+
189
+ ### HIGH — trusting an id from the model's arguments instead of `ctx.actor`
190
+
191
+ ```ts
192
+ // Wrong — the MODEL chose this customerId; any caller can read anyone's orders.
193
+ async execute({ customerId }: Input) {
194
+ return Order.query().where('customer_id', customerId)
195
+ }
196
+ ```
197
+
198
+ ```ts
199
+ // Correct — scope to the acting identity the resolver authenticated.
200
+ async execute(_input: Input, ctx: AiToolCtx) {
201
+ return Order.query().where('customer_ref', ctx.actor.id)
202
+ }
203
+ ```
204
+
205
+ Mechanism: tool arguments are untrusted model output; identity is single-sourced on
206
+ `ctx.actor` (`id`, `roles`, `tenantRef`) populated from the server-side actor resolver.
207
+ Source: `packages/adonis/docs/authoring/tools.mdx` ("The tool context"),
208
+ `packages/adonis/src/spi/tool.ts`.
209
+
210
+ ### MEDIUM — renaming the generated barrel's output path
211
+
212
+ ```ts
213
+ // Wrong — the provider imports .adonisjs/agent/tools.js; elsewhere = silently ignored.
214
+ toolsHook({ source: 'app/ai/tools', importAlias: '#ai_tools', output: '.adonisjs/mytools.ts' })
215
+ ```
216
+
217
+ ```ts
218
+ // Correct — change source + importAlias together; leave output at its default.
219
+ toolsHook({ source: 'app/ai/tools', importAlias: '#ai_tools' })
220
+ ```
221
+
222
+ Mechanism: the provider imports the compiled barrel from the fixed
223
+ `.adonisjs/agent/tools.js`; a moved file is absent there, so boot falls back to scanning
224
+ `app/agent_tools` — which no longer holds your tools — and the agent runs with none of
225
+ them, logging only a quiet fallback.
226
+ Source: `packages/adonis/docs/authoring/tools.mdx` ("Point `output` elsewhere…"),
227
+ `packages/adonis/src/hooks/tools.ts` (`GENERATED_TOOLS_OUTPUT`).
228
+
229
+ ### MEDIUM — debugging "zero tools" after adding one that throws on import
230
+
231
+ ```ts
232
+ // Wrong assumption — a throwing tool file aborts discovery, so NOTHING registers.
233
+ // app/agent_tools/broken.ts references a service whose `app` is undefined during boot.
234
+ ```
235
+
236
+ ```ts
237
+ // Correct mental model — the bad file is skipped loudly; every OTHER tool still registers.
238
+ // Fix the import error surfaced in the logs; the rest of agent_tools keeps working meanwhile.
239
+ ```
240
+
241
+ Mechanism: `discoverTools` wraps each dynamic import in its own try/catch, logs the
242
+ failure, and continues — so the symptom is one missing tool (the model narrating calls it
243
+ was never given), not a dead registry.
244
+ Source: `packages/adonis/src/tool-discovery.ts` (`discoverTools` import try/catch and log
245
+ message).
246
+
247
+ ### LOW — declaring `kind` on a ReadTool/ActionTool subclass
248
+
249
+ ```ts
250
+ // Wrong — the base already pins kind; the literal widens and fights the inherited static type.
251
+ export default class CloseTicket extends ActionTool<Input, Row> {
252
+ static tool = { name: 'close_ticket', kind: 'action', description: '...', input }
253
+ }
254
+ ```
255
+
256
+ ```ts
257
+ // Correct — truly bare static; the base carries kind.
258
+ export default class CloseTicket extends ActionTool<Input, Row> {
259
+ static tool = { name: 'close_ticket', description: '...', input }
260
+ }
261
+ ```
262
+
263
+ Mechanism: `ReadTool`/`ActionTool` pin `static readonly kind`; redeclaring it adds a union
264
+ field the inherited `static tool?: BaseToolOptions` doesn't contextualize, so the literal's
265
+ `kind: string` widens and type-checking degrades instead of failing.
266
+ Source: `packages/adonis/src/base-tool.ts` (`BaseToolOptions`, kind-specific bases),
267
+ `packages/adonis/docs/authoring/tools.mdx` ("truly bare").
268
+
269
+ See also: `agent-governance/SKILL.md` — swapping `rolesPolicy` for ability checks;
270
+ `agent-personas-agents/SKILL.md` — narrowing tools per persona/agent.
@@ -1 +0,0 @@
1
- :root{--bg: #09090b;--panel: #0c0c0f;--panel-2: #101017;--line: #1c1c22;--line-soft: #16161a;--text: #e7e7ea;--muted: #76767f;--agora-primary: #a78bfa;--agora-primary-soft: color-mix(in srgb, #a78bfa 14%, transparent);--agora-accent: #60a5fa;--primary: var(--agora-primary);--primary-fg: #09090b;--good: #34d399;--warn: #fbbf24;--bad: #f87171;--info: var(--agora-accent)}:root[data-theme=light],:root:not([data-theme=dark]){color-scheme:dark}:root[data-theme=dark]{color-scheme:dark}:root[data-theme=light]{color-scheme:light;--bg: hsl(40 30% 96%);--panel: hsl(40 28% 98%);--panel-2: hsl(40 24% 95%);--line: hsl(240 10% 14% / .12);--line-soft: hsl(240 10% 14% / .06);--text: hsl(240 14% 12%);--muted: hsl(240 6% 42%);--primary-fg: hsl(40 40% 97%)}*{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{background:var(--bg);color:var(--text);font-family:Space Grotesk,ui-sans-serif,system-ui,sans-serif;-webkit-font-smoothing:antialiased}.mono{font-family:JetBrains Mono,ui-monospace,SF Mono,Menlo,monospace}.tnum{font-variant-numeric:tabular-nums}.app-bg{position:fixed;inset:0;z-index:0;pointer-events:none;background-image:radial-gradient(900px 420px at 82% -10%,hsl(250 74% 51% / .12),transparent 70%),radial-gradient(700px 360px at 8% -12%,hsl(190 90% 50% / .06),transparent 70%);opacity:.9}.shell{position:relative;z-index:1;max-width:1180px;margin:0 auto;padding:28px 24px 64px}.masthead{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-bottom:22px}.brand{display:flex;align-items:center;gap:12px}.brand-mark{width:32px;height:32px;border-radius:8px;display:grid;place-items:center;color:var(--primary);background:color-mix(in srgb,var(--primary) 10%,transparent);border:1px solid color-mix(in srgb,var(--primary) 40%,transparent);font-weight:700}.brand h1{font-size:14px;font-weight:600;margin:0;letter-spacing:-.01em}.brand p{margin:1px 0 0;font-family:JetBrains Mono,ui-monospace,SF Mono,Menlo,monospace;font-size:10px;text-transform:uppercase;letter-spacing:.2em;color:var(--muted)}.tabs{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:20px}.tab{appearance:none;border:1px solid var(--line);background:var(--panel);color:var(--muted);border-radius:8px;padding:6px 12px;font-size:12px;cursor:pointer;display:inline-flex;align-items:center;gap:7px;transition:color .15s,border-color .15s,background .15s}.tab:hover{color:var(--text)}.tab[aria-selected=true]{color:var(--text);background:color-mix(in srgb,var(--primary) 10%,transparent);border-color:color-mix(in srgb,var(--primary) 50%,var(--line))}.controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.range-input{background:var(--panel);border:1px solid var(--line);color:var(--text);border-radius:8px;padding:6px 9px;font-size:12px;font-family:inherit}.price-input{min-width:225px}.icon-btn{appearance:none;border:1px solid var(--line);background:var(--panel);color:var(--muted);border-radius:8px;width:34px;height:32px;cursor:pointer;display:grid;place-items:center}.icon-btn:hover{color:var(--text)}.panel{background:linear-gradient(180deg,var(--panel-2),var(--panel));border:1px solid var(--line);border-radius:12px;padding:18px}.grid{display:grid;gap:16px}.grid.cols-3{grid-template-columns:repeat(3,1fr)}.grid.cols-2{grid-template-columns:repeat(2,1fr)}.grid.stat-4{grid-template-columns:repeat(4,1fr)}@media(max-width:900px){.grid.cols-3,.grid.cols-2,.grid.stat-4{grid-template-columns:repeat(2,1fr)}}@media(max-width:560px){.grid.cols-3,.grid.cols-2,.grid.stat-4{grid-template-columns:1fr}}.stat .label{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);margin-bottom:8px}.stat .value{font-size:24px;font-weight:600;letter-spacing:-.02em}.stat .sub{font-size:12px;color:var(--muted);margin-top:4px}.section-title{display:flex;align-items:center;justify-content:space-between;margin:0 0 14px;gap:12px}.section-title h2{font-size:14px;margin:0;letter-spacing:.01em}.section-title .hint{font-size:12px;color:var(--muted)}.table{width:100%;border-collapse:collapse;font-size:12px}.table tbody{font-family:JetBrains Mono,ui-monospace,SF Mono,Menlo,monospace;font-variant-numeric:tabular-nums}.table th{text-align:left;font-weight:500;font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);padding:0 10px 10px;border-bottom:1px solid var(--line)}.table td{padding:10px;border-bottom:1px solid var(--line-soft);vertical-align:middle}.table tr:last-child td{border-bottom:none}.table .num{text-align:right}.table-wrap{overflow-x:auto}.bar{height:6px;border-radius:999px;background:var(--line);overflow:hidden;min-width:60px}.bar>span{display:block;height:100%;border-radius:999px;background:var(--primary)}.legend{display:flex;flex-direction:column;gap:8px;font-size:12px}.legend .row{display:flex;align-items:center;gap:8px}.swatch{width:10px;height:10px;border-radius:3px;flex:none}.legend .name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pill{display:inline-flex;align-items:center;gap:6px;padding:2px 9px;border-radius:999px;font-size:11px;border:1px solid var(--line);color:var(--muted)}.dot{width:7px;height:7px;border-radius:999px;background:currentColor;display:inline-block}.s-completed,.s-ok,.s-success,.s-approved{color:var(--good)}.s-running,.s-pending,.s-pending_approval,.s-awaiting_approval{color:var(--info)}.s-failed,.s-error,.s-rejected,.s-forbidden{color:var(--bad)}.s-cancelled,.s-canceled{color:var(--warn)}.btn{appearance:none;border:1px solid var(--line);background:var(--panel);color:var(--text);border-radius:6px;padding:6px 12px;font-size:12px;font-family:inherit;cursor:pointer;transition:color .15s,border-color .15s,background .15s}.btn:hover:not(:disabled){border-color:var(--muted)}.btn:disabled{opacity:.45;cursor:not-allowed}.btn.approve{color:var(--good);background:color-mix(in srgb,var(--good) 10%,var(--panel));border-color:color-mix(in srgb,var(--good) 50%,var(--line))}.btn.reject{color:var(--bad);background:color-mix(in srgb,var(--bad) 10%,var(--panel));border-color:color-mix(in srgb,var(--bad) 50%,var(--line))}.rowbtns{display:inline-flex;gap:6px;justify-content:flex-end}.table tr.clickable{cursor:pointer}.table tr.clickable:hover td{background:var(--panel-2)}.th-sort{appearance:none;background:none;border:none;padding:0;margin:0;font:inherit;color:inherit;text-transform:inherit;letter-spacing:inherit;cursor:pointer}.th-sort:hover{color:var(--text)}.warnnum{color:var(--warn)}.msg{border:1px solid var(--line);border-radius:10px;padding:10px 12px;background:var(--panel-2)}.msg-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:6px}.msg-body{font-size:13px;white-space:pre-wrap;word-break:break-word}.empty{color:var(--muted);font-size:13px;text-align:center;padding:26px 10px}.err{color:var(--bad);font-size:13px;padding:20px 10px;text-align:center}.skeleton{border-radius:8px;background:linear-gradient(90deg,var(--line-soft),var(--line),var(--line-soft));background-size:200% 100%;animation:shimmer 1.3s ease-in-out infinite;height:14px}@keyframes shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}.stack{display:flex;flex-direction:column;gap:16px}.muted{color:var(--muted)}.right{text-align:right}.chart-svg{width:100%;height:auto;display:block}.foot{margin-top:26px;font-size:11px;color:var(--muted);text-align:center}