@kindgi/agents 0.0.0-bootstrap.0 → 0.1.1

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 (240) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +131 -2
  3. package/dist/agent-turn-flow.d.ts +57 -0
  4. package/dist/agent-turn-flow.d.ts.map +1 -0
  5. package/dist/agent-turn-flow.js +172 -0
  6. package/dist/agent-turn-flow.js.map +1 -0
  7. package/dist/conversation-binding.d.ts +111 -0
  8. package/dist/conversation-binding.d.ts.map +1 -0
  9. package/dist/conversation-binding.js +4 -0
  10. package/dist/conversation-binding.js.map +1 -0
  11. package/dist/define.d.ts +180 -0
  12. package/dist/define.d.ts.map +1 -0
  13. package/dist/define.js +361 -0
  14. package/dist/define.js.map +1 -0
  15. package/dist/errors.d.ts +62 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +4 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/guardrails-gate.d.ts +169 -0
  20. package/dist/guardrails-gate.d.ts.map +1 -0
  21. package/dist/guardrails-gate.js +202 -0
  22. package/dist/guardrails-gate.js.map +1 -0
  23. package/dist/handlers/budget-check.d.ts +22 -0
  24. package/dist/handlers/budget-check.d.ts.map +1 -0
  25. package/dist/handlers/budget-check.js +109 -0
  26. package/dist/handlers/budget-check.js.map +1 -0
  27. package/dist/handlers/build-initial-messages.d.ts +17 -0
  28. package/dist/handlers/build-initial-messages.d.ts.map +1 -0
  29. package/dist/handlers/build-initial-messages.js +86 -0
  30. package/dist/handlers/build-initial-messages.js.map +1 -0
  31. package/dist/handlers/compose-result.d.ts +10 -0
  32. package/dist/handlers/compose-result.d.ts.map +1 -0
  33. package/dist/handlers/compose-result.js +79 -0
  34. package/dist/handlers/compose-result.js.map +1 -0
  35. package/dist/handlers/constants.d.ts +8 -0
  36. package/dist/handlers/constants.d.ts.map +1 -0
  37. package/dist/handlers/constants.js +10 -0
  38. package/dist/handlers/constants.js.map +1 -0
  39. package/dist/handlers/context.d.ts +210 -0
  40. package/dist/handlers/context.d.ts.map +1 -0
  41. package/dist/handlers/context.js +4 -0
  42. package/dist/handlers/context.js.map +1 -0
  43. package/dist/handlers/dispatch-tools.d.ts +15 -0
  44. package/dist/handlers/dispatch-tools.d.ts.map +1 -0
  45. package/dist/handlers/dispatch-tools.js +511 -0
  46. package/dist/handlers/dispatch-tools.js.map +1 -0
  47. package/dist/handlers/errors.d.ts +133 -0
  48. package/dist/handlers/errors.d.ts.map +1 -0
  49. package/dist/handlers/errors.js +134 -0
  50. package/dist/handlers/errors.js.map +1 -0
  51. package/dist/handlers/evaluate-guardrails.d.ts +14 -0
  52. package/dist/handlers/evaluate-guardrails.d.ts.map +1 -0
  53. package/dist/handlers/evaluate-guardrails.js +128 -0
  54. package/dist/handlers/evaluate-guardrails.js.map +1 -0
  55. package/dist/handlers/final-iteration.d.ts +7 -0
  56. package/dist/handlers/final-iteration.d.ts.map +1 -0
  57. package/dist/handlers/final-iteration.js +26 -0
  58. package/dist/handlers/final-iteration.js.map +1 -0
  59. package/dist/handlers/index.d.ts +5 -0
  60. package/dist/handlers/index.d.ts.map +1 -0
  61. package/dist/handlers/index.js +41 -0
  62. package/dist/handlers/index.js.map +1 -0
  63. package/dist/handlers/model-call.d.ts +13 -0
  64. package/dist/handlers/model-call.d.ts.map +1 -0
  65. package/dist/handlers/model-call.js +136 -0
  66. package/dist/handlers/model-call.js.map +1 -0
  67. package/dist/handlers/persist-final-message.d.ts +14 -0
  68. package/dist/handlers/persist-final-message.d.ts.map +1 -0
  69. package/dist/handlers/persist-final-message.js +54 -0
  70. package/dist/handlers/persist-final-message.js.map +1 -0
  71. package/dist/handlers/persist-provenance.d.ts +12 -0
  72. package/dist/handlers/persist-provenance.d.ts.map +1 -0
  73. package/dist/handlers/persist-provenance.js +34 -0
  74. package/dist/handlers/persist-provenance.js.map +1 -0
  75. package/dist/handlers/persist-user-message.d.ts +15 -0
  76. package/dist/handlers/persist-user-message.d.ts.map +1 -0
  77. package/dist/handlers/persist-user-message.js +59 -0
  78. package/dist/handlers/persist-user-message.js.map +1 -0
  79. package/dist/handlers/public-types.d.ts +201 -0
  80. package/dist/handlers/public-types.d.ts.map +1 -0
  81. package/dist/handlers/public-types.js +4 -0
  82. package/dist/handlers/public-types.js.map +1 -0
  83. package/dist/handlers/rehydrate.d.ts +8 -0
  84. package/dist/handlers/rehydrate.d.ts.map +1 -0
  85. package/dist/handlers/rehydrate.js +94 -0
  86. package/dist/handlers/rehydrate.js.map +1 -0
  87. package/dist/handlers/render-prompt.d.ts +12 -0
  88. package/dist/handlers/render-prompt.d.ts.map +1 -0
  89. package/dist/handlers/render-prompt.js +41 -0
  90. package/dist/handlers/render-prompt.js.map +1 -0
  91. package/dist/handlers/resolve-tools.d.ts +10 -0
  92. package/dist/handlers/resolve-tools.d.ts.map +1 -0
  93. package/dist/handlers/resolve-tools.js +55 -0
  94. package/dist/handlers/resolve-tools.js.map +1 -0
  95. package/dist/handlers/result-shape.d.ts +94 -0
  96. package/dist/handlers/result-shape.d.ts.map +1 -0
  97. package/dist/handlers/result-shape.js +19 -0
  98. package/dist/handlers/result-shape.js.map +1 -0
  99. package/dist/handlers/run-retrievals.d.ts +10 -0
  100. package/dist/handlers/run-retrievals.d.ts.map +1 -0
  101. package/dist/handlers/run-retrievals.js +64 -0
  102. package/dist/handlers/run-retrievals.js.map +1 -0
  103. package/dist/handlers/run-snapshot.d.ts +4 -0
  104. package/dist/handlers/run-snapshot.d.ts.map +1 -0
  105. package/dist/handlers/run-snapshot.js +26 -0
  106. package/dist/handlers/run-snapshot.js.map +1 -0
  107. package/dist/handlers/setup.d.ts +25 -0
  108. package/dist/handlers/setup.d.ts.map +1 -0
  109. package/dist/handlers/setup.js +231 -0
  110. package/dist/handlers/setup.js.map +1 -0
  111. package/dist/handlers/structured-output.d.ts +38 -0
  112. package/dist/handlers/structured-output.d.ts.map +1 -0
  113. package/dist/handlers/structured-output.js +89 -0
  114. package/dist/handlers/structured-output.js.map +1 -0
  115. package/dist/handlers/tool-errors.d.ts +56 -0
  116. package/dist/handlers/tool-errors.d.ts.map +1 -0
  117. package/dist/handlers/tool-errors.js +73 -0
  118. package/dist/handlers/tool-errors.js.map +1 -0
  119. package/dist/handlers/tool-hitl.d.ts +45 -0
  120. package/dist/handlers/tool-hitl.d.ts.map +1 -0
  121. package/dist/handlers/tool-hitl.js +81 -0
  122. package/dist/handlers/tool-hitl.js.map +1 -0
  123. package/dist/handlers/turn-environment.d.ts +26 -0
  124. package/dist/handlers/turn-environment.d.ts.map +1 -0
  125. package/dist/handlers/turn-environment.js +154 -0
  126. package/dist/handlers/turn-environment.js.map +1 -0
  127. package/dist/hitl-policy.d.ts +45 -0
  128. package/dist/hitl-policy.d.ts.map +1 -0
  129. package/dist/hitl-policy.js +74 -0
  130. package/dist/hitl-policy.js.map +1 -0
  131. package/dist/index.d.ts +31 -0
  132. package/dist/index.d.ts.map +1 -0
  133. package/dist/index.js +19 -0
  134. package/dist/index.js.map +1 -0
  135. package/dist/invoke.d.ts +36 -0
  136. package/dist/invoke.d.ts.map +1 -0
  137. package/dist/invoke.js +228 -0
  138. package/dist/invoke.js.map +1 -0
  139. package/dist/migrations-dir.d.ts +11 -0
  140. package/dist/migrations-dir.d.ts.map +1 -0
  141. package/dist/migrations-dir.js +14 -0
  142. package/dist/migrations-dir.js.map +1 -0
  143. package/dist/project-run-result.d.ts +23 -0
  144. package/dist/project-run-result.d.ts.map +1 -0
  145. package/dist/project-run-result.js +116 -0
  146. package/dist/project-run-result.js.map +1 -0
  147. package/dist/prompt.d.ts +83 -0
  148. package/dist/prompt.d.ts.map +1 -0
  149. package/dist/prompt.js +119 -0
  150. package/dist/prompt.js.map +1 -0
  151. package/dist/provenance-emit.d.ts +44 -0
  152. package/dist/provenance-emit.d.ts.map +1 -0
  153. package/dist/provenance-emit.js +51 -0
  154. package/dist/provenance-emit.js.map +1 -0
  155. package/dist/registry.d.ts +38 -0
  156. package/dist/registry.d.ts.map +1 -0
  157. package/dist/registry.js +125 -0
  158. package/dist/registry.js.map +1 -0
  159. package/dist/retrieval.d.ts +47 -0
  160. package/dist/retrieval.d.ts.map +1 -0
  161. package/dist/retrieval.js +155 -0
  162. package/dist/retrieval.js.map +1 -0
  163. package/dist/run-snapshot-binding.d.ts +77 -0
  164. package/dist/run-snapshot-binding.d.ts.map +1 -0
  165. package/dist/run-snapshot-binding.js +4 -0
  166. package/dist/run-snapshot-binding.js.map +1 -0
  167. package/dist/schema.d.ts +497 -0
  168. package/dist/schema.d.ts.map +1 -0
  169. package/dist/schema.js +133 -0
  170. package/dist/schema.js.map +1 -0
  171. package/dist/streaming.d.ts +118 -0
  172. package/dist/streaming.d.ts.map +1 -0
  173. package/dist/streaming.js +17 -0
  174. package/dist/streaming.js.map +1 -0
  175. package/dist/tenant-policy.d.ts +16 -0
  176. package/dist/tenant-policy.d.ts.map +1 -0
  177. package/dist/tenant-policy.js +77 -0
  178. package/dist/tenant-policy.js.map +1 -0
  179. package/dist/types.d.ts +435 -0
  180. package/dist/types.d.ts.map +1 -0
  181. package/dist/types.js +4 -0
  182. package/dist/types.js.map +1 -0
  183. package/dist/versioning.d.ts +29 -0
  184. package/dist/versioning.d.ts.map +1 -0
  185. package/dist/versioning.js +58 -0
  186. package/dist/versioning.js.map +1 -0
  187. package/migrations/0000_sparkling_talkback.sql +18 -0
  188. package/migrations/0001_tired_warhawk.sql +16 -0
  189. package/migrations/0002_violet_ezekiel.sql +2 -0
  190. package/migrations/meta/0000_snapshot.json +172 -0
  191. package/migrations/meta/0001_snapshot.json +275 -0
  192. package/migrations/meta/0002_snapshot.json +287 -0
  193. package/migrations/meta/_journal.json +27 -0
  194. package/package.json +76 -4
  195. package/src/agent-turn-flow.ts +183 -0
  196. package/src/conversation-binding.ts +147 -0
  197. package/src/define.ts +572 -0
  198. package/src/errors.ts +80 -0
  199. package/src/guardrails-gate.ts +342 -0
  200. package/src/handlers/budget-check.ts +143 -0
  201. package/src/handlers/build-initial-messages.ts +103 -0
  202. package/src/handlers/compose-result.ts +90 -0
  203. package/src/handlers/constants.ts +10 -0
  204. package/src/handlers/context.ts +226 -0
  205. package/src/handlers/dispatch-tools.ts +633 -0
  206. package/src/handlers/errors.ts +282 -0
  207. package/src/handlers/evaluate-guardrails.ts +153 -0
  208. package/src/handlers/final-iteration.ts +30 -0
  209. package/src/handlers/index.ts +63 -0
  210. package/src/handlers/model-call.ts +151 -0
  211. package/src/handlers/persist-final-message.ts +67 -0
  212. package/src/handlers/persist-provenance.ts +39 -0
  213. package/src/handlers/persist-user-message.ts +70 -0
  214. package/src/handlers/public-types.ts +209 -0
  215. package/src/handlers/rehydrate.ts +161 -0
  216. package/src/handlers/render-prompt.ts +46 -0
  217. package/src/handlers/resolve-tools.ts +68 -0
  218. package/src/handlers/result-shape.ts +113 -0
  219. package/src/handlers/run-retrievals.ts +77 -0
  220. package/src/handlers/run-snapshot.ts +44 -0
  221. package/src/handlers/setup.ts +269 -0
  222. package/src/handlers/structured-output.ts +117 -0
  223. package/src/handlers/tool-errors.ts +122 -0
  224. package/src/handlers/tool-hitl.ts +126 -0
  225. package/src/handlers/turn-environment.ts +191 -0
  226. package/src/hitl-policy.ts +128 -0
  227. package/src/index.ts +154 -0
  228. package/src/invoke.ts +299 -0
  229. package/src/migrations-dir.ts +17 -0
  230. package/src/project-run-result.ts +131 -0
  231. package/src/prompt.ts +185 -0
  232. package/src/provenance-emit.ts +100 -0
  233. package/src/registry.ts +164 -0
  234. package/src/retrieval.ts +219 -0
  235. package/src/run-snapshot-binding.ts +87 -0
  236. package/src/schema.ts +154 -0
  237. package/src/streaming.ts +153 -0
  238. package/src/tenant-policy.ts +78 -0
  239. package/src/types.ts +453 -0
  240. package/src/versioning.ts +77 -0
