@kindgi/agents 0.1.4 → 0.1.5-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/README.md +1 -1
- package/dist/blocks.d.ts +19 -0
- package/dist/blocks.d.ts.map +1 -1
- package/dist/blocks.js +59 -1
- package/dist/blocks.js.map +1 -1
- package/dist/conversation-binding.d.ts +49 -3
- package/dist/conversation-binding.d.ts.map +1 -1
- package/dist/define.d.ts +7 -1
- package/dist/define.d.ts.map +1 -1
- package/dist/define.js +132 -7
- package/dist/define.js.map +1 -1
- package/dist/drafted-template.d.ts +34 -0
- package/dist/drafted-template.d.ts.map +1 -0
- package/dist/drafted-template.js +95 -0
- package/dist/drafted-template.js.map +1 -0
- package/dist/guardrails-gate.d.ts +28 -13
- package/dist/guardrails-gate.d.ts.map +1 -1
- package/dist/guardrails-gate.js +59 -21
- package/dist/guardrails-gate.js.map +1 -1
- package/dist/handlers/build-initial-messages.d.ts +8 -2
- package/dist/handlers/build-initial-messages.d.ts.map +1 -1
- package/dist/handlers/build-initial-messages.js +23 -21
- package/dist/handlers/build-initial-messages.js.map +1 -1
- package/dist/handlers/compose-result.d.ts.map +1 -1
- package/dist/handlers/compose-result.js +2 -0
- package/dist/handlers/compose-result.js.map +1 -1
- package/dist/handlers/context.d.ts +6 -1
- 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 +17 -5
- package/dist/handlers/dispatch-tools.js.map +1 -1
- package/dist/handlers/errors.d.ts +14 -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 +6 -1
- package/dist/handlers/evaluate-guardrails.d.ts.map +1 -1
- package/dist/handlers/evaluate-guardrails.js +46 -4
- package/dist/handlers/evaluate-guardrails.js.map +1 -1
- package/dist/handlers/history.d.ts +24 -0
- package/dist/handlers/history.d.ts.map +1 -0
- package/dist/handlers/history.js +52 -0
- package/dist/handlers/history.js.map +1 -0
- package/dist/handlers/persist-final-message.d.ts.map +1 -1
- package/dist/handlers/persist-final-message.js +2 -0
- package/dist/handlers/persist-final-message.js.map +1 -1
- package/dist/handlers/persist-user-message.d.ts.map +1 -1
- package/dist/handlers/persist-user-message.js +2 -0
- package/dist/handlers/persist-user-message.js.map +1 -1
- package/dist/handlers/public-types.d.ts +12 -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 +7 -4
- package/dist/handlers/rehydrate.js.map +1 -1
- package/dist/handlers/remember-tool.d.ts +20 -0
- package/dist/handlers/remember-tool.d.ts.map +1 -0
- package/dist/handlers/remember-tool.js +149 -0
- package/dist/handlers/remember-tool.js.map +1 -0
- package/dist/handlers/replay.d.ts +55 -1
- package/dist/handlers/replay.d.ts.map +1 -1
- package/dist/handlers/replay.js +23 -6
- package/dist/handlers/replay.js.map +1 -1
- package/dist/handlers/resolve-blocks.d.ts.map +1 -1
- package/dist/handlers/resolve-blocks.js +19 -8
- package/dist/handlers/resolve-blocks.js.map +1 -1
- package/dist/handlers/result-shape.d.ts +3 -1
- 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 +2 -1
- package/dist/handlers/run-retrievals.d.ts.map +1 -1
- package/dist/handlers/run-retrievals.js +42 -16
- package/dist/handlers/run-retrievals.js.map +1 -1
- package/dist/handlers/turn-environment.d.ts.map +1 -1
- package/dist/handlers/turn-environment.js +2 -1
- package/dist/handlers/turn-environment.js.map +1 -1
- package/dist/handlers/turn-provenance.d.ts +19 -3
- package/dist/handlers/turn-provenance.d.ts.map +1 -1
- package/dist/handlers/turn-provenance.js +116 -2
- package/dist/handlers/turn-provenance.js.map +1 -1
- package/dist/index.d.ts +13 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/invoke.d.ts +1 -1
- package/dist/invoke.d.ts.map +1 -1
- package/dist/invoke.js +2 -0
- package/dist/invoke.js.map +1 -1
- package/dist/remember.d.ts +49 -0
- package/dist/remember.d.ts.map +1 -0
- package/dist/remember.js +89 -0
- package/dist/remember.js.map +1 -0
- package/dist/retrieval.d.ts +111 -28
- package/dist/retrieval.d.ts.map +1 -1
- package/dist/retrieval.js +432 -104
- package/dist/retrieval.js.map +1 -1
- package/dist/schema.d.ts +17 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +6 -0
- package/dist/schema.js.map +1 -1
- package/dist/streaming.d.ts +16 -1
- package/dist/streaming.d.ts.map +1 -1
- package/dist/streaming.js.map +1 -1
- package/dist/types.d.ts +131 -10
- package/dist/types.d.ts.map +1 -1
- package/migrations/0005_condemned_hellcat.sql +1 -0
- package/migrations/meta/0005_snapshot.json +333 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +15 -15
- package/src/blocks.ts +75 -1
- package/src/conversation-binding.ts +53 -3
- package/src/define.ts +144 -9
- package/src/drafted-template.ts +118 -0
- package/src/guardrails-gate.ts +90 -26
- package/src/handlers/build-initial-messages.ts +29 -22
- package/src/handlers/compose-result.ts +2 -0
- package/src/handlers/context.ts +12 -1
- package/src/handlers/dispatch-tools.ts +19 -5
- package/src/handlers/errors.ts +16 -1
- package/src/handlers/evaluate-guardrails.ts +47 -4
- package/src/handlers/history.ts +57 -0
- package/src/handlers/persist-final-message.ts +2 -0
- package/src/handlers/persist-user-message.ts +2 -0
- package/src/handlers/public-types.ts +18 -2
- package/src/handlers/rehydrate.ts +12 -8
- package/src/handlers/remember-tool.ts +200 -0
- package/src/handlers/replay.ts +80 -8
- package/src/handlers/resolve-blocks.ts +21 -7
- package/src/handlers/result-shape.ts +8 -1
- package/src/handlers/run-retrievals.ts +52 -19
- package/src/handlers/turn-environment.ts +2 -1
- package/src/handlers/turn-provenance.ts +133 -2
- package/src/index.ts +33 -2
- package/src/invoke.ts +3 -0
- package/src/remember.ts +135 -0
- package/src/retrieval.ts +587 -125
- package/src/schema.ts +6 -0
- package/src/streaming.ts +17 -0
- package/src/types.ts +129 -10
package/src/define.ts
CHANGED
|
@@ -9,18 +9,22 @@ import {
|
|
|
9
9
|
loadZodConverterSync,
|
|
10
10
|
toJSONSchemaSync,
|
|
11
11
|
} from '@kindgi/schema';
|
|
12
|
-
import { pickVersion } from '@kindgi/tools';
|
|
12
|
+
import { BUILT_IN_TOOL_PREFIX, pickVersion } from '@kindgi/tools';
|
|
13
13
|
import type { Result, Semver } from '@kindgi/types';
|
|
14
14
|
|
|
15
15
|
import type { InvalidAgentError } from './errors.js';
|
|
16
|
+
import { MAX_REMEMBER_DAYS, REMEMBER_TOOL_ID } from './remember.js';
|
|
16
17
|
import type {
|
|
17
18
|
Agent,
|
|
18
19
|
AgentId,
|
|
20
|
+
AgentMemoryPolicy,
|
|
19
21
|
AgentOutputSpec,
|
|
20
22
|
BlockRef,
|
|
21
23
|
ConversationPolicy,
|
|
22
24
|
PromptParameter,
|
|
23
25
|
PromptRef,
|
|
26
|
+
RememberPolicy,
|
|
27
|
+
RememberScope,
|
|
24
28
|
RetrievalIntent,
|
|
25
29
|
ToolRef,
|
|
26
30
|
TurnBudget,
|
|
@@ -54,6 +58,7 @@ export function defineAgent(spec: DefineAgentSpec): Result<Agent, InvalidAgentEr
|
|
|
54
58
|
...validateBlockRefs(spec),
|
|
55
59
|
...validateArrays(spec),
|
|
56
60
|
...validateRetrieval(spec),
|
|
61
|
+
...validateMemoryPolicy(spec),
|
|
57
62
|
...validatePromptParameters(spec.parameters),
|
|
58
63
|
...validateBudget(spec.budget),
|
|
59
64
|
...validateConversationPolicy(spec.conversationPolicy),
|
|
@@ -171,6 +176,12 @@ export interface DefineAgentSpec {
|
|
|
171
176
|
* conversation history + user message.
|
|
172
177
|
*/
|
|
173
178
|
readonly retrieval: readonly RetrievalIntent[];
|
|
179
|
+
/**
|
|
180
|
+
* How the agent uses what it retrieves: `instructionTypes` lists fact
|
|
181
|
+
* types whose verified facts are policies (in the system message).
|
|
182
|
+
* Absent: every retrieved fact is data.
|
|
183
|
+
*/
|
|
184
|
+
readonly memory?: AgentMemoryPolicy;
|
|
174
185
|
/**
|
|
175
186
|
* Guardrail IDs that guard this agent's turns. Each id must
|
|
176
187
|
* resolve among the guardrails bound for the run, or the turn fails
|
|
@@ -364,6 +375,11 @@ function validateToolRef(ref: unknown, i: number): Issue[] {
|
|
|
364
375
|
const obj = ref as Record<string, unknown>;
|
|
365
376
|
if (typeof obj.id !== 'string' || obj.id.trim().length === 0) {
|
|
366
377
|
out.push({ path: `/tools/${i}/id`, message: 'tool id must be a non-empty string' });
|
|
378
|
+
} else if (obj.id.startsWith(BUILT_IN_TOOL_PREFIX)) {
|
|
379
|
+
out.push({
|
|
380
|
+
path: `/tools/${i}/id`,
|
|
381
|
+
message: `"${obj.id}" is a tool built into Kindgi (the "${BUILT_IN_TOOL_PREFIX}" prefix): an agent gets it from its declaration (memory.remember for ${REMEMBER_TOOL_ID}), not from tools`,
|
|
382
|
+
});
|
|
367
383
|
}
|
|
368
384
|
if (typeof obj.version !== 'string' || obj.version.trim().length === 0) {
|
|
369
385
|
out.push({
|
|
@@ -386,28 +402,137 @@ function validateRetrieval(spec: DefineAgentSpec): Issue[] {
|
|
|
386
402
|
|
|
387
403
|
function validateIntent(intent: RetrievalIntent, i: number): Issue[] {
|
|
388
404
|
const out: Issue[] = [];
|
|
389
|
-
|
|
405
|
+
const source = intent.source ?? 'facts';
|
|
406
|
+
if (source !== 'facts' && source !== 'conversations') {
|
|
407
|
+
out.push({
|
|
408
|
+
path: `/retrieval/${i}/source`,
|
|
409
|
+
message: 'source must be facts or conversations (or absent: facts)',
|
|
410
|
+
});
|
|
411
|
+
return out;
|
|
412
|
+
}
|
|
413
|
+
if (source === 'facts' && (!Array.isArray(intent.types) || intent.types.length === 0)) {
|
|
390
414
|
out.push({
|
|
391
415
|
path: `/retrieval/${i}/types`,
|
|
392
|
-
message: 'each retrieval intent must declare at least one type',
|
|
416
|
+
message: 'each retrieval intent over facts must declare at least one type',
|
|
393
417
|
});
|
|
394
418
|
}
|
|
395
|
-
if (
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
419
|
+
if (source === 'conversations' && intent.types !== undefined) {
|
|
420
|
+
out.push({
|
|
421
|
+
path: `/retrieval/${i}/types`,
|
|
422
|
+
message: 'an intent over conversations has no types: it recalls messages',
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
if (intent.roles !== undefined) {
|
|
426
|
+
const roles = intent.roles as readonly unknown[];
|
|
427
|
+
if (source !== 'conversations') {
|
|
428
|
+
out.push({
|
|
429
|
+
path: `/retrieval/${i}/roles`,
|
|
430
|
+
message: 'roles are for an intent over conversations',
|
|
431
|
+
});
|
|
432
|
+
} else if (
|
|
433
|
+
!Array.isArray(roles) ||
|
|
434
|
+
roles.length === 0 ||
|
|
435
|
+
roles.some((r) => r !== 'user' && r !== 'agent')
|
|
436
|
+
) {
|
|
437
|
+
out.push({
|
|
438
|
+
path: `/retrieval/${i}/roles`,
|
|
439
|
+
message: "roles must list 'user', 'agent' or both (absent: 'user', the people's own words)",
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const scopes = source === 'facts' ? RETRIEVAL_SCOPES : RECALL_SCOPES;
|
|
444
|
+
if (!scopes.includes(intent.scope)) {
|
|
400
445
|
out.push({
|
|
401
446
|
path: `/retrieval/${i}/scope`,
|
|
402
|
-
message:
|
|
447
|
+
message: `scope for ${source} must be one of ${scopes.join(', ')}`,
|
|
403
448
|
});
|
|
404
449
|
}
|
|
405
450
|
if (intent.limit !== undefined && (!Number.isInteger(intent.limit) || intent.limit <= 0)) {
|
|
406
451
|
out.push({ path: `/retrieval/${i}/limit`, message: 'limit must be a positive integer' });
|
|
407
452
|
}
|
|
453
|
+
if (intent.mode !== undefined && !RETRIEVAL_MODES.includes(intent.mode)) {
|
|
454
|
+
out.push({
|
|
455
|
+
path: `/retrieval/${i}/mode`,
|
|
456
|
+
message: `mode must be one of ${RETRIEVAL_MODES.join(', ')} (or absent: the newest facts)`,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
return out;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const RETRIEVAL_SCOPES: readonly RetrievalIntent['scope'][] = [
|
|
463
|
+
'same-conversation',
|
|
464
|
+
'same-user',
|
|
465
|
+
'same-project',
|
|
466
|
+
'tenant',
|
|
467
|
+
];
|
|
468
|
+
const RECALL_SCOPES: readonly RetrievalIntent['scope'][] = [
|
|
469
|
+
'same-user',
|
|
470
|
+
'same-conversation',
|
|
471
|
+
'same-segment',
|
|
472
|
+
'same-project',
|
|
473
|
+
];
|
|
474
|
+
const RETRIEVAL_MODES: readonly NonNullable<RetrievalIntent['mode']>[] = [
|
|
475
|
+
'keyword',
|
|
476
|
+
'semantic',
|
|
477
|
+
'both',
|
|
478
|
+
];
|
|
479
|
+
|
|
480
|
+
const REMEMBER_SCOPES: readonly RememberScope[] = [
|
|
481
|
+
'same-user',
|
|
482
|
+
'same-conversation',
|
|
483
|
+
'same-project',
|
|
484
|
+
'tenant',
|
|
485
|
+
];
|
|
486
|
+
|
|
487
|
+
function validateMemoryPolicy(spec: DefineAgentSpec): Issue[] {
|
|
488
|
+
const memory = spec.memory;
|
|
489
|
+
if (memory === undefined) return [];
|
|
490
|
+
if (memory === null || typeof memory !== 'object' || Array.isArray(memory)) {
|
|
491
|
+
return [{ path: '/memory', message: 'memory must be an object' }];
|
|
492
|
+
}
|
|
493
|
+
const out: Issue[] = [];
|
|
494
|
+
const types = memory.instructionTypes;
|
|
495
|
+
if (types !== undefined && !isTypeList(types)) {
|
|
496
|
+
out.push({
|
|
497
|
+
path: '/memory/instructionTypes',
|
|
498
|
+
message: 'instructionTypes must be a list of fact type names',
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
if (memory.remember !== undefined) out.push(...validateRemember(memory.remember));
|
|
502
|
+
return out;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function validateRemember(remember: RememberPolicy): Issue[] {
|
|
506
|
+
if (remember === null || typeof remember !== 'object' || Array.isArray(remember)) {
|
|
507
|
+
return [{ path: '/memory/remember', message: 'remember must be an object' }];
|
|
508
|
+
}
|
|
509
|
+
const out: Issue[] = [];
|
|
510
|
+
if (!isTypeList(remember.types) || remember.types.length === 0) {
|
|
511
|
+
out.push({
|
|
512
|
+
path: '/memory/remember/types',
|
|
513
|
+
message: 'remember.types must list at least one fact type name',
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
if (!REMEMBER_SCOPES.includes(remember.scope)) {
|
|
517
|
+
out.push({
|
|
518
|
+
path: '/memory/remember/scope',
|
|
519
|
+
message: `remember.scope must be one of: ${REMEMBER_SCOPES.join(', ')}`,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
const days = remember.keepDays;
|
|
523
|
+
if (days !== undefined && (!Number.isInteger(days) || days < 1 || days > MAX_REMEMBER_DAYS)) {
|
|
524
|
+
out.push({
|
|
525
|
+
path: '/memory/remember/keepDays',
|
|
526
|
+
message: `remember.keepDays must be a whole number of days from 1 to ${MAX_REMEMBER_DAYS}`,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
408
529
|
return out;
|
|
409
530
|
}
|
|
410
531
|
|
|
532
|
+
function isTypeList(types: unknown): types is readonly string[] {
|
|
533
|
+
return Array.isArray(types) && types.every((t) => typeof t === 'string' && t.trim().length > 0);
|
|
534
|
+
}
|
|
535
|
+
|
|
411
536
|
const VALID_PARAM_TYPES = new Set(['string', 'number', 'boolean', 'date']);
|
|
412
537
|
const AUTO_INJECTED_NAMES = new Set(['today', 'now', 'agent', 'conversation', 'settings']);
|
|
413
538
|
|
|
@@ -649,6 +774,16 @@ function buildAgent(spec: DefineAgentSpec, output: AgentOutputSpec | undefined):
|
|
|
649
774
|
capabilities: spec.capabilities.map((c) => ({ ...c })),
|
|
650
775
|
tools: [...spec.tools],
|
|
651
776
|
retrieval: spec.retrieval.map((r) => ({ ...r })),
|
|
777
|
+
...(spec.memory !== undefined && {
|
|
778
|
+
memory: {
|
|
779
|
+
...(spec.memory.instructionTypes !== undefined && {
|
|
780
|
+
instructionTypes: [...spec.memory.instructionTypes],
|
|
781
|
+
}),
|
|
782
|
+
...(spec.memory.remember !== undefined && {
|
|
783
|
+
remember: { ...spec.memory.remember, types: [...spec.memory.remember.types] },
|
|
784
|
+
}),
|
|
785
|
+
},
|
|
786
|
+
}),
|
|
652
787
|
guardrails: [...spec.guardrails],
|
|
653
788
|
...(spec.parameters !== undefined && {
|
|
654
789
|
parameters: spec.parameters.map((p) => ({ ...p })),
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import { Liquid } from 'liquidjs';
|
|
5
|
+
|
|
6
|
+
import type { BlockIssue, PromptBlockContent } from './blocks.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* What a drafted prompt template (an improvement pass's candidate) is
|
|
10
|
+
* checked against: the template it would replace, and what the agent
|
|
11
|
+
* version already has.
|
|
12
|
+
*/
|
|
13
|
+
export interface DraftedTemplateContext {
|
|
14
|
+
/** The prompt block content the version pins: the template, and its parameters. */
|
|
15
|
+
readonly current: PromptBlockContent;
|
|
16
|
+
/** The settings blocks the version pins: `settings["<id>"]` may read these. */
|
|
17
|
+
readonly settingsBlocks: readonly string[];
|
|
18
|
+
/** The ids the agent already uses (its own, its tools', its blocks'): a template may name these. */
|
|
19
|
+
readonly knownIds: readonly string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The most a drafted template may be: twice the current one, at least 2,000 characters. */
|
|
23
|
+
export const DRAFTED_TEMPLATE_MAX = 20_000;
|
|
24
|
+
|
|
25
|
+
/** Variables any template may read: the turn's clock and identity. */
|
|
26
|
+
const HARMLESS_VARS: ReadonlySet<string> = new Set(['today', 'now', 'agent', 'conversation']);
|
|
27
|
+
|
|
28
|
+
/** A dotted id (`acme.export`): two or more segments, the first at least two characters. */
|
|
29
|
+
const DOTTED_ID = /\b[a-z][a-z0-9_-]+(?:\.[a-z0-9_-]+)+\b/gi;
|
|
30
|
+
|
|
31
|
+
const liquid = new Liquid();
|
|
32
|
+
|
|
33
|
+
function rootsOf(template: string): Array<readonly (string | number)[]> {
|
|
34
|
+
return liquid.globalVariableSegmentsSync(template).map((s) => s as readonly (string | number)[]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Whether a drafted template may replace the current one, as data: the
|
|
39
|
+
* problems, none when it may. Drafted templates come from a model fed
|
|
40
|
+
* judges' reasons and case data, so a template is held to what the agent
|
|
41
|
+
* already has, whatever the model was told:
|
|
42
|
+
*
|
|
43
|
+
* - it parses as Liquid;
|
|
44
|
+
* - the variables it reads are the declared parameters, the variables
|
|
45
|
+
* the current template reads, the turn's clock and identity, and
|
|
46
|
+
* `settings["<id>"]` of a settings block the version pins;
|
|
47
|
+
* - the dotted ids it names (`acme.export`, say) are the agent's own,
|
|
48
|
+
* its tools' and blocks', or ones the current template names: no new
|
|
49
|
+
* tool, agent or data reference;
|
|
50
|
+
* - it isn't empty or the current template, and at most twice as long
|
|
51
|
+
* as it (at least 2,000 characters; never over `DRAFTED_TEMPLATE_MAX`).
|
|
52
|
+
*/
|
|
53
|
+
export function checkDraftedTemplate(
|
|
54
|
+
candidate: string,
|
|
55
|
+
context: DraftedTemplateContext,
|
|
56
|
+
): BlockIssue[] {
|
|
57
|
+
const current = context.current.template;
|
|
58
|
+
const issues: BlockIssue[] = [];
|
|
59
|
+
if (candidate.trim() === '') return [{ path: '/template', message: 'the template is empty' }];
|
|
60
|
+
if (candidate === current) {
|
|
61
|
+
return [{ path: '/template', message: 'the template is the current one' }];
|
|
62
|
+
}
|
|
63
|
+
const max = Math.min(DRAFTED_TEMPLATE_MAX, Math.max(2 * current.length, 2000));
|
|
64
|
+
if (candidate.length > max) {
|
|
65
|
+
issues.push({
|
|
66
|
+
path: '/template',
|
|
67
|
+
message: `the template is ${candidate.length} characters; at most ${max} (twice the current one)`,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let read: Array<readonly (string | number)[]>;
|
|
72
|
+
try {
|
|
73
|
+
read = rootsOf(candidate);
|
|
74
|
+
} catch (cause) {
|
|
75
|
+
issues.push({
|
|
76
|
+
path: '/template',
|
|
77
|
+
message: `the template isn't valid Liquid: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
78
|
+
});
|
|
79
|
+
return issues;
|
|
80
|
+
}
|
|
81
|
+
const parameters = new Set((context.current.parameters ?? []).map((p) => p.name));
|
|
82
|
+
let currentRoots: Set<string>;
|
|
83
|
+
try {
|
|
84
|
+
currentRoots = new Set(rootsOf(current).map((s) => String(s[0])));
|
|
85
|
+
} catch {
|
|
86
|
+
currentRoots = new Set();
|
|
87
|
+
}
|
|
88
|
+
const settingsBlocks = new Set(context.settingsBlocks);
|
|
89
|
+
const named = new Set<string>();
|
|
90
|
+
for (const segments of read) {
|
|
91
|
+
const root = String(segments[0]);
|
|
92
|
+
if (root === 'settings') {
|
|
93
|
+
const block = segments[1];
|
|
94
|
+
if (block === undefined || !settingsBlocks.has(String(block))) {
|
|
95
|
+
named.add(`settings${block === undefined ? '' : `["${String(block)}"]`}`);
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (parameters.has(root) || currentRoots.has(root) || HARMLESS_VARS.has(root)) continue;
|
|
100
|
+
named.add(root);
|
|
101
|
+
}
|
|
102
|
+
for (const variable of named) {
|
|
103
|
+
issues.push({
|
|
104
|
+
path: '/template',
|
|
105
|
+
message: `it reads "${variable}", which the agent doesn't have (a declared parameter, a variable the current template reads, or a settings block the version pins)`,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const known = new Set([...context.knownIds, ...(current.match(DOTTED_ID) ?? [])]);
|
|
110
|
+
const newIds = new Set((candidate.match(DOTTED_ID) ?? []).filter((id) => !known.has(id)));
|
|
111
|
+
for (const id of newIds) {
|
|
112
|
+
issues.push({
|
|
113
|
+
path: '/template',
|
|
114
|
+
message: `it names "${id}", which the agent doesn't use (its tools, blocks, or what the current template names)`,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return issues;
|
|
118
|
+
}
|
package/src/guardrails-gate.ts
CHANGED
|
@@ -9,7 +9,9 @@ import {
|
|
|
9
9
|
type EvaluationOutcome,
|
|
10
10
|
type EvaluationResult,
|
|
11
11
|
type Guardrail,
|
|
12
|
+
type GuardrailSeverity,
|
|
12
13
|
type ModelCallRecord,
|
|
14
|
+
type OnViolation,
|
|
13
15
|
type RunTrace,
|
|
14
16
|
type ToolCallRecord,
|
|
15
17
|
type ToolResultRecord,
|
|
@@ -201,7 +203,20 @@ export async function evaluateGate(
|
|
|
201
203
|
abortSignal?: AbortSignal,
|
|
202
204
|
usage?: UsageSink,
|
|
203
205
|
): Promise<readonly EvaluationOutcome[]> {
|
|
204
|
-
if (guardrails.length === 0
|
|
206
|
+
if (guardrails.length === 0) return [];
|
|
207
|
+
// No check registry at all: every guardrail is one whose check can't run, never one that
|
|
208
|
+
// passed, so a `halt` guardrail fails the turn here too (it fails closed).
|
|
209
|
+
if (bindings.checks === undefined) {
|
|
210
|
+
return guardrails.map((g) => ({
|
|
211
|
+
kind: 'err' as const,
|
|
212
|
+
error: {
|
|
213
|
+
code: 'unknown-check' as const,
|
|
214
|
+
message: `guardrail "${g.id}": no check registry is bound, so its check "${g.check}" can't run`,
|
|
215
|
+
guardrailId: g.id,
|
|
216
|
+
checkId: g.check,
|
|
217
|
+
},
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
205
220
|
const evalBindings: EvaluationBindings = {
|
|
206
221
|
...(bindings.providerRegistry !== undefined && { providerRegistry: bindings.providerRegistry }),
|
|
207
222
|
...(bindings.compliance !== undefined && { compliance: bindings.compliance }),
|
|
@@ -212,6 +227,20 @@ export async function evaluateGate(
|
|
|
212
227
|
return await evaluateAll(guardrails, bindings.checks, trace, evalBindings);
|
|
213
228
|
}
|
|
214
229
|
|
|
230
|
+
/**
|
|
231
|
+
* A guardrail whose check couldn't run (no such check, a bad configuration, a judge that couldn't
|
|
232
|
+
* be routed), with what the guardrail would have done. Never counted as a violation.
|
|
233
|
+
*/
|
|
234
|
+
export interface GuardrailEvaluationError {
|
|
235
|
+
readonly guardrailId: string;
|
|
236
|
+
readonly message: string;
|
|
237
|
+
/** The engine's error code: `unknown-check`, `invalid-check-config`, `judge-routing-failed`, … */
|
|
238
|
+
readonly code: string;
|
|
239
|
+
/** The guardrail's `on-violation` action and severity; absent when the guardrail is unknown. */
|
|
240
|
+
readonly action?: OnViolation;
|
|
241
|
+
readonly severity?: GuardrailSeverity;
|
|
242
|
+
}
|
|
243
|
+
|
|
215
244
|
/**
|
|
216
245
|
* Sort evaluation outcomes by the failed guardrail's action. `halt` is
|
|
217
246
|
* blocking — the turn fails with `guardrail-violation`. `log-only` and
|
|
@@ -220,38 +249,58 @@ export async function evaluateGate(
|
|
|
220
249
|
* `other`. Warnings and `other` are attached to the successful turn
|
|
221
250
|
* result under `result.violations`; the turn does not carry out those
|
|
222
251
|
* actions. Evaluation errors (a check that could not run) are collected
|
|
223
|
-
* in `errors
|
|
252
|
+
* in `errors`; those of a `halt` guardrail are also in `blockingErrors`,
|
|
253
|
+
* which fail the turn as a violation would (a guardrail that can't check
|
|
254
|
+
* fails closed). `guardrails` is the list the outcomes came from, one
|
|
255
|
+
* outcome per guardrail in order (`evaluateGate`), which says each error's
|
|
256
|
+
* action; without it, an error's guardrail is looked up by id.
|
|
224
257
|
*/
|
|
225
|
-
export function categorizeOutcomes(
|
|
258
|
+
export function categorizeOutcomes(
|
|
259
|
+
outcomes: readonly EvaluationOutcome[],
|
|
260
|
+
guardrails: readonly Guardrail[] = [],
|
|
261
|
+
): {
|
|
226
262
|
readonly blocking: readonly EvaluationResult[];
|
|
227
263
|
readonly warnings: readonly EvaluationResult[];
|
|
228
264
|
readonly other: readonly EvaluationResult[];
|
|
229
|
-
readonly errors: readonly
|
|
265
|
+
readonly errors: readonly GuardrailEvaluationError[];
|
|
266
|
+
readonly blockingErrors: readonly GuardrailEvaluationError[];
|
|
230
267
|
} {
|
|
231
268
|
const blocking: EvaluationResult[] = [];
|
|
232
269
|
const warnings: EvaluationResult[] = [];
|
|
233
270
|
const other: EvaluationResult[] = [];
|
|
234
|
-
const errors:
|
|
235
|
-
|
|
271
|
+
const errors: GuardrailEvaluationError[] = [];
|
|
272
|
+
const blockingErrors: GuardrailEvaluationError[] = [];
|
|
273
|
+
const byId = new Map(guardrails.map((g) => [g.id as string, g]));
|
|
274
|
+
const inOrder = guardrails.length === outcomes.length;
|
|
275
|
+
outcomes.forEach((outcome, i) => {
|
|
236
276
|
if (outcome.kind === 'err') {
|
|
237
|
-
|
|
238
|
-
guardrailId
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
277
|
+
const named =
|
|
278
|
+
'guardrailId' in outcome.error && typeof outcome.error.guardrailId === 'string'
|
|
279
|
+
? outcome.error.guardrailId
|
|
280
|
+
: undefined;
|
|
281
|
+
const guardrail = inOrder ? guardrails[i] : named !== undefined ? byId.get(named) : undefined;
|
|
282
|
+
const error: GuardrailEvaluationError = {
|
|
283
|
+
guardrailId: guardrail?.id ?? named ?? '<unknown>',
|
|
242
284
|
message: outcome.error.message,
|
|
243
|
-
|
|
244
|
-
|
|
285
|
+
code: outcome.error.code,
|
|
286
|
+
...(guardrail !== undefined && {
|
|
287
|
+
action: guardrail.action['on-violation'],
|
|
288
|
+
severity: guardrail.severity ?? 'error',
|
|
289
|
+
}),
|
|
290
|
+
};
|
|
291
|
+
errors.push(error);
|
|
292
|
+
if (error.action === 'halt') blockingErrors.push(error);
|
|
293
|
+
return;
|
|
245
294
|
}
|
|
246
|
-
if (outcome.kind === 'skip')
|
|
295
|
+
if (outcome.kind === 'skip') return;
|
|
247
296
|
const evalResult = outcome.value;
|
|
248
|
-
if (evalResult.result.passed)
|
|
297
|
+
if (evalResult.result.passed) return;
|
|
249
298
|
if (evalResult.action === 'halt') blocking.push(evalResult);
|
|
250
299
|
else if (evalResult.action === 'log-only' || evalResult.action === 'noop') {
|
|
251
300
|
warnings.push(evalResult);
|
|
252
301
|
} else other.push(evalResult);
|
|
253
|
-
}
|
|
254
|
-
return { blocking, warnings, other, errors };
|
|
302
|
+
});
|
|
303
|
+
return { blocking, warnings, other, errors, blockingErrors };
|
|
255
304
|
}
|
|
256
305
|
|
|
257
306
|
/**
|
|
@@ -261,21 +310,33 @@ export function categorizeOutcomes(outcomes: readonly EvaluationOutcome[]): {
|
|
|
261
310
|
* Turn blocked by guardrail 'no-pii': Response contains an email address
|
|
262
311
|
* Turn blocked by 2 guardrails: 'no-pii' (Response contains …); 'max-length'
|
|
263
312
|
*/
|
|
264
|
-
export function describeBlockingViolations(
|
|
313
|
+
export function describeBlockingViolations(
|
|
314
|
+
blocking: readonly EvaluationResult[],
|
|
315
|
+
blockingErrors: readonly GuardrailEvaluationError[] = [],
|
|
316
|
+
): string {
|
|
265
317
|
const reasonOf = (v: EvaluationResult): string | undefined => {
|
|
266
318
|
const reason = v.result.reason?.trim();
|
|
267
319
|
return reason === undefined || reason === '' ? undefined : reason;
|
|
268
320
|
};
|
|
321
|
+
const couldNotRun = (e: GuardrailEvaluationError) =>
|
|
322
|
+
`'${e.guardrailId}' couldn't run its check (${e.code}: ${e.message})`;
|
|
269
323
|
const [only] = blocking;
|
|
270
|
-
|
|
324
|
+
const [onlyError] = blockingErrors;
|
|
325
|
+
if (blocking.length === 1 && only !== undefined && blockingErrors.length === 0) {
|
|
271
326
|
const reason = reasonOf(only);
|
|
272
327
|
return `Turn blocked by guardrail '${only.guardrailId}'${reason !== undefined ? `: ${reason}` : ''}`;
|
|
273
328
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
329
|
+
if (blocking.length === 0 && blockingErrors.length === 1 && onlyError !== undefined) {
|
|
330
|
+
return `Turn blocked: guardrail ${couldNotRun(onlyError)}`;
|
|
331
|
+
}
|
|
332
|
+
const named = [
|
|
333
|
+
...blocking.map((v) => {
|
|
334
|
+
const reason = reasonOf(v);
|
|
335
|
+
return `'${v.guardrailId}'${reason !== undefined ? ` (${reason})` : ''}`;
|
|
336
|
+
}),
|
|
337
|
+
...blockingErrors.map(couldNotRun),
|
|
338
|
+
];
|
|
339
|
+
return `Turn blocked by ${named.length} guardrails: ${named.join('; ')}`;
|
|
279
340
|
}
|
|
280
341
|
|
|
281
342
|
/**
|
|
@@ -324,8 +385,11 @@ export interface GuardrailViolationError {
|
|
|
324
385
|
readonly code: 'guardrail-violation';
|
|
325
386
|
readonly message: string;
|
|
326
387
|
readonly violations: readonly EvaluationResult[];
|
|
327
|
-
/**
|
|
328
|
-
|
|
388
|
+
/**
|
|
389
|
+
* Guardrails whose check couldn't even evaluate (missing check, bad config). A `halt`
|
|
390
|
+
* guardrail's error blocks the turn on its own, so `violations` can be empty.
|
|
391
|
+
*/
|
|
392
|
+
readonly evaluationErrors: readonly GuardrailEvaluationError[];
|
|
329
393
|
}
|
|
330
394
|
|
|
331
395
|
/**
|
|
@@ -4,21 +4,33 @@
|
|
|
4
4
|
import type { ModelMessage, ModelToolCall } from '@kindgi/capabilities';
|
|
5
5
|
import type { NodeHandler } from '@kindgi/handler';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
MEMORY_DATA_RULE,
|
|
9
|
+
formatPoliciesForPrompt,
|
|
10
|
+
formatRetrievedForPrompt,
|
|
11
|
+
isPolicyFact,
|
|
12
|
+
} from '../retrieval.js';
|
|
8
13
|
import type { ConversationMessage } from '../types.js';
|
|
9
14
|
|
|
10
15
|
import type { TurnContext } from './context.js';
|
|
11
16
|
import { throwAgentTurnFailure } from './errors.js';
|
|
17
|
+
import { readHistory } from './history.js';
|
|
12
18
|
|
|
13
19
|
/**
|
|
14
20
|
* Compose the initial `modelMessages` array the loop's first iteration
|
|
15
21
|
* feeds to the model:
|
|
16
22
|
*
|
|
17
|
-
* [ system: rendered prompt
|
|
18
|
-
*
|
|
23
|
+
* [ system: rendered prompt
|
|
24
|
+
* (+ the memory rule, + "Policies (verified)", when there are),
|
|
19
25
|
* ...history,
|
|
26
|
+
* user: <memory> data block (if anything was retrieved),
|
|
20
27
|
* user: current message ]
|
|
21
28
|
*
|
|
29
|
+
* Retrieved facts are data: a labelled block in a user-role message, read
|
|
30
|
+
* as information about the world, never as instructions. Only a verified
|
|
31
|
+
* fact of a type the agent lists in `memory.instructionTypes` is an
|
|
32
|
+
* instruction, in the system message.
|
|
33
|
+
*
|
|
22
34
|
* Output shape: `{ nextMessages: ModelMessage[] }` — matches the loop
|
|
23
35
|
* iteration output's `nextMessages` field so the loop body can treat
|
|
24
36
|
* iteration 0's input identically to subsequent iterations'.
|
|
@@ -37,24 +49,14 @@ export function buildBuildInitialMessagesHandler(ctx: TurnContext): NodeHandler
|
|
|
37
49
|
// the closure by render-prompt through the local field `renderedPrompt`).
|
|
38
50
|
void input;
|
|
39
51
|
|
|
40
|
-
const
|
|
41
|
-
const messages = await ctx.bindings.conversationBinding.readMessages({
|
|
42
|
-
tenantId: ctx.input.tenantId,
|
|
43
|
-
conversationId: ctx.input.conversationId,
|
|
44
|
-
});
|
|
45
|
-
if (messages.kind === 'err') throwAgentTurnFailure(messages.error);
|
|
46
|
-
// The user message just appended is included in the history read;
|
|
47
|
-
// drop it because the composer adds it explicitly as the last
|
|
48
|
-
// element.
|
|
49
|
-
const historyRaw = messages.value.filter((m) => m.sequence !== ctx.userMessage?.sequence);
|
|
50
|
-
const history =
|
|
51
|
-
historyLimit === undefined
|
|
52
|
-
? historyRaw
|
|
53
|
-
: historyRaw.slice(Math.max(0, historyRaw.length - historyLimit));
|
|
52
|
+
const history = await readHistory(ctx);
|
|
54
53
|
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
|
|
54
|
+
const agent = ctx.input.agent;
|
|
55
|
+
const policies = ctx.retrieved.filter((r) => isPolicyFact(agent, r));
|
|
56
|
+
const data = ctx.retrieved.filter((r) => !isPolicyFact(agent, r));
|
|
57
|
+
const memoryBlock = formatRetrievedForPrompt(data, ctx.recalled ?? []);
|
|
58
|
+
const memoryMessage: ModelMessage | undefined =
|
|
59
|
+
memoryBlock.length > 0 ? { role: 'user', content: memoryBlock } : undefined;
|
|
58
60
|
|
|
59
61
|
const rendered = ctx.renderedPrompt;
|
|
60
62
|
if (rendered === undefined) {
|
|
@@ -65,10 +67,15 @@ export function buildBuildInitialMessagesHandler(ctx: TurnContext): NodeHandler
|
|
|
65
67
|
});
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
const system = [
|
|
71
|
+
rendered,
|
|
72
|
+
...(memoryMessage !== undefined ? [MEMORY_DATA_RULE] : []),
|
|
73
|
+
...(policies.length > 0 ? [formatPoliciesForPrompt(policies)] : []),
|
|
74
|
+
].join('\n\n');
|
|
68
75
|
const modelMessages: ModelMessage[] = [
|
|
69
|
-
{ role: 'system', content:
|
|
70
|
-
...(contextMessage !== undefined ? [contextMessage] : []),
|
|
76
|
+
{ role: 'system', content: system },
|
|
71
77
|
...history.map(conversationToModelMessage),
|
|
78
|
+
...(memoryMessage !== undefined ? [memoryMessage] : []),
|
|
72
79
|
{ role: 'user', content: ctx.input.userMessage },
|
|
73
80
|
];
|
|
74
81
|
|
|
@@ -72,6 +72,7 @@ export function buildComposeResultHandler(ctx: TurnContext): NodeHandler {
|
|
|
72
72
|
appended: ctx.appended,
|
|
73
73
|
response: ctx.finalMessage,
|
|
74
74
|
retrieved: ctx.retrieved ?? [],
|
|
75
|
+
...(ctx.recalled !== undefined && ctx.recalled.length > 0 && { recalled: ctx.recalled }),
|
|
75
76
|
violations: ctx.nonBlockingViolations ?? [],
|
|
76
77
|
usage,
|
|
77
78
|
provider,
|
|
@@ -90,6 +91,7 @@ export function buildComposeResultHandler(ctx: TurnContext): NodeHandler {
|
|
|
90
91
|
turnNumber: ctx.conversation.turnCount + 1,
|
|
91
92
|
response: ctx.finalMessage,
|
|
92
93
|
retrieved: ctx.retrieved ?? [],
|
|
94
|
+
...(ctx.recalled !== undefined && ctx.recalled.length > 0 && { recalled: ctx.recalled }),
|
|
93
95
|
durationMs,
|
|
94
96
|
totalCostUsd: ctx.usage.totalCostUsd,
|
|
95
97
|
});
|
package/src/handlers/context.ts
CHANGED
|
@@ -16,7 +16,13 @@ import type { EvaluationResult } from '@kindgi/guardrails';
|
|
|
16
16
|
|
|
17
17
|
import type { EffectiveHitlPolicy } from '../hitl-policy.js';
|
|
18
18
|
import type { ProvenanceBindings } from '../provenance-emit.js';
|
|
19
|
-
import type {
|
|
19
|
+
import type {
|
|
20
|
+
Agent,
|
|
21
|
+
Conversation,
|
|
22
|
+
ConversationMessage,
|
|
23
|
+
RecalledMemory,
|
|
24
|
+
RetrievedFact,
|
|
25
|
+
} from '../types.js';
|
|
20
26
|
|
|
21
27
|
import type { HitlBindings, InvokeAgentBindings, InvokeAgentInput } from './public-types.js';
|
|
22
28
|
import type { TurnBlocks } from './resolve-blocks.js';
|
|
@@ -103,6 +109,11 @@ export interface TurnContext {
|
|
|
103
109
|
* Populated by `run-retrievals` — facts pulled by declared intents.
|
|
104
110
|
*/
|
|
105
111
|
retrieved?: readonly RetrievedFact[];
|
|
112
|
+
/**
|
|
113
|
+
* Populated by `run-retrievals` — messages of earlier conversations
|
|
114
|
+
* recalled by intents over conversations.
|
|
115
|
+
*/
|
|
116
|
+
recalled?: readonly RecalledMemory[];
|
|
106
117
|
/**
|
|
107
118
|
* Populated by `persist-final-message` — the terminal assistant
|
|
108
119
|
* message. `compose-result` reads it back for the returned
|