@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.
Files changed (137) hide show
  1. package/README.md +1 -1
  2. package/dist/blocks.d.ts +19 -0
  3. package/dist/blocks.d.ts.map +1 -1
  4. package/dist/blocks.js +59 -1
  5. package/dist/blocks.js.map +1 -1
  6. package/dist/conversation-binding.d.ts +49 -3
  7. package/dist/conversation-binding.d.ts.map +1 -1
  8. package/dist/define.d.ts +7 -1
  9. package/dist/define.d.ts.map +1 -1
  10. package/dist/define.js +132 -7
  11. package/dist/define.js.map +1 -1
  12. package/dist/drafted-template.d.ts +34 -0
  13. package/dist/drafted-template.d.ts.map +1 -0
  14. package/dist/drafted-template.js +95 -0
  15. package/dist/drafted-template.js.map +1 -0
  16. package/dist/guardrails-gate.d.ts +28 -13
  17. package/dist/guardrails-gate.d.ts.map +1 -1
  18. package/dist/guardrails-gate.js +59 -21
  19. package/dist/guardrails-gate.js.map +1 -1
  20. package/dist/handlers/build-initial-messages.d.ts +8 -2
  21. package/dist/handlers/build-initial-messages.d.ts.map +1 -1
  22. package/dist/handlers/build-initial-messages.js +23 -21
  23. package/dist/handlers/build-initial-messages.js.map +1 -1
  24. package/dist/handlers/compose-result.d.ts.map +1 -1
  25. package/dist/handlers/compose-result.js +2 -0
  26. package/dist/handlers/compose-result.js.map +1 -1
  27. package/dist/handlers/context.d.ts +6 -1
  28. package/dist/handlers/context.d.ts.map +1 -1
  29. package/dist/handlers/dispatch-tools.d.ts.map +1 -1
  30. package/dist/handlers/dispatch-tools.js +17 -5
  31. package/dist/handlers/dispatch-tools.js.map +1 -1
  32. package/dist/handlers/errors.d.ts +14 -1
  33. package/dist/handlers/errors.d.ts.map +1 -1
  34. package/dist/handlers/errors.js.map +1 -1
  35. package/dist/handlers/evaluate-guardrails.d.ts +6 -1
  36. package/dist/handlers/evaluate-guardrails.d.ts.map +1 -1
  37. package/dist/handlers/evaluate-guardrails.js +46 -4
  38. package/dist/handlers/evaluate-guardrails.js.map +1 -1
  39. package/dist/handlers/history.d.ts +24 -0
  40. package/dist/handlers/history.d.ts.map +1 -0
  41. package/dist/handlers/history.js +52 -0
  42. package/dist/handlers/history.js.map +1 -0
  43. package/dist/handlers/persist-final-message.d.ts.map +1 -1
  44. package/dist/handlers/persist-final-message.js +2 -0
  45. package/dist/handlers/persist-final-message.js.map +1 -1
  46. package/dist/handlers/persist-user-message.d.ts.map +1 -1
  47. package/dist/handlers/persist-user-message.js +2 -0
  48. package/dist/handlers/persist-user-message.js.map +1 -1
  49. package/dist/handlers/public-types.d.ts +12 -2
  50. package/dist/handlers/public-types.d.ts.map +1 -1
  51. package/dist/handlers/rehydrate.d.ts.map +1 -1
  52. package/dist/handlers/rehydrate.js +7 -4
  53. package/dist/handlers/rehydrate.js.map +1 -1
  54. package/dist/handlers/remember-tool.d.ts +20 -0
  55. package/dist/handlers/remember-tool.d.ts.map +1 -0
  56. package/dist/handlers/remember-tool.js +149 -0
  57. package/dist/handlers/remember-tool.js.map +1 -0
  58. package/dist/handlers/replay.d.ts +55 -1
  59. package/dist/handlers/replay.d.ts.map +1 -1
  60. package/dist/handlers/replay.js +23 -6
  61. package/dist/handlers/replay.js.map +1 -1
  62. package/dist/handlers/resolve-blocks.d.ts.map +1 -1
  63. package/dist/handlers/resolve-blocks.js +19 -8
  64. package/dist/handlers/resolve-blocks.js.map +1 -1
  65. package/dist/handlers/result-shape.d.ts +3 -1
  66. package/dist/handlers/result-shape.d.ts.map +1 -1
  67. package/dist/handlers/result-shape.js.map +1 -1
  68. package/dist/handlers/run-retrievals.d.ts +2 -1
  69. package/dist/handlers/run-retrievals.d.ts.map +1 -1
  70. package/dist/handlers/run-retrievals.js +42 -16
  71. package/dist/handlers/run-retrievals.js.map +1 -1
  72. package/dist/handlers/turn-environment.d.ts.map +1 -1
  73. package/dist/handlers/turn-environment.js +2 -1
  74. package/dist/handlers/turn-environment.js.map +1 -1
  75. package/dist/handlers/turn-provenance.d.ts +19 -3
  76. package/dist/handlers/turn-provenance.d.ts.map +1 -1
  77. package/dist/handlers/turn-provenance.js +116 -2
  78. package/dist/handlers/turn-provenance.js.map +1 -1
  79. package/dist/index.d.ts +13 -8
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +5 -3
  82. package/dist/index.js.map +1 -1
  83. package/dist/invoke.d.ts +1 -1
  84. package/dist/invoke.d.ts.map +1 -1
  85. package/dist/invoke.js +2 -0
  86. package/dist/invoke.js.map +1 -1
  87. package/dist/remember.d.ts +49 -0
  88. package/dist/remember.d.ts.map +1 -0
  89. package/dist/remember.js +89 -0
  90. package/dist/remember.js.map +1 -0
  91. package/dist/retrieval.d.ts +111 -28
  92. package/dist/retrieval.d.ts.map +1 -1
  93. package/dist/retrieval.js +432 -104
  94. package/dist/retrieval.js.map +1 -1
  95. package/dist/schema.d.ts +17 -0
  96. package/dist/schema.d.ts.map +1 -1
  97. package/dist/schema.js +6 -0
  98. package/dist/schema.js.map +1 -1
  99. package/dist/streaming.d.ts +16 -1
  100. package/dist/streaming.d.ts.map +1 -1
  101. package/dist/streaming.js.map +1 -1
  102. package/dist/types.d.ts +131 -10
  103. package/dist/types.d.ts.map +1 -1
  104. package/migrations/0005_condemned_hellcat.sql +1 -0
  105. package/migrations/meta/0005_snapshot.json +333 -0
  106. package/migrations/meta/_journal.json +7 -0
  107. package/package.json +15 -15
  108. package/src/blocks.ts +75 -1
  109. package/src/conversation-binding.ts +53 -3
  110. package/src/define.ts +144 -9
  111. package/src/drafted-template.ts +118 -0
  112. package/src/guardrails-gate.ts +90 -26
  113. package/src/handlers/build-initial-messages.ts +29 -22
  114. package/src/handlers/compose-result.ts +2 -0
  115. package/src/handlers/context.ts +12 -1
  116. package/src/handlers/dispatch-tools.ts +19 -5
  117. package/src/handlers/errors.ts +16 -1
  118. package/src/handlers/evaluate-guardrails.ts +47 -4
  119. package/src/handlers/history.ts +57 -0
  120. package/src/handlers/persist-final-message.ts +2 -0
  121. package/src/handlers/persist-user-message.ts +2 -0
  122. package/src/handlers/public-types.ts +18 -2
  123. package/src/handlers/rehydrate.ts +12 -8
  124. package/src/handlers/remember-tool.ts +200 -0
  125. package/src/handlers/replay.ts +80 -8
  126. package/src/handlers/resolve-blocks.ts +21 -7
  127. package/src/handlers/result-shape.ts +8 -1
  128. package/src/handlers/run-retrievals.ts +52 -19
  129. package/src/handlers/turn-environment.ts +2 -1
  130. package/src/handlers/turn-provenance.ts +133 -2
  131. package/src/index.ts +33 -2
  132. package/src/invoke.ts +3 -0
  133. package/src/remember.ts +135 -0
  134. package/src/retrieval.ts +587 -125
  135. package/src/schema.ts +6 -0
  136. package/src/streaming.ts +17 -0
  137. 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