package/src/define.ts ADDED
@@ -0,0 +1,572 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import { type ToolErrorsSpec, validateToolErrorsSpec } from '@kindgi/policy-contract';
5
+ import {
6
+ type AnySchema,
7
+ compileInlineSchema,
8
+ isZodSchema,
9
+ loadZodConverterSync,
10
+ toJSONSchemaSync,
11
+ } from '@kindgi/schema';
12
+ import type { Result, Semver } from '@kindgi/types';
13
+
14
+ import type { InvalidAgentError } from './errors.js';
15
+ import type {
16
+ Agent,
17
+ AgentId,
18
+ AgentOutputSpec,
19
+ ConversationPolicy,
20
+ PromptParameter,
21
+ RetrievalIntent,
22
+ ToolRef,
23
+ TurnBudget,
24
+ } from './types.js';
25
+
26
+ type Issue = { path: string; message: string };
27
+
28
+ /**
29
+ * Public API to construct an agent definition. Validates every field,
30
+ * brands ids, and returns a `Result` — no throws.
31
+ *
32
+ * ```ts
33
+ * const agent = defineAgent({
34
+ * id: 'acme.citation-verifier',
35
+ * version: '1.0.0',
36
+ * name: 'Citation Verifier',
37
+ * instructions: 'Verify every citation against the case-law index.',
38
+ * capabilities: [{ needs: [{ feature: 'structured-output' }] }],
39
+ * tools: [{ id: 'acme.verify-citation', version: '1.0.0' }],
40
+ * retrieval: [{ types: ['prior-verification'], scope: 'same-project' }],
41
+ * guardrails: ['no-hallucinated-citations'],
42
+ * });
43
+ * if (agent.kind === 'err') throw new Error(agent.error.message);
44
+ * registry.register(agent.value);
45
+ * ```
46
+ */
47
+ export function defineAgent(spec: DefineAgentSpec): Result<Agent, InvalidAgentError> {
48
+ const issues: Issue[] = [
49
+ ...validateIdentity(spec),
50
+ ...validateContent(spec),
51
+ ...validateArrays(spec),
52
+ ...validateRetrieval(spec),
53
+ ...validateParameters(spec.parameters),
54
+ ...validateBudget(spec.budget),
55
+ ...validateConversationPolicy(spec.conversationPolicy),
56
+ ...validateToolErrors(spec.toolErrors),
57
+ ];
58
+ const output = resolveOutput(spec.output);
59
+ if (output.kind === 'err') issues.push(...output.issues);
60
+
61
+ if (issues.length > 0) {
62
+ return {
63
+ kind: 'err',
64
+ error: {
65
+ code: 'invalid-agent',
66
+ message: `Agent "${String(spec.id)}" is invalid (${issues.length} issue${issues.length === 1 ? '' : 's'})`,
67
+ issues,
68
+ },
69
+ };
70
+ }
71
+
72
+ return {
73
+ kind: 'ok',
74
+ value: buildAgent(spec, output.kind === 'ok' ? output.value : undefined),
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Author-facing input shape for `defineAgent`. `id` and `version` are
80
+ * strings (they get branded inside `defineAgent`); array fields are
81
+ * `readonly` so callers can pass literal arrays.
82
+ *
83
+ * ## Field-by-field authoring guide
84
+ *
85
+ * The most common author mistake is treating `tools` as a list of
86
+ * tool ids (strings). It is NOT — it is a list of `{id, version}`
87
+ * refs where `version` is an npm-style semver RANGE (`'^1.0.0'`,
88
+ * `'~1.2.3'`, `'1.0.0'`, `'>=1.0.0 <2.0.0'`). Resolution happens at
89
+ * run start via `semver.maxSatisfying`.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * const agent = defineAgent({
94
+ * id: 'my-pack.summarizer',
95
+ * version: '1.0.0',
96
+ * name: 'Summarizer',
97
+ * instructions: 'Summarize the user message in one sentence.',
98
+ * capabilities: [{ needs: [{ feature: 'tool-use' }] }],
99
+ * tools: [
100
+ * { id: 'my-pack.fetch-doc', version: '^0.1.0' },
101
+ * ],
102
+ * retrieval: [],
103
+ * guardrails: [],
104
+ * budget: { maxSteps: 6, maxCostUsd: 0.1, maxWallMs: 30_000 },
105
+ * });
106
+ * ```
107
+ */
108
+ export interface DefineAgentSpec {
109
+ /**
110
+ * Business identifier. Convention: `<pack-id>.<agent-name>` (e.g.
111
+ * `'acme.contract-reviewer'`). Non-empty. Gets branded as
112
+ * `AgentId` inside `defineAgent`.
113
+ */
114
+ readonly id: string;
115
+ /**
116
+ * Exact semver of THIS agent revision (e.g. `'1.0.0'`,
117
+ * `'0.2.1-alpha.3'`). Distinct from tool version RANGES on
118
+ * `tools[]`. Every publish under the same id must bump this.
119
+ */
120
+ readonly version: string;
121
+ /** Human-readable name for UI and logs. */
122
+ readonly name: string;
123
+ /** Optional longer description shown in catalogs / admin surfaces. */
124
+ readonly description?: string;
125
+ /**
126
+ * The system prompt. Sent to the model with every turn as the
127
+ * baseline instructions. Load-bearing — this is where you shape
128
+ * the agent's behavior (persona, output format, tool-use policy).
129
+ */
130
+ readonly instructions: string;
131
+ /**
132
+ * Required capabilities the agent needs from a `ModelProvider`.
133
+ * Typically one entry: `[{ needs: [{ feature: 'tool-use' }] }]`
134
+ * for a tool-calling agent, `[{ needs: [{ feature: 'structured-output' }] }]`
135
+ * for an agent that returns schema-constrained JSON. The router uses
136
+ * the first entry to pick a compatible provider from the tenant's
137
+ * `ProviderRegistry`.
138
+ */
139
+ readonly capabilities: Agent['capabilities'];
140
+ /**
141
+ * Tools the agent may call. **NOT** a list of tool ids —
142
+ * a list of `{id, version}` refs where `version` is an npm-style
143
+ * semver range. At run start, each ref resolves against the
144
+ * registered versions via `semver.maxSatisfying`; unresolvable
145
+ * refs fail the turn with `tool-version-unresolvable`.
146
+ *
147
+ * The model sees each tool's `id`, `description`, and input
148
+ * schema — write tool descriptions carefully (that's what the
149
+ * LLM reads to decide when to call).
150
+ */
151
+ readonly tools: readonly ToolRef[];
152
+ /**
153
+ * Retrieval intents the agent runs BEFORE calling the model.
154
+ * Each intent names fact types, a scope, and a retrieval mode; the
155
+ * results are injected into the model input as retrieved
156
+ * context. Empty array `[]` = no retrieval, model sees only the
157
+ * conversation history + user message.
158
+ */
159
+ readonly retrieval: readonly RetrievalIntent[];
160
+ /**
161
+ * Guardrail IDs that guard this agent's turns. Each id must
162
+ * resolve among the guardrails bound for the run, or the turn fails
163
+ * with `unresolved-guardrail`. Guardrails are evaluated once per
164
+ * turn, on the final response before it is stored — see
165
+ * `defineCheck` for the checker side.
166
+ *
167
+ * Empty array `[]` = no guarding.
168
+ */
169
+ readonly guardrails: readonly string[];
170
+ /**
171
+ * Optional named prompt parameters. Callers pass values for these
172
+ * in `invokeAgent({parameters: {...}})`; the framework substitutes
173
+ * them into `instructions`. Each parameter declares its type +
174
+ * default. Absent = agent takes no runtime parameters.
175
+ */
176
+ readonly parameters?: readonly PromptParameter[];
177
+ /**
178
+ * Preferred model provider by id. Soft hint — the router prefers
179
+ * this provider when it satisfies `capabilities.needs` + tenant
180
+ * policy, falling back to normal capability-based selection when
181
+ * the preferred provider is unregistered or filtered out. Enables
182
+ * A/B'ing agents across providers without churning registrations:
183
+ * register several, pin the agent to the one you want to test.
184
+ *
185
+ * The value is a `ProviderMetadata.id` string (e.g.
186
+ * `'anthropic-claude-sonnet-4-6'`). Unset = capability-match only.
187
+ */
188
+ readonly preferredProvider?: string;
189
+ /**
190
+ * Preferred model name within the selected provider. Soft hint — see
191
+ * `Agent.preferredModel`.
192
+ */
193
+ readonly preferredModel?: string;
194
+ /**
195
+ * A typed result: the final answer must be JSON matching `schema`
196
+ * (JSON Schema, or a Zod schema converted at definition time). An
197
+ * invalid answer is sent back to the model with the problems listed,
198
+ * up to `maxRepairs` times (default 1). See `AgentOutputSpec`.
199
+ */
200
+ readonly output?: {
201
+ readonly schema: AnySchema;
202
+ readonly name?: string;
203
+ readonly maxRepairs?: number;
204
+ };
205
+ /**
206
+ * What the turn does when a tool call fails: the failure goes back to
207
+ * the model as the call's result, so it can correct the call, up to
208
+ * `maxRetries` times per turn, for the kinds in `retryOn`. Default:
209
+ * one retry, for `invalid-arguments` and `unknown-tool` (nothing ran).
210
+ * A tenant's `tool-errors` policy can lower it. See `ToolErrorsSpec`.
211
+ */
212
+ readonly toolErrors?: ToolErrorsSpec;
213
+ /**
214
+ * Per-conversation behavior — how much history to load, when to
215
+ * gate on HITL, when to auto-close. Every field optional; defaults
216
+ * are "load the full history, no HITL, no auto-close". See
217
+ * `ConversationPolicy` for the full shape including tool-level
218
+ * HITL rules.
219
+ */
220
+ readonly conversationPolicy?: ConversationPolicy;
221
+ /**
222
+ * Turn budget caps. `maxSteps` limits model+tool call loop
223
+ * iterations per turn; `maxCostUsd` caps model spend per turn;
224
+ * `maxWallMs` bounds turn wall-clock time. Exceeding any cap
225
+ * ends the turn with a `budget-exceeded` failure. Sensible dev
226
+ * defaults: `{maxSteps: 6, maxCostUsd: 0.1, maxWallMs: 30_000}`.
227
+ */
228
+ readonly budget?: TurnBudget;
229
+ /** Free-form tags for catalog filtering. Not interpreted by the runtime. */
230
+ readonly tags?: readonly string[];
231
+ }
232
+
233
+ const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
234
+
235
+ function validateIdentity(spec: DefineAgentSpec): Issue[] {
236
+ const out: Issue[] = [];
237
+ if (typeof spec.id !== 'string' || spec.id.trim().length === 0) {
238
+ out.push({ path: '/id', message: 'id must be a non-empty string' });
239
+ }
240
+ if (typeof spec.version !== 'string' || !SEMVER_RE.test(spec.version)) {
241
+ out.push({ path: '/version', message: 'version must be a valid semver string' });
242
+ }
243
+ if (typeof spec.name !== 'string' || spec.name.trim().length === 0) {
244
+ out.push({ path: '/name', message: 'name must be a non-empty string' });
245
+ }
246
+ return out;
247
+ }
248
+
249
+ function validateContent(spec: DefineAgentSpec): Issue[] {
250
+ const out: Issue[] = [];
251
+ if (typeof spec.instructions !== 'string' || spec.instructions.trim().length === 0) {
252
+ out.push({ path: '/instructions', message: 'instructions must be a non-empty string' });
253
+ }
254
+ if (!Array.isArray(spec.capabilities) || spec.capabilities.length === 0) {
255
+ out.push({
256
+ path: '/capabilities',
257
+ message: 'capabilities must be a non-empty array of Capability declarations',
258
+ });
259
+ }
260
+ return out;
261
+ }
262
+
263
+ function validateArrays(spec: DefineAgentSpec): Issue[] {
264
+ const out: Issue[] = [];
265
+ if (!Array.isArray(spec.tools)) {
266
+ out.push({
267
+ path: '/tools',
268
+ message:
269
+ 'tools must be an array of { id: string, version: string } refs. No bare-string ids — every tool must carry a semver range.',
270
+ });
271
+ } else {
272
+ spec.tools.forEach((ref, i) => out.push(...validateToolRef(ref, i)));
273
+ }
274
+ if (!Array.isArray(spec.guardrails)) {
275
+ out.push({ path: '/guardrails', message: 'guardrails must be an array of guardrail ids' });
276
+ }
277
+ return out;
278
+ }
279
+
280
+ function validateToolRef(ref: unknown, i: number): Issue[] {
281
+ const out: Issue[] = [];
282
+ if (ref === null || typeof ref !== 'object') {
283
+ out.push({
284
+ path: `/tools/${i}`,
285
+ message: 'each tool must be a { id, version } object; bare-string ids are not allowed',
286
+ });
287
+ return out;
288
+ }
289
+ const obj = ref as Record<string, unknown>;
290
+ if (typeof obj.id !== 'string' || obj.id.trim().length === 0) {
291
+ out.push({ path: `/tools/${i}/id`, message: 'tool id must be a non-empty string' });
292
+ }
293
+ if (typeof obj.version !== 'string' || obj.version.trim().length === 0) {
294
+ out.push({
295
+ path: `/tools/${i}/version`,
296
+ message:
297
+ 'tool version must be a non-empty semver range string (e.g., "1.2.3", "^1.2.3", "~1.2.3"). Deep grammar validation runs at dispatch time via the `semver` library.',
298
+ });
299
+ }
300
+ return out;
301
+ }
302
+
303
+ function validateRetrieval(spec: DefineAgentSpec): Issue[] {
304
+ if (!Array.isArray(spec.retrieval)) {
305
+ return [{ path: '/retrieval', message: 'retrieval must be an array of RetrievalIntents' }];
306
+ }
307
+ const out: Issue[] = [];
308
+ spec.retrieval.forEach((intent, i) => out.push(...validateIntent(intent, i)));
309
+ return out;
310
+ }
311
+
312
+ function validateIntent(intent: RetrievalIntent, i: number): Issue[] {
313
+ const out: Issue[] = [];
314
+ if (!Array.isArray(intent.types) || intent.types.length === 0) {
315
+ out.push({
316
+ path: `/retrieval/${i}/types`,
317
+ message: 'each retrieval intent must declare at least one type',
318
+ });
319
+ }
320
+ if (
321
+ intent.scope !== 'same-conversation' &&
322
+ intent.scope !== 'same-project' &&
323
+ intent.scope !== 'tenant'
324
+ ) {
325
+ out.push({
326
+ path: `/retrieval/${i}/scope`,
327
+ message: 'scope must be same-conversation, same-project, or tenant',
328
+ });
329
+ }
330
+ if (intent.limit !== undefined && (!Number.isInteger(intent.limit) || intent.limit <= 0)) {
331
+ out.push({ path: `/retrieval/${i}/limit`, message: 'limit must be a positive integer' });
332
+ }
333
+ return out;
334
+ }
335
+
336
+ const VALID_PARAM_TYPES = new Set(['string', 'number', 'boolean', 'date']);
337
+ const AUTO_INJECTED_NAMES = new Set(['today', 'now', 'agent', 'conversation']);
338
+
339
+ function validateParameters(parameters?: readonly PromptParameter[]): Issue[] {
340
+ if (parameters === undefined) return [];
341
+ const out: Issue[] = [];
342
+ const seen = new Set<string>();
343
+ parameters.forEach((p, i) => {
344
+ if (typeof p.name !== 'string' || p.name.trim().length === 0) {
345
+ out.push({ path: `/parameters/${i}/name`, message: 'name must be a non-empty string' });
346
+ }
347
+ if (seen.has(p.name)) {
348
+ out.push({
349
+ path: `/parameters/${i}/name`,
350
+ message: `duplicate parameter name "${p.name}"`,
351
+ });
352
+ }
353
+ seen.add(p.name);
354
+ if (AUTO_INJECTED_NAMES.has(p.name)) {
355
+ out.push({
356
+ path: `/parameters/${i}/name`,
357
+ message: `"${p.name}" is a reserved auto-injected variable — do not declare it as a parameter`,
358
+ });
359
+ }
360
+ if (!VALID_PARAM_TYPES.has(p.type)) {
361
+ out.push({
362
+ path: `/parameters/${i}/type`,
363
+ message: 'type must be string, number, boolean, or date',
364
+ });
365
+ }
366
+ });
367
+ return out;
368
+ }
369
+
370
+ function validateBudget(budget?: TurnBudget): Issue[] {
371
+ if (budget === undefined) return [];
372
+ const out: Issue[] = [];
373
+ if (
374
+ budget.maxSteps !== undefined &&
375
+ (!Number.isInteger(budget.maxSteps) || budget.maxSteps <= 0)
376
+ ) {
377
+ out.push({ path: '/budget/maxSteps', message: 'maxSteps must be a positive integer' });
378
+ }
379
+ if (
380
+ budget.maxCostUsd !== undefined &&
381
+ (typeof budget.maxCostUsd !== 'number' || budget.maxCostUsd < 0)
382
+ ) {
383
+ out.push({
384
+ path: '/budget/maxCostUsd',
385
+ message: 'maxCostUsd must be a non-negative number',
386
+ });
387
+ }
388
+ if (
389
+ budget.maxWallMs !== undefined &&
390
+ (!Number.isInteger(budget.maxWallMs) || budget.maxWallMs <= 0)
391
+ ) {
392
+ out.push({ path: '/budget/maxWallMs', message: 'maxWallMs must be a positive integer' });
393
+ }
394
+ return out;
395
+ }
396
+
397
+ function validateToolErrors(toolErrors: DefineAgentSpec['toolErrors']): Issue[] {
398
+ if (toolErrors === undefined) return [];
399
+ const checked = validateToolErrorsSpec(toolErrors);
400
+ return checked.kind === 'ok'
401
+ ? []
402
+ : checked.issues.map((i) => ({ path: `/toolErrors${i.path}`, message: i.message }));
403
+ }
404
+
405
+ function validateConversationPolicy(policy?: ConversationPolicy): Issue[] {
406
+ if (policy === undefined) return [];
407
+ const out: Issue[] = [];
408
+ if (
409
+ policy.historyLimit !== undefined &&
410
+ (!Number.isInteger(policy.historyLimit) || policy.historyLimit <= 0)
411
+ ) {
412
+ out.push({
413
+ path: '/conversationPolicy/historyLimit',
414
+ message: 'historyLimit must be a positive integer',
415
+ });
416
+ }
417
+ if (
418
+ policy.autoCloseAfterInactiveSeconds !== undefined &&
419
+ (!Number.isInteger(policy.autoCloseAfterInactiveSeconds) ||
420
+ policy.autoCloseAfterInactiveSeconds <= 0)
421
+ ) {
422
+ out.push({
423
+ path: '/conversationPolicy/autoCloseAfterInactiveSeconds',
424
+ message: 'autoCloseAfterInactiveSeconds must be a positive integer',
425
+ });
426
+ }
427
+ if (
428
+ policy.hitlAfterTurns !== undefined &&
429
+ (!Number.isInteger(policy.hitlAfterTurns) || policy.hitlAfterTurns <= 0)
430
+ ) {
431
+ out.push({
432
+ path: '/conversationPolicy/hitlAfterTurns',
433
+ message: 'hitlAfterTurns must be a positive integer',
434
+ });
435
+ }
436
+ // Nested hitl policy.
437
+ if (policy.hitl !== undefined) {
438
+ const h = policy.hitl;
439
+ if (h.afterTurns !== undefined && (!Number.isInteger(h.afterTurns) || h.afterTurns <= 0)) {
440
+ out.push({
441
+ path: '/conversationPolicy/hitl/afterTurns',
442
+ message: 'hitl.afterTurns must be a positive integer',
443
+ });
444
+ }
445
+ if (h.timeoutMs !== undefined && (!Number.isInteger(h.timeoutMs) || h.timeoutMs <= 0)) {
446
+ out.push({
447
+ path: '/conversationPolicy/hitl/timeoutMs',
448
+ message: 'hitl.timeoutMs must be a positive integer (ms)',
449
+ });
450
+ }
451
+ const ROLES: ReadonlySet<string> = new Set(['standard', 'senior', 'admin']);
452
+ if (
453
+ h.defaultReviewerRole !== undefined &&
454
+ !ROLES.has(h.defaultReviewerRole as unknown as string)
455
+ ) {
456
+ out.push({
457
+ path: '/conversationPolicy/hitl/defaultReviewerRole',
458
+ message: 'hitl.defaultReviewerRole must be one of: standard, senior, admin',
459
+ });
460
+ }
461
+ const MODES: ReadonlySet<string> = new Set(['never_ask', 'ask_on_first_use', 'always_ask']);
462
+ if (h.tools?.default !== undefined && !MODES.has(h.tools.default)) {
463
+ out.push({
464
+ path: '/conversationPolicy/hitl/tools/default',
465
+ message: 'hitl.tools.default must be one of: never_ask, ask_on_first_use, always_ask',
466
+ });
467
+ }
468
+ if (h.tools?.overrides !== undefined) {
469
+ for (const [toolId, rule] of Object.entries(h.tools.overrides)) {
470
+ const mode = typeof rule === 'string' ? rule : rule.mode;
471
+ if (!MODES.has(mode)) {
472
+ out.push({
473
+ path: `/conversationPolicy/hitl/tools/overrides/${toolId}`,
474
+ message: 'override mode must be one of: never_ask, ask_on_first_use, always_ask',
475
+ });
476
+ }
477
+ if (
478
+ typeof rule === 'object' &&
479
+ rule.requiredRole !== undefined &&
480
+ !ROLES.has(rule.requiredRole)
481
+ ) {
482
+ out.push({
483
+ path: `/conversationPolicy/hitl/tools/overrides/${toolId}/requiredRole`,
484
+ message: 'override requiredRole must be one of: standard, senior, admin',
485
+ });
486
+ }
487
+ }
488
+ }
489
+ }
490
+ return out;
491
+ }
492
+
493
+ type OutputOutcome =
494
+ | { readonly kind: 'none' }
495
+ | { readonly kind: 'ok'; readonly value: AgentOutputSpec }
496
+ | { readonly kind: 'err'; readonly issues: readonly Issue[] };
497
+
498
+ /** The output spec in its wire form: JSON Schema that compiles, and a valid repair count. */
499
+ function resolveOutput(output: DefineAgentSpec['output']): OutputOutcome {
500
+ if (output === undefined) return { kind: 'none' };
501
+ const issues: Issue[] = [];
502
+ if (
503
+ output.maxRepairs !== undefined &&
504
+ (!Number.isInteger(output.maxRepairs) || output.maxRepairs < 0)
505
+ ) {
506
+ issues.push({
507
+ path: '/output/maxRepairs',
508
+ message: 'maxRepairs must be a non-negative integer',
509
+ });
510
+ }
511
+ if (output.name !== undefined && (typeof output.name !== 'string' || output.name.length === 0)) {
512
+ issues.push({ path: '/output/name', message: 'name must be a non-empty string' });
513
+ }
514
+ let schema: Readonly<Record<string, unknown>>;
515
+ if (isZodSchema(output.schema)) {
516
+ // The output side: downstream steps read every field, defaulted ones included.
517
+ const converted = toJSONSchemaSync(output.schema, loadZodConverterSync(), 'output');
518
+ if (converted.kind === 'err') {
519
+ return {
520
+ kind: 'err',
521
+ issues: [...issues, { path: '/output/schema', message: converted.error.message }],
522
+ };
523
+ }
524
+ schema = converted.value;
525
+ } else {
526
+ schema = output.schema as Readonly<Record<string, unknown>>;
527
+ }
528
+ const compiled = compileInlineSchema(schema);
529
+ if (compiled.kind === 'err') {
530
+ issues.push({ path: '/output/schema', message: compiled.error.message });
531
+ }
532
+ if (issues.length > 0) return { kind: 'err', issues };
533
+ return {
534
+ kind: 'ok',
535
+ value: {
536
+ schema,
537
+ ...(output.name !== undefined && { name: output.name }),
538
+ ...(output.maxRepairs !== undefined && { maxRepairs: output.maxRepairs }),
539
+ },
540
+ };
541
+ }
542
+
543
+ function buildAgent(spec: DefineAgentSpec, output: AgentOutputSpec | undefined): Agent {
544
+ return {
545
+ id: spec.id as AgentId,
546
+ version: spec.version as Semver,
547
+ name: spec.name,
548
+ ...(spec.description !== undefined && { description: spec.description }),
549
+ instructions: spec.instructions,
550
+ capabilities: spec.capabilities.map((c) => ({ ...c })),
551
+ tools: [...spec.tools],
552
+ retrieval: spec.retrieval.map((r) => ({ ...r })),
553
+ guardrails: [...spec.guardrails],
554
+ ...(spec.parameters !== undefined && {
555
+ parameters: spec.parameters.map((p) => ({ ...p })),
556
+ }),
557
+ ...(spec.preferredProvider !== undefined && { preferredProvider: spec.preferredProvider }),
558
+ ...(spec.preferredModel !== undefined && { preferredModel: spec.preferredModel }),
559
+ ...(spec.conversationPolicy !== undefined && {
560
+ conversationPolicy: { ...spec.conversationPolicy },
561
+ }),
562
+ ...(spec.budget !== undefined && { budget: { ...spec.budget } }),
563
+ ...(spec.tags !== undefined && { tags: [...spec.tags] }),
564
+ ...(output !== undefined && { output }),
565
+ ...(spec.toolErrors !== undefined && {
566
+ toolErrors: {
567
+ ...spec.toolErrors,
568
+ ...(spec.toolErrors.retryOn !== undefined && { retryOn: [...spec.toolErrors.retryOn] }),
569
+ },
570
+ }),
571
+ };
572
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,80 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ export type AgentError =
5
+ | InvalidAgentError
6
+ | AgentNotFoundError
7
+ | AgentAlreadyRegisteredError
8
+ | AgentVersionMismatchError
9
+ | ConversationNotFoundError
10
+ | ConversationClosedError
11
+ | InvalidMessageError
12
+ | PersistenceError;
13
+
14
+ /** `defineAgent` was called with a shape that fails validation. */
15
+ export interface InvalidAgentError {
16
+ readonly code: 'invalid-agent';
17
+ readonly message: string;
18
+ readonly issues: readonly {
19
+ readonly path: string;
20
+ readonly message: string;
21
+ }[];
22
+ }
23
+
24
+ /** A registry lookup by (id, version) missed. */
25
+ export interface AgentNotFoundError {
26
+ readonly code: 'agent-not-found';
27
+ readonly message: string;
28
+ readonly agentId: string;
29
+ readonly version?: string;
30
+ }
31
+
32
+ /** Trying to register an agent that already exists at that (id, version). */
33
+ export interface AgentAlreadyRegisteredError {
34
+ readonly code: 'agent-already-registered';
35
+ readonly message: string;
36
+ readonly agentId: string;
37
+ readonly version: string;
38
+ }
39
+
40
+ /**
41
+ * A caller resumed a conversation but supplied an agent version that
42
+ * doesn't match the conversation's opening agent. Agents evolve; a
43
+ * conversation must be resumed with the same agent version it started
44
+ * with.
45
+ */
46
+ export interface AgentVersionMismatchError {
47
+ readonly code: 'agent-version-mismatch';
48
+ readonly message: string;
49
+ readonly conversationId: string;
50
+ readonly expectedVersion: string;
51
+ readonly actualVersion: string;
52
+ }
53
+
54
+ /** No conversation with the given id exists (for this tenant). */
55
+ export interface ConversationNotFoundError {
56
+ readonly code: 'conversation-not-found';
57
+ readonly message: string;
58
+ readonly conversationId: string;
59
+ }
60
+
61
+ /** Attempted to append or otherwise mutate a closed conversation. */
62
+ export interface ConversationClosedError {
63
+ readonly code: 'conversation-closed';
64
+ readonly message: string;
65
+ readonly conversationId: string;
66
+ }
67
+
68
+ /** `appendMessage` was called with malformed inputs (blank content, bad role, etc.). */
69
+ export interface InvalidMessageError {
70
+ readonly code: 'invalid-message';
71
+ readonly message: string;
72
+ readonly reason: string;
73
+ }
74
+
75
+ /** Storage-layer error. Cause carries the underlying DB / IO failure. */
76
+ export interface PersistenceError {
77
+ readonly code: 'persistence-error';
78
+ readonly message: string;
79
+ readonly cause: unknown;
80
+ }