@n1k1t/pipelain 0.4.14 → 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.
Files changed (187) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/AGENTS.md +20 -0
  3. package/README.md +185 -12
  4. package/assets/report/index.hbs +201 -79
  5. package/assets/report/main.css +135 -3
  6. package/cli/index.ts +77 -0
  7. package/lib/src/index.d.ts +2 -0
  8. package/lib/src/index.d.ts.map +1 -1
  9. package/lib/src/index.js +2 -0
  10. package/lib/src/index.js.map +1 -1
  11. package/lib/src/models/content/factory.d.ts +7 -1
  12. package/lib/src/models/content/factory.d.ts.map +1 -1
  13. package/lib/src/models/content/factory.js +15 -0
  14. package/lib/src/models/content/factory.js.map +1 -1
  15. package/lib/src/models/content/kinds/article.d.ts +3 -1
  16. package/lib/src/models/content/kinds/article.d.ts.map +1 -1
  17. package/lib/src/models/content/kinds/article.js +7 -0
  18. package/lib/src/models/content/kinds/article.js.map +1 -1
  19. package/lib/src/models/content/kinds/attachment.d.ts +3 -1
  20. package/lib/src/models/content/kinds/attachment.d.ts.map +1 -1
  21. package/lib/src/models/content/kinds/attachment.js +7 -0
  22. package/lib/src/models/content/kinds/attachment.js.map +1 -1
  23. package/lib/src/models/content/kinds/group.d.ts +3 -1
  24. package/lib/src/models/content/kinds/group.d.ts.map +1 -1
  25. package/lib/src/models/content/kinds/group.js +7 -0
  26. package/lib/src/models/content/kinds/group.js.map +1 -1
  27. package/lib/src/models/content/kinds/model.d.ts +5 -0
  28. package/lib/src/models/content/kinds/model.d.ts.map +1 -1
  29. package/lib/src/models/content/kinds/model.js +6 -0
  30. package/lib/src/models/content/kinds/model.js.map +1 -1
  31. package/lib/src/models/content/kinds/plain.d.ts +3 -1
  32. package/lib/src/models/content/kinds/plain.d.ts.map +1 -1
  33. package/lib/src/models/content/kinds/plain.js +7 -0
  34. package/lib/src/models/content/kinds/plain.js.map +1 -1
  35. package/lib/src/models/content/kinds/rules.d.ts +3 -1
  36. package/lib/src/models/content/kinds/rules.d.ts.map +1 -1
  37. package/lib/src/models/content/kinds/rules.js +7 -0
  38. package/lib/src/models/content/kinds/rules.js.map +1 -1
  39. package/lib/src/models/content/kinds/sources.d.ts +3 -1
  40. package/lib/src/models/content/kinds/sources.d.ts.map +1 -1
  41. package/lib/src/models/content/kinds/sources.js +7 -0
  42. package/lib/src/models/content/kinds/sources.js.map +1 -1
  43. package/lib/src/models/content/kinds/tasks.d.ts +3 -1
  44. package/lib/src/models/content/kinds/tasks.d.ts.map +1 -1
  45. package/lib/src/models/content/kinds/tasks.js +7 -0
  46. package/lib/src/models/content/kinds/tasks.js.map +1 -1
  47. package/lib/src/models/index.d.ts +1 -0
  48. package/lib/src/models/index.d.ts.map +1 -1
  49. package/lib/src/models/index.js +1 -0
  50. package/lib/src/models/index.js.map +1 -1
  51. package/lib/src/models/llm/hook.d.ts +128 -0
  52. package/lib/src/models/llm/hook.d.ts.map +1 -0
  53. package/lib/src/models/llm/hook.js +129 -0
  54. package/lib/src/models/llm/hook.js.map +1 -0
  55. package/lib/src/models/llm/index.d.ts +1 -0
  56. package/lib/src/models/llm/index.d.ts.map +1 -1
  57. package/lib/src/models/llm/index.js +1 -0
  58. package/lib/src/models/llm/index.js.map +1 -1
  59. package/lib/src/models/llm/mcp.d.ts +4 -0
  60. package/lib/src/models/llm/mcp.d.ts.map +1 -1
  61. package/lib/src/models/llm/mcp.js +5 -2
  62. package/lib/src/models/llm/mcp.js.map +1 -1
  63. package/lib/src/models/llm/providers/model.d.ts +1 -1
  64. package/lib/src/models/llm/providers/model.d.ts.map +1 -1
  65. package/lib/src/models/llm/providers/proxy.d.ts +6 -0
  66. package/lib/src/models/llm/providers/proxy.d.ts.map +1 -1
  67. package/lib/src/models/llm/providers/proxy.js +5 -3
  68. package/lib/src/models/llm/providers/proxy.js.map +1 -1
  69. package/lib/src/models/llm/router.d.ts +6 -0
  70. package/lib/src/models/llm/router.d.ts.map +1 -1
  71. package/lib/src/models/llm/router.js +12 -0
  72. package/lib/src/models/llm/router.js.map +1 -1
  73. package/lib/src/models/llm/tools/attachment.js +2 -2
  74. package/lib/src/models/llm/tools/attachment.js.map +1 -1
  75. package/lib/src/models/llm/tools/edit.js +3 -3
  76. package/lib/src/models/llm/tools/edit.js.map +1 -1
  77. package/lib/src/models/llm/tools/fetch.js +4 -4
  78. package/lib/src/models/llm/tools/fetch.js.map +1 -1
  79. package/lib/src/models/llm/tools/grep.js +2 -2
  80. package/lib/src/models/llm/tools/grep.js.map +1 -1
  81. package/lib/src/models/llm/tools/ls.js +2 -2
  82. package/lib/src/models/llm/tools/ls.js.map +1 -1
  83. package/lib/src/models/llm/tools/mkdir.js +2 -2
  84. package/lib/src/models/llm/tools/mkdir.js.map +1 -1
  85. package/lib/src/models/llm/tools/model.d.ts +4 -4
  86. package/lib/src/models/llm/tools/model.d.ts.map +1 -1
  87. package/lib/src/models/llm/tools/model.js +19 -10
  88. package/lib/src/models/llm/tools/model.js.map +1 -1
  89. package/lib/src/models/llm/tools/npm.js +1 -1
  90. package/lib/src/models/llm/tools/npm.js.map +1 -1
  91. package/lib/src/models/llm/tools/npx.js +1 -1
  92. package/lib/src/models/llm/tools/npx.js.map +1 -1
  93. package/lib/src/models/llm/tools/read.js +3 -3
  94. package/lib/src/models/llm/tools/read.js.map +1 -1
  95. package/lib/src/models/llm/tools/rm.js +4 -4
  96. package/lib/src/models/llm/tools/rm.js.map +1 -1
  97. package/lib/src/models/llm/tools/search.js +2 -2
  98. package/lib/src/models/llm/tools/search.js.map +1 -1
  99. package/lib/src/models/llm/tools/skill.js +2 -2
  100. package/lib/src/models/llm/tools/skill.js.map +1 -1
  101. package/lib/src/models/llm/tools/write.js +3 -3
  102. package/lib/src/models/llm/tools/write.js.map +1 -1
  103. package/lib/src/models/meta.d.ts +12 -0
  104. package/lib/src/models/meta.d.ts.map +1 -0
  105. package/lib/src/models/meta.js +32 -0
  106. package/lib/src/models/meta.js.map +1 -0
  107. package/lib/src/models/pipeline/model.d.ts +2 -0
  108. package/lib/src/models/pipeline/model.d.ts.map +1 -1
  109. package/lib/src/models/pipeline/model.js +8 -4
  110. package/lib/src/models/pipeline/model.js.map +1 -1
  111. package/lib/src/models/pipeline/parameters.d.ts.map +1 -1
  112. package/lib/src/models/pipeline/parameters.js +5 -1
  113. package/lib/src/models/pipeline/parameters.js.map +1 -1
  114. package/lib/src/models/pipeline/report/index.d.ts.map +1 -1
  115. package/lib/src/models/pipeline/report/index.js +28 -10
  116. package/lib/src/models/pipeline/report/index.js.map +1 -1
  117. package/lib/src/models/pipeline/report/types.d.ts +15 -7
  118. package/lib/src/models/pipeline/report/types.d.ts.map +1 -1
  119. package/lib/src/models/pipeline/session.d.ts +16 -36
  120. package/lib/src/models/pipeline/session.d.ts.map +1 -1
  121. package/lib/src/models/pipeline/session.js +15 -10
  122. package/lib/src/models/pipeline/session.js.map +1 -1
  123. package/lib/src/models/pipeline/stdout/compiled/console.js +16 -13
  124. package/lib/src/models/pipeline/stdout/compiled/console.js.map +1 -1
  125. package/lib/src/models/pipeline/stdout/model.d.ts +1 -1
  126. package/lib/src/models/pipeline/stdout/model.d.ts.map +1 -1
  127. package/lib/src/models/pipeline/stdout/model.js +12 -7
  128. package/lib/src/models/pipeline/stdout/model.js.map +1 -1
  129. package/lib/src/models/pipeline/steps/ai/actions/fallback.d.ts +31 -0
  130. package/lib/src/models/pipeline/steps/ai/actions/fallback.d.ts.map +1 -0
  131. package/lib/src/models/pipeline/steps/ai/actions/fallback.js +51 -0
  132. package/lib/src/models/pipeline/steps/ai/actions/fallback.js.map +1 -0
  133. package/lib/src/models/pipeline/steps/ai/actions/index.d.ts +1 -0
  134. package/lib/src/models/pipeline/steps/ai/actions/index.d.ts.map +1 -1
  135. package/lib/src/models/pipeline/steps/ai/actions/index.js +1 -0
  136. package/lib/src/models/pipeline/steps/ai/actions/index.js.map +1 -1
  137. package/lib/src/models/pipeline/steps/ai/actions/model.d.ts +3 -6
  138. package/lib/src/models/pipeline/steps/ai/actions/model.d.ts.map +1 -1
  139. package/lib/src/models/pipeline/steps/ai/actions/model.js +3 -11
  140. package/lib/src/models/pipeline/steps/ai/actions/model.js.map +1 -1
  141. package/lib/src/models/pipeline/steps/ai/actions/reasoning.d.ts +1 -1
  142. package/lib/src/models/pipeline/steps/ai/actions/reasoning.d.ts.map +1 -1
  143. package/lib/src/models/pipeline/steps/ai/actions/reasoning.js +6 -4
  144. package/lib/src/models/pipeline/steps/ai/actions/reasoning.js.map +1 -1
  145. package/lib/src/models/pipeline/steps/ai/actions/tool.d.ts +1 -1
  146. package/lib/src/models/pipeline/steps/ai/actions/tool.d.ts.map +1 -1
  147. package/lib/src/models/pipeline/steps/ai/actions/tool.js +4 -3
  148. package/lib/src/models/pipeline/steps/ai/actions/tool.js.map +1 -1
  149. package/lib/src/models/pipeline/steps/ai/index.d.ts +5 -1
  150. package/lib/src/models/pipeline/steps/ai/index.d.ts.map +1 -1
  151. package/lib/src/models/pipeline/steps/ai/index.js +134 -97
  152. package/lib/src/models/pipeline/steps/ai/index.js.map +1 -1
  153. package/lib/src/models/pipeline/steps/ai/types.d.ts +5 -1
  154. package/lib/src/models/pipeline/steps/ai/types.d.ts.map +1 -1
  155. package/lib/src/models/pipeline/steps/ai/utils.js +1 -1
  156. package/lib/src/models/pipeline/steps/ai/utils.js.map +1 -1
  157. package/lib/src/models/pipeline/steps/loop.d.ts.map +1 -1
  158. package/lib/src/models/pipeline/steps/loop.js +10 -7
  159. package/lib/src/models/pipeline/steps/loop.js.map +1 -1
  160. package/lib/src/models/pipeline/steps/model.d.ts +2 -0
  161. package/lib/src/models/pipeline/steps/model.d.ts.map +1 -1
  162. package/lib/src/models/pipeline/steps/model.js +2 -0
  163. package/lib/src/models/pipeline/steps/model.js.map +1 -1
  164. package/lib/src/models/pipeline/steps/self.d.ts.map +1 -1
  165. package/lib/src/models/pipeline/steps/self.js +6 -5
  166. package/lib/src/models/pipeline/steps/self.js.map +1 -1
  167. package/lib/src/models/pipeline/steps/swarm.d.ts +13 -6
  168. package/lib/src/models/pipeline/steps/swarm.d.ts.map +1 -1
  169. package/lib/src/models/pipeline/steps/swarm.js +46 -23
  170. package/lib/src/models/pipeline/steps/swarm.js.map +1 -1
  171. package/lib/src/models/pipeline/types.d.ts +1 -0
  172. package/lib/src/models/pipeline/types.d.ts.map +1 -1
  173. package/lib/src/setup.js +30 -3
  174. package/lib/src/setup.js.map +1 -1
  175. package/lib/src/utils/common.d.ts +8 -0
  176. package/lib/src/utils/common.d.ts.map +1 -1
  177. package/lib/src/utils/common.js +14 -1
  178. package/lib/src/utils/common.js.map +1 -1
  179. package/lib/src/utils/index.d.ts +0 -1
  180. package/lib/src/utils/index.d.ts.map +1 -1
  181. package/lib/src/utils/index.js +0 -1
  182. package/lib/src/utils/index.js.map +1 -1
  183. package/package.json +13 -12
  184. package/lib/src/utils/meta.d.ts +0 -9
  185. package/lib/src/utils/meta.d.ts.map +0 -1
  186. package/lib/src/utils/meta.js +0 -15
  187. package/lib/src/utils/meta.js.map +0 -1
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npx jest *)"
5
+ ]
6
+ }
7
+ }
package/AGENTS.md ADDED
@@ -0,0 +1,20 @@
1
+ # AGENTS.md
2
+
3
+ Instructions for AI agents working in this repository (`@n1k1t/pipelain`).
4
+
5
+ ## Mandatory rules
6
+
7
+ - If the `caveman` skill is installed, use it for communication in this session.
8
+
9
+ ## Project documentation
10
+
11
+ Before starting work, review the documentation in `.ai/docs`:
12
+
13
+ - [`.ai/docs/structure.md`](.ai/docs/structure.md) — project structure, purpose of the main directories and files.
14
+ - [`.ai/docs/style.md`](.ai/docs/style.md) — code style: import/export order, object and function formatting.
15
+
16
+ ## Useful commands
17
+
18
+ - `npm test` — run tests (Jest).
19
+ - `npm run build:check` — type-check without building (`tsc --noEmit`).
20
+ - `npm run build` — build the project into `lib/`.
package/README.md CHANGED
@@ -33,14 +33,17 @@ Powerful utility to build and execute type-safe AI pipelines with structured out
33
33
  - [AI Step with LLM Configuration](#ai-step-with-llm-configuration)
34
34
  - [AI Step with Fallback](#ai-step-with-fallback)
35
35
  - [Debugging AI Steps](#debugging-ai-steps)
36
+ - [Execution Reports](#execution-reports)
36
37
  - [Combining self and ai Steps](#combining-self-and-ai-steps)
37
38
  - [Parallel Execution with swarm](#parallel-execution-with-swarm)
38
39
  - [Iterative Execution with loop](#iterative-execution-with-loop)
39
40
  - [Multi-Provider Pipelines](#multi-provider-pipelines)
41
+ - [Custom Model Routing](#custom-model-routing)
40
42
  - [MCP (Model Context Protocol) Integration](#mcp-model-context-protocol-integration)
41
43
  - [Using Skills](#using-skills)
42
44
  - [Extensions](#extensions)
43
45
  - [Custom LLM Tools](#custom-llm-tools)
46
+ - [LLM Tool Hooks](#llm-tool-hooks)
44
47
  - [Custom LLM Skills](#custom-llm-skills)
45
48
  - [Registering Skills from Markdown](#registering-skills-from-markdown)
46
49
  - [License](#license)
@@ -114,9 +117,11 @@ The following environment variables are supported:
114
117
  | --- | --- | --- |
115
118
  | `PIPELAIN_API_KEY` | API key for the LLM provider. | - |
116
119
  | `PIPELAIN_API_URL` | Custom API URL for the LLM provider. | - |
117
- | `PIPELAIN_SKILLS_PATHS` | Directories paths where LLM skills are stored (separated by `;`). | `~/.agents/skills` |
120
+ | `PIPELAIN_SKILLS_PATHS` | Directories paths where LLM skills are stored (separated by `;`). | `./.agents/skills;~/.agents/skills` |
118
121
  | `PIPELAIN_MODEL` | Default LLM model to use. | `gemini-flash-latest` |
119
122
  | `PIPELAIN_PROVIDER` | LLM provider name (e.g., `google`, `openai`). | - |
123
+ | `PIPELAIN_FLAGS_REPORT` | Save an HTML report of the pipeline execution. | `false` |
124
+ | `PIPELAIN_FLAGS_DEBUG` | Enable debug mode for all AI steps. | `false` |
120
125
  | `EXA_API_KEY` | API key for Exa web search tools. | - |
121
126
 
122
127
  ## Pipeline Step Utilities
@@ -136,7 +141,7 @@ Used to create different types of steps:
136
141
  General-purpose utilities:
137
142
  - `content`: `ContentFactory` instance to create structured prompt content (articles, tasks, rules, attachments).
138
143
  - `bash`: Execute shell commands on the local machine.
139
- - `log`: Emit log events for the current pipeline session.
144
+ - `log`: Emit `INFO` log events for the current pipeline session (shown in stdout and HTML reports).
140
145
 
141
146
  ### `context`
142
147
  Shared state and configuration:
@@ -158,6 +163,10 @@ Used to create structured prompt content. These methods are available via `utils
158
163
  | `file(title, path)` | Reads a local file and attaches it as context. | `await utils.content.file('Config', 'package.json')` |
159
164
  | `glob(title, pattern)` | Reads multiple files by pattern and attaches them. | `await utils.content.glob('Source', 'src/**/*.ts')` |
160
165
  | `plain(text)` | Adds raw markdown text. | `utils.content.plain('### Subtitle\nText')` |
166
+ | `user(content)` | Forces content to be placed into the `user` message. | `utils.content.user('Say hi')` |
167
+ | `system(content)` | Forces content to be placed into the `system` message. | `utils.content.system('You are a helpful assistant')` |
168
+
169
+ Each content kind has a default location it is rendered into (`tasks` and plain strings default to `user`, everything else defaults to `system`). Wrap any content in `utils.content.user()` / `utils.content.system()` to override that placement explicitly.
161
170
 
162
171
  ### `factory.tools` (LlmToolsFactory)
163
172
  Used to provide tools to the AI. These methods are available via `factory.tools` in pipeline steps.
@@ -205,13 +214,13 @@ To use a custom logger or override specific event handlers, use `PipelineStdout`
205
214
  import { PipelineStdout } from '@n1k1t/pipelain';
206
215
 
207
216
  const customStdout = PipelineStdout
208
- .build(console) // Pass any logger with .info and .warn methods
209
- .override('log', (event) => {
210
- console.log(`[CUSTOM LOG] ${event.message}`);
217
+ .build(console) // Pass any logger with .info, .warn and .debug methods
218
+ .override('log', ({ level, message }) => {
219
+ console.log(`[${level}]`, ...message);
211
220
  })
212
- .override('step:run', (event) => {
213
- if (event.meta.state === 'SUCCESS') {
214
- console.log(`Step ${event.step.title} finished in ${event.meta.spent}ms`);
221
+ .override('step:run', ({ step }) => {
222
+ if (step.meta.is('DONE')) {
223
+ console.log(`Step ${step.title} finished in ${step.meta.spent}ms`);
215
224
  }
216
225
  });
217
226
 
@@ -221,6 +230,17 @@ const customStdout = PipelineStdout
221
230
  })();
222
231
  ```
223
232
 
233
+ Each pipeline, step and AI action has a `meta` object with its execution state:
234
+
235
+ | Property / Method | Description |
236
+ | --- | --- |
237
+ | `state` | `'INIT' \| 'PENDING' \| 'DONE' \| 'ERROR'` |
238
+ | `spent` | Time spent since creation (ms). |
239
+ | `timestamp` | Creation timestamp. |
240
+ | `is(state \| state[])` | Checks the current state. |
241
+
242
+ The `log` event has a `level` field: `'DEBUG' | 'INFO' | 'WARN'`. `utils.log(...)` emits `INFO`.
243
+
224
244
  ### Structured Prompt Content
225
245
 
226
246
  You can combine multiple content types to create a rich, structured prompt for the AI:
@@ -349,7 +369,28 @@ You can use the `.debug()` method to inspect the prompts sent to the AI. When de
349
369
  )
350
370
  ```
351
371
 
352
- The prompt will be saved to: `.pipelain/${timestamp}-${session-id}/${step-title}.md`.
372
+ The prompt will be saved to: `.pipelain/debug/YYYY-MM-DD--HH-mm-ss--<session-id>/<step-number>.<step-trace>.md`.
373
+
374
+ ### Execution Reports
375
+
376
+ Use `.report()` on the pipeline compiler (or set `PIPELAIN_FLAGS_REPORT=true`) to save an HTML report of the AI steps execution:
377
+
378
+ ```ts
379
+ const pipeline = PipelineCompiler
380
+ .build('Translate')
381
+ .report() // Enables HTML report
382
+ .input(z.string())
383
+ .step('translated', ({ factory, context }) => factory
384
+ .ai('Translating')
385
+ .prompt([`Translate "${context.input}" into Spanish`])
386
+ );
387
+ ```
388
+
389
+ The report is saved into `.pipelain/reports/YYYY-MM-DD--HH-mm-ss--<session-id>.html` and includes:
390
+
391
+ - AI steps with messages, actions (tools, reasoning, fallbacks), output and timing;
392
+ - `INFO` and `WARN` logs emitted via `utils.log`;
393
+ - token usage per provider and model (prompt, cached, completion) and total session time.
353
394
 
354
395
  ### Combining `self` and `ai` Steps
355
396
 
@@ -389,12 +430,14 @@ const pipeline = PipelineCompiler
389
430
 
390
431
  ### Parallel Execution with `swarm`
391
432
 
392
- Use `swarm` to execute multiple AI tasks in parallel. You can define subtasks using the `subtasks` method:
433
+ Use `swarm` to execute multiple AI tasks in parallel. You can define subtasks as a `list` (array) or as a `map` (keyed object), optionally limiting how many run concurrently with `.limit()`.
434
+
435
+ #### `list`
393
436
 
394
437
  ```ts
395
438
  .step('analysis', ({ factory }) => factory
396
439
  .swarm('Parallel Analysis')
397
- .subtasks([
440
+ .list([
398
441
  factory
399
442
  .ai('Sentiment Analysis')
400
443
  .schema(z.object({ score: z.number() }))
@@ -410,6 +453,32 @@ Use `swarm` to execute multiple AI tasks in parallel. You can define subtasks us
410
453
  // Results will be available in context.state.analysis as an array of PromiseSettledResult
411
454
  ```
412
455
 
456
+ > `subtasks(...)` is a deprecated alias of `list(...)` kept for backward compatibility — prefer `list`.
457
+
458
+ #### `map`
459
+
460
+ Use `map` when you need to address each subtask result by name instead of by array index:
461
+
462
+ ```ts
463
+ .step('analysis', ({ factory }) => factory
464
+ .swarm('Parallel Analysis')
465
+ .map({
466
+ sentiment: factory
467
+ .ai('Sentiment Analysis')
468
+ .schema(z.object({ score: z.number() }))
469
+ .prompt(({ context }) => [`Analyze sentiment of: ${context.input}`]),
470
+
471
+ keywords: factory
472
+ .ai('Keyword Extraction')
473
+ .schema(z.object({ tags: z.array(z.string()) }))
474
+ .prompt(({ context }) => [`Extract keywords from: ${context.input}`]),
475
+ })
476
+ .limit(2) // Optional: limit parallel executions
477
+ )
478
+ // context.state.analysis.sentiment -> PromiseSettledResult<{ score: number }>
479
+ // context.state.analysis.keywords -> PromiseSettledResult<{ tags: string[] }>
480
+ ```
481
+
413
482
  ### Iterative Execution with `loop`
414
483
 
415
484
  Use `loop` for tasks that require multiple iterations or validation, such as self-correction:
@@ -472,6 +541,29 @@ const pipeline = PipelineCompiler
472
541
  );
473
542
  ```
474
543
 
544
+ ### Custom Model Routing
545
+
546
+ `LlmRouter` picks a provider by model name. Use `register` to route models matched by a [minimatch](https://github.com/isaacs/minimatch) pattern to your own provider:
547
+
548
+ ```ts
549
+ import { LlmRouter, llm } from '@n1k1t/pipelain';
550
+
551
+ const router = LlmRouter
552
+ .build()
553
+ .register('corp-*', (model) => llm.providers.LlmProxyProvider.build(model, {
554
+ name: 'corp', // Shown in usage stats and reports instead of "proxy"
555
+ connection: { key: process.env.CORP_API_KEY!, url: 'https://llm.corp.local/v1' },
556
+ }));
557
+
558
+ pipeline.step('analysis', ({ factory }) => factory
559
+ .ai('Corp Analysis')
560
+ .llm(() => router.provide('corp-gpt-4o'))
561
+ .prompt(['...'])
562
+ );
563
+ ```
564
+
565
+ Registrations are checked in order and the first match wins. Unmatched models fall back to the default routing.
566
+
475
567
  ### MCP (Model Context Protocol) Integration
476
568
 
477
569
  You can integrate MCP servers into your pipeline steps. This allows the AI to use tools provided by external MCP servers. You can also filter which tools are enabled:
@@ -494,6 +586,9 @@ pipeline.step('mcp_research', ({ factory, context }) => factory
494
586
  enabled: ['list-files', 'read-file'],
495
587
  // Or disable specific tools
496
588
  // disabled: ['delete-file'],
589
+
590
+ // Add prefix to tool names (e.g. "gdrive_read-file")
591
+ prefix: 'gdrive_',
497
592
  },
498
593
  }),
499
594
  ],
@@ -502,6 +597,8 @@ pipeline.step('mcp_research', ({ factory, context }) => factory
502
597
  );
503
598
  ```
504
599
 
600
+ Use `tools.prefix` to avoid name conflicts between MCP servers or with built-in tools. The `enabled` and `disabled` patterns match tool names without the prefix. Hooks by name (`LlmHook.build('gdrive_read-file')`) match tool names with the prefix.
601
+
505
602
  ### Using Skills
506
603
 
507
604
  Skills allow you to inject reusable domain-specific instructions or workflows into your AI steps. You can manage them via `factory.skills`:
@@ -530,7 +627,7 @@ You can create custom tools for the AI using `LlmToolCompiler`. This allows the
530
627
 
531
628
  ```ts
532
629
  import z from 'zod/v3';
533
- import { LlmToolCompiler } from '@n1k1t/pipelain';
630
+ import { LlmToolCompiler, LlmToolExecutionError } from '@n1k1t/pipelain';
534
631
 
535
632
  // 1. Define the tool
536
633
  const weatherTool = LlmToolCompiler
@@ -543,6 +640,11 @@ const weatherTool = LlmToolCompiler
543
640
  condition: z.string()
544
641
  }))
545
642
  .execute(() => async ({ city }) => {
643
+ if (!city.trim()) {
644
+ // Errors are reported back to the AI as "Execution failed: <reason>"
645
+ throw LlmToolExecutionError.build('City name is empty');
646
+ }
647
+
546
648
  // Your implementation here
547
649
  return { temperature: 22, condition: 'Sunny' };
548
650
  });
@@ -560,6 +662,77 @@ pipeline.step('weather_report', ({ factory, context }) => factory
560
662
  );
561
663
  ```
562
664
 
665
+ ### LLM Tool Hooks
666
+
667
+ Use `LlmHook` to intercept tool calls inside an `ai` step. With a hook you can validate or modify the input, override the output, skip the original execution or handle errors. Provide hooks to the step with the `.hooks([...])` method.
668
+
669
+ A hook matches a tool by one of these targets:
670
+
671
+ - `LlmHook.build('name')` matches a tool by its name (built-in, custom or MCP tool).
672
+ - `LlmHook.build(compiler)` matches a tool compiled from the same `LlmToolCompiler` instance.
673
+ - `LlmHook.build(tool)` matches the same `Tool` instance.
674
+
675
+ When a hook has a name (`LlmHook.build(compiler, { name: 'weather' })`), the name has priority over the compiler or tool.
676
+
677
+ Each hook has two handlers:
678
+
679
+ - `before({ input, next, session, context, step })` runs instead of the original `execute`. The original `execute` runs only when you call `next(input)`. If you do not call `next`, the returned value becomes the tool output.
680
+ - `after({ output, input, session, context, step })` runs after `before` is settled. `output` is a `PromiseSettledResult`, so you can handle errors. The returned value becomes the final tool output and must match the tool output type. By default, `after` rethrows the error or returns the value.
681
+
682
+ ```ts
683
+ import { LlmHook, LlmToolExecutionError } from '@n1k1t/pipelain';
684
+
685
+ // Restrict paths of the built-in "write" tool
686
+ const guard = LlmHook
687
+ .build<{ input: { path: string; content: string } }>('write')
688
+ .before(({ input, next }) => {
689
+ if (input.path.startsWith('/etc')) {
690
+ throw LlmToolExecutionError.build('Writing to /etc is not allowed');
691
+ }
692
+
693
+ return next(input);
694
+ });
695
+
696
+ // Cache results of the custom tool and convert errors for the AI
697
+ const cache = new Map<string, { temperature: number; condition: string }>();
698
+ const weather = LlmHook
699
+ .build(weatherTool)
700
+ .before(async ({ input, next }) => {
701
+ const cached = cache.get(input.city);
702
+ if (cached) {
703
+ return cached;
704
+ }
705
+
706
+ const output = await next(input);
707
+
708
+ cache.set(input.city, output);
709
+ return output;
710
+ })
711
+ .after(({ output }) => {
712
+ if (output.status === 'rejected') {
713
+ throw LlmToolExecutionError.build(output.reason);
714
+ }
715
+
716
+ return output.value;
717
+ });
718
+
719
+ pipeline.step('hooked', ({ factory, context }) => factory
720
+ .ai('Executing with hooks')
721
+ .llm(({ context }) => context.llm.assign({
722
+ tools: factory.tools
723
+ .files('read-write')
724
+ .custom({ weather: weatherTool })
725
+ .provide(),
726
+ }))
727
+ .hooks([guard, weather])
728
+ .prompt([`Check weather in ${context.input} and save it to weather.md`])
729
+ );
730
+ ```
731
+
732
+ When several hooks match the same tool, they run in the order of the array: the `before` handler of the first hook runs first, and its `after` handler runs last.
733
+
734
+ > Built-in tools with options (for example, `write` in `files('read-write')`) are clones of the original compiler. Match built-in tools by name.
735
+
563
736
  ### Custom LLM Skills
564
737
 
565
738
  You can also provide custom skills programmatically: