@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,87 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { ProjectId, Result, RunId, Semver, TenantId, Timestamp } from '@kindgi/types';
5
+
6
+ import type { PersistenceError } from './errors.js';
7
+ import type { AgentId, ConversationId } from './types.js';
8
+
9
+ /**
10
+ * Input for `RunSnapshotBinding.write` — the InvokeAgentInput envelope
11
+ * captured at run-start so `resumeAgentTurn(runId)` can rebuild the
12
+ * same TurnContext after a kernel waitpoint resolves. Idempotent under
13
+ * flow replay via the runId primary key.
14
+ */
15
+ export interface RunSnapshotWriteInput {
16
+ readonly runId: RunId;
17
+ readonly tenantId: TenantId;
18
+ readonly projectId: ProjectId;
19
+ readonly agentId: AgentId;
20
+ readonly agentVersion: Semver;
21
+ readonly conversationId: ConversationId;
22
+ readonly userMessage: string;
23
+ /** The turn's prompt parameters — a resumed turn renders its instructions with them. */
24
+ readonly parameters?: Readonly<Record<string, string | number | boolean>>;
25
+ /** The turn's structured input (`InvokeAgentInput.input`); same persistence contract. */
26
+ readonly input?: unknown;
27
+ readonly participantId?: string;
28
+ readonly dryRun?: boolean;
29
+ /**
30
+ * Optional serialized Principal (authorization). Impls MUST persist
31
+ * unchanged and return it verbatim from `read` so resumed turns get
32
+ * the same enforcement context. Wire shape is opaque to this layer.
33
+ */
34
+ readonly principal?: unknown;
35
+ /**
36
+ * Optional authz config (`{ fgaApiUrl }`). Same persistence contract
37
+ * as `principal`.
38
+ */
39
+ readonly authz?: unknown;
40
+ }
41
+
42
+ /**
43
+ * Persisted run-snapshot row returned by `RunSnapshotBinding.read`.
44
+ * Field shape mirrors the `agent_run_snapshots` table but is expressed
45
+ * as a plain interface so alternative impls (non-Postgres) match the
46
+ * same contract.
47
+ */
48
+ export interface RunSnapshotRecord {
49
+ readonly runId: RunId;
50
+ readonly tenantId: TenantId;
51
+ readonly projectId: ProjectId;
52
+ readonly agentId: AgentId;
53
+ readonly agentVersion: Semver;
54
+ readonly conversationId: ConversationId;
55
+ readonly userMessage: string;
56
+ readonly parameters?: Readonly<Record<string, string | number | boolean>>;
57
+ readonly input?: unknown;
58
+ readonly participantId?: string;
59
+ readonly dryRun: boolean;
60
+ readonly principal?: unknown;
61
+ readonly authz?: unknown;
62
+ readonly createdAt: Timestamp;
63
+ }
64
+
65
+ /**
66
+ * `RunSnapshotBinding` — the public seam between @kindgi/agents and
67
+ * whatever run-snapshot store a deployment plugs in (the Kindgi
68
+ * runtime ships a Postgres-backed one; alternatives possible).
69
+ *
70
+ * Ownership: @kindgi/agents (not the kernel). Kernel schemas stay
71
+ * generic across all flow runners; agent-specific reconstruction
72
+ * context lives at this layer.
73
+ *
74
+ * `write` is best-effort — a failure does NOT fail the turn (the run
75
+ * already started; snapshot-write hiccups shouldn't block execution).
76
+ * Callers of a resumed turn get `run-snapshot-missing` and know the
77
+ * run is unrecoverable. Impls MUST be idempotent under kernel replay
78
+ * (ON CONFLICT DO NOTHING on the runId PK, or equivalent).
79
+ */
80
+ export interface RunSnapshotBinding {
81
+ write(input: RunSnapshotWriteInput): Promise<Result<void, PersistenceError>>;
82
+
83
+ read(
84
+ tenantId: TenantId,
85
+ runId: RunId,
86
+ ): Promise<Result<RunSnapshotRecord | null, PersistenceError>>;
87
+ }
package/src/schema.ts ADDED
@@ -0,0 +1,154 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import { sql } from 'drizzle-orm';
5
+ import { index, integer, jsonb, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
6
+
7
+ /**
8
+ * Agents schema — first-class conversation primitive.
9
+ *
10
+ * A conversation is a durable thread grouping many agent invocations
11
+ * into one user-visible unit. Messages inside a conversation live as
12
+ * memory facts of type `agent-message`, scoped to
13
+ * `threadId = <conversation id>`. This split lets conversation
14
+ * lifecycle (open/close, list, cheap turn-count updates) use a real
15
+ * table while message content inherits memory's versioning +
16
+ * retention + RLS + search infrastructure.
17
+ *
18
+ * `tenant_id` on every table; implementations enable row-level
19
+ * security on `AGENTS_TENANT_SCOPED_TABLES` and run every statement in
20
+ * a tenant-scoped connection so the RLS predicate has context.
21
+ */
22
+
23
+ export const agentConversations = pgTable(
24
+ 'agent_conversations',
25
+ {
26
+ id: uuid('id').primaryKey().defaultRandom(),
27
+ tenantId: uuid('tenant_id').notNull(),
28
+ /**
29
+ * Agent id + version this conversation was opened with. Resuming
30
+ * with a different version is refused — the agent's semantics may
31
+ * have changed.
32
+ */
33
+ agentId: text('agent_id').notNull(),
34
+ agentVersion: text('agent_version').notNull(),
35
+ /**
36
+ * Human-facing title. Auto-generated on first turn (typically from
37
+ * the initial user message) or explicitly set. Not versioned — the
38
+ * latest string wins.
39
+ */
40
+ title: text('title').notNull(),
41
+ /**
42
+ * Optional identifier for the human participant (email, user id,
43
+ * SSO subject). Kept as text so the caller decides the shape.
44
+ */
45
+ participantId: text('participant_id'),
46
+ /**
47
+ * Structural scope — project id, matter id, etc. Persisted through
48
+ * the versioning envelope so shape evolution is migrate-on-read.
49
+ */
50
+ scope: jsonb('scope').notNull(),
51
+ openedAt: timestamp('opened_at', { withTimezone: true }).notNull().defaultNow(),
52
+ /** Set when the conversation is closed. Closed conversations are read-only. */
53
+ closedAt: timestamp('closed_at', { withTimezone: true }),
54
+ /**
55
+ * Denormalized turn counter — incremented by `appendMessage` for
56
+ * each turn's final (non-intermediate) agent message. Read by the
57
+ * session HITL gate and the UI conversation list.
58
+ */
59
+ turnCount: integer('turn_count').notNull().default(0),
60
+ /**
61
+ * Denormalized last-activity timestamp. Kept in sync by
62
+ * `appendMessage`. Enables cheap sort by recency in the UI list.
63
+ */
64
+ lastMessageAt: timestamp('last_message_at', { withTimezone: true }),
65
+ /**
66
+ * Free-form metadata (participant details, feature flags,
67
+ * conversation-level notes). Versioned envelope.
68
+ */
69
+ metadata: jsonb('metadata'),
70
+ },
71
+ (t) => ({
72
+ tenantAgentIdx: index('agent_conversations_tenant_agent_idx').on(t.tenantId, t.agentId),
73
+ tenantParticipantIdx: index('agent_conversations_tenant_participant_idx').on(
74
+ t.tenantId,
75
+ t.participantId,
76
+ ),
77
+ /** Partial index: open conversations sorted by recency. Used by the UI list. */
78
+ openRecentIdx: index('agent_conversations_open_recent_idx')
79
+ .on(t.tenantId, t.lastMessageAt.desc())
80
+ .where(sql`${t.closedAt} IS NULL`),
81
+ }),
82
+ );
83
+
84
+ export type AgentConversationRow = typeof agentConversations.$inferSelect;
85
+ export type NewAgentConversationRow = typeof agentConversations.$inferInsert;
86
+
87
+ /**
88
+ * `agent_run_snapshots` — reconstruction envelope for a suspended agent
89
+ * turn. Written by the setup handler on the FIRST execution of a run
90
+ * (idempotent via the runId primary key); used by
91
+ * `resumeAgentTurn(runId)` to rebuild the same `InvokeAgentInput` the
92
+ * turn was invoked with — so kernel replay after a HITL waitpoint
93
+ * resolution lands in an identical context.
94
+ *
95
+ * Owned by @kindgi/agents (not the kernel) so the kernel's
96
+ * schema stays generic — the kernel has no notion of agents,
97
+ * conversations, or participant identities.
98
+ *
99
+ * Idempotent insert: `runId` is the PK; implementations insert with
100
+ * `ON CONFLICT (id) DO NOTHING` (or equivalent) so a replay is a
101
+ * no-op. All fields are set at first execution and never modified.
102
+ */
103
+ export const agentRunSnapshots = pgTable(
104
+ 'agent_run_snapshots',
105
+ {
106
+ /** `runId` — the kernel run this snapshot reconstructs. Primary key. */
107
+ id: uuid('id').primaryKey(),
108
+ tenantId: uuid('tenant_id').notNull(),
109
+ projectId: uuid('project_id').notNull(),
110
+ /** Agent id + version pinned at turn start. Same as agent_conversations. */
111
+ agentId: text('agent_id').notNull(),
112
+ agentVersion: text('agent_version').notNull(),
113
+ /** Conversation this turn belongs to — required for the setup handler to load state. */
114
+ conversationId: uuid('conversation_id').notNull(),
115
+ /** The user message that started this turn. */
116
+ userMessage: text('user_message').notNull(),
117
+ /**
118
+ * The turn's prompt parameters and structured input, as given to
119
+ * `invokeAgent`. A resumed turn renders its instructions with the
120
+ * same values. Versioned envelopes, like `principal`.
121
+ */
122
+ parameters: jsonb('parameters'),
123
+ input: jsonb('input'),
124
+ /** Optional participant identifier — mirrors InvokeAgentInput.participantId. */
125
+ participantId: text('participant_id'),
126
+ /** dryRun marker from the original invocation. Affects handler side-effect behavior. */
127
+ dryRun: integer('dry_run').notNull().default(0),
128
+ /**
129
+ * Optional serialized Principal (authorization) from the original
130
+ * invocation. Threaded back into the resumed TurnContext so tool
131
+ * dispatch stays enforcement-consistent. Versioned envelope so shape
132
+ * evolution is migrate-on-read.
133
+ */
134
+ principal: jsonb('principal'),
135
+ /**
136
+ * Optional authz config (`{fgaApiUrl}`) from the original invocation.
137
+ * Threaded back into the resumed TurnContext.
138
+ */
139
+ authz: jsonb('authz'),
140
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
141
+ },
142
+ (t) => ({
143
+ tenantIdx: index('agent_run_snapshots_tenant_idx').on(t.tenantId),
144
+ }),
145
+ );
146
+
147
+ export type AgentRunSnapshotRow = typeof agentRunSnapshots.$inferSelect;
148
+ export type NewAgentRunSnapshotRow = typeof agentRunSnapshots.$inferInsert;
149
+
150
+ /**
151
+ * Tables that carry `tenant_id`; register them with the migration
152
+ * runner so row-level security gets enabled on them.
153
+ */
154
+ export const AGENTS_TENANT_SCOPED_TABLES = ['agent_conversations', 'agent_run_snapshots'] as const;
@@ -0,0 +1,153 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { EvaluationResult } from '@kindgi/guardrails';
5
+ import type { Semver } from '@kindgi/types';
6
+
7
+ import type { AgentId, ConversationId, ConversationMessage, RetrievedFact } from './types.js';
8
+
9
+ /**
10
+ * Discriminated union of every event a turn emits during execution.
11
+ * Callers wire `bindings.onEvent` to their transport (SSE, WebSocket,
12
+ * background job queue, or nothing at all).
13
+ *
14
+ * Order is deterministic per turn: `turn.started` first, `turn.completed`
15
+ * or `turn.failed` last. Intermediate events fire in the order the
16
+ * corresponding action happens.
17
+ */
18
+ export type TurnEvent =
19
+ | TurnStartedEvent
20
+ | RetrievalCompletedEvent
21
+ | ModelCallStartedEvent
22
+ | ModelCallCompletedEvent
23
+ | ToolStartedEvent
24
+ | ToolCompletedEvent
25
+ | ToolFailedEvent
26
+ | AgentMessageEvent
27
+ | GuardrailViolatedEvent
28
+ | TurnCompletedEvent
29
+ | TurnFailedEvent;
30
+
31
+ export interface TurnStartedEvent {
32
+ readonly kind: 'turn.started';
33
+ readonly conversationId: ConversationId;
34
+ readonly turnNumber: number;
35
+ readonly agentId: AgentId;
36
+ readonly agentVersion: Semver;
37
+ readonly userMessage: string;
38
+ }
39
+
40
+ export interface RetrievalCompletedEvent {
41
+ readonly kind: 'retrieval.completed';
42
+ readonly count: number;
43
+ /** Fact ids retrieved this turn. Empty when no intents declared. */
44
+ readonly factIds: readonly string[];
45
+ }
46
+
47
+ export interface ModelCallStartedEvent {
48
+ readonly kind: 'model.call.started';
49
+ readonly step: number;
50
+ readonly providerId: string;
51
+ readonly model: string;
52
+ }
53
+
54
+ export interface ModelCallCompletedEvent {
55
+ readonly kind: 'model.call.completed';
56
+ readonly step: number;
57
+ readonly finishReason: 'stop' | 'length' | 'tool-use' | 'content-filter' | 'error';
58
+ readonly promptTokens: number;
59
+ readonly completionTokens: number;
60
+ readonly costUsd: number;
61
+ readonly durationMs: number;
62
+ }
63
+
64
+ export interface ToolStartedEvent {
65
+ readonly kind: 'tool.started';
66
+ readonly step: number;
67
+ readonly toolId: string;
68
+ /**
69
+ * The exact version dispatched (picked at run start by `resolve(id,
70
+ * range)` on the tenant's tool registry) so subscribers can
71
+ * distinguish `kb-search@1.2.3` from `kb-search@2.0.0`.
72
+ */
73
+ readonly toolVersion: string;
74
+ /** The semver range the agent binding declared for this tool. */
75
+ readonly toolVersionRange: string;
76
+ readonly invocationId: string;
77
+ readonly arguments: Readonly<Record<string, unknown>>;
78
+ }
79
+
80
+ export interface ToolCompletedEvent {
81
+ readonly kind: 'tool.completed';
82
+ readonly step: number;
83
+ readonly toolId: string;
84
+ readonly toolVersion: string;
85
+ readonly toolVersionRange: string;
86
+ readonly invocationId: string;
87
+ readonly output: unknown;
88
+ readonly durationMs: number;
89
+ }
90
+
91
+ export interface ToolFailedEvent {
92
+ readonly kind: 'tool.failed';
93
+ readonly step: number;
94
+ readonly toolId: string;
95
+ readonly invocationId: string;
96
+ readonly error: { readonly code: string; readonly message: string };
97
+ }
98
+
99
+ export interface AgentMessageEvent {
100
+ readonly kind: 'agent.message';
101
+ readonly step: number;
102
+ /** True when this is the final message that closes the turn. */
103
+ readonly isFinal: boolean;
104
+ readonly message: ConversationMessage;
105
+ }
106
+
107
+ export interface GuardrailViolatedEvent {
108
+ readonly kind: 'guardrail.violated';
109
+ readonly action: EvaluationResult['action'];
110
+ readonly severity: EvaluationResult['severity'];
111
+ readonly guardrailId: string;
112
+ readonly reason?: string;
113
+ }
114
+
115
+ export interface TurnCompletedEvent {
116
+ readonly kind: 'turn.completed';
117
+ readonly conversationId: ConversationId;
118
+ readonly turnNumber: number;
119
+ readonly response: ConversationMessage;
120
+ readonly retrieved: readonly RetrievedFact[];
121
+ readonly durationMs: number;
122
+ readonly totalCostUsd: number;
123
+ }
124
+
125
+ export interface TurnFailedEvent {
126
+ readonly kind: 'turn.failed';
127
+ readonly conversationId: ConversationId;
128
+ readonly errorCode: string;
129
+ readonly message: string;
130
+ }
131
+
132
+ /**
133
+ * Adapter shape callers plug into `bindings.onEvent`. Sync + async both
134
+ * supported. Errors thrown inside the handler are caught by the runtime
135
+ * — a broken telemetry sink never breaks the turn.
136
+ */
137
+ export type OnTurnEvent = (event: TurnEvent) => void | Promise<void>;
138
+
139
+ /**
140
+ * Safe emit wrapper. Called by the turn's handlers at each observation
141
+ * point; catches handler throws so a broken sink can't break the run.
142
+ */
143
+ export async function emitTurnEvent(
144
+ onEvent: OnTurnEvent | undefined,
145
+ event: TurnEvent,
146
+ ): Promise<void> {
147
+ if (onEvent === undefined) return;
148
+ try {
149
+ await onEvent(event);
150
+ } catch {
151
+ // Intentionally swallow — telemetry sinks must not affect run outcome.
152
+ }
153
+ }
@@ -0,0 +1,78 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { TenantPolicy } from '@kindgi/capabilities';
5
+
6
+ type AllowDeny = { readonly allow?: readonly string[]; readonly deny?: readonly string[] };
7
+
8
+ /**
9
+ * Combine two tenant policies (the statically bound one and one derived
10
+ * from the policy registry) so the result is at least as strict as each:
11
+ *
12
+ * - allow lists (`providers.allow`, `models.allow`, `regionAllow`) →
13
+ * INTERSECTION when both set one; a list set on one side only applies
14
+ * as-is. An empty result allows nothing (the router fails closed).
15
+ * - deny lists (`providers.deny`, `models.deny`) → UNION.
16
+ * - `maxCostPerCallUsd` / `maxTokensPerCall` → the smaller value.
17
+ *
18
+ * Returns `undefined` when both are undefined (routing runs with no
19
+ * tenant policy).
20
+ */
21
+ export function mergeTenantPolicies(
22
+ a: TenantPolicy | undefined,
23
+ b: TenantPolicy | undefined,
24
+ ): TenantPolicy | undefined {
25
+ if (a === undefined) return b;
26
+ if (b === undefined) return a;
27
+ const providers = mergeAllowDeny(a.providers, b.providers);
28
+ const models = mergeAllowDeny(a.models, b.models);
29
+ const regionAllow = intersectStrings(a.regionAllow, b.regionAllow);
30
+ const maxCost = minDefined(a.maxCostPerCallUsd, b.maxCostPerCallUsd);
31
+ const maxTokens = minDefined(a.maxTokensPerCall, b.maxTokensPerCall);
32
+ const merged: { -readonly [K in keyof TenantPolicy]: TenantPolicy[K] } = {
33
+ tenantId: a.tenantId,
34
+ };
35
+ if (providers !== undefined) merged.providers = providers;
36
+ if (models !== undefined) merged.models = models;
37
+ if (regionAllow !== undefined) merged.regionAllow = regionAllow;
38
+ if (maxCost !== undefined) merged.maxCostPerCallUsd = maxCost;
39
+ if (maxTokens !== undefined) merged.maxTokensPerCall = maxTokens;
40
+ return merged;
41
+ }
42
+
43
+ function mergeAllowDeny(a: AllowDeny | undefined, b: AllowDeny | undefined): AllowDeny | undefined {
44
+ if (a === undefined) return b;
45
+ if (b === undefined) return a;
46
+ const allow = intersectStrings(a.allow, b.allow);
47
+ const deny = unionStrings(a.deny, b.deny);
48
+ if (allow === undefined && deny === undefined) return undefined;
49
+ return {
50
+ ...(allow !== undefined && { allow }),
51
+ ...(deny !== undefined && { deny }),
52
+ };
53
+ }
54
+
55
+ function intersectStrings(
56
+ a: readonly string[] | undefined,
57
+ b: readonly string[] | undefined,
58
+ ): readonly string[] | undefined {
59
+ if (a === undefined) return b;
60
+ if (b === undefined) return a;
61
+ const inB = new Set(b);
62
+ return Array.from(new Set(a)).filter((v) => inB.has(v));
63
+ }
64
+
65
+ function unionStrings(
66
+ a: readonly string[] | undefined,
67
+ b: readonly string[] | undefined,
68
+ ): readonly string[] | undefined {
69
+ if (a === undefined) return b;
70
+ if (b === undefined) return a;
71
+ return Array.from(new Set([...a, ...b]));
72
+ }
73
+
74
+ function minDefined(a: number | undefined, b: number | undefined): number | undefined {
75
+ if (a === undefined) return b;
76
+ if (b === undefined) return a;
77
+ return Math.min(a, b);
78
+ }