@kindgi/agents 0.1.2 → 0.1.4-rc.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/dist/blocks.d.ts +101 -0
- package/dist/blocks.d.ts.map +1 -0
- package/dist/blocks.js +149 -0
- package/dist/blocks.js.map +1 -0
- package/dist/conversation-binding.d.ts +9 -1
- package/dist/conversation-binding.d.ts.map +1 -1
- package/dist/define.d.ts +17 -2
- package/dist/define.d.ts.map +1 -1
- package/dist/define.js +93 -6
- 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/compose-result.d.ts.map +1 -1
- package/dist/handlers/compose-result.js +3 -0
- package/dist/handlers/compose-result.js.map +1 -1
- package/dist/handlers/context.d.ts +24 -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 +69 -43
- package/dist/handlers/dispatch-tools.js.map +1 -1
- package/dist/handlers/errors.d.ts +12 -1
- package/dist/handlers/errors.d.ts.map +1 -1
- package/dist/handlers/errors.js.map +1 -1
- package/dist/handlers/evaluate-guardrails.d.ts.map +1 -1
- package/dist/handlers/evaluate-guardrails.js +36 -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 +120 -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 +37 -3
- package/dist/handlers/public-types.d.ts.map +1 -1
- package/dist/handlers/rehydrate.d.ts.map +1 -1
- package/dist/handlers/rehydrate.js +91 -1
- package/dist/handlers/rehydrate.js.map +1 -1
- package/dist/handlers/render-prompt.d.ts.map +1 -1
- package/dist/handlers/render-prompt.js +4 -1
- package/dist/handlers/render-prompt.js.map +1 -1
- package/dist/handlers/replay.d.ts +143 -0
- package/dist/handlers/replay.d.ts.map +1 -0
- package/dist/handlers/replay.js +177 -0
- package/dist/handlers/replay.js.map +1 -0
- package/dist/handlers/resolve-blocks.d.ts +32 -0
- package/dist/handlers/resolve-blocks.d.ts.map +1 -0
- package/dist/handlers/resolve-blocks.js +129 -0
- package/dist/handlers/resolve-blocks.js.map +1 -0
- package/dist/handlers/resolve-tools.d.ts +6 -2
- package/dist/handlers/resolve-tools.d.ts.map +1 -1
- package/dist/handlers/resolve-tools.js +59 -27
- package/dist/handlers/resolve-tools.js.map +1 -1
- package/dist/handlers/result-shape.d.ts +7 -0
- package/dist/handlers/result-shape.d.ts.map +1 -1
- package/dist/handlers/result-shape.js.map +1 -1
- package/dist/handlers/run-retrievals.d.ts.map +1 -1
- package/dist/handlers/run-retrievals.js +35 -35
- package/dist/handlers/run-retrievals.js.map +1 -1
- package/dist/handlers/run-snapshot.d.ts.map +1 -1
- package/dist/handlers/run-snapshot.js +1 -0
- package/dist/handlers/run-snapshot.js.map +1 -1
- package/dist/handlers/setup.d.ts +2 -0
- package/dist/handlers/setup.d.ts.map +1 -1
- package/dist/handlers/setup.js +29 -12
- 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-environment.d.ts +13 -2
- package/dist/handlers/turn-environment.d.ts.map +1 -1
- package/dist/handlers/turn-environment.js +12 -3
- package/dist/handlers/turn-environment.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 +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -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 +37 -19
- package/dist/invoke.js.map +1 -1
- package/dist/pins.d.ts +54 -0
- package/dist/pins.d.ts.map +1 -0
- package/dist/pins.js +51 -0
- package/dist/pins.js.map +1 -0
- package/dist/prompt.d.ts +12 -2
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +41 -4
- package/dist/prompt.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/run-snapshot-binding.d.ts +5 -0
- package/dist/run-snapshot-binding.d.ts.map +1 -1
- package/dist/schema.d.ts +34 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +16 -0
- package/dist/schema.js.map +1 -1
- package/dist/streaming.d.ts +5 -0
- package/dist/streaming.d.ts.map +1 -1
- package/dist/streaming.js.map +1 -1
- package/dist/types.d.ts +65 -2
- package/dist/types.d.ts.map +1 -1
- package/migrations/0003_hesitant_captain_cross.sql +2 -0
- package/migrations/0004_stormy_moondragon.sql +1 -0
- package/migrations/meta/0003_snapshot.json +321 -0
- package/migrations/meta/0004_snapshot.json +327 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +15 -15
- package/src/blocks.ts +231 -0
- package/src/conversation-binding.ts +9 -1
- package/src/define.ts +104 -7
- package/src/guardrails-gate.ts +17 -3
- package/src/handlers/compose-result.ts +3 -0
- package/src/handlers/context.ts +24 -0
- package/src/handlers/dispatch-tools.ts +75 -44
- package/src/handlers/errors.ts +13 -0
- package/src/handlers/evaluate-guardrails.ts +41 -1
- package/src/handlers/gate-decision.ts +101 -0
- package/src/handlers/model-call.ts +163 -33
- package/src/handlers/persist-provenance.ts +3 -1
- package/src/handlers/persist-user-message.ts +3 -16
- package/src/handlers/public-types.ts +38 -3
- package/src/handlers/rehydrate.ts +141 -9
- package/src/handlers/render-prompt.ts +13 -7
- package/src/handlers/replay.ts +314 -0
- package/src/handlers/resolve-blocks.ts +188 -0
- package/src/handlers/resolve-tools.ts +76 -28
- package/src/handlers/result-shape.ts +8 -0
- package/src/handlers/run-retrievals.ts +45 -41
- package/src/handlers/run-snapshot.ts +1 -0
- package/src/handlers/setup.ts +30 -22
- package/src/handlers/tool-hitl.ts +6 -10
- package/src/handlers/turn-environment.ts +28 -3
- package/src/handlers/turn-provenance.ts +230 -0
- package/src/index.ts +44 -0
- package/src/invoke.ts +49 -20
- package/src/pins.ts +98 -0
- package/src/prompt.ts +52 -4
- package/src/provenance-emit.ts +4 -1
- package/src/run-snapshot-binding.ts +6 -0
- package/src/schema.ts +16 -0
- package/src/streaming.ts +5 -0
- package/src/types.ts +68 -2
package/src/define.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
loadZodConverterSync,
|
|
10
10
|
toJSONSchemaSync,
|
|
11
11
|
} from '@kindgi/schema';
|
|
12
|
+
import { pickVersion } from '@kindgi/tools';
|
|
12
13
|
import type { Result, Semver } from '@kindgi/types';
|
|
13
14
|
|
|
14
15
|
import type { InvalidAgentError } from './errors.js';
|
|
@@ -16,8 +17,10 @@ import type {
|
|
|
16
17
|
Agent,
|
|
17
18
|
AgentId,
|
|
18
19
|
AgentOutputSpec,
|
|
20
|
+
BlockRef,
|
|
19
21
|
ConversationPolicy,
|
|
20
22
|
PromptParameter,
|
|
23
|
+
PromptRef,
|
|
21
24
|
RetrievalIntent,
|
|
22
25
|
ToolRef,
|
|
23
26
|
TurnBudget,
|
|
@@ -48,9 +51,10 @@ export function defineAgent(spec: DefineAgentSpec): Result<Agent, InvalidAgentEr
|
|
|
48
51
|
const issues: Issue[] = [
|
|
49
52
|
...validateIdentity(spec),
|
|
50
53
|
...validateContent(spec),
|
|
54
|
+
...validateBlockRefs(spec),
|
|
51
55
|
...validateArrays(spec),
|
|
52
56
|
...validateRetrieval(spec),
|
|
53
|
-
...
|
|
57
|
+
...validatePromptParameters(spec.parameters),
|
|
54
58
|
...validateBudget(spec.budget),
|
|
55
59
|
...validateConversationPolicy(spec.conversationPolicy),
|
|
56
60
|
...validateToolErrors(spec.toolErrors),
|
|
@@ -126,8 +130,16 @@ export interface DefineAgentSpec {
|
|
|
126
130
|
* The system prompt. Sent to the model with every turn as the
|
|
127
131
|
* baseline instructions. Load-bearing — this is where you shape
|
|
128
132
|
* the agent's behavior (persona, output format, tool-use policy).
|
|
133
|
+
*
|
|
134
|
+
* Or a prompt block by range (`{ prompt: 'acme.intake-prompt',
|
|
135
|
+
* version: '^1.0.0' }`), whose template and parameters are used
|
|
136
|
+
* instead; then `parameters` stays unset (the block declares them).
|
|
129
137
|
*/
|
|
130
|
-
readonly instructions: string;
|
|
138
|
+
readonly instructions: string | PromptRef;
|
|
139
|
+
/** Settings blocks the agent reads, by range (see `Agent.settings`). */
|
|
140
|
+
readonly settings?: readonly BlockRef[];
|
|
141
|
+
/** A model-settings block, by range (see `Agent.modelSettings`). */
|
|
142
|
+
readonly modelSettings?: BlockRef;
|
|
131
143
|
/**
|
|
132
144
|
* Required capabilities the agent needs from a `ModelProvider`.
|
|
133
145
|
* Typically one entry: `[{ needs: [{ feature: 'tool-use' }] }]`
|
|
@@ -248,8 +260,19 @@ function validateIdentity(spec: DefineAgentSpec): Issue[] {
|
|
|
248
260
|
|
|
249
261
|
function validateContent(spec: DefineAgentSpec): Issue[] {
|
|
250
262
|
const out: Issue[] = [];
|
|
251
|
-
if (typeof spec.instructions
|
|
252
|
-
out.push(
|
|
263
|
+
if (typeof spec.instructions === 'object' && spec.instructions !== null) {
|
|
264
|
+
out.push(...blockRefIssues(spec.instructions, '/instructions', 'prompt'));
|
|
265
|
+
if (spec.parameters !== undefined) {
|
|
266
|
+
out.push({
|
|
267
|
+
path: '/parameters',
|
|
268
|
+
message: 'the prompt block declares the parameters: leave parameters unset',
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
} else if (typeof spec.instructions !== 'string' || spec.instructions.trim().length === 0) {
|
|
272
|
+
out.push({
|
|
273
|
+
path: '/instructions',
|
|
274
|
+
message: 'instructions must be a non-empty string, or a prompt block { prompt, version }',
|
|
275
|
+
});
|
|
253
276
|
}
|
|
254
277
|
if (!Array.isArray(spec.capabilities) || spec.capabilities.length === 0) {
|
|
255
278
|
out.push({
|
|
@@ -260,6 +283,56 @@ function validateContent(spec: DefineAgentSpec): Issue[] {
|
|
|
260
283
|
return out;
|
|
261
284
|
}
|
|
262
285
|
|
|
286
|
+
const BLOCK_ID = /^[a-z0-9][a-z0-9-]*(?:\.[a-z0-9][a-z0-9-]*)+$/;
|
|
287
|
+
|
|
288
|
+
/** A block reference: a dotted block id and a valid semver range. */
|
|
289
|
+
function blockRefIssues(ref: unknown, path: string, idKey: 'prompt' | 'id'): Issue[] {
|
|
290
|
+
if (ref === null || typeof ref !== 'object') {
|
|
291
|
+
return [{ path, message: `must be { ${idKey}, version }` }];
|
|
292
|
+
}
|
|
293
|
+
const r = ref as Record<string, unknown>;
|
|
294
|
+
const out: Issue[] = [];
|
|
295
|
+
if (typeof r[idKey] !== 'string' || !BLOCK_ID.test(r[idKey] as string)) {
|
|
296
|
+
out.push({
|
|
297
|
+
path: `${path}/${idKey}`,
|
|
298
|
+
message: 'must be a dotted lowercase block id, e.g. "acme.weights"',
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
if (typeof r.version !== 'string' || pickVersion([], r.version).kind === 'invalid-range') {
|
|
302
|
+
out.push({
|
|
303
|
+
path: `${path}/version`,
|
|
304
|
+
message: 'must be a semver range, e.g. "^1.0.0" or "1.2.0"',
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Settings and model-settings references: valid, and each block named once. */
|
|
311
|
+
function validateBlockRefs(spec: DefineAgentSpec): Issue[] {
|
|
312
|
+
const out: Issue[] = [];
|
|
313
|
+
const seen = new Set<string>();
|
|
314
|
+
const once = (id: unknown, path: string) => {
|
|
315
|
+
if (typeof id !== 'string') return;
|
|
316
|
+
if (seen.has(id)) out.push({ path, message: `settings block "${id}" is referenced twice` });
|
|
317
|
+
seen.add(id);
|
|
318
|
+
};
|
|
319
|
+
if (spec.settings !== undefined) {
|
|
320
|
+
if (!Array.isArray(spec.settings)) {
|
|
321
|
+
out.push({ path: '/settings', message: 'settings must be an array of { id, version }' });
|
|
322
|
+
} else {
|
|
323
|
+
spec.settings.forEach((ref, i) => {
|
|
324
|
+
out.push(...blockRefIssues(ref, `/settings/${i}`, 'id'));
|
|
325
|
+
once((ref as { id?: unknown }).id, `/settings/${i}/id`);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (spec.modelSettings !== undefined) {
|
|
330
|
+
out.push(...blockRefIssues(spec.modelSettings, '/modelSettings', 'id'));
|
|
331
|
+
once((spec.modelSettings as { id?: unknown }).id, '/modelSettings/id');
|
|
332
|
+
}
|
|
333
|
+
return out;
|
|
334
|
+
}
|
|
335
|
+
|
|
263
336
|
function validateArrays(spec: DefineAgentSpec): Issue[] {
|
|
264
337
|
const out: Issue[] = [];
|
|
265
338
|
if (!Array.isArray(spec.tools)) {
|
|
@@ -334,9 +407,10 @@ function validateIntent(intent: RetrievalIntent, i: number): Issue[] {
|
|
|
334
407
|
}
|
|
335
408
|
|
|
336
409
|
const VALID_PARAM_TYPES = new Set(['string', 'number', 'boolean', 'date']);
|
|
337
|
-
const AUTO_INJECTED_NAMES = new Set(['today', 'now', 'agent', 'conversation']);
|
|
410
|
+
const AUTO_INJECTED_NAMES = new Set(['today', 'now', 'agent', 'conversation', 'settings']);
|
|
338
411
|
|
|
339
|
-
|
|
412
|
+
/** An agent's or a prompt block's declared parameters: the problems, none when they're valid. */
|
|
413
|
+
export function validatePromptParameters(parameters?: readonly PromptParameter[]): Issue[] {
|
|
340
414
|
if (parameters === undefined) return [];
|
|
341
415
|
const out: Issue[] = [];
|
|
342
416
|
const seen = new Set<string>();
|
|
@@ -448,6 +522,7 @@ function validateConversationPolicy(policy?: ConversationPolicy): Issue[] {
|
|
|
448
522
|
message: 'hitl.timeoutMs must be a positive integer (ms)',
|
|
449
523
|
});
|
|
450
524
|
}
|
|
525
|
+
out.push(...validateOnTimeout(h.onTimeout));
|
|
451
526
|
const ROLES: ReadonlySet<string> = new Set(['standard', 'senior', 'admin']);
|
|
452
527
|
if (
|
|
453
528
|
h.defaultReviewerRole !== undefined &&
|
|
@@ -490,6 +565,25 @@ function validateConversationPolicy(policy?: ConversationPolicy): Issue[] {
|
|
|
490
565
|
return out;
|
|
491
566
|
}
|
|
492
567
|
|
|
568
|
+
/**
|
|
569
|
+
* `hitl.onTimeout`: only `'escalate'`, what the runtime does when an
|
|
570
|
+
* approval's time runs out. `'auto-approve'` / `'auto-reject'` aren't
|
|
571
|
+
* implemented, so they're refused instead of accepted and ignored.
|
|
572
|
+
*/
|
|
573
|
+
function validateOnTimeout(onTimeout: unknown): Issue[] {
|
|
574
|
+
if (onTimeout === undefined || onTimeout === 'escalate') return [];
|
|
575
|
+
const path = '/conversationPolicy/hitl/onTimeout';
|
|
576
|
+
if (onTimeout === 'auto-approve' || onTimeout === 'auto-reject') {
|
|
577
|
+
return [
|
|
578
|
+
{
|
|
579
|
+
path,
|
|
580
|
+
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.`,
|
|
581
|
+
},
|
|
582
|
+
];
|
|
583
|
+
}
|
|
584
|
+
return [{ path, message: "hitl.onTimeout must be 'escalate'" }];
|
|
585
|
+
}
|
|
586
|
+
|
|
493
587
|
type OutputOutcome =
|
|
494
588
|
| { readonly kind: 'none' }
|
|
495
589
|
| { readonly kind: 'ok'; readonly value: AgentOutputSpec }
|
|
@@ -546,7 +640,10 @@ function buildAgent(spec: DefineAgentSpec, output: AgentOutputSpec | undefined):
|
|
|
546
640
|
version: spec.version as Semver,
|
|
547
641
|
name: spec.name,
|
|
548
642
|
...(spec.description !== undefined && { description: spec.description }),
|
|
549
|
-
instructions:
|
|
643
|
+
instructions:
|
|
644
|
+
typeof spec.instructions === 'string' ? spec.instructions : { ...spec.instructions },
|
|
645
|
+
...(spec.settings !== undefined && { settings: spec.settings.map((r) => ({ ...r })) }),
|
|
646
|
+
...(spec.modelSettings !== undefined && { modelSettings: { ...spec.modelSettings } }),
|
|
550
647
|
capabilities: spec.capabilities.map((c) => ({ ...c })),
|
|
551
648
|
tools: [...spec.tools],
|
|
552
649
|
retrieval: spec.retrieval.map((r) => ({ ...r })),
|
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
|
}
|
|
@@ -8,6 +8,7 @@ import type { AgentTurnResult, AgentTurnUsage } from './result-shape.js';
|
|
|
8
8
|
|
|
9
9
|
import type { TurnContext } from './context.js';
|
|
10
10
|
import { throwAgentTurnFailure } from './errors.js';
|
|
11
|
+
import { replayReport } from './replay.js';
|
|
11
12
|
import { parseJsonAnswer } from './structured-output.js';
|
|
12
13
|
|
|
13
14
|
/** The final answer as JSON — `budget-check` already validated it against the schema. */
|
|
@@ -45,6 +46,7 @@ export function buildComposeResultHandler(ctx: TurnContext): NodeHandler {
|
|
|
45
46
|
model: ctx.model?.name ?? 'unknown',
|
|
46
47
|
};
|
|
47
48
|
|
|
49
|
+
const replay = replayReport(ctx);
|
|
48
50
|
const result: AgentTurnResult = {
|
|
49
51
|
runId: kctx.runId,
|
|
50
52
|
conversationId: ctx.input.conversationId,
|
|
@@ -70,6 +72,7 @@ export function buildComposeResultHandler(ctx: TurnContext): NodeHandler {
|
|
|
70
72
|
}),
|
|
71
73
|
...(ctx.persistedProvenance !== undefined && { provenance: ctx.persistedProvenance }),
|
|
72
74
|
...(kctx.dryRun && { dryRun: true }),
|
|
75
|
+
...(replay !== undefined && { replay }),
|
|
73
76
|
};
|
|
74
77
|
|
|
75
78
|
await emitTurnEvent(ctx.bindings.onEvent, {
|
package/src/handlers/context.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type { ProvenanceBindings } from '../provenance-emit.js';
|
|
|
19
19
|
import type { Agent, Conversation, ConversationMessage, RetrievedFact } from '../types.js';
|
|
20
20
|
|
|
21
21
|
import type { HitlBindings, InvokeAgentBindings, InvokeAgentInput } from './public-types.js';
|
|
22
|
+
import type { TurnBlocks } from './resolve-blocks.js';
|
|
22
23
|
import type { ToolErrorPolicy } from './tool-errors.js';
|
|
23
24
|
|
|
24
25
|
/**
|
|
@@ -53,6 +54,8 @@ export interface TurnContext {
|
|
|
53
54
|
* Populated by `setup` — resolved tools (per-name map + model
|
|
54
55
|
* definitions).
|
|
55
56
|
*/
|
|
57
|
+
/** The data blocks the turn runs with (set by setup; none when the agent references none). */
|
|
58
|
+
blocks?: TurnBlocks;
|
|
56
59
|
tools?: {
|
|
57
60
|
readonly definitions: readonly ModelToolDefinition[];
|
|
58
61
|
/**
|
|
@@ -143,6 +146,20 @@ export interface TurnContext {
|
|
|
143
146
|
* Present only when the builder was wired AND persistence succeeded.
|
|
144
147
|
*/
|
|
145
148
|
persistedProvenance?: import('@kindgi/provenance').Provenance;
|
|
149
|
+
/**
|
|
150
|
+
* This turn's tool results so far, by invocation id: a model call read
|
|
151
|
+
* them all (they're in its input), so its provenance node is
|
|
152
|
+
* `influenced-by` each. Filled by `dispatch-tools`, and by the rebuild
|
|
153
|
+
* of a resumed turn.
|
|
154
|
+
*/
|
|
155
|
+
toolResultIds?: string[];
|
|
156
|
+
/**
|
|
157
|
+
* The tool-call approvals this turn's journal shows decided, by
|
|
158
|
+
* invocation id: each call's provenance shows the approval it waited on.
|
|
159
|
+
* Populated by `rehydrateTurnContext` (a decision only reaches a turn
|
|
160
|
+
* that parked, and resumes).
|
|
161
|
+
*/
|
|
162
|
+
toolApprovals?: ReadonlyMap<string, import('./turn-provenance.js').ToolApproval>;
|
|
146
163
|
/**
|
|
147
164
|
* The tenant policy this turn's model was routed under (bound policy
|
|
148
165
|
* merged with the policy registry's). Populated by `setup`; guardrail
|
|
@@ -175,6 +192,13 @@ export interface TurnContext {
|
|
|
175
192
|
* `AgentTurnResult.violations`.
|
|
176
193
|
*/
|
|
177
194
|
nonBlockingViolations?: readonly EvaluationResult[];
|
|
195
|
+
/**
|
|
196
|
+
* A replay turn's tool calls so far, and what happened to each
|
|
197
|
+
* (`decideReplayTool`). Rebuilt from the journal on resume.
|
|
198
|
+
*/
|
|
199
|
+
replayTrace?: import('./replay.js').ReplayToolTrace[];
|
|
200
|
+
/** A replay turn's session approval, when it reached the gate (`replaySessionApproval`). */
|
|
201
|
+
replayApproval?: 'followed' | 'skipped';
|
|
178
202
|
/**
|
|
179
203
|
* Reason recorded when `turnAbort` fires. Used to distinguish
|
|
180
204
|
* external cancellation from wall-clock timeout in the projected
|
|
@@ -19,13 +19,16 @@ import {
|
|
|
19
19
|
type UnresolvedToolError,
|
|
20
20
|
throwAgentTurnFailure,
|
|
21
21
|
} from './errors.js';
|
|
22
|
+
import { TOOL_CALL_GATE_SUBJECT, readGateDecision } from './gate-decision.js';
|
|
23
|
+
import { decideReplayTool } from './replay.js';
|
|
22
24
|
import {
|
|
23
25
|
effectiveToolErrorPolicy,
|
|
24
26
|
toolErrorKindOf,
|
|
25
27
|
toolErrorResult,
|
|
26
28
|
toolRetriesSoFar,
|
|
27
29
|
} from './tool-errors.js';
|
|
28
|
-
import {
|
|
30
|
+
import { TOOL_GATE_RECORD_PREFIX, computeToolCallWaitToken, hashToolArgs } from './tool-hitl.js';
|
|
31
|
+
import { addStepToolNodes } from './turn-provenance.js';
|
|
29
32
|
|
|
30
33
|
/**
|
|
31
34
|
* Resolves the effective HITL mode + reviewer role for a specific
|
|
@@ -111,7 +114,7 @@ async function decideToolGate(
|
|
|
111
114
|
cause: null,
|
|
112
115
|
});
|
|
113
116
|
}
|
|
114
|
-
return kctx.record(
|
|
117
|
+
return kctx.record(`${TOOL_GATE_RECORD_PREFIX}${call.id}`, (): ToolGateRecord | undefined => {
|
|
115
118
|
const resolved = resolveEffectiveToolHitl(effectiveHitl, tool);
|
|
116
119
|
if (resolved.mode === 'never_ask') return undefined;
|
|
117
120
|
const argsHash = hashToolArgs(call.arguments);
|
|
@@ -242,6 +245,50 @@ export function buildDispatchToolsHandler(ctx: TurnContext): NodeHandler {
|
|
|
242
245
|
// skip the reviewer's answer. Cross-turn `ask_on_first_use` caching
|
|
243
246
|
// (via conversation metadata) is not implemented.
|
|
244
247
|
const gate = await decideToolGate(ctx, kctx, call, tool);
|
|
248
|
+
|
|
249
|
+
// A replay turn decides the call first: a recorded or refused call
|
|
250
|
+
// runs nothing, so it asks for no approval either.
|
|
251
|
+
const replayed =
|
|
252
|
+
ctx.input.replay === undefined
|
|
253
|
+
? undefined
|
|
254
|
+
: await decideReplayTool(ctx, kctx, {
|
|
255
|
+
step: partial.step,
|
|
256
|
+
callId: call.id,
|
|
257
|
+
tool,
|
|
258
|
+
version: resolvedVersion,
|
|
259
|
+
arguments: call.arguments,
|
|
260
|
+
gated: gate !== undefined,
|
|
261
|
+
});
|
|
262
|
+
if (replayed !== undefined && replayed.kind !== 'live') {
|
|
263
|
+
const replayStarted = Date.now();
|
|
264
|
+
await emitTurnEvent(ctx.bindings.onEvent, {
|
|
265
|
+
kind: 'tool.started',
|
|
266
|
+
step: partial.step,
|
|
267
|
+
toolId: call.name,
|
|
268
|
+
toolVersion: resolvedVersion,
|
|
269
|
+
toolVersionRange: requestedRange,
|
|
270
|
+
invocationId: call.id,
|
|
271
|
+
arguments: call.arguments,
|
|
272
|
+
});
|
|
273
|
+
nextMessages = await appendToolResult(ctx, call, replayed.result, {
|
|
274
|
+
toolId: tool.id as unknown as string,
|
|
275
|
+
nextMessages,
|
|
276
|
+
iterationAppended,
|
|
277
|
+
});
|
|
278
|
+
await emitTurnEvent(ctx.bindings.onEvent, {
|
|
279
|
+
kind: 'tool.completed',
|
|
280
|
+
step: partial.step,
|
|
281
|
+
toolId: call.name,
|
|
282
|
+
toolVersion: resolvedVersion,
|
|
283
|
+
toolVersionRange: requestedRange,
|
|
284
|
+
invocationId: call.id,
|
|
285
|
+
output: replayed.result as never,
|
|
286
|
+
durationMs: Date.now() - replayStarted,
|
|
287
|
+
replay: replayed.kind,
|
|
288
|
+
});
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
|
|
245
292
|
let toolRejectionPayload: { readonly rationale?: string } | null = null;
|
|
246
293
|
if (gate !== undefined) {
|
|
247
294
|
const { argsHash, waitTokenId, timeoutMs } = gate;
|
|
@@ -251,7 +298,8 @@ export function buildDispatchToolsHandler(ctx: TurnContext): NodeHandler {
|
|
|
251
298
|
try {
|
|
252
299
|
await ctx.bindings.hitl.enqueue({
|
|
253
300
|
tenantId: ctx.input.tenantId,
|
|
254
|
-
|
|
301
|
+
projectId: ctx.input.projectId,
|
|
302
|
+
subjectKind: TOOL_CALL_GATE_SUBJECT,
|
|
255
303
|
subjectRef: {
|
|
256
304
|
conversationId: ctx.input.conversationId,
|
|
257
305
|
agentId: ctx.input.agent.id,
|
|
@@ -275,14 +323,16 @@ export function buildDispatchToolsHandler(ctx: TurnContext): NodeHandler {
|
|
|
275
323
|
}
|
|
276
324
|
|
|
277
325
|
try {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
326
|
+
// Fails closed: only an explicit approve runs the tool. A reject,
|
|
327
|
+
// or an answer that isn't a decision at all, gives the model a
|
|
328
|
+
// rejected result instead.
|
|
329
|
+
const decision = readGateDecision(
|
|
330
|
+
await kctx.waitForToken<unknown>(waitTokenId, { timeoutMs }),
|
|
331
|
+
);
|
|
332
|
+
if (!decision.approved) {
|
|
282
333
|
toolRejectionPayload =
|
|
283
334
|
decision.rationale !== undefined ? { rationale: decision.rationale } : {};
|
|
284
335
|
}
|
|
285
|
-
// approve → fall through to dispatch
|
|
286
336
|
} catch (cause) {
|
|
287
337
|
if (cause instanceof WaitpointCancelledError) {
|
|
288
338
|
throwAgentTurnFailure({
|
|
@@ -371,44 +421,15 @@ export function buildDispatchToolsHandler(ctx: TurnContext): NodeHandler {
|
|
|
371
421
|
invocationId: call.id,
|
|
372
422
|
output: dispatched.value.persisted.content,
|
|
373
423
|
durationMs: Date.now() - toolStarted,
|
|
424
|
+
...(replayed !== undefined && { replay: replayed.kind }),
|
|
374
425
|
});
|
|
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
426
|
}
|
|
411
427
|
|
|
428
|
+
// Every call's nodes, once the step has all its results: the ones it
|
|
429
|
+
// ran, the rejected and failed ones, and those it took from before a
|
|
430
|
+
// park in this step.
|
|
431
|
+
addStepToolNodes(ctx, partial.step, iterationAppended);
|
|
432
|
+
|
|
412
433
|
const forwarded: Partial<AgentTurnIterationOutput> & {
|
|
413
434
|
readonly step: number;
|
|
414
435
|
readonly hasToolCalls: true;
|
|
@@ -520,7 +541,12 @@ async function appendToolResult(
|
|
|
520
541
|
target.iterationAppended.push(persisted.value);
|
|
521
542
|
return [
|
|
522
543
|
...target.nextMessages,
|
|
523
|
-
{
|
|
544
|
+
{
|
|
545
|
+
role: 'tool',
|
|
546
|
+
// As a tool's own result reaches the model: a string as it is.
|
|
547
|
+
content: typeof output === 'string' ? output : JSON.stringify(output),
|
|
548
|
+
toolCallId: call.id,
|
|
549
|
+
},
|
|
524
550
|
];
|
|
525
551
|
}
|
|
526
552
|
|
|
@@ -573,12 +599,17 @@ async function dispatchOne(
|
|
|
573
599
|
const toolCtx: ToolContext = {
|
|
574
600
|
tenantId: ctx.input.tenantId,
|
|
575
601
|
runId,
|
|
602
|
+
// The run's own project and org, never the model's arguments.
|
|
603
|
+
projectId: ctx.input.projectId,
|
|
604
|
+
...(ctx.input.orgId !== undefined && { orgId: ctx.input.orgId }),
|
|
576
605
|
requestId: call.id,
|
|
577
606
|
abortSignal: ctx.turnAbort.signal,
|
|
578
607
|
// HTTP tools built via defineTool({spec: {kind: 'http'}}) resolve declared
|
|
579
608
|
// secret_refs at invoke time. Present iff the caller wired
|
|
580
609
|
// `bindings.resolveSecret` from a tenant-scoped `SecretBinding`.
|
|
581
610
|
...(ctx.bindings.resolveSecret !== undefined && { resolveSecret: ctx.bindings.resolveSecret }),
|
|
611
|
+
// The pinned settings blocks' values, by block id.
|
|
612
|
+
...(ctx.blocks !== undefined && { settings: ctx.blocks.settings }),
|
|
582
613
|
};
|
|
583
614
|
const result = await invokeTool(tool, call.arguments, toolCtx);
|
|
584
615
|
if (result.kind === 'err') {
|
package/src/handlers/errors.ts
CHANGED
|
@@ -16,6 +16,7 @@ export type InvokeAgentError =
|
|
|
16
16
|
| AgentError
|
|
17
17
|
| UnresolvedToolError
|
|
18
18
|
| ToolVersionUnresolvableError
|
|
19
|
+
| BlockUnresolvableError
|
|
19
20
|
| CapabilityRoutingError
|
|
20
21
|
| ModelInvocationError
|
|
21
22
|
| ToolInvocationError
|
|
@@ -75,6 +76,18 @@ export interface ToolVersionUnresolvableError {
|
|
|
75
76
|
readonly availableVersions?: readonly string[];
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
/**
|
|
80
|
+
* A data block the agent references can't be loaded: no version in its
|
|
81
|
+
* range, a pinned version that's gone, the wrong kind, model settings
|
|
82
|
+
* that aren't, or a runtime that serves no blocks.
|
|
83
|
+
*/
|
|
84
|
+
export interface BlockUnresolvableError {
|
|
85
|
+
readonly code: 'block-unresolvable';
|
|
86
|
+
readonly message: string;
|
|
87
|
+
readonly blockId: string;
|
|
88
|
+
readonly requestedRange?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
78
91
|
export interface CapabilityRoutingError {
|
|
79
92
|
readonly code: 'capability-routing-failed';
|
|
80
93
|
readonly message: string;
|
|
@@ -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 {
|
|
@@ -16,6 +18,7 @@ import type { ConversationMessage } from '../types.js';
|
|
|
16
18
|
import type { TurnContext } from './context.js';
|
|
17
19
|
import { throwAgentTurnFailure } from './errors.js';
|
|
18
20
|
import { finalIteration } from './final-iteration.js';
|
|
21
|
+
import { replayTag } from './replay.js';
|
|
19
22
|
import { parseJsonAnswer } from './structured-output.js';
|
|
20
23
|
|
|
21
24
|
/**
|
|
@@ -77,6 +80,7 @@ export function buildEvaluateGuardrailsHandler(ctx: TurnContext): NodeHandler {
|
|
|
77
80
|
runId: kctx.runId,
|
|
78
81
|
tenantId: ctx.input.tenantId,
|
|
79
82
|
projectId: ctx.input.projectId,
|
|
83
|
+
...(ctx.input.orgId !== undefined && { orgId: ctx.input.orgId }),
|
|
80
84
|
conversationId: ctx.input.conversationId,
|
|
81
85
|
turnNumber: (ctx.conversation?.turnCount ?? 0) + 1,
|
|
82
86
|
agent: ctx.input.agent,
|
|
@@ -93,7 +97,9 @@ export function buildEvaluateGuardrailsHandler(ctx: TurnContext): NodeHandler {
|
|
|
93
97
|
ctx.bindings,
|
|
94
98
|
ctx.tenantPolicy,
|
|
95
99
|
ctx.turnAbort.signal,
|
|
100
|
+
judgeUsageSink(ctx, kctx),
|
|
96
101
|
);
|
|
102
|
+
throwIfJudgeCallsUnrecorded(outcomes);
|
|
97
103
|
const categorized = categorizeOutcomes(outcomes);
|
|
98
104
|
|
|
99
105
|
const allViolations = [...categorized.blocking, ...categorized.warnings, ...categorized.other];
|
|
@@ -151,3 +157,37 @@ export function buildEvaluateGuardrailsHandler(ctx: TurnContext): NodeHandler {
|
|
|
151
157
|
};
|
|
152
158
|
};
|
|
153
159
|
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The turn's usage sink for its llm-judge calls, adding what only the
|
|
163
|
+
* turn knows: the step that made them and the agent's version.
|
|
164
|
+
*/
|
|
165
|
+
function judgeUsageSink(ctx: TurnContext, kctx: NodeContext): UsageSink | undefined {
|
|
166
|
+
const sink = ctx.bindings.usage;
|
|
167
|
+
if (sink === undefined) return undefined;
|
|
168
|
+
return {
|
|
169
|
+
record: (call) =>
|
|
170
|
+
sink.record({
|
|
171
|
+
nodeId: kctx.nodeId as unknown as string,
|
|
172
|
+
agentVersion: ctx.input.agent.version,
|
|
173
|
+
...(ctx.input.replay !== undefined && { replay: replayTag(ctx.input.replay) }),
|
|
174
|
+
...call,
|
|
175
|
+
}),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* A judge call that answered but couldn't be recorded fails the step, as
|
|
181
|
+
* the turn's own model calls do: no answered call is left unrecorded.
|
|
182
|
+
*/
|
|
183
|
+
function throwIfJudgeCallsUnrecorded(outcomes: readonly EvaluationOutcome[]): void {
|
|
184
|
+
for (const outcome of outcomes) {
|
|
185
|
+
if (outcome.kind === 'err' && outcome.error.code === 'judge-usage-unrecorded') {
|
|
186
|
+
throwAgentTurnFailure({
|
|
187
|
+
code: 'persistence-error',
|
|
188
|
+
message: outcome.error.message,
|
|
189
|
+
cause: outcome.error,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|