- if (!Array.isArray(intent.types) || intent.types.length === 0) {
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
- intent.scope !== 'same-conversation' &&
397
- intent.scope !== 'same-project' &&
398
- intent.scope !== 'tenant'
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: 'scope must be same-conversation, same-project, or tenant',
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
+ }
@@ -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 || bindings.checks === undefined) return [];
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(outcomes: readonly EvaluationOutcome[]): {
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 { readonly guardrailId: string; readonly message: string }[];
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: { guardrailId: string; message: string }[] = [];
235
- for (const outcome of outcomes) {
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
- errors.push({
238
- guardrailId:
239
- 'guardrailId' in outcome.error && typeof outcome.error.guardrailId === 'string'
240
- ? outcome.error.guardrailId
241
- : '<unknown>',
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
- continue;
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') continue;
295
+ if (outcome.kind === 'skip') return;
247
296
  const evalResult = outcome.value;
248
- if (evalResult.result.passed) continue;
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(blocking: readonly EvaluationResult[]): string {
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
- if (blocking.length === 1 && only !== undefined) {
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
- const named = blocking.map((v) => {
275
- const reason = reasonOf(v);
276
- return `'${v.guardrailId}'${reason !== undefined ? ` (${reason})` : ''}`;
277
- });
278
- return `Turn blocked by ${blocking.length} guardrails: ${named.join('; ')}`;
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
- /** Errors from checks that couldn't even evaluate (missing check, bad config). */
328
- readonly evaluationErrors: readonly { readonly guardrailId: string; readonly message: string }[];
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 { formatRetrievedForPrompt } from '../retrieval.js';
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
- * system: retrieved context (if any),
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 historyLimit = ctx.input.agent.conversationPolicy?.historyLimit;
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 contextBlock = formatRetrievedForPrompt(ctx.retrieved);
56
- const contextMessage: ModelMessage | undefined =
57
- contextBlock.length > 0 ? { role: 'system', content: contextBlock } : undefined;
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: rendered },
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
  });
@@ -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 { Agent, Conversation, ConversationMessage, RetrievedFact } from '../types.js';
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