@kindgi/agents 0.0.0-bootstrap.0 → 0.1.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 (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
@@ -0,0 +1,122 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ /**
5
+ * Retrying failed tool calls. A failure the turn's policy retries goes
6
+ * back to the model as the call's result — what failed and why — and
7
+ * the turn continues; the model can correct the call. Each retry costs a
8
+ * step, so `budget.maxSteps` still bounds the turn. A failure the policy
9
+ * doesn't retry, or one past `maxRetries`, fails the turn as before.
10
+ *
11
+ * Retries are counted from the turn's messages (the results this module
12
+ * writes carry a marker), so a replayed turn counts the same.
13
+ */
14
+
15
+ import type { ModelMessage } from '@kindgi/capabilities';
16
+ import { TOOL_ERROR_KINDS, type ToolErrorKind, type ToolErrorsSpec } from '@kindgi/policy-contract';
17
+
18
+ import { isRepairMessage } from './structured-output.js';
19
+
20
+ /** Without an agent setting: one retry, for failures where nothing ran. */
21
+ export const DEFAULT_TOOL_ERRORS: Required<ToolErrorsSpec> = {
22
+ maxRetries: 1,
23
+ retryOn: ['invalid-arguments', 'unknown-tool'],
24
+ };
25
+
26
+ /** The policy a turn applies. */
27
+ export interface ToolErrorPolicy {
28
+ readonly maxRetries: number;
29
+ readonly retryOn: ReadonlySet<ToolErrorKind>;
30
+ }
31
+
32
+ /**
33
+ * The agent's setting (or the default), capped by the tenant's
34
+ * `tool-errors` policy: the fewer retries, and only the kinds both
35
+ * allow — like every tenant policy, it can only make the turn stricter.
36
+ */
37
+ export function effectiveToolErrorPolicy(
38
+ agent: ToolErrorsSpec | undefined,
39
+ tenantCap: ToolErrorsSpec | undefined,
40
+ ): ToolErrorPolicy {
41
+ const maxRetries = Math.min(
42
+ agent?.maxRetries ?? DEFAULT_TOOL_ERRORS.maxRetries,
43
+ tenantCap?.maxRetries ?? Number.POSITIVE_INFINITY,
44
+ );
45
+ const allowed = new Set(tenantCap?.retryOn ?? TOOL_ERROR_KINDS);
46
+ const retryOn = (agent?.retryOn ?? DEFAULT_TOOL_ERRORS.retryOn).filter((k) => allowed.has(k));
47
+ return { maxRetries, retryOn: new Set(retryOn) };
48
+ }
49
+
50
+ /**
51
+ * Which kind of failure a tool call's error is: arguments that failed
52
+ * the input schema, a tool the agent doesn't have, or a tool that ran
53
+ * and failed.
54
+ */
55
+ export function toolErrorKindOf(error: {
56
+ readonly code: string;
57
+ readonly cause?: unknown;
58
+ }): ToolErrorKind {
59
+ if (error.code === 'unresolved-tool') return 'unknown-tool';
60
+ const inner = (error.cause as { readonly code?: unknown } | undefined)?.code;
61
+ return inner === 'input-validation-failed' ? 'invalid-arguments' : 'tool-error';
62
+ }
63
+
64
+ /** Marks the results this module writes, so they can be counted. */
65
+ const RETRY_MARKER = 'tool-error-retry';
66
+
67
+ /** What the model sees for a failed call it may retry. */
68
+ export interface ToolErrorResult {
69
+ readonly kindgi: typeof RETRY_MARKER;
70
+ readonly status: 'failed';
71
+ readonly error: {
72
+ readonly kind: ToolErrorKind;
73
+ readonly message: string;
74
+ readonly issues?: readonly unknown[];
75
+ };
76
+ readonly instruction: string;
77
+ }
78
+
79
+ const INSTRUCTIONS: Readonly<Record<ToolErrorKind, string>> = {
80
+ 'invalid-arguments': "Fix the arguments to match the tool's input schema and call it again.",
81
+ 'unknown-tool': 'Call one of the tools you were given instead.',
82
+ 'tool-error': 'The tool failed. Call it again if a retry makes sense, or answer without it.',
83
+ };
84
+
85
+ export function toolErrorResult(
86
+ kind: ToolErrorKind,
87
+ message: string,
88
+ issues: readonly unknown[] | undefined,
89
+ ): ToolErrorResult {
90
+ return {
91
+ kindgi: RETRY_MARKER,
92
+ status: 'failed',
93
+ error: { kind, message, ...(issues !== undefined && { issues }) },
94
+ instruction: INSTRUCTIONS[kind],
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Retries this turn has taken: the marked tool results after the turn's
100
+ * user message. Earlier turns' results come back as history; they don't
101
+ * count.
102
+ */
103
+ export function toolRetriesSoFar(messages: readonly ModelMessage[]): number {
104
+ let start = 0;
105
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
106
+ const m = messages[i];
107
+ if (m !== undefined && m.role === 'user' && !isRepairMessage(m)) {
108
+ start = i + 1;
109
+ break;
110
+ }
111
+ }
112
+ return messages.slice(start).filter(isRetryResult).length;
113
+ }
114
+
115
+ function isRetryResult(m: ModelMessage): boolean {
116
+ if (m.role !== 'tool') return false;
117
+ try {
118
+ return (JSON.parse(m.content) as { readonly kindgi?: unknown }).kindgi === RETRY_MARKER;
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
@@ -0,0 +1,126 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ //
5
+ // Tool-level HITL — resolves the effective `ToolHitlMode` for a given
6
+ // (agent, tool) pair. Consumed by `dispatch-tools.ts` to
7
+ // decide whether a tool call parks the run on a kernel waitpoint before
8
+ // dispatch.
9
+ //
10
+ // Resolution order (narrower wins, framework default is fail-open):
11
+ // 1. `agent.conversationPolicy.hitl.tools.overrides[toolId]` — exact match
12
+ // 2. `agent.conversationPolicy.hitl.tools.default` — agent-wide default
13
+ // 3. per-tool default from `Tool.mutating` — safe fallback
14
+ // - `mutating: false` → `never_ask`
15
+ // - `mutating: true` or absent → `ask_on_first_use` (defaults safer)
16
+ //
17
+ // The effective-policy resolver (`hitl-policy.ts`) layers the tenant
18
+ // cap on top.
19
+ //
20
+
21
+ import { createHash } from 'node:crypto';
22
+
23
+ import type { Tool } from '@kindgi/tools';
24
+
25
+ import type { Agent, ToolHitlMode, ToolHitlRule } from '../types.js';
26
+
27
+ export interface ResolvedToolHitl {
28
+ readonly mode: ToolHitlMode;
29
+ readonly requiredRole: 'standard' | 'senior' | 'admin';
30
+ }
31
+
32
+ export function resolveToolHitl(agent: Agent, tool: Tool): ResolvedToolHitl {
33
+ const hitl = agent.conversationPolicy?.hitl;
34
+ const toolsPolicy = hitl?.tools;
35
+ const defaultRole = hitl?.defaultReviewerRole ?? 'standard';
36
+
37
+ // Opt-in: agents that don't declare `hitl.tools` get no tool-level
38
+ // gates. Matches the design's "fail-open" default — HITL is opt-in
39
+ // at the agent level, not implicit.
40
+ if (toolsPolicy === undefined) {
41
+ return { mode: 'never_ask', requiredRole: defaultRole };
42
+ }
43
+
44
+ // 1. Explicit override for this toolId.
45
+ const rawOverride = toolsPolicy.overrides?.[tool.id as unknown as string];
46
+ if (rawOverride !== undefined) {
47
+ const rule: ToolHitlRule =
48
+ typeof rawOverride === 'string' ? { mode: rawOverride } : rawOverride;
49
+ return {
50
+ mode: rule.mode,
51
+ requiredRole: rule.requiredRole ?? defaultRole,
52
+ };
53
+ }
54
+
55
+ // 2. Agent-wide default.
56
+ if (toolsPolicy.default !== undefined) {
57
+ return { mode: toolsPolicy.default, requiredRole: defaultRole };
58
+ }
59
+
60
+ // 3. Per-tool default from Tool.mutating — only reached when
61
+ // `hitl.tools` is present (opt-in) but neither an override nor a
62
+ // default matches. Read-only tools skip the gate; mutating tools
63
+ // ask on first use.
64
+ const mode: ToolHitlMode = tool.mutating === false ? 'never_ask' : 'ask_on_first_use';
65
+ return { mode, requiredRole: defaultRole };
66
+ }
67
+
68
+ /**
69
+ * Deterministic hash of tool arguments — used as the cache key for
70
+ * `ask_on_first_use`. Sorted-key JSON so reordered args don't produce
71
+ * a different hash.
72
+ */
73
+ export function hashToolArgs(args: unknown): string {
74
+ return createHash('sha256').update(canonicalStringify(args)).digest('hex').slice(0, 40);
75
+ }
76
+
77
+ function canonicalStringify(value: unknown): string {
78
+ if (value === null || typeof value !== 'object') return JSON.stringify(value);
79
+ if (Array.isArray(value)) return `[${value.map(canonicalStringify).join(',')}]`;
80
+ const entries = Object.entries(value as Record<string, unknown>).sort(([a], [b]) =>
81
+ a.localeCompare(b),
82
+ );
83
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalStringify(v)}`).join(',')}}`;
84
+ }
85
+
86
+ /**
87
+ * Deterministic waitpoint token for a tool-call gate. Includes both
88
+ * the model-generated call id (unique per iteration) AND the args hash
89
+ * so kernel flow replay lands on the same token, and a re-issued
90
+ * tool-call in a later iteration gets its own gate.
91
+ */
92
+ export function computeToolCallWaitToken(input: {
93
+ readonly runId: string;
94
+ readonly callId: string;
95
+ readonly argsHash: string;
96
+ }): string {
97
+ return createHash('sha256')
98
+ .update(`tool-call:${input.runId}:${input.callId}:${input.argsHash}`)
99
+ .digest('hex')
100
+ .slice(0, 40);
101
+ }
102
+
103
+ /**
104
+ * Shape the tool-level waitpoint resolves to when the reviewer decides.
105
+ * Same shape as session-gate decisions — the approvals-complete route
106
+ * materializes it identically.
107
+ */
108
+ export interface ToolHitlDecision {
109
+ readonly decided: 'approve' | 'reject';
110
+ readonly rationale?: string;
111
+ }
112
+
113
+ /**
114
+ * In-conversation cache of decisions for `ask_on_first_use`. Persisted
115
+ * on `agent_conversations.metadata.hitlToolDecisions` — a flat map of
116
+ * `${toolId}:${argsHash}` → decision. Read/write goes through the
117
+ * conversation row's metadata via the standard update path.
118
+ */
119
+ export interface ToolDecisionCacheKey {
120
+ readonly toolId: string;
121
+ readonly argsHash: string;
122
+ }
123
+
124
+ export function cacheKeyFor(key: ToolDecisionCacheKey): string {
125
+ return `${key.toolId}:${key.argsHash}`;
126
+ }
@@ -0,0 +1,191 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ /**
5
+ * What a turn runs against: its conversation, and the guardrails, tools,
6
+ * policies and model it resolves at the start. `setup` resolves them
7
+ * once per turn; a resumed turn resolves them again (see
8
+ * `rehydrateTurnContext`), pinned to the model `setup` routed to, since
9
+ * the kernel doesn't re-run a step that already completed and these live
10
+ * on the in-memory `TurnContext`.
11
+ */
12
+
13
+ import { route } from '@kindgi/capabilities';
14
+ import type { TenantPolicy } from '@kindgi/capabilities';
15
+ import type { HitlSpec, ToolErrorsSpec } from '@kindgi/policy-contract';
16
+
17
+ import type { ConversationClosedError } from '../errors.js';
18
+ import { resolveGuardrails } from '../guardrails-gate.js';
19
+ import { type EffectiveHitlPolicy, resolveEffectiveHitlPolicy } from '../hitl-policy.js';
20
+ import { mergeTenantPolicies } from '../tenant-policy.js';
21
+ import type { Conversation } from '../types.js';
22
+ import type { TurnContext } from './context.js';
23
+ import { throwAgentTurnFailure } from './errors.js';
24
+ import { resolveTurnTools } from './resolve-tools.js';
25
+ import { type ToolErrorPolicy, effectiveToolErrorPolicy } from './tool-errors.js';
26
+
27
+ /** The conversation the turn runs in: open, and opened with this agent version. */
28
+ export async function loadTurnConversation(ctx: TurnContext): Promise<Conversation> {
29
+ const conv = await ctx.bindings.conversationBinding.getConversation(
30
+ ctx.input.tenantId,
31
+ ctx.input.conversationId,
32
+ );
33
+ if (conv.kind === 'err') throwAgentTurnFailure(conv.error);
34
+ if (conv.value.closedAt !== undefined) {
35
+ const err: ConversationClosedError = {
36
+ code: 'conversation-closed',
37
+ message: `Conversation "${ctx.input.conversationId}" is closed`,
38
+ conversationId: ctx.input.conversationId,
39
+ };
40
+ throwAgentTurnFailure(err);
41
+ }
42
+ if (
43
+ conv.value.agentId !== ctx.input.agent.id ||
44
+ conv.value.agentVersion !== ctx.input.agent.version
45
+ ) {
46
+ throwAgentTurnFailure({
47
+ code: 'agent-version-mismatch',
48
+ message: `Conversation opened with ${conv.value.agentId}@${conv.value.agentVersion}; invoked with ${ctx.input.agent.id}@${ctx.input.agent.version}`,
49
+ conversationId: ctx.input.conversationId,
50
+ expectedVersion: conv.value.agentVersion,
51
+ actualVersion: ctx.input.agent.version,
52
+ });
53
+ }
54
+ ctx.conversation = conv.value;
55
+ return conv.value;
56
+ }
57
+
58
+ /** The model a turn was routed to, as `setup` journals it. */
59
+ export interface PinnedRoute {
60
+ readonly providerId: string;
61
+ readonly model: string;
62
+ }
63
+
64
+ /**
65
+ * Resolve the turn's guardrails, tools, tenant policy, tool-error policy
66
+ * and model onto `ctx`. With `pinned`, the route is that provider and
67
+ * model, still under the tenant's current policy; one no longer
68
+ * registered or allowed fails the turn.
69
+ */
70
+ export async function resolveTurnEnvironment(
71
+ ctx: TurnContext,
72
+ pinned?: PinnedRoute,
73
+ ): Promise<PinnedRoute & { readonly toolCount: number }> {
74
+ const invResolution = resolveGuardrails(ctx.input.agent, ctx.bindings);
75
+ if (invResolution.missing.length > 0) {
76
+ throwAgentTurnFailure({
77
+ code: 'unresolved-guardrail',
78
+ message: `Agent "${ctx.input.agent.id}" references guardrails not in the registry: ${invResolution.missing.join(', ')}`,
79
+ guardrailIds: invResolution.missing,
80
+ });
81
+ }
82
+ ctx.guardrails = invResolution.resolved;
83
+
84
+ // This turn's tools come from the tenant's own registry — never a
85
+ // registry shared across concurrent turns of other tenants.
86
+ const tenantTools = await ctx.bindings.toolRegistry.forTenant(ctx.input.tenantId);
87
+ ctx.tools = resolveTurnTools(tenantTools, ctx.input.agent);
88
+
89
+ const capability = ctx.input.agent.capabilities[0];
90
+ if (capability === undefined) {
91
+ throwAgentTurnFailure({
92
+ code: 'capability-routing-failed',
93
+ message: `Agent "${ctx.input.agent.id}" has no capabilities declared`,
94
+ cause: null,
95
+ });
96
+ }
97
+ // Merge the static tenant policy with the one derived from the
98
+ // policy registry (if wired); the result is at least as strict as
99
+ // each (see `mergeTenantPolicies`).
100
+ const derivedPolicy =
101
+ ctx.bindings.policyRegistry !== undefined
102
+ ? await ctx.bindings.policyRegistry.evaluate<
103
+ { readonly tenantId: typeof ctx.input.tenantId },
104
+ TenantPolicy | undefined
105
+ >('model-routing', { tenantId: ctx.input.tenantId })
106
+ : undefined;
107
+ const effectivePolicy = mergeTenantPolicies(ctx.bindings.tenantPolicy, derivedPolicy);
108
+ if (effectivePolicy !== undefined) ctx.tenantPolicy = effectivePolicy;
109
+ ctx.toolErrorPolicy = await resolveToolErrorPolicy(ctx);
110
+ // Storage-backed registries need an async hydration
111
+ // step before the sync `list(tenantId)` call — they read persisted
112
+ // providers from `ProviderRegistryBinding` and instantiate each via
113
+ // its adapter factory. In-memory implementations (dev-echo,
114
+ // tests) leave `hydrate` undefined and this is a no-op.
115
+ if (ctx.bindings.providerRegistry.hydrate !== undefined) {
116
+ await ctx.bindings.providerRegistry.hydrate(ctx.input.tenantId);
117
+ }
118
+ // A pinned route narrows the policy to exactly that provider and model.
119
+ const routingPolicy =
120
+ pinned === undefined
121
+ ? effectivePolicy
122
+ : mergeTenantPolicies(effectivePolicy, {
123
+ tenantId: ctx.input.tenantId,
124
+ providers: { allow: [pinned.providerId] },
125
+ models: { allow: [pinned.model] },
126
+ });
127
+ const routed = route({
128
+ capability,
129
+ providers: ctx.bindings.providerRegistry.list(ctx.input.tenantId),
130
+ ...(routingPolicy !== undefined && { tenantPolicy: routingPolicy }),
131
+ ...(ctx.input.agent.preferredProvider !== undefined && {
132
+ preferredProvider: ctx.input.agent.preferredProvider,
133
+ }),
134
+ ...(ctx.input.agent.preferredModel !== undefined && {
135
+ preferredModel: ctx.input.agent.preferredModel,
136
+ }),
137
+ });
138
+ if (routed.kind === 'err') {
139
+ throwAgentTurnFailure({
140
+ code: 'capability-routing-failed',
141
+ message:
142
+ pinned === undefined
143
+ ? routed.error.message
144
+ : `The turn was routed to ${pinned.providerId}/${pinned.model}, which is no longer registered or allowed: ${routed.error.message}`,
145
+ cause: routed.error,
146
+ });
147
+ }
148
+ ctx.provider = routed.value.provider;
149
+ ctx.model = routed.value.model;
150
+ return {
151
+ providerId: routed.value.provider.metadata.id,
152
+ model: routed.value.model.name,
153
+ toolCount: ctx.tools.definitions.length,
154
+ };
155
+ }
156
+
157
+ /**
158
+ * The turn's approval rules: the agent's, held to the tenant's `hitl`
159
+ * policy. A policy that can't be evaluated fails the turn — running
160
+ * without it would skip the tenant's approvals.
161
+ */
162
+ export async function resolveTurnHitlPolicy(ctx: TurnContext): Promise<EffectiveHitlPolicy> {
163
+ let tenant: HitlSpec | undefined;
164
+ if (ctx.bindings.policyRegistry !== undefined) {
165
+ try {
166
+ tenant = await ctx.bindings.policyRegistry.evaluate<
167
+ { readonly tenantId: typeof ctx.input.tenantId },
168
+ HitlSpec | undefined
169
+ >('hitl', { tenantId: ctx.input.tenantId });
170
+ } catch (cause) {
171
+ throwAgentTurnFailure({
172
+ code: 'tenant-policy-unavailable',
173
+ message: `The tenant's hitl policy could not be applied: ${cause instanceof Error ? cause.message : String(cause)}`,
174
+ policyKind: 'hitl',
175
+ });
176
+ }
177
+ }
178
+ return resolveEffectiveHitlPolicy({ tenant, agent: ctx.input.agent });
179
+ }
180
+
181
+ /** The agent's `toolErrors`, capped by the tenant's `tool-errors` policy. */
182
+ async function resolveToolErrorPolicy(ctx: TurnContext): Promise<ToolErrorPolicy> {
183
+ const cap =
184
+ ctx.bindings.policyRegistry !== undefined
185
+ ? await ctx.bindings.policyRegistry.evaluate<
186
+ { readonly tenantId: typeof ctx.input.tenantId },
187
+ ToolErrorsSpec | undefined
188
+ >('tool-errors', { tenantId: ctx.input.tenantId })
189
+ : undefined;
190
+ return effectiveToolErrorPolicy(ctx.input.agent.toolErrors, cap);
191
+ }
@@ -0,0 +1,128 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ //
5
+ // Effective HITL policy resolver — computes the runtime policy that
6
+ // gates + timeouts consult, merging in this order:
7
+ //
8
+ // 1. Framework defaults (session gate off, no tool gates, 24h timeout,
9
+ // standard reviewer, `escalate` on timeout).
10
+ // 2. Agent's `conversationPolicy.hitl` (and `hitlAfterTurns`).
11
+ // 3. The tenant's `hitl` policy (`HitlSpec`) — only stricter: it can
12
+ // shorten the timeout, raise the reviewer role, and set a floor
13
+ // under a tool's gate, never loosen.
14
+ //
15
+ // The agent turn resolves it once, at the start (see
16
+ // `resolveTurnHitlPolicy`), instead of reading
17
+ // `agent.conversationPolicy.hitl` directly.
18
+ //
19
+
20
+ import { type HitlSpec, higherRole, toolHitlRule } from '@kindgi/policy-contract';
21
+
22
+ import type { Agent, ConversationHitlPolicy, ToolHitlMode, ToolHitlRule } from './types.js';
23
+
24
+ export interface EffectiveHitlPolicy {
25
+ /**
26
+ * Session-turn count gate. Absent = no session gate.
27
+ * Merged from `agent.conversationPolicy.hitl.afterTurns` and
28
+ * `agent.conversationPolicy.hitlAfterTurns` (either fires;
29
+ * `hitl.afterTurns` wins when both are set).
30
+ */
31
+ readonly turn?: { readonly afterTurns: number };
32
+ /**
33
+ * Tool-level policy. Absent = no tool gates (agents opt in
34
+ * explicitly). Same shape as `agent.conversationPolicy.hitl.tools`,
35
+ * with string overrides normalized to rules and tenant overrides
36
+ * applied; the per-tool default (from `Tool.mutating`) is applied at
37
+ * dispatch time.
38
+ */
39
+ readonly tools?: {
40
+ readonly default?: ToolHitlMode;
41
+ readonly overrides: ReadonlyMap<string, ToolHitlRule>;
42
+ };
43
+ /**
44
+ * The tenant's per-tool rules: a floor under the agent's gate for
45
+ * that tool. A tool's gate is the stricter of the two; tools the
46
+ * tenant names nothing for keep the agent's.
47
+ */
48
+ readonly toolFloors?: ReadonlyMap<string, ToolHitlRule>;
49
+ readonly defaultReviewerRole: 'standard' | 'senior' | 'admin';
50
+ /** Millisecond timeout used at enqueue time. */
51
+ readonly timeoutMs: number;
52
+ readonly onTimeout: 'auto-approve' | 'auto-reject' | 'escalate';
53
+ }
54
+
55
+ /**
56
+ * Framework defaults. Every field non-optional so the resolver's
57
+ * return type is fully-populated regardless of caller policy state.
58
+ */
59
+ const FRAMEWORK_DEFAULTS = {
60
+ defaultReviewerRole: 'standard' as const,
61
+ timeoutMs: 24 * 60 * 60 * 1000,
62
+ onTimeout: 'escalate' as const,
63
+ } as const;
64
+
65
+ /**
66
+ * Compute the effective HITL policy for a given (tenant, agent) pair:
67
+ * framework defaults overlaid with the agent's policy, then held to the
68
+ * tenant's `hitl` policy (only stricter). `tenant: undefined` — the
69
+ * tenant has none.
70
+ */
71
+ export function resolveEffectiveHitlPolicy(input: {
72
+ readonly tenant: HitlSpec | undefined;
73
+ readonly agent: Agent;
74
+ }): EffectiveHitlPolicy {
75
+ const agentHitl: ConversationHitlPolicy | undefined = input.agent.conversationPolicy?.hitl;
76
+ const legacyAfterTurns = input.agent.conversationPolicy?.hitlAfterTurns;
77
+ const agentAfterTurns = agentHitl?.afterTurns ?? legacyAfterTurns;
78
+
79
+ const merged: {
80
+ turn?: { afterTurns: number };
81
+ tools?: {
82
+ default?: ToolHitlMode;
83
+ overrides: ReadonlyMap<string, ToolHitlRule>;
84
+ };
85
+ toolFloors?: ReadonlyMap<string, ToolHitlRule>;
86
+ defaultReviewerRole: 'standard' | 'senior' | 'admin';
87
+ timeoutMs: number;
88
+ onTimeout: 'auto-approve' | 'auto-reject' | 'escalate';
89
+ } = {
90
+ defaultReviewerRole: agentHitl?.defaultReviewerRole ?? FRAMEWORK_DEFAULTS.defaultReviewerRole,
91
+ timeoutMs: agentHitl?.timeoutMs ?? FRAMEWORK_DEFAULTS.timeoutMs,
92
+ onTimeout: FRAMEWORK_DEFAULTS.onTimeout,
93
+ };
94
+
95
+ if (agentAfterTurns !== undefined) {
96
+ merged.turn = { afterTurns: agentAfterTurns };
97
+ }
98
+
99
+ const agentToolsPolicy = agentHitl?.tools;
100
+ if (agentToolsPolicy !== undefined) {
101
+ const overrides = new Map<string, ToolHitlRule>();
102
+ if (agentToolsPolicy.overrides !== undefined) {
103
+ for (const [k, v] of Object.entries(agentToolsPolicy.overrides)) {
104
+ overrides.set(k, typeof v === 'string' ? { mode: v } : v);
105
+ }
106
+ }
107
+ merged.tools = {
108
+ ...(agentToolsPolicy.default !== undefined && { default: agentToolsPolicy.default }),
109
+ overrides,
110
+ };
111
+ }
112
+
113
+ // The tenant's policy — only stricter.
114
+ const tenant = input.tenant;
115
+ if (tenant !== undefined) {
116
+ if (tenant.maxTimeoutMs !== undefined && merged.timeoutMs > tenant.maxTimeoutMs) {
117
+ merged.timeoutMs = tenant.maxTimeoutMs;
118
+ }
119
+ merged.defaultReviewerRole =
120
+ higherRole(merged.defaultReviewerRole, tenant.minReviewerRole) ?? merged.defaultReviewerRole;
121
+ const floors = Object.entries(tenant.tools ?? {});
122
+ if (floors.length > 0) {
123
+ merged.toolFloors = new Map(floors.map(([toolId, value]) => [toolId, toolHitlRule(value)]));
124
+ }
125
+ }
126
+
127
+ return merged as EffectiveHitlPolicy;
128
+ }