@kindgi/agents 0.1.2 → 0.1.3
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/dist/conversation-binding.d.ts +9 -1
- package/dist/conversation-binding.d.ts.map +1 -1
- package/dist/define.js +20 -0
- package/dist/define.js.map +1 -1
- package/dist/guardrails-gate.d.ts +7 -4
- package/dist/guardrails-gate.d.ts.map +1 -1
- package/dist/guardrails-gate.js +5 -2
- package/dist/guardrails-gate.js.map +1 -1
- package/dist/handlers/context.d.ts +14 -0
- package/dist/handlers/context.d.ts.map +1 -1
- package/dist/handlers/dispatch-tools.d.ts.map +1 -1
- package/dist/handlers/dispatch-tools.js +18 -42
- package/dist/handlers/dispatch-tools.js.map +1 -1
- package/dist/handlers/evaluate-guardrails.d.ts.map +1 -1
- package/dist/handlers/evaluate-guardrails.js +34 -1
- package/dist/handlers/evaluate-guardrails.js.map +1 -1
- package/dist/handlers/gate-decision.d.ts +62 -0
- package/dist/handlers/gate-decision.d.ts.map +1 -0
- package/dist/handlers/gate-decision.js +55 -0
- package/dist/handlers/gate-decision.js.map +1 -0
- package/dist/handlers/model-call.d.ts +10 -0
- package/dist/handlers/model-call.d.ts.map +1 -1
- package/dist/handlers/model-call.js +109 -34
- package/dist/handlers/model-call.js.map +1 -1
- package/dist/handlers/persist-provenance.d.ts.map +1 -1
- package/dist/handlers/persist-provenance.js +3 -1
- package/dist/handlers/persist-provenance.js.map +1 -1
- package/dist/handlers/persist-user-message.d.ts.map +1 -1
- package/dist/handlers/persist-user-message.js +5 -16
- package/dist/handlers/persist-user-message.js.map +1 -1
- package/dist/handlers/public-types.d.ts +16 -2
- package/dist/handlers/public-types.d.ts.map +1 -1
- package/dist/handlers/rehydrate.d.ts.map +1 -1
- package/dist/handlers/rehydrate.js +85 -0
- package/dist/handlers/rehydrate.js.map +1 -1
- package/dist/handlers/run-retrievals.d.ts.map +1 -1
- package/dist/handlers/run-retrievals.js +2 -19
- package/dist/handlers/run-retrievals.js.map +1 -1
- package/dist/handlers/setup.d.ts.map +1 -1
- package/dist/handlers/setup.js +15 -10
- package/dist/handlers/setup.js.map +1 -1
- package/dist/handlers/tool-hitl.d.ts +5 -9
- package/dist/handlers/tool-hitl.d.ts.map +1 -1
- package/dist/handlers/tool-hitl.js +5 -0
- package/dist/handlers/tool-hitl.js.map +1 -1
- package/dist/handlers/turn-provenance.d.ts +67 -0
- package/dist/handlers/turn-provenance.d.ts.map +1 -0
- package/dist/handlers/turn-provenance.js +160 -0
- package/dist/handlers/turn-provenance.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/invoke.d.ts +14 -1
- package/dist/invoke.d.ts.map +1 -1
- package/dist/invoke.js +34 -19
- package/dist/invoke.js.map +1 -1
- package/dist/provenance-emit.d.ts +4 -2
- package/dist/provenance-emit.d.ts.map +1 -1
- package/dist/provenance-emit.js +4 -2
- package/dist/provenance-emit.js.map +1 -1
- package/dist/schema.d.ts +17 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +11 -0
- package/dist/schema.js.map +1 -1
- package/dist/types.d.ts +15 -1
- package/dist/types.d.ts.map +1 -1
- package/migrations/0003_hesitant_captain_cross.sql +2 -0
- package/migrations/meta/0003_snapshot.json +321 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +15 -15
- package/src/conversation-binding.ts +9 -1
- package/src/define.ts +20 -0
- package/src/guardrails-gate.ts +17 -3
- package/src/handlers/context.ts +14 -0
- package/src/handlers/dispatch-tools.ts +21 -43
- package/src/handlers/evaluate-guardrails.ts +39 -1
- package/src/handlers/gate-decision.ts +101 -0
- package/src/handlers/model-call.ts +149 -33
- package/src/handlers/persist-provenance.ts +3 -1
- package/src/handlers/persist-user-message.ts +3 -16
- package/src/handlers/public-types.ts +16 -2
- package/src/handlers/rehydrate.ts +120 -3
- package/src/handlers/run-retrievals.ts +2 -19
- package/src/handlers/setup.ts +17 -20
- package/src/handlers/tool-hitl.ts +6 -10
- package/src/handlers/turn-provenance.ts +230 -0
- package/src/index.ts +7 -0
- package/src/invoke.ts +46 -20
- package/src/provenance-emit.ts +4 -1
- package/src/schema.ts +11 -0
- package/src/types.ts +15 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kindgi/agents",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Agent primitive for Kindgi. Composes capabilities (model routing), tools (execution), memory (facts + retrieval), guardrails (declarative guardrails), and provenance into a single declarative unit. Agents are data-as-code — a defineAgent() call produces a durable definition that runs on the kernel, with first-class multi-turn conversations, streaming output, and a provenance record per turn.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -31,20 +31,20 @@
|
|
|
31
31
|
"type-manifest.json"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@kindgi/authz": "0.1.
|
|
35
|
-
"@kindgi/capabilities": "0.1.
|
|
36
|
-
"@kindgi/compliance": "0.1.
|
|
37
|
-
"@kindgi/embedding": "0.1.
|
|
38
|
-
"@kindgi/flow": "0.1.
|
|
39
|
-
"@kindgi/handler": "0.1.
|
|
40
|
-
"@kindgi/guardrails": "0.1.
|
|
41
|
-
"@kindgi/memory": "0.1.
|
|
42
|
-
"@kindgi/policy-contract": "0.1.
|
|
43
|
-
"@kindgi/provenance": "0.1.
|
|
44
|
-
"@kindgi/runtime": "0.1.
|
|
45
|
-
"@kindgi/schema": "0.1.
|
|
46
|
-
"@kindgi/tools": "0.1.
|
|
47
|
-
"@kindgi/types": "0.1.
|
|
34
|
+
"@kindgi/authz": "0.1.3",
|
|
35
|
+
"@kindgi/capabilities": "0.1.3",
|
|
36
|
+
"@kindgi/compliance": "0.1.3",
|
|
37
|
+
"@kindgi/embedding": "0.1.3",
|
|
38
|
+
"@kindgi/flow": "0.1.3",
|
|
39
|
+
"@kindgi/handler": "0.1.3",
|
|
40
|
+
"@kindgi/guardrails": "0.1.3",
|
|
41
|
+
"@kindgi/memory": "0.1.3",
|
|
42
|
+
"@kindgi/policy-contract": "0.1.3",
|
|
43
|
+
"@kindgi/provenance": "0.1.3",
|
|
44
|
+
"@kindgi/runtime": "0.1.3",
|
|
45
|
+
"@kindgi/schema": "0.1.3",
|
|
46
|
+
"@kindgi/tools": "0.1.3",
|
|
47
|
+
"@kindgi/types": "0.1.3",
|
|
48
48
|
"drizzle-orm": "^0.45.3",
|
|
49
49
|
"liquidjs": "^10.29.0"
|
|
50
50
|
},
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright (C) 2026 Kindgi Inc.
|
|
3
3
|
|
|
4
4
|
import type { RunBinding } from '@kindgi/runtime';
|
|
5
|
-
import type { Result, Semver, TenantId } from '@kindgi/types';
|
|
5
|
+
import type { ListScope, ProjectId, Result, Semver, TenantId } from '@kindgi/types';
|
|
6
6
|
|
|
7
7
|
import type { AgentError } from './errors.js';
|
|
8
8
|
import type {
|
|
@@ -21,6 +21,8 @@ export interface OpenConversationInput {
|
|
|
21
21
|
readonly agentVersion: Semver;
|
|
22
22
|
readonly title: string;
|
|
23
23
|
readonly participantId?: string;
|
|
24
|
+
/** The project the conversation is in: its turns' project. Lists filter by it. */
|
|
25
|
+
readonly projectId?: ProjectId;
|
|
24
26
|
readonly scope: Readonly<Record<string, unknown>>;
|
|
25
27
|
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
26
28
|
}
|
|
@@ -72,6 +74,12 @@ export interface ConversationPageCursor {
|
|
|
72
74
|
|
|
73
75
|
export interface ListConversationsPageInput {
|
|
74
76
|
readonly tenantId: TenantId;
|
|
77
|
+
/**
|
|
78
|
+
* Only one project's conversations, or every project's in an org.
|
|
79
|
+
* Absent: the tenant's. A conversation opened without a project is
|
|
80
|
+
* listed only without a scope.
|
|
81
|
+
*/
|
|
82
|
+
readonly scope?: ListScope;
|
|
75
83
|
readonly agentId?: AgentId;
|
|
76
84
|
/** When set, filter to open (no closedAt) or closed (closedAt set) rows only. */
|
|
77
85
|
readonly status?: 'open' | 'closed';
|
package/src/define.ts
CHANGED
|
@@ -448,6 +448,7 @@ function validateConversationPolicy(policy?: ConversationPolicy): Issue[] {
|
|
|
448
448
|
message: 'hitl.timeoutMs must be a positive integer (ms)',
|
|
449
449
|
});
|
|
450
450
|
}
|
|
451
|
+
out.push(...validateOnTimeout(h.onTimeout));
|
|
451
452
|
const ROLES: ReadonlySet<string> = new Set(['standard', 'senior', 'admin']);
|
|
452
453
|
if (
|
|
453
454
|
h.defaultReviewerRole !== undefined &&
|
|
@@ -490,6 +491,25 @@ function validateConversationPolicy(policy?: ConversationPolicy): Issue[] {
|
|
|
490
491
|
return out;
|
|
491
492
|
}
|
|
492
493
|
|
|
494
|
+
/**
|
|
495
|
+
* `hitl.onTimeout`: only `'escalate'`, what the runtime does when an
|
|
496
|
+
* approval's time runs out. `'auto-approve'` / `'auto-reject'` aren't
|
|
497
|
+
* implemented, so they're refused instead of accepted and ignored.
|
|
498
|
+
*/
|
|
499
|
+
function validateOnTimeout(onTimeout: unknown): Issue[] {
|
|
500
|
+
if (onTimeout === undefined || onTimeout === 'escalate') return [];
|
|
501
|
+
const path = '/conversationPolicy/hitl/onTimeout';
|
|
502
|
+
if (onTimeout === 'auto-approve' || onTimeout === 'auto-reject') {
|
|
503
|
+
return [
|
|
504
|
+
{
|
|
505
|
+
path,
|
|
506
|
+
message: `hitl.onTimeout '${onTimeout}' isn't supported: an approval that times out escalates one reviewer tier, and at admin it expires (the turn fails with hitl-cancelled). Use 'escalate', or leave it out.`,
|
|
507
|
+
},
|
|
508
|
+
];
|
|
509
|
+
}
|
|
510
|
+
return [{ path, message: "hitl.onTimeout must be 'escalate'" }];
|
|
511
|
+
}
|
|
512
|
+
|
|
493
513
|
type OutputOutcome =
|
|
494
514
|
| { readonly kind: 'none' }
|
|
495
515
|
| { readonly kind: 'ok'; readonly value: AgentOutputSpec }
|
package/src/guardrails-gate.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright (C) 2026 Kindgi Inc.
|
|
3
3
|
|
|
4
|
-
import type { ProviderRegistry, TenantPolicy } from '@kindgi/capabilities';
|
|
4
|
+
import type { ProviderRegistry, TenantPolicy, UsageSink } from '@kindgi/capabilities';
|
|
5
5
|
import type { ComplianceProvider } from '@kindgi/compliance';
|
|
6
6
|
import {
|
|
7
7
|
type CheckRegistry,
|
|
@@ -15,7 +15,15 @@ import {
|
|
|
15
15
|
type ToolResultRecord,
|
|
16
16
|
evaluateAll,
|
|
17
17
|
} from '@kindgi/guardrails';
|
|
18
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
AgentId,
|
|
20
|
+
GuardrailId,
|
|
21
|
+
OrgId,
|
|
22
|
+
ProjectId,
|
|
23
|
+
Result,
|
|
24
|
+
RunId,
|
|
25
|
+
TenantId,
|
|
26
|
+
} from '@kindgi/types';
|
|
19
27
|
|
|
20
28
|
import type { Agent, ConversationId, ConversationMessage } from './types.js';
|
|
21
29
|
|
|
@@ -66,6 +74,8 @@ export function buildRunTrace(input: {
|
|
|
66
74
|
readonly tenantId: TenantId;
|
|
67
75
|
/** Lets the engine record compliance evidence for a failed check. */
|
|
68
76
|
readonly projectId: ProjectId;
|
|
77
|
+
/** The project's org, when it has one. */
|
|
78
|
+
readonly orgId?: OrgId;
|
|
69
79
|
readonly conversationId: ConversationId;
|
|
70
80
|
/** 1-based number of this turn in the conversation. */
|
|
71
81
|
readonly turnNumber: number;
|
|
@@ -133,6 +143,7 @@ export function buildRunTrace(input: {
|
|
|
133
143
|
runId: input.runId,
|
|
134
144
|
tenantId: input.tenantId,
|
|
135
145
|
projectId: input.projectId,
|
|
146
|
+
...(input.orgId !== undefined && { orgId: input.orgId }),
|
|
136
147
|
agentId: input.agent.id as unknown as AgentId,
|
|
137
148
|
output,
|
|
138
149
|
toolCalls,
|
|
@@ -179,7 +190,8 @@ export function resolveGuardrails(
|
|
|
179
190
|
* doesn't halt on its own. `tenantPolicy` (the policy the turn was
|
|
180
191
|
* routed under) also governs which models llm-judge guardrails may use;
|
|
181
192
|
* `abortSignal` (the turn's) reaches every check, so a slow judge or
|
|
182
|
-
* pack check stops when the turn does.
|
|
193
|
+
* pack check stops when the turn does. `usage` (the turn's sink) records
|
|
194
|
+
* every llm-judge call, as the turn's own model calls are.
|
|
183
195
|
*/
|
|
184
196
|
export async function evaluateGate(
|
|
185
197
|
guardrails: readonly Guardrail[],
|
|
@@ -187,6 +199,7 @@ export async function evaluateGate(
|
|
|
187
199
|
bindings: GuardrailsBindings,
|
|
188
200
|
tenantPolicy?: TenantPolicy,
|
|
189
201
|
abortSignal?: AbortSignal,
|
|
202
|
+
usage?: UsageSink,
|
|
190
203
|
): Promise<readonly EvaluationOutcome[]> {
|
|
191
204
|
if (guardrails.length === 0 || bindings.checks === undefined) return [];
|
|
192
205
|
const evalBindings: EvaluationBindings = {
|
|
@@ -194,6 +207,7 @@ export async function evaluateGate(
|
|
|
194
207
|
...(bindings.compliance !== undefined && { compliance: bindings.compliance }),
|
|
195
208
|
...(tenantPolicy !== undefined && { tenantPolicy }),
|
|
196
209
|
...(abortSignal !== undefined && { abortSignal }),
|
|
210
|
+
...(usage !== undefined && { usage }),
|
|
197
211
|
};
|
|
198
212
|
return await evaluateAll(guardrails, bindings.checks, trace, evalBindings);
|
|
199
213
|
}
|
package/src/handlers/context.ts
CHANGED
|
@@ -143,6 +143,20 @@ export interface TurnContext {
|
|
|
143
143
|
* Present only when the builder was wired AND persistence succeeded.
|
|
144
144
|
*/
|
|
145
145
|
persistedProvenance?: import('@kindgi/provenance').Provenance;
|
|
146
|
+
/**
|
|
147
|
+
* This turn's tool results so far, by invocation id: a model call read
|
|
148
|
+
* them all (they're in its input), so its provenance node is
|
|
149
|
+
* `influenced-by` each. Filled by `dispatch-tools`, and by the rebuild
|
|
150
|
+
* of a resumed turn.
|
|
151
|
+
*/
|
|
152
|
+
toolResultIds?: string[];
|
|
153
|
+
/**
|
|
154
|
+
* The tool-call approvals this turn's journal shows decided, by
|
|
155
|
+
* invocation id: each call's provenance shows the approval it waited on.
|
|
156
|
+
* Populated by `rehydrateTurnContext` (a decision only reaches a turn
|
|
157
|
+
* that parked, and resumes).
|
|
158
|
+
*/
|
|
159
|
+
toolApprovals?: ReadonlyMap<string, import('./turn-provenance.js').ToolApproval>;
|
|
146
160
|
/**
|
|
147
161
|
* The tenant policy this turn's model was routed under (bound policy
|
|
148
162
|
* merged with the policy registry's). Populated by `setup`; guardrail
|
|
@@ -19,13 +19,15 @@ import {
|
|
|
19
19
|
type UnresolvedToolError,
|
|
20
20
|
throwAgentTurnFailure,
|
|
21
21
|
} from './errors.js';
|
|
22
|
+
import { TOOL_CALL_GATE_SUBJECT, readGateDecision } from './gate-decision.js';
|
|
22
23
|
import {
|
|
23
24
|
effectiveToolErrorPolicy,
|
|
24
25
|
toolErrorKindOf,
|
|
25
26
|
toolErrorResult,
|
|
26
27
|
toolRetriesSoFar,
|
|
27
28
|
} from './tool-errors.js';
|
|
28
|
-
import {
|
|
29
|
+
import { TOOL_GATE_RECORD_PREFIX, computeToolCallWaitToken, hashToolArgs } from './tool-hitl.js';
|
|
30
|
+
import { addStepToolNodes } from './turn-provenance.js';
|
|
29
31
|
|
|
30
32
|
/**
|
|
31
33
|
* Resolves the effective HITL mode + reviewer role for a specific
|
|
@@ -111,7 +113,7 @@ async function decideToolGate(
|
|
|
111
113
|
cause: null,
|
|
112
114
|
});
|
|
113
115
|
}
|
|
114
|
-
return kctx.record(
|
|
116
|
+
return kctx.record(`${TOOL_GATE_RECORD_PREFIX}${call.id}`, (): ToolGateRecord | undefined => {
|
|
115
117
|
const resolved = resolveEffectiveToolHitl(effectiveHitl, tool);
|
|
116
118
|
if (resolved.mode === 'never_ask') return undefined;
|
|
117
119
|
const argsHash = hashToolArgs(call.arguments);
|
|
@@ -251,7 +253,8 @@ export function buildDispatchToolsHandler(ctx: TurnContext): NodeHandler {
|
|
|
251
253
|
try {
|
|
252
254
|
await ctx.bindings.hitl.enqueue({
|
|
253
255
|
tenantId: ctx.input.tenantId,
|
|
254
|
-
|
|
256
|
+
projectId: ctx.input.projectId,
|
|
257
|
+
subjectKind: TOOL_CALL_GATE_SUBJECT,
|
|
255
258
|
subjectRef: {
|
|
256
259
|
conversationId: ctx.input.conversationId,
|
|
257
260
|
agentId: ctx.input.agent.id,
|
|
@@ -275,14 +278,16 @@ export function buildDispatchToolsHandler(ctx: TurnContext): NodeHandler {
|
|
|
275
278
|
}
|
|
276
279
|
|
|
277
280
|
try {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
281
|
+
// Fails closed: only an explicit approve runs the tool. A reject,
|
|
282
|
+
// or an answer that isn't a decision at all, gives the model a
|
|
283
|
+
// rejected result instead.
|
|
284
|
+
const decision = readGateDecision(
|
|
285
|
+
await kctx.waitForToken<unknown>(waitTokenId, { timeoutMs }),
|
|
286
|
+
);
|
|
287
|
+
if (!decision.approved) {
|
|
282
288
|
toolRejectionPayload =
|
|
283
289
|
decision.rationale !== undefined ? { rationale: decision.rationale } : {};
|
|
284
290
|
}
|
|
285
|
-
// approve → fall through to dispatch
|
|
286
291
|
} catch (cause) {
|
|
287
292
|
if (cause instanceof WaitpointCancelledError) {
|
|
288
293
|
throwAgentTurnFailure({
|
|
@@ -372,43 +377,13 @@ export function buildDispatchToolsHandler(ctx: TurnContext): NodeHandler {
|
|
|
372
377
|
output: dispatched.value.persisted.content,
|
|
373
378
|
durationMs: Date.now() - toolStarted,
|
|
374
379
|
});
|
|
375
|
-
|
|
376
|
-
if (ctx.provenance !== undefined) {
|
|
377
|
-
const modelCallNodeId = `model-call:${partial.step}`;
|
|
378
|
-
const toolCallNodeId = `tool-call:${call.id}`;
|
|
379
|
-
const toolResultNodeId = `tool-result:${call.id}`;
|
|
380
|
-
ctx.provenance.addNode({
|
|
381
|
-
id: toolCallNodeId,
|
|
382
|
-
kind: 'tool-call',
|
|
383
|
-
timestamp: dispatched.value.persisted.createdAt,
|
|
384
|
-
attributes: {
|
|
385
|
-
toolId: call.name,
|
|
386
|
-
invocationId: call.id,
|
|
387
|
-
// Capture the exact version the
|
|
388
|
-
// registry picked at run start + the range the agent asked
|
|
389
|
-
// for, so a replay can pin against the same version.
|
|
390
|
-
toolVersion: resolvedVersion,
|
|
391
|
-
toolVersionRange: requestedRange,
|
|
392
|
-
},
|
|
393
|
-
});
|
|
394
|
-
ctx.provenance.addNode({
|
|
395
|
-
id: toolResultNodeId,
|
|
396
|
-
kind: 'tool-result',
|
|
397
|
-
timestamp: dispatched.value.persisted.createdAt,
|
|
398
|
-
});
|
|
399
|
-
ctx.provenance.addEdge({
|
|
400
|
-
from: toolCallNodeId,
|
|
401
|
-
to: modelCallNodeId,
|
|
402
|
-
kind: 'invoked',
|
|
403
|
-
});
|
|
404
|
-
ctx.provenance.addEdge({
|
|
405
|
-
from: toolResultNodeId,
|
|
406
|
-
to: toolCallNodeId,
|
|
407
|
-
kind: 'produced',
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
380
|
}
|
|
411
381
|
|
|
382
|
+
// Every call's nodes, once the step has all its results: the ones it
|
|
383
|
+
// ran, the rejected and failed ones, and those it took from before a
|
|
384
|
+
// park in this step.
|
|
385
|
+
addStepToolNodes(ctx, partial.step, iterationAppended);
|
|
386
|
+
|
|
412
387
|
const forwarded: Partial<AgentTurnIterationOutput> & {
|
|
413
388
|
readonly step: number;
|
|
414
389
|
readonly hasToolCalls: true;
|
|
@@ -573,6 +548,9 @@ async function dispatchOne(
|
|
|
573
548
|
const toolCtx: ToolContext = {
|
|
574
549
|
tenantId: ctx.input.tenantId,
|
|
575
550
|
runId,
|
|
551
|
+
// The run's own project and org, never the model's arguments.
|
|
552
|
+
projectId: ctx.input.projectId,
|
|
553
|
+
...(ctx.input.orgId !== undefined && { orgId: ctx.input.orgId }),
|
|
576
554
|
requestId: call.id,
|
|
577
555
|
abortSignal: ctx.turnAbort.signal,
|
|
578
556
|
// HTTP tools built via defineTool({spec: {kind: 'http'}}) resolve declared
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright (C) 2026 Kindgi Inc.
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type { UsageSink } from '@kindgi/capabilities';
|
|
5
|
+
import type { EvaluationOutcome } from '@kindgi/guardrails';
|
|
6
|
+
import type { NodeContext, NodeHandler } from '@kindgi/handler';
|
|
5
7
|
import type { Timestamp } from '@kindgi/types';
|
|
6
8
|
|
|
7
9
|
import {
|
|
@@ -77,6 +79,7 @@ export function buildEvaluateGuardrailsHandler(ctx: TurnContext): NodeHandler {
|
|
|
77
79
|
runId: kctx.runId,
|
|
78
80
|
tenantId: ctx.input.tenantId,
|
|
79
81
|
projectId: ctx.input.projectId,
|
|
82
|
+
...(ctx.input.orgId !== undefined && { orgId: ctx.input.orgId }),
|
|
80
83
|
conversationId: ctx.input.conversationId,
|
|
81
84
|
turnNumber: (ctx.conversation?.turnCount ?? 0) + 1,
|
|
82
85
|
agent: ctx.input.agent,
|
|
@@ -93,7 +96,9 @@ export function buildEvaluateGuardrailsHandler(ctx: TurnContext): NodeHandler {
|
|
|
93
96
|
ctx.bindings,
|
|
94
97
|
ctx.tenantPolicy,
|
|
95
98
|
ctx.turnAbort.signal,
|
|
99
|
+
judgeUsageSink(ctx, kctx),
|
|
96
100
|
);
|
|
101
|
+
throwIfJudgeCallsUnrecorded(outcomes);
|
|
97
102
|
const categorized = categorizeOutcomes(outcomes);
|
|
98
103
|
|
|
99
104
|
const allViolations = [...categorized.blocking, ...categorized.warnings, ...categorized.other];
|
|
@@ -151,3 +156,36 @@ export function buildEvaluateGuardrailsHandler(ctx: TurnContext): NodeHandler {
|
|
|
151
156
|
};
|
|
152
157
|
};
|
|
153
158
|
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The turn's usage sink for its llm-judge calls, adding what only the
|
|
162
|
+
* turn knows: the step that made them and the agent's version.
|
|
163
|
+
*/
|
|
164
|
+
function judgeUsageSink(ctx: TurnContext, kctx: NodeContext): UsageSink | undefined {
|
|
165
|
+
const sink = ctx.bindings.usage;
|
|
166
|
+
if (sink === undefined) return undefined;
|
|
167
|
+
return {
|
|
168
|
+
record: (call) =>
|
|
169
|
+
sink.record({
|
|
170
|
+
nodeId: kctx.nodeId as unknown as string,
|
|
171
|
+
agentVersion: ctx.input.agent.version,
|
|
172
|
+
...call,
|
|
173
|
+
}),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* A judge call that answered but couldn't be recorded fails the step, as
|
|
179
|
+
* the turn's own model calls do: no answered call is left unrecorded.
|
|
180
|
+
*/
|
|
181
|
+
function throwIfJudgeCallsUnrecorded(outcomes: readonly EvaluationOutcome[]): void {
|
|
182
|
+
for (const outcome of outcomes) {
|
|
183
|
+
if (outcome.kind === 'err' && outcome.error.code === 'judge-usage-unrecorded') {
|
|
184
|
+
throwAgentTurnFailure({
|
|
185
|
+
code: 'persistence-error',
|
|
186
|
+
message: outcome.error.message,
|
|
187
|
+
cause: outcome.error,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The reviewer's answer at an agent turn's approval gates: the tool-call
|
|
6
|
+
* gate (`dispatch-tools`) and the session gate (`setup`). A gate parks the
|
|
7
|
+
* turn on a waitpoint; the approvals route resumes it with the decision
|
|
8
|
+
* (`GateDecisionValue`): approve or reject, the reviewer's rationale, and
|
|
9
|
+
* who decided which approval.
|
|
10
|
+
*
|
|
11
|
+
* A gate fails closed. Only an explicit approve lets the tool run (or the
|
|
12
|
+
* session go on). Anything else blocks: a reject, and also a resume value
|
|
13
|
+
* that isn't a decision at all (a `value` that replaced it, a malformed
|
|
14
|
+
* payload), so no answer can be read as consent by omission.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** The approval subject an agent's tool-call gate parks on. */
|
|
18
|
+
export const TOOL_CALL_GATE_SUBJECT = 'tool-call:pending';
|
|
19
|
+
|
|
20
|
+
/** The approval subject an agent's session gate (`afterTurns`) parks on. */
|
|
21
|
+
export const SESSION_GATE_SUBJECT = 'agent-turn:session-hitl-gate';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The approval subjects an agent turn parks on. Their resume value is the
|
|
25
|
+
* reviewer's decision and nothing else, so the approvals route takes no
|
|
26
|
+
* `value` for them.
|
|
27
|
+
*/
|
|
28
|
+
export const AGENT_GATE_SUBJECTS: ReadonlySet<string> = new Set([
|
|
29
|
+
TOOL_CALL_GATE_SUBJECT,
|
|
30
|
+
SESSION_GATE_SUBJECT,
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A gate's resume value: the reviewer's decision, as the approvals route
|
|
35
|
+
* (and the runtime, delivering a decision whose delivery was lost) completes
|
|
36
|
+
* the gate's waitpoint with it. The run's journal keeps it as it came.
|
|
37
|
+
*/
|
|
38
|
+
export interface GateDecisionValue {
|
|
39
|
+
readonly decided: 'approve' | 'reject';
|
|
40
|
+
readonly rationale?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Who decided, as an actor: `user:<userId>`, the reviewer's user. Absent
|
|
43
|
+
* from values written before it was recorded.
|
|
44
|
+
*/
|
|
45
|
+
readonly decidedBy?: string;
|
|
46
|
+
/** The approval decided. Absent from values written before it was recorded. */
|
|
47
|
+
readonly approvalId?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Who decided a gate, and which approval, when its value says. */
|
|
51
|
+
interface GateDecider {
|
|
52
|
+
readonly decidedBy?: string;
|
|
53
|
+
readonly approvalId?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** What a gate's resume value decides. */
|
|
57
|
+
export type GateDecision = GateDecider &
|
|
58
|
+
(
|
|
59
|
+
| { readonly approved: true }
|
|
60
|
+
| {
|
|
61
|
+
readonly approved: false;
|
|
62
|
+
/** `rejected`: the reviewer said no. `unreadable`: the answer wasn't a decision. */
|
|
63
|
+
readonly reason: 'rejected' | 'unreadable';
|
|
64
|
+
/** The reviewer's rationale, or why an unreadable answer blocks. */
|
|
65
|
+
readonly rationale: string | undefined;
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
/** Why a gate blocks on an answer that isn't a decision. */
|
|
70
|
+
export const UNREADABLE_DECISION =
|
|
71
|
+
"the approval's answer wasn't a decision (approve or reject), so it counts as a rejection";
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The decision in a gate's resume value. Fails closed: only
|
|
75
|
+
* `{ decided: 'approve' }` approves.
|
|
76
|
+
*/
|
|
77
|
+
export function readGateDecision(value: unknown): GateDecision {
|
|
78
|
+
if (typeof value !== 'object' || value === null) {
|
|
79
|
+
return { approved: false, reason: 'unreadable', rationale: UNREADABLE_DECISION };
|
|
80
|
+
}
|
|
81
|
+
const { decided, rationale, decidedBy, approvalId } = value as {
|
|
82
|
+
decided?: unknown;
|
|
83
|
+
rationale?: unknown;
|
|
84
|
+
decidedBy?: unknown;
|
|
85
|
+
approvalId?: unknown;
|
|
86
|
+
};
|
|
87
|
+
const decider: GateDecider = {
|
|
88
|
+
...(typeof decidedBy === 'string' && { decidedBy }),
|
|
89
|
+
...(typeof approvalId === 'string' && { approvalId }),
|
|
90
|
+
};
|
|
91
|
+
if (decided === 'approve') return { approved: true, ...decider };
|
|
92
|
+
if (decided === 'reject') {
|
|
93
|
+
return {
|
|
94
|
+
approved: false,
|
|
95
|
+
reason: 'rejected',
|
|
96
|
+
rationale: typeof rationale === 'string' ? rationale : undefined,
|
|
97
|
+
...decider,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return { approved: false, reason: 'unreadable', rationale: UNREADABLE_DECISION };
|
|
101
|
+
}
|