@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.
- package/LICENSE +201 -0
- package/README.md +131 -2
- package/dist/agent-turn-flow.d.ts +57 -0
- package/dist/agent-turn-flow.d.ts.map +1 -0
- package/dist/agent-turn-flow.js +172 -0
- package/dist/agent-turn-flow.js.map +1 -0
- package/dist/conversation-binding.d.ts +111 -0
- package/dist/conversation-binding.d.ts.map +1 -0
- package/dist/conversation-binding.js +4 -0
- package/dist/conversation-binding.js.map +1 -0
- package/dist/define.d.ts +180 -0
- package/dist/define.d.ts.map +1 -0
- package/dist/define.js +361 -0
- package/dist/define.js.map +1 -0
- package/dist/errors.d.ts +62 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +4 -0
- package/dist/errors.js.map +1 -0
- package/dist/guardrails-gate.d.ts +169 -0
- package/dist/guardrails-gate.d.ts.map +1 -0
- package/dist/guardrails-gate.js +202 -0
- package/dist/guardrails-gate.js.map +1 -0
- package/dist/handlers/budget-check.d.ts +22 -0
- package/dist/handlers/budget-check.d.ts.map +1 -0
- package/dist/handlers/budget-check.js +109 -0
- package/dist/handlers/budget-check.js.map +1 -0
- package/dist/handlers/build-initial-messages.d.ts +17 -0
- package/dist/handlers/build-initial-messages.d.ts.map +1 -0
- package/dist/handlers/build-initial-messages.js +86 -0
- package/dist/handlers/build-initial-messages.js.map +1 -0
- package/dist/handlers/compose-result.d.ts +10 -0
- package/dist/handlers/compose-result.d.ts.map +1 -0
- package/dist/handlers/compose-result.js +79 -0
- package/dist/handlers/compose-result.js.map +1 -0
- package/dist/handlers/constants.d.ts +8 -0
- package/dist/handlers/constants.d.ts.map +1 -0
- package/dist/handlers/constants.js +10 -0
- package/dist/handlers/constants.js.map +1 -0
- package/dist/handlers/context.d.ts +210 -0
- package/dist/handlers/context.d.ts.map +1 -0
- package/dist/handlers/context.js +4 -0
- package/dist/handlers/context.js.map +1 -0
- package/dist/handlers/dispatch-tools.d.ts +15 -0
- package/dist/handlers/dispatch-tools.d.ts.map +1 -0
- package/dist/handlers/dispatch-tools.js +511 -0
- package/dist/handlers/dispatch-tools.js.map +1 -0
- package/dist/handlers/errors.d.ts +133 -0
- package/dist/handlers/errors.d.ts.map +1 -0
- package/dist/handlers/errors.js +134 -0
- package/dist/handlers/errors.js.map +1 -0
- package/dist/handlers/evaluate-guardrails.d.ts +14 -0
- package/dist/handlers/evaluate-guardrails.d.ts.map +1 -0
- package/dist/handlers/evaluate-guardrails.js +128 -0
- package/dist/handlers/evaluate-guardrails.js.map +1 -0
- package/dist/handlers/final-iteration.d.ts +7 -0
- package/dist/handlers/final-iteration.d.ts.map +1 -0
- package/dist/handlers/final-iteration.js +26 -0
- package/dist/handlers/final-iteration.js.map +1 -0
- package/dist/handlers/index.d.ts +5 -0
- package/dist/handlers/index.d.ts.map +1 -0
- package/dist/handlers/index.js +41 -0
- package/dist/handlers/index.js.map +1 -0
- package/dist/handlers/model-call.d.ts +13 -0
- package/dist/handlers/model-call.d.ts.map +1 -0
- package/dist/handlers/model-call.js +136 -0
- package/dist/handlers/model-call.js.map +1 -0
- package/dist/handlers/persist-final-message.d.ts +14 -0
- package/dist/handlers/persist-final-message.d.ts.map +1 -0
- package/dist/handlers/persist-final-message.js +54 -0
- package/dist/handlers/persist-final-message.js.map +1 -0
- package/dist/handlers/persist-provenance.d.ts +12 -0
- package/dist/handlers/persist-provenance.d.ts.map +1 -0
- package/dist/handlers/persist-provenance.js +34 -0
- package/dist/handlers/persist-provenance.js.map +1 -0
- package/dist/handlers/persist-user-message.d.ts +15 -0
- package/dist/handlers/persist-user-message.d.ts.map +1 -0
- package/dist/handlers/persist-user-message.js +59 -0
- package/dist/handlers/persist-user-message.js.map +1 -0
- package/dist/handlers/public-types.d.ts +201 -0
- package/dist/handlers/public-types.d.ts.map +1 -0
- package/dist/handlers/public-types.js +4 -0
- package/dist/handlers/public-types.js.map +1 -0
- package/dist/handlers/rehydrate.d.ts +8 -0
- package/dist/handlers/rehydrate.d.ts.map +1 -0
- package/dist/handlers/rehydrate.js +94 -0
- package/dist/handlers/rehydrate.js.map +1 -0
- package/dist/handlers/render-prompt.d.ts +12 -0
- package/dist/handlers/render-prompt.d.ts.map +1 -0
- package/dist/handlers/render-prompt.js +41 -0
- package/dist/handlers/render-prompt.js.map +1 -0
- package/dist/handlers/resolve-tools.d.ts +10 -0
- package/dist/handlers/resolve-tools.d.ts.map +1 -0
- package/dist/handlers/resolve-tools.js +55 -0
- package/dist/handlers/resolve-tools.js.map +1 -0
- package/dist/handlers/result-shape.d.ts +94 -0
- package/dist/handlers/result-shape.d.ts.map +1 -0
- package/dist/handlers/result-shape.js +19 -0
- package/dist/handlers/result-shape.js.map +1 -0
- package/dist/handlers/run-retrievals.d.ts +10 -0
- package/dist/handlers/run-retrievals.d.ts.map +1 -0
- package/dist/handlers/run-retrievals.js +64 -0
- package/dist/handlers/run-retrievals.js.map +1 -0
- package/dist/handlers/run-snapshot.d.ts +4 -0
- package/dist/handlers/run-snapshot.d.ts.map +1 -0
- package/dist/handlers/run-snapshot.js +26 -0
- package/dist/handlers/run-snapshot.js.map +1 -0
- package/dist/handlers/setup.d.ts +25 -0
- package/dist/handlers/setup.d.ts.map +1 -0
- package/dist/handlers/setup.js +231 -0
- package/dist/handlers/setup.js.map +1 -0
- package/dist/handlers/structured-output.d.ts +38 -0
- package/dist/handlers/structured-output.d.ts.map +1 -0
- package/dist/handlers/structured-output.js +89 -0
- package/dist/handlers/structured-output.js.map +1 -0
- package/dist/handlers/tool-errors.d.ts +56 -0
- package/dist/handlers/tool-errors.d.ts.map +1 -0
- package/dist/handlers/tool-errors.js +73 -0
- package/dist/handlers/tool-errors.js.map +1 -0
- package/dist/handlers/tool-hitl.d.ts +45 -0
- package/dist/handlers/tool-hitl.d.ts.map +1 -0
- package/dist/handlers/tool-hitl.js +81 -0
- package/dist/handlers/tool-hitl.js.map +1 -0
- package/dist/handlers/turn-environment.d.ts +26 -0
- package/dist/handlers/turn-environment.d.ts.map +1 -0
- package/dist/handlers/turn-environment.js +154 -0
- package/dist/handlers/turn-environment.js.map +1 -0
- package/dist/hitl-policy.d.ts +45 -0
- package/dist/hitl-policy.d.ts.map +1 -0
- package/dist/hitl-policy.js +74 -0
- package/dist/hitl-policy.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/invoke.d.ts +36 -0
- package/dist/invoke.d.ts.map +1 -0
- package/dist/invoke.js +228 -0
- package/dist/invoke.js.map +1 -0
- package/dist/migrations-dir.d.ts +11 -0
- package/dist/migrations-dir.d.ts.map +1 -0
- package/dist/migrations-dir.js +14 -0
- package/dist/migrations-dir.js.map +1 -0
- package/dist/project-run-result.d.ts +23 -0
- package/dist/project-run-result.d.ts.map +1 -0
- package/dist/project-run-result.js +116 -0
- package/dist/project-run-result.js.map +1 -0
- package/dist/prompt.d.ts +83 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +119 -0
- package/dist/prompt.js.map +1 -0
- package/dist/provenance-emit.d.ts +44 -0
- package/dist/provenance-emit.d.ts.map +1 -0
- package/dist/provenance-emit.js +51 -0
- package/dist/provenance-emit.js.map +1 -0
- package/dist/registry.d.ts +38 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +125 -0
- package/dist/registry.js.map +1 -0
- package/dist/retrieval.d.ts +47 -0
- package/dist/retrieval.d.ts.map +1 -0
- package/dist/retrieval.js +155 -0
- package/dist/retrieval.js.map +1 -0
- package/dist/run-snapshot-binding.d.ts +77 -0
- package/dist/run-snapshot-binding.d.ts.map +1 -0
- package/dist/run-snapshot-binding.js +4 -0
- package/dist/run-snapshot-binding.js.map +1 -0
- package/dist/schema.d.ts +497 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +133 -0
- package/dist/schema.js.map +1 -0
- package/dist/streaming.d.ts +118 -0
- package/dist/streaming.d.ts.map +1 -0
- package/dist/streaming.js +17 -0
- package/dist/streaming.js.map +1 -0
- package/dist/tenant-policy.d.ts +16 -0
- package/dist/tenant-policy.d.ts.map +1 -0
- package/dist/tenant-policy.js +77 -0
- package/dist/tenant-policy.js.map +1 -0
- package/dist/types.d.ts +435 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/versioning.d.ts +29 -0
- package/dist/versioning.d.ts.map +1 -0
- package/dist/versioning.js +58 -0
- package/dist/versioning.js.map +1 -0
- package/migrations/0000_sparkling_talkback.sql +18 -0
- package/migrations/0001_tired_warhawk.sql +16 -0
- package/migrations/0002_violet_ezekiel.sql +2 -0
- package/migrations/meta/0000_snapshot.json +172 -0
- package/migrations/meta/0001_snapshot.json +275 -0
- package/migrations/meta/0002_snapshot.json +287 -0
- package/migrations/meta/_journal.json +27 -0
- package/package.json +76 -4
- package/src/agent-turn-flow.ts +183 -0
- package/src/conversation-binding.ts +147 -0
- package/src/define.ts +572 -0
- package/src/errors.ts +80 -0
- package/src/guardrails-gate.ts +342 -0
- package/src/handlers/budget-check.ts +143 -0
- package/src/handlers/build-initial-messages.ts +103 -0
- package/src/handlers/compose-result.ts +90 -0
- package/src/handlers/constants.ts +10 -0
- package/src/handlers/context.ts +226 -0
- package/src/handlers/dispatch-tools.ts +633 -0
- package/src/handlers/errors.ts +282 -0
- package/src/handlers/evaluate-guardrails.ts +153 -0
- package/src/handlers/final-iteration.ts +30 -0
- package/src/handlers/index.ts +63 -0
- package/src/handlers/model-call.ts +151 -0
- package/src/handlers/persist-final-message.ts +67 -0
- package/src/handlers/persist-provenance.ts +39 -0
- package/src/handlers/persist-user-message.ts +70 -0
- package/src/handlers/public-types.ts +209 -0
- package/src/handlers/rehydrate.ts +161 -0
- package/src/handlers/render-prompt.ts +46 -0
- package/src/handlers/resolve-tools.ts +68 -0
- package/src/handlers/result-shape.ts +113 -0
- package/src/handlers/run-retrievals.ts +77 -0
- package/src/handlers/run-snapshot.ts +44 -0
- package/src/handlers/setup.ts +269 -0
- package/src/handlers/structured-output.ts +117 -0
- package/src/handlers/tool-errors.ts +122 -0
- package/src/handlers/tool-hitl.ts +126 -0
- package/src/handlers/turn-environment.ts +191 -0
- package/src/hitl-policy.ts +128 -0
- package/src/index.ts +154 -0
- package/src/invoke.ts +299 -0
- package/src/migrations-dir.ts +17 -0
- package/src/project-run-result.ts +131 -0
- package/src/prompt.ts +185 -0
- package/src/provenance-emit.ts +100 -0
- package/src/registry.ts +164 -0
- package/src/retrieval.ts +219 -0
- package/src/run-snapshot-binding.ts +87 -0
- package/src/schema.ts +154 -0
- package/src/streaming.ts +153 -0
- package/src/tenant-policy.ts +78 -0
- package/src/types.ts +453 -0
- 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;
|
package/src/streaming.ts
ADDED
|
@@ -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
|
+
}
|